@minnowdb/core 0.9.1 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/engine/auto-store.d.ts +52 -0
  2. package/dist/engine/auto-store.js +157 -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-audit-harness.js +123 -0
  6. package/dist/engine/client.d.ts +55 -6
  7. package/dist/engine/client.js +176 -46
  8. package/dist/engine/database.d.ts +15 -1
  9. package/dist/engine/database.js +1276 -251
  10. package/dist/engine/errors.d.ts +61 -2
  11. package/dist/engine/errors.js +116 -3
  12. package/dist/engine/index.d.ts +1 -0
  13. package/dist/engine/index.js +2 -0
  14. package/dist/engine/live.d.ts +24 -1
  15. package/dist/engine/live.js +33 -9
  16. package/dist/engine/scope-write-set.js +36 -0
  17. package/dist/engine/worker-auto.d.ts +1 -0
  18. package/dist/engine/worker-auto.js +3 -0
  19. package/dist/engine/worker-host.d.ts +2 -1
  20. package/dist/engine/worker-host.js +19 -1
  21. package/dist/engine/worker-server.d.ts +53 -1
  22. package/dist/engine/worker-server.js +122 -19
  23. package/dist/engine/worker-store-auto.js +36 -0
  24. package/dist/engine/worker-store-opfs.js +3 -2
  25. package/dist/engine/write-coordinator.js +44 -2
  26. package/dist/storage/indexeddb-audit-helpers.js +269 -0
  27. package/dist/storage/indexeddb.js +599 -374
  28. package/dist/storage/opfs/coordination-helpers.js +54 -0
  29. package/dist/storage/opfs/index.d.ts +1 -1
  30. package/dist/storage/opfs/index.js +3 -2
  31. package/dist/storage/opfs/leader.js +243 -17
  32. package/dist/storage/opfs/power-loss-model.js +62 -0
  33. package/dist/storage/opfs/rpc.js +24 -43
  34. package/dist/storage/opfs/store.d.ts +32 -0
  35. package/dist/storage/opfs/store.js +531 -65
  36. package/dist/storage/toolkit/record-core.js +67 -38
  37. package/dist/storage/toolkit/wal.js +16 -0
  38. package/dist/storage/toolkit/wire.d.ts +1 -1
  39. package/dist/storage/toolkit/wire.js +4 -4
  40. package/dist/storage/types.d.ts +31 -10
  41. package/dist/storage/types.js +27 -16
  42. package/dist/testing/opfs-shim.js +14 -6
  43. package/dist/transactions/index.d.ts +19 -0
  44. package/dist/transactions/index.js +99 -25
  45. package/dist/worker-protocol/index.d.ts +50 -2
  46. package/dist/worker-protocol/index.js +106 -4
  47. 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,13 @@ 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
+ flushedWork: 0,
5230
+ flushing: Promise.resolve()
5231
+ });
5143
5232
  let closed = false;
5144
5233
  let accepting = true;
5145
5234
  let tail = Promise.resolve();
@@ -5152,26 +5241,30 @@ class MinnowDatabase {
5152
5241
  signal.throwIfAborted();
5153
5242
  if (closed)
5154
5243
  throw new Error("The write scope has ended");
5244
+ poisoned ??= this.#scopeWrites.get(transaction)?.failure;
5155
5245
  if (poisoned !== void 0) {
5156
5246
  throw new Error("The write scope already failed and can only roll back", {
5157
5247
  cause: poisoned
5158
5248
  });
5159
5249
  }
5160
5250
  };
5251
+ const ownStagedWork = () => transaction.stagedWorkCount - (this.#scopeWrites.get(transaction)?.flushedWork ?? 0);
5161
5252
  const guarded = async (run) => {
5162
- const before = transaction.stagedWorkCount;
5253
+ const before = ownStagedWork();
5254
+ const generation = this.#scopeWriteGeneration(transaction);
5163
5255
  try {
5164
5256
  return await run();
5165
5257
  } catch (error) {
5166
- if (transaction.stagedWorkCount !== before)
5258
+ if (ownStagedWork() !== before || this.#scopeWriteGeneration(transaction) !== generation) {
5167
5259
  poisoned ??= error;
5260
+ }
5168
5261
  throw error;
5169
5262
  }
5170
5263
  };
5171
5264
  const operations = {
5172
5265
  query: async (sql, options2) => {
5173
5266
  open();
5174
- return this.#withReadReservation(() => this.#sessionQuery(transaction, sql, options2));
5267
+ return externalizeQueryResult(await this.#withReadReservation(() => this.#sessionQuery(transaction, sql, options2)));
5175
5268
  },
5176
5269
  execute: async (sql, params) => {
5177
5270
  open();
@@ -5179,7 +5272,7 @@ class MinnowDatabase {
5179
5272
  if (compiled.kind === "select") {
5180
5273
  return this.#withReadReservation(async () => ({
5181
5274
  kind: "rows",
5182
- result: await this.#sessionQuery(transaction, compiled.sql, params === void 0 ? {} : { params })
5275
+ result: externalizeQueryResult(await this.#sessionQuery(transaction, compiled.sql, params === void 0 ? {} : { params }))
5183
5276
  }));
5184
5277
  }
5185
5278
  const statement = bindStatementParameters(compiled, params);
@@ -5189,7 +5282,15 @@ class MinnowDatabase {
5189
5282
  if (!isTransactionalStatement(statement)) {
5190
5283
  throw new TypeError(`${statement.kind.toUpperCase().replace("-", " ")} is not allowed inside a write scope`);
5191
5284
  }
5192
- return writer.executeStatement(statement);
5285
+ if (statement.kind === "update" || statement.kind === "delete") {
5286
+ const keyed = await this.#scopeKeyedMutation(transaction, statement);
5287
+ if (keyed !== void 0) {
5288
+ return guarded(() => this.#runScopeKeyedMutation(transaction, keyed, () => {
5289
+ staged += 1;
5290
+ }));
5291
+ }
5292
+ }
5293
+ return externalizeExecuteResult(await writer.executeStatement(statement));
5193
5294
  },
5194
5295
  insertBatch: async (tableName, input) => {
5195
5296
  open();
@@ -5216,18 +5317,20 @@ class MinnowDatabase {
5216
5317
  ...operations,
5217
5318
  queryPlan: (plan) => this.#withReadReservation(async () => {
5218
5319
  const probe = transaction.initialCatalogProbe;
5219
- return transaction.stagedWorkCount === 0 && probe !== void 0 ? this.#queryCompiled(plan, {}, probe) : this.#sessionQueryPlan(transaction, await this.#applyCatalogRewrites(plan));
5320
+ return transaction.stagedWorkCount === 0 && !this.#hasPendingScopeWrites(transaction) && probe !== void 0 ? this.#queryCompiled(plan, {}, probe) : this.#sessionQueryPlan(transaction, await this.#applyCatalogRewrites(plan));
5220
5321
  }),
5221
5322
  queryFirstColumn: (plan) => this.#withReadReservation(async () => {
5222
5323
  const probe = transaction.initialCatalogProbe;
5223
- return transaction.stagedWorkCount === 0 && probe !== void 0 ? this.#queryCompiledFirstColumn(plan, probe) : void 0;
5324
+ return transaction.stagedWorkCount === 0 && !this.#hasPendingScopeWrites(transaction) && probe !== void 0 ? this.#queryCompiledFirstColumn(plan, probe) : void 0;
5224
5325
  }),
5225
5326
  executeStatement: (statement) => {
5226
5327
  open();
5227
5328
  return guarded(() => this.runStatement(statement, { writer }));
5228
5329
  },
