191 Commits

Author SHA1 Message Date
2a1d0fa036 etcd: support v2 as well as v3 2026-01-27 08:04:46 +13:00
5b9963eb77 rename kvstore_ApplyChanges -> ApplyChanges_binColumn 2026-01-27 08:03:59 +13:00
ca7c827e75 etcd: initial support (v3 only) 2026-01-24 18:52:16 +13:00
fd913fa1eb qbolt: use full installed list of qstyles 2026-01-24 17:47:07 +13:00
8e5f003e29 doc: update TODO, README, CHANGELOG 2026-01-24 17:06:06 +13:00
28afd05199 ui: add appearance changer (qstyle, density, row backgrounds) 2026-01-24 16:51:40 +13:00
99e138aa94 tooltips: show badger/bolt full paths, show in connmgr + main area 2026-01-24 16:16:11 +13:00
518b66b270 config: refactor connector.String() to separate function [...]
- lmdb: adds (readonly) to default names
- sqlite/ssh: focus on DB name
2026-01-24 16:05:15 +13:00
ee066ec7e7 ui: fix 'failed to save' error if we fail to load config dialog 2026-01-24 15:33:20 +13:00
638d2e69eb ui/mainwindow: reduce main padding, reduce data tab padding 2026-01-24 15:29:58 +13:00
894f730706 ui/connection: better dialog size for instant connection 2026-01-24 15:29:47 +13:00
4498542e71 config: use non-conflicting names when saving new entries 2026-01-24 15:23:23 +13:00
a1345f1c24 table: hovering integers show converted timestamp in tooltip 2026-01-22 22:22:42 +13:00
73bc1c050b table: add context menu actions, support setting cell null 2026-01-22 22:22:23 +13:00
6a5605e067 table: show data type when hovering column header 2026-01-22 22:21:01 +13:00
f345860468 doc: update TODO 2026-01-22 18:27:04 +13:00
965a257ebb badger: fix extra data appearing in k/v pairs 2026-01-22 17:00:51 +13:00
fc9d1b54de doc: update TODO 2026-01-17 10:29:02 +13:00
0d04effe90 buntdb: support advanced options 2026-01-17 10:11:51 +13:00
ee1ff5582f bolt: support advanced options 2026-01-17 10:02:18 +13:00
029f79d800 badger: allow resetting advanced options struct 2026-01-16 19:29:28 +13:00
ea0ee78f2e autoconfig: upgrade v0.5->v0.6 (fix int sizes, scroll dialogs, autoupgrade field types, re-reset buttons) 2026-01-16 19:21:18 +13:00
9774a8690b doc: update TODO 2026-01-13 19:19:08 +13:00
3a66197c2c sqlite/sqliteclidriver: fix crash if zero rows in table 2026-01-13 19:19:06 +13:00
a2badc6964 sqlite: clidriver doesn't like "!=", use "<>" instead 2026-01-13 19:10:21 +13:00
aee1a7ede1 badger: support fully advanced configuration 2026-01-13 19:07:33 +13:00
1b5c97abca main: add --config-export, --config-import CLI flags 2026-01-13 19:07:19 +13:00
1c37b71414 sshtunnel: fix warning in test output 2026-01-13 19:07:04 +13:00
e9bdbb8066 doc: update TODO 2026-01-11 14:21:24 +13:00
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
64 changed files with 3098 additions and 848 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

228
CHANGELOG.md Normal file
View File

@@ -0,0 +1,228 @@
# Changelog
Unreleased vNext
- Badger, Bolt, BuntDB: Support full advanced configuration options
- Badger, Bolt: Show full paths to database file on hover
- SQLite CLI/SSH driver: Fix a syntax issue browsing tables, fix an issue if a table has zero rows
- Allow exporting configuration via new `--config-export` and `--config-import` command-line arguments
- UI: Show column's datatype in hover tooltip
- UI: Add context menu actions to data view
- UI: Allow setting cells to NULL
- UI: Detect integer Unix timestamps (seconds or milliseconds) and show local+UTC time in hover tooltip
- UI: Allow configuring the appearance - Qt Style, Density/Padding, and alternating row backgrounds
- UI: Reduce default padding in the main window
- UI: Update autoconfig library to v0.6.0: large configuration dialogs now scrollable, adds "reset to default" buttons
- UI: Show basic database properties when hovering connection in Connection Manager and in main tree view
- Connection Manager: Add `(2)` prefixes when saving new connections
- Fix an issue with uninitialized slice data appearing in binary data columns, affecting at least Badger
- Fix a cosmetic issue Connection Manager saying "fail to save" error messages if we fail to load
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,15 @@ 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
.PHONY: test-etcd
test-etcd:
# v3.5 series: last version to support both etcd v2 and v3 APIs
# Optional: can use `--experimental-enable-v2v3 ''` flag to map v2 API into the v3 namespace,
# otherwise they are separate storages
# Test URL: http://127.0.0.1:2379/
sudo docker run --rm -p 2379:2379 -p 2380:2380 gcr.io/etcd-development/etcd:v3.5.26 /usr/local/bin/etcd --name s1 --enable-v2 --experimental-enable-v2v3 'mapping-prefix-here' --data-dir /etcd-data --listen-client-urls http://0.0.0.0:2379 --advertise-client-urls http://0.0.0.0:2379

199
README.md
View File

@@ -1,42 +1,54 @@
# yvbolt # QBolt
A graphical interface for multiple databases. A graphical interface for multiple databases.
## Features ## Features
- Native desktop application, running on Linux, Windows, macOS, and Android - Lightweight native desktop application, running on Linux, Windows, macOS, and Android
- Supports many database types
- 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
- "Set cell to null" via context menu
- 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 - Optimised grid renderer
- Hex viewer for binary data - Uses virtual-scrolling and a type-safe column-store
- Safe handling for non-UTF8 key and data fields
- Hex viewer for binary data
- Detect integer Unix timestamps (seconds or milliseconds) and show local+UTC time in hover tooltip
- Configurable appearance (style, density, alternating row backgrounds)
- Connection Manager saves connections with AEAD AES256-GCM using OS keychain - Connection Manager saves connections with AEAD AES256-GCM using OS keychain
- Command-line feature to import/export saved connections
See also [qbolt](https://code.ivysaur.me/qbolt) for more/different functionality. - SSH tunnel for supported databases
## Supported databases ## Supported databases
There are currently 15 supported databases: There are currently 19 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, advanced |Backup, restore, compact
Bitcask |Yes |Yes |No |Readonly, autorecovery |Backup Bitcask |Yes |Yes |No |Readonly, autorecovery |Backup
Bolt |Yes |Yes |No |Readonly |Create/delete child buckets, import/export as zip BuntDB |Yes |Yes |No |In-memory, advanced |Shrink
Bolt |Yes |Yes |No |Readonly, advanced |Create/delete child buckets, import/export as zip
Debconf |Yes |No |No | | Debconf |Yes |No |No | |
Etcd |Yes |Yes |No |v2/v3 |(v2) Create/delete child databases
Freedesktop.org Secret Service |Yes |No | No | |Unlock, create new collection Freedesktop.org Secret Service |Yes |No | No | |Unlock, create new collection
LevelDB |Yes |Yes |No |Readonly | LevelDB |Yes |Yes |No |Readonly |
LMDB |Yes |Yes |No |Multi-DB, readonly |Create/delete child databases LMDB |Yes |Yes |No |Multi-DB, readonly |Create/delete child databases
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 +59,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

57
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
@@ -24,8 +23,12 @@
- MySQL (& MariaDB/TiDB) - MySQL (& MariaDB/TiDB)
- Postgres - Postgres
- CLI using psql - CLI using psql
- Firebird/interbase (embedded and remote)
- 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
- DuckDB https://github.com/duckdb/duckdb-go
- 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,11 +39,7 @@
- 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
- v2: hierarchal
- v3: flat key namespace
- CSV file - CSV file
- Allow querying with sqlite or duckDB? - Allow querying with sqlite or duckDB?
- Parquet file - Parquet file
@@ -57,16 +56,20 @@
- 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, ...
- Other language DBs - Other language DBs
- C, C++ - C, C++
- Berkeley BDB
- Tokyo Cabinet, Kyoto Cabinet, Tkrzw - Tokyo Cabinet, Kyoto Cabinet, Tkrzw
- 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)
- Other classic DBM (Samba tdb, GNU gdbm, ...)
- `/var/cache/man/index.db` is a gdbm file
- 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,29 +78,47 @@
- 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:
- v1/v2/v3 support - v1/v2/v3 support
- option to use namespace separators for virtual buckets - option to use namespace separators for virtual buckets / isolate specific key ranges?
- 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?) - other special objects
- remove current hardcoded LIMIT 1000 - triggers? udf functions?
- attach additional db to same connection - 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")
- view the contents of an index, using imposter tables - https://sqlite.org/imposter.html
- ~~autoincrement: if column is autoincrement and left blank on insert, do not populate in INSERT statement~~ works with implicitly null columns
- BUG: non-nullable columns are being detected as nullable
- 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
- Popup prompt for SSHkey password
- Dynamic SSH_AUTH_SOCK env instead of static
- SSH over Cockpit - SSH over Cockpit
- Performance - Performance
- Warning if data table is filtered to 1000 rows, or add pagination - Warning if data table is filtered to 1000 rows, or add pagination
@@ -110,3 +131,11 @@
- 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
- Lotus, Rose, Pebble, ...: Support other advanced options
- UI: Save appearance settings to file
- UI: Apply appearance settings to query result table
- UI: List all available QStyles
- Etcd:
- Support SSH tunnel
- Other actions, compaction, backup/restore, ...

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_etcd.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 576 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

