rawsql-ts 0.28.0 → 0.30.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 (54) hide show
  1. package/dist/esm/index.d.ts +1 -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/ConditionDeduplicationOptimizer.d.ts +22 -0
  7. package/dist/esm/transformers/ConditionDeduplicationOptimizer.js +198 -0
  8. package/dist/esm/transformers/ConditionDeduplicationOptimizer.js.map +1 -0
  9. package/dist/esm/transformers/ConditionOptimization.d.ts +31 -1
  10. package/dist/esm/transformers/ConditionOptimization.js +321 -4
  11. package/dist/esm/transformers/ConditionOptimization.js.map +1 -1
  12. package/dist/esm/transformers/ParameterConditionPlacementOptimizer.d.ts +13 -0
  13. package/dist/esm/transformers/ParameterConditionPlacementOptimizer.js +238 -87
  14. package/dist/esm/transformers/ParameterConditionPlacementOptimizer.js.map +1 -1
  15. package/dist/esm/transformers/PredicateExpressionUtils.d.ts +16 -0
  16. package/dist/esm/transformers/PredicateExpressionUtils.js +74 -0
  17. package/dist/esm/transformers/PredicateExpressionUtils.js.map +1 -0
  18. package/dist/esm/transformers/PredicateReachabilityAnalyzer.d.ts +88 -0
  19. package/dist/esm/transformers/PredicateReachabilityAnalyzer.js +495 -0
  20. package/dist/esm/transformers/PredicateReachabilityAnalyzer.js.map +1 -0
  21. package/dist/esm/transformers/StaticPredicatePlacementOptimizer.d.ts +14 -0
  22. package/dist/esm/transformers/StaticPredicatePlacementOptimizer.js +344 -157
  23. package/dist/esm/transformers/StaticPredicatePlacementOptimizer.js.map +1 -1
  24. package/dist/esm/transformers/TopLevelAndConditionDeduper.d.ts +10 -0
  25. package/dist/esm/transformers/TopLevelAndConditionDeduper.js +66 -0
  26. package/dist/esm/transformers/TopLevelAndConditionDeduper.js.map +1 -0
  27. package/dist/index.js +1 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/index.min.js +2 -2
  30. package/dist/index.min.js.map +4 -4
  31. package/dist/src/index.d.ts +1 -0
  32. package/dist/src/transformers/ConditionDeduplicationOptimizer.d.ts +22 -0
  33. package/dist/src/transformers/ConditionOptimization.d.ts +31 -1
  34. package/dist/src/transformers/ParameterConditionPlacementOptimizer.d.ts +13 -0
  35. package/dist/src/transformers/PredicateExpressionUtils.d.ts +16 -0
  36. package/dist/src/transformers/PredicateReachabilityAnalyzer.d.ts +88 -0
  37. package/dist/src/transformers/StaticPredicatePlacementOptimizer.d.ts +14 -0
  38. package/dist/src/transformers/TopLevelAndConditionDeduper.d.ts +10 -0
  39. package/dist/transformers/ConditionDeduplicationOptimizer.js +202 -0
  40. package/dist/transformers/ConditionDeduplicationOptimizer.js.map +1 -0
  41. package/dist/transformers/ConditionOptimization.js +324 -4
  42. package/dist/transformers/ConditionOptimization.js.map +1 -1
  43. package/dist/transformers/ParameterConditionPlacementOptimizer.js +261 -110
  44. package/dist/transformers/ParameterConditionPlacementOptimizer.js.map +1 -1
  45. package/dist/transformers/PredicateExpressionUtils.js +88 -0
  46. package/dist/transformers/PredicateExpressionUtils.js.map +1 -0
  47. package/dist/transformers/PredicateReachabilityAnalyzer.js +508 -0
  48. package/dist/transformers/PredicateReachabilityAnalyzer.js.map +1 -0
  49. package/dist/transformers/StaticPredicatePlacementOptimizer.js +366 -179
  50. package/dist/transformers/StaticPredicatePlacementOptimizer.js.map +1 -1
  51. package/dist/transformers/TopLevelAndConditionDeduper.js +72 -0
  52. package/dist/transformers/TopLevelAndConditionDeduper.js.map +1 -0
  53. package/dist/tsconfig.browser.tsbuildinfo +1 -1
  54. package/package.json +1 -1
@@ -1,71 +1,12 @@
1
- import { DistinctOn, SubQuerySource, TableSource, WhereClause } from "../models/Clause";
1
+ import { DistinctOn, JoinOnClause, SubQuerySource, TableSource } from "../models/Clause";
2
2
  import { BinarySelectQuery, SimpleSelectQuery } from "../models/SelectQuery";