5229
- checkpoint: () => {
5330
+ stagedKeyPresence: (table, keyColumn, keys) => this.#scopeKeyPresence(transaction, table, keyColumn, keys),
5331
+ checkpoint: async () => {
5230
5332
  open();
5333
+ await this.#flushScopeWriteSets(transaction);
5231
5334
  return transaction.checkpoint();
5232
5335
  },
5233
5336
  checkpointRetainedBytes: () => {
@@ -5237,7 +5340,15 @@ class MinnowDatabase {
5237
5340
  rollbackTo: async (checkpoint) => {
5238
5341
  if (closed)
5239
5342
  throw new Error("The write scope has ended");
5240
- await transaction.rollbackTo(checkpoint);
5343
+ try {
5344
+ await transaction.rollbackTo(checkpoint);
5345
+ } catch (error) {
5346
+ const state = this.#scopeWrites.get(transaction);
5347
+ if (state !== void 0)
5348
+ state.failure ??= error;
5349
+ throw error;
5350
+ }
5351
+ this.#discardScopeWriteSets(transaction);
5241
5352
  poisoned = void 0;
5242
5353
  staged = transaction.stagedWorkCount === 0 ? 0 : 1;
5243
5354
  }
@@ -5290,6 +5401,8 @@ class MinnowDatabase {
5290
5401
  queryPlan: (plan) => enqueue(() => writer.queryPlan(plan), "read"),
5291
5402
  queryFirstColumn: (plan) => enqueue(() => writer.queryFirstColumn(plan), "read"),
5292
5403
  executeStatement: (statement) => enqueue(() => writer.executeStatement(statement)),
5404
+ checkpoint: () => enqueue(() => writer.checkpoint()),
5405
+ stagedKeyPresence: (table, keyColumn, keys) => enqueue(async () => writer.stagedKeyPresence?.(table, keyColumn, keys), "read"),
5293
5406
  rollbackTo: (checkpoint) => enqueue(() => writer.rollbackTo(checkpoint), "rollback")
5294
5407
  };
5295
5408
  try {
@@ -5298,6 +5411,7 @@ class MinnowDatabase {
5298
5411
  await tail;
5299
5412
  signal.throwIfAborted();
5300
5413
  closed = true;
5414
+ poisoned ??= this.#scopeWrites.get(transaction)?.failure;
5301
5415
  if (poisoned !== void 0) {
5302
5416
  throw new Error(`The write scope failed mid-stage and was rolled back: ${poisoned instanceof Error ? poisoned.message : "staging failed"}`, { cause: poisoned });
5303
5417
  }
@@ -5305,6 +5419,7 @@ class MinnowDatabase {
5305
5419
  await transaction.abort();
5306
5420
  return { result, version: await this.store.getCurrentManifestVersion() };
5307
5421
  }
5422
+ await this.#finishScopeWriteSets(transaction);
5308
5423
  for (let attempt = 0; attempt <= this.#maxCommitRetries; attempt += 1) {
5309
5424
  try {
5310
5425
  const manifest = await transaction.commit();
@@ -5330,9 +5445,33 @@ class MinnowDatabase {
5330
5445
  }
5331
5446
  }
5332
5447
  async #sessionQuery(transaction, sql, options = {}) {
5448
+ const requested = options;
5333
5449
  options = this.#effectiveQueryOptions(options);
5334
5450
  throwIfAborted(options.signal);
5335
- const bound = bindPlanParameters(this.#compileCached(sql), options.params);
5451
+ const compiled = this.#compileCached(sql);
5452
+ 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)) {
5453
+ const template = cachedPointReadTemplate(compiled);
5454
+ if (template !== null) {
5455
+ const shape = resolvePointReadShape(template, options.params ?? []);
5456
+ if (shape !== void 0) {
5457
+ let point;
5458
+ try {
5459
+ pointReadTestHooks.attempted += 1;
5460
+ const table = await this.#findTable(shape.table, transaction);
5461
+ point = await this.#withSessionVisibility(transaction, [table], options, (snapshot, visibility, realTables) => this.#pointReadAtSnapshot(shape, snapshot, realTables, visibility, options));
5462
+ } catch (error) {
5463
+ if (!(error instanceof UnknownTableError))
5464
+ throw error;
5465
+ }
5466
+ throwIfAborted(options.signal);
5467
+ if (point !== void 0) {
5468
+ pointReadTestHooks.served += 1;
5469
+ return point;
5470
+ }
5471
+ }
5472
+ }
5473
+ }
5474
+ const bound = bindPlanParameters(compiled, options.params);
5336
5475
  const plan = await this.#applyCatalogRewrites(bound);
5337
5476
  throwIfAborted(options.signal);
5338
5477
  return this.#sessionQueryPlan(transaction, plan, options);
@@ -5341,13 +5480,32 @@ class MinnowDatabase {
5341
5480
  options = this.#effectiveQueryOptions(options);
5342
5481
  throwIfAborted(options.signal);
5343
5482
  const names = collectRealTableNames(plan);
5344
- const tables = await Promise.all(names.map((name) => this.#findTable(name)));
5483
+ const tables = await Promise.all(names.map((name) => this.#findTable(name, transaction)));
5345
5484
  throwIfAborted(options.signal);
5485
+ return this.#withSessionVisibility(transaction, tables, options, (snapshot, visibility, realTables) => this.#queryAtVisibility(plan, snapshot, visibility, realTables, true, options));
5486
+ }
5487
+ #scopeCommittedListings = /* @__PURE__ */ new WeakMap();
5488
+ async #scopeCommittedSegments(transaction, table) {
5489
+ let byTable = this.#scopeCommittedListings.get(transaction);
5490
+ if (byTable === void 0) {
5491
+ byTable = /* @__PURE__ */ new Map();
5492
+ this.#scopeCommittedListings.set(transaction, byTable);
5493
+ }
5494
+ const cached = byTable.get(table.id);
5495
+ if (cached !== void 0)
5496
+ return cached;
5497
+ const segments = (await listTableSegmentsPaged(this.store, table.id)).filter((segment) => segment.transactionId !== transaction.id);
5498
+ const records = await this.#transactionRecordsForSegments(segments, transaction.id);
5499
+ const entry = { segments, records };
5500
+ byTable.set(table.id, entry);
5501
+ return entry;
5502
+ }
5503
+ async #withSessionVisibility(transaction, tables, options, read) {
5504
+ await this.#flushScopeWriteSets(transaction, tables.map((table) => table.id));
5346
5505
  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);
5506
+ const pendingBlocks = {
5507
+ has: (blockId) => transaction.hasPendingBlock(blockId)
5508
+ };
5351
5509
  return this.#withLeasedSnapshot(transaction.snapshotVersion, async (snapshot) => {
5352
5510
  const overlaySnapshot = {
5353
5511
  get version() {
@@ -5382,7 +5540,7 @@ class MinnowDatabase {
5382
5540
  committedIndexes.push(index);
5383
5541
  }
5384
5542
  });
5385
- const [committed, staged] = await Promise.all([
5543
+ const [committed, staged2] = await Promise.all([
5386
5544
  this.#getBlocksWindowed(committedIds),
5387
5545
  Promise.all(stagedIds.map((blockId) => transaction.getBlock(blockId)))
5388
5546
  ]);
@@ -5392,7 +5550,7 @@ class MinnowDatabase {
5392
5550
  ordered[index] = committed[offset];
5393
5551
  });
5394
5552
  stagedIndexes.forEach((index, offset) => {
5395
- ordered[index] = staged[offset];
5553
+ ordered[index] = staged2[offset];
5396
5554
  });
5397
5555
  blocks.push(...ordered);
5398
5556
  }
@@ -5402,35 +5560,31 @@ class MinnowDatabase {
5402
5560
  release: () => snapshot.release()
5403
5561
  };
5404
5562
  const segmentsByTable = /* @__PURE__ */ new Map();
5405
- const transactionRecords = /* @__PURE__ */ new Map();
5406
- const deferredSegments = transaction.deferredSegments;
5563
+ const transactionRecords = /* @__PURE__ */ new Map([
5564
+ [transaction.id, { committedVersion: STAGED_OVERLAY_ORDER_BASE }]
5565
+ ]);
5566
+ const staged = transaction.stagedSegments;
5407
5567
  for (const table of tables) {
5408
5568
  throwIfAborted(options.signal);
5409
- const segments = [
5410
- ...await listTableSegmentsPaged(this.store, table.id),
5411
- ...deferredSegments.filter((segment) => segment.tableId === table.id)
5412
- ];
5569
+ const committed = await this.#scopeCommittedSegments(transaction, table);
5413
5570
  throwIfAborted(options.signal);
5414
- const doctored = segments.map((segment) => pendingIds.has(segment.id) ? { ...segment, logicalOrder: STAGED_OVERLAY_ORDER_BASE + segment.commitOrdinal } : segment);
5571
+ const doctored = [
5572
+ ...committed.segments,
5573
+ ...staged.filter((segment) => segment.tableId === table.id).map((segment) => ({
5574
+ ...segment,
5575
+ logicalOrder: STAGED_OVERLAY_ORDER_BASE + segment.commitOrdinal
5576
+ }))
5577
+ ];
5415
5578
  segmentsByTable.set(table.id, doctored);
5416
- for (const record of await this.#transactionRecordsForSegments(doctored)) {
5579
+ for (const record of committed.records)
5417
5580
  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
5581
  }
5428
5582
  const visibility = {
5429
5583
  transactions: transactionRecords,
5430
5584
  segmentsByTable,
5431
5585
  overlayTransactionId: transaction.id
5432
5586
  };
5433
- return this.#queryAtVisibility(plan, overlaySnapshot, visibility, realTables, true, options);
5587
+ return read(overlaySnapshot, visibility, realTables);
5434
5588
  });
5435
5589
  }
5436
5590
  async #queryAtVisibility(plan, snapshot, visibility, realTables, cacheResults = true, options = {}) {
@@ -5470,23 +5624,634 @@ class MinnowDatabase {
5470
5624
  memory.close();
5471
5625
  }
5472
5626
  }
5627
+ #stagedKeyOverlays = /* @__PURE__ */ new WeakMap();
5473
5628
  #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)
5629
+ const entries = transaction.accumulatedUniqueKeyChanges;
5630
+ let cache = this.#stagedKeyOverlays.get(transaction);
5631
+ if (cache === void 0 || cache.consumed > entries.length) {
5632
+ cache = { consumed: 0, tables: /* @__PURE__ */ new Map() };
5633
+ this.#stagedKeyOverlays.set(transaction, cache);
5634
+ }
5635
+ for (; cache.consumed < entries.length; cache.consumed += 1) {
5636
+ const entry = entries[cache.consumed];
5637
+ if (entry === void 0)
5478
5638
  continue;
5639
+ let overlay = cache.tables.get(entry.tableId);
5640
+ if (overlay === void 0) {
5641
+ overlay = { added: /* @__PURE__ */ new Set(), removed: /* @__PURE__ */ new Set() };
5642
+ cache.tables.set(entry.tableId, overlay);
5643
+ }
5479
5644
  for (const token of entry.keyTokens) {
5480
5645
  if (entry.remove === true) {
5481
- added.delete(token);
5482
- removed.add(token);
5646
+ overlay.added.delete(token);
5647
+ overlay.removed.add(token);
5483
5648
  } else {
5484
- removed.delete(token);
5485
- added.add(token);
5649
+ overlay.removed.delete(token);
5650
+ overlay.added.add(token);
5651
+ }
5652
+ }
5653
+ }
5654
+ return cache.tables.get(tableId) ?? { added: /* @__PURE__ */ new Set(), removed: /* @__PURE__ */ new Set() };
5655
+ }
5656
+ #scopeWrites = /* @__PURE__ */ new WeakMap();
5657
+ #scopeWriteState(transaction) {
5658
+ return this.#scopeWrites.get(transaction);
5659
+ }
5660
+ #scopeWriteSet(state, table) {
5661
+ let set = state.tables.get(table.id);
5662
+ if (set === void 0) {
5663
+ set = {
5664
+ table,
5665
+ keyColumn: getUniqueKeyColumn(table),
5666
+ rows: /* @__PURE__ */ new Map(),
5667
+ pendingRows: 0,
5668
+ pendingBytes: 0,
5669
+ mirrorBytes: 0,
5670
+ opaque: false,
5671
+ unkeyedSequence: 0
5672
+ };
5673
+ state.tables.set(table.id, set);
5674
+ }
5675
+ return set;
5676
+ }
5677
+ #hasPendingScopeWrites(transaction) {
5678
+ const state = this.#scopeWrites.get(transaction);
5679
+ if (state === void 0)
5680
+ return false;
5681
+ for (const set of state.tables.values())
5682
+ if (set.pendingRows > 0)
5683
+ return true;
5684
+ return false;
5685
+ }
5686
+ #scopeWriteGeneration(transaction) {
5687
+ return this.#scopeWrites.get(transaction)?.generation ?? 0;
5688
+ }
5689
+ async #stageScopeDirectly(transaction, table) {
5690
+ const state = this.#scopeWriteState(transaction);
5691
+ if (state === void 0)
5692
+ return;
5693
+ await this.#flushScopeWriteSets(transaction, [table.id]);
5694
+ this.#scopeWriteSet(state, table).opaque = true;
5695
+ }
5696
+ async #finishScopeWriteSets(transaction) {
5697
+ const state = this.#scopeWriteState(transaction);
5698
+ if (state === void 0)
5699
+ return;
5700
+ state.mirrored = false;
5701
+ await this.#flushScopeWriteSets(transaction);
5702
+ }
5703
+ #applyScopeEffect(set, token, key, effect) {
5704
+ let entry = set.rows.get(token);
5705
+ if (entry === void 0) {
5706
+ entry = { key };
5707
+ set.rows.set(token, entry);
5708
+ }
5709
+ if (entry.pending === void 0)
5710
+ set.pendingRows += 1;
5711
+ entry.pending = composeScopeEffect(entry.pending, effect);
5712
+ }
5713
+ async #bufferScopeRows(transaction, table, batch, rowCount, kind) {
5714
+ const state = this.#scopeWriteState(transaction);
5715
+ if (state === void 0)
5716
+ throw new Error("Only a write scope buffers its statements");
5717
+ state.generation += 1;
5718
+ const set = this.#scopeWriteSet(state, table);
5719
+ const { keyColumn } = set;
5720
+ const keyValues = keyColumn === void 0 ? void 0 : batch.columns[keyColumn.name] ?? [];
5721
+ for (let row = 0; row < rowCount; row += 1) {
5722
+ const values = table.columns.map((column) => batch.columns[column.name]?.[row] ?? null);
5723
+ let token;
5724
+ let key;
5725
+ if (keyColumn === void 0 || keyValues === void 0) {
5726
+ token = `\0${String(set.unkeyedSequence)}`;
5727
+ set.unkeyedSequence += 1;
5728
+ } else {
5729
+ const value = keyValues[row] ?? null;
5730
+ if (value === null)
5731
+ throw new TypeError(`Unique key cannot be null: ${keyColumn.name}`);
5732
+ token = keyToken(keyColumn.type, value);
5733
+ key = value;
5734
+ }
5735
+ this.#applyScopeEffect(set, token, key, { kind, values });
5736
+ }
5737
+ set.pendingBytes += estimateBatchBytes(batch);
5738
+ await this.#settleScopeWriteSet(transaction, set);
5739
+ }
5740
+ async #bufferScopeUpdate(transaction, table, keyColumn, input) {
5741
+ const state = this.#scopeWriteState(transaction);
5742
+ if (state === void 0)
5743
+ throw new Error("Only a write scope buffers its statements");
5744
+ state.generation += 1;
5745
+ const set = this.#scopeWriteSet(state, table);
5746
+ const keyPosition = table.columns.findIndex((column) => column.id === keyColumn.id);
5747
+ const changes = Object.entries(input.changes).map(([name, values]) => ({
5748
+ position: table.columns.findIndex((column) => column.name === name),
5749
+ values
5750
+ }));
5751
+ let bytes = estimateValuesBytes(input.keys);
5752
+ for (const change of changes)
5753
+ bytes += estimateValuesBytes(change.values);
5754
+ for (let row = 0; row < input.keys.length; row += 1) {
5755
+ const key = input.keys[row] ?? null;
5756
+ if (key === null)
5757
+ throw new TypeError(`Unique key cannot be null: ${keyColumn.name}`);
5758
+ const values = new Array(table.columns.length).fill(void 0);
5759
+ values[keyPosition] = key;
5760
+ for (const change of changes)
5761
+ values[change.position] = change.values[row] ?? null;
5762
+ this.#applyScopeEffect(set, keyToken(keyColumn.type, key), key, { kind: "update", values });
5763
+ }
5764
+ set.pendingBytes += bytes;
5765
+ await this.#settleScopeWriteSet(transaction, set);
5766
+ }
5767
+ async #bufferScopeDelete(transaction, table, keys) {
5768
+ const state = this.#scopeWriteState(transaction);
5769
+ if (state === void 0)
5770
+ throw new Error("Only a write scope buffers its statements");
5771
+ state.generation += 1;
5772
+ const set = this.#scopeWriteSet(state, table);
5773
+ for (const [token, key] of keys) {
5774
+ this.#applyScopeEffect(set, token, key, { kind: "delete", values: [] });
5775
+ }
5776
+ set.pendingBytes += estimateValuesBytes([...keys.values()]);
5777
+ await this.#settleScopeWriteSet(transaction, set);
5778
+ }
5779
+ async #settleScopeWriteSet(transaction, set) {
5780
+ await transaction.renewIfDue();
5781
+ if (set.pendingRows >= this.#rowsPerBlock) {
5782
+ await this.#flushScopeWriteSets(transaction, [set.table.id]);
5783
+ return;
5784
+ }
5785
+ let pendingBytes = 0;
5786
+ for (const pending of this.#scopeWriteState(transaction)?.tables.values() ?? []) {
5787
+ pendingBytes += pending.pendingBytes;
5788
+ }
5789
+ if (pendingBytes >= scopeWriteSetTestHooks.budgetBytes) {
5790
+ await this.#flushScopeWriteSets(transaction);
5791
+ }
5792
+ }
5793
+ #flushScopeWriteSets(transaction, tableIds) {
5794
+ const state = this.#scopeWrites.get(transaction);
5795
+ if (state === void 0)
5796
+ return Promise.resolve();
5797
+ const ids = tableIds === void 0 ? void 0 : [...tableIds];
5798
+ const turn = state.flushing.then(() => this.#flushScopeWriteSetsNow(transaction, state, ids));
5799
+ state.flushing = turn.catch(() => void 0);
5800
+ return turn;
5801
+ }
5802
+ async #flushScopeWriteSetsNow(transaction, state, tableIds) {
5803
+ const ids = tableIds ?? [...state.tables.keys()];
5804
+ for (const tableId of ids) {
5805
+ const set = state.tables.get(tableId);
5806
+ if (set === void 0 || set.pendingRows === 0)
5807
+ continue;
5808
+ const before = transaction.stagedWorkCount;
5809
+ try {
5810
+ await this.#stageScopeWriteSet(transaction, state, set);
5811
+ } catch (error) {
5812
+ state.failure ??= error;
5813
+ throw error;
5814
+ } finally {
5815
+ state.flushedWork += transaction.stagedWorkCount - before;
5816
+ }
5817
+ }
5818
+ if (!state.mirrored)
5819
+ return;
5820
+ let retained = 0;
5821
+ for (const set of state.tables.values())
5822
+ retained += set.mirrorBytes + set.pendingBytes;
5823
+ if (retained <= scopeWriteSetTestHooks.budgetBytes)
5824
+ return;
5825
+ state.mirrored = false;
5826
+ for (const set of state.tables.values()) {
5827
+ for (const [token, entry] of set.rows) {
5828
+ if (entry.pending === void 0)
5829
+ set.rows.delete(token);
5830
+ else
5831
+ delete entry.staged;
5832
+ }
5833
+ set.mirrorBytes = 0;
5834
+ }
5835
+ }
5836
+ async #stageScopeWriteSet(transaction, state, set) {
5837
+ const { table, keyColumn } = set;
5838
+ const keyPosition = keyColumn === void 0 ? -1 : table.columns.findIndex((column) => column.id === keyColumn.id);
5839
+ const deletes = [];
5840
+ const updates = /* @__PURE__ */ new Map();
5841
+ const upserts = [];
5842
+ const inserts = [];
5843
+ for (const entry of set.rows.values()) {
5844
+ const pending = entry.pending;
5845
+ if (pending === void 0)
5846
+ continue;
5847
+ switch (pending.kind) {
5848
+ case "delete":
5849
+ if (entry.key !== void 0)
5850
+ deletes.push(entry.key);
5851
+ break;
5852
+ case "insert":
5853
+ inserts.push(pending);
5854
+ break;
5855
+ case "upsert":
5856
+ upserts.push(pending);
5857
+ break;
5858
+ case "update": {
5859
+ const positions = [];
5860
+ pending.values.forEach((value, position) => {
5861
+ if (value !== void 0 && position !== keyPosition)
5862
+ positions.push(position);
5863
+ });
5864
+ const signature = positions.join(",");
5865
+ let group = updates.get(signature);
5866
+ if (group === void 0) {
5867
+ group = { positions, entries: [] };
5868
+ updates.set(signature, group);
5869
+ }
5870
+ group.entries.push(entry);
5871
+ break;
5872
+ }
5873
+ }
5874
+ }
5875
+ const batchOf = (effects) => ({
5876
+ columns: Object.fromEntries(table.columns.map((column, position) => [
5877
+ column.name,
5878
+ effects.map((effect) => effect.values[position] ?? null)
5879
+ ])),
5880
+ rowCount: effects.length
5881
+ });
5882
+ if (keyColumn !== void 0) {
5883
+ if (deletes.length > 0) {
5884
+ await this.#stageDeleteSegment(transaction, table, keyColumn, deletes);
5885
+ }
5886
+ for (const group of updates.values()) {
5887
+ const changes = {};
5888
+ for (const position of group.positions) {
5889
+ const column = table.columns[position];
5890
+ if (column === void 0)
5891
+ continue;
5892
+ changes[column.name] = group.entries.map((entry) => entry.pending?.values[position] ?? null);
5893
+ }
5894
+ await this.#stageUpdateSegment(transaction, table, keyColumn, {
5895
+ keys: group.entries.map((entry) => entry.key ?? null),
5896
+ changes
5897
+ });
5898
+ }
5899
+ }
5900
+ if (upserts.length > 0) {
5901
+ await this.#stageInsertSegment(transaction, table, batchOf(upserts), upserts.length, "upsert");
5902
+ }
5903
+ if (inserts.length > 0) {
5904
+ await this.#stageInsertSegment(transaction, table, batchOf(inserts), inserts.length, "insert");
5905
+ }
5906
+ const mirror = keyColumn !== void 0 && state.mirrored && !set.opaque;
5907
+ for (const [token, entry] of set.rows) {
5908
+ if (entry.pending === void 0)
5909
+ continue;
5910
+ if (!mirror) {
5911
+ set.rows.delete(token);
5912
+ continue;
5913
+ }
5914
+ entry.staged = composeScopeEffect(entry.staged, entry.pending);
5915
+ delete entry.pending;
5916
+ }
5917
+ if (mirror)
5918
+ set.mirrorBytes += set.pendingBytes;
5919
+ set.pendingBytes = 0;
5920
+ set.pendingRows = 0;
5921
+ }
5922
+ #discardScopeWriteSets(transaction) {
5923
+ const generation = this.#scopeWriteGeneration(transaction) + 1;
5924
+ this.#scopeWrites.set(transaction, {
5925
+ tables: /* @__PURE__ */ new Map(),
5926
+ mirrored: false,
5927
+ generation,
5928
+ flushedWork: 0,
5929
+ flushing: Promise.resolve()
5930
+ });
5931
+ this.#stagedKeyOverlays.delete(transaction);
5932
+ }
5933
+ async #scopeRowsByKey(transaction, table, keyColumn, keys, projection) {
5934
+ const state = this.#scopeWrites.get(transaction);
5935
+ if (!state?.mirrored)
5936
+ return void 0;
5937
+ const set = state.tables.get(table.id);
5938
+ if (set?.opaque === true)
5939
+ return void 0;
5940
+ const names = projection === "*" ? visibleTableColumns(table).map(({ name }) => name) : [...projection];
5941
+ if (!names.includes(keyColumn.name))
5942
+ names.push(keyColumn.name);
5943
+ const wanted = new Set(names);
5944
+ const answered = /* @__PURE__ */ new Map();
5945
+ const committed = [];
5946
+ const patches = /* @__PURE__ */ new Map();
5947
+ for (const key of keys) {
5948
+ const token = keyToken(keyColumn.type, key);
5949
+ const entry = set?.rows.get(token);
5950
+ const effect = entry === void 0 ? void 0 : netScopeEffect(entry);
5951
+ if (effect === void 0) {
5952
+ committed.push(key);
5953
+ continue;
5954
+ }
5955
+ if (effect.kind === "delete")
5956
+ continue;
5957
+ if (effect.kind === "update") {
5958
+ committed.push(key);
5959
+ patches.set(token, effect);
5960
+ continue;
5961
+ }
5962
+ const row = {};
5963
+ table.columns.forEach((column, position) => {
5964
+ if (wanted.has(column.name))
5965
+ row[column.name] = effect.values[position] ?? null;
5966
+ });
5967
+ answered.set(token, row);
5968
+ }
5969
+ if (committed.length > 0 && transaction.snapshotVersion !== null) {
5970
+ const pointRows = await this.#scopeCommittedRowsByKey(transaction, table, keyColumn, committed, names);
5971
+ const windows = [];
5972
+ if (pointRows === void 0) {
5973
+ for (let start = 0; start < committed.length; start += SCOPE_KEY_LOOKUP_WINDOW) {
5974
+ windows.push(committed.slice(start, start + SCOPE_KEY_LOOKUP_WINDOW));
5975
+ }
5976
+ }
5977
+ const patched = (rows) => {
5978
+ for (const row of rows) {
5979
+ const key = row[keyColumn.name] ?? null;
5980
+ if (key === null)
5981
+ continue;
5982
+ const token = keyToken(keyColumn.type, key);
5983
+ const patch = patches.get(token);
5984
+ if (patch !== void 0) {
5985
+ table.columns.forEach((column, position) => {
5986
+ const value = patch.values[position];
5987
+ if (value !== void 0 && wanted.has(column.name))
5988
+ row[column.name] = value;
5989
+ });
5990
+ }
5991
+ answered.set(token, row);
5992
+ }
5993
+ };
5994
+ if (pointRows !== void 0)
5995
+ patched(pointRows);
5996
+ for (const window of windows) {
5997
+ const plan = {
5998
+ sql: "(scope keyed lookup)",
5999
+ base: { table: table.name, alias: table.name },
6000
+ joins: [],
6001
+ select: names.map((name) => ({
6002
+ expression: { kind: "column", reference: name },
6003
+ alias: name
6004
+ })),
6005
+ predicates: [
6006
+ {
6007
+ left: { kind: "column", reference: keyColumn.name },
6008
+ operator: "IN",
6009
+ right: { kind: "list", items: window.map((value) => ({ kind: "literal", value })) }
6010
+ }
6011
+ ],
6012
+ groupBy: [],
6013
+ having: [],
6014
+ orderBy: []
6015
+ };
6016
+ const result = await this.#queryCompiled(plan, {
6017
+ version: transaction.snapshotVersion,
6018
+ memoize: false
6019
+ });
6020
+ patched(result.rows);
6021
+ }
6022
+ }
6023
+ return [...answered.values()];
6024
+ }
6025
+ async #scopeCommittedRowsByKey(transaction, table, keyColumn, keys, names) {
6026
+ if (pointReadTestHooks.disabled)
6027
+ return void 0;
6028
+ const values = [];
6029
+ for (const key of keys) {
6030
+ if (typeof key !== "number" && typeof key !== "string" && typeof key !== "boolean" && !(key instanceof Date)) {
6031
+ return void 0;
6032
+ }
6033
+ values.push(key);
6034
+ }
6035
+ const { segments, records } = await this.#scopeCommittedSegments(transaction, table);
6036
+ const visibility = {
6037
+ transactions: new Map(records.map((record) => [record.id, record])),
6038
+ segmentsByTable: /* @__PURE__ */ new Map([[table.id, segments]])
6039
+ };
6040
+ const realTables = /* @__PURE__ */ new Map([[table.name, table]]);
6041
+ const select = names.map((name) => ({ column: name, alias: name }));
6042
+ return this.#withLeasedSnapshot(transaction.snapshotVersion, async (snapshot) => {
6043
+ const rows = [];
6044
+ for (const value of values) {
6045
+ const result = await this.#pointReadAtSnapshot({ table: table.name, equalities: [{ column: keyColumn.name, value }], select }, snapshot, realTables, visibility, {});
6046
+ if (result === void 0)
6047
+ return void 0;
6048
+ rows.push(...result.rows);
6049
+ }
6050
+ return rows;
6051
+ });
6052
+ }
6053
+ #scopeKeyLookup(transaction, table, keyColumn) {
6054
+ return async (keys, projection) => {
6055
+ const direct = await this.#scopeRowsByKey(transaction, table, keyColumn, keys, projection);
6056
+ if (direct !== void 0)
6057
+ return direct;
6058
+ const quote = (name) => `"${name.replaceAll('"', '""')}"`;
6059
+ const selected = projection === "*" ? "*" : projection.map(quote).join(", ");
6060
+ const rows = [];
6061
+ for (let start = 0; start < keys.length; start += SCOPE_KEY_LOOKUP_WINDOW) {
6062
+ const window = keys.slice(start, start + SCOPE_KEY_LOOKUP_WINDOW);
6063
+ const placeholders = window.map(() => "?").join(", ");
6064
+ const sql = `SELECT ${selected} FROM ${quote(table.name)} WHERE ${quote(keyColumn.name)} IN (${placeholders})`;
6065
+ rows.push(...(await this.#sessionQuery(transaction, sql, { params: [...window] })).rows);
6066
+ }
6067
+ return rows;
6068
+ };
6069
+ }
6070
+ async #scopeKeyedMutation(transaction, statement) {
6071
+ if (statement.from !== void 0 || statement.returning !== void 0 || statement.returningItems !== void 0 || statement.predicates.length !== 1) {
6072
+ return void 0;
6073
+ }
6074
+ const table = await this.#findTable(statement.table, transaction);
6075
+ const keyColumn = getUniqueKeyColumn(table);
6076
+ if (keyColumn === void 0 || keyColumn.hidden === true || keyColumn.sqlDomain !== void 0) {
6077
+ return void 0;
6078
+ }
6079
+ const predicate = statement.predicates[0];
6080
+ if (predicate === void 0)
6081
+ return void 0;
6082
+ const keyNames = /* @__PURE__ */ new Set([
6083
+ keyColumn.name,
6084
+ `${statement.alias ?? table.name}.${keyColumn.name}`
6085
+ ]);
6086
+ const isKey = (expression) => expression.kind === "column" && keyNames.has(expression.reference);
6087
+ const keys = [];
6088
+ const accept = (expression) => {
6089
+ const value = plainLiteralValue(expression);
6090
+ if (value === void 0 || value === null || !valueMatchesColumnType(keyColumn, value)) {
6091
+ return false;
6092
+ }
6093
+ keys.push(value);
6094
+ return true;
6095
+ };
6096
+ if (predicate.operator === "=") {
6097
+ if (isKey(predicate.left)) {
6098
+ if (!accept(predicate.right))
6099
+ return void 0;
6100
+ } else if (isKey(predicate.right)) {
6101
+ if (!accept(predicate.left))
6102
+ return void 0;
6103
+ } else {
6104
+ return void 0;
6105
+ }
6106
+ } else if (predicate.operator === "IN" && isKey(predicate.left)) {
6107
+ if (predicate.right.kind !== "list")
6108
+ return void 0;
6109
+ for (const item of predicate.right.items)
6110
+ if (!accept(item))
6111
+ return void 0;
6112
+ } else {
6113
+ return void 0;
6114
+ }
6115
+ if (statement.kind === "delete")
6116
+ return { kind: "delete", table, keyColumn, keys, changes: {} };
6117
+ const changes = {};
6118
+ for (const assignment of statement.assignments) {
6119
+ const column = table.columns.find((candidate) => candidate.name === assignment.column);
6120
+ 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)) {
6121
+ return void 0;
6122
+ }
6123
+ const value = plainLiteralValue(assignment.expression);
6124
+ if (value === void 0 || value !== null && !valueMatchesColumnType(column, value)) {
6125
+ return void 0;
6126
+ }
6127
+ changes[column.name] = value;
6128
+ }
6129
+ if (Object.keys(changes).length === 0)
6130
+ return void 0;
6131
+ return { kind: "update", table, keyColumn, keys, changes };
6132
+ }
6133
+ async #assertInsertKeysAbsent(transaction, table, batch, keys) {
6134
+ const inScope = this.#scopeWrites.has(transaction);
6135
+ const keyColumn = getUniqueKeyColumn(table);
6136
+ if (keys !== void 0 && keyColumn !== void 0) {
6137
+ const present = inScope ? await this.#scopeKeyPresence(transaction, table, keyColumn, [...keys.values()]) : this.#stagedKeyOverlay(transaction, table.id).added;
6138
+ for (const [token, value] of keys) {
6139
+ if (present.has(token)) {
6140
+ throw new UniqueConstraintError(table.name, publicKeyName(table, keyColumn), value);
6141
+ }
6142
+ }
6143
+ }
6144
+ if (!inScope)
6145
+ return;
6146
+ const rowCount = batch.rowCount ?? Object.values(batch.columns)[0]?.length ?? 0;
6147
+ for (const { indexId, index, columns } of readyUniqueSecondaryIndexes(table)) {
6148
+ const namespaceId = secondaryUniqueKeyNamespace(table.id, indexId);
6149
+ const terms = [];
6150
+ for (let row = 0; row < rowCount; row += 1) {
6151
+ const term = secondaryUniqueTerm(index, columns, columns.map((column) => batch.columns[column.name]?.[row] ?? null));
6152
+ if (term !== void 0)
6153
+ terms.push(term);
6154
+ }
6155
+ assertNoDuplicateUniqueTerms(index, terms);
6156
+ const overlay = this.#stagedKeyOverlay(transaction, namespaceId);
6157
+ const unresolved = [];
6158
+ for (const term of terms) {
6159
+ if (overlay.added.has(term)) {
6160
+ throw await this.#translateUniqueConflict(new UniqueKeyConflictError(namespaceId, term));
5486
6161
  }
6162
+ if (!overlay.removed.has(term))
6163
+ unresolved.push(term);
5487
6164
  }
6165
+ if (unresolved.length === 0)
6166
+ continue;
6167
+ const [existing] = await this.#existingUniqueKeysWindowed(namespaceId, unresolved);
6168
+ if (existing !== void 0) {
6169
+ throw await this.#translateUniqueConflict(new UniqueKeyConflictError(namespaceId, existing));
6170
+ }
6171
+ }
6172
+ }
6173
+ async #assertUpdateUniqueTermsAbsent(transaction, table, input, preImages) {
6174
+ for (const { indexId, index, columns } of readyUniqueSecondaryIndexes(table)) {
6175
+ if (!columns.some((column) => input.changes[column.name] !== void 0))
6176
+ continue;
6177
+ const namespaceId = secondaryUniqueKeyNamespace(table.id, indexId);
6178
+ const released = /* @__PURE__ */ new Set();
6179
+ const taken = [];
6180
+ preImages.forEach((old, row) => {
6181
+ if (old === void 0)
6182
+ return;
6183
+ const before = secondaryUniqueTerm(index, columns, columns.map((column) => old[column.name] ?? null));
6184
+ const after = secondaryUniqueTerm(index, columns, columns.map((column) => {
6185
+ const assigned = input.changes[column.name];
6186
+ return assigned === void 0 ? old[column.name] ?? null : assigned[row] ?? null;
6187
+ }));
6188
+ if (before === after)
6189
+ return;
6190
+ if (before !== void 0)
6191
+ released.add(before);
6192
+ if (after !== void 0)
6193
+ taken.push(after);
6194
+ });
6195
+ assertNoDuplicateUniqueTerms(index, taken);
6196
+ const overlay = this.#stagedKeyOverlay(transaction, namespaceId);
6197
+ const unresolved = [];
6198
+ for (const term of taken) {
6199
+ if (released.has(term))
6200
+ continue;
6201
+ if (overlay.added.has(term)) {
6202
+ throw await this.#translateUniqueConflict(new UniqueKeyConflictError(namespaceId, term));
6203
+ }
6204
+ if (!overlay.removed.has(term))
6205
+ unresolved.push(term);
6206
+ }
6207
+ if (unresolved.length === 0)
6208
+ continue;
6209
+ const [existing] = await this.#existingUniqueKeysWindowed(namespaceId, unresolved);
6210
+ if (existing !== void 0) {
6211
+ throw await this.#translateUniqueConflict(new UniqueKeyConflictError(namespaceId, existing));
6212
+ }
6213
+ }
6214
+ }
6215
+ async #scopeKeyPresence(transaction, table, keyColumn, keys) {
6216
+ const overlay = this.#stagedKeyOverlay(transaction, table.id);
6217
+ const present = /* @__PURE__ */ new Set();
6218
+ const unresolved = [];
6219
+ for (const key of keys) {
6220
+ const token = keyToken(keyColumn.type, key);
6221
+ if (overlay.added.has(token))
6222
+ present.add(token);
6223
+ else if (!overlay.removed.has(token))
6224
+ unresolved.push(token);
6225
+ }
6226
+ if (unresolved.length > 0) {
6227
+ const existing = await this.#existingKeyTokens(table, transaction.snapshotVersion, unresolved);
6228
+ for (const token of existing)
6229
+ present.add(token);
5488
6230
  }
