@orkestrel/database 0.0.13 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@ let _orkestrel_contract = require("@orkestrel/contract");
3
3
  let _orkestrel_emitter = require("@orkestrel/emitter");
4
4
  //#region src/core/constants.ts
5
5
  /**
6
- * Supplies the primary-key column assumed when {@link PrimaryMap} does not name one.
6
+ * Supplies the primary-key column, `'id'`, assumed when {@link PrimaryMap} does not name one.
7
7
  *
8
8
  * @remarks
9
9
  * `id` is the convention IndexedDB (`keyPath: 'id'`) and SQL (`id` / rowid) both
@@ -11,12 +11,13 @@ let _orkestrel_emitter = require("@orkestrel/emitter");
11
11
  */
12
12
  var DEFAULT_PRIMARY = "id";
13
13
  /**
14
- * Sets the longest `LIKE` / `GLOB` pattern the wildcard matcher accepts before rejecting it.
14
+ * Sets the longest `LIKE` / `GLOB` pattern the wildcard matcher accepts, 1024 characters, before
15
+ * rejecting it.
15
16
  *
16
17
  * @remarks
17
18
  * A `LIKE` / `GLOB` pattern is a caller-supplied operand, so
18
19
  * `matchesLikePattern` / `matchesGlobPattern` run patterns this package cannot
19
- * trust. The matcher is the LINEAR greedy two-pointer wildcard match — never a
20
+ * trust. The matcher is the linear greedy two-pointer wildcard match — never a
20
21
  * backtracking regex (`.*`-segments-separated-by-literals against a long input is the
21
22
  * catastrophic shape JS cannot bound without atomic groups), so it is O(value ×
22
23
  * pattern). Capping the pattern length bounds that pattern factor, leaving a match
@@ -90,11 +91,13 @@ var CONFORMANCE_POSTS_SCHEMA = Object.freeze({
90
91
  indexes: Object.freeze([])
91
92
  });
92
93
  /**
93
- * Holds the fixed two-table schema every driver-conformance phase opens.
94
+ * Holds the fixed `users` and `posts` schema every driver-conformance phase opens.
94
95
  *
95
96
  * @remarks
96
97
  * 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
+ * names a violated invariant rather than a setup difference between phases. The
99
+ * array and each schema in it are frozen, so a consumer holding it cannot change
100
+ * what a later phase opens.
98
101
  */
99
102
  var CONFORMANCE_SCHEMA = Object.freeze([CONFORMANCE_USERS_SCHEMA, CONFORMANCE_POSTS_SCHEMA]);
100
103
  //#endregion
