105 Commits

Author SHA1 Message Date
6145320858 doc: v0.7.0 changelog, update TODO 2024-07-18 18:00:56 +12:00
8296a2fec9 gui: hardcode better windows colours 2024-07-18 17:51:27 +12:00
223d13be58 gui: toggle edit buttons as well 2024-07-18 17:49:16 +12:00
eca27dcd4f sqlite: basic editing support 2024-07-18 17:46:13 +12:00
0f2a3e021a gui: toggle the Query form fields if selected db is not queryable 2024-07-18 17:10:02 +12:00
90259fb2b9 gui: prevent submitting blank queries to db (seems to hang sqlite) 2024-07-18 17:09:48 +12:00
7573cf0453 app: upcast loadedDatabase to more specific interfaces 2024-07-18 17:09:32 +12:00
6dd0635c9e doc/TODO: update 2024-07-14 15:35:56 +12:00
ce3d08740f sqlite: add context actions for compact, export, drop table 2024-07-14 15:34:17 +12:00
8f5e1054fb db: return error from contextAction.Callback (2) 2024-07-14 15:28:18 +12:00
1ac96eb133 move filter consts to each db file 2024-07-14 15:27:54 +12:00
53e9b6555e doc/TODO: more ideas 2024-07-13 18:03:47 +12:00
e1a9f187cb db: return error from RenderForNav, contextAction.Callback 2024-07-13 18:03:42 +12:00
ee3110162b doc/TODO: update 2024-07-06 12:44:37 +12:00
35a83eb483 gui: basic syntax highlighting implementation (disabled) 2024-07-06 12:41:57 +12:00
60add3be86 db: add common errunsupported 2024-07-06 12:02:58 +12:00
2f65ffdd70 db: lift execQuery error handling to parent 2024-07-06 11:59:55 +12:00
aad92d27e9 gui: use icons for toolbar 2024-07-06 11:54:48 +12:00
21151be8a3 gui/images: load more image assets 2024-07-06 11:54:36 +12:00
f78eec1872 bolt: support editing 2024-07-06 11:45:41 +12:00
8af27f8834 gui: change tracking for insert, edit, delete actions 2024-07-06 11:04:19 +12:00
0d3b90b879 gui: prep work for inserting rows 2024-07-05 20:07:19 +12:00
2b59efc410 gui: add refresh button on data tab 2024-07-05 19:46:04 +12:00
7fbf2ef1ed gui: common column handling, set widths automatically 2024-07-05 19:35:24 +12:00
d7e3363173 gui: convert data tables from TListView to TStringGrid 2024-07-05 19:21:08 +12:00
cecfc338d4 gui: bigger default window size 2024-07-05 18:43:02 +12:00
35f09fc072 doc/README: add v0.6.0 download links 2024-06-30 14:17:59 +12:00
2163b46907 doc/README: changelog for v0.6.0 2024-06-30 14:16:06 +12:00
81b6b08e7b doc/TODO: status update 2024-06-30 14:15:53 +12:00
f31724a110 gui: add extra space in help menu driver list 2024-06-30 14:14:50 +12:00
063a8ca837 debconf: add as database option 2024-06-30 14:14:41 +12:00
1cfc94a42b debconf: fix extra spaces, Name column ordering 2024-06-30 14:14:26 +12:00
053e07c319 debconf: implement dat file parser 2024-06-30 13:47:29 +12:00
0b91c379b8 doc/README: update partial changelog 2024-06-30 13:28:17 +12:00
7b4cc885f5 gui: use Consolas as monospace font on Windows 2024-06-30 13:16:27 +12:00
3b17ddd8a4 sqliteclidriver: always get columns in the right order 2024-06-30 13:12:11 +12:00
8d051a14e5 orderedkv: initial commit 2024-06-30 13:11:54 +12:00
4735c391bd doc/TODO: update status 2024-06-30 12:51:40 +12:00
0866e5edac sqliteclidriver: clear content from ret if sentinel was found 2024-06-30 12:51:05 +12:00
5c44dc5f54 sqliteclidriver: indicate driver in status bar 2024-06-30 12:48:22 +12:00
a7dd1ca340 makefile: compile cgo with -O2 2024-06-30 12:45:40 +12:00
abcf7dbfe5 sqliteclidriver: better bubble up stderr errors 2024-06-30 12:45:23 +12:00
d359f42b24 sqlite: support tables named using special characters 2024-06-30 12:45:03 +12:00
7cec5cee4c sqliteclidriver: use channel events, handle no results via sentinel 2024-06-30 12:33:47 +12:00
be91cd54c6 eventcmd: initial commit of channel-based process wrapper 2024-06-30 12:33:01 +12:00
b141aaaa6c lexer: separate tokens for top-level special characters 2024-06-30 11:26:00 +12:00
493ab846b9 gui: adjust styles for query frame 2024-06-30 11:10:19 +12:00
d3ebcb4666 gui: fix popup position for redis connection dialog 2024-06-30 11:05:25 +12:00
50cf207eae gui: fix properties tab background colour for windows build 2024-06-30 11:05:16 +12:00
e5cbbb6822 makefile/linux: don't upx, but xz harder, for faster startup 2024-06-30 10:45:19 +12:00
18674568dd makefile/windows: upx harder 2024-06-30 10:45:08 +12:00
748dd96267 makefile/windows: set windowsgui flag 2024-06-30 10:44:59 +12:00
c5578daa9f makefile: add cleanups before targets 2024-06-30 10:44:13 +12:00
3bc7f539ad doc/README: add v0.5.0 download links 2024-06-29 13:03:55 +12:00
52677224c1 doc/README: changelog for v0.5.0 2024-06-29 12:59:54 +12:00
0a31eab9f2 doc: update screenshot 2024-06-29 12:59:41 +12:00
011063597d makefile: improvements for dist archives 2024-06-29 12:54:57 +12:00
71c182692a makefile: use cgo for windows build 2024-06-29 12:54:47 +12:00
e15af5a544 makefile: add targets for release dist builds 2024-06-29 12:41:05 +12:00
ba7228ad44 doc/TODO: update 2024-06-29 12:15:26 +12:00
650c9e7183 sqlite: basic integration for the cli driver 2024-06-29 12:13:34 +12:00
471737f421 gui: add helper function for menu separators 2024-06-29 12:13:18 +12:00
d2b9618da0 sqliteclidriver: initial commit 2024-06-29 11:56:45 +12:00
f0f0ff7904 lexer: add re-quote helper 2024-06-29 11:56:26 +12:00
639da11ab3 add custom lexer, use for redis string splitting 2024-06-29 11:21:30 +12:00
fc084d7190 gui: seamless refresh after bucket action, support nav removal 2024-06-28 12:29:44 +12:00
e2111017eb gui: implement nav refresh 2024-06-28 12:04:39 +12:00
ef70e5825a gui: add nav context images for refresh + close 2024-06-28 12:00:18 +12:00
15b29b32ce bolt: support adding/removing child buckets 2024-06-28 11:53:07 +12:00
975e120530 main: move appname to constant 2024-06-28 11:53:01 +12:00
5b8883d31a bolt: add nav suffix for readonly connections 2024-06-28 11:52:50 +12:00
ce43f5765c bolt: support 'create new database' 2024-06-28 11:40:03 +12:00
feffa67677 main: infra for custom context actions 2024-06-28 11:34:00 +12:00
a14e58297a bolt: support open as readonly 2024-06-28 11:33:43 +12:00
2b309fbda7 gui: add help option to show driver versions 2024-06-28 11:21:14 +12:00
70db402cdf gui: use helper function for setting up menus 2024-06-28 11:11:34 +12:00
a82d5e6b26 gui: run optipng on some images 2024-06-28 11:05:57 +12:00
28570b0b96 versions: replace embed go.mod with stdlib debug api 2024-06-28 11:05:49 +12:00
9bf84fa140 pebble: initial support 2024-06-28 11:01:34 +12:00
eb34221620 doc/README: changelog for v0.4.0 2024-06-23 16:35:25 +12:00
cc3ba4d9f0 doc/TODO: update current progress 2024-06-23 16:29:53 +12:00
f79d17afed badger: support temporary in-memory databases 2024-06-23 16:29:53 +12:00
d7c2282335 badger: add dgraph vendor icon 2024-06-23 16:29:53 +12:00
43002a9fde main: restructure nav menu into alphabetical per-DB options 2024-06-23 16:29:53 +12:00
bc33d26cfd images: add vendor logos 2024-06-23 16:29:47 +12:00
38d9e6238f main: add help menu with website link 2024-06-23 15:58:12 +12:00
a653ef8ca4 main: get accurate cross-platform background colour for properties tab 2024-06-23 15:58:03 +12:00
cc336366c9 redis: show results in query tab 2024-06-23 15:57:33 +12:00
8f105183eb redis: handle data browsing for different typed keys 2024-06-23 15:57:20 +12:00
b45faa2e73 main: add nav context menu, support closing open connections 2024-06-23 15:28:15 +12:00
7e5d17100d main: preload recursive navigation only one layer at a time 2024-06-23 15:21:41 +12:00
a817e5fa21 main: recursively load all nav state at connection time 2024-06-23 15:12:26 +12:00
3d185033f3 main: allow scrolling long content on properties tab 2024-06-23 14:56:00 +12:00
924957d00d main: abstract the nil selection into a virtual database type 2024-06-23 14:55:46 +12:00
d674078071 main: auto switch to newly opened database 2024-06-23 14:54:49 +12:00
5992d19906 redis: complete basic multidatabase browse support 2024-06-23 14:54:33 +12:00
8cac46e9f2 doc: update README + TODO 2024-06-23 13:13:43 +12:00
db5f6816c5 main: allow running partial query by selection 2024-06-23 13:07:52 +12:00
3a4bdbde94 redis: improve dialog style, open connection, enumerate databases 2024-06-23 13:07:42 +12:00
0363bc65f4 platform_windows: move extra syso inclusion out of main 2024-06-23 13:06:41 +12:00
a47898e099 format: move to util_ file 2024-06-23 13:06:25 +12:00
1487b18a3a doc/TODO: add notes re virtual list rendering 2024-06-22 17:36:34 +12:00
04ef53720f redis: initial work on connection dialog 2024-06-22 17:36:25 +12:00
bc82aacd57 redis: add library dependency 2024-06-22 17:36:19 +12:00
6dcc1afd6b rename files to db_ prefixes 2024-06-22 16:22:33 +12:00
53 changed files with 4229 additions and 641 deletions

6
.gitignore vendored
View File

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

46
Makefile Normal file
View File

@@ -0,0 +1,46 @@
SHELL:=/bin/bash
SOURCES=$(find . -name '*.go' -type f)
liblcl-2.2.3.zip:
rm -f liblcl-2.2.3.zip
wget 'https://github.com/ying32/govcl/releases/download/v2.2.3/liblcl-2.2.3.zip'
liblcl.so: liblcl-2.2.3.zip
echo "154b4e4a1d5137a2ffe51cb4d0bf152dd997c12616ae30862775c0e4f0928e88 liblcl-2.2.3.zip" | sha256sum -c
rm -f liblcl.so
unzip -j liblcl-2.2.3.zip linux64-gtk2/liblcl.so -d .
touch liblcl.so
liblcl.dll: liblcl-2.2.3.zip
echo "154b4e4a1d5137a2ffe51cb4d0bf152dd997c12616ae30862775c0e4f0928e88 liblcl-2.2.3.zip" | sha256sum -c
rm -f liblcl.dll
unzip -j liblcl-2.2.3.zip win64/liblcl.dll -d .
touch liblcl.dll
yvbolt: $(SOURCES)
CGO_CFLAGS='-O2 -Wno-return-local-addr' GOOS=linux GOARCH=amd64 CGO_ENABLED=1 go build -trimpath -ldflags '-s -w'
chmod 755 yvbolt
yvbolt.exe: $(SOURCES)
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
yvbolt.linux64.tar.xz: yvbolt liblcl.so
rm -f yvbolt.linux64.tar.xz
XZ_OPT='-T0 -9' tar caf yvbolt.linux64.tar.xz --owner=0 --group=0 yvbolt liblcl.so
yvbolt.win64.zip: yvbolt.exe liblcl.dll
rm -f yvbolt.win64.zip
zip -9 yvbolt.win64.zip yvbolt.exe liblcl.dll
.PHONY: dist
dist: yvbolt.linux64.tar.xz yvbolt.win64.zip
.PHONY: clean
clean:
rm -f yvbolt.exe yvbolt yvbolt.linux64.tar.xz yvbolt.win64.zip
.PHONY: clean-all
clean-all: clean
rm -f liblcl-2.2.3.zip liblcl.so liblcl.dll

100
README.md
View File