5489
- return { added, removed };
6231
+ return present;
6232
+ }
6233
+ async #runScopeKeyedMutation(transaction, keyed, beforeMutation) {
6234
+ const { table, keyColumn } = keyed;
6235
+ const byToken = /* @__PURE__ */ new Map();
6236
+ for (const key of keyed.keys)
6237
+ byToken.set(keyToken(keyColumn.type, key), key);
6238
+ const presence = await this.#scopeKeyPresence(transaction, table, keyColumn, [
6239
+ ...byToken.values()
6240
+ ]);
6241
+ const present = [];
6242
+ for (const [token, key] of byToken)
6243
+ if (presence.has(token))
6244
+ present.push(key);
6245
+ if (present.length === 0)
6246
+ return { kind: keyed.kind, table: table.name, rowCount: 0 };
6247
+ beforeMutation();
6248
+ if (keyed.kind === "delete") {
6249
+ const deleted = await this.#sessionDelete(transaction, table.name, { keys: present });
6250
+ return { kind: "delete", table: table.name, rowCount: deleted.rowCount };
6251
+ }
6252
+ const changes = Object.fromEntries(Object.entries(keyed.changes).map(([name, value]) => [name, present.map(() => value)]));
6253
+ const updated = await this.#sessionUpdate(transaction, table.name, { keys: present, changes }, 1, true);
6254
+ return { kind: "update", table: table.name, rowCount: updated.rowCount };
5490
6255
  }
