@carllee1983/dbcli 1.53.0 → 1.54.1

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.
@@ -605,4 +605,4 @@ schema. Raw `query` / `export` invocations render a sortable table only.
605
605
  - Blacklisted tables and columns are redacted from query output.
606
606
  - `schema` reports `estimatedRowCount` and `sizeCategory` (small / medium / large / huge). For large/huge tables add `WHERE` or `LIMIT` — bands in [reference.md](../skills/dbcli/reference.md#schema).
607
607
  - `doctor` on `mongodb+srv://` reports whether SRV resolves natively or through the DoH fallback — useful when the runtime restricts DNS.
608
- - **Global flags:** `--version`, `--config <path>`, `--global`, `--use <name>`, `--timeout <ms>`, `-v` / `--verbose` / `-vv`, `-q` / `--quiet`, `--no-color` (also honours `NO_COLOR`). Root-level flags must precede the command unless the command explicitly declares a command-level option.
608
+ - **Global flags:** `--version`, `--config <path>`, `--global`, `--use <name>`, `--timeout <ms>`, `--statement-timeout <ms>`, `-v` / `--verbose` / `-vv`, `-q` / `--quiet`, `--no-color` (also honours `NO_COLOR`). Root-level flags must precede the command unless the command explicitly declares a command-level option.
@@ -83,13 +83,16 @@ command-level option is only valid after the command that declares it.
83
83
  | `--global` | Select the user-global registry at `~/.config/dbcli/config.json` instead of the current project's `.dbcli` config. Place it before the command path. |
84
84
  | `--use <connection>` | Select a named connection for this invocation; place it before the command path unless that command explicitly lists a command-level `--use`. |
85
85
  | `--timeout <ms>` | Connection timeout in milliseconds (integer, 100–600000), overriding the connection config's `timeout` field for this invocation. Applies to every engine adapter. Without either the flag or the config field, adapters fall back to their built-in 5000ms default. |
86
+ | `--statement-timeout <ms>` | How long a single statement may run, in milliseconds (integer, 0–3600000; `0` removes the limit), overriding the connection config's `statementTimeout` field. Independent of the connection timeout — raising it does not slow down detection of an unreachable host. Falls back to `--timeout` when unset, and to the server's own setting when neither is given. |
86
87
 
87
88
  `--timeout` is applied only when the adapter is constructed for this invocation — it is
88
89
  never written back to `config.json`. Set the connection's `timeout` field instead for a
89
- value that persists across runs. On PostgreSQL, the same value is also used as the
90
- session's `statement_timeout` (not just the connection timeout), so a low value can cut
91
- off a long-running query with an error that looks like a connection timeout; the 100ms
92
- floor exists specifically to keep that failure mode from being too easy to trigger.
90
+ value that persists across runs. `--timeout` caps statement time as well as connection time, so a
91
+ low value cuts off a long-running query with an error that reads like a connection
92
+ problem; the 100ms floor exists specifically to keep that failure mode from being too
93
+ easy to trigger. Use `--statement-timeout` when only the statement limit should change.
94
+ With neither flag, dbcli sets no statement limit at all — the server's setting decides,
95
+ so a query that runs longer than the connection timeout is no longer cut off.
93
96
  Elasticsearch applies its timeout per request rather than once for the whole connection.
94
97
  The `timeout` field itself always takes a literal number — unlike other connection
95
98
  fields, it does not accept an `{"$env": "..."}` reference.
@@ -220,8 +223,8 @@ dbcli list --include-system # Elasticsearch: include `.system` indices
220
223
  **Permission:** query-only+
221
224
 
222
225
  > **MongoDB:** Lists collections with estimated document count.
223
- > **Redis:** Returns up to 100 000 keys via `SCAN MATCH * COUNT 1000`. The header reads `Keys in db <n> (redis):` where `<n>` is the logical DB index.
224
- > **Elasticsearch:** Returns indices with `documentCount` from `/_stats/docs`; aliases are tagged separately. System indices (names starting with `.`) are hidden unless `--include-system` is passed.
226
+ > **Redis:** Samples the first 1 000 keys scanned via `SCAN MATCH * COUNT 1000`, applying the blacklist during the scan. When the keyspace is larger, table output adds a `Sampled the first 1000 keys scanned` line and JSON output carries `sampled: true` with `sampleLimit`. The header reads `Keys in db <n> (redis):` where `<n>` is the logical DB index.
227
+ > **Elasticsearch:** Returns indices with `documentCount` from one `/_cat/indices?h=index,docs.count&expand_wildcards=all` request (open and closed indices alike). The count is primaries-only, so a replicated index no longer reports its replica copies as extra documents. Aliases are tagged separately. System indices (names starting with `.`) are hidden unless `--include-system` is passed.
225
228
 
226
229
  ### schema
227
230
 
@@ -243,6 +246,13 @@ dbcli schema --use prod # Scan prod DB; saves to .dbcli/schemas/prod
243
246
  **Options:** `--format <table|json>`, `--refresh`, `--reset`, `--force`, `--use <connection>`, `--sample-size <n>` (mongo only), `--sample-method <random|natural>` (mongo only)
244
247
  **Permission:** query-only+
245
248
 
249
+ > **Row counts on a full scan:** scanning every table records
250
+ > `rowCountIsEstimate: true` and reports the engine's row *estimate* (`information_schema.TABLES.TABLE_ROWS` on MySQL/MariaDB,
251
+ > `pg_class.reltuples` on PostgreSQL) rather than running `COUNT(*)` per table —
252
+ > a hundred full-table counts is what made scanning a large database unusable.
253
+ > `dbcli schema <table>` on a single table still reports the exact count. Tables
254
+ > are scanned with bounded parallelism (4 at a time).
255
+
246
256
  **Schema storage (v1.4+):** Schema is persisted as layered files under `.dbcli/schemas/`. With v2 multi-connection config each connection gets its own subdirectory (`.dbcli/schemas/<connection>/`). Run `dbcli schema --use <connection>` once per connection before querying it — otherwise `schema <table>` may return data from the wrong connection's cache.
