@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/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, 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, 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 = {};
|
|
@@ -2738,6 +2811,11 @@ export class MinnowDatabase {
|
|
|
2738
2811
|
async #prepareCompiledPlan(plan, options = {}, probe) {
|
|
2739
2812
|
options = this.#effectiveQueryOptions(options);
|
|
2740
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());
|
|
2741
2819
|
// The ORDER-BY-expression desugar's wrapper is projection-only: prepare the inner block
|
|
2742
2820
|
// directly (no derived materialization) and project each result to the visible aliases,
|
|
2743
2821
|
// so `.search()` costs the same whether or not the caller also selects the score.
|
|
@@ -3312,6 +3390,9 @@ export class MinnowDatabase {
|
|
|
3312
3390
|
// cannot tell a fresh answer from a stale one.
|
|
3313
3391
|
plan.usesStatementDatetime !== true &&
|
|
3314
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 &&
|
|
3315
3396
|
options.version === undefined &&
|
|
3316
3397
|
options.executionMemoryBudgetBytes === undefined &&
|
|
3317
3398
|
options.spillToStorage === undefined &&
|
|
@@ -3461,7 +3542,7 @@ export class MinnowDatabase {
|
|
|
3461
3542
|
// read has to ask the catalog. It asks by epoch — an O(1) probe the store already serves for
|
|
3462
3543
|
// result memoization — and only re-reads the view set when the catalog has actually moved.
|
|
3463
3544
|
// A database with no views therefore pays one probe, not a catalog scan per query.
|
|
3464
|
-
const { views, domains, columns: catalogColumns } = await this.#catalogFacts(probe);
|
|
3545
|
+
const { views, domains, types, columns: catalogColumns } = await this.#catalogFacts(probe);
|
|
3465
3546
|
let rewritten = plan.usesSequenceCalls === true ? await this.#resolveSequenceCalls(plan) : plan;
|
|
3466
3547
|
if (views.size > 0 && planReadsViews(plan, (name) => views.has(name))) {
|
|
3467
3548
|
const bodies = new Map();
|
|
@@ -3477,8 +3558,8 @@ export class MinnowDatabase {
|
|
|
3477
3558
|
return compiled;
|
|
3478
3559
|
});
|
|
3479
3560
|
}
|
|
3480
|
-
if (
|
|
3481
|
-
rewritten = normalizePlanDomainLiterals(rewritten, domains);
|
|
3561
|
+
if (planReadsTable(rewritten, (name) => domains.has(name) || types.has(name))) {
|
|
3562
|
+
rewritten = normalizePlanDomainLiterals(rewritten, domains, types);
|
|
3482
3563
|
}
|
|
3483
3564
|
const columnsOf = (name) => {
|
|
3484
3565
|
const columns = catalogColumns.get(name);
|
|
@@ -3494,6 +3575,22 @@ export class MinnowDatabase {
|
|
|
3494
3575
|
const qualified = qualifyCorrelatedReferences(rewritten, catalogColumns);
|
|
3495
3576
|
return qualified === rewritten ? rewritten : optimizePlan(qualified);
|
|
3496
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
|
+
};
|
|
3593
|
+
}
|
|
3497
3594
|
/** Resolves connection-local sequence calls before either synchronous executor sees the plan. */
|
|
3498
3595
|
async #resolveSequenceCalls(plan) {
|
|
3499
3596
|
const usesSequence = (expression) => (expression.kind === "call" &&
|
|
@@ -3514,8 +3611,12 @@ export class MinnowDatabase {
|
|
|
3514
3611
|
throw new TypeError("NEXTVAL and CURRVAL are supported in the SELECT list");
|
|
3515
3612
|
}
|
|
3516
3613
|
});
|
|
3517
|
-
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
|
+
}
|
|
3518
3618
|
return plan;
|
|
3619
|
+
}
|
|
3519
3620
|
if (plan.base.table !== DUAL_TABLE || plan.joins.length > 0) {
|
|
3520
3621
|
throw new TypeError("NEXTVAL and CURRVAL currently require a SELECT without FROM");
|
|
3521
3622
|
}
|
|
@@ -3599,8 +3700,11 @@ export class MinnowDatabase {
|
|
|
3599
3700
|
const childKeys = new Map();
|
|
3600
3701
|
const domains = new Map();
|
|
3601
3702
|
const columns = new Map();
|
|
3703
|
+
const types = new Map();
|
|
3602
3704
|
for (const table of await this.store.listTables()) {
|
|
3603
|
-
|
|
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])));
|
|
3604
3708
|
const tableDomains = new Map(table.columns.flatMap((column) => column.sqlDomain === undefined ? [] : [[column.name, column.sqlDomain]]));
|
|
3605
3709
|
if (tableDomains.size > 0)
|
|
3606
3710
|
domains.set(table.name, tableDomains);
|
|
@@ -3614,7 +3718,7 @@ export class MinnowDatabase {
|
|
|
3614
3718
|
existing.push({ table, key });
|
|
3615
3719
|
}
|
|
3616
3720
|
}
|
|
3617
|
-
const facts = { views, childKeys, domains, columns };
|
|
3721
|
+
const facts = { views, childKeys, domains, columns, types };
|
|
3618
3722
|
this.#catalogCache = { epoch, facts };
|
|
3619
3723
|
return facts;
|
|
3620
3724
|
}
|
|
@@ -3742,7 +3846,10 @@ export class MinnowDatabase {
|
|
|
3742
3846
|
// paths cross the result boundary through the same externalizeQueryResult call, so
|
|
3743
3847
|
// reporting the column's domain below makes the answers identical by construction.
|
|
3744
3848
|
const projected = [];
|
|
3745
|
-
|
|
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) {
|
|
3746
3853
|
const column = columnByName.get(item.column);
|
|
3747
3854
|
if (column === undefined || column.hidden === true)
|
|
3748
3855
|
return undefined;
|
|
@@ -3753,7 +3860,17 @@ export class MinnowDatabase {
|
|
|
3753
3860
|
const kind = segment.kind;
|
|
3754
3861
|
return kind !== "insert" && kind !== "base";
|
|
3755
3862
|
})) {
|
|
3756
|
-
|
|
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);
|
|
3757
3874
|
}
|
|
3758
3875
|
// The most selective searchable component wins nothing provable without statistics, so
|
|
3759
3876
|
// prefer a numeric component (zone-map pruning plus binary search) over a string one
|
|
@@ -3904,7 +4021,177 @@ export class MinnowDatabase {
|
|
|
3904
4021
|
}
|
|
3905
4022
|
}
|
|
3906
4023
|
return {
|
|
3907
|
-
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),
|
|
3908
4195
|
columnDomains: projected.map((item) => item.column.sqlDomain ?? null),
|
|
3909
4196
|
rows,
|
|
3910
4197
|
};
|
|
@@ -6212,8 +6499,12 @@ export class MinnowDatabase {
|
|
|
6212
6499
|
else {
|
|
6213
6500
|
notes.push("materializes inputs at preparation");
|
|
6214
6501
|
}
|
|
6215
|
-
const table = reported.base.derived ??
|
|
6216
|
-
|
|
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) {
|
|
6217
6508
|
const record = await this.#findTable(reported.base.table);
|
|
6218
6509
|
const pointTemplate = record.view === undefined ? cachedPointReadTemplate(reported) : null;
|
|
6219
6510
|
if (pointTemplate !== null) {
|
|
@@ -6314,9 +6605,9 @@ export class MinnowDatabase {
|
|
|
6314
6605
|
if (!isTransactionalStatement(boundStatement)) {
|
|
6315
6606
|
throw new TypeError(`${boundStatement.kind.toUpperCase().replace("-", " ")} is not allowed inside a transaction`);
|
|
6316
6607
|
}
|
|
6317
|
-
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))));
|
|
6318
6609
|
}
|
|
6319
|
-
return externalizeExecuteResult(await this.runStatement(bindStatementParameters(statement, params)));
|
|
6610
|
+
return externalizeExecuteResult(await this.#projectReturningItems(statement, await this.runStatement(bindStatementParameters(statement, params))));
|
|
6320
6611
|
}
|
|
6321
6612
|
/**
|
|
6322
6613
|
* E141-04 on the child side: every non-null value written into a referencing column must name
|
|
@@ -6942,13 +7233,16 @@ export class MinnowDatabase {
|
|
|
6942
7233
|
async #filterConflictingInsertRows(statement, writer) {
|
|
6943
7234
|
const table = await this.#findTable(statement.table);
|
|
6944
7235
|
const keyColumn = getUniqueKeyColumn(table);
|
|
6945
|
-
const conflictColumns = statement.onConflict?.columns ??
|
|
6946
|
-
(statement.onConflict === undefined ? [] : [statement.onConflict.column]);
|
|
6947
7236
|
const addressColumns = keyColumn?.hidden === true
|
|
6948
7237
|
? primaryKeyColumns(table).map(({ name }) => name)
|
|
6949
7238
|
: keyColumn === undefined
|
|
6950
7239
|
? []
|
|
6951
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]));
|
|
6952
7246
|
if (keyColumn === undefined ||
|
|
6953
7247
|
conflictColumns.length !== addressColumns.length ||
|
|
6954
7248
|
conflictColumns.some((name, index) => name !== addressColumns[index])) {
|
|
@@ -7008,6 +7302,37 @@ export class MinnowDatabase {
|
|
|
7008
7302
|
}),
|
|
7009
7303
|
};
|
|
7010
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
|
+
}
|
|
7011
7336
|
/**
|
|
7012
7337
|
* MERGE (F312). One pass over the source, joined to the target on the match condition, decides
|
|
7013
7338
|
* each row's branch; the branches then apply as ordinary batched writes inside a single write
|
|
@@ -7586,10 +7911,14 @@ export class MinnowDatabase {
|
|
|
7586
7911
|
if (statement.kind === "insert") {
|
|
7587
7912
|
statement = await this.#materializeInsertExpressions(statement, statementNow);
|
|
7588
7913
|
}
|
|
7589
|
-
// A RETURNING clause parsed from SQL text applies unless the caller overrides it.
|
|
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.
|
|
7590
7916
|
if (statement.returning !== undefined && options.returning === undefined) {
|
|
7591
7917
|
options = { ...options, returning: statement.returning };
|
|
7592
7918
|
}
|
|
7919
|
+
else if (statement.returningItems !== undefined && options.returning === undefined) {
|
|
7920
|
+
options = { ...options, returning: "*" };
|
|
7921
|
+
}
|
|
7593
7922
|
if (statement.kind === "insert" && statement.onConflict?.action === "nothing") {
|
|
7594
7923
|
statement = await this.#filterConflictingInsertRows(statement, options.writer);
|
|
7595
7924
|
}
|
|
@@ -7654,6 +7983,20 @@ export class MinnowDatabase {
|
|
|
7654
7983
|
}
|
|
7655
7984
|
}
|
|
7656
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
|
+
}
|
|
7657
8000
|
// A scope stages and reports rows; a standalone write also publishes a version and any
|
|
7658
8001
|
// values the engine generated. Both shapes answer here, and the narrow one has less to
|
|
7659
8002
|
// report rather than something different.
|
|
@@ -7723,21 +8066,26 @@ export class MinnowDatabase {
|
|
|
7723
8066
|
}));
|
|
7724
8067
|
})();
|
|
7725
8068
|
const referenced = new Set([keyColumn.name, ...(returningColumns ?? [])]);
|
|
7726
|
-
|
|
7727
|
-
|
|
7728
|
-
|
|
7729
|
-
|
|
7730
|
-
}
|
|
7731
|
-
}
|
|
7732
|
-
}
|
|
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}`;
|
|
7733
8073
|
const plan = optimizePlan({
|
|
7734
8074
|
sql: `(${statement.kind})`,
|
|
7735
|
-
base: { table: table.name, alias: table.name },
|
|
8075
|
+
base: { table: table.name, alias: statement.alias ?? table.name },
|
|
7736
8076
|
joins: [],
|
|
7737
|
-
select: [
|
|
7738
|
-
|
|
7739
|
-
|
|
7740
|
-
|
|
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
|
+
],
|
|
7741
8089
|
predicates: statement.predicates,
|
|
7742
8090
|
groupBy: [],
|
|
7743
8091
|
having: [],
|
|
@@ -7834,7 +8182,7 @@ export class MinnowDatabase {
|
|
|
7834
8182
|
throw new TypeError(`Unknown column: ${assignment.column}`);
|
|
7835
8183
|
const executionValues = returnedChanges === undefined ? undefined : [];
|
|
7836
8184
|
changes[assignment.column] = rows.map((row) => {
|
|
7837
|
-
const value =
|
|
8185
|
+
const value = row[assignmentAlias(assignment.column)] ?? null;
|
|
7838
8186
|
if (typeof value === "number" && !Number.isFinite(value)) {
|
|
7839
8187
|
throw new TypeError(`UPDATE assignment produced a non-finite number: ${assignment.column}`);
|
|
7840
8188
|
}
|
|
@@ -8007,8 +8355,11 @@ export class MinnowDatabase {
|
|
|
8007
8355
|
throwIfAborted(options.signal);
|
|
8008
8356
|
const baseSegments = indexed.segments;
|
|
8009
8357
|
// Zone-map elimination composes after index pruning: whole row groups whose statistics
|
|
8010
|
-
// reject the plan's predicates never stream at all.
|
|
8011
|
-
|
|
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;
|
|
8012
8363
|
throwIfAborted(options.signal);
|
|
8013
8364
|
const baseView = this.#streamedViewFactory(baseTable, projectedBaseColumns, zonePruned?.segments ?? baseSegments, snapshot, zonePruned?.storedBlocks, zonePruned !== undefined || indexed.pruned);
|
|
8014
8365
|
if (baseView === undefined)
|
|
@@ -8049,7 +8400,7 @@ export class MinnowDatabase {
|
|
|
8049
8400
|
if (table.name === baseTable.name)
|
|
8050
8401
|
continue;
|
|
8051
8402
|
const requestedColumns = columns.get(table.name) ?? [];
|
|
8052
|
-
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));
|
|
8053
8404
|
throwIfAborted(options.signal);
|
|
8054
8405
|
}
|
|
8055
8406
|
prepared = createPreparedColumnarQuery(plan, inputTables, memory, {
|
|
@@ -8093,6 +8444,9 @@ export class MinnowDatabase {
|
|
|
8093
8444
|
try {
|
|
8094
8445
|
const streamedResult = await prepared.executeAsync({
|
|
8095
8446
|
loadScanWindow: streamed.load,
|
|
8447
|
+
...(indexed.rows === undefined
|
|
8448
|
+
? {}
|
|
8449
|
+
: { scanRows: streamed.remapScanRows?.(indexed.rows) ?? indexed.rows }),
|
|
8096
8450
|
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
8097
8451
|
});
|
|
8098
8452
|
options.onStats?.({ peakMemoryBytes: memory.usage.peakBytes });
|
|
@@ -8108,6 +8462,9 @@ export class MinnowDatabase {
|
|
|
8108
8462
|
...(spillPageRows === undefined ? {} : { spillPageRows }),
|
|
8109
8463
|
spillStore: this.#leasedSpillStore(),
|
|
8110
8464
|
loadScanWindow: streamed.load,
|
|
8465
|
+
...(indexed.rows === undefined
|
|
8466
|
+
? {}
|
|
8467
|
+
: { scanRows: streamed.remapScanRows?.(indexed.rows) ?? indexed.rows }),
|
|
8111
8468
|
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
8112
8469
|
});
|
|
8113
8470
|
options.onStats?.({ peakMemoryBytes: memory.usage.peakBytes });
|
|
@@ -8466,6 +8823,33 @@ export class MinnowDatabase {
|
|
|
8466
8823
|
* dictionary objects that stay identical across queries, which keeps per-dictionary
|
|
8467
8824
|
* expression caches (equality codes, LIKE match sets) hot between statements.
|
|
8468
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
|
+
}
|
|
8469
8853
|
#blockColumnVector(blockId, decoded) {
|
|
8470
8854
|
const cached = this.#cacheGet(`dbv ${blockId}`);
|
|
8471
8855
|
if (cached !== undefined)
|
|
@@ -8757,6 +9141,26 @@ export class MinnowDatabase {
|
|
|
8757
9141
|
cursorBase = baseEnd;
|
|
8758
9142
|
return start + liveRows;
|
|
8759
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
|
+
};
|
|
8760
9164
|
return {
|
|
8761
9165
|
table: {
|
|
8762
9166
|
name: table.name,
|
|
@@ -8764,6 +9168,7 @@ export class MinnowDatabase {
|
|
|
8764
9168
|
columns: new Map(states.map((state) => [state.column.name, state.vector])),
|
|
8765
9169
|
},
|
|
8766
9170
|
load,
|
|
9171
|
+
remapScanRows,
|
|
8767
9172
|
};
|
|
8768
9173
|
}
|
|
8769
9174
|
/**
|
|
@@ -9108,6 +9513,10 @@ export class MinnowDatabase {
|
|
|
9108
9513
|
return undefined;
|
|
9109
9514
|
if (!this.#artifactCache.enabled)
|
|
9110
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;
|
|
9111
9520
|
// Staged writes can change repeatedly without changing either the pinned manifest version or
|
|
9112
9521
|
// their transaction id. Do not cache them under an identity that could become stale.
|
|
9113
9522
|
if (visibility.overlayTransactionId !== undefined)
|
|
@@ -13235,11 +13644,29 @@ export class MinnowDatabase {
|
|
|
13235
13644
|
const cached = memo.get(key);
|
|
13236
13645
|
if (cached !== undefined)
|
|
13237
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);
|
|
13238
13652
|
// Shared leases live across queries at one version; results are version-deterministic, but
|
|
13239
13653
|
// candidate arrays can be large, so the memo sheds wholesale rather than growing unbounded.
|
|
13240
13654
|
if (memo.size >= 1)
|
|
13241
13655
|
memo.clear();
|
|
13242
|
-
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
|
+
});
|
|
13243
13670
|
memo.set(key, read);
|
|
13244
13671
|
return read;
|
|
13245
13672
|
}
|
|
@@ -13440,6 +13867,23 @@ export class MinnowDatabase {
|
|
|
13440
13867
|
if (candidates === undefined)
|
|
13441
13868
|
return { segments, pruned: false };
|
|
13442
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;
|
|
13443
13887
|
const selected = [];
|
|
13444
13888
|
for (const segment of segments) {
|
|
13445
13889
|
const anchorColumn = keyColumn ?? table.columns[0];
|
|
@@ -13473,15 +13917,43 @@ export class MinnowDatabase {
|
|
|
13473
13917
|
if (block.column.type !== keyColumn.type) {
|
|
13474
13918
|
return { segments, pruned: false };
|
|
13475
13919
|
}
|
|
13476
|
-
const
|
|
13477
|
-
|
|
13478
|
-
|
|
13479
|
-
|
|
13480
|
-
|
|
13481
|
-
|
|
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;
|
|
13482
13946
|
}
|
|
13483
13947
|
}
|
|
13484
|
-
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
|
+
}
|
|
13485
13957
|
blockIndexes.push(blockIndex);
|
|
13486
13958
|
rowCounts.push(vector.length);
|
|
13487
13959
|
}
|
|
@@ -13492,16 +13964,21 @@ export class MinnowDatabase {
|
|
|
13492
13964
|
return { segments, pruned: false };
|
|
13493
13965
|
if (blockIndexes.length === 0)
|
|
13494
13966
|
continue;
|
|
13967
|
+
const keptRowCount = rowCounts.reduce((total, count) => total + count, 0);
|
|
13968
|
+
if (segment.kind === "insert" || segment.kind === "base")
|
|
13969
|
+
selectedRowStart += keptRowCount;
|
|
13495
13970
|
selected.push({
|
|
13496
13971
|
...segment,
|
|
13497
|
-
rowCount:
|
|
13972
|
+
rowCount: keptRowCount,
|
|
13498
13973
|
columnBlockIds: Object.fromEntries(Object.entries(segment.columnBlockIds).map(([columnId, ids]) => [
|
|
13499
13974
|
columnId,
|
|
13500
13975
|
blockIndexes.map((blockIndex) => ids[blockIndex] ?? ""),
|
|
13501
13976
|
])),
|
|
13502
13977
|
});
|
|
13503
13978
|
}
|
|
13504
|
-
return
|
|
13979
|
+
return rowPositions === undefined
|
|
13980
|
+
? { segments: selected, pruned: true }
|
|
13981
|
+
: { segments: selected, pruned: true, rows: rowPositions };
|
|
13505
13982
|
}
|
|
13506
13983
|
#scheduleSecondaryIndexBuild(table, indexId) {
|
|
13507
13984
|
if (this.#closed)
|
|
@@ -13687,6 +14164,21 @@ export class MinnowDatabase {
|
|
|
13687
14164
|
const projectedKey = keyColumn !== undefined && projectedColumns.some((column) => column.id === keyColumn.id)
|
|
13688
14165
|
? keyColumn.name
|
|
13689
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
|
+
}
|
|
13690
14182
|
// Zone-map pruning shrinks the scan the same way index pruning does, so a scoring plan
|
|
13691
14183
|
// whose statistics come from the scan (no index serving it) must see the whole corpus.
|
|
13692
14184
|
const zonePruningAllowed = plan === undefined ||
|
|
@@ -14317,7 +14809,170 @@ export class MinnowDatabase {
|
|
|
14317
14809
|
return decoded;
|
|
14318
14810
|
});
|
|
14319
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
|
+
*/
|
|
14320
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) {
|
|
14321
14976
|
const validity = new Uint8Array(Math.ceil(rowCount / 8));
|
|
14322
14977
|
const values = column.type === "boolean"
|
|
14323
14978
|
? new Uint8Array(rowCount)
|
|
@@ -16146,10 +16801,42 @@ function normalizeDomainUpdate(table, input) {
|
|
|
16146
16801
|
: input.keys.map((value) => normalizeColumnLogicalValue(key, value));
|
|
16147
16802
|
return changed || keys !== input.keys ? { keys, changes } : input;
|
|
16148
16803
|
}
|
|
16149
|
-
/**
|
|
16150
|
-
|
|
16151
|
-
|
|
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
|
+
};
|
|
16152
16837
|
const rewriteBlock = (block) => {
|
|
16838
|
+
if (pass.probing && pass.needed)
|
|
16839
|
+
return;
|
|
16153
16840
|
for (const source of [block.base, ...block.joins]) {
|
|
16154
16841
|
if (source.derived !== undefined)
|
|
16155
16842
|
rewriteBlock(source.derived);
|
|
@@ -16162,27 +16849,55 @@ function normalizePlanDomainLiterals(input, domains) {
|
|
|
16162
16849
|
}
|
|
16163
16850
|
}
|
|
16164
16851
|
const sources = [block.base, ...block.joins];
|
|
16165
|
-
const
|
|
16852
|
+
const lookup = (reference, catalog) => {
|
|
16166
16853
|
const separator = reference.indexOf(".");
|
|
16167
16854
|
if (separator !== -1) {
|
|
16168
16855
|
const alias = reference.slice(0, separator);
|
|
16169
16856
|
const name = reference.slice(separator + 1);
|
|
16170
16857
|
const source = sources.find((candidate) => candidate.alias === alias);
|
|
16171
|
-
return source === undefined ? undefined :
|
|
16858
|
+
return source === undefined ? undefined : catalog.get(source.table)?.get(name);
|
|
16172
16859
|
}
|
|
16173
16860
|
const matches = sources.flatMap((source) => {
|
|
16174
|
-
const
|
|
16175
|
-
return
|
|
16861
|
+
const found = catalog.get(source.table)?.get(reference);
|
|
16862
|
+
return found === undefined ? [] : [found];
|
|
16176
16863
|
});
|
|
16177
16864
|
return matches.length === 1 ? matches[0] : undefined;
|
|
16178
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
|
+
};
|
|
16179
16888
|
const coercePair = (column, value) => {
|
|
16180
16889
|
if (column.kind !== "column")
|
|
16181
16890
|
return;
|
|
16182
16891
|
const domain = domainFor(column.reference);
|
|
16183
|
-
if (domain === undefined)
|
|
16892
|
+
if (domain === undefined) {
|
|
16893
|
+
coerceTyped(column, value);
|
|
16184
16894
|
return;
|
|
16895
|
+
}
|
|
16185
16896
|
if (value.kind === "literal") {
|
|
16897
|
+
if (pass.probing) {
|
|
16898
|
+
pass.needed = true;
|
|
16899
|
+
return;
|
|
16900
|
+
}
|
|
16186
16901
|
value.value = normalizeSqlDomainValue(domain, value.value);
|
|
16187
16902
|
value.internalSqlValue = true;
|
|
16188
16903
|
value.sqlDomain = domain;
|
|
@@ -16190,6 +16905,10 @@ function normalizePlanDomainLiterals(input, domains) {
|
|
|
16190
16905
|
else if (value.kind === "list") {
|
|
16191
16906
|
for (const item of value.items) {
|
|
16192
16907
|
if (item.kind === "literal") {
|
|
16908
|
+
if (pass.probing) {
|
|
16909
|
+
pass.needed = true;
|
|
16910
|
+
return;
|
|
16911
|
+
}
|
|
16193
16912
|
item.value = normalizeSqlDomainValue(domain, item.value);
|
|
16194
16913
|
item.internalSqlValue = true;
|
|
16195
16914
|
item.sqlDomain = domain;
|
|
@@ -16265,6 +16984,11 @@ function normalizePlanDomainLiterals(input, domains) {
|
|
|
16265
16984
|
for (const order of block.orderBy)
|
|
16266
16985
|
order.expression = rewrite(order.expression);
|
|
16267
16986
|
};
|
|
16987
|
+
rewriteBlock(input);
|
|
16988
|
+
if (!pass.needed)
|
|
16989
|
+
return input;
|
|
16990
|
+
pass.probing = false;
|
|
16991
|
+
const plan = structuredClone(input);
|
|
16268
16992
|
rewriteBlock(plan);
|
|
16269
16993
|
return plan;
|
|
16270
16994
|
}
|
|
@@ -16665,6 +17389,19 @@ function createStreamedColumnVector(type, length) {
|
|
|
16665
17389
|
* deliberately keep their tags; ordinary TEXT only copies the rare dictionary that collides
|
|
16666
17390
|
* with that namespace, preserving the cached zero-copy vector for every normal dictionary.
|
|
16667
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
|
+
}
|
|
16668
17405
|
function plainTextExecutionVector(column, vector) {
|
|
16669
17406
|
if (column.type !== "string" ||
|
|
16670
17407
|
column.sqlDomain !== undefined ||
|
|
@@ -17463,13 +18200,23 @@ function zonePredicates(plan, table) {
|
|
|
17463
18200
|
}
|
|
17464
18201
|
/** Top-level scalar predicates a ready index can answer through a leftmost key prefix. */
|
|
17465
18202
|
function secondaryIndexPredicates(plan, table) {
|
|
17466
|
-
|
|
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)
|
|
17467
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;
|
|
17468
18213
|
const resolveColumn = (reference) => {
|
|
17469
18214
|
const parts = reference.split(".");
|
|
17470
|
-
if (parts.length === 2 && parts[0] !==
|
|
18215
|
+
if (parts.length === 2 && parts[0] !== source.alias && parts[0] !== table.name) {
|
|
17471
18216
|
return undefined;
|
|
17472
18217
|
}
|
|
18218
|
+
if (parts.length !== 2 && joined)
|
|
18219
|
+
return undefined;
|
|
17473
18220
|
const name = parts.length === 2 ? parts[1] : parts[0];
|
|
17474
18221
|
return table.columns.find((candidate) => candidate.name === name);
|
|
17475
18222
|
};
|
|
@@ -17728,6 +18475,118 @@ function firstMemberAtLeast(members, value) {
|
|
|
17728
18475
|
}
|
|
17729
18476
|
return low;
|
|
17730
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
|
+
}
|
|
17731
18590
|
function reverseComparison(operator) {
|
|
17732
18591
|
if (operator === ">")
|
|
17733
18592
|
return "<";
|