5491
6256
  async #sessionInsert(transaction, tableName, input, kind, options, cascadeBudget = 1) {
5492
6257
  const table = await this.#findTable(tableName);
@@ -5507,7 +6272,7 @@ class MinnowDatabase {
5507
6272
  validateValue(autoIncrement.column, patched[rowIndex] ?? null, rowIndex);
5508
6273
  }
5509
6274
  }
5510
- let sessionUpsertFirings = sessionUpsertKeyColumn === void 0 ? void 0 : await this.#upsertTriggerFirings(table, sessionUpsertKeyColumn, batch, rowCount, (sql, params) => this.#sessionQuery(transaction, sql, { params }), normalizedConflictWhere);
6275
+ let sessionUpsertFirings = sessionUpsertKeyColumn === void 0 ? void 0 : await this.#upsertTriggerFirings(table, sessionUpsertKeyColumn, batch, rowCount, void 0, normalizedConflictWhere, this.#scopeKeyLookup(transaction, table, sessionUpsertKeyColumn));
5511
6276
  let skippedRowCount = 0;
5512
6277
  if (normalizedConflictWhere !== void 0) {
5513
6278
  if (sessionUpsertFirings === void 0) {
@@ -5533,7 +6298,12 @@ class MinnowDatabase {
5533
6298
  }
5534
6299
  }
5535
6300
  await this.#assertCompactionCapacity(table, transaction);
6301
+ await this.#assertForeignKeysPresent(table, (column) => batch.columns[column] ?? [], (sql, params) => this.#sessionQuery(transaction, sql, { params }), transaction);
5536
6302
  const keys = batchKeys(table, batch);
6303
+ if (kind === "insert")
6304
+ await this.#assertInsertKeysAbsent(transaction, table, batch, keys);
6305
+ else
6306
+ assertBatchSecondaryTermsDistinct(table, batch);
5537
6307
  if (keys !== void 0) {
5538
6308
  transaction.setUniqueKeyChanges({
5539
6309
  tableId: table.id,
@@ -5541,10 +6311,22 @@ class MinnowDatabase {
5541
6311
  requireAbsent: kind === "insert"
5542
6312
  });
5543
6313
  }
5544
- 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
6314
  const insertValueAt = (source, column, rowIndex) => source === "new" ? batch.columns[column]?.[rowIndex] ?? null : null;
5547
6315
  stageSecondaryUniqueInsertChanges(transaction, table, batch, kind === "upsert" ? sessionUpsertFirings?.oldImages : void 0);
6316
+ const buffered = this.#scopeWrites.has(transaction) && rowCount < SCOPE_DIRECT_STAGE_ROWS && !(table.triggers ?? []).some((trigger) => trigger.event === "insert" || trigger.event === "update");
6317
+ if (buffered) {
6318
+ await this.#bufferScopeRows(transaction, table, batch, rowCount, kind);
6319
+ collectAutoIncrementGenerated(batch, generated, autoIncrement);
6320
+ return {
6321
+ tableName: table.name,
6322
+ segmentId: null,
6323
+ rowCount,
6324
+ skippedRowCount,
6325
+ ...generated.size === 0 ? {} : { generatedColumns: Object.fromEntries(generated) }
6326
+ };
6327
+ }
6328
+ await this.#stageScopeDirectly(transaction, table);
6329
+ const rowIds = await this.store.reserveRowIds(table.id, rowCount);
5548
6330
  if (kind === "insert") {
5549
6331
  await this.#stageTriggerDerivedInserts(transaction, table, "insert", rowCount, insertValueAt, "before", cascadeBudget);
5550
6332
  } else if (sessionUpsertFirings !== void 0) {
@@ -5565,8 +6347,8 @@ class MinnowDatabase {
5565
6347
  ...generated.size === 0 ? {} : { generatedColumns: Object.fromEntries(generated) }
5566
6348
  };
5567
6349
  }
5568
- async #sessionUpdate(transaction, tableName, input, cascadeBudget = 1) {
5569
- const table = await this.#findTable(tableName);
6350
+ async #sessionUpdate(transaction, tableName, input, cascadeBudget = 1, keysVerified = false) {
6351
+ const table = await this.#findTable(tableName, transaction);
5570
6352
  await this.#assertCompactionCapacity(table, transaction);
5571
6353
  const keyColumn = getUniqueKeyColumn(table);
5572
6354
  if (keyColumn === void 0) {
@@ -5576,21 +6358,26 @@ class MinnowDatabase {
5576
6358
  rejectGeneratedUpdateAssignments(table, input);
5577
6359
  const keys = validateUpdateBatch(table, keyColumn, input);
5578
6360
  const overlay = this.#stagedKeyOverlay(transaction, table.id);
5579
- for (const [token, value] of keys) {
5580
- if (overlay.removed.has(token)) {
5581
- throw new MissingKeyError(table.name, keyColumn.name, value);
6361
+ if (!keysVerified) {
6362
+ for (const [token, value] of keys) {
6363
+ if (overlay.removed.has(token)) {
6364
+ throw new MissingKeyError(table.name, keyColumn.name, value);
6365
+ }
6366
+ }
6367
+ const committedKeys = new Map([...keys].filter(([token]) => !overlay.added.has(token)));
6368
+ if (committedKeys.size > 0) {
6369
+ await this.#assertKeysExist(table, keyColumn, transaction.snapshotVersion, committedKeys);
5582
6370
  }
5583
- }
5584
- const committedKeys = new Map([...keys].filter(([token]) => !overlay.added.has(token)));
5585
- if (committedKeys.size > 0) {
5586
- await this.#assertKeysExist(table, keyColumn, transaction.snapshotVersion, committedKeys);
5587
6371
  }
5588
6372
  const sessionChecks = table.checks ?? [];
5589
6373
  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)));
6374
+ 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
6375
  input = applyStoredGeneratedUpdateChanges(table, input, preImages);
5592
6376
  validateUpdateBatch(table, keyColumn, input);
5593
6377
  changedForeignKey = (table.foreignKeys ?? []).some((key) => key.enforced !== false && foreignKeyColumns(key).some((column) => input.changes[column] !== void 0));
6378
+ if (this.#scopeWrites.has(transaction)) {
6379
+ await this.#assertUpdateUniqueTermsAbsent(transaction, table, input, preImages);
6380
+ }
5594
6381
  stageSecondaryUniqueMutationChanges(transaction, table, input, preImages);
5595
6382
  const secondaryDeltas = buildSecondaryUpdateDeltas(table, input, preImages);
5596
6383
  if (secondaryDeltas.length > 0) {
@@ -5617,56 +6404,14 @@ class MinnowDatabase {
5617
6404
  }
5618
6405
  }
5619
6406
  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;
6407
+ const buffered = this.#scopeWrites.has(transaction) && input.keys.length < SCOPE_DIRECT_STAGE_ROWS && !(table.triggers ?? []).some((trigger) => trigger.event === "update");
6408
+ let segmentId = null;
6409
+ if (buffered) {
6410
+ await this.#bufferScopeUpdate(transaction, table, keyColumn, input);
6411
+ } else {
6412
+ await this.#stageScopeDirectly(transaction, table);
6413
+ segmentId = await this.#stageUpdateSegment(transaction, table, keyColumn, input);
5651
6414
  }
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
6415
  await this.#stageTriggerDerivedInserts(transaction, table, "update", input.keys.length, sessionUpdateValueAt, "after", cascadeBudget);
5671
6416
  return {
5672
6417
  tableName: table.name,
@@ -5681,7 +6426,7 @@ class MinnowDatabase {
5681
6426
  };
5682
6427
  }
5683
6428
  async #sessionDelete(transaction, tableName, input, cascadeBudget = 1, referentialBudget = REFERENTIAL_CASCADES) {
5684
- const table = await this.#findTable(tableName);
6429
+ const table = await this.#findTable(tableName, transaction);
5685
6430
  await this.#assertCompactionCapacity(table, transaction);
5686
6431
  const keyColumn = getUniqueKeyColumn(table);
5687
6432
  if (keyColumn === void 0) {
@@ -5727,14 +6472,79 @@ class MinnowDatabase {
5727
6472
  if (secondaryCoverage.length > 0) {
5728
6473
  transaction.setFtsChanges({ tableId: table.id, columns: secondaryCoverage });
5729
6474
  }
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);
6475
+ 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
6476
  stageSecondaryUniqueMutationChanges(transaction, table, void 0, preImages);
5732
6477
  const sessionDeleteValueAt = (source, column, rowIndex) => source === "old" ? preImages[rowIndex]?.[column] ?? null : null;
5733
6478
  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);
6479
+ const buffered = this.#scopeWrites.has(transaction) && keys.size < SCOPE_DIRECT_STAGE_ROWS && !(table.triggers ?? []).some((trigger) => trigger.event === "delete");
6480
+ let segmentId = null;
6481
+ if (buffered) {
6482
+ await this.#bufferScopeDelete(transaction, table, keys);
6483
+ } else {
6484
+ await this.#stageScopeDirectly(transaction, table);
6485
+ const values = [...keys.values()];
6486
+ if (keyStringByteLengths !== void 0) {
6487
+ validatedStringByteLengths.set(values, keyStringByteLengths);
6488
+ }
6489
+ segmentId = await this.#stageDeleteSegment(transaction, table, keyColumn, values);
6490
+ }
6491
+ await this.#stageTriggerDerivedInserts(transaction, table, "delete", preImages.length, sessionDeleteValueAt, "after", cascadeBudget);
6492
+ return { tableName: table.name, segmentId, rowCount: keys.size };
6493
+ }
6494
+ async #stageUpdateSegment(transaction, table, keyColumn, input) {
6495
+ const changedColumns = Object.keys(input.changes).sort();
6496
+ const columns = [keyColumn, ...changedColumns.map((name) => findColumn(table, name))];
6497
+ const segmentId = this.#createId();
6498
+ const columnBlockIds = {};
6499
+ const blockStager = new BoundedWriteBlockStager(transaction);
6500
+ const plannedColumns = columns.map((column) => writeColumnValues(column.type, column.id === keyColumn.id ? input.keys : input.changes[column.name] ?? []));
6501
+ const ranges = writeBlockRanges(plannedColumns, input.keys.length, this.#rowsPerBlock, this.#targetBlockBytes);
6502
+ for (const [columnIndex, column] of columns.entries()) {
6503
+ const values = column.id === keyColumn.id ? input.keys : input.changes[column.name] ?? [];
6504
+ const plannedColumn = plannedColumns[columnIndex];
6505
+ if (plannedColumn === void 0)
6506
+ throw new Error(`Write column disappeared: ${column.name}`);
6507
+ const blockIds = [];
6508
+ for (const [part, { start, end }] of ranges.entries()) {
6509
+ await blockStager.prepare(maximumWriteBlockStoredBytes(plannedColumn, start, end, this.#compression));
6510
+ const slice = values.slice(start, end);
6511
+ const bytes = await this.#encodeColumnBlock(column.id, asColumnInput(column.type, slice));
6512
+ const blockId = [
6513
+ "table",
6514
+ table.id,
6515
+ "segment",
6516
+ segmentId,
6517
+ "update-column",
6518
+ column.id,
6519
+ "part",
6520
+ String(part).padStart(6, "0")
6521
+ ].join("/");
6522
+ blockStager.add({ id: blockId, bytes });
6523
+ blockIds.push(blockId);
6524
+ }
6525
+ columnBlockIds[column.id] = blockIds;
5737
6526
  }
6527
+ await blockStager.stageWithSegments([
6528
+ {
6529
+ id: segmentId,
6530
+ tableId: table.id,
6531
+ transactionId: transaction.id,
6532
+ rowCount: input.keys.length,
6533
+ rowIdStart: 0n,
6534
+ rowIdEndExclusive: 0n,
6535
+ columnBlockIds,
6536
+ kind: "update",
6537
+ keyColumnId: keyColumn.id,
6538
+ level: 0,
6539
+ logicalOrder: 0,
6540
+ commitOrdinal: transaction.pendingSegmentCount,
6541
+ rowIdSpans: [],
6542
+ createdAt: dateIsoString(this.#now())
6543
+ }
6544
+ ]);
6545
+ return segmentId;
6546
+ }
6547
+ async #stageDeleteSegment(transaction, table, keyColumn, values) {
5738
6548
  const segmentId = this.#createId();
5739
6549
  const blockIds = [];
5740
6550
  const blockStager = new BoundedWriteBlockStager(transaction);
@@ -5762,7 +6572,7 @@ class MinnowDatabase {
5762
6572
  id: segmentId,
5763
6573
  tableId: table.id,
5764
6574
  transactionId: transaction.id,
5765
- rowCount: keys.size,
6575
+ rowCount: values.length,
5766
6576
  rowIdStart: 0n,
5767
6577
  rowIdEndExclusive: 0n,
5768
6578
  columnBlockIds: { [keyColumn.id]: blockIds },
@@ -5770,13 +6580,12 @@ class MinnowDatabase {
5770
6580
  keyColumnId: keyColumn.id,
5771
6581
  level: 0,
5772
6582
  logicalOrder: 0,
5773
- commitOrdinal: transaction.pendingSegmentIds.length,
6583
+ commitOrdinal: transaction.pendingSegmentCount,
5774
6584
  rowIdSpans: [],
5775
6585
  createdAt: dateIsoString(this.#now())
5776
6586
  }
5777
6587
  ]);
5778
- await this.#stageTriggerDerivedInserts(transaction, table, "delete", preImages.length, sessionDeleteValueAt, "after", cascadeBudget);
5779
- return { tableName: table.name, segmentId, rowCount: keys.size };
6588
+ return segmentId;
5780
6589
  }
5781
6590
  #notifyLiveCommit() {
5782
6591
  for (const set of this.#liveSets)
@@ -6181,6 +6990,9 @@ class MinnowDatabase {
6181
6990
  notes.push("BM25 scoring reads the full scan for corpus statistics; index pruning does not apply");
6182
6991
  }
6183
6992
  }
6993
+ const liveReasons = [];
6994
+ const liveState = await this.#liveMaintenancePlan(plan, await this.store.getCatalogProbe(), liveReasons);
6995
+ notes.push(liveState !== void 0 ? "live: maintained incrementally on change" : `live: re-executes on change: ${liveReasons.join("; ") || "the shape is not maintainable"}`);
6184
6996
  return `${renderPlan(plan)}
6185
6997
  ${notes.map((note) => `-- ${note}`).join("\n")}`;
6186
6998
  });
@@ -6505,7 +7317,8 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
6505
7317
  if (totalBytes > MAX_TRANSACTION_SAVEPOINT_BYTES) {
6506
7318
  throw new RangeError(`Transaction savepoints cannot retain more than ${String(MAX_TRANSACTION_SAVEPOINT_BYTES)} bytes`);
6507
7319
  }
6508
- open.savepoints.push({ name, checkpoint: open.session.checkpoint(), retainedBytes });
7320
+ const checkpoint = await open.session.checkpoint();
7321
+ open.savepoints.push({ name, checkpoint, retainedBytes });
6509
7322
  });