3
- import { ArrayExpression, ArrayQueryExpression, BetweenExpression, BinaryExpression, CaseExpression, CastExpression, ColumnReference, FunctionCall, InlineQuery, JsonPredicateExpression, ParameterExpression, ParenExpression, TupleExpression, TypeValue, UnaryExpression, ValueList } from "../models/ValueComponent";
3
+ import { ArrayExpression, ArrayQueryExpression, BetweenExpression, BinaryExpression, CaseExpression, CastExpression, ColumnReference, FunctionCall, InlineQuery, JsonPredicateExpression, ParameterExpression, TupleExpression, TypeValue, UnaryExpression, ValueList } from "../models/ValueComponent";
4
4
  import { SelectQueryParser } from "../parsers/SelectQueryParser";
5
- import { ValueParser } from "../parsers/ValueParser";
6
5
  import { formatSqlComponent, hasSqlComponentFormatOverride } from "./SqlComponentFormatter";
7
6
  import { SelectOutputCollector } from "./SelectOutputCollector";
7
+ import { dedupeTopLevelAndConditions, dedupeWhereTopLevelAndConditions } from "./TopLevelAndConditionDeduper";
8
+ import { appendUnique, cloneColumnReference, cloneValueComponent, collectTopLevelAndTerms, columnReferenceText, identifiersEqual, normalizeIdentifier, rebuildWhereWithoutTerms, sameColumnReference, unwrapParens } from "./PredicateExpressionUtils";
8
9
  const VOLATILE_OR_UNSUPPORTED_FUNCTION_REASON = "Predicate contains a function call; volatile and expression predicates are not moved in the safe-only implementation.";
