1 Commits

Author SHA1 Message Date
97c58d7fc4 fltk: initial commit 2024-07-18 17:01:02 +12:00
122 changed files with 2662 additions and 8104 deletions

11
.gitignore vendored
View File

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

View File

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

View File

@@ -1,8 +1,7 @@
ISC License ISC License
Copyright 2025 mappy Copyright 2024 mappy
Copyright 2025 The QBolt Author(s) Copyright 2024 The yvbolt 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

@@ -1,72 +1,46 @@
SHELL:=/bin/bash SHELL:=/bin/bash
SOURCES := $(shell find . -name '*.go' -type f)
GIT_REV := $(shell git describe --exact-match --tags 2>/dev/null || printf "%s-%s" $$(git describe --tags --abbrev=0) $$(git rev-parse HEAD | head -c8))
.DEFAULT_GOAL := dist
MIQT_UIC ?= ~/go/bin/miqt-uic
MIQT_RCC ?= ~/go/bin/miqt-rcc
MIQT_DOCKER ?= ~/go/bin/miqt-docker
GO_WINRES ?= ~/go/bin/go-winres
.PHONY: generate SOURCES=$(find . -name '*.go' -type f)
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 connectionDialog.ui -OutFile connectionDialog.go -Qt6
$(MIQT_UIC) -InFile connectionManagerDialog.ui -OutFile connectionManagerDialog.go -Qt6
$(MIQT_RCC) -Input embed.qrc -Qt6
.PHONY: designer liblcl-2.2.3.zip:
designer: rm -f liblcl-2.2.3.zip
/usr/lib/qt6/bin/designer & wget 'https://github.com/ying32/govcl/releases/download/v2.2.3/liblcl-2.2.3.zip'
.PHONY: optimize-images liblcl.so: liblcl-2.2.3.zip
optimize-images: echo "154b4e4a1d5137a2ffe51cb4d0bf152dd997c12616ae30862775c0e4f0928e88 liblcl-2.2.3.zip" | sha256sum -c
# Strip iCCC colour chunks that libpng/Qt complain about at runtime rm -f liblcl.so
for f in assets/*.png ; do convert "$$f" -strip "$$f" ; done unzip -j liblcl-2.2.3.zip linux64-gtk2/liblcl.so -d .
optipng -quiet -o5 assets/*.png touch liblcl.so
make generate
qbolt: $(SOURCES) liblcl.dll: liblcl-2.2.3.zip
# Target a debian-12 baseline build echo "154b4e4a1d5137a2ffe51cb4d0bf152dd997c12616ae30862775c0e4f0928e88 liblcl-2.2.3.zip" | sha256sum -c
sed -i -re 's/".+"/"$(GIT_REV)"/g' version.go rm -f liblcl.dll
$(MIQT_DOCKER) linux64-go1.26-qt6.4-dynamic -minify-build unzip -j liblcl-2.2.3.zip win64/liblcl.dll -d .
git checkout -- version.go touch liblcl.dll
chmod 755 qbolt
upx --lzma qbolt
qbolt.exe: $(SOURCES) yvbolt: $(SOURCES)
sed -i -re 's/".+"/"$(GIT_REV)"/g' version.go CGO_CFLAGS='-O2 -Wno-return-local-addr' GOOS=linux GOARCH=amd64 CGO_ENABLED=1 go build -trimpath -ldflags '-s -w'
$(MIQT_DOCKER) win64-cross-go1.26-qt6.5-static -windows-build --tags=windowsqtstatic chmod 755 yvbolt
git checkout -- version.go
$(GO_WINRES) patch --in winres.json --no-backup --product-version git-tag --file-version git-tag qbolt.exe
upx --lzma qbolt.exe
qbolt.apk: $(SOURCES) yvbolt.exe: $(SOURCES)
$(MIQT_DOCKER) android-armv8a-go1.23-qt6.6-dynamic -android-build CGO_CFLAGS='-O2 -Wno-return-local-addr' GOOS=windows GOARCH=amd64 CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc-win32 go build -trimpath -ldflags '-s -w -H windowsgui'
upx --lzma yvbolt.exe
qbolt.linux64.tar.xz: qbolt yvbolt.linux64.tar.xz: yvbolt liblcl.so
rm -f qbolt.linux64.tar.xz rm -f yvbolt.linux64.tar.xz
XZ_OPT='-T0 -9' tar caf qbolt.linux64.tar.xz --owner=0 --group=0 qbolt XZ_OPT='-T0 -9' tar caf yvbolt.linux64.tar.xz --owner=0 --group=0 yvbolt liblcl.so
qbolt.win64.zip: qbolt.exe yvbolt.win64.zip: yvbolt.exe liblcl.dll
rm -f qbolt.win64.zip rm -f yvbolt.win64.zip
zip -9 qbolt.win64.zip qbolt.exe zip -9 yvbolt.win64.zip yvbolt.exe liblcl.dll
.PHONY: dist .PHONY: dist
dist: qbolt.linux64.tar.xz qbolt.win64.zip dist: yvbolt.linux64.tar.xz yvbolt.win64.zip
.PHONY: clean .PHONY: clean
clean: clean:
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
##### .PHONY: clean-all
# Test databases in Docker clean-all: clean
rm -f liblcl-2.2.3.zip liblcl.so liblcl.dll
.PHONY: 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
.PHONY: test-redis
test-redis:
sudo docker run --rm -p 127.0.0.1:6379:6379 redis:latest

127
README.md
View File

@@ -1,59 +1,104 @@
# QBolt # yvbolt
A graphical interface for multiple databases. A graphical browser for multiple databases using [GoVCL](https://z-kit.cc/en/).
This is an experimental application and you should generally prefer to use [qbolt](https://code.ivysaur.me/qbolt).
## Features ## Features
- Native desktop application, running on Linux, Windows, macOS, and Android - Native desktop application, running on Linux, Windows, and macOS
- Connect to multiple databases at once - Connect to multiple databases
- Browse table/bucket content - Browse table/bucket content
- Use context menu to perform special table/bucket actions - Use context menu to perform special table/bucket actions
- Edit content, and add/delete rows for supported databases
- View database/bucket statistics and metadata
- Run custom SQL queries - Run custom SQL queries
- Select text to run partial query - Select text to run partial query
- Safe handling for non-UTF8 key and data fields - Safe handling for non-UTF8 key and data fields
- Hex viewer for binary data - Supported databases:
- Connection Manager saves connections with AEAD AES256-GCM using OS keychain - Badger v4
- Bolt
## Supported databases - Recursive bucket support
- Option to open as readonly
There are currently 18 supported databases: - Debconf
- Pebble
Database |Read |Editing |Query |Connection options |Context menu actions - Redis
-------------|------|---------|------|--------------------|-------- - SQLite
Badger v4 |Yes |Yes |No |Encrypted, readonly, in-memory |Backup, restore, compact - Drivers: mattn (CGo), modernc.org (no cgo), experimental command-line driver
Bitcask |Yes |Yes |No |Readonly, autorecovery |Backup
BuntDB |Yes |Yes |No |In-memory |Shrink
Bolt |Yes |Yes |No |Readonly |Create/delete child buckets, import/export as zip
Debconf |Yes |No |No | |
Freedesktop.org Secret Service |Yes |No | No | |Unlock, create new collection
LevelDB |Yes |Yes |No |Readonly |
LMDB |Yes |Yes |No |Multi-DB, readonly |Create/delete child databases
LotusDB |Yes |Yes |No | |
MongoDB |Yes |No |Yes |SSH tunnel |Create/delete child databases and collections
Pebble |Yes |Yes |No |Readonly, in-memory |
Pogreb |Yes |Yes |No | |Compact
Redis |Yes |No |Yes |SSH tunnel, RESP v3 |
RoseDB |Yes |Yes |No | |
SQLite |Yes |Yes |Yes |**SSH tunnel**, CLI driver, in-memory |Vacuum, export
SSH Agent |Yes |No |No |Unix/TCP |Lock, unlock
Starskey |Yes |Yes |No |Compression |
VoidDB |Yes |Yes |No |Multi-keyspace |Create/delete child keyspaces
## License ## License
The code in this project is licensed under the ISC license (see `LICENSE` file for details) with the following caveats: The code in this project is licensed under the ISC license (see `LICENSE` file for details).
- This project depends on third-party libraries under additional open source licenses. This project redistributes images from the famfamfam/silk icon set under the [CC-BY 2.5 license](http://creativecommons.org/licenses/by/2.5/).
- This project redistributes images from the [famfamfam/silk icon set](https://github.com/markjames/famfamfam-silk-icons) under the [CC-BY 2.5 license](http://creativecommons.org/licenses/by/2.5/).
- 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.
## Download This project includes trademarked logo images for each supported database type.
Get the latest version from [the releases page »](https://git.ivysaur.me/code.ivysaur.me/qbolt/releases) ## Usage
1. `CGO_ENABLED=1 go build`
2. [Download liblcl](https://github.com/ying32/govcl/releases/download/v2.2.3/liblcl-2.2.3.zip) for your platform, or [compile it yourself](https://github.com/ying32/liblcl) (tested with v2.2.3)
3. Place the liblcl library file in the same directory as `yvbolt`
4. Run `yvbolt` and use the main menu to open a database
## Changelog ## Changelog
See [the full change history »](https://git.ivysaur.me/code.ivysaur.me/qbolt/src/branch/master/CHANGELOG.md) 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

127
TODO
View File

@@ -1,129 +1,40 @@
- Drag and drop database into UI (QBolt parity) - Cast DB to wider interface to check feature support
- Portable mode (portable.txt or portable/ dir)
- Syntax highlighting in editor - Syntax highlighting in editor
- Autorefresh
- Sshagent
- want to trigger an async refresh from inside the LDB after lock/unlock
- 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
- Table: BSON view can't see data
- Table: quick filter
- QSortFilterProxyModel
- Cancellation
- Loading animations for connection + queries
- Delay rendering properties/data tab until tab is focused
- Mutation - Mutation
- Get real primary key for mutation instead of string approximation
- Badger: Support insert/update/delete
- Pebble: Support insert/update/delete
- Debconf: Support insert/update/delete - Debconf: Support insert/update/delete
- Redis: Support insert/update/delete - Redis: Support insert/update/delete
- SecretService: Support insert/update/delete - SQLite: Support insert/update/delete
- Binary data viewer - Binary data viewer
- Detect jpg/png and show as image - Detect jpg/png and show as image
- More DB types - More DB types
- MySQL (& MariaDB/TiDB) - MySQL
- Postgres - Postgres
- CLI using psql - CLI using psql
- Lungo: Mini embeddable Mongo - https://github.com/256dpi/lungo
- MSSQL (recursive navigation for instances) - MSSQL (recursive navigation for instances)
- NutsDB https://github.com/nutsdb/nutsdb
- Sniper https://github.com/recoilme/sniper
- Other K/V stores from https://github.com/smallnest/kvbench - Other K/V stores from https://github.com/smallnest/kvbench
- Windows registry - Windows registry
- Allow entering path for quick navigation
- LDAP - LDAP
- Dolt - Connection dialog
- Memcache - SSH tunnels
- Listing all keys is not well supported, needs hacks - Badger encryption key dialog
- Chai (built on Pebble) - https://github.com/chaisql/chai - Pebble: connection options dialog
- CloverDB (built on Bolt/Badger) - https://github.com/ostafen/clover
- APCu - need some sort of hook into the storage engine
- UnisonDB - https://github.com/ankur-anand/unisondb
- Etcd
- v2: hierarchal
- v3: flat key namespace
- CSV file
- Allow querying with sqlite or duckDB?
- Parquet file
- Allow querying with duckDB?
- SSDB (Redis-compatible)
- Time-series DBs
- Prometheus
- VictoriaMetrics
- FrostDB https://github.com/polarsignals/frostdb
- https://dbdb.io/browse?programming=go-lang&q=
- Maxmind GeoIP MMDB format
- https://github.com/maxmind/mmdbwriter
- KeePass kdbx
- Not-quite-DBs
- IRC client
- Docker daemon (images, containers, ...)
- ssh known-hosts
- golang.org/x/crypto/ssh/knownhosts - already using this package
- Generic ODBC, database/sql, ...
- Other language DBs
- C, C++
- Tokyo Cabinet, Kyoto Cabinet, Tkrzw
- https://github.com/TerraTech/go-tokyocabinet needs pkg-config tokyocabinet
- https://github.com/estraier/tkrzw-go needs pkg-config tkrzw
- cdb (DJB's Constant Database)
- RocksDB
- https://github.com/tecbot/gorocksdb Go bindings, need pkg-config rocksdb
- Rust
- Stoolap https://github.com/stoolap/stoolap
- Rust, needs C binding layer https://github.com/mozilla/cbindgen
- JDBC Java databases
- H2, HSQLDB, Apache Derby
- SQLite CLI driver: - SQLite CLI driver:
- Context support - Context 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: - Debconf: separate groups by first slash in name
- v1/v2/v3 support - SQLite: drop table doesn't autorefresh nav since callback is late
- option to use namespace separators for virtual buckets - Build
- SQLite: - Build own liblcl binaries in docker
- drop table doesn't autorefresh nav since callback is late - Win32 icon resource
- more accurate type handling - https://github.com/ying32/govcl/tree/master/Tools/winRes
- 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
- set cell null
- other special objects
- triggers? udf functions?
- virtual tables, shadow tables
- toggle showing system tables
- e.g. sqlite_schema a.k.a. sqlite_master not present in default list; sqlite_sequence, is present
- both show in pragma_table_list()
- integrity checks / quick_check
- attach additional db to same connection ("schemas")
- ~~autoincrement: if column is autoincrement and left blank on insert, do not populate in INSERT statement~~ works with implicitly null columns
- LMDB: dupsort mode (duplicate keys / entries-per-key)
- MongoDB
- UI for replica sets, ssl certs, cluster, custom auth database
- SSH tunnel: error `ssh: tcpChan: deadline not supported` - needs workaround
- Backup/restore
- drop db/collection doesn't autorefresh nav since server is asynchronous
- VoidDB:
- drop multidb doesn't autorefresh nav
- SSH tunnel
- option to use external/system SSH
- 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
- Context/interrupt slow queries - Context/interrupt slow queries
- Query history - Faster virtual rendering
- Query log - https://github.com/ying32/govcl/blob/master/samples/listviewvirtualdata/main.go
- Test suite
- `CREATE TABLE foo (id integer primary key, aaa text not null, bbb text not null);`
- Ability to convert database types
- Export all data from grid
- Export all data from all buckets within a DB
- Reconnect
- Connection manager: clone entry

Binary file not shown.

Before

Width:  |  Height:  |  Size: 714 B

After

Width:  |  Height:  |  Size: 733 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 707 B

After

Width:  |  Height:  |  Size: 685 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 637 B

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 892 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 754 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 337 B

After

Width:  |  Height:  |  Size: 390 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 685 B

After

Width:  |  Height:  |  Size: 658 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 715 B

After

Width:  |  Height:  |  Size: 659 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 803 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 819 B

After

Width:  |  Height:  |  Size: 775 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 812 B

After

Width:  |  Height:  |  Size: 755 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 692 B

After

Width:  |  Height:  |  Size: 715 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 793 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 755 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 583 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 644 B

After

Width:  |  Height:  |  Size: 634 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 770 B

After

Width:  |  Height:  |  Size: 739 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 639 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 907 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 391 B

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 540 B

After

Width:  |  Height:  |  Size: 589 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 558 B

After

Width:  |  Height:  |  Size: 603 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 615 B

After

Width:  |  Height:  |  Size: 666 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 348 B

After

Width:  |  Height:  |  Size: 395 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 551 B

After

Width:  |  Height:  |  Size: 566 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 629 B

After

Width:  |  Height:  |  Size: 663 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 645 B

After

Width:  |  Height:  |  Size: 660 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 744 B

After

Width:  |  Height:  |  Size: 723 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 611 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 353 B

After

Width:  |  Height:  |  Size: 752 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 622 B

After

Width:  |  Height:  |  Size: 831 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 763 B

After

Width:  |  Height:  |  Size: 948 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 821 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 303 B

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 624 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 738 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 460 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 545 B

After

Width:  |  Height:  |  Size: 824 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 980 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 384 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 825 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 719 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 970 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 665 B

After

Width:  |  Height:  |  Size: 919 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 377 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 802 B

431
config.go
View File

@@ -1,431 +0,0 @@
package main
import (
"context"
"encoding/json"
"fmt"
"os"
"reflect"
"time"
"github.com/mappu/autoconfig"
qt "github.com/mappu/miqt/qt6"
)
type DBConnector interface {
Connect(context.Context) (loadedDatabase, string, error)
}
type ConnectionConfig struct {
Type autoconfig.OneOf
Badger *badgerConnection `ylabel:"BadgerDB v4" yicon:":/assets/vendor_dgraph.png" json:",omitempty"`
Bitcask *bitcaskDBConnection `yicon:":/assets/vendor_riak.png" json:",omitempty"`
Bolt *boltConfig `yicon:":/assets/vendor_github.png" json:",omitempty"`
BuntDB *buntDBConnection `ylabel:"BuntDB" yicon:":/assets/vendor_buntdb.png" json:",omitempty"`
Debconf *debconfConnection `yicon:":/assets/vendor_debian.png" json:",omitempty"`
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"`
LMDB *lmdbConnection `yicon:":/assets/vendor_lmdb.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"`
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"`
RoseDB *roseDBConn `ylabel:"RoseDB" yicon:":/assets/vendor_rosedb.png" json:",omitempty"`
SQLite *sqliteConnection `ylabel:"SQLite" yicon:":/assets/vendor_sqlite.png" json:",omitempty"`
SSHAgent *sshAgentConn `yicon:":/assets/vendor_ssh.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 {
return &ConnectionConfig{
Type: "Bolt", // favouritism
}
}
// Icon gets the Qt string name for the icon of this database by parsing the
// 'yicon' struct tag with reflection.
// If there is no selection or if it's misconfigured, falls back to a generic
// database icon.
func (cc *ConnectionConfig) Icon() string {
taginfo, ok := reflect.ValueOf(cc).Type().Elem().FieldByName(string(cc.Type))
if !ok {
return ":/assets/database.png"
}
yicon, ok := taginfo.Tag.Lookup("yicon")
if !ok {
return ":/assets/database.png"
}
return yicon
}
func (cc *ConnectionConfig) String() string {
if selection, err := cc.selection(); err == nil {
if stringer, ok := selection.(fmt.Stringer); ok {
return stringer.String()
}
}
if string(cc.Type) == "" {
return "Not configured"
}
return string(cc.Type)
}
func (cc *ConnectionConfig) selection() (DBConnector, error) {
selection := reflect.ValueOf(cc).Elem().FieldByName(string(cc.Type))
if !selection.IsValid() {
return nil, fmt.Errorf("Invalid database engine %q", cc.Type)
}
con, ok := selection.Interface().(DBConnector)
if !ok {
return nil, fmt.Errorf("Can't connect to database on type %q (weird)", cc.Type)
}
return con, nil
}
func (cc *ConnectionConfig) Connect(ctx context.Context) (loadedDatabase, string, error) {
dbc, err := cc.selection()
if err != nil {
return nil, "", fmt.Errorf("Invalid database engine %q", cc.Type)
}
return dbc.Connect(ctx)
}
var _ DBConnector = &ConnectionConfig{}
func (f *App) OnMnuConnectClick() {
config := NewConnectionConfig()
f.showConnectDialog(config)
}
func (f *App) showConnectDialog(config *ConnectionConfig) {
// autoconfig.OpenDialog() does not give us a "yes/no" result back from the
// dialog, so we still have to custom construct the dialog and use the
// formlayout option instead
dlg := NewConnectDialogUi()
dlg.ConnectDialog.SetParent2(f.ui.MainWindow.QWidget, qt.Dialog)
dlg.ConnectDialog.SetModal(true)
dlg.ConnectDialog.SetAttribute(qt.WA_DeleteOnClose)
saver := autoconfig.MakeConfigArea(config, dlg.formLayout)
dlg.ConnectDialog.OnAccept(func(super func()) {
// Validate connection before closing
dlg.buttonBox.SetEnabled(false)
defer dlg.buttonBox.SetEnabled(true)
dlg.buttonBox.Repaint()
// Save changes from UI into struct
saver()
// Connect -> get ld
ctx := context.Background() // TODO do in background thread?
ld, displayName, err := config.Connect(ctx)
if err != nil {
_ = 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()
return
}
// Add ld to mainwindow
f.addTopLevelDatabaseConnection(ld, displayName)
// Connection OK
// Offer to save into connection-manager
if res := qt.QMessageBox_Question2(dlg.ConnectDialog.QWidget, APPNAME, "Connection successful. Save the details into Connection Manager?", qt.QMessageBox__Save, qt.QMessageBox__Ignore); res == int(qt.QMessageBox__Save) {
f.TrySaveIntoConnectionManager(config, displayName)
}
// Default accept behaviour is: setResult(Accepted), emits onFinished; && Hide()
super()
})
dlg.ConnectDialog.Open() // Modal, unlike .Show()
}
type ConnMgrLoadError struct {
e error
}
func (c ConnMgrLoadError) Error() string {
return fmt.Sprintf("Failed to load saved connections: %s", c.e)
}
func (c ConnMgrLoadError) Unwrap() error {
return c.e
}
type ConnMgrSaveError struct {
e error
}
func (c ConnMgrSaveError) Error() string {
return fmt.Sprintf("Failed to save connections: %s", c.e)
}
func (c ConnMgrSaveError) Unwrap() error {
return c.e
}
func (f *App) TrySaveIntoConnectionManager(cc *ConnectionConfig, displayName string) {
data, err := f.getConnectionManagerContents()
if err != nil {
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, ConnMgrLoadError{err}.Error())
}
if displayName == "" {
displayName = cc.String()
}
data.Entries = append(data.Entries, SavedConfigEntry{
Description: displayName,
Connection: *cc,
})
err = f.saveConnectionManagerContents(data)
if err != nil {
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, ConnMgrSaveError{err}.Error())
}
}
func (f *App) OnMnuConnectionManagerClick() {
data, err := f.getConnectionManagerContents()
if err != nil {
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, ConnMgrSaveError{err}.Error())
return
}
dlg := NewconnectionManagerDialogUi()
dlg.connectionManagerDialog.SetParent2(f.ui.MainWindow.QWidget, qt.Dialog)
dlg.connectionManagerDialog.SetModal(true)
dlg.connectionManagerDialog.SetAttribute(qt.WA_DeleteOnClose)
dlg.treeWidget.SetRootIsDecorated(false)
dlg.treeWidget.SetSelectionMode(qt.QAbstractItemView__ExtendedSelection)
addEntryFor := func(entry SavedConfigEntry) {
itm := qt.NewQTreeWidgetItem()
itm.SetText(0, entry.Description)
itm.SetIcon(0, qt.NewQIcon4(entry.Connection.Icon()))
dlg.treeWidget.AddTopLevelItem(itm)
}
refreshEntryFor := func(idx *qt.QModelIndex, entry SavedConfigEntry) {
itm := dlg.treeWidget.ItemFromIndex(idx)
itm.SetText(0, entry.Description) // Update label
itm.SetIcon(0, qt.NewQIcon4(entry.Connection.Icon())) // Update icon
}
// Populate entries
for _, entry := range data.Entries {
addEntryFor(entry)
}
refreshButtonState := func() {
ct := len(dlg.treeWidget.SelectedItems())
dlg.connectBtn.SetEnabled(ct > 0)
dlg.connDelete.SetEnabled(ct > 0)
dlg.connEdit.SetEnabled(ct == 1)
}
connectToItem := func(itm *qt.QTreeWidgetItem) bool {
ctx := context.Background() // TODO do in background thread?
dlg.connectBtn.SetEnabled(false)
dlg.treeWidget.SetEnabled(false)
defer func() {
dlg.connectBtn.SetEnabled(true)
dlg.treeWidget.SetEnabled(true) // FIXME block the other buttons too!
}()
entry := data.Entries[dlg.treeWidget.IndexFromItem(itm).Row()]
ld, _, err := entry.Connection.Connect(ctx)
if err != nil {
_ = qt.QMessageBox_Critical(dlg.connectionManagerDialog.QWidget, APPNAME, fmt.Sprintf("Connecting to %s database: %s", entry.Connection.Type, err.Error()))
return false
}
// Add ld to mainwindow
// Don't use the displayName from the Connect() function - use the saved
// displayname instead
f.addTopLevelDatabaseConnection(ld, entry.Description)
return true
}
dlg.treeWidget.OnItemDoubleClicked(func(itm *qt.QTreeWidgetItem, _ int) {
// connect to specific item
if connectToItem(itm) {
dlg.connectionManagerDialog.Accept()
}
})
dlg.connectBtn.OnClicked(func() {
// Based on selectedItems, not currentItem
itms := dlg.treeWidget.SelectedItems()
var ok bool = true
for _, itm := range itms {
itmConnectOK := connectToItem(itm) // connect to selected
ok = ok && itmConnectOK
}
if ok {
dlg.connectionManagerDialog.Accept()
}
})
dlg.connDelete.SetEnabled(false)
dlg.connEdit.SetEnabled(false)
dlg.treeWidget.OnItemSelectionChanged(refreshButtonState)
dlg.connDelete.OnClicked(func() {
itms := dlg.treeWidget.SelectedItems()
for _, itm := range itms {
idx := dlg.treeWidget.IndexFromItem(itm).Row() // n.b. will dynamically change as the item is removed
itm.Delete()
data.Entries = slice_remove_index(data.Entries, idx)
}
// All changes made in both ui + data model
// Save data model changes
err := f.saveConnectionManagerContents(data)
if err != nil {
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, ConnMgrSaveError{err}.Error())
return
}
})
dlg.connEdit.OnClicked(func() {
// Edit is based on selectedIndex, not currentIndex
items := dlg.treeWidget.SelectedItems()
if len(items) != 1 {
return
}
idx := dlg.treeWidget.IndexFromItem(items[0])
row := idx.Row()
props := data.Entries[row]
autoconfig.OpenDialog(&props, dlg.connectionManagerDialog.QWidget, "Editing connection", func() {
data.Entries[row] = props
refreshEntryFor(idx, props)
err := f.saveConnectionManagerContents(data)
if err != nil {
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, ConnMgrSaveError{err}.Error())
return
}
})
})
dlg.connAdd.OnClicked(func() {
obj := SavedConfigEntry{
Description: "New connection (" + time.Now().Format(time.DateTime) + ")",
}
vv := NewConnectionConfig()
obj.Connection = *vv
autoconfig.OpenDialog(&obj, dlg.connectionManagerDialog.QWidget, "New connection", func() {
data.Entries = append(data.Entries, obj)
addEntryFor(obj)
err := f.saveConnectionManagerContents(data)
if err != nil {
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, ConnMgrSaveError{err}.Error())
return
}
})
})
miniMenu := qt.NewQMenu(dlg.toolsBtn.QWidget)
exportAction := miniMenu.AddActionWithText("Export connections...")
importAction := miniMenu.AddActionWithText("Import connections...")
dlg.toolsBtn.SetMenu(miniMenu)
dlg.toolsBtn.SetPopupMode(qt.QToolButton__InstantPopup)
exportAction.OnTriggered(func() {
saveAs := qt.QFileDialog_GetSaveFileName4(dlg.toolsBtn.QWidget, "Export connections...", "", "JSON files (*.json);;All files (*)")
if saveAs == "" {
return // cancelled
}
jbytes, err := json.MarshalIndent(data, "", " ")
if err != nil {
qt.QMessageBox_Warning(dlg.toolsBtn.QWidget, APPNAME, ConnMgrSaveError{err}.Error())
return
}
err = os.WriteFile(saveAs, jbytes, 0600)
if err != nil {
qt.QMessageBox_Warning(dlg.toolsBtn.QWidget, APPNAME, ConnMgrSaveError{err}.Error())
return
}
// OK
qt.QMessageBox_Information(dlg.toolsBtn.QWidget, APPNAME, "The connections have been exported successfully.")
})
importAction.OnTriggered(func() {
loadFile := qt.QFileDialog_GetOpenFileName4(dlg.toolsBtn.QWidget, "Import connections...", "", "JSON files (*.json);;All files (*)")
if loadFile == "" {
return // cancelled
}
jbytes, err := os.ReadFile(loadFile)
if err != nil {
qt.QMessageBox_Warning(dlg.toolsBtn.QWidget, APPNAME, ConnMgrLoadError{err}.Error())
return
}
var importData SavedConfig
err = json.Unmarshal(jbytes, &importData)
if err != nil {
qt.QMessageBox_Warning(dlg.toolsBtn.QWidget, APPNAME, ConnMgrLoadError{err}.Error())
return
}
// Add to UI
for _, newEntry := range importData.Entries {
addEntryFor(newEntry)
}
// Add to data model
data.Entries = append(data.Entries, importData.Entries...)
// Save data model to disk
err = f.saveConnectionManagerContents(data)
if err != nil {
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, ConnMgrSaveError{err}.Error())
return
}
})
refreshButtonState()
dlg.connectionManagerDialog.Show()
}

View File

@@ -1,198 +0,0 @@
package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strings"
"github.com/zalando/go-keyring"
)
type SavedConfigEntry struct {
Description string
Connection ConnectionConfig
}
type SavedConfig struct {
UserAgent string // APPNAME/{ver}
Entries []SavedConfigEntry
}
const (
saveSettingsFilename = `settings.dat`
keychainUserName = `settings-encryption-key`
)
func (f *App) getConnectionManagerContents() (*SavedConfig, error) {
cfgd, err := os.UserConfigDir()
if err != nil {
return nil, err
}
_ = os.MkdirAll(filepath.Join(cfgd, APPNAME), 0700)
ciphertext, err := os.ReadFile(filepath.Join(cfgd, APPNAME, saveSettingsFilename))
if err != nil {
if os.IsNotExist(err) {
// No file exists. Use blank
return &SavedConfig{
UserAgent: APPNAME,
}, nil
}
return nil, err
}
// Get decryption key from OS keychain
// Since we successfully loaded the file on disk, the keychain entry must exist
details, err := keyring.Get(APPNAME, keychainUserName)
if err != nil {
return nil, err
}
// Decrypt
// The encryption key is random 256 bytes, no need for a KDF
encryptionKeyBytes, err := hex.DecodeString(details)
if err != nil {
return nil, err
}
cc, err := aes.NewCipher(encryptionKeyBytes)
if err != nil {
return nil, err
}
cw, err := cipher.NewGCMWithRandomNonce(cc)
if err != nil {
return nil, err
}
plaintext, err := cw.Open(ciphertext[:0], nil, ciphertext, nil) // @ref https://pkg.go.dev/crypto/cipher#AEAD
if err != nil {
return nil, err
}
var ret SavedConfig
err = json.Unmarshal(plaintext, &ret)
if err != nil {
return nil, err
}
// Before returning the configuration, sort the entries alphabetically
sort.Slice(ret.Entries, func(i, j int) bool {
// FIXME there is probably a slightly more efficient way of doing this
return strings.ToLower(ret.Entries[i].Description) < strings.ToLower(ret.Entries[j].Description)
})
// Success
return &ret, nil
}
func (f *App) saveConnectionManagerContents(sc *SavedConfig) error {
cfgd, err := os.UserConfigDir()
if err != nil {
return err
}
// If the file exists already, we must have an existing encryption key
var exists bool
savePath := filepath.Join(cfgd, APPNAME, saveSettingsFilename)
if _, err := os.Stat(savePath); err != nil {
if os.IsNotExist(err) {
_ = os.MkdirAll(filepath.Join(cfgd, APPNAME), 0700)
exists = false
} else {
return err // some other real error
}
} else {
exists = true
}
details, err := keyring.Get(APPNAME, keychainUserName)
if err != nil {
if errors.Is(err, keyring.ErrNotFound) {
// Does not exist in keyring
// That is OK if there is no saved file yet / we can generate a random
// key. But if the file does already exist, this is fatal
if exists {
// File exists on disk but there is no encryption key to parse it
// Fatal
return fmt.Errorf(
"There is already a saved file on disk (%q), but there is no matching encryption key in the system keychain provider. Has your keychain been destroyed? To confirm, please delete the saved settings file.",
savePath,
)
} else {
// Generate new
// 256 bits (32 bytes) of cryto-random data, and then hex encode
randBuff := make([]byte, 32)
n, err := rand.Read(randBuff)
if err != nil {
return err
}
if n != len(randBuff) {
return io.ErrShortWrite
}
details = hex.EncodeToString(randBuff)
err = keyring.Set(APPNAME, keychainUserName, details)
if err != nil {
return err
}
// Safe
}
} else {
// Real keychain error
return err
}
}
// 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
}
// Encrypt
encryptionKeyBytes, err := hex.DecodeString(details)
if err != nil {
return err
}
cc, err := aes.NewCipher(encryptionKeyBytes)
if err != nil {
return err
}
cw, err := cipher.NewGCMWithRandomNonce(cc)
if err != nil {
return err
}
ciphertext := cw.Seal(plaintext[:0], nil, plaintext, nil)
// Save to disk
err = os.WriteFile(savePath, ciphertext, 0600)
if err != nil {
return err
}
// Save OK
return nil
}

View File

@@ -1,69 +0,0 @@
// Generated by miqt-uic. To update this file, edit the .ui file in
// Qt Designer, and then run 'go generate'.
//
//go:generate miqt-uic -InFile connectionDialog.ui -OutFile connectionDialog.go -Qt6
package main
import (
qt "github.com/mappu/miqt/qt6"
)
type ConnectDialogUi struct {
ConnectDialog *qt.QDialog
gridLayout *qt.QGridLayout
buttonBox *qt.QDialogButtonBox
formWidget *qt.QWidget
formLayout *qt.QFormLayout
}
// NewConnectDialogUi creates all Qt widget classes for ConnectDialog.
func NewConnectDialogUi() *ConnectDialogUi {
ui := &ConnectDialogUi{}
ui.ConnectDialog = qt.NewQDialog(nil)
ConnectDialog__objectName := qt.NewQAnyStringView3("ConnectDialog")
ui.ConnectDialog.SetObjectName(*ConnectDialog__objectName)
ConnectDialog__objectName.Delete() // setter copied value
ui.ConnectDialog.Resize(385, 364)
icon0 := qt.NewQIcon()
icon0.AddFile4(":/assets/database_add.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.ConnectDialog.SetWindowIcon(icon0)
ui.gridLayout = qt.NewQGridLayout(ui.ConnectDialog.QWidget)
gridLayout__objectName := qt.NewQAnyStringView3("gridLayout")
ui.gridLayout.SetObjectName(*gridLayout__objectName)
gridLayout__objectName.Delete() // setter copied value
ui.gridLayout.SetContentsMargins(11, 11, 11, 11)
ui.gridLayout.SetSpacing(6)
ui.buttonBox = qt.NewQDialogButtonBox(ui.ConnectDialog.QWidget)
buttonBox__objectName := qt.NewQAnyStringView3("buttonBox")
ui.buttonBox.SetObjectName(*buttonBox__objectName)
buttonBox__objectName.Delete() // setter copied value
ui.buttonBox.SetOrientation(qt.Horizontal)
ui.buttonBox.SetStandardButtons(qt.QDialogButtonBox__Cancel | qt.QDialogButtonBox__Ok)
ui.gridLayout.AddWidget2(ui.buttonBox.QWidget, 2, 0)
ui.formWidget = qt.NewQWidget(ui.ConnectDialog.QWidget)
formWidget__objectName := qt.NewQAnyStringView3("formWidget")
ui.formWidget.SetObjectName(*formWidget__objectName)
formWidget__objectName.Delete() // setter copied value
ui.formLayout = qt.NewQFormLayout(ui.formWidget)
formLayout__objectName := qt.NewQAnyStringView3("formLayout")
ui.formLayout.SetObjectName(*formLayout__objectName)
formLayout__objectName.Delete() // setter copied value
ui.formLayout.SetContentsMargins(0, 0, 0, 11)
ui.formLayout.SetSpacing(6)
ui.gridLayout.AddWidget2(ui.formWidget, 1, 0)
ui.buttonBox.OnAccepted(ui.ConnectDialog.Accept)
ui.buttonBox.OnRejected(ui.ConnectDialog.Reject)
ui.Retranslate()
return ui
}
// Retranslate reapplies all text translations.
func (ui *ConnectDialogUi) Retranslate() {
ui.ConnectDialog.SetWindowTitle(qt.QCoreApplication_Tr("Connect..."))
}

View File

@@ -1,85 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ConnectDialog</class>
<widget class="QDialog" name="ConnectDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>385</width>
<height>364</height>
</rect>
</property>
<property name="windowTitle">
<string>Connect...</string>
</property>
<property name="windowIcon">
<iconset resource="embed.qrc">
<normaloff>:/assets/database_add.png</normaloff>:/assets/database_add.png</iconset>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="2" column="0">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok</set>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QWidget" name="formWidget" native="true">
<layout class="QFormLayout" name="formLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
</layout>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="embed.qrc"/>
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>ConnectDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>ConnectDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -1,136 +0,0 @@
// Generated by miqt-uic. To update this file, edit the .ui file in
// Qt Designer, and then run 'go generate'.
//
//go:generate miqt-uic -InFile connectionManagerDialog.ui -OutFile connectionManagerDialog.go -Qt6
package main
import (
qt "github.com/mappu/miqt/qt6"
)
type connectionManagerDialogUi struct {
connectionManagerDialog *qt.QDialog
verticalLayout_2 *qt.QVBoxLayout
horizontalLayout *qt.QHBoxLayout
treeWidget *qt.QTreeWidget
verticalLayout *qt.QVBoxLayout
connAdd *qt.QToolButton
connEdit *qt.QToolButton
connDelete *qt.QToolButton
verticalSpacer *qt.QSpacerItem
horizontalLayout_2 *qt.QHBoxLayout
toolsBtn *qt.QToolButton
horizontalSpacer *qt.QSpacerItem
connectBtn *qt.QPushButton
}
// NewconnectionManagerDialogUi creates all Qt widget classes for connectionManagerDialog.
func NewconnectionManagerDialogUi() *connectionManagerDialogUi {
ui := &connectionManagerDialogUi{}
ui.connectionManagerDialog = qt.NewQDialog(nil)
connectionManagerDialog__objectName := qt.NewQAnyStringView3("connectionManagerDialog")
ui.connectionManagerDialog.SetObjectName(*connectionManagerDialog__objectName)
connectionManagerDialog__objectName.Delete() // setter copied value
ui.connectionManagerDialog.Resize(332, 449)
ui.verticalLayout_2 = qt.NewQVBoxLayout(ui.connectionManagerDialog.QWidget)
verticalLayout_2__objectName := qt.NewQAnyStringView3("verticalLayout_2")
ui.verticalLayout_2.SetObjectName(*verticalLayout_2__objectName)
verticalLayout_2__objectName.Delete() // setter copied value
ui.verticalLayout_2.SetContentsMargins(11, 11, 11, 11)
ui.verticalLayout_2.SetSpacing(6)
ui.horizontalLayout = qt.NewQHBoxLayout2()
horizontalLayout__objectName := qt.NewQAnyStringView3("horizontalLayout")
ui.horizontalLayout.SetObjectName(*horizontalLayout__objectName)
horizontalLayout__objectName.Delete() // setter copied value
ui.horizontalLayout.SetContentsMargins(0, 0, 0, 0)
ui.horizontalLayout.SetSpacing(6)
ui.treeWidget = qt.NewQTreeWidget(ui.connectionManagerDialog.QWidget)
treeWidget__objectName := qt.NewQAnyStringView3("treeWidget")
ui.treeWidget.SetObjectName(*treeWidget__objectName)
treeWidget__objectName.Delete() // setter copied value
ui.horizontalLayout.AddWidget(ui.treeWidget.QWidget)
ui.verticalLayout = qt.NewQVBoxLayout2()
verticalLayout__objectName := qt.NewQAnyStringView3("verticalLayout")
ui.verticalLayout.SetObjectName(*verticalLayout__objectName)
verticalLayout__objectName.Delete() // setter copied value
ui.verticalLayout.SetContentsMargins(0, 0, 0, 0)
ui.verticalLayout.SetSpacing(6)
ui.connAdd = qt.NewQToolButton(ui.connectionManagerDialog.QWidget)
connAdd__objectName := qt.NewQAnyStringView3("connAdd")
ui.connAdd.SetObjectName(*connAdd__objectName)
connAdd__objectName.Delete() // setter copied value
icon0 := qt.NewQIcon()
icon0.AddFile4(":/assets/add.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.connAdd.SetIcon(icon0)
ui.connAdd.SetAutoRaise(true)
ui.verticalLayout.AddWidget(ui.connAdd.QWidget)
ui.connEdit = qt.NewQToolButton(ui.connectionManagerDialog.QWidget)
connEdit__objectName := qt.NewQAnyStringView3("connEdit")
ui.connEdit.SetObjectName(*connEdit__objectName)
connEdit__objectName.Delete() // setter copied value
icon1 := qt.NewQIcon()
icon1.AddFile4(":/assets/pencil.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.connEdit.SetIcon(icon1)
ui.connEdit.SetAutoRaise(true)
ui.verticalLayout.AddWidget(ui.connEdit.QWidget)
ui.connDelete = qt.NewQToolButton(ui.connectionManagerDialog.QWidget)
connDelete__objectName := qt.NewQAnyStringView3("connDelete")
ui.connDelete.SetObjectName(*connDelete__objectName)
connDelete__objectName.Delete() // setter copied value
icon2 := qt.NewQIcon()
icon2.AddFile4(":/assets/delete.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.connDelete.SetIcon(icon2)
ui.connDelete.SetAutoRaise(true)
ui.verticalLayout.AddWidget(ui.connDelete.QWidget)
ui.verticalSpacer = qt.NewQSpacerItem4(20, 40, qt.QSizePolicy__Minimum, qt.QSizePolicy__Expanding)
ui.verticalLayout.AddItem(ui.verticalSpacer.QLayoutItem)
ui.horizontalLayout.AddLayout(ui.verticalLayout.QLayout)
ui.verticalLayout_2.AddLayout(ui.horizontalLayout.QLayout)
ui.horizontalLayout_2 = qt.NewQHBoxLayout2()
horizontalLayout_2__objectName := qt.NewQAnyStringView3("horizontalLayout_2")
ui.horizontalLayout_2.SetObjectName(*horizontalLayout_2__objectName)
horizontalLayout_2__objectName.Delete() // setter copied value
ui.horizontalLayout_2.SetContentsMargins(0, 0, 0, 0)
ui.horizontalLayout_2.SetSpacing(6)
ui.toolsBtn = qt.NewQToolButton(ui.connectionManagerDialog.QWidget)
toolsBtn__objectName := qt.NewQAnyStringView3("toolsBtn")
ui.toolsBtn.SetObjectName(*toolsBtn__objectName)
toolsBtn__objectName.Delete() // setter copied value
ui.toolsBtn.SetAutoRaise(true)
ui.horizontalLayout_2.AddWidget(ui.toolsBtn.QWidget)
ui.horizontalSpacer = qt.NewQSpacerItem4(40, 20, qt.QSizePolicy__Expanding, qt.QSizePolicy__Minimum)
ui.horizontalLayout_2.AddItem(ui.horizontalSpacer.QLayoutItem)
ui.connectBtn = qt.NewQPushButton(ui.connectionManagerDialog.QWidget)
connectBtn__objectName := qt.NewQAnyStringView3("connectBtn")
ui.connectBtn.SetObjectName(*connectBtn__objectName)
connectBtn__objectName.Delete() // setter copied value
icon3 := qt.NewQIcon()
icon3.AddFile4(":/assets/database_lightning.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.connectBtn.SetIcon(icon3)
ui.horizontalLayout_2.AddWidget(ui.connectBtn.QWidget)
ui.verticalLayout_2.AddLayout(ui.horizontalLayout_2.QLayout)
ui.Retranslate()
return ui
}
// Retranslate reapplies all text translations.
func (ui *connectionManagerDialogUi) Retranslate() {
ui.connectionManagerDialog.SetWindowTitle(qt.QCoreApplication_Tr("Connection Manager"))
ui.treeWidget.HeaderItem().SetText(0, qt.QTreeWidget_Tr("Connection"))
ui.toolsBtn.SetText(qt.QDialog_Tr("Tools"))
ui.connectBtn.SetText(qt.QDialog_Tr("Connect"))
}

View File

@@ -1,124 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>connectionManagerDialog</class>
<widget class="QDialog" name="connectionManagerDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>332</width>
<height>449</height>
</rect>
</property>
<property name="windowTitle">
<string>Connection Manager</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QTreeWidget" name="treeWidget">
<column>
<property name="text">
<string>Connection</string>
</property>
</column>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QToolButton" name="connAdd">
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/add.png</normaloff>:/assets/add.png</iconset>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="connEdit">
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/pencil.png</normaloff>:/assets/pencil.png</iconset>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="connDelete">
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/delete.png</normaloff>:/assets/delete.png</iconset>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QToolButton" name="toolsBtn">
<property name="text">
<string>Tools</string>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="connectBtn">
<property name="text">
<string>Connect</string>
</property>
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/database_lightning.png</normaloff>:/assets/database_lightning.png</iconset>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources>
<include location="embed.qrc"/>
</resources>
<connections/>
</ui>

View File

@@ -1,37 +1,52 @@
package main package main
import ( import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt" "fmt"
"os"
"path/filepath" "path/filepath"
"unsafe"
"github.com/dgraph-io/badger/v4" "github.com/dgraph-io/badger/v4"
"github.com/mappu/autoconfig" "github.com/ying32/govcl/vcl"
qt "github.com/mappu/miqt/qt6"
) )
type badgerLoadedDatabase struct { type badgerLoadedDatabase struct {
displayName string
db *badger.DB db *badger.DB
nav *vcl.TTreeNode
arena []*navData // keepalive
}
func (ld *badgerLoadedDatabase) DisplayName() string {
return ld.displayName
} }
func (ld *badgerLoadedDatabase) DriverName() string { func (ld *badgerLoadedDatabase) DriverName() string {
return "BadgerDB v4" return "Badger v4"
} }
func (ld *badgerLoadedDatabase) Properties(bucketPath []string) (string, error) { func (ld *badgerLoadedDatabase) RootElement() *vcl.TTreeNode {
return ld.nav
}
func (ld *badgerLoadedDatabase) Keepalive(ndata *navData) {
ld.arena = append(ld.arena, ndata)
}
func (ld *badgerLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) error {
// Load properties
content := fmt.Sprintf("Table statistics: %#v", ld.db.Tables()) content := fmt.Sprintf("Table statistics: %#v", ld.db.Tables())
return content, nil f.propertiesBox.SetText(content)
}
func (ld *badgerLoadedDatabase) RenderForNav(f *tableState, bucketPath []string) error {
// Load data // Load data
// Badger always uses Key + Value as the columns // Badger always uses Key + Value as the columns
f.SetupColumns([]TableColumn{&binColumn{}, &binColumn{}}, []string{"Key", "Value"}) colKey := f.contentBox.Columns().Add()
colKey.Title().SetCaption("Key")
colVal := f.contentBox.Columns().Add()
colVal.Title().SetCaption("Value")
err := ld.db.View(func(txn *badger.Txn) error { err := ld.db.View(func(txn *badger.Txn) error {
@@ -45,7 +60,11 @@ func (ld *badgerLoadedDatabase) RenderForNav(f *tableState, bucketPath []string)
item := it.Item() item := it.Item()
k := item.Key() k := item.Key()
err := item.Value(func(v []byte) error { err := item.Value(func(v []byte) error {
f.AddRow_PK_Data(k, k, v) rpos := f.contentBox.RowCount()
f.contentBox.SetRowCount(rpos + 1)
f.contentBox.SetCells(0, rpos, formatUtf8(k))
f.contentBox.SetCells(1, rpos, formatUtf8(v))
return nil return nil
}) })
if err != nil { if err != nil {
@@ -59,150 +78,82 @@ func (ld *badgerLoadedDatabase) RenderForNav(f *tableState, bucketPath []string)
} }
// Valid // Valid
f.Ready() vcl_stringgrid_columnwidths(f.contentBox)
f.contentBox.SetEnabled(true)
return nil return nil
} }
func (n *badgerLoadedDatabase) ApplyChanges(f *tableState, bucketPath []string) error { func (n *badgerLoadedDatabase) ApplyChanges(f *TMainForm, ndata *navData) error {
return n.db.Update(func(txn *badger.Txn) error { return ErrNotSupported
return kvstore_ApplyChanges(f, txn.Set, txn.Delete)
})
} }
func (ld *badgerLoadedDatabase) NavChildren(bucketPath []string) ([]string, error) { func (ld *badgerLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
return []string{}, nil // No children // In the Badger implementation, there is only one child: "Data"
if len(ndata.bucketPath) == 0 {
return []string{"Data"}, nil
} else {
// No children deeper than that
return []string{}, nil
}
} }
func (ld *badgerLoadedDatabase) NavContext(bucketPath []string) ([]contextAction, error) { func (ld *badgerLoadedDatabase) NavContext(ndata *navData) ([]contextAction, error) {
return []contextAction{ return nil, nil // No special actions are supported
{Name: "Export backup...", Callback: ld.ExportBackup},
{Name: "Import backup...", Callback: ld.ImportBackup},
{Name: "Compact", Callback: ld.CompactGC},
}, nil
} }
func (ld *badgerLoadedDatabase) ExportBackup(sender *qt.QTreeWidgetItem, bucketPath []string) error { func (ld *badgerLoadedDatabase) ExecQuery(query string, resultArea *vcl.TStringGrid) error {
saveAs := qt.QFileDialog_GetSaveFileName4(sender.TreeWidget().QWidget, "Export backup...", "", "Badger database backups (*.bak);;All files (*)") return ErrNotSupported
if saveAs == "" {
return nil
}
fh, err := os.OpenFile(saveAs, os.O_CREATE|os.O_RDWR|os.O_EXCL, 0600)
if err != nil {
return err
}
defer fh.Close()
_, err = ld.db.Backup(fh, 0)
return err
}
func (ld *badgerLoadedDatabase) ImportBackup(sender *qt.QTreeWidgetItem, bucketPath []string) error {
openPath := qt.QFileDialog_GetOpenFileName4(sender.TreeWidget().QWidget, "Import backup...", "", "Badger database backups (*.bak);;All files (*)")
if openPath == "" {
return nil
}
fh, err := os.Open(openPath)
if err != nil {
return err
}
defer fh.Close()
return ld.db.Load(fh, 16) // concurrency - just a guess
}
func (ld *badgerLoadedDatabase) CompactGC(sender *qt.QTreeWidgetItem, bucketPath []string) error {
// Move data from value log into levels ->
err := ld.db.RunValueLogGC(0.0001) // Compact if we would save 0.1% of disk space
if err != nil {
return fmt.Errorf("RunValueLogGC: %w", err)
}
// -> then flatten all levels
err = ld.db.Flatten(4)
if err != nil {
return fmt.Errorf("Flatten: %w", err)
}
return nil
} }
func (ld *badgerLoadedDatabase) Close() { func (ld *badgerLoadedDatabase) Close() {
_ = ld.db.Close() _ = ld.db.Close()
ld.arena = nil
} }
var _ loadedDatabase = &badgerLoadedDatabase{} // interface assertion var _ loadedDatabase = &badgerLoadedDatabase{} // interface assertion
var _ editableLoadedDatabase = &badgerLoadedDatabase{} // interface assertion
// //
type badgerConnection struct { func (f *TMainForm) badgerAddDatabaseFromMemory() {
Type autoconfig.OneOf f.badgerAddDatabaseFrom(badger.DefaultOptions("").WithInMemory(true))
Disk *struct {
Directory autoconfig.ExistingDirectory
Readonly bool
Encryption *encryptionKey
} `json:",omitempty"`
Memory *struct{} `json:",omitempty"`
} }
type encryptionKey struct { func (f *TMainForm) badgerAddDatabaseFromDirectory(path string) {
Method autoconfig.EnumList `yenum:"Text;;Hex;;Passphrase (SHA256 KDF to AES-256)"` f.badgerAddDatabaseFrom(badger.DefaultOptions(path))
Key autoconfig.Password
}
func (e encryptionKey) Get() ([]byte, error) {
switch e.Method {
case 0: // Text
return []byte(e.Key), nil
case 1: // Hex
// For Badger, the input must be 16/24/32 bytes for AES-128/192/256
// The library checks this, we don't need to
return hex.DecodeString(string(e.Key))
case 2: // Passphrase (SHA256 KDF to AES-256)
hasher := sha256.New()
hasher.Write([]byte(e.Key))
return hasher.Sum(nil), nil
default:
return nil, fmt.Errorf("Unsupported encoding method for encryption key")
}
}
func (bdc *badgerConnection) Connect(ctx context.Context) (loadedDatabase, string, error) {
if bdc.Disk != nil {
opts := badger.DefaultOptions(string(bdc.Disk.Directory))
opts.ReadOnly = bdc.Disk.Readonly
opts.MetricsEnabled = false
if bdc.Disk.Encryption != nil {
ehx, err := bdc.Disk.Encryption.Get()
if err != nil {
return nil, "", fmt.Errorf("Loading encryption key: %w", err)
}
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))
}
opts.EncryptionKey = ehx
} }
func (f *TMainForm) badgerAddDatabaseFrom(opts badger.Options) {
// TODO load in background thread to stop blocking the UI
db, err := badger.Open(opts) db, err := badger.Open(opts)
if err != nil { if err != nil {
return nil, "", err vcl.ShowMessage(fmt.Sprintf("Failed to load database: %s", err.Error()))
return
} }
return &badgerLoadedDatabase{db: db}, filepath.Base(string(bdc.Disk.Directory)), nil ld := &badgerLoadedDatabase{
} else { // memory db: db,
db, err := badger.Open(badger.DefaultOptions("").WithInMemory(true))
if err != nil {
return nil, "", err
} }
return &badgerLoadedDatabase{db: db}, `:memory:`, nil // SQLite-style naming if opts.Dir == "" {
ld.displayName = ":memory:" // SQLite-style naming
} else {
ld.displayName = filepath.Base(opts.Dir)
}
ld.nav = f.Buckets.Items().Add(nil, ld.displayName)
ld.nav.SetHasChildren(true) // dynamically populate in OnNavExpanding
ld.nav.SetImageIndex(imgDatabase)
ld.nav.SetSelectedIndex(imgDatabase)
navData := &navData{
ld: ld,
childrenLoaded: false, // will be loaded dynamically
bucketPath: []string{}, // empty = root
} }
ld.nav.SetData(unsafe.Pointer(navData))
f.dbs = append(f.dbs, ld)
f.Buckets.SetSelected(ld.nav) // Select new element
ld.Keepalive(navData)
} }

View File

@@ -1,113 +0,0 @@
package main
import (
"context"
"fmt"
"path/filepath"
"github.com/mappu/autoconfig"
qt "github.com/mappu/miqt/qt6"
"go.mills.io/bitcask/v2"
)
type bitcaskLdb struct {
db *bitcask.Bitcask
}
func (ld *bitcaskLdb) DriverName() string {
return "Bitcask"
}
func (ld *bitcaskLdb) Properties(bucketPath []string) (string, error) {
stats, err := ld.db.Stats()
if err != nil {
return "", err
}
return fmt.Sprintf("Database: %s\n\nStats:\n%#v\n", ld.db.Path(), stats), nil
}
func (ld *bitcaskLdb) RenderForNav(f *tableState, bucketPath []string) error {
// Load data
f.SetupColumns([]TableColumn{&binColumn{}, &binColumn{}}, []string{"Key", "Value"})
cur := ld.db.Iterator()
defer cur.Close()
for {
itm, err := cur.Next()
if err != nil {
if err == bitcask.ErrStopIteration {
break // OK
}
return err
}
f.AddRow_PK_Data([]byte(itm.Key()), []byte(itm.Key()), []byte(itm.Value()))
}
// Valid
f.Ready()
return nil
}
func (n *bitcaskLdb) ApplyChanges(f *tableState, bucketPath []string) error {
return kvstore_ApplyChanges(
f,
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 (ld *bitcaskLdb) NavChildren(bucketPath []string) ([]string, error) {
return []string{}, nil // No children
}
func (ld *bitcaskLdb) actionBackup(sender *qt.QTreeWidgetItem, bucketPath []string) error {
backupDir := qt.QFileDialog_GetExistingDirectory3(sender.TreeWidget().QWidget, APPNAME, "Select an output directory to backup to...")
if backupDir == "" {
return nil // cancelled
}
return ld.db.Backup(backupDir)
}
func (ld *bitcaskLdb) NavContext(bucketPath []string) ([]contextAction, error) {
return []contextAction{
{"Backup...", ld.actionBackup},
}, nil
}
func (ld *bitcaskLdb) Close() {
_ = ld.db.Close()
}
var _ loadedDatabase = &bitcaskLdb{} // interface assertion
var _ editableLoadedDatabase = &bitcaskLdb{} // interface assertion
//
type bitcaskDBConnection struct {
Directory autoconfig.ExistingDirectory
Readonly bool
AutoRecovery bool
}
func (c *bitcaskDBConnection) Reset() {
c.AutoRecovery = true
}
func (c *bitcaskDBConnection) Connect(ctx context.Context) (loadedDatabase, string, error) {
db, err := bitcask.Open(
string(c.Directory),
bitcask.WithOpenReadonly(c.Readonly),
bitcask.WithAutoRecovery(c.AutoRecovery),
)
if err != nil {
return nil, "", err
}
return &bitcaskLdb{db: db}, filepath.Base(string(c.Directory)), nil
}

View File

@@ -1,84 +1,65 @@
package main package main
import ( import (
"bytes"
"context"
"errors" "errors"
"fmt" "fmt"
"path/filepath" "path/filepath"
"sort" "sort"
"strings" "strings"
"time" "time"
"unsafe"
"github.com/mappu/autoconfig" "github.com/ying32/govcl/vcl"
qt "github.com/mappu/miqt/qt6"
"go.etcd.io/bbolt" "go.etcd.io/bbolt"
"go.etcd.io/bbolt/version" "go.etcd.io/bbolt/version"
) )
const (
boltFilter = "Bolt database|*.db|All files|*.*"
)
type boltLoadedDatabase struct { type boltLoadedDatabase struct {
displayName string
path string path string
db *bbolt.DB db *bbolt.DB
nav *vcl.TTreeNode
arena []*navData // keepalive
} }
type boltConfig struct { func (ld *boltLoadedDatabase) DisplayName() string {
Path autoconfig.ExistingFile `yfilter:"Bolt database (*.db);;All files (*)"` return ld.displayName
Readonly bool
}
func (bc *boltConfig) String() string { // n.b. only used for default names in connection manager
return filepath.Base(string(bc.Path))
}
func (bc *boltConfig) Connect(ctx context.Context) (loadedDatabase, string, error) {
opts := bbolt.Options{
Timeout: 1 * time.Second,
ReadOnly: bc.Readonly,
}
db, err := bbolt.Open(string(bc.Path), 0644, &opts)
if err != nil {
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{
path: string(bc.Path),
db: db,
}
return ld, displayName, nil
} }
func (ld *boltLoadedDatabase) DriverName() string { func (ld *boltLoadedDatabase) DriverName() string {
return "Bolt " + version.Version return "Bolt " + version.Version
} }
func (ld *boltLoadedDatabase) Properties(bucketPath []string) (string, error) { func (ld *boltLoadedDatabase) RootElement() *vcl.TTreeNode {
content := fmt.Sprintf("Selected database: %#v", ld.db.Stats()) return ld.nav
return content, nil
} }
func (ld *boltLoadedDatabase) RenderForNav(f *tableState, bucketPath []string) error { func (ld *boltLoadedDatabase) Keepalive(ndata *navData) {
ld.arena = append(ld.arena, ndata)
}
func (ld *boltLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) error {
// Load properties // Load properties
if len(bucketPath) == 0 { bucketDisplayName := strings.Join(ndata.bucketPath, `/`)
return nil // Can't have data outside of the top bucket content := fmt.Sprintf("Selected database: %#v\n\n\nSelected bucket: %q\n", ld.db.Stats(), bucketDisplayName)
} f.propertiesBox.SetText(content)
// Load data // Load data
// Bolt always uses Key + Value as the columns // Bolt always uses Key + Value as the columns
colKey := f.contentBox.Columns().Add()
f.SetupColumns([]TableColumn{&binColumn{}, &binColumn{}}, []string{"Key", "Value"}) colKey.Title().SetCaption("Key")
colVal := f.contentBox.Columns().Add()
colVal.Title().SetCaption("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, ndata.bucketPath)
if b == nil { if b == nil {
// no such bucket // no such bucket
return nil return nil
@@ -87,14 +68,10 @@ func (ld *boltLoadedDatabase) RenderForNav(f *tableState, bucketPath []string) e
// Valid // Valid
c := b.Cursor() c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() { for k, v := c.First(); k != nil; k, v = c.Next() {
rpos := f.contentBox.RowCount()
// Nil values mean it's a bucket f.contentBox.SetRowCount(rpos + 1)
// Hide from the data table, it will be shown in the nav instead f.contentBox.SetCells(0, rpos, formatUtf8(k))
if v == nil { f.contentBox.SetCells(1, rpos, formatUtf8(v))
continue
}
f.AddRow_PK_Data(k, k, v)
} }
return nil return nil
}) })
@@ -103,44 +80,42 @@ func (ld *boltLoadedDatabase) RenderForNav(f *tableState, bucketPath []string) e
} }
// Valid // Valid
f.Ready() vcl_stringgrid_columnwidths(f.contentBox)
f.contentBox.SetEnabled(true)
return nil return nil
} }
// kvstore_ApplyChanges is a helper function to apply edits to K/V stores that func (n *boltLoadedDatabase) ApplyChanges(f *TMainForm, ndata *navData) error {
// can use a common abstraction. if n.db.IsReadOnly() {
// It always uses the "popupData" type i.e. []byte. return errors.New("Database was opened read-only")
func kvstore_ApplyChanges(f *tableState, Put func(k, v []byte) error, Delete func(k []byte) error) error { }
// Columns are two binColumn // We have rendered row IDs, need to convert back to a bolt primary key
keyCol := f.columns[0].(*binColumn) // TODO stash the real key inside f.contentBox.Objects()
valCol := f.columns[1].(*binColumn) // FIXME breaks if you try and edit the primary key(!)
primaryKeyForRendered := func(rowid int32) []byte {
return []byte(f.contentBox.Cells(0, rowid))
}
return n.db.Update(func(tx *bbolt.Tx) error {
// Get current bucket handle
b := boltTargetBucket(tx, ndata.bucketPath)
// Edit // Edit
for rowid, _ /*editcells*/ := range f.updateRows { for rowid, _ /*editcells*/ := range f.updateRows {
k_orig := f.primaryKeys[rowid] k := primaryKeyForRendered(rowid)
k_new := keyCol.vals[rowid] v := f.contentBox.Cells(1, rowid) // There's only one value cell
v := valCol.vals[rowid] err := b.Put(k, []byte(v))
if !bytes.Equal(k_orig, k_new) {
// Editing the primary key
// Delete k_orig and only put in k_new
err := Delete(k_orig)
if err != nil { if err != nil {
return fmt.Errorf("Deleting cell %q: %w", formatUtf8(k_orig), err) return fmt.Errorf("Updating cell %q: %w", formatUtf8(k), err)
}
}
err := Put(k_new, []byte(v))
if err != nil {
return fmt.Errorf("Updating cell %q: %w", formatUtf8(k_new), err)
} }
} }
// Delete by key (affects rowids after re-render) // Delete by key (affects rowids after re-render)
for rowid, _ := range f.deleteRows { for rowid, _ := range f.deleteRows {
k := f.primaryKeys[rowid] k := primaryKeyForRendered(rowid)
err := Delete(k) err := b.Delete(k)
if err != nil { if err != nil {
return fmt.Errorf("Deleting cell %q: %w", formatUtf8(k), err) return fmt.Errorf("Deleting cell %q: %w", formatUtf8(k), err)
} }
@@ -148,13 +123,9 @@ func kvstore_ApplyChanges(f *tableState, Put func(k, v []byte) error, Delete fun
// Insert all new entries // Insert all new entries
for rowid, _ := range f.insertRows { for rowid, _ := range f.insertRows {
// Newly inserted rows will not have a valid value stashed in the k := primaryKeyForRendered(rowid)
// f.primaryKeys slice v := f.contentBox.Cells(1, rowid) // There's only one value cell
// Have to get it from the data content of the cell directly err := b.Put(k, []byte(v))
k := keyCol.vals[rowid]
v := valCol.vals[rowid] // There's only one value cell
err := Put(k, v)
if err != nil { if err != nil {
return fmt.Errorf("Inserting cell %q: %w", formatUtf8(k), err) return fmt.Errorf("Inserting cell %q: %w", formatUtf8(k), err)
} }
@@ -162,61 +133,32 @@ func kvstore_ApplyChanges(f *tableState, Put func(k, v []byte) error, Delete fun
// Done // Done
return nil return nil
}
func (n *boltLoadedDatabase) ApplyChanges(f *tableState, bucketPath []string) error {
if n.db.IsReadOnly() {
return errors.New("Database was opened read-only")
}
return n.db.Update(func(tx *bbolt.Tx) error {
// Get current bucket handle
b := boltTargetBucket(tx, bucketPath)
return kvstore_ApplyChanges(f, b.Put, b.Delete)
}) })
} }
func (ld *boltLoadedDatabase) NavChildren(bucketPath []string) ([]string, error) { func (ld *boltLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
// In the bolt implementation, the nav is a recursive tree of child buckets // In the bolt implementation, the nav is a recursive tree of child buckets
return boltChildBucketNames(ld.db, bucketPath) return boltChildBucketNames(ld.db, ndata.bucketPath)
} }
func (ld *boltLoadedDatabase) NavContext(bucketPath []string) ([]contextAction, error) { func (ld *boltLoadedDatabase) NavContext(ndata *navData) (ret []contextAction, err error) {
ret := []contextAction{ ret = append(ret, contextAction{"Add bucket...", ld.AddChildBucket})
{"Add bucket...", ld.AddChildBucket},
}
if len(bucketPath) > 0 { if len(ndata.bucketPath) > 0 {
ret = append(ret, contextAction{"Delete bucket", ld.DeleteBucket}) ret = append(ret, contextAction{"Delete bucket", ld.DeleteBucket})
} }
if len(bucketPath) == 0 { return
ret = append(ret, contextAction{"Export to .zip...", ld.exportToZip})
} }
return ret, nil func (ld *boltLoadedDatabase) AddChildBucket(sender vcl.IComponent, ndata *navData) error {
} bucketName := ""
if !vcl.InputQuery(APPNAME, "Enter a name for the new bucket:", &bucketName) {
func (ld *boltLoadedDatabase) exportToZip(sender *qt.QTreeWidgetItem, bucketPath []string) error {
// Popup for output file
savePath := qt.QFileDialog_GetSaveFileName4(sender.TreeWidget().QWidget, "Save backup as...", "", "Zip archive (*.zip);;All files (*)")
if savePath == "" {
return nil // cancelled
}
return ld.exportDatabaseToZip(savePath)
}
func (ld *boltLoadedDatabase) AddChildBucket(sender *qt.QTreeWidgetItem, bucketPath []string) error {
bucketName := qt.QInputDialog_GetText(sender.TreeWidget().QWidget, APPNAME, "Enter a name for the new bucket:")
if bucketName == "" {
return nil // cancel return nil // cancel
} }
err := ld.db.Update(func(tx *bbolt.Tx) error { err := ld.db.Update(func(tx *bbolt.Tx) error {
parent := boltTargetBucket(tx, bucketPath) parent := boltTargetBucket(tx, ndata.bucketPath)
if parent != nil { if parent != nil {
_, err := parent.CreateBucket([]byte(bucketName)) _, err := parent.CreateBucket([]byte(bucketName))
return err return err
@@ -232,43 +174,77 @@ func (ld *boltLoadedDatabase) AddChildBucket(sender *qt.QTreeWidgetItem, bucketP
return nil return nil
} }
func (ld *boltLoadedDatabase) DeleteBucket(sender *qt.QTreeWidgetItem, bucketPath []string) error { func (ld *boltLoadedDatabase) DeleteBucket(sender vcl.IComponent, ndata *navData) error {
if !vcl_confirm_dialog(sender.TreeWidget().QWidget, fmt.Sprintf("Are you sure you want to delete the bucket %q?", bucketPath[0])) {
return nil // cancelled
}
err := ld.db.Update(func(tx *bbolt.Tx) error { err := ld.db.Update(func(tx *bbolt.Tx) error {
// Find parent of this bucket. // Find parent of this bucket.
if len(bucketPath) >= 2 { if len(ndata.bucketPath) >= 2 {
// child bucket // child bucket
parent := boltTargetBucket(tx, bucketPath[0:len(bucketPath)-1]) parent := boltTargetBucket(tx, ndata.bucketPath[0:len(ndata.bucketPath)-1])
return parent.DeleteBucket([]byte(bucketPath[len(bucketPath)-1])) return parent.DeleteBucket([]byte(ndata.bucketPath[len(ndata.bucketPath)-1]))
} else { } else {
// top-level bucket // top-level bucket
return tx.DeleteBucket([]byte(bucketPath[0])) return tx.DeleteBucket([]byte(ndata.bucketPath[0]))
} }
}) })
if err != nil { if err != nil {
return fmt.Errorf("Error deleting bucket %q: %w", strings.Join(bucketPath, `/`), err) return fmt.Errorf("Error deleting bucket %q: %w", strings.Join(ndata.bucketPath, `/`), err)
} }
return nil return nil
} }
func (ld *boltLoadedDatabase) ExecQuery(query string, resultArea *vcl.TStringGrid) error {
return ErrNotSupported
}
func (ld *boltLoadedDatabase) Close() { func (ld *boltLoadedDatabase) Close() {
_ = ld.db.Close() _ = ld.db.Close()
ld.arena = nil
} }
var _ loadedDatabase = &boltLoadedDatabase{} // interface assertion var _ loadedDatabase = &boltLoadedDatabase{} // interface assertion
var _ editableLoadedDatabase = &boltLoadedDatabase{} // interface assertion
// //
// boltTargetBucket resolves the bucketPath to a specific bolt.Bucket. func (f *TMainForm) boltAddDatabaseFromFile(path string, readonly bool) {
// If the path is empty, this function returns <nil> and you must manually access // TODO load in background thread to stop blocking the UI
// the root bucket.
// If the bucket does not exist, this function returns <nil>. It does not create opts := bbolt.Options{
// the bucket. Timeout: 1 * time.Second,
ReadOnly: readonly,
}
db, err := bbolt.Open(path, 0644, &opts)
if err != nil {
vcl.ShowMessage(fmt.Sprintf("Failed to load database '%s': %s", path, err.Error()))
return
}
ld := &boltLoadedDatabase{
path: path,
displayName: filepath.Base(path),
db: db,
}
if readonly {
ld.displayName += " (read-only)"
}
ld.nav = f.Buckets.Items().Add(nil, ld.displayName)
ld.nav.SetHasChildren(true) // dynamically populate in OnNavExpanding
ld.nav.SetImageIndex(imgDatabase)
ld.nav.SetSelectedIndex(imgDatabase)
navData := &navData{
ld: ld,
childrenLoaded: false, // will be loaded dynamically
bucketPath: []string{}, // empty = root
}
ld.nav.SetData(unsafe.Pointer(navData))
f.dbs = append(f.dbs, ld)
f.Buckets.SetSelected(ld.nav) // Select new element
ld.Keepalive(navData)
}
func boltTargetBucket(tx *bbolt.Tx, path []string) *bbolt.Bucket { func boltTargetBucket(tx *bbolt.Tx, path []string) *bbolt.Bucket {
// If we are already deep in buckets, go directly there to find children // If we are already deep in buckets, go directly there to find children

View File

@@ -1,230 +0,0 @@
package main
import (
"archive/zip"
"bytes"
"fmt"
"io"
"io/fs"
"os"
"path"
"strings"
"github.com/mappu/autoconfig"
qt "github.com/mappu/miqt/qt6"
"go.etcd.io/bbolt"
)
func (ld *boltLoadedDatabase) exportDatabaseToZip(zippath string) error {
fh, err := os.OpenFile(zippath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0644)
if err != nil {
return fmt.Errorf("Error opening output file: %w", err)
}
defer fh.Close()
zw := zip.NewWriter(fh)
// Filenames in zip files cannot contain `/` characters. Mangle it
// TODO undo this transfomation on import(!)
safename := func(n string) string {
return strings.ReplaceAll(string(n), `/`, `__`)
}
err = ld.db.View(func(tx *bbolt.Tx) error {
var process func(currentPath []string) error
process = func(currentPath []string) error {
// Create folder-entry for our own bucket
ourFolderName := path.Join(slice_apply(currentPath, safename)...)
ourBucket := zip.FileHeader{
Name: ourFolderName + `/`, // Trailing slash = directory
}
ourBucket.SetMode(fs.ModeDir | 0755)
_, err := zw.CreateHeader(&ourBucket)
if err != nil {
return err
}
// Create file entries for all non-bucket children
b := boltTargetBucket(tx, currentPath)
var childBuckets []string
var c *bbolt.Cursor
if b != nil {
c = b.Cursor() // in bucket
} else {
c = tx.Cursor()
}
for k, v := c.First(); k != nil; k, v = c.Next() {
if v == nil {
// That's a bucket
childBuckets = append(childBuckets, string(k))
continue
}
fileItem := zip.FileHeader{
Name: path.Join(ourFolderName, safename(string(k))),
}
fileItem.SetMode(0644)
fileW, err := zw.CreateHeader(&fileItem)
if err != nil {
return err
}
_, err = io.CopyN(fileW, bytes.NewReader(v), int64(len(v)))
if err != nil {
return err
}
}
// Recurse for all bucket-type children
for _, childBucketName := range childBuckets {
process(slice_and(currentPath, childBucketName))
}
// Done
return nil
}
return process([]string{})
})
if err != nil {
return err
}
err = zw.Flush()
if err != nil {
return err
}
err = zw.Close()
if err != nil {
return err
}
return fh.Close()
}
func (f *App) Bolt_ImportZipToDatabase_OnTriggered() {
zippath := qt.QFileDialog_GetOpenFileName4(f.ui.MainWindow.QWidget, "Select a zip archive to import...", "", "Zip archives (*.zip);;All files (*)")
if zippath == "" {
return // cancelled
}
dbpath := qt.QFileDialog_GetSaveFileName4(f.ui.MainWindow.QWidget, "Select an output file to save as...", "", "Bolt database (*.db);;All files (*)")
if dbpath == "" {
return // cancelled
}
err := Bolt_ImportZipToDatabase(dbpath, zippath)
if err != nil {
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, err.Error())
return
}
res := qt.QMessageBox_Question2(f.ui.MainWindow.QWidget, APPNAME, "The import was successful. Would you like to open the Bolt database now?", qt.QMessageBox__Yes, qt.QMessageBox__No)
if res != int(qt.QMessageBox__Yes) {
return
}
config := NewConnectionConfig()
config.Type = "Bolt"
config.Bolt = &boltConfig{
Path: autoconfig.ExistingFile(dbpath),
Readonly: false,
}
f.showConnectDialog(config)
}
func Bolt_ImportZipToDatabase(dbpath, zippath string) error {
db, err := bbolt.Open(dbpath, 0644, nil)
if err != nil {
return fmt.Errorf("Opening target database: %w", err)
}
defer db.Close()
fh, err := os.OpenFile(zippath, os.O_RDONLY, 0400)
if err != nil {
return fmt.Errorf("Opening input archive: %w", err)
}
defer fh.Close()
fstat, err := fh.Stat()
if err != nil {
return err
}
zr, err := zip.NewReader(fh, fstat.Size())
if err != nil {
return fmt.Errorf("Reading zip file format: %w", err)
}
return db.Update(func(tx *bbolt.Tx) error {
for _, zf := range zr.File {
if strings.HasSuffix(zf.Name, `/`) || (zf.Mode()&fs.ModeDir) != 0 {
// Bucket
if zf.Name == `/` {
continue // virtual entry for top-level directory, skip
}
bucketPath := strings.Split(strings.TrimSuffix(zf.Name, `/`), `/`)
parentBucket := boltTargetBucket(tx, bucketPath[0:len(bucketPath)-1])
newBucketName := []byte(bucketPath[len(bucketPath)-1])
var err error
if parentBucket == nil {
_, err = tx.CreateBucket(newBucketName) // at top level
} else {
_, err = parentBucket.CreateBucket(newBucketName) // child bucket
}
if err != nil {
return fmt.Errorf("Creating bucket %q: %w", zf.Name, err)
}
} else {
// Object
objectPath := strings.Split(zf.Name, `/`)
rc, err := zf.Open()
if err != nil {
return err
}
content, err := io.ReadAll(rc)
if err != nil {
return err
}
parentBucket := boltTargetBucket(tx, objectPath[0:len(objectPath)-1]) // Can't be nil, items always exist within a bucket
objectKey := []byte(objectPath[len(objectPath)-1])
err = parentBucket.Put(objectKey, content)
if err != nil {
return err
}
err = rc.Close()
if err != nil {
return err
}
}
}
// Done
return nil
})
}

View File

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

View File

@@ -1,121 +1,140 @@
package main package main
import ( import (
"context"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "unsafe"
"qbolt/debconf" "yvbolt/debconf"
"github.com/mappu/autoconfig" "github.com/ying32/govcl/vcl"
) )
type debconfLoadedDatabase struct { type debconfLoadedDatabase struct {
displayName string
db *debconf.Database db *debconf.Database
nav *vcl.TTreeNode
arena []*navData // keepalive
}
func (ld *debconfLoadedDatabase) DisplayName() string {
return ld.displayName
} }
func (ld *debconfLoadedDatabase) DriverName() string { func (ld *debconfLoadedDatabase) DriverName() string {
return "debconf" return "debconf"
} }
func (ld *debconfLoadedDatabase) Properties(bucketPath []string) (string, error) { func (ld *debconfLoadedDatabase) RootElement() *vcl.TTreeNode {
content := fmt.Sprintf("Applications: %d\nUnique attributes: %d\n", len(ld.db.Entries), len(ld.db.AllColumnNames)) return ld.nav
return content, nil
} }
func (ld *debconfLoadedDatabase) RenderForNav(f *tableState, bucketPath []string) error { func (ld *debconfLoadedDatabase) Keepalive(ndata *navData) {
ld.arena = append(ld.arena, ndata)
}
func (ld *debconfLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) error {
// Load properties
content := fmt.Sprintf("Entries: %d\nUnique attributes: %d\n", len(ld.db.Entries), len(ld.db.AllColumnNames))
f.propertiesBox.SetText(content)
// Load data // Load data
if len(bucketPath) == 0 {
return nil // No data at root level
}
// Find application entry for bucketPath
appInfo, ok := ld.db.FindApplicationByName(bucketPath[0])
if !ok {
return fmt.Errorf("Invalid application %q", bucketPath[0])
}
// Process entries for specific application
indexes := make(map[string]int) indexes := make(map[string]int)
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
col := f.contentBox.Columns().Add()
col.Title().SetCaption(cname)
} }
for _, entry := range appInfo.Entries { for _, entry := range ld.db.Entries {
rpos := f.AddRow() rpos := f.contentBox.RowCount()
f.SetCell(rpos, 0, entry.Name) f.contentBox.SetRowCount(rpos + 1)
f.contentBox.SetCells(0, rpos, entry.Name)
for _, proppair := range entry.Properties { for _, proppair := range entry.Properties {
f.SetCell(rpos, indexes[proppair[0]], proppair[1]) f.contentBox.SetCells(int32(indexes[proppair[0]]), rpos, proppair[1])
} }
} }
// Valid // Valid
f.Ready() vcl_stringgrid_columnwidths(f.contentBox)
f.contentBox.SetEnabled(true)
return nil return nil
} }
func (ld *debconfLoadedDatabase) NavChildren(bucketPath []string) ([]string, error) { func (n *debconfLoadedDatabase) ApplyChanges(f *TMainForm, ndata *navData) error {
if len(bucketPath) == 0 { return ErrNotSupported
ret := make([]string, 0, len(ld.db.Entries))
for _, app := range ld.db.Entries {
ret = append(ret, app.Name)
} }
return ret, nil
func (ld *debconfLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
// In the debconf implementation, there is only one child: "Data"
if len(ndata.bucketPath) == 0 {
return []string{"Data"}, nil
} else { } else {
return []string{}, nil // No further children // No children deeper than that
return []string{}, nil
} }
} }
func (ld *debconfLoadedDatabase) NavContext(bucketPath []string) ([]contextAction, error) { func (ld *debconfLoadedDatabase) NavContext(ndata *navData) ([]contextAction, error) {
return nil, nil // No special actions are supported return nil, nil // No special actions are supported
} }
func (ld *debconfLoadedDatabase) ExecQuery(query string, resultArea *vcl.TStringGrid) error {
return ErrNotSupported
}
func (ld *debconfLoadedDatabase) Close() { func (ld *debconfLoadedDatabase) Close() {
ld.arena = nil
} }
var _ loadedDatabase = &debconfLoadedDatabase{} // interface assertion var _ loadedDatabase = &debconfLoadedDatabase{} // interface assertion
// //
// func (f *TMainForm) debconfAddDatabaseFrom(path string) {
// TODO load in background thread to stop blocking the UI
type debconfConnection struct { fh, err := os.OpenFile(path, os.O_RDONLY, 0400)
Database autoconfig.ExistingFile `yfilter:"Debconf database (*.dat);;All files (*)"`
}
func (dc *debconfConnection) Reset() {
if runtime.GOOS == "linux" {
dc.Database = "/var/cache/debconf/config.dat" // Prefill default path
}
}
func (dc *debconfConnection) Connect(ctx context.Context) (loadedDatabase, string, error) {
fh, err := os.OpenFile(string(dc.Database), os.O_RDONLY, 0400)
if err != nil { if err != nil {
return nil, "", err vcl.ShowMessage(fmt.Sprintf("Failed to load database: %s", err.Error()))
return
} }
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 vcl.ShowMessage(fmt.Sprintf("Failed to load database: %s", err.Error()))
return
} }
return &debconfLoadedDatabase{db: db}, filepath.Base(string(dc.Database)), nil ld := &debconfLoadedDatabase{
db: db,
displayName: filepath.Base(path),
}
ld.nav = f.Buckets.Items().Add(nil, ld.displayName)
ld.nav.SetHasChildren(true) // dynamically populate in OnNavExpanding
ld.nav.SetImageIndex(imgDatabase)
ld.nav.SetSelectedIndex(imgDatabase)
navData := &navData{
ld: ld,
childrenLoaded: false, // will be loaded dynamically
bucketPath: []string{}, // empty = root
}
ld.nav.SetData(unsafe.Pointer(navData))
f.dbs = append(f.dbs, ld)
f.Buckets.SetSelected(ld.nav) // Select new element
ld.Keepalive(navData)
} }

View File

@@ -1,61 +0,0 @@
package main
import (
"context"
"errors"
"fmt"
"runtime/debug"
)
type evLdb struct {
mods *debug.BuildInfo
}
func (ld *evLdb) DriverName() string {
return APPNAME + " " + appVersion
}
func (ld *evLdb) Properties(bucketPath []string) (string, error) {
return fmt.Sprintf(
"%s %s\n- %d package dependencies\n- Compiler: %s",
APPNAME, appVersion, len(ld.mods.Deps), ld.mods.GoVersion,
), nil
}
func (ld *evLdb) RenderForNav(f *tableState, bucketPath []string) error {
f.SetupColumns([]TableColumn{&goModuleColumn{}, &stringColumn{}, &stringColumn{}}, []string{"Library", "Version", "Hash"})
for _, dep := range ld.mods.Deps {
f.AddRowData(dep.Path, dep.Version, dep.Sum)
}
// Valid
f.Ready()
return nil
}
func (ld *evLdb) NavChildren(bucketPath []string) ([]string, error) {
return []string{}, nil // No further children
}
func (ld *evLdb) NavContext(bucketPath []string) ([]contextAction, error) {
return nil, nil // No special actions are supported
}
func (ld *evLdb) Close() {}
var _ loadedDatabase = &evLdb{} // interface assertion
//
type evLdbConnection struct{}
func (dc *evLdbConnection) Connect(ctx context.Context) (loadedDatabase, string, error) {
mods, ok := debug.ReadBuildInfo()
if !ok {
return nil, "", errors.New("Missing build info")
}
return &evLdb{mods: mods}, APPNAME, nil
}

View File

@@ -1,102 +0,0 @@
package main
import (
"context"
"fmt"
"path/filepath"
"github.com/mappu/autoconfig"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/opt"
)
type leveldbLoadedDatabase struct {
db *leveldb.DB
}
func (ld *leveldbLoadedDatabase) DriverName() string {
return "LevelDB"
}
func (ld *leveldbLoadedDatabase) Properties(bucketPath []string) (string, error) {
var s leveldb.DBStats
err := ld.db.Stats(&s)
if err != nil {
return "", fmt.Errorf("Stats: %w", err)
}
content := fmt.Sprintf("LevelDB stats: %#v", s)
return content, nil
}
func (ld *leveldbLoadedDatabase) RenderForNav(f *tableState, bucketPath []string) error {
// Load data
// leveldb always uses Key + Value as the columns
f.SetupColumns([]TableColumn{&binColumn{}, &binColumn{}}, []string{"Key", "Value"})
itn := ld.db.NewIterator(nil, nil)
defer itn.Release()
for itn.Next() {
f.AddRow_PK_Data(itn.Key(), itn.Key(), itn.Value())
}
// Valid
f.Ready()
return nil
}
func (n *leveldbLoadedDatabase) ApplyChanges(f *tableState, bucketPath []string) error {
txn, err := n.db.OpenTransaction()
if err != nil {
return fmt.Errorf("OpenTransaction: %w", err)
}
err = kvstore_ApplyChanges(
f,
func(k, v []byte) error { return txn.Put(k, v, nil) },
func(k []byte) error { return txn.Delete(k, nil) },
)
if err != nil {
txn.Discard()
return err
}
return txn.Commit()
}
func (ld *leveldbLoadedDatabase) NavChildren(bucketPath []string) ([]string, error) {
return []string{}, nil // No children
}
func (ld *leveldbLoadedDatabase) NavContext(bucketPath []string) ([]contextAction, error) {
return nil, nil // No special actions are supported
}
func (ld *leveldbLoadedDatabase) Close() {
_ = ld.db.Close()
}
var _ loadedDatabase = &leveldbLoadedDatabase{} // interface assertion
var _ editableLoadedDatabase = &leveldbLoadedDatabase{} // interface assertion
//
type leveldbConnection struct {
Directory autoconfig.ExistingDirectory
Readonly bool
}
func (pdc *leveldbConnection) Connect(ctx context.Context) (loadedDatabase, string, error) {
var o opt.Options
o.ReadOnly = pdc.Readonly
db, err := leveldb.OpenFile(string(pdc.Directory), &o)
if err != nil {
return nil, "", err
}
return &leveldbLoadedDatabase{db: db}, filepath.Base(string(pdc.Directory)), nil
}

View File

@@ -1,284 +0,0 @@
package main
import (
"context"
"fmt"
"path/filepath"
"github.com/ledgerwatch/lmdb-go/lmdb"
"github.com/mappu/autoconfig"
qt "github.com/mappu/miqt/qt6"
)
type lmdbDatabase struct {
db *lmdb.Env
isMulti bool
}
func (ld *lmdbDatabase) DriverName() string {
return lmdb.VersionString() // Already includes "LMDB" prefix
}
func (ld *lmdbDatabase) Properties(bucketPath []string) (string, error) {
info, err := ld.db.Info()
if err != nil {
return "", err
}
return fmt.Sprintf("LMDB info: %#v", info), nil
}
func (ld *lmdbDatabase) RenderForNav(f *tableState, bucketPath []string) error {
// Load data
if ld.isMulti && len(bucketPath) == 0 {
// In multi-database mode, the only things in the root DB are keys
// naming the child databases
// Show no data, it will be shown in the nav area instead
return nil
}
// LMDB always uses Key + Value as the columns
f.SetupColumns([]TableColumn{&binColumn{}, &binColumn{}}, []string{"Key", "Value"})
err := ld.db.View(func(txn *lmdb.Txn) error {
var dbi lmdb.DBI
var err error
if len(bucketPath) == 0 {
dbi, err = txn.OpenRoot(0)
} else {
dbi, err = txn.OpenDBI(bucketPath[0], 0)
}
if err != nil {
return err
}
// defer ld.db.CloseDBI(dbi)
itn, err := txn.OpenCursor(dbi)
defer itn.Close()
if err != nil {
return err
}
// FIXME is this correct?
kbuff := make([]byte, ld.db.MaxKeySize())
vbuff := make([]byte, 4096)
var op uint = lmdb.First
for {
key, val, err := itn.Get(kbuff, vbuff, op)
if err != nil {
if lmdb.IsNotFound(err) {
break // reached end of iteration
}
return err // a real error
}
op = lmdb.Next
f.AddRow_PK_Data(key, key, val)
}
// Done
return nil
})
if err != nil {
return err
}
// Valid
f.Ready()
return nil
}
func (ld *lmdbDatabase) ApplyChanges(f *tableState, bucketPath []string) error {
return ld.db.Update(func(txn *lmdb.Txn) error {
var dbi lmdb.DBI
var err error
if len(bucketPath) == 0 {
dbi, err = txn.OpenRoot(0)
} else {
dbi, err = txn.OpenDBI(bucketPath[0], 0)
}
if err != nil {
return err
}
return kvstore_ApplyChanges(
f,
func(k, v []byte) error { return txn.Put(dbi, k, v, 0) },
func(k []byte) error { return txn.Del(dbi, k, nil) },
)
})
}
func (ld *lmdbDatabase) NavChildren(bucketPath []string) ([]string, error) {
// """To use named databases (with name != NULL), mdb_env_set_maxdbs() must
// be called before opening the environment. Database names are keys in the
// unnamed database, and may be read but not written."""
if len(bucketPath) == 0 && ld.isMulti {
// Read all keys from root DB
var allKeys []string
err := ld.db.View(func(txn *lmdb.Txn) error {
dbi, err := txn.OpenRoot(0)
if err != nil {
return err
}
//defer ld.db.CloseDBI(dbi)
itn, err := txn.OpenCursor(dbi)
defer itn.Close()
if err != nil {
return err
}
kbuff := make([]byte, ld.db.MaxKeySize())
var op uint = lmdb.First
for {
key, _, err := itn.Get(kbuff, nil, op)
if err != nil {
if lmdb.IsNotFound(err) {
break // reached end of iteration
}
return err // a real error
}
op = lmdb.Next
allKeys = append(allKeys, string(key))
}
// Done
return nil
})
return allKeys, err
} else {
return []string{}, nil // No children
}
}
func (ld *lmdbDatabase) createChildDatabase(sender *qt.QTreeWidgetItem, bucketPath []string) error {
// Prompt for child database name
ok := false
childDbName := qt.QInputDialog_GetText4(sender.TreeWidget().QWidget, APPNAME, "Enter a name for the child database:", qt.QLineEdit__Normal, "", &ok)
if !ok {
return nil // Cancelled
}
return ld.db.Update(func(txn *lmdb.Txn) error {
_, err := txn.CreateDBI(childDbName)
return err
})
}
func (ld *lmdbDatabase) truncateAllContent(sender *qt.QTreeWidgetItem, bucketPath []string) error {
if !vcl_confirm_dialog(sender.TreeWidget().QWidget, fmt.Sprintf("Are you sure you want to empty the child database %q?", bucketPath[0])) {
return nil // cancelled
}
return ld.drop(bucketPath[0], true)
}
func (ld *lmdbDatabase) deleteChildDatabase(sender *qt.QTreeWidgetItem, bucketPath []string) error {
if !vcl_confirm_dialog(sender.TreeWidget().QWidget, fmt.Sprintf("Are you sure you want to delete the child database %q?", bucketPath[0])) {
return nil // cancelled
}
return ld.drop(bucketPath[0], true)
}
func (ld *lmdbDatabase) drop(multiDbName string, delet bool) error {
return ld.db.Update(func(txn *lmdb.Txn) error {
dbi, err := txn.OpenDBI(multiDbName, 0)
if err != nil {
return err
}
return txn.Drop(dbi, true)
})
}
func (ld *lmdbDatabase) NavContext(bucketPath []string) ([]contextAction, error) {
if ld.isMulti {
// Allow create/delete databases
if len(bucketPath) == 0 {
return []contextAction{
contextAction{"Add child database...", ld.createChildDatabase},
}, nil
} else {
return []contextAction{
contextAction{"Truncate and remove all contents...", ld.truncateAllContent},
contextAction{"Remove child database...", ld.deleteChildDatabase},
}, nil
}
}
return nil, nil // No special actions are supported
}
func (ld *lmdbDatabase) Close() {
_ = ld.db.Close()
}
var _ loadedDatabase = &lmdbDatabase{} // interface assertion
var _ editableLoadedDatabase = &lmdbDatabase{} // interface assertion
//
type lmdbConnection struct {
Storage struct {
Type autoconfig.OneOf
Directory *autoconfig.ExistingDirectory `json:",omitempty"`
File *struct {
Path autoconfig.ExistingFile `yfilter:"LMDB database (*.mdb);;All files (*)"`
} `json:",omitempty"`
}
MultiDB *struct {
Slots int
} `ylabel:"Multiple databases mode"`
Readonly bool
}
func (pdc *lmdbConnection) Connect(ctx context.Context) (loadedDatabase, string, error) {
env, err := lmdb.NewEnv()
if err != nil {
return nil, "", err
}
var openPath string
var flags uint = 0
if pdc.Readonly {
flags |= lmdb.Readonly
}
if pdc.Storage.Directory != nil {
openPath = string(*pdc.Storage.Directory)
} else if pdc.Storage.File != nil {
openPath = string(pdc.Storage.File.Path)
flags |= lmdb.NoSubdir
}
isMulti := false
if pdc.MultiDB != nil {
env.SetMaxDBs(pdc.MultiDB.Slots)
isMulti = true
}
err = env.Open(openPath, flags, 0644)
if err != nil {
_ = env.Close()
return nil, "", err
}
return &lmdbDatabase{db: env, isMulti: isMulti}, filepath.Base(openPath), nil
}

View File

@@ -1,88 +0,0 @@
package main
import (
"context"
"fmt"
"path/filepath"
"github.com/lotusdblabs/lotusdb/v2"
"github.com/mappu/autoconfig"
)
type lotusLdb struct {
db *lotusdb.DB
}
func (ld *lotusLdb) DriverName() string {
return "LotusDB"
}
func (ld *lotusLdb) Properties(bucketPath []string) (string, error) {
return "", nil
}
func (ld *lotusLdb) RenderForNav(f *tableState, bucketPath []string) error {
// Load data
f.SetupColumns([]TableColumn{&binColumn{}, &binColumn{}}, []string{"Key", "Value"})
cur, err := ld.db.NewIterator(lotusdb.IteratorOptions{})
if err != nil {
return fmt.Errorf("NewIterator: %w", err)
}
defer cur.Close()
for cur.Valid() {
f.AddRow_PK_Data(cur.Key(), cur.Key(), cur.Value())
cur.Next()
}
// Valid
f.Ready()
return nil
}
func (n *lotusLdb) ApplyChanges(f *tableState, bucketPath []string) error {
txn := n.db.NewBatch(lotusdb.DefaultBatchOptions)
err := kvstore_ApplyChanges(f, txn.Put, txn.Delete)
if err != nil {
return err
}
return txn.Commit()
}
func (ld *lotusLdb) NavChildren(bucketPath []string) ([]string, error) {
return []string{}, nil // No children
}
func (ld *lotusLdb) NavContext(bucketPath []string) ([]contextAction, error) {
return nil, nil // No special actions are supported
}
func (ld *lotusLdb) Close() {
_ = ld.db.Close()
}
var _ loadedDatabase = &lotusLdb{} // interface assertion
var _ editableLoadedDatabase = &lotusLdb{} // interface assertion
//
type lotusDBConnection struct {
Directory autoconfig.ExistingDirectory
}
func (ldc *lotusDBConnection) Connect(ctx context.Context) (loadedDatabase, string, error) {
opts := lotusdb.DefaultOptions // copy
opts.DirPath = string(ldc.Directory)
db, err := lotusdb.Open(opts)
if err != nil {
return nil, "", err
}
return &lotusLdb{db: db}, filepath.Base(string(ldc.Directory)), nil
}

View File

@@ -1,348 +0,0 @@
package main
import (
"context"
"errors"
"fmt"
"time"
"golang.org/x/crypto/ssh"
"github.com/mappu/autoconfig"
qt "github.com/mappu/miqt/qt6"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
"go.mongodb.org/mongo-driver/v2/mongo/options"
"go.mongodb.org/mongo-driver/v2/mongo/readpref"
"go.mongodb.org/mongo-driver/v2/version"
)
// MongoDB support
// To test: `make test-mongo`
type mongoLdb struct {
client *mongo.Client
sshc *ssh.Client
}
func (ld *mongoLdb) DriverName() string {
return "MongoDB " + version.Driver
}
func (ld *mongoLdb) Properties(bucketPath []string) (string, error) {
ctx := context.Background()
if len(bucketPath) == 0 {
return "", nil // no properties
} else if len(bucketPath) == 1 {
// Database is selected
db := ld.client.Database(bucketPath[0])
return "Database " + db.Name(), nil
} else if len(bucketPath) == 2 {
// Collection is selected
db := ld.client.Database(bucketPath[0])
coll := db.Collection(bucketPath[1])
info := "Database " + db.Name() + "\n"
info += "Collection " + coll.Name() + "\n"
// Document count
count, err := coll.EstimatedDocumentCount(ctx)
if err != nil {
return "", fmt.Errorf("Estimating document count: %w", err)
}
info += fmt.Sprintf("Estimated document count: %d", count) + "\n"
// Index info
allIndexes, err := coll.Indexes().ListSpecifications(ctx)
if err != nil {
return "", fmt.Errorf("Checking indexes: %w", err)
}
info += fmt.Sprintf("\nIndexes (%d):\n", len(allIndexes))
for _, idxInfo := range allIndexes {
info += fmt.Sprintf("- %q (namespace=%q, version=%d)\n", idxInfo.Name, idxInfo.Namespace, idxInfo.Version)
}
return info, nil
} else {
return "", errors.New("??")
}
}
func (ld *mongoLdb) RenderForNav(f *tableState, bucketPath []string) error {
ctx := context.Background()
if len(bucketPath) == 0 || len(bucketPath) == 1 {
// Leave the table disabled
return nil
} else if len(bucketPath) == 2 {
f.SetupColumns([]TableColumn{&stringColumn{}, &bsonColumn{}}, []string{"_id", "Document"})
db := ld.client.Database(bucketPath[0])
coll := db.Collection(bucketPath[1])
cur, err := coll.Find(ctx, bson.D{}) // An empty document as filter = find all results
if err != nil {
return fmt.Errorf("Find: %w", err)
}
defer cur.Close(ctx)
return ld.populateRows(ctx, cur, f)
} else {
return errors.New("??")
}
}
func (ld *mongoLdb) populateRows(ctx context.Context, cur *mongo.Cursor, f *tableState) error {
for cur.Next(ctx) {
var result bson.D
if err := cur.Decode(&result); err != nil {
return fmt.Errorf("Decode: %w", err)
}
// The document is always an ordered map[string]any.
// MongoDB enforces there is an "_id" key.
idValue, ok := bson_find_id(result)
if !ok {
return errors.New("Surprised to find a document missing an '_id' field")
}
f.AddRow_PK_Data([]byte(idValue), idValue, result)
}
if err := cur.Err(); err != nil {
return fmt.Errorf("Cursor: %w", err)
}
// Done
f.Ready()
return nil
}
func (ld *mongoLdb) NavChildren(bucketPath []string) ([]string, error) {
ctx := context.Background()
if len(bucketPath) == 0 {
return ld.client.ListDatabaseNames(ctx, bson.D{})
} else if len(bucketPath) == 1 {
db := ld.client.Database(bucketPath[0])
return db.ListCollectionNames(ctx, bson.D{})
}
return []string{}, nil // No children
}
func (ld *mongoLdb) actionNewDatabase(sender *qt.QTreeWidgetItem, bucketPath []string) error {
ctx := context.Background()
dbName := qt.QInputDialog_GetText(sender.TreeWidget().QWidget, APPNAME, "Enter a name for the new database:")
if dbName == "" {
return nil // cancel
}
// MongoDB: databases just start to exist when you write to them
newDb := ld.client.Database(dbName)
return newDb.CreateCollection(ctx, "my.new.collection")
}
func (ld *mongoLdb) actionNewCollection(sender *qt.QTreeWidgetItem, bucketPath []string) error {
ctx := context.Background()
collName := qt.QInputDialog_GetText(sender.TreeWidget().QWidget, APPNAME, "Enter a name for the new collection:")
if collName == "" {
return nil // cancel
}
// MongoDB: databases just start to exist when you write to them
newDb := ld.client.Database(bucketPath[0])
return newDb.CreateCollection(ctx, collName)
}
func (ld *mongoLdb) actionDropDatabase(sender *qt.QTreeWidgetItem, bucketPath []string) error {
ctx := context.Background()
if !vcl_confirm_dialog(sender.TreeWidget().QWidget, fmt.Sprintf("Are you sure you want to drop the database %q?", bucketPath[0])) {
return nil // cancelled
}
db := ld.client.Database(bucketPath[0])
return db.Drop(ctx)
}
func (ld *mongoLdb) actionDropCollection(sender *qt.QTreeWidgetItem, bucketPath []string) error {
ctx := context.Background()
if !vcl_confirm_dialog(sender.TreeWidget().QWidget, fmt.Sprintf("Are you sure you want to delete the collection %q?", bucketPath[1])) {
return nil // cancelled
}
db := ld.client.Database(bucketPath[0])
coll := db.Collection(bucketPath[1])
return coll.Drop(ctx)
}
func (ld *mongoLdb) ExecQuery(query string, bucketPath []string, resultArea *tableState) error {
ctx := context.Background()
if len(bucketPath) == 0 {
return errors.New("Please select a database first.")
}
db := ld.client.Database(bucketPath[0])
// The query should be JSON, e.g.
// { "hello": 1 } or
// { "explain": { "count": "system.users" } } or
// { "listDatabases": 1 }
doc := bson.D{}
err := doc.UnmarshalJSON([]byte(query))
if err != nil {
return fmt.Errorf("Parsing JSON query: %w", err)
}
// FIXME how to tell if the response will have a cursor or a singleResult?
if false {
// Cursor
cur, err := db.RunCommandCursor(ctx, doc)
if err != nil {
return fmt.Errorf("Running command: %w", err)
}
defer cur.Close(ctx)
return ld.populateRows(ctx, cur, resultArea)
} else {
// Single result
res := db.RunCommand(ctx, doc)
response := bson.D{}
err = res.Decode(&response)
if err != nil {
return fmt.Errorf("Decoding response: %w", err)
}
responseJson, err := response.MarshalJSON()
if err != nil {
return fmt.Errorf("Decoding response: %w", err)
}
resultArea.SetupColumns([]TableColumn{&binColumn{}}, []string{"Response"})
resultArea.AddRowData(responseJson)
resultArea.Ready()
return nil
}
}
func (ld *mongoLdb) NavContext(bucketPath []string) ([]contextAction, error) {
if len(bucketPath) == 0 {
// Top-level connection
return []contextAction{
{"Create database...", ld.actionNewDatabase},
}, nil
} else if len(bucketPath) == 1 {
// Database selected
return []contextAction{
{"Create collection...", ld.actionNewCollection},
{"Drop database...", ld.actionDropDatabase},
}, nil
} else if len(bucketPath) == 2 {
// Collection selected
return []contextAction{
{"Drop collection...", ld.actionDropCollection},
}, nil
} else {
return nil, errors.New("???")
}
}
func (ld *mongoLdb) Close() {
_ = ld.client.Disconnect(context.Background())
if ld.sshc != nil {
_ = ld.sshc.Close()
}
}
var _ loadedDatabase = &mongoLdb{} // interface assertion
var _ queryableLoadedDatabase = &mongoLdb{} // interface assertion
//
type mongoConnection struct {
Conn struct {
Mode autoconfig.OneOf
Connection_String *string `json:",omitempty"`
}
SSH_Tunnel *SSHTunnel
}
func (moc *mongoConnection) Reset() {
moc.Conn.Mode = "Connection_String"
moc.Conn.Connection_String = address_of("mongodb://localhost:27017")
}
func (moc *mongoConnection) Connect(ctx context.Context) (loadedDatabase, string, error) {
ctx, _ = context.WithTimeout(ctx, 5*time.Second) // shadow parent ctx
opts := options.Client().ApplyURI(string(*moc.Conn.Connection_String))
// Our used library supports all compressors
opts.SetCompressors([]string{"zstd", "snappy", "zlib"})
ret := mongoLdb{}
if moc.SSH_Tunnel != nil {
sshc, err := moc.SSH_Tunnel.Open(ctx)
if err != nil {
return nil, "", err
}
opts.Dialer = sshc // interface implements DialContext()
ret.sshc = sshc
// The crypto/ssh library does not support deadlines over tcp tunnels
// Go-redis has a workaround for this, but go-mongo does not
}
//
client, err := mongo.Connect(opts)
if err != nil {
return nil, "", err
}
ret.client = client
// Connect() does not block for server discovery. Check that the server really
// is reachable
err = client.Ping(ctx, readpref.Primary())
if err != nil {
return nil, "", err
}
// We should be able to ListDatabases - there may be an authentication error
_, err = client.ListDatabaseNames(ctx, bson.D{})
if err != nil {
return nil, "", err
}
return &ret, "MongoDB", nil
}

View File

@@ -1,24 +1,45 @@
package main package main
import (
"github.com/ying32/govcl/vcl"
)
type noLoadedDatabase struct{} type noLoadedDatabase struct{}
func (n *noLoadedDatabase) DisplayName() string {
return APPNAME
}
func (n *noLoadedDatabase) DriverName() string { func (n *noLoadedDatabase) DriverName() string {
return "No database selected" return "No database selected"
} }
func (n *noLoadedDatabase) Properties(bucketPath []string) (string, error) { func (n *noLoadedDatabase) RootElement() *vcl.TTreeNode {
return "Open a database to get started...", nil
}
func (n *noLoadedDatabase) RenderForNav(f *tableState, bucketPath []string) error {
return nil return nil
} }
func (n *noLoadedDatabase) NavChildren(bucketPath []string) ([]string, error) { func (n *noLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) error {
f.propertiesBox.SetText("Open a database to get started...")
return nil
}
func (n *noLoadedDatabase) ApplyChanges(f *TMainForm, ndata *navData) error {
return ErrNotSupported
}
func (n *noLoadedDatabase) ExecQuery(query string, resultArea *vcl.TStringGrid) error {
return ErrNotSupported
}
func (n *noLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
return []string{}, nil return []string{}, nil
} }
func (ld *noLoadedDatabase) NavContext(bucketPath []string) ([]contextAction, error) { func (ld *noLoadedDatabase) NavContext(ndata *navData) ([]contextAction, error) {
return nil, nil // No special actions are supported return nil, nil // No special actions are supported
} }
func (n *noLoadedDatabase) Keepalive(ndata *navData) {
}
func (n *noLoadedDatabase) Close() {} func (n *noLoadedDatabase) Close() {}

View File

@@ -4,38 +4,55 @@ import (
"context" "context"
"fmt" "fmt"
"path/filepath" "path/filepath"
"unsafe"
"github.com/cockroachdb/pebble" "github.com/cockroachdb/pebble"
"github.com/cockroachdb/pebble/vfs" "github.com/cockroachdb/pebble/vfs"
"github.com/mappu/autoconfig" "github.com/ying32/govcl/vcl"
) )
type pebbleLoadedDatabase struct { type pebbleLoadedDatabase struct {
displayName string
db *pebble.DB db *pebble.DB
nav *vcl.TTreeNode
arena []*navData // keepalive
}
func (ld *pebbleLoadedDatabase) DisplayName() string {
return ld.displayName
} }
func (ld *pebbleLoadedDatabase) DriverName() string { func (ld *pebbleLoadedDatabase) DriverName() string {
return "Pebble" return "Pebble " + version_Pebble // Parsed from embedded go.mod copy
} }
func (ld *pebbleLoadedDatabase) Properties(bucketPath []string) (string, error) { func (ld *pebbleLoadedDatabase) RootElement() *vcl.TTreeNode {
content := fmt.Sprintf("Pebble metrics: %#v", ld.db.Metrics()) return ld.nav
return content, nil
} }
func (ld *pebbleLoadedDatabase) RenderForNav(f *tableState, bucketPath []string) error { func (ld *pebbleLoadedDatabase) Keepalive(ndata *navData) {
ld.arena = append(ld.arena, ndata)
}
func (ld *pebbleLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) error {
ctx := context.Background() ctx := context.Background()
// Load properties
content := fmt.Sprintf("Pebble metrics: %#v", ld.db.Metrics())
f.propertiesBox.SetText(content)
// Load data // Load data
// pebble always uses Key + Value as the columns // pebble always uses Key + Value as the columns
f.SetupColumns([]TableColumn{&binColumn{}, &binColumn{}}, []string{"Key", "Value"}) colKey := f.contentBox.Columns().Add()
colKey.Title().SetCaption("Key")
colVal := f.contentBox.Columns().Add()
colVal.Title().SetCaption("Value")
itr, err := ld.db.NewIterWithContext(ctx, nil) itr := ld.db.NewIterWithContext(ctx, nil)
if err != nil {
return err
}
defer itr.Close() defer itr.Close()
for itr.First(); itr.Valid(); itr.Next() { for itr.First(); itr.Valid(); itr.Next() {
@@ -45,76 +62,81 @@ func (ld *pebbleLoadedDatabase) RenderForNav(f *tableState, bucketPath []string)
return fmt.Errorf("Failed to load data for key %q: %w", formatAny(k), err) return fmt.Errorf("Failed to load data for key %q: %w", formatAny(k), err)
} }
f.AddRow_PK_Data(k, k, v) rpos := f.contentBox.RowCount()
f.contentBox.SetRowCount(rpos + 1)
f.contentBox.SetCells(0, rpos, formatUtf8(k))
f.contentBox.SetCells(0, rpos, formatUtf8(v))
} }
// Valid // Valid
f.Ready() vcl_stringgrid_columnwidths(f.contentBox)
f.contentBox.SetEnabled(true)
return nil return nil
} }
func (n *pebbleLoadedDatabase) ApplyChanges(f *tableState, bucketPath []string) error { func (n *pebbleLoadedDatabase) ApplyChanges(f *TMainForm, ndata *navData) error {
return ErrNotSupported
txn := n.db.NewBatch()
err := kvstore_ApplyChanges(
f,
func(k, v []byte) error { return txn.Set(k, v, nil) },
func(k []byte) error { return txn.Delete(k, nil) },
)
if err != nil {
return err
}
return txn.Commit(nil)
} }
func (ld *pebbleLoadedDatabase) NavChildren(bucketPath []string) ([]string, error) { func (ld *pebbleLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
return []string{}, nil // No children // In the pebble implementation, there is only one child: "Data"
if len(ndata.bucketPath) == 0 {
return []string{"Data"}, nil
} else {
// No children deeper than that
return []string{}, nil
}
} }
func (ld *pebbleLoadedDatabase) NavContext(bucketPath []string) ([]contextAction, error) { func (ld *pebbleLoadedDatabase) NavContext(ndata *navData) ([]contextAction, error) {
return nil, nil // No special actions are supported return nil, nil // No special actions are supported
} }
func (ld *pebbleLoadedDatabase) ExecQuery(query string, resultArea *vcl.TStringGrid) error {
return ErrNotSupported
}
func (ld *pebbleLoadedDatabase) Close() { func (ld *pebbleLoadedDatabase) Close() {
_ = ld.db.Close() _ = ld.db.Close()
ld.arena = nil
} }
var _ loadedDatabase = &pebbleLoadedDatabase{} // interface assertion var _ loadedDatabase = &pebbleLoadedDatabase{} // interface assertion
var _ editableLoadedDatabase = &pebbleLoadedDatabase{} // interface assertion
// //
type pebbleConnection struct { func (f *TMainForm) pebbleAddDatabaseFromMemory() {
Type autoconfig.OneOf f.pebbleAddDatabaseFrom("", &pebble.Options{FS: vfs.NewMem()})
Disk *struct {
Directory autoconfig.ExistingDirectory
Readonly bool
} `json:",omitempty"`
Memory *struct{} `json:",omitempty"`
} }
func (pdc *pebbleConnection) Connect(ctx context.Context) (loadedDatabase, string, error) { func (f *TMainForm) pebbleAddDatabaseFrom(dirname string, opts *pebble.Options) {
opts := (&pebble.Options{}).EnsureDefaults() // TODO load in background thread to stop blocking the UI
if pdc.Disk != nil { db, err := pebble.Open(dirname, opts)
opts.ReadOnly = pdc.Disk.Readonly
db, err := pebble.Open(string(pdc.Disk.Directory), opts)
if err != nil { if err != nil {
return nil, "", err vcl.ShowMessage(fmt.Sprintf("Failed to load database: %s", err.Error()))
return
} }
return &pebbleLoadedDatabase{db: db}, filepath.Base(string(pdc.Disk.Directory)), nil ld := &pebbleLoadedDatabase{
db: db,
} else { displayName: filepath.Base(dirname),
// Memory != nil
db, err := pebble.Open("", &pebble.Options{FS: vfs.NewMem()})
if err != nil {
return nil, "", err
} }
return &pebbleLoadedDatabase{db: db}, `:memory:`, nil // SQLite-style naming ld.nav = f.Buckets.Items().Add(nil, ld.displayName)
ld.nav.SetHasChildren(true) // dynamically populate in OnNavExpanding
ld.nav.SetImageIndex(imgDatabase)
ld.nav.SetSelectedIndex(imgDatabase)
navData := &navData{
ld: ld,
childrenLoaded: false, // will be loaded dynamically
bucketPath: []string{}, // empty = root
} }
ld.nav.SetData(unsafe.Pointer(navData))
f.dbs = append(f.dbs, ld)
f.Buckets.SetSelected(ld.nav) // Select new element
ld.Keepalive(navData)
} }

