@carllee1983/dbcli 1.47.1 → 1.48.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.48.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",
@@ -9316,6 +9317,14 @@ class BlacklistManager {
9316
9317
  }
9317
9318
  return Array.from(columnSet);
9318
9319
  }
9320
+ getAllBlacklistedColumns() {
9321
+ const all = new Set;
9322
+ for (const columnSet of this.state.columns.values()) {
9323
+ for (const column of columnSet)
9324
+ all.add(column);
9325
+ }
9326
+ return Array.from(all);
9327
+ }
9319
9328
  canOverrideBlacklist() {
9320
9329
  return this.overrideEnabled;
9321
9330
  }
@@ -9780,6 +9789,35 @@ var init_redis = __esm(() => {
9780
9789
  };
9781
9790
  });
9782
9791
 
9792
+ // src/utils/sql-lexical.ts
9793
+ function dollarQuoteDelimiterAt(sql, index) {
9794
+ if (sql[index] !== "$")
9795
+ return;
9796
+ if (continuesIdentifier(sql, index))
9797
+ return;
9798
+ return sql.slice(index).match(DOLLAR_QUOTE_DELIMITER)?.[0];
9799
+ }
9800
+ function continuesIdentifier(sql, index) {
9801
+ let start = index;
9802
+ while (start > 0 && IDENTIFIER_CONTINUATION.test(sql[start - 1] ?? ""))
9803
+ start--;
9804
+ if (start === index)
9805
+ return false;
9806
+ const run = sql.slice(start, index);
9807
+ const first = run[0];
9808
+ if (IDENTIFIER_START.test(first))
9809
+ return true;
9810
+ if (first === "$")
9811
+ return false;
9812
+ return run.replace(/^[0-9]+(?:[eE][0-9]+)?/, "").length > 0;
9813
+ }
9814
+ var IDENTIFIER_CONTINUATION, IDENTIFIER_START, DOLLAR_QUOTE_DELIMITER;
9815
+ var init_sql_lexical = __esm(() => {
9816
+ IDENTIFIER_CONTINUATION = /[A-Za-z0-9_$]|[\u0080-\uFFFF]/;
9817
+ IDENTIFIER_START = /[A-Za-z_]|[\u0080-\uFFFF]/;
9818
+ DOLLAR_QUOTE_DELIMITER = /^\$(?:(?:[A-Za-z_]|[\u0080-\uFFFF])(?:[A-Za-z0-9_]|[\u0080-\uFFFF])*)?\$/;
9819
+ });
9820
+
9783
9821
  // src/core/permission-guard.ts
9784
9822
  var exports_permission_guard = {};
