@minnowdb/core 0.10.0 → 0.10.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.
@@ -5226,6 +5226,7 @@ class MinnowDatabase {
5226
5226
  tables: /* @__PURE__ */ new Map(),
5227
5227
  mirrored: true,
5228
5228
  generation: 0,
5229
+ flushedWork: 0,
5229
5230
  flushing: Promise.resolve()
5230
5231
  });
5231
5232
  let closed = false;
@@ -5247,13 +5248,14 @@ class MinnowDatabase {
5247
5248
  });
5248
5249
  }
5249
5250
  };
5251
+ const ownStagedWork = () => transaction.stagedWorkCount - (this.#scopeWrites.get(transaction)?.flushedWork ?? 0);
5250
5252
  const guarded = async (run) => {
5251
- const before = transaction.stagedWorkCount;
5253
+ const before = ownStagedWork();
5252
5254
  const generation = this.#scopeWriteGeneration(transaction);
5253
5255
  try {
5254
5256
  return await run();
5255
5257
  } catch (error) {
5256
- if (transaction.stagedWorkCount !== before || this.#scopeWriteGeneration(transaction) !== generation) {
5258
+ if (ownStagedWork() !== before || this.#scopeWriteGeneration(transaction) !== generation) {
5257
5259
  poisoned ??= error;
5258
5260
  }
5259
5261
  throw error;
@@ -5262,7 +5264,7 @@ class MinnowDatabase {
5262
5264
  const operations = {
5263
5265
  query: async (sql, options2) => {
5264
5266
  open();
5265
- return this.#withReadReservation(() => this.#sessionQuery(transaction, sql, options2));
5267
+ return externalizeQueryResult(await this.#withReadReservation(() => this.#sessionQuery(transaction, sql, options2)));
5266
5268
  },
5267
5269
  execute: async (sql, params) => {
5268
5270
  open();
@@ -5270,7 +5272,7 @@ class MinnowDatabase {
5270
5272
  if (compiled.kind === "select") {
5271
5273
  return this.#withReadReservation(async () => ({
5272
5274
  kind: "rows",
5273
- result: await this.#sessionQuery(transaction, compiled.sql, params === void 0 ? {} : { params })
5275
+ result: externalizeQueryResult(await this.#sessionQuery(transaction, compiled.sql, params === void 0 ? {} : { params }))
5274
5276
  }));
5275
5277
  }
5276
5278
  const statement = bindStatementParameters(compiled, params);
@@ -5281,14 +5283,14 @@ class MinnowDatabase {
5281
5283
  throw new TypeError(`${statement.kind.toUpperCase().replace("-", " ")} is not allowed inside a write scope`);
5282
5284
  }
5283
5285
  if (statement.kind === "update" || statement.kind === "delete") {
5284
- const keyed = await this.#scopeKeyedMutation(statement);
5286
+ const keyed = await this.#scopeKeyedMutation(transaction, statement);
5285
5287
  if (keyed !== void 0) {
5286
5288
  return guarded(() => this.#runScopeKeyedMutation(transaction, keyed, () => {
5287
5289
  staged += 1;
5288
5290
  }));
5289
5291
  }
5290
5292
  }
5291
- return writer.executeStatement(statement);
5293
+ return externalizeExecuteResult(await writer.executeStatement(statement));
5292
5294
  },
5293
5295
  insertBatch: async (tableName, input) => {
5294
5296
  open();
@@ -5455,7 +5457,7 @@ class MinnowDatabase {
5455
5457
  let point;
5456
5458
  try {
5457
5459
  pointReadTestHooks.attempted += 1;
5458
- const table = await this.#findTable(shape.table);
5460
+ const table = await this.#findTable(shape.table, transaction);
5459
5461
  point = await this.#withSessionVisibility(transaction, [table], options, (snapshot, visibility, realTables) => this.#pointReadAtSnapshot(shape, snapshot, realTables, visibility, options));
5460
5462
  } catch (error) {
5461
5463
  if (!(error instanceof UnknownTableError))
@@ -5478,7 +5480,7 @@ class MinnowDatabase {
5478
5480
  options = this.#effectiveQueryOptions(options);
5479
5481
  throwIfAborted(options.signal);
5480
5482
  const names = collectRealTableNames(plan);
5481
- const tables = await Promise.all(names.map((name) => this.#findTable(name)));
5483
+ const tables = await Promise.all(names.map((name) => this.#findTable(name, transaction)));
5482
5484
  throwIfAborted(options.signal);
5483
5485
  return this.#withSessionVisibility(transaction, tables, options, (snapshot, visibility, realTables) => this.#queryAtVisibility(plan, snapshot, visibility, realTables, true, options));
5484
5486
  }
@@ -5775,6 +5777,7 @@ class MinnowDatabase {
5775
5777
  await this.#settleScopeWriteSet(transaction, set);
5776
5778
  }
5777
5779
  async #settleScopeWriteSet(transaction, set) {
5780
+ await transaction.renewIfDue();
5778
5781
  if (set.pendingRows >= this.#rowsPerBlock) {
5779
5782
  await this.#flushScopeWriteSets(transaction, [set.table.id]);
5780
5783
  return;
@@ -5802,11 +5805,14 @@ class MinnowDatabase {
5802
5805
  const set = state.tables.get(tableId);
5803
5806
  if (set === void 0 || set.pendingRows === 0)
5804
5807
  continue;
5808
+ const before = transaction.stagedWorkCount;
5805
5809
  try {
5806
5810
  await this.#stageScopeWriteSet(transaction, state, set);
5807
5811
  } catch (error) {
5808
5812
  state.failure ??= error;
5809
5813
  throw error;
5814
+ } finally {
5815
+ state.flushedWork += transaction.stagedWorkCount - before;
5810
5816
  }
5811
5817
  }
5812
5818
  if (!state.mirrored)
@@ -5919,6 +5925,7 @@ class MinnowDatabase {
5919
5925
  tables: /* @__PURE__ */ new Map(),
5920
5926
  mirrored: false,
5921
5927
  generation,
5928
+ flushedWork: 0,
5922
5929
  flushing: Promise.resolve()
5923
5930
  });
5924
5931
  this.#stagedKeyOverlays.delete(transaction);
@@ -5960,8 +5967,33 @@ class MinnowDatabase {
5960
5967
  answered.set(token, row);
5961
5968
  }
5962
5969
  if (committed.length > 0 && transaction.snapshotVersion !== null) {
5963
- for (let start = 0; start < committed.length; start += SCOPE_KEY_LOOKUP_WINDOW) {
5964
- const window = committed.slice(start, start + SCOPE_KEY_LOOKUP_WINDOW);
5970
+ const pointRows = await this.#scopeCommittedRowsByKey(transaction, table, keyColumn, committed, names);
5971
+ const windows = [];
5972
+ if (pointRows === void 0) {
5973
+ for (let start = 0; start < committed.length; start += SCOPE_KEY_LOOKUP_WINDOW) {
5974
+ windows.push(committed.slice(start, start + SCOPE_KEY_LOOKUP_WINDOW));
5975
+ }
5976
+ }
5977
+ const patched = (rows) => {
5978
+ for (const row of rows) {
5979
+ const key = row[keyColumn.name] ?? null;
5980
+ if (key === null)
5981
+ continue;
5982
+ const token = keyToken(keyColumn.type, key);
5983
+ const patch = patches.get(token);
5984
+ if (patch !== void 0) {
5985
+ table.columns.forEach((column, position) => {
5986
+ const value = patch.values[position];
5987
+ if (value !== void 0 && wanted.has(column.name))
5988
+ row[column.name] = value;
5989
+ });
5990
+ }
5991
+ answered.set(token, row);
5992
+ }
5993
+ };
5994
+ if (pointRows !== void 0)
5995
+ patched(pointRows);
5996
+ for (const window of windows) {
5965
5997
  const plan = {
5966
5998
  sql: "(scope keyed lookup)",
5967
5999
  base: { table: table.name, alias: table.name },
@@ -5985,25 +6017,39 @@ class MinnowDatabase {
5985
6017
  version: transaction.snapshotVersion,
5986
6018
  memoize: false
5987
6019
  });
5988
- for (const row of result.rows) {
5989
- const key = row[keyColumn.name] ?? null;
5990
- if (key === null)
5991
- continue;
5992
- const token = keyToken(keyColumn.type, key);
5993
- const patch = patches.get(token);
5994
- if (patch !== void 0) {
5995
- table.columns.forEach((column, position) => {
5996
- const value = patch.values[position];
5997
- if (value !== void 0 && wanted.has(column.name))
5998
- row[column.name] = value;
5999
- });
6000
- }
6001
- answered.set(token, row);
6002
- }
6020
+ patched(result.rows);
6003
6021
  }
6004
6022
  }
6005
6023
  return [...answered.values()];
6006
6024
  }
6025
+ async #scopeCommittedRowsByKey(transaction, table, keyColumn, keys, names) {
6026
+ if (pointReadTestHooks.disabled)
6027
+ return void 0;
6028
+ const values = [];
6029
+ for (const key of keys) {
6030
+ if (typeof key !== "number" && typeof key !== "string" && typeof key !== "boolean" && !(key instanceof Date)) {
6031
+ return void 0;
6032
+ }
6033
+ values.push(key);
6034
+ }
6035
+ const { segments, records } = await this.#scopeCommittedSegments(transaction, table);
6036
+ const visibility = {
6037
+ transactions: new Map(records.map((record) => [record.id, record])),
6038
+ segmentsByTable: /* @__PURE__ */ new Map([[table.id, segments]])
6039
+ };
6040
+ const realTables = /* @__PURE__ */ new Map([[table.name, table]]);
6041
+ const select = names.map((name) => ({ column: name, alias: name }));
6042
+ return this.#withLeasedSnapshot(transaction.snapshotVersion, async (snapshot) => {
6043
+ const rows = [];
6044
+ for (const value of values) {
6045
+ const result = await this.#pointReadAtSnapshot({ table: table.name, equalities: [{ column: keyColumn.name, value }], select }, snapshot, realTables, visibility, {});
6046
+ if (result === void 0)
6047
+ return void 0;
6048
+ rows.push(...result.rows);
6049
+ }
6050
+ return rows;
6051
+ });
6052
+ }
6007
6053
  #scopeKeyLookup(transaction, table, keyColumn) {
6008
6054
  return async (keys, projection) => {
6009
6055
  const direct = await this.#scopeRowsByKey(transaction, table, keyColumn, keys, projection);
@@ -6021,11 +6067,11 @@ class MinnowDatabase {
6021
6067
  return rows;
6022
6068
  };
6023
6069
  }
6024
- async #scopeKeyedMutation(statement) {
6070
+ async #scopeKeyedMutation(transaction, statement) {
6025
6071
  if (statement.from !== void 0 || statement.returning !== void 0 || statement.returningItems !== void 0 || statement.predicates.length !== 1) {
6026
6072
  return void 0;
6027
6073
  }
6028
- const table = await this.#findTable(statement.table);
6074
+ const table = await this.#findTable(statement.table, transaction);
6029
6075
  const keyColumn = getUniqueKeyColumn(table);
6030
6076
  if (keyColumn === void 0 || keyColumn.hidden === true || keyColumn.sqlDomain !== void 0) {
6031
6077
  return void 0;
@@ -6084,6 +6130,88 @@ class MinnowDatabase {
6084
6130
  return void 0;
6085
6131
  return { kind: "update", table, keyColumn, keys, changes };
6086
6132
  }
6133
+ async #assertInsertKeysAbsent(transaction, table, batch, keys) {
6134
+ const inScope = this.#scopeWrites.has(transaction);
6135
+ const keyColumn = getUniqueKeyColumn(table);
6136
+ if (keys !== void 0 && keyColumn !== void 0) {
6137
+ const present = inScope ? await this.#scopeKeyPresence(transaction, table, keyColumn, [...keys.values()]) : this.#stagedKeyOverlay(transaction, table.id).added;
6138
+ for (const [token, value] of keys) {
6139
+ if (present.has(token)) {
6140
+ throw new UniqueConstraintError(table.name, publicKeyName(table, keyColumn), value);
6141
+ }
6142
+ }
6143
+ }
6144
+ if (!inScope)
6145
+ return;
6146
+ const rowCount = batch.rowCount ?? Object.values(batch.columns)[0]?.length ?? 0;
6147
+ for (const { indexId, index, columns } of readyUniqueSecondaryIndexes(table)) {
6148
+ const namespaceId = secondaryUniqueKeyNamespace(table.id, indexId);
6149
+ const terms = [];
6150
+ for (let row = 0; row < rowCount; row += 1) {
6151
+ const term = secondaryUniqueTerm(index, columns, columns.map((column) => batch.columns[column.name]?.[row] ?? null));
6152
+ if (term !== void 0)
6153
+ terms.push(term);
6154
+ }
6155
+ assertNoDuplicateUniqueTerms(index, terms);
6156
+ const overlay = this.#stagedKeyOverlay(transaction, namespaceId);
6157
+ const unresolved = [];
6158
+ for (const term of terms) {
6159
+ if (overlay.added.has(term)) {
6160
+ throw await this.#translateUniqueConflict(new UniqueKeyConflictError(namespaceId, term));
6161
+ }
6162
+ if (!overlay.removed.has(term))
6163
+ unresolved.push(term);
6164
+ }
6165
+ if (unresolved.length === 0)
6166
+ continue;
6167
+ const [existing] = await this.#existingUniqueKeysWindowed(namespaceId, unresolved);
6168
+ if (existing !== void 0) {
6169
+ throw await this.#translateUniqueConflict(new UniqueKeyConflictError(namespaceId, existing));
6170
+ }
6171
+ }
6172
+ }
6173
+ async #assertUpdateUniqueTermsAbsent(transaction, table, input, preImages) {
6174
+ for (const { indexId, index, columns } of readyUniqueSecondaryIndexes(table)) {
6175
+ if (!columns.some((column) => input.changes[column.name] !== void 0))
6176
+ continue;
6177
+ const namespaceId = secondaryUniqueKeyNamespace(table.id, indexId);
6178
+ const released = /* @__PURE__ */ new Set();
6179
+ const taken = [];
6180
+ preImages.forEach((old, row) => {
6181
+ if (old === void 0)
6182
+ return;
6183
+ const before = secondaryUniqueTerm(index, columns, columns.map((column) => old[column.name] ?? null));
6184
+ const after = secondaryUniqueTerm(index, columns, columns.map((column) => {
6185
+ const assigned = input.changes[column.name];
6186
+ return assigned === void 0 ? old[column.name] ?? null : assigned[row] ?? null;
6187
+ }));
6188
+ if (before === after)
6189
+ return;
6190
+ if (before !== void 0)
6191
+ released.add(before);
6192
+ if (after !== void 0)
6193
+ taken.push(after);
6194
+ });
6195
+ assertNoDuplicateUniqueTerms(index, taken);
6196
+ const overlay = this.#stagedKeyOverlay(transaction, namespaceId);
6197
+ const unresolved = [];
6198
+ for (const term of taken) {
6199
+ if (released.has(term))
6200
+ continue;
6201
+ if (overlay.added.has(term)) {
6202
+ throw await this.#translateUniqueConflict(new UniqueKeyConflictError(namespaceId, term));
6203
+ }
6204
+ if (!overlay.removed.has(term))
6205
+ unresolved.push(term);
6206
+ }
6207
+ if (unresolved.length === 0)
6208
+ continue;
6209
+ const [existing] = await this.#existingUniqueKeysWindowed(namespaceId, unresolved);
6210
+ if (existing !== void 0) {
6211
+ throw await this.#translateUniqueConflict(new UniqueKeyConflictError(namespaceId, existing));
6212
+ }
6213
+ }
6214
+ }
6087
6215
  async #scopeKeyPresence(transaction, table, keyColumn, keys) {
6088
6216
  const overlay = this.#stagedKeyOverlay(transaction, table.id);
6089
6217
  const present = /* @__PURE__ */ new Set();
@@ -6122,7 +6250,7 @@ class MinnowDatabase {
6122
6250
  return { kind: "delete", table: table.name, rowCount: deleted.rowCount };
6123
6251
  }
6124
6252
  const changes = Object.fromEntries(Object.entries(keyed.changes).map(([name, value]) => [name, present.map(() => value)]));
6125
- const updated = await this.#sessionUpdate(transaction, table.name, { keys: present, changes });
6253
+ const updated = await this.#sessionUpdate(transaction, table.name, { keys: present, changes }, 1, true);
6126
6254
  return { kind: "update", table: table.name, rowCount: updated.rowCount };
6127
6255
  }
6128
6256
  async #sessionInsert(transaction, tableName, input, kind, options, cascadeBudget = 1) {
@@ -6170,24 +6298,19 @@ class MinnowDatabase {
6170
6298
  }
6171
6299
  }
6172
6300
  await this.#assertCompactionCapacity(table, transaction);
6301
+ await this.#assertForeignKeysPresent(table, (column) => batch.columns[column] ?? [], (sql, params) => this.#sessionQuery(transaction, sql, { params }), transaction);
6173
6302
  const keys = batchKeys(table, batch);
6303
+ if (kind === "insert")
6304
+ await this.#assertInsertKeysAbsent(transaction, table, batch, keys);
6305
+ else
6306
+ assertBatchSecondaryTermsDistinct(table, batch);
6174
6307
  if (keys !== void 0) {
6175
- if (kind === "insert") {
6176
- const keyColumn = getUniqueKeyColumn(table);
6177
- const overlay = this.#stagedKeyOverlay(transaction, table.id);
6178
- for (const [token, value] of keys) {
6179
- if (overlay.added.has(token) && keyColumn !== void 0) {
6180
- throw new UniqueConstraintError(table.name, keyColumn.name, value);
6181
- }
6182
- }
6183
- }
6184
6308
  transaction.setUniqueKeyChanges({
6185
6309
  tableId: table.id,
6186
6310
  keyTokens: [...keys.keys()],
6187
6311
  requireAbsent: kind === "insert"
6188
6312
  });
6189
6313
  }
6190
- await this.#assertForeignKeysPresent(table, (column) => batch.columns[column] ?? [], (sql, params) => this.#sessionQuery(transaction, sql, { params }), transaction);
6191
6314
  const insertValueAt = (source, column, rowIndex) => source === "new" ? batch.columns[column]?.[rowIndex] ?? null : null;
6192
6315
  stageSecondaryUniqueInsertChanges(transaction, table, batch, kind === "upsert" ? sessionUpsertFirings?.oldImages : void 0);
6193
6316
  const buffered = this.#scopeWrites.has(transaction) && rowCount < SCOPE_DIRECT_STAGE_ROWS && !(table.triggers ?? []).some((trigger) => trigger.event === "insert" || trigger.event === "update");
@@ -6224,8 +6347,8 @@ class MinnowDatabase {
6224
6347
  ...generated.size === 0 ? {} : { generatedColumns: Object.fromEntries(generated) }
6225
6348
  };
6226
6349
  }
6227
- async #sessionUpdate(transaction, tableName, input, cascadeBudget = 1) {
6228
- const table = await this.#findTable(tableName);
6350
+ async #sessionUpdate(transaction, tableName, input, cascadeBudget = 1, keysVerified = false) {
6351
+ const table = await this.#findTable(tableName, transaction);
6229
6352
  await this.#assertCompactionCapacity(table, transaction);
6230
6353
  const keyColumn = getUniqueKeyColumn(table);
6231
6354
  if (keyColumn === void 0) {
@@ -6235,14 +6358,16 @@ class MinnowDatabase {
6235
6358
  rejectGeneratedUpdateAssignments(table, input);
6236
6359
  const keys = validateUpdateBatch(table, keyColumn, input);
6237
6360
  const overlay = this.#stagedKeyOverlay(transaction, table.id);
6238
- for (const [token, value] of keys) {
6239
- if (overlay.removed.has(token)) {
6240
- throw new MissingKeyError(table.name, keyColumn.name, value);
6361
+ if (!keysVerified) {
6362
+ for (const [token, value] of keys) {
6363
+ if (overlay.removed.has(token)) {
6364
+ throw new MissingKeyError(table.name, keyColumn.name, value);
6365
+ }
6366
+ }
6367
+ const committedKeys = new Map([...keys].filter(([token]) => !overlay.added.has(token)));
6368
+ if (committedKeys.size > 0) {
6369
+ await this.#assertKeysExist(table, keyColumn, transaction.snapshotVersion, committedKeys);
6241
6370
  }
6242
- }
6243
- const committedKeys = new Map([...keys].filter(([token]) => !overlay.added.has(token)));
6244
- if (committedKeys.size > 0) {
6245
- await this.#assertKeysExist(table, keyColumn, transaction.snapshotVersion, committedKeys);
6246
6371
  }
6247
6372
  const sessionChecks = table.checks ?? [];
6248
6373
  let changedForeignKey = (table.foreignKeys ?? []).some((key) => key.enforced !== false && foreignKeyColumns(key).some((column) => input.changes[column] !== void 0));
@@ -6250,6 +6375,9 @@ class MinnowDatabase {
6250
6375
  input = applyStoredGeneratedUpdateChanges(table, input, preImages);
6251
6376
  validateUpdateBatch(table, keyColumn, input);
6252
6377
  changedForeignKey = (table.foreignKeys ?? []).some((key) => key.enforced !== false && foreignKeyColumns(key).some((column) => input.changes[column] !== void 0));
6378
+ if (this.#scopeWrites.has(transaction)) {
6379
+ await this.#assertUpdateUniqueTermsAbsent(transaction, table, input, preImages);
6380
+ }
6253
6381
  stageSecondaryUniqueMutationChanges(transaction, table, input, preImages);
6254
6382
  const secondaryDeltas = buildSecondaryUpdateDeltas(table, input, preImages);
6255
6383
  if (secondaryDeltas.length > 0) {
@@ -6298,7 +6426,7 @@ class MinnowDatabase {
6298
6426
  };
6299
6427
  }
6300
6428
  async #sessionDelete(transaction, tableName, input, cascadeBudget = 1, referentialBudget = REFERENTIAL_CASCADES) {
6301
- const table = await this.#findTable(tableName);
6429
+ const table = await this.#findTable(tableName, transaction);
6302
6430
  await this.#assertCompactionCapacity(table, transaction);
6303
6431
  const keyColumn = getUniqueKeyColumn(table);
6304
6432
  if (keyColumn === void 0) {
@@ -8404,9 +8532,7 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
8404
8532
  return void 0;
8405
8533
  const visibleBaseSegments = visibleByTable.get(freshBaseTable.name) ?? await this.#visibleSegmentRecords(freshBaseTable, snapshot, visibility);
8406
8534
  this.#maybeScheduleAutoCompaction(freshBaseTable, visibleBaseSegments);
8407
- const ftsSegments = await this.#ftsPrunedSegments(freshBaseTable, visibleBaseSegments, plan, snapshot);
8408
- throwIfAborted(options.signal);
8409
- const indexed = await this.#secondaryIndexPrunedSegments(freshBaseTable, ftsSegments, plan, snapshot);
8535
+ const indexed = await this.#indexPrunedSegments(freshBaseTable, visibleBaseSegments, plan, snapshot, visibility);
8410
8536
  throwIfAborted(options.signal);
8411
8537
  const baseSegments = indexed.segments;
8412
8538
  const zonePruned = indexed.rows === void 0 ? await this.#zonePrunedStreamSegments(plan, freshBaseTable, projectedBaseColumns, baseSegments, snapshot) : void 0;
@@ -13323,6 +13449,19 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
13323
13449
  }
13324
13450
  return surviving;
13325
13451
  }
13452
+ async #indexPrunedSegments(table, segments, plan, snapshot, visibility) {
13453
+ const own = overlayOwnedSegments(segments, visibility);
13454
+ if (own.length === 0) {
13455
+ const ftsSegments2 = await this.#ftsPrunedSegments(table, segments, plan, snapshot);
13456
+ return this.#secondaryIndexPrunedSegments(table, ftsSegments2, plan, snapshot);
13457
+ }
13458
+ if (own.some((segment) => segment.kind !== "insert"))
13459
+ return { segments, pruned: false };
13460
+ const committed = segments.filter((segment) => segment.transactionId !== visibility?.overlayTransactionId);
13461
+ const ftsSegments = await this.#ftsPrunedSegments(table, committed, plan, snapshot);
13462
+ const indexed = await this.#secondaryIndexPrunedSegments(table, ftsSegments, plan, snapshot);
13463
+ return { segments: [...indexed.segments, ...own], pruned: indexed.pruned };
13464
+ }
13326
13465
  async #secondaryIndexPrunedSegments(table, segments, plan, snapshot) {
13327
13466
  const predicates = secondaryIndexPredicates(plan, table);
13328
13467
  if (predicates.length === 0) {
@@ -13598,13 +13737,12 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
13598
13737
  }
13599
13738
  async #materializeColumnarTableAtSnapshot(table, snapshot, projectedColumns, visibility, plan) {
13600
13739
  const visibleSegments = await this.#visibleSegmentRecords(table, snapshot, visibility);
13601
- if (plan !== void 0) {
13740
+ if (plan !== void 0 && overlayOwnedSegments(visibleSegments, visibility).length === 0) {
13602
13741
  const covering = await this.#secondaryIndexCoveringTable(table, projectedColumns, visibleSegments, plan, snapshot);
13603
13742
  if (covering !== void 0)
13604
13743
  return covering;
13605
13744
  }
13606
- const ftsSegments = plan === void 0 ? visibleSegments : await this.#ftsPrunedSegments(table, visibleSegments, plan, snapshot);
13607
- const indexed = plan === void 0 ? { segments: ftsSegments, pruned: false } : await this.#secondaryIndexPrunedSegments(table, ftsSegments, plan, snapshot);
13745
+ const indexed = plan === void 0 ? { segments: visibleSegments, pruned: false } : await this.#indexPrunedSegments(table, visibleSegments, plan, snapshot, visibility);
13608
13746
  const segments = indexed.segments;
13609
13747
  const keyColumn = getUniqueKeyColumn(table);
13610
13748
  if (segments.every((segment) => {
@@ -14614,13 +14752,23 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
14614
14752
  this.#gzipVerdicts.delete(columnId);
14615
14753
  return bytes;
14616
14754
  }
14617
- async #findTable(name) {
14755
+ #scopeTableRecords = /* @__PURE__ */ new WeakMap();
14756
+ async #findTable(name, scope) {
14618
14757
  validateName(name, "Table");
14758
+ const cache = scope === void 0 || !this.#scopeWrites.has(scope) ? void 0 : this.#scopeTableRecords.get(scope) ?? (() => {
14759
+ const created = /* @__PURE__ */ new Map();
14760
+ this.#scopeTableRecords.set(scope, created);
14761
+ return created;
14762
+ })();
14763
+ const cached = cache?.get(name);
14764
+ if (cached !== void 0)
14765
+ return cached;
14619
14766
  const table = await this.store.getTableByName(name) ?? await this.store.getTableByName(await this.#foldTableName(name));
14620
14767
  if (table === void 0)
14621
14768
  throw new UnknownTableError(name);
14622
14769
  if (table.view !== void 0)
14623
14770
  throw new TypeError(`${name} is a view, not a table`);
14771
+ cache?.set(name, table);
14624
14772
  return table;
14625
14773
  }
14626
14774
  async #assertVisibleSegmentCursorTable(tableName, capturedTableId) {
@@ -15707,6 +15855,18 @@ function assertNoDuplicateUniqueTerms(index, terms) {
15707
15855
  seen.add(term);
15708
15856
  }
15709
15857
  }
15858
+ function assertBatchSecondaryTermsDistinct(table, input) {
15859
+ const rowCount = input.rowCount ?? Object.values(input.columns)[0]?.length ?? 0;
15860
+ for (const { index, columns } of readyUniqueSecondaryIndexes(table)) {
15861
+ const terms = [];
15862
+ for (let row = 0; row < rowCount; row += 1) {
15863
+ const term = secondaryUniqueTerm(index, columns, columns.map((column) => input.columns[column.name]?.[row] ?? null));
15864
+ if (term !== void 0)
15865
+ terms.push(term);
15866
+ }
15867
+ assertNoDuplicateUniqueTerms(index, terms);
15868
+ }
15869
+ }
15710
15870
  function stageSecondaryUniqueInsertChanges(transaction, table, input, oldImages) {
15711
15871
  const rowCount = input.rowCount ?? Object.values(input.columns)[0]?.length ?? 0;
15712
15872
  for (const { indexId, index, columns } of readyUniqueSecondaryIndexes(table)) {
@@ -15759,7 +15919,10 @@ function stageSecondaryUniqueMutationChanges(transaction, table, input, oldImage
15759
15919
  const added = oldImages.flatMap((old, row) => {
15760
15920
  if (old === void 0)
15761
15921
  return [];
15762
- const term = secondaryUniqueTerm(index, columns, columns.map((column) => input.changes[column.name]?.[row] ?? old[column.name] ?? null));
15922
+ const term = secondaryUniqueTerm(index, columns, columns.map((column) => {
15923
+ const assigned = input.changes[column.name];
15924
+ return assigned === void 0 ? old[column.name] ?? null : assigned[row] ?? null;
15925
+ }));
15763
15926
  return term === void 0 ? [] : [term];
15764
15927
  });
15765
15928
  assertNoDuplicateUniqueTerms(index, added);
@@ -18525,6 +18688,10 @@ function garbageCollectionProgress(job) {
18525
18688
  function firesAfterTriggers(table, ...events) {
18526
18689
  return (table.triggers ?? []).some((trigger) => trigger.timing === "after" && events.includes(trigger.event));
18527
18690
  }
18691
+ function overlayOwnedSegments(segments, visibility) {
18692
+ const id = visibility?.overlayTransactionId;
18693
+ return id === void 0 ? [] : segments.filter((segment) => segment.transactionId === id);
18694
+ }
18528
18695
  function collectRealTableNames(plan) {
18529
18696
  const names = /* @__PURE__ */ new Set();
18530
18697
  const excluded = /* @__PURE__ */ new Set();
@@ -14,6 +14,8 @@ async function createStore(descriptor, options) {
14
14
  ...descriptor.opfs?.durability === void 0 ? {} : { durability: descriptor.opfs.durability }
15
15
  } : { kind, name: descriptor.name, ...descriptor.indexeddb }, options);
16
16
  return opened;
17
+ }, {
18
+ opfsDatabaseExists: async (name) => (await import("../storage/opfs/index.js")).opfsDatabaseExists({ name })
17
19
  });
18
20
  }
19
21
  if (descriptor.kind === "memory") {
@@ -538,7 +538,7 @@ class DatabaseRpcServer {
538
538
  if (handle === void 0)
539
539
  throw new Error(`Unknown handle: ${handleId}`);
540
540
  if (handle.type === "write") {
541
- this.#beginWriteHandleCall(handle);
541
+ await this.#beginWriteHandleCall(handle);
542
542
  try {
543
543
  return await this.#callWriteHandle(handleId, handle, method, args, context);
544
544
  } finally {
@@ -847,12 +847,11 @@ class DatabaseRpcServer {
847
847
  #releaseHandleId(id) {
848
848
  this.#reservedHandleIds.delete(id);
849
849
  }
850
- #beginWriteHandleCall(handle) {
850
+ async #beginWriteHandleCall(handle) {
851
+ while (handle.activeCalls !== 0)
852
+ await handle.activeCallDone;
851
853
  if (!handle.open)
852
854
  throw new Error("Write handle is closed");
853
- if (handle.activeCalls !== 0) {
854
- throw new Error("Write handle already has a call in flight");
855
- }
856
855
  handle.activeCalls = 1;
857
856
  handle.activeCallDone = new Promise((resolve) => {
858
857
  handle.finishActiveCall = resolve;
@@ -1,5 +1,5 @@
1
1
  import { IndexedDbBlockStore } from "../storage/indexeddb.js";
2
- import { OpfsBlockStore } from "../storage/opfs/index.js";
2
+ import { OpfsBlockStore, opfsDatabaseExists } from "../storage/opfs/index.js";
3
3
  import { openAutoStore } from "./auto-store.js";
4
4
  import { unsupportedStoreKindError } from "./worker-server.js";
5
5
  const autoWorkerStore = async (descriptor, options) => {
@@ -26,7 +26,7 @@ const autoWorkerStore = async (descriptor, options) => {
26
26
  name: descriptor.name,
27
27
  ...descriptor.indexeddb?.durability === void 0 ? {} : { durability: descriptor.indexeddb.durability },
28
28
  ...descriptor.indexeddb?.uniqueKeyCacheBytes === void 0 ? {} : { uniqueKeyCacheBytes: descriptor.indexeddb.uniqueKeyCacheBytes }
29
- }));
29
+ }), { opfsDatabaseExists: (name) => opfsDatabaseExists({ name }) });
30
30
  default:
31
31
  throw unsupportedStoreKindError("auto", descriptor.kind);
32
32
  }
@@ -1,6 +1,11 @@
1
1
  const anonymous = /* @__PURE__ */ new WeakMap();
2
2
  const named = /* @__PURE__ */ new Map();
3
+ const bypassing = /* @__PURE__ */ new Set();
4
+ const NOT_GRANTED = /* @__PURE__ */ Symbol("write admission lock not granted");
3
5
  const WRITE_ADMISSION_WAIT_MS = 1e4;
6
+ function _resetWriteAdmissionForTests() {
7
+ bypassing.clear();
8
+ }
4
9
  async function coordinateWrite(store, run, signal, options = {}) {
5
10
  signal.throwIfAborted();
6
11
  const name = store.liveQueryChannelName;
@@ -25,6 +30,19 @@ async function coordinateWrite(store, run, signal, options = {}) {
25
30
  signal.throwIfAborted();
26
31
  if (name === void 0 || locks === void 0)
27
32
  return enter();
33
+ const lockName = `minnowdb-write:${name}`;
34
+ if (bypassing.has(name)) {
35
+ const result = await locks.request(lockName, { ifAvailable: true }, async (lock) => {
36
+ if (lock === null)
37
+ return NOT_GRANTED;
38
+ bypassing.delete(name);
39
+ return enter();
40
+ });
41
+ if (result !== NOT_GRANTED)
42
+ return result;
43
+ signal.throwIfAborted();
44
+ return await enter();
45
+ }
28
46
  const startedAt = Date.now();
29
47
  const wait = { ranOut: false };
30
48
  const waitTimer = setTimeout(() => {
@@ -33,11 +51,12 @@ async function coordinateWrite(store, run, signal, options = {}) {
33
51
  }, admissionWaitMs);
34
52
  waitTimer.unref?.();
35
53
  try {
36
- return await locks.request(`minnowdb-write:${name}`, { signal: lockController.signal }, enter);
54
+ return await locks.request(lockName, { signal: lockController.signal }, enter);
37
55
  } catch (error) {
38
56
  if (!wait.ranOut || admitted)
39
57
  throw error;
40
58
  signal.throwIfAborted();
59
+ bypassing.add(name);
41
60
  options.onAdmissionWaitExceeded?.(Date.now() - startedAt);
42
61
  return await enter();
43
62
  } finally {
@@ -72,5 +91,6 @@ async function coordinateWrite(store, run, signal, options = {}) {
72
91
  }
73
92
  export {
74
93
  WRITE_ADMISSION_WAIT_MS,
94
+ _resetWriteAdmissionForTests,
75
95
  coordinateWrite
76
96
  };