@minnowdb/core 0.4.1 → 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 (47) hide show
  1. package/README.md +5 -5
  2. package/dist/engine/cancellation.d.ts +2 -0
  3. package/dist/engine/cancellation.js +4 -0
  4. package/dist/engine/catalog.d.ts +5 -1
  5. package/dist/engine/catalog.js +5 -1
  6. package/dist/engine/client.d.ts +34 -6
  7. package/dist/engine/client.js +87 -19
  8. package/dist/engine/database.d.ts +43 -20
  9. package/dist/engine/database.js +823 -164
  10. package/dist/engine/defaults.js +11 -0
  11. package/dist/engine/errors.d.ts +19 -0
  12. package/dist/engine/errors.js +31 -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 +12 -13
  16. package/dist/engine/optimizer.js +546 -39
  17. package/dist/engine/query-cache.js +1 -0
  18. package/dist/engine/query.d.ts +16 -278
  19. package/dist/engine/query.js +260 -74
  20. package/dist/engine/result-wire.d.ts +2 -0
  21. package/dist/engine/result-wire.js +21 -5
  22. package/dist/engine/schema-wire.d.ts +14 -1
  23. package/dist/engine/schema-wire.js +7 -1
  24. package/dist/engine/schema.d.ts +83 -32
  25. package/dist/engine/schema.js +180 -14
  26. package/dist/engine/sql-domains.d.ts +11 -0
  27. package/dist/engine/sql-domains.js +65 -1
  28. package/dist/engine/sql-json.js +22 -3
  29. package/dist/engine/sql-semantics.js +21 -3
  30. package/dist/engine/vector.d.ts +2 -2
  31. package/dist/engine/vector.js +328 -79
  32. package/dist/engine/worker-host.js +119 -44
  33. package/dist/plan/index.d.ts +5 -4
  34. package/dist/plan/index.js +3 -3
  35. package/dist/plan/model.d.ts +218 -0
  36. package/dist/plan/model.js +1 -0
  37. package/dist/storage/indexeddb.js +4 -12
  38. package/dist/storage/toolkit/record-core.js +7 -22
  39. package/dist/storage/types.d.ts +26 -8
  40. package/dist/storage/types.js +85 -0
  41. package/dist/transactions/index.d.ts +5 -3
  42. package/dist/transactions/index.js +58 -8
  43. package/dist/worker-protocol/index.d.ts +6 -1
  44. package/dist/worker-protocol/index.js +5 -2
  45. package/package.json +1 -1
  46. package/postgres-feature-profile.json +5 -0
  47. package/sql-feature-matrix.json +75 -19
@@ -1,26 +1,28 @@
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, 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";
9
- import { MAX_CACHEABLE_TEXT_CHARACTERS } from "./cache-limits.js";
11
+ import { MAX_CACHEABLE_TEXT_CHARACTERS, MAX_SQL_PARAMETERS } from "./cache-limits.js";
10
12
  import { fillColumnDefaults, patchAutoIncrementValues, } from "./defaults.js";
11
13
  import { cachedQueryTerms, FTS_TOKENIZER_VERSION, renderDocumentValue, tokenize as ftsTokenize, } from "./fts.js";
12
14
  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";
13
15
  import { decodeSnapshotFrameStream, encodeSnapshotFrameStreamFooter, encodeSnapshotFrameStreamHeader, extendSnapshotFrameStreamChecksum, snapshotFrameEnvelopeParts, snapshotFrameStreamHeaderIdentity, } from "../storage/snapshot.js";
14
16
  import { Snapshot, TransactionManager, } from "../transactions/index.js";
15
- import { applyWindowFunctions, bindPlanParameters, bindStatementParameters, DUAL_TABLE, dualTableRows, blockHasSubqueries, combineUnionResults, compileCheckExpression, compileQuery, hasAggregate, createRecursiveCteState, compileStatement, createPreparedColumnarQuery, evaluateJoinedRowExpression, evaluateRowExpression, externalizeQueryResult, expressionColumnNames, inferBlockSchema, isDefaultInsertValue, referencedColumns, childExpressions, expandFtsColumns, expandNaturalJoins, expandSourceColumnAliases, expandViewSources, forEachBlockExpression, planContainsFts, planHasNaturalJoins, planHasSourceColumnAliases, planReadsTable, planReadsViews, planReadsBeyondSingleScan, projectResultColumns, queryResultNeedsExternalization, resolveStatementDatetimes, subqueryResolutionSteps, topLevelFtsMatchConjuncts, transparentProjectionSource, validateDefaultExpression, windowOutputDomain, windowOutputType, } from "./query.js";
17
+ import { applyWindowFunctions, bindPlanParameters, bindStatementParameters, DUAL_TABLE, dualTableRows, blockHasSubqueries, combineUnionResults, compileCheckExpression, compileQuery, hasAggregate, createRecursiveCteState, 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";
16
18
  import { copyQueryResult, planMemoKey, queryResultMemoKey, queryResultRetainedBytes, RESULT_MEMO_MAX_BYTES, } from "./query-cache.js";
17
19
  import { QueryMemoryBudgetError, QueryMemoryContext, DEFAULT_QUERY_MEMORY_BUDGET_BYTES, } from "./memory.js";
18
20
  import { LiveQueryLimitError, LiveQuerySet, MAX_LIVE_QUERY_SETS_PER_DATABASE, } from "./live.js";
19
21
  import { chooseJoinOrder, renderPlan } from "./optimizer.js";
20
22
  import { encodeSqlEqualityValue } from "./sql-semantics.js";
21
- import { externalSqlDomainValue, externalSqlTextValue, isSqlDomainValue, normalizeSqlDomainValue, protectedSqlTextValue, } from "./sql-domains.js";
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. */
@@ -272,7 +266,7 @@ function migrationResult(steps) {
272
266
  };
273
267
  }
274
268
  export const MAX_GARBAGE_COLLECTION_RETAIN_RECENT_VERSIONS = 1_024;
275
- export { CompactionBacklogError, CompactionJobCancelledError, CompactionMemoryBudgetError, CompactionWriteAmplificationError, MaintenanceBacklogError, MissingKeyError, SqlCompileError, TableInUseError, UniqueConstraintError, VisibleSegmentCursorStaleError, };
269
+ export { CompactionBacklogError, CompactionJobCancelledError, CompactionMemoryBudgetError, CompactionWriteAmplificationError, MaintenanceBacklogError, MissingKeyError, SqlCompileError, UnknownTableError, TableInUseError, UniqueConstraintError, VisibleSegmentCursorStaleError, };
276
270
  /**
277
271
  * How deep a delete's referential actions may cascade. A chain of CASCADE keys can reach further
278
272
  * tables; the bound stops a cycle from recursing, the same way the trigger chain is bounded.
@@ -291,13 +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
- returnedRows: externalizeQueryResult({ columns, rows: result.returnedRows }).rows,
304
+ returnedRows: externalizeQueryResult({
305
+ columns,
306
+ columnDomains,
307
+ rows: result.returnedRows,
308
+ }).rows,
309
+ returnedColumns: [...columns],
310
+ returnedColumnDomains: structuredClone(columnDomains),
301
311
  };
302
312
  }
303
313
  /** Rejects an INSERT value that is still an unbound `?`/`$n` slot. */
