@minnowdb/core 0.6.9 → 0.7.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/date-value.d.ts +5 -0
- package/dist/date-value.js +41 -0
- package/dist/engine/database.js +913 -54
- package/dist/engine/optimizer.js +446 -18
- package/dist/engine/point-read.d.ts +4 -2
- package/dist/engine/point-read.js +16 -6
- package/dist/engine/query.d.ts +39 -0
- package/dist/engine/query.js +811 -203
- package/dist/engine/sql-domains.d.ts +7 -1
- package/dist/engine/sql-domains.js +38 -1
- package/dist/engine/sql-functions.d.ts +11 -0
- package/dist/engine/sql-functions.js +1186 -0
- package/dist/engine/sql-semantics.d.ts +25 -4
- package/dist/engine/sql-semantics.js +134 -1
- package/dist/engine/vector.d.ts +7 -1
- package/dist/engine/vector.js +718 -124
- package/dist/plan/model.d.ts +3 -1
- package/dist/storage/types.js +44 -18
- package/package.json +1 -1
- package/postgres-feature-profile.json +15 -5
- package/sql-feature-matrix.json +252 -2
package/dist/engine/optimizer.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { dateIsoString, dateMilliseconds } from "../date-value.js";
|
|
2
2
|
import { crossJoinPlan } from "../plan/model.js";
|
|
3
|
-
import { blockHasSubqueries, childExpressions, DUAL_TABLE, expressionAliases, forEachBlockExpression, forEachNestedBlock, hasAggregate, isAggregateCall, isScalarFunctionName, mapBlockExpressions, mapChildExpressions, parseQuantified, scalarFunctionNames, scalarFunctionValue, splitCondition, volatileScalarFunctionNames, } from "./query.js";
|
|
3
|
+
import { blockHasSubqueries, childExpressions, DUAL_TABLE, expressionAliases, forEachBlockExpression, forEachNestedBlock, hasAggregate, isAggregateCall, isScalarFunctionName, mapBlockExpressions, mapChildExpressions, parseQuantified, scalarFunctionNames, scalarFunctionValue, splitCondition, volatileScalarFunctionNames, transparentProjectionSource, dateTruncValue, } from "./query.js";
|
|
4
4
|
import { concatenatedSqlValue, isSqlDomainValue } from "./sql-domains.js";
|
|
5
|
+
import { simpleScalarFunctions } from "./sql-functions.js";
|
|
5
6
|
/**
|
|
6
7
|
* Deterministic plan-to-plan rewrites over the shared compiled representation. Every rule
|
|
7
8
|
* preserves result semantics exactly; rules that cannot prove safety leave the plan unchanged.
|
|
@@ -175,52 +176,145 @@ function optimizeBlock(block, nextCorrelationAlias) {
|
|
|
175
176
|
extractJoinKeys(block);
|
|
176
177
|
normalizeBooleanPredicates(block);
|
|
177
178
|
coalesceOrEqualityLists(block);
|
|
179
|
+
rewriteCalendarEqualities(block);
|
|
180
|
+
propagateJoinKeyConstants(block);
|
|
178
181
|
pushPredicatesIntoDerived(block);
|
|
179
182
|
pruneDerivedProjections(block);
|
|
180
183
|
combineDerivedLimit(block);
|
|
181
184
|
}
|
|
182
|
-
/**
|
|
185
|
+
/**
|
|
186
|
+
* Converts correlated LATERAL derived tables into ordinary set-at-a-time joins. The inner
|
|
187
|
+
* block's correlation predicates become join keys; its grouping, per-row ordering, and LIMIT
|
|
188
|
+
* are kept by grouping on the keys and ranking within them, the same way a correlated scalar
|
|
189
|
+
* subquery is rewritten:
|
|
190
|
+
*
|
|
191
|
+
* - `GROUP BY` gains the equality keys, so each outer row still sees only its own group.
|
|
192
|
+
* - A global aggregate (`SELECT COUNT(*) …` with no GROUP BY) always yields one row per outer
|
|
193
|
+
* row, so the join becomes a left join and the outer block reads each aggregate through
|
|
194
|
+
* `CASE WHEN <key> IS NULL THEN <empty value> ELSE <column> END`: COUNT reads 0, others NULL.
|
|
195
|
+
* - `ORDER BY … LIMIT` becomes a row number partitioned by the keys, filtered to the window.
|
|
196
|
+
*
|
|
197
|
+
* Range correlations (`o.amount > c.threshold`) cannot be grouped or partitioned on, so those
|
|
198
|
+
* shapes keep the plain set-at-a-time join and are refused with grouping or LIMIT.
|
|
199
|
+
*/
|
|
183
200
|
function decorrelateLateralSources(block) {
|
|
184
201
|
if (block.base.lateral === true) {
|
|
185
202
|
throw new TypeError("LATERAL needs a source to its left");
|
|
186
203
|
}
|
|
187
204
|
const available = new Set([block.base.alias]);
|
|
205
|
+
let lateralSequence = 0;
|
|
188
206
|
for (const join of block.joins) {
|
|
189
207
|
if (join.lateral !== true) {
|
|
190
208
|
available.add(join.alias);
|
|
191
209
|
continue;
|
|
192
210
|
}
|
|
193
211
|
delete join.lateral;
|
|
194
|
-
const
|
|
195
|
-
if (
|
|
212
|
+
const derived = join.derived;
|
|
213
|
+
if (derived === undefined)
|
|
196
214
|
throw new TypeError("LATERAL requires a derived query");
|
|
197
|
-
if (!blockReferencesOutside(
|
|
215
|
+
if (!blockReferencesOutside(derived)) {
|
|
198
216
|
available.add(join.alias);
|
|
199
217
|
continue;
|
|
200
218
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
219
|
+
// An ORDER BY over an unselected expression parses as a projection over the ordered block;
|
|
220
|
+
// the correlation, grouping, and ranking live in that inner block, and the projection is
|
|
221
|
+
// widened afterwards so the join can reach the key columns it adds.
|
|
222
|
+
const wrapper = transparentProjectionSource(derived);
|
|
223
|
+
const inner = wrapper?.inner ?? derived;
|
|
207
224
|
const keys = extractCorrelation(inner, available, "LATERAL", true);
|
|
208
225
|
const keyAliases = keys.map((_, index) => `\u0000lateral_key_${String(index + 1)}`);
|
|
226
|
+
const grouped = inner.groupBy.length > 0;
|
|
227
|
+
const globalAggregate = !grouped &&
|
|
228
|
+
inner.select.some((item) => containsAggregateCall(item.expression)) &&
|
|
229
|
+
inner.having.length === 0;
|
|
230
|
+
const ranked = inner.limit !== undefined ||
|
|
231
|
+
inner.limitParameter !== undefined ||
|
|
232
|
+
inner.offset !== undefined ||
|
|
233
|
+
inner.offsetParameter !== undefined;
|
|
234
|
+
const cross = (join.on?.kind === "condition" &&
|
|
235
|
+
join.on.operator === "=" &&
|
|
236
|
+
join.on.left.kind === "literal" &&
|
|
237
|
+
join.on.left.value === 1 &&
|
|
238
|
+
join.on.right.kind === "literal" &&
|
|
239
|
+
join.on.right.value === 1) ||
|
|
240
|
+
(join.on?.kind === "literal" && join.on.value === true);
|
|
241
|
+
if (grouped || globalAggregate || ranked || inner.having.length > 0) {
|
|
242
|
+
if (!keys.every((key) => key.operator === "=")) {
|
|
243
|
+
throw new TypeError("Correlated LATERAL queries with grouping, HAVING, ORDER BY, or LIMIT need equality correlations");
|
|
244
|
+
}
|
|
245
|
+
if (inner.having.length > 0 && !grouped) {
|
|
246
|
+
throw new TypeError("HAVING in a correlated LATERAL query needs GROUP BY");
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (grouped || globalAggregate) {
|
|
250
|
+
for (const key of keys)
|
|
251
|
+
inner.groupBy.push(structuredClone(key.inner));
|
|
252
|
+
}
|
|
209
253
|
keys.forEach((key, index) => {
|
|
210
254
|
inner.select.push({ expression: key.inner, alias: keyAliases[index] ?? "" });
|
|
211
255
|
});
|
|
256
|
+
if (ranked) {
|
|
257
|
+
lateralSequence += 1;
|
|
258
|
+
const rankedRows = rankLateralRows(inner, keyAliases, lateralSequence);
|
|
259
|
+
if (wrapper === undefined)
|
|
260
|
+
join.derived = rankedRows;
|
|
261
|
+
else
|
|
262
|
+
derived.base.derived = rankedRows;
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
inner.orderBy = [];
|
|
266
|
+
}
|
|
267
|
+
if (wrapper !== undefined) {
|
|
268
|
+
for (const keyAlias of keyAliases) {
|
|
269
|
+
derived.select.push({
|
|
270
|
+
expression: { kind: "column", reference: `${derived.base.alias}.${keyAlias}` },
|
|
271
|
+
alias: keyAlias,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (globalAggregate) {
|
|
276
|
+
if (!cross) {
|
|
277
|
+
throw new TypeError("A correlated LATERAL aggregate without GROUP BY needs ON TRUE or a comma join");
|
|
278
|
+
}
|
|
279
|
+
join.kind = "left";
|
|
280
|
+
const keyReference = {
|
|
281
|
+
kind: "column",
|
|
282
|
+
reference: `${join.alias}.${keyAliases[0] ?? ""}`,
|
|
283
|
+
};
|
|
284
|
+
const replacements = new Map();
|
|
285
|
+
const visible = new Set(derived.select.map((item) => item.alias));
|
|
286
|
+
for (const item of inner.select) {
|
|
287
|
+
if (item.alias.startsWith("\u0000") || !visible.has(item.alias))
|
|
288
|
+
continue;
|
|
289
|
+
const empty = emptyScalarExpression(structuredClone(item.expression));
|
|
290
|
+
if (empty.kind === "literal" && empty.value === null)
|
|
291
|
+
continue;
|
|
292
|
+
replacements.set(`${join.alias}.${item.alias}`, {
|
|
293
|
+
kind: "case",
|
|
294
|
+
branches: [
|
|
295
|
+
{
|
|
296
|
+
when: {
|
|
297
|
+
kind: "condition",
|
|
298
|
+
operator: "IS NULL",
|
|
299
|
+
left: structuredClone(keyReference),
|
|
300
|
+
right: { kind: "literal", value: null },
|
|
301
|
+
},
|
|
302
|
+
then: empty,
|
|
303
|
+
},
|
|
304
|
+
],
|
|
305
|
+
otherwise: { kind: "column", reference: `${join.alias}.${item.alias}` },
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
if (replacements.size > 0) {
|
|
309
|
+
replaceOuterReferences(block, join.alias, replacements);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
212
312
|
const correlations = keys.map((key, index) => ({
|
|
213
313
|
kind: "condition",
|
|
214
314
|
operator: reverseComparison(key.operator),
|
|
215
315
|
left: key.outer,
|
|
216
316
|
right: { kind: "column", reference: `${join.alias}.${keyAliases[index] ?? ""}` },
|
|
217
317
|
}));
|
|
218
|
-
const cross = join.on?.kind === "condition" &&
|
|
219
|
-
join.on.operator === "=" &&
|
|
220
|
-
join.on.left.kind === "literal" &&
|
|
221
|
-
join.on.left.value === 1 &&
|
|
222
|
-
join.on.right.kind === "literal" &&
|
|
223
|
-
join.on.right.value === 1;
|
|
224
318
|
const original = cross
|
|
225
319
|
? undefined
|
|
226
320
|
: (join.on ?? {
|
|
@@ -252,6 +346,127 @@ function decorrelateLateralSources(block) {
|
|
|
252
346
|
available.add(join.alias);
|
|
253
347
|
}
|
|
254
348
|
}
|
|
349
|
+
/**
|
|
350
|
+
* Wraps a LATERAL inner block whose ORDER BY/LIMIT apply per outer row: the block's order
|
|
351
|
+
* expressions project as hidden columns, a row number (RANK for WITH TIES) partitioned by the
|
|
352
|
+
* correlation keys ranks them, and a filtering block keeps the requested window. The result
|
|
353
|
+
* exposes the inner block's own aliases, so the outer query's references still resolve.
|
|
354
|
+
*/
|
|
355
|
+
function rankLateralRows(inner, keyAliases, sequence) {
|
|
356
|
+
// An order term that names one of the block's own output aliases (the parser's hidden
|
|
357
|
+
// "(order n)" projections included) ranks on that column; any other term projects as a
|
|
358
|
+
// hidden column of its own.
|
|
359
|
+
const selectAliases = new Set(inner.select.map((item) => item.alias));
|
|
360
|
+
const orderAliases = inner.orderBy.map((order, index) => {
|
|
361
|
+
if (order.expression.kind === "column" &&
|
|
362
|
+
!order.expression.reference.includes(".") &&
|
|
363
|
+
selectAliases.has(order.expression.reference)) {
|
|
364
|
+
return order.expression.reference;
|
|
365
|
+
}
|
|
366
|
+
const alias = `\u0000lateral_order_${String(sequence)}_${String(index + 1)}`;
|
|
367
|
+
inner.select.push({ expression: structuredClone(order.expression), alias });
|
|
368
|
+
return alias;
|
|
369
|
+
});
|
|
370
|
+
const windows = [
|
|
371
|
+
{
|
|
372
|
+
alias: `\u0000lateral_row_${String(sequence)}`,
|
|
373
|
+
name: inner.limitWithTies === true ? "RANK" : "ROW_NUMBER",
|
|
374
|
+
partitionAliases: [...keyAliases],
|
|
375
|
+
orderAliases: inner.orderBy.map((order, index) => ({
|
|
376
|
+
alias: orderAliases[index] ?? "",
|
|
377
|
+
direction: order.direction,
|
|
378
|
+
...(order.nulls === undefined ? {} : { nulls: order.nulls }),
|
|
379
|
+
})),
|
|
380
|
+
},
|
|
381
|
+
];
|
|
382
|
+
const { limit, limitParameter, offset, offsetParameter, limitWithTies, ...rows } = inner;
|
|
383
|
+
void limitWithTies;
|
|
384
|
+
rows.orderBy = [];
|
|
385
|
+
const rankedAlias = `\u0000lateral_ranked_${String(sequence)}`;
|
|
386
|
+
const rowNumber = {
|
|
387
|
+
kind: "column",
|
|
388
|
+
reference: `${rankedAlias}.${windows[0]?.alias ?? ""}`,
|
|
389
|
+
};
|
|
390
|
+
const offsetExpression = offsetParameter === undefined
|
|
391
|
+
? { kind: "literal", value: offset ?? 0 }
|
|
392
|
+
: { kind: "parameter", index: offsetParameter };
|
|
393
|
+
const predicates = [];
|
|
394
|
+
if ((offset ?? 0) > 0 || offsetParameter !== undefined) {
|
|
395
|
+
predicates.push({ left: rowNumber, operator: ">", right: offsetExpression });
|
|
396
|
+
}
|
|
397
|
+
if (limit !== undefined || limitParameter !== undefined) {
|
|
398
|
+
const limitExpression = limitParameter === undefined
|
|
399
|
+
? { kind: "literal", value: limit ?? 0 }
|
|
400
|
+
: { kind: "parameter", index: limitParameter };
|
|
401
|
+
predicates.push({
|
|
402
|
+
left: structuredClone(rowNumber),
|
|
403
|
+
operator: "<=",
|
|
404
|
+
right: offset === undefined && offsetParameter === undefined
|
|
405
|
+
? limitExpression
|
|
406
|
+
: {
|
|
407
|
+
kind: "binary",
|
|
408
|
+
operator: "+",
|
|
409
|
+
left: structuredClone(offsetExpression),
|
|
410
|
+
right: limitExpression,
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
return {
|
|
415
|
+
sql: "(lateral ranked rows)",
|
|
416
|
+
base: { table: rankedAlias, alias: rankedAlias, windowed: { block: rows, windows } },
|
|
417
|
+
joins: [],
|
|
418
|
+
select: rows.select.map((item) => ({
|
|
419
|
+
expression: { kind: "column", reference: `${rankedAlias}.${item.alias}` },
|
|
420
|
+
alias: item.alias,
|
|
421
|
+
})),
|
|
422
|
+
predicates,
|
|
423
|
+
groupBy: [],
|
|
424
|
+
having: [],
|
|
425
|
+
orderBy: [],
|
|
426
|
+
...(limitParameter === undefined ? {} : { limitValidationParameters: [limitParameter] }),
|
|
427
|
+
...(offsetParameter === undefined ? {} : { offsetValidationParameters: [offsetParameter] }),
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Replaces the outer block's references to one join alias's columns — everywhere except the
|
|
432
|
+
* join's own key expressions, which must keep addressing the raw column.
|
|
433
|
+
*/
|
|
434
|
+
function replaceOuterReferences(block, alias, replacements) {
|
|
435
|
+
const rewrite = (expression) => {
|
|
436
|
+
if (expression.kind === "column") {
|
|
437
|
+
const replacement = replacements.get(expression.reference);
|
|
438
|
+
return replacement === undefined ? expression : structuredClone(replacement);
|
|
439
|
+
}
|
|
440
|
+
if (expression.kind === "subquery" || expression.kind === "exists") {
|
|
441
|
+
replaceOuterReferences(expression.block, alias, replacements);
|
|
442
|
+
return expression;
|
|
443
|
+
}
|
|
444
|
+
if (expression.kind === "window") {
|
|
445
|
+
expression.partitionBy = expression.partitionBy.map(rewrite);
|
|
446
|
+
for (const order of expression.orderBy)
|
|
447
|
+
order.expression = rewrite(order.expression);
|
|
448
|
+
if (expression.argument !== undefined)
|
|
449
|
+
expression.argument = rewrite(expression.argument);
|
|
450
|
+
return expression;
|
|
451
|
+
}
|
|
452
|
+
return mapChildExpressions(expression, rewrite);
|
|
453
|
+
};
|
|
454
|
+
for (const item of block.select)
|
|
455
|
+
item.expression = rewrite(item.expression);
|
|
456
|
+
for (const predicate of [...block.predicates, ...block.having]) {
|
|
457
|
+
predicate.left = rewrite(predicate.left);
|
|
458
|
+
predicate.right = rewrite(predicate.right);
|
|
459
|
+
}
|
|
460
|
+
block.groupBy = block.groupBy.map(rewrite);
|
|
461
|
+
for (const order of block.orderBy)
|
|
462
|
+
order.expression = rewrite(order.expression);
|
|
463
|
+
for (const join of block.joins) {
|
|
464
|
+
if (join.alias === alias)
|
|
465
|
+
continue;
|
|
466
|
+
if (join.on !== undefined)
|
|
467
|
+
join.on = rewrite(join.on);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
255
470
|
// --- Join key extraction ---------------------------------------------------------------------
|
|
256
471
|
/**
|
|
257
472
|
* Pulls a hash key out of a conjunctive ON clause.
|
|
@@ -289,6 +504,215 @@ function extractJoinKeys(block) {
|
|
|
289
504
|
available.add(join.alias);
|
|
290
505
|
}
|
|
291
506
|
}
|
|
507
|
+
/**
|
|
508
|
+
* `DATE_TRUNC('unit', col) = ts` and `EXTRACT(YEAR FROM col) = n` are equalities on a derived
|
|
509
|
+
* value that the scan cannot use; the same truth as a range on the column itself — `col >=
|
|
510
|
+
* start AND col < start + 1 unit` — reads by value range, prunes blocks by their statistics,
|
|
511
|
+
* and runs on the raw datetime kernel. A timestamp that is not aligned to its unit can never
|
|
512
|
+
* equal a truncation, so that predicate becomes a constant false.
|
|
513
|
+
*/
|
|
514
|
+
function rewriteCalendarEqualities(block) {
|
|
515
|
+
const rewritten = [];
|
|
516
|
+
for (const predicate of block.predicates) {
|
|
517
|
+
if (predicate.operator !== "=") {
|
|
518
|
+
rewritten.push(predicate);
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
let call;
|
|
522
|
+
let literal;
|
|
523
|
+
if (predicate.left.kind === "call" && predicate.right.kind === "literal") {
|
|
524
|
+
call = predicate.left;
|
|
525
|
+
literal = predicate.right;
|
|
526
|
+
}
|
|
527
|
+
else if (predicate.left.kind === "literal" && predicate.right.kind === "call") {
|
|
528
|
+
call = predicate.right;
|
|
529
|
+
literal = predicate.left;
|
|
530
|
+
}
|
|
531
|
+
if (call === undefined || literal === undefined) {
|
|
532
|
+
rewritten.push(predicate);
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
const range = calendarRange(call, literal);
|
|
536
|
+
if (range === undefined) {
|
|
537
|
+
rewritten.push(predicate);
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
if (range === "never") {
|
|
541
|
+
rewritten.push({
|
|
542
|
+
left: { kind: "literal", value: 1 },
|
|
543
|
+
operator: "=",
|
|
544
|
+
right: { kind: "literal", value: 0 },
|
|
545
|
+
});
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
rewritten.push({
|
|
549
|
+
left: structuredClone(range.column),
|
|
550
|
+
operator: ">=",
|
|
551
|
+
right: { kind: "literal", value: range.start },
|
|
552
|
+
}, {
|
|
553
|
+
left: structuredClone(range.column),
|
|
554
|
+
operator: "<",
|
|
555
|
+
right: { kind: "literal", value: range.end },
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
block.predicates = rewritten;
|
|
559
|
+
}
|
|
560
|
+
function calendarRange(call, literal) {
|
|
561
|
+
if (call.name === "DATE_TRUNC") {
|
|
562
|
+
const [unit, column] = call.arguments;
|
|
563
|
+
if (unit?.kind !== "literal" ||
|
|
564
|
+
typeof unit.value !== "string" ||
|
|
565
|
+
column?.kind !== "column" ||
|
|
566
|
+
!(literal.value instanceof Date)) {
|
|
567
|
+
return undefined;
|
|
568
|
+
}
|
|
569
|
+
const normalized = unit.value.toLowerCase();
|
|
570
|
+
if (!calendarUnitSteps.has(normalized))
|
|
571
|
+
return undefined;
|
|
572
|
+
const start = dateTruncValue(normalized, literal.value);
|
|
573
|
+
if (start === null || Number.isNaN(start.getTime()))
|
|
574
|
+
return undefined;
|
|
575
|
+
if (start.getTime() !== literal.value.getTime())
|
|
576
|
+
return "never";
|
|
577
|
+
return { column, start, end: addCalendarUnit(start, normalized) };
|
|
578
|
+
}
|
|
579
|
+
if (call.name === "EXTRACT") {
|
|
580
|
+
const [field, column] = call.arguments;
|
|
581
|
+
if (field?.kind !== "literal" ||
|
|
582
|
+
typeof field.value !== "string" ||
|
|
583
|
+
field.value.toLowerCase() !== "year" ||
|
|
584
|
+
column?.kind !== "column" ||
|
|
585
|
+
typeof literal.value !== "number") {
|
|
586
|
+
return undefined;
|
|
587
|
+
}
|
|
588
|
+
const year = literal.value;
|
|
589
|
+
if (!Number.isInteger(year) || year < 1 || year > 9999)
|
|
590
|
+
return "never";
|
|
591
|
+
return {
|
|
592
|
+
column,
|
|
593
|
+
start: new Date(Date.UTC(year, 0, 1)),
|
|
594
|
+
end: new Date(Date.UTC(year + 1, 0, 1)),
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
return undefined;
|
|
598
|
+
}
|
|
599
|
+
const calendarUnitSteps = new Set([
|
|
600
|
+
"year",
|
|
601
|
+
"quarter",
|
|
602
|
+
"month",
|
|
603
|
+
"week",
|
|
604
|
+
"day",
|
|
605
|
+
"hour",
|
|
606
|
+
"minute",
|
|
607
|
+
"second",
|
|
608
|
+
]);
|
|
609
|
+
/** The instant one calendar unit after an aligned UTC instant. */
|
|
610
|
+
function addCalendarUnit(start, unit) {
|
|
611
|
+
const year = start.getUTCFullYear();
|
|
612
|
+
const month = start.getUTCMonth();
|
|
613
|
+
const day = start.getUTCDate();
|
|
614
|
+
switch (unit) {
|
|
615
|
+
case "year":
|
|
616
|
+
return new Date(Date.UTC(year + 1, 0, 1));
|
|
617
|
+
case "quarter":
|
|
618
|
+
return new Date(Date.UTC(year, month + 3, 1));
|
|
619
|
+
case "month":
|
|
620
|
+
return new Date(Date.UTC(year, month + 1, 1));
|
|
621
|
+
case "week":
|
|
622
|
+
return new Date(start.getTime() + 7 * 86_400_000);
|
|
623
|
+
case "day":
|
|
624
|
+
return new Date(Date.UTC(year, month, day + 1));
|
|
625
|
+
case "hour":
|
|
626
|
+
return new Date(start.getTime() + 3_600_000);
|
|
627
|
+
case "minute":
|
|
628
|
+
return new Date(start.getTime() + 60_000);
|
|
629
|
+
default:
|
|
630
|
+
return new Date(start.getTime() + 1000);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* An inner equi-join makes its two key columns equal on every output row, so a constant
|
|
635
|
+
* equality or IN list on one key holds for the other: `c.id = 4 AND o.customer = c.id` implies
|
|
636
|
+
* `o.customer = 4`. The implied predicate is added (never substituted), which lets the table
|
|
637
|
+
* that only carried the join key filter its own scan — through a secondary index when it has
|
|
638
|
+
* one — instead of joining first and filtering after. Only inner joins qualify: an outer join
|
|
639
|
+
* keeps rows whose key is NULL, for which the implication fails.
|
|
640
|
+
*/
|
|
641
|
+
function propagateJoinKeyConstants(block) {
|
|
642
|
+
const earlier = new Set([block.base.alias]);
|
|
643
|
+
const pairs = [];
|
|
644
|
+
for (const join of block.joins) {
|
|
645
|
+
const ownAlias = join.alias;
|
|
646
|
+
// Only joins that drop an unmatched outer row qualify: a mirrored predicate is applied to
|
|
647
|
+
// the join's output, where a LEFT or anti join's null-extended row would fail it.
|
|
648
|
+
if (join.on === undefined &&
|
|
649
|
+
join.left.kind === "column" &&
|
|
650
|
+
join.right.kind === "column" &&
|
|
651
|
+
(join.kind === "inner" || join.kind === "semi")) {
|
|
652
|
+
const leftOwn = join.left.reference.startsWith(`${ownAlias}.`);
|
|
653
|
+
const rightOwn = join.right.reference.startsWith(`${ownAlias}.`);
|
|
654
|
+
if (leftOwn !== rightOwn) {
|
|
655
|
+
const inner = leftOwn ? join.left.reference : join.right.reference;
|
|
656
|
+
const outer = leftOwn ? join.right.reference : join.left.reference;
|
|
657
|
+
const outerAlias = outer.slice(0, outer.indexOf("."));
|
|
658
|
+
// Both directions hold: the key values are equal on every surviving row, and a row
|
|
659
|
+
// without a match on either side is dropped anyway.
|
|
660
|
+
if (earlier.has(outerAlias))
|
|
661
|
+
pairs.push({ inner, outer, toInner: true, toOuter: true });
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
earlier.add(ownAlias);
|
|
665
|
+
}
|
|
666
|
+
if (pairs.length === 0)
|
|
667
|
+
return;
|
|
668
|
+
const signature = (predicate) => JSON.stringify(predicate);
|
|
669
|
+
const present = new Set(block.predicates.map(signature));
|
|
670
|
+
const implied = [];
|
|
671
|
+
const constantSide = (predicate) => {
|
|
672
|
+
if (predicate.operator === "IN") {
|
|
673
|
+
return predicate.left.kind === "column" &&
|
|
674
|
+
predicate.right.kind === "list" &&
|
|
675
|
+
predicate.right.items.every((item) => item.kind === "literal")
|
|
676
|
+
? "right"
|
|
677
|
+
: undefined;
|
|
678
|
+
}
|
|
679
|
+
if (!rangeOrEqualityOperators.has(predicate.operator))
|
|
680
|
+
return undefined;
|
|
681
|
+
if (predicate.left.kind === "column" && predicate.right.kind === "literal")
|
|
682
|
+
return "right";
|
|
683
|
+
if (predicate.right.kind === "column" && predicate.left.kind === "literal")
|
|
684
|
+
return "left";
|
|
685
|
+
return undefined;
|
|
686
|
+
};
|
|
687
|
+
for (const predicate of block.predicates) {
|
|
688
|
+
const side = constantSide(predicate);
|
|
689
|
+
if (side === undefined)
|
|
690
|
+
continue;
|
|
691
|
+
const column = side === "right" ? predicate.left : predicate.right;
|
|
692
|
+
if (column.kind !== "column")
|
|
693
|
+
continue;
|
|
694
|
+
for (const pair of pairs) {
|
|
695
|
+
const target = column.reference === pair.outer && pair.toInner
|
|
696
|
+
? pair.inner
|
|
697
|
+
: column.reference === pair.inner && pair.toOuter
|
|
698
|
+
? pair.outer
|
|
699
|
+
: undefined;
|
|
700
|
+
if (target === undefined)
|
|
701
|
+
continue;
|
|
702
|
+
const replacement = { kind: "column", reference: target };
|
|
703
|
+
const mirrored = side === "right"
|
|
704
|
+
? { ...predicate, left: replacement, right: structuredClone(predicate.right) }
|
|
705
|
+
: { ...predicate, left: structuredClone(predicate.left), right: replacement };
|
|
706
|
+
const key = signature(mirrored);
|
|
707
|
+
if (present.has(key))
|
|
708
|
+
continue;
|
|
709
|
+
present.add(key);
|
|
710
|
+
implied.push(mirrored);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
block.predicates.push(...implied);
|
|
714
|
+
}
|
|
715
|
+
const rangeOrEqualityOperators = new Set(["=", "<", "<=", ">", ">="]);
|
|
292
716
|
/**
|
|
293
717
|
* Whether an equality's two sides split cleanly along this join: one side reads only the table
|
|
294
718
|
* being joined, the other only tables already in hand. Anything else — a side spanning both, a
|
|
@@ -2172,7 +2596,11 @@ function foldExpression(expression) {
|
|
|
2172
2596
|
expression.name === "JSON_ARRAY" ||
|
|
2173
2597
|
expression.name === "MINNOW_JSON_GET"
|
|
2174
2598
|
? { kind: "json" }
|
|
2175
|
-
:
|
|
2599
|
+
: simpleScalarFunctions.get(expression.name)?.returns === "date"
|
|
2600
|
+
? { kind: "date" }
|
|
2601
|
+
: simpleScalarFunctions.get(expression.name)?.returns === "interval"
|
|
2602
|
+
? { kind: "interval" }
|
|
2603
|
+
: undefined;
|
|
2176
2604
|
return {
|
|
2177
2605
|
kind: "literal",
|
|
2178
2606
|
value: folded,
|
|
@@ -9,10 +9,11 @@ export interface PointReadShape {
|
|
|
9
9
|
/** Conjunctive equalities, in predicate order; may repeat a column. */
|
|
10
10
|
equalities: PointReadEquality[];
|
|
11
11
|
/** Plain column projections, in select order. */
|
|
12
|
+
/** Projected columns, or "*" for a bare wildcard the catalog expands at execution. */
|
|
12
13
|
select: Array<{
|
|
13
14
|
column: string;
|
|
14
15
|
alias: string;
|
|
15
|
-
}
|
|
16
|
+
}> | "*";
|
|
16
17
|
}
|
|
17
18
|
/** The statement-shaped half of the analysis, computed once per cached plan. */
|
|
18
19
|
interface PointReadTemplate {
|
|
@@ -24,10 +25,11 @@ interface PointReadTemplate {
|
|
|
24
25
|
column: string;
|
|
25
26
|
parameter: number;
|
|
26
27
|
}>;
|
|
28
|
+
/** Projected columns, or "*" for a bare wildcard the catalog expands at execution. */
|
|
27
29
|
select: Array<{
|
|
28
30
|
column: string;
|
|
29
31
|
alias: string;
|
|
30
|
-
}
|
|
32
|
+
}> | "*";
|
|
31
33
|
}
|
|
32
34
|
/**
|
|
33
35
|
* Test-only escape hatch and counters. Not exported from any public entry point: in-repo
|
|
@@ -93,12 +93,22 @@ function pointReadTemplate(plan) {
|
|
|
93
93
|
plan.usesVolatileFunctions === true) {
|
|
94
94
|
return undefined;
|
|
95
95
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
96
|
+
let select = [];
|
|
97
|
+
const first = plan.select[0];
|
|
98
|
+
if (plan.select.length === 1 &&
|
|
99
|
+
first?.expression.kind === "wildcard" &&
|
|
100
|
+
(first.expression.table === undefined || first.expression.table === base.alias)) {
|
|
101
|
+
// `SELECT * FROM t WHERE key = ?` is the commonest point lookup; the catalog names the
|
|
102
|
+
// columns when the read is served, in declaration order like the ordinary executor.
|
|
103
|
+
select = "*";
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
for (const item of plan.select) {
|
|
107
|
+
const column = baseColumnReference(item.expression, base.alias);
|
|
108
|
+
if (column === undefined)
|
|
109
|
+
return undefined;
|
|
110
|
+
select.push({ column, alias: item.alias });
|
|
111
|
+
}
|
|
102
112
|
}
|
|
103
113
|
const equalities = [];
|
|
104
114
|
for (const predicate of plan.predicates) {
|