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