@orkestrel/database 0.0.6 → 0.0.8

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.
@@ -3,11 +3,11 @@ let _orkestrel_contract = require("@orkestrel/contract");
3
3
  let _orkestrel_emitter = require("@orkestrel/emitter");
4
4
  //#region src/core/constants.ts
5
5
  /**
6
- * The primary-key column assumed when {@link TableKeys} does not name one.
6
+ * The primary-key column assumed when {@link PrimaryMap} does not name one.
7
7
  *
8
8
  * @remarks
9
9
  * `id` is the convention IndexedDB (`keyPath: 'id'`) and SQL (`id` / rowid) both
10
- * lean on, so a table that omits `key` keys its rows by `id`.
10
+ * lean on, so a table without a `primary` override keys its rows by `id`.
11
11
  */
12
12
  var DEFAULT_PRIMARY = "id";
13
13
  /**
@@ -15,7 +15,7 @@ var DEFAULT_PRIMARY = "id";
15
15
  *
16
16
  * @remarks
17
17
  * A ReDoS bound (AGENTS §6.5): the SA1–SA4 migration lets a model supply `list`
18
- * criteria over the wire, so `likeMatch` / `globMatch` run attacker-controlled
18
+ * input over the wire, so `matchesLikePattern` / `matchesGlobPattern` run attacker-controlled
19
19
  * patterns. The matcher is the LINEAR greedy two-pointer wildcard match — never a
20
20
  * backtracking regex (`.*`-segments-separated-by-literals against a long input is the
21
21
  * catastrophic shape JS cannot bound without atomic groups), so it is O(value ×
@@ -24,10 +24,6 @@ var DEFAULT_PRIMARY = "id";
24
24
  * `VALIDATION` {@link DatabaseError}; the cap is generous for any legitimate search.
25
25
  */
26
26
  var MAX_PATTERN_LENGTH = 1024;
27
- /** The number of bytes encoded by an RFC 4122 UUID. */
28
- var UUID_BYTE_COUNT = 16;
29
- /** The number of distinct values one UUID byte may hold. */
30
- var UUID_BYTE_RANGE = 256;
31
27
  //#endregion
32
28
  //#region src/core/errors.ts
33
29
  /**
@@ -37,8 +33,8 @@ var UUID_BYTE_RANGE = 256;
37
33
  * Carries a {@link DatabaseErrorCode} and an optional `context` bag naming the
38
34
  * offending table / key. Thrown for: operating on a closed database (`CLOSED`), a
39
35
  * `resolve` miss (`NOT_FOUND`), an `add` onto an existing key (`CONFLICT`), a
40
- * row that fails its table's contract (`VALIDATION`), a cancelled operation whose
41
- * {@link ReadOptions.signal} aborted (`ABORTED`, carrying `signal.reason` in
36
+ * row that fails its table's contract (`VALIDATION`), an aborted operation whose
37
+ * {@link OperationOptions.signal} aborted (`ABORTED`, carrying `signal.reason` in
42
38
  * `context`), an inapplicable {@link Migration} plan (`MIGRATION`), a
43
39
  * driver that violates a {@link DriverInterface} invariant, thrown by the
44
40
  * `conformDriver` helper (`CONFORMANCE`), and an unexpected infrastructure
@@ -75,6 +71,219 @@ function isDatabaseError(value) {
75
71
  return value instanceof DatabaseError;
76
72
  }
77
73
  //#endregion
74
+ //#region src/core/validators.ts
75
+ /**
76
+ * Validate the paging fields of a portable query.
77
+ *
78
+ * @remarks
79
+ * A present `limit` or `offset` must be a finite nonnegative integer; zero is
80
+ * valid. Validation is deterministic (`limit` before `offset`). Non-finite
81
+ * values are rendered as strings in error context so JSON serialization cannot
82
+ * collapse `NaN` or infinity to `null`.
83
+ *
84
+ * @param input - The portable query whose paging fields to validate
85
+ * @throws {@link DatabaseError} `VALIDATION` when a paging field is invalid
86
+ */
87
+ function validatePage(input) {
88
+ const limit = input?.limit;
89
+ if (limit !== void 0 && (!Number.isInteger(limit) || limit < 0)) throw new DatabaseError("VALIDATION", "Query limit must be a nonnegative integer", {
90
+ field: "limit",
91
+ value: Number.isFinite(limit) ? limit : String(limit)
92
+ });
93
+ const offset = input?.offset;
94
+ if (offset !== void 0 && (!Number.isInteger(offset) || offset < 0)) throw new DatabaseError("VALIDATION", "Query offset must be a nonnegative integer", {
95
+ field: "offset",
96
+ value: Number.isFinite(offset) ? offset : String(offset)
97
+ });
98
+ }
99
+ /**
100
+ * Test whether a value is a usable database key.
101
+ *
102
+ * @param value - The value to test
103
+ * @returns Whether `value` is a string or finite number
104
+ */
105
+ function isKey(value) {
106
+ return typeof value === "string" || typeof value === "number" && Number.isFinite(value);
107
+ }
108
+ /**
109
+ * Test whether a value is a portable column schema.
110
+ *
111
+ * @param value - The value to test
112
+ * @returns Whether `value` is a complete {@link ColumnSchema}
113
+ */
114
+ function isColumnSchema(value) {
115
+ try {
116
+ const column = (0, _orkestrel_contract.cloneJSONRecord)(value);
117
+ const keys = Object.keys(column);
118
+ return keys.length === 4 && keys.includes("name") && keys.includes("storage") && keys.includes("optional") && keys.includes("nullable") && typeof column.name === "string" && column.name.length > 0 && (column.storage === "text" || column.storage === "integer" || column.storage === "real" || column.storage === "boolean" || column.storage === "json" || column.storage === "blob") && typeof column.optional === "boolean" && typeof column.nullable === "boolean";
119
+ } catch {
120
+ return false;
121
+ }
122
+ }
123
+ /**
124
+ * Test whether a value is a portable table schema.
125
+ *
126
+ * @param value - The value to test
127
+ * @returns Whether `value` is a complete {@link TableSchema}
128
+ */
129
+ function isTableSchema(value) {
130
+ try {
131
+ const table = (0, _orkestrel_contract.cloneJSONRecord)(value);
132
+ const keys = Object.keys(table);
133
+ if (keys.length !== 4 || !keys.includes("name") || !keys.includes("primary") || !keys.includes("columns") || !keys.includes("indexes") || typeof table.name !== "string" || table.name.length === 0 || typeof table.primary !== "string" || table.primary.length === 0 || !Array.isArray(table.columns) || !Array.isArray(table.indexes) || !table.columns.every(isColumnSchema)) return false;
134
+ const names = table.columns.map((column) => column.name);
135
+ if (new Set(names).size !== names.length || !names.includes(table.primary) || !table.indexes.every((index) => Array.isArray(index) && index.length > 0 && index.every((column) => typeof column === "string" && names.includes(column)))) return false;
136
+ const indexes = table.indexes.map((index) => JSON.stringify(index));
137
+ return new Set(indexes).size === indexes.length;
138
+ } catch {
139
+ return false;
140
+ }
141
+ }
142
+ /**
143
+ * Test whether a value is a complete portable driver schema.
144
+ *
145
+ * @param value - The value to test
146
+ * @returns Whether `value` is a table-schema collection with unique table names
147
+ */
148
+ function isDriverSchema(value) {
149
+ try {
150
+ const schema = (0, _orkestrel_contract.cloneJSONValue)(value);
151
+ if (!Array.isArray(schema) || !schema.every(isTableSchema)) return false;
152
+ const names = schema.map((table) => table.name);
153
+ return new Set(names).size === names.length;
154
+ } catch {
155
+ return false;
156
+ }
157
+ }
158
+ /**
159
+ * Test whether a value is one ordered migration step.
160
+ *
161
+ * @param value - The value to test
162
+ * @returns Whether `value` is a complete {@link MigrationStep}
163
+ */
164
+ function isMigrationStep(value) {
165
+ try {
166
+ const step = (0, _orkestrel_contract.cloneJSONRecord)(value);
167
+ if (typeof step.operation !== "string") return false;
168
+ const keys = Object.keys(step);
169
+ switch (step.operation) {
170
+ case "table.add": return keys.length === 2 && keys.includes("operation") && keys.includes("table") && isTableSchema(step.table);
171
+ case "table.remove": return keys.length === 2 && keys.includes("operation") && keys.includes("table") && typeof step.table === "string" && step.table.length > 0;
172
+ case "column.add": return keys.length === 3 && keys.includes("operation") && keys.includes("table") && keys.includes("column") && typeof step.table === "string" && step.table.length > 0 && isColumnSchema(step.column);
173
+ case "column.remove": return keys.length === 3 && keys.includes("operation") && keys.includes("table") && keys.includes("column") && typeof step.table === "string" && step.table.length > 0 && typeof step.column === "string" && step.column.length > 0;
174
+ case "index.add":
175
+ case "index.remove": return keys.length === 3 && keys.includes("operation") && keys.includes("table") && keys.includes("index") && typeof step.table === "string" && step.table.length > 0 && Array.isArray(step.index) && step.index.length > 0 && step.index.every((column) => typeof column === "string" && column.length > 0);
176
+ default: return false;
177
+ }
178
+ } catch {
179
+ return false;
180
+ }
181
+ }
182
+ /**
183
+ * Test whether a value is an ordered migration plan.
184
+ *
185
+ * @param value - The value to test
186
+ * @returns Whether `value` is a complete {@link Migration}
187
+ */
188
+ function isMigration(value) {
189
+ try {
190
+ const migration = (0, _orkestrel_contract.cloneJSONRecord)(value);
191
+ const keys = Object.keys(migration);
192
+ return keys.length === 3 && keys.includes("from") && keys.includes("to") && keys.includes("steps") && typeof migration.from === "number" && Number.isFinite(migration.from) && typeof migration.to === "number" && Number.isFinite(migration.to) && Array.isArray(migration.steps) && migration.steps.every(isMigrationStep);
193
+ } catch {
194
+ return false;
195
+ }
196
+ }
197
+ /**
198
+ * Test whether a value is persisted driver metadata.
199
+ *
200
+ * @param value - The value to test
201
+ * @returns Whether `value` is complete {@link DriverMetadata}
202
+ */
203
+ function isDriverMetadata(value) {
204
+ try {
205
+ const metadata = (0, _orkestrel_contract.cloneJSONRecord)(value);
206
+ const keys = Object.keys(metadata);
207
+ return keys.length === 2 && keys.includes("version") && keys.includes("schema") && typeof metadata.version === "number" && Number.isFinite(metadata.version) && isDriverSchema(metadata.schema);
208
+ } catch {
209
+ return false;
210
+ }
211
+ }
212
+ /**
213
+ * Test whether a value is one atomic migration request.
214
+ *
215
+ * @param value - The value to test
216
+ * @returns Whether `value` is a complete {@link MigrationInput}
217
+ */
218
+ function isMigrationInput(value) {
219
+ try {
220
+ const input = (0, _orkestrel_contract.cloneJSONRecord)(value);
221
+ const keys = Object.keys(input);
222
+ return (keys.length === 1 || keys.length === 2) && keys.includes("plan") && (keys.length === 1 || keys.includes("metadata")) && isMigration(input.plan) && (input.metadata === void 0 || isDriverMetadata(input.metadata));
223
+ } catch {
224
+ return false;
225
+ }
226
+ }
227
+ //#endregion
228
+ //#region src/core/cloners.ts
229
+ /**
230
+ * Clone unknown driver metadata into a distinct deeply frozen snapshot.
231
+ *
232
+ * @param value - Unknown metadata
233
+ * @returns Owned driver metadata
234
+ */
235
+ function cloneDriverMetadata(value) {
236
+ try {
237
+ const metadata = (0, _orkestrel_contract.cloneJSONRecord)(value);
238
+ if (isDriverMetadata(metadata)) return metadata;
239
+ throw new DatabaseError("VALIDATION", "Driver metadata is invalid", { path: "metadata" });
240
+ } catch (error) {
241
+ if (error instanceof DatabaseError) throw error;
242
+ throw new DatabaseError("VALIDATION", "Driver metadata is invalid", {
243
+ path: "metadata",
244
+ cause: error
245
+ });
246
+ }
247
+ }
248
+ /**
249
+ * Clone unknown driver schema into a distinct deeply frozen snapshot.
250
+ *
251
+ * @param value - Unknown table schema collection
252
+ * @returns Owned driver schema
253
+ */
254
+ function cloneDriverSchema(value) {
255
+ try {
256
+ const schema = (0, _orkestrel_contract.cloneJSONValue)(value);
257
+ if (isDriverSchema(schema)) return schema;
258
+ throw new DatabaseError("VALIDATION", "Driver schema is invalid", { path: "schema" });
259
+ } catch (error) {
260
+ if (error instanceof DatabaseError) throw error;
261
+ throw new DatabaseError("VALIDATION", "Driver schema is invalid", {
262
+ path: "schema",
263
+ cause: error
264
+ });
265
+ }
266
+ }
267
+ /**
268
+ * Clone unknown migration input into a distinct deeply frozen snapshot.
269
+ *
270
+ * @param value - Unknown migration input
271
+ * @returns Owned migration input
272
+ */
273
+ function cloneMigrationInput(value) {
274
+ try {
275
+ const input = (0, _orkestrel_contract.cloneJSONRecord)(value);
276
+ if (isMigrationInput(input)) return input;
277
+ throw new DatabaseError("VALIDATION", "Migration input is invalid", { path: "migration" });
278
+ } catch (error) {
279
+ if (error instanceof DatabaseError) throw error;
280
+ throw new DatabaseError("VALIDATION", "Migration input is invalid", {
281
+ path: "migration",
282
+ cause: error
283
+ });
284
+ }
285
+ }
286
+ //#endregion
78
287
  //#region src/core/helpers.ts
79
288
  /**
80
289
  * A total ordering over arbitrary values — the comparator behind sorting and the
@@ -107,14 +316,15 @@ function compareValues(left, right) {
107
316
  *
108
317
  * @remarks
109
318
  * Primitives compare by SameValueZero (`NaN` equals itself; `+0` equals `-0`).
110
- * Arrays compare by index (same length, every element `deepEqual`). Plain
319
+ * Arrays compare by index (same length, every element `equalsValue`). Plain
111
320
  * records (via `isRecord`) compare by their OWN enumerable keys: same key
112
321
  * COUNT and, for every key in `left`, `right` has that key (`Object.hasOwn`)
113
- * with a `deepEqual` value — so a key present with value `undefined` is NOT
322
+ * with a `equalsValue` value — so a key present with value `undefined` is NOT
114
323
  * equal to that key being absent (both differ in `Object.keys` membership).
115
324
  * Anything else (functions, class instances, mismatched shapes) falls through
116
- * to `false`. There is no cycle detection a cyclic input recurses forever;
117
- * callers pass acyclic data (rows, plans, config).
325
+ * to `false`. Container pairs are tracked iteratively, so self-referential and
326
+ * mutually cyclic arrays/records terminate without consuming the call stack.
327
+ * Hostile proxy traps and accessors are contained as a non-match.
118
328
  *
119
329
  * @param left - The left value
120
330
  * @param right - The right value
@@ -122,32 +332,67 @@ function compareValues(left, right) {
122
332
  *
123
333
  * @example
124
334
  * ```ts
125
- * deepEqual(Number.NaN, Number.NaN) // true
126
- * deepEqual({ a: [1, { b: 2 }] }, { a: [1, { b: 2 }] }) // true
127
- * deepEqual({ a: undefined }, {}) // false — present-undefined ≠ absent
335
+ * equalsValue(Number.NaN, Number.NaN) // true
336
+ * equalsValue({ a: [1, { b: 2 }] }, { a: [1, { b: 2 }] }) // true
337
+ * equalsValue({ a: undefined }, {}) // false — present-undefined ≠ absent
128
338
  * ```
129
339
  */
