@minnowdb/core 0.5.0 → 0.6.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.
Files changed (40) hide show
  1. package/README.md +1 -1
  2. package/dist/engine/cancellation.d.ts +2 -0
  3. package/dist/engine/cancellation.js +4 -0
  4. package/dist/engine/catalog.d.ts +3 -1
  5. package/dist/engine/catalog.js +1 -0
  6. package/dist/engine/client.d.ts +32 -4
  7. package/dist/engine/client.js +82 -15
  8. package/dist/engine/database.d.ts +23 -14
  9. package/dist/engine/database.js +516 -74
  10. package/dist/engine/defaults.js +11 -0
  11. package/dist/engine/errors.d.ts +13 -0
  12. package/dist/engine/errors.js +22 -0
  13. package/dist/engine/fts.d.ts +2 -15
  14. package/dist/engine/live.d.ts +1 -7
  15. package/dist/engine/live.js +2 -12
  16. package/dist/engine/optimizer.js +540 -38
  17. package/dist/engine/query.d.ts +5 -278
  18. package/dist/engine/query.js +78 -32
  19. package/dist/engine/schema-wire.d.ts +7 -1
  20. package/dist/engine/schema-wire.js +4 -0
  21. package/dist/engine/schema.d.ts +67 -33
  22. package/dist/engine/schema.js +138 -7
  23. package/dist/engine/sql-domains.d.ts +8 -0
  24. package/dist/engine/sql-domains.js +25 -0
  25. package/dist/engine/sql-json.js +22 -3
  26. package/dist/engine/vector.d.ts +2 -2
  27. package/dist/engine/vector.js +301 -39
  28. package/dist/engine/worker-host.js +119 -44
  29. package/dist/plan/index.d.ts +5 -4
  30. package/dist/plan/index.js +3 -3
  31. package/dist/plan/model.d.ts +218 -0
  32. package/dist/plan/model.js +1 -0
  33. package/dist/storage/types.d.ts +7 -0
  34. package/dist/storage/types.js +16 -0
  35. package/dist/transactions/index.d.ts +5 -3
  36. package/dist/transactions/index.js +58 -8
  37. package/dist/worker-protocol/index.d.ts +6 -1
  38. package/dist/worker-protocol/index.js +5 -2
  39. package/package.json +1 -1
  40. package/sql-feature-matrix.json +69 -19
@@ -1,8 +1,10 @@
1
1
  import { toColumnarBatch, } from "./batch.js";
2
2
  import { ArtifactCache } from "./artifact-cache.js";
3
+ import { throwIfAborted } from "./cancellation.js";
3
4
  import { BufferedTableWriter } from "./buffered-writer.js";
4
5
  export { attachLifecycleFlush, BufferedTableWriter, MAX_BUFFERED_WRITER_PENDING_ADDS, } from "./buffered-writer.js";
5
- import { CompactionJobCancelledError, CompactionMemoryBudgetError, CompactionWriteAmplificationError, MaintenanceBacklogError, MissingKeyError, SqlCompileError, UnknownTableError, UniqueConstraintError, VisibleSegmentCursorStaleError, } from "./errors.js";
6
+ import { CompactionJobCancelledError, CompactionMemoryBudgetError, CompactionWriteAmplificationError, DatabaseReadBacklogError, MaintenanceBacklogError, MissingKeyError, SqlCompileError, UnknownTableError, UniqueConstraintError, VisibleSegmentCursorStaleError, } from "./errors.js";
7
+ export { DatabaseReadBacklogError } from "./errors.js";
6
8
  import { BLOCK_HEADER_LENGTH, buildPhysicalColumnFromRanges, decodeBlock, decodePhysicalBlock, encodeBlock, encodePhysicalBlock, getCompressionMemoryBound, inspectBlock, maximumPhysicalBlockByteLength, MAX_BLOCK_METADATA_BYTE_LENGTH, MAX_BLOCK_ROW_COUNT, MAX_PHYSICAL_COLUMN_BYTE_LENGTH, MAX_STORED_BLOCK_BYTE_LENGTH, measurePhysicalColumnRanges, physicalColumnByteLength, slicePhysicalColumn, StoredBlockPayloadTooLargeError, wellFormedUtf8ByteLength, } from "../block-format/index.js";
7
9
  import { dateIsoString, dateMilliseconds } from "../date-value.js";
8
10
  import { estimateCompactionRowsPerOutput, planAlignedWriteBlockRanges as writeBlockRanges, } from "./write-block-planner.js";
@@ -20,7 +22,7 @@ import { chooseJoinOrder, renderPlan } from "./optimizer.js";
20
22
  import { encodeSqlEqualityValue } from "./sql-semantics.js";
21
23
  import { externalSqlDomainValue, externalSqlTextValue, isDateDomainValue, isSqlDomainValue, normalizeSqlDomainValue, protectedSqlTextValue, } from "./sql-domains.js";
22
24
  import { toCatalog } from "./catalog.js";
23
- import { applyColumnSteps, assertColumnDroppable, declaredForeignKeys, isDestructiveStep, planMigration, } from "./schema.js";
25
+ import { applyColumnSteps, assertColumnDroppable, compileGeneratedColumnExpression, declaredForeignKeys, isDestructiveStep, planMigration, } from "./schema.js";
24
26
  import { columnarTableFromRows, createColumnarTable, vectorValue, } from "./vector.js";
25
27
  // The on-disk format is little-endian; the bulk Float64 copy reads platform order.
26
28
  const PLATFORM_LITTLE_ENDIAN = new Uint8Array(new Uint32Array([1]).buffer)[0] === 1;
@@ -103,14 +105,6 @@ export const MAX_TRANSACTION_SAVEPOINT_BYTES = 8 * 1024 * 1024;
103
105
  export const MAX_DATABASE_PENDING_WRITES = 64;
104
106
  /** Concurrent direct reads retained by one database instance. Reads still execute in parallel. */
105
107
  export const MAX_DATABASE_ACTIVE_READS = 256;
106
- export class DatabaseReadBacklogError extends Error {
107
- limit;
108
- name = "DatabaseReadBacklogError";
109
- constructor(limit = MAX_DATABASE_ACTIVE_READS) {
110
- super(`A database cannot retain more than ${String(limit)} active reads; await a read`);
111
- this.limit = limit;
112
- }
113
- }
114
108
  /** Commits tolerated while collection fails before foreground writes assist and backpressure. */
115
109
  const AUTO_COLLECT_MAX_DEBT_COMMITS = 4_096;
116
110
  /** Failed background passes retry without another write, with bounded exponential backoff. */
@@ -291,17 +285,29 @@ function isTransactionalStatement(statement) {
291
285
  statement.kind === "delete" ||
292
286
  statement.kind === "select");
293
287
  }
288
+ function returningExecuteFields(table, columns, rows) {
289
+ return {
290
+ returnedRows: rows,
291
+ returnedColumns: [...columns],
292
+ returnedColumnDomains: columns.map((name) => table.columns.find((column) => column.name === name)?.sqlDomain ?? null),
293
+ };
294
+ }
294
295
  function externalizeExecuteResult(result) {
295
296
  if (!("returnedRows" in result) || !Array.isArray(result.returnedRows))
296
297
  return result;
297
- const columns = [...new Set(result.returnedRows.flatMap((row) => Object.keys(row)))];
298
+ const columns = result.returnedColumns ?? [
299
+ ...new Set(result.returnedRows.flatMap((row) => Object.keys(row))),
300
+ ];
301
+ const columnDomains = result.returnedColumnDomains ?? unknownColumnDomains(columns);
298
302
  return {
299
303
  ...result,
300
304
  returnedRows: externalizeQueryResult({
301
305
  columns,
302
- columnDomains: unknownColumnDomains(columns),
306
+ columnDomains,
303
307
  rows: result.returnedRows,
304
308
  }).rows,
309
+ returnedColumns: [...columns],
310
+ returnedColumnDomains: structuredClone(columnDomains),
305
311
  };
306
312
  }
307
313
  /** Rejects an INSERT value that is still an unbound `?`/`$n` slot. */
