@memberjunction/codegen-lib 6.1.0-edge.2 → 6.1.0-edge.4

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.
Files changed (133) hide show
  1. package/LICENSE +180 -4
  2. package/README.md +89 -43
  3. package/dist/Angular/angular-codegen.d.ts +0 -8
  4. package/dist/Angular/angular-codegen.d.ts.map +1 -1
  5. package/dist/Angular/angular-codegen.js +32 -22
  6. package/dist/Angular/angular-codegen.js.map +1 -1
  7. package/dist/Angular/entity-data-grid-related-entity-component.js +2 -2
  8. package/dist/Angular/entity-data-grid-related-entity-component.js.map +1 -1
  9. package/dist/Angular/related-entity-components.d.ts +1 -1
  10. package/dist/Angular/related-entity-components.js +1 -1
  11. package/dist/Config/config.d.ts +118 -1
  12. package/dist/Config/config.d.ts.map +1 -1
  13. package/dist/Config/config.js +202 -8
  14. package/dist/Config/config.js.map +1 -1
  15. package/dist/Database/__tests__/exclude-schema-permissions.test.d.ts +2 -0
  16. package/dist/Database/__tests__/exclude-schema-permissions.test.d.ts.map +1 -0
  17. package/dist/Database/__tests__/exclude-schema-permissions.test.js +32 -0
  18. package/dist/Database/__tests__/exclude-schema-permissions.test.js.map +1 -0
  19. package/dist/Database/codeGenDatabaseProvider.d.ts +178 -2
  20. package/dist/Database/codeGenDatabaseProvider.d.ts.map +1 -1
  21. package/dist/Database/codeGenDatabaseProvider.js +175 -10
  22. package/dist/Database/codeGenDatabaseProvider.js.map +1 -1
  23. package/dist/Database/heal-schema-params.d.ts +33 -0
  24. package/dist/Database/heal-schema-params.d.ts.map +1 -0
  25. package/dist/Database/heal-schema-params.js +51 -0
  26. package/dist/Database/heal-schema-params.js.map +1 -0
  27. package/dist/Database/manage-metadata.d.ts +301 -2
  28. package/dist/Database/manage-metadata.d.ts.map +1 -1
  29. package/dist/Database/manage-metadata.js +1242 -96
  30. package/dist/Database/manage-metadata.js.map +1 -1
  31. package/dist/Database/materializationAnalysis.d.ts +241 -0
  32. package/dist/Database/materializationAnalysis.d.ts.map +1 -0
  33. package/dist/Database/materializationAnalysis.js +415 -0
  34. package/dist/Database/materializationAnalysis.js.map +1 -0
  35. package/dist/Database/materializationBroadRender.d.ts +67 -0
  36. package/dist/Database/materializationBroadRender.d.ts.map +1 -0
  37. package/dist/Database/materializationBroadRender.js +104 -0
  38. package/dist/Database/materializationBroadRender.js.map +1 -0
  39. package/dist/Database/materializationDrift.d.ts +59 -0
  40. package/dist/Database/materializationDrift.d.ts.map +1 -0
  41. package/dist/Database/materializationDrift.js +85 -0
  42. package/dist/Database/materializationDrift.js.map +1 -0
  43. package/dist/Database/materializationParamClassifier.d.ts +92 -0
  44. package/dist/Database/materializationParamClassifier.d.ts.map +1 -0
  45. package/dist/Database/materializationParamClassifier.js +224 -0
  46. package/dist/Database/materializationParamClassifier.js.map +1 -0
  47. package/dist/Database/materializationParamVerifier.d.ts +71 -0
  48. package/dist/Database/materializationParamVerifier.d.ts.map +1 -0
  49. package/dist/Database/materializationParamVerifier.js +302 -0
  50. package/dist/Database/materializationParamVerifier.js.map +1 -0
  51. package/dist/Database/materializationSqlAst.d.ts +78 -0
  52. package/dist/Database/materializationSqlAst.d.ts.map +1 -0
  53. package/dist/Database/materializationSqlAst.js +142 -0
  54. package/dist/Database/materializationSqlAst.js.map +1 -0
  55. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts +93 -56
  56. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts.map +1 -1
  57. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js +399 -247
  58. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js.map +1 -1
  59. package/dist/Database/providers/postgresql/__tests__/PostgreSQLCodeGenProvider.materialization.test.d.ts +2 -0
  60. package/dist/Database/providers/postgresql/__tests__/PostgreSQLCodeGenProvider.materialization.test.d.ts.map +1 -0
  61. package/dist/Database/providers/postgresql/__tests__/PostgreSQLCodeGenProvider.materialization.test.js +119 -0
  62. package/dist/Database/providers/postgresql/__tests__/PostgreSQLCodeGenProvider.materialization.test.js.map +1 -0
  63. package/dist/Database/providers/postgresql/__tests__/PostgreSQLHierarchyFunctions.test.d.ts +2 -0
  64. package/dist/Database/providers/postgresql/__tests__/PostgreSQLHierarchyFunctions.test.d.ts.map +1 -0
  65. package/dist/Database/providers/postgresql/__tests__/PostgreSQLHierarchyFunctions.test.js +155 -0
  66. package/dist/Database/providers/postgresql/__tests__/PostgreSQLHierarchyFunctions.test.js.map +1 -0
  67. package/dist/Database/providers/postgresql/__tests__/SoftPrimaryKeyIndex.test.d.ts +2 -0
  68. package/dist/Database/providers/postgresql/__tests__/SoftPrimaryKeyIndex.test.d.ts.map +1 -0
  69. package/dist/Database/providers/postgresql/__tests__/SoftPrimaryKeyIndex.test.js +110 -0
  70. package/dist/Database/providers/postgresql/__tests__/SoftPrimaryKeyIndex.test.js.map +1 -0
  71. package/dist/Database/providers/postgresql/metadataSupportObjects.js +78 -10
  72. package/dist/Database/providers/postgresql/metadataSupportObjects.js.map +1 -1
  73. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts +72 -1
  74. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts.map +1 -1
  75. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js +322 -0
  76. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js.map +1 -1
  77. package/dist/Database/providers/sqlserver/__tests__/BaseViewEmissionGoldenMaster.test.d.ts +2 -0
  78. package/dist/Database/providers/sqlserver/__tests__/BaseViewEmissionGoldenMaster.test.d.ts.map +1 -0
  79. package/dist/Database/providers/sqlserver/__tests__/BaseViewEmissionGoldenMaster.test.js +547 -0
  80. package/dist/Database/providers/sqlserver/__tests__/BaseViewEmissionGoldenMaster.test.js.map +1 -0
  81. package/dist/Database/providers/sqlserver/__tests__/BaseViewRegenDecision.test.d.ts +2 -0
  82. package/dist/Database/providers/sqlserver/__tests__/BaseViewRegenDecision.test.d.ts.map +1 -0
  83. package/dist/Database/providers/sqlserver/__tests__/BaseViewRegenDecision.test.js +721 -0
  84. package/dist/Database/providers/sqlserver/__tests__/BaseViewRegenDecision.test.js.map +1 -0
  85. package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.materialization.test.d.ts +2 -0
  86. package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.materialization.test.d.ts.map +1 -0
  87. package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.materialization.test.js +78 -0
  88. package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.materialization.test.js.map +1 -0
  89. package/dist/Database/providers/sqlserver/__tests__/SQLServerHierarchyTVF.test.d.ts +2 -0
  90. package/dist/Database/providers/sqlserver/__tests__/SQLServerHierarchyTVF.test.d.ts.map +1 -0
  91. package/dist/Database/providers/sqlserver/__tests__/SQLServerHierarchyTVF.test.js +155 -0
  92. package/dist/Database/providers/sqlserver/__tests__/SQLServerHierarchyTVF.test.js.map +1 -0
  93. package/dist/Database/providers/sqlserver/__tests__/SoftPrimaryKeyIndex.test.d.ts +2 -0
  94. package/dist/Database/providers/sqlserver/__tests__/SoftPrimaryKeyIndex.test.d.ts.map +1 -0
  95. package/dist/Database/providers/sqlserver/__tests__/SoftPrimaryKeyIndex.test.js +195 -0
  96. package/dist/Database/providers/sqlserver/__tests__/SoftPrimaryKeyIndex.test.js.map +1 -0
  97. package/dist/Database/schema-filters.d.ts +23 -0
  98. package/dist/Database/schema-filters.d.ts.map +1 -0
  99. package/dist/Database/schema-filters.js +42 -0
  100. package/dist/Database/schema-filters.js.map +1 -0
  101. package/dist/Database/schema-scope.d.ts.map +1 -1
  102. package/dist/Database/schema-scope.js +4 -0
  103. package/dist/Database/schema-scope.js.map +1 -1
  104. package/dist/Database/sql_codegen.d.ts +39 -5
  105. package/dist/Database/sql_codegen.d.ts.map +1 -1
  106. package/dist/Database/sql_codegen.js +176 -67
  107. package/dist/Database/sql_codegen.js.map +1 -1
  108. package/dist/EntityNameScanner/EntityNameScanner.d.ts.map +1 -1
  109. package/dist/EntityNameScanner/EntityNameScanner.js +33 -2
  110. package/dist/EntityNameScanner/EntityNameScanner.js.map +1 -1
  111. package/dist/Manifest/GenerateClassRegistrationsManifest.d.ts +8 -4
  112. package/dist/Manifest/GenerateClassRegistrationsManifest.d.ts.map +1 -1
  113. package/dist/Manifest/GenerateClassRegistrationsManifest.js +63 -20
  114. package/dist/Manifest/GenerateClassRegistrationsManifest.js.map +1 -1
  115. package/dist/Misc/action_subclasses_codegen.d.ts.map +1 -1
  116. package/dist/Misc/action_subclasses_codegen.js +4 -4
  117. package/dist/Misc/action_subclasses_codegen.js.map +1 -1
  118. package/dist/Misc/entity_subclasses_codegen.d.ts +61 -0
  119. package/dist/Misc/entity_subclasses_codegen.d.ts.map +1 -1
  120. package/dist/Misc/entity_subclasses_codegen.js +437 -158
  121. package/dist/Misc/entity_subclasses_codegen.js.map +1 -1
  122. package/dist/Misc/graphql_server_codegen.d.ts +4 -58
  123. package/dist/Misc/graphql_server_codegen.d.ts.map +1 -1
  124. package/dist/Misc/graphql_server_codegen.js +22 -236
  125. package/dist/Misc/graphql_server_codegen.js.map +1 -1
  126. package/dist/Misc/sql_logging.d.ts +25 -0
  127. package/dist/Misc/sql_logging.d.ts.map +1 -1
  128. package/dist/Misc/sql_logging.js +51 -8
  129. package/dist/Misc/sql_logging.js.map +1 -1
  130. package/dist/Misc/system_integrity.d.ts.map +1 -1
  131. package/dist/Misc/system_integrity.js +5 -0
  132. package/dist/Misc/system_integrity.js.map +1 -1
  133. package/package.json +30 -30