6510
7323
  return { kind: "transaction", action, name };
6511
7324
  }
@@ -6767,6 +7580,24 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
6767
7580
  const generatedColumns = new Set(table.columns.flatMap((column) => column.generatedValue === void 0 ? [] : [column.name]));
6768
7581
  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
7582
  const keys = filled.batch.columns[keyColumn.name] ?? [];
7583
+ const presence = await writer?.stagedKeyPresence?.(table, keyColumn, keys.filter((value) => value !== null));
7584
+ if (presence !== void 0) {
7585
+ const taken = new Set(presence);
7586
+ return {
7587
+ ...statement,
7588
+ columns,
7589
+ rows: materializedRows.filter((_, index) => {
7590
+ const value = keys[index] ?? null;
7591
+ if (value === null)
7592
+ return true;
7593
+ const token = keyToken(keyColumn.type, value);
7594
+ if (taken.has(token))
7595
+ return false;
7596
+ taken.add(token);
7597
+ return true;
7598
+ })
7599
+ };
7600
+ }
6770
7601
  const plan = {
6771
7602
  sql: "(on conflict do nothing)",
6772
7603
  base: { table: table.name, alias: table.name },
@@ -6809,6 +7640,15 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
6809
7640
  const keys = insertBatchKeyValues(input, keyColumn.name);
6810
7641
  if (keys.length === 0)
6811
7642
  return;
7643
+ const present = await writer.stagedKeyPresence?.(table, keyColumn, keys.filter((value) => value !== null));
7644
+ if (present !== void 0) {
7645
+ for (const value of keys) {
7646
+ if (value !== null && present.has(keyToken(keyColumn.type, value))) {
7647
+ throw new UniqueConstraintError(table.name, keyColumn.name, value);
7648
+ }
7649
+ }
7650
+ return;
7651
+ }
6812
7652
  const plan = {
6813
7653
  sql: "(staged insert keys)",
6814
7654
  base: { table: table.name, alias: table.name },
@@ -7115,6 +7955,7 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
7115
7955
  normalizeDomainBatch(pendingTable, batch);
7116
7956
  validateBatch(pendingTable, batch);
7117
7957
  const endExclusive = nextRowId + BigInt(rows2.length);
7958
+ await this.#stageScopeDirectly(transaction, pendingTable);
7118
7959
  await this.#stageInsertSegment(transaction, pendingTable, batch, rows2.length, "insert", { start: nextRowId, endExclusive });
7119
7960
  nextRowId = endExclusive;
7120
7961
  stagedRows += rows2.length;
@@ -7691,9 +8532,7 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
7691
8532
  return void 0;
7692
8533
  const visibleBaseSegments = visibleByTable.get(freshBaseTable.name) ?? await this.#visibleSegmentRecords(freshBaseTable, snapshot, visibility);
7693
8534
  this.#maybeScheduleAutoCompaction(freshBaseTable, visibleBaseSegments);
7694
- const ftsSegments = await this.#ftsPrunedSegments(freshBaseTable, visibleBaseSegments, plan, snapshot);
7695
- throwIfAborted(options.signal);
7696
- const indexed = await this.#secondaryIndexPrunedSegments(freshBaseTable, ftsSegments, plan, snapshot);
8535
+ const indexed = await this.#indexPrunedSegments(freshBaseTable, visibleBaseSegments, plan, snapshot, visibility);
7697
8536
  throwIfAborted(options.signal);
7698
8537
  const baseSegments = indexed.segments;
7699
8538
  const zonePruned = indexed.rows === void 0 ? await this.#zonePrunedStreamSegments(plan, freshBaseTable, projectedBaseColumns, baseSegments, snapshot) : void 0;
@@ -7947,13 +8786,14 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
7947
8786
  #streamedViewFactory(table, projectedColumns, segments, snapshot, storedBlocks, zonePruned = false) {
7948
8787
  const scanSegments = segments.filter((segment) => {
7949
8788
  const kind = segment.kind;
7950
- return kind === "insert" || kind === "base";
8789
+ return kind === "insert" || kind === "base" || kind === "upsert";
7951
8790
  });
7952
8791
  const mutationSegments = segments.filter((segment) => segment.kind === "update" || segment.kind === "delete");
7953
8792
  if (scanSegments.length + mutationSegments.length !== segments.length)
7954
8793
  return void 0;
8794
+ const replays = mutationSegments.length > 0 || segments.some((segment) => segment.kind === "upsert");
7955
8795
  const keyColumn = getUniqueKeyColumn(table);
7956
- if (mutationSegments.length > 0) {
8796
+ if (replays) {
7957
8797
  if (keyColumn === void 0)
7958
8798
  return void 0;
7959
8799
  const keyBlocksPresent = segments.every((segment) => segment.rowCount === 0 || (segment.columnBlockIds[keyColumn.id]?.length ?? 0) > 0);
@@ -7965,7 +8805,7 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
7965
8805
  return void 0;
7966
8806
  const scanRowCount = scanSegments.reduce((total, segment) => total + segment.rowCount, 0);
7967
8807
  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)
8808
+ 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
8809
  };
7970
8810
  }
7971
8811
  async #runPartitionedJoin(plan, budgetBytes, estimatedBuildBytes, baseView, buildView, buildTableName, buildKeyName, signal) {
@@ -8239,11 +9079,71 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
8239
9079
  async #createStreamedMutationTable(table, keyColumn, projectedColumns, baseSegments, snapshot, memory, zonePruned = false, storedBlocks) {
8240
9080
  const scanSegments = baseSegments.filter((segment) => {
8241
9081
  const kind = segment.kind;
8242
- return kind === "insert" || kind === "base";
9082
+ return kind === "insert" || kind === "base" || kind === "upsert";
8243
9083
  });
8244
9084
  const overlay = await this.#streamedOverlayState(table, keyColumn, baseSegments, scanSegments, snapshot, memory, zonePruned);
8245
- const { baseRows, dead, deadCount, patches, patchedSlots } = overlay;
9085
+ const { baseRows, dead, deadCount, lazyPatches, patchedSlots } = overlay;
8246
9086
  const hasPatches = patchedSlots.length > 0;
9087
+ const windowPatches = async (from, to) => {
9088
+ const needed = /* @__PURE__ */ new Set();
9089
+ const inRange = [];
9090
+ const winning = (layers, columnId) => {
9091
+ for (let index = layers.length - 1; index >= 0; index -= 1) {
9092
+ const candidate = layers[index];
9093
+ if (candidate === void 0)
9094
+ continue;
9095
+ const blockId = candidate.segment.columnBlockIds[columnId]?.[candidate.blockIndex];
9096
+ if (blockId !== void 0)
9097
+ return { blockId, row: candidate.row };
9098
+ }
9099
+ return void 0;
9100
+ };
9101
+ for (const [slot, layers] of lazyPatches) {
9102
+ if (slot < from || slot >= to)
9103
+ continue;
9104
+ inRange.push([slot, layers]);
9105
+ for (const column of projectedColumns) {
9106
+ const hit = winning(layers, column.id);
9107
+ if (hit !== void 0)
9108
+ needed.add(hit.blockId);
9109
+ }
9110
+ }
9111
+ const empty = /* @__PURE__ */ new Map();
9112
+ if (inRange.length === 0)
9113
+ return empty;
9114
+ const ids = [...needed];
9115
+ const decoded = await this.#decodedBlocksThroughCache(ids, snapshot);
9116
+ const vectors = /* @__PURE__ */ new Map();
9117
+ ids.forEach((id, index) => {
9118
+ const block = decoded[index];
9119
+ if (block === void 0)
9120
+ throw new Error(`Visible block is missing: ${id}`);
9121
+ vectors.set(id, this.#blockColumnVector(id, block));
9122
+ });
9123
+ memory.tally(inRange.length * 96, "Streamed mutation replay");
9124
+ const resolved = /* @__PURE__ */ new Map();
9125
+ const plain = /* @__PURE__ */ new Map();
9126
+ for (const [slot, layers] of inRange) {
9127
+ const slotPatches = /* @__PURE__ */ new Map();
9128
+ for (const column of projectedColumns) {
9129
+ const hit = winning(layers, column.id);
9130
+ if (hit === void 0)
9131
+ continue;
9132
+ let vector = plain.get(hit.blockId);
9133
+ if (vector === void 0) {
9134
+ const raw = vectors.get(hit.blockId);
9135
+ if (raw === void 0)
9136
+ throw new Error(`Visible block is missing: ${hit.blockId}`);
9137
+ vector = plainTextExecutionVector(column, raw);
9138
+ plain.set(hit.blockId, vector);
9139
+ }
9140
+ slotPatches.set(column.id, { vector, row: hit.row });
9141
+ }
9142
+ if (slotPatches.size > 0)
9143
+ resolved.set(slot, slotPatches);
9144
+ }
9145
+ return resolved;
9146
+ };
8247
9147
  const outputRows = baseRows - deadCount;
8248
9148
  const inner = this.#createStreamedTable(table, projectedColumns, scanSegments, snapshot, baseRows, memory, storedBlocks);
8249
9149
  if (deadCount === 0 && !hasPatches)
@@ -8282,14 +9182,30 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
8282
9182
  }
8283
9183
  const baseStart = cursorBase;
8284
9184
  const innerEnd = await inner.load(baseStart, baseRows - baseStart);
8285
- const baseEnd = typeof innerEnd === "number" ? Math.min(innerEnd, baseRows) : baseRows;
8286
- if (baseEnd <= baseStart)
9185
+ const innerBaseEnd = typeof innerEnd === "number" ? Math.min(innerEnd, baseRows) : baseRows;
9186
+ if (innerBaseEnd <= baseStart)
8287
9187
  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;
9188
+ let baseEnd = innerBaseEnd;
9189
+ let deadInWindow = bitmapCountRange(dead, baseStart, baseEnd);
9190
+ let patchedInWindow = hasPatches ? sortedCountRange(patchedSlots, baseStart, baseEnd) : 0;
8291
9191
  const untouched = deadInWindow === 0 && patchedInWindow === 0;
9192
+ if (!untouched) {
9193
+ let live = 0;
9194
+ let row = baseStart;
9195
+ while (row < innerBaseEnd && live < length) {
9196
+ if (!bitmapHasValue(dead, row))
9197
+ live += 1;
9198
+ row += 1;
9199
+ }
9200
+ if (row < baseEnd) {
9201
+ baseEnd = row;
9202
+ deadInWindow = bitmapCountRange(dead, baseStart, baseEnd);
9203
+ patchedInWindow = hasPatches ? sortedCountRange(patchedSlots, baseStart, baseEnd) : 0;
9204
+ }
9205
+ }
9206
+ const liveRows = baseEnd - baseStart - deadInWindow;
8292
9207
  const runs = untouched ? void 0 : overlayWindowRuns(dead, patchedSlots, baseStart, baseEnd, patchedInWindow);
9208
+ const resolvedPatches = runs === void 0 || !hasPatches ? void 0 : await windowPatches(baseStart, baseEnd);
8293
9209
  const targets = [];
8294
9210
  try {
8295
9211
  for (const state of states) {
@@ -8303,7 +9219,7 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
8303
9219
  throw new Error(`Column row count mismatch: ${state.column.name}`);
8304
9220
  }
8305
9221
  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);
9222
+ 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
9223
  fields.window = { start, length: liveRows };
8308
9224
  targets.push({ state, fields, replacements });
8309
9225
  }
