162 Commits

Author SHA1 Message Date
2a9f06588b voiddb: only supported on linux 2026-01-04 11:24:40 +13:00
8ee74e16c2 doc: changelog for v2.1.0 2026-01-04 11:21:03 +13:00
96630dc940 makefile: add test-redis command 2026-01-04 11:05:50 +13:00
85e245658e autoconfig: update v0.5.0 (nicer string editors/labels) 2026-01-04 11:05:42 +13:00
1de694cb88 table/binary: hide extra APPNAME label from autoconfig editor 2026-01-02 19:14:14 +13:00
312898ab3b doc: update README and TODO 2026-01-02 19:14:05 +13:00
19e36ca615 pogreb: initial support + editing + compact 2026-01-02 17:50:18 +13:00
e1403f8e7d doc: update CHANGELOG and TODO 2026-01-02 17:36:02 +13:00
eecdc2b5f2 voiddb: initial support 2026-01-02 17:35:48 +13:00
beff8bc323 config: show multiple connection errors
Not clear why this fixed it
2026-01-02 16:35:49 +13:00
06d939b06e ui: enforce smaller toolbar icon size 2025-12-29 18:24:32 +13:00
addabd85f8 app: add 'set cell to null' button (no-op), separate toolbars into 3 2025-12-29 18:24:26 +13:00
85659885ab doc: update TODO 2025-12-28 19:30:28 +13:00
f34f2f84dc table: show nullable cells as grey 2025-12-28 19:30:23 +13:00
cbc14b261c sqlite: always use extra rowid column as primary key 2025-12-28 19:24:04 +13:00
4f29d531c1 sqlite: use GetCell for preserving typed data through update/insert 2025-12-28 19:21:54 +13:00
b1634e92d4 table: add getCell for dynamic values 2025-12-28 19:21:22 +13:00
4ce9b753e9 sqlite: hide sqlite_sequence and _stat1 tables 2025-12-28 19:04:48 +13:00
1c2567dc3c sqlite: remove builtin LIMIT for now 2025-12-28 19:04:40 +13:00
f2def1371b ui: fix text in context menu when toggling toolbar 2025-12-28 18:44:55 +13:00
4a3e37cdc4 sqliteclidriver: declare all columns are nullable BLOBs 2025-12-28 18:38:22 +13:00
f165853c6b sqlite: always rely on column information from query cursor 2025-12-28 18:28:08 +13:00
93ce3d4a90 ui: non-queryable databases should disable entire tab 2025-12-28 18:27:50 +13:00
88346852b3 ui: when adding new db, always start on properties tab 2025-12-28 18:27:41 +13:00
d15d42c37e sqlite: refactor to use strongly typed column data 2025-12-28 18:02:56 +13:00
76581b9454 table_binary: improve performance by eliding large data values for display 2025-12-28 18:02:28 +13:00
5c9f165aa7 table_binary, _string: show more detailed type errors 2025-12-28 18:02:18 +13:00
a576138428 table_dynamic: use correct format specifier for type information in error 2025-12-28 18:02:00 +13:00
f7f3bfb035 table: add dynamicColumn{}, use for sqlite 2025-12-23 16:46:12 +13:00
53c90bf0dc table: no need for ds_normal, setting delegate to nil is sufficient 2025-12-23 16:45:56 +13:00
2f3c956549 fix execquery being called twice 2025-12-23 15:56:31 +13:00
7c441ecc50 embeddedversions: clickable links to module dependencies 2025-12-23 15:53:24 +13:00
f75a161acd table: remove columnType abstraction 2025-12-23 15:38:02 +13:00
0002c82594 table: reuse delegate, move customization into interface method 2025-12-23 15:28:29 +13:00
13fedfa2f6 doc: update README and TODO 2025-12-22 19:48:36 +13:00
8b06ccef48 buntdb: add support, including editing+shrink 2025-12-22 18:43:48 +13:00
17d6b5172d ui: use compress icon for bolt-zip tool 2025-12-22 18:23:13 +13:00
708e8072ff sqlite: support ssh connections 2025-12-22 18:17:41 +13:00
c8125d2c84 sqliteclidriver: add ssh backend 2025-12-22 18:17:35 +13:00
a5138a51f3 sqliteclidriver: refactor cmd handling to support other implementations 2025-12-22 18:17:29 +13:00
02b5a8fd48 sqliteclidriver: add more comments, expose DriverName const 2025-12-22 18:17:06 +13:00
b2118c9196 config: add omitempty for many db types 2025-12-22 18:16:35 +13:00
5aa76a465c sshtunnel: add icons for connection modes, add omitempty 2025-12-22 18:16:11 +13:00
f3e729b023 assets: add compress, key, page_key icons 2025-12-22 18:15:55 +13:00
5c7a99d16b makefile: autogenerate embed.qrc from asset files 2025-12-22 18:15:49 +13:00
4d5ca19b47 doc/changelog: changelog for v2.0.0 2025-12-20 19:19:46 +13:00
a0e70636a1 doc/LICENSE: merge qbolt/yvbolt license headers 2025-12-20 19:19:46 +13:00
13e139e023 app: show version in main window titlebar 2025-12-20 19:19:46 +13:00
1f7d03e67a doc: update screenshots 2025-12-20 19:19:46 +13:00
0b1f662e99 doc/README: add other features from qbolt's readme 2025-12-20 19:19:46 +13:00
e5f36b0f66 doc/README: remove yvbolt/qbolt cross reference link 2025-12-20 19:19:46 +13:00
0eccb12744 doc/README: add direct links to changelog/downloads 2025-12-20 19:19:46 +13:00
f2d3240153 git: merge yvbolt/qbolt changelogs as separate file 2025-12-20 19:19:46 +13:00
9fb4302000 git: merge all yvbolt/qbolt names 2025-12-20 19:19:40 +13:00
e3f94f1eba git: synthetic merge of qbolt and yvbolt branches 2025-12-20 18:32:58 +13:00
4e13d8dffd doc/README: changelog for v1.1.0 2025-05-04 14:45:50 +12:00
20e5efa711 makefile: set version to 1.1.0 2025-05-04 14:45:42 +12:00
6378740051 bump bbolt to v1.4.0 2025-05-04 14:43:03 +12:00
b8ce7a667b makefile: use miqt-docker for windows build 2025-05-04 14:25:22 +12:00
281ca18d90 uic: update to latest miqt-uic 2025-05-04 14:23:14 +12:00
1725de6ace import/export: use background thread 2025-04-18 12:13:56 +12:00
0ad7c03db0 makefile: add workaround for nested menu generation 2025-04-17 21:53:04 +12:00
1cdd0d113b support drag+drop database files in 2025-04-17 21:48:52 +12:00
a0fae43690 makefile/uic: work around miqt issue with SetObjectName 2025-04-17 21:27:06 +12:00
04ac766125 windows build: update build command for latest miqt 2025-04-17 21:26:51 +12:00
9ac26467c0 add zip roundtrip conversion feature 2025-04-17 21:26:27 +12:00
cbfc038839 add f1/f5 keyboard shortcuts for help/refresh 2025-04-17 21:25:38 +12:00
7ae6462da0 qbolt: do more work in []byte space instead of strings 2025-04-17 21:25:02 +12:00
09a3e5b90f main: force highdpi attributes (helps on windows) 2025-04-16 18:37:39 +12:00
1cddd17017 make(linux): smaller output binary 2025-04-16 18:33:42 +12:00
8fdc3a0428 makefile: allow overriding $GO 2025-04-16 18:17:52 +12:00
18f10fc1b4 update to miqt 0.10, update to qt 6 2025-04-16 18:17:44 +12:00
18331ae007 doc/README: updates for v1.0.3 2024-10-05 17:44:27 +13:00
f08242e93c Merge branch 'qbolt-go' 2024-10-05 17:42:14 +13:00
23965230e2 make: add windows resources, improve output compression 2024-10-05 17:41:38 +13:00
e43b261752 build: upgrade makefile/dockerfile for miqt and win64 support 2024-10-05 16:37:23 +13:00
f9b4cb71a5 qbolt: show makefile-based version number in help dialog 2024-10-05 16:37:15 +13:00
96c05641bd mod: upgrade bbolt v1.3.5 -> v1.3.11 2024-10-05 16:36:41 +13:00
2c5d1946ec qbolt: fixes post-port 2024-10-05 15:58:56 +13:00
a6cbc5a9ed qbolt: initial port to go/miqt 2024-10-03 19:34:28 +13:00
6008ae44a2 doc: rename image directory for teafolio 2021-04-12 18:03:05 +12:00
99096b2360 doc/README: add v1.0.2 changelog 2021-04-12 18:01:46 +12:00
0e92459779 no-op merge with legacy-codesite branch 2021-04-12 17:52:00 +12:00
a13eaaf523 make: upx doesn't support current mxe mingw/qt linked binaries 2021-04-12 17:44:38 +12:00
dca8d277d3 make: option to build windows binary in docker 2021-04-12 17:44:17 +12:00
15c739c0b9 squash 2021-04-12 17:43:59 +12:00
fc2da972ef dummy-data: fix build for renamed bbolt package 2021-04-12 17:07:16 +12:00
e45eea7111 makefile: remove 'hg export' function 2021-04-12 17:04:40 +12:00
1e62d79c07 doc: delete TODO.txt (issues now being tracked in Gitea)
The new issue tracker is at https://git.ivysaur.me/code.ivysaur.me/qbolt/issues
2021-04-12 17:04:27 +12:00
c74c5ae5c0 doc: move README/TODO to top-level directory 2021-04-12 17:00:49 +12:00
b0092c4a6e vendor: switch bolt implementation to etcd-io/bbolt v1.3.5 2021-04-12 17:00:40 +12:00
5ce6368c4a qt: fix missing -lpthread on Debian Bullseye 2021-04-12 16:54:06 +12:00
3e7e54da4b go: add go.mod file for bolt dependency 2021-04-12 16:53:57 +12:00
9f80d687b2 hg2git: replace hgignore/hgtags files 2021-04-12 16:53:43 +12:00
96411e877f bump all versions to 1.0.2 2017-06-19 21:04:40 +12:00
ac7e078c02 Added tag release-1.0.1 for changeset 0528a0ab20b6 2017-06-19 21:04:23 +12:00
e13314f5dc 1.0.1 meta 2017-06-19 21:04:17 +12:00
b50c3e738a doc: update features list in readme 2017-06-19 21:02:42 +12:00
54ad6015b7 more binary correctness 2017-06-19 20:53:50 +12:00
40e84ac230 dummy-data: change to generate binary names 2017-06-19 20:45:26 +12:00
767eaa0a47 add fallback display for non-printable characters 2017-06-19 20:42:51 +12:00
bb594e768c doc: update readme 2017-06-19 20:42:42 +12:00
516bd99c4d doc: update TODO 2017-06-19 20:28:33 +12:00
571bfcf4b6 one more preservation for previous 2017-06-19 20:27:52 +12:00
26f7a11d80 preserve the binary content of keys and bucket names during edit operations 2017-06-19 20:27:05 +12:00
6fb8d1ba0c commit all archived files 2017-06-19 00:00:00 +00:00
21588021d3 doc: update TODO 2017-05-25 20:00:10 +12:00
7441e0c15b option to open database as read-only 2017-05-25 19:59:53 +12:00
142f3f6bf4 track screenshot of qbolt on windows 2017-05-25 19:54:35 +12:00
19ddb1c956 select parent when deleting bucket (maybe fixes a crash?) 2017-05-25 19:53:35 +12:00
97467eae4d add icon for win32 binary 2017-05-25 19:50:11 +12:00
17c37b6568 bump dist version to 1.0.1 2017-05-21 18:23:41 +12:00
57237ef2e8 Added tag release-1.0.0 for changeset 74cacbbe8f6c 2017-05-21 18:23:29 +12:00
164f5a071d doc: update README 2017-05-21 18:22:23 +12:00
ac46b16d0e hgignore: simplify 2017-05-21 18:19:47 +12:00
b9d114a2d8 remove extra .db file from dist archives 2017-05-21 18:18:45 +12:00
b368457247 add some screenshots 2017-05-21 18:18:14 +12:00
fb940d5f60 doc: TODO.txt 2017-05-21 18:18:10 +12:00
6d1e671e44 doc: README 2017-05-21 18:18:04 +12:00
b5149c4efa bolt: strict 10 second timeout for opening databases 2017-05-21 18:05:20 +12:00
f0f642b6b0 add many more icons 2017-05-21 18:05:11 +12:00
7c62abb352 working add items 2017-05-21 17:59:27 +12:00
4f1c48b55d working edit/delete items 2017-05-21 17:53:40 +12:00
37f9307db1 wip set/delete items 2017-05-21 17:44:07 +12:00
14e7ebb59c go: simplify out a wrapper function 2017-05-21 17:26:09 +12:00
14510545d4 fix right-hand default pane when a bucket is selected 2017-05-21 17:14:32 +12:00
906cff2e57 working bucket deletions 2017-05-21 17:14:20 +12:00
bf28495b1f add our application icon to system popups 2017-05-21 17:12:02 +12:00
98b78ab1e6 option to create new buckets / sub-buckets 2017-05-21 17:08:15 +12:00
4394c8a50a option to create new databases 2017-05-21 16:50:17 +12:00
9fb5bdad78 always open databases read/write 2017-05-21 16:47:28 +12:00
d2ec12798e popup editor to view record content 2017-05-21 16:44:44 +12:00
5270dd00bc display data keys, data item length 2017-05-21 16:15:49 +12:00
35f28fa5ed makefile: fix qbolt.a target 2017-05-21 15:59:00 +12:00
5ea969e10c pretty-print json display 2017-05-21 15:49:47 +12:00
d0becd0c3c clean up qt project, always refer to makefile-produced qbolt.a files 2017-05-21 15:00:53 +12:00
1c81444645 makefile: distribution targets 2017-05-21 14:51:22 +12:00
d7c3bfd1f5 unify makefile, make win32 builds 2017-05-21 14:33:46 +12:00
0f1cc014d7 working 'refresh buckets' action 2017-05-21 13:49:41 +12:00
6ac8c3e67b remove dead code 2017-05-21 13:49:36 +12:00
546681a0ef working cgo slices, working nested-bucket operations 2017-05-21 13:47:46 +12:00
679d1140cc recursive bucket scan - we're crashing when passing a slice to go code 2017-05-21 13:31:28 +12:00
60d71104e8 retrieve bucket properties 2017-05-21 12:39:55 +12:00
8d8374be24 initial meta commit 2017-05-21 00:00:00 +00:00
95a1bfeea5 retrieve database properties 2017-05-21 11:59:38 +12:00
8597f270f6 context menus, disconnection, re-refresh, selection behaviour, ^O shortcut 2017-05-21 11:47:16 +12:00
d32ab82d73 don't pass go strings to c - pass char*+len, callee must call free() 2017-05-20 23:02:58 +12:00
b6d1cafe54 cgo: minor cleanups, remove unused export 2017-05-20 18:15:39 +12:00
3911d7d66c convert cgo function pointer call to repeated iteration 2017-05-20 18:01:55 +12:00
be594cb1a5 call bucket enumeration - function pointers are misbehaving 2017-05-20 17:28:27 +12:00
8a93f163c4 c++ wrappers for bucket enumeration function 2017-05-20 15:47:18 +12:00
e82d0a5dd2 add top-level db items to ui 2017-05-20 15:47:05 +12:00
4b3fb783c4 wire up other menu items, add app logo icon 2017-05-20 15:20:30 +12:00
e8030a9579 cgo: change interface to not return already-collected error interfaces 2017-05-20 15:13:16 +12:00
a94a330345 add GetMagic() check, move c/go interop into separate classes 2017-05-20 14:57:51 +12:00
8ce03cbed6 go: build .a instead of .so 2017-05-20 14:57:19 +12:00
b76fad9512 hgignore: pro.user file 2017-05-18 20:11:19 +12:00
a9158c3a0c gms: fix missing initialisation 2017-05-16 19:47:50 +12:00
dc31787526 track sample.db file 2017-05-16 19:47:41 +12:00
ff7484079e track dummy-data generator 2017-05-16 19:47:26 +12:00
6e1f9ca1e1 hgignore 2017-05-16 19:47:17 +12:00
ac56f5e6c8 initial commit 2017-05-16 19:34:54 +12:00
61 changed files with 1872 additions and 675 deletions

11
.gitignore vendored
View File

@@ -1,8 +1,5 @@
testdata/ testdata/
liblcl-*.zip qbolt
liblcl.so qbolt.exe
liblcl.dll qbolt.linux64.tar.xz
yvbolt qbolt.win64.zip
yvbolt.exe
yvbolt.linux64.tar.xz
yvbolt.win64.zip

210
CHANGELOG.md Normal file
View File

