@carllee1983/dbcli 1.54.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.
- package/.cursor/skills/dbcli/reference.md +15 -0
- package/.github/skills/dbcli/reference.md +15 -0
- package/CHANGELOG.md +10 -0
- package/assets/reference.md +15 -0
- package/dist/cli-runtime.mjs +266 -27
- package/dist/cli.mjs +1 -1
- package/dist/core.d.ts +16 -6
- package/dist/core.mjs +95 -10
- package/package.json +1 -1
- package/plugins/dbcli-agent/skills/dbcli/reference.md +15 -0
- package/skills/dbcli/reference.md +15 -0
|
@@ -1660,6 +1660,21 @@ shape (`schemaVersion: 1`):
|
|
|
1660
1660
|
Recovery codes (fixed in v1.15.0):
|
|
1661
1661
|
- `CONFIG_MISSING` — no `.dbcli` config; run `dbcli init`.
|
|
1662
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).
|
|
1663
1678
|
- `PERMISSION_DENIED` — active permission level forbids the operation.
|
|
1664
1679
|
- `BLACKLIST_TABLE` / `BLACKLIST_COLUMN_WRITE` — blacklist violations.
|
|
1665
1680
|
- `SNIPPET_NOT_FOUND` / `SNIPPET_AMBIGUOUS` / `SNIPPET_PARAM_MISSING` — saved-query failures.
|
|
@@ -1660,6 +1660,21 @@ shape (`schemaVersion: 1`):
|
|
|
1660
1660
|
Recovery codes (fixed in v1.15.0):
|
|
1661
1661
|
- `CONFIG_MISSING` — no `.dbcli` config; run `dbcli init`.
|
|
1662
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).
|
|
1663
1678
|
- `PERMISSION_DENIED` — active permission level forbids the operation.
|
|
1664
1679
|
- `BLACKLIST_TABLE` / `BLACKLIST_COLUMN_WRITE` — blacklist violations.
|
|
1665
1680
|
- `SNIPPET_NOT_FOUND` / `SNIPPET_AMBIGUOUS` / `SNIPPET_PARAM_MISSING` — saved-query failures.
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,16 @@ 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
|
+
|
|
8
18
|
## [1.54.0] - 2026-08-13 - Query engine hardening: timeout semantics, load-on-demand, deterministic builds
|
|
9
19
|
|
|
10
20
|
### Added
|
package/assets/reference.md
CHANGED
|
@@ -1660,6 +1660,21 @@ shape (`schemaVersion: 1`):
|
|
|
1660
1660
|
Recovery codes (fixed in v1.15.0):
|
|
1661
1661
|
- `CONFIG_MISSING` — no `.dbcli` config; run `dbcli init`.
|
|
1662
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).
|
|
1663
1678
|
- `PERMISSION_DENIED` — active permission level forbids the operation.
|
|
1664
1679
|
- `BLACKLIST_TABLE` / `BLACKLIST_COLUMN_WRITE` — blacklist violations.
|
|
1665
1680
|
- `SNIPPET_NOT_FOUND` / `SNIPPET_AMBIGUOUS` / `SNIPPET_PARAM_MISSING` — saved-query failures.
|
package/dist/cli-runtime.mjs
CHANGED
|
@@ -51,7 +51,7 @@ var package_default;
|
|
|
51
51
|
var init_package = __esm(() => {
|
|
52
52
|
package_default = {
|
|
53
53
|
name: "@carllee1983/dbcli",
|
|
54
|
-
version: "1.54.
|
|
54
|
+
version: "1.54.1",
|
|
55
55
|
description: "Database CLI for AI agents",
|
|
56
56
|
type: "module",
|
|
57
57
|
publishConfig: {
|
|
@@ -9674,10 +9674,12 @@ var init_types2 = __esm(() => {
|
|
|
9674
9674
|
ConnectionError = class ConnectionError extends Error {
|
|
9675
9675
|
code;
|
|
9676
9676
|
hints;
|
|
9677
|
-
|
|
9677
|
+
limitMs;
|
|
9678
|
+
constructor(code, message, hints, limitMs) {
|
|
9678
9679
|
super(message);
|
|
9679
9680
|
this.code = code;
|
|
9680
9681
|
this.hints = hints;
|
|
9682
|
+
this.limitMs = limitMs;
|
|
9681
9683
|
this.name = "ConnectionError";
|
|
9682
9684
|
Object.setPrototypeOf(this, ConnectionError.prototype);
|
|
9683
9685
|
}
|
|
@@ -9836,18 +9838,28 @@ var init_capabilities = __esm(() => {
|
|
|
9836
9838
|
});
|
|
9837
9839
|
|
|
9838
9840
|
// src/adapters/error-mapper.ts
|
|
9839
|
-
function
|
|
9841
|
+
function isTransportDriverCode(code) {
|
|
9842
|
+
return TLS_CODE_PREFIX.test(code) || TRANSPORT_CODES[code] !== undefined;
|
|
9843
|
+
}
|
|
9844
|
+
function categorize(errCode, errno, errMsg, system) {
|
|
9840
9845
|
if (errCode) {
|
|
9846
|
+
if (TLS_CODE_PREFIX.test(errCode))
|
|
9847
|
+
return "TLS_ERROR";
|
|
9841
9848
|
const known = TRANSPORT_CODES[errCode] ?? MYSQL_CODES[errCode] ?? SQLSTATES[errCode] ?? REDIS_PREFIXES[errCode];
|
|
9849
|
+
if (known === "STATEMENT_TIMEOUT" && errCode === "57014" && !PG_CANCEL_BY_TIMEOUT.test(errMsg)) {
|
|
9850
|
+
return null;
|
|
9851
|
+
}
|
|
9842
9852
|
if (known)
|
|
9843
9853
|
return known;
|
|
9844
9854
|
}
|
|
9845
|
-
|
|
9855
|
+
const isMysqlFamily = system === "mysql" || system === "mariadb";
|
|
9856
|
+
if (isMysqlFamily && typeof errno === "number" && MYSQL_ERRNOS[errno]) {
|
|
9846
9857
|
return MYSQL_ERRNOS[errno];
|
|
9858
|
+
}
|
|
9847
9859
|
const redisPrefix = errMsg.match(/^([A-Z]+)\s/)?.[1];
|
|
9848
9860
|
if (redisPrefix && REDIS_PREFIXES[redisPrefix])
|
|
9849
9861
|
return REDIS_PREFIXES[redisPrefix];
|
|
9850
|
-
if (errCode || typeof errno === "number")
|
|
9862
|
+
if (errCode || isMysqlFamily && typeof errno === "number")
|
|
9851
9863
|
return null;
|
|
9852
9864
|
for (const [pattern, category] of FALLBACK_PATTERNS) {
|
|
9853
9865
|
if (pattern.test(errMsg))
|
|
@@ -9870,7 +9882,7 @@ function mapError(error, system, options) {
|
|
|
9870
9882
|
const errMsg = String(err?.message || String(error));
|
|
9871
9883
|
const errCode = String(err?.code || "");
|
|
9872
9884
|
const errno = err?.errno;
|
|
9873
|
-
switch (categorize(errCode, errno, errMsg)) {
|
|
9885
|
+
switch (categorize(errCode, errno, errMsg, system)) {
|
|
9874
9886
|
case "ECONNREFUSED":
|
|
9875
9887
|
return new ConnectionError("ECONNREFUSED", `Cannot connect to ${options.host}:${options.port} \u2014 server is not running or not listening on this port`, serviceHints(system, options));
|
|
9876
9888
|
case "ETIMEDOUT":
|
|
@@ -9879,6 +9891,42 @@ function mapError(error, system, options) {
|
|
|
9879
9891
|
`Increase timeout: edit .dbcli and add "timeout": 15000`,
|
|
9880
9892
|
`Verify network connectivity: ping ${options.host} -c 3`
|
|
9881
9893
|
]);
|
|
9894
|
+
case "CONNECTION_LOST":
|
|
9895
|
+
return new ConnectionError("CONNECTION_LOST", `Connection to ${options.host}:${options.port} was lost mid-session \u2014 the server closed it or the network dropped`, [
|
|
9896
|
+
"Re-run the command: a dropped connection is often transient",
|
|
9897
|
+
`Check whether the server restarted or is cycling: ${SYSTEM_FACTS[system].logFile}`,
|
|
9898
|
+
"If it happens on long-running work, look at the server idle/wait timeout"
|
|
9899
|
+
]);
|
|
9900
|
+
case "SERVER_NOT_READY":
|
|
9901
|
+
return new ConnectionError("SERVER_NOT_READY", `${options.host}:${options.port} is not accepting connections yet \u2014 the server is starting up or recovering`, [
|
|
9902
|
+
"Retry shortly: this clears on its own once startup or recovery finishes",
|
|
9903
|
+
`Watch progress in the server log: ${SYSTEM_FACTS[system].logFile}`,
|
|
9904
|
+
"On a replica, this also appears while it catches up with the primary"
|
|
9905
|
+
]);
|
|
9906
|
+
case "CONNECTION_REJECTED":
|
|
9907
|
+
return new ConnectionError("CONNECTION_REJECTED", `${options.host}:${options.port} answered but rejected the connection attempt`, [
|
|
9908
|
+
"Check the server access rules (pg_hba.conf) for this user, database, and client address",
|
|
9909
|
+
"If a pooler (pgbouncer / ProxySQL) sits in front, check its own rules and limits",
|
|
9910
|
+
"Check whether the connection limit for this user or database is exhausted"
|
|
9911
|
+
]);
|
|
9912
|
+
case "TOO_MANY_CONNECTIONS":
|
|
9913
|
+
return new ConnectionError("TOO_MANY_CONNECTIONS", `${options.host}:${options.port} refused the connection \u2014 the server has no connection slots left`, [
|
|
9914
|
+
"Wait and retry: the limit is on concurrent connections, not on you",
|
|
9915
|
+
system === "postgresql" ? "Inspect usage: SELECT count(*) FROM pg_stat_activity \u2014 compare against max_connections" : 'Inspect usage: SHOW STATUS LIKE "Threads_connected" \u2014 compare against max_connections',
|
|
9916
|
+
"Close idle sessions, or raise max_connections if the load is legitimate"
|
|
9917
|
+
]);
|
|
9918
|
+
case "EHOSTUNREACH":
|
|
9919
|
+
return new ConnectionError("EHOSTUNREACH", `No route to ${options.host} \u2014 the name resolved, but the host or network is unreachable`, [
|
|
9920
|
+
"Check that you are on the right network or VPN",
|
|
9921
|
+
`Check routing and firewall between here and ${options.host}`,
|
|
9922
|
+
`Confirm the address is the one you meant: ${options.host}:${options.port}`
|
|
9923
|
+
]);
|
|
9924
|
+
case "TLS_ERROR":
|
|
9925
|
+
return new ConnectionError("TLS_ERROR", `TLS handshake failed: ${errMsg}`, [
|
|
9926
|
+
'Point the connection at the right CA bundle: set "caPath" in .dbcli',
|
|
9927
|
+
'For a self-signed certificate in a trusted network, set "rejectUnauthorized": false',
|
|
9928
|
+
`Confirm the certificate covers the host you connected to: ${options.host}`
|
|
9929
|
+
]);
|
|
9882
9930
|
case "ENOTFOUND":
|
|
9883
9931
|
return new ConnectionError("ENOTFOUND", `Host not found: ${options.host}`, [
|
|
9884
9932
|
`Check the hostname spelling: ${options.host}`,
|
|
@@ -9909,6 +9957,14 @@ function mapError(error, system, options) {
|
|
|
9909
9957
|
"Column names are case-sensitive on some databases (PostgreSQL with quoted identifiers)"
|
|
9910
9958
|
]);
|
|
9911
9959
|
}
|
|
9960
|
+
case "STATEMENT_TIMEOUT": {
|
|
9961
|
+
const limitMs = options.statementTimeout ?? options.timeout;
|
|
9962
|
+
return new ConnectionError("STATEMENT_TIMEOUT", `Statement timed out${limitMs ? ` (${limitMs}ms)` : ""} \u2014 the server canceled this query before it finished`, [
|
|
9963
|
+
'Inspect the query plan: dbcli explain "<sql>"',
|
|
9964
|
+
"Raise the ceiling for this run: dbcli --statement-timeout <ms> \u2026 (0 removes it)",
|
|
9965
|
+
"Narrow the query: add WHERE filters, reduce the LIMIT, or index the scanned columns"
|
|
9966
|
+
], limitMs);
|
|
9967
|
+
}
|
|
9912
9968
|
case "SQL_SYNTAX_ERROR":
|
|
9913
9969
|
return new ConnectionError("SQL_SYNTAX_ERROR", `SQL syntax error: ${errMsg}`, [
|
|
9914
9970
|
"Check your SQL syntax near the position reported above",
|
|
@@ -9929,7 +9985,7 @@ function mapError(error, system, options) {
|
|
|
9929
9985
|
`Try connecting directly with the ${SYSTEM_FACTS[system].client} command-line tool`
|
|
9930
9986
|
]);
|
|
9931
9987
|
}
|
|
9932
|
-
var TRANSPORT_CODES, MYSQL_CODES, MYSQL_ERRNOS, SQLSTATES, REDIS_PREFIXES, FALLBACK_PATTERNS, SYSTEM_FACTS;
|
|
9988
|
+
var TRANSPORT_CODES, TLS_CODE_PREFIX, MYSQL_CODES, MYSQL_ERRNOS, SQLSTATES, REDIS_PREFIXES, FALLBACK_PATTERNS, PG_CANCEL_BY_TIMEOUT, SYSTEM_FACTS;
|
|
9933
9989
|
var init_error_mapper = __esm(() => {
|
|
9934
9990
|
init_types2();
|
|
9935
9991
|
TRANSPORT_CODES = {
|
|
@@ -9938,9 +9994,16 @@ var init_error_mapper = __esm(() => {
|
|
|
9938
9994
|
ESOCKETTIMEDOUT: "ETIMEDOUT",
|
|
9939
9995
|
ENOTFOUND: "ENOTFOUND",
|
|
9940
9996
|
EAI_AGAIN: "ENOTFOUND",
|
|
9941
|
-
PROTOCOL_CONNECTION_LOST: "
|
|
9942
|
-
PROTOCOL_SEQUENCE_TIMEOUT: "ETIMEDOUT"
|
|
9943
|
-
|
|
9997
|
+
PROTOCOL_CONNECTION_LOST: "CONNECTION_LOST",
|
|
9998
|
+
PROTOCOL_SEQUENCE_TIMEOUT: "ETIMEDOUT",
|
|
9999
|
+
ECONNRESET: "CONNECTION_LOST",
|
|
10000
|
+
EPIPE: "CONNECTION_LOST",
|
|
10001
|
+
ECONNABORTED: "CONNECTION_LOST",
|
|
10002
|
+
EHOSTUNREACH: "EHOSTUNREACH",
|
|
10003
|
+
ENETUNREACH: "EHOSTUNREACH",
|
|
10004
|
+
ENETDOWN: "EHOSTUNREACH"
|
|
10005
|
+
};
|
|
10006
|
+
TLS_CODE_PREFIX = /^(CERT_|SELF_SIGNED_|DEPTH_ZERO_|UNABLE_TO_(?:GET|VERIFY)_|ERR_TLS_|ERR_SSL_)/;
|
|
9944
10007
|
MYSQL_CODES = {
|
|
9945
10008
|
ER_NO_SUCH_TABLE: "TABLE_NOT_FOUND",
|
|
9946
10009
|
ER_BAD_FIELD_ERROR: "COLUMN_NOT_FOUND",
|
|
@@ -9948,7 +10011,11 @@ var init_error_mapper = __esm(() => {
|
|
|
9948
10011
|
ER_ACCESS_DENIED_ERROR: "AUTH_FAILED",
|
|
9949
10012
|
ER_DBACCESS_DENIED_ERROR: "AUTH_FAILED",
|
|
9950
10013
|
ER_NOT_SUPPORTED_AUTH_MODE: "AUTH_FAILED",
|
|
9951
|
-
ER_MUST_CHANGE_PASSWORD_LOGIN: "AUTH_FAILED"
|
|
10014
|
+
ER_MUST_CHANGE_PASSWORD_LOGIN: "AUTH_FAILED",
|
|
10015
|
+
ER_QUERY_TIMEOUT: "STATEMENT_TIMEOUT",
|
|
10016
|
+
ER_STATEMENT_TIMEOUT: "STATEMENT_TIMEOUT",
|
|
10017
|
+
ER_CON_COUNT_ERROR: "TOO_MANY_CONNECTIONS",
|
|
10018
|
+
ER_SERVER_SHUTDOWN: "CONNECTION_LOST"
|
|
9952
10019
|
};
|
|
9953
10020
|
MYSQL_ERRNOS = {
|
|
9954
10021
|
1045: "AUTH_FAILED",
|
|
@@ -9957,14 +10024,30 @@ var init_error_mapper = __esm(() => {
|
|
|
9957
10024
|
1064: "SQL_SYNTAX_ERROR",
|
|
9958
10025
|
1146: "TABLE_NOT_FOUND",
|
|
9959
10026
|
1251: "AUTH_FAILED",
|
|
9960
|
-
1698: "AUTH_FAILED"
|
|
10027
|
+
1698: "AUTH_FAILED",
|
|
10028
|
+
1040: "TOO_MANY_CONNECTIONS",
|
|
10029
|
+
1053: "CONNECTION_LOST",
|
|
10030
|
+
2006: "CONNECTION_LOST",
|
|
10031
|
+
2013: "CONNECTION_LOST",
|
|
10032
|
+
1969: "STATEMENT_TIMEOUT",
|
|
10033
|
+
3024: "STATEMENT_TIMEOUT"
|
|
9961
10034
|
};
|
|
9962
10035
|
SQLSTATES = {
|
|
9963
10036
|
"28000": "AUTH_FAILED",
|
|
9964
10037
|
"28P01": "AUTH_FAILED",
|
|
9965
10038
|
"42601": "SQL_SYNTAX_ERROR",
|
|
9966
10039
|
"42P01": "TABLE_NOT_FOUND",
|
|
9967
|
-
"42703": "COLUMN_NOT_FOUND"
|
|
10040
|
+
"42703": "COLUMN_NOT_FOUND",
|
|
10041
|
+
"57014": "STATEMENT_TIMEOUT",
|
|
10042
|
+
"08000": "CONNECTION_LOST",
|
|
10043
|
+
"08003": "CONNECTION_LOST",
|
|
10044
|
+
"08006": "CONNECTION_LOST",
|
|
10045
|
+
"08001": "ECONNREFUSED",
|
|
10046
|
+
"08004": "CONNECTION_REJECTED",
|
|
10047
|
+
"53300": "TOO_MANY_CONNECTIONS",
|
|
10048
|
+
"57P01": "CONNECTION_LOST",
|
|
10049
|
+
"57P02": "CONNECTION_LOST",
|
|
10050
|
+
"57P03": "SERVER_NOT_READY"
|
|
9968
10051
|
};
|
|
9969
10052
|
REDIS_PREFIXES = {
|
|
9970
10053
|
NOAUTH: "AUTH_FAILED",
|
|
@@ -9976,6 +10059,10 @@ var init_error_mapper = __esm(() => {
|
|
|
9976
10059
|
[/connect(?:ion)? time(?:d )?out/i, "ETIMEDOUT"],
|
|
9977
10060
|
[/timeout exceeded/i, "ETIMEDOUT"],
|
|
9978
10061
|
[/timed out/i, "ETIMEDOUT"],
|
|
10062
|
+
[/connection terminated/i, "CONNECTION_LOST"],
|
|
10063
|
+
[/server closed the connection/i, "CONNECTION_LOST"],
|
|
10064
|
+
[/server has gone away/i, "CONNECTION_LOST"],
|
|
10065
|
+
[/lost connection to \S+ server/i, "CONNECTION_LOST"],
|
|
9979
10066
|
[/getaddrinfo/i, "ENOTFOUND"],
|
|
9980
10067
|
[/authentication failed/i, "AUTH_FAILED"],
|
|
9981
10068
|
[/access denied for user/i, "AUTH_FAILED"],
|
|
@@ -9983,6 +10070,7 @@ var init_error_mapper = __esm(() => {
|
|
|
9983
10070
|
[/role\s+".*?"\s+does not exist/i, "AUTH_FAILED"],
|
|
9984
10071
|
[/password supplied/i, "AUTH_FAILED"]
|
|
9985
10072
|
];
|
|
10073
|
+
PG_CANCEL_BY_TIMEOUT = /statement timeout/i;
|
|
9986
10074
|
SYSTEM_FACTS = {
|
|
9987
10075
|
postgresql: {
|
|
9988
10076
|
serviceCheck: "systemctl status postgresql",
|
|
@@ -17956,7 +18044,7 @@ async function mapWithConcurrency(items, limit, worker) {
|
|
|
17956
18044
|
}
|
|
17957
18045
|
|
|
17958
18046
|
// src/core/recovery/types.ts
|
|
17959
|
-
var RECOVERY_SCHEMA_VERSION = 1, RECOVERY_CODES, MAX_BRANCH_STEPS = 6, MAX_BRANCH_COUNT = 8, SchemaCacheMissingError, RECOVERY_CODE_METADATA;
|
|
18047
|
+
var RECOVERY_SCHEMA_VERSION = 1, RECOVERY_CODES, MAX_BRANCH_STEPS = 6, MAX_BRANCH_COUNT = 8, STATEMENT_TIMEOUT_CODE = "STATEMENT_TIMEOUT", SchemaCacheMissingError, RECOVERY_CODE_METADATA;
|
|
17960
18048
|
var init_types4 = __esm(() => {
|
|
17961
18049
|
RECOVERY_CODES = [
|
|
17962
18050
|
"CONFIG_MISSING",
|
|
@@ -18081,6 +18169,64 @@ function dryRunStepForWrite(ctx, quotedTable, placeholders) {
|
|
|
18081
18169
|
};
|
|
18082
18170
|
return draft;
|
|
18083
18171
|
}
|
|
18172
|
+
function statementTimeoutSteps() {
|
|
18173
|
+
return [
|
|
18174
|
+
{
|
|
18175
|
+
command: 'dbcli lint "<sql>"',
|
|
18176
|
+
rationale: "Static anti-pattern read of the statement; needs no connection, so it cannot time out in turn.",
|
|
18177
|
+
risk: "readonly",
|
|
18178
|
+
expects: "Findings with rewrite drafts, or an empty list when nothing is flagged.",
|
|
18179
|
+
placeholders: ["<sql>"]
|
|
18180
|
+
},
|
|
18181
|
+
{
|
|
18182
|
+
command: 'dbcli explain "<sql>"',
|
|
18183
|
+
rationale: "Read the query plan to find the scan or join that exceeded the statement limit.",
|
|
18184
|
+
risk: "readonly",
|
|
18185
|
+
expects: "Annotated query plan; look for sequential scans and unindexed joins.",
|
|
18186
|
+
placeholders: ["<sql>"]
|
|
18187
|
+
},
|
|
18188
|
+
{
|
|
18189
|
+
command: 'dbcli --statement-timeout <ms> query "<sql>"',
|
|
18190
|
+
rationale: "Re-run with an explicit ceiling once the cost is understood; 0 removes the limit entirely.",
|
|
18191
|
+
risk: "readonly",
|
|
18192
|
+
expects: "Query result, or the same timeout if <ms> is still below what the plan costs.",
|
|
18193
|
+
placeholders: ["<ms>", "<sql>"]
|
|
18194
|
+
}
|
|
18195
|
+
];
|
|
18196
|
+
}
|
|
18197
|
+
function tlsErrorSteps() {
|
|
18198
|
+
return [
|
|
18199
|
+
{
|
|
18200
|
+
command: "dbcli status --format json",
|
|
18201
|
+
rationale: "Read back the active connection without a live probe to confirm which host and TLS settings are in force.",
|
|
18202
|
+
risk: "readonly",
|
|
18203
|
+
expects: "JSON status with system / permission; no credentials."
|
|
18204
|
+
},
|
|
18205
|
+
{
|
|
18206
|
+
command: "dbcli doctor --format json",
|
|
18207
|
+
rationale: "Doctor reports the handshake failure verbatim, which names the certificate problem (expired, self-signed, altname mismatch).",
|
|
18208
|
+
risk: "readonly",
|
|
18209
|
+
expects: "JSON report whose connection check fails with the TLS error text."
|
|
18210
|
+
}
|
|
18211
|
+
];
|
|
18212
|
+
}
|
|
18213
|
+
function connectionsExhaustedSteps(ctx) {
|
|
18214
|
+
const inspectSql = ctx.system === "postgresql" ? "SELECT count(*) FROM pg_stat_activity" : 'SHOW STATUS LIKE "Threads_connected"';
|
|
18215
|
+
return [
|
|
18216
|
+
{
|
|
18217
|
+
command: `dbcli query ${shellQuote(inspectSql)} --format json`,
|
|
18218
|
+
rationale: "Count the connections currently held so the limit can be compared against real usage; needs one free slot, so it may have to wait.",
|
|
18219
|
+
risk: "readonly",
|
|
18220
|
+
expects: "A single row with the current connection count."
|
|
18221
|
+
},
|
|
18222
|
+
{
|
|
18223
|
+
command: "dbcli doctor --format json",
|
|
18224
|
+
rationale: "Once a slot frees up, confirm the connection itself is healthy \u2014 the config was never the problem.",
|
|
18225
|
+
risk: "readonly",
|
|
18226
|
+
expects: "JSON report with the connection check passing."
|
|
18227
|
+
}
|
|
18228
|
+
];
|
|
18229
|
+
}
|
|
18084
18230
|
function stepsForCode(code, ctx) {
|
|
18085
18231
|
const drafts = draftsForCode(code, ctx);
|
|
18086
18232
|
return drafts.slice(0, MAX_RECOVERY_STEPS).map((d, i) => ({ ...d, order: i + 1 }));
|
|
@@ -18106,6 +18252,13 @@ function draftsForCode(code, ctx) {
|
|
|
18106
18252
|
case "CONN_REFUSED":
|
|
18107
18253
|
case "CONN_TIMEOUT":
|
|
18108
18254
|
case "CONN_UNKNOWN": {
|
|
18255
|
+
if (code === "CONN_TIMEOUT" && ctx.connectionCode === STATEMENT_TIMEOUT_CODE) {
|
|
18256
|
+
return statementTimeoutSteps();
|
|
18257
|
+
}
|
|
18258
|
+
if (ctx.connectionCode === "TLS_ERROR")
|
|
18259
|
+
return tlsErrorSteps();
|
|
18260
|
+
if (ctx.connectionCode === "TOO_MANY_CONNECTIONS")
|
|
18261
|
+
return connectionsExhaustedSteps(ctx);
|
|
18109
18262
|
const out = [
|
|
18110
18263
|
{
|
|
18111
18264
|
command: "dbcli doctor --format json",
|
|
@@ -18329,7 +18482,9 @@ function draftsForCode(code, ctx) {
|
|
|
18329
18482
|
}
|
|
18330
18483
|
}
|
|
18331
18484
|
var MAX_RECOVERY_STEPS = 6;
|
|
18332
|
-
var init_recovery_steps = () => {
|
|
18485
|
+
var init_recovery_steps = __esm(() => {
|
|
18486
|
+
init_types4();
|
|
18487
|
+
});
|
|
18333
18488
|
|
|
18334
18489
|
// src/utils/sql-lexical.ts
|
|
18335
18490
|
function dollarQuoteDelimiterAt(sql, index) {
|
|
@@ -19055,7 +19210,9 @@ var init_types5 = __esm(() => {
|
|
|
19055
19210
|
});
|
|
19056
19211
|
|
|
19057
19212
|
// src/core/recovery/verify-steps.ts
|
|
19058
|
-
function verifyForCode(code,
|
|
19213
|
+
function verifyForCode(code, ctx) {
|
|
19214
|
+
if (ctx.connectionCode === STATEMENT_TIMEOUT_CODE)
|
|
19215
|
+
return null;
|
|
19059
19216
|
const command = VERIFY_COMMAND_BY_CODE[code];
|
|
19060
19217
|
if (!command)
|
|
19061
19218
|
return null;
|
|
@@ -19069,6 +19226,7 @@ function verifyForCode(code, _ctx) {
|
|
|
19069
19226
|
}
|
|
19070
19227
|
var VERIFY_COMMAND_BY_CODE, VERIFY_RATIONALE_BY_CODE, VERIFY_EXPECTS_BY_CODE;
|
|
19071
19228
|
var init_verify_steps = __esm(() => {
|
|
19229
|
+
init_types4();
|
|
19072
19230
|
VERIFY_COMMAND_BY_CODE = {
|
|
19073
19231
|
CONFIG_MISSING: "dbcli inspect --no-connect --format json",
|
|
19074
19232
|
CONN_REFUSED: "dbcli doctor --format json",
|
|
@@ -19326,7 +19484,7 @@ function classifyError(error, ctx) {
|
|
|
19326
19484
|
const ctxWithDetails = applyDetailsToContext(ctx, recoveryError);
|
|
19327
19485
|
const recovery = stepsForCode(recoveryError.code, ctxWithDetails);
|
|
19328
19486
|
const verify = verifyForCode(recoveryError.code, ctxWithDetails);
|
|
19329
|
-
const branchExtras = recoveryError.category === "connection" ? buildConnectionBranches(ctxWithDetails) : null;
|
|
19487
|
+
const branchExtras = recoveryError.category === "connection" && ctxWithDetails.connectionCode !== STATEMENT_TIMEOUT_CODE ? buildConnectionBranches(ctxWithDetails) : null;
|
|
19330
19488
|
return {
|
|
19331
19489
|
schemaVersion: RECOVERY_SCHEMA_VERSION,
|
|
19332
19490
|
generatedAt: new Date().toISOString(),
|
|
@@ -19365,8 +19523,19 @@ function errorToRecoveryError(error, ctx) {
|
|
|
19365
19523
|
return baseError("UNKNOWN");
|
|
19366
19524
|
}
|
|
19367
19525
|
function classifyConnection(err) {
|
|
19368
|
-
const code = err.code
|
|
19369
|
-
|
|
19526
|
+
const code = RECOVERY_CODE_BY_CONNECTION_CODE[err.code];
|
|
19527
|
+
const base = baseError(code, { connectionCode: err.code });
|
|
19528
|
+
const override = MESSAGE_BY_CONNECTION_CODE[err.code];
|
|
19529
|
+
if (override)
|
|
19530
|
+
return { ...base, message: override };
|
|
19531
|
+
if (err.code === STATEMENT_TIMEOUT_CODE) {
|
|
19532
|
+
const ceiling = err.limitMs !== undefined ? ` The ceiling in force was ${err.limitMs}ms.` : "";
|
|
19533
|
+
return {
|
|
19534
|
+
...base,
|
|
19535
|
+
message: `The server canceled the statement for exceeding the statement timeout.${ceiling}`
|
|
19536
|
+
};
|
|
19537
|
+
}
|
|
19538
|
+
return base;
|
|
19370
19539
|
}
|
|
19371
19540
|
function classifyBlacklist(err) {
|
|
19372
19541
|
if (/touches blacklisted columns:/i.test(err.message)) {
|
|
@@ -19417,9 +19586,11 @@ function applyDetailsToContext(ctx, err) {
|
|
|
19417
19586
|
...ctx,
|
|
19418
19587
|
table: err.details?.table ?? ctx.table,
|
|
19419
19588
|
snippet: err.details?.snippet ?? ctx.snippet,
|
|
19420
|
-
hint: err.details?.paramName ?? ctx.hint
|
|
19589
|
+
hint: err.details?.paramName ?? ctx.hint,
|
|
19590
|
+
connectionCode: err.details?.connectionCode ?? ctx.connectionCode
|
|
19421
19591
|
};
|
|
19422
19592
|
}
|
|
19593
|
+
var RECOVERY_CODE_BY_CONNECTION_CODE, MESSAGE_BY_CONNECTION_CODE;
|
|
19423
19594
|
var init_classify = __esm(() => {
|
|
19424
19595
|
init_types2();
|
|
19425
19596
|
init_permission_guard();
|
|
@@ -19429,6 +19600,31 @@ var init_classify = __esm(() => {
|
|
|
19429
19600
|
init_verify_steps();
|
|
19430
19601
|
init_connection_branches();
|
|
19431
19602
|
init_types4();
|
|
19603
|
+
RECOVERY_CODE_BY_CONNECTION_CODE = {
|
|
19604
|
+
ECONNREFUSED: "CONN_REFUSED",
|
|
19605
|
+
CONNECTION_LOST: "CONN_REFUSED",
|
|
19606
|
+
TOO_MANY_CONNECTIONS: "CONN_REFUSED",
|
|
19607
|
+
SERVER_NOT_READY: "CONN_REFUSED",
|
|
19608
|
+
CONNECTION_REJECTED: "CONN_REFUSED",
|
|
19609
|
+
ETIMEDOUT: "CONN_TIMEOUT",
|
|
19610
|
+
STATEMENT_TIMEOUT: "CONN_TIMEOUT",
|
|
19611
|
+
AUTH_FAILED: "CONN_AUTH_FAILED",
|
|
19612
|
+
TLS_ERROR: "CONN_UNKNOWN",
|
|
19613
|
+
ENOTFOUND: "CONN_HOST_NOT_FOUND",
|
|
19614
|
+
EHOSTUNREACH: "CONN_HOST_NOT_FOUND",
|
|
19615
|
+
SQL_SYNTAX_ERROR: "CONN_UNKNOWN",
|
|
19616
|
+
TABLE_NOT_FOUND: "CONN_UNKNOWN",
|
|
19617
|
+
COLUMN_NOT_FOUND: "CONN_UNKNOWN",
|
|
19618
|
+
UNKNOWN: "CONN_UNKNOWN"
|
|
19619
|
+
};
|
|
19620
|
+
MESSAGE_BY_CONNECTION_CODE = {
|
|
19621
|
+
EHOSTUNREACH: "The host name resolved, but the host or network is unreachable (routing or VPN).",
|
|
19622
|
+
TOO_MANY_CONNECTIONS: "The server has no connection slots left; the limit is on concurrent connections, not on this caller.",
|
|
19623
|
+
CONNECTION_LOST: "The connection was established and then dropped mid-session.",
|
|
19624
|
+
TLS_ERROR: "The TLS handshake failed (certificate or trust chain).",
|
|
19625
|
+
SERVER_NOT_READY: "The server is starting up or recovering and is not accepting connections yet.",
|
|
19626
|
+
CONNECTION_REJECTED: "The server answered and rejected the connection attempt (access rules, pooler, or a per-user limit)."
|
|
19627
|
+
};
|
|
19432
19628
|
});
|
|
19433
19629
|
|
|
19434
19630
|
// src/core/recovery/render-json.ts
|
|
@@ -20579,6 +20775,7 @@ __export(exports_recovery, {
|
|
|
20579
20775
|
__resetExecutorForTests: () => __resetExecutorForTests,
|
|
20580
20776
|
SchemaCacheMissingError: () => SchemaCacheMissingError,
|
|
20581
20777
|
STEP_RESULT_SUMMARY_FIELD_CAP: () => STEP_RESULT_SUMMARY_FIELD_CAP,
|
|
20778
|
+
STATEMENT_TIMEOUT_CODE: () => STATEMENT_TIMEOUT_CODE,
|
|
20582
20779
|
RECOVERY_SCHEMA_VERSION: () => RECOVERY_SCHEMA_VERSION,
|
|
20583
20780
|
RECOVERY_CODE_METADATA: () => RECOVERY_CODE_METADATA,
|
|
20584
20781
|
RECOVERY_CODES: () => RECOVERY_CODES,
|
|
@@ -24498,6 +24695,45 @@ var init_saved_queries = __esm(() => {
|
|
|
24498
24695
|
init_snippet_paths();
|
|
24499
24696
|
});
|
|
24500
24697
|
|
|
24698
|
+
// src/utils/connection-error-message.ts
|
|
24699
|
+
function isTransportFailure(error) {
|
|
24700
|
+
if (error instanceof ConnectionError)
|
|
24701
|
+
return IS_TRANSPORT_FAILURE[error.code];
|
|
24702
|
+
const raw = error?.code;
|
|
24703
|
+
return typeof raw === "string" && isTransportDriverCode(raw);
|
|
24704
|
+
}
|
|
24705
|
+
function presentConnectionError(error) {
|
|
24706
|
+
const key = IS_TRANSPORT_FAILURE[error.code] ? "errors.connection_failed" : "errors.message";
|
|
24707
|
+
return {
|
|
24708
|
+
message: t_vars(key, { message: error.message }),
|
|
24709
|
+
code: error.code,
|
|
24710
|
+
hints: error.hints
|
|
24711
|
+
};
|
|
24712
|
+
}
|
|
24713
|
+
var IS_TRANSPORT_FAILURE;
|
|
24714
|
+
var init_connection_error_message = __esm(() => {
|
|
24715
|
+
init_types2();
|
|
24716
|
+
init_error_mapper();
|
|
24717
|
+
init_message_loader();
|
|
24718
|
+
IS_TRANSPORT_FAILURE = {
|
|
24719
|
+
ECONNREFUSED: true,
|
|
24720
|
+
ETIMEDOUT: true,
|
|
24721
|
+
AUTH_FAILED: true,
|
|
24722
|
+
ENOTFOUND: true,
|
|
24723
|
+
EHOSTUNREACH: true,
|
|
24724
|
+
CONNECTION_LOST: true,
|
|
24725
|
+
TOO_MANY_CONNECTIONS: true,
|
|
24726
|
+
TLS_ERROR: true,
|
|
24727
|
+
SERVER_NOT_READY: true,
|
|
24728
|
+
CONNECTION_REJECTED: true,
|
|
24729
|
+
STATEMENT_TIMEOUT: false,
|
|
24730
|
+
SQL_SYNTAX_ERROR: false,
|
|
24731
|
+
TABLE_NOT_FOUND: false,
|
|
24732
|
+
COLUMN_NOT_FOUND: false,
|
|
24733
|
+
UNKNOWN: false
|
|
24734
|
+
};
|
|
24735
|
+
});
|
|
24736
|
+
|
|
24501
24737
|
// src/commands/q-mongo.ts
|
|
24502
24738
|
var exports_q_mongo = {};
|
|
24503
24739
|
__export(exports_q_mongo, {
|
|
@@ -24834,7 +25070,7 @@ async function handleQError(error, snippetName, options, config) {
|
|
|
24834
25070
|
process.exit(1);
|
|
24835
25071
|
}
|
|
24836
25072
|
if (error instanceof ConnectionError) {
|
|
24837
|
-
printLocalizedCliError(
|
|
25073
|
+
printLocalizedCliError(formatCliError(presentConnectionError(error)), error);
|
|
24838
25074
|
process.exit(1);
|
|
24839
25075
|
}
|
|
24840
25076
|
printLocalizedCliError(t_vars("errors.message", { message: error.message }), error);
|
|
@@ -24952,6 +25188,7 @@ var init_q = __esm(() => {
|
|
|
24952
25188
|
init_colors();
|
|
24953
25189
|
init_applied_limit();
|
|
24954
25190
|
init_cli_error();
|
|
25191
|
+
init_connection_error_message();
|
|
24955
25192
|
init_strategies();
|
|
24956
25193
|
init_slow_query_advisory();
|
|
24957
25194
|
});
|
|
@@ -26197,7 +26434,7 @@ async function insertCommand(table, options, command) {
|
|
|
26197
26434
|
process.exit(1);
|
|
26198
26435
|
}
|
|
26199
26436
|
if (error instanceof ConnectionError) {
|
|
26200
|
-
printLocalizedCliError(
|
|
26437
|
+
printLocalizedCliError(formatCliError(presentConnectionError(error)), error);
|
|
26201
26438
|
process.exit(1);
|
|
26202
26439
|
}
|
|
26203
26440
|
const output = {
|
|
@@ -26213,6 +26450,7 @@ async function insertCommand(table, options, command) {
|
|
|
26213
26450
|
var init_insert = __esm(() => {
|
|
26214
26451
|
init_message_loader();
|
|
26215
26452
|
init_cli_error();
|
|
26453
|
+
init_connection_error_message();
|
|
26216
26454
|
init_adapters();
|
|
26217
26455
|
init_data_executor();
|
|
26218
26456
|
init_config();
|
|
@@ -26504,7 +26742,7 @@ async function updateCommand(table, options, command) {
|
|
|
26504
26742
|
process.exit(1);
|
|
26505
26743
|
}
|
|
26506
26744
|
if (error instanceof ConnectionError) {
|
|
26507
|
-
printLocalizedCliError(
|
|
26745
|
+
printLocalizedCliError(formatCliError(presentConnectionError(error)), error);
|
|
26508
26746
|
process.exit(1);
|
|
26509
26747
|
}
|
|
26510
26748
|
const output = {
|
|
@@ -26520,6 +26758,7 @@ async function updateCommand(table, options, command) {
|
|
|
26520
26758
|
var init_update = __esm(() => {
|
|
26521
26759
|
init_message_loader();
|
|
26522
26760
|
init_cli_error();
|
|
26761
|
+
init_connection_error_message();
|
|
26523
26762
|
init_adapters();
|
|
26524
26763
|
init_data_executor();
|
|
26525
26764
|
init_config();
|
|
@@ -26795,7 +27034,7 @@ async function deleteCommand(table, options, command) {
|
|
|
26795
27034
|
process.exit(1);
|
|
26796
27035
|
}
|
|
26797
27036
|
if (error instanceof ConnectionError) {
|
|
26798
|
-
printLocalizedCliError(
|
|
27037
|
+
printLocalizedCliError(formatCliError(presentConnectionError(error)), error);
|
|
26799
27038
|
process.exit(1);
|
|
26800
27039
|
}
|
|
26801
27040
|
const output = {
|
|
@@ -26811,6 +27050,7 @@ async function deleteCommand(table, options, command) {
|
|
|
26811
27050
|
var init_delete = __esm(() => {
|
|
26812
27051
|
init_message_loader();
|
|
26813
27052
|
init_cli_error();
|
|
27053
|
+
init_connection_error_message();
|
|
26814
27054
|
init_adapters();
|
|
26815
27055
|
init_data_executor();
|
|
26816
27056
|
init_config();
|
|
@@ -43842,13 +44082,12 @@ class ReplEngine {
|
|
|
43842
44082
|
return null;
|
|
43843
44083
|
}
|
|
43844
44084
|
isConnectionError(error) {
|
|
43845
|
-
|
|
43846
|
-
const msg = (e.message ?? "").toLowerCase();
|
|
43847
|
-
return e.code === "ECONNREFUSED" || e.code === "ECONNRESET" || e.code === "ETIMEDOUT" || msg.includes("connection") || msg.includes("terminated") || msg.includes("socket");
|
|
44085
|
+
return isTransportFailure(error);
|
|
43848
44086
|
}
|
|
43849
44087
|
}
|
|
43850
44088
|
var import_picocolors3;
|
|
43851
44089
|
var init_repl_engine = __esm(() => {
|
|
44090
|
+
init_connection_error_message();
|
|
43852
44091
|
init_input_classifier();
|
|
43853
44092
|
init_meta_commands();
|
|
43854
44093
|
init_history_manager();
|
package/dist/cli.mjs
CHANGED
package/dist/core.d.ts
CHANGED
|
@@ -129,21 +129,31 @@ export interface TableSchema {
|
|
|
129
129
|
/** Type of table (table or view) */
|
|
130
130
|
tableType?: "table" | "view";
|
|
131
131
|
}
|
|
132
|
-
|
|
133
|
-
* Connection error with categorized error code and troubleshooting hints
|
|
134
|
-
*/
|
|
132
|
+
type ConnectionErrorCode = "ECONNREFUSED" | "ETIMEDOUT" | "AUTH_FAILED" | "ENOTFOUND" | "EHOSTUNREACH" | "CONNECTION_LOST" | "TOO_MANY_CONNECTIONS" | "TLS_ERROR" | "SERVER_NOT_READY" | "CONNECTION_REJECTED" | "SQL_SYNTAX_ERROR" | "STATEMENT_TIMEOUT" | "TABLE_NOT_FOUND" | "COLUMN_NOT_FOUND" | "UNKNOWN";
|
|
135
133
|
export declare class ConnectionError extends Error {
|
|
136
134
|
/** Error category code */
|
|
137
|
-
code:
|
|
135
|
+
code: ConnectionErrorCode;
|
|
138
136
|
/** Array of actionable troubleshooting hints */
|
|
139
137
|
hints: string[];
|
|
138
|
+
/**
|
|
139
|
+
* The ceiling that was in force, in milliseconds. Set on STATEMENT_TIMEOUT so
|
|
140
|
+
* consumers can state it without parsing `message`; the recovery envelope needs
|
|
141
|
+
* it to tell an agent what `--statement-timeout <ms>` it was already up against.
|
|
142
|
+
*/
|
|
143
|
+
limitMs?: number | undefined;
|
|
140
144
|
constructor(
|
|
141
145
|
/** Error category code */
|
|
142
|
-
code:
|
|
146
|
+
code: ConnectionErrorCode,
|
|
143
147
|
/** User-friendly error message */
|
|
144
148
|
message: string,
|
|
145
149
|
/** Array of actionable troubleshooting hints */
|
|
146
|
-
hints: string[]
|
|
150
|
+
hints: string[],
|
|
151
|
+
/**
|
|
152
|
+
* The ceiling that was in force, in milliseconds. Set on STATEMENT_TIMEOUT so
|
|
153
|
+
* consumers can state it without parsing `message`; the recovery envelope needs
|
|
154
|
+
* it to tell an agent what `--statement-timeout <ms>` it was already up against.
|
|
155
|
+
*/
|
|
156
|
+
limitMs?: number | undefined);
|
|
147
157
|
}
|
|
148
158
|
/**
|
|
149
159
|
* Result of a database query or command execution
|
package/dist/core.mjs
CHANGED
|
@@ -1091,10 +1091,12 @@ var init_schema_loader = __esm(() => {
|
|
|
1091
1091
|
class ConnectionError extends Error {
|
|
1092
1092
|
code;
|
|
1093
1093
|
hints;
|
|
1094
|
-
|
|
1094
|
+
limitMs;
|
|
1095
|
+
constructor(code, message, hints, limitMs) {
|
|
1095
1096
|
super(message);
|
|
1096
1097
|
this.code = code;
|
|
1097
1098
|
this.hints = hints;
|
|
1099
|
+
this.limitMs = limitMs;
|
|
1098
1100
|
this.name = "ConnectionError";
|
|
1099
1101
|
Object.setPrototypeOf(this, ConnectionError.prototype);
|
|
1100
1102
|
}
|
|
@@ -1250,9 +1252,16 @@ var TRANSPORT_CODES = {
|
|
|
1250
1252
|
ESOCKETTIMEDOUT: "ETIMEDOUT",
|
|
1251
1253
|
ENOTFOUND: "ENOTFOUND",
|
|
1252
1254
|
EAI_AGAIN: "ENOTFOUND",
|
|
1253
|
-
PROTOCOL_CONNECTION_LOST: "
|
|
1254
|
-
PROTOCOL_SEQUENCE_TIMEOUT: "ETIMEDOUT"
|
|
1255
|
+
PROTOCOL_CONNECTION_LOST: "CONNECTION_LOST",
|
|
1256
|
+
PROTOCOL_SEQUENCE_TIMEOUT: "ETIMEDOUT",
|
|
1257
|
+
ECONNRESET: "CONNECTION_LOST",
|
|
1258
|
+
EPIPE: "CONNECTION_LOST",
|
|
1259
|
+
ECONNABORTED: "CONNECTION_LOST",
|
|
1260
|
+
EHOSTUNREACH: "EHOSTUNREACH",
|
|
1261
|
+
ENETUNREACH: "EHOSTUNREACH",
|
|
1262
|
+
ENETDOWN: "EHOSTUNREACH"
|
|
1255
1263
|
};
|
|
1264
|
+
var TLS_CODE_PREFIX = /^(CERT_|SELF_SIGNED_|DEPTH_ZERO_|UNABLE_TO_(?:GET|VERIFY)_|ERR_TLS_|ERR_SSL_)/;
|
|
1256
1265
|
var MYSQL_CODES = {
|
|
1257
1266
|
ER_NO_SUCH_TABLE: "TABLE_NOT_FOUND",
|
|
1258
1267
|
ER_BAD_FIELD_ERROR: "COLUMN_NOT_FOUND",
|
|
@@ -1260,7 +1269,11 @@ var MYSQL_CODES = {
|
|
|
1260
1269
|
ER_ACCESS_DENIED_ERROR: "AUTH_FAILED",
|
|
1261
1270
|
ER_DBACCESS_DENIED_ERROR: "AUTH_FAILED",
|
|
1262
1271
|
ER_NOT_SUPPORTED_AUTH_MODE: "AUTH_FAILED",
|
|
1263
|
-
ER_MUST_CHANGE_PASSWORD_LOGIN: "AUTH_FAILED"
|
|
1272
|
+
ER_MUST_CHANGE_PASSWORD_LOGIN: "AUTH_FAILED",
|
|
1273
|
+
ER_QUERY_TIMEOUT: "STATEMENT_TIMEOUT",
|
|
1274
|
+
ER_STATEMENT_TIMEOUT: "STATEMENT_TIMEOUT",
|
|
1275
|
+
ER_CON_COUNT_ERROR: "TOO_MANY_CONNECTIONS",
|
|
1276
|
+
ER_SERVER_SHUTDOWN: "CONNECTION_LOST"
|
|
1264
1277
|
};
|
|
1265
1278
|
var MYSQL_ERRNOS = {
|
|
1266
1279
|
1045: "AUTH_FAILED",
|
|
@@ -1269,14 +1282,30 @@ var MYSQL_ERRNOS = {
|
|
|
1269
1282
|
1064: "SQL_SYNTAX_ERROR",
|
|
1270
1283
|
1146: "TABLE_NOT_FOUND",
|
|
1271
1284
|
1251: "AUTH_FAILED",
|
|
1272
|
-
1698: "AUTH_FAILED"
|
|
1285
|
+
1698: "AUTH_FAILED",
|
|
1286
|
+
1040: "TOO_MANY_CONNECTIONS",
|
|
1287
|
+
1053: "CONNECTION_LOST",
|
|
1288
|
+
2006: "CONNECTION_LOST",
|
|
1289
|
+
2013: "CONNECTION_LOST",
|
|
1290
|
+
1969: "STATEMENT_TIMEOUT",
|
|
1291
|
+
3024: "STATEMENT_TIMEOUT"
|
|
1273
1292
|
};
|
|
1274
1293
|
var SQLSTATES = {
|
|
1275
1294
|
"28000": "AUTH_FAILED",
|
|
1276
1295
|
"28P01": "AUTH_FAILED",
|
|
1277
1296
|
"42601": "SQL_SYNTAX_ERROR",
|
|
1278
1297
|
"42P01": "TABLE_NOT_FOUND",
|
|
1279
|
-
"42703": "COLUMN_NOT_FOUND"
|
|
1298
|
+
"42703": "COLUMN_NOT_FOUND",
|
|
1299
|
+
"57014": "STATEMENT_TIMEOUT",
|
|
1300
|
+
"08000": "CONNECTION_LOST",
|
|
1301
|
+
"08003": "CONNECTION_LOST",
|
|
1302
|
+
"08006": "CONNECTION_LOST",
|
|
1303
|
+
"08001": "ECONNREFUSED",
|
|
1304
|
+
"08004": "CONNECTION_REJECTED",
|
|
1305
|
+
"53300": "TOO_MANY_CONNECTIONS",
|
|
1306
|
+
"57P01": "CONNECTION_LOST",
|
|
1307
|
+
"57P02": "CONNECTION_LOST",
|
|
1308
|
+
"57P03": "SERVER_NOT_READY"
|
|
1280
1309
|
};
|
|
1281
1310
|
var REDIS_PREFIXES = {
|
|
1282
1311
|
NOAUTH: "AUTH_FAILED",
|
|
@@ -1288,6 +1317,10 @@ var FALLBACK_PATTERNS = [
|
|
|
1288
1317
|
[/connect(?:ion)? time(?:d )?out/i, "ETIMEDOUT"],
|
|
1289
1318
|
[/timeout exceeded/i, "ETIMEDOUT"],
|
|
1290
1319
|
[/timed out/i, "ETIMEDOUT"],
|
|
1320
|
+
[/connection terminated/i, "CONNECTION_LOST"],
|
|
1321
|
+
[/server closed the connection/i, "CONNECTION_LOST"],
|
|
1322
|
+
[/server has gone away/i, "CONNECTION_LOST"],
|
|
1323
|
+
[/lost connection to \S+ server/i, "CONNECTION_LOST"],
|
|
1291
1324
|
[/getaddrinfo/i, "ENOTFOUND"],
|
|
1292
1325
|
[/authentication failed/i, "AUTH_FAILED"],
|
|
1293
1326
|
[/access denied for user/i, "AUTH_FAILED"],
|
|
@@ -1295,18 +1328,26 @@ var FALLBACK_PATTERNS = [
|
|
|
1295
1328
|
[/role\s+".*?"\s+does not exist/i, "AUTH_FAILED"],
|
|
1296
1329
|
[/password supplied/i, "AUTH_FAILED"]
|
|
1297
1330
|
];
|
|
1298
|
-
|
|
1331
|
+
var PG_CANCEL_BY_TIMEOUT = /statement timeout/i;
|
|
1332
|
+
function categorize(errCode, errno, errMsg, system) {
|
|
1299
1333
|
if (errCode) {
|
|
1334
|
+
if (TLS_CODE_PREFIX.test(errCode))
|
|
1335
|
+
return "TLS_ERROR";
|
|
1300
1336
|
const known = TRANSPORT_CODES[errCode] ?? MYSQL_CODES[errCode] ?? SQLSTATES[errCode] ?? REDIS_PREFIXES[errCode];
|
|
1337
|
+
if (known === "STATEMENT_TIMEOUT" && errCode === "57014" && !PG_CANCEL_BY_TIMEOUT.test(errMsg)) {
|
|
1338
|
+
return null;
|
|
1339
|
+
}
|
|
1301
1340
|
if (known)
|
|
1302
1341
|
return known;
|
|
1303
1342
|
}
|
|
1304
|
-
|
|
1343
|
+
const isMysqlFamily = system === "mysql" || system === "mariadb";
|
|
1344
|
+
if (isMysqlFamily && typeof errno === "number" && MYSQL_ERRNOS[errno]) {
|
|
1305
1345
|
return MYSQL_ERRNOS[errno];
|
|
1346
|
+
}
|
|
1306
1347
|
const redisPrefix = errMsg.match(/^([A-Z]+)\s/)?.[1];
|
|
1307
1348
|
if (redisPrefix && REDIS_PREFIXES[redisPrefix])
|
|
1308
1349
|
return REDIS_PREFIXES[redisPrefix];
|
|
1309
|
-
if (errCode || typeof errno === "number")
|
|
1350
|
+
if (errCode || isMysqlFamily && typeof errno === "number")
|
|
1310
1351
|
return null;
|
|
1311
1352
|
for (const [pattern, category] of FALLBACK_PATTERNS) {
|
|
1312
1353
|
if (pattern.test(errMsg))
|
|
@@ -1359,7 +1400,7 @@ function mapError(error, system, options) {
|
|
|
1359
1400
|
const errMsg = String(err?.message || String(error));
|
|
1360
1401
|
const errCode = String(err?.code || "");
|
|
1361
1402
|
const errno = err?.errno;
|
|
1362
|
-
switch (categorize(errCode, errno, errMsg)) {
|
|
1403
|
+
switch (categorize(errCode, errno, errMsg, system)) {
|
|
1363
1404
|
case "ECONNREFUSED":
|
|
1364
1405
|
return new ConnectionError("ECONNREFUSED", `Cannot connect to ${options.host}:${options.port} \u2014 server is not running or not listening on this port`, serviceHints(system, options));
|
|
1365
1406
|
case "ETIMEDOUT":
|
|
@@ -1368,6 +1409,42 @@ function mapError(error, system, options) {
|
|
|
1368
1409
|
`Increase timeout: edit .dbcli and add "timeout": 15000`,
|
|
1369
1410
|
`Verify network connectivity: ping ${options.host} -c 3`
|
|
1370
1411
|
]);
|
|
1412
|
+
case "CONNECTION_LOST":
|
|
1413
|
+
return new ConnectionError("CONNECTION_LOST", `Connection to ${options.host}:${options.port} was lost mid-session \u2014 the server closed it or the network dropped`, [
|
|
1414
|
+
"Re-run the command: a dropped connection is often transient",
|
|
1415
|
+
`Check whether the server restarted or is cycling: ${SYSTEM_FACTS[system].logFile}`,
|
|
1416
|
+
"If it happens on long-running work, look at the server idle/wait timeout"
|
|
1417
|
+
]);
|
|
1418
|
+
case "SERVER_NOT_READY":
|
|
1419
|
+
return new ConnectionError("SERVER_NOT_READY", `${options.host}:${options.port} is not accepting connections yet \u2014 the server is starting up or recovering`, [
|
|
1420
|
+
"Retry shortly: this clears on its own once startup or recovery finishes",
|
|
1421
|
+
`Watch progress in the server log: ${SYSTEM_FACTS[system].logFile}`,
|
|
1422
|
+
"On a replica, this also appears while it catches up with the primary"
|
|
1423
|
+
]);
|
|
1424
|
+
case "CONNECTION_REJECTED":
|
|
1425
|
+
return new ConnectionError("CONNECTION_REJECTED", `${options.host}:${options.port} answered but rejected the connection attempt`, [
|
|
1426
|
+
"Check the server access rules (pg_hba.conf) for this user, database, and client address",
|
|
1427
|
+
"If a pooler (pgbouncer / ProxySQL) sits in front, check its own rules and limits",
|
|
1428
|
+
"Check whether the connection limit for this user or database is exhausted"
|
|
1429
|
+
]);
|
|
1430
|
+
case "TOO_MANY_CONNECTIONS":
|
|
1431
|
+
return new ConnectionError("TOO_MANY_CONNECTIONS", `${options.host}:${options.port} refused the connection \u2014 the server has no connection slots left`, [
|
|
1432
|
+
"Wait and retry: the limit is on concurrent connections, not on you",
|
|
1433
|
+
system === "postgresql" ? "Inspect usage: SELECT count(*) FROM pg_stat_activity \u2014 compare against max_connections" : 'Inspect usage: SHOW STATUS LIKE "Threads_connected" \u2014 compare against max_connections',
|
|
1434
|
+
"Close idle sessions, or raise max_connections if the load is legitimate"
|
|
1435
|
+
]);
|
|
1436
|
+
case "EHOSTUNREACH":
|
|
1437
|
+
return new ConnectionError("EHOSTUNREACH", `No route to ${options.host} \u2014 the name resolved, but the host or network is unreachable`, [
|
|
1438
|
+
"Check that you are on the right network or VPN",
|
|
1439
|
+
`Check routing and firewall between here and ${options.host}`,
|
|
1440
|
+
`Confirm the address is the one you meant: ${options.host}:${options.port}`
|
|
1441
|
+
]);
|
|
1442
|
+
case "TLS_ERROR":
|
|
1443
|
+
return new ConnectionError("TLS_ERROR", `TLS handshake failed: ${errMsg}`, [
|
|
1444
|
+
'Point the connection at the right CA bundle: set "caPath" in .dbcli',
|
|
1445
|
+
'For a self-signed certificate in a trusted network, set "rejectUnauthorized": false',
|
|
1446
|
+
`Confirm the certificate covers the host you connected to: ${options.host}`
|
|
1447
|
+
]);
|
|
1371
1448
|
case "ENOTFOUND":
|
|
1372
1449
|
return new ConnectionError("ENOTFOUND", `Host not found: ${options.host}`, [
|
|
1373
1450
|
`Check the hostname spelling: ${options.host}`,
|
|
@@ -1398,6 +1475,14 @@ function mapError(error, system, options) {
|
|
|
1398
1475
|
"Column names are case-sensitive on some databases (PostgreSQL with quoted identifiers)"
|
|
1399
1476
|
]);
|
|
1400
1477
|
}
|
|
1478
|
+
case "STATEMENT_TIMEOUT": {
|
|
1479
|
+
const limitMs = options.statementTimeout ?? options.timeout;
|
|
1480
|
+
return new ConnectionError("STATEMENT_TIMEOUT", `Statement timed out${limitMs ? ` (${limitMs}ms)` : ""} \u2014 the server canceled this query before it finished`, [
|
|
1481
|
+
'Inspect the query plan: dbcli explain "<sql>"',
|
|
1482
|
+
"Raise the ceiling for this run: dbcli --statement-timeout <ms> \u2026 (0 removes it)",
|
|
1483
|
+
"Narrow the query: add WHERE filters, reduce the LIMIT, or index the scanned columns"
|
|
1484
|
+
], limitMs);
|
|
1485
|
+
}
|
|
1401
1486
|
case "SQL_SYNTAX_ERROR":
|
|
1402
1487
|
return new ConnectionError("SQL_SYNTAX_ERROR", `SQL syntax error: ${errMsg}`, [
|
|
1403
1488
|
"Check your SQL syntax near the position reported above",
|
package/package.json
CHANGED
|
@@ -1660,6 +1660,21 @@ shape (`schemaVersion: 1`):
|
|
|
1660
1660
|
Recovery codes (fixed in v1.15.0):
|
|
1661
1661
|
- `CONFIG_MISSING` — no `.dbcli` config; run `dbcli init`.
|
|
1662
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).
|
|
1663
1678
|
- `PERMISSION_DENIED` — active permission level forbids the operation.
|
|
1664
1679
|
- `BLACKLIST_TABLE` / `BLACKLIST_COLUMN_WRITE` — blacklist violations.
|
|
1665
1680
|
- `SNIPPET_NOT_FOUND` / `SNIPPET_AMBIGUOUS` / `SNIPPET_PARAM_MISSING` — saved-query failures.
|
|
@@ -1660,6 +1660,21 @@ shape (`schemaVersion: 1`):
|
|
|
1660
1660
|
Recovery codes (fixed in v1.15.0):
|
|
1661
1661
|
- `CONFIG_MISSING` — no `.dbcli` config; run `dbcli init`.
|
|
1662
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).
|
|
1663
1678
|
- `PERMISSION_DENIED` — active permission level forbids the operation.
|
|
1664
1679
|
- `BLACKLIST_TABLE` / `BLACKLIST_COLUMN_WRITE` — blacklist violations.
|
|
1665
1680
|
- `SNIPPET_NOT_FOUND` / `SNIPPET_AMBIGUOUS` / `SNIPPET_PARAM_MISSING` — saved-query failures.
|