@orkestrel/database 0.0.11 → 0.0.13

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 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,12 @@ 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 before rejecting it.
14
14
  *
15
15
  * @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
16
+ * A `LIKE` / `GLOB` pattern is a caller-supplied operand, so
17
+ * `matchesLikePattern` / `matchesGlobPattern` run patterns this package cannot
18
+ * trust. The matcher is the LINEAR greedy two-pointer wildcard match — never a
19
19
  * backtracking regex (`.*`-segments-separated-by-literals against a long input is the
20
20
  * catastrophic shape JS cannot bound without atomic groups), so it is O(value ×
21
21
  * pattern). Capping the pattern length bounds that pattern factor, leaving a match
@@ -23,10 +23,83 @@ var DEFAULT_PRIMARY = "id";
23
23
  * `VALIDATION` {@link DatabaseError}; the cap is generous for any legitimate search.
24
24
  */
25
25
  var MAX_PATTERN_LENGTH = 1024;
26
+ /**
27
+ * Describes the `users` table the driver-conformance battery opens — keyed by the default
28
+ * `id` primary column.
29
+ *
30
+ * @remarks
31
+ * `age` is optional and `meta` is a declared `json` column, so the battery's
32
+ * nested-round-trip phase is fair to a typed-column backend: a SQL driver
33
+ * persists only declared columns, while a schemaless backend ignores the
34
+ * declarations entirely.
35
+ */
36
+ var CONFORMANCE_USERS_SCHEMA = Object.freeze({
37
+ name: "users",
38
+ primary: "id",
39
+ columns: Object.freeze([
40
+ {
41
+ name: "id",
42
+ storage: "text",
43
+ optional: false,
44
+ nullable: false
45
+ },
46
+ {
47
+ name: "name",
48
+ storage: "text",
49
+ optional: false,
50
+ nullable: false
51
+ },
52
+ {
53
+ name: "age",
54
+ storage: "integer",
55
+ optional: true,
56
+ nullable: false
57
+ },
58
+ {
59
+ name: "meta",
60
+ storage: "json",
61
+ optional: true,
62
+ nullable: false
63
+ }
64
+ ]),
65
+ indexes: Object.freeze([])
66
+ });
67
+ /**
68
+ * Describes the `posts` table the driver-conformance battery opens — keyed by a non-`id`
69
+ * `slug` primary column.
70
+ *
71
+ * @remarks
72
+ * Pairs with {@link CONFORMANCE_USERS_SCHEMA} so one battery exercises both
73
+ * primary-key shapes: the default `id` and an explicit override.
74
+ */
75
+ var CONFORMANCE_POSTS_SCHEMA = Object.freeze({
76
+ name: "posts",
77
+ primary: "slug",
78
+ columns: Object.freeze([{
79
+ name: "slug",
80
+ storage: "text",
81
+ optional: false,
82
+ nullable: false
83
+ }, {
84
+ name: "title",
85
+ storage: "text",
86
+ optional: false,
87
+ nullable: false
88
+ }]),
89
+ indexes: Object.freeze([])
90
+ });
91
+ /**
92
+ * Holds the fixed two-table schema every driver-conformance phase opens.
93
+ *
94
+ * @remarks
95
+ * Each phase mints a fresh driver and opens this exact schema, so a finding
96
+ * names a violated invariant rather than a setup difference between phases.
97
+ */
98
+ var CONFORMANCE_SCHEMA = Object.freeze([CONFORMANCE_USERS_SCHEMA, CONFORMANCE_POSTS_SCHEMA]);
26
99
  //#endregion
27
100
  //#region src/core/errors.ts
28
101
  /**
29
- * An error thrown by the database layer.
102
+ * Represents an error thrown by the database layer.
30
103
  *
31
104
  * @remarks
32
105
  * Carries a {@link DatabaseErrorCode} and an optional `context` bag naming the
@@ -37,7 +110,7 @@ var MAX_PATTERN_LENGTH = 1024;
37
110
  * `context`), an inapplicable {@link Migration} plan (`MIGRATION`), a
38
111
  * driver that violates a {@link DriverInterface} invariant, thrown by the
39
112
  * `conformDriver` helper (`CONFORMANCE`), and an unexpected infrastructure
40
- * fault surfaced by a driver seam — e.g. a filesystem failure while
113
+ * fault surfaced by a driver seam — for example a filesystem failure while
41
114
  * persisting (`DRIVER`) — as opposed to expected domain conditions, which
42
115
  * keep their specific codes.
43
116
  */
@@ -52,10 +125,10 @@ var DatabaseError = class extends Error {
52
125
  }
53
126
  };
54
127
  /**
55
- * Narrow an unknown caught value to a {@link DatabaseError}.
128
+ * Narrows an unknown caught value to a {@link DatabaseError}.
56
129
  *
57
130
  * @param value - The value to test (typically a `catch` binding)
58
- * @returns `true` when `value` is a {@link DatabaseError}
131
+ * @returns True if `value` is a {@link DatabaseError}; false otherwise
59
132
  *
60
133
  * @example
61
134
  * ```ts
@@ -72,43 +145,19 @@ function isDatabaseError(value) {
72
145
  //#endregion
73
146
  //#region src/core/validators.ts
74
147
  /**
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.
148
+ * Checks whether a value is a usable database key.
100
149
  *
101
150
  * @param value - The value to test
102
- * @returns Whether `value` is a string or finite number
151
+ * @returns True if `value` is a string or a finite number; false otherwise
103
152
  */
104
153
  function isKey(value) {
105
154
  return typeof value === "string" || typeof value === "number" && Number.isFinite(value);
106
155
  }
107
156
  /**
108
- * Test whether a value is a portable column schema.
157
+ * Checks whether a value is a portable column schema.
109
158
  *
110
159
  * @param value - The value to test
111
- * @returns Whether `value` is a complete {@link ColumnSchema}
160
+ * @returns True if `value` is a complete {@link ColumnSchema}; false otherwise
112
161
  */
