rawsql-ts 0.26.1 → 0.27.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.map +1 -1
- package/dist/esm/index.min.js +1 -1
- package/dist/esm/index.min.js.map +3 -3
- package/dist/esm/transformers/ConditionOptimization.d.ts +1 -0
- package/dist/esm/transformers/ConditionOptimization.js +97 -57
- package/dist/esm/transformers/ConditionOptimization.js.map +1 -1
- package/dist/esm/transformers/ParameterConditionPlacementOptimizer.d.ts +26 -7
- package/dist/esm/transformers/ParameterConditionPlacementOptimizer.js +408 -138
- package/dist/esm/transformers/ParameterConditionPlacementOptimizer.js.map +1 -1
- package/dist/esm/transformers/SqlComponentFormatter.d.ts +9 -1
- package/dist/esm/transformers/SqlComponentFormatter.js +10 -1
- package/dist/esm/transformers/SqlComponentFormatter.js.map +1 -1
- package/dist/esm/transformers/StaticPredicatePlacementOptimizer.d.ts +22 -4
- package/dist/esm/transformers/StaticPredicatePlacementOptimizer.js +332 -95
- package/dist/esm/transformers/StaticPredicatePlacementOptimizer.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +3 -3
- package/dist/src/index.d.ts +1 -0
- package/dist/src/transformers/ConditionOptimization.d.ts +1 -0
- package/dist/src/transformers/ParameterConditionPlacementOptimizer.d.ts +26 -7
- package/dist/src/transformers/SqlComponentFormatter.d.ts +9 -1
- package/dist/src/transformers/StaticPredicatePlacementOptimizer.d.ts +22 -4
- package/dist/transformers/ConditionOptimization.js +107 -54
- package/dist/transformers/ConditionOptimization.js.map +1 -1
- package/dist/transformers/ParameterConditionPlacementOptimizer.js +406 -136
- package/dist/transformers/ParameterConditionPlacementOptimizer.js.map +1 -1
- package/dist/transformers/SqlComponentFormatter.js +12 -2
- package/dist/transformers/SqlComponentFormatter.js.map +1 -1
- package/dist/transformers/StaticPredicatePlacementOptimizer.js +330 -93
- package/dist/transformers/StaticPredicatePlacementOptimizer.js.map +1 -1
- package/dist/tsconfig.browser.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { SubQuerySource, TableSource, WhereClause } from "../models/Clause";
|
|
1
|
+
import { DistinctOn, SubQuerySource, TableSource, WhereClause } from "../models/Clause";
|
|
2
2
|
import { BinarySelectQuery, SimpleSelectQuery } from "../models/SelectQuery";
|
|
3
3
|
import { ArrayExpression, ArrayQueryExpression, BetweenExpression, BinaryExpression, CaseExpression, CastExpression, ColumnReference, FunctionCall, InlineQuery, JsonPredicateExpression, ParameterExpression, ParenExpression, TupleExpression, TypeValue, UnaryExpression, ValueList } from "../models/ValueComponent";
|
|
4
4
|
import { SelectQueryParser } from "../parsers/SelectQueryParser";
|
|
5
5
|
import { ValueParser } from "../parsers/ValueParser";
|
|
6
|
-
import { formatSqlComponent } from "./SqlComponentFormatter";
|
|
6
|
+
import { formatSqlComponent, hasSqlComponentFormatOverride } from "./SqlComponentFormatter";
|
|
7
|
+
import { SelectOutputCollector } from "./SelectOutputCollector";
|
|
7
8
|
const VOLATILE_OR_UNSUPPORTED_FUNCTION_REASON = "Predicate contains a function call; volatile and expression predicates are not moved in the safe-only implementation.";
|
|
8
9
|
const normalizeIdentifier = (value) => value.trim().toLowerCase();
|
|
9
10
|
const unwrapParens = (expression) => {
|
|
@@ -44,8 +45,8 @@ const rebuildWhereWithoutTerms = (query, termsToRemove) => {
|
|
|
44
45
|
}
|
|
45
46
|
query.whereClause = new WhereClause(rebuilt);
|
|
46
47
|
};
|
|
47
|
-
const cloneValueComponent = (expression) => {
|
|
48
|
-
return ValueParser.parse(formatSqlComponent(expression));
|
|
48
|
+
const cloneValueComponent = (expression, options) => {
|
|
49
|
+
return ValueParser.parse(formatSqlComponent(expression, options));
|
|
49
50
|
};
|
|
50
51
|
const cloneColumnReference = (reference) => {
|
|
51
52
|
var _a, _b;
|
|
@@ -68,11 +69,12 @@ const appendUnique = (items, value) => {
|
|
|
68
69
|
export class StaticPredicatePlacementOptimizer {
|
|
69
70
|
plan(input, options = {}) {
|
|
70
71
|
var _a, _b, _c;
|
|
71
|
-
const parsed = this.parseInput(input);
|
|
72
|
+
const parsed = this.parseInput(input, options);
|
|
72
73
|
const warnings = [...parsed.warnings];
|
|
73
74
|
const errors = [...parsed.errors];
|
|
74
75
|
if (!parsed.query) {
|
|
75
76
|
return this.buildResult({
|
|
77
|
+
query: null,
|
|
76
78
|
sql: parsed.sql,
|
|
77
79
|
applied: [],
|
|
78
80
|
skipped: [],
|
|
@@ -88,6 +90,7 @@ export class StaticPredicatePlacementOptimizer {
|
|
|
88
90
|
message: "Static predicate placement currently supports only SimpleSelectQuery roots."
|
|
89
91
|
});
|
|
90
92
|
return this.buildResult({
|
|
93
|
+
query: parsed.query,
|
|
91
94
|
sql: parsed.sql,
|
|
92
95
|
applied: [],
|
|
93
96
|
skipped: [],
|
|
@@ -102,39 +105,74 @@ export class StaticPredicatePlacementOptimizer {
|
|
|
102
105
|
const skipped = [];
|
|
103
106
|
const movedTerms = [];
|
|
104
107
|
for (const term of query.whereClause ? collectTopLevelAndTerms(query.whereClause.condition) : []) {
|
|
105
|
-
const candidate = this.analyzeCandidate(query, term);
|
|
108
|
+
const candidate = this.analyzeCandidate(query, term, options);
|
|
106
109
|
if (!candidate) {
|
|
107
110
|
continue;
|
|
108
111
|
}
|
|
109
112
|
if ("code" in candidate) {
|
|
110
|
-
skipped.push(this.makeSkipped(term, candidate));
|
|
113
|
+
skipped.push(this.makeSkipped(term, candidate, options));
|
|
111
114
|
continue;
|
|
112
115
|
}
|
|
113
116
|
const target = this.resolveTarget(query, candidate);
|
|
114
117
|
if ("code" in target) {
|
|
115
|
-
skipped.push(this.makeSkipped(term, target));
|
|
118
|
+
skipped.push(this.makeSkipped(term, target, options));
|
|
116
119
|
continue;
|
|
117
120
|
}
|
|
118
|
-
|
|
119
|
-
if ("
|
|
120
|
-
|
|
121
|
-
|
|
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.";
|
|
122
159
|
}
|
|
123
|
-
const movedPredicate = this.rebasePredicate(candidate.expression, targetColumns);
|
|
124
|
-
target.query.appendWhere(movedPredicate);
|
|
125
160
|
movedTerms.push(term);
|
|
126
161
|
applied.push({
|
|
127
162
|
kind: "move_static_predicate",
|
|
128
163
|
predicateSql: candidate.predicateSql,
|
|
129
164
|
fromScopeId: "scope:root",
|
|
130
165
|
toScopeId: target.scopeId,
|
|
131
|
-
reason:
|
|
166
|
+
reason: appliedReason,
|
|
132
167
|
columnReferences: candidate.references.map(columnReferenceText)
|
|
133
168
|
});
|
|
134
169
|
}
|
|
135
170
|
rebuildWhereWithoutTerms(query, new Set(movedTerms));
|
|
136
|
-
const sql = applied.length > 0
|
|
171
|
+
const sql = applied.length > 0 || hasSqlComponentFormatOverride(options)
|
|
172
|
+
? formatSqlComponent(query, options)
|
|
173
|
+
: parsed.sql;
|
|
137
174
|
return this.buildResult({
|
|
175
|
+
query,
|
|
138
176
|
sql,
|
|
139
177
|
applied,
|
|
140
178
|
skipped,
|
|
@@ -148,13 +186,22 @@ export class StaticPredicatePlacementOptimizer {
|
|
|
148
186
|
var _a;
|
|
149
187
|
return this.plan(input, Object.assign(Object.assign({}, options), { dryRun: (_a = options.dryRun) !== null && _a !== void 0 ? _a : false }));
|
|
150
188
|
}
|
|
151
|
-
parseInput(input) {
|
|
189
|
+
parseInput(input, options) {
|
|
152
190
|
const warnings = [];
|
|
153
191
|
const errors = [];
|
|
154
192
|
try {
|
|
155
193
|
const sourceSql = typeof input === "string"
|
|
156
194
|
? input
|
|
157
|
-
: formatSqlComponent(input);
|
|
195
|
+
: formatSqlComponent(input, options);
|
|
196
|
+
if (typeof input !== "string" && options.cloneInput === false) {
|
|
197
|
+
return {
|
|
198
|
+
query: input,
|
|
199
|
+
sql: sourceSql,
|
|
200
|
+
formatterGeneratedSource: false,
|
|
201
|
+
warnings,
|
|
202
|
+
errors
|
|
203
|
+
};
|
|
204
|
+
}
|
|
158
205
|
if (typeof input !== "string") {
|
|
159
206
|
warnings.push({
|
|
160
207
|
code: "AST_INPUT_FORMATTED",
|
|
@@ -185,7 +232,7 @@ export class StaticPredicatePlacementOptimizer {
|
|
|
185
232
|
};
|
|
186
233
|
}
|
|
187
234
|
}
|
|
188
|
-
analyzeCandidate(root, expression) {
|
|
235
|
+
analyzeCandidate(root, expression, options) {
|
|
189
236
|
if (this.collectParameterNames(expression).length > 0) {
|
|
190
237
|
return null;
|
|
191
238
|
}
|
|
@@ -202,7 +249,7 @@ export class StaticPredicatePlacementOptimizer {
|
|
|
202
249
|
}
|
|
203
250
|
return {
|
|
204
251
|
expression,
|
|
205
|
-
predicateSql: formatSqlComponent(expression),
|
|
252
|
+
predicateSql: formatSqlComponent(expression, options),
|
|
206
253
|
references
|
|
207
254
|
};
|
|
208
255
|
}
|
|
@@ -263,13 +310,6 @@ export class StaticPredicatePlacementOptimizer {
|
|
|
263
310
|
}
|
|
264
311
|
const candidate = unwrapParens(value);
|
|
265
312
|
if (candidate instanceof BinaryExpression) {
|
|
266
|
-
if (candidate.operator.value.trim().toLowerCase() === "or") {
|
|
267
|
-
found = {
|
|
268
|
-
code: "OR_PREDICATE_UNSUPPORTED",
|
|
269
|
-
reason: "Predicate contains OR predicates, which are not moved in the first safe-only implementation."
|
|
270
|
-
};
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
313
|
visit(candidate.left);
|
|
274
314
|
visit(candidate.right);
|
|
275
315
|
return;
|
|
@@ -306,13 +346,6 @@ export class StaticPredicatePlacementOptimizer {
|
|
|
306
346
|
};
|
|
307
347
|
return;
|
|
308
348
|
}
|
|
309
|
-
if (candidate instanceof BetweenExpression) {
|
|
310
|
-
found = {
|
|
311
|
-
code: "BETWEEN_PREDICATE_UNSUPPORTED",
|
|
312
|
-
reason: "BETWEEN predicates are not moved in the first safe-only implementation."
|
|
313
|
-
};
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
349
|
if (candidate instanceof UnaryExpression) {
|
|
317
350
|
visit(candidate.expression);
|
|
318
351
|
return;
|
|
@@ -345,7 +378,7 @@ export class StaticPredicatePlacementOptimizer {
|
|
|
345
378
|
return found;
|
|
346
379
|
}
|
|
347
380
|
resolveTarget(root, candidate) {
|
|
348
|
-
const boundary = this.
|
|
381
|
+
const boundary = this.findRootQueryBoundary(root);
|
|
349
382
|
if (boundary) {
|
|
350
383
|
return boundary;
|
|
351
384
|
}
|
|
@@ -357,7 +390,7 @@ export class StaticPredicatePlacementOptimizer {
|
|
|
357
390
|
}
|
|
358
391
|
const bindings = [];
|
|
359
392
|
for (const reference of candidate.references) {
|
|
360
|
-
const binding = this.resolveSourceBinding(root, reference);
|
|
393
|
+
const binding = this.resolveSourceBinding(root, root, reference);
|
|
361
394
|
if ("code" in binding) {
|
|
362
395
|
return binding;
|
|
363
396
|
}
|
|
@@ -376,27 +409,17 @@ export class StaticPredicatePlacementOptimizer {
|
|
|
376
409
|
if (nullableSide) {
|
|
377
410
|
return nullableSide;
|
|
378
411
|
}
|
|
379
|
-
const upstream = this.resolveUpstreamQuery(root, binding);
|
|
412
|
+
const upstream = this.resolveUpstreamQuery(root, binding, candidate.references);
|
|
380
413
|
if ("code" in upstream) {
|
|
381
414
|
return upstream;
|
|
382
415
|
}
|
|
383
|
-
const targetBoundary = this.findTargetQueryBoundary(upstream.query);
|
|
384
|
-
if (targetBoundary) {
|
|
385
|
-
return targetBoundary;
|
|
386
|
-
}
|
|
387
416
|
return upstream;
|
|
388
417
|
}
|
|
389
|
-
|
|
390
|
-
if (query
|
|
418
|
+
findRootQueryBoundary(query) {
|
|
419
|
+
if (this.hasDistinctOnBoundary(query)) {
|
|
391
420
|
return {
|
|
392
421
|
code: "DISTINCT_BOUNDARY",
|
|
393
|
-
reason: "Predicate crosses DISTINCT boundary; moving it may change semantics."
|
|
394
|
-
};
|
|
395
|
-
}
|
|
396
|
-
if (query.groupByClause || query.havingClause) {
|
|
397
|
-
return {
|
|
398
|
-
code: "GROUP_BY_BOUNDARY",
|
|
399
|
-
reason: "Predicate crosses GROUP BY boundary; moving it may change semantics."
|
|
422
|
+
reason: "Predicate crosses DISTINCT ON boundary; moving it may change semantics."
|
|
400
423
|
};
|
|
401
424
|
}
|
|
402
425
|
if (this.hasWindowUsage(query)) {
|
|
@@ -407,10 +430,19 @@ export class StaticPredicatePlacementOptimizer {
|
|
|
407
430
|
}
|
|
408
431
|
return null;
|
|
409
432
|
}
|
|
410
|
-
|
|
411
|
-
const
|
|
412
|
-
if (
|
|
413
|
-
return
|
|
433
|
+
resolveTargetPlacement(query, targetColumns) {
|
|
434
|
+
const hasOrdinaryDistinct = this.hasOrdinaryDistinct(query);
|
|
435
|
+
if (this.hasDistinctOnBoundary(query)) {
|
|
436
|
+
return {
|
|
437
|
+
code: "DISTINCT_BOUNDARY",
|
|
438
|
+
reason: "Predicate crosses DISTINCT ON boundary; moving it may change semantics."
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
if (this.hasWindowUsage(query)) {
|
|
442
|
+
return {
|
|
443
|
+
code: "WINDOW_BOUNDARY",
|
|
444
|
+
reason: "Predicate crosses WINDOW boundary; moving it may change semantics."
|
|
445
|
+
};
|
|
414
446
|
}
|
|
415
447
|
if (query.limitClause || query.offsetClause || query.fetchClause) {
|
|
416
448
|
return {
|
|
@@ -424,10 +456,35 @@ export class StaticPredicatePlacementOptimizer {
|
|
|
424
456
|
reason: "Target query contains an OUTER JOIN boundary that is not moved across in the safe-only implementation."
|
|
425
457
|
};
|
|
426
458
|
}
|
|
427
|
-
|
|
459
|
+
if (query.groupByClause) {
|
|
460
|
+
const allReferencesAreGroupKeys = targetColumns.every(item => this.isGroupKeyColumn(query, item.targetColumn));
|
|
461
|
+
if (!allReferencesAreGroupKeys) {
|
|
462
|
+
return {
|
|
463
|
+
code: "GROUP_BY_BOUNDARY",
|
|
464
|
+
reason: "Predicate references a target column that is not proven to be a GROUP BY key."
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
return {
|
|
468
|
+
reason: "Predicate references only GROUP BY keys; it is moved into pre-aggregation WHERE."
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
if (query.havingClause) {
|
|
472
|
+
return {
|
|
473
|
+
code: "GROUP_BY_BOUNDARY",
|
|
474
|
+
reason: "Predicate crosses HAVING aggregation boundary; moving it may change semantics."
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
if (hasOrdinaryDistinct) {
|
|
478
|
+
return {
|
|
479
|
+
reason: "Predicate references direct ordinary DISTINCT output columns; it is moved into the DISTINCT input WHERE."
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
return {
|
|
483
|
+
reason: "All outer references resolve to direct upstream outputs before unsafe query boundaries."
|
|
484
|
+
};
|
|
428
485
|
}
|
|
429
|
-
resolveSourceBinding(
|
|
430
|
-
const fromClause =
|
|
486
|
+
resolveSourceBinding(contextRoot, query, column) {
|
|
487
|
+
const fromClause = query.fromClause;
|
|
431
488
|
if (!fromClause) {
|
|
432
489
|
return {
|
|
433
490
|
code: "NO_FROM_CLAUSE",
|
|
@@ -445,10 +502,7 @@ export class StaticPredicatePlacementOptimizer {
|
|
|
445
502
|
reason: `Column source alias '${column.getNamespace()}' is not uniquely resolvable.`
|
|
446
503
|
};
|
|
447
504
|
}
|
|
448
|
-
|
|
449
|
-
return matches[0];
|
|
450
|
-
}
|
|
451
|
-
const matchCount = this.getOutputColumnMatchCount(root, matches[0], columnName);
|
|
505
|
+
const matchCount = this.getOutputColumnMatchCount(contextRoot, matches[0], columnName);
|
|
452
506
|
if (matchCount === 0) {
|
|
453
507
|
return {
|
|
454
508
|
code: "COLUMN_NOT_AVAILABLE_UPSTREAM",
|
|
@@ -464,13 +518,8 @@ export class StaticPredicatePlacementOptimizer {
|
|
|
464
518
|
return matches[0];
|
|
465
519
|
}
|
|
466
520
|
const matches = [];
|
|
467
|
-
let unionSource = null;
|
|
468
521
|
for (const binding of bindings) {
|
|
469
|
-
|
|
470
|
-
unionSource !== null && unionSource !== void 0 ? unionSource : (unionSource = binding);
|
|
471
|
-
continue;
|
|
472
|
-
}
|
|
473
|
-
const matchCount = this.getOutputColumnMatchCount(root, binding, columnName);
|
|
522
|
+
const matchCount = this.getOutputColumnMatchCount(contextRoot, binding, columnName);
|
|
474
523
|
if (matchCount > 1) {
|
|
475
524
|
return {
|
|
476
525
|
code: "AMBIGUOUS_COLUMN_REFERENCE",
|
|
@@ -481,12 +530,6 @@ export class StaticPredicatePlacementOptimizer {
|
|
|
481
530
|
matches.push(binding);
|
|
482
531
|
}
|
|
483
532
|
}
|
|
484
|
-
if (matches.length === 0 && unionSource) {
|
|
485
|
-
return {
|
|
486
|
-
code: "UNION_BOUNDARY",
|
|
487
|
-
reason: "Predicate would need distribution into a UNION branch, which is unsupported."
|
|
488
|
-
};
|
|
489
|
-
}
|
|
490
533
|
if (matches.length !== 1) {
|
|
491
534
|
return {
|
|
492
535
|
code: "AMBIGUOUS_COLUMN_REFERENCE",
|
|
@@ -497,16 +540,20 @@ export class StaticPredicatePlacementOptimizer {
|
|
|
497
540
|
}
|
|
498
541
|
return matches[0];
|
|
499
542
|
}
|
|
500
|
-
resolveUpstreamQuery(root, binding) {
|
|
543
|
+
resolveUpstreamQuery(root, binding, references) {
|
|
501
544
|
const source = binding.source.datasource;
|
|
502
545
|
if (source instanceof SubQuerySource) {
|
|
503
546
|
if (source.query instanceof SimpleSelectQuery) {
|
|
504
547
|
return {
|
|
548
|
+
kind: "simple",
|
|
505
549
|
query: source.query,
|
|
506
550
|
scopeId: `subquery:${binding.alias}`,
|
|
507
551
|
sourceBinding: binding
|
|
508
552
|
};
|
|
509
553
|
}
|
|
554
|
+
if (source.query instanceof BinarySelectQuery) {
|
|
555
|
+
return this.resolveUnionTarget(root, source.query, `subquery:${binding.alias}`, references);
|
|
556
|
+
}
|
|
510
557
|
return {
|
|
511
558
|
code: "UNION_BOUNDARY",
|
|
512
559
|
reason: "Predicate would need distribution into a UNION or non-simple subquery, which is unsupported."
|
|
@@ -534,10 +581,7 @@ export class StaticPredicatePlacementOptimizer {
|
|
|
534
581
|
};
|
|
535
582
|
}
|
|
536
583
|
if (commonTable.query instanceof BinarySelectQuery) {
|
|
537
|
-
return {
|
|
538
|
-
code: "UNION_BOUNDARY",
|
|
539
|
-
reason: "Predicate would need distribution into a UNION branch, which is unsupported."
|
|
540
|
-
};
|
|
584
|
+
return this.resolveUnionTarget(root, commonTable.query, `cte:${commonTable.getSourceAliasName()}`, references);
|
|
541
585
|
}
|
|
542
586
|
if (!(commonTable.query instanceof SimpleSelectQuery)) {
|
|
543
587
|
return {
|
|
@@ -546,16 +590,48 @@ export class StaticPredicatePlacementOptimizer {
|
|
|
546
590
|
};
|
|
547
591
|
}
|
|
548
592
|
return {
|
|
593
|
+
kind: "simple",
|
|
549
594
|
query: commonTable.query,
|
|
550
595
|
scopeId: `cte:${commonTable.getSourceAliasName()}`,
|
|
551
596
|
sourceBinding: binding,
|
|
552
597
|
cteName: commonTable.getSourceAliasName()
|
|
553
598
|
};
|
|
554
599
|
}
|
|
555
|
-
|
|
600
|
+
resolveUnionTarget(root, query, scopeId, references) {
|
|
601
|
+
const branches = this.collectUnionBranches(query);
|
|
602
|
+
if ("code" in branches) {
|
|
603
|
+
return branches;
|
|
604
|
+
}
|
|
605
|
+
const outputIndexes = new Map();
|
|
606
|
+
for (const reference of references) {
|
|
607
|
+
const outputIndex = this.resolveUnionOutputIndex(root, branches[0], reference.column.name);
|
|
608
|
+
if ("code" in outputIndex) {
|
|
609
|
+
return outputIndex;
|
|
610
|
+
}
|
|
611
|
+
outputIndexes.set(reference, outputIndex.index);
|
|
612
|
+
}
|
|
613
|
+
const branchTargets = [];
|
|
614
|
+
for (const branch of branches) {
|
|
615
|
+
const targetColumns = [];
|
|
616
|
+
for (const [reference, outputIndex] of outputIndexes.entries()) {
|
|
617
|
+
const targetColumn = this.resolveTargetColumnByOutputIndex(root, branch, outputIndex, reference);
|
|
618
|
+
if ("code" in targetColumn) {
|
|
619
|
+
return targetColumn;
|
|
620
|
+
}
|
|
621
|
+
targetColumns.push(targetColumn);
|
|
622
|
+
}
|
|
623
|
+
branchTargets.push(this.resolveDeepestBranchTarget(root, branch, targetColumns));
|
|
624
|
+
}
|
|
625
|
+
return {
|
|
626
|
+
kind: "union",
|
|
627
|
+
scopeId,
|
|
628
|
+
branches: branchTargets
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
resolveTargetColumns(root, query, references) {
|
|
556
632
|
const resolved = [];
|
|
557
633
|
for (const reference of references) {
|
|
558
|
-
const matches = query.
|
|
634
|
+
const matches = this.collectSelectOutputs(root, query).filter(item => normalizeIdentifier(item.name) === normalizeIdentifier(reference.column.name));
|
|
559
635
|
if (matches.length !== 1) {
|
|
560
636
|
return {
|
|
561
637
|
code: "AMBIGUOUS_TARGET_COLUMN",
|
|
@@ -582,6 +658,50 @@ export class StaticPredicatePlacementOptimizer {
|
|
|
582
658
|
}
|
|
583
659
|
return resolved;
|
|
584
660
|
}
|
|
661
|
+
resolveDeepestBranchTarget(contextRoot, query, targetColumns, visited = new Set()) {
|
|
662
|
+
if (visited.has(query) || targetColumns.length === 0) {
|
|
663
|
+
return { query, targetColumns: [...targetColumns] };
|
|
664
|
+
}
|
|
665
|
+
const nextVisited = new Set(visited);
|
|
666
|
+
nextVisited.add(query);
|
|
667
|
+
const bindings = [];
|
|
668
|
+
for (const item of targetColumns) {
|
|
669
|
+
const binding = this.resolveSourceBinding(contextRoot, query, item.targetColumn);
|
|
670
|
+
if ("code" in binding) {
|
|
671
|
+
return { query, targetColumns: [...targetColumns] };
|
|
672
|
+
}
|
|
673
|
+
if (!bindings.some(existing => existing.source === binding.source)) {
|
|
674
|
+
bindings.push(binding);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
if (bindings.length !== 1) {
|
|
678
|
+
return { query, targetColumns: [...targetColumns] };
|
|
679
|
+
}
|
|
680
|
+
const binding = bindings[0];
|
|
681
|
+
const nullableSide = query.fromClause
|
|
682
|
+
? this.findNullableSideBoundary(query.fromClause, binding)
|
|
683
|
+
: null;
|
|
684
|
+
if (nullableSide) {
|
|
685
|
+
return { query, targetColumns: [...targetColumns] };
|
|
686
|
+
}
|
|
687
|
+
const upstream = this.resolveUpstreamQuery(contextRoot, binding, targetColumns.map(item => item.targetColumn));
|
|
688
|
+
if ("code" in upstream || upstream.kind === "union") {
|
|
689
|
+
return { query, targetColumns: [...targetColumns] };
|
|
690
|
+
}
|
|
691
|
+
const upstreamColumns = this.resolveTargetColumns(contextRoot, upstream.query, targetColumns.map(item => item.targetColumn));
|
|
692
|
+
if ("code" in upstreamColumns) {
|
|
693
|
+
return { query, targetColumns: [...targetColumns] };
|
|
694
|
+
}
|
|
695
|
+
const placement = this.resolveTargetPlacement(upstream.query, upstreamColumns);
|
|
696
|
+
if ("code" in placement) {
|
|
697
|
+
return { query, targetColumns: [...targetColumns] };
|
|
698
|
+
}
|
|
699
|
+
const rebasedColumns = upstreamColumns.map((item, index) => ({
|
|
700
|
+
sourceColumn: targetColumns[index].sourceColumn,
|
|
701
|
+
targetColumn: item.targetColumn
|
|
702
|
+
}));
|
|
703
|
+
return this.resolveDeepestBranchTarget(contextRoot, upstream.query, rebasedColumns, nextVisited);
|
|
704
|
+
}
|
|
585
705
|
verifyColumnResolvableInQuery(query, column) {
|
|
586
706
|
if (!query.fromClause) {
|
|
587
707
|
return {
|
|
@@ -608,8 +728,42 @@ export class StaticPredicatePlacementOptimizer {
|
|
|
608
728
|
reason: `Unqualified target column '${column.column.name}' is ambiguous in a multi-source destination query.`
|
|
609
729
|
};
|
|
610
730
|
}
|
|
611
|
-
|
|
612
|
-
const
|
|
731
|
+
isGroupKeyColumn(query, column) {
|
|
732
|
+
const groupBy = query.groupByClause;
|
|
733
|
+
if (!groupBy || groupBy.mode || groupBy.grouping.length === 0) {
|
|
734
|
+
return false;
|
|
735
|
+
}
|
|
736
|
+
return groupBy.grouping.some(grouping => {
|
|
737
|
+
const candidate = unwrapParens(grouping);
|
|
738
|
+
return candidate instanceof ColumnReference
|
|
739
|
+
&& this.sameResolvableColumnInQuery(query, candidate, column);
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
sameResolvableColumnInQuery(query, left, right) {
|
|
743
|
+
if (sameColumnReference(left, right)) {
|
|
744
|
+
return true;
|
|
745
|
+
}
|
|
746
|
+
if (normalizeIdentifier(left.column.name) !== normalizeIdentifier(right.column.name)) {
|
|
747
|
+
return false;
|
|
748
|
+
}
|
|
749
|
+
const leftSource = this.resolveColumnSourceAlias(query, left);
|
|
750
|
+
const rightSource = this.resolveColumnSourceAlias(query, right);
|
|
751
|
+
return leftSource !== null && leftSource === rightSource;
|
|
752
|
+
}
|
|
753
|
+
resolveColumnSourceAlias(query, column) {
|
|
754
|
+
if (!query.fromClause) {
|
|
755
|
+
return null;
|
|
756
|
+
}
|
|
757
|
+
const bindings = this.getSourceBindings(query.fromClause);
|
|
758
|
+
const namespace = normalizeIdentifier(column.getNamespace());
|
|
759
|
+
if (namespace) {
|
|
760
|
+
const matches = bindings.filter(binding => normalizeIdentifier(binding.alias) === namespace);
|
|
761
|
+
return matches.length === 1 ? normalizeIdentifier(matches[0].alias) : null;
|
|
762
|
+
}
|
|
763
|
+
return bindings.length === 1 ? normalizeIdentifier(bindings[0].alias) : null;
|
|
764
|
+
}
|
|
765
|
+
rebasePredicate(expression, targetColumns, options) {
|
|
766
|
+
const cloned = cloneValueComponent(expression, options);
|
|
613
767
|
const visitSelect = (query, inheritedLocalAliases) => {
|
|
614
768
|
var _a, _b, _c, _d;
|
|
615
769
|
if (!(query instanceof SimpleSelectQuery)) {
|
|
@@ -794,12 +948,103 @@ export class StaticPredicatePlacementOptimizer {
|
|
|
794
948
|
return joinType.includes("left") || joinType.includes("right") || joinType.includes("full") || joinType.includes("outer");
|
|
795
949
|
});
|
|
796
950
|
}
|
|
951
|
+
hasDistinctOnBoundary(query) {
|
|
952
|
+
return query.selectClause.distinct instanceof DistinctOn;
|
|
953
|
+
}
|
|
954
|
+
hasOrdinaryDistinct(query) {
|
|
955
|
+
return query.selectClause.distinct !== null && !this.hasDistinctOnBoundary(query);
|
|
956
|
+
}
|
|
797
957
|
getOutputColumnMatchCount(root, binding, columnName) {
|
|
798
958
|
const target = this.resolveSourceQueryForColumns(root, binding.source);
|
|
799
|
-
if (!target
|
|
959
|
+
if (!target) {
|
|
960
|
+
return 0;
|
|
961
|
+
}
|
|
962
|
+
if (target instanceof BinarySelectQuery) {
|
|
963
|
+
const branches = this.collectUnionBranches(target);
|
|
964
|
+
if ("code" in branches) {
|
|
965
|
+
return 0;
|
|
966
|
+
}
|
|
967
|
+
return this.collectSelectOutputs(root, branches[0]).filter(item => normalizeIdentifier(item.name) === normalizeIdentifier(columnName)).length;
|
|
968
|
+
}
|
|
969
|
+
if (!(target instanceof SimpleSelectQuery)) {
|
|
800
970
|
return 0;
|
|
801
971
|
}
|
|
802
|
-
return target.
|
|
972
|
+
return this.collectSelectOutputs(root, target).filter(item => normalizeIdentifier(item.name) === normalizeIdentifier(columnName)).length;
|
|
973
|
+
}
|
|
974
|
+
collectUnionBranches(query) {
|
|
975
|
+
const branches = [];
|
|
976
|
+
const visit = (select) => {
|
|
977
|
+
var _a;
|
|
978
|
+
if (select instanceof SimpleSelectQuery) {
|
|
979
|
+
branches.push(select);
|
|
980
|
+
return null;
|
|
981
|
+
}
|
|
982
|
+
if (!(select instanceof BinarySelectQuery)) {
|
|
983
|
+
return {
|
|
984
|
+
code: "UNION_BOUNDARY",
|
|
985
|
+
reason: "Predicate would need distribution into a non-simple UNION branch, which is unsupported."
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
const operator = select.operator.value.trim().toLowerCase();
|
|
989
|
+
if (operator !== "union" && operator !== "union all") {
|
|
990
|
+
return {
|
|
991
|
+
code: "UNION_BOUNDARY",
|
|
992
|
+
reason: `Predicate would need distribution through '${select.operator.value}', which is unsupported.`
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
return (_a = visit(select.left)) !== null && _a !== void 0 ? _a : visit(select.right);
|
|
996
|
+
};
|
|
997
|
+
const skip = visit(query);
|
|
998
|
+
return skip !== null && skip !== void 0 ? skip : branches;
|
|
999
|
+
}
|
|
1000
|
+
resolveUnionOutputIndex(root, firstBranch, outputColumnName) {
|
|
1001
|
+
const matches = [];
|
|
1002
|
+
this.collectSelectOutputs(root, firstBranch).forEach((item, index) => {
|
|
1003
|
+
if (normalizeIdentifier(item.name) === normalizeIdentifier(outputColumnName)) {
|
|
1004
|
+
matches.push(index);
|
|
1005
|
+
}
|
|
1006
|
+
});
|
|
1007
|
+
if (matches.length !== 1) {
|
|
1008
|
+
return {
|
|
1009
|
+
code: "AMBIGUOUS_TARGET_COLUMN",
|
|
1010
|
+
reason: matches.length === 0
|
|
1011
|
+
? `UNION output does not expose '${outputColumnName}' from its first branch.`
|
|
1012
|
+
: `UNION first branch exposes multiple '${outputColumnName}' columns.`
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
return { index: matches[0] };
|
|
1016
|
+
}
|
|
1017
|
+
resolveTargetColumnByOutputIndex(root, query, outputIndex, sourceColumn) {
|
|
1018
|
+
const output = this.collectSelectOutputs(root, query)[outputIndex];
|
|
1019
|
+
if (!output) {
|
|
1020
|
+
return {
|
|
1021
|
+
code: "UNION_COLUMN_MISMATCH",
|
|
1022
|
+
reason: `UNION branch does not expose column '${sourceColumn.column.name}' at output position ${outputIndex + 1}.`
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
if (!(output.value instanceof ColumnReference)) {
|
|
1026
|
+
return {
|
|
1027
|
+
code: "EXPRESSION_OUTPUT_UNSUPPORTED",
|
|
1028
|
+
reason: `UNION branch output at position ${outputIndex + 1} for '${sourceColumn.column.name}' is an expression, not a direct column reference.`
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
const sourceResolution = this.verifyColumnResolvableInQuery(query, output.value);
|
|
1032
|
+
if (sourceResolution) {
|
|
1033
|
+
return sourceResolution;
|
|
1034
|
+
}
|
|
1035
|
+
return {
|
|
1036
|
+
sourceColumn,
|
|
1037
|
+
targetColumn: output.value
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
collectSelectOutputs(root, query) {
|
|
1041
|
+
var _a, _b, _c, _d;
|
|
1042
|
+
const commonTables = [
|
|
1043
|
+
...((_b = (_a = query.withClause) === null || _a === void 0 ? void 0 : _a.tables) !== null && _b !== void 0 ? _b : []),
|
|
1044
|
+
...((_d = (_c = root.withClause) === null || _c === void 0 ? void 0 : _c.tables) !== null && _d !== void 0 ? _d : [])
|
|
1045
|
+
];
|
|
1046
|
+
const collector = new SelectOutputCollector(null, commonTables.length > 0 ? commonTables : null);
|
|
1047
|
+
return collector.collect(query);
|
|
803
1048
|
}
|
|
804
1049
|
resolveSourceQueryForColumns(root, source) {
|
|
805
1050
|
var _a;
|
|
@@ -814,15 +1059,6 @@ export class StaticPredicatePlacementOptimizer {
|
|
|
814
1059
|
}
|
|
815
1060
|
return null;
|
|
816
1061
|
}
|
|
817
|
-
getSelectItemOutputName(item) {
|
|
818
|
-
if (item.identifier) {
|
|
819
|
-
return item.identifier.name;
|
|
820
|
-
}
|
|
821
|
-
if (item.value instanceof ColumnReference) {
|
|
822
|
-
return item.value.column.name;
|
|
823
|
-
}
|
|
824
|
-
return null;
|
|
825
|
-
}
|
|
826
1062
|
findCte(root, name) {
|
|
827
1063
|
var _a, _b;
|
|
828
1064
|
const normalized = normalizeIdentifier(name);
|
|
@@ -1168,13 +1404,14 @@ export class StaticPredicatePlacementOptimizer {
|
|
|
1168
1404
|
visit(expression);
|
|
1169
1405
|
return names;
|
|
1170
1406
|
}
|
|
1171
|
-
makeSkipped(expression, draft) {
|
|
1172
|
-
return Object.assign({ predicateSql: formatSqlComponent(expression), scopeId: "scope:root" }, draft);
|
|
1407
|
+
makeSkipped(expression, draft, options) {
|
|
1408
|
+
return Object.assign({ predicateSql: formatSqlComponent(expression, options), scopeId: "scope:root" }, draft);
|
|
1173
1409
|
}
|
|
1174
1410
|
buildResult(params) {
|
|
1175
1411
|
return {
|
|
1176
1412
|
ok: params.errors.length === 0,
|
|
1177
1413
|
sql: params.sql,
|
|
1414
|
+
query: params.query,
|
|
1178
1415
|
applied: params.applied,
|
|
1179
1416
|
skipped: params.skipped,
|
|
1180
1417
|
warnings: params.warnings,
|