@minnowdb/core 0.9.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/engine/auto-store.d.ts +40 -0
  2. package/dist/engine/auto-store.js +115 -0
  3. package/dist/engine/buffered-writer.d.ts +2 -0
  4. package/dist/engine/buffered-writer.js +15 -2
  5. package/dist/engine/client.d.ts +55 -6
  6. package/dist/engine/client.js +155 -40
  7. package/dist/engine/database.d.ts +15 -1
  8. package/dist/engine/database.js +1085 -227
  9. package/dist/engine/errors.d.ts +61 -2
  10. package/dist/engine/errors.js +116 -3
  11. package/dist/engine/index.d.ts +1 -0
  12. package/dist/engine/index.js +2 -0
  13. package/dist/engine/live.d.ts +24 -1
  14. package/dist/engine/live.js +33 -9
  15. package/dist/engine/scope-write-set.js +36 -0
  16. package/dist/engine/worker-auto.d.ts +1 -0
  17. package/dist/engine/worker-auto.js +3 -0
  18. package/dist/engine/worker-host.d.ts +2 -1
  19. package/dist/engine/worker-host.js +17 -1
  20. package/dist/engine/worker-server.d.ts +53 -1
  21. package/dist/engine/worker-server.js +118 -14
  22. package/dist/engine/worker-store-auto.js +36 -0
  23. package/dist/engine/worker-store-opfs.js +3 -2
  24. package/dist/engine/write-coordinator.js +24 -2
  25. package/dist/storage/indexeddb.js +494 -207
  26. package/dist/storage/opfs/leader.js +201 -14
  27. package/dist/storage/opfs/rpc.js +23 -43
  28. package/dist/storage/opfs/store.d.ts +20 -0
  29. package/dist/storage/opfs/store.js +479 -60
  30. package/dist/storage/toolkit/record-core.js +67 -38
  31. package/dist/storage/toolkit/wire.d.ts +1 -1
  32. package/dist/storage/toolkit/wire.js +1 -1
  33. package/dist/storage/types.d.ts +29 -8
  34. package/dist/storage/types.js +27 -16
  35. package/dist/testing/opfs-shim.js +14 -6
  36. package/dist/transactions/index.d.ts +12 -0
  37. package/dist/transactions/index.js +83 -23
  38. package/dist/worker-protocol/index.d.ts +50 -2
  39. package/dist/worker-protocol/index.js +106 -4
  40. package/package.json +7 -2
@@ -7,6 +7,7 @@ import { crossJoinPlan } from "../plan/model.js";
7
7
  import { definedVectors, toColumnarBatch } from "./batch.js";
8
8
  import { ArtifactCache } from "./artifact-cache.js";
9
9
  import { estimateBatchBytes, estimateRowBytes, estimateValuesBytes } from "./byte-estimates.js";
10
+ import { composeScopeEffect, netScopeEffect, scopeWriteSetTestHooks } from "./scope-write-set.js";
10
11
  import { throwIfAborted } from "./cancellation.js";
11
12
  import { BufferedTableWriter } from "./buffered-writer.js";
12
13
  import { attachLifecycleFlush, BufferedTableWriter as BufferedTableWriter2, MAX_BUFFERED_WRITER_PENDING_ADDS } from "./buffered-writer.js";
