@carllee1983/dbcli 4.0.0 → 6.0.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
@@ -3,7 +3,7 @@
3
3
  // package.json
4
4
  var package_default = {
5
5
  name: "@carllee1983/dbcli",
6
- version: "4.0.0",
6
+ version: "6.0.0",
7
7
  description: "Database CLI for AI agents",
8
8
  type: "module",
9
9
  publishConfig: {
package/dist/core.d.ts CHANGED
@@ -540,10 +540,6 @@ declare const SQL_DIALECTS: readonly [
540
540
  "mariadb"
541
541
  ];
542
542
  type SqlDialect = (typeof SQL_DIALECTS)[number];
543
- /**
544
- * Manager class for loading and querying blacklist rules.
545
- * Instantiate once per CLI invocation.
546
- */
547
543
  export declare class BlacklistManager {
548
544
  private config;
549
545
  private state;
@@ -551,28 +547,49 @@ export declare class BlacklistManager {
551
547
  constructor(config: DbcliConfig, overrideEnvValue?: string);
552
548
  /**
553
549
  * Deserialize config.blacklist JSON into efficient Set/Map structures.
554
- * Case-insensitive table names (stored as lowercase).
555
- * Case-sensitive column names.
550
+ * Case-insensitive table names (stored as lowercase). Column entries are
551
+ * stored as written and folded where they are compared — see ADR-0018.
556
552
  *
557
553
  * @returns BlacklistState with Set<string> for tables, Map<string, Set<string>> for columns
558
554
  */
559
555
  loadBlacklist(): BlacklistState;
560
556
  /**
561
557
  * Check if a table is blacklisted.
562
- * Case-insensitive comparison.
558
+ *
559
+ * Case-insensitive, and each entry is a glob: `secrets*` covers
560
+ * `secrets_2026`. The same array is already a glob for Redis keys and
561
+ * Elasticsearch index expressions, and one config file gets one answer
562
+ * (ADR-0019 Decision 4). `report\*` escapes back to a literal name.
563
563
  *
564
564
  * @param tableName Table name to check
565
565
  * @returns true if the table is blacklisted
566
566
  */
567
567
  isTableBlacklisted(tableName: string): boolean;
568
+ /**
569
+ * Table entries carrying glob metacharacters.
570
+ *
571
+ * Computed with `state` rather than lazily: `getState()` hands the same `Set`
572
+ * out, and a lazy cache filled after someone added to it would leave `Set.has`
573
+ * seeing the new entry and the glob scan not — fail-open, and silently.
574
+ * Nothing mutates it today; the point is that nothing can start to.
575
+ */
576
+ private readonly wildcardTables;
568
577
  /**
569
578
  * Check if a specific column in a table is blacklisted.
570
- * Table name is case-insensitive; column name is case-sensitive.
579
+ * Both are case-insensitive on their first segment. A column entry's later
580
+ * segments are nested object keys and keep their case (ADR-0018).
571
581
  *
572
582
  * @param tableName Table name
573
583
  * @param columnName Column name
574
584
  * @returns true if the column is blacklisted
575
585
  */
586
+ /**
587
+ * The rules for a table, found by the reference as written and by its last
588
+ * segment. `extractTableReferences` keeps a qualified name whole, so a rule
589
+ * under `public.users` did not apply to `SELECT * FROM users` and the reverse
590
+ * was equally true. ADR-0018 Decision 3.
591
+ */
592
+ private columnRulesFor;
576
593
  isColumnBlacklisted(tableName: string, columnName: string): boolean;
577
594
  /**
578
595
  * Get all blacklisted column names for a specific table.
package/dist/core.mjs CHANGED
@@ -2024,6 +2024,224 @@ var messageLoader = MessageLoader.getInstance();
2024
2024
  var t = (key) => messageLoader.t(key);
2025
2025
  var t_vars = (key, vars) => messageLoader.interpolate(key, vars);
2026
2026
 
2027
+ // src/utils/glob.ts
2028
+ function findClassEnd(glob, open) {
2029
+ for (let i = open + 1;i < glob.length; i++) {
2030
+ if (glob[i] === "\\") {
2031
+ i++;
2032
+ continue;
2033
+ }
2034
+ if (glob[i] === "]")
2035
+ return i;
2036
+ }
2037
+ return -1;
2038
+ }
2039
+ function isValidCharacterClass(body) {
2040
+ try {
2041
+ new RegExp(body);
2042
+ return true;
2043
+ } catch {
2044
+ return false;
2045
+ }
2046
+ }
2047
+ var parsedGlobs = new Map;
2048
+ function parseGlob(glob) {
2049
+ const memo = parsedGlobs.get(glob);
2050
+ if (memo !== undefined)
2051
+ return memo;
2052
+ const parsed = parseGlobUncached(glob);
2053
+ parsedGlobs.set(glob, parsed);
2054
+ return parsed;
2055
+ }
2056
+ function parseGlobUncached(glob) {
2057
+ const runs = [];
2058
+ let current = [];
2059
+ let leadingStar = false;
2060
+ let trailingStar = false;
2061
+ let sawStar = false;
2062
+ const endRun = () => {
2063
+ if (current.length > 0)
2064
+ runs.push(current);
2065
+ current = [];
2066
+ };
2067
+ for (let i = 0;i < glob.length; i++) {
2068
+ const c = glob[i];
2069
+ if (c === "\\" && i + 1 < glob.length) {
2070
+ trailingStar = false;
2071
+ current.push({ kind: "literal", char: glob[++i] });
2072
+ continue;
2073
+ }
2074
+ if (c === "*") {
2075
+ if (!sawStar && current.length === 0)
2076
+ leadingStar = true;
2077
+ sawStar = true;
2078
+ trailingStar = true;
2079
+ endRun();
2080
+ continue;
2081
+ }
2082
+ trailingStar = false;
2083
+ if (c === "?") {
2084
+ current.push({ kind: "any" });
2085
+ continue;
2086
+ }
2087
+ if (c === "[") {
2088
+ const end = findClassEnd(glob, i);
2089
+ const body = end === -1 ? "" : glob.slice(i, end + 1);
2090
+ if (body !== "" && isValidCharacterClass(body)) {
2091
+ current.push({ kind: "class", test: new RegExp(`^${body}$`, "s") });
2092
+ i = end;
2093
+ continue;
2094
+ }
2095
+ current.push({ kind: "literal", char: "[" });
2096
+ continue;
2097
+ }
2098
+ current.push({ kind: "literal", char: c });
2099
+ }
2100
+ endRun();
2101
+ return { runs, leadingStar, trailingStar, anchored: !sawStar };
2102
+ }
2103
+ function runMatchesAt(run, text, at) {
2104
+ if (at + run.length > text.length)
2105
+ return false;
2106
+ for (let i = 0;i < run.length; i++) {
2107
+ const token = run[i];
2108
+ const ch = text[at + i];
2109
+ if (token.kind === "literal") {
2110
+ if (token.char !== ch)
2111
+ return false;
2112
+ } else if (token.kind === "class") {
2113
+ if (!token.test.test(ch))
2114
+ return false;
2115
+ }
2116
+ }
2117
+ return true;
2118
+ }
2119
+ function globMatches(glob, text) {
2120
+ const { runs, leadingStar, trailingStar, anchored } = parseGlob(glob);
2121
+ if (anchored) {
2122
+ const run = runs[0] ?? [];
2123
+ return text.length === run.length && runMatchesAt(run, text, 0);
2124
+ }
2125
+ if (runs.length === 0)
2126
+ return true;
2127
+ let cursor = 0;
2128
+ let first = 0;
2129
+ let last = runs.length;
2130
+ if (!leadingStar) {
2131
+ const head = runs[0];
2132
+ if (!runMatchesAt(head, text, 0))
2133
+ return false;
2134
+ cursor = head.length;
2135
+ first = 1;
2136
+ }
2137
+ if (!trailingStar) {
2138
+ const tail = runs[last - 1];
2139
+ const at = text.length - tail.length;
2140
+ if (at < cursor || !runMatchesAt(tail, text, at))
2141
+ return false;
2142
+ last -= 1;
2143
+ }
2144
+ const limit = trailingStar ? text.length : text.length - runs[runs.length - 1].length;
2145
+ for (let r = first;r < last; r++) {
2146
+ const run = runs[r];
2147
+ let found = -1;
2148
+ for (let at = cursor;at + run.length <= limit; at++) {
2149
+ if (runMatchesAt(run, text, at)) {
2150
+ found = at;
2151
+ break;
2152
+ }
2153
+ }
2154
+ if (found === -1)
2155
+ return false;
2156
+ cursor = found + run.length;
2157
+ }
2158
+ return true;
2159
+ }
2160
+ function globNeverMatches(glob) {
2161
+ for (let i = 0;i < glob.length; i++) {
2162
+ const c = glob[i];
2163
+ if (c === "\\") {
2164
+ i++;
2165
+ continue;
2166
+ }
2167
+ if (c !== "[")
2168
+ continue;
2169
+ const end = findClassEnd(glob, i);
2170
+ if (end === -1) {
2171
+ return "unclosed character class";
2172
+ }
2173
+ const body = glob.slice(i, end + 1);
2174
+ if (body === "[]" || body === "[^]")
2175
+ return "empty character class matches nothing";
2176
+ if (!isValidCharacterClass(body))
2177
+ return "invalid character class";
2178
+ i = end;
2179
+ }
2180
+ return null;
2181
+ }
2182
+
2183
+ // src/core/mongo/path-matcher.ts
2184
+ function compilePatterns(raw) {
2185
+ const patterns = [];
2186
+ const rejected = [];
2187
+ for (const entry of raw) {
2188
+ if (typeof entry !== "string" || entry.length === 0) {
2189
+ rejected.push({ raw: String(entry ?? ""), reason: "must be a non-empty string" });
2190
+ continue;
2191
+ }
2192
+ const segments = entry.split(".");
2193
+ if (segments.some((s) => s.length === 0)) {
2194
+ rejected.push({ raw: entry, reason: "empty path segment" });
2195
+ continue;
2196
+ }
2197
+ const globstar = segments.find((seg) => seg.includes("**"));
2198
+ if (globstar !== undefined) {
2199
+ rejected.push({
2200
+ raw: entry,
2201
+ reason: "`**` matches one segment, not a subtree; write `*` or a longer path"
2202
+ });
2203
+ continue;
2204
+ }
2205
+ const dead = segments.map((seg) => globNeverMatches(seg)).find((r) => r !== null);
2206
+ if (dead !== undefined && dead !== null) {
2207
+ rejected.push({ raw: entry, reason: dead });
2208
+ continue;
2209
+ }
2210
+ const wildcardTail = segments.length > 1 && segments[segments.length - 1] === "*";
2211
+ const literal = wildcardTail ? segments.slice(0, -1) : segments;
2212
+ patterns.push({
2213
+ raw: entry,
2214
+ segments: literal,
2215
+ wildcardTail
2216
+ });
2217
+ }
2218
+ return { patterns, rejected };
2219
+ }
2220
+ function matchAny(path, patterns) {
2221
+ if (patterns.length === 0)
2222
+ return false;
2223
+ const pathSegments = path.split(".");
2224
+ for (const pat of patterns) {
2225
+ if (pat.wildcardTail) {
2226
+ if (pathSegments.length < pat.segments.length)
2227
+ continue;
2228
+ } else {
2229
+ if (pat.segments.length !== pathSegments.length)
2230
+ continue;
2231
+ }
2232
+ let ok = true;
2233
+ for (let i = 0;i < pat.segments.length; i++) {
2234
+ if (!globMatches(pat.segments[i], pathSegments[i])) {
2235
+ ok = false;
2236
+ break;
2237
+ }
2238
+ }
2239
+ if (ok)
2240
+ return true;
2241
+ }
2242
+ return false;
2243
+ }
2244
+
2027
2245
  // src/core/mongo/request-fields.ts
2028
2246
  var WHOLE_DOCUMENT_VARIABLES = new Set(["ROOT", "CURRENT"]);
2029
2247
  var RESHAPES_DOCUMENT = new Set(["$objectToArray", "$replaceRoot", "$replaceWith"]);
@@ -2468,57 +2686,6 @@ var REDIS_COMMAND_TABLE = Object.freeze({
2468
2686
  })
2469
2687
  });
2470
2688
 
2471
- // src/utils/glob.ts
2472
- function globToRegex(glob) {
2473
- let out = "^";
2474
- for (let i = 0;i < glob.length; i++) {
2475
- const c = glob[i];
2476
- if (c === "\\" && i + 1 < glob.length) {
2477
- out += glob[++i].replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2478
- continue;
2479
- }
2480
- if (c === "*")
2481
- out += ".*";
2482
- else if (c === "?")
2483
- out += ".";
2484
- else if (c === "[") {
2485
- const end = findClassEnd(glob, i);
2486
- const body = end === -1 ? "" : glob.slice(i, end + 1);
2487
- if (body !== "" && isValidCharacterClass(body)) {
2488
- out += body;
2489
- i = end;
2490
- } else {
2491
- out += "\\[";
2492
- }
2493
- } else if (".^$+(){}|\\".includes(c)) {
2494
- out += "\\" + c;
2495
- } else {
2496
- out += c;
2497
- }
2498
- }
2499
- out += "$";
2500
- return new RegExp(out);
2501
- }
2502
- function findClassEnd(glob, open) {
2503
- for (let i = open + 1;i < glob.length; i++) {
2504
- if (glob[i] === "\\") {
2505
- i++;
2506
- continue;
2507
- }
2508
- if (glob[i] === "]")
2509
- return i;
2510
- }
2511
- return -1;
2512
- }
2513
- function isValidCharacterClass(body) {
2514
- try {
2515
- new RegExp(body);
2516
- return true;
2517
- } catch {
2518
- return false;
2519
- }
2520
- }
2521
-
2522
2689
  // src/adapters/redis/returned-key-names.ts
2523
2690
  var RETURNS_KEY_NAMES = new Set(["SCAN", "KEYS"]);
2524
2691
 
@@ -7977,10 +8144,11 @@ var RedisMaskRuleSchema = exports_external.object({
7977
8144
  var RedisConfigSchema = exports_external.object({
7978
8145
  mask: exports_external.array(RedisMaskRuleSchema).default([])
7979
8146
  }).optional();
8147
+ var DEFAULT_AUDIT_ROTATION = { max_bytes: 10485760, max_entries: 1e4 };
7980
8148
  var AuditRotationConfigSchema = exports_external.object({
7981
- max_bytes: exports_external.number().int().positive().default(10485760),
7982
- max_entries: exports_external.number().int().positive().default(1000)
7983
- }).optional().default({ max_bytes: 10485760, max_entries: 1000 });
8149
+ max_bytes: exports_external.number().int().positive().default(DEFAULT_AUDIT_ROTATION.max_bytes),
8150
+ max_entries: exports_external.number().int().positive().default(DEFAULT_AUDIT_ROTATION.max_entries)
8151
+ }).optional().default({ ...DEFAULT_AUDIT_ROTATION });
7984
8152
  var AuditConfigSchema = exports_external.object({
7985
8153
  enabled: exports_external.boolean().default(true),
7986
8154
  strict: exports_external.boolean().default(false),
@@ -7991,7 +8159,7 @@ var AuditConfigSchema = exports_external.object({
7991
8159
  }).optional().default({
7992
8160
  enabled: true,
7993
8161
  strict: false,
7994
- rotation: { max_bytes: 10485760, max_entries: 1000 }
8162
+ rotation: { ...DEFAULT_AUDIT_ROTATION }
7995
8163
  });
7996
8164
  var DbcliConfigSchema = exports_external.object({
7997
8165
  connection: ConnectionConfigSchema,
@@ -8509,7 +8677,7 @@ function migrateV1ToV2(v1) {
8509
8677
  schemas: {},
8510
8678
  metadata: v1.metadata ?? { version: "2.0" },
8511
8679
  blacklist: v1.blacklist ?? { tables: [], columns: {} },
8512
- audit: v1.audit ?? { enabled: true, rotation: { max_bytes: 10485760, max_entries: 1000 } }
8680
+ audit: v1.audit ?? { enabled: true, rotation: { ...DEFAULT_AUDIT_ROTATION } }
8513
8681
  };
8514
8682
  }
8515
8683
  function upsertConnection(config, input) {
@@ -8781,7 +8949,7 @@ var DEFAULT_CONFIG = {
8781
8949
  audit: {
8782
8950
  enabled: true,
8783
8951
  strict: false,
8784
- rotation: { max_bytes: 10485760, max_entries: 1000 }
8952
+ rotation: { ...DEFAULT_AUDIT_ROTATION }
8785
8953
  }
8786
8954
  };
8787
8955
  function isEnvReference(value) {
@@ -9036,6 +9204,23 @@ DBCLI_PASSWORD=${password}
9036
9204
  }
9037
9205
  };
9038
9206
  // src/core/blacklist-manager.ts
9207
+ function normalizeBlacklistEntry(raw) {
9208
+ const trimmed = raw.trim();
9209
+ const quote = trimmed[0];
9210
+ if ((quote === '"' || quote === "`") && trimmed.length > 1 && trimmed.endsWith(quote)) {
9211
+ return trimmed.slice(1, -1).trim();
9212
+ }
9213
+ return trimmed;
9214
+ }
9215
+ function assertNotTableQualified(tableKey, column) {
9216
+ const dot = column.indexOf(".");
9217
+ if (dot <= 0)
9218
+ return;
9219
+ if (column.slice(0, dot).toLowerCase() !== tableKey.toLowerCase())
9220
+ return;
9221
+ throw new Error(`[BlacklistManager] blacklist.columns["${tableKey}"] entry ${JSON.stringify(column)} is ` + `qualified with its own table and would never match. Write it as ` + `${JSON.stringify(column.slice(dot + 1))}.`);
9222
+ }
9223
+
9039
9224
  class BlacklistManager {
9040
9225
  config;
9041
9226
  state;
@@ -9044,6 +9229,7 @@ class BlacklistManager {
9044
9229
  this.config = config;
9045
9230
  this.overrideEnabled = (overrideEnvValue ?? Bun.env.DBCLI_OVERRIDE_BLACKLIST ?? "") === "true";
9046
9231
  this.state = this.loadBlacklist();
9232
+ this.wildcardTables = [...this.state.tables].filter((entry) => /[*?[\\]/.test(entry));
9047
9233
  }
9048
9234
  loadBlacklist() {
9049
9235
  const tables = new Set;
@@ -9055,7 +9241,7 @@ class BlacklistManager {
9055
9241
  if (Array.isArray(blacklistConfig.tables)) {
9056
9242
  for (const tableName of blacklistConfig.tables) {
9057
9243
  if (typeof tableName === "string") {
9058
- tables.add(tableName.toLowerCase());
9244
+ tables.add(normalizeBlacklistEntry(tableName).toLowerCase());
9059
9245
  } else {
9060
9246
  console.warn(`[BlacklistManager] Invalid table name in blacklist config: ${JSON.stringify(tableName)}`);
9061
9247
  }
@@ -9076,13 +9262,15 @@ class BlacklistManager {
9076
9262
  const columnSet = new Set;
9077
9263
  for (const col of cols) {
9078
9264
  if (typeof col === "string") {
9079
- columnSet.add(col);
9265
+ const entry = normalizeBlacklistEntry(col);
9266
+ assertNotTableQualified(normalizeBlacklistEntry(tableName), entry);
9267
+ columnSet.add(entry);
9080
9268
  } else {
9081
9269
  console.warn(`[BlacklistManager] Invalid column name in blacklist.columns["${tableName}"]: ${JSON.stringify(col)}`);
9082
9270
  }
9083
9271
  }
9084
9272
  if (columnSet.size > 0) {
9085
- columns.set(tableName.toLowerCase(), columnSet);
9273
+ columns.set(normalizeBlacklistEntry(tableName).toLowerCase(), columnSet);
9086
9274
  }
9087
9275
  }
9088
9276
  } else if (blacklistConfig.columns !== undefined) {
@@ -9091,17 +9279,47 @@ class BlacklistManager {
9091
9279
  return { tables, columns };
9092
9280
  }
9093
9281
  isTableBlacklisted(tableName) {
9094
- return this.state.tables.has(tableName.toLowerCase());
9282
+ const name = tableName.toLowerCase();
9283
+ if (this.state.tables.has(name))
9284
+ return true;
9285
+ for (const pattern of this.wildcardTables) {
9286
+ if (globMatches(pattern, name))
9287
+ return true;
9288
+ }
9289
+ return false;
9290
+ }
9291
+ wildcardTables;
9292
+ columnRulesFor(tableName) {
9293
+ const key = normalizeBlacklistEntry(tableName).toLowerCase();
9294
+ const direct = this.state.columns.get(key);
9295
+ if (direct)
9296
+ return direct;
9297
+ const dot = key.lastIndexOf(".");
9298
+ if (dot > 0) {
9299
+ const bare = this.state.columns.get(key.slice(dot + 1));
9300
+ if (bare)
9301
+ return bare;
9302
+ }
9303
+ let merged;
9304
+ for (const [ruleKey, columns] of this.state.columns) {
9305
+ const ruleDot = ruleKey.lastIndexOf(".");
9306
+ if (ruleDot <= 0 || ruleKey.slice(ruleDot + 1) !== key)
9307
+ continue;
9308
+ merged ??= new Set;
9309
+ for (const column of columns)
9310
+ merged.add(column);
9311
+ }
9312
+ return merged;
9095
9313
  }
9096
9314
  isColumnBlacklisted(tableName, columnName) {
9097
- const columnSet = this.state.columns.get(tableName.toLowerCase());
9315
+ const columnSet = this.columnRulesFor(tableName);
9098
9316
  if (!columnSet) {
9099
9317
  return false;
9100
9318
  }
9101
- return columnSet.has(columnName);
9319
+ return columnSet.has(normalizeBlacklistEntry(columnName).toLowerCase());
9102
9320
  }
9103
9321
  getBlacklistedColumns(tableName) {
9104
- const columnSet = this.state.columns.get(tableName.toLowerCase());
9322
+ const columnSet = this.columnRulesFor(tableName);
9105
9323
  if (!columnSet) {
9106
9324
  return [];
9107
9325
  }
@@ -9165,7 +9383,7 @@ function expandIndexTargets(target) {
9165
9383
  function matchesIndexGlob(pattern, name) {
9166
9384
  const normalized = pattern.toLowerCase() === "_all" ? "*" : pattern.toLowerCase();
9167
9385
  try {
9168
- return globToRegex(normalized).test(name.toLowerCase());
9386
+ return globMatches(normalized, name.toLowerCase());
9169
9387
  } catch {
9170
9388
  return true;
9171
9389
  }
@@ -9220,6 +9438,17 @@ function dedupe2(values) {
9220
9438
  }
9221
9439
  return result;
9222
9440
  }
9441
+ function compileGlobRules(rules, fold, table, operation) {
9442
+ const globbed = rules.filter((rule) => /[*?[\\]/.test(rule));
9443
+ if (globbed.length === 0)
9444
+ return [];
9445
+ const { patterns, rejected } = compilePatterns(globbed.map(fold));
9446
+ if (rejected.length > 0) {
9447
+ const detail = rejected.map((r) => `'${r.raw}' (${r.reason})`).join(", ");
9448
+ throw new BlacklistError(`blacklist.columns for '${table}' has entries this matcher cannot read: ${detail}`, table, operation);
9449
+ }
9450
+ return patterns;
9451
+ }
9223
9452
 
9224
9453
  class BlacklistValidator {
9225
9454
  manager;
@@ -9277,7 +9506,27 @@ class BlacklistValidator {
9277
9506
  if (blacklisted.length === 0 || fields.length === 0) {
9278
9507
  return;
9279
9508
  }
9280
- const conflicts = fields.filter((f) => blacklisted.includes(f));
9509
+ const protectedPaths = new Set(blacklisted.map((name) => name.toLowerCase()));
9510
+ const globs = compileGlobRules(blacklisted, (value) => value.toLowerCase(), tableName, operation);
9511
+ const conflicts = fields.filter((field) => {
9512
+ const name = field.toLowerCase();
9513
+ if (protectedPaths.has(name))
9514
+ return true;
9515
+ if (matchAny(name, globs))
9516
+ return true;
9517
+ let dot = name.indexOf(".");
9518
+ while (dot >= 0) {
9519
+ if (dot > 0) {
9520
+ const ancestor = name.slice(0, dot);
9521
+ if (protectedPaths.has(ancestor))
9522
+ return true;
9523
+ if (matchAny(ancestor, globs))
9524
+ return true;
9525
+ }
9526
+ dot = name.indexOf(".", dot + 1);
9527
+ }
9528
+ return false;
9529
+ });
9281
9530
  if (conflicts.length === 0) {
9282
9531
  return;
9283
9532
  }
@@ -9328,11 +9577,20 @@ class BlacklistValidator {
9328
9577
  }
9329
9578
  collect(row);
9330
9579
  }
9331
- const protectedPaths = new Set(blacklistedColumns);
9580
+ const foldHead = (path) => {
9581
+ const dot = path.indexOf(".");
9582
+ return dot < 0 ? path.toLowerCase() : path.slice(0, dot).toLowerCase() + path.slice(dot);
9583
+ };
9584
+ const protectedPaths = new Set(blacklistedColumns.map(foldHead));
9585
+ const globRules = compileGlobRules(blacklistedColumns, foldHead, tables[0] ?? "unknown", "SELECT");
9586
+ const presentByFolded = new Map;
9587
+ for (const column of presentColumns)
9588
+ presentByFolded.set(foldHead(column), column);
9332
9589
  const omitted = new Set;
9333
9590
  for (const path of blacklistedColumns) {
9334
- if (presentColumns.has(path)) {
9335
- omitted.add(path);
9591
+ const present = presentByFolded.get(foldHead(path));
9592
+ if (present !== undefined) {
9593
+ omitted.add(present);
9336
9594
  continue;
9337
9595
  }
9338
9596
  const dot = path.indexOf(".");
@@ -9347,13 +9605,30 @@ class BlacklistValidator {
9347
9605
  for (const column of presentColumns) {
9348
9606
  let dot = column.indexOf(".");
9349
9607
  while (dot >= 0) {
9350
- if (dot > 0 && protectedPaths.has(column.slice(0, dot))) {
9608
+ if (dot > 0 && protectedPaths.has(column.slice(0, dot).toLowerCase())) {
9351
9609
  omitted.add(column);
9352
9610
  break;
9353
9611
  }
9354
9612
  dot = column.indexOf(".", dot + 1);
9355
9613
  }
9356
9614
  }
9615
+ if (globRules.length > 0) {
9616
+ for (const column of presentColumns) {
9617
+ const folded = foldHead(column);
9618
+ if (matchAny(folded, globRules)) {
9619
+ omitted.add(column);
9620
+ continue;
9621
+ }
9622
+ let dot = folded.indexOf(".");
9623
+ while (dot >= 0) {
9624
+ if (dot > 0 && matchAny(folded.slice(0, dot), globRules)) {
9625
+ omitted.add(column);
9626
+ break;
9627
+ }
9628
+ dot = folded.indexOf(".", dot + 1);
9629
+ }
9630
+ }
9631
+ }
9357
9632
  const omittedColumns = Array.from(omitted);
9358
9633
  if (omittedColumns.length === 0) {
9359
9634
  return { filteredRows: rows, omittedColumns: [] };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dbcli-agent",
3
- "version": "4.0.0",
3
+ "version": "6.0.0",
4
4
  "description": "Database CLI skill and command reference for AI agents.",
5
5
  "contextFileName": "AGENTS.md"
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carllee1983/dbcli",
3
- "version": "4.0.0",
3
+ "version": "6.0.0",
4
4
  "description": "Database CLI for AI agents",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dbcli-agent",
3
- "version": "4.0.0",
3
+ "version": "6.0.0",
4
4
  "description": "Database CLI skill and command reference for AI agents",
5
5
  "author": {
6
6
  "name": "Carl Lee",