247
257
 
248
258
  > **PostgreSQL:** Introspection uses the exact `public` catalog identity throughout. Full catalog/schema/table joins prevent a reused constraint name from contaminating another table; enum lookup includes its namespace; composite primary-key order comes from the exact table OID and index ordinality; and row estimates are scoped to the exact `public` relation. Row-count SQL qualifies and quotes both `"public"` and the exact table identifier, escaping embedded quotes so mixed-case or punctuation-bearing names remain distinct and safe.
@@ -286,6 +296,12 @@ dbcli query "SELECT * FROM orders" --format html > orders.html # pipe to stdou
286
296
  **Options:** `--format <table|json|csv|html>`, `--ui` (open the dashboard in the system browser; implies `--format html`), `--limit <number>`, `--no-limit`, `--collection <name>` (MongoDB / Elasticsearch), `--index <name>` (Elasticsearch alias for `--collection`), `--fields <list>`, `--truncate <number>` / `--no-truncate`, `-f, --query-file <path>`, `--use <name[,name]>`, `--slow-ms <number>`, `--recovery`
287
297
  **Permission:** query-only+ (Redis: per-command; Elasticsearch: per HTTP method/path)
288
298
 
299
+ > **Server-side scripts are rejected on every path.** MongoDB `$where`,
300
+ > `$function`, and `$accumulator`, and Elasticsearch `script` / `script_fields`,
301
+ > execute code on the database server. The adapters reject them anywhere in a
302
+ > filter, pipeline, or DSL body — before the request is sent — so `query`, `q`,
303
+ > saved snippets, and DML planning all behave the same way.
304
+
289
305
  #### Passive slow-query hint (`--slow-ms`)
290
306
 
291
307
  `query` and `q` read the execution time they already measured for a finished
@@ -1644,6 +1660,21 @@ shape (`schemaVersion: 1`):
1644
1660
  Recovery codes (fixed in v1.15.0):
1645
1661
  - `CONFIG_MISSING` — no `.dbcli` config; run `dbcli init`.
1646
1662
  - `CONN_REFUSED` / `CONN_AUTH_FAILED` / `CONN_TIMEOUT` / `CONN_HOST_NOT_FOUND` / `CONN_UNKNOWN` — connection failure variants.
1663
+ `CONN_TIMEOUT` also covers a statement the server canceled for exceeding the statement
1664
+ timeout (PostgreSQL `57014`, MySQL `3024`, MariaDB `1969`); `details.connectionCode` is
1665
+ `STATEMENT_TIMEOUT` there instead of `ETIMEDOUT`, the plan targets the query (`lint` →
1666
+ `explain` → re-run with `--statement-timeout <ms>`) rather than `doctor`, and no
1667
+ `branches` / `branchFork` / `verify` is emitted. PostgreSQL `57014` only counts as a
1668
+ statement timeout when the server says so — a `pg_cancel_backend()` cancel keeps the
1669
+ verbatim `Database error (57014): …` form instead of claiming a ceiling.
1670
+ The same `details.connectionCode` field distinguishes the other causes that share a
1671
+ connection code: `CONNECTION_LOST` (server closed the connection mid-session — PostgreSQL
1672
+ class 08 and `57P01`/`57P02`, MySQL `1053`), `TOO_MANY_CONNECTIONS` (PostgreSQL `53300`,
1673
+ MySQL `1040` — its plan counts current connections instead of running `doctor`, because
1674
+ rewriting host/port cannot create a slot), `SERVER_NOT_READY` (`57P03`, still starting up),
1675
+ `CONNECTION_REJECTED` (`08004` — the server answered and refused), `EHOSTUNREACH` (resolved
1676
+ but unroutable) and `TLS_ERROR` (handshake failure; reported as `CONN_UNKNOWN` rather than
1677
+ `CONN_AUTH_FAILED`, whose plan re-runs `init` for credentials that are not the problem).
1647
1678
  - `PERMISSION_DENIED` — active permission level forbids the operation.
