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