rawsql-ts 0.26.0 → 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 (42) hide show
  1. package/dist/esm/index.d.ts +2 -0
  2. package/dist/esm/index.js +1 -0
  3. package/dist/esm/index.js.map +1 -1
  4. package/dist/esm/index.min.js +2 -2
  5. package/dist/esm/index.min.js.map +4 -4
  6. package/dist/esm/transformers/ConditionOptimization.d.ts +11 -1
  7. package/dist/esm/transformers/ConditionOptimization.js +122 -66
  8. package/dist/esm/transformers/ConditionOptimization.js.map +1 -1
  9. package/dist/esm/transformers/ParameterConditionPlacementOptimizer.d.ts +26 -7
  10. package/dist/esm/transformers/ParameterConditionPlacementOptimizer.js +408 -141
  11. package/dist/esm/transformers/ParameterConditionPlacementOptimizer.js.map +1 -1
  12. package/dist/esm/transformers/SSSQLFilterBuilder.d.ts +6 -0
  13. package/dist/esm/transformers/SSSQLFilterBuilder.js +84 -8
  14. package/dist/esm/transformers/SSSQLFilterBuilder.js.map +1 -1
  15. package/dist/esm/transformers/SqlComponentFormatter.d.ts +11 -0
  16. package/dist/esm/transformers/SqlComponentFormatter.js +16 -0
  17. package/dist/esm/transformers/SqlComponentFormatter.js.map +1 -0
  18. package/dist/esm/transformers/StaticPredicatePlacementOptimizer.d.ts +103 -0
  19. package/dist/esm/transformers/StaticPredicatePlacementOptimizer.js +1435 -0
  20. package/dist/esm/transformers/StaticPredicatePlacementOptimizer.js.map +1 -0
  21. package/dist/index.js +1 -0
  22. package/dist/index.js.map +1 -1
  23. package/dist/index.min.js +2 -2
  24. package/dist/index.min.js.map +4 -4
  25. package/dist/src/index.d.ts +2 -0
  26. package/dist/src/transformers/ConditionOptimization.d.ts +11 -1
  27. package/dist/src/transformers/ParameterConditionPlacementOptimizer.d.ts +26 -7
  28. package/dist/src/transformers/SSSQLFilterBuilder.d.ts +6 -0
  29. package/dist/src/transformers/SqlComponentFormatter.d.ts +11 -0
  30. package/dist/src/transformers/StaticPredicatePlacementOptimizer.d.ts +103 -0
  31. package/dist/transformers/ConditionOptimization.js +146 -65
  32. package/dist/transformers/ConditionOptimization.js.map +1 -1
  33. package/dist/transformers/ParameterConditionPlacementOptimizer.js +407 -140
  34. package/dist/transformers/ParameterConditionPlacementOptimizer.js.map +1 -1
  35. package/dist/transformers/SSSQLFilterBuilder.js +92 -16
  36. package/dist/transformers/SSSQLFilterBuilder.js.map +1 -1
  37. package/dist/transformers/SqlComponentFormatter.js +21 -0
  38. package/dist/transformers/SqlComponentFormatter.js.map +1 -0
  39. package/dist/transformers/StaticPredicatePlacementOptimizer.js +1445 -0
  40. package/dist/transformers/StaticPredicatePlacementOptimizer.js.map +1 -0
  41. package/dist/tsconfig.browser.tsbuildinfo +1 -1
  42. package/package.json +1 -1
@@ -6,13 +6,11 @@ const SelectQuery_1 = require("../models/SelectQuery");
6
6
  const ValueComponent_1 = require("../models/ValueComponent");
7
7
  const SelectQueryParser_1 = require("../parsers/SelectQueryParser");
8
8
  const ValueParser_1 = require("../parsers/ValueParser");
