@memberjunction/codegen-lib 6.1.0-edge.1 → 6.1.0-edge.3
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/LICENSE +180 -4
- package/README.md +39 -22
- package/dist/Angular/angular-codegen.d.ts +0 -8
- package/dist/Angular/angular-codegen.d.ts.map +1 -1
- package/dist/Angular/angular-codegen.js +32 -22
- package/dist/Angular/angular-codegen.js.map +1 -1
- package/dist/Angular/entity-data-grid-related-entity-component.js +2 -2
- package/dist/Angular/entity-data-grid-related-entity-component.js.map +1 -1
- package/dist/Angular/related-entity-components.d.ts +1 -1
- package/dist/Angular/related-entity-components.js +1 -1
- package/dist/Database/codeGenDatabaseProvider.d.ts +107 -1
- package/dist/Database/codeGenDatabaseProvider.d.ts.map +1 -1
- package/dist/Database/codeGenDatabaseProvider.js +77 -10
- package/dist/Database/codeGenDatabaseProvider.js.map +1 -1
- package/dist/Database/manage-metadata.d.ts +354 -14
- package/dist/Database/manage-metadata.d.ts.map +1 -1
- package/dist/Database/manage-metadata.js +1308 -65
- package/dist/Database/manage-metadata.js.map +1 -1
- package/dist/Database/materializationAnalysis.d.ts +241 -0
- package/dist/Database/materializationAnalysis.d.ts.map +1 -0
- package/dist/Database/materializationAnalysis.js +415 -0
- package/dist/Database/materializationAnalysis.js.map +1 -0
- package/dist/Database/materializationBroadRender.d.ts +67 -0
- package/dist/Database/materializationBroadRender.d.ts.map +1 -0
- package/dist/Database/materializationBroadRender.js +104 -0
- package/dist/Database/materializationBroadRender.js.map +1 -0
- package/dist/Database/materializationDrift.d.ts +59 -0
- package/dist/Database/materializationDrift.d.ts.map +1 -0
- package/dist/Database/materializationDrift.js +85 -0
- package/dist/Database/materializationDrift.js.map +1 -0
- package/dist/Database/materializationParamClassifier.d.ts +92 -0
- package/dist/Database/materializationParamClassifier.d.ts.map +1 -0
- package/dist/Database/materializationParamClassifier.js +224 -0
- package/dist/Database/materializationParamClassifier.js.map +1 -0
- package/dist/Database/materializationParamVerifier.d.ts +71 -0
- package/dist/Database/materializationParamVerifier.d.ts.map +1 -0
- package/dist/Database/materializationParamVerifier.js +302 -0
- package/dist/Database/materializationParamVerifier.js.map +1 -0
- package/dist/Database/materializationSqlAst.d.ts +78 -0
- package/dist/Database/materializationSqlAst.d.ts.map +1 -0
- package/dist/Database/materializationSqlAst.js +142 -0
- package/dist/Database/materializationSqlAst.js.map +1 -0
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts +69 -30
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts.map +1 -1
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js +325 -216
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js.map +1 -1
- package/dist/Database/providers/postgresql/__tests__/PostgreSQLCodeGenProvider.materialization.test.d.ts +2 -0
- package/dist/Database/providers/postgresql/__tests__/PostgreSQLCodeGenProvider.materialization.test.d.ts.map +1 -0
- package/dist/Database/providers/postgresql/__tests__/PostgreSQLCodeGenProvider.materialization.test.js +119 -0
- package/dist/Database/providers/postgresql/__tests__/PostgreSQLCodeGenProvider.materialization.test.js.map +1 -0
- package/dist/Database/providers/postgresql/__tests__/PostgreSQLHierarchyFunctions.test.d.ts +2 -0
- package/dist/Database/providers/postgresql/__tests__/PostgreSQLHierarchyFunctions.test.d.ts.map +1 -0
- package/dist/Database/providers/postgresql/__tests__/PostgreSQLHierarchyFunctions.test.js +155 -0
- package/dist/Database/providers/postgresql/__tests__/PostgreSQLHierarchyFunctions.test.js.map +1 -0
- package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts +61 -2
- package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts.map +1 -1
- package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js +306 -3
- package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js.map +1 -1
- package/dist/Database/providers/sqlserver/__tests__/BaseViewEmissionGoldenMaster.test.d.ts +2 -0
- package/dist/Database/providers/sqlserver/__tests__/BaseViewEmissionGoldenMaster.test.d.ts.map +1 -0
- package/dist/Database/providers/sqlserver/__tests__/BaseViewEmissionGoldenMaster.test.js +547 -0
- package/dist/Database/providers/sqlserver/__tests__/BaseViewEmissionGoldenMaster.test.js.map +1 -0
- package/dist/Database/providers/sqlserver/__tests__/BaseViewRegenDecision.test.d.ts +2 -0
- package/dist/Database/providers/sqlserver/__tests__/BaseViewRegenDecision.test.d.ts.map +1 -0
- package/dist/Database/providers/sqlserver/__tests__/BaseViewRegenDecision.test.js +721 -0
- package/dist/Database/providers/sqlserver/__tests__/BaseViewRegenDecision.test.js.map +1 -0
- package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.materialization.test.d.ts +2 -0
- package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.materialization.test.d.ts.map +1 -0
- package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.materialization.test.js +78 -0
- package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.materialization.test.js.map +1 -0
- package/dist/Database/providers/sqlserver/__tests__/SQLServerHierarchyTVF.test.d.ts +2 -0
- package/dist/Database/providers/sqlserver/__tests__/SQLServerHierarchyTVF.test.d.ts.map +1 -0
- package/dist/Database/providers/sqlserver/__tests__/SQLServerHierarchyTVF.test.js +155 -0
- package/dist/Database/providers/sqlserver/__tests__/SQLServerHierarchyTVF.test.js.map +1 -0
- package/dist/Database/sql_codegen.d.ts +21 -5
- package/dist/Database/sql_codegen.d.ts.map +1 -1
- package/dist/Database/sql_codegen.js +85 -27
- package/dist/Database/sql_codegen.js.map +1 -1
- package/dist/Misc/action_subclasses_codegen.d.ts.map +1 -1
- package/dist/Misc/action_subclasses_codegen.js +4 -4
- package/dist/Misc/action_subclasses_codegen.js.map +1 -1
- package/dist/Misc/createNewUser.js +1 -1
- package/dist/Misc/createNewUser.js.map +1 -1
- package/dist/Misc/entity_subclasses_codegen.d.ts +44 -0
- package/dist/Misc/entity_subclasses_codegen.d.ts.map +1 -1
- package/dist/Misc/entity_subclasses_codegen.js +369 -156
- package/dist/Misc/entity_subclasses_codegen.js.map +1 -1
- package/dist/Misc/graphql_server_codegen.d.ts +4 -58
- package/dist/Misc/graphql_server_codegen.d.ts.map +1 -1
- package/dist/Misc/graphql_server_codegen.js +22 -236
- package/dist/Misc/graphql_server_codegen.js.map +1 -1
- package/dist/Misc/sql_logging.d.ts +25 -0
- package/dist/Misc/sql_logging.d.ts.map +1 -1
- package/dist/Misc/sql_logging.js +51 -8
- package/dist/Misc/sql_logging.js.map +1 -1
- 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":"AACA,OAAO,EAAE,SAAS,EAAwB,MAAM,4BAA4B,CAAC;AAE7E,OAAO,EACH,QAAQ,EAAE,QAAQ,EAAE,eAAe,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,aAAa,GAE3F,MAAM,yBAAyB,CAAC;AAsCjC;;;GAGG;AACH,MAAM,CAAC,MAAM,gCAAgC,GAAG,wBAAwB,CAAC;AAEzE,+FAA+F;AAC/F,MAAM,CAAC,MAAM,0BAA0B,GAAG,mBAAmB,CAAC;AAE9D;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,8BAA8B,CAAC,IAM9C;IACG,MAAM,mBAAmB,GAAG,gCAAgC,CAAC;IAC7D,MAAM,gBAAgB,GAAG,IAAI,CAAC,gBAAgB,IAAI,0BAA0B,CAAC;IAC7E,MAAM,IAAI,GAAG,CAAC,MAAc,EAA2B,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,EAAE,mBAAmB,EAAE,CAAC,CAAC;IAE3H,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;QACvB,OAAO,IAAI,CAAC,UAAU,IAAI,CAAC,SAAS,qEAAqE,CAAC,CAAC;IAC/G,CAAC;IACD,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3C,OAAO,IAAI,CAAC,UAAU,IAAI,CAAC,SAAS,iFAAiF,CAAC,CAAC;IAC3H,CAAC;IACD,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,KAAK,mBAAmB,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC;QAC7F,OAAO,IAAI,CAAC,UAAU,IAAI,CAAC,SAAS,yCAAyC,mBAAmB,uDAAuD,CAAC,CAAC;IAC7J,CAAC;IAED,MAAM,SAAS,GAA2B;QACtC,IAAI,EAAE,mBAAmB;QACzB,OAAO,EAAE,gBAAgB;QACzB,QAAQ,EAAE,KAAK;QACf,YAAY,EAAE,IAAI;KACrB,CAAC;IACF,4FAA4F;IAC5F,MAAM,WAAW,GAA6B,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAClE,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,OAAO,EAAE,CAAC,CAAC,WAAW;QACtB,QAAQ,EAAE,IAAI;QACd,YAAY,EAAE,KAAK;KACtB,CAAC,CAAC,CAAC;IAEJ,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,SAAS,EAAE,GAAG,WAAW,CAAC,EAAE,mBAAmB,EAAE,CAAC;AAC1F,CAAC;AAeD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAwB,IAAI,GAAG,CAAS;IAC3E,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ;CACxD,CAAC,CAAC;AAoDH;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,yBAAyB,CAAC,IA6BzC;IACG,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,aAAa,EAAE,GAAG,IAAI,CAAC;IAClD,MAAM,kBAAkB,GAAG,IAAI,CAAC,kBAAkB,IAAI,KAAK,CAAC;IAC5D,MAAM,mBAAmB,GAAG,IAAI,CAAC,mBAAmB,IAAI,KAAK,CAAC;IAC9D,MAAM,MAAM,GAAG,CAAC,MAAc,EAAsB,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,gBAAgB,EAAE,EAAE,EAAE,cAAc,EAAE,EAAE,EAAE,CAAC,CAAC;IAEnJ,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACjC,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,gBAAgB,EAAE,EAAE,EAAE,cAAc,EAAE,EAAE,EAAE,CAAC;IAC5F,CAAC;IAED,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;IAC5E,MAAM,gBAAgB,GAAa,EAAE,CAAC;IACtC,MAAM,cAAc,GAA0B,EAAE,CAAC;IACjD,IAAI,aAAa,GAAG,KAAK,CAAC;IAE1B,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;QACrB,IAAI,CAAC,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;YACzB,OAAO,MAAM,CAAC,UAAU,SAAS,YAAY,CAAC,CAAC,IAAI,+IAA+I,CAAC,CAAC;QACxM,CAAC;QACD,IAAI,CAAC,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;YAC1B,aAAa,GAAG,IAAI,CAAC;YACrB,IAAI,CAAC,kBAAkB,EAAE,CAAC;gBACtB,OAAO,MAAM,CAAC,UAAU,SAAS,YAAY,CAAC,CAAC,IAAI,iGAAiG,CAAC,CAAC;YAC1J,CAAC;YACD,IAAI,CAAC,CAAC,CAAC,aAAa,IAAI,CAAC,CAAC,aAAa,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACnD,OAAO,MAAM,CAAC,UAAU,SAAS,YAAY,CAAC,CAAC,IAAI,oFAAoF,CAAC,CAAC;YAC7I,CAAC;YACD,SAAS;QACb,CAAC;QACD,0GAA0G;QAC1G,IAAI,CAAC,CAAC,CAAC,YAAY,EAAE,CAAC;YAClB,OAAO,MAAM,CAAC,UAAU,SAAS,YAAY,CAAC,CAAC,IAAI,uFAAuF,CAAC,CAAC;QAChJ,CAAC;QACD,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC;YACtD,OAAO,MAAM,CAAC,UAAU,SAAS,YAAY,CAAC,CAAC,IAAI,wBAAwB,CAAC,CAAC,YAAY,iGAAiG,CAAC,CAAC;QAChM,CAAC;QACD,iGAAiG;QACjG,iGAAiG;QACjG,2FAA2F;QAC3F,MAAM,EAAE,GAAG,CAAC,CAAC,cAAc,CAAC;QAC5B,IAAI,CAAC,EAAE,IAAI,CAAC,0BAA0B,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC;YAC7C,OAAO,MAAM,CAAC,UAAU,SAAS,YAAY,CAAC,CAAC,IAAI,iBAAiB,CAAC,CAAC,YAAY,oBAAoB,EAAE,IAAI,cAAc,uDAAuD,CAAC,GAAG,0BAA0B,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,yCAAyC,CAAC,CAAC;QAC1Q,CAAC;QACD,gGAAgG;QAChG,iGAAiG;QACjG,MAAM,QAAQ,GAAG,EAAE,KAAK,IAAI,IAAI,EAAE,KAAK,QAAQ,CAAC;QAChD,MAAM,YAAY,GAAG,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC;QAClD,IAAI,CAAC,CAAC,UAAU,KAAK,YAAY,EAAE,CAAC;YAChC,OAAO,MAAM,CAAC,UAAU,SAAS,YAAY,CAAC,CAAC,IAAI,eAAe,EAAE,eAAe,YAAY,qCAAqC,CAAC,CAAC,UAAU,IAAI,QAAQ,gCAAgC,CAAC,CAAC;QAClM,CAAC;QACD,oGAAoG;QACpG,kGAAkG;QAClG,oGAAoG;QACpG,+FAA+F;QAC/F,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;YAC7B,OAAO,MAAM,CAAC,UAAU,SAAS,YAAY,CAAC,CAAC,IAAI,iBAAiB,CAAC,CAAC,YAAY,8KAA8K,CAAC,CAAC;QACtQ,CAAC;QACD,MAAM,OAAO,GAAG,wBAAwB,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,YAAY,EAAE,CAAC,CAAC,YAAY,EAAE,CAAC,CAAC;QACjH,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC;YACpB,OAAO,MAAM,CAAC,UAAU,SAAS,YAAY,CAAC,CAAC,IAAI,iBAAiB,CAAC,CAAC,YAAY,qGAAqG,OAAO,CAAC,MAAM,mHAAmH,CAAC,CAAC;QAC9T,CAAC;QACD,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC;QACtC,cAAc,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,YAAY,EAAE,QAAQ,EAAE,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,CAAC;IACzG,CAAC;IAED,IAAI,gBAAgB,CAAC,MAAM,GAAG,CAAC,IAAI,aAAa,EAAE,CAAC;QAC/C,OAAO,MAAM,CAAC,UAAU,SAAS,0FAA0F,CAAC,CAAC;IACjI,CAAC;IACD,IAAI,aAAa,EAAE,CAAC;QAChB,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,eAAe,EAAE,gBAAgB,EAAE,EAAE,EAAE,cAAc,EAAE,EAAE,EAAE,CAAC;IACrG,CAAC;IACD,IAAI,CAAC,mBAAmB,EAAE,CAAC;QACvB,qGAAqG;QACrG,gGAAgG;QAChG,OAAO,MAAM,CAAC,UAAU,SAAS,qCAAqC,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,kIAAkI,CAAC,CAAC;IACzO,CAAC;IACD,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,cAAc,EAAE,CAAC;AAC9F,CAAC;AAED,uGAAuG;AACvG,gHAAgH;AAChH;;;;;;;;;;;GAWG;AACH,SAAS,mBAAmB,CAAC,IAAe;IACxC,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC;IACxB,IAAI,QAAQ,CAAC,EAAE,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC;QAAE,OAAO,EAAE,CAAC,OAAO,CAAC,CAAC,qCAAqC;IACvG,IAAI,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;QAAE,OAAO,EAAE,CAAC,CAAC,kCAAkC;IACpE,IAAI,QAAQ,CAAC,EAAE,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC,KAAK,CAAC,CAAC,uBAAuB;IACrF,OAAO,EAAE,CAAC;AACd,CAAC;AAED;;;;;GAKG;AACH,SAAS,mBAAmB,CAAC,GAAW,EAAE,OAAyB;IAC/D,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IAC9C,IAAI,CAAC,MAAM,CAAC,SAAS,IAAI,MAAM,CAAC,GAAG,IAAI,IAAI;QAAE,OAAO,IAAI,CAAC;IACzD,MAAM,IAAI,GAAG,aAAa,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACvC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,QAAQ,CAAC,IAAI,CAAC,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAChE,IAAI,kBAAkB,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1C,OAAO,IAAI,CAAC;AAChB,CAAC;AAED,0GAA0G;AAC1G,SAAS,eAAe,CAAC,IAAe;IACpC,OAAO,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;AAC3D,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,eAAe,CAAC,eAA8B,EAAE,YAA2B,EAAE,YAAqB;IACvG,IAAI,YAAY;QAAE,OAAO,IAAI,CAAC;IAC9B,OAAO,gBAAgB,CAAC,eAAe,EAAE,YAAY,CAAC,CAAC;AAC3D,CAAC;AAUD;;;GAGG;AACH,SAAS,uBAAuB,CAAC,IAAa,EAAE,MAAc,EAAE,KAAwB;IACpF,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACtB,KAAK,MAAM,KAAK,IAAI,IAAI;YAAE,uBAAuB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC;QACxE,OAAO;IACX,CAAC;IACD,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO;IAC5B,MAAM,EAAE,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC;IACjC,IAAI,EAAE,IAAI,IAAI,EAAE,CAAC;QACb,IAAI,gBAAgB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC;QAClE,OAAO,CAAC,qDAAqD;IACjE,CAAC;IACD,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACxC,IAAI,CAAC,KAAK,KAAK;YAAE,uBAAuB,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC;IAC/D,CAAC;AACL,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,UAAU,wBAAwB,CAAC,IAOxC;IACG,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,YAAY,EAAE,GAAG,IAAI,CAAC;IAC5C,MAAM,MAAM,GAAG,CAAC,MAAc,EAA4B,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;IAC3F,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,MAAM,CAAC,4BAA4B,CAAC,CAAC;IAEjF,IAAI,IAAsB,CAAC;IAC3B,IAAI,UAA6B,CAAC;IAClC,IAAI,CAAC;QACD,IAAI,GAAG,mBAAmB,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QACzC,UAAU,GAAG,SAAS,CAAC,oBAAoB,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IAC9D,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,MAAM,CAAC,sCAAsC,CAAC,CAAC;IAC1D,CAAC;IACD,IAAI,IAAI,IAAI,IAAI,EAAE,CAAC;QACf,OAAO,MAAM,CAAC,6IAA6I,CAAC,CAAC;IACjK,CAAC;IACD,MAAM,WAAW,GAAG,2BAA2B,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC;IAC1E,IAAI,CAAC,WAAW,CAAC,QAAQ;QAAE,OAAO,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;IAC7D,MAAM,MAAM,GAAG,WAAW,CAAC,MAAM,CAAC;IAElC,MAAM,UAAU,GAAsB,EAAE,CAAC;IACzC,uBAAuB,CAAC,IAAI,CAAC,KAAK,EAAE,YAAY,EAAE,UAAU,CAAC,CAAC;IAC9D,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,MAAM,CAAC,4EAA4E,YAAY,GAAG,CAAC,CAAC;IAC/G,CAAC;IACD,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC;IAC3F,IAAI,QAAQ,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;QACpB,OAAO,MAAM,CAAC,gCAAgC,YAAY,WAAW,QAAQ,CAAC,IAAI,6FAA6F,CAAC,CAAC;IACrL,CAAC;IACD,MAAM,kBAAkB,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC;IAEzC,MAAM,YAAY,GAAG,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACjD,IAAI,CAAC,YAAY,IAAI,CAAC,kBAAkB,IAAI,IAAI,IAAI,MAAM,CAAC,cAAc,IAAI,IAAI,CAAC,EAAE,CAAC;QACjF,OAAO,MAAM,CAAC,mBAAmB,eAAe,CAAC,IAAI,CAAC,sBAAsB,kBAAkB,IAAI,IAAI,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,2BAA2B,KAAK,YAAY,uEAAuE,CAAC,CAAC;IACzP,CAAC;IACD,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,cAAc,EAAE,kBAAkB,EAAE,YAAY,CAAC,EAAE,CAAC;QAC5E,OAAO,MAAM,CAAC,gCAAgC,kBAAkB,IAAI,YAAY,4BAA4B,YAAY,eAAe,MAAM,CAAC,cAAc,IAAI,YAAY,+BAA+B,CAAC,CAAC;IACjN,CAAC;IACD,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;AAC9B,CAAC;AAOD;;;;GAIG;AACH,SAAS,2BAA2B,CAAC,UAA6B,EAAE,YAAoB;IACpF,MAAM,EAAE,GAAG,CAAC,MAAc,EAA0B,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;IACrF,IAAI,CAAC,UAAU,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzC,OAAO,EAAE,CAAC,mCAAmC,CAAC,CAAC;IACnD,CAAC;IACD,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,GAAG,CAAC,EAAE,CAAC;QAC/C,OAAO,EAAE,CAAC,6FAA6F,CAAC,CAAC;IAC7G,CAAC;IACD,MAAM,OAAO,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,gBAAgB,CAAC,CAAC,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC,CAAC;IACvF,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,EAAE,CAAC,0CAA0C,YAAY,GAAG,CAAC,CAAC;IACzE,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrB,OAAO,EAAE,CAAC,4BAA4B,OAAO,CAAC,MAAM,0BAA0B,YAAY,GAAG,CAAC,CAAC;IACnG,CAAC;IACD,MAAM,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAC1B,IAAI,MAAM,CAAC,YAAY,EAAE,CAAC;QACtB,OAAO,EAAE,CAAC,sBAAsB,YAAY,uEAAuE,CAAC,CAAC;IACzH,CAAC;IACD,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,YAAY,EAAE,YAAY,CAAC,EAAE,CAAC;QACvD,OAAO,EAAE,CAAC,sBAAsB,YAAY,qCAAqC,MAAM,CAAC,YAAY,qCAAqC,YAAY,4CAA4C,YAAY,GAAG,CAAC,CAAC;IACtN,CAAC;IACD,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;AAC9C,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,2BAA2B,CAAC,IAI3C;IACG,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IACtC,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC,CAAC,uCAAuC;IAEtF,0GAA0G;IAC1G,sGAAsG;IACtG,iGAAiG;IACjG,uGAAuG;IACvG,yGAAyG;IACzG,wGAAwG;IACxG,uGAAuG;IACvG,kDAAkD;IAClD,IAAI,IAAsB,CAAC;IAC3B,IAAI,UAA6B,CAAC;IAClC,IAAI,CAAC;QACD,IAAI,GAAG,mBAAmB,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QACzC,UAAU,GAAG,SAAS,CAAC,oBAAoB,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IAC9D,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,IAAI,CAAC,CAAC,qEAAqE;IACtF,CAAC;IACD,IAAI,IAAI,IAAI,IAAI;QAAE,OAAO,IAAI,CAAC,CAAC,wDAAwD;IACvF,MAAM,YAAY,GAAG,mBAAmB,CAAC,IAAI,CAAC,CAAC;IAC/C,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,UAAU,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACrF,IAAI,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,YAAY,CAAC;QAAE,OAAO,IAAI,CAAC,CAAC,6BAA6B;IACtG,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,YAAY,CAAC;QAAE,OAAO,IAAI,CAAC,CAAC,4CAA4C;IAEtG,yGAAyG;IACzG,0GAA0G;IAC1G,0FAA0F;IAC1F,MAAM,YAAY,GAAG,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACjD,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IACnF,MAAM,GAAG,GAAqC,EAAE,CAAC;IACjD,KAAK,MAAM,IAAI,IAAI,YAAY,EAAE,CAAC;QAC9B,MAAM,EAAE,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC;QACjC,IAAI,EAAE,IAAI,IAAI;YAAE,OAAO,IAAI,CAAC;QAC5B,sGAAsG;QACtG,MAAM,SAAS,GAAG,UAAU,CAAC,MAAM,CAC/B,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,YAAY;eACf,gBAAgB,CAAC,CAAC,CAAC,YAAY,EAAE,EAAE,CAAC,MAAM,CAAC;eAC3C,eAAe,CAAC,CAAC,CAAC,cAAc,EAAE,EAAE,CAAC,SAAS,EAAE,YAAY,CAAC,CACvE,CAAC;QACF,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC,CAAC,2CAA2C;QACpF,MAAM,CAAC,GAAG,aAAa,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;QAC1E,IAAI,CAAC,CAAC;YAAE,OAAO,IAAI,CAAC,CAAC,yDAAyD;QAC9E,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;IACpD,CAAC;IACD,OAAO,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;AACvC,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,sBAAsB,CAAC,GAAW;IAC9C,IAAI,CAAC,GAAG;QAAE,OAAO,KAAK,CAAC;IACvB,MAAM,CAAC,GAAG,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACnC,4DAA4D;IAC5D,MAAM,WAAW,GAAG,+HAA+H,CAAC;IACpJ,IAAI,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC;IACtC,6GAA6G;IAC7G,sGAAsG;IACtG,0CAA0C;IAC1C,IAAI,kCAAkC,CAAC,IAAI,CAAC,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC;IAC7D,qDAAqD;IACrD,OAAO,qBAAqB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACzC,CAAC"}
|
|
@@ -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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"}
|