rawsql-ts 0.28.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/index.min.js +2 -2
- package/dist/esm/index.min.js.map +4 -4
- package/dist/esm/transformers/ConditionDeduplicationOptimizer.d.ts +22 -0
- package/dist/esm/transformers/ConditionDeduplicationOptimizer.js +198 -0
- package/dist/esm/transformers/ConditionDeduplicationOptimizer.js.map +1 -0
- package/dist/esm/transformers/ConditionOptimization.d.ts +31 -1
- package/dist/esm/transformers/ConditionOptimization.js +321 -4
- package/dist/esm/transformers/ConditionOptimization.js.map +1 -1
- package/dist/esm/transformers/ParameterConditionPlacementOptimizer.d.ts +13 -0
- package/dist/esm/transformers/ParameterConditionPlacementOptimizer.js +238 -87
- package/dist/esm/transformers/ParameterConditionPlacementOptimizer.js.map +1 -1
- package/dist/esm/transformers/PredicateExpressionUtils.d.ts +16 -0
- package/dist/esm/transformers/PredicateExpressionUtils.js +74 -0
- package/dist/esm/transformers/PredicateExpressionUtils.js.map +1 -0
- package/dist/esm/transformers/PredicateReachabilityAnalyzer.d.ts +88 -0
- package/dist/esm/transformers/PredicateReachabilityAnalyzer.js +495 -0
- package/dist/esm/transformers/PredicateReachabilityAnalyzer.js.map +1 -0
- package/dist/esm/transformers/StaticPredicatePlacementOptimizer.d.ts +14 -0
- package/dist/esm/transformers/StaticPredicatePlacementOptimizer.js +344 -157
- package/dist/esm/transformers/StaticPredicatePlacementOptimizer.js.map +1 -1
- package/dist/esm/transformers/TopLevelAndConditionDeduper.d.ts +10 -0
- package/dist/esm/transformers/TopLevelAndConditionDeduper.js +66 -0
- package/dist/esm/transformers/TopLevelAndConditionDeduper.js.map +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +2 -2
- package/dist/index.min.js.map +4 -4
- package/dist/src/index.d.ts +1 -0
- package/dist/src/transformers/ConditionDeduplicationOptimizer.d.ts +22 -0
- package/dist/src/transformers/ConditionOptimization.d.ts +31 -1
- package/dist/src/transformers/ParameterConditionPlacementOptimizer.d.ts +13 -0
- package/dist/src/transformers/PredicateExpressionUtils.d.ts +16 -0
- package/dist/src/transformers/PredicateReachabilityAnalyzer.d.ts +88 -0
- package/dist/src/transformers/StaticPredicatePlacementOptimizer.d.ts +14 -0
- package/dist/src/transformers/TopLevelAndConditionDeduper.d.ts +10 -0
- package/dist/transformers/ConditionDeduplicationOptimizer.js +202 -0
- package/dist/transformers/ConditionDeduplicationOptimizer.js.map +1 -0
- package/dist/transformers/ConditionOptimization.js +324 -4
- package/dist/transformers/ConditionOptimization.js.map +1 -1
- package/dist/transformers/ParameterConditionPlacementOptimizer.js +261 -110
- package/dist/transformers/ParameterConditionPlacementOptimizer.js.map +1 -1
- package/dist/transformers/PredicateExpressionUtils.js +88 -0
- package/dist/transformers/PredicateExpressionUtils.js.map +1 -0
- package/dist/transformers/PredicateReachabilityAnalyzer.js +508 -0
- package/dist/transformers/PredicateReachabilityAnalyzer.js.map +1 -0
- package/dist/transformers/StaticPredicatePlacementOptimizer.js +366 -179
- package/dist/transformers/StaticPredicatePlacementOptimizer.js.map +1 -1
- package/dist/transformers/TopLevelAndConditionDeduper.js +72 -0
- package/dist/transformers/TopLevelAndConditionDeduper.js.map +1 -0
- package/dist/tsconfig.browser.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -1,71 +1,12 @@
|
|
|
1
|
-
import { DistinctOn, SubQuerySource, TableSource
|
|
1
|
+
import { DistinctOn, JoinOnClause, SubQuerySource, TableSource } from "../models/Clause";
|
|
2
2
|
import { BinarySelectQuery, SimpleSelectQuery } from "../models/SelectQuery";
|
|
3
|
-
import { ArrayExpression, ArrayQueryExpression, BetweenExpression, BinaryExpression, CaseExpression, CastExpression, ColumnReference, FunctionCall, InlineQuery, JsonPredicateExpression, ParameterExpression,
|
|
3
|
+
import { ArrayExpression, ArrayQueryExpression, BetweenExpression, BinaryExpression, CaseExpression, CastExpression, ColumnReference, FunctionCall, InlineQuery, JsonPredicateExpression, ParameterExpression, TupleExpression, TypeValue, UnaryExpression, ValueList } from "../models/ValueComponent";
|
|
4
4
|
import { SelectQueryParser } from "../parsers/SelectQueryParser";
|
|
5
|
-
import { ValueParser } from "../parsers/ValueParser";
|
|
6
5
|
import { formatSqlComponent, hasSqlComponentFormatOverride } from "./SqlComponentFormatter";
|
|
7
6
|
import { SelectOutputCollector } from "./SelectOutputCollector";
|
|
7
|
+
import { dedupeTopLevelAndConditions, dedupeWhereTopLevelAndConditions } from "./TopLevelAndConditionDeduper";
|
|
8
|
+
import { appendUnique, cloneColumnReference, cloneValueComponent, collectTopLevelAndTerms, columnReferenceText, identifiersEqual, normalizeIdentifier, rebuildWhereWithoutTerms, sameColumnReference, unwrapParens } from "./PredicateExpressionUtils";
|
|
8
9
|
const VOLATILE_OR_UNSUPPORTED_FUNCTION_REASON = "Predicate contains a function call; volatile and expression predicates are not moved in the safe-only implementation.";
|
|
9
|
-
const normalizeIdentifier = (value) => value.trim().toLowerCase();
|
|
10
|
-
const unwrapParens = (expression) => {
|
|
11
|
-
let candidate = expression;
|
|
12
|
-
while (candidate instanceof ParenExpression) {
|
|
13
|
-
candidate = candidate.expression;
|
|
14
|
-
}
|
|
15
|
-
return candidate;
|
|
16
|
-
};
|
|
17
|
-
const isBinaryOperator = (expression, operator) => {
|
|
18
|
-
const candidate = unwrapParens(expression);
|
|
19
|
-
return candidate instanceof BinaryExpression
|
|
20
|
-
&& candidate.operator.value.trim().toLowerCase() === operator;
|
|
21
|
-
};
|
|
22
|
-
const collectTopLevelAndTerms = (expression) => {
|
|
23
|
-
const candidate = unwrapParens(expression);
|
|
24
|
-
if (!isBinaryOperator(candidate, "and")) {
|
|
25
|
-
return [expression];
|
|
26
|
-
}
|
|
27
|
-
return [
|
|
28
|
-
...collectTopLevelAndTerms(candidate.left),
|
|
29
|
-
...collectTopLevelAndTerms(candidate.right)
|
|
30
|
-
];
|
|
31
|
-
};
|
|
32
|
-
const rebuildWhereWithoutTerms = (query, termsToRemove) => {
|
|
33
|
-
if (!query.whereClause || termsToRemove.size === 0) {
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
const remaining = collectTopLevelAndTerms(query.whereClause.condition)
|
|
37
|
-
.filter(term => !termsToRemove.has(term));
|
|
38
|
-
if (remaining.length === 0) {
|
|
39
|
-
query.whereClause = null;
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
let rebuilt = remaining[0];
|
|
43
|
-
for (let index = 1; index < remaining.length; index += 1) {
|
|
44
|
-
rebuilt = new BinaryExpression(rebuilt, "and", remaining[index]);
|
|
45
|
-
}
|
|
46
|
-
query.whereClause = new WhereClause(rebuilt);
|
|
47
|
-
};
|
|
48
|
-
const cloneValueComponent = (expression, options) => {
|
|
49
|
-
return ValueParser.parse(formatSqlComponent(expression, options));
|
|
50
|
-
};
|
|
51
|
-
const cloneColumnReference = (reference) => {
|
|
52
|
-
var _a, _b;
|
|
53
|
-
const namespaces = (_b = (_a = reference.namespaces) === null || _a === void 0 ? void 0 : _a.map(namespace => namespace.name)) !== null && _b !== void 0 ? _b : null;
|
|
54
|
-
return new ColumnReference(namespaces, reference.column.name);
|
|
55
|
-
};
|
|
56
|
-
const columnReferenceText = (reference) => {
|
|
57
|
-
const namespace = reference.getNamespace();
|
|
58
|
-
return namespace ? `${namespace}.${reference.column.name}` : reference.column.name;
|
|
59
|
-
};
|
|
60
|
-
const sameColumnReference = (left, right) => {
|
|
61
|
-
return normalizeIdentifier(left.column.name) === normalizeIdentifier(right.column.name)
|
|
62
|
-
&& normalizeIdentifier(left.getNamespace()) === normalizeIdentifier(right.getNamespace());
|
|
63
|
-
};
|
|
64
|
-
const appendUnique = (items, value) => {
|
|
65
|
-
if (!items.includes(value)) {
|
|
66
|
-
items.push(value);
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
10
|
export class StaticPredicatePlacementOptimizer {
|
|
70
11
|
plan(input, options = {}) {
|
|
71
12
|
var _a, _b, _c;
|
|
@@ -103,71 +44,8 @@ export class StaticPredicatePlacementOptimizer {
|
|
|
103
44
|
const query = parsed.query;
|
|
104
45
|
const applied = [];
|
|
105
46
|
const skipped = [];
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const candidate = this.analyzeCandidate(query, term, options);
|
|
109
|
-
if (!candidate) {
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
if ("code" in candidate) {
|
|
113
|
-
skipped.push(this.makeSkipped(term, candidate, options));
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
const target = this.resolveTarget(query, candidate);
|
|
117
|
-
if ("code" in target) {
|
|
118
|
-
skipped.push(this.makeSkipped(term, target, options));
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
let appliedReason = "";
|
|
122
|
-
if (target.kind === "simple") {
|
|
123
|
-
const targetColumns = this.resolveTargetColumns(query, target.query, candidate.references);
|
|
124
|
-
if ("code" in targetColumns) {
|
|
125
|
-
skipped.push(this.makeSkipped(term, targetColumns, options));
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
const placement = this.resolveTargetPlacement(target.query, targetColumns);
|
|
129
|
-
if ("code" in placement) {
|
|
130
|
-
skipped.push(this.makeSkipped(term, placement, options));
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
const movedPredicate = this.rebasePredicate(candidate.expression, targetColumns, options);
|
|
134
|
-
target.query.appendWhere(movedPredicate);
|
|
135
|
-
appliedReason = placement.reason;
|
|
136
|
-
}
|
|
137
|
-
else {
|
|
138
|
-
const placements = [];
|
|
139
|
-
let skip = null;
|
|
140
|
-
for (const branch of target.branches) {
|
|
141
|
-
const placement = this.resolveTargetPlacement(branch.query, branch.targetColumns);
|
|
142
|
-
if ("code" in placement) {
|
|
143
|
-
skip = placement;
|
|
144
|
-
break;
|
|
145
|
-
}
|
|
146
|
-
placements.push(placement);
|
|
147
|
-
}
|
|
148
|
-
if (skip) {
|
|
149
|
-
skipped.push(this.makeSkipped(term, skip, options));
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
for (const branch of target.branches) {
|
|
153
|
-
const movedPredicate = this.rebasePredicate(candidate.expression, branch.targetColumns, options);
|
|
154
|
-
branch.query.appendWhere(movedPredicate);
|
|
155
|
-
}
|
|
156
|
-
appliedReason = placements.some(item => /group by/i.test(item.reason))
|
|
157
|
-
? "Predicate is distributed to every UNION branch by output column position; grouped branches only receive GROUP BY-key predicates."
|
|
158
|
-
: "Predicate is distributed to every UNION branch by output column position before unsafe query boundaries.";
|
|
159
|
-
}
|
|
160
|
-
movedTerms.push(term);
|
|
161
|
-
applied.push({
|
|
162
|
-
kind: "move_static_predicate",
|
|
163
|
-
predicateSql: candidate.predicateSql,
|
|
164
|
-
fromScopeId: "scope:root",
|
|
165
|
-
toScopeId: target.scopeId,
|
|
166
|
-
reason: appliedReason,
|
|
167
|
-
columnReferences: candidate.references.map(columnReferenceText)
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
rebuildWhereWithoutTerms(query, new Set(movedTerms));
|
|
47
|
+
this.placePredicatesInScopes(query, options, applied, skipped);
|
|
48
|
+
// API output shape review: keep result.sql/result.query compatibility while expanding scope-aware placement.
|
|
171
49
|
const sql = applied.length > 0 || hasSqlComponentFormatOverride(options)
|
|
172
50
|
? formatSqlComponent(query, options)
|
|
173
51
|
: parsed.sql;
|
|
@@ -182,6 +60,121 @@ export class StaticPredicatePlacementOptimizer {
|
|
|
182
60
|
formatterGeneratedSource: parsed.formatterGeneratedSource
|
|
183
61
|
});
|
|
184
62
|
}
|
|
63
|
+
placePredicatesInScopes(contextRoot, options, applied, skipped) {
|
|
64
|
+
var _a;
|
|
65
|
+
const queue = [{ query: contextRoot, scopeId: "scope:root" }];
|
|
66
|
+
let processed = 0;
|
|
67
|
+
while (queue.length > 0) {
|
|
68
|
+
processed += 1;
|
|
69
|
+
if (processed > 1000) {
|
|
70
|
+
skipped.push({
|
|
71
|
+
predicateSql: "",
|
|
72
|
+
scopeId: "scope:root",
|
|
73
|
+
code: "RECURSIVE_PLACEMENT_LIMIT",
|
|
74
|
+
reason: "Static predicate recursive placement stopped after reaching the safety iteration limit."
|
|
75
|
+
});
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const work = queue.shift();
|
|
79
|
+
const movedTerms = [];
|
|
80
|
+
const reportSkipped = work.pendingTerms === undefined;
|
|
81
|
+
const terms = (_a = work.pendingTerms) !== null && _a !== void 0 ? _a : (work.query.whereClause
|
|
82
|
+
? collectTopLevelAndTerms(work.query.whereClause.condition)
|
|
83
|
+
: []);
|
|
84
|
+
for (const term of terms) {
|
|
85
|
+
const candidate = this.analyzeCandidate(work.query, term, options);
|
|
86
|
+
if (!candidate) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if ("code" in candidate) {
|
|
90
|
+
if (reportSkipped) {
|
|
91
|
+
skipped.push(this.makeSkipped(term, work.scopeId, candidate, options));
|
|
92
|
+
}
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const target = this.resolveTarget(contextRoot, work.query, candidate);
|
|
96
|
+
if ("code" in target) {
|
|
97
|
+
if (reportSkipped) {
|
|
98
|
+
skipped.push(this.makeSkipped(term, work.scopeId, target, options));
|
|
99
|
+
}
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
let appliedReason = "";
|
|
103
|
+
if (target.kind === "join_on") {
|
|
104
|
+
this.appendJoinOnPredicate(target.join, candidate.expression, options);
|
|
105
|
+
appliedReason = target.reason;
|
|
106
|
+
}
|
|
107
|
+
else if (target.kind === "simple") {
|
|
108
|
+
const targetColumns = this.resolveTargetColumns(contextRoot, target.query, candidate.references);
|
|
109
|
+
if ("code" in targetColumns) {
|
|
110
|
+
if (reportSkipped) {
|
|
111
|
+
skipped.push(this.makeSkipped(term, work.scopeId, targetColumns, options));
|
|
112
|
+
}
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const placement = this.resolveTargetPlacement(contextRoot, target.query, targetColumns);
|
|
116
|
+
if ("code" in placement) {
|
|
117
|
+
if (reportSkipped) {
|
|
118
|
+
skipped.push(this.makeSkipped(term, work.scopeId, placement, options));
|
|
119
|
+
}
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const movedPredicate = this.rebasePredicate(candidate.expression, targetColumns, options);
|
|
123
|
+
target.query.appendWhere(movedPredicate);
|
|
124
|
+
dedupeWhereTopLevelAndConditions(target.query, options);
|
|
125
|
+
appliedReason = work.scopeId === "scope:root"
|
|
126
|
+
? placement.reason
|
|
127
|
+
: `${placement.reason} Predicate was safely rebased from a previously moved predicate.`;
|
|
128
|
+
const retainedTerms = target.query.whereClause
|
|
129
|
+
? collectTopLevelAndTerms(target.query.whereClause.condition)
|
|
130
|
+
: [];
|
|
131
|
+
if (retainedTerms.includes(movedPredicate)) {
|
|
132
|
+
queue.push({
|
|
133
|
+
query: target.query,
|
|
134
|
+
scopeId: target.scopeId,
|
|
135
|
+
pendingTerms: [movedPredicate]
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
const placements = [];
|
|
141
|
+
let skip = null;
|
|
142
|
+
for (const branch of target.branches) {
|
|
143
|
+
const placement = this.resolveTargetPlacement(contextRoot, branch.query, branch.targetColumns);
|
|
144
|
+
if ("code" in placement) {
|
|
145
|
+
skip = placement;
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
placements.push(placement);
|
|
149
|
+
}
|
|
150
|
+
if (skip) {
|
|
151
|
+
if (reportSkipped) {
|
|
152
|
+
skipped.push(this.makeSkipped(term, work.scopeId, skip, options));
|
|
153
|
+
}
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
for (const branch of target.branches) {
|
|
157
|
+
const movedPredicate = this.rebasePredicate(candidate.expression, branch.targetColumns, options);
|
|
158
|
+
branch.query.appendWhere(movedPredicate);
|
|
159
|
+
dedupeWhereTopLevelAndConditions(branch.query, options);
|
|
160
|
+
}
|
|
161
|
+
appliedReason = placements.some(item => /group by/i.test(item.reason))
|
|
162
|
+
? "Predicate is distributed to every UNION branch by output column position; grouped branches only receive GROUP BY-key predicates."
|
|
163
|
+
: "Predicate is distributed to every UNION branch by output column position before unsafe query boundaries.";
|
|
164
|
+
}
|
|
165
|
+
movedTerms.push(term);
|
|
166
|
+
applied.push({
|
|
167
|
+
kind: "move_static_predicate",
|
|
168
|
+
predicateSql: candidate.predicateSql,
|
|
169
|
+
fromScopeId: work.scopeId,
|
|
170
|
+
toScopeId: target.scopeId,
|
|
171
|
+
reason: appliedReason,
|
|
172
|
+
columnReferences: candidate.references.map(columnReferenceText)
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
rebuildWhereWithoutTerms(work.query, new Set(movedTerms));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
185
178
|
optimize(input, options = {}) {
|
|
186
179
|
var _a;
|
|
187
180
|
return this.plan(input, Object.assign(Object.assign({}, options), { dryRun: (_a = options.dryRun) !== null && _a !== void 0 ? _a : false }));
|
|
@@ -377,12 +370,13 @@ export class StaticPredicatePlacementOptimizer {
|
|
|
377
370
|
visit(expression);
|
|
378
371
|
return found;
|
|
379
372
|
}
|
|
380
|
-
resolveTarget(
|
|
381
|
-
|
|
373
|
+
resolveTarget(contextRoot, currentQuery, candidate) {
|
|
374
|
+
var _a;
|
|
375
|
+
const boundary = this.findRootQueryBoundary(currentQuery);
|
|
382
376
|
if (boundary) {
|
|
383
377
|
return boundary;
|
|
384
378
|
}
|
|
385
|
-
if (!
|
|
379
|
+
if (!currentQuery.fromClause) {
|
|
386
380
|
return {
|
|
387
381
|
code: "NO_FROM_CLAUSE",
|
|
388
382
|
reason: "Predicate has no FROM source that can receive it safely."
|
|
@@ -390,7 +384,7 @@ export class StaticPredicatePlacementOptimizer {
|
|
|
390
384
|
}
|
|
391
385
|
const bindings = [];
|
|
392
386
|
for (const reference of candidate.references) {
|
|
393
|
-
const binding = this.resolveSourceBinding(
|
|
387
|
+
const binding = this.resolveSourceBinding(contextRoot, currentQuery, reference);
|
|
394
388
|
if ("code" in binding) {
|
|
395
389
|
return binding;
|
|
396
390
|
}
|
|
@@ -405,12 +399,25 @@ export class StaticPredicatePlacementOptimizer {
|
|
|
405
399
|
};
|
|
406
400
|
}
|
|
407
401
|
const binding = bindings[0];
|
|
408
|
-
|
|
402
|
+
if ((_a = binding.join) === null || _a === void 0 ? void 0 : _a.lateral) {
|
|
403
|
+
return {
|
|
404
|
+
code: "LATERAL_JOIN_BOUNDARY",
|
|
405
|
+
reason: "Predicate crosses a LATERAL JOIN boundary; moving it may change semantics."
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
const nullableSide = this.findNullableSideBoundary(currentQuery.fromClause, binding);
|
|
409
409
|
if (nullableSide) {
|
|
410
410
|
return nullableSide;
|
|
411
411
|
}
|
|
412
|
-
const upstream = this.resolveUpstreamQuery(
|
|
412
|
+
const upstream = this.resolveUpstreamQuery(contextRoot, binding, candidate.references);
|
|
413
413
|
if ("code" in upstream) {
|
|
414
|
+
if (upstream.code === "NO_SAFE_UPSTREAM_QUERY") {
|
|
415
|
+
const joinOnTarget = this.resolveBaseTableJoinOnTarget(contextRoot, currentQuery, binding);
|
|
416
|
+
if (!("code" in joinOnTarget)) {
|
|
417
|
+
return joinOnTarget;
|
|
418
|
+
}
|
|
419
|
+
return joinOnTarget;
|
|
420
|
+
}
|
|
414
421
|
return upstream;
|
|
415
422
|
}
|
|
416
423
|
return upstream;
|
|
@@ -430,7 +437,7 @@ export class StaticPredicatePlacementOptimizer {
|
|
|
430
437
|
}
|
|
431
438
|
return null;
|
|
432
439
|
}
|
|
433
|
-
resolveTargetPlacement(query, targetColumns) {
|
|
440
|
+
resolveTargetPlacement(contextRoot, query, targetColumns) {
|
|
434
441
|
const hasOrdinaryDistinct = this.hasOrdinaryDistinct(query);
|
|
435
442
|
if (this.hasDistinctOnBoundary(query)) {
|
|
436
443
|
return {
|
|
@@ -450,11 +457,11 @@ export class StaticPredicatePlacementOptimizer {
|
|
|
450
457
|
reason: "Predicate crosses LIMIT/OFFSET/FETCH boundary; moving it may change row selection semantics."
|
|
451
458
|
};
|
|
452
459
|
}
|
|
453
|
-
if (query.fromClause
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
}
|
|
460
|
+
if (query.fromClause) {
|
|
461
|
+
const nullableSide = this.findNullableSideBoundaryForTargetColumns(contextRoot, query, targetColumns);
|
|
462
|
+
if (nullableSide) {
|
|
463
|
+
return nullableSide;
|
|
464
|
+
}
|
|
458
465
|
}
|
|
459
466
|
if (query.groupByClause) {
|
|
460
467
|
const allReferencesAreGroupKeys = targetColumns.every(item => this.isGroupKeyColumn(query, item.targetColumn));
|
|
@@ -483,6 +490,22 @@ export class StaticPredicatePlacementOptimizer {
|
|
|
483
490
|
reason: "All outer references resolve to direct upstream outputs before unsafe query boundaries."
|
|
484
491
|
};
|
|
485
492
|
}
|
|
493
|
+
findNullableSideBoundaryForTargetColumns(contextRoot, query, targetColumns) {
|
|
494
|
+
if (!query.fromClause) {
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
for (const item of targetColumns) {
|
|
498
|
+
const binding = this.resolveSourceBinding(contextRoot, query, item.targetColumn);
|
|
499
|
+
if ("code" in binding) {
|
|
500
|
+
return binding;
|
|
501
|
+
}
|
|
502
|
+
const nullableSide = this.findNullableSideBoundary(query.fromClause, binding);
|
|
503
|
+
if (nullableSide) {
|
|
504
|
+
return nullableSide;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
486
509
|
resolveSourceBinding(contextRoot, query, column) {
|
|
487
510
|
const fromClause = query.fromClause;
|
|
488
511
|
if (!fromClause) {
|
|
@@ -492,10 +515,10 @@ export class StaticPredicatePlacementOptimizer {
|
|
|
492
515
|
};
|
|
493
516
|
}
|
|
494
517
|
const bindings = this.getSourceBindings(fromClause);
|
|
495
|
-
const namespace =
|
|
518
|
+
const namespace = column.getNamespace();
|
|
496
519
|
const columnName = column.column.name;
|
|
497
520
|
if (namespace) {
|
|
498
|
-
const matches = bindings.filter(binding =>
|
|
521
|
+
const matches = bindings.filter(binding => identifiersEqual(binding.alias, namespace));
|
|
499
522
|
if (matches.length !== 1) {
|
|
500
523
|
return {
|
|
501
524
|
code: "AMBIGUOUS_COLUMN_SOURCE",
|
|
@@ -503,6 +526,9 @@ export class StaticPredicatePlacementOptimizer {
|
|
|
503
526
|
};
|
|
504
527
|
}
|
|
505
528
|
const matchCount = this.getOutputColumnMatchCount(contextRoot, matches[0], columnName);
|
|
529
|
+
if (matchCount === 0 && this.isBaseTableBinding(contextRoot, matches[0])) {
|
|
530
|
+
return matches[0];
|
|
531
|
+
}
|
|
506
532
|
if (matchCount === 0) {
|
|
507
533
|
return {
|
|
508
534
|
code: "COLUMN_NOT_AVAILABLE_UPSTREAM",
|
|
@@ -631,7 +657,7 @@ export class StaticPredicatePlacementOptimizer {
|
|
|
631
657
|
resolveTargetColumns(root, query, references) {
|
|
632
658
|
const resolved = [];
|
|
633
659
|
for (const reference of references) {
|
|
634
|
-
const matches = this.
|
|
660
|
+
const matches = this.collectDirectOutputMatches(root, query, reference.column.name);
|
|
635
661
|
if (matches.length !== 1) {
|
|
636
662
|
return {
|
|
637
663
|
code: "AMBIGUOUS_TARGET_COLUMN",
|
|
@@ -658,6 +684,57 @@ export class StaticPredicatePlacementOptimizer {
|
|
|
658
684
|
}
|
|
659
685
|
return resolved;
|
|
660
686
|
}
|
|
687
|
+
resolveBaseTableJoinOnTarget(contextRoot, currentQuery, binding) {
|
|
688
|
+
var _a, _b, _c;
|
|
689
|
+
if (!currentQuery.fromClause || !this.isBaseTableBinding(contextRoot, binding)) {
|
|
690
|
+
return {
|
|
691
|
+
code: "NO_SAFE_UPSTREAM_QUERY",
|
|
692
|
+
reason: "The referenced source is a base table, so there is no upstream query block to move into."
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
if (this.hasOuterJoin(currentQuery.fromClause)) {
|
|
696
|
+
return {
|
|
697
|
+
code: "OUTER_JOIN_BOUNDARY",
|
|
698
|
+
reason: "Predicate crosses an OUTER JOIN boundary; moving it into JOIN ON may change semantics."
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
const join = binding.isPrimary
|
|
702
|
+
? (_b = (_a = currentQuery.fromClause.joins) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : null
|
|
703
|
+
: binding.join;
|
|
704
|
+
if (join === null || join === void 0 ? void 0 : join.lateral) {
|
|
705
|
+
return {
|
|
706
|
+
code: "LATERAL_JOIN_BOUNDARY",
|
|
707
|
+
reason: "Predicate crosses a LATERAL JOIN boundary; moving it into JOIN ON may change semantics."
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
if (!join || !this.isInnerJoin(join)) {
|
|
711
|
+
return {
|
|
712
|
+
code: "NO_SAFE_JOIN_ON_TARGET",
|
|
713
|
+
reason: "Base-table predicates are moved only into INNER JOIN ON clauses in the safe-only implementation."
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
if (!(join.condition instanceof JoinOnClause)) {
|
|
717
|
+
return {
|
|
718
|
+
code: "NO_SAFE_JOIN_ON_TARGET",
|
|
719
|
+
reason: "Base-table predicates are moved only into existing JOIN ON clauses, not USING or conditionless joins."
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
return {
|
|
723
|
+
kind: "join_on",
|
|
724
|
+
join,
|
|
725
|
+
scopeId: `join_on:${(_c = join.source.getAliasName()) !== null && _c !== void 0 ? _c : "unknown"}`,
|
|
726
|
+
reason: binding.isPrimary
|
|
727
|
+
? "Predicate references the primary source of an INNER JOIN; it is moved into the first JOIN ON clause."
|
|
728
|
+
: "Predicate references the joined source of an INNER JOIN; it is moved into that JOIN ON clause."
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
appendJoinOnPredicate(join, expression, options) {
|
|
732
|
+
if (!(join.condition instanceof JoinOnClause)) {
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
join.condition.condition = new BinaryExpression(join.condition.condition, "and", cloneValueComponent(expression, options));
|
|
736
|
+
join.condition.condition = dedupeTopLevelAndConditions(join.condition.condition, options);
|
|
737
|
+
}
|
|
661
738
|
resolveDeepestBranchTarget(contextRoot, query, targetColumns, visited = new Set()) {
|
|
662
739
|
if (visited.has(query) || targetColumns.length === 0) {
|
|
663
740
|
return { query, targetColumns: [...targetColumns] };
|
|
@@ -685,14 +762,17 @@ export class StaticPredicatePlacementOptimizer {
|
|
|
685
762
|
return { query, targetColumns: [...targetColumns] };
|
|
686
763
|
}
|
|
687
764
|
const upstream = this.resolveUpstreamQuery(contextRoot, binding, targetColumns.map(item => item.targetColumn));
|
|
688
|
-
if ("code" in upstream
|
|
765
|
+
if ("code" in upstream) {
|
|
766
|
+
return { query, targetColumns: [...targetColumns] };
|
|
767
|
+
}
|
|
768
|
+
if (upstream.kind !== "simple") {
|
|
689
769
|
return { query, targetColumns: [...targetColumns] };
|
|
690
770
|
}
|
|
691
771
|
const upstreamColumns = this.resolveTargetColumns(contextRoot, upstream.query, targetColumns.map(item => item.targetColumn));
|
|
692
772
|
if ("code" in upstreamColumns) {
|
|
693
773
|
return { query, targetColumns: [...targetColumns] };
|
|
694
774
|
}
|
|
695
|
-
const placement = this.resolveTargetPlacement(upstream.query, upstreamColumns);
|
|
775
|
+
const placement = this.resolveTargetPlacement(contextRoot, upstream.query, upstreamColumns);
|
|
696
776
|
if ("code" in placement) {
|
|
697
777
|
return { query, targetColumns: [...targetColumns] };
|
|
698
778
|
}
|
|
@@ -710,9 +790,9 @@ export class StaticPredicatePlacementOptimizer {
|
|
|
710
790
|
};
|
|
711
791
|
}
|
|
712
792
|
const bindings = this.getSourceBindings(query.fromClause);
|
|
713
|
-
const namespace =
|
|
793
|
+
const namespace = column.getNamespace();
|
|
714
794
|
if (namespace) {
|
|
715
|
-
const matches = bindings.filter(binding =>
|
|
795
|
+
const matches = bindings.filter(binding => identifiersEqual(binding.alias, namespace));
|
|
716
796
|
return matches.length === 1
|
|
717
797
|
? null
|
|
718
798
|
: {
|
|
@@ -743,7 +823,7 @@ export class StaticPredicatePlacementOptimizer {
|
|
|
743
823
|
if (sameColumnReference(left, right)) {
|
|
744
824
|
return true;
|
|
745
825
|
}
|
|
746
|
-
if (
|
|
826
|
+
if (!identifiersEqual(left.column.name, right.column.name)) {
|
|
747
827
|
return false;
|
|
748
828
|
}
|
|
749
829
|
const leftSource = this.resolveColumnSourceAlias(query, left);
|
|
@@ -755,12 +835,12 @@ export class StaticPredicatePlacementOptimizer {
|
|
|
755
835
|
return null;
|
|
756
836
|
}
|
|
757
837
|
const bindings = this.getSourceBindings(query.fromClause);
|
|
758
|
-
const namespace =
|
|
838
|
+
const namespace = column.getNamespace();
|
|
759
839
|
if (namespace) {
|
|
760
|
-
const matches = bindings.filter(binding =>
|
|
761
|
-
return matches.length === 1 ?
|
|
840
|
+
const matches = bindings.filter(binding => identifiersEqual(binding.alias, namespace));
|
|
841
|
+
return matches.length === 1 ? matches[0].alias : null;
|
|
762
842
|
}
|
|
763
|
-
return bindings.length === 1 ?
|
|
843
|
+
return bindings.length === 1 ? bindings[0].alias : null;
|
|
764
844
|
}
|
|
765
845
|
rebasePredicate(expression, targetColumns, options) {
|
|
766
846
|
const cloned = cloneValueComponent(expression, options);
|
|
@@ -897,6 +977,14 @@ export class StaticPredicatePlacementOptimizer {
|
|
|
897
977
|
}
|
|
898
978
|
return bindings;
|
|
899
979
|
}
|
|
980
|
+
isBaseTableBinding(root, binding) {
|
|
981
|
+
const source = binding.source.datasource;
|
|
982
|
+
return source instanceof TableSource && !this.findCte(root, source.table.name);
|
|
983
|
+
}
|
|
984
|
+
isInnerJoin(join) {
|
|
985
|
+
const joinType = join.joinType.value.trim().toLowerCase();
|
|
986
|
+
return joinType === "join" || joinType === "inner join";
|
|
987
|
+
}
|
|
900
988
|
findNullableSideBoundary(fromClause, binding) {
|
|
901
989
|
var _a;
|
|
902
990
|
const joins = (_a = fromClause.joins) !== null && _a !== void 0 ? _a : [];
|
|
@@ -964,12 +1052,12 @@ export class StaticPredicatePlacementOptimizer {
|
|
|
964
1052
|
if ("code" in branches) {
|
|
965
1053
|
return 0;
|
|
966
1054
|
}
|
|
967
|
-
return this.
|
|
1055
|
+
return this.collectDirectOutputMatches(root, branches[0], columnName).length;
|
|
968
1056
|
}
|
|
969
1057
|
if (!(target instanceof SimpleSelectQuery)) {
|
|
970
1058
|
return 0;
|
|
971
1059
|
}
|
|
972
|
-
return this.
|
|
1060
|
+
return this.collectDirectOutputMatches(root, target, columnName).length;
|
|
973
1061
|
}
|
|
974
1062
|
collectUnionBranches(query) {
|
|
975
1063
|
const branches = [];
|
|
@@ -1000,7 +1088,7 @@ export class StaticPredicatePlacementOptimizer {
|
|
|
1000
1088
|
resolveUnionOutputIndex(root, firstBranch, outputColumnName) {
|
|
1001
1089
|
const matches = [];
|
|
1002
1090
|
this.collectSelectOutputs(root, firstBranch).forEach((item, index) => {
|
|
1003
|
-
if (
|
|
1091
|
+
if (identifiersEqual(item.name, outputColumnName)) {
|
|
1004
1092
|
matches.push(index);
|
|
1005
1093
|
}
|
|
1006
1094
|
});
|
|
@@ -1022,6 +1110,15 @@ export class StaticPredicatePlacementOptimizer {
|
|
|
1022
1110
|
reason: `UNION branch does not expose column '${sourceColumn.column.name}' at output position ${outputIndex + 1}.`
|
|
1023
1111
|
};
|
|
1024
1112
|
}
|
|
1113
|
+
else if (identifiersEqual(output.name, sourceColumn.column.name)) {
|
|
1114
|
+
const matches = this.collectDirectOutputMatches(root, query, sourceColumn.column.name);
|
|
1115
|
+
if (matches.length > 1) {
|
|
1116
|
+
return {
|
|
1117
|
+
code: "AMBIGUOUS_TARGET_COLUMN",
|
|
1118
|
+
reason: `UNION branch exposes multiple '${sourceColumn.column.name}' columns.`
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1025
1122
|
if (!(output.value instanceof ColumnReference)) {
|
|
1026
1123
|
return {
|
|
1027
1124
|
code: "EXPRESSION_OUTPUT_UNSUPPORTED",
|
|
@@ -1046,6 +1143,96 @@ export class StaticPredicatePlacementOptimizer {
|
|
|
1046
1143
|
const collector = new SelectOutputCollector(null, commonTables.length > 0 ? commonTables : null);
|
|
1047
1144
|
return collector.collect(query);
|
|
1048
1145
|
}
|
|
1146
|
+
collectDirectOutputMatches(root, query, columnName) {
|
|
1147
|
+
const collectedMatches = this.collectSelectOutputs(root, query).filter(item => identifiersEqual(item.name, columnName));
|
|
1148
|
+
if (collectedMatches.length > 0) {
|
|
1149
|
+
return this.hasExplicitOutputMatch(query, columnName)
|
|
1150
|
+
? [...collectedMatches, ...this.inferWildcardOutputMatches(root, query, columnName, true)]
|
|
1151
|
+
: collectedMatches;
|
|
1152
|
+
}
|
|
1153
|
+
return this.inferWildcardOutputMatches(root, query, columnName, false);
|
|
1154
|
+
}
|
|
1155
|
+
hasExplicitOutputMatch(query, columnName) {
|
|
1156
|
+
return query.selectClause.items.some(item => {
|
|
1157
|
+
if (item.identifier) {
|
|
1158
|
+
return identifiersEqual(item.identifier.name, columnName);
|
|
1159
|
+
}
|
|
1160
|
+
return item.value instanceof ColumnReference
|
|
1161
|
+
&& item.value.column.name !== "*"
|
|
1162
|
+
&& identifiersEqual(item.value.column.name, columnName);
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
inferWildcardOutputMatches(root, query, columnName, includeUnprovenPotential) {
|
|
1166
|
+
const matches = [];
|
|
1167
|
+
query.selectClause.items.forEach((item, outputIndex) => {
|
|
1168
|
+
if (item.identifier || !(item.value instanceof ColumnReference) || item.value.column.name !== "*") {
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
const targetColumn = this.inferWildcardTargetColumn(root, query, item.value, columnName, includeUnprovenPotential);
|
|
1172
|
+
if (!targetColumn) {
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
if (targetColumn === "ambiguous") {
|
|
1176
|
+
matches.push(this.createInferredOutputColumn(columnName, new ColumnReference(null, columnName), outputIndex), this.createInferredOutputColumn(columnName, new ColumnReference(null, columnName), outputIndex));
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
matches.push(this.createInferredOutputColumn(columnName, targetColumn, outputIndex));
|
|
1180
|
+
});
|
|
1181
|
+
return matches;
|
|
1182
|
+
}
|
|
1183
|
+
inferWildcardTargetColumn(root, query, wildcard, columnName, includeUnprovenPotential) {
|
|
1184
|
+
if (!query.fromClause) {
|
|
1185
|
+
return null;
|
|
1186
|
+
}
|
|
1187
|
+
const bindings = this.getSourceBindings(query.fromClause);
|
|
1188
|
+
if (wildcard.namespaces === null) {
|
|
1189
|
+
if (bindings.length !== 1) {
|
|
1190
|
+
return "ambiguous";
|
|
1191
|
+
}
|
|
1192
|
+
return this.resolveWildcardColumnFromBinding(root, query, bindings[0], columnName, includeUnprovenPotential);
|
|
1193
|
+
}
|
|
1194
|
+
const namespace = wildcard.getNamespace();
|
|
1195
|
+
const matches = bindings.filter(binding => identifiersEqual(binding.alias, namespace));
|
|
1196
|
+
if (matches.length === 0) {
|
|
1197
|
+
return null;
|
|
1198
|
+
}
|
|
1199
|
+
return matches.length === 1
|
|
1200
|
+
? this.resolveWildcardColumnFromBinding(root, query, matches[0], columnName, includeUnprovenPotential)
|
|
1201
|
+
: "ambiguous";
|
|
1202
|
+
}
|
|
1203
|
+
resolveWildcardColumnFromBinding(root, query, binding, columnName, includeUnprovenPotential) {
|
|
1204
|
+
const matches = this.collectSourceOutputMatches(root, query, binding.source, columnName);
|
|
1205
|
+
if (matches.length > 1) {
|
|
1206
|
+
return "ambiguous";
|
|
1207
|
+
}
|
|
1208
|
+
if (matches.length === 1) {
|
|
1209
|
+
const value = matches[0].value;
|
|
1210
|
+
return value instanceof ColumnReference ? value : null;
|
|
1211
|
+
}
|
|
1212
|
+
return includeUnprovenPotential ? this.createColumnForBinding(binding, columnName) : null;
|
|
1213
|
+
}
|
|
1214
|
+
collectSourceOutputMatches(root, query, source, columnName) {
|
|
1215
|
+
var _a, _b, _c, _d;
|
|
1216
|
+
const commonTables = [
|
|
1217
|
+
...((_b = (_a = query.withClause) === null || _a === void 0 ? void 0 : _a.tables) !== null && _b !== void 0 ? _b : []),
|
|
1218
|
+
...((_d = (_c = root.withClause) === null || _c === void 0 ? void 0 : _c.tables) !== null && _d !== void 0 ? _d : [])
|
|
1219
|
+
];
|
|
1220
|
+
const collector = new SelectOutputCollector(null, commonTables.length > 0 ? commonTables : null);
|
|
1221
|
+
return collector.collect(source).filter(item => identifiersEqual(item.name, columnName));
|
|
1222
|
+
}
|
|
1223
|
+
createColumnForBinding(binding, columnName) {
|
|
1224
|
+
return new ColumnReference(binding.alias ? [binding.alias] : null, columnName);
|
|
1225
|
+
}
|
|
1226
|
+
createInferredOutputColumn(name, value, outputIndex) {
|
|
1227
|
+
return {
|
|
1228
|
+
name,
|
|
1229
|
+
value,
|
|
1230
|
+
outputIndex,
|
|
1231
|
+
sourceAlias: value.getNamespace() || null,
|
|
1232
|
+
sourceName: null,
|
|
1233
|
+
sourceColumnName: name
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1049
1236
|
resolveSourceQueryForColumns(root, source) {
|
|
1050
1237
|
var _a;
|
|
1051
1238
|
if (source.datasource instanceof SubQuerySource) {
|
|
@@ -1404,8 +1591,8 @@ export class StaticPredicatePlacementOptimizer {
|
|
|
1404
1591
|
visit(expression);
|
|
1405
1592
|
return names;
|
|
1406
1593
|
}
|
|
1407
|
-
makeSkipped(expression, draft, options) {
|
|
1408
|
-
return Object.assign({ predicateSql: formatSqlComponent(expression, options), scopeId
|
|
1594
|
+
makeSkipped(expression, scopeId, draft, options) {
|
|
1595
|
+
return Object.assign({ predicateSql: formatSqlComponent(expression, options), scopeId }, draft);
|
|
1409
1596
|
}
|
|
1410
1597
|
buildResult(params) {
|
|
1411
1598
|
return {
|