rawsql-ts 0.29.0 → 0.30.1

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 (40) 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/ConditionOptimization.d.ts +16 -0
  7. package/dist/esm/transformers/ConditionOptimization.js +124 -2
  8. package/dist/esm/transformers/ConditionOptimization.js.map +1 -1
  9. package/dist/esm/transformers/ParameterConditionPlacementOptimizer.js +3 -71
  10. package/dist/esm/transformers/ParameterConditionPlacementOptimizer.js.map +1 -1
  11. package/dist/esm/transformers/PredicateExpressionUtils.d.ts +16 -0
  12. package/dist/esm/transformers/PredicateExpressionUtils.js +74 -0
  13. package/dist/esm/transformers/PredicateExpressionUtils.js.map +1 -0
  14. package/dist/esm/transformers/PredicateReachabilityAnalyzer.d.ts +92 -0
  15. package/dist/esm/transformers/PredicateReachabilityAnalyzer.js +516 -0
  16. package/dist/esm/transformers/PredicateReachabilityAnalyzer.js.map +1 -0
  17. package/dist/esm/transformers/StaticPredicatePlacementOptimizer.d.ts +2 -0
  18. package/dist/esm/transformers/StaticPredicatePlacementOptimizer.js +156 -162
  19. package/dist/esm/transformers/StaticPredicatePlacementOptimizer.js.map +1 -1
  20. package/dist/index.js +1 -0
  21. package/dist/index.js.map +1 -1
  22. package/dist/index.min.js +2 -2
  23. package/dist/index.min.js.map +4 -4
  24. package/dist/src/index.d.ts +1 -0
  25. package/dist/src/transformers/ConditionOptimization.d.ts +16 -0
  26. package/dist/src/transformers/PredicateExpressionUtils.d.ts +16 -0
  27. package/dist/src/transformers/PredicateReachabilityAnalyzer.d.ts +92 -0
  28. package/dist/src/transformers/StaticPredicatePlacementOptimizer.d.ts +2 -0
  29. package/dist/transformers/ConditionOptimization.js +138 -2
  30. package/dist/transformers/ConditionOptimization.js.map +1 -1
  31. package/dist/transformers/ParameterConditionPlacementOptimizer.js +38 -106
  32. package/dist/transformers/ParameterConditionPlacementOptimizer.js.map +1 -1
  33. package/dist/transformers/PredicateExpressionUtils.js +88 -0
  34. package/dist/transformers/PredicateExpressionUtils.js.map +1 -0
  35. package/dist/transformers/PredicateReachabilityAnalyzer.js +537 -0
  36. package/dist/transformers/PredicateReachabilityAnalyzer.js.map +1 -0
  37. package/dist/transformers/StaticPredicatePlacementOptimizer.js +190 -196
  38. package/dist/transformers/StaticPredicatePlacementOptimizer.js.map +1 -1
  39. package/dist/tsconfig.browser.tsbuildinfo +1 -1
  40. package/package.json +1 -1
