From 650c9e7183df35c93e8170b523570401f4fc652d Mon Sep 17 00:00:00 2001 From: mappu Date: Sat, 29 Jun 2024 12:13:34 +1200 Subject: [PATCH] sqlite: basic integration for the cli driver --- db_sqlite.go | 13 ++++-- main.go | 19 +++++++-- sqliteclidriver/sqliteclidriver.go | 9 ++++ sqliteclidriver/sqliteclidriver_test.go | 55 ++++++++++++++----------- 4 files changed, 64 insertions(+), 32 deletions(-) diff --git a/db_sqlite.go b/db_sqlite.go index 98b608d..e55acfe 100644 --- a/db_sqlite.go +++ b/db_sqlite.go @@ -6,6 +6,8 @@ import ( "path/filepath" "unsafe" + _ "yvbolt/sqliteclidriver" + "github.com/ying32/govcl/vcl" "github.com/ying32/govcl/vcl/types" ) @@ -102,7 +104,7 @@ func (ld *sqliteLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) { } func (ld *sqliteLoadedDatabase) sqliteGetColumnNamesForTable(tableName string) ([]string, error) { - colr, err := ld.db.Query(`SELECT name FROM pragma_table_info(?)`, tableName) + colr, err := ld.db.Query(`SELECT name FROM pragma_table_info( ? )`, tableName) if err != nil { return nil, fmt.Errorf("Query: %w", err) } @@ -243,10 +245,15 @@ var _ loadedDatabase = &sqliteLoadedDatabase{} // interface assertion // -func (f *TMainForm) sqliteAddDatabaseFromFile(path string) { +func (f *TMainForm) sqliteAddDatabaseFromFile(path string, cliDriver bool) { + + driver := "sqlite3" + if cliDriver { + driver = "sqliteclidriver" + } // TODO load in background thread to stop blocking the UI - db, err := sql.Open("sqlite3", path) + db, err := sql.Open(driver, path) if err != nil { vcl.ShowMessage(fmt.Sprintf("Failed to load database '%s': %s", path, err.Error())) return diff --git a/main.go b/main.go index db597b6..3aacee6 100644 --- a/main.go +++ b/main.go @@ -21,8 +21,11 @@ const ( type TMainForm struct { *vcl.TForm - ImageList *vcl.TImageList - Menu *vcl.TMainMenu + ImageList *vcl.TImageList + Menu *vcl.TMainMenu + + SQLiteUseCliDriver *vcl.TMenuItem + StatusBar *vcl.TStatusBar Buckets *vcl.TTreeView Tabs *vcl.TPageControl @@ -103,6 +106,10 @@ func (f *TMainForm) OnFormCreate(sender vcl.IObject) { vcl_menuitem(mnuFileSqlite, "Open database...", imgDatabaseAdd, f.OnMnuFileSqliteOpenClick) vcl_menuitem(mnuFileSqlite, "New in-memory database", imgDatabaseAdd, f.OnMnuFileSqliteMemoryClick) + vcl_menuseparator(mnuFileSqlite) + f.SQLiteUseCliDriver = vcl_menuitem(mnuFileSqlite, "Connect using CLI driver (experimental)", -1, nil) + f.SQLiteUseCliDriver.SetAutoCheck(true) + // vcl_menuseparator(mnuFile) @@ -282,12 +289,14 @@ func (f *TMainForm) OnMnuFileBoltOpenReadonlyClick(sender vcl.IObject) { } func (f *TMainForm) OnMnuFileSqliteOpenClick(sender vcl.IObject) { + cliDriver := f.SQLiteUseCliDriver.Checked() + dlg := vcl.NewOpenDialog(f) dlg.SetTitle("Select a database file...") dlg.SetFilter("SQLite database|*.db;*.db3;*.sqlite;*.sqlite3|All files|*.*") ret := dlg.Execute() // Fake blocking if ret { - f.sqliteAddDatabaseFromFile(dlg.FileName()) + f.sqliteAddDatabaseFromFile(dlg.FileName(), cliDriver) } } @@ -318,7 +327,9 @@ func (f *TMainForm) OnMnuFilePebbleMemoryClick(sender vcl.IObject) { } func (f *TMainForm) OnMnuFileSqliteMemoryClick(sender vcl.IObject) { - f.sqliteAddDatabaseFromFile(`:memory:`) + cliDriver := f.SQLiteUseCliDriver.Checked() + + f.sqliteAddDatabaseFromFile(`:memory:`, cliDriver) } func (f *TMainForm) OnMnuFileRedisConnectClick(sender vcl.IObject) { diff --git a/sqliteclidriver/sqliteclidriver.go b/sqliteclidriver/sqliteclidriver.go index cfb32f0..a6cb9d1 100644 --- a/sqliteclidriver/sqliteclidriver.go +++ b/sqliteclidriver/sqliteclidriver.go @@ -1,7 +1,16 @@ // sqliteclidriver is a database/sql driver for SQLite implemented on top of // the sqlite3 command-line tool. This allows it to be used over remote SSH // connections. +// // Functionality is limited. +// +// Known caveats: +// - Lexer only understands ? if it's separated by spaces +// - Bad error handling +// - Few supported types +// - Has to escape parameters for CLI instead of preparing them, so not safe for untrusted usage +// - No context handling +// - No way to configure sqlite3 command line path package sqliteclidriver import ( diff --git a/sqliteclidriver/sqliteclidriver_test.go b/sqliteclidriver/sqliteclidriver_test.go index b531100..bfbeb32 100644 --- a/sqliteclidriver/sqliteclidriver_test.go +++ b/sqliteclidriver/sqliteclidriver_test.go @@ -14,39 +14,44 @@ func TestSqliteCliDriver(t *testing.T) { _, err = db.Exec(`CREATE TABLE my_test_table ( id INTEGER PRIMARY KEY, extra TEXT NOT NULL );`) require.NoError(t, err) - _, err = db.Exec(`INSERT INTO my_test_table (id, extra) VALUES (1337, "abcdef"), (9001, "whoop");`) + _, err = db.Exec(`INSERT INTO my_test_table (id, extra) VALUES ( ? , ? ), ( ? , ? );`, 1337, "abcdef", 9001, "whoop") require.NoError(t, err) - res, err := db.Query(`SELECT * FROM my_test_table ORDER BY id ASC;`) - require.NoError(t, err) + // Repeat this part to ensure we can make followup queries on the same connection + for i := 0; i < 3; i++ { - cols, err := res.Columns() - require.NoError(t, err) - require.EqualValues(t, cols, []string{"id", "extra"}) + res, err := db.Query(`SELECT * FROM my_test_table ORDER BY id ASC;`) + require.NoError(t, err) - var rowCount int = 0 + cols, err := res.Columns() + require.NoError(t, err) + require.EqualValues(t, cols, []string{"id", "extra"}) - for res.Next() { - rowCount++ + var rowCount int = 0 - var idVal int - var extraVal string - err = res.Scan(&idVal, &extraVal) - if err != nil { - t.Fatal(err) + for res.Next() { + rowCount++ + + var idVal int + var extraVal string + err = res.Scan(&idVal, &extraVal) + if err != nil { + t.Fatal(err) + } + + switch rowCount { + case 1: + require.EqualValues(t, 1337, idVal) + require.EqualValues(t, "abcdef", extraVal) + + case 2: + require.EqualValues(t, 9001, idVal) + require.EqualValues(t, "whoop", extraVal) + + } } - switch rowCount { - case 1: - require.EqualValues(t, 1337, idVal) - require.EqualValues(t, "abcdef", extraVal) + require.Equal(t, rowCount, 2) - case 2: - require.EqualValues(t, 9001, idVal) - require.EqualValues(t, "whoop", extraVal) - - } } - - require.Equal(t, rowCount, 2) }