@orkestrel/database 0.0.13 → 0.0.15

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.
@@ -1,8 +1,8 @@
1
- import { cloneJSONRecord, cloneJSONValue, compileGuard, compileSchema, createContract, isArray, isRecord, isString, objectShape, parseNumber, resolveField } from "@orkestrel/contract";
1
+ import { arrayOf, cloneJSONRecord, cloneJSONValue, compileGuard, compileSchema, createContract, holds, isArray, isBoolean, isError, isFiniteNumber, isInstance, isInteger, isNumber, isRecord, isString, objectShape, parseNumber, resolveField } from "@orkestrel/contract";
2
2
  import { Emitter } from "@orkestrel/emitter";
3
3
  //#region src/core/constants.ts
4
4
  /**
5
- * Supplies the primary-key column assumed when {@link PrimaryMap} does not name one.
5
+ * Supplies the primary-key column, `'id'`, assumed when {@link PrimaryMap} does not name one.
6
6
  *
7
7
  * @remarks
8
8
  * `id` is the convention IndexedDB (`keyPath: 'id'`) and SQL (`id` / rowid) both
@@ -10,12 +10,13 @@ import { Emitter } from "@orkestrel/emitter";
10
10
  */
11
11
  var DEFAULT_PRIMARY = "id";
12
12
  /**
13
- * Sets the longest `LIKE` / `GLOB` pattern the wildcard matcher accepts before rejecting it.
13
+ * Sets the longest `LIKE` / `GLOB` pattern the wildcard matcher accepts, 1024 characters, before
14
+ * rejecting it.
14
15
  *
15
16
  * @remarks
16
17
  * A `LIKE` / `GLOB` pattern is a caller-supplied operand, so
17
18
  * `matchesLikePattern` / `matchesGlobPattern` run patterns this package cannot
18
- * trust. The matcher is the LINEAR greedy two-pointer wildcard match — never a
19
+ * trust. The matcher is the linear greedy two-pointer wildcard match — never a
19
20
  * backtracking regex (`.*`-segments-separated-by-literals against a long input is the
20
21
  * catastrophic shape JS cannot bound without atomic groups), so it is O(value ×
21
22
  * pattern). Capping the pattern length bounds that pattern factor, leaving a match
@@ -89,11 +90,13 @@ var CONFORMANCE_POSTS_SCHEMA = Object.freeze({
89
90
  indexes: Object.freeze([])
90
91
  });
91
92
  /**
92
- * Holds the fixed two-table schema every driver-conformance phase opens.
93
+ * Holds the fixed `users` and `posts` schema every driver-conformance phase opens.
93
94
  *
94
95
  * @remarks
95
96
  * Each phase mints a fresh driver and opens this exact schema, so a finding
96
- * names a violated invariant rather than a setup difference between phases.
97
+ * names a violated invariant rather than a setup difference between phases. The
98
+ * array and each schema in it are frozen, so a consumer holding it cannot change
99
+ * what a later phase opens.
97
100
  */
98
101
  var CONFORMANCE_SCHEMA = Object.freeze([CONFORMANCE_USERS_SCHEMA, CONFORMANCE_POSTS_SCHEMA]);
99
102
  //#endregion
