@minnowdb/core 0.6.8 → 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/README.md +3 -2
- package/dist/date-value.d.ts +5 -0
- package/dist/date-value.js +41 -0
- package/dist/engine/database.js +1085 -81
- 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 +53 -1
- package/dist/engine/query.js +1160 -239
- 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 +21 -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 +277 -6
package/dist/engine/database.js
CHANGED
|
@@ -15,12 +15,12 @@ import { cachedQueryTerms, FTS_TOKENIZER_VERSION, renderDocumentValue, tokenize
|
|
|
15
15
|
import { boundedMaintenanceBatchItems, simpleDataTypes, floorWholeNumberProduct, BlockReadBatchTooLargeError, validateColumnDefault, validateEnumValues, secondaryIndexColumnIds, secondaryIndexDirections, secondaryUniqueKeyNamespace, CompactionJobConflictError, CompactionBacklogError, GarbageCollectionJobConflictError, MAX_LEVEL_ZERO_SEGMENTS, MAX_FTS_CANDIDATE_ROW_IDS, MAX_FTS_POSTING_TERM_CHARACTERS, MAX_FTS_POSTINGS_PER_CHUNK, MAX_FTS_POSTING_ROW_IDS_PER_CHUNK, MAX_INDEXED_STRING_CHARACTERS, MAX_CATALOG_NAME_CHARACTERS, MAX_MANIFEST_BLOCK_PRESENCE_IDS, MAX_MAINTENANCE_BATCH_ITEMS, MAX_STORAGE_BULK_READ_ITEMS, MAX_TRANSACTION_STAGE_BLOCKS, MAX_TRANSACTION_STAGE_BYTES, MAX_SNAPSHOT_SESSION_TTL_MS, MAX_POSTING_BUILD_TTL_MS, MAX_TEMP_OWNER_TTL_MS, MAX_SNAPSHOT_FRAME_BATCH_BYTES, MAX_SNAPSHOT_FRAME_BATCH_ITEMS, MAX_SNAPSHOT_METADATA_BATCH_BYTES, SNAPSHOT_FRAME_KINDS, validateStorageId, SnapshotManifestMissingError, SchemaConflictError, TableInUseError, TableRecordConflictError, TransactionRecordConflictError, UniqueKeyConflictError, UniqueIndexCoverageError, WriteConflictError, } from "../storage/types.js";
|
|
16
16
|
import { decodeSnapshotFrameStream, encodeSnapshotFrameStreamFooter, encodeSnapshotFrameStreamHeader, extendSnapshotFrameStreamChecksum, snapshotFrameEnvelopeParts, snapshotFrameStreamHeaderIdentity, } from "../storage/snapshot.js";
|
|
17
17
|
import { Snapshot, TransactionManager, } from "../transactions/index.js";
|
|
18
|
-
import { applyWindowFunctions, bindPlanParameters, bindStatementParameters, DUAL_TABLE, dualTableRows, blockHasSubqueries, combineUnionResults, compileCheckExpression, compileQuery, hasAggregate, createRecursiveCteState, annotateAvgArgumentScales, compileStatement, comparisonHolds, createPreparedColumnarQuery, evaluateJoinedRowExpression, evaluateRowExpression, externalizeQueryResult, expressionColumnNames, inferBlockSchema, inferResultColumnDomains, isDefaultInsertValue, referencedColumns, childExpressions, expandFtsColumns, expandNaturalJoins, expandSourceColumnAliases, expandViewSources, forEachBlockExpression, planContainsFts, planHasNaturalJoins, planHasSourceColumnAliases, planReadsTable, planReadsViews, planReadsBeyondSingleScan, projectResultColumns, queryResultNeedsExternalization, resolveStatementDatetimes, subqueryResolutionSteps, topLevelFtsMatchConjuncts, transparentProjectionSource, unknownColumnDomains, validateDefaultExpression, windowOutputDomain, windowOutputType, } from "./query.js";
|
|
18
|
+
import { applyWindowFunctions, bindPendingSelectShapes, bindPlanParameters, bindStatementParameters, DUAL_TABLE, dualTableRows, blockHasSubqueries, combineUnionResults, compileCheckExpression, compileQuery, hasAggregate, createRecursiveCteState, annotateAvgArgumentScales, compileStatement, comparisonHolds, createPreparedColumnarQuery, evaluateJoinedRowExpression, evaluateRowExpression, externalizeQueryResult, expressionColumnNames, inferBlockSchema, inferResultColumnDomains, isDeferredInsertExpression, isDefaultInsertValue, referencedColumns, childExpressions, expandFtsColumns, expandNaturalJoins, expandSourceColumnAliases, expandViewSources, forEachBlockExpression, forEachNestedBlock, planContainsFts, planHasNaturalJoins, planHasSourceColumnAliases, planReadsTable, planReadsViews, planReadsBeyondSingleScan, projectResultColumns, queryResultNeedsExternalization, resolveStatementDatetimes, subqueryResolutionSteps, topLevelFtsMatchConjuncts, transparentProjectionSource, unknownColumnDomains, validateDefaultExpression, windowOutputDomain, windowOutputType, volatileScalarFunctionNames, } from "./query.js";
|
|
19
19
|
import { copyQueryResult, planMemoKey, queryResultMemoKey, queryResultRetainedBytes, RESULT_MEMO_MAX_BYTES, } from "./query-cache.js";
|
|
20
20
|
import { QueryMemoryBudgetError, QueryMemoryContext, DEFAULT_QUERY_MEMORY_BUDGET_BYTES, } from "./memory.js";
|
|
21
21
|
import { LiveQueryLimitError, LiveQuerySet, MAX_LIVE_QUERY_SETS_PER_DATABASE, } from "./live.js";
|
|
22
22
|
import { chooseJoinOrder, optimizePlan, qualifyCorrelatedReferences, renderPlan, } from "./optimizer.js";
|
|
23
|
-
import { encodeSqlEqualityValue } from "./sql-semantics.js";
|
|
23
|
+
import { encodeSqlEqualityValue, parseSqlTimestampText } from "./sql-semantics.js";
|
|
24
24
|
import { exactNumericAsNumber, externalSqlDomainValue, externalSqlTextValue, isDateDomainValue, isExactNumeric, isSqlDomainValue, normalizeSqlDomainValue, protectedSqlTextValue, } from "./sql-domains.js";
|
|
25
25
|
import { toCatalog } from "./catalog.js";
|
|
26
26
|
import { applyColumnSteps, assertColumnDroppable, compileGeneratedColumnExpression, declaredForeignKeys, isDestructiveStep, planMigration, } from "./schema.js";
|
|
@@ -40,8 +40,14 @@ const SEQUENCE_PREFIX = "\u0000minnow_sequence:";
|
|
|
40
40
|
* shape is not selective enough and handing the scan back to the vectorized executor.
|
|
41
41
|
*/
|
|
42
42
|
const MAX_POINT_READ_CANDIDATES = 1_024;
|
|
43
|
+
/** A keyed replay decoding more blocks than this leaves the read to the ordinary path. */
|
|
44
|
+
const MAX_POINT_READ_DELTA_BLOCKS = 256;
|
|
43
45
|
/** Delta-chunk tail length past which a search schedules a fold-by-rebuild of the base. */
|
|
44
46
|
const FTS_FOLD_DELTA_CHUNKS = 16;
|
|
47
|
+
/** Candidate lists wider than this scan their blocks instead of probing for exact rows. */
|
|
48
|
+
const SECONDARY_INDEX_ROW_SELECTION_CAP = 4096;
|
|
49
|
+
/** Candidate results at most this wide are pooled across snapshots by (column, version, terms). */
|
|
50
|
+
const MAX_POOLED_FTS_CANDIDATE_ROW_IDS = 65_536;
|
|
45
51
|
/** Persistent rebuild failure cannot let an accelerator append one durable delta per commit. */
|
|
46
52
|
const FTS_HARD_DELTA_CHUNKS = 64;
|
|
47
53
|
const DEFAULT_COMPACTION_TARGET_BLOCK_BYTES = 2 * 1024 * 1024;
|
|
@@ -292,6 +298,66 @@ function isTransactionalStatement(statement) {
|
|
|
292
298
|
statement.kind === "delete" ||
|
|
293
299
|
statement.kind === "select");
|
|
294
300
|
}
|
|
301
|
+
/**
|
|
302
|
+
* Evaluates RETURNING expression items over the affected rows (every visible column of the
|
|
303
|
+
* post-image for INSERT and UPDATE, the pre-image for DELETE) through the row executor, so a
|
|
304
|
+
* function, arithmetic, or CASE in RETURNING has exactly the semantics it has in a SELECT.
|
|
305
|
+
*/
|
|
306
|
+
function projectReturningItems(table, alias, items, rows, facts) {
|
|
307
|
+
// The same catalog normalization a SELECT receives, and the same columnar executor: the
|
|
308
|
+
// affected rows become a one-table input whose values carry the execution form a stored row
|
|
309
|
+
// reads with, so `amount * 2` over a NUMERIC column is exact arithmetic here as well.
|
|
310
|
+
const plan = normalizePlanDomainLiterals({
|
|
311
|
+
sql: "(returning)",
|
|
312
|
+
base: { table: table.name, alias },
|
|
313
|
+
joins: [],
|
|
314
|
+
select: items.map((item) => ({
|
|
315
|
+
expression: structuredClone(item.expression),
|
|
316
|
+
alias: item.alias,
|
|
317
|
+
})),
|
|
318
|
+
predicates: [],
|
|
319
|
+
groupBy: [],
|
|
320
|
+
having: [],
|
|
321
|
+
orderBy: [],
|
|
322
|
+
}, facts.domains, facts.types);
|
|
323
|
+
const visible = visibleTableColumns(table);
|
|
324
|
+
const columns = new Map(visible.map((column) => [
|
|
325
|
+
column.name,
|
|
326
|
+
{
|
|
327
|
+
type: column.type,
|
|
328
|
+
values: rows.map((row) => {
|
|
329
|
+
const value = row[column.name] ?? null;
|
|
330
|
+
return typeof value === "string" && value.charCodeAt(0) !== 0
|
|
331
|
+
? executionSqlValueFromInput(column, value)
|
|
332
|
+
: value;
|
|
333
|
+
}),
|
|
334
|
+
},
|
|
335
|
+
]));
|
|
336
|
+
const input = createColumnarTable(table.name, columns);
|
|
337
|
+
const typedSchemas = new Map([
|
|
338
|
+
[
|
|
339
|
+
table.name,
|
|
340
|
+
visible.map(({ name, type, integer, sqlDomain }) => ({
|
|
341
|
+
name,
|
|
342
|
+
type,
|
|
343
|
+
...(integer === true ? { integer: true } : {}),
|
|
344
|
+
...(sqlDomain === undefined ? {} : { sqlDomain }),
|
|
345
|
+
})),
|
|
346
|
+
],
|
|
347
|
+
]);
|
|
348
|
+
const memory = new QueryMemoryContext(undefined);
|
|
349
|
+
const prepared = createPreparedColumnarQuery(plan, new Map([[table.name, input]]), memory);
|
|
350
|
+
try {
|
|
351
|
+
return {
|
|
352
|
+
returnedRows: prepared.execute().rows,
|
|
353
|
+
returnedColumns: items.map((item) => item.alias),
|
|
354
|
+
returnedColumnDomains: inferResultColumnDomains(plan, typedSchemas),
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
finally {
|
|
358
|
+
prepared.close();
|
|
359
|
+
}
|
|
360
|
+
}
|
|
295
361
|
function returningExecuteFields(table, columns, rows) {
|
|
296
362
|
return {
|
|
297
363
|
returnedRows: rows,
|
|
@@ -328,6 +394,13 @@ function boundInsertValue(value) {
|
|
|
328
394
|
return value;
|
|
329
395
|
}
|
|
330
396
|
/** Keeps SQL DEFAULT distinct from explicit NULL while pivoting INSERT rows. */
|
|
397
|
+
/** The non-null unique-key values an insert batch proposes, in row order. */
|
|
398
|
+
function insertBatchKeyValues(input, keyColumn) {
|
|
399
|
+
const values = "columns" in input
|
|
400
|
+
? (input.columns[keyColumn] ?? [])
|
|
401
|
+
: input.map((row) => row[keyColumn] ?? null);
|
|
402
|
+
return values.filter((value) => value !== null);
|
|
403
|
+
}
|
|
331
404
|
function insertStatementBatch(statement) {
|
|
332
405
|
const columns = {};
|
|
333
406
|
const omitted = {};
|
|
@@ -353,6 +426,99 @@ function insertStatementBatch(statement) {
|
|
|
353
426
|
rowCount: statement.rows.length,
|
|
354
427
|
};
|
|
355
428
|
}
|
|
429
|
+
/** Resolves CURRENT_* in every retained DML expression with one statement-start clock. */
|
|
430
|
+
function resolveMutationStatementDatetimes(statement, now) {
|
|
431
|
+
const plan = (block) => resolveStatementDatetimes({ ...block, usesStatementDatetime: true }, now);
|
|
432
|
+
const expression = (value) => {
|
|
433
|
+
const resolved = plan({
|
|
434
|
+
sql: "(statement expression)",
|
|
435
|
+
base: { table: DUAL_TABLE, alias: DUAL_TABLE },
|
|
436
|
+
joins: [],
|
|
437
|
+
select: [{ expression: value, alias: "value" }],
|
|
438
|
+
predicates: [],
|
|
439
|
+
groupBy: [],
|
|
440
|
+
having: [],
|
|
441
|
+
orderBy: [],
|
|
442
|
+
});
|
|
443
|
+
return resolved.select[0]?.expression ?? value;
|
|
444
|
+
};
|
|
445
|
+
const predicates = (values) => values.map((predicate) => ({
|
|
446
|
+
...predicate,
|
|
447
|
+
left: expression(predicate.left),
|
|
448
|
+
right: expression(predicate.right),
|
|
449
|
+
}));
|
|
450
|
+
if (statement.kind === "insert") {
|
|
451
|
+
return {
|
|
452
|
+
...statement,
|
|
453
|
+
rows: statement.rows.map((row) => row.map((value) => isDeferredInsertExpression(value) ? { expression: expression(value.expression) } : value)),
|
|
454
|
+
...(statement.query === undefined ? {} : { query: plan(statement.query) }),
|
|
455
|
+
...(statement.onConflict === undefined
|
|
456
|
+
? {}
|
|
457
|
+
: {
|
|
458
|
+
onConflict: {
|
|
459
|
+
...statement.onConflict,
|
|
460
|
+
...(statement.onConflict.assignments === undefined
|
|
461
|
+
? {}
|
|
462
|
+
: {
|
|
463
|
+
assignments: statement.onConflict.assignments.map((assignment) => ({
|
|
464
|
+
...assignment,
|
|
465
|
+
expression: expression(assignment.expression),
|
|
466
|
+
})),
|
|
467
|
+
}),
|
|
468
|
+
...(statement.onConflict.where === undefined
|
|
469
|
+
? {}
|
|
470
|
+
: { where: expression(statement.onConflict.where) }),
|
|
471
|
+
},
|
|
472
|
+
}),
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
if (statement.kind === "update") {
|
|
476
|
+
return {
|
|
477
|
+
...statement,
|
|
478
|
+
assignments: statement.assignments.map((assignment) => ({
|
|
479
|
+
...assignment,
|
|
480
|
+
expression: expression(assignment.expression),
|
|
481
|
+
})),
|
|
482
|
+
predicates: predicates(statement.predicates),
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
if (statement.kind === "delete") {
|
|
486
|
+
return { ...statement, predicates: predicates(statement.predicates) };
|
|
487
|
+
}
|
|
488
|
+
if (statement.kind === "merge") {
|
|
489
|
+
return {
|
|
490
|
+
...statement,
|
|
491
|
+
on: expression(statement.on),
|
|
492
|
+
branches: statement.branches.map((branch) => {
|
|
493
|
+
const condition = branch.condition === undefined ? {} : { condition: expression(branch.condition) };
|
|
494
|
+
if (branch.when === "not-matched") {
|
|
495
|
+
return {
|
|
496
|
+
...branch,
|
|
497
|
+
...condition,
|
|
498
|
+
action: { ...branch.action, values: branch.action.values.map(expression) },
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
return {
|
|
502
|
+
...branch,
|
|
503
|
+
...condition,
|
|
504
|
+
action: branch.action.kind === "update"
|
|
505
|
+
? {
|
|
506
|
+
...branch.action,
|
|
507
|
+
assignments: branch.action.assignments.map((assignment) => ({
|
|
508
|
+
...assignment,
|
|
509
|
+
expression: expression(assignment.expression),
|
|
510
|
+
})),
|
|
511
|
+
}
|
|
512
|
+
: branch.action,
|
|
513
|
+
};
|
|
514
|
+
}),
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
if (statement.kind === "create-table-as") {
|
|
518
|
+
return { ...statement, query: plan(statement.query) };
|
|
519
|
+
}
|
|
520
|
+
return statement;
|
|
521
|
+
}
|
|
356
522
|
/** Quotes a catalog identifier for internally generated SQL. */
|
|
357
523
|
function quoteSqlIdentifier(identifier) {
|
|
358
524
|
return `"${identifier.replaceAll('"', '""')}"`;
|
|
@@ -1697,6 +1863,41 @@ export class MinnowDatabase {
|
|
|
1697
1863
|
}
|
|
1698
1864
|
throw new TypeError(`DEFAULT produced an unsupported value: ${sql}`);
|
|
1699
1865
|
}
|
|
1866
|
+
/** Materializes volatile/catalog-backed INSERT values once per execution, never in the cache. */
|
|
1867
|
+
async #materializeInsertExpressions(statement, statementNow) {
|
|
1868
|
+
if (!statement.rows.some((row) => row.some(isDeferredInsertExpression)))
|
|
1869
|
+
return statement;
|
|
1870
|
+
const rows = [];
|
|
1871
|
+
for (const row of statement.rows) {
|
|
1872
|
+
const materialized = [];
|
|
1873
|
+
for (const value of row) {
|
|
1874
|
+
if (!isDeferredInsertExpression(value)) {
|
|
1875
|
+
materialized.push(value);
|
|
1876
|
+
continue;
|
|
1877
|
+
}
|
|
1878
|
+
let plan = {
|
|
1879
|
+
sql: "(insert value)",
|
|
1880
|
+
base: { table: DUAL_TABLE, alias: DUAL_TABLE },
|
|
1881
|
+
joins: [],
|
|
1882
|
+
select: [{ expression: value.expression, alias: "value" }],
|
|
1883
|
+
predicates: [],
|
|
1884
|
+
groupBy: [],
|
|
1885
|
+
having: [],
|
|
1886
|
+
orderBy: [],
|
|
1887
|
+
usesStatementDatetime: true,
|
|
1888
|
+
usesSequenceCalls: true,
|
|
1889
|
+
usesVolatileFunctions: true,
|
|
1890
|
+
};
|
|
1891
|
+
// Every CURRENT_* occurrence in all rows sees the same statement clock. RANDOM,
|
|
1892
|
+
// GEN_RANDOM_UUID, and NEXTVAL remain per expression, as SQL requires.
|
|
1893
|
+
plan = resolveStatementDatetimes(plan, statementNow);
|
|
1894
|
+
const result = await this.#queryCompiled(plan, { memoize: false });
|
|
1895
|
+
materialized.push(result.rows[0]?.value ?? null);
|
|
1896
|
+
}
|
|
1897
|
+
rows.push(materialized);
|
|
1898
|
+
}
|
|
1899
|
+
return { ...statement, rows };
|
|
1900
|
+
}
|
|
1700
1901
|
async insert(tableName, row) {
|
|
1701
1902
|
return this.insertBatch(tableName, [row]);
|
|
1702
1903
|
}
|
|
@@ -2610,6 +2811,11 @@ export class MinnowDatabase {
|
|
|
2610
2811
|
async #prepareCompiledPlan(plan, options = {}, probe) {
|
|
2611
2812
|
options = this.#effectiveQueryOptions(options);
|
|
2612
2813
|
throwIfAborted(options.signal);
|
|
2814
|
+
// One statement clock for the whole plan tree, fixed before any nested block executes on
|
|
2815
|
+
// its own: a scalar subquery reading CURRENT_TIMESTAMP is resolved here, not left for the
|
|
2816
|
+
// executor that only ever sees the block it runs.
|
|
2817
|
+
if (plan.usesStatementDatetime === true)
|
|
2818
|
+
plan = resolveStatementDatetimes(plan, this.#now());
|
|
2613
2819
|
// The ORDER-BY-expression desugar's wrapper is projection-only: prepare the inner block
|
|
2614
2820
|
// directly (no derived materialization) and project each result to the visible aliases,
|
|
2615
2821
|
// so `.search()` costs the same whether or not the caller also selects the score.
|
|
@@ -3184,6 +3390,9 @@ export class MinnowDatabase {
|
|
|
3184
3390
|
// cannot tell a fresh answer from a stale one.
|
|
3185
3391
|
plan.usesStatementDatetime !== true &&
|
|
3186
3392
|
plan.usesVolatileFunctions !== true &&
|
|
3393
|
+
// A sequence call advances connection state on every execution; a memo hit would both
|
|
3394
|
+
// repeat the old value and skip the advance.
|
|
3395
|
+
plan.usesSequenceCalls !== true &&
|
|
3187
3396
|
options.version === undefined &&
|
|
3188
3397
|
options.executionMemoryBudgetBytes === undefined &&
|
|
3189
3398
|
options.spillToStorage === undefined &&
|
|
@@ -3333,7 +3542,7 @@ export class MinnowDatabase {
|
|
|
3333
3542
|
// read has to ask the catalog. It asks by epoch — an O(1) probe the store already serves for
|
|
3334
3543
|
// result memoization — and only re-reads the view set when the catalog has actually moved.
|
|
3335
3544
|
// A database with no views therefore pays one probe, not a catalog scan per query.
|
|
3336
|
-
const { views, domains, columns: catalogColumns } = await this.#catalogFacts(probe);
|
|
3545
|
+
const { views, domains, types, columns: catalogColumns } = await this.#catalogFacts(probe);
|
|
3337
3546
|
let rewritten = plan.usesSequenceCalls === true ? await this.#resolveSequenceCalls(plan) : plan;
|
|
3338
3547
|
if (views.size > 0 && planReadsViews(plan, (name) => views.has(name))) {
|
|
3339
3548
|
const bodies = new Map();
|
|
@@ -3349,31 +3558,50 @@ export class MinnowDatabase {
|
|
|
3349
3558
|
return compiled;
|
|
3350
3559
|
});
|
|
3351
3560
|
}
|
|
3352
|
-
if (
|
|
3353
|
-
rewritten = normalizePlanDomainLiterals(rewritten, domains);
|
|
3561
|
+
if (planReadsTable(rewritten, (name) => domains.has(name) || types.has(name))) {
|
|
3562
|
+
rewritten = normalizePlanDomainLiterals(rewritten, domains, types);
|
|
3354
3563
|
}
|
|
3355
|
-
const
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
return
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
const columns = new Map((await this.store.listTables()).map((table) => [
|
|
3363
|
-
table.name,
|
|
3364
|
-
table.columns.map(({ name }) => name),
|
|
3365
|
-
]));
|
|
3366
|
-
const columnsOf = (name) => columns.get(name);
|
|
3564
|
+
const columnsOf = (name) => {
|
|
3565
|
+
const columns = catalogColumns.get(name);
|
|
3566
|
+
if (columns === undefined)
|
|
3567
|
+
throw new UnknownTableError(name);
|
|
3568
|
+
return columns;
|
|
3569
|
+
};
|
|
3570
|
+
rewritten = bindPendingSelectShapes(rewritten, columnsOf);
|
|
3367
3571
|
if (aliased)
|
|
3368
3572
|
rewritten = expandSourceColumnAliases(rewritten, columnsOf);
|
|
3369
|
-
|
|
3573
|
+
if (natural)
|
|
3574
|
+
rewritten = expandNaturalJoins(rewritten, columnsOf);
|
|
3575
|
+
const qualified = qualifyCorrelatedReferences(rewritten, catalogColumns);
|
|
3576
|
+
return qualified === rewritten ? rewritten : optimizePlan(qualified);
|
|
3577
|
+
}
|
|
3578
|
+
/** Replaces a statement's whole-row RETURNING payload with its expression items. */
|
|
3579
|
+
async #projectReturningItems(statement, result) {
|
|
3580
|
+
if ((statement.kind !== "insert" && statement.kind !== "update" && statement.kind !== "delete") ||
|
|
3581
|
+
statement.returningItems === undefined ||
|
|
3582
|
+
(result.kind !== "insert" && result.kind !== "update" && result.kind !== "delete") ||
|
|
3583
|
+
result.returnedRows === undefined) {
|
|
3584
|
+
return result;
|
|
3585
|
+
}
|
|
3586
|
+
const table = await this.#findTable(statement.table);
|
|
3587
|
+
const alias = statement.kind !== "insert" && statement.alias !== undefined ? statement.alias : table.name;
|
|
3588
|
+
const facts = await this.#catalogFacts();
|
|
3589
|
+
return {
|
|
3590
|
+
...result,
|
|
3591
|
+
...projectReturningItems(table, alias, statement.returningItems, result.returnedRows, facts),
|
|
3592
|
+
};
|
|
3370
3593
|
}
|
|
3371
3594
|
/** Resolves connection-local sequence calls before either synchronous executor sees the plan. */
|
|
3372
3595
|
async #resolveSequenceCalls(plan) {
|
|
3373
3596
|
const usesSequence = (expression) => (expression.kind === "call" &&
|
|
3374
3597
|
(expression.name === "NEXTVAL" || expression.name === "CURRVAL")) ||
|
|
3375
3598
|
childExpressions(expression).some(usesSequence);
|
|
3376
|
-
const selected = new Set(
|
|
3599
|
+
const selected = new Set();
|
|
3600
|
+
const markSelected = (expression) => {
|
|
3601
|
+
selected.add(expression);
|
|
3602
|
+
childExpressions(expression).forEach(markSelected);
|
|
3603
|
+
};
|
|
3604
|
+
plan.select.forEach((item) => markSelected(item.expression));
|
|
3377
3605
|
const sequenceExpressions = [];
|
|
3378
3606
|
forEachBlockExpression(plan, (expression) => {
|
|
3379
3607
|
if (!usesSequence(expression))
|
|
@@ -3383,8 +3611,12 @@ export class MinnowDatabase {
|
|
|
3383
3611
|
throw new TypeError("NEXTVAL and CURRVAL are supported in the SELECT list");
|
|
3384
3612
|
}
|
|
3385
3613
|
});
|
|
3386
|
-
if (sequenceExpressions.length === 0)
|
|
3614
|
+
if (sequenceExpressions.length === 0) {
|
|
3615
|
+
if (blockCallsFunctions(plan, sequenceFunctionNames)) {
|
|
3616
|
+
throw new TypeError("NEXTVAL and CURRVAL are supported in the SELECT list of a SELECT without FROM");
|
|
3617
|
+
}
|
|
3387
3618
|
return plan;
|
|
3619
|
+
}
|
|
3388
3620
|
if (plan.base.table !== DUAL_TABLE || plan.joins.length > 0) {
|
|
3389
3621
|
throw new TypeError("NEXTVAL and CURRVAL currently require a SELECT without FROM");
|
|
3390
3622
|
}
|
|
@@ -3468,8 +3700,11 @@ export class MinnowDatabase {
|
|
|
3468
3700
|
const childKeys = new Map();
|
|
3469
3701
|
const domains = new Map();
|
|
3470
3702
|
const columns = new Map();
|
|
3703
|
+
const types = new Map();
|
|
3471
3704
|
for (const table of await this.store.listTables()) {
|
|
3472
|
-
|
|
3705
|
+
const visible = table.columns.filter(({ hidden }) => hidden !== true);
|
|
3706
|
+
columns.set(table.name, visible.map(({ name }) => name));
|
|
3707
|
+
types.set(table.name, new Map(visible.map((column) => [column.name, column.type])));
|
|
3473
3708
|
const tableDomains = new Map(table.columns.flatMap((column) => column.sqlDomain === undefined ? [] : [[column.name, column.sqlDomain]]));
|
|
3474
3709
|
if (tableDomains.size > 0)
|
|
3475
3710
|
domains.set(table.name, tableDomains);
|
|
@@ -3483,7 +3718,7 @@ export class MinnowDatabase {
|
|
|
3483
3718
|
existing.push({ table, key });
|
|
3484
3719
|
}
|
|
3485
3720
|
}
|
|
3486
|
-
const facts = { views, childKeys, domains, columns };
|
|
3721
|
+
const facts = { views, childKeys, domains, columns, types };
|
|
3487
3722
|
this.#catalogCache = { epoch, facts };
|
|
3488
3723
|
return facts;
|
|
3489
3724
|
}
|
|
@@ -3611,7 +3846,10 @@ export class MinnowDatabase {
|
|
|
3611
3846
|
// paths cross the result boundary through the same externalizeQueryResult call, so
|
|
3612
3847
|
// reporting the column's domain below makes the answers identical by construction.
|
|
3613
3848
|
const projected = [];
|
|
3614
|
-
|
|
3849
|
+
const selected = shape.select === "*"
|
|
3850
|
+
? visibleTableColumns(table).map((column) => ({ column: column.name, alias: column.name }))
|
|
3851
|
+
: shape.select;
|
|
3852
|
+
for (const item of selected) {
|
|
3615
3853
|
const column = columnByName.get(item.column);
|
|
3616
3854
|
if (column === undefined || column.hidden === true)
|
|
3617
3855
|
return undefined;
|
|
@@ -3622,7 +3860,17 @@ export class MinnowDatabase {
|
|
|
3622
3860
|
const kind = segment.kind;
|
|
3623
3861
|
return kind !== "insert" && kind !== "base";
|
|
3624
3862
|
})) {
|
|
3625
|
-
|
|
3863
|
+
// Update and delete deltas are replayed for the one key instead of the whole table: a
|
|
3864
|
+
// scalar key identifies its row in every segment, so the row's history is a handful of
|
|
3865
|
+
// small blocks. A composite (hidden) key keeps the ordinary replay.
|
|
3866
|
+
if (keyColumn.hidden === true ||
|
|
3867
|
+
segments.some((segment) => {
|
|
3868
|
+
const kind = segment.kind;
|
|
3869
|
+
return kind !== "insert" && kind !== "base" && kind !== "update" && kind !== "delete";
|
|
3870
|
+
})) {
|
|
3871
|
+
return undefined;
|
|
3872
|
+
}
|
|
3873
|
+
return this.#pointReadThroughDeltas(shape, keyColumn, segments, equalityColumns, projected, snapshot, options);
|
|
3626
3874
|
}
|
|
3627
3875
|
// The most selective searchable component wins nothing provable without statistics, so
|
|
3628
3876
|
// prefer a numeric component (zone-map pruning plus binary search) over a string one
|
|
@@ -3773,7 +4021,177 @@ export class MinnowDatabase {
|
|
|
3773
4021
|
}
|
|
3774
4022
|
}
|
|
3775
4023
|
return {
|
|
3776
|
-
columns:
|
|
4024
|
+
columns: projected.map((item) => item.alias),
|
|
4025
|
+
columnDomains: projected.map((item) => item.column.sqlDomain ?? null),
|
|
4026
|
+
rows,
|
|
4027
|
+
};
|
|
4028
|
+
}
|
|
4029
|
+
/**
|
|
4030
|
+
* The slots of one block's vector holding `target`: a sorted null-free numeric block by run
|
|
4031
|
+
* search, otherwise by scan; a string block through its reverse dictionary. Undefined when
|
|
4032
|
+
* the block cannot be searched for this target.
|
|
4033
|
+
*/
|
|
4034
|
+
#keySlotsInBlock(blockId, decoded, column, target) {
|
|
4035
|
+
const vector = this.#blockColumnVector(blockId, decoded);
|
|
4036
|
+
if (vector.kind !== column.type)
|
|
4037
|
+
return undefined;
|
|
4038
|
+
const slots = [];
|
|
4039
|
+
if ((vector.kind === "number" || vector.kind === "datetime") && typeof target === "number") {
|
|
4040
|
+
if (decoded.description.nullCount === 0 && valuesAreAscending(vector.values)) {
|
|
4041
|
+
const run = equalRunRange(vector.values, target);
|
|
4042
|
+
for (let slot = run.begin; slot < run.end; slot += 1)
|
|
4043
|
+
slots.push(slot);
|
|
4044
|
+
}
|
|
4045
|
+
else {
|
|
4046
|
+
for (let slot = 0; slot < vector.length; slot += 1) {
|
|
4047
|
+
if (vector.values[slot] === target && vectorValue(vector, slot) !== null) {
|
|
4048
|
+
slots.push(slot);
|
|
4049
|
+
}
|
|
4050
|
+
}
|
|
4051
|
+
}
|
|
4052
|
+
return slots;
|
|
4053
|
+
}
|
|
4054
|
+
if (vector.kind === "string" && typeof target === "string") {
|
|
4055
|
+
const code = this.#dictionaryCode(blockId, vector, target);
|
|
4056
|
+
if (code === undefined)
|
|
4057
|
+
return slots;
|
|
4058
|
+
for (let slot = 0; slot < vector.length; slot += 1) {
|
|
4059
|
+
if (vector.codes[slot] === code)
|
|
4060
|
+
slots.push(slot);
|
|
4061
|
+
}
|
|
4062
|
+
return slots;
|
|
4063
|
+
}
|
|
4064
|
+
if (vector.kind === "boolean" && typeof target === "boolean") {
|
|
4065
|
+
for (let slot = 0; slot < vector.length; slot += 1) {
|
|
4066
|
+
if (vectorValue(vector, slot) === target)
|
|
4067
|
+
slots.push(slot);
|
|
4068
|
+
}
|
|
4069
|
+
return slots;
|
|
4070
|
+
}
|
|
4071
|
+
return undefined;
|
|
4072
|
+
}
|
|
4073
|
+
/**
|
|
4074
|
+
* A keyed point read over a mutation history: the segments are walked in visible order,
|
|
4075
|
+
* an insert lands the row, a delete unmaps it, and an update patches the columns it carries
|
|
4076
|
+
* — the same last-writer-wins replay the streamed mutation table performs, applied to one
|
|
4077
|
+
* key. The remaining equalities are checked on the row as it stands at the end, since an
|
|
4078
|
+
* update may have changed a filtered column. Falls back when the history is wide enough
|
|
4079
|
+
* that decoding it would not beat the ordinary path.
|
|
4080
|
+
*/
|
|
4081
|
+
async #pointReadThroughDeltas(shape, keyColumn, segments, equalityColumns, projected, snapshot, options) {
|
|
4082
|
+
const keyEquality = shape.equalities.find((equality) => equality.column === keyColumn.name);
|
|
4083
|
+
if (keyEquality === undefined)
|
|
4084
|
+
return undefined;
|
|
4085
|
+
const target = keyEquality.value instanceof Date ? dateMilliseconds(keyEquality.value) : keyEquality.value;
|
|
4086
|
+
const neededColumns = [
|
|
4087
|
+
...new Set([...equalityColumns.values(), ...projected.map((item) => item.column)]),
|
|
4088
|
+
];
|
|
4089
|
+
let current;
|
|
4090
|
+
let decodedBlocks = 0;
|
|
4091
|
+
for (const segment of segments) {
|
|
4092
|
+
throwIfAborted(options.signal);
|
|
4093
|
+
if (segment.rowCount === 0)
|
|
4094
|
+
continue;
|
|
4095
|
+
const keyBlockIds = segment.columnBlockIds[keyColumn.id] ?? [];
|
|
4096
|
+
if (keyBlockIds.length === 0)
|
|
4097
|
+
return undefined;
|
|
4098
|
+
let blockIndexes = keyBlockIds.map((_, index) => index);
|
|
4099
|
+
if (typeof target === "number" && keyBlockIds.length > 1) {
|
|
4100
|
+
const descriptions = await this.#zoneDescriptions(keyBlockIds, snapshot);
|
|
4101
|
+
blockIndexes = blockIndexes.filter((blockIndex) => {
|
|
4102
|
+
const zone = descriptions.get(keyBlockIds[blockIndex] ?? "")?.metadata.zoneMap;
|
|
4103
|
+
return zone === undefined || (zone.min <= target && target <= zone.max);
|
|
4104
|
+
});
|
|
4105
|
+
}
|
|
4106
|
+
if (blockIndexes.length === 0)
|
|
4107
|
+
continue;
|
|
4108
|
+
decodedBlocks += blockIndexes.length;
|
|
4109
|
+
if (decodedBlocks > MAX_POINT_READ_DELTA_BLOCKS)
|
|
4110
|
+
return undefined;
|
|
4111
|
+
const keyBlocks = await this.#decodedBlocksThroughCache(blockIndexes.map((blockIndex) => keyBlockIds[blockIndex] ?? ""), snapshot);
|
|
4112
|
+
for (const [position, blockIndex] of blockIndexes.entries()) {
|
|
4113
|
+
const decoded = keyBlocks[position];
|
|
4114
|
+
if (decoded === undefined)
|
|
4115
|
+
return undefined;
|
|
4116
|
+
const blockId = keyBlockIds[blockIndex] ?? "";
|
|
4117
|
+
const slots = this.#keySlotsInBlock(blockId, decoded, keyColumn, target);
|
|
4118
|
+
if (slots === undefined)
|
|
4119
|
+
return undefined;
|
|
4120
|
+
if (slots.length === 0)
|
|
4121
|
+
continue;
|
|
4122
|
+
if (slots.length > 1)
|
|
4123
|
+
return undefined;
|
|
4124
|
+
const slot = slots[0] ?? 0;
|
|
4125
|
+
if (segment.kind === "delete") {
|
|
4126
|
+
current = undefined;
|
|
4127
|
+
continue;
|
|
4128
|
+
}
|
|
4129
|
+
if (segment.kind === "update" && current === undefined)
|
|
4130
|
+
continue;
|
|
4131
|
+
const carried = segment.kind === "update"
|
|
4132
|
+
? neededColumns.filter((column) => column.id !== keyColumn.id && segment.columnBlockIds[column.id] !== undefined)
|
|
4133
|
+
: neededColumns.filter((column) => column.id !== keyColumn.id);
|
|
4134
|
+
const blockIds = carried.map((column) => segment.columnBlockIds[column.id]?.[blockIndex]);
|
|
4135
|
+
if (blockIds.some((id) => id === undefined))
|
|
4136
|
+
return undefined;
|
|
4137
|
+
decodedBlocks += blockIds.length;
|
|
4138
|
+
if (decodedBlocks > MAX_POINT_READ_DELTA_BLOCKS)
|
|
4139
|
+
return undefined;
|
|
4140
|
+
const blocks = await this.#decodedBlocksThroughCache(blockIds, snapshot);
|
|
4141
|
+
const values = segment.kind === "update" && current !== undefined
|
|
4142
|
+
? current
|
|
4143
|
+
: new Map();
|
|
4144
|
+
if (segment.kind !== "update") {
|
|
4145
|
+
if (current !== undefined)
|
|
4146
|
+
return undefined;
|
|
4147
|
+
values.set(keyColumn.name, vectorValue(this.#blockColumnVector(blockId, decoded), slot));
|
|
4148
|
+
}
|
|
4149
|
+
for (const [index, column] of carried.entries()) {
|
|
4150
|
+
const block = blocks[index];
|
|
4151
|
+
const columnBlockId = blockIds[index];
|
|
4152
|
+
if (block === undefined || columnBlockId === undefined)
|
|
4153
|
+
return undefined;
|
|
4154
|
+
if (block.column.rowCount !== decoded.column.rowCount)
|
|
4155
|
+
return undefined;
|
|
4156
|
+
const vector = this.#blockColumnVector(columnBlockId, block);
|
|
4157
|
+
if (vector.kind !== column.type)
|
|
4158
|
+
return undefined;
|
|
4159
|
+
values.set(column.name, vectorValue(vector, slot));
|
|
4160
|
+
}
|
|
4161
|
+
current = values;
|
|
4162
|
+
}
|
|
4163
|
+
}
|
|
4164
|
+
const rows = [];
|
|
4165
|
+
if (current !== undefined) {
|
|
4166
|
+
let matches = true;
|
|
4167
|
+
for (const equality of shape.equalities) {
|
|
4168
|
+
const stored = current.get(equality.column);
|
|
4169
|
+
const wanted = equality.value;
|
|
4170
|
+
const equal = wanted instanceof Date
|
|
4171
|
+
? stored instanceof Date && dateMilliseconds(stored) === dateMilliseconds(wanted)
|
|
4172
|
+
: stored === wanted;
|
|
4173
|
+
if (!equal) {
|
|
4174
|
+
matches = false;
|
|
4175
|
+
break;
|
|
4176
|
+
}
|
|
4177
|
+
}
|
|
4178
|
+
if (matches) {
|
|
4179
|
+
const row = {};
|
|
4180
|
+
for (const item of projected) {
|
|
4181
|
+
const value = current.get(item.column.name);
|
|
4182
|
+
if (value === undefined)
|
|
4183
|
+
return undefined;
|
|
4184
|
+
if (typeof value === "string" && value.charCodeAt(0) === 0) {
|
|
4185
|
+
if (item.column.sqlDomain === undefined || !isSqlDomainValue(value))
|
|
4186
|
+
return undefined;
|
|
4187
|
+
}
|
|
4188
|
+
row[item.alias] = value;
|
|
4189
|
+
}
|
|
4190
|
+
rows.push(row);
|
|
4191
|
+
}
|
|
4192
|
+
}
|
|
4193
|
+
return {
|
|
4194
|
+
columns: projected.map((item) => item.alias),
|
|
3777
4195
|
columnDomains: projected.map((item) => item.column.sqlDomain ?? null),
|
|
3778
4196
|
rows,
|
|
3779
4197
|
};
|
|
@@ -4785,7 +5203,7 @@ export class MinnowDatabase {
|
|
|
4785
5203
|
}
|
|
4786
5204
|
for (const trigger of triggers) {
|
|
4787
5205
|
for (const statement of trigger.statements) {
|
|
4788
|
-
const compiled = compileStatement(statement.sql);
|
|
5206
|
+
const compiled = resolveMutationStatementDatetimes(compileStatement(statement.sql), this.#now());
|
|
4789
5207
|
if (compiled.kind === "insert") {
|
|
4790
5208
|
await this.#applyTriggerInsertBody(transaction, compiled, statement.bindings, rowCount, valueAt, cascadeBudget);
|
|
4791
5209
|
continue;
|
|
@@ -4810,8 +5228,9 @@ export class MinnowDatabase {
|
|
|
4810
5228
|
if (target.uniqueKeyColumnId !== undefined) {
|
|
4811
5229
|
throw new TypeError(`Trigger bodies insert into keyless tables only: ${target.name}`);
|
|
4812
5230
|
}
|
|
4813
|
-
const input = insertStatementBatch({ ...compiled, rows: derivedRows });
|
|
4814
5231
|
const statementNow = this.#now();
|
|
5232
|
+
const materialized = await this.#materializeInsertExpressions({ ...compiled, rows: derivedRows }, statementNow);
|
|
5233
|
+
const input = insertStatementBatch(materialized);
|
|
4815
5234
|
const filled = await fillColumnDefaults(target, input, (sql) => this.#evaluateDefaultExpression(sql, statementNow), derivedRows.length);
|
|
4816
5235
|
normalizeDomainBatch(target, filled.batch);
|
|
4817
5236
|
fillStoredGeneratedColumns(target, filled.batch, derivedRows.length, filled.generated);
|
|
@@ -6080,8 +6499,12 @@ export class MinnowDatabase {
|
|
|
6080
6499
|
else {
|
|
6081
6500
|
notes.push("materializes inputs at preparation");
|
|
6082
6501
|
}
|
|
6083
|
-
const table = reported.base.derived ??
|
|
6084
|
-
|
|
6502
|
+
const table = reported.base.derived ??
|
|
6503
|
+
reported.base.union ??
|
|
6504
|
+
reported.base.windowed ??
|
|
6505
|
+
reported.base.recursive;
|
|
6506
|
+
// A FROM-less query reads the dual row, which has no catalog record to report on.
|
|
6507
|
+
if (table === undefined && reported.joins.length === 0 && reported.base.table !== DUAL_TABLE) {
|
|
6085
6508
|
const record = await this.#findTable(reported.base.table);
|
|
6086
6509
|
const pointTemplate = record.view === undefined ? cachedPointReadTemplate(reported) : null;
|
|
6087
6510
|
if (pointTemplate !== null) {
|
|
@@ -6182,9 +6605,9 @@ export class MinnowDatabase {
|
|
|
6182
6605
|
if (!isTransactionalStatement(boundStatement)) {
|
|
6183
6606
|
throw new TypeError(`${boundStatement.kind.toUpperCase().replace("-", " ")} is not allowed inside a transaction`);
|
|
6184
6607
|
}
|
|
6185
|
-
return externalizeExecuteResult(await this.#duringTransaction(open, () => open.session.executeStatement(boundStatement)));
|
|
6608
|
+
return externalizeExecuteResult(await this.#projectReturningItems(boundStatement, await this.#duringTransaction(open, () => open.session.executeStatement(boundStatement))));
|
|
6186
6609
|
}
|
|
6187
|
-
return externalizeExecuteResult(await this.runStatement(bindStatementParameters(statement, params)));
|
|
6610
|
+
return externalizeExecuteResult(await this.#projectReturningItems(statement, await this.runStatement(bindStatementParameters(statement, params))));
|
|
6188
6611
|
}
|
|
6189
6612
|
/**
|
|
6190
6613
|
* E141-04 on the child side: every non-null value written into a referencing column must name
|
|
@@ -6810,13 +7233,16 @@ export class MinnowDatabase {
|
|
|
6810
7233
|
async #filterConflictingInsertRows(statement, writer) {
|
|
6811
7234
|
const table = await this.#findTable(statement.table);
|
|
6812
7235
|
const keyColumn = getUniqueKeyColumn(table);
|
|
6813
|
-
const conflictColumns = statement.onConflict?.columns ??
|
|
6814
|
-
(statement.onConflict === undefined ? [] : [statement.onConflict.column]);
|
|
6815
7236
|
const addressColumns = keyColumn?.hidden === true
|
|
6816
7237
|
? primaryKeyColumns(table).map(({ name }) => name)
|
|
6817
7238
|
: keyColumn === undefined
|
|
6818
7239
|
? []
|
|
6819
7240
|
: [keyColumn.name];
|
|
7241
|
+
// A targetless DO NOTHING means "any unique key", which for a Minnow table is its key.
|
|
7242
|
+
const conflictColumns = statement.onConflict?.anyTarget === true
|
|
7243
|
+
? addressColumns
|
|
7244
|
+
: (statement.onConflict?.columns ??
|
|
7245
|
+
(statement.onConflict === undefined ? [] : [statement.onConflict.column]));
|
|
6820
7246
|
if (keyColumn === undefined ||
|
|
6821
7247
|
conflictColumns.length !== addressColumns.length ||
|
|
6822
7248
|
conflictColumns.some((name, index) => name !== addressColumns[index])) {
|
|
@@ -6876,6 +7302,37 @@ export class MinnowDatabase {
|
|
|
6876
7302
|
}),
|
|
6877
7303
|
};
|
|
6878
7304
|
}
|
|
7305
|
+
/** Rejects an INSERT staged into a scope whose keys already exist there or in committed rows. */
|
|
7306
|
+
async #assertStagedInsertKeysFree(table, keyColumn, input, writer) {
|
|
7307
|
+
if (keyColumn.hidden === true)
|
|
7308
|
+
return;
|
|
7309
|
+
const keys = insertBatchKeyValues(input, keyColumn.name);
|
|
7310
|
+
if (keys.length === 0)
|
|
7311
|
+
return;
|
|
7312
|
+
const plan = {
|
|
7313
|
+
sql: "(staged insert keys)",
|
|
7314
|
+
base: { table: table.name, alias: table.name },
|
|
7315
|
+
joins: [],
|
|
7316
|
+
select: [{ expression: { kind: "column", reference: keyColumn.name }, alias: "key" }],
|
|
7317
|
+
predicates: [
|
|
7318
|
+
{
|
|
7319
|
+
left: { kind: "column", reference: keyColumn.name },
|
|
7320
|
+
operator: "IN",
|
|
7321
|
+
right: { kind: "list", items: keys.map((value) => ({ kind: "literal", value })) },
|
|
7322
|
+
},
|
|
7323
|
+
],
|
|
7324
|
+
groupBy: [],
|
|
7325
|
+
having: [],
|
|
7326
|
+
orderBy: [],
|
|
7327
|
+
};
|
|
7328
|
+
const existing = (await writer.queryPlan(plan)).rows[0]?.key;
|
|
7329
|
+
if (typeof existing === "string" ||
|
|
7330
|
+
typeof existing === "number" ||
|
|
7331
|
+
typeof existing === "boolean" ||
|
|
7332
|
+
existing instanceof Date) {
|
|
7333
|
+
throw new UniqueConstraintError(table.name, keyColumn.name, existing);
|
|
7334
|
+
}
|
|
7335
|
+
}
|
|
6879
7336
|
/**
|
|
6880
7337
|
* MERGE (F312). One pass over the source, joined to the target on the match condition, decides
|
|
6881
7338
|
* each row's branch; the branches then apply as ordinary batched writes inside a single write
|
|
@@ -6961,9 +7418,10 @@ export class MinnowDatabase {
|
|
|
6961
7418
|
if (statement.query === undefined)
|
|
6962
7419
|
return statement;
|
|
6963
7420
|
const target = await this.#findTable(statement.table);
|
|
7421
|
+
const plan = await this.#applyCatalogRewrites(statement.query);
|
|
6964
7422
|
let result;
|
|
6965
7423
|
if (writer === undefined) {
|
|
6966
|
-
const prepared = await this.#prepareCompiledPlan(
|
|
7424
|
+
const prepared = await this.#prepareCompiledPlan(plan);
|
|
6967
7425
|
try {
|
|
6968
7426
|
result = prepared.execute();
|
|
6969
7427
|
}
|
|
@@ -6972,14 +7430,22 @@ export class MinnowDatabase {
|
|
|
6972
7430
|
}
|
|
6973
7431
|
}
|
|
6974
7432
|
else {
|
|
6975
|
-
const plan = await this.#applyCatalogRewrites(statement.query);
|
|
6976
7433
|
result = await writer.queryPlan(plan);
|
|
6977
7434
|
}
|
|
7435
|
+
const insertColumns = statement.columns.length > 0
|
|
7436
|
+
? statement.columns
|
|
7437
|
+
: visibleTableColumns(target).map(({ name }) => name);
|
|
7438
|
+
if (result.columns.length !== insertColumns.length) {
|
|
7439
|
+
throw new TypeError(statement.columns.length > 0
|
|
7440
|
+
? "INSERT ... SELECT must produce exactly the insert column count"
|
|
7441
|
+
: "INSERT ... SELECT must produce exactly the table column count");
|
|
7442
|
+
}
|
|
6978
7443
|
const { query, ...rest } = statement;
|
|
6979
7444
|
void query;
|
|
6980
|
-
const targetColumns =
|
|
7445
|
+
const targetColumns = insertColumns.map((name) => target.columns.find((column) => column.name === name));
|
|
6981
7446
|
return {
|
|
6982
7447
|
...rest,
|
|
7448
|
+
columns: insertColumns,
|
|
6983
7449
|
rows: result.rows.map((row) => result.columns.map((column, position) => storedSqlValueFromExecution(targetColumns[position], row[column] ?? null))),
|
|
6984
7450
|
};
|
|
6985
7451
|
}
|
|
@@ -6991,6 +7457,8 @@ export class MinnowDatabase {
|
|
|
6991
7457
|
* (the snapshot row with assignments applied — the exact values the mutation wrote).
|
|
6992
7458
|
*/
|
|
6993
7459
|
async runStatement(statement, options = {}) {
|
|
7460
|
+
const statementNow = this.#now();
|
|
7461
|
+
statement = resolveMutationStatementDatetimes(statement, statementNow);
|
|
6994
7462
|
if (statement.kind === "select") {
|
|
6995
7463
|
return { kind: "rows", result: await this.query(statement.sql) };
|
|
6996
7464
|
}
|
|
@@ -7110,7 +7578,8 @@ export class MinnowDatabase {
|
|
|
7110
7578
|
...(sqlDomain === undefined ? {} : { sqlDomain }),
|
|
7111
7579
|
})),
|
|
7112
7580
|
]));
|
|
7113
|
-
const
|
|
7581
|
+
const plan = await this.#applyCatalogRewrites(statement.query, before);
|
|
7582
|
+
const tableColumns = inferBlockSchema(plan, schemas).map(({ name, type, integer, sqlDomain }) => ({
|
|
7114
7583
|
id: this.#createId(),
|
|
7115
7584
|
name: validateName(name, "Column"),
|
|
7116
7585
|
type,
|
|
@@ -7128,7 +7597,6 @@ export class MinnowDatabase {
|
|
|
7128
7597
|
};
|
|
7129
7598
|
const transaction = await this.#transactions.beginWithPendingTable(pendingTable, before.catalogEpoch);
|
|
7130
7599
|
try {
|
|
7131
|
-
const plan = await this.#applyCatalogRewrites(statement.query);
|
|
7132
7600
|
const executionMemoryBudgetBytes = Math.min(this.#queryExecutionMemoryBudgetBytes, DEFAULT_QUERY_MEMORY_BUDGET_BYTES);
|
|
7133
7601
|
const stageBatchRows = 16_384;
|
|
7134
7602
|
let nextRowId = 1n;
|
|
@@ -7393,6 +7861,9 @@ export class MinnowDatabase {
|
|
|
7393
7861
|
if (statement.kind === "transaction") {
|
|
7394
7862
|
return this.#runTransactionStatement(statement.action, statement.name);
|
|
7395
7863
|
}
|
|
7864
|
+
if (statement.kind === "insert") {
|
|
7865
|
+
statement = await this.#materializeInsertExpressions(statement, statementNow);
|
|
7866
|
+
}
|
|
7396
7867
|
if (options.writer === undefined) {
|
|
7397
7868
|
// Selection, assignment evaluation, constraint proofs, trigger derivation, and the write
|
|
7398
7869
|
// itself are one statement transaction. A genuine concurrent data commit does not rebase
|
|
@@ -7421,13 +7892,12 @@ export class MinnowDatabase {
|
|
|
7421
7892
|
}
|
|
7422
7893
|
}
|
|
7423
7894
|
}
|
|
7895
|
+
if (statement.kind === "insert" && statement.query !== undefined) {
|
|
7896
|
+
statement = await this.#materializeInsertSelect(statement, options.writer);
|
|
7897
|
+
}
|
|
7424
7898
|
if (statement.kind === "insert" && statement.columns.length === 0) {
|
|
7425
7899
|
const table = await this.#findTable(statement.table);
|
|
7426
7900
|
const columns = visibleTableColumns(table).map((column) => column.name);
|
|
7427
|
-
const producedColumns = statement.query?.select.length;
|
|
7428
|
-
if (producedColumns !== undefined && producedColumns !== columns.length) {
|
|
7429
|
-
throw new TypeError("INSERT ... SELECT must produce exactly the table column count");
|
|
7430
|
-
}
|
|
7431
7901
|
if (statement.defaultValues === true) {
|
|
7432
7902
|
// Keep the empty column list: omission, not NULL, is what invokes catalog defaults.
|
|
7433
7903
|
}
|
|
@@ -7438,12 +7908,16 @@ export class MinnowDatabase {
|
|
|
7438
7908
|
statement = { ...statement, columns };
|
|
7439
7909
|
}
|
|
7440
7910
|
}
|
|
7441
|
-
|
|
7911
|
+
if (statement.kind === "insert") {
|
|
7912
|
+
statement = await this.#materializeInsertExpressions(statement, statementNow);
|
|
7913
|
+
}
|
|
7914
|
+
// A RETURNING clause parsed from SQL text applies unless the caller overrides it. Expression
|
|
7915
|
+
// items read every column of the affected rows here; execute() projects them afterwards.
|
|
7442
7916
|
if (statement.returning !== undefined && options.returning === undefined) {
|
|
7443
7917
|
options = { ...options, returning: statement.returning };
|
|
7444
7918
|
}
|
|
7445
|
-
if (statement.
|
|
7446
|
-
|
|
7919
|
+
else if (statement.returningItems !== undefined && options.returning === undefined) {
|
|
7920
|
+
options = { ...options, returning: "*" };
|
|
7447
7921
|
}
|
|
7448
7922
|
if (statement.kind === "insert" && statement.onConflict?.action === "nothing") {
|
|
7449
7923
|
statement = await this.#filterConflictingInsertRows(statement, options.writer);
|
|
@@ -7509,6 +7983,20 @@ export class MinnowDatabase {
|
|
|
7509
7983
|
}
|
|
7510
7984
|
}
|
|
7511
7985
|
const writer = options.writer;
|
|
7986
|
+
const stagedKeyColumn = writer === undefined ? undefined : getUniqueKeyColumn(table);
|
|
7987
|
+
if (writer !== undefined && !viaUpsert && stagedKeyColumn !== undefined) {
|
|
7988
|
+
// Statement validity first, so an invalid projection reports as itself, not as a
|
|
7989
|
+
// duplicate the invalid statement would also have hit.
|
|
7990
|
+
const assignedGenerated = table.columns.find((column) => column.generatedValue !== undefined && statement.columns.includes(column.name));
|
|
7991
|
+
if (assignedGenerated !== undefined) {
|
|
7992
|
+
throw new TypeError(`Generated column cannot be assigned: ${assignedGenerated.name}`);
|
|
7993
|
+
}
|
|
7994
|
+
// Inside BEGIN ... COMMIT a duplicate key must fail the INSERT itself, as it does in
|
|
7995
|
+
// PostgreSQL, not surface at COMMIT after the application has moved on. The scope's
|
|
7996
|
+
// read sees committed rows plus everything it staged, so one keyed lookup settles it;
|
|
7997
|
+
// commit still re-validates as the last word.
|
|
7998
|
+
await this.#assertStagedInsertKeysFree(table, stagedKeyColumn, input, writer);
|
|
7999
|
+
}
|
|
7512
8000
|
// A scope stages and reports rows; a standalone write also publishes a version and any
|
|
7513
8001
|
// values the engine generated. Both shapes answer here, and the narrow one has less to
|
|
7514
8002
|
// report rather than something different.
|
|
@@ -7578,21 +8066,26 @@ export class MinnowDatabase {
|
|
|
7578
8066
|
}));
|
|
7579
8067
|
})();
|
|
7580
8068
|
const referenced = new Set([keyColumn.name, ...(returningColumns ?? [])]);
|
|
7581
|
-
|
|
7582
|
-
|
|
7583
|
-
|
|
7584
|
-
|
|
7585
|
-
}
|
|
7586
|
-
}
|
|
7587
|
-
}
|
|
8069
|
+
// Each assignment rides the read as a hidden select item, so a scalar subquery in SET —
|
|
8070
|
+
// correlated or not — resolves through the ordinary query pipeline (decorrelation, one
|
|
8071
|
+
// snapshot for the uncorrelated) instead of a per-row evaluator that cannot run one.
|
|
8072
|
+
const assignmentAlias = (column) => `\u0000set:${column}`;
|
|
7588
8073
|
const plan = optimizePlan({
|
|
7589
8074
|
sql: `(${statement.kind})`,
|
|
7590
|
-
base: { table: table.name, alias: table.name },
|
|
8075
|
+
base: { table: table.name, alias: statement.alias ?? table.name },
|
|
7591
8076
|
joins: [],
|
|
7592
|
-
select: [
|
|
7593
|
-
|
|
7594
|
-
|
|
7595
|
-
|
|
8077
|
+
select: [
|
|
8078
|
+
...[...referenced].map((name) => ({
|
|
8079
|
+
expression: { kind: "column", reference: name },
|
|
8080
|
+
alias: name,
|
|
8081
|
+
})),
|
|
8082
|
+
...(statement.kind === "update"
|
|
8083
|
+
? updateAssignments.map((assignment) => ({
|
|
8084
|
+
expression: assignment.expression,
|
|
8085
|
+
alias: assignmentAlias(assignment.column),
|
|
8086
|
+
}))
|
|
8087
|
+
: []),
|
|
8088
|
+
],
|
|
7596
8089
|
predicates: statement.predicates,
|
|
7597
8090
|
groupBy: [],
|
|
7598
8091
|
having: [],
|
|
@@ -7689,7 +8182,7 @@ export class MinnowDatabase {
|
|
|
7689
8182
|
throw new TypeError(`Unknown column: ${assignment.column}`);
|
|
7690
8183
|
const executionValues = returnedChanges === undefined ? undefined : [];
|
|
7691
8184
|
changes[assignment.column] = rows.map((row) => {
|
|
7692
|
-
const value =
|
|
8185
|
+
const value = row[assignmentAlias(assignment.column)] ?? null;
|
|
7693
8186
|
if (typeof value === "number" && !Number.isFinite(value)) {
|
|
7694
8187
|
throw new TypeError(`UPDATE assignment produced a non-finite number: ${assignment.column}`);
|
|
7695
8188
|
}
|
|
@@ -7862,8 +8355,11 @@ export class MinnowDatabase {
|
|
|
7862
8355
|
throwIfAborted(options.signal);
|
|
7863
8356
|
const baseSegments = indexed.segments;
|
|
7864
8357
|
// Zone-map elimination composes after index pruning: whole row groups whose statistics
|
|
7865
|
-
// reject the plan's predicates never stream at all.
|
|
7866
|
-
|
|
8358
|
+
// reject the plan's predicates never stream at all. An exact index row selection is
|
|
8359
|
+
// numbered over the pruned blocks, so it is used as is.
|
|
8360
|
+
const zonePruned = indexed.rows === undefined
|
|
8361
|
+
? await this.#zonePrunedStreamSegments(plan, freshBaseTable, projectedBaseColumns, baseSegments, snapshot)
|
|
8362
|
+
: undefined;
|
|
7867
8363
|
throwIfAborted(options.signal);
|
|
7868
8364
|
const baseView = this.#streamedViewFactory(baseTable, projectedBaseColumns, zonePruned?.segments ?? baseSegments, snapshot, zonePruned?.storedBlocks, zonePruned !== undefined || indexed.pruned);
|
|
7869
8365
|
if (baseView === undefined)
|
|
@@ -7904,7 +8400,7 @@ export class MinnowDatabase {
|
|
|
7904
8400
|
if (table.name === baseTable.name)
|
|
7905
8401
|
continue;
|
|
7906
8402
|
const requestedColumns = columns.get(table.name) ?? [];
|
|
7907
|
-
inputTables.set(table.name, await this.#materializeColumnarTableAtSnapshot(table, snapshot, requestedColumns.length === 0 ? [] : resolveReadColumns(table, requestedColumns), visibility));
|
|
8403
|
+
inputTables.set(table.name, await this.#materializeColumnarTableAtSnapshot(table, snapshot, requestedColumns.length === 0 ? [] : resolveReadColumns(table, requestedColumns), visibility, plan));
|
|
7908
8404
|
throwIfAborted(options.signal);
|
|
7909
8405
|
}
|
|
7910
8406
|
prepared = createPreparedColumnarQuery(plan, inputTables, memory, {
|
|
@@ -7948,6 +8444,9 @@ export class MinnowDatabase {
|
|
|
7948
8444
|
try {
|
|
7949
8445
|
const streamedResult = await prepared.executeAsync({
|
|
7950
8446
|
loadScanWindow: streamed.load,
|
|
8447
|
+
...(indexed.rows === undefined
|
|
8448
|
+
? {}
|
|
8449
|
+
: { scanRows: streamed.remapScanRows?.(indexed.rows) ?? indexed.rows }),
|
|
7951
8450
|
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
7952
8451
|
});
|
|
7953
8452
|
options.onStats?.({ peakMemoryBytes: memory.usage.peakBytes });
|
|
@@ -7963,6 +8462,9 @@ export class MinnowDatabase {
|
|
|
7963
8462
|
...(spillPageRows === undefined ? {} : { spillPageRows }),
|
|
7964
8463
|
spillStore: this.#leasedSpillStore(),
|
|
7965
8464
|
loadScanWindow: streamed.load,
|
|
8465
|
+
...(indexed.rows === undefined
|
|
8466
|
+
? {}
|
|
8467
|
+
: { scanRows: streamed.remapScanRows?.(indexed.rows) ?? indexed.rows }),
|
|
7966
8468
|
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
7967
8469
|
});
|
|
7968
8470
|
options.onStats?.({ peakMemoryBytes: memory.usage.peakBytes });
|
|
@@ -8321,6 +8823,33 @@ export class MinnowDatabase {
|
|
|
8321
8823
|
* dictionary objects that stay identical across queries, which keeps per-dictionary
|
|
8322
8824
|
* expression caches (equality codes, LIKE match sets) hot between statements.
|
|
8323
8825
|
*/
|
|
8826
|
+
/**
|
|
8827
|
+
* Secondary-index key locators for one block's key column, mapped to the block rows that
|
|
8828
|
+
* carry them, pooled by block id. A locator is a hash, so two keys may share one; the scan's
|
|
8829
|
+
* own predicate settles such collisions.
|
|
8830
|
+
*/
|
|
8831
|
+
#blockKeyLocators(blockId, vector, type) {
|
|
8832
|
+
const cached = this.#cacheGet(`dkl ${blockId}`);
|
|
8833
|
+
if (cached !== undefined)
|
|
8834
|
+
return cached;
|
|
8835
|
+
const locators = new Map();
|
|
8836
|
+
for (let row = 0; row < vector.length; row += 1) {
|
|
8837
|
+
const value = vectorValue(vector, row);
|
|
8838
|
+
if (value === null)
|
|
8839
|
+
continue;
|
|
8840
|
+
const locator = secondaryKeyLocator(type, value);
|
|
8841
|
+
const existing = locators.get(locator);
|
|
8842
|
+
if (existing === undefined)
|
|
8843
|
+
locators.set(locator, row);
|
|
8844
|
+
else if (typeof existing === "number")
|
|
8845
|
+
locators.set(locator, [existing, row]);
|
|
8846
|
+
else
|
|
8847
|
+
existing.push(row);
|
|
8848
|
+
}
|
|
8849
|
+
// A Map entry for a small bigint and a row number: key, value, hash slot, chain pointer.
|
|
8850
|
+
this.#cachePut(`dkl ${blockId}`, locators, 64 + locators.size * 48);
|
|
8851
|
+
return locators;
|
|
8852
|
+
}
|
|
8324
8853
|
#blockColumnVector(blockId, decoded) {
|
|
8325
8854
|
const cached = this.#cacheGet(`dbv ${blockId}`);
|
|
8326
8855
|
if (cached !== undefined)
|
|
@@ -8612,6 +9141,26 @@ export class MinnowDatabase {
|
|
|
8612
9141
|
cursorBase = baseEnd;
|
|
8613
9142
|
return start + liveRows;
|
|
8614
9143
|
};
|
|
9144
|
+
// Index-selected rows are numbered over the base scan; the replay removes dead rows, so a
|
|
9145
|
+
// selected row's output position is its base position less the dead rows before it, and a
|
|
9146
|
+
// dead selected row is not visited at all.
|
|
9147
|
+
const remapScanRows = (rows) => {
|
|
9148
|
+
const remapped = [];
|
|
9149
|
+
let removedBefore = 0;
|
|
9150
|
+
let scanned = 0;
|
|
9151
|
+
for (const row of rows) {
|
|
9152
|
+
if (row >= baseRows)
|
|
9153
|
+
break;
|
|
9154
|
+
for (; scanned < row; scanned += 1) {
|
|
9155
|
+
if (((dead[scanned >>> 3] ?? 0) & (1 << (scanned & 7))) !== 0)
|
|
9156
|
+
removedBefore += 1;
|
|
9157
|
+
}
|
|
9158
|
+
if (((dead[row >>> 3] ?? 0) & (1 << (row & 7))) !== 0)
|
|
9159
|
+
continue;
|
|
9160
|
+
remapped.push(row - removedBefore);
|
|
9161
|
+
}
|
|
9162
|
+
return remapped;
|
|
9163
|
+
};
|
|
8615
9164
|
return {
|
|
8616
9165
|
table: {
|
|
8617
9166
|
name: table.name,
|
|
@@ -8619,6 +9168,7 @@ export class MinnowDatabase {
|
|
|
8619
9168
|
columns: new Map(states.map((state) => [state.column.name, state.vector])),
|
|
8620
9169
|
},
|
|
8621
9170
|
load,
|
|
9171
|
+
remapScanRows,
|
|
8622
9172
|
};
|
|
8623
9173
|
}
|
|
8624
9174
|
/**
|
|
@@ -8963,6 +9513,10 @@ export class MinnowDatabase {
|
|
|
8963
9513
|
return undefined;
|
|
8964
9514
|
if (!this.#artifactCache.enabled)
|
|
8965
9515
|
return undefined;
|
|
9516
|
+
// A block whose value is not a function of the data — RANDOM(), a sequence call, an
|
|
9517
|
+
// unresolved clock — must run every time; the version key cannot tell its answers apart.
|
|
9518
|
+
if (blockCallsFunctions(block, nonDeterministicFunctionNames))
|
|
9519
|
+
return undefined;
|
|
8966
9520
|
// Staged writes can change repeatedly without changing either the pinned manifest version or
|
|
8967
9521
|
// their transaction id. Do not cache them under an identity that could become stale.
|
|
8968
9522
|
if (visibility.overlayTransactionId !== undefined)
|
|
@@ -13090,11 +13644,29 @@ export class MinnowDatabase {
|
|
|
13090
13644
|
const cached = memo.get(key);
|
|
13091
13645
|
if (cached !== undefined)
|
|
13092
13646
|
return cached;
|
|
13647
|
+
// The result is a function of the postings at one version, so it is also pooled across
|
|
13648
|
+
// snapshots: a repeated point lookup then skips the chunk read and validation entirely.
|
|
13649
|
+
const pooled = this.#cacheGet(`ftc\0${key}`);
|
|
13650
|
+
if (pooled !== undefined)
|
|
13651
|
+
return Promise.resolve(pooled);
|
|
13093
13652
|
// Shared leases live across queries at one version; results are version-deterministic, but
|
|
13094
13653
|
// candidate arrays can be large, so the memo sheds wholesale rather than growing unbounded.
|
|
13095
13654
|
if (memo.size >= 1)
|
|
13096
13655
|
memo.clear();
|
|
13097
|
-
const read = this.store
|
|
13656
|
+
const read = this.store
|
|
13657
|
+
.readFtsCandidates(tableId, columnId, terms, upToVersion, MAX_FTS_CANDIDATE_ROW_IDS)
|
|
13658
|
+
.then((result) => {
|
|
13659
|
+
const rowIds = result.rowIdsByTerm.reduce((total, ids) => total + ids.length, 0);
|
|
13660
|
+
// Only a complete answer is pooled: a missing base or a build past the asked version
|
|
13661
|
+
// is transient state the next read must observe again, not a fact about the version.
|
|
13662
|
+
if (result.hasBase &&
|
|
13663
|
+
!result.overflow &&
|
|
13664
|
+
result.coversVersion <= upToVersion &&
|
|
13665
|
+
rowIds <= MAX_POOLED_FTS_CANDIDATE_ROW_IDS) {
|
|
13666
|
+
this.#cachePut(`ftc\0${key}`, result, 128 + key.length * 2 + rowIds * 16);
|
|
13667
|
+
}
|
|
13668
|
+
return result;
|
|
13669
|
+
});
|
|
13098
13670
|
memo.set(key, read);
|
|
13099
13671
|
return read;
|
|
13100
13672
|
}
|
|
@@ -13295,6 +13867,23 @@ export class MinnowDatabase {
|
|
|
13295
13867
|
if (candidates === undefined)
|
|
13296
13868
|
return { segments, pruned: false };
|
|
13297
13869
|
const candidateSet = keyColumn === undefined ? undefined : new Set(candidates);
|
|
13870
|
+
// With a key column and only appended segments, the block locator maps give the exact scan
|
|
13871
|
+
// rows, in the streamed scan's numbering over the kept blocks: an equality lookup then
|
|
13872
|
+
// visits a handful of rows instead of every block that holds one. Mutation segments replay
|
|
13873
|
+
// keys and renumber rows, so they keep block-level pruning. A very wide candidate list is
|
|
13874
|
+
// cheaper to scan than to probe.
|
|
13875
|
+
// Positions count the scan segments' kept rows only; update and delete deltas are replayed
|
|
13876
|
+
// over that numbering by the streamed mutation table, which remaps the selection past the
|
|
13877
|
+
// rows the deltas removed.
|
|
13878
|
+
const rowPositions = keyColumn !== undefined &&
|
|
13879
|
+
candidates.length <= SECONDARY_INDEX_ROW_SELECTION_CAP &&
|
|
13880
|
+
segments.every((segment) => segment.kind === "insert" ||
|
|
13881
|
+
segment.kind === "base" ||
|
|
13882
|
+
segment.kind === "update" ||
|
|
13883
|
+
segment.kind === "delete")
|
|
13884
|
+
? []
|
|
13885
|
+
: undefined;
|
|
13886
|
+
let selectedRowStart = 0;
|
|
13298
13887
|
const selected = [];
|
|
13299
13888
|
for (const segment of segments) {
|
|
13300
13889
|
const anchorColumn = keyColumn ?? table.columns[0];
|
|
@@ -13328,15 +13917,43 @@ export class MinnowDatabase {
|
|
|
13328
13917
|
if (block.column.type !== keyColumn.type) {
|
|
13329
13918
|
return { segments, pruned: false };
|
|
13330
13919
|
}
|
|
13331
|
-
const
|
|
13332
|
-
|
|
13333
|
-
|
|
13334
|
-
|
|
13335
|
-
|
|
13336
|
-
|
|
13920
|
+
const blockId = anchorIds[blockIndex] ?? "";
|
|
13921
|
+
const vector = this.#blockColumnVector(blockId, block);
|
|
13922
|
+
// A block's key locators are a pure function of its immutable bytes, so they are
|
|
13923
|
+
// hashed once and pooled; a lookup then probes the (few) candidates against the
|
|
13924
|
+
// block's set instead of re-hashing every stored key on every query. A candidate
|
|
13925
|
+
// list wider than the block walks the block's locators against the candidate set.
|
|
13926
|
+
const locators = this.#blockKeyLocators(blockId, vector, keyColumn.type);
|
|
13927
|
+
const positions = [];
|
|
13928
|
+
const collect = (hit) => {
|
|
13929
|
+
if (hit === undefined)
|
|
13930
|
+
return false;
|
|
13931
|
+
if (typeof hit === "number")
|
|
13932
|
+
positions.push(hit);
|
|
13933
|
+
else
|
|
13934
|
+
positions.push(...hit);
|
|
13935
|
+
return rowPositions === undefined;
|
|
13936
|
+
};
|
|
13937
|
+
if (candidates.length <= locators.size) {
|
|
13938
|
+
for (const candidate of candidates)
|
|
13939
|
+
if (collect(locators.get(candidate)))
|
|
13940
|
+
break;
|
|
13941
|
+
}
|
|
13942
|
+
else if (candidateSet !== undefined) {
|
|
13943
|
+
for (const [locator, hit] of locators) {
|
|
13944
|
+
if (candidateSet.has(locator) && collect(hit))
|
|
13945
|
+
break;
|
|
13337
13946
|
}
|
|
13338
13947
|
}
|
|
13339
|
-
if (
|
|
13948
|
+
if (positions.length > 0) {
|
|
13949
|
+
if (rowPositions !== undefined &&
|
|
13950
|
+
(segment.kind === "insert" || segment.kind === "base")) {
|
|
13951
|
+
positions.sort((a, b) => a - b);
|
|
13952
|
+
const keptRows = rowCounts.reduce((total, count) => total + count, 0);
|
|
13953
|
+
for (const position of positions) {
|
|
13954
|
+
rowPositions.push(selectedRowStart + keptRows + position);
|
|
13955
|
+
}
|
|
13956
|
+
}
|
|
13340
13957
|
blockIndexes.push(blockIndex);
|
|
13341
13958
|
rowCounts.push(vector.length);
|
|
13342
13959
|
}
|
|
@@ -13347,16 +13964,21 @@ export class MinnowDatabase {
|
|
|
13347
13964
|
return { segments, pruned: false };
|
|
13348
13965
|
if (blockIndexes.length === 0)
|
|
13349
13966
|
continue;
|
|
13967
|
+
const keptRowCount = rowCounts.reduce((total, count) => total + count, 0);
|
|
13968
|
+
if (segment.kind === "insert" || segment.kind === "base")
|
|
13969
|
+
selectedRowStart += keptRowCount;
|
|
13350
13970
|
selected.push({
|
|
13351
13971
|
...segment,
|
|
13352
|
-
rowCount:
|
|
13972
|
+
rowCount: keptRowCount,
|
|
13353
13973
|
columnBlockIds: Object.fromEntries(Object.entries(segment.columnBlockIds).map(([columnId, ids]) => [
|
|
13354
13974
|
columnId,
|
|
13355
13975
|
blockIndexes.map((blockIndex) => ids[blockIndex] ?? ""),
|
|
13356
13976
|
])),
|
|
13357
13977
|
});
|
|
13358
13978
|
}
|
|
13359
|
-
return
|
|
13979
|
+
return rowPositions === undefined
|
|
13980
|
+
? { segments: selected, pruned: true }
|
|
13981
|
+
: { segments: selected, pruned: true, rows: rowPositions };
|
|
13360
13982
|
}
|
|
13361
13983
|
#scheduleSecondaryIndexBuild(table, indexId) {
|
|
13362
13984
|
if (this.#closed)
|
|
@@ -13542,6 +14164,21 @@ export class MinnowDatabase {
|
|
|
13542
14164
|
const projectedKey = keyColumn !== undefined && projectedColumns.some((column) => column.id === keyColumn.id)
|
|
13543
14165
|
? keyColumn.name
|
|
13544
14166
|
: undefined;
|
|
14167
|
+
// An exact row selection materializes just those rows: a join side narrowed to a handful
|
|
14168
|
+
// of rows is then a handful of rows, not a table's worth of decoded columns. The rows
|
|
14169
|
+
// come from a secondary index, or from the unique key itself when the plan pins it to
|
|
14170
|
+
// literals (zone maps and sorted-run search locate them without an index).
|
|
14171
|
+
if (keyColumn !== undefined) {
|
|
14172
|
+
const rows = indexed.rows ??
|
|
14173
|
+
(plan === undefined
|
|
14174
|
+
? undefined
|
|
14175
|
+
: await this.#uniqueKeyRowSelection(table, keyColumn, segments, plan, snapshot));
|
|
14176
|
+
if (rows !== undefined) {
|
|
14177
|
+
const selected = await this.#materializeSelectedRows(table, keyColumn, projectedColumns, segments, rows, snapshot, projectedKey);
|
|
14178
|
+
if (selected !== undefined)
|
|
14179
|
+
return selected;
|
|
14180
|
+
}
|
|
14181
|
+
}
|
|
13545
14182
|
// Zone-map pruning shrinks the scan the same way index pruning does, so a scoring plan
|
|
13546
14183
|
// whose statistics come from the scan (no index serving it) must see the whole corpus.
|
|
13547
14184
|
const zonePruningAllowed = plan === undefined ||
|
|
@@ -14172,7 +14809,170 @@ export class MinnowDatabase {
|
|
|
14172
14809
|
return decoded;
|
|
14173
14810
|
});
|
|
14174
14811
|
}
|
|
14812
|
+
/**
|
|
14813
|
+
* Scan positions of the rows whose unique key the plan pins to literals (`alias.key = 4`,
|
|
14814
|
+
* `alias.key IN (...)`), over append-only segments: zone maps skip blocks a numeric key
|
|
14815
|
+
* cannot be in, a sorted block is searched by run, and a string key goes through the block
|
|
14816
|
+
* dictionary. Undefined when the plan does not pin the key or a block cannot be searched.
|
|
14817
|
+
*/
|
|
14818
|
+
async #uniqueKeyRowSelection(table, keyColumn, segments, plan, snapshot) {
|
|
14819
|
+
const members = uniqueKeyMembers(plan, table, keyColumn);
|
|
14820
|
+
if (members === undefined || members.length > SECONDARY_INDEX_ROW_SELECTION_CAP) {
|
|
14821
|
+
return undefined;
|
|
14822
|
+
}
|
|
14823
|
+
const targets = new Set(members.map((value) => (value instanceof Date ? dateMilliseconds(value) : value)));
|
|
14824
|
+
const numericTargets = [...targets].filter((value) => typeof value === "number");
|
|
14825
|
+
const rows = [];
|
|
14826
|
+
let rowStart = 0;
|
|
14827
|
+
for (const segment of segments) {
|
|
14828
|
+
const segmentStart = rowStart;
|
|
14829
|
+
const blockIds = segment.columnBlockIds[keyColumn.id] ?? [];
|
|
14830
|
+
if (blockIds.length === 0 && segment.rowCount > 0)
|
|
14831
|
+
return undefined;
|
|
14832
|
+
if (Object.values(segment.columnBlockIds).some((ids) => ids.length !== blockIds.length)) {
|
|
14833
|
+
return undefined;
|
|
14834
|
+
}
|
|
14835
|
+
const descriptions = keyColumn.type === "number" || keyColumn.type === "datetime"
|
|
14836
|
+
? await this.#zoneDescriptions(blockIds, snapshot)
|
|
14837
|
+
: undefined;
|
|
14838
|
+
for (const blockId of blockIds) {
|
|
14839
|
+
const description = descriptions?.get(blockId);
|
|
14840
|
+
const zone = description?.metadata.zoneMap;
|
|
14841
|
+
if (description !== undefined && zone !== undefined) {
|
|
14842
|
+
if (!numericTargets.some((target) => zone.min <= target && target <= zone.max)) {
|
|
14843
|
+
rowStart += description.rowCount;
|
|
14844
|
+
continue;
|
|
14845
|
+
}
|
|
14846
|
+
}
|
|
14847
|
+
const [decoded] = await this.#decodedBlocksThroughCache([blockId], snapshot);
|
|
14848
|
+
if (decoded === undefined)
|
|
14849
|
+
return undefined;
|
|
14850
|
+
const vector = this.#blockColumnVector(blockId, decoded);
|
|
14851
|
+
if (vector.kind !== keyColumn.type)
|
|
14852
|
+
return undefined;
|
|
14853
|
+
const found = [];
|
|
14854
|
+
if (vector.kind === "number" || vector.kind === "datetime") {
|
|
14855
|
+
if (decoded.description.nullCount === 0 && valuesAreAscending(vector.values)) {
|
|
14856
|
+
for (const target of numericTargets) {
|
|
14857
|
+
const run = equalRunRange(vector.values, target);
|
|
14858
|
+
for (let slot = run.begin; slot < run.end; slot += 1)
|
|
14859
|
+
found.push(slot);
|
|
14860
|
+
}
|
|
14861
|
+
}
|
|
14862
|
+
else {
|
|
14863
|
+
for (let slot = 0; slot < vector.length; slot += 1) {
|
|
14864
|
+
const value = vector.values[slot] ?? 0;
|
|
14865
|
+
if (targets.has(value) && vectorValue(vector, slot) !== null)
|
|
14866
|
+
found.push(slot);
|
|
14867
|
+
}
|
|
14868
|
+
}
|
|
14869
|
+
}
|
|
14870
|
+
else if (vector.kind === "string") {
|
|
14871
|
+
const codes = new Set();
|
|
14872
|
+
for (const target of targets) {
|
|
14873
|
+
if (typeof target !== "string")
|
|
14874
|
+
continue;
|
|
14875
|
+
const code = this.#dictionaryCode(blockId, vector, target);
|
|
14876
|
+
if (code !== undefined)
|
|
14877
|
+
codes.add(code);
|
|
14878
|
+
}
|
|
14879
|
+
if (codes.size > 0) {
|
|
14880
|
+
for (let slot = 0; slot < vector.length; slot += 1) {
|
|
14881
|
+
const code = vector.codes[slot];
|
|
14882
|
+
if (code !== undefined && codes.has(code))
|
|
14883
|
+
found.push(slot);
|
|
14884
|
+
}
|
|
14885
|
+
}
|
|
14886
|
+
}
|
|
14887
|
+
else {
|
|
14888
|
+
for (let slot = 0; slot < vector.length; slot += 1) {
|
|
14889
|
+
if (targets.has(vectorValue(vector, slot)))
|
|
14890
|
+
found.push(slot);
|
|
14891
|
+
}
|
|
14892
|
+
}
|
|
14893
|
+
found.sort((a, b) => a - b);
|
|
14894
|
+
for (const slot of found)
|
|
14895
|
+
rows.push(rowStart + slot);
|
|
14896
|
+
rowStart += vector.length;
|
|
14897
|
+
}
|
|
14898
|
+
if (rowStart - segmentStart !== segment.rowCount)
|
|
14899
|
+
return undefined;
|
|
14900
|
+
}
|
|
14901
|
+
return rows;
|
|
14902
|
+
}
|
|
14903
|
+
/**
|
|
14904
|
+
* Materializes the rows at ascending scan positions over append-only segments, reading only
|
|
14905
|
+
* the blocks that hold a selected row. Undefined when the block layout does not line up,
|
|
14906
|
+
* which sends the caller to the whole-segment path.
|
|
14907
|
+
*/
|
|
14908
|
+
async #materializeSelectedRows(table, keyColumn, projectedColumns, segments, rows, snapshot, projectedKey) {
|
|
14909
|
+
const values = projectedColumns.map(() => []);
|
|
14910
|
+
let cursor = 0;
|
|
14911
|
+
let rowStart = 0;
|
|
14912
|
+
for (const segment of segments) {
|
|
14913
|
+
const anchorIds = segment.columnBlockIds[keyColumn.id] ?? [];
|
|
14914
|
+
const anchorBlocks = await this.#decodedBlocksThroughCache(anchorIds, snapshot);
|
|
14915
|
+
for (let blockIndex = 0; blockIndex < anchorIds.length; blockIndex += 1) {
|
|
14916
|
+
const count = anchorBlocks[blockIndex]?.column.rowCount ?? 0;
|
|
14917
|
+
const end = rowStart + count;
|
|
14918
|
+
const local = [];
|
|
14919
|
+
while (cursor < rows.length && (rows[cursor] ?? Number.POSITIVE_INFINITY) < end) {
|
|
14920
|
+
local.push((rows[cursor] ?? 0) - rowStart);
|
|
14921
|
+
cursor += 1;
|
|
14922
|
+
}
|
|
14923
|
+
rowStart = end;
|
|
14924
|
+
if (local.length === 0)
|
|
14925
|
+
continue;
|
|
14926
|
+
const blockIds = projectedColumns.map((column) => segment.columnBlockIds[column.id]?.[blockIndex]);
|
|
14927
|
+
if (blockIds.some((id) => id === undefined))
|
|
14928
|
+
return undefined;
|
|
14929
|
+
const decoded = await this.#decodedBlocksThroughCache(blockIds, snapshot);
|
|
14930
|
+
for (let index = 0; index < projectedColumns.length; index += 1) {
|
|
14931
|
+
const column = projectedColumns[index];
|
|
14932
|
+
const block = decoded[index];
|
|
14933
|
+
const blockId = blockIds[index];
|
|
14934
|
+
if (column === undefined || block === undefined || blockId === undefined)
|
|
14935
|
+
return undefined;
|
|
14936
|
+
if (block.column.rowCount !== count || block.column.type !== column.type)
|
|
14937
|
+
return undefined;
|
|
14938
|
+
const vector = plainTextExecutionVector(column, this.#blockColumnVector(blockId, block));
|
|
14939
|
+
const output = values[index];
|
|
14940
|
+
if (output === undefined)
|
|
14941
|
+
return undefined;
|
|
14942
|
+
for (const row of local)
|
|
14943
|
+
output.push(vectorValue(vector, row));
|
|
14944
|
+
}
|
|
14945
|
+
}
|
|
14946
|
+
}
|
|
14947
|
+
if (cursor !== rows.length)
|
|
14948
|
+
return undefined;
|
|
14949
|
+
return createColumnarTable(table.name, new Map(projectedColumns.map((column, index) => [
|
|
14950
|
+
column.name,
|
|
14951
|
+
{ type: column.type, values: values[index] ?? [] },
|
|
14952
|
+
])), projectedKey);
|
|
14953
|
+
}
|
|
14954
|
+
/**
|
|
14955
|
+
* A whole-segment column vector is a pure function of its immutable blocks, so the plain
|
|
14956
|
+
* shape (no caller-supplied bytes or row expectations) is pooled by segment list: a
|
|
14957
|
+
* dimension table joined on every query is then decoded once, not per query.
|
|
14958
|
+
*/
|
|
14175
14959
|
async #materializeAppendColumnVector(column, segments, snapshot, rowCount, storedBlocks, expectedRows) {
|
|
14960
|
+
const poolable = storedBlocks === undefined && expectedRows === undefined;
|
|
14961
|
+
const key = poolable
|
|
14962
|
+
? `mav\0${column.id}\0${String(rowCount)}\0${segments.map((segment) => segment.id).join("\0")}`
|
|
14963
|
+
: undefined;
|
|
14964
|
+
if (key !== undefined) {
|
|
14965
|
+
const cached = this.#cacheGet(key);
|
|
14966
|
+
if (cached !== undefined)
|
|
14967
|
+
return cached;
|
|
14968
|
+
}
|
|
14969
|
+
const vector = await this.#buildAppendColumnVector(column, segments, snapshot, rowCount, storedBlocks, expectedRows);
|
|
14970
|
+
if (key !== undefined && key.length <= MAX_CACHEABLE_TEXT_CHARACTERS) {
|
|
14971
|
+
this.#cachePut(key, vector, 128 + columnVectorPayloadBytes(vector));
|
|
14972
|
+
}
|
|
14973
|
+
return vector;
|
|
14974
|
+
}
|
|
14975
|
+
async #buildAppendColumnVector(column, segments, snapshot, rowCount, storedBlocks, expectedRows) {
|
|
14176
14976
|
const validity = new Uint8Array(Math.ceil(rowCount / 8));
|
|
14177
14977
|
const values = column.type === "boolean"
|
|
14178
14978
|
? new Uint8Array(rowCount)
|
|
@@ -16001,10 +16801,42 @@ function normalizeDomainUpdate(table, input) {
|
|
|
16001
16801
|
: input.keys.map((value) => normalizeColumnLogicalValue(key, value));
|
|
16002
16802
|
return changed || keys !== input.keys ? { keys, changes } : input;
|
|
16003
16803
|
}
|
|
16004
|
-
/**
|
|
16005
|
-
|
|
16006
|
-
|
|
16804
|
+
/**
|
|
16805
|
+
* Coerces literals and bound parameters opposite a domain column before planning/index probes,
|
|
16806
|
+
* and reads an untyped string constant beside a datetime, number, or boolean column in that
|
|
16807
|
+
* column's type — `joined >= '2026-01-01'`, `id = '5'`, `active = 't'` — the way PostgreSQL
|
|
16808
|
+
* types an unknown-typed literal by its context. Text that does not parse is left alone and
|
|
16809
|
+
* fails at comparison time as a type mismatch. Coercing here, rather than only at comparison,
|
|
16810
|
+
* keeps the typed kernels, zone-map pruning, and the keyed point read on the fast paths.
|
|
16811
|
+
*/
|
|
16812
|
+
function normalizePlanDomainLiterals(input, domains, types = new Map()) {
|
|
16813
|
+
// Most plans have nothing to coerce. A probing pass over the shared (cached) plan finds out
|
|
16814
|
+
// without touching it; only a plan that needs a rewrite is cloned and rewritten, so the
|
|
16815
|
+
// per-query cost of a plan that compares typed columns to typed literals is one walk, not a
|
|
16816
|
+
// structured clone.
|
|
16817
|
+
const pass = { probing: true, needed: false };
|
|
16818
|
+
const coerceText = (type, text) => {
|
|
16819
|
+
if (type === "datetime")
|
|
16820
|
+
return parseSqlTimestampText(text);
|
|
16821
|
+
if (type === "number") {
|
|
16822
|
+
const trimmed = text.trim();
|
|
16823
|
+
if (trimmed === "")
|
|
16824
|
+
return undefined;
|
|
16825
|
+
const parsed = Number(trimmed);
|
|
16826
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
16827
|
+
}
|
|
16828
|
+
if (type === "boolean") {
|
|
16829
|
+
const lowered = text.trim().toLowerCase();
|
|
16830
|
+
if (lowered === "t" || lowered === "true" || lowered === "1")
|
|
16831
|
+
return true;
|
|
16832
|
+
if (lowered === "f" || lowered === "false" || lowered === "0")
|
|
16833
|
+
return false;
|
|
16834
|
+
}
|
|
16835
|
+
return undefined;
|
|
16836
|
+
};
|
|
16007
16837
|
const rewriteBlock = (block) => {
|
|
16838
|
+
if (pass.probing && pass.needed)
|
|
16839
|
+
return;
|
|
16008
16840
|
for (const source of [block.base, ...block.joins]) {
|
|
16009
16841
|
if (source.derived !== undefined)
|
|
16010
16842
|
rewriteBlock(source.derived);
|
|
@@ -16017,27 +16849,55 @@ function normalizePlanDomainLiterals(input, domains) {
|
|
|
16017
16849
|
}
|
|
16018
16850
|
}
|
|
16019
16851
|
const sources = [block.base, ...block.joins];
|
|
16020
|
-
const
|
|
16852
|
+
const lookup = (reference, catalog) => {
|
|
16021
16853
|
const separator = reference.indexOf(".");
|
|
16022
16854
|
if (separator !== -1) {
|
|
16023
16855
|
const alias = reference.slice(0, separator);
|
|
16024
16856
|
const name = reference.slice(separator + 1);
|
|
16025
16857
|
const source = sources.find((candidate) => candidate.alias === alias);
|
|
16026
|
-
return source === undefined ? undefined :
|
|
16858
|
+
return source === undefined ? undefined : catalog.get(source.table)?.get(name);
|
|
16027
16859
|
}
|
|
16028
16860
|
const matches = sources.flatMap((source) => {
|
|
16029
|
-
const
|
|
16030
|
-
return
|
|
16861
|
+
const found = catalog.get(source.table)?.get(reference);
|
|
16862
|
+
return found === undefined ? [] : [found];
|
|
16031
16863
|
});
|
|
16032
16864
|
return matches.length === 1 ? matches[0] : undefined;
|
|
16033
16865
|
};
|
|
16866
|
+
const domainFor = (reference) => lookup(reference, domains);
|
|
16867
|
+
const coerceTyped = (column, value) => {
|
|
16868
|
+
if (column.kind !== "column")
|
|
16869
|
+
return;
|
|
16870
|
+
const type = lookup(column.reference, types);
|
|
16871
|
+
if (type === undefined || type === "string")
|
|
16872
|
+
return;
|
|
16873
|
+
const literals = value.kind === "literal" ? [value] : value.kind === "list" ? value.items : [];
|
|
16874
|
+
for (const literal of literals) {
|
|
16875
|
+
if (literal.kind !== "literal" || typeof literal.value !== "string")
|
|
16876
|
+
continue;
|
|
16877
|
+
if (literal.internalSqlValue === true)
|
|
16878
|
+
continue;
|
|
16879
|
+
const coerced = coerceText(type, literal.value);
|
|
16880
|
+
if (coerced === undefined)
|
|
16881
|
+
continue;
|
|
16882
|
+
if (pass.probing)
|
|
16883
|
+
pass.needed = true;
|
|
16884
|
+
else
|
|
16885
|
+
literal.value = coerced;
|
|
16886
|
+
}
|
|
16887
|
+
};
|
|
16034
16888
|
const coercePair = (column, value) => {
|
|
16035
16889
|
if (column.kind !== "column")
|
|
16036
16890
|
return;
|
|
16037
16891
|
const domain = domainFor(column.reference);
|
|
16038
|
-
if (domain === undefined)
|
|
16892
|
+
if (domain === undefined) {
|
|
16893
|
+
coerceTyped(column, value);
|
|
16039
16894
|
return;
|
|
16895
|
+
}
|
|
16040
16896
|
if (value.kind === "literal") {
|
|
16897
|
+
if (pass.probing) {
|
|
16898
|
+
pass.needed = true;
|
|
16899
|
+
return;
|
|
16900
|
+
}
|
|
16041
16901
|
value.value = normalizeSqlDomainValue(domain, value.value);
|
|
16042
16902
|
value.internalSqlValue = true;
|
|
16043
16903
|
value.sqlDomain = domain;
|
|
@@ -16045,6 +16905,10 @@ function normalizePlanDomainLiterals(input, domains) {
|
|
|
16045
16905
|
else if (value.kind === "list") {
|
|
16046
16906
|
for (const item of value.items) {
|
|
16047
16907
|
if (item.kind === "literal") {
|
|
16908
|
+
if (pass.probing) {
|
|
16909
|
+
pass.needed = true;
|
|
16910
|
+
return;
|
|
16911
|
+
}
|
|
16048
16912
|
item.value = normalizeSqlDomainValue(domain, item.value);
|
|
16049
16913
|
item.internalSqlValue = true;
|
|
16050
16914
|
item.sqlDomain = domain;
|
|
@@ -16120,6 +16984,11 @@ function normalizePlanDomainLiterals(input, domains) {
|
|
|
16120
16984
|
for (const order of block.orderBy)
|
|
16121
16985
|
order.expression = rewrite(order.expression);
|
|
16122
16986
|
};
|
|
16987
|
+
rewriteBlock(input);
|
|
16988
|
+
if (!pass.needed)
|
|
16989
|
+
return input;
|
|
16990
|
+
pass.probing = false;
|
|
16991
|
+
const plan = structuredClone(input);
|
|
16123
16992
|
rewriteBlock(plan);
|
|
16124
16993
|
return plan;
|
|
16125
16994
|
}
|
|
@@ -16520,6 +17389,19 @@ function createStreamedColumnVector(type, length) {
|
|
|
16520
17389
|
* deliberately keep their tags; ordinary TEXT only copies the rare dictionary that collides
|
|
16521
17390
|
* with that namespace, preserving the cached zero-copy vector for every normal dictionary.
|
|
16522
17391
|
*/
|
|
17392
|
+
/** Bytes a materialized vector retains, for the buffer pool's accounting. */
|
|
17393
|
+
function columnVectorPayloadBytes(vector) {
|
|
17394
|
+
let total = vector.validity.byteLength;
|
|
17395
|
+
if (vector.kind === "string") {
|
|
17396
|
+
total += vector.codes.byteLength;
|
|
17397
|
+
for (const value of vector.dictionary)
|
|
17398
|
+
total += 16 + value.length * 2;
|
|
17399
|
+
}
|
|
17400
|
+
else {
|
|
17401
|
+
total += vector.values.byteLength;
|
|
17402
|
+
}
|
|
17403
|
+
return total;
|
|
17404
|
+
}
|
|
16523
17405
|
function plainTextExecutionVector(column, vector) {
|
|
16524
17406
|
if (column.type !== "string" ||
|
|
16525
17407
|
column.sqlDomain !== undefined ||
|
|
@@ -17318,13 +18200,23 @@ function zonePredicates(plan, table) {
|
|
|
17318
18200
|
}
|
|
17319
18201
|
/** Top-level scalar predicates a ready index can answer through a leftmost key prefix. */
|
|
17320
18202
|
function secondaryIndexPredicates(plan, table) {
|
|
17321
|
-
|
|
18203
|
+
// The table may be the base or one join source; a self-join names it twice, and a
|
|
18204
|
+
// predicate on one alias says nothing about the other, so that shape gets no index.
|
|
18205
|
+
const sources = [plan.base, ...plan.joins].filter((source) => source.table === table.name);
|
|
18206
|
+
const source = sources[0];
|
|
18207
|
+
if (source === undefined || sources.length !== 1)
|
|
17322
18208
|
return [];
|
|
18209
|
+
// In a join, an unqualified reference is resolved at execution against every source, so only
|
|
18210
|
+
// a reference qualified by this source's alias (or the table name) can be claimed for its
|
|
18211
|
+
// index.
|
|
18212
|
+
const joined = plan.joins.length > 0;
|
|
17323
18213
|
const resolveColumn = (reference) => {
|
|
17324
18214
|
const parts = reference.split(".");
|
|
17325
|
-
if (parts.length === 2 && parts[0] !==
|
|
18215
|
+
if (parts.length === 2 && parts[0] !== source.alias && parts[0] !== table.name) {
|
|
17326
18216
|
return undefined;
|
|
17327
18217
|
}
|
|
18218
|
+
if (parts.length !== 2 && joined)
|
|
18219
|
+
return undefined;
|
|
17328
18220
|
const name = parts.length === 2 ? parts[1] : parts[0];
|
|
17329
18221
|
return table.columns.find((candidate) => candidate.name === name);
|
|
17330
18222
|
};
|
|
@@ -17583,6 +18475,118 @@ function firstMemberAtLeast(members, value) {
|
|
|
17583
18475
|
}
|
|
17584
18476
|
return low;
|
|
17585
18477
|
}
|
|
18478
|
+
const sequenceFunctionNames = new Set(["NEXTVAL", "CURRVAL"]);
|
|
18479
|
+
const nonDeterministicFunctionNames = new Set([
|
|
18480
|
+
...volatileScalarFunctionNames,
|
|
18481
|
+
...sequenceFunctionNames,
|
|
18482
|
+
"CURRENT_DATE",
|
|
18483
|
+
"CURRENT_TIMESTAMP",
|
|
18484
|
+
"CURRENT_TIME",
|
|
18485
|
+
"LOCALTIME",
|
|
18486
|
+
"LOCALTIMESTAMP",
|
|
18487
|
+
"NOW",
|
|
18488
|
+
]);
|
|
18489
|
+
/** Whether any expression of a block, or of a block nested anywhere under it, calls a name. */
|
|
18490
|
+
function blockCallsFunctions(block, names) {
|
|
18491
|
+
const state = { found: false };
|
|
18492
|
+
const visit = (expression) => {
|
|
18493
|
+
if (state.found)
|
|
18494
|
+
return;
|
|
18495
|
+
if (expression.kind === "call" && names.has(expression.name)) {
|
|
18496
|
+
state.found = true;
|
|
18497
|
+
return;
|
|
18498
|
+
}
|
|
18499
|
+
if (expression.kind === "subquery" || expression.kind === "exists") {
|
|
18500
|
+
if (blockCallsFunctions(expression.block, names))
|
|
18501
|
+
state.found = true;
|
|
18502
|
+
return;
|
|
18503
|
+
}
|
|
18504
|
+
childExpressions(expression).forEach(visit);
|
|
18505
|
+
};
|
|
18506
|
+
forEachBlockExpression(block, visit);
|
|
18507
|
+
forEachNestedBlock(block, (nested) => {
|
|
18508
|
+
if (!state.found && blockCallsFunctions(nested, names))
|
|
18509
|
+
state.found = true;
|
|
18510
|
+
});
|
|
18511
|
+
return state.found;
|
|
18512
|
+
}
|
|
18513
|
+
/**
|
|
18514
|
+
* The literal values a plan's conjunctive predicates pin one table's unique key to, through
|
|
18515
|
+
* `alias.key = literal` and `alias.key IN (literals)`; undefined when the key is unconstrained.
|
|
18516
|
+
* References are resolved with the same alias rules as secondary-index predicates.
|
|
18517
|
+
*/
|
|
18518
|
+
function uniqueKeyMembers(plan, table, keyColumn) {
|
|
18519
|
+
const sources = [plan.base, ...plan.joins].filter((source) => source.table === table.name);
|
|
18520
|
+
const source = sources[0];
|
|
18521
|
+
if (source === undefined || sources.length !== 1)
|
|
18522
|
+
return undefined;
|
|
18523
|
+
const joined = plan.joins.length > 0;
|
|
18524
|
+
const isKey = (expression) => {
|
|
18525
|
+
if (expression.kind !== "column")
|
|
18526
|
+
return false;
|
|
18527
|
+
const parts = expression.reference.split(".");
|
|
18528
|
+
if (parts.length === 2) {
|
|
18529
|
+
return (parts[0] === source.alias || parts[0] === table.name) && parts[1] === keyColumn.name;
|
|
18530
|
+
}
|
|
18531
|
+
return !joined && parts[0] === keyColumn.name;
|
|
18532
|
+
};
|
|
18533
|
+
const literal = (expression) => {
|
|
18534
|
+
if (expression.kind !== "literal" || expression.value === null)
|
|
18535
|
+
return undefined;
|
|
18536
|
+
let value = expression.value;
|
|
18537
|
+
if (keyColumn.type === "datetime" && isDateDomainValue(value)) {
|
|
18538
|
+
const datetime = normalizedDatetimeLiteral(value);
|
|
18539
|
+
if (datetime === undefined)
|
|
18540
|
+
return undefined;
|
|
18541
|
+
value = datetime;
|
|
18542
|
+
}
|
|
18543
|
+
if (keyColumn.type === "datetime" && !(value instanceof Date))
|
|
18544
|
+
return undefined;
|
|
18545
|
+
if (keyColumn.type === "number" && typeof value !== "number")
|
|
18546
|
+
return undefined;
|
|
18547
|
+
if (keyColumn.type === "string" && typeof value !== "string")
|
|
18548
|
+
return undefined;
|
|
18549
|
+
if (keyColumn.type === "boolean" && typeof value !== "boolean")
|
|
18550
|
+
return undefined;
|
|
18551
|
+
return value;
|
|
18552
|
+
};
|
|
18553
|
+
let members;
|
|
18554
|
+
for (const predicate of plan.predicates) {
|
|
18555
|
+
let found;
|
|
18556
|
+
if (predicate.operator === "=") {
|
|
18557
|
+
const value = isKey(predicate.left)
|
|
18558
|
+
? literal(predicate.right)
|
|
18559
|
+
: isKey(predicate.right)
|
|
18560
|
+
? literal(predicate.left)
|
|
18561
|
+
: undefined;
|
|
18562
|
+
if (value !== undefined)
|
|
18563
|
+
found = [value];
|
|
18564
|
+
}
|
|
18565
|
+
else if (predicate.operator === "IN" &&
|
|
18566
|
+
isKey(predicate.left) &&
|
|
18567
|
+
predicate.right.kind === "list") {
|
|
18568
|
+
const values = [];
|
|
18569
|
+
for (const item of predicate.right.items) {
|
|
18570
|
+
const value = literal(item);
|
|
18571
|
+
if (value === undefined) {
|
|
18572
|
+
if (item.kind === "literal" && item.value === null)
|
|
18573
|
+
continue;
|
|
18574
|
+
values.length = 0;
|
|
18575
|
+
break;
|
|
18576
|
+
}
|
|
18577
|
+
values.push(value);
|
|
18578
|
+
}
|
|
18579
|
+
if (values.length > 0)
|
|
18580
|
+
found = values;
|
|
18581
|
+
}
|
|
18582
|
+
if (found === undefined)
|
|
18583
|
+
continue;
|
|
18584
|
+
// Two constraints on the key intersect; the narrower one is enough for a superset.
|
|
18585
|
+
if (members === undefined || found.length < members.length)
|
|
18586
|
+
members = found;
|
|
18587
|
+
}
|
|
18588
|
+
return members;
|
|
18589
|
+
}
|
|
17586
18590
|
function reverseComparison(operator) {
|
|
17587
18591
|
if (operator === ">")
|
|
17588
18592
|
return "<";
|