@carllee1983/dbcli 1.47.1 → 1.49.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.47.1",
55
+ version: "1.49.0",
56
56
  description: "Database CLI for AI agents",
57
57
  type: "module",
58
58
  publishConfig: {
@@ -99,6 +99,7 @@ var init_package = __esm(() => {
99
99
  },
100
100
  files: [
101
101
  "dist/",
102
+ "!dist/.build-stamp",
102
103
  "assets/",
103
104
  "plugins/",
104
105
  "skills/",
@@ -120,7 +121,7 @@ var init_package = __esm(() => {
120
121
  "release:check": "bash scripts/release-check.sh",
121
122
  "plugin:sync": "bun run scripts/sync-plugin-assets.ts --write",
122
123
  "plugin:check": "bun run scripts/sync-plugin-assets.ts",
123
- test: "bun test",
124
+ test: "bun test --timeout 30000",
124
125
  "test:unit": "bun test tests/unit tests/core",
125
126
  "test:integration": "bun test tests/integration",
126
127
  "test:docker": "docker compose -f docker-compose.test.yml up -d --wait && bun test tests/integration/adapters; docker compose -f docker-compose.test.yml down",
@@ -132,8 +133,9 @@ var init_package = __esm(() => {
132
133
  typecheck: "tsc --noEmit --pretty false",
133
134
  "test:perf": "bun test ./tests/perf/*.bench.ts",
134
135
  lint: "eslint src tests scripts --ext .ts --max-warnings=0",
135
- "lint:fix": "eslint src tests scripts --ext .ts --fix --max-warnings=0",
136
- format: 'prettier --write "src/**/*.ts" "tests/**/*.ts"'
136
+ "format:check": "prettier --check .",
137
+ format: "prettier --write .",
138
+ "lint:fix": "eslint src tests scripts --ext .ts --fix --max-warnings=0"
137
139
  },
138
140
  dependencies: {
139
141
  "cli-table3": "^0.6.5",
@@ -9316,6 +9318,14 @@ class BlacklistManager {
9316
9318
  }
9317
9319
  return Array.from(columnSet);
9318
9320
  }
9321
+ getAllBlacklistedColumns() {
9322
+ const all = new Set;
9323
+ for (const columnSet of this.state.columns.values()) {
9324
+ for (const column of columnSet)
9325
+ all.add(column);
9326
+ }
9327
+ return Array.from(all);
9328
+ }
9319
9329
  canOverrideBlacklist() {
9320
9330
  return this.overrideEnabled;
9321
9331
  }
@@ -9780,6 +9790,35 @@ var init_redis = __esm(() => {
9780
9790
  };
9781
9791
  });
9782
9792
 
9793
+ // src/utils/sql-lexical.ts
9794
+ function dollarQuoteDelimiterAt(sql, index) {
9795
+ if (sql[index] !== "$")
9796
+ return;
9797
+ if (continuesIdentifier(sql, index))
9798
+ return;
9799
+ return sql.slice(index).match(DOLLAR_QUOTE_DELIMITER)?.[0];
9800
+ }
9801
+ function continuesIdentifier(sql, index) {
9802
+ let start = index;
9803
+ while (start > 0 && IDENTIFIER_CONTINUATION.test(sql[start - 1] ?? ""))
9804
+ start--;
9805
+ if (start === index)
9806
+ return false;
9807
+ const run = sql.slice(start, index);
9808
+ const first = run[0];
9809
+ if (IDENTIFIER_START.test(first))
9810
+ return true;
9811
+ if (first === "$")
9812
+ return false;
9813
+ return run.replace(/^[0-9]+(?:[eE][0-9]+)?/, "").length > 0;
9814
+ }
9815
+ var IDENTIFIER_CONTINUATION, IDENTIFIER_START, DOLLAR_QUOTE_DELIMITER;
9816
+ var init_sql_lexical = __esm(() => {
9817
+ IDENTIFIER_CONTINUATION = /[A-Za-z0-9_$]|[\u0080-\uFFFF]/;
9818
+ IDENTIFIER_START = /[A-Za-z_]|[\u0080-\uFFFF]/;
9819
+ DOLLAR_QUOTE_DELIMITER = /^\$(?:(?:[A-Za-z_]|[\u0080-\uFFFF])(?:[A-Za-z0-9_]|[\u0080-\uFFFF])*)?\$/;
9820
+ });
9821
+
9783
9822
  // src/core/permission-guard.ts
9784
9823
  var exports_permission_guard = {};
9785
9824
  __export(exports_permission_guard, {
@@ -9873,8 +9912,7 @@ function stripCommentsAndStrings2(sql, options = {}) {
9873
9912
  continue;
9874
9913
  }
9875
9914
  if (options.dialect === "postgresql" && char === "$") {
9876
- const opensToken = !IDENTIFIER_CONTINUATION.test(sql[i - 1] ?? "");
9877
- const delimiter = opensToken ? sql.slice(i).match(/^\$(?:[A-Za-z_][A-Za-z0-9_]*)?\$/)?.[0] : undefined;
9915
+ const delimiter = dollarQuoteDelimiterAt(sql, i);
9878
9916
  if (delimiter) {
9879
9917
  i += delimiter.length;
9880
9918
  const closingIndex = sql.indexOf(delimiter, i);
@@ -10348,8 +10386,9 @@ function checkElasticsearchPermission(classification, permission) {
10348
10386
  reason: `Elasticsearch ${classification.type} operation requires higher permission tier`
10349
10387
  };
10350
10388
  }
10351
- var PermissionError, IDENTIFIER_CONTINUATION, SQL_DIALECTS, SQL_WRITE_OR_DDL_KEYWORDS, SQL_LOCK_CLAUSE, ESCALATABLE_READ_TYPES, REDIS_COMMAND_PERMISSION, PERMISSION_RANK;
10389
+ var PermissionError, SQL_DIALECTS, SQL_WRITE_OR_DDL_KEYWORDS, SQL_LOCK_CLAUSE, ESCALATABLE_READ_TYPES, REDIS_COMMAND_PERMISSION, PERMISSION_RANK;
10352
10390
  var init_permission_guard = __esm(() => {
10391
+ init_sql_lexical();
10353
10392
  PermissionError = class PermissionError extends Error {
10354
10393
  classification;
10355
10394
  requiredPermission;
@@ -10361,7 +10400,6 @@ var init_permission_guard = __esm(() => {
10361
10400
  Object.setPrototypeOf(this, PermissionError.prototype);
10362
10401
  }
10363
10402
  };
10364
- IDENTIFIER_CONTINUATION = /[A-Za-z0-9_$]|[\u0080-\uFFFF]/;
10365
10403
  SQL_DIALECTS = ["postgresql", "mysql", "mariadb"];
10366
10404
  SQL_WRITE_OR_DDL_KEYWORDS = /(?<![.\w])(INSERT|UPDATE|DELETE|MERGE|UPSERT|REPLACE|TRUNCATE|DROP|ALTER|CREATE|GRANT|REVOKE|RENAME|INTO)\b(?!\s*\()/i;
10367
10405
  SQL_LOCK_CLAUSE = /\bFOR\s+(?:NO\s+KEY\s+)?UPDATE\b|\bFOR\s+(?:KEY\s+)?SHARE\b/gi;
@@ -26063,22 +26101,27 @@ var init_size_guard2 = __esm(() => {
26063
26101
  init_types4();
26064
26102
  });
26065
26103
 
26066
- // src/adapters/redis/blacklist-enforcer.ts
26104
+ // src/utils/glob.ts
26067
26105
  function globToRegex(glob) {
26068
26106
  let out = "^";
26069
26107
  for (let i = 0;i < glob.length; i++) {
26070
26108
  const c2 = glob[i];
26109
+ if (c2 === "\\" && i + 1 < glob.length) {
26110
+ out += glob[++i].replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
26111
+ continue;
26112
+ }
26071
26113
  if (c2 === "*")
26072
26114
  out += ".*";
26073
26115
  else if (c2 === "?")
26074
26116
  out += ".";
26075
26117
  else if (c2 === "[") {
26076
- const end = glob.indexOf("]", i);
26077
- if (end === -1)
26078
- out += "\\[";
26079
- else {
26080
- out += glob.slice(i, end + 1);
26118
+ const end = findClassEnd(glob, i);
26119
+ const body = end === -1 ? "" : glob.slice(i, end + 1);
26120
+ if (body !== "" && isValidCharacterClass(body)) {
26121
+ out += body;
26081
26122
  i = end;
26123
+ } else {
26124
+ out += "\\[";
26082
26125
  }
26083
26126
  } else if (".^$+(){}|\\".includes(c2)) {
26084
26127
  out += "\\" + c2;
@@ -26089,6 +26132,27 @@ function globToRegex(glob) {
26089
26132
  out += "$";
26090
26133
  return new RegExp(out);
26091
26134
  }
26135
+ function findClassEnd(glob, open2) {
26136
+ for (let i = open2 + 1;i < glob.length; i++) {
26137
+ if (glob[i] === "\\") {
26138
+ i++;
26139
+ continue;
26140
+ }
26141
+ if (glob[i] === "]")
26142
+ return i;
26143
+ }
26144
+ return -1;
26145
+ }
26146
+ function isValidCharacterClass(body) {
26147
+ try {
26148
+ new RegExp(body);
26149
+ return true;
26150
+ } catch {
26151
+ return false;
26152
+ }
26153
+ }
26154
+
26155
+ // src/adapters/redis/blacklist-enforcer.ts
26092
26156
  function patternsOverlap(a, b) {
26093
26157
  const ra = globToRegex(a);
26094
26158
  const rb = globToRegex(b);
@@ -30248,16 +30312,36 @@ function projectRows(rows, selection) {
30248
30312
  const columnNames = collectColumnNames(projectedRows);
30249
30313
  return { rows: normalizeRows(projectedRows, columnNames), columnNames };
30250
30314
  }
30251
- function hasFieldPath(row, path5) {
30252
- return readPath(row, path5.split(".")).found;
30315
+ function hasFieldPath(row, segments) {
30316
+ return readPath(row, segments).found;
30253
30317
  }
30254
30318
  function omitFieldPaths(rows, paths) {
30255
- return rows.map((row) => {
30256
- let projected = cloneRecord(row);
30257
- for (const path5 of paths)
30258
- projected = omitPath(projected, path5.split("."));
30319
+ const topLevel = new Set(paths);
30320
+ const dotted = paths.filter((path5) => path5.includes(".")).map((path5) => ({ head: path5.slice(0, path5.indexOf(".")), segments: path5.split(".") }));
30321
+ const maskRecord = (row) => {
30322
+ let projected = cloneRecord(row, topLevel);
30323
+ for (const { head, segments } of dotted) {
30324
+ const value = row[head];
30325
+ if (value !== null && typeof value === "object")
30326
+ projected = omitPath(projected, segments);
30327
+ }
30259
30328
  return projected;
30329
+ };
30330
+ return rows.map((row) => {
30331
+ if (row === null || typeof row !== "object")
30332
+ return {};
30333
+ if (Array.isArray(row))
30334
+ return maskArrayRow(row, maskRecord);
30335
+ return maskRecord(row);
30336
+ });
30337
+ }
30338
+ function maskArrayRow(row, maskRecord) {
30339
+ const out = {};
30340
+ row.forEach((item, index) => {
30341
+ const masked = Array.isArray(item) ? maskArrayRow(item, maskRecord) : item !== null && typeof item === "object" ? maskRecord(item) : item;
30342
+ defineData(out, String(index), masked);
30260
30343
  });
30344
+ return out;
30261
30345
  }
30262
30346
  function toMongoProjection(selection) {
30263
30347
  const projection = {};
@@ -30321,10 +30405,10 @@ function omitPath(value, segments) {
30321
30405
  }
30322
30406
  return out;
30323
30407
  }
30324
- function cloneRecord(value, omittedKey) {
30408
+ function cloneRecord(value, omittedKeys) {
30325
30409
  const out = {};
30326
30410
  for (const [key, child] of Object.entries(value)) {
30327
- if (key !== omittedKey)
30411
+ if (!omittedKeys.has(key))
30328
30412
  defineData(out, key, child);
30329
30413
  }
30330
30414
  return out;
@@ -30374,28 +30458,171 @@ var init_field_projection = __esm(() => {
30374
30458
  UNSAFE_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]);
30375
30459
  });
30376
30460
 
30461
+ // src/utils/es-index-target.ts
30462
+ function normalizeEsPath(path5) {
30463
+ let current = path5;
30464
+ for (let pass = 0;pass < MAX_DECODE_PASSES; pass++) {
30465
+ let decoded = current;
30466
+ try {
30467
+ decoded = decodeURIComponent(current);
30468
+ } catch {}
30469
+ const segments = [];
30470
+ for (const segment of decoded.split("/")) {
30471
+ if (segment === "." || segment === "")
30472
+ continue;
30473
+ if (segment === "..") {
30474
+ segments.pop();
30475
+ continue;
30476
+ }
30477
+ segments.push(segment);
30478
+ }
30479
+ const resolved = `/${segments.join("/")}`;
30480
+ if (resolved === current)
30481
+ return resolved;
30482
+ current = resolved;
30483
+ }
30484
+ return current;
30485
+ }
30486
+ function unwrap(target) {
30487
+ let current = target;
30488
+ for (let pass = 0;pass < MAX_DECODE_PASSES; pass++) {
30489
+ let next = current;
30490
+ try {
30491
+ next = decodeURIComponent(next);
30492
+ } catch {}
30493
+ if (next.startsWith("<") && next.endsWith(">")) {
30494
+ next = next.slice(1, -1).replace(/\{[^}]*\}/g, "*");
30495
+ }
30496
+ if (next === current)
30497
+ return current;
30498
+ current = next;
30499
+ }
30500
+ return current;
30501
+ }
30502
+ function expandIndexTargets(target) {
30503
+ const concrete = [];
30504
+ const wildcards = [];
30505
+ const add = (candidate) => {
30506
+ if (candidate.length === 0)
30507
+ return;
30508
+ if (/[*?]/.test(candidate) || candidate.toLowerCase() === "_all")
30509
+ wildcards.push(candidate);
30510
+ else
30511
+ concrete.push(candidate);
30512
+ };
30513
+ for (const rawPart of unwrap(target).split(",")) {
30514
+ const part = unwrap(rawPart.trim()).trim().replace(/^[-+]/, "");
30515
+ if (part.length === 0)
30516
+ continue;
30517
+ add(part);
30518
+ if (part.includes(":"))
30519
+ for (const section of part.split(":"))
30520
+ add(section);
30521
+ }
30522
+ return { concrete, wildcards };
30523
+ }
30524
+ function matchesIndexGlob(pattern, name2) {
30525
+ const normalized = pattern.toLowerCase() === "_all" ? "*" : pattern.toLowerCase();
30526
+ try {
30527
+ return globToRegex(normalized).test(name2.toLowerCase());
30528
+ } catch {
30529
+ return true;
30530
+ }
30531
+ }
30532
+ function reachesByConvention(name2, entry) {
30533
+ const lower = name2.toLowerCase();
30534
+ const target = entry.toLowerCase();
30535
+ return new RegExp(`^\\.ds-${escapeRegExp(target)}-`).test(lower) || new RegExp(`^${escapeRegExp(target)}-\\d+$`).test(lower);
30536
+ }
30537
+ function escapeRegExp(text2) {
30538
+ return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
30539
+ }
30540
+ function indexExpressionReaches(expression, blacklisted) {
30541
+ if (blacklisted.length === 0)
30542
+ return false;
30543
+ const { concrete, wildcards } = expandIndexTargets(expression);
30544
+ return concrete.some((name2) => blacklisted.some((entry) => entry.toLowerCase() === name2.toLowerCase() || reachesByConvention(name2, entry))) || wildcards.some((pattern) => blacklisted.some((entry) => matchesIndexGlob(pattern, entry)));
30545
+ }
30546
+ var MAX_DECODE_PASSES = 4;
30547
+ var init_es_index_target = () => {};
30548
+
30377
30549
  // src/core/blacklist-validator.ts
30550
+ function flattenArrayRow(row) {
30551
+ const records = [];
30552
+ for (const item of row) {
30553
+ if (Array.isArray(item))
30554
+ records.push(...flattenArrayRow(item));
30555
+ else if (item !== null && typeof item === "object")
30556
+ records.push(item);
30557
+ }
30558
+ return records;
30559
+ }
30560
+ function dedupe2(values) {
30561
+ const seen = new Set;
30562
+ const result = [];
30563
+ for (const value of values) {
30564
+ const key = value.toLowerCase();
30565
+ if (value.length === 0 || seen.has(key))
30566
+ continue;
30567
+ seen.add(key);
30568
+ result.push(value);
30569
+ }
30570
+ return result;
30571
+ }
30572
+
30378
30573
  class BlacklistValidator {
30379
30574
  manager;
30380
30575
  constructor(manager) {
30381
30576
  this.manager = manager;
30382
30577
  }
30383
- checkTableBlacklist(operation, tableName, _tableList = []) {
30578
+ checkTableBlacklist(operation, tableName, tableList = []) {
30579
+ this.checkTablesBlacklist(operation, [tableName, ...tableList]);
30580
+ }
30581
+ checkTablesBlacklist(operation, tableNames) {
30582
+ const tables = dedupe2(tableNames);
30583
+ if (tables.length === 0) {
30584
+ return;
30585
+ }
30384
30586
  if (this.manager.canOverrideBlacklist()) {
30385
- const message = t_vars("warnings.blacklist_override_used", {
30587
+ const message2 = t_vars("warnings.blacklist_override_used", {
30386
30588
  operation,
30387
- table: tableName
30589
+ table: tables.join(", ")
30388
30590
  });
30389
- console.error(message);
30591
+ console.error(message2);
30390
30592
  return;
30391
30593
  }
30392
- if (this.manager.isTableBlacklisted(tableName)) {
30393
- const message = t_vars("errors.table_blacklisted", {
30394
- table: tableName,
30395
- operation
30396
- });
30397
- throw new BlacklistError(message, tableName, operation);
30594
+ const blocked = tables.filter((table) => this.manager.isTableBlacklisted(table));
30595
+ if (blocked.length === 0) {
30596
+ return;
30398
30597
  }
30598
+ const message = t_vars("errors.table_blacklisted", {
30599
+ table: blocked.join(", "),
30600
+ operation
30601
+ });
30602
+ throw new BlacklistError(message, blocked[0], operation);
30603
+ }
30604
+ checkIndexBlacklist(operation, target) {
30605
+ const { concrete, wildcards } = expandIndexTargets(target);
30606
+ this.checkTablesBlacklist(operation, concrete);
30607
+ if (wildcards.length === 0 || this.manager.canOverrideBlacklist())
30608
+ return;
30609
+ const blacklisted = Array.from(this.manager.getState().tables);
30610
+ if (blacklisted.length === 0)
30611
+ return;
30612
+ const reachable = wildcards.filter((pattern) => blacklisted.some((entry) => matchesIndexGlob(pattern, entry)));
30613
+ if (reachable.length === 0)
30614
+ return;
30615
+ const message = t_vars("errors.table_blacklisted", {
30616
+ table: reachable.join(", "),
30617
+ operation
30618
+ });
30619
+ throw new BlacklistError(message, reachable[0], operation);
30620
+ }
30621
+ filterColumnsForIndexExpression(target, rows, columnList) {
30622
+ const { concrete, wildcards } = expandIndexTargets(target);
30623
+ const ruleKeys = Array.from(this.manager.getState().columns.keys());
30624
+ const reachable = ruleKeys.filter((key) => wildcards.some((pattern) => matchesIndexGlob(pattern, key)));
30625
+ return this.filterColumnsForTables([...concrete, ...reachable], rows, columnList);
30399
30626
  }
30400
30627
  checkColumnBlacklistOnWrite(tableName, fields, operation = "WRITE") {
30401
30628
  const blacklisted = this.manager.getBlacklistedColumns(tableName);
@@ -30422,11 +30649,64 @@ class BlacklistValidator {
30422
30649
  throw new BlacklistError(message, tableName, operation);
30423
30650
  }
30424
30651
  filterColumns(tableName, rows, columnList) {
30425
- const blacklistedColumns = this.manager.getBlacklistedColumns(tableName);
30652
+ return this.filterColumnsForTables([tableName], rows, columnList);
30653
+ }
30654
+ filterColumnsForTables(tableNames, rows, columnList) {
30655
+ const tables = dedupe2(tableNames);
30656
+ const blacklistedColumns = tables.length === 0 ? this.manager.getAllBlacklistedColumns() : Array.from(new Set(tables.flatMap((table) => this.manager.getBlacklistedColumns(table))));
30426
30657
  if (blacklistedColumns.length === 0) {
30427
30658
  return { filteredRows: rows, omittedColumns: [] };
30428
30659
  }
30429
- const omittedColumns = blacklistedColumns.filter((path5) => columnList.includes(path5) || rows.some((row) => hasFieldPath(row, path5)));
30660
+ const probeNested = blacklistedColumns.some((path5) => path5.includes("."));
30661
+ const presentColumns = new Set(columnList);
30662
+ const nestedHeads = new Set;
30663
+ const collect2 = (record) => {
30664
+ for (const key of Object.getOwnPropertyNames(record)) {
30665
+ presentColumns.add(key);
30666
+ if (!probeNested)
30667
+ continue;
30668
+ const value = record[key];
30669
+ if (value !== null && typeof value === "object")
30670
+ nestedHeads.add(key);
30671
+ }
30672
+ };
30673
+ for (const row of rows) {
30674
+ if (row === null || typeof row !== "object")
30675
+ continue;
30676
+ if (Array.isArray(row)) {
30677
+ for (const item of flattenArrayRow(row))
30678
+ collect2(item);
30679
+ continue;
30680
+ }
30681
+ collect2(row);
30682
+ }
30683
+ const protectedPaths = new Set(blacklistedColumns);
30684
+ const omitted = new Set;
30685
+ for (const path5 of blacklistedColumns) {
30686
+ if (presentColumns.has(path5)) {
30687
+ omitted.add(path5);
30688
+ continue;
30689
+ }
30690
+ const dot = path5.indexOf(".");
30691
+ if (dot < 0)
30692
+ continue;
30693
+ if (!nestedHeads.has(path5.slice(0, dot)))
30694
+ continue;
30695
+ const segments = path5.split(".");
30696
+ if (rows.some((row) => hasFieldPath(row, segments)))
30697
+ omitted.add(path5);
30698
+ }
30699
+ for (const column of presentColumns) {
30700
+ let dot = column.indexOf(".");
30701
+ while (dot >= 0) {
30702
+ if (dot > 0 && protectedPaths.has(column.slice(0, dot))) {
30703
+ omitted.add(column);
30704
+ break;
30705
+ }
30706
+ dot = column.indexOf(".", dot + 1);
30707
+ }
30708
+ }
30709
+ const omittedColumns = Array.from(omitted);
30430
30710
  if (omittedColumns.length === 0) {
30431
30711
  return { filteredRows: rows, omittedColumns: [] };
30432
30712
  }
@@ -30446,6 +30726,7 @@ var init_blacklist_validator = __esm(() => {
30446
30726
  init_blacklist();
30447
30727
  init_message_loader();
30448
30728
  init_field_projection();
30729
+ init_es_index_target();
30449
30730
  });
30450
30731
 
30451
30732
  // src/core/verification/types.ts
@@ -34433,6 +34714,355 @@ async function openInBrowser(target) {
34433
34714
  }
34434
34715
  var init_opener = () => {};
34435
34716
 
34717
+ // src/utils/sql-tables.ts
34718
+ var exports_sql_tables = {};
34719
+ __export(exports_sql_tables, {
34720
+ extractTableReferences: () => extractTableReferences
34721
+ });
34722
+ function tokenize(sql, dialect, backslashEscapes) {
34723
+ const tokens = [];
34724
+ const mysqlDialect = dialect === "mysql" || dialect === "mariadb";
34725
+ let i = 0;
34726
+ while (i < sql.length) {
34727
+ const char = sql[i];
34728
+ const dashFollowerCode = sql.charCodeAt(i + 2);
34729
+ if (char === "-" && sql[i + 1] === "-" && (!mysqlDialect || sql[i + 2] === undefined || dashFollowerCode <= 32 || dashFollowerCode === 127)) {
34730
+ while (i < sql.length && sql[i] !== `
34731
+ `)
34732
+ i++;
34733
+ continue;
34734
+ }
34735
+ if (mysqlDialect && char === "#") {
34736
+ while (i < sql.length && sql[i] !== `
34737
+ `)
34738
+ i++;
34739
+ continue;
34740
+ }
34741
+ if (char === "/" && sql[i + 1] === "*") {
34742
+ if (mysqlDialect && (sql.startsWith("/*!", i) || sql.startsWith("/*M!", i))) {
34743
+ const prefixLength = sql.startsWith("/*M!", i) ? 4 : 3;
34744
+ const closingIndex = sql.indexOf("*/", i + prefixLength);
34745
+ const bodyEnd = closingIndex === -1 ? sql.length : closingIndex;
34746
+ const body = sql.slice(i + prefixLength, bodyEnd).replace(/^\d+/, " ");
34747
+ tokens.push(...tokenize(body, dialect, backslashEscapes));
34748
+ i = closingIndex === -1 ? sql.length : closingIndex + 2;
34749
+ continue;
34750
+ }
34751
+ const nests = dialect === "postgresql";
34752
+ let depth = 1;
34753
+ i += 2;
34754
+ while (i < sql.length && depth > 0) {
34755
+ if (nests && sql[i] === "/" && sql[i + 1] === "*") {
34756
+ depth++;
34757
+ i += 2;
34758
+ continue;
34759
+ }
34760
+ if (sql[i] === "*" && sql[i + 1] === "/") {
34761
+ depth--;
34762
+ i += 2;
34763
+ continue;
34764
+ }
34765
+ i++;
34766
+ }
34767
+ continue;
34768
+ }
34769
+ if (dialect === "postgresql" && char === "$") {
34770
+ const delimiter = dollarQuoteDelimiterAt(sql, i);
34771
+ if (delimiter) {
34772
+ i += delimiter.length;
34773
+ const closingIndex = sql.indexOf(delimiter, i);
34774
+ i = closingIndex === -1 ? sql.length : closingIndex + delimiter.length;
34775
+ continue;
34776
+ }
34777
+ }
34778
+ if (char === "'") {
34779
+ i++;
34780
+ while (i < sql.length) {
34781
+ if (sql[i] === "'") {
34782
+ if (sql[i + 1] === "'") {
34783
+ i += 2;
34784
+ continue;
34785
+ }
34786
+ i++;
34787
+ break;
34788
+ }
34789
+ if (backslashEscapes && sql[i] === "\\") {
34790
+ i += 2;
34791
+ continue;
34792
+ }
34793
+ i++;
34794
+ }
34795
+ continue;
34796
+ }
34797
+ if (char === '"' || char === "`") {
34798
+ const quote = char;
34799
+ i++;
34800
+ let value = "";
34801
+ while (i < sql.length) {
34802
+ if (sql[i] === quote) {
34803
+ if (sql[i + 1] === quote) {
34804
+ value += quote;
34805
+ i += 2;
34806
+ continue;
34807
+ }
34808
+ i++;
34809
+ break;
34810
+ }
34811
+ if (backslashEscapes && sql[i] === "\\") {
34812
+ value += sql[i + 1] ?? "";
34813
+ i += 2;
34814
+ continue;
34815
+ }
34816
+ value += sql[i];
34817
+ i++;
34818
+ }
34819
+ tokens.push({ value, kind: "identifier", quoted: true });
34820
+ continue;
34821
+ }
34822
+ if (IDENTIFIER_START2.test(char)) {
34823
+ let value = "";
34824
+ while (i < sql.length && IDENTIFIER_PART.test(sql[i])) {
34825
+ value += sql[i];
34826
+ i++;
34827
+ }
34828
+ tokens.push({ value, kind: "identifier", quoted: false });
34829
+ continue;
34830
+ }
34831
+ if (char === "." || char === "," || char === "(" || char === ")" || char === ";") {
34832
+ tokens.push({ value: char, kind: "punctuation", quoted: false });
34833
+ i++;
34834
+ continue;
34835
+ }
34836
+ i++;
34837
+ }
34838
+ return tokens;
34839
+ }
34840
+ function isKeyword(token, keywords) {
34841
+ if (!token || token.kind !== "identifier" || token.quoted)
34842
+ return false;
34843
+ return keywords.has(token.value.toUpperCase());
34844
+ }
34845
+ function isPunctuation(token, value) {
34846
+ return token?.kind === "punctuation" && token.value === value;
34847
+ }
34848
+ function readQualifiedName(tokens, index) {
34849
+ const first = tokens[index];
34850
+ if (!first || first.kind !== "identifier")
34851
+ return null;
34852
+ const parts = [first.value];
34853
+ let cursor = index + 1;
34854
+ while (isPunctuation(tokens[cursor], ".") && tokens[cursor + 1]?.kind === "identifier") {
34855
+ parts.push(tokens[cursor + 1].value);
34856
+ cursor += 2;
34857
+ }
34858
+ return { parts, next: cursor };
34859
+ }
34860
+ function decodedVariants(value) {
34861
+ const decoded = value.replace(ANY_ESCAPE_SEQUENCE, (whole, _escape, long, short) => fromCodePointOrRaw(long ?? short, whole));
34862
+ return decoded === value ? [] : [decoded];
34863
+ }
34864
+ function fromCodePointOrRaw(hex, whole) {
34865
+ const codePoint = parseInt(hex, 16);
34866
+ return codePoint <= 1114111 ? String.fromCodePoint(codePoint) : whole;
34867
+ }
34868
+ function extractTableReferences(sql, options = {}) {
34869
+ const seen = new Set;
34870
+ const references = [];
34871
+ const record = (name2) => {
34872
+ const key = name2.toLowerCase();
34873
+ if (name2.length === 0 || seen.has(key))
34874
+ return;
34875
+ seen.add(key);
34876
+ references.push(name2);
34877
+ };
34878
+ const dialects = options.dialect ? [options.dialect] : ["postgresql", "mysql", undefined];
34879
+ for (const dialect of dialects) {
34880
+ for (const backslashEscapes of [false, true]) {
34881
+ collectReferences(tokenize(sql, dialect, backslashEscapes), record);
34882
+ }
34883
+ }
34884
+ return references;
34885
+ }
34886
+ function collectReferences(tokens, record) {
34887
+ const recordName = (parts) => {
34888
+ const bare = parts[parts.length - 1];
34889
+ record(bare);
34890
+ for (const variant of decodedVariants(bare))
34891
+ record(variant);
34892
+ if (parts.length > 1)
34893
+ record(parts.join("."));
34894
+ };
34895
+ let i = 0;
34896
+ while (i < tokens.length) {
34897
+ if (!isKeyword(tokens[i], TABLE_INTRODUCERS)) {
34898
+ i++;
34899
+ continue;
34900
+ }
34901
+ const introducer = tokens[i].value.toUpperCase();
34902
+ const parenMeansFunction = introducer === "FROM" || introducer === "JOIN";
34903
+ let cursor = i + 1;
34904
+ while (isKeyword(tokens[cursor], PRE_TABLE_NOISE))
34905
+ cursor++;
34906
+ while (parenMeansFunction && isPunctuation(tokens[cursor], "(") && !isKeyword(tokens[cursor + 1], SUBQUERY_OPENERS) && tokens[cursor + 1]?.kind === "identifier") {
34907
+ cursor++;
34908
+ }
34909
+ let expectTable = true;
34910
+ while (expectTable) {
34911
+ expectTable = false;
34912
+ const name2 = readQualifiedName(tokens, cursor);
34913
+ if (!name2)
34914
+ break;
34915
+ const isFunctionCall = parenMeansFunction && isPunctuation(tokens[name2.next], "(");
34916
+ if (!isFunctionCall)
34917
+ recordName(name2.parts);
34918
+ cursor = name2.next;
34919
+ if (isFunctionCall)
34920
+ break;
34921
+ if (isKeyword(tokens[cursor], AS_KEYWORD))
34922
+ cursor++;
34923
+ if (tokens[cursor]?.kind === "identifier" && !isKeyword(tokens[cursor], POST_TABLE_KEYWORDS)) {
34924
+ cursor++;
34925
+ }
34926
+ while (isPunctuation(tokens[cursor], "(")) {
34927
+ let depth = 0;
34928
+ do {
34929
+ if (isPunctuation(tokens[cursor], "("))
34930
+ depth++;
34931
+ else if (isPunctuation(tokens[cursor], ")"))
34932
+ depth--;
34933
+ cursor++;
34934
+ } while (depth > 0 && cursor < tokens.length);
34935
+ if (tokens[cursor]?.kind === "identifier" && !isKeyword(tokens[cursor], POST_TABLE_KEYWORDS))
34936
+ cursor++;
34937
+ }
34938
+ if (isPunctuation(tokens[cursor], ",")) {
34939
+ cursor++;
34940
+ expectTable = true;
34941
+ }
34942
+ }
34943
+ i = Math.max(cursor, i + 1);
34944
+ }
34945
+ let index = 0;
34946
+ while (index < tokens.length) {
34947
+ const token = tokens[index];
34948
+ if (!token || token.kind !== "identifier") {
34949
+ index++;
34950
+ continue;
34951
+ }
34952
+ const name2 = readQualifiedName(tokens, index);
34953
+ for (let part = 0;part < name2.parts.length; part++) {
34954
+ const value = name2.parts[part];
34955
+ const isQuoted = tokens[index + part * 2]?.quoted === true;
34956
+ if (isQuoted || !RESERVED_KEYWORDS.has(value.toUpperCase())) {
34957
+ record(value);
34958
+ for (const variant of decodedVariants(value))
34959
+ record(variant);
34960
+ }
34961
+ }
34962
+ if (name2.parts.length > 1)
34963
+ record(name2.parts.join("."));
34964
+ index = name2.next;
34965
+ }
34966
+ }
34967
+ var TABLE_INTRODUCERS, PRE_TABLE_NOISE, SUBQUERY_OPENERS, AS_KEYWORD, POST_TABLE_KEYWORDS, RESERVED_KEYWORDS, IDENTIFIER_START2, IDENTIFIER_PART, ANY_ESCAPE_SEQUENCE;
34968
+ var init_sql_tables = __esm(() => {
34969
+ init_sql_lexical();
34970
+ TABLE_INTRODUCERS = new Set([
34971
+ "FROM",
34972
+ "JOIN",
34973
+ "INTO",
34974
+ "UPDATE",
34975
+ "TABLE",
34976
+ "TRUNCATE",
34977
+ "COPY",
34978
+ "USING",
34979
+ "STRAIGHT_JOIN"
34980
+ ]);
34981
+ PRE_TABLE_NOISE = new Set(["ONLY", "LATERAL", "TABLE"]);
34982
+ SUBQUERY_OPENERS = new Set(["SELECT", "WITH", "VALUES", "TABLE"]);
34983
+ AS_KEYWORD = new Set(["AS"]);
34984
+ POST_TABLE_KEYWORDS = new Set([
34985
+ "AS",
34986
+ "ON",
34987
+ "USING",
34988
+ "WHERE",
34989
+ "GROUP",
34990
+ "ORDER",
34991
+ "HAVING",
34992
+ "LIMIT",
34993
+ "OFFSET",
34994
+ "FETCH",
34995
+ "WINDOW",
34996
+ "UNION",
34997
+ "INTERSECT",
34998
+ "EXCEPT",
34999
+ "JOIN",
35000
+ "INNER",
35001
+ "LEFT",
35002
+ "RIGHT",
35003
+ "FULL",
35004
+ "OUTER",
35005
+ "CROSS",
35006
+ "NATURAL",
35007
+ "STRAIGHT_JOIN",
35008
+ "SET",
35009
+ "VALUES",
35010
+ "SELECT",
35011
+ "RETURNING",
35012
+ "FOR",
35013
+ "INTO",
35014
+ "PARTITION",
35015
+ "WITH",
35016
+ "TABLESAMPLE",
35017
+ "FORCE",
35018
+ "IGNORE",
35019
+ "USE"
35020
+ ]);
35021
+ RESERVED_KEYWORDS = new Set([
35022
+ "ALL",
35023
+ "AND",
35024
+ "AS",
35025
+ "ASC",
35026
+ "CASE",
35027
+ "CROSS",
35028
+ "DESC",
35029
+ "DISTINCT",
35030
+ "ELSE",
35031
+ "FALSE",
35032
+ "FOR",
35033
+ "FROM",
35034
+ "GROUP",
35035
+ "HAVING",
35036
+ "IN",
35037
+ "INNER",
35038
+ "INTO",
35039
+ "IS",
35040
+ "JOIN",
35041
+ "LEFT",
35042
+ "LIKE",
35043
+ "LIMIT",
35044
+ "NOT",
35045
+ "NULL",
35046
+ "ON",
35047
+ "OR",
35048
+ "ORDER",
35049
+ "OUTER",
35050
+ "RIGHT",
35051
+ "SELECT",
35052
+ "THEN",
35053
+ "TRUE",
35054
+ "UNION",
35055
+ "USING",
35056
+ "VALUES",
35057
+ "WHEN",
35058
+ "WHERE",
35059
+ "WITH"
35060
+ ]);
35061
+ IDENTIFIER_START2 = /[A-Za-z_\u0080-\uFFFF]/;
35062
+ IDENTIFIER_PART = /[A-Za-z0-9_$\u0080-\uFFFF]/;
35063
+ ANY_ESCAPE_SEQUENCE = /([^0-9a-fA-F+'"\s])(?:\+([0-9a-fA-F]{6})|([0-9a-fA-F]{4}))/g;
35064
+ });
35065
+
34436
35066
  // src/core/limits.ts
34437
35067
  var DEFAULT_QUERY_ONLY_LIMIT = 1000;
34438
35068
 
@@ -34486,11 +35116,9 @@ class QueryExecutor {
34486
35116
  }
34487
35117
  }
34488
35118
  }
34489
- if (this.blacklistValidator) {
34490
- const tableName = extractTableName(sql);
34491
- if (tableName) {
34492
- this.blacklistValidator.checkTableBlacklist(classification.type, tableName, []);
34493
- }
35119
+ const referencedTables = this.blacklistValidator ? extractTableReferences(sql, { dialect: this.resolveDialect() }) : [];
35120
+ if (this.blacklistValidator && referencedTables.length > 0) {
35121
+ this.blacklistValidator.checkTablesBlacklist(classification.type, referencedTables);
34494
35122
  }
34495
35123
  const resultData = await this.adapter.execute(executeSql);
34496
35124
  const executionTimeMs = Math.round(performance.now() - start);
@@ -34503,15 +35131,12 @@ class QueryExecutor {
34503
35131
  let securityNotification;
34504
35132
  let omittedColumns = [];
34505
35133
  if (this.blacklistValidator) {
34506
- const tableName = extractTableName(sql);
34507
- if (tableName) {
34508
- const filterResult = this.blacklistValidator.filterColumns(tableName, rows, columnNames);
34509
- filteredRows = filterResult.filteredRows;
34510
- if (filterResult.omittedColumns.length > 0) {
34511
- omittedColumns = filterResult.omittedColumns;
34512
- columnNames = columnNames.filter((col) => !filterResult.omittedColumns.includes(col));
34513
- securityNotification = this.blacklistValidator.buildSecurityNotification(tableName, filterResult.omittedColumns);
34514
- }
35134
+ const filterResult = this.blacklistValidator.filterColumnsForTables(referencedTables, rows, columnNames);
35135
+ filteredRows = filterResult.filteredRows;
35136
+ if (filterResult.omittedColumns.length > 0) {
35137
+ omittedColumns = filterResult.omittedColumns;
35138
+ columnNames = columnNames.filter((col) => !filterResult.omittedColumns.includes(col));
35139
+ securityNotification = this.blacklistValidator.buildSecurityNotification(referencedTables[0] ?? "", filterResult.omittedColumns);
34515
35140
  }
34516
35141
  }
34517
35142
  if (options?.fieldSelection) {
@@ -34617,13 +35242,30 @@ function inferColumnType(value) {
34617
35242
  var init_query_executor = __esm(() => {
34618
35243
  init_permission_guard();
34619
35244
  init_error_suggester();
34620
- init_engine_hints();
35245
+ init_sql_tables();
34621
35246
  init_applied_limit();
34622
35247
  init_integration_helper();
34623
35248
  init_field_projection();
34624
35249
  });
34625
35250
 
34626
35251
  // src/core/mongo/field-masker.ts
35252
+ function maskMongoRowsForCollections(rows, collections, blacklist) {
35253
+ const columns = blacklist.columns ?? {};
35254
+ return collections.reduce((masked, entry) => {
35255
+ const scope = typeof entry === "string" ? { collection: entry } : entry;
35256
+ const atTopLevel = maskMongoRows(masked, scope.collection, blacklist);
35257
+ if (!scope.prefix)
35258
+ return atTopLevel;
35259
+ const rules = columns[scope.collection] ?? findCaseInsensitive(columns, scope.collection);
35260
+ if (!rules || rules.length === 0)
35261
+ return atTopLevel;
35262
+ const prefixKey = `\x00${scope.collection}@${scope.prefix}`;
35263
+ return maskMongoRows(atTopLevel, prefixKey, {
35264
+ ...blacklist,
35265
+ columns: { ...columns, [prefixKey]: rules.map((rule) => `${scope.prefix}.${rule}`) }
35266
+ });
35267
+ }, rows);
35268
+ }
34627
35269
  function maskMongoRows(rows, collection, blacklist) {
34628
35270
  const columns = blacklist.columns ?? {};
34629
35271
  const raw = columns[collection] ?? findCaseInsensitive(columns, collection);
@@ -34676,6 +35318,86 @@ function findCaseInsensitive(columns, name2) {
34676
35318
  var REDACTED2 = "[REDACTED]";
34677
35319
  var init_field_masker = () => {};
34678
35320
 
35321
+ // src/core/mongo/collection-references.ts
35322
+ function recordTarget(value, found) {
35323
+ if (typeof value === "string") {
35324
+ if (value.length > 0)
35325
+ found.add(value);
35326
+ return;
35327
+ }
35328
+ if (value === null || typeof value !== "object")
35329
+ return;
35330
+ const record = value;
35331
+ for (const field of COLLECTION_FIELDS) {
35332
+ const target = record[field];
35333
+ if (typeof target === "string" && target.length > 0)
35334
+ found.add(target);
35335
+ else if (target !== null && typeof target === "object")
35336
+ recordTarget(target, found);
35337
+ }
35338
+ }
35339
+ function joinPrefix(parent, child) {
35340
+ if (!parent)
35341
+ return child;
35342
+ if (!child)
35343
+ return parent;
35344
+ return `${parent}.${child}`;
35345
+ }
35346
+ function collect2(pipeline, found, scopes, parentPrefix) {
35347
+ if (!Array.isArray(pipeline))
35348
+ return;
35349
+ for (const stage of pipeline) {
35350
+ if (stage === null || typeof stage !== "object")
35351
+ continue;
35352
+ const record = stage;
35353
+ for (const name2 of COLLECTION_STAGES) {
35354
+ if (!Object.prototype.hasOwnProperty.call(record, name2))
35355
+ continue;
35356
+ const stageTargets = new Set;
35357
+ recordTarget(record[name2], stageTargets);
35358
+ for (const collection of stageTargets)
35359
+ found.add(collection);
35360
+ const value = record[name2];
35361
+ const as = value !== null && typeof value === "object" ? value["as"] : undefined;
35362
+ const prefix = joinPrefix(parentPrefix, typeof as === "string" && as ? as : undefined);
35363
+ for (const collection of stageTargets) {
35364
+ scopes.push({ collection, ...prefix ? { prefix } : {} });
35365
+ }
35366
+ }
35367
+ for (const holder of SUB_PIPELINE_HOLDERS2) {
35368
+ const value = record[holder];
35369
+ if (value === undefined || value === null)
35370
+ continue;
35371
+ if (Array.isArray(value)) {
35372
+ collect2(value, found, scopes, parentPrefix);
35373
+ continue;
35374
+ }
35375
+ if (typeof value !== "object")
35376
+ continue;
35377
+ for (const [key, nested] of Object.entries(value)) {
35378
+ const holderPrefix = holder === "$facet" ? joinPrefix(parentPrefix, key) : joinPrefix(parentPrefix, typeof value["as"] === "string" ? value["as"] : undefined);
35379
+ collect2(nested, found, scopes, holderPrefix);
35380
+ }
35381
+ }
35382
+ }
35383
+ }
35384
+ function findMongoCollectionReferences(pipeline) {
35385
+ const found = new Set;
35386
+ collect2(pipeline, found, []);
35387
+ return Array.from(found);
35388
+ }
35389
+ function findMongoCollectionScopes(pipeline) {
35390
+ const scopes = [];
35391
+ collect2(pipeline, new Set, scopes);
35392
+ return scopes;
35393
+ }
35394
+ var COLLECTION_FIELDS, SUB_PIPELINE_HOLDERS2, COLLECTION_STAGES;
35395
+ var init_collection_references = __esm(() => {
35396
+ COLLECTION_FIELDS = ["from", "coll", "into"];
35397
+ SUB_PIPELINE_HOLDERS2 = ["$facet", "$lookup", "$unionWith", "$graphLookup"];
35398
+ COLLECTION_STAGES = ["$lookup", "$unionWith", "$graphLookup", "$out", "$merge"];
35399
+ });
35400
+
34679
35401
  // src/core/query-input.ts
34680
35402
  function trimOuterWhitespace(value) {
34681
35403
  let start = 0;
@@ -34857,6 +35579,13 @@ var init_query_size_guard = __esm(() => {
34857
35579
  import crypto3 from "crypto";
34858
35580
  import { tmpdir } from "os";
34859
35581
  import { join as join23 } from "path";
35582
+ function mongoCollectionRefs(query) {
35583
+ try {
35584
+ return findMongoCollectionScopes(JSON.parse(query));
35585
+ } catch {
35586
+ return [];
35587
+ }
35588
+ }
34860
35589
  function requireSqlConnection2(connection) {
34861
35590
  if (!["postgresql", "mysql", "mariadb"].includes(connection.system)) {
34862
35591
  throw new Error(`This command requires a SQL connection, got: ${connection.system}`);
@@ -35008,17 +35737,22 @@ async function preflightQuery(query, options, context, fieldSelection, multiConn
35008
35737
  await preflightSqlSizeGuard(query, options, config);
35009
35738
  }
35010
35739
  async function preflightSqlSizeGuard(query, options, config) {
35011
- const { extractTableName: extractTableName2 } = await Promise.resolve().then(() => (init_engine_hints(), exports_engine_hints));
35012
- const mainTable = extractTableName2(query);
35013
- if (!mainTable || !config.schema || options.noLimit)
35014
- return;
35015
- const tableSchema = config.schema[mainTable];
35016
- if (!tableSchema)
35740
+ if (!config.schema || options.noLimit)
35017
35741
  return;
35742
+ const { extractTableReferences: extractTableReferences2 } = await Promise.resolve().then(() => (init_sql_tables(), exports_sql_tables));
35743
+ const { SQL_DIALECTS: SQL_DIALECTS2 } = await Promise.resolve().then(() => (init_permission_guard(), exports_permission_guard));
35744
+ const dialect = SQL_DIALECTS2.find((candidate) => candidate === config.connection?.system);
35745
+ const tables = extractTableReferences2(query, { ...dialect ? { dialect } : {} });
35746
+ const schema = config.schema;
35018
35747
  const { shouldBlockQuery: shouldBlockQuery2 } = await Promise.resolve().then(() => (init_query_size_guard(), exports_query_size_guard));
35019
- const guard = shouldBlockQuery2(query, tableSchema);
35020
- if (guard.blocked)
35021
- throw new Error(`\u26A0 ${guard.reason}`);
35748
+ for (const table of tables) {
35749
+ const tableSchema = schema[table];
35750
+ if (!tableSchema)
35751
+ continue;
35752
+ const guard = shouldBlockQuery2(query, tableSchema);
35753
+ if (guard.blocked)
35754
+ throw new Error(`\u26A0 ${guard.reason}`);
35755
+ }
35022
35756
  }
35023
35757
  async function preflightMongoQuery(query, options, config, multiConnection) {
35024
35758
  const collection = options.collection;
@@ -35044,7 +35778,10 @@ async function preflightMongoQuery(query, options, config, multiConnection) {
35044
35778
  context: "MongoDB multi-connection pipelines"
35045
35779
  });
35046
35780
  const blacklistValidator = new BlacklistValidator(new BlacklistManager(config));
35047
- blacklistValidator.checkTableBlacklist("SELECT", collection, []);
35781
+ blacklistValidator.checkTablesBlacklist("SELECT", [
35782
+ collection,
35783
+ ...findMongoCollectionReferences(parsedQuery)
35784
+ ]);
35048
35785
  if (!config.schema || options.noLimit)
35049
35786
  return;
35050
35787
  const tableSchema = config.schema[collection];
@@ -35068,7 +35805,7 @@ function preflightElasticsearchQuery(query, options, config, multiConnection) {
35068
35805
  JSON.parse(body);
35069
35806
  enforceElasticsearchPermission({ method: "POST", apiPath: `/${indexName}/_search`, body }, multiConnection ? "query-only" : config.permission);
35070
35807
  const blacklistValidator = new BlacklistValidator(new BlacklistManager(config));
35071
- blacklistValidator.checkTableBlacklist("SELECT", indexName, []);
35808
+ blacklistValidator.checkIndexBlacklist("SELECT", indexName);
35072
35809
  }
35073
35810
  async function executeConnectionQuery(query, options, context, fieldSelection) {
35074
35811
  const { config, configPath, connectionName } = context;
@@ -35221,7 +35958,7 @@ async function mongoQueryBranch(queryStr, options, context, fieldSelection) {
35221
35958
  const limitedResult = appliedLimit === undefined ? undefined : trimAppliedLimit(result.rows, appliedLimit);
35222
35959
  const visibleRows = limitedResult?.rows ?? result.rows;
35223
35960
  const blacklistCfg = config.blacklist ?? { tables: [], columns: {} };
35224
- const maskedRows = maskMongoRows(visibleRows, collection, blacklistCfg);
35961
+ const maskedRows = maskMongoRowsForCollections(visibleRows, [collection, ...mongoCollectionRefs(queryStr)], blacklistCfg);
35225
35962
  const projected = fieldSelection ? projectRows(maskedRows, fieldSelection) : undefined;
35226
35963
  const outputRows = projected?.rows ?? maskedRows;
35227
35964
  const columnNames = projected?.columnNames ?? (outputRows[0] ? Object.keys(outputRows[0]) : []);
@@ -35325,7 +36062,7 @@ async function elasticsearchQueryBranch(queryStr, options, context) {
35325
36062
  const limitedResult = appliedLimit === undefined ? undefined : trimAppliedLimit(result.rows, appliedLimit);
35326
36063
  const visibleRows = limitedResult?.rows ?? result.rows;
35327
36064
  const columnNames = visibleRows[0] ? Object.keys(visibleRows[0]) : [];
35328
- const filterResult = blacklistValidator.filterColumns(indexName, visibleRows, columnNames);
36065
+ const filterResult = blacklistValidator.filterColumnsForIndexExpression(indexName, visibleRows, columnNames);
35329
36066
  const queryResult = {
35330
36067
  rows: filterResult.filteredRows,
35331
36068
  rowCount: filterResult.filteredRows.length,
@@ -35387,6 +36124,7 @@ var init_query = __esm(() => {
35387
36124
  init_applied_limit();
35388
36125
  init_integration_helper();
35389
36126
  init_field_masker();
36127
+ init_collection_references();
35390
36128
  init_types4();
35391
36129
  init_query_input();
35392
36130
  init_field_projection();
@@ -35950,7 +36688,8 @@ async function qMongoBranch(snippet, prepared, options, config) {
35950
36688
  });
35951
36689
  const blacklistManager = new BlacklistManager(config);
35952
36690
  const blacklistValidator = new BlacklistValidator(blacklistManager);
35953
- blacklistValidator.checkTableBlacklist("SELECT", collection);
36691
+ const collections = [collection, ...findMongoCollectionReferences(parsedBody)];
36692
+ blacklistValidator.checkTablesBlacklist("SELECT", collections);
35954
36693
  if (options.dryRun) {
35955
36694
  console.log(`Dry-run preview (no execution):`);
35956
36695
  console.log(`Collection: ${collection}`);
@@ -35965,7 +36704,7 @@ async function qMongoBranch(snippet, prepared, options, config) {
35965
36704
  const result = await adapter.execute(prepared.driver.sql, [collection]);
35966
36705
  const executionTimeMs = Math.round(performance.now() - start);
35967
36706
  const blacklistCfg = config.blacklist ?? { tables: [], columns: {} };
35968
- const masked = maskMongoRows(result.rows, collection, blacklistCfg);
36707
+ const masked = maskMongoRowsForCollections(result.rows, [collection, ...findMongoCollectionScopes(parsedBody)], blacklistCfg);
35969
36708
  const securityNotification = (blacklistCfg.columns[collection] ?? []).length > 0 ? "Some fields may have been redacted as [REDACTED] per .dbcli blacklist." : undefined;
35970
36709
  if (options.ui || options.format === "html") {
35971
36710
  const html = await generateHtmlReport({
@@ -36006,6 +36745,7 @@ var init_q_mongo = __esm(() => {
36006
36745
  init_adapters();
36007
36746
  init_blacklist_validator();
36008
36747
  init_field_masker();
36748
+ init_collection_references();
36009
36749
  init_formatters();
36010
36750
  init_html_formatter();
36011
36751
  init_opener();
@@ -36053,7 +36793,8 @@ async function qCommand(name2, options, command) {
36053
36793
  config = await configModule.read(configPath);
36054
36794
  if (!config.connection)
36055
36795
  throw new Error('Run "dbcli init" first');
36056
- const engine = mapSystemToEngine(config.connection.system);
36796
+ const connectionSystem = config.connection.system;
36797
+ const engine = mapSystemToEngine(connectionSystem);
36057
36798
  const dirs = resolveSnippetDirs(process.cwd());
36058
36799
  const map = await loadSnippets(dirs);
36059
36800
  const snippet = resolveByName(map, name2, engine);
@@ -36088,10 +36829,16 @@ async function qCommand(name2, options, command) {
36088
36829
  const blacklistManager = new BlacklistManager(config);
36089
36830
  const blacklistValidator = new BlacklistValidator(blacklistManager);
36090
36831
  const family = engineFamily(engine);
36091
- const targetName = family === "sql" ? extractTableName(prepared.rewrittenSql) ?? "" : family === "es" ? prepared.execHints?.index ?? "" : "";
36832
+ const sqlDialect = SQL_DIALECTS.find((dialect) => dialect === connectionSystem);
36833
+ const targets = family === "sql" ? extractTableReferences(prepared.rewrittenSql, {
36834
+ ...sqlDialect ? { dialect: sqlDialect } : {}
36835
+ }) : family === "es" ? [prepared.execHints?.index ?? ""] : [];
36836
+ const targetName = targets[0] ?? "";
36092
36837
  targetNameForAudit = targetName || name2;
36093
- if (family !== "redis" && targetName) {
36094
- blacklistValidator.checkTableBlacklist("SELECT", targetName);
36838
+ if (family === "es") {
36839
+ blacklistValidator.checkIndexBlacklist("SELECT", targetName);
36840
+ } else if (family !== "redis" && targets.length > 0) {
36841
+ blacklistValidator.checkTablesBlacklist("SELECT", targets);
36095
36842
  }
36096
36843
  const adapter = AdapterFactory.createAdapter(config.connection);
36097
36844
  await adapter.connect();
@@ -36106,7 +36853,7 @@ async function qCommand(name2, options, command) {
36106
36853
  const limitedResult = prepared.guardLimit === undefined ? undefined : trimAppliedLimit(result.rows, prepared.guardLimit);
36107
36854
  const resultRows = limitedResult?.rows ?? result.rows;
36108
36855
  const columnNames = resultRows[0] ? Object.keys(resultRows[0]) : [];
36109
- const filtered = family === "redis" ? { filteredRows: resultRows, omittedColumns: [] } : blacklistValidator.filterColumns(targetName, resultRows, columnNames);
36856
+ const filtered = family === "redis" ? { filteredRows: resultRows, omittedColumns: [] } : family === "es" ? blacklistValidator.filterColumnsForIndexExpression(targetName, resultRows, columnNames) : blacklistValidator.filterColumnsForTables(targets, resultRows, columnNames);
36110
36857
  const securityNotification = family === "redis" || filtered.omittedColumns.length === 0 ? undefined : blacklistValidator.buildSecurityNotification(targetName, filtered.omittedColumns);
36111
36858
  if (options.ui || options.format === "html") {
36112
36859
  const html = await generateHtmlReport({
@@ -36166,6 +36913,11 @@ async function qCommand(name2, options, command) {
36166
36913
  } else {
36167
36914
  try {
36168
36915
  console.error(colors.dim(`Executing verification query: ${verifySpec.query}`));
36916
+ if (family === "sql") {
36917
+ blacklistValidator.checkTablesBlacklist("SELECT", extractTableReferences(verifySpec.query, {
36918
+ ...sqlDialect ? { dialect: sqlDialect } : {}
36919
+ }));
36920
+ }
36169
36921
  const verifyResult = await adapter.execute(verifySpec.query);
36170
36922
  const firstRow = verifyResult.rows[0];
36171
36923
  const evalResult = evaluateExpectation(firstRow, verifySpec.expects);
@@ -36348,7 +37100,7 @@ var init_q = __esm(() => {
36348
37100
  init_blacklist_validator();
36349
37101
  init_blacklist();
36350
37102
  init_permission_guard();
36351
- init_engine_hints();
37103
+ init_sql_tables();
36352
37104
  init_formatters();
36353
37105
  init_html_formatter();
36354
37106
  init_opener();
@@ -39186,7 +39938,8 @@ async function exportCommand(sql, options, command) {
39186
39938
  }
39187
39939
  const adapter = AdapterFactory.createSqlAdapter(requireSqlConnection6(config.connection));
39188
39940
  await adapter.connect();
39189
- const executor3 = new QueryExecutor(adapter, config.permission, undefined, config, {
39941
+ const blacklistValidator = new BlacklistValidator(new BlacklistManager(config));
39942
+ const executor3 = new QueryExecutor(adapter, config.permission, blacklistValidator, config, {
39190
39943
  recovery: options.recovery,
39191
39944
  deferDiagnostics: true
39192
39945
  });
@@ -39329,15 +40082,19 @@ async function esExportBranch(query, options, config) {
39329
40082
  let rowCount;
39330
40083
  const diagnostics = [];
39331
40084
  try {
40085
+ const declaredTarget = query.trim().startsWith("{") ? options.index ?? options.collection : query.trim();
40086
+ if (declaredTarget)
40087
+ blacklistValidator.checkIndexBlacklist("SELECT", declaredTarget);
39332
40088
  const {
39333
40089
  rows: fetched,
39334
40090
  target,
39335
40091
  cap: cap2
39336
40092
  } = await buildEsExportRows(query, options, adapter);
39337
- blacklistValidator.checkTableBlacklist("SELECT", target, []);
40093
+ blacklistValidator.checkIndexBlacklist("SELECT", target);
39338
40094
  const limitedResult = cap2 === undefined ? undefined : trimAppliedLimit(fetched, cap2);
39339
40095
  assertExportNotSilentlyTruncated(limitedResult?.metadata, options);
39340
- const rows = limitedResult?.rows ?? fetched;
40096
+ const visibleRows = limitedResult?.rows ?? fetched;
40097
+ const rows = blacklistValidator.filterColumnsForIndexExpression(target, visibleRows, collectColumnUnion(visibleRows)).filteredRows;
39341
40098
  rowCount = rows.length;
39342
40099
  const columns = collectColumnUnion(rows);
39343
40100
  if (options.format === "html") {
@@ -39393,7 +40150,9 @@ async function mongoExportBranch(query, options, config) {
39393
40150
  });
39394
40151
  const blacklistManager = new BlacklistManager(config);
39395
40152
  const blacklistValidator = new BlacklistValidator(blacklistManager);
39396
- blacklistValidator.checkTableBlacklist("SELECT", collection, []);
40153
+ const mongoScopes = findMongoCollectionScopes(JSON.parse(query));
40154
+ const mongoCollections = [collection, ...findMongoCollectionReferences(JSON.parse(query))];
40155
+ blacklistValidator.checkTablesBlacklist("SELECT", mongoCollections);
39397
40156
  let effectiveLimit;
39398
40157
  if (options.noLimit) {
39399
40158
  effectiveLimit = undefined;
@@ -39413,7 +40172,7 @@ async function mongoExportBranch(query, options, config) {
39413
40172
  const limitedResult = effectiveLimit === undefined ? undefined : trimAppliedLimit(result.rows, effectiveLimit);
39414
40173
  assertExportNotSilentlyTruncated(limitedResult?.metadata, options);
39415
40174
  const blacklistCfg = config.blacklist ?? { tables: [], columns: {} };
39416
- const maskedRows = maskMongoRows(limitedResult?.rows ?? result.rows, collection, blacklistCfg);
40175
+ const maskedRows = maskMongoRowsForCollections(limitedResult?.rows ?? result.rows, [collection, ...mongoScopes], blacklistCfg);
39417
40176
  rowCount = maskedRows.length;
39418
40177
  hasBlacklistedColumns = (blacklistCfg.columns[collection] ?? []).length > 0;
39419
40178
  const visibleColumns = collectColumnUnion(maskedRows);
@@ -39542,6 +40301,7 @@ var init_export = __esm(() => {
39542
40301
  init_integration_helper();
39543
40302
  init_engine_hints();
39544
40303
  init_field_masker();
40304
+ init_collection_references();
39545
40305
  SQL_PATTERN = /^\s*(SELECT|INSERT|UPDATE|DELETE|CREATE|DROP|ALTER|SHOW|DESCRIBE)\b/i;
39546
40306
  });
39547
40307
 
@@ -93845,6 +94605,25 @@ async function runDiagnostic(input) {
93845
94605
  return { ...base, rowCount: 0, rows: [], status: "skipped", reason, durationMs: 0 };
93846
94606
  }
93847
94607
  const family = engineFamily(input.engine);
94608
+ const referencedTables = family === "sql" ? extractTableReferences(prepared.rewrittenSql, {
94609
+ ...ENGINE_DIALECT2[input.engine] ? { dialect: ENGINE_DIALECT2[input.engine] } : {}
94610
+ }) : family === "es" && prepared.execHints?.index ? [prepared.execHints.index] : [];
94611
+ if (input.blacklistValidator && referencedTables.length > 0) {
94612
+ try {
94613
+ input.blacklistValidator.checkTablesBlacklist("SELECT", referencedTables);
94614
+ } catch (err) {
94615
+ if (!(err instanceof BlacklistError))
94616
+ throw err;
94617
+ return {
94618
+ ...base,
94619
+ rowCount: 0,
94620
+ rows: [],
94621
+ status: "skipped",
94622
+ reason: err.message,
94623
+ durationMs: 0
94624
+ };
94625
+ }
94626
+ }
93848
94627
  const start = performance.now();
93849
94628
  const exec2 = (async () => {
93850
94629
  const indexParams = family === "es" && prepared.execHints?.index ? [prepared.execHints.index] : [];
@@ -93880,7 +94659,8 @@ async function runDiagnostic(input) {
93880
94659
  durationMs
93881
94660
  };
93882
94661
  }
93883
- const rows = outcome.rows ?? [];
94662
+ const fetched = outcome.rows ?? [];
94663
+ const rows = input.blacklistValidator ? input.blacklistValidator.filterColumnsForTables(referencedTables, fetched, fetched[0] ? Object.keys(fetched[0]) : []).filteredRows : fetched;
93884
94664
  if (rows.length === 0) {
93885
94665
  return { ...base, rowCount: 0, rows: [], status: "no-data", durationMs };
93886
94666
  }
@@ -93892,9 +94672,17 @@ async function runDiagnostic(input) {
93892
94672
  durationMs
93893
94673
  };
93894
94674
  }
94675
+ var ENGINE_DIALECT2;
93895
94676
  var init_run_diagnostic = __esm(() => {
93896
94677
  init_saved_queries();
93897
94678
  init_strategies();
94679
+ init_sql_tables();
94680
+ init_blacklist();
94681
+ ENGINE_DIALECT2 = {
94682
+ postgres: "postgresql",
94683
+ mysql: "mysql",
94684
+ mariadb: "mariadb"
94685
+ };
93898
94686
  });
93899
94687
 
93900
94688
  // src/core/report/collector.ts
@@ -93950,6 +94738,7 @@ async function collectReport(opts) {
93950
94738
  return finalize({ context, sections: [], warnings, generatedAt });
93951
94739
  }
93952
94740
  const adapter = AdapterFactory.createAdapter(config.connection);
94741
+ const blacklistValidator = new BlacklistValidator(new BlacklistManager(config));
93953
94742
  const sectionEvidence = new Map;
93954
94743
  for (const id of sections)
93955
94744
  sectionEvidence.set(id, []);
@@ -93963,7 +94752,8 @@ async function collectReport(opts) {
93963
94752
  adapter,
93964
94753
  engine,
93965
94754
  timeoutMs: timeout,
93966
- maxRows
94755
+ maxRows,
94756
+ blacklistValidator
93967
94757
  });
93968
94758
  const sectionId = sectionForIntent(ev.intent);
93969
94759
  if (sectionId && sectionEvidence.has(sectionId)) {
@@ -94027,6 +94817,7 @@ var init_collector2 = __esm(() => {
94027
94817
  init_saved_queries();
94028
94818
  init_select_snippets();
94029
94819
  init_run_diagnostic();
94820
+ init_blacklist_validator();
94030
94821
  init_section_map();
94031
94822
  init_types7();
94032
94823
  });
@@ -97934,7 +98725,8 @@ var init_snapshot = __esm(() => {
97934
98725
  init_validation();
97935
98726
  init_blacklist_validator();
97936
98727
  init_query_executor();
97937
- init_engine_hints();
98728
+ init_permission_guard();
98729
+ init_sql_tables();
97938
98730
  init_fingerprint();
97939
98731
  init_serializer();
97940
98732
  init_saved_queries();
@@ -97962,10 +98754,12 @@ var init_snapshot = __esm(() => {
97962
98754
  try {
97963
98755
  const blacklistManager = new BlacklistManager(config);
97964
98756
  const blacklistValidator = new BlacklistValidator(blacklistManager);
98757
+ const sqlDialect = SQL_DIALECTS.find((dialect) => dialect === config.connection?.system);
97965
98758
  const executor3 = new QueryExecutor(adapter, config.permission, blacklistValidator, config, options);
97966
98759
  const result = await executor3.execute(sql, { autoLimit: options.limit !== false });
97967
- const table = extractTableName(sql);
97968
- const redactedColumns = table ? blacklistManager.getBlacklistedColumns(table) : [];
98760
+ const redactedColumns = Array.from(new Set(extractTableReferences(sql, {
98761
+ ...sqlDialect ? { dialect: sqlDialect } : {}
98762
+ }).flatMap((table) => blacklistManager.getBlacklistedColumns(table))));
97969
98763
  const snap = buildFingerprint(result, {
97970
98764
  includeRows: options.rows === true,
97971
98765
  redactedColumns,
@@ -99531,8 +100325,20 @@ function buildRealRunners(ctx) {
99531
100325
  const blacklist = config.blacklist ?? { tables: [], columns: {} };
99532
100326
  const schema = config.schema ?? {};
99533
100327
  const schemaLookup = { tables: schema, cacheAvailable: Object.keys(schema).length > 0 };
99534
- const analyze = (sql) => analyzeQueryRisk({ sql: sql.trim(), permission: config.permission, blacklist, schemaLookup, dialect: toSqlDialect(config.connection?.system) });
99535
- const analyzePlan = (sql) => analyzeQueryRisk({ sql: sql.trim(), permission: "read-write", blacklist, schemaLookup, dialect: toSqlDialect(config.connection?.system) });
100328
+ const analyze = (sql) => analyzeQueryRisk({
100329
+ sql: sql.trim(),
100330
+ permission: config.permission,
100331
+ blacklist,
100332
+ schemaLookup,
100333
+ dialect: toSqlDialect(config.connection?.system)
100334
+ });
100335
+ const analyzePlan = (sql) => analyzeQueryRisk({
100336
+ sql: sql.trim(),
100337
+ permission: "read-write",
100338
+ blacklist,
100339
+ schemaLookup,
100340
+ dialect: toSqlDialect(config.connection?.system)
100341
+ });
99536
100342
  return {
99537
100343
  blacklistGuard: async (table) => {
99538
100344
  const bm = new BlacklistManager(config);
@@ -99605,7 +100411,13 @@ function buildMigrationRunners(ctx) {
99605
100411
  const blacklist = config.blacklist ?? { tables: [], columns: {} };
99606
100412
  const schema = config.schema ?? {};
99607
100413
  const schemaLookup = { tables: schema, cacheAvailable: Object.keys(schema).length > 0 };
99608
- const analyze = (sql) => analyzeQueryRisk({ sql: sql.trim(), permission: config.permission, blacklist, schemaLookup, dialect: toSqlDialect(config.connection?.system) });
100414
+ const analyze = (sql) => analyzeQueryRisk({
100415
+ sql: sql.trim(),
100416
+ permission: config.permission,
100417
+ blacklist,
100418
+ schemaLookup,
100419
+ dialect: toSqlDialect(config.connection?.system)
100420
+ });
99609
100421
  return {
99610
100422
  blacklistGuard: async (table) => {
99611
100423
  const bm = new BlacklistManager(config);
@@ -99673,8 +100485,20 @@ function buildRollbackRunners(ctx, input) {
99673
100485
  const blacklist = config.blacklist ?? { tables: [], columns: {} };
99674
100486
  const schema = config.schema ?? {};
99675
100487
  const schemaLookup = { tables: schema, cacheAvailable: Object.keys(schema).length > 0 };
99676
- const analyze = (sql) => analyzeQueryRisk({ sql: sql.trim(), permission: config.permission, blacklist, schemaLookup, dialect: toSqlDialect(config.connection?.system) });
99677
- const analyzePlan = (sql) => analyzeQueryRisk({ sql: sql.trim(), permission: "read-write", blacklist, schemaLookup, dialect: toSqlDialect(config.connection?.system) });
100488
+ const analyze = (sql) => analyzeQueryRisk({
100489
+ sql: sql.trim(),
100490
+ permission: config.permission,
100491
+ blacklist,
100492
+ schemaLookup,
100493
+ dialect: toSqlDialect(config.connection?.system)
100494
+ });
100495
+ const analyzePlan = (sql) => analyzeQueryRisk({
100496
+ sql: sql.trim(),
100497
+ permission: "read-write",
100498
+ blacklist,
100499
+ schemaLookup,
100500
+ dialect: toSqlDialect(config.connection?.system)
100501
+ });
99678
100502
  const ddlStatementGuard = async (statement, table) => {
99679
100503
  if (!isSingleStatement(statement)) {
99680
100504
  return {
@@ -99786,7 +100610,13 @@ function buildConstraintRunners(ctx, input) {
99786
100610
  const blacklist = config.blacklist ?? { tables: [], columns: {} };
99787
100611
  const schema = config.schema ?? {};
99788
100612
  const schemaLookup = { tables: schema, cacheAvailable: Object.keys(schema).length > 0 };
99789
- const analyze = (sql) => analyzeQueryRisk({ sql: sql.trim(), permission: config.permission, blacklist, schemaLookup, dialect: toSqlDialect(config.connection?.system) });
100613
+ const analyze = (sql) => analyzeQueryRisk({
100614
+ sql: sql.trim(),
100615
+ permission: config.permission,
100616
+ blacklist,
100617
+ schemaLookup,
100618
+ dialect: toSqlDialect(config.connection?.system)
100619
+ });
99790
100620
  const engine = constraintEngineOf(config.connection.system);
99791
100621
  const violationSql = buildViolationQuery(input, engine);
99792
100622
  const columnsExist = async (table, cols) => {
@@ -103041,17 +103871,20 @@ class ReplEngine {
103041
103871
  };
103042
103872
  }
103043
103873
  }
103044
- if (this.config?.blacklist) {
103045
- const tableName = this.extractTableName(sql);
103046
- if (tableName) {
103047
- const blacklistedTables = this.config.blacklist.tables ?? [];
103048
- const isBlacklisted = blacklistedTables.some((t2) => t2.toLowerCase() === tableName.toLowerCase());
103049
- if (isBlacklisted) {
103050
- return {
103051
- action: "continue",
103052
- output: import_picocolors3.default.red(t_vars("shell.error_blacklisted", { table: tableName }))
103053
- };
103054
- }
103874
+ const referencedTables = extractTableReferences(sql, {
103875
+ dialect: SQL_DIALECTS.find((dialect) => dialect === this.context.system)
103876
+ });
103877
+ const blacklistValidator = this.config?.blacklist ? new BlacklistValidator(new BlacklistManager(this.config)) : undefined;
103878
+ if (blacklistValidator && referencedTables.length > 0) {
103879
+ try {
103880
+ blacklistValidator.checkTablesBlacklist("SELECT", referencedTables);
103881
+ } catch (error) {
103882
+ if (!(error instanceof BlacklistError))
103883
+ throw error;
103884
+ return {
103885
+ action: "continue",
103886
+ output: import_picocolors3.default.red(t_vars("shell.error_blacklisted", { table: error.message }))
103887
+ };
103055
103888
  }
103056
103889
  }
103057
103890
  const startTime = Date.now();
@@ -103060,8 +103893,11 @@ class ReplEngine {
103060
103893
  noLimit: this.state.noLimit
103061
103894
  });
103062
103895
  const elapsed = Date.now() - startTime;
103063
- const rows = result.rows;
103064
- const columnNames = rows.length > 0 && rows[0] ? Object.keys(rows[0]) : [];
103896
+ const fetched = result.rows;
103897
+ const fetchedColumns = fetched.length > 0 && fetched[0] ? Object.keys(fetched[0]) : [];
103898
+ const filtered = blacklistValidator ? blacklistValidator.filterColumnsForTables(referencedTables, fetched, fetchedColumns) : { filteredRows: fetched, omittedColumns: [] };
103899
+ const rows = filtered.filteredRows;
103900
+ const columnNames = fetchedColumns.filter((col) => !filtered.omittedColumns.includes(col));
103065
103901
  const queryResult = {
103066
103902
  rows,
103067
103903
  rowCount: rows.length,
@@ -103113,10 +103949,6 @@ class ReplEngine {
103113
103949
  }
103114
103950
  return null;
103115
103951
  }
103116
- extractTableName(sql) {
103117
- const match = sql.match(/\b(?:FROM|INTO|UPDATE)\s+["'`]?(\w+)["'`]?/i);
103118
- return match?.[1];
103119
- }
103120
103952
  isConnectionError(error) {
103121
103953
  const e = error;
103122
103954
  const msg = (e.message ?? "").toLowerCase();
@@ -103131,6 +103963,9 @@ var init_repl_engine = __esm(() => {
103131
103963
  init_permission_guard();
103132
103964
  init_query_result_formatter();
103133
103965
  init_message_loader();
103966
+ init_sql_tables();
103967
+ init_blacklist_validator();
103968
+ init_blacklist();
103134
103969
  import_picocolors3 = __toESM(require_picocolors(), 1);
103135
103970
  });
103136
103971
 
@@ -103340,22 +104175,115 @@ function extractIndexFromPath(path6) {
103340
104175
  return;
103341
104176
  return seg.split("?")[0];
103342
104177
  }
103343
- async function runEsRequest(req, adapter, blacklistTables) {
103344
- const index = extractIndexFromPath(req.path);
103345
- if (index && blacklistTables.some((t2) => t2.toLowerCase() === index.toLowerCase())) {
103346
- throw new Error(`BlacklistRejection: index '${index}' is blacklist-protected`);
104178
+ function isUnscopedMetadataPath(path6) {
104179
+ const first = path6.replace(/^\//, "").split("/")[0]?.split("?")[0] ?? "";
104180
+ return UNSCOPED_METADATA_PREFIXES.includes(first);
104181
+ }
104182
+ function findIndexNamesInBody(body) {
104183
+ const found = [];
104184
+ const walk = (node) => {
104185
+ if (Array.isArray(node)) {
104186
+ for (const item of node)
104187
+ walk(item);
104188
+ return;
104189
+ }
104190
+ if (node === null || typeof node !== "object")
104191
+ return;
104192
+ for (const [key, value] of Object.entries(node)) {
104193
+ if (key === "_index" || key === "index") {
104194
+ for (const candidate of Array.isArray(value) ? value : [value]) {
104195
+ if (typeof candidate === "string" && candidate.length > 0)
104196
+ found.push(candidate);
104197
+ }
104198
+ }
104199
+ walk(value);
104200
+ }
104201
+ };
104202
+ walk(body);
104203
+ return found;
104204
+ }
104205
+ async function runEsRequest(req, adapter, blacklistTables, blacklistColumns = {}) {
104206
+ const rawPath = req.path.split("?")[0] ?? req.path;
104207
+ const routedPath = normalizeEsPath(rawPath);
104208
+ const index = extractIndexFromPath(routedPath);
104209
+ if (blacklistTables.length > 0) {
104210
+ const literalSegments = `/${rawPath.split("/").filter(Boolean).join("/")}`;
104211
+ if (routedPath !== literalSegments) {
104212
+ throw new Error(`BlacklistRejection: '${req.path}' routes to '${routedPath}', which is not what it ` + `spells. Write the path the server will receive.`);
104213
+ }
104214
+ const blacklistedSegment = routedPath.split("/").find((segment) => segment.length > 0 && indexExpressionReaches(segment, blacklistTables));
104215
+ if (blacklistedSegment !== undefined) {
104216
+ throw new Error(`BlacklistRejection: index '${blacklistedSegment}' is blacklist-protected`);
104217
+ }
104218
+ if (index === undefined) {
104219
+ if (!isUnscopedMetadataPath(routedPath)) {
104220
+ throw new Error(`BlacklistRejection: '${req.path}' names no index, so it cannot be checked against ` + `the blacklist. Scope the request to an index, e.g. GET /<index>/_search.`);
104221
+ }
104222
+ }
104223
+ const inBody = findIndexNamesInBody(req.body).find((name2) => indexExpressionReaches(name2, blacklistTables));
104224
+ if (inBody !== undefined) {
104225
+ throw new Error(`BlacklistRejection: index '${inBody}' is blacklist-protected`);
104226
+ }
104227
+ }
104228
+ const protectedFields = new Set(Object.values(blacklistColumns).flat());
104229
+ if (protectedFields.size > 0) {
104230
+ const named = findStrings(req.body).find((text2) => protectedFields.has(text2));
104231
+ if (named !== undefined) {
104232
+ throw new Error(`BlacklistRejection: field '${named}' is blacklist-protected and cannot be named in a ` + `request \u2014 sorting, aggregating or scripting on it would return its values.`);
104233
+ }
103347
104234
  }
103348
104235
  let body = req.body;
103349
104236
  if (req.path.includes("_search") && body !== null && typeof body === "object" && !Array.isArray(body) && body.size === undefined) {
103350
104237
  body = { ...body, size: ES_SHELL_SIZE_CAP };
103351
104238
  }
103352
- return adapter.request(req.method, req.path, body);
104239
+ const response = await adapter.request(req.method, req.path, body);
104240
+ return protectedFields.size === 0 ? response : redactFields(response, protectedFields);
104241
+ }
104242
+ function findStrings(node) {
104243
+ const found = [];
104244
+ const walk = (value) => {
104245
+ if (typeof value === "string") {
104246
+ found.push(value);
104247
+ for (const piece of value.split(/[^A-Za-z0-9_.]+/)) {
104248
+ if (piece.length > 0 && piece !== value)
104249
+ found.push(piece);
104250
+ }
104251
+ return;
104252
+ }
104253
+ if (Array.isArray(value)) {
104254
+ for (const item of value)
104255
+ walk(item);
104256
+ return;
104257
+ }
104258
+ if (value === null || typeof value !== "object")
104259
+ return;
104260
+ for (const [key, nested] of Object.entries(value)) {
104261
+ found.push(key);
104262
+ walk(nested);
104263
+ }
104264
+ };
104265
+ walk(node);
104266
+ return found;
104267
+ }
104268
+ function redactFields(node, fields) {
104269
+ if (Array.isArray(node))
104270
+ return node.map((item) => redactFields(item, fields));
104271
+ if (node === null || typeof node !== "object")
104272
+ return node;
104273
+ const out = {};
104274
+ for (const [key, value] of Object.entries(node)) {
104275
+ if (fields.has(key))
104276
+ continue;
104277
+ out[key] = redactFields(value, fields);
104278
+ }
104279
+ return out;
103353
104280
  }
103354
104281
  async function runEsShell(configPath) {
103355
104282
  const config = await configModule.read(configPath);
103356
104283
  const adapter = AdapterFactory.createElasticsearchAdapter(config.connection);
103357
104284
  await adapter.connect();
103358
104285
  const blacklistTables = config.blacklist?.tables ?? [];
104286
+ const blacklistColumns = config.blacklist?.columns ?? {};
103359
104287
  console.error(import_picocolors4.default.bold("Elasticsearch shell \u2014 Kibana Dev Tools syntax"));
103360
104288
  console.error(import_picocolors4.default.dim('Enter "<METHOD> /<path>" then an optional JSON body; submit with a blank line. Try: GET /_cat/indices'));
103361
104289
  console.error(import_picocolors4.default.dim('Ctrl+C cancels the current block; Ctrl+D or "exit" quits.'));
@@ -103380,7 +104308,7 @@ async function runEsShell(configPath) {
103380
104308
  }
103381
104309
  try {
103382
104310
  const req = parseEsRequest(block2);
103383
- const res = await runEsRequest(req, adapter, blacklistTables);
104311
+ const res = await runEsRequest(req, adapter, blacklistTables, blacklistColumns);
103384
104312
  console.log(JSON.stringify(res, null, 2));
103385
104313
  } catch (error) {
103386
104314
  console.error(import_picocolors4.default.red(error.message));
@@ -103408,11 +104336,13 @@ async function runEsShell(configPath) {
103408
104336
  process.exit(0);
103409
104337
  });
103410
104338
  }
103411
- var import_picocolors4, ES_SHELL_SIZE_CAP = 1000;
104339
+ var import_picocolors4, UNSCOPED_METADATA_PREFIXES, ES_SHELL_SIZE_CAP = 1000;
103412
104340
  var init_es_shell = __esm(() => {
103413
104341
  init_config();
103414
104342
  init_adapters();
104343
+ init_es_index_target();
103415
104344
  import_picocolors4 = __toESM(require_picocolors(), 1);
104345
+ UNSCOPED_METADATA_PREFIXES = ["_cat", "_cluster", "_nodes", "_tasks", "_ingest", "_license"];
103416
104346
  });
103417
104347
 
103418
104348
  // src/core/repl/command-registry.ts
@@ -105380,6 +106310,10 @@ function fingerprintSql(sql) {
105380
106310
  function shellEscapeDq(s) {
105381
106311
  return s.replace(/\\/g, "\\\\").replace(/\$/g, "\\$").replace(/`/g, "\\`").replace(/"/g, "\\\"");
105382
106312
  }
106313
+ function explainSuggestions(sql) {
106314
+ const esc = shellEscapeDq(sql);
106315
+ return [`dbcli explain "${esc}"`, `dbcli guide missing-index-for "${esc}"`];
106316
+ }
105383
106317
  function buildByFingerprint(events, slowMs, top) {
105384
106318
  const errorByFp = new Map;
105385
106319
  for (const e of events.filter(isErrored)) {
@@ -105448,11 +106382,7 @@ function buildByFingerprint(events, slowMs, top) {
105448
106382
  stats.sort((a, b) => b.durationMs.total - a.durationMs.total);
105449
106383
  return stats.map((s, i) => {
105450
106384
  if (i < top && s.statement === "SELECT") {
105451
- const sql = shellEscapeDq(s.exampleSql);
105452
- return {
105453
- ...s,
105454
- suggestedCommands: [`dbcli explain "${sql}"`, `dbcli guide missing-index-for "${sql}"`]
105455
- };
106385
+ return { ...s, suggestedCommands: explainSuggestions(s.exampleSql) };
105456
106386
  }
105457
106387
  return s;
105458
106388
  });
@@ -105479,13 +106409,27 @@ function buildErrors(events) {
105479
106409
  message: e.error.message,
105480
106410
  count: 0,
105481
106411
  fingerprint: fingerprintSql(e.sql),
105482
- exampleSql: e.sql
106412
+ exampleSql: e.sql,
106413
+ tables: e.tables
105483
106414
  };
105484
106415
  groups.set(key, g);
105485
106416
  }
105486
106417
  g.count += 1;
105487
106418
  }
105488
- return [...groups.values()].sort((a, b) => b.count - a.count);
106419
+ return [...groups.values()].sort((a, b) => b.count - a.count).map((g) => {
106420
+ const schemaCmds = g.tables.slice(0, MAX_ERROR_SCHEMA_TABLES).map((t2) => `dbcli schema ${t2}`);
106421
+ const hint = `failed ${g.count}\xD7: verify the involved table/column names against the live schema ` + `before fixing the SQL \u2014 never guess column names`;
106422
+ return {
106423
+ code: g.code,
106424
+ message: g.message,
106425
+ count: g.count,
106426
+ fingerprint: g.fingerprint,
106427
+ exampleSql: g.exampleSql,
106428
+ tables: g.tables,
106429
+ ...schemaCmds.length ? { suggestedCommands: schemaCmds } : {},
106430
+ hints: [hint]
106431
+ };
106432
+ });
105489
106433
  }
105490
106434
  function buildHotTables(events) {
105491
106435
  const map = new Map;
@@ -105514,10 +106458,13 @@ function buildRepetition(events, threshold) {
105514
106458
  fingerprint: fp,
105515
106459
  sessionId: e.sessionId,
105516
106460
  tables: e.tables,
106461
+ statement: e.statement,
105517
106462
  count: 0,
105518
106463
  totalDurationMs: 0,
105519
106464
  minTs: ts,
105520
- maxTs: ts
106465
+ maxTs: ts,
106466
+ exampleSql: e.sql,
106467
+ exampleDuration: e.durationMs
105521
106468
  };
105522
106469
  groups.set(key, g);
105523
106470
  }
@@ -105527,15 +106474,27 @@ function buildRepetition(events, threshold) {
105527
106474
  g.minTs = ts;
105528
106475
  if (ts > g.maxTs)
105529
106476
  g.maxTs = ts;
106477
+ if (e.durationMs > g.exampleDuration) {
106478
+ g.exampleDuration = e.durationMs;
106479
+ g.exampleSql = e.sql;
106480
+ }
105530
106481
  }
105531
- return [...groups.values()].filter((g) => g.count >= threshold).map((g) => ({
105532
- fingerprint: g.fingerprint,
105533
- sessionId: g.sessionId,
105534
- count: g.count,
105535
- spanMs: g.maxTs - g.minTs,
105536
- totalDurationMs: g.totalDurationMs,
105537
- tables: g.tables
105538
- })).sort((a, b) => b.count - a.count);
106482
+ return [...groups.values()].filter((g) => g.count >= threshold).sort((a, b) => b.count - a.count).map((g) => {
106483
+ const spanMs = g.maxTs - g.minTs;
106484
+ const hint = `N+1 suspect: the same query ran ${g.count}\xD7 within one session over ${spanMs}ms \u2014 ` + `collapse into a single query (JOIN / IN (...)) or cache the result`;
106485
+ return {
106486
+ fingerprint: g.fingerprint,
106487
+ sessionId: g.sessionId,
106488
+ count: g.count,
106489
+ spanMs,
106490
+ totalDurationMs: g.totalDurationMs,
106491
+ tables: g.tables,
106492
+ statement: g.statement,
106493
+ exampleSql: g.exampleSql,
106494
+ ...g.statement === "SELECT" ? { suggestedCommands: explainSuggestions(g.exampleSql) } : {},
106495
+ hints: [hint]
106496
+ };
106497
+ });
105539
106498
  }
105540
106499
  function analyzeEvents(events, opts) {
105541
106500
  const timestamps = events.map((e) => e.timestamp).filter(Boolean).sort();
@@ -105589,7 +106548,7 @@ function buildSummary(events, slowMs) {
105589
106548
  }
105590
106549
  };
105591
106550
  }
105592
- var isCompleted = (e) => e.type === "query_completed", isErrored = (e) => e.type === "query_errored";
106551
+ var isCompleted = (e) => e.type === "query_completed", isErrored = (e) => e.type === "query_errored", MAX_ERROR_SCHEMA_TABLES = 3;
105593
106552
  var init_analyze = __esm(() => {
105594
106553
  init_sql_metadata();
105595
106554
  });
@@ -105629,12 +106588,29 @@ function renderAnalysisText(report, top) {
105629
106588
  for (const r of report.repetition.slice(0, top)) {
105630
106589
  L2.push(` ${r.count}x in session ${r.sessionId} (${r.spanMs}ms) ${r.fingerprint}`);
105631
106590
  }
105632
- const cmds = [...new Set(report.byFingerprint.flatMap((f) => f.suggestedCommands ?? []))];
106591
+ const cmds = [
106592
+ ...new Set([
106593
+ ...report.byFingerprint.flatMap((f) => f.suggestedCommands ?? []),
106594
+ ...report.errors.flatMap((e) => e.suggestedCommands ?? []),
106595
+ ...report.repetition.flatMap((r) => r.suggestedCommands ?? [])
106596
+ ])
106597
+ ];
105633
106598
  if (cmds.length) {
105634
106599
  L2.push("", "SUGGESTED COMMANDS");
105635
106600
  for (const c2 of cmds)
105636
106601
  L2.push(` ${c2}`);
105637
106602
  }
106603
+ const hints = [
106604
+ ...new Set([
106605
+ ...report.errors.flatMap((e) => e.hints ?? []),
106606
+ ...report.repetition.flatMap((r) => r.hints ?? [])
106607
+ ])
106608
+ ];
106609
+ if (hints.length) {
106610
+ L2.push("", "HINTS");
106611
+ for (const h of hints)
106612
+ L2.push(` ${h}`);
106613
+ }
105638
106614
  return L2.join(`
105639
106615
  `);
105640
106616
  }