Compare commits
No commits in common. "master" and "v0.6.0" have entirely different histories.
2
Makefile
|
@ -1,6 +1,6 @@
|
||||||
SHELL:=/bin/bash
|
SHELL:=/bin/bash
|
||||||
|
|
||||||
SOURCES=$(find . -name '*.go' -type f)
|
SOURCES=$(find . -name '*.go' -type f)
|
||||||
.DEFAULT_GOAL := dist
|
|
||||||
|
|
||||||
liblcl-2.2.3.zip:
|
liblcl-2.2.3.zip:
|
||||||
rm -f liblcl-2.2.3.zip
|
rm -f liblcl-2.2.3.zip
|
||||||
|
|
32
README.md
|
@ -1,11 +1,13 @@
|
||||||
# yvbolt
|
# yvbolt
|
||||||
|
|
||||||
A graphical interface for multiple databases using [GoVCL](https://z-kit.cc/en/).
|
A graphical browser for multiple databases using [GoVCL](https://z-kit.cc/en/).
|
||||||
|
|
||||||
|
This is an experimental application and you should generally prefer to use [qbolt](https://code.ivysaur.me/qbolt).
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Native desktop application, running on Linux, Windows, and macOS
|
- Native desktop application, running on Linux, Windows, and macOS
|
||||||
- Connect to multiple databases at once
|
- Connect to multiple databases
|
||||||
- Browse table/bucket content
|
- Browse table/bucket content
|
||||||
- Use context menu to perform special table/bucket actions
|
- Use context menu to perform special table/bucket actions
|
||||||
- Run custom SQL queries
|
- Run custom SQL queries
|
||||||
|
@ -15,16 +17,12 @@ A graphical interface for multiple databases using [GoVCL](https://z-kit.cc/en/)
|
||||||
- Badger v4
|
- Badger v4
|
||||||
- Bolt
|
- Bolt
|
||||||
- Recursive bucket support
|
- Recursive bucket support
|
||||||
- Option to open as readonly for shared access
|
- Option to open as readonly
|
||||||
- Supports editing
|
|
||||||
- See also [qbolt](https://code.ivysaur.me/qbolt) for more/different functionality
|
|
||||||
- Debconf
|
- Debconf
|
||||||
- Pebble
|
- Pebble
|
||||||
- Redis
|
- Redis
|
||||||
- SQLite
|
- SQLite
|
||||||
- Drivers: mattn (CGo), modernc.org (no cgo), experimental command-line driver
|
- Drivers: mattn (CGo), modernc.org (no cgo), experimental command-line driver
|
||||||
- Supports editing
|
|
||||||
- Integrated vacuum and export commands
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
@ -34,7 +32,7 @@ This project redistributes images from the famfamfam/silk icon set under the [CC
|
||||||
|
|
||||||
This project includes trademarked logo images for each supported database type.
|
This project includes trademarked logo images for each supported database type.
|
||||||
|
|
||||||
## Compiling
|
## Usage
|
||||||
|
|
||||||
1. `CGO_ENABLED=1 go build`
|
1. `CGO_ENABLED=1 go build`
|
||||||
2. [Download liblcl](https://github.com/ying32/govcl/releases/download/v2.2.3/liblcl-2.2.3.zip) for your platform, or [compile it yourself](https://github.com/ying32/liblcl) (tested with v2.2.3)
|
2. [Download liblcl](https://github.com/ying32/govcl/releases/download/v2.2.3/liblcl-2.2.3.zip) for your platform, or [compile it yourself](https://github.com/ying32/liblcl) (tested with v2.2.3)
|
||||||
|
@ -43,20 +41,6 @@ This project includes trademarked logo images for each supported database type.
|
||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
2024-07-18 v0.7.0
|
|
||||||
|
|
||||||
- SQLite, Bolt: Initial support for editing data (insert, per-cell update, delete)
|
|
||||||
- SQLite: Add context menu actions for compact (vacuum), export, and drop table
|
|
||||||
- App: New grid widget
|
|
||||||
- App: Add refresh button
|
|
||||||
- App: Bigger window size, use icons for toolbars, better UI colours for Windows
|
|
||||||
- App: Prevent submitting blank queries to database
|
|
||||||
- Refactor database interface and error handling
|
|
||||||
|
|
||||||
[⬇️ Download for Windows x86_64](https://git.ivysaur.me/code.ivysaur.me/yvbolt/releases/download/v0.7.0/yvbolt.win64.zip)
|
|
||||||
|
|
||||||
[⬇️ Download for Linux x86_64](https://git.ivysaur.me/code.ivysaur.me/yvbolt/releases/download/v0.7.0/yvbolt.linux64.tar.xz)
|
|
||||||
|
|
||||||
2024-06-30 v0.6.0
|
2024-06-30 v0.6.0
|
||||||
|
|
||||||
- Debconf: Add as supported database
|
- Debconf: Add as supported database
|
||||||
|
@ -67,10 +51,6 @@ This project includes trademarked logo images for each supported database type.
|
||||||
- Build: Change compression parameters for release builds
|
- Build: Change compression parameters for release builds
|
||||||
- Build: Compile CGO with -O2 for release builds
|
- Build: Compile CGO with -O2 for release builds
|
||||||
|
|
||||||
[⬇️ Download for Windows x86_64](https://git.ivysaur.me/code.ivysaur.me/yvbolt/releases/download/v0.6.0/yvbolt.win64.zip)
|
|
||||||
|
|
||||||
[⬇️ Download for Linux x86_64](https://git.ivysaur.me/code.ivysaur.me/yvbolt/releases/download/v0.6.0/yvbolt.linux64.tar.xz)
|
|
||||||
|
|
||||||
2024-06-29 v0.5.0
|
2024-06-29 v0.5.0
|
||||||
|
|
||||||
- Pebble: Add as supported database
|
- Pebble: Add as supported database
|
||||||
|
|
27
TODO
|
@ -1,10 +1,6 @@
|
||||||
- Syntax highlighting in editor
|
- Insert
|
||||||
- Mutation
|
- Update cell
|
||||||
- Get real primary key for mutation instead of string approximation
|
- Delete row(s)
|
||||||
- Badger: Support insert/update/delete
|
|
||||||
- Pebble: Support insert/update/delete
|
|
||||||
- Debconf: Support insert/update/delete
|
|
||||||
- Redis: Support insert/update/delete
|
|
||||||
- Binary data viewer
|
- Binary data viewer
|
||||||
- Detect jpg/png and show as image
|
- Detect jpg/png and show as image
|
||||||
- More DB types
|
- More DB types
|
||||||
|
@ -14,19 +10,19 @@
|
||||||
- MSSQL (recursive navigation for instances)
|
- MSSQL (recursive navigation for instances)
|
||||||
- Other K/V stores from https://github.com/smallnest/kvbench
|
- Other K/V stores from https://github.com/smallnest/kvbench
|
||||||
- Windows registry
|
- Windows registry
|
||||||
- LDAP
|
- SSH tunnels
|
||||||
- Connection dialog
|
- Badger encryption key dialog
|
||||||
- SSH tunnels
|
- Pebble: connection options dialog
|
||||||
- Badger encryption key dialog
|
- SQLite: DB compact action
|
||||||
- Pebble: connection options dialog
|
- SQLite: DB export backup action
|
||||||
|
- SQLite: drop table action
|
||||||
|
- SQLite: show views, triggers, indexes in nav
|
||||||
- SQLite CLI driver:
|
- SQLite CLI driver:
|
||||||
- Context support
|
|
||||||
- Attach to SSH tunnel
|
- Attach to SSH tunnel
|
||||||
- Configure binary path
|
- Configure binary path
|
||||||
- https://ricardoanderegg.com/posts/sqlite-remote-explorer-gui/
|
- https://ricardoanderegg.com/posts/sqlite-remote-explorer-gui/
|
||||||
- https://github.com/litements/litexplore
|
- https://github.com/litements/litexplore
|
||||||
- Debconf: separate groups by first slash in name
|
- Debconf: separate groups by first slash in name
|
||||||
- SQLite: drop table doesn't autorefresh nav since callback is late
|
|
||||||
- Build
|
- Build
|
||||||
- Build own liblcl binaries in docker
|
- Build own liblcl binaries in docker
|
||||||
- Win32 icon resource
|
- Win32 icon resource
|
||||||
|
@ -36,6 +32,3 @@
|
||||||
- Context/interrupt slow queries
|
- Context/interrupt slow queries
|
||||||
- Faster virtual rendering
|
- Faster virtual rendering
|
||||||
- https://github.com/ying32/govcl/blob/master/samples/listviewvirtualdata/main.go
|
- https://github.com/ying32/govcl/blob/master/samples/listviewvirtualdata/main.go
|
||||||
- Query history
|
|
||||||
- Test suite
|
|
||||||
- `CREATE TABLE foo (id integer primary key, aaa text not null, bbb text not null);`
|
|
||||||
|
|
BIN
assets/add.png
Before Width: | Height: | Size: 733 B |
Before Width: | Height: | Size: 715 B |
Before Width: | Height: | Size: 739 B |
Before Width: | Height: | Size: 450 B |
Before Width: | Height: | Size: 589 B |
Before Width: | Height: | Size: 603 B |
Before Width: | Height: | Size: 666 B |
Before Width: | Height: | Size: 395 B |
33
db_badger.go
|
@ -7,6 +7,7 @@ import (
|
||||||
|
|
||||||
"github.com/dgraph-io/badger/v4"
|
"github.com/dgraph-io/badger/v4"
|
||||||
"github.com/ying32/govcl/vcl"
|
"github.com/ying32/govcl/vcl"
|
||||||
|
"github.com/ying32/govcl/vcl/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
type badgerLoadedDatabase struct {
|
type badgerLoadedDatabase struct {
|
||||||
|
@ -33,7 +34,7 @@ func (ld *badgerLoadedDatabase) Keepalive(ndata *navData) {
|
||||||
ld.arena = append(ld.arena, ndata)
|
ld.arena = append(ld.arena, ndata)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ld *badgerLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) error {
|
func (ld *badgerLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) {
|
||||||
|
|
||||||
// Load properties
|
// Load properties
|
||||||
|
|
||||||
|
@ -41,15 +42,25 @@ func (ld *badgerLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) error
|
||||||
f.propertiesBox.SetText(content)
|
f.propertiesBox.SetText(content)
|
||||||
|
|
||||||
// Load data
|
// Load data
|
||||||
|
|
||||||
|
f.contentBox.SetEnabled(false)
|
||||||
|
f.contentBox.Clear()
|
||||||
|
|
||||||
// Badger always uses Key + Value as the columns
|
// Badger always uses Key + Value as the columns
|
||||||
|
|
||||||
|
f.contentBox.Columns().Clear()
|
||||||
colKey := f.contentBox.Columns().Add()
|
colKey := f.contentBox.Columns().Add()
|
||||||
colKey.Title().SetCaption("Key")
|
colKey.SetCaption("Key")
|
||||||
|
colKey.SetWidth(MY_WIDTH)
|
||||||
|
colKey.SetAlignment(types.TaLeftJustify)
|
||||||
colVal := f.contentBox.Columns().Add()
|
colVal := f.contentBox.Columns().Add()
|
||||||
colVal.Title().SetCaption("Value")
|
colVal.SetCaption("Value")
|
||||||
|
|
||||||
err := ld.db.View(func(txn *badger.Txn) error {
|
err := ld.db.View(func(txn *badger.Txn) error {
|
||||||
|
|
||||||
|
// Valid
|
||||||
|
f.contentBox.Clear()
|
||||||
|
|
||||||
// Create iterator
|
// Create iterator
|
||||||
opts := badger.DefaultIteratorOptions
|
opts := badger.DefaultIteratorOptions
|
||||||
opts.PrefetchSize = 64
|
opts.PrefetchSize = 64
|
||||||
|
@ -60,10 +71,9 @@ func (ld *badgerLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) error
|
||||||
item := it.Item()
|
item := it.Item()
|
||||||
k := item.Key()
|
k := item.Key()
|
||||||
err := item.Value(func(v []byte) error {
|
err := item.Value(func(v []byte) error {
|
||||||
rpos := f.contentBox.RowCount()
|
dataEntry := f.contentBox.Items().Add()
|
||||||
f.contentBox.SetRowCount(rpos + 1)
|
dataEntry.SetCaption(formatUtf8(k))
|
||||||
f.contentBox.SetCells(0, rpos, formatUtf8(k))
|
dataEntry.SubItems().Add(formatUtf8(v))
|
||||||
f.contentBox.SetCells(1, rpos, formatUtf8(v))
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
@ -74,13 +84,12 @@ func (ld *badgerLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) error
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
vcl.ShowMessage(fmt.Sprintf("Failed to load data: %s", err.Error()))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Valid
|
// Valid
|
||||||
vcl_stringgrid_columnwidths(f.contentBox)
|
|
||||||
f.contentBox.SetEnabled(true)
|
f.contentBox.SetEnabled(true)
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ld *badgerLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
|
func (ld *badgerLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
|
||||||
|
@ -98,6 +107,10 @@ func (ld *badgerLoadedDatabase) NavContext(ndata *navData) ([]contextAction, err
|
||||||
return nil, nil // No special actions are supported
|
return nil, nil // No special actions are supported
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ld *badgerLoadedDatabase) ExecQuery(query string, resultArea *vcl.TListView) {
|
||||||
|
vcl.ShowMessage("Badger doesn't support querying")
|
||||||
|
}
|
||||||
|
|
||||||
func (ld *badgerLoadedDatabase) Close() {
|
func (ld *badgerLoadedDatabase) Close() {
|
||||||
_ = ld.db.Close()
|
_ = ld.db.Close()
|
||||||
ld.arena = nil
|
ld.arena = nil
|
||||||
|
|
102
db_bolt.go
|
@ -1,7 +1,6 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
|
@ -10,14 +9,11 @@ import (
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"github.com/ying32/govcl/vcl"
|
"github.com/ying32/govcl/vcl"
|
||||||
|
"github.com/ying32/govcl/vcl/types"
|
||||||
"go.etcd.io/bbolt"
|
"go.etcd.io/bbolt"
|
||||||
"go.etcd.io/bbolt/version"
|
"go.etcd.io/bbolt/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
boltFilter = "Bolt database|*.db|All files|*.*"
|
|
||||||
)
|
|
||||||
|
|
||||||
type boltLoadedDatabase struct {
|
type boltLoadedDatabase struct {
|
||||||
displayName string
|
displayName string
|
||||||
path string
|
path string
|
||||||
|
@ -43,7 +39,7 @@ func (ld *boltLoadedDatabase) Keepalive(ndata *navData) {
|
||||||
ld.arena = append(ld.arena, ndata)
|
ld.arena = append(ld.arena, ndata)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ld *boltLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) error {
|
func (ld *boltLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) {
|
||||||
|
|
||||||
// Load properties
|
// Load properties
|
||||||
|
|
||||||
|
@ -52,11 +48,19 @@ func (ld *boltLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) error {
|
||||||
f.propertiesBox.SetText(content)
|
f.propertiesBox.SetText(content)
|
||||||
|
|
||||||
// Load data
|
// Load data
|
||||||
|
|
||||||
|
f.contentBox.SetEnabled(false)
|
||||||
|
f.contentBox.Clear()
|
||||||
|
|
||||||
// Bolt always uses Key + Value as the columns
|
// Bolt always uses Key + Value as the columns
|
||||||
|
|
||||||
|
f.contentBox.Columns().Clear()
|
||||||
colKey := f.contentBox.Columns().Add()
|
colKey := f.contentBox.Columns().Add()
|
||||||
colKey.Title().SetCaption("Key")
|
colKey.SetCaption("Key")
|
||||||
|
colKey.SetWidth(MY_WIDTH)
|
||||||
|
colKey.SetAlignment(types.TaLeftJustify)
|
||||||
colVal := f.contentBox.Columns().Add()
|
colVal := f.contentBox.Columns().Add()
|
||||||
colVal.Title().SetCaption("Value")
|
colVal.SetCaption("Value")
|
||||||
|
|
||||||
err := ld.db.View(func(tx *bbolt.Tx) error {
|
err := ld.db.View(func(tx *bbolt.Tx) error {
|
||||||
b := boltTargetBucket(tx, ndata.bucketPath)
|
b := boltTargetBucket(tx, ndata.bucketPath)
|
||||||
|
@ -66,74 +70,23 @@ func (ld *boltLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Valid
|
// Valid
|
||||||
|
f.contentBox.Clear()
|
||||||
|
|
||||||
c := b.Cursor()
|
c := b.Cursor()
|
||||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||||
rpos := f.contentBox.RowCount()
|
dataEntry := f.contentBox.Items().Add()
|
||||||
f.contentBox.SetRowCount(rpos + 1)
|
dataEntry.SetCaption(formatUtf8(k))
|
||||||
f.contentBox.SetCells(0, rpos, formatUtf8(k))
|
dataEntry.SubItems().Add(formatUtf8(v))
|
||||||
f.contentBox.SetCells(1, rpos, formatUtf8(v))
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
vcl.ShowMessage(fmt.Sprintf("Failed to load data for bucket %q: %s", bucketDisplayName, err.Error()))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Valid
|
// Valid
|
||||||
vcl_stringgrid_columnwidths(f.contentBox)
|
|
||||||
f.contentBox.SetEnabled(true)
|
f.contentBox.SetEnabled(true)
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
func (ld *boltLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
|
||||||
|
@ -147,14 +100,13 @@ func (ld *boltLoadedDatabase) NavContext(ndata *navData) (ret []contextAction, e
|
||||||
if len(ndata.bucketPath) > 0 {
|
if len(ndata.bucketPath) > 0 {
|
||||||
ret = append(ret, contextAction{"Delete bucket", ld.DeleteBucket})
|
ret = append(ret, contextAction{"Delete bucket", ld.DeleteBucket})
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ld *boltLoadedDatabase) AddChildBucket(sender vcl.IComponent, ndata *navData) error {
|
func (ld *boltLoadedDatabase) AddChildBucket(ndata *navData) {
|
||||||
bucketName := ""
|
bucketName := ""
|
||||||
if !vcl.InputQuery(APPNAME, "Enter a name for the new bucket:", &bucketName) {
|
if !vcl.InputQuery(APPNAME, "Enter a name for the new bucket:", &bucketName) {
|
||||||
return nil // cancel
|
return // cancel
|
||||||
}
|
}
|
||||||
|
|
||||||
err := ld.db.Update(func(tx *bbolt.Tx) error {
|
err := ld.db.Update(func(tx *bbolt.Tx) error {
|
||||||
|
@ -169,12 +121,11 @@ func (ld *boltLoadedDatabase) AddChildBucket(sender vcl.IComponent, ndata *navDa
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Error adding bucket: %w", err)
|
vcl.ShowMessageFmt("Error adding bucket: %v", err)
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ld *boltLoadedDatabase) DeleteBucket(sender vcl.IComponent, ndata *navData) error {
|
func (ld *boltLoadedDatabase) DeleteBucket(ndata *navData) {
|
||||||
err := ld.db.Update(func(tx *bbolt.Tx) error {
|
err := ld.db.Update(func(tx *bbolt.Tx) error {
|
||||||
// Find parent of this bucket.
|
// Find parent of this bucket.
|
||||||
if len(ndata.bucketPath) >= 2 {
|
if len(ndata.bucketPath) >= 2 {
|
||||||
|
@ -187,9 +138,12 @@ func (ld *boltLoadedDatabase) DeleteBucket(sender vcl.IComponent, ndata *navData
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Error deleting bucket %q: %w", strings.Join(ndata.bucketPath, `/`), err)
|
vcl.ShowMessageFmt("Error deleting bucket %q: %v", strings.Join(ndata.bucketPath, `/`), err)
|
||||||
}
|
}
|
||||||
return nil
|
}
|
||||||
|
|
||||||
|
func (ld *boltLoadedDatabase) ExecQuery(query string, resultArea *vcl.TListView) {
|
||||||
|
vcl.ShowMessage("Bolt doesn't support querying")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ld *boltLoadedDatabase) Close() {
|
func (ld *boltLoadedDatabase) Close() {
|
||||||
|
|
|
@ -35,7 +35,7 @@ func (ld *debconfLoadedDatabase) Keepalive(ndata *navData) {
|
||||||
ld.arena = append(ld.arena, ndata)
|
ld.arena = append(ld.arena, ndata)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ld *debconfLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) error {
|
func (ld *debconfLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) {
|
||||||
|
|
||||||
// Load properties
|
// Load properties
|
||||||
|
|
||||||
|
@ -44,30 +44,36 @@ func (ld *debconfLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) erro
|
||||||
|
|
||||||
// Load data
|
// Load data
|
||||||
|
|
||||||
|
f.contentBox.SetEnabled(false)
|
||||||
|
f.contentBox.Clear()
|
||||||
|
|
||||||
|
// debconf always uses Key + Value as the columns
|
||||||
|
|
||||||
indexes := make(map[string]int)
|
indexes := make(map[string]int)
|
||||||
|
|
||||||
|
f.contentBox.Columns().Clear()
|
||||||
for i, cname := range ld.db.AllColumnNames {
|
for i, cname := range ld.db.AllColumnNames {
|
||||||
indexes[cname] = i
|
indexes[cname] = i
|
||||||
|
|
||||||
col := f.contentBox.Columns().Add()
|
col := f.contentBox.Columns().Add()
|
||||||
col.Title().SetCaption(cname)
|
col.SetCaption(cname)
|
||||||
|
col.SetWidth(MY_WIDTH)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, entry := range ld.db.Entries {
|
for _, entry := range ld.db.Entries {
|
||||||
|
|
||||||
rpos := f.contentBox.RowCount()
|
cell := f.contentBox.Items().Add()
|
||||||
f.contentBox.SetRowCount(rpos + 1)
|
cell.SetCaption(entry.Name)
|
||||||
f.contentBox.SetCells(0, rpos, entry.Name)
|
|
||||||
|
|
||||||
|
texts := make([]string, len(ld.db.AllColumnNames))
|
||||||
for _, proppair := range entry.Properties {
|
for _, proppair := range entry.Properties {
|
||||||
f.contentBox.SetCells(int32(indexes[proppair[0]]), rpos, proppair[1])
|
texts[indexes[proppair[0]]-1 /* compensate for 'Name' always being first */] = proppair[1]
|
||||||
}
|
}
|
||||||
|
cell.SubItems().AddStrings2(texts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Valid
|
// Valid
|
||||||
vcl_stringgrid_columnwidths(f.contentBox)
|
|
||||||
f.contentBox.SetEnabled(true)
|
f.contentBox.SetEnabled(true)
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ld *debconfLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
|
func (ld *debconfLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
|
||||||
|
@ -85,6 +91,10 @@ func (ld *debconfLoadedDatabase) NavContext(ndata *navData) ([]contextAction, er
|
||||||
return nil, nil // No special actions are supported
|
return nil, nil // No special actions are supported
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ld *debconfLoadedDatabase) ExecQuery(query string, resultArea *vcl.TListView) {
|
||||||
|
vcl.ShowMessage("debconf doesn't support querying")
|
||||||
|
}
|
||||||
|
|
||||||
func (ld *debconfLoadedDatabase) Close() {
|
func (ld *debconfLoadedDatabase) Close() {
|
||||||
ld.arena = nil
|
ld.arena = nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,9 +18,13 @@ func (n *noLoadedDatabase) RootElement() *vcl.TTreeNode {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *noLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) error {
|
func (n *noLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) {
|
||||||
f.propertiesBox.SetText("Open a database to get started...")
|
f.propertiesBox.SetText("Open a database to get started...")
|
||||||
return nil
|
f.contentBox.SetEnabled(false)
|
||||||
|
f.contentBox.Clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *noLoadedDatabase) ExecQuery(query string, resultArea *vcl.TListView) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *noLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
|
func (n *noLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
|
||||||
|
|
30
db_pebble.go
|
@ -9,6 +9,7 @@ import (
|
||||||
"github.com/cockroachdb/pebble"
|
"github.com/cockroachdb/pebble"
|
||||||
"github.com/cockroachdb/pebble/vfs"
|
"github.com/cockroachdb/pebble/vfs"
|
||||||
"github.com/ying32/govcl/vcl"
|
"github.com/ying32/govcl/vcl"
|
||||||
|
"github.com/ying32/govcl/vcl/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
type pebbleLoadedDatabase struct {
|
type pebbleLoadedDatabase struct {
|
||||||
|
@ -35,7 +36,7 @@ func (ld *pebbleLoadedDatabase) Keepalive(ndata *navData) {
|
||||||
ld.arena = append(ld.arena, ndata)
|
ld.arena = append(ld.arena, ndata)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ld *pebbleLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) error {
|
func (ld *pebbleLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) {
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
@ -45,12 +46,19 @@ func (ld *pebbleLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) error
|
||||||
f.propertiesBox.SetText(content)
|
f.propertiesBox.SetText(content)
|
||||||
|
|
||||||
// Load data
|
// Load data
|
||||||
|
|
||||||
|
f.contentBox.SetEnabled(false)
|
||||||
|
f.contentBox.Clear()
|
||||||
|
|
||||||
// pebble always uses Key + Value as the columns
|
// pebble always uses Key + Value as the columns
|
||||||
|
|
||||||
|
f.contentBox.Columns().Clear()
|
||||||
colKey := f.contentBox.Columns().Add()
|
colKey := f.contentBox.Columns().Add()
|
||||||
colKey.Title().SetCaption("Key")
|
colKey.SetCaption("Key")
|
||||||
|
colKey.SetWidth(MY_WIDTH)
|
||||||
|
colKey.SetAlignment(types.TaLeftJustify)
|
||||||
colVal := f.contentBox.Columns().Add()
|
colVal := f.contentBox.Columns().Add()
|
||||||
colVal.Title().SetCaption("Value")
|
colVal.SetCaption("Value")
|
||||||
|
|
||||||
itr := ld.db.NewIterWithContext(ctx, nil)
|
itr := ld.db.NewIterWithContext(ctx, nil)
|
||||||
defer itr.Close()
|
defer itr.Close()
|
||||||
|
@ -59,19 +67,17 @@ func (ld *pebbleLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) error
|
||||||
k := itr.Key()
|
k := itr.Key()
|
||||||
v, err := itr.ValueAndErr()
|
v, err := itr.ValueAndErr()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to load data for key %q: %w", formatAny(k), err)
|
vcl.ShowMessage(fmt.Sprintf("Failed to load data for key %q: %s", formatAny(k), err.Error()))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
rpos := f.contentBox.RowCount()
|
dataEntry := f.contentBox.Items().Add()
|
||||||
f.contentBox.SetRowCount(rpos + 1)
|
dataEntry.SetCaption(formatUtf8(k))
|
||||||
f.contentBox.SetCells(0, rpos, formatUtf8(k))
|
dataEntry.SubItems().Add(formatUtf8(v))
|
||||||
f.contentBox.SetCells(0, rpos, formatUtf8(v))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Valid
|
// Valid
|
||||||
vcl_stringgrid_columnwidths(f.contentBox)
|
|
||||||
f.contentBox.SetEnabled(true)
|
f.contentBox.SetEnabled(true)
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ld *pebbleLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
|
func (ld *pebbleLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
|
||||||
|
@ -89,6 +95,10 @@ func (ld *pebbleLoadedDatabase) NavContext(ndata *navData) ([]contextAction, err
|
||||||
return nil, nil // No special actions are supported
|
return nil, nil // No special actions are supported
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ld *pebbleLoadedDatabase) ExecQuery(query string, resultArea *vcl.TListView) {
|
||||||
|
vcl.ShowMessage("pebble doesn't support querying")
|
||||||
|
}
|
||||||
|
|
||||||
func (ld *pebbleLoadedDatabase) Close() {
|
func (ld *pebbleLoadedDatabase) Close() {
|
||||||
_ = ld.db.Close()
|
_ = ld.db.Close()
|
||||||
ld.arena = nil
|
ld.arena = nil
|
||||||
|
|
84
db_redis.go
|
@ -10,6 +10,7 @@ import (
|
||||||
|
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
"github.com/ying32/govcl/vcl"
|
"github.com/ying32/govcl/vcl"
|
||||||
|
"github.com/ying32/govcl/vcl/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
type redisLoadedDatabase struct {
|
type redisLoadedDatabase struct {
|
||||||
|
@ -40,71 +41,83 @@ func (ld *redisLoadedDatabase) Keepalive(ndata *navData) {
|
||||||
ld.arena = append(ld.arena, ndata)
|
ld.arena = append(ld.arena, ndata)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ld *redisLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) error {
|
func (ld *redisLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
if len(ndata.bucketPath) == 0 {
|
if len(ndata.bucketPath) == 0 {
|
||||||
// Top-level: Show info() on main Properties tab
|
// Top-level: Show info() on main Properties tab
|
||||||
infostr, err := ld.db.Info(ctx).Result()
|
infostr, err := ld.db.Info(ctx).Result()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Retreiving database info: %w", err)
|
vcl.ShowMessage(fmt.Sprintf("Retreiving database info: %v", err))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
f.propertiesBox.SetText(infostr)
|
f.propertiesBox.SetText(infostr)
|
||||||
|
|
||||||
// Leave data tab disabled (default behaviour)
|
// Disable data tab
|
||||||
return nil
|
f.contentBox.SetEnabled(false)
|
||||||
|
f.contentBox.Clear()
|
||||||
|
|
||||||
} else if len(ndata.bucketPath) == 1 {
|
} else if len(ndata.bucketPath) == 1 {
|
||||||
// One selected database
|
// One selected database
|
||||||
// Figure out its content
|
// Figure out its content
|
||||||
err := ld.db.Do(ctx, "SELECT", ndata.bucketPath[0]).Err()
|
err := ld.db.Do(ctx, "SELECT", ndata.bucketPath[0]).Err()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Switching to database %q: %w", ndata.bucketPath[0], err)
|
vcl.ShowMessage(fmt.Sprintf("Switching to database %q: %v", ndata.bucketPath[0], err))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
allKeys, err := ld.db.Keys(ctx, "*").Result()
|
allKeys, err := ld.db.Keys(ctx, "*").Result()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Listing keys in database %q: %w", ndata.bucketPath[0], err)
|
vcl.ShowMessage(fmt.Sprintf("Listing keys in database %q: %v", ndata.bucketPath[0], err))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
f.propertiesBox.SetText(fmt.Sprintf("Database %s\nTotal keys: %d\n", ndata.bucketPath[0], len(allKeys)))
|
f.propertiesBox.SetText(fmt.Sprintf("Database %s\nTotal keys: %d\n", ndata.bucketPath[0], len(allKeys)))
|
||||||
|
|
||||||
// Redis always uses Key + Value as the columns
|
// Redis always uses Key + Value as the columns
|
||||||
|
|
||||||
|
f.contentBox.SetEnabled(false)
|
||||||
|
f.contentBox.Clear()
|
||||||
|
|
||||||
|
f.contentBox.Columns().Clear()
|
||||||
colKey := f.contentBox.Columns().Add()
|
colKey := f.contentBox.Columns().Add()
|
||||||
colKey.Title().SetCaption("Key")
|
colKey.SetCaption("Key")
|
||||||
|
colKey.SetWidth(MY_WIDTH)
|
||||||
|
colKey.SetAlignment(types.TaLeftJustify)
|
||||||
colType := f.contentBox.Columns().Add()
|
colType := f.contentBox.Columns().Add()
|
||||||
colType.Title().SetCaption("Type")
|
colType.SetCaption("Type")
|
||||||
colVal := f.contentBox.Columns().Add()
|
colVal := f.contentBox.Columns().Add()
|
||||||
colVal.Title().SetCaption("Value")
|
colVal.SetCaption("Value")
|
||||||
|
|
||||||
for _, key := range allKeys {
|
for _, key := range allKeys {
|
||||||
typeName, err := ld.db.Type(ctx, key).Result()
|
typeName, err := ld.db.Type(ctx, key).Result()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Loading %q/%q: %w", ndata.bucketPath[0], key, err)
|
vcl.ShowMessage(fmt.Sprintf("Loading %q/%q: %v", ndata.bucketPath[0], key, err))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
rpos := f.contentBox.RowCount()
|
dataEntry := f.contentBox.Items().Add()
|
||||||
f.contentBox.SetRowCount(rpos + 1)
|
dataEntry.SetCaption(key) // formatUtf8
|
||||||
f.contentBox.SetCells(0, rpos, formatUtf8([]byte(key)))
|
dataEntry.SubItems().Add(typeName)
|
||||||
f.contentBox.SetCells(1, rpos, typeName)
|
|
||||||
|
|
||||||
switch typeName {
|
switch typeName {
|
||||||
case "string":
|
case "string":
|
||||||
val, err := ld.db.Get(ctx, key).Result()
|
val, err := ld.db.Get(ctx, key).Result()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Loading %q/%q: %w", ndata.bucketPath[0], key, err)
|
vcl.ShowMessage(fmt.Sprintf("Loading %q/%q: %v", ndata.bucketPath[0], key, err))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
f.contentBox.SetCells(2, rpos, val)
|
dataEntry.SubItems().Add(val)
|
||||||
|
|
||||||
case "hash":
|
case "hash":
|
||||||
val, err := ld.db.HGetAll(ctx, key).Result()
|
val, err := ld.db.HGetAll(ctx, key).Result()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Loading %q/%q: %w", ndata.bucketPath[0], key, err)
|
vcl.ShowMessage(fmt.Sprintf("Loading %q/%q: %v", ndata.bucketPath[0], key, err))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
// It's a map[string]string
|
// It's a map[string]string
|
||||||
f.contentBox.SetCells(2, rpos, formatAny(val))
|
dataEntry.SubItems().Add(formatAny(val))
|
||||||
|
|
||||||
case "lists":
|
case "lists":
|
||||||
fallthrough
|
fallthrough
|
||||||
|
@ -116,18 +129,17 @@ func (ld *redisLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) error
|
||||||
fallthrough
|
fallthrough
|
||||||
|
|
||||||
default:
|
default:
|
||||||
f.contentBox.SetCells(2, rpos, "<<<other object type>>>")
|
dataEntry.SubItems().Add("<<<other object type>>>")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Valid
|
// Valid
|
||||||
vcl_stringgrid_columnwidths(f.contentBox)
|
|
||||||
f.contentBox.SetEnabled(true)
|
f.contentBox.SetEnabled(true)
|
||||||
return nil
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
return fmt.Errorf("Unexpected nav position %q", ndata.bucketPath)
|
vcl.ShowMessage(fmt.Sprintf("Unexpected nav position %q", ndata.bucketPath))
|
||||||
|
return
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -158,50 +170,48 @@ func (ld *redisLoadedDatabase) NavContext(ndata *navData) ([]contextAction, erro
|
||||||
return nil, nil // No special actions are supported
|
return nil, nil // No special actions are supported
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ld *redisLoadedDatabase) ExecQuery(query string, resultArea *vcl.TStringGrid) error {
|
func (ld *redisLoadedDatabase) ExecQuery(query string, resultArea *vcl.TListView) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
// Need to parse the query into separate string+args fields for the protocol
|
// Need to parse the query into separate string+args fields for the protocol
|
||||||
fields, err := lexer.Fields(query)
|
fields, err := lexer.Fields(query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Parsing the query: %w", err)
|
vcl.ShowMessage(fmt.Sprintf("Parsing the query: %v", err))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fields_boxed := box_interface(fields)
|
fields_boxed := box_interface(fields)
|
||||||
|
|
||||||
ret, err := ld.db.Do(ctx, fields_boxed...).Result()
|
ret, err := ld.db.Do(ctx, fields_boxed...).Result()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("The redis query returned an error: %w", err)
|
vcl.ShowMessage(fmt.Sprintf("The redis query returned an error: %v", err))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
vcl_stringgrid_clear(resultArea)
|
resultArea.SetEnabled(false)
|
||||||
|
resultArea.Clear()
|
||||||
|
|
||||||
|
resultArea.Columns().Clear()
|
||||||
colVal := resultArea.Columns().Add()
|
colVal := resultArea.Columns().Add()
|
||||||
colVal.Title().SetCaption("Result")
|
colVal.SetCaption("Result")
|
||||||
|
|
||||||
// The result is probably a single value or a string slice
|
// The result is probably a single value or a string slice
|
||||||
switch ret := ret.(type) {
|
switch ret := ret.(type) {
|
||||||
case []string:
|
case []string:
|
||||||
// Multiple values
|
// Multiple values
|
||||||
for _, single := range ret {
|
for _, single := range ret {
|
||||||
|
cell := resultArea.Items().Add()
|
||||||
rpos := resultArea.RowCount()
|
cell.SetCaption(single) // formatUtf8
|
||||||
resultArea.SetRowCount(rpos + 1)
|
|
||||||
resultArea.SetCells(0, rpos, formatUtf8([]byte(single)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// Single value
|
// Single value
|
||||||
rpos := resultArea.RowCount()
|
dataEntry := resultArea.Items().Add()
|
||||||
resultArea.SetRowCount(rpos + 1)
|
dataEntry.SetCaption(formatAny(ret)) // formatUtf8
|
||||||
resultArea.SetCells(0, rpos, formatAny(ret)) // formatUtf8
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
vcl_stringgrid_columnwidths(resultArea)
|
|
||||||
resultArea.SetEnabled(true)
|
resultArea.SetEnabled(true)
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ld *redisLoadedDatabase) Close() {
|
func (ld *redisLoadedDatabase) Close() {
|
||||||
|
|
224
db_sqlite.go
|
@ -1,22 +1,19 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
_ "yvbolt/sqliteclidriver"
|
_ "yvbolt/sqliteclidriver"
|
||||||
|
|
||||||
"github.com/ying32/govcl/vcl"
|
"github.com/ying32/govcl/vcl"
|
||||||
|
"github.com/ying32/govcl/vcl/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
sqliteTablesCaption = "Tables"
|
sqliteTablesCaption = "Tables"
|
||||||
sqliteFilter = "SQLite database|*.db;*.db3;*.sqlite;*.sqlite3|All files|*.*"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type sqliteLoadedDatabase struct {
|
type sqliteLoadedDatabase struct {
|
||||||
|
@ -40,17 +37,19 @@ func (ld *sqliteLoadedDatabase) Keepalive(ndata *navData) {
|
||||||
ld.arena = append(ld.arena, ndata)
|
ld.arena = append(ld.arena, ndata)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ld *sqliteLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) error {
|
func (ld *sqliteLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) {
|
||||||
|
|
||||||
if len(ndata.bucketPath) == 0 {
|
if len(ndata.bucketPath) == 0 {
|
||||||
// Top-level
|
// Top-level
|
||||||
f.propertiesBox.SetText("Please select...")
|
f.propertiesBox.SetText("Please select...")
|
||||||
return nil
|
f.contentBox.SetEnabled(false)
|
||||||
|
f.contentBox.Clear()
|
||||||
|
|
||||||
} else if len(ndata.bucketPath) == 1 {
|
} else if len(ndata.bucketPath) == 1 {
|
||||||
// Category (tables, ...)
|
// Category (tables, ...)
|
||||||
f.propertiesBox.SetText("Please select...")
|
f.propertiesBox.SetText("Please select...")
|
||||||
return nil
|
f.contentBox.SetEnabled(false)
|
||||||
|
f.contentBox.Clear()
|
||||||
|
|
||||||
} else if len(ndata.bucketPath) == 2 && ndata.bucketPath[0] == sqliteTablesCaption {
|
} else if len(ndata.bucketPath) == 2 && ndata.bucketPath[0] == sqliteTablesCaption {
|
||||||
// Render for specific table
|
// Render for specific table
|
||||||
|
@ -67,13 +66,17 @@ func (ld *sqliteLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) error
|
||||||
// Display table properties
|
// Display table properties
|
||||||
f.propertiesBox.SetText(fmt.Sprintf("Selected table %q\n\nSchema:\n\n%s", tableName, schemaStmt))
|
f.propertiesBox.SetText(fmt.Sprintf("Selected table %q\n\nSchema:\n\n%s", tableName, schemaStmt))
|
||||||
|
|
||||||
|
f.contentBox.SetEnabled(false)
|
||||||
|
f.contentBox.Clear()
|
||||||
|
|
||||||
// Load column details
|
// Load column details
|
||||||
// Use SELECT form instead of common PRAGMA table_info so we can just get names
|
// Use SELECT form instead of common PRAGMA table_info so we can just get names
|
||||||
// We could possibly get this from the main data select, but this will
|
// We could possibly get this from the main data select, but this will
|
||||||
// work even when there are 0 results
|
// work even when there are 0 results
|
||||||
columnNames, err := ld.sqliteGetColumnNamesForTable(tableName)
|
columnNames, err := ld.sqliteGetColumnNamesForTable(tableName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to load columns for table %q: %w", tableName)
|
vcl.ShowMessageFmt("Failed to load columns for table %q: %s", tableName, err.Error())
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
populateColumns(columnNames, f.contentBox)
|
populateColumns(columnNames, f.contentBox)
|
||||||
|
@ -84,19 +87,17 @@ func (ld *sqliteLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) error
|
||||||
// Select * with small limit
|
// Select * with small limit
|
||||||
datar, err := ld.db.Query(`SELECT * FROM "` + tableName + `" LIMIT 1000`) // WARNING can't prepare this parameter, but it comes from the DB (trusted)
|
datar, err := ld.db.Query(`SELECT * FROM "` + tableName + `" LIMIT 1000`) // WARNING can't prepare this parameter, but it comes from the DB (trusted)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to load data for table %q: %w", tableName, err)
|
vcl.ShowMessageFmt("Failed to load data for table %q: %s", tableName, err.Error())
|
||||||
|
return
|
||||||
}
|
}
|
||||||
defer datar.Close()
|
defer datar.Close()
|
||||||
populateRows(datar, f.contentBox)
|
populateRows(datar, f.contentBox)
|
||||||
|
|
||||||
// We successfully populated the data grid
|
// We successfully populated the data grid
|
||||||
vcl_stringgrid_columnwidths(f.contentBox)
|
|
||||||
f.contentBox.SetEnabled(true)
|
f.contentBox.SetEnabled(true)
|
||||||
return nil
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// ??? unknown
|
// ??? unknown
|
||||||
return errors.New("?")
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,14 +129,17 @@ func (ld *sqliteLoadedDatabase) sqliteGetColumnNamesForTable(tableName string) (
|
||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func populateColumns(names []string, dest *vcl.TStringGrid) {
|
func populateColumns(names []string, dest *vcl.TListView) {
|
||||||
|
dest.Columns().Clear()
|
||||||
for _, columnName := range names {
|
for _, columnName := range names {
|
||||||
col := dest.Columns().Add()
|
col := dest.Columns().Add()
|
||||||
col.Title().SetCaption(columnName)
|
col.SetCaption(columnName)
|
||||||
|
col.SetWidth(MY_WIDTH)
|
||||||
|
col.SetAlignment(types.TaLeftJustify)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func populateRows(rr *sql.Rows, dest *vcl.TStringGrid) {
|
func populateRows(rr *sql.Rows, dest *vcl.TListView) {
|
||||||
|
|
||||||
numColumns := int(dest.Columns().Count())
|
numColumns := int(dest.Columns().Count())
|
||||||
|
|
||||||
|
@ -152,10 +156,10 @@ func populateRows(rr *sql.Rows, dest *vcl.TStringGrid) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
rpos := dest.RowCount()
|
dataEntry := dest.Items().Add()
|
||||||
dest.SetRowCount(rpos + 1)
|
dataEntry.SetCaption(formatAny(fields[0]))
|
||||||
for i := 0; i < len(fields); i += 1 {
|
for i := 1; i < len(fields); i += 1 {
|
||||||
dest.SetCells(int32(i), rpos, formatAny(fields[i]))
|
dataEntry.SubItems().Add(formatAny(fields[i]))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -165,149 +169,29 @@ func populateRows(rr *sql.Rows, dest *vcl.TStringGrid) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ld *sqliteLoadedDatabase) ExecQuery(query string, resultArea *vcl.TStringGrid) error {
|
func (ld *sqliteLoadedDatabase) ExecQuery(query string, resultArea *vcl.TListView) {
|
||||||
rr, err := ld.db.Query(query)
|
rr, err := ld.db.Query(query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
vcl.ShowMessage(err.Error())
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
defer rr.Close()
|
defer rr.Close()
|
||||||
|
|
||||||
vcl_stringgrid_clear(resultArea)
|
resultArea.SetEnabled(false)
|
||||||
|
resultArea.Clear()
|
||||||
|
|
||||||
columns, err := rr.Columns()
|
columns, err := rr.Columns()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
vcl.ShowMessage(err.Error())
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
populateColumns(columns, resultArea)
|
populateColumns(columns, resultArea)
|
||||||
|
|
||||||
populateRows(rr, resultArea)
|
populateRows(rr, resultArea)
|
||||||
|
|
||||||
vcl_stringgrid_columnwidths(resultArea)
|
|
||||||
resultArea.SetEnabled(true)
|
resultArea.SetEnabled(true)
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *sqliteLoadedDatabase) ApplyChanges(f *TMainForm, ndata *navData) (retErr error) {
|
|
||||||
|
|
||||||
if len(ndata.bucketPath) != 2 {
|
|
||||||
return errors.New("invalid selection")
|
|
||||||
}
|
|
||||||
tableName := ndata.bucketPath[1]
|
|
||||||
|
|
||||||
// We have rendered row IDs, need to convert back to an SQLite primary key
|
|
||||||
// TODO stash the real key inside f.contentBox.Objects()
|
|
||||||
// FIXME breaks if you try and edit the primary key(!)
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
tx, err := n.db.BeginTx(ctx, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var commitOK bool = false
|
|
||||||
defer func() {
|
|
||||||
if !commitOK {
|
|
||||||
err := tx.Rollback()
|
|
||||||
if err != nil {
|
|
||||||
retErr = err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Data grid properties
|
|
||||||
var columnNames []string
|
|
||||||
for i := int32(0); i < f.contentBox.ColCount(); i++ {
|
|
||||||
columnNames = append(columnNames, f.contentBox.Columns().Items(i).Title().Caption())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query sqlite table metadata to determine which of these is the PRIMARY KEY
|
|
||||||
var primaryColumnName string
|
|
||||||
err = tx.QueryRowContext(ctx, `SELECT l.name FROM pragma_table_info(?) as l WHERE l.pk = 1;`, tableName).Scan(&primaryColumnName)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Finding primary key for update: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert it to an index
|
|
||||||
var primaryColumnIdx int = -1
|
|
||||||
for i := 0; i < len(columnNames); i++ {
|
|
||||||
if columnNames[i] == primaryColumnName {
|
|
||||||
primaryColumnIdx = i
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if primaryColumnIdx == -1 {
|
|
||||||
return fmt.Errorf("Primary key %q missing from available columns", primaryColumnName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SQLite can only LIMIT 1 on update/delete if it was compiled with
|
|
||||||
// SQLITE_ENABLE_UPDATE_DELETE_LIMIT, which isn't the case for the mattn
|
|
||||||
// cgo library
|
|
||||||
// Skip that, and just rely on primary key uniqueness
|
|
||||||
|
|
||||||
// Edit
|
|
||||||
for rowid, editcells := range f.updateRows {
|
|
||||||
stmt := `UPDATE "` + tableName + `" SET `
|
|
||||||
params := []interface{}{} // FIXME reinstate types for the driver (although SQLite doesn't mind)
|
|
||||||
|
|
||||||
for ct, cell := range editcells {
|
|
||||||
if ct > 0 {
|
|
||||||
stmt += `, `
|
|
||||||
}
|
|
||||||
stmt += `"` + columnNames[cell] + `" = ?`
|
|
||||||
params = append(params, f.contentBox.Cells(cell, rowid))
|
|
||||||
}
|
|
||||||
stmt += ` WHERE "` + primaryColumnName + `" = ?`
|
|
||||||
pkVal := f.contentBox.Cells(int32(primaryColumnIdx), rowid)
|
|
||||||
params = append(params, pkVal)
|
|
||||||
|
|
||||||
_, err = tx.ExecContext(ctx, stmt, params...)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Updating row %q: %w", pkVal, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete by key (affects rowids after re-render)
|
|
||||||
for rowid, _ := range f.deleteRows {
|
|
||||||
pkVal := f.contentBox.Cells(int32(primaryColumnIdx), rowid)
|
|
||||||
|
|
||||||
stmt := `DELETE FROM "` + tableName + `" WHERE "` + primaryColumnName + `" = ?`
|
|
||||||
|
|
||||||
_, err = tx.ExecContext(ctx, stmt, pkVal)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Deleting row %q: %w", pkVal, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert all new entries
|
|
||||||
for rowid, _ := range f.insertRows {
|
|
||||||
stmt := `INSERT INTO "` + tableName + `" (` + strings.Join(columnNames, `, `) + `) VALUES (`
|
|
||||||
params := []interface{}{} // FIXME reinstate types for the driver (although SQLite doesn't mind)
|
|
||||||
|
|
||||||
for colid := 0; colid < len(columnNames); colid++ {
|
|
||||||
if colid > 0 {
|
|
||||||
stmt += `, `
|
|
||||||
}
|
|
||||||
stmt += "?"
|
|
||||||
params = append(params, f.contentBox.Cells(int32(colid), rowid))
|
|
||||||
}
|
|
||||||
stmt += `)`
|
|
||||||
|
|
||||||
_, err = tx.ExecContext(ctx, stmt, params...)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Inserting row: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = tx.Commit()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
commitOK = true // no need for rollback
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ld *sqliteLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
|
func (ld *sqliteLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
|
||||||
|
@ -348,52 +232,8 @@ func (ld *sqliteLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
|
||||||
return nil, fmt.Errorf("unknown nav path %#v", ndata.bucketPath)
|
return nil, fmt.Errorf("unknown nav path %#v", ndata.bucketPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ld *sqliteLoadedDatabase) NavContext(ndata *navData) (ret []contextAction, err error) {
|
func (ld *sqliteLoadedDatabase) NavContext(ndata *navData) ([]contextAction, error) {
|
||||||
|
return nil, nil // No special actions are supported
|
||||||
if len(ndata.bucketPath) == 0 {
|
|
||||||
ret = append(ret, contextAction{"Compact database", ld.CompactDatabase})
|
|
||||||
ret = append(ret, contextAction{"Export backup...", ld.ExportBackup})
|
|
||||||
}
|
|
||||||
if len(ndata.bucketPath) == 2 {
|
|
||||||
ret = append(ret, contextAction{"Drop table", ld.DropTable})
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ld *sqliteLoadedDatabase) CompactDatabase(sender vcl.IComponent, ndata *navData) error {
|
|
||||||
_, err := ld.db.Exec(`VACUUM;`)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ld *sqliteLoadedDatabase) ExportBackup(sender vcl.IComponent, ndata *navData) error {
|
|
||||||
// Popup for output file
|
|
||||||
dlg := vcl.NewSaveDialog(sender)
|
|
||||||
dlg.SetTitle("Save backup as...")
|
|
||||||
dlg.SetFilter(sqliteFilter)
|
|
||||||
ret := dlg.Execute() // Fake blocking
|
|
||||||
if !ret {
|
|
||||||
return nil // cancelled
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := ld.db.Exec(`VACUUM INTO ?`, dlg.FileName())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ld *sqliteLoadedDatabase) DropTable(sender vcl.IComponent, ndata *navData) error {
|
|
||||||
if len(ndata.bucketPath) != 2 {
|
|
||||||
return errors.New("Invalid selection")
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
|
|
||||||
tableName := ndata.bucketPath[1]
|
|
||||||
|
|
||||||
if !vcl_confirm_dialog(sender, "Drop table", fmt.Sprintf("Are you sure you want to drop the table %q?", tableName)) {
|
|
||||||
return nil // cancelled
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := ld.db.Exec(`DROP TABLE "` + tableName + `"`) // WARNING can't prepare this parameter, but it comes from the DB (trusted)
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ld *sqliteLoadedDatabase) Close() {
|
func (ld *sqliteLoadedDatabase) Close() {
|
||||||
|
|
21
images.go
|
@ -10,22 +10,14 @@ import (
|
||||||
var assetsFs embed.FS
|
var assetsFs embed.FS
|
||||||
|
|
||||||
const (
|
const (
|
||||||
imgAdd int32 = iota
|
imgArrowRefresh int32 = iota
|
||||||
imgArrowRefresh
|
|
||||||
imgChartBar
|
imgChartBar
|
||||||
imgDatabase
|
imgDatabase
|
||||||
imgDatabaseAdd
|
imgDatabaseAdd
|
||||||
imgDatabaseDelete
|
imgDatabaseDelete
|
||||||
imgDatabaseLightning
|
imgDatabaseLightning
|
||||||
imgDatabaseSave
|
imgDatabaseSave
|
||||||
imgDelete
|
|
||||||
imgLightning
|
imgLightning
|
||||||
imgLightningGo
|
|
||||||
imgPencil
|
|
||||||
imgPencilAdd
|
|
||||||
imgPencilDelete
|
|
||||||
imgPencilGo
|
|
||||||
imgResultsetNext
|
|
||||||
imgTable
|
imgTable
|
||||||
imgTableAdd
|
imgTableAdd
|
||||||
imgTableDelete
|
imgTableDelete
|
||||||
|
@ -56,9 +48,6 @@ func loadImages(owner vcl.IComponent) *vcl.TImageList {
|
||||||
}
|
}
|
||||||
|
|
||||||
ilist := vcl.NewImageList(owner)
|
ilist := vcl.NewImageList(owner)
|
||||||
|
|
||||||
// ls assets | sort | sed -re 's~(.+)~ilist.Add(mustLoad("assets/\1"), nil)~'
|
|
||||||
ilist.Add(mustLoad("assets/add.png"), nil)
|
|
||||||
ilist.Add(mustLoad("assets/arrow_refresh.png"), nil)
|
ilist.Add(mustLoad("assets/arrow_refresh.png"), nil)
|
||||||
ilist.Add(mustLoad("assets/chart_bar.png"), nil)
|
ilist.Add(mustLoad("assets/chart_bar.png"), nil)
|
||||||
ilist.Add(mustLoad("assets/database.png"), nil)
|
ilist.Add(mustLoad("assets/database.png"), nil)
|
||||||
|
@ -66,14 +55,7 @@ func loadImages(owner vcl.IComponent) *vcl.TImageList {
|
||||||
ilist.Add(mustLoad("assets/database_delete.png"), nil)
|
ilist.Add(mustLoad("assets/database_delete.png"), nil)
|
||||||
ilist.Add(mustLoad("assets/database_lightning.png"), nil)
|
ilist.Add(mustLoad("assets/database_lightning.png"), nil)
|
||||||
ilist.Add(mustLoad("assets/database_save.png"), nil)
|
ilist.Add(mustLoad("assets/database_save.png"), nil)
|
||||||
ilist.Add(mustLoad("assets/delete.png"), nil)
|
|
||||||
ilist.Add(mustLoad("assets/lightning.png"), nil)
|
ilist.Add(mustLoad("assets/lightning.png"), nil)
|
||||||
ilist.Add(mustLoad("assets/lightning_go.png"), nil)
|
|
||||||
ilist.Add(mustLoad("assets/pencil.png"), nil)
|
|
||||||
ilist.Add(mustLoad("assets/pencil_add.png"), nil)
|
|
||||||
ilist.Add(mustLoad("assets/pencil_delete.png"), nil)
|
|
||||||
ilist.Add(mustLoad("assets/pencil_go.png"), nil)
|
|
||||||
ilist.Add(mustLoad("assets/resultset_next.png"), nil)
|
|
||||||
ilist.Add(mustLoad("assets/table.png"), nil)
|
ilist.Add(mustLoad("assets/table.png"), nil)
|
||||||
ilist.Add(mustLoad("assets/table_add.png"), nil)
|
ilist.Add(mustLoad("assets/table_add.png"), nil)
|
||||||
ilist.Add(mustLoad("assets/table_delete.png"), nil)
|
ilist.Add(mustLoad("assets/table_delete.png"), nil)
|
||||||
|
@ -85,7 +67,6 @@ func loadImages(owner vcl.IComponent) *vcl.TImageList {
|
||||||
ilist.Add(mustLoad("assets/vendor_mysql.png"), nil)
|
ilist.Add(mustLoad("assets/vendor_mysql.png"), nil)
|
||||||
ilist.Add(mustLoad("assets/vendor_redis.png"), nil)
|
ilist.Add(mustLoad("assets/vendor_redis.png"), nil)
|
||||||
ilist.Add(mustLoad("assets/vendor_sqlite.png"), nil)
|
ilist.Add(mustLoad("assets/vendor_sqlite.png"), nil)
|
||||||
|
|
||||||
return ilist
|
return ilist
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ var ErrNavNotExist error = errors.New("The selected item no longer exists")
|
||||||
|
|
||||||
type contextAction struct {
|
type contextAction struct {
|
||||||
Name string
|
Name string
|
||||||
Callback func(sender vcl.IComponent, ndata *navData) error
|
Callback func(ndata *navData)
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadedDatabase is a DB-agnostic interface for each loaded database.
|
// loadedDatabase is a DB-agnostic interface for each loaded database.
|
||||||
|
@ -18,21 +18,14 @@ type loadedDatabase interface {
|
||||||
DisplayName() string
|
DisplayName() string
|
||||||
DriverName() string
|
DriverName() string
|
||||||
RootElement() *vcl.TTreeNode
|
RootElement() *vcl.TTreeNode
|
||||||
RenderForNav(f *TMainForm, ndata *navData) error
|
RenderForNav(f *TMainForm, ndata *navData)
|
||||||
|
ExecQuery(query string, resultArea *vcl.TListView)
|
||||||
NavChildren(ndata *navData) ([]string, error)
|
NavChildren(ndata *navData) ([]string, error)
|
||||||
NavContext(ndata *navData) ([]contextAction, error)
|
NavContext(ndata *navData) ([]contextAction, error)
|
||||||
Keepalive(ndata *navData)
|
Keepalive(ndata *navData)
|
||||||
Close()
|
Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
type queryableLoadedDatabase interface {
|
|
||||||
ExecQuery(query string, resultArea *vcl.TStringGrid) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type editableLoadedDatabase interface {
|
|
||||||
ApplyChanges(f *TMainForm, ndata *navData) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// navData is the .Data() pointer for each TTreeNode in the left-hand tree.
|
// navData is the .Data() pointer for each TTreeNode in the left-hand tree.
|
||||||
type navData struct {
|
type navData struct {
|
||||||
ld loadedDatabase
|
ld loadedDatabase
|
||||||
|
|
341
main.go
|
@ -11,17 +11,11 @@ import (
|
||||||
"github.com/pkg/browser"
|
"github.com/pkg/browser"
|
||||||
"github.com/ying32/govcl/vcl"
|
"github.com/ying32/govcl/vcl"
|
||||||
"github.com/ying32/govcl/vcl/types"
|
"github.com/ying32/govcl/vcl/types"
|
||||||
"github.com/ying32/govcl/vcl/types/colors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
APPNAME = "yvbolt"
|
APPNAME = "yvbolt"
|
||||||
HOMEPAGE_URL = "https://code.ivysaur.me/yvbolt"
|
HOMEPAGE_URL = "https://code.ivysaur.me/yvbolt"
|
||||||
|
|
||||||
CO_INSERT = colors.ClYellow
|
|
||||||
CO_EDIT_IMPLICIT = colors.ClLightgreen
|
|
||||||
CO_EDIT_EXPLICIT = colors.ClGreen
|
|
||||||
CO_DELETE = colors.ClRed
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type TMainForm struct {
|
type TMainForm struct {
|
||||||
|
@ -36,17 +30,9 @@ type TMainForm struct {
|
||||||
Buckets *vcl.TTreeView
|
Buckets *vcl.TTreeView
|
||||||
Tabs *vcl.TPageControl
|
Tabs *vcl.TPageControl
|
||||||
propertiesBox *vcl.TMemo
|
propertiesBox *vcl.TMemo
|
||||||
contentBox *vcl.TStringGrid
|
contentBox *vcl.TListView
|
||||||
dataInsertBtn *vcl.TToolButton
|
queryInput *vcl.TMemo
|
||||||
dataDelRowBtn *vcl.TToolButton
|
queryResult *vcl.TListView
|
||||||
dataCommitBtn *vcl.TToolButton
|
|
||||||
isEditing bool
|
|
||||||
insertRows map[int32]struct{} // Rows in the StringGrid that are to-be-inserted
|
|
||||||
deleteRows map[int32]struct{}
|
|
||||||
updateRows map[int32][]int32 // Row->cells that are to-be-updated
|
|
||||||
queryExecBtn *vcl.TToolButton
|
|
||||||
queryInput *vcl.TRichEdit
|
|
||||||
queryResult *vcl.TStringGrid
|
|
||||||
|
|
||||||
none *noLoadedDatabase
|
none *noLoadedDatabase
|
||||||
dbs []loadedDatabase
|
dbs []loadedDatabase
|
||||||
|
@ -65,8 +51,6 @@ func (f *TMainForm) OnFormCreate(sender vcl.IObject) {
|
||||||
|
|
||||||
f.SetCaption(APPNAME)
|
f.SetCaption(APPNAME)
|
||||||
f.ScreenCenter()
|
f.ScreenCenter()
|
||||||
f.SetWidth(1280)
|
|
||||||
f.SetHeight(640)
|
|
||||||
f.ImageList.GetIcon(imgDatabaseLightning, f.Icon())
|
f.ImageList.GetIcon(imgDatabaseLightning, f.Icon())
|
||||||
|
|
||||||
mnuFile := vcl.NewMenuItem(f)
|
mnuFile := vcl.NewMenuItem(f)
|
||||||
|
@ -151,7 +135,7 @@ func (f *TMainForm) OnFormCreate(sender vcl.IObject) {
|
||||||
mnuQueryExecute.SetCaption("Execute")
|
mnuQueryExecute.SetCaption("Execute")
|
||||||
mnuQueryExecute.SetShortCutFromString("F5")
|
mnuQueryExecute.SetShortCutFromString("F5")
|
||||||
mnuQueryExecute.SetOnClick(f.OnQueryExecute)
|
mnuQueryExecute.SetOnClick(f.OnQueryExecute)
|
||||||
mnuQueryExecute.SetImageIndex(imgResultsetNext)
|
mnuQueryExecute.SetImageIndex(imgLightning)
|
||||||
mnuQuery.Add(mnuQueryExecute)
|
mnuQuery.Add(mnuQueryExecute)
|
||||||
|
|
||||||
mnuHelp := vcl.NewMenuItem(f)
|
mnuHelp := vcl.NewMenuItem(f)
|
||||||
|
@ -223,56 +207,14 @@ func (f *TMainForm) OnFormCreate(sender vcl.IObject) {
|
||||||
dataTab.SetCaption("Data")
|
dataTab.SetCaption("Data")
|
||||||
dataTab.SetImageIndex(imgTable)
|
dataTab.SetImageIndex(imgTable)
|
||||||
|
|
||||||
dataButtonBar := vcl.NewToolBar(dataTab)
|
f.contentBox = vcl.NewListView(dataTab)
|
||||||
dataButtonBar.SetParent(dataTab)
|
|
||||||
dataButtonBar.SetAlign(types.AlTop)
|
|
||||||
dataButtonBar.BorderSpacing().SetLeft(MY_SPACING)
|
|
||||||
dataButtonBar.BorderSpacing().SetTop(MY_SPACING)
|
|
||||||
dataButtonBar.BorderSpacing().SetBottom(1)
|
|
||||||
dataButtonBar.BorderSpacing().SetRight(MY_SPACING)
|
|
||||||
dataButtonBar.SetEdgeBorders(0)
|
|
||||||
dataButtonBar.SetImages(f.ImageList)
|
|
||||||
|
|
||||||
dataRefreshBtn := vcl.NewToolButton(dataButtonBar)
|
|
||||||
dataRefreshBtn.SetParent(dataButtonBar)
|
|
||||||
dataRefreshBtn.SetHint("Refresh")
|
|
||||||
dataRefreshBtn.SetShowHint(true)
|
|
||||||
dataRefreshBtn.SetImageIndex(imgArrowRefresh)
|
|
||||||
dataRefreshBtn.SetOnClick(func(sender vcl.IObject) { f.RefreshCurrentItem() })
|
|
||||||
|
|
||||||
f.dataInsertBtn = vcl.NewToolButton(dataButtonBar)
|
|
||||||
f.dataInsertBtn.SetParent(dataButtonBar)
|
|
||||||
f.dataInsertBtn.SetImageIndex(imgAdd)
|
|
||||||
f.dataInsertBtn.SetHint("Insert")
|
|
||||||
f.dataInsertBtn.SetShowHint(true)
|
|
||||||
f.dataInsertBtn.SetOnClick(f.OnDataInsertClick)
|
|
||||||
|
|
||||||
f.dataDelRowBtn = vcl.NewToolButton(dataButtonBar)
|
|
||||||
f.dataDelRowBtn.SetParent(dataButtonBar)
|
|
||||||
f.dataDelRowBtn.SetImageIndex(imgDelete)
|
|
||||||
f.dataDelRowBtn.SetHint("Delete Row")
|
|
||||||
f.dataDelRowBtn.SetShowHint(true)
|
|
||||||
f.dataDelRowBtn.SetOnClick(f.OnDataDeleteRowClick)
|
|
||||||
|
|
||||||
f.dataCommitBtn = vcl.NewToolButton(dataButtonBar)
|
|
||||||
f.dataCommitBtn.SetParent(dataButtonBar)
|
|
||||||
f.dataCommitBtn.SetImageIndex(imgPencilGo)
|
|
||||||
f.dataCommitBtn.SetHint("Commit")
|
|
||||||
f.dataCommitBtn.SetShowHint(true)
|
|
||||||
f.dataCommitBtn.SetOnClick(f.OnDataCommitClick)
|
|
||||||
|
|
||||||
f.contentBox = vcl.NewStringGrid(dataTab)
|
|
||||||
f.contentBox.SetParent(dataTab)
|
f.contentBox.SetParent(dataTab)
|
||||||
f.contentBox.BorderSpacing().SetLeft(MY_SPACING)
|
f.contentBox.BorderSpacing().SetAround(MY_SPACING)
|
||||||
f.contentBox.BorderSpacing().SetRight(MY_SPACING)
|
f.contentBox.SetAlign(types.AlClient) // fill remaining space
|
||||||
f.contentBox.BorderSpacing().SetBottom(MY_SPACING)
|
f.contentBox.SetViewStyle(types.VsReport) // "Report style" i.e. has columns
|
||||||
f.contentBox.SetAlign(types.AlClient) // fill remaining space
|
f.contentBox.SetAutoWidthLastColumn(true)
|
||||||
f.contentBox.SetOptions(f.contentBox.Options().Include(types.GoThumbTracking, types.GoColSizing, types.GoDblClickAutoSize, types.GoEditing))
|
f.contentBox.SetReadOnly(true)
|
||||||
f.contentBox.SetOnPrepareCanvas(f.OnDataPrepareCanvas)
|
f.contentBox.Columns().Clear()
|
||||||
f.contentBox.SetOnEditingDone(f.OnDataCellEdited)
|
|
||||||
f.contentBox.SetOnGetEditText(f.OnDataCellEditStarting)
|
|
||||||
f.contentBox.SetDefaultColWidth(MY_WIDTH)
|
|
||||||
vcl_stringgrid_clear(f.contentBox)
|
|
||||||
|
|
||||||
queryTab := vcl.NewTabSheet(f.Tabs)
|
queryTab := vcl.NewTabSheet(f.Tabs)
|
||||||
queryTab.SetParent(f.Tabs)
|
queryTab.SetParent(f.Tabs)
|
||||||
|
@ -290,14 +232,13 @@ func (f *TMainForm) OnFormCreate(sender vcl.IObject) {
|
||||||
queryButtonBar.SetImages(f.ImageList)
|
queryButtonBar.SetImages(f.ImageList)
|
||||||
queryButtonBar.SetShowCaptions(true)
|
queryButtonBar.SetShowCaptions(true)
|
||||||
|
|
||||||
f.queryExecBtn = vcl.NewToolButton(queryButtonBar)
|
queryExecBtn := vcl.NewToolButton(queryButtonBar)
|
||||||
f.queryExecBtn.SetParent(queryButtonBar)
|
queryExecBtn.SetParent(queryButtonBar)
|
||||||
f.queryExecBtn.SetHint("Execute")
|
queryExecBtn.SetCaption("Execute")
|
||||||
f.queryExecBtn.SetShowHint(true)
|
// queryExecBtn.SetImageIndex(imgLightning)
|
||||||
f.queryExecBtn.SetImageIndex(imgResultsetNext)
|
queryExecBtn.SetOnClick(f.OnQueryExecute)
|
||||||
f.queryExecBtn.SetOnClick(f.OnQueryExecute)
|
|
||||||
|
|
||||||
f.queryInput = vcl.NewRichEdit(queryTab)
|
f.queryInput = vcl.NewMemo(queryTab)
|
||||||
f.queryInput.SetParent(queryTab)
|
f.queryInput.SetParent(queryTab)
|
||||||
f.queryInput.SetHeight(MY_HEIGHT)
|
f.queryInput.SetHeight(MY_HEIGHT)
|
||||||
f.queryInput.SetAlign(types.AlTop)
|
f.queryInput.SetAlign(types.AlTop)
|
||||||
|
@ -307,27 +248,26 @@ func (f *TMainForm) OnFormCreate(sender vcl.IObject) {
|
||||||
} else {
|
} else {
|
||||||
f.queryInput.Font().SetName("monospace")
|
f.queryInput.Font().SetName("monospace")
|
||||||
}
|
}
|
||||||
f.queryInput.SetCursor(types.CrIBeam) // Use text cursor instead of default pointer cursor
|
|
||||||
f.queryInput.BorderSpacing().SetLeft(MY_SPACING)
|
f.queryInput.BorderSpacing().SetLeft(MY_SPACING)
|
||||||
//f.queryInput.BorderSpacing().SetTop(1)
|
//f.queryInput.BorderSpacing().SetTop(1)
|
||||||
f.queryInput.BorderSpacing().SetRight(MY_SPACING)
|
f.queryInput.BorderSpacing().SetRight(MY_SPACING)
|
||||||
f.queryInput.SetBorderStyle(types.BsFrame)
|
f.queryInput.SetBorderStyle(types.BsFrame)
|
||||||
// f.queryInput.SetOnKeyUp(f.OnQueryTextChanged) // Performs extremely slowly
|
|
||||||
|
|
||||||
vsplit := vcl.NewSplitter(queryTab)
|
vsplit := vcl.NewSplitter(queryTab)
|
||||||
vsplit.SetParent(queryTab)
|
vsplit.SetParent(queryTab)
|
||||||
vsplit.SetAlign(types.AlTop)
|
vsplit.SetAlign(types.AlTop)
|
||||||
vsplit.SetTop(2)
|
vsplit.SetTop(2)
|
||||||
|
|
||||||
f.queryResult = vcl.NewStringGrid(queryTab)
|
f.queryResult = vcl.NewListView(queryTab)
|
||||||
f.queryResult.SetParent(queryTab)
|
f.queryResult.SetParent(queryTab)
|
||||||
f.queryResult.SetAlign(types.AlClient) // fill remaining space
|
f.queryResult.SetAlign(types.AlClient) // fill remaining space
|
||||||
|
f.queryResult.SetViewStyle(types.VsReport) // "Report style" i.e. has columns
|
||||||
|
f.queryResult.SetAutoWidthLastColumn(true)
|
||||||
|
f.queryResult.SetReadOnly(true)
|
||||||
|
f.queryResult.Columns().Clear()
|
||||||
f.queryResult.BorderSpacing().SetLeft(MY_SPACING)
|
f.queryResult.BorderSpacing().SetLeft(MY_SPACING)
|
||||||
f.queryResult.BorderSpacing().SetRight(MY_SPACING)
|
f.queryResult.BorderSpacing().SetRight(MY_SPACING)
|
||||||
f.queryResult.BorderSpacing().SetBottom(MY_SPACING)
|
f.queryResult.BorderSpacing().SetBottom(MY_SPACING)
|
||||||
f.queryResult.SetOptions(f.queryResult.Options().Include(types.GoThumbTracking))
|
|
||||||
f.queryResult.SetDefaultColWidth(MY_WIDTH)
|
|
||||||
vcl_stringgrid_clear(f.queryResult)
|
|
||||||
|
|
||||||
f.none = &noLoadedDatabase{}
|
f.none = &noLoadedDatabase{}
|
||||||
f.OnNavChange(f, nil) // calls f.none.RenderForNav and sets up status bar content
|
f.OnNavChange(f, nil) // calls f.none.RenderForNav and sets up status bar content
|
||||||
|
@ -336,7 +276,7 @@ func (f *TMainForm) OnFormCreate(sender vcl.IObject) {
|
||||||
func (f *TMainForm) OnMnuFileBoltNewClick(sender vcl.IObject) {
|
func (f *TMainForm) OnMnuFileBoltNewClick(sender vcl.IObject) {
|
||||||
dlg := vcl.NewSaveDialog(f)
|
dlg := vcl.NewSaveDialog(f)
|
||||||
dlg.SetTitle("Save database as...")
|
dlg.SetTitle("Save database as...")
|
||||||
dlg.SetFilter(boltFilter)
|
dlg.SetFilter("Bolt database|*.db|All files|*.*")
|
||||||
ret := dlg.Execute() // Fake blocking
|
ret := dlg.Execute() // Fake blocking
|
||||||
if ret {
|
if ret {
|
||||||
f.boltAddDatabaseFromFile(dlg.FileName(), false)
|
f.boltAddDatabaseFromFile(dlg.FileName(), false)
|
||||||
|
@ -346,7 +286,7 @@ func (f *TMainForm) OnMnuFileBoltNewClick(sender vcl.IObject) {
|
||||||
func (f *TMainForm) OnMnuFileBoltOpenClick(sender vcl.IObject) {
|
func (f *TMainForm) OnMnuFileBoltOpenClick(sender vcl.IObject) {
|
||||||
dlg := vcl.NewOpenDialog(f)
|
dlg := vcl.NewOpenDialog(f)
|
||||||
dlg.SetTitle("Select a database file...")
|
dlg.SetTitle("Select a database file...")
|
||||||
dlg.SetFilter(boltFilter)
|
dlg.SetFilter("Bolt database|*.db|All files|*.*")
|
||||||
ret := dlg.Execute() // Fake blocking
|
ret := dlg.Execute() // Fake blocking
|
||||||
if ret {
|
if ret {
|
||||||
f.boltAddDatabaseFromFile(dlg.FileName(), false)
|
f.boltAddDatabaseFromFile(dlg.FileName(), false)
|
||||||
|
@ -356,7 +296,7 @@ func (f *TMainForm) OnMnuFileBoltOpenClick(sender vcl.IObject) {
|
||||||
func (f *TMainForm) OnMnuFileBoltOpenReadonlyClick(sender vcl.IObject) {
|
func (f *TMainForm) OnMnuFileBoltOpenReadonlyClick(sender vcl.IObject) {
|
||||||
dlg := vcl.NewOpenDialog(f)
|
dlg := vcl.NewOpenDialog(f)
|
||||||
dlg.SetTitle("Select a database file...")
|
dlg.SetTitle("Select a database file...")
|
||||||
dlg.SetFilter(boltFilter)
|
dlg.SetFilter("Bolt database|*.db|All files|*.*")
|
||||||
ret := dlg.Execute() // Fake blocking
|
ret := dlg.Execute() // Fake blocking
|
||||||
if ret {
|
if ret {
|
||||||
f.boltAddDatabaseFromFile(dlg.FileName(), true)
|
f.boltAddDatabaseFromFile(dlg.FileName(), true)
|
||||||
|
@ -368,7 +308,7 @@ func (f *TMainForm) OnMnuFileSqliteOpenClick(sender vcl.IObject) {
|
||||||
|
|
||||||
dlg := vcl.NewOpenDialog(f)
|
dlg := vcl.NewOpenDialog(f)
|
||||||
dlg.SetTitle("Select a database file...")
|
dlg.SetTitle("Select a database file...")
|
||||||
dlg.SetFilter(sqliteFilter)
|
dlg.SetFilter("SQLite database|*.db;*.db3;*.sqlite;*.sqlite3|All files|*.*")
|
||||||
ret := dlg.Execute() // Fake blocking
|
ret := dlg.Execute() // Fake blocking
|
||||||
if ret {
|
if ret {
|
||||||
f.sqliteAddDatabaseFromFile(dlg.FileName(), cliDriver)
|
f.sqliteAddDatabaseFromFile(dlg.FileName(), cliDriver)
|
||||||
|
@ -511,10 +451,7 @@ func (f *TMainForm) OnNavContextPopup(sender vcl.IObject, mousePos types.TPoint,
|
||||||
mnuAction.SetCaption(action.Name)
|
mnuAction.SetCaption(action.Name)
|
||||||
cb := action.Callback // Copy to avoid reuse of loop variable
|
cb := action.Callback // Copy to avoid reuse of loop variable
|
||||||
mnuAction.SetOnClick(func(sender vcl.IObject) {
|
mnuAction.SetOnClick(func(sender vcl.IObject) {
|
||||||
err = cb(f, ndata)
|
cb(ndata)
|
||||||
if err != nil {
|
|
||||||
vcl.ShowMessage(err.Error())
|
|
||||||
}
|
|
||||||
f.OnNavContextRefresh(curItem, ndata)
|
f.OnNavContextRefresh(curItem, ndata)
|
||||||
})
|
})
|
||||||
mnu.Items().Add(mnuAction)
|
mnu.Items().Add(mnuAction)
|
||||||
|
@ -538,18 +475,6 @@ func (f *TMainForm) OnNavContextPopup(sender vcl.IObject, mousePos types.TPoint,
|
||||||
mnu.Popup2()
|
mnu.Popup2()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *TMainForm) RefreshCurrentItem() {
|
|
||||||
item := f.Buckets.Selected()
|
|
||||||
if item == nil {
|
|
||||||
return // nothing to do
|
|
||||||
}
|
|
||||||
|
|
||||||
ndata := (*navData)(item.Data())
|
|
||||||
|
|
||||||
f.OnNavContextRefresh(item, ndata) // Refresh LHS pane/children
|
|
||||||
f.OnNavChange(f.Buckets, item) // Refresh RHS pane/data content
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *TMainForm) OnNavContextRefresh(item *vcl.TTreeNode, ndata *navData) {
|
func (f *TMainForm) OnNavContextRefresh(item *vcl.TTreeNode, ndata *navData) {
|
||||||
isExpanded := item.Expanded()
|
isExpanded := item.Expanded()
|
||||||
|
|
||||||
|
@ -566,131 +491,6 @@ func (f *TMainForm) OnNavContextRefresh(item *vcl.TTreeNode, ndata *navData) {
|
||||||
item.SetExpanded(isExpanded)
|
item.SetExpanded(isExpanded)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *TMainForm) OnDataPrepareCanvas(sender vcl.IObject, aCol, aRow int32, aState types.TGridDrawState) {
|
|
||||||
|
|
||||||
if _, ok := f.deleteRows[aRow]; ok {
|
|
||||||
f.contentBox.Canvas().Brush().SetColor(CO_DELETE)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := f.insertRows[aRow]; ok {
|
|
||||||
f.contentBox.Canvas().Brush().SetColor(CO_INSERT)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if er, ok := f.updateRows[aRow]; ok {
|
|
||||||
// This row is being edited
|
|
||||||
if int32slice_contains(er, aCol) {
|
|
||||||
f.contentBox.Canvas().Brush().SetColor(CO_EDIT_EXPLICIT)
|
|
||||||
} else {
|
|
||||||
f.contentBox.Canvas().Brush().SetColor(CO_EDIT_IMPLICIT)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// func (f *TMainForm) OnDataCellEditStarting(sender vcl.IObject, aCol, aRow int32, editor **vcl.TWinControl) {
|
|
||||||
func (f *TMainForm) OnDataCellEditStarting(sender vcl.IObject, aCol, aRow int32, value *string) {
|
|
||||||
f.isEditing = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *TMainForm) OnDataCellEdited(sender vcl.IObject) {
|
|
||||||
// The OnEditingDone event fires whenever the TStringGrid loses focus, even
|
|
||||||
// if editing was not currently taking place
|
|
||||||
// To detect real edits, set a flag in the OnSelectEditor event
|
|
||||||
if !f.isEditing {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
f.isEditing = false
|
|
||||||
|
|
||||||
aRow := f.contentBox.Row()
|
|
||||||
aCol := f.contentBox.Col()
|
|
||||||
|
|
||||||
// If this is an insert row, no need to patch updateRows
|
|
||||||
if _, ok := f.insertRows[aRow]; ok {
|
|
||||||
return // nothing to do
|
|
||||||
}
|
|
||||||
|
|
||||||
if chk, ok := f.updateRows[aRow]; ok {
|
|
||||||
if int32slice_contains(chk, aCol) {
|
|
||||||
// nothing to do
|
|
||||||
} else {
|
|
||||||
chk = append(chk, aCol)
|
|
||||||
f.updateRows[aRow] = chk
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
f.updateRows[aRow] = []int32{aCol}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If this row was marked for deletion, this new event takes priority
|
|
||||||
delete(f.deleteRows, aRow)
|
|
||||||
|
|
||||||
// Signal repaint
|
|
||||||
f.contentBox.InvalidateRow(aRow)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *TMainForm) OnDataInsertClick(sender vcl.IObject) {
|
|
||||||
if !f.contentBox.Enabled() {
|
|
||||||
return // Not an active data view
|
|
||||||
}
|
|
||||||
|
|
||||||
rpos := f.contentBox.RowCount()
|
|
||||||
f.contentBox.SetRowCount(rpos + 1)
|
|
||||||
|
|
||||||
f.insertRows[rpos] = struct{}{}
|
|
||||||
|
|
||||||
// Scroll to bottom
|
|
||||||
f.contentBox.SetTopRow(rpos)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *TMainForm) OnDataDeleteRowClick(sender vcl.IObject) {
|
|
||||||
if !f.contentBox.Enabled() {
|
|
||||||
return // Not an active data view
|
|
||||||
}
|
|
||||||
|
|
||||||
rpos := f.contentBox.Row()
|
|
||||||
f.deleteRows[rpos] = struct{}{}
|
|
||||||
|
|
||||||
// If this row was marked for edit, this takes priority
|
|
||||||
delete(f.updateRows, rpos)
|
|
||||||
|
|
||||||
// Repaint
|
|
||||||
f.contentBox.InvalidateRow(rpos)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *TMainForm) OnDataCommitClick(sender vcl.IObject) {
|
|
||||||
if !f.contentBox.Enabled() {
|
|
||||||
return // Not an active data view
|
|
||||||
}
|
|
||||||
|
|
||||||
node := f.Buckets.Selected()
|
|
||||||
if node == nil {
|
|
||||||
vcl.ShowMessage("No database selected")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollPos := f.contentBox.TopRow()
|
|
||||||
|
|
||||||
ndata := (*navData)(node.Data())
|
|
||||||
|
|
||||||
editableLd, ok := ndata.ld.(editableLoadedDatabase)
|
|
||||||
if !ok {
|
|
||||||
vcl.ShowMessage("Unsupported action for this database")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err := editableLd.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) {
|
func (f *TMainForm) OnNavContextClose(sender vcl.IObject) {
|
||||||
curItem := f.Buckets.Selected()
|
curItem := f.Buckets.Selected()
|
||||||
if curItem == nil {
|
if curItem == nil {
|
||||||
|
@ -708,46 +508,6 @@ func (f *TMainForm) OnNavContextClose(sender vcl.IObject) {
|
||||||
// n.b. This triggers OnNavChange, which will then re-render from noLoadedDatabase{}
|
// n.b. This triggers OnNavChange, which will then re-render from noLoadedDatabase{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// func (f *TMainForm) OnQueryTextChanged(sender vcl.IObject) {
|
|
||||||
func (f *TMainForm) OnQueryTextChanged(sender vcl.IObject, key *types.Char, shift types.TShiftState) {
|
|
||||||
|
|
||||||
// FIXME changing the text colour calls the onchange handler recursively
|
|
||||||
// FIXME changing the text colour pushes into the undo stack
|
|
||||||
|
|
||||||
// Preserve
|
|
||||||
f.queryInput.Lines().BeginUpdate()
|
|
||||||
origPos := f.queryInput.SelStart()
|
|
||||||
origLen := f.queryInput.SelLength()
|
|
||||||
defer func() {
|
|
||||||
f.queryInput.SetSelStart(origPos)
|
|
||||||
f.queryInput.SetSelLength(origLen)
|
|
||||||
f.queryInput.Lines().EndUpdate()
|
|
||||||
}()
|
|
||||||
|
|
||||||
tx := strings.ToLower(f.queryInput.Text())
|
|
||||||
|
|
||||||
// Reset all existing colors
|
|
||||||
f.queryInput.SetSelStart(0)
|
|
||||||
f.queryInput.SetSelLength(int32(len(tx)))
|
|
||||||
f.queryInput.SelAttributes().SetColor(colors.ClBlack)
|
|
||||||
|
|
||||||
searchPos := 0
|
|
||||||
for {
|
|
||||||
matchPos := strings.Index(tx[searchPos:], "select")
|
|
||||||
if matchPos == -1 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
matchPos += searchPos // compensate for slicing
|
|
||||||
|
|
||||||
f.queryInput.SetSelStart(int32(matchPos))
|
|
||||||
f.queryInput.SetSelLength(6)
|
|
||||||
f.queryInput.SelAttributes().SetColor(colors.ClRed)
|
|
||||||
|
|
||||||
searchPos = matchPos + 6 // strlen(SELECT)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *TMainForm) OnQueryExecute(sender vcl.IObject) {
|
func (f *TMainForm) OnQueryExecute(sender vcl.IObject) {
|
||||||
// If query tab is not selected, switch to it, but do not exec
|
// If query tab is not selected, switch to it, but do not exec
|
||||||
if f.Tabs.ActivePageIndex() != 2 {
|
if f.Tabs.ActivePageIndex() != 2 {
|
||||||
|
@ -760,10 +520,6 @@ func (f *TMainForm) OnQueryExecute(sender vcl.IObject) {
|
||||||
queryString = f.queryInput.SelText() // Just the selected text
|
queryString = f.queryInput.SelText() // Just the selected text
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.TrimSpace(queryString) == "" {
|
|
||||||
return // prevent blank query
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
node := f.Buckets.Selected()
|
node := f.Buckets.Selected()
|
||||||
if node == nil {
|
if node == nil {
|
||||||
|
@ -772,18 +528,7 @@ func (f *TMainForm) OnQueryExecute(sender vcl.IObject) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ndata := (*navData)(node.Data())
|
ndata := (*navData)(node.Data())
|
||||||
|
ndata.ld.ExecQuery(queryString, f.queryResult)
|
||||||
queryableLd, ok := ndata.ld.(queryableLoadedDatabase)
|
|
||||||
if !ok {
|
|
||||||
vcl.ShowMessage("Unsupported action for this database")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err := queryableLd.ExecQuery(queryString, f.queryResult)
|
|
||||||
if err != nil {
|
|
||||||
vcl.ShowMessage(err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *TMainForm) OnNavChange(sender vcl.IObject, node *vcl.TTreeNode) {
|
func (f *TMainForm) OnNavChange(sender vcl.IObject, node *vcl.TTreeNode) {
|
||||||
|
@ -796,33 +541,7 @@ func (f *TMainForm) OnNavChange(sender vcl.IObject, node *vcl.TTreeNode) {
|
||||||
ld = ndata.ld
|
ld = ndata.ld
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset some controls that the render function is expected to populate
|
ld.RenderForNav(f, ndata) // Handover to the database type's own renderer function
|
||||||
f.insertRows = make(map[int32]struct{})
|
|
||||||
f.updateRows = make(map[int32][]int32)
|
|
||||||
f.deleteRows = make(map[int32]struct{})
|
|
||||||
f.isEditing = false
|
|
||||||
f.propertiesBox.Clear()
|
|
||||||
vcl_stringgrid_clear(f.contentBox)
|
|
||||||
|
|
||||||
err := ld.RenderForNav(f, ndata) // Handover to the database type's own renderer function
|
|
||||||
if err != nil {
|
|
||||||
vcl.ShowMessage(err.Error())
|
|
||||||
|
|
||||||
// Ensure elements are disabled
|
|
||||||
f.contentBox.SetEnabled(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Toggle the Edit functionality
|
|
||||||
_, ok := ld.(editableLoadedDatabase)
|
|
||||||
f.dataCommitBtn.SetEnabled(ok)
|
|
||||||
f.dataDelRowBtn.SetEnabled(ok)
|
|
||||||
f.dataInsertBtn.SetEnabled(ok)
|
|
||||||
|
|
||||||
// Toggle the Query functionality
|
|
||||||
_, ok = ld.(queryableLoadedDatabase)
|
|
||||||
f.queryInput.SetEnabled(ok)
|
|
||||||
f.queryResult.SetEnabled(ok)
|
|
||||||
f.queryExecBtn.SetEnabled(ok)
|
|
||||||
|
|
||||||
// We're in charge of common status bar text updates
|
// We're in charge of common status bar text updates
|
||||||
f.StatusBar.SetSimpleText(ld.DisplayName() + " | " + ld.DriverName())
|
f.StatusBar.SetSimpleText(ld.DisplayName() + " | " + ld.DriverName())
|
||||||
|
|
|
@ -9,13 +9,3 @@ func box_interface[T any](input []T) []interface{} {
|
||||||
}
|
}
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func int32slice_contains(haystack []int32, needle int32) bool {
|
|
||||||
for _, v := range haystack {
|
|
||||||
if v == needle {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
61
util_vcl.go
|
@ -12,8 +12,6 @@ const (
|
||||||
MY_SPACING = 6
|
MY_SPACING = 6
|
||||||
MY_HEIGHT = 90
|
MY_HEIGHT = 90
|
||||||
MY_WIDTH = 180
|
MY_WIDTH = 180
|
||||||
|
|
||||||
MAX_AUTO_COL_WIDTH = 240
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// vcl_row makes a TPanel row inside the target component.
|
// vcl_row makes a TPanel row inside the target component.
|
||||||
|
@ -59,65 +57,8 @@ func vcl_default_tab_background() types.TColor {
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
// Assuming that uxtheme is loaded
|
// Assuming that uxtheme is loaded
|
||||||
// @ref https://stackoverflow.com/a/20332712
|
// @ref https://stackoverflow.com/a/20332712
|
||||||
// return colors.ClBtnHighlight
|
return colors.ClBtnHighlight
|
||||||
|
|
||||||
// None of the colors.** constants seem to be quite right on a test
|
|
||||||
// Windows 11 machine - should be #f9f9f9
|
|
||||||
return 0x00f9f9f9
|
|
||||||
} else {
|
} else {
|
||||||
return colors.ClBtnFace // 0x00f0f0f0
|
return colors.ClBtnFace // 0x00f0f0f0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func vcl_stringgrid_clear(d *vcl.TStringGrid) {
|
|
||||||
d.SetFixedCols(0) // No left-hand cols
|
|
||||||
d.SetRowCount(1) // The 0th row is the column headers
|
|
||||||
d.SetEnabled(false)
|
|
||||||
d.Columns().Clear()
|
|
||||||
d.Invalidate()
|
|
||||||
}
|
|
||||||
|
|
||||||
func vcl_stringgrid_columnwidths(d *vcl.TStringGrid) {
|
|
||||||
// Skip slow processing for very large result sets
|
|
||||||
if d.RowCount() > 1000 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
d.AutoAdjustColumns()
|
|
||||||
|
|
||||||
// AutoAdjustColumns will leave some columns massively too large by themselves
|
|
||||||
// Reign them back in
|
|
||||||
ct := d.Columns().Count()
|
|
||||||
for i := int32(0); i < ct; i++ {
|
|
||||||
if d.ColWidths(i) > MAX_AUTO_COL_WIDTH {
|
|
||||||
d.SetColWidths(i, MAX_AUTO_COL_WIDTH)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func vcl_confirm_dialog(sender vcl.IComponent, title string, message string) bool {
|
|
||||||
|
|
||||||
dlg := vcl.NewTaskDialog(sender)
|
|
||||||
dlg.SetCaption(APPNAME)
|
|
||||||
dlg.SetTitle(title)
|
|
||||||
dlg.SetText(message)
|
|
||||||
dlg.SetCommonButtons(types.NewSet())
|
|
||||||
|
|
||||||
yesBtn := dlg.Buttons().Add()
|
|
||||||
yesBtn.SetCaption("Confirm")
|
|
||||||
yesBtn.SetModalResult(types.MrYes)
|
|
||||||
|
|
||||||
noBtn := dlg.Buttons().Add()
|
|
||||||
noBtn.SetCaption("Cancel")
|
|
||||||
noBtn.SetModalResult(types.MrCancel)
|
|
||||||
|
|
||||||
ret := dlg.Execute()
|
|
||||||
if !ret {
|
|
||||||
return false // dialog closed
|
|
||||||
}
|
|
||||||
if dlg.ModalResult() != types.MrYes {
|
|
||||||
return false // other button clicked
|
|
||||||
}
|
|
||||||
|
|
||||||
return true // confirmed
|
|
||||||
}
|
|
||||||
|
|