@@ -191,6 +192,8 @@ function migrationResult(steps) {
191
192
  function compactUpdateBatchInput(input) {
192
193
  return Object.values(input.changes).includes(void 0) ? { keys: input.keys, changes: definedVectors(input.changes) } : input;
193
194
  }
195
+ const SCOPE_KEY_LOOKUP_WINDOW = Math.min(MAX_SQL_PARAMETERS, 1024);
196
+ const SCOPE_DIRECT_STAGE_ROWS = 4096;
194
197
  const MAX_GARBAGE_COLLECTION_RETAIN_RECENT_VERSIONS = 1024;
195
198
  const REFERENTIAL_CASCADES = 8;
196
199
  class TransactionRollback extends Error {
@@ -586,6 +589,7 @@ class MinnowDatabase {
586
589
  #spillOwnerLeaseMs;
587
590
  #transactionOwnerLeaseMs;
588
591
  #createId;
592
+ #onBackgroundError;
589
593
  #internalLeaseOwnerId;
590
594
  #liveSets = /* @__PURE__ */ new Set();
591
595
  #liveProofContexts = /* @__PURE__ */ new Map();
@@ -664,6 +668,7 @@ class MinnowDatabase {
664
668
  }
665
669
  this.#now = options.now ?? (() => /* @__PURE__ */ new Date());
666
670
  this.#createId = options.createId ?? (() => crypto.randomUUID());
671
+ this.#onBackgroundError = options.onBackgroundError;
667
672
  this.#internalLeaseOwnerId = `minnow/${this.#createId()}`;
668
673
  this.#spillOwnerLeaseMs = options.spillOwnerLeaseMs ?? 6e4;
669
674
  if (!Number.isSafeInteger(this.#spillOwnerLeaseMs) || this.#spillOwnerLeaseMs <= 0 || this.#spillOwnerLeaseMs > MAX_TEMP_OWNER_TTL_MS) {
@@ -1906,7 +1911,7 @@ class MinnowDatabase {
1906
1911
  keyColumnId: keyColumn.id,
1907
1912
  level: 0,
1908
1913
  logicalOrder: 0,
1909
- commitOrdinal: transaction.pendingSegmentIds.length,
1914
+ commitOrdinal: transaction.pendingSegmentCount,
1910
1915
  rowIdSpans: [],
1911
1916
  createdAt: dateIsoString(this.#now())
1912
1917
  };
@@ -1961,7 +1966,10 @@ class MinnowDatabase {
1961
1966
  }
1962
1967
  }
1963
1968
  bufferedWriter(tableName, options = {}) {
1964
- return new BufferedTableWriter(this.#erased, tableName, options);
1969
+ return new BufferedTableWriter(this.#erased, tableName, {
1970
+ ...options,
1971
+ onBackgroundError: (error, context) => this.#reportBackgroundError(error, context)
1972
+ });
1965
1973
  }
1966
1974
  async #writeUpdateBatch(table, keyColumn, input, keys) {
1967
1975
  await this.#assertCompactionCapacity(table);
@@ -2059,7 +2067,7 @@ class MinnowDatabase {
2059
2067
  keyColumnId: keyColumn.id,
2060
2068
  level: 0,
2061
2069
  logicalOrder: 0,
2062
- commitOrdinal: transaction.pendingSegmentIds.length,
2070
+ commitOrdinal: transaction.pendingSegmentCount,
2063
2071
  rowIdSpans: [],
2064
2072
  createdAt: dateIsoString(this.#now())
2065
2073
  };
@@ -2313,7 +2321,7 @@ class MinnowDatabase {
2313
2321
  ...table.uniqueKeyColumnId === void 0 ? {} : { keyColumnId: table.uniqueKeyColumnId },
2314
2322
  level: 0,
2315
2323
  logicalOrder: 0,
2316
- commitOrdinal: transaction.pendingSegmentIds.length,
2324
+ commitOrdinal: transaction.pendingSegmentCount,
2317
2325
  rowIdSpans: [],
2318
2326
  createdAt: dateIsoString(this.#now())
2319
2327
  };
@@ -3497,13 +3505,9 @@ class MinnowDatabase {
3497
3505
  const kind = segment.kind;
3498
3506
  return kind !== "insert" && kind !== "base";
3499
3507
  })) {
3500
- if (keyColumn.hidden === true || segments.some((segment) => {
3501
- const kind = segment.kind;
3502
- return kind !== "insert" && kind !== "base" && kind !== "update" && kind !== "delete";
3503
- })) {
3508
+ if (keyColumn.hidden === true)
3504
3509
  return void 0;
3505
- }
3506
- return this.#pointReadThroughDeltas(shape, keyColumn, segments, equalityColumns, projected, snapshot, options);
3510
+ return this.#pointReadThroughDeltas(shape, keyColumn, segments, equalityColumns, projected, snapshot, options, visibility.overlayTransactionId === void 0 ? MAX_POINT_READ_DELTA_BLOCKS : Number.POSITIVE_INFINITY);
3507
3511
  }
3508
3512
  const searchColumn = keyComponents.find((component) => component.type === "number" || component.type === "datetime") ?? keyComponents.find((component) => component.type === "string") ?? keyComponents[0];
3509
3513
  if (searchColumn === void 0)
@@ -3642,7 +3646,7 @@ class MinnowDatabase {
3642
3646
  }
3643
3647
  return void 0;
3644
3648
  }
3645
- async #pointReadThroughDeltas(shape, keyColumn, segments, equalityColumns, projected, snapshot, options) {
3649
+ async #pointReadThroughDeltas(shape, keyColumn, segments, equalityColumns, projected, snapshot, options, maxDecodedBlocks = MAX_POINT_READ_DELTA_BLOCKS) {
3646
3650
  const keyEquality = shape.equalities.find((equality) => equality.column === keyColumn.name);
3647
3651
  if (keyEquality === void 0)
3648
3652
  return void 0;
@@ -3670,7 +3674,7 @@ class MinnowDatabase {
3670
3674
  if (blockIndexes.length === 0)
3671
3675
  continue;
3672
3676
  decodedBlocks += blockIndexes.length;
3673
- if (decodedBlocks > MAX_POINT_READ_DELTA_BLOCKS)
3677
+ if (decodedBlocks > maxDecodedBlocks)
3674
3678
  return void 0;
3675
3679
  const keyBlocks = await this.#pointReadVectors(blockIndexes.map((blockIndex) => keyBlockIds[blockIndex] ?? ""), snapshot);
3676
3680
  for (const [position, blockIndex] of blockIndexes.entries()) {
@@ -3699,12 +3703,12 @@ class MinnowDatabase {
3699
3703
  if (blockIds.some((id) => id === void 0))
3700
3704
  return void 0;
3701
3705
  decodedBlocks += blockIds.length;
3702
- if (decodedBlocks > MAX_POINT_READ_DELTA_BLOCKS)
3706
+ if (decodedBlocks > maxDecodedBlocks)
3703
3707
  return void 0;
3704
3708
  const blocks = await this.#pointReadVectors(blockIds, snapshot);
3705
3709
  const values = segment.kind === "update" && current !== void 0 ? current : /* @__PURE__ */ new Map();
3706
3710
  if (segment.kind !== "update") {
3707
- if (current !== void 0)
3711
+ if (current !== void 0 && segment.kind !== "upsert")
3708
3712
  return void 0;
3709
3713
  values.set(keyColumn.name, vectorValue(keyVector, slot));
3710
3714
  }
@@ -3943,34 +3947,79 @@ class MinnowDatabase {
3943
3947
  return this.#queryWithReadReservation(query, {}, probe, memoize);
3944
3948
  return this.#queryWithReadReservation(query.sql, { params: query.params }, probe, memoize);
3945
3949
  }
3946
- async #liveMaintenancePlan(compiled, probe) {
3947
- if (compiled.usesStatementDatetime === true || compiled.usesVolatileFunctions === true || compiled.usesSequenceCalls === true) {
3950
+ async #liveMaintenancePlan(compiled, probe, reasons) {
3951
+ const decline = (reason) => {
3952
+ reasons?.push(reason);
3948
3953
  return void 0;
3949
- }
3954
+ };
3955
+ if (compiled.usesStatementDatetime === true)
3956
+ return decline("reads the statement clock");
3957
+ if (compiled.usesVolatileFunctions === true)
3958
+ return decline("calls a volatile function");
3959
+ if (compiled.usesSequenceCalls === true)
3960
+ return decline("calls a sequence");
3950
3961
  const plan = await this.#applyCatalogRewrites(compiled, probe);
3951
- if (blockCallsFunctions(plan, nonDeterministicFunctionNames))
3952
- return void 0;
3962
+ if (blockCallsFunctions(plan, nonDeterministicFunctionNames)) {
3963
+ return decline("calls a non-deterministic function");
3964
+ }
3953
3965
  const base = plan.base;
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)) {
3955
- return void 0;
3966
+ if (base.table === DUAL_TABLE)
3967
+ return decline("FROM has no base table");
3968
+ if (base.derived !== void 0)
3969
+ return decline("FROM is a derived table or CTE");
3970
+ if (base.union !== void 0)
3971
+ return decline("UNION, INTERSECT, or EXCEPT");
3972
+ if (base.recursive !== void 0)
3973
+ return decline("recursive CTE");
3974
+ if (base.windowed !== void 0)
3975
+ return decline("window function");
3976
+ if (plan.pendingSelectShape !== void 0)
3977
+ return decline("unexpanded SELECT shape");
3978
+ if (plan.distinctWildcard === true)
3979
+ return decline("SELECT DISTINCT *");
3980
+ if (plan.limitParameter !== void 0 || plan.offsetParameter !== void 0) {
3981
+ return decline("LIMIT or OFFSET given as a parameter");
3956
3982
  }
3983
+ if (planReadsBeyondSingleScan(plan))
3984
+ return decline("subquery or EXISTS");
3985
+ if (planContainsFts(plan))
3986
+ return decline("full-text MATCH or BM25");
3957
3987
  if (plan.joins.length > 0) {
3958
3988
  const join = plan.joins[0];
3959
- if (plan.joins.length !== 1 || join === void 0 || !["inner", "left"].includes(join.kind) || join.on !== void 0 || join.derived !== void 0 || join.union !== void 0 || join.windowed !== void 0 || join.recursive !== void 0)
3960
- return void 0;
3989
+ if (plan.joins.length !== 1 || join === void 0) {
3990
+ return decline(`${String(plan.joins.length)} joins; incremental maintenance supports one inner or left join to a unique key`);
3991
+ }
3992
+ if (!["inner", "left"].includes(join.kind)) {
3993
+ return decline(`${join.kind} join; only an inner or left join is maintained`);
3994
+ }
3995
+ if (join.on !== void 0) {
3996
+ return decline("join ON is not a single equality on the lookup table's unique key");
3997
+ }
3998
+ if (join.derived !== void 0 || join.union !== void 0 || join.windowed !== void 0 || join.recursive !== void 0) {
3999
+ return decline("join side is a derived table, union, window, or recursive CTE");
4000
+ }
3961
4001
  const lookup = await this.#findTable(join.table);
3962
4002
  const lookupKey = getUniqueKeyColumn(lookup);
3963
- if (lookupKey === void 0 || lookupKey.sqlDomain !== void 0)
3964
- return void 0;
4003
+ if (lookupKey === void 0)
4004
+ return decline(`${lookup.name} has no unique key to join on`);
4005
+ if (lookupKey.sqlDomain !== void 0) {
4006
+ return decline(`the unique key of ${lookup.name} has a SQL domain`);
4007
+ }
3965
4008
  const lookupReference = `${join.alias}.${lookupKey.name}`;
3966
4009
  const joinedKey = [join.left, join.right].find((expression) => expression.kind === "column" && expression.reference === lookupReference);
3967
- if (joinedKey === void 0)
3968
- return void 0;
4010
+ if (joinedKey === void 0) {
4011
+ return decline(`join ON does not name the unique key of ${lookup.name}`);
4012
+ }
3969
4013
  }
3970
4014
  const table = await this.#findTable(base.table);
3971
4015
  const keyColumn = getUniqueKeyColumn(table);
3972
- if (keyColumn === void 0 || keyColumn.hidden === true || keyColumn.sqlDomain !== void 0)
3973
- return void 0;
4016
+ if (keyColumn === void 0)
4017
+ return decline(`${table.name} has no unique key`);
4018
+ if (keyColumn.hidden === true)
4019
+ return decline(`${table.name} has a composite unique key`);
4020
+ if (keyColumn.sqlDomain !== void 0) {
4021
+ return decline(`the unique key of ${table.name} has a SQL domain`);
4022
+ }
3974
4023
  const aggregate = LiveAggregate.plan(plan, `${base.alias}.${keyColumn.name}`);
3975
4024
  if (aggregate !== void 0)
3976
4025
  return {
@@ -3992,24 +4041,27 @@ class MinnowDatabase {
3992
4041
  order: [],
3993
4042
  aggregate
3994
4043
  };
3995
- if (plan.groupBy.length > 0 || plan.having.length > 0)
3996
- return void 0;
4044
+ if (plan.groupBy.length > 0 || plan.having.length > 0) {
4045
+ return decline("GROUP BY or HAVING without additive aggregates (COUNT, SUM, AVG of one row-local argument)");
4046
+ }
3997
4047
  const rowLocal = (expression) => {
3998
4048
  if (expression.kind === "subquery" || expression.kind === "exists" || expression.kind === "window" || expression.kind === "wildcard" || expression.kind === "parameter" || hasAggregate(expression)) {
3999
4049
  return false;
4000
4050
  }
4001
4051
  return childExpressions(expression).every(rowLocal);
4002
4052
  };
4053
+ const notRowLocal = "a SELECT, WHERE, or ORDER BY term is not row-local (an aggregate, subquery, window, wildcard, or parameter)";
4003
4054
  if (!plan.select.every((item) => rowLocal(item.expression)))
4004
- return void 0;
4055
+ return decline(notRowLocal);
4005
4056
  if (!plan.predicates.every(({ left, right }) => rowLocal(left) && rowLocal(right))) {
4006
- return void 0;
4057
+ return decline(notRowLocal);
4007
4058
  }
4008
4059
  if (!plan.orderBy.every((term) => rowLocal(term.expression)))
4009
- return void 0;
4060
+ return decline(notRowLocal);
4010
4061
  const publicColumns = plan.select.map((item) => item.alias);
4011
- if (publicColumns.some((name) => name.startsWith(LIVE_HIDDEN_PREFIX)))
4012
- return void 0;
4062
+ if (publicColumns.some((name) => name.startsWith(LIVE_HIDDEN_PREFIX))) {
4063
+ return decline("an output alias uses the reserved __minnow_live prefix");
4064
+ }
4013
4065
  const fullPlan = clonePlanTree(plan);
4014
4066
  const qualifiedKey = `${base.alias}.${keyColumn.name}`;
4015
4067
  fullPlan.select.push({
@@ -4026,7 +4078,7 @@ class MinnowDatabase {
4026
4078
  } else if (term.expression.kind === "literal" && typeof term.expression.value === "number") {
4027
4079
  const position = term.expression.value;
4028
4080
  if (!Number.isInteger(position) || position < 1 || position > publicColumns.length) {
4029
- return void 0;
4081
+ return decline("ORDER BY ordinal is out of range");
4030
4082
  }
4031
4083
  alias = publicColumns[position - 1];
4032
4084
  }
@@ -4064,9 +4116,10 @@ class MinnowDatabase {
4064
4116
  }
4065
4117
  async #liveExecuteMaintainable(compiled, context) {
4066
4118
  const probe = context?.probe ?? await this.store.getCatalogProbe();
4067
- const state = await this.#liveMaintenancePlan(compiled, probe);
4119
+ const reasons = [];
4120
+ const state = await this.#liveMaintenancePlan(compiled, probe, reasons);
4068
4121
  if (state === void 0)
4069
- return void 0;
4122
+ return { declined: reasons };
4070
4123
  if (state.aggregate !== void 0) {
4071
4124
  try {
4072
4125
  const input = await this.#withReadReservation(() => this.#queryCompiled(state.fullPlan, {}, probe));
@@ -4828,10 +4881,16 @@ class MinnowDatabase {
4828
4881
  }
4829
4882
  }
4830
4883
  }
4831
- async #upsertTriggerFirings(table, keyColumn, batch, rowCount, readRows, conflictWhere) {
4884
+ async #readStoredRows(sql, params) {
4885
+ const plan = await this.#applyCatalogRewrites(bindPlanParameters(this.#compileCached(sql), [...params]));
4886
+ return this.#queryCompiled(plan, { memoize: false });
4887
+ }
4888
+ async #upsertTriggerFirings(table, keyColumn, batch, rowCount, readRows, conflictWhere, lookup) {
4832
4889
  const fires = (table.triggers ?? []).some((trigger) => trigger.event === "insert" || trigger.event === "update");
4833
- if (!fires && conflictWhere === void 0 || rowCount === 0)
4890
+ const retiresUniqueTerms = readyUniqueSecondaryIndexes(table).length > 0;
4891
+ if (!fires && conflictWhere === void 0 && !retiresUniqueTerms || rowCount === 0) {
4834
4892
  return void 0;
4893
+ }
4835
4894
  const quote = (name) => `"${name.replaceAll('"', '""')}"`;
4836
4895
  const keyValues = batch.columns[keyColumn.name] ?? [];
4837
4896
  const distinct = /* @__PURE__ */ new Map();
@@ -4841,13 +4900,24 @@ class MinnowDatabase {
4841
4900
  }
4842
4901
  const params = [...distinct.values()];
4843
4902
  const rows = [];
4844
- const keyWindowSize = Math.min(MAX_SQL_PARAMETERS, 1024);
4845
- for (let start = 0; start < params.length; start += keyWindowSize) {
4846
- const window = params.slice(start, start + keyWindowSize);
4847
- const placeholders = window.map(() => "?").join(", ");
4848
- const sql = `SELECT * FROM ${quote(table.name)} WHERE ${quote(keyColumn.name)} IN (${placeholders})`;
4849
- const result = readRows === void 0 ? await this.query(sql, { params: window, memoize: false }) : await readRows(sql, window);
4850
- rows.push(...result.rows);
4903
+ const projectedColumns = /* @__PURE__ */ new Set([keyColumn.name]);
4904
+ if (conflictWhere !== void 0)
4905
+ projectedColumns.add(conflictWhere.column.name);
4906
+ for (const { columns } of readyUniqueSecondaryIndexes(table)) {
4907
+ for (const column of columns)
4908
+ projectedColumns.add(column.name);
4909
+ }
4910
+ if (lookup !== void 0) {
4911
+ rows.push(...await lookup(params, fires ? "*" : [...projectedColumns]));
4912
+ } else {
4913
+ const projection = fires ? "*" : [...projectedColumns].map(quote).join(", ");
4914
+ for (let start = 0; start < params.length; start += SCOPE_KEY_LOOKUP_WINDOW) {
4915
+ const window = params.slice(start, start + SCOPE_KEY_LOOKUP_WINDOW);
4916
+ const placeholders = window.map(() => "?").join(", ");
4917
+ const sql = `SELECT ${projection} FROM ${quote(table.name)} WHERE ${quote(keyColumn.name)} IN (${placeholders})`;
4918
+ const result = readRows === void 0 ? await this.#readStoredRows(sql, window) : await readRows(sql, window);
4919
+ rows.push(...result.rows);
4920
+ }
4851
4921
  }
4852
4922
  const byToken = new Map(rows.map((row) => {
4853
4923
  const key = row[keyColumn.name];
@@ -4895,14 +4965,20 @@ class MinnowDatabase {
4895
4965
  return firings.oldImages[rowIndex]?.[column] ?? null;
4896
4966
  }, timing, cascadeBudget);
4897
4967
  }
4898
- async #triggerPreImages(table, keyColumn, keys, event, readRows, force = false) {
4968
+ async #triggerPreImages(table, keyColumn, keys, event, readRows, force = false, lookup) {
4899
4969
  if (!force && !(table.triggers ?? []).some((trigger) => trigger.event === event))
4900
4970
  return [];
4901
- const quote = (name) => `"${name.replaceAll('"', '""')}"`;
4902
- const placeholders = keys.map(() => "?").join(", ");
4903
- const preImageSql = `SELECT * FROM ${quote(table.name)} WHERE ${quote(keyColumn.name)} IN (${placeholders})`;
4904
- const result = readRows === void 0 ? await this.query(preImageSql, { params: [...keys], memoize: false }) : await readRows(preImageSql, [...keys]);
4905
- const byToken = new Map(result.rows.map((row) => {
4971
+ let rows;
4972
+ if (lookup !== void 0) {
4973
+ rows = await lookup(keys, "*");
4974
+ } else {
4975
+ const quote = (name) => `"${name.replaceAll('"', '""')}"`;
4976
+ const placeholders = keys.map(() => "?").join(", ");
4977
+ const preImageSql = `SELECT * FROM ${quote(table.name)} WHERE ${quote(keyColumn.name)} IN (${placeholders})`;
4978
+ const result = readRows === void 0 ? await this.#readStoredRows(preImageSql, [...keys]) : await readRows(preImageSql, [...keys]);
4979
+ rows = result.rows;
4980
+ }
4981
+ const byToken = new Map(rows.map((row) => {
4906
4982
  const key = row[keyColumn.name];
4907
4983
  return [
4908
4984
  key === null || key === void 0 ? "" : keyToken(keyColumn.type, key),
@@ -5099,7 +5175,7 @@ class MinnowDatabase {
5099
5175
  ...table.uniqueKeyColumnId === void 0 ? {} : { keyColumnId: table.uniqueKeyColumnId },
5100
5176
  level: 0,
5101
5177
  logicalOrder: 0,
5102
- commitOrdinal: transaction.pendingSegmentIds.length,
5178
+ commitOrdinal: transaction.pendingSegmentCount,
5103
5179
  rowIdSpans: [],
5104
5180
  createdAt: dateIsoString(this.#now())
5105
5181
  };
@@ -5110,7 +5186,13 @@ class MinnowDatabase {
5110
5186
  return this.#openWriteScope((session) => action(session));
5111
5187
  }
5112
5188
  #coordinateWrite(run) {
5113
- return this.#coordinateWrites ? coordinateWrite(this.store, run, this.#shutdown.signal) : run();
5189
+ if (!this.#coordinateWrites)
5190
+ return run();
5191
+ return coordinateWrite(this.store, run, this.#shutdown.signal, {
5192
+ onAdmissionWaitExceeded: (waitedMs) => {
5193
+ this.#reportBackgroundError(new Error(`A write waited ${String(waitedMs)}ms for the cross-tab admission lock and went ahead without it; another tab holds the lock and is not letting go`), "write admission");
5194
+ }
5195
+ });
5114
5196
  }
5115
5197
  async #openWriteScope(action, options = {}) {
5116
5198
  return this.#ownScope((signal) => this.#runWriteScope(action, options, signal));
@@ -5140,6 +5222,12 @@ class MinnowDatabase {
5140
5222
  durableSnapshot: options.durableSnapshot ?? true,
5141
5223
  coalesceArtifacts: true
5142
5224
  });
5225
+ this.#scopeWrites.set(transaction, {
5226
+ tables: /* @__PURE__ */ new Map(),
5227
+ mirrored: true,
5228
+ generation: 0,
5229
+ flushing: Promise.resolve()
5230
+ });
5143
5231
  let closed = false;
5144
5232
  let accepting = true;
5145
5233
  let tail = Promise.resolve();
@@ -5152,6 +5240,7 @@ class MinnowDatabase {
5152
5240
  signal.throwIfAborted();
5153
5241
  if (closed)
5154
5242
  throw new Error("The write scope has ended");
5243
+ poisoned ??= this.#scopeWrites.get(transaction)?.failure;
5155
5244
  if (poisoned !== void 0) {
5156
5245
  throw new Error("The write scope already failed and can only roll back", {
5157
5246
  cause: poisoned
@@ -5160,11 +5249,13 @@ class MinnowDatabase {
5160
5249
  };
5161
5250
  const guarded = async (run) => {
5162
5251
  const before = transaction.stagedWorkCount;
5252
+ const generation = this.#scopeWriteGeneration(transaction);
5163
5253
  try {
5164
5254
  return await run();
5165
5255
  } catch (error) {
5166
- if (transaction.stagedWorkCount !== before)
5256
+ if (transaction.stagedWorkCount !== before || this.#scopeWriteGeneration(transaction) !== generation) {
5167
5257
  poisoned ??= error;
5258
+ }
5168
5259
  throw error;
5169
5260
  }
5170
5261
  };
@@ -5189,6 +5280,14 @@ class MinnowDatabase {
5189
5280
  if (!isTransactionalStatement(statement)) {
5190
5281
  throw new TypeError(`${statement.kind.toUpperCase().replace("-", " ")} is not allowed inside a write scope`);
5191
5282
  }
5283
+ if (statement.kind === "update" || statement.kind === "delete") {
5284
+ const keyed = await this.#scopeKeyedMutation(statement);
5285
+ if (keyed !== void 0) {
5286
+ return guarded(() => this.#runScopeKeyedMutation(transaction, keyed, () => {
5287
+ staged += 1;
5288
+ }));
5289
+ }
5290
+ }
5192
5291
  return writer.executeStatement(statement);
5193
5292
  },
5194
5293
  insertBatch: async (tableName, input) => {
@@ -5216,18 +5315,20 @@ class MinnowDatabase {
5216
5315
  ...operations,
5217
5316
  queryPlan: (plan) => this.#withReadReservation(async () => {
5218
5317
  const probe = transaction.initialCatalogProbe;
5219
- return transaction.stagedWorkCount === 0 && probe !== void 0 ? this.#queryCompiled(plan, {}, probe) : this.#sessionQueryPlan(transaction, await this.#applyCatalogRewrites(plan));
5318
+ return transaction.stagedWorkCount === 0 && !this.#hasPendingScopeWrites(transaction) && probe !== void 0 ? this.#queryCompiled(plan, {}, probe) : this.#sessionQueryPlan(transaction, await this.#applyCatalogRewrites(plan));
5220
5319
  }),
5221
5320
  queryFirstColumn: (plan) => this.#withReadReservation(async () => {
5222
5321
  const probe = transaction.initialCatalogProbe;
5223
- return transaction.stagedWorkCount === 0 && probe !== void 0 ? this.#queryCompiledFirstColumn(plan, probe) : void 0;
5322
+ return transaction.stagedWorkCount === 0 && !this.#hasPendingScopeWrites(transaction) && probe !== void 0 ? this.#queryCompiledFirstColumn(plan, probe) : void 0;
5224
5323
  }),
5225
5324
  executeStatement: (statement) => {
5226
5325
  open();
5227
5326
  return guarded(() => this.runStatement(statement, { writer }));
5228
5327
  },
5229
- checkpoint: () => {
5328
+ stagedKeyPresence: (table, keyColumn, keys) => this.#scopeKeyPresence(transaction, table, keyColumn, keys),
5329
+ checkpoint: async () => {
5230
5330
  open();
5331
+ await this.#flushScopeWriteSets(transaction);
5231
5332
  return transaction.checkpoint();
5232
5333
  },
5233
5334
  checkpointRetainedBytes: () => {
@@ -5237,7 +5338,15 @@ class MinnowDatabase {
5237
5338
  rollbackTo: async (checkpoint) => {
5238
5339
  if (closed)
5239
5340
  throw new Error("The write scope has ended");
5240
- await transaction.rollbackTo(checkpoint);
5341
+ try {
5342
+ await transaction.rollbackTo(checkpoint);
5343
+ } catch (error) {
5344
+ const state = this.#scopeWrites.get(transaction);
5345
+ if (state !== void 0)
5346
+ state.failure ??= error;
5347
+ throw error;
5348
+ }
5349
+ this.#discardScopeWriteSets(transaction);
5241
5350
  poisoned = void 0;
5242
5351
  staged = transaction.stagedWorkCount === 0 ? 0 : 1;
5243
5352
  }
@@ -5290,6 +5399,8 @@ class MinnowDatabase {
5290
5399
  queryPlan: (plan) => enqueue(() => writer.queryPlan(plan), "read"),
5291
5400
  queryFirstColumn: (plan) => enqueue(() => writer.queryFirstColumn(plan), "read"),
5292
5401
  executeStatement: (statement) => enqueue(() => writer.executeStatement(statement)),
5402
+ checkpoint: () => enqueue(() => writer.checkpoint()),
5403
+ stagedKeyPresence: (table, keyColumn, keys) => enqueue(async () => writer.stagedKeyPresence?.(table, keyColumn, keys), "read"),
5293
5404
  rollbackTo: (checkpoint) => enqueue(() => writer.rollbackTo(checkpoint), "rollback")
5294
5405
  };
5295
5406
  try {
@@ -5298,6 +5409,7 @@ class MinnowDatabase {
5298
5409
  await tail;
5299
5410
  signal.throwIfAborted();
5300
5411
  closed = true;
5412
+ poisoned ??= this.#scopeWrites.get(transaction)?.failure;
5301
5413
  if (poisoned !== void 0) {
5302
5414
  throw new Error(`The write scope failed mid-stage and was rolled back: ${poisoned instanceof Error ? poisoned.message : "staging failed"}`, { cause: poisoned });
5303
5415
  }
@@ -5305,6 +5417,7 @@ class MinnowDatabase {
5305
5417
  await transaction.abort();
5306
5418
  return { result, version: await this.store.getCurrentManifestVersion() };
5307
5419
  }
5420
+ await this.#finishScopeWriteSets(transaction);
5308
5421
  for (let attempt = 0; attempt <= this.#maxCommitRetries; attempt += 1) {
5309
5422
  try {
5310
5423
  const manifest = await transaction.commit();
@@ -5330,9 +5443,33 @@ class MinnowDatabase {
5330
5443
  }
5331
5444
  }
5332
5445
  async #sessionQuery(transaction, sql, options = {}) {
5446
+ const requested = options;
5333
5447
  options = this.#effectiveQueryOptions(options);
5334
5448
  throwIfAborted(options.signal);
5335
- const bound = bindPlanParameters(this.#compileCached(sql), options.params);
5449
+ const compiled = this.#compileCached(sql);
5450
+ if (!pointReadTestHooks.disabled && requested.version === void 0 && requested.spillToStorage === void 0 && requested.spillPageRows === void 0 && requested.executionMemoryBudgetBytes === void 0 && (compiled.parameterCount ?? 0) === (requested.params?.length ?? 0)) {
5451
+ const template = cachedPointReadTemplate(compiled);
5452
+ if (template !== null) {
5453
+ const shape = resolvePointReadShape(template, options.params ?? []);
5454
+ if (shape !== void 0) {
5455
+ let point;
5456
+ try {
5457
+ pointReadTestHooks.attempted += 1;
5458
+ const table = await this.#findTable(shape.table);
5459
+ point = await this.#withSessionVisibility(transaction, [table], options, (snapshot, visibility, realTables) => this.#pointReadAtSnapshot(shape, snapshot, realTables, visibility, options));
5460
+ } catch (error) {
5461
+ if (!(error instanceof UnknownTableError))
5462
+ throw error;
5463
+ }
5464
+ throwIfAborted(options.signal);
5465
+ if (point !== void 0) {
5466
+ pointReadTestHooks.served += 1;
5467
+ return point;
5468
+ }
5469
+ }
5470
+ }
5471
+ }
5472
+ const bound = bindPlanParameters(compiled, options.params);
5336
5473
  const plan = await this.#applyCatalogRewrites(bound);
5337
5474
  throwIfAborted(options.signal);
5338
5475
  return this.#sessionQueryPlan(transaction, plan, options);
@@ -5343,11 +5480,30 @@ class MinnowDatabase {
5343
5480
  const names = collectRealTableNames(plan);
5344
5481
  const tables = await Promise.all(names.map((name) => this.#findTable(name)));
5345
5482
  throwIfAborted(options.signal);
5483
+ return this.#withSessionVisibility(transaction, tables, options, (snapshot, visibility, realTables) => this.#queryAtVisibility(plan, snapshot, visibility, realTables, true, options));
5484
+ }
5485
+ #scopeCommittedListings = /* @__PURE__ */ new WeakMap();
5486
+ async #scopeCommittedSegments(transaction, table) {
5487
+ let byTable = this.#scopeCommittedListings.get(transaction);
5488
+ if (byTable === void 0) {
5489
+ byTable = /* @__PURE__ */ new Map();
5490
+ this.#scopeCommittedListings.set(transaction, byTable);
5491
+ }
5492
+ const cached = byTable.get(table.id);
5493
+ if (cached !== void 0)
5494
+ return cached;
5495
+ const segments = (await listTableSegmentsPaged(this.store, table.id)).filter((segment) => segment.transactionId !== transaction.id);
5496
+ const records = await this.#transactionRecordsForSegments(segments, transaction.id);
5497
+ const entry = { segments, records };
5498
+ byTable.set(table.id, entry);
5499
+ return entry;
5500
+ }
5501
+ async #withSessionVisibility(transaction, tables, options, read) {
5502
+ await this.#flushScopeWriteSets(transaction, tables.map((table) => table.id));
5346
5503
  const realTables = new Map(tables.map((table) => [table.name, table]));
5347
- const pendingIds = new Set(transaction.pendingSegmentIds);
5348
- const pendingBlocks = new Set(transaction.pendingBlockIds);
5349
- const ourRecord = await this.store.getTransaction(transaction.id);
5350
- throwIfAborted(options.signal);
5504
+ const pendingBlocks = {
5505
+ has: (blockId) => transaction.hasPendingBlock(blockId)
5506
+ };
5351
5507
  return this.#withLeasedSnapshot(transaction.snapshotVersion, async (snapshot) => {
5352
5508
  const overlaySnapshot = {
5353
5509
  get version() {
@@ -5382,7 +5538,7 @@ class MinnowDatabase {
5382
5538
  committedIndexes.push(index);
5383
5539
  }
5384
5540
  });
5385
- const [committed, staged] = await Promise.all([
5541
+ const [committed, staged2] = await Promise.all([
5386
5542
  this.#getBlocksWindowed(committedIds),
5387
5543
  Promise.all(stagedIds.map((blockId) => transaction.getBlock(blockId)))
5388
5544
  ]);
@@ -5392,7 +5548,7 @@ class MinnowDatabase {
5392
5548
  ordered[index] = committed[offset];
5393
5549
  });
5394
5550
  stagedIndexes.forEach((index, offset) => {
5395
- ordered[index] = staged[offset];
5551
+ ordered[index] = staged2[offset];
5396
5552
  });
5397
5553
  blocks.push(...ordered);
5398
5554
  }
@@ -5402,35 +5558,31 @@ class MinnowDatabase {
5402
5558
  release: () => snapshot.release()
5403
5559
  };
5404
5560
  const segmentsByTable = /* @__PURE__ */ new Map();
5405
- const transactionRecords = /* @__PURE__ */ new Map();
5406
- const deferredSegments = transaction.deferredSegments;
5561
+ const transactionRecords = /* @__PURE__ */ new Map([
5562
+ [transaction.id, { committedVersion: STAGED_OVERLAY_ORDER_BASE }]
5563
+ ]);
5564
+ const staged = transaction.stagedSegments;
5407
5565
  for (const table of tables) {
5408
5566
  throwIfAborted(options.signal);
5409
- const segments = [
5410
- ...await listTableSegmentsPaged(this.store, table.id),
5411
- ...deferredSegments.filter((segment) => segment.tableId === table.id)
5412
- ];
5567
+ const committed = await this.#scopeCommittedSegments(transaction, table);
5413
5568
  throwIfAborted(options.signal);
5414
- const doctored = segments.map((segment) => pendingIds.has(segment.id) ? { ...segment, logicalOrder: STAGED_OVERLAY_ORDER_BASE + segment.commitOrdinal } : segment);
5569
+ const doctored = [
5570
+ ...committed.segments,
5571
+ ...staged.filter((segment) => segment.tableId === table.id).map((segment) => ({
5572
+ ...segment,
5573
+ logicalOrder: STAGED_OVERLAY_ORDER_BASE + segment.commitOrdinal
5574
+ }))
5575
+ ];
5415
5576
  segmentsByTable.set(table.id, doctored);
5416
- for (const record of await this.#transactionRecordsForSegments(doctored)) {
5577
+ for (const record of committed.records)
5417
5578
  transactionRecords.set(record.id, record);
5418
- }
5419
- throwIfAborted(options.signal);
5420
- }
5421
- if (ourRecord !== void 0) {
5422
- transactionRecords.set(transaction.id, {
5423
- ...ourRecord,
5424
- status: "committed",
5425
- committedVersion: STAGED_OVERLAY_ORDER_BASE
5426
- });
5427
5579
  }
5428
5580
  const visibility = {
5429
5581
  transactions: transactionRecords,
5430
5582
  segmentsByTable,
5431
5583
  overlayTransactionId: transaction.id
5432
5584
  };
5433
- return this.#queryAtVisibility(plan, overlaySnapshot, visibility, realTables, true, options);
5585
+ return read(overlaySnapshot, visibility, realTables);
5434
5586
  });
5435
5587
  }
5436
5588
  async #queryAtVisibility(plan, snapshot, visibility, realTables, cacheResults = true, options = {}) {
@@ -5470,23 +5622,508 @@ class MinnowDatabase {
5470
5622
  memory.close();
5471
5623
  }
5472
5624
  }
5625
+ #stagedKeyOverlays = /* @__PURE__ */ new WeakMap();
5473
5626
  #stagedKeyOverlay(transaction, tableId) {
5474
- const added = /* @__PURE__ */ new Set();
5475
- const removed = /* @__PURE__ */ new Set();
5476
- for (const entry of transaction.accumulatedUniqueKeyChanges) {
5477
- if (entry.tableId !== tableId)
5627
+ const entries = transaction.accumulatedUniqueKeyChanges;
5628
+ let cache = this.#stagedKeyOverlays.get(transaction);
5629
+ if (cache === void 0 || cache.consumed > entries.length) {
5630
+ cache = { consumed: 0, tables: /* @__PURE__ */ new Map() };
5631
+ this.#stagedKeyOverlays.set(transaction, cache);
5632
+ }
5633
+ for (; cache.consumed < entries.length; cache.consumed += 1) {
5634
+ const entry = entries[cache.consumed];
5635
+ if (entry === void 0)
5478
5636
  continue;
5637
+ let overlay = cache.tables.get(entry.tableId);
5638
+ if (overlay === void 0) {
5639
+ overlay = { added: /* @__PURE__ */ new Set(), removed: /* @__PURE__ */ new Set() };
5640
+ cache.tables.set(entry.tableId, overlay);
5641
+ }
5479
5642
  for (const token of entry.keyTokens) {
5480
5643
  if (entry.remove === true) {
5481
- added.delete(token);
5482
- removed.add(token);
5644
+ overlay.added.delete(token);
5645
+ overlay.removed.add(token);
5483
5646
  } else {
5484
- removed.delete(token);
5485
- added.add(token);
5647
+ overlay.removed.delete(token);
5648
+ overlay.added.add(token);
5649
+ }
5650
+ }
5651
+ }
5652
+ return cache.tables.get(tableId) ?? { added: /* @__PURE__ */ new Set(), removed: /* @__PURE__ */ new Set() };
5653
+ }
5654
+ #scopeWrites = /* @__PURE__ */ new WeakMap();
5655
+ #scopeWriteState(transaction) {
5656
+ return this.#scopeWrites.get(transaction);
5657
+ }
5658
+ #scopeWriteSet(state, table) {
5659
+ let set = state.tables.get(table.id);
5660
+ if (set === void 0) {
5661
+ set = {
5662
+ table,
5663
+ keyColumn: getUniqueKeyColumn(table),
5664
+ rows: /* @__PURE__ */ new Map(),
5665
+ pendingRows: 0,
5666
+ pendingBytes: 0,
5667
+ mirrorBytes: 0,
5668
+ opaque: false,
5669
+ unkeyedSequence: 0
5670
+ };
5671
+ state.tables.set(table.id, set);
5672
+ }
5673
+ return set;
5674
+ }
5675
+ #hasPendingScopeWrites(transaction) {
5676
+ const state = this.#scopeWrites.get(transaction);
5677
+ if (state === void 0)
5678
+ return false;
5679
+ for (const set of state.tables.values())
5680
+ if (set.pendingRows > 0)
5681
+ return true;
5682
+ return false;
5683
+ }
5684
+ #scopeWriteGeneration(transaction) {
5685
+ return this.#scopeWrites.get(transaction)?.generation ?? 0;
5686
+ }
5687
+ async #stageScopeDirectly(transaction, table) {
5688
+ const state = this.#scopeWriteState(transaction);
5689
+ if (state === void 0)
5690
+ return;
5691
+ await this.#flushScopeWriteSets(transaction, [table.id]);
5692
+ this.#scopeWriteSet(state, table).opaque = true;
5693
+ }
5694
+ async #finishScopeWriteSets(transaction) {
5695
+ const state = this.#scopeWriteState(transaction);
5696
+ if (state === void 0)
5697
+ return;
5698
+ state.mirrored = false;
5699
+ await this.#flushScopeWriteSets(transaction);
5700
+ }
5701
+ #applyScopeEffect(set, token, key, effect) {
5702
+ let entry = set.rows.get(token);
5703
+ if (entry === void 0) {
5704
+ entry = { key };
5705
+ set.rows.set(token, entry);
5706
+ }
5707
+ if (entry.pending === void 0)
5708
+ set.pendingRows += 1;
5709
+ entry.pending = composeScopeEffect(entry.pending, effect);
5710
+ }
5711
+ async #bufferScopeRows(transaction, table, batch, rowCount, kind) {
5712
+ const state = this.#scopeWriteState(transaction);
5713
+ if (state === void 0)
5714
+ throw new Error("Only a write scope buffers its statements");
5715
+ state.generation += 1;
5716
+ const set = this.#scopeWriteSet(state, table);
5717
+ const { keyColumn } = set;
5718
+ const keyValues = keyColumn === void 0 ? void 0 : batch.columns[keyColumn.name] ?? [];
5719
+ for (let row = 0; row < rowCount; row += 1) {
5720
+ const values = table.columns.map((column) => batch.columns[column.name]?.[row] ?? null);
5721
+ let token;
5722
+ let key;
5723
+ if (keyColumn === void 0 || keyValues === void 0) {
5724
+ token = `\0${String(set.unkeyedSequence)}`;
5725
+ set.unkeyedSequence += 1;
5726
+ } else {
5727
+ const value = keyValues[row] ?? null;
5728
+ if (value === null)
5729
+ throw new TypeError(`Unique key cannot be null: ${keyColumn.name}`);
5730
+ token = keyToken(keyColumn.type, value);
5731
+ key = value;
5732
+ }
5733
+ this.#applyScopeEffect(set, token, key, { kind, values });
5734
+ }
5735
+ set.pendingBytes += estimateBatchBytes(batch);
5736
+ await this.#settleScopeWriteSet(transaction, set);
5737
+ }
5738
+ async #bufferScopeUpdate(transaction, table, keyColumn, input) {
5739
+ const state = this.#scopeWriteState(transaction);
5740
+ if (state === void 0)
5741
+ throw new Error("Only a write scope buffers its statements");
5742
+ state.generation += 1;
5743
+ const set = this.#scopeWriteSet(state, table);
5744
+ const keyPosition = table.columns.findIndex((column) => column.id === keyColumn.id);
5745
+ const changes = Object.entries(input.changes).map(([name, values]) => ({
5746
+ position: table.columns.findIndex((column) => column.name === name),
5747
+ values
5748
+ }));
5749
+ let bytes = estimateValuesBytes(input.keys);
5750
+ for (const change of changes)
5751
+ bytes += estimateValuesBytes(change.values);
5752
+ for (let row = 0; row < input.keys.length; row += 1) {
5753
+ const key = input.keys[row] ?? null;
5754
+ if (key === null)
5755
+ throw new TypeError(`Unique key cannot be null: ${keyColumn.name}`);
5756
+ const values = new Array(table.columns.length).fill(void 0);
5757
+ values[keyPosition] = key;
5758
+ for (const change of changes)
5759
+ values[change.position] = change.values[row] ?? null;
5760
+ this.#applyScopeEffect(set, keyToken(keyColumn.type, key), key, { kind: "update", values });
5761
+ }
5762
+ set.pendingBytes += bytes;
5763
+ await this.#settleScopeWriteSet(transaction, set);
5764
+ }
5765
+ async #bufferScopeDelete(transaction, table, keys) {
5766
+ const state = this.#scopeWriteState(transaction);
5767
+ if (state === void 0)
5768
+ throw new Error("Only a write scope buffers its statements");
5769
+ state.generation += 1;
5770
+ const set = this.#scopeWriteSet(state, table);
5771
+ for (const [token, key] of keys) {
5772
+ this.#applyScopeEffect(set, token, key, { kind: "delete", values: [] });
5773
+ }
5774
+ set.pendingBytes += estimateValuesBytes([...keys.values()]);
5775
+ await this.#settleScopeWriteSet(transaction, set);
5776
+ }
5777
+ async #settleScopeWriteSet(transaction, set) {
5778
+ if (set.pendingRows >= this.#rowsPerBlock) {
5779
+ await this.#flushScopeWriteSets(transaction, [set.table.id]);
5780
+ return;
5781
+ }
5782
+ let pendingBytes = 0;
5783
+ for (const pending of this.#scopeWriteState(transaction)?.tables.values() ?? []) {
5784
+ pendingBytes += pending.pendingBytes;
5785
+ }
5786
+ if (pendingBytes >= scopeWriteSetTestHooks.budgetBytes) {
5787
+ await this.#flushScopeWriteSets(transaction);
5788
+ }
5789
+ }
5790
+ #flushScopeWriteSets(transaction, tableIds) {
5791
+ const state = this.#scopeWrites.get(transaction);
5792
+ if (state === void 0)
5793
+ return Promise.resolve();
5794
+ const ids = tableIds === void 0 ? void 0 : [...tableIds];
5795
+ const turn = state.flushing.then(() => this.#flushScopeWriteSetsNow(transaction, state, ids));
5796
+ state.flushing = turn.catch(() => void 0);
5797
+ return turn;
5798
+ }
5799
+ async #flushScopeWriteSetsNow(transaction, state, tableIds) {
5800
+ const ids = tableIds ?? [...state.tables.keys()];
5801
+ for (const tableId of ids) {
5802
+ const set = state.tables.get(tableId);
5803
+ if (set === void 0 || set.pendingRows === 0)
5804
+ continue;
5805
+ try {
5806
+ await this.#stageScopeWriteSet(transaction, state, set);
5807
+ } catch (error) {
5808
+ state.failure ??= error;
5809
+ throw error;
5810
+ }
5811
+ }
5812
+ if (!state.mirrored)
5813
+ return;
5814
+ let retained = 0;
5815
+ for (const set of state.tables.values())
5816
+ retained += set.mirrorBytes + set.pendingBytes;
5817
+ if (retained <= scopeWriteSetTestHooks.budgetBytes)
5818
+ return;
5819
+ state.mirrored = false;
5820
+ for (const set of state.tables.values()) {
5821
+ for (const [token, entry] of set.rows) {
5822
+ if (entry.pending === void 0)
5823
+ set.rows.delete(token);
5824
+ else
5825
+ delete entry.staged;
5826
+ }
5827
+ set.mirrorBytes = 0;
5828
+ }
5829
+ }
5830
+ async #stageScopeWriteSet(transaction, state, set) {
5831
+ const { table, keyColumn } = set;
5832
+ const keyPosition = keyColumn === void 0 ? -1 : table.columns.findIndex((column) => column.id === keyColumn.id);
5833
+ const deletes = [];
5834
+ const updates = /* @__PURE__ */ new Map();
5835
+ const upserts = [];
5836
+ const inserts = [];
5837
+ for (const entry of set.rows.values()) {
5838
+ const pending = entry.pending;
5839
+ if (pending === void 0)
5840
+ continue;
5841
+ switch (pending.kind) {
5842
+ case "delete":
5843
+ if (entry.key !== void 0)
5844
+ deletes.push(entry.key);
5845
+ break;
5846
+ case "insert":
5847
+ inserts.push(pending);
5848
+ break;
5849
+ case "upsert":
5850
+ upserts.push(pending);
5851
+ break;
5852
+ case "update": {
5853
+ const positions = [];
5854
+ pending.values.forEach((value, position) => {
5855
+ if (value !== void 0 && position !== keyPosition)
5856
+ positions.push(position);
5857
+ });
5858
+ const signature = positions.join(",");
5859
+ let group = updates.get(signature);
5860
+ if (group === void 0) {
5861
+ group = { positions, entries: [] };
5862
+ updates.set(signature, group);
5863
+ }
5864
+ group.entries.push(entry);
5865
+ break;
5866
+ }
5867
+ }
5868
+ }
5869
+ const batchOf = (effects) => ({
5870
+ columns: Object.fromEntries(table.columns.map((column, position) => [
5871
+ column.name,
5872
+ effects.map((effect) => effect.values[position] ?? null)
5873
+ ])),
5874
+ rowCount: effects.length
5875
+ });
5876
+ if (keyColumn !== void 0) {
5877
+ if (deletes.length > 0) {
5878
+ await this.#stageDeleteSegment(transaction, table, keyColumn, deletes);
5879
+ }
5880
+ for (const group of updates.values()) {
5881
+ const changes = {};
5882
+ for (const position of group.positions) {
5883
+ const column = table.columns[position];
5884
+ if (column === void 0)
5885
+ continue;
5886
+ changes[column.name] = group.entries.map((entry) => entry.pending?.values[position] ?? null);
5887
+ }
5888
+ await this.#stageUpdateSegment(transaction, table, keyColumn, {
5889
+ keys: group.entries.map((entry) => entry.key ?? null),
5890
+ changes
5891
+ });
5892
+ }
5893
+ }
5894
+ if (upserts.length > 0) {
5895
+ await this.#stageInsertSegment(transaction, table, batchOf(upserts), upserts.length, "upsert");
5896
+ }
5897
+ if (inserts.length > 0) {
5898
+ await this.#stageInsertSegment(transaction, table, batchOf(inserts), inserts.length, "insert");
5899
+ }
5900
+ const mirror = keyColumn !== void 0 && state.mirrored && !set.opaque;
5901
+ for (const [token, entry] of set.rows) {
5902
+ if (entry.pending === void 0)
5903
+ continue;
5904
+ if (!mirror) {
5905
+ set.rows.delete(token);
5906
+ continue;
5907
+ }
5908
+ entry.staged = composeScopeEffect(entry.staged, entry.pending);
5909
+ delete entry.pending;
5910
+ }
5911
+ if (mirror)
5912
+ set.mirrorBytes += set.pendingBytes;
5913
+ set.pendingBytes = 0;
5914
+ set.pendingRows = 0;
5915
+ }
5916
+ #discardScopeWriteSets(transaction) {
5917
+ const generation = this.#scopeWriteGeneration(transaction) + 1;
5918
+ this.#scopeWrites.set(transaction, {
5919
+ tables: /* @__PURE__ */ new Map(),
5920
+ mirrored: false,
5921
+ generation,
5922
+ flushing: Promise.resolve()
5923
+ });
5924
+ this.#stagedKeyOverlays.delete(transaction);
5925
+ }
5926
+ async #scopeRowsByKey(transaction, table, keyColumn, keys, projection) {
5927
+ const state = this.#scopeWrites.get(transaction);
5928
+ if (!state?.mirrored)
5929
+ return void 0;
5930
+ const set = state.tables.get(table.id);
5931
+ if (set?.opaque === true)
5932
+ return void 0;
5933
+ const names = projection === "*" ? visibleTableColumns(table).map(({ name }) => name) : [...projection];
5934
+ if (!names.includes(keyColumn.name))
5935
+ names.push(keyColumn.name);
5936
+ const wanted = new Set(names);
5937
+ const answered = /* @__PURE__ */ new Map();
5938
+ const committed = [];
5939
+ const patches = /* @__PURE__ */ new Map();
5940
+ for (const key of keys) {
5941
+ const token = keyToken(keyColumn.type, key);
5942
+ const entry = set?.rows.get(token);
5943
+ const effect = entry === void 0 ? void 0 : netScopeEffect(entry);
5944
+ if (effect === void 0) {
5945
+ committed.push(key);
5946
+ continue;
5947
+ }
5948
+ if (effect.kind === "delete")
5949
+ continue;
5950
+ if (effect.kind === "update") {
5951
+ committed.push(key);
5952
+ patches.set(token, effect);
5953
+ continue;
5954
+ }
5955
+ const row = {};
5956
+ table.columns.forEach((column, position) => {
5957
+ if (wanted.has(column.name))
5958
+ row[column.name] = effect.values[position] ?? null;
5959
+ });
5960
+ answered.set(token, row);
5961
+ }
5962
+ 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);
5965
+ const plan = {
5966
+ sql: "(scope keyed lookup)",
5967
+ base: { table: table.name, alias: table.name },
5968
+ joins: [],
5969
+ select: names.map((name) => ({
5970
+ expression: { kind: "column", reference: name },
5971
+ alias: name
5972
+ })),
5973
+ predicates: [
5974
+ {
5975
+ left: { kind: "column", reference: keyColumn.name },
5976
+ operator: "IN",
5977
+ right: { kind: "list", items: window.map((value) => ({ kind: "literal", value })) }
5978
+ }
5979
+ ],
5980
+ groupBy: [],
5981
+ having: [],
5982
+ orderBy: []
5983
+ };
5984
+ const result = await this.#queryCompiled(plan, {
5985
+ version: transaction.snapshotVersion,
5986
+ memoize: false
5987
+ });
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);
5486
6002
  }
