@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,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 4: materialization drift detection (plan §13 + §17.2 "flag-and-hold").
|
|
3
|
+
*
|
|
4
|
+
* A materialization snapshots a specific source shape. When CodeGen re-syncs entity metadata to the
|
|
5
|
+
* live schema, a source entity/field a materialization depends on may have been dropped, renamed, or
|
|
6
|
+
* reshaped — leaving the materialized table out of sync with what its source now produces. Per §17.2
|
|
7
|
+
* the policy is FLAG-AND-HOLD: mark the row `DriftHold` and stop refreshing (surface for review),
|
|
8
|
+
* never silently auto-rebuild.
|
|
9
|
+
*
|
|
10
|
+
* This module is the pure decision core: given already-resolved existence facts (gathered against the
|
|
11
|
+
* freshly-synced metadata by the CodeGen orchestration), decide whether a materialization has drifted
|
|
12
|
+
* and why. No DB/IO — fully unit-testable.
|
|
13
|
+
*/
|
|
14
|
+
/** Resolved drift-relevant facts for one materialization (gathered against current metadata). */
|
|
15
|
+
export interface MaterializationDriftFacts {
|
|
16
|
+
sourceType: 'Query' | 'EntityBaseView';
|
|
17
|
+
/** EntityBaseView case (1:1 copy of a source entity). */
|
|
18
|
+
baseView?: {
|
|
19
|
+
/** The source entity (SourceEntityID) still resolves in current metadata. */
|
|
20
|
+
sourceEntityExists: boolean;
|
|
21
|
+
/** Current field names on the source entity. */
|
|
22
|
+
currentEntityFields: string[];
|
|
23
|
+
/** The materialized table's current columns (what the snapshot was actually built with). */
|
|
24
|
+
materializedColumns: string[];
|
|
25
|
+
};
|
|
26
|
+
/** Query case (provenance via QueryEntity / QueryField / QueryDependency). */
|
|
27
|
+
query?: {
|
|
28
|
+
/** Names/IDs of QueryEntity source entities that no longer exist. */
|
|
29
|
+
missingSourceEntities: string[];
|
|
30
|
+
/** `Entity.Field` provenance refs (QueryField.SourceEntityID+SourceFieldName) that no longer resolve. */
|
|
31
|
+
missingSourceFields: string[];
|
|
32
|
+
/** Composed inner queries (QueryDependency.DependsOnQueryID) that no longer exist. */
|
|
33
|
+
missingComposedQueries: string[];
|
|
34
|
+
/** The query's CURRENT declared output column names (MJ: Query Fields). Empty when unknown / not yet
|
|
35
|
+
* analyzed — the output-shape check is then skipped (can't judge → don't false-flag). */
|
|
36
|
+
currentOutputColumns: string[];
|
|
37
|
+
/** The materialized table's current DATA columns, with the synthetic surrogate PK and any `__mj_*`
|
|
38
|
+
* system columns already excluded. Empty when the table can't be introspected — shape check skipped. */
|
|
39
|
+
materializedColumns: string[];
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** The drift verdict for one materialization. */
|
|
43
|
+
export interface MaterializationDriftVerdict {
|
|
44
|
+
drift: boolean;
|
|
45
|
+
reason?: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Decides whether a materialization has drifted from its source. Pure.
|
|
49
|
+
*
|
|
50
|
+
* Base-view (1:1): drift if the source entity is gone, or its current field set no longer matches the
|
|
51
|
+
* columns the snapshot was built with (a field added/removed/renamed on the source).
|
|
52
|
+
* Query: drift if any provenance dependency is broken — a source entity removed, a mapped source field
|
|
53
|
+
* removed/renamed, or a composed inner query removed. Provenance refs with unknown source (no
|
|
54
|
+
* SourceEntityID/SourceFieldName) are NOT judged here (the orchestration excludes them) to avoid false
|
|
55
|
+
* positives from incomplete provenance — the §10 bias runs the other way for drift (only flag on a
|
|
56
|
+
* definite broken dependency).
|
|
57
|
+
*/
|
|
58
|
+
export declare function evaluateMaterializationDrift(facts: MaterializationDriftFacts): MaterializationDriftVerdict;
|
|
59
|
+
//# sourceMappingURL=materializationDrift.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"materializationDrift.d.ts","sourceRoot":"","sources":["../../src/Database/materializationDrift.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,iGAAiG;AACjG,MAAM,WAAW,yBAAyB;IACtC,UAAU,EAAE,OAAO,GAAG,gBAAgB,CAAC;IACvC,yDAAyD;IACzD,QAAQ,CAAC,EAAE;QACP,6EAA6E;QAC7E,kBAAkB,EAAE,OAAO,CAAC;QAC5B,gDAAgD;QAChD,mBAAmB,EAAE,MAAM,EAAE,CAAC;QAC9B,4FAA4F;QAC5F,mBAAmB,EAAE,MAAM,EAAE,CAAC;KACjC,CAAC;IACF,8EAA8E;IAC9E,KAAK,CAAC,EAAE;QACJ,qEAAqE;QACrE,qBAAqB,EAAE,MAAM,EAAE,CAAC;QAChC,yGAAyG;QACzG,mBAAmB,EAAE,MAAM,EAAE,CAAC;QAC9B,sFAAsF;QACtF,sBAAsB,EAAE,MAAM,EAAE,CAAC;QACjC;kGAC0F;QAC1F,oBAAoB,EAAE,MAAM,EAAE,CAAC;QAC/B;iHACyG;QACzG,mBAAmB,EAAE,MAAM,EAAE,CAAC;KACjC,CAAC;CACL;AAED,iDAAiD;AACjD,MAAM,WAAW,2BAA2B;IACxC,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAID;;;;;;;;;;GAUG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,yBAAyB,GAAG,2BAA2B,CA4D1G"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 4: materialization drift detection (plan §13 + §17.2 "flag-and-hold").
|
|
3
|
+
*
|
|
4
|
+
* A materialization snapshots a specific source shape. When CodeGen re-syncs entity metadata to the
|
|
5
|
+
* live schema, a source entity/field a materialization depends on may have been dropped, renamed, or
|
|
6
|
+
* reshaped — leaving the materialized table out of sync with what its source now produces. Per §17.2
|
|
7
|
+
* the policy is FLAG-AND-HOLD: mark the row `DriftHold` and stop refreshing (surface for review),
|
|
8
|
+
* never silently auto-rebuild.
|
|
9
|
+
*
|
|
10
|
+
* This module is the pure decision core: given already-resolved existence facts (gathered against the
|
|
11
|
+
* freshly-synced metadata by the CodeGen orchestration), decide whether a materialization has drifted
|
|
12
|
+
* and why. No DB/IO — fully unit-testable.
|
|
13
|
+
*/
|
|
14
|
+
const lc = (s) => s.trim().toLowerCase();
|
|
15
|
+
/**
|
|
16
|
+
* Decides whether a materialization has drifted from its source. Pure.
|
|
17
|
+
*
|
|
18
|
+
* Base-view (1:1): drift if the source entity is gone, or its current field set no longer matches the
|
|
19
|
+
* columns the snapshot was built with (a field added/removed/renamed on the source).
|
|
20
|
+
* Query: drift if any provenance dependency is broken — a source entity removed, a mapped source field
|
|
21
|
+
* removed/renamed, or a composed inner query removed. Provenance refs with unknown source (no
|
|
22
|
+
* SourceEntityID/SourceFieldName) are NOT judged here (the orchestration excludes them) to avoid false
|
|
23
|
+
* positives from incomplete provenance — the §10 bias runs the other way for drift (only flag on a
|
|
24
|
+
* definite broken dependency).
|
|
25
|
+
*/
|
|
26
|
+
export function evaluateMaterializationDrift(facts) {
|
|
27
|
+
if (facts.sourceType === 'EntityBaseView' && facts.baseView) {
|
|
28
|
+
const bv = facts.baseView;
|
|
29
|
+
if (!bv.sourceEntityExists) {
|
|
30
|
+
return { drift: true, reason: 'source entity no longer exists' };
|
|
31
|
+
}
|
|
32
|
+
// Skip the shape comparison when EITHER set is empty (mirrors the Query branch): an empty
|
|
33
|
+
// materializedColumns — the table couldn't be introspected under the CodeGen connection (transient
|
|
34
|
+
// failure, permission/visibility edge) — would otherwise make every source field look 'added' and
|
|
35
|
+
// false-flag DriftHold on a materialization that has not actually drifted (§10: only flag on a
|
|
36
|
+
// definite mismatch). currentEntityFields is never empty for a resolved entity, but guard it for symmetry.
|
|
37
|
+
if (bv.materializedColumns.length === 0 || bv.currentEntityFields.length === 0) {
|
|
38
|
+
return { drift: false };
|
|
39
|
+
}
|
|
40
|
+
const cur = new Set(bv.currentEntityFields.map(lc));
|
|
41
|
+
const mat = new Set(bv.materializedColumns.map(lc));
|
|
42
|
+
const added = [...cur].filter((c) => !mat.has(c));
|
|
43
|
+
const removed = [...mat].filter((c) => !cur.has(c));
|
|
44
|
+
if (added.length || removed.length) {
|
|
45
|
+
return {
|
|
46
|
+
drift: true,
|
|
47
|
+
reason: `base-view shape changed vs the snapshot (source added: [${added.join(', ')}]; snapshot has orphaned: [${removed.join(', ')}])`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return { drift: false };
|
|
51
|
+
}
|
|
52
|
+
if (facts.sourceType === 'Query' && facts.query) {
|
|
53
|
+
const q = facts.query;
|
|
54
|
+
if (q.missingSourceEntities.length) {
|
|
55
|
+
return { drift: true, reason: `source entit${q.missingSourceEntities.length === 1 ? 'y' : 'ies'} removed: [${q.missingSourceEntities.join(', ')}]` };
|
|
56
|
+
}
|
|
57
|
+
if (q.missingSourceFields.length) {
|
|
58
|
+
return { drift: true, reason: `source field${q.missingSourceFields.length === 1 ? '' : 's'} removed/renamed: [${q.missingSourceFields.join(', ')}]` };
|
|
59
|
+
}
|
|
60
|
+
if (q.missingComposedQueries.length) {
|
|
61
|
+
return { drift: true, reason: `composed inner quer${q.missingComposedQueries.length === 1 ? 'y' : 'ies'} removed: [${q.missingComposedQueries.join(', ')}]` };
|
|
62
|
+
}
|
|
63
|
+
// Output-shape drift: the physical table is create-if-absent (no ALTER path), so an edit to the query's
|
|
64
|
+
// SELECT list (add/rename/remove an output column) while all source entities/fields still exist would
|
|
65
|
+
// otherwise go undetected and serve the OLD column set forever. Compare the query's current output
|
|
66
|
+
// columns against the snapshot's data columns — but ONLY when both are known (skip if either is empty:
|
|
67
|
+
// a query whose fields aren't analyzed yet, or a table we couldn't introspect, must not be false-flagged
|
|
68
|
+
// — the §10 "only flag on a definite mismatch" bias).
|
|
69
|
+
if (q.currentOutputColumns.length && q.materializedColumns.length) {
|
|
70
|
+
const cur = new Set(q.currentOutputColumns.map(lc));
|
|
71
|
+
const mat = new Set(q.materializedColumns.map(lc));
|
|
72
|
+
const added = [...cur].filter((c) => !mat.has(c));
|
|
73
|
+
const removed = [...mat].filter((c) => !cur.has(c));
|
|
74
|
+
if (added.length || removed.length) {
|
|
75
|
+
return {
|
|
76
|
+
drift: true,
|
|
77
|
+
reason: `query output shape changed vs the snapshot (query added: [${added.join(', ')}]; snapshot has orphaned: [${removed.join(', ')}]) — the create-if-absent table was not rebuilt`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return { drift: false };
|
|
82
|
+
}
|
|
83
|
+
return { drift: false };
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=materializationDrift.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"materializationDrift.js","sourceRoot":"","sources":["../../src/Database/materializationDrift.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { SQLParserDialect } from '@memberjunction/sql-dialect';
|
|
2
|
+
import { type VerifiedParamRole } from './materializationParamVerifier.js';
|
|
3
|
+
import { type ParamQualification } from './materializationAnalysis.js';
|
|
4
|
+
/**
|
|
5
|
+
* Phase 2c — deterministic parameter classification for query materialization
|
|
6
|
+
* (plan /plans/query-entity-materialization.md §9 / §10).
|
|
7
|
+
*
|
|
8
|
+
* Orchestrates the render-and-diff oracle ({@link verifyParamRole}) across every parameter of a
|
|
9
|
+
* query and folds the per-parameter verdicts into the overall {@link qualifyParameterizedQuery}
|
|
10
|
+
* decision. For each parameter it renders the query template ≥2 times — that parameter varied
|
|
11
|
+
* across distinct probe values while **all others are held fixed** — and asks the verifier whether
|
|
12
|
+
* the variation is a clean row filter, a structural change, or unverifiable.
|
|
13
|
+
*
|
|
14
|
+
* **Deterministic-only (v1):** probe values are generated from each parameter's declared type; no
|
|
15
|
+
* LLM is involved (the LLM proposer is deferred to the Bucket-2 / bounded-domain phase). The
|
|
16
|
+
* verifier is the oracle — this module never trusts author intent.
|
|
17
|
+
*
|
|
18
|
+
* The renderer is **injected** ({@link VariantRenderer}) so this module stays free of the Nunjucks
|
|
19
|
+
* engine and the per-request platform singleton: the soundness-critical orchestration is pure and
|
|
20
|
+
* fully unit-testable, and the manage-metadata caller supplies the production renderer.
|
|
21
|
+
*/
|
|
22
|
+
/** A query parameter's declared type (mirrors `MJ: Query Parameter`.Type). */
|
|
23
|
+
export type QueryParamType = 'string' | 'number' | 'date' | 'boolean' | 'array';
|
|
24
|
+
/** Subset of a `MJ: Query Parameter` row needed to probe and classify the parameter. */
|
|
25
|
+
export interface QueryParamDef {
|
|
26
|
+
/** Parameter name (the Nunjucks variable). */
|
|
27
|
+
Name: string;
|
|
28
|
+
/** Declared type — drives probe-value generation and the held value. */
|
|
29
|
+
Type: QueryParamType;
|
|
30
|
+
/** Author-provided example value (used as the held value for *other* params when valid). */
|
|
31
|
+
SampleValue?: string | null;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Renders the query template to final SQL for a complete parameter-value map. Deterministic and
|
|
35
|
+
* side-effect free from this module's perspective. Supplied by the caller (production: the
|
|
36
|
+
* Nunjucks render pipeline with SQL-safe filters). May throw if a value breaks the template.
|
|
37
|
+
*/
|
|
38
|
+
export type VariantRenderer = (paramValues: Record<string, unknown>) => string;
|
|
39
|
+
/** A single parameter's classification outcome (for logging / diagnostics). */
|
|
40
|
+
export interface ParamVerdict {
|
|
41
|
+
name: string;
|
|
42
|
+
verdict: VerifiedParamRole;
|
|
43
|
+
}
|
|
44
|
+
/** Result of classifying all of a query's parameters. */
|
|
45
|
+
export interface QueryParamClassification {
|
|
46
|
+
/** The overall qualification (drives whether/how the query materializes). */
|
|
47
|
+
qualification: ParamQualification;
|
|
48
|
+
/** Per-parameter verdicts, in declaration order (for precise logs). */
|
|
49
|
+
perParam: ParamVerdict[];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Generates ≥2 distinct probe values for a parameter type. Booleans have `[true, false]`. String/number/date
|
|
53
|
+
* get truthy variants PLUS a falsy one (`''`, `0`, `''`) so a CONDITIONAL predicate (`{% if param %}WHERE …
|
|
54
|
+
* {% endif %}`) reveals itself: the falsy value renders the FALSE branch (no WHERE), which the AST oracle diffs
|
|
55
|
+
* as a structural change vs. the truthy renders → the parameter is refused as `Structural` instead of being
|
|
56
|
+
* mis-classified as an unconditional row filter (which would materialize broad and then WRONGLY apply the
|
|
57
|
+
* predicate at read time when the param is empty/omitted, diverging from the live query). For these types an
|
|
58
|
+
* unconditional row filter (`WHERE col = {{param}}`) renders the falsy value as a plain LITERAL (`= ''`/`= 0`),
|
|
59
|
+
* same SQL shape → stays `RowFilter`, so it isn't over-refused.
|
|
60
|
+
*
|
|
61
|
+
* Arrays deliberately get NO empty probe: an empty array renders `IN ()`, which changes the SQL SHAPE even for
|
|
62
|
+
* an UNCONDITIONAL `WHERE x IN {{arr}}` row filter — the oracle can't distinguish that from a conditional, so
|
|
63
|
+
* an empty probe would over-refuse the legitimate (and supported) varying-length IN-list row filter. The rare
|
|
64
|
+
* conditional-array param (`{% if arr %}… IN {{arr}} …{% endif %}`) is therefore a documented residual, not
|
|
65
|
+
* covered by this structural probe. A falsy value that breaks the template render fails safe (→ Unbounded →
|
|
66
|
+
* refused). Truthy values are kept FIRST so the held baseline is unaffected (holdValue is independent).
|
|
67
|
+
*/
|
|
68
|
+
export declare function probeValues(type: QueryParamType): unknown[];
|
|
69
|
+
/** Builds the base value map with every parameter held at its stable value. */
|
|
70
|
+
export declare function buildHeldValues(params: QueryParamDef[]): Record<string, unknown>;
|
|
71
|
+
/**
|
|
72
|
+
* Classifies every parameter of a query and folds the verdicts into the overall qualification.
|
|
73
|
+
*
|
|
74
|
+
* Pure orchestration: pass the rendered-SQL producer via `render`. Returns both the qualification
|
|
75
|
+
* (feeds the materialization gate) and the per-parameter verdicts (for precise logging).
|
|
76
|
+
*
|
|
77
|
+
* Note: bounded-domain (`Structural` Bucket-2) classification is **not** populated in v1 — verdicts
|
|
78
|
+
* carry no `boundedDomain`, so a structural parameter qualifies only if `allowPerValueCache` is on
|
|
79
|
+
* (off by default) *and* a domain is supplied, which it never is here → structural ⇒ refuse. This is
|
|
80
|
+
* intentional: per-value caching is a later phase.
|
|
81
|
+
*/
|
|
82
|
+
export declare function classifyQueryParameters(opts: {
|
|
83
|
+
queryName: string;
|
|
84
|
+
params: QueryParamDef[];
|
|
85
|
+
outputColumns: string[];
|
|
86
|
+
dialect: SQLParserDialect;
|
|
87
|
+
render: VariantRenderer;
|
|
88
|
+
allowPerValueCache?: boolean;
|
|
89
|
+
/** Phase-2 enablement for Bucket-1 row-filter broad materialization (default false → refuse, stay live-only). */
|
|
90
|
+
allowRowFilterBroad?: boolean;
|
|
91
|
+
}): QueryParamClassification;
|
|
92
|
+
//# sourceMappingURL=materializationParamClassifier.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"materializationParamClassifier.d.ts","sourceRoot":"","sources":["../../src/Database/materializationParamClassifier.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AACpE,OAAO,EAAmB,KAAK,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACzF,OAAO,EAAuD,KAAK,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAEzH;;;;;;;;;;;;;;;;;GAiBG;AAEH,8EAA8E;AAC9E,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC;AAEhF,wFAAwF;AACxF,MAAM,WAAW,aAAa;IAC1B,8CAA8C;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,wEAAwE;IACxE,IAAI,EAAE,cAAc,CAAC;IACrB,4FAA4F;IAC5F,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,MAAM,CAAC;AAE/E,+EAA+E;AAC/E,MAAM,WAAW,YAAY;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,iBAAiB,CAAC;CAC9B;AAED,yDAAyD;AACzD,MAAM,WAAW,wBAAwB;IACrC,6EAA6E;IAC7E,aAAa,EAAE,kBAAkB,CAAC;IAClC,uEAAuE;IACvE,QAAQ,EAAE,YAAY,EAAE,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,EAAE,CAa3D;AA8CD,+EAA+E;AAC/E,wBAAgB,eAAe,CAAC,MAAM,EAAE,aAAa,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAMhF;AAoGD;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,aAAa,EAAE,CAAC;IACxB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,OAAO,EAAE,gBAAgB,CAAC;IAC1B,MAAM,EAAE,eAAe,CAAC;IACxB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,iHAAiH;IACjH,mBAAmB,CAAC,EAAE,OAAO,CAAC;CACjC,GAAG,wBAAwB,CAyC3B"}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { verifyParamRole } from './materializationParamVerifier.js';
|
|
2
|
+
import { qualifyParameterizedQuery } from './materializationAnalysis.js';
|
|
3
|
+
/**
|
|
4
|
+
* Generates ≥2 distinct probe values for a parameter type. Booleans have `[true, false]`. String/number/date
|
|
5
|
+
* get truthy variants PLUS a falsy one (`''`, `0`, `''`) so a CONDITIONAL predicate (`{% if param %}WHERE …
|
|
6
|
+
* {% endif %}`) reveals itself: the falsy value renders the FALSE branch (no WHERE), which the AST oracle diffs
|
|
7
|
+
* as a structural change vs. the truthy renders → the parameter is refused as `Structural` instead of being
|
|
8
|
+
* mis-classified as an unconditional row filter (which would materialize broad and then WRONGLY apply the
|
|
9
|
+
* predicate at read time when the param is empty/omitted, diverging from the live query). For these types an
|
|
10
|
+
* unconditional row filter (`WHERE col = {{param}}`) renders the falsy value as a plain LITERAL (`= ''`/`= 0`),
|
|
11
|
+
* same SQL shape → stays `RowFilter`, so it isn't over-refused.
|
|
12
|
+
*
|
|
13
|
+
* Arrays deliberately get NO empty probe: an empty array renders `IN ()`, which changes the SQL SHAPE even for
|
|
14
|
+
* an UNCONDITIONAL `WHERE x IN {{arr}}` row filter — the oracle can't distinguish that from a conditional, so
|
|
15
|
+
* an empty probe would over-refuse the legitimate (and supported) varying-length IN-list row filter. The rare
|
|
16
|
+
* conditional-array param (`{% if arr %}… IN {{arr}} …{% endif %}`) is therefore a documented residual, not
|
|
17
|
+
* covered by this structural probe. A falsy value that breaks the template render fails safe (→ Unbounded →
|
|
18
|
+
* refused). Truthy values are kept FIRST so the held baseline is unaffected (holdValue is independent).
|
|
19
|
+
*/
|
|
20
|
+
export function probeValues(type) {
|
|
21
|
+
switch (type) {
|
|
22
|
+
case 'string':
|
|
23
|
+
return ['__mj_probe_alpha', '__mj_probe_beta', '__mj_probe_gamma', ''];
|
|
24
|
+
case 'number':
|
|
25
|
+
return [101, 202, 303, 0];
|
|
26
|
+
case 'date':
|
|
27
|
+
return ['2020-01-15', '2021-06-15', '2022-12-31', ''];
|
|
28
|
+
case 'boolean':
|
|
29
|
+
return [true, false];
|
|
30
|
+
case 'array':
|
|
31
|
+
return [['__mj_a', '__mj_b'], ['__mj_c'], ['__mj_d', '__mj_e', '__mj_f']];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Parses a parameter's SampleValue into the runtime shape its type expects (best-effort). */
|
|
35
|
+
function parseSampleValue(type, sample) {
|
|
36
|
+
switch (type) {
|
|
37
|
+
case 'number': {
|
|
38
|
+
const n = Number(sample);
|
|
39
|
+
return Number.isFinite(n) ? n : undefined;
|
|
40
|
+
}
|
|
41
|
+
case 'boolean':
|
|
42
|
+
return sample.trim().toLowerCase() === 'true';
|
|
43
|
+
case 'array':
|
|
44
|
+
return sample.includes(',') ? sample.split(',').map((s) => s.trim()) : [sample];
|
|
45
|
+
case 'string':
|
|
46
|
+
case 'date':
|
|
47
|
+
return sample;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** A stable value to hold a parameter at while a *different* parameter is varied. */
|
|
51
|
+
function holdValue(param) {
|
|
52
|
+
if (param.SampleValue != null && param.SampleValue.trim().length > 0) {
|
|
53
|
+
const parsed = parseSampleValue(param.Type, param.SampleValue);
|
|
54
|
+
if (parsed !== undefined) {
|
|
55
|
+
return parsed;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return defaultHoldValue(param.Type);
|
|
59
|
+
}
|
|
60
|
+
/** Type-based fallback held value when a parameter has no usable SampleValue. */
|
|
61
|
+
function defaultHoldValue(type) {
|
|
62
|
+
switch (type) {
|
|
63
|
+
case 'string':
|
|
64
|
+
return '__mj_hold';
|
|
65
|
+
case 'number':
|
|
66
|
+
return 1;
|
|
67
|
+
case 'date':
|
|
68
|
+
return '2020-01-01';
|
|
69
|
+
case 'boolean':
|
|
70
|
+
return true;
|
|
71
|
+
case 'array':
|
|
72
|
+
return ['__mj_hold'];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** Builds the base value map with every parameter held at its stable value. */
|
|
76
|
+
export function buildHeldValues(params) {
|
|
77
|
+
const held = {};
|
|
78
|
+
for (const p of params) {
|
|
79
|
+
held[p.Name] = holdValue(p);
|
|
80
|
+
}
|
|
81
|
+
return held;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Renders the variant SQL strings for one parameter varied across its probe values, all other
|
|
85
|
+
* parameters held fixed. Returns null if any render throws (the parameter is then unverifiable).
|
|
86
|
+
*/
|
|
87
|
+
function renderVariantsForParam(param, held, render) {
|
|
88
|
+
const variants = [];
|
|
89
|
+
for (const v of probeValues(param.Type)) {
|
|
90
|
+
try {
|
|
91
|
+
variants.push(render({ ...held, [param.Name]: v }));
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return null; // a probe value broke the template → cannot verify this parameter
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return variants;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* A distinctive sentinel value per parameter type for the value-passthrough guard, chosen so its verbatim
|
|
101
|
+
* appearance in a rendered query is proof the value survived template rendering unchanged (extremely
|
|
102
|
+
* unlikely to occur coincidentally). Booleans/dates use a plain value that a SQL-formatting filter will
|
|
103
|
+
* typically coerce (`true`→`1`, a reformatted date) — which the guard then treats as non-passthrough.
|
|
104
|
+
*/
|
|
105
|
+
function passthroughSentinel(type) {
|
|
106
|
+
// Each sentinel deliberately combines features the common value-transforming filters would alter, so the
|
|
107
|
+
// verbatim-contains check catches them:
|
|
108
|
+
// - STRING/ARRAY: leading/trailing + interior WHITESPACE (trim/ltrim/rtrim/collapse), MIXED CASE (upper/
|
|
109
|
+
// lower/capitalize/title), and DIGITS + SEPARATORS — a dash and a dot — so a normalizing `replace('-','')`
|
|
110
|
+
// / `replace('.','')` / digit-strip alters it. No single-quote (which sqlString escapes → would
|
|
111
|
+
// false-refuse an identity render).
|
|
112
|
+
// - NUMBER: a NEGATIVE, NON-INTEGER value. An integer-preserving numeric filter (round/int/floor/ceil)
|
|
113
|
+
// drops the fractional part and abs drops the sign, so any of them changes it → refuse — while sqlNumber
|
|
114
|
+
// renders it verbatim, so a genuine identity render still passes. (A plain integer sentinel would be
|
|
115
|
+
// identity under round/int and slip through.)
|
|
116
|
+
// - DATE: a time + non-UTC offset that rolls to the NEXT calendar day in UTC, so sqlDate's
|
|
117
|
+
// `new Date(x).toISOString()` normalization changes the date portion → refuse. In practice every SQL-safe
|
|
118
|
+
// date filter reformats, so date row-filters are conservatively refused (a never-wrong missed optimization).
|
|
119
|
+
// The guard is conservative best-effort, NOT a proof: a pathological custom filter that is identity on this
|
|
120
|
+
// exact sentinel could still slip through (plan §9) — most realistically a `replace()` of a character the
|
|
121
|
+
// sentinel happens not to contain.
|
|
122
|
+
switch (type) {
|
|
123
|
+
case 'string': return ' Mj0_Pt9-zQx.Kw ';
|
|
124
|
+
case 'number': return -305419896.5073; // negative + fractional → catches round/int/floor/ceil/abs; sqlNumber renders verbatim
|
|
125
|
+
case 'date': return '2019-03-14T20:00:00-05:00'; // → 2019-03-15T01:00:00.000Z in UTC: sqlDate changes the date portion
|
|
126
|
+
case 'boolean': return true;
|
|
127
|
+
case 'array': return [' Mj0_PtA9-zx. ', ' Mj0_PtB9-zx. '];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Phase-2 value-passthrough guard (plan §9). A proven `RowFilter` is only safe to inject at read time if
|
|
132
|
+
* the template renders the parameter's value UNCHANGED into the predicate literal — because the read path
|
|
133
|
+
* binds the caller's **raw** value. If the template transforms it (`{{ x | upper | sqlString }}`, a custom
|
|
134
|
+
* filter) the materialized read would filter on a different value than the live query → silently wrong rows.
|
|
135
|
+
*
|
|
136
|
+
* We render the query with a distinctive sentinel and require it to appear VERBATIM in the rendered SQL.
|
|
137
|
+
* If it doesn't (a value transform, or a type coercion we can't verify), the parameter is refused. This is
|
|
138
|
+
* **conservative by design** (§10 refuse-under-uncertainty): a coerced-but-faithful value (e.g. a boolean
|
|
139
|
+
* rendered as `1`) refuses too, leaving the query live-only — a missed optimization, never wrong.
|
|
140
|
+
*/
|
|
141
|
+
function isValuePassthrough(param, held, render) {
|
|
142
|
+
const sentinel = passthroughSentinel(param.Type);
|
|
143
|
+
let rendered;
|
|
144
|
+
try {
|
|
145
|
+
rendered = render({ ...held, [param.Name]: sentinel });
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return false; // cannot render the sentinel → cannot prove passthrough → refuse
|
|
149
|
+
}
|
|
150
|
+
const parts = Array.isArray(sentinel) ? sentinel : [sentinel];
|
|
151
|
+
return parts.every((part) => rendered.includes(String(part)));
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Verifies a single parameter's role by rendering its variants and running the AST oracle. `held` is the
|
|
155
|
+
* shared base value map (all params at their stable values) — computed ONCE by the caller and reused, since
|
|
156
|
+
* the baseline is identical for every parameter being probed (renderVariantsForParam overrides just the one
|
|
157
|
+
* varied param). Rebuilding it per-call would make classification O(N^2) in parameter count.
|
|
158
|
+
*/
|
|
159
|
+
function verifyOneParam(param, held, dialect, render) {
|
|
160
|
+
const variants = renderVariantsForParam(param, held, render);
|
|
161
|
+
if (variants == null) {
|
|
162
|
+
return { role: 'Unbounded', reason: `parameter "${param.Name}" produced a template error while probing — cannot verify; refusing under uncertainty` };
|
|
163
|
+
}
|
|
164
|
+
const verdict = verifyParamRole(variants, dialect);
|
|
165
|
+
// Value-passthrough guard: a proven row filter is only injectable if the raw value survives rendering
|
|
166
|
+
// into the predicate literal. A transforming template would make the materialized read diverge from live.
|
|
167
|
+
if (verdict.role === 'RowFilter' && !isValuePassthrough(param, held, render)) {
|
|
168
|
+
return {
|
|
169
|
+
role: 'Unbounded',
|
|
170
|
+
reason: `parameter "${param.Name}" filters on "${verdict.filterColumn ?? '?'}" but the template transforms its value before the predicate literal (the raw value does not survive rendering) — a materialized read would filter on a different value than the live query; refusing (query stays live-only)`,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
return verdict;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Classifies every parameter of a query and folds the verdicts into the overall qualification.
|
|
177
|
+
*
|
|
178
|
+
* Pure orchestration: pass the rendered-SQL producer via `render`. Returns both the qualification
|
|
179
|
+
* (feeds the materialization gate) and the per-parameter verdicts (for precise logging).
|
|
180
|
+
*
|
|
181
|
+
* Note: bounded-domain (`Structural` Bucket-2) classification is **not** populated in v1 — verdicts
|
|
182
|
+
* carry no `boundedDomain`, so a structural parameter qualifies only if `allowPerValueCache` is on
|
|
183
|
+
* (off by default) *and* a domain is supplied, which it never is here → structural ⇒ refuse. This is
|
|
184
|
+
* intentional: per-value caching is a later phase.
|
|
185
|
+
*/
|
|
186
|
+
export function classifyQueryParameters(opts) {
|
|
187
|
+
const { queryName, params, outputColumns, dialect, render } = opts;
|
|
188
|
+
// Compute the held baseline ONCE (not per-parameter) — see verifyOneParam.
|
|
189
|
+
const held = buildHeldValues(params);
|
|
190
|
+
const perParam = params.map((p) => ({
|
|
191
|
+
name: p.Name,
|
|
192
|
+
verdict: verifyOneParam(p, held, dialect, render),
|
|
193
|
+
}));
|
|
194
|
+
const classifications = perParam.map((pv) => ({
|
|
195
|
+
name: pv.name,
|
|
196
|
+
role: pv.verdict.role,
|
|
197
|
+
filterColumn: pv.verdict.filterColumn,
|
|
198
|
+
filterOperator: pv.verdict.filterOperator,
|
|
199
|
+
filterKind: pv.verdict.filterKind,
|
|
200
|
+
}));
|
|
201
|
+
// The qualifier needs the CONCRETE SQL to prove each RowFilter predicate binds to the materialized
|
|
202
|
+
// output column of the same name (a bare column-name match cannot see a join collision or an alias
|
|
203
|
+
// rebinding — see proveFilterColumnBinding). The held baseline render is the right instance: it is the
|
|
204
|
+
// same shape the broad-render step strips, and it still carries the row-filter predicate. If the render
|
|
205
|
+
// throws, we pass nothing and RowFilter params refuse (fail closed → the query stays live-only).
|
|
206
|
+
let heldSQL;
|
|
207
|
+
try {
|
|
208
|
+
heldSQL = render(held);
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
heldSQL = undefined;
|
|
212
|
+
}
|
|
213
|
+
const qualification = qualifyParameterizedQuery({
|
|
214
|
+
queryName,
|
|
215
|
+
params: classifications,
|
|
216
|
+
outputColumns,
|
|
217
|
+
allowPerValueCache: opts.allowPerValueCache ?? false,
|
|
218
|
+
allowRowFilterBroad: opts.allowRowFilterBroad ?? false,
|
|
219
|
+
sql: heldSQL,
|
|
220
|
+
dialect,
|
|
221
|
+
});
|
|
222
|
+
return { qualification, perParam };
|
|
223
|
+
}
|
|
224
|
+
//# sourceMappingURL=materializationParamClassifier.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"materializationParamClassifier.js","sourceRoot":"","sources":["../../src/Database/materializationParamClassifier.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { SQLParserDialect } from '@memberjunction/sql-dialect';
|
|
2
|
+
import type { ParamRole } from './materializationAnalysis.js';
|
|
3
|
+
/**
|
|
4
|
+
* Phase 2b — the render-and-diff verifier (the AST oracle for query parameterization).
|
|
5
|
+
*
|
|
6
|
+
* This is the **soundness-critical** core of parameterized materialization (plan
|
|
7
|
+
* /plans/query-entity-materialization.md §9 Bucket 1, §10 asymmetric-risk). Given a stored
|
|
8
|
+
* query's templated SQL rendered with the **same** parameter set except for **one** parameter
|
|
9
|
+
* varied across ≥2 distinct values, it decides that parameter's {@link ParamRole}:
|
|
10
|
+
*
|
|
11
|
+
* - `RowFilter` — varying the value changes only a literal at a *clean* top-level `WHERE`
|
|
12
|
+
* predicate on a single column (so the query can be materialized broad and
|
|
13
|
+
* that column filtered at read time — Phase 2d).
|
|
14
|
+
* - `Structural` — varying the value changes the SQL *shape* (different branch / columns /
|
|
15
|
+
* joins). Not a row filter; may be per-value-cacheable later (Bucket 2).
|
|
16
|
+
* - `Unbounded` — anything we cannot prove safe: a parse failure, no observable effect, a
|
|
17
|
+
* literal that varies outside a clean top-level conjunctive `WHERE` equality
|
|
18
|
+
* (projection, `OR`/`NOT`, subquery, function, `JOIN ... ON`), or a value that
|
|
19
|
+
* touches more than one column. **Default to this when unsure.**
|
|
20
|
+
*
|
|
21
|
+
* The verifier observes *actual rendered behavior* — it never trusts the template author's intent.
|
|
22
|
+
* Its output ({@link VerifiedParamRole}) feeds `qualifyParameterizedQuery` in materializationAnalysis.
|
|
23
|
+
*
|
|
24
|
+
* **Why "clean top-level conjunctive WHERE" is mandatory:** read-time row filtering re-applies the
|
|
25
|
+
* predicate against the *broad* materialization. That is only equivalent to the live parameterized
|
|
26
|
+
* query when the predicate is ANDed into the outer WHERE on a materialized column. Under an `OR`
|
|
27
|
+
* (`WHERE Status = {{s}} OR IsAdmin = 1`), re-filtering by `Status` would wrongly drop the `IsAdmin`
|
|
28
|
+
* rows; inside a subquery / function / JOIN it may not correspond to an output column at all. Those
|
|
29
|
+
* all *taint* the site → refuse.
|
|
30
|
+
*
|
|
31
|
+
* Pure — no DB / IO / LLM. The render step (turning a template + values into these SQL strings)
|
|
32
|
+
* lives in the Phase 2c adapter; this module only consumes already-rendered SQL, so it is fully
|
|
33
|
+
* unit-testable with hand-written SQL pairs.
|
|
34
|
+
*/
|
|
35
|
+
/** Comparison operator of a proven row filter, normalized so it always reads `column <op> value`. */
|
|
36
|
+
export type FilterOperator = '=' | '!=' | '<>' | '<' | '>' | '<=' | '>=' | 'IN' | 'NOT IN' | 'LIKE' | 'NOT LIKE' | 'IS' | 'IS NOT' | 'BETWEEN' | 'NOT BETWEEN';
|
|
37
|
+
/** Shape of a proven row-filter value: a single scalar vs. an all-literal list bag (`IN`/`NOT IN`). */
|
|
38
|
+
export type FilterKind = 'scalar' | 'list';
|
|
39
|
+
/** The verifier's verdict for a single parameter. */
|
|
40
|
+
export interface VerifiedParamRole {
|
|
41
|
+
/** Proven role under the §10 asymmetric-risk posture. */
|
|
42
|
+
role: ParamRole;
|
|
43
|
+
/** For `RowFilter`: the single column the value filters on (as written in the predicate). */
|
|
44
|
+
filterColumn?: string;
|
|
45
|
+
/**
|
|
46
|
+
* For `RowFilter`: the comparison operator, **normalized to the `column <op> value` reading** — if the
|
|
47
|
+
* predicate was written `value < column`, the operator is flipped (`>`) so read-time injection can emit
|
|
48
|
+
* `column > value` faithfully. This is the Phase-2 metadata that `filterColumn` alone cannot supply
|
|
49
|
+
* (`Score >= x` vs `Score = x` are otherwise indistinguishable). Absent for non-RowFilter verdicts.
|
|
50
|
+
*/
|
|
51
|
+
filterOperator?: FilterOperator;
|
|
52
|
+
/** For `RowFilter`: whether the value is a single scalar or an `IN`/`NOT IN` list bag. */
|
|
53
|
+
filterKind?: FilterKind;
|
|
54
|
+
/** Human-readable justification (logged; never guessed past). */
|
|
55
|
+
reason: string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Verifies a single parameter's {@link ParamRole} from ≥2 full-query SQL variants — the same query
|
|
59
|
+
* template rendered with the same values for every parameter *except* this one, which is varied
|
|
60
|
+
* across distinct values. Pure; the Phase 2c adapter supplies the rendered strings.
|
|
61
|
+
*
|
|
62
|
+
* Decision (refuse-under-uncertainty, §10):
|
|
63
|
+
* - <2 variants, or any variant fails to parse → `Unbounded`.
|
|
64
|
+
* - any pair differs structurally → `Structural` (shape depends on the value; possibly Bucket 2).
|
|
65
|
+
* - all pairs structurally equal but no literal varied → `Unbounded` (no observable effect to prove).
|
|
66
|
+
* - a varying literal is tainted (projection / `OR` / subquery / function / non-top-WHERE) → `Unbounded`.
|
|
67
|
+
* - varying literals touch >1 column → `Unbounded` (multi-column not modeled in v1).
|
|
68
|
+
* - else → `RowFilter` on the single bound column.
|
|
69
|
+
*/
|
|
70
|
+
export declare function verifyParamRole(variants: string[], dialect: SQLParserDialect): VerifiedParamRole;
|
|
71
|
+
//# sourceMappingURL=materializationParamVerifier.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"materializationParamVerifier.d.ts","sourceRoot":"","sources":["../../src/Database/materializationParamVerifier.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AACpE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAM3D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,qGAAqG;AACrG,MAAM,MAAM,cAAc,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI,GAAG,GAAG,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU,GAAG,IAAI,GAAG,QAAQ,GAAG,SAAS,GAAG,aAAa,CAAC;AAE/J,uGAAuG;AACvG,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,MAAM,CAAC;AAE3C,qDAAqD;AACrD,MAAM,WAAW,iBAAiB;IAC9B,yDAAyD;IACzD,IAAI,EAAE,SAAS,CAAC;IAChB,6FAA6F;IAC7F,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,0FAA0F;IAC1F,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,iEAAiE;IACjE,MAAM,EAAE,MAAM,CAAC;CAClB;AAySD;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,gBAAgB,GAAG,iBAAiB,CAmDhG"}
|