@@ -567,6 +577,8 @@ export class MinnowDatabase {
567
577
  #droppingTables = new Set();
568
578
  /** Per table: the visible segment count a failed auto-compaction must see before retrying. */
569
579
  #autoCompactionBackoff = new Map();
580
+ /** Exact current-layout counters; local commits advance them without rescanning history. */
581
+ #autoCompactionHints = new Map();
570
582
  /** Data commits per table since its last write-path auto-compaction check. */
571
583
  #commitsSinceCompactionCheck = new Map();
572
584
  /** The compaction step in flight per table, so steps on one table run one at a time. */
@@ -745,6 +757,7 @@ export class MinnowDatabase {
745
757
  this.#autoCompactionsRequested.clear();
746
758
  this.#idleCompactionTableIds.clear();
747
759
  this.#autoCompactionBackoff.clear();
760
+ this.#autoCompactionHints.clear();
748
761
  this.#commitsSinceCompactionCheck.clear();
749
762
  this.#artifactCache.clear();
750
763
  }
@@ -809,6 +822,9 @@ export class MinnowDatabase {
809
822
  ...(sqlDomain === undefined ? {} : { sqlDomain: structuredClone(sqlDomain) }),
810
823
  nullable: column.nullable ?? false,
811
824
  ...(column.defaultValue === undefined ? {} : { defaultValue: column.defaultValue }),
825
+ ...(column.generatedValue === undefined
826
+ ? {}
827
+ : { generatedValue: structuredClone(column.generatedValue) }),
812
828
  ...(column.enumValues === undefined
813
829
  ? {}
814
830
  : { enumValues: validateEnumValues(column.enumValues, columnName) }),
@@ -867,6 +883,15 @@ export class MinnowDatabase {
867
883
  throw new TypeError(`Unique key cannot be nullable: ${uniqueKeyColumn.name}`);
868
884
  }
869
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
+ }
870
895
  if (column.defaultValue !== undefined) {
871
896
  validateColumnDefault({ ...column, isUniqueKey: column === uniqueKeyColumn }, column.defaultValue);
872
897
  if (column.defaultValue.kind === "literal" && column.sqlDomain !== undefined) {
@@ -894,6 +919,9 @@ export class MinnowDatabase {
894
919
  const checkNames = (input.checks ?? []).map((check) => claimConstraintName(check.name));
895
920
  const uniqueConstraintNames = (input.uniqueConstraints ?? []).map((constraint) => claimConstraintName(constraint.name));
896
921
  const foreignKeys = await Promise.all((input.foreignKeys ?? []).map(async (key, keyIndex) => {
922
+ if (key.enforced === false && key.onDelete !== "restrict") {
923
+ throw new TypeError(`Informational FOREIGN KEY ${key.name} cannot declare ON DELETE actions`);
924
+ }
897
925
  const childNames = key.columns ?? [key.column];
898
926
  const children = childNames.map((columnName) => {
899
927
  const column = columns.find((candidate) => candidate.name === columnName);
@@ -959,6 +987,7 @@ export class MinnowDatabase {
959
987
  parentTable: key.parentTable,
960
988
  parentColumns: parentNames,
961
989
  onDelete: key.onDelete,
990
+ ...(key.enforced === false ? { enforced: false } : {}),
962
991
  };
963
992
  }));
964
993
  const checks = (input.checks ?? []).map((check, checkIndex) => {
@@ -1051,6 +1080,9 @@ export class MinnowDatabase {
1051
1080
  : { sqlDomain: structuredClone(column.sqlDomain) }),
1052
1081
  nullable: column.nullable,
1053
1082
  ...(column.defaultValue === undefined ? {} : { defaultValue: column.defaultValue }),
1083
+ ...(column.generatedValue === undefined
1084
+ ? {}
1085
+ : { generatedValue: column.generatedValue }),
1054
1086
  ...(column.enumValues === undefined ? {} : { enumValues: [...column.enumValues] }),
1055
1087
  })),
1056
1088
  ...(uniqueKey === undefined ? {} : { uniqueKey }),
@@ -1121,7 +1153,7 @@ export class MinnowDatabase {
1121
1153
  }
1122
1154
  async #withReadReservation(run) {
1123
1155
  if (this.#activeReadReservations >= MAX_DATABASE_ACTIVE_READS) {
1124
- throw new DatabaseReadBacklogError();
1156
+ throw new DatabaseReadBacklogError(MAX_DATABASE_ACTIVE_READS);
1125
1157
  }
1126
1158
  this.#activeReadReservations += 1;
1127
1159
  try {
@@ -1132,7 +1164,9 @@ export class MinnowDatabase {
1132
1164
  }
1133
1165
  }
1134
1166
  async #queryWithReadReservation(sql, options) {
1167
+ throwIfAborted(options.signal);
1135
1168
  await this.#settleExpiredStatementTransaction();
1169
+ throwIfAborted(options.signal);
1136
1170
  // An open statement transaction routes through its session, which owns the reservation for
1137
1171
  // the actual execution. Avoid charging the same query twice at this public boundary.
1138
1172
  return this.#openTransaction === undefined
@@ -1383,7 +1417,7 @@ export class MinnowDatabase {
1383
1417
  throw new TypeError(`${tableName} is a view, not a table`);
1384
1418
  if (options.ifExists === true)
1385
1419
  return false;
1386
- throw new Error(`Table not found: ${tableName}`);
1420
+ throw new UnknownTableError(tableName);
1387
1421
  }
1388
1422
  pinnedTableId ??= table.id;
1389
1423
  if (table.id !== pinnedTableId) {
@@ -1513,7 +1547,7 @@ export class MinnowDatabase {
1513
1547
  }
1514
1548
  if (options.ifExists === true)
1515
1549
  return false;
1516
- throw new Error(`Table not found: ${tableName}`);
1550
+ throw new UnknownTableError(tableName);
1517
1551
  }
1518
1552
  pinnedTableId ??= table.id;
1519
1553
  if (table.id !== pinnedTableId) {
@@ -1561,6 +1595,7 @@ export class MinnowDatabase {
1561
1595
  for (const column of table.columns)
1562
1596
  this.#gzipVerdicts.delete(column.id);
1563
1597
  this.#autoCompactionBackoff.delete(table.id);
1598
+ this.#autoCompactionHints.delete(table.id);
1564
1599
  this.#commitsSinceCompactionCheck.delete(table.id);
1565
1600
  this.#idleCompactionTableIds.delete(table.id);
1566
1601
  const postingPrefix = `${table.id}/`;
@@ -1596,12 +1631,14 @@ export class MinnowDatabase {
1596
1631
  : undefined;
1597
1632
  const current = await this.store.getTable(table.id);
1598
1633
  if (current === undefined)
1599
- throw new TypeError(`Unknown table: ${tableName}`);
1634
+ throw new UnknownTableError(tableName);
1600
1635
  const result = await this.#writeBatch(current, batch, "insert", keys, autoIncrement);
1601
1636
  return { result, batch, generated, autoIncrement, rowCount };
1602
1637
  });
1603
1638
  const { result, batch, generated, autoIncrement, rowCount } = completed;
1604
1639
  collectAutoIncrementGenerated(batch, generated, autoIncrement);
1640
+ if (result.segmentId === null)
1641
+ throw new Error("An insert did not create a segment");
1605
1642
  return {
1606
1643
  tableName: result.tableName,
1607
1644
  segmentId: result.segmentId,
@@ -1624,6 +1661,7 @@ export class MinnowDatabase {
1624
1661
  Object.values(pivoted.columns).find((values) => values.length > 0)?.length ??
1625
1662
  0;
1626
1663
  normalizeDomainBatch(table, filled.batch);
1664
+ fillStoredGeneratedColumns(table, filled.batch, inputRowCount, filled.generated);
1627
1665
  for (const name of filled.generated.keys()) {
1628
1666
  const column = table.columns.find((candidate) => candidate.name === name);
1629
1667
  const values = filled.batch.columns[name];
@@ -1655,35 +1693,49 @@ export class MinnowDatabase {
1655
1693
  async insert(tableName, row) {
1656
1694
  return this.insertBatch(tableName, [row]);
1657
1695
  }
1658
- upsertBatch(tableName, input) {
1659
- return this.#withWriteReservation(() => this.#upsertBatchReserved(tableName, input));
1696
+ upsertBatch(tableName, input, options = {}) {
1697
+ return this.#withWriteReservation(() => this.#upsertBatchReserved(tableName, input, options));
1660
1698
  }
1661
- async #upsertBatchReserved(tableName, input) {
1699
+ async #upsertBatchReserved(tableName, input, options) {
1662
1700
  const completed = await this.#runWrite(async () => {
1663
1701
  const table = await this.#findTable(tableName);
1664
1702
  const keyColumn = getUniqueKeyColumn(table);
1665
1703
  if (keyColumn === undefined) {
1666
1704
  throw new TypeError(`Table needs a unique key before it can be upserted: ${table.name}`);
1667
1705
  }
1668
- const { batch, generated, autoIncrement, rowCount } = await this.#fillDefaults(table, input);
1706
+ const { batch, generated, autoIncrement } = await this.#fillDefaults(table, input);
1669
1707
  const deferred = autoIncrement !== undefined && autoIncrement.missingIndexes.length > 0;
1670
1708
  const keys = deferred ? undefined : batchKeys(table, batch);
1671
1709
  const current = await this.store.getTable(table.id);
1672
1710
  if (current === undefined)
1673
- throw new TypeError(`Unknown table: ${tableName}`);
1674
- const result = await this.#writeBatch(current, batch, "upsert", keys, autoIncrement);
1675
- return { result, batch, generated, autoIncrement, rowCount };
1711
+ throw new UnknownTableError(tableName);
1712
+ const result = await this.#writeBatch(current, batch, "upsert", keys, autoIncrement, options.conflictWhere);
1713
+ return { result, batch, generated, autoIncrement };
1676
1714
  });
1677
- const { result, batch, generated, autoIncrement, rowCount } = completed;
1715
+ const { result, batch, generated, autoIncrement } = completed;
1678
1716
  collectAutoIncrementGenerated(batch, generated, autoIncrement);
1717
+ if (result.acceptedRowIndexes.length !== result.requestedRowCount) {
1718
+ for (const [name, values] of generated) {
1719
+ generated.set(name, result.acceptedRowIndexes.map((index) => values[index] ?? null));
1720
+ }
1721
+ }
1679
1722
  return {
1680
- ...result,
1681
- rowCount,
1723
+ tableName: result.tableName,
1724
+ segmentId: result.segmentId,
1725
+ requestedRowCount: result.requestedRowCount,
1726
+ rowCount: result.rowCount,
1727
+ insertedRowCount: result.insertedRowCount,
1728
+ updatedRowCount: result.updatedRowCount,
1729
+ skippedRowCount: result.skippedRowCount,
1730
+ blockCount: result.blockCount,
1731
+ storedBytes: result.storedBytes,
1732
+ version: result.version,
1733
+ metrics: result.metrics,
1682
1734
  ...(generated.size === 0 ? {} : { generatedColumns: Object.fromEntries(generated) }),
1683
1735
  };
1684
1736
  }
1685
- async upsert(tableName, row) {
1686
- return this.upsertBatch(tableName, [row]);
1737
+ async upsert(tableName, row, options = {}) {
1738
+ return this.upsertBatch(tableName, [row], options);
1687
1739
  }
1688
1740
  updateBatch(tableName, input) {
1689
1741
  return this.#withWriteReservation(() => this.#updateBatchReserved(tableName, input));
@@ -1696,10 +1748,11 @@ export class MinnowDatabase {
1696
1748
  throw new TypeError(`Table needs a unique key before rows can be updated: ${table.name}`);
1697
1749
  }
1698
1750
  const normalizedInput = normalizeDomainUpdate(table, input);
1751
+ rejectGeneratedUpdateAssignments(table, normalizedInput);
1699
1752
  const keys = validateUpdateBatch(table, keyColumn, normalizedInput);
1700
1753
  const current = await this.store.getTable(table.id);
1701
1754
  if (current === undefined)
1702
- throw new TypeError(`Unknown table: ${tableName}`);
1755
+ throw new UnknownTableError(tableName);
1703
1756
  const currentKey = getUniqueKeyColumn(current);
1704
1757
  if (currentKey?.id !== keyColumn.id) {
1705
1758
  throw new TypeError(`Table unique key changed during update: ${tableName}`);
@@ -1713,6 +1766,10 @@ export class MinnowDatabase {
1713
1766
  changes: Object.fromEntries(Object.entries(changes).map(([name, value]) => [name, [value]])),
1714
1767
  });
1715
1768
  }
1769
+ /** Deletes one row by the table's unique key. */
1770
+ async delete(tableName, key) {
1771
+ return this.deleteBatch(tableName, { keys: [key] });
1772
+ }
1716
1773
  deleteBatch(tableName, input) {
1717
1774
  return this.#withWriteReservation(() => this.#deleteBatchReserved(tableName, input));
1718
1775
  }
@@ -1948,15 +2005,13 @@ export class MinnowDatabase {
1948
2005
  async #writeUpdateBatch(table, keyColumn, input, keys) {
1949
2006
  await this.#assertCompactionCapacity(table);
1950
2007
  const started = performance.now();
1951
- const logicalBytes = estimateValuesBytes(input.keys) +
1952
- Object.values(input.changes).reduce((total, values) => total + estimateValuesBytes(values), 0);
2008
+ let logicalBytes;
1953
2009
  // Deferred: the record rides the single-shot commit below unless trigger rows stage first.
1954
2010
  const transaction = await this.#transactions.beginDeferred({ durableSnapshot: false });
1955
2011
  transaction.limitLevelZeroSegments(table.id, MAX_LEVEL_ZERO_SEGMENTS);
1956
2012
  const blockStager = new BoundedWriteBlockStager(transaction);
1957
2013
  const segmentId = this.#createId();
1958
2014
  const columnBlockIds = {};
1959
- const changedColumns = Object.keys(input.changes).sort();
1960
2015
  let storedBytes = 0;
1961
2016
  let blockCount = 0;
1962
2017
  let encodeMs = 0;
@@ -1965,6 +2020,22 @@ export class MinnowDatabase {
1965
2020
  let retries = 0;
1966
2021
  try {
1967
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();
1968
2039
  const columns = [keyColumn, ...changedColumns.map((name) => findColumn(table, name))];
1969
2040
  const plannedColumns = columns.map((column) => writeColumnValues(column.type, column.id === keyColumn.id ? input.keys : (input.changes[column.name] ?? [])));
1970
2041
  const ranges = writeBlockRanges(plannedColumns, input.keys.length, this.#rowsPerBlock, this.#targetBlockBytes);
@@ -1998,12 +2069,6 @@ export class MinnowDatabase {
1998
2069
  }
1999
2070
  columnBlockIds[column.id] = blockIds;
2000
2071
  }
2001
- const checks = table.checks ?? [];
2002
- const changedForeignKey = (table.foreignKeys ?? []).some((key) => foreignKeyColumns(key).some((column) => input.changes[column] !== undefined));
2003
- const preImages = await this.#triggerPreImages(table, keyColumn, input.keys.filter((key) => key !== null), "update", (sql, params) => this.#sessionQuery(transaction, sql, { params }), checks.length > 0 ||
2004
- changedForeignKey ||
2005
- secondaryIndexUpdateNeedsPreImages(table, input) ||
2006
- readyUniqueSecondaryIndexes(table).some(({ columns }) => columns.some((column) => input.changes[column.name] !== undefined)));
2007
2072
  stageSecondaryUniqueMutationChanges(transaction, table, input, preImages);
2008
2073
  const secondaryDeltas = buildSecondaryUpdateDeltas(table, input, preImages);
2009
2074
  if (secondaryDeltas.length > 0) {
@@ -2076,6 +2141,14 @@ export class MinnowDatabase {
2076
2141
  blockCount,
2077
2142
  storedBytes,
2078
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
+ }),
2079
2152
  metrics: createWriteMetrics({
2080
2153
  logicalBytes,
2081
2154
  storedBytes,
@@ -2117,12 +2190,13 @@ export class MinnowDatabase {
2117
2190
  }
2118
2191
  throw new Error(`Unique-key lookup returned an inconsistent result: ${table.name}`);
2119
2192
  }
2120
- async #writeBatch(table, input, kind, keys, autoIncrement) {
2193
+ async #writeBatch(table, input, kind, keys, autoIncrement, conflictWhere) {
2121
2194
  await this.#assertCompactionCapacity(table);
2122
2195
  const started = performance.now();
2123
- const rowCount = input.columns[table.columns[0]?.name ?? ""]?.length ?? 0;
2124
- const logicalBytes = estimateBatchBytes(input);
2125
- const { transaction, rowIds: reservedRowIds, autoIncrementValues, } = await this.#transactions.beginWithReservation({ tableId: table.id, count: rowCount }, autoIncrement === undefined
2196
+ const requestedRowCount = input.columns[table.columns[0]?.name ?? ""]?.length ?? 0;
2197
+ let rowCount = requestedRowCount;
2198
+ let logicalBytes = estimateBatchBytes(input);
2199
+ const { transaction, rowIds: reservedRowIds, autoIncrementValues, } = await this.#transactions.beginWithReservation({ tableId: table.id, count: requestedRowCount }, autoIncrement === undefined
2126
2200
  ? undefined
2127
2201
  : {
2128
2202
  tableId: table.id,
@@ -2133,11 +2207,14 @@ export class MinnowDatabase {
2133
2207
  transaction.limitLevelZeroSegments(table.id, MAX_LEVEL_ZERO_SEGMENTS);
2134
2208
  const blockStager = new BoundedWriteBlockStager(transaction);
2135
2209
  let resolvedKeys = keys;
2136
- const segmentId = this.#createId();
2210
+ let segmentId = null;
2137
2211
  const columnBlockIds = {};
2138
2212
  let storedBytes = 0;
2139
2213
  let blockCount = 0;
2140
2214
  let counts;
2215
+ let skippedRowCount = 0;
2216
+ let acceptedRowIndexes = Array.from({ length: requestedRowCount }, (_, index) => index);
2217
+ let upsertFirings;
2141
2218
  let encodeMs = 0;
2142
2219
  let stageMs = 0;
2143
2220
  let commitMs = 0;
@@ -2157,6 +2234,65 @@ export class MinnowDatabase {
2157
2234
  }
2158
2235
  resolvedKeys = batchKeys(table, input);
2159
2236
  }
2237
+ const normalizedConflictWhere = kind === "upsert" && conflictWhere !== undefined
2238
+ ? normalizeUpsertConflictWhere(table, conflictWhere)
2239
+ : undefined;
2240
+ const upsertKeyColumn = kind === "upsert" ? getUniqueKeyColumn(table) : undefined;
2241
+ if (upsertKeyColumn !== undefined) {
2242
+ upsertFirings = await this.#upsertTriggerFirings(table, upsertKeyColumn, input, rowCount, (sql, params) => this.#sessionQuery(transaction, sql, { params }), normalizedConflictWhere, normalizedConflictWhere !== undefined);
2243
+ }
2244
+ if (normalizedConflictWhere !== undefined) {
2245
+ if (upsertFirings === undefined) {
2246
+ throw new Error("Upsert conflict classification is missing");
2247
+ }
2248
+ const classifiedFirings = upsertFirings;
2249
+ acceptedRowIndexes = [...classifiedFirings.inserts, ...classifiedFirings.updates].sort((left, right) => left - right);
2250
+ skippedRowCount = classifiedFirings.skipped.length;
2251
+ input = selectBatchRows(input, acceptedRowIndexes);
2252
+ rowCount = acceptedRowIndexes.length;
2253
+ logicalBytes = estimateBatchBytes(input);
2254
+ resolvedKeys = batchKeys(table, input);
2255
+ counts = {
2256
+ inserted: classifiedFirings.inserts.length,
2257
+ updated: classifiedFirings.updates.length,
2258
+ };
2259
+ const acceptedPosition = new Map(acceptedRowIndexes.map((original, position) => [original, position]));
2260
+ upsertFirings = {
2261
+ inserts: classifiedFirings.inserts.map((index) => acceptedPosition.get(index) ?? -1),
2262
+ updates: classifiedFirings.updates.map((index) => acceptedPosition.get(index) ?? -1),
2263
+ skipped: [],
2264
+ oldImages: acceptedRowIndexes.map((index) => classifiedFirings.oldImages[index]),
2265
+ };
2266
+ if (rowCount === 0) {
2267
+ await transaction.abort();
2268
+ const version = transaction.snapshotVersion;
2269
+ if (version === null)
2270
+ throw new Error("An existing table has no catalog version");
2271
+ return {
2272
+ tableName: table.name,
2273
+ segmentId: null,
2274
+ requestedRowCount,
2275
+ rowCount: 0,
2276
+ insertedRowCount: 0,
2277
+ updatedRowCount: 0,
2278
+ skippedRowCount,
2279
+ acceptedRowIndexes,
2280
+ blockCount: 0,
2281
+ storedBytes: 0,
2282
+ version,
2283
+ metrics: createWriteMetrics({
2284
+ logicalBytes: 0,
2285
+ storedBytes: 0,
2286
+ encodeMs: 0,
2287
+ stageMs: 0,
2288
+ commitMs: 0,
2289
+ totalMs: performance.now() - started,
2290
+ retries: 0,
2291
+ rows: 0,
2292
+ }),
2293
+ };
2294
+ }
2295
+ }
2160
2296
  if (resolvedKeys !== undefined) {
2161
2297
  transaction.setUniqueKeyChanges({
2162
2298
  tableId: table.id,
@@ -2169,11 +2305,20 @@ export class MinnowDatabase {
2169
2305
  // generated values are patched also covers an auto-incremented referencing column, and
2170
2306
  // registering this batch's key membership first permits valid self-references.
2171
2307
  await assertForeignKeys();
2172
- counts =
2173
- kind === "insert" && resolvedKeys !== undefined
2174
- ? { inserted: resolvedKeys.size, updated: 0 }
2175
- : await this.#classifyKeys(table, transaction.snapshotVersion, kind, resolvedKeys);
2176
- const rowIds = reservedRowIds ?? (await this.store.reserveRowIds(table.id, rowCount));
2308
+ if (normalizedConflictWhere === undefined) {
2309
+ counts =
2310
+ kind === "insert" && resolvedKeys !== undefined
2311
+ ? { inserted: resolvedKeys.size, updated: 0 }
2312
+ : await this.#classifyKeys(table, transaction.snapshotVersion, kind, resolvedKeys);
2313
+ }
2314
+ if (counts === undefined)
2315
+ throw new Error("Write classification is missing");
2316
+ const reserved = reservedRowIds ?? (await this.store.reserveRowIds(table.id, rowCount));
2317
+ const rowIds = {
2318
+ start: reserved.start,
2319
+ endExclusive: reserved.start + BigInt(rowCount),
2320
+ };
2321
+ segmentId = this.#createId();
2177
2322
  // Insert batches maintain any active full-text index by tokenizing their own rows into a
2178
2323
  // commit delta. Other mutation kinds emit nothing on purpose: the store's stale-writer
2179
2324
  // rule then flips the index to "invalid" (keyed histories are unindexable) and the scan
@@ -2267,10 +2412,6 @@ export class MinnowDatabase {
2267
2412
  const insertValueAt = (source, column, rowIndex) => (source === "new" ? (input.columns[column]?.[rowIndex] ?? null) : null);
2268
2413
  // Upserts fire per-row: INSERT triggers for fresh keys, UPDATE triggers (with the
2269
2414
  // replaced row as OLD) for conflicting ones — ON CONFLICT DO UPDATE semantics.
2270
- const upsertKeyColumn = kind === "upsert" ? getUniqueKeyColumn(table) : undefined;
2271
- const upsertFirings = upsertKeyColumn === undefined
2272
- ? undefined
2273
- : await this.#upsertTriggerFirings(table, upsertKeyColumn, input, rowCount);
2274
2415
  stageSecondaryUniqueInsertChanges(transaction, table, input, kind === "upsert" ? upsertFirings?.oldImages : undefined);
2275
2416
  if (kind === "insert") {
2276
2417
  await this.#stageTriggerDerivedInserts(transaction, table, "insert", rowCount, insertValueAt, "before");
@@ -2304,12 +2445,15 @@ export class MinnowDatabase {
2304
2445
  return {
2305
2446
  tableName: table.name,
2306
2447
  segmentId,
2448
+ requestedRowCount,
2307
2449
  rowCount,
2308
2450
  blockCount,
2309
2451
  storedBytes,
2310
2452
  version: manifest.version,
2311
2453
  insertedRowCount: counts.inserted,
2312
2454
  updatedRowCount: counts.updated,
2455
+ skippedRowCount,
2456
+ acceptedRowIndexes,
2313
2457
  metrics: createWriteMetrics({
2314
2458
  logicalBytes,
2315
2459
  storedBytes,
@@ -2329,7 +2473,7 @@ export class MinnowDatabase {
2329
2473
  }
2330
2474
  const stagedDerivations = kind === "insert"
2331
2475
  ? (table.triggers ?? []).some((trigger) => trigger.event === "insert")
2332
- : upsertFirings !== undefined;
2476
+ : upsertFirings !== undefined || conflictWhere !== undefined;
2333
2477
  if (stagedDerivations)
2334
2478
  throw new StaleTriggerDerivationsError(error);
2335
2479
  retries += 1;
@@ -2383,6 +2527,7 @@ export class MinnowDatabase {
2383
2527
  });
2384
2528
  return externalizeQueryResult({
2385
2529
  columns: columns.map(({ name }) => name),
2530
+ columnDomains: columns.map(({ sqlDomain }) => sqlDomain ?? null),
2386
2531
  rows,
2387
2532
  }).rows;
2388
2533
  }
@@ -2457,12 +2602,14 @@ export class MinnowDatabase {
2457
2602
  }
2458
2603
  async #prepareCompiledPlan(plan, options = {}, probe) {
2459
2604
  options = this.#effectiveQueryOptions(options);
2605
+ throwIfAborted(options.signal);
2460
2606
  // The ORDER-BY-expression desugar's wrapper is projection-only: prepare the inner block
2461
2607
  // directly (no derived materialization) and project each result to the visible aliases,
2462
2608
  // so `.search()` costs the same whether or not the caller also selects the score.
2463
2609
  const wrapper = transparentProjectionSource(plan);
2464
2610
  if (wrapper !== undefined) {
2465
2611
  const prepared = await this.#prepareCompiledPlan(wrapper.inner, options, probe);
2612
+ throwIfAborted(options.signal);
2466
2613
  return {
2467
2614
  sql: prepared.sql,
2468
2615
  tables: prepared.tables,
@@ -2481,7 +2628,9 @@ export class MinnowDatabase {
2481
2628
  let resolvedPlan = plan;
2482
2629
  let ftsStats;
2483
2630
  let outputNeedsExternalization = true;
2631
+ let outputColumnDomains = [];
2484
2632
  const prepareAtSnapshot = async (snapshot, realTables, visibility) => {
2633
+ throwIfAborted(options.signal);
2485
2634
  const typedSchemas = new Map([...realTables.values()].map((table) => [
2486
2635
  table.name,
2487
2636
  table.columns.map(({ name, type, integer, sqlDomain }) => ({
@@ -2497,32 +2646,41 @@ export class MinnowDatabase {
2497
2646
  const expandedPlan = expandFtsColumns(plan, (tableName) => searchableFtsColumns(realTables.get(tableName)));
2498
2647
  const resolution = subqueryResolutionSteps(expandedPlan);
2499
2648
  for (const step of resolution.steps) {
2500
- 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);
2501
2652
  }
2502
2653
  resolvedPlan = resolution.plan;
2503
2654
  // Index-served BM25 statistics, computed against the same catalog snapshot the pruner
2504
2655
  // reads, so a pruned scoring scan always carries exact corpus numbers.
2505
2656
  ftsStats = await this.#ftsIndexStats(resolvedPlan, realTables, snapshot, visibility);
2657
+ throwIfAborted(options.signal);
2506
2658
  columnarTables = await this.#prepareBlockInputs(resolvedPlan, snapshot, visibility, memory, realTables, typedSchemas, undefined,
2507
2659
  // memoize: false means "compute this statement's results" — that covers the
2508
2660
  // columnar forms of derived and windowed sources too, not just the result memo.
2509
- 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);
2510
2663
  outputNeedsExternalization = queryResultNeedsExternalization(resolvedPlan, typedSchemas);
2664
+ outputColumnDomains = inferResultColumnDomains(resolvedPlan, typedSchemas);
2511
2665
  };
2512
2666
  if (options.version !== undefined) {
2513
2667
  // Explicit time travel keeps the per-call lease and version-anchored reads.
2514
2668
  const realTables = await this.#findRealBlockTables(plan);
2669
+ throwIfAborted(options.signal);
2515
2670
  await this.#withLeasedSnapshot(options.version, async (snapshot) => {
2516
2671
  const visibility = await this.#blockSegmentVisibility(realTables);
2672
+ throwIfAborted(options.signal);
2517
2673
  await prepareAtSnapshot(snapshot, realTables, visibility);
2518
2674
  });
2519
2675
  }
2520
2676
  else {
2521
2677
  await this.#withSharedCatalogSnapshot(collectRealTableNames(plan), prepareAtSnapshot, probe);
2522
2678
  }
2679
+ throwIfAborted(options.signal);
2523
2680
  return createPreparedColumnarQuery(chooseJoinOrder(resolvedPlan, columnarTables), columnarTables, memory, {
2524
2681
  ...(ftsStats === undefined ? {} : { ftsStats }),
2525
2682
  outputNeedsExternalization,
2683
+ outputColumnDomains,
2526
2684
  });
2527
2685
  }
2528
2686
  catch (error) {
@@ -2550,7 +2708,7 @@ export class MinnowDatabase {
2550
2708
  names.forEach((name, index) => {
2551
2709
  const table = state.tables[index];
2552
2710
  if (table === undefined)
2553
- throw new Error(`Table not found: ${name}`);
2711
+ throw new UnknownTableError(name);
2554
2712
  realTables.set(table.name, table);
2555
2713
  });
2556
2714
  const segmentsByTable = new Map();
@@ -2809,38 +2967,50 @@ export class MinnowDatabase {
2809
2967
  * materialize their referenced columns. Each block resolves its own inputs, so one real table
2810
2968
  * projected differently by two blocks never collides.
2811
2969
  */
2812
- 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);
2813
2972
  const inputs = new Map(extraInputs ?? []);
2814
2973
  const sources = [block.base, ...block.joins];
2815
2974
  for (const source of sources) {
2975
+ throwIfAborted(signal);
2816
2976
  if (inputs.has(source.table))
2817
2977
  continue;
2818
2978
  if (source.recursive !== undefined) {
2819
2979
  const { reference, base, step, all } = source.recursive;
2820
- 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);
2821
2981
  typedSchemas.set(reference, schema);
2822
2982
  const state = createRecursiveCteState(baseResult, all);
2823
2983
  while (state.frontier.length > 0) {
2984
+ throwIfAborted(signal);
2824
2985
  // Each iteration's inputs release with its own context, so only the accumulated rows
2825
2986
  // and the final columnar table hold memory across the fixpoint loop.
2826
2987
  const iterationMemory = memory.createChild();
2827
2988
  try {
2828
- const frontierTable = derivedColumnarTable(reference, { columns: [...state.columns], rows: state.frontier }, schema);
2829
- state.absorb(await this.#executeBlock(step, snapshot, visibility, iterationMemory, realTables, typedSchemas, new Map([[reference, frontierTable]]), cacheResults, allowSpill, forceSpill, spillPageRows));
2989
+ const frontierTable = derivedColumnarTable(reference, {
2990
+ columns: [...state.columns],
2991
+ columnDomains: schema.map(({ sqlDomain }) => sqlDomain ?? null),
2992
+ rows: state.frontier,
2993
+ }, schema);
2994
+ state.absorb(await this.#executeBlock(step, snapshot, visibility, iterationMemory, realTables, typedSchemas, new Map([[reference, frontierTable]]), cacheResults, allowSpill, forceSpill, spillPageRows, signal));
2830
2995
  }
2831
2996
  finally {
2832
2997
  iterationMemory.close();
2833
2998
  }
2834
2999
  }
2835
3000
  typedSchemas.set(source.table, schema);
2836
- inputs.set(source.table, derivedColumnarTable(source.table, { columns: [...state.columns], rows: state.rows }, schema));
3001
+ inputs.set(source.table, derivedColumnarTable(source.table, {
3002
+ columns: [...state.columns],
3003
+ columnDomains: schema.map(({ sqlDomain }) => sqlDomain ?? null),
3004
+ rows: state.rows,
3005
+ }, schema));
2837
3006
  continue;
2838
3007
  }
2839
3008
  if (source.union !== undefined) {
2840
3009
  const results = [];
2841
3010
  let schema;
2842
3011
  for (const member of source.union.blocks) {
2843
- 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);
2844
3014
  if (schema === undefined)
2845
3015
  schema = memberSchema;
2846
3016
  else {
@@ -2869,6 +3039,7 @@ export class MinnowDatabase {
2869
3039
  // key plus the window spec, so a warm repeat skips the window pass and the
2870
3040
  // rows-to-columnar conversion entirely.
2871
3041
  const innerKey = await this.#blockResultCacheKey(source.windowed.block, snapshot, visibility, realTables, cacheResults);
3042
+ throwIfAborted(signal);
2872
3043
  const columnarKey = innerKey === undefined
2873
3044
  ? undefined
2874
3045
  : `ctw|${JSON.stringify(source.windowed.windows)}|${innerKey}`;
@@ -2880,7 +3051,7 @@ export class MinnowDatabase {
2880
3051
  continue;
2881
3052
  }
2882
3053
  }
2883
- 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);
2884
3055
  const windowed = applyWindowFunctions(inner, source.windowed.windows, {
2885
3056
  copyRows: cacheResults,
2886
3057
  });
@@ -2907,6 +3078,7 @@ export class MinnowDatabase {
2907
3078
  if (derived === undefined)
2908
3079
  continue;
2909
3080
  const derivedKey = await this.#blockResultCacheKey(derived, snapshot, visibility, realTables, cacheResults);
3081
+ throwIfAborted(signal);
2910
3082
  const columnarKey = derivedKey === undefined ? undefined : `ctd|${derivedKey}`;
2911
3083
  if (columnarKey !== undefined) {
2912
3084
  const hit = this.#cacheGet(columnarKey);
@@ -2916,7 +3088,7 @@ export class MinnowDatabase {
2916
3088
  continue;
2917
3089
  }
2918
3090
  }
2919
- 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);
2920
3092
  typedSchemas.set(source.table, schema);
2921
3093
  const derivedTable = derivedColumnarTable(source.table, result, schema);
2922
3094
  if (columnarKey !== undefined) {
@@ -2931,6 +3103,7 @@ export class MinnowDatabase {
2931
3103
  const columns = referencedColumns(block, nameSchemas);
2932
3104
  const onlyRealSource = sources.length === 1 && sources[0]?.derived === undefined ? block : undefined;
2933
3105
  for (const source of sources) {
3106
+ throwIfAborted(signal);
2934
3107
  if (source.derived !== undefined || inputs.has(source.table))
2935
3108
  continue;
2936
3109
  if (source.table === DUAL_TABLE) {
@@ -2939,9 +3112,10 @@ export class MinnowDatabase {
2939
3112
  }
2940
3113
  const table = realTables.get(source.table);
2941
3114
  if (table === undefined)
2942
- throw new TypeError(`Unknown table: ${source.table}`);
3115
+ throw new UnknownTableError(source.table);
2943
3116
  const requestedColumns = columns.get(table.name) ?? [];
2944
3117
  inputs.set(table.name, await this.#materializeColumnarTableAtSnapshot(table, snapshot, requestedColumns.length === 0 ? [] : resolveReadColumns(table, requestedColumns), visibility, onlyRealSource));
3118
+ throwIfAborted(signal);
2945
3119
  }
2946
3120
  return inputs;
2947
3121
  }
@@ -2958,6 +3132,7 @@ export class MinnowDatabase {
2958
3132
  return this.#queryWithReadReservation(sql, options);
2959
3133
  }
2960
3134
  async #queryUnreserved(sql, options) {
3135
+ throwIfAborted(options.signal);
2961
3136
  const open = this.#openTransaction;
2962
3137
  if (open !== undefined) {
2963
3138
  // Read-your-writes: inside BEGIN … COMMIT a read sees the pre-scope snapshot plus what
@@ -2983,6 +3158,7 @@ export class MinnowDatabase {
2983
3158
  const result = !memoizable
2984
3159
  ? await this.#queryCompiled(plan, options)
2985
3160
  : await this.#memoizedQuery(plan, `res ${queryResultMemoKey(sql, options.params ?? [])}`, options, probe);
3161
+ throwIfAborted(options.signal);
2986
3162
  return externalizeQueryResult(result);
2987
3163
  }
2988
3164
  /**
@@ -3047,7 +3223,8 @@ export class MinnowDatabase {
3047
3223
  };
3048
3224
  }
3049
3225
  async #queryBatches(sql, options, cursor) {
3050
- options = this.#effectiveQueryOptions(options);
3226
+ options = this.#effectiveQueryOptions({ ...options, signal: cursor.signal });
3227
+ cursor.signal.throwIfAborted();
3051
3228
  // A transaction's staged overlay has its own executor. Keep its read-your-writes semantics
3052
3229
  // and page the materialized result; native scan batching is for durable snapshots.
3053
3230
  if (this.#openTransaction === undefined) {
@@ -3076,6 +3253,7 @@ export class MinnowDatabase {
3076
3253
  cursor.signal.throwIfAborted();
3077
3254
  await cursor.consume({
3078
3255
  columns: [...result.columns],
3256
+ columnDomains: [...result.columnDomains],
3079
3257
  rows: result.rows.slice(start, start + cursor.batchRows),
3080
3258
  });
3081
3259
  }
@@ -3087,16 +3265,22 @@ export class MinnowDatabase {
3087
3265
  * and on IndexedDB every probe is a read transaction, a floor under every small query.
3088
3266
  */
3089
3267
  async #memoizedQuery(plan, key, options, probe) {
3268
+ throwIfAborted(options.signal);
3090
3269
  const before = await probe();
3270
+ throwIfAborted(options.signal);
3091
3271
  const cached = this.#cacheGet(`${key}\u0001${String(before.catalogEpoch)}`);
3092
- if (cached !== undefined)
3272
+ if (cached !== undefined) {
3273
+ options.onStats?.({ peakMemoryBytes: 0 });
3093
3274
  return copyQueryResult(cached);
3275
+ }
3094
3276
  const result = await this.#queryCompiled(plan, options, before);
3277
+ throwIfAborted(options.signal);
3095
3278
  const bytes = queryResultRetainedBytes(result);
3096
3279
  if (bytes <= RESULT_MEMO_MAX_BYTES) {
3097
3280
  // Cache only when the epoch did not move during execution: the result is then exactly
3098
3281
  // that epoch's answer. A moved epoch simply skips the cache — never mis-files.
3099
3282
  const after = await probe();
3283
+ throwIfAborted(options.signal);
3100
3284
  if (after.catalogEpoch === before.catalogEpoch) {
3101
3285
  this.#cachePut(`${key}\u0001${String(before.catalogEpoch)}`, copyQueryResult(result), bytes);
3102
3286
  }
@@ -3276,10 +3460,13 @@ export class MinnowDatabase {
3276
3460
  */
3277
3461
  async #queryCompiled(plan, options = {}, probe) {
3278
3462
  options = this.#effectiveQueryOptions(options);
3463
+ throwIfAborted(options.signal);
3279
3464
  // One freshness probe per query: read here unless the caller already has one, and handed
3280
3465
  // to the view lookup and the catalog state below, which would otherwise probe again each.
3281
3466
  probe ??= await this.store.getCatalogProbe();
3467
+ throwIfAborted(options.signal);
3282
3468
  plan = await this.#applyCatalogRewrites(plan, probe);
3469
+ throwIfAborted(options.signal);
3283
3470
  const spillPageRows = options.spillPageRows === undefined
3284
3471
  ? undefined
3285
3472
  : positiveWholeNumber(options.spillPageRows, "Query spill page rows");
@@ -3300,6 +3487,7 @@ export class MinnowDatabase {
3300
3487
  }
3301
3488
  }
3302
3489
  const prepared = await this.#prepareCompiledPlan(plan, options, probe);
3490
+ throwIfAborted(options.signal);
3303
3491
  // Read the peak before close(): closing releases the context and zeroes what it tracked.
3304
3492
  const report = (result) => {
3305
3493
  options.onStats?.({ peakMemoryBytes: prepared.memoryUsage.peakBytes });
@@ -3307,11 +3495,16 @@ export class MinnowDatabase {
3307
3495
  };
3308
3496
  try {
3309
3497
  const spill = options.spillToStorage ?? options.executionMemoryBudgetBytes !== undefined;
3310
- if (!spill)
3311
- return report(prepared.execute());
3498
+ if (!spill) {
3499
+ const result = prepared.execute();
3500
+ throwIfAborted(options.signal);
3501
+ return report(result);
3502
+ }
3312
3503
  if (options.spillToStorage !== true) {
3313
3504
  try {
3314
- return report(prepared.execute());
3505
+ const result = prepared.execute();
3506
+ throwIfAborted(options.signal);
3507
+ return report(result);
3315
3508
  }
3316
3509
  catch (error) {
3317
3510
  if (!(error instanceof QueryMemoryBudgetError))
@@ -3321,12 +3514,33 @@ export class MinnowDatabase {
3321
3514
  return report(await prepared.executeAsync({
3322
3515
  ...(spillPageRows === undefined ? {} : { spillPageRows }),
3323
3516
  spillStore: this.#leasedSpillStore(),
3517
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
3324
3518
  }));
3325
3519
  }
3326
3520
  finally {
3327
3521
  prepared.close();
3328
3522
  }
3329
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
+ }
3330
3544
  /**
3331
3545
  * Backs the executor's spill pages with durable owner leases: each owner registers a lease
3332
3546
  * before its first page write and renews it while pages are read or written, so an abandoned
@@ -3714,15 +3928,18 @@ export class MinnowDatabase {
3714
3928
  * one, so a table that cannot be compacted today costs one attempt, not one per query.
3715
3929
  */
3716
3930
  #maybeScheduleAutoCompaction(table, segments) {
3931
+ this.#maybeScheduleAutoCompactionFromHint(table, autoCompactionHint(null, segments));
3932
+ }
3933
+ #maybeScheduleAutoCompactionFromHint(table, hint) {
3717
3934
  if (this.#closed)
3718
3935
  return;
3719
3936
  if (!this.#autoCompact)
3720
3937
  return;
3721
3938
  if (this.#droppingTables.has(table.id))
3722
3939
  return;
3723
- if (!autoCompactionDue(segments))
3940
+ if (!autoCompactionDueHint(hint))
3724
3941
  return;
3725
- if (segments.length < (this.#autoCompactionBackoff.get(table.id) ?? 0))
3942
+ if (hint.visible < (this.#autoCompactionBackoff.get(table.id) ?? 0))
3726
3943
  return;
3727
3944
  if (this.#autoCompactionsInFlight.has(table.id)) {
3728
3945
  // A final burst can cross the threshold while the prior fold is still planning or
@@ -3736,14 +3953,14 @@ export class MinnowDatabase {
3736
3953
  if (folded)
3737
3954
  this.#autoCompactionBackoff.delete(table.id);
3738
3955
  else {
3739
- 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)));
3740
3957
  }
3741
3958
  })
3742
3959
  .catch(() => {
3743
3960
  // Back off deterministic failures, but never beyond the maximum L0 prefix a fold can
3744
3961
  // consume. A transient conflict near the end of a burst must not strand hundreds of
3745
3962
  // segments waiting for a segment count the idle database can never reach.
3746
- 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)));
3747
3964
  })
3748
3965
  .finally(() => {
3749
3966
  if (this.#autoCompactionsInFlight.get(table.id) === run) {
@@ -3763,6 +3980,10 @@ export class MinnowDatabase {
3763
3980
  // every write and let the commit return the identical CompactionBacklogError at the CAS.
3764
3981
  if (!this.#autoCompact)
3765
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;
3766
3987
  let segments = await this.#currentVisibleSegments(table);
3767
3988
  let levelZero = 0;
3768
3989
  for (const segment of segments) {
@@ -3836,9 +4057,11 @@ export class MinnowDatabase {
3836
4057
  const version = await this.store.getCurrentManifestVersion();
3837
4058
  segments = await this.#visibleSegmentRecords(table, new Snapshot(this.store, version));
3838
4059
  if ((await this.store.getCurrentManifestVersion()) === version) {
4060
+ this.#autoCompactionHints.set(table.id, autoCompactionHint(version, segments));
3839
4061
  return segments;
3840
4062
  }
3841
4063
  }
4064
+ this.#autoCompactionHints.delete(table.id);
3842
4065
  return segments;
3843
4066
  }
3844
4067
  /**
@@ -3849,6 +4072,7 @@ export class MinnowDatabase {
3849
4072
  if (this.#closed)
3850
4073
  return;
3851
4074
  const contribution = transaction?.committedCatalogContribution;
4075
+ this.#advanceAutoCompactionHints(manifest, contribution?.segments);
3852
4076
  const initial = contribution?.initialCatalogProbe;
3853
4077
  const contributedTableIds = new Set(contribution?.segments.map((segment) => segment.tableId) ?? []);
3854
4078
  if (contribution !== undefined &&
@@ -3942,7 +4166,7 @@ export class MinnowDatabase {
3942
4166
  this.#armIdleCompactionCheck();
3943
4167
  }
3944
4168
  /** Counts one physical rewrite version once in this engine, including lost-ack recovery. */
3945
- #afterCompactionCommit(manifest) {
4169
+ #afterCompactionCommit(manifest, tableId) {
3946
4170
  if (this.#accountedCompactionVersions.has(manifest.version))
3947
4171
  return;
3948
4172
  this.#accountedCompactionVersions.add(manifest.version);
@@ -3951,8 +4175,49 @@ export class MinnowDatabase {
3951
4175
  if (oldest !== undefined)
3952
4176
  this.#accountedCompactionVersions.delete(oldest);
3953
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);
3954
4182
  this.#afterCommit(manifest);
3955
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
+ }
3956
4221
  /**
3957
4222
  * Debounces the final write-path check for a burst. Sampling every few commits keeps the hot
3958
4223
  * path cheap, but the last one through seven commits can be the ones that cross a fold
@@ -3990,6 +4255,12 @@ export class MinnowDatabase {
3990
4255
  const table = await this.store.getTable(tableId);
3991
4256
  if (table === undefined)
3992
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
+ }
3993
4264
  this.#maybeScheduleAutoCompaction(table, await this.#currentVisibleSegments(table));
3994
4265
  }
3995
4266
  catch {
@@ -4013,7 +4284,7 @@ export class MinnowDatabase {
4013
4284
  const tables = proof.records;
4014
4285
  const table = tables.find((record) => record.name === tableName);
4015
4286
  if (table === undefined)
4016
- throw new TypeError(`Table not found: ${tableName}`);
4287
+ throw new UnknownTableError(tableName);
4017
4288
  if (table.view !== undefined)
4018
4289
  throw new TypeError(`Cannot create a trigger on view ${tableName}`);
4019
4290
  pinnedTableId ??= table.id;
@@ -4046,7 +4317,7 @@ export class MinnowDatabase {
4046
4317
  }
4047
4318
  const target = tables.find((record) => record.name === compiled.table);
4048
4319
  if (target === undefined)
4049
- throw new TypeError(`Table not found: ${compiled.table}`);
4320
+ throw new UnknownTableError(compiled.table);
4050
4321
  if (target.view !== undefined) {
4051
4322
  throw new TypeError(`Trigger bodies cannot write to views: ${compiled.table}`);
4052
4323
  }
@@ -4124,19 +4395,32 @@ export class MinnowDatabase {
4124
4395
  * in-batch duplicate key fires as an UPDATE of the earlier occurrence. Returns undefined
4125
4396
  * when the table has no insert or update triggers.
4126
4397
  */
4127
- async #upsertTriggerFirings(table, keyColumn, batch, rowCount, readRows) {
4398
+ async #upsertTriggerFirings(table, keyColumn, batch, rowCount, readRows, conflictWhere, force = false) {
4128
4399
  const fires = (table.triggers ?? []).some((trigger) => trigger.event === "insert" || trigger.event === "update");
4129
- if (!fires || rowCount === 0)
4400
+ if ((!fires && !force) || rowCount === 0)
4130
4401
  return undefined;
4131
4402
  const quote = (name) => `"${name.replaceAll('"', '""')}"`;
4132
4403
  const keyValues = batch.columns[keyColumn.name] ?? [];
4133
- const params = keyValues.filter((value) => value !== null);
4134
- const placeholders = params.map(() => "?").join(", ");
4135
- const sql = `SELECT * FROM ${quote(table.name)} WHERE ${quote(keyColumn.name)} IN (${placeholders})`;
4136
- const result = readRows === undefined
4137
- ? await this.query(sql, { params, memoize: false })
4138
- : await readRows(sql, params);
4139
- const byToken = new Map(result.rows.map((row) => {
4404
+ const distinct = new Map();
4405
+ for (const value of keyValues) {
4406
+ if (value !== null)
4407
+ distinct.set(keyToken(keyColumn.type, value), value);
4408
+ }
4409
+ const params = [...distinct.values()];
4410
+ const rows = [];
4411
+ // Each value also contributes a comma token, so the parser's token cap is reached before
4412
+ // its parameter cap. A fixed 1,024-key window stays comfortably inside both limits.
4413
+ const keyWindowSize = Math.min(MAX_SQL_PARAMETERS, 1_024);
4414
+ for (let start = 0; start < params.length; start += keyWindowSize) {
4415
+ const window = params.slice(start, start + keyWindowSize);
4416
+ const placeholders = window.map(() => "?").join(", ");
4417
+ const sql = `SELECT * FROM ${quote(table.name)} WHERE ${quote(keyColumn.name)} IN (${placeholders})`;
4418
+ const result = readRows === undefined
4419
+ ? await this.query(sql, { params: window, memoize: false })
4420
+ : await readRows(sql, window);
4421
+ rows.push(...result.rows);
4422
+ }
4423
+ const byToken = new Map(rows.map((row) => {
4140
4424
  const key = row[keyColumn.name];
4141
4425
  return [
4142
4426
  key === null || key === undefined ? "" : keyToken(keyColumn.type, key),
@@ -4145,29 +4429,36 @@ export class MinnowDatabase {
4145
4429
  }));
4146
4430
  const inserts = [];
4147
4431
  const updates = [];
4432
+ const skipped = [];
4148
4433
  const oldImages = [];
4149
- const seenInBatch = new Map();
4434
+ const currentByToken = new Map(byToken);
4150
4435
  for (let rowIndex = 0; rowIndex < rowCount; rowIndex += 1) {
4151
4436
  const key = keyValues[rowIndex] ?? null;
4152
4437
  const token = key === null ? "" : keyToken(keyColumn.type, key);
4153
- const earlier = seenInBatch.get(token);
4154
- const old = earlier === undefined
4155
- ? byToken.get(token)
4156
- : Object.fromEntries(table.columns.map((column) => [
4157
- column.name,
4158
- batch.columns[column.name]?.[earlier] ?? null,
4159
- ]));
4438
+ const old = currentByToken.get(token);
4160
4439
  if (old === undefined) {
4161
4440
  inserts.push(rowIndex);
4162
4441
  oldImages.push(undefined);
4163
4442
  }
4164
4443
  else {
4165
- updates.push(rowIndex);
4166
4444
  oldImages.push(old);
4445
+ const rawLeft = conflictWhere === undefined ? null : (old[conflictWhere.column.name] ?? null);
4446
+ const left = conflictWhere?.column.sqlDomain === undefined
4447
+ ? rawLeft
4448
+ : normalizeSqlDomainValue(conflictWhere.column.sqlDomain, rawLeft);
4449
+ if (conflictWhere !== undefined &&
4450
+ !comparisonHolds(conflictWhere.operator, left, conflictWhere.value)) {
4451
+ skipped.push(rowIndex);
4452
+ continue;
4453
+ }
4454
+ updates.push(rowIndex);
4167
4455
  }
4168
- seenInBatch.set(token, rowIndex);
4456
+ currentByToken.set(token, Object.fromEntries(table.columns.map((column) => [
4457
+ column.name,
4458
+ batch.columns[column.name]?.[rowIndex] ?? null,
4459
+ ])));
4169
4460
  }
4170
- return { inserts, updates, oldImages };
4461
+ return { inserts, updates, skipped, oldImages };
4171
4462
  }
4172
4463
  /** Fires INSERT and UPDATE triggers for one upsert batch at one timing, per the plan. */
4173
4464
  async #stageUpsertTriggerFirings(transaction, table, batch, firings, timing, cascadeBudget = 1) {
@@ -4250,6 +4541,7 @@ export class MinnowDatabase {
4250
4541
  const statementNow = this.#now();
4251
4542
  const filled = await fillColumnDefaults(target, input, (sql) => this.#evaluateDefaultExpression(sql, statementNow), derivedRows.length);
4252
4543
  normalizeDomainBatch(target, filled.batch);
4544
+ fillStoredGeneratedColumns(target, filled.batch, derivedRows.length, filled.generated);
4253
4545
  fillCompositePrimaryKey(target, filled.batch, derivedRows.length);
4254
4546
  validateBatch(target, filled.batch, filled.autoIncrement?.column.name);
4255
4547
  if (filled.autoIncrement !== undefined && filled.autoIncrement.missingIndexes.length > 0) {
@@ -4522,6 +4814,15 @@ export class MinnowDatabase {
4522
4814
  ? this.#queryCompiled(plan, {}, probe)
4523
4815
  : this.#sessionQueryPlan(transaction, await this.#applyCatalogRewrites(plan));
4524
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
+ }),
4525
4826
  executeStatement: (statement) => {
4526
4827
  open();
4527
4828
  return guarded(() => this.runStatement(statement, { writer }));
@@ -4590,21 +4891,26 @@ export class MinnowDatabase {
4590
4891
  */
4591
4892
  async #sessionQuery(transaction, sql, options = {}) {
4592
4893
  options = this.#effectiveQueryOptions(options);
4894
+ throwIfAborted(options.signal);
4593
4895
  const bound = bindPlanParameters(this.#compileCached(sql), options.params);
4594
4896
  // The same catalog rewrites a read outside a scope gets: a scope that could not see a view
4595
4897
  // would make the scope's reads a different language from everyone else's.
4596
4898
  const plan = await this.#applyCatalogRewrites(bound);
4899
+ throwIfAborted(options.signal);
4597
4900
  return this.#sessionQueryPlan(transaction, plan, options);
4598
4901
  }
4599
4902
  /** #sessionQuery for an already-bound plan: trigger bodies resolve their reads through it. */
4600
4903
  async #sessionQueryPlan(transaction, plan, options = {}) {
4601
4904
  options = this.#effectiveQueryOptions(options);
4905
+ throwIfAborted(options.signal);
4602
4906
  const names = collectRealTableNames(plan);
4603
4907
  const tables = await Promise.all(names.map((name) => this.#findTable(name)));
4908
+ throwIfAborted(options.signal);
4604
4909
  const realTables = new Map(tables.map((table) => [table.name, table]));
4605
4910
  const pendingIds = new Set(transaction.pendingSegmentIds);
4606
4911
  const pendingBlocks = new Set(transaction.pendingBlockIds);
4607
4912
  const ourRecord = await this.store.getTransaction(transaction.id);
4913
+ throwIfAborted(options.signal);
4608
4914
  // Pinned to the scope's own snapshot, not the current manifest: the documented contract
4609
4915
  // is "the pre-scope snapshot plus everything this scope staged", and #assertKeysExist
4610
4916
  // already validates against transaction.snapshotVersion — reading the current version
@@ -4628,6 +4934,7 @@ export class MinnowDatabase {
4628
4934
  getBlocks: async (blockIds) => {
4629
4935
  const blocks = [];
4630
4936
  for (let start = 0; start < blockIds.length; start += 64) {
4937
+ throwIfAborted(options.signal);
4631
4938
  const window = blockIds.slice(start, start + 64);
4632
4939
  const committedIds = [];
4633
4940
  const committedIndexes = [];
@@ -4652,6 +4959,7 @@ export class MinnowDatabase {
4652
4959
  this.#getBlocksWindowed(committedIds),
4653
4960
  Promise.all(stagedIds.map((blockId) => transaction.getBlock(blockId))),
4654
4961
  ]);
4962
+ throwIfAborted(options.signal);
4655
4963
  const ordered = new Array(window.length);
4656
4964
  committedIndexes.forEach((index, offset) => {
4657
4965
  ordered[index] = committed[offset];
@@ -4670,10 +4978,12 @@ export class MinnowDatabase {
4670
4978
  const transactionRecords = new Map();
4671
4979
  const deferredSegments = transaction.deferredSegments;
4672
4980
  for (const table of tables) {
4981
+ throwIfAborted(options.signal);
4673
4982
  const segments = [
4674
4983
  ...(await listTableSegmentsPaged(this.store, table.id)),
4675
4984
  ...deferredSegments.filter((segment) => segment.tableId === table.id),
4676
4985
  ];
4986
+ throwIfAborted(options.signal);
4677
4987
  // Staged segments sort after every committed one; commitOrdinal keeps their
4678
4988
  // staging order (the journal's id list is sorted, so it cannot).
4679
4989
  const doctored = segments.map((segment) => pendingIds.has(segment.id)
@@ -4683,6 +4993,7 @@ export class MinnowDatabase {
4683
4993
  for (const record of await this.#transactionRecordsForSegments(doctored)) {
4684
4994
  transactionRecords.set(record.id, record);
4685
4995
  }
4996
+ throwIfAborted(options.signal);
4686
4997
  }
4687
4998
  if (ourRecord !== undefined) {
4688
4999
  transactionRecords.set(transaction.id, {
@@ -4702,6 +5013,7 @@ export class MinnowDatabase {
4702
5013
  /** Executes one plan at an explicit snapshot and visibility, covering every plan shape. */
4703
5014
  async #queryAtVisibility(plan, snapshot, visibility, realTables, cacheResults = true, options = {}) {
4704
5015
  options = this.#effectiveQueryOptions(options);
5016
+ throwIfAborted(options.signal);
4705
5017
  plan = expandFtsColumns(plan, (name) => searchableFtsColumns(realTables.get(name)));
4706
5018
  const typedSchemas = new Map([...realTables.values()].map((table) => [
4707
5019
  table.name,
@@ -4718,16 +5030,22 @@ export class MinnowDatabase {
4718
5030
  const spillPageRows = options.spillPageRows === undefined
4719
5031
  ? undefined
4720
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
+ };
4721
5038
  try {
4722
5039
  const resolution = subqueryResolutionSteps(plan);
4723
5040
  for (const step of resolution.steps) {
4724
- 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));
4725
5043
  }
4726
5044
  const wrapper = transparentProjectionSource(resolution.plan);
4727
5045
  if (wrapper !== undefined) {
4728
- 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));
4729
5047
  }
4730
- 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));
4731
5049
  }
4732
5050
  finally {
4733
5051
  memory.close();
@@ -4812,6 +5130,7 @@ export class MinnowDatabase {
4812
5130
  throw new TypeError(`Table needs a unique key before rows can be updated: ${table.name}`);
4813
5131
  }
4814
5132
  input = normalizeDomainUpdate(table, input);
5133
+ rejectGeneratedUpdateAssignments(table, input);
4815
5134
  const keys = validateUpdateBatch(table, keyColumn, input);
4816
5135
  // Read-your-writes membership: keys staged by this scope pass, keys the scope removed
4817
5136
  // fail, and everything else checks against the committed snapshot as usual.
@@ -4826,11 +5145,17 @@ export class MinnowDatabase {
4826
5145
  await this.#assertKeysExist(table, keyColumn, transaction.snapshotVersion, committedKeys);
4827
5146
  }
4828
5147
  const sessionChecks = table.checks ?? [];
4829
- const changedForeignKey = (table.foreignKeys ?? []).some((key) => foreignKeyColumns(key).some((column) => input.changes[column] !== undefined));
5148
+ let changedForeignKey = (table.foreignKeys ?? []).some((key) => key.enforced !== false &&
5149
+ foreignKeyColumns(key).some((column) => input.changes[column] !== undefined));
4830
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 ||
4831
5151
  changedForeignKey ||
5152
+ tableGeneratedExpressions(table).length > 0 ||
4832
5153
  secondaryIndexUpdateNeedsPreImages(table, input) ||
4833
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));
4834
5159
  stageSecondaryUniqueMutationChanges(transaction, table, input, preImages);
4835
5160
  const secondaryDeltas = buildSecondaryUpdateDeltas(table, input, preImages);
4836
5161
  if (secondaryDeltas.length > 0) {
@@ -4912,7 +5237,19 @@ export class MinnowDatabase {
4912
5237
  },
4913
5238
  ]);
4914
5239
  await this.#stageTriggerDerivedInserts(transaction, table, "update", input.keys.length, sessionUpdateValueAt, "after", cascadeBudget);
4915
- 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
+ };
4916
5253
  }
4917
5254
  async #sessionDelete(transaction, tableName, input, cascadeBudget = 1, referentialBudget = REFERENTIAL_CASCADES) {
4918
5255
  const table = await this.#findTable(tableName);
@@ -5249,6 +5586,9 @@ export class MinnowDatabase {
5249
5586
  ...(columnDefinition.defaultSpec === undefined
5250
5587
  ? {}
5251
5588
  : { defaultValue: columnDefinition.defaultSpec }),
5589
+ ...(columnDefinition.generatedSpec === undefined
5590
+ ? {}
5591
+ : { generatedValue: columnDefinition.generatedSpec }),
5252
5592
  ...(columnDefinition.enumValues === undefined
5253
5593
  ? {}
5254
5594
  : { enumValues: columnDefinition.enumValues }),
@@ -5386,10 +5726,37 @@ export class MinnowDatabase {
5386
5726
  }
5387
5727
  }
5388
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
+ }
5389
5751
  // The remaining metadata-only steps share one catalog/manifest CAS. Physical drops ran
5390
5752
  // above because segment rewriting and payload retirement have a stronger atomic boundary.
5391
5753
  const columns = applyColumnSteps(record, steps, this.#createId);
5392
- if (JSON.stringify(columns) === JSON.stringify(record.columns)) {
5754
+ const foreignKeyStep = steps.find((step) => step.kind === "alter-foreign-keys");
5755
+ const foreignKeys = foreignKeyStep?.kind === "alter-foreign-keys"
5756
+ ? foreignKeyStep.foreignKeys.map((key) => ({ ...key }))
5757
+ : record.foreignKeys;
5758
+ if (JSON.stringify(columns) === JSON.stringify(record.columns) &&
5759
+ JSON.stringify(foreignKeys ?? []) === JSON.stringify(record.foreignKeys ?? [])) {
5393
5760
  if (changed)
5394
5761
  alteredTables.push(tableName);
5395
5762
  continue;
@@ -5400,6 +5767,7 @@ export class MinnowDatabase {
5400
5767
  assertDependentViewsKeepSchema(proof.records, record, candidateSchemas);
5401
5768
  const updated = await this.store.updateTable(record.id, record.revision, {
5402
5769
  columns,
5770
+ ...(foreignKeyStep === undefined ? {} : { foreignKeys }),
5403
5771
  ...(autoIncrementSeed === undefined ? {} : { autoIncrementSeed }),
5404
5772
  expectedManifestVersion: { value: proof.manifestVersion },
5405
5773
  expectedCatalogEpoch: proof.catalogEpoch,
@@ -5533,6 +5901,8 @@ export class MinnowDatabase {
5533
5901
  */
5534
5902
  async #assertForeignKeysPresent(table, rowsByColumn, read, transaction) {
5535
5903
  for (const key of table.foreignKeys ?? []) {
5904
+ if (key.enforced === false)
5905
+ continue;
5536
5906
  const childNames = foreignKeyColumns(key);
5537
5907
  const parentNames = foreignKeyParentColumns(key);
5538
5908
  if (childNames.length > 1) {
@@ -5671,7 +6041,7 @@ export class MinnowDatabase {
5671
6041
  async #applyReferentialActions(parent, keys, session, cascadeBudget) {
5672
6042
  if (keys.length === 0)
5673
6043
  return;
5674
- const children = await this.#childForeignKeys(parent.name);
6044
+ const children = (await this.#childForeignKeys(parent.name)).filter(({ key }) => key.enforced !== false);
5675
6045
  if (children.length === 0)
5676
6046
  return;
5677
6047
  if (cascadeBudget < 0) {
@@ -5929,6 +6299,7 @@ export class MinnowDatabase {
5929
6299
  const statementNow = this.#now();
5930
6300
  const filledProposed = await fillColumnDefaults(table, proposedBatch, (sql) => this.#evaluateDefaultExpression(sql, statementNow), statement.rows.length);
5931
6301
  normalizeDomainBatch(table, filledProposed.batch);
6302
+ fillStoredGeneratedColumns(table, filledProposed.batch, statement.rows.length, filledProposed.generated);
5932
6303
  fillCompositePrimaryKey(table, filledProposed.batch, statement.rows.length);
5933
6304
  const proposed = filledProposed.batch;
5934
6305
  // UPSERT only handles a uniqueness conflict. Other INSERT-domain failures are still errors,
@@ -5999,7 +6370,10 @@ export class MinnowDatabase {
5999
6370
  // and UUID defaults twice and could even change which row conflicts.
6000
6371
  const columns = visibleTableColumns(table).map(({ name }) => name);
6001
6372
  const deferredAutoIncrement = new Set(filledProposed.autoIncrement?.missingIndexes ?? []);
6002
- 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))
6003
6377
  ? { default: true }
6004
6378
  : (proposed.columns[name]?.[rowIndex] ?? null)));
6005
6379
  return this.runStatement({
@@ -6055,7 +6429,12 @@ export class MinnowDatabase {
6055
6429
  return value;
6056
6430
  });
6057
6431
  const selected = await transaction.query(`SELECT * FROM ${quoteSqlIdentifier(table.name)} WHERE ${quoteSqlIdentifier(keyColumn.name)} IN (${keys.map(() => "?").join(", ")})`, { params: keys });
6058
- const currentByKey = new Map(selected.rows.map((row) => [keyToken(row[keyColumn.name] ?? null), row]));
6432
+ const currentByKey = new Map(selected.rows.map((row) => {
6433
+ const value = keyColumn.hidden
6434
+ ? compositeKeyValue(primaryKeyColumns(table), primaryKeyColumns(table).map(({ name }) => row[name] ?? null))
6435
+ : (row[keyColumn.name] ?? null);
6436
+ return [keyToken(value), row];
6437
+ }));
6059
6438
  const updateKeys = [];
6060
6439
  const changes = Object.fromEntries(assignments.map(({ column }) => [column, []]));
6061
6440
  for (const [position, rowIndex] of conflictingRows.entries()) {
@@ -6088,6 +6467,8 @@ export class MinnowDatabase {
6088
6467
  if (freshRows.length > 0) {
6089
6468
  const columns = {};
6090
6469
  for (const column of table.columns) {
6470
+ if (column.generatedValue !== undefined)
6471
+ continue;
6091
6472
  columns[column.name] = freshRows.map((rowIndex) => proposed.columns[column.name]?.[rowIndex] ?? null);
6092
6473
  }
6093
6474
  await transaction.insertBatch(table.name, { columns });
@@ -6126,7 +6507,9 @@ export class MinnowDatabase {
6126
6507
  table: statement.table,
6127
6508
  rowCount: outcome.affectedRows.length,
6128
6509
  ...(version === null || version === undefined ? {} : { version }),
6129
- ...(outcome.returnedRows === undefined ? {} : { returnedRows: outcome.returnedRows }),
6510
+ ...(outcome.returnedRows === undefined || returningColumns === undefined
6511
+ ? {}
6512
+ : returningExecuteFields(table, returningColumns, outcome.returnedRows)),
6130
6513
  };
6131
6514
  }
6132
6515
  /** ON CONFLICT DO NOTHING: drops insert rows whose unique key already exists at a snapshot. */
@@ -6149,10 +6532,13 @@ export class MinnowDatabase {
6149
6532
  const statementNow = this.#now();
6150
6533
  const filled = await fillColumnDefaults(table, proposed, (sql) => this.#evaluateDefaultExpression(sql, statementNow), statement.rows.length);
6151
6534
  normalizeDomainBatch(table, filled.batch);
6535
+ fillStoredGeneratedColumns(table, filled.batch, statement.rows.length, filled.generated);
6152
6536
  fillCompositePrimaryKey(table, filled.batch, statement.rows.length);
6153
6537
  const columns = visibleTableColumns(table).map(({ name }) => name);
6154
6538
  const deferredAutoIncrement = new Set(filled.autoIncrement?.missingIndexes ?? []);
6155
- 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))
6156
6542
  ? { default: true }
6157
6543
  : (filled.batch.columns[name]?.[rowIndex] ?? null)));
6158
6544
  const keys = filled.batch.columns[keyColumn.name] ?? [];
@@ -6457,7 +6843,11 @@ export class MinnowDatabase {
6457
6843
  const stageRows = async (rows) => {
6458
6844
  if (rows.length === 0)
6459
6845
  return;
6460
- const batchResult = { columns: tableColumns.map(({ name }) => name), rows: [...rows] };
6846
+ const batchResult = {
6847
+ columns: tableColumns.map(({ name }) => name),
6848
+ columnDomains: tableColumns.map(({ sqlDomain }) => sqlDomain ?? null),
6849
+ rows: [...rows],
6850
+ };
6461
6851
  if (queryResultRetainedBytes(batchResult) > executionMemoryBudgetBytes) {
6462
6852
  throw new QueryMemoryBudgetError("CREATE TABLE AS result batch", queryResultRetainedBytes(batchResult), 0, executionMemoryBudgetBytes);
6463
6853
  }
@@ -6592,7 +6982,7 @@ export class MinnowDatabase {
6592
6982
  }
6593
6983
  const record = proof.records.find((candidate) => candidate.name === addColumnStatement.table);
6594
6984
  if (record === undefined)
6595
- throw new TypeError(`Unknown table: ${addColumnStatement.table}`);
6985
+ throw new UnknownTableError(addColumnStatement.table);
6596
6986
  if (record.view !== undefined)
6597
6987
  throw new TypeError(`${addColumnStatement.table} is a view, not a table`);
6598
6988
  pinnedTableId ??= record.id;
@@ -6607,6 +6997,9 @@ export class MinnowDatabase {
6607
6997
  if (record.columns.some(({ name }) => name === added.name)) {
6608
6998
  throw new TypeError(`Column already exists: ${addColumnStatement.table}.${added.name}`);
6609
6999
  }
7000
+ if (added.generatedValue !== undefined) {
7001
+ throw new TypeError("ALTER TABLE cannot add a generated column without rewriting existing rows");
7002
+ }
6610
7003
  if (added.defaultValue !== undefined) {
6611
7004
  validateColumnDefault({
6612
7005
  name: added.name,
@@ -6765,11 +7158,19 @@ export class MinnowDatabase {
6765
7158
  return this.#mergeConflictingInsertRows(statement, options);
6766
7159
  }
6767
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];
6768
7167
  return {
6769
7168
  kind: "insert",
6770
7169
  table: statement.table,
6771
7170
  rowCount: 0,
6772
- ...(options.returning === undefined ? {} : { returnedRows: [] }),
7171
+ ...(returningColumns === undefined
7172
+ ? {}
7173
+ : returningExecuteFields(table, returningColumns, [])),
6773
7174
  };
6774
7175
  }
6775
7176
  if (statement.kind === "insert") {
@@ -6831,7 +7232,7 @@ export class MinnowDatabase {
6831
7232
  ...(returningColumns === undefined
6832
7233
  ? {}
6833
7234
  : {
6834
- 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) => {
6835
7236
  const column = table.columns.find((candidate) => candidate.name === name);
6836
7237
  const position = statement.columns.indexOf(name);
6837
7238
  const inserted = position < 0 ? undefined : row[position];
@@ -6841,7 +7242,7 @@ export class MinnowDatabase {
6841
7242
  ? null
6842
7243
  : boundInsertValue(inserted));
6843
7244
  return [name, executionSqlValueFromInput(column, value)];
6844
- }))),
7245
+ })))),
6845
7246
  }),
6846
7247
  };
6847
7248
  }
@@ -6903,6 +7304,32 @@ export class MinnowDatabase {
6903
7304
  having: [],
6904
7305
  orderBy: [],
6905
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
+ }
6906
7333
  let rows;
6907
7334
  if (options.writer !== undefined) {
6908
7335
  // Inside a scope the rows to touch are the ones the scope can see, which includes what it
@@ -6930,7 +7357,9 @@ export class MinnowDatabase {
6930
7357
  kind: "delete",
6931
7358
  table: table.name,
6932
7359
  rowCount: 0,
6933
- ...(returnedRows === undefined ? {} : { returnedRows }),
7360
+ ...(returnedRows === undefined || returningColumns === undefined
7361
+ ? {}
7362
+ : returningExecuteFields(table, returningColumns, returnedRows)),
6934
7363
  };
6935
7364
  }
6936
7365
  const deleted = options.writer === undefined
@@ -6943,7 +7372,9 @@ export class MinnowDatabase {
6943
7372
  ...(deleted.version === undefined || deleted.version === null
6944
7373
  ? {}
6945
7374
  : { version: deleted.version }),
6946
- ...(returnedRows === undefined ? {} : { returnedRows }),
7375
+ ...(returnedRows === undefined || returningColumns === undefined
7376
+ ? {}
7377
+ : returningExecuteFields(table, returningColumns, returnedRows)),
6947
7378
  };
6948
7379
  }
6949
7380
  if (keys.length === 0) {
@@ -6951,7 +7382,9 @@ export class MinnowDatabase {
6951
7382
  kind: "update",
6952
7383
  table: table.name,
6953
7384
  rowCount: 0,
6954
- ...(returningColumns === undefined ? {} : { returnedRows: [] }),
7385
+ ...(returningColumns === undefined
7386
+ ? {}
7387
+ : returningExecuteFields(table, returningColumns, [])),
6955
7388
  };
6956
7389
  }
6957
7390
  const changes = {};
@@ -6984,7 +7417,9 @@ export class MinnowDatabase {
6984
7417
  name,
6985
7418
  returnedChanges !== undefined && name in returnedChanges
6986
7419
  ? (returnedChanges[name]?.[index] ?? null)
6987
- : (row[name] ?? null),
7420
+ : updated.generatedColumns?.[name] !== undefined
7421
+ ? (updated.generatedColumns[name][index] ?? null)
7422
+ : (row[name] ?? null),
6988
7423
  ])));
6989
7424
  return {
6990
7425
  kind: "update",
@@ -6993,7 +7428,9 @@ export class MinnowDatabase {
6993
7428
  ...(updated.version === undefined || updated.version === null
6994
7429
  ? {}
6995
7430
  : { version: updated.version }),
6996
- ...(returnedRows === undefined ? {} : { returnedRows }),
7431
+ ...(returnedRows === undefined || returningColumns === undefined
7432
+ ? {}
7433
+ : returningExecuteFields(table, returningColumns, returnedRows)),
6997
7434
  };
6998
7435
  }
6999
7436
  /**
@@ -7028,6 +7465,7 @@ export class MinnowDatabase {
7028
7465
  */
7029
7466
  async #queryStreamed(plan, options, spillPageRows, probe, cursor) {
7030
7467
  options = this.#effectiveQueryOptions(options);
7468
+ throwIfAborted(options.signal);
7031
7469
  const tableNames = [plan.base.table, ...plan.joins.map((join) => join.table)];
7032
7470
  const uniqueTableNames = [...new Set(tableNames)];
7033
7471
  if (options.version === undefined) {
@@ -7057,6 +7495,7 @@ export class MinnowDatabase {
7057
7495
  });
7058
7496
  }
7059
7497
  async #queryStreamedAtSnapshot(plan, options, spillPageRows, snapshot, tables, visibility, cursor) {
7498
+ throwIfAborted(options.signal);
7060
7499
  // Copy-on-write: expansion clones only full-text plans, leaving the compile cache's copy
7061
7500
  // untouched.
7062
7501
  plan = expandFtsColumns(plan, (tableName) => searchableFtsColumns(tables.find((table) => table.name === tableName)));
@@ -7077,7 +7516,9 @@ export class MinnowDatabase {
7077
7516
  const visibleByTable = new Map();
7078
7517
  const rowCounts = new Map();
7079
7518
  for (const table of tables) {
7519
+ throwIfAborted(options.signal);
7080
7520
  const segments = await this.#visibleSegmentRecords(table, snapshot, visibility);
7521
+ throwIfAborted(options.signal);
7081
7522
  visibleByTable.set(table.name, segments);
7082
7523
  if (segments.every((segment) => {
7083
7524
  const kind = segment.kind;
@@ -7092,7 +7533,7 @@ export class MinnowDatabase {
7092
7533
  plan = chooseJoinOrder(plan, rowCounts);
7093
7534
  const baseTable = tables.find((table) => table.name === plan.base.table);
7094
7535
  if (baseTable === undefined)
7095
- throw new TypeError(`Unknown table: ${plan.base.table}`);
7536
+ throw new UnknownTableError(plan.base.table);
7096
7537
  const requestedBaseColumns = columns.get(baseTable.name) ?? [];
7097
7538
  const projectedBaseColumns = requestedBaseColumns.length === 0 ? [] : resolveReadColumns(baseTable, requestedBaseColumns);
7098
7539
  if (secondaryIndexOrderForPlan(plan, baseTable, projectedBaseColumns, false) !== undefined) {
@@ -7111,6 +7552,7 @@ export class MinnowDatabase {
7111
7552
  record.name,
7112
7553
  record.id === freshBaseTable.id ? freshBaseTable : record,
7113
7554
  ])), snapshot, visibility);
7555
+ throwIfAborted(options.signal);
7114
7556
  if (planContainsFts(plan, "bm25") && ftsStats === undefined)
7115
7557
  return undefined;
7116
7558
  // The streamed scan honors the same index pruning as the materialized path; a budgeted
@@ -7119,11 +7561,14 @@ export class MinnowDatabase {
7119
7561
  (await this.#visibleSegmentRecords(freshBaseTable, snapshot, visibility));
7120
7562
  this.#maybeScheduleAutoCompaction(freshBaseTable, visibleBaseSegments);
7121
7563
  const ftsSegments = await this.#ftsPrunedSegments(freshBaseTable, visibleBaseSegments, plan, snapshot);
7564
+ throwIfAborted(options.signal);
7122
7565
  const indexed = await this.#secondaryIndexPrunedSegments(freshBaseTable, ftsSegments, plan, snapshot);
7566
+ throwIfAborted(options.signal);
7123
7567
  const baseSegments = indexed.segments;
7124
7568
  // Zone-map elimination composes after index pruning: whole row groups whose statistics
7125
7569
  // reject the plan's predicates never stream at all.
7126
7570
  const zonePruned = await this.#zonePrunedStreamSegments(plan, freshBaseTable, projectedBaseColumns, baseSegments, snapshot);
7571
+ throwIfAborted(options.signal);
7127
7572
  const baseView = this.#streamedViewFactory(baseTable, projectedBaseColumns, zonePruned?.segments ?? baseSegments, snapshot, zonePruned?.storedBlocks, zonePruned !== undefined || indexed.pruned);
7128
7573
  if (baseView === undefined)
7129
7574
  return undefined;
@@ -7145,26 +7590,31 @@ export class MinnowDatabase {
7145
7590
  const budget = options.executionMemoryBudgetBytes;
7146
7591
  const estimate = estimatedColumnarBytes(buildSegments, projectedBuildColumns);
7147
7592
  if (buildView !== undefined && estimate > budget / 4) {
7148
- 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);
7149
7594
  }
7150
7595
  }
7151
7596
  }
7152
7597
  }
7153
7598
  const runStreamedExecution = async (attempt) => {
7599
+ throwIfAborted(options.signal);
7154
7600
  const memory = new QueryMemoryContext(options.executionMemoryBudgetBytes);
7155
7601
  let prepared;
7156
7602
  try {
7157
7603
  const streamed = await baseView.create(memory);
7604
+ throwIfAborted(options.signal);
7158
7605
  const inputTables = new Map([[baseTable.name, streamed.table]]);
7159
7606
  for (const table of tables) {
7607
+ throwIfAborted(options.signal);
7160
7608
  if (table.name === baseTable.name)
7161
7609
  continue;
7162
7610
  const requestedColumns = columns.get(table.name) ?? [];
7163
7611
  inputTables.set(table.name, await this.#materializeColumnarTableAtSnapshot(table, snapshot, requestedColumns.length === 0 ? [] : resolveReadColumns(table, requestedColumns), visibility));
7612
+ throwIfAborted(options.signal);
7164
7613
  }
7165
7614
  prepared = createPreparedColumnarQuery(plan, inputTables, memory, {
7166
7615
  ...(ftsStats === undefined ? {} : { ftsStats }),
7167
7616
  outputNeedsExternalization: queryResultNeedsExternalization(plan, typedSchemas),
7617
+ outputColumnDomains: inferResultColumnDomains(plan, typedSchemas),
7168
7618
  });
7169
7619
  if (cursor !== undefined) {
7170
7620
  const emission = { happened: false };
@@ -7172,20 +7622,37 @@ export class MinnowDatabase {
7172
7622
  batchRows: cursor.batchRows,
7173
7623
  signal: cursor.signal,
7174
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
+ }),
7175
7633
  }, async (batch) => {
7176
7634
  emission.happened = true;
7177
7635
  await cursor.consume(externalizeQueryResult(batch));
7178
7636
  });
7179
7637
  if (!emission.happened) {
7180
- await cursor.consume({ columns: [...columns], rows: [] });
7638
+ await cursor.consume({
7639
+ columns: [...columns],
7640
+ columnDomains: inferResultColumnDomains(plan, typedSchemas),
7641
+ rows: [],
7642
+ });
7181
7643
  }
7182
7644
  options.onStats?.({ peakMemoryBytes: memory.usage.peakBytes });
7183
- return { columns: [...columns], rows: [] };
7645
+ return {
7646
+ columns: [...columns],
7647
+ columnDomains: inferResultColumnDomains(plan, typedSchemas),
7648
+ rows: [],
7649
+ };
7184
7650
  }
7185
7651
  if (!attempt.spill) {
7186
7652
  try {
7187
7653
  const streamedResult = await prepared.executeAsync({
7188
7654
  loadScanWindow: streamed.load,
7655
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
7189
7656
  });
7190
7657
  options.onStats?.({ peakMemoryBytes: memory.usage.peakBytes });
7191
7658
  return streamedResult;
@@ -7200,6 +7667,7 @@ export class MinnowDatabase {
7200
7667
  ...(spillPageRows === undefined ? {} : { spillPageRows }),
7201
7668
  spillStore: this.#leasedSpillStore(),
7202
7669
  loadScanWindow: streamed.load,
7670
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
7203
7671
  });
7204
7672
  options.onStats?.({ peakMemoryBytes: memory.usage.peakBytes });
7205
7673
  return spilledResult;
@@ -7449,7 +7917,8 @@ export class MinnowDatabase {
7449
7917
  * like any unordered query. The rescans trade time for bounded memory; the decoded-block
7450
7918
  * cache keeps repeat decodes cheap.
7451
7919
  */
7452
- async #runPartitionedJoin(plan, budgetBytes, estimatedBuildBytes, baseView, buildView, buildTableName, buildKeyName) {
7920
+ async #runPartitionedJoin(plan, budgetBytes, estimatedBuildBytes, baseView, buildView, buildTableName, buildKeyName, signal) {
7921
+ throwIfAborted(signal);
7453
7922
  // One partition should fit in an eighth of the budget: the resident partition briefly
7454
7923
  // exists twice (boxed rows, then their vectors) and shares the budget with both streamed
7455
7924
  // windows and the accumulating result.
@@ -7462,6 +7931,7 @@ export class MinnowDatabase {
7462
7931
  try {
7463
7932
  const combined = [];
7464
7933
  for (let partition = 0; partition < partitions; partition += 1) {
7934
+ throwIfAborted(signal);
7465
7935
  if (combined.length >= wantedRows)
7466
7936
  break;
7467
7937
  const passMemory = root.createChild();
@@ -7472,6 +7942,7 @@ export class MinnowDatabase {
7472
7942
  const collectMemory = passMemory.createChild();
7473
7943
  const buildScanMemory = collectMemory.createChild();
7474
7944
  const build = await buildView.create(buildScanMemory);
7945
+ throwIfAborted(signal);
7475
7946
  let buildRows = [];
7476
7947
  const buildColumns = [...build.table.columns.entries()];
7477
7948
  // Small steps keep the collection windows a minor budget term next to the resident
@@ -7479,8 +7950,10 @@ export class MinnowDatabase {
7479
7950
  // consumed length clamps to the returned resident end.
7480
7951
  const step = 1_024;
7481
7952
  for (let start = 0; start < build.table.rowCount;) {
7953
+ throwIfAborted(signal);
7482
7954
  let length = Math.min(step, build.table.rowCount - start);
7483
7955
  const residentEnd = await build.load(start, length);
7956
+ throwIfAborted(signal);
7484
7957
  if (typeof residentEnd === "number" && residentEnd > start) {
7485
7958
  length = Math.min(length, residentEnd - start);
7486
7959
  }
@@ -7511,13 +7984,17 @@ export class MinnowDatabase {
7511
7984
  const buildInput = columnarTableFromRows(buildTableName, buildRows, undefined, false);
7512
7985
  buildRows = [];
7513
7986
  const streamedBase = await baseView.create(passMemory);
7987
+ throwIfAborted(signal);
7514
7988
  const inputTables = new Map([
7515
7989
  [buildTableName, buildInput],
7516
7990
  [streamedBase.table.name, streamedBase.table],
7517
7991
  ]);
7518
7992
  prepared = createPreparedColumnarQuery(strippedPlan, inputTables, passMemory);
7519
7993
  collectMemory.close();
7520
- const result = await prepared.executeAsync({ loadScanWindow: streamedBase.load });
7994
+ const result = await prepared.executeAsync({
7995
+ loadScanWindow: streamedBase.load,
7996
+ ...(signal === undefined ? {} : { signal }),
7997
+ });
7521
7998
  for (const row of result.rows) {
7522
7999
  if (combined.length >= wantedRows)
7523
8000
  break;
@@ -7534,7 +8011,8 @@ export class MinnowDatabase {
7534
8011
  const rows = startRow > 0 || limit !== undefined
7535
8012
  ? combined.slice(startRow, limit === undefined ? undefined : startRow + limit)
7536
8013
  : combined;
7537
- return { columns: plan.select.map((item) => item.alias), rows };
8014
+ const columns = plan.select.map((item) => item.alias);
8015
+ return { columns, columnDomains: unknownColumnDomains(columns), rows };
7538
8016
  }
7539
8017
  finally {
7540
8018
  root.close();
@@ -8080,17 +8558,24 @@ export class MinnowDatabase {
8080
8558
  };
8081
8559
  }
8082
8560
  /** Prepares one block's inputs and executes it, returning the caller-owned result. */
8083
- 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);
8084
8563
  const ftsStats = await this.#ftsIndexStats(block, realTables, snapshot, visibility);
8085
- 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);
8086
8567
  const prepared = createPreparedColumnarQuery(block, inputs, memory.createChild(), ftsStats === undefined ? {} : { ftsStats });
8087
8568
  try {
8088
8569
  if (!allowSpill || memory.usage.budgetBytes === Number.MAX_SAFE_INTEGER) {
8089
- return prepared.execute();
8570
+ const result = prepared.execute();
8571
+ throwIfAborted(signal);
8572
+ return result;
8090
8573
  }
8091
8574
  if (!forceSpill) {
8092
8575
  try {
8093
- return prepared.execute();
8576
+ const result = prepared.execute();
8577
+ throwIfAborted(signal);
8578
+ return result;
8094
8579
  }
8095
8580
  catch (error) {
8096
8581
  if (!(error instanceof QueryMemoryBudgetError))
@@ -8100,6 +8585,7 @@ export class MinnowDatabase {
8100
8585
  return await prepared.executeAsync({
8101
8586
  spillStore: this.#leasedSpillStore(),
8102
8587
  ...(spillPageRows === undefined ? {} : { spillPageRows }),
8588
+ ...(signal === undefined ? {} : { signal }),
8103
8589
  });
8104
8590
  }
8105
8591
  finally {
@@ -8113,14 +8599,17 @@ export class MinnowDatabase {
8113
8599
  * real tables it references, so an identical block over identical table states reuses
8114
8600
  * the previous result instead of re-reading and re-executing.
8115
8601
  */
8116
- 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);
8117
8604
  const key = await this.#blockResultCacheKey(block, snapshot, visibility, realTables, cacheResults);
8605
+ throwIfAborted(signal);
8118
8606
  if (key !== undefined) {
8119
8607
  const cached = this.#cacheGet(key);
8120
8608
  if (cached !== undefined)
8121
8609
  return cached;
8122
8610
  }
8123
- 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);
8124
8613
  if (key !== undefined)
8125
8614
  this.#cachePut(key, result, queryResultRetainedBytes(result));
8126
8615
  return result;
@@ -8131,15 +8620,18 @@ export class MinnowDatabase {
8131
8620
  * synthetic sources in typedSchemas, and a later cache hit skips that registration, so
8132
8621
  * re-inferring the schema at hit time would find those names missing.
8133
8622
  */
8134
- 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);
8135
8625
  const key = await this.#blockResultCacheKey(block, snapshot, visibility, realTables, cacheResults);
8626
+ throwIfAborted(signal);
8136
8627
  const schemaKey = key === undefined ? undefined : `s${key}`;
8137
8628
  if (schemaKey !== undefined) {
8138
8629
  const cached = this.#cacheGet(schemaKey);
8139
8630
  if (cached !== undefined)
8140
8631
  return cached;
8141
8632
  }
8142
- 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);
8143
8635
  const schema = inferBlockSchema(block, typedSchemas);
8144
8636
  const entry = { result, schema };
8145
8637
  if (schemaKey !== undefined) {
@@ -10512,7 +11004,7 @@ export class MinnowDatabase {
10512
11004
  if (recoveredManifest === undefined) {
10513
11005
  throw new Error(`Compaction manifest is missing: ${String(linkedTransaction.committedVersion)}`);
10514
11006
  }
10515
- this.#afterCompactionCommit(recoveredManifest);
11007
+ this.#afterCompactionCommit(recoveredManifest, table.id);
10516
11008
  return this.#publishedCompactionProgress(table, job);
10517
11009
  }
10518
11010
  const rewritePlan = job.rewritePlan;
@@ -10698,7 +11190,7 @@ export class MinnowDatabase {
10698
11190
  let publicationConflict;
10699
11191
  try {
10700
11192
  manifest = await transaction.commit();
10701
- this.#afterCompactionCommit(manifest);
11193
+ this.#afterCompactionCommit(manifest, table.id);
10702
11194
  break;
10703
11195
  }
10704
11196
  catch (error) {
@@ -11531,7 +12023,7 @@ export class MinnowDatabase {
11531
12023
  }
11532
12024
  const table = tables.find((candidate) => candidate.name === tableName);
11533
12025
  if (table === undefined || table.view !== undefined) {
11534
- throw new TypeError(`Unknown table: ${tableName}`);
12026
+ throw new UnknownTableError(tableName);
11535
12027
  }
11536
12028
  if (indexColumns.length === 0)
11537
12029
  throw new TypeError("An index needs at least one column");
@@ -13739,7 +14231,7 @@ export class MinnowDatabase {
13739
14231
  validateName(name, "Table");
13740
14232
  const table = await this.store.getTableByName(name);
13741
14233
  if (table === undefined)
13742
- throw new Error(`Table not found: ${name}`);
14234
+ throw new UnknownTableError(name);
13743
14235
  // Reads resolve a view into its query before reaching here, so a view arriving at this
13744
14236
  // point is a write, a DDL statement, or a path that forgot to rewrite: all of them errors.
13745
14237
  if (table.view !== undefined)
@@ -13939,13 +14431,27 @@ function assertTriggerBodyTargetSchema(compiled, target) {
13939
14431
  if (column !== undefined &&
13940
14432
  isDefaultInsertValue(value) &&
13941
14433
  !column.nullable &&
13942
- column.defaultValue === undefined) {
14434
+ column.defaultValue === undefined &&
14435
+ column.generatedValue === undefined) {
13943
14436
  throw new TypeError(`Trigger body INSERT uses DEFAULT for a non-nullable column without a default: ${column.name}`);
13944
14437
  }
13945
14438
  }
13946
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
+ }
13947
14450
  for (const column of columns) {
13948
- 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) {
13949
14455
  throw new TypeError(`Trigger body INSERT omits a non-nullable column without a default: ${column.name}`);
13950
14456
  }
13951
14457
  }
@@ -13967,6 +14473,9 @@ function assertTriggerBodyTargetSchema(compiled, target) {
13967
14473
  if (assignedColumn === undefined) {
13968
14474
  throw new TypeError(`Trigger body UPDATE assignment column does not exist: ${assignment.column}`);
13969
14475
  }
14476
+ if (assignedColumn.generatedValue !== undefined) {
14477
+ throw new TypeError(`Trigger body UPDATE assigns generated column: ${assignment.column}`);
14478
+ }
13970
14479
  if (assignedColumn.id === target.uniqueKeyColumnId ||
13971
14480
  (target.primaryKeyColumnIds ?? []).includes(assignedColumn.id)) {
13972
14481
  throw new TypeError(`Trigger body UPDATE cannot update a key column: ${assignment.column}`);
@@ -14116,6 +14625,7 @@ function visibleSegmentManifestVersion(value, label) {
14116
14625
  */
14117
14626
  const compiledChecks = new Map();
14118
14627
  const COMPILED_CHECK_CACHE_LIMIT = 64;
14628
+ const compiledGeneratedColumns = new Map();
14119
14629
  function tableChecks(table) {
14120
14630
  const key = `${table.id}/${String(table.revision)}`;
14121
14631
  const cached = compiledChecks.get(key);
@@ -14138,6 +14648,84 @@ function tableChecks(table) {
14138
14648
  compiledChecks.set(key, compiled);
14139
14649
  return compiled;
14140
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
+ }
14141
14729
  /**
14142
14730
  * Applies a table's CHECK constraints to one row (E141-06). A constraint fails only when it
14143
14731
  * evaluates to false: SQL's three-valued logic lets an unknown pass, which is why a NULL column
@@ -14995,32 +15583,72 @@ function compositeKeyValue(columns, values) {
14995
15583
  /** Canonicalizes logical PostgreSQL domains before validation and primitive block encoding. */
14996
15584
  function normalizeDomainBatch(table, input) {
14997
15585
  for (const column of table.columns) {
14998
- const domain = column.sqlDomain;
14999
- if (domain === undefined)
15000
- continue;
15001
15586
  const values = input.columns[column.name];
15002
15587
  if (values === undefined)
15003
15588
  continue;
15004
- input.columns[column.name] = values.map((value) => normalizeSqlDomainValue(domain, value));
15589
+ if (column.sqlDomain !== undefined || column.type === "datetime") {
15590
+ input.columns[column.name] = values.map((value) => normalizeColumnLogicalValue(column, value));
15591
+ }
15005
15592
  }
15006
15593
  }
15594
+ function normalizeColumnLogicalValue(column, value) {
15595
+ if (column.sqlDomain !== undefined)
15596
+ return normalizeSqlDomainValue(column.sqlDomain, value);
15597
+ if (column.type === "datetime" && isDateDomainValue(value)) {
15598
+ const external = externalSqlDomainValue(value);
15599
+ if (typeof external !== "string")
15600
+ throw new TypeError("Invalid DATE value");
15601
+ return new Date(`${external}T00:00:00.000Z`);
15602
+ }
15603
+ return value;
15604
+ }
15605
+ function selectBatchRows(input, indexes) {
15606
+ return {
15607
+ columns: Object.fromEntries(Object.entries(input.columns).map(([name, values]) => [
15608
+ name,
15609
+ indexes.map((index) => values[index] ?? null),
15610
+ ])),
15611
+ ...(input.omitted === undefined
15612
+ ? {}
15613
+ : {
15614
+ omitted: Object.fromEntries(Object.entries(input.omitted).map(([name, values]) => [
15615
+ name,
15616
+ indexes.map((index) => values[index] ?? false),
15617
+ ])),
15618
+ }),
15619
+ rowCount: indexes.length,
15620
+ };
15621
+ }
15622
+ function normalizeUpsertConflictWhere(table, predicate) {
15623
+ const operators = ["=", "!=", "<>", ">", ">=", "<", "<="];
15624
+ if (!operators.includes(predicate.operator)) {
15625
+ throw new TypeError(`Unsupported upsert conflict operator: ${predicate.operator}`);
15626
+ }
15627
+ const column = visibleTableColumns(table).find(({ name }) => name === predicate.column);
15628
+ if (column === undefined) {
15629
+ throw new TypeError(`Unknown upsert conflict column: ${table.name}.${predicate.column}`);
15630
+ }
15631
+ const value = normalizeColumnLogicalValue(column, predicate.value);
15632
+ if (value !== null)
15633
+ validateValue({ ...column, nullable: true }, value, 0);
15634
+ return { column, operator: predicate.operator, value };
15635
+ }
15007
15636
  function normalizeDomainUpdate(table, input) {
15008
- if (!table.columns.some((column) => column.sqlDomain !== undefined))
15637
+ if (!table.columns.some((column) => column.sqlDomain !== undefined || column.type === "datetime"))
15009
15638
  return input;
15010
15639
  let changed = false;
15011
15640
  const changes = { ...input.changes };
15012
15641
  for (const [name, values] of Object.entries(input.changes)) {
15013
- const domain = table.columns.find((column) => column.name === name)?.sqlDomain;
15014
- if (domain === undefined)
15642
+ const column = table.columns.find((candidate) => candidate.name === name);
15643
+ if (column === undefined || (column.sqlDomain === undefined && column.type !== "datetime"))
15015
15644
  continue;
15016
15645
  changed = true;
15017
- changes[name] = values.map((value) => normalizeSqlDomainValue(domain, value));
15646
+ changes[name] = values.map((value) => normalizeColumnLogicalValue(column, value));
15018
15647
  }
15019
15648
  const key = getUniqueKeyColumn(table);
15020
- const keyDomain = key?.sqlDomain;
15021
- const keys = keyDomain === undefined
15649
+ const keys = key === undefined || (key.sqlDomain === undefined && key.type !== "datetime")
15022
15650
  ? input.keys
15023
- : input.keys.map((value) => normalizeSqlDomainValue(keyDomain, value));
15651
+ : input.keys.map((value) => normalizeColumnLogicalValue(key, value));
15024
15652
  return changed || keys !== input.keys ? { keys, changes } : input;
15025
15653
  }
15026
15654
  /** Coerces literals and bound parameters opposite a domain column before planning/index probes. */
@@ -15217,7 +15845,7 @@ function tryKeyToken(type, value) {
15217
15845
  * (level one and above) are the folded state, not fragmentation, and do not count — a large
15218
15846
  * keyed table is many partitions by design.
15219
15847
  */
15220
- function autoCompactionDue(segments) {
15848
+ function autoCompactionHint(version, segments) {
15221
15849
  let levelZero = 0;
15222
15850
  let deltas = 0;
15223
15851
  for (const segment of segments) {
@@ -15227,7 +15855,18 @@ function autoCompactionDue(segments) {
15227
15855
  if (kind !== "insert" && kind !== "base")
15228
15856
  deltas += 1;
15229
15857
  }
15230
- 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));
15231
15870
  }
15232
15871
  /** A macrotask boundary, so background work lets queued queries and writes run between steps. */
15233
15872
  function yieldToEventLoop() {
@@ -16233,6 +16872,17 @@ function columnVectorKeyToken(type, vector, row) {
16233
16872
  }
16234
16873
  throw new Error("Column vector type mismatch");
16235
16874
  }
16875
+ function normalizedDatetimeLiteral(value) {
16876
+ if (value instanceof Date)
16877
+ return Number.isFinite(dateMilliseconds(value)) ? value : undefined;
16878
+ if (!isDateDomainValue(value))
16879
+ return undefined;
16880
+ const external = externalSqlDomainValue(value);
16881
+ if (typeof external !== "string")
16882
+ return undefined;
16883
+ const date = new Date(`${external}T00:00:00.000Z`);
16884
+ return Number.isFinite(dateMilliseconds(date)) ? date : undefined;
16885
+ }
16236
16886
  function zonePredicates(plan, table) {
16237
16887
  if (plan.joins.length > 0 || plan.base.table !== table.name)
16238
16888
  return [];
@@ -16251,10 +16901,11 @@ function zonePredicates(plan, table) {
16251
16901
  const literalValue = (column, expression) => {
16252
16902
  if (expression.kind !== "literal")
16253
16903
  return undefined;
16904
+ const datetime = column.type === "datetime" ? normalizedDatetimeLiteral(expression.value) : undefined;
16254
16905
  const value = column.type === "datetime"
16255
- ? expression.value instanceof Date
16256
- ? dateMilliseconds(expression.value)
16257
- : undefined
16906
+ ? datetime === undefined
16907
+ ? undefined
16908
+ : dateMilliseconds(datetime)
16258
16909
  : typeof expression.value === "number"
16259
16910
  ? expression.value
16260
16911
  : undefined;
@@ -16297,10 +16948,11 @@ function zonePredicates(plan, table) {
16297
16948
  const column = leftColumn ?? rightColumn;
16298
16949
  if (column === undefined || literal.kind !== "literal")
16299
16950
  continue;
16951
+ const datetime = column.type === "datetime" ? normalizedDatetimeLiteral(literal.value) : undefined;
16300
16952
  const value = column.type === "datetime"
16301
- ? literal.value instanceof Date
16302
- ? dateMilliseconds(literal.value)
16303
- : undefined
16953
+ ? datetime === undefined
16954
+ ? undefined
16955
+ : dateMilliseconds(datetime)
16304
16956
  : typeof literal.value === "number"
16305
16957
  ? literal.value
16306
16958
  : undefined;
@@ -16330,9 +16982,16 @@ function secondaryIndexPredicates(plan, table) {
16330
16982
  const literalValue = (column, expression) => {
16331
16983
  if (expression.kind !== "literal" || expression.value === null)
16332
16984
  return undefined;
16985
+ let value = expression.value;
16986
+ if (column.type === "datetime" && isDateDomainValue(value)) {
16987
+ const datetime = normalizedDatetimeLiteral(value);
16988
+ if (datetime === undefined)
16989
+ return undefined;
16990
+ value = datetime;
16991
+ }
16333
16992
  try {
16334
- secondaryIndexTerm(column.type, expression.value);
16335
- return expression.value;
16993
+ secondaryIndexTerm(column.type, value);
16994
+ return value;
16336
16995
  }
16337
16996
  catch {
16338
16997
  return undefined;