@@ -13,7 +13,12 @@ import (
) )
type DBConnector interface { type DBConnector interface {
Connect(context.Context) (loadedDatabase, string, error) Connect(context.Context) (loadedDatabase, error)
fmt.Stringer
}
type DetailedStringer interface {
DetailedString() string
} }
type ConnectionConfig struct { type ConnectionConfig struct {
@@ -22,6 +27,8 @@ 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"`
Etcd *etcdConn `ylabel:"etcd" yicon:":/assets/vendor_etcd.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 +36,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 {
@@ -60,10 +69,15 @@ func (cc *ConnectionConfig) Icon() string {
return yicon return yicon
} }
// String is used as the connection's name when (A) connecting directly, to show
// in the left-hand nav; and (B) when saving a new connection, the saved name
// It's usually a simple, single word or filename.
func (cc *ConnectionConfig) String() string { func (cc *ConnectionConfig) String() string {
if selection, err := cc.selection(); err == nil { if selection, err := cc.selection(); err == nil {
if stringer, ok := selection.(fmt.Stringer); ok { if stringer, ok := selection.(fmt.Stringer); ok {
return stringer.String() if candidate := stringer.String(); candidate != "" && candidate != "." {
return candidate
}
} }
} }
@@ -74,6 +88,24 @@ func (cc *ConnectionConfig) String() string {
return string(cc.Type) return string(cc.Type)
} }
// Tooltip is used as the hover tooltip in the connection manager.
// It can show more detail (e.g. full path spec).
// If there is no useful more detail, returns empty-string.
func (cc *ConnectionConfig) Tooltip() string {
// Try DetailedString() if there is such a method
if selection, err := cc.selection(); err == nil {
if getDetails, ok := selection.(DetailedStringer); ok {
if candidate := getDetails.DetailedString(); candidate != "" && candidate != "." {
return candidate
}
}
}
// Otherwise - just same as .String()
return cc.String()
}
func (cc *ConnectionConfig) selection() (DBConnector, error) { func (cc *ConnectionConfig) selection() (DBConnector, error) {
selection := reflect.ValueOf(cc).Elem().FieldByName(string(cc.Type)) selection := reflect.ValueOf(cc).Elem().FieldByName(string(cc.Type))
if !selection.IsValid() { if !selection.IsValid() {
@@ -88,10 +120,10 @@ func (cc *ConnectionConfig) selection() (DBConnector, error) {
return con, nil return con, nil
} }
func (cc *ConnectionConfig) Connect(ctx context.Context) (loadedDatabase, string, error) { func (cc *ConnectionConfig) Connect(ctx context.Context) (loadedDatabase, error) {
dbc, err := cc.selection() dbc, err := cc.selection()
if err != nil { if err != nil {
return nil, "", fmt.Errorf("Invalid database engine %q", cc.Type) return nil, fmt.Errorf("Invalid database engine %q", cc.Type)
} }
return dbc.Connect(ctx) return dbc.Connect(ctx)
@@ -114,6 +146,7 @@ func (f *App) showConnectDialog(config *ConnectionConfig) {
dlg.ConnectDialog.SetParent2(f.ui.MainWindow.QWidget, qt.Dialog) dlg.ConnectDialog.SetParent2(f.ui.MainWindow.QWidget, qt.Dialog)
dlg.ConnectDialog.SetModal(true) dlg.ConnectDialog.SetModal(true)
dlg.ConnectDialog.SetAttribute(qt.WA_DeleteOnClose) dlg.ConnectDialog.SetAttribute(qt.WA_DeleteOnClose)
dlg.formLayout.SetSizeConstraint(qt.QLayout__SetMinAndMaxSize) // Expand dialog to fit form content
saver := autoconfig.MakeConfigArea(config, dlg.formLayout) saver := autoconfig.MakeConfigArea(config, dlg.formLayout)
@@ -129,15 +162,17 @@ func (f *App) showConnectDialog(config *ConnectionConfig) {
// Connect -> get ld // Connect -> get ld
ctx := context.Background() // TODO do in background thread? ctx := context.Background() // TODO do in background thread?
ld, displayName, err := config.Connect(ctx) ld, err := config.Connect(ctx)
if err != nil { if err != nil {
_ = qt.QMessageBox_Critical(dlg.ConnectDialog.QWidget, APPNAME, fmt.Sprintf("Connecting to %s database: %s", config.Type, err.Error())) _ = qt.QMessageBox_Critical(dlg.ConnectDialog.QWidget, APPNAME, fmt.Sprintf("Connecting to %s database: %s", config.Type, err.Error()))
// Prevent the dialog from closing: do not call super() // Prevent the dialog from closing: do not call super()
return return
} }
displayName := config.String()
// Add ld to mainwindow // Add ld to mainwindow
f.addTopLevelDatabaseConnection(ld, displayName) f.addTopLevelDatabaseConnection(ld, displayName, config.Tooltip())
// Connection OK // Connection OK
// Offer to save into connection-manager // Offer to save into connection-manager
@@ -187,6 +222,8 @@ func (f *App) TrySaveIntoConnectionManager(cc *ConnectionConfig, displayName str
displayName = cc.String() displayName = cc.String()
} }
displayName = data.getNonConflictingName(displayName)
data.Entries = append(data.Entries, SavedConfigEntry{ data.Entries = append(data.Entries, SavedConfigEntry{
Description: displayName, Description: displayName,
Connection: *cc, Connection: *cc,
@@ -202,7 +239,7 @@ func (f *App) OnMnuConnectionManagerClick() {
data, err := f.getConnectionManagerContents() data, err := f.getConnectionManagerContents()
if err != nil { if err != nil {
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, ConnMgrSaveError{err}.Error()) qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, ConnMgrLoadError{err}.Error())
return return
} }
@@ -218,6 +255,10 @@ func (f *App) OnMnuConnectionManagerClick() {
itm.SetText(0, entry.Description) itm.SetText(0, entry.Description)
itm.SetIcon(0, qt.NewQIcon4(entry.Connection.Icon())) itm.SetIcon(0, qt.NewQIcon4(entry.Connection.Icon()))
if tooltip := entry.Connection.Tooltip(); tooltip != "" && tooltip != entry.Description {
itm.SetToolTip(0, tooltip)
}
dlg.treeWidget.AddTopLevelItem(itm) dlg.treeWidget.AddTopLevelItem(itm)
} }
@@ -252,7 +293,7 @@ func (f *App) OnMnuConnectionManagerClick() {
entry := data.Entries[dlg.treeWidget.IndexFromItem(itm).Row()] entry := data.Entries[dlg.treeWidget.IndexFromItem(itm).Row()]
ld, _, err := entry.Connection.Connect(ctx) ld, err := entry.Connection.Connect(ctx)
if err != nil { if err != nil {
_ = qt.QMessageBox_Critical(dlg.connectionManagerDialog.QWidget, APPNAME, fmt.Sprintf("Connecting to %s database: %s", entry.Connection.Type, err.Error())) _ = qt.QMessageBox_Critical(dlg.connectionManagerDialog.QWidget, APPNAME, fmt.Sprintf("Connecting to %s database: %s", entry.Connection.Type, err.Error()))
return false return false
@@ -261,7 +302,7 @@ func (f *App) OnMnuConnectionManagerClick() {
// Add ld to mainwindow // Add ld to mainwindow
// Don't use the displayName from the Connect() function - use the saved // Don't use the displayName from the Connect() function - use the saved
// displayname instead // displayname instead
f.addTopLevelDatabaseConnection(ld, entry.Description) f.addTopLevelDatabaseConnection(ld, entry.Description, entry.Connection.Tooltip())
return true return true
} }
@@ -278,7 +319,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 {
@@ -346,6 +388,8 @@ func (f *App) OnMnuConnectionManagerClick() {
autoconfig.OpenDialog(&obj, dlg.connectionManagerDialog.QWidget, "New connection", func() { autoconfig.OpenDialog(&obj, dlg.connectionManagerDialog.QWidget, "New connection", func() {
obj.Description = data.getNonConflictingName(obj.Description)
data.Entries = append(data.Entries, obj) data.Entries = append(data.Entries, obj)
addEntryFor(obj) addEntryFor(obj)

View File

@@ -12,6 +12,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"sort" "sort"
"strconv"
"strings" "strings"
"github.com/zalando/go-keyring" "github.com/zalando/go-keyring"
@@ -27,12 +28,36 @@ type SavedConfig struct {
Entries []SavedConfigEntry Entries []SavedConfigEntry
} }
// getNonConflictingName returns a version of the input name that does not
// conflict with any existing saved entry.
// It adds " (2)", " (3)", etc. prefixes.
func (s *SavedConfig) getNonConflictingName(nameSuggest string) string {
existingNames := make(map[string]struct{}, len(s.Entries))
for _, e := range s.Entries {
existingNames[e.Description] = struct{}{}
}
i := 0
for {
i++ // starts at 1
testName := nameSuggest
if i > 1 {
testName += ` (` + strconv.Itoa(i) + `)`
}
if _, ok := existingNames[testName]; !ok {
return testName
}
}
}
const ( const (
saveSettingsFilename = `settings.dat` saveSettingsFilename = `settings.dat`
keychainUserName = `settings-encryption-key` keychainUserName = `settings-encryption-key`
) )
func (f *App) getConnectionManagerContents() (*SavedConfig, error) { func (f *App) getConnectionManagerContentsJson() ([]byte, error) {
cfgd, err := os.UserConfigDir() cfgd, err := os.UserConfigDir()
if err != nil { if err != nil {
return nil, err return nil, err
@@ -42,13 +67,6 @@ func (f *App) getConnectionManagerContents() (*SavedConfig, error) {
ciphertext, err := os.ReadFile(filepath.Join(cfgd, APPNAME, saveSettingsFilename)) ciphertext, err := os.ReadFile(filepath.Join(cfgd, APPNAME, saveSettingsFilename))
if err != nil { if err != nil {
if os.IsNotExist(err) {
// No file exists. Use blank
return &SavedConfig{
UserAgent: APPNAME,
}, nil
}
return nil, err return nil, err
} }
@@ -77,8 +95,19 @@ func (f *App) getConnectionManagerContents() (*SavedConfig, error) {
return nil, err return nil, err
} }
plaintext, err := cw.Open(ciphertext[:0], nil, ciphertext, nil) // @ref https://pkg.go.dev/crypto/cipher#AEAD return cw.Open(ciphertext[:0], nil, ciphertext, nil) // @ref https://pkg.go.dev/crypto/cipher#AEAD
}
func (f *App) getConnectionManagerContents() (*SavedConfig, error) {
plaintext, err := f.getConnectionManagerContentsJson()
if err != nil { if err != nil {
if os.IsNotExist(err) {
// No file exists. Use blank
return &SavedConfig{
UserAgent: APPNAME,
}, nil
}
return nil, err return nil, err
} }
@@ -99,6 +128,21 @@ func (f *App) getConnectionManagerContents() (*SavedConfig, error) {
} }
func (f *App) saveConnectionManagerContents(sc *SavedConfig) error { func (f *App) saveConnectionManagerContents(sc *SavedConfig) error {
// Force update the saved version
sc.UserAgent = APPNAME + `/` + appVersion // e.g. QBolt/v0.0.0-devel
// Marshal
plaintext, err := json.Marshal(sc)
if err != nil {
return err
}
return f.saveConnectionManagerContentsJson(plaintext)
}
func (f *App) saveConnectionManagerContentsJson(plaintext []byte) error {
cfgd, err := os.UserConfigDir() cfgd, err := os.UserConfigDir()
if err != nil { if err != nil {
return err return err
@@ -158,16 +202,6 @@ func (f *App) saveConnectionManagerContents(sc *SavedConfig) error {
} }
} }
// Force update the saved version
sc.UserAgent = APPNAME + `/` + appVersion // e.g. yvbolt/v0.0.0-devel
// Marshal
plaintext, err := json.Marshal(sc)
if err != nil {
return err
}
// Encrypt // Encrypt
encryptionKeyBytes, err := hex.DecodeString(details) encryptionKeyBytes, err := hex.DecodeString(details)

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"errors"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
@@ -31,7 +32,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 {
@@ -65,7 +66,7 @@ func (ld *badgerLoadedDatabase) RenderForNav(f *tableState, bucketPath []string)
func (n *badgerLoadedDatabase) ApplyChanges(f *tableState, bucketPath []string) error { func (n *badgerLoadedDatabase) ApplyChanges(f *tableState, bucketPath []string) error {
return n.db.Update(func(txn *badger.Txn) error { return n.db.Update(func(txn *badger.Txn) error {
return kvstore_ApplyChanges(f, txn.Set, txn.Delete) return ApplyChanges_binColumn(f, txn.Set, txn.Delete)
}) })
} }
@@ -137,14 +138,30 @@ var _ editableLoadedDatabase = &badgerLoadedDatabase{} // interface assertion
// //
type BadgerResettableOptions badger.Options
func (bao *BadgerResettableOptions) Reset() {
if bao == nil {
return
}
opts := badger.DefaultOptions("")
opts.Logger = nil // Have to wipe this out otherwise we can't JSON marshal our struct
*bao = BadgerResettableOptions(opts)
}
type badgerConnection struct { type badgerConnection struct {
Type autoconfig.OneOf Type autoconfig.OneOf
Disk *struct { Disk *struct {
Directory autoconfig.ExistingDirectory Directory autoconfig.ExistingDirectory
Readonly bool Readonly bool
Encryption *encryptionKey Encryption *encryptionKey
} } `json:",omitempty"`
Memory *struct{} Memory *struct{} `json:",omitempty"`
Advanced *struct {
*BadgerResettableOptions `ylabel:"Options"`
} `json:",omitempty"`
} }
type encryptionKey struct { type encryptionKey struct {
@@ -172,37 +189,65 @@ func (e encryptionKey) Get() ([]byte, error) {
} }
} }
func (bdc *badgerConnection) Connect(ctx context.Context) (loadedDatabase, string, error) { func (bdc *badgerConnection) String() string {
if bdc.Disk != nil { return filepath.Base(bdc.DetailedString())
opts := badger.DefaultOptions(string(bdc.Disk.Directory)) }
func (bdc *badgerConnection) DetailedString() string {
if bdc.Type == "Disk" {
return string(bdc.Disk.Directory)
} else if bdc.Type == "Memory" {
return `:memory:` // SQLite-style naming
} else if bdc.Type == "Advanced" {
return bdc.Advanced.BadgerResettableOptions.Dir
}
return "" // unreachable
}
var _ DBConnector = &badgerConnection{} // interface assertion
func (bdc *badgerConnection) Connect(ctx context.Context) (loadedDatabase, error) {
var opts badger.Options
// Basic options
if bdc.Type == "Disk" {
opts = badger.DefaultOptions(string(bdc.Disk.Directory))
opts.ReadOnly = bdc.Disk.Readonly opts.ReadOnly = bdc.Disk.Readonly
opts.MetricsEnabled = false opts.MetricsEnabled = false
if bdc.Disk.Encryption != nil { if bdc.Disk.Encryption != nil {
ehx, err := bdc.Disk.Encryption.Get() ehx, err := bdc.Disk.Encryption.Get()
if err != nil { if err != nil {
return nil, "", fmt.Errorf("Loading encryption key: %w", err) return nil, fmt.Errorf("Loading encryption key: %w", err)
} }
if !(len(ehx) == 16 || len(ehx) == 24 || len(ehx) == 32) { if !(len(ehx) == 16 || len(ehx) == 24 || len(ehx) == 32) {
return nil, "", fmt.Errorf("Encryption key must be 16/24/32 bytes long, got %d", len(ehx)) return nil, fmt.Errorf("Encryption key must be 16/24/32 bytes long, got %d", len(ehx))
} }
opts.EncryptionKey = ehx opts.EncryptionKey = ehx
} }
} else if bdc.Type == "Memory" {
opts = badger.DefaultOptions("").WithInMemory(true)
} else if bdc.Type == "Advanced" {
if bdc.Advanced.BadgerResettableOptions == nil {
return nil, errors.New("Options not provided")
}
opts = badger.Options(*bdc.Advanced.BadgerResettableOptions)
// Reinstate default logger that we wiped out
tmpOpts := badger.DefaultOptions("")
opts.Logger = tmpOpts.Logger
}
db, err := badger.Open(opts) db, err := badger.Open(opts)
if err != nil { if err != nil {
return nil, "", err return nil, err
} }
return &badgerLoadedDatabase{db: db}, filepath.Base(string(bdc.Disk.Directory)), nil return &badgerLoadedDatabase{db: db}, nil
} else { // memory
db, err := badger.Open(badger.DefaultOptions("").WithInMemory(true))
if err != nil {
return nil, "", err
}
return &badgerLoadedDatabase{db: db}, `:memory:`, nil // SQLite-style naming
}
} }

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()
@@ -53,7 +53,7 @@ func (ld *bitcaskLdb) RenderForNav(f *tableState, bucketPath []string) error {
} }
func (n *bitcaskLdb) ApplyChanges(f *tableState, bucketPath []string) error { func (n *bitcaskLdb) ApplyChanges(f *tableState, bucketPath []string) error {
return kvstore_ApplyChanges( return ApplyChanges_binColumn(
f, f,
func(k, v []byte) error { return n.db.Put(bitcask.Key(k), bitcask.Value(v)) }, func(k, v []byte) error { return n.db.Put(bitcask.Key(k), bitcask.Value(v)) },
func(k []byte) error { return n.db.Delete(bitcask.Key(k)) }, func(k []byte) error { return n.db.Delete(bitcask.Key(k)) },
@@ -98,7 +98,7 @@ func (c *bitcaskDBConnection) Reset() {
c.AutoRecovery = true c.AutoRecovery = true
} }
func (c *bitcaskDBConnection) Connect(ctx context.Context) (loadedDatabase, string, error) { func (c *bitcaskDBConnection) Connect(ctx context.Context) (loadedDatabase, error) {
db, err := bitcask.Open( db, err := bitcask.Open(
string(c.Directory), string(c.Directory),
@@ -106,8 +106,14 @@ func (c *bitcaskDBConnection) Connect(ctx context.Context) (loadedDatabase, stri
bitcask.WithAutoRecovery(c.AutoRecovery), bitcask.WithAutoRecovery(c.AutoRecovery),
) )
if err != nil { if err != nil {
return nil, "", err return nil, err
} }
return &bitcaskLdb{db: db}, filepath.Base(string(c.Directory)), nil return &bitcaskLdb{db: db}, nil
} }
func (c *bitcaskDBConnection) String() string {
return filepath.Base(string(c.Directory))
}
var _ DBConnector = &bitcaskDBConnection{} // interface assertion

View File

@@ -21,30 +21,56 @@ type boltLoadedDatabase struct {
db *bbolt.DB db *bbolt.DB
} }
type boltAdvancedOptions struct {
bbolt.Options
}
func (bao *boltAdvancedOptions) Reset() {
bao.Options = *bbolt.DefaultOptions
// Interfaces not JSON marshallable
bao.OpenFile = nil
bao.Logger = nil
}
func (bao *boltAdvancedOptions) String() string {
return "Configured" // Override bbolt.Options.String() for autoconfig
}
type boltConfig struct { type boltConfig struct {
Path autoconfig.ExistingFile `yfilter:"Bolt database (*.db);;All files (*)"` Path autoconfig.ExistingFile `yfilter:"Bolt database (*.db);;All files (*)"`
Readonly bool Readonly bool
AdvancedOptions *boltAdvancedOptions `json:",omitempty"`
} }
func (bc *boltConfig) String() string { // n.b. only used for default names in connection manager var _ DBConnector = &boltConfig{} // interface assertion
return filepath.Base(string(bc.Path))
func (bc *boltConfig) String() string {
ret := filepath.Base(string(bc.Path))
if bc.Readonly {
ret += " (read-only)"
}
return ret
} }
func (bc *boltConfig) Connect(ctx context.Context) (loadedDatabase, string, error) { func (bc *boltConfig) DetailedString() string {
return string(bc.Path)
}
func (bc *boltConfig) Connect(ctx context.Context) (loadedDatabase, error) {
opts := bbolt.Options{ opts := bbolt.Options{
Timeout: 1 * time.Second, Timeout: 1 * time.Second,
ReadOnly: bc.Readonly, ReadOnly: bc.Readonly,
} }
if bc.AdvancedOptions != nil {
// Use them instead
// Q? fixup OpenFile/Logger?
opts = bc.AdvancedOptions.Options
}
db, err := bbolt.Open(string(bc.Path), 0644, &opts) db, err := bbolt.Open(string(bc.Path), 0644, &opts)
if err != nil { if err != nil {
return nil, "", fmt.Errorf("Failed to load database '%s': %w", bc.Path, err) return nil, fmt.Errorf("Failed to load database '%s': %w", bc.Path, err)
}
displayName := filepath.Base(string(bc.Path))
if bc.Readonly {
displayName += " (read-only)"
} }
ld := &boltLoadedDatabase{ ld := &boltLoadedDatabase{
@@ -52,7 +78,7 @@ func (bc *boltConfig) Connect(ctx context.Context) (loadedDatabase, string, erro
db: db, db: db,
} }
return ld, displayName, nil return ld, nil
} }
func (ld *boltLoadedDatabase) DriverName() string { func (ld *boltLoadedDatabase) DriverName() string {
@@ -75,7 +101,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)
@@ -107,10 +133,10 @@ func (ld *boltLoadedDatabase) RenderForNav(f *tableState, bucketPath []string) e
return nil return nil
} }
// kvstore_ApplyChanges is a helper function to apply edits to K/V stores that // ApplyChanges_binColumn is a helper function to apply edits to K/V stores that
// can use a common abstraction. // can use a common abstraction.
// It always uses the "popupData" type i.e. []byte. // It always uses the "binColumn" type i.e. []byte.
func kvstore_ApplyChanges(f *tableState, Put func(k, v []byte) error, Delete func(k []byte) error) error { func ApplyChanges_binColumn(f *tableState, Put func(k, v []byte) error, Delete func(k []byte) error) error {
// Columns are two binColumn // Columns are two binColumn
keyCol := f.columns[0].(*binColumn) keyCol := f.columns[0].(*binColumn)
@@ -174,7 +200,7 @@ func (n *boltLoadedDatabase) ApplyChanges(f *tableState, bucketPath []string) er
// Get current bucket handle // Get current bucket handle
b := boltTargetBucket(tx, bucketPath) b := boltTargetBucket(tx, bucketPath)
return kvstore_ApplyChanges(f, b.Put, b.Delete) return ApplyChanges_binColumn(f, b.Put, b.Delete)
}) })
} }

156
db_bunt.go Normal file
View File

@@ -0,0 +1,156 @@
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 ApplyChanges_binColumn 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 ApplyChanges_binColumn(
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 BuntDBLocation struct {
Type autoconfig.OneOf
Disk *struct {
File autoconfig.ExistingFile
} `json:",omitempty"`
Memory *struct{} `json:",omitempty"`
}
type buntDBAdvancedOptions struct {
buntdb.Config
}
func (bao *buntDBAdvancedOptions) Reset() {
// buntdb does not expose the default configuration, it's only set in the
// DB .Open() constructor
// Extract the defaults out from a temporary `:memory:` database
if memDb, err := buntdb.Open(`:memory:`); err == nil {
defer memDb.Close()
_ = memDb.ReadConfig(&bao.Config)
}
}
type buntDBConnection struct {
BuntDBLocation
AdvancedOptions *buntDBAdvancedOptions `json:",omitempty"`
}
var _ DBConnector = &buntDBConnection{} // interface assertion
func (ldc *buntDBConnection) String() string {
if ldc.Disk != nil {
return filepath.Base(string(ldc.Disk.File))
} else {
return `:memory:`
}
}
func (ldc *buntDBConnection) Connect(ctx context.Context) (loadedDatabase, 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
}
if ldc.AdvancedOptions != nil {
err = db.SetConfig(ldc.AdvancedOptions.Config)
if err != nil {
_ = db.Close()
return nil, err
}
}
return &buntLdb{db: db}, 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
@@ -95,23 +99,29 @@ type debconfConnection struct {
Database autoconfig.ExistingFile `yfilter:"Debconf database (*.dat);;All files (*)"` Database autoconfig.ExistingFile `yfilter:"Debconf database (*.dat);;All files (*)"`
} }
var _ DBConnector = &debconfConnection{} // interface assertion
func (dc *debconfConnection) Reset() { func (dc *debconfConnection) Reset() {
if runtime.GOOS == "linux" { if runtime.GOOS == "linux" {
dc.Database = "/var/cache/debconf/config.dat" // Prefill default path dc.Database = "/var/cache/debconf/config.dat" // Prefill default path
} }
} }
func (dc *debconfConnection) Connect(ctx context.Context) (loadedDatabase, string, error) { func (dc *debconfConnection) String() string {
return filepath.Base(string(dc.Database))
}
func (dc *debconfConnection) Connect(ctx context.Context) (loadedDatabase, error) {
fh, err := os.OpenFile(string(dc.Database), os.O_RDONLY, 0400) fh, err := os.OpenFile(string(dc.Database), os.O_RDONLY, 0400)
if err != nil { if err != nil {
return nil, "", err return nil, err
} }
defer fh.Close() defer fh.Close()
db, err := debconf.Parse(fh) db, err := debconf.Parse(fh)
if err != nil { if err != nil {
return nil, "", err return nil, err
} }
return &debconfLoadedDatabase{db: db}, filepath.Base(string(dc.Database)), nil return &debconfLoadedDatabase{db: db}, nil
} }

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)
@@ -51,11 +51,17 @@ var _ loadedDatabase = &evLdb{} // interface assertion
type evLdbConnection struct{} type evLdbConnection struct{}
func (dc *evLdbConnection) Connect(ctx context.Context) (loadedDatabase, string, error) { var _ DBConnector = &evLdbConnection{} // interface assertion
func (dc *evLdbConnection) String() string {
return APPNAME
}
func (dc *evLdbConnection) Connect(ctx context.Context) (loadedDatabase, error) {
mods, ok := debug.ReadBuildInfo() mods, ok := debug.ReadBuildInfo()
if !ok { if !ok {
return nil, "", errors.New("Missing build info") return nil, errors.New("Missing build info")
} }
return &evLdb{mods: mods}, APPNAME, nil return &evLdb{mods: mods}, nil
} }

319
db_etcd.go Normal file
View File

@@ -0,0 +1,319 @@
package main
import (
"context"
"fmt"
"strings"
"time"
"github.com/mappu/autoconfig"
qt "github.com/mappu/miqt/qt6"
etcd2 "go.etcd.io/etcd/client/v2"
etcd3 "go.etcd.io/etcd/client/v3"
)
///////////////////////////
// Client v3
///////////////////////////
type etcd3Ldb struct {
cl *etcd3.Client
}
func (ld *etcd3Ldb) DriverName() string {
return "etcd v3"
}
func (ld *etcd3Ldb) Properties(bucketPath []string) (string, error) {
return "", nil // No properties
}
func (ld *etcd3Ldb) RenderForNav(f *tableState, bucketPath []string) error {
// Load data
f.SetupColumns([]TableColumn{&binColumn{}, &binColumn{}}, []string{"Key", "Value"})
ctx := context.TODO()
resp, err := ld.cl.Get(ctx, "", etcd3.WithPrefix())
if err != nil {
return err
}
for _, kv := range resp.Kvs {
// Strange - reading data, you get it as a []byte, but setting it, you use strings
f.AddRow_PK_Data(kv.Key, kv.Key, kv.Value)
}
f.Ready()
return nil
}
func (n *etcd3Ldb) ApplyChanges(f *tableState, bucketPath []string) error {
ctx := context.TODO()
// TODO use a transaction
put := func(k, v []byte) error {
_, err := n.cl.Put(ctx, string(k), string(v))
return err
}
del := func(k []byte) error {
_, err := n.cl.Delete(ctx, string(k))
return err
}
return ApplyChanges_binColumn(f, put, del)
}
func (ld *etcd3Ldb) NavChildren(bucketPath []string) ([]string, error) {
// TODO: auth, members, ...
return []string{}, nil // No children
}
func (ld *etcd3Ldb) NavContext(bucketPath []string) ([]contextAction, error) {
return nil, nil // no actions
}
func (ld *etcd3Ldb) Close() {
_ = ld.cl.Close()
}
var _ loadedDatabase = &etcd3Ldb{} // interface assertion
var _ editableLoadedDatabase = &etcd3Ldb{} // interface assertion
///////////////////////////
// Client v2
///////////////////////////
type etcd2Ldb struct {
cl etcd2.Client // interface, by value
}
func (ld *etcd2Ldb) DriverName() string {
return "etcd v2"
}
func (ld *etcd2Ldb) Properties(bucketPath []string) (string, error) {
return "", nil // No properties
}
// etcd2_dirPath always has a leading + trailing `/` character.
func etcd2_dirPath(bucketPath []string) string {
if len(bucketPath) == 0 {
return `/`
}
return `/` + strings.Join(bucketPath, `/`) + `/`
}
func (ld *etcd2Ldb) RenderForNav(f *tableState, bucketPath []string) error {
// Load data
// Etcd v2 always uses string types, not []byte types
// However we use []byte columns anyway to get access to ApplyChanges_binColumn
f.SetupColumns([]TableColumn{&binColumn{}, &binColumn{}}, []string{"Key", "Value"})
// In etcd v2, the bucketPath should always turn into something with a final /
dirPath := etcd2_dirPath(bucketPath)
ctx := context.TODO()
resp, err := etcd2.NewKeysAPI(ld.cl).Get(ctx, dirPath, nil) // TODO any way to request only non-directories here?
if err != nil {
return err
}
if !resp.Node.Dir {
return fmt.Errorf("Path %q expected directory, got data", dirPath)
}
for _, child := range resp.Node.Nodes {
if child.Dir {
continue // Only looking for data here, not directories
}
if !strings.HasPrefix(child.Key, dirPath) {
return fmt.Errorf("Requested contents of %q but entry %q is missing expected prefix", dirPath, child.Key)
}
realKey := child.Key[len(dirPath):]
f.AddRow_PK_Data([]byte(realKey), []byte(realKey), []byte(child.Value))
}
f.Ready()
return nil
}
func (n *etcd2Ldb) ApplyChanges(f *tableState, bucketPath []string) error {
ctx := context.TODO()
ka := etcd2.NewKeysAPI(n.cl)
dirPath := etcd2_dirPath(bucketPath) // Has trailing slash
// TODO use a transaction
put := func(k, v []byte) error {
_, err := ka.Set(ctx, dirPath+string(k), string(v), nil)
return err
}
del := func(k []byte) error {
_, err := ka.Delete(ctx, dirPath+string(k), nil)
return err
}
return ApplyChanges_binColumn(f, put, del)
}
func (ld *etcd2Ldb) NavChildren(bucketPath []string) ([]string, error) {
// In etcd v2, the bucketPath should always turn into something with a final /
dirPath := etcd2_dirPath(bucketPath) // Has trailing slash
ctx := context.TODO()
resp, err := etcd2.NewKeysAPI(ld.cl).Get(ctx, dirPath, nil) // TODO any way to request only directories here?
if err != nil {
return nil, err
}
if !resp.Node.Dir {
return nil, fmt.Errorf("Path %q expected directory, got data", dirPath)
}
var ret []string
for _, child := range resp.Node.Nodes {
if !child.Dir {
continue // Only looking for data here, not directories
}
if !strings.HasPrefix(child.Key, dirPath) {
return nil, fmt.Errorf("Requested contents of %q but entry %q is missing expected prefix", dirPath, child.Key)
}
realKey := child.Key[len(dirPath):]
ret = append(ret, realKey)
}
return ret, nil
}
func (ld *etcd2Ldb) createDirectory(sender *qt.QTreeWidgetItem, bucketPath []string) error {
dirName := qt.QInputDialog_GetText(sender.TreeWidget().QWidget, APPNAME, "Enter a name for the new directory:")
if dirName == "" {
return nil // cancel
}
ctx := context.TODO()
ka := etcd2.NewKeysAPI(ld.cl)
newDirKey := etcd2_dirPath(bucketPath) + `/` + dirName
_, err := ka.Set(ctx, newDirKey, "", &etcd2.SetOptions{Dir: true})
return err
// TODO move the UI to select the newly created bucket
}
func (ld *etcd2Ldb) deleteDirectory(sender *qt.QTreeWidgetItem, bucketPath []string) error {
dirKey := etcd2_dirPath(bucketPath)
if !vcl_confirm_dialog(sender.TreeWidget().QWidget, fmt.Sprintf("Are you sure you want to delete the bucket %q and all its contents?", dirKey)) {
return nil // cancelled
}
ctx := context.TODO()
ka := etcd2.NewKeysAPI(ld.cl)
_, err := ka.Delete(ctx, dirKey, &etcd2.DeleteOptions{Dir: true, Recursive: true})
return err
// TODO The app tries to refresh the non-existent bucket which results in an error
// Need a way to move the current selection up to the parent level
}
func (ld *etcd2Ldb) NavContext(bucketPath []string) ([]contextAction, error) {
ret := []contextAction{
contextAction{"Create directory...", ld.createDirectory},
}
if len(bucketPath) > 0 {
ret = append(ret, contextAction{"Delete directory...", ld.deleteDirectory})
}
return ret, nil
}
func (ld *etcd2Ldb) Close() {
// v2 has nothing to close in the client
}
var _ loadedDatabase = &etcd2Ldb{} // interface assertion
var _ editableLoadedDatabase = &etcd2Ldb{} // interface assertion
///////////////////////////
// Connection
///////////////////////////
type etcdConn struct {
Endpoints []string
Username string
Password autoconfig.Password
H1 autoconfig.Header `ylabel:"Protocol version:" json:",omitempty"`
Protocol struct {
Type autoconfig.OneOf
V3 *struct{} `ylabel:"v3" json:",omitempty"`
V2 *struct{} `ylabel:"v2" json:",omitempty"`
}
}
var _ DBConnector = &etcdConn{} // interface assertion
func (c *etcdConn) String() string {
if len(c.Endpoints) == 1 {
return c.Endpoints[0]
} else if len(c.Endpoints) > 1 {
return fmt.Sprintf("%d endpoints", c.Endpoints)
} else {
return ""
}
}
func (c *etcdConn) Connect(ctx context.Context) (loadedDatabase, error) {
if c.Protocol.V3 != nil {
cfg := etcd3.Config{
Endpoints: c.Endpoints,
Username: c.Username,
Password: string(c.Password),
DialTimeout: 5 * time.Second,
// TODO SSH tunnel
}
cl, err := etcd3.New(cfg)
if err != nil {
return nil, err
}
return &etcd3Ldb{cl: cl}, nil
} else if c.Protocol.V2 != nil {
cfg := etcd2.Config{
Endpoints: c.Endpoints,
Username: c.Username,
Password: string(c.Password),
// TODO SSH tunnel
}
cl, err := etcd2.New(cfg)
if err != nil {
return nil, err
}
return &etcd2Ldb{cl: cl}, nil
} else {
return nil, fmt.Errorf("Unknown protocol version %q", c.Protocol.Type)
}
}

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()
@@ -54,7 +54,7 @@ func (n *leveldbLoadedDatabase) ApplyChanges(f *tableState, bucketPath []string)
return fmt.Errorf("OpenTransaction: %w", err) return fmt.Errorf("OpenTransaction: %w", err)
} }
err = kvstore_ApplyChanges( err = ApplyChanges_binColumn(
f, f,
func(k, v []byte) error { return txn.Put(k, v, nil) }, func(k, v []byte) error { return txn.Put(k, v, nil) },
func(k []byte) error { return txn.Delete(k, nil) }, func(k []byte) error { return txn.Delete(k, nil) },
@@ -89,14 +89,24 @@ type leveldbConnection struct {
Readonly bool Readonly bool
} }
func (pdc *leveldbConnection) Connect(ctx context.Context) (loadedDatabase, string, error) { var _ DBConnector = &leveldbConnection{} // interface assertion
func (pdc *leveldbConnection) String() string {
ret := filepath.Base(string(pdc.Directory))
if pdc.Readonly {
ret += " (read-only)"
}
return ret
}
func (pdc *leveldbConnection) Connect(ctx context.Context) (loadedDatabase, error) {
var o opt.Options var o opt.Options
o.ReadOnly = pdc.Readonly o.ReadOnly = pdc.Readonly
db, err := leveldb.OpenFile(string(pdc.Directory), &o) db, err := leveldb.OpenFile(string(pdc.Directory), &o)
if err != nil { if err != nil {
return nil, "", err return nil, err
} }
return &leveldbLoadedDatabase{db: db}, filepath.Base(string(pdc.Directory)), nil return &leveldbLoadedDatabase{db: db}, nil
} }

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
@@ -107,7 +107,7 @@ func (ld *lmdbDatabase) ApplyChanges(f *tableState, bucketPath []string) error {
return err return err
} }
return kvstore_ApplyChanges( return ApplyChanges_binColumn(
f, f,
func(k, v []byte) error { return txn.Put(dbi, k, v, 0) }, func(k, v []byte) error { return txn.Put(dbi, k, v, 0) },
func(k []byte) error { return txn.Del(dbi, k, nil) }, func(k []byte) error { return txn.Del(dbi, k, nil) },
@@ -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
@@ -249,11 +249,22 @@ type lmdbConnection struct {
Readonly bool Readonly bool
} }
func (pdc *lmdbConnection) Connect(ctx context.Context) (loadedDatabase, string, error) { var _ DBConnector = &lmdbConnection{} // interface assertion
func (pdc *lmdbConnection) String() string {
if pdc.Storage.Directory != nil {
return filepath.Base(string(*pdc.Storage.Directory))
} else if pdc.Storage.File != nil {
return filepath.Base(string(pdc.Storage.File.Path))
}
return "" // unreachable
}
func (pdc *lmdbConnection) Connect(ctx context.Context) (loadedDatabase, error) {
env, err := lmdb.NewEnv() env, err := lmdb.NewEnv()
if err != nil { if err != nil {
return nil, "", err return nil, err
} }
var openPath string var openPath string
@@ -277,8 +288,8 @@ func (pdc *lmdbConnection) Connect(ctx context.Context) (loadedDatabase, string,
err = env.Open(openPath, flags, 0644) err = env.Open(openPath, flags, 0644)
if err != nil { if err != nil {
_ = env.Close() _ = env.Close()
return nil, "", err return nil, err
} }
return &lmdbDatabase{db: env, isMulti: isMulti}, filepath.Base(openPath), nil return &lmdbDatabase{db: env, isMulti: isMulti}, nil
} }

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 {
@@ -45,7 +45,7 @@ func (ld *lotusLdb) RenderForNav(f *tableState, bucketPath []string) error {
func (n *lotusLdb) ApplyChanges(f *tableState, bucketPath []string) error { func (n *lotusLdb) ApplyChanges(f *tableState, bucketPath []string) error {
txn := n.db.NewBatch(lotusdb.DefaultBatchOptions) txn := n.db.NewBatch(lotusdb.DefaultBatchOptions)
err := kvstore_ApplyChanges(f, txn.Put, txn.Delete) err := ApplyChanges_binColumn(f, txn.Put, txn.Delete)
if err != nil { if err != nil {
return err return err
} }
@@ -74,15 +74,21 @@ type lotusDBConnection struct {
Directory autoconfig.ExistingDirectory Directory autoconfig.ExistingDirectory
} }
func (ldc *lotusDBConnection) Connect(ctx context.Context) (loadedDatabase, string, error) { var _ DBConnector = &lotusDBConnection{} // interface assertion
func (ldc *lotusDBConnection) String() string {
return filepath.Base(string(ldc.Directory))
}
func (ldc *lotusDBConnection) Connect(ctx context.Context) (loadedDatabase, error) {
opts := lotusdb.DefaultOptions // copy opts := lotusdb.DefaultOptions // copy
opts.DirPath = string(ldc.Directory) opts.DirPath = string(ldc.Directory)
db, err := lotusdb.Open(opts) db, err := lotusdb.Open(opts)
if err != nil { if err != nil {
return nil, "", err return nil, err
} }
return &lotusLdb{db: db}, filepath.Base(string(ldc.Directory)), nil return &lotusLdb{db: db}, 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,18 +287,24 @@ 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
} }
var _ DBConnector = &mongoConnection{} // interface assertion
func (moc *mongoConnection) Reset() { func (moc *mongoConnection) Reset() {
moc.Conn.Mode = "Connection_String" moc.Conn.Mode = "Connection_String"
moc.Conn.Connection_String = address_of("mongodb://localhost:27017") moc.Conn.Connection_String = address_of("mongodb://localhost:27017")
} }
func (moc *mongoConnection) Connect(ctx context.Context) (loadedDatabase, string, error) { func (moc *mongoConnection) String() string {
return "MongoDB" // TODO could be improved
}
func (moc *mongoConnection) Connect(ctx context.Context) (loadedDatabase, error) {
ctx, _ = context.WithTimeout(ctx, 5*time.Second) // shadow parent ctx ctx, _ = context.WithTimeout(ctx, 5*time.Second) // shadow parent ctx
@@ -312,7 +318,7 @@ func (moc *mongoConnection) Connect(ctx context.Context) (loadedDatabase, string
if moc.SSH_Tunnel != nil { if moc.SSH_Tunnel != nil {
sshc, err := moc.SSH_Tunnel.Open(ctx) sshc, err := moc.SSH_Tunnel.Open(ctx)
if err != nil { if err != nil {
return nil, "", err return nil, err
} }
opts.Dialer = sshc // interface implements DialContext() opts.Dialer = sshc // interface implements DialContext()
@@ -326,7 +332,7 @@ func (moc *mongoConnection) Connect(ctx context.Context) (loadedDatabase, string
client, err := mongo.Connect(opts) client, err := mongo.Connect(opts)
if err != nil { if err != nil {
return nil, "", err return nil, err
} }
ret.client = client ret.client = client
@@ -335,14 +341,14 @@ func (moc *mongoConnection) Connect(ctx context.Context) (loadedDatabase, string
err = client.Ping(ctx, readpref.Primary()) err = client.Ping(ctx, readpref.Primary())
if err != nil { if err != nil {
return nil, "", err return nil, err
} }
// We should be able to ListDatabases - there may be an authentication error // We should be able to ListDatabases - there may be an authentication error
_, err = client.ListDatabaseNames(ctx, bson.D{}) _, err = client.ListDatabaseNames(ctx, bson.D{})
if err != nil { if err != nil {
return nil, "", err return nil, err
} }
return &ret, "MongoDB", nil return &ret, nil
} }

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 {
@@ -57,7 +57,7 @@ func (n *pebbleLoadedDatabase) ApplyChanges(f *tableState, bucketPath []string)
txn := n.db.NewBatch() txn := n.db.NewBatch()
err := kvstore_ApplyChanges( err := ApplyChanges_binColumn(
f, f,
func(k, v []byte) error { return txn.Set(k, v, nil) }, func(k, v []byte) error { return txn.Set(k, v, nil) },
func(k []byte) error { return txn.Delete(k, nil) }, func(k []byte) error { return txn.Delete(k, nil) },
@@ -91,11 +91,21 @@ 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) { var _ DBConnector = &pebbleConnection{} // interface assertion
func (pdc *pebbleConnection) String() string {
if pdc.Disk != nil {
return filepath.Base(string(pdc.Disk.Directory))
} else {
return `:memory:` // SQLite-style naming
}
}
func (pdc *pebbleConnection) Connect(ctx context.Context) (loadedDatabase, error) {
opts := (&pebble.Options{}).EnsureDefaults() opts := (&pebble.Options{}).EnsureDefaults()
if pdc.Disk != nil { if pdc.Disk != nil {
@@ -103,18 +113,18 @@ func (pdc *pebbleConnection) Connect(ctx context.Context) (loadedDatabase, strin
db, err := pebble.Open(string(pdc.Disk.Directory), opts) db, err := pebble.Open(string(pdc.Disk.Directory), opts)
if err != nil { if err != nil {
return nil, "", err return nil, err
} }
return &pebbleLoadedDatabase{db: db}, filepath.Base(string(pdc.Disk.Directory)), nil return &pebbleLoadedDatabase{db: db}, nil
} else { } else {
// Memory != nil // Memory != nil
db, err := pebble.Open("", &pebble.Options{FS: vfs.NewMem()}) db, err := pebble.Open("", &pebble.Options{FS: vfs.NewMem()})
if err != nil { if err != nil {
return nil, "", err return nil, err
} }
return &pebbleLoadedDatabase{db: db}, `:memory:`, nil // SQLite-style naming return &pebbleLoadedDatabase{db: db}, nil
} }
} }

101
db_pogreb.go Normal file
View File

@@ -0,0 +1,101 @@
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 ApplyChanges_binColumn(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
}
var _ DBConnector = &pogrebConn{} // interface assertion
func (c *pogrebConn) String() string {
return filepath.Base(string(c.Directory))
}
func (c *pogrebConn) Connect(ctx context.Context) (loadedDatabase, error) {
db, err := pogreb.Open(string(c.Directory), nil)
if err != nil {
return nil, err
}
return &pogrebLdb{db: db}, 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"
@@ -19,11 +19,17 @@ type redisConnectionOptions struct {
SSH_Tunnel *SSHTunnel SSH_Tunnel *SSHTunnel
} }
var _ DBConnector = &redisConnectionOptions{} // interface assertion
func (config *redisConnectionOptions) String() string {
return config.Address.Address
}
func (config *redisConnectionOptions) Reset() { func (config *redisConnectionOptions) Reset() {
config.Address.Port = 6379 config.Address.Port = 6379
} }
func (config *redisConnectionOptions) Connect(ctx context.Context) (loadedDatabase, string, error) { func (config *redisConnectionOptions) Connect(ctx context.Context) (loadedDatabase, error) {
ld := &redisLoadedDatabase{ ld := &redisLoadedDatabase{
currentDb: 0, currentDb: 0,
@@ -40,7 +46,7 @@ func (config *redisConnectionOptions) Connect(ctx context.Context) (loadedDataba
if config.SSH_Tunnel != nil { if config.SSH_Tunnel != nil {
sshc, err := config.SSH_Tunnel.Open(ctx) sshc, err := config.SSH_Tunnel.Open(ctx)
if err != nil { if err != nil {
return nil, "", err return nil, err
} }
// When redis wants to open a tcp conn, don't dial directly, dial via the SSH tunnel // When redis wants to open a tcp conn, don't dial directly, dial via the SSH tunnel
@@ -62,7 +68,7 @@ func (config *redisConnectionOptions) Connect(ctx context.Context) (loadedDataba
// Make an INFO request (mandatory) // Make an INFO request (mandatory)
info, err := ld.db.InfoMap(ctx).Result() info, err := ld.db.InfoMap(ctx).Result()
if err != nil { if err != nil {
return nil, "", fmt.Errorf("During INFO: %w", err) return nil, fmt.Errorf("During INFO: %w", err)
} }
if serverInfo, ok := info["Server"]; ok { if serverInfo, ok := info["Server"]; ok {
@@ -78,15 +84,13 @@ func (config *redisConnectionOptions) Connect(ctx context.Context) (loadedDataba
// Got a result. Must parse it // Got a result. Must parse it
m, err := strconv.ParseInt(maxDatabases["databases"], 10, 64) m, err := strconv.ParseInt(maxDatabases["databases"], 10, 64)
if err != nil { if err != nil {
return nil, "", fmt.Errorf("During CONFIG GET databases: %v", err) return nil, fmt.Errorf("During CONFIG GET databases: %v", err)
} }
ld.maxDb = int(m) ld.maxDb = int(m)
} }
displayName := config.Address.Address return ld, nil
return ld, displayName, nil
} }
type redisLoadedDatabase struct { type redisLoadedDatabase struct {
@@ -142,7 +146,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 +244,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)
@@ -37,7 +37,7 @@ func (ld *roseLdb) RenderForNav(f *tableState, bucketPath []string) error {
} }
func (n *roseLdb) ApplyChanges(f *tableState, bucketPath []string) error { func (n *roseLdb) ApplyChanges(f *tableState, bucketPath []string) error {
return kvstore_ApplyChanges(f, n.db.Put, n.db.Delete) return ApplyChanges_binColumn(f, n.db.Put, n.db.Delete)
} }
func (ld *roseLdb) NavChildren(bucketPath []string) ([]string, error) { func (ld *roseLdb) NavChildren(bucketPath []string) ([]string, error) {
@@ -61,14 +61,20 @@ type roseDBConn struct {
Directory autoconfig.ExistingDirectory Directory autoconfig.ExistingDirectory
} }
func (c *roseDBConn) Connect(ctx context.Context) (loadedDatabase, string, error) { var _ DBConnector = &roseDBConn{} // interface assertion
func (c *roseDBConn) String() string {
return filepath.Base(string(c.Directory))
}
func (c *roseDBConn) Connect(ctx context.Context) (loadedDatabase, error) {
options := rosedb.DefaultOptions // copy options := rosedb.DefaultOptions // copy
options.DirPath = string(c.Directory) options.DirPath = string(c.Directory)
db, err := rosedb.Open(options) db, err := rosedb.Open(options)
if err != nil { if err != nil {
return nil, "", err return nil, err
} }
return &roseLdb{db: db}, filepath.Base(string(c.Directory)), nil return &roseLdb{db: db}, nil
} }

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"},
) )
@@ -173,17 +173,23 @@ var _ loadedDatabase = &secretServiceDb{} // interface assertion
type secretServiceConnection struct { type secretServiceConnection struct {
} }
func (ssc *secretServiceConnection) Connect(ctx context.Context) (loadedDatabase, string, error) { var _ DBConnector = &secretServiceConnection{} // interface assertion
func (ssc *secretServiceConnection) String() string {
return "dbus://SessionBus/org.freedesktop.secrets"
}
func (ssc *secretServiceConnection) Connect(ctx context.Context) (loadedDatabase, error) {
svc, err := secretservice.NewSecretService() svc, err := secretservice.NewSecretService()
if err != nil { if err != nil {
return nil, "", err return nil, err
} }
session, err := svc.OpenSession() session, err := svc.OpenSession()
if err != nil { if err != nil {
return nil, "", err return nil, err
} }
return &secretServiceDb{svc: svc, session: session}, "dbus://SessionBus/org.freedesktop.secrets", nil return &secretServiceDb{svc: svc, session: session}, nil
} }

View File

@@ -14,6 +14,12 @@ type secretServiceConnection struct {
H1 autoconfig.Header `ylabel:"Not supported on this operating system"` H1 autoconfig.Header `ylabel:"Not supported on this operating system"`
} }
func (ssc *secretServiceConnection) Connect(ctx context.Context) (loadedDatabase, string, error) { var _ DBConnector = &secretServiceConnection{} // interface assertion
func (ssc *secretServiceConnection) String() string {
return ""
}
func (ssc *secretServiceConnection) Connect(ctx context.Context) (loadedDatabase, error) {
return nil, "", errors.New("Not supported on this operating system") return nil, "", errors.New("Not supported on this operating system")
} }

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,110 @@ 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 && len(cNames) > 0 {
// Real SQLite driver: gives us back names even if 0 rows
// sqliteclidriver: Gives back no names if there were 0 rows
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 +238,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 +251,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 +275,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 +305,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 +317,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 +353,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,11 +446,28 @@ 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) { var _ DBConnector = &sqliteConnection{} // interface assertion
func (sc *sqliteConnection) String() string {
if sc.Disk != nil {
return filepath.Base(string(sc.Disk.Database))
} else if sc.Memory != nil {
return `:memory:`
} else if sc.SSH != nil {
return filepath.Base(string(sc.SSH.Database)) + " (SSH)"
}
return "" // unreachable
}
func (sc *sqliteConnection) Connect(ctx context.Context) (loadedDatabase, error) {
if sc.Disk != nil { if sc.Disk != nil {
driver := "sqlite3" driver := "sqlite3"
if sc.Disk.CliDriver { if sc.Disk.CliDriver {
@@ -419,17 +476,34 @@ func (sc *sqliteConnection) Connect(ctx context.Context) (loadedDatabase, string
db, err := sql.Open(driver, string(sc.Disk.Database)) db, err := sql.Open(driver, string(sc.Disk.Database))
if err != nil { if err != nil {
return nil, "", err return nil, err
} }
return &sqliteLoadedDatabase{db: db}, filepath.Base(string(sc.Disk.Database)), nil return &sqliteLoadedDatabase{db: db}, 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}, 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}, 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,14 @@ 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"`
}
var _ DBConnector = &sshAgentConn{} // interface assertion
func (c *sshAgentConn) String() string {
return "SSH Agent" // TODO could be improved
} }
func (c *sshAgentConn) Reset() { func (c *sshAgentConn) Reset() {
@@ -169,11 +175,11 @@ func (c *sshAgentConn) getAgent() (agent.ExtendedAgent, error) {
} }
} }
func (c *sshAgentConn) Connect(ctx context.Context) (loadedDatabase, string, error) { func (c *sshAgentConn) Connect(ctx context.Context) (loadedDatabase, error) {
agent, err := c.getAgent() agent, err := c.getAgent()
if err != nil { if err != nil {
return nil, "", err return nil, err
} }
return &sshAgentLdb{conn: agent}, "SSH Agent", nil return &sshAgentLdb{conn: agent}, nil
} }

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
@@ -62,7 +62,7 @@ func (ld *starskeyLdb) RenderForNav(f *tableState, bucketPath []string) error {
} }
func (n *starskeyLdb) ApplyChanges(f *tableState, bucketPath []string) error { func (n *starskeyLdb) ApplyChanges(f *tableState, bucketPath []string) error {
return kvstore_ApplyChanges(f, n.db.Put, n.db.Delete) return ApplyChanges_binColumn(f, n.db.Put, n.db.Delete)
} }
func (ld *starskeyLdb) NavChildren(bucketPath []string) ([]string, error) { func (ld *starskeyLdb) NavChildren(bucketPath []string) ([]string, error) {
@@ -87,7 +87,13 @@ type starskeyConnection struct {
Compression autoconfig.EnumList `yenum:"No compression;;Snappy;;S2"` Compression autoconfig.EnumList `yenum:"No compression;;Snappy;;S2"`
} }
func (pdc *starskeyConnection) Connect(ctx context.Context) (loadedDatabase, string, error) { var _ DBConnector = &starskeyConnection{} // interface assertion
func (pdc *starskeyConnection) String() string {
return filepath.Base(string(pdc.Directory))
}
func (pdc *starskeyConnection) Connect(ctx context.Context) (loadedDatabase, error) {
cfg := starskey.Config{ cfg := starskey.Config{
Permission: 0755, Permission: 0755,
@@ -108,8 +114,8 @@ func (pdc *starskeyConnection) Connect(ctx context.Context) (loadedDatabase, str
db, err := starskey.Open(&cfg) db, err := starskey.Open(&cfg)
if err != nil { if err != nil {
return nil, "", err return nil, err
} }
return &starskeyLdb{db: db}, filepath.Base(string(pdc.Directory)), nil return &starskeyLdb{db: db}, nil
} }

254
db_voiddb_linux.go Normal file
View File

@@ -0,0 +1,254 @@
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 ApplyChanges_binColumn(
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"`
}
var _ DBConnector = &voidDBConn{} // interface assertion
func (c *voidDBConn) String() string {
return filepath.Base(string(c.Path))
}
func (c *voidDBConn) Connect(ctx context.Context) (loadedDatabase, 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}, nil
}

25
db_voiddb_other.go Normal file
View File

@@ -0,0 +1,25 @@
//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"`
}
var _ DBConnector = &voidDBConn{} // interface assertion
func (ssc *voidDBConn) String() {
return ""
}
func (ssc *voidDBConn) Connect(ctx context.Context) (loadedDatabase, 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,9 +28,11 @@
<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>
<file>assets/vendor_etcd.png</file>
<file>assets/vendor_freedesktop.png</file> <file>assets/vendor_freedesktop.png</file>
<file>assets/vendor_github.png</file> <file>assets/vendor_github.png</file>
<file>assets/vendor_leveldb.png</file> <file>assets/vendor_leveldb.png</file>
@@ -35,11 +40,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.

32
go.mod
View File

@@ -1,17 +1,18 @@
module yvbolt module qbolt
go 1.24.0 go 1.24.0
toolchain go1.24.4 toolchain go1.24.4
require ( require (
github.com/akrylysov/pogreb v0.10.2
github.com/cockroachdb/pebble v1.1.5 github.com/cockroachdb/pebble v1.1.5
github.com/dgraph-io/badger/v4 v4.8.0 github.com/dgraph-io/badger/v4 v4.8.0
github.com/godbus/dbus/v5 v5.2.0 github.com/godbus/dbus/v5 v5.2.0
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.6.1-0.20260124043120-621a5fcf917e
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
@@ -19,8 +20,12 @@ require (
github.com/starskey-io/starskey v0.1.9 github.com/starskey-io/starskey v0.1.9
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/syndtr/goleveldb v1.0.0 github.com/syndtr/goleveldb v1.0.0
github.com/tidwall/buntdb v1.3.2
github.com/voidDB/voidDB v0.1.18
github.com/zalando/go-keyring v0.2.6 github.com/zalando/go-keyring v0.2.6
go.etcd.io/bbolt v1.4.3 go.etcd.io/bbolt v1.4.3
go.etcd.io/etcd/client/v2 v2.305.26
go.etcd.io/etcd/client/v3 v3.6.7
go.mills.io/bitcask/v2 v2.1.5 go.mills.io/bitcask/v2 v2.1.5
go.mongodb.org/mongo-driver/v2 v2.4.1 go.mongodb.org/mongo-driver/v2 v2.4.1
golang.org/x/crypto v0.46.0 golang.org/x/crypto v0.46.0
@@ -38,6 +43,8 @@ require (
github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 // indirect github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 // indirect
github.com/cockroachdb/redact v1.1.6 // indirect github.com/cockroachdb/redact v1.1.6 // indirect
github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb // indirect github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb // indirect
github.com/coreos/go-semver v0.3.1 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/danieljoos/wincred v1.2.3 // indirect github.com/danieljoos/wincred v1.2.3 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dgraph-io/ristretto/v2 v2.3.0 // indirect github.com/dgraph-io/ristretto/v2 v2.3.0 // indirect
@@ -48,16 +55,21 @@ require (
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/gofrs/flock v0.8.1 // indirect github.com/gofrs/flock v0.8.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v1.0.0 // indirect github.com/golang/snappy v1.0.0 // indirect
github.com/google/btree v1.1.2 // indirect github.com/google/btree v1.1.2 // indirect
github.com/google/flatbuffers v25.9.23+incompatible // indirect github.com/google/flatbuffers v25.9.23+incompatible // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
github.com/hashicorp/go-immutable-radix/v2 v2.0.0 // indirect github.com/hashicorp/go-immutable-radix/v2 v2.0.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.2 // indirect github.com/klauspost/compress v1.18.2 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kr/pretty v0.3.1 // indirect github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect github.com/kr/text v0.2.0 // indirect
github.com/mattetti/filebuffer v1.0.1 // indirect github.com/mattetti/filebuffer v1.0.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
@@ -71,23 +83,37 @@ 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/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/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
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
github.com/zeebo/xxh3 v1.0.2 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect
go.etcd.io/etcd/api/v3 v3.6.7 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.6.7 // indirect
go.mongodb.org/mongo-driver v1.17.6 // indirect go.mongodb.org/mongo-driver v1.17.6 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel v1.39.0 // indirect go.opentelemetry.io/otel v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 // indirect golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 // indirect
golang.org/x/net v0.48.0 // indirect golang.org/x/net v0.48.0 // indirect
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect golang.org/x/text v0.32.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260122232226-8e98ce8d340d // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d // indirect
google.golang.org/grpc v1.71.1 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

66
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=
@@ -28,6 +30,10 @@ github.com/cockroachdb/redact v1.1.6 h1:zXJBwDZ84xJNlHl1rMyCojqyIxv+7YUpQiJLQ7n4
github.com/cockroachdb/redact v1.1.6/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= github.com/cockroachdb/redact v1.1.6/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=
github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb h1:3bCgBvB8PbJVMX1ouCcSIxvsqKPYM7gs72o0zC76n9g= github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb h1:3bCgBvB8PbJVMX1ouCcSIxvsqKPYM7gs72o0zC76n9g=
github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ=
github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ= github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ=
github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs= github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs=
@@ -55,6 +61,7 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.2.0 h1:3WexO+U+yg9T70v9FdHr9kCxYlazaAXUhx2VMkbfax8= github.com/godbus/dbus/v5 v5.2.0 h1:3WexO+U+yg9T70v9FdHr9kCxYlazaAXUhx2VMkbfax8=
github.com/godbus/dbus/v5 v5.2.0/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= github.com/godbus/dbus/v5 v5.2.0/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
@@ -62,6 +69,8 @@ github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14j
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
@@ -71,6 +80,7 @@ github.com/google/flatbuffers v25.9.23+incompatible h1:rGZKv+wOb6QPzIdkM2KxhBZCD
github.com/google/flatbuffers v25.9.23+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/flatbuffers v25.9.23+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -78,6 +88,8 @@ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0= github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
github.com/hashicorp/go-immutable-radix/v2 v2.0.0 h1:nq9lQ5I71Heg2lRb2/+szuIWKY3Y73d8YKyXyN91WzU= github.com/hashicorp/go-immutable-radix/v2 v2.0.0 h1:nq9lQ5I71Heg2lRb2/+szuIWKY3Y73d8YKyXyN91WzU=
github.com/hashicorp/go-immutable-radix/v2 v2.0.0/go.mod h1:hgdqLXA4f6NIjRVisM1TJ9aOJVNRqKZj+xDGF6m7PBw= github.com/hashicorp/go-immutable-radix/v2 v2.0.0/go.mod h1:hgdqLXA4f6NIjRVisM1TJ9aOJVNRqKZj+xDGF6m7PBw=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
@@ -86,6 +98,8 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
@@ -104,12 +118,23 @@ 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/autoconfig v0.6.0 h1:pv6gFYC3eqnKzthaKzD/X+8tcCM+kiPDFZDSc/xn1b8=
github.com/mappu/autoconfig v0.6.0/go.mod h1:kca6kaYjqOwkZnY9z5YKo0oI/7Bxe2fMIHhYzXwGHEE=
github.com/mappu/autoconfig v0.6.1-0.20260124043120-621a5fcf917e h1:zVsg2l2o75SkbVOxwJXDTgT+dJ/88Wvyd263DEna4Vg=
github.com/mappu/autoconfig v0.6.1-0.20260124043120-621a5fcf917e/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=
github.com/mattetti/filebuffer v1.0.1/go.mod h1:YdMURNDOttIiruleeVr6f56OrMc+MydEnTcXwtkxNVs= github.com/mattetti/filebuffer v1.0.1/go.mod h1:YdMURNDOttIiruleeVr6f56OrMc+MydEnTcXwtkxNVs=
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -160,13 +185,33 @@ github.com/starskey-io/starskey v0.1.9/go.mod h1:qly4ec2C/4Y45jhpL+q4m+Uxzg3mjj0
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
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=
@@ -186,6 +231,14 @@ github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
go.etcd.io/etcd/api/v3 v3.6.7 h1:7BNJ2gQmc3DNM+9cRkv7KkGQDayElg8x3X+tFDYS+E0=
go.etcd.io/etcd/api/v3 v3.6.7/go.mod h1:xJ81TLj9hxrYYEDmXTeKURMeY3qEDN24hqe+q7KhbnI=
go.etcd.io/etcd/client/pkg/v3 v3.6.7 h1:vvzgyozz46q+TyeGBuFzVuI53/yd133CHceNb/AhBVs=
go.etcd.io/etcd/client/pkg/v3 v3.6.7/go.mod h1:2IVulJ3FZ/czIGl9T4lMF1uxzrhRahLqe+hSgy+Kh7Q=
go.etcd.io/etcd/client/v2 v2.305.26 h1:oReO+h1y3W/CJJa8axZ/3t9S6jg0I42tNx7AWKIvfCc=
go.etcd.io/etcd/client/v2 v2.305.26/go.mod h1:oni2jI2OMezwmakWDTlZRi6VANemxGx+KIuiQBuRRpQ=
go.etcd.io/etcd/client/v3 v3.6.7 h1:9WqA5RpIBtdMxAy1ukXLAdtg2pAxNqW5NUoO2wQrE6U=
go.etcd.io/etcd/client/v3 v3.6.7/go.mod h1:2XfROY56AXnUqGsvl+6k29wrwsSbEh1lAouQB1vHpeE=
go.mills.io/bitcask/v2 v2.1.5 h1:SKPa0TPasJJZ8rNbLDvV3+lRXvdyQ0mwBobm2RH7J7w= go.mills.io/bitcask/v2 v2.1.5 h1:SKPa0TPasJJZ8rNbLDvV3+lRXvdyQ0mwBobm2RH7J7w=
go.mills.io/bitcask/v2 v2.1.5/go.mod h1:ZQFykoTTCvMwy24lBstZhSRQuleYIB4EzWKSOgEv6+k= go.mills.io/bitcask/v2 v2.1.5/go.mod h1:ZQFykoTTCvMwy24lBstZhSRQuleYIB4EzWKSOgEv6+k=
go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss= go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss=
@@ -202,6 +255,10 @@ go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -263,8 +320,17 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
google.golang.org/genproto/googleapis/api v0.0.0-20260122232226-8e98ce8d340d h1:tUKoKfdZnSjTf5LW7xpG4c6SZ3Ozisn5eumcoTuMEN4=
google.golang.org/genproto/googleapis/api v0.0.0-20260122232226-8e98ce8d340d/go.mod h1:p3MLuOwURrGBRoEyFHBT3GjUwaCQVKeNqqWxlcISGdw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d h1:xXzuihhT3gL/ntduUZwHECzAn57E8dA6l8SOtYWdD8Q=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI=
google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

129
main.go
View File

@@ -4,22 +4,25 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"io"
"os" "os"
"strings" "strings"
"github.com/mappu/miqt/qt6/mainthread" "github.com/mappu/autoconfig"
qt "github.com/mappu/miqt/qt6" qt "github.com/mappu/miqt/qt6"
"github.com/mappu/miqt/qt6/mainthread"
) )
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 {
ui *MainWindowUi ui *MainWindowUi
currentDensity int
contentTbl *tableState contentTbl *tableState
resultsTbl *tableState resultsTbl *tableState
@@ -33,15 +36,23 @@ func newApp() *App {
a := &App{} a := &App{}
autoconfig.SetEnumStringOptions(`qstyle`, qt.QStyleFactory_Keys())
a.dbs_next = 0 a.dbs_next = 0
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)
@@ -49,6 +60,8 @@ func newApp() *App {
a.ui.actionE_xit.OnTriggered(a.OnMnuFileExitClick) a.ui.actionE_xit.OnTriggered(a.OnMnuFileExitClick)
a.ui.action_Appearance.OnTriggered(a.OnMnuAppearance)
a.ui.mnuExecute.OnTriggered(a.OnQueryExecute) a.ui.mnuExecute.OnTriggered(a.OnQueryExecute)
a.ui.mnuDriverVersions.OnTriggered(a.OnMenuHelpVersion) a.ui.mnuDriverVersions.OnTriggered(a.OnMenuHelpVersion)
@@ -96,8 +109,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)
@@ -113,8 +124,38 @@ func main() {
// no need to change anything here // no need to change anything here
qt.NewQApplication(os.Args) qt.NewQApplication(os.Args)
app := newApp() app := newApp()
if len(os.Args) > 1 {
switch os.Args[1] {
case "--config-export":
content, err := app.getConnectionManagerContentsJson()
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
fmt.Println(string(content))
os.Exit(0)
case "--config-import":
content, err := io.ReadAll(os.Stdin)
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
err = app.saveConnectionManagerContentsJson(content)
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
os.Exit(0)
case "--help":
fmt.Println("Usage: " + APPNAME + " [--config-export|--config-import]")
os.Exit(0)
}
}
app.ui.MainWindow.Show() app.ui.MainWindow.Show()
qt.QApplication_Exec() qt.QApplication_Exec()
@@ -124,6 +165,63 @@ func (f *App) OnMnuFileExitClick() {
f.ui.MainWindow.Close() f.ui.MainWindow.Close()
} }
type appearanceProps struct {
QtStyle autoconfig.EnumString `yenum:"qstyle"`
Density autoconfig.EnumList `yenum:"System Default;;Compact;;Padded"`
AlternatingRowColours bool
}
func (f *App) OnMnuAppearance() {
var opts appearanceProps
// Current style from QApplication_Style().Name() is lowercase, but the
// style list has title-case
// First-character-uppercase-only does not work for Qt6 "Adwaita-Dark" style
// Get the whole list and do a manual case-insensitive match
// Assume it's first-character-uppercase-only
currentStyleName := qt.QApplication_Style().Name()
for _, check := range qt.QStyleFactory_Keys() {
if strings.ToLower(check) == currentStyleName {
currentStyleName = check
break
}
}
opts.QtStyle = autoconfig.EnumString(currentStyleName)
opts.Density = autoconfig.EnumList(f.currentDensity)
opts.AlternatingRowColours = f.contentTbl.tbl.AlternatingRowColors()
autoconfig.OpenDialog(&opts, f.ui.MainWindow.QWidget, "Appearance...", func() {
qt.QApplication_SetStyleWithStyle(string(opts.QtStyle))
vhh := f.contentTbl.tbl.VerticalHeader()
vhh.ResetDefaultSectionSize()
f.contentTbl.tbl.SetStyleSheet("") // Default density
if opts.Density == 0 {
// nothing
} else if opts.Density == 1 {
vhh.SetDefaultSectionSize(0) // vhh.DefaultSectionSize() - 10)
// Default 30, seems to not go below 25
// Breeze: can only reach 29, Windows: can only reach 25, Fusion: can reach 21
} else if opts.Density == 2 {
vhh.SetDefaultSectionSize(vhh.DefaultSectionSize() + 5)
// Setting stylesheet: can only add additional horizontal padding
f.contentTbl.tbl.SetStyleSheet(`QTableView::item { padding: 6px; background: transparent; }`)
}
f.currentDensity = int(opts.Density)
// TODO also apply all customizations to data result pane
f.contentTbl.tbl.SetAlternatingRowColors(opts.AlternatingRowColours)
// TODO persist
})
}
func (f *App) OnMnuHelpHomepage() { func (f *App) OnMnuHelpHomepage() {
url := qt.NewQUrl3(HOMEPAGE_URL) url := qt.NewQUrl3(HOMEPAGE_URL)
ok := qt.QDesktopServices_OpenUrl(url) ok := qt.QDesktopServices_OpenUrl(url)
@@ -137,13 +235,13 @@ func (f *App) OnMnuHelpHomepage() {
func (f *App) OnMenuHelpVersion() { func (f *App) OnMenuHelpVersion() {
connector := evLdbConnection{} connector := evLdbConnection{}
ld, name, err := connector.Connect(context.Background()) ld, err := connector.Connect(context.Background())
if err != nil { if err != nil {
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, err.Error()) qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, err.Error())
return return
} }
f.addTopLevelDatabaseConnection(ld, name) f.addTopLevelDatabaseConnection(ld, connector.String(), "")
} }
func (f *App) OnNavContextPopup(pos *qt.QPoint) { func (f *App) OnNavContextPopup(pos *qt.QPoint) {
@@ -481,6 +579,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())
@@ -612,7 +713,7 @@ func (f *App) NavLoadChildren(item *qt.QTreeWidgetItem, bucketPath []string) err
return nil return nil
} }
func (f *App) addTopLevelDatabaseConnection(ld loadedDatabase, displayName string) { func (f *App) addTopLevelDatabaseConnection(ld loadedDatabase, displayName, tooltip string) {
nav := qt.NewQTreeWidgetItem() nav := qt.NewQTreeWidgetItem()
nav.SetText(0, displayName) nav.SetText(0, displayName)
@@ -632,7 +733,15 @@ func (f *App) addTopLevelDatabaseConnection(ld loadedDatabase, displayName strin
nav.SetData(0, LoadedDatabaseIdRole, qt.NewQVariant4(ldID)) nav.SetData(0, LoadedDatabaseIdRole, qt.NewQVariant4(ldID))
nav.SetData(0, BucketPathBSliceRole, qt.NewQVariant12(emptyData)) nav.SetData(0, BucketPathBSliceRole, qt.NewQVariant12(emptyData))
if tooltip != "" && tooltip != displayName {
nav.SetToolTip(0, tooltip)
}
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

@@ -33,8 +33,11 @@ type MainWindowUi struct {
menu_Help *qt.QMenu menu_Help *qt.QMenu
menu_Data *qt.QMenu menu_Data *qt.QMenu
menu_Tools *qt.QMenu menu_Tools *qt.QMenu
menu_View *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 +50,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
action_Appearance *qt.QAction
} }
// NewMainWindowUi creates all Qt widget classes for MainWindow. // NewMainWindowUi creates all Qt widget classes for MainWindow.
@@ -143,6 +147,13 @@ 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.action_Appearance = qt.NewQAction()
action_Appearance__objectName := qt.NewQAnyStringView3("action_Appearance")
ui.action_Appearance.SetObjectName(*action_Appearance__objectName)
action_Appearance__objectName.Delete() // setter copied value
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)
@@ -151,7 +162,7 @@ func NewMainWindowUi() *MainWindowUi {
gridLayout__objectName := qt.NewQAnyStringView3("gridLayout") gridLayout__objectName := qt.NewQAnyStringView3("gridLayout")
ui.gridLayout.SetObjectName(*gridLayout__objectName) ui.gridLayout.SetObjectName(*gridLayout__objectName)
gridLayout__objectName.Delete() // setter copied value gridLayout__objectName.Delete() // setter copied value
ui.gridLayout.SetContentsMargins(11, 11, 11, 11) ui.gridLayout.SetContentsMargins(0, 0, 0, 0)
ui.gridLayout.SetSpacing(6) ui.gridLayout.SetSpacing(6)
ui.splitter = qt.NewQSplitter(ui.centralwidget) ui.splitter = qt.NewQSplitter(ui.centralwidget)
splitter__objectName := qt.NewQAnyStringView3("splitter") splitter__objectName := qt.NewQAnyStringView3("splitter")
@@ -163,6 +174,7 @@ func NewMainWindowUi() *MainWindowUi {
Buckets__objectName := qt.NewQAnyStringView3("Buckets") Buckets__objectName := qt.NewQAnyStringView3("Buckets")
ui.Buckets.SetObjectName(*Buckets__objectName) ui.Buckets.SetObjectName(*Buckets__objectName)
Buckets__objectName.Delete() // setter copied value Buckets__objectName.Delete() // setter copied value
ui.Buckets.SetFrameShape(qt.QFrame__NoFrame)
ui.Buckets.SetUniformRowHeights(true) ui.Buckets.SetUniformRowHeights(true)
ui.Buckets.SetHeaderHidden(true) ui.Buckets.SetHeaderHidden(true)
/* miqt-uic: no handler for Buckets attribute 'headerVisible' */ /* miqt-uic: no handler for Buckets attribute 'headerVisible' */
@@ -198,9 +210,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() icon11 := qt.NewQIcon()
icon10.AddFile4(":/assets/chart_bar.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off) icon11.AddFile4(":/assets/chart_bar.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.tabWidget.AddTab2(ui.tabProperties, icon10, "") ui.tabWidget.AddTab2(ui.tabProperties, icon11, "")
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)
@@ -209,7 +221,7 @@ func NewMainWindowUi() *MainWindowUi {
verticalLayout__objectName := qt.NewQAnyStringView3("verticalLayout") verticalLayout__objectName := qt.NewQAnyStringView3("verticalLayout")
ui.verticalLayout.SetObjectName(*verticalLayout__objectName) ui.verticalLayout.SetObjectName(*verticalLayout__objectName)
verticalLayout__objectName.Delete() // setter copied value verticalLayout__objectName.Delete() // setter copied value
ui.verticalLayout.SetContentsMargins(11, 11, 11, 11) ui.verticalLayout.SetContentsMargins(2, 2, 2, 2)
ui.verticalLayout.SetSpacing(6) ui.verticalLayout.SetSpacing(6)
ui.contentBox = qt.NewQTableView(ui.tabData) ui.contentBox = qt.NewQTableView(ui.tabData)
contentBox__objectName := qt.NewQAnyStringView3("contentBox") contentBox__objectName := qt.NewQAnyStringView3("contentBox")
@@ -219,9 +231,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() icon12 := qt.NewQIcon()
icon11.AddFile4(":/assets/table.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off) icon12.AddFile4(":/assets/table.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.tabWidget.AddTab2(ui.tabData, icon11, "") ui.tabWidget.AddTab2(ui.tabData, icon12, "")
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)
@@ -230,7 +242,7 @@ func NewMainWindowUi() *MainWindowUi {
verticalLayout_2__objectName := qt.NewQAnyStringView3("verticalLayout_2") verticalLayout_2__objectName := qt.NewQAnyStringView3("verticalLayout_2")
ui.verticalLayout_2.SetObjectName(*verticalLayout_2__objectName) ui.verticalLayout_2.SetObjectName(*verticalLayout_2__objectName)
verticalLayout_2__objectName.Delete() // setter copied value verticalLayout_2__objectName.Delete() // setter copied value
ui.verticalLayout_2.SetContentsMargins(11, 11, 11, 11) ui.verticalLayout_2.SetContentsMargins(2, 2, 2, 2)
ui.verticalLayout_2.SetSpacing(6) ui.verticalLayout_2.SetSpacing(6)
ui.splitter_2 = qt.NewQSplitter(ui.tabQuery) ui.splitter_2 = qt.NewQSplitter(ui.tabQuery)
splitter_2__objectName := qt.NewQAnyStringView3("splitter_2") splitter_2__objectName := qt.NewQAnyStringView3("splitter_2")
@@ -253,9 +265,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() icon13 := qt.NewQIcon()
icon12.AddFile4(":/assets/lightning.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off) icon13.AddFile4(":/assets/lightning.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.tabWidget.AddTab2(ui.tabQuery, icon12, "") ui.tabWidget.AddTab2(ui.tabQuery, icon13, "")
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)
@@ -300,9 +312,15 @@ func NewMainWindowUi() *MainWindowUi {
ui.menu_Tools.SetObjectName(*menu_Tools__objectName) ui.menu_Tools.SetObjectName(*menu_Tools__objectName)
menu_Tools__objectName.Delete() // setter copied value menu_Tools__objectName.Delete() // setter copied value
ui.menu_Tools.QWidget.AddAction(ui.actionCreate_Bolt_database_from_zip) ui.menu_Tools.QWidget.AddAction(ui.actionCreate_Bolt_database_from_zip)
ui.menu_View = qt.NewQMenu(ui.menubar.QWidget)
menu_View__objectName := qt.NewQAnyStringView3("menu_View")
ui.menu_View.SetObjectName(*menu_View__objectName)
menu_View__objectName.Delete() // setter copied value
ui.menu_View.QWidget.AddAction(ui.action_Appearance)
ui.menubar.AddMenu(ui.menu_File) ui.menubar.AddMenu(ui.menu_File)
ui.menubar.AddMenu(ui.menu_Data) ui.menubar.AddMenu(ui.menu_Data)
ui.menubar.AddMenu(ui.menu_Query) ui.menubar.AddMenu(ui.menu_Query)
ui.menubar.AddMenu(ui.menu_View)
ui.menubar.AddMenu(ui.menu_Tools) ui.menubar.AddMenu(ui.menu_Tools)
ui.menubar.AddMenu(ui.menu_Help) ui.menubar.AddMenu(ui.menu_Help)
ui.MainWindow.SetMenuBar(ui.menubar) ui.MainWindow.SetMenuBar(ui.menubar)
@@ -311,23 +329,38 @@ 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.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 +371,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 +391,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.action_Appearance.SetText(qt.QMainWindow_Tr("&Appearance..."))
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 +400,8 @@ 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.menu_View.SetTitle(qt.QMenuBar_Tr("&View"))
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">
@@ -19,6 +19,18 @@
</property> </property>
<widget class="QWidget" name="centralwidget"> <widget class="QWidget" name="centralwidget">
<layout class="QGridLayout" name="gridLayout"> <layout class="QGridLayout" name="gridLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item row="0" column="0"> <item row="0" column="0">
<widget class="QSplitter" name="splitter"> <widget class="QSplitter" name="splitter">
<property name="orientation"> <property name="orientation">
@@ -28,6 +40,9 @@
<bool>false</bool> <bool>false</bool>
</property> </property>
<widget class="QTreeWidget" name="Buckets"> <widget class="QTreeWidget" name="Buckets">
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="uniformRowHeights"> <property name="uniformRowHeights">
<bool>true</bool> <bool>true</bool>
</property> </property>
@@ -83,6 +98,18 @@
<string>Data</string> <string>Data</string>
</attribute> </attribute>
<layout class="QVBoxLayout" name="verticalLayout"> <layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>2</number>
</property>
<property name="topMargin">
<number>2</number>
</property>
<property name="rightMargin">
<number>2</number>
</property>
<property name="bottomMargin">
<number>2</number>
</property>
<item> <item>
<widget class="QTableView" name="contentBox"> <widget class="QTableView" name="contentBox">
<property name="verticalScrollMode"> <property name="verticalScrollMode">
@@ -104,6 +131,18 @@
<string>Query</string> <string>Query</string>
</attribute> </attribute>
<layout class="QVBoxLayout" name="verticalLayout_2"> <layout class="QVBoxLayout" name="verticalLayout_2">
<property name="leftMargin">
<number>2</number>
</property>
<property name="topMargin">
<number>2</number>
</property>
<property name="rightMargin">
<number>2</number>
</property>
<property name="bottomMargin">
<number>2</number>
</property>
<item> <item>
<widget class="QSplitter" name="splitter_2"> <widget class="QSplitter" name="splitter_2">
<property name="orientation"> <property name="orientation">
@@ -185,16 +224,23 @@
</property> </property>
<addaction name="actionCreate_Bolt_database_from_zip"/> <addaction name="actionCreate_Bolt_database_from_zip"/>
</widget> </widget>
<widget class="QMenu" name="menu_View">
<property name="title">
<string>&amp;View</string>
</property>
<addaction name="action_Appearance"/>
</widget>
<addaction name="menu_File"/> <addaction name="menu_File"/>
<addaction name="menu_Data"/> <addaction name="menu_Data"/>
<addaction name="menu_Query"/> <addaction name="menu_Query"/>
<addaction name="menu_View"/>
<addaction name="menu_Tools"/> <addaction name="menu_Tools"/>
<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 +251,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 +262,50 @@
</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="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 +339,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 +445,19 @@
</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="action_Appearance">
<property name="text">
<string>&amp;Appearance...</string>
</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"`
} }
} }
@@ -136,9 +136,10 @@ func (s SSHTunnel) Open(ctx context.Context) (*ssh.Client, error) {
cfg.HostKeyCallback = ssh.InsecureIgnoreHostKey() cfg.HostKeyCallback = ssh.InsecureIgnoreHostKey()
} else if s.HostVerification.ExternalKnownHostsFile != nil { } else if s.HostVerification.ExternalKnownHostsFile != nil {
cb, err := knownhosts.New(string(s.HostVerification.ExternalKnownHostsFile.Path)) loadPath := string(s.HostVerification.ExternalKnownHostsFile.Path)
cb, err := knownhosts.New(loadPath)
if err != nil { if err != nil {
return nil, fmt.Errorf("Parsing known_hosts file %q: %w") return nil, fmt.Errorf("Parsing known_hosts file %q: %w", loadPath, err)
} }
cfg.HostKeyCallback = cb cfg.HostKeyCallback = cb

275
table.go
View File

@@ -5,8 +5,10 @@ import (
) )
type TableColumn interface { type TableColumn interface {
DataType() string
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 +16,26 @@ type TableColumn interface {
IndicateBlue() bool IndicateBlue() bool
} }
type EditAdvancedTableColumn interface {
CreateEditor(parent *qt.QWidget, aRow int, resolve, reject func())
}
type TooltipColumn interface {
Tooltip(aRow int) *qt.QVariant
}
type NullableTableColumn interface {
CellIsNull(aRow int) bool
SetCellNull(aRow int)
}
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 +54,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 +75,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) {
@@ -172,6 +159,26 @@ func (ts *tableState) SetCell(aRow, aCol int, data any) {
ts.columns[aCol].SetCell(aRow, data) ts.columns[aCol].SetCell(aRow, data)
} }
func (ts *tableState) IsColumnNullable(aCol int) bool {
_, ok := ts.columns[aCol].(NullableTableColumn)
return ok
}
func (ts *tableState) SetCellNull(aRow, aCol int) bool {
if aRow >= ts.rowCount {
panic("SetCell with too large row")
}
setNull, ok := ts.columns[aCol].(NullableTableColumn)
if !ok {
return false
}
setNull.SetCellNull(aRow)
ts.dataIsEdited(aRow, aCol)
return true
}
// SetRowPrimaryKey // SetRowPrimaryKey
// The slice data will be copied. // The slice data will be copied.
func (ts *tableState) SetRowPrimaryKey(aRow int, data []byte) { func (ts *tableState) SetRowPrimaryKey(aRow int, data []byte) {
@@ -187,9 +194,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"},
) )
@@ -213,6 +220,12 @@ func (ts *tableState) DeleteSelectedRows() {
// this won't find all rows, just fully-selected rows from clicking on the // this won't find all rows, just fully-selected rows from clicking on the
// vertical header bar // vertical header bar
ts.DeleteRows(rows...)
}
// DeleteSelectedRows marks the rows for deletion.
func (ts *tableState) DeleteRows(rows ...qt.QModelIndex) {
for _, row := range rows { for _, row := range rows {
row_ := row.Row() // your boat row_ := row.Row() // your boat
@@ -253,6 +266,36 @@ func (ts *tableState) InsertNewRow() {
ts.OnEdited() ts.OnEdited()
} }
// dataIsEdited updates the updateRows array, and emits our OnEdited signal.
func (ts *tableState) dataIsEdited(aRow, aCol int) {
// If this is an insert row, no need to patch updateRows
if _, ok := ts.insertRows[aRow]; ok {
// nothing to do
} else {
// If this row was marked for deletion, this new edit takes priority
delete(ts.deleteRows, aRow)
// Set background colour
cells, ok := ts.updateRows[aRow]
if !ok {
cells = make([]int, 0, 1)
}
if slice_contains(cells, aCol) {
// already ok
} else {
cells = append(cells, aCol)
}
ts.updateRows[aRow] = cells
}
// Emit signal
// TODO ideally this would run after this function returned?
ts.OnEdited()
}
func NewTableState(tbl *qt.QTableView) *tableState { func NewTableState(tbl *qt.QTableView) *tableState {
// Stylistic changes // Stylistic changes
@@ -285,6 +328,14 @@ func NewTableState(tbl *qt.QTableView) *tableState {
} }
return qt.NewQVariant11(ts.columnLabels[section]) return qt.NewQVariant11(ts.columnLabels[section])
} else if role == int(qt.ToolTipRole) && orientation == qt.Horizontal {
if section >= len(ts.columnLabels) {
return qt.NewQVariant() // invalid request
}
tooltip := `Data type: ` + ts.columns[section].DataType()
return qt.NewQVariant11(tooltip)
} }
return super(section, orientation, role) return super(section, orientation, role)
@@ -317,8 +368,19 @@ 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.ToolTipRole):
if tipper, ok := ts.columns[aCol].(TooltipColumn); ok {
return tipper.Tooltip(aRow)
}
return qt.NewQVariant() // no tooltip
case int(qt.EditRole): case int(qt.EditRole):
// Supplying to the editor, return a QVariant of raw editing type // Supplying to the editor, return a QVariant of raw editing type
@@ -363,31 +425,7 @@ func NewTableState(tbl *qt.QTableView) *tableState {
// Update content // Update content
ts.columns[aCol].ApplySimpleChange(aRow, value) ts.columns[aCol].ApplySimpleChange(aRow, value)
// If this is an insert row, no need to patch updateRows ts.dataIsEdited(aRow, aCol)
if _, ok := ts.insertRows[aRow]; ok {
// nothing to do
} else {
// If this row was marked for deletion, this new edit takes priority
delete(ts.deleteRows, aRow)
// Set background colour
cells, ok := ts.updateRows[aRow]
if !ok {
cells = make([]int, 0, 1)
}
if slice_contains(cells, aCol) {
// already ok
} else {
cells = append(cells, aCol)
}
ts.updateRows[aRow] = cells
}
// Emit signal
// TODO ideally this would run after this function returned?
ts.OnEdited()
// Done // Done
return true return true
@@ -417,9 +455,104 @@ func NewTableState(tbl *qt.QTableView) *tableState {
} }
}) })
tbl.SetContextMenuPolicy(qt.CustomContextMenu)
tbl.OnCustomContextMenuRequested(func(pos *qt.QPoint) {
idx := tbl.IndexAt(pos)
if !idx.IsValid() {
return // No popup
}
mnu := qt.NewQMenu(tbl.QWidget)
mnu.SetAttribute(qt.WA_DeleteOnClose)
_ = mnu.AddSection("Row")
if ts.allowEdit {
deleteRowAction := mnu.AddAction2(qt.NewQIcon4(`:/assets/delete.png`), "Delete row")
deleteRowAction.OnTriggered(func() {
ts.DeleteRows(*idx)
})
}
_ = mnu.AddSection("Cell")
if ts.allowEdit {
editCellAction := mnu.AddAction2(qt.NewQIcon4(`:/assets/pencil.png`), "Edit cell...")
editCellAction.OnTriggered(func() {
tbl.Edit(idx)
})
}
if ts.IsColumnNullable(idx.Column()) {
setCellNullAction := mnu.AddAction2(qt.NewQIcon4(`:/assets/note_delete.png`), "Set cell to null")
setCellNullAction.OnTriggered(func() {
ts.SetCellNull(idx.Row(), idx.Column())
})
}
mnu.Popup(tbl.Viewport().MapToGlobalWithQPoint(pos))
})
// Set target QTableView's model // Set target QTableView's model
tbl.SetModel(model.QAbstractItemModel) tbl.SetModel(model.QAbstractItemModel)
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

@@ -13,6 +13,10 @@ type binColumn struct {
vals [][]byte vals [][]byte
} }
func (*binColumn) DataType() string {
return "Binary"
}
func (bc *binColumn) SetRowCount(newlen int) { func (bc *binColumn) SetRowCount(newlen int) {
bc.vals = slice_set_len(bc.vals, newlen) bc.vals = slice_set_len(bc.vals, newlen)
} }
@@ -20,14 +24,29 @@ 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 // TODO Use slice_dup() here because many iterator loops reuse the backing []byte
// slice. However it would be more efficient if slice_dup() was explicitly called
// only by each the DB implementation that really needs it
bc.vals[aRow] = slice_dup(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 +70,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 +120,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

@@ -13,6 +13,10 @@ type bsonColumn struct {
vals []bson.D vals []bson.D
} }
func (*bsonColumn) DataType() string {
return "BSON"
}
func (bc *bsonColumn) SetRowCount(newlen int) { func (bc *bsonColumn) SetRowCount(newlen int) {
bc.vals = slice_set_len(bc.vals, newlen) bc.vals = slice_set_len(bc.vals, newlen)
} }
@@ -26,6 +30,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 +87,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

80
table_dynamic.go Normal file
View File

@@ -0,0 +1,80 @@
package main
import (
"fmt"
qt "github.com/mappu/miqt/qt6"
)
type dynamicColumn struct {
vals []any
}
func (*dynamicColumn) DataType() string {
return "Dynamic"
}
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{}

58
table_gomodule.go Normal file
View File

@@ -0,0 +1,58 @@
package main
import (
qt "github.com/mappu/miqt/qt6"
)
type goModuleColumn struct {
vals []string
}
func (*goModuleColumn) DataType() string {
return "Go Module"
}
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{}

99
table_int64.go Normal file
View File

@@ -0,0 +1,99 @@
package main
import (
"fmt"
"time"
qt "github.com/mappu/miqt/qt6"
)
type int64Column struct {
vals []int64
}
func (*int64Column) DataType() string {
return "Integer"
}
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 makeIntegerTooltip(val int64) *qt.QVariant {
// If this looks like a unix timestamp, convert it
const dateFormatNice = "2006-01-02 15:04:05 MST -07:00"
if 1000000000 <= val && val <= 5000000000 { // circa year 2001 -> 2128
// Show the timestamp in UTC and in the OS's current location
return qt.NewQVariant11(
"Unix timestamp (seconds):\n" +
time.Unix(val, 0).UTC().Format(dateFormatNice) + "\n" +
time.Unix(val, 0).Local().Format(dateFormatNice))
}
if 1000000000000 <= val && val <= 5000000000000 { // circa year 2001 -> 2128
// Show the timestamp in UTC and in the OS's current location
return qt.NewQVariant11(
"Unix timestamp (milliseconds):\n" +
time.Unix(val/1000, val%1000).UTC().Format(dateFormatNice) + "\n" +
time.Unix(val/1000, val%1000).Local().Format(dateFormatNice))
}
// Not in the normal time looking range
return qt.NewQVariant() // invalid
}
func (sc *int64Column) Tooltip(aRow int) *qt.QVariant {
return makeIntegerTooltip(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{}
var _ TooltipColumn = &int64Column{}

87
table_sqlnullint64.go Normal file
View File

@@ -0,0 +1,87 @@
package main
import (
"database/sql"
"fmt"
qt "github.com/mappu/miqt/qt6"
)
type sqlNullInt64Column struct {
vals []sql.NullInt64
}
func (*sqlNullInt64Column) DataType() string {
return "Integer (Nullable)"
}
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) Tooltip(aRow int) *qt.QVariant {
cell := sc.vals[aRow]
if cell.Valid {
return makeIntegerTooltip(cell.Int64)
}
return qt.NewQVariant()
}
func (sc *sqlNullInt64Column) CellIsNull(aRow int) bool {
return !sc.vals[aRow].Valid
}
func (sc *sqlNullInt64Column) SetCellNull(aRow int) {
sc.vals[aRow] = sql.NullInt64{Valid: false}
}
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{}
var _ TooltipColumn = &sqlNullInt64Column{}

78
table_sqlnullstring.go Normal file
View File

@@ -0,0 +1,78 @@
package main
import (
"database/sql"
"fmt"
qt "github.com/mappu/miqt/qt6"
)
type sqlNullStringColumn struct {
vals []sql.NullString
}
func (*sqlNullStringColumn) DataType() string {
return "String (Nullable)"
}
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) SetCellNull(aRow int) {
sc.vals[aRow] = sql.NullString{Valid: false}
}
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"
) )
@@ -8,6 +10,10 @@ type stringColumn struct {
vals []string vals []string
} }
func (*stringColumn) DataType() string {
return "String"
}
func (sc *stringColumn) SetRowCount(newlen int) { func (sc *stringColumn) SetRowCount(newlen int) {
sc.vals = slice_set_len(sc.vals, newlen) sc.vals = slice_set_len(sc.vals, newlen)
} }
@@ -15,12 +21,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 +40,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"
} }
} }