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 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
- const targetColumns = this.resolveTargetColumns(target.query, candidate.references);
119
- if ("code" in targetColumns) {
120
- skipped.push(this.makeSkipped(term, targetColumns));
121
- continue;
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: "All outer references resolve to direct upstream outputs before unsafe query boundaries.",
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 ? formatSqlComponent(query) : parsed.sql;
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.findCurrentQueryBoundary(root);
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
- findCurrentQueryBoundary(query) {
390
- if (query.selectClause.distinct) {
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
- findTargetQueryBoundary(query) {
411
- const commonBoundary = this.findCurrentQueryBoundary(query);
412
- if (commonBoundary) {
413
- return commonBoundary;
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
- return null;
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(root, column) {
430
- const fromClause = root.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
- if (this.resolveSourceQueryForColumns(root, matches[0].source) instanceof BinarySelectQuery) {
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
- if (this.resolveSourceQueryForColumns(root, binding.source) instanceof BinarySelectQuery) {
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
- resolveTargetColumns(query, references) {
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.selectClause.items.filter(item => { var _a; return normalizeIdentifier((_a = this.getSelectItemOutputName(item)) !== null && _a !== void 0 ? _a : "") === normalizeIdentifier(reference.column.name); });
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
- rebasePredicate(expression, targetColumns) {
612
- const cloned = cloneValueComponent(expression);
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 || !(target instanceof SimpleSelectQuery)) {
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.selectClause.items.filter(item => { var _a; return normalizeIdentifier((_a = this.getSelectItemOutputName(item)) !== null && _a !== void 0 ? _a : "") === normalizeIdentifier(columnName); }).length;
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,