@minnowdb/core 0.8.0 → 0.9.1

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 (66) hide show
  1. package/dist/engine/artifact-cache.js +3 -2
  2. package/dist/engine/buffered-writer.js +12 -2
  3. package/dist/engine/client.d.ts +8 -2
  4. package/dist/engine/client.js +106 -22
  5. package/dist/engine/database.d.ts +5 -3
  6. package/dist/engine/database.js +2044 -1674
  7. package/dist/engine/errors.d.ts +19 -0
  8. package/dist/engine/errors.js +29 -0
  9. package/dist/engine/index.d.ts +1 -0
  10. package/dist/engine/index.js +4 -0
  11. package/dist/engine/live-accept.js +13 -0
  12. package/dist/engine/live-aggregate.js +54 -16
  13. package/dist/engine/live.d.ts +2 -0
  14. package/dist/engine/live.js +109 -108
  15. package/dist/engine/query-cache.js +17 -2
  16. package/dist/engine/query.d.ts +1 -2
  17. package/dist/engine/query.js +4 -7
  18. package/dist/engine/result-state.d.ts +7 -0
  19. package/dist/engine/result-state.js +15 -0
  20. package/dist/engine/vector.d.ts +4 -0
  21. package/dist/engine/vector.js +14 -14
  22. package/dist/engine/windows.js +15 -14
  23. package/dist/engine/worker-server.d.ts +1 -1
  24. package/dist/engine/worker-server.js +28 -15
  25. package/dist/engine/write-coordinator.js +54 -0
  26. package/dist/storage/indexeddb.js +134 -89
  27. package/dist/storage/opfs/index.d.ts +1 -1
  28. package/dist/storage/opfs/index.js +3 -1
  29. package/dist/storage/opfs/leader.js +20 -9
  30. package/dist/storage/opfs/rpc.js +3 -1
  31. package/dist/storage/opfs/store.d.ts +2 -0
  32. package/dist/storage/opfs/store.js +128 -40
  33. package/dist/storage/toolkit/record-core.js +2 -1
  34. package/dist/storage/types.d.ts +14 -0
  35. package/dist/storage/types.js +21 -0
  36. package/dist/transactions/index.d.ts +3 -0
  37. package/dist/transactions/index.js +4 -1
  38. package/dist/worker-protocol/index.d.ts +1 -1
  39. package/dist/worker-protocol/index.js +1 -1
  40. package/package.json +2 -2
  41. package/dist/date-value.d.ts +0 -20
  42. package/dist/engine/artifact-cache.d.ts +0 -29
  43. package/dist/engine/byte-estimates.d.ts +0 -11
  44. package/dist/engine/cancellation.d.ts +0 -2
  45. package/dist/engine/defaults.d.ts +0 -29
  46. package/dist/engine/group-index.d.ts +0 -33
  47. package/dist/engine/join-index.d.ts +0 -10
  48. package/dist/engine/live-aggregate.d.ts +0 -12
  49. package/dist/engine/live-equal.d.ts +0 -7
  50. package/dist/engine/point-read.d.ts +0 -59
  51. package/dist/engine/query-cache.d.ts +0 -21
  52. package/dist/engine/query-generations.d.ts +0 -8
  53. package/dist/engine/query-identity.d.ts +0 -3
  54. package/dist/engine/result-wire.d.ts +0 -70
  55. package/dist/engine/sort-keys.d.ts +0 -73
  56. package/dist/engine/sql-domains.d.ts +0 -97
  57. package/dist/engine/sql-functions.d.ts +0 -11
  58. package/dist/engine/sql-json.d.ts +0 -40
  59. package/dist/engine/sql-semantics.d.ts +0 -66
  60. package/dist/engine/worker-store-indexeddb.d.ts +0 -2
  61. package/dist/engine/worker-store-memory.d.ts +0 -2
  62. package/dist/engine/worker-store-opfs.d.ts +0 -2
  63. package/dist/engine/write-block-planner.d.ts +0 -19
  64. package/dist/storage/opfs/leader.d.ts +0 -460
  65. package/dist/storage/opfs/rpc.d.ts +0 -82
  66. package/dist/storage/opfs/snapshot-ledger.d.ts +0 -41
@@ -1,4 +1,6 @@
1
+ import { coordinateWrite } from "./write-coordinator.js";
1
2
  import { LiveAggregate } from "./live-aggregate.js";
3
+ import { stageLiveExecution } from "./live-accept.js";
2
4
  import { QueryGenerations } from "./query-generations.js";
3
5
  import { applyWindowFunctionsAsync } from "./windows.js";
4
6
  import { crossJoinPlan } from "../plan/model.js";
@@ -8,7 +10,7 @@ import { estimateBatchBytes, estimateRowBytes, estimateValuesBytes } from "./byt
8
10
  import { throwIfAborted } from "./cancellation.js";
9
11
  import { BufferedTableWriter } from "./buffered-writer.js";
10
12
  import { attachLifecycleFlush, BufferedTableWriter as BufferedTableWriter2, MAX_BUFFERED_WRITER_PENDING_ADDS } from "./buffered-writer.js";
11
- import { CompactionJobCancelledError, CompactionMemoryBudgetError, CompactionWriteAmplificationError, DatabaseReadBacklogError, MaintenanceBacklogError, MissingKeyError, SqlCompileError, UnknownTableError, UniqueConstraintError, VisibleSegmentCursorStaleError } from "./errors.js";
13
+ import { CompactionJobCancelledError, CompactionMemoryBudgetError, CompactionWriteAmplificationError, DatabaseReadBacklogError, TransactionExpiredError, MaintenanceBacklogError, MissingKeyError, SqlCompileError, UnknownTableError, UniqueConstraintError, VisibleSegmentCursorStaleError } from "./errors.js";
12
14
  import { DatabaseReadBacklogError as DatabaseReadBacklogError2 } from "./errors.js";
13
15
  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";
14
16
  import { dateIsoString, dateMilliseconds } from "../date-value.js";
