@orkestrel/database 0.0.12 → 0.0.14

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,7 +3,7 @@ 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 PrimaryMap} does not name one.
6
+ * Supplies the primary-key column, `'id'`, 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
@@ -11,12 +11,13 @@ let _orkestrel_emitter = require("@orkestrel/emitter");
11
11
  */
12
12
  var DEFAULT_PRIMARY = "id";
13
13
  /**
14
- * The longest `LIKE` / `GLOB` pattern the wildcard matcher accepts before rejecting it.
14
+ * Sets the longest `LIKE` / `GLOB` pattern the wildcard matcher accepts, 1024 characters, before
15
+ * rejecting it.
15
16
  *
16
17
  * @remarks
17
- * A ReDoS bound (AGENTS §6.5): the SA1–SA4 migration lets a model supply `list`
18
- * input over the wire, so `matchesLikePattern` / `matchesGlobPattern` run attacker-controlled
19
- * patterns. The matcher is the LINEAR greedy two-pointer wildcard match — never a
18
+ * A `LIKE` / `GLOB` pattern is a caller-supplied operand, so
19
+ * `matchesLikePattern` / `matchesGlobPattern` run patterns this package cannot
20
+ * trust. The matcher is the linear greedy two-pointer wildcard match — never a
20
21
  * backtracking regex (`.*`-segments-separated-by-literals against a long input is the
21
22
  * catastrophic shape JS cannot bound without atomic groups), so it is O(value ×
22
23
  * pattern). Capping the pattern length bounds that pattern factor, leaving a match
@@ -24,10 +25,85 @@ var DEFAULT_PRIMARY = "id";
24
25
  * `VALIDATION` {@link DatabaseError}; the cap is generous for any legitimate search.
25
26
  */
26
27
  var MAX_PATTERN_LENGTH = 1024;
28
+ /**
29
+ * Describes the `users` table the driver-conformance battery opens — keyed by the default
30
+ * `id` primary column.
31
+ *
32
+ * @remarks
33
+ * `age` is optional and `meta` is a declared `json` column, so the battery's
34
+ * nested-round-trip phase is fair to a typed-column backend: a SQL driver
35
+ * persists only declared columns, while a schemaless backend ignores the
36
+ * declarations entirely.
37
+ */
38
+ var CONFORMANCE_USERS_SCHEMA = Object.freeze({
39
+ name: "users",
40
+ primary: "id",
41
+ columns: Object.freeze([
42
+ {
43
+ name: "id",
44
+ storage: "text",
45
+ optional: false,
46
+ nullable: false
47
+ },
48
+ {
49
+ name: "name",
50
+ storage: "text",
51
+ optional: false,
52
+ nullable: false
53
+ },
54
+ {
55
+ name: "age",
56
+ storage: "integer",
57
+ optional: true,
58
+ nullable: false
59
+ },
60
+ {
61
+ name: "meta",
62
+ storage: "json",
63
+ optional: true,
64
+ nullable: false
65
+ }
66
+ ]),
67
+ indexes: Object.freeze([])
68
+ });
69
+ /**
70
+ * Describes the `posts` table the driver-conformance battery opens — keyed by a non-`id`
71
+ * `slug` primary column.
72
+ *
73
+ * @remarks
74
+ * Pairs with {@link CONFORMANCE_USERS_SCHEMA} so one battery exercises both
75
+ * primary-key shapes: the default `id` and an explicit override.
76
+ */
77
+ var CONFORMANCE_POSTS_SCHEMA = Object.freeze({
78
+ name: "posts",
79
+ primary: "slug",
80
+ columns: Object.freeze([{
81
+ name: "slug",
82
+ storage: "text",
83
+ optional: false,
84
+ nullable: false
85
+ }, {
86
+ name: "title",
87
+ storage: "text",
88
+ optional: false,
89
+ nullable: false
90
+ }]),
91
+ indexes: Object.freeze([])
92
+ });
93
+ /**
94
+ * Holds the fixed `users` and `posts` schema every driver-conformance phase opens.
95
+ *
96
+ * @remarks
97
+ * Each phase mints a fresh driver and opens this exact schema, so a finding
98
+ * names a violated invariant rather than a setup difference between phases. The
99
+ * array and each schema in it are frozen, so a consumer holding it cannot change
100
+ * what a later phase opens.
101
+ */
102
+ var CONFORMANCE_SCHEMA = Object.freeze([CONFORMANCE_USERS_SCHEMA, CONFORMANCE_POSTS_SCHEMA]);
27
103
  //#endregion
28
104
  //#region src/core/errors.ts
29
105
  /**
30
- * An error thrown by the database layer.
106
+ * Represents an error thrown by the database layer.
31
107
  *
32
108
  * @remarks
33
109
  * Carries a {@link DatabaseErrorCode} and an optional `context` bag naming the
@@ -38,7 +114,7 @@ var MAX_PATTERN_LENGTH = 1024;
38
114
  * `context`), an inapplicable {@link Migration} plan (`MIGRATION`), a
39
115
  * driver that violates a {@link DriverInterface} invariant, thrown by the
40
116
  * `conformDriver` helper (`CONFORMANCE`), and an unexpected infrastructure
41
- * fault surfaced by a driver seam — e.g. a filesystem failure while
117
+ * fault surfaced by a driver seam — for example a filesystem failure while
42
118
  * persisting (`DRIVER`) — as opposed to expected domain conditions, which
43
119
  * keep their specific codes.
44
120
  */
@@ -53,10 +129,10 @@ var DatabaseError = class extends Error {
53
129
  }
54
130
  };
55
131
  /**
56
- * Narrow an unknown caught value to a {@link DatabaseError}.
132
+ * Narrows an unknown caught value to a {@link DatabaseError}.
57
133
  *
58
134
  * @param value - The value to test (typically a `catch` binding)
59
- * @returns `true` when `value` is a {@link DatabaseError}
135
+ * @returns True if `value` is a {@link DatabaseError}; false otherwise
60
136
  *
61
137
  * @example
62
138
  * ```ts
@@ -73,43 +149,23 @@ function isDatabaseError(value) {
73
149
  //#endregion
74
150
  //#region src/core/validators.ts
75
151
  /**
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.
152
+ * Checks whether a value is a usable database key.
101
153
  *
102
154
  * @param value - The value to test
103
- * @returns Whether `value` is a string or finite number
155
+ * @returns True if `value` is a string or a finite number; false otherwise
104
156
  */
105
157
  function isKey(value) {
106
158
  return typeof value === "string" || typeof value === "number" && Number.isFinite(value);
107
159
  }
108
160
  /**
109
- * Test whether a value is a portable column schema.
161
+ * Checks whether a value is a portable column schema.
162
+ *
163
+ * @remarks
164
+ * Total over any input: a hostile getter, a revoked proxy, or a cyclic value is
165
+ * contained as a non-match rather than a throw.
110
166
  *
111
167
  * @param value - The value to test
112
- * @returns Whether `value` is a complete {@link ColumnSchema}
168
+ * @returns True if `value` is a complete {@link ColumnSchema}; false otherwise
113
169
  */