130
- function deepEqual(left, right) {
131
- if (typeof left === "number" && typeof right === "number") return Number.isNaN(left) && Number.isNaN(right) || left === right;
132
- if (left === right) return true;
133
- if (Array.isArray(left) && Array.isArray(right)) return left.length === right.length && left.every((item, index) => deepEqual(item, right[index]));
134
- if ((0, _orkestrel_contract.isRecord)(left) && (0, _orkestrel_contract.isRecord)(right)) {
135
- const leftKeys = Object.keys(left);
136
- const rightKeys = Object.keys(right);
137
- if (leftKeys.length !== rightKeys.length) return false;
138
- return leftKeys.every((key) => Object.hasOwn(right, key) && deepEqual(left[key], right[key]));
139
- }
140
- return false;
340
+ function equalsValue(left, right) {
341
+ const pending = [[left, right]];
342
+ const compared = /* @__PURE__ */ new WeakMap();
343
+ try {
344
+ while (pending.length > 0) {
345
+ const pair = pending.pop();
346
+ if (pair === void 0) continue;
347
+ const [currentLeft, currentRight] = pair;
348
+ if (typeof currentLeft === "number" && typeof currentRight === "number") {
349
+ if (Number.isNaN(currentLeft) && Number.isNaN(currentRight) || currentLeft === currentRight) continue;
350
+ return false;
351
+ }
352
+ if (currentLeft === currentRight) continue;
353
+ const leftArray = Array.isArray(currentLeft);
354
+ const rightArray = Array.isArray(currentRight);
355
+ const leftRecord = (0, _orkestrel_contract.isRecord)(currentLeft);
356
+ const rightRecord = (0, _orkestrel_contract.isRecord)(currentRight);
357
+ if (leftArray !== rightArray || leftRecord !== rightRecord) return false;
358
+ if (!leftArray && !leftRecord || !rightArray && !rightRecord) return false;
359
+ const prior = compared.get(currentLeft);
360
+ if (prior?.has(currentRight)) continue;
361
+ if (prior === void 0) compared.set(currentLeft, new WeakSet([currentRight]));
362
+ else prior.add(currentRight);
363
+ if (leftArray && rightArray) {
364
+ if (currentLeft.length !== currentRight.length) return false;
365
+ for (let index = 0; index < currentLeft.length; index += 1) {
366
+ const leftOwn = Object.hasOwn(currentLeft, index);
367
+ if (leftOwn !== Object.hasOwn(currentRight, index)) return false;
368
+ if (leftOwn) pending.push([currentLeft[index], currentRight[index]]);
369
+ }
370
+ continue;
371
+ }
372
+ if (leftRecord && rightRecord) {
373
+ const leftKeys = Object.keys(currentLeft);
374
+ const rightKeys = Object.keys(currentRight);
375
+ if (leftKeys.length !== rightKeys.length) return false;
376
+ for (const key of leftKeys) {
377
+ if (!Object.hasOwn(currentRight, key)) return false;
378
+ pending.push([currentLeft[key], currentRight[key]]);
379
+ }
380
+ }
381
+ }
382
+ return true;
383
+ } catch {
384
+ return false;
385
+ }
141
386
  }
142
387
  /**
143
388
  * Match a value against a wildcard pattern in LINEAR time — the shared, ReDoS-SAFE
144
- * engine behind {@link likeMatch} and {@link globMatch}.
389
+ * engine behind {@link matchesLikePattern} and {@link matchesGlobPattern}.
145
390
  *
146
391
  * @remarks
147
392
  * A backtracking RegExp (`a%b%c` → `^a.*b.*c$`) is CATASTROPHIC on a hostile pattern:
148
393
  * `.*` segments separated by literals, matched against a long non-matching input, blow
149
394
  * up super-linearly — and JS has no atomic groups / possessive quantifiers to bound it
150
- * (AGENTS §6.5, now that the authed server runs model-supplied `list` criteria over the
395
+ * (AGENTS §6.5, now that the authed server runs model-supplied `list` input over the
151
396
  * wire). So this builds NO regex. It runs the classic GREEDY TWO-POINTER wildcard match:
152
397
  * the `any` wildcard records its position and, on a later mismatch, backtracks ONLY to
153
398
  * that last `any` (letting it absorb one more char) — so the work is O(value × pattern),
@@ -170,7 +415,7 @@ function deepEqual(left, right) {
170
415
  * @returns Whether `value` matches `pattern`
171
416
  * @throws A `VALIDATION` {@link DatabaseError} when `pattern` exceeds {@link MAX_PATTERN_LENGTH}
172
417
  */
173
- function wildcardMatch(value, pattern, any, single, fold) {
418
+ function matchesWildcardPattern(value, pattern, any, single, fold) {
174
419
  if (pattern.length > 1024) throw new DatabaseError("VALIDATION", `Pattern exceeds the maximum length of ${MAX_PATTERN_LENGTH}`, {
175
420
  length: pattern.length,
176
421
  limit: MAX_PATTERN_LENGTH
@@ -199,11 +444,11 @@ function wildcardMatch(value, pattern, any, single, fold) {
199
444
  while (pi < needle.length && needle[pi] === any) pi += 1;
200
445
  return pi === needle.length;
201
446
  }
202
- function likeMatch(value, pattern) {
203
- return wildcardMatch(value, pattern, "%", "_", true);
447
+ function matchesLikePattern(value, pattern) {
448
+ return matchesWildcardPattern(value, pattern, "%", "_", true);
204
449
  }
205
- function globMatch(value, pattern) {
206
- return wildcardMatch(value, pattern, "*", "?", false);
450
+ function matchesGlobPattern(value, pattern) {
451
+ return matchesWildcardPattern(value, pattern, "*", "?", false);
207
452
  }
208
453
  /**
209
454
  * Evaluate one {@link Condition} against a row — the per-operator predicate.
@@ -213,10 +458,10 @@ function globMatch(value, pattern) {
213
458
  * string is one column; an array descends a nested value) — and applies the
214
459
  * operator. Range operators (`above` / `below` / `from` / `to` / `between`) use
215
460
  * {@link compareValues}, the total order; the equality family (`equals` / `not`
216
- * / `any` / `none`) uses {@link deepEqual} — STRUCTURAL equality, not the total
461
+ * / `any` / `none`) uses {@link equalsValue} — STRUCTURAL equality, not the total
217
462
  * order's rank-5-collapses-all-objects behavior, so `equals` on an object/array
218
463
  * operand only matches a structurally-equal value, never every row holding any
219
- * object. This is a semantics change from ranking: `deepEqual` is SameValueZero
464
+ * object. This is a semantics change from ranking: `equalsValue` is SameValueZero
220
465
  * on leaves, so `NaN` now equals `NaN` under `equals` / `any` (it never matched
221
466
  * anything under the old rank-based comparison). `like` / `glob` / `starts` /
222
467
  * `ends` match only strings; `absent` / `present` test nullishness. Total — a
@@ -231,19 +476,19 @@ function matchesCondition(row, condition) {
231
476
  const first = condition.values[0];
232
477
  const second = condition.values[1];
233
478
  switch (condition.operator) {
234
- case "equals": return deepEqual(value, first);
235
- case "not": return !deepEqual(value, first);
479
+ case "equals": return equalsValue(value, first);
480
+ case "not": return !equalsValue(value, first);
236
481
  case "above": return compareValues(value, first) > 0;
237
482
  case "below": return compareValues(value, first) < 0;
238
483
  case "from": return compareValues(value, first) >= 0;
239
484
  case "to": return compareValues(value, first) <= 0;
240
485
  case "between": return compareValues(value, first) >= 0 && compareValues(value, second) <= 0;
241
- case "like": return (0, _orkestrel_contract.isString)(value) && (0, _orkestrel_contract.isString)(first) && likeMatch(value, first);
242
- case "glob": return (0, _orkestrel_contract.isString)(value) && (0, _orkestrel_contract.isString)(first) && globMatch(value, first);
486
+ case "like": return (0, _orkestrel_contract.isString)(value) && (0, _orkestrel_contract.isString)(first) && matchesLikePattern(value, first);
487
+ case "glob": return (0, _orkestrel_contract.isString)(value) && (0, _orkestrel_contract.isString)(first) && matchesGlobPattern(value, first);
243
488
  case "starts": return (0, _orkestrel_contract.isString)(value) && (0, _orkestrel_contract.isString)(first) && value.startsWith(first);
244
489
  case "ends": return (0, _orkestrel_contract.isString)(value) && (0, _orkestrel_contract.isString)(first) && value.endsWith(first);
245
- case "any": return condition.values.some((candidate) => deepEqual(value, candidate));
246
- case "none": return !condition.values.some((candidate) => deepEqual(value, candidate));
490
+ case "any": return condition.values.some((candidate) => equalsValue(value, candidate));
491
+ case "none": return !condition.values.some((candidate) => equalsValue(value, candidate));
247
492
  case "absent": return value === void 0 || value === null;
248
493
  case "present": return value !== void 0 && value !== null;
249
494
  }
@@ -261,7 +506,7 @@ function matchesCondition(row, condition) {
261
506
  * @param conditions - The conditions to fold
262
507
  * @returns Whether the row satisfies the combined conditions
263
508
  */
264
- function matchesCriteria(row, conditions) {
509
+ function matchesQuery(row, conditions) {
265
510
  let result = true;
266
511
  let seeded = false;
267
512
  for (const condition of conditions) {
@@ -275,11 +520,11 @@ function matchesCriteria(row, conditions) {
275
520
  }
276
521
  /**
277
522
  * Filter rows by a list of conditions — the shared basis for a table's count
278
- * and aggregate paths (no sort/page, unlike {@link applyCriteria}).
523
+ * and aggregate paths (no sort/page, unlike {@link applyQuery}).
279
524
  *
280
525
  * @remarks
281
526
  * An empty condition list matches every row (returned as-is, no copy). Folds
282
- * each row through {@link matchesCriteria}.
527
+ * each row through {@link matchesQuery}.
283
528
  *
284
529
  * @param rows - The rows to filter
285
530
  * @param conditions - The conditions to apply (empty matches everything)
@@ -295,7 +540,7 @@ function matchesCriteria(row, conditions) {
295
540
  */
296
541
  function filterRows(rows, conditions) {
297
542
  if (conditions.length === 0) return rows;
298
- return rows.filter((row) => matchesCriteria(row, conditions));
543
+ return rows.filter((row) => matchesQuery(row, conditions));
299
544
  }
300
545
  /**
301
546
  * Sort rows by an ordering specification, leaving the input untouched.
@@ -320,26 +565,27 @@ function sortRows(rows, order) {
320
565
  return sorted;
321
566
  }
322
567
  /**
323
- * Apply a {@link Criteria} to rows — filter, then sort, then page.
568
+ * Apply a {@link QueryInput} to rows — filter, then sort, then page.
324
569
  *
325
570
  * @remarks
326
571
  * The whole portable read pipeline in one place: conditions filter, `order`
327
572
  * sorts, and `offset` / `limit` window the result. Each step is skipped when its
328
- * part of the criteria is absent. The reference {@link DriverInterface} backends
573
+ * part of the input is absent. The reference {@link DriverInterface} backends
329
574
  * lean on this rather than each re-deriving it.
330
575
  *
331
576
  * @param rows - The rows to process (typically a table's full `scan`)
332
- * @param criteria - The read specification, or `undefined` for all rows as-is
577
+ * @param input - The read specification, or `undefined` for all rows as-is
333
578
  * @returns The filtered, sorted, paged rows
334
579
  */
335
- function applyCriteria(rows, criteria) {
580
+ function applyQuery(rows, input) {
581
+ validatePage(input);
336
582
  let result = rows;
337
- const conditions = criteria?.conditions;
338
- if (conditions !== void 0 && conditions.length > 0) result = result.filter((row) => matchesCriteria(row, conditions));
339
- const order = criteria?.order;
583
+ const conditions = input?.conditions;
584
+ if (conditions !== void 0 && conditions.length > 0) result = result.filter((row) => matchesQuery(row, conditions));
585
+ const order = input?.order;
340
586
  if (order !== void 0 && order.length > 0) result = sortRows(result, order);
341
- const offset = criteria?.offset ?? 0;
342
- const limit = criteria?.limit;
587
+ const offset = input?.offset ?? 0;
588
+ const limit = input?.limit;
343
589
  if (offset > 0 || limit !== void 0) result = result.slice(offset, limit !== void 0 ? offset + limit : void 0);
344
590
  return result;
345
591
  }
@@ -381,11 +627,24 @@ function computeAggregate(rows, operation, column) {
381
627
  */
382
628
  function extractKey(row, column) {
383
629
  const value = row[column];
384
- if ((0, _orkestrel_contract.isString)(value)) return value;
385
- if ((0, _orkestrel_contract.isFiniteNumber)(value)) return value;
630
+ return isKey(value) ? value : void 0;
631
+ }
632
+ /**
633
+ * Return a fresh row whose primary column is authoritatively bound to its storage key.
634
+ *
635
+ * @param row - The caller row
636
+ * @param primary - The primary column
637
+ * @param key - The authoritative storage key
638
+ * @returns A fresh row with the bound primary
639
+ */
640
+ function bindRowKey(row, primary, key) {
641
+ return {
642
+ ...row,
643
+ [primary]: key
644
+ };
386
645
  }
387
646
  /**
388
- * Map a column's {@link ContractShape} to its portable {@link ColumnType} — the
647
+ * Map a column's {@link ContractShape} to its portable {@link ColumnStorage} — the
389
648
  * value a `TableSchema` carries so a native backend can declare a real column.
390
649
  *
391
650
  * @remarks
@@ -402,13 +661,13 @@ function extractKey(row, column) {
402
661
  *
403
662
  * @example
404
663
  * ```ts
405
- * shapeToColumnType(stringShape()) // 'text'
406
- * shapeToColumnType(integerShape()) // 'integer'
407
- * shapeToColumnType(optionalShape(integerShape())) // 'integer'
408
- * shapeToColumnType(objectShape({ a: stringShape() })) // 'json'
664
+ * shapeToColumnStorage(stringShape()) // 'text'
665
+ * shapeToColumnStorage(integerShape()) // 'integer'
666
+ * shapeToColumnStorage(optionalShape(integerShape())) // 'integer'
667
+ * shapeToColumnStorage(objectShape({ a: stringShape() })) // 'json'
409
668
  * ```
410
669
  */
411
- function shapeToColumnType(shape) {
670
+ function shapeToColumnStorage(shape) {
412
671
  switch (shape.type) {
413
672
  case "string": return "text";
414
673
  case "number": return shape.integer === true ? "integer" : "real";
@@ -418,7 +677,7 @@ function shapeToColumnType(shape) {
418
677
  if (shape.values.every((value) => typeof value === "number")) return shape.values.every((value) => Number.isInteger(value)) ? "integer" : "real";
419
678
  return "text";
420
679
  case "optional":
421
- case "nullable": return shapeToColumnType(shape.inner);
680
+ case "nullable": return shapeToColumnStorage(shape.inner);
422
681
  case "null":
423
682
  case "object":
424
683
  case "array":
@@ -428,48 +687,31 @@ function shapeToColumnType(shape) {
428
687
  }
429
688
  }
430
689
  /**
431
- * Whether a value is a well-formed {@link DriverMeta} — the boundary guard a
432
- * versioning driver's `meta()` narrows a stored (structured-clone or
433
- * `JSON.parse`d) value through before trusting it, replacing the per-driver
434
- * duplicated narrowing every backend used to hand-roll (AGENTS §14: never `as`).
435
- *
436
- * @remarks
437
- * Total and total-recursive over the whole shape: a finite `version`, and a
438
- * `schema` array of well-formed {@link TableSchema} entries — each a `name` /
439
- * `primary` string pair, a `columns` array of well-formed {@link ColumnSchema}
440
- * entries (a `name` string, a {@link ColumnType} literal, a `nullable`
441
- * boolean), and an `indexes` array of string arrays. Anything off-shape
442
- * (including a non-record) returns `false` rather than throwing.
690
+ * Project one contract shape into a portable column schema.
443
691
  *
444
- * @param value - The value to test
445
- * @returns `true` when `value` is a well-formed `DriverMeta`
446
- *
447
- * @example
448
- * ```ts
449
- * isDriverMeta({ version: 1, schema: [] }) // true
450
- * isDriverMeta({ version: 1, schema: [{ name: 'users' }] }) // false
451
- * ```
692
+ * @param name - The column name
693
+ * @param shape - The column contract shape
694
+ * @returns The portable storage and independent absence/null acceptance
452
695
  */
453
- function isDriverMeta(value) {
454
- return (0, _orkestrel_contract.isRecord)(value) && (0, _orkestrel_contract.isFiniteNumber)(value.version) && (0, _orkestrel_contract.isArray)(value.schema) && value.schema.every((table) => (0, _orkestrel_contract.isRecord)(table) && (0, _orkestrel_contract.isString)(table.name) && (0, _orkestrel_contract.isString)(table.primary) && (0, _orkestrel_contract.isArray)(table.columns) && table.columns.every((column) => (0, _orkestrel_contract.isRecord)(column) && (0, _orkestrel_contract.isString)(column.name) && (0, _orkestrel_contract.isString)(column.type) && [
455
- "text",
456
- "integer",
457
- "real",
458
- "boolean",
459
- "json",
460
- "blob"
461
- ].some((type) => type === column.type) && (0, _orkestrel_contract.isBoolean)(column.nullable)) && (0, _orkestrel_contract.isArray)(table.indexes) && table.indexes.every((index) => (0, _orkestrel_contract.isArray)(index) && index.every((column) => (0, _orkestrel_contract.isString)(column))));
696
+ function shapeToColumnSchema(name, shape) {
697
+ const isColumn = (0, _orkestrel_contract.compileGuard)((0, _orkestrel_contract.objectShape)({ value: shape }));
698
+ return {
699
+ name,
700
+ storage: shapeToColumnStorage(shape),
701
+ optional: isColumn({}),
702
+ nullable: isColumn({ value: null })
703
+ };
462
704
  }
463
705
  /**
464
- * Throw when an {@link ReadOptions.signal | AbortSignal} has fired — the shared
465
- * cancellation gate checked at operation boundaries and between streamed rows.
706
+ * Throw when an {@link OperationOptions.signal | AbortSignal} has fired — the shared
707
+ * abort gate checked at operation boundaries and between streamed rows.
466
708
  *
467
709
  * @remarks
468
710
  * A no-op for `undefined` or a live signal, so callers thread `options?.signal`
469
711
  * straight through. When the signal has aborted, throws an `ABORTED`
470
712
  * {@link DatabaseError} carrying the signal's `reason` in its context — callers
471
- * mint signals with whatever tool they like (`AbortSignal.timeout(ms)`,
472
- * `new AbortController()`, `@orkestrel/abort`).
713
+ * mint signals with native APIs such as `AbortSignal.timeout(ms)` or
714
+ * `new AbortController()`.
473
715
  *
474
716
  * @param signal - The signal to check, if any
475
717
  * @returns Nothing — returns normally while the signal is live
@@ -501,11 +743,12 @@ function checkAbort(signal) {
501
743
  * `column.remove` / `index.add` / `index.remove` steps. Step order is
502
744
  * deterministic: every `table.remove`, then every `table.add`, then each
503
745
  * shared table's column/index changes in `declared` order. `from` / `to` are
504
- * plan labels only version tracking itself is deferred to persistent
505
- * backends.
746
+ * plan labels only; versioning drivers persist and reconcile them through
747
+ * {@link DriverMetadata}.
506
748
  *
507
749
  * A column present in BOTH schemas under the same name but with a different
508
- * `type` or `nullable` throws a `MIGRATION` {@link DatabaseError} naming the
750
+ * `storage`, `optional`, or `nullable` value throws a `MIGRATION`
751
+ * {@link DatabaseError} naming the
509
752
  * table, the column, and the from→to difference — a name-only diff would
510
753
  * otherwise silently produce NO step for the drift, and versioned
511
754
  * reconciliation would stamp over it. There is no automatic in-place
@@ -518,42 +761,66 @@ function checkAbort(signal) {
518
761
  * @param from - The plan's source version label (defaults to `0`)
519
762
  * @param to - The plan's target version label (defaults to `1`)
520
763
  * @returns The migration plan moving `deployed` toward `declared`
521
- * @throws A `MIGRATION` {@link DatabaseError} when a shared column's `type` or
522
- * `nullable` differs between `deployed` and `declared`
764
+ * @throws A `MIGRATION` {@link DatabaseError} when a shared table's primary or
765
+ * a shared column's `storage`, `optional`, or `nullable` differs, or when a
766
+ * required non-null column would be added to an existing table without a
767
+ * portable backfill
523
768
  *
524
769
  * @example
525
770
  * ```ts
526
771
  * const plan = planMigration(
527
- * [{ name: 'users', primary: 'id', columns: [], indexes: [] }],
528
- * [{ name: 'users', primary: 'id', columns: [{ name: 'age', type: 'integer', nullable: false }], indexes: [] }],
772
+ * [{ name: 'users', primary: 'id', columns: [{ name: 'id', storage: 'text', optional: false, nullable: false }], indexes: [] }],
773
+ * [{ name: 'users', primary: 'id', columns: [{ name: 'id', storage: 'text', optional: false, nullable: false }, { name: 'age', storage: 'integer', optional: true, nullable: false }], indexes: [] }],
529
774
  * )
530
775
  * // plan.steps === [{ operation: 'column.add', table: 'users', column: { name: 'age', ... } }]
531
776
  * ```
532
777
  */
533
778
  function planMigration(deployed, declared, from = 0, to = 1) {
534
- const deployedByName = new Map(deployed.map((table) => [table.name, table]));
535
- const declaredByName = new Map(declared.map((table) => [table.name, table]));
779
+ if (!Number.isFinite(from) || !Number.isFinite(to)) throw new DatabaseError("MIGRATION", "Migration versions must be finite", {
780
+ from,
781
+ to
782
+ });
783
+ let beforeSchema;
784
+ let targetSchema;
785
+ try {
786
+ beforeSchema = normalizeDriverSchema(deployed);
787
+ targetSchema = normalizeDriverSchema(declared);
788
+ } catch (error) {
789
+ throw new DatabaseError("MIGRATION", "Migration schema is invalid", { cause: error });
790
+ }
791
+ const deployedByName = new Map(beforeSchema.map((table) => [table.name, table]));
792
+ const declaredByName = new Map(targetSchema.map((table) => [table.name, table]));
536
793
  const steps = [];
537
- for (const table of deployed) if (!declaredByName.has(table.name)) steps.push({
794
+ for (const table of beforeSchema) if (!declaredByName.has(table.name)) steps.push({
538
795
  operation: "table.remove",
539
796
  table: table.name
540
797
  });
541
- for (const table of declared) if (!deployedByName.has(table.name)) steps.push({
798
+ for (const table of targetSchema) if (!deployedByName.has(table.name)) steps.push({
542
799
  operation: "table.add",
543
800
  table
544
801
  });
545
- for (const table of declared) {
802
+ for (const table of targetSchema) {
546
803
  const before = deployedByName.get(table.name);
547
804
  if (before === void 0) continue;
548
- const beforeColumns = new Map(before.columns.map((column) => [column.name, column]));
549
- const afterColumns = new Map(table.columns.map((column) => [column.name, column]));
550
- for (const column of before.columns) if (!afterColumns.has(column.name)) steps.push({
805
+ if (before.primary !== table.primary) throw new DatabaseError("MIGRATION", `planMigration: primary column on table '${table.name}' changed from '${before.primary}' to '${table.primary}'`, {
806
+ table: table.name,
807
+ from: before.primary,
808
+ to: table.primary
809
+ });
810
+ const beforeColumnMap = new Map(before.columns.map((column) => [column.name, column]));
811
+ const afterColumnMap = new Map(table.columns.map((column) => [column.name, column]));
812
+ for (const index of before.indexes) if (!table.indexes.some((candidate) => equalsValue(candidate, index))) steps.push({
813
+ operation: "index.remove",
814
+ table: table.name,
815
+ index
816
+ });
817
+ for (const column of before.columns) if (!afterColumnMap.has(column.name)) steps.push({
551
818
  operation: "column.remove",
552
819
  table: table.name,
553
820
  column: column.name
554
821
  });
555
822
  for (const column of table.columns) {
556
- const previous = beforeColumns.get(column.name);
823
+ const previous = beforeColumnMap.get(column.name);
557
824
  if (previous === void 0) {
558
825
  steps.push({
559
826
  operation: "column.add",
@@ -562,35 +829,158 @@ function planMigration(deployed, declared, from = 0, to = 1) {
562
829
  });
563
830
  continue;
564
831
  }
565
- if (previous.type !== column.type || previous.nullable !== column.nullable) throw new DatabaseError("MIGRATION", `planMigration: column '${column.name}' on table '${table.name}' changed shape (type ${previous.type}→${column.type}, nullable ${previous.nullable}→${column.nullable}) — in-place type/nullability changes are not auto-migrated; add a new column, copy/convert the data, then remove the old column`, {
832
+ if (previous.storage !== column.storage || previous.optional !== column.optional || previous.nullable !== column.nullable) throw new DatabaseError("MIGRATION", `planMigration: column '${column.name}' on table '${table.name}' changed shape (storage ${previous.storage}→${column.storage}, optional ${previous.optional}→${column.optional}, nullable ${previous.nullable}→${column.nullable}) — in-place storage/optionality/nullability changes are not auto-migrated; add a new column, copy/convert the data, then remove the old column`, {
566
833
  table: table.name,
567
834
  column: column.name,
568
835
  from: {
569
- type: previous.type,
836
+ storage: previous.storage,
837
+ optional: previous.optional,
570
838
  nullable: previous.nullable
571
839
  },
572
840
  to: {
573
- type: column.type,
841
+ storage: column.storage,
842
+ optional: column.optional,
574
843
  nullable: column.nullable
575
844
  }
576
845
  });
577
846
  }
578
- for (const index of before.indexes) if (!table.indexes.some((candidate) => deepEqual(candidate, index))) steps.push({
579
- operation: "index.remove",
580
- table: table.name,
581
- index
582
- });
583
- for (const index of table.indexes) if (!before.indexes.some((candidate) => deepEqual(candidate, index))) steps.push({
847
+ for (const index of table.indexes) if (!before.indexes.some((candidate) => equalsValue(candidate, index))) steps.push({
584
848
  operation: "index.add",
585
849
  table: table.name,
586
850
  index
587
851
  });
588
852
  }
589
- return {
853
+ const projected = projectMigrationSchema(beforeSchema, steps);
854
+ if (!equalsValue(projected, targetSchema)) throw new DatabaseError("MIGRATION", "Migration plan does not project to the declared schema", {
855
+ projected,
856
+ declared: targetSchema
857
+ });
858
+ return cloneMigrationInput({ plan: {
590
859
  from,
591
860
  to,
592
861
  steps
593
- };
862
+ } }).plan;
863
+ }
864
+ /**
865
+ * Sequentially project migration steps over a canonical validated owned schema.
866
+ * Adding a required non-null column to an existing table rejects with
867
+ * `MIGRATION`; optional-only and nullable-only additions remain portable.
868
+ *
869
+ * @param schema - The initial deployed schema
870
+ * @param steps - The ordered migration steps
871
+ * @returns A fresh owned final schema
872
+ */
873
+ function projectMigrationSchema(schema, steps) {
874
+ let owned;
875
+ let projectedSteps;
876
+ try {
877
+ owned = normalizeDriverSchema(schema);
878
+ projectedSteps = cloneMigrationInput({ plan: {
879
+ from: 0,
880
+ to: 1,
881
+ steps
882
+ } }).plan.steps;
883
+ } catch (error) {
884
+ throw new DatabaseError("MIGRATION", "Migration input is invalid", { cause: error });
885
+ }
886
+ const tables = new Map(owned.map((table) => [table.name, table]));
887
+ for (const step of projectedSteps) {
888
+ if (step.operation === "table.add") {
889
+ if (tables.has(step.table.name)) throw new DatabaseError("MIGRATION", `migrate: table '${step.table.name}' already exists`, { table: step.table.name });
890
+ tables.set(step.table.name, step.table);
891
+ continue;
892
+ }
893
+ const table = tables.get(step.table);
894
+ if (table === void 0) throw new DatabaseError("MIGRATION", `migrate: table '${step.table}' does not exist`, { table: step.table });
895
+ if (step.operation === "table.remove") {
896
+ tables.delete(step.table);
897
+ continue;
898
+ }
899
+ if (step.operation === "column.add") {
900
+ if (table.columns.some((column) => column.name === step.column.name)) throw new DatabaseError("MIGRATION", `migrate: column '${step.column.name}' already exists`, {
901
+ table: step.table,
902
+ column: step.column.name
903
+ });
904
+ if (!step.column.optional && !step.column.nullable) throw new DatabaseError("MIGRATION", `migrate: required non-null column '${step.column.name}' cannot be added automatically to existing table '${step.table}'`, {
905
+ table: step.table,
906
+ column: step.column.name
907
+ });
908
+ tables.set(step.table, {
909
+ ...table,
910
+ columns: [...table.columns, step.column]
911
+ });
912
+ continue;
913
+ }
914
+ if (step.operation === "column.remove") {
915
+ if (!table.columns.some((column) => column.name === step.column)) throw new DatabaseError("MIGRATION", `migrate: column '${step.column}' does not exist`, {
916
+ table: step.table,
917
+ column: step.column
918
+ });
919
+ if (table.primary === step.column) throw new DatabaseError("MIGRATION", "migrate: cannot remove the primary column", {
920
+ table: step.table,
921
+ column: step.column
922
+ });
923
+ if (table.indexes.some((index) => index.includes(step.column))) throw new DatabaseError("MIGRATION", "migrate: cannot remove an indexed column", {
924
+ table: step.table,
925
+ column: step.column
926
+ });
927
+ tables.set(step.table, {
928
+ ...table,
929
+ columns: table.columns.filter((column) => column.name !== step.column)
930
+ });
931
+ continue;
932
+ }
933
+ if (step.operation === "index.add") {
934
+ if (step.index.length === 0 || step.index.some((name) => !table.columns.some((column) => column.name === name))) throw new DatabaseError("MIGRATION", "migrate: index references a missing column", {
935
+ table: step.table,
936
+ index: step.index
937
+ });
938
+ if (table.indexes.some((index) => equalsValue(index, step.index))) throw new DatabaseError("MIGRATION", "migrate: index already exists", {
939
+ table: step.table,
940
+ index: step.index
941
+ });
942
+ tables.set(step.table, {
943
+ ...table,
944
+ indexes: [...table.indexes, step.index]
945
+ });
946
+ continue;
947
+ }
948
+ if (!table.indexes.some((index) => equalsValue(index, step.index))) throw new DatabaseError("MIGRATION", "migrate: index does not exist", {
949
+ table: step.table,
950
+ index: step.index
951
+ });
952
+ tables.set(step.table, {
953
+ ...table,
954
+ indexes: table.indexes.filter((index) => !equalsValue(index, step.index))
955
+ });
956
+ }
957
+ try {
958
+ return normalizeDriverSchema([...tables.values()]);
959
+ } catch (error) {
960
+ throw new DatabaseError("MIGRATION", "Projected migration schema is invalid", { cause: error });
961
+ }
962
+ }
963
+ /**
964
+ * Canonicalize an unknown driver schema into a distinct deeply frozen snapshot.
965
+ *
966
+ * @remarks
967
+ * Table and column lists are sorted by name. The index list is sorted by the
968
+ * complete serialized tuple while column order inside each compound index is
969
+ * preserved because it carries index semantics. Validation and ownership flow
970
+ * through {@link cloneDriverSchema} before and after projection.
971
+ *
972
+ * @param value - Unknown driver schema
973
+ * @returns A validated, owned canonical schema
974
+ */
975
+ function normalizeDriverSchema(value) {
976
+ const tables = cloneDriverSchema(value).map((table) => ({
977
+ name: table.name,
978
+ primary: table.primary,
979
+ columns: [...table.columns].sort((left, right) => compareValues(left.name, right.name)),
980
+ indexes: [...table.indexes].sort((left, right) => compareValues(JSON.stringify(left), JSON.stringify(right)))
981
+ }));
982
+ tables.sort((left, right) => compareValues(left.name, right.name));
983
+ return cloneDriverSchema(tables);
594
984
  }
595
985
  /**
596
986
  * Apply one table's {@link MigrationStep}s to its rows — a pure row transform.
@@ -638,21 +1028,24 @@ function migrateRows(rows, steps) {
638
1028
  * `read` of a missing key returns `undefined`; `write`/`read` round-trip with
639
1029
  * DEEP copy-in/copy-out isolation (mutating the caller's row — including a
640
1030
  * NESTED field — after `write`, or a row `read` returns, never perturbs
641
- * stored state) and upsert-overwrite; `delete` returns `true` then `false`;
1031
+ * stored state) and upsert-overwrite; simultaneous same-key `insert` calls
1032
+ * produce exactly one commit and one `CONFLICT`; pre-aborted `write`,
1033
+ * `insert`, and `delete` calls leave storage unchanged; `delete` returns
1034
+ * `true` then `false`;
642
1035
  * `keys`/`scan` yield in ascending key order; `clear` empties only its target
643
1036
  * table; `snapshot`'s rollback thunk restores pre-snapshot state, including a
644
1037
  * NESTED field mutated in place on a read-back row between capture and
645
1038
  * restore; a scoped `snapshot(['users'])` rolls back only the named table,
646
1039
  * leaving a concurrent mutation to another table intact; a
647
1040
  * non-`id` primary key (`posts.slug`) round-trips; a nested-object row
648
- * round-trips structurally (via {@link deepEqual}). The optional surface is
1041
+ * round-trips structurally (via {@link equalsValue}). The optional surface is
649
1042
  * presence-gated: when `migrate` exists, a `column.remove` plan strips the
650
1043
  * column from stored rows and a plan referencing an unknown table throws
651
1044
  * `DatabaseError` `MIGRATION`; when `stream` exists, it yields only
652
1045
  * condition-matching rows and honors `offset`/`limit`; when `transaction`
653
- * exists, `commit` persists and `rollback` restores; when both `meta` and
654
- * `stamp` exist, a fresh store's `meta()` is `undefined`, and after
655
- * `stamp({ version, schema })`, `meta()` returns the exact stamped value.
1046
+ * exists, `commit` persists and `rollback` restores; when both `metadata` and
1047
+ * `stamp` exist, a fresh store's `metadata()` is `undefined`, and after
1048
+ * `stamp({ version, schema })`, `metadata()` returns the exact stamped value.
656
1049
  *
657
1050
  * Each phase runs within a `try`/`catch`: an EXPECTED mismatch yields a
658
1051
  * finding built from the assertion, while an UNEXPECTED throw (a driver
@@ -684,23 +1077,27 @@ async function* driverFindings(factory) {
684
1077
  columns: [
685
1078
  {
686
1079
  name: "id",
687
- type: "text",
1080
+ storage: "text",
1081
+ optional: false,
688
1082
  nullable: false
689
1083
  },
690
1084
  {
691
1085
  name: "name",
692
- type: "text",
1086
+ storage: "text",
1087
+ optional: false,
693
1088
  nullable: false
694
1089
  },
695
1090
  {
696
1091
  name: "age",
697
- type: "integer",
698
- nullable: true
1092
+ storage: "integer",
1093
+ optional: true,
1094
+ nullable: false
699
1095
  },
700
1096
  {
701
1097
  name: "meta",
702
- type: "json",
703
- nullable: true
1098
+ storage: "json",
1099
+ optional: true,
1100
+ nullable: false
704
1101
  }
705
1102
  ],
706
1103
  indexes: []
@@ -710,11 +1107,13 @@ async function* driverFindings(factory) {
710
1107
  primary: "slug",
711
1108
  columns: [{
712
1109
  name: "slug",
713
- type: "text",
1110
+ storage: "text",
1111
+ optional: false,
714
1112
  nullable: false
715
1113
  }, {
716
1114
  name: "title",
717
- type: "text",
1115
+ storage: "text",
1116
+ optional: false,
718
1117
  nullable: false
719
1118
  }],
720
1119
  indexes: []
@@ -756,7 +1155,7 @@ async function* driverFindings(factory) {
756
1155
  const driver = factory();
757
1156
  await driver.open(CONFORMANCE_SCHEMA);
758
1157
  const input = {
759
- id: "u1",
1158
+ id: "caller",
760
1159
  name: "Ada",
761
1160
  age: 30,
762
1161
  meta: { tags: ["a"] }
@@ -771,7 +1170,7 @@ async function* driverFindings(factory) {
771
1170
  age: 30,
772
1171
  meta: { tags: ["a"] }
773
1172
  };
774
- if (stored === void 0 || !deepEqual(stored, original)) {
1173
+ if (stored === void 0 || !equalsValue(stored, original)) {
775
1174
  await driver.close();
776
1175
  yield {
777
1176
  check: "copy-in",
@@ -787,7 +1186,7 @@ async function* driverFindings(factory) {
787
1186
  stored.name = "Mutated after read";
788
1187
  if ((0, _orkestrel_contract.isRecord)(stored.meta) && Array.isArray(stored.meta.tags)) stored.meta.tags.push("mutated");
789
1188
  const reread = await driver.read("users", "u1");
790
- if (reread === void 0 || !deepEqual(reread, original)) {
1189
+ if (reread === void 0 || !equalsValue(reread, original)) {
791
1190
  await driver.close();
792
1191
  yield {
793
1192
  check: "copy-out",
@@ -801,19 +1200,23 @@ async function* driverFindings(factory) {
801
1200
  break writeRead;
802
1201
  }
803
1202
  const overwrite = {
804
- id: "u1",
1203
+ id: "caller",
805
1204
  name: "Ada Overwritten",
806
1205
  age: 31
807
1206
  };
808
1207
  await driver.write("users", "u1", overwrite);
809
1208
  const overwritten = await driver.read("users", "u1");
810
1209
  await driver.close();
811
- if (overwritten === void 0 || !deepEqual(overwritten, overwrite)) yield {
1210
+ const expectedOverwrite = {
1211
+ ...overwrite,
1212
+ id: "u1"
1213
+ };
1214
+ if (overwritten === void 0 || !equalsValue(overwritten, expectedOverwrite)) yield {
812
1215
  check: "upsert",
813
1216
  message: "write must upsert-overwrite an existing key",
814
1217
  context: {
815
1218
  table: "users",
816
- expected: overwrite,
1219
+ expected: expectedOverwrite,
817
1220
  actual: overwritten
818
1221
  }
819
1222
  };
@@ -824,6 +1227,47 @@ async function* driverFindings(factory) {
824
1227
  context: { error }
825
1228
  };
826
1229
  }
1230
+ try {
1231
+ const driver = factory();
1232
+ await driver.open(CONFORMANCE_SCHEMA);
1233
+ const outcomes = await Promise.allSettled([driver.insert("users", "u1", {
1234
+ id: "u1",
1235
+ name: "Ada",
1236
+ age: 30
1237
+ }), driver.insert("users", "u1", {
1238
+ id: "u1",
1239
+ name: "Grace",
1240
+ age: 40
1241
+ })]);
1242
+ let fulfilled = 0;
1243
+ let conflicted = 0;
1244
+ for (const outcome of outcomes) if (outcome.status === "fulfilled") fulfilled += 1;
1245
+ else if (isDatabaseError(outcome.reason) && outcome.reason.code === "CONFLICT") conflicted += 1;
1246
+ const keys = await driver.keys("users");
1247
+ await driver.close();
1248
+ if (fulfilled !== 1 || conflicted !== 1 || !equalsValue(keys, ["u1"])) yield {
1249
+ check: "insert-atomic",
1250
+ message: "concurrent same-key inserts must produce one commit and one CONFLICT",
1251
+ context: {
1252
+ expected: {
1253
+ fulfilled: 1,
1254
+ conflicted: 1,
1255
+ keys: ["u1"]
1256
+ },
1257
+ actual: {
1258
+ fulfilled,
1259
+ conflicted,
1260
+ keys
1261
+ }
1262
+ }
1263
+ };
1264
+ } catch (error) {
1265
+ yield {
1266
+ check: "insert-atomic",
1267
+ message: error instanceof Error ? error.message : String(error),
1268
+ context: { error }
1269
+ };
1270
+ }
827
1271
  deletePhase: try {
828
1272
  const driver = factory();
829
1273
  await driver.open(CONFORMANCE_SCHEMA);
@@ -864,6 +1308,69 @@ async function* driverFindings(factory) {
864
1308
  context: { error }
865
1309
  };
866
1310
  }
1311
+ try {
1312
+ const driver = factory();
1313
+ await driver.open(CONFORMANCE_SCHEMA);
1314
+ await driver.write("users", "u1", {
1315
+ id: "u1",
1316
+ name: "Ada",
1317
+ age: 30
1318
+ });
1319
+ const controller = new AbortController();
1320
+ controller.abort("conformance abort");
1321
+ let writeError;
1322
+ let insertError;
1323
+ let deleteError;
1324
+ try {
1325
+ await driver.write("users", "u2", {
1326
+ id: "u2",
1327
+ name: "Grace",
1328
+ age: 40
1329
+ }, { signal: controller.signal });
1330
+ } catch (error) {
1331
+ writeError = error;
1332
+ }
1333
+ try {
1334
+ await driver.insert("users", "u2", {
1335
+ id: "u2",
1336
+ name: "Grace",
1337
+ age: 40
1338
+ }, { signal: controller.signal });
1339
+ } catch (error) {
1340
+ insertError = error;
1341
+ }
1342
+ try {
1343
+ await driver.delete("users", "u1", { signal: controller.signal });
1344
+ } catch (error) {
1345
+ deleteError = error;
1346
+ }
1347
+ const keys = await driver.keys("users");
1348
+ await driver.close();
1349
+ if (!isDatabaseError(writeError) || writeError.code !== "ABORTED" || !isDatabaseError(insertError) || insertError.code !== "ABORTED" || !isDatabaseError(deleteError) || deleteError.code !== "ABORTED" || !equalsValue(keys, ["u1"])) yield {
1350
+ check: "mutation-abort",
1351
+ message: "pre-aborted write/insert/delete must reject ABORTED without changing rows",
1352
+ context: {
1353
+ expected: {
1354
+ write: "ABORTED",
1355
+ insert: "ABORTED",
1356
+ delete: "ABORTED",
1357
+ keys: ["u1"]
1358
+ },
1359
+ actual: {
1360
+ write: isDatabaseError(writeError) ? writeError.code : writeError,
1361
+ insert: isDatabaseError(insertError) ? insertError.code : insertError,
1362
+ delete: isDatabaseError(deleteError) ? deleteError.code : deleteError,
1363
+ keys
1364
+ }
1365
+ }
1366
+ };
1367
+ } catch (error) {
1368
+ yield {
1369
+ check: "mutation-abort",
1370
+ message: error instanceof Error ? error.message : String(error),
1371
+ context: { error }
1372
+ };
1373
+ }
867
1374
  orderPhase: try {
868
1375
  const driver = factory();
869
1376
  await driver.open(CONFORMANCE_SCHEMA);
@@ -890,7 +1397,7 @@ async function* driverFindings(factory) {
890
1397
  "c"
891
1398
  ];
892
1399
  const keys = [...await driver.keys("users")];
893
- if (!deepEqual(keys, expected)) {
1400
+ if (!equalsValue(keys, expected)) {
894
1401
  await driver.close();
895
1402
  yield {
896
1403
  check: "keys-order",
@@ -907,7 +1414,7 @@ async function* driverFindings(factory) {
907
1414
  for await (const row of driver.scan("users")) scanned.push(row);
908
1415
  const scannedIds = scanned.map((row) => row.id);
909
1416
  await driver.close();
910
- if (!deepEqual(scannedIds, expected)) yield {
1417
+ if (!equalsValue(scannedIds, expected)) yield {
911
1418
  check: "scan-order",
912
1419
  message: "scan must yield rows in ascending key order",
913
1420
  context: {
@@ -985,7 +1492,7 @@ async function* driverFindings(factory) {
985
1492
  await driver.delete("users", "u1");
986
1493
  await rollback();
987
1494
  const keys = [...await driver.keys("users")];
988
- if (!deepEqual(keys, ["u1"])) {
1495
+ if (!equalsValue(keys, ["u1"])) {
989
1496
  await driver.close();
990
1497
  yield {
991
1498
  check: "snapshot-rollback",
@@ -1000,7 +1507,7 @@ async function* driverFindings(factory) {
1000
1507
  }
1001
1508
  const restored = await driver.read("users", "u1");
1002
1509
  await driver.close();
1003
- if (restored === void 0 || !deepEqual(restored, original)) yield {
1510
+ if (restored === void 0 || !equalsValue(restored, original)) yield {
1004
1511
  check: "snapshot-rollback-value",
1005
1512
  message: "snapshot rollback must restore pre-snapshot row values",
1006
1513
  context: {
@@ -1038,7 +1545,7 @@ async function* driverFindings(factory) {
1038
1545
  await rollback();
1039
1546
  const restored = await driver.read("users", "u3");
1040
1547
  await driver.close();
1041
- if (restored === void 0 || !deepEqual(restored, original)) yield {
1548
+ if (restored === void 0 || !equalsValue(restored, original)) yield {
1042
1549
  check: "snapshot-nested",
1043
1550
  message: "snapshot rollback must restore pre-snapshot nested field values, unaffected by a later in-place mutation of a read-back row",
1044
1551
  context: {
@@ -1058,7 +1565,7 @@ async function* driverFindings(factory) {
1058
1565
  const driver = factory();
1059
1566
  await driver.open(CONFORMANCE_SCHEMA);
1060
1567
  await driver.write("posts", "hello-world", {
1061
- slug: "hello-world",
1568
+ slug: "caller",
1062
1569
  title: "Hello"
1063
1570
  });
1064
1571
  const post = await driver.read("posts", "hello-world");
@@ -1095,7 +1602,7 @@ async function* driverFindings(factory) {
1095
1602
  await driver.write("users", "u3", nested);
1096
1603
  const readBack = await driver.read("users", "u3");
1097
1604
  await driver.close();
1098
- if (readBack === void 0 || !deepEqual(readBack, nested)) yield {
1605
+ if (readBack === void 0 || !equalsValue(readBack, nested)) yield {
1099
1606
  check: "nested-roundtrip",
1100
1607
  message: "a nested-object row must round-trip structurally",
1101
1608
  context: {
@@ -1118,8 +1625,9 @@ async function* driverFindings(factory) {
1118
1625
  ...CONFORMANCE_USERS_SCHEMA,
1119
1626
  columns: [...CONFORMANCE_USERS_SCHEMA.columns, {
1120
1627
  name: "legacy",
1121
- type: "boolean",
1122
- nullable: true
1628
+ storage: "boolean",
1629
+ optional: true,
1630
+ nullable: false
1123
1631
  }]
1124
1632
  };
1125
1633
  await driver.open([deployedUsers, CONFORMANCE_POSTS_SCHEMA]);
@@ -1130,7 +1638,7 @@ async function* driverFindings(factory) {
1130
1638
  legacy: true
1131
1639
  });
1132
1640
  const removePlan = planMigration([deployedUsers], [CONFORMANCE_USERS_SCHEMA]);
1133
- await driver.migrate(removePlan);
1641
+ await driver.migrate({ plan: removePlan });
1134
1642
  const migrated = await driver.read("users", "u1");
1135
1643
  if (migrated === void 0 || "legacy" in migrated) {
1136
1644
  await driver.close();
@@ -1147,14 +1655,14 @@ async function* driverFindings(factory) {
1147
1655
  }
1148
1656
  let caught;
1149
1657
  try {
1150
- await driver.migrate({
1658
+ await driver.migrate({ plan: {
1151
1659
  from: 0,
1152
1660
  to: 1,
1153
1661
  steps: [{
1154
1662
  operation: "table.remove",
1155
1663
  table: "ghost"
1156
1664
  }]
1157
- });
1665
+ } });
1158
1666
  } catch (error) {
1159
1667
  caught = error;
1160
1668
  }
@@ -1196,16 +1704,16 @@ async function* driverFindings(factory) {
1196
1704
  age: 30
1197
1705
  }
1198
1706
  ]) await driver.write("users", row.id, row);
1199
- const criteria = { conditions: [{
1707
+ const input = { conditions: [{
1200
1708
  column: "age",
1201
1709
  operator: "above",
1202
1710
  values: [10],
1203
1711
  connector: "and"
1204
1712
  }] };
1205
1713
  const matched = [];
1206
- for await (const row of driver.stream("users", criteria)) matched.push(row);
1714
+ for await (const row of driver.stream("users", input)) matched.push(row);
1207
1715
  const matchedIds = matched.map((row) => row.id).sort();
1208
- if (!deepEqual(matchedIds, ["b", "c"])) {
1716
+ if (!equalsValue(matchedIds, ["b", "c"])) {
1209
1717
  await driver.close();
1210
1718
  yield {
1211
1719
  check: "stream-match",
@@ -1249,15 +1757,15 @@ async function* driverFindings(factory) {
1249
1757
  name: "Ada",
1250
1758
  age: 30
1251
1759
  });
1252
- const committing = await driver.transaction();
1253
- await driver.write("users", "u2", {
1254
- id: "u2",
1255
- name: "Grace",
1256
- age: 40
1760
+ await driver.transaction(async (transaction) => {
1761
+ await transaction.write("users", "u2", {
1762
+ id: "u2",
1763
+ name: "Grace",
1764
+ age: 40
1765
+ });
1257
1766
  });
1258
- await committing.commit();
1259
1767
  const afterCommit = [...await driver.keys("users")].sort();
1260
- if (!deepEqual(afterCommit, ["u1", "u2"])) {
1768
+ if (!equalsValue(afterCommit, ["u1", "u2"])) {
1261
1769
  await driver.close();
1262
1770
  yield {
1263
1771
  check: "transaction-commit",
@@ -1270,16 +1778,22 @@ async function* driverFindings(factory) {
1270
1778
  };
1271
1779
  break transactionPhase;
1272
1780
  }
1273
- const rollingBack = await driver.transaction();
1274
- await driver.write("users", "u3", {
1275
- id: "u3",
1276
- name: "Marie",
1277
- age: 50
1278
- });
1279
- await rollingBack.rollback();
1781
+ const reason = {};
1782
+ try {
1783
+ await driver.transaction(async (transaction) => {
1784
+ await transaction.write("users", "u3", {
1785
+ id: "u3",
1786
+ name: "Marie",
1787
+ age: 50
1788
+ });
1789
+ throw reason;
1790
+ });
1791
+ } catch (error) {
1792
+ if (error !== reason) throw error;
1793
+ }
1280
1794
  const afterRollback = [...await driver.keys("users")].sort();
1281
1795
  await driver.close();
1282
- if (!deepEqual(afterRollback, ["u1", "u2"])) yield {
1796
+ if (!equalsValue(afterRollback, ["u1", "u2"])) yield {
1283
1797
  check: "transaction-rollback",
1284
1798
  message: "transaction rollback must restore pre-transaction state",
1285
1799
  context: {
@@ -1295,33 +1809,33 @@ async function* driverFindings(factory) {
1295
1809
  context: { error }
1296
1810
  };
1297
1811
  }
1298
- metaPhase: try {
1812
+ metadataPhase: try {
1299
1813
  const driver = factory();
1300
- if (driver.meta === void 0 || driver.stamp === void 0) break metaPhase;
1814
+ if (driver.metadata === void 0 || driver.stamp === void 0) break metadataPhase;
1301
1815
  await driver.open(CONFORMANCE_SCHEMA);
1302
- const fresh = await driver.meta();
1816
+ const fresh = await driver.metadata();
1303
1817
  if (fresh !== void 0) {
1304
1818
  await driver.close();
1305
1819
  yield {
1306
- check: "meta-fresh",
1307
- message: "a fresh store must report undefined meta",
1820
+ check: "metadata-fresh",
1821
+ message: "a fresh store must report undefined metadata",
1308
1822
  context: {
1309
1823
  expected: void 0,
1310
1824
  actual: fresh
1311
1825
  }
1312
1826
  };
1313
- break metaPhase;
1827
+ break metadataPhase;
1314
1828
  }
1315
1829
  const stamped = {
1316
1830
  version: 1,
1317
1831
  schema: CONFORMANCE_SCHEMA
1318
1832
  };
1319
1833
  await driver.stamp(stamped);
1320
- const read = await driver.meta();
1834
+ const read = await driver.metadata();
1321
1835
  await driver.close();
1322
- if (read === void 0 || !deepEqual(read, stamped)) yield {
1323
- check: "meta-stamp",
1324
- message: "meta() must return exactly the last-stamped value",
1836
+ if (read === void 0 || !equalsValue(read, stamped)) yield {
1837
+ check: "metadata-stamp",
1838
+ message: "metadata() must return exactly the last-stamped value",
1325
1839
  context: {
1326
1840
  expected: stamped,
1327
1841
  actual: read
@@ -1329,7 +1843,7 @@ async function* driverFindings(factory) {
1329
1843
  };
1330
1844
  } catch (error) {
1331
1845
  yield {
1332
- check: "meta-stamp",
1846
+ check: "metadata-stamp",
1333
1847
  message: error instanceof Error ? error.message : String(error),
1334
1848
  context: { error }
1335
1849
  };
@@ -1358,7 +1872,7 @@ async function* driverFindings(factory) {
1358
1872
  });
1359
1873
  await rollback();
1360
1874
  const usersKeys = [...await driver.keys("users")];
1361
- if (!deepEqual(usersKeys, ["u1"])) {
1875
+ if (!equalsValue(usersKeys, ["u1"])) {
1362
1876
  await driver.close();
1363
1877
  yield {
1364
1878
  check: "snapshot-scoped-users",
@@ -1373,7 +1887,7 @@ async function* driverFindings(factory) {
1373
1887
  }
1374
1888
  const postsKeys = [...await driver.keys("posts")].sort();
1375
1889
  await driver.close();
1376
- if (!deepEqual(postsKeys, ["p1", "p2"])) yield {
1890
+ if (!equalsValue(postsKeys, ["p1", "p2"])) yield {
1377
1891
  check: "snapshot-scoped-posts",
1378
1892
  message: "a scoped snapshot must leave an unnamed table's mutations intact",
1379
1893
  context: {
@@ -1446,62 +1960,451 @@ async function auditDriver(factory) {
1446
1960
  for await (const finding of driverFindings(factory)) findings.push(finding);
1447
1961
  return findings;
1448
1962
  }
1449
- /**
1450
- * Generate an RFC 4122 version 4 UUID from a number source — no host crypto global.
1451
- *
1452
- * @remarks
1453
- * Draws exactly {@link UUID_BYTE_COUNT} values from `random`, one per byte, then
1454
- * forces the version (`4`) and variant (`10xx`) bits. The default source is
1455
- * `Math.random` — a pure-ECMAScript intrinsic, so generation works on every host;
1456
- * pass a seeded source (`seededRandom` from `@orkestrel/contract`) and reuse it
1457
- * across calls for reproducible sequences in tests and fixtures — production
1458
- * identifiers should keep the default source, whose engine entropy is far larger
1459
- * than a 32-bit seed. Each byte is floored and masked, so a source straying
1460
- * outside `[0, 1)` (negative, `>= 1`, `NaN`, `Infinity`) can never yield a
1461
- * malformed UUID. Suitable as a collision-resistant record identifier — not a
1462
- * cryptographic token; never use one as a secret.
1463
- *
1464
- * @param random - A number source returning values in the half-open range `[0, 1)` (defaults to `Math.random`)
1465
- * @returns A lowercase RFC 4122 version 4 UUID
1466
- *
1467
- * @example
1468
- * ```ts
1469
- * import { generateUUID } from '@orkestrel/database'
1470
- * import { seededRandom } from '@orkestrel/contract'
1471
- *
1472
- * generateUUID() // e.g. '9b2f7c1e-3d4a-4f6b-8e2d-5a1c0b9f8e7d'
1473
- * generateUUID(seededRandom(42)) // the same UUID on every run
1474
- * ```
1475
- */
1476
- function generateUUID(random = Math.random) {
1477
- const hex = Array.from({ length: 16 }, (_, index) => {
1478
- const byte = Math.floor(random() * 256) & 255;
1479
- if (index === 6) return byte & 15 | 64;
1480
- if (index === 8) return byte & 63 | 128;
1481
- return byte;
1482
- }).map((byte) => byte.toString(16).padStart(2, "0"));
1483
- return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10).join("")}`;
1484
- }
1485
1963
  //#endregion
1486
- //#region src/core/Cursor.ts
1964
+ //#region src/core/TransactionIterator.ts
1487
1965
  /**
1488
- * A forward row cursor for bulk in-place mutation.
1966
+ * The internal continuation boundary for one transaction-scoped async iterable.
1489
1967
  *
1490
1968
  * @remarks
1491
- * Iterates a snapshot of the table's keys captured when the cursor was opened,
1492
- * reading each row lazily through the owning table so a mutation made during
1493
- * iteration cannot corrupt the walk, and a key removed mid-iteration is simply
1494
- * skipped. `update` and `remove` act on the row at the current position.
1969
+ * Each active continuation enters the owning transaction ledger independently,
1970
+ * so an idle iterator never pins settlement. A continuation requested after
1971
+ * admission closes rejects while still attempting source cleanup exactly once.
1495
1972
  */
1496
- var Cursor = class {
1497
- #table;
1498
- #keys;
1499
- #index = -1;
1973
+ var TransactionIterator = class {
1974
+ #source;
1975
+ #scope;
1976
+ #cleaned = false;
1977
+ constructor(source, scope) {
1978
+ this.#source = source[Symbol.asyncIterator]();
1979
+ this.#scope = scope;
1980
+ }
1981
+ [Symbol.asyncIterator]() {
1982
+ return this;
1983
+ }
1984
+ next() {
1985
+ return this.#continue(() => this.#next());
1986
+ }
1987
+ return() {
1988
+ return this.#continue(() => this.#return());
1989
+ }
1990
+ throw(error) {
1991
+ return this.#continue(() => this.#throw(error));
1992
+ }
1993
+ async #next() {
1994
+ if (this.#cleaned) return {
1995
+ done: true,
1996
+ value: void 0
1997
+ };
1998
+ const result = await this.#source.next();
1999
+ if (result.done === true) this.#cleaned = true;
2000
+ return result;
2001
+ }
2002
+ async #return() {
2003
+ if (this.#cleaned || this.#source.return === void 0) {
2004
+ this.#cleaned = true;
2005
+ return {
2006
+ done: true,
2007
+ value: void 0
2008
+ };
2009
+ }
2010
+ this.#cleaned = true;
2011
+ return this.#source.return();
2012
+ }
2013
+ async #throw(error) {
2014
+ if (this.#source.throw !== void 0) {
2015
+ const result = await this.#source.throw(error);
2016
+ if (result.done === true) this.#cleaned = true;
2017
+ return result;
2018
+ }
2019
+ try {
2020
+ await this.#return();
2021
+ } catch {}
2022
+ throw error;
2023
+ }
2024
+ #continue(operation) {
2025
+ if (!this.#scope.accepting) this.#cleanup();
2026
+ return this.#scope.track(operation);
2027
+ }
2028
+ #cleanup() {
2029
+ if (this.#cleaned) return;
2030
+ this.#cleaned = true;
2031
+ try {
2032
+ (this.#source.return?.())?.catch(() => {});
2033
+ } catch {}
2034
+ }
2035
+ };
2036
+ //#endregion
2037
+ //#region src/core/TransactionScope.ts
2038
+ /**
2039
+ * The internal lifetime boundary for one database transaction callback.
2040
+ *
2041
+ * @remarks
2042
+ * Promise operations enter synchronously through {@link track}. Closing stops new
2043
+ * work while {@link drain} contains every operation already accepted, including
2044
+ * work the callback started without awaiting. {@link stream} applies the same
2045
+ * boundary to each iterator continuation without retaining an idle iterator.
2046
+ */
2047
+ var TransactionScope = class {
2048
+ #operations = /* @__PURE__ */ new Set();
2049
+ #accepting = true;
2050
+ #failed = false;
2051
+ #error;
2052
+ get accepting() {
2053
+ return this.#accepting;
2054
+ }
2055
+ check() {
2056
+ if (!this.#accepting) throw new DatabaseError("CONFLICT", "Transaction scope has settled");
2057
+ }
2058
+ track(operation) {
2059
+ try {
2060
+ this.check();
2061
+ } catch (error) {
2062
+ return Promise.reject(error);
2063
+ }
2064
+ let promise;
2065
+ try {
2066
+ promise = operation();
2067
+ } catch (error) {
2068
+ promise = Promise.reject(error);
2069
+ }
2070
+ this.#operations.add(promise);
2071
+ promise.then(() => {
2072
+ this.#operations.delete(promise);
2073
+ }, (error) => {
2074
+ this.#operations.delete(promise);
2075
+ if (!this.#failed) {
2076
+ this.#failed = true;
2077
+ this.#error = error;
2078
+ }
2079
+ });
2080
+ return promise;
2081
+ }
2082
+ stream(source) {
2083
+ return new TransactionIterator(source, this);
2084
+ }
2085
+ stop() {
2086
+ this.#accepting = false;
2087
+ }
2088
+ async drain() {
2089
+ while (this.#operations.size > 0) await Promise.allSettled(this.#operations);
2090
+ if (this.#failed) throw this.#error;
2091
+ }
2092
+ };
2093
+ //#endregion
2094
+ //#region src/core/DatabaseContext.ts
2095
+ /**
2096
+ * The internal shared owner behind every typed view of one database.
2097
+ *
2098
+ * @remarks
2099
+ * A context owns the driver, merged physical schema, lifecycle, observation,
2100
+ * migration, and single transaction admission. It is deliberately omitted from
2101
+ * the public barrel; {@link Database} is the consumer-facing typed view.
2102
+ */
2103
+ var DatabaseContext = class {
2104
+ #driver;
2105
+ #name;
2106
+ #version;
2107
+ #error;
2108
+ #emitter;
2109
+ #operations = /* @__PURE__ */ new Set();
2110
+ #schema = [];
2111
+ #transaction;
2112
+ #status = "idle";
2113
+ #ready;
2114
+ #failure;
2115
+ constructor(options) {
2116
+ this.#driver = options.driver;
2117
+ this.#name = options.name ?? "database";
2118
+ this.#version = options.version;
2119
+ this.#error = options.error;
2120
+ this.#emitter = new _orkestrel_emitter.Emitter({
2121
+ ...options.on === void 0 ? {} : { on: options.on },
2122
+ ...options.error === void 0 ? {} : { error: options.error }
2123
+ });
2124
+ }
2125
+ get driver() {
2126
+ return this.#driver;
2127
+ }
2128
+ get emitter() {
2129
+ return this.#emitter;
2130
+ }
2131
+ get error() {
2132
+ return this.#error;
2133
+ }
2134
+ get name() {
2135
+ return this.#name;
2136
+ }
2137
+ get accepting() {
2138
+ return this.#status !== "closed" && this.#transaction === void 0;
2139
+ }
2140
+ get status() {
2141
+ return this.#status;
2142
+ }
2143
+ get version() {
2144
+ return this.#version;
2145
+ }
2146
+ register(schema) {
2147
+ if (this.#status === "closed") throw new DatabaseError("CLOSED", `Database '${this.#name}' is closed`, { name: this.#name });
2148
+ if (this.#ready !== void 0 || this.#transaction !== void 0) throw new DatabaseError("CONFLICT", `Database '${this.#name}' cannot import tables after opening has started`, {
2149
+ name: this.#name,
2150
+ status: this.#status
2151
+ });
2152
+ const registered = normalizeDriverSchema(schema);
2153
+ const merged = [...this.#schema];
2154
+ for (const table of registered) {
2155
+ const existing = merged.find((candidate) => candidate.name === table.name);
2156
+ if (existing === void 0) {
2157
+ merged.push(table);
2158
+ continue;
2159
+ }
2160
+ if (!equalsValue(existing, table)) throw new DatabaseError("VALIDATION", `Table '${table.name}' conflicts with its registered schema`, { table: table.name });
2161
+ }
2162
+ this.#schema = normalizeDriverSchema(merged);
2163
+ }
2164
+ async open() {
2165
+ this.#outside();
2166
+ await this.connect();
2167
+ }
2168
+ async close() {
2169
+ this.#outside();
2170
+ if (this.#status === "closed") return;
2171
+ this.#status = "closed";
2172
+ await this.#drain();
2173
+ const ready = this.#ready;
2174
+ if (ready !== void 0) await ready.catch(() => {});
2175
+ this.#ready = void 0;
2176
+ await this.#driver.close();
2177
+ this.#emitter.emit("close");
2178
+ }
2179
+ connect() {
2180
+ return this.#connect();
2181
+ }
2182
+ track(operation) {
2183
+ try {
2184
+ this.#admit();
2185
+ } catch (error) {
2186
+ return Promise.reject(error);
2187
+ }
2188
+ let promise;
2189
+ try {
2190
+ promise = operation();
2191
+ } catch (error) {
2192
+ promise = Promise.reject(error);
2193
+ }
2194
+ this.#operations.add(promise);
2195
+ promise.then(() => {
2196
+ this.#operations.delete(promise);
2197
+ }, () => {
2198
+ this.#operations.delete(promise);
2199
+ });
2200
+ return promise;
2201
+ }
2202
+ async transaction(scope, options) {
2203
+ checkAbort(options?.signal);
2204
+ this.#admit();
2205
+ const token = {};
2206
+ this.#transaction = token;
2207
+ try {
2208
+ await this.#drain();
2209
+ await this.#connect();
2210
+ if (this.#driver.transaction !== void 0) {
2211
+ const rejection = {
2212
+ rejected: false,
2213
+ error: void 0,
2214
+ marker: {}
2215
+ };
2216
+ try {
2217
+ const value = await this.#driver.transaction(async (storage) => {
2218
+ this.#emitter.emit("transaction");
2219
+ const outcome = await scope(storage, new TransactionScope());
2220
+ if (!outcome.success) {
2221
+ rejection.rejected = true;
2222
+ rejection.error = outcome.error;
2223
+ throw rejection.marker;
2224
+ }
2225
+ return outcome.value;
2226
+ });
2227
+ this.#emitter.emit("commit");
2228
+ return value;
2229
+ } catch (error) {
2230
+ if (rejection.rejected) {
2231
+ if (Object.is(error, rejection.marker)) {
2232
+ this.#emitter.emit("rollback", rejection.error);
2233
+ throw rejection.error;
2234
+ }
2235
+ throw this.#rollbackError(rejection.error, error);
2236
+ }
2237
+ throw error;
2238
+ }
2239
+ }
2240
+ const rollback = await this.#driver.snapshot();
2241
+ this.#emitter.emit("transaction");
2242
+ const outcome = await scope(this.#driver, new TransactionScope());
2243
+ if (outcome.success) {
2244
+ this.#emitter.emit("commit");
2245
+ return outcome.value;
2246
+ }
2247
+ try {
2248
+ await rollback();
2249
+ } catch (cause) {
2250
+ throw this.#rollbackError(outcome.error, cause);
2251
+ }
2252
+ this.#emitter.emit("rollback", outcome.error);
2253
+ throw outcome.error;
2254
+ } finally {
2255
+ if (this.#transaction === token) this.#transaction = void 0;
2256
+ }
2257
+ }
2258
+ async migrate(deployed, options) {
2259
+ checkAbort(options?.signal);
2260
+ this.#outside();
2261
+ if (this.#ready !== void 0 || this.#status !== "idle") throw new DatabaseError("CONFLICT", `Database '${this.#name}' cannot apply an explicit deployed schema after opening`, {
2262
+ name: this.#name,
2263
+ status: this.#status
2264
+ });
2265
+ if (this.#driver.migrate === void 0) throw new DatabaseError("MIGRATION", `Database '${this.#name}' driver does not support migration`, { name: this.#name });
2266
+ const plan = planMigration(deployed, this.#schema);
2267
+ const readiness = this.#transition(deployed, plan).catch((error) => {
2268
+ if (this.#ready === readiness) this.#ready = void 0;
2269
+ this.#failure = { error };
2270
+ throw error;
2271
+ });
2272
+ this.#failure = void 0;
2273
+ this.#ready = readiness;
2274
+ await readiness;
2275
+ return plan;
2276
+ }
2277
+ #admit() {
2278
+ if (this.#status === "closed") throw new DatabaseError("CLOSED", `Database '${this.#name}' is closed`, { name: this.#name });
2279
+ if (this.#transaction !== void 0) throw new DatabaseError("CONFLICT", `Database '${this.#name}' has an active transaction`, { name: this.#name });
2280
+ if (this.#failure !== void 0) throw this.#failure.error;
2281
+ }
2282
+ #outside() {
2283
+ if (this.#transaction !== void 0) throw new DatabaseError("CONFLICT", `Database '${this.#name}' has an active transaction`, { name: this.#name });
2284
+ }
2285
+ #connect() {
2286
+ if (this.#ready !== void 0) return this.#ready;
2287
+ if (this.#failure !== void 0) throw this.#failure.error;
2288
+ if (this.#status === "closed") throw new DatabaseError("CLOSED", `Database '${this.#name}' is closed`, { name: this.#name });
2289
+ const readiness = this.#driver.open(this.#schema).then(async () => {
2290
+ if (this.#status === "idle") {
2291
+ this.#status = "open";
2292
+ this.#emitter.emit("open");
2293
+ }
2294
+ await this.#reconcile();
2295
+ }).catch((error) => {
2296
+ if (this.#ready === readiness) this.#ready = void 0;
2297
+ throw error;
2298
+ });
2299
+ this.#ready = readiness;
2300
+ return readiness;
2301
+ }
2302
+ async #drain() {
2303
+ if (this.#operations.size === 0) return;
2304
+ await Promise.allSettled(this.#operations);
2305
+ }
2306
+ async #reconcile() {
2307
+ if (this.#version === void 0 || this.#driver.metadata === void 0 || this.#driver.stamp === void 0) return;
2308
+ const metadata = await this.#driver.metadata();
2309
+ if (metadata === void 0) {
2310
+ await this.#stamp();
2311
+ return;
2312
+ }
2313
+ if (metadata.version > this.#version) throw new DatabaseError("MIGRATION", `Database '${this.#name}' store version ${metadata.version} is newer than declared version ${this.#version}`, {
2314
+ name: this.#name,
2315
+ stored: metadata.version,
2316
+ declared: this.#version
2317
+ });
2318
+ if (metadata.version === this.#version) {
2319
+ if (!equalsValue(normalizeDriverSchema(metadata.schema), this.#schema)) throw new DatabaseError("MIGRATION", `Database '${this.#name}' stored schema differs at version ${this.#version}`, {
2320
+ name: this.#name,
2321
+ version: this.#version
2322
+ });
2323
+ return;
2324
+ }
2325
+ const plan = planMigration(metadata.schema, this.#schema, metadata.version, this.#version);
2326
+ if (plan.steps.length > 0 && this.#driver.migrate === void 0) throw new DatabaseError("MIGRATION", `Database '${this.#name}' driver does not support migration`, {
2327
+ name: this.#name,
2328
+ stored: metadata.version,
2329
+ declared: this.#version
2330
+ });
2331
+ await this.#apply(plan);
2332
+ }
2333
+ async #apply(plan) {
2334
+ if (this.#driver.transaction !== void 0) {
2335
+ await this.#driver.transaction(async (storage) => {
2336
+ if (plan.steps.length > 0 && storage.migrate === void 0) throw new DatabaseError("MIGRATION", `Database '${this.#name}' transaction does not support migration`, { name: this.#name });
2337
+ if (storage.migrate === void 0) await this.#stamp(storage);
2338
+ else await storage.migrate(this.#migration(plan));
2339
+ });
2340
+ this.#emitter.emit("migrate", plan);
2341
+ return;
2342
+ }
2343
+ if (this.#driver.migrate === void 0) await this.#stamp();
2344
+ else await this.#driver.migrate(this.#migration(plan));
2345
+ this.#emitter.emit("migrate", plan);
2346
+ }
2347
+ async #transition(deployed, plan) {
2348
+ await this.#driver.open(deployed);
2349
+ await this.#apply(plan);
2350
+ if (this.#status === "idle") {
2351
+ this.#status = "open";
2352
+ this.#emitter.emit("open");
2353
+ }
2354
+ }
2355
+ async #stamp(storage) {
2356
+ const target = storage ?? this.#driver;
2357
+ if (this.#version === void 0 || target.stamp === void 0) return;
2358
+ const metadata = {
2359
+ version: this.#version,
2360
+ schema: this.#schema
2361
+ };
2362
+ await target.stamp(metadata);
2363
+ }
2364
+ #migration(plan) {
2365
+ if (this.#version === void 0) return { plan };
2366
+ return {
2367
+ plan,
2368
+ metadata: {
2369
+ version: this.#version,
2370
+ schema: this.#schema
2371
+ }
2372
+ };
2373
+ }
2374
+ #rollbackError(transaction, cause) {
2375
+ return new DatabaseError("DRIVER", `Database '${this.#name}' rollback failed`, {
2376
+ cause,
2377
+ transaction
2378
+ });
2379
+ }
2380
+ };
2381
+ //#endregion
2382
+ //#region src/core/Cursor.ts
2383
+ /**
2384
+ * A forward row cursor for bulk in-place mutation.
2385
+ *
2386
+ * @remarks
2387
+ * Iterates a snapshot of the table's keys captured when the cursor was opened,
2388
+ * reading each row lazily through the owning table — so a mutation made during
2389
+ * iteration cannot corrupt the walk, and a key removed mid-iteration is simply
2390
+ * skipped. `update` and `remove` act on the row at the current position.
2391
+ */
2392
+ var Cursor = class {
2393
+ #keys;
2394
+ #read;
2395
+ #update;
2396
+ #remove;
2397
+ #track;
2398
+ #tail = Promise.resolve();
2399
+ #index = -1;
1500
2400
  #value;
1501
2401
  #closed = false;
1502
- constructor(table, keys) {
1503
- this.#table = table;
2402
+ constructor(keys, read, update, remove, track) {
1504
2403
  this.#keys = keys;
2404
+ this.#read = read;
2405
+ this.#update = update;
2406
+ this.#remove = remove;
2407
+ this.#track = track;
1505
2408
  }
1506
2409
  get value() {
1507
2410
  return this.#value;
@@ -1512,16 +2415,31 @@ var Cursor = class {
1512
2415
  get done() {
1513
2416
  return this.#closed || this.#index >= this.#keys.length;
1514
2417
  }
1515
- async next() {
2418
+ next() {
2419
+ return this.#track(() => this.#queue(() => this.#advance()));
2420
+ }
2421
+ update(changes) {
2422
+ return this.#track(() => this.#queue(() => this.#revise(changes)));
2423
+ }
2424
+ remove() {
2425
+ return this.#track(() => this.#queue(() => this.#delete()));
2426
+ }
2427
+ close() {
2428
+ this.#closed = true;
2429
+ this.#value = void 0;
2430
+ }
2431
+ async #advance() {
1516
2432
  if (this.#closed) return;
1517
2433
  this.#index += 1;
1518
2434
  while (this.#index < this.#keys.length) {
2435
+ if (this.#closed) return;
1519
2436
  const key = this.#keys[this.#index];
1520
2437
  if (key === void 0) {
1521
2438
  this.#index += 1;
1522
2439
  continue;
1523
2440
  }
1524
- const row = await this.#table.get(key);
2441
+ const row = await this.#read(key);
2442
+ if (this.#closed) return;
1525
2443
  if (row !== void 0) {
1526
2444
  this.#value = row;
1527
2445
  return;
@@ -1530,97 +2448,103 @@ var Cursor = class {
1530
2448
  }
1531
2449
  this.#value = void 0;
1532
2450
  }
1533
- async update(changes) {
2451
+ async #revise(changes) {
1534
2452
  if (this.#closed || this.#value === void 0) return;
1535
2453
  const key = this.#keys[this.#index];
1536
2454
  if (key === void 0) return;
1537
- await this.#table.update(key, changes);
1538
- this.#value = await this.#table.get(key);
2455
+ await this.#update(key, changes);
2456
+ if (this.#closed) return;
2457
+ const row = await this.#read(key);
2458
+ if (this.#closed) return;
2459
+ this.#value = row;
1539
2460
  }
1540
- async remove() {
2461
+ async #delete() {
1541
2462
  if (this.#closed || this.#value === void 0) return;
1542
2463
  const key = this.#keys[this.#index];
1543
2464
  if (key === void 0) return;
1544
- await this.#table.remove(key);
2465
+ await this.#remove(key);
2466
+ if (this.#closed) return;
1545
2467
  this.#value = void 0;
1546
2468
  }
1547
- close() {
1548
- this.#closed = true;
1549
- this.#value = void 0;
2469
+ #queue(operation) {
2470
+ const result = this.#tail.then(operation);
2471
+ this.#tail = result.then(() => void 0, () => void 0);
2472
+ return result;
1550
2473
  }
1551
2474
  };
1552
2475
  //#endregion
1553
- //#region src/core/Clause.ts
2476
+ //#region src/core/DatabaseIterator.ts
1554
2477
  /**
1555
- * A pending condition opened by a query's `where` / `and` / `or`.
2478
+ * The internal continuation admission boundary for a root database stream.
1556
2479
  *
1557
2480
  * @remarks
1558
- * Holds the column, the connector that will join this condition to the ones
1559
- * before it, and a recorder the owning query supplies. Each operator builds the
1560
- * {@link Condition}, hands it to the recorder, and returns the query — so the
1561
- * fluent chain flows straight back into the builder without exposing a mutator.
2481
+ * Each continuation enters the shared root operation ledger independently, so
2482
+ * an idle iterator never delays a transaction or close. A continuation rejected
2483
+ * after transaction or close admission closes attempts source cleanup exactly
2484
+ * once and leaves the iterator terminal.
1562
2485
  */
1563
- var Clause = class {
1564
- #record;
1565
- #column;
1566
- #connector;
1567
- constructor(record, column, connector) {
1568
- this.#record = record;
1569
- this.#column = column;
1570
- this.#connector = connector;
1571
- }
1572
- equals(value) {
1573
- return this.#apply("equals", [value]);
1574
- }
1575
- not(value) {
1576
- return this.#apply("not", [value]);
1577
- }
1578
- above(value) {
1579
- return this.#apply("above", [value]);
1580
- }
1581
- below(value) {
1582
- return this.#apply("below", [value]);
1583
- }
1584
- from(value) {
1585
- return this.#apply("from", [value]);
1586
- }
1587
- to(value) {
1588
- return this.#apply("to", [value]);
1589
- }
1590
- between(lower, upper) {
1591
- return this.#apply("between", [lower, upper]);
1592
- }
1593
- like(pattern) {
1594
- return this.#apply("like", [pattern]);
1595
- }
1596
- glob(pattern) {
1597
- return this.#apply("glob", [pattern]);
2486
+ var DatabaseIterator = class {
2487
+ #source;
2488
+ #context;
2489
+ #cleaned = false;
2490
+ constructor(source, context) {
2491
+ this.#source = source[Symbol.asyncIterator]();
2492
+ this.#context = context;
2493
+ }
2494
+ [Symbol.asyncIterator]() {
2495
+ return this;
1598
2496
  }
1599
- starts(prefix) {
1600
- return this.#apply("starts", [prefix]);
2497
+ next() {
2498
+ return this.#continue(() => this.#next());
1601
2499
  }
1602
- ends(suffix) {
1603
- return this.#apply("ends", [suffix]);
2500
+ return() {
2501
+ return this.#continue(() => this.#return());
1604
2502
  }
1605
- any(values) {
1606
- return this.#apply("any", values);
2503
+ throw(error) {
2504
+ return this.#continue(() => this.#throw(error));
1607
2505
  }
1608
- none(values) {
1609
- return this.#apply("none", values);
2506
+ async #next() {
2507
+ if (this.#cleaned) return {
2508
+ done: true,
2509
+ value: void 0
2510
+ };
2511
+ await this.#context.connect();
2512
+ const result = await this.#source.next();
2513
+ if (result.done === true) this.#cleaned = true;
2514
+ return result;
1610
2515
  }
1611
- absent() {
1612
- return this.#apply("absent", []);
2516
+ async #return() {
2517
+ if (this.#cleaned || this.#source.return === void 0) {
2518
+ this.#cleaned = true;
2519
+ return {
2520
+ done: true,
2521
+ value: void 0
2522
+ };
2523
+ }
2524
+ this.#cleaned = true;
2525
+ return this.#source.return();
2526
+ }
2527
+ async #throw(error) {
2528
+ if (this.#source.throw !== void 0) {
2529
+ const result = await this.#source.throw(error);
2530
+ if (result.done === true) this.#cleaned = true;
2531
+ return result;
2532
+ }
2533
+ try {
2534
+ await this.#return();
2535
+ } catch {}
2536
+ throw error;
1613
2537
  }
1614
- present() {
1615
- return this.#apply("present", []);
2538
+ #continue(operation) {
2539
+ if (!this.#context.accepting) this.#cleanup();
2540
+ return this.#context.track(operation);
1616
2541
  }
1617
- #apply(operator, values) {
1618
- return this.#record({
1619
- column: this.#column,
1620
- operator,
1621
- values,
1622
- connector: this.#connector
1623
- });
2542
+ #cleanup() {
2543
+ if (this.#cleaned) return;
2544
+ this.#cleaned = true;
2545
+ try {
2546
+ (this.#source.return?.())?.catch(() => {});
2547
+ } catch {}
1624
2548
  }
1625
2549
  };
1626
2550
  //#endregion
@@ -1629,12 +2553,9 @@ var Clause = class {
1629
2553
  * A fluent query builder bound to one table.
1630
2554
  *
1631
2555
  * @remarks
1632
- * Accumulates conditions, ordering, JS filters, and a page; each builder method
1633
- * mutates and returns the same instance, so a chain reads as one statement. The
1634
- * portable parts (conditions, order, page) compile into a {@link Criteria} the
1635
- * table resolves; a `filter` predicate is applied in memory after the read and
1636
- * before paging, so it composes with the rest without a backend ever seeing a
1637
- * JS callback.
2556
+ * Accumulates typed conditions, ordering, JS filters, and a page. Each builder
2557
+ * method mutates and returns the same instance. Portable inputs flow to the
2558
+ * table, while predicates remain an in-memory refinement.
1638
2559
  */
1639
2560
  var Query = class {
1640
2561
  #table;
@@ -1646,42 +2567,29 @@ var Query = class {
1646
2567
  constructor(table) {
1647
2568
  this.#table = table;
1648
2569
  }
1649
- where(column) {
1650
- return this.#clause(column, "and");
1651
- }
1652
- and(column) {
1653
- return this.#clause(column, "and");
1654
- }
1655
- or(column) {
1656
- return this.#clause(column, "or");
1657
- }
1658
- filter(predicate) {
1659
- this.#filters.push(predicate);
2570
+ condition(input) {
2571
+ this.#conditions.push(input);
1660
2572
  return this;
1661
2573
  }
1662
- ascending(column) {
1663
- this.#orders.push({
1664
- column,
1665
- direction: "ascending"
1666
- });
2574
+ order(input) {
2575
+ this.#orders.push(input);
1667
2576
  return this;
1668
2577
  }
1669
- descending(column) {
1670
- this.#orders.push({
1671
- column,
1672
- direction: "descending"
1673
- });
2578
+ filter(predicate) {
2579
+ this.#filters.push(predicate);
1674
2580
  return this;
1675
2581
  }
1676
2582
  limit(count) {
2583
+ validatePage({ limit: count });
1677
2584
  this.#limit = count;
1678
2585
  return this;
1679
2586
  }
1680
2587
  offset(count) {
2588
+ validatePage({ offset: count });
1681
2589
  this.#offset = count;
1682
2590
  return this;
1683
2591
  }
1684
- async all() {
2592
+ async collect() {
1685
2593
  if (this.#filters.length === 0) return this.#table.records({
1686
2594
  conditions: this.#conditions,
1687
2595
  order: this.#orders,
@@ -1694,8 +2602,8 @@ var Query = class {
1694
2602
  });
1695
2603
  return this.#page(this.#filtered(fetched));
1696
2604
  }
1697
- async first() {
1698
- return (await this.all())[0];
2605
+ async find() {
2606
+ return (await this.collect())[0];
1699
2607
  }
1700
2608
  async count() {
1701
2609
  if (this.#filters.length === 0) return this.#table.count({ conditions: this.#conditions });
@@ -1703,17 +2611,10 @@ var Query = class {
1703
2611
  return this.#filtered(fetched).length;
1704
2612
  }
1705
2613
  /**
1706
- * Lazy per-row evaluation of this query's conditions / filters / offset /
1707
- * limit.
1708
- *
1709
- * @remarks
1710
- * `order` and its comparators are IGNORED (streaming yields unsorted, as rows
1711
- * are evaluated one at a time). Same abort semantics as
1712
- * `TableInterface.scan`: the signal (if any) is checked before each yield,
1713
- * and breaking out early closes the underlying source.
2614
+ * Lazily evaluate conditions, filters, offset, and limit.
1714
2615
  *
1715
- * @param options - `signal` to cancel the iteration; checked before each yield
1716
- * @returns An async iterable of matching rows
2616
+ * @param options - Optional abort options
2617
+ * @returns Matching rows in storage order
1717
2618
  */
1718
2619
  async *stream(options) {
1719
2620
  if (this.#filters.length === 0) {
@@ -1748,24 +2649,6 @@ var Query = class {
1748
2649
  if (this.#filters.length === 0) return this.#table.aggregate(operation, column, { conditions: this.#conditions });
1749
2650
  return this.#table.records({ conditions: this.#conditions }).then((fetched) => computeAggregate(this.#filtered(fetched), operation, column));
1750
2651
  }
1751
- sum(column) {
1752
- return this.aggregate("sum", column);
1753
- }
1754
- average(column) {
1755
- return this.aggregate("average", column);
1756
- }
1757
- minimum(column) {
1758
- return this.aggregate("minimum", column);
1759
- }
1760
- maximum(column) {
1761
- return this.aggregate("maximum", column);
1762
- }
1763
- #clause(column, connector) {
1764
- return new Clause((condition) => {
1765
- this.#conditions.push(condition);
1766
- return this;
1767
- }, column, connector);
1768
- }
1769
2652
  #filtered(rows) {
1770
2653
  let result = rows;
1771
2654
  for (const predicate of this.#filters) result = result.filter(predicate);
@@ -1808,8 +2691,10 @@ var Table = class {
1808
2691
  #contract;
1809
2692
  #guard;
1810
2693
  #generate;
2694
+ #context;
2695
+ #scope;
1811
2696
  #emitter;
1812
- constructor(ready, driver, name, key, contract, generate, on, error) {
2697
+ constructor(ready, driver, name, key, contract, generate, error, context, scope) {
1813
2698
  this.#ready = ready;
1814
2699
  this.#driver = driver;
1815
2700
  this.#name = name;
@@ -1817,10 +2702,9 @@ var Table = class {
1817
2702
  this.#contract = contract;
1818
2703
  this.#guard = contract.is;
1819
2704
  this.#generate = generate;
1820
- this.#emitter = new _orkestrel_emitter.Emitter({
1821
- ...on !== void 0 ? { on } : {},
1822
- ...error !== void 0 ? { error } : {}
1823
- });
2705
+ this.#context = context;
2706
+ this.#scope = scope;
2707
+ this.#emitter = new _orkestrel_emitter.Emitter({ ...error !== void 0 ? { error } : {} });
1824
2708
  }
1825
2709
  get emitter() {
1826
2710
  return this.#emitter;
@@ -1834,58 +2718,77 @@ var Table = class {
1834
2718
  get contract() {
1835
2719
  return this.#contract;
1836
2720
  }
1837
- async get(keys) {
1838
- await this.#ready();
1839
- if ((0, _orkestrel_contract.isArray)(keys)) return this.#each(keys, (key) => this.#read(key));
1840
- return this.#read(keys);
2721
+ get(keys) {
2722
+ return this.#track(async () => {
2723
+ await this.#ready();
2724
+ if ((0, _orkestrel_contract.isArray)(keys)) return this.#each(keys, (key) => this.#read(key));
2725
+ return this.#read(keys);
2726
+ });
1841
2727
  }
1842
- async resolve(keys) {
1843
- await this.#ready();
1844
- if ((0, _orkestrel_contract.isArray)(keys)) return this.#each(keys, (key) => this.#resolveOne(key));
1845
- return this.#resolveOne(keys);
2728
+ resolve(keys) {
2729
+ return this.#track(async () => {
2730
+ await this.#ready();
2731
+ if ((0, _orkestrel_contract.isArray)(keys)) return this.#each(keys, (key) => this.#resolveOne(key));
2732
+ return this.#resolveOne(keys);
2733
+ });
1846
2734
  }
1847
- async has(keys) {
1848
- await this.#ready();
1849
- if ((0, _orkestrel_contract.isArray)(keys)) return this.#each(keys, async (key) => await this.#read(key) !== void 0);
1850
- return await this.#read(keys) !== void 0;
2735
+ has(keys) {
2736
+ return this.#track(async () => {
2737
+ await this.#ready();
2738
+ if ((0, _orkestrel_contract.isArray)(keys)) return this.#each(keys, async (key) => await this.#read(key) !== void 0);
2739
+ return await this.#read(keys) !== void 0;
2740
+ });
1851
2741
  }
1852
- async keys() {
1853
- await this.#ready();
1854
- return this.#driver.keys(this.#name);
2742
+ keys() {
2743
+ return this.#track(async () => {
2744
+ await this.#ready();
2745
+ return this.#driver.keys(this.#name);
2746
+ });
1855
2747
  }
1856
- async records(criteria, options) {
1857
- checkAbort(options?.signal);
1858
- await this.#ready();
1859
- const source = await this.#driver.records?.(this.#name, criteria ?? {}) ?? applyCriteria(await this.#collect(), criteria);
1860
- const rows = [];
1861
- for (const row of source) if (this.#guard(row)) rows.push(row);
1862
- return rows;
2748
+ async records(input, options) {
2749
+ validatePage(input);
2750
+ return this.#track(async () => {
2751
+ checkAbort(options?.signal);
2752
+ await this.#ready();
2753
+ const candidate = {
2754
+ ...input?.conditions === void 0 ? {} : { conditions: input.conditions },
2755
+ ...input?.order === void 0 ? {} : { order: input.order }
2756
+ };
2757
+ const source = await this.#driver.records?.(this.#name, candidate) ?? applyQuery(await this.#collect(), candidate);
2758
+ const rows = [];
2759
+ for (const row of source) if (this.#guard(row)) rows.push(row);
2760
+ const offset = input?.offset ?? 0;
2761
+ const limit = input?.limit;
2762
+ return rows.slice(offset, limit === void 0 ? void 0 : offset + limit);
2763
+ });
1863
2764
  }
1864
2765
  /**
1865
- * Count rows matching `criteria`'s conditions.
2766
+ * Count contract-valid rows matching `input`'s conditions.
1866
2767
  *
1867
2768
  * @remarks
1868
- * Unlike {@link records}, which narrows every row through the table's
1869
- * contract guard before returning it, `count` operates on STORED rows
1870
- * WITHOUT that guard (both the native `driver.count` hook and the
1871
- * `filterRows`-over-`#collect()` fallback count raw storage) — so it can
1872
- * exceed `(await records(criteria)).length` when storage holds rows that
1873
- * no longer conform to the table's contract (legacy or migrated data).
2769
+ * Paging is ignored. Candidate rows use the driver's native `records` hook
2770
+ * when present, then the table contract guard determines the count so legacy
2771
+ * invalid rows cannot make `count()` disagree with `records()`.
1874
2772
  *
1875
- * @param criteria - Optional conditions to filter by (paging is ignored)
2773
+ * @param input - Optional conditions to filter by (paging is ignored)
1876
2774
  * @param options - `{ signal }` to abort
1877
- * @returns The count of matching stored rows
2775
+ * @returns The count of matching contract-valid rows
1878
2776
  */
1879
- async count(criteria, options) {
1880
- checkAbort(options?.signal);
1881
- await this.#ready();
1882
- const conditions = criteria?.conditions;
1883
- const native = await this.#driver.count?.(this.#name, conditions ? { conditions } : {});
1884
- if (native !== void 0) return native;
1885
- return filterRows(await this.#collect(), criteria?.conditions ?? []).length;
2777
+ async count(input, options) {
2778
+ validatePage(input);
2779
+ return this.#track(async () => {
2780
+ checkAbort(options?.signal);
2781
+ await this.#ready();
2782
+ const conditions = input?.conditions;
2783
+ const candidate = conditions === void 0 ? {} : { conditions };
2784
+ const rows = await this.#driver.records?.(this.#name, candidate) ?? filterRows(await this.#collect(), conditions ?? []);
2785
+ let count = 0;
2786
+ for (const row of rows) if (this.#guard(row)) count += 1;
2787
+ return count;
2788
+ });
1886
2789
  }
1887
2790
  /**
1888
- * Compute an aggregate over `column` across rows matching `criteria`'s
2791
+ * Compute an aggregate over `column` across rows matching `input`'s
1889
2792
  * conditions.
1890
2793
  *
1891
2794
  * @remarks
@@ -1897,115 +2800,138 @@ var Table = class {
1897
2800
  *
1898
2801
  * @param operation - The aggregate to compute
1899
2802
  * @param column - The column to aggregate
1900
- * @param criteria - Optional conditions to filter by (paging is ignored)
2803
+ * @param input - Optional conditions to filter by (paging is ignored)
1901
2804
  * @param options - `{ signal }` to abort
1902
2805
  * @returns The aggregate value, or `undefined` when undefined for the inputs
1903
2806
  */
1904
- async aggregate(operation, column, criteria, options) {
1905
- checkAbort(options?.signal);
1906
- await this.#ready();
1907
- const conditions = criteria?.conditions;
1908
- const filter = conditions ? { conditions } : {};
1909
- const native = this.#driver.aggregate?.(this.#name, operation, column, filter);
1910
- if (native !== void 0) return native;
1911
- return computeAggregate(await this.#driver.records?.(this.#name, filter) ?? filterRows(await this.#collect(), criteria?.conditions ?? []), operation, column);
2807
+ async aggregate(operation, column, input, options) {
2808
+ validatePage(input);
2809
+ return this.#track(async () => {
2810
+ checkAbort(options?.signal);
2811
+ await this.#ready();
2812
+ const conditions = input?.conditions;
2813
+ const filter = conditions ? { conditions } : {};
2814
+ const native = this.#driver.aggregate?.(this.#name, operation, column, filter);
2815
+ if (native !== void 0) return native;
2816
+ return computeAggregate(await this.#driver.records?.(this.#name, filter) ?? filterRows(await this.#collect(), input?.conditions ?? []), operation, column);
2817
+ });
1912
2818
  }
1913
2819
  /**
1914
- * Stream the table's rows matching `criteria`, applying offset/limit paging.
2820
+ * Stream the table's rows matching `input`, applying offset/limit paging.
1915
2821
  *
1916
2822
  * @remarks
1917
- * `criteria.limit` counts rows that pass BOTH the criteria conditions AND the
1918
- * table's contract guard (a stored row that fails the guard is skipped and
1919
- * does not count toward `limit`) this can differ from {@link records}'s
1920
- * `limit`, which a driver's optional native `records` hook applies BEFORE
1921
- * the contract guard runs, when storage holds rows that no longer conform
1922
- * to the table's contract.
2823
+ * `input.limit` counts rows that pass both the input conditions and the
2824
+ * table's contract guard. Native streams receive only the conditions; this
2825
+ * table rechecks them, narrows each candidate, and applies offset/limit last,
2826
+ * matching {@link records} when storage contains legacy invalid rows.
1923
2827
  *
1924
- * @param criteria - Optional conditions plus offset/limit paging
2828
+ * @param input - Optional conditions plus offset/limit paging
1925
2829
  * @param options - `{ signal }` to abort mid-stream
1926
2830
  * @returns An async iterable of matching, guard-conforming rows
1927
2831
  */
1928
- async *scan(criteria, options) {
1929
- checkAbort(options?.signal);
1930
- await this.#ready();
1931
- if (this.#driver.stream !== void 0) {
1932
- for await (const row of this.#driver.stream(this.#name, criteria ?? {})) {
1933
- checkAbort(options?.signal);
1934
- const narrowed = this.#cast(row);
1935
- if (narrowed !== void 0) yield narrowed;
1936
- }
1937
- return;
1938
- }
1939
- const conditions = criteria?.conditions;
1940
- const offset = criteria?.offset ?? 0;
1941
- const limit = criteria?.limit;
1942
- let matched = 0;
1943
- let yielded = 0;
1944
- for await (const row of this.#driver.scan(this.#name)) {
1945
- checkAbort(options?.signal);
1946
- if (limit !== void 0 && yielded >= limit) break;
1947
- if (conditions !== void 0 && conditions.length > 0 && !matchesCriteria(row, conditions)) continue;
1948
- if (matched < offset) {
1949
- matched += 1;
1950
- continue;
1951
- }
1952
- matched += 1;
1953
- const narrowed = this.#cast(row);
1954
- if (narrowed !== void 0) {
1955
- yielded += 1;
1956
- yield narrowed;
1957
- }
1958
- }
1959
- }
1960
- async set(rows, options) {
1961
- checkAbort(options?.signal);
1962
- await this.#ready();
1963
- if ((0, _orkestrel_contract.isArray)(rows)) return this.#each(rows, (row) => this.#put(row, false), options?.signal);
1964
- return this.#put(rows, false);
2832
+ scan(input, options) {
2833
+ validatePage(input);
2834
+ const source = this.#scan(input, options);
2835
+ if (this.#context !== void 0) return new DatabaseIterator(source, this.#context);
2836
+ return this.#scope === void 0 ? source : this.#scope.stream(source);
2837
+ }
2838
+ set(rows, options) {
2839
+ return this.#track(async () => {
2840
+ await this.#wait(options?.signal);
2841
+ if ((0, _orkestrel_contract.isArray)(rows)) return this.#each(rows, (row) => this.#put(row, false, options), options?.signal);
2842
+ return this.#put(rows, false, options);
2843
+ });
1965
2844
  }
1966
- async add(rows, options) {
1967
- checkAbort(options?.signal);
1968
- await this.#ready();
1969
- if ((0, _orkestrel_contract.isArray)(rows)) return this.#each(rows, (row) => this.#put(row, true), options?.signal);
1970
- return this.#put(rows, true);
2845
+ add(rows, options) {
2846
+ return this.#track(async () => {
2847
+ await this.#wait(options?.signal);
2848
+ if ((0, _orkestrel_contract.isArray)(rows)) return this.#each(rows, (row) => this.#put(row, true, options), options?.signal);
2849
+ return this.#put(rows, true, options);
2850
+ });
1971
2851
  }
1972
- async update(keys, changes, options) {
1973
- checkAbort(options?.signal);
1974
- await this.#ready();
1975
- if ((0, _orkestrel_contract.isArray)(keys)) return this.#each(keys, (key) => this.#updateOne(key, changes), options?.signal);
1976
- return this.#updateOne(keys, changes);
2852
+ update(keys, changes, options) {
2853
+ return this.#track(async () => {
2854
+ await this.#wait(options?.signal);
2855
+ if ((0, _orkestrel_contract.isArray)(keys)) return this.#each(keys, (key) => this.#updateOne(key, changes, options), options?.signal);
2856
+ return this.#updateOne(keys, changes, options);
2857
+ });
1977
2858
  }
1978
- async remove(keys, options) {
1979
- checkAbort(options?.signal);
1980
- await this.#ready();
1981
- if ((0, _orkestrel_contract.isArray)(keys)) return this.#each(keys, (key) => this.#delete(key), options?.signal);
1982
- return this.#delete(keys);
2859
+ remove(keys, options) {
2860
+ return this.#track(async () => {
2861
+ await this.#wait(options?.signal);
2862
+ if ((0, _orkestrel_contract.isArray)(keys)) return this.#each(keys, (key) => this.#delete(key, options), options?.signal);
2863
+ return this.#delete(keys, options);
2864
+ });
1983
2865
  }
1984
- async clear() {
1985
- await this.#ready();
1986
- await this.#driver.clear(this.#name);
1987
- this.#emitter.emit("clear");
2866
+ clear() {
2867
+ return this.#track(async () => {
2868
+ await this.#ready();
2869
+ await this.#driver.clear(this.#name);
2870
+ this.#emitter.emit("clear");
2871
+ });
1988
2872
  }
1989
2873
  query() {
1990
2874
  return new Query(this);
1991
2875
  }
1992
- async cursor() {
2876
+ cursor() {
2877
+ return this.#track(async () => {
2878
+ await this.#ready();
2879
+ let initializing = true;
2880
+ const cursor = new Cursor(await this.#driver.keys(this.#name), (key) => this.#readCursor(key), (key, changes) => this.#updateCursor(key, changes), (key) => this.#deleteCursor(key), (operation) => initializing ? operation() : this.#track(operation));
2881
+ await cursor.next();
2882
+ initializing = false;
2883
+ return cursor;
2884
+ });
2885
+ }
2886
+ async *#scan(input, options) {
2887
+ checkAbort(options?.signal);
1993
2888
  await this.#ready();
1994
- const cursor = new Cursor(this, await this.#driver.keys(this.#name));
1995
- await cursor.next();
1996
- return cursor;
2889
+ const conditions = input?.conditions;
2890
+ const offset = input?.offset ?? 0;
2891
+ const limit = input?.limit;
2892
+ let matched = 0;
2893
+ let yielded = 0;
2894
+ const iterator = (this.#driver.stream === void 0 ? this.#driver.scan(this.#name) : this.#driver.stream(this.#name, conditions === void 0 ? {} : { conditions }))[Symbol.asyncIterator]();
2895
+ try {
2896
+ while (true) {
2897
+ await this.#ready();
2898
+ checkAbort(options?.signal);
2899
+ if (limit !== void 0 && yielded >= limit) return;
2900
+ const step = await iterator.next();
2901
+ await this.#ready();
2902
+ checkAbort(options?.signal);
2903
+ if (step.done === true) return;
2904
+ const row = step.value;
2905
+ if (conditions !== void 0 && conditions.length > 0 && !matchesQuery(row, conditions)) continue;
2906
+ const narrowed = this.#cast(row);
2907
+ if (narrowed === void 0) continue;
2908
+ if (matched < offset) {
2909
+ matched += 1;
2910
+ continue;
2911
+ }
2912
+ matched += 1;
2913
+ yielded += 1;
2914
+ yield narrowed;
2915
+ }
2916
+ } finally {
2917
+ await iterator.return?.();
2918
+ }
1997
2919
  }
1998
- async #each(items, operation, signal) {
2920
+ async #each(elements, operation, signal) {
1999
2921
  const results = [];
2000
- for (const item of items) {
2922
+ for (const element of elements) {
2001
2923
  checkAbort(signal);
2002
- results.push(await operation(item));
2924
+ results.push(await operation(element));
2003
2925
  }
2004
2926
  return results;
2005
2927
  }
2006
2928
  async #read(key) {
2007
2929
  return this.#cast(await this.#driver.read(this.#name, key));
2008
2930
  }
2931
+ async #readCursor(key) {
2932
+ await this.#ready();
2933
+ return this.#read(key);
2934
+ }
2009
2935
  async #resolveOne(key) {
2010
2936
  const row = await this.#read(key);
2011
2937
  if (row === void 0) throw new DatabaseError("NOT_FOUND", `No row '${key}' in table '${this.#name}'`, {
@@ -2014,29 +2940,79 @@ var Table = class {
2014
2940
  });
2015
2941
  return row;
2016
2942
  }
2017
- async #put(row, exclusive) {
2943
+ async #put(row, insert, options) {
2018
2944
  const validated = this.#validate(this.#prepare(row));
2019
2945
  const key = this.#resolveKey(validated);
2020
- if (exclusive && await this.#driver.read(this.#name, key) !== void 0) throw new DatabaseError("CONFLICT", `Row '${key}' already exists in table '${this.#name}'`, {
2021
- table: this.#name,
2022
- key
2023
- });
2024
- await this.#driver.write(this.#name, key, validated);
2946
+ if (insert) await this.#driver.insert(this.#name, key, validated, options);
2947
+ else await this.#driver.write(this.#name, key, validated, options);
2025
2948
  this.#emitter.emit("write", key);
2026
2949
  return key;
2027
2950
  }
2028
- async #updateOne(key, changes) {
2951
+ async #updateOne(key, changes, options) {
2029
2952
  const existing = await this.#driver.read(this.#name, key);
2030
- if (existing === void 0) return false;
2031
- await this.#driver.write(this.#name, key, this.#validate(Object.assign({}, existing, changes)));
2953
+ if (existing === void 0) {
2954
+ checkAbort(options?.signal);
2955
+ return false;
2956
+ }
2957
+ const input = changes;
2958
+ if ((0, _orkestrel_contract.isRecord)(input) && Object.hasOwn(input, this.#key) && !equalsValue(input[this.#key], key)) throw new DatabaseError("VALIDATION", `Update cannot change primary column '${this.#key}' on table '${this.#name}'`, {
2959
+ table: this.#name,
2960
+ column: this.#key,
2961
+ key
2962
+ });
2963
+ await this.#driver.write(this.#name, key, this.#validate(Object.assign({}, existing, changes)), options);
2032
2964
  this.#emitter.emit("write", key);
2033
2965
  return true;
2034
2966
  }
2035
- async #delete(key) {
2036
- const removed = await this.#driver.delete(this.#name, key);
2967
+ async #updateCursor(key, changes) {
2968
+ await this.#wait(void 0);
2969
+ return this.#updateOne(key, changes);
2970
+ }
2971
+ async #delete(key, options) {
2972
+ const removed = await this.#driver.delete(this.#name, key, options);
2037
2973
  if (removed) this.#emitter.emit("remove", key);
2038
2974
  return removed;
2039
2975
  }
2976
+ async #deleteCursor(key) {
2977
+ await this.#wait(void 0);
2978
+ return this.#delete(key);
2979
+ }
2980
+ async #wait(signal) {
2981
+ checkAbort(signal);
2982
+ const ready = this.#ready();
2983
+ if (signal === void 0) {
2984
+ await ready;
2985
+ return;
2986
+ }
2987
+ const cleanup = new AbortController();
2988
+ try {
2989
+ await new Promise((resolve, reject) => {
2990
+ signal.addEventListener("abort", () => {
2991
+ ready.catch(() => {});
2992
+ try {
2993
+ checkAbort(signal);
2994
+ } catch (error) {
2995
+ reject(error);
2996
+ }
2997
+ }, {
2998
+ once: true,
2999
+ signal: cleanup.signal
3000
+ });
3001
+ ready.then(resolve, reject);
3002
+ if (signal.aborted) {
3003
+ ready.catch(() => {});
3004
+ try {
3005
+ checkAbort(signal);
3006
+ } catch (error) {
3007
+ reject(error);
3008
+ }
3009
+ }
3010
+ });
3011
+ } finally {
3012
+ cleanup.abort();
3013
+ }
3014
+ checkAbort(signal);
3015
+ }
2040
3016
  async #collect() {
2041
3017
  const rows = [];
2042
3018
  for await (const row of this.#driver.scan(this.#name)) rows.push(row);
@@ -2045,21 +3021,38 @@ var Table = class {
2045
3021
  #prepare(row) {
2046
3022
  if (!(0, _orkestrel_contract.isRecord)(row)) throw new DatabaseError("VALIDATION", `Row for table '${this.#name}' is not a record`, { table: this.#name });
2047
3023
  const prepared = { ...row };
2048
- if (prepared[this.#key] === void 0) {
2049
- if (this.#generate === void 0) throw new DatabaseError("VALIDATION", `Row for table '${this.#name}' is missing its key column '${this.#key}' and no key factory was provided`, {
3024
+ if (prepared[this.#key] === void 0) if (this.#generate !== void 0) try {
3025
+ prepared[this.#key] = this.#generate();
3026
+ } catch (cause) {
3027
+ throw new DatabaseError("VALIDATION", `Failed to generate primary column '${this.#key}' for table '${this.#name}'`, {
2050
3028
  table: this.#name,
2051
- column: this.#key
3029
+ column: this.#key,
3030
+ cause
3031
+ });
3032
+ }
3033
+ else try {
3034
+ prepared[this.#key] = crypto.randomUUID();
3035
+ } catch (cause) {
3036
+ throw new DatabaseError("DRIVER", `Host failed to generate primary column '${this.#key}' for table '${this.#name}'`, {
3037
+ table: this.#name,
3038
+ column: this.#key,
3039
+ cause
2052
3040
  });
2053
- prepared[this.#key] = this.#generate();
2054
3041
  }
2055
3042
  return prepared;
2056
3043
  }
2057
3044
  #validate(row) {
2058
3045
  const parsed = this.#contract.parse(row);
2059
- if (parsed === void 0 || !(0, _orkestrel_contract.isRecord)(parsed)) throw new DatabaseError("VALIDATION", `Row failed the '${this.#name}' contract`, {
2060
- table: this.#name,
2061
- row
2062
- });
3046
+ if (parsed === void 0 || !(0, _orkestrel_contract.isRecord)(parsed)) {
3047
+ const [fault] = this.#contract.explain(row);
3048
+ throw new DatabaseError("VALIDATION", `Row failed the '${this.#name}' contract`, {
3049
+ table: this.#name,
3050
+ ...fault === void 0 ? {} : {
3051
+ field: fault.path,
3052
+ reason: fault.reason
3053
+ }
3054
+ });
3055
+ }
2063
3056
  return parsed;
2064
3057
  }
2065
3058
  #resolveKey(row) {
@@ -2073,80 +3066,99 @@ var Table = class {
2073
3066
  #cast(row) {
2074
3067
  return row !== void 0 && this.#guard(row) ? row : void 0;
2075
3068
  }
3069
+ #track(operation) {
3070
+ if (this.#context !== void 0) return this.#context.track(operation);
3071
+ return this.#scope === void 0 ? operation() : this.#scope.track(operation);
3072
+ }
2076
3073
  };
2077
3074
  //#endregion
2078
- //#region src/core/Database.ts
3075
+ //#region src/core/DatabaseTransaction.ts
2079
3076
  /**
2080
- * A database the ergonomic entry point over a {@link DriverInterface}.
3077
+ * A table-only database view bound to one driver transaction scope.
3078
+ *
3079
+ * @typeParam T - The declared table shape map
2081
3080
  *
2082
3081
  * @remarks
2083
- * Owns the driver and a `tables` shape map, connecting the driver lazily on first
2084
- * use so a freshly created database is immediately usable. `table(name)` returns
2085
- * a table typed by that table's shape `Infer`. `import` registers more tables and
2086
- * returns a database re-typed with them over the **same** driver and storage;
2087
- * `export` emits a portable {@link TableExport} per table. `transaction` snapshots
2088
- * the driver, runs the scope, and rolls every table back if it throws — an
2089
- * optimistic model that works uniformly across backends rather than reconciling
2090
- * SQL's and IndexedDB's incompatible native transactions.
3082
+ * The view gives transaction work the same typed tables as its owning database
3083
+ * while enforcing a materially narrower contract and lifetime. `table` and
3084
+ * every table operation call the owning scope check, so a captured capability
3085
+ * cannot escape its transaction.
3086
+ */
3087
+ var DatabaseTransaction = class {
3088
+ #driver;
3089
+ #tables;
3090
+ #primary;
3091
+ #generate;
3092
+ #error;
3093
+ #scope;
3094
+ constructor(driver, tables, primary, generate, error, scope) {
3095
+ this.#driver = driver;
3096
+ this.#tables = tables;
3097
+ this.#primary = primary;
3098
+ this.#generate = generate;
3099
+ this.#error = error;
3100
+ this.#scope = scope;
3101
+ }
3102
+ table(name) {
3103
+ this.#scope.check();
3104
+ const columns = this.#columns(name);
3105
+ return this.#build(name, this.#key(name), (0, _orkestrel_contract.createContract)((0, _orkestrel_contract.objectShape)(columns)));
3106
+ }
3107
+ #build(name, key, contract) {
3108
+ return new Table(() => Promise.resolve(), this.#driver, name, key, contract, this.#generate, this.#error, void 0, this.#scope);
3109
+ }
3110
+ #key(name) {
3111
+ return this.#primary[name] ?? "id";
3112
+ }
3113
+ #columns(name) {
3114
+ const columns = this.#tables[name];
3115
+ if (columns === void 0) throw new DatabaseError("NOT_FOUND", `Table '${name}' is not declared`, { table: name });
3116
+ return columns;
3117
+ }
3118
+ };
3119
+ //#endregion
3120
+ //#region src/core/Database.ts
3121
+ /**
3122
+ * A typed database view over one shared internal lifecycle and storage context.
2091
3123
  *
2092
3124
  * @remarks
2093
- * - **Versioned (optional).** When {@link DatabaseOptions.version} is set and the driver
2094
- * implements both {@link DriverInterface.meta} and {@link DriverInterface.stamp},
2095
- * `open()` reconciles the driver's persisted {@link DriverMeta} against the declared
2096
- * version INSIDE the same lazy-connect chain, AFTER the `open` event fires — see
2097
- * {@link DatabaseOptions.version} for the full reconciliation contract.
2098
- * - **Observable (§13).** The owned {@link emitter} ({@link DatabaseEventMap}) carries the
2099
- * connection + transaction lifecycle — `open` / `close` / `transaction` / `commit` /
2100
- * `rollback` — for fire-and-forget observers, ALONGSIDE each table's per-row events. Every
2101
- * event is emitted directly, strictly AFTER the relevant transition: `commit` only after
2102
- * the scope succeeds, `rollback` only after every table is restored. The `rollback` emit
2103
- * OBSERVES the propagated error — it never swallows it (the original throw propagates
2104
- * exactly as before). The emitter isolates a listener throw and routes it to its `error`
2105
- * handler (the `error` option), so observation can never reorder, throw into, or corrupt
2106
- * the snapshot / commit / rollback flow.
3125
+ * Each view owns only its table contracts, primary columns, indexes, and key
3126
+ * generator. Imported views register their physical schemas with the same
3127
+ * internal context before opening begins, so every view observes one driver,
3128
+ * merged schema, emitter, status, transaction boundary, and terminal close.
2107
3129
  */
2108
3130
  var Database = class Database {
2109
- #driver;
3131
+ #context;
2110
3132
  #tables;
2111
- #keys;
3133
+ #primary;
2112
3134
  #indexes;
2113
- #name;
2114
3135
  #generate;
2115
- #version;
2116
- #emitter;
2117
- #status = "idle";
2118
- #ready;
2119
3136
  constructor(options) {
2120
- this.#driver = options.driver;
2121
3137
  this.#tables = options.tables;
2122
- this.#keys = options.keys ?? {};
3138
+ this.#primary = options.primary ?? {};
2123
3139
  this.#indexes = options.indexes ?? {};
2124
- this.#name = options.name ?? "database";
2125
- this.#generate = options.key;
2126
- this.#version = options.version;
2127
- this.#emitter = new _orkestrel_emitter.Emitter({
2128
- ...options.on !== void 0 ? { on: options.on } : {},
2129
- ...options.error !== void 0 ? { error: options.error } : {}
2130
- });
3140
+ this.#generate = options.generator;
3141
+ this.#context = new DatabaseContext(options);
3142
+ this.#context.register(this.#schema());
2131
3143
  }
2132
3144
  get emitter() {
2133
- return this.#emitter;
3145
+ return this.#context.emitter;
2134
3146
  }
2135
3147
  get name() {
2136
- return this.#name;
3148
+ return this.#context.name;
2137
3149
  }
2138
3150
  get status() {
2139
- return this.#status;
3151
+ return this.#context.status;
2140
3152
  }
2141
3153
  table(name) {
2142
- if (this.#status === "closed") throw new DatabaseError("CLOSED", `Database '${this.#name}' is closed`, { name: this.#name });
3154
+ if (this.#context.status === "closed") throw new DatabaseError("CLOSED", `Database '${this.#context.name}' is closed`, { name: this.#context.name });
2143
3155
  const columns = this.#columns(name);
2144
3156
  return this.#build(name, this.#key(name), (0, _orkestrel_contract.createContract)((0, _orkestrel_contract.objectShape)(columns)));
2145
3157
  }
2146
- import(tables, keys) {
3158
+ import(tables, primary) {
2147
3159
  return this.#spawn(tables, {
2148
- ...this.#keys,
2149
- ...keys
3160
+ ...this.#primary,
3161
+ ...primary
2150
3162
  });
2151
3163
  }
2152
3164
  export() {
@@ -2154,109 +3166,44 @@ var Database = class Database {
2154
3166
  for (const name of Object.keys(this.#tables)) {
2155
3167
  const columns = this.#columns(name);
2156
3168
  result[name] = {
2157
- key: this.#key(name),
3169
+ primary: this.#key(name),
2158
3170
  columns,
2159
3171
  schema: (0, _orkestrel_contract.compileSchema)((0, _orkestrel_contract.objectShape)(columns))
2160
3172
  };
2161
3173
  }
2162
3174
  return result;
2163
3175
  }
2164
- async open() {
2165
- await this.#connect();
3176
+ open() {
3177
+ return this.#context.open();
2166
3178
  }
2167
- async close() {
2168
- this.#status = "closed";
2169
- this.#ready = void 0;
2170
- await this.#driver.close();
2171
- this.#emitter.emit("close");
3179
+ close() {
3180
+ return this.#context.close();
2172
3181
  }
2173
- /**
2174
- * Run `scope` transactionally: commit its writes on success, roll every table
2175
- * back if it throws.
2176
- *
2177
- * @remarks
2178
- * When the driver implements the optional native {@link DriverInterface.transaction}
2179
- * hook, that native `commit` / `rollback` handle drives the transaction; otherwise
2180
- * the universal snapshot floor (`driver.snapshot()`) runs unchanged. Either path
2181
- * emits the same `transaction` / `commit` / `rollback` lifecycle (AGENTS §13).
2182
- * `options.signal` is checked ONCE at entry, before connecting or starting any
2183
- * transactional work — an already-aborted signal throws `ABORTED` and neither the
2184
- * native hook nor the snapshot floor is invoked. Nesting is unguarded and
2185
- * unsupported exactly as before: this is a single-writer model, not reentrant.
2186
- * On the native path, a `scope` throw rolls back via the native handle; a
2187
- * native `commit` failure propagates as-is with no rollback attempt — the
2188
- * engine owns transaction state after a failed COMMIT.
2189
- *
2190
- * @param scope - The transactional work to run
2191
- * @param options - `{ signal }` to abort before the transaction starts
2192
- * @returns The scope's resolved value
2193
- * @throws An `ABORTED` {@link DatabaseError} when `options.signal` has already fired
2194
- */
2195
- async transaction(scope, options) {
2196
- checkAbort(options?.signal);
2197
- await this.#connect();
2198
- const native = await this.#driver.transaction?.();
2199
- if (native !== void 0) {
2200
- this.#emitter.emit("transaction");
2201
- let value;
2202
- try {
2203
- value = await scope();
2204
- } catch (error) {
2205
- await native.rollback();
2206
- this.#emitter.emit("rollback", error);
2207
- throw error;
2208
- }
2209
- await native.commit();
2210
- this.#emitter.emit("commit");
2211
- return value;
2212
- }
2213
- const rollback = await this.#driver.snapshot();
2214
- this.#emitter.emit("transaction");
2215
- try {
2216
- const value = await scope();
2217
- this.#emitter.emit("commit");
2218
- return value;
2219
- } catch (error) {
2220
- await rollback();
2221
- this.#emitter.emit("rollback", error);
2222
- throw error;
2223
- }
3182
+ transaction(scope, options) {
3183
+ return this.#context.transaction(async (storage, lifetime) => {
3184
+ const transaction = new DatabaseTransaction(storage, this.#tables, this.#primary, this.#generate, this.#context.error, lifetime);
3185
+ return this.#settle(scope, transaction, lifetime);
3186
+ }, options);
2224
3187
  }
2225
- /**
2226
- * Diff `deployed` against this database's declared schema and apply the
2227
- * resulting plan through the driver's optional `migrate` hook.
2228
- *
2229
- * @param deployed - The schema currently deployed, as {@link TableSchema}s
2230
- * @param options - `{ signal }` to abort before the migration starts
2231
- * @returns The applied {@link Migration} plan
2232
- * @throws A `MIGRATION` {@link DatabaseError} when the driver does not
2233
- * implement `migrate`, or when a step references an unknown table
2234
- * (propagated from the driver)
2235
- * @throws An `ABORTED` {@link DatabaseError} when `options.signal` has
2236
- * already fired at entry
2237
- */
2238
- async migrate(deployed, options) {
2239
- checkAbort(options?.signal);
2240
- await this.#connect();
2241
- const plan = planMigration(deployed, this.#schema());
2242
- if (this.#driver.migrate === void 0) throw new DatabaseError("MIGRATION", `Database '${this.#name}' driver does not support migration`, { name: this.#name });
2243
- await this.#apply(plan);
2244
- return plan;
3188
+ migrate(deployed, options) {
3189
+ return this.#context.migrate(deployed, options);
2245
3190
  }
2246
3191
  #build(name, key, contract) {
2247
- return new Table(() => this.#connect(), this.#driver, name, key, contract, this.#generate);
3192
+ return new Table(() => this.#context.connect(), this.#context.driver, name, key, contract, this.#generate, this.#context.error, this.#context);
2248
3193
  }
2249
- #spawn(tables, keys) {
2250
- return new Database({
2251
- driver: this.#driver,
3194
+ #spawn(tables, primary) {
3195
+ return Database.#attach({
3196
+ driver: this.#context.driver,
2252
3197
  tables,
2253
- keys,
2254
- name: this.#name,
2255
- ...this.#generate === void 0 ? {} : { key: this.#generate }
2256
- });
3198
+ primary,
3199
+ name: this.#context.name,
3200
+ ...this.#context.error === void 0 ? {} : { error: this.#context.error },
3201
+ ...this.#generate === void 0 ? {} : { generator: this.#generate },
3202
+ ...this.#context.version === void 0 ? {} : { version: this.#context.version }
3203
+ }, this.#context);
2257
3204
  }
2258
3205
  #key(name) {
2259
- return this.#keys[name] ?? "id";
3206
+ return this.#primary[name] ?? "id";
2260
3207
  }
2261
3208
  #columns(name) {
2262
3209
  const columns = this.#tables[name];
@@ -2269,74 +3216,36 @@ var Database = class Database {
2269
3216
  return {
2270
3217
  name,
2271
3218
  primary: this.#key(name),
2272
- columns: Object.entries(columns).map(([column, shape]) => {
2273
- return {
2274
- name: column,
2275
- type: shapeToColumnType(shape),
2276
- nullable: shape.type === "optional" || shape.type === "nullable"
2277
- };
2278
- }),
3219
+ columns: Object.entries(columns).map(([column, shape]) => shapeToColumnSchema(column, shape)),
2279
3220
  indexes: this.#indexes[name] ?? []
2280
3221
  };
2281
3222
  });
2282
3223
  }
2283
- #connect() {
2284
- if (this.#status === "closed") throw new DatabaseError("CLOSED", `Database '${this.#name}' is closed`, { name: this.#name });
2285
- if (this.#ready === void 0) this.#ready = this.#driver.open(this.#schema()).then(async () => {
2286
- if (this.#status === "idle") this.#status = "open";
2287
- this.#emitter.emit("open");
2288
- await this.#reconcile();
2289
- });
2290
- return this.#ready;
2291
- }
2292
- async #reconcile() {
2293
- if (this.#version === void 0 || this.#driver.meta === void 0) return;
2294
- const declared = this.#schema();
2295
- const meta = await this.#driver.meta();
2296
- if (meta === void 0) {
2297
- await this.#stamp();
2298
- return;
2299
- }
2300
- if (meta.version > this.#version) throw new DatabaseError("MIGRATION", `Database '${this.#name}' store version ${meta.version} is newer than declared version ${this.#version}`, {
2301
- name: this.#name,
2302
- stored: meta.version,
2303
- declared: this.#version
2304
- });
2305
- if (meta.version < this.#version) {
2306
- const plan = planMigration(meta.schema, declared, meta.version, this.#version);
2307
- if (plan.steps.length > 0 && this.#driver.migrate === void 0) throw new DatabaseError("MIGRATION", `Database '${this.#name}' driver does not support migration`, {
2308
- name: this.#name,
2309
- stored: meta.version,
2310
- declared: this.#version
2311
- });
2312
- await this.#apply(plan);
2313
- }
2314
- }
2315
- async #apply(plan) {
2316
- const native = await this.#driver.transaction?.();
2317
- if (native !== void 0) {
2318
- try {
2319
- await this.#driver.migrate?.(plan);
2320
- await this.#stamp();
2321
- } catch (error) {
2322
- await native.rollback();
2323
- throw error;
2324
- }
2325
- await native.commit();
2326
- this.#emitter.emit("migrate", plan);
2327
- return;
2328
- }
2329
- await this.#driver.migrate?.(plan);
2330
- await this.#stamp();
2331
- this.#emitter.emit("migrate", plan);
2332
- }
2333
- async #stamp() {
2334
- if (this.#version === void 0 || this.#driver.stamp === void 0) return;
2335
- const meta = {
2336
- version: this.#version,
2337
- schema: this.#schema()
2338
- };
2339
- await this.#driver.stamp(meta);
3224
+ async #settle(scope, transaction, lifetime) {
3225
+ const outcome = await Promise.resolve().then(() => scope(transaction)).then((value) => ({
3226
+ success: true,
3227
+ value
3228
+ }), (error) => ({
3229
+ success: false,
3230
+ error
3231
+ }));
3232
+ lifetime.stop();
3233
+ const drained = await lifetime.drain().then(() => ({
3234
+ success: true,
3235
+ value: void 0
3236
+ }), (error) => ({
3237
+ success: false,
3238
+ error
3239
+ }));
3240
+ if (!outcome.success) return outcome;
3241
+ if (!drained.success) return drained;
3242
+ return outcome;
3243
+ }
3244
+ static #attach(options, context) {
3245
+ const database = new Database(options);
3246
+ database.#context = context;
3247
+ context.register(database.#schema());
3248
+ return database;
2340
3249
  }
2341
3250
  };
2342
3251
  //#endregion
@@ -2352,29 +3261,54 @@ var Database = class Database {
2352
3261
  * snapshot capture and restore — so a caller mutating a nested field of an input
2353
3262
  * row, a returned row, or a row mutated in place between snapshot and rollback
2354
3263
  * can never perturb stored state (AGENTS §11); a shallow `{ ...row }` spread
2355
- * would still share nested object/array references. `snapshot`
3264
+ * would still share nested object/array references. Metadata instead routes
3265
+ * through `cloneDriverMetadata`: `stamp` and migration snapshot exact JSON at
3266
+ * ingress, and `metadata` returns a distinct deeply frozen owned copy. `snapshot`
2356
3267
  * clones every table to give transactions an exact rollback point. `scan` and
2357
3268
  * `keys` yield in key order — sorted by the core {@link compareValues} total
2358
3269
  * order, the same contract the SQLite (`ORDER BY`) and IndexedDB (key-ordered
2359
3270
  * reads) backends honor, so an unordered read agrees across every backend rather
2360
3271
  * than leaking Map insertion order. A persistent backend (IndexedDB, SQLite)
2361
- * implements the same nine methods over real storage.
3272
+ * implements the same required methods over real storage.
2362
3273
  */
2363
3274
  var MemoryDriver = class {
2364
3275
  #tables = /* @__PURE__ */ new Map();
2365
- #meta;
3276
+ #identities = /* @__PURE__ */ new Map();
3277
+ #schema = [];
3278
+ #metadata;
2366
3279
  async open(schema) {
2367
- for (const table of schema) if (!this.#tables.has(table.name)) this.#tables.set(table.name, /* @__PURE__ */ new Map());
3280
+ const owned = normalizeDriverSchema(schema);
3281
+ const deployed = normalizeDriverSchema(this.#metadata?.schema ?? owned);
3282
+ const names = new Set(deployed.map((table) => table.name));
3283
+ for (const name of this.#identities.keys()) if (!names.has(name)) this.#identities.delete(name);
3284
+ for (const table of deployed) {
3285
+ if (!this.#tables.has(table.name)) this.#tables.set(table.name, /* @__PURE__ */ new Map());
3286
+ if (!this.#identities.has(table.name)) this.#identities.set(table.name, {});
3287
+ }
3288
+ this.#schema = deployed;
2368
3289
  }
2369
3290
  async close() {}
2370
3291
  async read(table, key) {
2371
3292
  const row = this.#store(table).get(key);
2372
3293
  return row === void 0 ? void 0 : structuredClone(row);
2373
3294
  }
2374
- async write(table, key, row) {
2375
- this.#store(table).set(key, structuredClone(row));
3295
+ async write(table, key, row, options) {
3296
+ checkAbort(options?.signal);
3297
+ const primary = this.#table(table).primary;
3298
+ this.#store(table).set(key, structuredClone(bindRowKey(row, primary, key)));
3299
+ }
3300
+ async insert(table, key, row, options) {
3301
+ checkAbort(options?.signal);
3302
+ const store = this.#store(table);
3303
+ if (store.has(key)) throw new DatabaseError("CONFLICT", `Row '${key}' already exists in table '${table}'`, {
3304
+ table,
3305
+ key
3306
+ });
3307
+ const primary = this.#table(table).primary;
3308
+ store.set(key, structuredClone(bindRowKey(row, primary, key)));
2376
3309
  }
2377
- async delete(table, key) {
3310
+ async delete(table, key, options) {
3311
+ checkAbort(options?.signal);
2378
3312
  return this.#store(table).delete(key);
2379
3313
  }
2380
3314
  async keys(table) {
@@ -2393,17 +3327,17 @@ var MemoryDriver = class {
2393
3327
  * @remarks
2394
3328
  * Iterates the table's keys in the same key order `scan` and `keys` yield
2395
3329
  * (sorted by {@link compareValues}), testing each row against
2396
- * `criteria.conditions` (via {@link matchesCriteria}) before counting it
3330
+ * `input.conditions` (via {@link matchesQuery}) before counting it
2397
3331
  * toward `offset` / `limit`. Both are applied lazily as matches are found —
2398
3332
  * `offset` matches are skipped without being yielded, and iteration stops the
2399
3333
  * instant `limit` yields have been produced, so a large table is never fully
2400
- * walked for a small page. `criteria.order` is IGNORED (the same contract as
3334
+ * walked for a small page. `input.order` is IGNORED (the same contract as
2401
3335
  * `TableInterface.scan` and `QueryInterface.stream`): streaming yields key
2402
3336
  * order, sorted output is `records()`'s job. Rows yield copy-out (AGENTS
2403
3337
  * §11), and an unknown table mirrors `scan`'s empty-yield behavior.
2404
3338
  *
2405
3339
  * @param table - The table to stream
2406
- * @param criteria - The filter / offset / limit to apply lazily
3340
+ * @param input - The filter / offset / limit to apply lazily
2407
3341
  *
2408
3342
  * @example
2409
3343
  * ```ts
@@ -2412,18 +3346,22 @@ var MemoryDriver = class {
2412
3346
  * }
2413
3347
  * ```
2414
3348
  */
2415
- async *stream(table, criteria) {
3349
+ stream(table, input) {
3350
+ validatePage(input);
3351
+ return this.#stream(table, input);
3352
+ }
3353
+ async *#stream(table, input) {
2416
3354
  const store = this.#store(table);
2417
- const conditions = criteria.conditions;
2418
- const offset = criteria.offset ?? 0;
2419
- const limit = criteria.limit;
3355
+ const conditions = input.conditions;
3356
+ const offset = input.offset ?? 0;
3357
+ const limit = input.limit;
2420
3358
  let skipped = 0;
2421
3359
  let yielded = 0;
2422
3360
  for (const key of this.#ordered(table)) {
2423
3361
  if (limit !== void 0 && yielded >= limit) return;
2424
3362
  const row = store.get(key);
2425
3363
  if (row === void 0) continue;
2426
- if (conditions !== void 0 && conditions.length > 0 && !matchesCriteria(row, conditions)) continue;
3364
+ if (conditions !== void 0 && conditions.length > 0 && !matchesQuery(row, conditions)) continue;
2427
3365
  if (skipped < offset) {
2428
3366
  skipped += 1;
2429
3367
  continue;
@@ -2439,90 +3377,149 @@ var MemoryDriver = class {
2439
3377
  * Capture the current state and return a thunk that rolls back to it.
2440
3378
  *
2441
3379
  * @remarks
2442
- * `tables` omitted clones and restores the WHOLE store, byte-identical to the
2443
- * prior whole-store behavior. `tables` provided clones ONLY the named tables,
2444
- * and the returned thunk restores ONLY those every other table keeps
2445
- * whatever it was mutated to after the snapshot was taken.
3380
+ * Capture owns rows, schema, and one session-local table identity. Replay
3381
+ * adapts rows to each surviving same-identity table's current schema before
3382
+ * changing storage. Removed or replaced tables are skipped; uncaptured and
3383
+ * later-added tables retain their current rows. Schema and metadata are never
3384
+ * restored.
2446
3385
  *
2447
3386
  * @param tables - The table names to scope the snapshot to; omitted captures every table
2448
3387
  * @returns A thunk that restores the captured tables
2449
3388
  */
2450
3389
  async snapshot(tables) {
2451
- if (tables === void 0) {
2452
- const copy = /* @__PURE__ */ new Map();
2453
- for (const [name, store] of this.#tables) {
2454
- const cloned = /* @__PURE__ */ new Map();
2455
- for (const [key, row] of store) cloned.set(key, structuredClone(row));
2456
- copy.set(name, cloned);
2457
- }
2458
- return async () => {
2459
- this.#tables.clear();
2460
- for (const [name, store] of copy) {
2461
- const restored = /* @__PURE__ */ new Map();
2462
- for (const [key, row] of store) restored.set(key, structuredClone(row));
2463
- this.#tables.set(name, restored);
2464
- }
2465
- };
2466
- }
2467
- const copy = /* @__PURE__ */ new Map();
2468
- for (const name of tables) {
3390
+ const names = tables === void 0 ? this.#schema.map((table) => table.name) : [...new Set(tables)].filter((name) => this.#schema.some((table) => table.name === name));
3391
+ const captured = /* @__PURE__ */ new Map();
3392
+ for (const name of names) {
3393
+ const schema = this.#schema.find((table) => table.name === name);
2469
3394
  const store = this.#tables.get(name);
2470
- if (store === void 0) continue;
2471
- const cloned = /* @__PURE__ */ new Map();
2472
- for (const [key, row] of store) cloned.set(key, structuredClone(row));
2473
- copy.set(name, cloned);
3395
+ const identity = this.#identities.get(name);
3396
+ if (schema === void 0 || store === void 0 || identity === void 0) continue;
3397
+ const rows = /* @__PURE__ */ new Map();
3398
+ for (const [key, row] of store) rows.set(key, structuredClone(row));
3399
+ captured.set(name, {
3400
+ identity,
3401
+ rows,
3402
+ schema
3403
+ });
2474
3404
  }
2475
3405
  return async () => {
2476
- for (const [name, store] of copy) {
2477
- const restored = /* @__PURE__ */ new Map();
2478
- for (const [key, row] of store) restored.set(key, structuredClone(row));
2479
- this.#tables.set(name, restored);
3406
+ const replacements = /* @__PURE__ */ new Map();
3407
+ for (const [name, capture] of captured) {
3408
+ const schema = this.#schema.find((table) => table.name === name);
3409
+ const store = this.#tables.get(name);
3410
+ if (schema === void 0 || store === void 0 || this.#identities.get(name) !== capture.identity) continue;
3411
+ const plan = planMigration([capture.schema], [schema]);
3412
+ const entries = [...capture.rows.entries()];
3413
+ const migrated = migrateRows(entries.map(([, row]) => row), plan.steps);
3414
+ if (migrated.length !== entries.length) throw new DatabaseError("MIGRATION", "Snapshot row count changed during migration", { table: name });
3415
+ const rows = /* @__PURE__ */ new Map();
3416
+ for (const [index, [key]] of entries.entries()) {
3417
+ const row = migrated[index];
3418
+ if (!isKey(key) || row === void 0) throw new DatabaseError("MIGRATION", "Snapshot row has no usable primary key", {
3419
+ table: name,
3420
+ column: schema.primary,
3421
+ index
3422
+ });
3423
+ rows.set(key, structuredClone(bindRowKey(row, schema.primary, key)));
3424
+ }
3425
+ replacements.set(store, rows);
3426
+ }
3427
+ for (const [store, rows] of replacements) {
3428
+ store.clear();
3429
+ for (const [key, row] of rows) store.set(key, row);
2480
3430
  }
2481
3431
  };
2482
3432
  }
2483
3433
  /**
2484
- * Return the persisted {@link DriverMeta}, or `undefined` when the store has
3434
+ * Return the persisted {@link DriverMetadata}, or `undefined` when the store has
2485
3435
  * never been stamped.
2486
3436
  *
2487
3437
  * @remarks
2488
3438
  * In-process only — the metadata lives in this instance's memory, exactly
2489
- * like the rest of this driver's storage. A driver-conformance-valid
2490
- * implementation of the optional `meta` / `stamp` pair.
3439
+ * like the rest of this driver's storage. The returned value is a distinct
3440
+ * deeply frozen owned snapshot. A driver-conformance-valid implementation of
3441
+ * the optional `metadata` / `stamp` pair.
2491
3442
  *
2492
- * @returns The last-stamped {@link DriverMeta}, or `undefined`
3443
+ * @returns The last-stamped {@link DriverMetadata}, or `undefined`
2493
3444
  */
2494
- async meta() {
2495
- return this.#meta;
3445
+ async metadata() {
3446
+ return this.#metadata === void 0 ? void 0 : cloneDriverMetadata(this.#metadata);
2496
3447
  }
2497
3448
  /**
2498
- * Persist `meta` verbatim for a later `meta()` to return.
3449
+ * Persist an owned snapshot for a later `metadata()` to return.
2499
3450
  *
2500
- * @param meta - The {@link DriverMeta} to persist
3451
+ * @param metadata - The {@link DriverMetadata} to persist
2501
3452
  */
2502
- async stamp(meta) {
2503
- this.#meta = meta;
3453
+ async stamp(metadata) {
3454
+ this.#metadata = cloneDriverMetadata(metadata);
2504
3455
  }
2505
3456
  /**
2506
3457
  * Apply a {@link Migration} plan's steps against the in-memory store.
2507
3458
  *
2508
3459
  * @remarks
2509
- * A multi-step plan applies its steps sequentially and is NOT atomic — a
2510
- * failure partway through a plan leaves the earlier steps already applied.
3460
+ * Steps apply against an isolated candidate. Rows, schema changes, and
3461
+ * optional metadata publish together only after the whole request succeeds.
2511
3462
  *
2512
- * @param plan - The migration plan to apply
3463
+ * @param input - The migration plan and optional metadata to settle atomically
2513
3464
  */
2514
- async migrate(plan) {
2515
- for (const step of plan.steps) switch (step.operation) {
3465
+ async migrate(input) {
3466
+ const owned = cloneMigrationInput(input);
3467
+ const schema = projectMigrationSchema(this.#schema, owned.plan.steps);
3468
+ if (owned.metadata !== void 0 && !equalsValue(normalizeDriverSchema(owned.metadata.schema), schema)) throw new DatabaseError("MIGRATION", "Migration metadata schema does not match the plan", {
3469
+ projected: schema,
3470
+ metadata: owned.metadata.schema
3471
+ });
3472
+ const candidate = this.#copy(this.#tables);
3473
+ const identities = this.#projectIdentities(this.#identities, owned.plan.steps);
3474
+ for (const step of owned.plan.steps) this.#migrate(candidate, step);
3475
+ this.#tables.clear();
3476
+ for (const [name, store] of candidate) this.#tables.set(name, store);
3477
+ this.#identities = identities;
3478
+ this.#schema = schema;
3479
+ if (owned.metadata !== void 0) this.#metadata = owned.metadata;
3480
+ }
3481
+ #ordered(table) {
3482
+ return [...this.#store(table).keys()].sort(compareValues);
3483
+ }
3484
+ #require(tables, table) {
3485
+ const store = tables.get(table);
3486
+ if (store === void 0) throw new DatabaseError("MIGRATION", `migrate: unknown table '${table}'`, { table });
3487
+ return store;
3488
+ }
3489
+ #copy(tables) {
3490
+ const copy = /* @__PURE__ */ new Map();
3491
+ for (const [name, store] of tables) {
3492
+ const cloned = /* @__PURE__ */ new Map();
3493
+ for (const [key, row] of store) cloned.set(key, structuredClone(row));
3494
+ copy.set(name, cloned);
3495
+ }
3496
+ return copy;
3497
+ }
3498
+ #projectIdentities(identities, steps) {
3499
+ const projected = new Map(identities);
3500
+ for (const step of steps) {
3501
+ if (step.operation === "table.add") projected.set(step.table.name, {});
3502
+ if (step.operation === "table.remove") projected.delete(step.table);
3503
+ }
3504
+ return projected;
3505
+ }
3506
+ #table(name) {
3507
+ const schema = this.#schema.find((table) => table.name === name);
3508
+ if (schema === void 0) throw new DatabaseError("NOT_FOUND", `Unknown table '${name}'`, { table: name });
3509
+ return schema;
3510
+ }
3511
+ #migrate(tables, step) {
3512
+ switch (step.operation) {
2516
3513
  case "table.add":
2517
- if (!this.#tables.has(step.table.name)) this.#tables.set(step.table.name, /* @__PURE__ */ new Map());
3514
+ if (!tables.has(step.table.name)) tables.set(step.table.name, /* @__PURE__ */ new Map());
2518
3515
  break;
2519
3516
  case "table.remove":
2520
- this.#require(step.table);
2521
- this.#tables.delete(step.table);
3517
+ this.#require(tables, step.table);
3518
+ tables.delete(step.table);
2522
3519
  break;
2523
3520
  case "column.add":
2524
3521
  case "column.remove": {
2525
- const store = this.#require(step.table);
3522
+ const store = this.#require(tables, step.table);
2526
3523
  const rows = [...store.entries()];
2527
3524
  const migrated = migrateRows(rows.map(([, row]) => row), [step]);
2528
3525
  for (const [index, [key]] of rows.entries()) {
@@ -2536,25 +3533,13 @@ var MemoryDriver = class {
2536
3533
  break;
2537
3534
  }
2538
3535
  case "index.add":
2539
- case "index.remove":
2540
- this.#require(step.table);
2541
- break;
3536
+ case "index.remove": this.#require(tables, step.table);
2542
3537
  }
2543
3538
  }
2544
- #ordered(table) {
2545
- return [...this.#store(table).keys()].sort(compareValues);
2546
- }
2547
- #require(table) {
2548
- const store = this.#tables.get(table);
2549
- if (store === void 0) throw new DatabaseError("MIGRATION", `migrate: unknown table '${table}'`, { table });
2550
- return store;
2551
- }
2552
3539
  #store(table) {
2553
- let store = this.#tables.get(table);
2554
- if (store === void 0) {
2555
- store = /* @__PURE__ */ new Map();
2556
- this.#tables.set(table, store);
2557
- }
3540
+ this.#table(table);
3541
+ const store = this.#tables.get(table);
3542
+ if (store === void 0) throw new DatabaseError("NOT_FOUND", `Table '${table}' has no backing store`, { table });
2558
3543
  return store;
2559
3544
  }
2560
3545
  };
@@ -2569,10 +3554,10 @@ var MemoryDriver = class {
2569
3554
  * level. The `const` type parameter captures the literal names and columns, so
2570
3555
  * `db.table('users')` is checked against the schema and typed by `Infer` of its
2571
3556
  * columns — no annotations. Name a non-`id` primary-key column per table via the
2572
- * optional `keys` map.
3557
+ * optional `primary` and `indexes` maps.
2573
3558
  *
2574
- * @param options - The driver, the `tables` column map, optional `keys`, and an
2575
- * optional `name`
3559
+ * @param options - The driver, `tables`, and optional `primary`, `indexes`,
3560
+ * `name`, `generator`, `version`, and emitter hooks
2576
3561
  * @returns A typed {@link DatabaseInterface}
2577
3562
  *
2578
3563
  * @example
@@ -2586,7 +3571,7 @@ var MemoryDriver = class {
2586
3571
  * users: { id: stringShape(), age: integerShape() },
2587
3572
  * posts: { slug: stringShape(), title: stringShape() },
2588
3573
  * },
2589
- * keys: { posts: 'slug' },
3574
+ * primary: { posts: 'slug' },
2590
3575
  * })
2591
3576
  * await db.table('users').set({ id: 'u1', age: 36 }) // typed; coerced + validated
2592
3577
  * ```
@@ -2607,40 +3592,48 @@ function createMemoryDriver() {
2607
3592
  return new MemoryDriver();
2608
3593
  }
2609
3594
  //#endregion
2610
- exports.Clause = Clause;
2611
- exports.Cursor = Cursor;
2612
3595
  exports.DEFAULT_PRIMARY = DEFAULT_PRIMARY;
2613
3596
  exports.Database = Database;
2614
3597
  exports.DatabaseError = DatabaseError;
2615
3598
  exports.MAX_PATTERN_LENGTH = MAX_PATTERN_LENGTH;
2616
3599
  exports.MemoryDriver = MemoryDriver;
2617
- exports.Query = Query;
2618
- exports.Table = Table;
2619
- exports.UUID_BYTE_COUNT = UUID_BYTE_COUNT;
2620
- exports.UUID_BYTE_RANGE = UUID_BYTE_RANGE;
2621
- exports.applyCriteria = applyCriteria;
3600
+ exports.applyQuery = applyQuery;
2622
3601
  exports.auditDriver = auditDriver;
3602
+ exports.bindRowKey = bindRowKey;
2623
3603
  exports.checkAbort = checkAbort;
3604
+ exports.cloneDriverMetadata = cloneDriverMetadata;
3605
+ exports.cloneDriverSchema = cloneDriverSchema;
3606
+ exports.cloneMigrationInput = cloneMigrationInput;
2624
3607
  exports.compareValues = compareValues;
2625
3608
  exports.computeAggregate = computeAggregate;
2626
3609
  exports.conformDriver = conformDriver;
2627
3610
  exports.createDatabase = createDatabase;
2628
3611
  exports.createMemoryDriver = createMemoryDriver;
2629
- exports.deepEqual = deepEqual;
2630
3612
  exports.driverFindings = driverFindings;
3613
+ exports.equalsValue = equalsValue;
2631
3614
  exports.extractKey = extractKey;
2632
3615
  exports.filterRows = filterRows;
2633
- exports.generateUUID = generateUUID;
2634
- exports.globMatch = globMatch;
3616
+ exports.isColumnSchema = isColumnSchema;
2635
3617
  exports.isDatabaseError = isDatabaseError;
2636
- exports.isDriverMeta = isDriverMeta;
2637
- exports.likeMatch = likeMatch;
3618
+ exports.isDriverMetadata = isDriverMetadata;
3619
+ exports.isDriverSchema = isDriverSchema;
3620
+ exports.isKey = isKey;
3621
+ exports.isMigration = isMigration;
3622
+ exports.isMigrationInput = isMigrationInput;
3623
+ exports.isMigrationStep = isMigrationStep;
3624
+ exports.isTableSchema = isTableSchema;
2638
3625
  exports.matchesCondition = matchesCondition;
2639
- exports.matchesCriteria = matchesCriteria;
3626
+ exports.matchesGlobPattern = matchesGlobPattern;
3627
+ exports.matchesLikePattern = matchesLikePattern;
3628
+ exports.matchesQuery = matchesQuery;
3629
+ exports.matchesWildcardPattern = matchesWildcardPattern;
2640
3630
  exports.migrateRows = migrateRows;
3631
+ exports.normalizeDriverSchema = normalizeDriverSchema;
2641
3632
  exports.planMigration = planMigration;
2642
- exports.shapeToColumnType = shapeToColumnType;
3633
+ exports.projectMigrationSchema = projectMigrationSchema;
3634
+ exports.shapeToColumnSchema = shapeToColumnSchema;
3635
+ exports.shapeToColumnStorage = shapeToColumnStorage;
2643
3636
  exports.sortRows = sortRows;
2644
- exports.wildcardMatch = wildcardMatch;
3637
+ exports.validatePage = validatePage;
2645
3638
 
2646
3639
  //# sourceMappingURL=index.cjs.map