@@ -8377,12 +9293,11 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
8377
9293
  }
8378
9294
  async #buildStreamedOverlayState(table, keyColumn, baseSegments, scanSegments, snapshot, memory, zonePruned) {
8379
9295
  const deltaSegments = baseSegments.filter(mutationSegmentKind);
9296
+ const upsertSegments = baseSegments.filter((segment) => segment.kind === "upsert");
8380
9297
  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
- }
9298
+ for (const segment of [...deltaSegments, ...upsertSegments]) {
9299
+ for (const blockId of segment.columnBlockIds[keyColumn.id] ?? [])
9300
+ deltaBlockIds.add(blockId);
8386
9301
  }
8387
9302
  const decodedDeltaBlocks = /* @__PURE__ */ new Map();
8388
9303
  if (deltaBlockIds.size > 0) {
@@ -8413,11 +9328,32 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
8413
9328
  return vector;
8414
9329
  };
8415
9330
  const mutationKeyVectors = /* @__PURE__ */ new Map();
8416
- const mutationChangedVectors = /* @__PURE__ */ new Map();
9331
+ const blockStarts = /* @__PURE__ */ new Map();
9332
+ const layoutOf = (segment) => {
9333
+ const starts = [];
9334
+ let start = 0;
9335
+ for (const blockId of segment.columnBlockIds[keyColumn.id] ?? []) {
9336
+ starts.push(start);
9337
+ const decoded = decodedDeltaBlocks.get(blockId);
9338
+ if (decoded === void 0)
9339
+ throw new Error(`Visible block is missing: ${blockId}`);
9340
+ start += this.#blockColumnVector(blockId, decoded).length;
9341
+ }
9342
+ if (start !== segment.rowCount) {
9343
+ throw new Error(`Column row count mismatch: ${keyColumn.name}`);
9344
+ }
9345
+ return starts;
9346
+ };
9347
+ const locate = (segment, row) => {
9348
+ const starts = blockStarts.get(segment.id) ?? [];
9349
+ let blockIndex = starts.length - 1;
9350
+ while (blockIndex > 0 && (starts[blockIndex] ?? 0) > row)
9351
+ blockIndex -= 1;
9352
+ return { blockIndex, rowInBlock: row - (starts[blockIndex] ?? 0) };
9353
+ };
8417
9354
  const touched = /* @__PURE__ */ new Set();
