@carllee1983/dbcli 1.41.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/core.mjs CHANGED
@@ -13574,6 +13574,8 @@ function getEngineCapability(system, key) {
13574
13574
  }
13575
13575
  // src/adapters/error-mapper.ts
13576
13576
  function mapError(error, system, options) {
13577
+ if (error instanceof ConnectionError)
13578
+ return error;
13577
13579
  const err = error;
13578
13580
  const errMsg = String(err?.message || String(error));
13579
13581
  const errCode = String(err?.code || "");
@@ -14920,7 +14922,7 @@ class MySQLAdapter {
14920
14922
  JOIN information_schema.KEY_COLUMN_USAGE kcu
14921
14923
  ON rc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME AND rc.TABLE_NAME = kcu.TABLE_NAME
14922
14924
  WHERE kcu.TABLE_NAME = ? AND rc.CONSTRAINT_SCHEMA = DATABASE()
14923
- GROUP BY rc.CONSTRAINT_NAME
14925
+ GROUP BY rc.CONSTRAINT_NAME, rc.REFERENCED_TABLE_NAME
14924
14926
  `;
14925
14927
  const fkResult = await this.execute(fkQuery, [tableName]);
14926
14928
  const fkResults = fkResult.rows;
@@ -15152,13 +15154,14 @@ class MongoDBAdapter {
15152
15154
  let docs;
15153
15155
  if (Array.isArray(parsed)) {
15154
15156
  const stages = parsed;
15155
- const lastStage = stages[stages.length - 1];
15156
- const alreadyHasLimit = !!lastStage && typeof lastStage === "object" && "$limit" in lastStage;
15157
- const finalStages = typeof limit === "number" && limit > 0 && !alreadyHasLimit ? [...stages, { $limit: limit }] : stages;
15157
+ const alreadyHasLimit = stages.some((stage) => stage !== null && typeof stage === "object" && ("$limit" in stage));
15158
+ const limitedStages = typeof limit === "number" && limit > 0 && !alreadyHasLimit ? [...stages, { $limit: limit }] : stages;
15159
+ const finalStages = options?.projection ? [...limitedStages, { $project: options.projection }] : limitedStages;
15158
15160
  docs = await collection.aggregate(finalStages).toArray();
15159
15161
  } else {
15160
15162
  const cap2 = typeof limit === "number" && limit > 0 ? limit : 0;
15161
- docs = await collection.find(parsed).limit(cap2).toArray();
15163
+ const cursor = options?.projection ? collection.find(parsed, { projection: options.projection }) : collection.find(parsed);
15164
+ docs = await cursor.limit(cap2).toArray();
15162
15165
  }
15163
15166
  return { rows: docs, affectedRows: docs.length };
15164
15167
  }
@@ -16743,24 +16746,48 @@ function normalizeSQL(sql) {
16743
16746
  return sql.replace(/--[^\n]*\n/g, `
16744
16747
  `).replace(/\/\*[\s\S]*?\*\//g, " ").trim().replace(/\s+/g, " ");
16745
16748
  }
16746
- function stripCommentsAndStrings(sql) {
16749
+ function stripCommentsAndStrings(sql, options = {}) {
16747
16750
  let result = "";
16748
16751
  let i = 0;
16749
16752
  while (i < sql.length) {
16750
16753
  const char = sql[i];
16751
- if (char === "-" && sql[i + 1] === "-") {
16754
+ const mysqlDialect = options.dialect === "mysql" || options.dialect === "mariadb";
16755
+ const dashFollowerCode = sql.charCodeAt(i + 2);
16756
+ const dashStartsComment = char === "-" && sql[i + 1] === "-" && (!mysqlDialect || sql[i + 2] === undefined || dashFollowerCode <= 32 || dashFollowerCode === 127);
16757
+ if (dashStartsComment) {
16752
16758
  while (i < sql.length && sql[i] !== `
16753
16759
  `) {
16754
16760
  i++;
16755
16761
  }
16756
16762
  if (i < sql.length) {
16757
16763
  result += `
16764
+ `;
16765
+ i++;
16766
+ }
16767
+ continue;
16768
+ }
16769
+ if (mysqlDialect && char === "#") {
16770
+ while (i < sql.length && sql[i] !== `
16771
+ `)
16772
+ i++;
16773
+ if (i < sql.length) {
16774
+ result += `
16758
16775
  `;
16759
16776
  i++;
16760
16777
  }
16761
16778
  continue;
16762
16779
  }
16763
16780
  if (char === "/" && sql[i + 1] === "*") {
16781
+ const executableMysqlComment = mysqlDialect && (sql.startsWith("/*!", i) || sql.startsWith("/*M!", i));
16782
+ if (executableMysqlComment) {
16783
+ const prefixLength = sql.startsWith("/*M!", i) ? 4 : 3;
16784
+ const closingIndex = sql.indexOf("*/", i + prefixLength);
16785
+ const bodyEnd = closingIndex === -1 ? sql.length : closingIndex;
16786
+ const executableBody = sql.slice(i + prefixLength, bodyEnd).replace(/^\d+/, " ");
16787
+ result += " " + stripCommentsAndStrings(executableBody, options) + " ";
16788
+ i = closingIndex === -1 ? sql.length : closingIndex + 2;
16789
+ continue;
16790
+ }
16764
16791
  i += 2;
16765
16792
  while (i < sql.length) {
16766
16793
  if (sql[i] === "*" && sql[i + 1] === "/") {
@@ -16772,16 +16799,51 @@ function stripCommentsAndStrings(sql) {
16772
16799
  result += " ";
16773
16800
  continue;
16774
16801
  }
16775
- if (char === "'" || char === '"') {
16776
- const quote = char;
16802
+ if (options.dialect === "postgresql" && char === "$") {
16803
+ const delimiter = sql.slice(i).match(/^\$(?:[A-Za-z_][A-Za-z0-9_]*)?\$/)?.[0];
16804
+ if (delimiter) {
16805
+ i += delimiter.length;
16806
+ const closingIndex = sql.indexOf(delimiter, i);
16807
+ i = closingIndex === -1 ? sql.length : closingIndex + delimiter.length;
16808
+ result += " ";
16809
+ continue;
16810
+ }
16811
+ }
16812
+ if (mysqlDialect && char === "`") {
16777
16813
  i++;
16778
- while (i < sql.length && sql[i] !== quote) {
16779
- if (sql[i] === "\\") {
16814
+ while (i < sql.length) {
16815
+ if (sql[i] === "`") {
16816
+ if (sql[i + 1] === "`") {
16817
+ i += 2;
16818
+ continue;
16819
+ }
16780
16820
  i++;
16821
+ break;
16781
16822
  }
16782
16823
  i++;
16783
16824
  }
16784
- if (i < sql.length) {
16825
+ result += " ";
16826
+ continue;
16827
+ }
16828
+ if (char === "'" || char === '"') {
16829
+ const quote = char;
16830
+ const quoteIndex = i;
16831
+ const postgresEscapeString = options.dialect === "postgresql" && quote === "'" && /[eE]/.test(sql[quoteIndex - 1] ?? "") && !/[A-Za-z0-9_$]/.test(sql[quoteIndex - 2] ?? "");
16832
+ const backslashEscapes = options.dialect === undefined || postgresEscapeString;
16833
+ i++;
16834
+ while (i < sql.length) {
16835
+ if (sql[i] === quote) {
16836
+ if (sql[i + 1] === quote) {
16837
+ i += 2;
16838
+ continue;
16839
+ }
16840
+ i++;
16841
+ break;
16842
+ }
16843
+ if (backslashEscapes && sql[i] === "\\") {
16844
+ i += 2;
16845
+ continue;
16846
+ }
16785
16847
  i++;
16786
16848
  }
16787
16849
  result += " ";
@@ -17144,6 +17206,14 @@ function getOperationTarget(system, command, options, sql) {
17144
17206
  // src/core/limits.ts
17145
17207
  var DEFAULT_QUERY_ONLY_LIMIT = 1000;
17146
17208
 
17209
+ // src/agent-core/applied-limit.ts
17210
+ function trimAppliedLimit(rows, limit) {
17211
+ const truncated = rows.length > limit;
17212
+ return {
17213
+ rows: truncated ? rows.slice(0, limit) : [...rows],
17214
+ metadata: { truncated, limitApplied: limit }
17215
+ };
17216
+ }
17147
17217
  // src/core/audit/logger.ts
17148
17218
  import { appendFile, mkdir as mkdir2, readFile, stat } from "fs/promises";
17149
17219
  import { join } from "path";
@@ -21634,18 +21704,16 @@ var DbcliConfigV2Schema = exports_external.object({
21634
21704
  path: ["default"]
21635
21705
  });
21636
21706
 
21637
- // src/utils/errors.ts
21707
+ // src/agent-core/errors.ts
21638
21708
  class ConfigError extends Error {
21639
21709
  constructor(message) {
21640
21710
  super(message);
21641
21711
  this.name = "ConfigError";
21642
- if (Error.captureStackTrace) {
21712
+ if (Error.captureStackTrace)
21643
21713
  Error.captureStackTrace(this, ConfigError);
21644
- }
21645
21714
  }
21646
21715
  }
21647
-
21648
- // src/core/env-loader.ts
21716
+ // src/agent-core/env-loader.ts
21649
21717
  function parseEnvContent(content) {
21650
21718
  const entries = [];
21651
21719
  for (const line of content.split(`
@@ -21667,19 +21735,13 @@ function parseEnvContent(content) {
21667
21735
  }
21668
21736
  async function loadEnvFile(filePath) {
21669
21737
  const file = Bun.file(filePath);
21670
- const exists = await file.exists();
21671
- if (!exists) {
21738
+ if (!await file.exists())
21672
21739
  throw new ConfigError(`\u627E\u4E0D\u5230 env \u6A94\u6848\uFF1A${filePath}`);
21673
- }
21674
- const content = await file.text();
21675
- const entries = parseEnvContent(content);
21676
- for (const [key, value] of entries) {
21677
- if (process.env[key] === undefined) {
21740
+ for (const [key, value] of parseEnvContent(await file.text())) {
21741
+ if (process.env[key] === undefined)
21678
21742
  process.env[key] = value;
21679
- }
21680
21743
  }
21681
21744
  }
21682
-
21683
21745
  // src/core/config-v2.ts
21684
21746
  import { join as join4 } from "path";
21685
21747
  import { mkdir as mkdir5, rename as rename3 } from "fs/promises";
@@ -21749,7 +21811,25 @@ function listConnections(config) {
21749
21811
  // src/core/config.ts
21750
21812
  import { join as join9 } from "path";
21751
21813
  import { mkdir as mkdir6 } from "fs/promises";
21814
+ // src/agent-core/env-ref.ts
21815
+ function resolveEnvRef(value, fieldName, env = process.env) {
21816
+ if (typeof value === "string")
21817
+ return value;
21818
+ const envKey = value.$env;
21819
+ const resolved = env[envKey];
21820
+ if (resolved === undefined) {
21821
+ throw new ConfigError(`Environment variable not defined: ${envKey} (field: ${fieldName})
21822
+ ` + `Please set ${envKey} in an env file or your environment.`);
21823
+ }
21824
+ return resolved;
21825
+ }
21826
+ // src/core/config.ts
21752
21827
  var _globalConnectionName;
21828
+ function assertNoConnectionSelectorOnV1(connectionName) {
21829
+ if (connectionName === undefined)
21830
+ return;
21831
+ 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`);
21832
+ }
21753
21833
  function getGlobalConnectionName() {
21754
21834
  return _globalConnectionName;
21755
21835
  }
@@ -21776,18 +21856,10 @@ var DEFAULT_CONFIG = {
21776
21856
  function isEnvReference(value) {
21777
21857
  return typeof value === "object" && value !== null && "$env" in value && typeof value.$env === "string";
21778
21858
  }
21779
- function resolveEnvReferences(config, env, parentKey, strict = false) {
21859
+ function resolveEnvReferences(config, env, parentKey) {
21780
21860
  if (isEnvReference(config)) {
21781
21861
  const envKey = config.$env;
21782
- const value = env[envKey];
21783
- if (!value) {
21784
- if (!strict) {
21785
- return config;
21786
- }
21787
- throw new ConfigError(`Environment variable not defined: ${envKey}
21788
- ` + `Please set ${envKey} in .env or your environment.
21789
- ` + `Hint: check your .env file or run 'export ${envKey}=<value>'`);
21790
- }
21862
+ const value = resolveEnvRef(config, parentKey ?? "<root>", env);
21791
21863
  if (parentKey === "port") {
21792
21864
  const portNum = parseInt(value, 10);
21793
21865
  if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
@@ -21798,12 +21870,12 @@ function resolveEnvReferences(config, env, parentKey, strict = false) {
21798
21870
  return value;
21799
21871
  }
21800
21872
  if (Array.isArray(config)) {
21801
- return config.map((item) => resolveEnvReferences(item, env, parentKey, strict));
21873
+ return config.map((item) => resolveEnvReferences(item, env, parentKey));
21802
21874
  }
21803
21875
  if (typeof config === "object" && config !== null) {
21804
21876
  const resolved = {};
21805
21877
  for (const [key, value] of Object.entries(config)) {
21806
- resolved[key] = resolveEnvReferences(value, env, key, strict);
21878
+ resolved[key] = resolveEnvReferences(value, env, key);
21807
21879
  }
21808
21880
  return resolved;
21809
21881
  }
@@ -21853,7 +21925,7 @@ var configModule = {
21853
21925
  process.env.DBCLI_PASSWORD = legacyPassword;
21854
21926
  }
21855
21927
  }
21856
- const resolvedConnection = resolveEnvReferences(resolved.connection, process.env, undefined, false);
21928
+ const resolvedConnection = resolveEnvReferences(resolved.connection, process.env);
21857
21929
  if (!resolvedConnection.password && legacyPassword) {
21858
21930
  resolvedConnection.password = legacyPassword;
21859
21931
  }
@@ -21890,7 +21962,8 @@ var configModule = {
21890
21962
  redis: v2Config.redis
21891
21963
  });
21892
21964
  }
21893
- const resolvedConfig = resolveEnvReferences(config, process.env, undefined, false);
21965
+ assertNoConnectionSelectorOnV1(effectiveConnectionName);
21966
+ const resolvedConfig = resolveEnvReferences(config, process.env);
21894
21967
  const envPath = join9(storagePath, ".env.local");
21895
21968
  const envFile = Bun.file(envPath);
21896
21969
  if (await envFile.exists()) {
@@ -21906,9 +21979,10 @@ var configModule = {
21906
21979
  const file = Bun.file(path);
21907
21980
  const exists = await file.exists();
21908
21981
  if (exists) {
21982
+ assertNoConnectionSelectorOnV1(effectiveConnectionName);
21909
21983
  const content = await file.text();
21910
21984
  const raw = JSON.parse(content);
21911
- const resolved = resolveEnvReferences(raw, process.env, undefined, false);
21985
+ const resolved = resolveEnvReferences(raw, process.env);
21912
21986
  return DbcliConfigSchema.parse(resolved);
21913
21987
  }
21914
21988
  return { ...DEFAULT_CONFIG };
@@ -22016,7 +22090,9 @@ var REDACTED_VALUE_FLAGS = new Set([
22016
22090
  "--password",
22017
22091
  "--token",
22018
22092
  "--secret",
22019
- "--bulk"
22093
+ "--bulk",
22094
+ "--query-file",
22095
+ "-f"
22020
22096
  ]);
22021
22097
  var KEEP_VALUE_FLAGS = new Set([
22022
22098
  "--format",
@@ -22025,17 +22101,33 @@ var KEEP_VALUE_FLAGS = new Set([
22025
22101
  "--output",
22026
22102
  "--limit",
22027
22103
  "--collection",
22028
- "--index"
22104
+ "--index",
22105
+ "--fields",
22106
+ "--truncate"
22029
22107
  ]);
22030
22108
  var LINT_BOOLEAN_FLAGS = new Set(["--no-schema", "--recovery"]);
22109
+ var QUERY_BOOLEAN_FLAGS = new Set(["--ui", "--no-limit", "--no-truncate", "--recovery"]);
22031
22110
  function optionParts(token) {
22111
+ if (token.startsWith("-f") && !token.startsWith("--") && token.length > 2) {
22112
+ return { name: "-f", inlineValue: token.slice(2) };
22113
+ }
22032
22114
  const equals = token.indexOf("=");
22033
22115
  return equals === -1 ? { name: token, inlineValue: undefined } : { name: token.slice(0, equals), inlineValue: token.slice(equals + 1) };
22034
22116
  }
22117
+ function isOptionToken(token) {
22118
+ return token.startsWith("--") || token.startsWith("-f");
22119
+ }
22120
+ function isKnownBooleanFlag(command, name) {
22121
+ if (command === "lint")
22122
+ return LINT_BOOLEAN_FLAGS.has(name);
22123
+ if (command === "query")
22124
+ return QUERY_BOOLEAN_FLAGS.has(name);
22125
+ return false;
22126
+ }
22035
22127
  function findSensitiveSubcommand(argv) {
22036
22128
  for (let index = 1;index < argv.length; index++) {
22037
22129
  const token = argv[index];
22038
- if (token.startsWith("--")) {
22130
+ if (isOptionToken(token)) {
22039
22131
  const { name, inlineValue } = optionParts(token);
22040
22132
  if (inlineValue === undefined && (REDACTED_VALUE_FLAGS.has(name) || KEEP_VALUE_FLAGS.has(name))) {
22041
22133
  index++;
@@ -22058,7 +22150,7 @@ function sensitiveArgvValues(argv) {
22058
22150
  afterEndOfOptions = true;
22059
22151
  continue;
22060
22152
  }
22061
- if (!afterEndOfOptions && token.startsWith("--")) {
22153
+ if (!afterEndOfOptions && isOptionToken(token)) {
22062
22154
  const { name, inlineValue } = optionParts(token);
22063
22155
  if (REDACTED_VALUE_FLAGS.has(name)) {
22064
22156
  const value = inlineValue ?? argv[index + 1];
@@ -22077,7 +22169,7 @@ function sensitiveArgvValues(argv) {
22077
22169
  index++;
22078
22170
  } else if (inlineValue === undefined && KEEP_VALUE_FLAGS.has(name)) {
22079
22171
  index++;
22080
- } else if (sensitiveCommand?.command === "lint" && LINT_BOOLEAN_FLAGS.has(name)) {
22172
+ } else if (isKnownBooleanFlag(sensitiveCommand?.command, name)) {
22081
22173
  continue;
22082
22174
  } else if (!sensitiveCommand || index < sensitiveCommand.index) {
22083
22175
  continue;
@@ -22108,7 +22200,7 @@ function redactArgv(argv) {
22108
22200
  afterEndOfOptions = true;
22109
22201
  continue;
22110
22202
  }
22111
- if (!afterEndOfOptions && tok.startsWith("--")) {
22203
+ if (!afterEndOfOptions && isOptionToken(tok)) {
22112
22204
  const { name, inlineValue } = optionParts(tok);
22113
22205
  if (REDACTED_VALUE_FLAGS.has(name)) {
22114
22206
  out.push(`${name} <redacted>`);
@@ -22127,7 +22219,7 @@ function redactArgv(argv) {
22127
22219
  }
22128
22220
  continue;
22129
22221
  }
22130
- if (sensitiveCommand?.command === "lint" && LINT_BOOLEAN_FLAGS.has(name)) {
22222
+ if (isKnownBooleanFlag(sensitiveCommand?.command, name)) {
22131
22223
  out.push(tok);
22132
22224
  continue;
22133
22225
  }
@@ -22163,9 +22255,9 @@ function redactSql(sql) {
22163
22255
  // src/core/audit/integration-helper.ts
22164
22256
  var _sessionIdService = null;
22165
22257
  var _loggers = new Map;
22166
- async function getAuditLogger(config, configPath) {
22258
+ async function getAuditLogger(config, configPath, connectionName) {
22167
22259
  const storagePath = await resolveConfigStoragePath(configPath);
22168
- const connName = config.effectiveConnectionName || getGlobalConnectionName() || "default";
22260
+ const connName = connectionName || config.effectiveConnectionName || getGlobalConnectionName() || "default";
22169
22261
  const key = `${storagePath}:${connName}`;
22170
22262
  if (!_loggers.has(key)) {
22171
22263
  if (!_sessionIdService) {
@@ -22186,7 +22278,7 @@ async function getAuditLogger(config, configPath) {
22186
22278
  }
22187
22279
  async function writeAuditEntry(config, commandName, options, outcome) {
22188
22280
  try {
22189
- const logger = await getAuditLogger(config, options.config || ".dbcli");
22281
+ const logger = await getAuditLogger(config, options.config || ".dbcli", typeof options.connectionName === "string" ? options.connectionName : undefined);
22190
22282
  const engine = config.connection?.system || "postgresql";
22191
22283
  const target = outcome.target || getOperationTarget(engine, commandName, options, outcome.sql);
22192
22284
  let tier = getEngineCapability(engine, commandName).tier;
@@ -22218,6 +22310,129 @@ async function writeAuditEntry(config, commandName, options, outcome) {
22218
22310
  }
22219
22311
  }
22220
22312
 
22313
+ // src/core/field-projection.ts
22314
+ var UNSAFE_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]);
22315
+ function projectRows(rows, selection) {
22316
+ if (selection.mode === "include") {
22317
+ return {
22318
+ rows: rows.map((row) => {
22319
+ const projected = {};
22320
+ for (const path of selection.paths) {
22321
+ const result = readPath(row, path.split("."));
22322
+ defineData(projected, path, result.found ? result.value ?? null : null);
22323
+ }
22324
+ return projected;
22325
+ }),
22326
+ columnNames: [...selection.paths]
22327
+ };
22328
+ }
22329
+ const projectedRows = omitFieldPaths(rows, selection.paths);
22330
+ const columnNames = collectColumnNames(projectedRows);
22331
+ return { rows: normalizeRows(projectedRows, columnNames), columnNames };
22332
+ }
22333
+ function hasFieldPath(row, path) {
22334
+ return readPath(row, path.split(".")).found;
22335
+ }
22336
+ function omitFieldPaths(rows, paths) {
22337
+ return rows.map((row) => {
22338
+ let projected = cloneRecord(row);
22339
+ for (const path of paths)
22340
+ projected = omitPath(projected, path.split("."));
22341
+ return projected;
22342
+ });
22343
+ }
22344
+ function readPath(value, segments) {
22345
+ if (segments.length === 0)
22346
+ return { found: true, value };
22347
+ if (Array.isArray(value)) {
22348
+ const results = value.map((item) => readPath(item, segments));
22349
+ return {
22350
+ found: results.some((result) => result.found),
22351
+ value: results.map((result) => result.found ? result.value : undefined)
22352
+ };
22353
+ }
22354
+ if (!isRecord(value))
22355
+ return { found: false };
22356
+ const exactPath = segments.join(".");
22357
+ if (Object.prototype.hasOwnProperty.call(value, exactPath)) {
22358
+ return { found: true, value: value[exactPath] };
22359
+ }
22360
+ const [head, ...tail] = segments;
22361
+ if (!Object.prototype.hasOwnProperty.call(value, head))
22362
+ return { found: false };
22363
+ return readPath(value[head], tail);
22364
+ }
22365
+ function omitPath(value, segments) {
22366
+ if (segments.length === 0)
22367
+ return value;
22368
+ if (Array.isArray(value))
22369
+ return value.map((item) => omitPath(item, segments));
22370
+ if (!isPlainRecord(value))
22371
+ return value;
22372
+ const exactPath = segments.join(".");
22373
+ const [head, ...tail] = segments;
22374
+ const out = {};
22375
+ for (const [key, child] of Object.entries(value)) {
22376
+ if (key === exactPath)
22377
+ continue;
22378
+ if (key === head) {
22379
+ if (tail.length > 0)
22380
+ defineData(out, key, omitPath(child, tail));
22381
+ continue;
22382
+ }
22383
+ defineData(out, key, child);
22384
+ }
22385
+ return out;
22386
+ }
22387
+ function cloneRecord(value, omittedKey) {
22388
+ const out = {};
22389
+ for (const [key, child] of Object.entries(value)) {
22390
+ if (key !== omittedKey)
22391
+ defineData(out, key, child);
22392
+ }
22393
+ return out;
22394
+ }
22395
+ function collectColumnNames(rows) {
22396
+ const names = [];
22397
+ const seen = new Set;
22398
+ for (const row of rows) {
22399
+ for (const name of Object.keys(row)) {
22400
+ if (!seen.has(name)) {
22401
+ seen.add(name);
22402
+ names.push(name);
22403
+ }
22404
+ }
22405
+ }
22406
+ return names;
22407
+ }
22408
+ function normalizeRows(rows, columnNames) {
22409
+ return rows.map((row) => {
22410
+ const normalized = {};
22411
+ for (const column of columnNames) {
22412
+ const value = Object.prototype.hasOwnProperty.call(row, column) ? row[column] : null;
22413
+ defineData(normalized, column, value ?? null);
22414
+ }
22415
+ return normalized;
22416
+ });
22417
+ }
22418
+ function isRecord(value) {
22419
+ return value !== null && typeof value === "object" && !Array.isArray(value);
22420
+ }
22421
+ function isPlainRecord(value) {
22422
+ if (!isRecord(value))
22423
+ return false;
22424
+ const prototype = Object.getPrototypeOf(value);
22425
+ return prototype === Object.prototype || prototype === null;
22426
+ }
22427
+ function defineData(target, key, value) {
22428
+ Object.defineProperty(target, key, {
22429
+ value,
22430
+ enumerable: true,
22431
+ configurable: true,
22432
+ writable: true
22433
+ });
22434
+ }
22435
+
22221
22436
  // src/core/query-executor.ts
22222
22437
  class QueryExecutor {
22223
22438
  adapter;
@@ -22225,6 +22440,7 @@ class QueryExecutor {
22225
22440
  blacklistValidator;
22226
22441
  config;
22227
22442
  options;
22443
+ pendingDiagnostics = [];
22228
22444
  constructor(adapter, permission, blacklistValidator, config, options = {}) {
22229
22445
  this.adapter = adapter;
22230
22446
  this.permission = permission;
@@ -22232,19 +22448,31 @@ class QueryExecutor {
22232
22448
  this.config = config;
22233
22449
  this.options = options;
22234
22450
  }
22451
+ takeDiagnostics() {
22452
+ const diagnostics = this.pendingDiagnostics;
22453
+ this.pendingDiagnostics = [];
22454
+ return diagnostics;
22455
+ }
22235
22456
  async execute(sql, options) {
22236
22457
  const start = performance.now();
22458
+ this.pendingDiagnostics = [];
22237
22459
  try {
22238
22460
  const classification = enforcePermission(sql, this.permission);
22239
- if (classification.isDangerous && this.permission === "admin") {
22240
- console.error(`\u26A0 Warning: executing ${classification.type} operation (admin mode)`);
22241
- }
22461
+ const dangerousOperationWarning = classification.isDangerous && this.permission === "admin" ? `\u26A0 Warning: executing ${classification.type} operation (admin mode)` : undefined;
22242
22462
  const AUTO_LIMIT_TYPES = new Set(["SELECT"]);
22243
22463
  let executeSql = sql;
22244
- if (this.permission === "query-only" && AUTO_LIMIT_TYPES.has(classification.type) && !executeSql.match(/LIMIT\s+\d+/i) && options?.autoLimit !== false) {
22245
- const limitValue = options?.limitValue || DEFAULT_QUERY_ONLY_LIMIT;
22246
- executeSql = `${executeSql} LIMIT ${limitValue}`;
22247
- console.error(`Query-only mode: auto-limiting to ${limitValue} rows`);
22464
+ let appliedLimit;
22465
+ let autoLimitWarning;
22466
+ if (AUTO_LIMIT_TYPES.has(classification.type) && !hasUserAuthoredLimit(executeSql) && options?.autoLimit !== false) {
22467
+ const requestedLimit = options?.limitValue ?? (this.permission === "query-only" ? DEFAULT_QUERY_ONLY_LIMIT : undefined);
22468
+ if (requestedLimit !== undefined) {
22469
+ const fetchLimit = requestedLimit + (options?.detectTruncation === true ? 1 : 0);
22470
+ appliedLimit = options?.detectTruncation === true ? requestedLimit : undefined;
22471
+ executeSql = `${executeSql.replace(/;\s*$/, "")} LIMIT ${fetchLimit}`;
22472
+ if (options?.limitValue === undefined && this.permission === "query-only") {
22473
+ autoLimitWarning = `Query-only mode: auto-limiting to ${requestedLimit} rows`;
22474
+ }
22475
+ }
22248
22476
  }
22249
22477
  if (this.blacklistValidator) {
22250
22478
  const tableName = extractTableName(sql);
@@ -22254,26 +22482,39 @@ class QueryExecutor {
22254
22482
  }
22255
22483
  const resultData = await this.adapter.execute(executeSql);
22256
22484
  const executionTimeMs = Math.round(performance.now() - start);
22257
- const rows = resultData.rows;
22485
+ const limitedResult = appliedLimit === undefined ? undefined : trimAppliedLimit(resultData.rows, appliedLimit);
22486
+ const rows = limitedResult?.rows ?? resultData.rows;
22258
22487
  const affectedRows = resultData.affectedRows;
22488
+ const visibleAffectedRows = limitedResult ? rows.length : affectedRows;
22259
22489
  let columnNames = rows.length > 0 && rows[0] ? Object.keys(rows[0]) : [];
22260
- const columnTypes = columnNames.map((col) => {
22261
- const value = rows[0]?.[col];
22262
- return inferColumnType(value);
22263
- });
22264
22490
  let filteredRows = rows;
22265
22491
  let securityNotification;
22492
+ let omittedColumns = [];
22266
22493
  if (this.blacklistValidator) {
22267
22494
  const tableName = extractTableName(sql);
22268
22495
  if (tableName) {
22269
22496
  const filterResult = this.blacklistValidator.filterColumns(tableName, rows, columnNames);
22270
22497
  filteredRows = filterResult.filteredRows;
22271
22498
  if (filterResult.omittedColumns.length > 0) {
22499
+ omittedColumns = filterResult.omittedColumns;
22272
22500
  columnNames = columnNames.filter((col) => !filterResult.omittedColumns.includes(col));
22273
22501
  securityNotification = this.blacklistValidator.buildSecurityNotification(tableName, filterResult.omittedColumns);
22274
22502
  }
22275
22503
  }
22276
22504
  }
22505
+ if (options?.fieldSelection) {
22506
+ const fieldSelection = options.fieldSelection.mode === "include" ? {
22507
+ mode: "include",
22508
+ paths: options.fieldSelection.paths.filter((path) => !omittedColumns.some((omitted) => path === omitted || path.startsWith(`${omitted}.`)))
22509
+ } : options.fieldSelection;
22510
+ const projection = projectRows(filteredRows, fieldSelection);
22511
+ filteredRows = projection.rows;
22512
+ columnNames = projection.columnNames;
22513
+ }
22514
+ const columnTypes = columnNames.map((column) => {
22515
+ const value = filteredRows.find((row) => row[column] !== undefined)?.[column];
22516
+ return inferColumnType(value);
22517
+ });
22277
22518
  const result = {
22278
22519
  rows: filteredRows,
22279
22520
  rowCount: filteredRows.length,
@@ -22282,20 +22523,30 @@ class QueryExecutor {
22282
22523
  executionTimeMs,
22283
22524
  metadata: {
22284
22525
  statement: classification.type,
22285
- affectedRows,
22526
+ affectedRows: visibleAffectedRows,
22286
22527
  ...securityNotification ? { securityNotification } : {}
22287
- }
22528
+ },
22529
+ ...limitedResult ? { appliedLimit: limitedResult.metadata } : {}
22288
22530
  };
22289
22531
  if (this.config) {
22290
22532
  await writeAuditEntry(this.config, "query", this.options, {
22291
22533
  success: true,
22292
22534
  sql,
22293
22535
  metadata: {
22294
- rows_affected: affectedRows,
22536
+ rows_affected: visibleAffectedRows,
22295
22537
  execution_ms: executionTimeMs
22296
22538
  }
22297
22539
  });
22298
22540
  }
22541
+ if (this.options.recovery !== true) {
22542
+ const diagnostics = [dangerousOperationWarning, autoLimitWarning].filter((diagnostic) => diagnostic !== undefined);
22543
+ if (this.options.deferDiagnostics === true) {
22544
+ this.pendingDiagnostics = diagnostics;
22545
+ } else {
22546
+ for (const diagnostic of diagnostics)
22547
+ console.error(diagnostic);
22548
+ }
22549
+ }
22299
22550
  return result;
22300
22551
  } catch (error) {
22301
22552
  if (this.config) {
@@ -22329,6 +22580,10 @@ class QueryExecutor {
22329
22580
  }
22330
22581
  }
22331
22582
  }
22583
+ function hasUserAuthoredLimit(sql) {
22584
+ const executableSql = stripCommentsAndStrings(sql).replace(/`(?:``|[^`])*`/g, " ");
22585
+ return /\bLIMIT\s+(?:\(\s*)?(?:\d+|ALL\b|\?|\$\d+|:[A-Za-z_][A-Za-z0-9_]*)/i.test(executableSql);
22586
+ }
22332
22587
  function inferColumnType(value) {
22333
22588
  if (value === null || value === undefined) {
22334
22589
  return "null";
@@ -22964,19 +23219,11 @@ class BlacklistValidator {
22964
23219
  if (blacklistedColumns.length === 0) {
22965
23220
  return { filteredRows: rows, omittedColumns: [] };
22966
23221
  }
22967
- const omittedColumns = columnList.filter((col) => blacklistedColumns.includes(col));
23222
+ const omittedColumns = blacklistedColumns.filter((path3) => columnList.includes(path3) || rows.some((row) => hasFieldPath(row, path3)));
22968
23223
  if (omittedColumns.length === 0) {
22969
23224
  return { filteredRows: rows, omittedColumns: [] };
22970
23225
  }
22971
- const filteredRows = rows.map((row) => {
22972
- const newRow = {};
22973
- for (const [key, value] of Object.entries(row)) {
22974
- if (!omittedColumns.includes(key)) {
22975
- newRow[key] = value;
22976
- }
22977
- }
22978
- return newRow;
22979
- });
23226
+ const filteredRows = omitFieldPaths(rows, omittedColumns);
22980
23227
  return { filteredRows, omittedColumns };
22981
23228
  }
22982
23229
  buildSecurityNotification(_tableName, omittedColumns) {