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 SUPPORTED_OPERATORS = new Set(["=", "<>", "!=", "<", "<=", ">", ">=", "like", "ilike", "in"]);
|
|
8
9
|
const VOLATILE_OR_UNSUPPORTED_FUNCTION_REASON = "Condition contains a function call; volatile and expression predicates are not moved in the safe-only implementation.";
|
|
9
10
|
const normalizeIdentifier = (value) => value.trim().toLowerCase();
|
|
@@ -45,8 +46,8 @@ const rebuildWhereWithoutTerms = (query, termsToRemove) => {
|
|
|
45
46
|
}
|
|
46
47
|
query.whereClause = new WhereClause(rebuilt);
|
|
47
48
|
};
|
|
48
|
-
const cloneValueComponent = (expression) => {
|
|
49
|
-
return ValueParser.parse(formatSqlComponent(expression));
|
|
49
|
+
const cloneValueComponent = (expression, options) => {
|
|
50
|
+
return ValueParser.parse(formatSqlComponent(expression, options));
|
|
50
51
|
};
|
|
51
52
|
const cloneColumnReference = (reference) => {
|
|
52
53
|
var _a, _b;
|
|
@@ -69,11 +70,12 @@ const appendUnique = (items, value) => {
|
|
|
69
70
|
export class ParameterConditionPlacementOptimizer {
|
|
70
71
|
plan(input, options = {}) {
|
|
71
72
|
var _a, _b, _c;
|
|
72
|
-
const parsed = this.parseInput(input);
|
|
73
|
+
const parsed = this.parseInput(input, options);
|
|
73
74
|
const warnings = [...parsed.warnings];
|
|
74
75
|
const errors = [...parsed.errors];
|
|
75
76
|
if (!parsed.query) {
|
|
76
77
|
return this.buildResult({
|
|
78
|
+
query: null,
|
|
77
79
|
sql: parsed.sql,
|
|
78
80
|
applied: [],
|
|
79
81
|
skipped: [],
|
|
@@ -89,6 +91,7 @@ export class ParameterConditionPlacementOptimizer {
|
|
|
89
91
|
message: "Parameter condition placement currently supports only SimpleSelectQuery roots."
|
|
90
92
|
});
|
|
91
93
|
return this.buildResult({
|
|
94
|
+
query: parsed.query,
|
|
92
95
|
sql: parsed.sql,
|
|
93
96
|
applied: [],
|
|
94
97
|
skipped: [],
|
|
@@ -103,40 +106,75 @@ export class ParameterConditionPlacementOptimizer {
|
|
|
103
106
|
const skipped = [];
|
|
104
107
|
const movedTerms = [];
|
|
105
108
|
for (const term of query.whereClause ? collectTopLevelAndTerms(query.whereClause.condition) : []) {
|
|
106
|
-
const candidate = this.analyzeCandidate(term);
|
|
109
|
+
const candidate = this.analyzeCandidate(term, options);
|
|
107
110
|
if (!candidate) {
|
|
108
111
|
continue;
|
|
109
112
|
}
|
|
110
113
|
if ("code" in candidate) {
|
|
111
|
-
skipped.push(this.makeSkipped(term, candidate));
|
|
114
|
+
skipped.push(this.makeSkipped(term, candidate, options));
|
|
112
115
|
continue;
|
|
113
116
|
}
|
|
114
117
|
const target = this.resolveTarget(query, candidate);
|
|
115
118
|
if ("code" in target) {
|
|
116
|
-
skipped.push(this.makeSkipped(term, target));
|
|
119
|
+
skipped.push(this.makeSkipped(term, target, options));
|
|
117
120
|
continue;
|
|
118
121
|
}
|
|
119
|
-
|
|
120
|
-
if ("
|
|
121
|
-
|
|
122
|
-
|
|
122
|
+
let appliedReason = "";
|
|
123
|
+
if (target.kind === "simple") {
|
|
124
|
+
const targetColumns = this.resolveTargetColumns(query, target.query, candidate.references);
|
|
125
|
+
if ("code" in targetColumns) {
|
|
126
|
+
skipped.push(this.makeSkipped(term, targetColumns, options));
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const placement = this.resolveTargetPlacement(target.query, targetColumns);
|
|
130
|
+
if ("code" in placement) {
|
|
131
|
+
skipped.push(this.makeSkipped(term, placement, options));
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const movedCondition = this.rebaseCondition(candidate.expression, targetColumns, options);
|
|
135
|
+
target.query.appendWhere(movedCondition);
|
|
136
|
+
appliedReason = placement.reason;
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
const placements = [];
|
|
140
|
+
let skip = null;
|
|
141
|
+
for (const branch of target.branches) {
|
|
142
|
+
const placement = this.resolveTargetPlacement(branch.query, branch.targetColumns);
|
|
143
|
+
if ("code" in placement) {
|
|
144
|
+
skip = placement;
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
placements.push(placement);
|
|
148
|
+
}
|
|
149
|
+
if (skip) {
|
|
150
|
+
skipped.push(this.makeSkipped(term, skip, options));
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
for (const branch of target.branches) {
|
|
154
|
+
const movedCondition = this.rebaseCondition(candidate.expression, branch.targetColumns, options);
|
|
155
|
+
branch.query.appendWhere(movedCondition);
|
|
156
|
+
}
|
|
157
|
+
appliedReason = placements.some(item => /group by/i.test(item.reason))
|
|
158
|
+
? "Condition is distributed to every UNION branch by output column position; grouped branches only receive GROUP BY-key predicates."
|
|
159
|
+
: "Condition is distributed to every UNION branch by output column position before unsafe query boundaries.";
|
|
123
160
|
}
|
|
124
|
-
const movedCondition = this.rebaseCondition(candidate.expression, candidate.column, targetColumn.column);
|
|
125
|
-
target.query.appendWhere(movedCondition);
|
|
126
161
|
movedTerms.push(term);
|
|
127
162
|
applied.push({
|
|
128
163
|
kind: "move_condition",
|
|
129
164
|
conditionSql: candidate.conditionSql,
|
|
130
165
|
fromScopeId: "scope:root",
|
|
131
166
|
toScopeId: target.scopeId,
|
|
132
|
-
reason:
|
|
167
|
+
reason: appliedReason,
|
|
133
168
|
parameterNames: candidate.parameterNames,
|
|
134
|
-
columnReferences:
|
|
169
|
+
columnReferences: candidate.references.map(columnReferenceText)
|
|
135
170
|
});
|
|
136
171
|
}
|
|
137
172
|
rebuildWhereWithoutTerms(query, new Set(movedTerms));
|
|
138
|
-
const sql = applied.length > 0
|
|
173
|
+
const sql = applied.length > 0 || hasSqlComponentFormatOverride(options)
|
|
174
|
+
? formatSqlComponent(query, options)
|
|
175
|
+
: parsed.sql;
|
|
139
176
|
return this.buildResult({
|
|
177
|
+
query,
|
|
140
178
|
sql,
|
|
141
179
|
applied,
|
|
142
180
|
skipped,
|
|
@@ -150,13 +188,22 @@ export class ParameterConditionPlacementOptimizer {
|
|
|
150
188
|
var _a;
|
|
151
189
|
return this.plan(input, Object.assign(Object.assign({}, options), { dryRun: (_a = options.dryRun) !== null && _a !== void 0 ? _a : false }));
|
|
152
190
|
}
|
|
153
|
-
parseInput(input) {
|
|
191
|
+
parseInput(input, options) {
|
|
154
192
|
const warnings = [];
|
|
155
193
|
const errors = [];
|
|
156
194
|
try {
|
|
157
195
|
const sourceSql = typeof input === "string"
|
|
158
196
|
? input
|
|
159
|
-
: formatSqlComponent(input);
|
|
197
|
+
: formatSqlComponent(input, options);
|
|
198
|
+
if (typeof input !== "string" && options.cloneInput === false) {
|
|
199
|
+
return {
|
|
200
|
+
query: input,
|
|
201
|
+
sql: sourceSql,
|
|
202
|
+
formatterGeneratedSource: false,
|
|
203
|
+
warnings,
|
|
204
|
+
errors
|
|
205
|
+
};
|
|
206
|
+
}
|
|
160
207
|
if (typeof input !== "string") {
|
|
161
208
|
warnings.push({
|
|
162
209
|
code: "AST_INPUT_FORMATTED",
|
|
@@ -187,7 +234,7 @@ export class ParameterConditionPlacementOptimizer {
|
|
|
187
234
|
};
|
|
188
235
|
}
|
|
189
236
|
}
|
|
190
|
-
analyzeCandidate(expression) {
|
|
237
|
+
analyzeCandidate(expression, options) {
|
|
191
238
|
const parameterNames = this.collectParameterNames(expression);
|
|
192
239
|
if (parameterNames.length === 0) {
|
|
193
240
|
return null;
|
|
@@ -196,40 +243,22 @@ export class ParameterConditionPlacementOptimizer {
|
|
|
196
243
|
if (unsupported) {
|
|
197
244
|
return unsupported;
|
|
198
245
|
}
|
|
199
|
-
const candidate = unwrapParens(expression);
|
|
200
|
-
if (!(candidate instanceof BinaryExpression)) {
|
|
201
|
-
return {
|
|
202
|
-
code: "UNSUPPORTED_PARAMETER_CONDITION",
|
|
203
|
-
reason: "Only simple binary parameter predicates are moved in the safe-only implementation."
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
const operator = candidate.operator.value.trim().toLowerCase();
|
|
207
|
-
if (!SUPPORTED_OPERATORS.has(operator)) {
|
|
208
|
-
return {
|
|
209
|
-
code: "UNSUPPORTED_OPERATOR",
|
|
210
|
-
reason: `Operator '${candidate.operator.value}' is not supported for safe-only parameter condition placement.`
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
246
|
const columnReferences = this.collectColumnReferences(expression);
|
|
214
|
-
if (columnReferences.length
|
|
247
|
+
if (columnReferences.length === 0) {
|
|
215
248
|
return {
|
|
216
249
|
code: "AMBIGUOUS_COLUMN_REFERENCE",
|
|
217
|
-
reason:
|
|
218
|
-
? "Parameter condition has no column reference to anchor the move."
|
|
219
|
-
: "Parameter condition references multiple columns; moving it may change semantics."
|
|
250
|
+
reason: "Parameter condition has no column reference to anchor the move."
|
|
220
251
|
};
|
|
221
252
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
reason: "Only simple column IN (:parameter) predicates are moved in the safe-only implementation."
|
|
226
|
-
};
|
|
253
|
+
const unsupportedShape = this.findUnsupportedParameterPredicateShape(expression);
|
|
254
|
+
if (unsupportedShape) {
|
|
255
|
+
return unsupportedShape;
|
|
227
256
|
}
|
|
228
257
|
return {
|
|
229
258
|
expression,
|
|
230
|
-
conditionSql: formatSqlComponent(expression),
|
|
259
|
+
conditionSql: formatSqlComponent(expression, options),
|
|
231
260
|
parameterNames,
|
|
232
|
-
|
|
261
|
+
references: columnReferences
|
|
233
262
|
};
|
|
234
263
|
}
|
|
235
264
|
findUnsupportedExpression(expression) {
|
|
@@ -240,13 +269,6 @@ export class ParameterConditionPlacementOptimizer {
|
|
|
240
269
|
}
|
|
241
270
|
const candidate = unwrapParens(value);
|
|
242
271
|
if (candidate instanceof BinaryExpression) {
|
|
243
|
-
if (candidate.operator.value.trim().toLowerCase() === "or") {
|
|
244
|
-
found = {
|
|
245
|
-
code: "OR_PREDICATE_UNSUPPORTED",
|
|
246
|
-
reason: "Condition contains OR predicates, which are not moved in the first safe-only implementation."
|
|
247
|
-
};
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
272
|
visit(candidate.left);
|
|
251
273
|
visit(candidate.right);
|
|
252
274
|
return;
|
|
@@ -272,13 +294,6 @@ export class ParameterConditionPlacementOptimizer {
|
|
|
272
294
|
};
|
|
273
295
|
return;
|
|
274
296
|
}
|
|
275
|
-
if (candidate instanceof BetweenExpression) {
|
|
276
|
-
found = {
|
|
277
|
-
code: "BETWEEN_PREDICATE_UNSUPPORTED",
|
|
278
|
-
reason: "BETWEEN predicates are not moved in the first safe-only implementation."
|
|
279
|
-
};
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
297
|
if (candidate instanceof UnaryExpression) {
|
|
283
298
|
visit(candidate.expression);
|
|
284
299
|
return;
|
|
@@ -310,6 +325,39 @@ export class ParameterConditionPlacementOptimizer {
|
|
|
310
325
|
visit(expression);
|
|
311
326
|
return found;
|
|
312
327
|
}
|
|
328
|
+
findUnsupportedParameterPredicateShape(expression) {
|
|
329
|
+
const visit = (value) => {
|
|
330
|
+
var _a;
|
|
331
|
+
const candidate = unwrapParens(value);
|
|
332
|
+
if (candidate instanceof BetweenExpression) {
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
if (candidate instanceof BinaryExpression) {
|
|
336
|
+
const operator = candidate.operator.value.trim().toLowerCase();
|
|
337
|
+
if (operator === "and" || operator === "or") {
|
|
338
|
+
return (_a = visit(candidate.left)) !== null && _a !== void 0 ? _a : visit(candidate.right);
|
|
339
|
+
}
|
|
340
|
+
if (!SUPPORTED_OPERATORS.has(operator)) {
|
|
341
|
+
return {
|
|
342
|
+
code: "UNSUPPORTED_OPERATOR",
|
|
343
|
+
reason: `Operator '${candidate.operator.value}' is not supported for safe-only parameter condition placement.`
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
if (operator === "in" && !this.isSupportedInPredicate(candidate)) {
|
|
347
|
+
return {
|
|
348
|
+
code: "UNSUPPORTED_IN_PREDICATE",
|
|
349
|
+
reason: "Only simple column IN (:parameter) predicates are moved in the safe-only implementation."
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
return {
|
|
355
|
+
code: "UNSUPPORTED_PARAMETER_CONDITION",
|
|
356
|
+
reason: "Only simple binary, BETWEEN, and whole OR/AND parameter predicates are moved in the safe-only implementation."
|
|
357
|
+
};
|
|
358
|
+
};
|
|
359
|
+
return visit(expression);
|
|
360
|
+
}
|
|
313
361
|
isSupportedInPredicate(expression) {
|
|
314
362
|
const left = unwrapParens(expression.left);
|
|
315
363
|
const right = unwrapParens(expression.right);
|
|
@@ -326,7 +374,7 @@ export class ParameterConditionPlacementOptimizer {
|
|
|
326
374
|
&& right.values.every(value => unwrapParens(value) instanceof ParameterExpression);
|
|
327
375
|
}
|
|
328
376
|
resolveTarget(root, candidate) {
|
|
329
|
-
const boundary = this.
|
|
377
|
+
const boundary = this.findRootQueryBoundary(root);
|
|
330
378
|
if (boundary) {
|
|
331
379
|
return boundary;
|
|
332
380
|
}
|
|
@@ -336,35 +384,38 @@ export class ParameterConditionPlacementOptimizer {
|
|
|
336
384
|
reason: "Condition has no FROM source that can receive the predicate safely."
|
|
337
385
|
};
|
|
338
386
|
}
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
387
|
+
const bindings = [];
|
|
388
|
+
for (const reference of candidate.references) {
|
|
389
|
+
const binding = this.resolveSourceBinding(root, root, reference);
|
|
390
|
+
if ("code" in binding) {
|
|
391
|
+
return binding;
|
|
392
|
+
}
|
|
393
|
+
if (!bindings.some(item => item.source === binding.source)) {
|
|
394
|
+
bindings.push(binding);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (bindings.length !== 1) {
|
|
398
|
+
return {
|
|
399
|
+
code: "MULTIPLE_SOURCE_REFERENCES",
|
|
400
|
+
reason: "Parameter condition references multiple source query blocks; moving it may change semantics."
|
|
401
|
+
};
|
|
342
402
|
}
|
|
343
|
-
const
|
|
403
|
+
const binding = bindings[0];
|
|
404
|
+
const nullableSide = this.findNullableSideBoundary(root.fromClause, binding);
|
|
344
405
|
if (nullableSide) {
|
|
345
406
|
return nullableSide;
|
|
346
407
|
}
|
|
347
|
-
const upstream = this.resolveUpstreamQuery(root,
|
|
408
|
+
const upstream = this.resolveUpstreamQuery(root, binding, candidate.references);
|
|
348
409
|
if ("code" in upstream) {
|
|
349
410
|
return upstream;
|
|
350
411
|
}
|
|
351
|
-
const targetBoundary = this.findTargetQueryBoundary(upstream.query);
|
|
352
|
-
if (targetBoundary) {
|
|
353
|
-
return targetBoundary;
|
|
354
|
-
}
|
|
355
412
|
return upstream;
|
|
356
413
|
}
|
|
357
|
-
|
|
358
|
-
if (query
|
|
414
|
+
findRootQueryBoundary(query) {
|
|
415
|
+
if (this.hasDistinctOnBoundary(query)) {
|
|
359
416
|
return {
|
|
360
417
|
code: "DISTINCT_BOUNDARY",
|
|
361
|
-
reason: "Condition crosses DISTINCT boundary; moving it may change semantics."
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
if (query.groupByClause || query.havingClause) {
|
|
365
|
-
return {
|
|
366
|
-
code: "GROUP_BY_BOUNDARY",
|
|
367
|
-
reason: "Condition crosses GROUP BY boundary; moving it may change semantics."
|
|
418
|
+
reason: "Condition crosses DISTINCT ON boundary; moving it may change semantics."
|
|
368
419
|
};
|
|
369
420
|
}
|
|
370
421
|
if (this.hasWindowUsage(query)) {
|
|
@@ -375,10 +426,19 @@ export class ParameterConditionPlacementOptimizer {
|
|
|
375
426
|
}
|
|
376
427
|
return null;
|
|
377
428
|
}
|
|
378
|
-
|
|
379
|
-
const
|
|
380
|
-
if (
|
|
381
|
-
return
|
|
429
|
+
resolveTargetPlacement(query, targetColumns) {
|
|
430
|
+
const hasOrdinaryDistinct = this.hasOrdinaryDistinct(query);
|
|
431
|
+
if (this.hasDistinctOnBoundary(query)) {
|
|
432
|
+
return {
|
|
433
|
+
code: "DISTINCT_BOUNDARY",
|
|
434
|
+
reason: "Condition crosses DISTINCT ON boundary; moving it may change semantics."
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
if (this.hasWindowUsage(query)) {
|
|
438
|
+
return {
|
|
439
|
+
code: "WINDOW_BOUNDARY",
|
|
440
|
+
reason: "Condition crosses WINDOW boundary; moving it may change semantics."
|
|
441
|
+
};
|
|
382
442
|
}
|
|
383
443
|
if (query.limitClause || query.offsetClause || query.fetchClause) {
|
|
384
444
|
return {
|
|
@@ -392,10 +452,35 @@ export class ParameterConditionPlacementOptimizer {
|
|
|
392
452
|
reason: "Target query contains an OUTER JOIN boundary that is not moved across in the safe-only implementation."
|
|
393
453
|
};
|
|
394
454
|
}
|
|
395
|
-
|
|
455
|
+
if (query.groupByClause) {
|
|
456
|
+
const allReferencesAreGroupKeys = targetColumns.every(item => this.isGroupKeyColumn(query, item.targetColumn));
|
|
457
|
+
if (!allReferencesAreGroupKeys) {
|
|
458
|
+
return {
|
|
459
|
+
code: "GROUP_BY_BOUNDARY",
|
|
460
|
+
reason: "Condition references a target column that is not proven to be a GROUP BY key."
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
return {
|
|
464
|
+
reason: "Condition references only GROUP BY keys; it is moved into pre-aggregation WHERE."
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
if (query.havingClause) {
|
|
468
|
+
return {
|
|
469
|
+
code: "GROUP_BY_BOUNDARY",
|
|
470
|
+
reason: "Condition crosses HAVING aggregation boundary; moving it may change semantics."
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
if (hasOrdinaryDistinct) {
|
|
474
|
+
return {
|
|
475
|
+
reason: "Condition references a direct ordinary DISTINCT output column; it is moved into the DISTINCT input WHERE."
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
return {
|
|
479
|
+
reason: "All referenced columns resolve to a single direct upstream output before unsafe query boundaries."
|
|
480
|
+
};
|
|
396
481
|
}
|
|
397
|
-
resolveSourceBinding(
|
|
398
|
-
const fromClause =
|
|
482
|
+
resolveSourceBinding(contextRoot, query, column) {
|
|
483
|
+
const fromClause = query.fromClause;
|
|
399
484
|
if (!fromClause) {
|
|
400
485
|
return {
|
|
401
486
|
code: "NO_FROM_CLAUSE",
|
|
@@ -413,10 +498,7 @@ export class ParameterConditionPlacementOptimizer {
|
|
|
413
498
|
reason: `Column source alias '${column.getNamespace()}' is not uniquely resolvable.`
|
|
414
499
|
};
|
|
415
500
|
}
|
|
416
|
-
|
|
417
|
-
return matches[0];
|
|
418
|
-
}
|
|
419
|
-
const matchCount = this.getOutputColumnMatchCount(root, matches[0], columnName);
|
|
501
|
+
const matchCount = this.getOutputColumnMatchCount(contextRoot, matches[0], columnName);
|
|
420
502
|
if (matchCount === 0) {
|
|
421
503
|
return {
|
|
422
504
|
code: "COLUMN_NOT_AVAILABLE_UPSTREAM",
|
|
@@ -432,13 +514,8 @@ export class ParameterConditionPlacementOptimizer {
|
|
|
432
514
|
return matches[0];
|
|
433
515
|
}
|
|
434
516
|
const matches = [];
|
|
435
|
-
let unionSource = null;
|
|
436
517
|
for (const binding of bindings) {
|
|
437
|
-
|
|
438
|
-
unionSource !== null && unionSource !== void 0 ? unionSource : (unionSource = binding);
|
|
439
|
-
continue;
|
|
440
|
-
}
|
|
441
|
-
const matchCount = this.getOutputColumnMatchCount(root, binding, columnName);
|
|
518
|
+
const matchCount = this.getOutputColumnMatchCount(contextRoot, binding, columnName);
|
|
442
519
|
if (matchCount > 1) {
|
|
443
520
|
return {
|
|
444
521
|
code: "AMBIGUOUS_COLUMN_REFERENCE",
|
|
@@ -449,12 +526,6 @@ export class ParameterConditionPlacementOptimizer {
|
|
|
449
526
|
matches.push(binding);
|
|
450
527
|
}
|
|
451
528
|
}
|
|
452
|
-
if (matches.length === 0 && unionSource) {
|
|
453
|
-
return {
|
|
454
|
-
code: "UNION_BOUNDARY",
|
|
455
|
-
reason: "Condition would need distribution into a UNION branch, which is unsupported."
|
|
456
|
-
};
|
|
457
|
-
}
|
|
458
529
|
if (matches.length !== 1) {
|
|
459
530
|
return {
|
|
460
531
|
code: "AMBIGUOUS_COLUMN_REFERENCE",
|
|
@@ -465,17 +536,20 @@ export class ParameterConditionPlacementOptimizer {
|
|
|
465
536
|
}
|
|
466
537
|
return matches[0];
|
|
467
538
|
}
|
|
468
|
-
resolveUpstreamQuery(root, binding,
|
|
539
|
+
resolveUpstreamQuery(root, binding, references) {
|
|
469
540
|
const source = binding.source.datasource;
|
|
470
541
|
if (source instanceof SubQuerySource) {
|
|
471
542
|
if (source.query instanceof SimpleSelectQuery) {
|
|
472
543
|
return {
|
|
544
|
+
kind: "simple",
|
|
473
545
|
query: source.query,
|
|
474
546
|
scopeId: `subquery:${binding.alias}`,
|
|
475
|
-
outputColumnName,
|
|
476
547
|
sourceBinding: binding
|
|
477
548
|
};
|
|
478
549
|
}
|
|
550
|
+
if (source.query instanceof BinarySelectQuery) {
|
|
551
|
+
return this.resolveUnionTarget(root, source.query, `subquery:${binding.alias}`, references);
|
|
552
|
+
}
|
|
479
553
|
return {
|
|
480
554
|
code: "UNION_BOUNDARY",
|
|
481
555
|
reason: "Condition would need distribution into a UNION or non-simple subquery, which is unsupported."
|
|
@@ -503,10 +577,7 @@ export class ParameterConditionPlacementOptimizer {
|
|
|
503
577
|
};
|
|
504
578
|
}
|
|
505
579
|
if (commonTable.query instanceof BinarySelectQuery) {
|
|
506
|
-
return {
|
|
507
|
-
code: "UNION_BOUNDARY",
|
|
508
|
-
reason: "Condition would need distribution into a UNION branch, which is unsupported."
|
|
509
|
-
};
|
|
580
|
+
return this.resolveUnionTarget(root, commonTable.query, `cte:${commonTable.getSourceAliasName()}`, references);
|
|
510
581
|
}
|
|
511
582
|
if (!(commonTable.query instanceof SimpleSelectQuery)) {
|
|
512
583
|
return {
|
|
@@ -515,35 +586,140 @@ export class ParameterConditionPlacementOptimizer {
|
|
|
515
586
|
};
|
|
516
587
|
}
|
|
517
588
|
return {
|
|
589
|
+
kind: "simple",
|
|
518
590
|
query: commonTable.query,
|
|
519
591
|
scopeId: `cte:${commonTable.getSourceAliasName()}`,
|
|
520
|
-
outputColumnName,
|
|
521
592
|
sourceBinding: binding,
|
|
522
593
|
cteName: commonTable.getSourceAliasName()
|
|
523
594
|
};
|
|
524
595
|
}
|
|
525
|
-
|
|
526
|
-
const
|
|
527
|
-
if (
|
|
596
|
+
resolveUnionTarget(root, query, scopeId, references) {
|
|
597
|
+
const branches = this.collectUnionBranches(query);
|
|
598
|
+
if ("code" in branches) {
|
|
599
|
+
return branches;
|
|
600
|
+
}
|
|
601
|
+
const outputIndexes = new Map();
|
|
602
|
+
for (const reference of references) {
|
|
603
|
+
const outputIndex = this.resolveUnionOutputIndex(root, branches[0], reference.column.name);
|
|
604
|
+
if ("code" in outputIndex) {
|
|
605
|
+
return outputIndex;
|
|
606
|
+
}
|
|
607
|
+
outputIndexes.set(reference, outputIndex.index);
|
|
608
|
+
}
|
|
609
|
+
const branchTargets = [];
|
|
610
|
+
for (const branch of branches) {
|
|
611
|
+
const targetColumns = [];
|
|
612
|
+
for (const [reference, outputIndex] of outputIndexes.entries()) {
|
|
613
|
+
const targetColumn = this.resolveTargetColumnByOutputIndex(root, branch, outputIndex, reference);
|
|
614
|
+
if ("code" in targetColumn) {
|
|
615
|
+
return targetColumn;
|
|
616
|
+
}
|
|
617
|
+
targetColumns.push(targetColumn);
|
|
618
|
+
}
|
|
619
|
+
branchTargets.push(this.resolveDeepestBranchTarget(root, branch, targetColumns));
|
|
620
|
+
}
|
|
621
|
+
return {
|
|
622
|
+
kind: "union",
|
|
623
|
+
scopeId,
|
|
624
|
+
branches: branchTargets
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
resolveTargetColumns(root, query, references) {
|
|
628
|
+
const resolved = [];
|
|
629
|
+
for (const reference of references) {
|
|
630
|
+
const matches = this.collectSelectOutputs(root, query).filter(item => normalizeIdentifier(item.name) === normalizeIdentifier(reference.column.name));
|
|
631
|
+
if (matches.length !== 1) {
|
|
632
|
+
return {
|
|
633
|
+
code: "AMBIGUOUS_TARGET_COLUMN",
|
|
634
|
+
reason: matches.length === 0
|
|
635
|
+
? `Target query does not expose '${reference.column.name}' as a direct output column.`
|
|
636
|
+
: `Target query exposes multiple '${reference.column.name}' columns.`
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
const value = matches[0].value;
|
|
640
|
+
if (!(value instanceof ColumnReference)) {
|
|
641
|
+
return {
|
|
642
|
+
code: "EXPRESSION_OUTPUT_UNSUPPORTED",
|
|
643
|
+
reason: `Target output '${reference.column.name}' is an expression, not a direct column reference.`
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
const sourceResolution = this.verifyColumnResolvableInQuery(query, value);
|
|
647
|
+
if (sourceResolution) {
|
|
648
|
+
return sourceResolution;
|
|
649
|
+
}
|
|
650
|
+
resolved.push({
|
|
651
|
+
sourceColumn: reference,
|
|
652
|
+
targetColumn: value
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
return resolved;
|
|
656
|
+
}
|
|
657
|
+
resolveTargetColumnByOutputIndex(root, query, outputIndex, sourceColumn) {
|
|
658
|
+
const output = this.collectSelectOutputs(root, query)[outputIndex];
|
|
659
|
+
if (!output) {
|
|
528
660
|
return {
|
|
529
|
-
code: "
|
|
530
|
-
reason:
|
|
531
|
-
? `Target query does not expose '${outputColumnName}' as a direct output column.`
|
|
532
|
-
: `Target query exposes multiple '${outputColumnName}' columns.`
|
|
661
|
+
code: "UNION_COLUMN_MISMATCH",
|
|
662
|
+
reason: `UNION branch does not expose column '${sourceColumn.column.name}' at output position ${outputIndex + 1}.`
|
|
533
663
|
};
|
|
534
664
|
}
|
|
535
|
-
|
|
536
|
-
if (!(value instanceof ColumnReference)) {
|
|
665
|
+
if (!(output.value instanceof ColumnReference)) {
|
|
537
666
|
return {
|
|
538
667
|
code: "EXPRESSION_OUTPUT_UNSUPPORTED",
|
|
539
|
-
reason: `
|
|
668
|
+
reason: `UNION branch output at position ${outputIndex + 1} for '${sourceColumn.column.name}' is an expression, not a direct column reference.`
|
|
540
669
|
};
|
|
541
670
|
}
|
|
542
|
-
const sourceResolution = this.verifyColumnResolvableInQuery(query, value);
|
|
671
|
+
const sourceResolution = this.verifyColumnResolvableInQuery(query, output.value);
|
|
543
672
|
if (sourceResolution) {
|
|
544
673
|
return sourceResolution;
|
|
545
674
|
}
|
|
546
|
-
return {
|
|
675
|
+
return {
|
|
676
|
+
sourceColumn,
|
|
677
|
+
targetColumn: output.value
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
resolveDeepestBranchTarget(contextRoot, query, targetColumns, visited = new Set()) {
|
|
681
|
+
if (visited.has(query) || targetColumns.length === 0) {
|
|
682
|
+
return { query, targetColumns: [...targetColumns] };
|
|
683
|
+
}
|
|
684
|
+
const nextVisited = new Set(visited);
|
|
685
|
+
nextVisited.add(query);
|
|
686
|
+
const bindings = [];
|
|
687
|
+
for (const item of targetColumns) {
|
|
688
|
+
const binding = this.resolveSourceBinding(contextRoot, query, item.targetColumn);
|
|
689
|
+
if ("code" in binding) {
|
|
690
|
+
return { query, targetColumns: [...targetColumns] };
|
|
691
|
+
}
|
|
692
|
+
if (!bindings.some(existing => existing.source === binding.source)) {
|
|
693
|
+
bindings.push(binding);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
if (bindings.length !== 1) {
|
|
697
|
+
return { query, targetColumns: [...targetColumns] };
|
|
698
|
+
}
|
|
699
|
+
const binding = bindings[0];
|
|
700
|
+
const nullableSide = query.fromClause
|
|
701
|
+
? this.findNullableSideBoundary(query.fromClause, binding)
|
|
702
|
+
: null;
|
|
703
|
+
if (nullableSide) {
|
|
704
|
+
return { query, targetColumns: [...targetColumns] };
|
|
705
|
+
}
|
|
706
|
+
const upstream = this.resolveUpstreamQuery(contextRoot, binding, targetColumns.map(item => item.targetColumn));
|
|
707
|
+
if ("code" in upstream || upstream.kind === "union") {
|
|
708
|
+
return { query, targetColumns: [...targetColumns] };
|
|
709
|
+
}
|
|
710
|
+
const upstreamColumns = this.resolveTargetColumns(contextRoot, upstream.query, targetColumns.map(item => item.targetColumn));
|
|
711
|
+
if ("code" in upstreamColumns) {
|
|
712
|
+
return { query, targetColumns: [...targetColumns] };
|
|
713
|
+
}
|
|
714
|
+
const placement = this.resolveTargetPlacement(upstream.query, upstreamColumns);
|
|
715
|
+
if ("code" in placement) {
|
|
716
|
+
return { query, targetColumns: [...targetColumns] };
|
|
717
|
+
}
|
|
718
|
+
const rebasedColumns = upstreamColumns.map((item, index) => ({
|
|
719
|
+
sourceColumn: targetColumns[index].sourceColumn,
|
|
720
|
+
targetColumn: item.targetColumn
|
|
721
|
+
}));
|
|
722
|
+
return this.resolveDeepestBranchTarget(contextRoot, upstream.query, rebasedColumns, nextVisited);
|
|
547
723
|
}
|
|
548
724
|
verifyColumnResolvableInQuery(query, column) {
|
|
549
725
|
if (!query.fromClause) {
|
|
@@ -571,13 +747,47 @@ export class ParameterConditionPlacementOptimizer {
|
|
|
571
747
|
reason: `Unqualified target column '${column.column.name}' is ambiguous in a multi-source destination query.`
|
|
572
748
|
};
|
|
573
749
|
}
|
|
574
|
-
|
|
575
|
-
const
|
|
750
|
+
isGroupKeyColumn(query, column) {
|
|
751
|
+
const groupBy = query.groupByClause;
|
|
752
|
+
if (!groupBy || groupBy.mode || groupBy.grouping.length === 0) {
|
|
753
|
+
return false;
|
|
754
|
+
}
|
|
755
|
+
return groupBy.grouping.some(grouping => {
|
|
756
|
+
const candidate = unwrapParens(grouping);
|
|
757
|
+
return candidate instanceof ColumnReference
|
|
758
|
+
&& this.sameResolvableColumnInQuery(query, candidate, column);
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
sameResolvableColumnInQuery(query, left, right) {
|
|
762
|
+
if (sameColumnReference(left, right)) {
|
|
763
|
+
return true;
|
|
764
|
+
}
|
|
765
|
+
if (normalizeIdentifier(left.column.name) !== normalizeIdentifier(right.column.name)) {
|
|
766
|
+
return false;
|
|
767
|
+
}
|
|
768
|
+
const leftSource = this.resolveColumnSourceAlias(query, left);
|
|
769
|
+
const rightSource = this.resolveColumnSourceAlias(query, right);
|
|
770
|
+
return leftSource !== null && leftSource === rightSource;
|
|
771
|
+
}
|
|
772
|
+
resolveColumnSourceAlias(query, column) {
|
|
773
|
+
if (!query.fromClause) {
|
|
774
|
+
return null;
|
|
775
|
+
}
|
|
776
|
+
const bindings = this.getSourceBindings(query.fromClause);
|
|
777
|
+
const namespace = normalizeIdentifier(column.getNamespace());
|
|
778
|
+
if (namespace) {
|
|
779
|
+
const matches = bindings.filter(binding => normalizeIdentifier(binding.alias) === namespace);
|
|
780
|
+
return matches.length === 1 ? normalizeIdentifier(matches[0].alias) : null;
|
|
781
|
+
}
|
|
782
|
+
return bindings.length === 1 ? normalizeIdentifier(bindings[0].alias) : null;
|
|
783
|
+
}
|
|
784
|
+
rebaseCondition(expression, targetColumns, options) {
|
|
785
|
+
const cloned = cloneValueComponent(expression, options);
|
|
576
786
|
for (const reference of this.collectColumnReferences(cloned)) {
|
|
577
|
-
|
|
578
|
-
|
|
787
|
+
const target = targetColumns.find(item => sameColumnReference(reference, item.sourceColumn));
|
|
788
|
+
if (target) {
|
|
789
|
+
reference.qualifiedName = cloneColumnReference(target.targetColumn).qualifiedName;
|
|
579
790
|
}
|
|
580
|
-
reference.qualifiedName = cloneColumnReference(targetColumn).qualifiedName;
|
|
581
791
|
}
|
|
582
792
|
return cloned;
|
|
583
793
|
}
|
|
@@ -635,12 +845,80 @@ export class ParameterConditionPlacementOptimizer {
|
|
|
635
845
|
return joinType.includes("left") || joinType.includes("right") || joinType.includes("full") || joinType.includes("outer");
|
|
636
846
|
});
|
|
637
847
|
}
|
|
848
|
+
hasDistinctOnBoundary(query) {
|
|
849
|
+
return query.selectClause.distinct instanceof DistinctOn;
|
|
850
|
+
}
|
|
851
|
+
hasOrdinaryDistinct(query) {
|
|
852
|
+
return query.selectClause.distinct !== null && !this.hasDistinctOnBoundary(query);
|
|
853
|
+
}
|
|
638
854
|
getOutputColumnMatchCount(root, binding, columnName) {
|
|
639
855
|
const target = this.resolveSourceQueryForColumns(root, binding.source);
|
|
640
|
-
if (!target
|
|
856
|
+
if (!target) {
|
|
641
857
|
return 0;
|
|
642
858
|
}
|
|
643
|
-
|
|
859
|
+
if (target instanceof BinarySelectQuery) {
|
|
860
|
+
const branches = this.collectUnionBranches(target);
|
|
861
|
+
if ("code" in branches) {
|
|
862
|
+
return 0;
|
|
863
|
+
}
|
|
864
|
+
return this.collectSelectOutputs(root, branches[0]).filter(item => normalizeIdentifier(item.name) === normalizeIdentifier(columnName)).length;
|
|
865
|
+
}
|
|
866
|
+
if (!(target instanceof SimpleSelectQuery)) {
|
|
867
|
+
return 0;
|
|
868
|
+
}
|
|
869
|
+
return this.collectSelectOutputs(root, target).filter(item => normalizeIdentifier(item.name) === normalizeIdentifier(columnName)).length;
|
|
870
|
+
}
|
|
871
|
+
collectUnionBranches(query) {
|
|
872
|
+
const branches = [];
|
|
873
|
+
const visit = (select) => {
|
|
874
|
+
var _a;
|
|
875
|
+
if (select instanceof SimpleSelectQuery) {
|
|
876
|
+
branches.push(select);
|
|
877
|
+
return null;
|
|
878
|
+
}
|
|
879
|
+
if (!(select instanceof BinarySelectQuery)) {
|
|
880
|
+
return {
|
|
881
|
+
code: "UNION_BOUNDARY",
|
|
882
|
+
reason: "Condition would need distribution into a non-simple UNION branch, which is unsupported."
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
const operator = select.operator.value.trim().toLowerCase();
|
|
886
|
+
if (operator !== "union" && operator !== "union all") {
|
|
887
|
+
return {
|
|
888
|
+
code: "UNION_BOUNDARY",
|
|
889
|
+
reason: `Condition would need distribution through '${select.operator.value}', which is unsupported.`
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
return (_a = visit(select.left)) !== null && _a !== void 0 ? _a : visit(select.right);
|
|
893
|
+
};
|
|
894
|
+
const skip = visit(query);
|
|
895
|
+
return skip !== null && skip !== void 0 ? skip : branches;
|
|
896
|
+
}
|
|
897
|
+
resolveUnionOutputIndex(root, firstBranch, outputColumnName) {
|
|
898
|
+
const matches = [];
|
|
899
|
+
this.collectSelectOutputs(root, firstBranch).forEach((item, index) => {
|
|
900
|
+
if (normalizeIdentifier(item.name) === normalizeIdentifier(outputColumnName)) {
|
|
901
|
+
matches.push(index);
|
|
902
|
+
}
|
|
903
|
+
});
|
|
904
|
+
if (matches.length !== 1) {
|
|
905
|
+
return {
|
|
906
|
+
code: "AMBIGUOUS_TARGET_COLUMN",
|
|
907
|
+
reason: matches.length === 0
|
|
908
|
+
? `UNION output does not expose '${outputColumnName}' from its first branch.`
|
|
909
|
+
: `UNION first branch exposes multiple '${outputColumnName}' columns.`
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
return { index: matches[0] };
|
|
913
|
+
}
|
|
914
|
+
collectSelectOutputs(root, query) {
|
|
915
|
+
var _a, _b, _c, _d;
|
|
916
|
+
const commonTables = [
|
|
917
|
+
...((_b = (_a = query.withClause) === null || _a === void 0 ? void 0 : _a.tables) !== null && _b !== void 0 ? _b : []),
|
|
918
|
+
...((_d = (_c = root.withClause) === null || _c === void 0 ? void 0 : _c.tables) !== null && _d !== void 0 ? _d : [])
|
|
919
|
+
];
|
|
920
|
+
const collector = new SelectOutputCollector(null, commonTables.length > 0 ? commonTables : null);
|
|
921
|
+
return collector.collect(query);
|
|
644
922
|
}
|
|
645
923
|
resolveSourceQueryForColumns(root, source) {
|
|
646
924
|
var _a;
|
|
@@ -655,15 +933,6 @@ export class ParameterConditionPlacementOptimizer {
|
|
|
655
933
|
}
|
|
656
934
|
return null;
|
|
657
935
|
}
|
|
658
|
-
getSelectItemOutputName(item) {
|
|
659
|
-
if (item.identifier) {
|
|
660
|
-
return item.identifier.name;
|
|
661
|
-
}
|
|
662
|
-
if (item.value instanceof ColumnReference) {
|
|
663
|
-
return item.value.column.name;
|
|
664
|
-
}
|
|
665
|
-
return null;
|
|
666
|
-
}
|
|
667
936
|
findCte(root, name) {
|
|
668
937
|
var _a, _b;
|
|
669
938
|
const normalized = normalizeIdentifier(name);
|
|
@@ -909,13 +1178,14 @@ export class ParameterConditionPlacementOptimizer {
|
|
|
909
1178
|
visit(expression);
|
|
910
1179
|
return names;
|
|
911
1180
|
}
|
|
912
|
-
makeSkipped(expression, draft) {
|
|
913
|
-
return Object.assign({ conditionSql: formatSqlComponent(expression), scopeId: "scope:root" }, draft);
|
|
1181
|
+
makeSkipped(expression, draft, options) {
|
|
1182
|
+
return Object.assign({ conditionSql: formatSqlComponent(expression, options), scopeId: "scope:root" }, draft);
|
|
914
1183
|
}
|
|
915
1184
|
buildResult(params) {
|
|
916
1185
|
return {
|
|
917
1186
|
ok: params.errors.length === 0,
|
|
918
1187
|
sql: params.sql,
|
|
1188
|
+
query: params.query,
|
|
919
1189
|
applied: params.applied,
|
|
920
1190
|
skipped: params.skipped,
|
|
921
1191
|
warnings: params.warnings,
|