@@ -157,6 +160,10 @@ function isKey(value) {
157
160
  /**
158
161
  * Checks whether a value is a portable column schema.
159
162
  *
163
+ * @remarks
164
+ * Total over any input: a hostile getter, a revoked proxy, or a cyclic value is
165
+ * contained as a non-match rather than a throw.
166
+ *
160
167
  * @param value - The value to test
161
168
  * @returns True if `value` is a complete {@link ColumnSchema}; false otherwise
162
169
  */
@@ -172,6 +179,10 @@ function isColumnSchema(value) {
172
179
  /**
173
180
  * Checks whether a value is a portable table schema.
174
181
  *
182
+ * @remarks
183
+ * Total over any input: a hostile getter, a revoked proxy, or a cyclic value is
184
+ * contained as a non-match rather than a throw.
185
+ *
175
186
  * @param value - The value to test
176
187
  * @returns True if `value` is a complete {@link TableSchema}; false otherwise
177
188
  */
@@ -191,6 +202,10 @@ function isTableSchema(value) {
191
202
  /**
192
203
  * Checks whether a value is a complete portable driver schema.
193
204
  *
205
+ * @remarks
206
+ * Total over any input: a hostile getter, a revoked proxy, or a cyclic value is
207
+ * contained as a non-match rather than a throw.
208
+ *
194
209
  * @param value - The value to test
195
210
  * @returns True if `value` is a table-schema collection with unique table names; false otherwise
196
211
  */
@@ -207,6 +222,10 @@ function isDriverSchema(value) {
207
222
  /**
208
223
  * Checks whether a value is one ordered migration step.
209
224
  *
225
+ * @remarks
226
+ * Total over any input: a hostile getter, a revoked proxy, or a cyclic value is
227
+ * contained as a non-match rather than a throw.
228
+ *
210
229
  * @param value - The value to test
211
230
  * @returns True if `value` is a complete {@link MigrationStep}; false otherwise
212
231
  */
@@ -231,6 +250,10 @@ function isMigrationStep(value) {
231
250
  /**
232
251
  * Checks whether a value is an ordered migration plan.
233
252
  *
253
+ * @remarks
254
+ * Total over any input: a hostile getter, a revoked proxy, or a cyclic value is
255
+ * contained as a non-match rather than a throw.
256
+ *
234
257
  * @param value - The value to test
235
258
  * @returns True if `value` is a complete {@link Migration}; false otherwise
236
259
  */
@@ -246,6 +269,12 @@ function isMigration(value) {
246
269
  /**
247
270
  * Checks whether a value is persisted driver metadata.
248
271
  *
272
+ * @remarks
273
+ * The boundary check a versioning driver's `metadata()` narrows a stored or
274
+ * deserialized record through, so no call site needs an assertion. Total over any
275
+ * input: a hostile getter, a revoked proxy, or a cyclic value is contained as a
276
+ * non-match rather than a throw.
277
+ *
249
278
  * @param value - The value to test
250
279
  * @returns True if `value` is complete {@link DriverMetadata}; false otherwise
251
280
  */
@@ -261,6 +290,10 @@ function isDriverMetadata(value) {
261
290
  /**
262
291
  * Checks whether a value is one atomic migration request.
263
292
  *
293
+ * @remarks
294
+ * Total over any input: a hostile getter, a revoked proxy, or a cyclic value is
295
+ * contained as a non-match rather than a throw.
296
+ *
264
297
  * @param value - The value to test
265
298
  * @returns True if `value` is a complete {@link MigrationInput}; false otherwise
266
299
  */
@@ -278,6 +311,11 @@ function isMigrationInput(value) {
278
311
  /**
279
312
  * Clones unknown driver metadata into a distinct deeply frozen snapshot.
280
313
  *
314
+ * @remarks
315
+ * The clone is validated as {@link DriverMetadata} before it is returned, so a
316
+ * malformed or hostile value throws a `VALIDATION` {@link DatabaseError} at
317
+ * `context.path === 'metadata'` rather than surfacing a raw Contract or caller error.
318
+ *
281
319
  * @param value - Unknown metadata
282
320
  * @returns Owned driver metadata
283
321
  */
@@ -297,6 +335,11 @@ function cloneDriverMetadata(value) {
297
335
  /**
298
336
  * Clones unknown driver schema into a distinct deeply frozen snapshot.
299
337
  *
338
+ * @remarks
339
+ * The clone is validated as a table-schema collection before it is returned, so a
340
+ * malformed or hostile value throws a `VALIDATION` {@link DatabaseError} at
341
+ * `context.path === 'schema'` rather than surfacing a raw Contract or caller error.
342
+ *
300
343
  * @param value - Unknown table schema collection
301
344
  * @returns Owned driver schema
302
345
  */
@@ -316,6 +359,11 @@ function cloneDriverSchema(value) {
316
359
  /**
317
360
  * Clones unknown migration input into a distinct deeply frozen snapshot.
318
361
  *
362
+ * @remarks
363
+ * The clone is validated as a {@link MigrationInput} before it is returned, so a
364
+ * malformed or hostile value throws a `VALIDATION` {@link DatabaseError} at
365
+ * `context.path === 'migration'` rather than surfacing a raw Contract or caller error.
366
+ *
319
367
  * @param value - Unknown migration input
320
368
  * @returns Owned migration input
321
369
  */
@@ -391,9 +439,9 @@ function compareValues(left, right) {
391
439
  * @remarks
392
440
  * Primitives compare by SameValueZero (`NaN` equals itself; `+0` equals `-0`).
393
441
  * Arrays compare by index (same length, every element `equalsValue`). Plain
394
- * records (through `isRecord`) compare by their OWN enumerable keys: same key
395
- * COUNT and, for every key in `left`, `right` has that key (`Object.hasOwn`)
396
- * with a `equalsValue` value — so a key present with value `undefined` is NOT
442
+ * records (through `isRecord`) compare by their own enumerable keys: same key
443
+ * count and, for every key in `left`, `right` has that key (`Object.hasOwn`)
444
+ * with a `equalsValue` value — so a key present with value `undefined` is not
397
445
  * equal to that key being absent (both differ in `Object.keys` membership).
398
446
  * Anything else (functions, class instances, mismatched shapes) falls through
399
447
  * to `false`. Container pairs are tracked iteratively, so self-referential and
@@ -459,16 +507,16 @@ function equalsValue(left, right) {
459
507
  }
460
508
  }
461
509
  /**
462
- * Matches a value against a wildcard pattern in LINEAR time — the shared, ReDoS-SAFE
510
+ * Matches a value against a wildcard pattern in linear time — the shared, ReDoS-safe
463
511
  * engine behind {@link matchesLikePattern} and {@link matchesGlobPattern}.
464
512
  *
465
513
  * @remarks
466
- * A backtracking RegExp (`a%b%c` → `^a.*b.*c$`) is CATASTROPHIC on a hostile pattern:
514
+ * A backtracking RegExp (`a%b%c` → `^a.*b.*c$`) is catastrophic on a hostile pattern:
467
515
  * `.*` segments separated by literals, matched against a long non-matching input, blow
468
516
  * up super-linearly — and JS has no atomic groups / possessive quantifiers to bound it,
469
517
  * 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:
471
- * the `any` wildcard records its position and, on a later mismatch, backtracks ONLY to
518
+ * trust. So this builds no regex. It runs the classic greedy two-pointer wildcard match:
519
+ * the `any` wildcard records its position and, on a later mismatch, backtracks only to
472
520
  * that last `any` (letting it absorb one more char) — so the work is O(value × pattern),
473
521
  * never the exponential / polynomial backtracking a regex would do. The pattern length
474
522
  * is capped at {@link MAX_PATTERN_LENGTH} (a `VALIDATION` {@link DatabaseError} over it),
@@ -476,16 +524,16 @@ function equalsValue(left, right) {
476
524
  * pattern.
477
525
  *
478
526
  * The `any` wildcard matches any run (including empty); `single` matches exactly one
479
- * char; every other pattern char matches itself LITERALLY (a pattern `.` / `(` / `\` is
527
+ * char; every other pattern char matches itself literally (a pattern `.` / `(` / `\` is
480
528
  * a literal — the regex-metacharacter hazard is gone with the regex). `any` is tested
481
- * BEFORE a literal match, so a value that literally contains the wildcard char never
482
- * shadows the wildcard. Case folding is applied to BOTH sides when `fold` is set.
529
+ * before a literal match, so a value that literally contains the wildcard char never
530
+ * shadows the wildcard. Case folding is applied to both sides when `fold` is set.
483
531
  *
484
532
  * @param value - The value to test
485
533
  * @param pattern - The wildcard pattern
486
534
  * @param any - The any-run wildcard char (`%` for `LIKE`, `*` for `GLOB`)
487
535
  * @param single - The single-char wildcard char (`_` for `LIKE`, `?` for `GLOB`)
488
- * @param fold - Whether to match case-INSENSITIVELY (`LIKE` folds; `GLOB` does not)
536
+ * @param fold - Whether to match case-insensitively (`LIKE` folds; `GLOB` does not)
489
537
  * @returns True if `value` matches `pattern`; false otherwise
490
538
  * @throws A `VALIDATION` {@link DatabaseError} when `pattern` exceeds {@link MAX_PATTERN_LENGTH}
491
539
  */
@@ -547,7 +595,7 @@ function matchesLikePattern(value, pattern) {
547
595
  * @remarks
548
596
  * `*` matches any run of characters (including none) and `?` matches exactly one
549
597
  * character; every other pattern character matches itself literally, so a
550
- * character class such as `[a-z]` is NOT interpreted. Runs on
598
+ * character class such as `[a-z]` is not interpreted. Runs on
551
599
  * {@link matchesWildcardPattern}, so the match is linear in the value length and
552
600
  * the pattern is capped at {@link MAX_PATTERN_LENGTH}.
553
601
  *
@@ -573,7 +621,7 @@ function matchesGlobPattern(value, pattern) {
573
621
  * string is one column; an array descends a nested value) — and applies the
574
622
  * operator. Range operators (`above` / `below` / `from` / `to` / `between`) use
575
623
  * {@link compareValues}, the total order; the equality family (`equals` / `not`
576
- * / `any` / `none`) uses {@link equalsValue} — STRUCTURAL equality, not the total
624
+ * / `any` / `none`) uses {@link equalsValue} — structural equality, not the total
577
625
  * order's rank-5-collapses-all-objects behavior, so `equals` on an object/array
578
626
  * operand only matches a structurally-equal value, never every row holding any
579
627
  * object. This is a semantics change from ranking: `equalsValue` is SameValueZero
@@ -731,7 +779,9 @@ function computeAggregate(rows, operation, column) {
731
779
  const total = numbers.reduce((sum, value) => sum + value, 0);
732
780
  return operation === "average" ? total / numbers.length : total;
733
781
  }
734
- return operation === "minimum" ? Math.min(...numbers) : Math.max(...numbers);
782
+ let result = operation === "minimum" ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
783
+ for (const value of numbers) result = operation === "minimum" ? Math.min(result, value) : Math.max(result, value);
784
+ return result;
735
785
  }
736
786
  /**
737
787
  * Reads a row's primary key from a column, when it is a usable {@link Key}.
@@ -909,16 +959,16 @@ function checkAbort(signal) {
909
959
  * plan labels only; versioning drivers persist and reconcile them through
910
960
  * {@link DriverMetadata}.
911
961
  *
912
- * A column present in BOTH schemas under the same name but with a different
962
+ * A column present in both schemas under the same name but with a different
913
963
  * `storage`, `optional`, or `nullable` value throws a `MIGRATION`
914
964
  * {@link DatabaseError} naming the table, the column, and the from→to
915
- * difference — a name-only diff would otherwise silently produce NO step for
965
+ * difference — a name-only diff would otherwise silently produce no step for
916
966
  * the drift, and versioned reconciliation would stamp over it. There is no
917
967
  * automatic in-place type-change step: the manual path is to add a new column,
918
968
  * copy/convert the data at the application layer, then remove the old column —
919
969
  * two separate plans, never a single implicit "alter" step.
920
970
  *
921
- * @param deployed - The table schemas currently applied
971
+ * @param deployed - The already-applied table schemas
922
972
  * @param declared - The table schemas the caller wants applied
923
973
  * @param from - The plan's source version label (defaults to `0`)
924
974
  * @param to - The plan's target version label (defaults to `1`)
@@ -1025,6 +1075,8 @@ function planMigration(deployed, declared, from = 0, to = 1) {
1025
1075
  }
1026
1076
  /**
1027
1077
  * Projects migration steps sequentially over a canonical validated owned schema.
1078
+ *
1079
+ * @remarks
1028
1080
  * Adding a required non-null column to an existing table rejects with
1029
1081
  * `MIGRATION`; optional-only and nullable-only additions remain portable.
1030
1082
  *
@@ -1188,14 +1240,14 @@ function migrateRows(rows, steps) {
1188
1240
  * default `id`, `posts` keyed by a non-id `slug`) and, calling `factory()`
1189
1241
  * fresh for each phase so failures stay isolated, verifies: `open`/`close`;
1190
1242
  * `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
1243
+ * with deep copy-in/copy-out isolation (mutating the caller's row —
1244
+ * including a nested field — after `write`, or a row `read` returns, never
1193
1245
  * perturbs stored state) and upsert-overwrite; simultaneous same-key
1194
1246
  * `insert` calls produce exactly one commit and one `CONFLICT`; pre-aborted
1195
1247
  * `write`, `insert`, and `delete` calls leave storage unchanged; `delete`
1196
1248
  * returns `true` then `false`; `keys`/`scan` yield in ascending key order;
1197
1249
  * `clear` empties only its target table; `snapshot`'s rollback thunk
1198
- * restores pre-snapshot state, including a NESTED field mutated in place on
1250
+ * restores pre-snapshot state, including a nested field mutated in place on
1199
1251
  * a read-back row between capture and restore; a scoped
1200
1252
  * `snapshot(['users'])` rolls back only the named table, leaving a
1201
1253
  * concurrent mutation to another table intact; a non-`id` primary key
@@ -1209,12 +1261,12 @@ function migrateRows(rows, steps) {
1209
1261
  * store's `metadata()` is `undefined`, and after
1210
1262
  * `stamp({ version, schema })`, `metadata()` returns the exact stamped value.
1211
1263
  *
1212
- * Each phase runs within a `try`/`catch`: an EXPECTED mismatch yields a
1213
- * finding built from the assertion, while an UNEXPECTED throw (a driver
1264
+ * Each phase runs within a `try`/`catch`: an expected mismatch yields a
1265
+ * finding built from the assertion, while an unexpected throw (a driver
1214
1266
  * crash mid-phase) is caught and yielded as a finding too, naming the phase
1215
1267
  * as `check` and carrying the caught error in `context.error` — a broken
1216
1268
  * driver can never escape the battery as an unhandled rejection. Within a
1217
- * phase, the FIRST violated assertion yields and the phase stops (matching
1269
+ * phase, the first violated assertion yields and the phase stops (matching
1218
1270
  * the historical fail-fast shape at phase granularity); the generator then
1219
1271
  * moves on to the next phase regardless. Because this is a **generator**,
1220
1272
  * consuming only the first yielded value reproduces true fail-fast (later
@@ -2025,11 +2077,13 @@ async function* scanDriver(factory) {
2025
2077
  *
2026
2078
  * @remarks
2027
2079
  * Consumes only the first value {@link scanDriver} yields: because that
2028
- * generator is lazy, every LATER phase never runs — true fail-fast, not
2080
+ * generator is lazy, every later phase never runs — true fail-fast, not
2029
2081
  * merely "report only the first". The
2030
2082
  * thrown error is byte-compatible with the historical shape: a
2031
2083
  * `CONFORMANCE` {@link DatabaseError} whose `message` is the finding's
2032
- * `message` and whose `context` is `{ check, ...finding.context }`.
2084
+ * `message` and whose `context` is `{ check, ...finding.context }`. The battery
2085
+ * takes a driver factory and reports through a throw, so it binds no test
2086
+ * framework and runs from any runner.
2033
2087
  *
2034
2088
  * @param factory - Mints a fresh, unopened driver instance (called once per phase)
2035
2089
  * @returns Nothing — resolves once every phase has passed
@@ -2049,7 +2103,7 @@ async function conformDriver(factory) {
2049
2103
  });
2050
2104
  }
2051
2105
  /**
2052
- * Runs the FULL driver-conformance battery and collects every violation — the
2106
+ * Runs the full driver-conformance battery and collects every violation — the
2053
2107
  * audit entry point for a driver author who wants a complete report rather
2054
2108
  * than a single fail-fast throw.
2055
2109
  *
@@ -2719,10 +2773,10 @@ var Query = class {
2719
2773
  * @remarks
2720
2774
  * - **Observable.** The owned {@link emitter} ({@link TableEventMap}) carries the
2721
2775
  * per-row mutation moments — `write` (set / add / update), `remove`, `clear` — for
2722
- * fire-and-forget observers (cache invalidation, sync, an audit log), ALONGSIDE the
2723
- * database-level lifecycle. Events carry the affected KEY only (no value payload, to
2776
+ * fire-and-forget observers (cache invalidation, sync, an audit log), alongside the
2777
+ * database-level lifecycle. Events carry the affected key only (no value payload, to
2724
2778
  * keep fan-out lean); reads / queries / counts are not emitted. Every event is emitted
2725
- * directly, strictly AFTER the driver write / delete / clear completes; the emitter
2779
+ * directly, strictly after the driver write / delete / clear completes; the emitter
2726
2780
  * isolates a listener throw and routes it to its `error` handler (the `error` option),
2727
2781
  * so a buggy observer can never corrupt a write or perturb a transaction.
2728
2782
  */
@@ -2835,7 +2889,7 @@ var Table = class {
2835
2889
  * conditions.
2836
2890
  *
2837
2891
  * @remarks
2838
- * Unlike {@link count}, `aggregate` operates on STORED rows WITHOUT the
2892
+ * Unlike {@link count}, `aggregate` operates on stored rows without the
2839
2893
  * contract guard {@link records} / {@link scan} apply — a non-conforming
2840
2894
  * stored row still contributes to the computed aggregate when it matches
2841
2895
  * the conditions, even though it would never appear in `records()`'s
@@ -3163,6 +3217,10 @@ var DatabaseTransaction = class {
3163
3217
  * generator. Imported views register their physical schemas with the same
3164
3218
  * internal context before opening begins, so every view observes one driver,
3165
3219
  * merged schema, emitter, status, transaction boundary, and terminal close.
3220
+ *
3221
+ * The view owns the driver and its declared `tables`, connects that driver lazily on
3222
+ * first use, `import`s further tables and `export`s their portable definitions, and
3223
+ * runs `transaction` scopes over the shared context.
3166
3224
  */
3167
3225
  var Database = class Database {
3168
3226
  #context;
@@ -3285,7 +3343,7 @@ var Database = class Database {
3285
3343
  * @remarks
3286
3344
  * The in-between made concrete: it runs identically in a browser or on a server,
3287
3345
  * so it is the storage behind tests, ephemeral caches, and any code that wants
3288
- * the database API without a persistent backend. Rows are DEEP-copied (through
3346
+ * the database API without a persistent backend. Rows are deep-copied (through
3289
3347
  * `structuredClone`) in and out — at `write`, `read`, `scan`, `stream`, and both
3290
3348
  * snapshot capture and restore — so a caller mutating a nested field of an input
3291
3349
  * row, a returned row, or a row mutated in place between snapshot and rollback
@@ -3360,7 +3418,7 @@ var MemoryDriver = class {
3360
3418
  * toward `offset` / `limit`. Both are applied lazily as matches are found —
3361
3419
  * `offset` matches are skipped without being yielded, and iteration stops the
3362
3420
  * instant `limit` yields have been produced, so a large table is never fully
3363
- * walked for a small page. `input.order` is IGNORED (the same contract as
3421
+ * walked for a small page. `input.order` is ignored (the same contract as
3364
3422
  * `TableInterface.scan` and `QueryInterface.stream`): streaming yields key
3365
3423
  * order, sorted output is `records()`'s job. Rows yield copy-out, and an
3366
3424
  * unknown table mirrors `scan`'s empty-yield behavior.
@@ -3589,20 +3647,29 @@ var MemoryDriver = class {
3589
3647
  * `name`, `generator`, `version`, and emitter hooks
3590
3648
  * @returns A typed {@link DatabaseInterface}
3591
3649
  *
3592
- * @example
3650
+ * @example Create a database
3593
3651
  * ```ts
3594
3652
  * import { createDatabase, createMemoryDriver } from '@orkestrel/database'
3595
3653
  * import { integerShape, stringShape } from '@orkestrel/contract'
3596
3654
  *
3597
3655
  * const db = createDatabase({
3598
- * driver: createMemoryDriver(),
3656
+ * driver: createMemoryDriver(), // any DriverInterface — a persistent backend swaps in, same API
3599
3657
  * tables: {
3600
- * users: { id: stringShape(), age: integerShape() },
3658
+ * users: { id: stringShape(), name: stringShape(), age: integerShape() },
3601
3659
  * posts: { slug: stringShape(), title: stringShape() },
3602
3660
  * },
3603
- * primary: { posts: 'slug' },
3661
+ * primary: { posts: 'slug' }, // non-`id` primary-key columns, per table
3604
3662
  * })
3605
- * await db.table('users').set({ id: 'u1', age: 36 }) // typed; coerced + validated
3663
+ *
3664
+ * const users = db.table('users') // hold the handle; TableInterface<{ id; name; age }>
3665
+ *
3666
+ * await users.set({ id: 'u1', name: 'Ada', age: 36 }) // coerced + validated through the contract
3667
+ * await users.get('u1') // typed { id; name; age } | undefined — narrowed, never `as`
3668
+ * await users
3669
+ * .query()
3670
+ * .condition({ column: 'age', operator: 'from', values: [18], connector: 'and' })
3671
+ * .order({ column: 'age', direction: 'descending' })
3672
+ * .collect() // typed rows
3606
3673
  * ```
3607
3674
  */
3608
3675
  function createDatabase(options) {