@@ -1,23 +1,30 @@
# yvbolt # yvbolt
A graphical browser for multiple databases using [GoVCL](https://z-kit.cc/en/). A graphical interface for multiple databases using [GoVCL](https://z-kit.cc/en/).
This is an experimental application and you should generally prefer to use [qbolt](https://code.ivysaur.me/qbolt).
## Features ## Features
- Native desktop application, running on Linux, Windows, and macOS - Native desktop application, running on Linux, Windows, and macOS
- Connect to multiple databases - Connect to multiple databases at once
- Browse table/bucket content - Browse table/bucket content
- Use context menu to perform special table/bucket actions
- Run custom SQL queries - Run custom SQL queries
- Select text to run partial query
- Safe handling for non-UTF8 key and data fields - Safe handling for non-UTF8 key and data fields
- Supported databases: - Supported databases:
- Bolt
- Full compatibility via the upstream [etcd-io/bbolt](https://github.com/etcd-io/bbolt) library
- Recursive bucket support
- SQLite
- Uses CGo if available or modernc.org if not
- Badger v4 - Badger v4
- Bolt
- Recursive bucket support
- Option to open as readonly for shared access
- Supports editing
- See also [qbolt](https://code.ivysaur.me/qbolt) for more/different functionality
- Debconf
- Pebble
- Redis
- SQLite
- Drivers: mattn (CGo), modernc.org (no cgo), experimental command-line driver
- Supports editing
- Integrated vacuum and export commands
## License ## License
@@ -25,7 +32,9 @@ The code in this project is licensed under the ISC license (see `LICENSE` file f
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 under the [CC-BY 2.5 license](http://creativecommons.org/licenses/by/2.5/).
## Usage This project includes trademarked logo images for each supported database type.
## Compiling
1. `CGO_ENABLED=1 go build` 1. `CGO_ENABLED=1 go build`
2. [Download liblcl](https://github.com/ying32/govcl/releases/download/v2.2.3/liblcl-2.2.3.zip) for your platform, or [compile it yourself](https://github.com/ying32/liblcl) (tested with v2.2.3) 2. [Download liblcl](https://github.com/ying32/govcl/releases/download/v2.2.3/liblcl-2.2.3.zip) for your platform, or [compile it yourself](https://github.com/ying32/liblcl) (tested with v2.2.3)
@@ -34,20 +43,73 @@ This project redistributes images from the famfamfam/silk icon set under the [CC
## Changelog ## Changelog
2024-07-18 v0.7.0
- SQLite, Bolt: Initial support for editing data (insert, per-cell update, delete)
- SQLite: Add context menu actions for compact (vacuum), export, and drop table
- App: New grid widget
- App: Add refresh button
- App: Bigger window size, use icons for toolbars, better UI colours for Windows
- App: Prevent submitting blank queries to database
- Refactor database interface and error handling
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 2024-06-25 v0.3.0
- Add support for running custom queries - Badger: Add BadgerDB v4 as supported database
- Add BadgerDB v4 as supported database - SQLite: Add support for CGo-free SQLite driver under cross-compilation
- Add support for CGo-free SQLite driver under cross-compilation - Bolt: Update Bolt to v1.4.0-alpha.1
- Add status bar showing currently selected DB - App: Add support for running custom queries
- Update Bolt to v1.4.0-alpha.1 - App: Add status bar showing currently selected DB
- Fix missing icons in nav when selecting items - App: Fix missing icons in nav when selecting items
- Fix extra quotemarks when browsing string content of database - App: Fix extra quotemarks when browsing string content of database
2024-06-08 v0.2.0 2024-06-08 v0.2.0
- Add SQLite support (now requires CGo) - SQLite: Add SQLite support (now requires CGo)
- Add images for menu and navigation items - App: Add images for menu and navigation items
2024-06-03 v0.1.0 2024-06-03 v0.1.0

44
TODO
View File

@@ -1,23 +1,41 @@
- Insert - Syntax highlighting in editor
- Update cell - Mutation
- Delete row(s) - 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
- Redis: Support insert/update/delete
- Binary data viewer - Binary data viewer
- Detect jpg/png and show as image - Detect jpg/png and show as image
- Warning if data table is filtered to 1000 rows, or add pagination
- More DB types - More DB types
- MySQL - MySQL
- Postgres - Postgres
- Redis - CLI using psql
- MSSQL (recursive navigation for instances) - MSSQL (recursive navigation for instances)
- Other K/V stores from https://github.com/smallnest/kvbench
- Windows registry
- LDAP
- Connection dialog
- SSH tunnels - SSH tunnels
- Special actions - Badger encryption key dialog
- Refresh tables - Pebble: connection options dialog
- Per-db actions e.g. compact, export backup - SQLite CLI driver:
- Per-table actions e.g. drop table - Context support
- Sqlite cli driver for ssh tunnel - Attach to SSH tunnel
- Configure binary path
- https://ricardoanderegg.com/posts/sqlite-remote-explorer-gui/ - https://ricardoanderegg.com/posts/sqlite-remote-explorer-gui/
- https://github.com/litements/litexplore - https://github.com/litements/litexplore
- Badger encryption key dialog - Debconf: separate groups by first slash in name
- Badger in-memory - SQLite: drop table doesn't autorefresh nav since callback is late
- Makefile to cross-compile release binaries in docker - Build
- Build own liblcl binaries in docker
- Win32 icon resource - Win32 icon resource
- https://github.com/ying32/govcl/tree/master/Tools/winRes
- Performance
- Warning if data table is filtered to 1000 rows, or add pagination
- Context/interrupt slow queries
- Faster virtual rendering
- https://github.com/ying32/govcl/blob/master/samples/listviewvirtualdata/main.go
- Query history
- Test suite
- `CREATE TABLE foo (id integer primary key, aaa text not null, bbb text not null);`

BIN
assets/add.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 733 B

BIN
assets/arrow_refresh.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 685 B

BIN
assets/delete.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 B

BIN
assets/lightning_go.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 739 B

BIN
assets/pencil.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

BIN
assets/pencil_add.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 589 B

BIN
assets/pencil_delete.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 603 B

BIN
assets/pencil_go.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 666 B

BIN
assets/resultset_next.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 B

BIN
assets/vendor_cockroach.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 752 B

BIN
assets/vendor_debian.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 831 B

BIN
assets/vendor_dgraph.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 948 B

BIN
assets/vendor_github.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

BIN
assets/vendor_mysql.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 824 B

BIN
assets/vendor_redis.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
assets/vendor_sqlite.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 919 B

200
bolt.go
View File

@@ -1,200 +0,0 @@
package main
import (
"fmt"
"path/filepath"
"sort"
"strings"
"time"
"unsafe"
"github.com/ying32/govcl/vcl"
"github.com/ying32/govcl/vcl/types"
"go.etcd.io/bbolt"
"go.etcd.io/bbolt/version"
)
type boltLoadedDatabase struct {
displayName string
path string
db *bbolt.DB
nav *vcl.TTreeNode
arena []*navData // keepalive
}
func (ld *boltLoadedDatabase) DisplayName() string {
return ld.displayName
}
func (ld *boltLoadedDatabase) DriverName() string {
return "Bolt " + version.Version
}
func (ld *boltLoadedDatabase) RootElement() *vcl.TTreeNode {
return ld.nav
}
func (ld *boltLoadedDatabase) Keepalive(ndata *navData) {
ld.arena = append(ld.arena, ndata)
}
func (ld *boltLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) {
// Load properties
bucketDisplayName := strings.Join(ndata.bucketPath, `/`)
content := fmt.Sprintf("Selected database: %#v\n\n\nSelected bucket: %q\n", ld.db.Stats(), bucketDisplayName)
f.propertiesBox.SetText(content)
// Load data
f.contentBox.SetEnabled(false)
f.contentBox.Clear()
// Bolt always uses Key + Value as the columns
f.contentBox.Columns().Clear()
colKey := f.contentBox.Columns().Add()
colKey.SetCaption("Key")
colKey.SetWidth(MY_WIDTH)
colKey.SetAlignment(types.TaLeftJustify)
colVal := f.contentBox.Columns().Add()
colVal.SetCaption("Value")
err := ld.db.View(func(tx *bbolt.Tx) error {
b := boltTargetBucket(tx, ndata.bucketPath)
if b == nil {
// no such bucket
return nil
}
// Valid
f.contentBox.Clear()
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
dataEntry := f.contentBox.Items().Add()
dataEntry.SetCaption(formatUtf8(k))
dataEntry.SubItems().Add(formatUtf8(v))
}
return nil
})
if err != nil {
vcl.ShowMessage(fmt.Sprintf("Failed to load data for bucket %q: %s", bucketDisplayName, err.Error()))
return
}
// Valid
f.contentBox.SetEnabled(true)
}
func (ld *boltLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
// In the bolt implementation, the nav is a recursive tree of child buckets
return boltChildBucketNames(ld.db, ndata.bucketPath)
}
func (ld *boltLoadedDatabase) ExecQuery(query string, resultArea *vcl.TListView) {
vcl.ShowMessage("Bolt doesn't support querying")
}
var _ loadedDatabase = &boltLoadedDatabase{} // interface assertion
//
func (f *TMainForm) boltAddDatabaseFromFile(path string) {
// TODO load in background thread to stop blocking the UI
db, err := bbolt.Open(path, 0644, &bbolt.Options{Timeout: 1 * time.Second})
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,
}
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)
ld.Keepalive(navData)
}
func boltTargetBucket(tx *bbolt.Tx, path []string) *bbolt.Bucket {
// If we are already deep in buckets, go directly there to find children
if len(path) == 0 {
return nil
}
b := tx.Bucket([]byte(path[0]))
if b == nil {
return nil // unexpectedly missing
}
for i := 1; i < len(path); i += 1 {
b = b.Bucket([]byte(path[i]))
if b == nil {
return nil // unexpectedly missing
}
}
return b // OK
}
func boltChildBucketNames(db *bbolt.DB, path []string) ([]string, error) {
var nextBucketNames []string
err := db.View(func(tx *bbolt.Tx) error {
// If we are already deep in buckets, go directly there to find children
if len(path) > 0 {
b := tx.Bucket([]byte(path[0]))
if b == nil {
return fmt.Errorf("Unexpected missing root bucket %q", path[0])
}
for i := 1; i < len(path); i += 1 {
b = b.Bucket([]byte(path[i]))
if b == nil {
return fmt.Errorf("Unexpected missing bucket %q", strings.Join(path[0:i], `/`))
}
}
// Find child buckets of this bucket
b.ForEachBucket(func(bucketName []byte) error {
nextBucketNames = append(nextBucketNames, string(bucketName))
return nil
})
} else {
// Find root bucket names
return tx.ForEach(func(bucketName []byte, _ *bbolt.Bucket) error {
nextBucketNames = append(nextBucketNames, string(bucketName))
return nil
})
}
// OK
return nil
})
if err != nil {
return nil, err
}
sort.Strings(nextBucketNames)
return nextBucketNames, nil
}

View File

@@ -7,12 +7,10 @@ import (
"github.com/dgraph-io/badger/v4" "github.com/dgraph-io/badger/v4"
"github.com/ying32/govcl/vcl" "github.com/ying32/govcl/vcl"
"github.com/ying32/govcl/vcl/types"
) )
type badgerLoadedDatabase struct { type badgerLoadedDatabase struct {
displayName string displayName string
path string
db *badger.DB db *badger.DB
nav *vcl.TTreeNode nav *vcl.TTreeNode
@@ -35,7 +33,7 @@ func (ld *badgerLoadedDatabase) Keepalive(ndata *navData) {
ld.arena = append(ld.arena, ndata) ld.arena = append(ld.arena, ndata)
} }
func (ld *badgerLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) { func (ld *badgerLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) error {
// Load properties // Load properties
@@ -43,25 +41,15 @@ func (ld *badgerLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) {
f.propertiesBox.SetText(content) f.propertiesBox.SetText(content)
// Load data // Load data
f.contentBox.SetEnabled(false)
f.contentBox.Clear()
// Badger always uses Key + Value as the columns // Badger always uses Key + Value as the columns
f.contentBox.Columns().Clear()
colKey := f.contentBox.Columns().Add() colKey := f.contentBox.Columns().Add()
colKey.SetCaption("Key") colKey.Title().SetCaption("Key")
colKey.SetWidth(MY_WIDTH)
colKey.SetAlignment(types.TaLeftJustify)
colVal := f.contentBox.Columns().Add() colVal := f.contentBox.Columns().Add()
colVal.SetCaption("Value") colVal.Title().SetCaption("Value")
err := ld.db.View(func(txn *badger.Txn) error { err := ld.db.View(func(txn *badger.Txn) error {
// Valid
f.contentBox.Clear()
// Create iterator // Create iterator
opts := badger.DefaultIteratorOptions opts := badger.DefaultIteratorOptions
opts.PrefetchSize = 64 opts.PrefetchSize = 64
@@ -72,9 +60,10 @@ func (ld *badgerLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) {
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 {
dataEntry := f.contentBox.Items().Add() rpos := f.contentBox.RowCount()
dataEntry.SetCaption(formatUtf8(k)) f.contentBox.SetRowCount(rpos + 1)
dataEntry.SubItems().Add(formatUtf8(v)) f.contentBox.SetCells(0, rpos, formatUtf8(k))
f.contentBox.SetCells(1, rpos, formatUtf8(v))
return nil return nil
}) })
@@ -85,12 +74,13 @@ func (ld *badgerLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) {
return nil return nil
}) })
if err != nil { if err != nil {
vcl.ShowMessage(fmt.Sprintf("Failed to load data: %s", err.Error())) return err
return
} }
// Valid // Valid
vcl_stringgrid_columnwidths(f.contentBox)
f.contentBox.SetEnabled(true) f.contentBox.SetEnabled(true)
return nil
} }
func (ld *badgerLoadedDatabase) NavChildren(ndata *navData) ([]string, error) { func (ld *badgerLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
@@ -104,28 +94,45 @@ func (ld *badgerLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
} }
} }
func (ld *badgerLoadedDatabase) ExecQuery(query string, resultArea *vcl.TListView) { func (ld *badgerLoadedDatabase) NavContext(ndata *navData) ([]contextAction, error) {
vcl.ShowMessage("Badger doesn't support querying") return nil, nil // No special actions are supported
}
func (ld *badgerLoadedDatabase) Close() {
_ = ld.db.Close()
ld.arena = nil
} }
var _ loadedDatabase = &badgerLoadedDatabase{} // interface assertion var _ loadedDatabase = &badgerLoadedDatabase{} // interface assertion
// //
func (f *TMainForm) badgerAddDatabaseFromMemory() {
f.badgerAddDatabaseFrom(badger.DefaultOptions("").WithInMemory(true))
}
func (f *TMainForm) badgerAddDatabaseFromDirectory(path string) { func (f *TMainForm) badgerAddDatabaseFromDirectory(path string) {
f.badgerAddDatabaseFrom(badger.DefaultOptions(path))
}
func (f *TMainForm) badgerAddDatabaseFrom(opts badger.Options) {
// TODO load in background thread to stop blocking the UI // TODO load in background thread to stop blocking the UI
db, err := badger.Open(badger.DefaultOptions(path)) db, err := badger.Open(opts)
if err != nil { if err != nil {
vcl.ShowMessage(fmt.Sprintf("Failed to load database '%s': %s", path, err.Error())) vcl.ShowMessage(fmt.Sprintf("Failed to load database: %s", err.Error()))
return return
} }
ld := &badgerLoadedDatabase{ ld := &badgerLoadedDatabase{
path: path,
displayName: filepath.Base(path),
db: db, db: db,
} }
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 = f.Buckets.Items().Add(nil, ld.displayName)
ld.nav.SetHasChildren(true) // dynamically populate in OnNavExpanding ld.nav.SetHasChildren(true) // dynamically populate in OnNavExpanding
ld.nav.SetImageIndex(imgDatabase) ld.nav.SetImageIndex(imgDatabase)
@@ -138,6 +145,7 @@ func (f *TMainForm) badgerAddDatabaseFromDirectory(path string) {
ld.nav.SetData(unsafe.Pointer(navData)) ld.nav.SetData(unsafe.Pointer(navData))
f.dbs = append(f.dbs, ld) f.dbs = append(f.dbs, ld)
f.Buckets.SetSelected(ld.nav) // Select new element
ld.Keepalive(navData) ld.Keepalive(navData)
} }

309
db_bolt.go Normal file
View File

@@ -0,0 +1,309 @@
package main
import (
"errors"
"fmt"
"path/filepath"
"sort"
"strings"
"time"
"unsafe"
"github.com/ying32/govcl/vcl"
"go.etcd.io/bbolt"
"go.etcd.io/bbolt/version"
)
const (
boltFilter = "Bolt database|*.db|All files|*.*"
)
type boltLoadedDatabase struct {
displayName string
path string
db *bbolt.DB
nav *vcl.TTreeNode
arena []*navData // keepalive
}
func (ld *boltLoadedDatabase) DisplayName() string {
return ld.displayName
}
func (ld *boltLoadedDatabase) DriverName() string {
return "Bolt " + version.Version
}
func (ld *boltLoadedDatabase) RootElement() *vcl.TTreeNode {
return ld.nav
}
func (ld *boltLoadedDatabase) Keepalive(ndata *navData) {
ld.arena = append(ld.arena, ndata)
}
func (ld *boltLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) error {
// Load properties
bucketDisplayName := strings.Join(ndata.bucketPath, `/`)
content := fmt.Sprintf("Selected database: %#v\n\n\nSelected bucket: %q\n", ld.db.Stats(), bucketDisplayName)
f.propertiesBox.SetText(content)
// Load data
// Bolt always uses Key + Value as the columns
colKey := f.contentBox.Columns().Add()
colKey.Title().SetCaption("Key")
colVal := f.contentBox.Columns().Add()
colVal.Title().SetCaption("Value")
err := ld.db.View(func(tx *bbolt.Tx) error {
b := boltTargetBucket(tx, ndata.bucketPath)
if b == nil {
// no such bucket
return nil
}
// Valid
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
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
})
if err != nil {
return err
}
// Valid
vcl_stringgrid_columnwidths(f.contentBox)
f.contentBox.SetEnabled(true)
return nil
}
func (n *boltLoadedDatabase) ApplyChanges(f *TMainForm, ndata *navData) error {
if n.db.IsReadOnly() {
return errors.New("Database was opened read-only")
}
// We have rendered row IDs, need to convert back to a bolt primary key
// TODO stash the real key inside f.contentBox.Objects()
// FIXME breaks if you try and edit the primary key(!)
primaryKeyForRendered := func(rowid int32) []byte {
return []byte(f.contentBox.Cells(0, rowid))
}
return n.db.Update(func(tx *bbolt.Tx) error {
// Get current bucket handle
b := boltTargetBucket(tx, ndata.bucketPath)
// Edit
for rowid, _ /*editcells*/ := range f.updateRows {
k := primaryKeyForRendered(rowid)
v := f.contentBox.Cells(1, rowid) // There's only one value cell
err := b.Put(k, []byte(v))
if err != nil {
return fmt.Errorf("Updating cell %q: %w", formatUtf8(k), err)
}
}
// Delete by key (affects rowids after re-render)
for rowid, _ := range f.deleteRows {
k := primaryKeyForRendered(rowid)
err := b.Delete(k)
if err != nil {
return fmt.Errorf("Deleting cell %q: %w", formatUtf8(k), err)
}
}
// Insert all new entries
for rowid, _ := range f.insertRows {
k := primaryKeyForRendered(rowid)
v := f.contentBox.Cells(1, rowid) // There's only one value cell
err := b.Put(k, []byte(v))
if err != nil {
return fmt.Errorf("Inserting cell %q: %w", formatUtf8(k), err)
}
}
// Done
return nil
})
}
func (ld *boltLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
// In the bolt implementation, the nav is a recursive tree of child buckets
return boltChildBucketNames(ld.db, ndata.bucketPath)
}
func (ld *boltLoadedDatabase) NavContext(ndata *navData) (ret []contextAction, err error) {
ret = append(ret, contextAction{"Add bucket...", ld.AddChildBucket})
if len(ndata.bucketPath) > 0 {
ret = append(ret, contextAction{"Delete bucket", ld.DeleteBucket})
}
return
}
func (ld *boltLoadedDatabase) AddChildBucket(sender vcl.IComponent, ndata *navData) error {
bucketName := ""
if !vcl.InputQuery(APPNAME, "Enter a name for the new bucket:", &bucketName) {
return nil // cancel
}
err := ld.db.Update(func(tx *bbolt.Tx) error {
parent := boltTargetBucket(tx, ndata.bucketPath)
if parent != nil {
_, err := parent.CreateBucket([]byte(bucketName))
return err
}
// Top-level
_, err := tx.CreateBucket([]byte(bucketName))
return err
})
if err != nil {
return fmt.Errorf("Error adding bucket: %w", err)
}
return nil
}
func (ld *boltLoadedDatabase) DeleteBucket(sender vcl.IComponent, ndata *navData) error {
err := ld.db.Update(func(tx *bbolt.Tx) error {
// Find parent of this bucket.
if len(ndata.bucketPath) >= 2 {
// child bucket
parent := boltTargetBucket(tx, ndata.bucketPath[0:len(ndata.bucketPath)-1])
return parent.DeleteBucket([]byte(ndata.bucketPath[len(ndata.bucketPath)-1]))
} else {
// top-level bucket
return tx.DeleteBucket([]byte(ndata.bucketPath[0]))
}
})
if err != nil {
return fmt.Errorf("Error deleting bucket %q: %w", strings.Join(ndata.bucketPath, `/`), err)
}
return nil
}
func (ld *boltLoadedDatabase) Close() {
_ = ld.db.Close()
ld.arena = nil
}
var _ loadedDatabase = &boltLoadedDatabase{} // interface assertion
//
func (f *TMainForm) boltAddDatabaseFromFile(path string, readonly bool) {
// TODO load in background thread to stop blocking the UI
opts := bbolt.Options{
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 {
// If we are already deep in buckets, go directly there to find children
if len(path) == 0 {
return nil
}
b := tx.Bucket([]byte(path[0]))
if b == nil {
return nil // unexpectedly missing
}
for i := 1; i < len(path); i += 1 {
b = b.Bucket([]byte(path[i]))
if b == nil {
return nil // unexpectedly missing
}
}
return b // OK
}
func boltChildBucketNames(db *bbolt.DB, path []string) ([]string, error) {
var nextBucketNames []string
err := db.View(func(tx *bbolt.Tx) error {
// If we are already deep in buckets, go directly there to find children
if len(path) > 0 {
b := tx.Bucket([]byte(path[0]))
if b == nil {
return fmt.Errorf("Root bucket %q: %w", path[0], ErrNavNotExist)
}
for i := 1; i < len(path); i += 1 {
b = b.Bucket([]byte(path[i]))
if b == nil {
return fmt.Errorf("Bucket %q: %w", strings.Join(path[0:i], `/`), ErrNavNotExist)
}
}
// Find child buckets of this bucket
b.ForEachBucket(func(bucketName []byte) error {
nextBucketNames = append(nextBucketNames, string(bucketName))
return nil
})
} else {
// Find root bucket names
return tx.ForEach(func(bucketName []byte, _ *bbolt.Bucket) error {
nextBucketNames = append(nextBucketNames, string(bucketName))
return nil
})
}
// OK
return nil
})
if err != nil {
return nil, err
}
sort.Strings(nextBucketNames)
return nextBucketNames, nil
}

132
db_debconf.go Normal file
View File

@@ -0,0 +1,132 @@
package main
import (
"fmt"
"os"
"path/filepath"
"unsafe"
"yvbolt/debconf"
"github.com/ying32/govcl/vcl"
)
type debconfLoadedDatabase struct {
displayName string
db *debconf.Database
nav *vcl.TTreeNode
arena []*navData // keepalive
}
func (ld *debconfLoadedDatabase) DisplayName() string {
return ld.displayName
}
func (ld *debconfLoadedDatabase) DriverName() string {
return "debconf"
}
func (ld *debconfLoadedDatabase) RootElement() *vcl.TTreeNode {
return ld.nav
}
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
indexes := make(map[string]int)
for i, cname := range ld.db.AllColumnNames {
indexes[cname] = i
col := f.contentBox.Columns().Add()
col.Title().SetCaption(cname)
}
for _, entry := range ld.db.Entries {
rpos := f.contentBox.RowCount()
f.contentBox.SetRowCount(rpos + 1)
f.contentBox.SetCells(0, rpos, entry.Name)
for _, proppair := range entry.Properties {
f.contentBox.SetCells(int32(indexes[proppair[0]]), rpos, proppair[1])
}
}
// Valid
vcl_stringgrid_columnwidths(f.contentBox)
f.contentBox.SetEnabled(true)
return 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 {
// No children deeper than that
return []string{}, nil
}
}
func (ld *debconfLoadedDatabase) NavContext(ndata *navData) ([]contextAction, error) {
return nil, nil // No special actions are supported
}
func (ld *debconfLoadedDatabase) Close() {
ld.arena = nil
}
var _ loadedDatabase = &debconfLoadedDatabase{} // interface assertion
//
func (f *TMainForm) debconfAddDatabaseFrom(path string) {
// TODO load in background thread to stop blocking the UI
fh, err := os.OpenFile(path, os.O_RDONLY, 0400)
if err != nil {
vcl.ShowMessage(fmt.Sprintf("Failed to load database: %s", err.Error()))
return
}
defer fh.Close()
db, err := debconf.Parse(fh)
if err != nil {
vcl.ShowMessage(fmt.Sprintf("Failed to load database: %s", err.Error()))
return
}
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)
}

37
db_none.go Normal file
View File

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

134
db_pebble.go Normal file
View File

@@ -0,0 +1,134 @@
package main
import (
"context"
"fmt"
"path/filepath"
"unsafe"
"github.com/cockroachdb/pebble"
"github.com/cockroachdb/pebble/vfs"
"github.com/ying32/govcl/vcl"
)
type pebbleLoadedDatabase struct {
displayName string
db *pebble.DB
nav *vcl.TTreeNode
arena []*navData // keepalive
}
func (ld *pebbleLoadedDatabase) DisplayName() string {
return ld.displayName
}
func (ld *pebbleLoadedDatabase) DriverName() string {
return "Pebble " + version_Pebble // Parsed from embedded go.mod copy
}
func (ld *pebbleLoadedDatabase) RootElement() *vcl.TTreeNode {
return ld.nav
}
func (ld *pebbleLoadedDatabase) Keepalive(ndata *navData) {
ld.arena = append(ld.arena, ndata)
}
func (ld *pebbleLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) error {
ctx := context.Background()
// Load properties
content := fmt.Sprintf("Pebble metrics: %#v", ld.db.Metrics())
f.propertiesBox.SetText(content)
// Load data
// pebble always uses Key + Value as the columns
colKey := f.contentBox.Columns().Add()
colKey.Title().SetCaption("Key")
colVal := f.contentBox.Columns().Add()
colVal.Title().SetCaption("Value")
itr := ld.db.NewIterWithContext(ctx, nil)
defer itr.Close()
for itr.First(); itr.Valid(); itr.Next() {
k := itr.Key()
v, err := itr.ValueAndErr()
if err != nil {
return fmt.Errorf("Failed to load data for key %q: %w", formatAny(k), err)
}
rpos := f.contentBox.RowCount()
f.contentBox.SetRowCount(rpos + 1)
f.contentBox.SetCells(0, rpos, formatUtf8(k))
f.contentBox.SetCells(0, rpos, formatUtf8(v))
}
// Valid
vcl_stringgrid_columnwidths(f.contentBox)
f.contentBox.SetEnabled(true)
return nil
}
func (ld *pebbleLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
// 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(ndata *navData) ([]contextAction, error) {
return nil, nil // No special actions are supported
}
func (ld *pebbleLoadedDatabase) Close() {
_ = ld.db.Close()
ld.arena = nil
}
var _ loadedDatabase = &pebbleLoadedDatabase{} // interface assertion
//
func (f *TMainForm) pebbleAddDatabaseFromMemory() {
f.pebbleAddDatabaseFrom("", &pebble.Options{FS: vfs.NewMem()})
}
func (f *TMainForm) pebbleAddDatabaseFrom(dirname string, opts *pebble.Options) {
// TODO load in background thread to stop blocking the UI
db, err := pebble.Open(dirname, opts)
if err != nil {
vcl.ShowMessage(fmt.Sprintf("Failed to load database: %s", err.Error()))
return
}
ld := &pebbleLoadedDatabase{
db: db,
displayName: filepath.Base(dirname),
}
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)
}

280
db_redis.go Normal file
View File

@@ -0,0 +1,280 @@
package main
import (
"context"
"fmt"
"strconv"
"unsafe"
"yvbolt/lexer"
"github.com/redis/go-redis/v9"
"github.com/ying32/govcl/vcl"
)
type redisLoadedDatabase struct {
displayName string
db *redis.Client
nav *vcl.TTreeNode
currentDb int
maxDb int
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 {
return "Redis " + ld.serverVersion
}
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()
if len(ndata.bucketPath) == 0 {
// Top-level: Show info() on main Properties tab
infostr, err := ld.db.Info(ctx).Result()
if err != nil {
return fmt.Errorf("Retreiving database info: %w", err)
}
f.propertiesBox.SetText(infostr)
// Leave data tab disabled (default behaviour)
return nil
} else if len(ndata.bucketPath) == 1 {
// One selected database
// Figure out its content
err := ld.db.Do(ctx, "SELECT", ndata.bucketPath[0]).Err()
if err != nil {
return fmt.Errorf("Switching to database %q: %w", ndata.bucketPath[0], err)
}
allKeys, err := ld.db.Keys(ctx, "*").Result()
if err != nil {
return fmt.Errorf("Listing keys in database %q: %w", ndata.bucketPath[0], err)
}
f.propertiesBox.SetText(fmt.Sprintf("Database %s\nTotal keys: %d\n", ndata.bucketPath[0], len(allKeys)))
// Redis always uses Key + Value as the columns
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 {
typeName, err := ld.db.Type(ctx, key).Result()
if err != nil {
return fmt.Errorf("Loading %q/%q: %w", ndata.bucketPath[0], key, err)
}
rpos := f.contentBox.RowCount()
f.contentBox.SetRowCount(rpos + 1)
f.contentBox.SetCells(0, rpos, formatUtf8([]byte(key)))
f.contentBox.SetCells(1, rpos, typeName)
switch typeName {
case "string":
val, err := ld.db.Get(ctx, key).Result()
if err != nil {
return fmt.Errorf("Loading %q/%q: %w", ndata.bucketPath[0], key, err)
}
f.contentBox.SetCells(2, rpos, val)
case "hash":
val, err := ld.db.HGetAll(ctx, key).Result()
if err != nil {
return fmt.Errorf("Loading %q/%q: %w", ndata.bucketPath[0], key, err)
}
// It's a map[string]string
f.contentBox.SetCells(2, rpos, formatAny(val))
case "lists":
fallthrough
case "sets":
fallthrough
case "sorted sets":
fallthrough
case "stream":
fallthrough
default:
f.contentBox.SetCells(2, rpos, "<<<other object type>>>")
}
}
// Valid
vcl_stringgrid_columnwidths(f.contentBox)
f.contentBox.SetEnabled(true)
return nil
} else {
return fmt.Errorf("Unexpected nav position %q", ndata.bucketPath)
}
}
func (ld *redisLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
// ctx := context.Background()
if len(ndata.bucketPath) == 0 {
// Top-level: list of all child databases (usually 16x)
ret := make([]string, 0, ld.maxDb)
for i := 0; i < ld.maxDb; i++ {
ret = append(ret, fmt.Sprintf("%d", i))
}
return ret, nil
} else if len(ndata.bucketPath) == 1 {
// One selected database
// No child keys underneath it
return []string{}, nil
} else {
return nil, fmt.Errorf("Unexpected nav position %q", ndata.bucketPath)
}
}
func (ld *redisLoadedDatabase) NavContext(ndata *navData) ([]contextAction, error) {
return nil, nil // No special actions are supported
}
func (ld *redisLoadedDatabase) ExecQuery(query string, resultArea *vcl.TStringGrid) error {
ctx := context.Background()
// Need to parse the query into separate string+args fields for the protocol
fields, err := lexer.Fields(query)
if err != nil {
return fmt.Errorf("Parsing the query: %w", err)
}
fields_boxed := box_interface(fields)
ret, err := ld.db.Do(ctx, fields_boxed...).Result()
if err != nil {
return fmt.Errorf("The redis query returned an error: %w", err)
}
vcl_stringgrid_clear(resultArea)
colVal := resultArea.Columns().Add()
colVal.Title().SetCaption("Result")
// The result is probably a single value or a string slice
switch ret := ret.(type) {
case []string:
// Multiple values
for _, single := range ret {
rpos := resultArea.RowCount()
resultArea.SetRowCount(rpos + 1)
resultArea.SetCells(0, rpos, formatUtf8([]byte(single)))
}
default:
// Single value
rpos := resultArea.RowCount()
resultArea.SetRowCount(rpos + 1)
resultArea.SetCells(0, rpos, formatAny(ret)) // formatUtf8
}
vcl_stringgrid_columnwidths(resultArea)
resultArea.SetEnabled(true)
return nil
}
func (ld *redisLoadedDatabase) Close() {
_ = ld.db.Close()
ld.arena = nil
}
var _ loadedDatabase = &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)
}

443
db_sqlite.go Normal file
View File

@@ -0,0 +1,443 @@
package main
import (
"context"
"database/sql"
"errors"
"fmt"
"path/filepath"
"strings"
"unsafe"
_ "yvbolt/sqliteclidriver"
"github.com/ying32/govcl/vcl"
)
const (
sqliteTablesCaption = "Tables"
sqliteFilter = "SQLite database|*.db;*.db3;*.sqlite;*.sqlite3|All files|*.*"
)
type sqliteLoadedDatabase struct {
displayName string
path string
db *sql.DB
nav *vcl.TTreeNode
arena []*navData // keepalive
}
func (ld *sqliteLoadedDatabase) DisplayName() string {
return ld.displayName
}
func (ld *sqliteLoadedDatabase) RootElement() *vcl.TTreeNode {
return ld.nav
}
func (ld *sqliteLoadedDatabase) Keepalive(ndata *navData) {
ld.arena = append(ld.arena, ndata)
}
func (ld *sqliteLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) error {
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
r := ld.db.QueryRow(`SELECT sql FROM sqlite_schema WHERE name = ?;`, tableName)
var schemaStmt string
err := r.Scan(&schemaStmt)
if err != nil {
schemaStmt = fmt.Sprintf("* Failed to describe table %q: %s", tableName, err.Error())
}
// Display table properties
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)
}
populateColumns(columnNames, f.contentBox)
// Select count(*) so we know to display a warning if there are too many entries
// TODO
// Select * with small limit
datar, err := ld.db.Query(`SELECT * FROM "` + tableName + `" LIMIT 1000`) // WARNING can't prepare this parameter, but it comes from the DB (trusted)
if err != nil {
return fmt.Errorf("Failed to load data for table %q: %w", tableName, err)
}
defer datar.Close()
populateRows(datar, f.contentBox)
// We successfully populated the data grid
vcl_stringgrid_columnwidths(f.contentBox)
f.contentBox.SetEnabled(true)
return nil
} else {
// ??? unknown
return errors.New("?")
}
}
func (ld *sqliteLoadedDatabase) sqliteGetColumnNamesForTable(tableName string) ([]string, error) {
colr, err := ld.db.Query(`SELECT name FROM pragma_table_info( ? )`, tableName)
if err != nil {
return nil, fmt.Errorf("Query: %w", err)
}
defer colr.Close()
var ret []string
for colr.Next() {
var columnName string
err = colr.Scan(&columnName)
if err != nil {
return nil, fmt.Errorf("Scan: %w", colr.Err())
}
ret = append(ret, columnName)
}
if colr.Err() != nil {
return nil, colr.Err()
}
return ret, nil
}
func populateColumns(names []string, dest *vcl.TStringGrid) {
for _, columnName := range names {
col := dest.Columns().Add()
col.Title().SetCaption(columnName)
}
}
func populateRows(rr *sql.Rows, dest *vcl.TStringGrid) {
numColumns := int(dest.Columns().Count())
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...)
if err != nil {
vcl.ShowMessageFmt("Failed to load data: %s", err.Error())
return
}
rpos := dest.RowCount()
dest.SetRowCount(rpos + 1)
for i := 0; i < len(fields); i += 1 {
dest.SetCells(int32(i), rpos, formatAny(fields[i]))
}
}
if rr.Err() != nil {
vcl.ShowMessageFmt("Failed to load data: %s", rr.Err().Error())
return
}
}
func (ld *sqliteLoadedDatabase) ExecQuery(query string, resultArea *vcl.TStringGrid) error {
rr, err := ld.db.Query(query)
if err != nil {
return err
}
defer rr.Close()
vcl_stringgrid_clear(resultArea)
columns, err := rr.Columns()
if err != nil {
return err
}
populateColumns(columns, resultArea)
populateRows(rr, resultArea)
vcl_stringgrid_columnwidths(resultArea)
resultArea.SetEnabled(true)
return nil
}
func (n *sqliteLoadedDatabase) ApplyChanges(f *TMainForm, ndata *navData) (retErr error) {
if len(ndata.bucketPath) != 2 {
return errors.New("invalid selection")
}
tableName := ndata.bucketPath[1]
// We have rendered row IDs, need to convert back to an SQLite primary key
// TODO stash the real key inside f.contentBox.Objects()
// FIXME breaks if you try and edit the primary key(!)
ctx := context.Background()
tx, err := n.db.BeginTx(ctx, nil)
if err != nil {
return err
}
var commitOK bool = false
defer func() {
if !commitOK {
err := tx.Rollback()
if err != nil {
retErr = err
}
}
}()
// Data grid properties
var columnNames []string
for i := int32(0); i < f.contentBox.ColCount(); i++ {
columnNames = append(columnNames, f.contentBox.Columns().Items(i).Title().Caption())
}
// Query sqlite table metadata to determine which of these is the PRIMARY KEY
var primaryColumnName string
err = tx.QueryRowContext(ctx, `SELECT l.name FROM pragma_table_info(?) as l WHERE l.pk = 1;`, tableName).Scan(&primaryColumnName)
if err != nil {
return fmt.Errorf("Finding primary key for update: %w", err)
}
// Convert it to an index
var primaryColumnIdx int = -1
for i := 0; i < len(columnNames); i++ {
if columnNames[i] == primaryColumnName {
primaryColumnIdx = i
break
}
}
if primaryColumnIdx == -1 {
return fmt.Errorf("Primary key %q missing from available columns", primaryColumnName)
}
// SQLite can only LIMIT 1 on update/delete if it was compiled with
// SQLITE_ENABLE_UPDATE_DELETE_LIMIT, which isn't the case for the mattn
// cgo library
// Skip that, and just rely on primary key uniqueness
// Edit
for rowid, editcells := range f.updateRows {
stmt := `UPDATE "` + tableName + `" SET `
params := []interface{}{} // FIXME reinstate types for the driver (although SQLite doesn't mind)
for ct, cell := range editcells {
if ct > 0 {
stmt += `, `
}
stmt += `"` + columnNames[cell] + `" = ?`
params = append(params, f.contentBox.Cells(cell, rowid))
}
stmt += ` WHERE "` + primaryColumnName + `" = ?`
pkVal := f.contentBox.Cells(int32(primaryColumnIdx), rowid)
params = append(params, pkVal)
_, err = tx.ExecContext(ctx, stmt, params...)
if err != nil {
return fmt.Errorf("Updating row %q: %w", pkVal, err)
}
}
// Delete by key (affects rowids after re-render)
for rowid, _ := range f.deleteRows {
pkVal := f.contentBox.Cells(int32(primaryColumnIdx), rowid)
stmt := `DELETE FROM "` + tableName + `" WHERE "` + primaryColumnName + `" = ?`
_, err = tx.ExecContext(ctx, stmt, pkVal)
if err != nil {
return fmt.Errorf("Deleting row %q: %w", pkVal, err)
}
}
// Insert all new entries
for rowid, _ := range f.insertRows {
stmt := `INSERT INTO "` + tableName + `" (` + strings.Join(columnNames, `, `) + `) VALUES (`
params := []interface{}{} // FIXME reinstate types for the driver (although SQLite doesn't mind)
for colid := 0; colid < len(columnNames); colid++ {
if colid > 0 {
stmt += `, `
}
stmt += "?"
params = append(params, f.contentBox.Cells(int32(colid), rowid))
}
stmt += `)`
_, err = tx.ExecContext(ctx, stmt, params...)
if err != nil {
return fmt.Errorf("Inserting row: %w", err)
}
}
err = tx.Commit()
if err != nil {
return err
}
commitOK = true // no need for rollback
return nil
}
func (ld *sqliteLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
if len(ndata.bucketPath) == 0 {
// The top-level children are always:
return []string{sqliteTablesCaption}, nil
}
if len(ndata.bucketPath) == 1 && ndata.bucketPath[0] == sqliteTablesCaption {
rr, err := ld.db.Query(`SELECT name FROM sqlite_master WHERE type='table' ORDER BY name ASC;`)
if err != nil {
return nil, err
}
defer rr.Close()
var gather []string
for rr.Next() {
var tableName string
err = rr.Scan(&tableName)
if err != nil {
return nil, err
}
gather = append(gather, tableName)
}
if rr.Err() != nil {
return nil, rr.Err()
}
return gather, nil
}
if len(ndata.bucketPath) == 2 {
return nil, nil // Never any deeper children
}
return nil, fmt.Errorf("unknown nav path %#v", ndata.bucketPath)
}
func (ld *sqliteLoadedDatabase) NavContext(ndata *navData) (ret []contextAction, err error) {
if len(ndata.bucketPath) == 0 {
ret = append(ret, contextAction{"Compact database", ld.CompactDatabase})
ret = append(ret, contextAction{"Export backup...", ld.ExportBackup})
}
if len(ndata.bucketPath) == 2 {
ret = append(ret, contextAction{"Drop table", ld.DropTable})
}
return
}
func (ld *sqliteLoadedDatabase) CompactDatabase(sender vcl.IComponent, ndata *navData) error {
_, err := ld.db.Exec(`VACUUM;`)
return err
}
func (ld *sqliteLoadedDatabase) ExportBackup(sender vcl.IComponent, ndata *navData) error {
// Popup for output file
dlg := vcl.NewSaveDialog(sender)
dlg.SetTitle("Save backup as...")
dlg.SetFilter(sqliteFilter)
ret := dlg.Execute() // Fake blocking
if !ret {
return nil // cancelled
}
_, err := ld.db.Exec(`VACUUM INTO ?`, dlg.FileName())
return err
}
func (ld *sqliteLoadedDatabase) DropTable(sender vcl.IComponent, ndata *navData) error {
if len(ndata.bucketPath) != 2 {
return errors.New("Invalid selection")
}
//
tableName := ndata.bucketPath[1]
if !vcl_confirm_dialog(sender, "Drop table", fmt.Sprintf("Are you sure you want to drop the table %q?", tableName)) {
return nil // cancelled
}
_, err := ld.db.Exec(`DROP TABLE "` + tableName + `"`) // WARNING can't prepare this parameter, but it comes from the DB (trusted)
return err
}
func (ld *sqliteLoadedDatabase) Close() {
_ = ld.db.Close()
ld.arena = nil
}
var _ loadedDatabase = &sqliteLoadedDatabase{} // interface assertion
//
func (f *TMainForm) sqliteAddDatabaseFromFile(path string, cliDriver bool) {
driver := "sqlite3"
if cliDriver {
driver = "sqliteclidriver"
}
// TODO load in background thread to stop blocking the UI
db, err := sql.Open(driver, path)
if err != nil {
vcl.ShowMessage(fmt.Sprintf("Failed to load database '%s': %s", path, err.Error()))
return
}
ld := &sqliteLoadedDatabase{
path: path,
displayName: filepath.Base(path),
db: db,
}
ld.nav = f.Buckets.Items().Add(nil, ld.displayName)
ld.nav.SetImageIndex(imgDatabase)
ld.nav.SetSelectedIndex(imgDatabase)
ld.nav.SetHasChildren(true) // dynamically populate in OnNavExpanding
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

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

View File

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

94
debconf/debconf.go Normal file
View File

@@ -0,0 +1,94 @@
package debconf
import (
"bufio"
"fmt"
"io"
"strings"
)
const (
DefaultConfigDat = `/var/cache/debconf/config.dat`
DefaultPasswordsDat = `/var/cache/debconf/passwords.dat`
DefaultTemplatesDat = `/var/cache/debconf/templates.dat`
)
type Entry struct {
Name string
Properties [][2]string
}
type Database struct {
Entries []Entry
AllColumnNames []string
}
func Parse(r io.Reader) (*Database, error) {
sc := bufio.NewScanner(r)
var entries []Entry
var wip Entry
var linenum int = 0
knownColumnNames := map[string]struct{}{
"Name": struct{}{},
}
var discoveredColumns []string = []string{"Name"}
for sc.Scan() {
linenum++
line := sc.Text()
if line == "" {
if wip.Name != "" {
entries = append(entries, wip)
wip = Entry{}
}
continue
}
if line[0] == ' ' {
// continuation of last text entry
if len(wip.Properties) == 0 {
return nil, fmt.Errorf("Continuation of nonexistent entry on line %d", linenum)
}
wip.Properties[len(wip.Properties)-1][1] += line[1:]
} else {
// New pair on current element
key, rest, ok := strings.Cut(line, `:`)
if !ok {
return nil, fmt.Errorf("Missing : on line %d", linenum)
}
if _, ok := knownColumnNames[key]; !ok {
knownColumnNames[key] = struct{}{}
discoveredColumns = append(discoveredColumns, key)
}
rest = strings.TrimLeft(rest, " \t")
if key == `Name` {
wip.Name = rest
} else {
wip.Properties = append(wip.Properties, [2]string{key, rest})
}
}
}
if sc.Err() != nil {
return nil, sc.Err()
}
if wip.Name != "" {
entries = append(entries, wip)
}
return &Database{
Entries: entries,
AllColumnNames: discoveredColumns,
}, nil
}

30
debconf/debconf_test.go Normal file
View File

@@ -0,0 +1,30 @@
package debconf
import (
"os"
"testing"
)
func TestDebconfParse(t *testing.T) {
src, err := os.Open(DefaultConfigDat)
if err != nil {
if os.IsNotExist(err) {
t.Skip(err)
}
t.Fatal(err)
}
defer src.Close()
db, err := Parse(src)
if err != nil {
t.Fatalf("Parse: %v", err)
}
if len(db.Entries) == 0 {
t.Errorf("expected >0 entries, got %v", len(db.Entries))
}
if len(db.AllColumnNames) == 0 {
t.Errorf("expected >0 column names, got %v", len(db.AllColumnNames))
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 30 KiB

45
go.mod
View File

@@ -3,36 +3,59 @@ module yvbolt
go 1.19 go 1.19
require ( require (
github.com/cockroachdb/pebble v1.0.0
github.com/dgraph-io/badger/v4 v4.2.0 github.com/dgraph-io/badger/v4 v4.2.0
github.com/google/uuid v1.6.0
github.com/mattn/go-sqlite3 v1.14.22 github.com/mattn/go-sqlite3 v1.14.22
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
github.com/redis/go-redis/v9 v9.5.3
github.com/stretchr/testify v1.9.0
github.com/ying32/govcl v2.2.3+incompatible github.com/ying32/govcl v2.2.3+incompatible
go.etcd.io/bbolt v1.4.0-alpha.1 go.etcd.io/bbolt v1.4.0-alpha.1
modernc.org/sqlite v1.24.0 modernc.org/sqlite v1.24.0
) )
require ( require (
github.com/DataDog/zstd v1.4.5 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cockroachdb/errors v1.11.3 // indirect
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect
github.com/cockroachdb/redact v1.1.5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgraph-io/ristretto v0.1.1 // indirect github.com/dgraph-io/ristretto v0.1.1 // 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.27.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/glog v1.0.0 // indirect github.com/golang/glog v1.0.0 // indirect
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 // indirect github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
github.com/golang/protobuf v1.5.2 // indirect github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.3 // indirect github.com/golang/snappy v0.0.4 // indirect
github.com/google/flatbuffers v1.12.1 // indirect github.com/google/flatbuffers v1.12.1 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/klauspost/compress v1.12.3 // indirect github.com/klauspost/compress v1.16.0 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mattn/go-isatty v0.0.17 // 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.0 // indirect
github.com/prometheus/client_golang v1.12.0 // indirect
github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a // indirect
github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect
go.opencensus.io v0.22.5 // indirect go.opencensus.io v0.22.5 // indirect
golang.org/x/mod v0.3.0 // indirect golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect
golang.org/x/net v0.7.0 // indirect golang.org/x/mod v0.18.0 // indirect
golang.org/x/net v0.23.0 // indirect
golang.org/x/sys v0.19.0 // indirect golang.org/x/sys v0.19.0 // indirect
golang.org/x/tools v0.0.0-20210106214847-113979e3529a // indirect golang.org/x/text v0.14.0 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect golang.org/x/tools v0.13.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/uint128 v1.2.0 // indirect lukechampine.com/uint128 v1.2.0 // indirect
modernc.org/cc/v3 v3.40.0 // indirect modernc.org/cc/v3 v3.40.0 // indirect
modernc.org/ccgo/v3 v3.16.13 // indirect modernc.org/ccgo/v3 v3.16.13 // indirect

465
go.sum
View File

@@ -1,9 +1,71 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
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/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/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cockroachdb/datadriven v1.0.3-0.20230801171734-e384cf455877 h1:1MLK4YpFtIEo3ZtMA5C795Wtv5VuUnrXX7mQG+aHg6o=
github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I=
github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8=
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE=
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs=
github.com/cockroachdb/pebble v1.0.0 h1:WZWlV/s78glZbY2ylUITDOWSVBD3cLjcWPLRPFbHNYg=
github.com/cockroachdb/pebble v1.0.0/go.mod h1:bynZ3gvVyhlvjLI7PT6dmZ7g76xzJ7HpxfjgkzCGz6s=
github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30=
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/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 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=
@@ -13,126 +75,492 @@ github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWa
github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/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/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
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-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
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 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 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ=
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 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.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/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
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.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 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 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw=
github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= 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.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.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 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/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
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 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= 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.12.3 h1:G5AfA94pHPysR56qqrkO2pxEexdDzrpFJ6yt/VqWxVU= github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4=
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 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/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/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI=
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
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/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 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/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_golang v1.12.0 h1:C+UIj/QWtmqY13Arb8kwMt5j34/0Z2iKamrJ+ryC0Gg=
github.com/prometheus/client_golang v1.12.0/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a h1:CmF68hwI0XsOQ5UwlBopMi2Ow4Pbg32akc4KIVCOm+Y=
github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
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/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-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 h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 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/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
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.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/ying32/govcl v2.2.3+incompatible h1:Iyfcl26yNE1USm+3uG+btQyhkoFIV18+VITrUdHu8Lw= github.com/ying32/govcl v2.2.3+incompatible h1:Iyfcl26yNE1USm+3uG+btQyhkoFIV18+VITrUdHu8Lw=
github.com/ying32/govcl v2.2.3+incompatible/go.mod h1:yZVtbJ9Md1nAVxtHKIriKZn4K6TQYqI1en3sN/m9FJ8= github.com/ying32/govcl v2.2.3+incompatible/go.mod h1:yZVtbJ9Md1nAVxtHKIriKZn4K6TQYqI1en3sN/m9FJ8=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
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=
go.etcd.io/bbolt v1.4.0-alpha.1 h1:3yrqQzbRRPFPdOMWS/QQIVxVnzSkAZQYeWlZFv1kbj4= go.etcd.io/bbolt v1.4.0-alpha.1 h1:3yrqQzbRRPFPdOMWS/QQIVxVnzSkAZQYeWlZFv1kbj4=
go.etcd.io/bbolt v1.4.0-alpha.1/go.mod h1:S/Z/Nm3iuOnyO1W4XuFfPci51Gj6F1Hv0z8hisyYYOw= go.etcd.io/bbolt v1.4.0-alpha.1/go.mod h1:S/Z/Nm3iuOnyO1W4XuFfPci51Gj6F1Hv0z8hisyYYOw=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0= go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
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/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
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-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-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-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 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
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.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
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-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-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-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.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 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-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-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-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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-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-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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/text v0.0.0-20170915032832-14c0d48ead0c/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.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.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
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-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 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-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-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-20210106214847-113979e3529a h1:CB3a9Nez8M13wwlr/E2YtwoU+qYHKfC+JrDa45RXXoQ= 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.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 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
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/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
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.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.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-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-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.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 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-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.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 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 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= 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 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
@@ -157,3 +585,6 @@ 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 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY= 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=

View File

@@ -10,17 +10,33 @@ import (
var assetsFs embed.FS var assetsFs embed.FS
const ( const (
imgChartBar int32 = iota imgAdd int32 = iota
imgArrowRefresh
imgChartBar
imgDatabase imgDatabase
imgDatabaseAdd imgDatabaseAdd
imgDatabaseDelete imgDatabaseDelete
imgDatabaseLightning imgDatabaseLightning
imgDatabaseSave imgDatabaseSave
imgDelete
imgLightning imgLightning
imgLightningGo
imgPencil
imgPencilAdd
imgPencilDelete
imgPencilGo
imgResultsetNext
imgTable imgTable
imgTableAdd imgTableAdd
imgTableDelete imgTableDelete
imgTableSave imgTableSave
imgVendorCockroach
imgVendorDebian
imgVendorDgraph
imgVendorGithub
imgVendorMySQL
imgVendorRedis
imgVendorSqlite
) )
func loadImages(owner vcl.IComponent) *vcl.TImageList { func loadImages(owner vcl.IComponent) *vcl.TImageList {
@@ -40,17 +56,36 @@ func loadImages(owner vcl.IComponent) *vcl.TImageList {
} }
ilist := vcl.NewImageList(owner) ilist := vcl.NewImageList(owner)
// ls assets | sort | sed -re 's~(.+)~ilist.Add(mustLoad("assets/\1"), nil)~'
ilist.Add(mustLoad("assets/add.png"), nil)
ilist.Add(mustLoad("assets/arrow_refresh.png"), nil)
ilist.Add(mustLoad("assets/chart_bar.png"), nil) ilist.Add(mustLoad("assets/chart_bar.png"), nil)
ilist.Add(mustLoad("assets/database.png"), nil) ilist.Add(mustLoad("assets/database.png"), nil)
ilist.Add(mustLoad("assets/database_add.png"), nil) ilist.Add(mustLoad("assets/database_add.png"), nil)
ilist.Add(mustLoad("assets/database_delete.png"), nil) ilist.Add(mustLoad("assets/database_delete.png"), nil)
ilist.Add(mustLoad("assets/database_lightning.png"), nil) ilist.Add(mustLoad("assets/database_lightning.png"), nil)
ilist.Add(mustLoad("assets/database_save.png"), nil) ilist.Add(mustLoad("assets/database_save.png"), nil)
ilist.Add(mustLoad("assets/delete.png"), nil)
ilist.Add(mustLoad("assets/lightning.png"), nil) ilist.Add(mustLoad("assets/lightning.png"), nil)
ilist.Add(mustLoad("assets/lightning_go.png"), nil)
ilist.Add(mustLoad("assets/pencil.png"), nil)
ilist.Add(mustLoad("assets/pencil_add.png"), nil)
ilist.Add(mustLoad("assets/pencil_delete.png"), nil)
ilist.Add(mustLoad("assets/pencil_go.png"), nil)
ilist.Add(mustLoad("assets/resultset_next.png"), nil)
ilist.Add(mustLoad("assets/table.png"), nil) ilist.Add(mustLoad("assets/table.png"), nil)
ilist.Add(mustLoad("assets/table_add.png"), nil) ilist.Add(mustLoad("assets/table_add.png"), nil)
ilist.Add(mustLoad("assets/table_delete.png"), nil) ilist.Add(mustLoad("assets/table_delete.png"), nil)
ilist.Add(mustLoad("assets/table_save.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 return ilist
} }

155
lexer/lexer.go Normal file
View File

@@ -0,0 +1,155 @@
package lexer
import (
"fmt"
"strings"
)
func isWhitespace(r byte) bool {
return (r == ' ' || r == '\t' || r == '\r' || r == '\n')
}
func Quote(input string) string {
return `"` + strings.ReplaceAll(strings.ReplaceAll(input, `\`, `\\`), `"`, `\"`) + `"`
}
// Fields splits a string into separate tokens using something kind of vaguely
// like how SQL would do it.
// The result still includes the quote and backslash characters.
func Fields(input string) ([]string, error) {
const (
StateToplevel = 0
StateWhitespace = 1
StateInDoubleQuote = 2
StateInDoubleQuoteSlash = 3
StateInSingleQuote = 4
StateInSingleQuoteSlash = 5
)
var (
ret []string
state int = StateToplevel
wip string
)
for pos := 0; pos < len(input); pos++ {
c := input[pos]
switch state {
case StateToplevel:
if isWhitespace(c) {
state = StateWhitespace
if len(wip) != 0 {
ret = append(ret, wip)
wip = ""
}
} else if c == '"' {
if len(wip) != 0 {
return nil, fmt.Errorf(`Unexpected " at char %d`, pos)
}
wip += string(c)
state = StateInDoubleQuote
} else if c == '\'' {
if len(wip) != 0 {
return nil, fmt.Errorf(`Unexpected ' at char %d`, pos)
}
wip += string(c)
state = StateInSingleQuote
} else if c == '\\' {
return nil, fmt.Errorf(`Unexpected \ at char %d`, pos)
} else if c == '(' || c == ')' || c == '?' || c == ',' || c == '+' || c == '*' || c == '-' || c == '/' || c == '%' || c == ';' || c == '=' {
// Tokenize separately, even if they appear touching another top-level token
// Should still be safe to re-join
if len(wip) != 0 {
ret = append(ret, wip)
wip = ""
}
ret = append(ret, string(c))
} else {
wip += string(c)
}
case StateWhitespace:
if isWhitespace(c) {
// continue
} else {
state = StateToplevel
pos-- // reparse
}
case StateInDoubleQuote:
if c == '"' {
wip += string(c)
ret = append(ret, wip)
wip = ""
state = StateToplevel
} else if c == '\\' {
wip += string(c)
state = StateInDoubleQuoteSlash
} else {
wip += string(c)
}
case StateInDoubleQuoteSlash:
if isWhitespace(c) {
return nil, fmt.Errorf(`Unexpected whitespace after \ at char %d`, pos)
} else {
wip += string(c)
state = StateInDoubleQuote
}
case StateInSingleQuote:
if c == '\'' {
wip += string(c)
ret = append(ret, wip)
wip = ""
state = StateToplevel
} else if c == '\\' {
wip += string(c)
state = StateInSingleQuoteSlash
} else {
wip += string(c)
}
case StateInSingleQuoteSlash:
if isWhitespace(c) {
return nil, fmt.Errorf(`Unexpected whitespace after \ at char %d`, pos)
} else {
wip += string(c)
state = StateInSingleQuote
}
}
}
// Reached the end of input stream
switch state {
case StateToplevel:
if len(wip) > 0 {
ret = append(ret, wip)
wip = ""
}
return ret, nil
case StateWhitespace:
return ret, nil
default:
return nil, fmt.Errorf(`Unexpected end of quoted input`)
}
}

125
lexer/lexer_test.go Normal file
View File

@@ -0,0 +1,125 @@
package lexer
import (
"reflect"
"testing"
)
func TestLexer(t *testing.T) {
type testCase struct {
input string
expect []string
expectErr bool
}
cases := []testCase{
testCase{
input: "foo bar baz",
expect: []string{"foo", "bar", "baz"},
expectErr: false,
},
// Quotes
testCase{
input: `foo "bar" baz`,
expect: []string{"foo", `"bar"`, "baz"},
expectErr: false,
},
testCase{
input: `foo "bar baz" quux`,
expect: []string{"foo", `"bar baz"`, "quux"},
expectErr: false,
},
testCase{
input: `foo 'bar baz' quux`,
expect: []string{"foo", `'bar baz'`, "quux"},
expectErr: false,
},
// Escape characters
testCase{
input: `foo 'bar \n baz' quux`,
expect: []string{"foo", `'bar \n baz'`, "quux"},
expectErr: false,
},
testCase{
input: `foo "bar\"" baz`,
expect: []string{"foo", `"bar\""`, "baz"},
expectErr: false,
},
// Collapsing whitespace
testCase{
input: " foo bar \r\t\n baz\n",
expect: []string{"foo", "bar", "baz"},
expectErr: false,
},
// Special characters lexed as separate tokens, but only at top level
testCase{
input: `3+5*(2.3/6);`,
expect: []string{"3", `+`, "5", "*", "(", "2.3", "/", "6", ")", ";"},
expectErr: false,
},
testCase{
input: `SELECT "3+5*(2.3/6)" AS expression;`,
expect: []string{"SELECT", `"3+5*(2.3/6)"`, "AS", "expression", ";"},
expectErr: false,
},
testCase{
input: `INSERT INTO foo (bar, baz) VALUES (?, ?);`,
expect: []string{"INSERT", "INTO", "foo", "(", "bar", ",", "baz", ")", "VALUES", "(", "?", ",", "?", ")", ";"},
expectErr: false,
},
// Errors
testCase{
input: `foo "bar`,
expect: nil,
expectErr: true, // mismatched quotes
},
testCase{
input: `foo 'bar`,
expect: nil,
expectErr: true, // mismatched quotes
},
testCase{
input: `foo \"bar"`,
expect: nil,
expectErr: true, // invalid top-level escape
},
testCase{
input: `foo "bar\ "`,
expect: nil,
expectErr: true, // escaping nothing
},
}
for _, tc := range cases {
out, err := Fields(tc.input)
if err != nil {
if !tc.expectErr {
t.Errorf("Test %q got error %v, expected nil", tc.input, err)
}
} else {
if tc.expectErr {
t.Errorf("Test %q got error <nil>, expected error", tc.input)
continue
}
if !reflect.DeepEqual(out, tc.expect) {
t.Errorf("Test %q\n- got: %#v\n- expected %#v", tc.input, out, tc.expect)
}
}
}
}

View File

@@ -1,18 +1,36 @@
package main package main
import ( import (
"errors"
"github.com/ying32/govcl/vcl" "github.com/ying32/govcl/vcl"
) )
var ErrNavNotExist error = errors.New("The selected item no longer exists")
type contextAction struct {
Name string
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 DisplayName() string
DriverName() string DriverName() string
RootElement() *vcl.TTreeNode RootElement() *vcl.TTreeNode
RenderForNav(f *TMainForm, ndata *navData) RenderForNav(f *TMainForm, ndata *navData) error
ExecQuery(query string, resultArea *vcl.TListView)
NavChildren(ndata *navData) ([]string, error) NavChildren(ndata *navData) ([]string, error)
NavContext(ndata *navData) ([]contextAction, error)
Keepalive(ndata *navData) Keepalive(ndata *navData)
Close()
}
type queryableLoadedDatabase interface {
ExecQuery(query string, resultArea *vcl.TStringGrid) error
}
type editableLoadedDatabase interface {
ApplyChanges(f *TMainForm, ndata *navData) error
} }
// navData is the .Data() pointer for each TTreeNode in the left-hand tree. // navData is the .Data() pointer for each TTreeNode in the left-hand tree.

751
main.go
View File

@@ -1,19 +1,27 @@
package main package main
import ( import (
"errors"
"fmt" "fmt"
"runtime"
"runtime/debug"
"strings" "strings"
"unsafe" "unsafe"
_ "github.com/ying32/govcl/pkgs/winappres" // Extra _syso files for Windows "github.com/pkg/browser"
"github.com/ying32/govcl/vcl" "github.com/ying32/govcl/vcl"
"github.com/ying32/govcl/vcl/types" "github.com/ying32/govcl/vcl/types"
"github.com/ying32/govcl/vcl/types/colors"
) )
const ( const (
MY_SPACING = 6 APPNAME = "yvbolt"
MY_HEIGHT = 90 HOMEPAGE_URL = "https://code.ivysaur.me/yvbolt"
MY_WIDTH = 180
CO_INSERT = colors.ClYellow
CO_EDIT_IMPLICIT = colors.ClLightgreen
CO_EDIT_EXPLICIT = colors.ClGreen
CO_DELETE = colors.ClRed
) )
type TMainForm struct { type TMainForm struct {
@@ -21,14 +29,26 @@ type TMainForm struct {
ImageList *vcl.TImageList ImageList *vcl.TImageList
Menu *vcl.TMainMenu Menu *vcl.TMainMenu
SQLiteUseCliDriver *vcl.TMenuItem
StatusBar *vcl.TStatusBar StatusBar *vcl.TStatusBar
Buckets *vcl.TTreeView Buckets *vcl.TTreeView
Tabs *vcl.TPageControl Tabs *vcl.TPageControl
propertiesBox *vcl.TMemo propertiesBox *vcl.TMemo
contentBox *vcl.TListView contentBox *vcl.TStringGrid
queryInput *vcl.TMemo dataInsertBtn *vcl.TToolButton
queryResult *vcl.TListView dataDelRowBtn *vcl.TToolButton
dataCommitBtn *vcl.TToolButton
isEditing bool
insertRows map[int32]struct{} // Rows in the StringGrid that are to-be-inserted
deleteRows map[int32]struct{}
updateRows map[int32][]int32 // Row->cells that are to-be-updated
queryExecBtn *vcl.TToolButton
queryInput *vcl.TRichEdit
queryResult *vcl.TStringGrid
none *noLoadedDatabase
dbs []loadedDatabase dbs []loadedDatabase
} }
@@ -43,40 +63,81 @@ func main() {
func (f *TMainForm) OnFormCreate(sender vcl.IObject) { func (f *TMainForm) OnFormCreate(sender vcl.IObject) {
f.ImageList = loadImages(f) f.ImageList = loadImages(f)
f.SetCaption("yvbolt") f.SetCaption(APPNAME)
f.ScreenCenter()
f.SetWidth(1280)
f.SetHeight(640)
f.ImageList.GetIcon(imgDatabaseLightning, f.Icon()) f.ImageList.GetIcon(imgDatabaseLightning, f.Icon())
mnuFile := vcl.NewMenuItem(f) mnuFile := vcl.NewMenuItem(f)
mnuFile.SetCaption("File") mnuFile.SetCaption("File")
mnuFileOpen := vcl.NewMenuItem(mnuFile) //
mnuFileOpen.SetCaption("Open Bolt database...")
mnuFileOpen.SetImageIndex(imgDatabaseAdd)
mnuFileOpen.SetShortCutFromString("Ctrl+O")
mnuFileOpen.SetOnClick(f.OnMnuFileOpenClick)
mnuFile.Add(mnuFileOpen)
mnuFileSqliteOpen := vcl.NewMenuItem(mnuFile) mnuFileBadger := vcl.NewMenuItem(mnuFile)
mnuFileSqliteOpen.SetCaption("Open SQLite database...") mnuFileBadger.SetCaption("Badger")
mnuFileSqliteOpen.SetImageIndex(imgDatabaseAdd) mnuFileBadger.SetImageIndex(imgVendorDgraph)
mnuFileSqliteOpen.SetOnClick(f.OnMnuFileSqliteOpenClick) mnuFile.Add(mnuFileBadger)
mnuFile.Add(mnuFileSqliteOpen)
mnuFileBadgerOpen := vcl.NewMenuItem(mnuFile) vcl_menuitem(mnuFileBadger, "Open database...", imgDatabaseAdd, f.OnMnuFileBadgerOpenClick)
mnuFileBadgerOpen.SetCaption("Open Badger v4 database...") vcl_menuitem(mnuFileBadger, "New in-memory database", imgDatabaseAdd, f.OnMnuFileBadgerMemoryClick)
mnuFileBadgerOpen.SetImageIndex(imgDatabaseAdd)
mnuFileBadgerOpen.SetOnClick(f.OnMnuFileBadgerOpenClick)
mnuFile.Add(mnuFileBadgerOpen)
mnuFileSqliteMemory := vcl.NewMenuItem(mnuFile) //
mnuFileSqliteMemory.SetCaption("New SQLite in-memory database")
mnuFileSqliteMemory.SetImageIndex(imgDatabaseAdd)
mnuFileSqliteMemory.SetOnClick(f.OnMnuFileSqliteMemoryClick)
mnuFile.Add(mnuFileSqliteMemory)
mnuSep := vcl.NewMenuItem(mnuFile) mnuFileBolt := vcl.NewMenuItem(mnuFile)
mnuSep.SetCaption("-") // Creates separator mnuFileBolt.SetCaption("Bolt")
mnuFile.Add(mnuSep) mnuFileBolt.SetImageIndex(imgVendorGithub)
mnuFile.Add(mnuFileBolt)
vcl_menuitem(mnuFileBolt, "New database...", imgDatabaseAdd, f.OnMnuFileBoltNewClick)
vcl_menuitem(mnuFileBolt, "Open database...", imgDatabaseAdd, f.OnMnuFileBoltOpenClick)
vcl_menuitem(mnuFileBolt, "Open database (read-only)...", imgDatabaseAdd, f.OnMnuFileBoltOpenReadonlyClick)
//
mnuFileDebconf := vcl.NewMenuItem(mnuFile)
mnuFileDebconf.SetCaption("Debconf")
mnuFileDebconf.SetImageIndex(imgVendorDebian)
mnuFile.Add(mnuFileDebconf)
vcl_menuitem(mnuFileDebconf, "Open database...", imgDatabaseAdd, f.OnMnuFileDebianOpenClick)
//
mnuFilePebble := vcl.NewMenuItem(mnuFile)
mnuFilePebble.SetCaption("Pebble")
mnuFilePebble.SetImageIndex(imgVendorCockroach)
mnuFile.Add(mnuFilePebble)
vcl_menuitem(mnuFilePebble, "Open database...", imgDatabaseAdd, f.OnMnuFilePebbleOpenClick)
vcl_menuitem(mnuFilePebble, "New in-memory database", imgDatabaseAdd, f.OnMnuFilePebbleMemoryClick)
//
mnuFileRedis := vcl.NewMenuItem(mnuFile)
mnuFileRedis.SetCaption("Redis")
mnuFileRedis.SetImageIndex(imgVendorRedis)
mnuFile.Add(mnuFileRedis)
vcl_menuitem(mnuFileRedis, "Connect...", imgDatabaseAdd, f.OnMnuFileRedisConnectClick)
//
mnuFileSqlite := vcl.NewMenuItem(mnuFile)
mnuFileSqlite.SetCaption("SQLite")
mnuFileSqlite.SetImageIndex(imgVendorSqlite)
mnuFile.Add(mnuFileSqlite)
vcl_menuitem(mnuFileSqlite, "Open database...", imgDatabaseAdd, f.OnMnuFileSqliteOpenClick)
vcl_menuitem(mnuFileSqlite, "New in-memory database", imgDatabaseAdd, f.OnMnuFileSqliteMemoryClick)
vcl_menuseparator(mnuFileSqlite)
f.SQLiteUseCliDriver = vcl_menuitem(mnuFileSqlite, "Connect using CLI driver (experimental)", -1, nil)
f.SQLiteUseCliDriver.SetAutoCheck(true)
//
vcl_menuseparator(mnuFile)
mnuFileExit := vcl.NewMenuItem(mnuFile) mnuFileExit := vcl.NewMenuItem(mnuFile)
mnuFileExit.SetCaption("Exit") mnuFileExit.SetCaption("Exit")
@@ -90,13 +151,28 @@ func (f *TMainForm) OnFormCreate(sender vcl.IObject) {
mnuQueryExecute.SetCaption("Execute") mnuQueryExecute.SetCaption("Execute")
mnuQueryExecute.SetShortCutFromString("F5") mnuQueryExecute.SetShortCutFromString("F5")
mnuQueryExecute.SetOnClick(f.OnQueryExecute) mnuQueryExecute.SetOnClick(f.OnQueryExecute)
mnuQueryExecute.SetImageIndex(imgLightning) mnuQueryExecute.SetImageIndex(imgResultsetNext)
mnuQuery.Add(mnuQueryExecute) mnuQuery.Add(mnuQueryExecute)
mnuHelp := vcl.NewMenuItem(f)
mnuHelp.SetCaption("Help")
mnuHelpVersion := vcl.NewMenuItem(mnuHelp)
mnuHelpVersion.SetCaption("Driver versions...")
mnuHelpVersion.SetOnClick(f.OnMenuHelpVersion)
mnuHelp.Add(mnuHelpVersion)
mnuHelpHomepage := vcl.NewMenuItem(mnuHelp)
mnuHelpHomepage.SetCaption("About " + APPNAME)
mnuHelpHomepage.SetShortCutFromString("F1")
mnuHelpHomepage.SetOnClick(f.OnMnuHelpHomepage)
mnuHelp.Add(mnuHelpHomepage)
f.Menu = vcl.NewMainMenu(f) f.Menu = vcl.NewMainMenu(f)
f.Menu.SetImages(f.ImageList) f.Menu.SetImages(f.ImageList)
f.Menu.Items().Add(mnuFile) f.Menu.Items().Add(mnuFile)
f.Menu.Items().Add(mnuQuery) f.Menu.Items().Add(mnuQuery)
f.Menu.Items().Add(mnuHelp)
// //
@@ -114,6 +190,7 @@ func (f *TMainForm) OnFormCreate(sender vcl.IObject) {
f.Buckets.SetReadOnly(true) // prevent click to rename on nodes f.Buckets.SetReadOnly(true) // prevent click to rename on nodes
f.Buckets.SetOnExpanding(f.OnNavExpanding) f.Buckets.SetOnExpanding(f.OnNavExpanding)
f.Buckets.SetOnChange(f.OnNavChange) f.Buckets.SetOnChange(f.OnNavChange)
f.Buckets.SetOnContextPopup(f.OnNavContextPopup)
hsplit := vcl.NewSplitter(f) hsplit := vcl.NewSplitter(f)
hsplit.SetParent(f) hsplit.SetParent(f)
@@ -135,23 +212,67 @@ func (f *TMainForm) OnFormCreate(sender vcl.IObject) {
f.propertiesBox.BorderSpacing().SetAround(MY_SPACING) f.propertiesBox.BorderSpacing().SetAround(MY_SPACING)
f.propertiesBox.SetAlign(types.AlClient) // fill remaining space f.propertiesBox.SetAlign(types.AlClient) // fill remaining space
f.propertiesBox.SetReadOnly(true) f.propertiesBox.SetReadOnly(true)
f.propertiesBox.SetEnabled(false) f.propertiesBox.SetEnabled(true) // Need to leave it enabled so scrolling works
f.propertiesBox.SetColor(vcl_default_tab_background())
f.propertiesBox.SetBorderStyle(types.BsNone) f.propertiesBox.SetBorderStyle(types.BsNone)
f.propertiesBox.SetText("Open a database to get started...") f.propertiesBox.SetScrollBars(types.SsAutoVertical)
dataTab := vcl.NewTabSheet(f.Tabs) dataTab := vcl.NewTabSheet(f.Tabs)
dataTab.SetParent(f.Tabs) dataTab.SetParent(f.Tabs)
dataTab.SetCaption("Data") dataTab.SetCaption("Data")
dataTab.SetImageIndex(imgTable) dataTab.SetImageIndex(imgTable)
f.contentBox = vcl.NewListView(dataTab) dataButtonBar := vcl.NewToolBar(dataTab)
dataButtonBar.SetParent(dataTab)
dataButtonBar.SetAlign(types.AlTop)
dataButtonBar.BorderSpacing().SetLeft(MY_SPACING)
dataButtonBar.BorderSpacing().SetTop(MY_SPACING)
dataButtonBar.BorderSpacing().SetBottom(1)
dataButtonBar.BorderSpacing().SetRight(MY_SPACING)
dataButtonBar.SetEdgeBorders(0)
dataButtonBar.SetImages(f.ImageList)
dataRefreshBtn := vcl.NewToolButton(dataButtonBar)
dataRefreshBtn.SetParent(dataButtonBar)
dataRefreshBtn.SetHint("Refresh")
dataRefreshBtn.SetShowHint(true)
dataRefreshBtn.SetImageIndex(imgArrowRefresh)
dataRefreshBtn.SetOnClick(func(sender vcl.IObject) { f.RefreshCurrentItem() })
f.dataInsertBtn = vcl.NewToolButton(dataButtonBar)
f.dataInsertBtn.SetParent(dataButtonBar)
f.dataInsertBtn.SetImageIndex(imgAdd)
f.dataInsertBtn.SetHint("Insert")
f.dataInsertBtn.SetShowHint(true)
f.dataInsertBtn.SetOnClick(f.OnDataInsertClick)
f.dataDelRowBtn = vcl.NewToolButton(dataButtonBar)
f.dataDelRowBtn.SetParent(dataButtonBar)
f.dataDelRowBtn.SetImageIndex(imgDelete)
f.dataDelRowBtn.SetHint("Delete Row")
f.dataDelRowBtn.SetShowHint(true)
f.dataDelRowBtn.SetOnClick(f.OnDataDeleteRowClick)
f.dataCommitBtn = vcl.NewToolButton(dataButtonBar)
f.dataCommitBtn.SetParent(dataButtonBar)
f.dataCommitBtn.SetImageIndex(imgPencilGo)
f.dataCommitBtn.SetHint("Commit")
f.dataCommitBtn.SetShowHint(true)
f.dataCommitBtn.SetOnClick(f.OnDataCommitClick)
f.contentBox = vcl.NewStringGrid(dataTab)
f.contentBox.SetParent(dataTab) f.contentBox.SetParent(dataTab)
f.contentBox.BorderSpacing().SetAround(MY_SPACING) f.contentBox.BorderSpacing().SetLeft(MY_SPACING)
f.contentBox.BorderSpacing().SetRight(MY_SPACING)
f.contentBox.BorderSpacing().SetBottom(MY_SPACING)
f.contentBox.SetAlign(types.AlClient) // fill remaining space f.contentBox.SetAlign(types.AlClient) // fill remaining space
f.contentBox.SetViewStyle(types.VsReport) // "Report style" i.e. has columns f.contentBox.SetOptions(f.contentBox.Options().Include(types.GoThumbTracking, types.GoColSizing, types.GoDblClickAutoSize, types.GoEditing))
f.contentBox.SetAutoWidthLastColumn(true) f.contentBox.SetOnPrepareCanvas(f.OnDataPrepareCanvas)
f.contentBox.SetReadOnly(true) f.contentBox.SetOnEditingDone(f.OnDataCellEdited)
f.contentBox.Columns().Clear() f.contentBox.SetOnGetEditText(f.OnDataCellEditStarting)
f.contentBox.SetDefaultColWidth(MY_WIDTH)
vcl_stringgrid_clear(f.contentBox)
queryTab := vcl.NewTabSheet(f.Tabs) queryTab := vcl.NewTabSheet(f.Tabs)
queryTab.SetParent(f.Tabs) queryTab.SetParent(f.Tabs)
@@ -163,61 +284,94 @@ func (f *TMainForm) OnFormCreate(sender vcl.IObject) {
queryButtonBar.SetAlign(types.AlTop) queryButtonBar.SetAlign(types.AlTop)
queryButtonBar.BorderSpacing().SetLeft(MY_SPACING) queryButtonBar.BorderSpacing().SetLeft(MY_SPACING)
queryButtonBar.BorderSpacing().SetTop(MY_SPACING) queryButtonBar.BorderSpacing().SetTop(MY_SPACING)
queryButtonBar.BorderSpacing().SetBottom(0) //queryButtonBar.BorderSpacing().SetBottom(1)
queryButtonBar.BorderSpacing().SetRight(MY_SPACING) queryButtonBar.BorderSpacing().SetRight(MY_SPACING)
queryButtonBar.SetEdgeBorders(0)
queryButtonBar.SetImages(f.ImageList) queryButtonBar.SetImages(f.ImageList)
queryButtonBar.SetShowCaptions(true) queryButtonBar.SetShowCaptions(true)
queryExecBtn := vcl.NewToolButton(queryButtonBar) f.queryExecBtn = vcl.NewToolButton(queryButtonBar)
queryExecBtn.SetParent(queryButtonBar) f.queryExecBtn.SetParent(queryButtonBar)
queryExecBtn.SetCaption("Execute") f.queryExecBtn.SetHint("Execute")
// queryExecBtn.SetImageIndex(imgLightning) f.queryExecBtn.SetShowHint(true)
queryExecBtn.SetOnClick(f.OnQueryExecute) f.queryExecBtn.SetImageIndex(imgResultsetNext)
f.queryExecBtn.SetOnClick(f.OnQueryExecute)
f.queryInput = vcl.NewMemo(queryTab) f.queryInput = vcl.NewRichEdit(queryTab)
f.queryInput.SetParent(queryTab) f.queryInput.SetParent(queryTab)
f.queryInput.SetHeight(MY_HEIGHT) f.queryInput.SetHeight(MY_HEIGHT)
f.queryInput.SetAlign(types.AlTop) f.queryInput.SetAlign(types.AlTop)
f.queryInput.SetTop(1) f.queryInput.SetTop(1)
if runtime.GOOS == "windows" {
f.queryInput.Font().SetName("Consolas")
} else {
f.queryInput.Font().SetName("monospace") f.queryInput.Font().SetName("monospace")
}
f.queryInput.SetCursor(types.CrIBeam) // Use text cursor instead of default pointer cursor
f.queryInput.BorderSpacing().SetLeft(MY_SPACING) f.queryInput.BorderSpacing().SetLeft(MY_SPACING)
f.queryInput.BorderSpacing().SetTop(0) //f.queryInput.BorderSpacing().SetTop(1)
f.queryInput.BorderSpacing().SetRight(MY_SPACING) f.queryInput.BorderSpacing().SetRight(MY_SPACING)
f.queryInput.SetBorderStyle(types.BsFrame)
// f.queryInput.SetOnKeyUp(f.OnQueryTextChanged) // Performs extremely slowly
vsplit := vcl.NewSplitter(queryTab) vsplit := vcl.NewSplitter(queryTab)
vsplit.SetParent(queryTab) vsplit.SetParent(queryTab)
vsplit.SetAlign(types.AlTop) vsplit.SetAlign(types.AlTop)
vsplit.SetTop(2) vsplit.SetTop(2)
f.queryResult = vcl.NewListView(queryTab) f.queryResult = vcl.NewStringGrid(queryTab)
f.queryResult.SetParent(queryTab) f.queryResult.SetParent(queryTab)
f.queryResult.SetAlign(types.AlClient) // fill remaining space f.queryResult.SetAlign(types.AlClient) // fill remaining space
f.queryResult.SetViewStyle(types.VsReport) // "Report style" i.e. has columns
f.queryResult.SetAutoWidthLastColumn(true)
f.queryResult.SetReadOnly(true)
f.queryResult.Columns().Clear()
f.queryResult.BorderSpacing().SetLeft(MY_SPACING) f.queryResult.BorderSpacing().SetLeft(MY_SPACING)
f.queryResult.BorderSpacing().SetRight(MY_SPACING) f.queryResult.BorderSpacing().SetRight(MY_SPACING)
f.queryResult.BorderSpacing().SetBottom(MY_SPACING) f.queryResult.BorderSpacing().SetBottom(MY_SPACING)
f.queryResult.SetOptions(f.queryResult.Options().Include(types.GoThumbTracking))
f.queryResult.SetDefaultColWidth(MY_WIDTH)
vcl_stringgrid_clear(f.queryResult)
f.none = &noLoadedDatabase{}
f.OnNavChange(f, nil) // calls f.none.RenderForNav and sets up status bar content
} }
func (f *TMainForm) OnMnuFileOpenClick(sender vcl.IObject) { func (f *TMainForm) OnMnuFileBoltNewClick(sender vcl.IObject) {
dlg := vcl.NewOpenDialog(f) dlg := vcl.NewSaveDialog(f)
dlg.SetTitle("Select a database file...") dlg.SetTitle("Save database as...")
dlg.SetFilter("Bolt database|*.db|All files|*.*") dlg.SetFilter(boltFilter)
ret := dlg.Execute() // Fake blocking ret := dlg.Execute() // Fake blocking
if ret { if ret {
f.boltAddDatabaseFromFile(dlg.FileName()) f.boltAddDatabaseFromFile(dlg.FileName(), false)
}
}
func (f *TMainForm) OnMnuFileBoltOpenClick(sender vcl.IObject) {
dlg := vcl.NewOpenDialog(f)
dlg.SetTitle("Select a database file...")
dlg.SetFilter(boltFilter)
ret := dlg.Execute() // Fake blocking
if ret {
f.boltAddDatabaseFromFile(dlg.FileName(), false)
}
}
func (f *TMainForm) OnMnuFileBoltOpenReadonlyClick(sender vcl.IObject) {
dlg := vcl.NewOpenDialog(f)
dlg.SetTitle("Select a database file...")
dlg.SetFilter(boltFilter)
ret := dlg.Execute() // Fake blocking
if ret {
f.boltAddDatabaseFromFile(dlg.FileName(), true)
} }
} }
func (f *TMainForm) OnMnuFileSqliteOpenClick(sender vcl.IObject) { func (f *TMainForm) OnMnuFileSqliteOpenClick(sender vcl.IObject) {
cliDriver := f.SQLiteUseCliDriver.Checked()
dlg := vcl.NewOpenDialog(f) dlg := vcl.NewOpenDialog(f)
dlg.SetTitle("Select a database file...") dlg.SetTitle("Select a database file...")
dlg.SetFilter("SQLite database|*.db;*.db3;*.sqlite;*.sqlite3|All files|*.*") dlg.SetFilter(sqliteFilter)
ret := dlg.Execute() // Fake blocking ret := dlg.Execute() // Fake blocking
if ret { if ret {
f.sqliteAddDatabaseFromFile(dlg.FileName()) f.sqliteAddDatabaseFromFile(dlg.FileName(), cliDriver)
} }
} }
@@ -230,14 +384,370 @@ func (f *TMainForm) OnMnuFileBadgerOpenClick(sender vcl.IObject) {
} }
} }
func (f *TMainForm) OnMnuFileBadgerMemoryClick(sender vcl.IObject) {
f.badgerAddDatabaseFromMemory()
}
func (f *TMainForm) OnMnuFilePebbleOpenClick(sender vcl.IObject) {
dlg := vcl.NewSelectDirectoryDialog(f)
dlg.SetTitle("Select a database directory...")
ret := dlg.Execute() // Fake blocking
if ret {
f.pebbleAddDatabaseFrom(dlg.FileName(), nil)
}
}
func (f *TMainForm) OnMnuFileDebianOpenClick(sender vcl.IObject) {
dlg := vcl.NewOpenDialog(f)
dlg.SetTitle("Select a database file...")
dlg.SetFilter("Debconf database|*.dat|All files|*.*")
if runtime.GOOS == "linux" {
dlg.SetInitialDir(`/var/cache/debconf/`)
}
ret := dlg.Execute() // Fake blocking
if ret {
f.debconfAddDatabaseFrom(dlg.FileName())
}
}
func (f *TMainForm) OnMnuFilePebbleMemoryClick(sender vcl.IObject) {
f.pebbleAddDatabaseFromMemory()
}
func (f *TMainForm) OnMnuFileSqliteMemoryClick(sender vcl.IObject) { func (f *TMainForm) OnMnuFileSqliteMemoryClick(sender vcl.IObject) {
f.sqliteAddDatabaseFromFile(`:memory:`) cliDriver := f.SQLiteUseCliDriver.Checked()
f.sqliteAddDatabaseFromFile(`:memory:`, cliDriver)
}
func (f *TMainForm) OnMnuFileRedisConnectClick(sender vcl.IObject) {
var child *TRedisConnectionDialog
vcl.Application.CreateForm(&child)
defer child.Free()
var ret TRedisConnectionDialogResult
child.CustomReturn = &ret
mr := child.ShowModal() // Fake blocking
if mr != types.MrOk || ret == nil {
return // Cancelled
}
// Connect
f.redisConnect(ret)
} }
func (f *TMainForm) OnMnuFileExitClick(sender vcl.IObject) { func (f *TMainForm) OnMnuFileExitClick(sender vcl.IObject) {
f.Close() f.Close()
} }
func (f *TMainForm) OnMnuHelpHomepage(sender vcl.IObject) {
err := browser.OpenURL(HOMEPAGE_URL)
if err != nil {
vcl.ShowMessage("Opening browser: " + err.Error())
}
}
func (f *TMainForm) OnMenuHelpVersion(sender vcl.IObject) {
bi, ok := debug.ReadBuildInfo()
if !ok {
return
}
info := "This version of " + APPNAME + " was compiled with:\n\n"
for _, dep := range bi.Deps {
// Filter to only interesting things
switch dep.Path {
case `github.com/cockroachdb/pebble`,
`github.com/dgraph-io/badger/v4`,
`github.com/mattn/go-sqlite3`,
`github.com/redis/go-redis/v9`,
`go.etcd.io/bbolt`,
`modernc.org/sqlite`:
info += fmt.Sprintf("- %s %s\n", dep.Path, dep.Version)
}
}
vcl.ShowMessage(info)
}
func (f *TMainForm) OnNavContextPopup(sender vcl.IObject, mousePos types.TPoint, handled *bool) {
*handled = true
curItem := f.Buckets.Selected()
if curItem == nil {
// Nothing is selected at all
return
}
ndata := (*navData)(curItem.Data())
mnu := vcl.NewPopupMenu(f.Buckets)
mnu.SetImages(f.ImageList)
mnuRefresh := vcl.NewMenuItem(mnu)
mnuRefresh.SetCaption("Refresh")
mnuRefresh.SetImageIndex(imgArrowRefresh)
mnuRefresh.SetOnClick(func(sender vcl.IObject) { f.OnNavContextRefresh(curItem, ndata) })
mnu.Items().Add(mnuRefresh)
// Check what custom actions the ndata->db itself wants to add
actions, err := ndata.ld.NavContext(ndata)
if err != nil {
vcl.ShowMessage(err.Error())
return
}
if len(actions) > 0 {
mnuSep := vcl.NewMenuItem(mnu)
mnuSep.SetCaption("-")
mnu.Items().Add(mnuSep)
for _, action := range actions {
mnuAction := vcl.NewMenuItem(mnu)
mnuAction.SetCaption(action.Name)
cb := action.Callback // Copy to avoid reuse of loop variable
mnuAction.SetOnClick(func(sender vcl.IObject) {
err = cb(f, ndata)
if err != nil {
vcl.ShowMessage(err.Error())
}
f.OnNavContextRefresh(curItem, ndata)
})
mnu.Items().Add(mnuAction)
}
}
if curItem.Parent() == nil {
// Top-level item (database connection). Allow closing by right-click.
mnuSep := vcl.NewMenuItem(mnu)
mnuSep.SetCaption("-")
mnu.Items().Add(mnuSep)
mnuClose := vcl.NewMenuItem(mnu)
mnuClose.SetCaption("Close")
mnuClose.SetOnClick(f.OnNavContextClose)
mnuClose.SetImageIndex(imgDatabaseDelete)
mnu.Items().Add(mnuClose)
}
// Show popup
mnu.Popup2()
}
func (f *TMainForm) RefreshCurrentItem() {
item := f.Buckets.Selected()
if item == nil {
return // nothing to do
}
ndata := (*navData)(item.Data())
f.OnNavContextRefresh(item, ndata) // Refresh LHS pane/children
f.OnNavChange(f.Buckets, item) // Refresh RHS pane/data content
}
func (f *TMainForm) OnNavContextRefresh(item *vcl.TTreeNode, ndata *navData) {
isExpanded := item.Expanded()
// Reset nav node to 'unloaded' state
item.Collapse(true)
item.DeleteChildren()
item.SetHasChildren(true)
ndata.childrenLoaded = false
// Trigger a virtual reload
item.Expand(false) // Calls OnNavExpanding to dynamically detect children
// Restore previous gui state
item.SetExpanded(isExpanded)
}
func (f *TMainForm) OnDataPrepareCanvas(sender vcl.IObject, aCol, aRow int32, aState types.TGridDrawState) {
if _, ok := f.deleteRows[aRow]; ok {
f.contentBox.Canvas().Brush().SetColor(CO_DELETE)
return
}
if _, ok := f.insertRows[aRow]; ok {
f.contentBox.Canvas().Brush().SetColor(CO_INSERT)
return
}
if er, ok := f.updateRows[aRow]; ok {
// This row is being edited
if int32slice_contains(er, aCol) {
f.contentBox.Canvas().Brush().SetColor(CO_EDIT_EXPLICIT)
} else {
f.contentBox.Canvas().Brush().SetColor(CO_EDIT_IMPLICIT)
}
return
}
}
// func (f *TMainForm) OnDataCellEditStarting(sender vcl.IObject, aCol, aRow int32, editor **vcl.TWinControl) {
func (f *TMainForm) OnDataCellEditStarting(sender vcl.IObject, aCol, aRow int32, value *string) {
f.isEditing = true
}
func (f *TMainForm) OnDataCellEdited(sender vcl.IObject) {
// The OnEditingDone event fires whenever the TStringGrid loses focus, even
// if editing was not currently taking place
// To detect real edits, set a flag in the OnSelectEditor event
if !f.isEditing {
return
}
f.isEditing = false
aRow := f.contentBox.Row()
aCol := f.contentBox.Col()
// If this is an insert row, no need to patch updateRows
if _, ok := f.insertRows[aRow]; ok {
return // nothing to do
}
if chk, ok := f.updateRows[aRow]; ok {
if int32slice_contains(chk, aCol) {
// nothing to do
} else {
chk = append(chk, aCol)
f.updateRows[aRow] = chk
}
} else {
f.updateRows[aRow] = []int32{aCol}
}
// If this row was marked for deletion, this new event takes priority
delete(f.deleteRows, aRow)
// Signal repaint
f.contentBox.InvalidateRow(aRow)
}
func (f *TMainForm) OnDataInsertClick(sender vcl.IObject) {
if !f.contentBox.Enabled() {
return // Not an active data view
}
rpos := f.contentBox.RowCount()
f.contentBox.SetRowCount(rpos + 1)
f.insertRows[rpos] = struct{}{}
// Scroll to bottom
f.contentBox.SetTopRow(rpos)
}
func (f *TMainForm) OnDataDeleteRowClick(sender vcl.IObject) {
if !f.contentBox.Enabled() {
return // Not an active data view
}
rpos := f.contentBox.Row()
f.deleteRows[rpos] = struct{}{}
// If this row was marked for edit, this takes priority
delete(f.updateRows, rpos)
// Repaint
f.contentBox.InvalidateRow(rpos)
}
func (f *TMainForm) OnDataCommitClick(sender vcl.IObject) {
if !f.contentBox.Enabled() {
return // Not an active data view
}
node := f.Buckets.Selected()
if node == nil {
vcl.ShowMessage("No database selected")
return
}
scrollPos := f.contentBox.TopRow()
ndata := (*navData)(node.Data())
editableLd, ok := ndata.ld.(editableLoadedDatabase)
if !ok {
vcl.ShowMessage("Unsupported action for this database")
return
}
err := editableLd.ApplyChanges(f, ndata)
if err != nil {
vcl.ShowMessage(err.Error())
}
// Refresh content
f.OnNavChange(f.Buckets, node) // Refresh RHS pane/data content
// Preserve scroll position
f.contentBox.SetTopRow(scrollPos)
}
func (f *TMainForm) OnNavContextClose(sender vcl.IObject) {
curItem := f.Buckets.Selected()
if curItem == nil {
return // Nothing selected (shouldn't happen)
}
if curItem.Parent() != nil {
return // Selection is not top-level DB connection (shouldn't happen)
}
ndata := (*navData)(curItem.Data())
ndata.ld.Close()
curItem.Delete()
// n.b. This triggers OnNavChange, which will then re-render from noLoadedDatabase{}
}
// func (f *TMainForm) OnQueryTextChanged(sender vcl.IObject) {
func (f *TMainForm) OnQueryTextChanged(sender vcl.IObject, key *types.Char, shift types.TShiftState) {
// FIXME changing the text colour calls the onchange handler recursively
// FIXME changing the text colour pushes into the undo stack
// Preserve
f.queryInput.Lines().BeginUpdate()
origPos := f.queryInput.SelStart()
origLen := f.queryInput.SelLength()
defer func() {
f.queryInput.SetSelStart(origPos)
f.queryInput.SetSelLength(origLen)
f.queryInput.Lines().EndUpdate()
}()
tx := strings.ToLower(f.queryInput.Text())
// Reset all existing colors
f.queryInput.SetSelStart(0)
f.queryInput.SetSelLength(int32(len(tx)))
f.queryInput.SelAttributes().SetColor(colors.ClBlack)
searchPos := 0
for {
matchPos := strings.Index(tx[searchPos:], "select")
if matchPos == -1 {
break
}
matchPos += searchPos // compensate for slicing
f.queryInput.SetSelStart(int32(matchPos))
f.queryInput.SetSelLength(6)
f.queryInput.SelAttributes().SetColor(colors.ClRed)
searchPos = matchPos + 6 // strlen(SELECT)
}
}
func (f *TMainForm) OnQueryExecute(sender vcl.IObject) { func (f *TMainForm) OnQueryExecute(sender vcl.IObject) {
// If query tab is not selected, switch to it, but do not exec // If query tab is not selected, switch to it, but do not exec
if f.Tabs.ActivePageIndex() != 2 { if f.Tabs.ActivePageIndex() != 2 {
@@ -245,6 +755,15 @@ func (f *TMainForm) OnQueryExecute(sender vcl.IObject) {
return return
} }
queryString := f.queryInput.Text()
if f.queryInput.SelLength() > 0 {
queryString = f.queryInput.SelText() // Just the selected text
}
if strings.TrimSpace(queryString) == "" {
return // prevent blank query
}
// Execute // Execute
node := f.Buckets.Selected() node := f.Buckets.Selected()
if node == nil { if node == nil {
@@ -253,22 +772,60 @@ func (f *TMainForm) OnQueryExecute(sender vcl.IObject) {
} }
ndata := (*navData)(node.Data()) ndata := (*navData)(node.Data())
ndata.ld.ExecQuery(f.queryInput.Text(), f.queryResult)
queryableLd, ok := ndata.ld.(queryableLoadedDatabase)
if !ok {
vcl.ShowMessage("Unsupported action for this database")
return
}
err := queryableLd.ExecQuery(queryString, f.queryResult)
if err != nil {
vcl.ShowMessage(err.Error())
return
}
} }
func (f *TMainForm) OnNavChange(sender vcl.IObject, node *vcl.TTreeNode) { func (f *TMainForm) OnNavChange(sender vcl.IObject, node *vcl.TTreeNode) {
if node.Data() == nil { var ld loadedDatabase = f.none
vcl.ShowMessage("unexpected nil data") var ndata *navData = nil
return
if node != nil && node.Data() != nil {
ndata = (*navData)(node.Data())
ld = ndata.ld
} }
ndata := (*navData)(node.Data()) // Reset some controls that the render function is expected to populate
f.insertRows = make(map[int32]struct{})
f.updateRows = make(map[int32][]int32)
f.deleteRows = make(map[int32]struct{})
f.isEditing = false
f.propertiesBox.Clear()
vcl_stringgrid_clear(f.contentBox)
ndata.ld.RenderForNav(f, ndata) // Handover to the database type's own renderer function err := ld.RenderForNav(f, ndata) // Handover to the database type's own renderer function
if err != nil {
vcl.ShowMessage(err.Error())
// Ensure elements are disabled
f.contentBox.SetEnabled(false)
}
// Toggle the Edit functionality
_, ok := ld.(editableLoadedDatabase)
f.dataCommitBtn.SetEnabled(ok)
f.dataDelRowBtn.SetEnabled(ok)
f.dataInsertBtn.SetEnabled(ok)
// Toggle the Query functionality
_, ok = ld.(queryableLoadedDatabase)
f.queryInput.SetEnabled(ok)
f.queryResult.SetEnabled(ok)
f.queryExecBtn.SetEnabled(ok)
// We're in charge of common status bar text updates // We're in charge of common status bar text updates
f.StatusBar.SetSimpleText(ndata.ld.DisplayName() + " | " + ndata.ld.DriverName()) f.StatusBar.SetSimpleText(ld.DisplayName() + " | " + ld.DriverName())
} }
func (f *TMainForm) OnNavExpanding(sender vcl.IObject, node *vcl.TTreeNode, allowExpansion *bool) { func (f *TMainForm) OnNavExpanding(sender vcl.IObject, node *vcl.TTreeNode, allowExpansion *bool) {
@@ -281,25 +838,67 @@ func (f *TMainForm) OnNavExpanding(sender vcl.IObject, node *vcl.TTreeNode, allo
ndata := (*navData)(node.Data()) ndata := (*navData)(node.Data())
if ndata.childrenLoaded { err := f.NavLoadChildren(node, ndata)
if err != nil {
if errors.Is(err, ErrNavNotExist) {
// The nav entry has been deleted.
// This is a normal thing to happen when e.g. deleting a table
// f.StatusBar.SetSimpleText(err.Error()) // Just gets overridden when the selection changes
node.Delete()
return return
} }
vcl.ShowMessage(err.Error())
*allowExpansion = false // Permanently block
return
}
*allowExpansion = node.HasChildren()
// While we're here - preload one single level deep (not any deeper)
if node.HasChildren() {
// This node has children that we haven't processed. Process them now
cc := node.GetFirstChild()
for {
ndataChild := (*navData)(cc.Data())
if ndataChild.childrenLoaded {
break // We always do them together, so if one's done, no need to keep looking
}
f.NavLoadChildren(cc, ndataChild)
cc = cc.GetNextSibling()
if cc == nil {
break
}
}
}
}
func (f *TMainForm) NavLoadChildren(node *vcl.TTreeNode, ndata *navData) error {
if ndata.childrenLoaded {
return nil // Nothing to do
}
// Find the child buckets from this point under the element // Find the child buckets from this point under the element
nextBucketNames, err := ndata.ld.NavChildren(ndata) nextBucketNames, err := ndata.ld.NavChildren(ndata)
if err != nil { if err != nil {
vcl.ShowMessage(fmt.Sprintf("Failed to find child buckets under %q: %s", strings.Join(ndata.bucketPath, `/`), err.Error())) return fmt.Errorf("Failed to find child buckets under %q: %w", strings.Join(ndata.bucketPath, `/`), err)
*allowExpansion = false
return
} }
ndata.childrenLoaded = true // don't repeat this work ndata.childrenLoaded = true // don't repeat this work
if len(nextBucketNames) == 0 { if len(nextBucketNames) == 0 {
node.SetHasChildren(false) node.SetHasChildren(false)
*allowExpansion = false
} else { } else {
node.SetHasChildren(true)
// Populate LCL child nodes // Populate LCL child nodes
for _, bucketName := range nextBucketNames { for _, bucketName := range nextBucketNames {
@@ -319,7 +918,7 @@ func (f *TMainForm) OnNavExpanding(sender vcl.IObject, node *vcl.TTreeNode, allo
ndata.ld.Keepalive(navData) ndata.ld.Keepalive(navData)
} }
}
*allowExpansion = true return nil
}
} }

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
)

130
redisConnectionDialog.go Normal file
View File

@@ -0,0 +1,130 @@
package main
import (
"fmt"
"github.com/redis/go-redis/v9"
"github.com/ying32/govcl/vcl"
"github.com/ying32/govcl/vcl/types"
)
type TRedisConnectionDialogResult = *redis.Options
type TRedisConnectionDialog struct {
*vcl.TForm
Address *vcl.TEdit
Port *vcl.TSpinEdit
Password *vcl.TEdit
IsResp3Protocol *vcl.TCheckBox
ConnectBtn *vcl.TButton
// CustomReturn must be set by the caller to a *redis.Options variable.
CustomReturn *TRedisConnectionDialogResult // nil = no CustomReturn/cancelled dialog
}
func (f *TRedisConnectionDialog) OnFormCreate(sender vcl.IObject) {
f.SetCaption("Connect to Redis...")
f.SetWidth(320)
f.SetHeight(160)
f.SetPosition(types.PoOwnerFormCenter)
// row 1
row1 := vcl_row(f, 1)
lblAddress := vcl.NewLabel(row1)
lblAddress.SetParent(row1)
lblAddress.SetCaption("Address:")
lblAddress.SetAlign(types.AlLeft)
lblAddress.SetLayout(types.TlCenter)
lblAddress.SetLeft(1)
lblAddress.BorderSpacing().SetAround(MY_SPACING)
f.Address = vcl.NewEdit(row1)
f.Address.SetParent(row1)
f.Address.SetAlign(types.AlClient) // grow AlLeft)
f.Address.SetWidth(MY_WIDTH)
f.Address.SetLeft(2)
f.Address.SetText("127.0.0.1")
f.Address.BorderSpacing().SetAround(MY_SPACING)
lblPort := vcl.NewLabel(row1)
lblPort.SetParent(row1)
lblPort.SetCaption(":")
lblPort.SetAlign(types.AlRight)
lblPort.SetLayout(types.TlCenter)
lblPort.SetLeft(0)
lblPort.SetAutoSize(true)
lblPort.BorderSpacing().SetAround(MY_SPACING)
f.Port = vcl.NewSpinEdit(row1)
f.Port.SetParent(row1)
f.Port.SetMinValue(1)
f.Port.SetMaxValue(65535)
f.Port.SetValue(6379) // Redis default port
f.Port.SetAlign(types.AlRight)
f.Port.SetLeft(1000)
f.Port.BorderSpacing().SetAround(MY_SPACING)
// row 2
row2 := vcl_row(f, 2)
lblPassword := vcl.NewLabel(row2)
lblPassword.SetParent(row2)
lblPassword.SetCaption("Password:")
lblPassword.SetAlign(types.AlLeft)
lblPassword.SetLayout(types.TlCenter)
lblPassword.SetLeft(1)
lblPassword.BorderSpacing().SetAround(MY_SPACING)
f.Password = vcl.NewEdit(row2)
f.Password.SetParent(row2)
f.Password.SetAlign(types.AlClient) // grow
f.Password.SetLeft(2)
f.Password.SetWidth(MY_WIDTH)
f.Password.SetPasswordChar(uint16('*'))
f.Password.BorderSpacing().SetAround(MY_SPACING)
// row 3
f.IsResp3Protocol = vcl.NewCheckBox(f)
f.IsResp3Protocol.SetParent(f)
f.IsResp3Protocol.SetCaption("Use RESP v3 protocol")
f.IsResp3Protocol.SetChecked(true)
f.IsResp3Protocol.SetAlign(types.AlTop)
f.IsResp3Protocol.SetTop(3)
f.IsResp3Protocol.BorderSpacing().SetAround(MY_SPACING)
// buttons
row4 := vcl_row(f, 4)
row4.SetAlign(types.AlBottom)
f.ConnectBtn = vcl.NewButton(row4)
f.ConnectBtn.SetParent(row4)
f.ConnectBtn.SetAlign(types.AlRight)
f.ConnectBtn.SetCaption("Connect")
f.ConnectBtn.SetOnClick(f.OnConnectBtnClick)
}
func (f *TRedisConnectionDialog) OnConnectBtnClick(sender vcl.IObject) {
ret := &redis.Options{
Addr: fmt.Sprintf("%s:%d", f.Address.Text(), f.Port.Value()),
Password: f.Password.Text(),
DB: 0, // Start off with default DB, but, we will browse them ourselves
Protocol: 2, // RESP2
}
if f.IsResp3Protocol.Checked() {
ret.Protocol = 3 // RESP3
}
// Success.
// Fill in target pointer and close dialog with "MrOk" sentinel
*f.CustomReturn = ret
f.SetModalResult(types.MrOk)
}

266
sqlite.go
View File

@@ -1,266 +0,0 @@
package main
import (
"database/sql"
"fmt"
"path/filepath"
"unsafe"
"github.com/ying32/govcl/vcl"
"github.com/ying32/govcl/vcl/types"
)
const (
sqliteTablesCaption = "Tables"
)
type sqliteLoadedDatabase struct {
displayName string
path string
db *sql.DB
nav *vcl.TTreeNode
arena []*navData // keepalive
}
func (ld *sqliteLoadedDatabase) DisplayName() string {
return ld.displayName
}
func (ld *sqliteLoadedDatabase) RootElement() *vcl.TTreeNode {
return ld.nav
}
func (ld *sqliteLoadedDatabase) Keepalive(ndata *navData) {
ld.arena = append(ld.arena, ndata)
}
func (ld *sqliteLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) {
if len(ndata.bucketPath) == 0 {
// Top-level
f.propertiesBox.SetText("Please select...")
f.contentBox.SetEnabled(false)
f.contentBox.Clear()
} else if len(ndata.bucketPath) == 1 {
// Category (tables, ...)
f.propertiesBox.SetText("Please select...")
f.contentBox.SetEnabled(false)
f.contentBox.Clear()
} else if len(ndata.bucketPath) == 2 && ndata.bucketPath[0] == sqliteTablesCaption {
// Render for specific table
tableName := ndata.bucketPath[1]
// Get some basic properties
r := ld.db.QueryRow(`SELECT sql FROM sqlite_schema WHERE name = ?;`, tableName)
var schemaStmt string
err := r.Scan(&schemaStmt)
if err != nil {
schemaStmt = fmt.Sprintf("* Failed to describe table %q: %s", tableName, err.Error())
}
// Display table properties
f.propertiesBox.SetText(fmt.Sprintf("Selected table %q\n\nSchema:\n\n%s", tableName, schemaStmt))
f.contentBox.SetEnabled(false)
f.contentBox.Clear()
// Load column details
// 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 {
vcl.ShowMessageFmt("Failed to load columns for table %q: %s", tableName, err.Error())
return
}
populateColumns(columnNames, f.contentBox)
// Select count(*) so we know to display a warning if there are too many entries
// TODO
// Select * with small limit
datar, err := ld.db.Query(`SELECT * FROM ` + tableName + ` LIMIT 1000`) // WARNING can't prepare this parameter, but it comes from the DB (trusted)
if err != nil {
vcl.ShowMessageFmt("Failed to load data for table %q: %s", tableName, err.Error())
return
}
defer datar.Close()
populateRows(datar, f.contentBox)
// We successfully populated the data grid
f.contentBox.SetEnabled(true)
} else {
// ??? unknown
}
}
func (ld *sqliteLoadedDatabase) sqliteGetColumnNamesForTable(tableName string) ([]string, error) {
colr, err := ld.db.Query(`SELECT name FROM pragma_table_info(?)`, tableName)
if err != nil {
return nil, fmt.Errorf("Query: %w", err)
}
defer colr.Close()
var ret []string
for colr.Next() {
var columnName string
err = colr.Scan(&columnName)
if err != nil {
return nil, fmt.Errorf("Scan: %w", colr.Err())
}
ret = append(ret, columnName)
}
if colr.Err() != nil {
return nil, colr.Err()
}
return ret, nil
}
func populateColumns(names []string, dest *vcl.TListView) {
dest.Columns().Clear()
for _, columnName := range names {
col := dest.Columns().Add()
col.SetCaption(columnName)
col.SetWidth(MY_WIDTH)
col.SetAlignment(types.TaLeftJustify)
}
}
func populateRows(rr *sql.Rows, dest *vcl.TListView) {
numColumns := int(dest.Columns().Count())
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...)
if err != nil {
vcl.ShowMessageFmt("Failed to load data: %s", err.Error())
return
}
dataEntry := dest.Items().Add()
dataEntry.SetCaption(formatAny(fields[0]))
for i := 1; i < len(fields); i += 1 {
dataEntry.SubItems().Add(formatAny(fields[i]))
}
}
if rr.Err() != nil {
vcl.ShowMessageFmt("Failed to load data: %s", rr.Err().Error())
return
}
}
func (ld *sqliteLoadedDatabase) ExecQuery(query string, resultArea *vcl.TListView) {
rr, err := ld.db.Query(query)
if err != nil {
vcl.ShowMessage(err.Error())
return
}
defer rr.Close()
resultArea.SetEnabled(false)
resultArea.Clear()
columns, err := rr.Columns()
if err != nil {
vcl.ShowMessage(err.Error())
return
}
populateColumns(columns, resultArea)
populateRows(rr, resultArea)
resultArea.SetEnabled(true)
}
func (ld *sqliteLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
if len(ndata.bucketPath) == 0 {
// The top-level children are always:
return []string{sqliteTablesCaption}, nil
}
if len(ndata.bucketPath) == 1 && ndata.bucketPath[0] == sqliteTablesCaption {
rr, err := ld.db.Query(`SELECT name FROM sqlite_master WHERE type='table' ORDER BY name ASC;`)
if err != nil {
return nil, err
}
defer rr.Close()
var gather []string
for rr.Next() {
var tableName string
err = rr.Scan(&tableName)
if err != nil {
return nil, err
}
gather = append(gather, tableName)
}
if rr.Err() != nil {
return nil, rr.Err()
}
return gather, nil
}
if len(ndata.bucketPath) == 2 {
return nil, nil // Never any deeper children
}
return nil, fmt.Errorf("unknown nav path %#v", ndata.bucketPath)
}
var _ loadedDatabase = &sqliteLoadedDatabase{} // interface assertion
//
func (f *TMainForm) sqliteAddDatabaseFromFile(path string) {
// TODO load in background thread to stop blocking the UI
db, err := sql.Open("sqlite3", path)
if err != nil {
vcl.ShowMessage(fmt.Sprintf("Failed to load database '%s': %s", path, err.Error()))
return
}
ld := &sqliteLoadedDatabase{
path: path,
displayName: filepath.Base(path),
db: db,
}
ld.nav = f.Buckets.Items().Add(nil, ld.displayName)
ld.nav.SetImageIndex(imgDatabase)
ld.nav.SetSelectedIndex(imgDatabase)
ld.nav.SetHasChildren(true) // dynamically populate in OnNavExpanding
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)
ld.Keepalive(navData)
}

115
sqliteclidriver/eventcmd.go Normal file
View File

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

View File

@@ -0,0 +1,69 @@
package sqliteclidriver
import (
"errors"
"io"
"os/exec"
"sync"
"testing"
"github.com/stretchr/testify/require"
)
func TestEventCmd(t *testing.T) {
cmd := exec.Command("/bin/bash", "-c", `echo "hello world"`)
ch, _, err := ExecEvents(cmd)
if err != nil {
t.Fatal(err)
}
var consume []processEvent
for ev := range ch {
consume = append(consume, ev)
}
expect := []processEvent{
processEvent{evtype: evtypeStdout, data: []byte("hello world\n")},
processEvent{evtype: evtypeStdout, err: io.EOF},
processEvent{evtype: evtypeStderr, err: io.EOF},
processEvent{evtype: evtypeExit, err: nil},
}
require.EqualValues(t, expect, consume)
}
func TestEventCmdStdin(t *testing.T) {
cmd := exec.Command("/usr/bin/tr", "a-z", "A-Z")
ch, pw, err := ExecEvents(cmd)
if err != nil {
t.Fatal(err)
}
wg := sync.WaitGroup{}
wg.Add(1)
var consume []processEvent
go func() {
defer wg.Done()
for ev := range ch {
if ev.err != nil && errors.Is(ev.err, io.EOF) {
continue // skip flakey ordering of two EOF statements
}
consume = append(consume, ev)
}
}()
pw.Write([]byte("hello world"))
pw.Close()
wg.Wait()
expect := []processEvent{
processEvent{evtype: evtypeStdout, data: []byte("HELLO WORLD")},
processEvent{evtype: evtypeExit, err: nil},
}
require.EqualValues(t, expect, consume)
}

View File

@@ -0,0 +1,85 @@
package sqliteclidriver
import (
"bytes"
"encoding/json"
"fmt"
)
type Pair struct {
Key string
Value any
}
type OrderedKV []Pair
func (o *OrderedKV) UnmarshalJSON(data []byte) error {
// Rough estimate malloc size based on number of `:`
// This is a lower bound since there might be nested elements
var elCount int64
for _, c := range data {
if c == ':' {
elCount += 1
}
}
*o = make([]Pair, 0, elCount)
// Parse the initial opening { delimiter from the JSON stream
reader := bytes.NewReader(data)
dec := json.NewDecoder(reader)
tok, err := dec.Token()
if err != nil {
return fmt.Errorf("expected '{': %w", err)
}
if d, ok := tok.(json.Delim); !ok || (rune(d) != '{') {
return fmt.Errorf("expected '{', got %v", tok)
}
// Read remaining content
for dec.More() {
var p Pair
// Parse key: either string or Delim('}')
tok, err := dec.Token()
if err != nil {
return fmt.Errorf("expected '{': %w", err)
}
switch tok := tok.(type) {
case json.Delim:
if rune(tok) == '}' {
// Finished
break
}
// Something else
return fmt.Errorf("expected string or }, got %v", tok)
case string:
// Valid key
p.Key = tok
default:
return fmt.Errorf("expected string or }, got %v", tok)
}
// Parse value (any)
err = dec.Decode(&p.Value)
if err != nil {
return err
}
*o = append(*o, p)
}
// Assert that there is no remaining content
if reader.Len() != 0 {
return fmt.Errorf("Unexpected trailing data (%d bytes remaining)", reader.Len())
}
// Done
return nil
}

View File

@@ -0,0 +1,30 @@
package sqliteclidriver
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/require"
)
func TestOrderedKV(t *testing.T) {
input := `
{
"zzz": "foo",
"aaa": "bar"
}
`
var got OrderedKV
err := json.Unmarshal([]byte(input), &got)
if err != nil {
t.Fatal(err)
}
expect := OrderedKV{
Pair{Key: "zzz", Value: "foo"},
Pair{Key: "aaa", Value: "bar"},
}
require.EqualValues(t, expect, got)
}

View File

@@ -0,0 +1,378 @@
// sqliteclidriver is a database/sql driver for SQLite implemented on top of
// the sqlite3 command-line tool. This allows it to be used over remote SSH
// connections.
//
// Functionality is limited.
//
// Known caveats:
// - Bad error handling
// - Few supported types
// - Has to escape parameters for CLI instead of preparing them, so not safe for untrusted usage
// - No context handling
// - No way to configure sqlite3 command line path
package sqliteclidriver
import (
"bytes"
"context"
"database/sql"
"database/sql/driver"
"encoding/json"
"errors"
"fmt"
"io"
"os/exec"
"yvbolt/lexer"
"github.com/google/uuid"
)
var ErrNotSupported = errors.New("Not supported")
//
type SCDriver struct{}
func (d *SCDriver) Open(connectionString string) (driver.Conn, error) {
// TODO support custom binpath from our connection string
cmd := exec.Command(`/usr/bin/sqlite3`, `-noheader`, `-json`, connectionString) // n.b. doesn't support `--`
chEvents, pw, err := ExecEvents(cmd)
if err != nil {
return nil, err
}
return &SCConn{
listen: chEvents,
w: pw,
}, nil
}
var _ driver.Driver = &SCDriver{} // interface assertion
func init() {
sql.Register("sqliteclidriver", &SCDriver{})
}
//
type SCConnector struct {
connectionString string
driver *SCDriver
}
func (c *SCConnector) Connect(context.Context) (driver.Conn, error) {
return c.driver.Open(c.connectionString)
}
func (c *SCConnector) Driver() driver.Driver {
return c.driver
}
var _ driver.Connector = &SCConnector{} // interface assertion
//
type SCConn struct {
listen <-chan processEvent
w io.WriteCloser
}
func (c *SCConn) Prepare(query string) (driver.Stmt, error) {
f, err := lexer.Fields(query) // Rely on the query's escaping still being present
if err != nil {
return nil, err
}
if len(f) == 0 {
return nil, errors.New("Empty query")
}
if f[len(f)-1] != ";" {
f = append(f, ";") // Query must end in semicolon
}
return &SCStmt{
conn: c,
query: f,
}, nil
}
func (c *SCConn) Close() error {
return c.w.Close()
}
func (c *SCConn) Begin() (driver.Tx, error) {
return nil, ErrNotSupported
}
var _ driver.Conn = &SCConn{} // interface assertion
//
type SCStmt struct {
conn *SCConn
query []string
}
func (s *SCStmt) Close() error {
return nil
}
func (s *SCStmt) NumInput() int {
var ct int = 0
for _, token := range s.query {
if token == `?` {
ct++
}
}
return ct
}
func (s *SCStmt) buildQuery(args []driver.Value) ([]byte, error) {
// Embed query params
// WARNING: Not secure against injection? That's not a security boundary
// for the purposes of yvbolt, but maybe for other package users
var querybuilder bytes.Buffer
for _, token := range s.query {
if token == `?` {
if len(args) == 0 {
return nil, errors.New("Not enough arguments")
}
arg := args[0]
args = args[1:]
// Format the argument
if szarg, ok := arg.(string); ok {
querybuilder.WriteString(lexer.Quote(szarg))
} else if intarg, ok := arg.(int64); ok {
querybuilder.WriteString(fmt.Sprintf("%d", intarg))
} else if flarg, ok := arg.(float64); ok {
querybuilder.WriteString(fmt.Sprintf("%f", flarg))
} else {
return nil, fmt.Errorf("Parameter %v has unsupported type", arg)
}
} else {
// Normal token
querybuilder.WriteString(token)
}
// Whitespace
querybuilder.WriteByte(' ')
}
if len(args) != 0 {
return nil, errors.New("Too many arguments")
}
// Swap final whitespace for a \n
submit := querybuilder.Bytes()
submit[len(submit)-1] = '\n'
return submit, nil
}
func (s *SCStmt) Exec(args []driver.Value) (driver.Result, error) {
submit, err := s.buildQuery(args)
if err != nil {
return nil, err
}
_, err = io.CopyN(s.conn.w, bytes.NewReader(submit), int64(len(submit)))
if err != nil {
return nil, fmt.Errorf("Write: %w", err)
}
// Drain stdout
// TODO
// Drain stderr
// TODO
return &SCResult{}, nil
}
func (s *SCStmt) Query(args []driver.Value) (driver.Rows, error) {
ctx := context.Background()
submit, err := s.buildQuery(args)
if err != nil {
return nil, err
}
// If there are no results to the query, the sqlite3 -json mode does not
// print anything on stdout at all and we would hang forever
// Add a followup sentinel query that we can detect
const sentinelKey = `__sqliteclidriver_sentinel`
sentinelVal := uuid.Must(uuid.NewRandom()).String()
submit = append(submit, []byte(fmt.Sprintf("SELECT \"%s\" AS %s;\n", sentinelVal, sentinelKey))...)
//
_, err = io.CopyN(s.conn.w, bytes.NewReader(submit), int64(len(submit)))
if err != nil {
return nil, fmt.Errorf("Write: %w", err)
}
// Consume process events until either error or the json decoder is satisfied
pr, pw := io.Pipe()
listenContext, listenContextCancel := context.WithCancel(ctx) // Use to stop signalling once json decoder is satisfied
go func() {
defer pw.Close()
for {
select {
case msg, ok := <-s.conn.listen:
if !ok {
pw.CloseWithError(fmt.Errorf("process already closed"))
return
}
if msg.err != nil {
pw.CloseWithError(msg.err)
return
}
if msg.evtype == evtypeStdout {
_, err := io.CopyN(pw, bytes.NewReader(msg.data), int64(len(msg.data)))
if err != nil {
pw.CloseWithError(msg.err)
return
}
} else {
// Anything else (process event / stderr)
// Throw
pw.CloseWithError(msg)
return
}
case <-listenContext.Done():
return
}
}
}()
// We expect some kind of thing on stdout
// If something happens on stderr, or to the process, pr will read an error
ret := []OrderedKV{}
decoder := json.NewDecoder(pr)
err = decoder.Decode(&ret)
if err != nil {
return nil, err
}
// Check if this was the data or the sentinel
wasSentinel := false
if len(ret) > 0 && len(ret[0]) > 0 && ret[0][0].Key == sentinelKey {
if check, ok := ret[0][0].Value.(string); ok && check == sentinelVal {
// It was the sentinel
wasSentinel = true
// Nothing more to parse
}
}
if wasSentinel {
// There was no data.
// Wipe out `ret`
ret = nil
} else {
// There was data.
// Need to decode again (from the same decoder reader) until we find the sentinel
surplus := []map[string]any{}
err = decoder.Decode(&surplus)
if err != nil {
return nil, err
}
}
listenContextCancel()
// Drain stderr
// TODO
// Come up with a canonical ordering for the columns after json.NewDecoder
// wiped it out for us
// FIXME use a different json parsing library
var columnNames []string
if len(ret) > 0 {
for _, cell := range ret[0] {
columnNames = append(columnNames, cell.Key)
}
}
//
return &SCRows{
idx: 0,
columns: columnNames,
data: ret,
}, nil
}
var _ driver.Stmt = &SCStmt{} // interface assertion
//
type SCResult struct{}
func (r *SCResult) LastInsertId() (int64, error) {
return 0, ErrNotSupported
}
func (r *SCResult) RowsAffected() (int64, error) {
return 0, ErrNotSupported
}
var _ driver.Result = &SCResult{} // interface assertion
//
type SCRows struct {
idx int
columns []string
data []OrderedKV
}
func (r *SCRows) Columns() []string {
return r.columns
}
func (r *SCRows) Close() error {
r.idx = 0
r.data = nil
return nil
}
func (r *SCRows) Next(dest []driver.Value) error {
if r.idx >= len(r.data) {
return io.EOF
}
if len(dest) != len(r.data[r.idx]) {
return errors.New("Wrong number of arguments to Next()")
}
for i := 0; i < len(dest); i++ {
cell := r.data[r.idx][i]
dest[i] = cell.Value
}
r.idx++
return nil
}

View File

@@ -0,0 +1,78 @@
package sqliteclidriver
import (
"database/sql"
"testing"
"github.com/stretchr/testify/require"
)
func TestSqliteCliDriver(t *testing.T) {
db, err := sql.Open("sqliteclidriver", ":memory:")
require.NoError(t, err)
_, err = db.Exec(`CREATE TABLE my_test_table ( id INTEGER PRIMARY KEY, extra TEXT NOT NULL );`)
require.NoError(t, err)
_, err = db.Exec(`INSERT INTO my_test_table (id, extra) VALUES ( ? , ? ), ( ? , ? );`, 1337, "abcdef", 9001, "whoop")
require.NoError(t, err)
// Repeat this part to ensure we can make followup queries on the same connection
for i := 0; i < 3; i++ {
res, err := db.Query(`SELECT * FROM my_test_table ORDER BY id ASC;`)
require.NoError(t, err)
cols, err := res.Columns()
require.NoError(t, err)
require.EqualValues(t, cols, []string{"id", "extra"})
var rowCount int = 0
for res.Next() {
rowCount++
var idVal int
var extraVal string
err = res.Scan(&idVal, &extraVal)
if err != nil {
t.Fatal(err)
}
switch rowCount {
case 1:
require.EqualValues(t, 1337, idVal)
require.EqualValues(t, "abcdef", extraVal)
case 2:
require.EqualValues(t, 9001, idVal)
require.EqualValues(t, "whoop", extraVal)
}
}
require.Equal(t, rowCount, 2)
}
}
func TestSqliteCliDriverNoResults(t *testing.T) {
db, err := sql.Open("sqliteclidriver", ":memory:")
require.NoError(t, err)
// Repeat this part to ensure we can make followup queries on the same connection
for i := 0; i < 3; i++ {
_, err = db.Query(`SELECT 1 AS expect_no_result WHERE 1=2`)
require.NoError(t, err)
// Mix of results and no-results
rr := db.QueryRow(`SELECT 1 AS expect_result WHERE 1=1`)
require.NoError(t, rr.Err())
var result int64
err = rr.Scan(&result)
require.NoError(t, err)
require.EqualValues(t, result, 1)
}
}

21
util_types.go Normal file
View File

@@ -0,0 +1,21 @@
package main
// box_interface creates a slice where all elements of the input slice are boxed
// in an interface{} type.
func box_interface[T any](input []T) []interface{} {
ret := make([]interface{}, 0, len(input))
for _, v := range input {
ret = append(ret, v)
}
return ret
}
func int32slice_contains(haystack []int32, needle int32) bool {
for _, v := range haystack {
if v == needle {
return true
}
}
return false
}

123
util_vcl.go Normal file
View File

@@ -0,0 +1,123 @@
package main
import (
"runtime"
"github.com/ying32/govcl/vcl"
"github.com/ying32/govcl/vcl/types"
"github.com/ying32/govcl/vcl/types/colors"
)
const (
MY_SPACING = 6
MY_HEIGHT = 90
MY_WIDTH = 180
MAX_AUTO_COL_WIDTH = 240
)
// vcl_row makes a TPanel row inside the target component.
func vcl_row(owner vcl.IWinControl, top int32) *vcl.TPanel {
r := vcl.NewPanel(owner)
r.SetParent(owner)
r.SetBorderStyle(types.BsNone)
r.SetBevelInner(types.BvNone)
r.SetBevelOuter(types.BvNone)
r.SetAlign(types.AlTop)
r.SetTop(top)
r.SetAutoSize(true)
return r
}
// vcl_menuitem makes a child TMenuItem inside the parent menu.
func vcl_menuitem(parent *vcl.TMenuItem, caption string, imageIndex int32, onClick vcl.TNotifyEvent) *vcl.TMenuItem {
m := vcl.NewMenuItem(parent)
m.SetCaption(caption)
if imageIndex != -1 {
m.SetImageIndex(imageIndex)
}
if onClick != nil {
m.SetOnClick(onClick)
}
parent.Add(m)
return m
}
// vcl_menuseparator adds a separator to the parent TMenuItem.
func vcl_menuseparator(parent *vcl.TMenuItem) *vcl.TMenuItem {
s := vcl.NewMenuItem(parent)
s.SetCaption("-") // Creates separator
parent.Add(s)
return s
}
func vcl_default_tab_background() types.TColor {
if runtime.GOOS == "windows" {
// Assuming that uxtheme is loaded
// @ref https://stackoverflow.com/a/20332712
// return colors.ClBtnHighlight
// None of the colors.** constants seem to be quite right on a test
// Windows 11 machine - should be #f9f9f9
return 0x00f9f9f9
} else {
return colors.ClBtnFace // 0x00f0f0f0
}
}
func vcl_stringgrid_clear(d *vcl.TStringGrid) {
d.SetFixedCols(0) // No left-hand cols
d.SetRowCount(1) // The 0th row is the column headers
d.SetEnabled(false)
d.Columns().Clear()
d.Invalidate()
}
func vcl_stringgrid_columnwidths(d *vcl.TStringGrid) {
// Skip slow processing for very large result sets
if d.RowCount() > 1000 {
return
}
d.AutoAdjustColumns()
// AutoAdjustColumns will leave some columns massively too large by themselves
// Reign them back in
ct := d.Columns().Count()
for i := int32(0); i < ct; i++ {
if d.ColWidths(i) > MAX_AUTO_COL_WIDTH {
d.SetColWidths(i, MAX_AUTO_COL_WIDTH)
}
}
}
func vcl_confirm_dialog(sender vcl.IComponent, title string, message string) bool {
dlg := vcl.NewTaskDialog(sender)
dlg.SetCaption(APPNAME)
dlg.SetTitle(title)
dlg.SetText(message)
dlg.SetCommonButtons(types.NewSet())
yesBtn := dlg.Buttons().Add()
yesBtn.SetCaption("Confirm")
yesBtn.SetModalResult(types.MrYes)
noBtn := dlg.Buttons().Add()
noBtn.SetCaption("Cancel")
noBtn.SetModalResult(types.MrCancel)
ret := dlg.Execute()
if !ret {
return false // dialog closed
}
if dlg.ModalResult() != types.MrYes {
return false // other button clicked
}
return true // confirmed
}

25
versions.go Normal file
View File

@@ -0,0 +1,25 @@
package main
import (
"runtime/debug"
)
var (
version_Pebble string
)
func init() {
// ReadBuildInfo still works even with `-ldflags '-s -w'`.
mm, ok := debug.ReadBuildInfo()
if !ok {
panic("Missing build info")
}
for _, dep := range mm.Deps {
switch dep.Path {
case "github.com/cockroachdb/pebble":
version_Pebble = dep.Version
}
}
}