1648
1679
  - `BLACKLIST_TABLE` / `BLACKLIST_COLUMN_WRITE` — blacklist violations.
1649
1680
  - `SNIPPET_NOT_FOUND` / `SNIPPET_AMBIGUOUS` / `SNIPPET_PARAM_MISSING` — saved-query failures.
@@ -605,4 +605,4 @@ schema. Raw `query` / `export` invocations render a sortable table only.
605
605
  - Blacklisted tables and columns are redacted from query output.
606
606
  - `schema` reports `estimatedRowCount` and `sizeCategory` (small / medium / large / huge). For large/huge tables add `WHERE` or `LIMIT` — bands in [reference.md](reference.md#schema).
607
607
  - `doctor` on `mongodb+srv://` reports whether SRV resolves natively or through the DoH fallback — useful when the runtime restricts DNS.
608
- - **Global flags:** `--version`, `--config <path>`, `--global`, `--use <name>`, `--timeout <ms>`, `-v` / `--verbose` / `-vv`, `-q` / `--quiet`, `--no-color` (also honours `NO_COLOR`). Root-level flags must precede the command unless the command explicitly declares a command-level option.
608
+ - **Global flags:** `--version`, `--config <path>`, `--global`, `--use <name>`, `--timeout <ms>`, `--statement-timeout <ms>`, `-v` / `--verbose` / `-vv`, `-q` / `--quiet`, `--no-color` (also honours `NO_COLOR`). Root-level flags must precede the command unless the command explicitly declares a command-level option.
@@ -83,13 +83,16 @@ command-level option is only valid after the command that declares it.
83
83
  | `--global` | Select the user-global registry at `~/.config/dbcli/config.json` instead of the current project's `.dbcli` config. Place it before the command path. |
84
84
  | `--use <connection>` | Select a named connection for this invocation; place it before the command path unless that command explicitly lists a command-level `--use`. |
85
85
  | `--timeout <ms>` | Connection timeout in milliseconds (integer, 100–600000), overriding the connection config's `timeout` field for this invocation. Applies to every engine adapter. Without either the flag or the config field, adapters fall back to their built-in 5000ms default. |
86
+ | `--statement-timeout <ms>` | How long a single statement may run, in milliseconds (integer, 0–3600000; `0` removes the limit), overriding the connection config's `statementTimeout` field. Independent of the connection timeout — raising it does not slow down detection of an unreachable host. Falls back to `--timeout` when unset, and to the server's own setting when neither is given. |
86
87
 
87
88
  `--timeout` is applied only when the adapter is constructed for this invocation — it is
88
89
  never written back to `config.json`. Set the connection's `timeout` field instead for a
89
- value that persists across runs. On PostgreSQL, the same value is also used as the
90
- session's `statement_timeout` (not just the connection timeout), so a low value can cut
91
- off a long-running query with an error that looks like a connection timeout; the 100ms
92
- floor exists specifically to keep that failure mode from being too easy to trigger.
90
+ value that persists across runs. `--timeout` caps statement time as well as connection time, so a
91
+ low value cuts off a long-running query with an error that reads like a connection
92
+ problem; the 100ms floor exists specifically to keep that failure mode from being too
93
+ easy to trigger. Use `--statement-timeout` when only the statement limit should change.
94
+ With neither flag, dbcli sets no statement limit at all — the server's setting decides,
95
+ so a query that runs longer than the connection timeout is no longer cut off.
93
96
  Elasticsearch applies its timeout per request rather than once for the whole connection.
94
97
  The `timeout` field itself always takes a literal number — unlike other connection
95
98
  fields, it does not accept an `{"$env": "..."}` reference.
@@ -220,8 +223,8 @@ dbcli list --include-system # Elasticsearch: include `.system` indices
220
223
  **Permission:** query-only+
221
224
 
222
225
  > **MongoDB:** Lists collections with estimated document count.
223
- > **Redis:** Returns up to 100 000 keys via `SCAN MATCH * COUNT 1000`. The header reads `Keys in db <n> (redis):` where `<n>` is the logical DB index.
224
- > **Elasticsearch:** Returns indices with `documentCount` from `/_stats/docs`; aliases are tagged separately. System indices (names starting with `.`) are hidden unless `--include-system` is passed.
226
+ > **Redis:** Samples the first 1 000 keys scanned via `SCAN MATCH * COUNT 1000`, applying the blacklist during the scan. When the keyspace is larger, table output adds a `Sampled the first 1000 keys scanned` line and JSON output carries `sampled: true` with `sampleLimit`. The header reads `Keys in db <n> (redis):` where `<n>` is the logical DB index.
227
+ > **Elasticsearch:** Returns indices with `documentCount` from one `/_cat/indices?h=index,docs.count&expand_wildcards=all` request (open and closed indices alike). The count is primaries-only, so a replicated index no longer reports its replica copies as extra documents. Aliases are tagged separately. System indices (names starting with `.`) are hidden unless `--include-system` is passed.
225
228
 
226
229
  ### schema
227
230
 
@@ -243,6 +246,13 @@ dbcli schema --use prod # Scan prod DB; saves to .dbcli/schemas/prod
243
246
  **Options:** `--format <table|json>`, `--refresh`, `--reset`, `--force`, `--use <connection>`, `--sample-size <n>` (mongo only), `--sample-method <random|natural>` (mongo only)
244
247
  **Permission:** query-only+
245
248
 
249
+ > **Row counts on a full scan:** scanning every table records
250
+ > `rowCountIsEstimate: true` and reports the engine's row *estimate* (`information_schema.TABLES.TABLE_ROWS` on MySQL/MariaDB,
251
+ > `pg_class.reltuples` on PostgreSQL) rather than running `COUNT(*)` per table —
252
+ > a hundred full-table counts is what made scanning a large database unusable.
253
+ > `dbcli schema <table>` on a single table still reports the exact count. Tables
254
+ > are scanned with bounded parallelism (4 at a time).
255
+
246
256
  **Schema storage (v1.4+):** Schema is persisted as layered files under `.dbcli/schemas/`. With v2 multi-connection config each connection gets its own subdirectory (`.dbcli/schemas/<connection>/`). Run `dbcli schema --use <connection>` once per connection before querying it — otherwise `schema <table>` may return data from the wrong connection's cache.
