dbctl 0.7.3__tar.gz → 0.7.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. {dbctl-0.7.3 → dbctl-0.7.7}/CHANGELOG.md +125 -0
  2. {dbctl-0.7.3 → dbctl-0.7.7}/PKG-INFO +84 -12
  3. {dbctl-0.7.3 → dbctl-0.7.7}/README.md +81 -11
  4. {dbctl-0.7.3 → dbctl-0.7.7}/dbctl/cli.py +297 -2
  5. {dbctl-0.7.3 → dbctl-0.7.7}/dbctl/db.py +12 -4
  6. {dbctl-0.7.3 → dbctl-0.7.7}/dbctl/runtime.py +61 -11
  7. dbctl-0.7.7/dbctl/ui/__init__.py +8 -0
  8. dbctl-0.7.7/dbctl/ui/app.py +234 -0
  9. dbctl-0.7.7/dbctl/ui/connection_tree.py +539 -0
  10. dbctl-0.7.7/dbctl/ui/editor_tab.py +164 -0
  11. dbctl-0.7.7/dbctl/ui/grouping.py +63 -0
  12. dbctl-0.7.7/dbctl/ui/operation_tab.py +171 -0
  13. dbctl-0.7.7/dbctl/ui/registry.py +39 -0
  14. dbctl-0.7.7/dbctl/ui/results.py +29 -0
  15. dbctl-0.7.7/dbctl/ui/schema.py +89 -0
  16. dbctl-0.7.7/dbctl/ui/screens.py +146 -0
  17. dbctl-0.7.7/dbctl/ui/session.py +137 -0
  18. dbctl-0.7.7/dbctl/ui/splitter.py +109 -0
  19. dbctl-0.7.7/dbctl/ui/sql_templates.py +55 -0
  20. dbctl-0.7.7/dbctl/ui/tabs.py +87 -0
  21. dbctl-0.7.7/docs/execute.md +149 -0
  22. dbctl-0.7.7/docs/logo_small.png +0 -0
  23. dbctl-0.7.7/docs/tui.md +184 -0
  24. {dbctl-0.7.3 → dbctl-0.7.7}/pyproject.toml +14 -2
  25. dbctl-0.7.7/tests/test_execute.py +454 -0
  26. {dbctl-0.7.3 → dbctl-0.7.7}/tests/test_regressions.py +3 -3
  27. dbctl-0.7.7/tests/ui/conftest.py +76 -0
  28. dbctl-0.7.7/tests/ui/test_app.py +43 -0
  29. dbctl-0.7.7/tests/ui/test_connection_tree.py +140 -0
  30. dbctl-0.7.7/tests/ui/test_connection_tree_grouping.py +200 -0
  31. dbctl-0.7.7/tests/ui/test_editor_tab.py +99 -0
  32. dbctl-0.7.7/tests/ui/test_grouping.py +112 -0
  33. dbctl-0.7.7/tests/ui/test_operation_launcher.py +119 -0
  34. dbctl-0.7.7/tests/ui/test_operation_tab.py +126 -0
  35. dbctl-0.7.7/tests/ui/test_resize.py +79 -0
  36. dbctl-0.7.7/tests/ui/test_schema.py +50 -0
  37. dbctl-0.7.7/tests/ui/test_screens.py +115 -0
  38. dbctl-0.7.7/tests/ui/test_session.py +74 -0
  39. dbctl-0.7.7/tests/ui/test_sql_templates.py +89 -0
  40. dbctl-0.7.7/tests/ui/test_status_bar_and_loading.py +135 -0
  41. dbctl-0.7.7/tests/ui/test_tab_resize.py +139 -0
  42. dbctl-0.7.7/uv.lock +1196 -0
  43. dbctl-0.7.3/docs/logo_small.png +0 -0
  44. dbctl-0.7.3/uv.lock +0 -810
  45. {dbctl-0.7.3 → dbctl-0.7.7}/.dbctl/connections.yaml +0 -0
  46. {dbctl-0.7.3 → dbctl-0.7.7}/.dbctl/operations.yaml +0 -0
  47. {dbctl-0.7.3 → dbctl-0.7.7}/.github/workflows/ci.yml +0 -0
  48. {dbctl-0.7.3 → dbctl-0.7.7}/.github-local/ci.yml +0 -0
  49. {dbctl-0.7.3 → dbctl-0.7.7}/.gitignore +0 -0
  50. {dbctl-0.7.3 → dbctl-0.7.7}/Makefile +0 -0
  51. {dbctl-0.7.3 → dbctl-0.7.7}/dbctl/__init__.py +0 -0
  52. {dbctl-0.7.3 → dbctl-0.7.7}/dbctl/__main__.py +0 -0
  53. {dbctl-0.7.3 → dbctl-0.7.7}/dbctl/audit.py +0 -0
  54. {dbctl-0.7.3 → dbctl-0.7.7}/dbctl/config.py +0 -0
  55. {dbctl-0.7.3 → dbctl-0.7.7}/dbctl/connections.py +0 -0
  56. {dbctl-0.7.3 → dbctl-0.7.7}/dbctl/execute.py +0 -0
  57. {dbctl-0.7.3 → dbctl-0.7.7}/dbctl/init.py +0 -0
  58. {dbctl-0.7.3 → dbctl-0.7.7}/dbctl/multi.py +0 -0
  59. {dbctl-0.7.3 → dbctl-0.7.7}/dbctl/operations.py +0 -0
  60. {dbctl-0.7.3 → dbctl-0.7.7}/dbctl/refs.py +0 -0
  61. {dbctl-0.7.3 → dbctl-0.7.7}/dbctl/reports.py +0 -0
  62. {dbctl-0.7.3 → dbctl-0.7.7}/dbctl/tunnels/__init__.py +0 -0
  63. {dbctl-0.7.3 → dbctl-0.7.7}/dbctl/tunnels/azure.py +0 -0
  64. {dbctl-0.7.3 → dbctl-0.7.7}/dbctl/tunnels/base.py +0 -0
  65. {dbctl-0.7.3 → dbctl-0.7.7}/dbctl/tunnels/direct.py +0 -0
  66. {dbctl-0.7.3 → dbctl-0.7.7}/dbctl/tunnels/gcp.py +0 -0
  67. {dbctl-0.7.3 → dbctl-0.7.7}/dbctl/tunnels/k8s.py +0 -0
  68. {dbctl-0.7.3 → dbctl-0.7.7}/dbctl/tunnels/ssh.py +0 -0
  69. {dbctl-0.7.3 → dbctl-0.7.7}/dbctl/tunnels/ssm.py +0 -0
  70. {dbctl-0.7.3 → dbctl-0.7.7}/docker-compose.yml +0 -0
  71. {dbctl-0.7.3 → dbctl-0.7.7}/docs/ACTION_OUTPUT.md +0 -0
  72. {dbctl-0.7.3 → dbctl-0.7.7}/docs/DESIGN.md +0 -0
  73. {dbctl-0.7.3 → dbctl-0.7.7}/docs/SESSION_STATE.md +0 -0
  74. {dbctl-0.7.3 → dbctl-0.7.7}/docs/connections.md +0 -0
  75. {dbctl-0.7.3 → dbctl-0.7.7}/docs/logo.png +0 -0
  76. {dbctl-0.7.3 → dbctl-0.7.7}/docs/operations.md +0 -0
  77. {dbctl-0.7.3 → dbctl-0.7.7}/docs/tutorial.md +0 -0
  78. {dbctl-0.7.3 → dbctl-0.7.7}/seed/mssql.sql +0 -0
  79. {dbctl-0.7.3 → dbctl-0.7.7}/seed/mysql.sql +0 -0
  80. {dbctl-0.7.3 → dbctl-0.7.7}/seed/postgres.sql +0 -0
  81. {dbctl-0.7.3 → dbctl-0.7.7}/tests/test_azure_tunnel.py +0 -0
  82. {dbctl-0.7.3 → dbctl-0.7.7}/tests/test_bastion_tags.py +0 -0
  83. {dbctl-0.7.3 → dbctl-0.7.7}/tests/test_connections_loader.py +0 -0
  84. {dbctl-0.7.3 → dbctl-0.7.7}/tests/test_copy_features.py +0 -0
  85. {dbctl-0.7.3 → dbctl-0.7.7}/tests/test_gcp_tunnel.py +0 -0
  86. {dbctl-0.7.3 → dbctl-0.7.7}/tests/test_k8s_tunnel.py +0 -0
  87. {dbctl-0.7.3 → dbctl-0.7.7}/tests/test_refs.py +0 -0
  88. {dbctl-0.7.3 → dbctl-0.7.7}/tests/test_smoke.py +0 -0
  89. {dbctl-0.7.3 → dbctl-0.7.7}/tests/test_sso_cache.py +0 -0
