@carllee1983/dbcli 1.42.0 → 1.43.0
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/rules/dbcli.mdc +29 -5
- package/.cursor/skills/dbcli/reference.md +79 -4
- package/.cursor-plugin/plugin.json +1 -1
- package/.github/skills/dbcli/SKILL.md +29 -5
- package/.github/skills/dbcli/reference.md +79 -4
- package/CHANGELOG.md +32 -0
- package/README.dev.md +1 -1
- package/README.md +72 -10
- package/README.zh-TW.md +70 -10
- package/assets/SKILL.md +29 -5
- package/assets/SKILL.zh-TW.md +27 -4
- package/assets/reference.md +79 -4
- package/assets/ui-template.html +19 -19
- package/dist/agent-core.d.ts +32 -0
- package/dist/agent-core.mjs +92 -0
- package/dist/cli.mjs +1539 -686
- package/dist/core.d.ts +23 -0
- package/dist/core.mjs +323 -76
- package/dist/ui-style.css +1 -1
- package/gemini-extension.json +1 -1
- package/package.json +9 -3
- package/plugins/dbcli-agent/.codex-plugin/plugin.json +1 -1
- package/plugins/dbcli-agent/skills/dbcli/SKILL.md +29 -5
- package/plugins/dbcli-agent/skills/dbcli/reference.md +79 -4
- package/skills/dbcli/SKILL.md +29 -5
- package/skills/dbcli/reference.md +79 -4
package/dist/cli.mjs
CHANGED
|
@@ -52,7 +52,7 @@ var package_default;
|
|
|
52
52
|
var init_package = __esm(() => {
|
|
53
53
|
package_default = {
|
|
54
54
|
name: "@carllee1983/dbcli",
|
|
55
|
-
version: "1.
|
|
55
|
+
version: "1.43.0",
|
|
56
56
|
description: "Database CLI for AI agents",
|
|
57
57
|
type: "module",
|
|
58
58
|
publishConfig: {
|
|
@@ -66,6 +66,10 @@ var init_package = __esm(() => {
|
|
|
66
66
|
"./core": {
|
|
67
67
|
types: "./dist/core.d.ts",
|
|
68
68
|
import: "./dist/core.mjs"
|
|
69
|
+
},
|
|
70
|
+
"./agent-core": {
|
|
71
|
+
types: "./dist/agent-core.d.ts",
|
|
72
|
+
import: "./dist/agent-core.mjs"
|
|
69
73
|
}
|
|
70
74
|
},
|
|
71
75
|
license: "MIT",
|
|
@@ -123,6 +127,7 @@ var init_package = __esm(() => {
|
|
|
123
127
|
"docs:check": "bun run scripts/check-user-docs.ts",
|
|
124
128
|
"skill:check": "bun run scripts/check-skill-parity.ts",
|
|
125
129
|
"platform:check": "bun run scripts/check-platform-parity.ts",
|
|
130
|
+
"agent-core:check": "bun run scripts/check-agent-core-purity.ts",
|
|
126
131
|
typecheck: "tsc --noEmit --pretty false",
|
|
127
132
|
"test:perf": "bun test ./tests/perf/*.bench.ts",
|
|
128
133
|
lint: "eslint src tests scripts --ext .ts --max-warnings=0",
|
|
@@ -158,14 +163,15 @@ var init_package = __esm(() => {
|
|
|
158
163
|
"dts-bundle-generator": "^9.5.1",
|
|
159
164
|
eslint: "^10.4.0",
|
|
160
165
|
"happy-dom": "^20.10.6",
|
|
161
|
-
postcss: "^8.5.
|
|
166
|
+
postcss: "^8.5.25",
|
|
162
167
|
prettier: "^3.8.3",
|
|
163
168
|
tailwindcss: "3.4.1",
|
|
164
169
|
typescript: "^5.9.3",
|
|
165
170
|
"typescript-eslint": "^8.59.4"
|
|
166
171
|
},
|
|
167
172
|
overrides: {
|
|
168
|
-
"brace-expansion": "^5.0.
|
|
173
|
+
"brace-expansion": "^5.0.9",
|
|
174
|
+
postcss: "^8.5.25"
|
|
169
175
|
}
|
|
170
176
|
};
|
|
171
177
|
});
|
|
@@ -7110,24 +7116,6 @@ var init_zod = __esm(() => {
|
|
|
7110
7116
|
});
|
|
7111
7117
|
|
|
7112
7118
|
// src/utils/validation.ts
|
|
7113
|
-
var exports_validation = {};
|
|
7114
|
-
__export(exports_validation, {
|
|
7115
|
-
validateFormat: () => validateFormat,
|
|
7116
|
-
RedisMaskRuleSchema: () => RedisMaskRuleSchema,
|
|
7117
|
-
RedisConnectionConfigSchema: () => RedisConnectionConfigSchema,
|
|
7118
|
-
RedisConfigSchema: () => RedisConfigSchema,
|
|
7119
|
-
PermissionSchema: () => PermissionSchema,
|
|
7120
|
-
NamedConnectionSchema: () => NamedConnectionSchema,
|
|
7121
|
-
MongoDBConnectionConfigSchema: () => MongoDBConnectionConfigSchema,
|
|
7122
|
-
MetadataSchema: () => MetadataSchema,
|
|
7123
|
-
ElasticsearchConnectionConfigSchema: () => ElasticsearchConnectionConfigSchema,
|
|
7124
|
-
DbcliConfigV2Schema: () => DbcliConfigV2Schema,
|
|
7125
|
-
DbcliConfigSchema: () => DbcliConfigSchema,
|
|
7126
|
-
ConnectionConfigSchema: () => ConnectionConfigSchema,
|
|
7127
|
-
BlacklistConfigSchema: () => BlacklistConfigSchema,
|
|
7128
|
-
AuditRotationConfigSchema: () => AuditRotationConfigSchema,
|
|
7129
|
-
AuditConfigSchema: () => AuditConfigSchema
|
|
7130
|
-
});
|
|
7131
7119
|
function validateFormat(value, allowedFormats, commandName) {
|
|
7132
7120
|
if (!allowedFormats.includes(value)) {
|
|
7133
7121
|
const allowed = allowedFormats.join(", ");
|
|
@@ -7267,30 +7255,35 @@ var init_validation = __esm(() => {
|
|
|
7267
7255
|
});
|
|
7268
7256
|
});
|
|
7269
7257
|
|
|
7270
|
-
// src/
|
|
7271
|
-
var
|
|
7258
|
+
// src/agent-core/errors.ts
|
|
7259
|
+
var ConfigError;
|
|
7272
7260
|
var init_errors2 = __esm(() => {
|
|
7273
|
-
|
|
7261
|
+
ConfigError = class ConfigError extends Error {
|
|
7274
7262
|
constructor(message) {
|
|
7275
7263
|
super(message);
|
|
7276
|
-
this.name = "
|
|
7277
|
-
if (Error.captureStackTrace)
|
|
7278
|
-
Error.captureStackTrace(this,
|
|
7279
|
-
}
|
|
7264
|
+
this.name = "ConfigError";
|
|
7265
|
+
if (Error.captureStackTrace)
|
|
7266
|
+
Error.captureStackTrace(this, ConfigError);
|
|
7280
7267
|
}
|
|
7281
7268
|
};
|
|
7282
|
-
|
|
7269
|
+
});
|
|
7270
|
+
|
|
7271
|
+
// src/utils/errors.ts
|
|
7272
|
+
var EnvParseError;
|
|
7273
|
+
var init_errors3 = __esm(() => {
|
|
7274
|
+
init_errors2();
|
|
7275
|
+
EnvParseError = class EnvParseError extends Error {
|
|
7283
7276
|
constructor(message) {
|
|
7284
7277
|
super(message);
|
|
7285
|
-
this.name = "
|
|
7278
|
+
this.name = "EnvParseError";
|
|
7286
7279
|
if (Error.captureStackTrace) {
|
|
7287
|
-
Error.captureStackTrace(this,
|
|
7280
|
+
Error.captureStackTrace(this, EnvParseError);
|
|
7288
7281
|
}
|
|
7289
7282
|
}
|
|
7290
7283
|
};
|
|
7291
7284
|
});
|
|
7292
7285
|
|
|
7293
|
-
// src/core/env-loader.ts
|
|
7286
|
+
// src/agent-core/env-loader.ts
|
|
7294
7287
|
function parseEnvContent(content) {
|
|
7295
7288
|
const entries = [];
|
|
7296
7289
|
for (const line of content.split(`
|
|
@@ -7312,22 +7305,22 @@ function parseEnvContent(content) {
|
|
|
7312
7305
|
}
|
|
7313
7306
|
async function loadEnvFile(filePath) {
|
|
7314
7307
|
const file = Bun.file(filePath);
|
|
7315
|
-
|
|
7316
|
-
if (!exists) {
|
|
7308
|
+
if (!await file.exists())
|
|
7317
7309
|
throw new ConfigError(`\u627E\u4E0D\u5230 env \u6A94\u6848\uFF1A${filePath}`);
|
|
7318
|
-
|
|
7319
|
-
|
|
7320
|
-
const entries = parseEnvContent(content);
|
|
7321
|
-
for (const [key, value] of entries) {
|
|
7322
|
-
if (process.env[key] === undefined) {
|
|
7310
|
+
for (const [key, value] of parseEnvContent(await file.text())) {
|
|
7311
|
+
if (process.env[key] === undefined)
|
|
7323
7312
|
process.env[key] = value;
|
|
7324
|
-
}
|
|
7325
7313
|
}
|
|
7326
7314
|
}
|
|
7327
7315
|
var init_env_loader = __esm(() => {
|
|
7328
7316
|
init_errors2();
|
|
7329
7317
|
});
|
|
7330
7318
|
|
|
7319
|
+
// src/core/env-loader.ts
|
|
7320
|
+
var init_env_loader2 = __esm(() => {
|
|
7321
|
+
init_env_loader();
|
|
7322
|
+
});
|
|
7323
|
+
|
|
7331
7324
|
// src/core/config-binding.ts
|
|
7332
7325
|
import { createHash } from "crypto";
|
|
7333
7326
|
import { mkdir, unlink } from "fs/promises";
|
|
@@ -7464,9 +7457,66 @@ async function patchConnectionSchema(dbcliPath, connectionName, schema, metadata
|
|
|
7464
7457
|
}
|
|
7465
7458
|
var init_config_v2 = __esm(() => {
|
|
7466
7459
|
init_validation();
|
|
7460
|
+
init_errors3();
|
|
7461
|
+
init_env_loader2();
|
|
7462
|
+
init_config_binding();
|
|
7463
|
+
});
|
|
7464
|
+
|
|
7465
|
+
// src/agent-core/applied-limit.ts
|
|
7466
|
+
function trimAppliedLimit(rows, limit) {
|
|
7467
|
+
const truncated = rows.length > limit;
|
|
7468
|
+
return {
|
|
7469
|
+
rows: truncated ? rows.slice(0, limit) : [...rows],
|
|
7470
|
+
metadata: { truncated, limitApplied: limit }
|
|
7471
|
+
};
|
|
7472
|
+
}
|
|
7473
|
+
|
|
7474
|
+
// src/agent-core/connection-selector.ts
|
|
7475
|
+
function resolveConnectionSelector(inputs) {
|
|
7476
|
+
if (inputs.root !== undefined && inputs.command !== undefined && inputs.root !== inputs.command) {
|
|
7477
|
+
throw new Error(`Conflicting connection selectors: root value '${inputs.root}' does not match command value '${inputs.command}'`);
|
|
7478
|
+
}
|
|
7479
|
+
const explicit = inputs.command ?? inputs.root;
|
|
7480
|
+
if (explicit !== undefined)
|
|
7481
|
+
return explicit;
|
|
7482
|
+
const environment = inputs.environment?.trim();
|
|
7483
|
+
return environment ? environment : undefined;
|
|
7484
|
+
}
|
|
7485
|
+
function parseConnectionNames(selector) {
|
|
7486
|
+
const names = selector.split(",").map((name2) => name2.trim());
|
|
7487
|
+
if (names.some((name2) => name2 === "")) {
|
|
7488
|
+
throw new Error("Connection selector contains an empty connection name");
|
|
7489
|
+
}
|
|
7490
|
+
const seen = new Set;
|
|
7491
|
+
for (const name2 of names) {
|
|
7492
|
+
if (seen.has(name2)) {
|
|
7493
|
+
throw new Error(`Connection selector contains duplicate connection name '${name2}'`);
|
|
7494
|
+
}
|
|
7495
|
+
seen.add(name2);
|
|
7496
|
+
}
|
|
7497
|
+
return names;
|
|
7498
|
+
}
|
|
7499
|
+
|
|
7500
|
+
// src/agent-core/env-ref.ts
|
|
7501
|
+
function resolveEnvRef(value, fieldName, env = process.env) {
|
|
7502
|
+
if (typeof value === "string")
|
|
7503
|
+
return value;
|
|
7504
|
+
const envKey = value.$env;
|
|
7505
|
+
const resolved = env[envKey];
|
|
7506
|
+
if (resolved === undefined) {
|
|
7507
|
+
throw new ConfigError(`Environment variable not defined: ${envKey} (field: ${fieldName})
|
|
7508
|
+
` + `Please set ${envKey} in an env file or your environment.`);
|
|
7509
|
+
}
|
|
7510
|
+
return resolved;
|
|
7511
|
+
}
|
|
7512
|
+
var init_env_ref = __esm(() => {
|
|
7467
7513
|
init_errors2();
|
|
7514
|
+
});
|
|
7515
|
+
|
|
7516
|
+
// src/agent-core/public.ts
|
|
7517
|
+
var init_public = __esm(() => {
|
|
7468
7518
|
init_env_loader();
|
|
7469
|
-
|
|
7519
|
+
init_env_ref();
|
|
7470
7520
|
});
|
|
7471
7521
|
|
|
7472
7522
|
// node_modules/lru-cache/dist/esm/node/index.min.js
|
|
@@ -8450,6 +8500,11 @@ __export(exports_config, {
|
|
|
8450
8500
|
});
|
|
8451
8501
|
import { join as join8 } from "path";
|
|
8452
8502
|
import { mkdir as mkdir3 } from "fs/promises";
|
|
8503
|
+
function assertNoConnectionSelectorOnV1(connectionName) {
|
|
8504
|
+
if (connectionName === undefined)
|
|
8505
|
+
return;
|
|
8506
|
+
throw new ConfigError(`\u9023\u7DDA '${connectionName}' \u4E0D\u5B58\u5728\uFF1A\u6B64\u5C08\u6848\u4F7F\u7528\u55AE\u4E00\u9023\u7DDA (v1) \u8A2D\u5B9A\uFF0C\u6C92\u6709\u5177\u540D\u9023\u7DDA\u3002` + ` \u79FB\u9664 --use / DBCLI_CONNECTION\uFF0C\u6216\u6539\u7528 'dbcli init --conn-name <name>' \u5347\u7D1A\u70BA\u591A\u9023\u7DDA (v2) \u8A2D\u5B9A\u3002`);
|
|
8507
|
+
}
|
|
8453
8508
|
function setGlobalConnectionName(name2) {
|
|
8454
8509
|
_globalConnectionName = name2;
|
|
8455
8510
|
}
|
|
@@ -8480,18 +8535,10 @@ async function getSchemaIsolationConnectionName(dbcliPath) {
|
|
|
8480
8535
|
function isEnvReference(value) {
|
|
8481
8536
|
return typeof value === "object" && value !== null && "$env" in value && typeof value.$env === "string";
|
|
8482
8537
|
}
|
|
8483
|
-
function resolveEnvReferences(config, env, parentKey
|
|
8538
|
+
function resolveEnvReferences(config, env, parentKey) {
|
|
8484
8539
|
if (isEnvReference(config)) {
|
|
8485
8540
|
const envKey = config.$env;
|
|
8486
|
-
const value = env
|
|
8487
|
-
if (!value) {
|
|
8488
|
-
if (!strict) {
|
|
8489
|
-
return config;
|
|
8490
|
-
}
|
|
8491
|
-
throw new ConfigError(`Environment variable not defined: ${envKey}
|
|
8492
|
-
` + `Please set ${envKey} in .env or your environment.
|
|
8493
|
-
` + `Hint: check your .env file or run 'export ${envKey}=<value>'`);
|
|
8494
|
-
}
|
|
8541
|
+
const value = resolveEnvRef(config, parentKey ?? "<root>", env);
|
|
8495
8542
|
if (parentKey === "port") {
|
|
8496
8543
|
const portNum = parseInt(value, 10);
|
|
8497
8544
|
if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
|
|
@@ -8502,12 +8549,12 @@ function resolveEnvReferences(config, env, parentKey, strict = false) {
|
|
|
8502
8549
|
return value;
|
|
8503
8550
|
}
|
|
8504
8551
|
if (Array.isArray(config)) {
|
|
8505
|
-
return config.map((item) => resolveEnvReferences(item, env, parentKey
|
|
8552
|
+
return config.map((item) => resolveEnvReferences(item, env, parentKey));
|
|
8506
8553
|
}
|
|
8507
8554
|
if (typeof config === "object" && config !== null) {
|
|
8508
8555
|
const resolved = {};
|
|
8509
8556
|
for (const [key, value] of Object.entries(config)) {
|
|
8510
|
-
resolved[key] = resolveEnvReferences(value, env, key
|
|
8557
|
+
resolved[key] = resolveEnvReferences(value, env, key);
|
|
8511
8558
|
}
|
|
8512
8559
|
return resolved;
|
|
8513
8560
|
}
|
|
@@ -8520,9 +8567,10 @@ function parseEnvPassword(content) {
|
|
|
8520
8567
|
var _globalConnectionName, DEFAULT_CONFIG, configModule;
|
|
8521
8568
|
var init_config = __esm(() => {
|
|
8522
8569
|
init_validation();
|
|
8523
|
-
|
|
8570
|
+
init_errors3();
|
|
8524
8571
|
init_config_v2();
|
|
8525
8572
|
init_config_binding();
|
|
8573
|
+
init_public();
|
|
8526
8574
|
DEFAULT_CONFIG = {
|
|
8527
8575
|
connection: {
|
|
8528
8576
|
system: "postgresql",
|
|
@@ -8583,7 +8631,7 @@ var init_config = __esm(() => {
|
|
|
8583
8631
|
process.env.DBCLI_PASSWORD = legacyPassword;
|
|
8584
8632
|
}
|
|
8585
8633
|
}
|
|
8586
|
-
const resolvedConnection = resolveEnvReferences(resolved.connection, process.env
|
|
8634
|
+
const resolvedConnection = resolveEnvReferences(resolved.connection, process.env);
|
|
8587
8635
|
if (!resolvedConnection.password && legacyPassword) {
|
|
8588
8636
|
resolvedConnection.password = legacyPassword;
|
|
8589
8637
|
}
|
|
@@ -8620,7 +8668,8 @@ var init_config = __esm(() => {
|
|
|
8620
8668
|
redis: v2Config.redis
|
|
8621
8669
|
});
|
|
8622
8670
|
}
|
|
8623
|
-
|
|
8671
|
+
assertNoConnectionSelectorOnV1(effectiveConnectionName);
|
|
8672
|
+
const resolvedConfig = resolveEnvReferences(config, process.env);
|
|
8624
8673
|
const envPath = join8(storagePath, ".env.local");
|
|
8625
8674
|
const envFile = Bun.file(envPath);
|
|
8626
8675
|
if (await envFile.exists()) {
|
|
@@ -8636,9 +8685,10 @@ var init_config = __esm(() => {
|
|
|
8636
8685
|
const file = Bun.file(path2);
|
|
8637
8686
|
const exists = await file.exists();
|
|
8638
8687
|
if (exists) {
|
|
8688
|
+
assertNoConnectionSelectorOnV1(effectiveConnectionName);
|
|
8639
8689
|
const content = await file.text();
|
|
8640
8690
|
const raw = JSON.parse(content);
|
|
8641
|
-
const resolved = resolveEnvReferences(raw, process.env
|
|
8691
|
+
const resolved = resolveEnvReferences(raw, process.env);
|
|
8642
8692
|
return DbcliConfigSchema.parse(resolved);
|
|
8643
8693
|
}
|
|
8644
8694
|
return { ...DEFAULT_CONFIG };
|
|
@@ -8954,18 +9004,21 @@ var init_binder = __esm(() => {
|
|
|
8954
9004
|
function applySnippetGuard(sqlBody, opts) {
|
|
8955
9005
|
const trimmed = sqlBody.trim().replace(/;\s*$/, "");
|
|
8956
9006
|
if (opts.noLimit)
|
|
8957
|
-
return trimmed;
|
|
9007
|
+
return { sql: trimmed };
|
|
8958
9008
|
const masked = stripCommentsAndStrings(trimmed);
|
|
8959
9009
|
const literal = matchOuterLiteralLimit(masked);
|
|
8960
|
-
if (literal !== null && literal <
|
|
8961
|
-
return trimmed;
|
|
8962
|
-
return
|
|
9010
|
+
if (literal !== null && literal < SNIPPET_GUARD_LIMIT)
|
|
9011
|
+
return { sql: trimmed };
|
|
9012
|
+
return {
|
|
9013
|
+
sql: `SELECT * FROM (${trimmed}) AS _dbcli_guard LIMIT ${SNIPPET_GUARD_LIMIT + 1}`,
|
|
9014
|
+
guardLimit: SNIPPET_GUARD_LIMIT
|
|
9015
|
+
};
|
|
8963
9016
|
}
|
|
8964
9017
|
function matchOuterLiteralLimit(masked) {
|
|
8965
9018
|
const m = masked.match(/\bLIMIT\s+(\d+)\s*$/i);
|
|
8966
9019
|
return m ? parseInt(m[1], 10) : null;
|
|
8967
9020
|
}
|
|
8968
|
-
var
|
|
9021
|
+
var SNIPPET_GUARD_LIMIT = 1000;
|
|
8969
9022
|
var init_size_guard = __esm(() => {
|
|
8970
9023
|
init_parser();
|
|
8971
9024
|
});
|
|
@@ -8999,11 +9052,12 @@ var init_sql = __esm(() => {
|
|
|
8999
9052
|
if (rewritten.undeclared.length > 0) {
|
|
9000
9053
|
warnings.push(`SQL references undeclared params: ${rewritten.undeclared.join(", ")}`);
|
|
9001
9054
|
}
|
|
9002
|
-
const
|
|
9055
|
+
const guarded = applySnippetGuard(rewritten.sql, { noLimit: opts.noLimit });
|
|
9003
9056
|
return {
|
|
9004
|
-
driver: { sql:
|
|
9057
|
+
driver: { sql: guarded.sql, values: rewritten.values },
|
|
9005
9058
|
rewrittenBody: rewritten.sql,
|
|
9006
|
-
warnings
|
|
9059
|
+
warnings,
|
|
9060
|
+
...guarded.guardLimit !== undefined ? { guardLimit: guarded.guardLimit } : {}
|
|
9007
9061
|
};
|
|
9008
9062
|
}
|
|
9009
9063
|
};
|
|
@@ -10469,6 +10523,15 @@ ${t("upgrade.failed")}`));
|
|
|
10469
10523
|
});
|
|
10470
10524
|
});
|
|
10471
10525
|
|
|
10526
|
+
// src/core/connection-selector.ts
|
|
10527
|
+
function createConnectionSelectorOption() {
|
|
10528
|
+
return new Option("--use <connection>", CONNECTION_SELECTOR_DESCRIPTION);
|
|
10529
|
+
}
|
|
10530
|
+
var CONNECTION_SELECTOR_DESCRIPTION = "Use a specific named connection for this invocation (v2 config)";
|
|
10531
|
+
var init_connection_selector = __esm(() => {
|
|
10532
|
+
init_esm();
|
|
10533
|
+
});
|
|
10534
|
+
|
|
10472
10535
|
// src/adapters/defaults.ts
|
|
10473
10536
|
function getDefaultsForSystem(system) {
|
|
10474
10537
|
switch (system) {
|
|
@@ -10567,7 +10630,7 @@ function parseEnvDatabase(env) {
|
|
|
10567
10630
|
return null;
|
|
10568
10631
|
}
|
|
10569
10632
|
var init_env_parser = __esm(() => {
|
|
10570
|
-
|
|
10633
|
+
init_errors3();
|
|
10571
10634
|
});
|
|
10572
10635
|
|
|
10573
10636
|
// node_modules/@inquirer/core/dist/lib/key.js
|
|
@@ -10575,7 +10638,7 @@ var isTabKey = (key) => key.name === "tab", isEnterKey = (key) => key.name === "
|
|
|
10575
10638
|
|
|
10576
10639
|
// node_modules/@inquirer/core/dist/lib/errors.js
|
|
10577
10640
|
var AbortPromptError, CancelPromptError, ExitPromptError, HookError, ValidationError;
|
|
10578
|
-
var
|
|
10641
|
+
var init_errors4 = __esm(() => {
|
|
10579
10642
|
AbortPromptError = class AbortPromptError extends Error {
|
|
10580
10643
|
name = "AbortPromptError";
|
|
10581
10644
|
message = "Prompt was aborted";
|
|
@@ -10673,7 +10736,7 @@ function handleChange() {
|
|
|
10673
10736
|
}
|
|
10674
10737
|
var hookStorage, effectScheduler;
|
|
10675
10738
|
var init_hook_engine = __esm(() => {
|
|
10676
|
-
|
|
10739
|
+
init_errors4();
|
|
10677
10740
|
hookStorage = new AsyncLocalStorage;
|
|
10678
10741
|
effectScheduler = {
|
|
10679
10742
|
queue(cb) {
|
|
@@ -12109,7 +12172,7 @@ var init_create_prompt = __esm(() => {
|
|
|
12109
12172
|
init_screen_manager();
|
|
12110
12173
|
init_promise_polyfill();
|
|
12111
12174
|
init_hook_engine();
|
|
12112
|
-
|
|
12175
|
+
init_errors4();
|
|
12113
12176
|
import_mute_stream = __toESM(require_lib(), 1);
|
|
12114
12177
|
nativeSetImmediate = globalThis.setImmediate;
|
|
12115
12178
|
});
|
|
@@ -12121,7 +12184,7 @@ var init_dist5 = __esm(() => {
|
|
|
12121
12184
|
init_use_keypress();
|
|
12122
12185
|
init_make_theme();
|
|
12123
12186
|
init_create_prompt();
|
|
12124
|
-
|
|
12187
|
+
init_errors4();
|
|
12125
12188
|
});
|
|
12126
12189
|
|
|
12127
12190
|
// node_modules/@inquirer/confirm/dist/index.js
|
|
@@ -12457,6 +12520,8 @@ var init_capabilities = __esm(() => {
|
|
|
12457
12520
|
|
|
12458
12521
|
// src/adapters/error-mapper.ts
|
|
12459
12522
|
function mapError(error, system, options) {
|
|
12523
|
+
if (error instanceof ConnectionError)
|
|
12524
|
+
return error;
|
|
12460
12525
|
const err = error;
|
|
12461
12526
|
const errMsg = String(err?.message || String(error));
|
|
12462
12527
|
const errCode = String(err?.code || "");
|
|
@@ -13136,7 +13201,7 @@ class MySQLAdapter {
|
|
|
13136
13201
|
JOIN information_schema.KEY_COLUMN_USAGE kcu
|
|
13137
13202
|
ON rc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME AND rc.TABLE_NAME = kcu.TABLE_NAME
|
|
13138
13203
|
WHERE kcu.TABLE_NAME = ? AND rc.CONSTRAINT_SCHEMA = DATABASE()
|
|
13139
|
-
GROUP BY rc.CONSTRAINT_NAME
|
|
13204
|
+
GROUP BY rc.CONSTRAINT_NAME, rc.REFERENCED_TABLE_NAME
|
|
13140
13205
|
`;
|
|
13141
13206
|
const fkResult = await this.execute(fkQuery, [tableName]);
|
|
13142
13207
|
const fkResults = fkResult.rows;
|
|
@@ -13375,13 +13440,14 @@ class MongoDBAdapter {
|
|
|
13375
13440
|
let docs;
|
|
13376
13441
|
if (Array.isArray(parsed)) {
|
|
13377
13442
|
const stages = parsed;
|
|
13378
|
-
const
|
|
13379
|
-
const
|
|
13380
|
-
const finalStages =
|
|
13443
|
+
const alreadyHasLimit = stages.some((stage) => stage !== null && typeof stage === "object" && ("$limit" in stage));
|
|
13444
|
+
const limitedStages = typeof limit === "number" && limit > 0 && !alreadyHasLimit ? [...stages, { $limit: limit }] : stages;
|
|
13445
|
+
const finalStages = options?.projection ? [...limitedStages, { $project: options.projection }] : limitedStages;
|
|
13381
13446
|
docs = await collection.aggregate(finalStages).toArray();
|
|
13382
13447
|
} else {
|
|
13383
13448
|
const cap2 = typeof limit === "number" && limit > 0 ? limit : 0;
|
|
13384
|
-
|
|
13449
|
+
const cursor = options?.projection ? collection.find(parsed, { projection: options.projection }) : collection.find(parsed);
|
|
13450
|
+
docs = await cursor.limit(cap2).toArray();
|
|
13385
13451
|
}
|
|
13386
13452
|
return { rows: docs, affectedRows: docs.length };
|
|
13387
13453
|
}
|
|
@@ -17303,6 +17369,35 @@ var init_json_formatter = __esm(() => {
|
|
|
17303
17369
|
});
|
|
17304
17370
|
|
|
17305
17371
|
// src/formatters/query-result-formatter.ts
|
|
17372
|
+
function truncateSerializedCell(serialized, limit) {
|
|
17373
|
+
if (!Number.isSafeInteger(limit) || limit <= 0) {
|
|
17374
|
+
throw new Error("Table cell truncation limit must be a positive integer");
|
|
17375
|
+
}
|
|
17376
|
+
const codePoints = Array.from(serialized);
|
|
17377
|
+
if (codePoints.length <= limit)
|
|
17378
|
+
return serialized;
|
|
17379
|
+
const omitted = codePoints.length - limit;
|
|
17380
|
+
return `${codePoints.slice(0, limit).join("")}\u2026(+${omitted} chars)`;
|
|
17381
|
+
}
|
|
17382
|
+
function toPublicQueryResult(result) {
|
|
17383
|
+
const appliedLimit = result.appliedLimit;
|
|
17384
|
+
const metadata = result.metadata || appliedLimit ? {
|
|
17385
|
+
...result.metadata,
|
|
17386
|
+
...appliedLimit ? {
|
|
17387
|
+
truncated: appliedLimit.truncated,
|
|
17388
|
+
limit_applied: appliedLimit.limitApplied
|
|
17389
|
+
} : {}
|
|
17390
|
+
} : undefined;
|
|
17391
|
+
return {
|
|
17392
|
+
rows: result.rows,
|
|
17393
|
+
rowCount: result.rowCount,
|
|
17394
|
+
columnNames: result.columnNames,
|
|
17395
|
+
columnTypes: result.columnTypes,
|
|
17396
|
+
executionTimeMs: result.executionTimeMs,
|
|
17397
|
+
metadata
|
|
17398
|
+
};
|
|
17399
|
+
}
|
|
17400
|
+
|
|
17306
17401
|
class QueryResultFormatter {
|
|
17307
17402
|
format(result, options) {
|
|
17308
17403
|
const format = options?.format || "table";
|
|
@@ -17313,10 +17408,10 @@ class QueryResultFormatter {
|
|
|
17313
17408
|
return this.formatCSV(result);
|
|
17314
17409
|
case "table":
|
|
17315
17410
|
default:
|
|
17316
|
-
return this.formatTable(result);
|
|
17411
|
+
return this.formatTable(result, options?.truncate);
|
|
17317
17412
|
}
|
|
17318
17413
|
}
|
|
17319
|
-
formatTable(result) {
|
|
17414
|
+
formatTable(result, truncate) {
|
|
17320
17415
|
if (result.rows.length === 0) {
|
|
17321
17416
|
return this.formatEmptyTable(result);
|
|
17322
17417
|
}
|
|
@@ -17325,18 +17420,11 @@ class QueryResultFormatter {
|
|
|
17325
17420
|
style: { compact: false, "padding-left": 1, "padding-right": 1 }
|
|
17326
17421
|
});
|
|
17327
17422
|
result.rows.forEach((row) => {
|
|
17328
|
-
table.push(result.columnNames.map((col) => this.cellToString(row[col])));
|
|
17423
|
+
table.push(result.columnNames.map((col) => this.cellToString(row[col], truncate)));
|
|
17329
17424
|
});
|
|
17330
17425
|
let output = table.toString();
|
|
17331
|
-
|
|
17332
|
-
|
|
17333
|
-
if (result.executionTimeMs !== undefined) {
|
|
17334
|
-
footerLines.push(`Execution time: ${result.executionTimeMs}ms`);
|
|
17335
|
-
}
|
|
17336
|
-
if (footerLines.length > 0) {
|
|
17337
|
-
output += `
|
|
17338
|
-
` + footerLines.join(" | ");
|
|
17339
|
-
}
|
|
17426
|
+
output += `
|
|
17427
|
+
` + this.formatTableFooter(result);
|
|
17340
17428
|
if (result.metadata?.securityNotification) {
|
|
17341
17429
|
output += `
|
|
17342
17430
|
` + result.metadata.securityNotification;
|
|
@@ -17349,15 +17437,8 @@ class QueryResultFormatter {
|
|
|
17349
17437
|
style: { compact: false, "padding-left": 1, "padding-right": 1 }
|
|
17350
17438
|
});
|
|
17351
17439
|
let output = table.toString();
|
|
17352
|
-
|
|
17353
|
-
|
|
17354
|
-
if (result.executionTimeMs !== undefined) {
|
|
17355
|
-
footerLines.push(`Execution time: ${result.executionTimeMs}ms`);
|
|
17356
|
-
}
|
|
17357
|
-
if (footerLines.length > 0) {
|
|
17358
|
-
output += `
|
|
17359
|
-
` + footerLines.join(" | ");
|
|
17360
|
-
}
|
|
17440
|
+
output += `
|
|
17441
|
+
` + this.formatTableFooter(result);
|
|
17361
17442
|
if (result.metadata?.securityNotification) {
|
|
17362
17443
|
output += `
|
|
17363
17444
|
` + result.metadata.securityNotification;
|
|
@@ -17366,23 +17447,29 @@ class QueryResultFormatter {
|
|
|
17366
17447
|
}
|
|
17367
17448
|
formatJSON(result, compact) {
|
|
17368
17449
|
const spacing = compact ? undefined : 2;
|
|
17369
|
-
|
|
17370
|
-
|
|
17371
|
-
|
|
17372
|
-
|
|
17373
|
-
|
|
17374
|
-
|
|
17375
|
-
|
|
17376
|
-
|
|
17377
|
-
|
|
17450
|
+
return JSON.stringify(toPublicQueryResult(result), null, spacing);
|
|
17451
|
+
}
|
|
17452
|
+
formatTableFooter(result) {
|
|
17453
|
+
const footerLines = [];
|
|
17454
|
+
const appliedLimit = result.appliedLimit;
|
|
17455
|
+
const rowsText = appliedLimit?.truncated ? `Rows: ${result.rowCount} (truncated; limit ${appliedLimit.limitApplied})` : `Rows: ${result.rowCount}`;
|
|
17456
|
+
footerLines.push(rowsText);
|
|
17457
|
+
if (result.executionTimeMs !== undefined) {
|
|
17458
|
+
footerLines.push(`Execution time: ${result.executionTimeMs}ms`);
|
|
17459
|
+
}
|
|
17460
|
+
return footerLines.join(" | ");
|
|
17378
17461
|
}
|
|
17379
17462
|
formatCSV(result) {
|
|
17463
|
+
const truncationNotice = result.appliedLimit?.truncated ? `# truncated; limit ${result.appliedLimit.limitApplied} \u2014 rerun with --no-limit or --limit N for the full result` : undefined;
|
|
17380
17464
|
if (result.rows.length === 0) {
|
|
17381
17465
|
let csvOutput = result.columnNames.map((name2) => this.escapeCSVField(name2)).join(",");
|
|
17382
17466
|
if (result.metadata?.securityNotification) {
|
|
17383
17467
|
csvOutput += `
|
|
17384
17468
|
# ` + result.metadata.securityNotification;
|
|
17385
17469
|
}
|
|
17470
|
+
if (truncationNotice)
|
|
17471
|
+
csvOutput += `
|
|
17472
|
+
` + truncationNotice;
|
|
17386
17473
|
return csvOutput;
|
|
17387
17474
|
}
|
|
17388
17475
|
const lines = [];
|
|
@@ -17394,6 +17481,8 @@ class QueryResultFormatter {
|
|
|
17394
17481
|
if (result.metadata?.securityNotification) {
|
|
17395
17482
|
lines.push(`# ${result.metadata.securityNotification}`);
|
|
17396
17483
|
}
|
|
17484
|
+
if (truncationNotice)
|
|
17485
|
+
lines.push(truncationNotice);
|
|
17397
17486
|
return lines.join(`
|
|
17398
17487
|
`);
|
|
17399
17488
|
}
|
|
@@ -17408,17 +17497,20 @@ class QueryResultFormatter {
|
|
|
17408
17497
|
}
|
|
17409
17498
|
return str;
|
|
17410
17499
|
}
|
|
17411
|
-
cellToString(value) {
|
|
17500
|
+
cellToString(value, truncate) {
|
|
17412
17501
|
if (value === null || value === undefined) {
|
|
17413
17502
|
return "";
|
|
17414
17503
|
}
|
|
17504
|
+
let serialized;
|
|
17415
17505
|
if (typeof value === "object") {
|
|
17416
|
-
|
|
17506
|
+
serialized = JSON.stringify(value);
|
|
17507
|
+
} else {
|
|
17508
|
+
serialized = String(value);
|
|
17417
17509
|
}
|
|
17418
|
-
return
|
|
17510
|
+
return typeof truncate === "number" ? truncateSerializedCell(serialized, truncate) : serialized;
|
|
17419
17511
|
}
|
|
17420
17512
|
}
|
|
17421
|
-
var Table2;
|
|
17513
|
+
var Table2, DEFAULT_TABLE_CELL_LIMIT = 120;
|
|
17422
17514
|
var init_query_result_formatter = __esm(() => {
|
|
17423
17515
|
Table2 = require_table();
|
|
17424
17516
|
});
|
|
@@ -17438,60 +17530,49 @@ function requireSqlConnection(connection) {
|
|
|
17438
17530
|
return connection;
|
|
17439
17531
|
}
|
|
17440
17532
|
async function listAction(options, command) {
|
|
17533
|
+
validateFormat(options.format, ALLOWED_FORMATS, "list");
|
|
17534
|
+
const config = await configModule.read(resolveConfigPath(command, options));
|
|
17535
|
+
if (!config.connection) {
|
|
17536
|
+
throw new Error("Database not configured. Run: dbcli init");
|
|
17537
|
+
}
|
|
17538
|
+
if (config.connection.system === "mongodb") {
|
|
17539
|
+
return await mongoListBranch(config, options.format);
|
|
17540
|
+
}
|
|
17541
|
+
if (config.connection.system === "redis") {
|
|
17542
|
+
return await redisListBranch(config, options.format);
|
|
17543
|
+
}
|
|
17544
|
+
if (config.connection.system === "elasticsearch") {
|
|
17545
|
+
return await elasticsearchListBranch(config, options.format, options.includeSystem === true);
|
|
17546
|
+
}
|
|
17547
|
+
const adapter = AdapterFactory.createSqlAdapter(requireSqlConnection(config.connection));
|
|
17548
|
+
await adapter.connect();
|
|
17441
17549
|
try {
|
|
17442
|
-
|
|
17443
|
-
|
|
17444
|
-
|
|
17445
|
-
|
|
17446
|
-
process.exit(1);
|
|
17447
|
-
}
|
|
17448
|
-
if (config.connection.system === "mongodb") {
|
|
17449
|
-
return mongoListBranch(config, options.format);
|
|
17450
|
-
}
|
|
17451
|
-
if (config.connection.system === "redis") {
|
|
17452
|
-
return redisListBranch(config, options.format);
|
|
17550
|
+
const tables = await adapter.listTables();
|
|
17551
|
+
if (tables.length === 0) {
|
|
17552
|
+
console.log(t("list.no_tables"));
|
|
17553
|
+
return;
|
|
17453
17554
|
}
|
|
17454
|
-
if (
|
|
17455
|
-
|
|
17555
|
+
if (options.format === "json") {
|
|
17556
|
+
const listOutput = tables.map((t2) => ({
|
|
17557
|
+
name: t2.name,
|
|
17558
|
+
columnCount: t2.columnCount ?? t2.columns.length,
|
|
17559
|
+
rowCount: t2.rowCount ?? 0,
|
|
17560
|
+
engine: t2.engine,
|
|
17561
|
+
estimatedRowCount: t2.estimatedRowCount ?? t2.rowCount ?? 0,
|
|
17562
|
+
tableType: t2.tableType ?? "table"
|
|
17563
|
+
}));
|
|
17564
|
+
console.log(JSON.stringify(listOutput, null, 2));
|
|
17565
|
+
} else {
|
|
17566
|
+
const formatter = new TableListFormatter;
|
|
17567
|
+
console.log(formatter.format(tables));
|
|
17456
17568
|
}
|
|
17457
|
-
const
|
|
17458
|
-
|
|
17459
|
-
|
|
17460
|
-
|
|
17461
|
-
if (tables.length === 0) {
|
|
17462
|
-
console.log(t("list.no_tables"));
|
|
17463
|
-
return;
|
|
17464
|
-
}
|
|
17465
|
-
if (options.format === "json") {
|
|
17466
|
-
const listOutput = tables.map((t2) => ({
|
|
17467
|
-
name: t2.name,
|
|
17468
|
-
columnCount: t2.columnCount ?? t2.columns.length,
|
|
17469
|
-
rowCount: t2.rowCount ?? 0,
|
|
17470
|
-
engine: t2.engine,
|
|
17471
|
-
estimatedRowCount: t2.estimatedRowCount ?? t2.rowCount ?? 0,
|
|
17472
|
-
tableType: t2.tableType ?? "table"
|
|
17473
|
-
}));
|
|
17474
|
-
console.log(JSON.stringify(listOutput, null, 2));
|
|
17475
|
-
} else {
|
|
17476
|
-
const formatter = new TableListFormatter;
|
|
17477
|
-
console.log(formatter.format(tables));
|
|
17478
|
-
}
|
|
17479
|
-
const tableCount = tables.filter((t2) => t2.tableType !== "view").length;
|
|
17480
|
-
const viewCount = tables.filter((t2) => t2.tableType === "view").length;
|
|
17481
|
-
const viewSuffix = viewCount > 0 ? ` (${viewCount} views)` : "";
|
|
17482
|
-
console.log(`
|
|
17569
|
+
const tableCount = tables.filter((t2) => t2.tableType !== "view").length;
|
|
17570
|
+
const viewCount = tables.filter((t2) => t2.tableType === "view").length;
|
|
17571
|
+
const viewSuffix = viewCount > 0 ? ` (${viewCount} views)` : "";
|
|
17572
|
+
console.log(`
|
|
17483
17573
|
\u2713 Found ${tableCount} tables${viewSuffix}`);
|
|
17484
|
-
|
|
17485
|
-
|
|
17486
|
-
}
|
|
17487
|
-
} catch (error) {
|
|
17488
|
-
if (error instanceof Error) {
|
|
17489
|
-
console.error(t_vars("errors.message", { message: error.message }));
|
|
17490
|
-
if (error instanceof ConnectionError) {
|
|
17491
|
-
error.hints.forEach((hint) => console.error(` Hint: ${hint}`));
|
|
17492
|
-
}
|
|
17493
|
-
}
|
|
17494
|
-
process.exit(1);
|
|
17574
|
+
} finally {
|
|
17575
|
+
await adapter.disconnect();
|
|
17495
17576
|
}
|
|
17496
17577
|
}
|
|
17497
17578
|
async function mongoListBranch(config, format) {
|
|
@@ -17583,8 +17664,9 @@ var init_list = __esm(() => {
|
|
|
17583
17664
|
init_formatters();
|
|
17584
17665
|
init_config();
|
|
17585
17666
|
init_validation();
|
|
17667
|
+
init_connection_selector();
|
|
17586
17668
|
ALLOWED_FORMATS = ["table", "json"];
|
|
17587
|
-
listCommand = new Command().name("list").description("List all tables in the database with metadata").option("--format <format>", "Output format: table (default) or json", "table").option("--config <path>", "Path to .dbcli config file", ".dbcli").option("--include-system", "Elasticsearch only: include system indices whose names start with dot", false).action(listAction);
|
|
17669
|
+
listCommand = new Command().name("list").description("List all tables in the database with metadata").option("--format <format>", "Output format: table (default) or json", "table").option("--config <path>", "Path to .dbcli config file", ".dbcli").addOption(createConnectionSelectorOption()).option("--include-system", "Elasticsearch only: include system indices whose names start with dot", false).action(listAction);
|
|
17588
17670
|
});
|
|
17589
17671
|
|
|
17590
17672
|
// src/core/schema-diff.ts
|
|
@@ -17982,6 +18064,180 @@ var init_blacklist = __esm(() => {
|
|
|
17982
18064
|
};
|
|
17983
18065
|
});
|
|
17984
18066
|
|
|
18067
|
+
// src/core/field-projection.ts
|
|
18068
|
+
function parseFieldSelection(raw) {
|
|
18069
|
+
if (raw === undefined)
|
|
18070
|
+
return;
|
|
18071
|
+
const tokens = raw.split(",").map((token) => token.trim());
|
|
18072
|
+
if (tokens.length === 0 || tokens.some((token) => token === "")) {
|
|
18073
|
+
throw new Error("--fields must not contain empty fields");
|
|
18074
|
+
}
|
|
18075
|
+
let mode;
|
|
18076
|
+
const paths = [];
|
|
18077
|
+
const seen = new Set;
|
|
18078
|
+
for (const token of tokens) {
|
|
18079
|
+
const tokenMode = token.startsWith("-") ? "exclude" : "include";
|
|
18080
|
+
const path4 = tokenMode === "exclude" ? token.slice(1) : token;
|
|
18081
|
+
if (path4 === "")
|
|
18082
|
+
throw new Error("--fields exclusion requires a field name after -");
|
|
18083
|
+
if (mode !== undefined && mode !== tokenMode) {
|
|
18084
|
+
throw new Error("--fields cannot mix included and excluded fields");
|
|
18085
|
+
}
|
|
18086
|
+
validatePath(path4);
|
|
18087
|
+
if (seen.has(path4))
|
|
18088
|
+
throw new Error(`--fields contains duplicate field: ${path4}`);
|
|
18089
|
+
mode = tokenMode;
|
|
18090
|
+
seen.add(path4);
|
|
18091
|
+
paths.push(path4);
|
|
18092
|
+
}
|
|
18093
|
+
if (!mode || paths.length === 0)
|
|
18094
|
+
throw new Error("--fields must contain at least one field");
|
|
18095
|
+
return { mode, paths };
|
|
18096
|
+
}
|
|
18097
|
+
function projectRows(rows, selection) {
|
|
18098
|
+
if (selection.mode === "include") {
|
|
18099
|
+
return {
|
|
18100
|
+
rows: rows.map((row) => {
|
|
18101
|
+
const projected = {};
|
|
18102
|
+
for (const path4 of selection.paths) {
|
|
18103
|
+
const result = readPath(row, path4.split("."));
|
|
18104
|
+
defineData(projected, path4, result.found ? result.value ?? null : null);
|
|
18105
|
+
}
|
|
18106
|
+
return projected;
|
|
18107
|
+
}),
|
|
18108
|
+
columnNames: [...selection.paths]
|
|
18109
|
+
};
|
|
18110
|
+
}
|
|
18111
|
+
const projectedRows = omitFieldPaths(rows, selection.paths);
|
|
18112
|
+
const columnNames = collectColumnNames(projectedRows);
|
|
18113
|
+
return { rows: normalizeRows(projectedRows, columnNames), columnNames };
|
|
18114
|
+
}
|
|
18115
|
+
function hasFieldPath(row, path4) {
|
|
18116
|
+
return readPath(row, path4.split(".")).found;
|
|
18117
|
+
}
|
|
18118
|
+
function omitFieldPaths(rows, paths) {
|
|
18119
|
+
return rows.map((row) => {
|
|
18120
|
+
let projected = cloneRecord(row);
|
|
18121
|
+
for (const path4 of paths)
|
|
18122
|
+
projected = omitPath(projected, path4.split("."));
|
|
18123
|
+
return projected;
|
|
18124
|
+
});
|
|
18125
|
+
}
|
|
18126
|
+
function toMongoProjection(selection) {
|
|
18127
|
+
const projection = {};
|
|
18128
|
+
const value = selection.mode === "include" ? 1 : 0;
|
|
18129
|
+
for (const path4 of selection.paths)
|
|
18130
|
+
defineData(projection, path4, value);
|
|
18131
|
+
if (selection.mode === "include" && !selection.paths.includes("_id")) {
|
|
18132
|
+
defineData(projection, "_id", 0);
|
|
18133
|
+
}
|
|
18134
|
+
return projection;
|
|
18135
|
+
}
|
|
18136
|
+
function validatePath(path4) {
|
|
18137
|
+
const segments = path4.split(".");
|
|
18138
|
+
if (segments.some((segment) => segment === "")) {
|
|
18139
|
+
throw new Error(`--fields contains an invalid dotted path: ${path4}`);
|
|
18140
|
+
}
|
|
18141
|
+
const unsafe = segments.find((segment) => UNSAFE_SEGMENTS.has(segment));
|
|
18142
|
+
if (unsafe)
|
|
18143
|
+
throw new Error(`--fields contains an unsafe field segment: ${unsafe}`);
|
|
18144
|
+
}
|
|
18145
|
+
function readPath(value, segments) {
|
|
18146
|
+
if (segments.length === 0)
|
|
18147
|
+
return { found: true, value };
|
|
18148
|
+
if (Array.isArray(value)) {
|
|
18149
|
+
const results = value.map((item) => readPath(item, segments));
|
|
18150
|
+
return {
|
|
18151
|
+
found: results.some((result) => result.found),
|
|
18152
|
+
value: results.map((result) => result.found ? result.value : undefined)
|
|
18153
|
+
};
|
|
18154
|
+
}
|
|
18155
|
+
if (!isRecord(value))
|
|
18156
|
+
return { found: false };
|
|
18157
|
+
const exactPath = segments.join(".");
|
|
18158
|
+
if (Object.prototype.hasOwnProperty.call(value, exactPath)) {
|
|
18159
|
+
return { found: true, value: value[exactPath] };
|
|
18160
|
+
}
|
|
18161
|
+
const [head, ...tail] = segments;
|
|
18162
|
+
if (!Object.prototype.hasOwnProperty.call(value, head))
|
|
18163
|
+
return { found: false };
|
|
18164
|
+
return readPath(value[head], tail);
|
|
18165
|
+
}
|
|
18166
|
+
function omitPath(value, segments) {
|
|
18167
|
+
if (segments.length === 0)
|
|
18168
|
+
return value;
|
|
18169
|
+
if (Array.isArray(value))
|
|
18170
|
+
return value.map((item) => omitPath(item, segments));
|
|
18171
|
+
if (!isPlainRecord(value))
|
|
18172
|
+
return value;
|
|
18173
|
+
const exactPath = segments.join(".");
|
|
18174
|
+
const [head, ...tail] = segments;
|
|
18175
|
+
const out = {};
|
|
18176
|
+
for (const [key, child] of Object.entries(value)) {
|
|
18177
|
+
if (key === exactPath)
|
|
18178
|
+
continue;
|
|
18179
|
+
if (key === head) {
|
|
18180
|
+
if (tail.length > 0)
|
|
18181
|
+
defineData(out, key, omitPath(child, tail));
|
|
18182
|
+
continue;
|
|
18183
|
+
}
|
|
18184
|
+
defineData(out, key, child);
|
|
18185
|
+
}
|
|
18186
|
+
return out;
|
|
18187
|
+
}
|
|
18188
|
+
function cloneRecord(value, omittedKey) {
|
|
18189
|
+
const out = {};
|
|
18190
|
+
for (const [key, child] of Object.entries(value)) {
|
|
18191
|
+
if (key !== omittedKey)
|
|
18192
|
+
defineData(out, key, child);
|
|
18193
|
+
}
|
|
18194
|
+
return out;
|
|
18195
|
+
}
|
|
18196
|
+
function collectColumnNames(rows) {
|
|
18197
|
+
const names = [];
|
|
18198
|
+
const seen = new Set;
|
|
18199
|
+
for (const row of rows) {
|
|
18200
|
+
for (const name2 of Object.keys(row)) {
|
|
18201
|
+
if (!seen.has(name2)) {
|
|
18202
|
+
seen.add(name2);
|
|
18203
|
+
names.push(name2);
|
|
18204
|
+
}
|
|
18205
|
+
}
|
|
18206
|
+
}
|
|
18207
|
+
return names;
|
|
18208
|
+
}
|
|
18209
|
+
function normalizeRows(rows, columnNames) {
|
|
18210
|
+
return rows.map((row) => {
|
|
18211
|
+
const normalized = {};
|
|
18212
|
+
for (const column of columnNames) {
|
|
18213
|
+
const value = Object.prototype.hasOwnProperty.call(row, column) ? row[column] : null;
|
|
18214
|
+
defineData(normalized, column, value ?? null);
|
|
18215
|
+
}
|
|
18216
|
+
return normalized;
|
|
18217
|
+
});
|
|
18218
|
+
}
|
|
18219
|
+
function isRecord(value) {
|
|
18220
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
18221
|
+
}
|
|
18222
|
+
function isPlainRecord(value) {
|
|
18223
|
+
if (!isRecord(value))
|
|
18224
|
+
return false;
|
|
18225
|
+
const prototype = Object.getPrototypeOf(value);
|
|
18226
|
+
return prototype === Object.prototype || prototype === null;
|
|
18227
|
+
}
|
|
18228
|
+
function defineData(target, key, value) {
|
|
18229
|
+
Object.defineProperty(target, key, {
|
|
18230
|
+
value,
|
|
18231
|
+
enumerable: true,
|
|
18232
|
+
configurable: true,
|
|
18233
|
+
writable: true
|
|
18234
|
+
});
|
|
18235
|
+
}
|
|
18236
|
+
var UNSAFE_SEGMENTS;
|
|
18237
|
+
var init_field_projection = __esm(() => {
|
|
18238
|
+
UNSAFE_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]);
|
|
18239
|
+
});
|
|
18240
|
+
|
|
17985
18241
|
// src/core/blacklist-validator.ts
|
|
17986
18242
|
class BlacklistValidator {
|
|
17987
18243
|
manager;
|
|
@@ -18034,19 +18290,11 @@ class BlacklistValidator {
|
|
|
18034
18290
|
if (blacklistedColumns.length === 0) {
|
|
18035
18291
|
return { filteredRows: rows, omittedColumns: [] };
|
|
18036
18292
|
}
|
|
18037
|
-
const omittedColumns =
|
|
18293
|
+
const omittedColumns = blacklistedColumns.filter((path4) => columnList.includes(path4) || rows.some((row) => hasFieldPath(row, path4)));
|
|
18038
18294
|
if (omittedColumns.length === 0) {
|
|
18039
18295
|
return { filteredRows: rows, omittedColumns: [] };
|
|
18040
18296
|
}
|
|
18041
|
-
const filteredRows = rows
|
|
18042
|
-
const newRow = {};
|
|
18043
|
-
for (const [key, value] of Object.entries(row)) {
|
|
18044
|
-
if (!omittedColumns.includes(key)) {
|
|
18045
|
-
newRow[key] = value;
|
|
18046
|
-
}
|
|
18047
|
-
}
|
|
18048
|
-
return newRow;
|
|
18049
|
-
});
|
|
18297
|
+
const filteredRows = omitFieldPaths(rows, omittedColumns);
|
|
18050
18298
|
return { filteredRows, omittedColumns };
|
|
18051
18299
|
}
|
|
18052
18300
|
buildSecurityNotification(_tableName, omittedColumns) {
|
|
@@ -18061,6 +18309,7 @@ class BlacklistValidator {
|
|
|
18061
18309
|
var init_blacklist_validator = __esm(() => {
|
|
18062
18310
|
init_blacklist();
|
|
18063
18311
|
init_message_loader();
|
|
18312
|
+
init_field_projection();
|
|
18064
18313
|
});
|
|
18065
18314
|
|
|
18066
18315
|
// src/core/verification/types.ts
|
|
@@ -18216,13 +18465,26 @@ var init_artifact_writer = () => {};
|
|
|
18216
18465
|
|
|
18217
18466
|
// src/utils/redaction.ts
|
|
18218
18467
|
function optionParts(token) {
|
|
18468
|
+
if (token.startsWith("-f") && !token.startsWith("--") && token.length > 2) {
|
|
18469
|
+
return { name: "-f", inlineValue: token.slice(2) };
|
|
18470
|
+
}
|
|
18219
18471
|
const equals = token.indexOf("=");
|
|
18220
18472
|
return equals === -1 ? { name: token, inlineValue: undefined } : { name: token.slice(0, equals), inlineValue: token.slice(equals + 1) };
|
|
18221
18473
|
}
|
|
18474
|
+
function isOptionToken(token) {
|
|
18475
|
+
return token.startsWith("--") || token.startsWith("-f");
|
|
18476
|
+
}
|
|
18477
|
+
function isKnownBooleanFlag(command, name2) {
|
|
18478
|
+
if (command === "lint")
|
|
18479
|
+
return LINT_BOOLEAN_FLAGS.has(name2);
|
|
18480
|
+
if (command === "query")
|
|
18481
|
+
return QUERY_BOOLEAN_FLAGS.has(name2);
|
|
18482
|
+
return false;
|
|
18483
|
+
}
|
|
18222
18484
|
function findSensitiveSubcommand(argv) {
|
|
18223
18485
|
for (let index = 1;index < argv.length; index++) {
|
|
18224
18486
|
const token = argv[index];
|
|
18225
|
-
if (token
|
|
18487
|
+
if (isOptionToken(token)) {
|
|
18226
18488
|
const { name: name2, inlineValue } = optionParts(token);
|
|
18227
18489
|
if (inlineValue === undefined && (REDACTED_VALUE_FLAGS.has(name2) || KEEP_VALUE_FLAGS.has(name2))) {
|
|
18228
18490
|
index++;
|
|
@@ -18245,7 +18507,7 @@ function sensitiveArgvValues(argv) {
|
|
|
18245
18507
|
afterEndOfOptions = true;
|
|
18246
18508
|
continue;
|
|
18247
18509
|
}
|
|
18248
|
-
if (!afterEndOfOptions && token
|
|
18510
|
+
if (!afterEndOfOptions && isOptionToken(token)) {
|
|
18249
18511
|
const { name: name2, inlineValue } = optionParts(token);
|
|
18250
18512
|
if (REDACTED_VALUE_FLAGS.has(name2)) {
|
|
18251
18513
|
const value = inlineValue ?? argv[index + 1];
|
|
@@ -18264,7 +18526,7 @@ function sensitiveArgvValues(argv) {
|
|
|
18264
18526
|
index++;
|
|
18265
18527
|
} else if (inlineValue === undefined && KEEP_VALUE_FLAGS.has(name2)) {
|
|
18266
18528
|
index++;
|
|
18267
|
-
} else if (sensitiveCommand?.command
|
|
18529
|
+
} else if (isKnownBooleanFlag(sensitiveCommand?.command, name2)) {
|
|
18268
18530
|
continue;
|
|
18269
18531
|
} else if (!sensitiveCommand || index < sensitiveCommand.index) {
|
|
18270
18532
|
continue;
|
|
@@ -18295,7 +18557,7 @@ function redactArgv(argv) {
|
|
|
18295
18557
|
afterEndOfOptions = true;
|
|
18296
18558
|
continue;
|
|
18297
18559
|
}
|
|
18298
|
-
if (!afterEndOfOptions && tok
|
|
18560
|
+
if (!afterEndOfOptions && isOptionToken(tok)) {
|
|
18299
18561
|
const { name: name2, inlineValue } = optionParts(tok);
|
|
18300
18562
|
if (REDACTED_VALUE_FLAGS.has(name2)) {
|
|
18301
18563
|
out.push(`${name2} <redacted>`);
|
|
@@ -18314,7 +18576,7 @@ function redactArgv(argv) {
|
|
|
18314
18576
|
}
|
|
18315
18577
|
continue;
|
|
18316
18578
|
}
|
|
18317
|
-
if (sensitiveCommand?.command
|
|
18579
|
+
if (isKnownBooleanFlag(sensitiveCommand?.command, name2)) {
|
|
18318
18580
|
out.push(tok);
|
|
18319
18581
|
continue;
|
|
18320
18582
|
}
|
|
@@ -18346,7 +18608,7 @@ function redactSql(sql) {
|
|
|
18346
18608
|
const redacted = sql.replace(/\$(\w*)\$[\s\S]*?\$\1\$/g, "'?'").replace(/(['"])(?:(?!\1|\\).|\\.)*\1/g, "'?'").replace(/\b\d+(\.\d+)?\b/g, "0");
|
|
18347
18609
|
return redactSensitive(redacted);
|
|
18348
18610
|
}
|
|
18349
|
-
var SQL_SUBCOMMANDS, REDACTED_VALUE_FLAGS, KEEP_VALUE_FLAGS, LINT_BOOLEAN_FLAGS;
|
|
18611
|
+
var SQL_SUBCOMMANDS, REDACTED_VALUE_FLAGS, KEEP_VALUE_FLAGS, LINT_BOOLEAN_FLAGS, QUERY_BOOLEAN_FLAGS;
|
|
18350
18612
|
var init_redaction = __esm(() => {
|
|
18351
18613
|
SQL_SUBCOMMANDS = new Set(["query", "export", "lint"]);
|
|
18352
18614
|
REDACTED_VALUE_FLAGS = new Set([
|
|
@@ -18360,7 +18622,9 @@ var init_redaction = __esm(() => {
|
|
|
18360
18622
|
"--password",
|
|
18361
18623
|
"--token",
|
|
18362
18624
|
"--secret",
|
|
18363
|
-
"--bulk"
|
|
18625
|
+
"--bulk",
|
|
18626
|
+
"--query-file",
|
|
18627
|
+
"-f"
|
|
18364
18628
|
]);
|
|
18365
18629
|
KEEP_VALUE_FLAGS = new Set([
|
|
18366
18630
|
"--format",
|
|
@@ -18369,9 +18633,12 @@ var init_redaction = __esm(() => {
|
|
|
18369
18633
|
"--output",
|
|
18370
18634
|
"--limit",
|
|
18371
18635
|
"--collection",
|
|
18372
|
-
"--index"
|
|
18636
|
+
"--index",
|
|
18637
|
+
"--fields",
|
|
18638
|
+
"--truncate"
|
|
18373
18639
|
]);
|
|
18374
18640
|
LINT_BOOLEAN_FLAGS = new Set(["--no-schema", "--recovery"]);
|
|
18641
|
+
QUERY_BOOLEAN_FLAGS = new Set(["--ui", "--no-limit", "--no-truncate", "--recovery"]);
|
|
18375
18642
|
});
|
|
18376
18643
|
|
|
18377
18644
|
// src/core/verification/assert-artifact.ts
|
|
@@ -19387,9 +19654,9 @@ var init_engine_hints = __esm(() => {
|
|
|
19387
19654
|
});
|
|
19388
19655
|
|
|
19389
19656
|
// src/core/audit/integration-helper.ts
|
|
19390
|
-
async function getAuditLogger(config, configPath) {
|
|
19657
|
+
async function getAuditLogger(config, configPath, connectionName) {
|
|
19391
19658
|
const storagePath = await resolveConfigStoragePath(configPath);
|
|
19392
|
-
const connName = config.effectiveConnectionName || getGlobalConnectionName() || "default";
|
|
19659
|
+
const connName = connectionName || config.effectiveConnectionName || getGlobalConnectionName() || "default";
|
|
19393
19660
|
const key = `${storagePath}:${connName}`;
|
|
19394
19661
|
if (!_loggers.has(key)) {
|
|
19395
19662
|
if (!_sessionIdService) {
|
|
@@ -19410,7 +19677,7 @@ async function getAuditLogger(config, configPath) {
|
|
|
19410
19677
|
}
|
|
19411
19678
|
async function writeAuditEntry(config, commandName, options, outcome) {
|
|
19412
19679
|
try {
|
|
19413
|
-
const logger = await getAuditLogger(config, options.config || ".dbcli");
|
|
19680
|
+
const logger = await getAuditLogger(config, options.config || ".dbcli", typeof options.connectionName === "string" ? options.connectionName : undefined);
|
|
19414
19681
|
const engine = config.connection?.system || "postgresql";
|
|
19415
19682
|
const target = outcome.target || getOperationTarget(engine, commandName, options, outcome.sql);
|
|
19416
19683
|
let tier = getEngineCapability(engine, commandName).tier;
|
|
@@ -20029,24 +20296,48 @@ function normalizeSQL(sql) {
|
|
|
20029
20296
|
return sql.replace(/--[^\n]*\n/g, `
|
|
20030
20297
|
`).replace(/\/\*[\s\S]*?\*\//g, " ").trim().replace(/\s+/g, " ");
|
|
20031
20298
|
}
|
|
20032
|
-
function stripCommentsAndStrings2(sql) {
|
|
20299
|
+
function stripCommentsAndStrings2(sql, options = {}) {
|
|
20033
20300
|
let result = "";
|
|
20034
20301
|
let i = 0;
|
|
20035
20302
|
while (i < sql.length) {
|
|
20036
20303
|
const char = sql[i];
|
|
20037
|
-
|
|
20304
|
+
const mysqlDialect = options.dialect === "mysql" || options.dialect === "mariadb";
|
|
20305
|
+
const dashFollowerCode = sql.charCodeAt(i + 2);
|
|
20306
|
+
const dashStartsComment = char === "-" && sql[i + 1] === "-" && (!mysqlDialect || sql[i + 2] === undefined || dashFollowerCode <= 32 || dashFollowerCode === 127);
|
|
20307
|
+
if (dashStartsComment) {
|
|
20038
20308
|
while (i < sql.length && sql[i] !== `
|
|
20039
20309
|
`) {
|
|
20040
20310
|
i++;
|
|
20041
20311
|
}
|
|
20042
20312
|
if (i < sql.length) {
|
|
20043
20313
|
result += `
|
|
20314
|
+
`;
|
|
20315
|
+
i++;
|
|
20316
|
+
}
|
|
20317
|
+
continue;
|
|
20318
|
+
}
|
|
20319
|
+
if (mysqlDialect && char === "#") {
|
|
20320
|
+
while (i < sql.length && sql[i] !== `
|
|
20321
|
+
`)
|
|
20322
|
+
i++;
|
|
20323
|
+
if (i < sql.length) {
|
|
20324
|
+
result += `
|
|
20044
20325
|
`;
|
|
20045
20326
|
i++;
|
|
20046
20327
|
}
|
|
20047
20328
|
continue;
|
|
20048
20329
|
}
|
|
20049
20330
|
if (char === "/" && sql[i + 1] === "*") {
|
|
20331
|
+
const executableMysqlComment = mysqlDialect && (sql.startsWith("/*!", i) || sql.startsWith("/*M!", i));
|
|
20332
|
+
if (executableMysqlComment) {
|
|
20333
|
+
const prefixLength = sql.startsWith("/*M!", i) ? 4 : 3;
|
|
20334
|
+
const closingIndex = sql.indexOf("*/", i + prefixLength);
|
|
20335
|
+
const bodyEnd = closingIndex === -1 ? sql.length : closingIndex;
|
|
20336
|
+
const executableBody = sql.slice(i + prefixLength, bodyEnd).replace(/^\d+/, " ");
|
|
20337
|
+
result += " " + stripCommentsAndStrings2(executableBody, options) + " ";
|
|
20338
|
+
i = closingIndex === -1 ? sql.length : closingIndex + 2;
|
|
20339
|
+
continue;
|
|
20340
|
+
}
|
|
20050
20341
|
i += 2;
|
|
20051
20342
|
while (i < sql.length) {
|
|
20052
20343
|
if (sql[i] === "*" && sql[i + 1] === "/") {
|
|
@@ -20058,16 +20349,51 @@ function stripCommentsAndStrings2(sql) {
|
|
|
20058
20349
|
result += " ";
|
|
20059
20350
|
continue;
|
|
20060
20351
|
}
|
|
20061
|
-
if (
|
|
20062
|
-
const
|
|
20352
|
+
if (options.dialect === "postgresql" && char === "$") {
|
|
20353
|
+
const delimiter = sql.slice(i).match(/^\$(?:[A-Za-z_][A-Za-z0-9_]*)?\$/)?.[0];
|
|
20354
|
+
if (delimiter) {
|
|
20355
|
+
i += delimiter.length;
|
|
20356
|
+
const closingIndex = sql.indexOf(delimiter, i);
|
|
20357
|
+
i = closingIndex === -1 ? sql.length : closingIndex + delimiter.length;
|
|
20358
|
+
result += " ";
|
|
20359
|
+
continue;
|
|
20360
|
+
}
|
|
20361
|
+
}
|
|
20362
|
+
if (mysqlDialect && char === "`") {
|
|
20063
20363
|
i++;
|
|
20064
|
-
while (i < sql.length
|
|
20065
|
-
if (sql[i] === "
|
|
20364
|
+
while (i < sql.length) {
|
|
20365
|
+
if (sql[i] === "`") {
|
|
20366
|
+
if (sql[i + 1] === "`") {
|
|
20367
|
+
i += 2;
|
|
20368
|
+
continue;
|
|
20369
|
+
}
|
|
20066
20370
|
i++;
|
|
20371
|
+
break;
|
|
20067
20372
|
}
|
|
20068
20373
|
i++;
|
|
20069
20374
|
}
|
|
20070
|
-
|
|
20375
|
+
result += " ";
|
|
20376
|
+
continue;
|
|
20377
|
+
}
|
|
20378
|
+
if (char === "'" || char === '"') {
|
|
20379
|
+
const quote = char;
|
|
20380
|
+
const quoteIndex = i;
|
|
20381
|
+
const postgresEscapeString = options.dialect === "postgresql" && quote === "'" && /[eE]/.test(sql[quoteIndex - 1] ?? "") && !/[A-Za-z0-9_$]/.test(sql[quoteIndex - 2] ?? "");
|
|
20382
|
+
const backslashEscapes = options.dialect === undefined || postgresEscapeString;
|
|
20383
|
+
i++;
|
|
20384
|
+
while (i < sql.length) {
|
|
20385
|
+
if (sql[i] === quote) {
|
|
20386
|
+
if (sql[i + 1] === quote) {
|
|
20387
|
+
i += 2;
|
|
20388
|
+
continue;
|
|
20389
|
+
}
|
|
20390
|
+
i++;
|
|
20391
|
+
break;
|
|
20392
|
+
}
|
|
20393
|
+
if (backslashEscapes && sql[i] === "\\") {
|
|
20394
|
+
i += 2;
|
|
20395
|
+
continue;
|
|
20396
|
+
}
|
|
20071
20397
|
i++;
|
|
20072
20398
|
}
|
|
20073
20399
|
result += " ";
|
|
@@ -21625,7 +21951,7 @@ function parseJsonSafely(text2) {
|
|
|
21625
21951
|
return;
|
|
21626
21952
|
}
|
|
21627
21953
|
}
|
|
21628
|
-
function
|
|
21954
|
+
function readPath2(obj, path4) {
|
|
21629
21955
|
let cur = obj;
|
|
21630
21956
|
for (const key of path4) {
|
|
21631
21957
|
if (cur === null || cur === undefined || typeof cur !== "object")
|
|
@@ -21638,14 +21964,14 @@ function checkTruthyPath(text2, path4) {
|
|
|
21638
21964
|
const obj = parseJsonSafely(text2);
|
|
21639
21965
|
if (obj === undefined)
|
|
21640
21966
|
return false;
|
|
21641
|
-
const v =
|
|
21967
|
+
const v = readPath2(obj, path4);
|
|
21642
21968
|
return v !== null && v !== undefined && v !== "" && v !== false;
|
|
21643
21969
|
}
|
|
21644
21970
|
function checkExactValue(text2, path4, expected) {
|
|
21645
21971
|
const obj = parseJsonSafely(text2);
|
|
21646
21972
|
if (obj === undefined)
|
|
21647
21973
|
return false;
|
|
21648
|
-
return
|
|
21974
|
+
return readPath2(obj, path4) === expected;
|
|
21649
21975
|
}
|
|
21650
21976
|
|
|
21651
21977
|
// src/core/recovery/apply-verify.ts
|
|
@@ -22144,15 +22470,13 @@ async function schemaAction(table, options, command) {
|
|
|
22144
22470
|
const storagePath = await resolveConfigStoragePath(configPath);
|
|
22145
22471
|
config = await configModule.read(configPath);
|
|
22146
22472
|
if (!config.connection) {
|
|
22147
|
-
|
|
22148
|
-
process.exit(1);
|
|
22473
|
+
throw new Error("Database not configured. Run: dbcli init");
|
|
22149
22474
|
}
|
|
22150
22475
|
let inferenceOptions;
|
|
22151
22476
|
if (options.sampleSize !== undefined) {
|
|
22152
22477
|
const parsed = Number(options.sampleSize);
|
|
22153
22478
|
if (!Number.isFinite(parsed) || parsed < 1) {
|
|
22154
|
-
|
|
22155
|
-
process.exit(1);
|
|
22479
|
+
throw new Error(`--sample-size must be a positive integer (received ${options.sampleSize})`);
|
|
22156
22480
|
}
|
|
22157
22481
|
if (config.connection.system === "mongodb") {
|
|
22158
22482
|
inferenceOptions = { sampleSize: Math.floor(parsed) };
|
|
@@ -22162,8 +22486,7 @@ async function schemaAction(table, options, command) {
|
|
|
22162
22486
|
}
|
|
22163
22487
|
if (options.sampleMethod && options.sampleMethod !== "random") {
|
|
22164
22488
|
if (options.sampleMethod !== "natural") {
|
|
22165
|
-
|
|
22166
|
-
process.exit(1);
|
|
22489
|
+
throw new Error(`--sample-method must be 'random' or 'natural' (received '${options.sampleMethod}')`);
|
|
22167
22490
|
}
|
|
22168
22491
|
if (config.connection.system === "mongodb") {
|
|
22169
22492
|
inferenceOptions = { ...inferenceOptions ?? {}, sampleMethod: "natural" };
|
|
@@ -22185,12 +22508,10 @@ async function schemaAction(table, options, command) {
|
|
|
22185
22508
|
}
|
|
22186
22509
|
if (config.connection.system === "redis") {
|
|
22187
22510
|
if (options.reset || options.refresh) {
|
|
22188
|
-
|
|
22189
|
-
process.exit(1);
|
|
22511
|
+
throw new Error("schema --reset/--refresh is not supported for Redis connections.");
|
|
22190
22512
|
}
|
|
22191
22513
|
if (!table) {
|
|
22192
|
-
|
|
22193
|
-
process.exit(1);
|
|
22514
|
+
throw new Error("Redis schema inspection requires a key name: dbcli schema <key>");
|
|
22194
22515
|
}
|
|
22195
22516
|
const redisAdapter = AdapterFactory.createRedisAdapter(config.connection, config.blacklist?.tables ?? []);
|
|
22196
22517
|
await redisAdapter.connect();
|
|
@@ -22284,13 +22605,7 @@ Key: ${schema.name}`);
|
|
|
22284
22605
|
const { emitRecoveryEnvelope: emitRecoveryEnvelope2 } = await Promise.resolve().then(() => (init_recovery(), exports_recovery));
|
|
22285
22606
|
emitRecoveryEnvelope2(error, { operation: "schema", table }, { envelopeId, auditRef: auditId ?? undefined });
|
|
22286
22607
|
}
|
|
22287
|
-
|
|
22288
|
-
console.error(t_vars("errors.message", { message: error.message }));
|
|
22289
|
-
if (error instanceof ConnectionError) {
|
|
22290
|
-
error.hints.forEach((hint) => console.error(` Hint: ${hint}`));
|
|
22291
|
-
}
|
|
22292
|
-
}
|
|
22293
|
-
process.exit(1);
|
|
22608
|
+
throw error;
|
|
22294
22609
|
}
|
|
22295
22610
|
}
|
|
22296
22611
|
async function handleSingleTableSchema(adapter, tableName, format, inferenceOptions, mongoMeta) {
|
|
@@ -22554,9 +22869,45 @@ var init_schema = __esm(() => {
|
|
|
22554
22869
|
init_core();
|
|
22555
22870
|
init_integration_helper();
|
|
22556
22871
|
init_validation();
|
|
22872
|
+
init_connection_selector();
|
|
22557
22873
|
init_error_suggester();
|
|
22558
22874
|
ALLOWED_FORMATS2 = ["table", "json"];
|
|
22559
|
-
schemaCommand = new Command().name("schema").description("Display table schema, scan database schema, or refresh existing schema with detected changes").argument("[table]", "Optional: table name to inspect (if omitted, scans all tables)").option("--format <format>", "Output format: table (default) or json", "table").option("--config <path>", "Path to .dbcli config file", ".dbcli").option("--refresh", "Refresh schema by detecting changes from database", false).option("--reset", "Clear all existing schema data and re-fetch from database", false).option("--force", "Skip confirmation when updating schema data", false).option("--sample-size <n>", "MongoDB only: number of documents to sample for schema inference (default 50, max 1000). Ignored on SQL connections.").option("--sample-method <method>", 'MongoDB only: "random" (default, uses $sample) or "natural" (uses find().limit()). Ignored on SQL connections.', "random").option("--recovery", "On failure, emit a structured recovery envelope to stdout (suppresses human stderr message)", false).action(schemaAction);
|
|
22875
|
+
schemaCommand = new Command().name("schema").description("Display table schema, scan database schema, or refresh existing schema with detected changes").argument("[table]", "Optional: table name to inspect (if omitted, scans all tables)").option("--format <format>", "Output format: table (default) or json", "table").option("--config <path>", "Path to .dbcli config file", ".dbcli").addOption(createConnectionSelectorOption()).option("--refresh", "Refresh schema by detecting changes from database", false).option("--reset", "Clear all existing schema data and re-fetch from database", false).option("--force", "Skip confirmation when updating schema data", false).option("--sample-size <n>", "MongoDB only: number of documents to sample for schema inference (default 50, max 1000). Ignored on SQL connections.").option("--sample-method <method>", 'MongoDB only: "random" (default, uses $sample) or "natural" (uses find().limit()). Ignored on SQL connections.', "random").option("--recovery", "On failure, emit a structured recovery envelope to stdout (suppresses human stderr message)", false).action(schemaAction);
|
|
22876
|
+
});
|
|
22877
|
+
|
|
22878
|
+
// src/formatters/multi-query-result-formatter.ts
|
|
22879
|
+
class MultiQueryResultFormatter {
|
|
22880
|
+
format(outcomes, options) {
|
|
22881
|
+
if (options.format === "json") {
|
|
22882
|
+
return JSON.stringify({
|
|
22883
|
+
results: outcomes.map((outcome) => outcome.status === "ok" ? {
|
|
22884
|
+
connection: outcome.connection,
|
|
22885
|
+
status: outcome.status,
|
|
22886
|
+
...toPublicQueryResult(outcome.result)
|
|
22887
|
+
} : outcome)
|
|
22888
|
+
}, null, 2);
|
|
22889
|
+
}
|
|
22890
|
+
const formatter = new QueryResultFormatter;
|
|
22891
|
+
return outcomes.map((outcome) => {
|
|
22892
|
+
const heading = `Connection: ${outcome.connection} [${outcome.status}]`;
|
|
22893
|
+
if (outcome.status === "ok") {
|
|
22894
|
+
return `${heading}
|
|
22895
|
+
${formatter.format(outcome.result, {
|
|
22896
|
+
format: "table",
|
|
22897
|
+
truncate: options.truncate
|
|
22898
|
+
})}`;
|
|
22899
|
+
}
|
|
22900
|
+
const prefix = outcome.error.code ? `${outcome.error.code}: ` : "";
|
|
22901
|
+
const hints = outcome.error.hints.map((hint) => `Hint: ${hint}`);
|
|
22902
|
+
return [heading, `${prefix}${outcome.error.message}`, ...hints].join(`
|
|
22903
|
+
`);
|
|
22904
|
+
}).join(`
|
|
22905
|
+
|
|
22906
|
+
`);
|
|
22907
|
+
}
|
|
22908
|
+
}
|
|
22909
|
+
var init_multi_query_result_formatter = __esm(() => {
|
|
22910
|
+
init_query_result_formatter();
|
|
22560
22911
|
});
|
|
22561
22912
|
|
|
22562
22913
|
// src/formatters/html-formatter.ts
|
|
@@ -22594,6 +22945,9 @@ var init_opener = () => {};
|
|
|
22594
22945
|
// src/core/limits.ts
|
|
22595
22946
|
var DEFAULT_QUERY_ONLY_LIMIT = 1000;
|
|
22596
22947
|
|
|
22948
|
+
// src/core/applied-limit.ts
|
|
22949
|
+
var init_applied_limit = () => {};
|
|
22950
|
+
|
|
22597
22951
|
// src/core/query-executor.ts
|
|
22598
22952
|
class QueryExecutor {
|
|
22599
22953
|
adapter;
|
|
@@ -22601,6 +22955,7 @@ class QueryExecutor {
|
|
|
22601
22955
|
blacklistValidator;
|
|
22602
22956
|
config;
|
|
22603
22957
|
options;
|
|
22958
|
+
pendingDiagnostics = [];
|
|
22604
22959
|
constructor(adapter, permission, blacklistValidator, config, options = {}) {
|
|
22605
22960
|
this.adapter = adapter;
|
|
22606
22961
|
this.permission = permission;
|
|
@@ -22608,19 +22963,31 @@ class QueryExecutor {
|
|
|
22608
22963
|
this.config = config;
|
|
22609
22964
|
this.options = options;
|
|
22610
22965
|
}
|
|
22966
|
+
takeDiagnostics() {
|
|
22967
|
+
const diagnostics = this.pendingDiagnostics;
|
|
22968
|
+
this.pendingDiagnostics = [];
|
|
22969
|
+
return diagnostics;
|
|
22970
|
+
}
|
|
22611
22971
|
async execute(sql, options) {
|
|
22612
22972
|
const start = performance.now();
|
|
22973
|
+
this.pendingDiagnostics = [];
|
|
22613
22974
|
try {
|
|
22614
22975
|
const classification = enforcePermission(sql, this.permission);
|
|
22615
|
-
|
|
22616
|
-
console.error(`\u26A0 Warning: executing ${classification.type} operation (admin mode)`);
|
|
22617
|
-
}
|
|
22976
|
+
const dangerousOperationWarning = classification.isDangerous && this.permission === "admin" ? `\u26A0 Warning: executing ${classification.type} operation (admin mode)` : undefined;
|
|
22618
22977
|
const AUTO_LIMIT_TYPES = new Set(["SELECT"]);
|
|
22619
22978
|
let executeSql = sql;
|
|
22620
|
-
|
|
22621
|
-
|
|
22622
|
-
|
|
22623
|
-
|
|
22979
|
+
let appliedLimit;
|
|
22980
|
+
let autoLimitWarning;
|
|
22981
|
+
if (AUTO_LIMIT_TYPES.has(classification.type) && !hasUserAuthoredLimit(executeSql) && options?.autoLimit !== false) {
|
|
22982
|
+
const requestedLimit = options?.limitValue ?? (this.permission === "query-only" ? DEFAULT_QUERY_ONLY_LIMIT : undefined);
|
|
22983
|
+
if (requestedLimit !== undefined) {
|
|
22984
|
+
const fetchLimit = requestedLimit + (options?.detectTruncation === true ? 1 : 0);
|
|
22985
|
+
appliedLimit = options?.detectTruncation === true ? requestedLimit : undefined;
|
|
22986
|
+
executeSql = `${executeSql.replace(/;\s*$/, "")} LIMIT ${fetchLimit}`;
|
|
22987
|
+
if (options?.limitValue === undefined && this.permission === "query-only") {
|
|
22988
|
+
autoLimitWarning = `Query-only mode: auto-limiting to ${requestedLimit} rows`;
|
|
22989
|
+
}
|
|
22990
|
+
}
|
|
22624
22991
|
}
|
|
22625
22992
|
if (this.blacklistValidator) {
|
|
22626
22993
|
const tableName = extractTableName(sql);
|
|
@@ -22630,26 +22997,39 @@ class QueryExecutor {
|
|
|
22630
22997
|
}
|
|
22631
22998
|
const resultData = await this.adapter.execute(executeSql);
|
|
22632
22999
|
const executionTimeMs = Math.round(performance.now() - start);
|
|
22633
|
-
const
|
|
23000
|
+
const limitedResult = appliedLimit === undefined ? undefined : trimAppliedLimit(resultData.rows, appliedLimit);
|
|
23001
|
+
const rows = limitedResult?.rows ?? resultData.rows;
|
|
22634
23002
|
const affectedRows = resultData.affectedRows;
|
|
23003
|
+
const visibleAffectedRows = limitedResult ? rows.length : affectedRows;
|
|
22635
23004
|
let columnNames = rows.length > 0 && rows[0] ? Object.keys(rows[0]) : [];
|
|
22636
|
-
const columnTypes = columnNames.map((col) => {
|
|
22637
|
-
const value = rows[0]?.[col];
|
|
22638
|
-
return inferColumnType(value);
|
|
22639
|
-
});
|
|
22640
23005
|
let filteredRows = rows;
|
|
22641
23006
|
let securityNotification;
|
|
23007
|
+
let omittedColumns = [];
|
|
22642
23008
|
if (this.blacklistValidator) {
|
|
22643
23009
|
const tableName = extractTableName(sql);
|
|
22644
23010
|
if (tableName) {
|
|
22645
23011
|
const filterResult = this.blacklistValidator.filterColumns(tableName, rows, columnNames);
|
|
22646
23012
|
filteredRows = filterResult.filteredRows;
|
|
22647
23013
|
if (filterResult.omittedColumns.length > 0) {
|
|
23014
|
+
omittedColumns = filterResult.omittedColumns;
|
|
22648
23015
|
columnNames = columnNames.filter((col) => !filterResult.omittedColumns.includes(col));
|
|
22649
23016
|
securityNotification = this.blacklistValidator.buildSecurityNotification(tableName, filterResult.omittedColumns);
|
|
22650
23017
|
}
|
|
22651
23018
|
}
|
|
22652
23019
|
}
|
|
23020
|
+
if (options?.fieldSelection) {
|
|
23021
|
+
const fieldSelection = options.fieldSelection.mode === "include" ? {
|
|
23022
|
+
mode: "include",
|
|
23023
|
+
paths: options.fieldSelection.paths.filter((path4) => !omittedColumns.some((omitted) => path4 === omitted || path4.startsWith(`${omitted}.`)))
|
|
23024
|
+
} : options.fieldSelection;
|
|
23025
|
+
const projection = projectRows(filteredRows, fieldSelection);
|
|
23026
|
+
filteredRows = projection.rows;
|
|
23027
|
+
columnNames = projection.columnNames;
|
|
23028
|
+
}
|
|
23029
|
+
const columnTypes = columnNames.map((column) => {
|
|
23030
|
+
const value = filteredRows.find((row) => row[column] !== undefined)?.[column];
|
|
23031
|
+
return inferColumnType(value);
|
|
23032
|
+
});
|
|
22653
23033
|
const result = {
|
|
22654
23034
|
rows: filteredRows,
|
|
22655
23035
|
rowCount: filteredRows.length,
|
|
@@ -22658,20 +23038,30 @@ class QueryExecutor {
|
|
|
22658
23038
|
executionTimeMs,
|
|
22659
23039
|
metadata: {
|
|
22660
23040
|
statement: classification.type,
|
|
22661
|
-
affectedRows,
|
|
23041
|
+
affectedRows: visibleAffectedRows,
|
|
22662
23042
|
...securityNotification ? { securityNotification } : {}
|
|
22663
|
-
}
|
|
23043
|
+
},
|
|
23044
|
+
...limitedResult ? { appliedLimit: limitedResult.metadata } : {}
|
|
22664
23045
|
};
|
|
22665
23046
|
if (this.config) {
|
|
22666
23047
|
await writeAuditEntry(this.config, "query", this.options, {
|
|
22667
23048
|
success: true,
|
|
22668
23049
|
sql,
|
|
22669
23050
|
metadata: {
|
|
22670
|
-
rows_affected:
|
|
23051
|
+
rows_affected: visibleAffectedRows,
|
|
22671
23052
|
execution_ms: executionTimeMs
|
|
22672
23053
|
}
|
|
22673
23054
|
});
|
|
22674
23055
|
}
|
|
23056
|
+
if (this.options.recovery !== true) {
|
|
23057
|
+
const diagnostics = [dangerousOperationWarning, autoLimitWarning].filter((diagnostic) => diagnostic !== undefined);
|
|
23058
|
+
if (this.options.deferDiagnostics === true) {
|
|
23059
|
+
this.pendingDiagnostics = diagnostics;
|
|
23060
|
+
} else {
|
|
23061
|
+
for (const diagnostic of diagnostics)
|
|
23062
|
+
console.error(diagnostic);
|
|
23063
|
+
}
|
|
23064
|
+
}
|
|
22675
23065
|
return result;
|
|
22676
23066
|
} catch (error) {
|
|
22677
23067
|
if (this.config) {
|
|
@@ -22705,6 +23095,10 @@ class QueryExecutor {
|
|
|
22705
23095
|
}
|
|
22706
23096
|
}
|
|
22707
23097
|
}
|
|
23098
|
+
function hasUserAuthoredLimit(sql) {
|
|
23099
|
+
const executableSql = stripCommentsAndStrings2(sql).replace(/`(?:``|[^`])*`/g, " ");
|
|
23100
|
+
return /\bLIMIT\s+(?:\(\s*)?(?:\d+|ALL\b|\?|\$\d+|:[A-Za-z_][A-Za-z0-9_]*)/i.test(executableSql);
|
|
23101
|
+
}
|
|
22708
23102
|
function inferColumnType(value) {
|
|
22709
23103
|
if (value === null || value === undefined) {
|
|
22710
23104
|
return "null";
|
|
@@ -22727,7 +23121,9 @@ var init_query_executor = __esm(() => {
|
|
|
22727
23121
|
init_permission_guard();
|
|
22728
23122
|
init_error_suggester();
|
|
22729
23123
|
init_engine_hints();
|
|
23124
|
+
init_applied_limit();
|
|
22730
23125
|
init_integration_helper();
|
|
23126
|
+
init_field_projection();
|
|
22731
23127
|
});
|
|
22732
23128
|
|
|
22733
23129
|
// src/core/mongo/field-masker.ts
|
|
@@ -22783,6 +23179,152 @@ function findCaseInsensitive(columns, name2) {
|
|
|
22783
23179
|
var REDACTED2 = "[REDACTED]";
|
|
22784
23180
|
var init_field_masker = () => {};
|
|
22785
23181
|
|
|
23182
|
+
// src/core/query-input.ts
|
|
23183
|
+
function trimOuterWhitespace(value) {
|
|
23184
|
+
let start = 0;
|
|
23185
|
+
let end = value.length;
|
|
23186
|
+
while (start < end) {
|
|
23187
|
+
const character = String.fromCodePoint(value.codePointAt(start));
|
|
23188
|
+
if (character === "\uFEFF" || !WHITESPACE.test(character))
|
|
23189
|
+
break;
|
|
23190
|
+
start += character.length;
|
|
23191
|
+
}
|
|
23192
|
+
while (end > start) {
|
|
23193
|
+
const codePoint = value.codePointAt(end - 1);
|
|
23194
|
+
const character = String.fromCodePoint(codePoint);
|
|
23195
|
+
if (character === "\uFEFF" || !WHITESPACE.test(character))
|
|
23196
|
+
break;
|
|
23197
|
+
end -= character.length;
|
|
23198
|
+
}
|
|
23199
|
+
return value.slice(start, end);
|
|
23200
|
+
}
|
|
23201
|
+
function errorMessage(error) {
|
|
23202
|
+
return error instanceof Error ? error.message : String(error);
|
|
23203
|
+
}
|
|
23204
|
+
async function resolveQueryInput(sources, readers = defaultReaders) {
|
|
23205
|
+
const hasPositional = sources.positional !== undefined;
|
|
23206
|
+
const hasQueryFile = sources.queryFile !== undefined;
|
|
23207
|
+
if (hasPositional && hasQueryFile) {
|
|
23208
|
+
throw new Error("Query source conflict: provide either positional query text or --query-file <path>, not both");
|
|
23209
|
+
}
|
|
23210
|
+
if (!hasPositional && !hasQueryFile) {
|
|
23211
|
+
throw new Error("Exactly one query source is required: provide positional query text or --query-file <path>");
|
|
23212
|
+
}
|
|
23213
|
+
let input;
|
|
23214
|
+
if (hasPositional) {
|
|
23215
|
+
input = sources.positional;
|
|
23216
|
+
} else if (sources.queryFile === "-") {
|
|
23217
|
+
if (readers.stdinIsInteractive?.() === true) {
|
|
23218
|
+
throw new Error("Reading the query from stdin requires piped input; pipe the query in or use --query-file <path>");
|
|
23219
|
+
}
|
|
23220
|
+
try {
|
|
23221
|
+
input = await readers.readStdin();
|
|
23222
|
+
} catch (error) {
|
|
23223
|
+
throw new Error(`Failed to read query from stdin: ${errorMessage(error)}`);
|
|
23224
|
+
}
|
|
23225
|
+
} else {
|
|
23226
|
+
const path4 = sources.queryFile;
|
|
23227
|
+
try {
|
|
23228
|
+
input = await readers.readFile(path4);
|
|
23229
|
+
} catch (error) {
|
|
23230
|
+
throw new Error(`Failed to read query file "${path4}": ${errorMessage(error)}`);
|
|
23231
|
+
}
|
|
23232
|
+
}
|
|
23233
|
+
const withoutBom = input.startsWith("\uFEFF") ? input.slice(1) : input;
|
|
23234
|
+
const query = trimOuterWhitespace(withoutBom);
|
|
23235
|
+
if (query.length === 0) {
|
|
23236
|
+
throw new Error("Query input is empty");
|
|
23237
|
+
}
|
|
23238
|
+
return query;
|
|
23239
|
+
}
|
|
23240
|
+
var defaultReaders, WHITESPACE;
|
|
23241
|
+
var init_query_input = __esm(() => {
|
|
23242
|
+
defaultReaders = {
|
|
23243
|
+
readFile: (path4) => Bun.file(path4).text(),
|
|
23244
|
+
readStdin: () => Bun.stdin.text(),
|
|
23245
|
+
stdinIsInteractive: () => {
|
|
23246
|
+
try {
|
|
23247
|
+
return process.stdin.isTTY === true;
|
|
23248
|
+
} catch {
|
|
23249
|
+
return false;
|
|
23250
|
+
}
|
|
23251
|
+
}
|
|
23252
|
+
};
|
|
23253
|
+
WHITESPACE = /\s/u;
|
|
23254
|
+
});
|
|
23255
|
+
|
|
23256
|
+
// src/utils/cli-error.ts
|
|
23257
|
+
function mapCliError(error, includeStack = false) {
|
|
23258
|
+
const candidate = error !== null && (typeof error === "object" || typeof error === "function") ? error : undefined;
|
|
23259
|
+
const message = typeof candidate?.message === "string" && candidate.message.trim() !== "" ? candidate.message : typeof error === "string" && error.trim() !== "" ? error : "An unexpected error occurred";
|
|
23260
|
+
const code = typeof candidate?.code === "string" || typeof candidate?.code === "number" ? String(candidate.code) : undefined;
|
|
23261
|
+
const hints = Array.isArray(candidate?.hints) ? candidate.hints.filter((hint) => typeof hint === "string" && hint !== "") : [];
|
|
23262
|
+
const stack = includeStack && typeof candidate?.stack === "string" && candidate.stack !== "" ? candidate.stack : undefined;
|
|
23263
|
+
return { message, ...code && { code }, hints, ...stack && { stack } };
|
|
23264
|
+
}
|
|
23265
|
+
function formatCliError(presentation) {
|
|
23266
|
+
const lines = [presentation.message];
|
|
23267
|
+
if (presentation.code)
|
|
23268
|
+
lines.push(`Code: ${presentation.code}`);
|
|
23269
|
+
for (const hint of presentation.hints)
|
|
23270
|
+
lines.push(`Hint: ${hint}`);
|
|
23271
|
+
if (presentation.stack)
|
|
23272
|
+
lines.push("Stack:", presentation.stack);
|
|
23273
|
+
return lines.join(`
|
|
23274
|
+
`);
|
|
23275
|
+
}
|
|
23276
|
+
function presentCliError(error) {
|
|
23277
|
+
const includeStack = getLogger().level >= 2 /* VERBOSE */;
|
|
23278
|
+
process.stderr.write(`${formatCliError(mapCliError(error, includeStack))}
|
|
23279
|
+
`);
|
|
23280
|
+
}
|
|
23281
|
+
function printLocalizedCliError(message, error) {
|
|
23282
|
+
console.error(message);
|
|
23283
|
+
const stack = mapCliError(error, getLogger().level >= 2 /* VERBOSE */).stack;
|
|
23284
|
+
if (stack)
|
|
23285
|
+
console.error(`Stack:
|
|
23286
|
+
${stack}`);
|
|
23287
|
+
}
|
|
23288
|
+
var init_cli_error = __esm(() => {
|
|
23289
|
+
init_logger();
|
|
23290
|
+
});
|
|
23291
|
+
|
|
23292
|
+
// src/core/query-fanout.ts
|
|
23293
|
+
async function runQueryFanOut(connectionNames, execute) {
|
|
23294
|
+
const settled = await Promise.allSettled(connectionNames.map((name2) => execute(name2)));
|
|
23295
|
+
return settled.map((outcome, index) => {
|
|
23296
|
+
const connection = connectionNames[index];
|
|
23297
|
+
return outcome.status === "fulfilled" ? { connection, status: "ok", result: outcome.value } : { connection, status: "error", error: mapCliError(outcome.reason) };
|
|
23298
|
+
});
|
|
23299
|
+
}
|
|
23300
|
+
function aggregateFanOutExitCode(outcomes) {
|
|
23301
|
+
const successCount = outcomes.filter((outcome) => outcome.status === "ok").length;
|
|
23302
|
+
if (successCount === outcomes.length)
|
|
23303
|
+
return 0;
|
|
23304
|
+
if (successCount === 0)
|
|
23305
|
+
return 1;
|
|
23306
|
+
return 2;
|
|
23307
|
+
}
|
|
23308
|
+
function assertFanOutReadOnlySql(sql, dialect) {
|
|
23309
|
+
const classification = enforcePermission(sql, "query-only");
|
|
23310
|
+
const executableSql = stripCommentsAndStrings2(sql, { dialect });
|
|
23311
|
+
const statements = executableSql.split(";").map((statement) => statement.trim()).filter(Boolean);
|
|
23312
|
+
if (statements.length !== 1) {
|
|
23313
|
+
throw new Error("Multi-connection SQL must contain exactly one read-only statement");
|
|
23314
|
+
}
|
|
23315
|
+
const containsWrite = SQL_WRITE_OR_DDL_KEYWORDS.test(executableSql);
|
|
23316
|
+
const explainExecutes = classification.type === "EXPLAIN" && /\bANALYZE\b/i.test(executableSql);
|
|
23317
|
+
if (classification.type === "SELECT" && containsWrite || explainExecutes && containsWrite) {
|
|
23318
|
+
throw new Error("Multi-connection SQL must be proven read-only before any connection executes");
|
|
23319
|
+
}
|
|
23320
|
+
}
|
|
23321
|
+
var SQL_WRITE_OR_DDL_KEYWORDS;
|
|
23322
|
+
var init_query_fanout = __esm(() => {
|
|
23323
|
+
init_cli_error();
|
|
23324
|
+
init_permission_guard();
|
|
23325
|
+
SQL_WRITE_OR_DDL_KEYWORDS = /\b(INSERT|UPDATE|DELETE|MERGE|UPSERT|REPLACE|TRUNCATE|DROP|ALTER|CREATE|GRANT|REVOKE|RENAME|INTO)\b/i;
|
|
23326
|
+
});
|
|
23327
|
+
|
|
22786
23328
|
// src/commands/query-size-guard.ts
|
|
22787
23329
|
var exports_query_size_guard = {};
|
|
22788
23330
|
__export(exports_query_size_guard, {
|
|
@@ -22827,90 +23369,45 @@ function requireSqlConnection2(connection) {
|
|
|
22827
23369
|
}
|
|
22828
23370
|
async function queryCommand(sql, options, command) {
|
|
22829
23371
|
let config;
|
|
23372
|
+
let resolvedSql = sql ?? "";
|
|
23373
|
+
let configPath = options.config ?? ".dbcli";
|
|
23374
|
+
let connectionName;
|
|
23375
|
+
let multiConnectionRequest = false;
|
|
22830
23376
|
try {
|
|
22831
|
-
|
|
22832
|
-
|
|
23377
|
+
resolvedSql = await resolveQueryInput({ positional: sql, queryFile: options.queryFile });
|
|
23378
|
+
const fieldSelection = parseFieldSelection(options.fields);
|
|
23379
|
+
if (options.limit !== undefined && (!Number.isInteger(options.limit) || options.limit <= 0)) {
|
|
23380
|
+
throw new Error("--limit must be a positive integer");
|
|
22833
23381
|
}
|
|
22834
23382
|
if (options.format) {
|
|
22835
23383
|
validateFormat(options.format, [...ALLOWED_FORMATS3, "html"], "query");
|
|
22836
23384
|
}
|
|
22837
|
-
|
|
22838
|
-
|
|
22839
|
-
|
|
22840
|
-
|
|
22841
|
-
|
|
22842
|
-
|
|
22843
|
-
|
|
22844
|
-
return
|
|
22845
|
-
}
|
|
22846
|
-
if (config.connection.system === "redis") {
|
|
22847
|
-
return await redisQueryBranch(sql, options, config);
|
|
22848
|
-
}
|
|
22849
|
-
if (config.connection.system === "elasticsearch") {
|
|
22850
|
-
return await elasticsearchQueryBranch(sql, options, config);
|
|
22851
|
-
}
|
|
22852
|
-
const { extractTableName: extractTableName2 } = await Promise.resolve().then(() => (init_engine_hints(), exports_engine_hints));
|
|
22853
|
-
const mainTable = extractTableName2(sql);
|
|
22854
|
-
if (mainTable && config.schema && !options.noLimit) {
|
|
22855
|
-
const tableSchema = config.schema[mainTable];
|
|
22856
|
-
if (tableSchema) {
|
|
22857
|
-
const { shouldBlockQuery: shouldBlockQuery2 } = await Promise.resolve().then(() => (init_query_size_guard(), exports_query_size_guard));
|
|
22858
|
-
const guard = shouldBlockQuery2(sql, tableSchema);
|
|
22859
|
-
if (guard.blocked) {
|
|
22860
|
-
throw new Error(`\u26A0 ${guard.reason}`);
|
|
22861
|
-
}
|
|
22862
|
-
}
|
|
22863
|
-
}
|
|
22864
|
-
const adapter = AdapterFactory.createSqlAdapter(requireSqlConnection2(config.connection));
|
|
22865
|
-
await adapter.connect();
|
|
22866
|
-
try {
|
|
22867
|
-
const blacklistManager = new BlacklistManager(config);
|
|
22868
|
-
const blacklistValidator = new BlacklistValidator(blacklistManager);
|
|
22869
|
-
const executor3 = new QueryExecutor(adapter, config.permission, blacklistValidator, config, options);
|
|
22870
|
-
const autoLimit = !options.noLimit;
|
|
22871
|
-
const result = await executor3.execute(sql, {
|
|
22872
|
-
autoLimit,
|
|
22873
|
-
limitValue: options.limit
|
|
22874
|
-
});
|
|
22875
|
-
if (options.ui || options.format === "html") {
|
|
22876
|
-
const html = await generateHtmlReport({
|
|
22877
|
-
meta: {
|
|
22878
|
-
name: "Query Results",
|
|
22879
|
-
key: "raw-sql",
|
|
22880
|
-
params: [],
|
|
22881
|
-
tags: [],
|
|
22882
|
-
description: sql.length > 100 ? sql.slice(0, 97) + "..." : sql
|
|
22883
|
-
},
|
|
22884
|
-
rows: result.rows
|
|
22885
|
-
});
|
|
22886
|
-
if (options.ui) {
|
|
22887
|
-
const tempPath = join20(tmpdir(), `dbcli-query-${Date.now()}.html`);
|
|
22888
|
-
await Bun.write(tempPath, html);
|
|
22889
|
-
await openInBrowser(tempPath);
|
|
22890
|
-
} else {
|
|
22891
|
-
console.log(html);
|
|
22892
|
-
}
|
|
22893
|
-
return;
|
|
22894
|
-
}
|
|
22895
|
-
const formatter = new QueryResultFormatter;
|
|
22896
|
-
const output = formatter.format(result, {
|
|
22897
|
-
format: options.format || "table"
|
|
22898
|
-
});
|
|
22899
|
-
console.log(output);
|
|
22900
|
-
} finally {
|
|
22901
|
-
await adapter.disconnect();
|
|
23385
|
+
const tableCellLimit = resolveTableCellLimit(options);
|
|
23386
|
+
configPath = resolveConfigPath(command, options);
|
|
23387
|
+
multiConnectionRequest = options.connectionSelector?.includes(",") ?? false;
|
|
23388
|
+
const connectionNames = options.connectionSelector === undefined ? [] : parseConnectionNames(options.connectionSelector);
|
|
23389
|
+
multiConnectionRequest = connectionNames.length > 1;
|
|
23390
|
+
if (multiConnectionRequest) {
|
|
23391
|
+
await runMultiConnectionQuery(resolvedSql, options, configPath, connectionNames, fieldSelection, tableCellLimit);
|
|
23392
|
+
return;
|
|
22902
23393
|
}
|
|
23394
|
+
connectionName = connectionNames[0];
|
|
23395
|
+
config = await configModule.read(configPath, connectionName);
|
|
23396
|
+
const context = { config, configPath, connectionName };
|
|
23397
|
+
await preflightQuery(resolvedSql, options, context, fieldSelection, false);
|
|
23398
|
+
const execution = await executeConnectionQuery(resolvedSql, options, context, fieldSelection);
|
|
23399
|
+
await presentSingleResult(resolvedSql, options, execution, tableCellLimit);
|
|
22903
23400
|
} catch (error) {
|
|
22904
23401
|
let auditId = null;
|
|
22905
23402
|
let envelopeId;
|
|
22906
|
-
if (options.recovery === true) {
|
|
23403
|
+
if (options.recovery === true && !multiConnectionRequest) {
|
|
22907
23404
|
envelopeId = crypto3.randomUUID();
|
|
22908
23405
|
}
|
|
22909
23406
|
const alreadyAudited = error?.__auditWritten === true;
|
|
22910
23407
|
if (config && !alreadyAudited) {
|
|
22911
|
-
auditId = await writeAuditEntry(config, "query", options, {
|
|
23408
|
+
auditId = await writeAuditEntry(config, "query", { ...options, config: configPath, connectionName }, {
|
|
22912
23409
|
success: false,
|
|
22913
|
-
sql,
|
|
23410
|
+
sql: resolvedSql,
|
|
22914
23411
|
error,
|
|
22915
23412
|
...envelopeId && { recovery_ref: envelopeId }
|
|
22916
23413
|
});
|
|
@@ -22919,62 +23416,288 @@ async function queryCommand(sql, options, command) {
|
|
|
22919
23416
|
const { emitRecoveryEnvelope: emitRecoveryEnvelope2 } = await Promise.resolve().then(() => (init_recovery(), exports_recovery));
|
|
22920
23417
|
emitRecoveryEnvelope2(error, {
|
|
22921
23418
|
operation: "query",
|
|
22922
|
-
table: (await Promise.resolve().then(() => (init_engine_hints(), exports_engine_hints))).extractTableName(
|
|
23419
|
+
table: (await Promise.resolve().then(() => (init_engine_hints(), exports_engine_hints))).extractTableName(resolvedSql) ?? undefined
|
|
22923
23420
|
}, {
|
|
22924
23421
|
envelopeId,
|
|
22925
23422
|
auditRef: auditId ?? undefined
|
|
22926
23423
|
});
|
|
22927
23424
|
}
|
|
22928
|
-
|
|
22929
|
-
|
|
22930
|
-
|
|
22931
|
-
|
|
22932
|
-
|
|
22933
|
-
|
|
22934
|
-
|
|
22935
|
-
|
|
22936
|
-
|
|
22937
|
-
|
|
22938
|
-
|
|
22939
|
-
|
|
22940
|
-
|
|
23425
|
+
throw error;
|
|
23426
|
+
}
|
|
23427
|
+
}
|
|
23428
|
+
async function runMultiConnectionQuery(query, options, configPath, connectionNames, fieldSelection, tableCellLimit) {
|
|
23429
|
+
if (options.recovery === true) {
|
|
23430
|
+
throw new Error("--recovery is not supported with multiple connections");
|
|
23431
|
+
}
|
|
23432
|
+
if (options.ui === true || !["table", "json"].includes(options.format ?? "table")) {
|
|
23433
|
+
throw new Error("Multiple connections support only --format table or --format json");
|
|
23434
|
+
}
|
|
23435
|
+
const contexts = [];
|
|
23436
|
+
for (const connectionName of connectionNames) {
|
|
23437
|
+
const config = await configModule.read(configPath, connectionName);
|
|
23438
|
+
contexts.push({ config, configPath, connectionName });
|
|
23439
|
+
}
|
|
23440
|
+
for (const context of contexts) {
|
|
23441
|
+
await preflightQuery(query, options, context, fieldSelection, true);
|
|
23442
|
+
}
|
|
23443
|
+
const executions = new Map;
|
|
23444
|
+
const contextsByName = new Map(contexts.map((context) => [context.connectionName, context]));
|
|
23445
|
+
const outcomes = await runQueryFanOut(connectionNames, async (selectedName) => {
|
|
23446
|
+
const context = contextsByName.get(selectedName);
|
|
23447
|
+
const execution = scopeMultiConnectionNotices(await executeConnectionQuery(query, options, context, fieldSelection));
|
|
23448
|
+
executions.set(selectedName, execution);
|
|
23449
|
+
return execution.result;
|
|
23450
|
+
});
|
|
23451
|
+
const format = options.format ?? "table";
|
|
23452
|
+
const formatter = new MultiQueryResultFormatter;
|
|
23453
|
+
console.log(formatter.format(outcomes, {
|
|
23454
|
+
format,
|
|
23455
|
+
truncate: format === "table" ? tableCellLimit : false
|
|
23456
|
+
}));
|
|
23457
|
+
for (const connection of connectionNames) {
|
|
23458
|
+
const execution = executions.get(connection);
|
|
23459
|
+
if (!execution)
|
|
23460
|
+
continue;
|
|
23461
|
+
for (const diagnostic of execution.diagnostics)
|
|
23462
|
+
console.error(diagnostic);
|
|
23463
|
+
}
|
|
23464
|
+
process.exitCode = aggregateFanOutExitCode(outcomes);
|
|
23465
|
+
}
|
|
23466
|
+
function scopeMultiConnectionNotices(execution) {
|
|
23467
|
+
if (execution.notices.length === 0)
|
|
23468
|
+
return execution;
|
|
23469
|
+
const securityNotification = execution.notices.map((notice) => notice.trim().replace(/^\u2139\s*/, "")).filter(Boolean).join(`
|
|
23470
|
+
`);
|
|
23471
|
+
if (!securityNotification)
|
|
23472
|
+
return { ...execution, notices: [] };
|
|
23473
|
+
return {
|
|
23474
|
+
...execution,
|
|
23475
|
+
result: {
|
|
23476
|
+
...execution.result,
|
|
23477
|
+
metadata: {
|
|
23478
|
+
statement: execution.result.metadata?.statement ?? "SELECT",
|
|
23479
|
+
...execution.result.metadata,
|
|
23480
|
+
securityNotification
|
|
23481
|
+
}
|
|
23482
|
+
},
|
|
23483
|
+
notices: []
|
|
23484
|
+
};
|
|
23485
|
+
}
|
|
23486
|
+
async function preflightQuery(query, options, context, fieldSelection, multiConnection) {
|
|
23487
|
+
const { config } = context;
|
|
23488
|
+
if (!config.connection)
|
|
23489
|
+
throw new Error('Run "dbcli init" first');
|
|
23490
|
+
const system = config.connection.system;
|
|
23491
|
+
if (fieldSelection && (system === "redis" || system === "elasticsearch")) {
|
|
23492
|
+
throw new Error(`--fields is not supported for ${system} queries`);
|
|
23493
|
+
}
|
|
23494
|
+
if (system === "redis") {
|
|
23495
|
+
if (multiConnection) {
|
|
23496
|
+
throw new Error("Redis queries do not support multiple connections");
|
|
22941
23497
|
}
|
|
22942
|
-
|
|
22943
|
-
|
|
23498
|
+
const { enforceRedisPermission: enforceRedisPermission2 } = await Promise.resolve().then(() => (init_permission_guard(), exports_permission_guard));
|
|
23499
|
+
enforceRedisPermission2(query, config.permission);
|
|
23500
|
+
return;
|
|
23501
|
+
}
|
|
23502
|
+
if (system === "mongodb") {
|
|
23503
|
+
await preflightMongoQuery(query, options, config, multiConnection);
|
|
23504
|
+
return;
|
|
23505
|
+
}
|
|
23506
|
+
if (system === "elasticsearch") {
|
|
23507
|
+
preflightElasticsearchQuery(query, options, config, multiConnection);
|
|
23508
|
+
return;
|
|
22944
23509
|
}
|
|
23510
|
+
if (multiConnection)
|
|
23511
|
+
assertFanOutReadOnlySql(query, system);
|
|
23512
|
+
await preflightSqlSizeGuard(query, options, config);
|
|
23513
|
+
}
|
|
23514
|
+
async function preflightSqlSizeGuard(query, options, config) {
|
|
23515
|
+
const { extractTableName: extractTableName2 } = await Promise.resolve().then(() => (init_engine_hints(), exports_engine_hints));
|
|
23516
|
+
const mainTable = extractTableName2(query);
|
|
23517
|
+
if (!mainTable || !config.schema || options.noLimit)
|
|
23518
|
+
return;
|
|
23519
|
+
const tableSchema = config.schema[mainTable];
|
|
23520
|
+
if (!tableSchema)
|
|
23521
|
+
return;
|
|
23522
|
+
const { shouldBlockQuery: shouldBlockQuery2 } = await Promise.resolve().then(() => (init_query_size_guard(), exports_query_size_guard));
|
|
23523
|
+
const guard = shouldBlockQuery2(query, tableSchema);
|
|
23524
|
+
if (guard.blocked)
|
|
23525
|
+
throw new Error(`\u26A0 ${guard.reason}`);
|
|
22945
23526
|
}
|
|
22946
|
-
async function
|
|
23527
|
+
async function preflightMongoQuery(query, options, config, multiConnection) {
|
|
22947
23528
|
const collection = options.collection;
|
|
22948
|
-
|
|
22949
|
-
if (!collection) {
|
|
23529
|
+
if (!collection)
|
|
22950
23530
|
throw new Error("MongoDB \u67E5\u8A62\u9700\u8981\u6307\u5B9A --collection <name>");
|
|
22951
|
-
}
|
|
22952
23531
|
const SQL_PATTERN = /^\s*(SELECT|INSERT|UPDATE|DELETE|CREATE|DROP|ALTER|SHOW|DESCRIBE)\b/i;
|
|
22953
|
-
if (SQL_PATTERN.test(
|
|
23532
|
+
if (SQL_PATTERN.test(query)) {
|
|
22954
23533
|
throw new Error(`\u9019\u662F MongoDB \u9023\u7DDA\uFF0C\u8ACB\u4F7F\u7528 JSON filter \u8A9E\u6CD5\u3002
|
|
22955
23534
|
\u7BC4\u4F8B\uFF1Adbcli query '{"field": "value"}' --collection <name>`);
|
|
22956
23535
|
}
|
|
23536
|
+
let parsedQuery;
|
|
22957
23537
|
try {
|
|
22958
|
-
JSON.parse(
|
|
23538
|
+
parsedQuery = JSON.parse(query);
|
|
22959
23539
|
} catch {
|
|
22960
23540
|
throw new Error("MongoDB \u67E5\u8A62\u5FC5\u9808\u662F\u6709\u6548\u7684 JSON\uFF08object filter \u6216 array pipeline\uFF09");
|
|
22961
23541
|
}
|
|
22962
|
-
|
|
22963
|
-
|
|
23542
|
+
if (multiConnection && (parsedQuery === null || typeof parsedQuery !== "object" && !Array.isArray(parsedQuery))) {
|
|
23543
|
+
throw new Error("MongoDB multi-connection query must be an object filter or array pipeline");
|
|
23544
|
+
}
|
|
23545
|
+
if (multiConnection && Array.isArray(parsedQuery) && parsedQuery.some((stage) => stage !== null && typeof stage === "object" && (Object.prototype.hasOwnProperty.call(stage, "$out") || Object.prototype.hasOwnProperty.call(stage, "$merge")))) {
|
|
23546
|
+
throw new Error("MongoDB multi-connection pipelines cannot contain $out or $merge");
|
|
23547
|
+
}
|
|
23548
|
+
const blacklistValidator = new BlacklistValidator(new BlacklistManager(config));
|
|
22964
23549
|
blacklistValidator.checkTableBlacklist("SELECT", collection, []);
|
|
22965
|
-
if (config.schema
|
|
22966
|
-
|
|
22967
|
-
|
|
22968
|
-
|
|
22969
|
-
|
|
22970
|
-
|
|
22971
|
-
|
|
22972
|
-
|
|
22973
|
-
|
|
22974
|
-
|
|
23550
|
+
if (!config.schema || options.noLimit)
|
|
23551
|
+
return;
|
|
23552
|
+
const tableSchema = config.schema[collection];
|
|
23553
|
+
if (!tableSchema)
|
|
23554
|
+
return;
|
|
23555
|
+
const { shouldBlockQuery: shouldBlockQuery2 } = await Promise.resolve().then(() => (init_query_size_guard(), exports_query_size_guard));
|
|
23556
|
+
const isFiltered = query.length > 2;
|
|
23557
|
+
const hasLimit = options.limit !== undefined;
|
|
23558
|
+
const dummySql = `SELECT * FROM ${collection}${isFiltered ? " WHERE" : ""}${hasLimit ? " LIMIT" : ""}`;
|
|
23559
|
+
const guard = shouldBlockQuery2(dummySql, tableSchema);
|
|
23560
|
+
if (guard.blocked)
|
|
23561
|
+
throw new Error(`\u26A0 ${guard.reason}`);
|
|
23562
|
+
}
|
|
23563
|
+
function preflightElasticsearchQuery(query, options, config, multiConnection) {
|
|
23564
|
+
const indexName = options.index ?? options.collection;
|
|
23565
|
+
if (!indexName) {
|
|
23566
|
+
throw new Error("Elasticsearch \u67E5\u8A62\u9700\u8981\u6307\u5B9A --collection <index> \u6216 --index <index>");
|
|
23567
|
+
}
|
|
23568
|
+
const body = query.trim().startsWith("{") ? query : undefined;
|
|
23569
|
+
if (body !== undefined)
|
|
23570
|
+
JSON.parse(body);
|
|
23571
|
+
enforceElasticsearchPermission({ method: "POST", apiPath: `/${indexName}/_search`, body }, multiConnection ? "query-only" : config.permission);
|
|
23572
|
+
const blacklistValidator = new BlacklistValidator(new BlacklistManager(config));
|
|
23573
|
+
blacklistValidator.checkTableBlacklist("SELECT", indexName, []);
|
|
23574
|
+
}
|
|
23575
|
+
async function executeConnectionQuery(query, options, context, fieldSelection) {
|
|
23576
|
+
const { config, configPath, connectionName } = context;
|
|
23577
|
+
const system = config.connection.system;
|
|
23578
|
+
const auditOptions = { ...options, config: configPath, connectionName };
|
|
23579
|
+
const start = performance.now();
|
|
23580
|
+
try {
|
|
23581
|
+
let execution;
|
|
23582
|
+
if (system === "mongodb") {
|
|
23583
|
+
execution = await mongoQueryBranch(query, options, context, fieldSelection);
|
|
23584
|
+
} else if (system === "redis") {
|
|
23585
|
+
execution = await redisQueryBranch(query, options, context);
|
|
23586
|
+
} else if (system === "elasticsearch") {
|
|
23587
|
+
execution = await elasticsearchQueryBranch(query, options, context);
|
|
23588
|
+
} else {
|
|
23589
|
+
execution = await sqlQueryBranch(query, options, context, fieldSelection);
|
|
23590
|
+
}
|
|
23591
|
+
await writeAuditEntry(config, "query", auditOptions, {
|
|
23592
|
+
success: true,
|
|
23593
|
+
sql: query,
|
|
23594
|
+
target: queryAuditTarget(system, query, options),
|
|
23595
|
+
metadata: {
|
|
23596
|
+
rows_affected: execution.result.rowCount,
|
|
23597
|
+
execution_ms: execution.result.executionTimeMs ?? Math.round(performance.now() - start)
|
|
22975
23598
|
}
|
|
23599
|
+
});
|
|
23600
|
+
return execution;
|
|
23601
|
+
} catch (error) {
|
|
23602
|
+
if (options.recovery !== true) {
|
|
23603
|
+
const metadata = error instanceof BlacklistRejection ? {
|
|
23604
|
+
rejection_reason: "blacklist",
|
|
23605
|
+
matched_pattern: error.matchedPattern,
|
|
23606
|
+
...error.matchedKey ? { matched_key: error.matchedKey } : {},
|
|
23607
|
+
execution_ms: Math.round(performance.now() - start)
|
|
23608
|
+
} : { execution_ms: Math.round(performance.now() - start) };
|
|
23609
|
+
await writeAuditEntry(config, "query", auditOptions, {
|
|
23610
|
+
success: false,
|
|
23611
|
+
sql: query,
|
|
23612
|
+
target: queryAuditTarget(system, query, options),
|
|
23613
|
+
error,
|
|
23614
|
+
metadata
|
|
23615
|
+
});
|
|
23616
|
+
error.__auditWritten = true;
|
|
23617
|
+
}
|
|
23618
|
+
throw error;
|
|
23619
|
+
}
|
|
23620
|
+
}
|
|
23621
|
+
function queryAuditTarget(system, query, options) {
|
|
23622
|
+
if (system === "mongodb")
|
|
23623
|
+
return options.collection;
|
|
23624
|
+
if (system === "elasticsearch")
|
|
23625
|
+
return options.index ?? options.collection;
|
|
23626
|
+
if (system === "redis")
|
|
23627
|
+
return query.trim().split(/\s+/)[1] || "<unknown-key>";
|
|
23628
|
+
return;
|
|
23629
|
+
}
|
|
23630
|
+
async function sqlQueryBranch(query, options, context, fieldSelection) {
|
|
23631
|
+
const { config } = context;
|
|
23632
|
+
const adapter = AdapterFactory.createSqlAdapter(requireSqlConnection2(config.connection));
|
|
23633
|
+
let executionError;
|
|
23634
|
+
try {
|
|
23635
|
+
await adapter.connect();
|
|
23636
|
+
const blacklistValidator = new BlacklistValidator(new BlacklistManager(config));
|
|
23637
|
+
const executor3 = new QueryExecutor(adapter, config.permission, blacklistValidator, undefined, {
|
|
23638
|
+
...options,
|
|
23639
|
+
deferDiagnostics: true
|
|
23640
|
+
});
|
|
23641
|
+
const result = await executor3.execute(query, {
|
|
23642
|
+
autoLimit: !options.noLimit,
|
|
23643
|
+
limitValue: options.limit,
|
|
23644
|
+
detectTruncation: true,
|
|
23645
|
+
fieldSelection
|
|
23646
|
+
});
|
|
23647
|
+
return { result, diagnostics: executor3.takeDiagnostics(), notices: [] };
|
|
23648
|
+
} catch (error) {
|
|
23649
|
+
executionError = error;
|
|
23650
|
+
throw error;
|
|
23651
|
+
} finally {
|
|
23652
|
+
await disconnectPreservingError(adapter, executionError);
|
|
23653
|
+
}
|
|
23654
|
+
}
|
|
23655
|
+
async function presentSingleResult(query, options, execution, tableCellLimit) {
|
|
23656
|
+
const noticeText = execution.notices.map((notice) => notice.trim().replace(/^\u2139\s*/, "")).filter(Boolean).join(`
|
|
23657
|
+
`);
|
|
23658
|
+
if (options.ui || options.format === "html") {
|
|
23659
|
+
const html = await generateHtmlReport({
|
|
23660
|
+
meta: {
|
|
23661
|
+
name: "Query Results",
|
|
23662
|
+
key: "raw-sql",
|
|
23663
|
+
params: [],
|
|
23664
|
+
tags: [],
|
|
23665
|
+
description: query.length > 100 ? query.slice(0, 97) + "..." : query
|
|
23666
|
+
},
|
|
23667
|
+
rows: execution.result.rows,
|
|
23668
|
+
...execution.result.appliedLimit ? { appliedLimit: execution.result.appliedLimit } : {},
|
|
23669
|
+
...execution.result.metadata?.securityNotification || noticeText ? {
|
|
23670
|
+
securityNotification: execution.result.metadata?.securityNotification ?? noticeText
|
|
23671
|
+
} : {}
|
|
23672
|
+
});
|
|
23673
|
+
if (options.ui) {
|
|
23674
|
+
const tempPath = join20(tmpdir(), `dbcli-query-${Date.now()}.html`);
|
|
23675
|
+
await Bun.write(tempPath, html);
|
|
23676
|
+
await openInBrowser(tempPath);
|
|
23677
|
+
} else {
|
|
23678
|
+
console.log(html);
|
|
22976
23679
|
}
|
|
23680
|
+
} else {
|
|
23681
|
+
const format = options.format ?? "table";
|
|
23682
|
+
const formatter = new QueryResultFormatter;
|
|
23683
|
+
console.log(formatter.format(execution.result, {
|
|
23684
|
+
format,
|
|
23685
|
+
truncate: format === "table" ? tableCellLimit : false
|
|
23686
|
+
}));
|
|
23687
|
+
}
|
|
23688
|
+
if (!options.ui && options.format !== "html") {
|
|
23689
|
+
for (const notice of execution.notices)
|
|
23690
|
+
console.log(notice);
|
|
22977
23691
|
}
|
|
23692
|
+
if (options.recovery !== true) {
|
|
23693
|
+
for (const diagnostic of execution.diagnostics)
|
|
23694
|
+
console.error(diagnostic);
|
|
23695
|
+
}
|
|
23696
|
+
}
|
|
23697
|
+
async function mongoQueryBranch(queryStr, options, context, fieldSelection) {
|
|
23698
|
+
const { config } = context;
|
|
23699
|
+
const collection = options.collection;
|
|
23700
|
+
const parsedQuery = JSON.parse(queryStr);
|
|
22978
23701
|
let effectiveLimit;
|
|
22979
23702
|
if (options.noLimit) {
|
|
22980
23703
|
effectiveLimit = undefined;
|
|
@@ -22982,177 +23705,195 @@ async function mongoQueryBranch(queryStr, options, config) {
|
|
|
22982
23705
|
effectiveLimit = options.limit;
|
|
22983
23706
|
} else if (config.permission === "query-only") {
|
|
22984
23707
|
effectiveLimit = DEFAULT_QUERY_ONLY_LIMIT;
|
|
22985
|
-
console.error(`Query-only mode: auto-limiting to ${effectiveLimit} rows`);
|
|
22986
23708
|
}
|
|
23709
|
+
const hasUserAuthoredLimit2 = Array.isArray(parsedQuery) && parsedQuery.some((stage) => stage !== null && typeof stage === "object" && Object.prototype.hasOwnProperty.call(stage, "$limit"));
|
|
23710
|
+
const appliedLimit = effectiveLimit !== undefined && effectiveLimit > 0 && !hasUserAuthoredLimit2 ? effectiveLimit : undefined;
|
|
22987
23711
|
const mongoAdapter = AdapterFactory.createMongoDBAdapter(config.connection);
|
|
22988
|
-
|
|
22989
|
-
const start = performance.now();
|
|
23712
|
+
let executionError;
|
|
22990
23713
|
try {
|
|
22991
|
-
|
|
23714
|
+
await mongoAdapter.connect();
|
|
23715
|
+
const start = performance.now();
|
|
23716
|
+
const projection = fieldSelection ? toMongoProjection(fieldSelection) : undefined;
|
|
23717
|
+
const executeOptions = appliedLimit !== undefined || projection !== undefined ? {
|
|
23718
|
+
...appliedLimit !== undefined ? { limit: appliedLimit + 1 } : {},
|
|
23719
|
+
...projection !== undefined ? { projection } : {}
|
|
23720
|
+
} : undefined;
|
|
23721
|
+
const result = await mongoAdapter.execute(queryStr, [collection], executeOptions);
|
|
22992
23722
|
const executionTimeMs = Math.round(performance.now() - start);
|
|
22993
|
-
|
|
22994
|
-
|
|
22995
|
-
target: collection,
|
|
22996
|
-
metadata: {
|
|
22997
|
-
rows_affected: result.rows.length,
|
|
22998
|
-
execution_ms: executionTimeMs
|
|
22999
|
-
}
|
|
23000
|
-
});
|
|
23723
|
+
const limitedResult = appliedLimit === undefined ? undefined : trimAppliedLimit(result.rows, appliedLimit);
|
|
23724
|
+
const visibleRows = limitedResult?.rows ?? result.rows;
|
|
23001
23725
|
const blacklistCfg = config.blacklist ?? { tables: [], columns: {} };
|
|
23002
|
-
const maskedRows = maskMongoRows(
|
|
23003
|
-
const
|
|
23726
|
+
const maskedRows = maskMongoRows(visibleRows, collection, blacklistCfg);
|
|
23727
|
+
const projected = fieldSelection ? projectRows(maskedRows, fieldSelection) : undefined;
|
|
23728
|
+
const outputRows = projected?.rows ?? maskedRows;
|
|
23729
|
+
const columnNames = projected?.columnNames ?? (outputRows[0] ? Object.keys(outputRows[0]) : []);
|
|
23004
23730
|
const queryResult = {
|
|
23005
|
-
rows:
|
|
23006
|
-
rowCount:
|
|
23007
|
-
columnNames
|
|
23731
|
+
rows: outputRows,
|
|
23732
|
+
rowCount: outputRows.length,
|
|
23733
|
+
columnNames,
|
|
23734
|
+
executionTimeMs,
|
|
23735
|
+
...limitedResult ? { appliedLimit: limitedResult.metadata } : {}
|
|
23008
23736
|
};
|
|
23009
|
-
const
|
|
23010
|
-
const output = formatter.format(queryResult, {
|
|
23011
|
-
format
|
|
23012
|
-
});
|
|
23013
|
-
console.log(output);
|
|
23737
|
+
const notices = [];
|
|
23014
23738
|
if ((blacklistCfg.columns[collection] ?? []).length > 0) {
|
|
23015
|
-
|
|
23739
|
+
notices.push(`
|
|
23016
23740
|
\u2139 Some fields may have been redacted as [REDACTED] per .dbcli blacklist.`);
|
|
23017
23741
|
}
|
|
23742
|
+
const diagnostics = [];
|
|
23743
|
+
if (options.recovery !== true && appliedLimit !== undefined && options.limit === undefined && config.permission === "query-only") {
|
|
23744
|
+
diagnostics.push(`Query-only mode: auto-limiting to ${appliedLimit} rows`);
|
|
23745
|
+
}
|
|
23746
|
+
return {
|
|
23747
|
+
result: queryResult,
|
|
23748
|
+
diagnostics,
|
|
23749
|
+
notices
|
|
23750
|
+
};
|
|
23751
|
+
} catch (error) {
|
|
23752
|
+
executionError = error;
|
|
23753
|
+
throw error;
|
|
23018
23754
|
} finally {
|
|
23019
|
-
await mongoAdapter
|
|
23755
|
+
await disconnectPreservingError(mongoAdapter, executionError);
|
|
23020
23756
|
}
|
|
23021
23757
|
}
|
|
23022
|
-
|
|
23023
|
-
|
|
23024
|
-
|
|
23025
|
-
|
|
23026
|
-
|
|
23027
|
-
|
|
23758
|
+
function describeRedisWarning(warning) {
|
|
23759
|
+
switch (warning.code) {
|
|
23760
|
+
case "REDIS_SIZE_TRUNCATE":
|
|
23761
|
+
return `\u26A0 REDIS_SIZE_TRUNCATE: ${warning.command} reply kept ${warning.kept} entries and dropped at least ${warning.droppedAtLeast}. Use --no-limit for the full reply.`;
|
|
23762
|
+
case "REDIS_SIZE_REWRITE":
|
|
23763
|
+
return `\u26A0 REDIS_SIZE_REWRITE: ${warning.command} was rewritten to bound the reply (${warning.original.join(" ")} \u2192 ${warning.rewritten.join(" ")}). Use --no-limit to run it as written.`;
|
|
23764
|
+
case "REDIS_BLACKLIST_FILTERED":
|
|
23765
|
+
return `\u26A0 REDIS_BLACKLIST_FILTERED: ${warning.count} key(s) were withheld by the blacklist.`;
|
|
23028
23766
|
}
|
|
23029
|
-
|
|
23767
|
+
}
|
|
23768
|
+
async function redisQueryBranch(command, options, context) {
|
|
23769
|
+
const { config } = context;
|
|
23770
|
+
const head = command.trim().split(/\s+/)[0]?.toUpperCase() ?? "";
|
|
23030
23771
|
const redisAdapter = AdapterFactory.createRedisAdapter(config.connection, config.blacklist?.tables ?? []);
|
|
23031
|
-
|
|
23032
|
-
const start = performance.now();
|
|
23772
|
+
let executionError;
|
|
23033
23773
|
try {
|
|
23774
|
+
await redisAdapter.connect();
|
|
23034
23775
|
const result = await redisAdapter.execute(command, undefined, {
|
|
23035
23776
|
noLimit: options.noLimit ?? false
|
|
23036
23777
|
});
|
|
23037
|
-
const executionTimeMs = Math.round(performance.now() - start);
|
|
23038
|
-
const target = command.trim().split(/\s+/)[1] || "<unknown-key>";
|
|
23039
|
-
await writeAuditEntry(config, "query", options, {
|
|
23040
|
-
success: true,
|
|
23041
|
-
target,
|
|
23042
|
-
metadata: {
|
|
23043
|
-
rows_affected: result.rows.length,
|
|
23044
|
-
execution_ms: executionTimeMs
|
|
23045
|
-
}
|
|
23046
|
-
});
|
|
23047
23778
|
const columnNames = result.rows[0] ? Object.keys(result.rows[0]) : ["value"];
|
|
23779
|
+
const sizeTruncation = result.warnings?.find((w2) => w2.code === "REDIS_SIZE_TRUNCATE");
|
|
23048
23780
|
const queryResult = {
|
|
23049
23781
|
rows: result.rows,
|
|
23050
23782
|
rowCount: result.rows.length,
|
|
23051
|
-
columnNames
|
|
23783
|
+
columnNames,
|
|
23784
|
+
...sizeTruncation ? { appliedLimit: { truncated: true, limitApplied: sizeTruncation.kept } } : {}
|
|
23052
23785
|
};
|
|
23053
|
-
const
|
|
23054
|
-
|
|
23055
|
-
|
|
23056
|
-
|
|
23057
|
-
|
|
23058
|
-
|
|
23059
|
-
|
|
23060
|
-
|
|
23061
|
-
|
|
23062
|
-
|
|
23063
|
-
|
|
23064
|
-
|
|
23065
|
-
matched_pattern: err.matchedPattern,
|
|
23066
|
-
...err.matchedKey ? { matched_key: err.matchedKey } : {}
|
|
23067
|
-
}
|
|
23068
|
-
});
|
|
23069
|
-
} else {
|
|
23070
|
-
await writeAuditEntry(config, "query", options, {
|
|
23071
|
-
success: false,
|
|
23072
|
-
error: err
|
|
23073
|
-
});
|
|
23074
|
-
}
|
|
23075
|
-
err.__auditWritten = true;
|
|
23076
|
-
throw err;
|
|
23786
|
+
const diagnostics = options.recovery === true ? [] : [
|
|
23787
|
+
...head === "KEYS" ? ['\u26A0 Warning: "KEYS" command is dangerous on production servers.'] : [],
|
|
23788
|
+
...(result.warnings ?? []).map(describeRedisWarning)
|
|
23789
|
+
];
|
|
23790
|
+
return {
|
|
23791
|
+
result: queryResult,
|
|
23792
|
+
diagnostics,
|
|
23793
|
+
notices: []
|
|
23794
|
+
};
|
|
23795
|
+
} catch (error) {
|
|
23796
|
+
executionError = error;
|
|
23797
|
+
throw error;
|
|
23077
23798
|
} finally {
|
|
23078
|
-
await redisAdapter
|
|
23799
|
+
await disconnectPreservingError(redisAdapter, executionError);
|
|
23079
23800
|
}
|
|
23080
23801
|
}
|
|
23081
|
-
async function elasticsearchQueryBranch(queryStr, options,
|
|
23802
|
+
async function elasticsearchQueryBranch(queryStr, options, context) {
|
|
23803
|
+
const { config } = context;
|
|
23082
23804
|
const indexName = options.index ?? options.collection;
|
|
23083
|
-
const format = options.format ?? "table";
|
|
23084
|
-
if (!indexName) {
|
|
23085
|
-
throw new Error("Elasticsearch \u67E5\u8A62\u9700\u8981\u6307\u5B9A --collection <index> \u6216 --index <index>");
|
|
23086
|
-
}
|
|
23087
|
-
const { enforceElasticsearchPermission: enforceElasticsearchPermission2 } = await Promise.resolve().then(() => (init_permission_guard(), exports_permission_guard));
|
|
23088
|
-
enforceElasticsearchPermission2({
|
|
23089
|
-
method: "POST",
|
|
23090
|
-
apiPath: `/${indexName}/_search`,
|
|
23091
|
-
body: queryStr.trim().startsWith("{") ? queryStr : undefined
|
|
23092
|
-
}, config.permission);
|
|
23093
23805
|
let effectiveLimit;
|
|
23094
23806
|
if (options.noLimit) {
|
|
23095
23807
|
effectiveLimit = 1e4;
|
|
23096
|
-
console.error("Elasticsearch --no-limit is capped at size 10000; for more rows use saved-query with search_after.");
|
|
23097
23808
|
} else {
|
|
23098
23809
|
effectiveLimit = options.limit || DEFAULT_QUERY_ONLY_LIMIT;
|
|
23099
23810
|
}
|
|
23100
|
-
|
|
23101
|
-
|
|
23102
|
-
|
|
23811
|
+
let hasUserAuthoredSize = false;
|
|
23812
|
+
if (queryStr.trim().startsWith("{")) {
|
|
23813
|
+
const parsed = JSON.parse(queryStr);
|
|
23814
|
+
hasUserAuthoredSize = parsed !== null && typeof parsed === "object" && Object.prototype.hasOwnProperty.call(parsed, "size");
|
|
23815
|
+
}
|
|
23816
|
+
const appliedLimit = !options.noLimit && !hasUserAuthoredSize ? effectiveLimit : undefined;
|
|
23817
|
+
const blacklistValidator = new BlacklistValidator(new BlacklistManager(config));
|
|
23103
23818
|
const esAdapter = AdapterFactory.createElasticsearchAdapter(config.connection);
|
|
23104
|
-
|
|
23105
|
-
const start = performance.now();
|
|
23819
|
+
let executionError;
|
|
23106
23820
|
try {
|
|
23821
|
+
await esAdapter.connect();
|
|
23822
|
+
const start = performance.now();
|
|
23107
23823
|
const result = await esAdapter.execute(queryStr, [indexName], {
|
|
23108
|
-
limit: effectiveLimit
|
|
23824
|
+
limit: appliedLimit === undefined ? effectiveLimit : appliedLimit + 1
|
|
23109
23825
|
});
|
|
23110
23826
|
const executionTimeMs = Math.round(performance.now() - start);
|
|
23111
|
-
|
|
23112
|
-
|
|
23113
|
-
|
|
23114
|
-
|
|
23115
|
-
rows_affected: result.rows.length,
|
|
23116
|
-
execution_ms: executionTimeMs
|
|
23117
|
-
}
|
|
23118
|
-
});
|
|
23119
|
-
const columnNames = result.rows[0] ? Object.keys(result.rows[0]) : [];
|
|
23120
|
-
const filterResult = blacklistValidator.filterColumns(indexName, result.rows, columnNames);
|
|
23827
|
+
const limitedResult = appliedLimit === undefined ? undefined : trimAppliedLimit(result.rows, appliedLimit);
|
|
23828
|
+
const visibleRows = limitedResult?.rows ?? result.rows;
|
|
23829
|
+
const columnNames = visibleRows[0] ? Object.keys(visibleRows[0]) : [];
|
|
23830
|
+
const filterResult = blacklistValidator.filterColumns(indexName, visibleRows, columnNames);
|
|
23121
23831
|
const queryResult = {
|
|
23122
23832
|
rows: filterResult.filteredRows,
|
|
23123
23833
|
rowCount: filterResult.filteredRows.length,
|
|
23124
|
-
columnNames: columnNames.filter((col) => !filterResult.omittedColumns.includes(col))
|
|
23834
|
+
columnNames: columnNames.filter((col) => !filterResult.omittedColumns.includes(col)),
|
|
23835
|
+
executionTimeMs,
|
|
23836
|
+
...limitedResult ? { appliedLimit: limitedResult.metadata } : {}
|
|
23125
23837
|
};
|
|
23126
|
-
const formatter = new QueryResultFormatter;
|
|
23127
|
-
const output = formatter.format(queryResult, {
|
|
23128
|
-
format
|
|
23129
|
-
});
|
|
23130
|
-
console.log(output);
|
|
23131
23838
|
const securityNote = blacklistValidator.buildSecurityNotification(indexName, filterResult.omittedColumns);
|
|
23132
|
-
|
|
23133
|
-
|
|
23134
|
-
|
|
23135
|
-
|
|
23839
|
+
return {
|
|
23840
|
+
result: queryResult,
|
|
23841
|
+
diagnostics: options.noLimit && options.recovery !== true ? [
|
|
23842
|
+
"Elasticsearch --no-limit is capped at size 10000; for more rows use saved-query with search_after."
|
|
23843
|
+
] : [],
|
|
23844
|
+
notices: securityNote ? [`
|
|
23845
|
+
\u2139 ${securityNote}`] : []
|
|
23846
|
+
};
|
|
23847
|
+
} catch (error) {
|
|
23848
|
+
executionError = error;
|
|
23849
|
+
throw error;
|
|
23136
23850
|
} finally {
|
|
23137
|
-
await esAdapter
|
|
23851
|
+
await disconnectPreservingError(esAdapter, executionError);
|
|
23852
|
+
}
|
|
23853
|
+
}
|
|
23854
|
+
async function disconnectPreservingError(adapter, executionError) {
|
|
23855
|
+
try {
|
|
23856
|
+
await adapter.disconnect();
|
|
23857
|
+
} catch (disconnectError) {
|
|
23858
|
+
if (executionError === undefined)
|
|
23859
|
+
throw disconnectError;
|
|
23860
|
+
}
|
|
23861
|
+
}
|
|
23862
|
+
function resolveTableCellLimit(options) {
|
|
23863
|
+
if (options.truncate !== undefined && (!Number.isSafeInteger(options.truncate) || options.truncate <= 0)) {
|
|
23864
|
+
throw new Error("--truncate must be a positive integer");
|
|
23138
23865
|
}
|
|
23866
|
+
if (options.truncate !== undefined && options.noTruncate === true) {
|
|
23867
|
+
throw new Error("--truncate and --no-truncate cannot be used together");
|
|
23868
|
+
}
|
|
23869
|
+
if (options.truncate !== undefined && (options.ui === true || (options.format ?? "table") !== "table")) {
|
|
23870
|
+
throw new Error("--truncate is supported only with --format table");
|
|
23871
|
+
}
|
|
23872
|
+
if (options.noTruncate === true)
|
|
23873
|
+
return false;
|
|
23874
|
+
return options.truncate ?? DEFAULT_TABLE_CELL_LIMIT;
|
|
23139
23875
|
}
|
|
23140
23876
|
var ALLOWED_FORMATS3;
|
|
23141
23877
|
var init_query = __esm(() => {
|
|
23142
|
-
init_message_loader();
|
|
23143
23878
|
init_adapters();
|
|
23144
23879
|
init_formatters();
|
|
23880
|
+
init_query_result_formatter();
|
|
23881
|
+
init_multi_query_result_formatter();
|
|
23145
23882
|
init_html_formatter();
|
|
23146
23883
|
init_opener();
|
|
23147
23884
|
init_query_executor();
|
|
23148
23885
|
init_config();
|
|
23149
|
-
init_permission_guard();
|
|
23150
23886
|
init_blacklist_validator();
|
|
23151
|
-
init_blacklist();
|
|
23152
23887
|
init_validation();
|
|
23888
|
+
init_applied_limit();
|
|
23153
23889
|
init_integration_helper();
|
|
23154
23890
|
init_field_masker();
|
|
23155
23891
|
init_types4();
|
|
23892
|
+
init_query_input();
|
|
23893
|
+
init_field_projection();
|
|
23894
|
+
init_connection_selector();
|
|
23895
|
+
init_query_fanout();
|
|
23896
|
+
init_permission_guard();
|
|
23156
23897
|
ALLOWED_FORMATS3 = ["table", "json", "csv"];
|
|
23157
23898
|
});
|
|
23158
23899
|
|
|
@@ -23645,7 +24386,8 @@ function prepareExecution(snippet, opts, cliParams, fileParams) {
|
|
|
23645
24386
|
driver: prepared.driver,
|
|
23646
24387
|
warnings: prepared.warnings,
|
|
23647
24388
|
rewrittenSql: prepared.rewrittenBody,
|
|
23648
|
-
execHints: prepared.execHints
|
|
24389
|
+
execHints: prepared.execHints,
|
|
24390
|
+
...prepared.guardLimit !== undefined ? { guardLimit: prepared.guardLimit } : {}
|
|
23649
24391
|
};
|
|
23650
24392
|
}
|
|
23651
24393
|
var init_runner = __esm(() => {
|
|
@@ -23711,8 +24453,13 @@ async function qMongoBranch(snippet, prepared, options, config) {
|
|
|
23711
24453
|
const executionTimeMs = Math.round(performance.now() - start);
|
|
23712
24454
|
const blacklistCfg = config.blacklist ?? { tables: [], columns: {} };
|
|
23713
24455
|
const masked = maskMongoRows(result.rows, collection, blacklistCfg);
|
|
24456
|
+
const securityNotification = (blacklistCfg.columns[collection] ?? []).length > 0 ? "Some fields may have been redacted as [REDACTED] per .dbcli blacklist." : undefined;
|
|
23714
24457
|
if (options.ui || options.format === "html") {
|
|
23715
|
-
const html = await generateHtmlReport({
|
|
24458
|
+
const html = await generateHtmlReport({
|
|
24459
|
+
meta: snippet.query.meta,
|
|
24460
|
+
rows: masked,
|
|
24461
|
+
...securityNotification ? { securityNotification } : {}
|
|
24462
|
+
});
|
|
23716
24463
|
if (options.ui) {
|
|
23717
24464
|
const tempPath = join21(tmpdir2(), `dbcli-report-${Date.now()}.html`);
|
|
23718
24465
|
await Bun.write(tempPath, html);
|
|
@@ -23776,6 +24523,9 @@ function formatDryRun(input) {
|
|
|
23776
24523
|
}
|
|
23777
24524
|
lines.push(input.driverSql);
|
|
23778
24525
|
lines.push("Bind values: " + JSON.stringify(input.values));
|
|
24526
|
+
if (input.guardLimit !== undefined) {
|
|
24527
|
+
lines.push(`Size guard: capped at ${input.guardLimit} rows; the extra row is fetched only to detect truncation and is discarded. Use --no-limit to remove the cap.`);
|
|
24528
|
+
}
|
|
23779
24529
|
return lines.join(`
|
|
23780
24530
|
`);
|
|
23781
24531
|
}
|
|
@@ -23806,7 +24556,8 @@ async function qCommand(name2, options, command) {
|
|
|
23806
24556
|
family: engineFamily(engine),
|
|
23807
24557
|
driverSql: prepared.driver.sql,
|
|
23808
24558
|
values: prepared.driver.values,
|
|
23809
|
-
execHints: prepared.execHints
|
|
24559
|
+
execHints: prepared.execHints,
|
|
24560
|
+
...prepared.guardLimit !== undefined ? { guardLimit: prepared.guardLimit } : {}
|
|
23810
24561
|
}));
|
|
23811
24562
|
await writeAuditEntry(config, "q", options, {
|
|
23812
24563
|
success: true,
|
|
@@ -23839,12 +24590,17 @@ async function qCommand(name2, options, command) {
|
|
|
23839
24590
|
const start = performance.now();
|
|
23840
24591
|
const result = await adapter.execute(prepared.driver.sql, family === "sql" ? prepared.driver.values : indexParams);
|
|
23841
24592
|
const executionTimeMs = Math.round(performance.now() - start);
|
|
23842
|
-
const
|
|
23843
|
-
const
|
|
24593
|
+
const limitedResult = prepared.guardLimit === undefined ? undefined : trimAppliedLimit(result.rows, prepared.guardLimit);
|
|
24594
|
+
const resultRows = limitedResult?.rows ?? result.rows;
|
|
24595
|
+
const columnNames = resultRows[0] ? Object.keys(resultRows[0]) : [];
|
|
24596
|
+
const filtered = family === "redis" ? { filteredRows: resultRows, omittedColumns: [] } : blacklistValidator.filterColumns(targetName, resultRows, columnNames);
|
|
24597
|
+
const securityNotification = family === "redis" || filtered.omittedColumns.length === 0 ? undefined : blacklistValidator.buildSecurityNotification(targetName, filtered.omittedColumns);
|
|
23844
24598
|
if (options.ui || options.format === "html") {
|
|
23845
24599
|
const html = await generateHtmlReport({
|
|
23846
24600
|
meta: snippet.query.meta,
|
|
23847
|
-
rows: filtered.filteredRows
|
|
24601
|
+
rows: filtered.filteredRows,
|
|
24602
|
+
...limitedResult ? { appliedLimit: limitedResult.metadata } : {},
|
|
24603
|
+
...securityNotification ? { securityNotification } : {}
|
|
23848
24604
|
});
|
|
23849
24605
|
if (options.ui) {
|
|
23850
24606
|
const tempPath = join22(tmpdir3(), `dbcli-report-${Date.now()}.html`);
|
|
@@ -23874,10 +24630,9 @@ async function qCommand(name2, options, command) {
|
|
|
23874
24630
|
metadata: {
|
|
23875
24631
|
statement: "SELECT",
|
|
23876
24632
|
affectedRows: 0,
|
|
23877
|
-
...
|
|
23878
|
-
|
|
23879
|
-
|
|
23880
|
-
}
|
|
24633
|
+
...securityNotification ? { securityNotification } : {}
|
|
24634
|
+
},
|
|
24635
|
+
...limitedResult ? { appliedLimit: limitedResult.metadata } : {}
|
|
23881
24636
|
}, { format: options.format ?? "table" });
|
|
23882
24637
|
console.log(out);
|
|
23883
24638
|
await writeAuditEntry(config, "q", options, {
|
|
@@ -23959,22 +24714,22 @@ async function handleQError(error, snippetName, options, config) {
|
|
|
23959
24714
|
emitRecoveryEnvelope2(error, { operation: "q", snippet: snippetName }, { envelopeId, auditRef: auditId ?? undefined });
|
|
23960
24715
|
}
|
|
23961
24716
|
if (error instanceof SavedQueryError) {
|
|
23962
|
-
|
|
24717
|
+
printLocalizedCliError(error.message, error);
|
|
23963
24718
|
process.exit(1);
|
|
23964
24719
|
}
|
|
23965
24720
|
if (error instanceof BlacklistError) {
|
|
23966
|
-
|
|
24721
|
+
printLocalizedCliError(error.message, error);
|
|
23967
24722
|
process.exit(1);
|
|
23968
24723
|
}
|
|
23969
24724
|
if (error instanceof PermissionError) {
|
|
23970
|
-
|
|
24725
|
+
printLocalizedCliError(t_vars("errors.permission_denied", { required: error.requiredPermission }), error);
|
|
23971
24726
|
process.exit(1);
|
|
23972
24727
|
}
|
|
23973
24728
|
if (error instanceof ConnectionError) {
|
|
23974
|
-
|
|
24729
|
+
printLocalizedCliError(t_vars("errors.connection_failed", { message: error.message }), error);
|
|
23975
24730
|
process.exit(1);
|
|
23976
24731
|
}
|
|
23977
|
-
|
|
24732
|
+
printLocalizedCliError(t_vars("errors.message", { message: error.message }), error);
|
|
23978
24733
|
process.exit(1);
|
|
23979
24734
|
}
|
|
23980
24735
|
function evaluateExpectation(row, expects) {
|
|
@@ -24086,6 +24841,8 @@ var init_q = __esm(() => {
|
|
|
24086
24841
|
init_integration_helper();
|
|
24087
24842
|
init_saved_queries();
|
|
24088
24843
|
init_colors();
|
|
24844
|
+
init_applied_limit();
|
|
24845
|
+
init_cli_error();
|
|
24089
24846
|
init_strategies();
|
|
24090
24847
|
});
|
|
24091
24848
|
|
|
@@ -24957,7 +25714,7 @@ Parameters:`);
|
|
|
24957
25714
|
if (error instanceof BlacklistError) {
|
|
24958
25715
|
throw error;
|
|
24959
25716
|
}
|
|
24960
|
-
const
|
|
25717
|
+
const errorMessage2 = error instanceof Error ? error.message : String(error);
|
|
24961
25718
|
if (error instanceof PermissionError) {
|
|
24962
25719
|
return {
|
|
24963
25720
|
status: "error",
|
|
@@ -24972,7 +25729,7 @@ Parameters:`);
|
|
|
24972
25729
|
operation: "insert",
|
|
24973
25730
|
rows_affected: 0,
|
|
24974
25731
|
timestamp,
|
|
24975
|
-
error: `INSERT failed: ${
|
|
25732
|
+
error: `INSERT failed: ${errorMessage2}`
|
|
24976
25733
|
};
|
|
24977
25734
|
}
|
|
24978
25735
|
}
|
|
@@ -25023,7 +25780,7 @@ Parameters:`);
|
|
|
25023
25780
|
if (error instanceof BlacklistError) {
|
|
25024
25781
|
throw error;
|
|
25025
25782
|
}
|
|
25026
|
-
const
|
|
25783
|
+
const errorMessage2 = error instanceof Error ? error.message : String(error);
|
|
25027
25784
|
if (error instanceof PermissionError) {
|
|
25028
25785
|
return {
|
|
25029
25786
|
status: "error",
|
|
@@ -25038,7 +25795,7 @@ Parameters:`);
|
|
|
25038
25795
|
operation: "update",
|
|
25039
25796
|
rows_affected: 0,
|
|
25040
25797
|
timestamp,
|
|
25041
|
-
error: `UPDATE failed: ${
|
|
25798
|
+
error: `UPDATE failed: ${errorMessage2}`
|
|
25042
25799
|
};
|
|
25043
25800
|
}
|
|
25044
25801
|
}
|
|
@@ -25099,13 +25856,13 @@ Parameters:`);
|
|
|
25099
25856
|
if (error instanceof BlacklistError) {
|
|
25100
25857
|
throw error;
|
|
25101
25858
|
}
|
|
25102
|
-
const
|
|
25859
|
+
const errorMessage2 = error instanceof Error ? error.message : String(error);
|
|
25103
25860
|
return {
|
|
25104
25861
|
status: "error",
|
|
25105
25862
|
operation: "delete",
|
|
25106
25863
|
rows_affected: 0,
|
|
25107
25864
|
timestamp,
|
|
25108
|
-
error: `DELETE failed: ${
|
|
25865
|
+
error: `DELETE failed: ${errorMessage2}`
|
|
25109
25866
|
};
|
|
25110
25867
|
}
|
|
25111
25868
|
}
|
|
@@ -26215,7 +26972,7 @@ async function insertCommand(table, options, command) {
|
|
|
26215
26972
|
process.exit(1);
|
|
26216
26973
|
}
|
|
26217
26974
|
if (error instanceof ConnectionError) {
|
|
26218
|
-
|
|
26975
|
+
printLocalizedCliError(t_vars("errors.connection_failed", { message: error.message }), error);
|
|
26219
26976
|
process.exit(1);
|
|
26220
26977
|
}
|
|
26221
26978
|
const output = {
|
|
@@ -26230,6 +26987,7 @@ async function insertCommand(table, options, command) {
|
|
|
26230
26987
|
}
|
|
26231
26988
|
var init_insert = __esm(() => {
|
|
26232
26989
|
init_message_loader();
|
|
26990
|
+
init_cli_error();
|
|
26233
26991
|
init_adapters();
|
|
26234
26992
|
init_data_executor();
|
|
26235
26993
|
init_config();
|
|
@@ -26516,7 +27274,7 @@ async function updateCommand(table, options, command) {
|
|
|
26516
27274
|
process.exit(1);
|
|
26517
27275
|
}
|
|
26518
27276
|
if (error instanceof ConnectionError) {
|
|
26519
|
-
|
|
27277
|
+
printLocalizedCliError(t_vars("errors.connection_failed", { message: error.message }), error);
|
|
26520
27278
|
process.exit(1);
|
|
26521
27279
|
}
|
|
26522
27280
|
const output = {
|
|
@@ -26531,6 +27289,7 @@ async function updateCommand(table, options, command) {
|
|
|
26531
27289
|
}
|
|
26532
27290
|
var init_update = __esm(() => {
|
|
26533
27291
|
init_message_loader();
|
|
27292
|
+
init_cli_error();
|
|
26534
27293
|
init_adapters();
|
|
26535
27294
|
init_data_executor();
|
|
26536
27295
|
init_config();
|
|
@@ -26801,7 +27560,7 @@ async function deleteCommand(table, options, command) {
|
|
|
26801
27560
|
process.exit(1);
|
|
26802
27561
|
}
|
|
26803
27562
|
if (error instanceof ConnectionError) {
|
|
26804
|
-
|
|
27563
|
+
printLocalizedCliError(t_vars("errors.connection_failed", { message: error.message }), error);
|
|
26805
27564
|
process.exit(1);
|
|
26806
27565
|
}
|
|
26807
27566
|
const output = {
|
|
@@ -26816,6 +27575,7 @@ async function deleteCommand(table, options, command) {
|
|
|
26816
27575
|
}
|
|
26817
27576
|
var init_delete = __esm(() => {
|
|
26818
27577
|
init_message_loader();
|
|
27578
|
+
init_cli_error();
|
|
26819
27579
|
init_adapters();
|
|
26820
27580
|
init_data_executor();
|
|
26821
27581
|
init_config();
|
|
@@ -26898,10 +27658,20 @@ async function exportCommand(sql, options, command) {
|
|
|
26898
27658
|
}
|
|
26899
27659
|
const adapter = AdapterFactory.createSqlAdapter(requireSqlConnection6(config.connection));
|
|
26900
27660
|
await adapter.connect();
|
|
27661
|
+
const executor3 = new QueryExecutor(adapter, config.permission, undefined, undefined, {
|
|
27662
|
+
recovery: options.recovery,
|
|
27663
|
+
deferDiagnostics: true
|
|
27664
|
+
});
|
|
27665
|
+
let formatted;
|
|
27666
|
+
let rowCount;
|
|
26901
27667
|
try {
|
|
26902
|
-
const
|
|
26903
|
-
|
|
26904
|
-
|
|
27668
|
+
const result = await executor3.execute(sql, {
|
|
27669
|
+
autoLimit: options.noLimit !== true,
|
|
27670
|
+
...typeof options.limit === "number" ? { limitValue: options.limit } : {},
|
|
27671
|
+
detectTruncation: true
|
|
27672
|
+
});
|
|
27673
|
+
assertExportNotSilentlyTruncated(result.appliedLimit, options);
|
|
27674
|
+
rowCount = result.rowCount;
|
|
26905
27675
|
if (options.format === "html") {
|
|
26906
27676
|
formatted = await generateHtmlReport({
|
|
26907
27677
|
meta: {
|
|
@@ -26911,7 +27681,9 @@ async function exportCommand(sql, options, command) {
|
|
|
26911
27681
|
tags: [],
|
|
26912
27682
|
description: sql
|
|
26913
27683
|
},
|
|
26914
|
-
rows: result.rows
|
|
27684
|
+
rows: result.rows,
|
|
27685
|
+
...result.appliedLimit ? { appliedLimit: result.appliedLimit } : {},
|
|
27686
|
+
...result.metadata?.securityNotification ? { securityNotification: result.metadata.securityNotification } : {}
|
|
26915
27687
|
});
|
|
26916
27688
|
} else {
|
|
26917
27689
|
const formatter = new QueryResultFormatter;
|
|
@@ -26919,21 +27691,6 @@ async function exportCommand(sql, options, command) {
|
|
|
26919
27691
|
format: options.format
|
|
26920
27692
|
});
|
|
26921
27693
|
}
|
|
26922
|
-
if (options.output) {
|
|
26923
|
-
const file = Bun.file(options.output);
|
|
26924
|
-
const exists = await file.exists();
|
|
26925
|
-
if (exists && !options.force) {
|
|
26926
|
-
const confirmed = await promptUser.confirm(t_vars("export.overwrite_confirmation", { file: options.output }));
|
|
26927
|
-
if (!confirmed) {
|
|
26928
|
-
console.error("Operation cancelled by user");
|
|
26929
|
-
return;
|
|
26930
|
-
}
|
|
26931
|
-
}
|
|
26932
|
-
await file.write(formatted);
|
|
26933
|
-
console.error(t_vars("export.exported", { count: result.rowCount, file: options.output }));
|
|
26934
|
-
} else {
|
|
26935
|
-
console.log(formatted);
|
|
26936
|
-
}
|
|
26937
27694
|
await writeAuditEntry(config, "export", options, {
|
|
26938
27695
|
success: true,
|
|
26939
27696
|
target: extractTableName(sql) ?? "*",
|
|
@@ -26947,6 +27704,11 @@ async function exportCommand(sql, options, command) {
|
|
|
26947
27704
|
} finally {
|
|
26948
27705
|
await adapter.disconnect();
|
|
26949
27706
|
}
|
|
27707
|
+
const emitted = await emitExportOutput(formatted, rowCount, options);
|
|
27708
|
+
if (emitted && options.recovery !== true) {
|
|
27709
|
+
for (const diagnostic of executor3.takeDiagnostics())
|
|
27710
|
+
console.error(diagnostic);
|
|
27711
|
+
}
|
|
26950
27712
|
} catch (error) {
|
|
26951
27713
|
let auditId = null;
|
|
26952
27714
|
let envelopeId;
|
|
@@ -26967,22 +27729,7 @@ async function exportCommand(sql, options, command) {
|
|
|
26967
27729
|
const { emitRecoveryEnvelope: emitRecoveryEnvelope2 } = await Promise.resolve().then(() => (init_recovery(), exports_recovery));
|
|
26968
27730
|
emitRecoveryEnvelope2(error, { operation: "export", table: m?.[1] }, { envelopeId, auditRef: auditId ?? undefined });
|
|
26969
27731
|
}
|
|
26970
|
-
|
|
26971
|
-
console.error(t_vars("errors.permission_denied", { required: error.requiredPermission }));
|
|
26972
|
-
console.error(` Operation: ${error.classification.type}`);
|
|
26973
|
-
console.error(` Message: ${error.message}`);
|
|
26974
|
-
process.exit(1);
|
|
26975
|
-
}
|
|
26976
|
-
if (error instanceof ConnectionError) {
|
|
26977
|
-
console.error(t_vars("errors.connection_failed", { message: error.message }));
|
|
26978
|
-
process.exit(1);
|
|
26979
|
-
}
|
|
26980
|
-
if (error instanceof BlacklistError) {
|
|
26981
|
-
console.error(error.message);
|
|
26982
|
-
process.exit(1);
|
|
26983
|
-
}
|
|
26984
|
-
console.error(t_vars("errors.message", { message: error.message }));
|
|
26985
|
-
process.exit(1);
|
|
27732
|
+
throw error;
|
|
26986
27733
|
}
|
|
26987
27734
|
}
|
|
26988
27735
|
async function redisExportBranch(command, options, config) {
|
|
@@ -26990,6 +27737,8 @@ async function redisExportBranch(command, options, config) {
|
|
|
26990
27737
|
enforceRedisPermission2(command, config.permission);
|
|
26991
27738
|
const redisAdapter = AdapterFactory.createRedisAdapter(config.connection, config.blacklist?.tables ?? [], config.redis?.mask ?? []);
|
|
26992
27739
|
await redisAdapter.connect();
|
|
27740
|
+
let formatted;
|
|
27741
|
+
let rowCount;
|
|
26993
27742
|
try {
|
|
26994
27743
|
const result = await redisAdapter.execute(command);
|
|
26995
27744
|
const columnNames = result.rows[0] ? Object.keys(result.rows[0]) : ["value"];
|
|
@@ -26999,31 +27748,14 @@ async function redisExportBranch(command, options, config) {
|
|
|
26999
27748
|
columnNames
|
|
27000
27749
|
};
|
|
27001
27750
|
const formatter = new QueryResultFormatter;
|
|
27002
|
-
|
|
27003
|
-
|
|
27004
|
-
const file = Bun.file(options.output);
|
|
27005
|
-
const exists = await file.exists();
|
|
27006
|
-
if (exists && !options.force) {
|
|
27007
|
-
const confirmed = await promptUser.confirm(t_vars("export.overwrite_confirmation", { file: options.output }));
|
|
27008
|
-
if (!confirmed) {
|
|
27009
|
-
console.error("Operation cancelled by user");
|
|
27010
|
-
return;
|
|
27011
|
-
}
|
|
27012
|
-
}
|
|
27013
|
-
await file.write(formatted);
|
|
27014
|
-
console.error(t_vars("export.exported", {
|
|
27015
|
-
count: result.rowCount ?? result.rows.length ?? 0,
|
|
27016
|
-
file: options.output
|
|
27017
|
-
}));
|
|
27018
|
-
} else {
|
|
27019
|
-
console.log(formatted);
|
|
27020
|
-
}
|
|
27751
|
+
formatted = formatter.format(queryResult, { format: options.format });
|
|
27752
|
+
rowCount = result.rowCount ?? result.rows.length ?? 0;
|
|
27021
27753
|
const target = command.trim().split(/\s+/)[1] || "<unknown-key>";
|
|
27022
27754
|
await writeAuditEntry(config, "export", options, {
|
|
27023
27755
|
success: true,
|
|
27024
27756
|
target,
|
|
27025
27757
|
metadata: {
|
|
27026
|
-
rows_affected:
|
|
27758
|
+
rows_affected: rowCount,
|
|
27027
27759
|
output_format: options.format,
|
|
27028
27760
|
...options.output && { output_file: options.output }
|
|
27029
27761
|
}
|
|
@@ -27031,9 +27763,20 @@ async function redisExportBranch(command, options, config) {
|
|
|
27031
27763
|
} finally {
|
|
27032
27764
|
await redisAdapter.disconnect();
|
|
27033
27765
|
}
|
|
27766
|
+
await emitExportOutput(formatted, rowCount, options);
|
|
27767
|
+
}
|
|
27768
|
+
function assertExportNotSilentlyTruncated(appliedLimit, options) {
|
|
27769
|
+
if (!appliedLimit?.truncated)
|
|
27770
|
+
return;
|
|
27771
|
+
if (options.noLimit === true || typeof options.limit === "number")
|
|
27772
|
+
return;
|
|
27773
|
+
throw new Error(`Export would silently drop rows \u2014 ${appliedLimit.limitApplied}-row auto-limit reached.
|
|
27774
|
+
Re-run with --no-limit to export everything,
|
|
27775
|
+
or --limit ${appliedLimit.limitApplied} to accept the cap explicitly.`);
|
|
27034
27776
|
}
|
|
27035
27777
|
async function buildEsExportRows(query, options, adapter) {
|
|
27036
27778
|
const cap2 = options.noLimit ? Number.POSITIVE_INFINITY : options.limit ?? ES_EXPORT_CAP;
|
|
27779
|
+
const capped = Number.isFinite(cap2);
|
|
27037
27780
|
const isDsl = query.trim().startsWith("{");
|
|
27038
27781
|
if (isDsl) {
|
|
27039
27782
|
const index2 = options.index ?? options.collection;
|
|
@@ -27041,47 +27784,54 @@ async function buildEsExportRows(query, options, adapter) {
|
|
|
27041
27784
|
throw new Error("Elasticsearch DSL export requires --index <name>");
|
|
27042
27785
|
}
|
|
27043
27786
|
const res = await adapter.execute(query, [index2], {
|
|
27044
|
-
limit:
|
|
27787
|
+
limit: capped ? cap2 + 1 : 1e4
|
|
27045
27788
|
});
|
|
27046
|
-
return { rows: res.rows, target: index2 };
|
|
27789
|
+
return { rows: res.rows, target: index2, ...capped ? { cap: cap2 } : {} };
|
|
27047
27790
|
}
|
|
27048
27791
|
const index = query.trim();
|
|
27049
|
-
const rows = await scrollAll(adapter, index,
|
|
27050
|
-
return { rows, target: index };
|
|
27792
|
+
const rows = await scrollAll(adapter, index, capped ? cap2 + 1 : 1e6);
|
|
27793
|
+
return { rows, target: index, ...capped ? { cap: cap2 } : {} };
|
|
27051
27794
|
}
|
|
27052
27795
|
async function esExportBranch(query, options, config) {
|
|
27053
27796
|
const blacklistManager = new BlacklistManager(config);
|
|
27054
27797
|
const blacklistValidator = new BlacklistValidator(blacklistManager);
|
|
27055
27798
|
const adapter = AdapterFactory.createElasticsearchAdapter(config.connection);
|
|
27056
27799
|
await adapter.connect();
|
|
27800
|
+
let formatted;
|
|
27801
|
+
let rowCount;
|
|
27802
|
+
const diagnostics = [];
|
|
27057
27803
|
try {
|
|
27058
|
-
const {
|
|
27804
|
+
const {
|
|
27805
|
+
rows: fetched,
|
|
27806
|
+
target,
|
|
27807
|
+
cap: cap2
|
|
27808
|
+
} = await buildEsExportRows(query, options, adapter);
|
|
27059
27809
|
blacklistValidator.checkTableBlacklist("SELECT", target, []);
|
|
27060
|
-
|
|
27061
|
-
|
|
27062
|
-
|
|
27810
|
+
const limitedResult = cap2 === undefined ? undefined : trimAppliedLimit(fetched, cap2);
|
|
27811
|
+
assertExportNotSilentlyTruncated(limitedResult?.metadata, options);
|
|
27812
|
+
const rows = limitedResult?.rows ?? fetched;
|
|
27813
|
+
rowCount = rows.length;
|
|
27063
27814
|
const columns = collectColumnUnion(rows);
|
|
27064
|
-
|
|
27065
|
-
|
|
27066
|
-
|
|
27067
|
-
|
|
27068
|
-
|
|
27069
|
-
|
|
27070
|
-
|
|
27071
|
-
|
|
27072
|
-
|
|
27073
|
-
|
|
27074
|
-
|
|
27075
|
-
|
|
27076
|
-
console.error(t_vars("export.exported", { count: rows.length, file: options.output }));
|
|
27815
|
+
if (options.format === "html") {
|
|
27816
|
+
formatted = await generateHtmlReport({
|
|
27817
|
+
meta: {
|
|
27818
|
+
name: "Exported Report",
|
|
27819
|
+
key: "export",
|
|
27820
|
+
params: [],
|
|
27821
|
+
tags: [],
|
|
27822
|
+
description: query
|
|
27823
|
+
},
|
|
27824
|
+
rows,
|
|
27825
|
+
...limitedResult ? { appliedLimit: limitedResult.metadata } : {}
|
|
27826
|
+
});
|
|
27077
27827
|
} else {
|
|
27078
|
-
|
|
27828
|
+
formatted = formatMongoRows(rows, columns, options.format, options.recovery === true ? undefined : diagnostics);
|
|
27079
27829
|
}
|
|
27080
27830
|
await writeAuditEntry(config, "export", options, {
|
|
27081
27831
|
success: true,
|
|
27082
27832
|
target,
|
|
27083
27833
|
metadata: {
|
|
27084
|
-
rows_affected:
|
|
27834
|
+
rows_affected: rowCount,
|
|
27085
27835
|
output_format: options.format,
|
|
27086
27836
|
...options.output && { output_file: options.output }
|
|
27087
27837
|
}
|
|
@@ -27089,22 +27839,23 @@ async function esExportBranch(query, options, config) {
|
|
|
27089
27839
|
} finally {
|
|
27090
27840
|
await adapter.disconnect();
|
|
27091
27841
|
}
|
|
27842
|
+
const emitted = await emitExportOutput(formatted, rowCount, options);
|
|
27843
|
+
if (emitted && options.recovery !== true) {
|
|
27844
|
+
for (const diagnostic of diagnostics)
|
|
27845
|
+
console.error(diagnostic);
|
|
27846
|
+
}
|
|
27092
27847
|
}
|
|
27093
27848
|
async function mongoExportBranch(query, options, config) {
|
|
27094
27849
|
if (SQL_PATTERN.test(query)) {
|
|
27095
|
-
|
|
27096
|
-
console.error(`\u7BC4\u4F8B\uFF1Adbcli export '{"status":"open"}' --collection orders --format jsonl`);
|
|
27097
|
-
process.exit(1);
|
|
27850
|
+
throw new Error("\u9019\u662F MongoDB \u9023\u7DDA\uFF0C\u8ACB\u4F7F\u7528 JSON filter \u6216 aggregation pipeline\u3002" + ` \u7BC4\u4F8B\uFF1Adbcli export '{"status":"open"}' --collection orders --format jsonl`);
|
|
27098
27851
|
}
|
|
27099
27852
|
if (!options.collection) {
|
|
27100
|
-
|
|
27101
|
-
process.exit(1);
|
|
27853
|
+
throw new Error("MongoDB export \u9700\u8981\u6307\u5B9A --collection <name>");
|
|
27102
27854
|
}
|
|
27103
27855
|
try {
|
|
27104
27856
|
JSON.parse(query);
|
|
27105
27857
|
} catch {
|
|
27106
|
-
|
|
27107
|
-
process.exit(1);
|
|
27858
|
+
throw new Error("MongoDB \u67E5\u8A62\u5FC5\u9808\u662F\u6709\u6548\u7684 JSON\uFF08object filter \u6216 array pipeline\uFF09");
|
|
27108
27859
|
}
|
|
27109
27860
|
const collection = options.collection;
|
|
27110
27861
|
const blacklistManager = new BlacklistManager(config);
|
|
@@ -27117,42 +27868,45 @@ async function mongoExportBranch(query, options, config) {
|
|
|
27117
27868
|
effectiveLimit = options.limit;
|
|
27118
27869
|
} else if (config.permission === "query-only") {
|
|
27119
27870
|
effectiveLimit = DEFAULT_QUERY_ONLY_LIMIT;
|
|
27120
|
-
console.error(`Query-only mode: auto-limiting to ${effectiveLimit} rows`);
|
|
27121
27871
|
}
|
|
27122
27872
|
const adapter = AdapterFactory.createMongoDBAdapter(config.connection);
|
|
27123
27873
|
await adapter.connect();
|
|
27874
|
+
let formatted;
|
|
27875
|
+
let rowCount;
|
|
27876
|
+
let hasBlacklistedColumns = false;
|
|
27877
|
+
const diagnostics = [];
|
|
27124
27878
|
try {
|
|
27125
|
-
const result = await adapter.execute(query, [collection], effectiveLimit !== undefined ? { limit: effectiveLimit } : undefined);
|
|
27879
|
+
const result = await adapter.execute(query, [collection], effectiveLimit !== undefined ? { limit: effectiveLimit + 1 } : undefined);
|
|
27880
|
+
const limitedResult = effectiveLimit === undefined ? undefined : trimAppliedLimit(result.rows, effectiveLimit);
|
|
27881
|
+
assertExportNotSilentlyTruncated(limitedResult?.metadata, options);
|
|
27126
27882
|
const blacklistCfg = config.blacklist ?? { tables: [], columns: {} };
|
|
27127
|
-
const maskedRows = maskMongoRows(result.rows, collection, blacklistCfg);
|
|
27883
|
+
const maskedRows = maskMongoRows(limitedResult?.rows ?? result.rows, collection, blacklistCfg);
|
|
27884
|
+
rowCount = maskedRows.length;
|
|
27885
|
+
hasBlacklistedColumns = (blacklistCfg.columns[collection] ?? []).length > 0;
|
|
27128
27886
|
const visibleColumns = collectColumnUnion(maskedRows);
|
|
27129
|
-
|
|
27130
|
-
|
|
27131
|
-
|
|
27132
|
-
|
|
27133
|
-
|
|
27134
|
-
|
|
27135
|
-
|
|
27136
|
-
|
|
27137
|
-
|
|
27138
|
-
|
|
27139
|
-
|
|
27140
|
-
|
|
27141
|
-
|
|
27142
|
-
|
|
27143
|
-
|
|
27144
|
-
}));
|
|
27887
|
+
if (options.format === "html") {
|
|
27888
|
+
formatted = await generateHtmlReport({
|
|
27889
|
+
meta: {
|
|
27890
|
+
name: "Exported Report",
|
|
27891
|
+
key: "export",
|
|
27892
|
+
params: [],
|
|
27893
|
+
tags: [],
|
|
27894
|
+
description: query
|
|
27895
|
+
},
|
|
27896
|
+
rows: maskedRows,
|
|
27897
|
+
...limitedResult ? { appliedLimit: limitedResult.metadata } : {},
|
|
27898
|
+
...hasBlacklistedColumns ? {
|
|
27899
|
+
securityNotification: "Some fields may have been redacted as [REDACTED] per .dbcli blacklist."
|
|
27900
|
+
} : {}
|
|
27901
|
+
});
|
|
27145
27902
|
} else {
|
|
27146
|
-
|
|
27147
|
-
}
|
|
27148
|
-
if ((blacklistCfg.columns[collection] ?? []).length > 0) {
|
|
27149
|
-
console.error(`\u2139 Some fields may have been redacted as [REDACTED] per .dbcli blacklist.`);
|
|
27903
|
+
formatted = formatMongoRows(maskedRows, visibleColumns, options.format, options.recovery === true ? undefined : diagnostics);
|
|
27150
27904
|
}
|
|
27151
27905
|
await writeAuditEntry(config, "export", options, {
|
|
27152
27906
|
success: true,
|
|
27153
27907
|
target: collection,
|
|
27154
27908
|
metadata: {
|
|
27155
|
-
rows_affected:
|
|
27909
|
+
rows_affected: rowCount,
|
|
27156
27910
|
output_format: options.format,
|
|
27157
27911
|
...options.output && { output_file: options.output }
|
|
27158
27912
|
}
|
|
@@ -27160,6 +27914,19 @@ async function mongoExportBranch(query, options, config) {
|
|
|
27160
27914
|
} finally {
|
|
27161
27915
|
await adapter.disconnect();
|
|
27162
27916
|
}
|
|
27917
|
+
const emitted = await emitExportOutput(formatted, rowCount, options);
|
|
27918
|
+
if (!emitted)
|
|
27919
|
+
return;
|
|
27920
|
+
if (options.recovery !== true) {
|
|
27921
|
+
for (const diagnostic of diagnostics)
|
|
27922
|
+
console.error(diagnostic);
|
|
27923
|
+
}
|
|
27924
|
+
if (hasBlacklistedColumns) {
|
|
27925
|
+
console.error(`\u2139 Some fields may have been redacted as [REDACTED] per .dbcli blacklist.`);
|
|
27926
|
+
}
|
|
27927
|
+
if (options.recovery !== true && options.limit === undefined && !options.noLimit && config.permission === "query-only") {
|
|
27928
|
+
console.error(`Query-only mode: auto-limiting to ${effectiveLimit} rows`);
|
|
27929
|
+
}
|
|
27163
27930
|
}
|
|
27164
27931
|
function collectColumnUnion(rows) {
|
|
27165
27932
|
const seen = new Set;
|
|
@@ -27174,7 +27941,7 @@ function collectColumnUnion(rows) {
|
|
|
27174
27941
|
}
|
|
27175
27942
|
return order;
|
|
27176
27943
|
}
|
|
27177
|
-
function formatMongoRows(rows, columns, format) {
|
|
27944
|
+
function formatMongoRows(rows, columns, format, diagnostics) {
|
|
27178
27945
|
if (format === "jsonl") {
|
|
27179
27946
|
return rows.map((row) => JSON.stringify(row)).join(`
|
|
27180
27947
|
`);
|
|
@@ -27194,12 +27961,29 @@ function formatMongoRows(rows, columns, format) {
|
|
|
27194
27961
|
return escapeCsvField(value);
|
|
27195
27962
|
}).join(",");
|
|
27196
27963
|
});
|
|
27197
|
-
if (nestedSeen) {
|
|
27198
|
-
|
|
27964
|
+
if (nestedSeen && diagnostics) {
|
|
27965
|
+
diagnostics.push("Warning: nested object/array fields were JSON-stringified for CSV. Use --format jsonl to preserve structure.");
|
|
27199
27966
|
}
|
|
27200
27967
|
return [headerLine, ...dataLines].join(`
|
|
27201
27968
|
`);
|
|
27202
27969
|
}
|
|
27970
|
+
async function emitExportOutput(formatted, rowCount, options) {
|
|
27971
|
+
if (!options.output) {
|
|
27972
|
+
console.log(formatted);
|
|
27973
|
+
return true;
|
|
27974
|
+
}
|
|
27975
|
+
const file = Bun.file(options.output);
|
|
27976
|
+
if (await file.exists() && !options.force) {
|
|
27977
|
+
const confirmed = await promptUser.confirm(t_vars("export.overwrite_confirmation", { file: options.output }));
|
|
27978
|
+
if (!confirmed) {
|
|
27979
|
+
console.error("Operation cancelled by user");
|
|
27980
|
+
return false;
|
|
27981
|
+
}
|
|
27982
|
+
}
|
|
27983
|
+
await file.write(formatted);
|
|
27984
|
+
console.error(t_vars("export.exported", { count: rowCount, file: options.output }));
|
|
27985
|
+
return true;
|
|
27986
|
+
}
|
|
27203
27987
|
function escapeCsvField(value) {
|
|
27204
27988
|
if (value === null || value === undefined)
|
|
27205
27989
|
return "";
|
|
@@ -27218,10 +28002,9 @@ var init_export = __esm(() => {
|
|
|
27218
28002
|
init_html_formatter();
|
|
27219
28003
|
init_query_executor();
|
|
27220
28004
|
init_config();
|
|
27221
|
-
init_permission_guard();
|
|
27222
28005
|
init_prompts();
|
|
27223
28006
|
init_blacklist_validator();
|
|
27224
|
-
|
|
28007
|
+
init_applied_limit();
|
|
27225
28008
|
init_integration_helper();
|
|
27226
28009
|
init_engine_hints();
|
|
27227
28010
|
init_field_masker();
|
|
@@ -28134,86 +28917,73 @@ function requireSqlConnection7(connection) {
|
|
|
28134
28917
|
return connection;
|
|
28135
28918
|
}
|
|
28136
28919
|
async function checkAction(table, options) {
|
|
28920
|
+
validateFormat(options.format, ALLOWED_FORMATS6, "check");
|
|
28921
|
+
const config = await configModule.read(options.config);
|
|
28922
|
+
if (!config.connection) {
|
|
28923
|
+
throw new Error("Database not configured. Run: dbcli init");
|
|
28924
|
+
}
|
|
28925
|
+
const adapter = AdapterFactory.createSqlAdapter(requireSqlConnection7(config.connection));
|
|
28926
|
+
await adapter.connect();
|
|
28137
28927
|
try {
|
|
28138
|
-
|
|
28139
|
-
const
|
|
28140
|
-
|
|
28141
|
-
|
|
28142
|
-
|
|
28143
|
-
|
|
28144
|
-
|
|
28145
|
-
|
|
28146
|
-
|
|
28147
|
-
|
|
28148
|
-
const
|
|
28149
|
-
const
|
|
28150
|
-
|
|
28151
|
-
|
|
28152
|
-
|
|
28153
|
-
|
|
28154
|
-
|
|
28155
|
-
|
|
28156
|
-
|
|
28928
|
+
const checker = new HealthChecker(adapter);
|
|
28929
|
+
const blacklistManager = new BlacklistManager(config);
|
|
28930
|
+
const blacklistedColumns = getBlacklistedColumnSet(blacklistManager);
|
|
28931
|
+
const blacklistedTables = getBlacklistedTableSet(blacklistManager);
|
|
28932
|
+
const checkTypes = options.checks ? options.checks.split(",") : undefined;
|
|
28933
|
+
const sampleSize = parseInt(options.sample, 10) || 1e4;
|
|
28934
|
+
if (table) {
|
|
28935
|
+
if (blacklistedTables.has(table.toLowerCase())) {
|
|
28936
|
+
throw new Error(`Table "${table}" is blacklisted`);
|
|
28937
|
+
}
|
|
28938
|
+
const schema = await adapter.getTableSchema(table);
|
|
28939
|
+
const report = await checker.check(schema, {
|
|
28940
|
+
checks: checkTypes,
|
|
28941
|
+
sample: sampleSize,
|
|
28942
|
+
blacklistedColumns
|
|
28943
|
+
});
|
|
28944
|
+
outputReport(report, options.format);
|
|
28945
|
+
} else if (options.all) {
|
|
28946
|
+
const tables = await adapter.listTables();
|
|
28947
|
+
const reports = [];
|
|
28948
|
+
const skipped = [];
|
|
28949
|
+
for (const t2 of tables) {
|
|
28950
|
+
if (blacklistedTables.has(t2.name.toLowerCase())) {
|
|
28951
|
+
skipped.push(`${t2.name} (blacklisted)`);
|
|
28952
|
+
continue;
|
|
28953
|
+
}
|
|
28954
|
+
if (t2.tableType === "view") {
|
|
28955
|
+
skipped.push(`${t2.name} (view)`);
|
|
28956
|
+
continue;
|
|
28957
|
+
}
|
|
28958
|
+
const category = getSizeCategory(t2.estimatedRowCount);
|
|
28959
|
+
if (category === "huge" && !options.includeLarge) {
|
|
28960
|
+
skipped.push(`${t2.name} (~${(t2.estimatedRowCount || 0).toLocaleString()} rows, huge)`);
|
|
28961
|
+
continue;
|
|
28157
28962
|
}
|
|
28158
|
-
const schema = await adapter.getTableSchema(
|
|
28963
|
+
const schema = await adapter.getTableSchema(t2.name);
|
|
28159
28964
|
const report = await checker.check(schema, {
|
|
28160
28965
|
checks: checkTypes,
|
|
28161
28966
|
sample: sampleSize,
|
|
28162
28967
|
blacklistedColumns
|
|
28163
28968
|
});
|
|
28164
|
-
|
|
28165
|
-
}
|
|
28166
|
-
|
|
28167
|
-
|
|
28168
|
-
|
|
28169
|
-
for (const
|
|
28170
|
-
|
|
28171
|
-
|
|
28172
|
-
continue;
|
|
28173
|
-
}
|
|
28174
|
-
if (t2.tableType === "view") {
|
|
28175
|
-
skipped.push(`${t2.name} (view)`);
|
|
28176
|
-
continue;
|
|
28177
|
-
}
|
|
28178
|
-
const category = getSizeCategory(t2.estimatedRowCount);
|
|
28179
|
-
if (category === "huge" && !options.includeLarge) {
|
|
28180
|
-
skipped.push(`${t2.name} (~${(t2.estimatedRowCount || 0).toLocaleString()} rows, huge)`);
|
|
28181
|
-
continue;
|
|
28182
|
-
}
|
|
28183
|
-
const schema = await adapter.getTableSchema(t2.name);
|
|
28184
|
-
const report = await checker.check(schema, {
|
|
28185
|
-
checks: checkTypes,
|
|
28186
|
-
sample: sampleSize,
|
|
28187
|
-
blacklistedColumns
|
|
28188
|
-
});
|
|
28189
|
-
reports.push(report);
|
|
28969
|
+
reports.push(report);
|
|
28970
|
+
}
|
|
28971
|
+
if (options.format === "json") {
|
|
28972
|
+
console.log(JSON.stringify({ reports, skipped }, null, 2));
|
|
28973
|
+
} else {
|
|
28974
|
+
for (const report of reports) {
|
|
28975
|
+
outputReport(report, "table");
|
|
28976
|
+
console.log("");
|
|
28190
28977
|
}
|
|
28191
|
-
if (
|
|
28192
|
-
console.log(
|
|
28193
|
-
} else {
|
|
28194
|
-
for (const report of reports) {
|
|
28195
|
-
outputReport(report, "table");
|
|
28196
|
-
console.log("");
|
|
28197
|
-
}
|
|
28198
|
-
if (skipped.length > 0) {
|
|
28199
|
-
console.log(`Skipped: ${skipped.join(", ")}`);
|
|
28200
|
-
}
|
|
28978
|
+
if (skipped.length > 0) {
|
|
28979
|
+
console.log(`Skipped: ${skipped.join(", ")}`);
|
|
28201
28980
|
}
|
|
28202
|
-
} else {
|
|
28203
|
-
console.error("Specify a table name or use --all");
|
|
28204
|
-
process.exit(1);
|
|
28205
|
-
}
|
|
28206
|
-
} finally {
|
|
28207
|
-
await adapter.disconnect();
|
|
28208
|
-
}
|
|
28209
|
-
} catch (error) {
|
|
28210
|
-
if (error instanceof Error) {
|
|
28211
|
-
console.error(error.message);
|
|
28212
|
-
if (error instanceof ConnectionError) {
|
|
28213
|
-
error.hints.forEach((hint) => console.error(` Hint: ${hint}`));
|
|
28214
28981
|
}
|
|
28982
|
+
} else {
|
|
28983
|
+
throw new Error("Specify a table name or use --all");
|
|
28215
28984
|
}
|
|
28216
|
-
|
|
28985
|
+
} finally {
|
|
28986
|
+
await adapter.disconnect();
|
|
28217
28987
|
}
|
|
28218
28988
|
}
|
|
28219
28989
|
function outputReport(report, format) {
|
|
@@ -28265,8 +29035,9 @@ var init_check = __esm(() => {
|
|
|
28265
29035
|
init_health_checker();
|
|
28266
29036
|
init_size_category();
|
|
28267
29037
|
init_validation();
|
|
29038
|
+
init_connection_selector();
|
|
28268
29039
|
ALLOWED_FORMATS6 = ["json", "table"];
|
|
28269
|
-
checkCommand = new Command().name("check").description("Run data health checks on tables").argument("[table]", "Table to check (omit for --all)").option("--all", "Check all tables (skips huge tables unless --include-large)", false).option("--include-large", "Include huge tables in --all scan", false).option("--checks <types>", "Comma-separated checks: nulls,duplicates,orphans,emptyStrings", undefined).option("--sample <number>", "Sample size for large tables (default: 10000)", "10000").option("--format <format>", "Output format: json (default) or table", "json").option("--config <path>", "Path to .dbcli config file", ".dbcli").action(checkAction);
|
|
29040
|
+
checkCommand = new Command().name("check").description("Run data health checks on tables").argument("[table]", "Table to check (omit for --all)").option("--all", "Check all tables (skips huge tables unless --include-large)", false).option("--include-large", "Include huge tables in --all scan", false).option("--checks <types>", "Comma-separated checks: nulls,duplicates,orphans,emptyStrings", undefined).option("--sample <number>", "Sample size for large tables (default: 10000)", "10000").option("--format <format>", "Output format: json (default) or table", "json").option("--config <path>", "Path to .dbcli config file", ".dbcli").addOption(createConnectionSelectorOption()).action(checkAction);
|
|
28270
29041
|
});
|
|
28271
29042
|
|
|
28272
29043
|
// src/core/orm-drift/table-identity.ts
|
|
@@ -81533,12 +82304,12 @@ async function runDiagnostic(input) {
|
|
|
81533
82304
|
})();
|
|
81534
82305
|
const timer = new Promise((resolve7) => setTimeout(() => resolve7("timeout"), input.timeoutMs));
|
|
81535
82306
|
let outcome;
|
|
81536
|
-
let
|
|
82307
|
+
let errorMessage2 = null;
|
|
81537
82308
|
try {
|
|
81538
82309
|
outcome = await Promise.race([exec2, timer]);
|
|
81539
82310
|
} catch (err) {
|
|
81540
82311
|
outcome = "error";
|
|
81541
|
-
|
|
82312
|
+
errorMessage2 = err.message;
|
|
81542
82313
|
}
|
|
81543
82314
|
const durationMs = Math.round(performance.now() - start);
|
|
81544
82315
|
if (outcome === "timeout") {
|
|
@@ -81557,7 +82328,7 @@ async function runDiagnostic(input) {
|
|
|
81557
82328
|
rowCount: 0,
|
|
81558
82329
|
rows: [],
|
|
81559
82330
|
status: "error",
|
|
81560
|
-
reason:
|
|
82331
|
+
reason: errorMessage2 ?? "unknown error",
|
|
81561
82332
|
durationMs
|
|
81562
82333
|
};
|
|
81563
82334
|
}
|
|
@@ -91636,7 +92407,7 @@ class DDLExecutor {
|
|
|
91636
92407
|
dryRun: false
|
|
91637
92408
|
};
|
|
91638
92409
|
} catch (error) {
|
|
91639
|
-
const
|
|
92410
|
+
const errorMessage2 = error instanceof Error ? error.message : String(error);
|
|
91640
92411
|
return {
|
|
91641
92412
|
status: "error",
|
|
91642
92413
|
operation: operation.kind,
|
|
@@ -91644,7 +92415,7 @@ class DDLExecutor {
|
|
|
91644
92415
|
warnings: [],
|
|
91645
92416
|
timestamp,
|
|
91646
92417
|
dryRun,
|
|
91647
|
-
error: `DDL execution failed: ${
|
|
92418
|
+
error: `DDL execution failed: ${errorMessage2}`
|
|
91648
92419
|
};
|
|
91649
92420
|
}
|
|
91650
92421
|
}
|
|
@@ -91997,7 +92768,7 @@ var init_use = __esm(() => {
|
|
|
91997
92768
|
init_esm();
|
|
91998
92769
|
init_config_v2();
|
|
91999
92770
|
init_config();
|
|
92000
|
-
|
|
92771
|
+
init_errors3();
|
|
92001
92772
|
init_message_loader();
|
|
92002
92773
|
init_config_binding();
|
|
92003
92774
|
useCommand = new Command("use").description("Switch or display the default database connection (v2 config)").argument("[name]", "Connection name to switch to").option("--list", "List all connections").action(async (name2, options) => {
|
|
@@ -93257,30 +94028,73 @@ var init_proxy = __esm(() => {
|
|
|
93257
94028
|
// src/program.ts
|
|
93258
94029
|
var exports_program = {};
|
|
93259
94030
|
__export(exports_program, {
|
|
94031
|
+
normalizeLimitFlags: () => normalizeLimitFlags,
|
|
93260
94032
|
buildProgram: () => buildProgram
|
|
93261
94033
|
});
|
|
94034
|
+
function parsePositiveInteger(value) {
|
|
94035
|
+
if (!/^\d+$/.test(value)) {
|
|
94036
|
+
throw new InvalidArgumentError("must be a positive integer");
|
|
94037
|
+
}
|
|
94038
|
+
const parsed = Number(value);
|
|
94039
|
+
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
|
|
94040
|
+
throw new InvalidArgumentError("must be a positive integer");
|
|
94041
|
+
}
|
|
94042
|
+
return parsed;
|
|
94043
|
+
}
|
|
94044
|
+
function normalizeLimitFlags(options) {
|
|
94045
|
+
const limit = options.limit;
|
|
94046
|
+
if (typeof limit === "boolean") {
|
|
94047
|
+
return { ...options, limit: undefined, noLimit: limit === false };
|
|
94048
|
+
}
|
|
94049
|
+
return { ...options, noLimit: false };
|
|
94050
|
+
}
|
|
94051
|
+
function findLongOptionValue(rawArgs, option) {
|
|
94052
|
+
let value;
|
|
94053
|
+
for (let index = 0;index < rawArgs.length; index++) {
|
|
94054
|
+
const token = rawArgs[index];
|
|
94055
|
+
if (token === "--")
|
|
94056
|
+
break;
|
|
94057
|
+
if (token === option) {
|
|
94058
|
+
value = rawArgs[index + 1];
|
|
94059
|
+
index++;
|
|
94060
|
+
} else if (token.startsWith(`${option}=`)) {
|
|
94061
|
+
value = token.slice(option.length + 1);
|
|
94062
|
+
}
|
|
94063
|
+
}
|
|
94064
|
+
return value;
|
|
94065
|
+
}
|
|
94066
|
+
function hasLongOption(rawArgs, option) {
|
|
94067
|
+
for (const token of rawArgs) {
|
|
94068
|
+
if (token === "--")
|
|
94069
|
+
return false;
|
|
94070
|
+
if (token === option || token.startsWith(`${option}=`))
|
|
94071
|
+
return true;
|
|
94072
|
+
}
|
|
94073
|
+
return false;
|
|
94074
|
+
}
|
|
93262
94075
|
function buildProgram() {
|
|
93263
|
-
const program2 = new Command().name("dbcli").description("Database CLI for AI agents").version(package_default.version).option("--no-color", "Disable colored output").option("-v, --verbose", "Increase verbosity (-v verbose, -vv debug)", (_, prev) => prev + 1, 0).option("-q, --quiet", "Suppress non-essential output").option("--config <path>", "Path to .dbcli config file", ".dbcli").
|
|
94076
|
+
const program2 = new Command().name("dbcli").description("Database CLI for AI agents").version(package_default.version).option("--no-color", "Disable colored output").option("-v, --verbose", "Increase verbosity (-v verbose, -vv debug)", (_, prev) => prev + 1, 0).option("-q, --quiet", "Suppress non-essential output").option("--config <path>", "Path to .dbcli config file", ".dbcli").addOption(createConnectionSelectorOption()).enablePositionalOptions();
|
|
93264
94077
|
program2.addCommand(initCommand);
|
|
93265
94078
|
program2.addCommand(listCommand);
|
|
93266
94079
|
program2.addCommand(schemaCommand);
|
|
93267
|
-
program2.command("query
|
|
93268
|
-
|
|
93269
|
-
|
|
93270
|
-
|
|
93271
|
-
|
|
93272
|
-
|
|
93273
|
-
|
|
93274
|
-
|
|
93275
|
-
|
|
93276
|
-
|
|
93277
|
-
|
|
94080
|
+
program2.command("query [sql]").description(t("query.description")).option("-f, --query-file <path>", "Read query text from a UTF-8 file, or - for stdin").option("--format <type>", "Output format: table, json, csv, html", "table").option("--ui", "Show interactive dashboard in browser", false).option("--limit <number>", "Limit result rows (overrides auto-limit)", parsePositiveInteger).option("--no-limit", "Disable auto-limit in query-only mode").option("--collection <name>", "MongoDB collection name; Elasticsearch index name").option("--index <name>", "Elasticsearch index name (alias for --collection)").option("--fields <list>", "Include fields, or exclude them with --fields=-field_a,-field_b").option("--truncate <number>", "Limit serialized table cells to N Unicode characters", parsePositiveInteger).option("--no-truncate", "Disable the default table-cell truncation").addOption(createConnectionSelectorOption()).option("--recovery", "On failure, emit a structured recovery envelope to stdout (suppresses human stderr message)", false).action(async (sql, options, command) => {
|
|
94081
|
+
const rawArgs = command.parent?.rawArgs ?? [];
|
|
94082
|
+
const rawTruncate = findLongOptionValue(rawArgs, "--truncate");
|
|
94083
|
+
const rootUse = command.parent?.opts().use;
|
|
94084
|
+
const commandUse = options.use;
|
|
94085
|
+
const connectionSelector = rootUse !== undefined || commandUse !== undefined ? resolveConnectionSelector({ root: rootUse, command: commandUse }) : undefined;
|
|
94086
|
+
await queryCommand(sql, {
|
|
94087
|
+
...normalizeLimitFlags(options),
|
|
94088
|
+
connectionSelector,
|
|
94089
|
+
truncate: rawTruncate === undefined ? undefined : parsePositiveInteger(rawTruncate),
|
|
94090
|
+
noTruncate: hasLongOption(rawArgs, "--no-truncate")
|
|
94091
|
+
}, command);
|
|
93278
94092
|
});
|
|
93279
94093
|
program2.command("plan <sql>").description("Analyze SQL risk without executing").option("--format <type>", "Output format: text, json", "text").action(async (sql, options, command) => {
|
|
93280
94094
|
await planCommand(sql, options, command);
|
|
93281
94095
|
});
|
|
93282
94096
|
program2.command("q <name>").description(t("q.description")).option("--format <type>", "Output format: table, json, csv, html", "table").option("--ui", "Show interactive dashboard in browser", false).option("--no-limit", "Disable size guard wrap (LIMIT 1000)").option("--dry-run", "Show final SQL + bind values; do not execute").option("--param <kv>", "Pass parameter as key=value (repeatable)", (val, prev = []) => prev.concat([val]), []).option("--param-file <path>", "JSON file containing param values").option("--recovery", "On failure, emit a structured recovery envelope to stdout (suppresses human stderr message)", false).option("--verify", "Run verification check after execution if defined", false).action(async (name2, options, command) => {
|
|
93283
|
-
await qCommand(name2, options, command);
|
|
94097
|
+
await qCommand(name2, normalizeLimitFlags(options), command);
|
|
93284
94098
|
});
|
|
93285
94099
|
program2.command("insert <table>").description(t("insert.description")).option("--data <json>", "JSON object to insert").option("--dry-run", "Show generated SQL without executing").option("--force", "Skip confirmation prompt").option("--plan", "Analyze risk without connecting or executing").option("--format <type>", "Output format for --plan: text or json", "text").option("--recovery", "On failure, emit a structured recovery envelope to stdout (suppresses human stderr message)", false).action(async (table, options, command) => {
|
|
93286
94100
|
try {
|
|
@@ -93290,7 +94104,7 @@ function buildProgram() {
|
|
|
93290
94104
|
const { emitRecoveryEnvelope: emitRecoveryEnvelope2 } = await Promise.resolve().then(() => (init_recovery(), exports_recovery));
|
|
93291
94105
|
emitRecoveryEnvelope2(error, { operation: "insert", table, writeOperation: "INSERT" });
|
|
93292
94106
|
}
|
|
93293
|
-
|
|
94107
|
+
printLocalizedCliError(error.message, error);
|
|
93294
94108
|
process.exit(1);
|
|
93295
94109
|
}
|
|
93296
94110
|
});
|
|
@@ -93302,7 +94116,7 @@ function buildProgram() {
|
|
|
93302
94116
|
const { emitRecoveryEnvelope: emitRecoveryEnvelope2 } = await Promise.resolve().then(() => (init_recovery(), exports_recovery));
|
|
93303
94117
|
emitRecoveryEnvelope2(error, { operation: "update", table, writeOperation: "UPDATE" });
|
|
93304
94118
|
}
|
|
93305
|
-
|
|
94119
|
+
printLocalizedCliError(error.message, error);
|
|
93306
94120
|
process.exit(1);
|
|
93307
94121
|
}
|
|
93308
94122
|
});
|
|
@@ -93314,24 +94128,12 @@ function buildProgram() {
|
|
|
93314
94128
|
const { emitRecoveryEnvelope: emitRecoveryEnvelope2 } = await Promise.resolve().then(() => (init_recovery(), exports_recovery));
|
|
93315
94129
|
emitRecoveryEnvelope2(error, { operation: "delete", table, writeOperation: "DELETE" });
|
|
93316
94130
|
}
|
|
93317
|
-
|
|
94131
|
+
printLocalizedCliError(error.message, error);
|
|
93318
94132
|
process.exit(1);
|
|
93319
94133
|
}
|
|
93320
94134
|
});
|
|
93321
|
-
program2.command("export <sql>").description(t("export.description")).option("--format <format>", "Output format: json, jsonl, csv, html", "json").option("--output <path>", "Output file path (if omitted, write to stdout)", undefined).option("--force", "Skip overwrite confirmation", false).option("--collection <name>", "MongoDB collection name; Elasticsearch index name").option("--index <name>", "Elasticsearch index name (alias for --collection)").option("--limit <number>", "Limit result rows (overrides auto-limit)", (val) => parseInt(val, 10)).option("--no-limit", "Disable auto-limit in query-only mode").option("--recovery", "On failure, emit a structured recovery envelope to stdout (suppresses human stderr message)", false).action(async (sql, options, command) => {
|
|
93322
|
-
|
|
93323
|
-
const { validateFormat: validateFormat2 } = await Promise.resolve().then(() => (init_validation(), exports_validation));
|
|
93324
|
-
validateFormat2(options.format, ["json", "jsonl", "csv", "html"], "export");
|
|
93325
|
-
return await exportCommand(sql, options, command);
|
|
93326
|
-
} catch (error) {
|
|
93327
|
-
if (options.recovery === true) {
|
|
93328
|
-
const { emitRecoveryEnvelope: emitRecoveryEnvelope2 } = await Promise.resolve().then(() => (init_recovery(), exports_recovery));
|
|
93329
|
-
const m = sql.match(/\bFROM\s+[`"']?(\w+)[`"']?/i);
|
|
93330
|
-
emitRecoveryEnvelope2(error, { operation: "export", table: m?.[1] });
|
|
93331
|
-
}
|
|
93332
|
-
console.error(error.message);
|
|
93333
|
-
process.exit(1);
|
|
93334
|
-
}
|
|
94135
|
+
program2.command("export <sql>").description(t("export.description")).option("--format <format>", "Output format: json, jsonl, csv, html", "json").option("--output <path>", "Output file path (if omitted, write to stdout)", undefined).option("--force", "Skip overwrite confirmation", false).option("--collection <name>", "MongoDB collection name; Elasticsearch index name").option("--index <name>", "Elasticsearch index name (alias for --collection)").addOption(createConnectionSelectorOption()).option("--limit <number>", "Limit result rows (overrides auto-limit)", (val) => parseInt(val, 10)).option("--no-limit", "Disable auto-limit in query-only mode").option("--recovery", "On failure, emit a structured recovery envelope to stdout (suppresses human stderr message)", false).action(async (sql, options, command) => {
|
|
94136
|
+
return await exportCommand(sql, normalizeLimitFlags(options), command);
|
|
93335
94137
|
});
|
|
93336
94138
|
const skillCmd = registerSkillCommand(program2);
|
|
93337
94139
|
registerSkillTasksCommand(skillCmd);
|
|
@@ -93402,6 +94204,8 @@ var init_program = __esm(() => {
|
|
|
93402
94204
|
init_migrate();
|
|
93403
94205
|
init_use();
|
|
93404
94206
|
init_proxy();
|
|
94207
|
+
init_connection_selector();
|
|
94208
|
+
init_cli_error();
|
|
93405
94209
|
init_package();
|
|
93406
94210
|
});
|
|
93407
94211
|
|
|
@@ -93412,8 +94216,49 @@ init_upgrade();
|
|
|
93412
94216
|
init_version_check();
|
|
93413
94217
|
init_skill();
|
|
93414
94218
|
init_config();
|
|
94219
|
+
init_connection_selector();
|
|
93415
94220
|
init_program();
|
|
94221
|
+
init_cli_error();
|
|
93416
94222
|
import { join as join35 } from "path";
|
|
94223
|
+
import { writeSync as writeSync2 } from "fs";
|
|
94224
|
+
import { format } from "util";
|
|
94225
|
+
function installSynchronousRedirectedStdout() {
|
|
94226
|
+
if (process.stdout.isTTY)
|
|
94227
|
+
return;
|
|
94228
|
+
const retrySignal = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
|
|
94229
|
+
const writeAll = (bytes) => {
|
|
94230
|
+
let offset = 0;
|
|
94231
|
+
while (offset < bytes.byteLength) {
|
|
94232
|
+
let written;
|
|
94233
|
+
try {
|
|
94234
|
+
written = writeSync2(1, bytes, offset, bytes.byteLength - offset);
|
|
94235
|
+
} catch (error) {
|
|
94236
|
+
const code = error.code;
|
|
94237
|
+
if (code === "EAGAIN" || code === "EWOULDBLOCK") {
|
|
94238
|
+
Atomics.wait(retrySignal, 0, 0, 1);
|
|
94239
|
+
continue;
|
|
94240
|
+
}
|
|
94241
|
+
throw error;
|
|
94242
|
+
}
|
|
94243
|
+
if (written === 0)
|
|
94244
|
+
throw new Error("Unable to write CLI output to stdout");
|
|
94245
|
+
offset += written;
|
|
94246
|
+
}
|
|
94247
|
+
};
|
|
94248
|
+
process.stdout.write = (chunk, encodingOrCallback, callback) => {
|
|
94249
|
+
const encoding = typeof encodingOrCallback === "string" ? encodingOrCallback : undefined;
|
|
94250
|
+
const onComplete = typeof encodingOrCallback === "function" ? encodingOrCallback : callback;
|
|
94251
|
+
const bytes = typeof chunk === "string" ? Buffer.from(chunk, encoding) : Buffer.from(chunk);
|
|
94252
|
+
writeAll(bytes);
|
|
94253
|
+
onComplete?.();
|
|
94254
|
+
return true;
|
|
94255
|
+
};
|
|
94256
|
+
console.log = (...args) => {
|
|
94257
|
+
writeAll(Buffer.from(`${format(...args)}
|
|
94258
|
+
`));
|
|
94259
|
+
};
|
|
94260
|
+
}
|
|
94261
|
+
installSynchronousRedirectedStdout();
|
|
93417
94262
|
var _bgVersionCheckResult;
|
|
93418
94263
|
function shouldSkipBackgroundChecks() {
|
|
93419
94264
|
return process.env.DBCLI_NO_UPDATE_CHECK === "1" || process.env.DBCLI_NO_UPDATE_CHECK === "true" || false;
|
|
@@ -93422,8 +94267,11 @@ var QUIET_OUTPUT_COMMANDS = new Set(["upgrade", "completion"]);
|
|
|
93422
94267
|
var program2 = buildProgram();
|
|
93423
94268
|
program2.hook("preAction", (thisCommand, actionCommand) => {
|
|
93424
94269
|
const opts = thisCommand.opts();
|
|
93425
|
-
|
|
93426
|
-
|
|
94270
|
+
setGlobalConnectionName(resolveConnectionSelector({
|
|
94271
|
+
root: opts.use,
|
|
94272
|
+
command: actionCommand.opts().use,
|
|
94273
|
+
environment: process.env.DBCLI_CONNECTION
|
|
94274
|
+
}));
|
|
93427
94275
|
if (opts.color === false) {
|
|
93428
94276
|
process.env.NO_COLOR = "1";
|
|
93429
94277
|
}
|
|
@@ -93472,7 +94320,12 @@ program2.hook("postAction", async (thisCommand, actionCommand) => {
|
|
|
93472
94320
|
if (!process.argv.slice(2).length) {
|
|
93473
94321
|
program2.outputHelp();
|
|
93474
94322
|
}
|
|
93475
|
-
|
|
94323
|
+
try {
|
|
94324
|
+
await program2.parseAsync(process.argv);
|
|
94325
|
+
} catch (error) {
|
|
94326
|
+
presentCliError(error);
|
|
94327
|
+
process.exitCode = 1;
|
|
94328
|
+
}
|
|
93476
94329
|
var cli_default = program2;
|
|
93477
94330
|
export {
|
|
93478
94331
|
cli_default as default
|