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.
Files changed (34) hide show
  1. package/dist/esm/index.d.ts +1 -0
  2. package/dist/esm/index.js.map +1 -1
  3. package/dist/esm/index.min.js +1 -1
  4. package/dist/esm/index.min.js.map +3 -3
  5. package/dist/esm/transformers/ConditionOptimization.d.ts +1 -0
  6. package/dist/esm/transformers/ConditionOptimization.js +97 -57
  7. package/dist/esm/transformers/ConditionOptimization.js.map +1 -1
  8. package/dist/esm/transformers/ParameterConditionPlacementOptimizer.d.ts +26 -7
  9. package/dist/esm/transformers/ParameterConditionPlacementOptimizer.js +408 -138
  10. package/dist/esm/transformers/ParameterConditionPlacementOptimizer.js.map +1 -1
  11. package/dist/esm/transformers/SqlComponentFormatter.d.ts +9 -1
  12. package/dist/esm/transformers/SqlComponentFormatter.js +10 -1
  13. package/dist/esm/transformers/SqlComponentFormatter.js.map +1 -1
  14. package/dist/esm/transformers/StaticPredicatePlacementOptimizer.d.ts +22 -4
  15. package/dist/esm/transformers/StaticPredicatePlacementOptimizer.js +332 -95
  16. package/dist/esm/transformers/StaticPredicatePlacementOptimizer.js.map +1 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/index.min.js +1 -1
  19. package/dist/index.min.js.map +3 -3
  20. package/dist/src/index.d.ts +1 -0
  21. package/dist/src/transformers/ConditionOptimization.d.ts +1 -0
  22. package/dist/src/transformers/ParameterConditionPlacementOptimizer.d.ts +26 -7
  23. package/dist/src/transformers/SqlComponentFormatter.d.ts +9 -1
  24. package/dist/src/transformers/StaticPredicatePlacementOptimizer.d.ts +22 -4
  25. package/dist/transformers/ConditionOptimization.js +107 -54
  26. package/dist/transformers/ConditionOptimization.js.map +1 -1
  27. package/dist/transformers/ParameterConditionPlacementOptimizer.js +406 -136
  28. package/dist/transformers/ParameterConditionPlacementOptimizer.js.map +1 -1
  29. package/dist/transformers/SqlComponentFormatter.js +12 -2
  30. package/dist/transformers/SqlComponentFormatter.js.map +1 -1
  31. package/dist/transformers/StaticPredicatePlacementOptimizer.js +330 -93
  32. package/dist/transformers/StaticPredicatePlacementOptimizer.js.map +1 -1
  33. package/dist/tsconfig.browser.tsbuildinfo +1 -1
  34. 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
- const targetColumn = this.resolveTargetColumn(target.query, target.outputColumnName);
120
- if ("code" in targetColumn) {
121
- skipped.push(this.makeSkipped(term, targetColumn));
122
- continue;
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: "All referenced columns resolve to a single direct upstream output before unsafe query boundaries.",
167
+ reason: appliedReason,
133
168
  parameterNames: candidate.parameterNames,
134
- columnReferences: [columnReferenceText(candidate.column)]
169
+ columnReferences: candidate.references.map(columnReferenceText)
135
170
  });
136
171
  }
137
172
  rebuildWhereWithoutTerms(query, new Set(movedTerms));
138
- const sql = applied.length > 0 ? formatSqlComponent(query) : parsed.sql;
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 !== 1) {
247
+ if (columnReferences.length === 0) {
215
248
  return {
216
249
  code: "AMBIGUOUS_COLUMN_REFERENCE",
217
- reason: columnReferences.length === 0
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
- if (operator === "in" && !this.isSupportedInPredicate(candidate)) {
223
- return {
224
- code: "UNSUPPORTED_IN_PREDICATE",
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
- column: columnReferences[0]
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.findCurrentQueryBoundary(root);
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 bindingResult = this.resolveSourceBinding(root, candidate.column);
340
- if ("code" in bindingResult) {
341
- return bindingResult;
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 nullableSide = this.findNullableSideBoundary(root.fromClause, bindingResult);
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, bindingResult, candidate.column.column.name);
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
- findCurrentQueryBoundary(query) {
358
- if (query.selectClause.distinct) {
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
- findTargetQueryBoundary(query) {
379
- const commonBoundary = this.findCurrentQueryBoundary(query);
380
- if (commonBoundary) {
381
- return commonBoundary;
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
- return null;
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(root, column) {
398
- const fromClause = root.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
- if (this.resolveSourceQueryForColumns(root, matches[0].source) instanceof BinarySelectQuery) {
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
- if (this.resolveSourceQueryForColumns(root, binding.source) instanceof BinarySelectQuery) {
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, outputColumnName) {
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
- resolveTargetColumn(query, outputColumnName) {
526
- const matches = query.selectClause.items.filter(item => { var _a; return normalizeIdentifier((_a = this.getSelectItemOutputName(item)) !== null && _a !== void 0 ? _a : "") === normalizeIdentifier(outputColumnName); });
527
- if (matches.length !== 1) {
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: "AMBIGUOUS_TARGET_COLUMN",
530
- reason: matches.length === 0
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
- const value = matches[0].value;
536
- if (!(value instanceof ColumnReference)) {
665
+ if (!(output.value instanceof ColumnReference)) {
537
666
  return {
538
667
  code: "EXPRESSION_OUTPUT_UNSUPPORTED",
539
- reason: `Target output '${outputColumnName}' is an expression, not a direct column reference.`
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 { column: value };
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
- rebaseCondition(expression, sourceColumn, targetColumn) {
575
- const cloned = cloneValueComponent(expression);
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
- if (!sameColumnReference(reference, sourceColumn)) {
578
- continue;
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 || !(target instanceof SimpleSelectQuery)) {
856
+ if (!target) {
641
857
  return 0;
642
858
  }
643
- return target.selectClause.items.filter(item => { var _a; return normalizeIdentifier((_a = this.getSelectItemOutputName(item)) !== null && _a !== void 0 ? _a : "") === normalizeIdentifier(columnName); }).length;
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,