@minnowdb/core 0.7.5 → 0.7.7

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.
@@ -1,7 +1,7 @@
1
1
  import { type CompactionJobRecord, type GarbageCollectionJobRecord, type StorageIntegrityMode, type StorageIntegrityReport, type StorageStats, type InterruptedSnapshotImport, type InterruptedSnapshotImportAbortResult } from "../storage/types.js";
2
2
  import { type BatchRow, type InsertBatchInput } from "./batch.js";
3
3
  import type { Catalog } from "./catalog.js";
4
- import type { BatchValue, BufferPoolStats, StagedWriteResult, BufferedFlushResult, BufferedWriterOptions, CancelCompactionJobResult, CollectGarbageOptions, CollectGarbageStepOptions, CompactTableOptions, CompactTableResult, CompactTableStepOptions, CompactionJobProgress, CreateTableInput, DatabaseRow, MigrateOptions, DeleteBatchInput, DeleteBatchResult, ExecuteOptions, ExecuteResult, GarbageCollectionProgress, GarbageCollectionResult, MaintenanceStatus, InsertBatchResult, QueryOptions, QueryExecutionStats, QueryCursorOptions, QuerySpillCleanupOptions, QuerySpillCleanupResult, ReadTableOptions, RunStatementOptions, SnapshotExportOptions, SnapshotImportOptions, TableDefinition, UpdateBatchInput, UpdateBatchResult, UpsertBatchResult, UpsertOptions, VisibleSegmentPage, VisibleSegmentPageOptions } from "./database.js";
4
+ import type { BatchValue, BufferPoolStats, StagedWriteResult, StagedUpsertResult, BufferedFlushResult, BufferedWriterOptions, CancelCompactionJobResult, CollectGarbageOptions, CollectGarbageStepOptions, CompactTableOptions, CompactTableResult, CompactTableStepOptions, CompactionJobProgress, CreateTableInput, DatabaseRow, MigrateOptions, DeleteBatchInput, DeleteBatchResult, ExecuteOptions, ExecuteResult, GarbageCollectionProgress, GarbageCollectionResult, MaintenanceStatus, InsertBatchResult, QueryOptions, QueryExecutionStats, QueryCursorOptions, QuerySpillCleanupOptions, QuerySpillCleanupResult, ReadTableOptions, RunStatementOptions, SnapshotExportOptions, SnapshotImportOptions, TableDefinition, UpdateBatchInput, UpdateBatchResult, UpsertBatchResult, UpsertOptions, VisibleSegmentPage, VisibleSegmentPageOptions } from "./database.js";
5
5
  import type { LiveQueryInput, LiveQueryInvalidation, LiveQueryObserveOptions, LiveQueryStats, LiveQuerySubscribeOptions } from "./live.js";
6
6
  import type { CompiledQuery, CompiledStatement, QueryResult, QueryValue } from "./query.js";
7
7
  import type { AnyTable, SchemaDefinition } from "./schema.js";
@@ -210,7 +210,7 @@ export interface ClientWriteSession {
210
210
  query(sql: string, options?: QueryOptions): Promise<QueryResult>;
211
211
  execute(sql: string, params?: readonly QueryValue[]): Promise<ExecuteResult>;
212
212
  insertBatch(tableName: string, input: InsertBatchInput): Promise<StagedWriteResult>;
213
- upsertBatch(tableName: string, input: InsertBatchInput): Promise<StagedWriteResult>;
213
+ upsertBatch(tableName: string, input: InsertBatchInput, options?: UpsertOptions): Promise<StagedUpsertResult>;
214
214
  updateBatch(tableName: string, input: UpdateBatchInput): Promise<StagedWriteResult>;
215
215
  deleteBatch(tableName: string, input: DeleteBatchInput): Promise<StagedWriteResult>;
216
216
  }
@@ -290,7 +290,12 @@ class MinnowDatabaseClient {
290
290
  }