@@ -1,80 +1,12 @@
1
- import { DistinctOn, JoinOnClause, 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";
8
7
  import { dedupeTopLevelAndConditions, dedupeWhereTopLevelAndConditions } from "./TopLevelAndConditionDeduper";
8
+ import { appendUnique, cloneColumnReference, cloneValueComponent, collectTopLevelAndTerms, columnReferenceText, identifiersEqual, normalizeIdentifier, rebuildWhereWithoutTerms, sameColumnReference, unwrapParens } from "./PredicateExpressionUtils";
9
9
  const VOLATILE_OR_UNSUPPORTED_FUNCTION_REASON = "Predicate contains a function call; volatile and expression predicates are not moved in the safe-only implementation.";
10
- const normalizeIdentifier = (value) => value.trim().toLowerCase();
11
- const isCaseSensitiveIdentifier = (value) => /[A-Z]/.test(value.trim());
12
- const identifiersEqual = (left, right) => {
13
- const trimmedLeft = left.trim();
14
- const trimmedRight = right.trim();
15
- return isCaseSensitiveIdentifier(trimmedLeft) || isCaseSensitiveIdentifier(trimmedRight)
16
- ? trimmedLeft === trimmedRight
17
- : trimmedLeft.toLowerCase() === trimmedRight.toLowerCase();
18
- };
19
- const unwrapParens = (expression) => {
20
- let candidate = expression;
21
- while (candidate instanceof ParenExpression) {
22
- candidate = candidate.expression;
23
- }
24
- return candidate;
25
- };
26
- const isBinaryOperator = (expression, operator) => {
27
- const candidate = unwrapParens(expression);
28
- return candidate instanceof BinaryExpression
29
- && candidate.operator.value.trim().toLowerCase() === operator;
30
- };
31
- const collectTopLevelAndTerms = (expression) => {
32
- const candidate = unwrapParens(expression);
33
- if (!isBinaryOperator(candidate, "and")) {
34
- return [expression];
35
- }
36
- return [
37
- ...collectTopLevelAndTerms(candidate.left),
38
- ...collectTopLevelAndTerms(candidate.right)
39
- ];
40
- };
41
- const rebuildWhereWithoutTerms = (query, termsToRemove) => {
42
- if (!query.whereClause || termsToRemove.size === 0) {
43
- return;
44
- }
45
- const remaining = collectTopLevelAndTerms(query.whereClause.condition)
46
- .filter(term => !termsToRemove.has(term));
47
- if (remaining.length === 0) {
48
- query.whereClause = null;
49
- return;
50
- }
51
- let rebuilt = remaining[0];
52
- for (let index = 1; index < remaining.length; index += 1) {
53
- rebuilt = new BinaryExpression(rebuilt, "and", remaining[index]);
54
- }
55
- query.whereClause = new WhereClause(rebuilt);
56
- };
57
- const cloneValueComponent = (expression, options) => {
58
- return ValueParser.parse(formatSqlComponent(expression, options));
59
- };
60
- const cloneColumnReference = (reference) => {
61
- var _a, _b;
62
- const namespaces = (_b = (_a = reference.namespaces) === null || _a === void 0 ? void 0 : _a.map(namespace => namespace.name)) !== null && _b !== void 0 ? _b : null;
63
- return new ColumnReference(namespaces, reference.column.name);
64
- };
65
- const columnReferenceText = (reference) => {
66
- const namespace = reference.getNamespace();
67
- return namespace ? `${namespace}.${reference.column.name}` : reference.column.name;
68
- };
69
- const sameColumnReference = (left, right) => {
70
- return identifiersEqual(left.column.name, right.column.name)
71
- && identifiersEqual(left.getNamespace(), right.getNamespace());
72
- };
73
- const appendUnique = (items, value) => {
74
- if (!items.includes(value)) {
75
- items.push(value);
76
- }
77
- };
78
10
  export class StaticPredicatePlacementOptimizer {
79
11
  plan(input, options = {}) {
80
12
  var _a, _b, _c;
@@ -112,77 +44,8 @@ export class StaticPredicatePlacementOptimizer {
112
44
  const query = parsed.query;
113
45
  const applied = [];
114
46
  const skipped = [];
115
- const movedTerms = [];
116
- for (const term of query.whereClause ? collectTopLevelAndTerms(query.whereClause.condition) : []) {
117
- const candidate = this.analyzeCandidate(query, term, options);
118
- if (!candidate) {
119
- continue;
120
- }
121
- if ("code" in candidate) {
122
- skipped.push(this.makeSkipped(term, candidate, options));
123
- continue;
124
- }
125
- const target = this.resolveTarget(query, candidate);
126
- if ("code" in target) {
127
- skipped.push(this.makeSkipped(term, target, options));
128
- continue;
129
- }
130
- let appliedReason = "";
131
- if (target.kind === "join_on") {
132
- this.appendJoinOnPredicate(target.join, candidate.expression, options);
133
- appliedReason = target.reason;
134
- }
135
- else if (target.kind === "simple") {
136
- const targetColumns = this.resolveTargetColumns(query, target.query, candidate.references);
137
- if ("code" in targetColumns) {
138
- skipped.push(this.makeSkipped(term, targetColumns, options));
139
- continue;
140
- }
141
- const placement = this.resolveTargetPlacement(target.query, targetColumns);
142
- if ("code" in placement) {
143
- skipped.push(this.makeSkipped(term, placement, options));
144
- continue;
145
- }
146
- const movedPredicate = this.rebasePredicate(candidate.expression, targetColumns, options);
147
- target.query.appendWhere(movedPredicate);
148
- dedupeWhereTopLevelAndConditions(target.query, options);
149
- appliedReason = placement.reason;
150
- }
151
- else {
152
- const placements = [];
153
- let skip = null;
154
- for (const branch of target.branches) {
155
- const placement = this.resolveTargetPlacement(branch.query, branch.targetColumns);
156
- if ("code" in placement) {
157
- skip = placement;
158
- break;
159
- }
160
- placements.push(placement);
161
- }
162
- if (skip) {
163
- skipped.push(this.makeSkipped(term, skip, options));
164
- continue;
165
- }
166
- for (const branch of target.branches) {
167
- const movedPredicate = this.rebasePredicate(candidate.expression, branch.targetColumns, options);
168
- branch.query.appendWhere(movedPredicate);
169
- dedupeWhereTopLevelAndConditions(branch.query, options);
170
- }
171
- appliedReason = placements.some(item => /group by/i.test(item.reason))
172
- ? "Predicate is distributed to every UNION branch by output column position; grouped branches only receive GROUP BY-key predicates."
173
- : "Predicate is distributed to every UNION branch by output column position before unsafe query boundaries.";
174
- }
175
- movedTerms.push(term);
176
- applied.push({
177
- kind: "move_static_predicate",
178
- predicateSql: candidate.predicateSql,
179
- fromScopeId: "scope:root",
180
- toScopeId: target.scopeId,
181
- reason: appliedReason,
182
- columnReferences: candidate.references.map(columnReferenceText)
183
- });
184
- }
185
- 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.
186
49
  const sql = applied.length > 0 || hasSqlComponentFormatOverride(options)
187
50
  ? formatSqlComponent(query, options)
188
51
  : parsed.sql;
@@ -197,6 +60,121 @@ export class StaticPredicatePlacementOptimizer {
197
60
  formatterGeneratedSource: parsed.formatterGeneratedSource
198
61
  });
199
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
+ }
200
178
  optimize(input, options = {}) {
201
179
  var _a;
202
180
  return this.plan(input, Object.assign(Object.assign({}, options), { dryRun: (_a = options.dryRun) !== null && _a !== void 0 ? _a : false }));
@@ -392,13 +370,13 @@ export class StaticPredicatePlacementOptimizer {
392
370
  visit(expression);
393
371
  return found;
394
372
  }
395
- resolveTarget(root, candidate) {
373
+ resolveTarget(contextRoot, currentQuery, candidate) {
396
374
  var _a;
397
- const boundary = this.findRootQueryBoundary(root);
375
+ const boundary = this.findRootQueryBoundary(currentQuery);
398
376
  if (boundary) {
399
377
  return boundary;
400
378
  }
401
- if (!root.fromClause) {
379
+ if (!currentQuery.fromClause) {
402
380
  return {
403
381
  code: "NO_FROM_CLAUSE",
404
382
  reason: "Predicate has no FROM source that can receive it safely."
@@ -406,7 +384,7 @@ export class StaticPredicatePlacementOptimizer {
406
384
  }
407
385
  const bindings = [];
408
386
  for (const reference of candidate.references) {
409
- const binding = this.resolveSourceBinding(root, root, reference);
387
+ const binding = this.resolveSourceBinding(contextRoot, currentQuery, reference);
410
388
  if ("code" in binding) {
411
389
  return binding;
412
390
  }
@@ -427,14 +405,14 @@ export class StaticPredicatePlacementOptimizer {
427
405
  reason: "Predicate crosses a LATERAL JOIN boundary; moving it may change semantics."
428
406
  };
429
407
  }
430
- const nullableSide = this.findNullableSideBoundary(root.fromClause, binding);
408
+ const nullableSide = this.findNullableSideBoundary(currentQuery.fromClause, binding);
431
409
  if (nullableSide) {
432
410
  return nullableSide;
433
411
  }
434
- const upstream = this.resolveUpstreamQuery(root, binding, candidate.references);
412
+ const upstream = this.resolveUpstreamQuery(contextRoot, binding, candidate.references);
435
413
  if ("code" in upstream) {
436
414
  if (upstream.code === "NO_SAFE_UPSTREAM_QUERY") {
437
- const joinOnTarget = this.resolveBaseTableJoinOnTarget(root, binding);
415
+ const joinOnTarget = this.resolveBaseTableJoinOnTarget(contextRoot, currentQuery, binding);
438
416
  if (!("code" in joinOnTarget)) {
439
417
  return joinOnTarget;
440
418
  }
@@ -459,7 +437,7 @@ export class StaticPredicatePlacementOptimizer {
459
437
  }
460
438
  return null;
461
439
  }
462
- resolveTargetPlacement(query, targetColumns) {
440
+ resolveTargetPlacement(contextRoot, query, targetColumns) {
463
441
  const hasOrdinaryDistinct = this.hasOrdinaryDistinct(query);
464
442
  if (this.hasDistinctOnBoundary(query)) {
465
443
  return {
@@ -479,11 +457,11 @@ export class StaticPredicatePlacementOptimizer {
479
457
  reason: "Predicate crosses LIMIT/OFFSET/FETCH boundary; moving it may change row selection semantics."
480
458
  };
481
459
  }
482
- if (query.fromClause && this.hasOuterJoin(query.fromClause)) {
483
- return {
484
- code: "OUTER_JOIN_BOUNDARY",
485
- reason: "Target query contains an OUTER JOIN boundary that is not moved across in the safe-only implementation."
486
- };
460
+ if (query.fromClause) {
461
+ const nullableSide = this.findNullableSideBoundaryForTargetColumns(contextRoot, query, targetColumns);
462
+ if (nullableSide) {
463
+ return nullableSide;
464
+ }
487
465
  }
488
466
  if (query.groupByClause) {
489
467
  const allReferencesAreGroupKeys = targetColumns.every(item => this.isGroupKeyColumn(query, item.targetColumn));
@@ -512,6 +490,22 @@ export class StaticPredicatePlacementOptimizer {
512
490
  reason: "All outer references resolve to direct upstream outputs before unsafe query boundaries."
513
491
  };
514
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
+ }
515
509
  resolveSourceBinding(contextRoot, query, column) {
516
510
  const fromClause = query.fromClause;
517
511
  if (!fromClause) {
@@ -690,22 +684,22 @@ export class StaticPredicatePlacementOptimizer {
690
684
  }
691
685
  return resolved;
692
686
  }
693
- resolveBaseTableJoinOnTarget(root, binding) {
687
+ resolveBaseTableJoinOnTarget(contextRoot, currentQuery, binding) {
694
688
  var _a, _b, _c;
695
- if (!root.fromClause || !this.isBaseTableBinding(root, binding)) {
689
+ if (!currentQuery.fromClause || !this.isBaseTableBinding(contextRoot, binding)) {
696
690
  return {
697
691
  code: "NO_SAFE_UPSTREAM_QUERY",
698
692
  reason: "The referenced source is a base table, so there is no upstream query block to move into."
699
693
  };
700
694
  }
701
- if (this.hasOuterJoin(root.fromClause)) {
695
+ if (this.hasOuterJoin(currentQuery.fromClause)) {
702
696
  return {
703
697
  code: "OUTER_JOIN_BOUNDARY",
704
698
  reason: "Predicate crosses an OUTER JOIN boundary; moving it into JOIN ON may change semantics."
705
699
  };
706
700
  }
707
701
  const join = binding.isPrimary
708
- ? (_b = (_a = root.fromClause.joins) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : null
702
+ ? (_b = (_a = currentQuery.fromClause.joins) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : null
709
703
  : binding.join;
710
704
  if (join === null || join === void 0 ? void 0 : join.lateral) {
711
705
  return {
@@ -778,7 +772,7 @@ export class StaticPredicatePlacementOptimizer {
778
772
  if ("code" in upstreamColumns) {
779
773
  return { query, targetColumns: [...targetColumns] };
780
774
  }
781
- const placement = this.resolveTargetPlacement(upstream.query, upstreamColumns);
775
+ const placement = this.resolveTargetPlacement(contextRoot, upstream.query, upstreamColumns);
782
776
  if ("code" in placement) {
783
777
  return { query, targetColumns: [...targetColumns] };
784
778
  }
@@ -1597,8 +1591,8 @@ export class StaticPredicatePlacementOptimizer {
1597
1591
  visit(expression);
1598
1592
  return names;
1599
1593
  }
1600
- makeSkipped(expression, draft, options) {
1601
- 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);
1602
1596
  }
1603
1597
  buildResult(params) {
1604
1598
  return {