113
162
  function isColumnSchema(value) {
114
163
  try {
@@ -120,10 +169,10 @@ function isColumnSchema(value) {
120
169
  }
121
170
  }
122
171
  /**
123
- * Test whether a value is a portable table schema.
172
+ * Checks whether a value is a portable table schema.
124
173
  *
125
174
  * @param value - The value to test
126
- * @returns Whether `value` is a complete {@link TableSchema}
175
+ * @returns True if `value` is a complete {@link TableSchema}; false otherwise
127
176
  */
128
177
  function isTableSchema(value) {
129
178
  try {
@@ -139,10 +188,10 @@ function isTableSchema(value) {
139
188
  }
140
189
  }
141
190
  /**
142
- * Test whether a value is a complete portable driver schema.
191
+ * Checks whether a value is a complete portable driver schema.
143
192
  *
144
193
  * @param value - The value to test
145
- * @returns Whether `value` is a table-schema collection with unique table names
194
+ * @returns True if `value` is a table-schema collection with unique table names; false otherwise
146
195
  */
147
196
  function isDriverSchema(value) {
148
197
  try {
@@ -155,10 +204,10 @@ function isDriverSchema(value) {
155
204
  }
156
205
  }
157
206
  /**
158
- * Test whether a value is one ordered migration step.
207
+ * Checks whether a value is one ordered migration step.
159
208
  *
160
209
  * @param value - The value to test
161
- * @returns Whether `value` is a complete {@link MigrationStep}
210
+ * @returns True if `value` is a complete {@link MigrationStep}; false otherwise
162
211
  */
163
212
  function isMigrationStep(value) {
164
213
  try {
@@ -179,10 +228,10 @@ function isMigrationStep(value) {
179
228
  }
180
229
  }
181
230
  /**
182
- * Test whether a value is an ordered migration plan.
231
+ * Checks whether a value is an ordered migration plan.
183
232
  *
184
233
  * @param value - The value to test
185
- * @returns Whether `value` is a complete {@link Migration}
234
+ * @returns True if `value` is a complete {@link Migration}; false otherwise
186
235
  */
187
236
  function isMigration(value) {
188
237
  try {
@@ -194,10 +243,10 @@ function isMigration(value) {
194
243
  }
195
244
  }
196
245
  /**
197
- * Test whether a value is persisted driver metadata.
246
+ * Checks whether a value is persisted driver metadata.
198
247
  *
199
248
  * @param value - The value to test
200
- * @returns Whether `value` is complete {@link DriverMetadata}
249
+ * @returns True if `value` is complete {@link DriverMetadata}; false otherwise
201
250
  */
202
251
  function isDriverMetadata(value) {
203
252
  try {
@@ -209,10 +258,10 @@ function isDriverMetadata(value) {
209
258
  }
210
259
  }
211
260
  /**
212
- * Test whether a value is one atomic migration request.
261
+ * Checks whether a value is one atomic migration request.
213
262
  *
214
263
  * @param value - The value to test
215
- * @returns Whether `value` is a complete {@link MigrationInput}
264
+ * @returns True if `value` is a complete {@link MigrationInput}; false otherwise
216
265
  */
217
266
  function isMigrationInput(value) {
218
267
  try {
@@ -226,7 +275,7 @@ function isMigrationInput(value) {
226
275
  //#endregion
227
276
  //#region src/core/cloners.ts
228
277
  /**
229
- * Clone unknown driver metadata into a distinct deeply frozen snapshot.
278
+ * Clones unknown driver metadata into a distinct deeply frozen snapshot.
230
279
  *
231
280
  * @param value - Unknown metadata
232
281
  * @returns Owned driver metadata
@@ -245,7 +294,7 @@ function cloneDriverMetadata(value) {
245
294
  }
246
295
  }
247
296
  /**
248
- * Clone unknown driver schema into a distinct deeply frozen snapshot.
297
+ * Clones unknown driver schema into a distinct deeply frozen snapshot.
249
298
  *
250
299
  * @param value - Unknown table schema collection
251
300
  * @returns Owned driver schema
@@ -264,7 +313,7 @@ function cloneDriverSchema(value) {
264
313
  }
265
314
  }
266
315
  /**
267
- * Clone unknown migration input into a distinct deeply frozen snapshot.
316
+ * Clones unknown migration input into a distinct deeply frozen snapshot.
268
317
  *
269
318
  * @param value - Unknown migration input
270
319
  * @returns Owned migration input
@@ -285,8 +334,32 @@ function cloneMigrationInput(value) {
285
334
  //#endregion
286
335
  //#region src/core/helpers.ts
287
336
  /**
288
- * A total ordering over arbitrary values the comparator behind sorting and the
289
- * range operators.
337
+ * Validates the paging fields of a portable query.
338
+ *
339
+ * @remarks
340
+ * A present `limit` or `offset` must be a finite nonnegative integer; zero is
341
+ * valid. Validation is deterministic (`limit` before `offset`). Non-finite
342
+ * values are rendered as strings in error context so JSON serialization cannot
343
+ * collapse `NaN` or infinity to `null`.
344
+ *
345
+ * @param input - The portable query whose paging fields to validate
346
+ * @throws {@link DatabaseError} `VALIDATION` when a paging field is invalid
347
+ */
348
+ function validatePage(input) {
349
+ const limit = input?.limit;
350
+ if (limit !== void 0 && (!Number.isInteger(limit) || limit < 0)) throw new DatabaseError("VALIDATION", "Query limit must be a nonnegative integer", {
351
+ field: "limit",
352
+ value: Number.isFinite(limit) ? limit : String(limit)
353
+ });
354
+ const offset = input?.offset;
355
+ if (offset !== void 0 && (!Number.isInteger(offset) || offset < 0)) throw new DatabaseError("VALIDATION", "Query offset must be a nonnegative integer", {
356
+ field: "offset",
357
+ value: Number.isFinite(offset) ? offset : String(offset)
358
+ });
359
+ }
360
+ /**
361
+ * Compares two arbitrary values under one total order — the comparator behind
362
+ * sorting and the range operators.
290
363
  *
291
364
  * @remarks
292
365
  * Values of different types order by a fixed type rank (`undefined` < `null` <
@@ -310,13 +383,14 @@ function compareValues(left, right) {
310
383
  return 0;
311
384
  }
312
385
  /**
313
- * Structural equality by SameValueZero leaves — the comparator behind conformance
314
- * checks and any test/fixture that needs "same data", not "same reference".
386
+ * Compares two values structurally by SameValueZero leaves — the comparator
387
+ * behind conformance checks and any test/fixture that needs "same data", not
388
+ * "same reference".
315
389
  *
316
390
  * @remarks
317
391
  * Primitives compare by SameValueZero (`NaN` equals itself; `+0` equals `-0`).
318
392
  * Arrays compare by index (same length, every element `equalsValue`). Plain
319
- * records (via `isRecord`) compare by their OWN enumerable keys: same key
393
+ * records (through `isRecord`) compare by their OWN enumerable keys: same key
320
394
  * COUNT and, for every key in `left`, `right` has that key (`Object.hasOwn`)
321
395
  * with a `equalsValue` value — so a key present with value `undefined` is NOT
322
396
  * equal to that key being absent (both differ in `Object.keys` membership).
@@ -327,7 +401,7 @@ function compareValues(left, right) {
327
401
  *
328
402
  * @param left - The left value
329
403
  * @param right - The right value
330
- * @returns Whether `left` and `right` are structurally equal
404
+ * @returns True if `left` and `right` are structurally equal; false otherwise
331
405
  *
332
406
  * @example
333
407
  * ```ts
@@ -384,46 +458,15 @@ function equalsValue(left, right) {
384
458
  }
385
459
  }
386
460
  /**
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
461
+ * Matches a value against a wildcard pattern in LINEAR time — the shared, ReDoS-SAFE
419
462
  * engine behind {@link matchesLikePattern} and {@link matchesGlobPattern}.
420
463
  *
421
464
  * @remarks
422
465
  * A backtracking RegExp (`a%b%c` → `^a.*b.*c$`) is CATASTROPHIC on a hostile pattern:
423
466
  * `.*` 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:
467
+ * up super-linearly — and JS has no atomic groups / possessive quantifiers to bound it,
468
+ * while a `LIKE` / `GLOB` pattern is a caller-supplied operand this package cannot
469
+ * trust. So this builds NO regex. It runs the classic GREEDY TWO-POINTER wildcard match:
427
470
  * the `any` wildcard records its position and, on a later mismatch, backtracks ONLY to
428
471
  * that last `any` (letting it absorb one more char) — so the work is O(value × pattern),
429
472
  * never the exponential / polynomial backtracking a regex would do. The pattern length
@@ -442,7 +485,7 @@ function matchesFuzzy(value, query) {
442
485
  * @param any - The any-run wildcard char (`%` for `LIKE`, `*` for `GLOB`)
443
486
  * @param single - The single-char wildcard char (`_` for `LIKE`, `?` for `GLOB`)
444
487
  * @param fold - Whether to match case-INSENSITIVELY (`LIKE` folds; `GLOB` does not)
445
- * @returns Whether `value` matches `pattern`
488
+ * @returns True if `value` matches `pattern`; false otherwise
446
489
  * @throws A `VALIDATION` {@link DatabaseError} when `pattern` exceeds {@link MAX_PATTERN_LENGTH}
447
490
  */
448
491
  function matchesWildcardPattern(value, pattern, any, single, fold) {
@@ -474,14 +517,55 @@ function matchesWildcardPattern(value, pattern, any, single, fold) {
474
517
  while (pi < needle.length && needle[pi] === any) pi += 1;
475
518
  return pi === needle.length;
476
519
  }
520
+ /**
521
+ * Matches a value against a SQL `LIKE` pattern, folding case.
522
+ *
523
+ * @remarks
524
+ * `%` matches any run of characters (including none) and `_` matches exactly one
525
+ * character; every other pattern character matches itself literally. Runs on
526
+ * {@link matchesWildcardPattern}, so the match is linear in the value length and
527
+ * the pattern is capped at {@link MAX_PATTERN_LENGTH}.
528
+ *
529
+ * @param value - The value to test
530
+ * @param pattern - The `LIKE` pattern
531
+ * @returns True if `value` matches `pattern` under case folding; false otherwise
532
+ * @throws A `VALIDATION` {@link DatabaseError} when `pattern` exceeds {@link MAX_PATTERN_LENGTH}
533
+ *
534
+ * @example
535
+ * ```ts
536
+ * matchesLikePattern('Hello', 'h%o') // true — `%` spans any run, and case folds
537
+ * matchesLikePattern('Hello', 'h_llo') // true — `_` matches exactly one character
538
+ * ```
539
+ */
477
540
  function matchesLikePattern(value, pattern) {
478
541
  return matchesWildcardPattern(value, pattern, "%", "_", true);
479
542
  }
543
+ /**
544
+ * Matches a value against a `GLOB` pattern, preserving case.
545
+ *
546
+ * @remarks
547
+ * `*` matches any run of characters (including none) and `?` matches exactly one
548
+ * character; every other pattern character matches itself literally, so a
549
+ * character class such as `[a-z]` is NOT interpreted. Runs on
550
+ * {@link matchesWildcardPattern}, so the match is linear in the value length and
551
+ * the pattern is capped at {@link MAX_PATTERN_LENGTH}.
552
+ *
553
+ * @param value - The value to test
554
+ * @param pattern - The `GLOB` pattern
555
+ * @returns True if `value` matches `pattern` case-sensitively; false otherwise
556
+ * @throws A `VALIDATION` {@link DatabaseError} when `pattern` exceeds {@link MAX_PATTERN_LENGTH}
557
+ *
558
+ * @example
559
+ * ```ts
560
+ * matchesGlobPattern('hello', 'h*o') // true — `*` spans any run
561
+ * matchesGlobPattern('Hello', 'h*o') // false — `GLOB` is case-sensitive
562
+ * ```
563
+ */
480
564
  function matchesGlobPattern(value, pattern) {
481
565
  return matchesWildcardPattern(value, pattern, "*", "?", false);
482
566
  }
483
567
  /**
484
- * Evaluate one {@link Condition} against a row — the per-operator predicate.
568
+ * Evaluates one {@link Condition} against a row — the per-operator predicate.
485
569
  *
486
570
  * @remarks
487
571
  * Reads the condition's column — a `FieldPath`, resolved with `resolveField` (a
@@ -495,11 +579,11 @@ function matchesGlobPattern(value, pattern) {
495
579
  * on leaves, so `NaN` now equals `NaN` under `equals` / `any` (it never matched
496
580
  * anything under the old rank-based comparison). `like` / `glob` / `starts` /
497
581
  * `ends` match only strings; `absent` / `present` test nullishness. Total — a
498
- * type mismatch is simply a non-match.
582
+ * type mismatch is a non-match.
499
583
  *
500
584
  * @param row - The row to test
501
585
  * @param condition - The condition to apply
502
- * @returns Whether the row satisfies the condition
586
+ * @returns True if the row satisfies the condition; false otherwise
503
587
  */
504
588
  function matchesCondition(row, condition) {
505
589
  const value = resolveField(row, condition.column);
@@ -524,7 +608,7 @@ function matchesCondition(row, condition) {
524
608
  }
525
609
  }
526
610
  /**
527
- * Fold a row through a list of conditions, joining each by its connector.
611
+ * Folds a row through a list of conditions, joining each by its connector.
528
612
  *
529
613
  * @remarks
530
614
  * Evaluated left-to-right: the first condition seeds the result, and each later
@@ -534,7 +618,7 @@ function matchesCondition(row, condition) {
534
618
  *
535
619
  * @param row - The row to test
536
620
  * @param conditions - The conditions to fold
537
- * @returns Whether the row satisfies the combined conditions
621
+ * @returns True if the row satisfies the combined conditions; false otherwise
538
622
  */
539
623
  function matchesQuery(row, conditions) {
540
624
  let result = true;
@@ -549,7 +633,7 @@ function matchesQuery(row, conditions) {
549
633
  return result;
550
634
  }
551
635
  /**
552
- * Filter rows by a list of conditions — the shared basis for a table's count
636
+ * Filters rows by a list of conditions — the shared basis for a table's count
553
637
  * and aggregate paths (no sort/page, unlike {@link applyQuery}).
554
638
  *
555
639
  * @remarks
@@ -573,7 +657,7 @@ function filterRows(rows, conditions) {
573
657
  return rows.filter((row) => matchesQuery(row, conditions));
574
658
  }
575
659
  /**
576
- * Sort rows by an ordering specification, leaving the input untouched.
660
+ * Sorts rows by an ordering specification, leaving the input untouched.
577
661
  *
578
662
  * @remarks
579
663
  * Applies the terms in priority order — the first term that distinguishes two
@@ -595,7 +679,7 @@ function sortRows(rows, order) {
595
679
  return sorted;
596
680
  }
597
681
  /**
598
- * Apply a {@link QueryInput} to rows — filter, then sort, then page.
682
+ * Applies a {@link QueryInput} to rows — filter, then sort, then page.
599
683
  *
600
684
  * @remarks
601
685
  * The whole portable read pipeline in one place: conditions filter, `order`
@@ -620,7 +704,7 @@ function applyQuery(rows, input) {
620
704
  return result;
621
705
  }
622
706
  /**
623
- * Compute an aggregate over a column across rows.
707
+ * Computes an aggregate over a column across rows.
624
708
  *
625
709
  * @remarks
626
710
  * `count` returns the row count. The numeric aggregates coerce each cell with
@@ -649,7 +733,7 @@ function computeAggregate(rows, operation, column) {
649
733
  return operation === "minimum" ? Math.min(...numbers) : Math.max(...numbers);
650
734
  }
651
735
  /**
652
- * Read a row's primary key from a column, when it is a usable {@link Key}.
736
+ * Reads a row's primary key from a column, when it is a usable {@link Key}.
653
737
  *
654
738
  * @param row - The row to read
655
739
  * @param column - The primary-key column name
@@ -660,7 +744,7 @@ function extractKey(row, column) {
660
744
  return isKey(value) ? value : void 0;
661
745
  }
662
746
  /**
663
- * Return a fresh row whose primary column is authoritatively bound to its storage key.
747
+ * Returns a fresh row whose primary column is authoritatively bound to its storage key.
664
748
  *
665
749
  * @param row - The caller row
666
750
  * @param primary - The primary column
@@ -674,7 +758,7 @@ function bindRowKey(row, primary, key) {
674
758
  };
675
759
  }
676
760
  /**
677
- * Map a column's {@link ContractShape} to its portable {@link ColumnStorage} — the
761
+ * Maps a column's {@link ContractShape} to its portable {@link ColumnStorage} — the
678
762
  * value a `TableSchema` carries so a native backend can declare a real column.
679
763
  *
680
764
  * @remarks
@@ -698,7 +782,7 @@ function bindRowKey(row, primary, key) {
698
782
  * ```
699
783
  */
700
784
  function shapeToColumnStorage(shape) {
701
- switch (shape.type) {
785
+ switch (shape.category) {
702
786
  case "string": return "text";
703
787
  case "number": return shape.integer === true ? "integer" : "real";
704
788
  case "boolean": return "boolean";
@@ -717,7 +801,7 @@ function shapeToColumnStorage(shape) {
717
801
  }
718
802
  }
719
803
  /**
720
- * Project one contract shape into a portable column schema.
804
+ * Projects one contract shape into a portable column schema.
721
805
  *
722
806
  * @param name - The column name
723
807
  * @param shape - The column contract shape
@@ -733,7 +817,55 @@ function shapeToColumnSchema(name, shape) {
733
817
  };
734
818
  }
735
819
  /**
736
- * Throw when an {@link OperationOptions.signal | AbortSignal} has fired — the shared
820
+ * Reads one flat column's declaration out of a table schema.
821
+ *
822
+ * @remarks
823
+ * The single lookup behind every declared-column question — storage type,
824
+ * optionality, and nullability all come off the returned {@link ColumnSchema},
825
+ * so a caller that needs more than one of them reads them from one result. A
826
+ * nested {@link FieldPath} names no declared column, so resolve the path's head
827
+ * before calling. A schema that does not declare the column returns `undefined`.
828
+ *
829
+ * @param name - The flat column name
830
+ * @param schema - The table's schema
831
+ * @returns The column's {@link ColumnSchema}, or `undefined` when the schema does not declare it
832
+ *
833
+ * @example
834
+ * ```ts
835
+ * findColumn('age', schema)?.storage // 'integer'
836
+ * findColumn('absent', schema) // undefined
837
+ * ```
838
+ */
839
+ function findColumn(name, schema) {
840
+ return schema.columns.find((candidate) => candidate.name === name);
841
+ }
842
+ /**
843
+ * Resolves the primary-key column one table keys its rows by.
844
+ *
845
+ * @remarks
846
+ * A table absent from the {@link PrimaryMap} keys its rows by
847
+ * {@link DEFAULT_PRIMARY}, so this is total over any table name.
848
+ *
849
+ * @param primary - The per-table primary-key overrides
850
+ * @param name - The table name
851
+ * @returns The table's primary-key column
852
+ *
853
+ * @example
854
+ * ```ts
855
+ * resolvePrimary({ posts: 'slug' }, 'posts') // 'slug'
856
+ * resolvePrimary({ posts: 'slug' }, 'users') // 'id' — the default primary
857
+ * ```
858
+ */
859
+ function resolvePrimary(primary, name) {
860
+ return primary[name] ?? "id";
861
+ }
862
+ function requireColumns(tables, name) {
863
+ const columns = tables[name];
864
+ if (columns === void 0) throw new DatabaseError("NOT_FOUND", `Table '${name}' is not declared`, { table: name });
865
+ return columns;
866
+ }
867
+ /**
868
+ * Throws when an {@link OperationOptions.signal | AbortSignal} has fired — the shared
737
869
  * abort gate checked at operation boundaries and between streamed rows.
738
870
  *
739
871
  * @remarks
@@ -761,7 +893,7 @@ function checkAbort(signal) {
761
893
  if (signal?.aborted) throw new DatabaseError("ABORTED", "Operation aborted", { reason: signal.reason });
762
894
  }
763
895
  /**
764
- * Structurally diff a deployed and a declared table set into a {@link Migration}
896
+ * Diffs a deployed and a declared table set structurally into a {@link Migration}
765
897
  * plan.
766
898
  *
767
899
  * @remarks
@@ -778,13 +910,12 @@ function checkAbort(signal) {
778
910
  *
779
911
  * A column present in BOTH schemas under the same name but with a different
780
912
  * `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.
913
+ * {@link DatabaseError} naming the table, the column, and the from→to
914
+ * difference — a name-only diff would otherwise silently produce NO step for
915
+ * the drift, and versioned reconciliation would stamp over it. There is no
916
+ * automatic in-place type-change step: the manual path is to add a new column,
917
+ * copy/convert the data at the application layer, then remove the old column
918
+ * two separate plans, never a single implicit "alter" step.
788
919
  *
789
920
  * @param deployed - The table schemas currently applied
790
921
  * @param declared - The table schemas the caller wants applied
@@ -892,7 +1023,7 @@ function planMigration(deployed, declared, from = 0, to = 1) {
892
1023
  } }).plan;
893
1024
  }
894
1025
  /**
895
- * Sequentially project migration steps over a canonical validated owned schema.
1026
+ * Projects migration steps sequentially over a canonical validated owned schema.
896
1027
  * Adding a required non-null column to an existing table rejects with
897
1028
  * `MIGRATION`; optional-only and nullable-only additions remain portable.
898
1029
  *
@@ -991,7 +1122,7 @@ function projectMigrationSchema(schema, steps) {
991
1122
  }
992
1123
  }
993
1124
  /**
994
- * Canonicalize an unknown driver schema into a distinct deeply frozen snapshot.
1125
+ * Canonicalizes an unknown driver schema into a distinct deeply frozen snapshot.
995
1126
  *
996
1127
  * @remarks
997
1128
  * Table and column lists are sorted by name. The index list is sorted by the
@@ -1013,11 +1144,11 @@ function normalizeDriverSchema(value) {
1013
1144
  return cloneDriverSchema(tables);
1014
1145
  }
1015
1146
  /**
1016
- * Apply one table's {@link MigrationStep}s to its rows — a pure row transform.
1147
+ * Applies one table's {@link MigrationStep}s to its rows — a pure row transform.
1017
1148
  *
1018
1149
  * @remarks
1019
1150
  * `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
1151
+ * never mutated); `column.add` leaves rows as-is (an absent field
1021
1152
  * reads as `undefined`, backfill is application policy). `table.add` /
1022
1153
  * `table.remove` / `index.add` / `index.remove` are no-ops here (they operate
1023
1154
  * on storage shape, not row shape). Steps for tables other than the one
@@ -1044,7 +1175,7 @@ function migrateRows(rows, steps) {
1044
1175
  });
1045
1176
  }
1046
1177
  /**
1047
- * Run the driver-conformance battery against a fresh {@link DriverInterface}
1178
+ * Walks the driver-conformance battery against a fresh {@link DriverInterface}
1048
1179
  * per phase, yielding one {@link ConformanceFinding} per violated invariant —
1049
1180
  * the shared invariant suite every backend (in-memory, SQLite, IndexedDB)
1050
1181
  * must uphold to be a drop-in {@link DriverInterface}.
@@ -1055,26 +1186,26 @@ function migrateRows(rows, steps) {
1055
1186
  * driver's own README. Opens a fixed two-table schema (`users` keyed by the
1056
1187
  * default `id`, `posts` keyed by a non-id `slug`) and, calling `factory()`
1057
1188
  * 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
1189
+ * `read` of a missing key returns `undefined`; `write`/`read` round-trip
1190
+ * with DEEP copy-in/copy-out isolation (mutating the caller's row —
1191
+ * including a NESTED field — after `write`, or a row `read` returns, never
1192
+ * perturbs stored state) and upsert-overwrite; simultaneous same-key
1193
+ * `insert` calls produce exactly one commit and one `CONFLICT`; pre-aborted
1194
+ * `write`, `insert`, and `delete` calls leave storage unchanged; `delete`
1195
+ * returns `true` then `false`; `keys`/`scan` yield in ascending key order;
1196
+ * `clear` empties only its target table; `snapshot`'s rollback thunk
1197
+ * restores pre-snapshot state, including a NESTED field mutated in place on
1198
+ * a read-back row between capture and restore; a scoped
1199
+ * `snapshot(['users'])` rolls back only the named table, leaving a
1200
+ * concurrent mutation to another table intact; a non-`id` primary key
1201
+ * (`posts.slug`) round-trips; a nested-object row round-trips structurally
1202
+ * (through {@link equalsValue}). The optional surface is presence-gated: when
1203
+ * `migrate` exists, a `column.remove` plan strips the column from stored
1204
+ * rows and a plan referencing an unknown table throws `DatabaseError`
1205
+ * `MIGRATION`; when `stream` exists, it yields only condition-matching rows
1206
+ * and honors `offset`/`limit`; when `transaction` exists, `commit` persists
1207
+ * and `rollback` restores; when both `metadata` and `stamp` exist, a fresh
1208
+ * store's `metadata()` is `undefined`, and after
1078
1209
  * `stamp({ version, schema })`, `metadata()` returns the exact stamped value.
1079
1210
  *
1080
1211
  * Each phase runs within a `try`/`catch`: an EXPECTED mismatch yields a
@@ -1093,62 +1224,14 @@ function migrateRows(rows, steps) {
1093
1224
  *
1094
1225
  * @example
1095
1226
  * ```ts
1096
- * import { createMemoryDriver, driverFindings } from '@orkestrel/database'
1227
+ * import { createMemoryDriver, scanDriver } from '@orkestrel/database'
1097
1228
  *
1098
- * for await (const finding of driverFindings(() => createMemoryDriver())) {
1229
+ * for await (const finding of scanDriver(() => createMemoryDriver())) {
1099
1230
  * console.log(finding.check, finding.message)
1100
1231
  * }
1101
1232
  * ```
1102
1233
  */
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];
1234
+ async function* scanDriver(factory) {
1152
1235
  try {
1153
1236
  const driver = factory();
1154
1237
  await driver.open(CONFORMANCE_SCHEMA);
@@ -1935,14 +2018,14 @@ async function* driverFindings(factory) {
1935
2018
  }
1936
2019
  }
1937
2020
  /**
1938
- * Run the driver-conformance battery, throwing on the first violated
2021
+ * Runs the driver-conformance battery, throwing on the first violated
1939
2022
  * invariant — the fail-fast entry point most callers (test setup, CI smoke
1940
2023
  * checks) want.
1941
2024
  *
1942
2025
  * @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
2026
+ * Consumes only the first value {@link scanDriver} yields: because that
2027
+ * generator is lazy, every LATER phase never runs true fail-fast, not
2028
+ * merely "report only the first". The
1946
2029
  * thrown error is byte-compatible with the historical shape: a
1947
2030
  * `CONFORMANCE` {@link DatabaseError} whose `message` is the finding's
1948
2031
  * `message` and whose `context` is `{ check, ...finding.context }`.
@@ -1959,18 +2042,18 @@ async function* driverFindings(factory) {
1959
2042
  * ```
1960
2043
  */
1961
2044
  async function conformDriver(factory) {
1962
- for await (const finding of driverFindings(factory)) throw new DatabaseError("CONFORMANCE", finding.message, {
2045
+ for await (const finding of scanDriver(factory)) throw new DatabaseError("CONFORMANCE", finding.message, {
1963
2046
  check: finding.check,
1964
2047
  ...finding.context
1965
2048
  });
1966
2049
  }
1967
2050
  /**
1968
- * Run the FULL driver-conformance battery and collect every violation — the
2051
+ * Runs the FULL driver-conformance battery and collects every violation — the
1969
2052
  * audit entry point for a driver author who wants a complete report rather
1970
2053
  * than a single fail-fast throw.
1971
2054
  *
1972
2055
  * @remarks
1973
- * Drains {@link driverFindings} to completion: every phase runs regardless
2056
+ * Drains {@link scanDriver} to completion: every phase runs regardless
1974
2057
  * of earlier violations, so a driver breaking two independent invariants
1975
2058
  * reports both. An empty array means the driver is fully conformant.
1976
2059
  *
@@ -1987,26 +2070,32 @@ async function conformDriver(factory) {
1987
2070
  */
1988
2071
  async function auditDriver(factory) {
1989
2072
  const findings = [];
1990
- for await (const finding of driverFindings(factory)) findings.push(finding);
2073
+ for await (const finding of scanDriver(factory)) findings.push(finding);
1991
2074
  return findings;
1992
2075
  }
1993
2076
  //#endregion
1994
- //#region src/core/TransactionIterator.ts
2077
+ //#region src/core/ScopedIterator.ts
1995
2078
  /**
1996
- * The internal continuation boundary for one transaction-scoped async iterable.
2079
+ * Forms the internal continuation admission boundary for one scoped async iterable.
1997
2080
  *
1998
2081
  * @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.
2082
+ * Each continuation enters the owning {@link AdmissionInterface} ledger
2083
+ * independently, so an idle iterator never delays a transaction, a settlement,
2084
+ * or a close. `ready` runs inside the tracked continuation before the source
2085
+ * advances, which is where a root stream re-establishes the lazy connection and
2086
+ * a transaction-scoped stream does nothing. A continuation requested after
2087
+ * admission closes attempts source cleanup exactly once and leaves the iterator
2088
+ * terminal.
2002
2089
  */
2003
- var TransactionIterator = class {
2090
+ var ScopedIterator = class {
2004
2091
  #source;
2005
- #scope;
2092
+ #admission;
2093
+ #ready;
2006
2094
  #cleaned = false;
2007
- constructor(source, scope) {
2095
+ constructor(source, admission, ready) {
2008
2096
  this.#source = source[Symbol.asyncIterator]();
2009
- this.#scope = scope;
2097
+ this.#admission = admission;
2098
+ this.#ready = ready;
2010
2099
  }
2011
2100
  [Symbol.asyncIterator]() {
2012
2101
  return this;
@@ -2025,6 +2114,7 @@ var TransactionIterator = class {
2025
2114
  done: true,
2026
2115
  value: void 0
2027
2116
  };
2117
+ await this.#ready();
2028
2118
  const result = await this.#source.next();
2029
2119
  if (result.done === true) this.#cleaned = true;
2030
2120
  return result;
@@ -2052,8 +2142,8 @@ var TransactionIterator = class {
2052
2142
  throw error;
2053
2143
  }
2054
2144
  #continue(operation) {
2055
- if (!this.#scope.accepting) this.#cleanup();
2056
- return this.#scope.track(operation);
2145
+ if (!this.#admission.accepting) this.#cleanup();
2146
+ return this.#admission.track(operation);
2057
2147
  }
2058
2148
  #cleanup() {
2059
2149
  if (this.#cleaned) return;
@@ -2066,7 +2156,7 @@ var TransactionIterator = class {
2066
2156
  //#endregion
2067
2157
  //#region src/core/TransactionScope.ts
2068
2158
  /**
2069
- * The internal lifetime boundary for one database transaction callback.
2159
+ * Forms the internal lifetime boundary for one database transaction callback.
2070
2160
  *
2071
2161
  * @remarks
2072
2162
  * Promise operations enter synchronously through {@link track}. Closing stops new
@@ -2110,7 +2200,7 @@ var TransactionScope = class {
2110
2200
  return promise;
2111
2201
  }
2112
2202
  stream(source) {
2113
- return new TransactionIterator(source, this);
2203
+ return new ScopedIterator(source, this, () => Promise.resolve());
2114
2204
  }
2115
2205
  stop() {
2116
2206
  this.#accepting = false;
@@ -2123,7 +2213,7 @@ var TransactionScope = class {
2123
2213
  //#endregion
2124
2214
  //#region src/core/DatabaseContext.ts
2125
2215
  /**
2126
- * The internal shared owner behind every typed view of one database.
2216
+ * Owns the internal shared state behind every typed view of one database.
2127
2217
  *
2128
2218
  * @remarks
2129
2219
  * A context owns the driver, merged physical schema, lifecycle, observation,
@@ -2207,7 +2297,21 @@ var DatabaseContext = class {
2207
2297
  this.#emitter.emit("close");
2208
2298
  }
2209
2299
  connect() {
2210
- return this.#connect();
2300
+ if (this.#ready !== void 0) return this.#ready;
2301
+ if (this.#failure !== void 0) throw this.#failure.error;
2302
+ if (this.#status === "closed") throw new DatabaseError("CLOSED", `Database '${this.#name}' is closed`, { name: this.#name });
2303
+ const readiness = this.#driver.open(this.#schema).then(async () => {
2304
+ if (this.#status === "idle") {
2305
+ this.#status = "open";
2306
+ this.#emitter.emit("open");
2307
+ }
2308
+ await this.#reconcile();
2309
+ }).catch((error) => {
2310
+ if (this.#ready === readiness) this.#ready = void 0;
2311
+ throw error;
2312
+ });
2313
+ this.#ready = readiness;
2314
+ return readiness;
2211
2315
  }
2212
2316
  track(operation) {
2213
2317
  try {
@@ -2236,7 +2340,7 @@ var DatabaseContext = class {
2236
2340
  this.#transaction = token;
2237
2341
  try {
2238
2342
  await this.#drain();
2239
- await this.#connect();
2343
+ await this.connect();
2240
2344
  if (this.#driver.transaction !== void 0) {
2241
2345
  const rejection = {
2242
2346
  rejected: false,
@@ -2312,23 +2416,6 @@ var DatabaseContext = class {
2312
2416
  #outside() {
2313
2417
  if (this.#transaction !== void 0) throw new DatabaseError("CONFLICT", `Database '${this.#name}' has an active transaction`, { name: this.#name });
2314
2418
  }
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
2419
  async #drain() {
2333
2420
  if (this.#operations.size === 0) return;
2334
2421
  await Promise.allSettled(this.#operations);
@@ -2411,12 +2498,12 @@ var DatabaseContext = class {
2411
2498
  //#endregion
2412
2499
  //#region src/core/Cursor.ts
2413
2500
  /**
2414
- * A forward row cursor for bulk in-place mutation.
2501
+ * Walks a table's rows forward for bulk in-place mutation.
2415
2502
  *
2416
2503
  * @remarks
2417
2504
  * Iterates a snapshot of the table's keys captured when the cursor was opened,
2418
2505
  * 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
2506
+ * iteration cannot corrupt the walk, and a key removed mid-iteration is
2420
2507
  * skipped. `update` and `remove` act on the row at the current position.
2421
2508
  */
2422
2509
  var Cursor = class {
@@ -2503,84 +2590,9 @@ var Cursor = class {
2503
2590
  }
2504
2591
  };
2505
2592
  //#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
2593
  //#region src/core/Query.ts
2582
2594
  /**
2583
- * A fluent query builder bound to one table.
2595
+ * Builds a read against one table through a fluent chain.
2584
2596
  *
2585
2597
  * @remarks
2586
2598
  * Accumulates typed conditions, ordering, JS filters, and a page. Each builder
@@ -2641,7 +2653,7 @@ var Query = class {
2641
2653
  return this.#filtered(fetched).length;
2642
2654
  }
2643
2655
  /**
2644
- * Lazily evaluate conditions, filters, offset, and limit.
2656
+ * Evaluates conditions, filters, offset, and limit lazily.
2645
2657
  *
2646
2658
  * @param options - Optional abort options
2647
2659
  * @returns Matching rows in storage order
@@ -2693,18 +2705,18 @@ var Query = class {
2693
2705
  //#endregion
2694
2706
  //#region src/core/Table.ts
2695
2707
  /**
2696
- * A table — typed keyed CRUD plus fluent query and cursor access over a driver.
2708
+ * Exposes typed keyed CRUD plus fluent query and cursor access over a driver.
2697
2709
  *
2698
2710
  * @remarks
2699
2711
  * The table's contract is the load-bearing piece: writes go through `parse`
2700
2712
  * (coercing inputs and rejecting rows that don't fit with a `VALIDATION` throw),
2701
2713
  * 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
2714
+ * the table's type — no assertion), and `contract` is exposed for
2703
2715
  * introspection and seeding. The driver only stores and scans; all querying is
2704
2716
  * the shared core engine in `helpers.ts`.
2705
2717
  *
2706
2718
  * @remarks
2707
- * - **Observable (§13).** The owned {@link emitter} ({@link TableEventMap}) carries the
2719
+ * - **Observable.** The owned {@link emitter} ({@link TableEventMap}) carries the
2708
2720
  * per-row mutation moments — `write` (set / add / update), `remove`, `clear` — for
2709
2721
  * fire-and-forget observers (cache invalidation, sync, an audit log), ALONGSIDE the
2710
2722
  * database-level lifecycle. Events carry the affected KEY only (no value payload, to
@@ -2793,7 +2805,7 @@ var Table = class {
2793
2805
  });
2794
2806
  }
2795
2807
  /**
2796
- * Count contract-valid rows matching `input`'s conditions.
2808
+ * Counts contract-valid rows matching `input`'s conditions.
2797
2809
  *
2798
2810
  * @remarks
2799
2811
  * Paging is ignored. Candidate rows use the driver's native `records` hook
@@ -2818,11 +2830,11 @@ var Table = class {
2818
2830
  });
2819
2831
  }
2820
2832
  /**
2821
- * Compute an aggregate over `column` across rows matching `input`'s
2833
+ * Computes an aggregate over `column` across rows matching `input`'s
2822
2834
  * conditions.
2823
2835
  *
2824
2836
  * @remarks
2825
- * Like {@link count}, `aggregate` operates on STORED rows WITHOUT the
2837
+ * Unlike {@link count}, `aggregate` operates on STORED rows WITHOUT the
2826
2838
  * contract guard {@link records} / {@link scan} apply — a non-conforming
2827
2839
  * stored row still contributes to the computed aggregate when it matches
2828
2840
  * the conditions, even though it would never appear in `records()`'s
@@ -2847,7 +2859,7 @@ var Table = class {
2847
2859
  });
2848
2860
  }
2849
2861
  /**
2850
- * Stream the table's rows matching `input`, applying offset/limit paging.
2862
+ * Streams the table's rows matching `input`, applying offset/limit paging.
2851
2863
  *
2852
2864
  * @remarks
2853
2865
  * `input.limit` counts rows that pass both the input conditions and the
@@ -2862,7 +2874,7 @@ var Table = class {
2862
2874
  scan(input, options) {
2863
2875
  validatePage(input);
2864
2876
  const source = this.#scan(input, options);
2865
- if (this.#context !== void 0) return new DatabaseIterator(source, this.#context);
2877
+ if (this.#context !== void 0) return new ScopedIterator(source, this.#context, this.#ready);
2866
2878
  return this.#scope === void 0 ? source : this.#scope.stream(source);
2867
2879
  }
2868
2880
  set(rows, options) {
@@ -3106,7 +3118,7 @@ var Table = class {
3106
3118
  //#endregion
3107
3119
  //#region src/core/DatabaseTransaction.ts
3108
3120
  /**
3109
- * A table-only database view bound to one driver transaction scope.
3121
+ * Binds a table-only database view to one driver transaction scope.
3110
3122
  *
3111
3123
  * @typeParam T - The declared table shape map
3112
3124
  *
@@ -3133,25 +3145,17 @@ var DatabaseTransaction = class {
3133
3145
  }
3134
3146
  table(name) {
3135
3147
  this.#scope.check();
3136
- const columns = this.#columns(name);
3137
- return this.#build(name, this.#key(name), createContract(objectShape(columns)));
3148
+ const columns = requireColumns(this.#tables, name);
3149
+ return this.#build(name, resolvePrimary(this.#primary, name), createContract(objectShape(columns)));
3138
3150
  }
3139
3151
  #build(name, key, contract) {
3140
3152
  return new Table(() => Promise.resolve(), this.#driver, name, key, contract, this.#generate, this.#error, void 0, this.#scope);
3141
3153
  }
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
3154
  };
3151
3155
  //#endregion
3152
3156
  //#region src/core/Database.ts
3153
3157
  /**
3154
- * A typed database view over one shared internal lifecycle and storage context.
3158
+ * Exposes a typed view over one shared internal lifecycle and storage context.
3155
3159
  *
3156
3160
  * @remarks
3157
3161
  * Each view owns only its table contracts, primary columns, indexes, and key
@@ -3184,8 +3188,8 @@ var Database = class Database {
3184
3188
  }
3185
3189
  table(name) {
3186
3190
  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)));
3191
+ const columns = requireColumns(this.#tables, name);
3192
+ return this.#build(name, resolvePrimary(this.#primary, name), createContract(objectShape(columns)));
3189
3193
  }
3190
3194
  import(tables, primary) {
3191
3195
  return this.#spawn(tables, {
@@ -3196,9 +3200,9 @@ var Database = class Database {
3196
3200
  export() {
3197
3201
  const result = {};
3198
3202
  for (const name of Object.keys(this.#tables)) {
3199
- const columns = this.#columns(name);
3203
+ const columns = requireColumns(this.#tables, name);
3200
3204
  result[name] = {
3201
- primary: this.#key(name),
3205
+ primary: resolvePrimary(this.#primary, name),
3202
3206
  columns,
3203
3207
  schema: compileSchema(objectShape(columns))
3204
3208
  };
@@ -3234,20 +3238,12 @@ var Database = class Database {
3234
3238
  ...this.#context.version === void 0 ? {} : { version: this.#context.version }
3235
3239
  }, this.#context);
3236
3240
  }
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
3241
  #schema() {
3246
3242
  return Object.keys(this.#tables).map((name) => {
3247
- const columns = this.#columns(name);
3243
+ const columns = requireColumns(this.#tables, name);
3248
3244
  return {
3249
3245
  name,
3250
- primary: this.#key(name),
3246
+ primary: resolvePrimary(this.#primary, name),
3251
3247
  columns: Object.entries(columns).map(([column, shape]) => shapeToColumnSchema(column, shape)),
3252
3248
  indexes: this.#indexes[name] ?? []
3253
3249
  };
@@ -3283,16 +3279,16 @@ var Database = class Database {
3283
3279
  //#endregion
3284
3280
  //#region src/core/drivers/MemoryDriver.ts
3285
3281
  /**
3286
- * The reference {@link DriverInterface} — nested maps, no I/O.
3282
+ * Implements the reference {@link DriverInterface} — nested maps, no I/O.
3287
3283
  *
3288
3284
  * @remarks
3289
3285
  * The in-between made concrete: it runs identically in a browser or on a server,
3290
3286
  * 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
3287
+ * the database API without a persistent backend. Rows are DEEP-copied (through
3292
3288
  * `structuredClone`) in and out — at `write`, `read`, `scan`, `stream`, and both
3293
3289
  * snapshot capture and restore — so a caller mutating a nested field of an input
3294
3290
  * 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
3291
+ * can never perturb stored state; a shallow `{ ...row }` spread
3296
3292
  * would still share nested object/array references. Metadata instead routes
3297
3293
  * through `cloneDriverMetadata`: `stamp` and migration snapshot exact JSON at
3298
3294
  * ingress, and `metadata` returns a distinct deeply frozen owned copy. `snapshot`
@@ -3354,19 +3350,19 @@ var MemoryDriver = class {
3354
3350
  }
3355
3351
  }
3356
3352
  /**
3357
- * Natively filtered lazy iteration — the {@link DriverInterface.stream} hook.
3353
+ * Iterates rows lazily with native filtering — the {@link DriverInterface.stream} hook.
3358
3354
  *
3359
3355
  * @remarks
3360
3356
  * Iterates the table's keys in the same key order `scan` and `keys` yield
3361
3357
  * (sorted by {@link compareValues}), testing each row against
3362
- * `input.conditions` (via {@link matchesQuery}) before counting it
3358
+ * `input.conditions` (through {@link matchesQuery}) before counting it
3363
3359
  * toward `offset` / `limit`. Both are applied lazily as matches are found —
3364
3360
  * `offset` matches are skipped without being yielded, and iteration stops the
3365
3361
  * instant `limit` yields have been produced, so a large table is never fully
3366
3362
  * walked for a small page. `input.order` is IGNORED (the same contract as
3367
3363
  * `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.
3364
+ * order, sorted output is `records()`'s job. Rows yield copy-out, and an
3365
+ * unknown table mirrors `scan`'s empty-yield behavior.
3370
3366
  *
3371
3367
  * @param table - The table to stream
3372
3368
  * @param input - The filter / offset / limit to apply lazily
@@ -3382,31 +3378,11 @@ var MemoryDriver = class {
3382
3378
  validatePage(input);
3383
3379
  return this.#stream(table, input);
3384
3380
  }
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
3381
  async clear(table) {
3406
3382
  this.#store(table).clear();
3407
3383
  }
3408
3384
  /**
3409
- * Capture the current state and return a thunk that rolls back to it.
3385
+ * Captures the current state and returns a thunk that rolls back to it.
3410
3386
  *
3411
3387
  * @remarks
3412
3388
  * Capture owns rows, schema, and one session-local table identity. Replay
@@ -3463,7 +3439,7 @@ var MemoryDriver = class {
3463
3439
  };
3464
3440
  }
3465
3441
  /**
3466
- * Return the persisted {@link DriverMetadata}, or `undefined` when the store has
3442
+ * Returns the persisted {@link DriverMetadata}, or `undefined` when the store has
3467
3443
  * never been stamped.
3468
3444
  *
3469
3445
  * @remarks
@@ -3478,7 +3454,7 @@ var MemoryDriver = class {
3478
3454
  return this.#metadata === void 0 ? void 0 : cloneDriverMetadata(this.#metadata);
3479
3455
  }
3480
3456
  /**
3481
- * Persist an owned snapshot for a later `metadata()` to return.
3457
+ * Persists an owned snapshot for a later `metadata()` to return.
3482
3458
  *
3483
3459
  * @param metadata - The {@link DriverMetadata} to persist
3484
3460
  */
@@ -3486,7 +3462,7 @@ var MemoryDriver = class {
3486
3462
  this.#metadata = cloneDriverMetadata(metadata);
3487
3463
  }
3488
3464
  /**
3489
- * Apply a {@link Migration} plan's steps against the in-memory store.
3465
+ * Applies a {@link Migration} plan's steps against the in-memory store.
3490
3466
  *
3491
3467
  * @remarks
3492
3468
  * Steps apply against an isolated candidate. Rows, schema changes, and
@@ -3568,6 +3544,26 @@ var MemoryDriver = class {
3568
3544
  case "index.remove": this.#require(tables, step.table);
3569
3545
  }
3570
3546
  }
3547
+ async *#stream(table, input) {
3548
+ const store = this.#store(table);
3549
+ const conditions = input.conditions;
3550
+ const offset = input.offset ?? 0;
3551
+ const limit = input.limit;
3552
+ let skipped = 0;
3553
+ let yielded = 0;
3554
+ for (const key of this.#ordered(table)) {
3555
+ if (limit !== void 0 && yielded >= limit) return;
3556
+ const row = store.get(key);
3557
+ if (row === void 0) continue;
3558
+ if (conditions !== void 0 && conditions.length > 0 && !matchesQuery(row, conditions)) continue;
3559
+ if (skipped < offset) {
3560
+ skipped += 1;
3561
+ continue;
3562
+ }
3563
+ yield structuredClone(row);
3564
+ yielded += 1;
3565
+ }
3566
+ }
3571
3567
  #store(table) {
3572
3568
  this.#table(table);
3573
3569
  const store = this.#tables.get(table);
@@ -3578,15 +3574,15 @@ var MemoryDriver = class {
3578
3574
  //#endregion
3579
3575
  //#region src/core/factories.ts
3580
3576
  /**
3581
- * Create a database over a driver and a declared `tables` schema.
3577
+ * Creates a database over a driver and a declared `tables` schema.
3582
3578
  *
3583
3579
  * @remarks
3584
3580
  * `tables` maps each name to its columns (a `column → shape` map); the database
3585
3581
  * wraps each in an `objectShape`, so you never write `objectShape` at the table
3586
3582
  * level. The `const` type parameter captures the literal names and columns, so
3587
3583
  * `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.
3584
+ * columns — no annotations. Name a non-`id` primary-key column per table through
3585
+ * the optional `primary` and `indexes` maps.
3590
3586
  *
3591
3587
  * @param options - The driver, `tables`, and optional `primary`, `indexes`,
3592
3588
  * `name`, `generator`, `version`, and emitter hooks
@@ -3612,7 +3608,7 @@ function createDatabase(options) {
3612
3608
  return new Database(options);
3613
3609
  }
3614
3610
  /**
3615
- * Create the in-memory reference {@link DriverInterface}.
3611
+ * Creates the in-memory reference {@link DriverInterface}.
3616
3612
  *
3617
3613
  * @remarks
3618
3614
  * Backed by nested maps with no I/O — the same driver runs in a browser or on a
@@ -3624,6 +3620,122 @@ function createMemoryDriver() {
3624
3620
  return new MemoryDriver();
3625
3621
  }
3626
3622
  //#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 };
3623
+ //#region src/core/DriverIterator.ts
3624
+ /**
3625
+ * Forms the internal continuation boundary for a root driver async iterator.
3626
+ *
3627
+ * @remarks
3628
+ * A driver transaction can begin while a caller holds an idle root iterator.
3629
+ * Every `next` therefore checks the driver's root-state guard immediately
3630
+ * before and after advancing the source. A failed continuation terminalizes the
3631
+ * iterator, discards any row produced before the post-advance guard failed, and
3632
+ * attempts source cleanup exactly once.
3633
+ *
3634
+ * A driver implementing the published `DriverInterface` extension seam wraps its
3635
+ * own source iterator in one so a root `scan` / `stream` cannot outlive the
3636
+ * driver state it was opened against.
3637
+ *
3638
+ * @typeParam T - The value the wrapped source yields
3639
+ *
3640
+ * @example
3641
+ * ```ts
3642
+ * import type { Row } from '@orkestrel/database'
3643
+ * import { DatabaseError, DriverIterator } from '@orkestrel/database'
3644
+ *
3645
+ * // Inside a driver's `scan`, over its own row source and root-state guard.
3646
+ * declare const rows: AsyncIterator<Row>
3647
+ * declare const transacting: () => boolean
3648
+ * const scan = new DriverIterator(rows, () => {
3649
+ * if (transacting()) {
3650
+ * throw new DatabaseError('CONFLICT', 'scan: a transaction is active')
3651
+ * }
3652
+ * })
3653
+ * for await (const row of scan) row // one row at a time, guarded around each advance
3654
+ * ```
3655
+ */
3656
+ var DriverIterator = class {
3657
+ #source;
3658
+ #guard;
3659
+ #terminal = false;
3660
+ #cleaned = false;
3661
+ /**
3662
+ * Wraps one source iterator in the continuation boundary.
3663
+ *
3664
+ * @param source - The driver's own row iterator, advanced once per `next`
3665
+ * @param guard - The root-state check, run immediately before and after each advance; it throws to terminalize the iteration
3666
+ */
3667
+ constructor(source, guard) {
3668
+ this.#source = source;
3669
+ this.#guard = guard;
3670
+ }
3671
+ [Symbol.asyncIterator]() {
3672
+ return this;
3673
+ }
3674
+ async next() {
3675
+ if (this.#terminal) return {
3676
+ done: true,
3677
+ value: void 0
3678
+ };
3679
+ try {
3680
+ this.#guard();
3681
+ const result = await this.#source.next();
3682
+ this.#guard();
3683
+ if (result.done === true) {
3684
+ this.#terminal = true;
3685
+ this.#cleaned = true;
3686
+ }
3687
+ return result;
3688
+ } catch (error) {
3689
+ this.#terminal = true;
3690
+ await this.#discard();
3691
+ throw error;
3692
+ }
3693
+ }
3694
+ async return() {
3695
+ if (this.#terminal) return {
3696
+ done: true,
3697
+ value: void 0
3698
+ };
3699
+ this.#terminal = true;
3700
+ if (this.#cleaned || this.#source.return === void 0) {
3701
+ this.#cleaned = true;
3702
+ return {
3703
+ done: true,
3704
+ value: void 0
3705
+ };
3706
+ }
3707
+ this.#cleaned = true;
3708
+ return this.#source.return();
3709
+ }
3710
+ async throw(error) {
3711
+ if (this.#terminal) throw error;
3712
+ if (this.#source.throw === void 0) {
3713
+ this.#terminal = true;
3714
+ await this.#discard();
3715
+ throw error;
3716
+ }
3717
+ try {
3718
+ const result = await this.#source.throw(error);
3719
+ if (result.done === true) {
3720
+ this.#terminal = true;
3721
+ this.#cleaned = true;
3722
+ }
3723
+ return result;
3724
+ } catch (cause) {
3725
+ this.#terminal = true;
3726
+ await this.#discard();
3727
+ throw cause;
3728
+ }
3729
+ }
3730
+ async #discard() {
3731
+ if (this.#cleaned) return;
3732
+ this.#cleaned = true;
3733
+ try {
3734
+ await this.#source.return?.();
3735
+ } catch {}
3736
+ }
3737
+ };
3738
+ //#endregion
3739
+ 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
3740
 
3629
3741
  //# sourceMappingURL=index.js.map