@carllee1983/dbcli 6.0.0 → 7.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/core.mjs CHANGED
@@ -1323,6 +1323,7 @@ var messages_default = {
1323
1323
  unsupported_lang: "Language '{lang}' not supported, using English",
1324
1324
  invalid_json: "Invalid JSON: {message}",
1325
1325
  table_blacklisted: "Error: Table '{table}' is blacklisted for {operation} operations",
1326
+ redis_target_blacklisted: "Redis command refused by blacklist",
1326
1327
  column_blacklisted_write: "Error: {operation} on table '{table}' touches blacklisted columns: {columns}",
1327
1328
  table_already_blacklisted: "Error: Table '{table}' is already blacklisted",
1328
1329
  table_not_in_blacklist: "Error: Table '{table}' is not in the blacklist",
@@ -1414,7 +1415,8 @@ Reference: {referencePath}`,
1414
1415
  columns_omitted: "Security: {count} column(s) were omitted based on your blacklist"
1415
1416
  },
1416
1417
  warnings: {
1417
- blacklist_override_used: "Warning: Blacklist override enabled (DBCLI_OVERRIDE_BLACKLIST=true). Executing {operation} on blacklisted table '{table}'"
1418
+ blacklist_override_used: "Warning: Blacklist override enabled (DBCLI_OVERRIDE_BLACKLIST=true). Executing {operation} on blacklisted table '{table}'",
1419
+ redis_blacklist_override_active: "Warning: Redis blacklist enforcement disabled by DBCLI_OVERRIDE_BLACKLIST=true"
1418
1420
  },
1419
1421
  version: {
1420
1422
  unsupported_warning: "{system} {version} is below minimum supported version {minVersion}. Some features may not work correctly.",
@@ -1637,6 +1639,7 @@ var messages_default2 = {
1637
1639
  unsupported_lang: "\u4E0D\u652F\u63F4\u7684\u8A9E\u8A00 '{lang}'\uFF0C\u4F7F\u7528\u82F1\u6587",
1638
1640
  invalid_json: "\u7121\u6548\u7684 JSON\uFF1A{message}",
1639
1641
  table_blacklisted: "\u932F\u8AA4: \u8868\u683C '{table}' \u7684 {operation} \u64CD\u4F5C\u5DF2\u88AB\u9ED1\u540D\u55AE",
1642
+ redis_target_blacklisted: "Redis \u6307\u4EE4\u5DF2\u88AB\u9ED1\u540D\u55AE\u62D2\u7D55",
1640
1643
  column_blacklisted_write: "\u932F\u8AA4: \u8868\u683C '{table}' \u7684 {operation} \u64CD\u4F5C\u5305\u542B\u9ED1\u540D\u55AE\u6B04\u4F4D: {columns}",
1641
1644
  table_already_blacklisted: "\u932F\u8AA4: \u8868\u683C '{table}' \u5DF2\u5728\u9ED1\u540D\u55AE\u4E2D",
1642
1645
  table_not_in_blacklist: "\u932F\u8AA4: \u8868\u683C '{table}' \u4E0D\u5728\u9ED1\u540D\u55AE\u4E2D",
@@ -1728,7 +1731,8 @@ var messages_default2 = {
1728
1731
  columns_omitted: "\u5B89\u5168: {count} \u500B\u6B04\u4F4D\u6839\u64DA\u4F60\u7684\u9ED1\u540D\u55AE\u5DF2\u88AB\u96B1\u85CF"
1729
1732
  },
1730
1733
  warnings: {
1731
- blacklist_override_used: "\u8B66\u544A: \u5DF2\u555F\u7528\u9ED1\u540D\u55AE\u8986\u84CB (DBCLI_OVERRIDE_BLACKLIST=true)\u3002\u57F7\u884C {operation} \u5728\u5DF2\u9ED1\u540D\u55AE\u7684\u8868\u683C '{table}' \u4E0A"
1734
+ blacklist_override_used: "\u8B66\u544A: \u5DF2\u555F\u7528\u9ED1\u540D\u55AE\u8986\u84CB (DBCLI_OVERRIDE_BLACKLIST=true)\u3002\u57F7\u884C {operation} \u5728\u5DF2\u9ED1\u540D\u55AE\u7684\u8868\u683C '{table}' \u4E0A",
1735
+ redis_blacklist_override_active: "\u8B66\u544A: DBCLI_OVERRIDE_BLACKLIST=true \u5DF2\u505C\u7528 Redis \u9ED1\u540D\u55AE\u5F37\u5236\u57F7\u884C"
1732
1736
  },
1733
1737
  version: {
1734
1738
  unsupported_warning: "{system} {version} \u4F4E\u65BC\u6700\u4F4E\u652F\u63F4\u7248\u672C {minVersion}\u3002\u90E8\u5206\u529F\u80FD\u53EF\u80FD\u7121\u6CD5\u6B63\u5E38\u904B\u4F5C\u3002",
@@ -2024,6 +2028,12 @@ var messageLoader = MessageLoader.getInstance();
2024
2028
  var t = (key) => messageLoader.t(key);
2025
2029
  var t_vars = (key, vars) => messageLoader.interpolate(key, vars);
2026
2030
 
2031
+ // src/utils/case-fold.ts
2032
+ function foldCase(text) {
2033
+ const lowered = (text.includes("\u0130") ? text.replaceAll("\u0130", "i") : text).toLowerCase();
2034
+ return lowered.includes("\u03C2") ? lowered.replaceAll("\u03C2", "\u03C3") : lowered;
2035
+ }
2036
+
2027
2037
  // src/utils/glob.ts
2028
2038
  function findClassEnd(glob, open) {
2029
2039
  for (let i = open + 1;i < glob.length; i++) {
@@ -2045,15 +2055,19 @@ function isValidCharacterClass(body) {
2045
2055
  }
2046
2056
  }
2047
2057
  var parsedGlobs = new Map;
2048
- function parseGlob(glob) {
2049
- const memo = parsedGlobs.get(glob);
2058
+ var PARSED_GLOB_LIMIT = 4096;
2059
+ function parseGlob(glob, caseInsensitive) {
2060
+ const key = `${caseInsensitive ? "i" : "c"}\x00${glob}`;
2061
+ const memo = parsedGlobs.get(key);
2050
2062
  if (memo !== undefined)
2051
2063
  return memo;
2052
- const parsed = parseGlobUncached(glob);
2053
- parsedGlobs.set(glob, parsed);
2064
+ const parsed = parseGlobUncached(glob, caseInsensitive);
2065
+ if (parsedGlobs.size >= PARSED_GLOB_LIMIT)
2066
+ parsedGlobs.clear();
2067
+ parsedGlobs.set(key, parsed);
2054
2068
  return parsed;
2055
2069
  }
2056
- function parseGlobUncached(glob) {
2070
+ function parseGlobUncached(glob, caseInsensitive) {
2057
2071
  const runs = [];
2058
2072
  let current = [];
2059
2073
  let leadingStar = false;
@@ -2068,7 +2082,9 @@ function parseGlobUncached(glob) {
2068
2082
  const c = glob[i];
2069
2083
  if (c === "\\" && i + 1 < glob.length) {
2070
2084
  trailingStar = false;
2071
- current.push({ kind: "literal", char: glob[++i] });
2085
+ const escaped = String.fromCodePoint(glob.codePointAt(++i));
2086
+ i += escaped.length - 1;
2087
+ current.push(...literalToken(escaped, caseInsensitive));
2072
2088
  continue;
2073
2089
  }
2074
2090
  if (c === "*") {
@@ -2088,39 +2104,53 @@ function parseGlobUncached(glob) {
2088
2104
  const end = findClassEnd(glob, i);
2089
2105
  const body = end === -1 ? "" : glob.slice(i, end + 1);
2090
2106
  if (body !== "" && isValidCharacterClass(body)) {
2091
- current.push({ kind: "class", test: new RegExp(`^${body}$`, "s") });
2107
+ current.push({
2108
+ kind: "class",
2109
+ test: new RegExp(`^${body}$`, caseInsensitive ? "si" : "s")
2110
+ });
2092
2111
  i = end;
2093
2112
  continue;
2094
2113
  }
2095
- current.push({ kind: "literal", char: "[" });
2114
+ current.push(...literalToken("[", caseInsensitive));
2096
2115
  continue;
2097
2116
  }
2098
- current.push({ kind: "literal", char: c });
2117
+ const literal = String.fromCodePoint(glob.codePointAt(i));
2118
+ i += literal.length - 1;
2119
+ current.push(...literalToken(literal, caseInsensitive));
2099
2120
  }
2100
2121
  endRun();
2101
2122
  return { runs, leadingStar, trailingStar, anchored: !sawStar };
2102
2123
  }
2103
- function runMatchesAt(run, text, at) {
2124
+ function literalToken(codePoint, caseInsensitive) {
2125
+ const folded = caseInsensitive ? foldCase(codePoint) : codePoint;
2126
+ return folded.split("").map((unit) => ({ kind: "literal", char: unit }));
2127
+ }
2128
+ function runMatchesAt(run, text, raw, at) {
2104
2129
  if (at + run.length > text.length)
2105
2130
  return false;
2106
2131
  for (let i = 0;i < run.length; i++) {
2107
2132
  const token = run[i];
2108
- const ch = text[at + i];
2109
2133
  if (token.kind === "literal") {
2110
- if (token.char !== ch)
2134
+ if (token.char !== text[at + i])
2111
2135
  return false;
2112
2136
  } else if (token.kind === "class") {
2113
- if (!token.test.test(ch))
2137
+ const rawChar = raw[at + i];
2138
+ if (rawChar === undefined) {
2139
+ throw new Error("globMatches: folded and unfolded subject lengths disagree");
2140
+ }
2141
+ if (!token.test.test(rawChar))
2114
2142
  return false;
2115
2143
  }
2116
2144
  }
2117
2145
  return true;
2118
2146
  }
2119
- function globMatches(glob, text) {
2120
- const { runs, leadingStar, trailingStar, anchored } = parseGlob(glob);
2147
+ function globMatches(glob, subject, options) {
2148
+ const caseInsensitive = options?.caseInsensitive === true;
2149
+ const { runs, leadingStar, trailingStar, anchored } = parseGlob(glob, caseInsensitive);
2150
+ const text = caseInsensitive ? foldCase(subject) : subject;
2121
2151
  if (anchored) {
2122
2152
  const run = runs[0] ?? [];
2123
- return text.length === run.length && runMatchesAt(run, text, 0);
2153
+ return text.length === run.length && runMatchesAt(run, text, subject, 0);
2124
2154
  }
2125
2155
  if (runs.length === 0)
2126
2156
  return true;
@@ -2129,7 +2159,7 @@ function globMatches(glob, text) {
2129
2159
  let last = runs.length;
2130
2160
  if (!leadingStar) {
2131
2161
  const head = runs[0];
2132
- if (!runMatchesAt(head, text, 0))
2162
+ if (!runMatchesAt(head, text, subject, 0))
2133
2163
  return false;
2134
2164
  cursor = head.length;
2135
2165
  first = 1;
@@ -2137,7 +2167,7 @@ function globMatches(glob, text) {
2137
2167
  if (!trailingStar) {
2138
2168
  const tail = runs[last - 1];
2139
2169
  const at = text.length - tail.length;
2140
- if (at < cursor || !runMatchesAt(tail, text, at))
2170
+ if (at < cursor || !runMatchesAt(tail, text, subject, at))
2141
2171
  return false;
2142
2172
  last -= 1;
2143
2173
  }
@@ -2146,7 +2176,7 @@ function globMatches(glob, text) {
2146
2176
  const run = runs[r];
2147
2177
  let found = -1;
2148
2178
  for (let at = cursor;at + run.length <= limit; at++) {
2149
- if (runMatchesAt(run, text, at)) {
2179
+ if (runMatchesAt(run, text, subject, at)) {
2150
2180
  found = at;
2151
2181
  break;
2152
2182
  }
@@ -2157,6 +2187,9 @@ function globMatches(glob, text) {
2157
2187
  }
2158
2188
  return true;
2159
2189
  }
2190
+ function isGlobPattern(value) {
2191
+ return /[*?[\\]/.test(value);
2192
+ }
2160
2193
  function globNeverMatches(glob) {
2161
2194
  for (let i = 0;i < glob.length; i++) {
2162
2195
  const c = glob[i];
@@ -2180,6 +2213,11 @@ function globNeverMatches(glob) {
2180
2213
  return null;
2181
2214
  }
2182
2215
 
2216
+ // src/core/blacklist-fold.ts
2217
+ function foldFieldPath(path) {
2218
+ return foldCase(path);
2219
+ }
2220
+
2183
2221
  // src/core/mongo/path-matcher.ts
2184
2222
  function compilePatterns(raw) {
2185
2223
  const patterns = [];
@@ -2217,6 +2255,10 @@ function compilePatterns(raw) {
2217
2255
  }
2218
2256
  return { patterns, rejected };
2219
2257
  }
2258
+ var FOLD_CASE = { caseInsensitive: true };
2259
+ function matchSegment(pattern, segment) {
2260
+ return globMatches(pattern, segment, FOLD_CASE);
2261
+ }
2220
2262
  function matchAny(path, patterns) {
2221
2263
  if (patterns.length === 0)
2222
2264
  return false;
@@ -2231,7 +2273,7 @@ function matchAny(path, patterns) {
2231
2273
  }
2232
2274
  let ok = true;
2233
2275
  for (let i = 0;i < pat.segments.length; i++) {
2234
- if (!globMatches(pat.segments[i], pathSegments[i])) {
2276
+ if (!globMatches(pat.segments[i], pathSegments[i], FOLD_CASE)) {
2235
2277
  ok = false;
2236
2278
  break;
2237
2279
  }
@@ -2241,6 +2283,7 @@ function matchAny(path, patterns) {
2241
2283
  }
2242
2284
  return false;
2243
2285
  }
2286
+ var compiledRuleSets = new WeakMap;
2244
2287
 
2245
2288
  // src/core/mongo/request-fields.ts
2246
2289
  var WHOLE_DOCUMENT_VARIABLES = new Set(["ROOT", "CURRENT"]);
@@ -2691,6 +2734,175 @@ var RETURNS_KEY_NAMES = new Set(["SCAN", "KEYS"]);
2691
2734
 
2692
2735
  // src/adapters/redis/value-masker.ts
2693
2736
  var MASKABLE = new Set(["GET", "GETRANGE", "HGETALL", "HGET", "HMGET", "HVALS"]);
2737
+
2738
+ // src/core/blacklist-manager.ts
2739
+ function isBlacklistOverrideEnabled(value) {
2740
+ return (value ?? Bun.env.DBCLI_OVERRIDE_BLACKLIST ?? "") === "true";
2741
+ }
2742
+ function normalizeBlacklistEntry(raw) {
2743
+ const trimmed = raw.trim();
2744
+ const quote = trimmed[0];
2745
+ if ((quote === '"' || quote === "`") && trimmed.length > 1 && trimmed.endsWith(quote)) {
2746
+ return trimmed.slice(1, -1).trim();
2747
+ }
2748
+ return trimmed;
2749
+ }
2750
+ function assertNotTableQualified(tableKey, column) {
2751
+ const dot = column.indexOf(".");
2752
+ if (dot <= 0)
2753
+ return;
2754
+ if (foldFieldPath(column.slice(0, dot)) !== foldFieldPath(tableKey))
2755
+ return;
2756
+ 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))}.`);
2757
+ }
2758
+
2759
+ class BlacklistManager {
2760
+ config;
2761
+ state;
2762
+ overrideEnabled;
2763
+ rawTables = [];
2764
+ constructor(config, overrideEnvValue) {
2765
+ this.config = config;
2766
+ this.overrideEnabled = isBlacklistOverrideEnabled(overrideEnvValue);
2767
+ this.state = this.loadBlacklist();
2768
+ this.wildcardTables = this.rawTables.filter(isGlobPattern);
2769
+ }
2770
+ loadBlacklist() {
2771
+ this.rawTables.length = 0;
2772
+ const tables = new Set;
2773
+ const columns = new Map;
2774
+ const blacklistConfig = this.config.blacklist;
2775
+ if (!blacklistConfig) {
2776
+ return { tables, columns };
2777
+ }
2778
+ if (Array.isArray(blacklistConfig.tables)) {
2779
+ for (const tableName of blacklistConfig.tables) {
2780
+ if (typeof tableName === "string") {
2781
+ const entry = normalizeBlacklistEntry(tableName);
2782
+ this.rawTables.push(entry);
2783
+ tables.add(foldFieldPath(entry));
2784
+ } else {
2785
+ console.warn(`[BlacklistManager] Invalid table name in blacklist config: ${JSON.stringify(tableName)}`);
2786
+ }
2787
+ }
2788
+ } else if (blacklistConfig.tables !== undefined) {
2789
+ console.warn("[BlacklistManager] blacklist.tables must be an array, ignoring");
2790
+ }
2791
+ if (blacklistConfig.columns && typeof blacklistConfig.columns === "object" && !Array.isArray(blacklistConfig.columns)) {
2792
+ for (const [tableName, cols] of Object.entries(blacklistConfig.columns)) {
2793
+ if (typeof tableName !== "string") {
2794
+ console.warn(`[BlacklistManager] Invalid table name key in columns config: ${JSON.stringify(tableName)}`);
2795
+ continue;
2796
+ }
2797
+ if (!Array.isArray(cols)) {
2798
+ console.warn(`[BlacklistManager] blacklist.columns["${tableName}"] must be an array, ignoring`);
2799
+ continue;
2800
+ }
2801
+ const columnSet = new Set;
2802
+ for (const col of cols) {
2803
+ if (typeof col === "string") {
2804
+ const entry = normalizeBlacklistEntry(col);
2805
+ assertNotTableQualified(normalizeBlacklistEntry(tableName), entry);
2806
+ columnSet.add(entry);
2807
+ } else {
2808
+ console.warn(`[BlacklistManager] Invalid column name in blacklist.columns["${tableName}"]: ${JSON.stringify(col)}`);
2809
+ }
2810
+ }
2811
+ if (columnSet.size > 0) {
2812
+ columns.set(foldFieldPath(normalizeBlacklistEntry(tableName)), columnSet);
2813
+ }
2814
+ }
2815
+ } else if (blacklistConfig.columns !== undefined) {
2816
+ console.warn("[BlacklistManager] blacklist.columns must be an object, ignoring");
2817
+ }
2818
+ return { tables, columns };
2819
+ }
2820
+ isTableBlacklisted(tableName) {
2821
+ const name = foldFieldPath(tableName);
2822
+ if (this.state.tables.has(name))
2823
+ return true;
2824
+ for (const pattern of this.wildcardTables) {
2825
+ if (globMatches(pattern, tableName, { caseInsensitive: true }))
2826
+ return true;
2827
+ }
2828
+ return false;
2829
+ }
2830
+ wildcardTables;
2831
+ columnRulesFor(tableName) {
2832
+ const key = foldFieldPath(normalizeBlacklistEntry(tableName));
2833
+ const direct = this.state.columns.get(key);
2834
+ if (direct)
2835
+ return direct;
2836
+ const dot = key.lastIndexOf(".");
2837
+ if (dot > 0) {
2838
+ const bare = this.state.columns.get(key.slice(dot + 1));
2839
+ if (bare)
2840
+ return bare;
2841
+ }
2842
+ let merged;
2843
+ for (const [ruleKey, columns] of this.state.columns) {
2844
+ const ruleDot = ruleKey.lastIndexOf(".");
2845
+ if (ruleDot <= 0 || ruleKey.slice(ruleDot + 1) !== key)
2846
+ continue;
2847
+ merged ??= new Set;
2848
+ for (const column of columns)
2849
+ merged.add(column);
2850
+ }
2851
+ return merged;
2852
+ }
2853
+ isColumnBlacklisted(tableName, columnName) {
2854
+ const columnSet = this.columnRulesFor(tableName);
2855
+ if (!columnSet) {
2856
+ return false;
2857
+ }
2858
+ const target = foldFieldPath(normalizeBlacklistEntry(columnName));
2859
+ const compiled = this.foldedColumnsFor(columnSet);
2860
+ if (compiled.literals.has(target))
2861
+ return true;
2862
+ return compiled.globs.length > 0 && matchAny(target, compiled.globs);
2863
+ }
2864
+ foldedColumns = new WeakMap;
2865
+ foldedColumnsFor(columnSet) {
2866
+ const memo = this.foldedColumns.get(columnSet);
2867
+ if (memo !== undefined)
2868
+ return memo;
2869
+ const literals = new Set;
2870
+ const globbed = [];
2871
+ for (const rule of columnSet) {
2872
+ if (isGlobPattern(rule))
2873
+ globbed.push(rule);
2874
+ else
2875
+ literals.add(foldFieldPath(rule));
2876
+ }
2877
+ const folded = {
2878
+ literals,
2879
+ globs: globbed.length > 0 ? compilePatterns(globbed).patterns : []
2880
+ };
2881
+ this.foldedColumns.set(columnSet, folded);
2882
+ return folded;
2883
+ }
2884
+ getBlacklistedColumns(tableName) {
2885
+ const columnSet = this.columnRulesFor(tableName);
2886
+ if (!columnSet) {
2887
+ return [];
2888
+ }
2889
+ return Array.from(columnSet);
2890
+ }
2891
+ getAllBlacklistedColumns() {
2892
+ const all = new Set;
2893
+ for (const columnSet of this.state.columns.values()) {
2894
+ for (const column of columnSet)
2895
+ all.add(column);
2896
+ }
2897
+ return Array.from(all);
2898
+ }
2899
+ canOverrideBlacklist() {
2900
+ return this.overrideEnabled;
2901
+ }
2902
+ getState() {
2903
+ return this.state;
2904
+ }
2905
+ }
2694
2906
  // src/utils/sql-lexical.ts
2695
2907
  var IDENTIFIER_CONTINUATION = /[A-Za-z0-9_$]|[\u0080-\uFFFF]/;
2696
2908
  var IDENTIFIER_START = /[A-Za-z_]|[\u0080-\uFFFF]/;
@@ -3611,18 +3823,139 @@ function projectRows(rows, selection) {
3611
3823
  const columnNames = collectColumnNames(projectedRows);
3612
3824
  return { rows: normalizeRows(projectedRows, columnNames), columnNames };
3613
3825
  }
3614
- function hasFieldPath(row, segments) {
3615
- return readPath(row, segments).found;
3826
+ function hasFieldPath(row, segments, options) {
3827
+ const caseInsensitive = options?.caseInsensitive === true;
3828
+ const folded = caseInsensitive && options?.alreadyFolded !== true ? segments.map(foldCase) : segments;
3829
+ return readPath(row, folded, caseInsensitive).found;
3830
+ }
3831
+ function narrow(rules, key) {
3832
+ const parts = key.includes(".") ? key.split(".") : [key];
3833
+ const live = [];
3834
+ for (const rule of rules) {
3835
+ const remaining = rule.segments.length;
3836
+ if (remaining < parts.length && !rule.wildcardTail)
3837
+ continue;
3838
+ let matches = true;
3839
+ for (let i = 0;i < Math.min(remaining, parts.length); i++) {
3840
+ if (!matchSegment(rule.segments[i], parts[i])) {
3841
+ matches = false;
3842
+ break;
3843
+ }
3844
+ }
3845
+ if (!matches)
3846
+ continue;
3847
+ if (remaining <= parts.length)
3848
+ return { drop: true, live: [] };
3849
+ live.push({
3850
+ root: rule.root,
3851
+ rootIndex: rule.rootIndex,
3852
+ offset: rule.offset + parts.length,
3853
+ segments: rule.segments.slice(parts.length),
3854
+ wildcardTail: rule.wildcardTail
3855
+ });
3856
+ }
3857
+ return { drop: false, live };
3616
3858
  }
3617
- function omitFieldPaths(rows, paths) {
3618
- const topLevel = new Set(paths);
3619
- const dotted = paths.filter((path) => path.includes(".")).map((path) => ({ head: path.slice(0, path.indexOf(".")), segments: path.split(".") }));
3859
+ function liveRulesOf(patterns) {
3860
+ return patterns.map((pattern, index) => ({
3861
+ root: pattern,
3862
+ rootIndex: index,
3863
+ offset: 0,
3864
+ segments: pattern.segments,
3865
+ wildcardTail: pattern.wildcardTail
3866
+ }));
3867
+ }
3868
+ var NARROW_STATE_LIMIT = 4096;
3869
+
3870
+ class NarrowStates {
3871
+ states = [];
3872
+ ids = new Map;
3873
+ transitions = new Map;
3874
+ constructor(rules) {
3875
+ this.intern(rules);
3876
+ }
3877
+ rules(state) {
3878
+ const rules = this.states[state];
3879
+ if (rules === undefined)
3880
+ throw new Error(`unknown nested-rule state ${state}`);
3881
+ return rules;
3882
+ }
3883
+ step(state, key) {
3884
+ const memoKey = `${state}\x00${key}`;
3885
+ const memo = this.transitions.get(memoKey);
3886
+ if (memo !== undefined)
3887
+ return memo;
3888
+ const { drop, live } = narrow(this.rules(state), key);
3889
+ const step = drop ? { drop: true } : { drop: false, next: this.intern(live) };
3890
+ if (this.transitions.size >= NARROW_STATE_LIMIT)
3891
+ this.transitions = new Map;
3892
+ this.transitions.set(memoKey, step);
3893
+ return step;
3894
+ }
3895
+ intern(rules) {
3896
+ const key = rules.map((rule) => `${rule.rootIndex}:${rule.offset}`).join(",");
3897
+ const existing = this.ids.get(key);
3898
+ if (existing !== undefined)
3899
+ return existing;
3900
+ const id = this.states.length;
3901
+ this.states.push(rules);
3902
+ this.ids.set(key, id);
3903
+ return id;
3904
+ }
3905
+ }
3906
+ function collectNestedMatches(row, states, pending, onMatch) {
3907
+ if (pending.size === 0)
3908
+ return;
3909
+ const walk = (value, state, depth) => {
3910
+ if (pending.size === 0 || states.rules(state).length === 0)
3911
+ return;
3912
+ if (Array.isArray(value)) {
3913
+ for (const item of value)
3914
+ walk(item, state, depth);
3915
+ return;
3916
+ }
3917
+ if (!isPlainRecord(value))
3918
+ return;
3919
+ for (const key of Object.getOwnPropertyNames(value)) {
3920
+ const step = states.step(state, key);
3921
+ if (step.drop) {
3922
+ if (depth > 0) {
3923
+ for (const rule of states.rules(state)) {
3924
+ if (!pending.has(rule.root))
3925
+ continue;
3926
+ if (!narrow([rule], key).drop)
3927
+ continue;
3928
+ pending.delete(rule.root);
3929
+ onMatch(rule.root);
3930
+ }
3931
+ }
3932
+ continue;
3933
+ }
3934
+ walk(value[key], step.next, depth + 1);
3935
+ }
3936
+ };
3937
+ walk(row, 0, 0);
3938
+ }
3939
+ function compileNestedRules(patterns) {
3940
+ return new NarrowStates(liveRulesOf(patterns));
3941
+ }
3942
+ function omitFieldPaths(rows, paths, options) {
3943
+ const caseInsensitive = options?.caseInsensitive === true;
3944
+ const nestedPatterns = options?.patterns ?? [];
3945
+ const nestedStates = nestedPatterns.length > 0 ? compileNestedRules(nestedPatterns) : undefined;
3946
+ const fold = (value) => caseInsensitive ? foldCase(value) : value;
3947
+ const topLevel = new Set(paths.map(fold));
3948
+ const dotted = paths.filter((path) => path.includes(".")).map((path) => ({ head: fold(path.slice(0, path.indexOf("."))), segments: path.split(".") }));
3620
3949
  const maskRecord = (row) => {
3621
- let projected = cloneRecord(row, topLevel);
3950
+ let projected = cloneRecord(row, topLevel, caseInsensitive);
3622
3951
  for (const { head, segments } of dotted) {
3623
- const value = row[head];
3624
- if (value !== null && typeof value === "object")
3625
- projected = omitPath(projected, segments);
3952
+ const value = headValue(row, head, caseInsensitive);
3953
+ if (value !== null && typeof value === "object") {
3954
+ projected = omitPath(projected, segments, caseInsensitive);
3955
+ }
3956
+ }
3957
+ if (nestedStates !== undefined && hasNestedRecord(row)) {
3958
+ projected = omitMatching(projected, nestedStates, 0);
3626
3959
  }
3627
3960
  return projected;
3628
3961
  };
@@ -3642,11 +3975,38 @@ function maskArrayRow(row, maskRecord) {
3642
3975
  });
3643
3976
  return out;
3644
3977
  }
3645
- function readPath(value, segments) {
3978
+ function headValue(row, head, caseInsensitive) {
3979
+ if (!caseInsensitive)
3980
+ return row[head];
3981
+ const key = foldedKeys(row).get(head);
3982
+ return key === undefined ? undefined : row[key];
3983
+ }
3984
+ var foldedKeyIndex = new WeakMap;
3985
+ function foldedKeys(value) {
3986
+ const memo = foldedKeyIndex.get(value);
3987
+ if (memo !== undefined)
3988
+ return memo;
3989
+ const index = new Map;
3990
+ for (const key of Object.getOwnPropertyNames(value)) {
3991
+ const folded = foldCase(key);
3992
+ if (!index.has(folded))
3993
+ index.set(folded, key);
3994
+ }
3995
+ foldedKeyIndex.set(value, index);
3996
+ return index;
3997
+ }
3998
+ function ownKey(value, name, caseInsensitive) {
3999
+ if (Object.prototype.hasOwnProperty.call(value, name))
4000
+ return name;
4001
+ if (!caseInsensitive)
4002
+ return;
4003
+ return foldedKeys(value).get(name);
4004
+ }
4005
+ function readPath(value, segments, caseInsensitive = false) {
3646
4006
  if (segments.length === 0)
3647
4007
  return { found: true, value };
3648
4008
  if (Array.isArray(value)) {
3649
- const results = value.map((item) => readPath(item, segments));
4009
+ const results = value.map((item) => readPath(item, segments, caseInsensitive));
3650
4010
  return {
3651
4011
  found: results.some((result) => result.found),
3652
4012
  value: results.map((result) => result.found ? result.value : undefined)
@@ -3654,41 +4014,68 @@ function readPath(value, segments) {
3654
4014
  }
3655
4015
  if (!isRecord(value))
3656
4016
  return { found: false };
3657
- const exactPath = segments.join(".");
3658
- if (Object.prototype.hasOwnProperty.call(value, exactPath)) {
3659
- return { found: true, value: value[exactPath] };
3660
- }
4017
+ const exact = ownKey(value, segments.join("."), caseInsensitive);
4018
+ if (exact !== undefined)
4019
+ return { found: true, value: value[exact] };
3661
4020
  const [head, ...tail] = segments;
3662
- if (!Object.prototype.hasOwnProperty.call(value, head))
4021
+ const headKey = ownKey(value, head, caseInsensitive);
4022
+ if (headKey === undefined)
3663
4023
  return { found: false };
3664
- return readPath(value[head], tail);
4024
+ return readPath(value[headKey], tail, caseInsensitive);
3665
4025
  }
3666
- function omitPath(value, segments) {
4026
+ function omitMatching(value, states, state) {
4027
+ if (states.rules(state).length === 0)
4028
+ return value;
4029
+ if (Array.isArray(value))
4030
+ return value.map((item) => omitMatching(item, states, state));
4031
+ if (!isPlainRecord(value))
4032
+ return value;
4033
+ const out = {};
4034
+ for (const [key, child] of Object.entries(value)) {
4035
+ const step = states.step(state, key);
4036
+ if (step.drop)
4037
+ continue;
4038
+ defineData(out, key, omitMatching(child, states, step.next));
4039
+ }
4040
+ return out;
4041
+ }
4042
+ function hasNestedRecord(row) {
4043
+ for (const key of Object.getOwnPropertyNames(row)) {
4044
+ const value = row[key];
4045
+ if (Array.isArray(value) || isPlainRecord(value))
4046
+ return true;
4047
+ }
4048
+ return false;
4049
+ }
4050
+ function omitPath(value, segments, caseInsensitive) {
3667
4051
  if (segments.length === 0)
3668
4052
  return value;
3669
4053
  if (Array.isArray(value))
3670
- return value.map((item) => omitPath(item, segments));
4054
+ return value.map((item) => omitPath(item, segments, caseInsensitive));
3671
4055
  if (!isPlainRecord(value))
3672
4056
  return value;
3673
- const exactPath = segments.join(".");
3674
- const [head, ...tail] = segments;
4057
+ const fold = (name) => caseInsensitive ? foldCase(name) : name;
4058
+ const exactPath = fold(segments.join("."));
4059
+ const head = fold(segments[0]);
4060
+ const tail = segments.slice(1);
3675
4061
  const out = {};
3676
4062
  for (const [key, child] of Object.entries(value)) {
3677
- if (key === exactPath)
4063
+ const folded = fold(key);
4064
+ if (folded === exactPath)
3678
4065
  continue;
3679
- if (key === head) {
4066
+ if (folded === head) {
3680
4067
  if (tail.length > 0)
3681
- defineData(out, key, omitPath(child, tail));
4068
+ defineData(out, key, omitPath(child, tail, caseInsensitive));
3682
4069
  continue;
3683
4070
  }
3684
4071
  defineData(out, key, child);
3685
4072
  }
3686
4073
  return out;
3687
4074
  }
3688
- function cloneRecord(value, omittedKeys) {
4075
+ function cloneRecord(value, omittedKeys, caseInsensitive = false) {
3689
4076
  const out = {};
3690
4077
  for (const [key, child] of Object.entries(value)) {
3691
- if (!omittedKeys.has(key))
4078
+ if (!omittedKeys.has(caseInsensitive ? foldCase(key) : key))
3692
4079
  defineData(out, key, child);
3693
4080
  }
3694
4081
  return out;
@@ -3794,12 +4181,12 @@ class QueryExecutor {
3794
4181
  let columnNames = rows.length > 0 && rows[0] ? Object.keys(rows[0]) : [];
3795
4182
  let filteredRows = rows;
3796
4183
  let securityNotification;
3797
- let omittedColumns = [];
4184
+ let reachesOmitted = () => false;
3798
4185
  if (this.blacklistValidator) {
3799
4186
  const filterResult = this.blacklistValidator.filterColumnsForTables(referencedTables, rows, columnNames);
3800
4187
  filteredRows = filterResult.filteredRows;
3801
4188
  if (filterResult.omittedColumns.length > 0) {
3802
- omittedColumns = filterResult.omittedColumns;
4189
+ reachesOmitted = filterResult.reachesOmitted;
3803
4190
  columnNames = columnNames.filter((col) => !filterResult.omittedColumns.includes(col));
3804
4191
  securityNotification = this.blacklistValidator.buildSecurityNotification(referencedTables[0] ?? "", filterResult.omittedColumns);
3805
4192
  }
@@ -3807,7 +4194,7 @@ class QueryExecutor {
3807
4194
  if (options?.fieldSelection) {
3808
4195
  const fieldSelection = options.fieldSelection.mode === "include" ? {
3809
4196
  mode: "include",
3810
- paths: options.fieldSelection.paths.filter((path) => !omittedColumns.some((omitted) => path === omitted || path.startsWith(`${omitted}.`)))
4197
+ paths: options.fieldSelection.paths.filter((path) => !reachesOmitted(path))
3811
4198
  } : options.fieldSelection;
3812
4199
  const projection = projectRows(filteredRows, fieldSelection);
3813
4200
  filteredRows = projection.rows;
@@ -9203,143 +9590,6 @@ DBCLI_PASSWORD=${password}
9203
9590
  }
9204
9591
  }
9205
9592
  };
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
-
9224
- class BlacklistManager {
9225
- config;
9226
- state;
9227
- overrideEnabled;
9228
- constructor(config, overrideEnvValue) {
9229
- this.config = config;
9230
- this.overrideEnabled = (overrideEnvValue ?? Bun.env.DBCLI_OVERRIDE_BLACKLIST ?? "") === "true";
9231
- this.state = this.loadBlacklist();
9232
- this.wildcardTables = [...this.state.tables].filter((entry) => /[*?[\\]/.test(entry));
9233
- }
9234
- loadBlacklist() {
9235
- const tables = new Set;
9236
- const columns = new Map;
9237
- const blacklistConfig = this.config.blacklist;
9238
- if (!blacklistConfig) {
9239
- return { tables, columns };
9240
- }
9241
- if (Array.isArray(blacklistConfig.tables)) {
9242
- for (const tableName of blacklistConfig.tables) {
9243
- if (typeof tableName === "string") {
9244
- tables.add(normalizeBlacklistEntry(tableName).toLowerCase());
9245
- } else {
9246
- console.warn(`[BlacklistManager] Invalid table name in blacklist config: ${JSON.stringify(tableName)}`);
9247
- }
9248
- }
9249
- } else if (blacklistConfig.tables !== undefined) {
9250
- console.warn("[BlacklistManager] blacklist.tables must be an array, ignoring");
9251
- }
9252
- if (blacklistConfig.columns && typeof blacklistConfig.columns === "object" && !Array.isArray(blacklistConfig.columns)) {
9253
- for (const [tableName, cols] of Object.entries(blacklistConfig.columns)) {
9254
- if (typeof tableName !== "string") {
9255
- console.warn(`[BlacklistManager] Invalid table name key in columns config: ${JSON.stringify(tableName)}`);
9256
- continue;
9257
- }
9258
- if (!Array.isArray(cols)) {
9259
- console.warn(`[BlacklistManager] blacklist.columns["${tableName}"] must be an array, ignoring`);
9260
- continue;
9261
- }
9262
- const columnSet = new Set;
9263
- for (const col of cols) {
9264
- if (typeof col === "string") {
9265
- const entry = normalizeBlacklistEntry(col);
9266
- assertNotTableQualified(normalizeBlacklistEntry(tableName), entry);
9267
- columnSet.add(entry);
9268
- } else {
9269
- console.warn(`[BlacklistManager] Invalid column name in blacklist.columns["${tableName}"]: ${JSON.stringify(col)}`);
9270
- }
9271
- }
9272
- if (columnSet.size > 0) {
9273
- columns.set(normalizeBlacklistEntry(tableName).toLowerCase(), columnSet);
9274
- }
9275
- }
9276
- } else if (blacklistConfig.columns !== undefined) {
9277
- console.warn("[BlacklistManager] blacklist.columns must be an object, ignoring");
9278
- }
9279
- return { tables, columns };
9280
- }
9281
- isTableBlacklisted(tableName) {
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;
9313
- }
9314
- isColumnBlacklisted(tableName, columnName) {
9315
- const columnSet = this.columnRulesFor(tableName);
9316
- if (!columnSet) {
9317
- return false;
9318
- }
9319
- return columnSet.has(normalizeBlacklistEntry(columnName).toLowerCase());
9320
- }
9321
- getBlacklistedColumns(tableName) {
9322
- const columnSet = this.columnRulesFor(tableName);
9323
- if (!columnSet) {
9324
- return [];
9325
- }
9326
- return Array.from(columnSet);
9327
- }
9328
- getAllBlacklistedColumns() {
9329
- const all = new Set;
9330
- for (const columnSet of this.state.columns.values()) {
9331
- for (const column of columnSet)
9332
- all.add(column);
9333
- }
9334
- return Array.from(all);
9335
- }
9336
- canOverrideBlacklist() {
9337
- return this.overrideEnabled;
9338
- }
9339
- getState() {
9340
- return this.state;
9341
- }
9342
- }
9343
9593
  // src/utils/es-index-target.ts
9344
9594
  var MAX_DECODE_PASSES = 4;
9345
9595
  function unwrap(target) {
@@ -9381,16 +9631,16 @@ function expandIndexTargets(target) {
9381
9631
  return { concrete, wildcards };
9382
9632
  }
9383
9633
  function matchesIndexGlob(pattern, name) {
9384
- const normalized = pattern.toLowerCase() === "_all" ? "*" : pattern.toLowerCase();
9634
+ const normalized = pattern.toLowerCase() === "_all" ? "*" : pattern;
9385
9635
  try {
9386
- return globMatches(normalized, name.toLowerCase());
9636
+ return globMatches(normalized, name, { caseInsensitive: true });
9387
9637
  } catch {
9388
9638
  return true;
9389
9639
  }
9390
9640
  }
9391
9641
  function reachesByConvention(name, entry) {
9392
- const lower = name.toLowerCase();
9393
- const target = entry.toLowerCase();
9642
+ const lower = foldFieldPath(name);
9643
+ const target = foldFieldPath(entry);
9394
9644
  return new RegExp(`^\\.ds-${escapeRegExp(target)}-`).test(lower) || new RegExp(`^${escapeRegExp(target)}-\\d+$`).test(lower);
9395
9645
  }
9396
9646
  function escapeRegExp(text) {
@@ -9411,11 +9661,13 @@ function indexExpressionReaches(expression, blacklisted) {
9411
9661
  return false;
9412
9662
  const { concrete, wildcards } = expandIndexTargets(expression);
9413
9663
  const entries = expandBlacklistEntries(blacklisted);
9414
- const reachesName = (name) => entries.names.some((entry) => entry.toLowerCase() === name.toLowerCase() || reachesByConvention(name, entry)) || entries.patterns.some((pattern) => matchesIndexGlob(pattern, name));
9664
+ const reachesName = (name) => entries.names.some((entry) => foldFieldPath(entry) === foldFieldPath(name) || reachesByConvention(name, entry)) || entries.patterns.some((pattern) => matchesIndexGlob(pattern, name));
9415
9665
  return concrete.some(reachesName) || wildcards.some((pattern) => entries.names.some((entry) => matchesIndexGlob(pattern, entry)) || entries.patterns.length > 0);
9416
9666
  }
9417
9667
 
9418
9668
  // src/core/blacklist-validator.ts
9669
+ var FOLD_CASE2 = { caseInsensitive: true };
9670
+ var PRE_FOLDED = { caseInsensitive: true, alreadyFolded: true };
9419
9671
  function flattenArrayRow(row) {
9420
9672
  const records = [];
9421
9673
  for (const item of row) {
@@ -9430,7 +9682,7 @@ function dedupe2(values) {
9430
9682
  const seen = new Set;
9431
9683
  const result = [];
9432
9684
  for (const value of values) {
9433
- const key = value.toLowerCase();
9685
+ const key = foldFieldPath(value);
9434
9686
  if (value.length === 0 || seen.has(key))
9435
9687
  continue;
9436
9688
  seen.add(key);
@@ -9438,11 +9690,14 @@ function dedupe2(values) {
9438
9690
  }
9439
9691
  return result;
9440
9692
  }
9441
- function compileGlobRules(rules, fold, table, operation) {
9442
- const globbed = rules.filter((rule) => /[*?[\\]/.test(rule));
9693
+ function omittedByName(omittedColumns, path) {
9694
+ return omittedColumns.some((omitted) => path === omitted || path.startsWith(`${omitted}.`));
9695
+ }
9696
+ function compileGlobRules(rules, table, operation) {
9697
+ const globbed = rules.filter(isGlobPattern);
9443
9698
  if (globbed.length === 0)
9444
9699
  return [];
9445
- const { patterns, rejected } = compilePatterns(globbed.map(fold));
9700
+ const { patterns, rejected } = compilePatterns(globbed);
9446
9701
  if (rejected.length > 0) {
9447
9702
  const detail = rejected.map((r) => `'${r.raw}' (${r.reason})`).join(", ");
9448
9703
  throw new BlacklistError(`blacklist.columns for '${table}' has entries this matcher cannot read: ${detail}`, table, operation);
@@ -9506,10 +9761,10 @@ class BlacklistValidator {
9506
9761
  if (blacklisted.length === 0 || fields.length === 0) {
9507
9762
  return;
9508
9763
  }
9509
- const protectedPaths = new Set(blacklisted.map((name) => name.toLowerCase()));
9510
- const globs = compileGlobRules(blacklisted, (value) => value.toLowerCase(), tableName, operation);
9764
+ const protectedPaths = new Set(blacklisted.map(foldFieldPath));
9765
+ const globs = compileGlobRules(blacklisted, tableName, operation);
9511
9766
  const conflicts = fields.filter((field) => {
9512
- const name = field.toLowerCase();
9767
+ const name = foldFieldPath(field);
9513
9768
  if (protectedPaths.has(name))
9514
9769
  return true;
9515
9770
  if (matchAny(name, globs))
@@ -9552,7 +9807,7 @@ class BlacklistValidator {
9552
9807
  const tables = dedupe2(tableNames);
9553
9808
  const blacklistedColumns = tables.length === 0 ? this.manager.getAllBlacklistedColumns() : Array.from(new Set(tables.flatMap((table) => this.manager.getBlacklistedColumns(table))));
9554
9809
  if (blacklistedColumns.length === 0) {
9555
- return { filteredRows: rows, omittedColumns: [] };
9810
+ return { filteredRows: rows, omittedColumns: [], reachesOmitted: () => false };
9556
9811
  }
9557
9812
  const probeNested = blacklistedColumns.some((path) => path.includes("."));
9558
9813
  const presentColumns = new Set(columnList);
@@ -9564,7 +9819,7 @@ class BlacklistValidator {
9564
9819
  continue;
9565
9820
  const value = record[key];
9566
9821
  if (value !== null && typeof value === "object")
9567
- nestedHeads.add(key);
9822
+ nestedHeads.add(foldFieldPath(key));
9568
9823
  }
9569
9824
  };
9570
9825
  for (const row of rows) {
@@ -9577,35 +9832,54 @@ class BlacklistValidator {
9577
9832
  }
9578
9833
  collect(row);
9579
9834
  }
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");
9835
+ const protectedPaths = new Set(blacklistedColumns.map(foldFieldPath));
9836
+ const globRules = compileGlobRules(blacklistedColumns, tables[0] ?? "unknown", "SELECT");
9586
9837
  const presentByFolded = new Map;
9587
- for (const column of presentColumns)
9588
- presentByFolded.set(foldHead(column), column);
9838
+ for (const column of presentColumns) {
9839
+ const folded = foldFieldPath(column);
9840
+ const names = presentByFolded.get(folded);
9841
+ if (names === undefined)
9842
+ presentByFolded.set(folded, [column]);
9843
+ else
9844
+ names.push(column);
9845
+ }
9589
9846
  const omitted = new Set;
9590
9847
  for (const path of blacklistedColumns) {
9591
- const present = presentByFolded.get(foldHead(path));
9848
+ const present = presentByFolded.get(foldFieldPath(path));
9592
9849
  if (present !== undefined) {
9593
- omitted.add(present);
9850
+ for (const name of present)
9851
+ omitted.add(name);
9594
9852
  continue;
9595
9853
  }
9596
9854
  const dot = path.indexOf(".");
9597
9855
  if (dot < 0)
9598
9856
  continue;
9599
- if (!nestedHeads.has(path.slice(0, dot)))
9857
+ if (!nestedHeads.has(foldFieldPath(path.slice(0, dot))))
9600
9858
  continue;
9601
- const segments = path.split(".");
9602
- if (rows.some((row) => hasFieldPath(row, segments)))
9859
+ const segments = path.split(".").map(foldFieldPath);
9860
+ if (rows.some((row) => hasFieldPath(row, segments, PRE_FOLDED)))
9603
9861
  omitted.add(path);
9604
9862
  }
9863
+ const nestedGlobs = globRules.filter((pattern) => pattern.segments.length > 1);
9864
+ const matchedNested = [];
9865
+ if (nestedGlobs.length > 0 && nestedHeads.size > 0) {
9866
+ const pending = new Set(nestedGlobs);
9867
+ const states = compileNestedRules(nestedGlobs);
9868
+ for (const row of rows) {
9869
+ if (pending.size === 0)
9870
+ break;
9871
+ if (row === null || typeof row !== "object")
9872
+ continue;
9873
+ collectNestedMatches(row, states, pending, (pattern) => {
9874
+ matchedNested.push(pattern);
9875
+ omitted.add(pattern.raw);
9876
+ });
9877
+ }
9878
+ }
9605
9879
  for (const column of presentColumns) {
9606
9880
  let dot = column.indexOf(".");
9607
9881
  while (dot >= 0) {
9608
- if (dot > 0 && protectedPaths.has(column.slice(0, dot).toLowerCase())) {
9882
+ if (dot > 0 && protectedPaths.has(foldFieldPath(column.slice(0, dot)))) {
9609
9883
  omitted.add(column);
9610
9884
  break;
9611
9885
  }
@@ -9614,7 +9888,7 @@ class BlacklistValidator {
9614
9888
  }
9615
9889
  if (globRules.length > 0) {
9616
9890
  for (const column of presentColumns) {
9617
- const folded = foldHead(column);
9891
+ const folded = foldFieldPath(column);
9618
9892
  if (matchAny(folded, globRules)) {
9619
9893
  omitted.add(column);
9620
9894
  continue;
@@ -9631,10 +9905,30 @@ class BlacklistValidator {
9631
9905
  }
9632
9906
  const omittedColumns = Array.from(omitted);
9633
9907
  if (omittedColumns.length === 0) {
9634
- return { filteredRows: rows, omittedColumns: [] };
9908
+ return { filteredRows: rows, omittedColumns: [], reachesOmitted: () => false };
9635
9909
  }
9636
- const filteredRows = omitFieldPaths(rows, omittedColumns);
9637
- return { filteredRows, omittedColumns };
9910
+ const removalPaths = matchedNested.length === 0 ? omittedColumns : omittedColumns.filter((path) => !matchedNested.some((p) => p.raw === path));
9911
+ const filteredRows = omitFieldPaths(rows, removalPaths, matchedNested.length > 0 ? { ...FOLD_CASE2, patterns: matchedNested } : FOLD_CASE2);
9912
+ return {
9913
+ filteredRows,
9914
+ omittedColumns,
9915
+ reachesOmitted: (path) => {
9916
+ if (omittedByName(removalPaths, path))
9917
+ return true;
9918
+ if (matchedNested.length === 0)
9919
+ return false;
9920
+ const folded = foldFieldPath(path);
9921
+ if (matchAny(folded, matchedNested))
9922
+ return true;
9923
+ let dot = folded.indexOf(".");
9924
+ while (dot > 0) {
9925
+ if (matchAny(folded.slice(0, dot), matchedNested))
9926
+ return true;
9927
+ dot = folded.indexOf(".", dot + 1);
9928
+ }
9929
+ return false;
9930
+ }
9931
+ };
9638
9932
  }
9639
9933
  buildSecurityNotification(_tableName, omittedColumns) {
9640
9934
  if (omittedColumns.length === 0) {