@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/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.42.0",
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.14",
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.6"
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/utils/errors.ts
7271
- var EnvParseError, ConfigError;
7258
+ // src/agent-core/errors.ts
7259
+ var ConfigError;
7272
7260
  var init_errors2 = __esm(() => {
7273
- EnvParseError = class EnvParseError extends Error {
7261
+ ConfigError = class ConfigError extends Error {
7274
7262
  constructor(message) {
7275
7263
  super(message);
7276
- this.name = "EnvParseError";
7277
- if (Error.captureStackTrace) {
7278
- Error.captureStackTrace(this, EnvParseError);
7279
- }
7264
+ this.name = "ConfigError";
7265
+ if (Error.captureStackTrace)
7266
+ Error.captureStackTrace(this, ConfigError);
7280
7267
  }
7281
7268
  };
7282
- ConfigError = class ConfigError extends Error {
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 = "ConfigError";
7278
+ this.name = "EnvParseError";
7286
7279
  if (Error.captureStackTrace) {
7287
- Error.captureStackTrace(this, ConfigError);
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
- const exists = await file.exists();
7316
- if (!exists) {
7308
+ if (!await file.exists())
7317
7309
  throw new ConfigError(`\u627E\u4E0D\u5230 env \u6A94\u6848\uFF1A${filePath}`);
7318
- }
7319
- const content = await file.text();
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
- init_config_binding();
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, strict = false) {
8538
+ function resolveEnvReferences(config, env, parentKey) {
8484
8539
  if (isEnvReference(config)) {
8485
8540
  const envKey = config.$env;
8486
- const value = env[envKey];
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, strict));
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, strict);
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
- init_errors2();
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, undefined, false);
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
- const resolvedConfig = resolveEnvReferences(config, process.env, undefined, false);
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, undefined, false);
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 < GUARD_LIMIT)
8961
- return trimmed;
8962
- return `SELECT * FROM (${trimmed}) AS _dbcli_guard LIMIT ${GUARD_LIMIT}`;
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 GUARD_LIMIT = 1000;
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 wrapped = applySnippetGuard(rewritten.sql, { noLimit: opts.noLimit });
9055
+ const guarded = applySnippetGuard(rewritten.sql, { noLimit: opts.noLimit });
9003
9056
  return {
9004
- driver: { sql: wrapped, values: rewritten.values },
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
- init_errors2();
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 init_errors3 = __esm(() => {
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
- init_errors3();
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
- init_errors3();
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
- init_errors3();
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 lastStage = stages[stages.length - 1];
13379
- const alreadyHasLimit = !!lastStage && typeof lastStage === "object" && "$limit" in lastStage;
13380
- const finalStages = typeof limit === "number" && limit > 0 && !alreadyHasLimit ? [...stages, { $limit: limit }] : stages;
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
- docs = await collection.find(parsed).limit(cap2).toArray();
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
- const footerLines = [];
17332
- footerLines.push(`Rows: ${result.rowCount}`);
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
- const footerLines = [];
17353
- footerLines.push(`Rows: ${result.rowCount}`);
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
- const output = {
17370
- rows: result.rows,
17371
- rowCount: result.rowCount,
17372
- columnNames: result.columnNames,
17373
- columnTypes: result.columnTypes,
17374
- executionTimeMs: result.executionTimeMs,
17375
- metadata: result.metadata
17376
- };
17377
- return JSON.stringify(output, null, spacing);
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
- return JSON.stringify(value);
17506
+ serialized = JSON.stringify(value);
17507
+ } else {
17508
+ serialized = String(value);
17417
17509
  }
17418
- return String(value);
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
- validateFormat(options.format, ALLOWED_FORMATS, "list");
17443
- const config = await configModule.read(resolveConfigPath(command, options));
17444
- if (!config.connection) {
17445
- console.error("Database not configured. Run: dbcli init");
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 (config.connection.system === "elasticsearch") {
17455
- return elasticsearchListBranch(config, options.format, options.includeSystem === true);
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 adapter = AdapterFactory.createSqlAdapter(requireSqlConnection(config.connection));
17458
- await adapter.connect();
17459
- try {
17460
- const tables = await adapter.listTables();
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
- } finally {
17485
- await adapter.disconnect();
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 = columnList.filter((col) => blacklistedColumns.includes(col));
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.map((row) => {
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.startsWith("--")) {
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.startsWith("--")) {
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 === "lint" && LINT_BOOLEAN_FLAGS.has(name2)) {
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.startsWith("--")) {
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 === "lint" && LINT_BOOLEAN_FLAGS.has(name2)) {
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
- if (char === "-" && sql[i + 1] === "-") {
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 (char === "'" || char === '"') {
20062
- const quote = char;
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 && sql[i] !== quote) {
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
- if (i < sql.length) {
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 readPath(obj, path4) {
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 = readPath(obj, path4);
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 readPath(obj, path4) === expected;
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
- console.error("Database not configured. Run: dbcli init");
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
- console.error(`--sample-size must be a positive integer (received ${options.sampleSize})`);
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
- console.error(`--sample-method must be 'random' or 'natural' (received '${options.sampleMethod}')`);
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
- console.error("schema --reset/--refresh is not supported for Redis connections.");
22189
- process.exit(1);
22511
+ throw new Error("schema --reset/--refresh is not supported for Redis connections.");
22190
22512
  }
22191
22513
  if (!table) {
22192
- console.error("Redis schema inspection requires a key name: dbcli schema <key>");
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
- if (error instanceof Error) {
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
- if (classification.isDangerous && this.permission === "admin") {
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
- if (this.permission === "query-only" && AUTO_LIMIT_TYPES.has(classification.type) && !executeSql.match(/LIMIT\s+\d+/i) && options?.autoLimit !== false) {
22621
- const limitValue = options?.limitValue || DEFAULT_QUERY_ONLY_LIMIT;
22622
- executeSql = `${executeSql} LIMIT ${limitValue}`;
22623
- console.error(`Query-only mode: auto-limiting to ${limitValue} rows`);
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 rows = resultData.rows;
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: affectedRows,
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
- if (!sql || sql.trim() === "") {
22832
- throw new Error("SQL query required");
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
- sql = sql.trim();
22838
- const configPath = resolveConfigPath(command, options);
22839
- config = await configModule.read(configPath);
22840
- if (!config.connection) {
22841
- throw new Error('Run "dbcli init" first');
22842
- }
22843
- if (config.connection.system === "mongodb") {
22844
- return await mongoQueryBranch(sql, options, config);
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(sql) ?? undefined
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
- if (error instanceof BlacklistError) {
22929
- console.error(error.message);
22930
- process.exit(1);
22931
- }
22932
- if (error instanceof PermissionError) {
22933
- console.error(t_vars("errors.permission_denied", { required: error.requiredPermission }));
22934
- console.error(` Operation: ${error.classification.type}`);
22935
- console.error(` Message: ${error.message}`);
22936
- process.exit(1);
22937
- }
22938
- if (error instanceof ConnectionError) {
22939
- console.error(t_vars("errors.connection_failed", { message: error.message }));
22940
- process.exit(1);
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
- console.error(t_vars("errors.message", { message: error.message }));
22943
- process.exit(1);
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 mongoQueryBranch(queryStr, options, config) {
23527
+ async function preflightMongoQuery(query, options, config, multiConnection) {
22947
23528
  const collection = options.collection;
22948
- const format = options.format ?? "table";
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(queryStr)) {
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(queryStr);
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
- const blacklistManager = new BlacklistManager(config);
22963
- const blacklistValidator = new BlacklistValidator(blacklistManager);
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 && !options.noLimit) {
22966
- const tableSchema = config.schema[collection];
22967
- if (tableSchema) {
22968
- const { shouldBlockQuery: shouldBlockQuery2 } = await Promise.resolve().then(() => (init_query_size_guard(), exports_query_size_guard));
22969
- const isFiltered = queryStr.length > 2;
22970
- const hasLimit = options.limit !== undefined;
22971
- const dummySql = `SELECT * FROM ${collection}${isFiltered ? " WHERE" : ""}${hasLimit ? " LIMIT" : ""}`;
22972
- const guard = shouldBlockQuery2(dummySql, tableSchema);
22973
- if (guard.blocked) {
22974
- throw new Error(`\u26A0 ${guard.reason}`);
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
- await mongoAdapter.connect();
22989
- const start = performance.now();
23712
+ let executionError;
22990
23713
  try {
22991
- const result = await mongoAdapter.execute(queryStr, [collection], effectiveLimit !== undefined ? { limit: effectiveLimit } : undefined);
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
- await writeAuditEntry(config, "query", options, {
22994
- success: true,
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(result.rows, collection, blacklistCfg);
23003
- const columnNames = maskedRows[0] ? Object.keys(maskedRows[0]) : [];
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: maskedRows,
23006
- rowCount: maskedRows.length,
23007
- columnNames
23731
+ rows: outputRows,
23732
+ rowCount: outputRows.length,
23733
+ columnNames,
23734
+ executionTimeMs,
23735
+ ...limitedResult ? { appliedLimit: limitedResult.metadata } : {}
23008
23736
  };
23009
- const formatter = new QueryResultFormatter;
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
- console.log(`
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.disconnect();
23755
+ await disconnectPreservingError(mongoAdapter, executionError);
23020
23756
  }
23021
23757
  }
23022
- async function redisQueryBranch(command, options, config) {
23023
- const format = options.format ?? "table";
23024
- const { enforceRedisPermission: enforceRedisPermission2 } = await Promise.resolve().then(() => (init_permission_guard(), exports_permission_guard));
23025
- const head = command.trim().split(/\s+/)[0]?.toUpperCase() ?? "";
23026
- if (head === "KEYS" && options.recovery !== true) {
23027
- console.error('\u26A0 Warning: "KEYS" command is dangerous on production servers.');
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
- enforceRedisPermission2(command, config.permission);
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
- await redisAdapter.connect();
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 formatter = new QueryResultFormatter;
23054
- const output = formatter.format(queryResult, {
23055
- format
23056
- });
23057
- console.log(output);
23058
- } catch (err) {
23059
- if (err instanceof BlacklistRejection) {
23060
- await writeAuditEntry(config, "query", options, {
23061
- success: false,
23062
- error: err,
23063
- metadata: {
23064
- rejection_reason: "blacklist",
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.disconnect();
23799
+ await disconnectPreservingError(redisAdapter, executionError);
23079
23800
  }
23080
23801
  }
23081
- async function elasticsearchQueryBranch(queryStr, options, config) {
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
- const blacklistManager = new BlacklistManager(config);
23101
- const blacklistValidator = new BlacklistValidator(blacklistManager);
23102
- blacklistValidator.checkTableBlacklist("SELECT", indexName, []);
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
- await esAdapter.connect();
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
- await writeAuditEntry(config, "query", options, {
23112
- success: true,
23113
- target: indexName,
23114
- metadata: {
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
- if (securityNote) {
23133
- console.log(`
23134
- \u2139 ${securityNote}`);
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.disconnect();
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({ meta: snippet.query.meta, rows: masked });
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 columnNames = result.rows[0] ? Object.keys(result.rows[0]) : [];
23843
- const filtered = family === "redis" ? { filteredRows: result.rows, omittedColumns: [] } : blacklistValidator.filterColumns(targetName, result.rows, columnNames);
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
- ...filtered.omittedColumns.length > 0 ? {
23878
- securityNotification: blacklistValidator.buildSecurityNotification(targetName, filtered.omittedColumns)
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
- console.error(error.message);
24717
+ printLocalizedCliError(error.message, error);
23963
24718
  process.exit(1);
23964
24719
  }
23965
24720
  if (error instanceof BlacklistError) {
23966
- console.error(error.message);
24721
+ printLocalizedCliError(error.message, error);
23967
24722
  process.exit(1);
23968
24723
  }
23969
24724
  if (error instanceof PermissionError) {
23970
- console.error(t_vars("errors.permission_denied", { required: error.requiredPermission }));
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
- console.error(t_vars("errors.connection_failed", { message: error.message }));
24729
+ printLocalizedCliError(t_vars("errors.connection_failed", { message: error.message }), error);
23975
24730
  process.exit(1);
23976
24731
  }
23977
- console.error(t_vars("errors.message", { message: error.message }));
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 errorMessage = error instanceof Error ? error.message : String(error);
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: ${errorMessage}`
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 errorMessage = error instanceof Error ? error.message : String(error);
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: ${errorMessage}`
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 errorMessage = error instanceof Error ? error.message : String(error);
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: ${errorMessage}`
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
- console.error(t_vars("errors.connection_failed", { message: error.message }));
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
- console.error(t_vars("errors.connection_failed", { message: error.message }));
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
- console.error(t_vars("errors.connection_failed", { message: error.message }));
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 executor3 = new QueryExecutor(adapter, config.permission);
26903
- const result = await executor3.execute(sql, { autoLimit: true });
26904
- let formatted;
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
- if (error instanceof PermissionError) {
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
- const formatted = formatter.format(queryResult, { format: options.format });
27003
- if (options.output) {
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: result.rowCount ?? result.rows.length ?? 0,
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: cap2 === Number.POSITIVE_INFINITY ? 1e4 : cap2
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, cap2 === Number.POSITIVE_INFINITY ? 1e6 : cap2);
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 { rows, target } = await buildEsExportRows(query, options, adapter);
27804
+ const {
27805
+ rows: fetched,
27806
+ target,
27807
+ cap: cap2
27808
+ } = await buildEsExportRows(query, options, adapter);
27059
27809
  blacklistValidator.checkTableBlacklist("SELECT", target, []);
27060
- if (!options.noLimit && rows.length >= ES_EXPORT_CAP) {
27061
- console.error(`Warning: result capped at ${ES_EXPORT_CAP} rows. Use --no-limit to export the full index.`);
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
- const formatted = formatMongoRows(rows, columns, options.format);
27065
- if (options.output) {
27066
- const file = Bun.file(options.output);
27067
- const exists = await file.exists();
27068
- if (exists && !options.force) {
27069
- const confirmed = await promptUser.confirm(t_vars("export.overwrite_confirmation", { file: options.output }));
27070
- if (!confirmed) {
27071
- console.error("Operation cancelled by user");
27072
- return;
27073
- }
27074
- }
27075
- await file.write(formatted);
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
- console.log(formatted);
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: rows.length,
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
- console.error("\u9019\u662F MongoDB \u9023\u7DDA\uFF0C\u8ACB\u4F7F\u7528 JSON filter \u6216 aggregation pipeline\u3002");
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
- console.error("MongoDB export \u9700\u8981\u6307\u5B9A --collection <name>");
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
- console.error("MongoDB \u67E5\u8A62\u5FC5\u9808\u662F\u6709\u6548\u7684 JSON\uFF08object filter \u6216 array pipeline\uFF09");
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
- const formatted = formatMongoRows(maskedRows, visibleColumns, options.format);
27130
- if (options.output) {
27131
- const file = Bun.file(options.output);
27132
- const exists = await file.exists();
27133
- if (exists && !options.force) {
27134
- const confirmed = await promptUser.confirm(t_vars("export.overwrite_confirmation", { file: options.output }));
27135
- if (!confirmed) {
27136
- console.error("Operation cancelled by user");
27137
- return;
27138
- }
27139
- }
27140
- await file.write(formatted);
27141
- console.error(t_vars("export.exported", {
27142
- count: maskedRows.length,
27143
- file: options.output
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
- console.log(formatted);
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: maskedRows.length,
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
- console.error("Warning: nested object/array fields were JSON-stringified for CSV. Use --format jsonl to preserve structure.");
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
- init_blacklist();
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
- validateFormat(options.format, ALLOWED_FORMATS6, "check");
28139
- const config = await configModule.read(options.config);
28140
- if (!config.connection) {
28141
- console.error("Database not configured. Run: dbcli init");
28142
- process.exit(1);
28143
- }
28144
- const adapter = AdapterFactory.createSqlAdapter(requireSqlConnection7(config.connection));
28145
- await adapter.connect();
28146
- try {
28147
- const checker = new HealthChecker(adapter);
28148
- const blacklistManager = new BlacklistManager(config);
28149
- const blacklistedColumns = getBlacklistedColumnSet(blacklistManager);
28150
- const blacklistedTables = getBlacklistedTableSet(blacklistManager);
28151
- const checkTypes = options.checks ? options.checks.split(",") : undefined;
28152
- const sampleSize = parseInt(options.sample, 10) || 1e4;
28153
- if (table) {
28154
- if (blacklistedTables.has(table.toLowerCase())) {
28155
- console.error(`Table "${table}" is blacklisted`);
28156
- process.exit(1);
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(table);
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
- outputReport(report, options.format);
28165
- } else if (options.all) {
28166
- const tables = await adapter.listTables();
28167
- const reports = [];
28168
- const skipped = [];
28169
- for (const t2 of tables) {
28170
- if (blacklistedTables.has(t2.name.toLowerCase())) {
28171
- skipped.push(`${t2.name} (blacklisted)`);
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 (options.format === "json") {
28192
- console.log(JSON.stringify({ reports, skipped }, null, 2));
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
- process.exit(1);
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 errorMessage = null;
82307
+ let errorMessage2 = null;
81537
82308
  try {
81538
82309
  outcome = await Promise.race([exec2, timer]);
81539
82310
  } catch (err) {
81540
82311
  outcome = "error";
81541
- errorMessage = err.message;
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: errorMessage ?? "unknown error",
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 errorMessage = error instanceof Error ? error.message : String(error);
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: ${errorMessage}`
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
- init_errors2();
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").option("--use <connection>", "Use a specific named connection (v2 config)").enablePositionalOptions();
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 <sql>").description(t("query.description")).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)", (val) => parseInt(val, 10)).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("--recovery", "On failure, emit a structured recovery envelope to stdout (suppresses human stderr message)", false).action(async (sql, options, command) => {
93268
- try {
93269
- await queryCommand(sql, options, command);
93270
- } catch (error) {
93271
- if (options.recovery === true) {
93272
- const { emitRecoveryEnvelope: emitRecoveryEnvelope2 } = await Promise.resolve().then(() => (init_recovery(), exports_recovery));
93273
- emitRecoveryEnvelope2(error, { operation: "query" });
93274
- }
93275
- console.error(error.message);
93276
- process.exit(1);
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
- console.error(error.message);
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
- console.error(error.message);
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
- console.error(error.message);
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
- try {
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
- const useConnection = opts.use;
93426
- setGlobalConnectionName(useConnection);
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
- program2.parse(process.argv);
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