114
170
  function isColumnSchema(value) {
115
171
  try {
@@ -121,10 +177,14 @@ function isColumnSchema(value) {
121
177
  }
122
178
  }
123
179
  /**
124
- * Test whether a value is a portable table schema.
180
+ * Checks whether a value is a portable table schema.
181
+ *
182
+ * @remarks
183
+ * Total over any input: a hostile getter, a revoked proxy, or a cyclic value is
184
+ * contained as a non-match rather than a throw.
125
185
  *
126
186
  * @param value - The value to test
127
- * @returns Whether `value` is a complete {@link TableSchema}
187
+ * @returns True if `value` is a complete {@link TableSchema}; false otherwise
128
188
  */
129
189
  function isTableSchema(value) {
130
190
  try {
@@ -140,10 +200,14 @@ function isTableSchema(value) {
140
200
  }
141
201
  }
142
202
  /**
143
- * Test whether a value is a complete portable driver schema.
203
+ * Checks whether a value is a complete portable driver schema.
204
+ *
205
+ * @remarks
206
+ * Total over any input: a hostile getter, a revoked proxy, or a cyclic value is
207
+ * contained as a non-match rather than a throw.
144
208
  *
145
209
  * @param value - The value to test
146
- * @returns Whether `value` is a table-schema collection with unique table names
210
+ * @returns True if `value` is a table-schema collection with unique table names; false otherwise
147
211
  */
148
212
  function isDriverSchema(value) {
149
213
  try {
@@ -156,10 +220,14 @@ function isDriverSchema(value) {
156
220
  }
157
221
  }
158
222
  /**
159
- * Test whether a value is one ordered migration step.
223
+ * Checks whether a value is one ordered migration step.
224
+ *
225
+ * @remarks
226
+ * Total over any input: a hostile getter, a revoked proxy, or a cyclic value is
227
+ * contained as a non-match rather than a throw.
160
228
  *
161
229
  * @param value - The value to test
162
- * @returns Whether `value` is a complete {@link MigrationStep}
230
+ * @returns True if `value` is a complete {@link MigrationStep}; false otherwise
163
231
  */
164
232
  function isMigrationStep(value) {
165
233
  try {
@@ -180,10 +248,14 @@ function isMigrationStep(value) {
180
248
  }
181
249
  }
182
250
  /**
183
- * Test whether a value is an ordered migration plan.
251
+ * Checks whether a value is an ordered migration plan.
252
+ *
253
+ * @remarks
254
+ * Total over any input: a hostile getter, a revoked proxy, or a cyclic value is
255
+ * contained as a non-match rather than a throw.
184
256
  *
185
257
  * @param value - The value to test
186
- * @returns Whether `value` is a complete {@link Migration}
258
+ * @returns True if `value` is a complete {@link Migration}; false otherwise
187
259
  */
188
260
  function isMigration(value) {
189
261
  try {
@@ -195,10 +267,16 @@ function isMigration(value) {
195
267
  }
196
268
  }
197
269
  /**
198
- * Test whether a value is persisted driver metadata.
270
+ * Checks whether a value is persisted driver metadata.
271
+ *
272
+ * @remarks
273
+ * The boundary check a versioning driver's `metadata()` narrows a stored or
274
+ * deserialized record through, so no call site needs an assertion. Total over any
275
+ * input: a hostile getter, a revoked proxy, or a cyclic value is contained as a
276
+ * non-match rather than a throw.
199
277
  *
200
278
  * @param value - The value to test
201
- * @returns Whether `value` is complete {@link DriverMetadata}
279
+ * @returns True if `value` is complete {@link DriverMetadata}; false otherwise
202
280
  */
203
281
  function isDriverMetadata(value) {
204
282
  try {
@@ -210,10 +288,14 @@ function isDriverMetadata(value) {
210
288
  }
211
289
  }
212
290
  /**
213
- * Test whether a value is one atomic migration request.
291
+ * Checks whether a value is one atomic migration request.
292
+ *
293
+ * @remarks
294
+ * Total over any input: a hostile getter, a revoked proxy, or a cyclic value is
295
+ * contained as a non-match rather than a throw.
214
296
  *
215
297
  * @param value - The value to test
216
- * @returns Whether `value` is a complete {@link MigrationInput}
298
+ * @returns True if `value` is a complete {@link MigrationInput}; false otherwise
217
299
  */
218
300
  function isMigrationInput(value) {
219
301
  try {
@@ -227,7 +309,12 @@ function isMigrationInput(value) {
227
309
  //#endregion
228
310
  //#region src/core/cloners.ts
229
311
  /**
230
- * Clone unknown driver metadata into a distinct deeply frozen snapshot.
312
+ * Clones unknown driver metadata into a distinct deeply frozen snapshot.
313
+ *
314
+ * @remarks
315
+ * The clone is validated as {@link DriverMetadata} before it is returned, so a
316
+ * malformed or hostile value throws a `VALIDATION` {@link DatabaseError} at
317
+ * `context.path === 'metadata'` rather than surfacing a raw Contract or caller error.
231
318
  *
232
319
  * @param value - Unknown metadata
233
320
  * @returns Owned driver metadata
@@ -246,7 +333,12 @@ function cloneDriverMetadata(value) {
246
333
  }
247
334
  }
248
335
  /**
249
- * Clone unknown driver schema into a distinct deeply frozen snapshot.
336
+ * Clones unknown driver schema into a distinct deeply frozen snapshot.
337
+ *
338
+ * @remarks
339
+ * The clone is validated as a table-schema collection before it is returned, so a
340
+ * malformed or hostile value throws a `VALIDATION` {@link DatabaseError} at
341
+ * `context.path === 'schema'` rather than surfacing a raw Contract or caller error.
250
342
  *
251
343
  * @param value - Unknown table schema collection
252
344
  * @returns Owned driver schema
@@ -265,7 +357,12 @@ function cloneDriverSchema(value) {
265
357
  }
266
358
  }
267
359
  /**
268
- * Clone unknown migration input into a distinct deeply frozen snapshot.
360
+ * Clones unknown migration input into a distinct deeply frozen snapshot.
361
+ *
362
+ * @remarks
363
+ * The clone is validated as a {@link MigrationInput} before it is returned, so a
364
+ * malformed or hostile value throws a `VALIDATION` {@link DatabaseError} at
365
+ * `context.path === 'migration'` rather than surfacing a raw Contract or caller error.
269
366
  *
270
367
  * @param value - Unknown migration input
271
368
  * @returns Owned migration input
@@ -286,8 +383,32 @@ function cloneMigrationInput(value) {
286
383
  //#endregion
287
384
  //#region src/core/helpers.ts
288
385
  /**
289
- * A total ordering over arbitrary values the comparator behind sorting and the
290
- * range operators.
386
+ * Validates the paging fields of a portable query.
387
+ *
388
+ * @remarks
389
+ * A present `limit` or `offset` must be a finite nonnegative integer; zero is
390
+ * valid. Validation is deterministic (`limit` before `offset`). Non-finite
391
+ * values are rendered as strings in error context so JSON serialization cannot
392
+ * collapse `NaN` or infinity to `null`.
393
+ *
394
+ * @param input - The portable query whose paging fields to validate
395
+ * @throws {@link DatabaseError} `VALIDATION` when a paging field is invalid
396
+ */
397
+ function validatePage(input) {
398
+ const limit = input?.limit;
399
+ if (limit !== void 0 && (!Number.isInteger(limit) || limit < 0)) throw new DatabaseError("VALIDATION", "Query limit must be a nonnegative integer", {
400
+ field: "limit",
401
+ value: Number.isFinite(limit) ? limit : String(limit)
402
+ });
403
+ const offset = input?.offset;
404
+ if (offset !== void 0 && (!Number.isInteger(offset) || offset < 0)) throw new DatabaseError("VALIDATION", "Query offset must be a nonnegative integer", {
405
+ field: "offset",
406
+ value: Number.isFinite(offset) ? offset : String(offset)
407
+ });
408
+ }
409
+ /**
410
+ * Compares two arbitrary values under one total order — the comparator behind
411
+ * sorting and the range operators.
291
412
  *
292
413
  * @remarks
293
414
  * Values of different types order by a fixed type rank (`undefined` < `null` <
@@ -311,15 +432,16 @@ function compareValues(left, right) {
311
432
  return 0;
312
433
  }
313
434
  /**
314
- * Structural equality by SameValueZero leaves — the comparator behind conformance
315
- * checks and any test/fixture that needs "same data", not "same reference".
435
+ * Compares two values structurally by SameValueZero leaves — the comparator
436
+ * behind conformance checks and any test/fixture that needs "same data", not
437
+ * "same reference".
316
438
  *
317
439
  * @remarks
318
440
  * Primitives compare by SameValueZero (`NaN` equals itself; `+0` equals `-0`).
319
441
  * Arrays compare by index (same length, every element `equalsValue`). Plain
320
- * records (via `isRecord`) compare by their OWN enumerable keys: same key
321
- * COUNT and, for every key in `left`, `right` has that key (`Object.hasOwn`)
322
- * with a `equalsValue` value — so a key present with value `undefined` is NOT
442
+ * records (through `isRecord`) compare by their own enumerable keys: same key
443
+ * count and, for every key in `left`, `right` has that key (`Object.hasOwn`)
444
+ * with a `equalsValue` value — so a key present with value `undefined` is not
323
445
  * equal to that key being absent (both differ in `Object.keys` membership).
324
446
  * Anything else (functions, class instances, mismatched shapes) falls through
325
447
  * to `false`. Container pairs are tracked iteratively, so self-referential and
@@ -328,7 +450,7 @@ function compareValues(left, right) {
328
450
  *
329
451
  * @param left - The left value
330
452
  * @param right - The right value
331
- * @returns Whether `left` and `right` are structurally equal
453
+ * @returns True if `left` and `right` are structurally equal; false otherwise
332
454
  *
333
455
  * @example
334
456
  * ```ts
@@ -385,47 +507,16 @@ function equalsValue(left, right) {
385
507
  }
386
508
  }
387
509
  /**
388
- * Match a query against a value as a case-insensitive ordered subsequence.
389
- *
390
- * @remarks
391
- * Every query character must appear in order in the value, but the characters
392
- * do not need to be contiguous. Query characters are literal, including
393
- * whitespace. Matching applies JavaScript `toLowerCase()` to both inputs
394
- * without locale-specific folding or Unicode normalization. An empty query
395
- * matches every value.
396
- *
397
- * @param value - The text searched for the query's characters
398
- * @param query - The characters that must all appear in order
399
- * @returns Whether the case-folded query is a subsequence of the case-folded value
400
- *
401
- * @example
402
- * ```ts
403
- * matchesFuzzy('Database', 'dbe') // true
404
- * matchesFuzzy('Database', 'abd') // false
405
- * ```
406
- */
407
- function matchesFuzzy(value, query) {
408
- const folded = value.toLowerCase();
409
- const wanted = query.toLowerCase();
410
- let cursor = 0;
411
- for (const char of wanted) {
412
- const found = folded.indexOf(char, cursor);
413
- if (found === -1) return false;
414
- cursor = found + 1;
415
- }
416
- return true;
417
- }
418
- /**
419
- * Match a value against a wildcard pattern in LINEAR time — the shared, ReDoS-SAFE
510
+ * Matches a value against a wildcard pattern in linear time — the shared, ReDoS-safe
420
511
  * engine behind {@link matchesLikePattern} and {@link matchesGlobPattern}.
421
512
  *
422
513
  * @remarks
423
- * A backtracking RegExp (`a%b%c` → `^a.*b.*c$`) is CATASTROPHIC on a hostile pattern:
514
+ * A backtracking RegExp (`a%b%c` → `^a.*b.*c$`) is catastrophic on a hostile pattern:
424
515
  * `.*` segments separated by literals, matched against a long non-matching input, blow
425
- * up super-linearly — and JS has no atomic groups / possessive quantifiers to bound it
426
- * (AGENTS §6.5, now that the authed server runs model-supplied `list` input over the
427
- * wire). So this builds NO regex. It runs the classic GREEDY TWO-POINTER wildcard match:
428
- * the `any` wildcard records its position and, on a later mismatch, backtracks ONLY to
516
+ * up super-linearly — and JS has no atomic groups / possessive quantifiers to bound it,
517
+ * while a `LIKE` / `GLOB` pattern is a caller-supplied operand this package cannot
518
+ * trust. So this builds no regex. It runs the classic greedy two-pointer wildcard match:
519
+ * the `any` wildcard records its position and, on a later mismatch, backtracks only to
429
520
  * that last `any` (letting it absorb one more char) — so the work is O(value × pattern),
430
521
  * never the exponential / polynomial backtracking a regex would do. The pattern length
431
522
  * is capped at {@link MAX_PATTERN_LENGTH} (a `VALIDATION` {@link DatabaseError} over it),
@@ -433,17 +524,17 @@ function matchesFuzzy(value, query) {
433
524
  * pattern.
434
525
  *
435
526
  * The `any` wildcard matches any run (including empty); `single` matches exactly one
436
- * char; every other pattern char matches itself LITERALLY (a pattern `.` / `(` / `\` is
527
+ * char; every other pattern char matches itself literally (a pattern `.` / `(` / `\` is
437
528
  * a literal — the regex-metacharacter hazard is gone with the regex). `any` is tested
438
- * BEFORE a literal match, so a value that literally contains the wildcard char never
439
- * shadows the wildcard. Case folding is applied to BOTH sides when `fold` is set.
529
+ * before a literal match, so a value that literally contains the wildcard char never
530
+ * shadows the wildcard. Case folding is applied to both sides when `fold` is set.
440
531
  *
441
532
  * @param value - The value to test
442
533
  * @param pattern - The wildcard pattern
443
534
  * @param any - The any-run wildcard char (`%` for `LIKE`, `*` for `GLOB`)
444
535
  * @param single - The single-char wildcard char (`_` for `LIKE`, `?` for `GLOB`)
445
- * @param fold - Whether to match case-INSENSITIVELY (`LIKE` folds; `GLOB` does not)
446
- * @returns Whether `value` matches `pattern`
536
+ * @param fold - Whether to match case-insensitively (`LIKE` folds; `GLOB` does not)
537
+ * @returns True if `value` matches `pattern`; false otherwise
447
538
  * @throws A `VALIDATION` {@link DatabaseError} when `pattern` exceeds {@link MAX_PATTERN_LENGTH}
448
539
  */
449
540
  function matchesWildcardPattern(value, pattern, any, single, fold) {
@@ -475,32 +566,73 @@ function matchesWildcardPattern(value, pattern, any, single, fold) {
475
566
  while (pi < needle.length && needle[pi] === any) pi += 1;
476
567
  return pi === needle.length;
477
568
  }
569
+ /**
570
+ * Matches a value against a SQL `LIKE` pattern, folding case.
571
+ *
572
+ * @remarks
573
+ * `%` matches any run of characters (including none) and `_` matches exactly one
574
+ * character; every other pattern character matches itself literally. Runs on
575
+ * {@link matchesWildcardPattern}, so the match is linear in the value length and
576
+ * the pattern is capped at {@link MAX_PATTERN_LENGTH}.
577
+ *
578
+ * @param value - The value to test
579
+ * @param pattern - The `LIKE` pattern
580
+ * @returns True if `value` matches `pattern` under case folding; false otherwise
581
+ * @throws A `VALIDATION` {@link DatabaseError} when `pattern` exceeds {@link MAX_PATTERN_LENGTH}
582
+ *
583
+ * @example
584
+ * ```ts
585
+ * matchesLikePattern('Hello', 'h%o') // true — `%` spans any run, and case folds
586
+ * matchesLikePattern('Hello', 'h_llo') // true — `_` matches exactly one character
587
+ * ```
588
+ */
478
589
  function matchesLikePattern(value, pattern) {
479
590
  return matchesWildcardPattern(value, pattern, "%", "_", true);
480
591
  }
592
+ /**
593
+ * Matches a value against a `GLOB` pattern, preserving case.
594
+ *
595
+ * @remarks
596
+ * `*` matches any run of characters (including none) and `?` matches exactly one
597
+ * character; every other pattern character matches itself literally, so a
598
+ * character class such as `[a-z]` is not interpreted. Runs on
599
+ * {@link matchesWildcardPattern}, so the match is linear in the value length and
600
+ * the pattern is capped at {@link MAX_PATTERN_LENGTH}.
601
+ *
602
+ * @param value - The value to test
603
+ * @param pattern - The `GLOB` pattern
604
+ * @returns True if `value` matches `pattern` case-sensitively; false otherwise
605
+ * @throws A `VALIDATION` {@link DatabaseError} when `pattern` exceeds {@link MAX_PATTERN_LENGTH}
606
+ *
607
+ * @example
608
+ * ```ts
609
+ * matchesGlobPattern('hello', 'h*o') // true — `*` spans any run
610
+ * matchesGlobPattern('Hello', 'h*o') // false — `GLOB` is case-sensitive
611
+ * ```
612
+ */
481
613
  function matchesGlobPattern(value, pattern) {
482
614
  return matchesWildcardPattern(value, pattern, "*", "?", false);
483
615
  }
484
616
  /**
485
- * Evaluate one {@link Condition} against a row — the per-operator predicate.
617
+ * Evaluates one {@link Condition} against a row — the per-operator predicate.
486
618
  *
487
619
  * @remarks
488
620
  * Reads the condition's column — a `FieldPath`, resolved with `resolveField` (a
489
621
  * string is one column; an array descends a nested value) — and applies the
490
622
  * operator. Range operators (`above` / `below` / `from` / `to` / `between`) use
491
623
  * {@link compareValues}, the total order; the equality family (`equals` / `not`
492
- * / `any` / `none`) uses {@link equalsValue} — STRUCTURAL equality, not the total
624
+ * / `any` / `none`) uses {@link equalsValue} — structural equality, not the total
493
625
  * order's rank-5-collapses-all-objects behavior, so `equals` on an object/array
494
626
  * operand only matches a structurally-equal value, never every row holding any
495
627
  * object. This is a semantics change from ranking: `equalsValue` is SameValueZero
496
628
  * on leaves, so `NaN` now equals `NaN` under `equals` / `any` (it never matched
497
629
  * anything under the old rank-based comparison). `like` / `glob` / `starts` /
498
630
  * `ends` match only strings; `absent` / `present` test nullishness. Total — a
499
- * type mismatch is simply a non-match.
631
+ * type mismatch is a non-match.
500
632
  *
501
633
  * @param row - The row to test
502
634
  * @param condition - The condition to apply
503
- * @returns Whether the row satisfies the condition
635
+ * @returns True if the row satisfies the condition; false otherwise
504
636
  */
505
637
  function matchesCondition(row, condition) {
506
638
  const value = (0, _orkestrel_contract.resolveField)(row, condition.column);
@@ -525,7 +657,7 @@ function matchesCondition(row, condition) {
525
657
  }
526
658
  }
527
659
  /**
528
- * Fold a row through a list of conditions, joining each by its connector.
660
+ * Folds a row through a list of conditions, joining each by its connector.
529
661
  *
530
662
  * @remarks
531
663
  * Evaluated left-to-right: the first condition seeds the result, and each later
@@ -535,7 +667,7 @@ function matchesCondition(row, condition) {
535
667
  *
536
668
  * @param row - The row to test
537
669
  * @param conditions - The conditions to fold
538
- * @returns Whether the row satisfies the combined conditions
670
+ * @returns True if the row satisfies the combined conditions; false otherwise
539
671
  */
540
672
  function matchesQuery(row, conditions) {
541
673
  let result = true;
@@ -550,7 +682,7 @@ function matchesQuery(row, conditions) {
550
682
  return result;
551
683
  }
552
684
  /**
553
- * Filter rows by a list of conditions — the shared basis for a table's count
685
+ * Filters rows by a list of conditions — the shared basis for a table's count
554
686
  * and aggregate paths (no sort/page, unlike {@link applyQuery}).
555
687
  *
556
688
  * @remarks
@@ -574,7 +706,7 @@ function filterRows(rows, conditions) {
574
706
  return rows.filter((row) => matchesQuery(row, conditions));
575
707
  }
576
708
  /**
577
- * Sort rows by an ordering specification, leaving the input untouched.
709
+ * Sorts rows by an ordering specification, leaving the input untouched.
578
710
  *
579
711
  * @remarks
580
712
  * Applies the terms in priority order — the first term that distinguishes two
@@ -596,7 +728,7 @@ function sortRows(rows, order) {
596
728
  return sorted;
597
729
  }
598
730
  /**
599
- * Apply a {@link QueryInput} to rows — filter, then sort, then page.
731
+ * Applies a {@link QueryInput} to rows — filter, then sort, then page.
600
732
  *
601
733
  * @remarks
602
734
  * The whole portable read pipeline in one place: conditions filter, `order`
@@ -621,7 +753,7 @@ function applyQuery(rows, input) {
621
753
  return result;
622
754
  }
623
755
  /**
624
- * Compute an aggregate over a column across rows.
756
+ * Computes an aggregate over a column across rows.
625
757
  *
626
758
  * @remarks
627
759
  * `count` returns the row count. The numeric aggregates coerce each cell with
@@ -647,10 +779,12 @@ function computeAggregate(rows, operation, column) {
647
779
  const total = numbers.reduce((sum, value) => sum + value, 0);
648
780
  return operation === "average" ? total / numbers.length : total;
649
781
  }
650
- return operation === "minimum" ? Math.min(...numbers) : Math.max(...numbers);
782
+ let result = operation === "minimum" ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
783
+ for (const value of numbers) result = operation === "minimum" ? Math.min(result, value) : Math.max(result, value);
784
+ return result;
651
785
  }
652
786
  /**
653
- * Read a row's primary key from a column, when it is a usable {@link Key}.
787
+ * Reads a row's primary key from a column, when it is a usable {@link Key}.
654
788
  *
655
789
  * @param row - The row to read
656
790
  * @param column - The primary-key column name
@@ -661,7 +795,7 @@ function extractKey(row, column) {
661
795
  return isKey(value) ? value : void 0;
662
796
  }
663
797
  /**
664
- * Return a fresh row whose primary column is authoritatively bound to its storage key.
798
+ * Returns a fresh row whose primary column is authoritatively bound to its storage key.
665
799
  *
666
800
  * @param row - The caller row
667
801
  * @param primary - The primary column
@@ -675,7 +809,7 @@ function bindRowKey(row, primary, key) {
675
809
  };
676
810
  }
677
811
  /**
678
- * Map a column's {@link ContractShape} to its portable {@link ColumnStorage} — the
812
+ * Maps a column's {@link ContractShape} to its portable {@link ColumnStorage} — the
679
813
  * value a `TableSchema` carries so a native backend can declare a real column.
680
814
  *
681
815
  * @remarks
@@ -699,7 +833,7 @@ function bindRowKey(row, primary, key) {
699
833
  * ```
700
834
  */
701
835
  function shapeToColumnStorage(shape) {
702
- switch (shape.type) {
836
+ switch (shape.category) {
703
837
  case "string": return "text";
704
838
  case "number": return shape.integer === true ? "integer" : "real";
705
839
  case "boolean": return "boolean";
@@ -718,7 +852,7 @@ function shapeToColumnStorage(shape) {
718
852
  }
719
853
  }
720
854
  /**
721
- * Project one contract shape into a portable column schema.
855
+ * Projects one contract shape into a portable column schema.
722
856
  *
723
857
  * @param name - The column name
724
858
  * @param shape - The column contract shape
@@ -734,7 +868,55 @@ function shapeToColumnSchema(name, shape) {
734
868
  };
735
869
  }
736
870
  /**
737
- * Throw when an {@link OperationOptions.signal | AbortSignal} has fired — the shared
871
+ * Reads one flat column's declaration out of a table schema.
872
+ *
873
+ * @remarks
874
+ * The single lookup behind every declared-column question — storage type,
875
+ * optionality, and nullability all come off the returned {@link ColumnSchema},
876
+ * so a caller that needs more than one of them reads them from one result. A
877
+ * nested {@link FieldPath} names no declared column, so resolve the path's head
878
+ * before calling. A schema that does not declare the column returns `undefined`.
879
+ *
880
+ * @param name - The flat column name
881
+ * @param schema - The table's schema
882
+ * @returns The column's {@link ColumnSchema}, or `undefined` when the schema does not declare it
883
+ *
884
+ * @example
885
+ * ```ts
886
+ * findColumn('age', schema)?.storage // 'integer'
887
+ * findColumn('absent', schema) // undefined
888
+ * ```
889
+ */
890
+ function findColumn(name, schema) {
891
+ return schema.columns.find((candidate) => candidate.name === name);
892
+ }
893
+ /**
894
+ * Resolves the primary-key column one table keys its rows by.
895
+ *
896
+ * @remarks
897
+ * A table absent from the {@link PrimaryMap} keys its rows by
898
+ * {@link DEFAULT_PRIMARY}, so this is total over any table name.
899
+ *
900
+ * @param primary - The per-table primary-key overrides
901
+ * @param name - The table name
902
+ * @returns The table's primary-key column
903
+ *
904
+ * @example
905
+ * ```ts
906
+ * resolvePrimary({ posts: 'slug' }, 'posts') // 'slug'
907
+ * resolvePrimary({ posts: 'slug' }, 'users') // 'id' — the default primary
908
+ * ```
909
+ */
910
+ function resolvePrimary(primary, name) {
911
+ return primary[name] ?? "id";
912
+ }
913
+ function requireColumns(tables, name) {
914
+ const columns = tables[name];
915
+ if (columns === void 0) throw new DatabaseError("NOT_FOUND", `Table '${name}' is not declared`, { table: name });
916
+ return columns;
917
+ }
918
+ /**
919
+ * Throws when an {@link OperationOptions.signal | AbortSignal} has fired — the shared
738
920
  * abort gate checked at operation boundaries and between streamed rows.
739
921
  *
740
922
  * @remarks
@@ -762,7 +944,7 @@ function checkAbort(signal) {
762
944
  if (signal?.aborted) throw new DatabaseError("ABORTED", "Operation aborted", { reason: signal.reason });
763
945
  }
764
946
  /**
765
- * Structurally diff a deployed and a declared table set into a {@link Migration}
947
+ * Diffs a deployed and a declared table set structurally into a {@link Migration}
766
948
  * plan.
767
949
  *
768
950
  * @remarks
@@ -777,17 +959,16 @@ function checkAbort(signal) {
777
959
  * plan labels only; versioning drivers persist and reconcile them through
778
960
  * {@link DriverMetadata}.
779
961
  *
780
- * A column present in BOTH schemas under the same name but with a different
962
+ * A column present in both schemas under the same name but with a different
781
963
  * `storage`, `optional`, or `nullable` value throws a `MIGRATION`
782
- * {@link DatabaseError} naming the
783
- * table, the column, and the from→to difference — a name-only diff would
784
- * otherwise silently produce NO step for the drift, and versioned
785
- * reconciliation would stamp over it. There is no automatic in-place
786
- * type-change step: the manual path is to add a new column, copy/convert the
787
- * data at the application layer, then remove the old column — two separate
788
- * plans, never a single implicit "alter" step.
789
- *
790
- * @param deployed - The table schemas currently applied
964
+ * {@link DatabaseError} naming the table, the column, and the from→to
965
+ * difference — a name-only diff would otherwise silently produce no step for
966
+ * the drift, and versioned reconciliation would stamp over it. There is no
967
+ * automatic in-place type-change step: the manual path is to add a new column,
968
+ * copy/convert the data at the application layer, then remove the old column
969
+ * two separate plans, never a single implicit "alter" step.
970
+ *
971
+ * @param deployed - The already-applied table schemas
791
972
  * @param declared - The table schemas the caller wants applied
792
973
  * @param from - The plan's source version label (defaults to `0`)
793
974
  * @param to - The plan's target version label (defaults to `1`)
@@ -893,7 +1074,9 @@ function planMigration(deployed, declared, from = 0, to = 1) {
893
1074
  } }).plan;
894
1075
  }
895
1076
  /**
896
- * Sequentially project migration steps over a canonical validated owned schema.
1077
+ * Projects migration steps sequentially over a canonical validated owned schema.
1078
+ *
1079
+ * @remarks
897
1080
  * Adding a required non-null column to an existing table rejects with
898
1081
  * `MIGRATION`; optional-only and nullable-only additions remain portable.
899
1082
  *
@@ -992,7 +1175,7 @@ function projectMigrationSchema(schema, steps) {
992
1175
  }
993
1176
  }
994
1177
  /**
995
- * Canonicalize an unknown driver schema into a distinct deeply frozen snapshot.
1178
+ * Canonicalizes an unknown driver schema into a distinct deeply frozen snapshot.
996
1179
  *
997
1180
  * @remarks
998
1181
  * Table and column lists are sorted by name. The index list is sorted by the
@@ -1014,11 +1197,11 @@ function normalizeDriverSchema(value) {
1014
1197
  return cloneDriverSchema(tables);
1015
1198
  }
1016
1199
  /**
1017
- * Apply one table's {@link MigrationStep}s to its rows — a pure row transform.
1200
+ * Applies one table's {@link MigrationStep}s to its rows — a pure row transform.
1018
1201
  *
1019
1202
  * @remarks
1020
1203
  * `column.remove` drops that field from every row (a fresh copy — inputs are
1021
- * never mutated, AGENTS §11); `column.add` leaves rows as-is (an absent field
1204
+ * never mutated); `column.add` leaves rows as-is (an absent field
1022
1205
  * reads as `undefined`, backfill is application policy). `table.add` /
1023
1206
  * `table.remove` / `index.add` / `index.remove` are no-ops here (they operate
1024
1207
  * on storage shape, not row shape). Steps for tables other than the one
@@ -1045,7 +1228,7 @@ function migrateRows(rows, steps) {
1045
1228
  });
1046
1229
  }
1047
1230
  /**
1048
- * Run the driver-conformance battery against a fresh {@link DriverInterface}
1231
+ * Walks the driver-conformance battery against a fresh {@link DriverInterface}
1049
1232
  * per phase, yielding one {@link ConformanceFinding} per violated invariant —
1050
1233
  * the shared invariant suite every backend (in-memory, SQLite, IndexedDB)
1051
1234
  * must uphold to be a drop-in {@link DriverInterface}.
@@ -1056,34 +1239,34 @@ function migrateRows(rows, steps) {
1056
1239
  * driver's own README. Opens a fixed two-table schema (`users` keyed by the
1057
1240
  * default `id`, `posts` keyed by a non-id `slug`) and, calling `factory()`
1058
1241
  * fresh for each phase so failures stay isolated, verifies: `open`/`close`;
1059
- * `read` of a missing key returns `undefined`; `write`/`read` round-trip with
1060
- * DEEP copy-in/copy-out isolation (mutating the caller's row — including a
1061
- * NESTED field — after `write`, or a row `read` returns, never perturbs
1062
- * stored state) and upsert-overwrite; simultaneous same-key `insert` calls
1063
- * produce exactly one commit and one `CONFLICT`; pre-aborted `write`,
1064
- * `insert`, and `delete` calls leave storage unchanged; `delete` returns
1065
- * `true` then `false`;
1066
- * `keys`/`scan` yield in ascending key order; `clear` empties only its target
1067
- * table; `snapshot`'s rollback thunk restores pre-snapshot state, including a
1068
- * NESTED field mutated in place on a read-back row between capture and
1069
- * restore; a scoped `snapshot(['users'])` rolls back only the named table,
1070
- * leaving a concurrent mutation to another table intact; a
1071
- * non-`id` primary key (`posts.slug`) round-trips; a nested-object row
1072
- * round-trips structurally (via {@link equalsValue}). The optional surface is
1073
- * presence-gated: when `migrate` exists, a `column.remove` plan strips the
1074
- * column from stored rows and a plan referencing an unknown table throws
1075
- * `DatabaseError` `MIGRATION`; when `stream` exists, it yields only
1076
- * condition-matching rows and honors `offset`/`limit`; when `transaction`
1077
- * exists, `commit` persists and `rollback` restores; when both `metadata` and
1078
- * `stamp` exist, a fresh store's `metadata()` is `undefined`, and after
1242
+ * `read` of a missing key returns `undefined`; `write`/`read` round-trip
1243
+ * with deep copy-in/copy-out isolation (mutating the caller's row —
1244
+ * including a nested field — after `write`, or a row `read` returns, never
1245
+ * perturbs stored state) and upsert-overwrite; simultaneous same-key
1246
+ * `insert` calls produce exactly one commit and one `CONFLICT`; pre-aborted
1247
+ * `write`, `insert`, and `delete` calls leave storage unchanged; `delete`
1248
+ * returns `true` then `false`; `keys`/`scan` yield in ascending key order;
1249
+ * `clear` empties only its target table; `snapshot`'s rollback thunk
1250
+ * restores pre-snapshot state, including a nested field mutated in place on
1251
+ * a read-back row between capture and restore; a scoped
1252
+ * `snapshot(['users'])` rolls back only the named table, leaving a
1253
+ * concurrent mutation to another table intact; a non-`id` primary key
1254
+ * (`posts.slug`) round-trips; a nested-object row round-trips structurally
1255
+ * (through {@link equalsValue}). The optional surface is presence-gated: when
1256
+ * `migrate` exists, a `column.remove` plan strips the column from stored
1257
+ * rows and a plan referencing an unknown table throws `DatabaseError`
1258
+ * `MIGRATION`; when `stream` exists, it yields only condition-matching rows
1259
+ * and honors `offset`/`limit`; when `transaction` exists, `commit` persists
1260
+ * and `rollback` restores; when both `metadata` and `stamp` exist, a fresh
1261
+ * store's `metadata()` is `undefined`, and after
1079
1262
  * `stamp({ version, schema })`, `metadata()` returns the exact stamped value.
1080
1263
  *
1081
- * Each phase runs within a `try`/`catch`: an EXPECTED mismatch yields a
1082
- * finding built from the assertion, while an UNEXPECTED throw (a driver
1264
+ * Each phase runs within a `try`/`catch`: an expected mismatch yields a
1265
+ * finding built from the assertion, while an unexpected throw (a driver
1083
1266
  * crash mid-phase) is caught and yielded as a finding too, naming the phase
1084
1267
  * as `check` and carrying the caught error in `context.error` — a broken
1085
1268
  * driver can never escape the battery as an unhandled rejection. Within a
1086
- * phase, the FIRST violated assertion yields and the phase stops (matching
1269
+ * phase, the first violated assertion yields and the phase stops (matching
1087
1270
  * the historical fail-fast shape at phase granularity); the generator then
1088
1271
  * moves on to the next phase regardless. Because this is a **generator**,
1089
1272
  * consuming only the first yielded value reproduces true fail-fast (later
@@ -1094,62 +1277,14 @@ function migrateRows(rows, steps) {
1094
1277
  *
1095
1278
  * @example
1096
1279
  * ```ts
1097
- * import { createMemoryDriver, driverFindings } from '@orkestrel/database'
1280
+ * import { createMemoryDriver, scanDriver } from '@orkestrel/database'
1098
1281
  *
1099
- * for await (const finding of driverFindings(() => createMemoryDriver())) {
1282
+ * for await (const finding of scanDriver(() => createMemoryDriver())) {
1100
1283
  * console.log(finding.check, finding.message)
1101
1284
  * }
1102
1285
  * ```
1103
1286
  */
1104
- async function* driverFindings(factory) {
1105
- const CONFORMANCE_USERS_SCHEMA = {
1106
- name: "users",
1107
- primary: "id",
1108
- columns: [
1109
- {
1110
- name: "id",
1111
- storage: "text",
1112
- optional: false,
1113
- nullable: false
1114
- },
1115
- {
1116
- name: "name",
1117
- storage: "text",
1118
- optional: false,
1119
- nullable: false
1120
- },
1121
- {
1122
- name: "age",
1123
- storage: "integer",
1124
- optional: true,
1125
- nullable: false
1126
- },
1127
- {
1128
- name: "meta",
1129
- storage: "json",
1130
- optional: true,
1131
- nullable: false
1132
- }
1133
- ],
1134
- indexes: []
1135
- };
1136
- const CONFORMANCE_POSTS_SCHEMA = {
1137
- name: "posts",
1138
- primary: "slug",
1139
- columns: [{
1140
- name: "slug",
1141
- storage: "text",
1142
- optional: false,
1143
- nullable: false
1144
- }, {
1145
- name: "title",
1146
- storage: "text",
1147
- optional: false,
1148
- nullable: false
1149
- }],
1150
- indexes: []
1151
- };
1152
- const CONFORMANCE_SCHEMA = [CONFORMANCE_USERS_SCHEMA, CONFORMANCE_POSTS_SCHEMA];
1287
+ async function* scanDriver(factory) {
1153
1288
  try {
1154
1289
  const driver = factory();
1155
1290
  await driver.open(CONFORMANCE_SCHEMA);
@@ -1936,17 +2071,19 @@ async function* driverFindings(factory) {
1936
2071
  }
1937
2072
  }
1938
2073
  /**
1939
- * Run the driver-conformance battery, throwing on the first violated
2074
+ * Runs the driver-conformance battery, throwing on the first violated
1940
2075
  * invariant — the fail-fast entry point most callers (test setup, CI smoke
1941
2076
  * checks) want.
1942
2077
  *
1943
2078
  * @remarks
1944
- * A thin driver over {@link driverFindings}: because that generator is
1945
- * lazy, consuming only its first yielded value means every LATER phase
1946
- * never runs — true fail-fast, not merely "report only the first". The
2079
+ * Consumes only the first value {@link scanDriver} yields: because that
2080
+ * generator is lazy, every later phase never runs true fail-fast, not
2081
+ * merely "report only the first". The
1947
2082
  * thrown error is byte-compatible with the historical shape: a
1948
2083
  * `CONFORMANCE` {@link DatabaseError} whose `message` is the finding's
1949
- * `message` and whose `context` is `{ check, ...finding.context }`.
2084
+ * `message` and whose `context` is `{ check, ...finding.context }`. The battery
2085
+ * takes a driver factory and reports through a throw, so it binds no test
2086
+ * framework and runs from any runner.
1950
2087
  *
1951
2088
  * @param factory - Mints a fresh, unopened driver instance (called once per phase)
1952
2089
  * @returns Nothing — resolves once every phase has passed
@@ -1960,18 +2097,18 @@ async function* driverFindings(factory) {
1960
2097
  * ```
1961
2098
  */
1962
2099
  async function conformDriver(factory) {
1963
- for await (const finding of driverFindings(factory)) throw new DatabaseError("CONFORMANCE", finding.message, {
2100
+ for await (const finding of scanDriver(factory)) throw new DatabaseError("CONFORMANCE", finding.message, {
1964
2101
  check: finding.check,
1965
2102
  ...finding.context
1966
2103
  });
1967
2104
  }
1968
2105
  /**
1969
- * Run the FULL driver-conformance battery and collect every violation — the
2106
+ * Runs the full driver-conformance battery and collects every violation — the
1970
2107
  * audit entry point for a driver author who wants a complete report rather
1971
2108
  * than a single fail-fast throw.
1972
2109
  *
1973
2110
  * @remarks
1974
- * Drains {@link driverFindings} to completion: every phase runs regardless
2111
+ * Drains {@link scanDriver} to completion: every phase runs regardless
1975
2112
  * of earlier violations, so a driver breaking two independent invariants
1976
2113
  * reports both. An empty array means the driver is fully conformant.
1977
2114
  *
@@ -1988,26 +2125,32 @@ async function conformDriver(factory) {
1988
2125
  */
1989
2126
  async function auditDriver(factory) {
1990
2127
  const findings = [];
1991
- for await (const finding of driverFindings(factory)) findings.push(finding);
2128
+ for await (const finding of scanDriver(factory)) findings.push(finding);
1992
2129
  return findings;
1993
2130
  }
1994
2131
  //#endregion
1995
- //#region src/core/TransactionIterator.ts
2132
+ //#region src/core/ScopedIterator.ts
1996
2133
  /**
1997
- * The internal continuation boundary for one transaction-scoped async iterable.
2134
+ * Forms the internal continuation admission boundary for one scoped async iterable.
1998
2135
  *
1999
2136
  * @remarks
2000
- * Each active continuation enters the owning transaction ledger independently,
2001
- * so an idle iterator never pins settlement. A continuation requested after
2002
- * admission closes rejects while still attempting source cleanup exactly once.
2137
+ * Each continuation enters the owning {@link AdmissionInterface} ledger
2138
+ * independently, so an idle iterator never delays a transaction, a settlement,
2139
+ * or a close. `ready` runs inside the tracked continuation before the source
2140
+ * advances, which is where a root stream re-establishes the lazy connection and
2141
+ * a transaction-scoped stream does nothing. A continuation requested after
2142
+ * admission closes attempts source cleanup exactly once and leaves the iterator
2143
+ * terminal.
2003
2144
  */
2004
- var TransactionIterator = class {
2145
+ var ScopedIterator = class {
2005
2146
  #source;
2006
- #scope;
2147
+ #admission;
2148
+ #ready;
2007
2149
  #cleaned = false;
2008
- constructor(source, scope) {
2150
+ constructor(source, admission, ready) {
2009
2151
  this.#source = source[Symbol.asyncIterator]();
2010
- this.#scope = scope;
2152
+ this.#admission = admission;
2153
+ this.#ready = ready;
2011
2154
  }
2012
2155
  [Symbol.asyncIterator]() {
2013
2156
  return this;
@@ -2026,6 +2169,7 @@ var TransactionIterator = class {
2026
2169
  done: true,
2027
2170
  value: void 0
2028
2171
  };
2172
+ await this.#ready();
2029
2173
  const result = await this.#source.next();
2030
2174
  if (result.done === true) this.#cleaned = true;
2031
2175
  return result;
@@ -2053,8 +2197,8 @@ var TransactionIterator = class {
2053
2197
  throw error;
2054
2198
  }
2055
2199
  #continue(operation) {
2056
- if (!this.#scope.accepting) this.#cleanup();
2057
- return this.#scope.track(operation);
2200
+ if (!this.#admission.accepting) this.#cleanup();
2201
+ return this.#admission.track(operation);
2058
2202
  }
2059
2203
  #cleanup() {
2060
2204
  if (this.#cleaned) return;
@@ -2067,7 +2211,7 @@ var TransactionIterator = class {
2067
2211
  //#endregion
2068
2212
  //#region src/core/TransactionScope.ts
2069
2213
  /**
2070
- * The internal lifetime boundary for one database transaction callback.
2214
+ * Forms the internal lifetime boundary for one database transaction callback.
2071
2215
  *
2072
2216
  * @remarks
2073
2217
  * Promise operations enter synchronously through {@link track}. Closing stops new
@@ -2111,7 +2255,7 @@ var TransactionScope = class {
2111
2255
  return promise;
2112
2256
  }
2113
2257
  stream(source) {
2114
- return new TransactionIterator(source, this);
2258
+ return new ScopedIterator(source, this, () => Promise.resolve());
2115
2259
  }
2116
2260
  stop() {
2117
2261
  this.#accepting = false;
@@ -2124,7 +2268,7 @@ var TransactionScope = class {
2124
2268
  //#endregion
2125
2269
  //#region src/core/DatabaseContext.ts
2126
2270
  /**
2127
- * The internal shared owner behind every typed view of one database.
2271
+ * Owns the internal shared state behind every typed view of one database.
2128
2272
  *
2129
2273
  * @remarks
2130
2274
  * A context owns the driver, merged physical schema, lifecycle, observation,
@@ -2208,7 +2352,21 @@ var DatabaseContext = class {
2208
2352
  this.#emitter.emit("close");
2209
2353
  }
2210
2354
  connect() {
2211
- return this.#connect();
2355
+ if (this.#ready !== void 0) return this.#ready;
2356
+ if (this.#failure !== void 0) throw this.#failure.error;
2357
+ if (this.#status === "closed") throw new DatabaseError("CLOSED", `Database '${this.#name}' is closed`, { name: this.#name });
2358
+ const readiness = this.#driver.open(this.#schema).then(async () => {
2359
+ if (this.#status === "idle") {
2360
+ this.#status = "open";
2361
+ this.#emitter.emit("open");
2362
+ }
2363
+ await this.#reconcile();
2364
+ }).catch((error) => {
2365
+ if (this.#ready === readiness) this.#ready = void 0;
2366
+ throw error;
2367
+ });
2368
+ this.#ready = readiness;
2369
+ return readiness;
2212
2370
  }
2213
2371
  track(operation) {
2214
2372
  try {
@@ -2237,7 +2395,7 @@ var DatabaseContext = class {
2237
2395
  this.#transaction = token;
2238
2396
  try {
2239
2397
  await this.#drain();
2240
- await this.#connect();
2398
+ await this.connect();
2241
2399
  if (this.#driver.transaction !== void 0) {
2242
2400
  const rejection = {
2243
2401
  rejected: false,
@@ -2313,23 +2471,6 @@ var DatabaseContext = class {
2313
2471
  #outside() {
2314
2472
  if (this.#transaction !== void 0) throw new DatabaseError("CONFLICT", `Database '${this.#name}' has an active transaction`, { name: this.#name });
2315
2473
  }
2316
- #connect() {
2317
- if (this.#ready !== void 0) return this.#ready;
2318
- if (this.#failure !== void 0) throw this.#failure.error;
2319
- if (this.#status === "closed") throw new DatabaseError("CLOSED", `Database '${this.#name}' is closed`, { name: this.#name });
2320
- const readiness = this.#driver.open(this.#schema).then(async () => {
2321
- if (this.#status === "idle") {
2322
- this.#status = "open";
2323
- this.#emitter.emit("open");
2324
- }
2325
- await this.#reconcile();
2326
- }).catch((error) => {
2327
- if (this.#ready === readiness) this.#ready = void 0;
2328
- throw error;
2329
- });
2330
- this.#ready = readiness;
2331
- return readiness;
2332
- }
2333
2474
  async #drain() {
2334
2475
  if (this.#operations.size === 0) return;
2335
2476
  await Promise.allSettled(this.#operations);
@@ -2412,12 +2553,12 @@ var DatabaseContext = class {
2412
2553
  //#endregion
2413
2554
  //#region src/core/Cursor.ts
2414
2555
  /**
2415
- * A forward row cursor for bulk in-place mutation.
2556
+ * Walks a table's rows forward for bulk in-place mutation.
2416
2557
  *
2417
2558
  * @remarks
2418
2559
  * Iterates a snapshot of the table's keys captured when the cursor was opened,
2419
2560
  * reading each row lazily through the owning table — so a mutation made during
2420
- * iteration cannot corrupt the walk, and a key removed mid-iteration is simply
2561
+ * iteration cannot corrupt the walk, and a key removed mid-iteration is
2421
2562
  * skipped. `update` and `remove` act on the row at the current position.
2422
2563
  */
2423
2564
  var Cursor = class {
@@ -2504,84 +2645,9 @@ var Cursor = class {
2504
2645
  }
2505
2646
  };
2506
2647
  //#endregion
2507
- //#region src/core/DatabaseIterator.ts
2508
- /**
2509
- * The internal continuation admission boundary for a root database stream.
2510
- *
2511
- * @remarks
2512
- * Each continuation enters the shared root operation ledger independently, so
2513
- * an idle iterator never delays a transaction or close. A continuation rejected
2514
- * after transaction or close admission closes attempts source cleanup exactly
2515
- * once and leaves the iterator terminal.
2516
- */
2517
- var DatabaseIterator = class {
2518
- #source;
2519
- #context;
2520
- #cleaned = false;
2521
- constructor(source, context) {
2522
- this.#source = source[Symbol.asyncIterator]();
2523
- this.#context = context;
2524
- }
2525
- [Symbol.asyncIterator]() {
2526
- return this;
2527
- }
2528
- next() {
2529
- return this.#continue(() => this.#next());
2530
- }
2531
- return() {
2532
- return this.#continue(() => this.#return());
2533
- }
2534
- throw(error) {
2535
- return this.#continue(() => this.#throw(error));
2536
- }
2537
- async #next() {
2538
- if (this.#cleaned) return {
2539
- done: true,
2540
- value: void 0
2541
- };
2542
- await this.#context.connect();
2543
- const result = await this.#source.next();
2544
- if (result.done === true) this.#cleaned = true;
2545
- return result;
2546
- }
2547
- async #return() {
2548
- if (this.#cleaned || this.#source.return === void 0) {
2549
- this.#cleaned = true;
2550
- return {
2551
- done: true,
2552
- value: void 0
2553
- };
2554
- }
2555
- this.#cleaned = true;
2556
- return this.#source.return();
2557
- }
2558
- async #throw(error) {
2559
- if (this.#source.throw !== void 0) {
2560
- const result = await this.#source.throw(error);
2561
- if (result.done === true) this.#cleaned = true;
2562
- return result;
2563
- }
2564
- try {
2565
- await this.#return();
2566
- } catch {}
2567
- throw error;
2568
- }
2569
- #continue(operation) {
2570
- if (!this.#context.accepting) this.#cleanup();
2571
- return this.#context.track(operation);
2572
- }
2573
- #cleanup() {
2574
- if (this.#cleaned) return;
2575
- this.#cleaned = true;
2576
- try {
2577
- (this.#source.return?.())?.catch(() => {});
2578
- } catch {}
2579
- }
2580
- };
2581
- //#endregion
2582
2648
  //#region src/core/Query.ts
2583
2649
  /**
2584
- * A fluent query builder bound to one table.
2650
+ * Builds a read against one table through a fluent chain.
2585
2651
  *
2586
2652
  * @remarks
2587
2653
  * Accumulates typed conditions, ordering, JS filters, and a page. Each builder
@@ -2642,7 +2708,7 @@ var Query = class {
2642
2708
  return this.#filtered(fetched).length;
2643
2709
  }
2644
2710
  /**
2645
- * Lazily evaluate conditions, filters, offset, and limit.
2711
+ * Evaluates conditions, filters, offset, and limit lazily.
2646
2712
  *
2647
2713
  * @param options - Optional abort options
2648
2714
  * @returns Matching rows in storage order
@@ -2694,23 +2760,23 @@ var Query = class {
2694
2760
  //#endregion
2695
2761
  //#region src/core/Table.ts
2696
2762
  /**
2697
- * A table — typed keyed CRUD plus fluent query and cursor access over a driver.
2763
+ * Exposes typed keyed CRUD plus fluent query and cursor access over a driver.
2698
2764
  *
2699
2765
  * @remarks
2700
2766
  * The table's contract is the load-bearing piece: writes go through `parse`
2701
2767
  * (coercing inputs and rejecting rows that don't fit with a `VALIDATION` throw),
2702
2768
  * reads come back through the contract guard (narrowing a stored {@link Row} to
2703
- * the table's type — no assertion, AGENTS §1), and `contract` is exposed for
2769
+ * the table's type — no assertion), and `contract` is exposed for
2704
2770
  * introspection and seeding. The driver only stores and scans; all querying is
2705
2771
  * the shared core engine in `helpers.ts`.
2706
2772
  *
2707
2773
  * @remarks
2708
- * - **Observable (§13).** The owned {@link emitter} ({@link TableEventMap}) carries the
2774
+ * - **Observable.** The owned {@link emitter} ({@link TableEventMap}) carries the
2709
2775
  * per-row mutation moments — `write` (set / add / update), `remove`, `clear` — for
2710
- * fire-and-forget observers (cache invalidation, sync, an audit log), ALONGSIDE the
2711
- * database-level lifecycle. Events carry the affected KEY only (no value payload, to
2776
+ * fire-and-forget observers (cache invalidation, sync, an audit log), alongside the
2777
+ * database-level lifecycle. Events carry the affected key only (no value payload, to
2712
2778
  * keep fan-out lean); reads / queries / counts are not emitted. Every event is emitted
2713
- * directly, strictly AFTER the driver write / delete / clear completes; the emitter
2779
+ * directly, strictly after the driver write / delete / clear completes; the emitter
2714
2780
  * isolates a listener throw and routes it to its `error` handler (the `error` option),
2715
2781
  * so a buggy observer can never corrupt a write or perturb a transaction.
2716
2782
  */
@@ -2794,7 +2860,7 @@ var Table = class {
2794
2860
  });
2795
2861
  }
2796
2862
  /**
2797
- * Count contract-valid rows matching `input`'s conditions.
2863
+ * Counts contract-valid rows matching `input`'s conditions.
2798
2864
  *
2799
2865
  * @remarks
2800
2866
  * Paging is ignored. Candidate rows use the driver's native `records` hook
@@ -2819,11 +2885,11 @@ var Table = class {
2819
2885
  });
2820
2886
  }
2821
2887
  /**
2822
- * Compute an aggregate over `column` across rows matching `input`'s
2888
+ * Computes an aggregate over `column` across rows matching `input`'s
2823
2889
  * conditions.
2824
2890
  *
2825
2891
  * @remarks
2826
- * Like {@link count}, `aggregate` operates on STORED rows WITHOUT the
2892
+ * Unlike {@link count}, `aggregate` operates on stored rows without the
2827
2893
  * contract guard {@link records} / {@link scan} apply — a non-conforming
2828
2894
  * stored row still contributes to the computed aggregate when it matches
2829
2895
  * the conditions, even though it would never appear in `records()`'s
@@ -2848,7 +2914,7 @@ var Table = class {
2848
2914
  });
2849
2915
  }
2850
2916
  /**
2851
- * Stream the table's rows matching `input`, applying offset/limit paging.
2917
+ * Streams the table's rows matching `input`, applying offset/limit paging.
2852
2918
  *
2853
2919
  * @remarks
2854
2920
  * `input.limit` counts rows that pass both the input conditions and the
@@ -2863,7 +2929,7 @@ var Table = class {
2863
2929
  scan(input, options) {
2864
2930
  validatePage(input);
2865
2931
  const source = this.#scan(input, options);
2866
- if (this.#context !== void 0) return new DatabaseIterator(source, this.#context);
2932
+ if (this.#context !== void 0) return new ScopedIterator(source, this.#context, this.#ready);
2867
2933
  return this.#scope === void 0 ? source : this.#scope.stream(source);
2868
2934
  }
2869
2935
  set(rows, options) {
@@ -3107,7 +3173,7 @@ var Table = class {
3107
3173
  //#endregion
3108
3174
  //#region src/core/DatabaseTransaction.ts
3109
3175
  /**
3110
- * A table-only database view bound to one driver transaction scope.
3176
+ * Binds a table-only database view to one driver transaction scope.
3111
3177
  *
3112
3178
  * @typeParam T - The declared table shape map
3113
3179
  *
@@ -3134,31 +3200,27 @@ var DatabaseTransaction = class {
3134
3200
  }
3135
3201
  table(name) {
3136
3202
  this.#scope.check();
3137
- const columns = this.#columns(name);
3138
- return this.#build(name, this.#key(name), (0, _orkestrel_contract.createContract)((0, _orkestrel_contract.objectShape)(columns)));
3203
+ const columns = requireColumns(this.#tables, name);
3204
+ return this.#build(name, resolvePrimary(this.#primary, name), (0, _orkestrel_contract.createContract)((0, _orkestrel_contract.objectShape)(columns)));
3139
3205
  }
3140
3206
  #build(name, key, contract) {
3141
3207
  return new Table(() => Promise.resolve(), this.#driver, name, key, contract, this.#generate, this.#error, void 0, this.#scope);
3142
3208
  }
3143
- #key(name) {
3144
- return this.#primary[name] ?? "id";
3145
- }
3146
- #columns(name) {
3147
- const columns = this.#tables[name];
3148
- if (columns === void 0) throw new DatabaseError("NOT_FOUND", `Table '${name}' is not declared`, { table: name });
3149
- return columns;
3150
- }
3151
3209
  };
3152
3210
  //#endregion
3153
3211
  //#region src/core/Database.ts
3154
3212
  /**
3155
- * A typed database view over one shared internal lifecycle and storage context.
3213
+ * Exposes a typed view over one shared internal lifecycle and storage context.
3156
3214
  *
3157
3215
  * @remarks
3158
3216
  * Each view owns only its table contracts, primary columns, indexes, and key
3159
3217
  * generator. Imported views register their physical schemas with the same
3160
3218
  * internal context before opening begins, so every view observes one driver,
3161
3219
  * merged schema, emitter, status, transaction boundary, and terminal close.
3220
+ *
3221
+ * The view owns the driver and its declared `tables`, connects that driver lazily on
3222
+ * first use, `import`s further tables and `export`s their portable definitions, and
3223
+ * runs `transaction` scopes over the shared context.
3162
3224
  */
3163
3225
  var Database = class Database {
3164
3226
  #context;
@@ -3185,8 +3247,8 @@ var Database = class Database {
3185
3247
  }
3186
3248
  table(name) {
3187
3249
  if (this.#context.status === "closed") throw new DatabaseError("CLOSED", `Database '${this.#context.name}' is closed`, { name: this.#context.name });
3188
- const columns = this.#columns(name);
3189
- return this.#build(name, this.#key(name), (0, _orkestrel_contract.createContract)((0, _orkestrel_contract.objectShape)(columns)));
3250
+ const columns = requireColumns(this.#tables, name);
3251
+ return this.#build(name, resolvePrimary(this.#primary, name), (0, _orkestrel_contract.createContract)((0, _orkestrel_contract.objectShape)(columns)));
3190
3252
  }
3191
3253
  import(tables, primary) {
3192
3254
  return this.#spawn(tables, {
@@ -3197,9 +3259,9 @@ var Database = class Database {
3197
3259
  export() {
3198
3260
  const result = {};
3199
3261
  for (const name of Object.keys(this.#tables)) {
3200
- const columns = this.#columns(name);
3262
+ const columns = requireColumns(this.#tables, name);
3201
3263
  result[name] = {
3202
- primary: this.#key(name),
3264
+ primary: resolvePrimary(this.#primary, name),
3203
3265
  columns,
3204
3266
  schema: (0, _orkestrel_contract.compileSchema)((0, _orkestrel_contract.objectShape)(columns))
3205
3267
  };
@@ -3235,20 +3297,12 @@ var Database = class Database {
3235
3297
  ...this.#context.version === void 0 ? {} : { version: this.#context.version }
3236
3298
  }, this.#context);
3237
3299
  }
3238
- #key(name) {
3239
- return this.#primary[name] ?? "id";
3240
- }
3241
- #columns(name) {
3242
- const columns = this.#tables[name];
3243
- if (columns === void 0) throw new DatabaseError("NOT_FOUND", `Table '${name}' is not declared`, { table: name });
3244
- return columns;
3245
- }
3246
3300
  #schema() {
3247
3301
  return Object.keys(this.#tables).map((name) => {
3248
- const columns = this.#columns(name);
3302
+ const columns = requireColumns(this.#tables, name);
3249
3303
  return {
3250
3304
  name,
3251
- primary: this.#key(name),
3305
+ primary: resolvePrimary(this.#primary, name),
3252
3306
  columns: Object.entries(columns).map(([column, shape]) => shapeToColumnSchema(column, shape)),
3253
3307
  indexes: this.#indexes[name] ?? []
3254
3308
  };
@@ -3284,16 +3338,16 @@ var Database = class Database {
3284
3338
  //#endregion
3285
3339
  //#region src/core/drivers/MemoryDriver.ts
3286
3340
  /**
3287
- * The reference {@link DriverInterface} — nested maps, no I/O.
3341
+ * Implements the reference {@link DriverInterface} — nested maps, no I/O.
3288
3342
  *
3289
3343
  * @remarks
3290
3344
  * The in-between made concrete: it runs identically in a browser or on a server,
3291
3345
  * so it is the storage behind tests, ephemeral caches, and any code that wants
3292
- * the database API without a persistent backend. Rows are DEEP-copied (via
3346
+ * the database API without a persistent backend. Rows are deep-copied (through
3293
3347
  * `structuredClone`) in and out — at `write`, `read`, `scan`, `stream`, and both
3294
3348
  * snapshot capture and restore — so a caller mutating a nested field of an input
3295
3349
  * row, a returned row, or a row mutated in place between snapshot and rollback
3296
- * can never perturb stored state (AGENTS §11); a shallow `{ ...row }` spread
3350
+ * can never perturb stored state; a shallow `{ ...row }` spread
3297
3351
  * would still share nested object/array references. Metadata instead routes
3298
3352
  * through `cloneDriverMetadata`: `stamp` and migration snapshot exact JSON at
3299
3353
  * ingress, and `metadata` returns a distinct deeply frozen owned copy. `snapshot`
@@ -3355,19 +3409,19 @@ var MemoryDriver = class {
3355
3409
  }
3356
3410
  }
3357
3411
  /**
3358
- * Natively filtered lazy iteration — the {@link DriverInterface.stream} hook.
3412
+ * Iterates rows lazily with native filtering — the {@link DriverInterface.stream} hook.
3359
3413
  *
3360
3414
  * @remarks
3361
3415
  * Iterates the table's keys in the same key order `scan` and `keys` yield
3362
3416
  * (sorted by {@link compareValues}), testing each row against
3363
- * `input.conditions` (via {@link matchesQuery}) before counting it
3417
+ * `input.conditions` (through {@link matchesQuery}) before counting it
3364
3418
  * toward `offset` / `limit`. Both are applied lazily as matches are found —
3365
3419
  * `offset` matches are skipped without being yielded, and iteration stops the
3366
3420
  * instant `limit` yields have been produced, so a large table is never fully
3367
- * walked for a small page. `input.order` is IGNORED (the same contract as
3421
+ * walked for a small page. `input.order` is ignored (the same contract as
3368
3422
  * `TableInterface.scan` and `QueryInterface.stream`): streaming yields key
3369
- * order, sorted output is `records()`'s job. Rows yield copy-out (AGENTS
3370
- * §11), and an unknown table mirrors `scan`'s empty-yield behavior.
3423
+ * order, sorted output is `records()`'s job. Rows yield copy-out, and an
3424
+ * unknown table mirrors `scan`'s empty-yield behavior.
3371
3425
  *
3372
3426
  * @param table - The table to stream
3373
3427
  * @param input - The filter / offset / limit to apply lazily
@@ -3383,31 +3437,11 @@ var MemoryDriver = class {
3383
3437
  validatePage(input);
3384
3438
  return this.#stream(table, input);
3385
3439
  }
3386
- async *#stream(table, input) {
3387
- const store = this.#store(table);
3388
- const conditions = input.conditions;
3389
- const offset = input.offset ?? 0;
3390
- const limit = input.limit;
3391
- let skipped = 0;
3392
- let yielded = 0;
3393
- for (const key of this.#ordered(table)) {
3394
- if (limit !== void 0 && yielded >= limit) return;
3395
- const row = store.get(key);
3396
- if (row === void 0) continue;
3397
- if (conditions !== void 0 && conditions.length > 0 && !matchesQuery(row, conditions)) continue;
3398
- if (skipped < offset) {
3399
- skipped += 1;
3400
- continue;
3401
- }
3402
- yield structuredClone(row);
3403
- yielded += 1;
3404
- }
3405
- }
3406
3440
  async clear(table) {
3407
3441
  this.#store(table).clear();
3408
3442
  }
3409
3443
  /**
3410
- * Capture the current state and return a thunk that rolls back to it.
3444
+ * Captures the current state and returns a thunk that rolls back to it.
3411
3445
  *
3412
3446
  * @remarks
3413
3447
  * Capture owns rows, schema, and one session-local table identity. Replay
@@ -3464,7 +3498,7 @@ var MemoryDriver = class {
3464
3498
  };
3465
3499
  }
3466
3500
  /**
3467
- * Return the persisted {@link DriverMetadata}, or `undefined` when the store has
3501
+ * Returns the persisted {@link DriverMetadata}, or `undefined` when the store has
3468
3502
  * never been stamped.
3469
3503
  *
3470
3504
  * @remarks
@@ -3479,7 +3513,7 @@ var MemoryDriver = class {
3479
3513
  return this.#metadata === void 0 ? void 0 : cloneDriverMetadata(this.#metadata);
3480
3514
  }
3481
3515
  /**
3482
- * Persist an owned snapshot for a later `metadata()` to return.
3516
+ * Persists an owned snapshot for a later `metadata()` to return.
3483
3517
  *
3484
3518
  * @param metadata - The {@link DriverMetadata} to persist
3485
3519
  */
@@ -3487,7 +3521,7 @@ var MemoryDriver = class {
3487
3521
  this.#metadata = cloneDriverMetadata(metadata);
3488
3522
  }
3489
3523
  /**
3490
- * Apply a {@link Migration} plan's steps against the in-memory store.
3524
+ * Applies a {@link Migration} plan's steps against the in-memory store.
3491
3525
  *
3492
3526
  * @remarks
3493
3527
  * Steps apply against an isolated candidate. Rows, schema changes, and
@@ -3569,6 +3603,26 @@ var MemoryDriver = class {
3569
3603
  case "index.remove": this.#require(tables, step.table);
3570
3604
  }
3571
3605
  }
3606
+ async *#stream(table, input) {
3607
+ const store = this.#store(table);
3608
+ const conditions = input.conditions;
3609
+ const offset = input.offset ?? 0;
3610
+ const limit = input.limit;
3611
+ let skipped = 0;
3612
+ let yielded = 0;
3613
+ for (const key of this.#ordered(table)) {
3614
+ if (limit !== void 0 && yielded >= limit) return;
3615
+ const row = store.get(key);
3616
+ if (row === void 0) continue;
3617
+ if (conditions !== void 0 && conditions.length > 0 && !matchesQuery(row, conditions)) continue;
3618
+ if (skipped < offset) {
3619
+ skipped += 1;
3620
+ continue;
3621
+ }
3622
+ yield structuredClone(row);
3623
+ yielded += 1;
3624
+ }
3625
+ }
3572
3626
  #store(table) {
3573
3627
  this.#table(table);
3574
3628
  const store = this.#tables.get(table);
@@ -3579,41 +3633,50 @@ var MemoryDriver = class {
3579
3633
  //#endregion
3580
3634
  //#region src/core/factories.ts
3581
3635
  /**
3582
- * Create a database over a driver and a declared `tables` schema.
3636
+ * Creates a database over a driver and a declared `tables` schema.
3583
3637
  *
3584
3638
  * @remarks
3585
3639
  * `tables` maps each name to its columns (a `column → shape` map); the database
3586
3640
  * wraps each in an `objectShape`, so you never write `objectShape` at the table
3587
3641
  * level. The `const` type parameter captures the literal names and columns, so
3588
3642
  * `db.table('users')` is checked against the schema and typed by `Infer` of its
3589
- * columns — no annotations. Name a non-`id` primary-key column per table via the
3590
- * optional `primary` and `indexes` maps.
3643
+ * columns — no annotations. Name a non-`id` primary-key column per table through
3644
+ * the optional `primary` and `indexes` maps.
3591
3645
  *
3592
3646
  * @param options - The driver, `tables`, and optional `primary`, `indexes`,
3593
3647
  * `name`, `generator`, `version`, and emitter hooks
3594
3648
  * @returns A typed {@link DatabaseInterface}
3595
3649
  *
3596
- * @example
3650
+ * @example Create a database
3597
3651
  * ```ts
3598
3652
  * import { createDatabase, createMemoryDriver } from '@orkestrel/database'
3599
3653
  * import { integerShape, stringShape } from '@orkestrel/contract'
3600
3654
  *
3601
3655
  * const db = createDatabase({
3602
- * driver: createMemoryDriver(),
3656
+ * driver: createMemoryDriver(), // any DriverInterface — a persistent backend swaps in, same API
3603
3657
  * tables: {
3604
- * users: { id: stringShape(), age: integerShape() },
3658
+ * users: { id: stringShape(), name: stringShape(), age: integerShape() },
3605
3659
  * posts: { slug: stringShape(), title: stringShape() },
3606
3660
  * },
3607
- * primary: { posts: 'slug' },
3661
+ * primary: { posts: 'slug' }, // non-`id` primary-key columns, per table
3608
3662
  * })
3609
- * await db.table('users').set({ id: 'u1', age: 36 }) // typed; coerced + validated
3663
+ *
3664
+ * const users = db.table('users') // hold the handle; TableInterface<{ id; name; age }>
3665
+ *
3666
+ * await users.set({ id: 'u1', name: 'Ada', age: 36 }) // coerced + validated through the contract
3667
+ * await users.get('u1') // typed { id; name; age } | undefined — narrowed, never `as`
3668
+ * await users
3669
+ * .query()
3670
+ * .condition({ column: 'age', operator: 'from', values: [18], connector: 'and' })
3671
+ * .order({ column: 'age', direction: 'descending' })
3672
+ * .collect() // typed rows
3610
3673
  * ```
3611
3674
  */
3612
3675
  function createDatabase(options) {
3613
3676
  return new Database(options);
3614
3677
  }
3615
3678
  /**
3616
- * Create the in-memory reference {@link DriverInterface}.
3679
+ * Creates the in-memory reference {@link DriverInterface}.
3617
3680
  *
3618
3681
  * @remarks
3619
3682
  * Backed by nested maps with no I/O — the same driver runs in a browser or on a
@@ -3625,9 +3688,129 @@ function createMemoryDriver() {
3625
3688
  return new MemoryDriver();
3626
3689
  }
3627
3690
  //#endregion
3691
+ //#region src/core/DriverIterator.ts
3692
+ /**
3693
+ * Forms the internal continuation boundary for a root driver async iterator.
3694
+ *
3695
+ * @remarks
3696
+ * A driver transaction can begin while a caller holds an idle root iterator.
3697
+ * Every `next` therefore checks the driver's root-state guard immediately
3698
+ * before and after advancing the source. A failed continuation terminalizes the
3699
+ * iterator, discards any row produced before the post-advance guard failed, and
3700
+ * attempts source cleanup exactly once.
3701
+ *
3702
+ * A driver implementing the published `DriverInterface` extension seam wraps its
3703
+ * own source iterator in one so a root `scan` / `stream` cannot outlive the
3704
+ * driver state it was opened against.
3705
+ *
3706
+ * @typeParam T - The value the wrapped source yields
3707
+ *
3708
+ * @example
3709
+ * ```ts
3710
+ * import type { Row } from '@orkestrel/database'
3711
+ * import { DatabaseError, DriverIterator } from '@orkestrel/database'
3712
+ *
3713
+ * // Inside a driver's `scan`, over its own row source and root-state guard.
3714
+ * declare const rows: AsyncIterator<Row>
3715
+ * declare const transacting: () => boolean
3716
+ * const scan = new DriverIterator(rows, () => {
3717
+ * if (transacting()) {
3718
+ * throw new DatabaseError('CONFLICT', 'scan: a transaction is active')
3719
+ * }
3720
+ * })
3721
+ * for await (const row of scan) row // one row at a time, guarded around each advance
3722
+ * ```
3723
+ */
3724
+ var DriverIterator = class {
3725
+ #source;
3726
+ #guard;
3727
+ #terminal = false;
3728
+ #cleaned = false;
3729
+ /**
3730
+ * Wraps one source iterator in the continuation boundary.
3731
+ *
3732
+ * @param source - The driver's own row iterator, advanced once per `next`
3733
+ * @param guard - The root-state check, run immediately before and after each advance; it throws to terminalize the iteration
3734
+ */
3735
+ constructor(source, guard) {
3736
+ this.#source = source;
3737
+ this.#guard = guard;
3738
+ }
3739
+ [Symbol.asyncIterator]() {
3740
+ return this;
3741
+ }
3742
+ async next() {
3743
+ if (this.#terminal) return {
3744
+ done: true,
3745
+ value: void 0
3746
+ };
3747
+ try {
3748
+ this.#guard();
3749
+ const result = await this.#source.next();
3750
+ this.#guard();
3751
+ if (result.done === true) {
3752
+ this.#terminal = true;
3753
+ this.#cleaned = true;
3754
+ }
3755
+ return result;
3756
+ } catch (error) {
3757
+ this.#terminal = true;
3758
+ await this.#discard();
3759
+ throw error;
3760
+ }
3761
+ }
3762
+ async return() {
3763
+ if (this.#terminal) return {
3764
+ done: true,
3765
+ value: void 0
3766
+ };
3767
+ this.#terminal = true;
3768
+ if (this.#cleaned || this.#source.return === void 0) {
3769
+ this.#cleaned = true;
3770
+ return {
3771
+ done: true,
3772
+ value: void 0
3773
+ };
3774
+ }
3775
+ this.#cleaned = true;
3776
+ return this.#source.return();
3777
+ }
3778
+ async throw(error) {
3779
+ if (this.#terminal) throw error;
3780
+ if (this.#source.throw === void 0) {
3781
+ this.#terminal = true;
3782
+ await this.#discard();
3783
+ throw error;
3784
+ }
3785
+ try {
3786
+ const result = await this.#source.throw(error);
3787
+ if (result.done === true) {
3788
+ this.#terminal = true;
3789
+ this.#cleaned = true;
3790
+ }
3791
+ return result;
3792
+ } catch (cause) {
3793
+ this.#terminal = true;
3794
+ await this.#discard();
3795
+ throw cause;
3796
+ }
3797
+ }
3798
+ async #discard() {
3799
+ if (this.#cleaned) return;
3800
+ this.#cleaned = true;
3801
+ try {
3802
+ await this.#source.return?.();
3803
+ } catch {}
3804
+ }
3805
+ };
3806
+ //#endregion
3807
+ exports.CONFORMANCE_POSTS_SCHEMA = CONFORMANCE_POSTS_SCHEMA;
3808
+ exports.CONFORMANCE_SCHEMA = CONFORMANCE_SCHEMA;
3809
+ exports.CONFORMANCE_USERS_SCHEMA = CONFORMANCE_USERS_SCHEMA;
3628
3810
  exports.DEFAULT_PRIMARY = DEFAULT_PRIMARY;
3629
3811
  exports.Database = Database;
3630
3812
  exports.DatabaseError = DatabaseError;
3813
+ exports.DriverIterator = DriverIterator;
3631
3814
  exports.MAX_PATTERN_LENGTH = MAX_PATTERN_LENGTH;
3632
3815
  exports.MemoryDriver = MemoryDriver;
3633
3816
  exports.applyQuery = applyQuery;
@@ -3642,10 +3825,10 @@ exports.computeAggregate = computeAggregate;
3642
3825
  exports.conformDriver = conformDriver;
3643
3826
  exports.createDatabase = createDatabase;
3644
3827
  exports.createMemoryDriver = createMemoryDriver;
3645
- exports.driverFindings = driverFindings;
3646
3828
  exports.equalsValue = equalsValue;
3647
3829
  exports.extractKey = extractKey;
3648
3830
  exports.filterRows = filterRows;
3831
+ exports.findColumn = findColumn;
3649
3832
  exports.isColumnSchema = isColumnSchema;
3650
3833
  exports.isDatabaseError = isDatabaseError;
3651
3834
  exports.isDriverMetadata = isDriverMetadata;
@@ -3656,7 +3839,6 @@ exports.isMigrationInput = isMigrationInput;
3656
3839
  exports.isMigrationStep = isMigrationStep;
3657
3840
  exports.isTableSchema = isTableSchema;
3658
3841
  exports.matchesCondition = matchesCondition;
3659
- exports.matchesFuzzy = matchesFuzzy;
3660
3842
  exports.matchesGlobPattern = matchesGlobPattern;
3661
3843
  exports.matchesLikePattern = matchesLikePattern;
3662
3844
  exports.matchesQuery = matchesQuery;
@@ -3665,6 +3847,9 @@ exports.migrateRows = migrateRows;
3665
3847
  exports.normalizeDriverSchema = normalizeDriverSchema;
3666
3848
  exports.planMigration = planMigration;
3667
3849
  exports.projectMigrationSchema = projectMigrationSchema;
3850
+ exports.requireColumns = requireColumns;
3851
+ exports.resolvePrimary = resolvePrimary;
3852
+ exports.scanDriver = scanDriver;
3668
3853
  exports.shapeToColumnSchema = shapeToColumnSchema;
3669
3854
  exports.shapeToColumnStorage = shapeToColumnStorage;
3670
3855
  exports.sortRows = sortRows;