@@ -571,6 +577,8 @@ export class MinnowDatabase {
571
577
  #droppingTables = new Set();
572
578
  /** Per table: the visible segment count a failed auto-compaction must see before retrying. */
573
579
  #autoCompactionBackoff = new Map();
580
+ /** Exact current-layout counters; local commits advance them without rescanning history. */
581
+ #autoCompactionHints = new Map();
574
582
  /** Data commits per table since its last write-path auto-compaction check. */
575
583
  #commitsSinceCompactionCheck = new Map();
576
584
  /** The compaction step in flight per table, so steps on one table run one at a time. */
@@ -749,6 +757,7 @@ export class MinnowDatabase {
749
757
  this.#autoCompactionsRequested.clear();
750
758
  this.#idleCompactionTableIds.clear();
751
759
  this.#autoCompactionBackoff.clear();
760
+ this.#autoCompactionHints.clear();
752
761
  this.#commitsSinceCompactionCheck.clear();
753
762
  this.#artifactCache.clear();
754
763
  }
@@ -813,6 +822,9 @@ export class MinnowDatabase {
813
822
  ...(sqlDomain === undefined ? {} : { sqlDomain: structuredClone(sqlDomain) }),
814
823
  nullable: column.nullable ?? false,
815
824
  ...(column.defaultValue === undefined ? {} : { defaultValue: column.defaultValue }),
825
+ ...(column.generatedValue === undefined
826
+ ? {}
827
+ : { generatedValue: structuredClone(column.generatedValue) }),
816
828
  ...(column.enumValues === undefined
817
829
  ? {}
818
830
  : { enumValues: validateEnumValues(column.enumValues, columnName) }),
@@ -871,6 +883,15 @@ export class MinnowDatabase {
871
883
  throw new TypeError(`Unique key cannot be nullable: ${uniqueKeyColumn.name}`);
872
884
  }
873
885
  for (const column of columns) {
886
+ if (column.generatedValue !== undefined) {
887
+ if (column.defaultValue !== undefined || column.backfill !== undefined) {
888
+ throw new TypeError(`A generated column cannot also have a default or backfill: ${name}.${column.name}`);
889
+ }
890
+ compileGeneratedColumnExpression(name, column.name, column.generatedValue.sql, columns);
891
+ if (column === uniqueKeyColumn || compositePrimaryColumns.includes(column)) {
892
+ throw new TypeError(`Generated columns cannot be row-addressing keys: ${name}.${column.name}`);
893
+ }
894
+ }
874
895
  if (column.defaultValue !== undefined) {
875
896
  validateColumnDefault({ ...column, isUniqueKey: column === uniqueKeyColumn }, column.defaultValue);
876
897
  if (column.defaultValue.kind === "literal" && column.sqlDomain !== undefined) {
@@ -1059,6 +1080,9 @@ export class MinnowDatabase {
1059
1080
  : { sqlDomain: structuredClone(column.sqlDomain) }),
1060
1081
  nullable: column.nullable,
1061
1082
  ...(column.defaultValue === undefined ? {} : { defaultValue: column.defaultValue }),
1083
+ ...(column.generatedValue === undefined
1084
+ ? {}
1085
+ : { generatedValue: column.generatedValue }),
1062
1086
  ...(column.enumValues === undefined ? {} : { enumValues: [...column.enumValues] }),
1063
1087
  })),
1064
1088
  ...(uniqueKey === undefined ? {} : { uniqueKey }),
@@ -1129,7 +1153,7 @@ export class MinnowDatabase {
1129
1153
  }
1130
1154
  async #withReadReservation(run) {
1131
1155
  if (this.#activeReadReservations >= MAX_DATABASE_ACTIVE_READS) {
1132
- throw new DatabaseReadBacklogError();
1156
+ throw new DatabaseReadBacklogError(MAX_DATABASE_ACTIVE_READS);
1133
1157
  }
1134
1158
  this.#activeReadReservations += 1;
1135
1159
  try {
@@ -1140,7 +1164,9 @@ export class MinnowDatabase {
1140
1164
  }
1141
1165
  }
1142
1166
  async #queryWithReadReservation(sql, options) {
1167
+ throwIfAborted(options.signal);
1143
1168
  await this.#settleExpiredStatementTransaction();
1169
+ throwIfAborted(options.signal);
1144
1170
  // An open statement transaction routes through its session, which owns the reservation for
1145
1171
  // the actual execution. Avoid charging the same query twice at this public boundary.
1146
1172
  return this.#openTransaction === undefined
@@ -1569,6 +1595,7 @@ export class MinnowDatabase {
1569
1595
  for (const column of table.columns)
1570
1596
  this.#gzipVerdicts.delete(column.id);
1571
1597
  this.#autoCompactionBackoff.delete(table.id);
1598
+ this.#autoCompactionHints.delete(table.id);
1572
1599
  this.#commitsSinceCompactionCheck.delete(table.id);
1573
1600
  this.#idleCompactionTableIds.delete(table.id);
1574
1601
  const postingPrefix = `${table.id}/`;
@@ -1634,6 +1661,7 @@ export class MinnowDatabase {
1634
1661
  Object.values(pivoted.columns).find((values) => values.length > 0)?.length ??
1635
1662
  0;
1636
1663
  normalizeDomainBatch(table, filled.batch);
1664
+ fillStoredGeneratedColumns(table, filled.batch, inputRowCount, filled.generated);
1637
1665
  for (const name of filled.generated.keys()) {
1638
1666
  const column = table.columns.find((candidate) => candidate.name === name);
1639
1667
  const values = filled.batch.columns[name];
@@ -1720,6 +1748,7 @@ export class MinnowDatabase {
1720
1748
  throw new TypeError(`Table needs a unique key before rows can be updated: ${table.name}`);
1721
1749
  }
1722
1750
  const normalizedInput = normalizeDomainUpdate(table, input);
1751
+ rejectGeneratedUpdateAssignments(table, normalizedInput);
1723
1752
  const keys = validateUpdateBatch(table, keyColumn, normalizedInput);
1724
1753
  const current = await this.store.getTable(table.id);
1725
1754
  if (current === undefined)
@@ -1737,6 +1766,10 @@ export class MinnowDatabase {
1737
1766
  changes: Object.fromEntries(Object.entries(changes).map(([name, value]) => [name, [value]])),
1738
1767
  });
1739
1768
  }
1769
+ /** Deletes one row by the table's unique key. */
1770
+ async delete(tableName, key) {
1771
+ return this.deleteBatch(tableName, { keys: [key] });
1772
+ }
1740
1773
  deleteBatch(tableName, input) {
1741
1774
  return this.#withWriteReservation(() => this.#deleteBatchReserved(tableName, input));
1742
1775
  }
@@ -1972,15 +2005,13 @@ export class MinnowDatabase {
1972
2005
  async #writeUpdateBatch(table, keyColumn, input, keys) {
1973
2006
  await this.#assertCompactionCapacity(table);
1974
2007
  const started = performance.now();
1975
- const logicalBytes = estimateValuesBytes(input.keys) +
1976
- Object.values(input.changes).reduce((total, values) => total + estimateValuesBytes(values), 0);
2008
+ let logicalBytes;
1977
2009
  // Deferred: the record rides the single-shot commit below unless trigger rows stage first.
1978
2010
  const transaction = await this.#transactions.beginDeferred({ durableSnapshot: false });
1979
2011
  transaction.limitLevelZeroSegments(table.id, MAX_LEVEL_ZERO_SEGMENTS);
1980
2012
  const blockStager = new BoundedWriteBlockStager(transaction);
1981
2013
  const segmentId = this.#createId();
1982
2014
  const columnBlockIds = {};
1983
- const changedColumns = Object.keys(input.changes).sort();
1984
2015
  let storedBytes = 0;
1985
2016
  let blockCount = 0;
1986
2017
  let encodeMs = 0;
@@ -1989,6 +2020,22 @@ export class MinnowDatabase {
1989
2020
  let retries = 0;
1990
2021
  try {
1991
2022
  await this.#assertKeysExist(table, keyColumn, transaction.snapshotVersion, keys);
2023
+ const checks = table.checks ?? [];
2024
+ let changedForeignKey = (table.foreignKeys ?? []).some((key) => key.enforced !== false &&
2025
+ foreignKeyColumns(key).some((column) => input.changes[column] !== undefined));
2026
+ const preImages = await this.#triggerPreImages(table, keyColumn, input.keys.filter((key) => key !== null), "update", (sql, params) => this.#sessionQuery(transaction, sql, { params }), checks.length > 0 ||
2027
+ changedForeignKey ||
2028
+ tableGeneratedExpressions(table).length > 0 ||
2029
+ secondaryIndexUpdateNeedsPreImages(table, input) ||
2030
+ readyUniqueSecondaryIndexes(table).some(({ columns }) => columns.some((column) => input.changes[column.name] !== undefined)));
2031
+ input = applyStoredGeneratedUpdateChanges(table, input, preImages);
2032
+ validateUpdateBatch(table, keyColumn, input);
2033
+ changedForeignKey = (table.foreignKeys ?? []).some((key) => key.enforced !== false &&
2034
+ foreignKeyColumns(key).some((column) => input.changes[column] !== undefined));
2035
+ logicalBytes =
2036
+ estimateValuesBytes(input.keys) +
2037
+ Object.values(input.changes).reduce((total, values) => total + estimateValuesBytes(values), 0);
2038
+ const changedColumns = Object.keys(input.changes).sort();
1992
2039
  const columns = [keyColumn, ...changedColumns.map((name) => findColumn(table, name))];
1993
2040
  const plannedColumns = columns.map((column) => writeColumnValues(column.type, column.id === keyColumn.id ? input.keys : (input.changes[column.name] ?? [])));
1994
2041
  const ranges = writeBlockRanges(plannedColumns, input.keys.length, this.#rowsPerBlock, this.#targetBlockBytes);
@@ -2022,13 +2069,6 @@ export class MinnowDatabase {
2022
2069
  }
2023
2070
  columnBlockIds[column.id] = blockIds;
2024
2071
  }
2025
- const checks = table.checks ?? [];
2026
- const changedForeignKey = (table.foreignKeys ?? []).some((key) => key.enforced !== false &&
2027
- foreignKeyColumns(key).some((column) => input.changes[column] !== undefined));
2028
- const preImages = await this.#triggerPreImages(table, keyColumn, input.keys.filter((key) => key !== null), "update", (sql, params) => this.#sessionQuery(transaction, sql, { params }), checks.length > 0 ||
2029
- changedForeignKey ||
2030
- secondaryIndexUpdateNeedsPreImages(table, input) ||
2031
- readyUniqueSecondaryIndexes(table).some(({ columns }) => columns.some((column) => input.changes[column.name] !== undefined)));
2032
2072
  stageSecondaryUniqueMutationChanges(transaction, table, input, preImages);
2033
2073
  const secondaryDeltas = buildSecondaryUpdateDeltas(table, input, preImages);
2034
2074
  if (secondaryDeltas.length > 0) {
@@ -2101,6 +2141,14 @@ export class MinnowDatabase {
2101
2141
  blockCount,
2102
2142
  storedBytes,
2103
2143
  version: manifest.version,
2144
+ ...(tableGeneratedExpressions(table).length === 0
2145
+ ? {}
2146
+ : {
2147
+ generatedColumns: Object.fromEntries(tableGeneratedExpressions(table).map(({ column }) => [
2148
+ column.name,
2149
+ (input.changes[column.name] ?? []).map((value) => externalSqlDomainValue(value)),
2150
+ ])),
2151
+ }),
2104
2152
  metrics: createWriteMetrics({
2105
2153
  logicalBytes,
2106
2154
  storedBytes,
@@ -2554,12 +2602,14 @@ export class MinnowDatabase {
2554
2602
  }
2555
2603
  async #prepareCompiledPlan(plan, options = {}, probe) {
2556
2604
  options = this.#effectiveQueryOptions(options);
2605
+ throwIfAborted(options.signal);
2557
2606
  // The ORDER-BY-expression desugar's wrapper is projection-only: prepare the inner block
2558
2607
  // directly (no derived materialization) and project each result to the visible aliases,
2559
2608
  // so `.search()` costs the same whether or not the caller also selects the score.
2560
2609
  const wrapper = transparentProjectionSource(plan);
2561
2610
  if (wrapper !== undefined) {
2562
2611
  const prepared = await this.#prepareCompiledPlan(wrapper.inner, options, probe);
2612
+ throwIfAborted(options.signal);
2563
2613
  return {
2564
2614
  sql: prepared.sql,
2565
2615
  tables: prepared.tables,
@@ -2580,6 +2630,7 @@ export class MinnowDatabase {
2580
2630
  let outputNeedsExternalization = true;
2581
2631
  let outputColumnDomains = [];
2582
2632
  const prepareAtSnapshot = async (snapshot, realTables, visibility) => {
2633
+ throwIfAborted(options.signal);
2583
2634
  const typedSchemas = new Map([...realTables.values()].map((table) => [
2584
2635
  table.name,
2585
2636
  table.columns.map(({ name, type, integer, sqlDomain }) => ({
@@ -2595,30 +2646,37 @@ export class MinnowDatabase {
2595
2646
  const expandedPlan = expandFtsColumns(plan, (tableName) => searchableFtsColumns(realTables.get(tableName)));
2596
2647
  const resolution = subqueryResolutionSteps(expandedPlan);
2597
2648
  for (const step of resolution.steps) {
2598
- step.substitute(await this.#executeBlockCached(step.block, snapshot, visibility, memory, realTables, typedSchemas, options.memoize !== false, options.spillToStorage !== false, options.spillToStorage === true, options.spillPageRows));
2649
+ throwIfAborted(options.signal);
2650
+ step.substitute(await this.#executeBlockCached(step.block, snapshot, visibility, memory, realTables, typedSchemas, options.memoize !== false, options.spillToStorage !== false, options.spillToStorage === true, options.spillPageRows, options.signal));
2651
+ throwIfAborted(options.signal);
2599
2652
  }
2600
2653
  resolvedPlan = resolution.plan;
2601
2654
  // Index-served BM25 statistics, computed against the same catalog snapshot the pruner
2602
2655
  // reads, so a pruned scoring scan always carries exact corpus numbers.
2603
2656
  ftsStats = await this.#ftsIndexStats(resolvedPlan, realTables, snapshot, visibility);
2657
+ throwIfAborted(options.signal);
2604
2658
  columnarTables = await this.#prepareBlockInputs(resolvedPlan, snapshot, visibility, memory, realTables, typedSchemas, undefined,
2605
2659
  // memoize: false means "compute this statement's results" — that covers the
2606
2660
  // columnar forms of derived and windowed sources too, not just the result memo.
2607
- options.memoize !== false, options.spillToStorage !== false, options.spillToStorage === true, options.spillPageRows);
2661
+ options.memoize !== false, options.spillToStorage !== false, options.spillToStorage === true, options.spillPageRows, options.signal);
2662
+ throwIfAborted(options.signal);
2608
2663
  outputNeedsExternalization = queryResultNeedsExternalization(resolvedPlan, typedSchemas);
2609
2664
  outputColumnDomains = inferResultColumnDomains(resolvedPlan, typedSchemas);
2610
2665
  };
2611
2666
  if (options.version !== undefined) {
2612
2667
  // Explicit time travel keeps the per-call lease and version-anchored reads.
2613
2668
  const realTables = await this.#findRealBlockTables(plan);
2669
+ throwIfAborted(options.signal);
2614
2670
  await this.#withLeasedSnapshot(options.version, async (snapshot) => {
2615
2671
  const visibility = await this.#blockSegmentVisibility(realTables);
2672
+ throwIfAborted(options.signal);
2616
2673
  await prepareAtSnapshot(snapshot, realTables, visibility);
2617
2674
  });
2618
2675
  }
2619
2676
  else {
2620
2677
  await this.#withSharedCatalogSnapshot(collectRealTableNames(plan), prepareAtSnapshot, probe);
2621
2678
  }
2679
+ throwIfAborted(options.signal);
2622
2680
  return createPreparedColumnarQuery(chooseJoinOrder(resolvedPlan, columnarTables), columnarTables, memory, {
2623
2681
  ...(ftsStats === undefined ? {} : { ftsStats }),
2624
2682
  outputNeedsExternalization,
@@ -2909,18 +2967,21 @@ export class MinnowDatabase {
2909
2967
  * materialize their referenced columns. Each block resolves its own inputs, so one real table
2910
2968
  * projected differently by two blocks never collides.
2911
2969
  */
2912
- async #prepareBlockInputs(block, snapshot, visibility, memory, realTables, typedSchemas, extraInputs, cacheResults = true, allowSpill = true, forceSpill = false, spillPageRows) {
2970
+ async #prepareBlockInputs(block, snapshot, visibility, memory, realTables, typedSchemas, extraInputs, cacheResults = true, allowSpill = true, forceSpill = false, spillPageRows, signal) {
2971
+ throwIfAborted(signal);
2913
2972
  const inputs = new Map(extraInputs ?? []);
2914
2973
  const sources = [block.base, ...block.joins];
2915
2974
  for (const source of sources) {
2975
+ throwIfAborted(signal);
2916
2976
  if (inputs.has(source.table))
2917
2977
  continue;
2918
2978
  if (source.recursive !== undefined) {
2919
2979
  const { reference, base, step, all } = source.recursive;
2920
- const { result: baseResult, schema } = await this.#executeBlockWithSchemaCached(base, snapshot, visibility, memory, realTables, typedSchemas, cacheResults, allowSpill, forceSpill, spillPageRows);
2980
+ const { result: baseResult, schema } = await this.#executeBlockWithSchemaCached(base, snapshot, visibility, memory, realTables, typedSchemas, cacheResults, allowSpill, forceSpill, spillPageRows, signal);
2921
2981
  typedSchemas.set(reference, schema);
2922
2982
  const state = createRecursiveCteState(baseResult, all);
2923
2983
  while (state.frontier.length > 0) {
2984
+ throwIfAborted(signal);
2924
2985
  // Each iteration's inputs release with its own context, so only the accumulated rows
2925
2986
  // and the final columnar table hold memory across the fixpoint loop.
2926
2987
  const iterationMemory = memory.createChild();
@@ -2930,7 +2991,7 @@ export class MinnowDatabase {
2930
2991
  columnDomains: schema.map(({ sqlDomain }) => sqlDomain ?? null),
2931
2992
  rows: state.frontier,
2932
2993
  }, schema);
2933
- state.absorb(await this.#executeBlock(step, snapshot, visibility, iterationMemory, realTables, typedSchemas, new Map([[reference, frontierTable]]), cacheResults, allowSpill, forceSpill, spillPageRows));
2994
+ state.absorb(await this.#executeBlock(step, snapshot, visibility, iterationMemory, realTables, typedSchemas, new Map([[reference, frontierTable]]), cacheResults, allowSpill, forceSpill, spillPageRows, signal));
2934
2995
  }
2935
2996
  finally {
2936
2997
  iterationMemory.close();
@@ -2948,7 +3009,8 @@ export class MinnowDatabase {
2948
3009
  const results = [];
2949
3010
  let schema;
2950
3011
  for (const member of source.union.blocks) {
2951
- const { result, schema: memberSchema } = await this.#executeBlockWithSchemaCached(member, snapshot, visibility, memory, realTables, typedSchemas, cacheResults, allowSpill, forceSpill, spillPageRows);
3012
+ throwIfAborted(signal);
3013
+ const { result, schema: memberSchema } = await this.#executeBlockWithSchemaCached(member, snapshot, visibility, memory, realTables, typedSchemas, cacheResults, allowSpill, forceSpill, spillPageRows, signal);
2952
3014
  if (schema === undefined)
2953
3015
  schema = memberSchema;
2954
3016
  else {
@@ -2977,6 +3039,7 @@ export class MinnowDatabase {
2977
3039
  // key plus the window spec, so a warm repeat skips the window pass and the
2978
3040
  // rows-to-columnar conversion entirely.
2979
3041
  const innerKey = await this.#blockResultCacheKey(source.windowed.block, snapshot, visibility, realTables, cacheResults);
3042
+ throwIfAborted(signal);
2980
3043
  const columnarKey = innerKey === undefined
2981
3044
  ? undefined
2982
3045
  : `ctw|${JSON.stringify(source.windowed.windows)}|${innerKey}`;
@@ -2988,7 +3051,7 @@ export class MinnowDatabase {
2988
3051
  continue;
2989
3052
  }
2990
3053
  }
2991
- const { result: inner, schema: innerSchema } = await this.#executeBlockWithSchemaCached(source.windowed.block, snapshot, visibility, memory, realTables, typedSchemas, cacheResults, allowSpill, forceSpill, spillPageRows);
3054
+ const { result: inner, schema: innerSchema } = await this.#executeBlockWithSchemaCached(source.windowed.block, snapshot, visibility, memory, realTables, typedSchemas, cacheResults, allowSpill, forceSpill, spillPageRows, signal);
2992
3055
  const windowed = applyWindowFunctions(inner, source.windowed.windows, {
2993
3056
  copyRows: cacheResults,
2994
3057
  });
@@ -3015,6 +3078,7 @@ export class MinnowDatabase {
3015
3078
  if (derived === undefined)
3016
3079
  continue;
3017
3080
  const derivedKey = await this.#blockResultCacheKey(derived, snapshot, visibility, realTables, cacheResults);
3081
+ throwIfAborted(signal);
3018
3082
  const columnarKey = derivedKey === undefined ? undefined : `ctd|${derivedKey}`;
3019
3083
  if (columnarKey !== undefined) {
3020
3084
  const hit = this.#cacheGet(columnarKey);
@@ -3024,7 +3088,7 @@ export class MinnowDatabase {
3024
3088
  continue;
3025
3089
  }
3026
3090
  }
3027
- const { result, schema } = await this.#executeBlockWithSchemaCached(derived, snapshot, visibility, memory, realTables, typedSchemas, cacheResults, allowSpill, forceSpill, spillPageRows);
3091
+ const { result, schema } = await this.#executeBlockWithSchemaCached(derived, snapshot, visibility, memory, realTables, typedSchemas, cacheResults, allowSpill, forceSpill, spillPageRows, signal);
3028
3092
  typedSchemas.set(source.table, schema);
3029
3093
  const derivedTable = derivedColumnarTable(source.table, result, schema);
3030
3094
  if (columnarKey !== undefined) {
@@ -3039,6 +3103,7 @@ export class MinnowDatabase {
3039
3103
  const columns = referencedColumns(block, nameSchemas);
3040
3104
  const onlyRealSource = sources.length === 1 && sources[0]?.derived === undefined ? block : undefined;
3041
3105
  for (const source of sources) {
3106
+ throwIfAborted(signal);
3042
3107
  if (source.derived !== undefined || inputs.has(source.table))
3043
3108
  continue;
3044
3109
  if (source.table === DUAL_TABLE) {
@@ -3050,6 +3115,7 @@ export class MinnowDatabase {
3050
3115
  throw new UnknownTableError(source.table);
3051
3116
  const requestedColumns = columns.get(table.name) ?? [];
3052
3117
  inputs.set(table.name, await this.#materializeColumnarTableAtSnapshot(table, snapshot, requestedColumns.length === 0 ? [] : resolveReadColumns(table, requestedColumns), visibility, onlyRealSource));
3118
+ throwIfAborted(signal);
3053
3119
  }
3054
3120
  return inputs;
3055
3121
  }
@@ -3066,6 +3132,7 @@ export class MinnowDatabase {
3066
3132
  return this.#queryWithReadReservation(sql, options);
3067
3133
  }
3068
3134
  async #queryUnreserved(sql, options) {
3135
+ throwIfAborted(options.signal);
3069
3136
  const open = this.#openTransaction;
3070
3137
  if (open !== undefined) {
3071
3138
  // Read-your-writes: inside BEGIN … COMMIT a read sees the pre-scope snapshot plus what
@@ -3091,6 +3158,7 @@ export class MinnowDatabase {
3091
3158
  const result = !memoizable
3092
3159
  ? await this.#queryCompiled(plan, options)
3093
3160
  : await this.#memoizedQuery(plan, `res ${queryResultMemoKey(sql, options.params ?? [])}`, options, probe);
3161
+ throwIfAborted(options.signal);
3094
3162
  return externalizeQueryResult(result);
3095
3163
  }
3096
3164
  /**
@@ -3155,7 +3223,8 @@ export class MinnowDatabase {
3155
3223
  };
3156
3224
  }
3157
3225
  async #queryBatches(sql, options, cursor) {
3158
- options = this.#effectiveQueryOptions(options);
3226
+ options = this.#effectiveQueryOptions({ ...options, signal: cursor.signal });
3227
+ cursor.signal.throwIfAborted();
3159
3228
  // A transaction's staged overlay has its own executor. Keep its read-your-writes semantics
3160
3229
  // and page the materialized result; native scan batching is for durable snapshots.
3161
3230
  if (this.#openTransaction === undefined) {
@@ -3196,16 +3265,22 @@ export class MinnowDatabase {
3196
3265
  * and on IndexedDB every probe is a read transaction, a floor under every small query.
3197
3266
  */
3198
3267
  async #memoizedQuery(plan, key, options, probe) {
3268
+ throwIfAborted(options.signal);
3199
3269
  const before = await probe();
3270
+ throwIfAborted(options.signal);
3200
3271
  const cached = this.#cacheGet(`${key}\u0001${String(before.catalogEpoch)}`);
3201
- if (cached !== undefined)
3272
+ if (cached !== undefined) {
3273
+ options.onStats?.({ peakMemoryBytes: 0 });
3202
3274
  return copyQueryResult(cached);
3275
+ }
3203
3276
  const result = await this.#queryCompiled(plan, options, before);
3277
+ throwIfAborted(options.signal);
3204
3278
  const bytes = queryResultRetainedBytes(result);
3205
3279
  if (bytes <= RESULT_MEMO_MAX_BYTES) {
3206
3280
  // Cache only when the epoch did not move during execution: the result is then exactly
3207
3281
  // that epoch's answer. A moved epoch simply skips the cache — never mis-files.
3208
3282
  const after = await probe();
3283
+ throwIfAborted(options.signal);
3209
3284
  if (after.catalogEpoch === before.catalogEpoch) {
3210
3285
  this.#cachePut(`${key}\u0001${String(before.catalogEpoch)}`, copyQueryResult(result), bytes);
3211
3286
  }
@@ -3385,10 +3460,13 @@ export class MinnowDatabase {
3385
3460
  */
3386
3461
  async #queryCompiled(plan, options = {}, probe) {
3387
3462
  options = this.#effectiveQueryOptions(options);
3463
+ throwIfAborted(options.signal);
3388
3464
  // One freshness probe per query: read here unless the caller already has one, and handed
3389
3465
  // to the view lookup and the catalog state below, which would otherwise probe again each.
3390
3466
  probe ??= await this.store.getCatalogProbe();
3467
+ throwIfAborted(options.signal);
3391
3468
  plan = await this.#applyCatalogRewrites(plan, probe);
3469
+ throwIfAborted(options.signal);
3392
3470
  const spillPageRows = options.spillPageRows === undefined
3393
3471
  ? undefined
3394
3472
  : positiveWholeNumber(options.spillPageRows, "Query spill page rows");
@@ -3409,6 +3487,7 @@ export class MinnowDatabase {
3409
3487
  }
3410
3488
  }
3411
3489
  const prepared = await this.#prepareCompiledPlan(plan, options, probe);
3490
+ throwIfAborted(options.signal);
3412
3491
  // Read the peak before close(): closing releases the context and zeroes what it tracked.
3413
3492
  const report = (result) => {
3414
3493
  options.onStats?.({ peakMemoryBytes: prepared.memoryUsage.peakBytes });
@@ -3416,11 +3495,16 @@ export class MinnowDatabase {
3416
3495
  };
3417
3496
  try {
3418
3497
  const spill = options.spillToStorage ?? options.executionMemoryBudgetBytes !== undefined;
3419
- if (!spill)
3420
- return report(prepared.execute());
3498
+ if (!spill) {
3499
+ const result = prepared.execute();
3500
+ throwIfAborted(options.signal);
3501
+ return report(result);
3502
+ }
3421
3503
  if (options.spillToStorage !== true) {
3422
3504
  try {
3423
- return report(prepared.execute());
3505
+ const result = prepared.execute();
3506
+ throwIfAborted(options.signal);
3507
+ return report(result);
3424
3508
  }
3425
3509
  catch (error) {
3426
3510
  if (!(error instanceof QueryMemoryBudgetError))
@@ -3430,12 +3514,33 @@ export class MinnowDatabase {
3430
3514
  return report(await prepared.executeAsync({
3431
3515
  ...(spillPageRows === undefined ? {} : { spillPageRows }),
3432
3516
  spillStore: this.#leasedSpillStore(),
3517
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
3433
3518
  }));
3434
3519
  }
3435
3520
  finally {
3436
3521
  prepared.close();
3437
3522
  }
3438
3523
  }
3524
+ /**
3525
+ * Streams a one-column snapshot projection into bounded pages. DELETE uses this before its
3526
+ * first staged write: selection keeps all ordinary pruning/predicate semantics without
3527
+ * retaining every QueryRow object until the key array is complete. Undefined means the shape
3528
+ * or physical history needs the established row-shaped executor.
3529
+ */
3530
+ async #queryCompiledFirstColumn(plan, probe) {
3531
+ const options = this.#effectiveQueryOptions({ memoize: false });
3532
+ plan = await this.#applyCatalogRewrites(plan, probe);
3533
+ const values = [];
3534
+ const streamed = await this.#queryStreamed(plan, options, undefined, probe, {
3535
+ batchRows: 2_048,
3536
+ signal: new AbortController().signal,
3537
+ consumeFirstColumn: async (batch) => {
3538
+ values.push(...batch);
3539
+ },
3540
+ consume: async () => undefined,
3541
+ });
3542
+ return streamed === undefined ? undefined : values;
3543
+ }
3439
3544
  /**
3440
3545
  * Backs the executor's spill pages with durable owner leases: each owner registers a lease
3441
3546
  * before its first page write and renews it while pages are read or written, so an abandoned
@@ -3823,15 +3928,18 @@ export class MinnowDatabase {
3823
3928
  * one, so a table that cannot be compacted today costs one attempt, not one per query.
3824
3929
  */
3825
3930
  #maybeScheduleAutoCompaction(table, segments) {
3931
+ this.#maybeScheduleAutoCompactionFromHint(table, autoCompactionHint(null, segments));
3932
+ }
3933
+ #maybeScheduleAutoCompactionFromHint(table, hint) {
3826
3934
  if (this.#closed)
3827
3935
  return;
3828
3936
  if (!this.#autoCompact)
3829
3937
  return;
3830
3938
  if (this.#droppingTables.has(table.id))
3831
3939
  return;
3832
- if (!autoCompactionDue(segments))
3940
+ if (!autoCompactionDueHint(hint))
3833
3941
  return;
3834
- if (segments.length < (this.#autoCompactionBackoff.get(table.id) ?? 0))
3942
+ if (hint.visible < (this.#autoCompactionBackoff.get(table.id) ?? 0))
3835
3943
  return;
3836
3944
  if (this.#autoCompactionsInFlight.has(table.id)) {
3837
3945
  // A final burst can cross the threshold while the prior fold is still planning or
@@ -3845,14 +3953,14 @@ export class MinnowDatabase {
3845
3953
  if (folded)
3846
3954
  this.#autoCompactionBackoff.delete(table.id);
3847
3955
  else {
3848
- this.#autoCompactionBackoff.set(table.id, Math.min(AUTO_COMPACT_MAX_LEVEL_ZERO_SEGMENTS, Math.max(2, segments.length * 2)));
3956
+ this.#autoCompactionBackoff.set(table.id, Math.min(AUTO_COMPACT_MAX_LEVEL_ZERO_SEGMENTS, Math.max(2, hint.visible * 2)));
3849
3957
  }
3850
3958
  })
3851
3959
  .catch(() => {
3852
3960
  // Back off deterministic failures, but never beyond the maximum L0 prefix a fold can
3853
3961
  // consume. A transient conflict near the end of a burst must not strand hundreds of
3854
3962
  // segments waiting for a segment count the idle database can never reach.
3855
- this.#autoCompactionBackoff.set(table.id, Math.min(AUTO_COMPACT_MAX_LEVEL_ZERO_SEGMENTS, Math.max(2, segments.length * 2)));
3963
+ this.#autoCompactionBackoff.set(table.id, Math.min(AUTO_COMPACT_MAX_LEVEL_ZERO_SEGMENTS, Math.max(2, hint.visible * 2)));
3856
3964
  })
3857
3965
  .finally(() => {
3858
3966
  if (this.#autoCompactionsInFlight.get(table.id) === run) {
@@ -3872,6 +3980,10 @@ export class MinnowDatabase {
3872
3980
  // every write and let the commit return the identical CompactionBacklogError at the CAS.
3873
3981
  if (!this.#autoCompact)
3874
3982
  return;
3983
+ const hintVersion = transaction?.snapshotVersion ?? (await this.store.getCurrentManifestVersion());
3984
+ const hint = this.#autoCompactionHints.get(table.id);
3985
+ if (hint?.version === hintVersion && hint.levelZero < MAX_LEVEL_ZERO_SEGMENTS)
3986
+ return;
3875
3987
  let segments = await this.#currentVisibleSegments(table);
3876
3988
  let levelZero = 0;
3877
3989
  for (const segment of segments) {
@@ -3945,9 +4057,11 @@ export class MinnowDatabase {
3945
4057
  const version = await this.store.getCurrentManifestVersion();
3946
4058
  segments = await this.#visibleSegmentRecords(table, new Snapshot(this.store, version));
3947
4059
  if ((await this.store.getCurrentManifestVersion()) === version) {
4060
+ this.#autoCompactionHints.set(table.id, autoCompactionHint(version, segments));
3948
4061
  return segments;
3949
4062
  }
3950
4063
  }
4064
+ this.#autoCompactionHints.delete(table.id);
3951
4065
  return segments;
3952
4066
  }
3953
4067
  /**
@@ -3958,6 +4072,7 @@ export class MinnowDatabase {
3958
4072
  if (this.#closed)
3959
4073
  return;
3960
4074
  const contribution = transaction?.committedCatalogContribution;
4075
+ this.#advanceAutoCompactionHints(manifest, contribution?.segments);
3961
4076
  const initial = contribution?.initialCatalogProbe;
3962
4077
  const contributedTableIds = new Set(contribution?.segments.map((segment) => segment.tableId) ?? []);
3963
4078
  if (contribution !== undefined &&
@@ -4051,7 +4166,7 @@ export class MinnowDatabase {
4051
4166
  this.#armIdleCompactionCheck();
4052
4167
  }
4053
4168
  /** Counts one physical rewrite version once in this engine, including lost-ack recovery. */
4054
- #afterCompactionCommit(manifest) {
4169
+ #afterCompactionCommit(manifest, tableId) {
4055
4170
  if (this.#accountedCompactionVersions.has(manifest.version))
4056
4171
  return;
4057
4172
  this.#accountedCompactionVersions.add(manifest.version);
@@ -4060,8 +4175,49 @@ export class MinnowDatabase {
4060
4175
  if (oldest !== undefined)
4061
4176
  this.#accountedCompactionVersions.delete(oldest);
4062
4177
  }
4178
+ // A fold replaces source segments with a different physical layout. Its manifest is
4179
+ // logically unchanged and therefore carries no changed-table ID, so invalidate explicitly;
4180
+ // #runAutoCompaction's stable post-fold probe seeds the replacement counters.
4181
+ this.#autoCompactionHints.delete(tableId);
4063
4182
  this.#afterCommit(manifest);
4064
4183
  }
4184
+ /** Advances exact local hints, or discards them when a commit's provenance is incomplete. */
4185
+ #advanceAutoCompactionHints(manifest, contributedSegments) {
4186
+ for (const [tableId, hint] of this.#autoCompactionHints) {
4187
+ if (hint.version !== manifest.previousVersion) {
4188
+ // Another database committed since this instance last proved the layout.
4189
+ this.#autoCompactionHints.delete(tableId);
4190
+ continue;
4191
+ }
4192
+ this.#autoCompactionHints.set(tableId, { ...hint, version: manifest.version });
4193
+ }
4194
+ const additionsByTable = new Map();
4195
+ for (const segment of contributedSegments ?? []) {
4196
+ const additions = additionsByTable.get(segment.tableId);
4197
+ if (additions === undefined)
4198
+ additionsByTable.set(segment.tableId, [segment]);
4199
+ else
4200
+ additions.push(segment);
4201
+ }
4202
+ for (const tableId of manifest.changedTableIds) {
4203
+ const hint = this.#autoCompactionHints.get(tableId);
4204
+ if (hint === undefined)
4205
+ continue;
4206
+ const additions = additionsByTable.get(tableId);
4207
+ if (additions === undefined) {
4208
+ // DDL and future write paths without an exact segment contribution fail closed.
4209
+ this.#autoCompactionHints.delete(tableId);
4210
+ continue;
4211
+ }
4212
+ const contribution = autoCompactionHint(manifest.version, additions);
4213
+ this.#autoCompactionHints.set(tableId, {
4214
+ version: manifest.version,
4215
+ visible: hint.visible + contribution.visible,
4216
+ levelZero: hint.levelZero + contribution.levelZero,
4217
+ deltas: hint.deltas + contribution.deltas,
4218
+ });
4219
+ }
4220
+ }
4065
4221
  /**
4066
4222
  * Debounces the final write-path check for a burst. Sampling every few commits keeps the hot
4067
4223
  * path cheap, but the last one through seven commits can be the ones that cross a fold
@@ -4099,6 +4255,12 @@ export class MinnowDatabase {
4099
4255
  const table = await this.store.getTable(tableId);
4100
4256
  if (table === undefined)
4101
4257
  return;
4258
+ const version = await this.store.getCurrentManifestVersion();
4259
+ const hint = this.#autoCompactionHints.get(tableId);
4260
+ if (hint?.version === version) {
4261
+ this.#maybeScheduleAutoCompactionFromHint(table, hint);
4262
+ return;
4263
+ }
4102
4264
  this.#maybeScheduleAutoCompaction(table, await this.#currentVisibleSegments(table));
4103
4265
  }
4104
4266
  catch {
@@ -4379,6 +4541,7 @@ export class MinnowDatabase {
4379
4541
  const statementNow = this.#now();
4380
4542
  const filled = await fillColumnDefaults(target, input, (sql) => this.#evaluateDefaultExpression(sql, statementNow), derivedRows.length);
4381
4543
  normalizeDomainBatch(target, filled.batch);
4544
+ fillStoredGeneratedColumns(target, filled.batch, derivedRows.length, filled.generated);
4382
4545
  fillCompositePrimaryKey(target, filled.batch, derivedRows.length);
4383
4546
  validateBatch(target, filled.batch, filled.autoIncrement?.column.name);
4384
4547
  if (filled.autoIncrement !== undefined && filled.autoIncrement.missingIndexes.length > 0) {
@@ -4651,6 +4814,15 @@ export class MinnowDatabase {
4651
4814
  ? this.#queryCompiled(plan, {}, probe)
4652
4815
  : this.#sessionQueryPlan(transaction, await this.#applyCatalogRewrites(plan));
4653
4816
  }),
4817
+ queryFirstColumn: (plan) => this.#withReadReservation(async () => {
4818
+ // A staged overlay can add, replace, or remove keys without changing the pinned
4819
+ // manifest. Keep that case on the read-your-writes row executor; before the first
4820
+ // stage, the transaction's exact catalog probe makes the streamed snapshot safe.
4821
+ const probe = transaction.initialCatalogProbe;
4822
+ return transaction.stagedWorkCount === 0 && probe !== undefined
4823
+ ? this.#queryCompiledFirstColumn(plan, probe)
4824
+ : undefined;
4825
+ }),
4654
4826
  executeStatement: (statement) => {
4655
4827
  open();
4656
4828
  return guarded(() => this.runStatement(statement, { writer }));
@@ -4719,21 +4891,26 @@ export class MinnowDatabase {
4719
4891
  */
4720
4892
  async #sessionQuery(transaction, sql, options = {}) {
4721
4893
  options = this.#effectiveQueryOptions(options);
4894
+ throwIfAborted(options.signal);
4722
4895
  const bound = bindPlanParameters(this.#compileCached(sql), options.params);
4723
4896
  // The same catalog rewrites a read outside a scope gets: a scope that could not see a view
4724
4897
  // would make the scope's reads a different language from everyone else's.
4725
4898
  const plan = await this.#applyCatalogRewrites(bound);
4899
+ throwIfAborted(options.signal);
4726
4900
  return this.#sessionQueryPlan(transaction, plan, options);
4727
4901
  }
4728
4902
  /** #sessionQuery for an already-bound plan: trigger bodies resolve their reads through it. */
4729
4903
  async #sessionQueryPlan(transaction, plan, options = {}) {
4730
4904
  options = this.#effectiveQueryOptions(options);
4905
+ throwIfAborted(options.signal);
4731
4906
  const names = collectRealTableNames(plan);
4732
4907
  const tables = await Promise.all(names.map((name) => this.#findTable(name)));
4908
+ throwIfAborted(options.signal);
4733
4909
  const realTables = new Map(tables.map((table) => [table.name, table]));
4734
4910
  const pendingIds = new Set(transaction.pendingSegmentIds);
4735
4911
  const pendingBlocks = new Set(transaction.pendingBlockIds);
4736
4912
  const ourRecord = await this.store.getTransaction(transaction.id);
4913
+ throwIfAborted(options.signal);
4737
4914
  // Pinned to the scope's own snapshot, not the current manifest: the documented contract
4738
4915
  // is "the pre-scope snapshot plus everything this scope staged", and #assertKeysExist
4739
4916
  // already validates against transaction.snapshotVersion — reading the current version
@@ -4757,6 +4934,7 @@ export class MinnowDatabase {
4757
4934
  getBlocks: async (blockIds) => {
4758
4935
  const blocks = [];
4759
4936
  for (let start = 0; start < blockIds.length; start += 64) {
4937
+ throwIfAborted(options.signal);
4760
4938
  const window = blockIds.slice(start, start + 64);
4761
4939
  const committedIds = [];
4762
4940
  const committedIndexes = [];
@@ -4781,6 +4959,7 @@ export class MinnowDatabase {
4781
4959
  this.#getBlocksWindowed(committedIds),
4782
4960
  Promise.all(stagedIds.map((blockId) => transaction.getBlock(blockId))),
4783
4961
  ]);
4962
+ throwIfAborted(options.signal);
4784
4963
  const ordered = new Array(window.length);
4785
4964
  committedIndexes.forEach((index, offset) => {
4786
4965
  ordered[index] = committed[offset];
@@ -4799,10 +4978,12 @@ export class MinnowDatabase {
4799
4978
  const transactionRecords = new Map();
4800
4979
  const deferredSegments = transaction.deferredSegments;
4801
4980
  for (const table of tables) {
4981
+ throwIfAborted(options.signal);
4802
4982
  const segments = [
4803
4983
  ...(await listTableSegmentsPaged(this.store, table.id)),
4804
4984
  ...deferredSegments.filter((segment) => segment.tableId === table.id),
4805
4985
  ];
4986
+ throwIfAborted(options.signal);
4806
4987
  // Staged segments sort after every committed one; commitOrdinal keeps their
4807
4988
  // staging order (the journal's id list is sorted, so it cannot).
4808
4989
  const doctored = segments.map((segment) => pendingIds.has(segment.id)
@@ -4812,6 +4993,7 @@ export class MinnowDatabase {
4812
4993
  for (const record of await this.#transactionRecordsForSegments(doctored)) {
4813
4994
  transactionRecords.set(record.id, record);
4814
4995
  }
4996
+ throwIfAborted(options.signal);
4815
4997
  }
4816
4998
  if (ourRecord !== undefined) {
4817
4999
  transactionRecords.set(transaction.id, {
@@ -4831,6 +5013,7 @@ export class MinnowDatabase {
4831
5013
  /** Executes one plan at an explicit snapshot and visibility, covering every plan shape. */
4832
5014
  async #queryAtVisibility(plan, snapshot, visibility, realTables, cacheResults = true, options = {}) {
4833
5015
  options = this.#effectiveQueryOptions(options);
5016
+ throwIfAborted(options.signal);
4834
5017
  plan = expandFtsColumns(plan, (name) => searchableFtsColumns(realTables.get(name)));
4835
5018
  const typedSchemas = new Map([...realTables.values()].map((table) => [
4836
5019
  table.name,
@@ -4847,16 +5030,22 @@ export class MinnowDatabase {
4847
5030
  const spillPageRows = options.spillPageRows === undefined
4848
5031
  ? undefined
4849
5032
  : positiveWholeNumber(options.spillPageRows, "Query spill page rows");
5033
+ const report = (result) => {
5034
+ throwIfAborted(options.signal);
5035
+ options.onStats?.({ peakMemoryBytes: memory.usage.peakBytes });
5036
+ return result;
5037
+ };
4850
5038
  try {
4851
5039
  const resolution = subqueryResolutionSteps(plan);
4852
5040
  for (const step of resolution.steps) {
4853
- step.substitute(await this.#executeBlock(step.block, snapshot, visibility, memory, realTables, typedSchemas, undefined, cacheResults, allowSpill, forceSpill, spillPageRows));
5041
+ throwIfAborted(options.signal);
5042
+ step.substitute(await this.#executeBlock(step.block, snapshot, visibility, memory, realTables, typedSchemas, undefined, cacheResults, allowSpill, forceSpill, spillPageRows, options.signal));
4854
5043
  }
4855
5044
  const wrapper = transparentProjectionSource(resolution.plan);
4856
5045
  if (wrapper !== undefined) {
4857
- return projectResultColumns(await this.#executeBlock(wrapper.inner, snapshot, visibility, memory, realTables, typedSchemas, undefined, cacheResults, allowSpill, forceSpill, spillPageRows), wrapper.aliases);
5046
+ return report(projectResultColumns(await this.#executeBlock(wrapper.inner, snapshot, visibility, memory, realTables, typedSchemas, undefined, cacheResults, allowSpill, forceSpill, spillPageRows, options.signal), wrapper.aliases));
4858
5047
  }
4859
- return await this.#executeBlock(resolution.plan, snapshot, visibility, memory, realTables, typedSchemas, undefined, cacheResults, allowSpill, forceSpill, spillPageRows);
5048
+ return report(await this.#executeBlock(resolution.plan, snapshot, visibility, memory, realTables, typedSchemas, undefined, cacheResults, allowSpill, forceSpill, spillPageRows, options.signal));
4860
5049
  }
4861
5050
  finally {
4862
5051
  memory.close();
@@ -4941,6 +5130,7 @@ export class MinnowDatabase {
4941
5130
  throw new TypeError(`Table needs a unique key before rows can be updated: ${table.name}`);
4942
5131
  }
4943
5132
  input = normalizeDomainUpdate(table, input);
5133
+ rejectGeneratedUpdateAssignments(table, input);
4944
5134
  const keys = validateUpdateBatch(table, keyColumn, input);
4945
5135
  // Read-your-writes membership: keys staged by this scope pass, keys the scope removed
4946
5136
  // fail, and everything else checks against the committed snapshot as usual.
@@ -4955,12 +5145,17 @@ export class MinnowDatabase {
4955
5145
  await this.#assertKeysExist(table, keyColumn, transaction.snapshotVersion, committedKeys);
4956
5146
  }
4957
5147
  const sessionChecks = table.checks ?? [];
4958
- const changedForeignKey = (table.foreignKeys ?? []).some((key) => key.enforced !== false &&
5148
+ let changedForeignKey = (table.foreignKeys ?? []).some((key) => key.enforced !== false &&
4959
5149
  foreignKeyColumns(key).some((column) => input.changes[column] !== undefined));
4960
5150
  const preImages = await this.#triggerPreImages(table, keyColumn, input.keys.filter((key) => key !== null), "update", (preImageSql, params) => this.#sessionQuery(transaction, preImageSql, { params }), sessionChecks.length > 0 ||
4961
5151
  changedForeignKey ||
5152
+ tableGeneratedExpressions(table).length > 0 ||
4962
5153
  secondaryIndexUpdateNeedsPreImages(table, input) ||
4963
5154
  readyUniqueSecondaryIndexes(table).some(({ columns }) => columns.some((column) => input.changes[column.name] !== undefined)));
5155
+ input = applyStoredGeneratedUpdateChanges(table, input, preImages);
5156
+ validateUpdateBatch(table, keyColumn, input);
5157
+ changedForeignKey = (table.foreignKeys ?? []).some((key) => key.enforced !== false &&
5158
+ foreignKeyColumns(key).some((column) => input.changes[column] !== undefined));
4964
5159
  stageSecondaryUniqueMutationChanges(transaction, table, input, preImages);
4965
5160
  const secondaryDeltas = buildSecondaryUpdateDeltas(table, input, preImages);
4966
5161
  if (secondaryDeltas.length > 0) {
@@ -5042,7 +5237,19 @@ export class MinnowDatabase {
5042
5237
  },
5043
5238
  ]);
5044
5239
  await this.#stageTriggerDerivedInserts(transaction, table, "update", input.keys.length, sessionUpdateValueAt, "after", cascadeBudget);
5045
- return { tableName: table.name, segmentId, rowCount: input.keys.length };
5240
+ return {
5241
+ tableName: table.name,
5242
+ segmentId,
5243
+ rowCount: input.keys.length,
5244
+ ...(tableGeneratedExpressions(table).length === 0
5245
+ ? {}
5246
+ : {
5247
+ generatedColumns: Object.fromEntries(tableGeneratedExpressions(table).map(({ column }) => [
5248
+ column.name,
5249
+ (input.changes[column.name] ?? []).map((value) => externalSqlDomainValue(value)),
5250
+ ])),
5251
+ }),
5252
+ };
5046
5253
  }
5047
5254
  async #sessionDelete(transaction, tableName, input, cascadeBudget = 1, referentialBudget = REFERENTIAL_CASCADES) {
5048
5255
  const table = await this.#findTable(tableName);
@@ -5379,6 +5586,9 @@ export class MinnowDatabase {
5379
5586
  ...(columnDefinition.defaultSpec === undefined
5380
5587
  ? {}
5381
5588
  : { defaultValue: columnDefinition.defaultSpec }),
5589
+ ...(columnDefinition.generatedSpec === undefined
5590
+ ? {}
5591
+ : { generatedValue: columnDefinition.generatedSpec }),
5382
5592
  ...(columnDefinition.enumValues === undefined
5383
5593
  ? {}
5384
5594
  : { enumValues: columnDefinition.enumValues }),
@@ -5516,6 +5726,28 @@ export class MinnowDatabase {
5516
5726
  }
5517
5727
  }
5518
5728
  }
5729
+ // Adopting generation on an existing physical column is metadata-only only when every
5730
+ // stored value already equals the declared expression. This supports migrations from an
5731
+ // application-maintained column without silently blessing stale historical rows.
5732
+ for (const step of steps) {
5733
+ if (step.kind !== "alter-generated" || step.generatedValue === null)
5734
+ continue;
5735
+ const target = record.columns.find(({ name }) => name === step.columnName);
5736
+ if (target === undefined)
5737
+ continue;
5738
+ const expression = compileGeneratedColumnExpression(record.name, target.name, step.generatedValue.sql, record.columns);
5739
+ const rows = (await this.query(`SELECT * FROM ${quoteSqlIdentifier(record.name)}`, { memoize: false })).rows;
5740
+ for (const [rowIndex, row] of rows.entries()) {
5741
+ const expected = normalizeGeneratedColumnValue(target, evaluateRowExpression(expression, record.name, generatedEvaluationRow(record, row)), rowIndex);
5742
+ const actual = normalizeGeneratedColumnValue(target, row[target.name] ?? null, rowIndex);
5743
+ const equal = expected instanceof Date && actual instanceof Date
5744
+ ? dateMilliseconds(expected) === dateMilliseconds(actual)
5745
+ : Object.is(expected, actual);
5746
+ if (!equal) {
5747
+ throw new TypeError(`Generated column cannot be adopted: ${record.name}.${target.name} differs from its expression at row ${String(rowIndex)}`);
5748
+ }
5749
+ }
5750
+ }
5519
5751
  // The remaining metadata-only steps share one catalog/manifest CAS. Physical drops ran
5520
5752
  // above because segment rewriting and payload retirement have a stronger atomic boundary.
5521
5753
  const columns = applyColumnSteps(record, steps, this.#createId);
@@ -6067,6 +6299,7 @@ export class MinnowDatabase {
6067
6299
  const statementNow = this.#now();
6068
6300
  const filledProposed = await fillColumnDefaults(table, proposedBatch, (sql) => this.#evaluateDefaultExpression(sql, statementNow), statement.rows.length);
6069
6301
  normalizeDomainBatch(table, filledProposed.batch);
6302
+ fillStoredGeneratedColumns(table, filledProposed.batch, statement.rows.length, filledProposed.generated);
6070
6303
  fillCompositePrimaryKey(table, filledProposed.batch, statement.rows.length);
6071
6304
  const proposed = filledProposed.batch;
6072
6305
  // UPSERT only handles a uniqueness conflict. Other INSERT-domain failures are still errors,
@@ -6137,7 +6370,10 @@ export class MinnowDatabase {
6137
6370
  // and UUID defaults twice and could even change which row conflicts.
6138
6371
  const columns = visibleTableColumns(table).map(({ name }) => name);
6139
6372
  const deferredAutoIncrement = new Set(filledProposed.autoIncrement?.missingIndexes ?? []);
6140
- const rows = Array.from({ length: statement.rows.length }, (_, rowIndex) => columns.map((name) => name === filledProposed.autoIncrement?.column.name && deferredAutoIncrement.has(rowIndex)
6373
+ const generatedColumns = new Set(table.columns.flatMap((column) => column.generatedValue === undefined ? [] : [column.name]));
6374
+ const rows = Array.from({ length: statement.rows.length }, (_, rowIndex) => columns.map((name) => generatedColumns.has(name) ||
6375
+ (name === filledProposed.autoIncrement?.column.name &&
6376
+ deferredAutoIncrement.has(rowIndex))
6141
6377
  ? { default: true }
6142
6378
  : (proposed.columns[name]?.[rowIndex] ?? null)));
6143
6379
  return this.runStatement({
@@ -6231,6 +6467,8 @@ export class MinnowDatabase {
6231
6467
  if (freshRows.length > 0) {
6232
6468
  const columns = {};
6233
6469
  for (const column of table.columns) {
6470
+ if (column.generatedValue !== undefined)
6471
+ continue;
6234
6472
  columns[column.name] = freshRows.map((rowIndex) => proposed.columns[column.name]?.[rowIndex] ?? null);
6235
6473
  }
6236
6474
  await transaction.insertBatch(table.name, { columns });
@@ -6269,7 +6507,9 @@ export class MinnowDatabase {
6269
6507
  table: statement.table,
6270
6508
  rowCount: outcome.affectedRows.length,
6271
6509
  ...(version === null || version === undefined ? {} : { version }),
6272
- ...(outcome.returnedRows === undefined ? {} : { returnedRows: outcome.returnedRows }),
6510
+ ...(outcome.returnedRows === undefined || returningColumns === undefined
6511
+ ? {}
6512
+ : returningExecuteFields(table, returningColumns, outcome.returnedRows)),
6273
6513
  };
6274
6514
  }
6275
6515
  /** ON CONFLICT DO NOTHING: drops insert rows whose unique key already exists at a snapshot. */
@@ -6292,10 +6532,13 @@ export class MinnowDatabase {
6292
6532
  const statementNow = this.#now();
6293
6533
  const filled = await fillColumnDefaults(table, proposed, (sql) => this.#evaluateDefaultExpression(sql, statementNow), statement.rows.length);
6294
6534
  normalizeDomainBatch(table, filled.batch);
6535
+ fillStoredGeneratedColumns(table, filled.batch, statement.rows.length, filled.generated);
6295
6536
  fillCompositePrimaryKey(table, filled.batch, statement.rows.length);
6296
6537
  const columns = visibleTableColumns(table).map(({ name }) => name);
6297
6538
  const deferredAutoIncrement = new Set(filled.autoIncrement?.missingIndexes ?? []);
6298
- const materializedRows = Array.from({ length: statement.rows.length }, (_, rowIndex) => columns.map((name) => name === filled.autoIncrement?.column.name && deferredAutoIncrement.has(rowIndex)
6539
+ const generatedColumns = new Set(table.columns.flatMap((column) => (column.generatedValue === undefined ? [] : [column.name])));
6540
+ const materializedRows = Array.from({ length: statement.rows.length }, (_, rowIndex) => columns.map((name) => generatedColumns.has(name) ||
6541
+ (name === filled.autoIncrement?.column.name && deferredAutoIncrement.has(rowIndex))
6299
6542
  ? { default: true }
6300
6543
  : (filled.batch.columns[name]?.[rowIndex] ?? null)));
6301
6544
  const keys = filled.batch.columns[keyColumn.name] ?? [];
@@ -6754,6 +6997,9 @@ export class MinnowDatabase {
6754
6997
  if (record.columns.some(({ name }) => name === added.name)) {
6755
6998
  throw new TypeError(`Column already exists: ${addColumnStatement.table}.${added.name}`);
6756
6999
  }
7000
+ if (added.generatedValue !== undefined) {
7001
+ throw new TypeError("ALTER TABLE cannot add a generated column without rewriting existing rows");
7002
+ }
6757
7003
  if (added.defaultValue !== undefined) {
6758
7004
  validateColumnDefault({
6759
7005
  name: added.name,
@@ -6912,11 +7158,19 @@ export class MinnowDatabase {
6912
7158
  return this.#mergeConflictingInsertRows(statement, options);
6913
7159
  }
6914
7160
  if (statement.kind === "insert" && statement.rows.length === 0) {
7161
+ const table = await this.#findTable(statement.table);
7162
+ const returningColumns = options.returning === undefined
7163
+ ? undefined
7164
+ : options.returning === "*"
7165
+ ? visibleTableColumns(table).map(({ name }) => name)
7166
+ : [...options.returning];
6915
7167
  return {
6916
7168
  kind: "insert",
6917
7169
  table: statement.table,
6918
7170
  rowCount: 0,
6919
- ...(options.returning === undefined ? {} : { returnedRows: [] }),
7171
+ ...(returningColumns === undefined
7172
+ ? {}
7173
+ : returningExecuteFields(table, returningColumns, [])),
6920
7174
  };
6921
7175
  }
6922
7176
  if (statement.kind === "insert") {
@@ -6978,7 +7232,7 @@ export class MinnowDatabase {
6978
7232
  ...(returningColumns === undefined
6979
7233
  ? {}
6980
7234
  : {
6981
- returnedRows: statement.rows.map((row, rowIndex) => Object.fromEntries(returningColumns.map((name) => {
7235
+ ...returningExecuteFields(table, returningColumns, statement.rows.map((row, rowIndex) => Object.fromEntries(returningColumns.map((name) => {
6982
7236
  const column = table.columns.find((candidate) => candidate.name === name);
6983
7237
  const position = statement.columns.indexOf(name);
6984
7238
  const inserted = position < 0 ? undefined : row[position];
@@ -6988,7 +7242,7 @@ export class MinnowDatabase {
6988
7242
  ? null
6989
7243
  : boundInsertValue(inserted));
6990
7244
  return [name, executionSqlValueFromInput(column, value)];
6991
- }))),
7245
+ })))),
6992
7246
  }),
6993
7247
  };
6994
7248
  }
@@ -7050,6 +7304,32 @@ export class MinnowDatabase {
7050
7304
  having: [],
7051
7305
  orderBy: [],
7052
7306
  };
7307
+ if (statement.kind === "delete" && returningColumns === undefined) {
7308
+ const internalWriter = options.writer;
7309
+ if (internalWriter.queryFirstColumn !== undefined) {
7310
+ const selectedKeys = await internalWriter.queryFirstColumn(plan);
7311
+ if (selectedKeys !== undefined) {
7312
+ const keys = selectedKeys;
7313
+ if (keyColumn.type === "string" && keyColumn.sqlDomain === undefined) {
7314
+ for (let index = 0; index < keys.length; index += 1) {
7315
+ keys[index] = storedSqlValueFromExecution(keyColumn, keys[index] ?? null);
7316
+ }
7317
+ }
7318
+ if (keys.some((key) => key === null)) {
7319
+ throw new TypeError(`Unique key values must not be null: ${keyColumn.name}`);
7320
+ }
7321
+ if (keys.length === 0) {
7322
+ return { kind: "delete", table: table.name, rowCount: 0 };
7323
+ }
7324
+ const deleted = await internalWriter.deleteBatch(table.name, { keys });
7325
+ return {
7326
+ kind: "delete",
7327
+ table: table.name,
7328
+ rowCount: deleted.rowCount,
7329
+ };
7330
+ }
7331
+ }
7332
+ }
7053
7333
  let rows;
7054
7334
  if (options.writer !== undefined) {
7055
7335
  // Inside a scope the rows to touch are the ones the scope can see, which includes what it
@@ -7077,7 +7357,9 @@ export class MinnowDatabase {
7077
7357
  kind: "delete",
7078
7358
  table: table.name,
7079
7359
  rowCount: 0,
7080
- ...(returnedRows === undefined ? {} : { returnedRows }),
7360
+ ...(returnedRows === undefined || returningColumns === undefined
7361
+ ? {}
7362
+ : returningExecuteFields(table, returningColumns, returnedRows)),
7081
7363
  };
7082
7364
  }
7083
7365
  const deleted = options.writer === undefined
@@ -7090,7 +7372,9 @@ export class MinnowDatabase {
7090
7372
  ...(deleted.version === undefined || deleted.version === null
7091
7373
  ? {}
7092
7374
  : { version: deleted.version }),
7093
- ...(returnedRows === undefined ? {} : { returnedRows }),
7375
+ ...(returnedRows === undefined || returningColumns === undefined
7376
+ ? {}
7377
+ : returningExecuteFields(table, returningColumns, returnedRows)),
7094
7378
  };
7095
7379
  }
7096
7380
  if (keys.length === 0) {
@@ -7098,7 +7382,9 @@ export class MinnowDatabase {
7098
7382
  kind: "update",
7099
7383
  table: table.name,
7100
7384
  rowCount: 0,
7101
- ...(returningColumns === undefined ? {} : { returnedRows: [] }),
7385
+ ...(returningColumns === undefined
7386
+ ? {}
7387
+ : returningExecuteFields(table, returningColumns, [])),
7102
7388
  };
7103
7389
  }
7104
7390
  const changes = {};
@@ -7131,7 +7417,9 @@ export class MinnowDatabase {
7131
7417
  name,
7132
7418
  returnedChanges !== undefined && name in returnedChanges
7133
7419
  ? (returnedChanges[name]?.[index] ?? null)
7134
- : (row[name] ?? null),
7420
+ : updated.generatedColumns?.[name] !== undefined
7421
+ ? (updated.generatedColumns[name][index] ?? null)
7422
+ : (row[name] ?? null),
7135
7423
  ])));
7136
7424
  return {
7137
7425
  kind: "update",
@@ -7140,7 +7428,9 @@ export class MinnowDatabase {
7140
7428
  ...(updated.version === undefined || updated.version === null
7141
7429
  ? {}
7142
7430
  : { version: updated.version }),
7143
- ...(returnedRows === undefined ? {} : { returnedRows }),
7431
+ ...(returnedRows === undefined || returningColumns === undefined
7432
+ ? {}
7433
+ : returningExecuteFields(table, returningColumns, returnedRows)),
7144
7434
  };
7145
7435
  }
7146
7436
  /**
@@ -7175,6 +7465,7 @@ export class MinnowDatabase {
7175
7465
  */
7176
7466
  async #queryStreamed(plan, options, spillPageRows, probe, cursor) {
7177
7467
  options = this.#effectiveQueryOptions(options);
7468
+ throwIfAborted(options.signal);
7178
7469
  const tableNames = [plan.base.table, ...plan.joins.map((join) => join.table)];
7179
7470
  const uniqueTableNames = [...new Set(tableNames)];
7180
7471
  if (options.version === undefined) {
@@ -7204,6 +7495,7 @@ export class MinnowDatabase {
7204
7495
  });
7205
7496
  }
7206
7497
  async #queryStreamedAtSnapshot(plan, options, spillPageRows, snapshot, tables, visibility, cursor) {
7498
+ throwIfAborted(options.signal);
7207
7499
  // Copy-on-write: expansion clones only full-text plans, leaving the compile cache's copy
7208
7500
  // untouched.
7209
7501
  plan = expandFtsColumns(plan, (tableName) => searchableFtsColumns(tables.find((table) => table.name === tableName)));
@@ -7224,7 +7516,9 @@ export class MinnowDatabase {
7224
7516
  const visibleByTable = new Map();
7225
7517
  const rowCounts = new Map();
7226
7518
  for (const table of tables) {
7519
+ throwIfAborted(options.signal);
7227
7520
  const segments = await this.#visibleSegmentRecords(table, snapshot, visibility);
7521
+ throwIfAborted(options.signal);
7228
7522
  visibleByTable.set(table.name, segments);
7229
7523
  if (segments.every((segment) => {
7230
7524
  const kind = segment.kind;
@@ -7258,6 +7552,7 @@ export class MinnowDatabase {
7258
7552
  record.name,
7259
7553
  record.id === freshBaseTable.id ? freshBaseTable : record,
7260
7554
  ])), snapshot, visibility);
7555
+ throwIfAborted(options.signal);
7261
7556
  if (planContainsFts(plan, "bm25") && ftsStats === undefined)
7262
7557
  return undefined;
7263
7558
  // The streamed scan honors the same index pruning as the materialized path; a budgeted
@@ -7266,11 +7561,14 @@ export class MinnowDatabase {
7266
7561
  (await this.#visibleSegmentRecords(freshBaseTable, snapshot, visibility));
7267
7562
  this.#maybeScheduleAutoCompaction(freshBaseTable, visibleBaseSegments);
7268
7563
  const ftsSegments = await this.#ftsPrunedSegments(freshBaseTable, visibleBaseSegments, plan, snapshot);
7564
+ throwIfAborted(options.signal);
7269
7565
  const indexed = await this.#secondaryIndexPrunedSegments(freshBaseTable, ftsSegments, plan, snapshot);
7566
+ throwIfAborted(options.signal);
7270
7567
  const baseSegments = indexed.segments;
7271
7568
  // Zone-map elimination composes after index pruning: whole row groups whose statistics
7272
7569
  // reject the plan's predicates never stream at all.
7273
7570
  const zonePruned = await this.#zonePrunedStreamSegments(plan, freshBaseTable, projectedBaseColumns, baseSegments, snapshot);
7571
+ throwIfAborted(options.signal);
7274
7572
  const baseView = this.#streamedViewFactory(baseTable, projectedBaseColumns, zonePruned?.segments ?? baseSegments, snapshot, zonePruned?.storedBlocks, zonePruned !== undefined || indexed.pruned);
7275
7573
  if (baseView === undefined)
7276
7574
  return undefined;
@@ -7292,22 +7590,26 @@ export class MinnowDatabase {
7292
7590
  const budget = options.executionMemoryBudgetBytes;
7293
7591
  const estimate = estimatedColumnarBytes(buildSegments, projectedBuildColumns);
7294
7592
  if (buildView !== undefined && estimate > budget / 4) {
7295
- return this.#runPartitionedJoin(plan, budget, estimate, baseView, buildView, buildTable.name, partitionedShape.buildKeyName);
7593
+ return this.#runPartitionedJoin(plan, budget, estimate, baseView, buildView, buildTable.name, partitionedShape.buildKeyName, options.signal);
7296
7594
  }
7297
7595
  }
7298
7596
  }
7299
7597
  }
7300
7598
  const runStreamedExecution = async (attempt) => {
7599
+ throwIfAborted(options.signal);
7301
7600
  const memory = new QueryMemoryContext(options.executionMemoryBudgetBytes);
7302
7601
  let prepared;
7303
7602
  try {
7304
7603
  const streamed = await baseView.create(memory);
7604
+ throwIfAborted(options.signal);
7305
7605
  const inputTables = new Map([[baseTable.name, streamed.table]]);
7306
7606
  for (const table of tables) {
7607
+ throwIfAborted(options.signal);
7307
7608
  if (table.name === baseTable.name)
7308
7609
  continue;
7309
7610
  const requestedColumns = columns.get(table.name) ?? [];
7310
7611
  inputTables.set(table.name, await this.#materializeColumnarTableAtSnapshot(table, snapshot, requestedColumns.length === 0 ? [] : resolveReadColumns(table, requestedColumns), visibility));
7612
+ throwIfAborted(options.signal);
7311
7613
  }
7312
7614
  prepared = createPreparedColumnarQuery(plan, inputTables, memory, {
7313
7615
  ...(ftsStats === undefined ? {} : { ftsStats }),
@@ -7320,6 +7622,14 @@ export class MinnowDatabase {
7320
7622
  batchRows: cursor.batchRows,
7321
7623
  signal: cursor.signal,
7322
7624
  loadScanWindow: streamed.load,
7625
+ ...(cursor.consumeFirstColumn === undefined
7626
+ ? {}
7627
+ : {
7628
+ consumeFirstColumn: async (values) => {
7629
+ emission.happened = true;
7630
+ await cursor.consumeFirstColumn?.(values);
7631
+ },
7632
+ }),
7323
7633
  }, async (batch) => {
7324
7634
  emission.happened = true;
7325
7635
  await cursor.consume(externalizeQueryResult(batch));
@@ -7342,6 +7652,7 @@ export class MinnowDatabase {
7342
7652
  try {
7343
7653
  const streamedResult = await prepared.executeAsync({
7344
7654
  loadScanWindow: streamed.load,
7655
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
7345
7656
  });
7346
7657
  options.onStats?.({ peakMemoryBytes: memory.usage.peakBytes });
7347
7658
  return streamedResult;
@@ -7356,6 +7667,7 @@ export class MinnowDatabase {
7356
7667
  ...(spillPageRows === undefined ? {} : { spillPageRows }),
7357
7668
  spillStore: this.#leasedSpillStore(),
7358
7669
  loadScanWindow: streamed.load,
7670
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
7359
7671
  });
7360
7672
  options.onStats?.({ peakMemoryBytes: memory.usage.peakBytes });
7361
7673
  return spilledResult;
@@ -7605,7 +7917,8 @@ export class MinnowDatabase {
7605
7917
  * like any unordered query. The rescans trade time for bounded memory; the decoded-block
7606
7918
  * cache keeps repeat decodes cheap.
7607
7919
  */
7608
- async #runPartitionedJoin(plan, budgetBytes, estimatedBuildBytes, baseView, buildView, buildTableName, buildKeyName) {
7920
+ async #runPartitionedJoin(plan, budgetBytes, estimatedBuildBytes, baseView, buildView, buildTableName, buildKeyName, signal) {
7921
+ throwIfAborted(signal);
7609
7922
  // One partition should fit in an eighth of the budget: the resident partition briefly
7610
7923
  // exists twice (boxed rows, then their vectors) and shares the budget with both streamed
7611
7924
  // windows and the accumulating result.
@@ -7618,6 +7931,7 @@ export class MinnowDatabase {
7618
7931
  try {
7619
7932
  const combined = [];
7620
7933
  for (let partition = 0; partition < partitions; partition += 1) {
7934
+ throwIfAborted(signal);
7621
7935
  if (combined.length >= wantedRows)
7622
7936
  break;
7623
7937
  const passMemory = root.createChild();
@@ -7628,6 +7942,7 @@ export class MinnowDatabase {
7628
7942
  const collectMemory = passMemory.createChild();
7629
7943
  const buildScanMemory = collectMemory.createChild();
7630
7944
  const build = await buildView.create(buildScanMemory);
7945
+ throwIfAborted(signal);
7631
7946
  let buildRows = [];
7632
7947
  const buildColumns = [...build.table.columns.entries()];
7633
7948
  // Small steps keep the collection windows a minor budget term next to the resident
@@ -7635,8 +7950,10 @@ export class MinnowDatabase {
7635
7950
  // consumed length clamps to the returned resident end.
7636
7951
  const step = 1_024;
7637
7952
  for (let start = 0; start < build.table.rowCount;) {
7953
+ throwIfAborted(signal);
7638
7954
  let length = Math.min(step, build.table.rowCount - start);
7639
7955
  const residentEnd = await build.load(start, length);
7956
+ throwIfAborted(signal);
7640
7957
  if (typeof residentEnd === "number" && residentEnd > start) {
7641
7958
  length = Math.min(length, residentEnd - start);
7642
7959
  }
@@ -7667,13 +7984,17 @@ export class MinnowDatabase {
7667
7984
  const buildInput = columnarTableFromRows(buildTableName, buildRows, undefined, false);
7668
7985
  buildRows = [];
7669
7986
  const streamedBase = await baseView.create(passMemory);
7987
+ throwIfAborted(signal);
7670
7988
  const inputTables = new Map([
7671
7989
  [buildTableName, buildInput],
7672
7990
  [streamedBase.table.name, streamedBase.table],
7673
7991
  ]);
7674
7992
  prepared = createPreparedColumnarQuery(strippedPlan, inputTables, passMemory);
7675
7993
  collectMemory.close();
7676
- const result = await prepared.executeAsync({ loadScanWindow: streamedBase.load });
7994
+ const result = await prepared.executeAsync({
7995
+ loadScanWindow: streamedBase.load,
7996
+ ...(signal === undefined ? {} : { signal }),
7997
+ });
7677
7998
  for (const row of result.rows) {
7678
7999
  if (combined.length >= wantedRows)
7679
8000
  break;
@@ -8237,17 +8558,24 @@ export class MinnowDatabase {
8237
8558
  };
8238
8559
  }
8239
8560
  /** Prepares one block's inputs and executes it, returning the caller-owned result. */
8240
- async #executeBlock(block, snapshot, visibility, memory, realTables, typedSchemas, extraInputs, cacheResults = true, allowSpill = true, forceSpill = false, spillPageRows) {
8561
+ async #executeBlock(block, snapshot, visibility, memory, realTables, typedSchemas, extraInputs, cacheResults = true, allowSpill = true, forceSpill = false, spillPageRows, signal) {
8562
+ throwIfAborted(signal);
8241
8563
  const ftsStats = await this.#ftsIndexStats(block, realTables, snapshot, visibility);
8242
- const inputs = await this.#prepareBlockInputs(block, snapshot, visibility, memory, realTables, typedSchemas, extraInputs, cacheResults, allowSpill, forceSpill, spillPageRows);
8564
+ throwIfAborted(signal);
8565
+ const inputs = await this.#prepareBlockInputs(block, snapshot, visibility, memory, realTables, typedSchemas, extraInputs, cacheResults, allowSpill, forceSpill, spillPageRows, signal);
8566
+ throwIfAborted(signal);
8243
8567
  const prepared = createPreparedColumnarQuery(block, inputs, memory.createChild(), ftsStats === undefined ? {} : { ftsStats });
8244
8568
  try {
8245
8569
  if (!allowSpill || memory.usage.budgetBytes === Number.MAX_SAFE_INTEGER) {
8246
- return prepared.execute();
8570
+ const result = prepared.execute();
8571
+ throwIfAborted(signal);
8572
+ return result;
8247
8573
  }
8248
8574
  if (!forceSpill) {
8249
8575
  try {
8250
- return prepared.execute();
8576
+ const result = prepared.execute();
8577
+ throwIfAborted(signal);
8578
+ return result;
8251
8579
  }
8252
8580
  catch (error) {
8253
8581
  if (!(error instanceof QueryMemoryBudgetError))
@@ -8257,6 +8585,7 @@ export class MinnowDatabase {
8257
8585
  return await prepared.executeAsync({
8258
8586
  spillStore: this.#leasedSpillStore(),
8259
8587
  ...(spillPageRows === undefined ? {} : { spillPageRows }),
8588
+ ...(signal === undefined ? {} : { signal }),
8260
8589
  });
8261
8590
  }
8262
8591
  finally {
@@ -8270,14 +8599,17 @@ export class MinnowDatabase {
8270
8599
  * real tables it references, so an identical block over identical table states reuses
8271
8600
  * the previous result instead of re-reading and re-executing.
8272
8601
  */
8273
- async #executeBlockCached(block, snapshot, visibility, memory, realTables, typedSchemas, cacheResults = true, allowSpill = true, forceSpill = false, spillPageRows) {
8602
+ async #executeBlockCached(block, snapshot, visibility, memory, realTables, typedSchemas, cacheResults = true, allowSpill = true, forceSpill = false, spillPageRows, signal) {
8603
+ throwIfAborted(signal);
8274
8604
  const key = await this.#blockResultCacheKey(block, snapshot, visibility, realTables, cacheResults);
8605
+ throwIfAborted(signal);
8275
8606
  if (key !== undefined) {
8276
8607
  const cached = this.#cacheGet(key);
8277
8608
  if (cached !== undefined)
8278
8609
  return cached;
8279
8610
  }
8280
- const result = await this.#executeBlock(block, snapshot, visibility, memory, realTables, typedSchemas, undefined, cacheResults, allowSpill, forceSpill, spillPageRows);
8611
+ const result = await this.#executeBlock(block, snapshot, visibility, memory, realTables, typedSchemas, undefined, cacheResults, allowSpill, forceSpill, spillPageRows, signal);
8612
+ throwIfAborted(signal);
8281
8613
  if (key !== undefined)
8282
8614
  this.#cachePut(key, result, queryResultRetainedBytes(result));
8283
8615
  return result;
@@ -8288,15 +8620,18 @@ export class MinnowDatabase {
8288
8620
  * synthetic sources in typedSchemas, and a later cache hit skips that registration, so
8289
8621
  * re-inferring the schema at hit time would find those names missing.
8290
8622
  */
8291
- async #executeBlockWithSchemaCached(block, snapshot, visibility, memory, realTables, typedSchemas, cacheResults = true, allowSpill = true, forceSpill = false, spillPageRows) {
8623
+ async #executeBlockWithSchemaCached(block, snapshot, visibility, memory, realTables, typedSchemas, cacheResults = true, allowSpill = true, forceSpill = false, spillPageRows, signal) {
8624
+ throwIfAborted(signal);
8292
8625
  const key = await this.#blockResultCacheKey(block, snapshot, visibility, realTables, cacheResults);
8626
+ throwIfAborted(signal);
8293
8627
  const schemaKey = key === undefined ? undefined : `s${key}`;
8294
8628
  if (schemaKey !== undefined) {
8295
8629
  const cached = this.#cacheGet(schemaKey);
8296
8630
  if (cached !== undefined)
8297
8631
  return cached;
8298
8632
  }
8299
- const result = await this.#executeBlock(block, snapshot, visibility, memory, realTables, typedSchemas, undefined, cacheResults, allowSpill, forceSpill, spillPageRows);
8633
+ const result = await this.#executeBlock(block, snapshot, visibility, memory, realTables, typedSchemas, undefined, cacheResults, allowSpill, forceSpill, spillPageRows, signal);
8634
+ throwIfAborted(signal);
8300
8635
  const schema = inferBlockSchema(block, typedSchemas);
8301
8636
  const entry = { result, schema };
8302
8637
  if (schemaKey !== undefined) {
@@ -10669,7 +11004,7 @@ export class MinnowDatabase {
10669
11004
  if (recoveredManifest === undefined) {
10670
11005
  throw new Error(`Compaction manifest is missing: ${String(linkedTransaction.committedVersion)}`);
10671
11006
  }
10672
- this.#afterCompactionCommit(recoveredManifest);
11007
+ this.#afterCompactionCommit(recoveredManifest, table.id);
10673
11008
  return this.#publishedCompactionProgress(table, job);
10674
11009
  }
10675
11010
  const rewritePlan = job.rewritePlan;
@@ -10855,7 +11190,7 @@ export class MinnowDatabase {
10855
11190
  let publicationConflict;
10856
11191
  try {
10857
11192
  manifest = await transaction.commit();
10858
- this.#afterCompactionCommit(manifest);
11193
+ this.#afterCompactionCommit(manifest, table.id);
10859
11194
  break;
10860
11195
  }
10861
11196
  catch (error) {
@@ -14096,13 +14431,27 @@ function assertTriggerBodyTargetSchema(compiled, target) {
14096
14431
  if (column !== undefined &&
14097
14432
  isDefaultInsertValue(value) &&
14098
14433
  !column.nullable &&
14099
- column.defaultValue === undefined) {
14434
+ column.defaultValue === undefined &&
14435
+ column.generatedValue === undefined) {
14100
14436
  throw new TypeError(`Trigger body INSERT uses DEFAULT for a non-nullable column without a default: ${column.name}`);
14101
14437
  }
14102
14438
  }
14103
14439
  }
14440
+ for (const [index, column] of insertColumns.entries()) {
14441
+ if (column.generatedValue === undefined)
14442
+ continue;
14443
+ for (const row of compiled.rows) {
14444
+ const value = row[index];
14445
+ if (value === undefined || !isDefaultInsertValue(value)) {
14446
+ throw new TypeError(`Trigger body INSERT assigns generated column: ${column.name}`);
14447
+ }
14448
+ }
14449
+ }
14104
14450
  for (const column of columns) {
14105
- if (!provided.has(column.name) && !column.nullable && column.defaultValue === undefined) {
14451
+ if (!provided.has(column.name) &&
14452
+ !column.nullable &&
14453
+ column.defaultValue === undefined &&
14454
+ column.generatedValue === undefined) {
14106
14455
  throw new TypeError(`Trigger body INSERT omits a non-nullable column without a default: ${column.name}`);
14107
14456
  }
14108
14457
  }
@@ -14124,6 +14473,9 @@ function assertTriggerBodyTargetSchema(compiled, target) {
14124
14473
  if (assignedColumn === undefined) {
14125
14474
  throw new TypeError(`Trigger body UPDATE assignment column does not exist: ${assignment.column}`);
14126
14475
  }
14476
+ if (assignedColumn.generatedValue !== undefined) {
14477
+ throw new TypeError(`Trigger body UPDATE assigns generated column: ${assignment.column}`);
14478
+ }
14127
14479
  if (assignedColumn.id === target.uniqueKeyColumnId ||
14128
14480
  (target.primaryKeyColumnIds ?? []).includes(assignedColumn.id)) {
14129
14481
  throw new TypeError(`Trigger body UPDATE cannot update a key column: ${assignment.column}`);
@@ -14273,6 +14625,7 @@ function visibleSegmentManifestVersion(value, label) {
14273
14625
  */
14274
14626
  const compiledChecks = new Map();
14275
14627
  const COMPILED_CHECK_CACHE_LIMIT = 64;
14628
+ const compiledGeneratedColumns = new Map();
14276
14629
  function tableChecks(table) {
14277
14630
  const key = `${table.id}/${String(table.revision)}`;
14278
14631
  const cached = compiledChecks.get(key);
@@ -14295,6 +14648,84 @@ function tableChecks(table) {
14295
14648
  compiledChecks.set(key, compiled);
14296
14649
  return compiled;
14297
14650
  }
14651
+ function tableGeneratedExpressions(table) {
14652
+ const key = `${table.id}/${String(table.revision)}`;
14653
+ const cached = compiledGeneratedColumns.get(key);
14654
+ if (cached !== undefined)
14655
+ return cached;
14656
+ const compiled = table.columns.flatMap((column) => column.generatedValue === undefined
14657
+ ? []
14658
+ : [
14659
+ {
14660
+ column,
14661
+ expression: compileGeneratedColumnExpression(table.name, column.name, column.generatedValue.sql, table.columns),
14662
+ },
14663
+ ]);
14664
+ if (compiledGeneratedColumns.size >= COMPILED_CHECK_CACHE_LIMIT) {
14665
+ const oldest = compiledGeneratedColumns.keys().next().value;
14666
+ if (oldest !== undefined)
14667
+ compiledGeneratedColumns.delete(oldest);
14668
+ }
14669
+ compiledGeneratedColumns.set(key, compiled);
14670
+ return compiled;
14671
+ }
14672
+ function normalizeGeneratedColumnValue(column, value, rowIndex) {
14673
+ const stored = column.sqlDomain === undefined
14674
+ ? storedSqlValueFromExecution(column, value)
14675
+ : normalizeSqlDomainValue(column.sqlDomain, value);
14676
+ validateValue(column, stored, rowIndex);
14677
+ return stored;
14678
+ }
14679
+ /** Computes every stored generated column after defaults/domains have produced the base row. */
14680
+ function fillStoredGeneratedColumns(table, batch, rowCount, generated) {
14681
+ for (const { column, expression } of tableGeneratedExpressions(table)) {
14682
+ const values = new Array(rowCount);
14683
+ for (let rowIndex = 0; rowIndex < rowCount; rowIndex += 1) {
14684
+ const row = batchRowAt(table, batch, rowIndex);
14685
+ const value = evaluateRowExpression(expression, table.name, row);
14686
+ values[rowIndex] = normalizeGeneratedColumnValue(column, value, rowIndex);
14687
+ }
14688
+ batch.columns[column.name] = values;
14689
+ generated?.set(column.name, values.map((value) => externalSqlDomainValue(value)));
14690
+ }
14691
+ }
14692
+ function rejectGeneratedUpdateAssignments(table, input) {
14693
+ for (const column of table.columns) {
14694
+ if (column.generatedValue !== undefined && input.changes[column.name] !== undefined) {
14695
+ throw new TypeError(`Generated column cannot be assigned: ${column.name}`);
14696
+ }
14697
+ }
14698
+ }
14699
+ function generatedEvaluationRow(table, source) {
14700
+ const row = {};
14701
+ for (const column of table.columns) {
14702
+ const value = source[column.name] ?? null;
14703
+ row[column.name] =
14704
+ column.sqlDomain === undefined
14705
+ ? executionSqlValueFromStorage(column, value)
14706
+ : normalizeSqlDomainValue(column.sqlDomain, value);
14707
+ }
14708
+ return row;
14709
+ }
14710
+ function applyStoredGeneratedUpdateChanges(table, input, preImages) {
14711
+ const generated = tableGeneratedExpressions(table);
14712
+ if (generated.length === 0)
14713
+ return input;
14714
+ const changes = { ...input.changes };
14715
+ for (const { column, expression } of generated) {
14716
+ changes[column.name] = input.keys.map((_, rowIndex) => {
14717
+ const old = preImages[rowIndex];
14718
+ if (old === undefined)
14719
+ throw new Error(`Generated-column pre-image is missing: ${table.name}`);
14720
+ const row = generatedEvaluationRow(table, old);
14721
+ for (const [name, values] of Object.entries(input.changes)) {
14722
+ row[name] = values[rowIndex] ?? null;
14723
+ }
14724
+ return normalizeGeneratedColumnValue(column, evaluateRowExpression(expression, table.name, row), rowIndex);
14725
+ });
14726
+ }
14727
+ return { keys: input.keys, changes };
14728
+ }
14298
14729
  /**
14299
14730
  * Applies a table's CHECK constraints to one row (E141-06). A constraint fails only when it
14300
14731
  * evaluates to false: SQL's three-valued logic lets an unknown pass, which is why a NULL column
@@ -15414,7 +15845,7 @@ function tryKeyToken(type, value) {
15414
15845
  * (level one and above) are the folded state, not fragmentation, and do not count — a large
15415
15846
  * keyed table is many partitions by design.
15416
15847
  */
15417
- function autoCompactionDue(segments) {
15848
+ function autoCompactionHint(version, segments) {
15418
15849
  let levelZero = 0;
15419
15850
  let deltas = 0;
15420
15851
  for (const segment of segments) {
@@ -15424,7 +15855,18 @@ function autoCompactionDue(segments) {
15424
15855
  if (kind !== "insert" && kind !== "base")
15425
15856
  deltas += 1;
15426
15857
  }
15427
- return levelZero >= AUTO_COMPACT_SCAN_SEGMENTS || deltas >= AUTO_COMPACT_DELTA_SEGMENTS;
15858
+ return {
15859
+ version,
15860
+ visible: segments.length,
15861
+ levelZero,
15862
+ deltas,
15863
+ };
15864
+ }
15865
+ function autoCompactionDueHint(hint) {
15866
+ return hint.levelZero >= AUTO_COMPACT_SCAN_SEGMENTS || hint.deltas >= AUTO_COMPACT_DELTA_SEGMENTS;
15867
+ }
15868
+ function autoCompactionDue(segments) {
15869
+ return autoCompactionDueHint(autoCompactionHint(null, segments));
15428
15870
  }
15429
15871
  /** A macrotask boundary, so background work lets queued queries and writes run between steps. */
15430
15872
  function yieldToEventLoop() {