@@ -140,7 +143,7 @@ var DatabaseError = class extends Error {
140
143
  * ```
141
144
  */
142
145
  function isDatabaseError(value) {
143
- return value instanceof DatabaseError;
146
+ return isInstance(value, DatabaseError);
144
147
  }
145
148
  //#endregion
146
149
  //#region src/core/validators.ts
@@ -151,132 +154,153 @@ function isDatabaseError(value) {
151
154
  * @returns True if `value` is a string or a finite number; false otherwise
152
155
  */
153
156
  function isKey(value) {
154
- return typeof value === "string" || typeof value === "number" && Number.isFinite(value);
157
+ return isString(value) || isFiniteNumber(value);
155
158
  }
156
159
  /**
157
160
  * Checks whether a value is a portable column schema.
158
161
  *
162
+ * @remarks
163
+ * Total over any input: a hostile getter, a revoked proxy, or a cyclic value is
164
+ * contained as a non-match rather than a throw.
165
+ *
159
166
  * @param value - The value to test
160
167
  * @returns True if `value` is a complete {@link ColumnSchema}; false otherwise
161
168
  */
162
169
  function isColumnSchema(value) {
163
- try {
170
+ return holds(() => {
164
171
  const column = cloneJSONRecord(value);
165
172
  const keys = Object.keys(column);
166
- return keys.length === 4 && keys.includes("name") && keys.includes("storage") && keys.includes("optional") && keys.includes("nullable") && typeof column.name === "string" && column.name.length > 0 && (column.storage === "text" || column.storage === "integer" || column.storage === "real" || column.storage === "boolean" || column.storage === "json" || column.storage === "blob") && typeof column.optional === "boolean" && typeof column.nullable === "boolean";
167
- } catch {
168
- return false;
169
- }
173
+ return keys.length === 4 && keys.includes("name") && keys.includes("storage") && keys.includes("optional") && keys.includes("nullable") && isString(column.name) && column.name.length > 0 && (column.storage === "text" || column.storage === "integer" || column.storage === "real" || column.storage === "boolean" || column.storage === "json" || column.storage === "blob") && isBoolean(column.optional) && isBoolean(column.nullable);
174
+ });
170
175
  }
171
176
  /**
172
177
  * Checks whether a value is a portable table schema.
173
178
  *
179
+ * @remarks
180
+ * Total over any input: a hostile getter, a revoked proxy, or a cyclic value is
181
+ * contained as a non-match rather than a throw.
182
+ *
174
183
  * @param value - The value to test
175
184
  * @returns True if `value` is a complete {@link TableSchema}; false otherwise
176
185
  */
177
186
  function isTableSchema(value) {
178
- try {
187
+ return holds(() => {
179
188
  const table = cloneJSONRecord(value);
180
189
  const keys = Object.keys(table);
181
- if (keys.length !== 4 || !keys.includes("name") || !keys.includes("primary") || !keys.includes("columns") || !keys.includes("indexes") || typeof table.name !== "string" || table.name.length === 0 || typeof table.primary !== "string" || table.primary.length === 0 || !Array.isArray(table.columns) || !Array.isArray(table.indexes) || !table.columns.every(isColumnSchema)) return false;
190
+ if (keys.length !== 4 || !keys.includes("name") || !keys.includes("primary") || !keys.includes("columns") || !keys.includes("indexes") || !isString(table.name) || table.name.length === 0 || !isString(table.primary) || table.primary.length === 0 || !arrayOf(isColumnSchema)(table.columns) || !isArray(table.indexes)) return false;
182
191
  const names = table.columns.map((column) => column.name);
183
- if (new Set(names).size !== names.length || !names.includes(table.primary) || !table.indexes.every((index) => Array.isArray(index) && index.length > 0 && index.every((column) => typeof column === "string" && names.includes(column)))) return false;
192
+ if (new Set(names).size !== names.length || !names.includes(table.primary) || !table.indexes.every((index) => isArray(index) && index.length > 0 && index.every((column) => isString(column) && names.includes(column)))) return false;
184
193
  const indexes = table.indexes.map((index) => JSON.stringify(index));
185
194
  return new Set(indexes).size === indexes.length;
186
- } catch {
187
- return false;
188
- }
195
+ });
189
196
  }
190
197
  /**
191
198
  * Checks whether a value is a complete portable driver schema.
192
199
  *
200
+ * @remarks
201
+ * Total over any input: a hostile getter, a revoked proxy, or a cyclic value is
202
+ * contained as a non-match rather than a throw.
203
+ *
193
204
  * @param value - The value to test
194
205
  * @returns True if `value` is a table-schema collection with unique table names; false otherwise
195
206
  */
196
207
  function isDriverSchema(value) {
197
- try {
208
+ return holds(() => {
198
209
  const schema = cloneJSONValue(value);
199
- if (!Array.isArray(schema) || !schema.every(isTableSchema)) return false;
210
+ if (!arrayOf(isTableSchema)(schema)) return false;
200
211
  const names = schema.map((table) => table.name);
201
212
  return new Set(names).size === names.length;
202
- } catch {
203
- return false;
204
- }
213
+ });
205
214
  }
206
215
  /**
207
216
  * Checks whether a value is one ordered migration step.
208
217
  *
218
+ * @remarks
219
+ * Total over any input: a hostile getter, a revoked proxy, or a cyclic value is
220
+ * contained as a non-match rather than a throw.
221
+ *
209
222
  * @param value - The value to test
210
223
  * @returns True if `value` is a complete {@link MigrationStep}; false otherwise
211
224
  */
212
225
  function isMigrationStep(value) {
213
- try {
226
+ return holds(() => {
214
227
  const step = cloneJSONRecord(value);
215
- if (typeof step.operation !== "string") return false;
228
+ if (!isString(step.operation)) return false;
216
229
  const keys = Object.keys(step);
217
230
  switch (step.operation) {
218
231
  case "table.add": return keys.length === 2 && keys.includes("operation") && keys.includes("table") && isTableSchema(step.table);
219
- case "table.remove": return keys.length === 2 && keys.includes("operation") && keys.includes("table") && typeof step.table === "string" && step.table.length > 0;
220
- case "column.add": return keys.length === 3 && keys.includes("operation") && keys.includes("table") && keys.includes("column") && typeof step.table === "string" && step.table.length > 0 && isColumnSchema(step.column);
221
- case "column.remove": return keys.length === 3 && keys.includes("operation") && keys.includes("table") && keys.includes("column") && typeof step.table === "string" && step.table.length > 0 && typeof step.column === "string" && step.column.length > 0;
232
+ case "table.remove": return keys.length === 2 && keys.includes("operation") && keys.includes("table") && isString(step.table) && step.table.length > 0;
233
+ case "column.add": return keys.length === 3 && keys.includes("operation") && keys.includes("table") && keys.includes("column") && isString(step.table) && step.table.length > 0 && isColumnSchema(step.column);
234
+ case "column.remove": return keys.length === 3 && keys.includes("operation") && keys.includes("table") && keys.includes("column") && isString(step.table) && step.table.length > 0 && isString(step.column) && step.column.length > 0;
222
235
  case "index.add":
223
- case "index.remove": return keys.length === 3 && keys.includes("operation") && keys.includes("table") && keys.includes("index") && typeof step.table === "string" && step.table.length > 0 && Array.isArray(step.index) && step.index.length > 0 && step.index.every((column) => typeof column === "string" && column.length > 0);
236
+ case "index.remove": return keys.length === 3 && keys.includes("operation") && keys.includes("table") && keys.includes("index") && isString(step.table) && step.table.length > 0 && isArray(step.index) && step.index.length > 0 && step.index.every((column) => isString(column) && column.length > 0);
224
237
  default: return false;
225
238
  }
226
- } catch {
227
- return false;
228
- }
239
+ });
229
240
  }
230
241
  /**
231
242
  * Checks whether a value is an ordered migration plan.
232
243
  *
244
+ * @remarks
245
+ * Total over any input: a hostile getter, a revoked proxy, or a cyclic value is
246
+ * contained as a non-match rather than a throw.
247
+ *
233
248
  * @param value - The value to test
234
249
  * @returns True if `value` is a complete {@link Migration}; false otherwise
235
250
  */
236
251
  function isMigration(value) {
237
- try {
252
+ return holds(() => {
238
253
  const migration = cloneJSONRecord(value);
239
254
  const keys = Object.keys(migration);
240
- return keys.length === 3 && keys.includes("from") && keys.includes("to") && keys.includes("steps") && typeof migration.from === "number" && Number.isFinite(migration.from) && typeof migration.to === "number" && Number.isFinite(migration.to) && Array.isArray(migration.steps) && migration.steps.every(isMigrationStep);
241
- } catch {
242
- return false;
243
- }
255
+ return keys.length === 3 && keys.includes("from") && keys.includes("to") && keys.includes("steps") && isFiniteNumber(migration.from) && isFiniteNumber(migration.to) && isArray(migration.steps) && migration.steps.every(isMigrationStep);
256
+ });
244
257
  }
245
258
  /**
246
259
  * Checks whether a value is persisted driver metadata.
247
260
  *
261
+ * @remarks
262
+ * The boundary check a versioning driver's `metadata()` narrows a stored or
263
+ * deserialized record through, so no call site needs an assertion. Total over any
264
+ * input: a hostile getter, a revoked proxy, or a cyclic value is contained as a
265
+ * non-match rather than a throw.
266
+ *
248
267
  * @param value - The value to test
249
268
  * @returns True if `value` is complete {@link DriverMetadata}; false otherwise
250
269
  */
251
270
  function isDriverMetadata(value) {
252
- try {
271
+ return holds(() => {
253
272
  const metadata = cloneJSONRecord(value);
254
273
  const keys = Object.keys(metadata);
255
- return keys.length === 2 && keys.includes("version") && keys.includes("schema") && typeof metadata.version === "number" && Number.isFinite(metadata.version) && isDriverSchema(metadata.schema);
256
- } catch {
257
- return false;
258
- }
274
+ return keys.length === 2 && keys.includes("version") && keys.includes("schema") && isFiniteNumber(metadata.version) && isDriverSchema(metadata.schema);
275
+ });
259
276
  }
260
277
  /**
261
278
  * Checks whether a value is one atomic migration request.
262
279
  *
280
+ * @remarks
281
+ * Total over any input: a hostile getter, a revoked proxy, or a cyclic value is
282
+ * contained as a non-match rather than a throw.
283
+ *
263
284
  * @param value - The value to test
264
285
  * @returns True if `value` is a complete {@link MigrationInput}; false otherwise
265
286
  */
266
287
  function isMigrationInput(value) {
267
- try {
288
+ return holds(() => {
268
289
  const input = cloneJSONRecord(value);
269
290
  const keys = Object.keys(input);
270
291
  return (keys.length === 1 || keys.length === 2) && keys.includes("plan") && (keys.length === 1 || keys.includes("metadata")) && isMigration(input.plan) && (input.metadata === void 0 || isDriverMetadata(input.metadata));
271
- } catch {
272
- return false;
273
- }
292
+ });
274
293
  }
275
294
  //#endregion
276
295
  //#region src/core/cloners.ts
277
296
  /**
278
297
  * Clones unknown driver metadata into a distinct deeply frozen snapshot.
279
298
  *
299
+ * @remarks
300
+ * The clone is validated as {@link DriverMetadata} before it is returned, so a
301
+ * malformed or hostile value throws a `VALIDATION` {@link DatabaseError} at
302
+ * `context.path === 'metadata'` rather than surfacing a raw Contract or caller error.
303
+ *
280
304
  * @param value - Unknown metadata
281
305
  * @returns Owned driver metadata
282
306
  */
@@ -286,7 +310,7 @@ function cloneDriverMetadata(value) {
286
310
  if (isDriverMetadata(metadata)) return metadata;
287
311
  throw new DatabaseError("VALIDATION", "Driver metadata is invalid", { path: "metadata" });
288
312
  } catch (error) {
289
- if (error instanceof DatabaseError) throw error;
313
+ if (isDatabaseError(error)) throw error;
290
314
  throw new DatabaseError("VALIDATION", "Driver metadata is invalid", {
291
315
  path: "metadata",
292
316
  cause: error
@@ -296,6 +320,11 @@ function cloneDriverMetadata(value) {
296
320
  /**
297
321
  * Clones unknown driver schema into a distinct deeply frozen snapshot.
298
322
  *
323
+ * @remarks
324
+ * The clone is validated as a table-schema collection before it is returned, so a
325
+ * malformed or hostile value throws a `VALIDATION` {@link DatabaseError} at
326
+ * `context.path === 'schema'` rather than surfacing a raw Contract or caller error.
327
+ *
299
328
  * @param value - Unknown table schema collection
300
329
  * @returns Owned driver schema
301
330
  */
@@ -305,7 +334,7 @@ function cloneDriverSchema(value) {
305
334
  if (isDriverSchema(schema)) return schema;
306
335
  throw new DatabaseError("VALIDATION", "Driver schema is invalid", { path: "schema" });
307
336
  } catch (error) {
308
- if (error instanceof DatabaseError) throw error;
337
+ if (isDatabaseError(error)) throw error;
309
338
  throw new DatabaseError("VALIDATION", "Driver schema is invalid", {
310
339
  path: "schema",
311
340
  cause: error
@@ -315,6 +344,11 @@ function cloneDriverSchema(value) {
315
344
  /**
316
345
  * Clones unknown migration input into a distinct deeply frozen snapshot.
317
346
  *
347
+ * @remarks
348
+ * The clone is validated as a {@link MigrationInput} before it is returned, so a
349
+ * malformed or hostile value throws a `VALIDATION` {@link DatabaseError} at
350
+ * `context.path === 'migration'` rather than surfacing a raw Contract or caller error.
351
+ *
318
352
  * @param value - Unknown migration input
319
353
  * @returns Owned migration input
320
354
  */
@@ -324,7 +358,7 @@ function cloneMigrationInput(value) {
324
358
  if (isMigrationInput(input)) return input;
325
359
  throw new DatabaseError("VALIDATION", "Migration input is invalid", { path: "migration" });
326
360
  } catch (error) {
327
- if (error instanceof DatabaseError) throw error;
361
+ if (isDatabaseError(error)) throw error;
328
362
  throw new DatabaseError("VALIDATION", "Migration input is invalid", {
329
363
  path: "migration",
330
364
  cause: error
@@ -347,14 +381,14 @@ function cloneMigrationInput(value) {
347
381
  */
348
382
  function validatePage(input) {
349
383
  const limit = input?.limit;
350
- if (limit !== void 0 && (!Number.isInteger(limit) || limit < 0)) throw new DatabaseError("VALIDATION", "Query limit must be a nonnegative integer", {
384
+ if (limit !== void 0 && (!isInteger(limit) || limit < 0)) throw new DatabaseError("VALIDATION", "Query limit must be a nonnegative integer", {
351
385
  field: "limit",
352
- value: Number.isFinite(limit) ? limit : String(limit)
386
+ value: isFiniteNumber(limit) ? limit : String(limit)
353
387
  });
354
388
  const offset = input?.offset;
355
- if (offset !== void 0 && (!Number.isInteger(offset) || offset < 0)) throw new DatabaseError("VALIDATION", "Query offset must be a nonnegative integer", {
389
+ if (offset !== void 0 && (!isInteger(offset) || offset < 0)) throw new DatabaseError("VALIDATION", "Query offset must be a nonnegative integer", {
356
390
  field: "offset",
357
- value: Number.isFinite(offset) ? offset : String(offset)
391
+ value: isFiniteNumber(offset) ? offset : String(offset)
358
392
  });
359
393
  }
360
394
  /**
@@ -372,14 +406,14 @@ function validatePage(input) {
372
406
  * @returns `-1`, `0`, or `1`
373
407
  */
374
408
  function compareValues(left, right) {
375
- const [leftRank = 5, rightRank = 5] = [left, right].map((value) => value === void 0 ? 0 : value === null ? 1 : typeof value === "boolean" ? 2 : typeof value === "number" ? 3 : typeof value === "string" ? 4 : 5);
409
+ const [leftRank = 5, rightRank = 5] = [left, right].map((value) => value === void 0 ? 0 : value === null ? 1 : isBoolean(value) ? 2 : isNumber(value) ? 3 : isString(value) ? 4 : 5);
376
410
  if (leftRank !== rightRank) return leftRank < rightRank ? -1 : 1;
377
- if (typeof left === "number" && typeof right === "number") {
411
+ if (isNumber(left) && isNumber(right)) {
378
412
  if (Number.isNaN(left) || Number.isNaN(right)) return Number.isNaN(left) ? Number.isNaN(right) ? 0 : 1 : -1;
379
413
  return left < right ? -1 : left > right ? 1 : 0;
380
414
  }
381
- if (typeof left === "string" && typeof right === "string") return left < right ? -1 : left > right ? 1 : 0;
382
- if (typeof left === "boolean" && typeof right === "boolean") return left === right ? 0 : left ? 1 : -1;
415
+ if (isString(left) && isString(right)) return left < right ? -1 : left > right ? 1 : 0;
416
+ if (isBoolean(left) && isBoolean(right)) return left === right ? 0 : left ? 1 : -1;
383
417
  return 0;
384
418
  }
385
419
  /**
@@ -390,9 +424,9 @@ function compareValues(left, right) {
390
424
  * @remarks
391
425
  * Primitives compare by SameValueZero (`NaN` equals itself; `+0` equals `-0`).
392
426
  * Arrays compare by index (same length, every element `equalsValue`). Plain
393
- * records (through `isRecord`) compare by their OWN enumerable keys: same key
394
- * COUNT and, for every key in `left`, `right` has that key (`Object.hasOwn`)
395
- * with a `equalsValue` value — so a key present with value `undefined` is NOT
427
+ * records (through `isRecord`) compare by their own enumerable keys: same key
428
+ * count and, for every key in `left`, `right` has that key (`Object.hasOwn`)
429
+ * with a `equalsValue` value — so a key present with value `undefined` is not
396
430
  * equal to that key being absent (both differ in `Object.keys` membership).
397
431
  * Anything else (functions, class instances, mismatched shapes) falls through
398
432
  * to `false`. Container pairs are tracked iteratively, so self-referential and
@@ -418,13 +452,13 @@ function equalsValue(left, right) {
418
452
  const pair = pending.pop();
419
453
  if (pair === void 0) continue;
420
454
  const [currentLeft, currentRight] = pair;
421
- if (typeof currentLeft === "number" && typeof currentRight === "number") {
455
+ if (isNumber(currentLeft) && isNumber(currentRight)) {
422
456
  if (Number.isNaN(currentLeft) && Number.isNaN(currentRight) || currentLeft === currentRight) continue;
423
457
  return false;
424
458
  }
425
459
  if (currentLeft === currentRight) continue;
426
- const leftArray = Array.isArray(currentLeft);
427
- const rightArray = Array.isArray(currentRight);
460
+ const leftArray = isArray(currentLeft);
461
+ const rightArray = isArray(currentRight);
428
462
  const leftRecord = isRecord(currentLeft);
429
463
  const rightRecord = isRecord(currentRight);
430
464
  if (leftArray !== rightArray || leftRecord !== rightRecord) return false;
@@ -458,16 +492,16 @@ function equalsValue(left, right) {
458
492
  }
459
493
  }
460
494
  /**
461
- * Matches a value against a wildcard pattern in LINEAR time — the shared, ReDoS-SAFE
495
+ * Matches a value against a wildcard pattern in linear time — the shared, ReDoS-safe
462
496
  * engine behind {@link matchesLikePattern} and {@link matchesGlobPattern}.
463
497
  *
464
498
  * @remarks
465
- * A backtracking RegExp (`a%b%c` → `^a.*b.*c$`) is CATASTROPHIC on a hostile pattern:
499
+ * A backtracking RegExp (`a%b%c` → `^a.*b.*c$`) is catastrophic on a hostile pattern:
466
500
  * `.*` segments separated by literals, matched against a long non-matching input, blow
467
501
  * up super-linearly — and JS has no atomic groups / possessive quantifiers to bound it,
468
502
  * while a `LIKE` / `GLOB` pattern is a caller-supplied operand this package cannot
469
- * trust. So this builds NO regex. It runs the classic GREEDY TWO-POINTER wildcard match:
470
- * the `any` wildcard records its position and, on a later mismatch, backtracks ONLY to
503
+ * trust. So this builds no regex. It runs the classic greedy two-pointer wildcard match:
504
+ * the `any` wildcard records its position and, on a later mismatch, backtracks only to
471
505
  * that last `any` (letting it absorb one more char) — so the work is O(value × pattern),
472
506
  * never the exponential / polynomial backtracking a regex would do. The pattern length
473
507
  * is capped at {@link MAX_PATTERN_LENGTH} (a `VALIDATION` {@link DatabaseError} over it),
@@ -475,16 +509,16 @@ function equalsValue(left, right) {
475
509
  * pattern.
476
510
  *
477
511
  * The `any` wildcard matches any run (including empty); `single` matches exactly one
478
- * char; every other pattern char matches itself LITERALLY (a pattern `.` / `(` / `\` is
512
+ * char; every other pattern char matches itself literally (a pattern `.` / `(` / `\` is
479
513
  * a literal — the regex-metacharacter hazard is gone with the regex). `any` is tested
480
- * BEFORE a literal match, so a value that literally contains the wildcard char never
481
- * shadows the wildcard. Case folding is applied to BOTH sides when `fold` is set.
514
+ * before a literal match, so a value that literally contains the wildcard char never
515
+ * shadows the wildcard. Case folding is applied to both sides when `fold` is set.
482
516
  *
483
517
  * @param value - The value to test
484
518
  * @param pattern - The wildcard pattern
485
519
  * @param any - The any-run wildcard char (`%` for `LIKE`, `*` for `GLOB`)
486
520
  * @param single - The single-char wildcard char (`_` for `LIKE`, `?` for `GLOB`)
487
- * @param fold - Whether to match case-INSENSITIVELY (`LIKE` folds; `GLOB` does not)
521
+ * @param fold - Whether to match case-insensitively (`LIKE` folds; `GLOB` does not)
488
522
  * @returns True if `value` matches `pattern`; false otherwise
489
523
  * @throws A `VALIDATION` {@link DatabaseError} when `pattern` exceeds {@link MAX_PATTERN_LENGTH}
490
524
  */
@@ -546,7 +580,7 @@ function matchesLikePattern(value, pattern) {
546
580
  * @remarks
547
581
  * `*` matches any run of characters (including none) and `?` matches exactly one
548
582
  * character; every other pattern character matches itself literally, so a
549
- * character class such as `[a-z]` is NOT interpreted. Runs on
583
+ * character class such as `[a-z]` is not interpreted. Runs on
550
584
  * {@link matchesWildcardPattern}, so the match is linear in the value length and
551
585
  * the pattern is capped at {@link MAX_PATTERN_LENGTH}.
552
586
  *
@@ -572,7 +606,7 @@ function matchesGlobPattern(value, pattern) {
572
606
  * string is one column; an array descends a nested value) — and applies the
573
607
  * operator. Range operators (`above` / `below` / `from` / `to` / `between`) use
574
608
  * {@link compareValues}, the total order; the equality family (`equals` / `not`
575
- * / `any` / `none`) uses {@link equalsValue} — STRUCTURAL equality, not the total
609
+ * / `any` / `none`) uses {@link equalsValue} — structural equality, not the total
576
610
  * order's rank-5-collapses-all-objects behavior, so `equals` on an object/array
577
611
  * operand only matches a structurally-equal value, never every row holding any
578
612
  * object. This is a semantics change from ranking: `equalsValue` is SameValueZero
@@ -730,7 +764,9 @@ function computeAggregate(rows, operation, column) {
730
764
  const total = numbers.reduce((sum, value) => sum + value, 0);
731
765
  return operation === "average" ? total / numbers.length : total;
732
766
  }
733
- return operation === "minimum" ? Math.min(...numbers) : Math.max(...numbers);
767
+ let result = operation === "minimum" ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
768
+ for (const value of numbers) result = operation === "minimum" ? Math.min(result, value) : Math.max(result, value);
769
+ return result;
734
770
  }
735
771
  /**
736
772
  * Reads a row's primary key from a column, when it is a usable {@link Key}.
@@ -787,8 +823,8 @@ function shapeToColumnStorage(shape) {
787
823
  case "number": return shape.integer === true ? "integer" : "real";
788
824
  case "boolean": return "boolean";
789
825
  case "literal":
790
- if (shape.values.every((value) => typeof value === "boolean")) return "boolean";
791
- if (shape.values.every((value) => typeof value === "number")) return shape.values.every((value) => Number.isInteger(value)) ? "integer" : "real";
826
+ if (shape.values.every((value) => isBoolean(value))) return "boolean";
827
+ if (shape.values.every((value) => isNumber(value))) return shape.values.every((value) => isInteger(value)) ? "integer" : "real";
792
828
  return "text";
793
829
  case "optional":
794
830
  case "nullable": return shapeToColumnStorage(shape.inner);
@@ -908,16 +944,16 @@ function checkAbort(signal) {
908
944
  * plan labels only; versioning drivers persist and reconcile them through
909
945
  * {@link DriverMetadata}.
910
946
  *
911
- * A column present in BOTH schemas under the same name but with a different
947
+ * A column present in both schemas under the same name but with a different
912
948
  * `storage`, `optional`, or `nullable` value throws a `MIGRATION`
913
949
  * {@link DatabaseError} naming the table, the column, and the from→to
914
- * difference — a name-only diff would otherwise silently produce NO step for
950
+ * difference — a name-only diff would otherwise silently produce no step for
915
951
  * the drift, and versioned reconciliation would stamp over it. There is no
916
952
  * automatic in-place type-change step: the manual path is to add a new column,
917
953
  * copy/convert the data at the application layer, then remove the old column —
918
954
  * two separate plans, never a single implicit "alter" step.
919
955
  *
920
- * @param deployed - The table schemas currently applied
956
+ * @param deployed - The already-applied table schemas
921
957
  * @param declared - The table schemas the caller wants applied
922
958
  * @param from - The plan's source version label (defaults to `0`)
923
959
  * @param to - The plan's target version label (defaults to `1`)
@@ -937,7 +973,7 @@ function checkAbort(signal) {
937
973
  * ```
938
974
  */
939
975
  function planMigration(deployed, declared, from = 0, to = 1) {
940
- if (!Number.isFinite(from) || !Number.isFinite(to)) throw new DatabaseError("MIGRATION", "Migration versions must be finite", {
976
+ if (!isFiniteNumber(from) || !isFiniteNumber(to)) throw new DatabaseError("MIGRATION", "Migration versions must be finite", {
941
977
  from,
942
978
  to
943
979
  });
@@ -1024,6 +1060,8 @@ function planMigration(deployed, declared, from = 0, to = 1) {
1024
1060
  }
1025
1061
  /**
1026
1062
  * Projects migration steps sequentially over a canonical validated owned schema.
1063
+ *
1064
+ * @remarks
1027
1065
  * Adding a required non-null column to an existing table rejects with
1028
1066
  * `MIGRATION`; optional-only and nullable-only additions remain portable.
1029
1067
  *
@@ -1187,14 +1225,14 @@ function migrateRows(rows, steps) {
1187
1225
  * default `id`, `posts` keyed by a non-id `slug`) and, calling `factory()`
1188
1226
  * fresh for each phase so failures stay isolated, verifies: `open`/`close`;
1189
1227
  * `read` of a missing key returns `undefined`; `write`/`read` round-trip
1190
- * with DEEP copy-in/copy-out isolation (mutating the caller's row —
1191
- * including a NESTED field — after `write`, or a row `read` returns, never
1228
+ * with deep copy-in/copy-out isolation (mutating the caller's row —
1229
+ * including a nested field — after `write`, or a row `read` returns, never
1192
1230
  * perturbs stored state) and upsert-overwrite; simultaneous same-key
1193
1231
  * `insert` calls produce exactly one commit and one `CONFLICT`; pre-aborted
1194
1232
  * `write`, `insert`, and `delete` calls leave storage unchanged; `delete`
1195
1233
  * returns `true` then `false`; `keys`/`scan` yield in ascending key order;
1196
1234
  * `clear` empties only its target table; `snapshot`'s rollback thunk
1197
- * restores pre-snapshot state, including a NESTED field mutated in place on
1235
+ * restores pre-snapshot state, including a nested field mutated in place on
1198
1236
  * a read-back row between capture and restore; a scoped
1199
1237
  * `snapshot(['users'])` rolls back only the named table, leaving a
1200
1238
  * concurrent mutation to another table intact; a non-`id` primary key
@@ -1208,12 +1246,12 @@ function migrateRows(rows, steps) {
1208
1246
  * store's `metadata()` is `undefined`, and after
1209
1247
  * `stamp({ version, schema })`, `metadata()` returns the exact stamped value.
1210
1248
  *
1211
- * Each phase runs within a `try`/`catch`: an EXPECTED mismatch yields a
1212
- * finding built from the assertion, while an UNEXPECTED throw (a driver
1249
+ * Each phase runs within a `try`/`catch`: an expected mismatch yields a
1250
+ * finding built from the assertion, while an unexpected throw (a driver
1213
1251
  * crash mid-phase) is caught and yielded as a finding too, naming the phase
1214
1252
  * as `check` and carrying the caught error in `context.error` — a broken
1215
1253
  * driver can never escape the battery as an unhandled rejection. Within a
1216
- * phase, the FIRST violated assertion yields and the phase stops (matching
1254
+ * phase, the first violated assertion yields and the phase stops (matching
1217
1255
  * the historical fail-fast shape at phase granularity); the generator then
1218
1256
  * moves on to the next phase regardless. Because this is a **generator**,
1219
1257
  * consuming only the first yielded value reproduces true fail-fast (later
@@ -1239,7 +1277,7 @@ async function* scanDriver(factory) {
1239
1277
  } catch (error) {
1240
1278
  yield {
1241
1279
  check: "open-close",
1242
- message: error instanceof Error ? error.message : String(error),
1280
+ message: isError(error) ? error.message : String(error),
1243
1281
  context: { error }
1244
1282
  };
1245
1283
  }
@@ -1260,7 +1298,7 @@ async function* scanDriver(factory) {
1260
1298
  } catch (error) {
1261
1299
  yield {
1262
1300
  check: "read-missing",
1263
- message: error instanceof Error ? error.message : String(error),
1301
+ message: isError(error) ? error.message : String(error),
1264
1302
  context: { error }
1265
1303
  };
1266
1304
  }
@@ -1336,7 +1374,7 @@ async function* scanDriver(factory) {
1336
1374
  } catch (error) {
1337
1375
  yield {
1338
1376
  check: "write-read",
1339
- message: error instanceof Error ? error.message : String(error),
1377
+ message: isError(error) ? error.message : String(error),
1340
1378
  context: { error }
1341
1379
  };
1342
1380
  }
@@ -1377,7 +1415,7 @@ async function* scanDriver(factory) {
1377
1415
  } catch (error) {
1378
1416
  yield {
1379
1417
  check: "insert-atomic",
1380
- message: error instanceof Error ? error.message : String(error),
1418
+ message: isError(error) ? error.message : String(error),
1381
1419
  context: { error }
1382
1420
  };
1383
1421
  }
@@ -1417,7 +1455,7 @@ async function* scanDriver(factory) {
1417
1455
  } catch (error) {
1418
1456
  yield {
1419
1457
  check: "delete",
1420
- message: error instanceof Error ? error.message : String(error),
1458
+ message: isError(error) ? error.message : String(error),
1421
1459
  context: { error }
1422
1460
  };
1423
1461
  }
@@ -1480,7 +1518,7 @@ async function* scanDriver(factory) {
1480
1518
  } catch (error) {
1481
1519
  yield {
1482
1520
  check: "mutation-abort",
1483
- message: error instanceof Error ? error.message : String(error),
1521
+ message: isError(error) ? error.message : String(error),
1484
1522
  context: { error }
1485
1523
  };
1486
1524
  }
@@ -1539,7 +1577,7 @@ async function* scanDriver(factory) {
1539
1577
  } catch (error) {
1540
1578
  yield {
1541
1579
  check: "order",
1542
- message: error instanceof Error ? error.message : String(error),
1580
+ message: isError(error) ? error.message : String(error),
1543
1581
  context: { error }
1544
1582
  };
1545
1583
  }
@@ -1583,7 +1621,7 @@ async function* scanDriver(factory) {
1583
1621
  } catch (error) {
1584
1622
  yield {
1585
1623
  check: "clear",
1586
- message: error instanceof Error ? error.message : String(error),
1624
+ message: isError(error) ? error.message : String(error),
1587
1625
  context: { error }
1588
1626
  };
1589
1627
  }
@@ -1632,7 +1670,7 @@ async function* scanDriver(factory) {
1632
1670
  } catch (error) {
1633
1671
  yield {
1634
1672
  check: "snapshot",
1635
- message: error instanceof Error ? error.message : String(error),
1673
+ message: isError(error) ? error.message : String(error),
1636
1674
  context: { error }
1637
1675
  };
1638
1676
  }
@@ -1670,7 +1708,7 @@ async function* scanDriver(factory) {
1670
1708
  } catch (error) {
1671
1709
  yield {
1672
1710
  check: "snapshot-nested",
1673
- message: error instanceof Error ? error.message : String(error),
1711
+ message: isError(error) ? error.message : String(error),
1674
1712
  context: { error }
1675
1713
  };
1676
1714
  }
@@ -1696,7 +1734,7 @@ async function* scanDriver(factory) {
1696
1734
  } catch (error) {
1697
1735
  yield {
1698
1736
  check: "non-id-primary",
1699
- message: error instanceof Error ? error.message : String(error),
1737
+ message: isError(error) ? error.message : String(error),
1700
1738
  context: { error }
1701
1739
  };
1702
1740
  }
@@ -1727,7 +1765,7 @@ async function* scanDriver(factory) {
1727
1765
  } catch (error) {
1728
1766
  yield {
1729
1767
  check: "nested-roundtrip",
1730
- message: error instanceof Error ? error.message : String(error),
1768
+ message: isError(error) ? error.message : String(error),
1731
1769
  context: { error }
1732
1770
  };
1733
1771
  }
@@ -1792,7 +1830,7 @@ async function* scanDriver(factory) {
1792
1830
  } catch (error) {
1793
1831
  yield {
1794
1832
  check: "migrate",
1795
- message: error instanceof Error ? error.message : String(error),
1833
+ message: isError(error) ? error.message : String(error),
1796
1834
  context: { error }
1797
1835
  };
1798
1836
  }
@@ -1857,7 +1895,7 @@ async function* scanDriver(factory) {
1857
1895
  } catch (error) {
1858
1896
  yield {
1859
1897
  check: "stream",
1860
- message: error instanceof Error ? error.message : String(error),
1898
+ message: isError(error) ? error.message : String(error),
1861
1899
  context: { error }
1862
1900
  };
1863
1901
  }
@@ -1918,7 +1956,7 @@ async function* scanDriver(factory) {
1918
1956
  } catch (error) {
1919
1957
  yield {
1920
1958
  check: "transaction",
1921
- message: error instanceof Error ? error.message : String(error),
1959
+ message: isError(error) ? error.message : String(error),
1922
1960
  context: { error }
1923
1961
  };
1924
1962
  }
@@ -1957,7 +1995,7 @@ async function* scanDriver(factory) {
1957
1995
  } catch (error) {
1958
1996
  yield {
1959
1997
  check: "metadata-stamp",
1960
- message: error instanceof Error ? error.message : String(error),
1998
+ message: isError(error) ? error.message : String(error),
1961
1999
  context: { error }
1962
2000
  };
1963
2001
  }
@@ -2012,7 +2050,7 @@ async function* scanDriver(factory) {
2012
2050
  } catch (error) {
2013
2051
  yield {
2014
2052
  check: "snapshot-scoped",
2015
- message: error instanceof Error ? error.message : String(error),
2053
+ message: isError(error) ? error.message : String(error),
2016
2054
  context: { error }
2017
2055
  };
2018
2056
  }
@@ -2024,11 +2062,13 @@ async function* scanDriver(factory) {
2024
2062
  *
2025
2063
  * @remarks
2026
2064
  * Consumes only the first value {@link scanDriver} yields: because that
2027
- * generator is lazy, every LATER phase never runs — true fail-fast, not
2065
+ * generator is lazy, every later phase never runs — true fail-fast, not
2028
2066
  * merely "report only the first". The
2029
2067
  * thrown error is byte-compatible with the historical shape: a
2030
2068
  * `CONFORMANCE` {@link DatabaseError} whose `message` is the finding's
2031
- * `message` and whose `context` is `{ check, ...finding.context }`.
2069
+ * `message` and whose `context` is `{ check, ...finding.context }`. The battery
2070
+ * takes a driver factory and reports through a throw, so it binds no test
2071
+ * framework and runs from any runner.
2032
2072
  *
2033
2073
  * @param factory - Mints a fresh, unopened driver instance (called once per phase)
2034
2074
  * @returns Nothing — resolves once every phase has passed
@@ -2048,7 +2088,7 @@ async function conformDriver(factory) {
2048
2088
  });
2049
2089
  }
2050
2090
  /**
2051
- * Runs the FULL driver-conformance battery and collects every violation — the
2091
+ * Runs the full driver-conformance battery and collects every violation — the
2052
2092
  * audit entry point for a driver author who wants a complete report rather
2053
2093
  * than a single fail-fast throw.
2054
2094
  *
@@ -2718,10 +2758,10 @@ var Query = class {
2718
2758
  * @remarks
2719
2759
  * - **Observable.** The owned {@link emitter} ({@link TableEventMap}) carries the
2720
2760
  * per-row mutation moments — `write` (set / add / update), `remove`, `clear` — for
2721
- * fire-and-forget observers (cache invalidation, sync, an audit log), ALONGSIDE the
2722
- * database-level lifecycle. Events carry the affected KEY only (no value payload, to
2761
+ * fire-and-forget observers (cache invalidation, sync, an audit log), alongside the
2762
+ * database-level lifecycle. Events carry the affected key only (no value payload, to
2723
2763
  * keep fan-out lean); reads / queries / counts are not emitted. Every event is emitted
2724
- * directly, strictly AFTER the driver write / delete / clear completes; the emitter
2764
+ * directly, strictly after the driver write / delete / clear completes; the emitter
2725
2765
  * isolates a listener throw and routes it to its `error` handler (the `error` option),
2726
2766
  * so a buggy observer can never corrupt a write or perturb a transaction.
2727
2767
  */
@@ -2834,7 +2874,7 @@ var Table = class {
2834
2874
  * conditions.
2835
2875
  *
2836
2876
  * @remarks
2837
- * Unlike {@link count}, `aggregate` operates on STORED rows WITHOUT the
2877
+ * Unlike {@link count}, `aggregate` operates on stored rows without the
2838
2878
  * contract guard {@link records} / {@link scan} apply — a non-conforming
2839
2879
  * stored row still contributes to the computed aggregate when it matches
2840
2880
  * the conditions, even though it would never appear in `records()`'s
@@ -3162,6 +3202,10 @@ var DatabaseTransaction = class {
3162
3202
  * generator. Imported views register their physical schemas with the same
3163
3203
  * internal context before opening begins, so every view observes one driver,
3164
3204
  * merged schema, emitter, status, transaction boundary, and terminal close.
3205
+ *
3206
+ * The view owns the driver and its declared `tables`, connects that driver lazily on
3207
+ * first use, `import`s further tables and `export`s their portable definitions, and
3208
+ * runs `transaction` scopes over the shared context.
3165
3209
  */
3166
3210
  var Database = class Database {
3167
3211
  #context;
@@ -3284,7 +3328,7 @@ var Database = class Database {
3284
3328
  * @remarks
3285
3329
  * The in-between made concrete: it runs identically in a browser or on a server,
3286
3330
  * so it is the storage behind tests, ephemeral caches, and any code that wants
3287
- * the database API without a persistent backend. Rows are DEEP-copied (through
3331
+ * the database API without a persistent backend. Rows are deep-copied (through
3288
3332
  * `structuredClone`) in and out — at `write`, `read`, `scan`, `stream`, and both
3289
3333
  * snapshot capture and restore — so a caller mutating a nested field of an input
3290
3334
  * row, a returned row, or a row mutated in place between snapshot and rollback
@@ -3359,7 +3403,7 @@ var MemoryDriver = class {
3359
3403
  * toward `offset` / `limit`. Both are applied lazily as matches are found —
3360
3404
  * `offset` matches are skipped without being yielded, and iteration stops the
3361
3405
  * instant `limit` yields have been produced, so a large table is never fully
3362
- * walked for a small page. `input.order` is IGNORED (the same contract as
3406
+ * walked for a small page. `input.order` is ignored (the same contract as
3363
3407
  * `TableInterface.scan` and `QueryInterface.stream`): streaming yields key
3364
3408
  * order, sorted output is `records()`'s job. Rows yield copy-out, and an
3365
3409
  * unknown table mirrors `scan`'s empty-yield behavior.
@@ -3588,20 +3632,29 @@ var MemoryDriver = class {
3588
3632
  * `name`, `generator`, `version`, and emitter hooks
3589
3633
  * @returns A typed {@link DatabaseInterface}
3590
3634
  *
3591
- * @example
3635
+ * @example Create a database
3592
3636
  * ```ts
3593
3637
  * import { createDatabase, createMemoryDriver } from '@orkestrel/database'
3594
3638
  * import { integerShape, stringShape } from '@orkestrel/contract'
3595
3639
  *
3596
3640
  * const db = createDatabase({
3597
- * driver: createMemoryDriver(),
3641
+ * driver: createMemoryDriver(), // any DriverInterface — a persistent backend swaps in, same API
3598
3642
  * tables: {
3599
- * users: { id: stringShape(), age: integerShape() },
3643
+ * users: { id: stringShape(), name: stringShape(), age: integerShape() },
3600
3644
  * posts: { slug: stringShape(), title: stringShape() },
3601
3645
  * },
3602
- * primary: { posts: 'slug' },
3646
+ * primary: { posts: 'slug' }, // non-`id` primary-key columns, per table
3603
3647
  * })
3604
- * await db.table('users').set({ id: 'u1', age: 36 }) // typed; coerced + validated
3648
+ *
3649
+ * const users = db.table('users') // hold the handle; TableInterface<{ id; name; age }>
3650
+ *
3651
+ * await users.set({ id: 'u1', name: 'Ada', age: 36 }) // coerced + validated through the contract
3652
+ * await users.get('u1') // typed { id; name; age } | undefined — narrowed, never `as`
3653
+ * await users
3654
+ * .query()
3655
+ * .condition({ column: 'age', operator: 'from', values: [18], connector: 'and' })
3656
+ * .order({ column: 'age', direction: 'descending' })
3657
+ * .collect() // typed rows
3605
3658
  * ```
3606
3659
  */
3607
3660
  function createDatabase(options) {