@@ -0,0 +1,210 @@
# Changelog
2026-01-04 v2.1.0
- SQLite: Support SSH tunnel
- SQLite: Strict type handling, support nullable columns, always rely on `rowid` as PK
- SQLite: Remove hardcoded LIMIT
- SQLite: Hide internal sqlite_sequence and sqlite_stat1 tables
- BuntDB: Initial support, including editing
- VoidDB: Initial support, including editing and multiple keyspace management
- Pogreb: Initial support, including compaction
- Fix an issue with SQL queries running twice
- Fix an issue with not showing multiple connection errors from the connection manager dialog
- Truncate large binary column display in grid
- UI: Add icons for SSH/Password connection options, for Bolt/Zip conversion tool
- UI: Show 'null' as grey in tables
- UI: Clickable links to Go.mod dependencies in 'Driver versions' data table
- UI: When connecting to a new DB, always start on the Properties tab
- UI: Allow toggling separate toolbar sections in context menu
- UI: Disable Query tab for non-queryable database types
- UI: Fix extra labels appearing when editing large binary columns
2025-12-20 v2.0.0
- Merge yvbolt and QBolt together
- App: Show application version in main window titlebar
2025-12-20 v0.11.0 (yvbolt)
- Badger: Change encryption configuration options (**BREAKING**)
- SSH Agent: Initial support, including lock/unlock
- Badger, SQLite: Fix "all files" spec when choosing an export file path
- Bolt: Support import/export as zip archive
- Support SSH agents when using SSH tunnels (e.g. for Redis or Mongo)
2025-12-17 v0.10.1 (yvbolt)
- Fix mixed Qt 5 / Qt 6 syntax
- Build release binaries with Go1.26
- [⬇️ Download here](https://git.ivysaur.me/code.ivysaur.me/yvbolt/releases/tag/v0.10.1)
2025-12-16 v0.10.0 (yvbolt)
- MongoDB: Initial support, including SSH tunnel, managing collections, traversing BSON documents, and querying
- LotusDB: Initial support, including editing
- Bitcask: Initial support, including editing and backup
- RoseDB: Initial support, including editing
- Badger: Fix k/v column display, support editing
- LMDB: Support editing, add warning for destructive actions
- LevelDB: Support editing
- Pebble: Support editing
- Starskey: Fix k/v column display, support editing
- Redis: Bump driver version v9.16.0 -> v9.17.2
- Bolt: Add warning for destructive actions
- Connection manager: When saving, use the database's preferred name
- App: Add hex viewer for binary data if it is not valid UTF-8
- App: Use virtualized table renderer
- App: Use multiple plug-in typed column stores in table backend
- App: Fix graphical flicker when editing connections on Windows
- App: Set up icon and exe properties on Windows
- App: Embed version number in release builds
- App: Show all driver versions as a virtual data table
- App: Show unsaved-changes warning also when changing database
- App: Fix rich text formatting appearing when pasting into the query window
2025-12-02 v0.9.0 (yvbolt)
- LMDB: Initial support, including multi-database mode, data editing, and managing child databases
- Starskey: Initial support
- Freedesktop.org Secret Service: Initial support, including unlocking collections and creating child collections
- App: Use global toolbar style
- App: Add connection manager, encrypting credentials with AEAD AES256-GCM using OS keychain
- App: Offer to save valid quick-connection to the connection manager
- App: Add 'About Qt' menu option
- App: Fixed file extension filter for database files with no extension
- App: Fixed background colour for Properties area on different OSes
- App: Fixed libpng warnings about greyscale image data for embedded logo images
- App: Fixed issue with non-UTF8 child database names
- Debconf: Improve navigation speed by splitting applications into virtual tables
- SSH: Redesign options to pick only one of the available auth methods
- Fixed running release binary on Debian 12
- [⬇️ Download here](https://git.ivysaur.me/code.ivysaur.me/yvbolt/releases/tag/v0.9.0)
2025-11-23 v0.8.0 (yvbolt)
- Port from [liblcl](https://github.com/ying32/liblcl) to [MIQT Qt 6](https://github.com/mappu/miqt)
- Badger: Upgrade v4.2.0 -> v4.8.0
- Pebble: Upgrade v1.0.0 -> v1.1.5
- SQLite: Upgrade v1.14.22 -> v1.14.32
- Redis: Upgrade v9.5.3 -> v9.16.0
- Bolt: Upgrade v1.4.0-alpha.1 -> v1.4.3
- Bolt: Fix child buckets appearing in data area
- Badger, Pebble, Debconf: Remove redundant "Data" navigation layer
- Badger: Support encrypted databases
- Badger: Support readonly databases
- Badger: Add context-menu actions for backup, restore, and compact
- Pebble: Support readonly databases
- LevelDB: Add LevelDB database integration
- Redis: Support SSH tunnel
- SQLite: Allow editing the primary key column
- App: New style connection dialog
- App: Updated keyboard shortcuts (Ctrl+O to open new connection, F5 to refresh, F9 to execute query)
- App: Add confirmation when refreshing the data table if there are uncommitted changes
- [⬇️ Download here](https://git.ivysaur.me/code.ivysaur.me/yvbolt/releases/tag/v0.8.0)
2025-05-04 1.1.0 (qbolt)
- New feature to import/export database as zip archive
- Upgrade to Qt 6
- Add keyboard shortcuts for refresh
- Improve High DPI support
- Rebuild artefacts with miqt v0.10.0, etcd-io/bbolt v1.4.0, go 1.23, Qt 6.8 (win64)
- [⬇ Download here](https://git.ivysaur.me/code.ivysaur.me/qbolt/releases/tag/v1.1.0)
2024-10-05 1.0.3 (qbolt)
- Port from hybrid Go/C++ to now using [MIQT](https://github.com/mappu/miqt)
- Switch Windows build to win64
- Rebuild artefacts with miqt v0.5.0, etcd-io/bbolt v1.3.11, go 1.19 (deb12), go 1.23 (win64)
- [⬇ Download here](https://git.ivysaur.me/code.ivysaur.me/qbolt/releases/tag/v1.0.3)
2024-07-18 v0.7.0 (yvbolt)
- 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 here](https://git.ivysaur.me/code.ivysaur.me/yvbolt/releases/tag/v0.7.0)
2024-06-30 v0.6.0 (yvbolt)
- Debconf: Add as supported database
- SQLite: Support table names containing special characters
- SQLite: Improvements for experimental command-line driver
- Redis: Improve connection dialog window position
- App: Cosmetic fixes for frame borders, help dialog, and Windows fonts+colours
- Build: Change compression parameters for release builds
- Build: Compile CGO with -O2 for release builds
- [⬇️ Download here](https://git.ivysaur.me/code.ivysaur.me/yvbolt/releases/tag/v0.6.0)
2024-06-29 v0.5.0 (yvbolt)
- Pebble: Add as supported database
- Bolt: Support opening as readonly
- Bolt: Support creating new databases
- Bolt: Support adding/removing recursive child buckets
- SQLite: Support custom CLI driver that parses `/usr/bin/sqlite3 -json` output (experimental)
- Redis: Improve query parser to support quoted strings
- App: Support refreshing elements in nav tree
- App: Help menu option to show driver versions
- App: Add image icons for refresh and close context menu actions
- Build: Add makefile for cross-compiling release binaries
- [⬇️ Download here](https://git.ivysaur.me/code.ivysaur.me/yvbolt/releases/tag/v0.5.0)
2024-06-23 v0.4.0 (yvbolt)
- Redis: Add as supported database
- Badger: Allow creating in-memory databases
- App: Allow selecting partial query text to execute
- App: Allow closing database connections from context menu
- App: Allow scrolling large content on Properties pane
- App: Preload recursive navigation
- App: Automatically switch to selected database when new connection is created
- App: Add help website link
- App: Add database logo images
2024-06-25 v0.3.0 (yvbolt)
- Badger: Add BadgerDB v4 as supported database
- SQLite: Add support for CGo-free SQLite driver under cross-compilation
- Bolt: Update Bolt to v1.4.0-alpha.1
- App: Add support for running custom queries
- App: Add status bar showing currently selected DB
- App: Fix missing icons in nav when selecting items
- App: Fix extra quotemarks when browsing string content of database
2024-06-08 v0.2.0 (yvbolt)
- SQLite: Add SQLite support (now requires CGo)
- App: Add images for menu and navigation items
2024-06-03 v0.1.0 (yvbolt)
- Initial public release
2020-04-12 1.0.2 (qbolt)
- Rebuild artefacts with etcd-io/bbolt v1.3.5, go 1.15, Qt 5.15, and new GCC versions
- Switch from hg to Git
- Use Go modules
- Add support for building Windows binary in Docker
- [⬇ Download here](https://git.ivysaur.me/code.ivysaur.me/qbolt/releases/tag/v1.0.2)
2017-06-19 1.0.1 (qbolt)
- Feature: Option to open database as read-only
- Fix an issue with support for bucket names and keys not surviving UTF-8 roundtrips (now binary-clean)
- Fix an issue with crashing when deleting a bucket other than the selected one
- Fix a cosmetic issue with application icon on Windows
- [⬇ Download here](https://git.ivysaur.me/code.ivysaur.me/qbolt/releases/tag/v1.0.1)
2017-05-21 1.0.0 (qbolt)
- Initial public release
- The project consists of two parts; a C binding (CGo) for the embeddable Bolt database engine, and a graphical interface built in C++/Qt that links to it.
- [⬇ Download here](https://git.ivysaur.me/code.ivysaur.me/qbolt/releases/tag/v1.0.0)

View File

@@ -1,7 +1,8 @@
ISC License ISC License
Copyright 2024 mappy Copyright 2025 mappy
Copyright 2024 The yvbolt Author(s) Copyright 2025 The QBolt Author(s)
Copyright 2025 The yvbolt Author(s)
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.

View File

@@ -9,6 +9,7 @@ GO_WINRES ?= ~/go/bin/go-winres
.PHONY: generate .PHONY: generate
generate: generate:
/bin/bash -c '( echo "<RCC>" ; echo " <qresource prefix=\"/\">" ; for f in assets/* ; do echo " <file>$$f</file>" ; done ; echo " </qresource>" ; echo "</RCC>" ) > embed.qrc'
$(MIQT_UIC) -InFile mainwindow.ui -OutFile mainwindow.go -Qt6 $(MIQT_UIC) -InFile mainwindow.ui -OutFile mainwindow.go -Qt6
$(MIQT_UIC) -InFile connectionDialog.ui -OutFile connectionDialog.go -Qt6 $(MIQT_UIC) -InFile connectionDialog.ui -OutFile connectionDialog.go -Qt6
$(MIQT_UIC) -InFile connectionManagerDialog.ui -OutFile connectionManagerDialog.go -Qt6 $(MIQT_UIC) -InFile connectionManagerDialog.ui -OutFile connectionManagerDialog.go -Qt6
@@ -25,39 +26,39 @@ optimize-images:
optipng -quiet -o5 assets/*.png optipng -quiet -o5 assets/*.png
make generate make generate
yvbolt: $(SOURCES) qbolt: $(SOURCES)
# Target a debian-12 baseline build # Target a debian-12 baseline build
sed -i -re 's/".+"/"$(GIT_REV)"/g' version.go sed -i -re 's/".+"/"$(GIT_REV)"/g' version.go
$(MIQT_DOCKER) linux64-go1.26-qt6.4-dynamic -minify-build $(MIQT_DOCKER) linux64-go1.26-qt6.4-dynamic -minify-build
git checkout -- version.go git checkout -- version.go
chmod 755 yvbolt chmod 755 qbolt
upx --lzma yvbolt upx --lzma qbolt
yvbolt.exe: $(SOURCES) qbolt.exe: $(SOURCES)
sed -i -re 's/".+"/"$(GIT_REV)"/g' version.go sed -i -re 's/".+"/"$(GIT_REV)"/g' version.go
$(MIQT_DOCKER) win64-cross-go1.26-qt6.5-static -windows-build --tags=windowsqtstatic $(MIQT_DOCKER) win64-cross-go1.26-qt6.5-static -windows-build --tags=windowsqtstatic
git checkout -- version.go git checkout -- version.go
$(GO_WINRES) patch --in winres.json --no-backup --product-version git-tag --file-version git-tag yvbolt.exe $(GO_WINRES) patch --in winres.json --no-backup --product-version git-tag --file-version git-tag qbolt.exe
upx --lzma yvbolt.exe upx --lzma qbolt.exe
yvbolt.apk: $(SOURCES) qbolt.apk: $(SOURCES)
$(MIQT_DOCKER) android-armv8a-go1.23-qt6.6-dynamic -android-build $(MIQT_DOCKER) android-armv8a-go1.23-qt6.6-dynamic -android-build
yvbolt.linux64.tar.xz: yvbolt qbolt.linux64.tar.xz: qbolt
rm -f yvbolt.linux64.tar.xz rm -f qbolt.linux64.tar.xz
XZ_OPT='-T0 -9' tar caf yvbolt.linux64.tar.xz --owner=0 --group=0 yvbolt XZ_OPT='-T0 -9' tar caf qbolt.linux64.tar.xz --owner=0 --group=0 qbolt
yvbolt.win64.zip: yvbolt.exe qbolt.win64.zip: qbolt.exe
rm -f yvbolt.win64.zip rm -f qbolt.win64.zip
zip -9 yvbolt.win64.zip yvbolt.exe zip -9 qbolt.win64.zip qbolt.exe
.PHONY: dist .PHONY: dist
dist: yvbolt.linux64.tar.xz yvbolt.win64.zip dist: qbolt.linux64.tar.xz qbolt.win64.zip
.PHONY: clean .PHONY: clean
clean: clean:
git checkout -- version.go git checkout -- version.go
rm -f yvbolt.exe yvbolt yvbolt.linux64.tar.xz yvbolt.win64.zip rm -f qbolt.exe qbolt qbolt.linux64.tar.xz qbolt.win64.zip
##### #####
# Test databases in Docker # Test databases in Docker
@@ -65,3 +66,7 @@ clean:
.PHONY: test-mongo .PHONY: test-mongo
test-mongo: test-mongo:
sudo docker run --rm -p 127.0.0.1:27017:27017 -e MONGO_INITDB_ROOT_USERNAME=root -e MONGO_INITDB_ROOT_PASSWORD=toor mongo:latest sudo docker run --rm -p 127.0.0.1:27017:27017 -e MONGO_INITDB_ROOT_USERNAME=root -e MONGO_INITDB_ROOT_PASSWORD=toor mongo:latest
.PHONY: test-redis
test-redis:
sudo docker run --rm -p 127.0.0.1:6379:6379 redis:latest

180
README.md
View File

@@ -1,4 +1,4 @@
# yvbolt # QBolt
A graphical interface for multiple databases. A graphical interface for multiple databases.
@@ -8,22 +8,23 @@ A graphical interface for multiple databases.
- Connect to multiple databases at once - Connect to multiple databases at once
- 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
- Edit content, and add/delete rows for supported databases
- View database/bucket statistics and metadata
- Run custom SQL queries - Run custom SQL queries
- Select text to run partial query - Select text to run partial query
- Safe handling for non-UTF8 key and data fields - Safe handling for non-UTF8 key and data fields
- Hex viewer for binary data - Hex viewer for binary data
- Connection Manager saves connections with AEAD AES256-GCM using OS keychain - Connection Manager saves connections with AEAD AES256-GCM using OS keychain
See also [qbolt](https://code.ivysaur.me/qbolt) for more/different functionality.
## Supported databases ## Supported databases
There are currently 15 supported databases: There are currently 18 supported databases:
Database |Read |Editing |Query |Connection options |Context menu actions Database |Read |Editing |Query |Connection options |Context menu actions
-------------|------|---------|------|--------------------|-------- -------------|------|---------|------|--------------------|--------
Badger v4 |Yes |Yes |No |Encrypted, readonly, in-memory |Backup, restore, compact Badger v4 |Yes |Yes |No |Encrypted, readonly, in-memory |Backup, restore, compact
Bitcask |Yes |Yes |No |Readonly, autorecovery |Backup Bitcask |Yes |Yes |No |Readonly, autorecovery |Backup
BuntDB |Yes |Yes |No |In-memory |Shrink
Bolt |Yes |Yes |No |Readonly |Create/delete child buckets, import/export as zip Bolt |Yes |Yes |No |Readonly |Create/delete child buckets, import/export as zip
Debconf |Yes |No |No | | Debconf |Yes |No |No | |
Freedesktop.org Secret Service |Yes |No | No | |Unlock, create new collection Freedesktop.org Secret Service |Yes |No | No | |Unlock, create new collection
@@ -32,11 +33,13 @@ LMDB |Yes |Yes |No |Multi-DB, readonly |Create/delete child d
LotusDB |Yes |Yes |No | | LotusDB |Yes |Yes |No | |
MongoDB |Yes |No |Yes |SSH tunnel |Create/delete child databases and collections MongoDB |Yes |No |Yes |SSH tunnel |Create/delete child databases and collections
Pebble |Yes |Yes |No |Readonly, in-memory | Pebble |Yes |Yes |No |Readonly, in-memory |
Pogreb |Yes |Yes |No | |Compact
Redis |Yes |No |Yes |SSH tunnel, RESP v3 | Redis |Yes |No |Yes |SSH tunnel, RESP v3 |
RoseDB |Yes |Yes |No | | RoseDB |Yes |Yes |No | |
SQLite |Yes |Yes |Yes |CLI driver, in-memory |Vacuum, export SQLite |Yes |Yes |Yes |**SSH tunnel**, CLI driver, in-memory |Vacuum, export
SSH Agent |Yes |No |No |Unix/TCP |Lock, unlock SSH Agent |Yes |No |No |Unix/TCP |Lock, unlock
Starskey |Yes |Yes |No |Compression | Starskey |Yes |Yes |No |Compression |
VoidDB |Yes |Yes |No |Multi-keyspace |Create/delete child keyspaces
## License ## License
@@ -47,167 +50,10 @@ The code in this project is licensed under the ISC license (see `LICENSE` file f
- This project includes trademarked logo images for each supported database type. - This project includes trademarked logo images for each supported database type.
- The Windows binary is released under LGPL-3+ owing to the static copy of Qt. - The Windows binary is released under LGPL-3+ owing to the static copy of Qt.
## Download
Get the latest version from [the releases page »](https://git.ivysaur.me/code.ivysaur.me/qbolt/releases)
## Changelog ## Changelog
2025-12-20 v0.11.0 See [the full change history »](https://git.ivysaur.me/code.ivysaur.me/qbolt/src/branch/master/CHANGELOG.md)
- Badger: Change encryption configuration options (**BREAKING**)
- SSH Agent: Initial support, including lock/unlock
- Badger, SQLite: Fix "all files" spec when choosing an export file path
- Bolt: Support import/export as zip archive
- Support SSH agents when using SSH tunnels (e.g. for Redis or Mongo)
2025-12-17 v0.10.1
- Fix mixed Qt 5 / Qt 6 syntax
- Build release binaries with Go1.26
[⬇️ Download for Windows x86_64](https://git.ivysaur.me/code.ivysaur.me/yvbolt/releases/download/v0.10.1/yvbolt.win64.zip)
[⬇️ Download for Linux x86_64](https://git.ivysaur.me/code.ivysaur.me/yvbolt/releases/download/v0.10.1/yvbolt.linux64.tar.xz)
2025-12-16 v0.10.0
- MongoDB: Initial support, including SSH tunnel, managing collections, traversing BSON documents, and querying
- LotusDB: Initial support, including editing
- Bitcask: Initial support, including editing and backup
- RoseDB: Initial support, including editing
- Badger: Fix k/v column display, support editing
- LMDB: Support editing, add warning for destructive actions
- LevelDB: Support editing
- Pebble: Support editing
- Starskey: Fix k/v column display, support editing
- Redis: Bump driver version v9.16.0 -> v9.17.2
- Bolt: Add warning for destructive actions
- Connection manager: When saving, use the database's preferred name
- App: Add hex viewer for binary data if it is not valid UTF-8
- App: Use virtualized table renderer
- App: Use multiple plug-in typed column stores in table backend
- App: Fix graphical flicker when editing connections on Windows
- App: Set up icon and exe properties on Windows
- App: Embed version number in release builds
- App: Show all driver versions as a virtual data table
- App: Show unsaved-changes warning also when changing database
- App: Fix rich text formatting appearing when pasting into the query window
2025-12-02 v0.9.0
- LMDB: Initial support, including multi-database mode, data editing, and managing child databases
- Starskey: Initial support
- Freedesktop.org Secret Service: Initial support, including unlocking collections and creating child collections
- App: Use global toolbar style
- App: Add connection manager, encrypting credentials with AEAD AES256-GCM using OS keychain
- App: Offer to save valid quick-connection to the connection manager
- App: Add 'About Qt' menu option
- App: Fixed file extension filter for database files with no extension
- App: Fixed background colour for Properties area on different OSes
- App: Fixed libpng warnings about greyscale image data for embedded logo images
- App: Fixed issue with non-UTF8 child database names
- Debconf: Improve navigation speed by splitting applications into virtual tables
- SSH: Redesign options to pick only one of the available auth methods
- Fixed running release binary on Debian 12
[⬇️ Download for Windows x86_64](https://git.ivysaur.me/code.ivysaur.me/yvbolt/releases/download/v0.9.0/yvbolt.win64.zip)
[⬇️ Download for Linux x86_64](https://git.ivysaur.me/code.ivysaur.me/yvbolt/releases/download/v0.9.0/yvbolt.linux64.tar.xz)
2025-11-23 v0.8.0
- Port from [liblcl](https://github.com/ying32/liblcl) to [MIQT Qt 6](https://github.com/mappu/miqt)
- Badger: Upgrade v4.2.0 -> v4.8.0
- Pebble: Upgrade v1.0.0 -> v1.1.5
- SQLite: Upgrade v1.14.22 -> v1.14.32
- Redis: Upgrade v9.5.3 -> v9.16.0
- Bolt: Upgrade v1.4.0-alpha.1 -> v1.4.3
- Bolt: Fix child buckets appearing in data area
- Badger, Pebble, Debconf: Remove redundant "Data" navigation layer
- Badger: Support encrypted databases
- Badger: Support readonly databases
- Badger: Add context-menu actions for backup, restore, and compact
- Pebble: Support readonly databases
- LevelDB: Add LevelDB database integration
- Redis: Support SSH tunnel
- SQLite: Allow editing the primary key column
- App: New style connection dialog
- App: Updated keyboard shortcuts (Ctrl+O to open new connection, F5 to refresh, F9 to execute query)
- App: Add confirmation when refreshing the data table if there are uncommitted changes
[⬇️ Download for Windows x86_64](https://git.ivysaur.me/code.ivysaur.me/yvbolt/releases/download/v0.8.0/yvbolt.win64.zip)
[⬇️ Download for Linux x86_64](https://git.ivysaur.me/code.ivysaur.me/yvbolt/releases/download/v0.8.0/yvbolt.linux64.tar.xz)
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
- Debconf: Add as supported database
- SQLite: Support table names containing special characters
- SQLite: Improvements for experimental command-line driver
- Redis: Improve connection dialog window position
- App: Cosmetic fixes for frame borders, help dialog, and Windows fonts+colours
- Build: Change compression parameters 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
- Pebble: Add as supported database
- Bolt: Support opening as readonly
- Bolt: Support creating new databases
- Bolt: Support adding/removing recursive child buckets
- SQLite: Support custom CLI driver that parses `/usr/bin/sqlite3 -json` output (experimental)
- Redis: Improve query parser to support quoted strings
- App: Support refreshing elements in nav tree
- App: Help menu option to show driver versions
- App: Add image icons for refresh and close context menu actions
- Build: Add makefile for cross-compiling release binaries
[⬇️ Download for Windows x86_64](https://git.ivysaur.me/code.ivysaur.me/yvbolt/releases/download/v0.5.0/yvbolt.win64.zip)
[⬇️ Download for Linux x86_64](https://git.ivysaur.me/code.ivysaur.me/yvbolt/releases/download/v0.5.0/yvbolt.linux64.tar.xz)
2024-06-23 v0.4.0
- Redis: Add as supported database
- Badger: Allow creating in-memory databases
- App: Allow selecting partial query text to execute
- App: Allow closing database connections from context menu
- App: Allow scrolling large content on Properties pane
- App: Preload recursive navigation
- App: Automatically switch to selected database when new connection is created
- App: Add help website link
- App: Add database logo images
2024-06-25 v0.3.0
- Badger: Add BadgerDB v4 as supported database
- SQLite: Add support for CGo-free SQLite driver under cross-compilation
- Bolt: Update Bolt to v1.4.0-alpha.1
- App: Add support for running custom queries
- App: Add status bar showing currently selected DB
- App: Fix missing icons in nav when selecting items
- App: Fix extra quotemarks when browsing string content of database
2024-06-08 v0.2.0
- SQLite: Add SQLite support (now requires CGo)
- App: Add images for menu and navigation items
2024-06-03 v0.1.0
- Initial public release

37
TODO
View File

@@ -1,19 +1,18 @@
- BUG: Connecting to multiple DBs from connection manager: if one has an error, the error popup from the others is lost
- BUG: ExecQuery being called multiple times on error?
- Drag and drop database into UI (QBolt parity) - Drag and drop database into UI (QBolt parity)
- Merge with QBolt
- Portable mode (portable.txt or portable/ dir) - Portable mode (portable.txt or portable/ dir)
- Syntax highlighting in editor - Syntax highlighting in editor
- Autorefresh - Autorefresh
- Sshagent - Sshagent
- want to trigger an async refresh from inside the LDB after lock/unlock - want to trigger an async refresh from inside the LDB after lock/unlock
- support adding/removing keys (will need per-row actions) - support adding/removing keys (will need per-row actions)
- SSH: knownhost parser is stricter than openssh, does not support hostname if there is known a knownhost for the IP address
- Bolt: import/export should support passworded zips - Bolt: import/export should support passworded zips
- Table: BSON view can't see data - Table: BSON view can't see data
- Table: quick filter - Table: quick filter
- QSortFilterProxyModel - QSortFilterProxyModel
- Cancellation - Cancellation
- Loading animations for connection + queries - Loading animations for connection + queries
- Delay rendering properties/data tab until tab is focused
- Mutation - Mutation
- Debconf: Support insert/update/delete - Debconf: Support insert/update/delete
- Redis: Support insert/update/delete - Redis: Support insert/update/delete
@@ -26,6 +25,8 @@
- CLI using psql - CLI using psql
- Lungo: Mini embeddable Mongo - https://github.com/256dpi/lungo - Lungo: Mini embeddable Mongo - https://github.com/256dpi/lungo
- MSSQL (recursive navigation for instances) - MSSQL (recursive navigation for instances)
- NutsDB https://github.com/nutsdb/nutsdb
- Sniper https://github.com/recoilme/sniper
- Other K/V stores from https://github.com/smallnest/kvbench - Other K/V stores from https://github.com/smallnest/kvbench
- Windows registry - Windows registry
- Allow entering path for quick navigation - Allow entering path for quick navigation
@@ -36,7 +37,6 @@
- Chai (built on Pebble) - https://github.com/chaisql/chai - Chai (built on Pebble) - https://github.com/chaisql/chai
- CloverDB (built on Bolt/Badger) - https://github.com/ostafen/clover - CloverDB (built on Bolt/Badger) - https://github.com/ostafen/clover
- APCu - need some sort of hook into the storage engine - APCu - need some sort of hook into the storage engine
- VoidDB - https://github.com/voidDB/voidDB
- UnisonDB - https://github.com/ankur-anand/unisondb - UnisonDB - https://github.com/ankur-anand/unisondb
- Etcd - Etcd
- v2: hierarchal - v2: hierarchal
@@ -57,7 +57,6 @@
- Not-quite-DBs - Not-quite-DBs
- IRC client - IRC client
- Docker daemon (images, containers, ...) - Docker daemon (images, containers, ...)
- ssh-agent
- ssh known-hosts - ssh known-hosts
- golang.org/x/crypto/ssh/knownhosts - already using this package - golang.org/x/crypto/ssh/knownhosts - already using this package
- Generic ODBC, database/sql, ... - Generic ODBC, database/sql, ...
@@ -67,6 +66,8 @@
- https://github.com/TerraTech/go-tokyocabinet needs pkg-config tokyocabinet - https://github.com/TerraTech/go-tokyocabinet needs pkg-config tokyocabinet
- https://github.com/estraier/tkrzw-go needs pkg-config tkrzw - https://github.com/estraier/tkrzw-go needs pkg-config tkrzw
- cdb (DJB's Constant Database) - cdb (DJB's Constant Database)
- RocksDB
- https://github.com/tecbot/gorocksdb Go bindings, need pkg-config rocksdb
- Rust - Rust
- Stoolap https://github.com/stoolap/stoolap - Stoolap https://github.com/stoolap/stoolap
- Rust, needs C binding layer https://github.com/mozilla/cbindgen - Rust, needs C binding layer https://github.com/mozilla/cbindgen
@@ -75,8 +76,11 @@
- SQLite CLI driver: - SQLite CLI driver:
- Context support - Context support
- Write support - Write support
- Attach to SSH tunnel - Type handling for columns
- Binary data is losing its \uXXXX escaping and appearing as string
- Unix timestamps are appearing with scientific notation
- Configure binary path - Configure binary path
- Error handling: if an error occurs, listing db tables has problems/shows separators
- 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
- Badger: - Badger:
@@ -85,17 +89,29 @@
- SQLite: - SQLite:
- drop table doesn't autorefresh nav since callback is late - drop table doesn't autorefresh nav since callback is late
- more accurate type handling - more accurate type handling
- binary data currently shows as "<<binary>>", edits wrongly - generated columns, hidden columns
- switch to table_xinfo
- https://sqlite.org/gencol.html - probably want to show
- https://sqlite.org/vtab.html#hiddencol hidden columns - probably want to leave hidden (option to show??)
- views - views
- other special objects (triggers? udf functions?) - set cell null
- remove current hardcoded LIMIT 1000 - other special objects
- attach additional db to same connection - triggers? udf functions?
- virtual tables, shadow tables
- toggle showing system tables
- e.g. sqlite_schema a.k.a. sqlite_master not present in default list; sqlite_sequence, is present
- both show in pragma_table_list()
- integrity checks / quick_check
- attach additional db to same connection ("schemas")
- ~~autoincrement: if column is autoincrement and left blank on insert, do not populate in INSERT statement~~ works with implicitly null columns
- LMDB: dupsort mode (duplicate keys / entries-per-key) - LMDB: dupsort mode (duplicate keys / entries-per-key)
- MongoDB - MongoDB
- UI for replica sets, ssl certs, cluster, custom auth database - UI for replica sets, ssl certs, cluster, custom auth database
- SSH tunnel: error `ssh: tcpChan: deadline not supported` - needs workaround - SSH tunnel: error `ssh: tcpChan: deadline not supported` - needs workaround
- Backup/restore - Backup/restore
- drop db/collection doesn't autorefresh nav since server is asynchronous - drop db/collection doesn't autorefresh nav since server is asynchronous
- VoidDB:
- drop multidb doesn't autorefresh nav
- SSH tunnel - SSH tunnel
- option to use external/system SSH - option to use external/system SSH
- SSH over Cockpit - SSH over Cockpit
@@ -110,3 +126,4 @@
- Export all data from grid - Export all data from grid
- Export all data from all buckets within a DB - Export all data from all buckets within a DB
- Reconnect - Reconnect
- Connection manager: clone entry

BIN
assets/compress.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 892 B

BIN
assets/key.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 583 B

BIN
assets/note_delete.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 639 B

BIN
assets/page_key.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 907 B

BIN
assets/vendor_buntdb.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 611 B

BIN
assets/vendor_pogreb.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 980 B

BIN
assets/vendor_voiddb.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 802 B

View File

@@ -22,6 +22,7 @@ type ConnectionConfig struct {
Badger *badgerConnection `ylabel:"BadgerDB v4" yicon:":/assets/vendor_dgraph.png" json:",omitempty"` Badger *badgerConnection `ylabel:"BadgerDB v4" yicon:":/assets/vendor_dgraph.png" json:",omitempty"`
Bitcask *bitcaskDBConnection `yicon:":/assets/vendor_riak.png" json:",omitempty"` Bitcask *bitcaskDBConnection `yicon:":/assets/vendor_riak.png" json:",omitempty"`
Bolt *boltConfig `yicon:":/assets/vendor_github.png" json:",omitempty"` Bolt *boltConfig `yicon:":/assets/vendor_github.png" json:",omitempty"`
BuntDB *buntDBConnection `ylabel:"BuntDB" yicon:":/assets/vendor_buntdb.png" json:",omitempty"`
Debconf *debconfConnection `yicon:":/assets/vendor_debian.png" json:",omitempty"` Debconf *debconfConnection `yicon:":/assets/vendor_debian.png" json:",omitempty"`
SecretService *secretServiceConnection `ylabel:"Freedesktop.org Secret Service" yicon:":/assets/vendor_freedesktop.png" json:",omitempty"` SecretService *secretServiceConnection `ylabel:"Freedesktop.org Secret Service" yicon:":/assets/vendor_freedesktop.png" json:",omitempty"`
LevelDB *leveldbConnection `ylabel:"LevelDB" yicon:":/assets/vendor_leveldb.png" json:",omitempty"` LevelDB *leveldbConnection `ylabel:"LevelDB" yicon:":/assets/vendor_leveldb.png" json:",omitempty"`
@@ -29,11 +30,13 @@ type ConnectionConfig struct {
LotusDB *lotusDBConnection `ylabel:"LotusDB" yicon:":/assets/vendor_lotus.png" json:",omitempty"` LotusDB *lotusDBConnection `ylabel:"LotusDB" yicon:":/assets/vendor_lotus.png" json:",omitempty"`
MongoDB *mongoConnection `ylabel:"MongoDB" yicon:":/assets/vendor_mongodb.png" json:",omitempty"` MongoDB *mongoConnection `ylabel:"MongoDB" yicon:":/assets/vendor_mongodb.png" json:",omitempty"`
Pebble *pebbleConnection `yicon:":/assets/vendor_cockroach.png" json:",omitempty"` Pebble *pebbleConnection `yicon:":/assets/vendor_cockroach.png" json:",omitempty"`
Pogreb *pogrebConn `yicon:":/assets/vendor_pogreb.png" json:",omitempty"`
Redis *redisConnectionOptions `yicon:":/assets/vendor_redis.png" json:",omitempty"` Redis *redisConnectionOptions `yicon:":/assets/vendor_redis.png" json:",omitempty"`
RoseDB *roseDBConn `ylabel:"RoseDB" yicon:":/assets/vendor_rosedb.png" json:",omitempty"` RoseDB *roseDBConn `ylabel:"RoseDB" yicon:":/assets/vendor_rosedb.png" json:",omitempty"`
SQLite *sqliteConnection `ylabel:"SQLite" yicon:":/assets/vendor_sqlite.png" json:",omitempty"` SQLite *sqliteConnection `ylabel:"SQLite" yicon:":/assets/vendor_sqlite.png" json:",omitempty"`
SSHAgent *sshAgentConn `yicon:":/assets/vendor_ssh.png" json:",omitempty"` SSHAgent *sshAgentConn `yicon:":/assets/vendor_ssh.png" json:",omitempty"`
Starskey *starskeyConnection `yicon:":/assets/vendor_starskey.png" json:",omitempty"` Starskey *starskeyConnection `yicon:":/assets/vendor_starskey.png" json:",omitempty"`
VoidDB *voidDBConn `ylabel:"VoidDB" yicon:":/assets/vendor_voiddb.png" json:",omitempty"`
} }
func NewConnectionConfig() *ConnectionConfig { func NewConnectionConfig() *ConnectionConfig {
@@ -278,7 +281,8 @@ func (f *App) OnMnuConnectionManagerClick() {
itms := dlg.treeWidget.SelectedItems() itms := dlg.treeWidget.SelectedItems()
var ok bool = true var ok bool = true
for _, itm := range itms { for _, itm := range itms {
ok = ok && connectToItem(itm) // connect to selected itmConnectOK := connectToItem(itm) // connect to selected
ok = ok && itmConnectOK
} }
if ok { if ok {

View File

@@ -159,7 +159,7 @@ func (f *App) saveConnectionManagerContents(sc *SavedConfig) error {
} }
// Force update the saved version // Force update the saved version
sc.UserAgent = APPNAME + `/` + appVersion // e.g. yvbolt/v0.0.0-devel sc.UserAgent = APPNAME + `/` + appVersion // e.g. QBolt/v0.0.0-devel
// Marshal // Marshal

View File

@@ -31,7 +31,7 @@ func (ld *badgerLoadedDatabase) RenderForNav(f *tableState, bucketPath []string)
// Load data // Load data
// Badger always uses Key + Value as the columns // Badger always uses Key + Value as the columns
f.SetupColumns([]columnType{columnType_popupData, columnType_popupData}, []string{"Key", "Value"}) f.SetupColumns([]TableColumn{&binColumn{}, &binColumn{}}, []string{"Key", "Value"})
err := ld.db.View(func(txn *badger.Txn) error { err := ld.db.View(func(txn *badger.Txn) error {
@@ -143,8 +143,8 @@ type badgerConnection struct {
Directory autoconfig.ExistingDirectory Directory autoconfig.ExistingDirectory
Readonly bool Readonly bool
Encryption *encryptionKey Encryption *encryptionKey
} } `json:",omitempty"`
Memory *struct{} Memory *struct{} `json:",omitempty"`
} }
type encryptionKey struct { type encryptionKey struct {

View File

@@ -30,7 +30,7 @@ func (ld *bitcaskLdb) Properties(bucketPath []string) (string, error) {
func (ld *bitcaskLdb) RenderForNav(f *tableState, bucketPath []string) error { func (ld *bitcaskLdb) RenderForNav(f *tableState, bucketPath []string) error {
// Load data // Load data
f.SetupColumns([]columnType{columnType_popupData, columnType_popupData}, []string{"Key", "Value"}) f.SetupColumns([]TableColumn{&binColumn{}, &binColumn{}}, []string{"Key", "Value"})
cur := ld.db.Iterator() cur := ld.db.Iterator()
defer cur.Close() defer cur.Close()

View File

@@ -75,7 +75,7 @@ func (ld *boltLoadedDatabase) RenderForNav(f *tableState, bucketPath []string) e
// Load data // Load data
// Bolt always uses Key + Value as the columns // Bolt always uses Key + Value as the columns
f.SetupColumns([]columnType{columnType_popupData, columnType_popupData}, []string{"Key", "Value"}) f.SetupColumns([]TableColumn{&binColumn{}, &binColumn{}}, []string{"Key", "Value"})
err := ld.db.View(func(tx *bbolt.Tx) error { err := ld.db.View(func(tx *bbolt.Tx) error {
b := boltTargetBucket(tx, bucketPath) b := boltTargetBucket(tx, bucketPath)

118
db_bunt.go Normal file
View File

@@ -0,0 +1,118 @@
package main
import (
"context"
"fmt"
"path/filepath"
"strings"
"github.com/mappu/autoconfig"
qt "github.com/mappu/miqt/qt6"
"github.com/tidwall/buntdb"
)
type buntLdb struct {
db *buntdb.DB
}
func (ld *buntLdb) DriverName() string {
return "BuntDB"
}
func (ld *buntLdb) Properties(bucketPath []string) (string, error) {
idxInfo, err := ld.db.Indexes()
if err != nil {
return "", err
}
return fmt.Sprintf("Indexes (%d):\n%s", len(idxInfo), "-"+strings.Join(idxInfo, "\n- ")+"\n"), nil
}
func (ld *buntLdb) RenderForNav(f *tableState, bucketPath []string) error {
// Load data
// Interestingly, BuntDB internally uses string data, not []byte data
// That would stop us from using kvstore_ApplyChanges that only works with
// []byte data, so, fake []byte casts ourselves
f.SetupColumns([]TableColumn{&binColumn{}, &binColumn{}}, []string{"Key", "Value"})
err := ld.db.View(func(tx *buntdb.Tx) error {
return tx.Ascend("", func(k, v string) bool {
f.AddRow_PK_Data([]byte(k), []byte(k), []byte(v))
return true
})
})
if err != nil {
return err
}
// Valid
f.Ready()
return nil
}
func (n *buntLdb) ApplyChanges(f *tableState, bucketPath []string) error {
return n.db.Update(func(tx *buntdb.Tx) error {
return kvstore_ApplyChanges(
f,
func(k, v []byte) error {
_, _, err := tx.Set(string(k), string(v), nil)
return err
},
func(k []byte) error {
_, err := tx.Delete(string(k))
return err
},
)
})
}
func (ld *buntLdb) doShrink(sender *qt.QTreeWidgetItem, bucketPath []string) error {
return ld.db.Shrink()
}
func (ld *buntLdb) NavChildren(bucketPath []string) ([]string, error) {
return []string{}, nil // No children
}
func (ld *buntLdb) NavContext(bucketPath []string) ([]contextAction, error) {
return []contextAction{
{"Shrink", ld.doShrink},
}, nil
}
func (ld *buntLdb) Close() {
_ = ld.db.Close()
}
var _ loadedDatabase = &buntLdb{} // interface assertion
var _ editableLoadedDatabase = &buntLdb{} // interface assertion
//
type buntDBConnection struct {
Type autoconfig.OneOf
Disk *struct {
File autoconfig.ExistingFile
} `json:",omitempty"`
Memory *struct{} `json:",omitempty"`
}
func (ldc *buntDBConnection) Connect(ctx context.Context) (loadedDatabase, string, error) {
var path string
if ldc.Disk != nil {
path = string(ldc.Disk.File)
} else {
path = `:memory:` // Special string known by driver
}
db, err := buntdb.Open(path)
if err != nil {
return nil, "", err
}
return &buntLdb{db: db}, filepath.Base(path), nil
}

View File

@@ -7,7 +7,7 @@ import (
"path/filepath" "path/filepath"
"runtime" "runtime"
"yvbolt/debconf" "qbolt/debconf"
"github.com/mappu/autoconfig" "github.com/mappu/autoconfig"
) )
@@ -44,7 +44,11 @@ func (ld *debconfLoadedDatabase) RenderForNav(f *tableState, bucketPath []string
indexes := make(map[string]int) indexes := make(map[string]int)
f.SetupColumns(slice_repeat(columnType_inlineText, len(ld.db.AllColumnNames)), ld.db.AllColumnNames) cols := make([]TableColumn, 0, len(ld.db.AllColumnNames))
for i := 0; i < len(ld.db.AllColumnNames); i++ {
cols = append(cols, &stringColumn{})
}
f.SetupColumns(cols, ld.db.AllColumnNames)
for i, cname := range ld.db.AllColumnNames { for i, cname := range ld.db.AllColumnNames {
indexes[cname] = i indexes[cname] = i

View File

@@ -24,7 +24,7 @@ func (ld *evLdb) Properties(bucketPath []string) (string, error) {
func (ld *evLdb) RenderForNav(f *tableState, bucketPath []string) error { func (ld *evLdb) RenderForNav(f *tableState, bucketPath []string) error {
f.SetupColumns([]columnType{columnType_inlineText, columnType_inlineText, columnType_inlineText}, []string{"Library", "Version", "Hash"}) f.SetupColumns([]TableColumn{&goModuleColumn{}, &stringColumn{}, &stringColumn{}}, []string{"Library", "Version", "Hash"})
for _, dep := range ld.mods.Deps { for _, dep := range ld.mods.Deps {
f.AddRowData(dep.Path, dep.Version, dep.Sum) f.AddRowData(dep.Path, dep.Version, dep.Sum)

View File

@@ -34,7 +34,7 @@ func (ld *leveldbLoadedDatabase) RenderForNav(f *tableState, bucketPath []string
// Load data // Load data
// leveldb always uses Key + Value as the columns // leveldb always uses Key + Value as the columns
f.SetupColumns([]columnType{columnType_popupData, columnType_popupData}, []string{"Key", "Value"}) f.SetupColumns([]TableColumn{&binColumn{}, &binColumn{}}, []string{"Key", "Value"})
itn := ld.db.NewIterator(nil, nil) itn := ld.db.NewIterator(nil, nil)
defer itn.Release() defer itn.Release()

View File

@@ -41,7 +41,7 @@ func (ld *lmdbDatabase) RenderForNav(f *tableState, bucketPath []string) error {
// LMDB always uses Key + Value as the columns // LMDB always uses Key + Value as the columns
f.SetupColumns([]columnType{columnType_popupData, columnType_popupData}, []string{"Key", "Value"}) f.SetupColumns([]TableColumn{&binColumn{}, &binColumn{}}, []string{"Key", "Value"})
err := ld.db.View(func(txn *lmdb.Txn) error { err := ld.db.View(func(txn *lmdb.Txn) error {
var dbi lmdb.DBI var dbi lmdb.DBI
@@ -238,10 +238,10 @@ var _ editableLoadedDatabase = &lmdbDatabase{} // interface assertion
type lmdbConnection struct { type lmdbConnection struct {
Storage struct { Storage struct {
Type autoconfig.OneOf Type autoconfig.OneOf
Directory *autoconfig.ExistingDirectory Directory *autoconfig.ExistingDirectory `json:",omitempty"`
File *struct { File *struct {
Path autoconfig.ExistingFile `yfilter:"LMDB database (*.mdb);;All files (*)"` Path autoconfig.ExistingFile `yfilter:"LMDB database (*.mdb);;All files (*)"`
} } `json:",omitempty"`
} }
MultiDB *struct { MultiDB *struct {
Slots int Slots int

View File

@@ -24,7 +24,7 @@ func (ld *lotusLdb) Properties(bucketPath []string) (string, error) {
func (ld *lotusLdb) RenderForNav(f *tableState, bucketPath []string) error { func (ld *lotusLdb) RenderForNav(f *tableState, bucketPath []string) error {
// Load data // Load data
f.SetupColumns([]columnType{columnType_popupData, columnType_popupData}, []string{"Key", "Value"}) f.SetupColumns([]TableColumn{&binColumn{}, &binColumn{}}, []string{"Key", "Value"})
cur, err := ld.db.NewIterator(lotusdb.IteratorOptions{}) cur, err := ld.db.NewIterator(lotusdb.IteratorOptions{})
if err != nil { if err != nil {

View File

@@ -82,7 +82,7 @@ func (ld *mongoLdb) RenderForNav(f *tableState, bucketPath []string) error {
} else if len(bucketPath) == 2 { } else if len(bucketPath) == 2 {
f.SetupColumns([]columnType{columnType_inlineText, columnType_bsonDoc}, []string{"_id", "Document"}) f.SetupColumns([]TableColumn{&stringColumn{}, &bsonColumn{}}, []string{"_id", "Document"})
db := ld.client.Database(bucketPath[0]) db := ld.client.Database(bucketPath[0])
coll := db.Collection(bucketPath[1]) coll := db.Collection(bucketPath[1])
@@ -239,7 +239,7 @@ func (ld *mongoLdb) ExecQuery(query string, bucketPath []string, resultArea *tab
return fmt.Errorf("Decoding response: %w", err) return fmt.Errorf("Decoding response: %w", err)
} }
resultArea.SetupColumns([]columnType{columnType_popupData}, []string{"Response"}) resultArea.SetupColumns([]TableColumn{&binColumn{}}, []string{"Response"})
resultArea.AddRowData(responseJson) resultArea.AddRowData(responseJson)
resultArea.Ready() resultArea.Ready()
return nil return nil
@@ -287,7 +287,7 @@ type mongoConnection struct {
Conn struct { Conn struct {
Mode autoconfig.OneOf Mode autoconfig.OneOf
Connection_String *string Connection_String *string `json:",omitempty"`
} }
SSH_Tunnel *SSHTunnel SSH_Tunnel *SSHTunnel

View File

@@ -30,7 +30,7 @@ func (ld *pebbleLoadedDatabase) RenderForNav(f *tableState, bucketPath []string)
// Load data // Load data
// pebble always uses Key + Value as the columns // pebble always uses Key + Value as the columns
f.SetupColumns([]columnType{columnType_popupData, columnType_popupData}, []string{"Key", "Value"}) f.SetupColumns([]TableColumn{&binColumn{}, &binColumn{}}, []string{"Key", "Value"})
itr, err := ld.db.NewIterWithContext(ctx, nil) itr, err := ld.db.NewIterWithContext(ctx, nil)
if err != nil { if err != nil {
@@ -91,8 +91,8 @@ type pebbleConnection struct {
Disk *struct { Disk *struct {
Directory autoconfig.ExistingDirectory Directory autoconfig.ExistingDirectory
Readonly bool Readonly bool
} } `json:",omitempty"`
Memory *struct{} Memory *struct{} `json:",omitempty"`
} }
func (pdc *pebbleConnection) Connect(ctx context.Context) (loadedDatabase, string, error) { func (pdc *pebbleConnection) Connect(ctx context.Context) (loadedDatabase, string, error) {

95
db_pogreb.go Normal file
View File

@@ -0,0 +1,95 @@
package main
import (
"context"
"errors"
"fmt"
"path/filepath"
"github.com/akrylysov/pogreb"
"github.com/mappu/autoconfig"
qt "github.com/mappu/miqt/qt6"
)
type pogrebLdb struct {
db *pogreb.DB
}
func (ld *pogrebLdb) DriverName() string {
return "Pogreb"
}
func (ld *pogrebLdb) Properties(bucketPath []string) (string, error) {
m := ld.db.Metrics()
return fmt.Sprintf("Metrics: %#v\n", m), nil
}
func (ld *pogrebLdb) RenderForNav(f *tableState, bucketPath []string) error {
// Load data
f.SetupColumns([]TableColumn{&binColumn{}, &binColumn{}}, []string{"Key", "Value"})
itn := ld.db.Items()
for {
k, v, err := itn.Next()
if err != nil {
if errors.Is(err, pogreb.ErrIterationDone) {
break
}
return err // Real error
}
f.AddRow_PK_Data(k, k, v)
}
f.Ready()
return nil
}
func (n *pogrebLdb) ApplyChanges(f *tableState, bucketPath []string) error {
return kvstore_ApplyChanges(f, n.db.Put, n.db.Delete)
}
func (ld *pogrebLdb) NavChildren(bucketPath []string) ([]string, error) {
return []string{}, nil // No children
}
func (ld *pogrebLdb) compactDB(sender *qt.QTreeWidgetItem, bucketPath []string) error {
info, err := ld.db.Compact()
if err != nil {
return err
}
qt.QMessageBox_Information(sender.TreeWidget().QWidget, APPNAME,
fmt.Sprintf("Compaction completed successfully.\n\nStatistics: %#v", info))
return nil
}
func (ld *pogrebLdb) NavContext(bucketPath []string) ([]contextAction, error) {
return []contextAction{
{"Compact", ld.compactDB},
}, nil
}
func (ld *pogrebLdb) Close() {
_ = ld.db.Close()
}
var _ loadedDatabase = &pogrebLdb{} // interface assertion
var _ editableLoadedDatabase = &pogrebLdb{} // interface assertion
//
type pogrebConn struct {
Directory autoconfig.ExistingDirectory
}
func (c *pogrebConn) Connect(ctx context.Context) (loadedDatabase, string, error) {
db, err := pogreb.Open(string(c.Directory), nil)
if err != nil {
return nil, "", err
}
return &pogrebLdb{db: db}, filepath.Base(string(c.Directory)), nil
}

View File

@@ -5,7 +5,7 @@ import (
"fmt" "fmt"
"strconv" "strconv"
"yvbolt/lexer" "qbolt/lexer"
"github.com/mappu/autoconfig" "github.com/mappu/autoconfig"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
@@ -142,7 +142,7 @@ func (ld *redisLoadedDatabase) RenderForNav(f *tableState, bucketPath []string)
// Redis always uses Key string, Type string, Value []byte as the columns // Redis always uses Key string, Type string, Value []byte as the columns
f.SetupColumns( f.SetupColumns(
[]columnType{columnType_popupData, columnType_inlineText, columnType_popupData}, []TableColumn{&binColumn{}, &stringColumn{}, &binColumn{}},
[]string{"Key", "Type", "Value"}, []string{"Key", "Type", "Value"},
) )
@@ -240,7 +240,7 @@ func (ld *redisLoadedDatabase) ExecQuery(query string, _ []string, resultArea *t
return fmt.Errorf("The redis query returned an error: %w", err) return fmt.Errorf("The redis query returned an error: %w", err)
} }
resultArea.SetupColumns([]columnType{columnType_inlineText}, []string{"Result"}) resultArea.SetupColumns([]TableColumn{&stringColumn{}}, []string{"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) {

View File

@@ -25,7 +25,7 @@ func (ld *roseLdb) Properties(bucketPath []string) (string, error) {
func (ld *roseLdb) RenderForNav(f *tableState, bucketPath []string) error { func (ld *roseLdb) RenderForNav(f *tableState, bucketPath []string) error {
// Load data // Load data
f.SetupColumns([]columnType{columnType_popupData, columnType_popupData}, []string{"Key", "Value"}) f.SetupColumns([]TableColumn{&binColumn{}, &binColumn{}}, []string{"Key", "Value"})
ld.db.Ascend(func(k, v []byte) (bool, error) { ld.db.Ascend(func(k, v []byte) (bool, error) {
f.AddRow_PK_Data(k, k, v) f.AddRow_PK_Data(k, k, v)

View File

@@ -37,7 +37,7 @@ func (ld *secretServiceDb) RenderForNav(f *tableState, bucketPath []string) erro
} else if len(bucketPath) == 1 { } else if len(bucketPath) == 1 {
f.SetupColumns( f.SetupColumns(
[]columnType{columnType_inlineText, columnType_inlineText, columnType_inlineText, columnType_inlineText, columnType_inlineText, columnType_popupData}, []TableColumn{&stringColumn{}, &stringColumn{}, &stringColumn{}, &stringColumn{}, &stringColumn{}, &binColumn{}},
[]string{"ID", "Label", "Attributes", "ContentType", "Parameters", "Value"}, []string{"ID", "Label", "Attributes", "ContentType", "Parameters", "Value"},
) )

View File

@@ -6,9 +6,10 @@ import (
"errors" "errors"
"fmt" "fmt"
"path/filepath" "path/filepath"
"reflect"
"strings" "strings"
"yvbolt/sqliteclidriver" "qbolt/sqliteclidriver"
"github.com/mappu/autoconfig" "github.com/mappu/autoconfig"
qt "github.com/mappu/miqt/qt6" qt "github.com/mappu/miqt/qt6"
@@ -55,8 +56,6 @@ func (ld *sqliteLoadedDatabase) Properties(bucketPath []string) (string, error)
func (ld *sqliteLoadedDatabase) RenderForNav(f *tableState, bucketPath []string) error { func (ld *sqliteLoadedDatabase) RenderForNav(f *tableState, bucketPath []string) error {
ctx := context.TODO()
if len(bucketPath) == 0 { if len(bucketPath) == 0 {
return nil return nil
@@ -68,36 +67,17 @@ func (ld *sqliteLoadedDatabase) RenderForNav(f *tableState, bucketPath []string)
// Render for specific table // Render for specific table
tableName := bucketPath[1] tableName := bucketPath[1]
// Load column details
// 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
// work even when there are 0 results
columnNames, err := ld.sqliteGetColumnNamesForTable(tableName)
if err != nil {
return fmt.Errorf("Failed to load columns for table %q: %w", tableName, err)
}
populateColumns(columnNames, f)
// Find primary key, if any
var primaryKeyIdx int = -1
if primaryKeyColumnName, err := ld.getPrimaryKeyForTable(ctx, ld.db, tableName); err == nil {
if search, ok := slice_find(columnNames, primaryKeyColumnName); ok {
primaryKeyIdx = search
}
}
// Select count(*) so we know to display a warning if there are too many entries // Select count(*) so we know to display a warning if there are too many entries
// TODO // TODO
// 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 rowid, * FROM [` + tableName + `]`) // 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) return fmt.Errorf("Failed to load data for table %q: %w", tableName, err)
} }
defer datar.Close() defer datar.Close()
err = populateRows(datar, f, primaryKeyIdx) err = populateRows(datar, f, true)
if err != nil { if err != nil {
return err return err
} }
@@ -140,34 +120,108 @@ func (ld *sqliteLoadedDatabase) sqliteGetColumnNamesForTable(tableName string) (
return ret, nil return ret, nil
} }
func populateColumns(names []string, dest *tableState) { func populateRows(rr *sql.Rows, dest *tableState, firstColumnIsExtraRowID bool) error {
// FIXME better column types?
dest.SetupColumns(slice_repeat(columnType_inlineText, len(names)), names)
}
func populateRows(rr *sql.Rows, dest *tableState, pk_index int) error {
numColumns := len(dest.columns) numColumns := len(dest.columns)
for rr.Next() { var pfields []interface{} = nil
fields := make([]interface{}, numColumns)
pfields := make([]interface{}, numColumns) cNames, err := rr.Columns()
for i := 0; i < numColumns; i += 1 { if err != nil {
pfields[i] = &fields[i] return err
} }
cTypes, err := rr.ColumnTypes()
if err != nil {
return err
}
if len(cNames) != len(cTypes) {
return errors.New("unexpected column metadata mismatch") // assert
}
numColumns = len(cNames)
rrMakeCtypes := []TableColumn{}
for i := 0; i < len(cTypes); i++ {
if firstColumnIsExtraRowID && i == 0 {
pfields = append(pfields, new(int64))
continue
}
nullable, ok := cTypes[i].Nullable()
if !ok {
return errors.New("can't tell if column is nullable?")
}
// TODO support all SQLite column names/type-affinities
// TODO support nullable variants for all types here
// @ref https://www.sqlite.org/datatype3.html
switch cTypes[i].DatabaseTypeName() {
case "BLOB":
// Binary column
rrMakeCtypes = append(rrMakeCtypes, &binColumn{})
pfields = append(pfields, new([]byte))
case "INTEGER":
if nullable {
rrMakeCtypes = append(rrMakeCtypes, &sqlNullInt64Column{})
pfields = append(pfields, new(sql.NullInt64))
} else {
rrMakeCtypes = append(rrMakeCtypes, &int64Column{})
pfields = append(pfields, new(int64))
}
default:
if nullable {
rrMakeCtypes = append(rrMakeCtypes, &sqlNullStringColumn{})
pfields = append(pfields, new(sql.NullString))
} else {
rrMakeCtypes = append(rrMakeCtypes, &stringColumn{})
pfields = append(pfields, new(string))
}
}
}
// Resetup table
if firstColumnIsExtraRowID {
cNames = cNames[1:]
}
dest.SetupColumns(rrMakeCtypes, cNames)
for rr.Next() {
err := rr.Scan(pfields...) err := rr.Scan(pfields...)
if err != nil { if err != nil {
return fmt.Errorf("Scan: %w", err) return fmt.Errorf("Scan: %w", err)
} }
rpos := dest.AddRow() rpos := dest.AddRow()
for i := 0; i < len(fields); i += 1 {
dest.SetCell(rpos, i, formatAny(fields[i])) // FIXME stop doing string conversion here if firstColumnIsExtraRowID {
dest.SetRowPrimaryKey(rpos, int64_to_binary8(*(pfields[0].(*int64))))
for i := 1; i < numColumns; i += 1 { // skip first column
interior := reflect.ValueOf(pfields[i]).Elem().Interface()
dest.SetCell(rpos, i-1, interior)
}
} else {
// all columns
for i := 0; i < numColumns; i += 1 {
interior := reflect.ValueOf(pfields[i]).Elem().Interface()
dest.SetCell(rpos, i, interior)
} }
if pk_index >= 0 {
dest.SetRowPrimaryKey(rpos, []byte(formatAny(fields[pk_index]))) // FIXME stop doing string conversion here
} }
} }
@@ -182,14 +236,7 @@ func (ld *sqliteLoadedDatabase) ExecQuery(query string, _ []string, resultArea *
defer rr.Close() defer rr.Close()
columns, err := rr.Columns() err = populateRows(rr, resultArea, false)
if err != nil {
return err
}
populateColumns(columns, resultArea)
err = populateRows(rr, resultArea, -1)
if err != nil { if err != nil {
return err return err
} }
@@ -202,12 +249,6 @@ type RowQueryContexter interface {
QueryRowContext(context.Context, string, ...interface{}) *sql.Row QueryRowContext(context.Context, string, ...interface{}) *sql.Row
} }
func (n *sqliteLoadedDatabase) getPrimaryKeyForTable(ctx context.Context, tx RowQueryContexter, tableName string) (string, error) {
var primaryColumnName string
err := tx.QueryRowContext(ctx, `SELECT l.name FROM pragma_table_info(?) as l WHERE l.pk = 1;`, tableName).Scan(&primaryColumnName)
return primaryColumnName, err
}
func (n *sqliteLoadedDatabase) ApplyChanges(f *tableState, bucketPath []string) (retErr error) { func (n *sqliteLoadedDatabase) ApplyChanges(f *tableState, bucketPath []string) (retErr error) {
if len(bucketPath) != 2 { if len(bucketPath) != 2 {
@@ -232,33 +273,27 @@ func (n *sqliteLoadedDatabase) ApplyChanges(f *tableState, bucketPath []string)
} }
}() }()
// Query sqlite table metadata to determine which of these is the PRIMARY KEY
primaryColumnName, err := n.getPrimaryKeyForTable(ctx, tx, tableName)
if err != nil {
return fmt.Errorf("Finding primary key for update: %w", err)
}
// SQLite can only LIMIT 1 on update/delete if it was compiled with // 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 // SQLITE_ENABLE_UPDATE_DELETE_LIMIT, which isn't the case for the mattn
// cgo library // cgo library
// Skip that, and just rely on primary key uniqueness // Skip that, and just rely on primary key uniqueness
// Edit // Edit
for rowid, editcells := range f.updateRows { for aRow, editcells := range f.updateRows {
stmt := `UPDATE [` + tableName + `] SET ` stmt := `UPDATE [` + tableName + `] SET `
params := []interface{}{} // FIXME reinstate types for the driver (although SQLite doesn't mind) params := []interface{}{}
for ct, cell := range editcells { for ct, cell := range editcells {
if ct > 0 { if ct > 0 {
stmt += `, ` stmt += `, `
} }
stmt += `[` + f.columnLabels[cell] + `] = ?` stmt += `[` + f.columnLabels[cell] + `] = ?`
params = append(params, (f.columns[cell].(*stringColumn)).vals[rowid]) // FIXME stop doing string conversion params = append(params, f.columns[cell].GetCell(aRow))
} }
stmt += ` WHERE [` + primaryColumnName + `] = ?` stmt += ` WHERE [rowid] = ?`
// Update by primary key (stored separately) // Update by primary key (stored separately)
pkVal := string(f.primaryKeys[rowid]) // FIXME avoid string marshalling pkVal := binary8_to_int64(f.primaryKeys[aRow])
params = append(params, pkVal) params = append(params, pkVal)
_, err = tx.ExecContext(ctx, stmt, params...) _, err = tx.ExecContext(ctx, stmt, params...)
@@ -268,10 +303,10 @@ func (n *sqliteLoadedDatabase) ApplyChanges(f *tableState, bucketPath []string)
} }
// Delete by key (affects rowids after re-render) // Delete by key (affects rowids after re-render)
for rowid, _ := range f.deleteRows { for aRow, _ := range f.deleteRows {
pkVal := string(f.primaryKeys[rowid]) // FIXME avoid string marshalling pkVal := binary8_to_int64(f.primaryKeys[aRow])
stmt := `DELETE FROM [` + tableName + `] WHERE [` + primaryColumnName + `] = ?` stmt := `DELETE FROM [` + tableName + `] WHERE [rowid] = ?`
_, err = tx.ExecContext(ctx, stmt, pkVal) _, err = tx.ExecContext(ctx, stmt, pkVal)
if err != nil { if err != nil {
@@ -280,16 +315,16 @@ func (n *sqliteLoadedDatabase) ApplyChanges(f *tableState, bucketPath []string)
} }
// Insert all new entries // Insert all new entries
for rowid, _ := range f.insertRows { for aRow, _ := range f.insertRows {
stmt := `INSERT INTO [` + tableName + `] ([` + strings.Join(f.columnLabels, `], [`) + `]) VALUES (` stmt := `INSERT INTO [` + tableName + `] ([` + strings.Join(f.columnLabels, `], [`) + `]) VALUES (`
params := []interface{}{} // FIXME reinstate types for the driver (although SQLite doesn't mind) params := []interface{}{}
for colid := 0; colid < len(f.columnLabels); colid++ { for colid := 0; colid < len(f.columnLabels); colid++ {
if colid > 0 { if colid > 0 {
stmt += `, ` stmt += `, `
} }
stmt += "?" stmt += "?"
params = append(params, (f.columns[colid].(*stringColumn)).vals[rowid]) // FIXME stop doing string conversion params = append(params, f.columns[colid].GetCell(aRow))
} }
stmt += `)` stmt += `)`
@@ -316,7 +351,10 @@ func (ld *sqliteLoadedDatabase) NavChildren(bucketPath []string) ([]string, erro
} }
if len(bucketPath) == 1 && bucketPath[0] == sqliteTablesCaption { if len(bucketPath) == 1 && bucketPath[0] == sqliteTablesCaption {
rr, err := ld.db.Query(`SELECT name FROM sqlite_master WHERE type='table' ORDER BY name ASC;`) // The sequence and stat1 tables are not marked as hidden tables
// They are created automatically when using (A) autoincrement and (B) ???.
rr, err := ld.db.Query(`SELECT name FROM sqlite_master WHERE type='table' AND name != 'sqlite_sequence' AND name != 'sqlite_stat1' ORDER BY name ASC;`)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -406,8 +444,12 @@ type sqliteConnection struct {
Disk *struct { Disk *struct {
Database autoconfig.ExistingFile `yfilter:"SQLite database (*.db *.db3 *.sqlite *.sqlite3);;All files (*)"` Database autoconfig.ExistingFile `yfilter:"SQLite database (*.db *.db3 *.sqlite *.sqlite3);;All files (*)"`
CliDriver bool `ylabel:"Use experimental CLI driver"` CliDriver bool `ylabel:"Use experimental CLI driver"`
} } `json:",omitempty"`
Memory *struct{} Memory *struct{} `json:",omitempty"`
SSH *struct {
SSHServer *SSHTunnel
Database string
} `json:",omitempty"`
} }
func (sc *sqliteConnection) Connect(ctx context.Context) (loadedDatabase, string, error) { func (sc *sqliteConnection) Connect(ctx context.Context) (loadedDatabase, string, error) {
@@ -424,12 +466,29 @@ func (sc *sqliteConnection) Connect(ctx context.Context) (loadedDatabase, string
return &sqliteLoadedDatabase{db: db}, filepath.Base(string(sc.Disk.Database)), nil return &sqliteLoadedDatabase{db: db}, filepath.Base(string(sc.Disk.Database)), nil
} else { // memory } else if sc.Memory != nil { // memory
db, err := sql.Open("sqlite3", ":memory:") db, err := sql.Open("sqlite3", ":memory:")
if err != nil { if err != nil {
return nil, "", err return nil, "", err
} }
return &sqliteLoadedDatabase{db: db}, ":memory:", nil return &sqliteLoadedDatabase{db: db}, ":memory:", nil
} else if sc.SSH != nil {
if sc.SSH.SSHServer == nil {
return nil, "", errors.New("Invalid configuration")
}
cl, err := sc.SSH.SSHServer.Open(ctx)
if err != nil {
return nil, "", err
}
db := sqliteclidriver.OpenSSH(cl, sc.SSH.Database)
return &sqliteLoadedDatabase{db: db}, "SSH[" + filepath.Base(string(sc.SSH.Database)) + "]", nil
} else {
return nil, "", errors.New("Invalid configuration")
} }
} }

View File

@@ -31,7 +31,7 @@ func (ld *sshAgentLdb) RenderForNav(f *tableState, bucketPath []string) error {
return err return err
} }
f.SetupColumns([]columnType{columnType_inlineText, columnType_inlineText, columnType_popupData}, []string{"Comment", "Type", "Public Key"}) f.SetupColumns([]TableColumn{&stringColumn{}, &stringColumn{}, &binColumn{}}, []string{"Comment", "Type", "Public Key"})
for _, key := range keys { for _, key := range keys {
// The publicKey blob is the effective primary-key for DB manipulation // The publicKey blob is the effective primary-key for DB manipulation
@@ -117,8 +117,8 @@ var _ loadedDatabase = &sshAgentLdb{} // interface assertion
type sshAgentConn struct { type sshAgentConn struct {
Type autoconfig.OneOf Type autoconfig.OneOf
Unix *autoconfig.ExistingFile Unix *autoconfig.ExistingFile `json:",omitempty"`
TCP *autoconfig.AddressPort TCP *autoconfig.AddressPort `json:",omitempty"`
} }
func (c *sshAgentConn) Reset() { func (c *sshAgentConn) Reset() {

View File

@@ -26,7 +26,7 @@ func (ld *starskeyLdb) RenderForNav(f *tableState, bucketPath []string) error {
// Load data // Load data
// Starskey always uses Key + Value as the columns // Starskey always uses Key + Value as the columns
f.SetupColumns([]columnType{columnType_popupData, columnType_popupData}, []string{"Key", "Value"}) f.SetupColumns([]TableColumn{&binColumn{}, &binColumn{}}, []string{"Key", "Value"})
// It's possible to create a transaction in Starskey, but you can't enumerate keys // It's possible to create a transaction in Starskey, but you can't enumerate keys
// within the transaction - doesn't really help us // within the transaction - doesn't really help us

248
db_voiddb_linux.go Normal file
View File

@@ -0,0 +1,248 @@
package main
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"github.com/mappu/autoconfig"
qt "github.com/mappu/miqt/qt6"
"github.com/voidDB/voidDB"
"github.com/voidDB/voidDB/common"
"github.com/voidDB/voidDB/cursor"
)
type voidLdb struct {
db *voidDB.Void
multiKeyspace bool
}
func (ld *voidLdb) DriverName() string {
return "VoidDB"
}
func (ld *voidLdb) Properties(bucketPath []string) (string, error) {
return "", nil
}
// cursor gets a VoidDB cursor for the given transaction and bucketPath.
func (ld *voidLdb) cursor(txn *voidDB.Txn, bucketPath []string) (*cursor.Cursor, error) {
if ld.multiKeyspace {
if len(bucketPath) == 0 {
return nil, errors.New("No data at VoidDB root keyspace when in multi-keyspace mode")
}
return txn.OpenCursor([]byte(bucketPath[0]))
} else {
if len(bucketPath) != 0 {
return nil, errors.New("Invalid navigation")
}
return txn.Cursor, nil // The Txn embeds a new cursor over the default keyspace
}
}
func (ld *voidLdb) RenderForNav(f *tableState, bucketPath []string) error {
// Load data
f.SetupColumns([]TableColumn{&binColumn{}, &binColumn{}}, []string{"Key", "Value"})
if ld.multiKeyspace && len(bucketPath) == 0 {
// Nothing to see/do in the default keyspace, sorry
// Exit without calling .Ready()
return nil
}
err := ld.db.View(func(tx *voidDB.Txn) error {
cur, err := ld.cursor(tx, bucketPath)
if err != nil {
return err
}
for {
k, v, err := cur.GetNext()
if err != nil {
if errors.Is(err, common.ErrorNotFound) {
break // Reached end
}
return fmt.Errorf("GetNext: %w", err) // Some other real error
}
f.AddRow_PK_Data(k, k, v)
}
return nil
})
if err != nil {
return err
}
f.Ready()
return nil
}
func (n *voidLdb) ApplyChanges(f *tableState, bucketPath []string) error {
return n.db.Update(true, func(tx *voidDB.Txn) error {
cur, err := n.cursor(tx, bucketPath)
if err != nil {
return err
}
return kvstore_ApplyChanges(
f,
cur.Put,
func(k []byte) error {
// To delete keys in VoidDB, first move the cursor to the target
_, err := cur.Get(k)
if err != nil {
return fmt.Errorf("Finding key %q for deletion: %w", formatUtf8(k), err)
}
return cur.Del()
},
)
})
}
func (ld *voidLdb) NavChildren(bucketPath []string) ([]string, error) {
if len(bucketPath) == 0 && ld.multiKeyspace {
// In multi-db mode, the root keyspace is filled with pointers to
// other keyspaces
// Iterate the names
var rootNames []string
err := ld.db.View(func(tx *voidDB.Txn) error {
for {
k, _, err := tx.GetNext()
if err != nil {
if errors.Is(err, common.ErrorNotFound) {
return nil // Reached end
}
return fmt.Errorf("GetNext: %w", err) // Some other real error
}
rootNames = append(rootNames, string(k))
}
})
if err != nil {
return nil, err
}
return rootNames, nil
}
return []string{}, nil // No children
}
// addKeyspace is valid only in multi-keyspace mode.
func (ld *voidLdb) addKeyspace(sender *qt.QTreeWidgetItem, bucketPath []string) error {
if len(bucketPath) != 0 {
return errors.New("Can only add keyspaces at the root")
}
ksName := qt.QInputDialog_GetText(sender.TreeWidget().QWidget, APPNAME, "Enter a name for the new keyspace:")
if ksName == "" {
return nil // cancel
}
return ld.db.Update(true, func(tx *voidDB.Txn) error {
_, err := tx.OpenCursor([]byte(ksName))
return err
})
}
// deleteKeyspace is valid only in multi-keyspace mode. It deletes the target
// keyspace and all its content.
func (ld *voidLdb) deleteKeyspace(sender *qt.QTreeWidgetItem, bucketPath []string) error {
if len(bucketPath) != 1 {
return errors.New("Invalid navigation")
}
if !vcl_confirm_dialog(sender.TreeWidget().QWidget, fmt.Sprintf("Are you sure you want to delete the keyspace %q?", bucketPath[0])) {
return nil // cancelled
}
return ld.db.Update(true, func(tx *voidDB.Txn) error {
// Open the keyspace
cur, err := tx.OpenCursor([]byte(bucketPath[0]))
if err != nil {
return err
}
// Delete all its content
for {
_, _, err := cur.GetNext()
if err != nil {
if errors.Is(err, common.ErrorNotFound) {
break // Done
}
return fmt.Errorf("GetNext: %w", err) // real error
}
err = cur.Del()
if err != nil {
return err
}
}
// Delete the root-level keyspace pointer
// In VoidDB you delete by moving the cursor to the target item first
_, err = tx.Get([]byte(bucketPath[0]))
if err != nil {
return err
}
return tx.Del()
})
}
func (ld *voidLdb) NavContext(bucketPath []string) ([]contextAction, error) {
if ld.multiKeyspace {
if len(bucketPath) == 0 {
return []contextAction{
{"Add keyspace...", ld.addKeyspace},
}, nil
} else {
return []contextAction{
{"Delete keyspace...", ld.deleteKeyspace},
}, nil
}
}
return nil, nil // No supported actions
}
func (ld *voidLdb) Close() {
_ = ld.db.Close()
}
var _ loadedDatabase = &voidLdb{} // interface assertion
var _ editableLoadedDatabase = &voidLdb{} // interface assertion
//
type voidDBConn struct {
Path autoconfig.ExistingFile
MultiKeyspace bool `ylabel:"Use multiple keyspaces"`
}
func (c *voidDBConn) Connect(ctx context.Context) (loadedDatabase, string, error) {
const capacity = 1 << 40 // 1 TiB max size before returning ErrorFull
openFunc := voidDB.OpenVoid
if _, err := os.Stat(string(c.Path)); os.IsNotExist(err) {
openFunc = voidDB.NewVoid
}
db, err := openFunc(string(c.Path), capacity)
if err != nil {
return nil, "", err
}
return &voidLdb{db: db, multiKeyspace: c.MultiKeyspace}, filepath.Base(string(c.Path)), nil
}

19
db_voiddb_other.go Normal file
View File

@@ -0,0 +1,19 @@
//go:build !linux
// +build !linux
package main
import (
"context"
"errors"
"github.com/mappu/autoconfig"
)
type voidDBConn struct {
H1 autoconfig.Header `ylabel:"Not supported on this operating system"`
}
func (ssc *voidDBConn) Connect(ctx context.Context) (loadedDatabase, string, error) {
return nil, "", errors.New("Not supported on this operating system")
}

BIN
doc/screenshot-000.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -1,21 +1,24 @@
<RCC> <RCC>
<qresource prefix="/"> <qresource prefix="/">
<file>assets/vendor_qt.png</file>
<file>assets/connect.png</file>
<file>assets/disconnect.png</file>
<file>assets/database_key.png</file>
<file>assets/help.png</file>
<file>assets/add.png</file> <file>assets/add.png</file>
<file>assets/arrow_refresh.png</file> <file>assets/arrow_refresh.png</file>
<file>assets/chart_bar.png</file> <file>assets/chart_bar.png</file>
<file>assets/compress.png</file>
<file>assets/connect.png</file>
<file>assets/database.png</file> <file>assets/database.png</file>
<file>assets/database_add.png</file> <file>assets/database_add.png</file>
<file>assets/database_delete.png</file> <file>assets/database_delete.png</file>
<file>assets/database_key.png</file>
<file>assets/database_lightning.png</file> <file>assets/database_lightning.png</file>
<file>assets/database_save.png</file> <file>assets/database_save.png</file>
<file>assets/delete.png</file> <file>assets/delete.png</file>
<file>assets/disconnect.png</file>
<file>assets/help.png</file>
<file>assets/key.png</file>
<file>assets/lightning.png</file> <file>assets/lightning.png</file>
<file>assets/lightning_go.png</file> <file>assets/lightning_go.png</file>
<file>assets/note_delete.png</file>
<file>assets/page_key.png</file>
<file>assets/pencil.png</file> <file>assets/pencil.png</file>
<file>assets/pencil_add.png</file> <file>assets/pencil_add.png</file>
<file>assets/pencil_delete.png</file> <file>assets/pencil_delete.png</file>
@@ -25,6 +28,7 @@
<file>assets/table_add.png</file> <file>assets/table_add.png</file>
<file>assets/table_delete.png</file> <file>assets/table_delete.png</file>
<file>assets/table_save.png</file> <file>assets/table_save.png</file>
<file>assets/vendor_buntdb.png</file>
<file>assets/vendor_cockroach.png</file> <file>assets/vendor_cockroach.png</file>
<file>assets/vendor_debian.png</file> <file>assets/vendor_debian.png</file>
<file>assets/vendor_dgraph.png</file> <file>assets/vendor_dgraph.png</file>
@@ -35,11 +39,14 @@
<file>assets/vendor_lotus.png</file> <file>assets/vendor_lotus.png</file>
<file>assets/vendor_mongodb.png</file> <file>assets/vendor_mongodb.png</file>
<file>assets/vendor_mysql.png</file> <file>assets/vendor_mysql.png</file>
<file>assets/vendor_pogreb.png</file>
<file>assets/vendor_qt.png</file>
<file>assets/vendor_redis.png</file> <file>assets/vendor_redis.png</file>
<file>assets/vendor_riak.png</file> <file>assets/vendor_riak.png</file>
<file>assets/vendor_rosedb.png</file> <file>assets/vendor_rosedb.png</file>
<file>assets/vendor_sqlite.png</file> <file>assets/vendor_sqlite.png</file>
<file>assets/vendor_ssh.png</file> <file>assets/vendor_ssh.png</file>
<file>assets/vendor_starskey.png</file> <file>assets/vendor_starskey.png</file>
<file>assets/vendor_voiddb.png</file>
</qresource> </qresource>
</RCC> </RCC>

BIN
embed.rcc

Binary file not shown.

14
go.mod
View File

@@ -1,4 +1,4 @@
module yvbolt module qbolt
go 1.24.0 go 1.24.0
@@ -11,7 +11,7 @@ require (
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/ledgerwatch/lmdb-go v1.18.2 github.com/ledgerwatch/lmdb-go v1.18.2
github.com/lotusdblabs/lotusdb/v2 v2.1.0 github.com/lotusdblabs/lotusdb/v2 v2.1.0
github.com/mappu/autoconfig v0.4.1 github.com/mappu/autoconfig v0.5.0
github.com/mappu/miqt v0.12.1-0.20251106063543-466b60e47edf github.com/mappu/miqt v0.12.1-0.20251106063543-466b60e47edf
github.com/mattn/go-sqlite3 v1.14.32 github.com/mattn/go-sqlite3 v1.14.32
github.com/redis/go-redis/v9 v9.17.2 github.com/redis/go-redis/v9 v9.17.2
@@ -30,6 +30,7 @@ require (
al.essio.dev/pkg/shellescape v1.6.0 // indirect al.essio.dev/pkg/shellescape v1.6.0 // indirect
github.com/DataDog/zstd v1.5.7 // indirect github.com/DataDog/zstd v1.5.7 // indirect
github.com/abcum/lcp v0.0.0-20201209214815-7a3f3840be81 // indirect github.com/abcum/lcp v0.0.0-20201209214815-7a3f3840be81 // indirect
github.com/akrylysov/pogreb v0.10.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/bwmarrin/snowflake v0.3.0 // indirect github.com/bwmarrin/snowflake v0.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
@@ -71,7 +72,16 @@ require (
github.com/rosedblabs/wal v1.3.6 // indirect github.com/rosedblabs/wal v1.3.6 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect github.com/sirupsen/logrus v1.9.3 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/tidwall/btree v1.7.0 // indirect
github.com/tidwall/buntdb v1.3.2 // indirect
github.com/tidwall/gjson v1.14.3 // indirect
github.com/tidwall/grect v0.1.4 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tidwall/rtred v0.1.2 // indirect
github.com/tidwall/tinyqueue v0.1.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/voidDB/voidDB v0.1.18 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.2.0 // indirect github.com/xdg-go/scram v1.2.0 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect

23
go.sum
View File

@@ -4,6 +4,8 @@ github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE=
github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
github.com/abcum/lcp v0.0.0-20201209214815-7a3f3840be81 h1:uHogIJ9bXH75ZYrXnVShHIyywFiUZ7OOabwd9Sfd8rw= github.com/abcum/lcp v0.0.0-20201209214815-7a3f3840be81 h1:uHogIJ9bXH75ZYrXnVShHIyywFiUZ7OOabwd9Sfd8rw=
github.com/abcum/lcp v0.0.0-20201209214815-7a3f3840be81/go.mod h1:6ZvnjTZX1LNo1oLpfaJK8h+MXqHxcBFBIwkgsv+xlv0= github.com/abcum/lcp v0.0.0-20201209214815-7a3f3840be81/go.mod h1:6ZvnjTZX1LNo1oLpfaJK8h+MXqHxcBFBIwkgsv+xlv0=
github.com/akrylysov/pogreb v0.10.2 h1:e6PxmeyEhWyi2AKOBIJzAEi4HkiC+lKyCocRGlnDi78=
github.com/akrylysov/pogreb v0.10.2/go.mod h1:pNs6QmpQ1UlTJKDezuRWmaqkgUE2TuU0YTWyqJZ7+lI=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
@@ -104,6 +106,8 @@ github.com/lotusdblabs/lotusdb/v2 v2.1.0 h1:rCBrwED8Po12FzrxxX4zppxoHb2O+sCtddyW
github.com/lotusdblabs/lotusdb/v2 v2.1.0/go.mod h1:MyOEvqL3Hxm3HiBOYZ4BlZBnqCIcc2QQkF34VBD76fk= github.com/lotusdblabs/lotusdb/v2 v2.1.0/go.mod h1:MyOEvqL3Hxm3HiBOYZ4BlZBnqCIcc2QQkF34VBD76fk=
github.com/mappu/autoconfig v0.4.1 h1:ekO7mzN+beFu7VhNfJxNlL/5wkYcP9PAl9VTG4EDxYs= github.com/mappu/autoconfig v0.4.1 h1:ekO7mzN+beFu7VhNfJxNlL/5wkYcP9PAl9VTG4EDxYs=
github.com/mappu/autoconfig v0.4.1/go.mod h1:kca6kaYjqOwkZnY9z5YKo0oI/7Bxe2fMIHhYzXwGHEE= github.com/mappu/autoconfig v0.4.1/go.mod h1:kca6kaYjqOwkZnY9z5YKo0oI/7Bxe2fMIHhYzXwGHEE=
github.com/mappu/autoconfig v0.5.0 h1:pSune+YObUOZmo8PXhxg41PsmcJJ8mP0L/3juDicUhE=
github.com/mappu/autoconfig v0.5.0/go.mod h1:kca6kaYjqOwkZnY9z5YKo0oI/7Bxe2fMIHhYzXwGHEE=
github.com/mappu/miqt v0.12.1-0.20251106063543-466b60e47edf h1:SmBzNUevLUzu1msJ5xzWH/Kot+GtOtoz0u9la42dRU4= github.com/mappu/miqt v0.12.1-0.20251106063543-466b60e47edf h1:SmBzNUevLUzu1msJ5xzWH/Kot+GtOtoz0u9la42dRU4=
github.com/mappu/miqt v0.12.1-0.20251106063543-466b60e47edf/go.mod h1:xFg7ADaO1QSkmXPsPODoKe/bydJpRG9fgCYyIDl/h1U= github.com/mappu/miqt v0.12.1-0.20251106063543-466b60e47edf/go.mod h1:xFg7ADaO1QSkmXPsPODoKe/bydJpRG9fgCYyIDl/h1U=
github.com/mattetti/filebuffer v1.0.1 h1:gG7pyfnSIZCxdoKq+cPa8T0hhYtD9NxCdI4D7PTjRLM= github.com/mattetti/filebuffer v1.0.1 h1:gG7pyfnSIZCxdoKq+cPa8T0hhYtD9NxCdI4D7PTjRLM=
@@ -165,8 +169,27 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI=
github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY=
github.com/tidwall/buntdb v1.3.2 h1:qd+IpdEGs0pZci37G4jF51+fSKlkuUTMXuHhXL1AkKg=
github.com/tidwall/buntdb v1.3.2/go.mod h1:lZZrZUWzlyDJKlLQ6DKAy53LnG7m5kHyrEHvvcDmBpU=
github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/grect v0.1.4 h1:dA3oIgNgWdSspFzn1kS4S/RDpZFLrIxAZOdJKjYapOg=
github.com/tidwall/grect v0.1.4/go.mod h1:9FBsaYRaR0Tcy4UwefBX/UDcDcDy9V5jUcxHzv2jd5Q=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/rtred v0.1.2 h1:exmoQtOLvDoO8ud++6LwVsAMTu0KPzLTUrMln8u1yu8=
github.com/tidwall/rtred v0.1.2/go.mod h1:hd69WNXQ5RP9vHd7dqekAz+RIdtfBogmglkZSRxCHFQ=
github.com/tidwall/tinyqueue v0.1.1 h1:SpNEvEggbpyN5DIReaJ2/1ndroY8iyEGxPYxoSaymYE=
github.com/tidwall/tinyqueue v0.1.1/go.mod h1:O/QNHwrnjqr6IHItYrzoHAKYhBkLI67Q096fQP5zMYw=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/voidDB/voidDB v0.1.18 h1:Y2a+2nle/EUZTfM9uuIAAbeRlaxMVejFIE1K4kR8on0=
github.com/voidDB/voidDB v0.1.18/go.mod h1:Yj7wPJtpUHq27xPRsZmyUIlCIYize+VlxK5pHtMRoK0=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs= github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs=

20
main.go
View File

@@ -13,8 +13,8 @@ import (
) )
const ( const (
APPNAME = "yvbolt" APPNAME = "QBolt"
HOMEPAGE_URL = "https://code.ivysaur.me/yvbolt" HOMEPAGE_URL = "https://code.ivysaur.me/qbolt"
) )
type App struct { type App struct {
@@ -37,11 +37,17 @@ func newApp() *App {
a.dbs = make(map[int]loadedDatabase, 0) a.dbs = make(map[int]loadedDatabase, 0)
a.ui = NewMainWindowUi() a.ui = NewMainWindowUi()
a.ui.MainWindow.SetWindowTitle(APPNAME + " " + appVersion)
// Using stylesheet works better than picking a palette colour across all // Using stylesheet works better than picking a palette colour across all
// different QStyles // different QStyles
a.ui.propertiesBox.SetStyleSheet("background-color: transparent;") a.ui.propertiesBox.SetStyleSheet("background-color: transparent;")
// These properties are not supported by miqt-uic yet
a.ui.tbConnect.SetIconSize(qt.NewQSize2(16, 16))
a.ui.tbData.SetIconSize(qt.NewQSize2(16, 16))
a.ui.tbQuery.SetIconSize(qt.NewQSize2(16, 16))
// //
a.ui.actionConnect.OnTriggered(a.OnMnuConnectClick) a.ui.actionConnect.OnTriggered(a.OnMnuConnectClick)
@@ -96,8 +102,6 @@ func newApp() *App {
// a.ui.queryInput.OnTextChanged(a.OnQueryTextChanged) // apply syntax highlighting // a.ui.queryInput.OnTextChanged(a.OnQueryTextChanged) // apply syntax highlighting
a.ui.mnuExecute.OnTriggered(a.OnQueryExecute)
a.ui.queryInput.SetFont(vcl_monospace()) a.ui.queryInput.SetFont(vcl_monospace())
a.resultsTbl = NewTableState(a.ui.queryResult) a.resultsTbl = NewTableState(a.ui.queryResult)
@@ -473,6 +477,7 @@ func (f *App) refreshContent(node *qt.QTreeWidgetItem) {
f.ui.actionApply_changes.SetEnabled(false) // will be enabled after add/delete/edit f.ui.actionApply_changes.SetEnabled(false) // will be enabled after add/delete/edit
f.ui.actionDelete_row.SetEnabled(editable) f.ui.actionDelete_row.SetEnabled(editable)
f.ui.actionAddRow.SetEnabled(editable) f.ui.actionAddRow.SetEnabled(editable)
f.ui.actionSet_cell_to_null.SetEnabled(false) // TODO
f.contentTbl.SetAllowEditing(editable) f.contentTbl.SetAllowEditing(editable)
// Toggle the Query functionality // Toggle the Query functionality
@@ -481,6 +486,9 @@ func (f *App) refreshContent(node *qt.QTreeWidgetItem) {
f.ui.queryResult.SetEnabled(queryable) f.ui.queryResult.SetEnabled(queryable)
f.ui.mnuExecute.SetEnabled(queryable) f.ui.mnuExecute.SetEnabled(queryable)
tabindex_Query := f.ui.tabWidget.IndexOf(f.ui.tabQuery)
f.ui.tabWidget.SetTabEnabled(tabindex_Query, queryable) // If we were on the query tab at this time, Qt automatically moves us back 1 tab off it
// We're in charge of common status bar text updates // We're in charge of common status bar text updates
// Find database displayname // Find database displayname
f.ui.MainWindow.StatusBar().ShowMessage(f.DatabaseDisplayName(node) + " | " + ld.DriverName()) f.ui.MainWindow.StatusBar().ShowMessage(f.DatabaseDisplayName(node) + " | " + ld.DriverName())
@@ -633,6 +641,10 @@ func (f *App) addTopLevelDatabaseConnection(ld loadedDatabase, displayName strin
nav.SetData(0, BucketPathBSliceRole, qt.NewQVariant12(emptyData)) nav.SetData(0, BucketPathBSliceRole, qt.NewQVariant12(emptyData))
f.ui.Buckets.AddTopLevelItem(nav) f.ui.Buckets.AddTopLevelItem(nav)
// For a newly added DB, switch us back to the Properties pane
f.ui.tabWidget.SetCurrentIndex(0)
f.ui.Buckets.SetCurrentItem(nav) // Select new element f.ui.Buckets.SetCurrentItem(nav) // Select new element
f.handleNavExpansion(nav, 0) // Load child contents but do not recurse further f.handleNavExpansion(nav, 0) // Load child contents but do not recurse further

View File

@@ -34,7 +34,9 @@ type MainWindowUi struct {
menu_Data *qt.QMenu menu_Data *qt.QMenu
menu_Tools *qt.QMenu menu_Tools *qt.QMenu
statusbar *qt.QStatusBar statusbar *qt.QStatusBar
toolBar *qt.QToolBar tbConnect *qt.QToolBar
tbData *qt.QToolBar
tbQuery *qt.QToolBar
actionE_xit *qt.QAction actionE_xit *qt.QAction
mnuExecute *qt.QAction mnuExecute *qt.QAction
mnuDriverVersions *qt.QAction mnuDriverVersions *qt.QAction
@@ -47,6 +49,7 @@ type MainWindowUi struct {
actionApply_changes *qt.QAction actionApply_changes *qt.QAction
actionConnectionManager *qt.QAction actionConnectionManager *qt.QAction
actionCreate_Bolt_database_from_zip *qt.QAction actionCreate_Bolt_database_from_zip *qt.QAction
actionSet_cell_to_null *qt.QAction
} }
// NewMainWindowUi creates all Qt widget classes for MainWindow. // NewMainWindowUi creates all Qt widget classes for MainWindow.
@@ -143,6 +146,17 @@ func NewMainWindowUi() *MainWindowUi {
actionCreate_Bolt_database_from_zip__objectName := qt.NewQAnyStringView3("actionCreate_Bolt_database_from_zip") actionCreate_Bolt_database_from_zip__objectName := qt.NewQAnyStringView3("actionCreate_Bolt_database_from_zip")
ui.actionCreate_Bolt_database_from_zip.SetObjectName(*actionCreate_Bolt_database_from_zip__objectName) ui.actionCreate_Bolt_database_from_zip.SetObjectName(*actionCreate_Bolt_database_from_zip__objectName)
actionCreate_Bolt_database_from_zip__objectName.Delete() // setter copied value actionCreate_Bolt_database_from_zip__objectName.Delete() // setter copied value
icon10 := qt.NewQIcon()
icon10.AddFile4(":/assets/compress.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.actionCreate_Bolt_database_from_zip.SetIcon(icon10)
ui.actionSet_cell_to_null = qt.NewQAction()
actionSet_cell_to_null__objectName := qt.NewQAnyStringView3("actionSet_cell_to_null")
ui.actionSet_cell_to_null.SetObjectName(*actionSet_cell_to_null__objectName)
actionSet_cell_to_null__objectName.Delete() // setter copied value
icon11 := qt.NewQIcon()
icon11.AddFile4(":/assets/note_delete.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.actionSet_cell_to_null.SetIcon(icon11)
/* miqt-uic: no handler for QAction property 'menuRole' */
ui.centralwidget = qt.NewQWidget(ui.MainWindow.QWidget) ui.centralwidget = qt.NewQWidget(ui.MainWindow.QWidget)
centralwidget__objectName := qt.NewQAnyStringView3("centralwidget") centralwidget__objectName := qt.NewQAnyStringView3("centralwidget")
ui.centralwidget.SetObjectName(*centralwidget__objectName) ui.centralwidget.SetObjectName(*centralwidget__objectName)
@@ -198,9 +212,9 @@ func NewMainWindowUi() *MainWindowUi {
ui.propertiesBox.SetReadOnly(true) ui.propertiesBox.SetReadOnly(true)
ui.gridLayout_2.AddWidget2(ui.propertiesBox.QWidget, 0, 0) ui.gridLayout_2.AddWidget2(ui.propertiesBox.QWidget, 0, 0)
icon10 := qt.NewQIcon() icon12 := qt.NewQIcon()
icon10.AddFile4(":/assets/chart_bar.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off) icon12.AddFile4(":/assets/chart_bar.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.tabWidget.AddTab2(ui.tabProperties, icon10, "") ui.tabWidget.AddTab2(ui.tabProperties, icon12, "")
ui.tabData = qt.NewQWidget(ui.tabWidget.QWidget) ui.tabData = qt.NewQWidget(ui.tabWidget.QWidget)
tabData__objectName := qt.NewQAnyStringView3("tabData") tabData__objectName := qt.NewQAnyStringView3("tabData")
ui.tabData.SetObjectName(*tabData__objectName) ui.tabData.SetObjectName(*tabData__objectName)
@@ -219,9 +233,9 @@ func NewMainWindowUi() *MainWindowUi {
ui.contentBox.SetHorizontalScrollMode(qt.QAbstractItemView__ScrollPerPixel) ui.contentBox.SetHorizontalScrollMode(qt.QAbstractItemView__ScrollPerPixel)
ui.verticalLayout.AddWidget(ui.contentBox.QWidget) ui.verticalLayout.AddWidget(ui.contentBox.QWidget)
icon11 := qt.NewQIcon() icon13 := qt.NewQIcon()
icon11.AddFile4(":/assets/table.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off) icon13.AddFile4(":/assets/table.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.tabWidget.AddTab2(ui.tabData, icon11, "") ui.tabWidget.AddTab2(ui.tabData, icon13, "")
ui.tabQuery = qt.NewQWidget(ui.tabWidget.QWidget) ui.tabQuery = qt.NewQWidget(ui.tabWidget.QWidget)
tabQuery__objectName := qt.NewQAnyStringView3("tabQuery") tabQuery__objectName := qt.NewQAnyStringView3("tabQuery")
ui.tabQuery.SetObjectName(*tabQuery__objectName) ui.tabQuery.SetObjectName(*tabQuery__objectName)
@@ -253,9 +267,9 @@ func NewMainWindowUi() *MainWindowUi {
ui.splitter_2.AddWidget(ui.queryResult.QWidget) ui.splitter_2.AddWidget(ui.queryResult.QWidget)
ui.verticalLayout_2.AddWidget(ui.splitter_2.QWidget) ui.verticalLayout_2.AddWidget(ui.splitter_2.QWidget)
icon12 := qt.NewQIcon() icon14 := qt.NewQIcon()
icon12.AddFile4(":/assets/lightning.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off) icon14.AddFile4(":/assets/lightning.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.tabWidget.AddTab2(ui.tabQuery, icon12, "") ui.tabWidget.AddTab2(ui.tabQuery, icon14, "")
ui.splitter.AddWidget(ui.tabWidget.QWidget) ui.splitter.AddWidget(ui.tabWidget.QWidget)
ui.gridLayout.AddWidget2(ui.splitter.QWidget, 0, 0) ui.gridLayout.AddWidget2(ui.splitter.QWidget, 0, 0)
@@ -294,6 +308,7 @@ func NewMainWindowUi() *MainWindowUi {
ui.menu_Data.AddSeparator() ui.menu_Data.AddSeparator()
ui.menu_Data.QWidget.AddAction(ui.actionAddRow) ui.menu_Data.QWidget.AddAction(ui.actionAddRow)
ui.menu_Data.QWidget.AddAction(ui.actionDelete_row) ui.menu_Data.QWidget.AddAction(ui.actionDelete_row)
ui.menu_Data.QWidget.AddAction(ui.actionSet_cell_to_null)
ui.menu_Data.QWidget.AddAction(ui.actionApply_changes) ui.menu_Data.QWidget.AddAction(ui.actionApply_changes)
ui.menu_Tools = qt.NewQMenu(ui.menubar.QWidget) ui.menu_Tools = qt.NewQMenu(ui.menubar.QWidget)
menu_Tools__objectName := qt.NewQAnyStringView3("menu_Tools") menu_Tools__objectName := qt.NewQAnyStringView3("menu_Tools")
@@ -311,23 +326,39 @@ func NewMainWindowUi() *MainWindowUi {
ui.statusbar.SetObjectName(*statusbar__objectName) ui.statusbar.SetObjectName(*statusbar__objectName)
statusbar__objectName.Delete() // setter copied value statusbar__objectName.Delete() // setter copied value
ui.MainWindow.SetStatusBar(ui.statusbar) ui.MainWindow.SetStatusBar(ui.statusbar)
ui.toolBar = qt.NewQToolBar(ui.MainWindow.QWidget) ui.tbConnect = qt.NewQToolBar(ui.MainWindow.QWidget)
toolBar__objectName := qt.NewQAnyStringView3("toolBar") tbConnect__objectName := qt.NewQAnyStringView3("tbConnect")
ui.toolBar.SetObjectName(*toolBar__objectName) ui.tbConnect.SetObjectName(*tbConnect__objectName)
toolBar__objectName.Delete() // setter copied value tbConnect__objectName.Delete() // setter copied value
/* miqt-uic: no handler for toolBar property 'iconSize' */ /* miqt-uic: no handler for tbConnect property 'iconSize' */
ui.toolBar.SetToolButtonStyle(qt.ToolButtonIconOnly) ui.tbConnect.SetToolButtonStyle(qt.ToolButtonIconOnly)
ui.MainWindow.AddToolBar(qt.TopToolBarArea, ui.toolBar) ui.tbConnect.SetFloatable(false)
/* miqt-uic: no handler for toolBar attribute 'toolBarBreak' */ ui.MainWindow.AddToolBar(qt.TopToolBarArea, ui.tbConnect)
ui.toolBar.QWidget.AddAction(ui.actionConnectionManager) /* miqt-uic: no handler for tbConnect attribute 'toolBarBreak' */
ui.toolBar.QWidget.AddAction(ui.actionConnect) ui.tbConnect.QWidget.AddAction(ui.actionConnectionManager)
ui.toolBar.AddSeparator() ui.tbConnect.QWidget.AddAction(ui.actionConnect)
ui.toolBar.QWidget.AddAction(ui.actionRefresh) ui.tbData = qt.NewQToolBar(ui.MainWindow.QWidget)
ui.toolBar.QWidget.AddAction(ui.actionAddRow) tbData__objectName := qt.NewQAnyStringView3("tbData")
ui.toolBar.QWidget.AddAction(ui.actionDelete_row) ui.tbData.SetObjectName(*tbData__objectName)
ui.toolBar.QWidget.AddAction(ui.actionApply_changes) tbData__objectName.Delete() // setter copied value
ui.toolBar.AddSeparator() /* miqt-uic: no handler for tbData property 'iconSize' */
ui.toolBar.QWidget.AddAction(ui.mnuExecute) ui.tbData.SetFloatable(false)
ui.MainWindow.AddToolBar(qt.TopToolBarArea, ui.tbData)
/* miqt-uic: no handler for tbData attribute 'toolBarBreak' */
ui.tbData.QWidget.AddAction(ui.actionRefresh)
ui.tbData.QWidget.AddAction(ui.actionAddRow)
ui.tbData.QWidget.AddAction(ui.actionDelete_row)
ui.tbData.QWidget.AddAction(ui.actionSet_cell_to_null)
ui.tbData.QWidget.AddAction(ui.actionApply_changes)
ui.tbQuery = qt.NewQToolBar(ui.MainWindow.QWidget)
tbQuery__objectName := qt.NewQAnyStringView3("tbQuery")
ui.tbQuery.SetObjectName(*tbQuery__objectName)
tbQuery__objectName.Delete() // setter copied value
/* miqt-uic: no handler for tbQuery property 'iconSize' */
ui.tbQuery.SetFloatable(false)
ui.MainWindow.AddToolBar(qt.TopToolBarArea, ui.tbQuery)
/* miqt-uic: no handler for tbQuery attribute 'toolBarBreak' */
ui.tbQuery.QWidget.AddAction(ui.mnuExecute)
ui.Retranslate() ui.Retranslate()
@@ -338,12 +369,13 @@ func NewMainWindowUi() *MainWindowUi {
// Retranslate reapplies all text translations. // Retranslate reapplies all text translations.
func (ui *MainWindowUi) Retranslate() { func (ui *MainWindowUi) Retranslate() {
ui.MainWindow.SetWindowTitle(qt.QCoreApplication_Tr("yvbolt")) ui.MainWindow.SetWindowTitle(qt.QCoreApplication_Tr("QBolt"))
ui.actionE_xit.SetText(qt.QMainWindow_Tr("E&xit")) ui.actionE_xit.SetText(qt.QMainWindow_Tr("E&xit"))
ui.mnuExecute.SetText(qt.QMainWindow_Tr("E&xecute")) ui.mnuExecute.SetText(qt.QMainWindow_Tr("E&xecute"))
ui.mnuExecute.SetShortcut(qt.NewQKeySequence2(qt.QMainWindow_Tr("F9"))) ui.mnuExecute.SetShortcut(qt.NewQKeySequence2(qt.QMainWindow_Tr("F9")))
ui.mnuDriverVersions.SetText(qt.QMainWindow_Tr("Driver versions...")) ui.mnuDriverVersions.SetText(qt.QMainWindow_Tr("Driver versions..."))
ui.mnuHelpAbout.SetText(qt.QMainWindow_Tr("&About yvbolt")) ui.mnuHelpAbout.SetText(qt.QMainWindow_Tr("&About QBolt"))
ui.mnuHelpAbout.SetToolTip(qt.QMainWindow_Tr("About QBolt"))
ui.mnuHelpAbout.SetShortcut(qt.NewQKeySequence2(qt.QMainWindow_Tr("F1"))) ui.mnuHelpAbout.SetShortcut(qt.NewQKeySequence2(qt.QMainWindow_Tr("F1")))
ui.actionConnect.SetText(qt.QMainWindow_Tr("Connect...")) ui.actionConnect.SetText(qt.QMainWindow_Tr("Connect..."))
ui.actionConnect.SetShortcut(qt.NewQKeySequence2(qt.QMainWindow_Tr("Ctrl+O"))) ui.actionConnect.SetShortcut(qt.NewQKeySequence2(qt.QMainWindow_Tr("Ctrl+O")))
@@ -357,6 +389,7 @@ func (ui *MainWindowUi) Retranslate() {
ui.actionConnectionManager.SetText(qt.QMainWindow_Tr("Connection Manager")) ui.actionConnectionManager.SetText(qt.QMainWindow_Tr("Connection Manager"))
ui.actionConnectionManager.SetToolTip(qt.QMainWindow_Tr("Connection Manager")) ui.actionConnectionManager.SetToolTip(qt.QMainWindow_Tr("Connection Manager"))
ui.actionCreate_Bolt_database_from_zip.SetText(qt.QMainWindow_Tr("Create Bolt database from zip")) ui.actionCreate_Bolt_database_from_zip.SetText(qt.QMainWindow_Tr("Create Bolt database from zip"))
ui.actionSet_cell_to_null.SetText(qt.QMainWindow_Tr("Set cell to null"))
ui.tabWidget.SetTabText(ui.tabWidget.IndexOf(ui.tabProperties), qt.QTabWidget_Tr("Properties")) ui.tabWidget.SetTabText(ui.tabWidget.IndexOf(ui.tabProperties), qt.QTabWidget_Tr("Properties"))
ui.tabWidget.SetTabText(ui.tabWidget.IndexOf(ui.tabData), qt.QTabWidget_Tr("Data")) ui.tabWidget.SetTabText(ui.tabWidget.IndexOf(ui.tabData), qt.QTabWidget_Tr("Data"))
ui.tabWidget.SetTabText(ui.tabWidget.IndexOf(ui.tabQuery), qt.QTabWidget_Tr("Query")) ui.tabWidget.SetTabText(ui.tabWidget.IndexOf(ui.tabQuery), qt.QTabWidget_Tr("Query"))
@@ -365,5 +398,7 @@ func (ui *MainWindowUi) Retranslate() {
ui.menu_Help.SetTitle(qt.QMenuBar_Tr("&Help")) ui.menu_Help.SetTitle(qt.QMenuBar_Tr("&Help"))
ui.menu_Data.SetTitle(qt.QMenuBar_Tr("&Data")) ui.menu_Data.SetTitle(qt.QMenuBar_Tr("&Data"))
ui.menu_Tools.SetTitle(qt.QMenuBar_Tr("&Tools")) ui.menu_Tools.SetTitle(qt.QMenuBar_Tr("&Tools"))
ui.toolBar.SetWindowTitle(qt.QMainWindow_Tr("toolBar")) ui.tbConnect.SetWindowTitle(qt.QMainWindow_Tr("Connection Toolbar"))
ui.tbData.SetWindowTitle(qt.QMainWindow_Tr("Data Toolbar"))
ui.tbQuery.SetWindowTitle(qt.QMainWindow_Tr("Query Toolbar"))
} }

View File

@@ -11,7 +11,7 @@
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
<string>yvbolt</string> <string>QBolt</string>
</property> </property>
<property name="windowIcon"> <property name="windowIcon">
<iconset resource="embed.qrc"> <iconset resource="embed.qrc">
@@ -177,6 +177,7 @@
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="actionAddRow"/> <addaction name="actionAddRow"/>
<addaction name="actionDelete_row"/> <addaction name="actionDelete_row"/>
<addaction name="actionSet_cell_to_null"/>
<addaction name="actionApply_changes"/> <addaction name="actionApply_changes"/>
</widget> </widget>
<widget class="QMenu" name="menu_Tools"> <widget class="QMenu" name="menu_Tools">
@@ -192,9 +193,9 @@
<addaction name="menu_Help"/> <addaction name="menu_Help"/>
</widget> </widget>
<widget class="QStatusBar" name="statusbar"/> <widget class="QStatusBar" name="statusbar"/>
<widget class="QToolBar" name="toolBar"> <widget class="QToolBar" name="tbConnect">
<property name="windowTitle"> <property name="windowTitle">
<string>toolBar</string> <string>Connection Toolbar</string>
</property> </property>
<property name="iconSize"> <property name="iconSize">
<size> <size>
@@ -205,6 +206,9 @@
<property name="toolButtonStyle"> <property name="toolButtonStyle">
<enum>Qt::ToolButtonStyle::ToolButtonIconOnly</enum> <enum>Qt::ToolButtonStyle::ToolButtonIconOnly</enum>
</property> </property>
<property name="floatable">
<bool>false</bool>
</property>
<attribute name="toolBarArea"> <attribute name="toolBarArea">
<enum>TopToolBarArea</enum> <enum>TopToolBarArea</enum>
</attribute> </attribute>
@@ -213,12 +217,51 @@
</attribute> </attribute>
<addaction name="actionConnectionManager"/> <addaction name="actionConnectionManager"/>
<addaction name="actionConnect"/> <addaction name="actionConnect"/>
<addaction name="separator"/> </widget>
<widget class="QToolBar" name="tbData">
<property name="windowTitle">
<string>Data Toolbar</string>
</property>
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
<property name="floatable">
<bool>false</bool>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="actionRefresh"/> <addaction name="actionRefresh"/>
<addaction name="actionAddRow"/> <addaction name="actionAddRow"/>
<addaction name="actionDelete_row"/> <addaction name="actionDelete_row"/>
<addaction name="actionSet_cell_to_null"/>
<addaction name="actionApply_changes"/> <addaction name="actionApply_changes"/>
<addaction name="separator"/> </widget>
<widget class="QToolBar" name="tbQuery">
<property name="windowTitle">
<string>Query Toolbar</string>
</property>
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
<property name="floatable">
<bool>false</bool>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="mnuExecute"/> <addaction name="mnuExecute"/>
</widget> </widget>
<action name="actionE_xit"> <action name="actionE_xit">
@@ -252,7 +295,10 @@
<normaloff>:/assets/help.png</normaloff>:/assets/help.png</iconset> <normaloff>:/assets/help.png</normaloff>:/assets/help.png</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>&amp;About yvbolt</string> <string>&amp;About QBolt</string>
</property>
<property name="toolTip">
<string>About QBolt</string>
</property> </property>
<property name="shortcut"> <property name="shortcut">
<string>F1</string> <string>F1</string>
@@ -355,10 +401,26 @@
</property> </property>
</action> </action>
<action name="actionCreate_Bolt_database_from_zip"> <action name="actionCreate_Bolt_database_from_zip">
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/compress.png</normaloff>:/assets/compress.png</iconset>
</property>
<property name="text"> <property name="text">
<string>Create Bolt database from zip</string> <string>Create Bolt database from zip</string>
</property> </property>
</action> </action>
<action name="actionSet_cell_to_null">
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/note_delete.png</normaloff>:/assets/note_delete.png</iconset>
</property>
<property name="text">
<string>Set cell to null</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
</widget> </widget>
<resources> <resources>
<include location="embed.qrc"/> <include location="embed.qrc"/>

94
sqliteclidriver/event.go Normal file
View File

@@ -0,0 +1,94 @@
package sqliteclidriver
import (
"io"
"sync"
)
const (
evtypeStdout int = iota
evtypeStderr
evtypeExit
)
type processEvent struct {
evtype int
data []byte
err error
}
func (pe processEvent) Error() string {
if pe.err != nil {
return pe.err.Error()
}
if pe.evtype == evtypeStderr {
return string(pe.data)
}
return "<no error>"
}
func (pe processEvent) Unwrap() error {
return pe.err
}
//
func handleEvents(pw io.WriteCloser, pr, pe io.Reader, waiter func() error) (<-chan processEvent, io.WriteCloser, error) {
chEvents := make(chan processEvent, 0)
var wg sync.WaitGroup
go func() {
defer wg.Done()
processEventWorker(pr, evtypeStdout, chEvents)
}()
go func() {
defer wg.Done()
processEventWorker(pe, evtypeStderr, chEvents)
}()
wg.Add(2)
go func() {
// Only call cmd.Wait() after pipes are closed
wg.Wait()
err := waiter()
chEvents <- processEvent{
evtype: evtypeExit,
err: err,
}
close(chEvents)
}()
return chEvents, pw, nil
}
func processEventWorker(p io.Reader, evtype int, dest chan<- processEvent) {
for {
buf := make([]byte, 1024)
n, err := p.Read(buf)
if n > 0 {
dest <- processEvent{
evtype: evtype,
data: buf[0:n],
}
}
if err != nil {
dest <- processEvent{
evtype: evtype,
err: err,
}
// Assume all errors are permanent
// Ordering can produce either io.EOF, ErrClosedPipe, or PathError{"file already closed"}
return
}
}
}

View File

@@ -3,40 +3,15 @@ package sqliteclidriver
import ( import (
"io" "io"
"os/exec" "os/exec"
"sync"
) )
const ( func LocalBinEvents(databasePath string) (<-chan processEvent, io.WriteCloser, error) {
evtypeStdout int = iota // TODO find a way to strictly separate parameters and paths (`--` is not supported here)
evtypeStderr cmd := exec.Command(`/usr/bin/sqlite3`, `-noheader`, `-json`, databasePath)
evtypeExit return execEvents(cmd)
)
type processEvent struct {
evtype int
data []byte
err error
} }
func (pe processEvent) Error() string { func execEvents(cmd *exec.Cmd) (<-chan processEvent, io.WriteCloser, error) {
if pe.err != nil {
return pe.err.Error()
}
if pe.evtype == evtypeStderr {
return string(pe.data)
}
return "<no error>"
}
func (pe processEvent) Unwrap() error {
return pe.err
}
//
func ExecEvents(cmd *exec.Cmd) (<-chan processEvent, io.WriteCloser, error) {
pw, err := cmd.StdinPipe() pw, err := cmd.StdinPipe()
if err != nil { if err != nil {
@@ -58,58 +33,5 @@ func ExecEvents(cmd *exec.Cmd) (<-chan processEvent, io.WriteCloser, error) {
return nil, nil, err return nil, nil, err
} }
chEvents := make(chan processEvent, 0) return handleEvents(pw, pr, pe, cmd.Wait)
var wg sync.WaitGroup
go func() {
defer wg.Done()
processEventWorker(pr, evtypeStdout, chEvents)
}()
go func() {
defer wg.Done()
processEventWorker(pe, evtypeStderr, chEvents)
}()
wg.Add(2)
go func() {
// Only call cmd.Wait() after pipes are closed
wg.Wait()
err = cmd.Wait()
chEvents <- processEvent{
evtype: evtypeExit,
err: err,
}
close(chEvents)
}()
return chEvents, pw, nil
}
func processEventWorker(p io.Reader, evtype int, dest chan<- processEvent) {
for {
buf := make([]byte, 1024)
n, err := p.Read(buf)
if n > 0 {
dest <- processEvent{
evtype: evtype,
data: buf[0:n],
}
}
if err != nil {
dest <- processEvent{
evtype: evtype,
err: err,
}
// Assume all errors are permanent
// Ordering can produce either io.EOF, ErrClosedPipe, or PathError{"file already closed"}
return
}
}
} }

View File

@@ -13,7 +13,7 @@ import (
func TestEventCmd(t *testing.T) { func TestEventCmd(t *testing.T) {
cmd := exec.Command("/bin/bash", "-c", `echo "hello world"`) cmd := exec.Command("/bin/bash", "-c", `echo "hello world"`)
ch, _, err := ExecEvents(cmd) ch, _, err := execEvents(cmd)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -36,7 +36,7 @@ func TestEventCmd(t *testing.T) {
func TestEventCmdStdin(t *testing.T) { func TestEventCmdStdin(t *testing.T) {
cmd := exec.Command("/usr/bin/tr", "a-z", "A-Z") cmd := exec.Command("/usr/bin/tr", "a-z", "A-Z")
ch, pw, err := ExecEvents(cmd) ch, pw, err := execEvents(cmd)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@@ -0,0 +1,94 @@
package sqliteclidriver
import (
"context"
"database/sql"
"database/sql/driver"
"fmt"
"io"
"strconv"
"golang.org/x/crypto/ssh"
)
func SSHEvents(cl *ssh.Client, databasePath string) (<-chan processEvent, io.WriteCloser, error) {
// Client must have already been Dial()'ed
ses, err := cl.NewSession()
if err != nil {
return nil, nil, fmt.Errorf("NewSession: %w", err)
}
pw, err := ses.StdinPipe()
if err != nil {
return nil, nil, fmt.Errorf("StdinPipe: %w", err)
}
pr, err := ses.StdoutPipe()
if err != nil {
return nil, nil, fmt.Errorf("StdoutPipe: %w", err)
}
pe, err := ses.StderrPipe()
if err != nil {
return nil, nil, fmt.Errorf("StderrPipe: %w", err)
}
// FIXME use accurate Bash escaping
// TODO find a way to strictly separate parameters and paths (`--` is not supported here)
ses.Start(`/usr/bin/sqlite3 -noheader -json "` + strconv.Quote(databasePath) + `"`)
return handleEvents(pw, pr, pe, ses.Wait)
}
//
type sshDriver struct {
cl *ssh.Client
}
func (d *sshDriver) Open(connectionString string) (driver.Conn, error) {
// TODO support custom binpath from our connection string
chEvents, pw, err := SSHEvents(d.cl, connectionString)
if err != nil {
return nil, err
}
return &SCConn{
listen: chEvents,
w: pw,
}, nil
}
var _ driver.Driver = &sshDriver{} // interface assertion
//
type sshConnector struct {
connectionString string
driver *sshDriver
}
func (c *sshConnector) Connect(context.Context) (driver.Conn, error) {
// FIXME support context
return c.driver.Open(c.connectionString)
}
func (c *sshConnector) Driver() driver.Driver {
return c.driver
}
var _ driver.Connector = &sshConnector{} // interface assertion
//
// OpenSSH opens an sql.DB connection over SSH using the supplied ssh.Client
// object.
func OpenSSH(cl *ssh.Client, databasePath string) *sql.DB {
return sql.OpenDB(&sshConnector{
connectionString: databasePath,
driver: &sshDriver{
cl: cl,
},
})
}

View File

@@ -21,25 +21,26 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"os/exec" "reflect"
"yvbolt/lexer" "qbolt/lexer"
"github.com/google/uuid" "github.com/google/uuid"
) )
const DriverName = `sqliteclidriver`
var ErrNotSupported = errors.New("Not supported") var ErrNotSupported = errors.New("Not supported")
// //
// SCDriver is a thing that can open connections to the database for a given
// connection string. It has no particular state.
type SCDriver struct{} type SCDriver struct{}
func (d *SCDriver) Open(connectionString string) (driver.Conn, error) { func (d *SCDriver) Open(connectionString string) (driver.Conn, error) {
// TODO support custom binpath from our connection string // TODO support custom binpath from our connection string
chEvents, pw, err := LocalBinEvents(connectionString)
cmd := exec.Command(`/usr/bin/sqlite3`, `-noheader`, `-json`, connectionString) // n.b. doesn't support `--`
chEvents, pw, err := ExecEvents(cmd)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -53,17 +54,21 @@ func (d *SCDriver) Open(connectionString string) (driver.Conn, error) {
var _ driver.Driver = &SCDriver{} // interface assertion var _ driver.Driver = &SCDriver{} // interface assertion
func init() { func init() {
sql.Register("sqliteclidriver", &SCDriver{}) sql.Register(DriverName, &SCDriver{})
} }
// //
// SCConnector is a container holding a driver and connection string together.
// It is the result of parsing a sql.Open(driverName, connectionString) call
// and has not parsed anything.
type SCConnector struct { type SCConnector struct {
connectionString string connectionString string
driver *SCDriver driver *SCDriver
} }
func (c *SCConnector) Connect(context.Context) (driver.Conn, error) { func (c *SCConnector) Connect(context.Context) (driver.Conn, error) {
// FIXME support context
return c.driver.Open(c.connectionString) return c.driver.Open(c.connectionString)
} }
@@ -75,6 +80,7 @@ var _ driver.Connector = &SCConnector{} // interface assertion
// //
// SCConn represents a single, active, open, connection to the database.
type SCConn struct { type SCConn struct {
listen <-chan processEvent listen <-chan processEvent
w io.WriteCloser w io.WriteCloser
@@ -136,7 +142,7 @@ func (s *SCStmt) buildQuery(args []driver.Value) ([]byte, error) {
// Embed query params // Embed query params
// WARNING: Not secure against injection? That's not a security boundary // WARNING: Not secure against injection? That's not a security boundary
// for the purposes of yvbolt, but maybe for other package users // for the purposes of qbolt, but maybe for other package users
var querybuilder bytes.Buffer var querybuilder bytes.Buffer
@@ -376,3 +382,15 @@ func (r *SCRows) Next(dest []driver.Value) error {
r.idx++ r.idx++
return nil return nil
} }
func (r *SCRows) ColumnTypeDatabaseTypeName(index int) string {
return "BLOB" // FIXME We are not type-safe
}
func (r *SCRows) ColumnTypeScanType(index int) reflect.Type {
return reflect.TypeOf([]byte{})
}
func (r *SCRows) ColumnTypeNullable(index int) (nullable, ok bool) {
return true, true // FIXME
}

View File

@@ -21,24 +21,24 @@ type SSHTunnel struct {
Type autoconfig.OneOf Type autoconfig.OneOf
Password *struct { Password *struct {
Password autoconfig.Password Password autoconfig.Password
} } `yicon:":/assets/key.png" json:",omitempty"`
PrivateKeyExternal *struct { PrivateKeyExternal *struct {
PrivateKeyFile autoconfig.ExistingFile `ylabel:"Private Key"` PrivateKeyFile autoconfig.ExistingFile `ylabel:"Private Key"`
PrivateKeyFilePassword autoconfig.Password `ylabel:"Password (optional)"` PrivateKeyFilePassword autoconfig.Password `ylabel:"Password (optional)"`
} `ylabel:"Private key (External)"` } `ylabel:"Private key (External)" yicon:":/assets/page_key.png" json:",omitempty"`
PrivateKeyInternal *struct { PrivateKeyInternal *struct {
PrivateKeyContent autoconfig.MultiLineString `ylabel:"Private Key"` PrivateKeyContent autoconfig.MultiLineString `ylabel:"Private Key"`
PrivateKeyContentPassword autoconfig.Password `ylabel:"Password (optional)"` PrivateKeyContentPassword autoconfig.Password `ylabel:"Password (optional)"`
} `ylabel:"Private key (Internal)"` } `ylabel:"Private key (Internal)" yicon:":/assets/page_key.png" json:",omitempty"`
SSHAgent *sshAgentConn `ylabel:"SSH Agent"` SSHAgent *sshAgentConn `ylabel:"SSH Agent" yicon:":/assets/vendor_ssh.png" json:",omitempty"`
} }
H2 autoconfig.Header `ylabel:"Server host key:" json:",omitempty"` H2 autoconfig.Header `ylabel:"Server host key:" json:",omitempty"`
HostVerification struct { HostVerification struct {
Type autoconfig.OneOf Type autoconfig.OneOf
InsecureSkipVerify *struct{} InsecureSkipVerify *struct{} `json:",omitempty"`
ExternalKnownHostsFile *struct { ExternalKnownHostsFile *struct {
Path autoconfig.ExistingFile Path autoconfig.ExistingFile
} } `json:",omitempty"`
} }
} }

132
table.go
View File

@@ -7,6 +7,7 @@ import (
type TableColumn interface { type TableColumn interface {
SetRowCount(newlen int) SetRowCount(newlen int)
SetCell(aRow int, data any) SetCell(aRow int, data any)
GetCell(aRow int) any // only needed for editing
Display(aRow int) *qt.QVariant Display(aRow int) *qt.QVariant
Inspect(parent *qt.QWidget, aRow int) Inspect(parent *qt.QWidget, aRow int)
ApplySimpleChange(aRow int, data *qt.QVariant) ApplySimpleChange(aRow int, data *qt.QVariant)
@@ -14,22 +15,21 @@ type TableColumn interface {
IndicateBlue() bool IndicateBlue() bool
} }
type EditAdvancedTableColumn interface {
CreateEditor(parent *qt.QWidget, aRow int, resolve, reject func())
}
type NullableTableColumn interface {
CellIsNull(aRow int) bool
}
const ( const (
CO_INSERT = qt.Yellow CO_INSERT = qt.Yellow
CO_EDIT_IMPLICIT = qt.Green CO_EDIT_IMPLICIT = qt.Green
CO_EDIT_EXPLICIT = qt.DarkGreen CO_EDIT_EXPLICIT = qt.DarkGreen
CO_DELETE = qt.Red CO_DELETE = qt.Red
CO_OBJECT = qt.Blue CO_OBJECT = qt.Blue
) CO_CELL_NULL = qt.Gray
type columnType int
const (
columnType_invalid columnType = iota
columnType_inlineText
columnType_bsonDoc
columnType_popupData
// TODO add a column type for map[string]string that can show content in a mini popup?? (Useful for redis + secretsvc)
) )
type tableState struct { type tableState struct {
@@ -48,19 +48,19 @@ type tableState struct {
model *qt.QAbstractTableModel model *qt.QAbstractTableModel
tbl *qt.QTableView tbl *qt.QTableView
dg_normal, dg_bson, dg_binary *qt.QAbstractItemDelegate dg_advanced *qt.QAbstractItemDelegate
} }
// SetupColumns resets the table's content. // SetupColumns resets the table's content.
// It preserves the OnEdited callback // It preserves the OnEdited callback
func (ts *tableState) SetupColumns(columns []columnType, labels []string) { func (ts *tableState) SetupColumns(columns []TableColumn, labels []string) {
if ts.tbl.IsEnabled() { if ts.tbl.IsEnabled() {
ts.tbl.SetEnabled(false) ts.tbl.SetEnabled(false)
ts.model.BeginResetModel() ts.model.BeginResetModel()
} }
ts.columns = make([]TableColumn, len(columns)) ts.columns = columns
ts.columnLabels = labels ts.columnLabels = labels
ts.SetAllowEditing(false) ts.SetAllowEditing(false)
@@ -69,44 +69,25 @@ func (ts *tableState) SetupColumns(columns []columnType, labels []string) {
ts.deleteRows = make(map[int]struct{}, 0) ts.deleteRows = make(map[int]struct{}, 0)
ts.primaryKeys = nil ts.primaryKeys = nil
for aCol, ctype := range columns { for aCol, _ := range columns {
switch ctype {
case columnType_inlineText:
ts.columns[aCol] = &stringColumn{} // Column types that implement the EditAdvancedTableColumn interface
// get hooked up to the custom delegate renderer
// Reset renderer to something like the default Qt one if _, ok := ts.columns[aCol].(EditAdvancedTableColumn); ok {
// Persist the delegate for the lifetime of this tableState - the if ts.dg_advanced == nil {
// QTableView does not take ownership of a supplied delegate(??) // Persist the delegate for the lifetime of this tableState
// TODO is that true? Then why are we passing in the parent? ts.dg_advanced = table_makeAdvancedEditorItemDelegate(ts, ts.tbl.QObject)
if ts.dg_normal == nil {
ts.dg_normal = qt.NewQStyledItemDelegate2(ts.tbl.QObject).QAbstractItemDelegate
} }
ts.tbl.SetItemDelegateForColumn(aCol, ts.dg_normal)
case columnType_popupData: ts.tbl.SetItemDelegateForColumn(aCol, ts.dg_advanced)
ts.columns[aCol] = &binColumn{}
if ts.dg_binary == nil { } else {
ts.dg_binary = table_makeBinaryItemDelegate(ts, ts.tbl.QObject) // Reset renderer to the default Qt one
} ts.tbl.SetItemDelegateForColumn(aCol, nil)
ts.tbl.SetItemDelegateForColumn(aCol, ts.dg_binary)
case columnType_bsonDoc:
ts.columns[aCol] = &bsonColumn{}
if ts.dg_bson == nil {
ts.dg_bson = table_makeBsonItemDelegate(ts, ts.tbl.QObject)
}
ts.tbl.SetItemDelegateForColumn(aCol, ts.dg_bson)
default:
panic("bad columnType")
} }
} }
ts.SetRowCount(0) // populate slices ts.SetRowCount(0) // populate slices
} }
func (ts *tableState) SetAllowEditing(allow bool) { func (ts *tableState) SetAllowEditing(allow bool) {
@@ -187,9 +168,9 @@ func (ts *tableState) SetRowPrimaryKey(aRow int, data []byte) {
func (ts *tableState) Wipeout() { func (ts *tableState) Wipeout() {
ts.SetupColumns( ts.SetupColumns(
[]columnType{ []TableColumn{
columnType_inlineText, &stringColumn{},
columnType_popupData, &binColumn{},
}, },
[]string{"Key", "Value"}, []string{"Key", "Value"},
) )
@@ -317,6 +298,10 @@ func NewTableState(tbl *qt.QTableView) *tableState {
return qt.NewQColor2(CO_OBJECT).ToQVariant() return qt.NewQColor2(CO_OBJECT).ToQVariant()
} }
if supportsNull, ok := ts.columns[aCol].(NullableTableColumn); ok && supportsNull.CellIsNull(aRow) {
return qt.NewQColor2(CO_CELL_NULL).ToQVariant()
}
return qt.NewQVariant() // just default return qt.NewQVariant() // just default
case int(qt.EditRole): case int(qt.EditRole):
@@ -423,3 +408,58 @@ func NewTableState(tbl *qt.QTableView) *tableState {
return &ts return &ts
} }
func table_makeAdvancedEditorItemDelegate(ts *tableState, parent *qt.QObject) *qt.QAbstractItemDelegate {
zombie := qt.NewQStyledItemDelegate2(parent)
zombie.OnCreateEditor(func(super func(parent *qt.QWidget, option *qt.QStyleOptionViewItem, index *qt.QModelIndex) *qt.QWidget, parent *qt.QWidget, option *qt.QStyleOptionViewItem, index *qt.QModelIndex) *qt.QWidget {
// return a custom editor widget
aCol, aRow := index.Column(), index.Row()
self := ts.columns[aCol].(EditAdvancedTableColumn)
// The widget here is rendered stacked on top of the cell's existing DisplayRole
// It doesn't replace it
editor := qt.NewQLabel(parent)
editor.SetAutoFillBackground(true) // Hide parent
editor.SetText("Editing...")
self.CreateEditor(
editor.QWidget,
aRow,
func() {
// Signal success/finished editing
zombie.CommitData(editor.QWidget)
zombie.CloseEditor2(editor.QWidget, qt.QAbstractItemDelegate__NoHint)
},
func() {
// close editor, without, signalling commitData (this was readonly)
//zombie.CommitData(editor.QWidget)
zombie.CloseEditor2(editor.QWidget, qt.QAbstractItemDelegate__NoHint)
},
)
return editor.QWidget
})
zombie.OnSetEditorData(func(super func(editor *qt.QWidget, index *qt.QModelIndex), editor *qt.QWidget, index *qt.QModelIndex) {
// TODO we may want to conditionally do nothing here??
super(editor, index)
})
zombie.OnSetModelData(func(super func(editor *qt.QWidget, model *qt.QAbstractItemModel, index *qt.QModelIndex), editor *qt.QWidget, model *qt.QAbstractItemModel, index *qt.QModelIndex) {
// TODO we may want to conditionally do nothing here??
super(editor, model, index) // Save
})
zombie.OnUpdateEditorGeometry(func(super func(editor *qt.QWidget, option *qt.QStyleOptionViewItem, index *qt.QModelIndex), editor *qt.QWidget, option *qt.QStyleOptionViewItem, index *qt.QModelIndex) {
// after having created our custom editor, set the widget's size as per the table column
editor.SetGeometryWithGeometry(option.Rect())
// super(editor, option, index)
})
return zombie.QAbstractItemDelegate
}

View File

@@ -20,14 +20,26 @@ func (bc *binColumn) SetRowCount(newlen int) {
func (bc *binColumn) SetCell(aRow int, data any) { func (bc *binColumn) SetCell(aRow int, data any) {
str, ok := data.([]byte) str, ok := data.([]byte)
if !ok { if !ok {
panic("bad data type") panic(fmt.Sprintf("bad data type (got %T)", data))
} }
bc.vals[aRow] = str bc.vals[aRow] = str
} }
func (bc *binColumn) GetCell(aRow int) any {
return bc.vals[aRow]
}
func (bc *binColumn) Display(aRow int) *qt.QVariant { func (bc *binColumn) Display(aRow int) *qt.QVariant {
return qt.NewQVariant11(formatUtf8(bc.vals[aRow])) const showLen = 140
cell := bc.vals[aRow]
if len(cell) > showLen {
return qt.NewQVariant11(formatUtf8(cell[0:showLen]) + "...")
}
return qt.NewQVariant11(formatUtf8(cell))
} }
func (bc *binColumn) Inspect(parent *qt.QWidget, aRow int) { func (bc *binColumn) Inspect(parent *qt.QWidget, aRow int) {
@@ -51,72 +63,37 @@ func (bc *binColumn) IndicateBlue() bool {
return false return false
} }
var _ TableColumn = &binColumn{} func (bc *binColumn) CreateEditor(parent *qt.QWidget, aRow int, resolve, reject func()) {
func table_makeBinaryItemDelegate(ts *tableState, parent *qt.QObject) *qt.QAbstractItemDelegate { // Only called if allowEdit=true
const ( bslice := bc.vals[aRow]
ActionMode__invalid int = iota
ActionMode_Text_Readonly // Always a popup, otherwise you would just look at it
ActionMode_Text_Editable_Popup
//ActionMode_Text_Editable_Inline // TODO
ActionMode_Hex_Readonly
)
const ActionModeProperty string = `x-am`
zombie := qt.NewQStyledItemDelegate2(parent)
zombie.OnCreateEditor(func(super func(parent *qt.QWidget, option *qt.QStyleOptionViewItem, index *qt.QModelIndex) *qt.QWidget, parent *qt.QWidget, option *qt.QStyleOptionViewItem, index *qt.QModelIndex) *qt.QWidget {
// return a custom editor widget
aCol, aRow := index.Column(), index.Row()
self := ts.columns[aCol].(*binColumn)
bslice := self.vals[aRow]
// Check if this contains valid UTF-8 data. // Check if this contains valid UTF-8 data.
// If so, show in a multiline text editor + allow editing if this tableState allowed editing // If so, show in a multiline text editor
// If not, use the hexview popup // If not, use the hexview popup + reject all edits
isText := utf8.Valid(bslice) isText := utf8.Valid(bslice)
// The widget here is rendered stacked on top of the cell's existing DisplayRole if isText {
// It doesn't replace it
editor := qt.NewQLabel(parent)
editor.SetAutoFillBackground(true) // Hide parent
if isText && !ts.allowEdit { var textEdit struct {
Value autoconfig.MultiLineString `ylabel:""`
}
editor.SetProperty(ActionModeProperty, qt.NewQVariant4(ActionMode_Text_Readonly)) textEdit.Value = autoconfig.MultiLineString(string(bslice))
editor.SetText("Viewing...")
// TODO autoconfig.OpenDialog(&textEdit, parent, APPNAME, func() {
} else if isText && ts.allowEdit {
editor.SetProperty(ActionModeProperty, qt.NewQVariant4(ActionMode_Text_Editable_Popup))
editor.SetText("Editing...")
var textEdit autoconfig.MultiLineString
textEdit = autoconfig.MultiLineString(string(bslice))
autoconfig.OpenDialog(&textEdit, editor.QWidget, APPNAME, func() {
// Store change directly back into column store // Store change directly back into column store
self.vals[aRow] = []byte(string(textEdit)) bc.vals[aRow] = []byte(string(textEdit.Value))
// Signal success/finished editing // Signal success/finished editing
zombie.CommitData(editor.QWidget) resolve()
zombie.CloseEditor2(editor.QWidget, qt.QAbstractItemDelegate__NoHint)
}) })
} else { } else {
// Binary data // Binary data
// Popup hexview // Popup hexview
editor.SetProperty(ActionModeProperty, qt.NewQVariant4(ActionMode_Hex_Readonly))
editor.SetText("Viewing...")
dlg := qt.NewQDialog(parent) dlg := qt.NewQDialog(parent)
dlg.SetModal(true) dlg.SetModal(true)
dlg.SetWindowTitle(fmt.Sprintf("%s - Hex view (%d bytes)", APPNAME, len(bslice))) dlg.SetWindowTitle(fmt.Sprintf("%s - Hex view (%d bytes)", APPNAME, len(bslice)))
@@ -136,41 +113,11 @@ func table_makeBinaryItemDelegate(ts *tableState, parent *qt.QObject) *qt.QAbstr
dlg.Show() dlg.Show()
dlg.OnFinished(func(_ int) { dlg.OnFinished(func(_ int) {
// close editor, without, signalling commitData (this was readonly) // close editor, without, signalling commitData (this was readonly)
//zombie.CommitData(editor.QWidget) reject()
zombie.CloseEditor2(editor.QWidget, qt.QAbstractItemDelegate__NoHint)
}) })
} }
return editor.QWidget
})
zombie.OnSetEditorData(func(super func(editor *qt.QWidget, index *qt.QModelIndex), editor *qt.QWidget, index *qt.QModelIndex) {
actionType := editor.Property("x-am").ToInt()
switch actionType {
case ActionMode_Text_Readonly:
// TODO
case ActionMode_Text_Editable_Popup:
super(editor, index) // Save
case ActionMode_Hex_Readonly:
// do nothing
default:
panic("bad actionMode in editor delegate widget")
}
})
zombie.OnSetModelData(func(super func(editor *qt.QWidget, model *qt.QAbstractItemModel, index *qt.QModelIndex), editor *qt.QWidget, model *qt.QAbstractItemModel, index *qt.QModelIndex) {
actionType := editor.Property("x-am").ToInt()
switch actionType {
case ActionMode_Text_Readonly:
// TODO
case ActionMode_Text_Editable_Popup:
super(editor, model, index) // Save
case ActionMode_Hex_Readonly:
// do nothing
default:
panic("bad actionMode in editor delegate widget")
}
})
return zombie.QAbstractItemDelegate
} }
var _ TableColumn = &binColumn{}
var _ EditAdvancedTableColumn = &binColumn{} // interface assertion

View File

@@ -26,6 +26,10 @@ func (bc *bsonColumn) SetCell(aRow int, data any) {
bc.vals[aRow] = obj bc.vals[aRow] = obj
} }
func (bc *bsonColumn) GetCell(aRow int) any {
return bc.vals[aRow]
}
func (bc *bsonColumn) Display(aRow int) *qt.QVariant { func (bc *bsonColumn) Display(aRow int) *qt.QVariant {
return qt.NewQVariant11(fmt.Sprintf("BSON (%d keys)", len(bc.vals[aRow]))) return qt.NewQVariant11(fmt.Sprintf("BSON (%d keys)", len(bc.vals[aRow])))
} }
@@ -79,64 +83,20 @@ func (bc *bsonColumn) IndicateBlue() bool {
return true return true
} }
var _ TableColumn = &bsonColumn{} func (bc *bsonColumn) CreateEditor(parent *qt.QWidget, aRow int, resolve, reject func()) {
func table_makeBsonItemDelegate(ts *tableState, parent *qt.QObject) *qt.QAbstractItemDelegate { // TODO this is not currently being called because MongoDB is not marked
zombie := qt.NewQStyledItemDelegate2(parent) // as being editable
zombie.OnCreateEditor(func(super func(parent *qt.QWidget, option *qt.QStyleOptionViewItem, index *qt.QModelIndex) *qt.QWidget, parent *qt.QWidget, option *qt.QStyleOptionViewItem, index *qt.QModelIndex) *qt.QWidget { ptr_bData := &bc.vals[aRow]
// return a custom editor widget
aCol, aRow := index.Column(), index.Row() autoconfig.OpenDialog(ptr_bData, parent, APPNAME, func() {
self := ts.columns[aCol].(*bsonColumn)
ptr_bData := &self.vals[aRow]
// The widget here is rendered stacked on top of the cell's existing DisplayRole
// It doesn't replace it
foo := qt.NewQLabel(parent)
foo.SetAutoFillBackground(true) // Hide parent
foo.SetText("Editing...")
autoconfig.OpenDialog(ptr_bData, foo.QWidget, APPNAME, func() {
// onFinished // onFinished
// We're already working with the data in-pointer, nothing to do here // We're already working with the data in-pointer, nothing to do here
// However - signal to the model that it's finished, // However - signal to the model that it's finished,
resolve()
zombie.CommitData(foo.QWidget)
zombie.CloseEditor2(foo.QWidget, qt.QAbstractItemDelegate__NoHint)
}) })
return foo.QWidget
// return the default editor widget
//return super(parent, option, index)
})
zombie.OnSetEditorData(func(super func(editor *qt.QWidget, index *qt.QModelIndex), editor *qt.QWidget, index *qt.QModelIndex) {
// populate the custom editor with the table's current data
// DO NOTHING HERE - we have already set the data internally
// This calls onSetData
super(editor, index)
})
zombie.OnSetModelData(func(super func(editor *qt.QWidget, model *qt.QAbstractItemModel, index *qt.QModelIndex), editor *qt.QWidget, model *qt.QAbstractItemModel, index *qt.QModelIndex) {
// the table has requested to save data from the editor back into the model
// DO NOTHING HERE - autoconfig{} has already updated the data internally -
// However we do need to trigger model.OnSetData so that the green colour shows?
// FIXME
super(editor, model, index)
})
zombie.OnUpdateEditorGeometry(func(super func(editor *qt.QWidget, option *qt.QStyleOptionViewItem, index *qt.QModelIndex), editor *qt.QWidget, option *qt.QStyleOptionViewItem, index *qt.QModelIndex) {
// after having created our custom editor, set the widget's size as per the table column
editor.SetGeometryWithGeometry(option.Rect())
// super(editor, option, index)
})
return zombie.QAbstractItemDelegate
} }
var _ TableColumn = &bsonColumn{}
var _ EditAdvancedTableColumn = &binColumn{} // interface assertion

76
table_dynamic.go Normal file
View File

@@ -0,0 +1,76 @@
package main
import (
"fmt"
qt "github.com/mappu/miqt/qt6"
)
type dynamicColumn struct {
vals []any
}
func (sc *dynamicColumn) SetRowCount(newlen int) {
sc.vals = slice_set_len(sc.vals, newlen)
}
func (sc *dynamicColumn) SetCell(aRow int, data any) {
sc.vals[aRow] = data
}
func (bc *dynamicColumn) GetCell(aRow int) any {
return bc.vals[aRow]
}
func (sc *dynamicColumn) Display(aRow int) *qt.QVariant {
return sc.SimpleEditData(aRow) // same as Qt::EditDataRole
}
func (sc *dynamicColumn) Inspect(parent *qt.QWidget, aRow int) {
// do nothing
}
func (sc *dynamicColumn) SimpleEditData(aRow int) *qt.QVariant {
// Try and format this usefully for display
switch vv := sc.vals[aRow].(type) {
case string:
return qt.NewQVariant11(vv)
case []byte:
return qt.NewQVariant12(vv) // TODO can we switch to the dynamic editor - just for this cell?
case int64:
return qt.NewQVariant6(vv)
case bool:
return qt.NewQVariant8(vv)
default:
panic(fmt.Sprintf("dynamicColumn missing handling for type %T", vv))
}
}
func (sc *dynamicColumn) ApplySimpleChange(aRow int, data *qt.QVariant) {
switch vv := sc.vals[aRow].(type) {
case string:
sc.vals[aRow] = data.ToString()
case []byte:
sc.vals[aRow] = data.ToByteArray()
case int64:
sc.vals[aRow] = data.ToLongLong()
case bool:
sc.vals[aRow] = data.ToBool()
default:
panic(fmt.Sprintf("dynamicColumn missing handling for type %T", vv))
}
}
func (sc *dynamicColumn) IndicateBlue() bool {
return false
}
var _ TableColumn = &dynamicColumn{}

54
table_gomodule.go Normal file
View File

@@ -0,0 +1,54 @@
package main
import (
qt "github.com/mappu/miqt/qt6"
)
type goModuleColumn struct {
vals []string
}
func (sc *goModuleColumn) SetRowCount(newlen int) {
sc.vals = slice_set_len(sc.vals, newlen)
}
func (sc *goModuleColumn) SetCell(aRow int, data any) {
str, ok := data.(string)
if !ok {
panic("bad data type")
}
sc.vals[aRow] = str
}
func (sc *goModuleColumn) GetCell(aRow int) any {
return sc.vals[aRow]
}
func (sc *goModuleColumn) Display(aRow int) *qt.QVariant {
return qt.NewQVariant11(sc.vals[aRow])
}
func (sc *goModuleColumn) Inspect(parent *qt.QWidget, aRow int) {
// Convert the 'go mod' URL into a (probable) web URL
urlStr := `https://` + sc.vals[aRow] + `/`
url := qt.NewQUrl3(urlStr)
qt.QDesktopServices_OpenUrl(url) // ignore return value
}
func (sc *goModuleColumn) SimpleEditData(aRow int) *qt.QVariant {
// Works directly with the simple (inline) editor, not a custom editor
return qt.NewQVariant11(sc.vals[aRow])
}
func (sc *goModuleColumn) ApplySimpleChange(aRow int, data *qt.QVariant) {
// Works directly with the simple (inline) editor, not a custom editor
sc.vals[aRow] = data.ToString()
}
func (sc *goModuleColumn) IndicateBlue() bool {
return true
}
var _ TableColumn = &goModuleColumn{}

62
table_int64.go Normal file
View File

@@ -0,0 +1,62 @@
package main
import (
"fmt"
qt "github.com/mappu/miqt/qt6"
)
type int64Column struct {
vals []int64
}
func (sc *int64Column) SetRowCount(newlen int) {
sc.vals = slice_set_len(sc.vals, newlen)
}
func (sc *int64Column) SetCell(aRow int, data any) {
str, ok := data.(int64)
if !ok {
panic(fmt.Sprintf("bad data type (got %T)", data))
}
sc.vals[aRow] = str
}
func (bc *int64Column) GetCell(aRow int) any {
return bc.vals[aRow]
}
func (sc *int64Column) Display(aRow int) *qt.QVariant {
// TODO this does not seem sufficient to get QTableView to create a spinbox
// widget by itself
// This may be related to QSpinBox having 32-bit precision
//return qt.NewQVariant4(int(sc.vals[aRow]))
return qt.NewQVariant6(sc.vals[aRow])
}
func (sc *int64Column) Inspect(parent *qt.QWidget, aRow int) {
// do nothing
}
func (sc *int64Column) SimpleEditData(aRow int) *qt.QVariant {
// Works directly with the simple (inline) editor, not a custom editor
return qt.NewQVariant6(sc.vals[aRow])
}
func (sc *int64Column) ApplySimpleChange(aRow int, data *qt.QVariant) {
// Works directly with the simple (inline) editor, not a custom editor
var ok bool
val := data.ToLongLongWithOk(&ok)
if !ok {
panic("Table provided non-numeric data")
}
sc.vals[aRow] = val
}
func (sc *int64Column) IndicateBlue() bool {
return false
}
var _ TableColumn = &int64Column{}

70
table_sqlnullint64.go Normal file
View File

@@ -0,0 +1,70 @@
package main
import (
"database/sql"
"fmt"
qt "github.com/mappu/miqt/qt6"
)
type sqlNullInt64Column struct {
vals []sql.NullInt64
}
func (sc *sqlNullInt64Column) SetRowCount(newlen int) {
sc.vals = slice_set_len(sc.vals, newlen)
}
func (sc *sqlNullInt64Column) SetCell(aRow int, data any) {
str, ok := data.(sql.NullInt64)
if !ok {
panic(fmt.Sprintf("bad data type (got %T)", data))
}
sc.vals[aRow] = str
}
func (bc *sqlNullInt64Column) GetCell(aRow int) any {
return bc.vals[aRow]
}
func (sc *sqlNullInt64Column) Display(aRow int) *qt.QVariant {
cell := sc.vals[aRow]
if cell.Valid {
return qt.NewQVariant6(cell.Int64)
} else {
return qt.NewQVariant11("NULL") // TODO render this somehow differently?
}
}
func (sc *sqlNullInt64Column) CellIsNull(aRow int) bool {
return !sc.vals[aRow].Valid
}
func (sc *sqlNullInt64Column) Inspect(parent *qt.QWidget, aRow int) {
// do nothing
}
func (sc *sqlNullInt64Column) SimpleEditData(aRow int) *qt.QVariant {
// Works directly with the simple (inline) editor, not a custom editor
cell := sc.vals[aRow]
if cell.Valid {
return qt.NewQVariant6(cell.Int64)
} else {
return qt.NewQVariant6(0)
}
}
func (sc *sqlNullInt64Column) ApplySimpleChange(aRow int, data *qt.QVariant) {
// Works directly with the simple (inline) editor, not a custom editor
val := data.ToLongLong()
sc.vals[aRow] = sql.NullInt64{Int64: val, Valid: true}
}
func (sc *sqlNullInt64Column) IndicateBlue() bool {
return false
}
var _ TableColumn = &sqlNullInt64Column{}
var _ NullableTableColumn = &sqlNullInt64Column{}

70
table_sqlnullstring.go Normal file
View File

@@ -0,0 +1,70 @@
package main
import (
"database/sql"
"fmt"
qt "github.com/mappu/miqt/qt6"
)
type sqlNullStringColumn struct {
vals []sql.NullString
}
func (sc *sqlNullStringColumn) SetRowCount(newlen int) {
sc.vals = slice_set_len(sc.vals, newlen)
}
func (sc *sqlNullStringColumn) SetCell(aRow int, data any) {
str, ok := data.(sql.NullString)
if !ok {
panic(fmt.Sprintf("bad data type (got %T)", data))
}
sc.vals[aRow] = str
}
func (bc *sqlNullStringColumn) GetCell(aRow int) any {
return bc.vals[aRow]
}
func (sc *sqlNullStringColumn) CellIsNull(aRow int) bool {
return !sc.vals[aRow].Valid
}
func (sc *sqlNullStringColumn) Display(aRow int) *qt.QVariant {
cell := sc.vals[aRow]
if cell.Valid {
return qt.NewQVariant11(cell.String)
} else {
return qt.NewQVariant11("NULL") // TODO render this somehow differently?
}
}
func (sc *sqlNullStringColumn) Inspect(parent *qt.QWidget, aRow int) {
// do nothing
}
func (sc *sqlNullStringColumn) SimpleEditData(aRow int) *qt.QVariant {
// Works directly with the simple (inline) editor, not a custom editor
cell := sc.vals[aRow]
if cell.Valid {
return qt.NewQVariant11(cell.String)
} else {
return qt.NewQVariant11("")
}
}
func (sc *sqlNullStringColumn) ApplySimpleChange(aRow int, data *qt.QVariant) {
// Works directly with the simple (inline) editor, not a custom editor
str := data.ToString()
sc.vals[aRow] = sql.NullString{String: str, Valid: true}
}
func (sc *sqlNullStringColumn) IndicateBlue() bool {
return false
}
var _ TableColumn = &sqlNullStringColumn{}
var _ NullableTableColumn = &sqlNullStringColumn{}

View File

@@ -1,6 +1,8 @@
package main package main
import ( import (
"fmt"
qt "github.com/mappu/miqt/qt6" qt "github.com/mappu/miqt/qt6"
) )
@@ -15,12 +17,16 @@ func (sc *stringColumn) SetRowCount(newlen int) {
func (sc *stringColumn) SetCell(aRow int, data any) { func (sc *stringColumn) SetCell(aRow int, data any) {
str, ok := data.(string) str, ok := data.(string)
if !ok { if !ok {
panic("bad data type") panic(fmt.Sprintf("bad data type (got %T)", data))
} }
sc.vals[aRow] = str sc.vals[aRow] = str
} }
func (bc *stringColumn) GetCell(aRow int) any {
return bc.vals[aRow]
}
func (sc *stringColumn) Display(aRow int) *qt.QVariant { func (sc *stringColumn) Display(aRow int) *qt.QVariant {
return qt.NewQVariant11(sc.vals[aRow]) return qt.NewQVariant11(sc.vals[aRow])
} }
@@ -30,12 +36,12 @@ func (sc *stringColumn) Inspect(parent *qt.QWidget, aRow int) {
} }
func (sc *stringColumn) SimpleEditData(aRow int) *qt.QVariant { func (sc *stringColumn) SimpleEditData(aRow int) *qt.QVariant {
// Theoretically don't need to return anything here for simple-edits since // Works directly with the simple (inline) editor, not a custom editor
// the popup editor works directly with the column-store
return qt.NewQVariant11(sc.vals[aRow]) return qt.NewQVariant11(sc.vals[aRow])
} }
func (sc *stringColumn) ApplySimpleChange(aRow int, data *qt.QVariant) { func (sc *stringColumn) ApplySimpleChange(aRow int, data *qt.QVariant) {
// Works directly with the simple (inline) editor, not a custom editor
sc.vals[aRow] = data.ToString() sc.vals[aRow] = data.ToString()
} }

View File

@@ -2,6 +2,7 @@ package main
import ( import (
"bytes" "bytes"
"encoding/binary"
"encoding/gob" "encoding/gob"
"go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/bson"
@@ -117,3 +118,14 @@ func alloc_inplace[T any](target **T) {
var newObj T var newObj T
*target = &newObj *target = &newObj
} }
func int64_to_binary8(i int64) []byte {
b := make([]byte, 8)
binary.LittleEndian.PutUint64(b, uint64(i))
return b
}
func binary8_to_int64(val []byte) int64 {
// If this is supplied with a too-short value, we will panic
return int64(binary.LittleEndian.Uint64(val))
}

View File

@@ -15,11 +15,11 @@
"0409": { "0409": {
"Comments": "", "Comments": "",
"CompanyName": "code.ivysaur.me", "CompanyName": "code.ivysaur.me",
"FileDescription": "yvbolt Database Editor", "FileDescription": "QBolt Database Editor",
"FileVersion": "0.0.0.0", "FileVersion": "0.0.0.0",
"LegalCopyright": "code.ivysaur.me", "LegalCopyright": "code.ivysaur.me",
"LegalTrademarks": "", "LegalTrademarks": "",
"ProductName": "yvbolt", "ProductName": "QBolt",
"ProductVersion": "0.0.0" "ProductVersion": "0.0.0"
} }
} }