@minnowdb/core 0.6.8 → 0.7.0

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