247
257
 
248
258
  > **PostgreSQL:** Introspection uses the exact `public` catalog identity throughout. Full catalog/schema/table joins prevent a reused constraint name from contaminating another table; enum lookup includes its namespace; composite primary-key order comes from the exact table OID and index ordinality; and row estimates are scoped to the exact `public` relation. Row-count SQL qualifies and quotes both `"public"` and the exact table identifier, escaping embedded quotes so mixed-case or punctuation-bearing names remain distinct and safe.
@@ -286,6 +296,12 @@ dbcli query "SELECT * FROM orders" --format html > orders.html # pipe to stdou
286
296
  **Options:** `--format <table|json|csv|html>`, `--ui` (open the dashboard in the system browser; implies `--format html`), `--limit <number>`, `--no-limit`, `--collection <name>` (MongoDB / Elasticsearch), `--index <name>` (Elasticsearch alias for `--collection`), `--fields <list>`, `--truncate <number>` / `--no-truncate`, `-f, --query-file <path>`, `--use <name[,name]>`, `--slow-ms <number>`, `--recovery`
287
297
  **Permission:** query-only+ (Redis: per-command; Elasticsearch: per HTTP method/path)
288
298
 
299
+ > **Server-side scripts are rejected on every path.** MongoDB `$where`,
300
+ > `$function`, and `$accumulator`, and Elasticsearch `script` / `script_fields`,
301
+ > execute code on the database server. The adapters reject them anywhere in a
302
+ > filter, pipeline, or DSL body — before the request is sent — so `query`, `q`,
303
+ > saved snippets, and DML planning all behave the same way.
304
+
289
305
  #### Passive slow-query hint (`--slow-ms`)
290
306
 
291
307
  `query` and `q` read the execution time they already measured for a finished
@@ -1644,6 +1660,21 @@ shape (`schemaVersion: 1`):
1644
1660
  Recovery codes (fixed in v1.15.0):
1645
1661
  - `CONFIG_MISSING` — no `.dbcli` config; run `dbcli init`.
1646
1662
  - `CONN_REFUSED` / `CONN_AUTH_FAILED` / `CONN_TIMEOUT` / `CONN_HOST_NOT_FOUND` / `CONN_UNKNOWN` — connection failure variants.
1663
+ `CONN_TIMEOUT` also covers a statement the server canceled for exceeding the statement
1664
+ timeout (PostgreSQL `57014`, MySQL `3024`, MariaDB `1969`); `details.connectionCode` is
1665
+ `STATEMENT_TIMEOUT` there instead of `ETIMEDOUT`, the plan targets the query (`lint` →
1666
+ `explain` → re-run with `--statement-timeout <ms>`) rather than `doctor`, and no
1667
+ `branches` / `branchFork` / `verify` is emitted. PostgreSQL `57014` only counts as a
1668
+ statement timeout when the server says so — a `pg_cancel_backend()` cancel keeps the
1669
+ verbatim `Database error (57014): …` form instead of claiming a ceiling.
1670
+ The same `details.connectionCode` field distinguishes the other causes that share a
1671
+ connection code: `CONNECTION_LOST` (server closed the connection mid-session — PostgreSQL
1672
+ class 08 and `57P01`/`57P02`, MySQL `1053`), `TOO_MANY_CONNECTIONS` (PostgreSQL `53300`,
1673
+ MySQL `1040` — its plan counts current connections instead of running `doctor`, because
1674
+ rewriting host/port cannot create a slot), `SERVER_NOT_READY` (`57P03`, still starting up),
1675
+ `CONNECTION_REJECTED` (`08004` — the server answered and refused), `EHOSTUNREACH` (resolved
1676
+ but unroutable) and `TLS_ERROR` (handshake failure; reported as `CONN_UNKNOWN` rather than
1677
+ `CONN_AUTH_FAILED`, whose plan re-runs `init` for credentials that are not the problem).
1647
1678
  - `PERMISSION_DENIED` — active permission level forbids the operation.
