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