5487
6003
  }
5488
6004
  }
5489
- return { added, removed };
6005
+ return [...answered.values()];
6006
+ }
6007
+ #scopeKeyLookup(transaction, table, keyColumn) {
6008
+ return async (keys, projection) => {
6009
+ const direct = await this.#scopeRowsByKey(transaction, table, keyColumn, keys, projection);
6010
+ if (direct !== void 0)
6011
+ return direct;
6012
+ const quote = (name) => `"${name.replaceAll('"', '""')}"`;
6013
+ const selected = projection === "*" ? "*" : projection.map(quote).join(", ");
6014
+ const rows = [];
6015
+ for (let start = 0; start < keys.length; start += SCOPE_KEY_LOOKUP_WINDOW) {
6016
+ const window = keys.slice(start, start + SCOPE_KEY_LOOKUP_WINDOW);
6017
+ const placeholders = window.map(() => "?").join(", ");
6018
+ const sql = `SELECT ${selected} FROM ${quote(table.name)} WHERE ${quote(keyColumn.name)} IN (${placeholders})`;
6019
+ rows.push(...(await this.#sessionQuery(transaction, sql, { params: [...window] })).rows);
6020
+ }
6021
+ return rows;
6022
+ };
6023
+ }
6024
+ async #scopeKeyedMutation(statement) {
6025
+ if (statement.from !== void 0 || statement.returning !== void 0 || statement.returningItems !== void 0 || statement.predicates.length !== 1) {
6026
+ return void 0;
6027
+ }
6028
+ const table = await this.#findTable(statement.table);
6029
+ const keyColumn = getUniqueKeyColumn(table);
6030
+ if (keyColumn === void 0 || keyColumn.hidden === true || keyColumn.sqlDomain !== void 0) {
6031
+ return void 0;
6032
+ }
6033
+ const predicate = statement.predicates[0];
6034
+ if (predicate === void 0)
6035
+ return void 0;
6036
+ const keyNames = /* @__PURE__ */ new Set([
6037
+ keyColumn.name,
6038
+ `${statement.alias ?? table.name}.${keyColumn.name}`
6039
+ ]);
6040
+ const isKey = (expression) => expression.kind === "column" && keyNames.has(expression.reference);
6041
+ const keys = [];
6042
+ const accept = (expression) => {
6043
+ const value = plainLiteralValue(expression);
6044
+ if (value === void 0 || value === null || !valueMatchesColumnType(keyColumn, value)) {
6045
+ return false;
6046
+ }
6047
+ keys.push(value);
6048
+ return true;
6049
+ };
6050
+ if (predicate.operator === "=") {
6051
+ if (isKey(predicate.left)) {
6052
+ if (!accept(predicate.right))
6053
+ return void 0;
6054
+ } else if (isKey(predicate.right)) {
6055
+ if (!accept(predicate.left))
6056
+ return void 0;
6057
+ } else {
6058
+ return void 0;
6059
+ }
6060
+ } else if (predicate.operator === "IN" && isKey(predicate.left)) {
6061
+ if (predicate.right.kind !== "list")
6062
+ return void 0;
6063
+ for (const item of predicate.right.items)
6064
+ if (!accept(item))
6065
+ return void 0;
6066
+ } else {
6067
+ return void 0;
6068
+ }
6069
+ if (statement.kind === "delete")
6070
+ return { kind: "delete", table, keyColumn, keys, changes: {} };
6071
+ const changes = {};
6072
+ for (const assignment of statement.assignments) {
6073
+ const column = table.columns.find((candidate) => candidate.name === assignment.column);
6074
+ if (column === void 0 || column.sqlDomain !== void 0 || column.generatedValue !== void 0 || column.id === keyColumn.id || (table.primaryKeyColumnIds ?? []).includes(column.id) || Object.hasOwn(changes, column.name)) {
6075
+ return void 0;
6076
+ }
6077
+ const value = plainLiteralValue(assignment.expression);
6078
+ if (value === void 0 || value !== null && !valueMatchesColumnType(column, value)) {
6079
+ return void 0;
6080
+ }
6081
+ changes[column.name] = value;
6082
+ }
6083
+ if (Object.keys(changes).length === 0)
6084
+ return void 0;
6085
+ return { kind: "update", table, keyColumn, keys, changes };
6086
+ }
6087
+ async #scopeKeyPresence(transaction, table, keyColumn, keys) {
6088
+ const overlay = this.#stagedKeyOverlay(transaction, table.id);
6089
+ const present = /* @__PURE__ */ new Set();
6090
+ const unresolved = [];
6091
+ for (const key of keys) {
6092
+ const token = keyToken(keyColumn.type, key);
6093
+ if (overlay.added.has(token))
6094
+ present.add(token);
6095
+ else if (!overlay.removed.has(token))
6096
+ unresolved.push(token);
6097
+ }
6098
+ if (unresolved.length > 0) {
6099
+ const existing = await this.#existingKeyTokens(table, transaction.snapshotVersion, unresolved);
6100
+ for (const token of existing)
6101
+ present.add(token);
6102
+ }
6103
+ return present;
6104
+ }
6105
+ async #runScopeKeyedMutation(transaction, keyed, beforeMutation) {
6106
+ const { table, keyColumn } = keyed;
6107
+ const byToken = /* @__PURE__ */ new Map();
6108
+ for (const key of keyed.keys)
6109
+ byToken.set(keyToken(keyColumn.type, key), key);
6110
+ const presence = await this.#scopeKeyPresence(transaction, table, keyColumn, [
6111
+ ...byToken.values()
6112
+ ]);
6113
+ const present = [];
6114
+ for (const [token, key] of byToken)
6115
+ if (presence.has(token))
6116
+ present.push(key);
6117
+ if (present.length === 0)
6118
+ return { kind: keyed.kind, table: table.name, rowCount: 0 };
6119
+ beforeMutation();
6120
+ if (keyed.kind === "delete") {
6121
+ const deleted = await this.#sessionDelete(transaction, table.name, { keys: present });
6122
+ return { kind: "delete", table: table.name, rowCount: deleted.rowCount };
6123
+ }
6124
+ 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 });
6126
+ return { kind: "update", table: table.name, rowCount: updated.rowCount };
5490
6127
  }
