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