8418
- let retainedBytes = 0;
9355
+ const retainedBytes = 0;
8419
9356
  for (const segment of deltaSegments) {
8420
- const kind = segment.kind;
8421
9357
  const keyVector = await deltaVector(keyColumn, segment);
8422
9358
  memory.reserve(columnVectorRetainedBytes(keyVector), "Streamed mutation replay");
8423
9359
  mutationKeyVectors.set(segment.id, keyVector);
@@ -8425,21 +9361,16 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
8425
9361
  for (let row = 0; row < segment.rowCount; row += 1) {
8426
9362
  touched.add(readMutationKey(row));
8427
9363
  }
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
- }
9364
+ if (segment.kind === "update")
9365
+ blockStarts.set(segment.id, layoutOf(segment));
9366
+ }
9367
+ for (const segment of upsertSegments) {
9368
+ const keyVector = await deltaVector(keyColumn, segment);
9369
+ memory.reserve(columnVectorRetainedBytes(keyVector), "Streamed mutation replay");
9370
+ mutationKeyVectors.set(segment.id, keyVector);
9371
+ const readMutationKey = requiredColumnVectorKeyReader(keyVector);
9372
+ for (let row = 0; row < segment.rowCount; row += 1)
9373
+ touched.add(readMutationKey(row));
8443
9374
  }
8444
9375
  const touchedByScanSegment = /* @__PURE__ */ new Map();
8445
9376
  const touchedPredicate = touchedKeyPredicate(keyColumn, touched);
@@ -8449,7 +9380,7 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
8449
9380
  const entries = [];
8450
9381
  touchedByScanSegment.set(segment.id, entries);
8451
9382
  let segmentRows = 0;
8452
- for (const blockId of segment.columnBlockIds[keyColumn.id] ?? []) {
9383
+ for (const [blockIndex, blockId] of (segment.columnBlockIds[keyColumn.id] ?? []).entries()) {
8453
9384
  const description = keyDescriptions.get(blockId);
8454
9385
  if (touchedPredicate !== void 0 && description !== void 0) {
8455
9386
  if (description.type !== keyColumn.type) {
@@ -8471,8 +9402,9 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
8471
9402
  const readBlockKey = requiredColumnVectorKeyReader(blockVector);
8472
9403
  for (let row = 0; row < rows; row += 1) {
8473
9404
  const key = readBlockKey(row);
8474
- if (touched.has(key))
8475
- entries.push({ key, slot: baseRows + segmentRows + row });
9405
+ if (touched.has(key)) {
9406
+ entries.push({ key, slot: baseRows + segmentRows + row, blockIndex, row });
9407
+ }
8476
9408
  }
8477
9409
  segmentRows += rows;
8478
9410
  }
@@ -8485,8 +9417,15 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
8485
9417
  const dead = new Uint8Array(Math.ceil(baseRows / 8));
8486
9418
  memory.reserve(dead.byteLength, "Streamed mutation replay");
8487
9419
  const slotByKey = /* @__PURE__ */ new Map();
8488
- const patches = /* @__PURE__ */ new Map();
9420
+ const lazyPatches = /* @__PURE__ */ new Map();
8489
9421
  let deadCount = 0;
9422
+ const layer = (slot, patch) => {
9423
+ const layers2 = lazyPatches.get(slot);
9424
+ if (layers2 === void 0)
9425
+ lazyPatches.set(slot, [patch]);
9426
+ else
9427
+ layers2.push(patch);
9428
+ };
8490
9429
  for (const segment of baseSegments) {
8491
9430
  const kind = segment.kind;
8492
9431
  if (kind === "insert" || kind === "base") {
@@ -8498,6 +9437,21 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
8498
9437
  }
8499
9438
  continue;
8500
9439
  }
9440
+ if (kind === "upsert") {
9441
+ for (const entry of touchedByScanSegment.get(segment.id) ?? []) {
9442
+ const existing = slotByKey.get(entry.key);
9443
+ if (existing === void 0) {
9444
+ slotByKey.set(entry.key, entry.slot);
9445
+ continue;
9446
+ }
9447
+ layer(existing, { segment, blockIndex: entry.blockIndex, row: entry.row });
9448
+ if (!bitmapHasValue(dead, entry.slot)) {
9449
+ setBitmapValue(dead, entry.slot);
9450
+ deadCount += 1;
9451
+ }
9452
+ }
9453
+ continue;
9454
+ }
8501
9455
  const keyVector = mutationKeyVectors.get(segment.id);
8502
9456
  if (keyVector === void 0) {
8503
9457
  throw new Error(`Mutation segment key vector is missing: ${segment.id}`);
@@ -8510,13 +9464,12 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
8510
9464
  if (slot !== void 0 && !bitmapHasValue(dead, slot)) {
8511
9465
  setBitmapValue(dead, slot);
8512
9466
  deadCount += 1;
8513
- patches.delete(slot);
9467
+ lazyPatches.delete(slot);
8514
9468
  }
8515
9469
  slotByKey.delete(key);
8516
9470
  }
8517
9471
  continue;
8518
9472
  }
8519
- const changed = mutationChangedVectors.get(segment.id) ?? /* @__PURE__ */ new Map();
8520
9473
  for (let row = 0; row < segment.rowCount; row += 1) {
8521
9474
  const slot = slotByKey.get(readKey(row));
8522
9475
  if (slot === void 0) {
@@ -8524,27 +9477,22 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
8524
9477
  continue;
8525
9478
  throw new Error(`Update segment references a missing key: ${segment.id}`);
8526
9479
  }
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 });
9480
+ const { blockIndex, rowInBlock } = locate(segment, row);
9481
+ layer(slot, { segment, blockIndex, row: rowInBlock });
8534
9482
  }
8535
9483
  }
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();
9484
+ let layers = 0;
9485
+ for (const slotLayers of lazyPatches.values())
9486
+ layers += slotLayers.length;
9487
+ memory.tally(lazyPatches.size * 96 + layers * 48, "Streamed mutation replay");
9488
+ const patchedSlots = Uint32Array.from(lazyPatches.keys()).sort();
8541
9489
  return {
8542
9490
  baseRows,
8543
9491
  deadCount,
8544
9492
  dead,
8545
- patches,
9493
+ lazyPatches,
8546
9494
  patchedSlots,
8547
- bytes: dead.byteLength + retainedBytes + patchedSlots.byteLength + patches.size * 96 + patchCells * 48
9495
+ bytes: dead.byteLength + retainedBytes + patchedSlots.byteLength + lazyPatches.size * 96 + layers * 48
8548
9496
  };
8549
9497
  }
8550
9498
  async #executeBlock(block, snapshot, visibility, memory, realTables, typedSchemas, extraInputs, cacheResults = true, allowSpill = true, forceSpill = false, spillPageRows, signal) {
@@ -9197,6 +10145,7 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
9197
10145
  message: error instanceof Error ? error.message : String(error),
9198
10146
  at
9199
10147
  };
10148
+ this.#reportBackgroundError(error, "auto collection");
9200
10149
  this.#scheduleAutoCollectionRetry();
9201
10150
  }).finally(() => {
9202
10151
  if (this.#autoCollectionTask === run)
@@ -9217,6 +10166,12 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
9217
10166
  this.#autoCollectionTask = run;
9218
10167
  void run;
9219
10168
  }
10169
+ #reportBackgroundError(error, context) {
10170
+ try {
10171
+ this.#onBackgroundError?.(error, context);
10172
+ } catch {
10173
+ }
10174
+ }
9220
10175
  #scheduleAutoCollectionRetry() {
9221
10176
  if (this.#closed || !this.#autoCollect || this.#autoCollectionRetryTimer !== void 0)
9222
10177
  return;
@@ -12494,6 +13449,19 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
12494
13449
  }
12495
13450
  return surviving;
12496
13451
  }
13452
+ async #indexPrunedSegments(table, segments, plan, snapshot, visibility) {
13453
+ const own = overlayOwnedSegments(segments, visibility);
13454
+ if (own.length === 0) {
13455
+ const ftsSegments2 = await this.#ftsPrunedSegments(table, segments, plan, snapshot);
13456
+ return this.#secondaryIndexPrunedSegments(table, ftsSegments2, plan, snapshot);
13457
+ }
13458
+ if (own.some((segment) => segment.kind !== "insert"))
13459
+ return { segments, pruned: false };
13460
+ const committed = segments.filter((segment) => segment.transactionId !== visibility?.overlayTransactionId);
13461
+ const ftsSegments = await this.#ftsPrunedSegments(table, committed, plan, snapshot);
13462
+ const indexed = await this.#secondaryIndexPrunedSegments(table, ftsSegments, plan, snapshot);
13463
+ return { segments: [...indexed.segments, ...own], pruned: indexed.pruned };
13464
+ }
12497
13465
  async #secondaryIndexPrunedSegments(table, segments, plan, snapshot) {
