@minnowdb/core 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +5 -5
  2. package/dist/engine/cancellation.d.ts +2 -0
  3. package/dist/engine/cancellation.js +4 -0
  4. package/dist/engine/catalog.d.ts +5 -1
  5. package/dist/engine/catalog.js +5 -1
  6. package/dist/engine/client.d.ts +34 -6
  7. package/dist/engine/client.js +87 -19
  8. package/dist/engine/database.d.ts +43 -20
  9. package/dist/engine/database.js +823 -164
  10. package/dist/engine/defaults.js +11 -0
  11. package/dist/engine/errors.d.ts +19 -0
  12. package/dist/engine/errors.js +31 -0
  13. package/dist/engine/fts.d.ts +2 -15
  14. package/dist/engine/live.d.ts +1 -7
  15. package/dist/engine/live.js +12 -13
  16. package/dist/engine/optimizer.js +546 -39
  17. package/dist/engine/query-cache.js +1 -0
  18. package/dist/engine/query.d.ts +16 -278
  19. package/dist/engine/query.js +260 -74
  20. package/dist/engine/result-wire.d.ts +2 -0
  21. package/dist/engine/result-wire.js +21 -5
  22. package/dist/engine/schema-wire.d.ts +14 -1
  23. package/dist/engine/schema-wire.js +7 -1
  24. package/dist/engine/schema.d.ts +83 -32
  25. package/dist/engine/schema.js +180 -14
  26. package/dist/engine/sql-domains.d.ts +11 -0
  27. package/dist/engine/sql-domains.js +65 -1
  28. package/dist/engine/sql-json.js +22 -3
  29. package/dist/engine/sql-semantics.js +21 -3
  30. package/dist/engine/vector.d.ts +2 -2
  31. package/dist/engine/vector.js +328 -79
  32. package/dist/engine/worker-host.js +119 -44
  33. package/dist/plan/index.d.ts +5 -4
  34. package/dist/plan/index.js +3 -3
  35. package/dist/plan/model.d.ts +218 -0
  36. package/dist/plan/model.js +1 -0
  37. package/dist/storage/indexeddb.js +4 -12
  38. package/dist/storage/toolkit/record-core.js +7 -22
  39. package/dist/storage/types.d.ts +26 -8
  40. package/dist/storage/types.js +85 -0
  41. package/dist/transactions/index.d.ts +5 -3
  42. package/dist/transactions/index.js +58 -8
  43. package/dist/worker-protocol/index.d.ts +6 -1
  44. package/dist/worker-protocol/index.js +5 -2
  45. package/package.json +1 -1
  46. package/postgres-feature-profile.json +5 -0
  47. package/sql-feature-matrix.json +75 -19
@@ -12,6 +12,54 @@ const stageOps = ["insertBatch", "upsertBatch", "updateBatch", "deleteBatch"];
12
12
  function isStageOp(value) {
13
13
  return typeof value === "string" && stageOps.includes(value);
14
14
  }
15
+ /**
16
+ * Root methods whose arguments and results are already structured-clone-safe. `satisfies` makes
17
+ * a removed or renamed database method a compile error, while the runtime guard below fails
18
+ * closed if a caller supplies a malformed database object through the manual host seam.
19
+ */
20
+ const directRootMethods = [
21
+ "createTable",
22
+ "createView",
23
+ "dropView",
24
+ "dropColumn",
25
+ "dropTable",
26
+ "createIndex",
27
+ "dropIndex",
28
+ "buildFtsIndex",
29
+ "introspect",
30
+ "listTables",
31
+ "insertBatch",
32
+ "insert",
33
+ "upsertBatch",
34
+ "upsert",
35
+ "updateBatch",
36
+ "update",
37
+ "deleteBatch",
38
+ "delete",
39
+ "runStatement",
40
+ "explain",
41
+ "execute",
42
+ "listVisibleSegmentPage",
43
+ "cleanupQuerySpill",
44
+ "compactTable",
45
+ "compactTableStep",
46
+ "resumeCompactionJob",
47
+ "listCompactionJobs",
48
+ "cancelCompactionJob",
49
+ "collectGarbage",
50
+ "collectGarbageStep",
51
+ "resumeGarbageCollectionJob",
52
+ "bufferPoolStats",
53
+ "maintenanceStatus",
54
+ "checkIntegrity",
55
+ "storageStats",
56
+ "inspectInterruptedImport",
57
+ "abortInterruptedImport",
58
+ "listGarbageCollectionJobs",
59
+ ];
60
+ function isDirectRootMethod(value) {
61
+ return directRootMethods.includes(value);
62
+ }
15
63
  /**
16
64
  * A call result that travels as a columnar frame with its buffers transferred. Only query
17
65
  * results take this path: rows of objects are what structured clone is slowest at, so they are
@@ -120,10 +168,17 @@ class SnapshotImportChunkQueue {
120
168
  function snapshotQueueError(value) {
121
169
  return value instanceof Error ? value : new Error("Snapshot transfer failed", { cause: value });
122
170
  }
171
+ function abortError(message) {
172
+ const error = new Error(message);
173
+ error.name = "AbortError";
174
+ return error;
175
+ }
123
176
  /** Hard per-connection ceiling for resident worker resources abandoned by a client. */
124
177
  export const MAX_WORKER_HANDLES_PER_CONNECTION = 256;
125
178
  const DEFAULT_WRITE_HANDLE_IDLE_TIMEOUT_MS = 30_000;
126
179
  const MAX_TIMER_DELAY_MS = 2_147_483_647;