291
291
  async write(action) {
292
292
  const opened = await this.#call("writeOpen", []);
293
- const stage = (op, tableName, input) => this._invoke(opened.handleId, "stage", [op, tableName, input]);
293
+ const stage = (op, tableName, input, options) => this._invoke(opened.handleId, "stage", [
294
+ op,
295
+ tableName,
296
+ input,
297
+ options
298
+ ]);
294
299
  const session = {
295
300
  query: async (sql, options = {}) => {
296
301
  const { signal, onStats, ...wireOptions } = options;
@@ -298,7 +303,7 @@ class MinnowDatabaseClient {
298
303
  },
299
304
  execute: (sql, params) => this._invoke(opened.handleId, "execute", params === void 0 ? [sql] : [sql, params]),
300
305
  insertBatch: (tableName, input) => stage("insertBatch", tableName, input),
301
- upsertBatch: (tableName, input) => stage("upsertBatch", tableName, input),
306
+ upsertBatch: (tableName, input, options) => stage("upsertBatch", tableName, input, options),
302
307
  updateBatch: (tableName, input) => stage("updateBatch", tableName, input),
303
308
  deleteBatch: (tableName, input) => stage("deleteBatch", tableName, input)
304
309
  };
@@ -174,6 +174,10 @@ export interface StagedWriteResult {
174
174
  /** Values filled by defaults or auto-increment while the rows were staged. */
175
175
  generatedColumns?: Record<string, QueryValue[]>;
176
176
  }
177
+ export interface StagedUpsertResult extends StagedWriteResult {
178
+ /** Input rows rejected by `conflictWhere`; always 0 without it. */
179
+ skippedRowCount: number;
180
+ }
177
181
  /**
178
182
  * The scope handed to `write()`: every mutation stages into one transaction and publishes
179
183
  * as one commit — all of it or none of it, in every tab. Reads observe the pre-scope snapshot
@@ -194,7 +198,7 @@ export interface WriteSession {
194
198
  /** Runs a SELECT, INSERT, UPDATE, or DELETE inside this write scope. */
195
199
  execute(sql: string, params?: readonly QueryValue[]): Promise<ExecuteResult>;
196
200
  insertBatch(tableName: string, input: InsertBatchInput): Promise<StagedWriteResult>;
197
- upsertBatch(tableName: string, input: InsertBatchInput): Promise<StagedWriteResult>;
201
+ upsertBatch(tableName: string, input: InsertBatchInput, options?: UpsertOptions): Promise<StagedUpsertResult>;
198
202
  updateBatch(tableName: string, input: UpdateBatchInput): Promise<StagedWriteResult>;
199
203
  deleteBatch(tableName: string, input: DeleteBatchInput): Promise<StagedWriteResult>;
200
204
  }
@@ -13,7 +13,7 @@ import { estimateCompactionRowsPerOutput, planAlignedWriteBlockRanges as writeBl
13
13
  import { MAX_CACHEABLE_TEXT_CHARACTERS, MAX_SQL_PARAMETERS } from "./cache-limits.js";
14
14
  import { fillColumnDefaults, patchAutoIncrementValues } from "./defaults.js";
15
15
  import { cachedQueryTerms, FTS_TOKENIZER_VERSION, renderDocumentValue, tokenize as ftsTokenize } from "./fts.js";
16
- import { boundedMaintenanceBatchItems, simpleDataTypes, floorWholeNumberProduct, BlockReadBatchTooLargeError, validateColumnDefault, validateEnumValues, secondaryIndexColumnIds, secondaryIndexDirections, secondaryUniqueKeyNamespace, CompactionJobConflictError, CompactionBacklogError, GarbageCollectionJobConflictError, MAX_LEVEL_ZERO_SEGMENTS, MAX_FTS_CANDIDATE_ROW_IDS, MAX_FTS_POSTING_TERM_CHARACTERS, MAX_FTS_POSTINGS_PER_CHUNK, MAX_FTS_POSTING_ROW_IDS_PER_CHUNK, MAX_INDEXED_STRING_CHARACTERS, MAX_CATALOG_NAME_CHARACTERS, MAX_MANIFEST_BLOCK_PRESENCE_IDS, MAX_MAINTENANCE_BATCH_ITEMS, MAX_STORAGE_BULK_READ_ITEMS, MAX_TRANSACTION_STAGE_BLOCKS, MAX_TRANSACTION_STAGE_BYTES, MAX_SNAPSHOT_SESSION_TTL_MS, MAX_POSTING_BUILD_TTL_MS, MAX_TEMP_OWNER_TTL_MS, MAX_SNAPSHOT_FRAME_BATCH_BYTES, MAX_SNAPSHOT_FRAME_BATCH_ITEMS, MAX_SNAPSHOT_METADATA_BATCH_BYTES, SNAPSHOT_FRAME_KINDS, validateStorageId, SnapshotManifestMissingError, SchemaConflictError, TableInUseError, TableRecordConflictError, TransactionRecordConflictError, UniqueKeyConflictError, UniqueIndexCoverageError, WriteConflictError } from "../storage/types.js";
16
+ 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";
17
17
  import { decodeSnapshotFrameStream, encodeSnapshotFrameStreamFooter, encodeSnapshotFrameStreamHeader, extendSnapshotFrameStreamChecksum, snapshotFrameEnvelopeParts, snapshotFrameStreamHeaderIdentity } from "../storage/snapshot.js";
18
18
  import { Snapshot, TransactionManager } from "../transactions/index.js";
19
19
  import { applyWindowFunctions, bindPendingSelectShapes, bindPlanParameters, bindStatementParameters, DUAL_TABLE, dualTableRows, blockHasSubqueries, combineUnionResults, compileCheckExpression, compileQuery, hasAggregate, createRecursiveCteState, annotateAvgArgumentScales, compileStatement, comparisonHolds, createPreparedColumnarQuery, evaluateJoinedRowExpression, evaluateRowExpression, externalizeQueryResult, expressionColumnNames, inferBlockSchema, inferResultColumnDomains, isDeferredInsertExpression, isDefaultInsertValue, referencedColumns, childExpressions, expandFtsColumns, expandNaturalJoins, expandSourceColumnAliases, expandViewSources, forEachBlockExpression, forEachNestedBlock, planContainsFts, planHasNaturalJoins, planHasSourceColumnAliases, planReadsTable, planReadsViews, planReadsBeyondSingleScan, projectResultColumns, queryResultNeedsExternalization, resolveStatementDatetimes, subqueryResolutionSteps, topLevelFtsMatchConjuncts, transparentProjectionSource, unknownColumnDomains, validateDefaultExpression, windowOutputDomain, windowOutputType, volatileScalarFunctionNames, annotatePlanIntegerDivision, clonePlanTree, planMayHaveIntegerDivision, foldIdentifierCase, extendGroupByWithKeyDependents, expandRowReferences } from "./query.js";
@@ -78,6 +78,7 @@ const AUTO_COLLECT_RETRY_MIN_MS = 1e3;
78
78
  const AUTO_COLLECT_RETRY_MAX_MS = 6e4;
79
79
  const AUTO_COMPACT_STEP_BLOCKS = 4;
80
80
  const AUTO_COMPACT_MAX_LEVEL_ZERO_SEGMENTS = 256;
81
+ const AUTO_COMPACT_BACKPRESSURE_LEVEL_ZERO_SEGMENTS = 2 * AUTO_COMPACT_MAX_LEVEL_ZERO_SEGMENTS;
81
82
  const AUTO_COMPACT_RETRY_MIN_MS = 250;
82
83
  const AUTO_COMPACT_RETRY_MAX_MS = 6e4;
83
84
  const MAX_COMPACTION_PUBLICATION_CONFLICTS = 64;
@@ -1612,9 +1613,7 @@ class MinnowDatabase {
1612
1613
  const { result, batch, generated, autoIncrement } = completed;
1613
1614
  collectAutoIncrementGenerated(batch, generated, autoIncrement);
1614
1615
  if (result.acceptedRowIndexes.length !== result.requestedRowCount) {
1615
- for (const [name, values] of generated) {
1616
- generated.set(name, result.acceptedRowIndexes.map((index) => values[index] ?? null));
1617
- }
1616
+ remapGeneratedColumns(generated, result.acceptedRowIndexes);
1618
1617
  }
1619
1618
  return {
1620
1619
  tableName: result.tableName,
@@ -2092,30 +2091,21 @@ class MinnowDatabase {
2092
2091
  const normalizedConflictWhere = kind === "upsert" && conflictWhere !== void 0 ? normalizeUpsertConflictWhere(table, conflictWhere) : void 0;
2093
2092
  const upsertKeyColumn = kind === "upsert" ? getUniqueKeyColumn(table) : void 0;
2094
2093
  if (upsertKeyColumn !== void 0) {
2095
- upsertFirings = await this.#upsertTriggerFirings(table, upsertKeyColumn, input, rowCount, (sql, params) => this.#sessionQuery(transaction, sql, { params }), normalizedConflictWhere, normalizedConflictWhere !== void 0);
2094
+ upsertFirings = await this.#upsertTriggerFirings(table, upsertKeyColumn, input, rowCount, (sql, params) => this.#sessionQuery(transaction, sql, { params }), normalizedConflictWhere);
2096
2095
  }
2097
2096
  if (normalizedConflictWhere !== void 0) {
2098
2097
  if (upsertFirings === void 0) {
2099
2098
  throw new Error("Upsert conflict classification is missing");
2100
2099
  }
2101
- const classifiedFirings = upsertFirings;
2102
- acceptedRowIndexes = [...classifiedFirings.inserts, ...classifiedFirings.updates].sort((left, right) => left - right);
2103
- skippedRowCount = classifiedFirings.skipped.length;
2104
- input = selectBatchRows(input, acceptedRowIndexes);
2105
- rowCount = acceptedRowIndexes.length;
2100
+ counts = { inserted: upsertFirings.inserts.length, updated: upsertFirings.updates.length };
2101
+ const filtered = applyUpsertConflictFilter(input, rowCount, upsertFirings);
2102
+ acceptedRowIndexes = filtered.acceptedRowIndexes;
2103
+ skippedRowCount = filtered.skippedRowCount;
2104
+ input = filtered.batch;
2105
+ rowCount = filtered.rowCount;
2106
+ upsertFirings = filtered.firings;
2106
2107
  logicalBytes = estimateBatchBytes(input);
2107
2108
  resolvedKeys = batchKeys(table, input);
2108
- counts = {
2109
- inserted: classifiedFirings.inserts.length,
2110
- updated: classifiedFirings.updates.length
2111
- };
2112
- const acceptedPosition = new Map(acceptedRowIndexes.map((original, position) => [original, position]));
2113
- upsertFirings = {
2114
- inserts: classifiedFirings.inserts.map((index) => acceptedPosition.get(index) ?? -1),
2115
- updates: classifiedFirings.updates.map((index) => acceptedPosition.get(index) ?? -1),
2116
- skipped: [],
2117
- oldImages: acceptedRowIndexes.map((index) => classifiedFirings.oldImages[index])
2118
- };
2119
2109
  if (rowCount === 0) {
2120
2110
  await transaction.abort();
2121
2111
  const version = transaction.snapshotVersion;
@@ -3973,23 +3963,38 @@ class MinnowDatabase {
3973
3963
  return;
3974
3964
  const hintVersion = transaction?.snapshotVersion ?? await this.store.getCurrentManifestVersion();
3975
3965
  const hint = this.#autoCompactionHints.get(table.id);
3976
- if (hint?.version === hintVersion && hint.levelZero < MAX_LEVEL_ZERO_SEGMENTS)
3977
- return;
3978
- let segments = await this.#currentVisibleSegments(table);
3979
- let levelZero = 0;
3980
- for (const segment of segments) {
3981
- if (segment.level === 0)
3982
- levelZero += 1;
3966
+ let visible;
3967
+ let levelZero;
3968
+ if (hint?.version === hintVersion) {
3969
+ ({ visible, levelZero } = hint);
3970
+ } else {
3971
+ const segments = await this.#currentVisibleSegments(table);
3972
+ visible = segments.length;
3973
+ levelZero = countLevelZeroSegments(segments);
3983
3974
  }
3984
- if (levelZero < MAX_LEVEL_ZERO_SEGMENTS)
3975
+ if (levelZero < AUTO_COMPACT_BACKPRESSURE_LEVEL_ZERO_SEGMENTS)
3976
+ return;
3977
+ const atCeiling = levelZero >= MAX_LEVEL_ZERO_SEGMENTS;
3978
+ if (!atCeiling && this.#autoCompactionBackoff.get(table.id)?.retryTimer !== void 0)
3985
3979
  return;
3986
3980
  await this.#publishPendingCompactions();
3987
- await this.compactTableStep(table.name, {
3988
- maxBlocks: AUTO_COMPACT_STEP_BLOCKS,
3989
- maxLevel0Segments: AUTO_COMPACT_MAX_LEVEL_ZERO_SEGMENTS
3990
- });
3991
- segments = await this.#currentVisibleSegments(table);
3992
- levelZero = segments.reduce((count, segment) => count + (segment.level === 0 ? 1 : 0), 0);
3981
+ try {
3982
+ const progress = await this.compactTableStep(table.name, {
3983
+ maxBlocks: AUTO_COMPACT_STEP_BLOCKS,
3984
+ maxLevel0Segments: AUTO_COMPACT_MAX_LEVEL_ZERO_SEGMENTS
3985
+ });
3986
+ if (!atCeiling && progress.result !== null && !progress.result.compacted) {
3987
+ this.#backOffAutoCompaction(table.id, visible);
3988
+ }
3989
+ } catch (error) {
3990
+ if (atCeiling)
3991
+ throw error;
3992
+ this.#backOffAutoCompaction(table.id, visible);
3993
+ return;
3994
+ }
3995
+ if (!atCeiling)
3996
+ return;
3997
+ levelZero = countLevelZeroSegments(await this.#currentVisibleSegments(table));
3993
3998
  if (levelZero >= MAX_LEVEL_ZERO_SEGMENTS) {
3994
3999
  throw new CompactionBacklogError(table.name, levelZero, MAX_LEVEL_ZERO_SEGMENTS);
3995
4000
  }
@@ -4299,9 +4304,9 @@ class MinnowDatabase {
4299
4304
  }
4300
4305
  }
4301
4306
  }
4302
- async #upsertTriggerFirings(table, keyColumn, batch, rowCount, readRows, conflictWhere, force = false) {
4307
+ async #upsertTriggerFirings(table, keyColumn, batch, rowCount, readRows, conflictWhere) {
4303
4308
  const fires = (table.triggers ?? []).some((trigger) => trigger.event === "insert" || trigger.event === "update");
4304
- if (!fires && !force || rowCount === 0)
4309
+ if (!fires && conflictWhere === void 0 || rowCount === 0)
4305
4310
  return void 0;
4306
4311
  const quote = (name) => `"${name.replaceAll('"', '""')}"`;
4307
4312
  const keyValues = batch.columns[keyColumn.name] ?? [];
@@ -4415,8 +4420,15 @@ class MinnowDatabase {
4415
4420
  if (target.uniqueKeyColumnId !== void 0) {
4416
4421
  throw new TypeError(`Trigger bodies insert into keyless tables only: ${target.name}`);
4417
4422
  }
4423
+ let columns = compiled.columns;
4424
+ if (columns.length === 0 && compiled.defaultValues !== true) {
4425
+ columns = visibleTableColumns(target).map((column) => column.name);
4426
+ if (derivedRows.some((row) => row.length !== columns.length)) {
4427
+ throw new TypeError(`Trigger body INSERT must match the ${String(columns.length)} visible columns of ${target.name}`);
4428
+ }
4429
+ }
4418
4430
  const statementNow = this.#now();
4419
- const materialized = await this.#materializeInsertExpressions({ ...compiled, rows: derivedRows }, statementNow);
4431
+ const materialized = await this.#materializeInsertExpressions({ ...compiled, columns, rows: derivedRows }, statementNow);
4420
4432
  const input = insertStatementBatch(materialized);
4421
4433
  coerceSqlWriteBatch(target, input);
4422
4434
  const filled = await fillColumnDefaults(target, input, (sql) => this.#evaluateDefaultExpression(sql, statementNow), derivedRows.length);
@@ -4629,10 +4641,10 @@ class MinnowDatabase {
4629
4641
  staged += 1;
4630
4642
  return guarded(() => this.#sessionInsert(transaction, tableName, input, "insert"));
4631
4643
  },
4632
- upsertBatch: async (tableName, input) => {
4644
+ upsertBatch: async (tableName, input, options2) => {
4633
4645
  open();
4634
4646
  staged += 1;
4635
- return guarded(() => this.#sessionInsert(transaction, tableName, input, "upsert"));
4647
+ return guarded(() => this.#sessionInsert(transaction, tableName, input, "upsert", options2));
4636
4648
  },
4637
4649
  updateBatch: async (tableName, input) => {
4638
4650
  open();
@@ -4866,10 +4878,12 @@ class MinnowDatabase {
4866
4878
  }
4867
4879
  return { added, removed };
4868
4880
  }
4869
- async #sessionInsert(transaction, tableName, input, kind, cascadeBudget = 1) {
4881
+ async #sessionInsert(transaction, tableName, input, kind, options, cascadeBudget = 1) {
4870
4882
  const table = await this.#findTable(tableName);
4871
- await this.#assertCompactionCapacity(table, transaction);
4872
- const { batch, generated, autoIncrement, rowCount } = await this.#fillDefaults(table, input);
4883
+ const filled = await this.#fillDefaults(table, input);
4884
+ const { generated, autoIncrement } = filled;
4885
+ let batch = filled.batch;
4886
+ let rowCount = filled.rowCount;
4873
4887
  if (autoIncrement !== void 0 && autoIncrement.missingIndexes.length > 0) {
4874
4888
  const values = await this.store.reserveAutoIncrement(table.id, autoIncrement.column.id, autoIncrement.missingIndexes.length, autoIncrement.atLeast);
4875
4889
  patchAutoIncrementValues(batch, autoIncrement, values);
@@ -4878,6 +4892,36 @@ class MinnowDatabase {
4878
4892
  validateValue(autoIncrement.column, patched[rowIndex] ?? null, rowIndex);
4879
4893
  }
4880
4894
  }
4895
+ const sessionUpsertKeyColumn = kind === "upsert" ? getUniqueKeyColumn(table) : void 0;
4896
+ const normalizedConflictWhere = kind === "upsert" && options?.conflictWhere !== void 0 ? normalizeUpsertConflictWhere(table, options.conflictWhere) : void 0;
4897
+ if (normalizedConflictWhere !== void 0 && sessionUpsertKeyColumn === void 0) {
4898
+ throw new TypeError(`Table needs a unique key before it can be upserted: ${table.name}`);
4899
+ }
4900
+ let sessionUpsertFirings = sessionUpsertKeyColumn === void 0 ? void 0 : await this.#upsertTriggerFirings(table, sessionUpsertKeyColumn, batch, rowCount, (sql, params) => this.#sessionQuery(transaction, sql, { params }), normalizedConflictWhere);
4901
+ let skippedRowCount = 0;
4902
+ if (normalizedConflictWhere !== void 0) {
4903
+ if (sessionUpsertFirings === void 0) {
4904
+ throw new Error("Upsert conflict classification is missing");
4905
+ }
4906
+ const filtered = applyUpsertConflictFilter(batch, rowCount, sessionUpsertFirings);
4907
+ skippedRowCount = filtered.skippedRowCount;
4908
+ if (filtered.rowCount !== rowCount) {
4909
+ remapGeneratedColumns(generated, filtered.acceptedRowIndexes);
4910
+ }
4911
+ batch = filtered.batch;
4912
+ rowCount = filtered.rowCount;
4913
+ sessionUpsertFirings = filtered.firings;
4914
+ if (rowCount === 0) {
4915
+ return {
4916
+ tableName: table.name,
4917
+ segmentId: null,
4918
+ rowCount: 0,
4919
+ skippedRowCount,
4920
+ ...generated.size === 0 ? {} : { generatedColumns: Object.fromEntries(generated) }
4921
+ };
4922
+ }
4923
+ }
4924
+ await this.#assertCompactionCapacity(table, transaction);
4881
4925
  const keys = batchKeys(table, batch);
4882
4926
  if (keys !== void 0) {
4883
4927
  transaction.setUniqueKeyChanges({
@@ -4889,8 +4933,6 @@ class MinnowDatabase {
4889
4933
  await this.#assertForeignKeysPresent(table, (column) => batch.columns[column] ?? [], (sql, params) => this.#sessionQuery(transaction, sql, { params }), transaction);
4890
4934
  const rowIds = await this.store.reserveRowIds(table.id, rowCount);
4891
4935
  const insertValueAt = (source, column, rowIndex) => source === "new" ? batch.columns[column]?.[rowIndex] ?? null : null;
4892
- const sessionUpsertKeyColumn = kind === "upsert" ? getUniqueKeyColumn(table) : void 0;
4893
- const sessionUpsertFirings = sessionUpsertKeyColumn === void 0 ? void 0 : await this.#upsertTriggerFirings(table, sessionUpsertKeyColumn, batch, rowCount, (sql, params) => this.#sessionQuery(transaction, sql, { params }));
4894
4936
  stageSecondaryUniqueInsertChanges(transaction, table, batch, kind === "upsert" ? sessionUpsertFirings?.oldImages : void 0);
4895
4937
  if (kind === "insert") {
4896
4938
  await this.#stageTriggerDerivedInserts(transaction, table, "insert", rowCount, insertValueAt, "before", cascadeBudget);
@@ -4908,6 +4950,7 @@ class MinnowDatabase {
4908
4950
  tableName: table.name,
4909
4951
  segmentId,
4910
4952
  rowCount,
4953
+ skippedRowCount,
4911
4954
  ...generated.size === 0 ? {} : { generatedColumns: Object.fromEntries(generated) }
4912
4955
  };
4913
4956
  }
@@ -9275,7 +9318,7 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
9275
9318
  }
9276
9319
  const priorAttemptOutputStoredBytes = await this.#priorCompactionAttemptOutputStoredBytes(table.id, baseJobId);
9277
9320
  const maximumOutputStoredBytes = Math.max(0, floorWholeNumberProduct(selection.level0SourceStoredBytes, maxWriteAmplification, "Compaction maximum output stored bytes") - priorAttemptOutputStoredBytes);
9278
- const plannedOutputStoredBytesUpperBound = await this.#plannedPhysicalOutputStoredBytesUpperBound(rewritePlan, snapshot);
9321
+ const plannedOutputStoredBytesUpperBound = await this.#plannedPhysicalOutputStoredBytesUpperBound(rewritePlan, memoryBudgetBytes, snapshot);
9279
9322
  levelTwoBudget = {
9280
9323
  outputPartitionOrdinal,
9281
9324
  maxWriteAmplification,
@@ -9731,17 +9774,15 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
9731
9774
  const columnIndexById = new Map(table.columns.map((column, index) => [column.id, index]));
9732
9775
  const touched = /* @__PURE__ */ new Set();
9733
9776
  const deltaKeys = /* @__PURE__ */ new Map();
9734
- for (const segment of segments) {
9735
- if (!mergeSourceReferencesKeys(segment.kind))
9736
- continue;
9737
- const keys = [];
9738
- await this.#forEachMergeSourceKey(segment, keyColumn, snapshot, (value) => {
9739
- const key = overlayKeyOf(keyColumn.type, value);
9740
- keys.push(key);
9741
- touched.add(key);
9742
- });
9743
- deltaKeys.set(segment.segmentId, keys);
9744
- }
9777
+ const deltas = segments.filter((segment) => mergeSourceReferencesKeys(segment.kind));
9778
+ for (const segment of deltas)
9779
+ deltaKeys.set(segment.segmentId, []);
9780
+ const unusedBudgetBytes = memoryBudgetBytes - plannerMemoryBytes;
9781
+ await this.#forEachMergeSourceKey(deltas, keyColumn, unusedBudgetBytes, snapshot, (segment, value) => {
9782
+ const key = overlayKeyOf(keyColumn.type, value);
9783
+ deltaKeys.get(segment.segmentId)?.push(key);
9784
+ touched.add(key);
9785
+ });
9745
9786
  let slotCount = 0;
9746
9787
  for (const segment of segments) {
9747
9788
  if (mergeSourceBearsRows(segment.kind))
@@ -9813,7 +9854,7 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
9813
9854
  if (keys !== void 0) {
9814
9855
  keys.forEach(visit);
9815
9856
  } else if (touched.size > 0) {
9816
- await this.#forEachMergeSourceKey(segment, keyColumn, snapshot, (value, rowIndex) => {
9857
+ await this.#forEachMergeSourceKey([segment], keyColumn, unusedBudgetBytes, snapshot, (_, value, rowIndex) => {
9817
9858
  visit(overlayKeyOf(keyColumn.type, value), rowIndex);
9818
9859
  });
9819
9860
  }
@@ -9850,28 +9891,48 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
9850
9891
  }
9851
9892
  return { ...output.finish(), sourceOutputRowStarts };
9852
9893
  }
9853
- async #forEachMergeSourceKey(segment, column, snapshot, action) {
9854
- const planned = segment.columns.find((candidate) => candidate.columnId === column.id);
9855
- if (planned === void 0) {
9856
- throw new Error(`Mutation segment has no key column: ${segment.segmentId}`);
9894
+ async #forEachMergeSourceKey(segments, column, unusedBudgetBytes, snapshot, action) {
9895
+ const blocks = [];
9896
+ let largestEncodedBytes = 0;
9897
+ for (const segment of segments) {
9898
+ const planned = segment.columns.find((candidate) => candidate.columnId === column.id);
9899
+ if (planned === void 0) {
9900
+ throw new Error(`Mutation segment has no key column: ${segment.segmentId}`);
9901
+ }
9902
+ for (const source of planned.sourceBlocks) {
9903
+ blocks.push({
9904
+ blockId: source.blockId,
9905
+ storedBytes: source.storedBytes,
9906
+ encodedBytes: source.encodedBytes,
9907
+ segment,
9908
+ source
9909
+ });
9910
+ largestEncodedBytes = Math.max(largestEncodedBytes, source.encodedBytes);
9911
+ }
9857
9912
  }
9858
- let rowIndex = 0;
9859
- for (const source of planned.sourceBlocks) {
9860
- await this.#renewInternalLeaseIfNeeded(snapshot);
9861
- const bytes = await this.store.getBlock(source.blockId);
9862
- if (bytes === void 0)
9863
- throw new Error(`Compaction source block is missing: ${source.blockId}`);
9913
+ const limitBytes = safeWholeNumberSum([
9914
+ Math.max(0, unusedBudgetBytes),
9915
+ safeWholeNumberProduct(largestEncodedBytes, MERGE_PLANNER_DECODED_KEY_BLOCK_FACTOR, "Mutation compaction key decode memory")
9916
+ ], "Mutation compaction key decode memory");
9917
+ const rowIndexes = /* @__PURE__ */ new Map();
9918
+ await this.#decodeCompactionBlocks(blocks, { limitBytes, retainsDecoded: false }, snapshot, async (bytes, { source }) => {
9864
9919
  const description = inspectBlock(bytes);
9865
9920
  if (bytes.byteLength !== source.storedBytes || description.encodedLength !== source.encodedBytes || description.checksum !== source.checksum || description.rowCount !== source.rowCount || description.type !== column.type) {
9866
9921
  throw new Error(`Compaction source block differs from its plan: ${source.blockId}`);
9867
9922
  }
9868
- for (const value of (await decodeBlock(bytes)).column.values) {
9869
- action(value, rowIndex);
9923
+ return (await decodeBlock(bytes)).column.values;
9924
+ }, (values, { segment }) => {
9925
+ let rowIndex = rowIndexes.get(segment.segmentId) ?? 0;
9926
+ for (const value of values) {
9927
+ action(segment, value, rowIndex);
9870
9928
  rowIndex += 1;
9871
9929
  }
9872
- }
9873
- if (rowIndex !== segment.rowCount) {
9874
- throw new Error(`Mutation segment key rows differ: ${segment.segmentId}`);
9930
+ rowIndexes.set(segment.segmentId, rowIndex);
9931
+ });
9932
+ for (const segment of segments) {
9933
+ if ((rowIndexes.get(segment.segmentId) ?? 0) !== segment.rowCount) {
9934
+ throw new Error(`Mutation segment key rows differ: ${segment.segmentId}`);
9935
+ }
9875
9936
  }
9876
9937
  }
9877
9938
  async #refinePhysicalOutputWindows(columns, estimatedOutputs, targetBlockBytes, outputCompression, memoryBudgetBytes, snapshot) {
@@ -9930,12 +9991,12 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
9930
9991
  const loaded = await this.#loadPhysicalCompactionRanges(column, output, outputCompression, memoryBudgetBytes, snapshot);
9931
9992
  return measurePhysicalColumnRanges(column.type, loaded.ranges);
9932
9993
  }
9933
- async #plannedPhysicalOutputStoredBytesUpperBound(plan, snapshot) {
9994
+ async #plannedPhysicalOutputStoredBytesUpperBound(plan, memoryBudgetBytes, snapshot) {
9934
9995
  const layout = physicalRewriteLayout(plan);
9935
9996
  let total = 0;
9936
9997
  for (const output of layout.outputs) {
9937
9998
  for (const column of layout.columns) {
9938
- const measurement = await this.#measurePhysicalCompactionOutput(column, output, plan.outputCompression, Number.MAX_SAFE_INTEGER, snapshot);
9999
+ const measurement = await this.#measurePhysicalCompactionOutput(column, output, plan.outputCompression, memoryBudgetBytes, snapshot);
9939
10000
  total = safeWholeNumberSum([
9940
10001
  total,
9941
10002
  maximumPhysicalBlockByteLength(measurement.encodedByteLength, measurement.metadata, plan.outputCompression)
@@ -10296,26 +10357,84 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
10296
10357
  throw new CompactionMemoryBudgetError(memoryBudgetBytes, memoryBound);
10297
10358
  }
10298
10359
  const outputEnd = safeWholeNumberSum([output.rowStart, output.rowCount], "Compaction output row range");
10299
- const ranges = [];
10300
- for (const sourceBlock of overlappingPhysicalSourceRanges(column, output)) {
10301
- if (snapshot !== void 0)
10302
- await this.#renewInternalLeaseIfNeeded(snapshot);
10303
- const bytes = await this.store.getBlock(sourceBlock.blockId);
10304
- if (bytes === void 0) {
10305
- throw new Error(`A compaction source block is missing: ${sourceBlock.blockId}`);
10306
- }
10360
+ const sourceBlocks = overlappingPhysicalSourceRanges(column, output);
10361
+ const distinct = [];
10362
+ const seen = /* @__PURE__ */ new Set();
10363
+ for (const sourceBlock of sourceBlocks) {
10364
+ if (seen.has(sourceBlock.blockId))
10365
+ continue;
10366
+ seen.add(sourceBlock.blockId);
10367
+ distinct.push(sourceBlock);
10368
+ }
10369
+ const decoded = /* @__PURE__ */ new Map();
10370
+ const peakWorkingBytes = await this.#decodeCompactionBlocks(distinct, { limitBytes: memoryBudgetBytes, retainsDecoded: true }, snapshot, async (bytes, sourceBlock) => {
10307
10371
  const description = inspectBlock(bytes);
10308
10372
  if (bytes.byteLength !== sourceBlock.storedBytes || description.encodedLength !== sourceBlock.encodedBytes || description.checksum !== sourceBlock.checksum || description.rowCount !== sourceBlock.sourceBlockRowCount || description.type !== column.type) {
10309
10373
  throw new Error(`A compaction source block differs from its plan: ${sourceBlock.blockId}`);
10310
10374
  }
10311
- const decoded = await decodePhysicalBlock(bytes);
10375
+ return decodePhysicalBlock(bytes);
10376
+ }, (block, sourceBlock) => {
10377
+ decoded.set(sourceBlock.blockId, block);
10378
+ });
10379
+ const ranges = [];
10380
+ for (const sourceBlock of sourceBlocks) {
10381
+ const block = decoded.get(sourceBlock.blockId);
10382
+ if (block === void 0) {
10383
+ throw new Error(`A compaction source block is missing: ${sourceBlock.blockId}`);
10384
+ }
10312
10385
  const sourceEnd = safeWholeNumberSum([sourceBlock.outputRowStart, sourceBlock.rowCount], "Compaction source row range");
10313
10386
  const start = sourceBlock.sourceRowStart + Math.max(output.rowStart, sourceBlock.outputRowStart) - sourceBlock.outputRowStart;
10314
10387
  const end = sourceBlock.sourceRowStart + Math.min(outputEnd, sourceEnd) - sourceBlock.outputRowStart;
10315
- const slice = slicePhysicalColumn(decoded.column, start, end);
10388
+ const slice = slicePhysicalColumn(block.column, start, end);
10316
10389
  ranges.push({ column: slice, start: 0, end: slice.rowCount });
10317
10390
  }
10318
- return { ranges, peakWorkingBytes: memoryBound };
10391
+ return { ranges, peakWorkingBytes: Math.max(memoryBound, peakWorkingBytes) };
10392
+ }
10393
+ async #decodeCompactionBlocks(blocks, budget, snapshot, decode, consume) {
10394
+ let retainedBytes = 0;
10395
+ let peakBytes = 0;
10396
+ let start = 0;
10397
+ while (start < blocks.length) {
10398
+ let end = start;
10399
+ let transientBytes = 0;
10400
+ let storedBytes = 0;
10401
+ for (; end < blocks.length && end - start < MAX_STORAGE_BULK_READ_ITEMS; end += 1) {
10402
+ const block = blocks[end];
10403
+ if (block === void 0)
10404
+ break;
10405
+ const blockTransientBytes = safeWholeNumberSum([
10406
+ safeWholeNumberProduct(block.storedBytes, 2, "Compaction decode memory"),
10407
+ safeWholeNumberProduct(block.encodedBytes, 2, "Compaction decode memory")
10408
+ ], "Compaction decode memory");
10409
+ if (end > start && (transientBytes + blockTransientBytes > budget.limitBytes - retainedBytes || storedBytes + block.storedBytes > MAX_BLOCK_READ_BATCH_BYTES)) {
10410
+ break;
10411
+ }
10412
+ transientBytes += blockTransientBytes;
10413
+ storedBytes += block.storedBytes;
10414
+ }
10415
+ const group = blocks.slice(start, end);
10416
+ if (snapshot !== void 0)
10417
+ await this.#renewInternalLeaseIfNeeded(snapshot);
10418
+ const bytes = await this.#readBlockWindow(group.map((block) => block.blockId));
10419
+ const decoded = await Promise.all(group.map((block, index) => {
10420
+ const stored = bytes[index];
10421
+ if (stored === void 0) {
10422
+ throw new Error(`A compaction source block is missing: ${block.blockId}`);
10423
+ }
10424
+ return decode(stored, block);
10425
+ }));
10426
+ peakBytes = Math.max(peakBytes, retainedBytes + transientBytes);
10427
+ for (const [index, block] of group.entries()) {
10428
+ const value = decoded[index];
10429
+ if (value === void 0)
10430
+ throw new Error("Compaction decode lost a block");
10431
+ consume(value, block);
10432
+ if (budget.retainsDecoded)
10433
+ retainedBytes += block.encodedBytes;
10434
+ }
10435
+ start = end;
10436
+ }
10437
+ return peakBytes;
10319
10438
  }
10320
10439
  async #loadCompactionSources(job) {
10321
10440
  const segments = await Promise.all(job.sourceSegmentIds.map((id) => this.store.getSegment(id)));
@@ -14152,6 +14271,49 @@ function normalizeColumnLogicalValue(column, value) {
14152
14271
  }
14153
14272
  return value;
14154
14273
  }
14274
+ function mergeAscendingIndexes(left, right) {
14275
+ const merged = [];
14276
+ let leftIndex = 0;
14277
+ let rightIndex = 0;
14278
+ while (leftIndex < left.length || rightIndex < right.length) {
14279
+ const a = left[leftIndex];
14280
+ const b = right[rightIndex];
14281
+ if (b === void 0 || a !== void 0 && a <= b) {
14282
+ if (a !== void 0)
14283
+ merged.push(a);
14284
+ leftIndex += 1;
14285
+ } else {
14286
+ merged.push(b);
14287
+ rightIndex += 1;
14288
+ }
14289
+ }
14290
+ return merged;
14291
+ }
14292
+ function applyUpsertConflictFilter(batch, rowCount, classified) {
14293
+ const acceptedRowIndexes = mergeAscendingIndexes(classified.inserts, classified.updates);
14294
+ const skippedRowCount = classified.skipped.length;
14295
+ if (acceptedRowIndexes.length === rowCount) {
14296
+ return { batch, rowCount, skippedRowCount, acceptedRowIndexes, firings: classified };
14297
+ }
14298
+ const acceptedPosition = new Map(acceptedRowIndexes.map((original, position) => [original, position]));
14299
+ return {
14300
+ batch: selectBatchRows(batch, acceptedRowIndexes),
14301
+ rowCount: acceptedRowIndexes.length,
14302
+ skippedRowCount,
14303
+ acceptedRowIndexes,
14304
+ firings: {
14305
+ inserts: classified.inserts.map((index) => acceptedPosition.get(index) ?? -1),
14306
+ updates: classified.updates.map((index) => acceptedPosition.get(index) ?? -1),
14307
+ skipped: [],
14308
+ oldImages: acceptedRowIndexes.map((index) => classified.oldImages[index])
14309
+ }
14310
+ };
14311
+ }
14312
+ function remapGeneratedColumns(generated, acceptedRowIndexes) {
14313
+ for (const [name, values] of generated) {
14314
+ generated.set(name, acceptedRowIndexes.map((index) => values[index] ?? null));
14315
+ }
14316
+ }
14155
14317
  function selectBatchRows(input, indexes) {
14156
14318
  return {
14157
14319
  columns: Object.fromEntries(Object.entries(input.columns).map(([name, values]) => [
@@ -14431,6 +14593,14 @@ function autoCompactionHint(version, segments) {
14431
14593
  deltas
14432
14594
  };
14433
14595
  }
14596
+ function countLevelZeroSegments(segments) {
14597
+ let levelZero = 0;
14598
+ for (const segment of segments) {
14599
+ if (segment.level === 0)
14600
+ levelZero += 1;
14601
+ }
14602
+ return levelZero;
14603
+ }
14434
14604
  function autoCompactionDueHint(hint) {
14435
14605
  return hint.levelZero >= AUTO_COMPACT_SCAN_SEGMENTS || hint.deltas >= AUTO_COMPACT_DELTA_SEGMENTS;
14436
14606
  }
@@ -15881,6 +16051,7 @@ function sourceOrderTuple(segment, transactions, label) {
15881
16051
  };
15882
16052
  }
15883
16053
  const MERGE_PLANNER_KEY_BYTES = 96;
16054
+ const MERGE_PLANNER_DECODED_KEY_BLOCK_FACTOR = 4;
15884
16055
  function planLinearOutputPartitions(totalRows, partitionRows, firstOrder, nextOrder) {
15885
16056
  const count = Math.max(1, Math.ceil(totalRows / partitionRows));
15886
16057
  const orders = fractionalLogicalOrders(firstOrder, nextOrder, count);
@@ -15996,7 +16167,6 @@ function mergePlannerMemoryBound(table, segments, keyColumnId) {
15996
16167
  const PATCH_ROW_BYTES = 64;
15997
16168
  const PATCH_CELL_BYTES = 48;
15998
16169
  const RANGE_BYTES = 80;
15999
- const DECODED_KEY_BLOCK_FACTOR = 4;
16000
16170
  let slotRows = 0;
16001
16171
  let deltaKeys = 0;
16002
16172
  let patchRows = 0;
@@ -16027,7 +16197,7 @@ function mergePlannerMemoryBound(table, segments, keyColumnId) {
16027
16197
  safeWholeNumberProduct(columns, PATCH_CELL_BYTES, "Mutation patch cells")
16028
16198
  ], "Mutation compaction patch row"), "Mutation compaction patches"),
16029
16199
  safeWholeNumberProduct(safeWholeNumberSum([sourceBlocks, safeWholeNumberProduct(patchRows, columns, "Mutation patched cells")], "Mutation compaction ranges"), RANGE_BYTES, "Mutation compaction range bytes"),
16030
- safeWholeNumberProduct(largestKeyBlockBytes, DECODED_KEY_BLOCK_FACTOR, "Mutation compaction decoded key block")
16200
+ safeWholeNumberProduct(largestKeyBlockBytes, MERGE_PLANNER_DECODED_KEY_BLOCK_FACTOR, "Mutation compaction decoded key block")
16031
16201
  ], "Mutation compaction planner memory");
16032
16202
  }
16033
16203
  function mergeSourceBearsRows(kind) {
@@ -554,10 +554,13 @@ class DatabaseRpcServer {
554
554
  return handle.session.execute(sql, params);
555
555
  }
556
556
  if (method === "stage") {
557
- const [op, tableName, input] = args;
557
+ const [op, tableName, input, options] = args;
558
558
  if (!isStageOp(op)) {
559
559
  throw new Error(`Unsupported write stage operation: ${String(op)}`);
560
560
  }
561
+ if (op === "upsertBatch") {
562
+ return handle.session.upsertBatch(tableName, input, options);
563
+ }
561
564
  return handle.session[op](tableName, input);
562
565
  }
563
566
  if (method === "commit")
@@ -3919,7 +3919,12 @@ class IndexedDbBlockStore {
3919
3919
  await assertActiveGarbageCollectionMarker(gcStore, current);
3920
3920
  }
3921
3921
  const updated = updateGarbageCollectionPlanningRecord(current, input);
3922
- await assertGarbageCollectionCandidateProvenanceInTransaction(transaction, updated);
3922
+ await assertGarbageCollectionCandidateProvenanceInTransaction(transaction, {
3923
+ candidateManifestVersions: input.candidateManifestVersions ?? [],
3924
+ candidateSegmentIds: input.candidateSegmentIds ?? [],
3925
+ candidateBlockIds: input.candidateBlockIds ?? [],
3926
+ candidateTransactionIds: input.candidateTransactionIds ?? []
3927
+ });
3923
3928
  gcStore.put(garbageCollectionJobEnvelope(updated), key);
3924
3929
  await transactionDone(transaction);
3925
3930
  return structuredClone(updated);
@@ -9717,15 +9722,15 @@ function assertGenericTransactionUpdateAllowed(record, update) {
9717
9722
  throw new TypeError("Only commitTransaction can set a committed transaction version");
9718
9723
  }
9719
9724
  }
9720
- async function assertGarbageCollectionCandidateProvenanceInTransaction(transaction, job) {
9725
+ async function assertGarbageCollectionCandidateProvenanceInTransaction(transaction, candidates) {
9721
9726
  const manifestStore = transaction.objectStore("manifests");
9722
- for (const version of job.candidateManifestVersions) {
9727
+ for (const version of candidates.candidateManifestVersions) {
9723
9728
  const value = await requestResult(manifestStore.get(version));
9724
9729
  if (value === void 0) {
9725
9730
  throw new Error(`Garbage collection candidate manifest is missing: ${String(version)}`);
9726
9731
  }
9727
9732
  }
9728
- for (const id of job.candidateTransactionIds) {
9733
+ for (const id of candidates.candidateTransactionIds) {
9729
9734
  const value = await requestResult(transaction.objectStore("transactions").get(id));
9730
9735
  const record = value === void 0 ? void 0 : asTransactionRecord(value);
9731
9736
  if (record === void 0 || record.status !== "aborted" && (record.status !== "committed" || record.committedVersion === null)) {
@@ -9733,10 +9738,10 @@ async function assertGarbageCollectionCandidateProvenanceInTransaction(transacti
9733
9738
  }
9734
9739
  }
9735
9740
  const manifestProvenBlockIds = /* @__PURE__ */ new Set();
9736
- const manifestValues = await Promise.all(job.candidateBlockIds.map((id) => requestResult(transaction.objectStore("catalog").get(manifestBlockKey(id)))));
9741
+ const manifestValues = await Promise.all(candidates.candidateBlockIds.map((id) => requestResult(transaction.objectStore("catalog").get(manifestBlockKey(id)))));
9737
9742
  for (const [index, value] of manifestValues.entries()) {
9738
9743
  if (value !== void 0) {
9739
- const id = job.candidateBlockIds[index] ?? "";
9744
+ const id = candidates.candidateBlockIds[index] ?? "";
9740
9745
  asManifestBlockRecord(value, id);
9741
9746
  manifestProvenBlockIds.add(id);
9742
9747
  }
@@ -9757,12 +9762,12 @@ async function assertGarbageCollectionCandidateProvenanceInTransaction(transacti
9757
9762
  return isTerminalCompactionJob(record) && (record.sourceBlockIds.includes(id) || record.outputBlockIds.includes(id));
9758
9763
  });
9759
9764
  };
9760
- for (const id of job.candidateBlockIds) {
9765
+ for (const id of candidates.candidateBlockIds) {
9761
9766
  if (await blockHasProvenance(id))
9762
9767
  continue;
9763
9768
  throw new Error(`Garbage collection block candidate has no persisted provenance: ${id}`);
9764
9769
  }
9765
- for (const id of job.candidateSegmentIds) {
9770
+ for (const id of candidates.candidateSegmentIds) {
9766
9771
  const segmentValue = await requestResult(transaction.objectStore("segments").get(id));
9767
9772
  if (segmentValue !== void 0)
9768
9773
  continue;
@@ -3567,7 +3567,12 @@ class RecordCore {
3567
3567
  throw new GarbageCollectionJobConflictError(input.jobId, input.expectedRevision, current?.revision ?? null);
3568
3568
  }
3569
3569
  const updated = updateGarbageCollectionPlanningRecord(current, input);
3570
- assertGarbageCollectionCandidateProvenance(updated, this.#manifests, this.#manifestBlocks, this.#segments, this.#transactions, this.#roots);
3570
+ assertGarbageCollectionCandidateProvenance({
3571
+ candidateManifestVersions: input.candidateManifestVersions ?? [],
3572
+ candidateSegmentIds: input.candidateSegmentIds ?? [],
3573
+ candidateBlockIds: input.candidateBlockIds ?? [],
3574
+ candidateTransactionIds: input.candidateTransactionIds ?? []
3575
+ }, this.#manifests, this.#manifestBlocks, this.#segments, this.#transactions, this.#roots);
3571
3576
  this.#garbageCollectionJobs.set(updated.id, updated);
3572
3577
  return cloneRecord(updated);
3573
3578
  }
@@ -5661,23 +5666,23 @@ function assertPendingArtifactsAvailable(transaction, physical, segments, valida
5661
5666
  }
5662
5667
  }
5663
5668
  }
5664
- function assertGarbageCollectionCandidateProvenance(job, manifests, manifestBlocks, segments, transactions, roots) {
5665
- for (const version of job.candidateManifestVersions) {
5669
+ function assertGarbageCollectionCandidateProvenance(candidates, manifests, manifestBlocks, segments, transactions, roots) {
5670
+ for (const version of candidates.candidateManifestVersions) {
5666
5671
  if (!manifests.has(version)) {
5667
5672
  throw new Error(`Garbage collection candidate manifest is missing: ${String(version)}`);
5668
5673
  }
5669
5674
  }
5670
- for (const id of job.candidateTransactionIds) {
5675
+ for (const id of candidates.candidateTransactionIds) {
5671
5676
  const transaction = transactions.get(id);
5672
5677
  if (transaction === void 0 || transaction.status !== "aborted" && (transaction.status !== "committed" || transaction.committedVersion === null)) {
5673
5678
  throw new Error(`Garbage collection transaction candidate is not terminal: ${id}`);
5674
5679
  }
5675
5680
  }
5676
- const unprovenBlockId = job.candidateBlockIds.find((id) => !manifestBlocks.has(id) && roots.abortedTransactionBlockCount(id) === 0 && roots.terminalJobBlockCount(id) === 0);
5681
+ const unprovenBlockId = candidates.candidateBlockIds.find((id) => !manifestBlocks.has(id) && roots.abortedTransactionBlockCount(id) === 0 && roots.terminalJobBlockCount(id) === 0);
5677
5682
  if (unprovenBlockId !== void 0) {
5678
5683
  throw new Error(`Garbage collection block candidate has no persisted provenance: ${unprovenBlockId}`);
5679
5684
  }
5680
- const unprovenSegmentId = job.candidateSegmentIds.find((id) => {
5685
+ const unprovenSegmentId = candidates.candidateSegmentIds.find((id) => {
5681
5686
  return !segments.has(id);
5682
5687
  });
5683
5688
  if (unprovenSegmentId !== void 0) {
@@ -815,6 +815,17 @@ export interface UpdateGarbageCollectionPlanningInput {
815
815
  discovery: GarbageCollectionDiscovery;
816
816
  updatedAt: string;
817
817
  }
818
+ /**
819
+ * One page's (or one job's full) proposed garbage-collection candidates, checked for persisted
820
+ * provenance before being accepted. Shared by every adapter's provenance assertion so a candidate
821
+ * kind added to one cannot be forgotten in another.
822
+ */
823
+ export interface GarbageCollectionCandidateSet {
824
+ readonly candidateManifestVersions: readonly number[];
825
+ readonly candidateSegmentIds: readonly string[];
826
+ readonly candidateBlockIds: readonly string[];
827
+ readonly candidateTransactionIds: readonly string[];
828
+ }
818
829
  export interface GarbageCollectionJobRecord {
819
830
  id: string;
820
831
  candidateManifestVersions: number[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minnowdb/core",
3
- "version": "0.7.5",
3
+ "version": "0.7.7",
4
4
  "description": "A columnar SQL database for the browser: PostgreSQL-style SQL over durable IndexedDB or OPFS data, with no server or WebAssembly module.",
5
5
  "license": "MIT",
6
6
  "author": "Eric Wilhite",