12498
13466
  const predicates = secondaryIndexPredicates(plan, table);
12499
13467
  if (predicates.length === 0) {
@@ -12769,13 +13737,12 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
12769
13737
  }
12770
13738
  async #materializeColumnarTableAtSnapshot(table, snapshot, projectedColumns, visibility, plan) {
12771
13739
  const visibleSegments = await this.#visibleSegmentRecords(table, snapshot, visibility);
12772
- if (plan !== void 0) {
13740
+ if (plan !== void 0 && overlayOwnedSegments(visibleSegments, visibility).length === 0) {
12773
13741
  const covering = await this.#secondaryIndexCoveringTable(table, projectedColumns, visibleSegments, plan, snapshot);
12774
13742
  if (covering !== void 0)
12775
13743
  return covering;
12776
13744
  }
12777
- const ftsSegments = plan === void 0 ? visibleSegments : await this.#ftsPrunedSegments(table, visibleSegments, plan, snapshot);
12778
- const indexed = plan === void 0 ? { segments: ftsSegments, pruned: false } : await this.#secondaryIndexPrunedSegments(table, ftsSegments, plan, snapshot);
13745
+ const indexed = plan === void 0 ? { segments: visibleSegments, pruned: false } : await this.#indexPrunedSegments(table, visibleSegments, plan, snapshot, visibility);
12779
13746
  const segments = indexed.segments;
12780
13747
  const keyColumn = getUniqueKeyColumn(table);
12781
13748
  if (segments.every((segment) => {
@@ -13660,14 +14627,17 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
13660
14627
  async #visibleSegmentRecordsUncached(table, snapshot, catalog) {
13661
14628
  const segments = catalog === void 0 ? await listTableSegmentsPaged(this.store, table.id) : catalog.segmentsByTable.get(table.id) ?? [];
13662
14629
  const transactions = catalog?.transactions ?? new Map((await this.#transactionRecordsForSegments(segments)).map((record) => [record.id, record]));
14630
+ const overlay = [];
13663
14631
  const versionEligible = segments.filter((segment) => {
13664
- if (segment.transactionId === catalog?.overlayTransactionId)
13665
- return true;
14632
+ if (segment.transactionId === catalog?.overlayTransactionId) {
14633
+ overlay.push(segment);
14634
+ return false;
14635
+ }
13666
14636
  const committedVersion = transactions.get(segment.transactionId)?.committedVersion;
13667
14637
  return snapshot.version !== null && committedVersion !== null && committedVersion !== void 0 && committedVersion <= snapshot.version;
13668
14638
  });
13669
- const visible = await filterSnapshotSegments(snapshot, versionEligible);
13670
- return [...visible].sort((left, right) => {
14639
+ const visible = [...await filterSnapshotSegments(snapshot, versionEligible), ...overlay];
14640
+ return visible.sort((left, right) => {
13671
14641
  const leftVersion = transactions.get(left.transactionId)?.committedVersion ?? -1;
13672
14642
  const rightVersion = transactions.get(right.transactionId)?.committedVersion ?? -1;
13673
14643
  const leftOrder = left.logicalOrder;
@@ -13675,8 +14645,8 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
13675
14645
  return leftOrder - rightOrder || leftVersion - rightVersion || left.commitOrdinal - right.commitOrdinal || left.id.localeCompare(right.id);
13676
14646
  });
13677
14647
  }
13678
- async #transactionRecordsForSegments(segments) {
13679
- const transactionIds = [...new Set(segments.map((segment) => segment.transactionId))];
14648
+ async #transactionRecordsForSegments(segments, excludeTransactionId) {
14649
+ const transactionIds = [...new Set(segments.map((segment) => segment.transactionId))].filter((id) => id !== excludeTransactionId);
13680
14650
  const records = [];
13681
14651
  for (let start = 0; start < transactionIds.length; start += 64) {
13682
14652
  const window = transactionIds.slice(start, start + 64);
@@ -13782,13 +14752,23 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
13782
14752
  this.#gzipVerdicts.delete(columnId);
13783
14753
  return bytes;
13784
14754
  }
13785
- async #findTable(name) {
14755
+ #scopeTableRecords = /* @__PURE__ */ new WeakMap();
14756
+ async #findTable(name, scope) {
13786
14757
  validateName(name, "Table");
14758
+ const cache = scope === void 0 || !this.#scopeWrites.has(scope) ? void 0 : this.#scopeTableRecords.get(scope) ?? (() => {
14759
+ const created = /* @__PURE__ */ new Map();
14760
+ this.#scopeTableRecords.set(scope, created);
14761
+ return created;
14762
+ })();
14763
+ const cached = cache?.get(name);
14764
+ if (cached !== void 0)
14765
+ return cached;
13787
14766
  const table = await this.store.getTableByName(name) ?? await this.store.getTableByName(await this.#foldTableName(name));
13788
14767
  if (table === void 0)
13789
14768
  throw new UnknownTableError(name);
13790
14769
  if (table.view !== void 0)
13791
14770
  throw new TypeError(`${name} is a view, not a table`);
14771
+ cache?.set(name, table);
13792
14772
  return table;
13793
14773
  }
13794
14774
  async #assertVisibleSegmentCursorTable(tableName, capturedTableId) {
@@ -14875,6 +15855,18 @@ function assertNoDuplicateUniqueTerms(index, terms) {
14875
15855
  seen.add(term);
14876
15856
  }
14877
15857
  }
15858
+ function assertBatchSecondaryTermsDistinct(table, input) {
15859
+ const rowCount = input.rowCount ?? Object.values(input.columns)[0]?.length ?? 0;
15860
+ for (const { index, columns } of readyUniqueSecondaryIndexes(table)) {
15861
+ const terms = [];
15862
+ for (let row = 0; row < rowCount; row += 1) {
15863
+ const term = secondaryUniqueTerm(index, columns, columns.map((column) => input.columns[column.name]?.[row] ?? null));
15864
+ if (term !== void 0)
15865
+ terms.push(term);
15866
+ }
15867
+ assertNoDuplicateUniqueTerms(index, terms);
15868
+ }
15869
+ }
14878
15870
  function stageSecondaryUniqueInsertChanges(transaction, table, input, oldImages) {
14879
15871
  const rowCount = input.rowCount ?? Object.values(input.columns)[0]?.length ?? 0;
14880
15872
  for (const { indexId, index, columns } of readyUniqueSecondaryIndexes(table)) {
@@ -14927,7 +15919,10 @@ function stageSecondaryUniqueMutationChanges(transaction, table, input, oldImage
14927
15919
  const added = oldImages.flatMap((old, row) => {
14928
15920
  if (old === void 0)
14929
15921
  return [];
14930
- const term = secondaryUniqueTerm(index, columns, columns.map((column) => input.changes[column.name]?.[row] ?? old[column.name] ?? null));
15922
+ const term = secondaryUniqueTerm(index, columns, columns.map((column) => {
15923
+ const assigned = input.changes[column.name];
15924
+ return assigned === void 0 ? old[column.name] ?? null : assigned[row] ?? null;
15925
+ }));
14931
15926
  return term === void 0 ? [] : [term];
14932
15927
  });
14933
15928
  assertNoDuplicateUniqueTerms(index, added);
@@ -15593,6 +16588,24 @@ function boundedExpiryMilliseconds(nowMs, ttlMs) {
15593
16588
  }
15594
16589
  return expiresAt;
15595
16590
  }
16591
+ function plainLiteralValue(expression) {
16592
+ if (expression.kind !== "literal" || expression.internalSqlValue === true || expression.sqlDomain !== void 0 || expression.exactText !== void 0 || expression.decimal === true) {
16593
+ return void 0;
16594
+ }
16595
+ return expression.value;
16596
+ }
16597
+ function valueMatchesColumnType(column, value) {
16598
+ switch (column.type) {
16599
+ case "number":
16600
+ return typeof value === "number" && Number.isFinite(value);
16601
+ case "string":
16602
+ return typeof value === "string";
16603
+ case "boolean":
16604
+ return typeof value === "boolean";
16605
+ case "datetime":
16606
+ return value instanceof Date;
16607
+ }
16608
+ }
15596
16609
  function keyToken(type, value) {
15597
16610
  if (value === null)
15598
16611
  throw new TypeError("Unique key cannot be null");
@@ -16240,9 +17253,17 @@ function overlayWindowCompacted(inner, innerWindowStart, steps, rows, patches, c
16240
17253
  const values = inner.kind === "boolean" ? new Uint8Array(rows) : inner.kind === "string" ? void 0 : new Float64Array(rows);
16241
17254
  const codes = inner.kind === "string" ? new Uint32Array(rows) : void 0;
16242
17255
  codes?.fill(NULL_STRING_VECTOR_CODE);
16243
- let dictionary = inner.kind === "string" ? inner.dictionary : void 0;
16244
- let dictionaryIndex;
16245
- let dictionaryCopied = false;
17256
+ let patched = false;
17257
+ if (inner.kind === "string" && patches !== void 0) {
17258
+ for (let index = 0; index < steps.length; index += 2) {
17259
+ if ((steps[index + 1] ?? 0) === 0 && patches.get(steps[index] ?? 0)?.has(column.id) === true) {
17260
+ patched = true;
17261
+ break;
17262
+ }
17263
+ }
17264
+ }
17265
+ const dictionary = inner.kind === "string" ? patched ? [] : inner.dictionary : void 0;
17266
+ const dictionaryIndex = patched ? /* @__PURE__ */ new Map() : void 0;
16246
17267
  const target = codes !== void 0 ? { kind: "string", length: rows, validity, codes, dictionary: dictionary ?? [] } : { kind: inner.kind, length: rows, validity, values };
16247
17268
  let out = 0;
16248
17269
  for (let index = 0; index < steps.length; index += 2) {
@@ -16251,22 +17272,22 @@ function overlayWindowCompacted(inner, innerWindowStart, steps, rows, patches, c
16251
17272
  const patch = length === 0 ? patches?.get(start)?.get(column.id) : void 0;
16252
17273
  if (patch === void 0) {
16253
17274
  const count = Math.max(1, length);
16254
- copyVectorSpan(inner, start - innerWindowStart, count, target, out);
17275
+ if (dictionaryIndex === void 0) {
17276
+ copyVectorSpan(inner, start - innerWindowStart, count, target, out);
17277
+ } else {
17278
+ for (let row = 0; row < count; row += 1) {
17279
+ copyColumnVectorValue(inner, start - innerWindowStart + row, target, out + row, dictionaryIndex);
17280
+ }
17281
+ }
16255
17282
  out += count;
16256
17283
  continue;
16257
17284
  }
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
17285
  copyColumnVectorValue(patch.vector, patch.row, target, out, dictionaryIndex);
16265
17286
  out += 1;
16266
17287
  }
16267
17288
  if (out !== rows)
16268
17289
  throw new Error(`Column row count mismatch: ${column.name}`);
16269
- if (dictionaryCopied && dictionary !== void 0) {
17290
+ if (patched && dictionary !== void 0) {
16270
17291
  let dictionaryBytes = 0;
16271
17292
  for (const value of dictionary)
16272
17293
  dictionaryBytes += 16 + value.length * 2;
@@ -17667,6 +18688,10 @@ function garbageCollectionProgress(job) {
17667
18688
  function firesAfterTriggers(table, ...events) {
17668
18689
  return (table.triggers ?? []).some((trigger) => trigger.timing === "after" && events.includes(trigger.event));
17669
18690
  }
18691
+ function overlayOwnedSegments(segments, visibility) {
18692
+ const id = visibility?.overlayTransactionId;
18693
+ return id === void 0 ? [] : segments.filter((segment) => segment.transactionId === id);
18694
+ }
17670
18695
  function collectRealTableNames(plan) {
17671
18696
  const names = /* @__PURE__ */ new Set();
17672
18697
  const excluded = /* @__PURE__ */ new Set();