5491
6128
  async #sessionInsert(transaction, tableName, input, kind, options, cascadeBudget = 1) {
5492
6129
  const table = await this.#findTable(tableName);
@@ -5507,7 +6144,7 @@ class MinnowDatabase {
5507
6144
  validateValue(autoIncrement.column, patched[rowIndex] ?? null, rowIndex);
5508
6145
  }
5509
6146
  }
5510
- let sessionUpsertFirings = sessionUpsertKeyColumn === void 0 ? void 0 : await this.#upsertTriggerFirings(table, sessionUpsertKeyColumn, batch, rowCount, (sql, params) => this.#sessionQuery(transaction, sql, { params }), normalizedConflictWhere);
6147
+ let sessionUpsertFirings = sessionUpsertKeyColumn === void 0 ? void 0 : await this.#upsertTriggerFirings(table, sessionUpsertKeyColumn, batch, rowCount, void 0, normalizedConflictWhere, this.#scopeKeyLookup(transaction, table, sessionUpsertKeyColumn));
5511
6148
  let skippedRowCount = 0;
5512
6149
  if (normalizedConflictWhere !== void 0) {
5513
6150
  if (sessionUpsertFirings === void 0) {
@@ -5535,6 +6172,15 @@ class MinnowDatabase {
5535
6172
  await this.#assertCompactionCapacity(table, transaction);
5536
6173
  const keys = batchKeys(table, batch);
5537
6174
  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
+ }
5538
6184
  transaction.setUniqueKeyChanges({
5539
6185
  tableId: table.id,
5540
6186
  keyTokens: [...keys.keys()],
@@ -5542,9 +6188,22 @@ class MinnowDatabase {
5542
6188
  });
5543
6189
  }
5544
6190
  await this.#assertForeignKeysPresent(table, (column) => batch.columns[column] ?? [], (sql, params) => this.#sessionQuery(transaction, sql, { params }), transaction);
5545
- const rowIds = await this.store.reserveRowIds(table.id, rowCount);
5546
6191
  const insertValueAt = (source, column, rowIndex) => source === "new" ? batch.columns[column]?.[rowIndex] ?? null : null;
5547
6192
  stageSecondaryUniqueInsertChanges(transaction, table, batch, kind === "upsert" ? sessionUpsertFirings?.oldImages : void 0);
6193
+ const buffered = this.#scopeWrites.has(transaction) && rowCount < SCOPE_DIRECT_STAGE_ROWS && !(table.triggers ?? []).some((trigger) => trigger.event === "insert" || trigger.event === "update");
6194
+ if (buffered) {
6195
+ await this.#bufferScopeRows(transaction, table, batch, rowCount, kind);
6196
+ collectAutoIncrementGenerated(batch, generated, autoIncrement);
6197
+ return {
6198
+ tableName: table.name,
6199
+ segmentId: null,
6200
+ rowCount,
6201
+ skippedRowCount,
6202
+ ...generated.size === 0 ? {} : { generatedColumns: Object.fromEntries(generated) }
6203
+ };
6204
+ }
6205
+ await this.#stageScopeDirectly(transaction, table);
6206
+ const rowIds = await this.store.reserveRowIds(table.id, rowCount);
5548
6207
  if (kind === "insert") {
5549
6208
  await this.#stageTriggerDerivedInserts(transaction, table, "insert", rowCount, insertValueAt, "before", cascadeBudget);
5550
6209
  } else if (sessionUpsertFirings !== void 0) {
@@ -5587,7 +6246,7 @@ class MinnowDatabase {
5587
6246
  }
5588
6247
  const sessionChecks = table.checks ?? [];
5589
6248
  let changedForeignKey = (table.foreignKeys ?? []).some((key) => key.enforced !== false && foreignKeyColumns(key).some((column) => input.changes[column] !== void 0));
5590
- const preImages = await this.#triggerPreImages(table, keyColumn, input.keys.filter((key) => key !== null), "update", (preImageSql, params) => this.#sessionQuery(transaction, preImageSql, { params }), sessionChecks.length > 0 || changedForeignKey || tableGeneratedExpressions(table).length > 0 || secondaryIndexUpdateNeedsPreImages(table, input) || readyUniqueSecondaryIndexes(table).some(({ columns: columns2 }) => columns2.some((column) => input.changes[column.name] !== void 0)));
6249
+ const preImages = await this.#triggerPreImages(table, keyColumn, input.keys.filter((key) => key !== null), "update", void 0, sessionChecks.length > 0 || changedForeignKey || tableGeneratedExpressions(table).length > 0 || secondaryIndexUpdateNeedsPreImages(table, input) || readyUniqueSecondaryIndexes(table).some(({ columns }) => columns.some((column) => input.changes[column.name] !== void 0)), this.#scopeKeyLookup(transaction, table, keyColumn));
5591
6250
  input = applyStoredGeneratedUpdateChanges(table, input, preImages);
5592
6251
  validateUpdateBatch(table, keyColumn, input);
5593
6252
  changedForeignKey = (table.foreignKeys ?? []).some((key) => key.enforced !== false && foreignKeyColumns(key).some((column) => input.changes[column] !== void 0));
@@ -5617,56 +6276,14 @@ class MinnowDatabase {
5617
6276
  }
5618
6277
  }
5619
6278
  await this.#stageTriggerDerivedInserts(transaction, table, "update", input.keys.length, sessionUpdateValueAt, "before", cascadeBudget);
5620
- const changedColumns = Object.keys(input.changes).sort();
5621
- const columns = [keyColumn, ...changedColumns.map((name) => findColumn(table, name))];
5622
- const segmentId = this.#createId();
5623
- const columnBlockIds = {};
5624
- const blockStager = new BoundedWriteBlockStager(transaction);
5625
- const plannedColumns = columns.map((column) => writeColumnValues(column.type, column.id === keyColumn.id ? input.keys : input.changes[column.name] ?? []));
5626
- const ranges = writeBlockRanges(plannedColumns, input.keys.length, this.#rowsPerBlock, this.#targetBlockBytes);
5627
- for (const [columnIndex, column] of columns.entries()) {
5628
- const values = column.id === keyColumn.id ? input.keys : input.changes[column.name] ?? [];
5629
- const plannedColumn = plannedColumns[columnIndex];
5630
- if (plannedColumn === void 0)
5631
- throw new Error(`Write column disappeared: ${column.name}`);
5632
- const blockIds = [];
5633
- for (const [part, { start, end }] of ranges.entries()) {
5634
- await blockStager.prepare(maximumWriteBlockStoredBytes(plannedColumn, start, end, this.#compression));
5635
- const slice = values.slice(start, end);
5636
- const bytes = await this.#encodeColumnBlock(column.id, asColumnInput(column.type, slice));
5637
- const blockId = [
5638
- "table",
5639
- table.id,
5640
- "segment",
5641
- segmentId,
5642
- "update-column",
5643
- column.id,
5644
- "part",
5645
- String(part).padStart(6, "0")
5646
- ].join("/");
5647
- blockStager.add({ id: blockId, bytes });
5648
- blockIds.push(blockId);
5649
- }
5650
- columnBlockIds[column.id] = blockIds;
6279
+ const buffered = this.#scopeWrites.has(transaction) && input.keys.length < SCOPE_DIRECT_STAGE_ROWS && !(table.triggers ?? []).some((trigger) => trigger.event === "update");
6280
+ let segmentId = null;
6281
+ if (buffered) {
6282
+ await this.#bufferScopeUpdate(transaction, table, keyColumn, input);
6283
+ } else {
6284
+ await this.#stageScopeDirectly(transaction, table);
6285
+ segmentId = await this.#stageUpdateSegment(transaction, table, keyColumn, input);
5651
6286
  }
5652
- await blockStager.stageWithSegments([
5653
- {
5654
- id: segmentId,
5655
- tableId: table.id,
5656
- transactionId: transaction.id,
5657
- rowCount: input.keys.length,
5658
- rowIdStart: 0n,
5659
- rowIdEndExclusive: 0n,
5660
- columnBlockIds,
5661
- kind: "update",
5662
- keyColumnId: keyColumn.id,
5663
- level: 0,
5664
- logicalOrder: 0,
5665
- commitOrdinal: transaction.pendingSegmentIds.length,
5666
- rowIdSpans: [],
5667
- createdAt: dateIsoString(this.#now())
5668
- }
5669
- ]);
5670
6287
  await this.#stageTriggerDerivedInserts(transaction, table, "update", input.keys.length, sessionUpdateValueAt, "after", cascadeBudget);
5671
6288
  return {
5672
6289
  tableName: table.name,
@@ -5727,14 +6344,79 @@ class MinnowDatabase {
5727
6344
  if (secondaryCoverage.length > 0) {
5728
6345
  transaction.setFtsChanges({ tableId: table.id, columns: secondaryCoverage });
5729
6346
  }
5730
- const preImages = (await this.#triggerPreImages(table, keyColumn, [...keys.values()], "delete", (preImageSql, params) => this.#sessionQuery(transaction, preImageSql, { params }), readyUniqueSecondaryIndexes(table).length > 0)).filter((row) => row !== void 0);
6347
+ const preImages = (await this.#triggerPreImages(table, keyColumn, [...keys.values()], "delete", void 0, readyUniqueSecondaryIndexes(table).length > 0, this.#scopeKeyLookup(transaction, table, keyColumn))).filter((row) => row !== void 0);
5731
6348
  stageSecondaryUniqueMutationChanges(transaction, table, void 0, preImages);
5732
6349
  const sessionDeleteValueAt = (source, column, rowIndex) => source === "old" ? preImages[rowIndex]?.[column] ?? null : null;
5733
6350
  await this.#stageTriggerDerivedInserts(transaction, table, "delete", preImages.length, sessionDeleteValueAt, "before", cascadeBudget);
5734
- const values = [...keys.values()];
5735
- if (keyStringByteLengths !== void 0) {
5736
- validatedStringByteLengths.set(values, keyStringByteLengths);
6351
+ const buffered = this.#scopeWrites.has(transaction) && keys.size < SCOPE_DIRECT_STAGE_ROWS && !(table.triggers ?? []).some((trigger) => trigger.event === "delete");
6352
+ let segmentId = null;
6353
+ if (buffered) {
6354
+ await this.#bufferScopeDelete(transaction, table, keys);
6355
+ } else {
6356
+ await this.#stageScopeDirectly(transaction, table);
6357
+ const values = [...keys.values()];
6358
+ if (keyStringByteLengths !== void 0) {
6359
+ validatedStringByteLengths.set(values, keyStringByteLengths);
6360
+ }
6361
+ segmentId = await this.#stageDeleteSegment(transaction, table, keyColumn, values);
5737
6362
  }
6363
+ await this.#stageTriggerDerivedInserts(transaction, table, "delete", preImages.length, sessionDeleteValueAt, "after", cascadeBudget);
6364
+ return { tableName: table.name, segmentId, rowCount: keys.size };
6365
+ }
6366
+ async #stageUpdateSegment(transaction, table, keyColumn, input) {
6367
+ const changedColumns = Object.keys(input.changes).sort();
6368
+ const columns = [keyColumn, ...changedColumns.map((name) => findColumn(table, name))];
6369
+ const segmentId = this.#createId();
6370
+ const columnBlockIds = {};
6371
+ const blockStager = new BoundedWriteBlockStager(transaction);
6372
+ const plannedColumns = columns.map((column) => writeColumnValues(column.type, column.id === keyColumn.id ? input.keys : input.changes[column.name] ?? []));
6373
+ const ranges = writeBlockRanges(plannedColumns, input.keys.length, this.#rowsPerBlock, this.#targetBlockBytes);
6374
+ for (const [columnIndex, column] of columns.entries()) {
6375
+ const values = column.id === keyColumn.id ? input.keys : input.changes[column.name] ?? [];
6376
+ const plannedColumn = plannedColumns[columnIndex];
6377
+ if (plannedColumn === void 0)
6378
+ throw new Error(`Write column disappeared: ${column.name}`);
6379
+ const blockIds = [];
6380
+ for (const [part, { start, end }] of ranges.entries()) {
6381
+ await blockStager.prepare(maximumWriteBlockStoredBytes(plannedColumn, start, end, this.#compression));
6382
+ const slice = values.slice(start, end);
6383
+ const bytes = await this.#encodeColumnBlock(column.id, asColumnInput(column.type, slice));
6384
+ const blockId = [
6385
+ "table",
6386
+ table.id,
6387
+ "segment",
6388
+ segmentId,
6389
+ "update-column",
6390
+ column.id,
6391
+ "part",
6392
+ String(part).padStart(6, "0")
6393
+ ].join("/");
6394
+ blockStager.add({ id: blockId, bytes });
6395
+ blockIds.push(blockId);
6396
+ }
6397
+ columnBlockIds[column.id] = blockIds;
6398
+ }
6399
+ await blockStager.stageWithSegments([
6400
+ {
6401
+ id: segmentId,
6402
+ tableId: table.id,
6403
+ transactionId: transaction.id,
6404
+ rowCount: input.keys.length,
6405
+ rowIdStart: 0n,
6406
+ rowIdEndExclusive: 0n,
6407
+ columnBlockIds,
6408
+ kind: "update",
6409
+ keyColumnId: keyColumn.id,
6410
+ level: 0,
6411
+ logicalOrder: 0,
6412
+ commitOrdinal: transaction.pendingSegmentCount,
6413
+ rowIdSpans: [],
6414
+ createdAt: dateIsoString(this.#now())
6415
+ }
6416
+ ]);
6417
+ return segmentId;
6418
+ }
6419
+ async #stageDeleteSegment(transaction, table, keyColumn, values) {
5738
6420
  const segmentId = this.#createId();
5739
6421
  const blockIds = [];
5740
6422
  const blockStager = new BoundedWriteBlockStager(transaction);
@@ -5762,7 +6444,7 @@ class MinnowDatabase {
5762
6444
  id: segmentId,
5763
6445
  tableId: table.id,
5764
6446
  transactionId: transaction.id,
5765
- rowCount: keys.size,
6447
+ rowCount: values.length,
5766
6448
  rowIdStart: 0n,
5767
6449
  rowIdEndExclusive: 0n,
5768
6450
  columnBlockIds: { [keyColumn.id]: blockIds },
@@ -5770,13 +6452,12 @@ class MinnowDatabase {
5770
6452
  keyColumnId: keyColumn.id,
5771
6453
  level: 0,
5772
6454
  logicalOrder: 0,
5773
- commitOrdinal: transaction.pendingSegmentIds.length,
6455
+ commitOrdinal: transaction.pendingSegmentCount,
5774
6456
  rowIdSpans: [],
5775
6457
  createdAt: dateIsoString(this.#now())
5776
6458
  }
5777
6459
  ]);
5778
- await this.#stageTriggerDerivedInserts(transaction, table, "delete", preImages.length, sessionDeleteValueAt, "after", cascadeBudget);
5779
- return { tableName: table.name, segmentId, rowCount: keys.size };
6460
+ return segmentId;
5780
6461
  }
5781
6462
  #notifyLiveCommit() {
5782
6463
  for (const set of this.#liveSets)
@@ -6181,6 +6862,9 @@ class MinnowDatabase {
6181
6862
  notes.push("BM25 scoring reads the full scan for corpus statistics; index pruning does not apply");
6182
6863
  }
6183
6864
  }
6865
+ const liveReasons = [];
6866
+ const liveState = await this.#liveMaintenancePlan(plan, await this.store.getCatalogProbe(), liveReasons);
6867
+ notes.push(liveState !== void 0 ? "live: maintained incrementally on change" : `live: re-executes on change: ${liveReasons.join("; ") || "the shape is not maintainable"}`);
6184
6868
  return `${renderPlan(plan)}
6185
6869
  ${notes.map((note) => `-- ${note}`).join("\n")}`;
6186
6870
  });
@@ -6505,7 +7189,8 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
6505
7189
  if (totalBytes > MAX_TRANSACTION_SAVEPOINT_BYTES) {
6506
7190
  throw new RangeError(`Transaction savepoints cannot retain more than ${String(MAX_TRANSACTION_SAVEPOINT_BYTES)} bytes`);
6507
7191
  }
6508
- open.savepoints.push({ name, checkpoint: open.session.checkpoint(), retainedBytes });
7192
+ const checkpoint = await open.session.checkpoint();
7193
+ open.savepoints.push({ name, checkpoint, retainedBytes });
6509
7194
  });
6510
7195
  return { kind: "transaction", action, name };
6511
7196
  }
@@ -6767,6 +7452,24 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
6767
7452
  const generatedColumns = new Set(table.columns.flatMap((column) => column.generatedValue === void 0 ? [] : [column.name]));
6768
7453
  const materializedRows = Array.from({ length: statement.rows.length }, (_, rowIndex) => columns.map((name) => generatedColumns.has(name) || name === filled.autoIncrement?.column.name && deferredAutoIncrement.has(rowIndex) ? { default: true } : filled.batch.columns[name]?.[rowIndex] ?? null));
6769
7454
  const keys = filled.batch.columns[keyColumn.name] ?? [];
7455
+ const presence = await writer?.stagedKeyPresence?.(table, keyColumn, keys.filter((value) => value !== null));
7456
+ if (presence !== void 0) {
7457
+ const taken = new Set(presence);
7458
+ return {
7459
+ ...statement,
7460
+ columns,
7461
+ rows: materializedRows.filter((_, index) => {
7462
+ const value = keys[index] ?? null;
7463
+ if (value === null)
7464
+ return true;
7465
+ const token = keyToken(keyColumn.type, value);
7466
+ if (taken.has(token))
7467
+ return false;
7468
+ taken.add(token);
7469
+ return true;
7470
+ })
7471
+ };
7472
+ }
6770
7473
  const plan = {
6771
7474
  sql: "(on conflict do nothing)",
6772
7475
  base: { table: table.name, alias: table.name },
@@ -6809,6 +7512,15 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
6809
7512
  const keys = insertBatchKeyValues(input, keyColumn.name);
6810
7513
  if (keys.length === 0)
6811
7514
  return;
7515
+ const present = await writer.stagedKeyPresence?.(table, keyColumn, keys.filter((value) => value !== null));
7516
+ if (present !== void 0) {
7517
+ for (const value of keys) {
7518
+ if (value !== null && present.has(keyToken(keyColumn.type, value))) {
7519
+ throw new UniqueConstraintError(table.name, keyColumn.name, value);
7520
+ }
7521
+ }
7522
+ return;
7523
+ }
6812
7524
  const plan = {
6813
7525
  sql: "(staged insert keys)",
6814
7526
  base: { table: table.name, alias: table.name },
@@ -7115,6 +7827,7 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
7115
7827
  normalizeDomainBatch(pendingTable, batch);
7116
7828
  validateBatch(pendingTable, batch);
7117
7829
  const endExclusive = nextRowId + BigInt(rows2.length);
7830
+ await this.#stageScopeDirectly(transaction, pendingTable);
7118
7831
  await this.#stageInsertSegment(transaction, pendingTable, batch, rows2.length, "insert", { start: nextRowId, endExclusive });
7119
7832
  nextRowId = endExclusive;
7120
7833
  stagedRows += rows2.length;
@@ -7947,13 +8660,14 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
7947
8660
  #streamedViewFactory(table, projectedColumns, segments, snapshot, storedBlocks, zonePruned = false) {
7948
8661
  const scanSegments = segments.filter((segment) => {
7949
8662
  const kind = segment.kind;
7950
- return kind === "insert" || kind === "base";
8663
+ return kind === "insert" || kind === "base" || kind === "upsert";
7951
8664
  });
7952
8665
  const mutationSegments = segments.filter((segment) => segment.kind === "update" || segment.kind === "delete");
7953
8666
  if (scanSegments.length + mutationSegments.length !== segments.length)
7954
8667
  return void 0;
8668
+ const replays = mutationSegments.length > 0 || segments.some((segment) => segment.kind === "upsert");
7955
8669
  const keyColumn = getUniqueKeyColumn(table);
7956
- if (mutationSegments.length > 0) {
8670
+ if (replays) {
7957
8671
  if (keyColumn === void 0)
7958
8672
  return void 0;
7959
8673
  const keyBlocksPresent = segments.every((segment) => segment.rowCount === 0 || (segment.columnBlockIds[keyColumn.id]?.length ?? 0) > 0);
@@ -7965,7 +8679,7 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
7965
8679
  return void 0;
7966
8680
  const scanRowCount = scanSegments.reduce((total, segment) => total + segment.rowCount, 0);
7967
8681
  return {
7968
- create: async (memory) => mutationSegments.length > 0 && keyColumn !== void 0 ? this.#createStreamedMutationTable(table, keyColumn, projectedColumns, segments, snapshot, memory, zonePruned, storedBlocks) : this.#createStreamedTable(table, projectedColumns, scanSegments, snapshot, scanRowCount, memory, storedBlocks)
8682
+ create: async (memory) => replays && keyColumn !== void 0 ? this.#createStreamedMutationTable(table, keyColumn, projectedColumns, segments, snapshot, memory, zonePruned, storedBlocks) : this.#createStreamedTable(table, projectedColumns, scanSegments, snapshot, scanRowCount, memory, storedBlocks)
7969
8683
  };
7970
8684
  }
7971
8685
  async #runPartitionedJoin(plan, budgetBytes, estimatedBuildBytes, baseView, buildView, buildTableName, buildKeyName, signal) {
@@ -8239,11 +8953,71 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
8239
8953
  async #createStreamedMutationTable(table, keyColumn, projectedColumns, baseSegments, snapshot, memory, zonePruned = false, storedBlocks) {
8240
8954
  const scanSegments = baseSegments.filter((segment) => {
8241
8955
  const kind = segment.kind;
8242
- return kind === "insert" || kind === "base";
8956
+ return kind === "insert" || kind === "base" || kind === "upsert";
8243
8957
  });
8244
8958
  const overlay = await this.#streamedOverlayState(table, keyColumn, baseSegments, scanSegments, snapshot, memory, zonePruned);
8245
- const { baseRows, dead, deadCount, patches, patchedSlots } = overlay;
8959
+ const { baseRows, dead, deadCount, lazyPatches, patchedSlots } = overlay;
8246
8960
  const hasPatches = patchedSlots.length > 0;
8961
+ const windowPatches = async (from, to) => {
8962
+ const needed = /* @__PURE__ */ new Set();
8963
+ const inRange = [];
8964
+ const winning = (layers, columnId) => {
8965
+ for (let index = layers.length - 1; index >= 0; index -= 1) {
8966
+ const candidate = layers[index];
8967
+ if (candidate === void 0)
8968
+ continue;
8969
+ const blockId = candidate.segment.columnBlockIds[columnId]?.[candidate.blockIndex];
8970
+ if (blockId !== void 0)
8971
+ return { blockId, row: candidate.row };
8972
+ }
8973
+ return void 0;
8974
+ };
8975
+ for (const [slot, layers] of lazyPatches) {
8976
+ if (slot < from || slot >= to)
8977
+ continue;
8978
+ inRange.push([slot, layers]);
8979
+ for (const column of projectedColumns) {
8980
+ const hit = winning(layers, column.id);
8981
+ if (hit !== void 0)
8982
+ needed.add(hit.blockId);
8983
+ }
8984
+ }
8985
+ const empty = /* @__PURE__ */ new Map();
8986
+ if (inRange.length === 0)
8987
+ return empty;
8988
+ const ids = [...needed];
8989
+ const decoded = await this.#decodedBlocksThroughCache(ids, snapshot);
8990
+ const vectors = /* @__PURE__ */ new Map();
8991
+ ids.forEach((id, index) => {
8992
+ const block = decoded[index];
8993
+ if (block === void 0)
8994
+ throw new Error(`Visible block is missing: ${id}`);
8995
+ vectors.set(id, this.#blockColumnVector(id, block));
8996
+ });
8997
+ memory.tally(inRange.length * 96, "Streamed mutation replay");
8998
+ const resolved = /* @__PURE__ */ new Map();
8999
+ const plain = /* @__PURE__ */ new Map();
9000
+ for (const [slot, layers] of inRange) {
9001
+ const slotPatches = /* @__PURE__ */ new Map();
9002
+ for (const column of projectedColumns) {
9003
+ const hit = winning(layers, column.id);
9004
+ if (hit === void 0)
9005
+ continue;
9006
+ let vector = plain.get(hit.blockId);
9007
+ if (vector === void 0) {
9008
+ const raw = vectors.get(hit.blockId);
9009
+ if (raw === void 0)
9010
+ throw new Error(`Visible block is missing: ${hit.blockId}`);
9011
+ vector = plainTextExecutionVector(column, raw);
9012
+ plain.set(hit.blockId, vector);
9013
+ }
9014
+ slotPatches.set(column.id, { vector, row: hit.row });
9015
+ }
9016
+ if (slotPatches.size > 0)
9017
+ resolved.set(slot, slotPatches);
9018
+ }
9019
+ return resolved;
9020
+ };
8247
9021
  const outputRows = baseRows - deadCount;
8248
9022
  const inner = this.#createStreamedTable(table, projectedColumns, scanSegments, snapshot, baseRows, memory, storedBlocks);
8249
9023
  if (deadCount === 0 && !hasPatches)
@@ -8282,14 +9056,30 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
8282
9056
  }
8283
9057
  const baseStart = cursorBase;
8284
9058
  const innerEnd = await inner.load(baseStart, baseRows - baseStart);
8285
- const baseEnd = typeof innerEnd === "number" ? Math.min(innerEnd, baseRows) : baseRows;
8286
- if (baseEnd <= baseStart)
9059
+ const innerBaseEnd = typeof innerEnd === "number" ? Math.min(innerEnd, baseRows) : baseRows;
9060
+ if (innerBaseEnd <= baseStart)
8287
9061
  throw new Error(`Column row count mismatch: ${table.name}`);
8288
- const deadInWindow = bitmapCountRange(dead, baseStart, baseEnd);
8289
- const patchedInWindow = hasPatches ? sortedCountRange(patchedSlots, baseStart, baseEnd) : 0;
8290
- const liveRows = baseEnd - baseStart - deadInWindow;
9062
+ let baseEnd = innerBaseEnd;
9063
+ let deadInWindow = bitmapCountRange(dead, baseStart, baseEnd);
9064
+ let patchedInWindow = hasPatches ? sortedCountRange(patchedSlots, baseStart, baseEnd) : 0;
8291
9065
  const untouched = deadInWindow === 0 && patchedInWindow === 0;
9066
+ if (!untouched) {
9067
+ let live = 0;
9068
+ let row = baseStart;
9069
+ while (row < innerBaseEnd && live < length) {
9070
+ if (!bitmapHasValue(dead, row))
9071
+ live += 1;
9072
+ row += 1;
9073
+ }
9074
+ if (row < baseEnd) {
9075
+ baseEnd = row;
9076
+ deadInWindow = bitmapCountRange(dead, baseStart, baseEnd);
9077
+ patchedInWindow = hasPatches ? sortedCountRange(patchedSlots, baseStart, baseEnd) : 0;
9078
+ }
9079
+ }
9080
+ const liveRows = baseEnd - baseStart - deadInWindow;
8292
9081
  const runs = untouched ? void 0 : overlayWindowRuns(dead, patchedSlots, baseStart, baseEnd, patchedInWindow);
9082
+ const resolvedPatches = runs === void 0 || !hasPatches ? void 0 : await windowPatches(baseStart, baseEnd);
8293
9083
  const targets = [];
8294
9084
  try {
8295
9085
  for (const state of states) {
@@ -8303,7 +9093,7 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
8303
9093
  throw new Error(`Column row count mismatch: ${state.column.name}`);
8304
9094
  }
8305
9095
  const replacements = [];
8306
- const fields = runs === void 0 ? overlayWindowView(innerVector, offset, liveRows, memory, state.column, replacements) : overlayWindowCompacted(innerVector, innerWindow.start, runs, liveRows, hasPatches ? patches : void 0, state.column, memory, replacements);
9096
+ const fields = runs === void 0 ? overlayWindowView(innerVector, offset, liveRows, memory, state.column, replacements) : overlayWindowCompacted(innerVector, innerWindow.start, runs, liveRows, resolvedPatches, state.column, memory, replacements);
8307
9097
  fields.window = { start, length: liveRows };
8308
9098
  targets.push({ state, fields, replacements });
8309
9099
  }
@@ -8377,12 +9167,11 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
8377
9167
  }
8378
9168
  async #buildStreamedOverlayState(table, keyColumn, baseSegments, scanSegments, snapshot, memory, zonePruned) {
8379
9169
  const deltaSegments = baseSegments.filter(mutationSegmentKind);
9170
+ const upsertSegments = baseSegments.filter((segment) => segment.kind === "upsert");
8380
9171
  const deltaBlockIds = /* @__PURE__ */ new Set();
8381
- for (const segment of deltaSegments) {
8382
- for (const column of table.columns) {
8383
- for (const blockId of segment.columnBlockIds[column.id] ?? [])
8384
- deltaBlockIds.add(blockId);
8385
- }
9172
+ for (const segment of [...deltaSegments, ...upsertSegments]) {
9173
+ for (const blockId of segment.columnBlockIds[keyColumn.id] ?? [])
9174
+ deltaBlockIds.add(blockId);
8386
9175
  }
8387
9176
  const decodedDeltaBlocks = /* @__PURE__ */ new Map();
8388
9177
  if (deltaBlockIds.size > 0) {
@@ -8413,11 +9202,32 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
8413
9202
  return vector;
8414
9203
  };
8415
9204
  const mutationKeyVectors = /* @__PURE__ */ new Map();
8416
- const mutationChangedVectors = /* @__PURE__ */ new Map();
9205
+ const blockStarts = /* @__PURE__ */ new Map();
9206
+ const layoutOf = (segment) => {
9207
+ const starts = [];
9208
+ let start = 0;
9209
+ for (const blockId of segment.columnBlockIds[keyColumn.id] ?? []) {
9210
+ starts.push(start);
9211
+ const decoded = decodedDeltaBlocks.get(blockId);
9212
+ if (decoded === void 0)
9213
+ throw new Error(`Visible block is missing: ${blockId}`);
9214
+ start += this.#blockColumnVector(blockId, decoded).length;
9215
+ }
9216
+ if (start !== segment.rowCount) {
9217
+ throw new Error(`Column row count mismatch: ${keyColumn.name}`);
9218
+ }
9219
+ return starts;
9220
+ };
9221
+ const locate = (segment, row) => {
9222
+ const starts = blockStarts.get(segment.id) ?? [];
9223
+ let blockIndex = starts.length - 1;
9224
+ while (blockIndex > 0 && (starts[blockIndex] ?? 0) > row)
9225
+ blockIndex -= 1;
9226
+ return { blockIndex, rowInBlock: row - (starts[blockIndex] ?? 0) };
9227
+ };
8417
9228
  const touched = /* @__PURE__ */ new Set();
8418
- let retainedBytes = 0;
9229
+ const retainedBytes = 0;
8419
9230
  for (const segment of deltaSegments) {
8420
- const kind = segment.kind;
8421
9231
  const keyVector = await deltaVector(keyColumn, segment);
8422
9232
  memory.reserve(columnVectorRetainedBytes(keyVector), "Streamed mutation replay");
8423
9233
  mutationKeyVectors.set(segment.id, keyVector);
@@ -8425,21 +9235,16 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
8425
9235
  for (let row = 0; row < segment.rowCount; row += 1) {
8426
9236
  touched.add(readMutationKey(row));
8427
9237
  }
8428
- if (kind === "update") {
8429
- const changed = /* @__PURE__ */ new Map();
8430
- for (const column of table.columns) {
8431
- if (column.id === keyColumn.id)
8432
- continue;
8433
- if ((segment.columnBlockIds[column.id]?.length ?? 0) === 0)
8434
- continue;
8435
- const vector = await deltaVector(column, segment);
8436
- const bytes = columnVectorRetainedBytes(vector);
8437
- memory.reserve(bytes, "Streamed mutation replay");
8438
- retainedBytes += bytes;
8439
- changed.set(column.id, plainTextExecutionVector(column, vector));
8440
- }
8441
- mutationChangedVectors.set(segment.id, changed);
8442
- }
9238
+ if (segment.kind === "update")
9239
+ blockStarts.set(segment.id, layoutOf(segment));
9240
+ }
9241
+ for (const segment of upsertSegments) {
9242
+ const keyVector = await deltaVector(keyColumn, segment);
9243
+ memory.reserve(columnVectorRetainedBytes(keyVector), "Streamed mutation replay");
9244
+ mutationKeyVectors.set(segment.id, keyVector);
9245
+ const readMutationKey = requiredColumnVectorKeyReader(keyVector);
9246
+ for (let row = 0; row < segment.rowCount; row += 1)
9247
+ touched.add(readMutationKey(row));
8443
9248
  }
8444
9249
  const touchedByScanSegment = /* @__PURE__ */ new Map();
8445
9250
  const touchedPredicate = touchedKeyPredicate(keyColumn, touched);
@@ -8449,7 +9254,7 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
8449
9254
  const entries = [];
8450
9255
  touchedByScanSegment.set(segment.id, entries);
8451
9256
  let segmentRows = 0;
8452
- for (const blockId of segment.columnBlockIds[keyColumn.id] ?? []) {
9257
+ for (const [blockIndex, blockId] of (segment.columnBlockIds[keyColumn.id] ?? []).entries()) {
8453
9258
  const description = keyDescriptions.get(blockId);
8454
9259
  if (touchedPredicate !== void 0 && description !== void 0) {
8455
9260
  if (description.type !== keyColumn.type) {
@@ -8471,8 +9276,9 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
8471
9276
  const readBlockKey = requiredColumnVectorKeyReader(blockVector);
8472
9277
  for (let row = 0; row < rows; row += 1) {
8473
9278
  const key = readBlockKey(row);
8474
- if (touched.has(key))
8475
- entries.push({ key, slot: baseRows + segmentRows + row });
9279
+ if (touched.has(key)) {
9280
+ entries.push({ key, slot: baseRows + segmentRows + row, blockIndex, row });
9281
+ }
8476
9282
  }
8477
9283
  segmentRows += rows;
8478
9284
  }
@@ -8485,8 +9291,15 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
8485
9291
  const dead = new Uint8Array(Math.ceil(baseRows / 8));
8486
9292
  memory.reserve(dead.byteLength, "Streamed mutation replay");
8487
9293
  const slotByKey = /* @__PURE__ */ new Map();
8488
- const patches = /* @__PURE__ */ new Map();
9294
+ const lazyPatches = /* @__PURE__ */ new Map();
8489
9295
  let deadCount = 0;
9296
+ const layer = (slot, patch) => {
9297
+ const layers2 = lazyPatches.get(slot);
9298
+ if (layers2 === void 0)
9299
+ lazyPatches.set(slot, [patch]);
9300
+ else
9301
+ layers2.push(patch);
9302
+ };
8490
9303
  for (const segment of baseSegments) {
8491
9304
  const kind = segment.kind;
8492
9305
  if (kind === "insert" || kind === "base") {
@@ -8498,6 +9311,21 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
8498
9311
  }
8499
9312
  continue;
8500
9313
  }
9314
+ if (kind === "upsert") {
9315
+ for (const entry of touchedByScanSegment.get(segment.id) ?? []) {
9316
+ const existing = slotByKey.get(entry.key);
9317
+ if (existing === void 0) {
9318
+ slotByKey.set(entry.key, entry.slot);
9319
+ continue;
9320
+ }
9321
+ layer(existing, { segment, blockIndex: entry.blockIndex, row: entry.row });
9322
+ if (!bitmapHasValue(dead, entry.slot)) {
9323
+ setBitmapValue(dead, entry.slot);
9324
+ deadCount += 1;
9325
+ }
9326
+ }
9327
+ continue;
9328
+ }
8501
9329
  const keyVector = mutationKeyVectors.get(segment.id);
8502
9330
  if (keyVector === void 0) {
8503
9331
  throw new Error(`Mutation segment key vector is missing: ${segment.id}`);
@@ -8510,13 +9338,12 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
8510
9338
  if (slot !== void 0 && !bitmapHasValue(dead, slot)) {
8511
9339
  setBitmapValue(dead, slot);
8512
9340
  deadCount += 1;
8513
- patches.delete(slot);
9341
+ lazyPatches.delete(slot);
8514
9342
  }
8515
9343
  slotByKey.delete(key);
8516
9344
  }
8517
9345
  continue;
8518
9346
  }
8519
- const changed = mutationChangedVectors.get(segment.id) ?? /* @__PURE__ */ new Map();
8520
9347
  for (let row = 0; row < segment.rowCount; row += 1) {
8521
9348
  const slot = slotByKey.get(readKey(row));
8522
9349
  if (slot === void 0) {
@@ -8524,27 +9351,22 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
8524
9351
  continue;
8525
9352
  throw new Error(`Update segment references a missing key: ${segment.id}`);
8526
9353
  }
8527
- let slotPatches = patches.get(slot);
8528
- if (slotPatches === void 0) {
8529
- slotPatches = /* @__PURE__ */ new Map();
8530
- patches.set(slot, slotPatches);
8531
- }
8532
- for (const [columnId, vector] of changed)
8533
- slotPatches.set(columnId, { vector, row });
9354
+ const { blockIndex, rowInBlock } = locate(segment, row);
9355
+ layer(slot, { segment, blockIndex, row: rowInBlock });
8534
9356
  }
8535
9357
  }
8536
- let patchCells = 0;
8537
- for (const slotPatches of patches.values())
8538
- patchCells += slotPatches.size;
8539
- memory.tally(patches.size * 96 + patchCells * 48, "Streamed mutation replay");
8540
- const patchedSlots = Uint32Array.from(patches.keys()).sort();
9358
+ let layers = 0;
9359
+ for (const slotLayers of lazyPatches.values())
9360
+ layers += slotLayers.length;
9361
+ memory.tally(lazyPatches.size * 96 + layers * 48, "Streamed mutation replay");
9362
+ const patchedSlots = Uint32Array.from(lazyPatches.keys()).sort();
8541
9363
  return {
8542
9364
  baseRows,
8543
9365
  deadCount,
8544
9366
  dead,
8545
- patches,
9367
+ lazyPatches,
8546
9368
  patchedSlots,
8547
- bytes: dead.byteLength + retainedBytes + patchedSlots.byteLength + patches.size * 96 + patchCells * 48
9369
+ bytes: dead.byteLength + retainedBytes + patchedSlots.byteLength + lazyPatches.size * 96 + layers * 48
8548
9370
  };
8549
9371
  }
8550
9372
  async #executeBlock(block, snapshot, visibility, memory, realTables, typedSchemas, extraInputs, cacheResults = true, allowSpill = true, forceSpill = false, spillPageRows, signal) {
@@ -9197,6 +10019,7 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
9197
10019
  message: error instanceof Error ? error.message : String(error),
9198
10020
  at
9199
10021
  };
10022
+ this.#reportBackgroundError(error, "auto collection");
9200
10023
  this.#scheduleAutoCollectionRetry();
9201
10024
  }).finally(() => {
9202
10025
  if (this.#autoCollectionTask === run)
@@ -9217,6 +10040,12 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
9217
10040
  this.#autoCollectionTask = run;
9218
10041
  void run;
9219
10042
  }
10043
+ #reportBackgroundError(error, context) {
10044
+ try {
10045
+ this.#onBackgroundError?.(error, context);
10046
+ } catch {
10047
+ }
10048
+ }
9220
10049
  #scheduleAutoCollectionRetry() {
9221
10050
  if (this.#closed || !this.#autoCollect || this.#autoCollectionRetryTimer !== void 0)
9222
10051
  return;
@@ -13660,14 +14489,17 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
13660
14489
  async #visibleSegmentRecordsUncached(table, snapshot, catalog) {
13661
14490
  const segments = catalog === void 0 ? await listTableSegmentsPaged(this.store, table.id) : catalog.segmentsByTable.get(table.id) ?? [];
13662
14491
  const transactions = catalog?.transactions ?? new Map((await this.#transactionRecordsForSegments(segments)).map((record) => [record.id, record]));
14492
+ const overlay = [];
13663
14493
  const versionEligible = segments.filter((segment) => {
13664
- if (segment.transactionId === catalog?.overlayTransactionId)
13665
- return true;
14494
+ if (segment.transactionId === catalog?.overlayTransactionId) {
14495
+ overlay.push(segment);
14496
+ return false;
14497
+ }
13666
14498
  const committedVersion = transactions.get(segment.transactionId)?.committedVersion;
13667
14499
  return snapshot.version !== null && committedVersion !== null && committedVersion !== void 0 && committedVersion <= snapshot.version;
13668
14500
  });
13669
- const visible = await filterSnapshotSegments(snapshot, versionEligible);
13670
- return [...visible].sort((left, right) => {
14501
+ const visible = [...await filterSnapshotSegments(snapshot, versionEligible), ...overlay];
14502
+ return visible.sort((left, right) => {
13671
14503
  const leftVersion = transactions.get(left.transactionId)?.committedVersion ?? -1;
13672
14504
  const rightVersion = transactions.get(right.transactionId)?.committedVersion ?? -1;
13673
14505
  const leftOrder = left.logicalOrder;
@@ -13675,8 +14507,8 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
13675
14507
  return leftOrder - rightOrder || leftVersion - rightVersion || left.commitOrdinal - right.commitOrdinal || left.id.localeCompare(right.id);
13676
14508
  });
13677
14509
  }
13678
- async #transactionRecordsForSegments(segments) {
13679
- const transactionIds = [...new Set(segments.map((segment) => segment.transactionId))];
14510
+ async #transactionRecordsForSegments(segments, excludeTransactionId) {
14511
+ const transactionIds = [...new Set(segments.map((segment) => segment.transactionId))].filter((id) => id !== excludeTransactionId);
13680
14512
  const records = [];
13681
14513
  for (let start = 0; start < transactionIds.length; start += 64) {
13682
14514
  const window = transactionIds.slice(start, start + 64);
@@ -15593,6 +16425,24 @@ function boundedExpiryMilliseconds(nowMs, ttlMs) {
15593
16425
  }
15594
16426
  return expiresAt;
15595
16427
  }
16428
+ function plainLiteralValue(expression) {
16429
+ if (expression.kind !== "literal" || expression.internalSqlValue === true || expression.sqlDomain !== void 0 || expression.exactText !== void 0 || expression.decimal === true) {
16430
+ return void 0;
16431
+ }
16432
+ return expression.value;
16433
+ }
16434
+ function valueMatchesColumnType(column, value) {
16435
+ switch (column.type) {
16436
+ case "number":
16437
+ return typeof value === "number" && Number.isFinite(value);
16438
+ case "string":
16439
+ return typeof value === "string";
16440
+ case "boolean":
16441
+ return typeof value === "boolean";
16442
+ case "datetime":
16443
+ return value instanceof Date;
16444
+ }
16445
+ }
15596
16446
  function keyToken(type, value) {
15597
16447
  if (value === null)
15598
16448
  throw new TypeError("Unique key cannot be null");
@@ -16240,9 +17090,17 @@ function overlayWindowCompacted(inner, innerWindowStart, steps, rows, patches, c
16240
17090
  const values = inner.kind === "boolean" ? new Uint8Array(rows) : inner.kind === "string" ? void 0 : new Float64Array(rows);
16241
17091
  const codes = inner.kind === "string" ? new Uint32Array(rows) : void 0;
16242
17092
  codes?.fill(NULL_STRING_VECTOR_CODE);
16243
- let dictionary = inner.kind === "string" ? inner.dictionary : void 0;
16244
- let dictionaryIndex;
16245
- let dictionaryCopied = false;
17093
+ let patched = false;
17094
+ if (inner.kind === "string" && patches !== void 0) {
17095
+ for (let index = 0; index < steps.length; index += 2) {
17096
+ if ((steps[index + 1] ?? 0) === 0 && patches.get(steps[index] ?? 0)?.has(column.id) === true) {
17097
+ patched = true;
17098
+ break;
17099
+ }
17100
+ }
17101
+ }
17102
+ const dictionary = inner.kind === "string" ? patched ? [] : inner.dictionary : void 0;
17103
+ const dictionaryIndex = patched ? /* @__PURE__ */ new Map() : void 0;
16246
17104
  const target = codes !== void 0 ? { kind: "string", length: rows, validity, codes, dictionary: dictionary ?? [] } : { kind: inner.kind, length: rows, validity, values };
16247
17105
  let out = 0;
16248
17106
  for (let index = 0; index < steps.length; index += 2) {
@@ -16251,22 +17109,22 @@ function overlayWindowCompacted(inner, innerWindowStart, steps, rows, patches, c
16251
17109
  const patch = length === 0 ? patches?.get(start)?.get(column.id) : void 0;
16252
17110
  if (patch === void 0) {
16253
17111
  const count = Math.max(1, length);
16254
- copyVectorSpan(inner, start - innerWindowStart, count, target, out);
17112
+ if (dictionaryIndex === void 0) {
17113
+ copyVectorSpan(inner, start - innerWindowStart, count, target, out);
17114
+ } else {
17115
+ for (let row = 0; row < count; row += 1) {
17116
+ copyColumnVectorValue(inner, start - innerWindowStart + row, target, out + row, dictionaryIndex);
17117
+ }
17118
+ }
16255
17119
  out += count;
16256
17120
  continue;
16257
17121
  }
16258
- if (target.kind === "string" && !dictionaryCopied) {
16259
- dictionary = [...dictionary ?? []];
16260
- target.dictionary = dictionary;
16261
- dictionaryIndex = new Map(dictionary.map((value, code) => [value, code]));
16262
- dictionaryCopied = true;
16263
- }
16264
17122
  copyColumnVectorValue(patch.vector, patch.row, target, out, dictionaryIndex);
16265
17123
  out += 1;
16266
17124
  }
16267
17125
  if (out !== rows)
16268
17126
  throw new Error(`Column row count mismatch: ${column.name}`);
16269
- if (dictionaryCopied && dictionary !== void 0) {
17127
+ if (patched && dictionary !== void 0) {
16270
17128
  let dictionaryBytes = 0;
16271
17129
  for (const value of dictionary)
16272
17130
  dictionaryBytes += 16 + value.length * 2;