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