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