@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.
@@ -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 (domains.size > 0 && planReadsTable(rewritten, (name) => domains.has(name))) {
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
- columns.set(table.name, table.columns.filter(({ hidden }) => hidden !== true).map(({ name }) => name));
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
- for (const item of shape.select) {
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
- return undefined;
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: shape.select.map((item) => item.alias),
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 ?? reported.base.union ?? reported.base.windowed;
6216
- if (table === undefined && reported.joins.length === 0) {
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
- if (statement.kind === "update") {
7727
- for (const assignment of updateAssignments) {
7728
- for (const column of expressionColumnNames(assignment.expression)) {
7729
- referenced.add(column.split(".").at(-1) ?? column);
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: [...referenced].map((name) => ({
7738
- expression: { kind: "column", reference: name },
7739
- alias: name,
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 = evaluateRowExpression(assignment.expression, table.name, row);
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
- const zonePruned = await this.#zonePrunedStreamSegments(plan, freshBaseTable, projectedBaseColumns, baseSegments, snapshot);
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.readFtsCandidates(tableId, columnId, terms, upToVersion, MAX_FTS_CANDIDATE_ROW_IDS);
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 vector = this.#blockColumnVector(anchorIds[blockIndex] ?? "", block);
13477
- let matches = false;
13478
- for (let row = 0; row < vector.length && !matches; row += 1) {
13479
- const value = vectorValue(vector, row);
13480
- if (value !== null && candidateSet?.has(secondaryKeyLocator(keyColumn.type, value))) {
13481
- matches = true;
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 (matches) {
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: rowCounts.reduce((total, count) => total + count, 0),
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 { segments: selected, pruned: true };
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
- /** Coerces literals and bound parameters opposite a domain column before planning/index probes. */
16150
- function normalizePlanDomainLiterals(input, domains) {
16151
- const plan = structuredClone(input);
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 domainFor = (reference) => {
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 : domains.get(source.table)?.get(name);
16858
+ return source === undefined ? undefined : catalog.get(source.table)?.get(name);
16172
16859
  }
16173
16860
  const matches = sources.flatMap((source) => {
16174
- const domain = domains.get(source.table)?.get(reference);
16175
- return domain === undefined ? [] : [domain];
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
- if (plan.joins.length > 0 || plan.base.table !== table.name)
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] !== plan.base.alias && parts[0] !== table.name) {
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 "<";