View File

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

View File

@@ -4,174 +4,107 @@ import (
"context" "context"
"fmt" "fmt"
"strconv" "strconv"
"unsafe"
"qbolt/lexer" "yvbolt/lexer"
"github.com/mappu/autoconfig"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
"golang.org/x/crypto/ssh" "github.com/ying32/govcl/vcl"
) )
type redisConnectionOptions struct {
Address autoconfig.AddressPort
Password autoconfig.Password
UseRespv3 bool `ylabel:"Use RESP v3 protocol"`
SSH_Tunnel *SSHTunnel
}
func (config *redisConnectionOptions) Reset() {
config.Address.Port = 6379
}
func (config *redisConnectionOptions) Connect(ctx context.Context) (loadedDatabase, string, error) {
ld := &redisLoadedDatabase{
currentDb: 0,
maxDb: 1,
serverVersion: "<unknown>",
}
opts := redis.Options{
Addr: fmt.Sprintf("%s:%d", config.Address.Address, config.Address.Port),
Password: string(config.Password),
UnstableResp3: config.UseRespv3,
}
if config.SSH_Tunnel != nil {
sshc, err := config.SSH_Tunnel.Open(ctx)
if err != nil {
return nil, "", err
}
// When redis wants to open a tcp conn, don't dial directly, dial via the SSH tunnel
// Setting 'Dialer' takes priority over Addr/Network fields.
// ssh.Client.DialContext has a matching signature to redis.Options.Dialer
opts.Dialer = sshc.DialContext
ld.sshc = sshc
}
// Patch in a hook to remember current DB after keepalive reconnection
opts.DB = 0 // Default
opts.OnConnect = func(ctx context.Context, cn *redis.Conn) error {
return cn.Select(ctx, ld.currentDb).Err()
}
// NewClient doesn't necessarily connect, so it can't throw an err
ld.db = redis.NewClient(&opts)
// Make an INFO request (mandatory)
info, err := ld.db.InfoMap(ctx).Result()
if err != nil {
return nil, "", fmt.Errorf("During INFO: %w", err)
}
if serverInfo, ok := info["Server"]; ok {
if v, ok := serverInfo["redis_version"]; ok {
ld.serverVersion = v
}
}
// List available databases (usually 1..16) with "0" as default
// If this fails, probably the target redis does not support multiple databases
// (e.g. Redis Cluster). Assume max=0.
if maxDatabases, err := ld.db.ConfigGet(ctx, "databases").Result(); err == nil {
// Got a result. Must parse it
m, err := strconv.ParseInt(maxDatabases["databases"], 10, 64)
if err != nil {
return nil, "", fmt.Errorf("During CONFIG GET databases: %v", err)
}
ld.maxDb = int(m)
}
displayName := config.Address.Address
return ld, displayName, nil
}
type redisLoadedDatabase struct { type redisLoadedDatabase struct {
displayName string
db *redis.Client db *redis.Client
sshc *ssh.Client nav *vcl.TTreeNode
currentDb int currentDb int
maxDb int maxDb int
serverVersion string // populated at connection-time from INFO command serverVersion string // populated at connection-time from INFO command
arena []*navData // keepalive
}
func (ld *redisLoadedDatabase) DisplayName() string {
return ld.displayName
} }
func (ld *redisLoadedDatabase) DriverName() string { func (ld *redisLoadedDatabase) DriverName() string {
return "Redis " + ld.serverVersion return "Redis " + ld.serverVersion
} }
func (ld *redisLoadedDatabase) Properties(bucketPath []string) (string, error) { func (ld *redisLoadedDatabase) RootElement() *vcl.TTreeNode {
return ld.nav
}
func (ld *redisLoadedDatabase) Keepalive(ndata *navData) {
ld.arena = append(ld.arena, ndata)
}
func (ld *redisLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) error {
ctx := context.Background() ctx := context.Background()
if len(bucketPath) == 0 { if len(ndata.bucketPath) == 0 {
// Top-level: Show info() on main Properties tab // Top-level: Show info() on main Properties tab
infostr, err := ld.db.Info(ctx).Result() infostr, err := ld.db.Info(ctx).Result()
if err != nil { if err != nil {
return "", fmt.Errorf("Retreiving database info: %w", err) return fmt.Errorf("Retreiving database info: %w", err)
} }
return infostr, nil f.propertiesBox.SetText(infostr)
} else {
return "Database " + bucketPath[0], nil
}
}
func (ld *redisLoadedDatabase) RenderForNav(f *tableState, bucketPath []string) error {
ctx := context.Background()
if len(bucketPath) == 0 {
// Leave data tab disabled (default behaviour) // Leave data tab disabled (default behaviour)
return nil return nil
} else if len(bucketPath) == 1 { } else if len(ndata.bucketPath) == 1 {
// One selected database // One selected database
// Figure out its content // Figure out its content
err := ld.db.Do(ctx, "SELECT", bucketPath[0]).Err() err := ld.db.Do(ctx, "SELECT", ndata.bucketPath[0]).Err()
if err != nil { if err != nil {
return fmt.Errorf("Switching to database %q: %w", bucketPath[0], err) return fmt.Errorf("Switching to database %q: %w", ndata.bucketPath[0], err)
} }
allKeys, err := ld.db.Keys(ctx, "*").Result() allKeys, err := ld.db.Keys(ctx, "*").Result()
if err != nil { if err != nil {
return fmt.Errorf("Listing keys in database %q: %w", bucketPath[0], err) return fmt.Errorf("Listing keys in database %q: %w", ndata.bucketPath[0], err)
} }
// Redis always uses Key string, Type string, Value []byte as the columns f.propertiesBox.SetText(fmt.Sprintf("Database %s\nTotal keys: %d\n", ndata.bucketPath[0], len(allKeys)))
f.SetupColumns( // Redis always uses Key + Value as the columns
[]TableColumn{&binColumn{}, &stringColumn{}, &binColumn{}},
[]string{"Key", "Type", "Value"}, colKey := f.contentBox.Columns().Add()
) colKey.Title().SetCaption("Key")
colType := f.contentBox.Columns().Add()
colType.Title().SetCaption("Type")
colVal := f.contentBox.Columns().Add()
colVal.Title().SetCaption("Value")
for _, key := range allKeys { for _, key := range allKeys {
typeName, err := ld.db.Type(ctx, key).Result() typeName, err := ld.db.Type(ctx, key).Result()
if err != nil { if err != nil {
return fmt.Errorf("Loading %q/%q: %w", bucketPath[0], key, err) return fmt.Errorf("Loading %q/%q: %w", ndata.bucketPath[0], key, err)
} }
rpos := f.AddRow() rpos := f.contentBox.RowCount()
f.SetCell(rpos, 0, key) f.contentBox.SetRowCount(rpos + 1)
f.SetCell(rpos, 1, typeName) f.contentBox.SetCells(0, rpos, formatUtf8([]byte(key)))
f.contentBox.SetCells(1, rpos, typeName)
switch typeName { switch typeName {
case "string": case "string":
val, err := ld.db.Get(ctx, key).Result() val, err := ld.db.Get(ctx, key).Result()
if err != nil { if err != nil {
return fmt.Errorf("Loading %q/%q: %w", bucketPath[0], key, err) return fmt.Errorf("Loading %q/%q: %w", ndata.bucketPath[0], key, err)
} }
f.contentBox.SetCells(2, rpos, val)
f.SetCell(rpos, 2, []byte(val))
case "hash": case "hash":
val, err := ld.db.HGetAll(ctx, key).Result() val, err := ld.db.HGetAll(ctx, key).Result()
if err != nil { if err != nil {
return fmt.Errorf("Loading %q/%q: %w", bucketPath[0], key, err) return fmt.Errorf("Loading %q/%q: %w", ndata.bucketPath[0], key, err)
} }
// It's a map[string]string // It's a map[string]string
f.SetCell(rpos, 2, []byte(formatAny(val))) f.contentBox.SetCells(2, rpos, formatAny(val))
case "lists": case "lists":
fallthrough fallthrough
@@ -183,25 +116,30 @@ func (ld *redisLoadedDatabase) RenderForNav(f *tableState, bucketPath []string)
fallthrough fallthrough
default: default:
f.SetCell(rpos, 2, []byte("<<<other object type>>>")) f.contentBox.SetCells(2, rpos, "<<<other object type>>>")
} }
} }
// Valid // Valid
f.Ready() vcl_stringgrid_columnwidths(f.contentBox)
f.contentBox.SetEnabled(true)
return nil return nil
} else { } else {
return fmt.Errorf("Unexpected nav position %q", bucketPath) return fmt.Errorf("Unexpected nav position %q", ndata.bucketPath)
} }
} }
func (ld *redisLoadedDatabase) NavChildren(bucketPath []string) ([]string, error) { func (n *redisLoadedDatabase) ApplyChanges(f *TMainForm, ndata *navData) error {
return ErrNotSupported
}
func (ld *redisLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
// ctx := context.Background() // ctx := context.Background()
if len(bucketPath) == 0 { if len(ndata.bucketPath) == 0 {
// Top-level: list of all child databases (usually 16x) // Top-level: list of all child databases (usually 16x)
ret := make([]string, 0, ld.maxDb) ret := make([]string, 0, ld.maxDb)
for i := 0; i < ld.maxDb; i++ { for i := 0; i < ld.maxDb; i++ {
@@ -209,22 +147,22 @@ func (ld *redisLoadedDatabase) NavChildren(bucketPath []string) ([]string, error
} }
return ret, nil return ret, nil
} else if len(bucketPath) == 1 { } else if len(ndata.bucketPath) == 1 {
// One selected database // One selected database
// No child keys underneath it // No child keys underneath it
return []string{}, nil return []string{}, nil
} else { } else {
return nil, fmt.Errorf("Unexpected nav position %q", bucketPath) return nil, fmt.Errorf("Unexpected nav position %q", ndata.bucketPath)
} }
} }
func (ld *redisLoadedDatabase) NavContext(bucketPath []string) ([]contextAction, error) { func (ld *redisLoadedDatabase) NavContext(ndata *navData) ([]contextAction, error) {
return nil, nil // No special actions are supported return nil, nil // No special actions are supported
} }
func (ld *redisLoadedDatabase) ExecQuery(query string, _ []string, resultArea *tableState) error { func (ld *redisLoadedDatabase) ExecQuery(query string, resultArea *vcl.TStringGrid) error {
ctx := context.Background() ctx := context.Background()
// Need to parse the query into separate string+args fields for the protocol // Need to parse the query into separate string+args fields for the protocol
@@ -240,32 +178,107 @@ 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([]TableColumn{&stringColumn{}}, []string{"Result"}) vcl_stringgrid_clear(resultArea)
colVal := resultArea.Columns().Add()
colVal.Title().SetCaption("Result")
// The result is probably a single value or a string slice // The result is probably a single value or a string slice
switch ret := ret.(type) { switch ret := ret.(type) {
case []string: case []string:
// Multiple values // Multiple values
for _, single := range ret { for _, single := range ret {
resultArea.AddRowData(single)
rpos := resultArea.RowCount()
resultArea.SetRowCount(rpos + 1)
resultArea.SetCells(0, rpos, formatUtf8([]byte(single)))
} }
default: default:
// Single value // Single value
// Unknown object type rpos := resultArea.RowCount()
resultArea.AddRowData(formatAny(ret)) // formatUtf8 resultArea.SetRowCount(rpos + 1)
resultArea.SetCells(0, rpos, formatAny(ret)) // formatUtf8
} }
resultArea.Ready() vcl_stringgrid_columnwidths(resultArea)
resultArea.SetEnabled(true)
return nil return nil
} }
func (ld *redisLoadedDatabase) Close() { func (ld *redisLoadedDatabase) Close() {
if ld.sshc != nil {
_ = ld.sshc.Close() // TODO does this also SIGINT remote processes?
}
_ = ld.db.Close() _ = ld.db.Close()
ld.arena = nil
} }
var _ loadedDatabase = &redisLoadedDatabase{} // interface assertion var _ loadedDatabase = &redisLoadedDatabase{} // interface assertion
var _ queryableLoadedDatabase = &redisLoadedDatabase{} // interface assertion
//
func (f *TMainForm) redisConnect(opts *redis.Options) {
// TODO load in background thread to stop blocking the UI
ctx := context.Background()
ld := &redisLoadedDatabase{
displayName: opts.Addr,
currentDb: 0,
maxDb: 1,
serverVersion: "<unknown>",
}
// Patch in a hook to remember current DB after keepalive reconnection
opts.DB = 0 // Default
opts.OnConnect = func(ctx context.Context, cn *redis.Conn) error {
return cn.Select(ctx, ld.currentDb).Err()
}
// NewClient doesn't necessarily connect, so it can't throw an err
ld.db = redis.NewClient(opts)
// Make an INFO request (mandatory)
info, err := ld.db.InfoMap(ctx).Result()
if err != nil {
vcl.ShowMessage(fmt.Sprintf("Failed to connect to Redis server: During INFO: %v", err))
return
}
if serverInfo, ok := info["Server"]; ok {
if v, ok := serverInfo["redis_version"]; ok {
ld.serverVersion = v
}
}
// List available databases (usually 1..16) with "0" as default
// If this fails, probably the target redis does not support multiple databases
// (e.g. Redis Cluster). Assume max=0.
if maxDatabases, err := ld.db.ConfigGet(ctx, "databases").Result(); err == nil {
// Got a result. Must parse it
m, err := strconv.ParseInt(maxDatabases["databases"], 10, 64)
if err != nil {
vcl.ShowMessage(fmt.Sprintf("Failed to connect to Redis server: During CONFIG GET databases: %v", err))
return
}
ld.maxDb = int(m)
}
// Done with setup
ld.nav = f.Buckets.Items().Add(nil, ld.displayName)
ld.nav.SetHasChildren(true) // dynamically populate in OnNavExpanding
ld.nav.SetImageIndex(imgDatabase)
ld.nav.SetSelectedIndex(imgDatabase)
navData := &navData{
ld: ld,
childrenLoaded: false, // will be loaded dynamically
bucketPath: []string{}, // empty = root
}
ld.nav.SetData(unsafe.Pointer(navData))
f.dbs = append(f.dbs, ld)
f.Buckets.SetSelected(ld.nav) // Select new element
ld.Keepalive(navData)
}

View File

@@ -1,74 +0,0 @@
package main
import (
"context"
"fmt"
"path/filepath"
"github.com/mappu/autoconfig"
"github.com/rosedblabs/rosedb/v2"
)
type roseLdb struct {
db *rosedb.DB
}
func (ld *roseLdb) DriverName() string {
return "RoseDB"
}
func (ld *roseLdb) Properties(bucketPath []string) (string, error) {
stats := ld.db.Stat()
return fmt.Sprintf("Statistics: %#v\n", stats), nil
}
func (ld *roseLdb) RenderForNav(f *tableState, bucketPath []string) error {
// Load data
f.SetupColumns([]TableColumn{&binColumn{}, &binColumn{}}, []string{"Key", "Value"})
ld.db.Ascend(func(k, v []byte) (bool, error) {
f.AddRow_PK_Data(k, k, v)
return true, nil
})
f.Ready()
return nil
}
func (n *roseLdb) ApplyChanges(f *tableState, bucketPath []string) error {
return kvstore_ApplyChanges(f, n.db.Put, n.db.Delete)
}
func (ld *roseLdb) NavChildren(bucketPath []string) ([]string, error) {
return []string{}, nil // No children
}
func (ld *roseLdb) NavContext(bucketPath []string) ([]contextAction, error) {
return nil, nil // No supported actions
}
func (ld *roseLdb) Close() {
_ = ld.db.Close()
}
var _ loadedDatabase = &roseLdb{} // interface assertion
var _ editableLoadedDatabase = &roseLdb{} // interface assertion
//
type roseDBConn struct {
Directory autoconfig.ExistingDirectory
}
func (c *roseDBConn) Connect(ctx context.Context) (loadedDatabase, string, error) {
options := rosedb.DefaultOptions // copy
options.DirPath = string(c.Directory)
db, err := rosedb.Open(options)
if err != nil {
return nil, "", err
}
return &roseLdb{db: db}, filepath.Base(string(c.Directory)), nil
}

View File

@@ -1,189 +0,0 @@
package main
import (
"context"
"errors"
"fmt"
"strings"
dbus "github.com/godbus/dbus/v5"
qt "github.com/mappu/miqt/qt6"
secretservice "github.com/zalando/go-keyring/secret_service"
)
type secretServiceDb struct {
svc *secretservice.SecretService
session dbus.BusObject
}
func (ld *secretServiceDb) DriverName() string {
return "FreeDesktop.org Secret Service"
}
func (ld *secretServiceDb) Properties(bucketPath []string) (string, error) {
return "", nil // No properties
}
func (ld *secretServiceDb) RenderForNav(f *tableState, bucketPath []string) error {
const (
collectionBasePath = "/org/freedesktop/secrets/collection/"
itemInterface = "org.freedesktop.Secret.Item"
serviceName = "org.freedesktop.secrets"
)
if len(bucketPath) == 0 {
// No data
} else if len(bucketPath) == 1 {
f.SetupColumns(
[]TableColumn{&stringColumn{}, &stringColumn{}, &stringColumn{}, &stringColumn{}, &stringColumn{}, &binColumn{}},
[]string{"ID", "Label", "Attributes", "ContentType", "Parameters", "Value"},
)
// Collection is selected
collection := ld.svc.GetCollection(bucketPath[0])
// Perform an empty search to find all items
allItems, err := ld.svc.SearchItems(collection, map[string]string{})
if err != nil {
return err
}
for _, item := range allItems {
//
obj := ld.svc.Object(serviceName, item)
label, err := obj.GetProperty(itemInterface + ".Label")
if err != nil {
return fmt.Errorf("Reading label %q: %w", item, err)
}
// Attributes: a map[string]string{}
// Is this optional?
// Calling attrs.String() gives a JSON representation
attrs, err := obj.GetProperty(itemInterface + ".Attributes")
if err != nil {
return fmt.Errorf("Reading attributes %q: %w", item, err)
}
secr, err := ld.svc.GetSecret(item, ld.session.Path())
if err != nil {
return fmt.Errorf("Reading secret %q: %w", item, err)
}
f.AddRowData(
string(item), // ID
label.Value().(string), // Label
attrs.String(), // Attributes (JSON)
secr.ContentType, // ContentType
string(secr.Parameters), // Parameters
secr.Value, // Value - []byte
)
}
// Valid
f.Ready()
}
return nil
}
func (ld *secretServiceDb) listCollections() ([]string, error) {
const (
serviceName = "org.freedesktop.secrets"
servicePath = "/org/freedesktop/secrets"
collectionsInterface = "org.freedesktop.Secret.Service.Collections"
collectionBasePath = "/org/freedesktop/secrets/collection/"
)
obj := ld.svc.Conn.Object(serviceName, servicePath)
val, err := obj.GetProperty(collectionsInterface)
if err != nil {
return nil, err
}
paths := val.Value().([]dbus.ObjectPath)
ret := make([]string, 0, len(paths))
for _, p := range paths {
// They are expected to have {collectionBasePath} as prefix
colName := string(p)
if strings.HasPrefix(colName, collectionBasePath) {
colName = colName[len(collectionBasePath):]
}
ret = append(ret, colName)
}
return ret, nil
}
func (ld *secretServiceDb) NavChildren(bucketPath []string) ([]string, error) {
if len(bucketPath) == 0 {
return ld.listCollections()
}
return []string{}, nil // No children
}
func (ld *secretServiceDb) newCollection(sender *qt.QTreeWidgetItem, bucketPath []string) error {
name := qt.QInputDialog_GetText(sender.TreeWidget().QWidget, APPNAME, "Enter a name for the new collection:")
if name == "" {
return nil // cancelled
}
_, err := ld.svc.CreateCollection(name)
return err
}
func (ld *secretServiceDb) unlockKeychain(sender *qt.QTreeWidgetItem, bucketPath []string) error {
if len(bucketPath) != 1 {
return errors.New("Invalid selection")
}
coll := ld.svc.GetCollection(bucketPath[0])
return ld.svc.Unlock(coll.Path())
}
func (ld *secretServiceDb) NavContext(bucketPath []string) ([]contextAction, error) {
if len(bucketPath) == 0 {
return []contextAction{
contextAction{"Create collection...", ld.newCollection},
}, nil
} else if len(bucketPath) == 1 {
return []contextAction{
contextAction{"Unlock", ld.unlockKeychain},
}, nil
} else {
return nil, nil // Unreachable
}
}
func (ld *secretServiceDb) Close() {
_ = ld.svc.Close(ld.session)
}
var _ loadedDatabase = &secretServiceDb{} // interface assertion
//
type secretServiceConnection struct {
}
func (ssc *secretServiceConnection) Connect(ctx context.Context) (loadedDatabase, string, error) {
svc, err := secretservice.NewSecretService()
if err != nil {
return nil, "", err
}
session, err := svc.OpenSession()
if err != nil {
return nil, "", err
}
return &secretServiceDb{svc: svc, session: session}, "dbus://SessionBus/org.freedesktop.secrets", nil
}

View File

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

View File

@@ -1,45 +1,58 @@
package main package main
import ( import (
"context"
"database/sql" "database/sql"
"errors" "errors"
"fmt" "fmt"
"path/filepath" "path/filepath"
"reflect" "unsafe"
"strings"
"qbolt/sqliteclidriver" _ "yvbolt/sqliteclidriver"
"github.com/mappu/autoconfig" "github.com/ying32/govcl/vcl"
qt "github.com/mappu/miqt/qt6"
mattn_sqlite3 "github.com/mattn/go-sqlite3"
) )
const ( const (
sqliteTablesCaption = "Tables" sqliteTablesCaption = "Tables"
sqliteFilter = "SQLite database|*.db;*.db3;*.sqlite;*.sqlite3|All files|*.*"
) )
type sqliteLoadedDatabase struct { type sqliteLoadedDatabase struct {
displayName string
path string
db *sql.DB db *sql.DB
nav *vcl.TTreeNode
arena []*navData // keepalive
} }
func (ld *sqliteLoadedDatabase) DriverName() string { func (ld *sqliteLoadedDatabase) DisplayName() string {
if _, ok := ld.db.Driver().(*sqliteclidriver.SCDriver); ok { return ld.displayName
return "SQLite (sqliteclidriver)"
} }
ver1, _, _ := mattn_sqlite3.Version() func (ld *sqliteLoadedDatabase) RootElement() *vcl.TTreeNode {
return "SQLite " + ver1 return ld.nav
} }
func (ld *sqliteLoadedDatabase) Properties(bucketPath []string) (string, error) { func (ld *sqliteLoadedDatabase) Keepalive(ndata *navData) {
if len(bucketPath) == 0 || len(bucketPath) == 1 { ld.arena = append(ld.arena, ndata)
return "Please select...", nil // No properties }
} else { func (ld *sqliteLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) error {
tableName := bucketPath[1] if len(ndata.bucketPath) == 0 {
// Top-level
f.propertiesBox.SetText("Please select...")
return nil
} else if len(ndata.bucketPath) == 1 {
// Category (tables, ...)
f.propertiesBox.SetText("Please select...")
return nil
} else if len(ndata.bucketPath) == 2 && ndata.bucketPath[0] == sqliteTablesCaption {
// Render for specific table
tableName := ndata.bucketPath[1]
// Get some basic properties // Get some basic properties
r := ld.db.QueryRow(`SELECT sql FROM sqlite_schema WHERE name = ?;`, tableName) r := ld.db.QueryRow(`SELECT sql FROM sqlite_schema WHERE name = ?;`, tableName)
@@ -50,40 +63,33 @@ func (ld *sqliteLoadedDatabase) Properties(bucketPath []string) (string, error)
} }
// Display table properties // Display table properties
return fmt.Sprintf("Selected table %q\n\nSchema:\n\n%s", tableName, schemaStmt), nil f.propertiesBox.SetText(fmt.Sprintf("Selected table %q\n\nSchema:\n\n%s", tableName, schemaStmt))
}
// 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)
} }
func (ld *sqliteLoadedDatabase) RenderForNav(f *tableState, bucketPath []string) error { populateColumns(columnNames, f.contentBox)
if len(bucketPath) == 0 {
return nil
} else if len(bucketPath) == 1 {
// Category (tables, ...)
return nil
} else if len(bucketPath) == 2 && bucketPath[0] == sqliteTablesCaption {
// Render for specific table
tableName := bucketPath[1]
// 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 rowid, * FROM [` + tableName + `]`) // WARNING can't prepare this parameter, but it comes from the DB (trusted) datar, err := ld.db.Query(`SELECT * FROM "` + tableName + `" LIMIT 1000`) // WARNING can't prepare this parameter, but it comes from the DB (trusted)
if err != nil { if err != nil {
return fmt.Errorf("Failed to load data for table %q: %w", tableName, err) return fmt.Errorf("Failed to load data for table %q: %w", tableName, err)
} }
defer datar.Close() defer datar.Close()
populateRows(datar, f.contentBox)
err = populateRows(datar, f, true)
if err != nil {
return err
}
// We successfully populated the data grid // We successfully populated the data grid
f.Ready() vcl_stringgrid_columnwidths(f.contentBox)
f.contentBox.SetEnabled(true)
return nil return nil
} else { } else {
@@ -94,6 +100,10 @@ func (ld *sqliteLoadedDatabase) RenderForNav(f *tableState, bucketPath []string)
} }
func (n *sqliteLoadedDatabase) ApplyChanges(f *TMainForm, ndata *navData) error {
return ErrNotSupported
}
func (ld *sqliteLoadedDatabase) sqliteGetColumnNamesForTable(tableName string) ([]string, error) { func (ld *sqliteLoadedDatabase) sqliteGetColumnNamesForTable(tableName string) ([]string, error) {
colr, err := ld.db.Query(`SELECT name FROM pragma_table_info( ? )`, tableName) colr, err := ld.db.Query(`SELECT name FROM pragma_table_info( ? )`, tableName)
if err != nil { if err != nil {
@@ -120,115 +130,44 @@ func (ld *sqliteLoadedDatabase) sqliteGetColumnNamesForTable(tableName string) (
return ret, nil return ret, nil
} }
func populateRows(rr *sql.Rows, dest *tableState, firstColumnIsExtraRowID bool) error { func populateColumns(names []string, dest *vcl.TStringGrid) {
for _, columnName := range names {
numColumns := len(dest.columns) col := dest.Columns().Add()
col.Title().SetCaption(columnName)
var pfields []interface{} = nil }
cNames, err := rr.Columns()
if err != nil {
return err
} }
cTypes, err := rr.ColumnTypes() func populateRows(rr *sql.Rows, dest *vcl.TStringGrid) {
if err != nil {
return err
}
if len(cNames) != len(cTypes) { numColumns := int(dest.Columns().Count())
return errors.New("unexpected column metadata mismatch") // assert
}
numColumns = len(cNames)
rrMakeCtypes := []TableColumn{}
for i := 0; i < len(cTypes); i++ {
if firstColumnIsExtraRowID && i == 0 {
pfields = append(pfields, new(int64))
continue
}
nullable, ok := cTypes[i].Nullable()
if !ok {
return errors.New("can't tell if column is nullable?")
}
// TODO support all SQLite column names/type-affinities
// TODO support nullable variants for all types here
// @ref https://www.sqlite.org/datatype3.html
switch cTypes[i].DatabaseTypeName() {
case "BLOB":
// Binary column
rrMakeCtypes = append(rrMakeCtypes, &binColumn{})
pfields = append(pfields, new([]byte))
case "INTEGER":
if nullable {
rrMakeCtypes = append(rrMakeCtypes, &sqlNullInt64Column{})
pfields = append(pfields, new(sql.NullInt64))
} else {
rrMakeCtypes = append(rrMakeCtypes, &int64Column{})
pfields = append(pfields, new(int64))
}
default:
if nullable {
rrMakeCtypes = append(rrMakeCtypes, &sqlNullStringColumn{})
pfields = append(pfields, new(sql.NullString))
} else {
rrMakeCtypes = append(rrMakeCtypes, &stringColumn{})
pfields = append(pfields, new(string))
}
}
}
// Resetup table
if firstColumnIsExtraRowID {
cNames = cNames[1:]
}
dest.SetupColumns(rrMakeCtypes, cNames)
for rr.Next() { for rr.Next() {
fields := make([]interface{}, numColumns)
pfields := make([]interface{}, numColumns)
for i := 0; i < numColumns; i += 1 {
pfields[i] = &fields[i]
}
err := rr.Scan(pfields...) err := rr.Scan(pfields...)
if err != nil { if err != nil {
return fmt.Errorf("Scan: %w", err) vcl.ShowMessageFmt("Failed to load data: %s", err.Error())
return
} }
rpos := dest.AddRow() rpos := dest.RowCount()
dest.SetRowCount(rpos + 1)
if firstColumnIsExtraRowID { for i := 0; i < len(fields); i += 1 {
dest.SetRowPrimaryKey(rpos, int64_to_binary8(*(pfields[0].(*int64)))) dest.SetCells(int32(i), rpos, formatAny(fields[i]))
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 rr.Err() != nil {
vcl.ShowMessageFmt("Failed to load data: %s", rr.Err().Error())
return
} }
return rr.Err()
} }
func (ld *sqliteLoadedDatabase) ExecQuery(query string, _ []string, resultArea *tableState) error { func (ld *sqliteLoadedDatabase) ExecQuery(query string, resultArea *vcl.TStringGrid) error {
rr, err := ld.db.Query(query) rr, err := ld.db.Query(query)
if err != nil { if err != nil {
return err return err
@@ -236,125 +175,31 @@ func (ld *sqliteLoadedDatabase) ExecQuery(query string, _ []string, resultArea *
defer rr.Close() defer rr.Close()
err = populateRows(rr, resultArea, false) vcl_stringgrid_clear(resultArea)
columns, err := rr.Columns()
if err != nil { if err != nil {
return err return err
} }
resultArea.Ready() populateColumns(columns, resultArea)
populateRows(rr, resultArea)
vcl_stringgrid_columnwidths(resultArea)
resultArea.SetEnabled(true)
return nil return nil
} }
type RowQueryContexter interface { func (ld *sqliteLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
}
func (n *sqliteLoadedDatabase) ApplyChanges(f *tableState, bucketPath []string) (retErr error) { if len(ndata.bucketPath) == 0 {
if len(bucketPath) != 2 {
return errors.New("invalid selection")
}
tableName := bucketPath[1]
ctx := context.Background()
tx, err := n.db.BeginTx(ctx, nil)
if err != nil {
return err
}
var commitOK bool = false
defer func() {
if !commitOK {
err := tx.Rollback()
if err != nil {
retErr = err
}
}
}()
// SQLite can only LIMIT 1 on update/delete if it was compiled with
// SQLITE_ENABLE_UPDATE_DELETE_LIMIT, which isn't the case for the mattn
// cgo library
// Skip that, and just rely on primary key uniqueness
// Edit
for aRow, editcells := range f.updateRows {
stmt := `UPDATE [` + tableName + `] SET `
params := []interface{}{}
for ct, cell := range editcells {
if ct > 0 {
stmt += `, `
}
stmt += `[` + f.columnLabels[cell] + `] = ?`
params = append(params, f.columns[cell].GetCell(aRow))
}
stmt += ` WHERE [rowid] = ?`
// Update by primary key (stored separately)
pkVal := binary8_to_int64(f.primaryKeys[aRow])
params = append(params, pkVal)
_, err = tx.ExecContext(ctx, stmt, params...)
if err != nil {
return fmt.Errorf("Updating row %q: %w", pkVal, err)
}
}
// Delete by key (affects rowids after re-render)
for aRow, _ := range f.deleteRows {
pkVal := binary8_to_int64(f.primaryKeys[aRow])
stmt := `DELETE FROM [` + tableName + `] WHERE [rowid] = ?`
_, err = tx.ExecContext(ctx, stmt, pkVal)
if err != nil {
return fmt.Errorf("Deleting row %q: %w", pkVal, err)
}
}
// Insert all new entries
for aRow, _ := range f.insertRows {
stmt := `INSERT INTO [` + tableName + `] ([` + strings.Join(f.columnLabels, `], [`) + `]) VALUES (`
params := []interface{}{}
for colid := 0; colid < len(f.columnLabels); colid++ {
if colid > 0 {
stmt += `, `
}
stmt += "?"
params = append(params, f.columns[colid].GetCell(aRow))
}
stmt += `)`
_, err = tx.ExecContext(ctx, stmt, params...)
if err != nil {
return fmt.Errorf("Inserting row: %w", err)
}
}
err = tx.Commit()
if err != nil {
return err
}
commitOK = true // no need for rollback
return nil
}
func (ld *sqliteLoadedDatabase) NavChildren(bucketPath []string) ([]string, error) {
if len(bucketPath) == 0 {
// The top-level children are always: // The top-level children are always:
return []string{sqliteTablesCaption}, nil return []string{sqliteTablesCaption}, nil
} }
if len(bucketPath) == 1 && bucketPath[0] == sqliteTablesCaption { if len(ndata.bucketPath) == 1 && ndata.bucketPath[0] == sqliteTablesCaption {
// The sequence and stat1 tables are not marked as hidden tables rr, err := ld.db.Query(`SELECT name FROM sqlite_master WHERE type='table' ORDER BY name ASC;`)
// 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
} }
@@ -377,51 +222,54 @@ func (ld *sqliteLoadedDatabase) NavChildren(bucketPath []string) ([]string, erro
return gather, nil return gather, nil
} }
if len(bucketPath) == 2 { if len(ndata.bucketPath) == 2 {
return nil, nil // Never any deeper children return nil, nil // Never any deeper children
} }
return nil, fmt.Errorf("unknown nav path %#v", bucketPath) return nil, fmt.Errorf("unknown nav path %#v", ndata.bucketPath)
} }
func (ld *sqliteLoadedDatabase) NavContext(bucketPath []string) (ret []contextAction, err error) { func (ld *sqliteLoadedDatabase) NavContext(ndata *navData) (ret []contextAction, err error) {
if len(bucketPath) == 0 { if len(ndata.bucketPath) == 0 {
ret = append(ret, contextAction{"Compact database", ld.CompactDatabase}) ret = append(ret, contextAction{"Compact database", ld.CompactDatabase})
ret = append(ret, contextAction{"Export backup...", ld.ExportBackup}) ret = append(ret, contextAction{"Export backup...", ld.ExportBackup})
} }
if len(bucketPath) == 2 { if len(ndata.bucketPath) == 2 {
ret = append(ret, contextAction{"Drop table", ld.DropTable}) ret = append(ret, contextAction{"Drop table", ld.DropTable})
} }
return return
} }
func (ld *sqliteLoadedDatabase) CompactDatabase(sender *qt.QTreeWidgetItem, bucketPath []string) error { func (ld *sqliteLoadedDatabase) CompactDatabase(sender vcl.IComponent, ndata *navData) error {
_, err := ld.db.Exec(`VACUUM;`) _, err := ld.db.Exec(`VACUUM;`)
return err return err
} }
func (ld *sqliteLoadedDatabase) ExportBackup(sender *qt.QTreeWidgetItem, bucketPath []string) error { func (ld *sqliteLoadedDatabase) ExportBackup(sender vcl.IComponent, ndata *navData) error {
// Popup for output file // Popup for output file
savePath := qt.QFileDialog_GetSaveFileName4(sender.TreeWidget().QWidget, "Save backup as...", "", "SQLite database (*.db *.db3 *.sqlite *.sqlite3);;All files (*)") dlg := vcl.NewSaveDialog(sender)
if savePath == "" { dlg.SetTitle("Save backup as...")
dlg.SetFilter(sqliteFilter)
ret := dlg.Execute() // Fake blocking
if !ret {
return nil // cancelled return nil // cancelled
} }
_, err := ld.db.Exec(`VACUUM INTO ?`, savePath) _, err := ld.db.Exec(`VACUUM INTO ?`, dlg.FileName())
return err return err
} }
func (ld *sqliteLoadedDatabase) DropTable(sender *qt.QTreeWidgetItem, bucketPath []string) error { func (ld *sqliteLoadedDatabase) DropTable(sender vcl.IComponent, ndata *navData) error {
if len(bucketPath) != 2 { if len(ndata.bucketPath) != 2 {
return errors.New("Invalid selection") return errors.New("Invalid selection")
} }
// //
tableName := bucketPath[1] tableName := ndata.bucketPath[1]
if !vcl_confirm_dialog(sender.TreeWidget().QWidget, fmt.Sprintf("Are you sure you want to drop the table %q?", tableName)) { if !vcl_confirm_dialog(sender, "Drop table", fmt.Sprintf("Are you sure you want to drop the table %q?", tableName)) {
return nil // cancelled return nil // cancelled
} }
@@ -431,64 +279,46 @@ func (ld *sqliteLoadedDatabase) DropTable(sender *qt.QTreeWidgetItem, bucketPath
func (ld *sqliteLoadedDatabase) Close() { func (ld *sqliteLoadedDatabase) Close() {
_ = ld.db.Close() _ = ld.db.Close()
ld.arena = nil
} }
var _ loadedDatabase = &sqliteLoadedDatabase{} // interface assertion var _ loadedDatabase = &sqliteLoadedDatabase{} // interface assertion
var _ editableLoadedDatabase = &sqliteLoadedDatabase{} // interface assertion
var _ queryableLoadedDatabase = &sqliteLoadedDatabase{} // interface assertion
// //
type sqliteConnection struct { func (f *TMainForm) sqliteAddDatabaseFromFile(path string, cliDriver bool) {
Type autoconfig.OneOf
Disk *struct {
Database autoconfig.ExistingFile `yfilter:"SQLite database (*.db *.db3 *.sqlite *.sqlite3);;All files (*)"`
CliDriver bool `ylabel:"Use experimental CLI driver"`
} `json:",omitempty"`
Memory *struct{} `json:",omitempty"`
SSH *struct {
SSHServer *SSHTunnel
Database string
} `json:",omitempty"`
}
func (sc *sqliteConnection) Connect(ctx context.Context) (loadedDatabase, string, error) {
if sc.Disk != nil {
driver := "sqlite3" driver := "sqlite3"
if sc.Disk.CliDriver { if cliDriver {
driver = "sqliteclidriver" driver = "sqliteclidriver"
} }
db, err := sql.Open(driver, string(sc.Disk.Database)) // TODO load in background thread to stop blocking the UI
db, err := sql.Open(driver, path)
if err != nil { if err != nil {
return nil, "", err vcl.ShowMessage(fmt.Sprintf("Failed to load database '%s': %s", path, err.Error()))
return
} }
return &sqliteLoadedDatabase{db: db}, filepath.Base(string(sc.Disk.Database)), nil ld := &sqliteLoadedDatabase{
path: path,
} else if sc.Memory != nil { // memory displayName: filepath.Base(path),
db, err := sql.Open("sqlite3", ":memory:") db: db,
if err != nil {
return nil, "", err
} }
return &sqliteLoadedDatabase{db: db}, ":memory:", nil ld.nav = f.Buckets.Items().Add(nil, ld.displayName)
ld.nav.SetImageIndex(imgDatabase)
} else if sc.SSH != nil { ld.nav.SetSelectedIndex(imgDatabase)
if sc.SSH.SSHServer == nil { ld.nav.SetHasChildren(true) // dynamically populate in OnNavExpanding
return nil, "", errors.New("Invalid configuration") navData := &navData{
ld: ld,
childrenLoaded: false, // will be loaded dynamically
bucketPath: []string{}, // empty = root
} }
ld.nav.SetData(unsafe.Pointer(navData))
cl, err := sc.SSH.SSHServer.Open(ctx) f.dbs = append(f.dbs, ld)
if err != nil { f.Buckets.SetSelected(ld.nav) // Select new element
return nil, "", err
} ld.Keepalive(navData)
db := sqliteclidriver.OpenSSH(cl, sc.SSH.Database)
return &sqliteLoadedDatabase{db: db}, "SSH[" + filepath.Base(string(sc.SSH.Database)) + "]", nil
} else {
return nil, "", errors.New("Invalid configuration")
}
} }

18
db_sqlite_cgo.go Normal file
View File

@@ -0,0 +1,18 @@
//+build cgo
package main
import (
"yvbolt/sqliteclidriver"
sqlite3 "github.com/mattn/go-sqlite3"
)
func (ld *sqliteLoadedDatabase) DriverName() string {
if _, ok := ld.db.Driver().(*sqliteclidriver.SCDriver); ok {
return "SQLite (sqliteclidriver)"
}
ver1, _, _ := sqlite3.Version()
return "SQLite " + ver1
}

17
db_sqlite_nocgo.go Normal file
View File

@@ -0,0 +1,17 @@
//+build !cgo
package main
import (
"yvbolt/sqliteclidriver"
_ "modernc.org/sqlite"
)
func (ld *sqliteLoadedDatabase) DriverName() string {
if _, ok := ld.db.Driver().(*sqliteclidriver.SCDriver); ok {
return "SQLite (sqliteclidriver)"
}
return "SQLite (modernc.org)"
}

View File

@@ -1,179 +0,0 @@
package main
import (
"context"
"fmt"
"net"
"net/netip"
"os"
"github.com/mappu/autoconfig"
qt "github.com/mappu/miqt/qt6"
"golang.org/x/crypto/ssh/agent"
)
type sshAgentLdb struct {
conn agent.ExtendedAgent
}
func (ld *sshAgentLdb) DriverName() string {
return "ssh-agent"
}
func (ld *sshAgentLdb) Properties(bucketPath []string) (string, error) {
return "", nil
}
func (ld *sshAgentLdb) RenderForNav(f *tableState, bucketPath []string) error {
keys, err := ld.conn.List()
if err != nil {
return err
}
f.SetupColumns([]TableColumn{&stringColumn{}, &stringColumn{}, &binColumn{}}, []string{"Comment", "Type", "Public Key"})
for _, key := range keys {
// The publicKey blob is the effective primary-key for DB manipulation
f.AddRow_PK_Data(key.Blob, key.Comment, key.Format, key.Blob)
}
f.Ready()
return nil
}
func (ld *sshAgentLdb) NavChildren(bucketPath []string) ([]string, error) {
return []string{}, nil // No children
}
func (ld *sshAgentLdb) lockPrompt(sender *qt.QTreeWidgetItem, bucketPath []string) error {
parent := sender.TreeWidget().QWidget
props := encryptionKey{}
autoconfig.OpenDialog(&props, parent, "Enter lock password...", func() {
key, err := props.Get()
if err != nil {
qt.QMessageBox_Warning(parent, APPNAME, err.Error())
return
}
if len(key) == 0 {
// Cancelled
return
}
err = ld.conn.Lock(key)
if err != nil {
qt.QMessageBox_Warning(parent, APPNAME, "Locking SSH agent: "+err.Error())
return
}
})
return nil // n.b. refreshes now
}
func (ld *sshAgentLdb) unlockPrompt(sender *qt.QTreeWidgetItem, bucketPath []string) error {
parent := sender.TreeWidget().QWidget
props := encryptionKey{}
autoconfig.OpenDialog(&props, parent, "Enter unlock password...", func() {
key, err := props.Get()
if err != nil {
qt.QMessageBox_Warning(parent, APPNAME, err.Error())
return
}
if len(key) == 0 {
// Cancelled
return
}
err = ld.conn.Unlock(key)
if err != nil {
qt.QMessageBox_Warning(parent, APPNAME, "Unlocking SSH agent: "+err.Error())
return
}
// Trigger a refresh
})
return nil // n.b. refreshes now, which may cause double-error if we are still locked
}
func (ld *sshAgentLdb) NavContext(bucketPath []string) ([]contextAction, error) {
return []contextAction{
{"Lock agent...", ld.lockPrompt},
{"Unlock agent...", ld.unlockPrompt},
}, nil
}
func (ld *sshAgentLdb) Close() {
// noop
}
var _ loadedDatabase = &sshAgentLdb{} // interface assertion
//
type sshAgentConn struct {
Type autoconfig.OneOf
Unix *autoconfig.ExistingFile `json:",omitempty"`
TCP *autoconfig.AddressPort `json:",omitempty"`
}
func (c *sshAgentConn) Reset() {
if sshAuthSock := os.Getenv("SSH_AUTH_SOCK"); sshAuthSock != "" {
if _, err := os.Stat(sshAuthSock); err == nil {
// File
c.Type = "Unix"
val := autoconfig.ExistingFile(sshAuthSock)
c.Unix = &val
} else if props, err := netip.ParseAddrPort(sshAuthSock); err == nil {
// IP:Port
c.Type = "TCP"
val := autoconfig.AddressPort{
Address: props.Addr().String(),
Port: int(props.Port()),
}
c.TCP = &val
} else {
// Can't parse env var
}
}
}
func (c *sshAgentConn) getAgent() (agent.ExtendedAgent, error) {
if c.Unix != nil {
conn, err := net.Dial("unix", string(*c.Unix))
if err != nil {
return nil, fmt.Errorf("Connecting to SSH agent %q: %w", c.Unix, err)
}
return agent.NewClient(conn), nil
} else if c.TCP != nil {
conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", c.TCP.Address, c.TCP.Port))
if err != nil {
return nil, fmt.Errorf("Connecting to SSH agent %q: %w", c.TCP.String(), err)
}
return agent.NewClient(conn), nil
} else {
return nil, fmt.Errorf("No connection details specified")
}
}
func (c *sshAgentConn) Connect(ctx context.Context) (loadedDatabase, string, error) {
agent, err := c.getAgent()
if err != nil {
return nil, "", err
}
return &sshAgentLdb{conn: agent}, "SSH Agent", nil
}

View File

@@ -1,115 +0,0 @@
package main
import (
"context"
"fmt"
"path/filepath"
"github.com/mappu/autoconfig"
"github.com/starskey-io/starskey"
)
type starskeyLdb struct {
db *starskey.Starskey
}
func (ld *starskeyLdb) DriverName() string {
return "Starskey"
}
func (ld *starskeyLdb) Properties(bucketPath []string) (string, error) {
return "", nil // No properties
}
func (ld *starskeyLdb) RenderForNav(f *tableState, bucketPath []string) error {
// Load data
// Starskey always uses Key + Value as the columns
f.SetupColumns([]TableColumn{&binColumn{}, &binColumn{}}, []string{"Key", "Value"})
// It's possible to create a transaction in Starskey, but you can't enumerate keys
// within the transaction - doesn't really help us
var allKeys [][]byte
_, err := ld.db.FilterKeys(func(key []byte) bool {
allKeys = append(allKeys, slice_dup(key))
return false // don't get value in here
})
if err != nil {
return fmt.Errorf("FilterKeys: %w", err)
}
for _, key := range allKeys {
val, err := ld.db.Get(key)
if err != nil {
// We get <nil, nil> if not found, so any error is a real error
return fmt.Errorf("Reading key %q: %w", string(key), err)
}
if val == nil {
// Key not found
// The hack to use FilterKeys() means this can happen if the key is
// pointing to a tombstone
continue
}
f.AddRow_PK_Data(key, key, val)
}
// Valid
f.Ready()
return nil
}
func (n *starskeyLdb) ApplyChanges(f *tableState, bucketPath []string) error {
return kvstore_ApplyChanges(f, n.db.Put, n.db.Delete)
}
func (ld *starskeyLdb) NavChildren(bucketPath []string) ([]string, error) {
return []string{}, nil // No children
}
func (ld *starskeyLdb) NavContext(bucketPath []string) ([]contextAction, error) {
return nil, nil // No special actions are supported
}
func (ld *starskeyLdb) Close() {
_ = ld.db.Close()
}
var _ loadedDatabase = &starskeyLdb{} // interface assertion
var _ editableLoadedDatabase = &starskeyLdb{} // interface assertion
//
type starskeyConnection struct {
Directory autoconfig.ExistingDirectory
Compression autoconfig.EnumList `yenum:"No compression;;Snappy;;S2"`
}
func (pdc *starskeyConnection) Connect(ctx context.Context) (loadedDatabase, string, error) {
cfg := starskey.Config{
Permission: 0755,
Directory: string(pdc.Directory),
FlushThreshold: (1024 * 1024) * 24, // Upstream default (24 MiB)
MaxLevel: 3, // Upstream default
SizeFactor: 10, // Upstream default
}
if pdc.Compression == 0 {
} else if pdc.Compression == 1 {
cfg.Compression = true
cfg.CompressionOption = starskey.SnappyCompression
} else if pdc.Compression == 2 {
cfg.Compression = true
cfg.CompressionOption = starskey.S2Compression
}
db, err := starskey.Open(&cfg)
if err != nil {
return nil, "", err
}
return &starskeyLdb{db: db}, filepath.Base(string(pdc.Directory)), nil
}

View File

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

View File

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

View File

@@ -18,26 +18,11 @@ type Entry struct {
Properties [][2]string Properties [][2]string
} }
type Application struct {
Name string
Entries []Entry
}
type Database struct { type Database struct {
Entries []Application Entries []Entry
AllColumnNames []string AllColumnNames []string
} }
func (d *Database) FindApplicationByName(search string) (*Application, bool) {
for idx, app := range d.Entries {
if app.Name == search {
return &d.Entries[idx], true // TODO faster lookup?
}
}
return nil, false
}
func Parse(r io.Reader) (*Database, error) { func Parse(r io.Reader) (*Database, error) {
sc := bufio.NewScanner(r) sc := bufio.NewScanner(r)
@@ -102,31 +87,8 @@ func Parse(r io.Reader) (*Database, error) {
entries = append(entries, wip) entries = append(entries, wip)
} }
// Group all entries by Application
apps := make([]Application, 0)
appIndexes := make(map[string]int, 0)
for _, entry := range entries {
spos := strings.Index(entry.Name, `/`)
appName := entry.Name[0:spos]
idx, ok := appIndexes[appName]
if !ok {
appIndexes[appName] = len(apps)
apps = append(apps, Application{
Name: appName,
Entries: []Entry{entry},
})
} else {
tmp := apps[idx]
tmp.Entries = append(tmp.Entries, entry)
apps[idx] = tmp
}
}
return &Database{ return &Database{
Entries: apps, Entries: entries,
AllColumnNames: discoveredColumns, AllColumnNames: discoveredColumns,
}, nil }, nil
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

BIN
doc/screenshot-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -1,17 +0,0 @@
package main
//go:generate miqt-rcc -Input "embed.qrc" -OutputGo "embed.go" -OutputRcc "embed.rcc" -Qt6
import (
"embed"
qt "github.com/mappu/miqt/qt6"
)
//go:embed embed.rcc
var _resourceRcc []byte
func init() {
_ = embed.FS{}
qt.QResource_RegisterResourceWithRccData(&_resourceRcc[0])
}

View File

@@ -1,52 +0,0 @@
<RCC>
<qresource prefix="/">
<file>assets/add.png</file>
<file>assets/arrow_refresh.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_add.png</file>
<file>assets/database_delete.png</file>
<file>assets/database_key.png</file>
<file>assets/database_lightning.png</file>
<file>assets/database_save.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_go.png</file>
<file>assets/note_delete.png</file>
<file>assets/page_key.png</file>
<file>assets/pencil.png</file>
<file>assets/pencil_add.png</file>
<file>assets/pencil_delete.png</file>
<file>assets/pencil_go.png</file>
<file>assets/resultset_next.png</file>
<file>assets/table.png</file>
<file>assets/table_add.png</file>
<file>assets/table_delete.png</file>
<file>assets/table_save.png</file>
<file>assets/vendor_buntdb.png</file>
<file>assets/vendor_cockroach.png</file>
<file>assets/vendor_debian.png</file>
<file>assets/vendor_dgraph.png</file>
<file>assets/vendor_freedesktop.png</file>
<file>assets/vendor_github.png</file>
<file>assets/vendor_leveldb.png</file>
<file>assets/vendor_lmdb.png</file>
<file>assets/vendor_lotus.png</file>
<file>assets/vendor_mongodb.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_riak.png</file>
<file>assets/vendor_rosedb.png</file>
<file>assets/vendor_sqlite.png</file>
<file>assets/vendor_ssh.png</file>
<file>assets/vendor_starskey.png</file>
<file>assets/vendor_voiddb.png</file>
</qresource>
</RCC>

BIN
embed.rcc

Binary file not shown.

47
fltktest/main.go Normal file
View File

@@ -0,0 +1,47 @@
package main
import (
"github.com/pwiecz/go-fltk"
)
const WNDCOLOR = 0xf0f0f000
const FONTSIZE = 12
func main() {
fltk.SetScheme("gtk+")
// ourFont := fltk.FREE_FONT + 1
// fltk.SetFont(ourFont, "Papyrus")
fltk.SetColor(fltk.BACKGROUND_COLOR, 0xf0, 0xf0, 0xf0)
fltk.SetColor(fltk.BACKGROUND2_COLOR, 0xff, 0x00, 0x00)
win := fltk.NewWindow(400, 300)
win.SetLabel("Main Window")
// win.SetLabelSize(FONTSIZE)
// win.SetColor(WNDCOLOR)
mnu := fltk.NewMenuBar(0, 0, win.W(), 25)
mnu.Add("File/Quit", func() {})
mnu.SetLabelSize(FONTSIZE)
mnu.SetBox(fltk.FLAT_BOX)
win.Add(mnu)
btn := fltk.NewButton(160, 200, 80, 30, "Click")
// btn.SetLabelFont(ourFont)
btn.SetLabelSize(FONTSIZE)
btn.SetCallback(func() {
btn.SetLabel("Clicked")
})
// win.End()
// win.Resizable()
win.SetResizeHandler(func() {
mnu.Resize(0, 0, win.W(), 25)
})
win.Show()
fltk.Run()
}

140
go.mod
View File

@@ -1,103 +1,69 @@
module qbolt module yvbolt
go 1.24.0 go 1.19
toolchain go1.24.4
require ( require (
github.com/cockroachdb/pebble v1.1.5 github.com/cockroachdb/pebble v1.0.0
github.com/dgraph-io/badger/v4 v4.8.0 github.com/dgraph-io/badger/v4 v4.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/mattn/go-sqlite3 v1.14.22
github.com/lotusdblabs/lotusdb/v2 v2.1.0 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
github.com/mappu/autoconfig v0.5.0 github.com/redis/go-redis/v9 v9.5.3
github.com/mappu/miqt v0.12.1-0.20251106063543-466b60e47edf github.com/stretchr/testify v1.9.0
github.com/mattn/go-sqlite3 v1.14.32 github.com/ying32/govcl v2.2.3+incompatible
github.com/redis/go-redis/v9 v9.17.2 go.etcd.io/bbolt v1.4.0-alpha.1
github.com/rosedblabs/rosedb/v2 v2.3.6 modernc.org/sqlite v1.24.0
github.com/starskey-io/starskey v0.1.9
github.com/stretchr/testify v1.11.1
github.com/syndtr/goleveldb v1.0.0
github.com/zalando/go-keyring v0.2.6
go.etcd.io/bbolt v1.4.3
go.mills.io/bitcask/v2 v2.1.5
go.mongodb.org/mongo-driver/v2 v2.4.1
golang.org/x/crypto v0.46.0
) )
require ( require (
al.essio.dev/pkg/shellescape v1.6.0 // indirect github.com/DataDog/zstd v1.4.5 // indirect
github.com/DataDog/zstd v1.5.7 // indirect
github.com/abcum/lcp v0.0.0-20201209214815-7a3f3840be81 // indirect
github.com/akrylysov/pogreb v0.10.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/bwmarrin/snowflake v0.3.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cockroachdb/errors v1.11.3 // indirect
github.com/cockroachdb/errors v1.12.0 // indirect github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect
github.com/cockroachdb/fifo v0.0.0-20240816210425-c5d0cb0b6fc0 // indirect github.com/cockroachdb/redact v1.1.5 // indirect
github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/cockroachdb/redact v1.1.6 // indirect github.com/dgraph-io/ristretto v0.1.1 // indirect
github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb // indirect
github.com/danieljoos/wincred v1.2.3 // 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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/getsentry/sentry-go v0.40.0 // indirect github.com/getsentry/sentry-go v0.27.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // 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/snappy v1.0.0 // indirect github.com/golang/glog v1.0.0 // indirect
github.com/google/btree v1.1.2 // indirect github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
github.com/google/flatbuffers v25.9.23+incompatible // indirect github.com/golang/protobuf v1.5.3 // indirect
github.com/hashicorp/go-immutable-radix/v2 v2.0.0 // indirect github.com/golang/snappy v0.0.4 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/google/flatbuffers v1.12.1 // indirect
github.com/klauspost/compress v1.18.2 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/compress v1.16.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/mattn/go-isatty v0.0.17 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // 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.0 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_golang v1.12.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a // indirect
github.com/prometheus/common v0.67.4 // indirect github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/procfs v0.19.2 // indirect github.com/prometheus/procfs v0.7.3 // indirect
github.com/robfig/cron/v3 v3.0.0 // indirect github.com/pwiecz/go-fltk v0.0.0-20240525043121-5313f8a5a643 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rosedblabs/diskhash v0.0.0-20230910084041-289755737e2a // indirect github.com/rogpeppe/go-internal v1.9.0 // indirect
github.com/rosedblabs/wal v1.3.6 // indirect go.opencensus.io v0.22.5 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect golang.org/x/mod v0.18.0 // indirect
github.com/tidwall/btree v1.7.0 // indirect golang.org/x/net v0.23.0 // indirect
github.com/tidwall/buntdb v1.3.2 // indirect golang.org/x/sys v0.19.0 // indirect
github.com/tidwall/gjson v1.14.3 // indirect golang.org/x/text v0.14.0 // indirect
github.com/tidwall/grect v0.1.4 // indirect golang.org/x/tools v0.13.0 // indirect
github.com/tidwall/match v1.1.1 // indirect google.golang.org/protobuf v1.33.0 // 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/voidDB/voidDB v0.1.18 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.2.0 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
github.com/zeebo/xxh3 v1.0.2 // indirect
go.mongodb.org/mongo-driver v1.17.6 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/uint128 v1.2.0 // indirect
modernc.org/cc/v3 v3.40.0 // indirect
modernc.org/ccgo/v3 v3.16.13 // indirect
modernc.org/libc v1.22.5 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/opt v0.1.3 // indirect
modernc.org/strutil v1.1.3 // indirect
modernc.org/token v1.0.1 // indirect
) )

762
go.sum
View File

@@ -1,302 +1,592 @@
al.essio.dev/pkg/shellescape v1.6.0 h1:NxFcEqzFSEVCGN2yq7Huv/9hyCEGVa/TncnOOBBeXHA= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
al.essio.dev/pkg/shellescape v1.6.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
github.com/abcum/lcp v0.0.0-20201209214815-7a3f3840be81 h1:uHogIJ9bXH75ZYrXnVShHIyywFiUZ7OOabwd9Sfd8rw= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
github.com/abcum/lcp v0.0.0-20201209214815-7a3f3840be81/go.mod h1:6ZvnjTZX1LNo1oLpfaJK8h+MXqHxcBFBIwkgsv+xlv0= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
github.com/akrylysov/pogreb v0.10.2 h1:e6PxmeyEhWyi2AKOBIJzAEi4HkiC+lKyCocRGlnDi78= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
github.com/akrylysov/pogreb v0.10.2/go.mod h1:pNs6QmpQ1UlTJKDezuRWmaqkgUE2TuU0YTWyqJZ7+lI= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
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=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/cockroachdb/errors v1.12.0 h1:d7oCs6vuIMUQRVbi6jWWWEJZahLCfJpnJSVobd1/sUo= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cockroachdb/errors v1.12.0/go.mod h1:SvzfYNNBshAVbZ8wzNc/UPK3w1vf0dKDUP41ucAIf7g= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cockroachdb/fifo v0.0.0-20240816210425-c5d0cb0b6fc0 h1:pU88SPhIFid6/k0egdR5V6eALQYq2qbSmukrkgIh/0A= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cockroachdb/fifo v0.0.0-20240816210425-c5d0cb0b6fc0/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M= github.com/cockroachdb/datadriven v1.0.3-0.20230801171734-e384cf455877 h1:1MLK4YpFtIEo3ZtMA5C795Wtv5VuUnrXX7mQG+aHg6o=
github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 h1:ASDL+UJcILMqgNeV5jiqR4j+sTuvQNHdf2chuKj1M5k= github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I=
github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506/go.mod h1:Mw7HqKr2kdtu6aYGn3tPmAftiP3QPX63LdK/zcariIo= github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8=
github.com/cockroachdb/pebble v1.1.5 h1:5AAWCBWbat0uE0blr8qzufZP5tBjkRyy/jWe1QWLnvw= github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE=
github.com/cockroachdb/pebble v1.1.5/go.mod h1:17wO9el1YEigxkP/YtV8NtCivQDgoCyBg5c4VR/eOWo= github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs=
github.com/cockroachdb/redact v1.1.6 h1:zXJBwDZ84xJNlHl1rMyCojqyIxv+7YUpQiJLQ7n4314= github.com/cockroachdb/pebble v1.0.0 h1:WZWlV/s78glZbY2ylUITDOWSVBD3cLjcWPLRPFbHNYg=
github.com/cockroachdb/redact v1.1.6/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= github.com/cockroachdb/pebble v1.0.0/go.mod h1:bynZ3gvVyhlvjLI7PT6dmZ7g76xzJ7HpxfjgkzCGz6s=
github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb h1:3bCgBvB8PbJVMX1ouCcSIxvsqKPYM7gs72o0zC76n9g= github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30=
github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=
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/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/dgraph-io/badger/v4 v4.2.0 h1:kJrlajbXXL9DFTNuhhu9yCx7JJa4qpYWxtE8BzuWsEs=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgraph-io/badger/v4 v4.2.0/go.mod h1:qfCqhPoWDFJRx1gp5QwwyGo8xk1lbHUxvK9nK0OGAak=
github.com/dgraph-io/badger/v4 v4.8.0 h1:JYph1ChBijCw8SLeybvPINizbDKWZ5n/GYbz2yhN/bs= github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
github.com/dgraph-io/badger/v4 v4.8.0/go.mod h1:U6on6e8k/RTbUWxqKR0MvugJuVmkxSNc79ap4917h4w= github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
github.com/dgraph-io/ristretto/v2 v2.3.0 h1:qTQ38m7oIyd4GAed/QkUZyPFNMnvVWyazGXRwvOt5zk= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
github.com/dgraph-io/ristretto/v2 v2.3.0/go.mod h1:gpoRV3VzrEY1a9dWAYV6T1U7YzfgttXdd/ZzL1s9OZM= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38=
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/getsentry/sentry-go v0.40.0 h1:VTJMN9zbTvqDqPwheRVLcp0qcUcM+8eFivvGocAaSbo= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/getsentry/sentry-go v0.40.0/go.mod h1:eRXCoh3uvmjQLY6qu63BjUZnaBu5L5WhMV1RwYO8W5s= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps=
github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/godbus/dbus/v5 v5.2.0 h1:3WexO+U+yg9T70v9FdHr9kCxYlazaAXUhx2VMkbfax8= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/godbus/dbus/v5 v5.2.0/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
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/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ=
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
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/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/google/flatbuffers v25.9.23+incompatible h1:rGZKv+wOb6QPzIdkM2KxhBZCDrA0DeN6DNmRDrqIsQU= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/google/flatbuffers v25.9.23+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw=
github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/go-immutable-radix/v2 v2.0.0 h1:nq9lQ5I71Heg2lRb2/+szuIWKY3Y73d8YKyXyN91WzU= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/go-immutable-radix/v2 v2.0.0/go.mod h1:hgdqLXA4f6NIjRVisM1TJ9aOJVNRqKZj+xDGF6m7PBw= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/ledgerwatch/lmdb-go v1.18.2 h1:6YKp/KYcqGunNRHKZBBhiYADcIcWKzvu5QZv89RhnFQ= github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/ledgerwatch/lmdb-go v1.18.2/go.mod h1:NKRpCxksoTQPyxsUcBiVOe0135uqnJsnf6cElxmOL0o= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/lotusdblabs/lotusdb/v2 v2.1.0 h1:rCBrwED8Po12FzrxxX4zppxoHb2O+sCtddyW4kyDiCQ= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/lotusdblabs/lotusdb/v2 v2.1.0/go.mod h1:MyOEvqL3Hxm3HiBOYZ4BlZBnqCIcc2QQkF34VBD76fk= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mappu/autoconfig v0.4.1 h1:ekO7mzN+beFu7VhNfJxNlL/5wkYcP9PAl9VTG4EDxYs= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mappu/autoconfig v0.4.1/go.mod h1:kca6kaYjqOwkZnY9z5YKo0oI/7Bxe2fMIHhYzXwGHEE= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI=
github.com/mappu/autoconfig v0.5.0 h1:pSune+YObUOZmo8PXhxg41PsmcJJ8mP0L/3juDicUhE= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/mappu/autoconfig v0.5.0/go.mod h1:kca6kaYjqOwkZnY9z5YKo0oI/7Bxe2fMIHhYzXwGHEE= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/mappu/miqt v0.12.1-0.20251106063543-466b60e47edf h1:SmBzNUevLUzu1msJ5xzWH/Kot+GtOtoz0u9la42dRU4= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/mappu/miqt v0.12.1-0.20251106063543-466b60e47edf/go.mod h1:xFg7ADaO1QSkmXPsPODoKe/bydJpRG9fgCYyIDl/h1U= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mattetti/filebuffer v1.0.1 h1:gG7pyfnSIZCxdoKq+cPa8T0hhYtD9NxCdI4D7PTjRLM= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mattetti/filebuffer v1.0.1/go.mod h1:YdMURNDOttIiruleeVr6f56OrMc+MydEnTcXwtkxNVs= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
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/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_golang v1.12.0 h1:C+UIj/QWtmqY13Arb8kwMt5j34/0Z2iKamrJ+ryC0Gg=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/client_golang v1.12.0/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI= github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a h1:CmF68hwI0XsOQ5UwlBopMi2Ow4Pbg32akc4KIVCOm+Y=
github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
github.com/robfig/cron/v3 v3.0.0 h1:kQ6Cb7aHOHTSzNVNEhmp8EcWKLb4CbiMW9h9VyIhO4E= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/robfig/cron/v3 v3.0.0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4=
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/pwiecz/go-fltk v0.0.0-20240525043121-5313f8a5a643 h1:t1fpLVVcboeJvXMiwMCpF1MBiQGg7VyTBqjLEEe+qXM=
github.com/pwiecz/go-fltk v0.0.0-20240525043121-5313f8a5a643/go.mod h1:uMK5daOr9p+ba2BPs5QadbfaqqrHR5TGj13yWGsAsmw=
github.com/redis/go-redis/v9 v9.5.3 h1:fOAp1/uJG+ZtcITgZOfYFmTKPE7n4Vclj1wZFgRciUU=
github.com/redis/go-redis/v9 v9.5.3/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/rosedblabs/diskhash v0.0.0-20230910084041-289755737e2a h1:BNp46nsknQivr3Gxzc6ytzG7xtBscBnLYZIkr0UfCko= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/rosedblabs/diskhash v0.0.0-20230910084041-289755737e2a/go.mod h1:3xvIg+7iOFUL/vMCE/6DwE6Yecb0okVYJBEfpdC/E+8=
github.com/rosedblabs/rosedb/v2 v2.3.6 h1:o8vVOp61hFdORrz/PTosqU21/Z2Bug5I7cy1D3MZh2M=
github.com/rosedblabs/rosedb/v2 v2.3.6/go.mod h1:/de9n2CoYaAGBDxZTJC5Jb0LCQOtoA3GOom+9QD9Z98=
github.com/rosedblabs/wal v1.3.6 h1:oxZYTPX/u4JuGDW98wQ1YamWqerlrlSUFKhgP6Gd/Ao=
github.com/rosedblabs/wal v1.3.6/go.mod h1:wdq54KJUyVTOv1uddMc6Cdh2d/YCIo8yjcwJAb1RCEM=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/starskey-io/starskey v0.1.9 h1:lABmD5KQgkpJZTCwSt+BHSOPXe82B9smbuScRL6T8Zk=
github.com/starskey-io/starskey v0.1.9/go.mod h1:qly4ec2C/4Y45jhpL+q4m+Uxzg3mjj0t7RjpJslB3ao=
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.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= github.com/ying32/govcl v2.2.3+incompatible h1:Iyfcl26yNE1USm+3uG+btQyhkoFIV18+VITrUdHu8Lw=
github.com/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI= github.com/ying32/govcl v2.2.3+incompatible/go.mod h1:yZVtbJ9Md1nAVxtHKIriKZn4K6TQYqI1en3sN/m9FJ8=
github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
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/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/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs=
github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.etcd.io/bbolt v1.4.0-alpha.1 h1:3yrqQzbRRPFPdOMWS/QQIVxVnzSkAZQYeWlZFv1kbj4=
github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s= go.etcd.io/bbolt v1.4.0-alpha.1/go.mod h1:S/Z/Nm3iuOnyO1W4XuFfPci51Gj6F1Hv0z8hisyYYOw=
github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.mills.io/bitcask/v2 v2.1.5 h1:SKPa0TPasJJZ8rNbLDvV3+lRXvdyQ0mwBobm2RH7J7w= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
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/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.mongodb.org/mongo-driver/v2 v2.4.1 h1:hGDMngUao03OVQ6sgV5csk+RWOIkF+CuLsTPobNMGNI=
go.mongodb.org/mongo-driver/v2 v2.4.1/go.mod h1:jHeEDJHJq7tm6ZF45Issun9dbogjfnPySb1vXA7EeAI=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
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/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
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=
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=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 h1:MDfG8Cvcqlt9XXrmEiD4epKn7VJHZO84hejP9Jmp0MM= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/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-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/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
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-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sqlite v1.24.0 h1:EsClRIWHGhLTCX44p+Ri/JLD+vFGo0QGjasg2/F9TlI=
modernc.org/sqlite v1.24.0/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY=
modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

91
images.go Normal file
View File

@@ -0,0 +1,91 @@
package main
import (
"embed"
"github.com/ying32/govcl/vcl"
)
//go:embed assets/*
var assetsFs embed.FS
const (
imgAdd int32 = iota
imgArrowRefresh
imgChartBar
imgDatabase
imgDatabaseAdd
imgDatabaseDelete
imgDatabaseLightning
imgDatabaseSave
imgDelete
imgLightning
imgLightningGo
imgPencil
imgPencilAdd
imgPencilDelete
imgPencilGo
imgResultsetNext
imgTable
imgTableAdd
imgTableDelete
imgTableSave
imgVendorCockroach
imgVendorDebian
imgVendorDgraph
imgVendorGithub
imgVendorMySQL
imgVendorRedis
imgVendorSqlite
)
func loadImages(owner vcl.IComponent) *vcl.TImageList {
mustLoad := func(n string) *vcl.TBitmap {
imgData, err := assetsFs.ReadFile(n)
if err != nil {
panic(err)
}
png := vcl.NewPngImage()
png.LoadFromBytes(imgData)
ret := vcl.NewBitmap()
ret.Assign(png)
return ret
}
ilist := vcl.NewImageList(owner)
// ls assets | sort | sed -re 's~(.+)~ilist.Add(mustLoad("assets/\1"), nil)~'
ilist.Add(mustLoad("assets/add.png"), nil)
ilist.Add(mustLoad("assets/arrow_refresh.png"), nil)
ilist.Add(mustLoad("assets/chart_bar.png"), nil)
ilist.Add(mustLoad("assets/database.png"), nil)
ilist.Add(mustLoad("assets/database_add.png"), nil)
ilist.Add(mustLoad("assets/database_delete.png"), nil)
ilist.Add(mustLoad("assets/database_lightning.png"), nil)
ilist.Add(mustLoad("assets/database_save.png"), nil)
ilist.Add(mustLoad("assets/delete.png"), nil)
ilist.Add(mustLoad("assets/lightning.png"), nil)
ilist.Add(mustLoad("assets/lightning_go.png"), nil)
ilist.Add(mustLoad("assets/pencil.png"), nil)
ilist.Add(mustLoad("assets/pencil_add.png"), nil)
ilist.Add(mustLoad("assets/pencil_delete.png"), nil)
ilist.Add(mustLoad("assets/pencil_go.png"), nil)
ilist.Add(mustLoad("assets/resultset_next.png"), nil)
ilist.Add(mustLoad("assets/table.png"), nil)
ilist.Add(mustLoad("assets/table_add.png"), nil)
ilist.Add(mustLoad("assets/table_delete.png"), nil)
ilist.Add(mustLoad("assets/table_save.png"), nil)
ilist.Add(mustLoad("assets/vendor_cockroach.png"), nil)
ilist.Add(mustLoad("assets/vendor_debian.png"), nil)
ilist.Add(mustLoad("assets/vendor_dgraph.png"), nil)
ilist.Add(mustLoad("assets/vendor_github.png"), nil)
ilist.Add(mustLoad("assets/vendor_mysql.png"), nil)
ilist.Add(mustLoad("assets/vendor_redis.png"), nil)
ilist.Add(mustLoad("assets/vendor_sqlite.png"), nil)
return ilist
}

View File

@@ -3,30 +3,34 @@ package main
import ( import (
"errors" "errors"
qt "github.com/mappu/miqt/qt6" "github.com/ying32/govcl/vcl"
) )
var ErrNavNotExist error = errors.New("The selected item no longer exists") var ErrNavNotExist error = errors.New("The selected item no longer exists")
var ErrNotSupported error = errors.New("Unsupported action for this database type")
type contextAction struct { type contextAction struct {
Name string Name string
Callback func(sender *qt.QTreeWidgetItem, bucketPath []string) error Callback func(sender vcl.IComponent, ndata *navData) error
} }
// loadedDatabase is a DB-agnostic interface for each loaded database. // loadedDatabase is a DB-agnostic interface for each loaded database.
type loadedDatabase interface { type loadedDatabase interface {
DisplayName() string
DriverName() string DriverName() string
Properties(bucketPath []string) (string, error) RootElement() *vcl.TTreeNode
RenderForNav(f *tableState, bucketPath []string) error RenderForNav(f *TMainForm, ndata *navData) error
NavChildren(bucketPath []string) ([]string, error) ApplyChanges(f *TMainForm, ndata *navData) error
NavContext(bucketPath []string) ([]contextAction, error) ExecQuery(query string, resultArea *vcl.TStringGrid) error
NavChildren(ndata *navData) ([]string, error)
NavContext(ndata *navData) ([]contextAction, error)
Keepalive(ndata *navData)
Close() Close()
} }
type queryableLoadedDatabase interface { // navData is the .Data() pointer for each TTreeNode in the left-hand tree.
ExecQuery(query string, bucketPath []string, resultArea *tableState) error type navData struct {
} ld loadedDatabase
childrenLoaded bool
type editableLoadedDatabase interface { bucketPath []string
ApplyChanges(f *tableState, bucketPath []string) error
} }

1131
main.go

File diff suppressed because it is too large Load Diff

View File

@@ -1,404 +0,0 @@
// Generated by miqt-uic. To update this file, edit the .ui file in
// Qt Designer, and then run 'go generate'.
//
//go:generate miqt-uic -InFile mainwindow.ui -OutFile mainwindow.go -Qt6
package main
import (
qt "github.com/mappu/miqt/qt6"
)
type MainWindowUi struct {
MainWindow *qt.QMainWindow
centralwidget *qt.QWidget
gridLayout *qt.QGridLayout
splitter *qt.QSplitter
Buckets *qt.QTreeWidget
tabWidget *qt.QTabWidget
tabProperties *qt.QWidget
gridLayout_2 *qt.QGridLayout
propertiesBox *qt.QTextEdit
tabData *qt.QWidget
verticalLayout *qt.QVBoxLayout
contentBox *qt.QTableView
tabQuery *qt.QWidget
verticalLayout_2 *qt.QVBoxLayout
splitter_2 *qt.QSplitter
queryInput *qt.QPlainTextEdit
queryResult *qt.QTableView
menubar *qt.QMenuBar
menu_File *qt.QMenu
menu_Query *qt.QMenu
menu_Help *qt.QMenu
menu_Data *qt.QMenu
menu_Tools *qt.QMenu
statusbar *qt.QStatusBar
tbConnect *qt.QToolBar
tbData *qt.QToolBar
tbQuery *qt.QToolBar
actionE_xit *qt.QAction
mnuExecute *qt.QAction
mnuDriverVersions *qt.QAction
mnuHelpAbout *qt.QAction
actionConnect *qt.QAction
actionRefresh *qt.QAction
actionAbout_Qt *qt.QAction
actionAddRow *qt.QAction
actionDelete_row *qt.QAction
actionApply_changes *qt.QAction
actionConnectionManager *qt.QAction
actionCreate_Bolt_database_from_zip *qt.QAction
actionSet_cell_to_null *qt.QAction
}
// NewMainWindowUi creates all Qt widget classes for MainWindow.
func NewMainWindowUi() *MainWindowUi {
ui := &MainWindowUi{}
ui.MainWindow = qt.NewQMainWindow(nil)
MainWindow__objectName := qt.NewQAnyStringView3("MainWindow")
ui.MainWindow.SetObjectName(*MainWindow__objectName)
MainWindow__objectName.Delete() // setter copied value
ui.MainWindow.Resize(1280, 640)
icon0 := qt.NewQIcon()
icon0.AddFile4(":/assets/database_lightning.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.MainWindow.SetWindowIcon(icon0)
ui.actionE_xit = qt.NewQAction()
actionE_xit__objectName := qt.NewQAnyStringView3("actionE_xit")
ui.actionE_xit.SetObjectName(*actionE_xit__objectName)
actionE_xit__objectName.Delete() // setter copied value
/* miqt-uic: no handler for QAction property 'menuRole' */
ui.mnuExecute = qt.NewQAction()
mnuExecute__objectName := qt.NewQAnyStringView3("mnuExecute")
ui.mnuExecute.SetObjectName(*mnuExecute__objectName)
mnuExecute__objectName.Delete() // setter copied value
icon1 := qt.NewQIcon()
icon1.AddFile4(":/assets/resultset_next.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.mnuExecute.SetIcon(icon1)
ui.mnuDriverVersions = qt.NewQAction()
mnuDriverVersions__objectName := qt.NewQAnyStringView3("mnuDriverVersions")
ui.mnuDriverVersions.SetObjectName(*mnuDriverVersions__objectName)
mnuDriverVersions__objectName.Delete() // setter copied value
ui.mnuHelpAbout = qt.NewQAction()
mnuHelpAbout__objectName := qt.NewQAnyStringView3("mnuHelpAbout")
ui.mnuHelpAbout.SetObjectName(*mnuHelpAbout__objectName)
mnuHelpAbout__objectName.Delete() // setter copied value
icon2 := qt.NewQIcon()
icon2.AddFile4(":/assets/help.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.mnuHelpAbout.SetIcon(icon2)
/* miqt-uic: no handler for QAction property 'menuRole' */
ui.actionConnect = qt.NewQAction()
actionConnect__objectName := qt.NewQAnyStringView3("actionConnect")
ui.actionConnect.SetObjectName(*actionConnect__objectName)
actionConnect__objectName.Delete() // setter copied value
icon3 := qt.NewQIcon()
icon3.AddFile4(":/assets/database_add.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.actionConnect.SetIcon(icon3)
ui.actionRefresh = qt.NewQAction()
actionRefresh__objectName := qt.NewQAnyStringView3("actionRefresh")
ui.actionRefresh.SetObjectName(*actionRefresh__objectName)
actionRefresh__objectName.Delete() // setter copied value
icon4 := qt.NewQIcon()
icon4.AddFile4(":/assets/arrow_refresh.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.actionRefresh.SetIcon(icon4)
/* miqt-uic: no handler for QAction property 'menuRole' */
ui.actionAbout_Qt = qt.NewQAction()
actionAbout_Qt__objectName := qt.NewQAnyStringView3("actionAbout_Qt")
ui.actionAbout_Qt.SetObjectName(*actionAbout_Qt__objectName)
actionAbout_Qt__objectName.Delete() // setter copied value
icon5 := qt.NewQIcon()
icon5.AddFile4(":/assets/vendor_qt.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.actionAbout_Qt.SetIcon(icon5)
/* miqt-uic: no handler for QAction property 'menuRole' */
ui.actionAddRow = qt.NewQAction()
actionAddRow__objectName := qt.NewQAnyStringView3("actionAddRow")
ui.actionAddRow.SetObjectName(*actionAddRow__objectName)
actionAddRow__objectName.Delete() // setter copied value
icon6 := qt.NewQIcon()
icon6.AddFile4(":/assets/add.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.actionAddRow.SetIcon(icon6)
/* miqt-uic: no handler for QAction property 'menuRole' */
ui.actionDelete_row = qt.NewQAction()
actionDelete_row__objectName := qt.NewQAnyStringView3("actionDelete_row")
ui.actionDelete_row.SetObjectName(*actionDelete_row__objectName)
actionDelete_row__objectName.Delete() // setter copied value
icon7 := qt.NewQIcon()
icon7.AddFile4(":/assets/delete.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.actionDelete_row.SetIcon(icon7)
/* miqt-uic: no handler for QAction property 'menuRole' */
ui.actionApply_changes = qt.NewQAction()
actionApply_changes__objectName := qt.NewQAnyStringView3("actionApply_changes")
ui.actionApply_changes.SetObjectName(*actionApply_changes__objectName)
actionApply_changes__objectName.Delete() // setter copied value
icon8 := qt.NewQIcon()
icon8.AddFile4(":/assets/pencil_go.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.actionApply_changes.SetIcon(icon8)
/* miqt-uic: no handler for QAction property 'menuRole' */
ui.actionConnectionManager = qt.NewQAction()
actionConnectionManager__objectName := qt.NewQAnyStringView3("actionConnectionManager")
ui.actionConnectionManager.SetObjectName(*actionConnectionManager__objectName)
actionConnectionManager__objectName.Delete() // setter copied value
icon9 := qt.NewQIcon()
icon9.AddFile4(":/assets/database_key.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.actionConnectionManager.SetIcon(icon9)
/* miqt-uic: no handler for QAction property 'menuRole' */
ui.actionCreate_Bolt_database_from_zip = qt.NewQAction()
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)
actionCreate_Bolt_database_from_zip__objectName.Delete() // setter copied value
icon10 := qt.NewQIcon()
icon10.AddFile4(":/assets/compress.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.actionCreate_Bolt_database_from_zip.SetIcon(icon10)
ui.actionSet_cell_to_null = qt.NewQAction()
actionSet_cell_to_null__objectName := qt.NewQAnyStringView3("actionSet_cell_to_null")
ui.actionSet_cell_to_null.SetObjectName(*actionSet_cell_to_null__objectName)
actionSet_cell_to_null__objectName.Delete() // setter copied value
icon11 := qt.NewQIcon()
icon11.AddFile4(":/assets/note_delete.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.actionSet_cell_to_null.SetIcon(icon11)
/* miqt-uic: no handler for QAction property 'menuRole' */
ui.centralwidget = qt.NewQWidget(ui.MainWindow.QWidget)
centralwidget__objectName := qt.NewQAnyStringView3("centralwidget")
ui.centralwidget.SetObjectName(*centralwidget__objectName)
centralwidget__objectName.Delete() // setter copied value
ui.gridLayout = qt.NewQGridLayout(ui.centralwidget)
gridLayout__objectName := qt.NewQAnyStringView3("gridLayout")
ui.gridLayout.SetObjectName(*gridLayout__objectName)
gridLayout__objectName.Delete() // setter copied value
ui.gridLayout.SetContentsMargins(11, 11, 11, 11)
ui.gridLayout.SetSpacing(6)
ui.splitter = qt.NewQSplitter(ui.centralwidget)
splitter__objectName := qt.NewQAnyStringView3("splitter")
ui.splitter.SetObjectName(*splitter__objectName)
splitter__objectName.Delete() // setter copied value
ui.splitter.SetOrientation(qt.Horizontal)
ui.splitter.SetChildrenCollapsible(false)
ui.Buckets = qt.NewQTreeWidget(ui.splitter.QWidget)
Buckets__objectName := qt.NewQAnyStringView3("Buckets")
ui.Buckets.SetObjectName(*Buckets__objectName)
Buckets__objectName.Delete() // setter copied value
ui.Buckets.SetUniformRowHeights(true)
ui.Buckets.SetHeaderHidden(true)
/* miqt-uic: no handler for Buckets attribute 'headerVisible' */
ui.Buckets.HeaderItem().SetText(0, "1")
ui.splitter.AddWidget(ui.Buckets.QWidget)
ui.tabWidget = qt.NewQTabWidget(ui.splitter.QWidget)
tabWidget__objectName := qt.NewQAnyStringView3("tabWidget")
ui.tabWidget.SetObjectName(*tabWidget__objectName)
tabWidget__objectName.Delete() // setter copied value
tabWidget__sizePolicy := qt.NewQSizePolicy()
tabWidget__sizePolicy.SetHorizontalPolicy(qt.QSizePolicy__Expanding)
tabWidget__sizePolicy.SetVerticalPolicy(qt.QSizePolicy__Expanding)
tabWidget__sizePolicy.SetHorizontalStretch(1)
tabWidget__sizePolicy.SetVerticalStretch(0)
tabWidget__sizePolicy.SetHeightForWidth(ui.tabWidget.SizePolicy().HasHeightForWidth())
ui.tabWidget.SetSizePolicy(*tabWidget__sizePolicy)
tabWidget__sizePolicy.Delete() // setter copies values
ui.tabProperties = qt.NewQWidget(ui.tabWidget.QWidget)
tabProperties__objectName := qt.NewQAnyStringView3("tabProperties")
ui.tabProperties.SetObjectName(*tabProperties__objectName)
tabProperties__objectName.Delete() // setter copied value
ui.gridLayout_2 = qt.NewQGridLayout(ui.tabProperties)
gridLayout_2__objectName := qt.NewQAnyStringView3("gridLayout_2")
ui.gridLayout_2.SetObjectName(*gridLayout_2__objectName)
gridLayout_2__objectName.Delete() // setter copied value
ui.gridLayout_2.SetContentsMargins(11, 11, 11, 11)
ui.gridLayout_2.SetSpacing(6)
ui.propertiesBox = qt.NewQTextEdit(ui.tabProperties)
propertiesBox__objectName := qt.NewQAnyStringView3("propertiesBox")
ui.propertiesBox.SetObjectName(*propertiesBox__objectName)
propertiesBox__objectName.Delete() // setter copied value
ui.propertiesBox.SetFrameShape(qt.QFrame__NoFrame)
ui.propertiesBox.SetReadOnly(true)
ui.gridLayout_2.AddWidget2(ui.propertiesBox.QWidget, 0, 0)
icon12 := qt.NewQIcon()
icon12.AddFile4(":/assets/chart_bar.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.tabWidget.AddTab2(ui.tabProperties, icon12, "")
ui.tabData = qt.NewQWidget(ui.tabWidget.QWidget)
tabData__objectName := qt.NewQAnyStringView3("tabData")
ui.tabData.SetObjectName(*tabData__objectName)
tabData__objectName.Delete() // setter copied value
ui.verticalLayout = qt.NewQVBoxLayout(ui.tabData)
verticalLayout__objectName := qt.NewQAnyStringView3("verticalLayout")
ui.verticalLayout.SetObjectName(*verticalLayout__objectName)
verticalLayout__objectName.Delete() // setter copied value
ui.verticalLayout.SetContentsMargins(11, 11, 11, 11)
ui.verticalLayout.SetSpacing(6)
ui.contentBox = qt.NewQTableView(ui.tabData)
contentBox__objectName := qt.NewQAnyStringView3("contentBox")
ui.contentBox.SetObjectName(*contentBox__objectName)
contentBox__objectName.Delete() // setter copied value
ui.contentBox.SetVerticalScrollMode(qt.QAbstractItemView__ScrollPerPixel)
ui.contentBox.SetHorizontalScrollMode(qt.QAbstractItemView__ScrollPerPixel)
ui.verticalLayout.AddWidget(ui.contentBox.QWidget)
icon13 := qt.NewQIcon()
icon13.AddFile4(":/assets/table.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.tabWidget.AddTab2(ui.tabData, icon13, "")
ui.tabQuery = qt.NewQWidget(ui.tabWidget.QWidget)
tabQuery__objectName := qt.NewQAnyStringView3("tabQuery")
ui.tabQuery.SetObjectName(*tabQuery__objectName)
tabQuery__objectName.Delete() // setter copied value
ui.verticalLayout_2 = qt.NewQVBoxLayout(ui.tabQuery)
verticalLayout_2__objectName := qt.NewQAnyStringView3("verticalLayout_2")
ui.verticalLayout_2.SetObjectName(*verticalLayout_2__objectName)
verticalLayout_2__objectName.Delete() // setter copied value
ui.verticalLayout_2.SetContentsMargins(11, 11, 11, 11)
ui.verticalLayout_2.SetSpacing(6)
ui.splitter_2 = qt.NewQSplitter(ui.tabQuery)
splitter_2__objectName := qt.NewQAnyStringView3("splitter_2")
ui.splitter_2.SetObjectName(*splitter_2__objectName)
splitter_2__objectName.Delete() // setter copied value
ui.splitter_2.SetOrientation(qt.Vertical)
ui.splitter_2.SetChildrenCollapsible(false)
ui.queryInput = qt.NewQPlainTextEdit(ui.splitter_2.QWidget)
queryInput__objectName := qt.NewQAnyStringView3("queryInput")
ui.queryInput.SetObjectName(*queryInput__objectName)
queryInput__objectName.Delete() // setter copied value
/* miqt-uic: no handler for queryInput property 'font' */
ui.splitter_2.AddWidget(ui.queryInput.QWidget)
ui.queryResult = qt.NewQTableView(ui.splitter_2.QWidget)
queryResult__objectName := qt.NewQAnyStringView3("queryResult")
ui.queryResult.SetObjectName(*queryResult__objectName)
queryResult__objectName.Delete() // setter copied value
ui.queryResult.SetVerticalScrollMode(qt.QAbstractItemView__ScrollPerPixel)
ui.queryResult.SetHorizontalScrollMode(qt.QAbstractItemView__ScrollPerPixel)
ui.splitter_2.AddWidget(ui.queryResult.QWidget)
ui.verticalLayout_2.AddWidget(ui.splitter_2.QWidget)
icon14 := qt.NewQIcon()
icon14.AddFile4(":/assets/lightning.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.tabWidget.AddTab2(ui.tabQuery, icon14, "")
ui.splitter.AddWidget(ui.tabWidget.QWidget)
ui.gridLayout.AddWidget2(ui.splitter.QWidget, 0, 0)
ui.MainWindow.SetCentralWidget(ui.centralwidget) // Set central widget
ui.menubar = qt.NewQMenuBar(ui.MainWindow.QWidget)
menubar__objectName := qt.NewQAnyStringView3("menubar")
ui.menubar.SetObjectName(*menubar__objectName)
menubar__objectName.Delete() // setter copied value
ui.menubar.Resize(1280, 29)
ui.menu_File = qt.NewQMenu(ui.menubar.QWidget)
menu_File__objectName := qt.NewQAnyStringView3("menu_File")
ui.menu_File.SetObjectName(*menu_File__objectName)
menu_File__objectName.Delete() // setter copied value
ui.menu_File.QWidget.AddAction(ui.actionConnectionManager)
ui.menu_File.QWidget.AddAction(ui.actionConnect)
ui.menu_File.AddSeparator()
ui.menu_File.QWidget.AddAction(ui.actionE_xit)
ui.menu_Query = qt.NewQMenu(ui.menubar.QWidget)
menu_Query__objectName := qt.NewQAnyStringView3("menu_Query")
ui.menu_Query.SetObjectName(*menu_Query__objectName)
menu_Query__objectName.Delete() // setter copied value
ui.menu_Query.QWidget.AddAction(ui.mnuExecute)
ui.menu_Help = qt.NewQMenu(ui.menubar.QWidget)
menu_Help__objectName := qt.NewQAnyStringView3("menu_Help")
ui.menu_Help.SetObjectName(*menu_Help__objectName)
menu_Help__objectName.Delete() // setter copied value
ui.menu_Help.QWidget.AddAction(ui.mnuDriverVersions)
ui.menu_Help.AddSeparator()
ui.menu_Help.QWidget.AddAction(ui.actionAbout_Qt)
ui.menu_Help.QWidget.AddAction(ui.mnuHelpAbout)
ui.menu_Data = qt.NewQMenu(ui.menubar.QWidget)
menu_Data__objectName := qt.NewQAnyStringView3("menu_Data")
ui.menu_Data.SetObjectName(*menu_Data__objectName)
menu_Data__objectName.Delete() // setter copied value
ui.menu_Data.QWidget.AddAction(ui.actionRefresh)
ui.menu_Data.AddSeparator()
ui.menu_Data.QWidget.AddAction(ui.actionAddRow)
ui.menu_Data.QWidget.AddAction(ui.actionDelete_row)
ui.menu_Data.QWidget.AddAction(ui.actionSet_cell_to_null)
ui.menu_Data.QWidget.AddAction(ui.actionApply_changes)
ui.menu_Tools = qt.NewQMenu(ui.menubar.QWidget)
menu_Tools__objectName := qt.NewQAnyStringView3("menu_Tools")
ui.menu_Tools.SetObjectName(*menu_Tools__objectName)
menu_Tools__objectName.Delete() // setter copied value
ui.menu_Tools.QWidget.AddAction(ui.actionCreate_Bolt_database_from_zip)
ui.menubar.AddMenu(ui.menu_File)
ui.menubar.AddMenu(ui.menu_Data)
ui.menubar.AddMenu(ui.menu_Query)
ui.menubar.AddMenu(ui.menu_Tools)
ui.menubar.AddMenu(ui.menu_Help)
ui.MainWindow.SetMenuBar(ui.menubar)
ui.statusbar = qt.NewQStatusBar(ui.MainWindow.QWidget)
statusbar__objectName := qt.NewQAnyStringView3("statusbar")
ui.statusbar.SetObjectName(*statusbar__objectName)
statusbar__objectName.Delete() // setter copied value
ui.MainWindow.SetStatusBar(ui.statusbar)
ui.tbConnect = qt.NewQToolBar(ui.MainWindow.QWidget)
tbConnect__objectName := qt.NewQAnyStringView3("tbConnect")
ui.tbConnect.SetObjectName(*tbConnect__objectName)
tbConnect__objectName.Delete() // setter copied value
/* miqt-uic: no handler for tbConnect property 'iconSize' */
ui.tbConnect.SetToolButtonStyle(qt.ToolButtonIconOnly)
ui.tbConnect.SetFloatable(false)
ui.MainWindow.AddToolBar(qt.TopToolBarArea, ui.tbConnect)
/* miqt-uic: no handler for tbConnect attribute 'toolBarBreak' */
ui.tbConnect.QWidget.AddAction(ui.actionConnectionManager)
ui.tbConnect.QWidget.AddAction(ui.actionConnect)
ui.tbData = qt.NewQToolBar(ui.MainWindow.QWidget)
tbData__objectName := qt.NewQAnyStringView3("tbData")
ui.tbData.SetObjectName(*tbData__objectName)
tbData__objectName.Delete() // setter copied value
/* miqt-uic: no handler for tbData property 'iconSize' */
ui.tbData.SetFloatable(false)
ui.MainWindow.AddToolBar(qt.TopToolBarArea, ui.tbData)
/* miqt-uic: no handler for tbData attribute 'toolBarBreak' */
ui.tbData.QWidget.AddAction(ui.actionRefresh)
ui.tbData.QWidget.AddAction(ui.actionAddRow)
ui.tbData.QWidget.AddAction(ui.actionDelete_row)
ui.tbData.QWidget.AddAction(ui.actionSet_cell_to_null)
ui.tbData.QWidget.AddAction(ui.actionApply_changes)
ui.tbQuery = qt.NewQToolBar(ui.MainWindow.QWidget)
tbQuery__objectName := qt.NewQAnyStringView3("tbQuery")
ui.tbQuery.SetObjectName(*tbQuery__objectName)
tbQuery__objectName.Delete() // setter copied value
/* miqt-uic: no handler for tbQuery property 'iconSize' */
ui.tbQuery.SetFloatable(false)
ui.MainWindow.AddToolBar(qt.TopToolBarArea, ui.tbQuery)
/* miqt-uic: no handler for tbQuery attribute 'toolBarBreak' */
ui.tbQuery.QWidget.AddAction(ui.mnuExecute)
ui.Retranslate()
ui.tabWidget.SetCurrentIndex(0)
return ui
}
// Retranslate reapplies all text translations.
func (ui *MainWindowUi) Retranslate() {
ui.MainWindow.SetWindowTitle(qt.QCoreApplication_Tr("QBolt"))
ui.actionE_xit.SetText(qt.QMainWindow_Tr("E&xit"))
ui.mnuExecute.SetText(qt.QMainWindow_Tr("E&xecute"))
ui.mnuExecute.SetShortcut(qt.NewQKeySequence2(qt.QMainWindow_Tr("F9")))
ui.mnuDriverVersions.SetText(qt.QMainWindow_Tr("Driver versions..."))
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.actionConnect.SetText(qt.QMainWindow_Tr("Connect..."))
ui.actionConnect.SetShortcut(qt.NewQKeySequence2(qt.QMainWindow_Tr("Ctrl+O")))
ui.actionRefresh.SetText(qt.QMainWindow_Tr("Refresh"))
ui.actionRefresh.SetShortcut(qt.NewQKeySequence2(qt.QMainWindow_Tr("F5")))
ui.actionAbout_Qt.SetText(qt.QMainWindow_Tr("About Qt"))
ui.actionAddRow.SetText(qt.QMainWindow_Tr("Add row"))
ui.actionAddRow.SetToolTip(qt.QMainWindow_Tr("Add row"))
ui.actionDelete_row.SetText(qt.QMainWindow_Tr("Delete row"))
ui.actionApply_changes.SetText(qt.QMainWindow_Tr("Apply changes"))
ui.actionConnectionManager.SetText(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.actionSet_cell_to_null.SetText(qt.QMainWindow_Tr("Set cell to null"))
ui.tabWidget.SetTabText(ui.tabWidget.IndexOf(ui.tabProperties), qt.QTabWidget_Tr("Properties"))
ui.tabWidget.SetTabText(ui.tabWidget.IndexOf(ui.tabData), qt.QTabWidget_Tr("Data"))
ui.tabWidget.SetTabText(ui.tabWidget.IndexOf(ui.tabQuery), qt.QTabWidget_Tr("Query"))
ui.menu_File.SetTitle(qt.QMenuBar_Tr("&File"))
ui.menu_Query.SetTitle(qt.QMenuBar_Tr("&Query"))
ui.menu_Help.SetTitle(qt.QMenuBar_Tr("&Help"))
ui.menu_Data.SetTitle(qt.QMenuBar_Tr("&Data"))
ui.menu_Tools.SetTitle(qt.QMenuBar_Tr("&Tools"))
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

@@ -1,429 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1280</width>
<height>640</height>
</rect>
</property>
<property name="windowTitle">
<string>QBolt</string>
</property>
<property name="windowIcon">
<iconset resource="embed.qrc">
<normaloff>:/assets/database_lightning.png</normaloff>:/assets/database_lightning.png</iconset>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="childrenCollapsible">
<bool>false</bool>
</property>
<widget class="QTreeWidget" name="Buckets">
<property name="uniformRowHeights">
<bool>true</bool>
</property>
<property name="headerHidden">
<bool>true</bool>
</property>
<attribute name="headerVisible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string notr="true">1</string>
</property>
</column>
</widget>
<widget class="QTabWidget" name="tabWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="tabProperties">
<attribute name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/chart_bar.png</normaloff>:/assets/chart_bar.png</iconset>
</attribute>
<attribute name="title">
<string>Properties</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<widget class="QTextEdit" name="propertiesBox">
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tabData">
<attribute name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/table.png</normaloff>:/assets/table.png</iconset>
</attribute>
<attribute name="title">
<string>Data</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTableView" name="contentBox">
<property name="verticalScrollMode">
<enum>QAbstractItemView::ScrollMode::ScrollPerPixel</enum>
</property>
<property name="horizontalScrollMode">
<enum>QAbstractItemView::ScrollMode::ScrollPerPixel</enum>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tabQuery">
<attribute name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/lightning.png</normaloff>:/assets/lightning.png</iconset>
</attribute>
<attribute name="title">
<string>Query</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QSplitter" name="splitter_2">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="childrenCollapsible">
<bool>false</bool>
</property>
<widget class="QPlainTextEdit" name="queryInput">
<property name="font">
<font>
<family>Monospace</family>
</font>
</property>
</widget>
<widget class="QTableView" name="queryResult">
<property name="verticalScrollMode">
<enum>QAbstractItemView::ScrollMode::ScrollPerPixel</enum>
</property>
<property name="horizontalScrollMode">
<enum>QAbstractItemView::ScrollMode::ScrollPerPixel</enum>
</property>
</widget>
</widget>
</item>
</layout>
</widget>
</widget>
</widget>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1280</width>
<height>29</height>
</rect>
</property>
<widget class="QMenu" name="menu_File">
<property name="title">
<string>&amp;File</string>
</property>
<addaction name="actionConnectionManager"/>
<addaction name="actionConnect"/>
<addaction name="separator"/>
<addaction name="actionE_xit"/>
</widget>
<widget class="QMenu" name="menu_Query">
<property name="title">
<string>&amp;Query</string>
</property>
<addaction name="mnuExecute"/>
</widget>
<widget class="QMenu" name="menu_Help">
<property name="title">
<string>&amp;Help</string>
</property>
<addaction name="mnuDriverVersions"/>
<addaction name="separator"/>
<addaction name="actionAbout_Qt"/>
<addaction name="mnuHelpAbout"/>
</widget>
<widget class="QMenu" name="menu_Data">
<property name="title">
<string>&amp;Data</string>
</property>
<addaction name="actionRefresh"/>
<addaction name="separator"/>
<addaction name="actionAddRow"/>
<addaction name="actionDelete_row"/>
<addaction name="actionSet_cell_to_null"/>
<addaction name="actionApply_changes"/>
</widget>
<widget class="QMenu" name="menu_Tools">
<property name="title">
<string>&amp;Tools</string>
</property>
<addaction name="actionCreate_Bolt_database_from_zip"/>
</widget>
<addaction name="menu_File"/>
<addaction name="menu_Data"/>
<addaction name="menu_Query"/>
<addaction name="menu_Tools"/>
<addaction name="menu_Help"/>
</widget>
<widget class="QStatusBar" name="statusbar"/>
<widget class="QToolBar" name="tbConnect">
<property name="windowTitle">
<string>Connection Toolbar</string>
</property>
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonStyle::ToolButtonIconOnly</enum>
</property>
<property name="floatable">
<bool>false</bool>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="actionConnectionManager"/>
<addaction name="actionConnect"/>
</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="actionAddRow"/>
<addaction name="actionDelete_row"/>
<addaction name="actionSet_cell_to_null"/>
<addaction name="actionApply_changes"/>
</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"/>
</widget>
<action name="actionE_xit">
<property name="text">
<string>E&amp;xit</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::QuitRole</enum>
</property>
</action>
<action name="mnuExecute">
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/resultset_next.png</normaloff>:/assets/resultset_next.png</iconset>
</property>
<property name="text">
<string>E&amp;xecute</string>
</property>
<property name="shortcut">
<string>F9</string>
</property>
</action>
<action name="mnuDriverVersions">
<property name="text">
<string>Driver versions...</string>
</property>
</action>
<action name="mnuHelpAbout">
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/help.png</normaloff>:/assets/help.png</iconset>
</property>
<property name="text">
<string>&amp;About QBolt</string>
</property>
<property name="toolTip">
<string>About QBolt</string>
</property>
<property name="shortcut">
<string>F1</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::AboutRole</enum>
</property>
</action>
<action name="actionConnect">
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/database_add.png</normaloff>:/assets/database_add.png</iconset>
</property>
<property name="text">
<string>Connect...</string>
</property>
<property name="shortcut">
<string>Ctrl+O</string>
</property>
</action>
<action name="actionRefresh">
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/arrow_refresh.png</normaloff>:/assets/arrow_refresh.png</iconset>
</property>
<property name="text">
<string>Refresh</string>
</property>
<property name="shortcut">
<string>F5</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
<action name="actionAbout_Qt">
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/vendor_qt.png</normaloff>:/assets/vendor_qt.png</iconset>
</property>
<property name="text">
<string>About Qt</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::AboutQtRole</enum>
</property>
</action>
<action name="actionAddRow">
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/add.png</normaloff>:/assets/add.png</iconset>
</property>
<property name="text">
<string>Add row</string>
</property>
<property name="toolTip">
<string>Add row</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
<action name="actionDelete_row">
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/delete.png</normaloff>:/assets/delete.png</iconset>
</property>
<property name="text">
<string>Delete row</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
<action name="actionApply_changes">
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/pencil_go.png</normaloff>:/assets/pencil_go.png</iconset>
</property>
<property name="text">
<string>Apply changes</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
<action name="actionConnectionManager">
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/database_key.png</normaloff>:/assets/database_key.png</iconset>
</property>
<property name="text">
<string>Connection Manager</string>
</property>
<property name="toolTip">
<string>Connection Manager</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
<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">
<string>Create Bolt database from zip</string>
</property>
</action>
<action name="actionSet_cell_to_null">
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/note_delete.png</normaloff>:/assets/note_delete.png</iconset>
</property>
<property name="text">
<string>Set cell to null</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
</widget>
<resources>
<include location="embed.qrc"/>
</resources>
<connections/>
</ui>

5
platform_windows.go Normal file
View File

@@ -0,0 +1,5 @@
package main
import (
_ "github.com/ying32/govcl/pkgs/winappres" // Extra _syso files for Windows
)

Some files were not shown because too many files have changed in this diff Show More