rawsql-ts 0.25.0 → 0.26.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.
@@ -0,0 +1,942 @@
1
+ import { SubQuerySource, TableSource, WhereClause } from "../models/Clause";
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";
4
+ import { SelectQueryParser } from "../parsers/SelectQueryParser";
5
+ import { ValueParser } from "../parsers/ValueParser";
6
+ import { SqlFormatter } from "./SqlFormatter";
7
+ const SUPPORTED_OPERATORS = new Set(["=", "<>", "!=", "<", "<=", ">", ">=", "like", "ilike", "in"]);
8
+ const VOLATILE_OR_UNSUPPORTED_FUNCTION_REASON = "Condition 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 formatSqlComponent = (component) => {
11
+ return new SqlFormatter().format(component).formattedSql;
12
+ };
13
+ const unwrapParens = (expression) => {
14
+ let candidate = expression;
15
+ while (candidate instanceof ParenExpression) {
16
+ candidate = candidate.expression;
17
+ }
18
+ return candidate;
19
+ };
20
+ const isBinaryOperator = (expression, operator) => {
21
+ const candidate = unwrapParens(expression);
22
+ return candidate instanceof BinaryExpression
23
+ && candidate.operator.value.trim().toLowerCase() === operator;
24
+ };
25
+ const collectTopLevelAndTerms = (expression) => {
26
+ const candidate = unwrapParens(expression);
27
+ if (!isBinaryOperator(candidate, "and")) {
28
+ return [expression];
29
+ }
30
+ return [
31
+ ...collectTopLevelAndTerms(candidate.left),
32
+ ...collectTopLevelAndTerms(candidate.right)
33
+ ];
34
+ };
35
+ const rebuildWhereWithoutTerms = (query, termsToRemove) => {
36
+ if (!query.whereClause || termsToRemove.size === 0) {
37
+ return;
38
+ }
39
+ const remaining = collectTopLevelAndTerms(query.whereClause.condition)
40
+ .filter(term => !termsToRemove.has(term));
41
+ if (remaining.length === 0) {
42
+ query.whereClause = null;
43
+ return;
44
+ }
45
+ let rebuilt = remaining[0];
46
+ for (let index = 1; index < remaining.length; index += 1) {
47
+ rebuilt = new BinaryExpression(rebuilt, "and", remaining[index]);
48
+ }
49
+ query.whereClause = new WhereClause(rebuilt);
50
+ };
51
+ const cloneValueComponent = (expression) => {
52
+ return ValueParser.parse(formatSqlComponent(expression));
53
+ };
54
+ const cloneColumnReference = (reference) => {
55
+ var _a, _b;
56
+ const namespaces = (_b = (_a = reference.namespaces) === null || _a === void 0 ? void 0 : _a.map(namespace => namespace.name)) !== null && _b !== void 0 ? _b : null;
57
+ return new ColumnReference(namespaces, reference.column.name);
58
+ };
59
+ const columnReferenceText = (reference) => {
60
+ const namespace = reference.getNamespace();
61
+ return namespace ? `${namespace}.${reference.column.name}` : reference.column.name;
62
+ };
63
+ const sameColumnReference = (left, right) => {
64
+ return normalizeIdentifier(left.column.name) === normalizeIdentifier(right.column.name)
65
+ && normalizeIdentifier(left.getNamespace()) === normalizeIdentifier(right.getNamespace());
66
+ };
67
+ const appendUnique = (items, value) => {
68
+ if (!items.includes(value)) {
69
+ items.push(value);
70
+ }
71
+ };
72
+ export class ParameterConditionPlacementOptimizer {
73
+ plan(input, options = {}) {
74
+ var _a, _b, _c;
75
+ const parsed = this.parseInput(input);
76
+ const warnings = [...parsed.warnings];
77
+ const errors = [...parsed.errors];
78
+ if (!parsed.query) {
79
+ return this.buildResult({
80
+ sql: parsed.sql,
81
+ applied: [],
82
+ skipped: [],
83
+ warnings,
84
+ errors,
85
+ dryRun: (_a = options.dryRun) !== null && _a !== void 0 ? _a : true,
86
+ formatterGeneratedSource: parsed.formatterGeneratedSource
87
+ });
88
+ }
89
+ if (!(parsed.query instanceof SimpleSelectQuery)) {
90
+ warnings.push({
91
+ code: "UNSUPPORTED_ROOT_QUERY",
92
+ message: "Parameter condition placement currently supports only SimpleSelectQuery roots."
93
+ });
94
+ return this.buildResult({
95
+ sql: parsed.sql,
96
+ applied: [],
97
+ skipped: [],
98
+ warnings,
99
+ errors,
100
+ dryRun: (_b = options.dryRun) !== null && _b !== void 0 ? _b : true,
101
+ formatterGeneratedSource: parsed.formatterGeneratedSource
102
+ });
103
+ }
104
+ const query = parsed.query;
105
+ const applied = [];
106
+ const skipped = [];
107
+ const movedTerms = [];
108
+ for (const term of query.whereClause ? collectTopLevelAndTerms(query.whereClause.condition) : []) {
109
+ const candidate = this.analyzeCandidate(term);
110
+ if (!candidate) {
111
+ continue;
112
+ }
113
+ if ("code" in candidate) {
114
+ skipped.push(this.makeSkipped(term, candidate));
115
+ continue;
116
+ }
117
+ const target = this.resolveTarget(query, candidate);
118
+ if ("code" in target) {
119
+ skipped.push(this.makeSkipped(term, target));
120
+ continue;
121
+ }
122
+ const targetColumn = this.resolveTargetColumn(target.query, target.outputColumnName);
123
+ if ("code" in targetColumn) {
124
+ skipped.push(this.makeSkipped(term, targetColumn));
125
+ continue;
126
+ }
127
+ const movedCondition = this.rebaseCondition(candidate.expression, candidate.column, targetColumn.column);
128
+ target.query.appendWhere(movedCondition);
129
+ movedTerms.push(term);
130
+ applied.push({
131
+ kind: "move_condition",
132
+ conditionSql: candidate.conditionSql,
133
+ fromScopeId: "scope:root",
134
+ toScopeId: target.scopeId,
135
+ reason: "All referenced columns resolve to a single direct upstream output before unsafe query boundaries.",
136
+ parameterNames: candidate.parameterNames,
137
+ columnReferences: [columnReferenceText(candidate.column)]
138
+ });
139
+ }
140
+ rebuildWhereWithoutTerms(query, new Set(movedTerms));
141
+ const sql = applied.length > 0 ? formatSqlComponent(query) : parsed.sql;
142
+ return this.buildResult({
143
+ sql,
144
+ applied,
145
+ skipped,
146
+ warnings,
147
+ errors,
148
+ dryRun: (_c = options.dryRun) !== null && _c !== void 0 ? _c : true,
149
+ formatterGeneratedSource: parsed.formatterGeneratedSource
150
+ });
151
+ }
152
+ optimize(input, options = {}) {
153
+ var _a;
154
+ return this.plan(input, Object.assign(Object.assign({}, options), { dryRun: (_a = options.dryRun) !== null && _a !== void 0 ? _a : false }));
155
+ }
156
+ parseInput(input) {
157
+ const warnings = [];
158
+ const errors = [];
159
+ try {
160
+ const sourceSql = typeof input === "string"
161
+ ? input
162
+ : formatSqlComponent(input);
163
+ if (typeof input !== "string") {
164
+ warnings.push({
165
+ code: "AST_INPUT_FORMATTED",
166
+ message: "AST input is cloned through formatter output so the caller-owned query is not mutated."
167
+ });
168
+ }
169
+ return {
170
+ query: SelectQueryParser.parse(sourceSql),
171
+ sql: sourceSql,
172
+ formatterGeneratedSource: typeof input !== "string",
173
+ warnings,
174
+ errors
175
+ };
176
+ }
177
+ catch (error) {
178
+ const detail = error instanceof Error ? error.message : String(error);
179
+ errors.push({
180
+ code: "PARSE_FAILED",
181
+ message: "Parameter condition optimization could not parse the input SQL.",
182
+ detail
183
+ });
184
+ return {
185
+ query: null,
186
+ sql: typeof input === "string" ? input : "",
187
+ formatterGeneratedSource: typeof input !== "string",
188
+ warnings,
189
+ errors
190
+ };
191
+ }
192
+ }
193
+ analyzeCandidate(expression) {
194
+ const parameterNames = this.collectParameterNames(expression);
195
+ if (parameterNames.length === 0) {
196
+ return null;
197
+ }
198
+ const unsupported = this.findUnsupportedExpression(expression);
199
+ if (unsupported) {
200
+ return unsupported;
201
+ }
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
+ const columnReferences = this.collectColumnReferences(expression);
217
+ if (columnReferences.length !== 1) {
218
+ return {
219
+ 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."
223
+ };
224
+ }
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
+ };
230
+ }
231
+ return {
232
+ expression,
233
+ conditionSql: formatSqlComponent(expression),
234
+ parameterNames,
235
+ column: columnReferences[0]
236
+ };
237
+ }
238
+ findUnsupportedExpression(expression) {
239
+ let found = null;
240
+ const visit = (value) => {
241
+ if (found) {
242
+ return;
243
+ }
244
+ const candidate = unwrapParens(value);
245
+ 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
+ visit(candidate.left);
254
+ visit(candidate.right);
255
+ return;
256
+ }
257
+ if (candidate instanceof FunctionCall) {
258
+ found = {
259
+ code: "FUNCTION_PREDICATE_UNSUPPORTED",
260
+ reason: VOLATILE_OR_UNSUPPORTED_FUNCTION_REASON
261
+ };
262
+ return;
263
+ }
264
+ if (candidate instanceof CaseExpression) {
265
+ found = {
266
+ code: "CASE_PREDICATE_UNSUPPORTED",
267
+ reason: "CASE predicates are not moved in the first safe-only implementation."
268
+ };
269
+ return;
270
+ }
271
+ if (candidate instanceof InlineQuery || candidate instanceof ArrayQueryExpression) {
272
+ found = {
273
+ code: "SUBQUERY_PREDICATE_UNSUPPORTED",
274
+ reason: "Subquery predicates are not moved in the first safe-only implementation."
275
+ };
276
+ return;
277
+ }
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
+ if (candidate instanceof UnaryExpression) {
286
+ visit(candidate.expression);
287
+ return;
288
+ }
289
+ if (candidate instanceof CastExpression) {
290
+ visit(candidate.input);
291
+ return;
292
+ }
293
+ if (candidate instanceof JsonPredicateExpression) {
294
+ visit(candidate.expression);
295
+ return;
296
+ }
297
+ if (candidate instanceof ArrayExpression) {
298
+ visit(candidate.expression);
299
+ return;
300
+ }
301
+ if (candidate instanceof ValueList) {
302
+ candidate.values.forEach(visit);
303
+ return;
304
+ }
305
+ if (candidate instanceof TupleExpression) {
306
+ candidate.values.forEach(visit);
307
+ return;
308
+ }
309
+ if (candidate instanceof TypeValue && candidate.argument) {
310
+ visit(candidate.argument);
311
+ }
312
+ };
313
+ visit(expression);
314
+ return found;
315
+ }
316
+ isSupportedInPredicate(expression) {
317
+ const left = unwrapParens(expression.left);
318
+ const right = unwrapParens(expression.right);
319
+ if (!(left instanceof ColumnReference)) {
320
+ return false;
321
+ }
322
+ if (right instanceof ParameterExpression) {
323
+ return true;
324
+ }
325
+ if (!(right instanceof ValueList)) {
326
+ return false;
327
+ }
328
+ return right.values.length > 0
329
+ && right.values.every(value => unwrapParens(value) instanceof ParameterExpression);
330
+ }
331
+ resolveTarget(root, candidate) {
332
+ const boundary = this.findCurrentQueryBoundary(root);
333
+ if (boundary) {
334
+ return boundary;
335
+ }
336
+ if (!root.fromClause) {
337
+ return {
338
+ code: "NO_FROM_CLAUSE",
339
+ reason: "Condition has no FROM source that can receive the predicate safely."
340
+ };
341
+ }
342
+ const bindingResult = this.resolveSourceBinding(root, candidate.column);
343
+ if ("code" in bindingResult) {
344
+ return bindingResult;
345
+ }
346
+ const nullableSide = this.findNullableSideBoundary(root.fromClause, bindingResult);
347
+ if (nullableSide) {
348
+ return nullableSide;
349
+ }
350
+ const upstream = this.resolveUpstreamQuery(root, bindingResult, candidate.column.column.name);
351
+ if ("code" in upstream) {
352
+ return upstream;
353
+ }
354
+ const targetBoundary = this.findTargetQueryBoundary(upstream.query);
355
+ if (targetBoundary) {
356
+ return targetBoundary;
357
+ }
358
+ return upstream;
359
+ }
360
+ findCurrentQueryBoundary(query) {
361
+ if (query.selectClause.distinct) {
362
+ return {
363
+ 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."
371
+ };
372
+ }
373
+ if (this.hasWindowUsage(query)) {
374
+ return {
375
+ code: "WINDOW_BOUNDARY",
376
+ reason: "Condition crosses WINDOW boundary; moving it may change semantics."
377
+ };
378
+ }
379
+ return null;
380
+ }
381
+ findTargetQueryBoundary(query) {
382
+ const commonBoundary = this.findCurrentQueryBoundary(query);
383
+ if (commonBoundary) {
384
+ return commonBoundary;
385
+ }
386
+ if (query.limitClause || query.offsetClause || query.fetchClause) {
387
+ return {
388
+ code: "ROW_LIMIT_BOUNDARY",
389
+ reason: "Condition crosses LIMIT/OFFSET/FETCH boundary; moving it may change row selection semantics."
390
+ };
391
+ }
392
+ if (query.fromClause && this.hasOuterJoin(query.fromClause)) {
393
+ return {
394
+ code: "OUTER_JOIN_BOUNDARY",
395
+ reason: "Target query contains an OUTER JOIN boundary that is not moved across in the safe-only implementation."
396
+ };
397
+ }
398
+ return null;
399
+ }
400
+ resolveSourceBinding(root, column) {
401
+ const fromClause = root.fromClause;
402
+ if (!fromClause) {
403
+ return {
404
+ code: "NO_FROM_CLAUSE",
405
+ reason: "Condition has no FROM source that can receive the predicate safely."
406
+ };
407
+ }
408
+ const bindings = this.getSourceBindings(fromClause);
409
+ const namespace = normalizeIdentifier(column.getNamespace());
410
+ const columnName = column.column.name;
411
+ if (namespace) {
412
+ const matches = bindings.filter(binding => normalizeIdentifier(binding.alias) === namespace);
413
+ if (matches.length !== 1) {
414
+ return {
415
+ code: "AMBIGUOUS_COLUMN_SOURCE",
416
+ reason: `Column source alias '${column.getNamespace()}' is not uniquely resolvable.`
417
+ };
418
+ }
419
+ if (this.resolveSourceQueryForColumns(root, matches[0].source) instanceof BinarySelectQuery) {
420
+ return matches[0];
421
+ }
422
+ const matchCount = this.getOutputColumnMatchCount(root, matches[0], columnName);
423
+ if (matchCount === 0) {
424
+ return {
425
+ code: "COLUMN_NOT_AVAILABLE_UPSTREAM",
426
+ reason: `Column '${columnReferenceText(column)}' is not a direct output of the referenced source.`
427
+ };
428
+ }
429
+ if (matchCount > 1) {
430
+ return {
431
+ code: "AMBIGUOUS_COLUMN_REFERENCE",
432
+ reason: `Column '${columnReferenceText(column)}' resolves to multiple outputs in the referenced source.`
433
+ };
434
+ }
435
+ return matches[0];
436
+ }
437
+ const matches = [];
438
+ let unionSource = null;
439
+ 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);
445
+ if (matchCount > 1) {
446
+ return {
447
+ code: "AMBIGUOUS_COLUMN_REFERENCE",
448
+ reason: `Column '${columnName}' resolves to multiple outputs in source '${binding.alias}'.`
449
+ };
450
+ }
451
+ if (matchCount === 1) {
452
+ matches.push(binding);
453
+ }
454
+ }
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
+ if (matches.length !== 1) {
462
+ return {
463
+ code: "AMBIGUOUS_COLUMN_REFERENCE",
464
+ reason: matches.length === 0
465
+ ? `Column '${columnName}' is not uniquely resolvable to a safe upstream source.`
466
+ : `Column '${columnName}' is ambiguous across source query blocks.`
467
+ };
468
+ }
469
+ return matches[0];
470
+ }
471
+ resolveUpstreamQuery(root, binding, outputColumnName) {
472
+ const source = binding.source.datasource;
473
+ if (source instanceof SubQuerySource) {
474
+ if (source.query instanceof SimpleSelectQuery) {
475
+ return {
476
+ query: source.query,
477
+ scopeId: `subquery:${binding.alias}`,
478
+ outputColumnName,
479
+ sourceBinding: binding
480
+ };
481
+ }
482
+ return {
483
+ code: "UNION_BOUNDARY",
484
+ reason: "Condition would need distribution into a UNION or non-simple subquery, which is unsupported."
485
+ };
486
+ }
487
+ if (!(source instanceof TableSource)) {
488
+ return {
489
+ code: "UNSUPPORTED_SOURCE",
490
+ reason: "Only CTE and simple derived-table sources can receive moved parameter conditions."
491
+ };
492
+ }
493
+ const cteName = source.table.name;
494
+ const commonTable = this.findCte(root, cteName);
495
+ if (!commonTable) {
496
+ return {
497
+ code: "NO_SAFE_UPSTREAM_QUERY",
498
+ reason: "The referenced source is a base table, so there is no upstream query block to move into."
499
+ };
500
+ }
501
+ const referenceCount = this.countTableSourceReferences(root, cteName);
502
+ if (referenceCount !== 1) {
503
+ return {
504
+ code: "CTE_REUSE_UNSUPPORTED",
505
+ reason: `CTE '${cteName}' is referenced ${referenceCount} times; moving a predicate into it may affect other consumers.`
506
+ };
507
+ }
508
+ 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
+ };
513
+ }
514
+ if (!(commonTable.query instanceof SimpleSelectQuery)) {
515
+ return {
516
+ code: "UNSUPPORTED_CTE_QUERY",
517
+ reason: "Writable or non-select CTE bodies are not moved into by parameter condition placement."
518
+ };
519
+ }
520
+ return {
521
+ query: commonTable.query,
522
+ scopeId: `cte:${commonTable.getSourceAliasName()}`,
523
+ outputColumnName,
524
+ sourceBinding: binding,
525
+ cteName: commonTable.getSourceAliasName()
526
+ };
527
+ }
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) {
531
+ 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.`
536
+ };
537
+ }
538
+ const value = matches[0].value;
539
+ if (!(value instanceof ColumnReference)) {
540
+ return {
541
+ code: "EXPRESSION_OUTPUT_UNSUPPORTED",
542
+ reason: `Target output '${outputColumnName}' is an expression, not a direct column reference.`
543
+ };
544
+ }
545
+ const sourceResolution = this.verifyColumnResolvableInQuery(query, value);
546
+ if (sourceResolution) {
547
+ return sourceResolution;
548
+ }
549
+ return { column: value };
550
+ }
551
+ verifyColumnResolvableInQuery(query, column) {
552
+ if (!query.fromClause) {
553
+ return {
554
+ code: "NO_TARGET_FROM_CLAUSE",
555
+ reason: "Target query has no FROM clause where the moved column can be resolved."
556
+ };
557
+ }
558
+ const bindings = this.getSourceBindings(query.fromClause);
559
+ const namespace = normalizeIdentifier(column.getNamespace());
560
+ if (namespace) {
561
+ const matches = bindings.filter(binding => normalizeIdentifier(binding.alias) === namespace);
562
+ return matches.length === 1
563
+ ? null
564
+ : {
565
+ code: "AMBIGUOUS_TARGET_COLUMN",
566
+ reason: `Target column '${columnReferenceText(column)}' is not uniquely resolvable in the destination query.`
567
+ };
568
+ }
569
+ if (bindings.length === 1) {
570
+ return null;
571
+ }
572
+ return {
573
+ code: "AMBIGUOUS_TARGET_COLUMN",
574
+ reason: `Unqualified target column '${column.column.name}' is ambiguous in a multi-source destination query.`
575
+ };
576
+ }
577
+ rebaseCondition(expression, sourceColumn, targetColumn) {
578
+ const cloned = cloneValueComponent(expression);
579
+ for (const reference of this.collectColumnReferences(cloned)) {
580
+ if (!sameColumnReference(reference, sourceColumn)) {
581
+ continue;
582
+ }
583
+ reference.qualifiedName = cloneColumnReference(targetColumn).qualifiedName;
584
+ }
585
+ return cloned;
586
+ }
587
+ getSourceBindings(fromClause) {
588
+ var _a, _b, _c;
589
+ const bindings = [{
590
+ source: fromClause.source,
591
+ alias: (_a = fromClause.source.getAliasName()) !== null && _a !== void 0 ? _a : "",
592
+ join: null,
593
+ isPrimary: true
594
+ }];
595
+ for (const join of (_b = fromClause.joins) !== null && _b !== void 0 ? _b : []) {
596
+ bindings.push({
597
+ source: join.source,
598
+ alias: (_c = join.source.getAliasName()) !== null && _c !== void 0 ? _c : "",
599
+ join,
600
+ isPrimary: false
601
+ });
602
+ }
603
+ return bindings;
604
+ }
605
+ findNullableSideBoundary(fromClause, binding) {
606
+ var _a;
607
+ if (!binding.join) {
608
+ const rightOrFull = ((_a = fromClause.joins) !== null && _a !== void 0 ? _a : []).some(join => {
609
+ const joinType = join.joinType.value.toLowerCase();
610
+ return joinType.includes("right") || joinType.includes("full");
611
+ });
612
+ return rightOrFull
613
+ ? {
614
+ code: "OUTER_JOIN_NULLABLE_SIDE",
615
+ reason: "Condition crosses OUTER JOIN nullable side; moving it may change semantics."
616
+ }
617
+ : null;
618
+ }
619
+ const joinType = binding.join.joinType.value.toLowerCase();
620
+ if (joinType.includes("left")) {
621
+ return {
622
+ code: "OUTER_JOIN_NULLABLE_SIDE",
623
+ reason: "Condition crosses LEFT JOIN nullable side; moving it may change semantics."
624
+ };
625
+ }
626
+ if (joinType.includes("full")) {
627
+ return {
628
+ code: "OUTER_JOIN_NULLABLE_SIDE",
629
+ reason: "Condition crosses FULL JOIN nullable side; moving it may change semantics."
630
+ };
631
+ }
632
+ return null;
633
+ }
634
+ hasOuterJoin(fromClause) {
635
+ var _a;
636
+ return ((_a = fromClause.joins) !== null && _a !== void 0 ? _a : []).some(join => {
637
+ const joinType = join.joinType.value.toLowerCase();
638
+ return joinType.includes("left") || joinType.includes("right") || joinType.includes("full") || joinType.includes("outer");
639
+ });
640
+ }
641
+ getOutputColumnMatchCount(root, binding, columnName) {
642
+ const target = this.resolveSourceQueryForColumns(root, binding.source);
643
+ if (!target || !(target instanceof SimpleSelectQuery)) {
644
+ return 0;
645
+ }
646
+ return target.selectClause.items.filter(item => { var _a; return normalizeIdentifier((_a = this.getSelectItemOutputName(item)) !== null && _a !== void 0 ? _a : "") === normalizeIdentifier(columnName); }).length;
647
+ }
648
+ resolveSourceQueryForColumns(root, source) {
649
+ var _a;
650
+ if (source.datasource instanceof SubQuerySource) {
651
+ return source.datasource.query;
652
+ }
653
+ if (source.datasource instanceof TableSource) {
654
+ const cteQuery = (_a = this.findCte(root, source.datasource.table.name)) === null || _a === void 0 ? void 0 : _a.query;
655
+ return cteQuery instanceof SimpleSelectQuery || cteQuery instanceof BinarySelectQuery
656
+ ? cteQuery
657
+ : null;
658
+ }
659
+ return null;
660
+ }
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
+ findCte(root, name) {
671
+ var _a, _b;
672
+ const normalized = normalizeIdentifier(name);
673
+ const matches = ((_b = (_a = root.withClause) === null || _a === void 0 ? void 0 : _a.tables) !== null && _b !== void 0 ? _b : [])
674
+ .filter(table => normalizeIdentifier(table.getSourceAliasName()) === normalized);
675
+ return matches.length === 1 ? matches[0] : null;
676
+ }
677
+ countTableSourceReferences(query, tableName) {
678
+ const normalized = normalizeIdentifier(tableName);
679
+ let count = 0;
680
+ const visitSelect = (select) => {
681
+ var _a, _b;
682
+ if (select instanceof BinarySelectQuery) {
683
+ visitSelect(select.left);
684
+ visitSelect(select.right);
685
+ return;
686
+ }
687
+ if (!(select instanceof SimpleSelectQuery)) {
688
+ return;
689
+ }
690
+ if (select.fromClause) {
691
+ for (const binding of this.getSourceBindings(select.fromClause)) {
692
+ const source = binding.source.datasource;
693
+ if (source instanceof TableSource && normalizeIdentifier(source.table.name) === normalized) {
694
+ count += 1;
695
+ }
696
+ if (source instanceof SubQuerySource) {
697
+ visitSelect(source.query);
698
+ }
699
+ }
700
+ }
701
+ for (const cte of (_b = (_a = select.withClause) === null || _a === void 0 ? void 0 : _a.tables) !== null && _b !== void 0 ? _b : []) {
702
+ if (cte.query instanceof SimpleSelectQuery || cte.query instanceof BinarySelectQuery) {
703
+ visitSelect(cte.query);
704
+ }
705
+ }
706
+ };
707
+ visitSelect(query);
708
+ return count;
709
+ }
710
+ hasWindowUsage(query) {
711
+ if (query.windowClause) {
712
+ return true;
713
+ }
714
+ let found = false;
715
+ const visit = (value) => {
716
+ if (found) {
717
+ return;
718
+ }
719
+ const candidate = unwrapParens(value);
720
+ if (candidate instanceof FunctionCall) {
721
+ if (candidate.over) {
722
+ found = true;
723
+ return;
724
+ }
725
+ if (candidate.argument) {
726
+ visit(candidate.argument);
727
+ }
728
+ if (candidate.filterCondition) {
729
+ visit(candidate.filterCondition);
730
+ }
731
+ return;
732
+ }
733
+ if (candidate instanceof BinaryExpression) {
734
+ visit(candidate.left);
735
+ visit(candidate.right);
736
+ return;
737
+ }
738
+ if (candidate instanceof UnaryExpression) {
739
+ visit(candidate.expression);
740
+ return;
741
+ }
742
+ if (candidate instanceof CastExpression) {
743
+ visit(candidate.input);
744
+ return;
745
+ }
746
+ if (candidate instanceof CaseExpression) {
747
+ if (candidate.condition) {
748
+ visit(candidate.condition);
749
+ }
750
+ for (const pair of candidate.switchCase.cases) {
751
+ visit(pair.key);
752
+ visit(pair.value);
753
+ }
754
+ if (candidate.switchCase.elseValue) {
755
+ visit(candidate.switchCase.elseValue);
756
+ }
757
+ return;
758
+ }
759
+ if (candidate instanceof ValueList) {
760
+ candidate.values.forEach(visit);
761
+ }
762
+ };
763
+ query.selectClause.items.forEach(item => visit(item.value));
764
+ return found;
765
+ }
766
+ collectColumnReferences(expression) {
767
+ const references = [];
768
+ const visit = (value) => {
769
+ const candidate = unwrapParens(value);
770
+ if (candidate instanceof ColumnReference) {
771
+ if (!references.some(existing => sameColumnReference(existing, candidate))) {
772
+ references.push(candidate);
773
+ }
774
+ return;
775
+ }
776
+ if (candidate instanceof BinaryExpression) {
777
+ visit(candidate.left);
778
+ visit(candidate.right);
779
+ return;
780
+ }
781
+ if (candidate instanceof UnaryExpression) {
782
+ visit(candidate.expression);
783
+ return;
784
+ }
785
+ if (candidate instanceof FunctionCall) {
786
+ if (candidate.argument) {
787
+ visit(candidate.argument);
788
+ }
789
+ if (candidate.filterCondition) {
790
+ visit(candidate.filterCondition);
791
+ }
792
+ return;
793
+ }
794
+ if (candidate instanceof CastExpression) {
795
+ visit(candidate.input);
796
+ return;
797
+ }
798
+ if (candidate instanceof CaseExpression) {
799
+ if (candidate.condition) {
800
+ visit(candidate.condition);
801
+ }
802
+ for (const pair of candidate.switchCase.cases) {
803
+ visit(pair.key);
804
+ visit(pair.value);
805
+ }
806
+ if (candidate.switchCase.elseValue) {
807
+ visit(candidate.switchCase.elseValue);
808
+ }
809
+ return;
810
+ }
811
+ if (candidate instanceof BetweenExpression) {
812
+ visit(candidate.expression);
813
+ visit(candidate.lower);
814
+ visit(candidate.upper);
815
+ return;
816
+ }
817
+ if (candidate instanceof JsonPredicateExpression) {
818
+ visit(candidate.expression);
819
+ return;
820
+ }
821
+ if (candidate instanceof ArrayExpression) {
822
+ visit(candidate.expression);
823
+ return;
824
+ }
825
+ if (candidate instanceof ValueList) {
826
+ candidate.values.forEach(visit);
827
+ return;
828
+ }
829
+ if (candidate instanceof TupleExpression) {
830
+ candidate.values.forEach(visit);
831
+ return;
832
+ }
833
+ if (candidate instanceof TypeValue && candidate.argument) {
834
+ visit(candidate.argument);
835
+ }
836
+ };
837
+ visit(expression);
838
+ return references;
839
+ }
840
+ collectParameterNames(expression) {
841
+ const names = [];
842
+ const visit = (value) => {
843
+ const candidate = unwrapParens(value);
844
+ if (candidate instanceof ParameterExpression) {
845
+ appendUnique(names, candidate.name.value);
846
+ return;
847
+ }
848
+ if (candidate instanceof BinaryExpression) {
849
+ visit(candidate.left);
850
+ visit(candidate.right);
851
+ return;
852
+ }
853
+ if (candidate instanceof UnaryExpression) {
854
+ visit(candidate.expression);
855
+ return;
856
+ }
857
+ if (candidate instanceof FunctionCall) {
858
+ if (candidate.argument) {
859
+ visit(candidate.argument);
860
+ }
861
+ if (candidate.filterCondition) {
862
+ visit(candidate.filterCondition);
863
+ }
864
+ return;
865
+ }
866
+ if (candidate instanceof CastExpression) {
867
+ visit(candidate.input);
868
+ return;
869
+ }
870
+ if (candidate instanceof CaseExpression) {
871
+ if (candidate.condition) {
872
+ visit(candidate.condition);
873
+ }
874
+ for (const pair of candidate.switchCase.cases) {
875
+ visit(pair.key);
876
+ visit(pair.value);
877
+ }
878
+ if (candidate.switchCase.elseValue) {
879
+ visit(candidate.switchCase.elseValue);
880
+ }
881
+ return;
882
+ }
883
+ if (candidate instanceof BetweenExpression) {
884
+ visit(candidate.expression);
885
+ visit(candidate.lower);
886
+ visit(candidate.upper);
887
+ return;
888
+ }
889
+ if (candidate instanceof JsonPredicateExpression) {
890
+ visit(candidate.expression);
891
+ return;
892
+ }
893
+ if (candidate instanceof ArrayExpression) {
894
+ visit(candidate.expression);
895
+ return;
896
+ }
897
+ if (candidate instanceof ArrayQueryExpression || candidate instanceof InlineQuery) {
898
+ return;
899
+ }
900
+ if (candidate instanceof ValueList) {
901
+ candidate.values.forEach(visit);
902
+ return;
903
+ }
904
+ if (candidate instanceof TupleExpression) {
905
+ candidate.values.forEach(visit);
906
+ return;
907
+ }
908
+ if (candidate instanceof TypeValue && candidate.argument) {
909
+ visit(candidate.argument);
910
+ }
911
+ };
912
+ visit(expression);
913
+ return names;
914
+ }
915
+ makeSkipped(expression, draft) {
916
+ return Object.assign({ conditionSql: formatSqlComponent(expression), scopeId: "scope:root" }, draft);
917
+ }
918
+ buildResult(params) {
919
+ return {
920
+ ok: params.errors.length === 0,
921
+ sql: params.sql,
922
+ applied: params.applied,
923
+ skipped: params.skipped,
924
+ warnings: params.warnings,
925
+ errors: params.errors,
926
+ safety: {
927
+ mode: "safe_only",
928
+ unsafeRewriteApplied: false,
929
+ dryRun: params.dryRun,
930
+ formatterGeneratedSource: params.formatterGeneratedSource
931
+ },
932
+ conditionMoves: params.applied
933
+ };
934
+ }
935
+ }
936
+ export const planParameterConditionOptimization = (input, options = {}) => {
937
+ return new ParameterConditionPlacementOptimizer().plan(input, options);
938
+ };
939
+ export const optimizeParameterConditionPlacement = (input, options = {}) => {
940
+ return new ParameterConditionPlacementOptimizer().optimize(input, options);
941
+ };
942
+ //# sourceMappingURL=ParameterConditionPlacementOptimizer.js.map