9
- const normalizeIdentifier = (value) => value.trim().toLowerCase();
10
- const unwrapParens = (expression) => {
11
- let candidate = expression;
12
- while (candidate instanceof ParenExpression) {
13
- candidate = candidate.expression;
14
- }
15
- return candidate;
16
- };
17
- const isBinaryOperator = (expression, operator) => {
18
- const candidate = unwrapParens(expression);
19
- return candidate instanceof BinaryExpression
20
- && candidate.operator.value.trim().toLowerCase() === operator;
21
- };
22
- const collectTopLevelAndTerms = (expression) => {
23
- const candidate = unwrapParens(expression);
24
- if (!isBinaryOperator(candidate, "and")) {
25
- return [expression];
26
- }
27
- return [
28
- ...collectTopLevelAndTerms(candidate.left),
29
- ...collectTopLevelAndTerms(candidate.right)
30
- ];
31
- };
32
- const rebuildWhereWithoutTerms = (query, termsToRemove) => {
33
- if (!query.whereClause || termsToRemove.size === 0) {
34
- return;
35
- }
36
- const remaining = collectTopLevelAndTerms(query.whereClause.condition)
37
- .filter(term => !termsToRemove.has(term));
38
- if (remaining.length === 0) {
39
- query.whereClause = null;
40
- return;
41
- }
42
- let rebuilt = remaining[0];
43
- for (let index = 1; index < remaining.length; index += 1) {
44
- rebuilt = new BinaryExpression(rebuilt, "and", remaining[index]);
45
- }
46
- query.whereClause = new WhereClause(rebuilt);
47
- };
48
- const cloneValueComponent = (expression, options) => {
49
- return ValueParser.parse(formatSqlComponent(expression, options));
50
- };
51
- const cloneColumnReference = (reference) => {
52
- var _a, _b;
53
- const namespaces = (_b = (_a = reference.namespaces) === null || _a === void 0 ? void 0 : _a.map(namespace => namespace.name)) !== null && _b !== void 0 ? _b : null;
54
- return new ColumnReference(namespaces, reference.column.name);
55
- };
56
- const columnReferenceText = (reference) => {
57
- const namespace = reference.getNamespace();
58
- return namespace ? `${namespace}.${reference.column.name}` : reference.column.name;
59
- };
60
- const sameColumnReference = (left, right) => {
61
- return normalizeIdentifier(left.column.name) === normalizeIdentifier(right.column.name)
62
- && normalizeIdentifier(left.getNamespace()) === normalizeIdentifier(right.getNamespace());
63
- };
64
- const appendUnique = (items, value) => {
65
- if (!items.includes(value)) {
66
- items.push(value);
67
- }
68
- };
69
10
  export class StaticPredicatePlacementOptimizer {
70
11
  plan(input, options = {}) {
71
12
  var _a, _b, _c;
@@ -103,71 +44,8 @@ export class StaticPredicatePlacementOptimizer {
103
44
  const query = parsed.query;
104
45
  const applied = [];
105
46
  const skipped = [];
106
- const movedTerms = [];
107
- for (const term of query.whereClause ? collectTopLevelAndTerms(query.whereClause.condition) : []) {
108
- const candidate = this.analyzeCandidate(query, term, options);
109
- if (!candidate) {
110
- continue;
111
- }
112
- if ("code" in candidate) {
113
- skipped.push(this.makeSkipped(term, candidate, options));
114
- continue;
115
- }
116
- const target = this.resolveTarget(query, candidate);
117
- if ("code" in target) {
118
- skipped.push(this.makeSkipped(term, target, options));
119
- continue;
120
- }
121
- let appliedReason = "";
122
- if (target.kind === "simple") {
123
- const targetColumns = this.resolveTargetColumns(query, target.query, candidate.references);
124
- if ("code" in targetColumns) {
125
- skipped.push(this.makeSkipped(term, targetColumns, options));
126
- continue;
127
- }
128
- const placement = this.resolveTargetPlacement(target.query, targetColumns);
129
- if ("code" in placement) {
130
- skipped.push(this.makeSkipped(term, placement, options));
131
- continue;
132
- }
133
- const movedPredicate = this.rebasePredicate(candidate.expression, targetColumns, options);
134
- target.query.appendWhere(movedPredicate);
135
- appliedReason = placement.reason;
136
- }
137
- else {
138
- const placements = [];
139
- let skip = null;
140
- for (const branch of target.branches) {
141
- const placement = this.resolveTargetPlacement(branch.query, branch.targetColumns);
142
- if ("code" in placement) {
143
- skip = placement;
144
- break;
145
- }
146
- placements.push(placement);
147
- }
148
- if (skip) {
149
- skipped.push(this.makeSkipped(term, skip, options));
150
- continue;
151
- }
152
- for (const branch of target.branches) {
153
- const movedPredicate = this.rebasePredicate(candidate.expression, branch.targetColumns, options);
154
- branch.query.appendWhere(movedPredicate);
155
- }
156
- appliedReason = placements.some(item => /group by/i.test(item.reason))
157
- ? "Predicate is distributed to every UNION branch by output column position; grouped branches only receive GROUP BY-key predicates."
158
- : "Predicate is distributed to every UNION branch by output column position before unsafe query boundaries.";
159
- }
160
- movedTerms.push(term);
161
- applied.push({
162
- kind: "move_static_predicate",
163
- predicateSql: candidate.predicateSql,
164
- fromScopeId: "scope:root",
165
- toScopeId: target.scopeId,
166
- reason: appliedReason,
167
- columnReferences: candidate.references.map(columnReferenceText)
168
- });
169
- }
170
- rebuildWhereWithoutTerms(query, new Set(movedTerms));
47
+ this.placePredicatesInScopes(query, options, applied, skipped);
48
+ // API output shape review: keep result.sql/result.query compatibility while expanding scope-aware placement.
171
49
  const sql = applied.length > 0 || hasSqlComponentFormatOverride(options)
172
50
  ? formatSqlComponent(query, options)
173
51
  : parsed.sql;
@@ -182,6 +60,121 @@ export class StaticPredicatePlacementOptimizer {
182
60
  formatterGeneratedSource: parsed.formatterGeneratedSource
183
61
  });
184
62
  }
63
+ placePredicatesInScopes(contextRoot, options, applied, skipped) {
64
+ var _a;
65
+ const queue = [{ query: contextRoot, scopeId: "scope:root" }];
66
+ let processed = 0;
67
+ while (queue.length > 0) {
68
+ processed += 1;
69
+ if (processed > 1000) {
70
+ skipped.push({
71
+ predicateSql: "",
72
+ scopeId: "scope:root",
73
+ code: "RECURSIVE_PLACEMENT_LIMIT",
74
+ reason: "Static predicate recursive placement stopped after reaching the safety iteration limit."
75
+ });
76
+ return;
77
+ }
78
+ const work = queue.shift();
79
+ const movedTerms = [];
80
+ const reportSkipped = work.pendingTerms === undefined;
81
+ const terms = (_a = work.pendingTerms) !== null && _a !== void 0 ? _a : (work.query.whereClause
82
+ ? collectTopLevelAndTerms(work.query.whereClause.condition)
83
+ : []);
84
+ for (const term of terms) {
85
+ const candidate = this.analyzeCandidate(work.query, term, options);
86
+ if (!candidate) {
87
+ continue;
88
+ }
89
+ if ("code" in candidate) {
90
+ if (reportSkipped) {
91
+ skipped.push(this.makeSkipped(term, work.scopeId, candidate, options));
92
+ }
93
+ continue;
94
+ }
95
+ const target = this.resolveTarget(contextRoot, work.query, candidate);
96
+ if ("code" in target) {
97
+ if (reportSkipped) {
98
+ skipped.push(this.makeSkipped(term, work.scopeId, target, options));
99
+ }
100
+ continue;
101
+ }
102
+ let appliedReason = "";
103
+ if (target.kind === "join_on") {
104
+ this.appendJoinOnPredicate(target.join, candidate.expression, options);
105
+ appliedReason = target.reason;
106
+ }
107
+ else if (target.kind === "simple") {
108
+ const targetColumns = this.resolveTargetColumns(contextRoot, target.query, candidate.references);
109
+ if ("code" in targetColumns) {
110
+ if (reportSkipped) {
111
+ skipped.push(this.makeSkipped(term, work.scopeId, targetColumns, options));
112
+ }
113
+ continue;
114
+ }
115
+ const placement = this.resolveTargetPlacement(contextRoot, target.query, targetColumns);
116
+ if ("code" in placement) {
117
+ if (reportSkipped) {
118
+ skipped.push(this.makeSkipped(term, work.scopeId, placement, options));
119
+ }
120
+ continue;
121
+ }
122
+ const movedPredicate = this.rebasePredicate(candidate.expression, targetColumns, options);
123
+ target.query.appendWhere(movedPredicate);
124
+ dedupeWhereTopLevelAndConditions(target.query, options);
125
+ appliedReason = work.scopeId === "scope:root"
126
+ ? placement.reason
127
+ : `${placement.reason} Predicate was safely rebased from a previously moved predicate.`;
128
+ const retainedTerms = target.query.whereClause
129
+ ? collectTopLevelAndTerms(target.query.whereClause.condition)
130
+ : [];
131
+ if (retainedTerms.includes(movedPredicate)) {
132
+ queue.push({
133
+ query: target.query,
134
+ scopeId: target.scopeId,
135
+ pendingTerms: [movedPredicate]
136
+ });
137
+ }
138
+ }
139
+ else {
140
+ const placements = [];
141
+ let skip = null;
142
+ for (const branch of target.branches) {
143
+ const placement = this.resolveTargetPlacement(contextRoot, branch.query, branch.targetColumns);
144
+ if ("code" in placement) {
145
+ skip = placement;
146
+ break;
147
+ }
148
+ placements.push(placement);
149
+ }
150
+ if (skip) {
151
+ if (reportSkipped) {
152
+ skipped.push(this.makeSkipped(term, work.scopeId, skip, options));
153
+ }
154
+ continue;
155
+ }
156
+ for (const branch of target.branches) {
157
+ const movedPredicate = this.rebasePredicate(candidate.expression, branch.targetColumns, options);
158
+ branch.query.appendWhere(movedPredicate);
159
+ dedupeWhereTopLevelAndConditions(branch.query, options);
160
+ }
161
+ appliedReason = placements.some(item => /group by/i.test(item.reason))
162
+ ? "Predicate is distributed to every UNION branch by output column position; grouped branches only receive GROUP BY-key predicates."
163
+ : "Predicate is distributed to every UNION branch by output column position before unsafe query boundaries.";
164
+ }
165
+ movedTerms.push(term);
166
+ applied.push({
167
+ kind: "move_static_predicate",
168
+ predicateSql: candidate.predicateSql,
169
+ fromScopeId: work.scopeId,
170
+ toScopeId: target.scopeId,
171
+ reason: appliedReason,
172
+ columnReferences: candidate.references.map(columnReferenceText)
173
+ });
174
+ }
175
+ rebuildWhereWithoutTerms(work.query, new Set(movedTerms));
176
+ }
177
+ }
185
178
  optimize(input, options = {}) {
186
179
  var _a;
187
180
  return this.plan(input, Object.assign(Object.assign({}, options), { dryRun: (_a = options.dryRun) !== null && _a !== void 0 ? _a : false }));
@@ -377,12 +370,13 @@ export class StaticPredicatePlacementOptimizer {
377
370
  visit(expression);
378
371
  return found;
379
372
  }
380
- resolveTarget(root, candidate) {
381
- const boundary = this.findRootQueryBoundary(root);
373
+ resolveTarget(contextRoot, currentQuery, candidate) {
374
+ var _a;
375
+ const boundary = this.findRootQueryBoundary(currentQuery);
382
376
  if (boundary) {
383
377
  return boundary;
384
378
  }
385
- if (!root.fromClause) {
379
+ if (!currentQuery.fromClause) {
386
380
  return {
387
381
  code: "NO_FROM_CLAUSE",
388
382
  reason: "Predicate has no FROM source that can receive it safely."
@@ -390,7 +384,7 @@ export class StaticPredicatePlacementOptimizer {
390
384
  }
391
385
  const bindings = [];
392
386
  for (const reference of candidate.references) {
393
- const binding = this.resolveSourceBinding(root, root, reference);
387
+ const binding = this.resolveSourceBinding(contextRoot, currentQuery, reference);
394
388
  if ("code" in binding) {
395
389
  return binding;
396
390
  }
@@ -405,12 +399,25 @@ export class StaticPredicatePlacementOptimizer {
405
399
  };
406
400
  }
407
401
  const binding = bindings[0];
408
- const nullableSide = this.findNullableSideBoundary(root.fromClause, binding);
402
+ if ((_a = binding.join) === null || _a === void 0 ? void 0 : _a.lateral) {
403
+ return {
404
+ code: "LATERAL_JOIN_BOUNDARY",
405
+ reason: "Predicate crosses a LATERAL JOIN boundary; moving it may change semantics."
406
+ };
407
+ }
408
+ const nullableSide = this.findNullableSideBoundary(currentQuery.fromClause, binding);
409
409
  if (nullableSide) {
410
410
  return nullableSide;
411
411
  }
412
- const upstream = this.resolveUpstreamQuery(root, binding, candidate.references);
412
+ const upstream = this.resolveUpstreamQuery(contextRoot, binding, candidate.references);
413
413
  if ("code" in upstream) {
414
+ if (upstream.code === "NO_SAFE_UPSTREAM_QUERY") {
415
+ const joinOnTarget = this.resolveBaseTableJoinOnTarget(contextRoot, currentQuery, binding);
416
+ if (!("code" in joinOnTarget)) {
417
+ return joinOnTarget;
418
+ }
419
+ return joinOnTarget;
420
+ }
414
421
  return upstream;
415
422
  }
416
423
  return upstream;
@@ -430,7 +437,7 @@ export class StaticPredicatePlacementOptimizer {
430
437
  }
431
438
  return null;
432
439
  }
433
- resolveTargetPlacement(query, targetColumns) {
440
+ resolveTargetPlacement(contextRoot, query, targetColumns) {
434
441
  const hasOrdinaryDistinct = this.hasOrdinaryDistinct(query);
435
442
  if (this.hasDistinctOnBoundary(query)) {
436
443
  return {
@@ -450,11 +457,11 @@ export class StaticPredicatePlacementOptimizer {
450
457
  reason: "Predicate crosses LIMIT/OFFSET/FETCH boundary; moving it may change row selection semantics."
451
458
  };
452
459
  }
453
- if (query.fromClause && this.hasOuterJoin(query.fromClause)) {
454
- return {
455
- code: "OUTER_JOIN_BOUNDARY",
456
- reason: "Target query contains an OUTER JOIN boundary that is not moved across in the safe-only implementation."
457
- };
460
+ if (query.fromClause) {
461
+ const nullableSide = this.findNullableSideBoundaryForTargetColumns(contextRoot, query, targetColumns);
462
+ if (nullableSide) {
463
+ return nullableSide;
464
+ }
458
465
  }
459
466
  if (query.groupByClause) {
460
467
  const allReferencesAreGroupKeys = targetColumns.every(item => this.isGroupKeyColumn(query, item.targetColumn));
@@ -483,6 +490,22 @@ export class StaticPredicatePlacementOptimizer {
483
490
  reason: "All outer references resolve to direct upstream outputs before unsafe query boundaries."
484
491
  };
485
492
  }
493
+ findNullableSideBoundaryForTargetColumns(contextRoot, query, targetColumns) {
494
+ if (!query.fromClause) {
495
+ return null;
496
+ }
497
+ for (const item of targetColumns) {
498
+ const binding = this.resolveSourceBinding(contextRoot, query, item.targetColumn);
499
+ if ("code" in binding) {
500
+ return binding;
501
+ }
502
+ const nullableSide = this.findNullableSideBoundary(query.fromClause, binding);
503
+ if (nullableSide) {
504
+ return nullableSide;
505
+ }
506
+ }
507
+ return null;
508
+ }
486
509
  resolveSourceBinding(contextRoot, query, column) {
487
510
  const fromClause = query.fromClause;
488
511
  if (!fromClause) {
@@ -492,10 +515,10 @@ export class StaticPredicatePlacementOptimizer {
492
515
  };
493
516
  }
494
517
  const bindings = this.getSourceBindings(fromClause);
495
- const namespace = normalizeIdentifier(column.getNamespace());
518
+ const namespace = column.getNamespace();
496
519
  const columnName = column.column.name;
497
520
  if (namespace) {
498
- const matches = bindings.filter(binding => normalizeIdentifier(binding.alias) === namespace);
521
+ const matches = bindings.filter(binding => identifiersEqual(binding.alias, namespace));
499
522
  if (matches.length !== 1) {
500
523
  return {
501
524
  code: "AMBIGUOUS_COLUMN_SOURCE",
@@ -503,6 +526,9 @@ export class StaticPredicatePlacementOptimizer {
503
526
  };
504
527
  }
505
528
  const matchCount = this.getOutputColumnMatchCount(contextRoot, matches[0], columnName);
529
+ if (matchCount === 0 && this.isBaseTableBinding(contextRoot, matches[0])) {
530
+ return matches[0];
531
+ }
506
532
  if (matchCount === 0) {
507
533
  return {
508
534
  code: "COLUMN_NOT_AVAILABLE_UPSTREAM",
@@ -631,7 +657,7 @@ export class StaticPredicatePlacementOptimizer {
631
657
  resolveTargetColumns(root, query, references) {
632
658
  const resolved = [];
633
659
  for (const reference of references) {
634
- const matches = this.collectSelectOutputs(root, query).filter(item => normalizeIdentifier(item.name) === normalizeIdentifier(reference.column.name));
660
+ const matches = this.collectDirectOutputMatches(root, query, reference.column.name);
635
661
  if (matches.length !== 1) {
636
662
  return {
637
663
  code: "AMBIGUOUS_TARGET_COLUMN",
@@ -658,6 +684,57 @@ export class StaticPredicatePlacementOptimizer {
658
684
  }
659
685
  return resolved;
660
686
  }
687
+ resolveBaseTableJoinOnTarget(contextRoot, currentQuery, binding) {
688
+ var _a, _b, _c;
689
+ if (!currentQuery.fromClause || !this.isBaseTableBinding(contextRoot, binding)) {
690
+ return {
691
+ code: "NO_SAFE_UPSTREAM_QUERY",
692
+ reason: "The referenced source is a base table, so there is no upstream query block to move into."
693
+ };
694
+ }
695
+ if (this.hasOuterJoin(currentQuery.fromClause)) {
696
+ return {
697
+ code: "OUTER_JOIN_BOUNDARY",
698
+ reason: "Predicate crosses an OUTER JOIN boundary; moving it into JOIN ON may change semantics."
699
+ };
700
+ }
701
+ const join = binding.isPrimary
702
+ ? (_b = (_a = currentQuery.fromClause.joins) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : null
703
+ : binding.join;
704
+ if (join === null || join === void 0 ? void 0 : join.lateral) {
705
+ return {
706
+ code: "LATERAL_JOIN_BOUNDARY",
707
+ reason: "Predicate crosses a LATERAL JOIN boundary; moving it into JOIN ON may change semantics."
708
+ };
709
+ }
710
+ if (!join || !this.isInnerJoin(join)) {
711
+ return {
712
+ code: "NO_SAFE_JOIN_ON_TARGET",
713
+ reason: "Base-table predicates are moved only into INNER JOIN ON clauses in the safe-only implementation."
714
+ };
715
+ }
716
+ if (!(join.condition instanceof JoinOnClause)) {
717
+ return {
718
+ code: "NO_SAFE_JOIN_ON_TARGET",
719
+ reason: "Base-table predicates are moved only into existing JOIN ON clauses, not USING or conditionless joins."
720
+ };
721
+ }
722
+ return {
723
+ kind: "join_on",
724
+ join,
725
+ scopeId: `join_on:${(_c = join.source.getAliasName()) !== null && _c !== void 0 ? _c : "unknown"}`,
726
+ reason: binding.isPrimary
727
+ ? "Predicate references the primary source of an INNER JOIN; it is moved into the first JOIN ON clause."
728
+ : "Predicate references the joined source of an INNER JOIN; it is moved into that JOIN ON clause."
729
+ };
730
+ }
731
+ appendJoinOnPredicate(join, expression, options) {
732
+ if (!(join.condition instanceof JoinOnClause)) {
733
+ return;
734
+ }
735
+ join.condition.condition = new BinaryExpression(join.condition.condition, "and", cloneValueComponent(expression, options));
736
+ join.condition.condition = dedupeTopLevelAndConditions(join.condition.condition, options);
737
+ }
661
738
  resolveDeepestBranchTarget(contextRoot, query, targetColumns, visited = new Set()) {
662
739
  if (visited.has(query) || targetColumns.length === 0) {
663
740
  return { query, targetColumns: [...targetColumns] };
@@ -685,14 +762,17 @@ export class StaticPredicatePlacementOptimizer {
685
762
  return { query, targetColumns: [...targetColumns] };
686
763
  }
687
764
  const upstream = this.resolveUpstreamQuery(contextRoot, binding, targetColumns.map(item => item.targetColumn));
688
- if ("code" in upstream || upstream.kind === "union") {
765
+ if ("code" in upstream) {
766
+ return { query, targetColumns: [...targetColumns] };
767
+ }
768
+ if (upstream.kind !== "simple") {
689
769
  return { query, targetColumns: [...targetColumns] };
690
770
  }
691
771
  const upstreamColumns = this.resolveTargetColumns(contextRoot, upstream.query, targetColumns.map(item => item.targetColumn));
692
772
  if ("code" in upstreamColumns) {
693
773
  return { query, targetColumns: [...targetColumns] };
694
774
  }
695
- const placement = this.resolveTargetPlacement(upstream.query, upstreamColumns);
775
+ const placement = this.resolveTargetPlacement(contextRoot, upstream.query, upstreamColumns);
696
776
  if ("code" in placement) {
697
777
  return { query, targetColumns: [...targetColumns] };
698
778
  }
@@ -710,9 +790,9 @@ export class StaticPredicatePlacementOptimizer {
710
790
  };
711
791
  }
712
792
  const bindings = this.getSourceBindings(query.fromClause);
713
- const namespace = normalizeIdentifier(column.getNamespace());
793
+ const namespace = column.getNamespace();
714
794
  if (namespace) {
715
- const matches = bindings.filter(binding => normalizeIdentifier(binding.alias) === namespace);
795
+ const matches = bindings.filter(binding => identifiersEqual(binding.alias, namespace));
716
796
  return matches.length === 1
717
797
  ? null
718
798
  : {
@@ -743,7 +823,7 @@ export class StaticPredicatePlacementOptimizer {
743
823
  if (sameColumnReference(left, right)) {
744
824
  return true;
745
825
  }
746
- if (normalizeIdentifier(left.column.name) !== normalizeIdentifier(right.column.name)) {
826
+ if (!identifiersEqual(left.column.name, right.column.name)) {
747
827
  return false;
748
828
  }
749
829
  const leftSource = this.resolveColumnSourceAlias(query, left);
@@ -755,12 +835,12 @@ export class StaticPredicatePlacementOptimizer {
755
835
  return null;
756
836
  }
757
837
  const bindings = this.getSourceBindings(query.fromClause);
758
- const namespace = normalizeIdentifier(column.getNamespace());
838
+ const namespace = column.getNamespace();
759
839
  if (namespace) {
760
- const matches = bindings.filter(binding => normalizeIdentifier(binding.alias) === namespace);
761
- return matches.length === 1 ? normalizeIdentifier(matches[0].alias) : null;
840
+ const matches = bindings.filter(binding => identifiersEqual(binding.alias, namespace));
841
+ return matches.length === 1 ? matches[0].alias : null;
762
842
  }
763
- return bindings.length === 1 ? normalizeIdentifier(bindings[0].alias) : null;
843
+ return bindings.length === 1 ? bindings[0].alias : null;
764
844
  }
765
845
  rebasePredicate(expression, targetColumns, options) {
766
846
  const cloned = cloneValueComponent(expression, options);
@@ -897,6 +977,14 @@ export class StaticPredicatePlacementOptimizer {
897
977
  }
898
978
  return bindings;
899
979
  }
980
+ isBaseTableBinding(root, binding) {
981
+ const source = binding.source.datasource;
982
+ return source instanceof TableSource && !this.findCte(root, source.table.name);
983
+ }
984
+ isInnerJoin(join) {
985
+ const joinType = join.joinType.value.trim().toLowerCase();
986
+ return joinType === "join" || joinType === "inner join";
987
+ }
900
988
  findNullableSideBoundary(fromClause, binding) {
901
989
  var _a;
902
990
  const joins = (_a = fromClause.joins) !== null && _a !== void 0 ? _a : [];
@@ -964,12 +1052,12 @@ export class StaticPredicatePlacementOptimizer {
964
1052
  if ("code" in branches) {
965
1053
  return 0;
966
1054
  }
967
- return this.collectSelectOutputs(root, branches[0]).filter(item => normalizeIdentifier(item.name) === normalizeIdentifier(columnName)).length;
1055
+ return this.collectDirectOutputMatches(root, branches[0], columnName).length;
968
1056
  }
969
1057
  if (!(target instanceof SimpleSelectQuery)) {
970
1058
  return 0;
971
1059
  }
972
- return this.collectSelectOutputs(root, target).filter(item => normalizeIdentifier(item.name) === normalizeIdentifier(columnName)).length;
1060
+ return this.collectDirectOutputMatches(root, target, columnName).length;
973
1061
  }
974
1062
  collectUnionBranches(query) {
975
1063
  const branches = [];
@@ -1000,7 +1088,7 @@ export class StaticPredicatePlacementOptimizer {
1000
1088
  resolveUnionOutputIndex(root, firstBranch, outputColumnName) {
1001
1089
  const matches = [];
1002
1090
  this.collectSelectOutputs(root, firstBranch).forEach((item, index) => {
1003
- if (normalizeIdentifier(item.name) === normalizeIdentifier(outputColumnName)) {
1091
+ if (identifiersEqual(item.name, outputColumnName)) {
1004
1092
  matches.push(index);
1005
1093
  }
1006
1094
  });
@@ -1022,6 +1110,15 @@ export class StaticPredicatePlacementOptimizer {
1022
1110
  reason: `UNION branch does not expose column '${sourceColumn.column.name}' at output position ${outputIndex + 1}.`
1023
1111
  };
1024
1112
  }
1113
+ else if (identifiersEqual(output.name, sourceColumn.column.name)) {
1114
+ const matches = this.collectDirectOutputMatches(root, query, sourceColumn.column.name);
1115
+ if (matches.length > 1) {
1116
+ return {
1117
+ code: "AMBIGUOUS_TARGET_COLUMN",
1118
+ reason: `UNION branch exposes multiple '${sourceColumn.column.name}' columns.`
1119
+ };
1120
+ }
1121
+ }
1025
1122
  if (!(output.value instanceof ColumnReference)) {
1026
1123
  return {
1027
1124
  code: "EXPRESSION_OUTPUT_UNSUPPORTED",
@@ -1046,6 +1143,96 @@ export class StaticPredicatePlacementOptimizer {
1046
1143
  const collector = new SelectOutputCollector(null, commonTables.length > 0 ? commonTables : null);
1047
1144
  return collector.collect(query);
1048
1145
  }
1146
+ collectDirectOutputMatches(root, query, columnName) {
1147
+ const collectedMatches = this.collectSelectOutputs(root, query).filter(item => identifiersEqual(item.name, columnName));
1148
+ if (collectedMatches.length > 0) {
1149
+ return this.hasExplicitOutputMatch(query, columnName)
1150
+ ? [...collectedMatches, ...this.inferWildcardOutputMatches(root, query, columnName, true)]
1151
+ : collectedMatches;
1152
+ }
1153
+ return this.inferWildcardOutputMatches(root, query, columnName, false);
1154
+ }
1155
+ hasExplicitOutputMatch(query, columnName) {
1156
+ return query.selectClause.items.some(item => {
1157
+ if (item.identifier) {
1158
+ return identifiersEqual(item.identifier.name, columnName);
1159
+ }
1160
+ return item.value instanceof ColumnReference
1161
+ && item.value.column.name !== "*"
1162
+ && identifiersEqual(item.value.column.name, columnName);
1163
+ });
1164
+ }
1165
+ inferWildcardOutputMatches(root, query, columnName, includeUnprovenPotential) {
1166
+ const matches = [];
1167
+ query.selectClause.items.forEach((item, outputIndex) => {
1168
+ if (item.identifier || !(item.value instanceof ColumnReference) || item.value.column.name !== "*") {
1169
+ return;
1170
+ }
1171
+ const targetColumn = this.inferWildcardTargetColumn(root, query, item.value, columnName, includeUnprovenPotential);
1172
+ if (!targetColumn) {
1173
+ return;
1174
+ }
1175
+ if (targetColumn === "ambiguous") {
1176
+ matches.push(this.createInferredOutputColumn(columnName, new ColumnReference(null, columnName), outputIndex), this.createInferredOutputColumn(columnName, new ColumnReference(null, columnName), outputIndex));
1177
+ return;
1178
+ }
1179
+ matches.push(this.createInferredOutputColumn(columnName, targetColumn, outputIndex));
1180
+ });
1181
+ return matches;
1182
+ }
1183
+ inferWildcardTargetColumn(root, query, wildcard, columnName, includeUnprovenPotential) {
1184
+ if (!query.fromClause) {
1185
+ return null;
1186
+ }
1187
+ const bindings = this.getSourceBindings(query.fromClause);
1188
+ if (wildcard.namespaces === null) {
1189
+ if (bindings.length !== 1) {
1190
+ return "ambiguous";
1191
+ }
1192
+ return this.resolveWildcardColumnFromBinding(root, query, bindings[0], columnName, includeUnprovenPotential);
1193
+ }
1194
+ const namespace = wildcard.getNamespace();
1195
+ const matches = bindings.filter(binding => identifiersEqual(binding.alias, namespace));
1196
+ if (matches.length === 0) {
1197
+ return null;
1198
+ }
1199
+ return matches.length === 1
1200
+ ? this.resolveWildcardColumnFromBinding(root, query, matches[0], columnName, includeUnprovenPotential)
1201
+ : "ambiguous";
1202
+ }
1203
+ resolveWildcardColumnFromBinding(root, query, binding, columnName, includeUnprovenPotential) {
1204
+ const matches = this.collectSourceOutputMatches(root, query, binding.source, columnName);
1205
+ if (matches.length > 1) {
1206
+ return "ambiguous";
1207
+ }
1208
+ if (matches.length === 1) {
1209
+ const value = matches[0].value;
1210
+ return value instanceof ColumnReference ? value : null;
1211
+ }
1212
+ return includeUnprovenPotential ? this.createColumnForBinding(binding, columnName) : null;
1213
+ }
1214
+ collectSourceOutputMatches(root, query, source, columnName) {
1215
+ var _a, _b, _c, _d;
1216
+ const commonTables = [
1217
+ ...((_b = (_a = query.withClause) === null || _a === void 0 ? void 0 : _a.tables) !== null && _b !== void 0 ? _b : []),
1218
+ ...((_d = (_c = root.withClause) === null || _c === void 0 ? void 0 : _c.tables) !== null && _d !== void 0 ? _d : [])
1219
+ ];
1220
+ const collector = new SelectOutputCollector(null, commonTables.length > 0 ? commonTables : null);
1221
+ return collector.collect(source).filter(item => identifiersEqual(item.name, columnName));
1222
+ }
1223
+ createColumnForBinding(binding, columnName) {
1224
+ return new ColumnReference(binding.alias ? [binding.alias] : null, columnName);
1225
+ }
1226
+ createInferredOutputColumn(name, value, outputIndex) {
1227
+ return {
1228
+ name,
1229
+ value,
1230
+ outputIndex,
1231
+ sourceAlias: value.getNamespace() || null,
1232
+ sourceName: null,
1233
+ sourceColumnName: name
1234
+ };
1235
+ }
1049
1236
  resolveSourceQueryForColumns(root, source) {
1050
1237
  var _a;
1051
1238
  if (source.datasource instanceof SubQuerySource) {
@@ -1404,8 +1591,8 @@ export class StaticPredicatePlacementOptimizer {
1404
1591
  visit(expression);
1405
1592
  return names;
1406
1593
  }
1407
- makeSkipped(expression, draft, options) {
1408
- return Object.assign({ predicateSql: formatSqlComponent(expression, options), scopeId: "scope:root" }, draft);
1594
+ makeSkipped(expression, scopeId, draft, options) {
1595
+ return Object.assign({ predicateSql: formatSqlComponent(expression, options), scopeId }, draft);
1409
1596
  }
1410
1597
  buildResult(params) {
1411
1598
  return {