180
+ /** Shared by RPC methods that do not expose cancellation, avoiding one controller per call. */
181
+ const passiveRpcSignal = new AbortController().signal;
127
182
  class DatabaseRpcServer {
128
183
  database;
129
184
  scope;
@@ -132,6 +187,7 @@ class DatabaseRpcServer {
132
187
  #reservedHandleIds = new Set();
133
188
  #openingHandlePromises = new Set();
134
189
  #settlingWritePromises = new Set();
190
+ #requestAborts = new Map();
135
191
  #disposed = false;
136
192
  #inFlightRpcCount = 0;
137
193
  #inFlightRpcDrain;
@@ -154,6 +210,12 @@ class DatabaseRpcServer {
154
210
  this.scope.postMessage(rpcResult(request.requestId, { ready: true }));
155
211
  return;
156
212
  }
213
+ if (request.kind === "rpc-cancel") {
214
+ this.#requestAborts
215
+ .get(request.requestId)
216
+ ?.abort(abortError("Database request was cancelled"));
217
+ return;
218
+ }
157
219
  const bypassLimit = request.method === "dispose";
158
220
  if (!bypassLimit && this.#inFlightRpcCount >= MAX_DATABASE_RPC_IN_FLIGHT) {
159
221
  this.scope.postMessage(rpcFailure(request.requestId, new RangeError(`A database worker connection cannot hold more than ${String(MAX_DATABASE_RPC_IN_FLIGHT)} in-flight requests`)));
@@ -161,12 +223,19 @@ class DatabaseRpcServer {
161
223
  }
162
224
  if (!bypassLimit)
163
225
  this.#inFlightRpcCount += 1;
226
+ const abort = request.method === "query" ? new AbortController() : undefined;
227
+ if (abort !== undefined)
228
+ this.#requestAborts.set(request.requestId, abort);
229
+ const context = {
230
+ requestId: request.requestId,
231
+ signal: abort?.signal ?? passiveRpcSignal,
232
+ };
164
233
  try {
165
234
  if (this.#disposed)
166
235
  throw new Error("Database connection is disposed");
167
236
  const result = request.handleId === null
168
- ? await this.#callRoot(request.method, request.args)
169
- : await this.#callHandle(request.handleId, request.method, request.args);
237
+ ? await this.#callRoot(request.method, request.args, context)
238
+ : await this.#callHandle(request.handleId, request.method, request.args, context);
170
239
  if (result instanceof ColumnarResult) {
171
240
  this.scope.postMessage(rpcResult(request.requestId, result.encoded.payload), {
172
241
  transfer: result.encoded.transfer,
@@ -190,6 +259,8 @@ class DatabaseRpcServer {
190
259
  this.scope.postMessage(rpcFailure(request.requestId, error));
191
260
  }
192
261
  finally {
262
+ if (abort !== undefined)
263
+ this.#requestAborts.delete(request.requestId);
193
264
  if (!bypassLimit)
194
265
  this.#inFlightRpcCount -= 1;
195
266
  if (this.#inFlightRpcCount === 0 && this.#resolveInFlightRpcDrain !== undefined) {
@@ -200,50 +271,44 @@ class DatabaseRpcServer {
200
271
  }
201
272
  }
202
273
  }
203
- async #callRoot(method, args) {
274
+ async #callRoot(method, args, context) {
204
275
  const database = this.database;
276
+ if (isDirectRootMethod(method)) {
277
+ const operation = database[method];
278
+ if (operation === undefined) {
279
+ throw new Error(`Database root method is unavailable: ${method}`);
280
+ }
281
+ return operation.apply(this.database, args);
282
+ }
205
283
  switch (method) {
206
- case "createTable":
207
- case "introspect":
208
- case "listTables":
209
- case "insertBatch":
210
- case "insert":
211
- case "upsertBatch":
212
- case "upsert":
213
- case "updateBatch":
214
- case "update":
215
- case "deleteBatch":
216
- case "delete":
217
- case "runStatement":
218
- case "explain":
219
- case "execute":
220
- case "listVisibleSegmentPage":
221
- case "cleanupQuerySpill":
222
- case "compactTable":
223
- case "compactTableStep":
224
- case "resumeCompactionJob":
225
- case "listCompactionJobs":
226
- case "cancelCompactionJob":
227
- case "collectGarbage":
228
- case "collectGarbageStep":
229
- case "resumeGarbageCollectionJob":
230
- case "bufferPoolStats":
231
- case "maintenanceStatus":
232
- case "checkIntegrity":
233
- case "storageStats":
234
- case "inspectInterruptedImport":
235
- case "abortInterruptedImport":
236
- case "listGarbageCollectionJobs":
237
- return database[method]?.(...args);
238
284
  case "query": {
239
- const [sql, options] = args;
240
- return new ColumnarResult(encodeQueryResult(await this.database.query(sql, options)));
285
+ const [sql, options, reportStats = false] = args;
286
+ return new ColumnarResult(encodeQueryResult(await this.database.query(sql, {
287
+ ...options,
288
+ signal: context.signal,
289
+ ...(reportStats
290
+ ? {
291
+ onStats: (stats) => {
292
+ this.scope.postMessage(rpcEvent(context.requestId, "stats", stats));
293
+ },
294
+ }
295
+ : {}),
296
+ })));
241
297
  }
242
298
  case "queryCursorOpen": {
243
299
  const handleId = this.#claimHandleId(args[0]);
244
- const [sql, options] = args.slice(1);
300
+ const [sql, options, reportStats = false] = args.slice(1);
245
301
  try {
246
- const iterator = this.database.queryCursor(sql, options);
302
+ const iterator = this.database.queryCursor(sql, {
303
+ ...options,
304
+ ...(reportStats
305
+ ? {
306
+ onStats: (stats) => {
307
+ this.scope.postMessage(rpcEvent(handleId, "stats", stats));
308
+ },
309
+ }
310
+ : {}),
311
+ });
247
312
  this.#publishHandle(handleId, { type: "query-cursor", iterator });
248
313
  }
249
314
  catch (error) {
@@ -465,14 +530,14 @@ class DatabaseRpcServer {
465
530
  void opening.then(() => this.#openingHandlePromises.delete(opening), () => this.#openingHandlePromises.delete(opening));
466
531
  return opening;
467
532
  }
468
- async #callHandle(handleId, method, args) {
533
+ async #callHandle(handleId, method, args, context) {
469
534
  const handle = this.#handles.get(handleId);
470
535
  if (handle === undefined)
471
536
  throw new Error(`Unknown handle: ${handleId}`);
472
537
  if (handle.type === "write") {
473
538
  this.#beginWriteHandleCall(handle);
474
539
  try {
475
- return await this.#callWriteHandle(handleId, handle, method, args);
540
+ return await this.#callWriteHandle(handleId, handle, method, args, context);
476
541
  }
477
542
  finally {
478
543
  this.#endWriteHandleCall(handleId, handle);
@@ -523,10 +588,20 @@ class DatabaseRpcServer {
523
588
  }
524
589
  }
525
590
  }
526
- async #callWriteHandle(handleId, handle, method, args) {
591
+ async #callWriteHandle(handleId, handle, method, args, context) {
527
592
  if (method === "query") {
528
- const [sql, options] = args;
529
- return new ColumnarResult(encodeQueryResult(await handle.session.query(sql, options)));
593
+ const [sql, options, reportStats = false] = args;
594
+ return new ColumnarResult(encodeQueryResult(await handle.session.query(sql, {
595
+ ...options,
596
+ signal: context.signal,
597
+ ...(reportStats
598
+ ? {
599
+ onStats: (stats) => {
600
+ this.scope.postMessage(rpcEvent(context.requestId, "stats", stats));
601
+ },
602
+ }
603
+ : {}),
604
+ })));
530
605
  }
531
606
  if (method === "execute") {
532
607
  const [sql, params] = args;
@@ -7,10 +7,11 @@
7
7
  * Exposing them here lets an external builder produce plans the engine treats as indistinguishable
8
8
  * from parsed SQL.
9
9
  *
10
- * This module re-exports rather than owns: the plan types still live beside the parser that
11
- * produces them. Splitting them into a leaf module of their own is the next step, and it will not
12
- * change this entry point.
10
+ * The recursive logical model lives in the dependency-light `model` leaf; executable assembly
11
+ * and validation stay with the compiler. This entry point keeps that distinction invisible to
12
+ * consumers.
13
13
  */
14
- export { assembleSelectBlock, compoundSelectBlock, derivedTableSource, hasAggregate, splitCondition, validateLimit, validateOffset, type AggregateName, type CompiledQuery, type CompiledStatement, type Expression, type JoinPlan, type Predicate, type PredicateOperator, type QueryResult, type QueryRow, type QueryValue, type SelectItem, type SetOperator, type TableSource, type WindowFunctionName, } from "../engine/query.js";
14
+ export { assembleSelectBlock, compoundSelectBlock, derivedTableSource, hasAggregate, splitCondition, validateLimit, validateOffset, type CompiledStatement, } from "../engine/query.js";
15
+ export type { AggregateName, CompiledQuery, Expression, JoinPlan, Predicate, PredicateOperator, QueryResult, QueryRow, QueryValue, SelectItem, SetOperator, TableSource, WindowFunctionName, } from "./model.js";
15
16
  export { optimizePlan, renderPlan } from "../engine/optimizer.js";
16
17
  export { validateFtsQuery } from "../engine/fts.js";
@@ -7,9 +7,9 @@
7
7
  * Exposing them here lets an external builder produce plans the engine treats as indistinguishable
8
8
  * from parsed SQL.
9
9
  *
10
- * This module re-exports rather than owns: the plan types still live beside the parser that
11
- * produces them. Splitting them into a leaf module of their own is the next step, and it will not
12
- * change this entry point.
10
+ * The recursive logical model lives in the dependency-light `model` leaf; executable assembly
11
+ * and validation stay with the compiler. This entry point keeps that distinction invisible to
12
+ * consumers.
13
13
  */
14
14
  export { assembleSelectBlock, compoundSelectBlock, derivedTableSource, hasAggregate, splitCondition, validateLimit, validateOffset, } from "../engine/query.js";
15
15
  export { optimizePlan, renderPlan } from "../engine/optimizer.js";
@@ -0,0 +1,218 @@
1
+ /**
2
+ * The dependency-light logical plan shared by the SQL compiler, optimizer, vector executor, and
3
+ * external typed-query builders. Keep executable parser/engine code out of this module: importing
4
+ * these types must never create a runtime dependency edge.
5
+ */
6
+ import type { SqlDomain } from "../storage/types.js";
7
+ export type QueryValue = boolean | number | string | Date | null;
8
+ export type QueryRow = Record<string, QueryValue>;
9
+ export interface QueryResult {
10
+ columns: string[];
11
+ /** Logical SQL domain for each output column, positionally aligned with `columns`. */
12
+ columnDomains: Array<SqlDomain | null>;
13
+ rows: QueryRow[];
14
+ }
15
+ export type BinaryOperator = "+" | "-" | "*" | "/" | "%" | "||";
16
+ export type ComparisonOperator = "=" | "!=" | "<>" | ">" | ">=" | "<" | "<=";
17
+ export type AggregateName = "COUNT" | "SUM" | "AVG" | "MIN" | "MAX" | "JSON_ARRAYAGG" | "STRING_AGG";
18
+ export type ScalarFunctionName = "ROUND" | "COALESCE" | "DATE_TRUNC" | "DATE_ADD" | "UPPER" | "LOWER" | "LENGTH" | "ABS" | "TRIM" | "LTRIM" | "RTRIM" | "SUBSTR" | "REPLACE" | "INSTR" | "NULLIF" | "GREATEST" | "LEAST" | "FLOOR" | "CEIL" | "MOD" | "POWER" | "SQRT" | "EXTRACT" | "CAST" | "OCTET_LENGTH" | "LPAD" | "RPAD" | "OVERLAY" | "CURRENT_DATE" | "CURRENT_TIMESTAMP" | "LOCALTIME" | "GROUPING" | "JSON_VALUE" | "JSON_QUERY" | "JSON_EXISTS" | "JSON_OBJECT" | "JSON_ARRAY" | "IS_JSON" | "ARRAY"
19
+ /** Optimizer-only, prefix-free equality key for hashable multi-column decorrelation. */
20
+ | "MINNOW_TUPLE_KEY"
21
+ /** Parser-produced wrapper carrying one explicit collation through ordering/comparison. */
22
+ | "MINNOW_COLLATE" | "NEXTVAL" | "CURRVAL" | "RANDOM" | "GEN_RANDOM_UUID";
23
+ /** Exact BM25 corpus statistics attached to a cloned scoring node before execution. */
24
+ export interface FtsStats {
25
+ /** Every row of the corpus, including all-null documents. */
26
+ docCount: number;
27
+ /** Total tokens across all documents. */
28
+ totalTokens: number;
29
+ /** Documents containing each query term, aligned with the query's term order. */
30
+ dfByTerm: number[];
31
+ }
32
+ export type Expression = {
33
+ kind: "literal";
34
+ value: QueryValue;
35
+ internalSqlValue?: true;
36
+ sqlDomain?: SqlDomain;
37
+ }
38
+ /** A `?` or `$n` placeholder; `index` is 0-based. Replaced by a literal at bind time. */
39
+ | {
40
+ kind: "parameter";
41
+ index: number;
42
+ } | {
43
+ kind: "column";
44
+ reference: string;
45
+ }
46
+ /** `*`, or `alias.*` when `table` is set. */
47
+ | {
48
+ kind: "wildcard";
49
+ table?: string;
50
+ } | {
51
+ kind: "binary";
52
+ operator: BinaryOperator;
53
+ left: Expression;
54
+ right: Expression;
55
+ } | {
56
+ kind: "call";
57
+ name: AggregateName | ScalarFunctionName;
58
+ arguments: Expression[];
59
+ distinct?: boolean;
60
+ aggregateOrderBy?: Array<{
61
+ expression: Expression;
62
+ direction: "asc" | "desc";
63
+ nulls?: "first" | "last";
64
+ }>;
65
+ } | {
66
+ kind: "list";
67
+ items: Expression[];
68
+ } | {
69
+ kind: "subquery";
70
+ block: CompiledQuery;
71
+ } | {
72
+ kind: "condition";
73
+ operator: PredicateOperator;
74
+ left: Expression;
75
+ right: Expression;
76
+ escape?: string;
77
+ } | {
78
+ kind: "logical";
79
+ operator: "and" | "or";
80
+ left: Expression;
81
+ right: Expression;
82
+ } | {
83
+ kind: "not";
84
+ operand: Expression;
85
+ } | {
86
+ kind: "exists";
87
+ block: CompiledQuery;
88
+ negated: boolean;
89
+ } | {
90
+ kind: "case";
91
+ branches: Array<{
92
+ when: Expression;
93
+ then: Expression;
94
+ }>;
95
+ otherwise?: Expression;
96
+ } | {
97
+ kind: "window";
98
+ name: WindowFunctionName;
99
+ partitionBy: Expression[];
100
+ orderBy: Array<{
101
+ expression: Expression;
102
+ direction: "asc" | "desc";
103
+ nulls?: "first" | "last";
104
+ }>;
105
+ argument?: Expression;
106
+ offset?: number;
107
+ fallback?: QueryValue;
108
+ frame?: WindowFrame;
109
+ } | {
110
+ kind: "fts";
111
+ op: "match" | "bm25";
112
+ /** Column references forming the document, or `*` for all searchable scan columns. */
113
+ columns: Expression[] | "*";
114
+ query: string;
115
+ queryParameter?: number;
116
+ stats?: FtsStats;
117
+ };
118
+ export type WindowFunctionName = "ROW_NUMBER" | "RANK" | "DENSE_RANK" | "PERCENT_RANK" | "CUME_DIST" | "NTILE" | "LAG" | "LEAD" | "FIRST_VALUE" | "LAST_VALUE" | "NTH_VALUE" | AggregateName;
119
+ export interface WindowFrameBound {
120
+ kind: "unbounded-preceding" | "preceding" | "current-row" | "following" | "unbounded-following";
121
+ offset?: number;
122
+ }
123
+ export type WindowFrameExclusion = "no-others" | "current-row" | "group" | "ties";
124
+ export interface WindowFrame {
125
+ unit: "rows" | "range" | "groups";
126
+ start: WindowFrameBound;
127
+ end: WindowFrameBound;
128
+ exclude?: WindowFrameExclusion;
129
+ }
130
+ export interface WindowSpec {
131
+ alias: string;
132
+ name: WindowFunctionName;
133
+ partitionAliases: string[];
134
+ orderAliases: Array<{
135
+ alias: string;
136
+ direction: "asc" | "desc";
137
+ nulls?: "first" | "last";
138
+ }>;
139
+ argumentAlias?: string;
140
+ offset?: number;
141
+ fallback?: QueryValue;
142
+ frame?: WindowFrame;
143
+ }
144
+ export interface SelectItem {
145
+ expression: Expression;
146
+ alias: string;
147
+ }
148
+ export interface TableSource {
149
+ table: string;
150
+ alias: string;
151
+ derived?: CompiledQuery;
152
+ union?: {
153
+ blocks: CompiledQuery[];
154
+ ops: SetOperator[];
155
+ };
156
+ recursive?: RecursiveCte;
157
+ windowed?: {
158
+ block: CompiledQuery;
159
+ windows: WindowSpec[];
160
+ };
161
+ columnAliases?: string[];
162
+ lateral?: true;
163
+ }
164
+ export interface JoinPlan extends TableSource {
165
+ kind: "inner" | "left" | "semi" | "anti";
166
+ left: Expression;
167
+ right: Expression;
168
+ on?: Expression;
169
+ full?: boolean;
170
+ natural?: boolean;
171
+ }
172
+ export type SetOperator = "union" | "union all" | "intersect" | "intersect all" | "except" | "except all";
173
+ export interface RecursiveCte {
174
+ reference: string;
175
+ base: CompiledQuery;
176
+ step: CompiledQuery;
177
+ all: boolean;
178
+ }
179
+ export type PredicateOperator = ComparisonOperator | `${ComparisonOperator} ANY` | `${ComparisonOperator} ALL` | "IN" | "NOT IN" | "IS NULL" | "IS NOT NULL" | "LIKE" | "NOT LIKE" | "ILIKE" | "NOT ILIKE" | "SIMILAR TO" | "NOT SIMILAR TO" | "IS DISTINCT FROM" | "IS NOT DISTINCT FROM" | "IS TRUE";
180
+ export interface Predicate {
181
+ left: Expression;
182
+ operator: PredicateOperator;
183
+ right: Expression;
184
+ escape?: string;
185
+ }
186
+ export interface CompiledQuery {
187
+ sql: string;
188
+ base: TableSource;
189
+ joins: JoinPlan[];
190
+ select: SelectItem[];
191
+ predicates: Predicate[];
192
+ groupBy: Expression[];
193
+ having: Predicate[];
194
+ orderBy: Array<{
195
+ expression: Expression;
196
+ direction: "asc" | "desc";
197
+ nulls?: "first" | "last";
198
+ }>;
199
+ limit?: number;
200
+ offset?: number;
201
+ distinctWildcard?: boolean;
202
+ limitParameter?: number;
203
+ offsetParameter?: number;
204
+ limitWithTies?: boolean;
205
+ parameterCount?: number;
206
+ usesStatementDatetime?: boolean;
207
+ usesSequenceCalls?: boolean;
208
+ usesVolatileFunctions?: boolean;
209
+ }
210
+ /** ORDER BY / LIMIT / OFFSET tail of a select or set operation. */
211
+ export interface SelectTail {
212
+ orderBy: CompiledQuery["orderBy"];
213
+ limit?: number;
214
+ offset?: number;
215
+ limitParameter?: number;
216
+ offsetParameter?: number;
217
+ limitWithTies?: boolean;
218
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,4 +1,4 @@
1
- import { CompactionJobConflictError, assertCompactionOutputProvenance, compactionOutputSegmentIds, CompactionBacklogError, createManifest, createGarbageCollectionJobRecord, storeNames, advanceGarbageCollectionJobRecord as advanceGarbageCollectionJobRecordUnchecked, BlockReadBatchTooLargeError, activePostingStorageColumnIds, assertTempRunPageBatchLimits, assertStorageBulkReadItems, assertTransactionArtifactBatchLimits, assertTransactionArtifactJournalLimits, boundedMaintenanceBatchItems, canonicalManifestChangedTableIds, catalogRecordRetainedBytes, manifestRecordRetainedReservationBytes, segmentRecordRetainedBytes, collectFtsPostings, ftsPostingQueryMatches, invalidateUncoveredFtsColumns, invalidateUncoveredSecondaryIndexes, GarbageCollectionJobConflictError, LeaseConflictError, LeaseExpiredError, LeaseOwnerConflictError, MAX_FTS_CANDIDATE_ROW_IDS, MAX_ACTIVE_UNIQUE_KEY_BUILDS, MAX_UNIQUE_KEY_BUILD_STAGED_BYTES, MAX_UNIQUE_KEY_BUILD_STAGED_BYTES_TOTAL, MAX_UNIQUE_KEY_BUILD_CHUNK_BYTES, MAX_UNIQUE_KEY_BUILD_TOKENS_PER_CHUNK, MAX_UNIQUE_KEY_BUILD_TTL_MS, MAX_FTS_BASE_CHUNKS, MAX_FTS_DELTA_CHUNKS, MAX_FTS_POSTINGS_PER_CHUNK, MAX_FTS_POSTING_ROW_IDS_PER_CHUNK, MAX_FTS_POSTING_TERM_CHARACTERS, MAX_FTS_TOKENS_PER_DOCUMENT, MAX_FTS_ORDERED_READ_BYTES, MAX_MANIFEST_BLOCK_PRESENCE_IDS, MAX_MANIFEST_RECORDS, MAX_MANIFEST_RETAINED_BYTES, MAX_BLOCK_READ_BATCH_BYTES, MAX_STORAGE_ID_CHARACTERS, MAX_LEASE_TTL_MS, MAX_ACTIVE_LEASES, MAX_ACTIVE_TRANSACTIONS, MAX_GLOBAL_STAGED_ARTIFACT_BYTES, MAX_GLOBAL_STAGED_BLOCKS, MAX_GLOBAL_STAGED_SEGMENTS, MAX_RETIRED_HISTORY_BYTES, MAX_PINNED_MANIFEST_VERSION_LAG, MAX_PINNED_RETIRED_BLOCKS, MAX_PINNED_RETIRED_BYTES, MAX_TERMINAL_TRANSACTION_RECORDS, MAX_ACTIVE_COMPACTION_JOBS, MAX_ACTIVE_GARBAGE_COLLECTION_JOBS, MAX_TERMINAL_COMPACTION_JOB_RECORDS, MAX_COMPLETED_GARBAGE_COLLECTION_JOB_RECORDS, MAX_CATALOG_RECORDS, MAX_CATALOG_RETAINED_BYTES, MAX_SEGMENT_RECORDS, MAX_SEGMENT_RETAINED_BYTES, MAX_ACTIVE_FTS_BASE_BUILDS, MAX_ACTIVE_SECONDARY_INDEX_BUILDS, MAX_ACCELERATOR_BUILD_STAGED_BYTES_TOTAL, MAX_ACCELERATOR_BUILD_STAGED_ENTRIES_TOTAL, MAX_POSTING_BUILD_TTL_MS, MAX_SNAPSHOT_SESSION_TTL_MS, MAX_SNAPSHOT_FRAME_BATCH_BYTES, MAX_SNAPSHOT_FRAME_BATCH_ITEMS, MAX_SNAPSHOT_METADATA_BATCH_BYTES, MAX_SNAPSHOT_METADATA_FRAME_BYTES, SNAPSHOT_FRAME_KINDS, MAX_LEVEL_ZERO_SEGMENTS, MAX_AUTO_INCREMENT_EXCLUSIVE_END, MAX_ROW_ID, MAX_ROW_ID_EXCLUSIVE_END, StorageCorruptionError, StorageFormatVersionError, IndexedDbSchemaUpgradeBlockedError, StorageResourceLimitError, SnapshotManifestMissingError, SnapshotImportConflictError, PostingBuildConflictError, TableInUseError, TableRecordConflictError, TempOwnerConflictError, MAX_TEMP_OWNER_TTL_MS, MAX_ACTIVE_TEMP_OWNERS, MAX_TEMP_RUNS_PER_OWNER, MAX_TEMP_PAGES_PER_OWNER, MAX_TEMP_RUNS_TOTAL, MAX_TEMP_PAGES_TOTAL, MAX_TEMP_BYTES_PER_OWNER, MAX_TEMP_BYTES_TOTAL, TransactionRecordConflictError, UniqueKeyConflictError, UniqueKeyBuildConflictError, UniqueIndexCoverageError, SchemaConflictError, normalizeCompactionJobRecord, normalizeGarbageCollectionJobRecord, normalizeSegmentRecord, updateCompactionJobRecord as updateCompactionJobRecordUnchecked, updateGarbageCollectionPlanningRecord, updateTransactionRecord as updateTransactionRecordUnchecked, validateColumnDefault, validateCatalogName, validateCanonicalManifestChangedTableIds, validateEnumValues, validateFtsOrderedReadLimits, validateFtsPostingQueries, validateStorageId, validateStorageDatabaseName, validateTableColumns, validateSecondaryIndexes, validateTableRecordBounds, secondaryIndexColumnIds, secondaryIndexWriteContractChanged, secondaryUniqueKeyNamespace, transactionCommitDeltaRetainedBytes, uniqueKeyBuildChunkRetainedBytes, WriteConflictError, } from "./types.js";
1
+ import { CompactionJobConflictError, assertCompactionOutputProvenance, compactionOutputSegmentIds, CompactionBacklogError, createManifest, createGarbageCollectionJobRecord, storeNames, advanceGarbageCollectionJobRecord as advanceGarbageCollectionJobRecordUnchecked, BlockReadBatchTooLargeError, activePostingStorageColumnIds, assertTempRunPageBatchLimits, assertStorageBulkReadItems, assertTransactionArtifactBatchLimits, assertTransactionArtifactJournalLimits, boundedMaintenanceBatchItems, canonicalManifestChangedTableIds, catalogRecordRetainedBytes, manifestRecordRetainedReservationBytes, segmentRecordRetainedBytes, collectFtsPostings, ftsPostingQueryMatches, invalidateUncoveredFtsColumns, invalidateUncoveredSecondaryIndexes, GarbageCollectionJobConflictError, LeaseConflictError, LeaseExpiredError, LeaseOwnerConflictError, MAX_FTS_CANDIDATE_ROW_IDS, MAX_ACTIVE_UNIQUE_KEY_BUILDS, MAX_UNIQUE_KEY_BUILD_STAGED_BYTES, MAX_UNIQUE_KEY_BUILD_STAGED_BYTES_TOTAL, MAX_UNIQUE_KEY_BUILD_CHUNK_BYTES, MAX_UNIQUE_KEY_BUILD_TOKENS_PER_CHUNK, MAX_UNIQUE_KEY_BUILD_TTL_MS, MAX_FTS_BASE_CHUNKS, MAX_FTS_DELTA_CHUNKS, MAX_FTS_POSTINGS_PER_CHUNK, MAX_FTS_POSTING_ROW_IDS_PER_CHUNK, MAX_FTS_POSTING_TERM_CHARACTERS, MAX_FTS_TOKENS_PER_DOCUMENT, MAX_FTS_ORDERED_READ_BYTES, MAX_MANIFEST_BLOCK_PRESENCE_IDS, MAX_MANIFEST_RECORDS, MAX_MANIFEST_RETAINED_BYTES, MAX_BLOCK_READ_BATCH_BYTES, MAX_STORAGE_ID_CHARACTERS, MAX_LEASE_TTL_MS, MAX_ACTIVE_LEASES, MAX_ACTIVE_TRANSACTIONS, MAX_GLOBAL_STAGED_ARTIFACT_BYTES, MAX_GLOBAL_STAGED_BLOCKS, MAX_GLOBAL_STAGED_SEGMENTS, MAX_RETIRED_HISTORY_BYTES, MAX_PINNED_MANIFEST_VERSION_LAG, MAX_PINNED_RETIRED_BLOCKS, MAX_PINNED_RETIRED_BYTES, MAX_TERMINAL_TRANSACTION_RECORDS, MAX_ACTIVE_COMPACTION_JOBS, MAX_ACTIVE_GARBAGE_COLLECTION_JOBS, MAX_TERMINAL_COMPACTION_JOB_RECORDS, MAX_COMPLETED_GARBAGE_COLLECTION_JOB_RECORDS, MAX_CATALOG_RECORDS, MAX_CATALOG_RETAINED_BYTES, MAX_SEGMENT_RECORDS, MAX_SEGMENT_RETAINED_BYTES, MAX_ACTIVE_FTS_BASE_BUILDS, MAX_ACTIVE_SECONDARY_INDEX_BUILDS, MAX_ACCELERATOR_BUILD_STAGED_BYTES_TOTAL, MAX_ACCELERATOR_BUILD_STAGED_ENTRIES_TOTAL, MAX_POSTING_BUILD_TTL_MS, MAX_SNAPSHOT_SESSION_TTL_MS, MAX_SNAPSHOT_FRAME_BATCH_BYTES, MAX_SNAPSHOT_FRAME_BATCH_ITEMS, MAX_SNAPSHOT_METADATA_BATCH_BYTES, MAX_SNAPSHOT_METADATA_FRAME_BYTES, SNAPSHOT_FRAME_KINDS, MAX_LEVEL_ZERO_SEGMENTS, MAX_AUTO_INCREMENT_EXCLUSIVE_END, MAX_ROW_ID, MAX_ROW_ID_EXCLUSIVE_END, StorageCorruptionError, StorageFormatVersionError, IndexedDbSchemaUpgradeBlockedError, StorageResourceLimitError, SnapshotManifestMissingError, SnapshotImportConflictError, PostingBuildConflictError, validateTableForeignKey, TableInUseError, TableRecordConflictError, TempOwnerConflictError, MAX_TEMP_OWNER_TTL_MS, MAX_ACTIVE_TEMP_OWNERS, MAX_TEMP_RUNS_PER_OWNER, MAX_TEMP_PAGES_PER_OWNER, MAX_TEMP_RUNS_TOTAL, MAX_TEMP_PAGES_TOTAL, MAX_TEMP_BYTES_PER_OWNER, MAX_TEMP_BYTES_TOTAL, TransactionRecordConflictError, UniqueKeyConflictError, UniqueKeyBuildConflictError, UniqueIndexCoverageError, SchemaConflictError, normalizeCompactionJobRecord, normalizeGarbageCollectionJobRecord, normalizeSegmentRecord, updateCompactionJobRecord as updateCompactionJobRecordUnchecked, updateGarbageCollectionPlanningRecord, updateTransactionRecord as updateTransactionRecordUnchecked, validateColumnDefault, validateCatalogName, validateCanonicalManifestChangedTableIds, validateEnumValues, validateFtsOrderedReadLimits, validateFtsPostingQueries, validateStorageId, validateStorageDatabaseName, validateTableColumns, validateSecondaryIndexes, validateTableRecordBounds, secondaryIndexColumnIds, secondaryIndexWriteContractChanged, secondaryUniqueKeyNamespace, transactionCommitDeltaRetainedBytes, uniqueKeyBuildChunkRetainedBytes, WriteConflictError, } from "./types.js";
2
2
  import { crc32, verifyStoredBlock } from "../block-format/index.js";
3
3
  import { dateIsoString } from "../date-value.js";
4
4
  import { decodeSnapshotMetadataItems, encodeSnapshotMetadataPage, extendSnapshotFrameStreamChecksum, prepareSnapshotFrameStreamHeader, snapshotFrameEnvelopeParts, snapshotFrameStreamHeaderIdentity, } from "./snapshot-stream.js";
@@ -9678,7 +9678,7 @@ function asTableRecord(value, location = "catalog/table") {
9678
9678
  }
9679
9679
  for (const [index, foreignKey] of rawForeignKeys.entries()) {
9680
9680
  if (isRecord(foreignKey)) {
9681
- assertKnownFields(foreignKey, ["name", "columns", "parentTable", "parentColumns", "onDelete"], `${location}/foreignKeys/${String(index)}`);
9681
+ assertKnownFields(foreignKey, ["name", "columns", "parentTable", "parentColumns", "onDelete", "enforced"], `${location}/foreignKeys/${String(index)}`);
9682
9682
  }
9683
9683
  if (!isRecord(foreignKey) ||
9684
9684
  !isCatalogName(foreignKey.name) ||
@@ -9690,6 +9690,7 @@ function asTableRecord(value, location = "catalog/table") {
9690
9690
  foreignKey.parentColumns.length === 0 ||
9691
9691
  foreignKey.columns.length !== foreignKey.parentColumns.length ||
9692
9692
  !["restrict", "cascade", "set null"].includes(String(foreignKey.onDelete)) ||
9693
+ (foreignKey.enforced !== undefined && typeof foreignKey.enforced !== "boolean") ||
9693
9694
  foreignKey.parentColumns.some((columnName) => !isCatalogName(columnName))) {
9694
9695
  throw corruption(`${location}/foreignKeys/${String(index)}`, "foreign-key metadata is invalid");
9695
9696
  }
@@ -11965,16 +11966,7 @@ async function assertTableForeignKeysInTransaction(catalog, record) {
11965
11966
  if (parent === undefined) {
11966
11967
  throw new TypeError(`FOREIGN KEY ${key.name} references a missing table: ${key.parentTable}`);
11967
11968
  }
11968
- const addressIds = parent.primaryKeyColumnIds?.length
11969
- ? parent.primaryKeyColumnIds
11970
- : parent.uniqueKeyColumnId === undefined
11971
- ? []
11972
- : [parent.uniqueKeyColumnId];
11973
- const addressNames = addressIds.map((id) => parent.columns.find((column) => column.id === id)?.name ?? "");
11974
- if (addressNames.length !== key.parentColumns.length ||
11975
- addressNames.some((name, index) => name !== key.parentColumns[index])) {
11976
- throw new TypeError(`FOREIGN KEY ${key.name} must reference the parent primary or unique key`);
11977
- }
11969
+ validateTableForeignKey(record, key, parent);
11978
11970
  }
11979
11971
  }
11980
11972
  async function updateRetiredHistoryLedger(store, deltaBytes) {
@@ -1,4 +1,4 @@
1
- import { CompactionBacklogError, CompactionJobConflictError, assertCompactionOutputProvenance, compactionOutputSegmentIds, createManifest, createGarbageCollectionJobRecord, advanceGarbageCollectionJobRecord, collectFtsCandidates, collectFtsPostingsBounded, activePostingStorageColumnIds, invalidateUncoveredFtsColumns, invalidateUncoveredSecondaryIndexes, GarbageCollectionJobConflictError, LeaseConflictError, LeaseExpiredError, LeaseOwnerConflictError, MAX_LEASE_TTL_MS, MAX_ACTIVE_LEASES, MAX_ACTIVE_TEMP_OWNERS, MAX_ACTIVE_COMPACTION_JOBS, MAX_ACTIVE_UNIQUE_KEY_BUILDS, MAX_ACTIVE_TRANSACTIONS, MAX_TERMINAL_TRANSACTION_RECORDS, MAX_TERMINAL_COMPACTION_JOB_RECORDS, MAX_COMPLETED_GARBAGE_COLLECTION_JOB_RECORDS, MAX_PINNED_MANIFEST_VERSION_LAG, MAX_PINNED_RETIRED_BLOCKS, MAX_PINNED_RETIRED_BYTES, MAX_RETIRED_HISTORY_BYTES, MAX_GLOBAL_STAGED_ARTIFACT_BYTES, MAX_GLOBAL_STAGED_BLOCKS, MAX_GLOBAL_STAGED_SEGMENTS, MAX_TEMP_OWNER_TTL_MS, MAX_ROW_ID, MAX_ROW_ID_EXCLUSIVE_END, MAX_AUTO_INCREMENT_EXCLUSIVE_END, MAX_LEVEL_ZERO_SEGMENTS, MAX_MANIFEST_BLOCK_PRESENCE_IDS, MAX_STORAGE_ID_CHARACTERS, MAX_CATALOG_RECORDS, MAX_CATALOG_RETAINED_BYTES, MAX_MANIFEST_RECORDS, MAX_MANIFEST_RETAINED_BYTES, MAX_SEGMENT_RECORDS, MAX_SEGMENT_RETAINED_BYTES, MAX_FTS_CANDIDATE_ROW_IDS, MAX_FTS_POSTINGS_PER_CHUNK, MAX_FTS_POSTING_ROW_IDS_PER_CHUNK, MAX_FTS_POSTING_TERM_CHARACTERS, MAX_FTS_TOKENS_PER_DOCUMENT, MAX_FTS_BASE_CHUNKS, MAX_FTS_DELTA_CHUNKS, SnapshotManifestMissingError, StorageResourceLimitError, TableInUseError, TableRecordConflictError, TempOwnerConflictError, TransactionRecordConflictError, UniqueKeyConflictError, UniqueKeyBuildConflictError, UniqueIndexCoverageError, assertTransactionArtifactBatchLimits, assertTransactionArtifactJournalLimits, assertTempRunPageBatchLimits, assertStorageBulkReadItems, boundedMaintenanceBatchItems, normalizeCompactionJobRecord, normalizeGarbageCollectionJobRecord, normalizeSegmentRecord, updateCompactionJobRecord, updateTransactionRecord, updateGarbageCollectionPlanningRecord, validateTableColumns, validateSecondaryIndexes, secondaryIndexColumnIds, secondaryIndexWriteContractChanged, secondaryUniqueKeyNamespace, transactionCommitDeltaRetainedBytes, uniqueKeyBuildChunkRetainedBytes, MAX_UNIQUE_KEY_BUILD_TTL_MS, MAX_UNIQUE_KEY_BUILD_STAGED_BYTES, MAX_UNIQUE_KEY_BUILD_STAGED_BYTES_TOTAL, MAX_UNIQUE_KEY_BUILD_TOKENS_PER_CHUNK, validateFtsPostingQueries, validateCanonicalManifestChangedTableIds, catalogRecordRetainedBytes, manifestRecordRetainedReservationBytes, segmentRecordRetainedBytes, snapshotAcceleratorItemRetainedUsage, assertSnapshotImportAcceleratorUsage, SchemaConflictError, WriteConflictError, } from "../types.js";
1
+ import { CompactionBacklogError, CompactionJobConflictError, assertCompactionOutputProvenance, compactionOutputSegmentIds, createManifest, createGarbageCollectionJobRecord, advanceGarbageCollectionJobRecord, collectFtsCandidates, collectFtsPostingsBounded, activePostingStorageColumnIds, invalidateUncoveredFtsColumns, invalidateUncoveredSecondaryIndexes, GarbageCollectionJobConflictError, LeaseConflictError, LeaseExpiredError, LeaseOwnerConflictError, MAX_LEASE_TTL_MS, MAX_ACTIVE_LEASES, MAX_ACTIVE_TEMP_OWNERS, MAX_ACTIVE_COMPACTION_JOBS, MAX_ACTIVE_UNIQUE_KEY_BUILDS, MAX_ACTIVE_TRANSACTIONS, MAX_TERMINAL_TRANSACTION_RECORDS, MAX_TERMINAL_COMPACTION_JOB_RECORDS, MAX_COMPLETED_GARBAGE_COLLECTION_JOB_RECORDS, MAX_PINNED_MANIFEST_VERSION_LAG, MAX_PINNED_RETIRED_BLOCKS, MAX_PINNED_RETIRED_BYTES, MAX_RETIRED_HISTORY_BYTES, MAX_GLOBAL_STAGED_ARTIFACT_BYTES, MAX_GLOBAL_STAGED_BLOCKS, MAX_GLOBAL_STAGED_SEGMENTS, MAX_TEMP_OWNER_TTL_MS, MAX_ROW_ID, MAX_ROW_ID_EXCLUSIVE_END, MAX_AUTO_INCREMENT_EXCLUSIVE_END, MAX_LEVEL_ZERO_SEGMENTS, MAX_MANIFEST_BLOCK_PRESENCE_IDS, MAX_STORAGE_ID_CHARACTERS, MAX_CATALOG_RECORDS, MAX_CATALOG_RETAINED_BYTES, MAX_MANIFEST_RECORDS, MAX_MANIFEST_RETAINED_BYTES, MAX_SEGMENT_RECORDS, MAX_SEGMENT_RETAINED_BYTES, MAX_FTS_CANDIDATE_ROW_IDS, MAX_FTS_POSTINGS_PER_CHUNK, MAX_FTS_POSTING_ROW_IDS_PER_CHUNK, MAX_FTS_POSTING_TERM_CHARACTERS, MAX_FTS_TOKENS_PER_DOCUMENT, MAX_FTS_BASE_CHUNKS, MAX_FTS_DELTA_CHUNKS, SnapshotManifestMissingError, StorageResourceLimitError, TableInUseError, TableRecordConflictError, TempOwnerConflictError, TransactionRecordConflictError, UniqueKeyConflictError, UniqueKeyBuildConflictError, UniqueIndexCoverageError, assertTransactionArtifactBatchLimits, assertTransactionArtifactJournalLimits, assertTempRunPageBatchLimits, assertStorageBulkReadItems, boundedMaintenanceBatchItems, normalizeCompactionJobRecord, normalizeGarbageCollectionJobRecord, normalizeSegmentRecord, updateCompactionJobRecord, updateTransactionRecord, updateGarbageCollectionPlanningRecord, validateTableColumns, validateTableForeignKey, validateSecondaryIndexes, secondaryIndexColumnIds, secondaryIndexWriteContractChanged, secondaryUniqueKeyNamespace, transactionCommitDeltaRetainedBytes, uniqueKeyBuildChunkRetainedBytes, MAX_UNIQUE_KEY_BUILD_TTL_MS, MAX_UNIQUE_KEY_BUILD_STAGED_BYTES, MAX_UNIQUE_KEY_BUILD_STAGED_BYTES_TOTAL, MAX_UNIQUE_KEY_BUILD_TOKENS_PER_CHUNK, validateFtsPostingQueries, validateCanonicalManifestChangedTableIds, catalogRecordRetainedBytes, manifestRecordRetainedReservationBytes, segmentRecordRetainedBytes, snapshotAcceleratorItemRetainedUsage, assertSnapshotImportAcceleratorUsage, SchemaConflictError, WriteConflictError, } from "../types.js";
2
2
  import { dateIsoString } from "../../date-value.js";
3
3
  import { assertWellFormedString, crc32, MAX_STORED_BLOCK_BYTE_LENGTH, } from "../../block-format/index.js";
4
4
  const postingTextEncoder = new TextEncoder();
@@ -1300,16 +1300,7 @@ export class RecordCore {
1300
1300
  if (parent === undefined) {
1301
1301
  throw new TypeError(`FOREIGN KEY ${key.name} references a missing table: ${key.parentTable}`);
1302
1302
  }
1303
- const addressIds = parent.primaryKeyColumnIds?.length
1304
- ? parent.primaryKeyColumnIds
1305
- : parent.uniqueKeyColumnId === undefined
1306
- ? []
1307
- : [parent.uniqueKeyColumnId];
1308
- const addressNames = addressIds.map((id) => parent.columns.find((column) => column.id === id)?.name ?? "");
1309
- if (addressNames.length !== key.parentColumns.length ||
1310
- addressNames.some((name, index) => name !== key.parentColumns[index])) {
1311
- throw new TypeError(`FOREIGN KEY ${key.name} must reference the parent primary or unique key`);
1312
- }
1303
+ validateTableForeignKey(record, key, parent);
1313
1304
  }
1314
1305
  }
1315
1306
  getTable(id) {
@@ -1342,7 +1333,7 @@ export class RecordCore {
1342
1333
  }
1343
1334
  if (update.columns !== undefined)
1344
1335
  validateTableColumns(update.columns);
1345
- const { ftsColumns: previousFts, secondaryIndexes: previousSecondary, triggers: previousTriggers, view: previousView, ...base } = record;
1336
+ const { ftsColumns: previousFts, secondaryIndexes: previousSecondary, triggers: previousTriggers, view: previousView, foreignKeys: previousForeignKeys, ...base } = record;
1346
1337
  let nextFts = update.ftsColumns === undefined ? previousFts : update.ftsColumns;
1347
1338
  let nextSecondary = update.secondaryIndexes === undefined ? previousSecondary : update.secondaryIndexes;
1348
1339
  const retainedColumnIds = update.columns === undefined
@@ -1373,6 +1364,7 @@ export class RecordCore {
1373
1364
  }
1374
1365
  const nextTriggers = update.triggers === undefined ? previousTriggers : update.triggers;
1375
1366
  const nextView = update.view === undefined ? previousView : update.view;
1367
+ const nextForeignKeys = update.foreignKeys ?? previousForeignKeys;
1376
1368
  validateTableView(nextView ?? undefined);
1377
1369
  const updated = {
1378
1370
  ...base,
@@ -1386,10 +1378,12 @@ export class RecordCore {
1386
1378
  ...(nextTriggers === null || nextTriggers === undefined
1387
1379
  ? {}
1388
1380
  : { triggers: structuredClone(nextTriggers) }),
1381
+ ...(nextForeignKeys === undefined ? {} : { foreignKeys: structuredClone(nextForeignKeys) }),
1389
1382
  ...(nextView === null || nextView === undefined ? {} : { view: structuredClone(nextView) }),
1390
1383
  revision: safeWholeIncrement(expectedRevision, "Table revision"),
1391
1384
  };
1392
1385
  validateSecondaryIndexes(updated);
1386
+ this.#assertTableForeignKeys(updated);
1393
1387
  this.#assertTableTriggerOwnership(updated);
1394
1388
  let autoIncrementCounter;
1395
1389
  if (update.autoIncrementSeed !== undefined) {
@@ -5552,16 +5546,7 @@ function validateRecordCoreState(state, physical) {
5552
5546
  if (parent === undefined) {
5553
5547
  throw new TypeError(`FOREIGN KEY ${key.name} references a missing table: ${key.parentTable}`);
5554
5548
  }
5555
- const addressIds = parent.primaryKeyColumnIds?.length
5556
- ? parent.primaryKeyColumnIds
5557
- : parent.uniqueKeyColumnId === undefined
5558
- ? []
5559
- : [parent.uniqueKeyColumnId];
5560
- const addressNames = addressIds.map((id) => parent.columns.find((column) => column.id === id)?.name ?? "");
5561
- if (addressNames.length !== key.parentColumns.length ||
5562
- addressNames.some((name, index) => name !== key.parentColumns[index])) {
5563
- throw new TypeError(`FOREIGN KEY ${key.name} must reference the parent primary or unique key`);
5564
- }
5549
+ validateTableForeignKey(table, key, parent);
5565
5550
  }
5566
5551
  }
5567
5552
  const terminalTransactionCount = state.transactions.filter((transaction) => transaction.status !== "active").length;