@@ -19,8 +21,8 @@ import { cachedQueryTerms, FTS_TOKENIZER_VERSION, renderDocumentValue, tokenize
19
21
  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_BLOCK_READ_BATCH_BYTES, 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";
20
22
  import { decodeSnapshotFrameStream, encodeSnapshotFrameStreamFooter, encodeSnapshotFrameStreamHeader, extendSnapshotFrameStreamChecksum, snapshotFrameEnvelopeParts, snapshotFrameStreamHeaderIdentity } from "../storage/snapshot.js";
21
23
  import { Snapshot, TransactionManager } from "../transactions/index.js";
22
- import { bindPendingSelectShapes, bindPlanParameters, bindStatementParameters, DUAL_TABLE, dualTableRows, blockHasSubqueries, combineUnionResults, compileCheckExpression, compileQuery, hasAggregate, createRecursiveCteState, annotateAvgArgumentScales, compileStatement, comparisonHolds, createPreparedColumnarQuery, evaluateJoinedRowExpression, evaluateRowExpression, externalizeQueryResult, expressionColumnNames, inferBlockSchema, inferResultColumnDomains, isDeferredInsertExpression, isDefaultInsertValue, referencedColumns, childExpressions, mapChildExpressions, expandFtsColumns, expandNaturalJoins, expandSourceColumnAliases, expandViewSources, forEachBlockExpression, forEachNestedBlock, planContainsFts, planHasNaturalJoins, planHasSourceColumnAliases, planReadsTable, planReadsViews, planReadsBeyondSingleScan, projectResultColumns, queryResultNeedsExternalization, resolveStatementDatetimes, subqueryResolutionSteps, topLevelFtsMatchConjuncts, transparentProjectionSource, unknownColumnDomains, validateDefaultExpression, windowOutputDomain, windowOutputType, volatileScalarFunctionNames, annotatePlanIntegerDivision, clonePlanTree, planMayHaveIntegerDivision, foldIdentifierCase, extendGroupByWithKeyDependents, expandRowReferences } from "./query.js";
23
- import { copyQueryResult, planMemoKey, queryResultMemoKey, queryResultRetainedBytes, RESULT_MEMO_MAX_BYTES } from "./query-cache.js";
24
+ import { bindPendingSelectShapes, bindPlanParameters, bindStatementParameters, DUAL_TABLE, dualTableRows, blockHasSubqueries, combineUnionResults, compileCheckExpression, compileQuery, hasAggregate, createRecursiveCteState, annotateAvgArgumentScales, compileStatement, comparisonHolds, createPreparedColumnarQuery, evaluateJoinedRowExpression, evaluateRowExpression, externalizeQueryResult, markQueryResultExternal, expressionColumnNames, inferBlockSchema, inferResultColumnDomains, isDeferredInsertExpression, isDefaultInsertValue, referencedColumns, childExpressions, mapChildExpressions, expandFtsColumns, expandNaturalJoins, expandSourceColumnAliases, expandViewSources, forEachBlockExpression, forEachNestedBlock, planContainsFts, planHasNaturalJoins, planHasSourceColumnAliases, planReadsTable, planReadsViews, planReadsBeyondSingleScan, projectResultColumns, queryResultNeedsExternalization, resolveStatementDatetimes, subqueryResolutionSteps, topLevelFtsMatchConjuncts, transparentProjectionSource, unknownColumnDomains, validateDefaultExpression, windowOutputDomain, windowOutputType, volatileScalarFunctionNames, annotatePlanIntegerDivision, clonePlanTree, planMayHaveIntegerDivision, foldIdentifierCase, extendGroupByWithKeyDependents, expandRowReferences } from "./query.js";
25
+ import { copyQueryResult, planMemoKey, queryResultMemoKey, queryResultRetainedBytes, RESULT_MEMO_MAX_BYTES, sameQueryRow as sameLiveRow } from "./query-cache.js";
24
26
  import { QueryMemoryBudgetError, QueryMemoryContext, DEFAULT_QUERY_MEMORY_BUDGET_BYTES } from "./memory.js";
25
27
  import { LiveQueryLimitError, LiveQuerySet, MAX_LIVE_QUERY_SETS_PER_DATABASE } from "./live.js";
26
28
  import { chooseJoinOrder, optimizePlan, qualifyCorrelatedReferences, renderPlan } from "./optimizer.js";
@@ -28,7 +30,7 @@ import { encodeSqlEqualityValue, readUntypedText, compareSqlValues } from "./sql
28
30
  import { exactNumericAsNumber, exactNumericValue, externalSqlDomainValue, externalSqlTextValue, isDateDomainValue, isExactNumeric, isSqlDomainValue, normalizeSqlDomainValue, protectedSqlTextValue } from "./sql-domains.js";
29
31
  import { toCatalog } from "./catalog.js";
30
32
  import { applyColumnSteps, assertColumnDroppable, compileGeneratedColumnExpression, declaredForeignKeys, isDestructiveStep, planMigration } from "./schema.js";
31
- import { columnarTableFromRows, createColumnarTable, vectorValue } from "./vector.js";
33
+ import { columnarTableFromRows, createColumnarTable, createColumnVector, vectorValue } from "./vector.js";
32
34
  import { cachedPointReadTemplate, equalRunRange, pointReadTestHooks, resolvePointReadShape, valuesAreAscending } from "./point-read.js";
33
35
  const PLATFORM_LITTLE_ENDIAN = new Uint8Array(new Uint32Array([1]).buffer)[0] === 1;
34
36
  const vectorTextDecoder = new TextDecoder("utf-8", { fatal: true });
@@ -75,6 +77,7 @@ const CATALOG_SEGMENT_READ_CONCURRENCY = 8;
75
77
  const MAX_TRANSACTION_SAVEPOINTS = 64;
76
78
  const MAX_TRANSACTION_SAVEPOINT_BYTES = 8 * 1024 * 1024;
77
79
  const MAX_DATABASE_PENDING_WRITES = 64;
80
+ const MAX_DATABASE_ACTIVE_SCOPES = 256;
78
81
  const MAX_DATABASE_ACTIVE_READS = 256;
79
82
  const AUTO_COLLECT_MAX_DEBT_COMMITS = 4096;
80
83
  const AUTO_COLLECT_RETRY_MIN_MS = 1e3;
@@ -566,11 +569,19 @@ class MinnowDatabase {
566
569
  #sequenceCurrValues = /* @__PURE__ */ new Map();
567
570
  #openTransaction;
568
571
  #statementTransactionCleanup;
572
+ #statementTransactionExpired = false;
573
+ #executeChain = Promise.resolve();
574
+ #pendingExecutes = 0;
575
+ #scopeTasks = /* @__PURE__ */ new Set();
576
+ #scopeControllers = /* @__PURE__ */ new Set();
569
577
  #compression;
570
578
  #gzipVerdicts = /* @__PURE__ */ new Map();
571
579
  #rowsPerBlock;
572
580
  #targetBlockBytes;
573
581
  #maxCommitRetries;
582
+ #coordinateWrites;
583
+ #shutdown = new AbortController();
584
+ #exports = /* @__PURE__ */ new Set();
574
585
  #now;
575
586
  #spillOwnerLeaseMs;
576
587
  #transactionOwnerLeaseMs;
@@ -646,6 +657,7 @@ class MinnowDatabase {
646
657
  if (!Number.isSafeInteger(this.#targetBlockBytes) || this.#targetBlockBytes <= 0 || this.#targetBlockBytes > MAX_PHYSICAL_COLUMN_BYTE_LENGTH) {
647
658
  throw new RangeError(`Target block bytes must be a positive whole number no larger than ${String(MAX_PHYSICAL_COLUMN_BYTE_LENGTH)}`);
648
659
  }
660
+ this.#coordinateWrites = options.coordinateWrites ?? true;
649
661
  this.#maxCommitRetries = options.maxCommitRetries ?? 8;
650
662
  if (!Number.isSafeInteger(this.#maxCommitRetries) || this.#maxCommitRetries < 0) {
651
663
  throw new RangeError("Commit retries must be a non-negative whole number");
@@ -685,11 +697,15 @@ class MinnowDatabase {
685
697
  if (this.#closePromise !== void 0)
686
698
  return this.#closePromise;
687
699
  this.#closed = true;
700
+ this.#shutdown.abort(new Error("Database is closed"));
688
701
  const closing = this.#closeResources();
689
702
  this.#closePromise = closing;
690
703
  return closing;
691
704
  }
692
705
  async #closeResources() {
706
+ for (const controller of this.#scopeControllers) {
707
+ controller.abort(new Error("Database is closed"));
708
+ }
693
709
  if (this.#idleCompactionTimer !== void 0) {
694
710
  clearTimeout(this.#idleCompactionTimer);
695
711
  this.#idleCompactionTimer = void 0;
@@ -708,6 +724,11 @@ class MinnowDatabase {
708
724
  set.close();
709
725
  this.#liveSets.clear();
710
726
  await Promise.allSettled([
727
+ ...[...this.#exports].map((iterator) => iterator.return(void 0)),
728
+ this.#writeChain,
729
+ this.#executeChain,
730
+ ...this.#scopeTasks,
731
+ ...this.#foregroundTasks,
711
732
  ...this.#autoCompactionsInFlight.values(),
712
733
  ...this.#ftsBuildsInFlight.values(),
713
734
  ...this.#autoCollectionTask === void 0 ? [] : [this.#autoCollectionTask],
@@ -761,264 +782,270 @@ class MinnowDatabase {
761
782
  };
762
783
  }
763
784
  async createTable(input) {
764
- const name = validateName(input.name, "Table");
765
- if (input.columns.length === 0)
766
- throw new TypeError("A table needs at least one column");
767
- const enumDomains = /* @__PURE__ */ new Map();
768
- for (const column of input.columns) {
769
- if (column.sqlDomain?.kind !== "enum" || column.sqlDomain.values.length > 0)
770
- continue;
771
- const record = await this.store.getTableByName(`${ENUM_TYPE_PREFIX}${column.sqlDomain.name}`);
772
- if (record?.enumType === void 0) {
773
- throw new TypeError(`Unsupported column type: ${column.sqlDomain.name}`);
774
- }
775
- enumDomains.set(column.name, {
776
- kind: "enum",
777
- name: record.enumType.name,
778
- values: [...record.enumType.values]
779
- });
780
- }
781
- const names = /* @__PURE__ */ new Set();
782
- const columns = input.columns.map((column) => {
783
- const columnName = validateName(column.name, "Column");
784
- if (names.has(columnName))
785
- throw new TypeError(`Duplicate column: ${columnName}`);
786
- names.add(columnName);
787
- if (!simpleDataTypes.includes(column.type)) {
788
- throw new TypeError(`Unsupported data type: ${column.type}`);
789
- }
790
- if (column.enumValues !== void 0 && column.type !== "string") {
791
- throw new TypeError(`Enum values require a string column: ${columnName}`);
792
- }
793
- const sqlDomain = enumDomains.get(column.name) ?? column.sqlDomain;
794
- return {
795
- id: this.#createId(),
796
- name: columnName,
797
- type: column.type,
798
- ...column.integer === true ? { integer: true } : {},
799
- ...sqlDomain === void 0 ? {} : { sqlDomain: structuredClone(sqlDomain) },
800
- nullable: column.nullable ?? false,
801
- ...column.defaultValue === void 0 ? {} : { defaultValue: column.defaultValue },
802
- ...column.generatedValue === void 0 ? {} : { generatedValue: structuredClone(column.generatedValue) },
803
- ...column.enumValues === void 0 ? {} : { enumValues: validateEnumValues(column.enumValues, columnName) },
804
- ...column.backfill === void 0 ? {} : {
805
- backfill: normalizedColumnBackfill({
806
- name: columnName,
807
- type: column.type,
808
- ...column.integer === true ? { integer: true } : {},
809
- ...sqlDomain === void 0 ? {} : { sqlDomain },
810
- ...column.enumValues === void 0 ? {} : { enumValues: column.enumValues }
811
- }, column.backfill)
812
- }
813
- };
814
- });
815
- if (input.uniqueKey !== void 0 && input.compositePrimaryKey !== void 0) {
816
- throw new TypeError("A table cannot declare both scalar and composite primary keys");
817
- }
818
- const compositePrimaryColumns = (input.compositePrimaryKey ?? []).map((columnName) => {
819
- const column = columns.find((candidate) => candidate.name === columnName);
820
- if (column === void 0) {
821
- throw new TypeError(`PRIMARY KEY column not found: ${columnName}`);
822
- }
823
- return column;
824
- });
825
- if (input.compositePrimaryKey !== void 0 && (compositePrimaryColumns.length < 2 || new Set(compositePrimaryColumns.map((column) => column.id)).size !== compositePrimaryColumns.length)) {
826
- throw new TypeError("A composite PRIMARY KEY needs at least two distinct columns");
827
- }
828
- for (const column of compositePrimaryColumns) {
829
- if (column.nullable)
830
- throw new TypeError(`PRIMARY KEY cannot be nullable: ${column.name}`);
831
- }
832
- const hiddenCompositeKey = compositePrimaryColumns.length === 0 ? void 0 : {
833
- id: this.#createId(),
834
- name: COMPOSITE_KEY_COLUMN_NAME,
835
- type: "string",
836
- nullable: false,
837
- hidden: true
838
- };
839
- if (hiddenCompositeKey !== void 0)
840
- columns.push(hiddenCompositeKey);
841
- const uniqueKeyColumn = hiddenCompositeKey ?? (input.uniqueKey === void 0 ? void 0 : columns.find((column) => column.name === input.uniqueKey));
842
- if (input.uniqueKey !== void 0 && uniqueKeyColumn === void 0) {
843
- throw new TypeError(`Unique key column not found: ${input.uniqueKey}`);
844
- }
845
- if (uniqueKeyColumn?.nullable === true) {
846
- throw new TypeError(`Unique key cannot be nullable: ${uniqueKeyColumn.name}`);
847
- }
848
- for (const column of columns) {
849
- if (column.generatedValue !== void 0) {
850
- if (column.defaultValue !== void 0 || column.backfill !== void 0) {
851
- throw new TypeError(`A generated column cannot also have a default or backfill: ${name}.${column.name}`);
852
- }
853
- compileGeneratedColumnExpression(name, column.name, column.generatedValue.sql, columns);
854
- if (column === uniqueKeyColumn || compositePrimaryColumns.includes(column)) {
855
- throw new TypeError(`Generated columns cannot be row-addressing keys: ${name}.${column.name}`);
856
- }
785
+ return this.#foreground(async () => {
786
+ const name = validateName(input.name, "Table");
787
+ if (input.columns.length === 0)
788
+ throw new TypeError("A table needs at least one column");
789
+ const enumDomains = /* @__PURE__ */ new Map();
790
+ for (const column of input.columns) {
791
+ if (column.sqlDomain?.kind !== "enum" || column.sqlDomain.values.length > 0)
792
+ continue;
793
+ const record = await this.store.getTableByName(`${ENUM_TYPE_PREFIX}${column.sqlDomain.name}`);
794
+ if (record?.enumType === void 0) {
795
+ throw new TypeError(`Unsupported column type: ${column.sqlDomain.name}`);
796
+ }
797
+ enumDomains.set(column.name, {
798
+ kind: "enum",
799
+ name: record.enumType.name,
800
+ values: [...record.enumType.values]
801
+ });
857
802
  }
858
- if (column.defaultValue !== void 0) {
859
- validateColumnDefault({ ...column, isUniqueKey: column === uniqueKeyColumn }, column.defaultValue);
860
- if (column.defaultValue.kind === "literal" && column.sqlDomain !== void 0) {
861
- normalizeSqlDomainValue(column.sqlDomain, column.defaultValue.value);
803
+ const names = /* @__PURE__ */ new Set();
804
+ const columns = input.columns.map((column) => {
805
+ const columnName = validateName(column.name, "Column");
806
+ if (names.has(columnName))
807
+ throw new TypeError(`Duplicate column: ${columnName}`);
808
+ names.add(columnName);
809
+ if (!simpleDataTypes.includes(column.type)) {
810
+ throw new TypeError(`Unsupported data type: ${column.type}`);
862
811
  }
863
- if (column.defaultValue.kind === "expression") {
864
- validateDefaultExpression(column.defaultValue.sql, {
865
- name: `${name}.${column.name}`,
866
- type: column.type,
867
- ...column.sqlDomain === void 0 ? {} : { sqlDomain: column.sqlDomain }
868
- });
812
+ if (column.enumValues !== void 0 && column.type !== "string") {
813
+ throw new TypeError(`Enum values require a string column: ${columnName}`);
869
814
  }
815
+ const sqlDomain = enumDomains.get(column.name) ?? column.sqlDomain;
816
+ return {
817
+ id: this.#createId(),
818
+ name: columnName,
819
+ type: column.type,
820
+ ...column.integer === true ? { integer: true } : {},
821
+ ...sqlDomain === void 0 ? {} : { sqlDomain: structuredClone(sqlDomain) },
822
+ nullable: column.nullable ?? false,
823
+ ...column.defaultValue === void 0 ? {} : { defaultValue: column.defaultValue },
824
+ ...column.generatedValue === void 0 ? {} : { generatedValue: structuredClone(column.generatedValue) },
825
+ ...column.enumValues === void 0 ? {} : { enumValues: validateEnumValues(column.enumValues, columnName) },
826
+ ...column.backfill === void 0 ? {} : {
827
+ backfill: normalizedColumnBackfill({
828
+ name: columnName,
829
+ type: column.type,
830
+ ...column.integer === true ? { integer: true } : {},
831
+ ...sqlDomain === void 0 ? {} : { sqlDomain },
832
+ ...column.enumValues === void 0 ? {} : { enumValues: column.enumValues }
833
+ }, column.backfill)
834
+ }
835
+ };
836
+ });
837
+ if (input.uniqueKey !== void 0 && input.compositePrimaryKey !== void 0) {
838
+ throw new TypeError("A table cannot declare both scalar and composite primary keys");
870
839
  }
871
- }
872
- const constraintNames = /* @__PURE__ */ new Set();
873
- const claimConstraintName = (rawName) => {
874
- const constraintName = validateName(rawName, "Constraint");
875
- if (constraintNames.has(constraintName)) {
876
- throw new TypeError(`Constraint already exists: ${constraintName}`);
877
- }
878
- constraintNames.add(constraintName);
879
- return constraintName;
880
- };
881
- const foreignKeyNames = (input.foreignKeys ?? []).map((key) => claimConstraintName(key.name));
882
- const checkNames = (input.checks ?? []).map((check) => claimConstraintName(check.name));
883
- const uniqueConstraintNames = (input.uniqueConstraints ?? []).map((constraint) => claimConstraintName(constraint.name));
884
- const foreignKeys = await Promise.all((input.foreignKeys ?? []).map(async (key, keyIndex) => {
885
- if (key.enforced === false && key.onDelete !== "restrict") {
886
- throw new TypeError(`Informational FOREIGN KEY ${key.name} cannot declare ON DELETE actions`);
887
- }
888
- const childNames = key.columns ?? [key.column];
889
- const children = childNames.map((columnName) => {
840
+ const compositePrimaryColumns = (input.compositePrimaryKey ?? []).map((columnName) => {
890
841
  const column = columns.find((candidate) => candidate.name === columnName);
891
842
  if (column === void 0) {
892
- throw new TypeError(`FOREIGN KEY ${key.name} names a column this table has no: ${columnName}`);
843
+ throw new TypeError(`PRIMARY KEY column not found: ${columnName}`);
893
844
  }
894
845
  return column;
895
846
  });
896
- const parent = key.parentTable === name ? void 0 : await this.store.getTableByName(key.parentTable);
897
- if (key.parentTable !== name && parent === void 0) {
898
- throw new TypeError(`FOREIGN KEY ${key.name} references a table that does not exist: ${key.parentTable}`);
847
+ if (input.compositePrimaryKey !== void 0 && (compositePrimaryColumns.length < 2 || new Set(compositePrimaryColumns.map((column) => column.id)).size !== compositePrimaryColumns.length)) {
848
+ throw new TypeError("A composite PRIMARY KEY needs at least two distinct columns");
899
849
  }
900
- const parentColumns = parent === void 0 ? columns : parent.columns;
901
- const parentPrimaryIds = parent === void 0 ? compositePrimaryColumns.length > 0 ? compositePrimaryColumns.map((column) => column.id) : uniqueKeyColumn === void 0 ? [] : [uniqueKeyColumn.id] : parent.primaryKeyColumnIds ?? (parent.uniqueKeyColumnId === void 0 ? [] : [parent.uniqueKeyColumnId]);
902
- const parentPrimary = parentPrimaryIds.map((columnId) => parentColumns.find((column) => column.id === columnId)).filter((column) => column !== void 0 && !column.hidden);
903
- if (parentPrimary.length === 0) {
904
- throw new TypeError(`FOREIGN KEY ${key.name} references a table with no unique key: ${key.parentTable}`);
850
+ for (const column of compositePrimaryColumns) {
851
+ if (column.nullable)
852
+ throw new TypeError(`PRIMARY KEY cannot be nullable: ${column.name}`);
905
853
  }
906
- const requestedParentNames = key.parentColumns ?? (key.parentColumn === void 0 ? parentPrimary.map((column) => column.name) : [key.parentColumn]);
907
- if (requestedParentNames.length !== parentPrimary.length || requestedParentNames.some((columnName, index) => columnName !== parentPrimary[index]?.name)) {
908
- throw new TypeError(`FOREIGN KEY ${key.name} must reference ${key.parentTable}'s primary key (${parentPrimary.map((column) => column.name).join(", ")})`);
854
+ const hiddenCompositeKey = compositePrimaryColumns.length === 0 ? void 0 : {
855
+ id: this.#createId(),
856
+ name: COMPOSITE_KEY_COLUMN_NAME,
857
+ type: "string",
858
+ nullable: false,
859
+ hidden: true
860
+ };
861
+ if (hiddenCompositeKey !== void 0)
862
+ columns.push(hiddenCompositeKey);
863
+ const uniqueKeyColumn = hiddenCompositeKey ?? (input.uniqueKey === void 0 ? void 0 : columns.find((column) => column.name === input.uniqueKey));
864
+ if (input.uniqueKey !== void 0 && uniqueKeyColumn === void 0) {
865
+ throw new TypeError(`Unique key column not found: ${input.uniqueKey}`);
909
866
  }
910
- if (children.length !== parentPrimary.length) {
911
- throw new TypeError(`FOREIGN KEY ${key.name} has ${String(children.length)} child columns for ${String(parentPrimary.length)} parent columns`);
867
+ if (uniqueKeyColumn?.nullable === true) {
868
+ throw new TypeError(`Unique key cannot be nullable: ${uniqueKeyColumn.name}`);
912
869
  }
913
- children.forEach((child, index) => {
914
- const parentKey = parentPrimary[index];
915
- if (parentKey === void 0) {
916
- throw new TypeError(`FOREIGN KEY ${key.name} is missing a parent key column`);
917
- }
918
- if (child.type !== parentKey.type) {
919
- throw new TypeError(`FOREIGN KEY ${key.name} compares ${child.type} with ${parentKey.type}`);
920
- }
921
- if (child.integer === true !== (parentKey.integer === true)) {
922
- throw new TypeError(`FOREIGN KEY ${key.name} compares an integer domain with an approximate number domain`);
870
+ for (const column of columns) {
871
+ if (column.generatedValue !== void 0) {
872
+ if (column.defaultValue !== void 0 || column.backfill !== void 0) {
873
+ throw new TypeError(`A generated column cannot also have a default or backfill: ${name}.${column.name}`);
874
+ }
875
+ compileGeneratedColumnExpression(name, column.name, column.generatedValue.sql, columns);
876
+ if (column === uniqueKeyColumn || compositePrimaryColumns.includes(column)) {
877
+ throw new TypeError(`Generated columns cannot be row-addressing keys: ${name}.${column.name}`);
878
+ }
923
879
  }
924
- if (JSON.stringify(child.sqlDomain ?? null) !== JSON.stringify(parentKey.sqlDomain ?? null)) {
925
- throw new TypeError(`FOREIGN KEY ${key.name} compares different SQL value domains`);
880
+ if (column.defaultValue !== void 0) {
881
+ validateColumnDefault({ ...column, isUniqueKey: column === uniqueKeyColumn }, column.defaultValue);
882
+ if (column.defaultValue.kind === "literal" && column.sqlDomain !== void 0) {
883
+ normalizeSqlDomainValue(column.sqlDomain, column.defaultValue.value);
884
+ }
885
+ if (column.defaultValue.kind === "expression") {
886
+ validateDefaultExpression(column.defaultValue.sql, {
887
+ name: `${name}.${column.name}`,
888
+ type: column.type,
889
+ ...column.sqlDomain === void 0 ? {} : { sqlDomain: column.sqlDomain }
890
+ });
891
+ }
926
892
  }
927
- if (key.onDelete === "set null" && !child.nullable) {
928
- throw new TypeError(`FOREIGN KEY ${key.name} cannot SET NULL a NOT NULL column`);
893
+ }
894
+ const constraintNames = /* @__PURE__ */ new Set();
895
+ const claimConstraintName = (rawName) => {
896
+ const constraintName = validateName(rawName, "Constraint");
897
+ if (constraintNames.has(constraintName)) {
898
+ throw new TypeError(`Constraint already exists: ${constraintName}`);
929
899
  }
930
- });
931
- const parentNames = parentPrimary.map((column) => column.name);
932
- return {
933
- name: foreignKeyNames[keyIndex] ?? key.name,
934
- columns: childNames,
935
- parentTable: key.parentTable,
936
- parentColumns: parentNames,
937
- onDelete: key.onDelete,
938
- ...key.enforced === false ? { enforced: false } : {}
900
+ constraintNames.add(constraintName);
901
+ return constraintName;
939
902
  };
940
- }));
941
- const checks = (input.checks ?? []).map((check, checkIndex) => {
942
- const referencedColumns2 = expressionColumnNames(compileCheckExpression(check.sql, check.name));
943
- for (const reference of referencedColumns2) {
944
- const pieces = reference.split(".");
945
- const columnName = pieces.at(-1) ?? reference;
946
- const qualifier = pieces.length > 1 ? pieces.slice(0, -1).join(".") : void 0;
947
- if (qualifier !== void 0 && qualifier !== name) {
948
- throw new TypeError(`CHECK ${check.name} references another table: ${reference}`);
903
+ const foreignKeyNames = (input.foreignKeys ?? []).map((key) => claimConstraintName(key.name));
904
+ const checkNames = (input.checks ?? []).map((check) => claimConstraintName(check.name));
905
+ const uniqueConstraintNames = (input.uniqueConstraints ?? []).map((constraint) => claimConstraintName(constraint.name));
906
+ const foreignKeys = await Promise.all((input.foreignKeys ?? []).map(async (key, keyIndex) => {
907
+ if (key.enforced === false && key.onDelete !== "restrict") {
908
+ throw new TypeError(`Informational FOREIGN KEY ${key.name} cannot declare ON DELETE actions`);
909
+ }
910
+ const childNames = key.columns ?? [key.column];
911
+ const children = childNames.map((columnName) => {
912
+ const column = columns.find((candidate) => candidate.name === columnName);
913
+ if (column === void 0) {
914
+ throw new TypeError(`FOREIGN KEY ${key.name} names a column this table has no: ${columnName}`);
915
+ }
916
+ return column;
917
+ });
918
+ const parent = key.parentTable === name ? void 0 : await this.store.getTableByName(key.parentTable);
919
+ if (key.parentTable !== name && parent === void 0) {
920
+ throw new TypeError(`FOREIGN KEY ${key.name} references a table that does not exist: ${key.parentTable}`);
921
+ }
922
+ const parentColumns = parent === void 0 ? columns : parent.columns;
923
+ const parentPrimaryIds = parent === void 0 ? compositePrimaryColumns.length > 0 ? compositePrimaryColumns.map((column) => column.id) : uniqueKeyColumn === void 0 ? [] : [uniqueKeyColumn.id] : parent.primaryKeyColumnIds ?? (parent.uniqueKeyColumnId === void 0 ? [] : [parent.uniqueKeyColumnId]);
924
+ const parentPrimary = parentPrimaryIds.map((columnId) => parentColumns.find((column) => column.id === columnId)).filter((column) => column !== void 0 && !column.hidden);
925
+ if (parentPrimary.length === 0) {
926
+ throw new TypeError(`FOREIGN KEY ${key.name} references a table with no unique key: ${key.parentTable}`);
927
+ }
928
+ const requestedParentNames = key.parentColumns ?? (key.parentColumn === void 0 ? parentPrimary.map((column) => column.name) : [key.parentColumn]);
929
+ if (requestedParentNames.length !== parentPrimary.length || requestedParentNames.some((columnName, index) => columnName !== parentPrimary[index]?.name)) {
930
+ throw new TypeError(`FOREIGN KEY ${key.name} must reference ${key.parentTable}'s primary key (${parentPrimary.map((column) => column.name).join(", ")})`);
931
+ }
932
+ if (children.length !== parentPrimary.length) {
933
+ throw new TypeError(`FOREIGN KEY ${key.name} has ${String(children.length)} child columns for ${String(parentPrimary.length)} parent columns`);
934
+ }
935
+ children.forEach((child, index) => {
936
+ const parentKey = parentPrimary[index];
937
+ if (parentKey === void 0) {
938
+ throw new TypeError(`FOREIGN KEY ${key.name} is missing a parent key column`);
939
+ }
940
+ if (child.type !== parentKey.type) {
941
+ throw new TypeError(`FOREIGN KEY ${key.name} compares ${child.type} with ${parentKey.type}`);
942
+ }
943
+ if (child.integer === true !== (parentKey.integer === true)) {
944
+ throw new TypeError(`FOREIGN KEY ${key.name} compares an integer domain with an approximate number domain`);
945
+ }
946
+ if (JSON.stringify(child.sqlDomain ?? null) !== JSON.stringify(parentKey.sqlDomain ?? null)) {
947
+ throw new TypeError(`FOREIGN KEY ${key.name} compares different SQL value domains`);
948
+ }
949
+ if (key.onDelete === "set null" && !child.nullable) {
950
+ throw new TypeError(`FOREIGN KEY ${key.name} cannot SET NULL a NOT NULL column`);
951
+ }
952
+ });
953
+ const parentNames = parentPrimary.map((column) => column.name);
954
+ return {
955
+ name: foreignKeyNames[keyIndex] ?? key.name,
956
+ columns: childNames,
957
+ parentTable: key.parentTable,
958
+ parentColumns: parentNames,
959
+ onDelete: key.onDelete,
960
+ ...key.enforced === false ? { enforced: false } : {}
961
+ };
962
+ }));
963
+ const checks = (input.checks ?? []).map((check, checkIndex) => {
964
+ const referencedColumns2 = expressionColumnNames(compileCheckExpression(check.sql, check.name));
965
+ for (const reference of referencedColumns2) {
966
+ const pieces = reference.split(".");
967
+ const columnName = pieces.at(-1) ?? reference;
968
+ const qualifier = pieces.length > 1 ? pieces.slice(0, -1).join(".") : void 0;
969
+ if (qualifier !== void 0 && qualifier !== name) {
970
+ throw new TypeError(`CHECK ${check.name} references another table: ${reference}`);
971
+ }
972
+ if (!columns.some((column) => column.name === columnName && !column.hidden)) {
973
+ throw new TypeError(`CHECK ${check.name} names an unknown column: ${columnName}`);
974
+ }
949
975
  }
950
- if (!columns.some((column) => column.name === columnName && !column.hidden)) {
951
- throw new TypeError(`CHECK ${check.name} names an unknown column: ${columnName}`);
976
+ return { name: checkNames[checkIndex] ?? check.name, sql: check.sql };
977
+ });
978
+ const tableId = this.#createId();
979
+ const currentVersion = (await this.store.getCurrentManifest())?.version ?? -1;
980
+ const secondaryIndexes = {};
981
+ for (const [constraintIndex, constraint] of (input.uniqueConstraints ?? []).entries()) {
982
+ const indexName = uniqueConstraintNames[constraintIndex] ?? constraint.name;
983
+ const indexedColumns = constraint.columns.map((columnName) => {
984
+ const column = columns.find((candidate) => candidate.name === columnName && !candidate.hidden);
985
+ if (column === void 0) {
986
+ throw new TypeError(`UNIQUE ${indexName} names an unknown column: ${columnName}`);
987
+ }
988
+ return column;
989
+ });
990
+ if (indexedColumns.length === 0 || new Set(indexedColumns.map((column) => column.id)).size !== indexedColumns.length) {
991
+ throw new TypeError(`UNIQUE ${indexName} needs distinct columns`);
952
992
  }
993
+ const indexId = this.#createId();
994
+ secondaryIndexes[indexId] = {
995
+ name: indexName,
996
+ columnId: indexedColumns[0]?.id ?? "",
997
+ columnIds: indexedColumns.map((column) => column.id),
998
+ directions: indexedColumns.map(() => "asc"),
999
+ unique: true,
1000
+ uniqueEnforced: true,
1001
+ termEncoding: "tuple-v1",
1002
+ storage: "postings-v1",
1003
+ storageColumnId: this.#createId(),
1004
+ locator: uniqueKeyColumn === void 0 ? "row-id" : "key-hash-v1",
1005
+ state: "ready",
1006
+ buildFromVersion: currentVersion
1007
+ };
953
1008
  }
954
- return { name: checkNames[checkIndex] ?? check.name, sql: check.sql };
955
- });
956
- const tableId = this.#createId();
957
- const currentVersion = (await this.store.getCurrentManifest())?.version ?? -1;
958
- const secondaryIndexes = {};
959
- for (const [constraintIndex, constraint] of (input.uniqueConstraints ?? []).entries()) {
960
- const indexName = uniqueConstraintNames[constraintIndex] ?? constraint.name;
961
- const indexedColumns = constraint.columns.map((columnName) => {
962
- const column = columns.find((candidate) => candidate.name === columnName && !candidate.hidden);
963
- if (column === void 0) {
964
- throw new TypeError(`UNIQUE ${indexName} names an unknown column: ${columnName}`);
965
- }
966
- return column;
1009
+ await this.store.addTable({
1010
+ id: tableId,
1011
+ name,
1012
+ columns,
1013
+ revision: 0,
1014
+ managed: input.managed === true,
1015
+ ...foreignKeys.length === 0 ? {} : { foreignKeys },
1016
+ ...checks.length === 0 ? {} : { checks },
1017
+ ...uniqueKeyColumn === void 0 ? {} : { uniqueKeyColumnId: uniqueKeyColumn.id },
1018
+ ...uniqueKeyColumn === void 0 ? {} : { uniqueKeyLookupReady: true },
1019
+ ...compositePrimaryColumns.length === 0 ? {} : { primaryKeyColumnIds: compositePrimaryColumns.map((column) => column.id) },
1020
+ ...Object.keys(secondaryIndexes).length === 0 ? {} : { secondaryIndexes },
1021
+ createdAt: dateIsoString(this.#now())
967
1022
  });
968
- if (indexedColumns.length === 0 || new Set(indexedColumns.map((column) => column.id)).size !== indexedColumns.length) {
969
- throw new TypeError(`UNIQUE ${indexName} needs distinct columns`);
970
- }
971
- const indexId = this.#createId();
972
- secondaryIndexes[indexId] = {
973
- name: indexName,
974
- columnId: indexedColumns[0]?.id ?? "",
975
- columnIds: indexedColumns.map((column) => column.id),
976
- directions: indexedColumns.map(() => "asc"),
977
- unique: true,
978
- uniqueEnforced: true,
979
- termEncoding: "tuple-v1",
980
- storage: "postings-v1",
981
- storageColumnId: this.#createId(),
982
- locator: uniqueKeyColumn === void 0 ? "row-id" : "key-hash-v1",
983
- state: "ready",
984
- buildFromVersion: currentVersion
985
- };
986
- }
987
- await this.store.addTable({
988
- id: tableId,
989
- name,
990
- columns,
991
- revision: 0,
992
- managed: input.managed === true,
993
- ...foreignKeys.length === 0 ? {} : { foreignKeys },
994
- ...checks.length === 0 ? {} : { checks },
995
- ...uniqueKeyColumn === void 0 ? {} : { uniqueKeyColumnId: uniqueKeyColumn.id },
996
- ...uniqueKeyColumn === void 0 ? {} : { uniqueKeyLookupReady: true },
997
- ...compositePrimaryColumns.length === 0 ? {} : { primaryKeyColumnIds: compositePrimaryColumns.map((column) => column.id) },
998
- ...Object.keys(secondaryIndexes).length === 0 ? {} : { secondaryIndexes },
999
- createdAt: dateIsoString(this.#now())
1000
1023
  });
1001
1024
  }
1002
1025
  async introspect() {
1003
- return toCatalog(await this.store.listTables());
1026
+ return this.#foreground(async () => {
1027
+ return toCatalog(await this.store.listTables());
1028
+ });
1004
1029
  }
1005
1030
  async listTables() {
1006
- return (await this.store.listTables()).filter((table) => table.enumType === void 0 && table.sequence === void 0).map((table) => {
1007
- const uniqueKey = table.columns.find((column) => column.id === table.uniqueKeyColumnId)?.name;
1008
- return {
1009
- name: table.name,
1010
- columns: visibleTableColumns(table).map((column) => ({
1011
- name: column.name,
1012
- type: column.type,
1013
- ...column.integer === true ? { integer: true } : {},
1014
- ...column.sqlDomain === void 0 ? {} : { sqlDomain: structuredClone(column.sqlDomain) },
1015
- nullable: column.nullable,
1016
- ...column.defaultValue === void 0 ? {} : { defaultValue: column.defaultValue },
1017
- ...column.generatedValue === void 0 ? {} : { generatedValue: column.generatedValue },
1018
- ...column.enumValues === void 0 ? {} : { enumValues: [...column.enumValues] }
1019
- })),
1020
- ...uniqueKey === void 0 ? {} : { uniqueKey }
1021
- };
1031
+ return this.#foreground(async () => {
1032
+ return (await this.store.listTables()).filter((table) => table.enumType === void 0 && table.sequence === void 0).map((table) => {
1033
+ const uniqueKey = table.columns.find((column) => column.id === table.uniqueKeyColumnId)?.name;
1034
+ return {
1035
+ name: table.name,
1036
+ columns: visibleTableColumns(table).map((column) => ({
1037
+ name: column.name,
1038
+ type: column.type,
1039
+ ...column.integer === true ? { integer: true } : {},
1040
+ ...column.sqlDomain === void 0 ? {} : { sqlDomain: structuredClone(column.sqlDomain) },
1041
+ nullable: column.nullable,
1042
+ ...column.defaultValue === void 0 ? {} : { defaultValue: column.defaultValue },
1043
+ ...column.generatedValue === void 0 ? {} : { generatedValue: column.generatedValue },
1044
+ ...column.enumValues === void 0 ? {} : { enumValues: [...column.enumValues] }
1045
+ })),
1046
+ ...uniqueKey === void 0 ? {} : { uniqueKey }
1047
+ };
1048
+ });
1022
1049
  });
1023
1050
  }
1024
1051
  async #runWrite(run) {
@@ -1047,7 +1074,8 @@ class MinnowDatabase {
1047
1074
  }
1048
1075
  };
1049
1076
  const previous = this.#writeChain;
1050
- const current = previous.then(restarting, restarting);
1077
+ const coordinated = () => this.#coordinateWrite(restarting);
1078
+ const current = previous.then(coordinated, coordinated);
1051
1079
  this.#writeChain = current;
1052
1080
  try {
1053
1081
  return await current;
@@ -1108,6 +1136,10 @@ class MinnowDatabase {
1108
1136
  await Promise.race([this.#eventLoopTurn(), this.#nextLocalCommit()]);
1109
1137
  }
1110
1138
  async #withWriteReservation(run) {
1139
+ if (this.#closed)
1140
+ throw new Error("Database is closed");
1141
+ if (this.#statementTransactionExpired)
1142
+ throw new TransactionExpiredError();
1111
1143
  if (this.#pendingWriteReservations >= MAX_DATABASE_PENDING_WRITES) {
1112
1144
  throw new RangeError(`A database cannot queue more than ${String(MAX_DATABASE_PENDING_WRITES)} writes; await a write for backpressure`);
1113
1145
  }
@@ -1118,7 +1150,21 @@ class MinnowDatabase {
1118
1150
  this.#pendingWriteReservations -= 1;
1119
1151
  }
1120
1152
  }
1153
+ #foregroundTasks = /* @__PURE__ */ new Set();
1154
+ async #foreground(run) {
1155
+ if (this.#closed)
1156
+ throw new Error("Database is closed");
1157
+ const task = run();
1158
+ this.#foregroundTasks.add(task);
1159
+ try {
1160
+ return await task;
1161
+ } finally {
1162
+ this.#foregroundTasks.delete(task);
1163
+ }
1164
+ }
1121
1165
  async #withReadReservation(run) {
1166
+ if (this.#closed)
1167
+ throw new Error("Database is closed");
1122
1168
  if (this.#activeReadReservations >= MAX_DATABASE_ACTIVE_READS) {
1123
1169
  throw new DatabaseReadBacklogError(MAX_DATABASE_ACTIVE_READS);
1124
1170
  }
@@ -1130,8 +1176,12 @@ class MinnowDatabase {
1130
1176
  }
1131
1177
  }
1132
1178
  async #queryWithReadReservation(sql, options, probe, storeMemo = true) {
1179
+ if (this.#closed)
1180
+ throw new Error("Database is closed");
1133
1181
  throwIfAborted(options.signal);
1134
1182
  await this.#settleExpiredStatementTransaction();
1183
+ if (this.#statementTransactionExpired)
1184
+ throw new TransactionExpiredError();
1135
1185
  throwIfAborted(options.signal);
1136
1186
  return this.#openTransaction === void 0 ? this.#withReadReservation(() => this.#queryUnreserved(sql, options, probe, storeMemo)) : this.#queryUnreserved(sql, options, probe, storeMemo);
1137
1187
  }
@@ -1190,116 +1240,120 @@ class MinnowDatabase {
1190
1240
  return error;
1191
1241
  }
1192
1242
  async createView(name, sql, options = {}) {
1193
- const viewName = validateName(name, "View");
1194
- const plan = compileQuery(sql);
1195
- let pinnedExistingId;
1196
- for (let attempt = 0; ; attempt += 1) {
1197
- const proof = await this.#stableCatalogProof();
1198
- const existing = proof.records.find((record) => record.name === viewName);
1199
- const existingId = existing?.id ?? null;
1200
- if (pinnedExistingId === void 0)
1201
- pinnedExistingId = existingId;
1202
- else if (pinnedExistingId !== existingId) {
1203
- throw new TableRecordConflictError(viewName, 0, existing?.revision ?? null);
1204
- }
1205
- if (existing !== void 0) {
1206
- if (existing.view === void 0)
1207
- throw new TypeError(`Table already exists: ${viewName}`);
1208
- if (options.orReplace !== true)
1209
- throw new TypeError(`View already exists: ${viewName}`);
1210
- }
1211
- assertViewDefinitionAcyclic(proof.records, viewName, plan);
1212
- const schemas = catalogQuerySchemas(proof.records);
1213
- const inferred = inferBlockSchema(plan, schemas);
1214
- if (inferred.length === 0) {
1215
- throw new TypeError(`A view needs at least one column: ${viewName}`);
1216
- }
1217
- if (existing !== void 0) {
1218
- schemas.set(viewName, inferred);
1219
- assertDependentViewsKeepSchema(proof.records, existing, schemas);
1220
- }
1221
- const columns = inferred.map((column, index) => {
1222
- const previous = existing?.columns[index];
1223
- return {
1224
- id: previous !== void 0 && querySchemasEqual([column], [previous]) ? previous.id : this.#createId(),
1225
- name: column.name,
1226
- type: column.type,
1227
- ...column.integer === true ? { integer: true } : {},
1228
- ...column.sqlDomain === void 0 ? {} : { sqlDomain: column.sqlDomain },
1229
- nullable: true
1230
- };
1231
- });
1232
- const view = { sql, managed: options.managed === true };
1233
- try {
1243
+ return this.#foreground(async () => {
1244
+ const viewName = validateName(name, "View");
1245
+ const plan = compileQuery(sql);
1246
+ let pinnedExistingId;
1247
+ for (let attempt = 0; ; attempt += 1) {
1248
+ const proof = await this.#stableCatalogProof();
1249
+ const existing = proof.records.find((record) => record.name === viewName);
1250
+ const existingId = existing?.id ?? null;
1251
+ if (pinnedExistingId === void 0)
1252
+ pinnedExistingId = existingId;
1253
+ else if (pinnedExistingId !== existingId) {
1254
+ throw new TableRecordConflictError(viewName, 0, existing?.revision ?? null);
1255
+ }
1234
1256
  if (existing !== void 0) {
1235
- await this.store.updateTable(existing.id, existing.revision, {
1236
- columns,
1237
- ftsColumns: null,
1238
- secondaryIndexes: null,
1239
- triggers: null,
1240
- view,
1241
- expectedCatalogEpoch: proof.catalogEpoch
1242
- });
1243
- } else {
1244
- await this.store.addTable({
1245
- id: this.#createId(),
1246
- name: viewName,
1247
- columns,
1248
- view,
1249
- managed: false,
1250
- revision: 0,
1251
- createdAt: dateIsoString(this.#now())
1252
- }, { expectedCatalogEpoch: proof.catalogEpoch });
1257
+ if (existing.view === void 0)
1258
+ throw new TypeError(`Table already exists: ${viewName}`);
1259
+ if (options.orReplace !== true)
1260
+ throw new TypeError(`View already exists: ${viewName}`);
1253
1261
  }
1254
- this.#planCache.clear();
1255
- return;
1256
- } catch (error) {
1257
- if (!(error instanceof TableRecordConflictError) || attempt >= this.#maxCommitRetries) {
1258
- throw error;
1262
+ assertViewDefinitionAcyclic(proof.records, viewName, plan);
1263
+ const schemas = catalogQuerySchemas(proof.records);
1264
+ const inferred = inferBlockSchema(plan, schemas);
1265
+ if (inferred.length === 0) {
1266
+ throw new TypeError(`A view needs at least one column: ${viewName}`);
1259
1267
  }
1260
1268
  if (existing !== void 0) {
1261
- const current = await this.store.getTable(existing.id);
1262
- if (current?.revision !== existing.revision)
1269
+ schemas.set(viewName, inferred);
1270
+ assertDependentViewsKeepSchema(proof.records, existing, schemas);
1271
+ }
1272
+ const columns = inferred.map((column, index) => {
1273
+ const previous = existing?.columns[index];
1274
+ return {
1275
+ id: previous !== void 0 && querySchemasEqual([column], [previous]) ? previous.id : this.#createId(),
1276
+ name: column.name,
1277
+ type: column.type,
1278
+ ...column.integer === true ? { integer: true } : {},
1279
+ ...column.sqlDomain === void 0 ? {} : { sqlDomain: column.sqlDomain },
1280
+ nullable: true
1281
+ };
1282
+ });
1283
+ const view = { sql, managed: options.managed === true };
1284
+ try {
1285
+ if (existing !== void 0) {
1286
+ await this.store.updateTable(existing.id, existing.revision, {
1287
+ columns,
1288
+ ftsColumns: null,
1289
+ secondaryIndexes: null,
1290
+ triggers: null,
1291
+ view,
1292
+ expectedCatalogEpoch: proof.catalogEpoch
1293
+ });
1294
+ } else {
1295
+ await this.store.addTable({
1296
+ id: this.#createId(),
1297
+ name: viewName,
1298
+ columns,
1299
+ view,
1300
+ managed: false,
1301
+ revision: 0,
1302
+ createdAt: dateIsoString(this.#now())
1303
+ }, { expectedCatalogEpoch: proof.catalogEpoch });
1304
+ }
1305
+ this.#planCache.clear();
1306
+ return;
1307
+ } catch (error) {
1308
+ if (!(error instanceof TableRecordConflictError) || attempt >= this.#maxCommitRetries) {
1263
1309
  throw error;
1310
+ }
1311
+ if (existing !== void 0) {
1312
+ const current = await this.store.getTable(existing.id);
1313
+ if (current?.revision !== existing.revision)
1314
+ throw error;
1315
+ }
1264
1316
  }
1265
1317
  }
1266
- }
1318
+ });
1267
1319
  }
1268
1320
  async dropView(name, options = {}) {
1269
- let pinnedId;
1270
- for (let attempt = 0; ; attempt += 1) {
1271
- const proof = await this.#stableCatalogProof();
1272
- const record = proof.records.find((candidate) => candidate.name === name);
1273
- if (record?.view === void 0) {
1274
- if (pinnedId !== void 0) {
1275
- throw new TableRecordConflictError(pinnedId, 0, record?.revision ?? null);
1321
+ return this.#foreground(async () => {
1322
+ let pinnedId;
1323
+ for (let attempt = 0; ; attempt += 1) {
1324
+ const proof = await this.#stableCatalogProof();
1325
+ const record = proof.records.find((candidate) => candidate.name === name);
1326
+ if (record?.view === void 0) {
1327
+ if (pinnedId !== void 0) {
1328
+ throw new TableRecordConflictError(pinnedId, 0, record?.revision ?? null);
1329
+ }
1330
+ if (record !== void 0)
1331
+ throw new TypeError(`Not a view: ${name}`);
1332
+ if (options.ifExists === true)
1333
+ return false;
1334
+ throw new Error(`View not found: ${name}`);
1276
1335
  }
1277
- if (record !== void 0)
1278
- throw new TypeError(`Not a view: ${name}`);
1279
- if (options.ifExists === true)
1280
- return false;
1281
- throw new Error(`View not found: ${name}`);
1282
- }
1283
- pinnedId ??= record.id;
1284
- if (record.id !== pinnedId) {
1285
- throw new TableRecordConflictError(pinnedId, 0, record.revision);
1286
- }
1287
- const dependent = proof.records.find((candidate) => candidate.id !== record.id && candidate.view !== void 0 && viewReadsTable(candidate.view.sql, record.name));
1288
- if (dependent !== void 0) {
1289
- throw new TypeError(`Cannot drop ${record.name}: view ${dependent.name} reads it`);
1290
- }
1291
- try {
1292
- await this.store.removeTable(record.id, record.revision, {
1293
- expectedCatalogEpoch: proof.catalogEpoch
1294
- });
1295
- this.#planCache.clear();
1296
- return true;
1297
- } catch (error) {
1298
- if (!(error instanceof TableRecordConflictError) || attempt >= this.#maxCommitRetries) {
1299
- throw error;
1336
+ pinnedId ??= record.id;
1337
+ if (record.id !== pinnedId) {
1338
+ throw new TableRecordConflictError(pinnedId, 0, record.revision);
1339
+ }
1340
+ const dependent = proof.records.find((candidate) => candidate.id !== record.id && candidate.view !== void 0 && viewReadsTable(candidate.view.sql, record.name));
1341
+ if (dependent !== void 0) {
1342
+ throw new TypeError(`Cannot drop ${record.name}: view ${dependent.name} reads it`);
1343
+ }
1344
+ try {
1345
+ await this.store.removeTable(record.id, record.revision, {
1346
+ expectedCatalogEpoch: proof.catalogEpoch
1347
+ });
1348
+ this.#planCache.clear();
1349
+ return true;
1350
+ } catch (error) {
1351
+ if (!(error instanceof TableRecordConflictError) || attempt >= this.#maxCommitRetries) {
1352
+ throw error;
1353
+ }
1300
1354
  }
1301
1355
  }
1302
- }
1356
+ });
1303
1357
  }
1304
1358
  async #stableCatalogProof() {
1305
1359
  for (let attempt = 0; ; attempt += 1) {
@@ -1319,71 +1373,73 @@ class MinnowDatabase {
1319
1373
  }
1320
1374
  }
1321
1375
  async dropColumn(tableName, columnName, options = {}) {
1322
- let pinnedTableId;
1323
- let pinnedColumnId;
1324
- for (let attempt = 0; ; attempt += 1) {
1325
- const catalogProof = await this.store.getCatalogProbe();
1326
- const records = await this.store.listTables();
1327
- if ((await this.store.getCatalogProbe()).catalogEpoch !== catalogProof.catalogEpoch) {
1328
- if (attempt >= this.#maxCommitRetries) {
1329
- throw new TableRecordConflictError(tableName, 0, null);
1376
+ return this.#foreground(async () => {
1377
+ let pinnedTableId;
1378
+ let pinnedColumnId;
1379
+ for (let attempt = 0; ; attempt += 1) {
1380
+ const catalogProof = await this.store.getCatalogProbe();
1381
+ const records = await this.store.listTables();
1382
+ if ((await this.store.getCatalogProbe()).catalogEpoch !== catalogProof.catalogEpoch) {
1383
+ if (attempt >= this.#maxCommitRetries) {
1384
+ throw new TableRecordConflictError(tableName, 0, null);
1385
+ }
1386
+ continue;
1330
1387
  }
1331
- continue;
1332
- }
1333
- const table = records.find((record) => record.name === tableName);
1334
- if (table === void 0 || table.view !== void 0) {
1335
- if (pinnedTableId !== void 0) {
1336
- throw new TableRecordConflictError(pinnedTableId, 0, table?.revision ?? null);
1388
+ const table = records.find((record) => record.name === tableName);
1389
+ if (table === void 0 || table.view !== void 0) {
1390
+ if (pinnedTableId !== void 0) {
1391
+ throw new TableRecordConflictError(pinnedTableId, 0, table?.revision ?? null);
1392
+ }
1393
+ if (table?.view !== void 0)
1394
+ throw new TypeError(`${tableName} is a view, not a table`);
1395
+ if (options.ifExists === true)
1396
+ return false;
1397
+ throw new UnknownTableError(tableName);
1337
1398
  }
1338
- if (table?.view !== void 0)
1339
- throw new TypeError(`${tableName} is a view, not a table`);
1340
- if (options.ifExists === true)
1341
- return false;
1342
- throw new UnknownTableError(tableName);
1343
- }
1344
- pinnedTableId ??= table.id;
1345
- if (table.id !== pinnedTableId) {
1346
- throw new TableRecordConflictError(pinnedTableId, 0, table.revision);
1347
- }
1348
- const column = table.columns.find((candidate) => candidate.name === columnName);
1349
- if (column === void 0) {
1350
- if (pinnedColumnId !== void 0) {
1399
+ pinnedTableId ??= table.id;
1400
+ if (table.id !== pinnedTableId) {
1401
+ throw new TableRecordConflictError(pinnedTableId, 0, table.revision);
1402
+ }
1403
+ const column = table.columns.find((candidate) => candidate.name === columnName);
1404
+ if (column === void 0) {
1405
+ if (pinnedColumnId !== void 0) {
1406
+ throw new TableRecordConflictError(pinnedTableId, table.revision, table.revision);
1407
+ }
1408
+ if (options.ifExists === true)
1409
+ return false;
1410
+ throw new TypeError(`Column not found: ${tableName}.${columnName}`);
1411
+ }
1412
+ pinnedColumnId ??= column.id;
1413
+ if (column.id !== pinnedColumnId) {
1351
1414
  throw new TableRecordConflictError(pinnedTableId, table.revision, table.revision);
1352
1415
  }
1353
- if (options.ifExists === true)
1354
- return false;
1355
- throw new TypeError(`Column not found: ${tableName}.${columnName}`);
1356
- }
1357
- pinnedColumnId ??= column.id;
1358
- if (column.id !== pinnedColumnId) {
1359
- throw new TableRecordConflictError(pinnedTableId, table.revision, table.revision);
1360
- }
1361
- this.#assertColumnDropSafe(records, table, column);
1362
- const buildKey = `${table.id}/${column.id}`;
1363
- this.#droppingFtsColumns.add(buildKey);
1364
- try {
1365
- await this.#cancelTableCompactions(table.id);
1366
- const manifest = await this.store.dropTableColumn({
1367
- tableId: table.id,
1368
- columnId: column.id,
1369
- expectedTableRevision: table.revision,
1370
- expectedManifestVersion: await this.store.getCurrentManifestVersion(),
1371
- expectedCatalogEpoch: catalogProof.catalogEpoch,
1372
- committedAt: dateIsoString(this.#now())
1373
- });
1374
- this.#afterCommit(manifest);
1375
- this.#gzipVerdicts.delete(column.id);
1376
- this.#postingDeltaTailCounts.delete(buildKey);
1377
- this.#planCache.clear();
1378
- return true;
1379
- } catch (error) {
1380
- if (!(error instanceof TableRecordConflictError) && !(error instanceof TableInUseError) && !(error instanceof WriteConflictError) || attempt >= this.#maxCommitRetries) {
1381
- throw error;
1416
+ this.#assertColumnDropSafe(records, table, column);
1417
+ const buildKey = `${table.id}/${column.id}`;
1418
+ this.#droppingFtsColumns.add(buildKey);
1419
+ try {
1420
+ await this.#cancelTableCompactions(table.id);
1421
+ const manifest = await this.store.dropTableColumn({
1422
+ tableId: table.id,
1423
+ columnId: column.id,
1424
+ expectedTableRevision: table.revision,
1425
+ expectedManifestVersion: await this.store.getCurrentManifestVersion(),
1426
+ expectedCatalogEpoch: catalogProof.catalogEpoch,
1427
+ committedAt: dateIsoString(this.#now())
1428
+ });
1429
+ this.#afterCommit(manifest);
1430
+ this.#gzipVerdicts.delete(column.id);
1431
+ this.#postingDeltaTailCounts.delete(buildKey);
1432
+ this.#planCache.clear();
1433
+ return true;
1434
+ } catch (error) {
1435
+ if (!(error instanceof TableRecordConflictError) && !(error instanceof TableInUseError) && !(error instanceof WriteConflictError) || attempt >= this.#maxCommitRetries) {
1436
+ throw error;
1437
+ }
1438
+ } finally {
1439
+ this.#droppingFtsColumns.delete(buildKey);
1382
1440
  }
1383
- } finally {
1384
- this.#droppingFtsColumns.delete(buildKey);
1385
1441
  }
1386
- }
1442
+ });
1387
1443
  }
1388
1444
  #assertColumnDropSafe(records, table, column) {
1389
1445
  if (table.columns.length === 1) {
@@ -1433,84 +1489,86 @@ class MinnowDatabase {
1433
1489
  }
1434
1490
  }
1435
1491
  async dropTable(tableName, options = {}) {
1436
- let pinnedTableId;
1437
- for (let attempt = 0; ; attempt += 1) {
1438
- const catalogProof = await this.store.getCatalogProbe();
1439
- const records = await this.store.listTables();
1440
- if ((await this.store.getCatalogProbe()).catalogEpoch !== catalogProof.catalogEpoch) {
1441
- if (attempt >= this.#maxCommitRetries) {
1442
- throw new TableRecordConflictError(tableName, 0, null);
1443
- }
1444
- continue;
1445
- }
1446
- const table = records.find((record) => record.name === tableName);
1447
- if (table === void 0) {
1448
- if (pinnedTableId !== void 0) {
1449
- throw new TableRecordConflictError(pinnedTableId, 0, null);
1492
+ return this.#foreground(async () => {
1493
+ let pinnedTableId;
1494
+ for (let attempt = 0; ; attempt += 1) {
1495
+ const catalogProof = await this.store.getCatalogProbe();
1496
+ const records = await this.store.listTables();
1497
+ if ((await this.store.getCatalogProbe()).catalogEpoch !== catalogProof.catalogEpoch) {
1498
+ if (attempt >= this.#maxCommitRetries) {
1499
+ throw new TableRecordConflictError(tableName, 0, null);
1500
+ }
1501
+ continue;
1450
1502
  }
1451
- if (options.ifExists === true)
1452
- return false;
1453
- throw new UnknownTableError(tableName);
1454
- }
1455
- pinnedTableId ??= table.id;
1456
- if (table.id !== pinnedTableId) {
1457
- throw new TableRecordConflictError(pinnedTableId, 0, table.revision);
1458
- }
1459
- if (table.view !== void 0)
1460
- throw new TypeError(`${tableName} is a view; use DROP VIEW`);
1461
- for (const owner of records) {
1462
- if (owner.id === table.id)
1463
- continue;
1464
- if (owner.view !== void 0 && viewReadsTable(owner.view.sql, table.name)) {
1465
- throw new TypeError(`Cannot drop ${table.name}: view ${owner.name} reads it`);
1503
+ const table = records.find((record) => record.name === tableName);
1504
+ if (table === void 0) {
1505
+ if (pinnedTableId !== void 0) {
1506
+ throw new TableRecordConflictError(pinnedTableId, 0, null);
1507
+ }
1508
+ if (options.ifExists === true)
1509
+ return false;
1510
+ throw new UnknownTableError(tableName);
1466
1511
  }
1467
- for (const key of owner.foreignKeys ?? []) {
1468
- if (key.parentTable === table.name) {
1469
- throw new TypeError(`Cannot drop ${table.name}: foreign key ${key.name} on ${owner.name} references it`);
1512
+ pinnedTableId ??= table.id;
1513
+ if (table.id !== pinnedTableId) {
1514
+ throw new TableRecordConflictError(pinnedTableId, 0, table.revision);
1515
+ }
1516
+ if (table.view !== void 0)
1517
+ throw new TypeError(`${tableName} is a view; use DROP VIEW`);
1518
+ for (const owner of records) {
1519
+ if (owner.id === table.id)
1520
+ continue;
1521
+ if (owner.view !== void 0 && viewReadsTable(owner.view.sql, table.name)) {
1522
+ throw new TypeError(`Cannot drop ${table.name}: view ${owner.name} reads it`);
1523
+ }
1524
+ for (const key of owner.foreignKeys ?? []) {
1525
+ if (key.parentTable === table.name) {
1526
+ throw new TypeError(`Cannot drop ${table.name}: foreign key ${key.name} on ${owner.name} references it`);
1527
+ }
1528
+ }
1529
+ for (const trigger of owner.triggers ?? []) {
1530
+ const writesHere = trigger.statements.some((triggerStatement) => {
1531
+ const compiled = compileStatement(triggerStatement.sql);
1532
+ return (compiled.kind === "insert" || compiled.kind === "update" || compiled.kind === "delete") && compiled.table === table.name;
1533
+ });
1534
+ if (writesHere) {
1535
+ throw new TypeError(`Cannot drop ${table.name}: trigger ${trigger.name} on ${owner.name} writes to it`);
1536
+ }
1470
1537
  }
1471
1538
  }
1472
- for (const trigger of owner.triggers ?? []) {
1473
- const writesHere = trigger.statements.some((triggerStatement) => {
1474
- const compiled = compileStatement(triggerStatement.sql);
1475
- return (compiled.kind === "insert" || compiled.kind === "update" || compiled.kind === "delete") && compiled.table === table.name;
1539
+ this.#droppingTables.add(table.id);
1540
+ try {
1541
+ await this.#cancelTableCompactions(table.id);
1542
+ const manifest = await this.store.dropTable({
1543
+ tableId: table.id,
1544
+ expectedTableRevision: table.revision,
1545
+ expectedManifestVersion: await this.store.getCurrentManifestVersion(),
1546
+ expectedCatalogEpoch: catalogProof.catalogEpoch,
1547
+ committedAt: dateIsoString(this.#now())
1476
1548
  });
1477
- if (writesHere) {
1478
- throw new TypeError(`Cannot drop ${table.name}: trigger ${trigger.name} on ${owner.name} writes to it`);
1549
+ this.#afterCommit(manifest);
1550
+ for (const column of table.columns)
1551
+ this.#gzipVerdicts.delete(column.id);
1552
+ this.#autoCompactionBackoff.delete(table.id);
1553
+ this.#autoCompactionHints.delete(table.id);
1554
+ this.#commitsSinceCompactionCheck.delete(table.id);
1555
+ this.#idleCompactionTableIds.delete(table.id);
1556
+ const postingPrefix = `${table.id}/`;
1557
+ for (const key of this.#postingDeltaTailCounts.keys()) {
1558
+ if (key.startsWith(postingPrefix))
1559
+ this.#postingDeltaTailCounts.delete(key);
1479
1560
  }
1561
+ this.#planCache.clear();
1562
+ return true;
1563
+ } catch (error) {
1564
+ if (!(error instanceof TableRecordConflictError) && !(error instanceof TableInUseError) && !(error instanceof WriteConflictError) || attempt >= this.#maxCommitRetries) {
1565
+ throw error;
1566
+ }
1567
+ } finally {
1568
+ this.#droppingTables.delete(table.id);
1480
1569
  }
1481
1570
  }
1482
- this.#droppingTables.add(table.id);
1483
- try {
1484
- await this.#cancelTableCompactions(table.id);
1485
- const manifest = await this.store.dropTable({
1486
- tableId: table.id,
1487
- expectedTableRevision: table.revision,
1488
- expectedManifestVersion: await this.store.getCurrentManifestVersion(),
1489
- expectedCatalogEpoch: catalogProof.catalogEpoch,
1490
- committedAt: dateIsoString(this.#now())
1491
- });
1492
- this.#afterCommit(manifest);
1493
- for (const column of table.columns)
1494
- this.#gzipVerdicts.delete(column.id);
1495
- this.#autoCompactionBackoff.delete(table.id);
1496
- this.#autoCompactionHints.delete(table.id);
1497
- this.#commitsSinceCompactionCheck.delete(table.id);
1498
- this.#idleCompactionTableIds.delete(table.id);
1499
- const postingPrefix = `${table.id}/`;
1500
- for (const key of this.#postingDeltaTailCounts.keys()) {
1501
- if (key.startsWith(postingPrefix))
1502
- this.#postingDeltaTailCounts.delete(key);
1503
- }
1504
- this.#planCache.clear();
1505
- return true;
1506
- } catch (error) {
1507
- if (!(error instanceof TableRecordConflictError) && !(error instanceof TableInUseError) && !(error instanceof WriteConflictError) || attempt >= this.#maxCommitRetries) {
1508
- throw error;
1509
- }
1510
- } finally {
1511
- this.#droppingTables.delete(table.id);
1512
- }
1513
- }
1571
+ });
1514
1572
  }
1515
1573
  insertBatch(tableName, input) {
1516
1574
  return this.#withWriteReservation(() => this.#insertBatchReserved(tableName, input));
@@ -1569,7 +1627,7 @@ class MinnowDatabase {
1569
1627
  }
1570
1628
  throw new TypeError(`DEFAULT produced an unsupported value: ${sql}`);
1571
1629
  }
1572
- async #materializeInsertExpressions(statement, statementNow) {
1630
+ async #materializeInsertExpressions(statement, statementNow, read) {
1573
1631
  if (!statement.rows.some((row) => row.some(isDeferredInsertExpression)))
1574
1632
  return statement;
1575
1633
  const rows = [];
@@ -1594,7 +1652,7 @@ class MinnowDatabase {
1594
1652
  usesVolatileFunctions: true
1595
1653
  };
1596
1654
  plan = resolveStatementDatetimes(plan, statementNow);
1597
- const result = await this.#queryCompiled(plan, { memoize: false });
1655
+ const result = await read(plan);
1598
1656
  materialized.push(result.rows[0]?.value ?? null);
1599
1657
  }
1600
1658
  rows.push(materialized);
@@ -1602,7 +1660,9 @@ class MinnowDatabase {
1602
1660
  return { ...statement, rows };
1603
1661
  }
1604
1662
  async insert(tableName, row) {
1605
- return this.#erased.insertBatch(tableName, [row]);
1663
+ return this.#foreground(async () => {
1664
+ return this.#erased.insertBatch(tableName, [row]);
1665
+ });
1606
1666
  }
1607
1667
  upsertBatch(tableName, input, options = {}) {
1608
1668
  return this.#withWriteReservation(() => this.#upsertBatchReserved(tableName, input, options));
@@ -1644,7 +1704,9 @@ class MinnowDatabase {
1644
1704
  };
1645
1705
  }
1646
1706
  async upsert(tableName, row, options = {}) {
1647
- return this.#erased.upsertBatch(tableName, [row], options);
1707
+ return this.#foreground(async () => {
1708
+ return this.#erased.upsertBatch(tableName, [row], options);
1709
+ });
1648
1710
  }
1649
1711
  updateBatch(tableName, input) {
1650
1712
  return this.#withWriteReservation(() => this.#updateBatchReserved(tableName, compactUpdateBatchInput(input)));
@@ -1670,13 +1732,17 @@ class MinnowDatabase {
1670
1732
  });
1671
1733
  }
1672
1734
  async update(tableName, key, changes) {
1673
- return this.#erased.updateBatch(tableName, {
1674
- keys: [key],
1675
- changes: Object.fromEntries(Object.entries(changes).flatMap(([name, value]) => value === void 0 ? [] : [[name, [value]]]))
1735
+ return this.#foreground(async () => {
1736
+ return this.#erased.updateBatch(tableName, {
1737
+ keys: [key],
1738
+ changes: Object.fromEntries(Object.entries(changes).flatMap(([name, value]) => value === void 0 ? [] : [[name, [value]]]))
1739
+ });
1676
1740
  });
1677
1741
  }
1678
1742
  async delete(tableName, key) {
1679
- return this.#erased.deleteBatch(tableName, { keys: [key] });
1743
+ return this.#foreground(async () => {
1744
+ return this.#erased.deleteBatch(tableName, { keys: [key] });
1745
+ });
1680
1746
  }
1681
1747
  deleteBatch(tableName, input) {
1682
1748
  return this.#withWriteReservation(() => this.#deleteBatchReserved(tableName, input));
@@ -2326,7 +2392,9 @@ class MinnowDatabase {
2326
2392
  }
2327
2393
  }
2328
2394
  async readTable(tableName, versionOrOptions) {
2329
- return this.#withReadReservation(() => this.#readTableUnreserved(tableName, versionOrOptions));
2395
+ return this.#foreground(async () => {
2396
+ return this.#withReadReservation(() => this.#readTableUnreserved(tableName, versionOrOptions));
2397
+ });
2330
2398
  }
2331
2399
  async #readTableUnreserved(tableName, versionOrOptions) {
2332
2400
  const table = await this.#findTable(tableName);
@@ -2352,21 +2420,50 @@ class MinnowDatabase {
2352
2420
  }).rows;
2353
2421
  }
2354
2422
  async snapshot(action) {
2423
+ return this.#ownScope((signal) => this.#snapshot(action, signal));
2424
+ }
2425
+ async #snapshot(action, signal) {
2355
2426
  for (; ; ) {
2356
2427
  const version = await this.store.getCurrentManifestVersion();
2357
2428
  const entry = await this.#acquireSharedLease(version);
2358
2429
  if (entry === void 0)
2359
2430
  continue;
2431
+ let active = true;
2432
+ let renewalError;
2433
+ let renewal;
2434
+ const renew = () => {
2435
+ renewal ??= this.#renewInternalLeaseIfNeeded(entry.lease).catch((error) => {
2436
+ renewalError = error instanceof Error ? error : new Error("Snapshot renewal failed", { cause: error });
2437
+ }).finally(() => {
2438
+ renewal = void 0;
2439
+ });
2440
+ return renewal;
2441
+ };
2442
+ const timer = setInterval(() => {
2443
+ void renew();
2444
+ }, INTERNAL_READ_LEASE_TTL_MS / 3);
2445
+ timer.unref?.();
2360
2446
  try {
2361
2447
  const session = {
2362
2448
  version,
2363
- query: (sql, options = {}) => this.#queryWithReadReservation(sql, {
2364
- ...options,
2365
- ...version === null ? {} : { version }
2366
- })
2449
+ query: async (sql, options = {}) => {
2450
+ signal.throwIfAborted();
2451
+ if (!active)
2452
+ throw new Error("The snapshot scope has ended");
2453
+ await renew();
2454
+ if (renewalError !== void 0)
2455
+ throw renewalError;
2456
+ return this.#queryWithReadReservation(sql, { ...options, version });
2457
+ }
2367
2458
  };
2368
- return await action(session);
2459
+ const result = await untilAborted(() => action(session), signal);
2460
+ if (renewalError !== void 0)
2461
+ throw renewalError;
2462
+ return result;
2369
2463
  } finally {
2464
+ active = false;
2465
+ clearInterval(timer);
2466
+ await renewal;
2370
2467
  this.#releaseSharedLease(entry);
2371
2468
  }
2372
2469
  }
@@ -2873,7 +2970,7 @@ class MinnowDatabase {
2873
2970
  async #queryUnreserved(sql, options, probe, storeMemo = true) {
2874
2971
  throwIfAborted(options.signal);
2875
2972
  const open = this.#openTransaction;
2876
- if (open !== void 0) {
2973
+ if (open !== void 0 && options.version === void 0) {
2877
2974
  return externalizeQueryResult(await this.#duringTransaction(open, () => open.session.query(sql, options)));
2878
2975
  }
2879
2976
  const compiled = this.#compileCached(sql);
@@ -2903,6 +3000,8 @@ class MinnowDatabase {
2903
3000
  return externalizeQueryResult(result);
2904
3001
  }
2905
3002
  queryCursor(sql, options = {}) {
3003
+ if (this.#closed)
3004
+ throw new Error("Database is closed");
2906
3005
  const batchRows = positiveWholeNumber(options.batchRows ?? 2048, "Query batch rows");
2907
3006
  const controller = new AbortController();
2908
3007
  const channel = new QueryBatchChannel();
@@ -2910,18 +3009,23 @@ class MinnowDatabase {
2910
3009
  void _batchRows;
2911
3010
  let producer;
2912
3011
  const abort = () => {
2913
- const reason = queryCursorError(signal?.reason, "Query cursor is closed");
3012
+ const reason = queryCursorError(signal?.reason ?? this.#shutdown.signal.reason, "Query cursor is closed");
2914
3013
  controller.abort(reason);
2915
3014
  channel.fail(reason);
2916
3015
  };
2917
- signal?.addEventListener("abort", abort, { once: true });
2918
- if (signal?.aborted === true)
2919
- abort();
2920
3016
  const start = () => {
2921
3017
  if (producer !== void 0)
2922
3018
  return;
3019
+ signal?.addEventListener("abort", abort, { once: true });
3020
+ if (signal?.aborted === true)
3021
+ abort();
3022
+ this.#shutdown.signal.addEventListener("abort", abort, { once: true });
3023
+ if (this.#closed)
3024
+ abort();
2923
3025
  if (controller.signal.aborted) {
2924
3026
  producer = Promise.resolve();
3027
+ this.#shutdown.signal.removeEventListener("abort", abort);
3028
+ signal?.removeEventListener("abort", abort);
2925
3029
  return;
2926
3030
  }
2927
3031
  producer = this.#withReadReservation(() => this.#queryBatches(sql, queryOptions, {
@@ -2929,6 +3033,13 @@ class MinnowDatabase {
2929
3033
  signal: controller.signal,
2930
3034
  consume: (batch) => channel.push(batch)
2931
3035
  })).then(() => channel.finish(), (error) => channel.fail(error));
3036
+ const task = producer;
3037
+ this.#foregroundTasks.add(task);
3038
+ void task.finally(() => {
3039
+ this.#foregroundTasks.delete(task);
3040
+ this.#shutdown.signal.removeEventListener("abort", abort);
3041
+ signal?.removeEventListener("abort", abort);
3042
+ });
2932
3043
  };
2933
3044
  const close = async () => {
2934
3045
  if (!controller.signal.aborted)
@@ -2991,15 +3102,9 @@ class MinnowDatabase {
2991
3102
  throwIfAborted(options.signal);
2992
3103
  before ??= await probe();
2993
3104
  throwIfAborted(options.signal);
2994
- const dependencyKey = `deps ${String(before.schemaEpoch)} ${key}`;
2995
- let dependencies = this.#cacheGet(dependencyKey);
2996
- if (dependencies === void 0) {
2997
- const rewritten = await this.#applyCatalogRewrites(plan, before);
2998
- dependencies = [...(await this.#findRealBlockTables(rewritten)).values()].map((table) => table.id).sort();
2999
- this.#cachePut(dependencyKey, dependencies, 64 + dependencies.reduce((bytes2, id) => bytes2 + id.length * 2 + 16, 0));
3000
- }
3001
- const generation = await this.#queryGenerations.key(dependencies, before.manifestVersion);
3002
- const resultKey = `${key}${String(before.schemaEpoch)}${generation}`;
3105
+ const resultKey = await this.#resultMemoIdentity(plan, key, before);
3106
+ if (resultKey === void 0)
3107
+ return this.#queryCompiled(plan, options, before);
3003
3108
  const cached = this.#cacheGet(resultKey);
3004
3109
  if (cached !== void 0) {
3005
3110
  options.onStats?.({ peakMemoryBytes: 0 });
@@ -3019,6 +3124,50 @@ class MinnowDatabase {
3019
3124
  }
3020
3125
  return result;
3021
3126
  }
3127
+ async #resultMemoIdentity(plan, key, before) {
3128
+ const dependencyKey = `deps ${String(before.schemaEpoch)} ${key}`;
3129
+ let proof = this.#cacheGet(dependencyKey);
3130
+ if (proof === void 0) {
3131
+ const rewritten = await this.#applyCatalogRewrites(plan, before);
3132
+ const dependencies = [...(await this.#findRealBlockTables(rewritten)).values()].map((table) => table.id).sort();
3133
+ proof = {
3134
+ dependencies,
3135
+ deterministic: !blockCallsFunctions(rewritten, nonDeterministicFunctionNames)
3136
+ };
3137
+ this.#cachePut(dependencyKey, proof, 64 + dependencies.reduce((bytes, id) => bytes + id.length * 2 + 16, 0));
3138
+ }
3139
+ if (!proof.deterministic)
3140
+ return void 0;
3141
+ const generation = await this.#queryGenerations.key(proof.dependencies, before.manifestVersion);
3142
+ return `${key}${String(before.schemaEpoch)}${generation}`;
3143
+ }
3144
+ async #memoizeLiveResult(query, result, before) {
3145
+ if (!this.#artifactCache.enabled)
3146
+ return;
3147
+ const bytes = queryResultRetainedBytes(result);
3148
+ if (bytes > RESULT_MEMO_MAX_BYTES)
3149
+ return;
3150
+ const plan = this.#compileLiveQuery(query);
3151
+ let key;
3152
+ if (typeof query !== "string" && query.kind === "typed-query") {
3153
+ key = `typed ${planMemoKey(plan)}`;
3154
+ } else {
3155
+ const sql = typeof query === "string" ? query : query.sql;
3156
+ const params = typeof query === "string" ? [] : query.params;
3157
+ if (!cacheableQueryInput(sql, params))
3158
+ return;
3159
+ key = `res ${queryResultMemoKey(sql, params)}`;
3160
+ }
3161
+ if (plan.usesStatementDatetime || plan.usesVolatileFunctions || plan.usesSequenceCalls)
3162
+ return;
3163
+ const resultKey = await this.#resultMemoIdentity(plan, key, before);
3164
+ if (resultKey === void 0)
3165
+ return;
3166
+ const after = await this.store.getCatalogProbe();
3167
+ if (after.catalogEpoch !== before.catalogEpoch)
3168
+ return;
3169
+ this.#cachePut(resultKey, markQueryResultExternal(copyQueryResult(result)), bytes);
3170
+ }
3022
3171
  async #applyCatalogRewrites(plan, probe) {
3023
3172
  const aliased = planHasSourceColumnAliases(plan);
3024
3173
  const natural = planHasNaturalJoins(plan);
@@ -3316,11 +3465,12 @@ class MinnowDatabase {
3316
3465
  const table = realTables.get(shape.table);
3317
3466
  if (table === void 0 || table.view !== void 0)
3318
3467
  return void 0;
3319
- const columnByName = new Map(table.columns.map((column) => [column.name, column]));
3468
+ const byName = shape.select === "*" || shape.select.length + shape.equalities.length > 8 ? new Map(table.columns.map((column) => [column.name, column])) : void 0;
3469
+ const findColumn2 = (name) => byName === void 0 ? table.columns.find((column) => column.name === name) : byName.get(name);
3320
3470
  const matchesStorageType = (column, value) => column.sqlDomain === void 0 && column.hidden !== true && (column.type === "number" && typeof value === "number" || column.type === "string" && typeof value === "string" || column.type === "boolean" && typeof value === "boolean" || column.type === "datetime" && value instanceof Date);
3321
3471
  const equalityColumns = /* @__PURE__ */ new Map();
3322
3472
  for (const equality of shape.equalities) {
3323
- const column = columnByName.get(equality.column);
3473
+ const column = findColumn2(equality.column);
3324
3474
  if (column === void 0 || !matchesStorageType(column, equality.value))
3325
3475
  return void 0;
3326
3476
  equalityColumns.set(column.name, column);
@@ -3337,7 +3487,7 @@ class MinnowDatabase {
3337
3487
  const projected = [];
3338
3488
  const selected = shape.select === "*" ? visibleTableColumns(table).map((column) => ({ column: column.name, alias: column.name })) : shape.select;
3339
3489
  for (const item of selected) {
3340
- const column = columnByName.get(item.column);
3490
+ const column = findColumn2(item.column);
3341
3491
  if (column === void 0 || column.hidden === true)
3342
3492
  return void 0;
3343
3493
  projected.push({ column, alias: item.alias });
@@ -3389,47 +3539,18 @@ class MinnowDatabase {
3389
3539
  }
3390
3540
  if (blockIndexes.length === 0)
3391
3541
  continue;
3392
- const searchBlocks = await this.#decodedBlocksThroughCache(blockIndexes.map((blockIndex) => searchBlockIds[blockIndex] ?? ""), snapshot);
3542
+ const searchBlocks = await this.#pointReadVectors(blockIndexes.map((blockIndex) => searchBlockIds[blockIndex] ?? ""), snapshot);
3393
3543
  for (const [position, blockIndex] of blockIndexes.entries()) {
3394
3544
  throwIfAborted(options.signal);
3395
- const decoded = searchBlocks[position];
3396
- if (decoded === void 0)
3545
+ const vector = searchBlocks[position];
3546
+ if (vector === void 0)
3397
3547
  return void 0;
3398
3548
  const blockId = searchBlockIds[blockIndex] ?? "";
3399
- const vector = this.#blockColumnVector(blockId, decoded);
3400
3549
  if (vector.kind !== searchColumn.type)
3401
3550
  return void 0;
3402
- const candidates = [];
3403
- if ((vector.kind === "number" || vector.kind === "datetime") && typeof searchTarget === "number") {
3404
- if (decoded.description.nullCount === 0 && valuesAreAscending(vector.values)) {
3405
- const run = equalRunRange(vector.values, searchTarget);
3406
- for (let slot = run.begin; slot < run.end; slot += 1)
3407
- candidates.push(slot);
3408
- } else {
3409
- for (let slot = 0; slot < vector.length; slot += 1) {
3410
- if (vector.values[slot] === searchTarget) {
3411
- const value = vectorValue(vector, slot);
3412
- if (value !== null)
3413
- candidates.push(slot);
3414
- }
3415
- }
3416
- }
3417
- } else if (vector.kind === "string" && typeof searchTarget === "string") {
3418
- const code = this.#dictionaryCode(blockId, vector, searchTarget);
3419
- if (code === void 0)
3420
- continue;
3421
- for (let slot = 0; slot < vector.length; slot += 1) {
3422
- if (vector.codes[slot] === code)
3423
- candidates.push(slot);
3424
- }
3425
- } else if (vector.kind === "boolean" && typeof searchTarget === "boolean") {
3426
- for (let slot = 0; slot < vector.length; slot += 1) {
3427
- if (vectorValue(vector, slot) === searchTarget)
3428
- candidates.push(slot);
3429
- }
3430
- } else {
3551
+ const candidates = this.#keySlotsInBlock(blockId, vector, searchTarget);
3552
+ if (candidates === void 0)
3431
3553
  return void 0;
3432
- }
3433
3554
  if (candidates.length === 0)
3434
3555
  continue;
3435
3556
  candidateBudget -= candidates.length;
@@ -3437,13 +3558,12 @@ class MinnowDatabase {
3437
3558
  return void 0;
3438
3559
  const remainingColumns = neededColumns.filter((column) => column.id !== searchColumn.id);
3439
3560
  const rowBlockIds = remainingColumns.map((column) => segment.columnBlockIds[column.id]?.[blockIndex] ?? "");
3440
- const rowBlocks = await this.#decodedBlocksThroughCache(rowBlockIds, snapshot);
3561
+ const rowBlocks = await this.#pointReadVectors(rowBlockIds, snapshot);
3441
3562
  const vectors = /* @__PURE__ */ new Map([[searchColumn.name, vector]]);
3442
3563
  for (const [index, column] of remainingColumns.entries()) {
3443
- const block = rowBlocks[index];
3444
- if (block === void 0)
3564
+ const columnVector = rowBlocks[index];
3565
+ if (columnVector === void 0)
3445
3566
  return void 0;
3446
- const columnVector = this.#blockColumnVector(rowBlockIds[index] ?? "", block);
3447
3567
  if (columnVector.kind !== column.type)
3448
3568
  return void 0;
3449
3569
  vectors.set(column.name, columnVector);
@@ -3487,13 +3607,10 @@ class MinnowDatabase {
3487
3607
  rows
3488
3608
  };
3489
3609
  }
3490
- #keySlotsInBlock(blockId, decoded, column, target) {
3491
- const vector = this.#blockColumnVector(blockId, decoded);
3492
- if (vector.kind !== column.type)
3493
- return void 0;
3610
+ #keySlotsInBlock(blockId, vector, target) {
3494
3611
  const slots = [];
3495
3612
  if ((vector.kind === "number" || vector.kind === "datetime") && typeof target === "number") {
3496
- if (decoded.description.nullCount === 0 && valuesAreAscending(vector.values)) {
3613
+ if (vector.nullCount === 0 && valuesAreAscending(vector.values)) {
3497
3614
  const run = equalRunRange(vector.values, target);
3498
3615
  for (let slot = run.begin; slot < run.end; slot += 1)
3499
3616
  slots.push(slot);
@@ -3555,13 +3672,15 @@ class MinnowDatabase {
3555
3672
  decodedBlocks += blockIndexes.length;
3556
3673
  if (decodedBlocks > MAX_POINT_READ_DELTA_BLOCKS)
3557
3674
  return void 0;
3558
- const keyBlocks = await this.#decodedBlocksThroughCache(blockIndexes.map((blockIndex) => keyBlockIds[blockIndex] ?? ""), snapshot);
3675
+ const keyBlocks = await this.#pointReadVectors(blockIndexes.map((blockIndex) => keyBlockIds[blockIndex] ?? ""), snapshot);
3559
3676
  for (const [position, blockIndex] of blockIndexes.entries()) {
3560
- const decoded = keyBlocks[position];
3561
- if (decoded === void 0)
3677
+ const keyVector = keyBlocks[position];
3678
+ if (keyVector === void 0)
3562
3679
  return void 0;
3563
3680
  const blockId = keyBlockIds[blockIndex] ?? "";
3564
- const slots = this.#keySlotsInBlock(blockId, decoded, keyColumn, target);
3681
+ if (keyVector.kind !== keyColumn.type)
3682
+ return void 0;
3683
+ const slots = this.#keySlotsInBlock(blockId, keyVector, target);
3565
3684
  if (slots === void 0)
3566
3685
  return void 0;
3567
3686
  if (slots.length === 0)
@@ -3582,21 +3701,17 @@ class MinnowDatabase {
3582
3701
  decodedBlocks += blockIds.length;
3583
3702
  if (decodedBlocks > MAX_POINT_READ_DELTA_BLOCKS)
3584
3703
  return void 0;
3585
- const blocks = await this.#decodedBlocksThroughCache(blockIds, snapshot);
3704
+ const blocks = await this.#pointReadVectors(blockIds, snapshot);
3586
3705
  const values = segment.kind === "update" && current !== void 0 ? current : /* @__PURE__ */ new Map();
3587
3706
  if (segment.kind !== "update") {
3588
3707
  if (current !== void 0)
3589
3708
  return void 0;
3590
- values.set(keyColumn.name, vectorValue(this.#blockColumnVector(blockId, decoded), slot));
3709
+ values.set(keyColumn.name, vectorValue(keyVector, slot));
3591
3710
  }
3592
3711
  for (const [index, column] of carried.entries()) {
3593
- const block = blocks[index];
3594
- const columnBlockId = blockIds[index];
3595
- if (block === void 0 || columnBlockId === void 0)
3596
- return void 0;
3597
- if (block.column.rowCount !== decoded.column.rowCount)
3712
+ const vector = blocks[index];
3713
+ if (vector?.length !== keyVector.length)
3598
3714
  return void 0;
3599
- const vector = this.#blockColumnVector(columnBlockId, block);
3600
3715
  if (vector.kind !== column.type)
3601
3716
  return void 0;
3602
3717
  values.set(column.name, vectorValue(vector, slot));
@@ -3719,30 +3834,32 @@ class MinnowDatabase {
3719
3834
  };
3720
3835
  }
3721
3836
  async cleanupQuerySpill(options = {}) {
3722
- const maxOwners = options.maxOwners === void 0 ? Number.MAX_SAFE_INTEGER : positiveWholeNumber(options.maxOwners, "Spill cleanup owner limit");
3723
- const cutoff = dateIsoString(this.#now());
3724
- const result = {
3725
- ownersExamined: 0,
3726
- ownersReclaimed: 0,
3727
- ownersRetained: 0
3728
- };
3729
- let cursor = null;
3730
- while (result.ownersExamined < maxOwners) {
3731
- const page = await this.store.listTempOwnerIdsPage(cursor, Math.min(64, maxOwners - result.ownersExamined));
3732
- for (const ownerId of page.records) {
3733
- result.ownersExamined += 1;
3734
- if (await this.store.removeTempOwnerIfExpired(ownerId, cutoff)) {
3735
- result.ownersReclaimed += 1;
3736
- } else {
3737
- result.ownersRetained += 1;
3837
+ return this.#foreground(async () => {
3838
+ const maxOwners = options.maxOwners === void 0 ? Number.MAX_SAFE_INTEGER : positiveWholeNumber(options.maxOwners, "Spill cleanup owner limit");
3839
+ const cutoff = dateIsoString(this.#now());
3840
+ const result = {
3841
+ ownersExamined: 0,
3842
+ ownersReclaimed: 0,
3843
+ ownersRetained: 0
3844
+ };
3845
+ let cursor = null;
3846
+ while (result.ownersExamined < maxOwners) {
3847
+ const page = await this.store.listTempOwnerIdsPage(cursor, Math.min(64, maxOwners - result.ownersExamined));
3848
+ for (const ownerId of page.records) {
3849
+ result.ownersExamined += 1;
3850
+ if (await this.store.removeTempOwnerIfExpired(ownerId, cutoff)) {
3851
+ result.ownersReclaimed += 1;
3852
+ } else {
3853
+ result.ownersRetained += 1;
3854
+ }
3738
3855
  }
3856
+ if (page.nextCursor === null)
3857
+ break;
3858
+ cursor = page.nextCursor;
3859
+ await this.#yieldMaintenance();
3739
3860
  }
3740
- if (page.nextCursor === null)
3741
- break;
3742
- cursor = page.nextCursor;
3743
- await this.#yieldMaintenance();
3744
- }
3745
- return result;
3861
+ return result;
3862
+ });
3746
3863
  }
3747
3864
  async #cleanupExpiredQuerySpill(maxOwners) {
3748
3865
  const cutoff = dateIsoString(this.#now());
@@ -3776,26 +3893,18 @@ class MinnowDatabase {
3776
3893
  throw new LiveQueryLimitError("set", MAX_LIVE_QUERY_SETS_PER_DATABASE);
3777
3894
  }
3778
3895
  const channelName = options.channel === void 0 && options.channelName === void 0 ? this.store.liveQueryChannelName : options.channelName;
3779
- const compileLiveQuery = (query) => {
3780
- if (typeof query === "string")
3781
- return this.#compileCached(query);
3782
- if (query.kind === "typed-query")
3783
- return query.plan;
3784
- return bindPlanParameters(this.#compileCached(query.sql), query.params);
3785
- };
3786
3896
  const set = new LiveQuerySet({
3787
- currentProbe: async () => {
3788
- return this.store.getCatalogProbe();
3789
- },
3897
+ currentProbe: () => this.store.getCatalogProbe(),
3790
3898
  manifestPage: (afterVersion, limit) => this.store.listManifestPage(afterVersion, limit),
3791
3899
  dependencyTableIds: async (query, probe) => {
3792
- const compiled = compileLiveQuery(query);
3900
+ const compiled = this.#compileLiveQuery(query);
3793
3901
  const plan = await this.#applyCatalogRewrites(compiled, probe);
3794
3902
  const tables = await this.#findRealBlockTables(plan);
3795
3903
  return new Set([...tables.values()].map((record) => record.id));
3796
3904
  },
3797
3905
  execute: (query, context) => this.#liveExecute(query, context),
3798
- executeMaintainable: (query, context) => this.#liveExecuteMaintainable(compileLiveQuery(query), context),
3906
+ memoize: (query, result, probe) => this.#memoizeLiveResult(query, result, probe),
3907
+ executeMaintainable: (query, context) => this.#liveExecuteMaintainable(this.#compileLiveQuery(query), context),
3799
3908
  maintain: (_query, result, state, tableIds, after, until, probe) => this.#liveMaintain(result, state, tableIds, after, until, probe),
3800
3909
  changeCanAffect: (query, tableIds, after, until) => this.#liveChangeCanAffect(query, tableIds, after, until)
3801
3910
  }, {
@@ -3809,6 +3918,13 @@ class MinnowDatabase {
3809
3918
  this.#liveSets.add(set);
3810
3919
  return set;
3811
3920
  }
3921
+ #compileLiveQuery(query) {
3922
+ if (typeof query === "string")
3923
+ return this.#compileCached(query);
3924
+ if (query.kind === "typed-query")
3925
+ return query.plan;
3926
+ return bindPlanParameters(this.#compileCached(query.sql), query.params);
3927
+ }
3812
3928
  async #liveExecute(query, context) {
3813
3929
  if (context === void 0) {
3814
3930
  if (typeof query === "string")
@@ -3832,6 +3948,8 @@ class MinnowDatabase {
3832
3948
  return void 0;
3833
3949
  }
3834
3950
  const plan = await this.#applyCatalogRewrites(compiled, probe);
3951
+ if (blockCallsFunctions(plan, nonDeterministicFunctionNames))
3952
+ return void 0;
3835
3953
  const base = plan.base;
3836
3954
  if (base.table === DUAL_TABLE || base.derived !== void 0 || base.union !== void 0 || base.recursive !== void 0 || base.windowed !== void 0 || plan.pendingSelectShape !== void 0 || plan.distinctWildcard === true || plan.limitParameter !== void 0 || plan.offsetParameter !== void 0 || planReadsBeyondSingleScan(plan) || planContainsFts(plan)) {
3837
3955
  return void 0;
@@ -3953,18 +4071,19 @@ class MinnowDatabase {
3953
4071
  try {
3954
4072
  const input = await this.#withReadReservation(() => this.#queryCompiled(state.fullPlan, {}, probe));
3955
4073
  const aggregate = state.aggregate.patch(input, /* @__PURE__ */ new Set(), liveKeyToken);
3956
- return {
3957
- result: aggregate.result(),
4074
+ const result = aggregate.result();
4075
+ return stageLiveExecution({
4076
+ result,
3958
4077
  state: { ...state, aggregate },
3959
- retainedBytes: aggregate.retainedBytes
3960
- };
4078
+ retainedBytes: aggregate.retainedBytes + queryResultRetainedBytes(result) + result.rows.length * 48
4079
+ }, () => aggregate.accept());
3961
4080
  } catch {
3962
4081
  return void 0;
3963
4082
  }
3964
4083
  }
3965
4084
  let executed;
3966
4085
  try {
3967
- executed = externalizeQueryResult(await this.#withReadReservation(() => this.#queryCompiled(state.fullPlan, {}, probe)));
4086
+ executed = await this.#withReadReservation(() => this.#queryCompiled(state.fullPlan, {}, probe));
3968
4087
  } catch {
3969
4088
  return void 0;
3970
4089
  }
@@ -4017,12 +4136,12 @@ class MinnowDatabase {
4017
4136
  const previous = retained.rows[index];
4018
4137
  return previous === void 0 || !sameLiveRow(previous, row, result.columns);
4019
4138
  });
4020
- return {
4139
+ return stageLiveExecution({
4021
4140
  result,
4022
4141
  state: { ...state, aggregate },
4023
- retainedBytes: aggregate.retainedBytes,
4142
+ retainedBytes: aggregate.retainedBytes + queryResultRetainedBytes(result) + result.rows.length * 48,
4024
4143
  changed: changed2
4025
- };
4144
+ }, () => aggregate.accept());
4026
4145
  }
4027
4146
  const affected = /* @__PURE__ */ new Set();
4028
4147
  const retainedTokens = state.positions;
@@ -4054,7 +4173,9 @@ class MinnowDatabase {
4054
4173
  }
4055
4174
  ]
4056
4175
  };
4057
- const delta = splitLiveHiddenColumns(externalizeQueryResult(await this.#withReadReservation(() => this.#queryCompiled(deltaPlan, {}, probe))), state);
4176
+ const delta = splitLiveHiddenColumns(await this.#withReadReservation(() => this.#queryCompiled(deltaPlan, {}, probe)), state);
4177
+ if (affected.size === 0 && delta.result.rows.length === 0)
4178
+ return { result: retained, state, changed: false };
4058
4179
  const compare = liveOrderComparator(state.orderTerms);
4059
4180
  const added = {
4060
4181
  rows: delta.result.rows,
@@ -4074,6 +4195,18 @@ class MinnowDatabase {
4074
4195
  } catch {
4075
4196
  return void 0;
4076
4197
  }
4198
+ let retainedBytes = state.retainedBytes;
4199
+ if (retainedBytes !== void 0) {
4200
+ for (const index of affected)
4201
+ retainedBytes -= liveRowStateBytes(state, index);
4202
+ for (let index = 0; index < added.rows.length; index += 1)
4203
+ retainedBytes += liveRowStateBytes(added, index);
4204
+ if (fullLimit !== void 0) {
4205
+ for (let index = fullLimit; index < merged.rows.length; index += 1)
4206
+ retainedBytes -= liveRowStateBytes(merged, index);
4207
+ }
4208
+ }
4209
+ const complete = state.complete && (fullLimit === void 0 || merged.rows.length <= fullLimit);
4077
4210
  if (fullLimit !== void 0 && merged.rows.length > fullLimit)
4078
4211
  merged = trimLiveRows(merged, fullLimit);
4079
4212
  if (truncated && affected.size > 0 && state.limit !== void 0) {
@@ -4083,10 +4216,17 @@ class MinnowDatabase {
4083
4216
  return void 0;
4084
4217
  }
4085
4218
  }
4086
- return liveMaintainedOutcome({ ...state, rows: merged.rows, keys: merged.keys, order: merged.order }, retained, merged.previousIndex);
4219
+ return liveMaintainedOutcome({
4220
+ ...state,
4221
+ complete,
4222
+ rows: merged.rows,
4223
+ keys: merged.keys,
4224
+ order: merged.order,
4225
+ ...retainedBytes === void 0 ? {} : { retainedBytes }
4226
+ }, retained, merged.previousIndex);
4087
4227
  }
4088
4228
  async #liveChangeCanAffect(query, tableIds, after, until) {
4089
- const plan = typeof query === "string" ? this.#compileCached(query) : query.kind === "typed-query" ? query.plan : bindPlanParameters(this.#compileCached(query.sql), query.params);
4229
+ const plan = this.#compileLiveQuery(query);
4090
4230
  if (planContainsFts(plan))
4091
4231
  return true;
4092
4232
  if (planReadsBeyondSingleScan(plan))
@@ -4812,7 +4952,7 @@ class MinnowDatabase {
4812
4952
  }
4813
4953
  }
4814
4954
  const statementNow = this.#now();
4815
- const materialized = await this.#materializeInsertExpressions({ ...compiled, columns, rows: derivedRows }, statementNow);
4955
+ const materialized = await this.#materializeInsertExpressions({ ...compiled, columns, rows: derivedRows }, statementNow, async (plan) => this.#sessionQueryPlan(transaction, await this.#applyCatalogRewrites(plan)));
4816
4956
  const input = insertStatementBatch(materialized);
4817
4957
  coerceSqlWriteBatch(target, input);
4818
4958
  const filled = await fillColumnDefaults(target, input, (sql) => this.#evaluateDefaultExpression(sql, statementNow), derivedRows.length);
@@ -4969,16 +5109,47 @@ class MinnowDatabase {
4969
5109
  async write(action) {
4970
5110
  return this.#openWriteScope((session) => action(session));
4971
5111
  }
5112
+ #coordinateWrite(run) {
5113
+ return this.#coordinateWrites ? coordinateWrite(this.store, run, this.#shutdown.signal) : run();
5114
+ }
4972
5115
  async #openWriteScope(action, options = {}) {
5116
+ return this.#ownScope((signal) => this.#runWriteScope(action, options, signal));
5117
+ }
5118
+ async #ownScope(run) {
5119
+ if (this.#closed)
5120
+ throw new Error("Database is closed");
5121
+ if (this.#scopeTasks.size >= MAX_DATABASE_ACTIVE_SCOPES) {
5122
+ throw new RangeError("Too many active database scopes; await a scope before opening another");
5123
+ }
5124
+ const controller = new AbortController();
5125
+ this.#scopeControllers.add(controller);
5126
+ const task = run(controller.signal);
5127
+ this.#scopeTasks.add(task);
5128
+ try {
5129
+ return await task;
5130
+ } finally {
5131
+ this.#scopeControllers.delete(controller);
5132
+ this.#scopeTasks.delete(task);
5133
+ }
5134
+ }
5135
+ async #runWriteScope(action, options, signal) {
5136
+ signal.throwIfAborted();
4973
5137
  await this.#publishPendingCompactions();
4974
5138
  await this.#assistAutomaticCollection();
4975
5139
  const transaction = await this.#transactions.beginDeferred({
4976
- durableSnapshot: options.durableSnapshot ?? true
5140
+ durableSnapshot: options.durableSnapshot ?? true,
5141
+ coalesceArtifacts: true
4977
5142
  });
4978
5143
  let closed = false;
5144
+ let accepting = true;
5145
+ let tail = Promise.resolve();
5146
+ let pendingReads = 0;
5147
+ let pendingWrites = 0;
5148
+ let writeBarrier = tail;
4979
5149
  let staged = 0;
4980
5150
  let poisoned;
4981
5151
  const open = () => {
5152
+ signal.throwIfAborted();
4982
5153
  if (closed)
4983
5154
  throw new Error("The write scope has ended");
4984
5155
  if (poisoned !== void 0) {
@@ -4997,7 +5168,7 @@ class MinnowDatabase {
4997
5168
  throw error;
4998
5169
  }
4999
5170
  };
5000
- const session = {
5171
+ const operations = {
5001
5172
  query: async (sql, options2) => {
5002
5173
  open();
5003
5174
  return this.#withReadReservation(() => this.#sessionQuery(transaction, sql, options2));
@@ -5042,7 +5213,7 @@ class MinnowDatabase {
5042
5213
  }
5043
5214
  };
5044
5215
  const writer = {
5045
- ...session,
5216
+ ...operations,
5046
5217
  queryPlan: (plan) => this.#withReadReservation(async () => {
5047
5218
  const probe = transaction.initialCatalogProbe;
5048
5219
  return transaction.stagedWorkCount === 0 && probe !== void 0 ? this.#queryCompiled(plan, {}, probe) : this.#sessionQueryPlan(transaction, await this.#applyCatalogRewrites(plan));
@@ -5071,8 +5242,61 @@ class MinnowDatabase {
5071
5242
  staged = transaction.stagedWorkCount === 0 ? 0 : 1;
5072
5243
  }
5073
5244
  };
5245
+ const enqueue = (run, kind = "write") => {
5246
+ if (!accepting)
5247
+ return Promise.reject(new Error("The write scope has ended"));
5248
+ const read = kind === "read";
5249
+ if (read && pendingReads >= MAX_DATABASE_ACTIVE_READS) {
5250
+ return Promise.reject(new DatabaseReadBacklogError(MAX_DATABASE_ACTIVE_READS));
5251
+ }
5252
+ if (!read && pendingWrites >= MAX_DATABASE_PENDING_WRITES) {
5253
+ return Promise.reject(new RangeError("Too many pending transaction operations; await a statement"));
5254
+ }
5255
+ if (read)
5256
+ pendingReads += 1;
5257
+ else
5258
+ pendingWrites += 1;
5259
+ const operation = (read ? writeBarrier : tail).then(() => {
5260
+ if (kind === "rollback") {
5261
+ signal.throwIfAborted();
5262
+ if (closed)
5263
+ throw new Error("The write scope has ended");
5264
+ } else
5265
+ open();
5266
+ return run();
5267
+ });
5268
+ const settled = operation.then(() => void 0, () => void 0);
5269
+ tail = Promise.all([tail, settled]).then(() => void 0);
5270
+ if (!read)
5271
+ writeBarrier = tail;
5272
+ return operation.finally(() => {
5273
+ if (read)
5274
+ pendingReads -= 1;
5275
+ else
5276
+ pendingWrites -= 1;
5277
+ });
5278
+ };
5279
+ const session = {
5280
+ query: (sql, queryOptions) => enqueue(() => operations.query(sql, queryOptions), "read"),
5281
+ execute: (sql, params) => enqueue(() => operations.execute(sql, params)),
5282
+ insertBatch: (name, input) => enqueue(() => operations.insertBatch(name, input)),
5283
+ upsertBatch: (name, input, upsertOptions) => enqueue(() => operations.upsertBatch(name, input, upsertOptions)),
5284
+ updateBatch: (name, input) => enqueue(() => operations.updateBatch(name, input)),
5285
+ deleteBatch: (name, input) => enqueue(() => operations.deleteBatch(name, input))
5286
+ };
5287
+ const queuedWriter = {
5288
+ ...writer,
5289
+ ...session,
5290
+ queryPlan: (plan) => enqueue(() => writer.queryPlan(plan), "read"),
5291
+ queryFirstColumn: (plan) => enqueue(() => writer.queryFirstColumn(plan), "read"),
5292
+ executeStatement: (statement) => enqueue(() => writer.executeStatement(statement)),
5293
+ rollbackTo: (checkpoint) => enqueue(() => writer.rollbackTo(checkpoint), "rollback")
5294
+ };
5074
5295
  try {
5075
- const result = await action(session, transaction, writer);
5296
+ const result = await untilAborted(() => action(session, transaction, queuedWriter), signal);
5297
+ accepting = false;
5298
+ await tail;
5299
+ signal.throwIfAborted();
5076
5300
  closed = true;
5077
5301
  if (poisoned !== void 0) {
5078
5302
  throw new Error(`The write scope failed mid-stage and was rolled back: ${poisoned instanceof Error ? poisoned.message : "staging failed"}`, { cause: poisoned });
@@ -5098,7 +5322,9 @@ class MinnowDatabase {
5098
5322
  }
5099
5323
  throw new Error("Commit retry limit was exceeded");
5100
5324
  } catch (error) {
5325
+ accepting = false;
5101
5326
  closed = true;
5327
+ await tail;
5102
5328
  await transaction.abort().catch(() => void 0);
5103
5329
  throw await this.#translateUniqueConflict(error);
5104
5330
  }
@@ -5557,33 +5783,39 @@ class MinnowDatabase {
5557
5783
  set.notifyLocalCommit();
5558
5784
  }
5559
5785
  async run(query) {
5560
- await this.#settleExpiredStatementTransaction();
5561
- const open = this.#openTransaction;
5562
- const plan = query.plan;
5563
- const execute = async () => {
5564
- const probe = this.store.getCatalogProbe.bind(this.store);
5565
- return plan.usesStatementDatetime === true || plan.usesVolatileFunctions === true || plan.usesSequenceCalls === true ? this.#queryCompiled(plan) : this.#memoizedQuery(plan, `typed ${planMemoKey(plan)}`, {}, probe);
5566
- };
5567
- const result = open === void 0 ? await this.#withReadReservation(execute) : await this.#duringTransaction(open, () => open.session.queryPlan(plan));
5568
- return externalizeQueryResult(result).rows;
5786
+ return this.#foreground(async () => {
5787
+ await this.#settleExpiredStatementTransaction();
5788
+ if (this.#statementTransactionExpired)
5789
+ throw new TransactionExpiredError();
5790
+ const open = this.#openTransaction;
5791
+ const plan = query.plan;
5792
+ const execute = async () => {
5793
+ const probe = this.store.getCatalogProbe.bind(this.store);
5794
+ return plan.usesStatementDatetime === true || plan.usesVolatileFunctions === true || plan.usesSequenceCalls === true ? this.#queryCompiled(plan) : this.#memoizedQuery(plan, `typed ${planMemoKey(plan)}`, {}, probe);
5795
+ };
5796
+ const result = open === void 0 ? await this.#withReadReservation(execute) : await this.#duringTransaction(open, () => open.session.queryPlan(plan));
5797
+ return externalizeQueryResult(result).rows;
5798
+ });
5569
5799
  }
5570
5800
  async migrate(definition = this.#schema, options = {}) {
5571
- if (definition === void 0) {
5572
- throw new TypeError("migrate() needs a schema: pass a definition, or construct the database with { schema }");
5573
- }
5574
- let originalSteps;
5575
- for (let attempt = 0; ; attempt += 1) {
5576
- try {
5577
- const result = await this.#migrateOnce(definition, options, (steps) => {
5578
- originalSteps ??= [...steps];
5579
- });
5580
- return originalSteps === void 0 ? result : migrationResult(originalSteps);
5581
- } catch (error) {
5582
- if (!(error instanceof TableRecordConflictError) && !(error instanceof WriteConflictError) || attempt >= this.#maxCommitRetries) {
5583
- throw error;
5801
+ return this.#foreground(async () => {
5802
+ if (definition === void 0) {
5803
+ throw new TypeError("migrate() needs a schema: pass a definition, or construct the database with { schema }");
5804
+ }
5805
+ let originalSteps;
5806
+ for (let attempt = 0; ; attempt += 1) {
5807
+ try {
5808
+ const result = await this.#migrateOnce(definition, options, (steps) => {
5809
+ originalSteps ??= [...steps];
5810
+ });
5811
+ return originalSteps === void 0 ? result : migrationResult(originalSteps);
5812
+ } catch (error) {
5813
+ if (!(error instanceof TableRecordConflictError) && !(error instanceof WriteConflictError) || attempt >= this.#maxCommitRetries) {
5814
+ throw error;
5815
+ }
5584
5816
  }
5585
5817
  }
5586
- }
5818
+ });
5587
5819
  }
5588
5820
  #assertViewMatchesDeclaration(record, step) {
5589
5821
  if (record === void 0)
@@ -5884,77 +6116,105 @@ class MinnowDatabase {
5884
6116
  };
5885
6117
  }
5886
6118
  async explain(sql) {
5887
- let plan = this.#compileCached(sql);
5888
- if (planContainsFts(plan)) {
5889
- const realTables = await this.#findRealBlockTables(plan);
5890
- plan = expandFtsColumns(plan, (tableName) => searchableFtsColumns(realTables.get(tableName)));
5891
- }
5892
- const reported = transparentProjectionSource(plan)?.inner ?? plan;
5893
- const notes = [];
5894
- if (this.#canStreamPlanShape(reported, {})) {
5895
- notes.push("streams the base scan through resident block windows");
5896
- } else {
5897
- notes.push("materializes inputs at preparation");
5898
- }
5899
- const table = reported.base.derived ?? reported.base.union ?? reported.base.windowed ?? reported.base.recursive;
5900
- if (table === void 0 && reported.joins.length === 0 && reported.base.table !== DUAL_TABLE) {
5901
- const record = await this.#findTable(reported.base.table);
5902
- const pointTemplate = record.view === void 0 ? cachedPointReadTemplate(reported) : null;
5903
- if (pointTemplate !== null) {
5904
- const keyColumn = getUniqueKeyColumn(record);
5905
- const keyComponents = keyColumn === void 0 ? [] : keyColumn.hidden === true ? primaryKeyColumns(record) : [keyColumn];
5906
- const covered = new Set(pointTemplate.equalities.map((equality) => equality.column));
5907
- if (keyComponents.length > 0 && keyComponents.every((component) => covered.has(component.name))) {
5908
- notes.push("key-covering equality answers as a point read when the visible history allows");
5909
- }
5910
- }
5911
- if (zonePredicates(reported, record).length > 0) {
5912
- notes.push("zone-map pruning applies to the unbudgeted scan");
5913
- }
5914
- if (secondaryIndexPredicates(reported, record).length > 0) {
5915
- notes.push("a ready secondary index prunes the scan to candidate row groups");
6119
+ return this.#foreground(async () => {
6120
+ let plan = this.#compileCached(sql);
6121
+ if (planContainsFts(plan)) {
6122
+ const realTables = await this.#findRealBlockTables(plan);
6123
+ plan = expandFtsColumns(plan, (tableName) => searchableFtsColumns(realTables.get(tableName)));
5916
6124
  }
5917
- const referenced = referencedColumns(reported, /* @__PURE__ */ new Map([[record.name, record.columns.map((column) => column.name)]])).get(record.name);
5918
- const projected = referenced === void 0 || referenced.length === 0 ? [] : resolveReadColumns(record, referenced);
5919
- if (secondaryIndexOrderForPlan(reported, record, projected, false) !== void 0) {
5920
- notes.push("a ready secondary index supplies ORDER BY without a sort");
5921
- if (secondaryIndexOrderForPlan(reported, record, projected) !== void 0) {
5922
- notes.push("a covering secondary-index scan avoids table blocks");
5923
- }
6125
+ const reported = transparentProjectionSource(plan)?.inner ?? plan;
6126
+ const notes = [];
6127
+ if (this.#canStreamPlanShape(reported, {})) {
6128
+ notes.push("streams the base scan through resident block windows");
6129
+ } else {
6130
+ notes.push("materializes inputs at preparation");
5924
6131
  }
5925
- }
5926
- if (blockHasSubqueries(reported))
5927
- notes.push("resolves uncorrelated subqueries at one snapshot");
5928
- if (planContainsFts(reported)) {
5929
- notes.push("full-text MATCH evaluates via per-dictionary term tables on the scan");
5930
- if (table === void 0 && reported.joins.length === 0) {
6132
+ const table = reported.base.derived ?? reported.base.union ?? reported.base.windowed ?? reported.base.recursive;
6133
+ if (table === void 0 && reported.joins.length === 0 && reported.base.table !== DUAL_TABLE) {
5931
6134
  const record = await this.#findTable(reported.base.table);
5932
- const readyColumnIds = new Set(Object.entries(record.ftsColumns ?? {}).filter(([, state]) => state.state === "ready" && state.tokenizerVersion === FTS_TOKENIZER_VERSION).map(([columnId]) => columnId));
5933
- const columnsByName = new Map(record.columns.map((column) => [column.name, column]));
5934
- const scoringServed = [...this.#ftsBm25Nodes(reported).values()].every((node) => node.columns.every((expression) => {
5935
- if (expression.kind !== "column")
5936
- return false;
5937
- const column = columnsByName.get(expression.reference.split(".").at(-1) ?? expression.reference);
5938
- return column !== void 0 && readyColumnIds.has(column.id);
5939
- }));
5940
- const scoring = planContainsFts(reported, "bm25");
5941
- if (readyColumnIds.size > 0 && (!scoring || scoringServed)) {
5942
- notes.push("a ready full-text index prunes the base scan to candidate segments");
6135
+ const pointTemplate = record.view === void 0 ? cachedPointReadTemplate(reported) : null;
6136
+ if (pointTemplate !== null) {
6137
+ const keyColumn = getUniqueKeyColumn(record);
6138
+ const keyComponents = keyColumn === void 0 ? [] : keyColumn.hidden === true ? primaryKeyColumns(record) : [keyColumn];
6139
+ const covered = new Set(pointTemplate.equalities.map((equality) => equality.column));
6140
+ if (keyComponents.length > 0 && keyComponents.every((component) => covered.has(component.name))) {
6141
+ notes.push("key-covering equality answers as a point read when the visible history allows");
6142
+ }
6143
+ }
6144
+ if (zonePredicates(reported, record).length > 0) {
6145
+ notes.push("zone-map pruning applies to the unbudgeted scan");
5943
6146
  }
5944
- if (scoring) {
5945
- notes.push(scoringServed ? "BM25 statistics come from the full-text index" : "BM25 scoring reads the full scan for corpus statistics; index pruning does not apply");
6147
+ if (secondaryIndexPredicates(reported, record).length > 0) {
6148
+ notes.push("a ready secondary index prunes the scan to candidate row groups");
6149
+ }
6150
+ const referenced = referencedColumns(reported, /* @__PURE__ */ new Map([[record.name, record.columns.map((column) => column.name)]])).get(record.name);
6151
+ const projected = referenced === void 0 || referenced.length === 0 ? [] : resolveReadColumns(record, referenced);
6152
+ if (secondaryIndexOrderForPlan(reported, record, projected, false) !== void 0) {
6153
+ notes.push("a ready secondary index supplies ORDER BY without a sort");
6154
+ if (secondaryIndexOrderForPlan(reported, record, projected) !== void 0) {
6155
+ notes.push("a covering secondary-index scan avoids table blocks");
6156
+ }
5946
6157
  }
5947
- } else if (planContainsFts(reported, "bm25")) {
5948
- notes.push("BM25 scoring reads the full scan for corpus statistics; index pruning does not apply");
5949
6158
  }
5950
- }
5951
- return `${renderPlan(plan)}
6159
+ if (blockHasSubqueries(reported))
6160
+ notes.push("resolves uncorrelated subqueries at one snapshot");
6161
+ if (planContainsFts(reported)) {
6162
+ notes.push("full-text MATCH evaluates via per-dictionary term tables on the scan");
6163
+ if (table === void 0 && reported.joins.length === 0) {
6164
+ const record = await this.#findTable(reported.base.table);
6165
+ const readyColumnIds = new Set(Object.entries(record.ftsColumns ?? {}).filter(([, state]) => state.state === "ready" && state.tokenizerVersion === FTS_TOKENIZER_VERSION).map(([columnId]) => columnId));
6166
+ const columnsByName = new Map(record.columns.map((column) => [column.name, column]));
6167
+ const scoringServed = [...this.#ftsBm25Nodes(reported).values()].every((node) => node.columns.every((expression) => {
6168
+ if (expression.kind !== "column")
6169
+ return false;
6170
+ const column = columnsByName.get(expression.reference.split(".").at(-1) ?? expression.reference);
6171
+ return column !== void 0 && readyColumnIds.has(column.id);
6172
+ }));
6173
+ const scoring = planContainsFts(reported, "bm25");
6174
+ if (readyColumnIds.size > 0 && (!scoring || scoringServed)) {
6175
+ notes.push("a ready full-text index prunes the base scan to candidate segments");
6176
+ }
6177
+ if (scoring) {
6178
+ notes.push(scoringServed ? "BM25 statistics come from the full-text index" : "BM25 scoring reads the full scan for corpus statistics; index pruning does not apply");
6179
+ }
6180
+ } else if (planContainsFts(reported, "bm25")) {
6181
+ notes.push("BM25 scoring reads the full scan for corpus statistics; index pruning does not apply");
6182
+ }
6183
+ }
6184
+ return `${renderPlan(plan)}
5952
6185
  ${notes.map((note) => `-- ${note}`).join("\n")}`;
6186
+ });
5953
6187
  }
5954
6188
  async execute(sql, params, options = {}) {
6189
+ if (this.#closed)
6190
+ throw new Error("Database is closed");
6191
+ if (this.#pendingExecutes >= MAX_DATABASE_PENDING_WRITES) {
6192
+ throw new RangeError("Too many pending SQL statements; await a statement");
6193
+ }
6194
+ this.#pendingExecutes += 1;
6195
+ const run = () => this.#executeSerial(sql, params, options);
6196
+ const operation = this.#executeChain.then(run, run);
6197
+ this.#executeChain = operation.then(() => void 0, () => void 0);
6198
+ try {
6199
+ return await operation;
6200
+ } finally {
6201
+ this.#pendingExecutes -= 1;
6202
+ }
6203
+ }
6204
+ async #executeSerial(sql, params, options) {
6205
+ if (this.#closed)
6206
+ throw new Error("Database is closed");
5955
6207
  throwIfAborted(options.signal);
5956
6208
  await this.#settleExpiredStatementTransaction();
5957
6209
  const statement = this.#compileStatementCached(sql);
6210
+ if (this.#statementTransactionExpired) {
6211
+ if (statement.kind !== "transaction" || statement.action !== "begin" && statement.action !== "rollback") {
6212
+ throw new TransactionExpiredError();
6213
+ }
6214
+ this.#statementTransactionExpired = false;
6215
+ if (statement.action === "rollback")
6216
+ return { kind: "transaction", action: "rollback" };
6217
+ }
5958
6218
  if (statement.kind === "transaction") {
5959
6219
  return this.#runTransactionStatement(statement.action, statement.name);
5960
6220
  }
@@ -6176,6 +6436,7 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
6176
6436
  if (open === void 0 || open.busy > 0 || dateMilliseconds(this.#now()) - open.activeAt < this.#transactionIdleTimeoutMs) {
6177
6437
  return;
6178
6438
  }
6439
+ this.#statementTransactionExpired = true;
6179
6440
  await this.#rollbackStatementTransaction(open);
6180
6441
  }
6181
6442
  async #runTransactionStatement(action, name) {
@@ -6219,6 +6480,7 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
6219
6480
  return;
6220
6481
  if (dateMilliseconds(this.#now()) - state.activeAt < this.#transactionIdleTimeoutMs)
6221
6482
  return;
6483
+ this.#statementTransactionExpired = true;
6222
6484
  void this.#rollbackStatementTransaction(state).catch(() => void 0);
6223
6485
  }, this.#transactionIdleTimeoutMs)
6224
6486
  };
@@ -6662,240 +6924,263 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
6662
6924
  };
6663
6925
  }
6664
6926
  async runStatement(statement, options = {}) {
6665
- const statementNow = this.#now();
6666
- statement = resolveMutationStatementDatetimes(statement, statementNow);
6667
- if (statement.kind === "select") {
6668
- return { kind: "rows", result: await this.query(statement.sql) };
6669
- }
6670
- if (statement.kind === "set") {
6671
- return { kind: "set", action: statement.action, name: statement.name };
6672
- }
6673
- if (statement.kind === "show") {
6674
- const settings = {
6675
- search_path: "public",
6676
- server_version: "16.0",
6677
- server_version_num: "160000",
6678
- timezone: "UTC",
6679
- time_zone: "UTC",
6680
- transaction_isolation: "repeatable read",
6681
- default_transaction_isolation: "repeatable read",
6682
- client_encoding: "UTF8",
6683
- server_encoding: "UTF8",
6684
- standard_conforming_strings: "on",
6685
- integer_datetimes: "on",
6686
- datestyle: "ISO, MDY",
6687
- intervalstyle: "postgres",
6688
- application_name: "",
6689
- is_superuser: "on",
6690
- max_identifier_length: "63"
6691
- };
6692
- const name = statement.name.toLowerCase();
6693
- const value = settings[name];
6694
- if (value === void 0) {
6695
- throw new TypeError(`unrecognized configuration parameter "${statement.name}"`);
6927
+ return this.#foreground(async () => {
6928
+ if (this.#closed)
6929
+ throw new Error("Database is closed");
6930
+ if (this.#statementTransactionExpired && statement.kind !== "transaction")
6931
+ throw new TransactionExpiredError();
6932
+ const statementNow = this.#now();
6933
+ statement = resolveMutationStatementDatetimes(statement, statementNow);
6934
+ if (statement.kind === "select") {
6935
+ return { kind: "rows", result: await this.query(statement.sql) };
6936
+ }
6937
+ if (statement.kind === "set") {
6938
+ return { kind: "set", action: statement.action, name: statement.name };
6939
+ }
6940
+ if (statement.kind === "show") {
6941
+ const settings = {
6942
+ search_path: "public",
6943
+ server_version: "16.0",
6944
+ server_version_num: "160000",
6945
+ timezone: "UTC",
6946
+ time_zone: "UTC",
6947
+ transaction_isolation: "repeatable read",
6948
+ default_transaction_isolation: "repeatable read",
6949
+ client_encoding: "UTF8",
6950
+ server_encoding: "UTF8",
6951
+ standard_conforming_strings: "on",
6952
+ integer_datetimes: "on",
6953
+ datestyle: "ISO, MDY",
6954
+ intervalstyle: "postgres",
6955
+ application_name: "",
6956
+ is_superuser: "on",
6957
+ max_identifier_length: "63"
6958
+ };
6959
+ const name = statement.name.toLowerCase();
6960
+ const value = settings[name];
6961
+ if (value === void 0) {
6962
+ throw new TypeError(`unrecognized configuration parameter "${statement.name}"`);
6963
+ }
6964
+ return {
6965
+ kind: "rows",
6966
+ result: await this.query(`SELECT $1 AS ${quoteIdentifier(name)}`, { params: [value] })
6967
+ };
6696
6968
  }
6697
- return {
6698
- kind: "rows",
6699
- result: await this.query(`SELECT $1 AS ${quoteIdentifier(name)}`, { params: [value] })
6700
- };
6701
- }
6702
- if (statement.kind === "create-enum") {
6703
- const values = validateEnumValues(statement.values, statement.name);
6704
- const storageName = `${ENUM_TYPE_PREFIX}${statement.name}`;
6705
- if (await this.store.getTableByName(storageName)) {
6706
- throw new TypeError(`Type already exists: ${statement.name}`);
6969
+ if (statement.kind === "create-enum") {
6970
+ const values = validateEnumValues(statement.values, statement.name);
6971
+ const storageName = `${ENUM_TYPE_PREFIX}${statement.name}`;
6972
+ if (await this.store.getTableByName(storageName)) {
6973
+ throw new TypeError(`Type already exists: ${statement.name}`);
6974
+ }
6975
+ await this.store.addTable({
6976
+ id: this.#createId(),
6977
+ name: storageName,
6978
+ columns: [
6979
+ {
6980
+ id: this.#createId(),
6981
+ name: "value",
6982
+ type: "string",
6983
+ nullable: false,
6984
+ hidden: true
6985
+ }
6986
+ ],
6987
+ managed: false,
6988
+ revision: 0,
6989
+ enumType: { name: statement.name, values },
6990
+ createdAt: dateIsoString(this.#now())
6991
+ });
6992
+ this.#planCache.clear();
6993
+ return { kind: "create-type", name: statement.name };
6707
6994
  }
6708
- await this.store.addTable({
6709
- id: this.#createId(),
6710
- name: storageName,
6711
- columns: [
6712
- {
6713
- id: this.#createId(),
6714
- name: "value",
6715
- type: "string",
6716
- nullable: false,
6717
- hidden: true
6718
- }
6719
- ],
6720
- managed: false,
6721
- revision: 0,
6722
- enumType: { name: statement.name, values },
6723
- createdAt: dateIsoString(this.#now())
6724
- });
6725
- this.#planCache.clear();
6726
- return { kind: "create-type", name: statement.name };
6727
- }
6728
- if (statement.kind === "create-sequence") {
6729
- const storageName = `${SEQUENCE_PREFIX}${statement.name}`;
6730
- if (await this.store.getTableByName(storageName)) {
6731
- throw new TypeError(`Sequence already exists: ${statement.name}`);
6732
- }
6733
- const columnId = this.#createId();
6734
- await this.store.addTable({
6735
- id: this.#createId(),
6736
- name: storageName,
6737
- columns: [
6738
- {
6739
- id: columnId,
6740
- name: "value",
6741
- type: "number",
6742
- integer: true,
6743
- nullable: false,
6744
- hidden: true,
6745
- defaultValue: { kind: "autoincrement" }
6746
- }
6747
- ],
6748
- managed: false,
6749
- revision: 0,
6750
- sequence: { name: statement.name, start: 1, columnId },
6751
- createdAt: dateIsoString(this.#now())
6752
- });
6753
- return { kind: "create-sequence", name: statement.name };
6754
- }
6755
- if (statement.kind === "create-table") {
6756
- if (statement.ifNotExists === true) {
6757
- const existing = await this.store.getTableByName(statement.table);
6758
- if (existing !== void 0)
6759
- return { kind: "create-table", table: statement.table };
6760
- }
6761
- try {
6762
- await this.createTable({
6763
- name: statement.table,
6764
- columns: statement.columns,
6765
- ...statement.checks === void 0 ? {} : { checks: statement.checks },
6766
- ...statement.foreignKeys === void 0 ? {} : { foreignKeys: statement.foreignKeys },
6767
- ...statement.uniqueKey === void 0 ? {} : { uniqueKey: statement.uniqueKey },
6768
- ...statement.compositePrimaryKey === void 0 ? {} : { compositePrimaryKey: statement.compositePrimaryKey },
6769
- ...statement.uniqueConstraints === void 0 ? {} : { uniqueConstraints: statement.uniqueConstraints },
6770
- ...statement.managed === void 0 ? {} : { managed: statement.managed }
6771
- });
6772
- } catch (error) {
6773
- if (statement.ifNotExists !== true || await this.store.getTableByName(statement.table) === void 0) {
6774
- throw error;
6995
+ if (statement.kind === "create-sequence") {
6996
+ const storageName = `${SEQUENCE_PREFIX}${statement.name}`;
6997
+ if (await this.store.getTableByName(storageName)) {
6998
+ throw new TypeError(`Sequence already exists: ${statement.name}`);
6775
6999
  }
7000
+ const columnId = this.#createId();
7001
+ await this.store.addTable({
7002
+ id: this.#createId(),
7003
+ name: storageName,
7004
+ columns: [
7005
+ {
7006
+ id: columnId,
7007
+ name: "value",
7008
+ type: "number",
7009
+ integer: true,
7010
+ nullable: false,
7011
+ hidden: true,
7012
+ defaultValue: { kind: "autoincrement" }
7013
+ }
7014
+ ],
7015
+ managed: false,
7016
+ revision: 0,
7017
+ sequence: { name: statement.name, start: 1, columnId },
7018
+ createdAt: dateIsoString(this.#now())
7019
+ });
7020
+ return { kind: "create-sequence", name: statement.name };
6776
7021
  }
6777
- return { kind: "create-table", table: statement.table };
6778
- }
6779
- if (statement.kind === "create-table-as") {
6780
- for (let attempt = 0; ; attempt += 1) {
6781
- const before = await this.store.getCatalogProbe();
6782
- const existing = await this.store.getTableByName(statement.table);
6783
- if (existing !== void 0) {
6784
- if (statement.ifNotExists === true) {
7022
+ if (statement.kind === "create-table") {
7023
+ if (statement.ifNotExists === true) {
7024
+ const existing = await this.store.getTableByName(statement.table);
7025
+ if (existing !== void 0)
6785
7026
  return { kind: "create-table", table: statement.table };
6786
- }
6787
- throw new TypeError(`Table already exists: ${statement.table}`);
6788
7027
  }
6789
- const records = await this.store.listTables();
6790
- const after = await this.store.getCatalogProbe();
6791
- if (after.catalogEpoch !== before.catalogEpoch) {
6792
- if (attempt >= this.#maxCommitRetries) {
6793
- throw new TableRecordConflictError(statement.table, 0, null);
7028
+ try {
7029
+ await this.createTable({
7030
+ name: statement.table,
7031
+ columns: statement.columns,
7032
+ ...statement.checks === void 0 ? {} : { checks: statement.checks },
7033
+ ...statement.foreignKeys === void 0 ? {} : { foreignKeys: statement.foreignKeys },
7034
+ ...statement.uniqueKey === void 0 ? {} : { uniqueKey: statement.uniqueKey },
7035
+ ...statement.compositePrimaryKey === void 0 ? {} : { compositePrimaryKey: statement.compositePrimaryKey },
7036
+ ...statement.uniqueConstraints === void 0 ? {} : { uniqueConstraints: statement.uniqueConstraints },
7037
+ ...statement.managed === void 0 ? {} : { managed: statement.managed }
7038
+ });
7039
+ } catch (error) {
7040
+ if (statement.ifNotExists !== true || await this.store.getTableByName(statement.table) === void 0) {
7041
+ throw error;
6794
7042
  }
6795
- continue;
6796
7043
  }
6797
- const schemas = new Map(records.map((record) => [
6798
- record.name,
6799
- record.columns.map(({ name, type, integer, sqlDomain }) => ({
6800
- name,
7044
+ return { kind: "create-table", table: statement.table };
7045
+ }
7046
+ if (statement.kind === "create-table-as") {
7047
+ for (let attempt = 0; ; attempt += 1) {
7048
+ const before = await this.store.getCatalogProbe();
7049
+ const existing = await this.store.getTableByName(statement.table);
7050
+ if (existing !== void 0) {
7051
+ if (statement.ifNotExists === true) {
7052
+ return { kind: "create-table", table: statement.table };
7053
+ }
7054
+ throw new TypeError(`Table already exists: ${statement.table}`);
7055
+ }
7056
+ const records = await this.store.listTables();
7057
+ const after = await this.store.getCatalogProbe();
7058
+ if (after.catalogEpoch !== before.catalogEpoch) {
7059
+ if (attempt >= this.#maxCommitRetries) {
7060
+ throw new TableRecordConflictError(statement.table, 0, null);
7061
+ }
7062
+ continue;
7063
+ }
7064
+ const schemas = new Map(records.map((record) => [
7065
+ record.name,
7066
+ record.columns.map(({ name, type, integer, sqlDomain }) => ({
7067
+ name,
7068
+ type,
7069
+ ...integer === true ? { integer: true } : {},
7070
+ ...sqlDomain === void 0 ? {} : { sqlDomain }
7071
+ }))
7072
+ ]));
7073
+ const plan2 = await this.#applyCatalogRewrites(statement.query, before);
7074
+ const tableColumns = inferBlockSchema(plan2, schemas).map(({ name, type, integer, sqlDomain }) => ({
7075
+ id: this.#createId(),
7076
+ name: validateName(name, "Column"),
6801
7077
  type,
6802
7078
  ...integer === true ? { integer: true } : {},
6803
- ...sqlDomain === void 0 ? {} : { sqlDomain }
6804
- }))
6805
- ]));
6806
- const plan2 = await this.#applyCatalogRewrites(statement.query, before);
6807
- const tableColumns = inferBlockSchema(plan2, schemas).map(({ name, type, integer, sqlDomain }) => ({
6808
- id: this.#createId(),
6809
- name: validateName(name, "Column"),
6810
- type,
6811
- ...integer === true ? { integer: true } : {},
6812
- ...sqlDomain === void 0 ? {} : { sqlDomain: structuredClone(sqlDomain) },
6813
- nullable: true
6814
- }));
6815
- const pendingTable = {
6816
- id: this.#createId(),
6817
- name: validateName(statement.table, "Table"),
6818
- columns: tableColumns,
6819
- revision: 0,
6820
- managed: false,
6821
- createdAt: dateIsoString(this.#now())
6822
- };
6823
- const transaction = await this.#transactions.beginWithPendingTable(pendingTable, before.catalogEpoch);
6824
- try {
6825
- const executionMemoryBudgetBytes = Math.min(this.#queryExecutionMemoryBudgetBytes, DEFAULT_QUERY_MEMORY_BUDGET_BYTES);
6826
- const stageBatchRows = 16384;
6827
- let nextRowId = 1n;
6828
- let stagedRows = 0;
6829
- transaction.limitLevelZeroSegments(pendingTable.id, MAX_LEVEL_ZERO_SEGMENTS);
6830
- const stageRows = async (rows2) => {
6831
- if (rows2.length === 0)
6832
- return;
6833
- const batchResult = {
6834
- columns: tableColumns.map(({ name }) => name),
6835
- columnDomains: tableColumns.map(({ sqlDomain }) => sqlDomain ?? null),
6836
- rows: [...rows2]
7079
+ ...sqlDomain === void 0 ? {} : { sqlDomain: structuredClone(sqlDomain) },
7080
+ nullable: true
7081
+ }));
7082
+ const pendingTable = {
7083
+ id: this.#createId(),
7084
+ name: validateName(statement.table, "Table"),
7085
+ columns: tableColumns,
7086
+ revision: 0,
7087
+ managed: false,
7088
+ createdAt: dateIsoString(this.#now())
7089
+ };
7090
+ const transaction = await this.#transactions.beginWithPendingTable(pendingTable, before.catalogEpoch);
7091
+ try {
7092
+ const executionMemoryBudgetBytes = Math.min(this.#queryExecutionMemoryBudgetBytes, DEFAULT_QUERY_MEMORY_BUDGET_BYTES);
7093
+ const stageBatchRows = 16384;
7094
+ let nextRowId = 1n;
7095
+ let stagedRows = 0;
7096
+ transaction.limitLevelZeroSegments(pendingTable.id, MAX_LEVEL_ZERO_SEGMENTS);
7097
+ const stageRows = async (rows2) => {
7098
+ if (rows2.length === 0)
7099
+ return;
7100
+ const batchResult = {
7101
+ columns: tableColumns.map(({ name }) => name),
7102
+ columnDomains: tableColumns.map(({ sqlDomain }) => sqlDomain ?? null),
7103
+ rows: [...rows2]
7104
+ };
7105
+ if (queryResultRetainedBytes(batchResult) > executionMemoryBudgetBytes) {
7106
+ throw new QueryMemoryBudgetError("CREATE TABLE AS result batch", queryResultRetainedBytes(batchResult), 0, executionMemoryBudgetBytes);
7107
+ }
7108
+ const batch = {
7109
+ columns: Object.fromEntries(tableColumns.map((column) => [
7110
+ column.name,
7111
+ rows2.map((row) => storedSqlValueFromExecution(column, row[column.name] ?? null))
7112
+ ])),
7113
+ rowCount: rows2.length
7114
+ };
7115
+ normalizeDomainBatch(pendingTable, batch);
7116
+ validateBatch(pendingTable, batch);
7117
+ const endExclusive = nextRowId + BigInt(rows2.length);
7118
+ await this.#stageInsertSegment(transaction, pendingTable, batch, rows2.length, "insert", { start: nextRowId, endExclusive });
7119
+ nextRowId = endExclusive;
7120
+ stagedRows += rows2.length;
6837
7121
  };
6838
- if (queryResultRetainedBytes(batchResult) > executionMemoryBudgetBytes) {
6839
- throw new QueryMemoryBudgetError("CREATE TABLE AS result batch", queryResultRetainedBytes(batchResult), 0, executionMemoryBudgetBytes);
6840
- }
6841
- const batch = {
6842
- columns: Object.fromEntries(tableColumns.map((column) => [
6843
- column.name,
6844
- rows2.map((row) => storedSqlValueFromExecution(column, row[column.name] ?? null))
6845
- ])),
6846
- rowCount: rows2.length
7122
+ const queryOptions = {
7123
+ ...transaction.snapshotVersion === null ? {} : { version: transaction.snapshotVersion },
7124
+ executionMemoryBudgetBytes,
7125
+ spillToStorage: true,
7126
+ memoize: false
6847
7127
  };
6848
- normalizeDomainBatch(pendingTable, batch);
6849
- validateBatch(pendingTable, batch);
6850
- const endExclusive = nextRowId + BigInt(rows2.length);
6851
- await this.#stageInsertSegment(transaction, pendingTable, batch, rows2.length, "insert", { start: nextRowId, endExclusive });
6852
- nextRowId = endExclusive;
6853
- stagedRows += rows2.length;
6854
- };
6855
- const queryOptions = {
6856
- ...transaction.snapshotVersion === null ? {} : { version: transaction.snapshotVersion },
6857
- executionMemoryBudgetBytes,
6858
- spillToStorage: true,
6859
- memoize: false
6860
- };
6861
- let streamed;
6862
- if (!compiledPlanIsGrouped(plan2) && plan2.orderBy.length === 0 && plan2.joins.length === 0 && this.#canStreamPlanShape(plan2, queryOptions)) {
6863
- streamed = await this.#queryStreamed(plan2, queryOptions, void 0, void 0, {
6864
- batchRows: stageBatchRows,
6865
- signal: new AbortController().signal,
6866
- consume: async (batch) => stageRows(batch.rows)
6867
- });
6868
- }
6869
- if (streamed === void 0) {
6870
- if (stagedRows !== 0) {
6871
- throw new Error("CREATE TABLE AS streaming stopped after staging rows");
7128
+ let streamed;
7129
+ if (!compiledPlanIsGrouped(plan2) && plan2.orderBy.length === 0 && plan2.joins.length === 0 && this.#canStreamPlanShape(plan2, queryOptions)) {
7130
+ streamed = await this.#queryStreamed(plan2, queryOptions, void 0, void 0, {
7131
+ batchRows: stageBatchRows,
7132
+ signal: new AbortController().signal,
7133
+ consume: async (batch) => stageRows(batch.rows)
7134
+ });
6872
7135
  }
6873
- const result = await this.#sessionQueryPlan(transaction, plan2, queryOptions);
6874
- const retainedBytes = queryResultRetainedBytes(result);
6875
- if (retainedBytes > executionMemoryBudgetBytes) {
6876
- throw new QueryMemoryBudgetError("CREATE TABLE AS result", retainedBytes, 0, executionMemoryBudgetBytes);
7136
+ if (streamed === void 0) {
7137
+ if (stagedRows !== 0) {
7138
+ throw new Error("CREATE TABLE AS streaming stopped after staging rows");
7139
+ }
7140
+ const result = await this.#sessionQueryPlan(transaction, plan2, queryOptions);
7141
+ const retainedBytes = queryResultRetainedBytes(result);
7142
+ if (retainedBytes > executionMemoryBudgetBytes) {
7143
+ throw new QueryMemoryBudgetError("CREATE TABLE AS result", retainedBytes, 0, executionMemoryBudgetBytes);
7144
+ }
7145
+ for (let start = 0; start < result.rows.length; start += stageBatchRows) {
7146
+ await stageRows(result.rows.slice(start, start + stageBatchRows));
7147
+ }
6877
7148
  }
6878
- for (let start = 0; start < result.rows.length; start += stageBatchRows) {
6879
- await stageRows(result.rows.slice(start, start + stageBatchRows));
7149
+ transaction.markTableChanged(pendingTable.id);
7150
+ const manifest = await transaction.commit();
7151
+ this.#afterCommit(manifest);
7152
+ return { kind: "create-table", table: statement.table };
7153
+ } catch (error) {
7154
+ await transaction.abort().catch(() => void 0);
7155
+ if ((error instanceof WriteConflictError || error instanceof SchemaConflictError || error instanceof TableRecordConflictError) && attempt < this.#maxCommitRetries) {
7156
+ continue;
6880
7157
  }
7158
+ throw error;
6881
7159
  }
6882
- transaction.markTableChanged(pendingTable.id);
6883
- const manifest = await transaction.commit();
6884
- this.#afterCommit(manifest);
6885
- return { kind: "create-table", table: statement.table };
6886
- } catch (error) {
6887
- await transaction.abort().catch(() => void 0);
6888
- if ((error instanceof WriteConflictError || error instanceof SchemaConflictError || error instanceof TableRecordConflictError) && attempt < this.#maxCommitRetries) {
6889
- continue;
6890
- }
6891
- throw error;
6892
7160
  }
6893
7161
  }
6894
- }
6895
- if (statement.kind === "create-index") {
6896
- const indexStatement = statement;
6897
- const existing = (await this.store.listTables()).some((table2) => Object.values(table2.secondaryIndexes ?? {}).some((index) => index.name === indexStatement.index));
6898
- if (existing && statement.ifNotExists === true) {
7162
+ if (statement.kind === "create-index") {
7163
+ const indexStatement = statement;
7164
+ const existing = (await this.store.listTables()).some((table2) => Object.values(table2.secondaryIndexes ?? {}).some((index) => index.name === indexStatement.index));
7165
+ if (existing && statement.ifNotExists === true) {
7166
+ return {
7167
+ kind: "create-index",
7168
+ index: statement.index,
7169
+ table: statement.table,
7170
+ column: statement.columns[0]?.name ?? "",
7171
+ columns: statement.columns.map((column) => column.name),
7172
+ unique: statement.unique === true
7173
+ };
7174
+ }
7175
+ try {
7176
+ await this.createIndex(statement.index, statement.table, statement.columns, {
7177
+ unique: statement.unique === true
7178
+ });
7179
+ } catch (error) {
7180
+ const admitted = statement.ifNotExists === true && (await this.store.listTables()).some((table2) => Object.values(table2.secondaryIndexes ?? {}).some((index) => index.name === indexStatement.index));
7181
+ if (!admitted)
7182
+ throw error;
7183
+ }
6899
7184
  return {
6900
7185
  kind: "create-index",
6901
7186
  index: statement.index,
@@ -6905,439 +7190,423 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
6905
7190
  unique: statement.unique === true
6906
7191
  };
6907
7192
  }
6908
- try {
6909
- await this.createIndex(statement.index, statement.table, statement.columns, {
6910
- unique: statement.unique === true
7193
+ if (statement.kind === "drop-index") {
7194
+ const dropped = await this.dropIndex(statement.index, {
7195
+ ...statement.ifExists === true ? { ifExists: true } : {}
6911
7196
  });
6912
- } catch (error) {
6913
- const admitted = statement.ifNotExists === true && (await this.store.listTables()).some((table2) => Object.values(table2.secondaryIndexes ?? {}).some((index) => index.name === indexStatement.index));
6914
- if (!admitted)
6915
- throw error;
7197
+ return { kind: "drop-index", index: statement.index, dropped };
6916
7198
  }
6917
- return {
6918
- kind: "create-index",
6919
- index: statement.index,
6920
- table: statement.table,
6921
- column: statement.columns[0]?.name ?? "",
6922
- columns: statement.columns.map((column) => column.name),
6923
- unique: statement.unique === true
6924
- };
6925
- }
6926
- if (statement.kind === "drop-index") {
6927
- const dropped = await this.dropIndex(statement.index, {
6928
- ...statement.ifExists === true ? { ifExists: true } : {}
6929
- });
6930
- return { kind: "drop-index", index: statement.index, dropped };
6931
- }
6932
- if (statement.kind === "add-column") {
6933
- let pinnedTableId;
6934
- const addColumnStatement = statement;
6935
- for (let attempt = 0; ; attempt += 1) {
6936
- const proof = await this.#stableCatalogProof();
6937
- try {
6938
- let added = addColumnStatement.column;
6939
- if (added.sqlDomain?.kind === "enum" && added.sqlDomain.values.length === 0) {
6940
- const enumName = added.sqlDomain.name;
6941
- const enumRecord = proof.records.find((record2) => record2.name === `${ENUM_TYPE_PREFIX}${enumName}`);
6942
- if (enumRecord?.enumType === void 0) {
6943
- throw new TypeError(`Unsupported column type: ${enumName}`);
6944
- }
6945
- added = {
6946
- ...added,
6947
- sqlDomain: {
6948
- kind: "enum",
6949
- name: enumRecord.enumType.name,
6950
- values: [...enumRecord.enumType.values]
7199
+ if (statement.kind === "add-column") {
7200
+ let pinnedTableId;
7201
+ const addColumnStatement = statement;
7202
+ for (let attempt = 0; ; attempt += 1) {
7203
+ const proof = await this.#stableCatalogProof();
7204
+ try {
7205
+ let added = addColumnStatement.column;
7206
+ if (added.sqlDomain?.kind === "enum" && added.sqlDomain.values.length === 0) {
7207
+ const enumName = added.sqlDomain.name;
7208
+ const enumRecord = proof.records.find((record2) => record2.name === `${ENUM_TYPE_PREFIX}${enumName}`);
7209
+ if (enumRecord?.enumType === void 0) {
7210
+ throw new TypeError(`Unsupported column type: ${enumName}`);
6951
7211
  }
6952
- };
6953
- }
6954
- const record = proof.records.find((candidate) => candidate.name === addColumnStatement.table);
6955
- if (record === void 0)
6956
- throw new UnknownTableError(addColumnStatement.table);
6957
- if (record.view !== void 0)
6958
- throw new TypeError(`${addColumnStatement.table} is a view, not a table`);
6959
- pinnedTableId ??= record.id;
6960
- if (record.id !== pinnedTableId) {
6961
- throw new TableRecordConflictError(pinnedTableId, 0, record.revision);
6962
- }
6963
- if (added.nullable !== true && !(addColumnStatement.allowNonNullableWithBackfill === true && added.backfill !== void 0)) {
6964
- throw new TypeError("ALTER TABLE ADD COLUMN adds a nullable column; existing rows have no value for it");
6965
- }
6966
- if (record.columns.some(({ name }) => name === added.name)) {
6967
- throw new TypeError(`Column already exists: ${addColumnStatement.table}.${added.name}`);
6968
- }
6969
- if (added.generatedValue !== void 0) {
6970
- throw new TypeError("ALTER TABLE cannot add a generated column without rewriting existing rows");
6971
- }
6972
- if (added.defaultValue !== void 0) {
6973
- validateColumnDefault({
6974
- name: added.name,
6975
- type: added.type,
6976
- ...added.integer === true ? { integer: true } : {},
6977
- ...added.sqlDomain === void 0 ? {} : { sqlDomain: added.sqlDomain },
6978
- nullable: added.nullable ?? false,
6979
- isUniqueKey: false,
6980
- ...added.enumValues === void 0 ? {} : { enumValues: added.enumValues }
6981
- }, added.defaultValue);
6982
- if (added.defaultValue.kind === "literal" && added.sqlDomain !== void 0) {
6983
- normalizeSqlDomainValue(added.sqlDomain, added.defaultValue.value);
7212
+ added = {
7213
+ ...added,
7214
+ sqlDomain: {
7215
+ kind: "enum",
7216
+ name: enumRecord.enumType.name,
7217
+ values: [...enumRecord.enumType.values]
7218
+ }
7219
+ };
7220
+ }
7221
+ const record = proof.records.find((candidate) => candidate.name === addColumnStatement.table);
7222
+ if (record === void 0)
7223
+ throw new UnknownTableError(addColumnStatement.table);
7224
+ if (record.view !== void 0)
7225
+ throw new TypeError(`${addColumnStatement.table} is a view, not a table`);
7226
+ pinnedTableId ??= record.id;
7227
+ if (record.id !== pinnedTableId) {
7228
+ throw new TableRecordConflictError(pinnedTableId, 0, record.revision);
6984
7229
  }
6985
- if (added.defaultValue.kind === "expression") {
6986
- validateDefaultExpression(added.defaultValue.sql, {
6987
- name: `${addColumnStatement.table}.${added.name}`,
7230
+ if (added.nullable !== true && !(addColumnStatement.allowNonNullableWithBackfill === true && added.backfill !== void 0)) {
7231
+ throw new TypeError("ALTER TABLE ADD COLUMN adds a nullable column; existing rows have no value for it");
7232
+ }
7233
+ if (record.columns.some(({ name }) => name === added.name)) {
7234
+ throw new TypeError(`Column already exists: ${addColumnStatement.table}.${added.name}`);
7235
+ }
7236
+ if (added.generatedValue !== void 0) {
7237
+ throw new TypeError("ALTER TABLE cannot add a generated column without rewriting existing rows");
7238
+ }
7239
+ if (added.defaultValue !== void 0) {
7240
+ validateColumnDefault({
7241
+ name: added.name,
6988
7242
  type: added.type,
6989
- ...added.sqlDomain === void 0 ? {} : { sqlDomain: added.sqlDomain }
6990
- });
7243
+ ...added.integer === true ? { integer: true } : {},
7244
+ ...added.sqlDomain === void 0 ? {} : { sqlDomain: added.sqlDomain },
7245
+ nullable: added.nullable ?? false,
7246
+ isUniqueKey: false,
7247
+ ...added.enumValues === void 0 ? {} : { enumValues: added.enumValues }
7248
+ }, added.defaultValue);
7249
+ if (added.defaultValue.kind === "literal" && added.sqlDomain !== void 0) {
7250
+ normalizeSqlDomainValue(added.sqlDomain, added.defaultValue.value);
7251
+ }
7252
+ if (added.defaultValue.kind === "expression") {
7253
+ validateDefaultExpression(added.defaultValue.sql, {
7254
+ name: `${addColumnStatement.table}.${added.name}`,
7255
+ type: added.type,
7256
+ ...added.sqlDomain === void 0 ? {} : { sqlDomain: added.sqlDomain }
7257
+ });
7258
+ }
6991
7259
  }
6992
- }
6993
- const columns = [
6994
- ...structuredClone(record.columns),
6995
- {
6996
- id: this.#createId(),
6997
- name: added.name,
6998
- type: added.type,
6999
- ...added.integer === true ? { integer: true } : {},
7000
- ...added.sqlDomain === void 0 ? {} : { sqlDomain: structuredClone(added.sqlDomain) },
7001
- nullable: added.nullable ?? false,
7002
- ...added.defaultValue === void 0 ? {} : { defaultValue: added.defaultValue },
7003
- ...added.enumValues === void 0 ? {} : { enumValues: [...added.enumValues] },
7004
- ...added.backfill === void 0 ? {} : { backfill: normalizedColumnBackfill(added, added.backfill) }
7260
+ const columns = [
7261
+ ...structuredClone(record.columns),
7262
+ {
7263
+ id: this.#createId(),
7264
+ name: added.name,
7265
+ type: added.type,
7266
+ ...added.integer === true ? { integer: true } : {},
7267
+ ...added.sqlDomain === void 0 ? {} : { sqlDomain: structuredClone(added.sqlDomain) },
7268
+ nullable: added.nullable ?? false,
7269
+ ...added.defaultValue === void 0 ? {} : { defaultValue: added.defaultValue },
7270
+ ...added.enumValues === void 0 ? {} : { enumValues: [...added.enumValues] },
7271
+ ...added.backfill === void 0 ? {} : { backfill: normalizedColumnBackfill(added, added.backfill) }
7272
+ }
7273
+ ];
7274
+ assertTriggerBodiesAcceptTableSchema(proof.records, record, columns);
7275
+ const candidateSchemas = catalogQuerySchemas(proof.records);
7276
+ candidateSchemas.set(record.name, columns);
7277
+ assertDependentViewsKeepSchema(proof.records, record, candidateSchemas);
7278
+ await this.store.updateTable(record.id, record.revision, {
7279
+ columns,
7280
+ expectedCatalogEpoch: proof.catalogEpoch
7281
+ });
7282
+ return { kind: "add-column", table: addColumnStatement.table, column: added.name };
7283
+ } catch (error) {
7284
+ if (!(error instanceof TableRecordConflictError) || attempt >= this.#maxCommitRetries) {
7285
+ throw error;
7005
7286
  }
7006
- ];
7007
- assertTriggerBodiesAcceptTableSchema(proof.records, record, columns);
7008
- const candidateSchemas = catalogQuerySchemas(proof.records);
7009
- candidateSchemas.set(record.name, columns);
7010
- assertDependentViewsKeepSchema(proof.records, record, candidateSchemas);
7011
- await this.store.updateTable(record.id, record.revision, {
7012
- columns,
7013
- expectedCatalogEpoch: proof.catalogEpoch
7014
- });
7015
- return { kind: "add-column", table: addColumnStatement.table, column: added.name };
7016
- } catch (error) {
7017
- if (!(error instanceof TableRecordConflictError) || attempt >= this.#maxCommitRetries) {
7018
- throw error;
7019
7287
  }
7020
7288
  }
7021
7289
  }
7022
- }
7023
- if (statement.kind === "drop-column") {
7024
- const dropped = await this.dropColumn(statement.table, statement.column, {
7025
- ...statement.ifExists === true ? { ifExists: true } : {}
7026
- });
7027
- return {
7028
- kind: "drop-column",
7029
- table: statement.table,
7030
- column: statement.column,
7031
- dropped
7032
- };
7033
- }
7034
- if (statement.kind === "merge")
7035
- return this.#runMerge(statement);
7036
- if (statement.kind === "create-view") {
7037
- await this.createView(statement.view, statement.sql, {
7038
- ...statement.orReplace === true ? { orReplace: true } : {},
7039
- ...statement.managed === true ? { managed: true } : {}
7040
- });
7041
- return { kind: "create-view", view: statement.view };
7042
- }
7043
- if (statement.kind === "drop-view") {
7044
- const dropped = await this.dropView(statement.view, {
7045
- ...statement.ifExists === true ? { ifExists: true } : {}
7046
- });
7047
- return { kind: "drop-view", view: statement.view, dropped };
7048
- }
7049
- if (statement.kind === "drop-table") {
7050
- const dropped = await this.dropTable(statement.table, {
7051
- ...statement.ifExists === true ? { ifExists: true } : {}
7052
- });
7053
- return { kind: "drop-table", table: statement.table, dropped };
7054
- }
7055
- if (statement.kind === "create-trigger") {
7056
- await this.#createTrigger(statement.table, statement.trigger);
7057
- return { kind: "create-trigger", table: statement.table, name: statement.trigger.name };
7058
- }
7059
- if (statement.kind === "drop-trigger") {
7060
- await this.#dropTrigger(statement.name);
7061
- return { kind: "drop-trigger", name: statement.name };
7062
- }
7063
- if (statement.kind === "transaction") {
7064
- return this.#runTransactionStatement(statement.action, statement.name);
7065
- }
7066
- if (statement.kind === "insert") {
7067
- statement = await this.#materializeInsertExpressions(statement, statementNow);
7068
- }
7069
- if (options.writer === void 0) {
7070
- for (let attempt = 0; ; attempt += 1) {
7071
- try {
7072
- const completed = await this.#openWriteScope((_session, _transaction, writer) => this.runStatement(statement, { ...options, writer }), { durableSnapshot: false });
7073
- const result = completed.result;
7074
- if (result.kind !== "insert" && result.kind !== "update" && result.kind !== "delete") {
7075
- throw new Error(`Mutation statement returned an unexpected result: ${result.kind}`);
7076
- }
7077
- return result.rowCount === 0 || completed.version === null ? result : { ...result, version: completed.version };
7078
- } catch (error) {
7079
- if (!(error instanceof WriteConflictError) && !(error instanceof SchemaConflictError) || attempt >= this.#maxCommitRetries) {
7080
- throw error;
7290
+ if (statement.kind === "drop-column") {
7291
+ const dropped = await this.dropColumn(statement.table, statement.column, {
7292
+ ...statement.ifExists === true ? { ifExists: true } : {}
7293
+ });
7294
+ return {
7295
+ kind: "drop-column",
7296
+ table: statement.table,
7297
+ column: statement.column,
7298
+ dropped
7299
+ };
7300
+ }
7301
+ if (statement.kind === "merge")
7302
+ return this.#runMerge(statement);
7303
+ if (statement.kind === "create-view") {
7304
+ await this.createView(statement.view, statement.sql, {
7305
+ ...statement.orReplace === true ? { orReplace: true } : {},
7306
+ ...statement.managed === true ? { managed: true } : {}
7307
+ });
7308
+ return { kind: "create-view", view: statement.view };
7309
+ }
7310
+ if (statement.kind === "drop-view") {
7311
+ const dropped = await this.dropView(statement.view, {
7312
+ ...statement.ifExists === true ? { ifExists: true } : {}
7313
+ });
7314
+ return { kind: "drop-view", view: statement.view, dropped };
7315
+ }
7316
+ if (statement.kind === "drop-table") {
7317
+ const dropped = await this.dropTable(statement.table, {
7318
+ ...statement.ifExists === true ? { ifExists: true } : {}
7319
+ });
7320
+ return { kind: "drop-table", table: statement.table, dropped };
7321
+ }
7322
+ if (statement.kind === "create-trigger") {
7323
+ await this.#createTrigger(statement.table, statement.trigger);
7324
+ return { kind: "create-trigger", table: statement.table, name: statement.trigger.name };
7325
+ }
7326
+ if (statement.kind === "drop-trigger") {
7327
+ await this.#dropTrigger(statement.name);
7328
+ return { kind: "drop-trigger", name: statement.name };
7329
+ }
7330
+ if (statement.kind === "transaction") {
7331
+ const action = statement.action === "rollback-to" ? "ROLLBACK TO" : statement.action;
7332
+ return this.execute(`${action}${statement.name === void 0 ? "" : ` ${quoteIdentifier(statement.name)}`}`);
7333
+ }
7334
+ if (options.writer === void 0) {
7335
+ return this.#coordinateWrite(async () => {
7336
+ for (let attempt = 0; ; attempt += 1) {
7337
+ try {
7338
+ const completed = await this.#openWriteScope((_session, _transaction, writer) => this.runStatement(statement, { ...options, writer }), { durableSnapshot: false });
7339
+ const result = completed.result;
7340
+ if (result.kind !== "insert" && result.kind !== "update" && result.kind !== "delete") {
7341
+ throw new Error(`Mutation statement returned an unexpected result: ${result.kind}`);
7342
+ }
7343
+ return result.rowCount === 0 || completed.version === null ? result : { ...result, version: completed.version };
7344
+ } catch (error) {
7345
+ if (!(error instanceof WriteConflictError) && !(error instanceof SchemaConflictError) || attempt >= this.#maxCommitRetries) {
7346
+ throw error;
7347
+ }
7348
+ }
7081
7349
  }
7350
+ });
7351
+ }
7352
+ if (statement.kind === "insert" && statement.query !== void 0) {
7353
+ statement = await this.#materializeInsertSelect(statement, options.writer);
7354
+ }
7355
+ if (statement.kind === "insert" && statement.columns.length === 0) {
7356
+ const table2 = await this.#findTable(statement.table);
7357
+ const columns = visibleTableColumns(table2).map((column) => column.name);
7358
+ if (statement.defaultValues === true) {
7359
+ } else if (statement.rows.some((row) => row.length !== columns.length)) {
7360
+ throw new TypeError("Each INSERT row must match the table column count");
7361
+ } else {
7362
+ statement = { ...statement, columns };
7082
7363
  }
7083
7364
  }
7084
- }
7085
- if (statement.kind === "insert" && statement.query !== void 0) {
7086
- statement = await this.#materializeInsertSelect(statement, options.writer);
7087
- }
7088
- if (statement.kind === "insert" && statement.columns.length === 0) {
7089
- const table2 = await this.#findTable(statement.table);
7090
- const columns = visibleTableColumns(table2).map((column) => column.name);
7091
- if (statement.defaultValues === true) {
7092
- } else if (statement.rows.some((row) => row.length !== columns.length)) {
7093
- throw new TypeError("Each INSERT row must match the table column count");
7094
- } else {
7095
- statement = { ...statement, columns };
7365
+ if (statement.kind === "insert") {
7366
+ const reader = options.writer;
7367
+ statement = await this.#materializeInsertExpressions(statement, statementNow, (plan2) => reader.queryPlan(plan2));
7096
7368
  }
7097
- }
7098
- if (statement.kind === "insert") {
7099
- statement = await this.#materializeInsertExpressions(statement, statementNow);
7100
- }
7101
- if (statement.returning !== void 0 && options.returning === void 0) {
7102
- options = { ...options, returning: statement.returning };
7103
- } else if (statement.returningItems !== void 0 && options.returning === void 0) {
7104
- options = { ...options, returning: "*" };
7105
- }
7106
- if (statement.kind === "insert" && statement.onConflict?.action === "nothing") {
7107
- statement = await this.#filterConflictingInsertRows(statement, options.writer);
7108
- }
7109
- if (statement.kind === "insert" && statement.onConflict?.action === "update") {
7110
- return this.#mergeConflictingInsertRows(statement, options);
7111
- }
7112
- if (statement.kind === "insert" && statement.rows.length === 0) {
7113
- const table2 = await this.#findTable(statement.table);
7114
- const returningColumns2 = options.returning === void 0 ? void 0 : options.returning === "*" ? visibleTableColumns(table2).map(({ name }) => name) : [...options.returning];
7115
- return {
7116
- kind: "insert",
7117
- table: statement.table,
7118
- rowCount: 0,
7119
- ...returningColumns2 === void 0 ? {} : returningExecuteFields(table2, returningColumns2, [])
7120
- };
7121
- }
7122
- if (statement.kind === "insert") {
7123
- const table2 = await this.#findTable(statement.table);
7124
- const viaUpsert = statement.onConflict?.action === "replace";
7125
- if (viaUpsert) {
7126
- const keyColumn2 = getUniqueKeyColumn(table2);
7127
- const conflictColumns = statement.onConflict?.columns ?? (statement.onConflict === void 0 ? [] : [statement.onConflict.column]);
7128
- const addressColumns = keyColumn2?.hidden === true ? primaryKeyColumns(table2).map(({ name }) => name) : keyColumn2 === void 0 ? [] : [keyColumn2.name];
7129
- if (keyColumn2 === void 0 || conflictColumns.length !== addressColumns.length || conflictColumns.some((name, index) => name !== addressColumns[index])) {
7130
- throw new TypeError(`ON CONFLICT targets the table's primary or unique key columns: ${addressColumns.join(", ") || "(none)"}`);
7131
- }
7132
- }
7133
- const spelledColumns = foldInsertColumns(table2, statement).columns;
7134
- for (const name of spelledColumns) {
7135
- if (!table2.columns.some((column) => column.name === name)) {
7136
- throw new TypeError(`INSERT column does not exist: ${name}`);
7137
- }
7138
- }
7139
- const input = insertStatementBatch({ ...statement, columns: spelledColumns });
7140
- coerceSqlWriteBatch(table2, input);
7141
- normalizeDomainBatch(table2, input);
7142
- let returningColumns2;
7143
- if (options.returning !== void 0) {
7144
- returningColumns2 = options.returning === "*" ? visibleTableColumns(table2).map((column) => column.name) : options.returning;
7145
- for (const name of returningColumns2) {
7369
+ if (statement.returning !== void 0 && options.returning === void 0) {
7370
+ options = { ...options, returning: statement.returning };
7371
+ } else if (statement.returningItems !== void 0 && options.returning === void 0) {
7372
+ options = { ...options, returning: "*" };
7373
+ }
7374
+ if (statement.kind === "insert" && statement.onConflict?.action === "nothing") {
7375
+ statement = await this.#filterConflictingInsertRows(statement, options.writer);
7376
+ }
7377
+ if (statement.kind === "insert" && statement.onConflict?.action === "update") {
7378
+ return this.#mergeConflictingInsertRows(statement, options);
7379
+ }
7380
+ if (statement.kind === "insert" && statement.rows.length === 0) {
7381
+ const table2 = await this.#findTable(statement.table);
7382
+ const returningColumns2 = options.returning === void 0 ? void 0 : options.returning === "*" ? visibleTableColumns(table2).map(({ name }) => name) : [...options.returning];
7383
+ return {
7384
+ kind: "insert",
7385
+ table: statement.table,
7386
+ rowCount: 0,
7387
+ ...returningColumns2 === void 0 ? {} : returningExecuteFields(table2, returningColumns2, [])
7388
+ };
7389
+ }
7390
+ if (statement.kind === "insert") {
7391
+ const table2 = await this.#findTable(statement.table);
7392
+ const viaUpsert = statement.onConflict?.action === "replace";
7393
+ if (viaUpsert) {
7394
+ const keyColumn2 = getUniqueKeyColumn(table2);
7395
+ const conflictColumns = statement.onConflict?.columns ?? (statement.onConflict === void 0 ? [] : [statement.onConflict.column]);
7396
+ const addressColumns = keyColumn2?.hidden === true ? primaryKeyColumns(table2).map(({ name }) => name) : keyColumn2 === void 0 ? [] : [keyColumn2.name];
7397
+ if (keyColumn2 === void 0 || conflictColumns.length !== addressColumns.length || conflictColumns.some((name, index) => name !== addressColumns[index])) {
7398
+ throw new TypeError(`ON CONFLICT targets the table's primary or unique key columns: ${addressColumns.join(", ") || "(none)"}`);
7399
+ }
7400
+ }
7401
+ const spelledColumns = foldInsertColumns(table2, statement).columns;
7402
+ for (const name of spelledColumns) {
7146
7403
  if (!table2.columns.some((column) => column.name === name)) {
7147
- throw new TypeError(`RETURNING column does not exist: ${name}`);
7404
+ throw new TypeError(`INSERT column does not exist: ${name}`);
7148
7405
  }
7149
7406
  }
7150
- }
7151
- const writer = options.writer;
7152
- const stagedKeyColumn = writer === void 0 ? void 0 : getUniqueKeyColumn(table2);
7153
- if (writer !== void 0 && !viaUpsert && stagedKeyColumn !== void 0) {
7154
- const assignedGenerated = table2.columns.find((column) => column.generatedValue !== void 0 && spelledColumns.includes(column.name));
7155
- if (assignedGenerated !== void 0) {
7156
- throw new TypeError(`Generated column cannot be assigned: ${assignedGenerated.name}`);
7407
+ const input = insertStatementBatch({ ...statement, columns: spelledColumns });
7408
+ coerceSqlWriteBatch(table2, input);
7409
+ normalizeDomainBatch(table2, input);
7410
+ let returningColumns2;
7411
+ if (options.returning !== void 0) {
7412
+ returningColumns2 = options.returning === "*" ? visibleTableColumns(table2).map((column) => column.name) : options.returning;
7413
+ for (const name of returningColumns2) {
7414
+ if (!table2.columns.some((column) => column.name === name)) {
7415
+ throw new TypeError(`RETURNING column does not exist: ${name}`);
7416
+ }
7417
+ }
7157
7418
  }
7158
- await this.#assertStagedInsertKeysFree(table2, stagedKeyColumn, input, writer);
7159
- }
7160
- const result = await (viaUpsert ? writer?.upsertBatch(statement.table, input) ?? this.#erased.upsertBatch(statement.table, input) : writer?.insertBatch(statement.table, input) ?? this.#erased.insertBatch(statement.table, input));
7161
- const generated = "generatedColumns" in result ? result.generatedColumns ?? {} : {};
7162
- return {
7163
- kind: "insert",
7164
- table: statement.table,
7165
- rowCount: result.rowCount,
7166
- ...result.version === void 0 ? {} : { version: result.version },
7167
- ...returningColumns2 === void 0 ? {} : {
7168
- ...returningExecuteFields(table2, returningColumns2, statement.rows.map((row, rowIndex) => Object.fromEntries(returningColumns2.map((name) => {
7169
- const column = table2.columns.find((candidate) => candidate.name === name);
7170
- const position = spelledColumns.indexOf(name);
7171
- const inserted = position < 0 ? void 0 : row[position];
7172
- const value = generated[name]?.[rowIndex] ?? input.columns[name]?.[rowIndex] ?? (inserted === void 0 || isDefaultInsertValue(inserted) ? null : boundInsertValue(inserted));
7173
- return [name, executionSqlValueFromInput(column, value)];
7174
- }))))
7419
+ const writer = options.writer;
7420
+ const stagedKeyColumn = writer === void 0 ? void 0 : getUniqueKeyColumn(table2);
7421
+ if (writer !== void 0 && !viaUpsert && stagedKeyColumn !== void 0) {
7422
+ const assignedGenerated = table2.columns.find((column) => column.generatedValue !== void 0 && spelledColumns.includes(column.name));
7423
+ if (assignedGenerated !== void 0) {
7424
+ throw new TypeError(`Generated column cannot be assigned: ${assignedGenerated.name}`);
7425
+ }
7426
+ await this.#assertStagedInsertKeysFree(table2, stagedKeyColumn, input, writer);
7175
7427
  }
7176
- };
7177
- }
7178
- const table = await this.#findTable(statement.table);
7179
- const keyColumn = getUniqueKeyColumn(table);
7180
- if (keyColumn === void 0) {
7181
- throw new TypeError(`${statement.kind === "update" ? "UPDATE" : "DELETE"} requires a table with a unique key: ${statement.table}`);
7182
- }
7183
- const returningColumns = options.returning === void 0 ? void 0 : options.returning === "*" ? visibleTableColumns(table).map(({ name }) => name) : [...options.returning];
7184
- const updateAssignments = statement.kind !== "update" ? [] : await (async () => {
7185
- const spelled = foldAssignmentColumns(table, statement.assignments);
7186
- if (!table.columns.some((column) => column.sqlDomain !== void 0)) {
7187
- return spelled;
7188
- }
7189
- const { domains } = await this.#catalogFacts();
7190
- if (!domains.has(table.name))
7191
- return statement.assignments;
7192
- const normalized = normalizePlanDomainLiterals({
7193
- sql: `UPDATE ${table.name}`,
7194
- base: { table: table.name, alias: table.name },
7195
- joins: [],
7196
- select: spelled.map((assignment) => ({
7197
- expression: assignment.expression,
7198
- alias: assignment.column
7199
- })),
7200
- predicates: [],
7428
+ const result = await (viaUpsert ? writer?.upsertBatch(statement.table, input) ?? this.#erased.upsertBatch(statement.table, input) : writer?.insertBatch(statement.table, input) ?? this.#erased.insertBatch(statement.table, input));
7429
+ const generated = "generatedColumns" in result ? result.generatedColumns ?? {} : {};
7430
+ return {
7431
+ kind: "insert",
7432
+ table: statement.table,
7433
+ rowCount: result.rowCount,
7434
+ ...result.version === void 0 ? {} : { version: result.version },
7435
+ ...returningColumns2 === void 0 ? {} : {
7436
+ ...returningExecuteFields(table2, returningColumns2, statement.rows.map((row, rowIndex) => Object.fromEntries(returningColumns2.map((name) => {
7437
+ const column = table2.columns.find((candidate) => candidate.name === name);
7438
+ const position = spelledColumns.indexOf(name);
7439
+ const inserted = position < 0 ? void 0 : row[position];
7440
+ const value = generated[name]?.[rowIndex] ?? input.columns[name]?.[rowIndex] ?? (inserted === void 0 || isDefaultInsertValue(inserted) ? null : boundInsertValue(inserted));
7441
+ return [name, executionSqlValueFromInput(column, value)];
7442
+ }))))
7443
+ }
7444
+ };
7445
+ }
7446
+ const table = await this.#findTable(statement.table);
7447
+ const keyColumn = getUniqueKeyColumn(table);
7448
+ if (keyColumn === void 0) {
7449
+ throw new TypeError(`${statement.kind === "update" ? "UPDATE" : "DELETE"} requires a table with a unique key: ${statement.table}`);
7450
+ }
7451
+ const returningColumns = options.returning === void 0 ? void 0 : options.returning === "*" ? visibleTableColumns(table).map(({ name }) => name) : [...options.returning];
7452
+ const updateAssignments = statement.kind !== "update" ? [] : await (async () => {
7453
+ const spelled = foldAssignmentColumns(table, statement.assignments);
7454
+ if (!table.columns.some((column) => column.sqlDomain !== void 0)) {
7455
+ return spelled;
7456
+ }
7457
+ const { domains } = await this.#catalogFacts();
7458
+ if (!domains.has(table.name))
7459
+ return statement.assignments;
7460
+ const normalized = normalizePlanDomainLiterals({
7461
+ sql: `UPDATE ${table.name}`,
7462
+ base: { table: table.name, alias: table.name },
7463
+ joins: [],
7464
+ select: spelled.map((assignment) => ({
7465
+ expression: assignment.expression,
7466
+ alias: assignment.column
7467
+ })),
7468
+ predicates: [],
7469
+ groupBy: [],
7470
+ having: [],
7471
+ orderBy: []
7472
+ }, domains);
7473
+ return spelled.map((assignment, index) => ({
7474
+ ...assignment,
7475
+ expression: normalized.select[index]?.expression ?? assignment.expression
7476
+ }));
7477
+ })();
7478
+ const referenced = /* @__PURE__ */ new Set([keyColumn.name, ...returningColumns ?? []]);
7479
+ const assignmentAlias = (column) => `\0set:${column}`;
7480
+ const targetAlias = statement.alias ?? table.name;
7481
+ const from = statement.from;
7482
+ const plan = optimizePlan({
7483
+ sql: `(${statement.kind})`,
7484
+ base: { table: table.name, alias: targetAlias },
7485
+ joins: from === void 0 ? [] : [crossJoinPlan(from.base), ...from.joins],
7486
+ select: [
7487
+ ...[...referenced].map((name) => ({
7488
+ expression: {
7489
+ kind: "column",
7490
+ reference: from === void 0 ? name : `${targetAlias}.${name}`
7491
+ },
7492
+ alias: name
7493
+ })),
7494
+ ...statement.kind === "update" ? updateAssignments.map((assignment) => ({
7495
+ expression: assignment.expression,
7496
+ alias: assignmentAlias(assignment.column)
7497
+ })) : []
7498
+ ],
7499
+ predicates: from === void 0 ? statement.predicates : [...statement.predicates, ...from.predicates],
7201
7500
  groupBy: [],
7202
7501
  having: [],
7203
7502
  orderBy: []
7204
- }, domains);
7205
- return spelled.map((assignment, index) => ({
7206
- ...assignment,
7207
- expression: normalized.select[index]?.expression ?? assignment.expression
7208
- }));
7209
- })();
7210
- const referenced = /* @__PURE__ */ new Set([keyColumn.name, ...returningColumns ?? []]);
7211
- const assignmentAlias = (column) => `\0set:${column}`;
7212
- const targetAlias = statement.alias ?? table.name;
7213
- const from = statement.from;
7214
- const plan = optimizePlan({
7215
- sql: `(${statement.kind})`,
7216
- base: { table: table.name, alias: targetAlias },
7217
- joins: from === void 0 ? [] : [crossJoinPlan(from.base), ...from.joins],
7218
- select: [
7219
- ...[...referenced].map((name) => ({
7220
- expression: {
7221
- kind: "column",
7222
- reference: from === void 0 ? name : `${targetAlias}.${name}`
7223
- },
7224
- alias: name
7225
- })),
7226
- ...statement.kind === "update" ? updateAssignments.map((assignment) => ({
7227
- expression: assignment.expression,
7228
- alias: assignmentAlias(assignment.column)
7229
- })) : []
7230
- ],
7231
- predicates: from === void 0 ? statement.predicates : [...statement.predicates, ...from.predicates],
7232
- groupBy: [],
7233
- having: [],
7234
- orderBy: []
7235
- });
7236
- if (statement.kind === "delete" && returningColumns === void 0 && from === void 0) {
7237
- const internalWriter = options.writer;
7238
- if (internalWriter.queryFirstColumn !== void 0) {
7239
- const selectedKeys = await internalWriter.queryFirstColumn(plan);
7240
- if (selectedKeys !== void 0) {
7241
- const keys2 = selectedKeys;
7242
- if (keyColumn.type === "string" && keyColumn.sqlDomain === void 0) {
7243
- for (let index = 0; index < keys2.length; index += 1) {
7244
- keys2[index] = storedSqlValueFromExecution(keyColumn, keys2[index] ?? null);
7503
+ });
7504
+ if (statement.kind === "delete" && returningColumns === void 0 && from === void 0) {
7505
+ const internalWriter = options.writer;
7506
+ if (internalWriter.queryFirstColumn !== void 0) {
7507
+ const selectedKeys = await internalWriter.queryFirstColumn(plan);
7508
+ if (selectedKeys !== void 0) {
7509
+ const keys2 = selectedKeys;
7510
+ if (keyColumn.type === "string" && keyColumn.sqlDomain === void 0) {
7511
+ for (let index = 0; index < keys2.length; index += 1) {
7512
+ keys2[index] = storedSqlValueFromExecution(keyColumn, keys2[index] ?? null);
7513
+ }
7245
7514
  }
7515
+ if (keys2.some((key) => key === null)) {
7516
+ throw new TypeError(`Unique key values must not be null: ${keyColumn.name}`);
7517
+ }
7518
+ if (keys2.length === 0) {
7519
+ return { kind: "delete", table: table.name, rowCount: 0 };
7520
+ }
7521
+ const deleted = await internalWriter.deleteBatch(table.name, { keys: keys2 });
7522
+ return {
7523
+ kind: "delete",
7524
+ table: table.name,
7525
+ rowCount: deleted.rowCount
7526
+ };
7246
7527
  }
7247
- if (keys2.some((key) => key === null)) {
7248
- throw new TypeError(`Unique key values must not be null: ${keyColumn.name}`);
7249
- }
7250
- if (keys2.length === 0) {
7251
- return { kind: "delete", table: table.name, rowCount: 0 };
7252
- }
7253
- const deleted = await internalWriter.deleteBatch(table.name, { keys: keys2 });
7528
+ }
7529
+ }
7530
+ let rows;
7531
+ if (options.writer !== void 0) {
7532
+ rows = (await options.writer.queryPlan(plan)).rows;
7533
+ } else {
7534
+ rows = (await this.#queryCompiled(plan)).rows;
7535
+ }
7536
+ if (from !== void 0) {
7537
+ const seen = /* @__PURE__ */ new Set();
7538
+ rows = rows.filter((row) => {
7539
+ const key = JSON.stringify(row[keyColumn.name] ?? null);
7540
+ if (seen.has(key))
7541
+ return false;
7542
+ seen.add(key);
7543
+ return true;
7544
+ });
7545
+ }
7546
+ const keys = keyColumn.type === "string" && keyColumn.sqlDomain === void 0 ? rows.map((row) => storedSqlValueFromExecution(keyColumn, row[keyColumn.name] ?? null)) : rows.map((row) => row[keyColumn.name] ?? null);
7547
+ if (keys.some((key) => key === null)) {
7548
+ throw new TypeError(`Unique key values must not be null: ${keyColumn.name}`);
7549
+ }
7550
+ if (statement.kind === "delete") {
7551
+ const returnedRows2 = returningColumns === void 0 ? void 0 : rows.map((row) => Object.fromEntries(returningColumns.map((name) => [name, row[name] ?? null])));
7552
+ if (keys.length === 0) {
7254
7553
  return {
7255
7554
  kind: "delete",
7256
7555
  table: table.name,
7257
- rowCount: deleted.rowCount
7556
+ rowCount: 0,
7557
+ ...returnedRows2 === void 0 || returningColumns === void 0 ? {} : returningExecuteFields(table, returningColumns, returnedRows2)
7258
7558
  };
7259
7559
  }
7560
+ const deleted = options.writer === void 0 ? await this.#erased.deleteBatch(table.name, { keys }) : await options.writer.deleteBatch(table.name, { keys });
7561
+ return {
7562
+ kind: "delete",
7563
+ table: table.name,
7564
+ rowCount: deleted.deletedRowCount ?? deleted.rowCount ?? 0,
7565
+ ...deleted.version === void 0 || deleted.version === null ? {} : { version: deleted.version },
7566
+ ...returnedRows2 === void 0 || returningColumns === void 0 ? {} : returningExecuteFields(table, returningColumns, returnedRows2)
7567
+ };
7260
7568
  }
7261
- }
7262
- let rows;
7263
- if (options.writer !== void 0) {
7264
- rows = (await options.writer.queryPlan(plan)).rows;
7265
- } else {
7266
- rows = (await this.#queryCompiled(plan)).rows;
7267
- }
7268
- if (from !== void 0) {
7269
- const seen = /* @__PURE__ */ new Set();
7270
- rows = rows.filter((row) => {
7271
- const key = JSON.stringify(row[keyColumn.name] ?? null);
7272
- if (seen.has(key))
7273
- return false;
7274
- seen.add(key);
7275
- return true;
7276
- });
7277
- }
7278
- const keys = keyColumn.type === "string" && keyColumn.sqlDomain === void 0 ? rows.map((row) => storedSqlValueFromExecution(keyColumn, row[keyColumn.name] ?? null)) : rows.map((row) => row[keyColumn.name] ?? null);
7279
- if (keys.some((key) => key === null)) {
7280
- throw new TypeError(`Unique key values must not be null: ${keyColumn.name}`);
7281
- }
7282
- if (statement.kind === "delete") {
7283
- const returnedRows2 = returningColumns === void 0 ? void 0 : rows.map((row) => Object.fromEntries(returningColumns.map((name) => [name, row[name] ?? null])));
7284
7569
  if (keys.length === 0) {
7285
7570
  return {
7286
- kind: "delete",
7571
+ kind: "update",
7287
7572
  table: table.name,
7288
7573
  rowCount: 0,
7289
- ...returnedRows2 === void 0 || returningColumns === void 0 ? {} : returningExecuteFields(table, returningColumns, returnedRows2)
7574
+ ...returningColumns === void 0 ? {} : returningExecuteFields(table, returningColumns, [])
7290
7575
  };
7291
7576
  }
7292
- const deleted = options.writer === void 0 ? await this.#erased.deleteBatch(table.name, { keys }) : await options.writer.deleteBatch(table.name, { keys });
7293
- return {
7294
- kind: "delete",
7295
- table: table.name,
7296
- rowCount: deleted.deletedRowCount ?? deleted.rowCount ?? 0,
7297
- ...deleted.version === void 0 || deleted.version === null ? {} : { version: deleted.version },
7298
- ...returnedRows2 === void 0 || returningColumns === void 0 ? {} : returningExecuteFields(table, returningColumns, returnedRows2)
7299
- };
7300
- }
7301
- if (keys.length === 0) {
7577
+ const changes = {};
7578
+ const returnedChanges = returningColumns === void 0 ? void 0 : {};
7579
+ for (const assignment of updateAssignments) {
7580
+ const column = table.columns.find((candidate) => candidate.name === assignment.column);
7581
+ if (column === void 0)
7582
+ throw new TypeError(`Unknown column: ${assignment.column}`);
7583
+ const executionValues = returnedChanges === void 0 ? void 0 : [];
7584
+ changes[assignment.column] = rows.map((row) => {
7585
+ const value = row[assignmentAlias(assignment.column)] ?? null;
7586
+ if (typeof value === "number" && !Number.isFinite(value)) {
7587
+ throw new TypeError(`UPDATE assignment produced a non-finite number: ${assignment.column}`);
7588
+ }
7589
+ const stored = storedSqlValueFromExecution(column, value);
7590
+ executionValues?.push(column.type === "string" && column.sqlDomain === void 0 ? value : stored);
7591
+ return stored;
7592
+ });
7593
+ if (executionValues !== void 0 && returnedChanges !== void 0) {
7594
+ returnedChanges[assignment.column] = executionValues;
7595
+ }
7596
+ }
7597
+ const updated = options.writer === void 0 ? await this.#erased.updateBatch(table.name, { keys, changes }) : await options.writer.updateBatch(table.name, { keys, changes });
7598
+ const returnedRows = returningColumns === void 0 ? void 0 : rows.map((row, index) => Object.fromEntries(returningColumns.map((name) => [
7599
+ name,
7600
+ returnedChanges !== void 0 && name in returnedChanges ? returnedChanges[name]?.[index] ?? null : updated.generatedColumns?.[name] !== void 0 ? updated.generatedColumns[name][index] ?? null : row[name] ?? null
7601
+ ])));
7302
7602
  return {
7303
7603
  kind: "update",
7304
7604
  table: table.name,
7305
- rowCount: 0,
7306
- ...returningColumns === void 0 ? {} : returningExecuteFields(table, returningColumns, [])
7605
+ rowCount: updated.updatedRowCount ?? updated.rowCount ?? 0,
7606
+ ...updated.version === void 0 || updated.version === null ? {} : { version: updated.version },
7607
+ ...returnedRows === void 0 || returningColumns === void 0 ? {} : returningExecuteFields(table, returningColumns, returnedRows)
7307
7608
  };
7308
- }
7309
- const changes = {};
7310
- const returnedChanges = returningColumns === void 0 ? void 0 : {};
7311
- for (const assignment of updateAssignments) {
7312
- const column = table.columns.find((candidate) => candidate.name === assignment.column);
7313
- if (column === void 0)
7314
- throw new TypeError(`Unknown column: ${assignment.column}`);
7315
- const executionValues = returnedChanges === void 0 ? void 0 : [];
7316
- changes[assignment.column] = rows.map((row) => {
7317
- const value = row[assignmentAlias(assignment.column)] ?? null;
7318
- if (typeof value === "number" && !Number.isFinite(value)) {
7319
- throw new TypeError(`UPDATE assignment produced a non-finite number: ${assignment.column}`);
7320
- }
7321
- const stored = storedSqlValueFromExecution(column, value);
7322
- executionValues?.push(column.type === "string" && column.sqlDomain === void 0 ? value : stored);
7323
- return stored;
7324
- });
7325
- if (executionValues !== void 0 && returnedChanges !== void 0) {
7326
- returnedChanges[assignment.column] = executionValues;
7327
- }
7328
- }
7329
- const updated = options.writer === void 0 ? await this.#erased.updateBatch(table.name, { keys, changes }) : await options.writer.updateBatch(table.name, { keys, changes });
7330
- const returnedRows = returningColumns === void 0 ? void 0 : rows.map((row, index) => Object.fromEntries(returningColumns.map((name) => [
7331
- name,
7332
- returnedChanges !== void 0 && name in returnedChanges ? returnedChanges[name]?.[index] ?? null : updated.generatedColumns?.[name] !== void 0 ? updated.generatedColumns[name][index] ?? null : row[name] ?? null
7333
- ])));
7334
- return {
7335
- kind: "update",
7336
- table: table.name,
7337
- rowCount: updated.updatedRowCount ?? updated.rowCount ?? 0,
7338
- ...updated.version === void 0 || updated.version === null ? {} : { version: updated.version },
7339
- ...returnedRows === void 0 || returningColumns === void 0 ? {} : returningExecuteFields(table, returningColumns, returnedRows)
7340
- };
7609
+ });
7341
7610
  }
7342
7611
  #canStreamPlanShape(plan, options) {
7343
7612
  return (options.executionMemoryBudgetBytes === void 0 || options.spillToStorage !== false) && plan.limitWithTies !== true && plan.base.table !== DUAL_TABLE && plan.base.derived === void 0 && plan.base.union === void 0 && plan.base.windowed === void 0 && plan.base.recursive === void 0 && plan.joins.every((join) => join.derived === void 0 && join.recursive === void 0) && !plan.joins.some((join) => join.table === plan.base.table) && !blockHasSubqueries(plan);
@@ -7809,6 +8078,24 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
7809
8078
  this.#cachePut(`dkl ${blockId}`, locators, 64 + locators.size * 48);
7810
8079
  return locators;
7811
8080
  }
8081
+ async #pointReadVectors(ids, snapshot) {
8082
+ const vectors = ids.map((id) => this.#cacheGet(`dbv ${id}`));
8083
+ const missing = [];
8084
+ const positions = [];
8085
+ for (let index = 0; index < ids.length; index += 1) {
8086
+ if (vectors[index] !== void 0)
8087
+ continue;
8088
+ missing.push(ids[index] ?? "");
8089
+ positions.push(index);
8090
+ }
8091
+ if (missing.length > 0) {
8092
+ const decoded = await this.#decodedBlocksThroughCache(missing, snapshot);
8093
+ for (const [index, block] of decoded.entries()) {
8094
+ vectors[positions[index] ?? 0] = this.#blockColumnVector(missing[index] ?? "", block);
8095
+ }
8096
+ }
8097
+ return vectors;
8098
+ }
7812
8099
  #blockColumnVector(blockId, decoded) {
7813
8100
  const cached = this.#cacheGet(`dbv ${blockId}`);
7814
8101
  if (cached !== void 0)
@@ -7821,7 +8108,10 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
7821
8108
  codes?.fill(NULL_STRING_VECTOR_CODE);
7822
8109
  const builder = new StringDictionaryBuilder();
7823
8110
  appendPhysicalColumnToVector(column, 0, validity, values, codes, builder);
7824
- const vector = codes !== void 0 ? { kind: "string", length: rows, validity, codes, dictionary: builder.dictionary } : { kind: column.type, length: rows, validity, values };
8111
+ const vector = {
8112
+ nullCount: decoded.description.nullCount,
8113
+ ...codes !== void 0 ? { kind: "string", length: rows, validity, codes, dictionary: builder.dictionary } : { kind: column.type, length: rows, validity, values }
8114
+ };
7825
8115
  this.#cachePut(`dbv ${blockId}`, vector, blockVectorRetainedBytes(vector));
7826
8116
  return vector;
7827
8117
  }
@@ -8359,87 +8649,119 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
8359
8649
  }
8360
8650
  }
8361
8651
  async listVisibleSegmentPage(tableName, options = {}) {
8362
- const normalizedTableName = validateName(tableName, "Table");
8363
- const limit = positiveWholeNumber(options.limit ?? MAX_VISIBLE_SEGMENT_PAGE_ITEMS, "Visible segment page limit");
8364
- if (limit > MAX_VISIBLE_SEGMENT_PAGE_ITEMS) {
8365
- throw new RangeError(`Visible segment page limit cannot exceed ${String(MAX_VISIBLE_SEGMENT_PAGE_ITEMS)}`);
8366
- }
8367
- const rawCursor = options.cursor;
8368
- if (rawCursor !== void 0 && (typeof rawCursor !== "object" || rawCursor === null)) {
8369
- throw new TypeError("A visible segment cursor must be an object");
8370
- }
8371
- const cursor = rawCursor;
8372
- if (cursor !== void 0 && options.version !== void 0) {
8373
- throw new TypeError("A visible segment cursor cannot be combined with a version");
8374
- }
8375
- let tableId;
8376
- let version;
8377
- let afterId;
8378
- if (cursor === void 0) {
8379
- tableId = (await this.#findTable(normalizedTableName)).id;
8380
- version = visibleSegmentManifestVersion(options.version, "Visible segment version");
8381
- afterId = null;
8382
- } else {
8383
- if (cursor.tableName !== normalizedTableName) {
8384
- throw new TypeError("A visible segment cursor belongs to a different table name");
8385
- }
8386
- tableId = validateStorageId(cursor.tableId, "Visible segment cursor table ID");
8387
- version = visibleSegmentManifestVersion(cursor.version, "Visible segment cursor version");
8388
- if (version === void 0)
8389
- throw new TypeError("A visible segment cursor needs a version");
8390
- afterId = validateStorageId(cursor.afterId, "Visible segment cursor ID");
8391
- }
8392
- return this.#withLeasedSnapshot(version, async (snapshot) => {
8393
- await this.#assertVisibleSegmentCursorTable(normalizedTableName, tableId);
8394
- const page = await this.store.listTableSegmentPage(tableId, afterId, limit);
8395
- await this.#assertVisibleSegmentCursorTable(normalizedTableName, tableId);
8396
- const transactions = new Map((await this.#transactionRecordsForSegments(page.records)).map((record) => [
8397
- record.id,
8398
- record
8399
- ]));
8400
- const versionEligible = page.records.filter((segment) => {
8401
- const committedVersion = transactions.get(segment.transactionId)?.committedVersion;
8402
- return snapshot.version !== null && committedVersion !== null && committedVersion !== void 0 && committedVersion <= snapshot.version;
8403
- });
8404
- const visible = await filterSnapshotSegments(snapshot, versionEligible);
8405
- return {
8406
- records: visible.map((segment) => ({
8407
- id: segment.id,
8408
- rowCount: segment.rowCount,
8409
- columnBlockIds: structuredClone(segment.columnBlockIds),
8410
- logicalOrder: segment.logicalOrder,
8411
- committedVersion: transactions.get(segment.transactionId)?.committedVersion ?? -1,
8412
- commitOrdinal: segment.commitOrdinal
8413
- })),
8414
- version: snapshot.version,
8415
- nextCursor: page.nextCursor === null ? null : {
8416
- tableName: normalizedTableName,
8417
- tableId,
8652
+ return this.#foreground(async () => {
8653
+ const normalizedTableName = validateName(tableName, "Table");
8654
+ const limit = positiveWholeNumber(options.limit ?? MAX_VISIBLE_SEGMENT_PAGE_ITEMS, "Visible segment page limit");
8655
+ if (limit > MAX_VISIBLE_SEGMENT_PAGE_ITEMS) {
8656
+ throw new RangeError(`Visible segment page limit cannot exceed ${String(MAX_VISIBLE_SEGMENT_PAGE_ITEMS)}`);
8657
+ }
8658
+ const rawCursor = options.cursor;
8659
+ if (rawCursor !== void 0 && (typeof rawCursor !== "object" || rawCursor === null)) {
8660
+ throw new TypeError("A visible segment cursor must be an object");
8661
+ }
8662
+ const cursor = rawCursor;
8663
+ if (cursor !== void 0 && options.version !== void 0) {
8664
+ throw new TypeError("A visible segment cursor cannot be combined with a version");
8665
+ }
8666
+ let tableId;
8667
+ let version;
8668
+ let afterId;
8669
+ if (cursor === void 0) {
8670
+ tableId = (await this.#findTable(normalizedTableName)).id;
8671
+ version = visibleSegmentManifestVersion(options.version, "Visible segment version");
8672
+ afterId = null;
8673
+ } else {
8674
+ if (cursor.tableName !== normalizedTableName) {
8675
+ throw new TypeError("A visible segment cursor belongs to a different table name");
8676
+ }
8677
+ tableId = validateStorageId(cursor.tableId, "Visible segment cursor table ID");
8678
+ version = visibleSegmentManifestVersion(cursor.version, "Visible segment cursor version");
8679
+ if (version === void 0)
8680
+ throw new TypeError("A visible segment cursor needs a version");
8681
+ afterId = validateStorageId(cursor.afterId, "Visible segment cursor ID");
8682
+ }
8683
+ return this.#withLeasedSnapshot(version, async (snapshot) => {
8684
+ await this.#assertVisibleSegmentCursorTable(normalizedTableName, tableId);
8685
+ const page = await this.store.listTableSegmentPage(tableId, afterId, limit);
8686
+ await this.#assertVisibleSegmentCursorTable(normalizedTableName, tableId);
8687
+ const transactions = new Map((await this.#transactionRecordsForSegments(page.records)).map((record) => [
8688
+ record.id,
8689
+ record
8690
+ ]));
8691
+ const versionEligible = page.records.filter((segment) => {
8692
+ const committedVersion = transactions.get(segment.transactionId)?.committedVersion;
8693
+ return snapshot.version !== null && committedVersion !== null && committedVersion !== void 0 && committedVersion <= snapshot.version;
8694
+ });
8695
+ const visible = await filterSnapshotSegments(snapshot, versionEligible);
8696
+ return {
8697
+ records: visible.map((segment) => ({
8698
+ id: segment.id,
8699
+ rowCount: segment.rowCount,
8700
+ columnBlockIds: structuredClone(segment.columnBlockIds),
8701
+ logicalOrder: segment.logicalOrder,
8702
+ committedVersion: transactions.get(segment.transactionId)?.committedVersion ?? -1,
8703
+ commitOrdinal: segment.commitOrdinal
8704
+ })),
8418
8705
  version: snapshot.version,
8419
- afterId: page.nextCursor
8420
- }
8421
- };
8706
+ nextCursor: page.nextCursor === null ? null : {
8707
+ tableName: normalizedTableName,
8708
+ tableId,
8709
+ version: snapshot.version,
8710
+ afterId: page.nextCursor
8711
+ }
8712
+ };
8713
+ });
8422
8714
  });
8423
8715
  }
8424
8716
  async exportSnapshot(options = {}) {
8425
- const chunks = [];
8426
- let byteLength = 0;
8427
- for await (const chunk of this.exportSnapshotStream(options)) {
8428
- byteLength = safeWholeNumberSum([byteLength, chunk.byteLength], "Snapshot byte-array length");
8429
- if (byteLength > MAX_SNAPSHOT_BYTE_ARRAY_LENGTH) {
8430
- throw new RangeError("Snapshot exceeds the byte-array API's 4 GiB limit");
8431
- }
8432
- chunks.push(chunk);
8433
- }
8434
- const bytes = new Uint8Array(byteLength);
8435
- let offset = 0;
8436
- for (const chunk of chunks) {
8437
- bytes.set(chunk, offset);
8438
- offset += chunk.byteLength;
8439
- }
8440
- return bytes;
8717
+ return this.#foreground(async () => {
8718
+ const chunks = [];
8719
+ let byteLength = 0;
8720
+ for await (const chunk of this.exportSnapshotStream(options)) {
8721
+ byteLength = safeWholeNumberSum([byteLength, chunk.byteLength], "Snapshot byte-array length");
8722
+ if (byteLength > MAX_SNAPSHOT_BYTE_ARRAY_LENGTH) {
8723
+ throw new RangeError("Snapshot exceeds the byte-array API's 4 GiB limit");
8724
+ }
8725
+ chunks.push(chunk);
8726
+ }
8727
+ const bytes = new Uint8Array(byteLength);
8728
+ let offset = 0;
8729
+ for (const chunk of chunks) {
8730
+ bytes.set(chunk, offset);
8731
+ offset += chunk.byteLength;
8732
+ }
8733
+ return bytes;
8734
+ });
8735
+ }
8736
+ exportSnapshotStream(options = {}) {
8737
+ if (this.#closed)
8738
+ throw new Error("Database is closed");
8739
+ if (this.#exports.size >= MAX_DATABASE_ACTIVE_SCOPES)
8740
+ throw new RangeError("Too many open snapshot exports");
8741
+ const iterator = this.#exportSnapshotChunks(options);
8742
+ this.#exports.add(iterator);
8743
+ const next = iterator.next.bind(iterator);
8744
+ const end = iterator.return.bind(iterator);
8745
+ const fail = iterator.throw.bind(iterator);
8746
+ iterator.next = (...args) => this.#foreground(() => next(...args)).then((result) => {
8747
+ if (result.done)
8748
+ this.#exports.delete(iterator);
8749
+ return result;
8750
+ }, (error) => {
8751
+ this.#exports.delete(iterator);
8752
+ throw error;
8753
+ });
8754
+ iterator.return = (value) => end(value).finally(() => {
8755
+ this.#exports.delete(iterator);
8756
+ });
8757
+ iterator.throw = (error) => fail(error).finally(() => {
8758
+ this.#exports.delete(iterator);
8759
+ });
8760
+ return iterator;
8441
8761
  }
8442
- async *exportSnapshotStream(options = {}) {
8762
+ async *#exportSnapshotChunks(options) {
8763
+ if (this.#closed)
8764
+ throw new Error("Database is closed");
8443
8765
  const store = streamingSnapshotExportStore(this.store);
8444
8766
  throwIfSnapshotAborted(options.signal);
8445
8767
  options.onProgress?.({ phase: "reading", transferredBytes: 0, totalBytes: 0 });
@@ -8531,160 +8853,178 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
8531
8853
  }
8532
8854
  }
8533
8855
  async importSnapshot(bytes, options = {}) {
8534
- await this.importSnapshotStream(singleSnapshotChunk(bytes), options);
8856
+ return this.#foreground(async () => {
8857
+ await this.importSnapshotStream(singleSnapshotChunk(bytes), options);
8858
+ });
8535
8859
  }
8536
8860
  async importSnapshotStream(source, options = {}) {
8537
- const store = streamingSnapshotImportStore(this.store);
8538
- const ownerId = `snapshot-import/${crypto.randomUUID()}`;
8539
- let identity;
8540
- let acquired = false;
8541
- let totalBytes = 0;
8542
- let writtenBytes = 0;
8543
- let batchBytes = 0;
8544
- let metadataBatchBytes = 0;
8545
- let batchBlockCount = 0;
8546
- const batch = [];
8547
- const flush = async () => {
8548
- if (batch.length === 0 || identity === void 0)
8549
- return;
8550
- throwIfSnapshotAborted(options.signal);
8551
- const times = snapshotSessionTimes(this.#now());
8552
- const session = await store.appendSnapshotImportFrames({
8553
- identity,
8554
- ownerId,
8555
- expiresAtCutoff: times.createdAt,
8556
- expiresAt: times.expiresAt,
8557
- frames: batch.splice(0)
8558
- });
8559
- batchBytes = 0;
8560
- metadataBatchBytes = 0;
8561
- batchBlockCount = 0;
8562
- writtenBytes = session.stagedBytes;
8563
- options.onProgress?.({ phase: "blocks", writtenBytes, totalBytes });
8564
- };
8565
- try {
8566
- throwIfSnapshotAborted(options.signal);
8567
- for await (const entry of decodeSnapshotFrameStream(abortableSnapshotSource(source, options.signal))) {
8861
+ return this.#foreground(async () => {
8862
+ const store = streamingSnapshotImportStore(this.store);
8863
+ const ownerId = `snapshot-import/${crypto.randomUUID()}`;
8864
+ let identity;
8865
+ let acquired = false;
8866
+ let totalBytes = 0;
8867
+ let writtenBytes = 0;
8868
+ let batchBytes = 0;
8869
+ let metadataBatchBytes = 0;
8870
+ let batchBlockCount = 0;
8871
+ const batch = [];
8872
+ const flush = async () => {
8873
+ if (batch.length === 0 || identity === void 0)
8874
+ return;
8875
+ throwIfSnapshotAborted(options.signal);
8876
+ const times = snapshotSessionTimes(this.#now());
8877
+ const session = await store.appendSnapshotImportFrames({
8878
+ identity,
8879
+ ownerId,
8880
+ expiresAtCutoff: times.createdAt,
8881
+ expiresAt: times.expiresAt,
8882
+ frames: batch.splice(0)
8883
+ });
8884
+ batchBytes = 0;
8885
+ metadataBatchBytes = 0;
8886
+ batchBlockCount = 0;
8887
+ writtenBytes = session.stagedBytes;
8888
+ options.onProgress?.({ phase: "blocks", writtenBytes, totalBytes });
8889
+ };
8890
+ try {
8568
8891
  throwIfSnapshotAborted(options.signal);
8569
- if (entry.type === "header") {
8570
- if (identity !== void 0)
8571
- throw new Error("Snapshot stream repeats its header");
8572
- identity = snapshotFrameStreamHeaderIdentity(entry.header);
8573
- totalBytes = Object.values(entry.header.kinds).reduce((total, kind) => safeWholeNumberSum([total, kind.storedBytes], "Snapshot streamed payload length"), 0);
8574
- const times = snapshotSessionTimes(this.#now());
8575
- await store.beginSnapshotFrameImport({
8892
+ for await (const entry of decodeSnapshotFrameStream(abortableSnapshotSource(source, options.signal))) {
8893
+ throwIfSnapshotAborted(options.signal);
8894
+ if (entry.type === "header") {
8895
+ if (identity !== void 0)
8896
+ throw new Error("Snapshot stream repeats its header");
8897
+ identity = snapshotFrameStreamHeaderIdentity(entry.header);
8898
+ totalBytes = Object.values(entry.header.kinds).reduce((total, kind) => safeWholeNumberSum([total, kind.storedBytes], "Snapshot streamed payload length"), 0);
8899
+ const times = snapshotSessionTimes(this.#now());
8900
+ await store.beginSnapshotFrameImport({
8901
+ identity,
8902
+ ownerId,
8903
+ ...times,
8904
+ header: entry.header
8905
+ });
8906
+ acquired = true;
8907
+ options.onProgress?.({ phase: "blocks", writtenBytes: 0, totalBytes });
8908
+ continue;
8909
+ }
8910
+ if (entry.type === "frame") {
8911
+ if (identity === void 0)
8912
+ throw new Error("Snapshot frame precedes its header");
8913
+ if (batch.length > 0 && (batch.length >= MAX_SNAPSHOT_FRAME_BATCH_ITEMS || batchBytes + entry.frame.payload.byteLength > MAX_SNAPSHOT_FRAME_BATCH_BYTES || entry.frame.kind === "block" && batchBlockCount > 0 || entry.frame.kind !== "block" && metadataBatchBytes + entry.frame.payload.byteLength > MAX_SNAPSHOT_METADATA_BATCH_BYTES)) {
8914
+ await flush();
8915
+ }
8916
+ batch.push(entry.frame);
8917
+ batchBytes = safeWholeNumberSum([batchBytes, entry.frame.payload.byteLength], "Snapshot import batch bytes");
8918
+ if (entry.frame.kind !== "block") {
8919
+ metadataBatchBytes = safeWholeNumberSum([metadataBatchBytes, entry.frame.payload.byteLength], "Snapshot import metadata batch bytes");
8920
+ } else {
8921
+ batchBlockCount += 1;
8922
+ }
8923
+ continue;
8924
+ }
8925
+ if (identity === void 0)
8926
+ throw new Error("Snapshot footer precedes its header");
8927
+ await flush();
8928
+ throwIfSnapshotAborted(options.signal);
8929
+ const cutoff = dateIsoString(this.#now());
8930
+ options.onProgress?.({ phase: "catalog", writtenBytes, totalBytes });
8931
+ await store.finishSnapshotFrameImport({
8576
8932
  identity,
8577
8933
  ownerId,
8578
- ...times,
8579
- header: entry.header
8934
+ expiresAtCutoff: cutoff,
8935
+ footer: entry.footer
8580
8936
  });
8581
- acquired = true;
8582
- options.onProgress?.({ phase: "blocks", writtenBytes: 0, totalBytes });
8583
- continue;
8584
- }
8585
- if (entry.type === "frame") {
8586
- if (identity === void 0)
8587
- throw new Error("Snapshot frame precedes its header");
8588
- if (batch.length > 0 && (batch.length >= MAX_SNAPSHOT_FRAME_BATCH_ITEMS || batchBytes + entry.frame.payload.byteLength > MAX_SNAPSHOT_FRAME_BATCH_BYTES || entry.frame.kind === "block" && batchBlockCount > 0 || entry.frame.kind !== "block" && metadataBatchBytes + entry.frame.payload.byteLength > MAX_SNAPSHOT_METADATA_BATCH_BYTES)) {
8589
- await flush();
8590
- }
8591
- batch.push(entry.frame);
8592
- batchBytes = safeWholeNumberSum([batchBytes, entry.frame.payload.byteLength], "Snapshot import batch bytes");
8593
- if (entry.frame.kind !== "block") {
8594
- metadataBatchBytes = safeWholeNumberSum([metadataBatchBytes, entry.frame.payload.byteLength], "Snapshot import metadata batch bytes");
8595
- } else {
8596
- batchBlockCount += 1;
8597
- }
8598
- continue;
8937
+ acquired = false;
8599
8938
  }
8600
8939
  if (identity === void 0)
8601
- throw new Error("Snapshot footer precedes its header");
8602
- await flush();
8603
- throwIfSnapshotAborted(options.signal);
8604
- const cutoff = dateIsoString(this.#now());
8605
- options.onProgress?.({ phase: "catalog", writtenBytes, totalBytes });
8606
- await store.finishSnapshotFrameImport({
8607
- identity,
8608
- ownerId,
8609
- expiresAtCutoff: cutoff,
8610
- footer: entry.footer
8611
- });
8612
- acquired = false;
8613
- }
8614
- if (identity === void 0)
8615
- throw new Error("Snapshot stream is empty");
8616
- if (acquired)
8617
- throw new Error("Snapshot stream ended without its footer");
8618
- } catch (error) {
8619
- if (options.signal?.aborted === true && acquired && identity !== void 0) {
8620
- await store.cancelSnapshotFrameImport({ identity, ownerId });
8940
+ throw new Error("Snapshot stream is empty");
8941
+ if (acquired)
8942
+ throw new Error("Snapshot stream ended without its footer");
8943
+ } catch (error) {
8944
+ if (options.signal?.aborted === true && acquired && identity !== void 0) {
8945
+ await store.cancelSnapshotFrameImport({ identity, ownerId });
8946
+ }
8947
+ throw error;
8621
8948
  }
8622
- throw error;
8623
- }
8624
- this.#planCache.clear();
8625
- this.#catalogStateCache.clear();
8626
- this.#catalogStateEpoch = void 0;
8627
- options.onProgress?.({ phase: "done", writtenBytes, totalBytes });
8949
+ this.#planCache.clear();
8950
+ this.#catalogStateCache.clear();
8951
+ this.#catalogStateEpoch = void 0;
8952
+ options.onProgress?.({ phase: "done", writtenBytes, totalBytes });
8953
+ });
8628
8954
  }
8629
8955
  async checkIntegrity(options = {}) {
8630
- const check = this.store.checkIntegrity?.bind(this.store);
8631
- if (check === void 0)
8632
- throw new Error("This database's block store cannot check integrity");
8633
- return check(options);
8956
+ return this.#foreground(async () => {
8957
+ const check = this.store.checkIntegrity?.bind(this.store);
8958
+ if (check === void 0)
8959
+ throw new Error("This database's block store cannot check integrity");
8960
+ return check(options);
8961
+ });
8634
8962
  }
8635
8963
  async storageStats() {
8636
- const inspect = this.store.getStorageStats?.bind(this.store);
8637
- if (inspect === void 0)
8638
- throw new Error("This database's block store cannot report storage stats");
8639
- return inspect();
8964
+ return this.#foreground(async () => {
8965
+ const inspect = this.store.getStorageStats?.bind(this.store);
8966
+ if (inspect === void 0)
8967
+ throw new Error("This database's block store cannot report storage stats");
8968
+ return inspect();
8969
+ });
8640
8970
  }
8641
8971
  async inspectInterruptedImport() {
8642
- const inspect = this.store.inspectInterruptedImport?.bind(this.store);
8643
- if (inspect === void 0)
8644
- return null;
8645
- return inspect();
8972
+ return this.#foreground(async () => {
8973
+ const inspect = this.store.inspectInterruptedImport?.bind(this.store);
8974
+ if (inspect === void 0)
8975
+ return null;
8976
+ return inspect();
8977
+ });
8646
8978
  }
8647
8979
  async abortInterruptedImport(identity) {
8648
- const abort = this.store.abortInterruptedImport?.bind(this.store);
8649
- if (abort === void 0) {
8650
- throw new Error("This database's block store cannot abort interrupted imports");
8651
- }
8652
- return abort(identity);
8980
+ return this.#foreground(async () => {
8981
+ const abort = this.store.abortInterruptedImport?.bind(this.store);
8982
+ if (abort === void 0) {
8983
+ throw new Error("This database's block store cannot abort interrupted imports");
8984
+ }
8985
+ return abort(identity);
8986
+ });
8653
8987
  }
8654
8988
  async compactTable(tableName, options = {}) {
8655
- const maxBlocks = boundedMaintenanceBatchItems(options.maxBlocksPerStep ?? 16, "Compaction blocks per step");
8656
- let progress = await this.compactTableStep(tableName, { ...options, maxBlocks });
8657
- while (progress.result === null) {
8658
- if (progress.jobId === null)
8659
- throw new Error("Compaction progress lost its job ID");
8660
- progress = await this.resumeCompactionJob(progress.jobId, { maxBlocks });
8661
- }
8662
- return progress.result;
8989
+ return this.#foreground(async () => {
8990
+ const maxBlocks = boundedMaintenanceBatchItems(options.maxBlocksPerStep ?? 16, "Compaction blocks per step");
8991
+ let progress = await this.compactTableStep(tableName, { ...options, maxBlocks });
8992
+ while (progress.result === null) {
8993
+ if (progress.jobId === null)
8994
+ throw new Error("Compaction progress lost its job ID");
8995
+ progress = await this.resumeCompactionJob(progress.jobId, { maxBlocks });
8996
+ }
8997
+ return progress.result;
8998
+ });
8663
8999
  }
8664
9000
  async compactTableStep(tableName, options = {}) {
8665
- const maxBlocks = boundedMaintenanceBatchItems(options.maxBlocks ?? options.maxBlocksPerStep ?? 1, "Compaction step block limit");
8666
- const table = await this.#findTable(tableName);
8667
- return this.#serializedCompactionStep(table.id, async () => {
8668
- const active = await this.#findActiveCompactionJob(table.id);
8669
- let job = active;
8670
- if (job === void 0) {
8671
- const planned = await this.#planCompaction(table, options);
8672
- if ("compacted" in planned)
8673
- return compactionSkippedProgress(planned);
8674
- job = planned;
8675
- }
8676
- return this.#runCompactionJob(table, job, maxBlocks);
9001
+ return this.#foreground(async () => {
9002
+ const maxBlocks = boundedMaintenanceBatchItems(options.maxBlocks ?? options.maxBlocksPerStep ?? 1, "Compaction step block limit");
9003
+ const table = await this.#findTable(tableName);
9004
+ return this.#serializedCompactionStep(table.id, async () => {
9005
+ const active = await this.#findActiveCompactionJob(table.id);
9006
+ let job = active;
9007
+ if (job === void 0) {
9008
+ const planned = await this.#planCompaction(table, options);
9009
+ if ("compacted" in planned)
9010
+ return compactionSkippedProgress(planned);
9011
+ job = planned;
9012
+ }
9013
+ return this.#runCompactionJob(table, job, maxBlocks);
9014
+ });
8677
9015
  });
8678
9016
  }
8679
9017
  async resumeCompactionJob(jobId, options = {}) {
8680
- const maxBlocks = boundedMaintenanceBatchItems(options.maxBlocks ?? 1, "Compaction step block limit");
8681
- const job = await this.store.getCompactionJob(jobId);
8682
- if (job === void 0)
8683
- throw new Error(`Compaction job not found: ${jobId}`);
8684
- const table = await this.store.getTable(job.tableId);
8685
- if (table === void 0)
8686
- throw new Error(`Compaction table not found: ${job.tableId}`);
8687
- return this.#serializedCompactionStep(table.id, () => this.#runCompactionJob(table, job, maxBlocks));
9018
+ return this.#foreground(async () => {
9019
+ const maxBlocks = boundedMaintenanceBatchItems(options.maxBlocks ?? 1, "Compaction step block limit");
9020
+ const job = await this.store.getCompactionJob(jobId);
9021
+ if (job === void 0)
9022
+ throw new Error(`Compaction job not found: ${jobId}`);
9023
+ const table = await this.store.getTable(job.tableId);
9024
+ if (table === void 0)
9025
+ throw new Error(`Compaction table not found: ${job.tableId}`);
9026
+ return this.#serializedCompactionStep(table.id, () => this.#runCompactionJob(table, job, maxBlocks));
9027
+ });
8688
9028
  }
8689
9029
  async #serializedCompactionStep(tableId, step) {
8690
9030
  const previous = this.#compactionSteps.get(tableId) ?? Promise.resolve();
@@ -8711,27 +9051,31 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
8711
9051
  return void 0;
8712
9052
  }
8713
9053
  async listCompactionJobs(tableName) {
8714
- if (tableName === void 0)
8715
- return this.store.listCompactionJobs();
8716
- const table = await this.#findTable(tableName);
8717
- return this.store.listCompactionJobs(table.id);
9054
+ return this.#foreground(async () => {
9055
+ if (tableName === void 0)
9056
+ return this.store.listCompactionJobs();
9057
+ const table = await this.#findTable(tableName);
9058
+ return this.store.listCompactionJobs(table.id);
9059
+ });
8718
9060
  }
8719
9061
  async cancelCompactionJob(jobId) {
8720
- for (; ; ) {
8721
- const job = await this.store.getCompactionJob(jobId);
8722
- if (job === void 0)
8723
- throw new Error(`Compaction job not found: ${jobId}`);
8724
- if (job.state === "cancelled" || job.state === "published" || job.state === "aborted") {
8725
- return compactionCancellationResult(job);
8726
- }
8727
- try {
8728
- return compactionCancellationResult(await this.store.cancelCompactionJob(job.id, job.revision, dateIsoString(this.#now())));
8729
- } catch (error) {
8730
- if (error instanceof CompactionJobConflictError)
8731
- continue;
8732
- throw error;
9062
+ return this.#foreground(async () => {
9063
+ for (; ; ) {
9064
+ const job = await this.store.getCompactionJob(jobId);
9065
+ if (job === void 0)
9066
+ throw new Error(`Compaction job not found: ${jobId}`);
9067
+ if (job.state === "cancelled" || job.state === "published" || job.state === "aborted") {
9068
+ return compactionCancellationResult(job);
9069
+ }
9070
+ try {
9071
+ return compactionCancellationResult(await this.store.cancelCompactionJob(job.id, job.revision, dateIsoString(this.#now())));
9072
+ } catch (error) {
9073
+ if (error instanceof CompactionJobConflictError)
9074
+ continue;
9075
+ throw error;
9076
+ }
8733
9077
  }
8734
- }
9078
+ });
8735
9079
  }
8736
9080
  async #cancelTableCompactions(tableId) {
8737
9081
  let cursor = null;
@@ -8748,28 +9092,32 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
8748
9092
  } while (cursor !== null);
8749
9093
  }
8750
9094
  async collectGarbage(options = {}) {
8751
- const maxItems = boundedMaintenanceBatchItems(options.maxItemsPerStep ?? 64, "Garbage collection items per step");
8752
- let reconciliationCursor = null;
8753
- do {
8754
- reconciliationCursor = (await this.#reconcileCompactionPage(reconciliationCursor)).nextCursor;
8755
- } while (reconciliationCursor !== null);
8756
- let progress = await this.collectGarbageStep({
8757
- maxItems,
8758
- ...options.maxPlanningItems === void 0 ? {} : { maxPlanningItems: options.maxPlanningItems },
8759
- ...options.retainRecentVersions === void 0 ? {} : { retainRecentVersions: options.retainRecentVersions }
9095
+ return this.#foreground(async () => {
9096
+ const maxItems = boundedMaintenanceBatchItems(options.maxItemsPerStep ?? 64, "Garbage collection items per step");
9097
+ let reconciliationCursor = null;
9098
+ do {
9099
+ reconciliationCursor = (await this.#reconcileCompactionPage(reconciliationCursor)).nextCursor;
9100
+ } while (reconciliationCursor !== null);
9101
+ let progress = await this.collectGarbageStep({
9102
+ maxItems,
9103
+ ...options.maxPlanningItems === void 0 ? {} : { maxPlanningItems: options.maxPlanningItems },
9104
+ ...options.retainRecentVersions === void 0 ? {} : { retainRecentVersions: options.retainRecentVersions }
9105
+ });
9106
+ while (progress.result === null) {
9107
+ progress = await this.resumeGarbageCollectionJob(progress.jobId, { maxItems });
9108
+ }
9109
+ await this.#pruneFinishedJobRecords();
9110
+ this.#autoCollectionDebtCommits = 0;
9111
+ this.#manualCollectionDebtInitialized = true;
9112
+ this.#commitsSinceCollection = 0;
9113
+ this.#lastCollectionCompletedAt = dateMilliseconds(this.#now());
9114
+ return progress.result;
8760
9115
  });
8761
- while (progress.result === null) {
8762
- progress = await this.resumeGarbageCollectionJob(progress.jobId, { maxItems });
8763
- }
8764
- await this.#pruneFinishedJobRecords();
8765
- this.#autoCollectionDebtCommits = 0;
8766
- this.#manualCollectionDebtInitialized = true;
8767
- this.#commitsSinceCollection = 0;
8768
- this.#lastCollectionCompletedAt = dateMilliseconds(this.#now());
8769
- return progress.result;
8770
9116
  }
8771
9117
  async collectGarbageStep(options = {}) {
8772
- return this.#collectGarbageStep(options);
9118
+ return this.#foreground(async () => {
9119
+ return this.#collectGarbageStep(options);
9120
+ });
8773
9121
  }
8774
9122
  async #collectGarbageStep(options, retainedVersionMaxAgeMs = Number.POSITIVE_INFINITY) {
8775
9123
  const maxPlanningItems = boundedMaintenanceBatchItems(options.maxPlanningItems ?? 1024, "Garbage collection planning limit");
@@ -8794,10 +9142,12 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
8794
9142
  return void 0;
8795
9143
  }
8796
9144
  async resumeGarbageCollectionJob(jobId, options = {}) {
8797
- const job = await this.store.getGarbageCollectionJob(jobId);
8798
- if (job === void 0)
8799
- throw new Error(`Garbage collection job not found: ${jobId}`);
8800
- return this.#serializedCollectionStep(() => this.#runGarbageCollectionJob(job, boundedMaintenanceBatchItems(options.maxItems ?? 1, "Garbage collection item limit")));
9145
+ return this.#foreground(async () => {
9146
+ const job = await this.store.getGarbageCollectionJob(jobId);
9147
+ if (job === void 0)
9148
+ throw new Error(`Garbage collection job not found: ${jobId}`);
9149
+ return this.#serializedCollectionStep(() => this.#runGarbageCollectionJob(job, boundedMaintenanceBatchItems(options.maxItems ?? 1, "Garbage collection item limit")));
9150
+ });
8801
9151
  }
8802
9152
  async #serializedCollectionStep(step) {
8803
9153
  const previous = this.#collectionSteps;
@@ -8915,11 +9265,12 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
8915
9265
  });
8916
9266
  }
8917
9267
  const result = progress.result;
8918
- moreWork = !(result.prunedManifestCount === 0 && result.reclaimedBlockCount === 0 && result.reclaimedSegmentCount === 0 && result.reclaimedTransactionCount === 0);
9268
+ const madeProgress = !(result.prunedManifestCount === 0 && result.reclaimedBlockCount === 0 && result.reclaimedSegmentCount === 0 && result.reclaimedTransactionCount === 0);
9269
+ reclaimed ||= madeProgress;
9270
+ moreWork = (await this.store.getGarbageCollectionJob(result.jobId))?.discovery?.resumePhase != null;
8919
9271
  if (!moreWork) {
8920
9272
  break;
8921
9273
  }
8922
- reclaimed = true;
8923
9274
  await this.#yieldMaintenance();
8924
9275
  }
8925
9276
  await this.#pruneFinishedJobRecords();
@@ -9090,7 +9441,9 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
9090
9441
  } while (cursor !== null);
9091
9442
  }
9092
9443
  async listGarbageCollectionJobs() {
9093
- return this.store.listGarbageCollectionJobs();
9444
+ return this.#foreground(async () => {
9445
+ return this.store.listGarbageCollectionJobs();
9446
+ });
9094
9447
  }
9095
9448
  async #planGarbageCollection(maxPlanningItems, retainRecentVersions, retainedVersionMaxAgeMs = Number.POSITIVE_INFINITY) {
9096
9449
  await this.#transactions.removeExpiredLeases(this.#now());
@@ -9248,9 +9601,9 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
9248
9601
  const complete = reachedPlanningCurrent || page.nextCursor === null || remaining <= 0;
9249
9602
  next = {
9250
9603
  ...next,
9251
- phase: complete ? remaining <= 0 ? "complete" : discovery.postManifestPhase ?? "manifest-blocks" : "manifests",
9604
+ phase: complete ? remaining <= 0 ? "complete" : discovery.postManifestPhase ?? "segments" : "manifests",
9252
9605
  resumePhase: remaining <= 0 ? "manifests" : null,
9253
- postManifestPhase: remaining <= 0 ? discovery.postManifestPhase ?? null : null,
9606
+ postManifestPhase: !complete || remaining <= 0 ? discovery.postManifestPhase ?? null : null,
9254
9607
  manifestCursor: remaining <= 0 ? lastVisited : complete ? null : page.nextCursor,
9255
9608
  visitedRecords: safeWholeNumberSum([discovery.visitedRecords, page.records.length], "Garbage collection discovery records")
9256
9609
  };
@@ -9270,7 +9623,7 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
9270
9623
  const lastExaminedBlockId = scanned.nextIndex === 0 ? afterBlockId : pageIds[scanned.nextIndex - 1] ?? afterBlockId;
9271
9624
  next = {
9272
9625
  ...next,
9273
- phase: remaining <= 0 ? "complete" : reachedEnd ? "segments" : "manifest-blocks",
9626
+ phase: remaining <= 0 ? "complete" : reachedEnd ? "transactions" : "manifest-blocks",
9274
9627
  resumePhase: remaining <= 0 ? "manifest-blocks" : null,
9275
9628
  manifestCursor: null,
9276
9629
  artifactCursor: reachedEnd && remaining > 0 ? null : {
@@ -9283,9 +9636,11 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
9283
9636
  visitedRecords: safeWholeNumberSum([discovery.visitedRecords, page.records.length], "Garbage collection discovery records")
9284
9637
  };
9285
9638
  } else if (discovery.phase === "segments") {
9286
- const page = await this.store.listSegmentPage(discovery.segmentCursor, 1);
9639
+ const page = await this.store.listSegmentPage(discovery.segmentCursor, Math.max(1, Math.min(MAINTENANCE_JOB_PAGE_SIZE, remaining)));
9640
+ const ownerIds = [...new Set(page.records.map((segment) => segment.transactionId))];
9641
+ const owners = new Map((await this.store.getTransactions(ownerIds)).filter((owner) => owner !== void 0).map((owner) => [owner.id, owner]));
9287
9642
  for (const segment of page.records) {
9288
- const listedOwner = await this.store.getTransaction(segment.transactionId);
9643
+ const listedOwner = owners.get(segment.transactionId);
9289
9644
  if (listedOwner?.status === "active" && Date.parse(listedOwner.expiresAt) <= Date.parse(job.leaseCutoff)) {
9290
9645
  await this.store.abortTransactionIfExpired({
9291
9646
  transactionId: listedOwner.id,
@@ -9301,11 +9656,11 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
9301
9656
  if (remaining <= 0)
9302
9657
  break;
9303
9658
  }
9304
- const record = page.records[0];
9659
+ const record = page.records.at(-1);
9305
9660
  const filled = remaining <= 0;
9306
9661
  next = {
9307
9662
  ...next,
9308
- phase: record === void 0 ? "transactions" : filled ? "complete" : "segments",
9663
+ phase: record === void 0 ? "manifest-blocks" : filled ? "complete" : "segments",
9309
9664
  resumePhase: filled ? "segments" : null,
9310
9665
  segmentCursor: record?.id ?? null,
9311
9666
  artifactCursor: null,
@@ -11369,57 +11724,59 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
11369
11724
  this.#artifactCache.put(key, payload, bytes);
11370
11725
  }
11371
11726
  async createIndex(indexName, tableName, requestedColumns, options = {}) {
11372
- const indexColumns = typeof requestedColumns === "string" ? [{ name: requestedColumns, direction: "asc" }] : requestedColumns;
11373
- const tables = await this.store.listTables();
11374
- if (tables.some((table2) => Object.values(table2.secondaryIndexes ?? {}).some((index) => index.name === indexName))) {
11375
- throw new TypeError(`Index already exists: ${indexName}`);
11376
- }
11377
- const table = tables.find((candidate) => candidate.name === tableName);
11378
- if (table === void 0 || table.view !== void 0) {
11379
- throw new UnknownTableError(tableName);
11380
- }
11381
- if (indexColumns.length === 0)
11382
- throw new TypeError("An index needs at least one column");
11383
- const duplicate = indexColumns.find((column, position) => indexColumns.findIndex((candidate) => candidate.name === column.name) !== position);
11384
- if (duplicate !== void 0) {
11385
- throw new TypeError(`Index column appears more than once: ${duplicate.name}`);
11386
- }
11387
- const columns = indexColumns.map(({ name }) => {
11388
- const column = table.columns.find((candidate) => candidate.name === name);
11389
- if (column === void 0)
11390
- throw new TypeError(`Unknown column: ${name}`);
11391
- return column;
11392
- });
11393
- const indexId = this.#createId();
11394
- const storageColumnId = `secondary-index:${indexId}`;
11395
- const buildId = this.#createId();
11396
- const marked = await this.store.updateTable(table.id, table.revision, {
11397
- secondaryIndexes: {
11398
- ...table.secondaryIndexes,
11399
- [indexId]: {
11400
- name: indexName,
11401
- columnId: columns[0]?.id ?? "",
11402
- columnIds: columns.map((column) => column.id),
11403
- directions: indexColumns.map((column) => column.direction),
11404
- ...options.unique === true ? { unique: true } : {},
11405
- termEncoding: "tuple-v1",
11406
- storage: "postings-v1",
11407
- storageColumnId,
11408
- locator: table.uniqueKeyColumnId === void 0 ? "row-id" : "key-hash-v1",
11409
- state: "building",
11410
- buildId,
11411
- buildFromVersion: -1
11727
+ return this.#foreground(async () => {
11728
+ const indexColumns = typeof requestedColumns === "string" ? [{ name: requestedColumns, direction: "asc" }] : requestedColumns;
11729
+ const tables = await this.store.listTables();
11730
+ if (tables.some((table2) => Object.values(table2.secondaryIndexes ?? {}).some((index) => index.name === indexName))) {
11731
+ throw new TypeError(`Index already exists: ${indexName}`);
11732
+ }
11733
+ const table = tables.find((candidate) => candidate.name === tableName);
11734
+ if (table === void 0 || table.view !== void 0) {
11735
+ throw new UnknownTableError(tableName);
11736
+ }
11737
+ if (indexColumns.length === 0)
11738
+ throw new TypeError("An index needs at least one column");
11739
+ const duplicate = indexColumns.find((column, position) => indexColumns.findIndex((candidate) => candidate.name === column.name) !== position);
11740
+ if (duplicate !== void 0) {
11741
+ throw new TypeError(`Index column appears more than once: ${duplicate.name}`);
11742
+ }
11743
+ const columns = indexColumns.map(({ name }) => {
11744
+ const column = table.columns.find((candidate) => candidate.name === name);
11745
+ if (column === void 0)
11746
+ throw new TypeError(`Unknown column: ${name}`);
11747
+ return column;
11748
+ });
11749
+ const indexId = this.#createId();
11750
+ const storageColumnId = `secondary-index:${indexId}`;
11751
+ const buildId = this.#createId();
11752
+ const marked = await this.store.updateTable(table.id, table.revision, {
11753
+ secondaryIndexes: {
11754
+ ...table.secondaryIndexes,
11755
+ [indexId]: {
11756
+ name: indexName,
11757
+ columnId: columns[0]?.id ?? "",
11758
+ columnIds: columns.map((column) => column.id),
11759
+ directions: indexColumns.map((column) => column.direction),
11760
+ ...options.unique === true ? { unique: true } : {},
11761
+ termEncoding: "tuple-v1",
11762
+ storage: "postings-v1",
11763
+ storageColumnId,
11764
+ locator: table.uniqueKeyColumnId === void 0 ? "row-id" : "key-hash-v1",
11765
+ state: "building",
11766
+ buildId,
11767
+ buildFromVersion: -1
11768
+ }
11412
11769
  }
11770
+ });
11771
+ try {
11772
+ await this.#buildSecondaryIndex(marked, indexId);
11773
+ } catch (error) {
11774
+ await this.#removeSecondaryIndexIfOwned(marked.id, indexId, indexName, storageColumnId).catch((cleanupError) => {
11775
+ throw new AggregateError([error, cleanupError], `CREATE INDEX ${indexName} failed and its catalog cleanup also failed`);
11776
+ });
11777
+ throw error;
11413
11778
  }
11414
11779
  });
11415
- try {
11416
- await this.#buildSecondaryIndex(marked, indexId);
11417
- } catch (error) {
11418
- await this.#removeSecondaryIndexIfOwned(marked.id, indexId, indexName, storageColumnId).catch((cleanupError) => {
11419
- throw new AggregateError([error, cleanupError], `CREATE INDEX ${indexName} failed and its catalog cleanup also failed`);
11420
- });
11421
- throw error;
11422
- }
11423
11780
  }
11424
11781
  async #removeSecondaryIndexIfOwned(tableId, indexId, indexName, storageColumnId) {
11425
11782
  for (let attempt = 0; attempt <= this.#maxCommitRetries; attempt += 1) {
@@ -11444,20 +11801,22 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
11444
11801
  }
11445
11802
  }
11446
11803
  async dropIndex(indexName, options = {}) {
11447
- for (const table of await this.store.listTables()) {
11448
- const entry = Object.entries(table.secondaryIndexes ?? {}).find(([, index]) => index.name === indexName);
11449
- if (entry === void 0)
11450
- continue;
11451
- const [indexId] = entry;
11452
- const indexes = Object.fromEntries(Object.entries(table.secondaryIndexes ?? {}).filter(([candidateId]) => candidateId !== indexId));
11453
- await this.store.updateTable(table.id, table.revision, {
11454
- secondaryIndexes: Object.keys(indexes).length === 0 ? null : indexes
11455
- });
11456
- return true;
11457
- }
11458
- if (options.ifExists === true)
11459
- return false;
11460
- throw new TypeError(`Unknown index: ${indexName}`);
11804
+ return this.#foreground(async () => {
11805
+ for (const table of await this.store.listTables()) {
11806
+ const entry = Object.entries(table.secondaryIndexes ?? {}).find(([, index]) => index.name === indexName);
11807
+ if (entry === void 0)
11808
+ continue;
11809
+ const [indexId] = entry;
11810
+ const indexes = Object.fromEntries(Object.entries(table.secondaryIndexes ?? {}).filter(([candidateId]) => candidateId !== indexId));
11811
+ await this.store.updateTable(table.id, table.revision, {
11812
+ secondaryIndexes: Object.keys(indexes).length === 0 ? null : indexes
11813
+ });
11814
+ return true;
11815
+ }
11816
+ if (options.ifExists === true)
11817
+ return false;
11818
+ throw new TypeError(`Unknown index: ${indexName}`);
11819
+ });
11461
11820
  }
11462
11821
  async #buildSecondaryIndex(table, indexId) {
11463
11822
  const index = table.secondaryIndexes?.[indexId];
@@ -11826,25 +12185,27 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
11826
12185
  }
11827
12186
  }
11828
12187
  async buildFtsIndex(tableName, columnName) {
11829
- const table = await this.#findTable(tableName);
11830
- const column = table.columns.find((candidate) => candidate.name === columnName);
11831
- if (column === void 0)
11832
- throw new TypeError(`Unknown column: ${columnName}`);
11833
- const key = `${table.id}/${column.id}`;
11834
- if (this.#droppingFtsColumns.has(key)) {
11835
- throw new TypeError(`Column is being dropped: ${tableName}.${columnName}`);
11836
- }
11837
- const existing = this.#ftsBuildsInFlight.get(key);
11838
- if (existing !== void 0)
11839
- return existing;
11840
- const build = this.#buildFtsIndex(table, column);
11841
- this.#ftsBuildsInFlight.set(key, build);
11842
- try {
11843
- await build;
11844
- } finally {
11845
- if (this.#ftsBuildsInFlight.get(key) === build)
11846
- this.#ftsBuildsInFlight.delete(key);
11847
- }
12188
+ return this.#foreground(async () => {
12189
+ const table = await this.#findTable(tableName);
12190
+ const column = table.columns.find((candidate) => candidate.name === columnName);
12191
+ if (column === void 0)
12192
+ throw new TypeError(`Unknown column: ${columnName}`);
12193
+ const key = `${table.id}/${column.id}`;
12194
+ if (this.#droppingFtsColumns.has(key)) {
12195
+ throw new TypeError(`Column is being dropped: ${tableName}.${columnName}`);
12196
+ }
12197
+ const existing = this.#ftsBuildsInFlight.get(key);
12198
+ if (existing !== void 0)
12199
+ return existing;
12200
+ const build = this.#buildFtsIndex(table, column);
12201
+ this.#ftsBuildsInFlight.set(key, build);
12202
+ try {
12203
+ await build;
12204
+ } finally {
12205
+ if (this.#ftsBuildsInFlight.get(key) === build)
12206
+ this.#ftsBuildsInFlight.delete(key);
12207
+ }
12208
+ });
11848
12209
  }
11849
12210
  async #buildFtsIndex(table, column) {
11850
12211
  const tableName = table.name;
@@ -13359,6 +13720,7 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
13359
13720
  }
13360
13721
  }
13361
13722
  }
13723
+ #retiredLeaseRenewals = /* @__PURE__ */ new WeakMap();
13362
13724
  async #renewInternalLeaseIfNeeded(snapshot) {
13363
13725
  if (dateMilliseconds(snapshot.expiresAt) - dateMilliseconds(this.#now()) > INTERNAL_READ_LEASE_TTL_MS / 2) {
13364
13726
  return;
@@ -13370,7 +13732,17 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
13370
13732
  await this.#sharedLeaseRenewal;
13371
13733
  return;
13372
13734
  }
13373
- await snapshot.renew(INTERNAL_READ_LEASE_TTL_MS);
13735
+ let renewal = this.#retiredLeaseRenewals.get(snapshot);
13736
+ if (renewal === void 0) {
13737
+ renewal = snapshot.renew(INTERNAL_READ_LEASE_TTL_MS).then(() => void 0);
13738
+ this.#retiredLeaseRenewals.set(snapshot, renewal);
13739
+ }
13740
+ try {
13741
+ await renewal;
13742
+ } finally {
13743
+ if (this.#retiredLeaseRenewals.get(snapshot) === renewal)
13744
+ this.#retiredLeaseRenewals.delete(snapshot);
13745
+ }
13374
13746
  }
13375
13747
  async #encodeColumnBlock(columnId, input, preferred = this.#compression) {
13376
13748
  return this.#encodePreferredBlock(columnId, preferred, columnInputBytesBelow(input, GZIP_MINIMUM_INPUT_BYTES), (compression) => encodeBlock(input, compression));
@@ -14577,19 +14949,6 @@ const LIVE_ORDER_ALIAS = `${LIVE_HIDDEN_PREFIX}order_`;
14577
14949
  const LIVE_MAINTENANCE_MAX_DELTA_ROWS = 2048;
14578
14950
  const LIVE_WINDOW_MARGIN_MIN = 16;
14579
14951
  const LIVE_WINDOW_MARGIN_MAX = 64;
14580
- function sameLiveRow(left, right, columns) {
14581
- for (const column of columns) {
14582
- const a = left[column] ?? null;
14583
- const b = right[column] ?? null;
14584
- if (a instanceof Date || b instanceof Date) {
14585
- if (!(a instanceof Date && b instanceof Date) || !Object.is(dateMilliseconds(a), dateMilliseconds(b))) {
14586
- return false;
14587
- }
14588
- } else if (!Object.is(a, b))
14589
- return false;
14590
- }
14591
- return true;
14592
- }
14593
14952
  function liveKeyToken(value) {
14594
14953
  if (typeof value === "number")
14595
14954
  return `n:${String(value)}`;
@@ -14609,7 +14968,7 @@ function splitLiveHiddenColumns(executed, state) {
14609
14968
  const order = state.orderTerms.map(() => new Array(count));
14610
14969
  for (let index = 0; index < count; index += 1) {
14611
14970
  const row = executed.rows[index] ?? {};
14612
- keys[index] = row[LIVE_KEY_ALIAS] ?? null;
14971
+ keys[index] = liveKeyToken(row[LIVE_KEY_ALIAS] ?? null);
14613
14972
  for (const [term, { alias }] of state.orderTerms.entries()) {
14614
14973
  const values = order[term];
14615
14974
  if (values !== void 0)
@@ -14619,28 +14978,26 @@ function splitLiveHiddenColumns(executed, state) {
14619
14978
  Reflect.deleteProperty(row, column);
14620
14979
  }
14621
14980
  return {
14622
- result: {
14981
+ result: externalizeQueryResult({
14623
14982
  columns: executed.columns.slice(0, publicCount),
14624
14983
  columnDomains: executed.columnDomains.slice(0, publicCount),
14625
14984
  rows: executed.rows
14626
- },
14985
+ }),
14627
14986
  keys,
14628
14987
  order
14629
14988
  };
14630
14989
  }
14631
14990
  function liveMaintainedOutcome(state, previous, previousIndex) {
14632
14991
  const positions = /* @__PURE__ */ new Map();
14633
- let retainedBytes = 128 + planMemoKey(state.fullPlan).length * 2 + planMemoKey(state.deltaPlan).length * 2;
14992
+ let retainedBytes = state.retainedBytes ?? 128 + planMemoKey(state.fullPlan).length * 2 + planMemoKey(state.deltaPlan).length * 2;
14634
14993
  for (const [index, key] of state.keys.entries()) {
14635
- const token = liveKeyToken(key);
14636
- positions.set(token, index);
14637
- retainedBytes += 64 + token.length * 2;
14994
+ if (positions.has(key))
14995
+ throw new TypeError("Duplicate live input key");
14996
+ positions.set(key, index);
14997
+ if (state.retainedBytes === void 0)
14998
+ retainedBytes += liveRowStateBytes(state, index);
14638
14999
  }
14639
- for (const row of state.rows)
14640
- retainedBytes += 48 + estimateValuesBytes(Object.values(row)) * 2;
14641
- for (const values of state.order)
14642
- retainedBytes += values.length * 8 + estimateValuesBytes(values) * 2;
14643
- state = { ...state, positions };
15000
+ state = { ...state, positions, retainedBytes };
14644
15001
  const visibleCount = state.limit === void 0 ? state.rows.length : Math.min(state.rows.length, state.limit);
14645
15002
  const visible = state.rows.slice(0, visibleCount);
14646
15003
  if (previous === void 0) {
@@ -14658,12 +15015,10 @@ function liveMaintainedOutcome(state, previous, previousIndex) {
14658
15015
  const previousCount = previous.rows.length;
14659
15016
  const retained = new Int32Array(visibleCount);
14660
15017
  let same = visibleCount === previousCount;
14661
- let kept = 0;
14662
15018
  for (let index = 0; index < visibleCount; index += 1) {
14663
15019
  const was = previousIndex?.[index] ?? -1;
14664
15020
  if (was >= 0 && was < previousCount) {
14665
15021
  retained[index] = was;
14666
- kept += 1;
14667
15022
  if (was !== index)
14668
15023
  same = false;
14669
15024
  continue;
@@ -14674,7 +15029,6 @@ function liveMaintainedOutcome(state, previous, previousIndex) {
14674
15029
  if (before !== void 0 && now !== void 0 && sameLiveRow(before, now, previous.columns)) {
14675
15030
  visible[index] = before;
14676
15031
  retained[index] = index;
14677
- kept += 1;
14678
15032
  } else
14679
15033
  same = false;
14680
15034
  }
@@ -14689,9 +15043,15 @@ function liveMaintainedOutcome(state, previous, previousIndex) {
14689
15043
  state,
14690
15044
  retainedBytes,
14691
15045
  changed: true,
14692
- ...kept > 0 ? { retained } : {}
15046
+ retained
14693
15047
  };
14694
15048
  }
15049
+ function liveRowStateBytes(state, index) {
15050
+ let bytes = 112 + (state.keys[index]?.length ?? 0) * 2 + estimateValuesBytes(Object.values(state.rows[index] ?? {})) * 2;
15051
+ for (const values of state.order)
15052
+ bytes += 8 + estimateValuesBytes([values[index]]) * 2;
15053
+ return bytes;
15054
+ }
14695
15055
  function filterLiveRows(state, keep) {
14696
15056
  const rows = [];
14697
15057
  const keys = [];
@@ -14702,7 +15062,7 @@ function filterLiveRows(state, keep) {
14702
15062
  if (row === void 0 || !keep(index))
14703
15063
  continue;
14704
15064
  rows.push(row);
14705
- keys.push(state.keys[index] ?? null);
15065
+ keys.push(state.keys[index] ?? "z");
14706
15066
  for (const [term, values] of state.order.entries()) {
14707
15067
  order[term]?.push(values[index] ?? null);
14708
15068
  }
@@ -14749,7 +15109,7 @@ function mergeLiveRows(kept, added, compare, ordered) {
14749
15109
  const previousIndex = new Int32Array(total);
14750
15110
  const take = (source, from, to) => {
14751
15111
  rows[to] = source.rows[from] ?? {};
14752
- keys[to] = source.keys[from] ?? null;
15112
+ keys[to] = source.keys[from] ?? "z";
14753
15113
  for (let term = 0; term < terms; term += 1) {
14754
15114
  const values = order[term];
14755
15115
  if (values !== void 0)
@@ -15560,17 +15920,14 @@ function installStreamedWindow(vector, resident, windowStart, windowRows, memory
15560
15920
  mutable.window = { start: windowStart, length: windowRows };
15561
15921
  }
15562
15922
  function derivedColumnarTable(name, result, schema) {
15563
- const columns = new Map(schema.map(({ name: columnName, type, sqlDomain }) => [
15564
- columnName,
15565
- {
15566
- type,
15567
- values: sqlDomain?.kind === "numeric" ? result.rows.map((row) => {
15568
- const value = row[columnName] ?? null;
15569
- return typeof value === "number" ? exactNumericValue(value) : value;
15570
- }) : result.rows.map((row) => row[columnName] ?? null)
15571
- }
15572
- ]));
15573
- return createColumnarTable(name, columns);
15923
+ const columns = /* @__PURE__ */ new Map();
15924
+ for (const { name: columnName, type, sqlDomain } of schema) {
15925
+ columns.set(columnName, createColumnVector(type, result.rows.length, (index) => {
15926
+ const value = result.rows[index]?.[columnName] ?? null;
15927
+ return sqlDomain?.kind === "numeric" && typeof value === "number" ? exactNumericValue(value) : value;
15928
+ }));
15929
+ }
15930
+ return { name, rowCount: result.rows.length, columns };
15574
15931
  }
15575
15932
  function fillColumnVectorRange(vector, start, count, value) {
15576
15933
  for (let offset = 0; offset < count; offset += 1) {
@@ -17399,7 +17756,7 @@ function blockVectorRetainedBytes(vector) {
17399
17756
  const memoized = blockVectorRetainedBytesMemo.get(vector);
17400
17757
  if (memoized !== void 0)
17401
17758
  return memoized;
17402
- const bytes = columnVectorRetainedBytes(vector);
17759
+ const bytes = 8 + columnVectorRetainedBytes(vector);
17403
17760
  blockVectorRetainedBytesMemo.set(vector, bytes);
17404
17761
  return bytes;
17405
17762
  }
@@ -17713,6 +18070,19 @@ function asColumnInput(type, values) {
17713
18070
  return { type, values };
17714
18071
  }
17715
18072
  }
18073
+ async function untilAborted(run, signal) {
18074
+ signal.throwIfAborted();
18075
+ let abort = () => void 0;
18076
+ const cancelled = new Promise((_resolve, reject) => {
18077
+ abort = () => reject(signal.reason instanceof Error ? signal.reason : new Error("Database scope cancelled", { cause: signal.reason }));
18078
+ signal.addEventListener("abort", abort, { once: true });
18079
+ });
18080
+ try {
18081
+ return await Promise.race([run(), cancelled]);
18082
+ } finally {
18083
+ signal.removeEventListener("abort", abort);
18084
+ }
18085
+ }
17716
18086
  export {
17717
18087
  BufferedTableWriter2 as BufferedTableWriter,
17718
18088
  CompactionBacklogError,