9
- const SqlFormatter_1 = require("./SqlFormatter");
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();
13
- const formatSqlComponent = (component) => {
14
- return new SqlFormatter_1.SqlFormatter().format(component).formattedSql;
15
- };
16
14
  const unwrapParens = (expression) => {
17
15
  let candidate = expression;
18
16
  while (candidate instanceof ValueComponent_1.ParenExpression) {
@@ -51,8 +49,8 @@ const rebuildWhereWithoutTerms = (query, termsToRemove) => {
51
49
  }
52
50
  query.whereClause = new Clause_1.WhereClause(rebuilt);
53
51
  };
54
- const cloneValueComponent = (expression) => {
55
- return ValueParser_1.ValueParser.parse(formatSqlComponent(expression));
52
+ const cloneValueComponent = (expression, options) => {
53
+ return ValueParser_1.ValueParser.parse((0, SqlComponentFormatter_1.formatSqlComponent)(expression, options));
56
54
  };
57
55
  const cloneColumnReference = (reference) => {
58
56
  var _a, _b;
@@ -75,11 +73,12 @@ const appendUnique = (items, value) => {
75
73
  class ParameterConditionPlacementOptimizer {
76
74
  plan(input, options = {}) {
77
75
  var _a, _b, _c;
78
- const parsed = this.parseInput(input);
76
+ const parsed = this.parseInput(input, options);
79
77
  const warnings = [...parsed.warnings];
80
78
  const errors = [...parsed.errors];
81
79
  if (!parsed.query) {
82
80
  return this.buildResult({
81
+ query: null,
83
82
  sql: parsed.sql,
84
83
  applied: [],
85
84
  skipped: [],
@@ -95,6 +94,7 @@ class ParameterConditionPlacementOptimizer {
95
94
  message: "Parameter condition placement currently supports only SimpleSelectQuery roots."
96
95
  });
97
96
  return this.buildResult({
97
+ query: parsed.query,
98
98
  sql: parsed.sql,
99
99
  applied: [],
100
100
  skipped: [],
@@ -109,40 +109,75 @@ class ParameterConditionPlacementOptimizer {
109
109
  const skipped = [];
110
110
  const movedTerms = [];
111
111
  for (const term of query.whereClause ? collectTopLevelAndTerms(query.whereClause.condition) : []) {
112
- const candidate = this.analyzeCandidate(term);
112
+ const candidate = this.analyzeCandidate(term, options);
113
113
  if (!candidate) {
114
114
  continue;
115
115
  }
116
116
  if ("code" in candidate) {
117
- skipped.push(this.makeSkipped(term, candidate));
117
+ skipped.push(this.makeSkipped(term, candidate, options));
118
118
  continue;
119
119
  }
120
120
  const target = this.resolveTarget(query, candidate);
121
121
  if ("code" in target) {
122
- skipped.push(this.makeSkipped(term, target));
122
+ skipped.push(this.makeSkipped(term, target, options));
123
123
  continue;
124
124
  }
125
- const targetColumn = this.resolveTargetColumn(target.query, target.outputColumnName);
126
- if ("code" in targetColumn) {
127
- skipped.push(this.makeSkipped(term, targetColumn));
128
- 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.";
129
163
  }
130
- const movedCondition = this.rebaseCondition(candidate.expression, candidate.column, targetColumn.column);
131
- target.query.appendWhere(movedCondition);
132
164
  movedTerms.push(term);
133
165
  applied.push({
134
166
  kind: "move_condition",
135
167
  conditionSql: candidate.conditionSql,
136
168
  fromScopeId: "scope:root",
137
169
  toScopeId: target.scopeId,
138
- reason: "All referenced columns resolve to a single direct upstream output before unsafe query boundaries.",
170
+ reason: appliedReason,
139
171
  parameterNames: candidate.parameterNames,
140
- columnReferences: [columnReferenceText(candidate.column)]
172
+ columnReferences: candidate.references.map(columnReferenceText)
141
173
  });
142
174
  }
143
175
  rebuildWhereWithoutTerms(query, new Set(movedTerms));
144
- const sql = applied.length > 0 ? 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;
145
179
  return this.buildResult({
180
+ query,
146
181
  sql,
147
182
  applied,
148
183
  skipped,
@@ -156,13 +191,22 @@ class ParameterConditionPlacementOptimizer {
156
191
  var _a;
157
192
  return this.plan(input, { ...options, dryRun: (_a = options.dryRun) !== null && _a !== void 0 ? _a : false });
158
193
  }
159
- parseInput(input) {
194
+ parseInput(input, options) {
160
195
  const warnings = [];
161
196
  const errors = [];
162
197
  try {
163
198
  const sourceSql = typeof input === "string"
164
199
  ? input
165
- : 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
+ }
166
210
  if (typeof input !== "string") {
167
211
  warnings.push({
168
212
  code: "AST_INPUT_FORMATTED",
@@ -193,7 +237,7 @@ class ParameterConditionPlacementOptimizer {
193
237
  };
194
238
  }
195
239
  }
196
- analyzeCandidate(expression) {
240
+ analyzeCandidate(expression, options) {
197
241
  const parameterNames = this.collectParameterNames(expression);
198
242
  if (parameterNames.length === 0) {
199
243
  return null;
@@ -202,40 +246,22 @@ class ParameterConditionPlacementOptimizer {
202
246
  if (unsupported) {
203
247
  return unsupported;
204
248
  }
205
- const candidate = unwrapParens(expression);
206
- if (!(candidate instanceof ValueComponent_1.BinaryExpression)) {
207
- return {
208
- code: "UNSUPPORTED_PARAMETER_CONDITION",
209
- reason: "Only simple binary parameter predicates are moved in the safe-only implementation."
210
- };
211
- }
212
- const operator = candidate.operator.value.trim().toLowerCase();
213
- if (!SUPPORTED_OPERATORS.has(operator)) {
214
- return {
215
- code: "UNSUPPORTED_OPERATOR",
216
- reason: `Operator '${candidate.operator.value}' is not supported for safe-only parameter condition placement.`
217
- };
218
- }
219
249
  const columnReferences = this.collectColumnReferences(expression);
220
- if (columnReferences.length !== 1) {
250
+ if (columnReferences.length === 0) {
221
251
  return {
222
252
  code: "AMBIGUOUS_COLUMN_REFERENCE",
223
- reason: columnReferences.length === 0
224
- ? "Parameter condition has no column reference to anchor the move."
225
- : "Parameter condition references multiple columns; moving it may change semantics."
253
+ reason: "Parameter condition has no column reference to anchor the move."
226
254
  };
227
255
  }
228
- if (operator === "in" && !this.isSupportedInPredicate(candidate)) {
229
- return {
230
- code: "UNSUPPORTED_IN_PREDICATE",
231
- reason: "Only simple column IN (:parameter) predicates are moved in the safe-only implementation."
232
- };
256
+ const unsupportedShape = this.findUnsupportedParameterPredicateShape(expression);
257
+ if (unsupportedShape) {
258
+ return unsupportedShape;
233
259
  }
234
260
  return {
235
261
  expression,
236
- conditionSql: formatSqlComponent(expression),
262
+ conditionSql: (0, SqlComponentFormatter_1.formatSqlComponent)(expression, options),
237
263
  parameterNames,
238
- column: columnReferences[0]
264
+ references: columnReferences
239
265
  };
240
266
  }
241
267
  findUnsupportedExpression(expression) {
@@ -246,13 +272,6 @@ class ParameterConditionPlacementOptimizer {
246
272
  }
247
273
  const candidate = unwrapParens(value);
248
274
  if (candidate instanceof ValueComponent_1.BinaryExpression) {
249
- if (candidate.operator.value.trim().toLowerCase() === "or") {
250
- found = {
251
- code: "OR_PREDICATE_UNSUPPORTED",
252
- reason: "Condition contains OR predicates, which are not moved in the first safe-only implementation."
253
- };
254
- return;
255
- }
256
275
  visit(candidate.left);
257
276
  visit(candidate.right);
258
277
  return;
@@ -278,13 +297,6 @@ class ParameterConditionPlacementOptimizer {
278
297
  };
279
298
  return;
280
299
  }
281
- if (candidate instanceof ValueComponent_1.BetweenExpression) {
282
- found = {
283
- code: "BETWEEN_PREDICATE_UNSUPPORTED",
284
- reason: "BETWEEN predicates are not moved in the first safe-only implementation."
285
- };
286
- return;
287
- }
288
300
  if (candidate instanceof ValueComponent_1.UnaryExpression) {
289
301
  visit(candidate.expression);
290
302
  return;
@@ -316,6 +328,39 @@ class ParameterConditionPlacementOptimizer {
316
328
  visit(expression);
317
329
  return found;
318
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
+ }
319
364
  isSupportedInPredicate(expression) {
320
365
  const left = unwrapParens(expression.left);
321
366
  const right = unwrapParens(expression.right);
@@ -332,7 +377,7 @@ class ParameterConditionPlacementOptimizer {
332
377
  && right.values.every(value => unwrapParens(value) instanceof ValueComponent_1.ParameterExpression);
333
378
  }
334
379
  resolveTarget(root, candidate) {
335
- const boundary = this.findCurrentQueryBoundary(root);
380
+ const boundary = this.findRootQueryBoundary(root);
336
381
  if (boundary) {
337
382
  return boundary;
338
383
  }
@@ -342,35 +387,38 @@ class ParameterConditionPlacementOptimizer {
342
387
  reason: "Condition has no FROM source that can receive the predicate safely."
343
388
  };
344
389
  }
345
- const bindingResult = this.resolveSourceBinding(root, candidate.column);
346
- if ("code" in bindingResult) {
347
- 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
+ };
348
405
  }
349
- const nullableSide = this.findNullableSideBoundary(root.fromClause, bindingResult);
406
+ const binding = bindings[0];
407
+ const nullableSide = this.findNullableSideBoundary(root.fromClause, binding);
350
408
  if (nullableSide) {
351
409
  return nullableSide;
352
410
  }
353
- const upstream = this.resolveUpstreamQuery(root, bindingResult, candidate.column.column.name);
411
+ const upstream = this.resolveUpstreamQuery(root, binding, candidate.references);
354
412
  if ("code" in upstream) {
355
413
  return upstream;
356
414
  }
357
- const targetBoundary = this.findTargetQueryBoundary(upstream.query);
358
- if (targetBoundary) {
359
- return targetBoundary;
360
- }
361
415
  return upstream;
362
416
  }
363
- findCurrentQueryBoundary(query) {
364
- if (query.selectClause.distinct) {
417
+ findRootQueryBoundary(query) {
418
+ if (this.hasDistinctOnBoundary(query)) {
365
419
  return {
366
420
  code: "DISTINCT_BOUNDARY",
367
- reason: "Condition crosses DISTINCT boundary; moving it may change semantics."
368
- };
369
- }
370
- if (query.groupByClause || query.havingClause) {
371
- return {
372
- code: "GROUP_BY_BOUNDARY",
373
- reason: "Condition crosses GROUP BY boundary; moving it may change semantics."
421
+ reason: "Condition crosses DISTINCT ON boundary; moving it may change semantics."
374
422
  };
375
423
  }
376
424
  if (this.hasWindowUsage(query)) {
@@ -381,10 +429,19 @@ class ParameterConditionPlacementOptimizer {
381
429
  }
382
430
  return null;
383
431
  }
384
- findTargetQueryBoundary(query) {
385
- const commonBoundary = this.findCurrentQueryBoundary(query);
386
- if (commonBoundary) {
387
- 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
+ };
388
445
  }
389
446
  if (query.limitClause || query.offsetClause || query.fetchClause) {
390
447
  return {
@@ -398,10 +455,35 @@ class ParameterConditionPlacementOptimizer {
398
455
  reason: "Target query contains an OUTER JOIN boundary that is not moved across in the safe-only implementation."
399
456
  };
400
457
  }
401
- 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
+ };
402
484
  }
403
- resolveSourceBinding(root, column) {
404
- const fromClause = root.fromClause;
485
+ resolveSourceBinding(contextRoot, query, column) {
486
+ const fromClause = query.fromClause;
405
487
  if (!fromClause) {
406
488
  return {
407
489
  code: "NO_FROM_CLAUSE",
@@ -419,10 +501,7 @@ class ParameterConditionPlacementOptimizer {
419
501
  reason: `Column source alias '${column.getNamespace()}' is not uniquely resolvable.`
420
502
  };
421
503
  }
422
- if (this.resolveSourceQueryForColumns(root, matches[0].source) instanceof SelectQuery_1.BinarySelectQuery) {
423
- return matches[0];
424
- }
425
- const matchCount = this.getOutputColumnMatchCount(root, matches[0], columnName);
504
+ const matchCount = this.getOutputColumnMatchCount(contextRoot, matches[0], columnName);
426
505
  if (matchCount === 0) {
427
506
  return {
428
507
  code: "COLUMN_NOT_AVAILABLE_UPSTREAM",
@@ -438,13 +517,8 @@ class ParameterConditionPlacementOptimizer {
438
517
  return matches[0];
439
518
  }
440
519
  const matches = [];
441
- let unionSource = null;
442
520
  for (const binding of bindings) {
443
- if (this.resolveSourceQueryForColumns(root, binding.source) instanceof SelectQuery_1.BinarySelectQuery) {
444
- unionSource !== null && unionSource !== void 0 ? unionSource : (unionSource = binding);
445
- continue;
446
- }
447
- const matchCount = this.getOutputColumnMatchCount(root, binding, columnName);
521
+ const matchCount = this.getOutputColumnMatchCount(contextRoot, binding, columnName);
448
522
  if (matchCount > 1) {
449
523
  return {
450
524
  code: "AMBIGUOUS_COLUMN_REFERENCE",
@@ -455,12 +529,6 @@ class ParameterConditionPlacementOptimizer {
455
529
  matches.push(binding);
456
530
  }
457
531
  }
458
- if (matches.length === 0 && unionSource) {
459
- return {
460
- code: "UNION_BOUNDARY",
461
- reason: "Condition would need distribution into a UNION branch, which is unsupported."
462
- };
463
- }
464
532
  if (matches.length !== 1) {
465
533
  return {
466
534
  code: "AMBIGUOUS_COLUMN_REFERENCE",
@@ -471,17 +539,20 @@ class ParameterConditionPlacementOptimizer {
471
539
  }
472
540
  return matches[0];
473
541
  }
474
- resolveUpstreamQuery(root, binding, outputColumnName) {
542
+ resolveUpstreamQuery(root, binding, references) {
475
543
  const source = binding.source.datasource;
476
544
  if (source instanceof Clause_1.SubQuerySource) {
477
545
  if (source.query instanceof SelectQuery_1.SimpleSelectQuery) {
478
546
  return {
547
+ kind: "simple",
479
548
  query: source.query,
480
549
  scopeId: `subquery:${binding.alias}`,
481
- outputColumnName,
482
550
  sourceBinding: binding
483
551
  };
484
552
  }
553
+ if (source.query instanceof SelectQuery_1.BinarySelectQuery) {
554
+ return this.resolveUnionTarget(root, source.query, `subquery:${binding.alias}`, references);
555
+ }
485
556
  return {
486
557
  code: "UNION_BOUNDARY",
487
558
  reason: "Condition would need distribution into a UNION or non-simple subquery, which is unsupported."
@@ -509,10 +580,7 @@ class ParameterConditionPlacementOptimizer {
509
580
  };
510
581
  }
511
582
  if (commonTable.query instanceof SelectQuery_1.BinarySelectQuery) {
512
- return {
513
- code: "UNION_BOUNDARY",
514
- reason: "Condition would need distribution into a UNION branch, which is unsupported."
515
- };
583
+ return this.resolveUnionTarget(root, commonTable.query, `cte:${commonTable.getSourceAliasName()}`, references);
516
584
  }
517
585
  if (!(commonTable.query instanceof SelectQuery_1.SimpleSelectQuery)) {
518
586
  return {
@@ -521,35 +589,140 @@ class ParameterConditionPlacementOptimizer {
521
589
  };
522
590
  }
523
591
  return {
592
+ kind: "simple",
524
593
  query: commonTable.query,
525
594
  scopeId: `cte:${commonTable.getSourceAliasName()}`,
526
- outputColumnName,
527
595
  sourceBinding: binding,
528
596
  cteName: commonTable.getSourceAliasName()
529
597
  };
530
598
  }
531
- resolveTargetColumn(query, outputColumnName) {
532
- const matches = query.selectClause.items.filter(item => { var _a; return normalizeIdentifier((_a = this.getSelectItemOutputName(item)) !== null && _a !== void 0 ? _a : "") === normalizeIdentifier(outputColumnName); });
533
- 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) {
534
663
  return {
535
- code: "AMBIGUOUS_TARGET_COLUMN",
536
- reason: matches.length === 0
537
- ? `Target query does not expose '${outputColumnName}' as a direct output column.`
538
- : `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}.`
539
666
  };
540
667
  }
541
- const value = matches[0].value;
542
- if (!(value instanceof ValueComponent_1.ColumnReference)) {
668
+ if (!(output.value instanceof ValueComponent_1.ColumnReference)) {
543
669
  return {
544
670
  code: "EXPRESSION_OUTPUT_UNSUPPORTED",
545
- 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.`
546
672
  };
547
673
  }
548
- const sourceResolution = this.verifyColumnResolvableInQuery(query, value);
674
+ const sourceResolution = this.verifyColumnResolvableInQuery(query, output.value);
549
675
  if (sourceResolution) {
550
676
  return sourceResolution;
551
677
  }
552
- 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);
553
726
  }
554
727
  verifyColumnResolvableInQuery(query, column) {
555
728
  if (!query.fromClause) {
@@ -577,13 +750,47 @@ class ParameterConditionPlacementOptimizer {
577
750
  reason: `Unqualified target column '${column.column.name}' is ambiguous in a multi-source destination query.`
578
751
  };
579
752
  }
580
- rebaseCondition(expression, sourceColumn, targetColumn) {
581
- 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);
582
789
  for (const reference of this.collectColumnReferences(cloned)) {
583
- if (!sameColumnReference(reference, sourceColumn)) {
584
- continue;
790
+ const target = targetColumns.find(item => sameColumnReference(reference, item.sourceColumn));
791
+ if (target) {
792
+ reference.qualifiedName = cloneColumnReference(target.targetColumn).qualifiedName;
585
793
  }
586
- reference.qualifiedName = cloneColumnReference(targetColumn).qualifiedName;
587
794
  }
588
795
  return cloned;
589
796
  }
@@ -641,12 +848,80 @@ class ParameterConditionPlacementOptimizer {
641
848
  return joinType.includes("left") || joinType.includes("right") || joinType.includes("full") || joinType.includes("outer");
642
849
  });
643
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
+ }
644
857
  getOutputColumnMatchCount(root, binding, columnName) {
645
858
  const target = this.resolveSourceQueryForColumns(root, binding.source);
646
- if (!target || !(target instanceof SelectQuery_1.SimpleSelectQuery)) {
859
+ if (!target) {
647
860
  return 0;
648
861
  }
649
- 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);
650
925
  }
651
926
  resolveSourceQueryForColumns(root, source) {
652
927
  var _a;
@@ -661,15 +936,6 @@ class ParameterConditionPlacementOptimizer {
661
936
  }
662
937
  return null;
663
938
  }
664
- getSelectItemOutputName(item) {
665
- if (item.identifier) {
666
- return item.identifier.name;
667
- }
668
- if (item.value instanceof ValueComponent_1.ColumnReference) {
669
- return item.value.column.name;
670
- }
671
- return null;
672
- }
673
939
  findCte(root, name) {
674
940
  var _a, _b;
675
941
  const normalized = normalizeIdentifier(name);
@@ -915,9 +1181,9 @@ class ParameterConditionPlacementOptimizer {
915
1181
  visit(expression);
916
1182
  return names;
917
1183
  }
918
- makeSkipped(expression, draft) {
1184
+ makeSkipped(expression, draft, options) {
919
1185
  return {
920
- conditionSql: formatSqlComponent(expression),
1186
+ conditionSql: (0, SqlComponentFormatter_1.formatSqlComponent)(expression, options),
921
1187
  scopeId: "scope:root",
922
1188
  ...draft
923
1189
  };
@@ -926,6 +1192,7 @@ class ParameterConditionPlacementOptimizer {
926
1192
  return {
927
1193
  ok: params.errors.length === 0,
928
1194
  sql: params.sql,
1195
+ query: params.query,
929
1196
  applied: params.applied,
930
1197
  skipped: params.skipped,
931
1198
  warnings: params.warnings,