From bba480975b7136da2f4016b2ba6133e68aa2a878 Mon Sep 17 00:00:00 2001 From: Conor Branagan Date: Thu, 10 Nov 2016 10:27:20 -0500 Subject: [PATCH 1/3] Add Go API for virtual tables See https://www.sqlite.org/vtab.html for more details. This work was started from https://github.com/gwenn/gosqlite/blob/master/vtab.{c,go} and adds: - Porting the API to go-sqlite3 APIs. - Support for >= Go 1.6 without requiring the `cgocheck` flag to be changed. - Filling out the unfinished callback functions for the `Vtable` struct. - A simple `Context` API layer for ease of use when adding modules. Tests are included. --- context.go | 103 ++++++++++++++ sqlite3.go | 2 +- vtable.c | 182 +++++++++++++++++++++++ vtable.go | 380 +++++++++++++++++++++++++++++++++++++++++++++++++ vtable_test.go | 132 +++++++++++++++++ 5 files changed, 798 insertions(+), 1 deletion(-) create mode 100644 context.go create mode 100644 vtable.c create mode 100644 vtable.go create mode 100644 vtable_test.go diff --git a/context.go b/context.go new file mode 100644 index 0000000..ba943da --- /dev/null +++ b/context.go @@ -0,0 +1,103 @@ +// Copyright (C) 2014 Yasuhiro Matsumoto . +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file. + +package sqlite3 + +/* + +#ifndef USE_LIBSQLITE3 +#include +#else +#include +#endif +#include +// These wrappers are necessary because SQLITE_TRANSIENT +// is a pointer constant, and cgo doesn't translate them correctly. + +static inline void my_result_text(sqlite3_context *ctx, char *p, int np) { + sqlite3_result_text(ctx, p, np, SQLITE_TRANSIENT); +} + +static inline void my_result_blob(sqlite3_context *ctx, void *p, int np) { + sqlite3_result_blob(ctx, p, np, SQLITE_TRANSIENT); +} +*/ +import "C" + +import ( + "math" + "reflect" + "unsafe" +) + +const i64 = unsafe.Sizeof(int(0)) > 4 + +type ZeroBlobLength int32 +type Context C.sqlite3_context + +// ResultBool sets the result of an SQL function. +func (c *Context) ResultBool(b bool) { + if b { + c.ResultInt(1) + } else { + c.ResultInt(0) + } +} + +// ResultBlob sets the result of an SQL function. +// See: sqlite3_result_blob, http://sqlite.org/c3ref/result_blob.html +func (c *Context) ResultBlob(b []byte) { + if i64 && len(b) > math.MaxInt32 { + C.sqlite3_result_error_toobig((*C.sqlite3_context)(c)) + return + } + var p *byte + if len(b) > 0 { + p = &b[0] + } + C.my_result_blob((*C.sqlite3_context)(c), unsafe.Pointer(p), C.int(len(b))) +} + +// ResultDouble sets the result of an SQL function. +// See: sqlite3_result_double, http://sqlite.org/c3ref/result_blob.html +func (c *Context) ResultDouble(d float64) { + C.sqlite3_result_double((*C.sqlite3_context)(c), C.double(d)) +} + +// ResultInt sets the result of an SQL function. +// See: sqlite3_result_int, http://sqlite.org/c3ref/result_blob.html +func (c *Context) ResultInt(i int) { + if i64 && (i > math.MaxInt32 || i < math.MinInt32) { + C.sqlite3_result_int64((*C.sqlite3_context)(c), C.sqlite3_int64(i)) + } else { + C.sqlite3_result_int((*C.sqlite3_context)(c), C.int(i)) + } +} + +// ResultInt64 sets the result of an SQL function. +// See: sqlite3_result_int64, http://sqlite.org/c3ref/result_blob.html +func (c *Context) ResultInt64(i int64) { + C.sqlite3_result_int64((*C.sqlite3_context)(c), C.sqlite3_int64(i)) +} + +// ResultNull sets the result of an SQL function. +// See: sqlite3_result_null, http://sqlite.org/c3ref/result_blob.html +func (c *Context) ResultNull() { + C.sqlite3_result_null((*C.sqlite3_context)(c)) +} + +// ResultText sets the result of an SQL function. +// See: sqlite3_result_text, http://sqlite.org/c3ref/result_blob.html +func (c *Context) ResultText(s string) { + h := (*reflect.StringHeader)(unsafe.Pointer(&s)) + cs, l := (*C.char)(unsafe.Pointer(h.Data)), C.int(h.Len) + C.my_result_text((*C.sqlite3_context)(c), cs, l) +} + +// ResultZeroblob sets the result of an SQL function. +// See: sqlite3_result_zeroblob, http://sqlite.org/c3ref/result_blob.html +func (c *Context) ResultZeroblob(n ZeroBlobLength) { + C.sqlite3_result_zeroblob((*C.sqlite3_context)(c), C.int(n)) +} diff --git a/sqlite3.go b/sqlite3.go index 6cd5c0e..c7eaf4d 100644 --- a/sqlite3.go +++ b/sqlite3.go @@ -627,11 +627,11 @@ func (d *SQLiteDriver) Open(dsn string) (driver.Conn, error) { // Close the connection. func (c *SQLiteConn) Close() error { - deleteHandles(c) rv := C.sqlite3_close_v2(c.db) if rv != C.SQLITE_OK { return c.lastError() } + deleteHandles(c) c.db = nil runtime.SetFinalizer(c, nil) return nil diff --git a/vtable.c b/vtable.c new file mode 100644 index 0000000..e1d7575 --- /dev/null +++ b/vtable.c @@ -0,0 +1,182 @@ +// Copyright (C) 2014 Yasuhiro Matsumoto . +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file. + +#ifndef USE_LIBSQLITE3 +#include +#else +#include +#endif +#include +#include +#include +#include "_cgo_export.h" + + +typedef struct goVTab goVTab; + +struct goVTab { + sqlite3_vtab base; + void *vTab; +}; + +static int cXInit(sqlite3 *db, void *pAux, int argc, const char *const*argv, sqlite3_vtab **ppVTab, char **pzErr, int isCreate) { + void *vTab = (void *)goMInit(db, pAux, argc, (char**)argv, pzErr, isCreate); + if (!vTab || *pzErr) { + return SQLITE_ERROR; + } + goVTab *pvTab = (goVTab *)sqlite3_malloc(sizeof(goVTab)); + if (!pvTab) { + *pzErr = sqlite3_mprintf("%s", "Out of memory"); + return SQLITE_NOMEM; + } + memset(pvTab, 0, sizeof(goVTab)); + pvTab->vTab = vTab; + + *ppVTab = (sqlite3_vtab *)pvTab; + *pzErr = 0; + return SQLITE_OK; +} + +static inline int cXCreate(sqlite3 *db, void *pAux, int argc, const char *const*argv, sqlite3_vtab **ppVTab, char **pzErr) { + return cXInit(db, pAux, argc, argv, ppVTab, pzErr, 1); +} +static inline int cXConnect(sqlite3 *db, void *pAux, int argc, const char *const*argv, sqlite3_vtab **ppVTab, char **pzErr) { + return cXInit(db, pAux, argc, argv, ppVTab, pzErr, 0); +} + +static inline int cXBestIndex(sqlite3_vtab *pVTab, sqlite3_index_info *info) { + char *pzErr = goVBestIndex(((goVTab*)pVTab)->vTab, info); + if (pzErr) { + if (pVTab->zErrMsg) + sqlite3_free(pVTab->zErrMsg); + pVTab->zErrMsg = pzErr; + return SQLITE_ERROR; + } + return SQLITE_OK; +} + +static int cXRelease(sqlite3_vtab *pVTab, int isDestroy) { + char *pzErr = goVRelease(((goVTab*)pVTab)->vTab, isDestroy); + if (pzErr) { + if (pVTab->zErrMsg) + sqlite3_free(pVTab->zErrMsg); + pVTab->zErrMsg = pzErr; + return SQLITE_ERROR; + } + if (pVTab->zErrMsg) + sqlite3_free(pVTab->zErrMsg); + sqlite3_free(pVTab); + return SQLITE_OK; +} + +static inline int cXDisconnect(sqlite3_vtab *pVTab) { + return cXRelease(pVTab, 0); +} +static inline int cXDestroy(sqlite3_vtab *pVTab) { + return cXRelease(pVTab, 1); +} + +typedef struct goVTabCursor goVTabCursor; + +struct goVTabCursor { + sqlite3_vtab_cursor base; + void *vTabCursor; +}; + +static int cXOpen(sqlite3_vtab *pVTab, sqlite3_vtab_cursor **ppCursor) { + void *vTabCursor = (void *)goVOpen(((goVTab*)pVTab)->vTab, &(pVTab->zErrMsg)); + goVTabCursor *pCursor = (goVTabCursor *)sqlite3_malloc(sizeof(goVTabCursor)); + if (!pCursor) { + return SQLITE_NOMEM; + } + memset(pCursor, 0, sizeof(goVTabCursor)); + pCursor->vTabCursor = vTabCursor; + *ppCursor = (sqlite3_vtab_cursor *)pCursor; + return SQLITE_OK; +} + +static int setErrMsg(sqlite3_vtab_cursor *pCursor, char *pzErr) { + if (pCursor->pVtab->zErrMsg) + sqlite3_free(pCursor->pVtab->zErrMsg); + pCursor->pVtab->zErrMsg = pzErr; + return SQLITE_ERROR; +} + +static int cXClose(sqlite3_vtab_cursor *pCursor) { + char *pzErr = goVClose(((goVTabCursor*)pCursor)->vTabCursor); + if (pzErr) { + return setErrMsg(pCursor, pzErr); + } + sqlite3_free(pCursor); + return SQLITE_OK; +} + +static int cXFilter(sqlite3_vtab_cursor *pCursor, int idxNum, const char *idxStr, int argc, sqlite3_value **argv) { + char *pzErr = goVFilter(((goVTabCursor*)pCursor)->vTabCursor, idxNum, (char*)idxStr, argc, argv); + if (pzErr) { + return setErrMsg(pCursor, pzErr); + } + return SQLITE_OK; +} + +static int cXNext(sqlite3_vtab_cursor *pCursor) { + char *pzErr = goVNext(((goVTabCursor*)pCursor)->vTabCursor); + if (pzErr) { + return setErrMsg(pCursor, pzErr); + } + return SQLITE_OK; +} + +static inline int cXEof(sqlite3_vtab_cursor *pCursor) { + return goVEof(((goVTabCursor*)pCursor)->vTabCursor); +} + +static int cXColumn(sqlite3_vtab_cursor *pCursor, sqlite3_context *ctx, int i) { + char *pzErr = goVColumn(((goVTabCursor*)pCursor)->vTabCursor, ctx, i); + if (pzErr) { + return setErrMsg(pCursor, pzErr); + } + return SQLITE_OK; +} + +static int cXRowid(sqlite3_vtab_cursor *pCursor, sqlite3_int64 *pRowid) { + char *pzErr = goVRowid(((goVTabCursor*)pCursor)->vTabCursor, pRowid); + if (pzErr) { + return setErrMsg(pCursor, pzErr); + } + return SQLITE_OK; +} + +static sqlite3_module goModule = { + 0, /* iVersion */ + cXCreate, /* xCreate - create a table */ + cXConnect, /* xConnect - connect to an existing table */ + cXBestIndex, /* xBestIndex - Determine search strategy */ + cXDisconnect, /* xDisconnect - Disconnect from a table */ + cXDestroy, /* xDestroy - Drop a table */ + cXOpen, /* xOpen - open a cursor */ + cXClose, /* xClose - close a cursor */ + cXFilter, /* xFilter - configure scan constraints */ + cXNext, /* xNext - advance a cursor */ + cXEof, /* xEof */ + cXColumn, /* xColumn - read data */ + cXRowid, /* xRowid - read data */ +// Not implemented + 0, /* xUpdate - write data */ + 0, /* xBegin - begin transaction */ + 0, /* xSync - sync transaction */ + 0, /* xCommit - commit transaction */ + 0, /* xRollback - rollback transaction */ + 0, /* xFindFunction - function overloading */ + 0, /* xRename - rename the table */ + 0, /* xSavepoint */ + 0, /* xRelease */ + 0 /* xRollbackTo */ +}; + + +int goSqlite3CreateModule(sqlite3 *db, const char *zName, uintptr_t pClientData) { + return sqlite3_create_module_v2(db, zName, &goModule, (void*) pClientData, goMDestroy); +} \ No newline at end of file diff --git a/vtable.go b/vtable.go new file mode 100644 index 0000000..907e2dc --- /dev/null +++ b/vtable.go @@ -0,0 +1,380 @@ +// Copyright (C) 2014 Yasuhiro Matsumoto . +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file. + +package sqlite3 + +/* +#cgo CFLAGS: -std=gnu99 +#cgo CFLAGS: -DSQLITE_ENABLE_RTREE -DSQLITE_THREADSAFE +#cgo CFLAGS: -DSQLITE_ENABLE_FTS3 -DSQLITE_ENABLE_FTS3_PARENTHESIS -DSQLITE_ENABLE_FTS4_UNICODE61 +#cgo CFLAGS: -DSQLITE_TRACE_SIZE_LIMIT=15 +#cgo CFLAGS: -DSQLITE_ENABLE_COLUMN_METADATA=1 +#cgo CFLAGS: -Wno-deprecated-declarations + +#ifndef USE_LIBSQLITE3 +#include +#else +#include +#endif +#include +#include + +int goSqlite3CreateModule(sqlite3 *db, const char *zName, uintptr_t pClientData); + +static inline char *my_mprintf(char *zFormat, char *arg) { + return sqlite3_mprintf(zFormat, arg); +} +*/ +import "C" + +import ( + "math" + "reflect" + "unsafe" +) + +type sqliteModule struct { + c *SQLiteConn + name string + module Module +} + +type sqliteVTab struct { + module *sqliteModule + vTab VTab +} + +type sqliteVTabCursor struct { + vTab *sqliteVTab + vTabCursor VTabCursor +} + +type Op uint8 + +const ( + OpEQ Op = 2 + OpGT = 4 + OpLE = 8 + OpLT = 16 + OpGE = 32 + OpMATCH = 64 + OpLIKE = 65 /* 3.10.0 and later only */ + OpGLOB = 66 /* 3.10.0 and later only */ + OpREGEXP = 67 /* 3.10.0 and later only */ + OpScanUnique = 1 /* Scan visits at most 1 row */ +) + +type InfoConstraint struct { + Column int + Op Op + Usable bool +} + +type InfoOrderBy struct { + Column int + Desc bool +} + +func constraints(info *C.sqlite3_index_info) []InfoConstraint { + l := info.nConstraint + var constraints *C.struct_sqlite3_index_constraint = info.aConstraint + slice := (*[1 << 30]C.struct_sqlite3_index_constraint)(unsafe.Pointer(constraints))[:l:l] + + cst := make([]InfoConstraint, 0, l) + for _, c := range slice { + var usable bool + if c.usable > 0 { + usable = true + } + cst = append(cst, InfoConstraint{ + Column: int(c.iColumn), + Op: Op(c.op), + Usable: usable, + }) + } + return cst +} + +func orderBys(info *C.sqlite3_index_info) []InfoOrderBy { + l := info.nOrderBy + var obys *C.struct_sqlite3_index_orderby = info.aOrderBy + slice := (*[1 << 30]C.struct_sqlite3_index_orderby)(unsafe.Pointer(obys))[:l:l] + + ob := make([]InfoOrderBy, 0, l) + for _, c := range slice { + var desc bool + if c.desc > 0 { + desc = true + } + ob = append(ob, InfoOrderBy{ + Column: int(c.iColumn), + Desc: desc, + }) + } + return ob +} + +// IndexResult is a Go struct represetnation of what eventually ends up in the +// output fields for `sqlite3_index_info` +// See: https://www.sqlite.org/c3ref/index_info.html +type IndexResult struct { + Used []bool // aConstraintUsage + IdxNum int + IdxStr string + AlreadyOrdered bool // orderByConsumed + EstimatedCost float64 + EstimatedRows float64 +} + +// mPrintf is a utility wrapper around sqlite3_mprintf +func mPrintf(format, arg string) *C.char { + cf := C.CString(format) + defer C.free(unsafe.Pointer(cf)) + ca := C.CString(arg) + defer C.free(unsafe.Pointer(ca)) + return C.my_mprintf(cf, ca) +} + +//export goMInit +func goMInit(db, pClientData unsafe.Pointer, argc int, argv **C.char, pzErr **C.char, isCreate int) C.uintptr_t { + m := lookupHandle(uintptr(pClientData)).(*sqliteModule) + if m.c.db != (*C.sqlite3)(db) { + *pzErr = mPrintf("%s", "Inconsistent db handles") + return 0 + } + args := make([]string, argc) + var A []*C.char + slice := reflect.SliceHeader{Data: uintptr(unsafe.Pointer(argv)), Len: argc, Cap: argc} + a := reflect.NewAt(reflect.TypeOf(A), unsafe.Pointer(&slice)).Elem().Interface() + for i, s := range a.([]*C.char) { + args[i] = C.GoString(s) + } + var vTab VTab + var err error + if isCreate == 1 { + vTab, err = m.module.Create(m.c, args) + } else { + vTab, err = m.module.Connect(m.c, args) + } + + if err != nil { + *pzErr = mPrintf("%s", err.Error()) + return 0 + } + vt := sqliteVTab{m, vTab} + *pzErr = nil + return C.uintptr_t(newHandle(m.c, &vt)) +} + +//export goVRelease +func goVRelease(pVTab unsafe.Pointer, isDestroy int) *C.char { + vt := lookupHandle(uintptr(pVTab)).(*sqliteVTab) + var err error + if isDestroy == 1 { + err = vt.vTab.Destroy() + } else { + err = vt.vTab.Disconnect() + } + if err != nil { + return mPrintf("%s", err.Error()) + } + return nil +} + +//export goVOpen +func goVOpen(pVTab unsafe.Pointer, pzErr **C.char) C.uintptr_t { + vt := lookupHandle(uintptr(pVTab)).(*sqliteVTab) + vTabCursor, err := vt.vTab.Open() + if err != nil { + *pzErr = mPrintf("%s", err.Error()) + return 0 + } + vtc := sqliteVTabCursor{vt, vTabCursor} + *pzErr = nil + return C.uintptr_t(newHandle(vt.module.c, &vtc)) +} + +//export goVBestIndex +func goVBestIndex(pVTab unsafe.Pointer, icp unsafe.Pointer) *C.char { + vt := lookupHandle(uintptr(pVTab)).(*sqliteVTab) + info := (*C.sqlite3_index_info)(icp) + csts := constraints(info) + res, err := vt.vTab.BestIndex(csts, orderBys(info)) + if err != nil { + return mPrintf("%s", err.Error()) + } + if len(res.Used) != len(csts) { + return mPrintf("Result.Used != expected value", "") + } + + // Get a pointer to constraint_usage struct so we can update in place. + l := info.nConstraint + var usg *C.struct_sqlite3_index_constraint_usage = info.aConstraintUsage + s := (*[1 << 30]C.struct_sqlite3_index_constraint_usage)(unsafe.Pointer(usg))[:l:l] + index := 1 + for i := C.int(0); i < info.nConstraint; i++ { + if res.Used[i] { + s[i].argvIndex = C.int(index) + s[i].omit = C.uchar(1) + index++ + } + } + + info.idxNum = C.int(res.IdxNum) + idxStr := C.CString(res.IdxStr) + defer C.free(unsafe.Pointer(idxStr)) + info.idxStr = idxStr + info.needToFreeIdxStr = C.int(0) + if res.AlreadyOrdered { + info.orderByConsumed = C.int(1) + } + info.estimatedCost = C.double(res.EstimatedCost) + info.estimatedRows = C.sqlite3_int64(res.EstimatedRows) + + return nil +} + +//export goVClose +func goVClose(pCursor unsafe.Pointer) *C.char { + vtc := lookupHandle(uintptr(pCursor)).(*sqliteVTabCursor) + err := vtc.vTabCursor.Close() + if err != nil { + return mPrintf("%s", err.Error()) + } + return nil +} + +//export goMDestroy +func goMDestroy(pClientData unsafe.Pointer) { + m := lookupHandle(uintptr(pClientData)).(*sqliteModule) + m.module.DestroyModule() +} + +//export goVFilter +func goVFilter(pCursor unsafe.Pointer, idxNum int, idxName *C.char, argc int, argv **C.sqlite3_value) *C.char { + vtc := lookupHandle(uintptr(pCursor)).(*sqliteVTabCursor) + args := (*[(math.MaxInt32 - 1) / unsafe.Sizeof((*C.sqlite3_value)(nil))]*C.sqlite3_value)(unsafe.Pointer(argv))[:argc:argc] + vals := make([]interface{}, 0, argc) + for _, v := range args { + conv, err := callbackArgGeneric(v) + if err != nil { + return mPrintf("%s", err.Error()) + } + vals = append(vals, conv.Interface()) + } + err := vtc.vTabCursor.Filter(idxNum, C.GoString(idxName), vals) + if err != nil { + return mPrintf("%s", err.Error()) + } + return nil +} + +//export goVNext +func goVNext(pCursor unsafe.Pointer) *C.char { + vtc := lookupHandle(uintptr(pCursor)).(*sqliteVTabCursor) + err := vtc.vTabCursor.Next() + if err != nil { + return mPrintf("%s", err.Error()) + } + return nil +} + +//export goVEof +func goVEof(pCursor unsafe.Pointer) C.int { + vtc := lookupHandle(uintptr(pCursor)).(*sqliteVTabCursor) + err := vtc.vTabCursor.EOF() + if err { + return 1 + } + return 0 +} + +//export goVColumn +func goVColumn(pCursor, cp unsafe.Pointer, col int) *C.char { + vtc := lookupHandle(uintptr(pCursor)).(*sqliteVTabCursor) + c := (*Context)(cp) + err := vtc.vTabCursor.Column(c, col) + if err != nil { + return mPrintf("%s", err.Error()) + } + return nil +} + +//export goVRowid +func goVRowid(pCursor unsafe.Pointer, pRowid *C.sqlite3_int64) *C.char { + vtc := lookupHandle(uintptr(pCursor)).(*sqliteVTabCursor) + rowid, err := vtc.vTabCursor.Rowid() + if err != nil { + return mPrintf("%s", err.Error()) + } + *pRowid = C.sqlite3_int64(rowid) + return nil +} + +// Module is a "virtual table module", it defines the implementation of a +// virtual tables. See: http://sqlite.org/c3ref/module.html +type Module interface { + // http://sqlite.org/vtab.html#xcreate + Create(c *SQLiteConn, args []string) (VTab, error) + // http://sqlite.org/vtab.html#xconnect + Connect(c *SQLiteConn, args []string) (VTab, error) + // http://sqlite.org/c3ref/create_module.html + DestroyModule() +} + +// VTab describes a particular instance of the virtual table. +// See: http://sqlite.org/c3ref/vtab.html +type VTab interface { + // http://sqlite.org/vtab.html#xbestindex + BestIndex([]InfoConstraint, []InfoOrderBy) (*IndexResult, error) + // http://sqlite.org/vtab.html#xdisconnect + Disconnect() error + // http://sqlite.org/vtab.html#sqlite3_module.xDestroy + Destroy() error + // http://sqlite.org/vtab.html#xopen + Open() (VTabCursor, error) +} + +// VTabCursor describes cursors that point into the virtual table and are used +// to loop through the virtual table. See: http://sqlite.org/c3ref/vtab_cursor.html +type VTabCursor interface { + // http://sqlite.org/vtab.html#xclose + Close() error + // http://sqlite.org/vtab.html#xfilter + Filter(idxNum int, idxStr string, vals []interface{}) error + // http://sqlite.org/vtab.html#xnext + Next() error + // http://sqlite.org/vtab.html#xeof + EOF() bool + // http://sqlite.org/vtab.html#xcolumn + Column(c *Context, col int) error + // http://sqlite.org/vtab.html#xrowid + Rowid() (int64, error) +} + +// DeclareVTab declares the Schema of a virtual table. +// See: http://sqlite.org/c3ref/declare_vtab.html +func (c *SQLiteConn) DeclareVTab(sql string) error { + zSQL := C.CString(sql) + defer C.free(unsafe.Pointer(zSQL)) + rv := C.sqlite3_declare_vtab(c.db, zSQL) + if rv != C.SQLITE_OK { + return c.lastError() + } + return nil +} + +// CreateModule registers a virtual table implementation. +// See: http://sqlite.org/c3ref/create_module.html +func (c *SQLiteConn) CreateModule(moduleName string, module Module) error { + mname := C.CString(moduleName) + defer C.free(unsafe.Pointer(mname)) + udm := sqliteModule{c, moduleName, module} + rv := C.goSqlite3CreateModule(c.db, mname, C.uintptr_t(newHandle(c, &udm))) + if rv != C.SQLITE_OK { + return c.lastError() + } + return nil +} diff --git a/vtable_test.go b/vtable_test.go new file mode 100644 index 0000000..4c7efcb --- /dev/null +++ b/vtable_test.go @@ -0,0 +1,132 @@ +// Copyright (C) 2014 Yasuhiro Matsumoto . +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file. + +package sqlite3 + +import ( + "database/sql" + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +type testModule struct { + t *testing.T + intarray []int +} + +type testVTab struct { + intarray []int +} + +type testVTabCursor struct { + vTab *testVTab + index int +} + +func (m testModule) Create(c *SQLiteConn, args []string) (VTab, error) { + assert.True(m.t, len(args) == 6, "six arguments expected") + assert.Equal(m.t, "test", args[0], "module name") + assert.Equal(m.t, "main", args[1], "db name") + assert.Equal(m.t, "vtab", args[2], "table name") + assert.Equal(m.t, "'1'", args[3], "first arg") + assert.Equal(m.t, "2", args[4], "second arg") + assert.Equal(m.t, "three", args[5], "third arg") + err := c.DeclareVTab("CREATE TABLE x(test TEXT)") + if err != nil { + return nil, err + } + return &testVTab{m.intarray}, nil +} + +func (m testModule) Connect(c *SQLiteConn, args []string) (VTab, error) { + return m.Create(c, args) +} + +func (m testModule) DestroyModule() {} + +func (v *testVTab) BestIndex(cst []InfoConstraint, ob []InfoOrderBy) (*IndexResult, error) { + used := make([]bool, 0, len(cst)) + for range cst { + used = append(used, false) + } + return &IndexResult{ + Used: used, + IdxNum: 0, + IdxStr: "test-index", + AlreadyOrdered: true, + EstimatedCost: 100, + EstimatedRows: 200, + }, nil +} + +func (v *testVTab) Disconnect() error { + return nil +} + +func (v *testVTab) Destroy() error { + return nil +} + +func (v *testVTab) Open() (VTabCursor, error) { + return &testVTabCursor{v, 0}, nil +} + +func (vc *testVTabCursor) Close() error { + return nil +} + +func (vc *testVTabCursor) Filter(idxNum int, idxStr string, vals []interface{}) error { + vc.index = 0 + return nil +} + +func (vc *testVTabCursor) Next() error { + vc.index++ + return nil +} + +func (vc *testVTabCursor) EOF() bool { + return vc.index >= len(vc.vTab.intarray) +} + +func (vc *testVTabCursor) Column(c *Context, col int) error { + if col != 0 { + return fmt.Errorf("column index out of bounds: %d", col) + } + c.ResultInt(vc.vTab.intarray[vc.index]) + return nil +} + +func (vc *testVTabCursor) Rowid() (int64, error) { + return int64(vc.index), nil +} + +func TestCreateModule(t *testing.T) { + tempFilename := TempFilename(t) + defer os.Remove(tempFilename) + intarray := []int{1, 2, 3} + sql.Register("sqlite3_TestCreateModule", &SQLiteDriver{ + ConnectHook: func(conn *SQLiteConn) error { + return conn.CreateModule("test", testModule{t, intarray}) + }, + }) + db, err := sql.Open("sqlite3_TestCreateModule", tempFilename) + assert.Nil(t, err, "could not open db") + _, err = db.Exec("CREATE VIRTUAL TABLE vtab USING test('1', 2, three)") + assert.Nil(t, err, "could not create vtable") + + var i, value int + rows, err := db.Query("SELECT rowid, * FROM vtab WHERE test = '3'") + assert.Nil(t, err, "couldn't select from virtual table") + for rows.Next() { + rows.Scan(&i, &value) + assert.Equal(t, intarray[i], value) + } + _, err = db.Exec("DROP TABLE vtab") + assert.Nil(t, err, "couldn't drop virtual table") +} From 618e784627e2d0c0883f92c1708de3107296a0f9 Mon Sep 17 00:00:00 2001 From: Conor Branagan Date: Sun, 11 Dec 2016 12:49:48 -0500 Subject: [PATCH 2/3] [vtable] Add pure Go example of GitHub repo vtable. --- _example/vtable/main.go | 37 +++++++++++++ _example/vtable/vtable.go | 110 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+) create mode 100644 _example/vtable/main.go create mode 100644 _example/vtable/vtable.go diff --git a/_example/vtable/main.go b/_example/vtable/main.go new file mode 100644 index 0000000..442069f --- /dev/null +++ b/_example/vtable/main.go @@ -0,0 +1,37 @@ +package main + +import ( + "database/sql" + "fmt" + "github.com/mattn/go-sqlite3" + "log" +) + +func main() { + sql.Register("sqlite3_with_extensions", &sqlite3.SQLiteDriver{ + ConnectHook: func(conn *sqlite3.SQLiteConn) error { + return conn.CreateModule("github", githubModule{}) + }, + }) + db, err := sql.Open("sqlite3_with_extensions", ":memory:") + if err != nil { + log.Fatal(err) + } + defer db.Close() + + _, err = db.Exec("create virtual table repo using github(id, full_name, description, html_url)") + if err != nil { + log.Fatal(err) + } + + rows, err := db.Query("select id, full_name, description, html_url from repo") + if err != nil { + log.Fatal(err) + } + defer rows.Close() + for rows.Next() { + var id, full_name, description, html_url string + rows.Scan(&id, &full_name, &description, &html_url) + fmt.Printf("%s: %s\n\t%s\n\t%s\n\n", id, full_name, description, html_url) + } +} diff --git a/_example/vtable/vtable.go b/_example/vtable/vtable.go new file mode 100644 index 0000000..cc0f308 --- /dev/null +++ b/_example/vtable/vtable.go @@ -0,0 +1,110 @@ +package main + +import ( + "encoding/json" + "fmt" + "github.com/mattn/go-sqlite3" + "io/ioutil" + "net/http" +) + +type GithubRepo struct { + ID int `json:"id"` + FullName string `json:"full_name"` + Description string `json:"description"` + HtmlURL string `json:"html_url"` +} + +type githubModule struct { +} + +func (m githubModule) Create(c *sqlite3.SQLiteConn, args []string) (sqlite3.VTab, error) { + err := c.DeclareVTab(fmt.Sprintf(` + CREATE TABLE %s ( + id INT, + full_name TEXT, + description TEXT, + html_url TEXT + )`, args[0])) + if err != nil { + return nil, err + } + return &ghRepoTable{}, nil +} + +func (m githubModule) Connect(c *sqlite3.SQLiteConn, args []string) (sqlite3.VTab, error) { + return m.Create(c, args) +} + +func (m githubModule) DestroyModule() {} + +type ghRepoTable struct { + repos []GithubRepo +} + +func (v *ghRepoTable) Open() (sqlite3.VTabCursor, error) { + resp, err := http.Get("https://api.github.com/repositories") + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + repos := make([]GithubRepo, 0) + if err := json.Unmarshal(body, &repos); err != nil { + return nil, err + } + return &ghRepoCursor{0, repos}, nil +} + +func (v *ghRepoTable) BestIndex(cst []sqlite3.InfoConstraint, ob []sqlite3.InfoOrderBy) (*sqlite3.IndexResult, error) { + return &sqlite3.IndexResult{}, nil +} + +func (v *ghRepoTable) Disconnect() error { return nil } +func (v *ghRepoTable) Destroy() error { return nil } + +type ghRepoCursor struct { + index int + repos []GithubRepo +} + +func (vc *ghRepoCursor) Column(c *sqlite3.Context, col int) error { + switch col { + case 0: + c.ResultInt(vc.repos[vc.index].ID) + case 1: + c.ResultText(vc.repos[vc.index].FullName) + case 2: + c.ResultText(vc.repos[vc.index].Description) + case 3: + c.ResultText(vc.repos[vc.index].HtmlURL) + } + return nil +} + +func (vc *ghRepoCursor) Filter(idxNum int, idxStr string, vals []interface{}) error { + vc.index = 0 + return nil +} + +func (vc *ghRepoCursor) Next() error { + vc.index++ + return nil +} + +func (vc *ghRepoCursor) EOF() bool { + return vc.index >= len(vc.repos) +} + +func (vc *ghRepoCursor) Rowid() (int64, error) { + return int64(vc.index), nil +} + +func (vc *ghRepoCursor) Close() error { + return nil +} From 9efa963d05cac8b780d1cb3abbd8493a94f1db82 Mon Sep 17 00:00:00 2001 From: Conor Branagan Date: Sat, 4 Mar 2017 18:15:00 -0500 Subject: [PATCH 3/3] [vtable] Rename Context to SQLiteContext To not conflict with core "context" package naming. --- _example/vtable/vtable.go | 4 ++-- context.go => sqlite_context.go | 18 +++++++++--------- vtable.go | 4 ++-- vtable_test.go | 2 +- 4 files changed, 14 insertions(+), 14 deletions(-) rename context.go => sqlite_context.go (86%) diff --git a/_example/vtable/vtable.go b/_example/vtable/vtable.go index cc0f308..13e2624 100644 --- a/_example/vtable/vtable.go +++ b/_example/vtable/vtable.go @@ -3,7 +3,7 @@ package main import ( "encoding/json" "fmt" - "github.com/mattn/go-sqlite3" + "github.com/DataDog/go-sqlite3" "io/ioutil" "net/http" ) @@ -73,7 +73,7 @@ type ghRepoCursor struct { repos []GithubRepo } -func (vc *ghRepoCursor) Column(c *sqlite3.Context, col int) error { +func (vc *ghRepoCursor) Column(c *sqlite3.SQLiteContext, col int) error { switch col { case 0: c.ResultInt(vc.repos[vc.index].ID) diff --git a/context.go b/sqlite_context.go similarity index 86% rename from context.go rename to sqlite_context.go index ba943da..7652902 100644 --- a/context.go +++ b/sqlite_context.go @@ -35,10 +35,10 @@ import ( const i64 = unsafe.Sizeof(int(0)) > 4 type ZeroBlobLength int32 -type Context C.sqlite3_context +type SQLiteContext C.sqlite3_context // ResultBool sets the result of an SQL function. -func (c *Context) ResultBool(b bool) { +func (c *SQLiteContext) ResultBool(b bool) { if b { c.ResultInt(1) } else { @@ -48,7 +48,7 @@ func (c *Context) ResultBool(b bool) { // ResultBlob sets the result of an SQL function. // See: sqlite3_result_blob, http://sqlite.org/c3ref/result_blob.html -func (c *Context) ResultBlob(b []byte) { +func (c *SQLiteContext) ResultBlob(b []byte) { if i64 && len(b) > math.MaxInt32 { C.sqlite3_result_error_toobig((*C.sqlite3_context)(c)) return @@ -62,13 +62,13 @@ func (c *Context) ResultBlob(b []byte) { // ResultDouble sets the result of an SQL function. // See: sqlite3_result_double, http://sqlite.org/c3ref/result_blob.html -func (c *Context) ResultDouble(d float64) { +func (c *SQLiteContext) ResultDouble(d float64) { C.sqlite3_result_double((*C.sqlite3_context)(c), C.double(d)) } // ResultInt sets the result of an SQL function. // See: sqlite3_result_int, http://sqlite.org/c3ref/result_blob.html -func (c *Context) ResultInt(i int) { +func (c *SQLiteContext) ResultInt(i int) { if i64 && (i > math.MaxInt32 || i < math.MinInt32) { C.sqlite3_result_int64((*C.sqlite3_context)(c), C.sqlite3_int64(i)) } else { @@ -78,19 +78,19 @@ func (c *Context) ResultInt(i int) { // ResultInt64 sets the result of an SQL function. // See: sqlite3_result_int64, http://sqlite.org/c3ref/result_blob.html -func (c *Context) ResultInt64(i int64) { +func (c *SQLiteContext) ResultInt64(i int64) { C.sqlite3_result_int64((*C.sqlite3_context)(c), C.sqlite3_int64(i)) } // ResultNull sets the result of an SQL function. // See: sqlite3_result_null, http://sqlite.org/c3ref/result_blob.html -func (c *Context) ResultNull() { +func (c *SQLiteContext) ResultNull() { C.sqlite3_result_null((*C.sqlite3_context)(c)) } // ResultText sets the result of an SQL function. // See: sqlite3_result_text, http://sqlite.org/c3ref/result_blob.html -func (c *Context) ResultText(s string) { +func (c *SQLiteContext) ResultText(s string) { h := (*reflect.StringHeader)(unsafe.Pointer(&s)) cs, l := (*C.char)(unsafe.Pointer(h.Data)), C.int(h.Len) C.my_result_text((*C.sqlite3_context)(c), cs, l) @@ -98,6 +98,6 @@ func (c *Context) ResultText(s string) { // ResultZeroblob sets the result of an SQL function. // See: sqlite3_result_zeroblob, http://sqlite.org/c3ref/result_blob.html -func (c *Context) ResultZeroblob(n ZeroBlobLength) { +func (c *SQLiteContext) ResultZeroblob(n ZeroBlobLength) { C.sqlite3_result_zeroblob((*C.sqlite3_context)(c), C.int(n)) } diff --git a/vtable.go b/vtable.go index 907e2dc..40ce2ea 100644 --- a/vtable.go +++ b/vtable.go @@ -294,7 +294,7 @@ func goVEof(pCursor unsafe.Pointer) C.int { //export goVColumn func goVColumn(pCursor, cp unsafe.Pointer, col int) *C.char { vtc := lookupHandle(uintptr(pCursor)).(*sqliteVTabCursor) - c := (*Context)(cp) + c := (*SQLiteContext)(cp) err := vtc.vTabCursor.Column(c, col) if err != nil { return mPrintf("%s", err.Error()) @@ -349,7 +349,7 @@ type VTabCursor interface { // http://sqlite.org/vtab.html#xeof EOF() bool // http://sqlite.org/vtab.html#xcolumn - Column(c *Context, col int) error + Column(c *SQLiteContext, col int) error // http://sqlite.org/vtab.html#xrowid Rowid() (int64, error) } diff --git a/vtable_test.go b/vtable_test.go index 4c7efcb..9b97927 100644 --- a/vtable_test.go +++ b/vtable_test.go @@ -94,7 +94,7 @@ func (vc *testVTabCursor) EOF() bool { return vc.index >= len(vc.vTab.intarray) } -func (vc *testVTabCursor) Column(c *Context, col int) error { +func (vc *testVTabCursor) Column(c *SQLiteContext, col int) error { if col != 0 { return fmt.Errorf("column index out of bounds: %d", col) }