@@ -5,6 +5,131 @@ All notable changes to this project will be documented here.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.7.7] — 2026-08-10
9
+
10
+ ### Fixed
11
+
12
+ - **`ruff format` failed in CI** on the v0.7.6 commit — three files
13
+ (`dbctl/cli.py`, `dbctl/runtime.py`, `tests/test_execute.py`) had
14
+ manually-typed signatures that fit on one line per `ruff format`'s
15
+ rules but were wrapped in the source. Reformatted to the canonical
16
+ layout (`def opened_engine(ctx: click.Context, canonical: str, conn:
17
+ Connection) -> Iterator[OpenedStub]:` on one line; same for the
18
+ `execute_cmd` callback signature and the inline-URL error `print`
19
+ call). Re-running `uv run ruff format --check dbctl tests` is now
20
+ clean and `make format` is a no-op on top of this commit.
21
+ - **Typo in `docs/execute.md`** — `# dduckdb inline URL` comment →
22
+ `# duckdb inline URL`.
23
+ - **README project layout** now lists `execute.md` under `docs/` so the
24
+ new reference page is discoverable from the layout block.
25
+
26
+ No behaviour change; re-running `dbctl execute --help` produces the
27
+ same output as v0.7.6.
28
+
29
+ ## [0.7.6] — 2026-08-10
30
+
31
+ ### Added
32
+
33
+ - **`dbctl execute`** — run ad-hoc SQL without declaring an operation in
34
+ `operations.yaml`. The new top-level verb takes a connection name *or* a
35
+ full SQLAlchemy URL, the SQL string as a single positional argument, and
36
+ the short option set the existing operation dispatch uses:
37
+
38
+ ```bash
39
+ dbctl execute -c pg -o json "SELECT * FROM users"
40
+ dbctl execute -c pg --show-sql --apply "DELETE FROM users WHERE name='bob'"
41
+ dbctl execute -c "postgresql+psycopg://u:p@host:5432/db" -o csv "SELECT 1"
42
+ dbctl execute -c pg -o yaml "SELECT * FROM users LIMIT 10"
43
+ ```
44
+
45
+ Behaviour:
46
+
47
+ - **`-c` / `--connection`** accepts a registered connection name /
48
+ alias (resolved via `connections.yaml`) **or** a full SQLAlchemy URL
49
+ (detected by the presence of `://`). Inline URLs build a transient
50
+ `direct` Connection so the SSM/SSH password-source plumbing, native-lib
51
+ install hints, and `connect_args` (connect-timeout, Windows SSO) all
52
+ apply uniformly.
53
+ - **`-o` / `--output`** chooses `table` (default) / `json` / `csv` /
54
+ `yaml` for SELECT-shaped results, exactly like declared `fetch` ops.
55
+ - The SQL is auto-classified by its first verb: `SELECT` / `WITH` /
56
+ `SHOW` / `EXPLAIN` / `DESCRIBE` / `PRAGMA` / `VALUES` / `TABLE` runs
57
+ as a query (rendered via `--output`); anything else runs as DML
58
+ (INSERT / UPDATE / DELETE / CREATE / DROP / ALTER / ...) inside
59
+ `engine.begin()`.
60
+ - DML respects the connection's `safety.confirm` (dry-run-by-default
61
+ unless `--apply`), `safety.read_only` (DML blocked with exit 6), and
62
+ prompts before commit unless `--yes` / `-y` is passed — exactly the
63
+ same gate as a declared `mode: execute` operation. Inline URLs
64
+ default to `safety.confirm: true`.
65
+ - DDL that returns `rowcount = -1` (CREATE TABLE, DROP, ...) is rendered
66
+ as `OK in <ms>ms` instead of the misleading `OK -1 row(s) affected`.
67
+ Real INSERT/UPDATE/DELETE row counts are still reported.
68
+ - Every run is appended to `~/.dbctl/history.jsonl` as
69
+ `operation="execute"` with the SQL (truncated to 500 chars) in the
70
+ `params.sql` field, so it shows up under `dbctl history list`. The
71
+ audit `connection` field is the canonical connection name, or the
72
+ literal `<inline>` token for inline URL runs.
73
+ - `--show-sql` prints the resolved SQL before executing; `--` separates
74
+ options from positional SQL that begins with a dash (Click would
75
+ otherwise treat the leading dash as an unknown option).
76
+ - The new verb is registered as a top-level static command alongside
77
+ `connections` / `operations` / `doctor` / `init` / `history` /
78
+ `tunnel` / `ui`, so shell completion picks it up.
79
+
80
+ This closes the long-standing "no ad-hoc query command" gap without
81
+ weakening the declarative operation registry: the declared
82
+ `operations.yaml` model remains the recommended way to make "what can
83
+ be run against this DB" discoverable from a versioned file, while
84
+ `execute` covers the exploratory / break-glass path the TUI already
85
+ serves — both share the same tunnel / safety / audit plumbing so every
86
+ run is still logged and policy-checked.
87
+
88
+ ## [0.7.4] — 2026-08-10
89
+
90
+ ### Added
91
+
92
+ - **`dbctl ui`** — an interactive [Textual](https://textual.textualize.io/)
93
+ TUI on top of the same connections/operations/tunnels/audit-log stack as
94
+ the CLI (`textual` is a base dependency, no separate install step). See
95
+ `docs/tui.md` for the full reference.
96
+ - **Connection tree** doubling as a lazily-loaded schema browser: `m`
97
+ toggles a simple `connection → table → column` view and a fuller
98
+ `connection → schema → Tables/Views → table/view → Columns/Indexes`
99
+ view; `g` toggles a flat list and a grouping that treats `-` as a path
100
+ separator (`in-gateway-ifp-dev` nests under `in-gateway` → `ifp`), with
101
+ single-child folders compressed and support for a name that's both a
102
+ group and a real connection at once (`ifp` / `ifp-gateway`). `c`/`d`/`t`
103
+ connect/disconnect/test-tunnel; `a`/`e` open `connections.yaml` in
104
+ `$EDITOR`.
105
+ - **Tabbed workspace**: a SQL editor (syntax-highlighted) or an
106
+ operation-launcher (a form built from a declared operation's
107
+ parameters) per tab, each with an independently resizable results
108
+ table and a status line (row count / rows-affected + duration) below
109
+ it. `Ctrl+N` new tab, `Ctrl+O` searchable operation launcher (type to
110
+ filter), `Ctrl+W` close, `Ctrl+R` run. Activating a table/view in the
111
+ tree opens a SQL tab pre-filled with a dialect-correct preview query
112
+ (`LIMIT` / `TOP` / `FETCH FIRST … ROWS ONLY`, with per-dialect
113
+ identifier quoting for mixed-case table/schema names).
114
+ - Connect/disconnect/test-tunnel and every SQL/operation run happen on a
115
+ background thread with a loading indicator, so the UI stays responsive
116
+ instead of freezing while a tunnel spins up or a query runs.
117
+ - Every run still respects `safety.read_only` / `safety.confirm` /
118
+ `allowed_operations` and is appended to `~/.dbctl/history.jsonl`,
119
+ exactly like a CLI-driven run.
120
+
121
+ ### Fixed
122
+
123
+ - **`uv.lock` could resolve against a contributor's local package-index
124
+ override instead of public PyPI** — a machine-level `uv` config pointing
125
+ at an internal package mirror silently became the index every regenerated
126
+ `uv.lock` entry was resolved and hashed against, which both leaked an
127
+ internal hostname into the lock file and broke `uv sync --frozen` for
128
+ anyone (CI included) without access to that mirror. Pinned
129
+ `[[tool.uv.index]]` in `pyproject.toml` with `default = true` so this
130
+ project always locks against `pypi.org`/`files.pythonhosted.org`
131
+ regardless of what index a contributor's environment defaults to.
132
+
8
133
  ## [0.7.3] — 2026-08-04
9
134
 
10
135
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dbctl
3
- Version: 0.7.3
3
+ Version: 0.7.7
4
4
  Summary: Generic CLI to monitor, control, and administer multiple databases via SSM, SSH, or direct connection.
5
5
  Author: dbctl contributors
6
6
  License: MIT
@@ -25,8 +25,10 @@ Requires-Dist: pyodbc>=5
25
25
  Requires-Dist: pyyaml>=6
26
26
  Requires-Dist: rich>=13
27
27
  Requires-Dist: sqlalchemy>=2
28
+ Requires-Dist: textual>=0.60
28
29
  Provides-Extra: dev
29
30
  Requires-Dist: mypy>=1.10; extra == 'dev'
31
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
30
32
  Requires-Dist: pytest-cov>=5; extra == 'dev'
31
33
  Requires-Dist: pytest>=8; extra == 'dev'
32
34
  Requires-Dist: ruff>=0.5; extra == 'dev'
@@ -71,11 +73,13 @@ leaving the terminal.
71
73
  | DML is one `Ctrl-Enter` away | DML is **dry-run by default** until `--apply`; `--yes` skips the prompt |
72
74
  | No cross-DB diff | `dbctl diff user-count prod-tenant1 int-tenant1` |
73
75
 
74
- `dbctl` is **not** a schema browser or a query playground — it deliberately
75
- has no ad-hoc query command. Declaring operations in YAML keeps "what can be
76
- run against this DB" discoverable from a versioned file instead of buried in
77
- your shell history. (For ad-hoc exploration open the tunnel with
78
- `dbctl tunnel open <conn>` and point your favourite client at the local bind.)
76
+ The declarative CLI above is still the primary interface: declaring
77
+ operations in YAML keeps "what can be run against this DB" discoverable from
78
+ a versioned file instead of buried in your shell history. For ad-hoc
79
+ exploration, `dbctl ui` (see below) gives you a schema browser and a SQL
80
+ editor without leaving `dbctl`'s tunnel/safety/audit model - or open the
81
+ tunnel with `dbctl tunnel open <conn>` and point your favourite client at
82
+ the local bind.
79
83
 
80
84
  ### How it reaches a database
81
85
 
@@ -118,9 +122,9 @@ the same params.
118
122
 
119
123
  ## Why "the operations are declarative"
120
124
 
121
- There is **no ad-hoc query command**. To run SQL through `dbctl` you must
122
- declare an operation in `operations.yaml` first — its name, its parameters
123
- (with types, descriptions, positional vs keyword), its SQL, and its mode
125
+ The recommended path for permanent, repeatable work is to declare an
126
+ operation in `operations.yaml` first — its name, its parameters (with
127
+ types, descriptions, positional vs keyword), its SQL, and its mode
124
128
  (`execute` / `fetch` / `fetch_one` / `script` / `upsert` for single-DB;
125
129
  `diff` / `compare` / `copy` / `sync` / `validate` / `replay` for multi-DB).
126
130
  The CLI then synthesises a Click subcommand per operation, so:
@@ -131,7 +135,18 @@ The CLI then synthesises a Click subcommand per operation, so:
131
135
  - the audit log is queryable by operation name (`dbctl history list`).
132
136
 
133
137
  This keeps "what can be run against this DB" discoverable from a versioned
134
- config file, instead of buried in shell history.
138
+ config file, instead of buried in shell history. For exploratory / break-glass
139
+ SQL — the path the TUI serves — `dbctl execute` (added in v0.7.6) runs
140
+ ad-hoc SQL through the same tunnel / safety / audit plumbing without
141
+ requiring a declared operation:
142
+
143
+ ```bash
144
+ dbctl execute -c pg -o json "SELECT * FROM users"
145
+ dbctl execute -c "postgresql+psycopg://u:p@host:5432/db" -o csv "SELECT 1"
146
+ dbctl execute -c pg --show-sql --apply "DELETE FROM users WHERE name='bob'"
147
+ ```
148
+
149
+ See [`docs/execute.md`](docs/execute.md) for the full reference.
135
150
 
136
151
  ## Install (uv)
137
152
 
@@ -184,6 +199,12 @@ dbctl pg add-user stephen 12 --show-sql # dry-run (prints SQL)
184
199
  dbctl pg add-user stephen 12 --apply --yes # commit (no prompt)
185
200
  dbctl pg increase-credits alice 10 --apply -y # +10% on alice's credits
186
201
 
202
+ # ad-hoc SQL without declaring an operation (since v0.7.6):
203
+ dbctl execute -c pg -o json "SELECT * FROM users"
204
+ dbctl execute -c "postgresql+psycopg://u:p@host:5432/db" -o csv "SELECT 1"
205
+ dbctl execute -c pg --show-sql --apply "DELETE FROM users WHERE name='bob'"
206
+ dbctl execute -c pg -o yaml -- "SELECT * FROM users WHERE name='-alice'" # SQL starting with a dash needs `--`
207
+
187
208
  # multi-DB modes (operation-first, preferred since v0.6):
188
209
  dbctl user-count pg my # multi-DB diff
189
210
  dbctl compare-credits pg my Daily
@@ -352,6 +373,41 @@ reference.
352
373
  Secret-typed **operation** parameters (`type: secret`) are redacted in the
353
374
  audit log regardless of which DB password source the connection uses.
354
375
 
376
+ ## Interactive UI (`dbctl ui`)
377
+
378
+ A [Textual](https://textual.textualize.io/) TUI for interactive work, on top
379
+ of the same connections/operations/tunnels/audit-log stack as the CLI - not
380
+ a separate code path with its own rules. `textual` is a base dependency, so
381
+ no separate install step.
382
+
383
+ ```bash
384
+ dbctl ui
385
+ ```
386
+
387
+ - **Left pane**: a connection tree that doubles as a lazily-loaded schema
388
+ browser (`m` toggles a simple `connection → table → column` view and a
389
+ fuller `connection → schema → Tables/Views → table/view →
390
+ Columns/Indexes` view). Expanding a connection connects it if needed;
391
+ activating (Enter) a table/view opens a SQL tab pre-filled with a
392
+ dialect-correct preview query using the real table name. `g` toggles a
393
+ grouped view that treats `-` as a path separator (`in-gateway-ifp-dev`
394
+ nests under `in-gateway` → `ifp`) - handy once a fleet has more than a
395
+ handful of connections.
396
+ - **Right pane**: a tabbed workspace of SQL editor / operation-launcher
397
+ tabs (`Ctrl+N` new tab, `Ctrl+O` searchable operation launcher, `Ctrl+W`
398
+ close, `Ctrl+R` run), each with an independently resizable results table
399
+ below it and a status line (row count / rows affected + duration) after
400
+ each run.
401
+ - Connecting, disconnecting, testing a tunnel, and every SQL/operation run
402
+ happen on a background thread with a loading indicator, so the UI stays
403
+ responsive instead of freezing while a tunnel spins up or a query runs.
404
+ - Every run still respects `safety.read_only` / `safety.confirm` /
405
+ `allowed_operations` and is appended to `~/.dbctl/history.jsonl`, exactly
406
+ like a CLI-driven run.
407
+
408
+ See [`docs/tui.md`](docs/tui.md) for the full keybinding reference and
409
+ per-dialect SQL details (row-limit clause, identifier quoting).
410
+
355
411
  ## Shell completion
356
412
 
357
413
  ```bash
@@ -375,6 +431,8 @@ dbctl/
375
431
  ├── docs/
376
432
  │ ├── connections.md # connections.yaml reference
377
433
  │ ├── operations.md # operations.yaml reference
434
+ │ ├── execute.md # dbctl execute (ad-hoc SQL) reference
435
+ │ ├── tui.md # dbctl ui reference (keybindings, dialect SQL)
378
436
  │ └── DESIGN.md # architecture and design decisions
379
437
  └── dbctl/
380
438
  ├── cli.py # dynamic groups + per-op Click commands
@@ -388,7 +446,21 @@ dbctl/
388
446
  ├── reports.py # rich tables / json / csv / yaml + diff + copy/sync/validate rendering
389
447
  ├── audit.py # history.jsonl
390
448
  ├── runtime.py # opened_conn() ctx-mgr (tunnel+engine+healthcheck)
391
- └── init.py # dbctl init wizard
449
+ ├── init.py # dbctl init wizard
450
+ └── ui/ # Textual TUI (`dbctl ui`)
451
+ ├── app.py # DbctlApp: connection tree + tabbed workspace
452
+ ├── session.py # per-connection tunnel+engine kept open across tab-runs
453
+ ├── connection_tree.py # lazy schema browser (simple/normal + flat/grouped views)
454
+ ├── grouping.py # "-" as a path separator -> compressed connection-name trie
455
+ ├── schema.py # sqlalchemy.inspect() wrapper for the tree
456
+ ├── editor_tab.py # SQL editor tab
457
+ ├── operation_tab.py # operation-launcher tab
458
+ ├── sql_templates.py # dialect-aware default SELECT + identifier quoting
459
+ ├── screens.py # confirm / new-tab / operation-launcher modal screens
460
+ ├── splitter.py # draggable pane-resize bars
461
+ ├── tabs.py # shared tab base: resize, toolbar, status bar, loading indicator
462
+ ├── results.py # DataTable rendering helpers
463
+ └── registry.py # tolerant connections/operations loading
392
464
  ```
393
465
 
394
466
  ## Development
@@ -397,7 +469,7 @@ dbctl/
397
469
  uv sync --extra dev
398
470
  make help # list all Makefile targets
399
471
  make check # lint + unit tests (the pre-commit gate)
400
- make test # unit tests (~80 tests, in-memory SQLite, no docker)
472
+ make test # unit tests (~270 tests, sqlite-backed, no docker)
401
473
  make typecheck # mypy strict (pre-existing debt; non-blocking)
402
474
  make smoke # docker compose up + dbctl doctor against the fleet
403
475
  make build # wheel + sdist via uv
@@ -37,11 +37,13 @@ leaving the terminal.
37
37
  | DML is one `Ctrl-Enter` away | DML is **dry-run by default** until `--apply`; `--yes` skips the prompt |
38
38
  | No cross-DB diff | `dbctl diff user-count prod-tenant1 int-tenant1` |
39
39
 
40
- `dbctl` is **not** a schema browser or a query playground — it deliberately
41
- has no ad-hoc query command. Declaring operations in YAML keeps "what can be
42
- run against this DB" discoverable from a versioned file instead of buried in
43
- your shell history. (For ad-hoc exploration open the tunnel with
44
- `dbctl tunnel open <conn>` and point your favourite client at the local bind.)
40
+ The declarative CLI above is still the primary interface: declaring
41
+ operations in YAML keeps "what can be run against this DB" discoverable from
42
+ a versioned file instead of buried in your shell history. For ad-hoc
43
+ exploration, `dbctl ui` (see below) gives you a schema browser and a SQL
44
+ editor without leaving `dbctl`'s tunnel/safety/audit model - or open the
45
+ tunnel with `dbctl tunnel open <conn>` and point your favourite client at
46
+ the local bind.
45
47
 
46
48
  ### How it reaches a database
47
49
 
@@ -84,9 +86,9 @@ the same params.
84
86
 
85
87
  ## Why "the operations are declarative"
86
88
 
87
- There is **no ad-hoc query command**. To run SQL through `dbctl` you must
88
- declare an operation in `operations.yaml` first — its name, its parameters
89
- (with types, descriptions, positional vs keyword), its SQL, and its mode
89
+ The recommended path for permanent, repeatable work is to declare an
90
+ operation in `operations.yaml` first — its name, its parameters (with
91
+ types, descriptions, positional vs keyword), its SQL, and its mode
90
92
  (`execute` / `fetch` / `fetch_one` / `script` / `upsert` for single-DB;
91
93
  `diff` / `compare` / `copy` / `sync` / `validate` / `replay` for multi-DB).
92
94
  The CLI then synthesises a Click subcommand per operation, so:
@@ -97,7 +99,18 @@ The CLI then synthesises a Click subcommand per operation, so:
97
99
  - the audit log is queryable by operation name (`dbctl history list`).
98
100
 
99
101
  This keeps "what can be run against this DB" discoverable from a versioned
100
- config file, instead of buried in shell history.
102
+ config file, instead of buried in shell history. For exploratory / break-glass
103
+ SQL — the path the TUI serves — `dbctl execute` (added in v0.7.6) runs
104
+ ad-hoc SQL through the same tunnel / safety / audit plumbing without
105
+ requiring a declared operation:
106
+
107
+ ```bash
108
+ dbctl execute -c pg -o json "SELECT * FROM users"
109
+ dbctl execute -c "postgresql+psycopg://u:p@host:5432/db" -o csv "SELECT 1"
110
+ dbctl execute -c pg --show-sql --apply "DELETE FROM users WHERE name='bob'"
111
+ ```
112
+
113
+ See [`docs/execute.md`](docs/execute.md) for the full reference.
101
114
 
102
115
  ## Install (uv)
103
116
 
@@ -150,6 +163,12 @@ dbctl pg add-user stephen 12 --show-sql # dry-run (prints SQL)
150
163
  dbctl pg add-user stephen 12 --apply --yes # commit (no prompt)
151
164
  dbctl pg increase-credits alice 10 --apply -y # +10% on alice's credits
152
165
 
166
+ # ad-hoc SQL without declaring an operation (since v0.7.6):
167
+ dbctl execute -c pg -o json "SELECT * FROM users"
168
+ dbctl execute -c "postgresql+psycopg://u:p@host:5432/db" -o csv "SELECT 1"
169
+ dbctl execute -c pg --show-sql --apply "DELETE FROM users WHERE name='bob'"
170
+ dbctl execute -c pg -o yaml -- "SELECT * FROM users WHERE name='-alice'" # SQL starting with a dash needs `--`
171
+
153
172
  # multi-DB modes (operation-first, preferred since v0.6):
154
173
  dbctl user-count pg my # multi-DB diff
155
174
  dbctl compare-credits pg my Daily
@@ -318,6 +337,41 @@ reference.
318
337
  Secret-typed **operation** parameters (`type: secret`) are redacted in the
319
338
  audit log regardless of which DB password source the connection uses.
320
339
 
340
+ ## Interactive UI (`dbctl ui`)
341
+
342
+ A [Textual](https://textual.textualize.io/) TUI for interactive work, on top
343
+ of the same connections/operations/tunnels/audit-log stack as the CLI - not
344
+ a separate code path with its own rules. `textual` is a base dependency, so
345
+ no separate install step.
346
+
347
+ ```bash
348
+ dbctl ui
349
+ ```
350
+
351
+ - **Left pane**: a connection tree that doubles as a lazily-loaded schema
352
+ browser (`m` toggles a simple `connection → table → column` view and a
353
+ fuller `connection → schema → Tables/Views → table/view →
354
+ Columns/Indexes` view). Expanding a connection connects it if needed;
355
+ activating (Enter) a table/view opens a SQL tab pre-filled with a
356
+ dialect-correct preview query using the real table name. `g` toggles a
357
+ grouped view that treats `-` as a path separator (`in-gateway-ifp-dev`
358
+ nests under `in-gateway` → `ifp`) - handy once a fleet has more than a
359
+ handful of connections.
360
+ - **Right pane**: a tabbed workspace of SQL editor / operation-launcher
361
+ tabs (`Ctrl+N` new tab, `Ctrl+O` searchable operation launcher, `Ctrl+W`
362
+ close, `Ctrl+R` run), each with an independently resizable results table
363
+ below it and a status line (row count / rows affected + duration) after
364
+ each run.
365
+ - Connecting, disconnecting, testing a tunnel, and every SQL/operation run
366
+ happen on a background thread with a loading indicator, so the UI stays
367
+ responsive instead of freezing while a tunnel spins up or a query runs.
368
+ - Every run still respects `safety.read_only` / `safety.confirm` /
369
+ `allowed_operations` and is appended to `~/.dbctl/history.jsonl`, exactly
370
+ like a CLI-driven run.
371
+
372
+ See [`docs/tui.md`](docs/tui.md) for the full keybinding reference and
373
+ per-dialect SQL details (row-limit clause, identifier quoting).
374
+
321
375
  ## Shell completion
322
376
 
323
377
  ```bash
@@ -341,6 +395,8 @@ dbctl/
341
395
  ├── docs/
342
396
  │ ├── connections.md # connections.yaml reference
343
397
  │ ├── operations.md # operations.yaml reference
398
+ │ ├── execute.md # dbctl execute (ad-hoc SQL) reference
399
+ │ ├── tui.md # dbctl ui reference (keybindings, dialect SQL)
344
400
  │ └── DESIGN.md # architecture and design decisions
345
401
  └── dbctl/
346
402
  ├── cli.py # dynamic groups + per-op Click commands
@@ -354,7 +410,21 @@ dbctl/
354
410
  ├── reports.py # rich tables / json / csv / yaml + diff + copy/sync/validate rendering
355
411
  ├── audit.py # history.jsonl
356
412
  ├── runtime.py # opened_conn() ctx-mgr (tunnel+engine+healthcheck)
357
- └── init.py # dbctl init wizard
413
+ ├── init.py # dbctl init wizard
414
+ └── ui/ # Textual TUI (`dbctl ui`)
415
+ ├── app.py # DbctlApp: connection tree + tabbed workspace
416
+ ├── session.py # per-connection tunnel+engine kept open across tab-runs
417
+ ├── connection_tree.py # lazy schema browser (simple/normal + flat/grouped views)
418
+ ├── grouping.py # "-" as a path separator -> compressed connection-name trie
419
+ ├── schema.py # sqlalchemy.inspect() wrapper for the tree
420
+ ├── editor_tab.py # SQL editor tab
421
+ ├── operation_tab.py # operation-launcher tab
422
+ ├── sql_templates.py # dialect-aware default SELECT + identifier quoting
423
+ ├── screens.py # confirm / new-tab / operation-launcher modal screens
424
+ ├── splitter.py # draggable pane-resize bars
425
+ ├── tabs.py # shared tab base: resize, toolbar, status bar, loading indicator
426
+ ├── results.py # DataTable rendering helpers
427
+ └── registry.py # tolerant connections/operations loading
358
428
  ```
359
429
 
360
430
  ## Development
@@ -363,7 +433,7 @@ dbctl/
363
433
  uv sync --extra dev
364
434
  make help # list all Makefile targets
365
435
  make check # lint + unit tests (the pre-commit gate)
366
- make test # unit tests (~80 tests, in-memory SQLite, no docker)
436
+ make test # unit tests (~270 tests, sqlite-backed, no docker)
367
437
  make typecheck # mypy strict (pre-existing debt; non-blocking)
368
438
  make smoke # docker compose up + dbctl doctor against the fleet
369
439
  make build # wheel + sdist via uv