From 232a1dd0e8087c13e005a530342c43766c71a2c0 Mon Sep 17 00:00:00 2001 From: mappu Date: Sat, 8 Jun 2024 13:44:18 +1200 Subject: [PATCH] sqlite: initial support --- go.mod | 1 + go.sum | 2 + main.go | 10 ++++- sqlite.go | 132 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 sqlite.go diff --git a/go.mod b/go.mod index be3ad01..de510a0 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module yvbolt go 1.19 require ( + github.com/mattn/go-sqlite3 v1.14.22 // indirect github.com/ying32/govcl v2.2.3+incompatible // indirect go.etcd.io/bbolt v1.3.10 // indirect golang.org/x/sys v0.4.0 // indirect diff --git a/go.sum b/go.sum index d4ca1e9..04149b1 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/ying32/govcl v2.2.3+incompatible h1:Iyfcl26yNE1USm+3uG+btQyhkoFIV18+VITrUdHu8Lw= github.com/ying32/govcl v2.2.3+incompatible/go.mod h1:yZVtbJ9Md1nAVxtHKIriKZn4K6TQYqI1en3sN/m9FJ8= go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0= diff --git a/main.go b/main.go index 2b9c02a..92b2096 100644 --- a/main.go +++ b/main.go @@ -10,7 +10,6 @@ import ( _ "github.com/ying32/govcl/pkgs/winappres" // Extra _syso files for Windows "github.com/ying32/govcl/vcl" "github.com/ying32/govcl/vcl/types" - ) type TMainForm struct { @@ -47,6 +46,11 @@ func (f *TMainForm) OnFormCreate(sender vcl.IObject) { mnuFileOpen.SetOnClick(f.OnMnuFileOpenClick) mnuFile.Add(mnuFileOpen) + mnuFileSqliteMemory := vcl.NewMenuItem(mnuFile) + mnuFileSqliteMemory.SetCaption("New SQLite in-memory database") + mnuFileSqliteMemory.SetOnClick(f.OnmnuFileSqliteMemoryClick) + mnuFile.Add(mnuFileSqliteMemory) + mnuSep := vcl.NewMenuItem(mnuFile) mnuSep.SetCaption("-") // Creates separator mnuFile.Add(mnuSep) @@ -123,6 +127,10 @@ func (f *TMainForm) OnMnuFileExitClick(sender vcl.IObject) { os.Exit(0) } +func (f *TMainForm) OnmnuFileSqliteMemoryClick(sender vcl.IObject) { + f.SQLite_AddFromFile(`:memory:`) +} + func (f *TMainForm) OnNavChange(sender vcl.IObject, node *vcl.TTreeNode) { if node.Data() == nil { diff --git a/sqlite.go b/sqlite.go new file mode 100644 index 0000000..04d2045 --- /dev/null +++ b/sqlite.go @@ -0,0 +1,132 @@ +package main + +import ( + "database/sql" + "fmt" + "path/filepath" + "unsafe" + + _ "github.com/mattn/go-sqlite3" + "github.com/ying32/govcl/vcl" +) + +const ( + sqliteTablesCaption = "Tables" +) + +type sqliteLoadedDatabase struct { + displayName string + path string + db *sql.DB + nav *vcl.TTreeNode + + arena []*navData // keepalive +} + +func (ld *sqliteLoadedDatabase) DisplayName() string { + return ld.displayName +} + +func (ld *sqliteLoadedDatabase) RootElement() *vcl.TTreeNode { + return ld.nav +} + +func (ld *sqliteLoadedDatabase) Keepalive(ndata *navData) { + ld.arena = append(ld.arena, ndata) +} + +func (ld *sqliteLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) { + + if len(ndata.bucketPath) == 0 { + // Top-level + f.propertiesBox.SetText("Please select...") + f.contentBox.SetEnabled(false) + f.contentBox.Clear() + + } else if len(ndata.bucketPath) == 1 { + // Category (tables, ...) + f.propertiesBox.SetText("Please select...") + f.contentBox.SetEnabled(false) + f.contentBox.Clear() + + } else if ndata.bucketPath[0] == sqliteTablesCaption { + // Render for specific table + f.propertiesBox.SetText(fmt.Sprintf("Selected table %q", ndata.bucketPath[1])) + // Load schema + // Select * with small limit + f.contentBox.SetEnabled(false) + f.contentBox.Clear() + + } else { + // ??? unknown + + } + +} + +func (ld *sqliteLoadedDatabase) NavChildren(ndata *navData) ([]string, error) { + + if len(ndata.bucketPath) == 0 { + // The top-level children are always: + return []string{sqliteTablesCaption}, nil + } + + if ndata.bucketPath[0] == sqliteTablesCaption { + rr, err := ld.db.Query(`SELECT name FROM sqlite_master WHERE type='table' ORDER BY name ASC;`) + if err != nil { + return nil, err + } + defer rr.Close() + + var gather []string + for rr.Next() { + var tableName string + err = rr.Scan(&tableName) + if err != nil { + return nil, err + } + + gather = append(gather, tableName) + } + if rr.Err() != nil { + return nil, rr.Err() + } + + return gather, nil + } + + return nil, fmt.Errorf("unknown nav path %#v", ndata.bucketPath) +} + +var _ loadedDatabase = &sqliteLoadedDatabase{} // interface assertion + +// + +func (f *TMainForm) SQLite_AddFromFile(path string) { + + // TODO load in background thread to stop blocking the UI + db, err := sql.Open("sqlite3", path) + if err != nil { + vcl.ShowMessage(fmt.Sprintf("Failed to load database '%s': %s", path, err.Error())) + return + } + + ld := &sqliteLoadedDatabase{ + path: path, + displayName: filepath.Base(path), + db: db, + } + + ld.nav = f.Buckets.Items().Add(nil, ld.displayName) + ld.nav.SetHasChildren(true) // dynamically populate in OnNavExpanding + navData := &navData{ + ld: ld, + childrenLoaded: false, // will be loaded dynamically + bucketPath: []string{}, // empty = root + } + ld.nav.SetData(unsafe.Pointer(navData)) + + f.dbs = append(f.dbs, ld) + + ld.Keepalive(navData) +}