@@ -0,0 +1,415 @@
1
+ import { SQLParser } from '@memberjunction/sql-parser';
2
+ import { isObject, nodeType, qualifiedColumn, identifiersEqual, isSetOperationRoot, soleStatement, } from './materializationSqlAst.js';
3
+ /**
4
+ * Name of the synthetic surrogate key column added to every query-materialized table in v1.
5
+ * Namespaced to avoid colliding with a query's own output columns.
6
+ */
7
+ export const MATERIALIZATION_SURROGATE_COLUMN = '__mj_MaterializedRowID';
8
+ /** Default surrogate column SQL type (SQL Server). Callers on other engines pass their own. */
9
+ export const DEFAULT_SURROGATE_SQL_TYPE = 'int IDENTITY(1,1)';
10
+ /**
11
+ * Analyzes a query for v1 materialization. Pure function — no DB/IO — so it is fully unit-testable.
12
+ *
13
+ * Qualifying rule (§9 / §10, asymmetric-risk: default to NOT materializable when unsure):
14
+ * - parameterized queries do not qualify in v1 (deferred);
15
+ * - a query with no declared output fields does not qualify (run query analysis first);
16
+ * - a query whose output already contains a column named like the surrogate does not qualify
17
+ * (we won't silently shadow it).
18
+ *
19
+ * On success, returns the column spec with a synthetic surrogate PK prepended; the query's
20
+ * own output columns are emitted as nullable snapshot columns (a snapshot may contain NULLs).
21
+ */
22
+ export function analyzeQueryForMaterialization(opts) {
23
+ const surrogateColumnName = MATERIALIZATION_SURROGATE_COLUMN;
24
+ const surrogateSQLType = opts.surrogateSQLType ?? DEFAULT_SURROGATE_SQL_TYPE;
25
+ const fail = (reason) => ({ qualifies: false, reason, columns: [], surrogateColumnName });
26
+ if (opts.isParameterized) {
27
+ return fail(`query "${opts.queryName}" is parameterized — not materializable in v1 (deferred to Phase 2)`);
28
+ }
29
+ if (!opts.fields || opts.fields.length === 0) {
30
+ return fail(`query "${opts.queryName}" has no declared output fields — run query field analysis before materializing`);
31
+ }
32
+ if (opts.fields.some((f) => f.Name.trim().toLowerCase() === surrogateColumnName.toLowerCase())) {
33
+ return fail(`query "${opts.queryName}" already has an output column named "${surrogateColumnName}" — cannot add the surrogate key without shadowing it`);
34
+ }
35
+ const surrogate = {
36
+ Name: surrogateColumnName,
37
+ SQLType: surrogateSQLType,
38
+ Nullable: false,
39
+ IsPrimaryKey: true,
40
+ };
41
+ // Query output columns become nullable snapshot columns (the result set may contain NULLs).
42
+ const dataColumns = opts.fields.map((f) => ({
43
+ Name: f.Name,
44
+ SQLType: f.SQLFullType,
45
+ Nullable: true,
46
+ IsPrimaryKey: false,
47
+ }));
48
+ return { qualifies: true, columns: [surrogate, ...dataColumns], surrogateColumnName };
49
+ }
50
+ /**
51
+ * Phase 2 read-time-injectable operators — the SAFE whitelist. A `column <op> value` predicate in this
52
+ * set can be faithfully re-applied against the broad materialized table with a bound parameter (§3 of the
53
+ * Phase-2 plan). `LIKE`/`IS`/`BETWEEN` and their negations are intentionally EXCLUDED in v1 (subtler
54
+ * pattern/null/two-operand semantics) — a query using them stays live-only, which is harmless (§10).
55
+ */
56
+ export const SAFE_READ_FILTER_OPERATORS = new Set([
57
+ '=', '!=', '<>', '<', '>', '<=', '>=', 'IN', 'NOT IN',
58
+ ]);
59
+ /**
60
+ * Decides the {@link ParamMode} for a (possibly parameterized) query from its per-param
61
+ * classifications — the §9 qualifying rule under the §10 asymmetric-risk posture
62
+ * (**default to NOT materializable unless every param is provably safe**). Pure — no DB/IO/LLM.
63
+ *
64
+ * Rules:
65
+ * - no params → `None` (qualifies; caller materializes the static query).
66
+ * - any `Unbounded` param (Bucket 3) → refuse (author can bind it to a fixed value → BoundFixed,
67
+ * which arrives here as effectively unparameterized).
68
+ * - `RowFilter` (Bucket 1) → the `filterColumn` MUST be present in the materialized output, else
69
+ * refuse (filtering on a projected-away column would be unsound).
70
+ * - `Structural` (Bucket 2) → only when `allowPerValueCache` is enabled AND the verifier bounded
71
+ * the domain; otherwise refuse (recompute live). Open decision §17 — defaults OFF.
72
+ * - a mix of row-filter and structural params is not modeled in v1 → refuse.
73
+ */
74
+ export function qualifyParameterizedQuery(opts) {
75
+ const { queryName, params, outputColumns } = opts;
76
+ const allowPerValueCache = opts.allowPerValueCache ?? false;
77
+ const allowRowFilterBroad = opts.allowRowFilterBroad ?? false;
78
+ const refuse = (reason) => ({ qualifies: false, reason, paramMode: 'None', rowFilterColumns: [], readFilterSpec: [] });
79
+ if (!params || params.length === 0) {
80
+ return { qualifies: true, paramMode: 'None', rowFilterColumns: [], readFilterSpec: [] };
81
+ }
82
+ const outputSet = new Set(outputColumns.map((c) => c.trim().toLowerCase()));
83
+ const rowFilterColumns = [];
84
+ const readFilterSpec = [];
85
+ let hasStructural = false;
86
+ for (const p of params) {
87
+ if (p.role === 'Unbounded') {
88
+ return refuse(`query "${queryName}" param "${p.name}" is unbounded/arbitrary structural (Bucket 3) — not materializable; bind it to a fixed value (BoundFixed) to materialize a specific instance`);
89
+ }
90
+ if (p.role === 'Structural') {
91
+ hasStructural = true;
92
+ if (!allowPerValueCache) {
93
+ return refuse(`query "${queryName}" param "${p.name}" is structural (Bucket 2) and per-value cache is disabled — recompute live, not materializable`);
94
+ }
95
+ if (!p.boundedDomain || p.boundedDomain.length === 0) {
96
+ return refuse(`query "${queryName}" param "${p.name}" is structural with no bounded domain — cannot enumerate a per-value cache safely`);
97
+ }
98
+ continue;
99
+ }
100
+ // RowFilter (Bucket 1): the filter column must be physically present in the materialized output (§9/§10).
101
+ if (!p.filterColumn) {
102
+ return refuse(`query "${queryName}" param "${p.name}" classified RowFilter but no filter column was resolved — refusing under uncertainty`);
103
+ }
104
+ if (!outputSet.has(p.filterColumn.trim().toLowerCase())) {
105
+ return refuse(`query "${queryName}" param "${p.name}" filters on column "${p.filterColumn}", which is not in the materialized output — disqualify (or project that column into the query)`);
106
+ }
107
+ // Phase 2 read-time-safety gate: only mint a RowFilterBroad materialization when the operator is
108
+ // provably reconstructable against the broad table with a bound param (§3). Anything outside the
109
+ // whitelist (LIKE/IS/BETWEEN, or an unresolved operator) stays live-only — harmless (§10).
110
+ const op = p.filterOperator;
111
+ if (!op || !SAFE_READ_FILTER_OPERATORS.has(op)) {
112
+ return refuse(`query "${queryName}" param "${p.name}" filters on "${p.filterColumn}" with operator "${op ?? '(unresolved)'}", which is not in the read-time-safe operator set {${[...SAFE_READ_FILTER_OPERATORS].join(', ')}} — deferred; the query stays live-only`);
113
+ }
114
+ // Operator/value-shape consistency: IN/NOT IN must carry a list value; every other op a scalar.
115
+ // A mismatch means our AST reading is inconsistent → refuse under uncertainty rather than guess.
116
+ const isListOp = op === 'IN' || op === 'NOT IN';
117
+ const expectedKind = isListOp ? 'list' : 'scalar';
118
+ if (p.filterKind !== expectedKind) {
119
+ return refuse(`query "${queryName}" param "${p.name}" operator "${op}" expects a ${expectedKind} value but the verifier reported "${p.filterKind ?? '(none)'}" — refusing under uncertainty`);
120
+ }
121
+ // Qualifier-aware binding proof: the predicate's SOURCE column must provably be the very column the
122
+ // identically-named materialized OUTPUT column carries. A bare-name match is not that proof (join
123
+ // collision / alias rebinding) — and neither is broad-render's removal count, which is exactly 1 in
124
+ // both wrong cases. Without the rendered SQL there is nothing to prove it with → refuse (§10).
125
+ if (!opts.sql || !opts.dialect) {
126
+ return refuse(`query "${queryName}" param "${p.name}" filters on "${p.filterColumn}" but no rendered SQL was supplied to prove that predicate binds to the materialized output column of the same name — refusing under uncertainty (the query stays live-only)`);
127
+ }
128
+ const binding = proveFilterColumnBinding({ sql: opts.sql, dialect: opts.dialect, filterColumn: p.filterColumn });
129
+ if (!binding.provable) {
130
+ return refuse(`query "${queryName}" param "${p.name}" filters on "${p.filterColumn}" but that predicate cannot be proven to bind to the materialized output column of the same name: ${binding.reason} — a materialized read would filter a different column than the live query. Refusing (the query stays live-only).`);
131
+ }
132
+ rowFilterColumns.push(p.filterColumn);
133
+ readFilterSpec.push({ column: p.filterColumn, operator: op, paramName: p.name, kind: expectedKind });
134
+ }
135
+ if (rowFilterColumns.length > 0 && hasStructural) {
136
+ return refuse(`query "${queryName}" mixes row-filter and structural params — not modeled in v1; refusing under uncertainty`);
137
+ }
138
+ if (hasStructural) {
139
+ return { qualifies: true, paramMode: 'PerValueCache', rowFilterColumns: [], readFilterSpec: [] };
140
+ }
141
+ if (!allowRowFilterBroad) {
142
+ // Enablement switch (Phase 2). When off, refuse row-filter queries to live-only — a defense-in-depth
143
+ // kill switch even though the read-time predicate injection is now implemented in the provider.
144
+ return refuse(`query "${queryName}" has row-filter parameter(s) on [${rowFilterColumns.join(', ')}] (Bucket 1 / RowFilterBroad) — parameterized row-filter materialization is not enabled in this build; the query stays live-only`);
145
+ }
146
+ return { qualifies: true, paramMode: 'RowFilterBroad', rowFilterColumns, readFilterSpec };
147
+ }
148
+ // AST-walking primitives are shared with the param verifier + broad-render via ./materializationSqlAst
149
+ // (single source of truth — see that module's header). extractGroupByTerms is GROUP-BY-specific and stays here.
150
+ /**
151
+ * Extracts the top-level GROUP BY term nodes, normalizing node-sql-parser's dialect-varying shapes.
152
+ * Returns `[]` (→ no key → full rebuild) for anything it cannot read as a single plain SELECT's GROUP BY.
153
+ *
154
+ * **Set-operation refusal (soundness-critical).** A `UNION` / `UNION ALL` / `EXCEPT` / `INTERSECT` parses to
155
+ * a SINGLE `select` root carrying `set_op`/`_next`, whose `groupby` and `columns` describe **only the first
156
+ * branch** (see {@link isSetOperationRoot}). Reading that root would report the first branch's grouping
157
+ * columns as the key of the WHOLE query — but the combined result legitimately contains one row per
158
+ * (branch × group), so those "key" columns are NOT unique across the result. The caller would then hash the
159
+ * key into the surrogate PK and pick the MERGE-upsert Incremental path, where the branches collide on the
160
+ * same hash and one silently overwrites the other — permanently wrong aggregates with no error. Refuse.
161
+ */
162
+ function extractGroupByTerms(stmt) {
163
+ const gb = stmt.groupby;
164
+ if (isObject(gb) && Array.isArray(gb.columns))
165
+ return gb.columns; // observed shape: { columns: [...] }
166
+ if (Array.isArray(gb))
167
+ return gb; // some dialects emit a bare array
168
+ if (isObject(gb) && Array.isArray(gb.value))
169
+ return gb.value; // …or { value: [...] }
170
+ return [];
171
+ }
172
+ /**
173
+ * Parses `sql` and returns its sole plain-SELECT statement root, or null when that is not what it is —
174
+ * unparseable, multi-statement, a non-SELECT, or a SET OPERATION (see {@link isSetOperationRoot}, whose
175
+ * first-branch-only view every analyzer here must refuse). Every analyzer in this module goes through
176
+ * this one gate so no future reader can reintroduce a bare `ast[0]` set-op blind spot.
177
+ */
178
+ function parseSoleSelectRoot(sql, dialect) {
179
+ const parsed = SQLParser.Astify(sql, dialect);
180
+ if (!parsed.astParsed || parsed.ast == null)
181
+ return null;
182
+ const stmt = soleStatement(parsed.ast);
183
+ if (!isObject(stmt) || nodeType(stmt) !== 'select')
184
+ return null;
185
+ if (isSetOperationRoot(stmt))
186
+ return null;
187
+ return stmt;
188
+ }
189
+ /** Number of relations in a SELECT's FROM clause (each JOIN / comma source counts). 0 when unreadable. */
190
+ function fromSourceCount(stmt) {
191
+ return Array.isArray(stmt.from) ? stmt.from.length : 0;
192
+ }
193
+ /**
194
+ * Whether a SELECT-list column reference and another reference to the same bare name (a GROUP BY term, a
195
+ * WHERE predicate operand) provably denote the SAME source column.
196
+ *
197
+ * With a SINGLE FROM relation, every reference to a given name necessarily resolves to that one relation,
198
+ * so the qualifier carries no information and the bare-name match is already a proof. With a JOIN the bare
199
+ * name is ambiguous — `o.Status` and `c.Status` are different columns — so BOTH sides must carry the same
200
+ * explicit qualifier; an unqualified reference on either side is unprovable and refuses (§10).
201
+ */
202
+ function sourceRefsMatch(selectQualifier, refQualifier, singleSource) {
203
+ if (singleSource)
204
+ return true;
205
+ return identifiersEqual(selectQualifier, refQualifier);
206
+ }
207
+ /**
208
+ * Collects the table qualifiers of every reference to `column` inside an AST subtree, in encounter order.
209
+ * A `null` entry means an unqualified reference. Used to read the WHERE clause's view of a filter column.
210
+ */
211
+ function collectColumnQualifiers(node, column, found) {
212
+ if (Array.isArray(node)) {
213
+ for (const child of node)
214
+ collectColumnQualifiers(child, column, found);
215
+ return;
216
+ }
217
+ if (!isObject(node))
218
+ return;
219
+ const qc = qualifiedColumn(node);
220
+ if (qc != null) {
221
+ if (identifiersEqual(qc.column, column))
222
+ found.push(qc.qualifier);
223
+ return; // a column_ref has no further column_ref descendants
224
+ }
225
+ for (const [k, v] of Object.entries(node)) {
226
+ if (k !== 'loc')
227
+ collectColumnQualifiers(v, column, found);
228
+ }
229
+ }
230
+ /**
231
+ * Proves that a verified row-filter predicate on `filterColumn` refers to **exactly** the materialized
232
+ * output column of that same name — the gap a bare-name `outputColumns.includes(filterColumn)` check leaves
233
+ * open, and which nothing downstream can close.
234
+ *
235
+ * Two silently-wrong-data cases motivate it, both of which pass every existing guard (in particular the
236
+ * broad-render count guard, since exactly ONE predicate is stripped in each):
237
+ *
238
+ * - **Join collision** — `SELECT o.ID, c.Status FROM Orders o JOIN Customers c … WHERE o.Status = {{s}}`.
239
+ * The output `Status` is the CUSTOMER's; the predicate is the ORDER's. The materialized read emits
240
+ * `WHERE [Status] = @p0` against the customer's value.
241
+ * - **Alias rebinding** — `SELECT ID, ShipRegion AS BillRegion FROM Orders WHERE BillRegion = {{r}}` on a
242
+ * table that has BOTH columns. The materialized `BillRegion` column holds `ShipRegion` values, so the
243
+ * read filters `ShipRegion = 'East'` while live filters `BillRegion = 'East'`.
244
+ *
245
+ * Refuses (never guesses) unless ALL of the following hold — falling back to the live query, always correct:
246
+ * 1. `sql` parses to a single plain SELECT (not multi-statement, not a set operation — a `UNION` root
247
+ * exposes only its first branch, so nothing about the whole result can be proven from it);
248
+ * 2. the SELECT list is readable and not a wildcard (`SELECT *` hides the real output↔source mapping);
249
+ * 3. exactly ONE output column is named `filterColumn` (two would make the read-time predicate ambiguous);
250
+ * 4. that output column is a plain column reference, not a computed expression;
251
+ * 5. it projects the SAME source column name (no alias rebinding);
252
+ * 6. the WHERE clause references `filterColumn` under a single, consistent qualifier; and
253
+ * 7. that qualifier provably denotes the same relation as the output column's — trivially true for a
254
+ * single-relation FROM, otherwise both must carry the same explicit qualifier ({@link sourceRefsMatch}).
255
+ *
256
+ * Pure — no DB/IO.
257
+ */
258
+ export function proveFilterColumnBinding(opts) {
259
+ const { sql, dialect, filterColumn } = opts;
260
+ const refuse = (reason) => ({ provable: false, reason });
261
+ if (!sql || sql.trim().length === 0)
262
+ return refuse('no rendered SQL to analyze');
263
+ let root;
264
+ let selectCols;
265
+ try {
266
+ root = parseSoleSelectRoot(sql, dialect);
267
+ selectCols = SQLParser.ExtractSelectColumns(sql, dialect);
268
+ }
269
+ catch {
270
+ return refuse('the rendered SQL could not be parsed');
271
+ }
272
+ if (root == null) {
273
+ return refuse('the rendered SQL is not a single plain SELECT (multi-statement, non-SELECT, or a UNION/EXCEPT/INTERSECT whose branches are not all visible)');
274
+ }
275
+ const outputCheck = resolveProvableOutputColumn(selectCols, filterColumn);
276
+ if (!outputCheck.provable)
277
+ return refuse(outputCheck.reason);
278
+ const output = outputCheck.column;
279
+ const qualifiers = [];
280
+ collectColumnQualifiers(root.where, filterColumn, qualifiers);
281
+ if (qualifiers.length === 0) {
282
+ return refuse(`the WHERE clause of the rendered SQL has no reference to a column named "${filterColumn}"`);
283
+ }
284
+ const distinct = new Set(qualifiers.map((q) => (q == null ? '' : q.trim().toLowerCase())));
285
+ if (distinct.size > 1) {
286
+ return refuse(`the WHERE clause references "${filterColumn}" under ${distinct.size} different table qualifiers — the parameter's predicate cannot be attributed to one of them`);
287
+ }
288
+ const predicateQualifier = qualifiers[0];
289
+ const singleSource = fromSourceCount(root) === 1;
290
+ if (!singleSource && (predicateQualifier == null || output.TableQualifier == null)) {
291
+ return refuse(`the query reads ${fromSourceCount(root)} relations and the ${predicateQualifier == null ? 'WHERE predicate on' : 'SELECT-list projection of'} "${filterColumn}" is unqualified, so the name cannot be attributed to a single source`);
292
+ }
293
+ if (!sourceRefsMatch(output.TableQualifier, predicateQualifier, singleSource)) {
294
+ return refuse(`the WHERE predicate filters "${predicateQualifier}.${filterColumn}" but the output column "${filterColumn}" projects "${output.TableQualifier}.${filterColumn}" — a different source column`);
295
+ }
296
+ return { provable: true };
297
+ }
298
+ /**
299
+ * Resolves the single SELECT-list output column that a read-time filter on `filterColumn` would target,
300
+ * or the reason it is not provably that source column (wildcard projection, duplicate/absent name,
301
+ * computed expression, or an alias rebinding a differently-named source column).
302
+ */
303
+ function resolveProvableOutputColumn(selectCols, filterColumn) {
304
+ const no = (reason) => ({ provable: false, reason });
305
+ if (!selectCols || selectCols.length === 0) {
306
+ return no('the SELECT list could not be read');
307
+ }
308
+ if (selectCols.some((c) => c.OutputName === '*')) {
309
+ return no('the query projects a wildcard (SELECT *), so the output-to-source column mapping is unknown');
310
+ }
311
+ const matches = selectCols.filter((c) => identifiersEqual(c.OutputName, filterColumn));
312
+ if (matches.length === 0) {
313
+ return no(`no SELECT-list output column is named "${filterColumn}"`);
314
+ }
315
+ if (matches.length > 1) {
316
+ return no(`the SELECT list projects ${matches.length} output columns named "${filterColumn}"`);
317
+ }
318
+ const output = matches[0];
319
+ if (output.IsExpression) {
320
+ return no(`the output column "${filterColumn}" is a computed expression, not a plain projection of a source column`);
321
+ }
322
+ if (!identifiersEqual(output.SourceColumn, filterColumn)) {
323
+ return no(`the output column "${filterColumn}" is an ALIAS over source column "${output.SourceColumn}", so filtering the materialized "${filterColumn}" is not the same predicate as the live "${filterColumn}"`);
324
+ }
325
+ return { provable: true, column: output };
326
+ }
327
+ /**
328
+ * Phase 3: detect the combined KEY of an aggregation query (for the surrogate hash). Conservative and
329
+ * safe-by-default — returns the grouping columns as the key ONLY when it can confidently identify an
330
+ * aggregation whose key is expressible in the materialized output; ANY ambiguity → null, and the caller
331
+ * then uses the synthetic surrogate + full rebuild (always correct, just not incrementally optimizable —
332
+ * the §10 "refuse under uncertainty" bias applied to keying). See the inline comment for the exact rules;
333
+ * the key is built from the AST GROUP BY terms (not a SELECT-list split), so grouping expressions and
334
+ * grouped-but-unprojected columns both refuse rather than silently producing a too-narrow key.
335
+ */
336
+ export function detectAggregationKeyColumns(opts) {
337
+ const { sql, dialect, fields } = opts;
338
+ if (!/\bgroup\s+by\b/i.test(sql))
339
+ return null; // no GROUP BY → not an aggregation key
340
+ // AST-level GROUP BY extraction (the "hardening follow-up" the heuristic promised) — robust vs. splitting
341
+ // the SELECT list, which can't tell a grouping EXPRESSION apart from an aggregate measure. Rules, all
342
+ // biased to refuse (null → synthetic surrogate + full rebuild, always correct; §10 "refuse under
343
+ // uncertainty"): (1) every grouping term must be a bare column reference — an expression grouping term
344
+ // (YEAR(date), a+b, CAST(…)) can't be reconstructed from the materialized output columns; (2) there must
345
+ // be at least one aggregate measure; (3) every grouping column must map to exactly one PROJECTED output
346
+ // column (so the key is expressible in the materialized table — a grouped-but-unprojected column would
347
+ // make the surrogate key too narrow and collide).
348
+ let root;
349
+ let selectCols;
350
+ try {
351
+ root = parseSoleSelectRoot(sql, dialect);
352
+ selectCols = SQLParser.ExtractSelectColumns(sql, dialect);
353
+ }
354
+ catch {
355
+ return null; // unparseable → not keyed (safe: synthetic surrogate + full rebuild)
356
+ }
357
+ if (root == null)
358
+ return null; // multi-statement / non-SELECT / set operation → refuse
359
+ const groupByTerms = extractGroupByTerms(root);
360
+ if (groupByTerms.length === 0 || !selectCols || selectCols.length === 0)
361
+ return null;
362
+ if (groupByTerms.some((t) => nodeType(t) !== 'column_ref'))
363
+ return null; // expression grouping → bail
364
+ if (!selectCols.some((c) => c.IsExpression))
365
+ return null; // no aggregate measure → not an aggregation
366
+ // (4) A grouping term is matched to a projected output column by SOURCE COLUMN NAME — which is ambiguous
367
+ // across a join (`GROUP BY o.Region` vs. a projected `c.Region` are different columns). Require the table
368
+ // qualifiers to agree whenever there is more than one FROM relation; see sourceRefsMatch.
369
+ const singleSource = fromSourceCount(root) === 1;
370
+ const fieldByOutput = new Map(fields.map((f) => [f.Name.trim().toLowerCase(), f]));
371
+ const key = [];
372
+ for (const term of groupByTerms) {
373
+ const gb = qualifiedColumn(term);
374
+ if (gb == null)
375
+ return null;
376
+ // The projected (non-expression) SELECT column whose pre-alias source column IS this grouping column.
377
+ const projected = selectCols.filter((c) => !c.IsExpression
378
+ && identifiersEqual(c.SourceColumn, gb.column)
379
+ && sourceRefsMatch(c.TableQualifier, gb.qualifier, singleSource));
380
+ if (projected.length !== 1)
381
+ return null; // unprojected (0) or ambiguous (>1) → bail
382
+ const f = fieldByOutput.get(projected[0].OutputName.trim().toLowerCase());
383
+ if (!f)
384
+ return null; // output column not in the materialized field set → bail
385
+ key.push({ name: f.Name, type: f.SQLFullType });
386
+ }
387
+ return key.length > 0 ? key : null;
388
+ }
389
+ /**
390
+ * Phase 4: is an aggregation's measure set PURELY ADDITIVE (only SUM / plain COUNT)? Drives the
391
+ * refresh-strategy choice: additive keyed aggregations use the MERGE-upsert `Incremental` path
392
+ * (in-place measure update, no row churn), non-additive ones use `DirtyGroupRecompute` (whole-group
393
+ * delete+reinsert). Conservative and safe-by-default: returns false on ANY sign of a non-additive
394
+ * aggregate (`AVG`/`MIN`/`MAX`/`STDEV*`/`VAR*`/`MEDIAN`/`PERCENTILE`/`*_AGG`, or `COUNT(DISTINCT …)`),
395
+ * and requires at least one additive aggregate to be present — anything ambiguous → false (the caller
396
+ * then uses DirtyGroupRecompute, which is correct for all measure types). Regex-based because the SQL
397
+ * parser doesn't expose per-measure expression text; the bias is toward the safe (non-additive) answer.
398
+ */
399
+ export function detectAdditiveMeasures(sql) {
400
+ if (!sql)
401
+ return false;
402
+ const s = sql.replace(/\s+/g, ' ');
403
+ // Any non-additive aggregate present → not purely additive.
404
+ const nonAdditive = /\b(AVG|MIN|MAX|STDEV|STDEVP|VAR|VARP|VARIANCE|MEDIAN|PERCENTILE_CONT|PERCENTILE_DISC|STRING_AGG|ARRAY_AGG|GROUP_CONCAT)\s*\(/i;
405
+ if (nonAdditive.test(s))
406
+ return false;
407
+ // SUM(DISTINCT ...) and COUNT(DISTINCT ...) are non-additive — their per-group value can't be delta-combined
408
+ // for an in-place incremental upsert — even though SUM(x)/COUNT(*) are. (AVG/MIN/MAX/etc. are already
409
+ // rejected above regardless of DISTINCT.)
410
+ if (/\b(SUM|COUNT)\s*\(\s*DISTINCT\b/i.test(s))
411
+ return false;
412
+ // Require at least one genuinely additive aggregate.
413
+ return /\b(SUM|COUNT)\s*\(/i.test(s);
414
+ }
415
+ //# sourceMappingURL=materializationAnalysis.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"materializationAnalysis.js","sourceRoot":"","sources":["../../src/Database/materializationAnalysis.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,67 @@
1
+ import type { SQLParserDialect } from '@memberjunction/sql-dialect';
2
+ /**
3
+ * Phase 2d — broad-render for row-filter materialization
4
+ * (plan /plans/query-entity-materialization.md §6.4 "materialize broad, filter at read").
5
+ *
6
+ * A parameterized query classified `RowFilterBroad` (by Phase 2b/2c) is materialized **broad** —
7
+ * every row the query could return for any value of the row-filter parameter — and the filter is
8
+ * re-applied at read time as an ordinary predicate on the (projected) filter column. This module
9
+ * produces that broad source SQL by **removing** the row-filter predicate from a rendered query.
10
+ *
11
+ * **Why this is sound:** the verifier only classifies a parameter `RowFilter(column)` when its value
12
+ * varies a literal at a *clean top-level conjunctive* `WHERE` predicate on a single column, and the
13
+ * qualifier only accepts it when that column is in the materialized output. A top-level conjunctive
14
+ * `WHERE` predicate on a projected (for GROUP-BY queries, grouped) column commutes with the rest of
15
+ * the query, so stripping it and re-applying it as an outer filter is algebraically identical to
16
+ * running the query with that value.
17
+ *
18
+ * **The isolation hazard, and how we refuse it:** this matcher works on *rendered* SQL (the parameter
19
+ * is already a concrete literal) and is qualifier-blind, so it cannot by itself tell a parameter
20
+ * predicate apart from (a) a *static* predicate on the same column (`… WHERE Region = {{r}} AND Region
21
+ * <> 'Internal'`) or (b) a same-named column on another table (`o.Region = {{r}} AND c.Region = 'X'`).
22
+ * Blindly stripping every match would broaden the materialization beyond what the live query can ever
23
+ * return. Guard: the caller passes `expectedRemovals` = the number of row-filter *parameter* predicates
24
+ * the classifier found, and we strip only when the match count equals it **exactly**. Any deviation
25
+ * sets `ambiguous: true` and the caller refuses (query stays live-only) — the §10 "refuse under
26
+ * uncertainty" bias. So we remove **exactly** the parameter predicates, or we decline.
27
+ *
28
+ * Pure — parses with @memberjunction/sql-parser, mutates the WHERE AST, re-emits. No DB/IO.
29
+ */
30
+ /** Outcome of stripping row-filter predicates from a rendered query. */
31
+ export interface BroadRenderResult {
32
+ /** The broad SQL (row-filter predicates removed). Equals the input when nothing was removed. */
33
+ sql: string;
34
+ /** How many top-level conjunctive predicates on the row-filter columns were removed. */
35
+ removedCount: number;
36
+ /**
37
+ * True when `removedCount` does NOT equal the caller-supplied `expectedRemovals` (the number of
38
+ * row-filter *parameter* predicates the classifier identified). This means broad-render either
39
+ * matched MORE predicates than there are parameters (a static predicate on the same column, or a
40
+ * same-named column on another table — this matcher is qualifier-blind) or FEWER (a parameter
41
+ * predicate it could not find). In either case the parameter predicate cannot be cleanly isolated,
42
+ * so re-applying only the filter column at read time would include/exclude rows the live query
43
+ * never would — the caller MUST refuse to materialize. Always false when `expectedRemovals` is
44
+ * omitted (the matcher makes no exact-count claim).
45
+ */
46
+ ambiguous: boolean;
47
+ }
48
+ /**
49
+ * Produces the broad source SQL for a row-filter materialization by removing the top-level
50
+ * conjunctive `WHERE` predicate(s) on `rowFilterColumns` from a **rendered** (concrete, non-templated)
51
+ * query. Other conjuncts are preserved; if the row-filter predicates were the only ones, the `WHERE`
52
+ * is dropped entirely.
53
+ *
54
+ * Returns the input unchanged with `removedCount: 0` when the SQL cannot be parsed as a single
55
+ * simple SELECT, or when no matching predicate is found — the caller should treat `removedCount: 0`
56
+ * on a query it believes is RowFilterBroad as an anomaly worth refusing (a broad materialization
57
+ * that still carries the filter would be wrong).
58
+ *
59
+ * When `expectedRemovals` is supplied (the classifier's row-filter *parameter* count), the result's
60
+ * `ambiguous` flag is set whenever the actual match count differs from it — and when ambiguous, the
61
+ * original SQL is returned **unmodified** (never a partially/over-stripped query), so a caller that
62
+ * ignores the flag fails safe (it would then still carry the filter, which is refused upstream rather
63
+ * than silently broadened). Omit `expectedRemovals` to keep the legacy "strip all matches" behavior
64
+ * with `ambiguous` always false.
65
+ */
66
+ export declare function buildBroadRowFilterSQL(renderedSQL: string, rowFilterColumns: string[], dialect: SQLParserDialect, expectedRemovals?: number): BroadRenderResult;
67
+ //# sourceMappingURL=materializationBroadRender.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"materializationBroadRender.d.ts","sourceRoot":"","sources":["../../src/Database/materializationBroadRender.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAMpE;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,wEAAwE;AACxE,MAAM,WAAW,iBAAiB;IAC9B,gGAAgG;IAChG,GAAG,EAAE,MAAM,CAAC;IACZ,wFAAwF;IACxF,YAAY,EAAE,MAAM,CAAC;IACrB;;;;;;;;;OASG;IACH,SAAS,EAAE,OAAO,CAAC;CACtB;AAsDD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,sBAAsB,CAClC,WAAW,EAAE,MAAM,EACnB,gBAAgB,EAAE,MAAM,EAAE,EAC1B,OAAO,EAAE,gBAAgB,EACzB,gBAAgB,CAAC,EAAE,MAAM,GAC1B,iBAAiB,CAoCnB"}
@@ -0,0 +1,104 @@
1
+ import { SQLParser } from '@memberjunction/sql-parser';
2
+ import { COMPARISON_OPS, isObject, nodeType, isLiteralNode, columnName, isAllLiteralBag, } from './materializationSqlAst.js';
3
+ /** Flattens a top-level AND-chain into its individual conjuncts (non-AND leaves). */
4
+ function collectConjuncts(node) {
5
+ if (nodeType(node) === 'binary_expr' && isObject(node) && node.operator === 'AND') {
6
+ return [...collectConjuncts(node.left), ...collectConjuncts(node.right)];
7
+ }
8
+ return [node];
9
+ }
10
+ /**
11
+ * True when a conjunct is a removable row-filter predicate: a comparison `column <op> value`
12
+ * (or `column IN/BETWEEN (literals)`) where the column is one of `cols` and the other side is a
13
+ * literal / all-literal bag. Anything else (OR-expr, function, column=column, subquery) is kept.
14
+ */
15
+ function isRemovablePredicate(conjunct, cols) {
16
+ if (nodeType(conjunct) !== 'binary_expr' || !isObject(conjunct)) {
17
+ return false;
18
+ }
19
+ const op = conjunct.operator;
20
+ if (typeof op !== 'string' || !COMPARISON_OPS.has(op)) {
21
+ return false;
22
+ }
23
+ const leftCol = columnName(conjunct.left);
24
+ const rightCol = columnName(conjunct.right);
25
+ const leftMatches = leftCol != null && cols.has(leftCol.trim().toLowerCase());
26
+ const rightMatches = rightCol != null && cols.has(rightCol.trim().toLowerCase());
27
+ if (leftMatches && !rightMatches) {
28
+ return isLiteralNode(conjunct.right) || isAllLiteralBag(conjunct.right);
29
+ }
30
+ if (rightMatches && !leftMatches) {
31
+ return isLiteralNode(conjunct.left) || isAllLiteralBag(conjunct.left);
32
+ }
33
+ return false;
34
+ }
35
+ /** Rebuilds a left-deep AND-chain from kept conjuncts, or null when none remain. */
36
+ function rebuildWhere(kept) {
37
+ if (kept.length === 0) {
38
+ return null;
39
+ }
40
+ let node = kept[0];
41
+ for (let i = 1; i < kept.length; i++) {
42
+ node = { type: 'binary_expr', operator: 'AND', left: node, right: kept[i] };
43
+ }
44
+ return node;
45
+ }
46
+ /** Returns the sole statement of an AST, or null if it is not exactly one statement. */
47
+ function soleStatement(ast) {
48
+ const node = Array.isArray(ast) ? (ast.length === 1 ? ast[0] : null) : ast;
49
+ return isObject(node) ? node : null;
50
+ }
51
+ /**
52
+ * Produces the broad source SQL for a row-filter materialization by removing the top-level
53
+ * conjunctive `WHERE` predicate(s) on `rowFilterColumns` from a **rendered** (concrete, non-templated)
54
+ * query. Other conjuncts are preserved; if the row-filter predicates were the only ones, the `WHERE`
55
+ * is dropped entirely.
56
+ *
57
+ * Returns the input unchanged with `removedCount: 0` when the SQL cannot be parsed as a single
58
+ * simple SELECT, or when no matching predicate is found — the caller should treat `removedCount: 0`
59
+ * on a query it believes is RowFilterBroad as an anomaly worth refusing (a broad materialization
60
+ * that still carries the filter would be wrong).
61
+ *
62
+ * When `expectedRemovals` is supplied (the classifier's row-filter *parameter* count), the result's
63
+ * `ambiguous` flag is set whenever the actual match count differs from it — and when ambiguous, the
64
+ * original SQL is returned **unmodified** (never a partially/over-stripped query), so a caller that
65
+ * ignores the flag fails safe (it would then still carry the filter, which is refused upstream rather
66
+ * than silently broadened). Omit `expectedRemovals` to keep the legacy "strip all matches" behavior
67
+ * with `ambiguous` always false.
68
+ */
69
+ export function buildBroadRowFilterSQL(renderedSQL, rowFilterColumns, dialect, expectedRemovals) {
70
+ // ambiguous is only ever asserted when the caller told us how many parameter predicates to expect.
71
+ const verdict = (sql, removedCount) => ({
72
+ sql,
73
+ removedCount,
74
+ ambiguous: expectedRemovals != null && removedCount !== expectedRemovals,
75
+ });
76
+ if (!rowFilterColumns || rowFilterColumns.length === 0) {
77
+ return verdict(renderedSQL, 0);
78
+ }
79
+ const parsed = SQLParser.Astify(renderedSQL, dialect);
80
+ if (!parsed.astParsed || parsed.ast == null) {
81
+ return verdict(renderedSQL, 0);
82
+ }
83
+ const stmt = soleStatement(parsed.ast);
84
+ if (stmt == null || nodeType(stmt) !== 'select' || stmt.where == null) {
85
+ return verdict(renderedSQL, 0);
86
+ }
87
+ const cols = new Set(rowFilterColumns.map((c) => c.trim().toLowerCase()));
88
+ const conjuncts = collectConjuncts(stmt.where);
89
+ const kept = conjuncts.filter((c) => !isRemovablePredicate(c, cols));
90
+ const removedCount = conjuncts.length - kept.length;
91
+ if (removedCount === 0) {
92
+ return verdict(renderedSQL, 0);
93
+ }
94
+ // Fail safe: when the match count doesn't match the expected parameter count, the parameter
95
+ // predicate can't be cleanly isolated — return the ORIGINAL SQL (unstripped) and flag ambiguous
96
+ // so an inattentive caller carries the filter (refused upstream) rather than materializing broad.
97
+ if (expectedRemovals != null && removedCount !== expectedRemovals) {
98
+ return verdict(renderedSQL, removedCount);
99
+ }
100
+ stmt.where = rebuildWhere(kept);
101
+ const sql = SQLParser.SqlifyAST(parsed.ast, dialect);
102
+ return verdict(sql, removedCount);
103
+ }
104
+ //# sourceMappingURL=materializationBroadRender.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"materializationBroadRender.js","sourceRoot":"","sources":["../../src/Database/materializationBroadRender.ts"],"names":[],"mappings":"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"}