From 92d23714a87355a0b7087795126bc0e0e2b06098 Mon Sep 17 00:00:00 2001 From: Patrick DeVivo Date: Sat, 26 Dec 2020 09:11:17 -0500 Subject: [PATCH] add support for defining an "eponymous only" virtual table (#885) * add support for defining an "eponymous only" virtual table As suggested here: https://github.com/mattn/go-sqlite3/issues/846#issuecomment-736206222 * add an example of an eponymous only vtab module * add a test case for an eponymous only vtab module --- _example/vtable_eponymous_only/main.go | 33 ++++++ _example/vtable_eponymous_only/vtable.go | 118 ++++++++++++++++++++++ sqlite3_opt_vtable.go | 55 +++++++++- sqlite3_opt_vtable_test.go | 122 +++++++++++++++++++++++ 4 files changed, 325 insertions(+), 3 deletions(-) create mode 100644 _example/vtable_eponymous_only/main.go create mode 100644 _example/vtable_eponymous_only/vtable.go diff --git a/_example/vtable_eponymous_only/main.go b/_example/vtable_eponymous_only/main.go new file mode 100644 index 00000000..17b58afa --- /dev/null +++ b/_example/vtable_eponymous_only/main.go @@ -0,0 +1,33 @@ +package main + +import ( + "database/sql" + "fmt" + "log" + + "github.com/mattn/go-sqlite3" +) + +func main() { + sql.Register("sqlite3_with_extensions", &sqlite3.SQLiteDriver{ + ConnectHook: func(conn *sqlite3.SQLiteConn) error { + return conn.CreateModule("series", &seriesModule{}) + }, + }) + db, err := sql.Open("sqlite3_with_extensions", ":memory:") + if err != nil { + log.Fatal(err) + } + defer db.Close() + + rows, err := db.Query("select * from series") + if err != nil { + log.Fatal(err) + } + defer rows.Close() + for rows.Next() { + var value int + rows.Scan(&value) + fmt.Printf("value: %d\n", value) + } +} diff --git a/_example/vtable_eponymous_only/vtable.go b/_example/vtable_eponymous_only/vtable.go new file mode 100644 index 00000000..49fc0b72 --- /dev/null +++ b/_example/vtable_eponymous_only/vtable.go @@ -0,0 +1,118 @@ +package main + +import ( + "fmt" + + "github.com/mattn/go-sqlite3" +) + +type seriesModule struct{} + +func (m *seriesModule) EponymousOnlyModule() {} + +func (m *seriesModule) Create(c *sqlite3.SQLiteConn, args []string) (sqlite3.VTab, error) { + err := c.DeclareVTab(fmt.Sprintf(` + CREATE TABLE %s ( + value INT, + start HIDDEN, + stop HIDDEN, + step HIDDEN + )`, args[0])) + if err != nil { + return nil, err + } + return &seriesTable{0, 0, 1}, nil +} + +func (m *seriesModule) Connect(c *sqlite3.SQLiteConn, args []string) (sqlite3.VTab, error) { + return m.Create(c, args) +} + +func (m *seriesModule) DestroyModule() {} + +type seriesTable struct { + start int64 + stop int64 + step int64 +} + +func (v *seriesTable) Open() (sqlite3.VTabCursor, error) { + return &seriesCursor{v, 0}, nil +} + +func (v *seriesTable) BestIndex(csts []sqlite3.InfoConstraint, ob []sqlite3.InfoOrderBy) (*sqlite3.IndexResult, error) { + used := make([]bool, len(csts)) + for c, cst := range csts { + if cst.Usable && cst.Op == sqlite3.OpEQ { + used[c] = true + } + } + + return &sqlite3.IndexResult{ + IdxNum: 0, + IdxStr: "default", + Used: used, + }, nil +} + +func (v *seriesTable) Disconnect() error { return nil } +func (v *seriesTable) Destroy() error { return nil } + +type seriesCursor struct { + *seriesTable + value int64 +} + +func (vc *seriesCursor) Column(c *sqlite3.SQLiteContext, col int) error { + switch col { + case 0: + c.ResultInt64(vc.value) + case 1: + c.ResultInt64(vc.seriesTable.start) + case 2: + c.ResultInt64(vc.seriesTable.stop) + case 3: + c.ResultInt64(vc.seriesTable.step) + } + return nil +} + +func (vc *seriesCursor) Filter(idxNum int, idxStr string, vals []interface{}) error { + switch { + case len(vals) < 1: + vc.seriesTable.start = 0 + vc.seriesTable.stop = 1000 + vc.value = vc.seriesTable.start + case len(vals) < 2: + vc.seriesTable.start = vals[0].(int64) + vc.seriesTable.stop = 1000 + vc.value = vc.seriesTable.start + case len(vals) < 3: + vc.seriesTable.start = vals[0].(int64) + vc.seriesTable.stop = vals[1].(int64) + vc.value = vc.seriesTable.start + case len(vals) < 4: + vc.seriesTable.start = vals[0].(int64) + vc.seriesTable.stop = vals[1].(int64) + vc.seriesTable.step = vals[2].(int64) + } + + return nil +} + +func (vc *seriesCursor) Next() error { + vc.value += vc.step + return nil +} + +func (vc *seriesCursor) EOF() bool { + return vc.value > vc.stop +} + +func (vc *seriesCursor) Rowid() (int64, error) { + return int64(vc.value), nil +} + +func (vc *seriesCursor) Close() error { + return nil +} diff --git a/sqlite3_opt_vtable.go b/sqlite3_opt_vtable.go index c0a6214e..8fd6cdff 100644 --- a/sqlite3_opt_vtable.go +++ b/sqlite3_opt_vtable.go @@ -226,11 +226,43 @@ static sqlite3_module goModule = { 0 // xRollbackTo }; +// See https://sqlite.org/vtab.html#eponymous_only_virtual_tables +static sqlite3_module goModuleEponymousOnly = { + 0, // iVersion + 0, // xCreate - create a table, which here is null + 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 + cXUpdate, // xUpdate - write data +// Not implemented + 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 +}; + void goMDestroy(void*); static int _sqlite3_create_module(sqlite3 *db, const char *zName, uintptr_t pClientData) { return sqlite3_create_module_v2(db, zName, &goModule, (void*) pClientData, goMDestroy); } + +static int _sqlite3_create_module_eponymous_only(sqlite3 *db, const char *zName, uintptr_t pClientData) { + return sqlite3_create_module_v2(db, zName, &goModuleEponymousOnly, (void*) pClientData, goMDestroy); +} */ import "C" @@ -595,6 +627,13 @@ type Module interface { DestroyModule() } +// EponymousOnlyModule is a "virtual table module" (as above), but +// for defining "eponymous only" virtual tables See: https://sqlite.org/vtab.html#eponymous_only_virtual_tables +type EponymousOnlyModule interface { + Module + EponymousOnlyModule() +} + // VTab describes a particular instance of the virtual table. // See: http://sqlite.org/c3ref/vtab.html type VTab interface { @@ -652,9 +691,19 @@ 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._sqlite3_create_module(c.db, mname, C.uintptr_t(uintptr(newHandle(c, &udm)))) - if rv != C.SQLITE_OK { - return c.lastError() + switch module.(type) { + case EponymousOnlyModule: + rv := C._sqlite3_create_module_eponymous_only(c.db, mname, C.uintptr_t(uintptr(newHandle(c, &udm)))) + if rv != C.SQLITE_OK { + return c.lastError() + } + return nil + case Module: + rv := C._sqlite3_create_module(c.db, mname, C.uintptr_t(uintptr(newHandle(c, &udm)))) + if rv != C.SQLITE_OK { + return c.lastError() + } + return nil } return nil } diff --git a/sqlite3_opt_vtable_test.go b/sqlite3_opt_vtable_test.go index b7382dba..fa5b700d 100644 --- a/sqlite3_opt_vtable_test.go +++ b/sqlite3_opt_vtable_test.go @@ -484,3 +484,125 @@ func (c *vtabUpdateCursor) Rowid() (int64, error) { func (c *vtabUpdateCursor) Close() error { return nil } + +type testModuleEponymousOnly struct { + t *testing.T + intarray []int +} + +type testVTabEponymousOnly struct { + intarray []int +} + +type testVTabCursorEponymousOnly struct { + vTab *testVTabEponymousOnly + index int +} + +func (m testModuleEponymousOnly) EponymousOnlyModule() {} + +func (m testModuleEponymousOnly) Create(c *SQLiteConn, args []string) (VTab, error) { + err := c.DeclareVTab("CREATE TABLE x(test INT)") + if err != nil { + return nil, err + } + return &testVTabEponymousOnly{m.intarray}, nil +} + +func (m testModuleEponymousOnly) Connect(c *SQLiteConn, args []string) (VTab, error) { + return m.Create(c, args) +} + +func (m testModuleEponymousOnly) DestroyModule() {} + +func (v *testVTabEponymousOnly) 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 *testVTabEponymousOnly) Disconnect() error { + return nil +} + +func (v *testVTabEponymousOnly) Destroy() error { + return nil +} + +func (v *testVTabEponymousOnly) Open() (VTabCursor, error) { + return &testVTabCursorEponymousOnly{v, 0}, nil +} + +func (vc *testVTabCursorEponymousOnly) Close() error { + return nil +} + +func (vc *testVTabCursorEponymousOnly) Filter(idxNum int, idxStr string, vals []interface{}) error { + vc.index = 0 + return nil +} + +func (vc *testVTabCursorEponymousOnly) Next() error { + vc.index++ + return nil +} + +func (vc *testVTabCursorEponymousOnly) EOF() bool { + return vc.index >= len(vc.vTab.intarray) +} + +func (vc *testVTabCursorEponymousOnly) Column(c *SQLiteContext, 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 *testVTabCursorEponymousOnly) Rowid() (int64, error) { + return int64(vc.index), nil +} + +func TestCreateModuleEponymousOnly(t *testing.T) { + tempFilename := TempFilename(t) + defer os.Remove(tempFilename) + intarray := []int{1, 2, 3} + sql.Register("sqlite3_TestCreateModuleEponymousOnly", &SQLiteDriver{ + ConnectHook: func(conn *SQLiteConn) error { + return conn.CreateModule("test", testModuleEponymousOnly{t, intarray}) + }, + }) + db, err := sql.Open("sqlite3_TestCreateModuleEponymousOnly", tempFilename) + if err != nil { + t.Fatalf("could not open db: %v", err) + } + + var i, value int + rows, err := db.Query("SELECT rowid, * FROM test") + if err != nil { + t.Fatalf("couldn't select from virtual table: %v", err) + } + for rows.Next() { + err := rows.Scan(&i, &value) + if err != nil { + t.Fatal(err) + } + if intarray[i] != value { + t.Fatalf("want %v but %v", intarray[i], value) + } + } + + _, err = db.Exec("DROP TABLE test") + if err != nil { + t.Fatalf("couldn't drop virtual table: %v", err) + } +}