1648
1679
  - `BLACKLIST_TABLE` / `BLACKLIST_COLUMN_WRITE` — blacklist violations.
1649
1680
  - `SNIPPET_NOT_FOUND` / `SNIPPET_AMBIGUOUS` / `SNIPPET_PARAM_MISSING` — saved-query failures.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,37 @@ All notable changes to dbcli are documented here.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.54.1] - 2026-08-13 - Error classification: a failure now says which layer broke
9
+
10
+ ### Fixed
11
+
12
+ - **A dropped connection, an exhausted connection pool, an unreachable host, and a TLS failure each say so.** `TRANSPORT_CODES` listed seven keys, so `ECONNRESET`, `EPIPE`, `EHOSTUNREACH`, `ENETUNREACH`, TLS certificate codes, PostgreSQL SQLSTATE class `08` and `53300`, and MySQL `1040` all carried a code, skipped the message-pattern fallback (which only runs when there is none), and landed on `Database error (ECONNRESET): …` with hints about `dbcli schema`. They now map to four new categories — `CONNECTION_LOST`, `TOO_MANY_CONNECTIONS`, `EHOSTUNREACH`, `TLS_ERROR` — each with the remedy that actually applies: retry a transient drop, inspect `pg_stat_activity` / `Threads_connected` against `max_connections`, check routing and VPN rather than DNS, or point `caPath` / `rejectUnauthorized` at the right certificate. PostgreSQL class `57` (`57P01` / `57P02` / `57P03`) and MySQL `1053` / `2006` / `2013` join them: restarting the server under a running query was reported as `Database error (57P01)` with hints about confirming the statement's objects. The codeless wordings drivers use for the same event — `Connection terminated unexpectedly`, `server closed the connection`, `server has gone away` — are recognized too. `57P03` (still starting up) and `08004` (the server answered and refused) get their own categories rather than borrowing a message that says the opposite, and TLS is matched by code prefix because enumerating OpenSSL's verify codes would keep missing some. The envelope states each cause in its own words instead of inheriting the shared recovery code's description — `CONN_HOST_NOT_FOUND` says the name could not be resolved, which is the one thing `EHOSTUNREACH` rules out — and TLS failures no longer route to the credentials plan, whose second step re-runs `init` for a certificate it never asks about. The adapter-code-to-envelope-code and the connection-versus-statement tables are both exhaustive `Record`s over the code union now, so a future code cannot silently inherit a generic category, and the REPL's reconnect decision reads the same table instead of its own list of codes and message substrings (#62).
13
+
14
+ - **`insert` / `update` / `delete` / `q` no longer report every failure as "failed to connect".** All four branched on `instanceof ConnectionError` and applied one message key, but that class carries every adapter-level error and only four of its nine codes are transport failures — so a missing table, a syntax error, or a statement timeout all arrived as `Failed to connect to database: …` while the connection was fine. The wording is now chosen by code, and rendering goes through the same `formatCliError` the central presenter uses, so these commands print the stable `Code:` line and the error's hints — both previously dropped on this path — exactly as `query` does (#61).
15
+
16
+ - **A statement the server canceled is no longer reported as a connection failure.** PostgreSQL `57014`, MySQL `3024`, and MariaDB `1969` fell through to `UNKNOWN` / `CONN_UNKNOWN`, so the CLI answered a query that ran out of statement time with connection-troubleshooting hints and a recovery plan that opened with `dbcli doctor` — the one thing that was not broken. They now map to a `STATEMENT_TIMEOUT` adapter code whose hints point at the query (`dbcli lint`, `dbcli explain`, re-run with an explicit `--statement-timeout <ms>`). The `--recovery` envelope keeps `schemaVersion` 1 and reports `CONN_TIMEOUT` with `details.connectionCode: "STATEMENT_TIMEOUT"`; that field selects the query-oriented plan, replaces the network-flavored message with one that states the ceiling that was in force, and suppresses both the `doctor-*` branches — whose `branchFork.after: 1` assumed step 1 was `doctor` — and the `verify` step, since nothing verifies this error except re-running the statement, which only the caller has. PostgreSQL `57014` is `query_canceled`, not only `statement_timeout`, so a `pg_cancel_backend()` or recovery-conflict cancel keeps the verbatim `Database error (57014): …` form rather than asserting a ceiling nobody set.
17
+
18
+ ## [1.54.0] - 2026-08-13 - Query engine hardening: timeout semantics, load-on-demand, deterministic builds
19
+
20
+ ### Added
21
+
22
+ - **Separate connection and statement timeouts.** `--timeout` previously did two jobs at once: PostgreSQL fed it to both `connectionTimeoutMillis` and `statement_timeout`, so the 5000ms connection default silently became a global query ceiling, while MySQL consumed neither and ignored `--timeout` entirely. Connection timeout keeps its 5000ms built-in default, statement timeout now has none (the server decides) unless you ask for one, and the new root-level `--statement-timeout <ms>` plus the `statementTimeout` connection field (0–3600000, `0` removes the ceiling) adjust it on its own. MySQL now consumes both, mapping the statement limit onto session-level `max_execution_time` / `max_statement_time` where the server supports it.
23
+
24
+ ### Changed
25
+
26
+ - **The CLI loads what a command actually needs.** Subcommands register lazily, SQL drivers load at connection time rather than at import, and `node-sql-parser` is both deferred and externalized from the bundle — measured at roughly 8ms off startup for the lazy registration alone.
27
+ - **Full-schema scans cost less.** Per-table queries are merged, row estimates replace `COUNT(*)`, and remaining work runs with bounded parallelism. The query path no longer loads the layered schema in full — it fetches the single table it needs.
28
+ - **Repeated lookups are cached within a process.** Config binding files are read and validated once per process, and the skill update check keeps a TTL cache instead of re-checking on every invocation. Redis and Elasticsearch list operations were narrowed to what the caller asked for.
29
+ - **Identifier quoting and error classification each have one implementation.** Quote/encode helpers were consolidated into a shared utility, and driver errors are now classified by error code first rather than by matching message text.
30
+ - **Server-side script protection lives in the adapter layer**, so every caller is covered by the same guard rather than each command re-implementing it.
31
+
32
+ ### Fixed
33
+
34
+ - **`bun run build` was non-deterministic.** Consecutive builds of identical sources alternated between two `dist/cli-runtime.mjs` outputs about 690KB apart, depending on whether the bundler pulled in 48 `@inquirer/*` modules. `@inquirer/prompts` is now external, which also removes a silent degradation path where the prompt implementation quietly changed between builds. `bun run build:determinism` checks this in CI.
35
+ - **One CLI query writes exactly one audit entry.** Some paths recorded the same query more than once.
36
+ - **Windows CI is green again.** Path separator assumptions, CRLF handling in test fixtures, and CRLF frontmatter stripping in skill sources were all Unix-only.
37
+ - **The startup benchmark measures the noise floor rather than the median**, which is what actually distinguishes a regression from scheduler jitter, and a guide test no longer flakes on a random UUID colliding with `'5432'`.
38
+
8
39
  ## [1.53.0] - 2026-08-09 - Offline evidence, semantic contracts, and impact assessment
9
40
 
10
41
  ### Added
package/assets/SKILL.md CHANGED
@@ -605,4 +605,4 @@ schema. Raw `query` / `export` invocations render a sortable table only.
605
605
  - Blacklisted tables and columns are redacted from query output.
606
606
  - `schema` reports `estimatedRowCount` and `sizeCategory` (small / medium / large / huge). For large/huge tables add `WHERE` or `LIMIT` — bands in [reference.md](reference.md#schema).
607
607
  - `doctor` on `mongodb+srv://` reports whether SRV resolves natively or through the DoH fallback — useful when the runtime restricts DNS.
608
- - **Global flags:** `--version`, `--config <path>`, `--global`, `--use <name>`, `--timeout <ms>`, `-v` / `--verbose` / `-vv`, `-q` / `--quiet`, `--no-color` (also honours `NO_COLOR`). Root-level flags must precede the command unless the command explicitly declares a command-level option.
608
+ - **Global flags:** `--version`, `--config <path>`, `--global`, `--use <name>`, `--timeout <ms>`, `--statement-timeout <ms>`, `-v` / `--verbose` / `-vv`, `-q` / `--quiet`, `--no-color` (also honours `NO_COLOR`). Root-level flags must precede the command unless the command explicitly declares a command-level option.
@@ -472,4 +472,4 @@ dbcli export "SELECT * FROM orders" --format html --output orders.html
472
472
  - 被 blacklist 的 table / column 會從查詢輸出中遮蔽。
473
473
  - `schema` 回報 `estimatedRowCount` 與 `sizeCategory`(small / medium / large / huge)。大 / 巨大表要加 `WHERE` 或 `LIMIT` — 分界值見 [reference.md](reference.md#schema)。
474
474
  - 對 `mongodb+srv://` 連線,`doctor` 會回報 SRV 是用原生解析或走 DoH fallback — 在執行環境限制 DNS 時很有用。
475
- - **全域旗標:** `--version`、`--config <path>`、`--global`、`--use <name>`、`--timeout <ms>`、`-v` / `--verbose` / `-vv`、`-q` / `--quiet`、`--no-color`(也尊重 `NO_COLOR`)。除非指令明確宣告 command-level 選項,否則 root-level 旗標必須放在指令之前。
475
+ - **全域旗標:** `--version`、`--config <path>`、`--global`、`--use <name>`、`--timeout <ms>`、`--statement-timeout <ms>`、`-v` / `--verbose` / `-vv`、`-q` / `--quiet`、`--no-color`(也尊重 `NO_COLOR`)。除非指令明確宣告 command-level 選項,否則 root-level 旗標必須放在指令之前。
@@ -83,13 +83,16 @@ command-level option is only valid after the command that declares it.
83
83
  | `--global` | Select the user-global registry at `~/.config/dbcli/config.json` instead of the current project's `.dbcli` config. Place it before the command path. |
84
84
  | `--use <connection>` | Select a named connection for this invocation; place it before the command path unless that command explicitly lists a command-level `--use`. |
85
85
  | `--timeout <ms>` | Connection timeout in milliseconds (integer, 100–600000), overriding the connection config's `timeout` field for this invocation. Applies to every engine adapter. Without either the flag or the config field, adapters fall back to their built-in 5000ms default. |
86
+ | `--statement-timeout <ms>` | How long a single statement may run, in milliseconds (integer, 0–3600000; `0` removes the limit), overriding the connection config's `statementTimeout` field. Independent of the connection timeout — raising it does not slow down detection of an unreachable host. Falls back to `--timeout` when unset, and to the server's own setting when neither is given. |
86
87
 
87
88
  `--timeout` is applied only when the adapter is constructed for this invocation — it is
88
89
  never written back to `config.json`. Set the connection's `timeout` field instead for a
89
- value that persists across runs. On PostgreSQL, the same value is also used as the
90
- session's `statement_timeout` (not just the connection timeout), so a low value can cut
91
- off a long-running query with an error that looks like a connection timeout; the 100ms
92
- floor exists specifically to keep that failure mode from being too easy to trigger.
90
+ value that persists across runs. `--timeout` caps statement time as well as connection time, so a
91
+ low value cuts off a long-running query with an error that reads like a connection
92
+ problem; the 100ms floor exists specifically to keep that failure mode from being too
93
+ easy to trigger. Use `--statement-timeout` when only the statement limit should change.
94
+ With neither flag, dbcli sets no statement limit at all — the server's setting decides,
95
+ so a query that runs longer than the connection timeout is no longer cut off.
93
96
  Elasticsearch applies its timeout per request rather than once for the whole connection.
94
97
  The `timeout` field itself always takes a literal number — unlike other connection
95
98
  fields, it does not accept an `{"$env": "..."}` reference.
@@ -220,8 +223,8 @@ dbcli list --include-system # Elasticsearch: include `.system` indices
220
223
  **Permission:** query-only+
221
224
 
222
225
  > **MongoDB:** Lists collections with estimated document count.
223
- > **Redis:** Returns up to 100 000 keys via `SCAN MATCH * COUNT 1000`. The header reads `Keys in db <n> (redis):` where `<n>` is the logical DB index.
224
- > **Elasticsearch:** Returns indices with `documentCount` from `/_stats/docs`; aliases are tagged separately. System indices (names starting with `.`) are hidden unless `--include-system` is passed.
226
+ > **Redis:** Samples the first 1 000 keys scanned via `SCAN MATCH * COUNT 1000`, applying the blacklist during the scan. When the keyspace is larger, table output adds a `Sampled the first 1000 keys scanned` line and JSON output carries `sampled: true` with `sampleLimit`. The header reads `Keys in db <n> (redis):` where `<n>` is the logical DB index.
227
+ > **Elasticsearch:** Returns indices with `documentCount` from one `/_cat/indices?h=index,docs.count&expand_wildcards=all` request (open and closed indices alike). The count is primaries-only, so a replicated index no longer reports its replica copies as extra documents. Aliases are tagged separately. System indices (names starting with `.`) are hidden unless `--include-system` is passed.
225
228
 
226
229
  ### schema
227
230
 
@@ -243,6 +246,13 @@ dbcli schema --use prod # Scan prod DB; saves to .dbcli/schemas/prod
243
246
  **Options:** `--format <table|json>`, `--refresh`, `--reset`, `--force`, `--use <connection>`, `--sample-size <n>` (mongo only), `--sample-method <random|natural>` (mongo only)
244
247
  **Permission:** query-only+
245
248
 
249
+ > **Row counts on a full scan:** scanning every table records
250
+ > `rowCountIsEstimate: true` and reports the engine's row *estimate* (`information_schema.TABLES.TABLE_ROWS` on MySQL/MariaDB,
251
+ > `pg_class.reltuples` on PostgreSQL) rather than running `COUNT(*)` per table —
252
+ > a hundred full-table counts is what made scanning a large database unusable.
253
+ > `dbcli schema <table>` on a single table still reports the exact count. Tables
254
+ > are scanned with bounded parallelism (4 at a time).
255
+
246
256
  **Schema storage (v1.4+):** Schema is persisted as layered files under `.dbcli/schemas/`. With v2 multi-connection config each connection gets its own subdirectory (`.dbcli/schemas/<connection>/`). Run `dbcli schema --use <connection>` once per connection before querying it — otherwise `schema <table>` may return data from the wrong connection's cache.
247
257
 
248
258
  > **PostgreSQL:** Introspection uses the exact `public` catalog identity throughout. Full catalog/schema/table joins prevent a reused constraint name from contaminating another table; enum lookup includes its namespace; composite primary-key order comes from the exact table OID and index ordinality; and row estimates are scoped to the exact `public` relation. Row-count SQL qualifies and quotes both `"public"` and the exact table identifier, escaping embedded quotes so mixed-case or punctuation-bearing names remain distinct and safe.
@@ -286,6 +296,12 @@ dbcli query "SELECT * FROM orders" --format html > orders.html # pipe to stdou
286
296
  **Options:** `--format <table|json|csv|html>`, `--ui` (open the dashboard in the system browser; implies `--format html`), `--limit <number>`, `--no-limit`, `--collection <name>` (MongoDB / Elasticsearch), `--index <name>` (Elasticsearch alias for `--collection`), `--fields <list>`, `--truncate <number>` / `--no-truncate`, `-f, --query-file <path>`, `--use <name[,name]>`, `--slow-ms <number>`, `--recovery`
287
297
  **Permission:** query-only+ (Redis: per-command; Elasticsearch: per HTTP method/path)
288
298
 
299
+ > **Server-side scripts are rejected on every path.** MongoDB `$where`,
300
+ > `$function`, and `$accumulator`, and Elasticsearch `script` / `script_fields`,
301
+ > execute code on the database server. The adapters reject them anywhere in a
302
+ > filter, pipeline, or DSL body — before the request is sent — so `query`, `q`,
303
+ > saved snippets, and DML planning all behave the same way.
304
+
289
305
  #### Passive slow-query hint (`--slow-ms`)
290
306
 
291
307
  `query` and `q` read the execution time they already measured for a finished
@@ -1644,6 +1660,21 @@ shape (`schemaVersion: 1`):
1644
1660
  Recovery codes (fixed in v1.15.0):
1645
1661
  - `CONFIG_MISSING` — no `.dbcli` config; run `dbcli init`.
1646
1662
  - `CONN_REFUSED` / `CONN_AUTH_FAILED` / `CONN_TIMEOUT` / `CONN_HOST_NOT_FOUND` / `CONN_UNKNOWN` — connection failure variants.
1663
+ `CONN_TIMEOUT` also covers a statement the server canceled for exceeding the statement
1664
+ timeout (PostgreSQL `57014`, MySQL `3024`, MariaDB `1969`); `details.connectionCode` is
1665
+ `STATEMENT_TIMEOUT` there instead of `ETIMEDOUT`, the plan targets the query (`lint` →
1666
+ `explain` → re-run with `--statement-timeout <ms>`) rather than `doctor`, and no
1667
+ `branches` / `branchFork` / `verify` is emitted. PostgreSQL `57014` only counts as a
1668
+ statement timeout when the server says so — a `pg_cancel_backend()` cancel keeps the
1669
+ verbatim `Database error (57014): …` form instead of claiming a ceiling.
1670
+ The same `details.connectionCode` field distinguishes the other causes that share a
1671
+ connection code: `CONNECTION_LOST` (server closed the connection mid-session — PostgreSQL
1672
+ class 08 and `57P01`/`57P02`, MySQL `1053`), `TOO_MANY_CONNECTIONS` (PostgreSQL `53300`,
1673
+ MySQL `1040` — its plan counts current connections instead of running `doctor`, because
1674
+ rewriting host/port cannot create a slot), `SERVER_NOT_READY` (`57P03`, still starting up),
1675
+ `CONNECTION_REJECTED` (`08004` — the server answered and refused), `EHOSTUNREACH` (resolved
1676
+ but unroutable) and `TLS_ERROR` (handshake failure; reported as `CONN_UNKNOWN` rather than
1677
+ `CONN_AUTH_FAILED`, whose plan re-runs `init` for credentials that are not the problem).
1647
1678
  - `PERMISSION_DENIED` — active permission level forbids the operation.
1648
1679
  - `BLACKLIST_TABLE` / `BLACKLIST_COLUMN_WRITE` — blacklist violations.
1649
1680
  - `SNIPPET_NOT_FOUND` / `SNIPPET_AMBIGUOUS` / `SNIPPET_PARAM_MISSING` — saved-query failures.