From f78eec1872656df489e80bfa87012e54ddccc3ce Mon Sep 17 00:00:00 2001 From: mappu Date: Sat, 6 Jul 2024 11:45:41 +1200 Subject: [PATCH] bolt: support editing --- db_badger.go | 5 +++++ db_bolt.go | 52 +++++++++++++++++++++++++++++++++++++++++++++++ db_debconf.go | 5 +++++ db_none.go | 6 ++++++ db_pebble.go | 5 +++++ db_redis.go | 5 +++++ db_sqlite.go | 5 +++++ loadedDatabase.go | 1 + main.go | 20 +++++++++++++++++- 9 files changed, 103 insertions(+), 1 deletion(-) diff --git a/db_badger.go b/db_badger.go index e784dc0..8310a23 100644 --- a/db_badger.go +++ b/db_badger.go @@ -1,6 +1,7 @@ package main import ( + "errors" "fmt" "path/filepath" "unsafe" @@ -83,6 +84,10 @@ func (ld *badgerLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) { f.contentBox.SetEnabled(true) } +func (n *badgerLoadedDatabase) ApplyChanges(f *TMainForm, ndata *navData) error { + return errors.New("Editing is not supported") +} + func (ld *badgerLoadedDatabase) NavChildren(ndata *navData) ([]string, error) { // In the Badger implementation, there is only one child: "Data" if len(ndata.bucketPath) == 0 { diff --git a/db_bolt.go b/db_bolt.go index 208d85d..63891f5 100644 --- a/db_bolt.go +++ b/db_bolt.go @@ -1,6 +1,7 @@ package main import ( + "errors" "fmt" "path/filepath" "sort" @@ -80,6 +81,57 @@ func (ld *boltLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) { f.contentBox.SetEnabled(true) } +func (n *boltLoadedDatabase) ApplyChanges(f *TMainForm, ndata *navData) error { + if n.db.IsReadOnly() { + return errors.New("Database was opened read-only") + } + + // We have rendered row IDs, need to convert back to a bolt primary key + // TODO stash the real key inside f.contentBox.Objects() + // FIXME breaks if you try and edit the primary key(!) + primaryKeyForRendered := func(rowid int32) []byte { + return []byte(f.contentBox.Cells(0, rowid)) + } + + return n.db.Update(func(tx *bbolt.Tx) error { + + // Get current bucket handle + b := boltTargetBucket(tx, ndata.bucketPath) + + // Edit + for rowid, _ /*editcells*/ := range f.updateRows { + k := primaryKeyForRendered(rowid) + v := f.contentBox.Cells(1, rowid) // There's only one value cell + err := b.Put(k, []byte(v)) + if err != nil { + return fmt.Errorf("Updating cell %q: %w", formatUtf8(k), err) + } + } + + // Delete by key (affects rowids after re-render) + for rowid, _ := range f.deleteRows { + k := primaryKeyForRendered(rowid) + err := b.Delete(k) + if err != nil { + return fmt.Errorf("Deleting cell %q: %w", formatUtf8(k), err) + } + } + + // Insert all new entries + for rowid, _ := range f.insertRows { + k := primaryKeyForRendered(rowid) + v := f.contentBox.Cells(1, rowid) // There's only one value cell + err := b.Put(k, []byte(v)) + if err != nil { + return fmt.Errorf("Inserting cell %q: %w", formatUtf8(k), err) + } + } + + // Done + return nil + }) +} + func (ld *boltLoadedDatabase) NavChildren(ndata *navData) ([]string, error) { // In the bolt implementation, the nav is a recursive tree of child buckets return boltChildBucketNames(ld.db, ndata.bucketPath) diff --git a/db_debconf.go b/db_debconf.go index 7361786..236eefc 100644 --- a/db_debconf.go +++ b/db_debconf.go @@ -1,6 +1,7 @@ package main import ( + "errors" "fmt" "os" "path/filepath" @@ -69,6 +70,10 @@ func (ld *debconfLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) { f.contentBox.SetEnabled(true) } +func (n *debconfLoadedDatabase) ApplyChanges(f *TMainForm, ndata *navData) error { + return errors.New("Editing is not supported") +} + func (ld *debconfLoadedDatabase) NavChildren(ndata *navData) ([]string, error) { // In the debconf implementation, there is only one child: "Data" if len(ndata.bucketPath) == 0 { diff --git a/db_none.go b/db_none.go index bf6e8a2..df5de41 100644 --- a/db_none.go +++ b/db_none.go @@ -1,6 +1,8 @@ package main import ( + "errors" + "github.com/ying32/govcl/vcl" ) @@ -22,6 +24,10 @@ func (n *noLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) { f.propertiesBox.SetText("Open a database to get started...") } +func (n *noLoadedDatabase) ApplyChanges(f *TMainForm, ndata *navData) error { + return errors.New("Editing is not supported") +} + func (n *noLoadedDatabase) ExecQuery(query string, resultArea *vcl.TStringGrid) { } diff --git a/db_pebble.go b/db_pebble.go index 5ea8499..e0b6f72 100644 --- a/db_pebble.go +++ b/db_pebble.go @@ -2,6 +2,7 @@ package main import ( "context" + "errors" "fmt" "path/filepath" "unsafe" @@ -74,6 +75,10 @@ func (ld *pebbleLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) { f.contentBox.SetEnabled(true) } +func (n *pebbleLoadedDatabase) ApplyChanges(f *TMainForm, ndata *navData) error { + return errors.New("Editing is not supported") +} + func (ld *pebbleLoadedDatabase) NavChildren(ndata *navData) ([]string, error) { // In the pebble implementation, there is only one child: "Data" if len(ndata.bucketPath) == 0 { diff --git a/db_redis.go b/db_redis.go index e49f3a2..8b70589 100644 --- a/db_redis.go +++ b/db_redis.go @@ -2,6 +2,7 @@ package main import ( "context" + "errors" "fmt" "strconv" "unsafe" @@ -137,6 +138,10 @@ func (ld *redisLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) { } } +func (n *redisLoadedDatabase) ApplyChanges(f *TMainForm, ndata *navData) error { + return errors.New("Editing is not supported") +} + func (ld *redisLoadedDatabase) NavChildren(ndata *navData) ([]string, error) { // ctx := context.Background() diff --git a/db_sqlite.go b/db_sqlite.go index ecc46e6..ed4349b 100644 --- a/db_sqlite.go +++ b/db_sqlite.go @@ -2,6 +2,7 @@ package main import ( "database/sql" + "errors" "fmt" "path/filepath" "unsafe" @@ -96,6 +97,10 @@ func (ld *sqliteLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) { } +func (n *sqliteLoadedDatabase) ApplyChanges(f *TMainForm, ndata *navData) error { + return errors.New("Editing is not supported") +} + func (ld *sqliteLoadedDatabase) sqliteGetColumnNamesForTable(tableName string) ([]string, error) { colr, err := ld.db.Query(`SELECT name FROM pragma_table_info( ? )`, tableName) if err != nil { diff --git a/loadedDatabase.go b/loadedDatabase.go index c94e3cc..88852c7 100644 --- a/loadedDatabase.go +++ b/loadedDatabase.go @@ -19,6 +19,7 @@ type loadedDatabase interface { DriverName() string RootElement() *vcl.TTreeNode RenderForNav(f *TMainForm, ndata *navData) + ApplyChanges(f *TMainForm, ndata *navData) error ExecQuery(query string, resultArea *vcl.TStringGrid) NavChildren(ndata *navData) ([]string, error) NavContext(ndata *navData) ([]contextAction, error) diff --git a/main.go b/main.go index e18b504..2b21195 100644 --- a/main.go +++ b/main.go @@ -647,7 +647,25 @@ func (f *TMainForm) OnDataCommitClick(sender vcl.IObject) { return // Not an active data view } - // TODO + node := f.Buckets.Selected() + if node == nil { + vcl.ShowMessage("No database selected") + return + } + + scrollPos := f.contentBox.TopRow() + + ndata := (*navData)(node.Data()) + err := ndata.ld.ApplyChanges(f, ndata) + if err != nil { + vcl.ShowMessage(err.Error()) + } + + // Refresh content + f.OnNavChange(f.Buckets, node) // Refresh RHS pane/data content + + // Preserve scroll position + f.contentBox.SetTopRow(scrollPos) } func (f *TMainForm) OnNavContextClose(sender vcl.IObject) {