9785
9823
  __export(exports_permission_guard, {
@@ -9873,8 +9911,7 @@ function stripCommentsAndStrings2(sql, options = {}) {
9873
9911
  continue;
9874
9912
  }
9875
9913
  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;
9914
+ const delimiter = dollarQuoteDelimiterAt(sql, i);
9878
9915
  if (delimiter) {
9879
9916
  i += delimiter.length;
9880
9917
  const closingIndex = sql.indexOf(delimiter, i);
@@ -10348,8 +10385,9 @@ function checkElasticsearchPermission(classification, permission) {
10348
10385
  reason: `Elasticsearch ${classification.type} operation requires higher permission tier`
10349
10386
  };
10350
10387
  }
10351
- var PermissionError, IDENTIFIER_CONTINUATION, SQL_DIALECTS, SQL_WRITE_OR_DDL_KEYWORDS, SQL_LOCK_CLAUSE, ESCALATABLE_READ_TYPES, REDIS_COMMAND_PERMISSION, PERMISSION_RANK;
10388
+ var PermissionError, SQL_DIALECTS, SQL_WRITE_OR_DDL_KEYWORDS, SQL_LOCK_CLAUSE, ESCALATABLE_READ_TYPES, REDIS_COMMAND_PERMISSION, PERMISSION_RANK;
10352
10389
  var init_permission_guard = __esm(() => {
10390
+ init_sql_lexical();
10353
10391
  PermissionError = class PermissionError extends Error {
10354
10392
  classification;
10355
10393
  requiredPermission;
@@ -10361,7 +10399,6 @@ var init_permission_guard = __esm(() => {
10361
10399
  Object.setPrototypeOf(this, PermissionError.prototype);
10362
10400
  }
10363
10401
  };
10364
- IDENTIFIER_CONTINUATION = /[A-Za-z0-9_$]|[\u0080-\uFFFF]/;
10365
10402
  SQL_DIALECTS = ["postgresql", "mysql", "mariadb"];
10366
10403
  SQL_WRITE_OR_DDL_KEYWORDS = /(?<![.\w])(INSERT|UPDATE|DELETE|MERGE|UPSERT|REPLACE|TRUNCATE|DROP|ALTER|CREATE|GRANT|REVOKE|RENAME|INTO)\b(?!\s*\()/i;
10367
10404
  SQL_LOCK_CLAUSE = /\bFOR\s+(?:NO\s+KEY\s+)?UPDATE\b|\bFOR\s+(?:KEY\s+)?SHARE\b/gi;
@@ -26063,22 +26100,27 @@ var init_size_guard2 = __esm(() => {
26063
26100
  init_types4();
26064
26101
  });
26065
26102
 
26066
- // src/adapters/redis/blacklist-enforcer.ts
26103
+ // src/utils/glob.ts
26067
26104
  function globToRegex(glob) {
26068
26105
  let out = "^";
26069
26106
  for (let i = 0;i < glob.length; i++) {
26070
26107
  const c2 = glob[i];
26108
+ if (c2 === "\\" && i + 1 < glob.length) {
26109
+ out += glob[++i].replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
26110
+ continue;
26111
+ }
26071
26112
  if (c2 === "*")
26072
26113
  out += ".*";
26073
26114
  else if (c2 === "?")
26074
26115
  out += ".";
26075
26116
  else if (c2 === "[") {
26076
- const end = glob.indexOf("]", i);
26077
- if (end === -1)
26078
- out += "\\[";
26079
- else {
26080
- out += glob.slice(i, end + 1);
26117
+ const end = findClassEnd(glob, i);
26118
+ const body = end === -1 ? "" : glob.slice(i, end + 1);
26119
+ if (body !== "" && isValidCharacterClass(body)) {
26120
+ out += body;
26081
26121
  i = end;
26122
+ } else {
26123
+ out += "\\[";
26082
26124
  }
26083
26125
  } else if (".^$+(){}|\\".includes(c2)) {
26084
26126
  out += "\\" + c2;
@@ -26089,6 +26131,27 @@ function globToRegex(glob) {
26089
26131
  out += "$";
26090
26132
  return new RegExp(out);
26091
26133
  }
26134
+ function findClassEnd(glob, open2) {
26135
+ for (let i = open2 + 1;i < glob.length; i++) {
26136
+ if (glob[i] === "\\") {
26137
+ i++;
26138
+ continue;
26139
+ }
26140
+ if (glob[i] === "]")
26141
+ return i;
26142
+ }
26143
+ return -1;
26144
+ }
26145
+ function isValidCharacterClass(body) {
26146
+ try {
26147
+ new RegExp(body);
26148
+ return true;
26149
+ } catch {
26150
+ return false;
26151
+ }
26152
+ }
26153
+
26154
+ // src/adapters/redis/blacklist-enforcer.ts
26092
26155
  function patternsOverlap(a, b) {
26093
26156
  const ra = globToRegex(a);
26094
26157
  const rb = globToRegex(b);
@@ -30374,28 +30437,161 @@ var init_field_projection = __esm(() => {
30374
30437
  UNSAFE_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]);
30375
30438
  });
30376
30439
 
30440
+ // src/utils/es-index-target.ts
30441
+ function normalizeEsPath(path5) {
30442
+ let current = path5;
30443
+ for (let pass = 0;pass < MAX_DECODE_PASSES; pass++) {
30444
+ let decoded = current;
30445
+ try {
30446
+ decoded = decodeURIComponent(current);
30447
+ } catch {}
30448
+ const segments = [];
30449
+ for (const segment of decoded.split("/")) {
30450
+ if (segment === "." || segment === "")
30451
+ continue;
30452
+ if (segment === "..") {
30453
+ segments.pop();
30454
+ continue;
30455
+ }
30456
+ segments.push(segment);
30457
+ }
30458
+ const resolved = `/${segments.join("/")}`;
30459
+ if (resolved === current)
30460
+ return resolved;
30461
+ current = resolved;
30462
+ }
30463
+ return current;
30464
+ }
30465
+ function unwrap(target) {
30466
+ let current = target;
30467
+ for (let pass = 0;pass < MAX_DECODE_PASSES; pass++) {
30468
+ let next = current;
30469
+ try {
30470
+ next = decodeURIComponent(next);
30471
+ } catch {}
30472
+ if (next.startsWith("<") && next.endsWith(">")) {
30473
+ next = next.slice(1, -1).replace(/\{[^}]*\}/g, "*");
30474
+ }
30475
+ if (next === current)
30476
+ return current;
30477
+ current = next;
30478
+ }
30479
+ return current;
30480
+ }
30481
+ function expandIndexTargets(target) {
30482
+ const concrete = [];
30483
+ const wildcards = [];
30484
+ const add = (candidate) => {
30485
+ if (candidate.length === 0)
30486
+ return;
30487
+ if (/[*?]/.test(candidate) || candidate.toLowerCase() === "_all")
30488
+ wildcards.push(candidate);
30489
+ else
30490
+ concrete.push(candidate);
30491
+ };
30492
+ for (const rawPart of unwrap(target).split(",")) {
30493
+ const part = unwrap(rawPart.trim()).trim().replace(/^[-+]/, "");
30494
+ if (part.length === 0)
30495
+ continue;
30496
+ add(part);
30497
+ if (part.includes(":"))
30498
+ for (const section of part.split(":"))
30499
+ add(section);
30500
+ }
30501
+ return { concrete, wildcards };
30502
+ }
30503
+ function matchesIndexGlob(pattern, name2) {
30504
+ const normalized = pattern.toLowerCase() === "_all" ? "*" : pattern.toLowerCase();
30505
+ try {
30506
+ return globToRegex(normalized).test(name2.toLowerCase());
30507
+ } catch {
30508
+ return true;
30509
+ }
30510
+ }
30511
+ function reachesByConvention(name2, entry) {
30512
+ const lower = name2.toLowerCase();
30513
+ const target = entry.toLowerCase();
30514
+ return new RegExp(`^\\.ds-${escapeRegExp(target)}-`).test(lower) || new RegExp(`^${escapeRegExp(target)}-\\d+$`).test(lower);
30515
+ }
30516
+ function escapeRegExp(text2) {
30517
+ return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
30518
+ }
30519
+ function indexExpressionReaches(expression, blacklisted) {
30520
+ if (blacklisted.length === 0)
30521
+ return false;
30522
+ const { concrete, wildcards } = expandIndexTargets(expression);
30523
+ return concrete.some((name2) => blacklisted.some((entry) => entry.toLowerCase() === name2.toLowerCase() || reachesByConvention(name2, entry))) || wildcards.some((pattern) => blacklisted.some((entry) => matchesIndexGlob(pattern, entry)));
30524
+ }
30525
+ var MAX_DECODE_PASSES = 4;
30526
+ var init_es_index_target = () => {};
30527
+
30377
30528
  // src/core/blacklist-validator.ts
30529
+ function dedupe2(values) {
30530
+ const seen = new Set;
30531
+ const result = [];
30532
+ for (const value of values) {
30533
+ const key = value.toLowerCase();
30534
+ if (value.length === 0 || seen.has(key))
30535
+ continue;
30536
+ seen.add(key);
30537
+ result.push(value);
30538
+ }
30539
+ return result;
30540
+ }
30541
+
30378
30542
  class BlacklistValidator {
30379
30543
  manager;
30380
30544
  constructor(manager) {
30381
30545
  this.manager = manager;
30382
30546
  }
30383
- checkTableBlacklist(operation, tableName, _tableList = []) {
30547
+ checkTableBlacklist(operation, tableName, tableList = []) {
30548
+ this.checkTablesBlacklist(operation, [tableName, ...tableList]);
30549
+ }
30550
+ checkTablesBlacklist(operation, tableNames) {
30551
+ const tables = dedupe2(tableNames);
30552
+ if (tables.length === 0) {
30553
+ return;
30554
+ }
30384
30555
  if (this.manager.canOverrideBlacklist()) {
30385
- const message = t_vars("warnings.blacklist_override_used", {
30556
+ const message2 = t_vars("warnings.blacklist_override_used", {
30386
30557
  operation,
30387
- table: tableName
30558
+ table: tables.join(", ")
30388
30559
  });
30389
- console.error(message);
30560
+ console.error(message2);
30390
30561
  return;
30391
30562
  }
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);
30563
+ const blocked = tables.filter((table) => this.manager.isTableBlacklisted(table));
30564
+ if (blocked.length === 0) {
30565
+ return;
30398
30566
  }
30567
+ const message = t_vars("errors.table_blacklisted", {
30568
+ table: blocked.join(", "),
30569
+ operation
30570
+ });
30571
+ throw new BlacklistError(message, blocked[0], operation);
30572
+ }
30573
+ checkIndexBlacklist(operation, target) {
30574
+ const { concrete, wildcards } = expandIndexTargets(target);
30575
+ this.checkTablesBlacklist(operation, concrete);
30576
+ if (wildcards.length === 0 || this.manager.canOverrideBlacklist())
30577
+ return;
30578
+ const blacklisted = Array.from(this.manager.getState().tables);
30579
+ if (blacklisted.length === 0)
30580
+ return;
30581
+ const reachable = wildcards.filter((pattern) => blacklisted.some((entry) => matchesIndexGlob(pattern, entry)));
30582
+ if (reachable.length === 0)
30583
+ return;
30584
+ const message = t_vars("errors.table_blacklisted", {
30585
+ table: reachable.join(", "),
30586
+ operation
30587
+ });
30588
+ throw new BlacklistError(message, reachable[0], operation);
30589
+ }
30590
+ filterColumnsForIndexExpression(target, rows, columnList) {
30591
+ const { concrete, wildcards } = expandIndexTargets(target);
30592
+ const ruleKeys = Array.from(this.manager.getState().columns.keys());
30593
+ const reachable = ruleKeys.filter((key) => wildcards.some((pattern) => matchesIndexGlob(pattern, key)));
30594
+ return this.filterColumnsForTables([...concrete, ...reachable], rows, columnList);
30399
30595
  }
30400
30596
  checkColumnBlacklistOnWrite(tableName, fields, operation = "WRITE") {
30401
30597
  const blacklisted = this.manager.getBlacklistedColumns(tableName);
@@ -30422,7 +30618,11 @@ class BlacklistValidator {
30422
30618
  throw new BlacklistError(message, tableName, operation);
30423
30619
  }
30424
30620
  filterColumns(tableName, rows, columnList) {
30425
- const blacklistedColumns = this.manager.getBlacklistedColumns(tableName);
30621
+ return this.filterColumnsForTables([tableName], rows, columnList);
30622
+ }
30623
+ filterColumnsForTables(tableNames, rows, columnList) {
30624
+ const tables = dedupe2(tableNames);
30625
+ const blacklistedColumns = tables.length === 0 ? this.manager.getAllBlacklistedColumns() : Array.from(new Set(tables.flatMap((table) => this.manager.getBlacklistedColumns(table))));
30426
30626
  if (blacklistedColumns.length === 0) {
30427
30627
  return { filteredRows: rows, omittedColumns: [] };
30428
30628
  }
@@ -30446,6 +30646,7 @@ var init_blacklist_validator = __esm(() => {
30446
30646
  init_blacklist();
30447
30647
  init_message_loader();
30448
30648
  init_field_projection();
30649
+ init_es_index_target();
30449
30650
  });
30450
30651
 
30451
30652
  // src/core/verification/types.ts
@@ -34433,6 +34634,355 @@ async function openInBrowser(target) {
34433
34634
  }
34434
34635
  var init_opener = () => {};
34435
34636
 
34637
+ // src/utils/sql-tables.ts
34638
+ var exports_sql_tables = {};
34639
+ __export(exports_sql_tables, {
34640
+ extractTableReferences: () => extractTableReferences
34641
+ });
34642
+ function tokenize(sql, dialect, backslashEscapes) {
34643
+ const tokens = [];
34644
+ const mysqlDialect = dialect === "mysql" || dialect === "mariadb";
34645
+ let i = 0;
34646
+ while (i < sql.length) {
34647
+ const char = sql[i];
34648
+ const dashFollowerCode = sql.charCodeAt(i + 2);
34649
+ if (char === "-" && sql[i + 1] === "-" && (!mysqlDialect || sql[i + 2] === undefined || dashFollowerCode <= 32 || dashFollowerCode === 127)) {
34650
+ while (i < sql.length && sql[i] !== `
34651
+ `)
34652
+ i++;
34653
+ continue;
34654
+ }
34655
+ if (mysqlDialect && char === "#") {
34656
+ while (i < sql.length && sql[i] !== `
34657
+ `)
34658
+ i++;
34659
+ continue;
34660
+ }
34661
+ if (char === "/" && sql[i + 1] === "*") {
34662
+ if (mysqlDialect && (sql.startsWith("/*!", i) || sql.startsWith("/*M!", i))) {
34663
+ const prefixLength = sql.startsWith("/*M!", i) ? 4 : 3;
34664
+ const closingIndex = sql.indexOf("*/", i + prefixLength);
34665
+ const bodyEnd = closingIndex === -1 ? sql.length : closingIndex;
34666
+ const body = sql.slice(i + prefixLength, bodyEnd).replace(/^\d+/, " ");
34667
+ tokens.push(...tokenize(body, dialect, backslashEscapes));
34668
+ i = closingIndex === -1 ? sql.length : closingIndex + 2;
34669
+ continue;
34670
+ }
34671
+ const nests = dialect === "postgresql";
34672
+ let depth = 1;
34673
+ i += 2;
34674
+ while (i < sql.length && depth > 0) {
34675
+ if (nests && sql[i] === "/" && sql[i + 1] === "*") {
34676
+ depth++;
34677
+ i += 2;
34678
+ continue;
34679
+ }
34680
+ if (sql[i] === "*" && sql[i + 1] === "/") {
34681
+ depth--;
34682
+ i += 2;
34683
+ continue;
34684
+ }
34685
+ i++;
34686
+ }
34687
+ continue;
34688
+ }
34689
+ if (dialect === "postgresql" && char === "$") {
34690
+ const delimiter = dollarQuoteDelimiterAt(sql, i);
34691
+ if (delimiter) {
34692
+ i += delimiter.length;
34693
+ const closingIndex = sql.indexOf(delimiter, i);
34694
+ i = closingIndex === -1 ? sql.length : closingIndex + delimiter.length;
34695
+ continue;
34696
+ }
34697
+ }
34698
+ if (char === "'") {
34699
+ i++;
34700
+ while (i < sql.length) {
34701
+ if (sql[i] === "'") {
34702
+ if (sql[i + 1] === "'") {
34703
+ i += 2;
34704
+ continue;
34705
+ }
34706
+ i++;
34707
+ break;
34708
+ }
34709
+ if (backslashEscapes && sql[i] === "\\") {
34710
+ i += 2;
34711
+ continue;
34712
+ }
34713
+ i++;
34714
+ }
34715
+ continue;
34716
+ }
34717
+ if (char === '"' || char === "`") {
34718
+ const quote = char;
34719
+ i++;
34720
+ let value = "";
34721
+ while (i < sql.length) {
34722
+ if (sql[i] === quote) {
34723
+ if (sql[i + 1] === quote) {
34724
+ value += quote;
34725
+ i += 2;
34726
+ continue;
34727
+ }
34728
+ i++;
34729
+ break;
34730
+ }
34731
+ if (backslashEscapes && sql[i] === "\\") {
34732
+ value += sql[i + 1] ?? "";
34733
+ i += 2;
34734
+ continue;
34735
+ }
34736
+ value += sql[i];
34737
+ i++;
34738
+ }
34739
+ tokens.push({ value, kind: "identifier", quoted: true });
34740
+ continue;
34741
+ }
34742
+ if (IDENTIFIER_START2.test(char)) {
34743
+ let value = "";
34744
+ while (i < sql.length && IDENTIFIER_PART.test(sql[i])) {
34745
+ value += sql[i];
34746
+ i++;
34747
+ }
34748
+ tokens.push({ value, kind: "identifier", quoted: false });
34749
+ continue;
34750
+ }
34751
+ if (char === "." || char === "," || char === "(" || char === ")" || char === ";") {
34752
+ tokens.push({ value: char, kind: "punctuation", quoted: false });
34753
+ i++;
34754
+ continue;
34755
+ }
34756
+ i++;
34757
+ }
34758
+ return tokens;
34759
+ }
34760
+ function isKeyword(token, keywords) {
34761
+ if (!token || token.kind !== "identifier" || token.quoted)
34762
+ return false;
34763
+ return keywords.has(token.value.toUpperCase());
34764
+ }
34765
+ function isPunctuation(token, value) {
34766
+ return token?.kind === "punctuation" && token.value === value;
34767
+ }
34768
+ function readQualifiedName(tokens, index) {
34769
+ const first = tokens[index];
34770
+ if (!first || first.kind !== "identifier")
34771
+ return null;
34772
+ const parts = [first.value];
34773
+ let cursor = index + 1;
34774
+ while (isPunctuation(tokens[cursor], ".") && tokens[cursor + 1]?.kind === "identifier") {
34775
+ parts.push(tokens[cursor + 1].value);
34776
+ cursor += 2;
34777
+ }
34778
+ return { parts, next: cursor };
34779
+ }
34780
+ function decodedVariants(value) {
34781
+ const decoded = value.replace(ANY_ESCAPE_SEQUENCE, (whole, _escape, long, short) => fromCodePointOrRaw(long ?? short, whole));
34782
+ return decoded === value ? [] : [decoded];
34783
+ }
34784
+ function fromCodePointOrRaw(hex, whole) {
34785
+ const codePoint = parseInt(hex, 16);
34786
+ return codePoint <= 1114111 ? String.fromCodePoint(codePoint) : whole;
34787
+ }
34788
+ function extractTableReferences(sql, options = {}) {
34789
+ const seen = new Set;
34790
+ const references = [];
34791
+ const record = (name2) => {
34792
+ const key = name2.toLowerCase();
34793
+ if (name2.length === 0 || seen.has(key))
34794
+ return;
34795
+ seen.add(key);
34796
+ references.push(name2);
34797
+ };
34798
+ const dialects = options.dialect ? [options.dialect] : ["postgresql", "mysql", undefined];
34799
+ for (const dialect of dialects) {
34800
+ for (const backslashEscapes of [false, true]) {
34801
+ collectReferences(tokenize(sql, dialect, backslashEscapes), record);
34802
+ }
34803
+ }
34804
+ return references;
34805
+ }
34806
+ function collectReferences(tokens, record) {
34807
+ const recordName = (parts) => {
34808
+ const bare = parts[parts.length - 1];
34809
+ record(bare);
34810
+ for (const variant of decodedVariants(bare))
34811
+ record(variant);
34812
+ if (parts.length > 1)
34813
+ record(parts.join("."));
34814
+ };
34815
+ let i = 0;
34816
+ while (i < tokens.length) {
34817
+ if (!isKeyword(tokens[i], TABLE_INTRODUCERS)) {
34818
+ i++;
34819
+ continue;
34820
+ }
34821
+ const introducer = tokens[i].value.toUpperCase();
34822
+ const parenMeansFunction = introducer === "FROM" || introducer === "JOIN";
34823
+ let cursor = i + 1;
34824
+ while (isKeyword(tokens[cursor], PRE_TABLE_NOISE))
34825
+ cursor++;
34826
+ while (parenMeansFunction && isPunctuation(tokens[cursor], "(") && !isKeyword(tokens[cursor + 1], SUBQUERY_OPENERS) && tokens[cursor + 1]?.kind === "identifier") {
34827
+ cursor++;
34828
+ }
34829
+ let expectTable = true;
34830
+ while (expectTable) {
34831
+ expectTable = false;
34832
+ const name2 = readQualifiedName(tokens, cursor);
34833
+ if (!name2)
34834
+ break;
34835
+ const isFunctionCall = parenMeansFunction && isPunctuation(tokens[name2.next], "(");
34836
+ if (!isFunctionCall)
34837
+ recordName(name2.parts);
34838
+ cursor = name2.next;
34839
+ if (isFunctionCall)
34840
+ break;
34841
+ if (isKeyword(tokens[cursor], AS_KEYWORD))
34842
+ cursor++;
34843
+ if (tokens[cursor]?.kind === "identifier" && !isKeyword(tokens[cursor], POST_TABLE_KEYWORDS)) {
34844
+ cursor++;
34845
+ }
34846
+ while (isPunctuation(tokens[cursor], "(")) {
34847
+ let depth = 0;
34848
+ do {
34849
+ if (isPunctuation(tokens[cursor], "("))
34850
+ depth++;
34851
+ else if (isPunctuation(tokens[cursor], ")"))
34852
+ depth--;
34853
+ cursor++;
34854
+ } while (depth > 0 && cursor < tokens.length);
34855
+ if (tokens[cursor]?.kind === "identifier" && !isKeyword(tokens[cursor], POST_TABLE_KEYWORDS))
34856
+ cursor++;
34857
+ }
34858
+ if (isPunctuation(tokens[cursor], ",")) {
34859
+ cursor++;
34860
+ expectTable = true;
34861
+ }
34862
+ }
34863
+ i = Math.max(cursor, i + 1);
34864
+ }
34865
+ let index = 0;
34866
+ while (index < tokens.length) {
34867
+ const token = tokens[index];
34868
+ if (!token || token.kind !== "identifier") {
34869
+ index++;
34870
+ continue;
34871
+ }
34872
+ const name2 = readQualifiedName(tokens, index);
34873
+ for (let part = 0;part < name2.parts.length; part++) {
34874
+ const value = name2.parts[part];
34875
+ const isQuoted = tokens[index + part * 2]?.quoted === true;
34876
+ if (isQuoted || !RESERVED_KEYWORDS.has(value.toUpperCase())) {
34877
+ record(value);
34878
+ for (const variant of decodedVariants(value))
34879
+ record(variant);
34880
+ }
34881
+ }
34882
+ if (name2.parts.length > 1)
34883
+ record(name2.parts.join("."));
34884
+ index = name2.next;
34885
+ }
34886
+ }
34887
+ var TABLE_INTRODUCERS, PRE_TABLE_NOISE, SUBQUERY_OPENERS, AS_KEYWORD, POST_TABLE_KEYWORDS, RESERVED_KEYWORDS, IDENTIFIER_START2, IDENTIFIER_PART, ANY_ESCAPE_SEQUENCE;
34888
+ var init_sql_tables = __esm(() => {
34889
+ init_sql_lexical();
34890
+ TABLE_INTRODUCERS = new Set([
34891
+ "FROM",
34892
+ "JOIN",
34893
+ "INTO",
34894
+ "UPDATE",
34895
+ "TABLE",
34896
+ "TRUNCATE",
34897
+ "COPY",
34898
+ "USING",
34899
+ "STRAIGHT_JOIN"
34900
+ ]);
34901
+ PRE_TABLE_NOISE = new Set(["ONLY", "LATERAL", "TABLE"]);
34902
+ SUBQUERY_OPENERS = new Set(["SELECT", "WITH", "VALUES", "TABLE"]);
34903
+ AS_KEYWORD = new Set(["AS"]);
34904
+ POST_TABLE_KEYWORDS = new Set([
34905
+ "AS",
34906
+ "ON",
34907
+ "USING",
34908
+ "WHERE",
34909
+ "GROUP",
34910
+ "ORDER",
34911
+ "HAVING",
34912
+ "LIMIT",
34913
+ "OFFSET",
34914
+ "FETCH",
34915
+ "WINDOW",
34916
+ "UNION",
34917
+ "INTERSECT",
34918
+ "EXCEPT",
34919
+ "JOIN",
34920
+ "INNER",
34921
+ "LEFT",
34922
+ "RIGHT",
34923
+ "FULL",
34924
+ "OUTER",
34925
+ "CROSS",
34926
+ "NATURAL",
34927
+ "STRAIGHT_JOIN",
34928
+ "SET",
34929
+ "VALUES",
34930
+ "SELECT",
34931
+ "RETURNING",
34932
+ "FOR",
34933
+ "INTO",
34934
+ "PARTITION",
34935
+ "WITH",
34936
+ "TABLESAMPLE",
34937
+ "FORCE",
34938
+ "IGNORE",
34939
+ "USE"
34940
+ ]);
34941
+ RESERVED_KEYWORDS = new Set([
34942
+ "ALL",
34943
+ "AND",
34944
+ "AS",
34945
+ "ASC",
34946
+ "CASE",
34947
+ "CROSS",
34948
+ "DESC",
34949
+ "DISTINCT",
34950
+ "ELSE",
34951
+ "FALSE",
34952
+ "FOR",
34953
+ "FROM",
34954
+ "GROUP",
34955
+ "HAVING",
34956
+ "IN",
34957
+ "INNER",
34958
+ "INTO",
34959
+ "IS",
34960
+ "JOIN",
34961
+ "LEFT",
34962
+ "LIKE",
34963
+ "LIMIT",
34964
+ "NOT",
34965
+ "NULL",
34966
+ "ON",
34967
+ "OR",
34968
+ "ORDER",
34969
+ "OUTER",
34970
+ "RIGHT",
34971
+ "SELECT",
34972
+ "THEN",
34973
+ "TRUE",
34974
+ "UNION",
34975
+ "USING",
34976
+ "VALUES",
34977
+ "WHEN",
34978
+ "WHERE",
34979
+ "WITH"
34980
+ ]);
34981
+ IDENTIFIER_START2 = /[A-Za-z_\u0080-\uFFFF]/;
34982
+ IDENTIFIER_PART = /[A-Za-z0-9_$\u0080-\uFFFF]/;
34983
+ ANY_ESCAPE_SEQUENCE = /([^0-9a-fA-F+'"\s])(?:\+([0-9a-fA-F]{6})|([0-9a-fA-F]{4}))/g;
34984
+ });
34985
+
34436
34986
  // src/core/limits.ts
34437
34987
  var DEFAULT_QUERY_ONLY_LIMIT = 1000;
34438
34988
 
@@ -34486,11 +35036,9 @@ class QueryExecutor {
34486
35036
  }
34487
35037
  }
34488
35038
  }
34489
- if (this.blacklistValidator) {
34490
- const tableName = extractTableName(sql);
34491
- if (tableName) {
34492
- this.blacklistValidator.checkTableBlacklist(classification.type, tableName, []);
34493
- }
35039
+ const referencedTables = this.blacklistValidator ? extractTableReferences(sql, { dialect: this.resolveDialect() }) : [];
35040
+ if (this.blacklistValidator && referencedTables.length > 0) {
35041
+ this.blacklistValidator.checkTablesBlacklist(classification.type, referencedTables);
34494
35042
  }
34495
35043
  const resultData = await this.adapter.execute(executeSql);
34496
35044
  const executionTimeMs = Math.round(performance.now() - start);
@@ -34503,15 +35051,12 @@ class QueryExecutor {
34503
35051
  let securityNotification;
34504
35052
  let omittedColumns = [];
34505
35053
  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
- }
35054
+ const filterResult = this.blacklistValidator.filterColumnsForTables(referencedTables, rows, columnNames);
35055
+ filteredRows = filterResult.filteredRows;
35056
+ if (filterResult.omittedColumns.length > 0) {
35057
+ omittedColumns = filterResult.omittedColumns;
35058
+ columnNames = columnNames.filter((col) => !filterResult.omittedColumns.includes(col));
35059
+ securityNotification = this.blacklistValidator.buildSecurityNotification(referencedTables[0] ?? "", filterResult.omittedColumns);
34515
35060
  }
34516
35061
  }
34517
35062
  if (options?.fieldSelection) {
@@ -34617,13 +35162,30 @@ function inferColumnType(value) {
34617
35162
  var init_query_executor = __esm(() => {
34618
35163
  init_permission_guard();
34619
35164
  init_error_suggester();
34620
- init_engine_hints();
35165
+ init_sql_tables();
34621
35166
  init_applied_limit();
34622
35167
  init_integration_helper();
34623
35168
  init_field_projection();
34624
35169
  });
34625
35170
 
34626
35171
  // src/core/mongo/field-masker.ts
35172
+ function maskMongoRowsForCollections(rows, collections, blacklist) {
35173
+ const columns = blacklist.columns ?? {};
35174
+ return collections.reduce((masked, entry) => {
35175
+ const scope = typeof entry === "string" ? { collection: entry } : entry;
35176
+ const atTopLevel = maskMongoRows(masked, scope.collection, blacklist);
35177
+ if (!scope.prefix)
35178
+ return atTopLevel;
35179
+ const rules = columns[scope.collection] ?? findCaseInsensitive(columns, scope.collection);
35180
+ if (!rules || rules.length === 0)
35181
+ return atTopLevel;
35182
+ const prefixKey = `\x00${scope.collection}@${scope.prefix}`;
35183
+ return maskMongoRows(atTopLevel, prefixKey, {
35184
+ ...blacklist,
35185
+ columns: { ...columns, [prefixKey]: rules.map((rule) => `${scope.prefix}.${rule}`) }
35186
+ });
35187
+ }, rows);
35188
+ }
34627
35189
  function maskMongoRows(rows, collection, blacklist) {
34628
35190
  const columns = blacklist.columns ?? {};
34629
35191
  const raw = columns[collection] ?? findCaseInsensitive(columns, collection);
@@ -34676,6 +35238,86 @@ function findCaseInsensitive(columns, name2) {
34676
35238
  var REDACTED2 = "[REDACTED]";
34677
35239
  var init_field_masker = () => {};
34678
35240
 
35241
+ // src/core/mongo/collection-references.ts
35242
+ function recordTarget(value, found) {
35243
+ if (typeof value === "string") {
35244
+ if (value.length > 0)
35245
+ found.add(value);
35246
+ return;
35247
+ }
35248
+ if (value === null || typeof value !== "object")
35249
+ return;
35250
+ const record = value;
35251
+ for (const field of COLLECTION_FIELDS) {
35252
+ const target = record[field];
35253
+ if (typeof target === "string" && target.length > 0)
35254
+ found.add(target);
35255
+ else if (target !== null && typeof target === "object")
35256
+ recordTarget(target, found);
35257
+ }
35258
+ }
35259
+ function joinPrefix(parent, child) {
35260
+ if (!parent)
35261
+ return child;
35262
+ if (!child)
35263
+ return parent;
35264
+ return `${parent}.${child}`;
35265
+ }
35266
+ function collect2(pipeline, found, scopes, parentPrefix) {
35267
+ if (!Array.isArray(pipeline))
35268
+ return;
35269
+ for (const stage of pipeline) {
35270
+ if (stage === null || typeof stage !== "object")
35271
+ continue;
35272
+ const record = stage;
35273
+ for (const name2 of COLLECTION_STAGES) {
35274
+ if (!Object.prototype.hasOwnProperty.call(record, name2))
35275
+ continue;
35276
+ const stageTargets = new Set;
35277
+ recordTarget(record[name2], stageTargets);
35278
+ for (const collection of stageTargets)
35279
+ found.add(collection);
35280
+ const value = record[name2];
35281
+ const as = value !== null && typeof value === "object" ? value["as"] : undefined;
35282
+ const prefix = joinPrefix(parentPrefix, typeof as === "string" && as ? as : undefined);
35283
+ for (const collection of stageTargets) {
35284
+ scopes.push({ collection, ...prefix ? { prefix } : {} });
35285
+ }
35286
+ }
35287
+ for (const holder of SUB_PIPELINE_HOLDERS2) {
35288
+ const value = record[holder];
35289
+ if (value === undefined || value === null)
35290
+ continue;
35291
+ if (Array.isArray(value)) {
35292
+ collect2(value, found, scopes, parentPrefix);
35293
+ continue;
35294
+ }
35295
+ if (typeof value !== "object")
35296
+ continue;
35297
+ for (const [key, nested] of Object.entries(value)) {
35298
+ const holderPrefix = holder === "$facet" ? joinPrefix(parentPrefix, key) : joinPrefix(parentPrefix, typeof value["as"] === "string" ? value["as"] : undefined);
35299
+ collect2(nested, found, scopes, holderPrefix);
35300
+ }
35301
+ }
35302
+ }
35303
+ }
35304
+ function findMongoCollectionReferences(pipeline) {
35305
+ const found = new Set;
35306
+ collect2(pipeline, found, []);
35307
+ return Array.from(found);
35308
+ }
35309
+ function findMongoCollectionScopes(pipeline) {
35310
+ const scopes = [];
35311
+ collect2(pipeline, new Set, scopes);
35312
+ return scopes;
35313
+ }
35314
+ var COLLECTION_FIELDS, SUB_PIPELINE_HOLDERS2, COLLECTION_STAGES;
35315
+ var init_collection_references = __esm(() => {
35316
+ COLLECTION_FIELDS = ["from", "coll", "into"];
35317
+ SUB_PIPELINE_HOLDERS2 = ["$facet", "$lookup", "$unionWith", "$graphLookup"];
35318
+ COLLECTION_STAGES = ["$lookup", "$unionWith", "$graphLookup", "$out", "$merge"];
35319
+ });
35320
+
34679
35321
  // src/core/query-input.ts
34680
35322
  function trimOuterWhitespace(value) {
34681
35323
  let start = 0;
@@ -34857,6 +35499,13 @@ var init_query_size_guard = __esm(() => {
34857
35499
  import crypto3 from "crypto";
34858
35500
  import { tmpdir } from "os";
34859
35501
  import { join as join23 } from "path";
35502
+ function mongoCollectionRefs(query) {
35503
+ try {
35504
+ return findMongoCollectionScopes(JSON.parse(query));
35505
+ } catch {
35506
+ return [];
35507
+ }
35508
+ }
34860
35509
  function requireSqlConnection2(connection) {
34861
35510
  if (!["postgresql", "mysql", "mariadb"].includes(connection.system)) {
34862
35511
  throw new Error(`This command requires a SQL connection, got: ${connection.system}`);
@@ -35008,17 +35657,22 @@ async function preflightQuery(query, options, context, fieldSelection, multiConn
35008
35657
  await preflightSqlSizeGuard(query, options, config);
35009
35658
  }
35010
35659
  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)
35660
+ if (!config.schema || options.noLimit)
35017
35661
  return;
35662
+ const { extractTableReferences: extractTableReferences2 } = await Promise.resolve().then(() => (init_sql_tables(), exports_sql_tables));
35663
+ const { SQL_DIALECTS: SQL_DIALECTS2 } = await Promise.resolve().then(() => (init_permission_guard(), exports_permission_guard));
35664
+ const dialect = SQL_DIALECTS2.find((candidate) => candidate === config.connection?.system);
35665
+ const tables = extractTableReferences2(query, { ...dialect ? { dialect } : {} });
35666
+ const schema = config.schema;
35018
35667
  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}`);
35668
+ for (const table of tables) {
35669
+ const tableSchema = schema[table];
35670
+ if (!tableSchema)
35671
+ continue;
35672
+ const guard = shouldBlockQuery2(query, tableSchema);
35673
+ if (guard.blocked)
35674
+ throw new Error(`\u26A0 ${guard.reason}`);
35675
+ }
35022
35676
  }
35023
35677
  async function preflightMongoQuery(query, options, config, multiConnection) {
35024
35678
  const collection = options.collection;
@@ -35044,7 +35698,10 @@ async function preflightMongoQuery(query, options, config, multiConnection) {
35044
35698
  context: "MongoDB multi-connection pipelines"
35045
35699
  });
35046
35700
  const blacklistValidator = new BlacklistValidator(new BlacklistManager(config));
35047
- blacklistValidator.checkTableBlacklist("SELECT", collection, []);
35701
+ blacklistValidator.checkTablesBlacklist("SELECT", [
35702
+ collection,
35703
+ ...findMongoCollectionReferences(parsedQuery)
35704
+ ]);
35048
35705
  if (!config.schema || options.noLimit)
35049
35706
  return;
35050
35707
  const tableSchema = config.schema[collection];
@@ -35068,7 +35725,7 @@ function preflightElasticsearchQuery(query, options, config, multiConnection) {
35068
35725
  JSON.parse(body);
35069
35726
  enforceElasticsearchPermission({ method: "POST", apiPath: `/${indexName}/_search`, body }, multiConnection ? "query-only" : config.permission);
35070
35727
  const blacklistValidator = new BlacklistValidator(new BlacklistManager(config));
35071
- blacklistValidator.checkTableBlacklist("SELECT", indexName, []);
35728
+ blacklistValidator.checkIndexBlacklist("SELECT", indexName);
35072
35729
  }
35073
35730
  async function executeConnectionQuery(query, options, context, fieldSelection) {
35074
35731
  const { config, configPath, connectionName } = context;
@@ -35221,7 +35878,7 @@ async function mongoQueryBranch(queryStr, options, context, fieldSelection) {
35221
35878
  const limitedResult = appliedLimit === undefined ? undefined : trimAppliedLimit(result.rows, appliedLimit);
35222
35879
  const visibleRows = limitedResult?.rows ?? result.rows;
35223
35880
  const blacklistCfg = config.blacklist ?? { tables: [], columns: {} };
35224
- const maskedRows = maskMongoRows(visibleRows, collection, blacklistCfg);
35881
+ const maskedRows = maskMongoRowsForCollections(visibleRows, [collection, ...mongoCollectionRefs(queryStr)], blacklistCfg);
35225
35882
  const projected = fieldSelection ? projectRows(maskedRows, fieldSelection) : undefined;
35226
35883
  const outputRows = projected?.rows ?? maskedRows;
35227
35884
  const columnNames = projected?.columnNames ?? (outputRows[0] ? Object.keys(outputRows[0]) : []);
@@ -35325,7 +35982,7 @@ async function elasticsearchQueryBranch(queryStr, options, context) {
35325
35982
  const limitedResult = appliedLimit === undefined ? undefined : trimAppliedLimit(result.rows, appliedLimit);
35326
35983
  const visibleRows = limitedResult?.rows ?? result.rows;
35327
35984
  const columnNames = visibleRows[0] ? Object.keys(visibleRows[0]) : [];
35328
- const filterResult = blacklistValidator.filterColumns(indexName, visibleRows, columnNames);
35985
+ const filterResult = blacklistValidator.filterColumnsForIndexExpression(indexName, visibleRows, columnNames);
35329
35986
  const queryResult = {
35330
35987
  rows: filterResult.filteredRows,
35331
35988
  rowCount: filterResult.filteredRows.length,
@@ -35387,6 +36044,7 @@ var init_query = __esm(() => {
35387
36044
  init_applied_limit();
35388
36045
  init_integration_helper();
35389
36046
  init_field_masker();
36047
+ init_collection_references();
35390
36048
  init_types4();
35391
36049
  init_query_input();
35392
36050
  init_field_projection();
@@ -35950,7 +36608,8 @@ async function qMongoBranch(snippet, prepared, options, config) {
35950
36608
  });
35951
36609
  const blacklistManager = new BlacklistManager(config);
35952
36610
  const blacklistValidator = new BlacklistValidator(blacklistManager);
35953
- blacklistValidator.checkTableBlacklist("SELECT", collection);
36611
+ const collections = [collection, ...findMongoCollectionReferences(parsedBody)];
36612
+ blacklistValidator.checkTablesBlacklist("SELECT", collections);
35954
36613
  if (options.dryRun) {
35955
36614
  console.log(`Dry-run preview (no execution):`);
35956
36615
  console.log(`Collection: ${collection}`);
@@ -35965,7 +36624,7 @@ async function qMongoBranch(snippet, prepared, options, config) {
35965
36624
  const result = await adapter.execute(prepared.driver.sql, [collection]);
35966
36625
  const executionTimeMs = Math.round(performance.now() - start);
35967
36626
  const blacklistCfg = config.blacklist ?? { tables: [], columns: {} };
35968
- const masked = maskMongoRows(result.rows, collection, blacklistCfg);
36627
+ const masked = maskMongoRowsForCollections(result.rows, [collection, ...findMongoCollectionScopes(parsedBody)], blacklistCfg);
35969
36628
  const securityNotification = (blacklistCfg.columns[collection] ?? []).length > 0 ? "Some fields may have been redacted as [REDACTED] per .dbcli blacklist." : undefined;
35970
36629
  if (options.ui || options.format === "html") {
35971
36630
  const html = await generateHtmlReport({
@@ -36006,6 +36665,7 @@ var init_q_mongo = __esm(() => {
36006
36665
  init_adapters();
36007
36666
  init_blacklist_validator();
36008
36667
  init_field_masker();
36668
+ init_collection_references();
36009
36669
  init_formatters();
36010
36670
  init_html_formatter();
36011
36671
  init_opener();
@@ -36053,7 +36713,8 @@ async function qCommand(name2, options, command) {
36053
36713
  config = await configModule.read(configPath);
36054
36714
  if (!config.connection)
36055
36715
  throw new Error('Run "dbcli init" first');
36056
- const engine = mapSystemToEngine(config.connection.system);
36716
+ const connectionSystem = config.connection.system;
36717
+ const engine = mapSystemToEngine(connectionSystem);
36057
36718
  const dirs = resolveSnippetDirs(process.cwd());
36058
36719
  const map = await loadSnippets(dirs);
36059
36720
  const snippet = resolveByName(map, name2, engine);
@@ -36088,10 +36749,16 @@ async function qCommand(name2, options, command) {
36088
36749
  const blacklistManager = new BlacklistManager(config);
36089
36750
  const blacklistValidator = new BlacklistValidator(blacklistManager);
36090
36751
  const family = engineFamily(engine);
36091
- const targetName = family === "sql" ? extractTableName(prepared.rewrittenSql) ?? "" : family === "es" ? prepared.execHints?.index ?? "" : "";
36752
+ const sqlDialect = SQL_DIALECTS.find((dialect) => dialect === connectionSystem);
36753
+ const targets = family === "sql" ? extractTableReferences(prepared.rewrittenSql, {
36754
+ ...sqlDialect ? { dialect: sqlDialect } : {}
36755
+ }) : family === "es" ? [prepared.execHints?.index ?? ""] : [];
36756
+ const targetName = targets[0] ?? "";
36092
36757
  targetNameForAudit = targetName || name2;
36093
- if (family !== "redis" && targetName) {
36094
- blacklistValidator.checkTableBlacklist("SELECT", targetName);
36758
+ if (family === "es") {
36759
+ blacklistValidator.checkIndexBlacklist("SELECT", targetName);
36760
+ } else if (family !== "redis" && targets.length > 0) {
36761
+ blacklistValidator.checkTablesBlacklist("SELECT", targets);
36095
36762
  }
36096
36763
  const adapter = AdapterFactory.createAdapter(config.connection);
36097
36764
  await adapter.connect();
@@ -36106,7 +36773,7 @@ async function qCommand(name2, options, command) {
36106
36773
  const limitedResult = prepared.guardLimit === undefined ? undefined : trimAppliedLimit(result.rows, prepared.guardLimit);
36107
36774
  const resultRows = limitedResult?.rows ?? result.rows;
36108
36775
  const columnNames = resultRows[0] ? Object.keys(resultRows[0]) : [];
36109
- const filtered = family === "redis" ? { filteredRows: resultRows, omittedColumns: [] } : blacklistValidator.filterColumns(targetName, resultRows, columnNames);
36776
+ const filtered = family === "redis" ? { filteredRows: resultRows, omittedColumns: [] } : family === "es" ? blacklistValidator.filterColumnsForIndexExpression(targetName, resultRows, columnNames) : blacklistValidator.filterColumnsForTables(targets, resultRows, columnNames);
36110
36777
  const securityNotification = family === "redis" || filtered.omittedColumns.length === 0 ? undefined : blacklistValidator.buildSecurityNotification(targetName, filtered.omittedColumns);
36111
36778
  if (options.ui || options.format === "html") {
36112
36779
  const html = await generateHtmlReport({
@@ -36166,6 +36833,11 @@ async function qCommand(name2, options, command) {
36166
36833
  } else {
36167
36834
  try {
36168
36835
  console.error(colors.dim(`Executing verification query: ${verifySpec.query}`));
36836
+ if (family === "sql") {
36837
+ blacklistValidator.checkTablesBlacklist("SELECT", extractTableReferences(verifySpec.query, {
36838
+ ...sqlDialect ? { dialect: sqlDialect } : {}
36839
+ }));
36840
+ }
36169
36841
  const verifyResult = await adapter.execute(verifySpec.query);
36170
36842
  const firstRow = verifyResult.rows[0];
36171
36843
  const evalResult = evaluateExpectation(firstRow, verifySpec.expects);
@@ -36348,7 +37020,7 @@ var init_q = __esm(() => {
36348
37020
  init_blacklist_validator();
36349
37021
  init_blacklist();
36350
37022
  init_permission_guard();
36351
- init_engine_hints();
37023
+ init_sql_tables();
36352
37024
  init_formatters();
36353
37025
  init_html_formatter();
36354
37026
  init_opener();
@@ -39186,7 +39858,8 @@ async function exportCommand(sql, options, command) {
39186
39858
  }
39187
39859
  const adapter = AdapterFactory.createSqlAdapter(requireSqlConnection6(config.connection));
39188
39860
  await adapter.connect();
39189
- const executor3 = new QueryExecutor(adapter, config.permission, undefined, config, {
39861
+ const blacklistValidator = new BlacklistValidator(new BlacklistManager(config));
39862
+ const executor3 = new QueryExecutor(adapter, config.permission, blacklistValidator, config, {
39190
39863
  recovery: options.recovery,
39191
39864
  deferDiagnostics: true
39192
39865
  });
@@ -39329,15 +40002,19 @@ async function esExportBranch(query, options, config) {
39329
40002
  let rowCount;
39330
40003
  const diagnostics = [];
39331
40004
  try {
40005
+ const declaredTarget = query.trim().startsWith("{") ? options.index ?? options.collection : query.trim();
40006
+ if (declaredTarget)
40007
+ blacklistValidator.checkIndexBlacklist("SELECT", declaredTarget);
39332
40008
  const {
39333
40009
  rows: fetched,
39334
40010
  target,
39335
40011
  cap: cap2
39336
40012
  } = await buildEsExportRows(query, options, adapter);
39337
- blacklistValidator.checkTableBlacklist("SELECT", target, []);
40013
+ blacklistValidator.checkIndexBlacklist("SELECT", target);
39338
40014
  const limitedResult = cap2 === undefined ? undefined : trimAppliedLimit(fetched, cap2);
39339
40015
  assertExportNotSilentlyTruncated(limitedResult?.metadata, options);
39340
- const rows = limitedResult?.rows ?? fetched;
40016
+ const visibleRows = limitedResult?.rows ?? fetched;
40017
+ const rows = blacklistValidator.filterColumnsForIndexExpression(target, visibleRows, collectColumnUnion(visibleRows)).filteredRows;
39341
40018
  rowCount = rows.length;
39342
40019
  const columns = collectColumnUnion(rows);
39343
40020
  if (options.format === "html") {
@@ -39393,7 +40070,9 @@ async function mongoExportBranch(query, options, config) {
39393
40070
  });
39394
40071
  const blacklistManager = new BlacklistManager(config);
39395
40072
  const blacklistValidator = new BlacklistValidator(blacklistManager);
39396
- blacklistValidator.checkTableBlacklist("SELECT", collection, []);
40073
+ const mongoScopes = findMongoCollectionScopes(JSON.parse(query));
40074
+ const mongoCollections = [collection, ...findMongoCollectionReferences(JSON.parse(query))];
40075
+ blacklistValidator.checkTablesBlacklist("SELECT", mongoCollections);
39397
40076
  let effectiveLimit;
39398
40077
  if (options.noLimit) {
39399
40078
  effectiveLimit = undefined;
@@ -39413,7 +40092,7 @@ async function mongoExportBranch(query, options, config) {
39413
40092
  const limitedResult = effectiveLimit === undefined ? undefined : trimAppliedLimit(result.rows, effectiveLimit);
39414
40093
  assertExportNotSilentlyTruncated(limitedResult?.metadata, options);
39415
40094
  const blacklistCfg = config.blacklist ?? { tables: [], columns: {} };
39416
- const maskedRows = maskMongoRows(limitedResult?.rows ?? result.rows, collection, blacklistCfg);
40095
+ const maskedRows = maskMongoRowsForCollections(limitedResult?.rows ?? result.rows, [collection, ...mongoScopes], blacklistCfg);
39417
40096
  rowCount = maskedRows.length;
39418
40097
  hasBlacklistedColumns = (blacklistCfg.columns[collection] ?? []).length > 0;
39419
40098
  const visibleColumns = collectColumnUnion(maskedRows);
@@ -39542,6 +40221,7 @@ var init_export = __esm(() => {
39542
40221
  init_integration_helper();
39543
40222
  init_engine_hints();
39544
40223
  init_field_masker();
40224
+ init_collection_references();
39545
40225
  SQL_PATTERN = /^\s*(SELECT|INSERT|UPDATE|DELETE|CREATE|DROP|ALTER|SHOW|DESCRIBE)\b/i;
39546
40226
  });
39547
40227
 
@@ -93845,6 +94525,25 @@ async function runDiagnostic(input) {
93845
94525
  return { ...base, rowCount: 0, rows: [], status: "skipped", reason, durationMs: 0 };
93846
94526
  }
93847
94527
  const family = engineFamily(input.engine);
94528
+ const referencedTables = family === "sql" ? extractTableReferences(prepared.rewrittenSql, {
94529
+ ...ENGINE_DIALECT2[input.engine] ? { dialect: ENGINE_DIALECT2[input.engine] } : {}
94530
+ }) : family === "es" && prepared.execHints?.index ? [prepared.execHints.index] : [];
94531
+ if (input.blacklistValidator && referencedTables.length > 0) {
94532
+ try {
94533
+ input.blacklistValidator.checkTablesBlacklist("SELECT", referencedTables);
94534
+ } catch (err) {
94535
+ if (!(err instanceof BlacklistError))
94536
+ throw err;
94537
+ return {
94538
+ ...base,
94539
+ rowCount: 0,
94540
+ rows: [],
94541
+ status: "skipped",
94542
+ reason: err.message,
94543
+ durationMs: 0
94544
+ };
94545
+ }
94546
+ }
93848
94547
  const start = performance.now();
93849
94548
  const exec2 = (async () => {
93850
94549
  const indexParams = family === "es" && prepared.execHints?.index ? [prepared.execHints.index] : [];
@@ -93880,7 +94579,8 @@ async function runDiagnostic(input) {
93880
94579
  durationMs
93881
94580
  };
93882
94581
  }
93883
- const rows = outcome.rows ?? [];
94582
+ const fetched = outcome.rows ?? [];
94583
+ const rows = input.blacklistValidator ? input.blacklistValidator.filterColumnsForTables(referencedTables, fetched, fetched[0] ? Object.keys(fetched[0]) : []).filteredRows : fetched;
93884
94584
  if (rows.length === 0) {
93885
94585
  return { ...base, rowCount: 0, rows: [], status: "no-data", durationMs };
93886
94586
  }
@@ -93892,9 +94592,17 @@ async function runDiagnostic(input) {
93892
94592
  durationMs
93893
94593
  };
93894
94594
  }
94595
+ var ENGINE_DIALECT2;
93895
94596
  var init_run_diagnostic = __esm(() => {
93896
94597
  init_saved_queries();
93897
94598
  init_strategies();
94599
+ init_sql_tables();
94600
+ init_blacklist();
94601
+ ENGINE_DIALECT2 = {
94602
+ postgres: "postgresql",
94603
+ mysql: "mysql",
94604
+ mariadb: "mariadb"
94605
+ };
93898
94606
  });
93899
94607
 
93900
94608
  // src/core/report/collector.ts
@@ -93950,6 +94658,7 @@ async function collectReport(opts) {
93950
94658
  return finalize({ context, sections: [], warnings, generatedAt });
93951
94659
  }
93952
94660
  const adapter = AdapterFactory.createAdapter(config.connection);
94661
+ const blacklistValidator = new BlacklistValidator(new BlacklistManager(config));
93953
94662
  const sectionEvidence = new Map;
93954
94663
  for (const id of sections)
93955
94664
  sectionEvidence.set(id, []);
@@ -93963,7 +94672,8 @@ async function collectReport(opts) {
93963
94672
  adapter,
93964
94673
  engine,
93965
94674
  timeoutMs: timeout,
93966
- maxRows
94675
+ maxRows,
94676
+ blacklistValidator
93967
94677
  });
93968
94678
  const sectionId = sectionForIntent(ev.intent);
93969
94679
  if (sectionId && sectionEvidence.has(sectionId)) {
@@ -94027,6 +94737,7 @@ var init_collector2 = __esm(() => {
94027
94737
  init_saved_queries();
94028
94738
  init_select_snippets();
94029
94739
  init_run_diagnostic();
94740
+ init_blacklist_validator();
94030
94741
  init_section_map();
94031
94742
  init_types7();
94032
94743
  });
@@ -97934,7 +98645,8 @@ var init_snapshot = __esm(() => {
97934
98645
  init_validation();
97935
98646
  init_blacklist_validator();
97936
98647
  init_query_executor();
97937
- init_engine_hints();
98648
+ init_permission_guard();
98649
+ init_sql_tables();
97938
98650
  init_fingerprint();
97939
98651
  init_serializer();
97940
98652
  init_saved_queries();
@@ -97962,10 +98674,12 @@ var init_snapshot = __esm(() => {
97962
98674
  try {
97963
98675
  const blacklistManager = new BlacklistManager(config);
97964
98676
  const blacklistValidator = new BlacklistValidator(blacklistManager);
98677
+ const sqlDialect = SQL_DIALECTS.find((dialect) => dialect === config.connection?.system);
97965
98678
  const executor3 = new QueryExecutor(adapter, config.permission, blacklistValidator, config, options);
97966
98679
  const result = await executor3.execute(sql, { autoLimit: options.limit !== false });
97967
- const table = extractTableName(sql);
97968
- const redactedColumns = table ? blacklistManager.getBlacklistedColumns(table) : [];
98680
+ const redactedColumns = Array.from(new Set(extractTableReferences(sql, {
98681
+ ...sqlDialect ? { dialect: sqlDialect } : {}
98682
+ }).flatMap((table) => blacklistManager.getBlacklistedColumns(table))));
97969
98683
  const snap = buildFingerprint(result, {
97970
98684
  includeRows: options.rows === true,
97971
98685
  redactedColumns,
@@ -99531,8 +100245,20 @@ function buildRealRunners(ctx) {
99531
100245
  const blacklist = config.blacklist ?? { tables: [], columns: {} };
99532
100246
  const schema = config.schema ?? {};
99533
100247
  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) });
100248
+ const analyze = (sql) => analyzeQueryRisk({
100249
+ sql: sql.trim(),
100250
+ permission: config.permission,
100251
+ blacklist,
100252
+ schemaLookup,
100253
+ dialect: toSqlDialect(config.connection?.system)
100254
+ });
100255
+ const analyzePlan = (sql) => analyzeQueryRisk({
100256
+ sql: sql.trim(),
100257
+ permission: "read-write",
100258
+ blacklist,
100259
+ schemaLookup,
100260
+ dialect: toSqlDialect(config.connection?.system)
100261
+ });
99536
100262
  return {
99537
100263
  blacklistGuard: async (table) => {
99538
100264
  const bm = new BlacklistManager(config);
@@ -99605,7 +100331,13 @@ function buildMigrationRunners(ctx) {
99605
100331
  const blacklist = config.blacklist ?? { tables: [], columns: {} };
99606
100332
  const schema = config.schema ?? {};
99607
100333
  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) });
100334
+ const analyze = (sql) => analyzeQueryRisk({
100335
+ sql: sql.trim(),
100336
+ permission: config.permission,
100337
+ blacklist,
100338
+ schemaLookup,
100339
+ dialect: toSqlDialect(config.connection?.system)
100340
+ });
99609
100341
  return {
99610
100342
  blacklistGuard: async (table) => {
99611
100343
  const bm = new BlacklistManager(config);
@@ -99673,8 +100405,20 @@ function buildRollbackRunners(ctx, input) {
99673
100405
  const blacklist = config.blacklist ?? { tables: [], columns: {} };
99674
100406
  const schema = config.schema ?? {};
99675
100407
  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) });
100408
+ const analyze = (sql) => analyzeQueryRisk({
100409
+ sql: sql.trim(),
100410
+ permission: config.permission,
100411
+ blacklist,
100412
+ schemaLookup,
100413
+ dialect: toSqlDialect(config.connection?.system)
100414
+ });
100415
+ const analyzePlan = (sql) => analyzeQueryRisk({
100416
+ sql: sql.trim(),
100417
+ permission: "read-write",
100418
+ blacklist,
100419
+ schemaLookup,
100420
+ dialect: toSqlDialect(config.connection?.system)
100421
+ });
99678
100422
  const ddlStatementGuard = async (statement, table) => {
99679
100423
  if (!isSingleStatement(statement)) {
99680
100424
  return {
@@ -99786,7 +100530,13 @@ function buildConstraintRunners(ctx, input) {
99786
100530
  const blacklist = config.blacklist ?? { tables: [], columns: {} };
99787
100531
  const schema = config.schema ?? {};
99788
100532
  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) });
100533
+ const analyze = (sql) => analyzeQueryRisk({
100534
+ sql: sql.trim(),
100535
+ permission: config.permission,
100536
+ blacklist,
100537
+ schemaLookup,
100538
+ dialect: toSqlDialect(config.connection?.system)
100539
+ });
99790
100540
  const engine = constraintEngineOf(config.connection.system);
99791
100541
  const violationSql = buildViolationQuery(input, engine);
99792
100542
  const columnsExist = async (table, cols) => {
@@ -103041,17 +103791,20 @@ class ReplEngine {
103041
103791
  };
103042
103792
  }
103043
103793
  }
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
- }
103794
+ const referencedTables = extractTableReferences(sql, {
103795
+ dialect: SQL_DIALECTS.find((dialect) => dialect === this.context.system)
103796
+ });
103797
+ const blacklistValidator = this.config?.blacklist ? new BlacklistValidator(new BlacklistManager(this.config)) : undefined;
103798
+ if (blacklistValidator && referencedTables.length > 0) {
103799
+ try {
103800
+ blacklistValidator.checkTablesBlacklist("SELECT", referencedTables);
103801
+ } catch (error) {
103802
+ if (!(error instanceof BlacklistError))
103803
+ throw error;
103804
+ return {
103805
+ action: "continue",
103806
+ output: import_picocolors3.default.red(t_vars("shell.error_blacklisted", { table: error.message }))
103807
+ };
103055
103808
  }
103056
103809
  }
103057
103810
  const startTime = Date.now();
@@ -103060,8 +103813,11 @@ class ReplEngine {
103060
103813
  noLimit: this.state.noLimit
103061
103814
  });
103062
103815
  const elapsed = Date.now() - startTime;
103063
- const rows = result.rows;
103064
- const columnNames = rows.length > 0 && rows[0] ? Object.keys(rows[0]) : [];
103816
+ const fetched = result.rows;
103817
+ const fetchedColumns = fetched.length > 0 && fetched[0] ? Object.keys(fetched[0]) : [];
103818
+ const filtered = blacklistValidator ? blacklistValidator.filterColumnsForTables(referencedTables, fetched, fetchedColumns) : { filteredRows: fetched, omittedColumns: [] };
103819
+ const rows = filtered.filteredRows;
103820
+ const columnNames = fetchedColumns.filter((col) => !filtered.omittedColumns.includes(col));
103065
103821
  const queryResult = {
103066
103822
  rows,
103067
103823
  rowCount: rows.length,
@@ -103113,10 +103869,6 @@ class ReplEngine {
103113
103869
  }
103114
103870
  return null;
103115
103871
  }
103116
- extractTableName(sql) {
103117
- const match = sql.match(/\b(?:FROM|INTO|UPDATE)\s+["'`]?(\w+)["'`]?/i);
103118
- return match?.[1];
103119
- }
103120
103872
  isConnectionError(error) {
103121
103873
  const e = error;
103122
103874
  const msg = (e.message ?? "").toLowerCase();
@@ -103131,6 +103883,9 @@ var init_repl_engine = __esm(() => {
103131
103883
  init_permission_guard();
103132
103884
  init_query_result_formatter();
103133
103885
  init_message_loader();
103886
+ init_sql_tables();
103887
+ init_blacklist_validator();
103888
+ init_blacklist();
103134
103889
  import_picocolors3 = __toESM(require_picocolors(), 1);
103135
103890
  });
103136
103891
 
@@ -103340,22 +104095,115 @@ function extractIndexFromPath(path6) {
103340
104095
  return;
103341
104096
  return seg.split("?")[0];
103342
104097
  }
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`);
104098
+ function isUnscopedMetadataPath(path6) {
104099
+ const first = path6.replace(/^\//, "").split("/")[0]?.split("?")[0] ?? "";
104100
+ return UNSCOPED_METADATA_PREFIXES.includes(first);
104101
+ }
104102
+ function findIndexNamesInBody(body) {
104103
+ const found = [];
104104
+ const walk = (node) => {
104105
+ if (Array.isArray(node)) {
104106
+ for (const item of node)
104107
+ walk(item);
104108
+ return;
104109
+ }
104110
+ if (node === null || typeof node !== "object")
104111
+ return;
104112
+ for (const [key, value] of Object.entries(node)) {
104113
+ if (key === "_index" || key === "index") {
104114
+ for (const candidate of Array.isArray(value) ? value : [value]) {
104115
+ if (typeof candidate === "string" && candidate.length > 0)
104116
+ found.push(candidate);
104117
+ }
104118
+ }
104119
+ walk(value);
104120
+ }
104121
+ };
104122
+ walk(body);
104123
+ return found;
104124
+ }
104125
+ async function runEsRequest(req, adapter, blacklistTables, blacklistColumns = {}) {
104126
+ const rawPath = req.path.split("?")[0] ?? req.path;
104127
+ const routedPath = normalizeEsPath(rawPath);
104128
+ const index = extractIndexFromPath(routedPath);
104129
+ if (blacklistTables.length > 0) {
104130
+ const literalSegments = `/${rawPath.split("/").filter(Boolean).join("/")}`;
104131
+ if (routedPath !== literalSegments) {
104132
+ throw new Error(`BlacklistRejection: '${req.path}' routes to '${routedPath}', which is not what it ` + `spells. Write the path the server will receive.`);
104133
+ }
104134
+ const blacklistedSegment = routedPath.split("/").find((segment) => segment.length > 0 && indexExpressionReaches(segment, blacklistTables));
104135
+ if (blacklistedSegment !== undefined) {
104136
+ throw new Error(`BlacklistRejection: index '${blacklistedSegment}' is blacklist-protected`);
104137
+ }
104138
+ if (index === undefined) {
104139
+ if (!isUnscopedMetadataPath(routedPath)) {
104140
+ 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.`);
104141
+ }
104142
+ }
104143
+ const inBody = findIndexNamesInBody(req.body).find((name2) => indexExpressionReaches(name2, blacklistTables));
104144
+ if (inBody !== undefined) {
104145
+ throw new Error(`BlacklistRejection: index '${inBody}' is blacklist-protected`);
104146
+ }
104147
+ }
104148
+ const protectedFields = new Set(Object.values(blacklistColumns).flat());
104149
+ if (protectedFields.size > 0) {
104150
+ const named = findStrings(req.body).find((text2) => protectedFields.has(text2));
104151
+ if (named !== undefined) {
104152
+ 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.`);
104153
+ }
103347
104154
  }
103348
104155
  let body = req.body;
103349
104156
  if (req.path.includes("_search") && body !== null && typeof body === "object" && !Array.isArray(body) && body.size === undefined) {
103350
104157
  body = { ...body, size: ES_SHELL_SIZE_CAP };
103351
104158
  }
103352
- return adapter.request(req.method, req.path, body);
104159
+ const response = await adapter.request(req.method, req.path, body);
104160
+ return protectedFields.size === 0 ? response : redactFields(response, protectedFields);
104161
+ }
104162
+ function findStrings(node) {
104163
+ const found = [];
104164
+ const walk = (value) => {
104165
+ if (typeof value === "string") {
104166
+ found.push(value);
104167
+ for (const piece of value.split(/[^A-Za-z0-9_.]+/)) {
104168
+ if (piece.length > 0 && piece !== value)
104169
+ found.push(piece);
104170
+ }
104171
+ return;
104172
+ }
104173
+ if (Array.isArray(value)) {
104174
+ for (const item of value)
104175
+ walk(item);
104176
+ return;
104177
+ }
104178
+ if (value === null || typeof value !== "object")
104179
+ return;
104180
+ for (const [key, nested] of Object.entries(value)) {
104181
+ found.push(key);
104182
+ walk(nested);
104183
+ }
104184
+ };
104185
+ walk(node);
104186
+ return found;
104187
+ }
104188
+ function redactFields(node, fields) {
104189
+ if (Array.isArray(node))
104190
+ return node.map((item) => redactFields(item, fields));
104191
+ if (node === null || typeof node !== "object")
104192
+ return node;
104193
+ const out = {};
104194
+ for (const [key, value] of Object.entries(node)) {
104195
+ if (fields.has(key))
104196
+ continue;
104197
+ out[key] = redactFields(value, fields);
104198
+ }
104199
+ return out;
103353
104200
  }
103354
104201
  async function runEsShell(configPath) {
103355
104202
  const config = await configModule.read(configPath);
103356
104203
  const adapter = AdapterFactory.createElasticsearchAdapter(config.connection);
103357
104204
  await adapter.connect();
103358
104205
  const blacklistTables = config.blacklist?.tables ?? [];
104206
+ const blacklistColumns = config.blacklist?.columns ?? {};
103359
104207
  console.error(import_picocolors4.default.bold("Elasticsearch shell \u2014 Kibana Dev Tools syntax"));
103360
104208
  console.error(import_picocolors4.default.dim('Enter "<METHOD> /<path>" then an optional JSON body; submit with a blank line. Try: GET /_cat/indices'));
103361
104209
  console.error(import_picocolors4.default.dim('Ctrl+C cancels the current block; Ctrl+D or "exit" quits.'));
@@ -103380,7 +104228,7 @@ async function runEsShell(configPath) {
103380
104228
  }
103381
104229
  try {
103382
104230
  const req = parseEsRequest(block2);
103383
- const res = await runEsRequest(req, adapter, blacklistTables);
104231
+ const res = await runEsRequest(req, adapter, blacklistTables, blacklistColumns);
103384
104232
  console.log(JSON.stringify(res, null, 2));
103385
104233
  } catch (error) {
103386
104234
  console.error(import_picocolors4.default.red(error.message));
@@ -103408,11 +104256,13 @@ async function runEsShell(configPath) {
103408
104256
  process.exit(0);
103409
104257
  });
103410
104258
  }
103411
- var import_picocolors4, ES_SHELL_SIZE_CAP = 1000;
104259
+ var import_picocolors4, UNSCOPED_METADATA_PREFIXES, ES_SHELL_SIZE_CAP = 1000;
103412
104260
  var init_es_shell = __esm(() => {
103413
104261
  init_config();
103414
104262
  init_adapters();
104263
+ init_es_index_target();
103415
104264
  import_picocolors4 = __toESM(require_picocolors(), 1);
104265
+ UNSCOPED_METADATA_PREFIXES = ["_cat", "_cluster", "_nodes", "_tasks", "_ingest", "_license"];
103416
104266
  });
103417
104267
 
103418
104268
  // src/core/repl/command-registry.ts