@orkestrel/database 0.0.1 → 0.0.3

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,3 +1,5 @@
1
+ import { compileSchema, createContract, isArray, isBoolean, isFiniteNumber, isRecord, isString, objectShape, parseNumber, resolveField } from "@orkestrel/contract";
2
+ import { Emitter } from "@orkestrel/emitter";
1
3
  //#region src/core/constants.ts
2
4
  /**
3
5
  * The primary-key column assumed when {@link TableKeys} does not name one.
@@ -21,6 +23,10 @@ var DEFAULT_PRIMARY = "id";
21
23
  * `VALIDATION` {@link DatabaseError}; the cap is generous for any legitimate search.
22
24
  */
23
25
  var MAX_PATTERN_LENGTH = 1024;
26
+ /** The number of bytes encoded by an RFC 4122 UUID. */
27
+ var UUID_BYTE_COUNT = 16;
28
+ /** The number of distinct values one UUID byte may hold. */
29
+ var UUID_BYTE_RANGE = 256;
24
30
  //#endregion
25
31
  //#region src/core/errors.ts
26
32
  /**
@@ -67,946 +73,6 @@ var DatabaseError = class extends Error {
67
73
  function isDatabaseError(value) {
68
74
  return value instanceof DatabaseError;
69
75
  }
70
- Object.freeze([
71
- "null",
72
- "boolean",
73
- "object",
74
- "array",
75
- "number",
76
- "integer",
77
- "string"
78
- ]);
79
- /** Determine whether a value is `null`. */
80
- function isNull(value) {
81
- return value === null;
82
- }
83
- /** Determine whether a value is `undefined`. */
84
- function isUndefined(value) {
85
- return value === void 0;
86
- }
87
- /** Determine whether a value is a string. */
88
- function isString(value) {
89
- return typeof value === "string";
90
- }
91
- /** Determine whether a value is a finite number (excludes `NaN` and `±Infinity`). */
92
- function isFiniteNumber(value) {
93
- return typeof value === "number" && Number.isFinite(value);
94
- }
95
- /** Determine whether a value is a finite integer (excludes `NaN`, `±Infinity`, and fractional numbers). */
96
- function isInteger(value) {
97
- return Number.isInteger(value);
98
- }
99
- /** Determine whether a value is a boolean. */
100
- function isBoolean(value) {
101
- return typeof value === "boolean";
102
- }
103
- /**
104
- * Determine whether a value is a non-null object.
105
- *
106
- * @remarks
107
- * `true` for arrays, class instances, plain objects, `Map`, `Set`, etc. — use
108
- * {@link isRecord} when you need a plain-record check.
109
- */
110
- function isObject(value) {
111
- return typeof value === "object" && value !== null;
112
- }
113
- /**
114
- * Determine whether a value is a plain record (object literal or null-prototype),
115
- * not an array or class instance.
116
- *
117
- * @remarks
118
- * Use instead of {@link isObject} to distinguish a plain `{}` /
119
- * `Object.create(null)` from arrays, `Date`, `Map`, etc. The prototype-chain
120
- * test is realm-agnostic: rather than comparing against the current realm's
121
- * `Object.prototype` (which a plain object from another `vm.Context`, iframe,
122
- * or worker would fail), it accepts any value whose prototype is `null`, OR
123
- * whose prototype's own prototype is `null` — the shape every plain object
124
- * has in every realm, since `Object.prototype` itself always sits one step
125
- * above `null`. Arrays and class instances are still rejected: an array's
126
- * prototype chain runs through `Array.prototype` before `null`, and a class
127
- * instance's runs through the class's own prototype. The whole body runs
128
- * inside `attempt` (AGENTS §14) so a revoked `Proxy` or a hostile
129
- * `getPrototypeOf` trap cannot escape as a thrown error.
130
- */
131
- function isRecord(value) {
132
- const outcome = attempt(() => {
133
- if (!isObject(value) || isArray(value)) return false;
134
- const prototype = Object.getPrototypeOf(value);
135
- return prototype === null || Object.getPrototypeOf(prototype) === null;
136
- });
137
- return outcome.success && outcome.value;
138
- }
139
- /** Determine whether a value is an array. */
140
- function isArray(value) {
141
- return Array.isArray(value);
142
- }
143
- /**
144
- * Determine whether a value is a cycle-safe JSON value.
145
- *
146
- * @remarks
147
- * Total guard: never throws, returns `false` for cycles, functions, `Date`
148
- * instances, class instances, `NaN`, and `±Infinity`. Arrays and plain records
149
- * are walked with an ancestor set so recursive input fails instead of hanging.
150
- * The whole walk runs inside `attempt` (AGENTS §14): a hostile getter on a
151
- * record property, or a revoked `Proxy` anywhere in the structure, is caught
152
- * and yields `false` instead of escaping as a thrown error.
153
- *
154
- * @param value - The value to test
155
- * @returns `true` when the value has a JSON representation
156
- *
157
- * @example
158
- * ```ts
159
- * isJSONValue({ nested: [1, 'x', null] }) // true
160
- * isJSONValue(Number.NaN) // false
161
- * ```
162
- */
163
- function isJSONValue(value) {
164
- const ancestors = /* @__PURE__ */ new WeakSet();
165
- const check = (entry) => {
166
- if (entry === null || isString(entry) || isBoolean(entry) || isFiniteNumber(entry)) return true;
167
- if (Array.isArray(entry)) {
168
- if (ancestors.has(entry)) return false;
169
- ancestors.add(entry);
170
- const valid = entry.every(check);
171
- ancestors.delete(entry);
172
- return valid;
173
- }
174
- if (!isRecord(entry)) return false;
175
- if (ancestors.has(entry)) return false;
176
- ancestors.add(entry);
177
- const valid = Object.values(entry).every(check);
178
- ancestors.delete(entry);
179
- return valid;
180
- };
181
- const outcome = attempt(() => check(value));
182
- return outcome.success && outcome.value;
183
- }
184
- /**
185
- * Invoke a callback and capture its outcome as a {@link Result}, never letting
186
- * a throw escape.
187
- *
188
- * @remarks
189
- * The single sanctioned never-throw boundary for the guards (AGENTS §14). The
190
- * `whereOf`, `lazyOf`, and `transformOf` combinators invoke caller-supplied
191
- * callbacks *inside* a guard body, yet a guard must NEVER throw — it returns a
192
- * `boolean`. This converts a throwing callback into a `Failure` so the
193
- * surrounding guard can treat it as a non-match instead of propagating the
194
- * exception, written once and shared rather than copy-pasted as ad-hoc
195
- * `try`/`catch`.
196
- *
197
- * @param callback - The callback to invoke with no arguments
198
- * @returns A `Success` carrying the return value, or a `Failure` carrying the
199
- * thrown reason normalised to an `Error`
200
- *
201
- * @example
202
- * ```ts
203
- * const outcome = attempt(() => predicate(value))
204
- * return outcome.success && outcome.value
205
- * ```
206
- */
207
- function attempt(callback) {
208
- try {
209
- return {
210
- success: true,
211
- value: callback()
212
- };
213
- } catch (reason) {
214
- if (reason instanceof Error) return {
215
- success: false,
216
- error: reason
217
- };
218
- let message = "Unknown thrown value";
219
- try {
220
- message = String(reason);
221
- } catch {}
222
- return {
223
- success: false,
224
- error: new Error(message)
225
- };
226
- }
227
- }
228
- /**
229
- * Resolve a (possibly nested) field value from a record by a key or key path.
230
- *
231
- * @remarks
232
- * A single `string` is ONE key (never split on `.`, so dotted keys are safe); a
233
- * string array descends left-to-right through nested objects. Intermediates may
234
- * be any object — records, class instances, or arrays indexed by string. Returns
235
- * `undefined` the moment a segment is missing or lands on a non-object, so the
236
- * lookup is total — even against a hostile getter or Proxy trap that throws on
237
- * read, contained via {@link attempt} so the throw never escapes.
238
- *
239
- * @param record - The source record
240
- * @param path - A property key, or a key path descending into nested objects
241
- * @returns The resolved value, or `undefined`
242
- *
243
- * @example
244
- * ```ts
245
- * resolveField({ user: { name: 'Ada' } }, ['user', 'name']) // 'Ada'
246
- * resolveField({ 'a.b': 1 }, 'a.b') // 1 (one key)
247
- * resolveField({ a: 1 }, ['a', 'b']) // undefined
248
- * ```
249
- */
250
- function resolveField(record, path) {
251
- const keys = isString(path) ? [path] : path;
252
- let current = record;
253
- for (const key of keys) {
254
- if (!isObject(current)) return void 0;
255
- const container = current;
256
- const outcome = attempt(() => Reflect.get(container, key));
257
- if (!outcome.success) return void 0;
258
- current = outcome.value;
259
- }
260
- return current;
261
- }
262
- /**
263
- * Build a deterministic pseudo-random source seeded from a single number.
264
- *
265
- * @remarks
266
- * A mulberry32 generator — the same seed always yields the same sequence, so
267
- * generated seed data is reproducible across runs. Used as the default random
268
- * source for {@link compileGenerator}, seeded from the wall clock so casual
269
- * callers still get varied output without passing a source themselves.
270
- *
271
- * @param seed - The seed for the sequence
272
- * @returns A {@link RandomFunction} returning values in `[0, 1)`
273
- *
274
- * @example
275
- * ```ts
276
- * const random = seededRandom(42)
277
- * random() // always the same first value for seed 42
278
- * ```
279
- */
280
- function seededRandom(seed) {
281
- let state = seed >>> 0;
282
- return () => {
283
- state = state + 1831565813 >>> 0;
284
- let t = state;
285
- t = Math.imul(t ^ t >>> 15, t | 1);
286
- t ^= t + Math.imul(t ^ t >>> 7, t | 61);
287
- return ((t ^ t >>> 14) >>> 0) / 4294967296;
288
- };
289
- }
290
- function arrayOf(elementGuard) {
291
- return (value) => {
292
- if (!isArray(value)) return false;
293
- const outcome = attempt(() => value.every(elementGuard));
294
- return outcome.success && outcome.value;
295
- };
296
- }
297
- /**
298
- * Build a guard that accepts values identical (via `Object.is`) to one of the
299
- * provided literal primitives.
300
- *
301
- * @example
302
- * ```ts
303
- * const isRole = literalOf('admin', 'member', 'guest')
304
- * isRole('admin') // true
305
- * isRole('owner') // false
306
- * ```
307
- */
308
- function literalOf(...literals) {
309
- return (value) => literals.some((literal) => Object.is(literal, value));
310
- }
311
- /**
312
- * Build a guard that accepts plain records matching a guard shape.
313
- *
314
- * @remarks
315
- * Three calling modes depending on the `optional` argument:
316
- * - **No `optional`** — all shape keys required; extra keys rejected.
317
- * - **`optional: K[]`** — the listed keys are optional; all others required.
318
- * - **`optional: true`** — every shape key is optional.
319
- *
320
- * Key presence is tested with `Object.hasOwn`, so a shape key satisfied only by
321
- * an inherited prototype member (`toString`, `constructor`, …) counts as absent.
322
- * A non-object / `null` / array input returns `false` rather than throwing. The
323
- * extra-key check only inspects `Object.keys` (string keys), so an extra
324
- * enumerable SYMBOL key is never rejected — intentional, for JSON fidelity, and
325
- * matches the compiled guard.
326
- *
327
- * @example
328
- * ```ts
329
- * const isUser = recordOf({ name: isString, age: isNumber })
330
- * isUser({ name: 'Ada', age: 36 }) // true
331
- * isUser({ name: 'Ada' }) // false — age missing
332
- *
333
- * const isPartial = recordOf({ name: isString, age: isNumber }, ['age'])
334
- * isPartial({ name: 'Ada' }) // true
335
- * ```
336
- */
337
- function recordOf(shape, optional) {
338
- const allowed = /* @__PURE__ */ new Set();
339
- for (const key in shape) if (Object.prototype.hasOwnProperty.call(shape, key)) allowed.add(key);
340
- const optionalSet = new Set(optional === true ? [...allowed] : isArray(optional) ? optional.map((key) => String(key)) : []);
341
- return (value) => {
342
- if (!isRecord(value)) return false;
343
- const outcome = attempt(() => {
344
- for (const key of Object.keys(value)) if (!allowed.has(key)) return false;
345
- for (const key in shape) {
346
- if (!Object.prototype.hasOwnProperty.call(shape, key)) continue;
347
- const present = Object.hasOwn(value, key);
348
- if (!optionalSet.has(key) && !present) return false;
349
- if (present) {
350
- const guard = shape[key];
351
- if (!guard(value[key])) return false;
352
- }
353
- }
354
- return true;
355
- });
356
- return outcome.success && outcome.value;
357
- };
358
- }
359
- function orOf(left, right) {
360
- return (value) => left(value) || right(value);
361
- }
362
- function unionOf(...guards) {
363
- return (value) => guards.some((guard) => guard(value));
364
- }
365
- function intersectionOf(...guards) {
366
- return (value) => guards.every((guard) => guard(value));
367
- }
368
- function whereOf(base, predicate) {
369
- return (value) => {
370
- if (!base(value)) return false;
371
- const outcome = attempt(() => predicate(value));
372
- return outcome.success && outcome.value;
373
- };
374
- }
375
- /**
376
- * Build a guard that accepts finite numbers within an inclusive `[min, max]`
377
- * range.
378
- *
379
- * @remarks
380
- * Refines {@link isFiniteNumber} with the bound comparison, so `NaN` /
381
- * `±Infinity` are rejected before any comparison runs. An absent bound never
382
- * constrains that side. Reused for a number's own value AND, applied to a
383
- * `.length`, for string and array length refinements — the single source of the
384
- * bound logic shared by the compiled guard and parser (compilers.ts).
385
- *
386
- * @example
387
- * ```ts
388
- * const inRange = boundsOf(1, 5)
389
- * inRange(3) // true
390
- * inRange(0) // false — below min
391
- * inRange(6) // false — above max
392
- *
393
- * const atLeastTwo = boundsOf(2)
394
- * atLeastTwo(2) // true — unbounded above
395
- * ```
396
- */
397
- function boundsOf(min, max) {
398
- return whereOf(isFiniteNumber, (value) => (min === void 0 || value >= min) && (max === void 0 || value <= max));
399
- }
400
- /**
401
- * Build a guard that accepts strings satisfying optional length and pattern
402
- * refinements — `min` / `max` length and a `pattern`.
403
- *
404
- * @remarks
405
- * Composes {@link isString} with {@link boundsOf} on the string's `.length` and
406
- * an inline `pattern.test` (the same refinement {@link matchOf} performs). When all three options are absent it returns
407
- * the bare {@link isString} guard (the unconstrained fast path), so an
408
- * unrefined string leaf pays no wrapping cost. The single source of the string
409
- * refinement shared by the compiled guard and parser (compilers.ts).
410
- *
411
- * @example
412
- * ```ts
413
- * const isSlug = stringOf({ min: 1, max: 32, pattern: /^[a-z-]+$/ })
414
- * isSlug('hello-world') // true
415
- * isSlug('') // false — below min
416
- * isSlug('Hello') // false — pattern miss
417
- *
418
- * stringOf() // identical to isString
419
- * ```
420
- */
421
- function stringOf(options) {
422
- const min = options?.min;
423
- const max = options?.max;
424
- const pattern = options?.pattern;
425
- if (min === void 0 && max === void 0 && pattern === void 0) return isString;
426
- const withinLength = boundsOf(min, max);
427
- return whereOf(isString, (value) => withinLength(value.length) && (pattern === void 0 || pattern.test(value)));
428
- }
429
- /**
430
- * Extend a guard to also allow `null`.
431
- *
432
- * @example
433
- * ```ts
434
- * const isNullableString = nullableOf(isString)
435
- * isNullableString('hi') // true
436
- * isNullableString(null) // true
437
- * isNullableString(42) // false
438
- * ```
439
- */
440
- function nullableOf(guard) {
441
- return (value) => value === null || guard(value);
442
- }
443
- /**
444
- * Parse an unknown value to a string.
445
- *
446
- * @remarks
447
- * A string is returned unchanged; a finite number is coerced to its decimal
448
- * string (`42` → `'42'`). `NaN`, `±Infinity`, and every other type → `undefined`.
449
- *
450
- * @param value - The value to parse
451
- * @returns A string, or `undefined`
452
- */
453
- function parseString(value) {
454
- if (isString(value)) return value;
455
- if (isFiniteNumber(value)) return String(value);
456
- }
457
- /**
458
- * Parse an unknown value to a finite number.
459
- *
460
- * @remarks
461
- * A finite number is returned unchanged; a non-blank numeric string is parsed
462
- * via `Number(...)`. `NaN`, `±Infinity`, blank/non-numeric strings, and every
463
- * other type → `undefined`.
464
- *
465
- * @param value - The value to parse
466
- * @returns A finite number, or `undefined`
467
- */
468
- function parseNumber(value) {
469
- if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
470
- if (isString(value)) {
471
- if (value.trim() === "") return void 0;
472
- const parsed = Number(value);
473
- return Number.isFinite(parsed) ? parsed : void 0;
474
- }
475
- }
476
- /**
477
- * Parse an unknown value to a finite integer.
478
- *
479
- * @remarks
480
- * Accepts whatever {@link parseNumber} accepts, then requires the result to have
481
- * no fractional part. `3.14` / `'3.14'` → `undefined`.
482
- *
483
- * @param value - The value to parse
484
- * @returns A finite integer, or `undefined`
485
- */
486
- function parseInteger(value) {
487
- const parsed = parseNumber(value);
488
- if (parsed === void 0) return void 0;
489
- return Number.isInteger(parsed) ? parsed : void 0;
490
- }
491
- /**
492
- * Parse an unknown value to a boolean.
493
- *
494
- * @remarks
495
- * A boolean is returned unchanged. The strings `'true'` / `'false'` / `'1'` /
496
- * `'0'` and the numbers `1` / `0` coerce to the matching boolean. Everything
497
- * else → `undefined`.
498
- *
499
- * @param value - The value to parse
500
- * @returns A boolean, or `undefined`
501
- */
502
- function parseBoolean(value) {
503
- if (typeof value === "boolean") return value;
504
- if (value === "true" || value === "1" || value === 1) return true;
505
- if (value === "false" || value === "0" || value === 0) return false;
506
- }
507
- /**
508
- * Parse an unknown value to a plain record — the input reference, never cloned.
509
- *
510
- * @param value - The value to parse
511
- * @returns The record, or `undefined`
512
- */
513
- function parseRecord(value) {
514
- return isRecord(value) ? value : void 0;
515
- }
516
- /**
517
- * Validate that a {@link ContractShape} tree is well-formed — a pure recursive
518
- * prepass run before compilation.
519
- *
520
- * @remarks
521
- * Fail-fast, per AGENTS §12: a malformed shape is a programmer error, so this
522
- * throws a plain `Error` immediately rather than surfacing as a silently-wrong
523
- * guard, parser, schema, or generator later. Checks, recursively:
524
- *
525
- * - An {@link OptionalShape} is only legal as a direct object-property value —
526
- * `optionalShape` wrapping an array item, a union variant, another
527
- * optional/nullable's inner shape, `additionalProperties`, or the top-level
528
- * shape all throw. An object property IS the one legal placement: its value
529
- * is unwrapped to `.inner` before recursing, so `.inner` itself is validated
530
- * as a normal (non-optional-wrapping) shape.
531
- * - A {@link UnionShape} needs at least one variant; a {@link LiteralShape}
532
- * needs at least one value and rejects non-finite (`NaN` / `Infinity` /
533
- * `-Infinity`) number values.
534
- * - A bounded {@link StringShape} / {@link NumberShape} / {@link ArrayShape}
535
- * needs `min <= max` when both are set.
536
- * - An integer {@link NumberShape} (`integer: true`) needs a non-empty integer
537
- * range: `Math.ceil(min ?? -Infinity) <= Math.floor(max ?? Infinity)`.
538
- * - `null` / `json` / `raw` / `boolean` are always-valid leaves. Recursion
539
- * continues into array items, object properties (and `additionalProperties`
540
- * when it is a shape), union variants, and optional/nullable inner shapes.
541
- *
542
- * @param shape - The shape to validate
543
- * @throws {Error} When the shape is malformed
544
- */
545
- function validateShape(shape) {
546
- switch (shape.type) {
547
- case "string":
548
- if (shape.min !== void 0 && shape.max !== void 0 && shape.min > shape.max) throw new Error("validateShape: a string shape has min greater than max");
549
- return;
550
- case "number":
551
- if (shape.min !== void 0 && shape.max !== void 0 && shape.min > shape.max) throw new Error("validateShape: a number shape has min greater than max");
552
- if (shape.integer === true) {
553
- if (Math.ceil(shape.min ?? Number.NEGATIVE_INFINITY) > Math.floor(shape.max ?? Number.POSITIVE_INFINITY)) throw new Error("validateShape: an integer number shape has an empty integer range");
554
- }
555
- return;
556
- case "boolean":
557
- case "null":
558
- case "json":
559
- case "raw": return;
560
- case "literal":
561
- if (shape.values.length === 0) throw new Error("validateShape: a literal shape needs at least one value");
562
- for (const value of shape.values) if (typeof value === "number" && !Number.isFinite(value)) throw new Error("validateShape: a literal shape may not contain non-finite number values");
563
- return;
564
- case "array":
565
- if (shape.min !== void 0 && shape.max !== void 0 && shape.min > shape.max) throw new Error("validateShape: an array shape has min greater than max");
566
- validateShape(shape.items);
567
- return;
568
- case "object": {
569
- for (const key of Object.keys(shape.properties)) {
570
- const child = shape.properties[key];
571
- if (child === void 0) continue;
572
- validateShape(child.type === "optional" ? child.inner : child);
573
- }
574
- const extra = shape.additionalProperties;
575
- if (extra !== void 0 && extra !== true && extra !== false) validateShape(extra);
576
- return;
577
- }
578
- case "union":
579
- if (shape.variants.length === 0) throw new Error("validateShape: a union shape needs at least one variant");
580
- for (const variant of shape.variants) validateShape(variant);
581
- return;
582
- case "optional": throw new Error("validateShape: an optional shape may only appear as a direct object-property value");
583
- case "nullable":
584
- validateShape(shape.inner);
585
- return;
586
- }
587
- }
588
- /**
589
- * Compile a {@link ContractShape} into a JSON Schema document.
590
- *
591
- * @remarks
592
- * Object shapes emit `additionalProperties: false` (unless opened) and list only
593
- * required keys in `required`; nullable shapes emit an `anyOf` with `{ type:
594
- * 'null' }`. Emission only — it never inspects a runtime value.
595
- *
596
- * @param shape - The shape to compile
597
- * @returns The emitted JSON Schema
598
- */
599
- function compileSchema(shape) {
600
- switch (shape.type) {
601
- case "string": return {
602
- type: "string",
603
- ...shape.min !== void 0 ? { minLength: shape.min } : {},
604
- ...shape.max !== void 0 ? { maxLength: shape.max } : {},
605
- ...shape.pattern !== void 0 ? { pattern: shape.pattern.source } : {},
606
- ...shape.description !== void 0 ? { description: shape.description } : {}
607
- };
608
- case "number": return {
609
- type: shape.integer === true ? "integer" : "number",
610
- ...shape.min !== void 0 ? { minimum: shape.min } : {},
611
- ...shape.max !== void 0 ? { maximum: shape.max } : {},
612
- ...shape.description !== void 0 ? { description: shape.description } : {}
613
- };
614
- case "boolean": return {
615
- type: "boolean",
616
- ...shape.description !== void 0 ? { description: shape.description } : {}
617
- };
618
- case "null": return {
619
- type: "null",
620
- ...shape.description !== void 0 ? { description: shape.description } : {}
621
- };
622
- case "json": return { ...shape.description !== void 0 ? { description: shape.description } : {} };
623
- case "literal": return {
624
- enum: [...shape.values],
625
- ...shape.description !== void 0 ? { description: shape.description } : {}
626
- };
627
- case "array": return {
628
- type: "array",
629
- items: compileSchema(shape.items),
630
- ...shape.min !== void 0 ? { minItems: shape.min } : {},
631
- ...shape.max !== void 0 ? { maxItems: shape.max } : {},
632
- ...shape.description !== void 0 ? { description: shape.description } : {}
633
- };
634
- case "object": {
635
- const properties = {};
636
- const required = [];
637
- for (const key of Object.keys(shape.properties)) {
638
- const child = shape.properties[key];
639
- if (child === void 0) continue;
640
- properties[key] = compileSchema(child);
641
- if (child.type !== "optional") required.push(key);
642
- }
643
- const extra = shape.additionalProperties;
644
- const additionalProperties = extra === true ? true : extra !== void 0 && extra !== false ? compileSchema(extra) : false;
645
- return {
646
- type: "object",
647
- ...Object.keys(properties).length > 0 ? { properties } : {},
648
- ...required.length > 0 ? { required } : {},
649
- additionalProperties,
650
- ...shape.description !== void 0 ? { description: shape.description } : {}
651
- };
652
- }
653
- case "union": return {
654
- ...shape.mode === "oneOf" ? { oneOf: shape.variants.map((variant) => compileSchema(variant)) } : { anyOf: shape.variants.map((variant) => compileSchema(variant)) },
655
- ...shape.description !== void 0 ? { description: shape.description } : {}
656
- };
657
- case "optional": return compileSchema(shape.inner);
658
- case "nullable": return { anyOf: [compileSchema(shape.inner), { type: "null" }] };
659
- case "raw": return shape.schema;
660
- }
661
- }
662
- /**
663
- * Compile a {@link ContractShape} into a runtime type guard.
664
- *
665
- * @remarks
666
- * Reuses the combinators: `literalOf` for literals, `arrayOf` for arrays,
667
- * `recordOf` for closed objects, `unionOf` for unions, `nullableOf` for nullable,
668
- * and `whereOf` for constraint refinement. Like every guard it is total — it
669
- * never throws (AGENTS §14).
670
- *
671
- * @param shape - The shape to compile
672
- * @returns A guard narrowing to the shape's inferred type
673
- */
674
- function compileGuard(shape) {
675
- switch (shape.type) {
676
- case "string": return stringOf({
677
- min: shape.min,
678
- max: shape.max,
679
- pattern: shape.pattern
680
- });
681
- case "number": {
682
- const base = shape.integer === true ? isInteger : isFiniteNumber;
683
- if (shape.min === void 0 && shape.max === void 0) return base;
684
- return shape.integer === true ? intersectionOf(isInteger, boundsOf(shape.min, shape.max)) : boundsOf(shape.min, shape.max);
685
- }
686
- case "boolean": return isBoolean;
687
- case "null": return isNull;
688
- case "json": return isJSONValue;
689
- case "literal": return literalOf(...shape.values);
690
- case "array": {
691
- const base = arrayOf(compileGuard(shape.items));
692
- if (shape.min === void 0 && shape.max === void 0) return base;
693
- const withinLength = boundsOf(shape.min, shape.max);
694
- return whereOf(base, (value) => withinLength(value.length));
695
- }
696
- case "object": {
697
- const map = Object.create(null);
698
- const optionalKeys = [];
699
- for (const key of Object.keys(shape.properties)) {
700
- const child = shape.properties[key];
701
- if (child === void 0) continue;
702
- if (child.type === "optional") {
703
- map[key] = compileGuard(child.inner);
704
- optionalKeys.push(key);
705
- } else map[key] = compileGuard(child);
706
- }
707
- const extra = shape.additionalProperties;
708
- if (extra === void 0 || extra === false) return optionalKeys.length > 0 ? recordOf(map, optionalKeys) : recordOf(map);
709
- const additional = extra === true ? void 0 : compileGuard(extra);
710
- const required = Object.keys(map).filter((key) => !optionalKeys.includes(key));
711
- return (value) => {
712
- if (!isRecord(value)) return false;
713
- for (const key of required) if (!Object.hasOwn(value, key)) return false;
714
- const outcome = attempt(() => {
715
- for (const key of Object.keys(value)) {
716
- const guard = Object.hasOwn(map, key) ? map[key] : void 0;
717
- if (guard !== void 0) {
718
- if (!guard(value[key])) return false;
719
- } else if (additional !== void 0 && !additional(value[key])) return false;
720
- }
721
- return true;
722
- });
723
- return outcome.success && outcome.value;
724
- };
725
- }
726
- case "union": return unionOf(...shape.variants.map((variant) => compileGuard(variant)));
727
- case "optional": return orOf(isUndefined, compileGuard(shape.inner));
728
- case "nullable": return nullableOf(compileGuard(shape.inner));
729
- case "raw": return (_value) => true;
730
- }
731
- }
732
- /**
733
- * Compile a {@link ContractShape} into an input parser.
734
- *
735
- * @remarks
736
- * Reuses the leaf parsers (`parseString` / `parseInteger` / `parseNumber` /
737
- * `parseBoolean` / `parseRecord`) and coerces structurally. An object fails as a
738
- * whole on any required-field failure; a union returns a guard-valid value
739
- * unchanged, otherwise the first variant that both parses and guards wins.
740
- *
741
- * After coercing a leaf, it re-applies that leaf's REFINEMENTS through the same
742
- * combinators `compileGuard` uses — `stringOf` for a string's length/pattern and
743
- * `boundsOf` for a number's value and an array's length — so a value that coerces
744
- * but violates a bound parses to `undefined`. The result is full parse↔guard
745
- * soundness (AGENTS §14): a non-`undefined` parse always satisfies the contract's
746
- * `is`, refinements included.
747
- *
748
- * @param shape - The shape to compile
749
- * @returns A parser yielding the shape's inferred type or `undefined`
750
- */
751
- function compileParser(shape) {
752
- switch (shape.type) {
753
- case "string": {
754
- if (shape.min === void 0 && shape.max === void 0 && shape.pattern === void 0) return parseString;
755
- const guard = stringOf({
756
- min: shape.min,
757
- max: shape.max,
758
- pattern: shape.pattern
759
- });
760
- return (value) => {
761
- const parsed = parseString(value);
762
- return parsed !== void 0 && guard(parsed) ? parsed : void 0;
763
- };
764
- }
765
- case "number": {
766
- const base = shape.integer === true ? parseInteger : parseNumber;
767
- if (shape.min === void 0 && shape.max === void 0) return base;
768
- const within = boundsOf(shape.min, shape.max);
769
- return (value) => {
770
- const parsed = base(value);
771
- return parsed !== void 0 && within(parsed) ? parsed : void 0;
772
- };
773
- }
774
- case "boolean": return parseBoolean;
775
- case "null": return (value) => value === null ? null : void 0;
776
- case "json": return (value) => isJSONValue(value) ? value : void 0;
777
- case "literal": {
778
- const allowed = new Set(shape.values);
779
- return (value) => {
780
- if (allowed.has(value)) return value;
781
- if (isString(value)) {
782
- const trimmed = value.trim();
783
- if (allowed.has(trimmed)) return trimmed;
784
- }
785
- };
786
- }
787
- case "array": {
788
- const item = compileParser(shape.items);
789
- const unbounded = shape.min === void 0 && shape.max === void 0;
790
- const withinLength = boundsOf(shape.min, shape.max);
791
- return (value) => {
792
- if (!isArray(value)) return void 0;
793
- const result = [];
794
- for (const entry of value) {
795
- const parsed = item(entry);
796
- if (parsed === void 0) return void 0;
797
- result.push(parsed);
798
- }
799
- return unbounded || withinLength(result.length) ? result : void 0;
800
- };
801
- }
802
- case "object": {
803
- const entries = [];
804
- for (const key of Object.keys(shape.properties)) {
805
- const child = shape.properties[key];
806
- if (child === void 0) continue;
807
- const optional = child.type === "optional";
808
- entries.push({
809
- key,
810
- parse: compileParser(optional ? child.inner : child),
811
- optional
812
- });
813
- }
814
- const known = new Set(entries.map((entry) => entry.key));
815
- const extra = shape.additionalProperties;
816
- const additional = extra === void 0 || extra === false || extra === true ? void 0 : compileParser(extra);
817
- const open = extra === true || additional !== void 0;
818
- return (value) => {
819
- const record = parseRecord(value);
820
- if (record === void 0) return void 0;
821
- const outcome = attempt(() => {
822
- const result = Object.create(null);
823
- for (const entry of entries) {
824
- const raw = record[entry.key];
825
- if (raw === void 0) {
826
- if (entry.optional) continue;
827
- return;
828
- }
829
- const parsed = entry.parse(raw);
830
- if (parsed === void 0) return void 0;
831
- result[entry.key] = parsed;
832
- }
833
- if (open) for (const key of Object.keys(record)) {
834
- if (known.has(key)) continue;
835
- if (additional === void 0) result[key] = record[key];
836
- else {
837
- const parsed = additional(record[key]);
838
- if (parsed === void 0) return void 0;
839
- result[key] = parsed;
840
- }
841
- }
842
- return result;
843
- });
844
- return outcome.success ? outcome.value : void 0;
845
- };
846
- }
847
- case "union": {
848
- const variants = shape.variants.map((variant) => ({
849
- parse: compileParser(variant),
850
- guard: compileGuard(variant)
851
- }));
852
- return (value) => {
853
- for (const variant of variants) if (variant.guard(value)) return value;
854
- for (const variant of variants) {
855
- const parsed = variant.parse(value);
856
- if (parsed !== void 0 && variant.guard(parsed)) return parsed;
857
- }
858
- };
859
- }
860
- case "optional": {
861
- const inner = compileParser(shape.inner);
862
- return (value) => value === void 0 ? void 0 : inner(value);
863
- }
864
- case "nullable": {
865
- const inner = compileParser(shape.inner);
866
- return (value) => value === null ? null : inner(value);
867
- }
868
- case "raw": return (value) => value;
869
- }
870
- }
871
- /**
872
- * Compile a {@link ContractShape} into a deterministic seed value.
873
- *
874
- * @remarks
875
- * The same shape and the same `random` source always produce the same value, so
876
- * seed data is reproducible. Defaults to a {@link seededRandom} source seeded
877
- * from the wall clock when none is supplied. Throws on a degenerate empty
878
- * `literalShape` / `unionShape`, on a pattern-constrained `stringShape` whose
879
- * generated sample cannot satisfy the pattern, or on a `rawShape` (its embedded
880
- * schema is arbitrary and cannot be auto-generated) — a programmer error that
881
- * cannot generate a value (AGENTS §12). `createContract` runs
882
- * {@link validateShape} first, so a degenerate `literalShape` / `unionShape` /
883
- * bounded shape is normally caught there; these throws remain here as defense
884
- * for standalone `compileGenerator` use.
885
- *
886
- * @param shape - The shape to generate from
887
- * @param random - A seeded random source (defaults to `seededRandom(Date.now())`)
888
- * @returns A value matching the shape
889
- */
890
- function compileGenerator(shape, random = seededRandom(Date.now())) {
891
- switch (shape.type) {
892
- case "string": {
893
- const min = shape.min ?? 0;
894
- const max = shape.max ?? Math.max(min, 12);
895
- const length = Math.max(min, Math.min(max, 8));
896
- const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789";
897
- let value = "";
898
- for (let index = 0; index < length; index += 1) value += alphabet[Math.floor(random() * 36)];
899
- if (shape.pattern !== void 0 && !shape.pattern.test(value)) throw new Error("compileGenerator: a pattern-constrained string shape cannot be auto-generated — supply or verify values another way");
900
- return value;
901
- }
902
- case "number": {
903
- const min = shape.min ?? 0;
904
- const max = shape.max ?? 100;
905
- if (shape.integer === true) {
906
- const lo = Math.ceil(min);
907
- const hi = Math.floor(max);
908
- return Math.floor(random() * (hi - lo + 1)) + lo;
909
- }
910
- return random() * (max - min) + min;
911
- }
912
- case "boolean": return random() >= .5;
913
- case "null": return null;
914
- case "json": {
915
- const pick = Math.floor(random() * 5);
916
- if (pick === 0) return null;
917
- if (pick === 1) return random() >= .5;
918
- if (pick === 2) return Math.floor(random() * 1e3);
919
- if (pick === 3) {
920
- const alphabet = "abcdefghijklmnopqrstuvwxyz";
921
- let value = "";
922
- for (let index = 0; index < 6; index += 1) value += alphabet[Math.floor(random() * 26)];
923
- return value;
924
- }
925
- return { value: Math.floor(random() * 1e3) };
926
- }
927
- case "literal":
928
- if (shape.values.length === 0) throw new Error("compileGenerator: a literal shape needs at least one value");
929
- return shape.values[Math.floor(random() * shape.values.length)];
930
- case "array": {
931
- const lo = shape.min ?? Math.min(1, shape.max ?? 1);
932
- const hi = shape.max ?? Math.max(lo, 3);
933
- const length = Math.floor(random() * (hi - lo + 1)) + lo;
934
- const result = [];
935
- for (let index = 0; index < length; index += 1) result.push(compileGenerator(shape.items, random));
936
- return result;
937
- }
938
- case "object": {
939
- const result = {};
940
- for (const key of Object.keys(shape.properties)) {
941
- const child = shape.properties[key];
942
- if (child === void 0) continue;
943
- if (child.type === "optional" && random() < .3) continue;
944
- result[key] = compileGenerator(child, random);
945
- }
946
- const extra = shape.additionalProperties;
947
- if (extra !== void 0 && extra !== true && extra !== false) {
948
- const count = 1 + Math.floor(random() * 2);
949
- for (let index = 0; index < count; index += 1) {
950
- const key = `key${index}`;
951
- if (Object.hasOwn(result, key)) continue;
952
- result[key] = compileGenerator(extra, random);
953
- }
954
- }
955
- return result;
956
- }
957
- case "union":
958
- if (shape.variants.length === 0) throw new Error("compileGenerator: a union shape needs at least one variant");
959
- return compileGenerator(shape.variants[Math.floor(random() * shape.variants.length)], random);
960
- case "optional": return compileGenerator(shape.inner, random);
961
- case "nullable": return random() < .2 ? null : compileGenerator(shape.inner, random);
962
- case "raw": throw new Error("compileGenerator: a raw shape embeds an arbitrary JSON Schema and cannot be auto-generated — supply values another way");
963
- }
964
- }
965
- function createContract(shape) {
966
- validateShape(shape);
967
- const schema = compileSchema(shape);
968
- const guard = compileGuard(shape);
969
- const parser = compileParser(shape);
970
- return {
971
- schema,
972
- is: guard,
973
- parse(value) {
974
- return parser(value);
975
- },
976
- generate(random) {
977
- return compileGenerator(shape, random);
978
- }
979
- };
980
- }
981
- /**
982
- * Build an {@link ObjectShape} from a property map.
983
- *
984
- * @remarks
985
- * Wrap any property in {@link optionalShape} to allow its absence. By default
986
- * the compiled guard rejects unknown keys; pass `additionalProperties` to open
987
- * the object.
988
- *
989
- * @param properties - Map of property names to child shapes
990
- * @param options - Optional `additionalProperties` and `description`
991
- * @returns An object shape
992
- *
993
- * @example
994
- * ```ts
995
- * const user = objectShape({
996
- * name: stringShape({ min: 1 }),
997
- * age: integerShape({ min: 0, max: 120 }),
998
- * bio: optionalShape(stringShape()),
999
- * })
1000
- * ```
1001
- */
1002
- function objectShape(properties, options) {
1003
- return {
1004
- type: "object",
1005
- properties,
1006
- additionalProperties: options?.additionalProperties,
1007
- description: options?.description
1008
- };
1009
- }
1010
76
  //#endregion
1011
77
  //#region src/core/helpers.ts
1012
78
  /**
@@ -1153,9 +219,16 @@ function globMatch(value, pattern) {
1153
219
  * @remarks
1154
220
  * Reads the condition's column — a `FieldPath`, resolved with `resolveField` (a
1155
221
  * string is one column; an array descends a nested value) — and applies the
1156
- * operator. Range operators use {@link compareValues}; `like` / `glob` / `starts`
1157
- * / `ends` match only strings; `any` / `none` test membership by value equality.
1158
- * Total a type mismatch is simply a non-match.
222
+ * operator. Range operators (`above` / `below` / `from` / `to` / `between`) use
223
+ * {@link compareValues}, the total order; the equality family (`equals` / `not`
224
+ * / `any` / `none`) uses {@link deepEqual} STRUCTURAL equality, not the total
225
+ * order's rank-5-collapses-all-objects behavior, so `equals` on an object/array
226
+ * operand only matches a structurally-equal value, never every row holding any
227
+ * object. This is a semantics change from ranking: `deepEqual` is SameValueZero
228
+ * on leaves, so `NaN` now equals `NaN` under `equals` / `any` (it never matched
229
+ * anything under the old rank-based comparison). `like` / `glob` / `starts` /
230
+ * `ends` match only strings; `absent` / `present` test nullishness. Total — a
231
+ * type mismatch is simply a non-match.
1159
232
  *
1160
233
  * @param row - The row to test
1161
234
  * @param condition - The condition to apply
@@ -1166,8 +239,8 @@ function matchesCondition(row, condition) {
1166
239
  const first = condition.values[0];
1167
240
  const second = condition.values[1];
1168
241
  switch (condition.operator) {
1169
- case "equals": return compareValues(value, first) === 0;
1170
- case "not": return compareValues(value, first) !== 0;
242
+ case "equals": return deepEqual(value, first);
243
+ case "not": return !deepEqual(value, first);
1171
244
  case "above": return compareValues(value, first) > 0;
1172
245
  case "below": return compareValues(value, first) < 0;
1173
246
  case "from": return compareValues(value, first) >= 0;
@@ -1177,8 +250,8 @@ function matchesCondition(row, condition) {
1177
250
  case "glob": return isString(value) && isString(first) && globMatch(value, first);
1178
251
  case "starts": return isString(value) && isString(first) && value.startsWith(first);
1179
252
  case "ends": return isString(value) && isString(first) && value.endsWith(first);
1180
- case "any": return condition.values.some((candidate) => compareValues(value, candidate) === 0);
1181
- case "none": return !condition.values.some((candidate) => compareValues(value, candidate) === 0);
253
+ case "any": return condition.values.some((candidate) => deepEqual(value, candidate));
254
+ case "none": return !condition.values.some((candidate) => deepEqual(value, candidate));
1182
255
  case "absent": return value === void 0 || value === null;
1183
256
  case "present": return value !== void 0 && value !== null;
1184
257
  }
@@ -1363,6 +436,44 @@ function shapeToColumnType(shape) {
1363
436
  }
1364
437
  }
1365
438
  /**
439
+ * Whether a value is a well-formed {@link DriverMeta} — the boundary guard a
440
+ * versioning driver's `meta()` narrows a stored (structured-clone or
441
+ * `JSON.parse`d) value through before trusting it, replacing the per-driver
442
+ * duplicated narrowing every backend used to hand-roll (AGENTS §14: never `as`).
443
+ *
444
+ * @remarks
445
+ * Total and total-recursive over the whole shape: a finite `version`, and a
446
+ * `schema` array of well-formed {@link TableSchema} entries — each a `name` /
447
+ * `primary` string pair, a `columns` array of well-formed {@link ColumnSchema}
448
+ * entries (a `name` string, a {@link ColumnType} literal, a `nullable`
449
+ * boolean), and an `indexes` array of string arrays. Anything off-shape
450
+ * (including a non-record) returns `false` rather than throwing.
451
+ *
452
+ * @param value - The value to test
453
+ * @returns `true` when `value` is a well-formed `DriverMeta`
454
+ *
455
+ * @example
456
+ * ```ts
457
+ * isDriverMeta({ version: 1, schema: [] }) // true
458
+ * isDriverMeta({ version: 1, schema: [{ name: 'users' }] }) // false
459
+ * ```
460
+ */
461
+ function isDriverMeta(value) {
462
+ const COLUMN_TYPES = [
463
+ "text",
464
+ "integer",
465
+ "real",
466
+ "boolean",
467
+ "json",
468
+ "blob"
469
+ ];
470
+ const isColumnType = (candidate) => isString(candidate) && COLUMN_TYPES.some((type) => type === candidate);
471
+ const isColumnSchema = (candidate) => isRecord(candidate) && isString(candidate.name) && isColumnType(candidate.type) && isBoolean(candidate.nullable);
472
+ const isIndexGroup = (candidate) => isArray(candidate) && candidate.every((entry) => isString(entry));
473
+ const isTableSchema = (candidate) => isRecord(candidate) && isString(candidate.name) && isString(candidate.primary) && isArray(candidate.columns) && candidate.columns.every(isColumnSchema) && isArray(candidate.indexes) && candidate.indexes.every(isIndexGroup);
474
+ return isRecord(value) && isFiniteNumber(value.version) && isArray(value.schema) && value.schema.every(isTableSchema);
475
+ }
476
+ /**
1366
477
  * Throw when an {@link ReadOptions.signal | AbortSignal} has fired — the shared
1367
478
  * cancellation gate checked at operation boundaries and between streamed rows.
1368
479
  *
@@ -1406,11 +517,22 @@ function checkAbort(signal) {
1406
517
  * plan labels only — version tracking itself is deferred to persistent
1407
518
  * backends.
1408
519
  *
520
+ * A column present in BOTH schemas under the same name but with a different
521
+ * `type` or `nullable` throws a `MIGRATION` {@link DatabaseError} naming the
522
+ * table, the column, and the from→to difference — a name-only diff would
523
+ * otherwise silently produce NO step for the drift, and versioned
524
+ * reconciliation would stamp over it. There is no automatic in-place
525
+ * type-change step: the manual path is to add a new column, copy/convert the
526
+ * data at the application layer, then remove the old column — two separate
527
+ * plans, never a single implicit "alter" step.
528
+ *
1409
529
  * @param deployed - The table schemas currently applied
1410
530
  * @param declared - The table schemas the caller wants applied
1411
531
  * @param from - The plan's source version label (defaults to `0`)
1412
532
  * @param to - The plan's target version label (defaults to `1`)
1413
533
  * @returns The migration plan moving `deployed` toward `declared`
534
+ * @throws A `MIGRATION` {@link DatabaseError} when a shared column's `type` or
535
+ * `nullable` differs between `deployed` and `declared`
1414
536
  *
1415
537
  * @example
1416
538
  * ```ts
@@ -1443,11 +565,29 @@ function planMigration(deployed, declared, from = 0, to = 1) {
1443
565
  table: table.name,
1444
566
  column: column.name
1445
567
  });
1446
- for (const column of table.columns) if (!beforeColumns.has(column.name)) steps.push({
1447
- operation: "column.add",
1448
- table: table.name,
1449
- column
1450
- });
568
+ for (const column of table.columns) {
569
+ const previous = beforeColumns.get(column.name);
570
+ if (previous === void 0) {
571
+ steps.push({
572
+ operation: "column.add",
573
+ table: table.name,
574
+ column
575
+ });
576
+ continue;
577
+ }
578
+ if (previous.type !== column.type || previous.nullable !== column.nullable) throw new DatabaseError("MIGRATION", `planMigration: column '${column.name}' on table '${table.name}' changed shape (type ${previous.type}→${column.type}, nullable ${previous.nullable}→${column.nullable}) — in-place type/nullability changes are not auto-migrated; add a new column, copy/convert the data, then remove the old column`, {
579
+ table: table.name,
580
+ column: column.name,
581
+ from: {
582
+ type: previous.type,
583
+ nullable: previous.nullable
584
+ },
585
+ to: {
586
+ type: column.type,
587
+ nullable: column.nullable
588
+ }
589
+ });
590
+ }
1451
591
  const sameIndex = (left, right) => left.length === right.length && left.every((column, position) => column === right[position]);
1452
592
  for (const index of before.indexes) if (!table.indexes.some((candidate) => sameIndex(candidate, index))) steps.push({
1453
593
  operation: "index.remove",
@@ -1510,12 +650,14 @@ function migrateRows(rows, steps) {
1510
650
  * default `id`, `posts` keyed by a non-id `slug`) and, calling `factory()`
1511
651
  * fresh for each phase so failures stay isolated, verifies: `open`/`close`;
1512
652
  * `read` of a missing key returns `undefined`; `write`/`read` round-trip with
1513
- * copy-in/copy-out isolation (mutating the caller's row after `write`, or the
1514
- * row `read` returns, never perturbs stored state) and upsert-overwrite;
1515
- * `delete` returns `true` then `false`; `keys`/`scan` yield in ascending key
1516
- * order; `clear` empties only its target table; `snapshot`'s rollback thunk
1517
- * restores pre-snapshot state; a scoped `snapshot(['users'])` rolls back only
1518
- * the named table, leaving a concurrent mutation to another table intact; a
653
+ * DEEP copy-in/copy-out isolation (mutating the caller's row including a
654
+ * NESTED field — after `write`, or a row `read` returns, never perturbs
655
+ * stored state) and upsert-overwrite; `delete` returns `true` then `false`;
656
+ * `keys`/`scan` yield in ascending key order; `clear` empties only its target
657
+ * table; `snapshot`'s rollback thunk restores pre-snapshot state, including a
658
+ * NESTED field mutated in place on a read-back row between capture and
659
+ * restore; a scoped `snapshot(['users'])` rolls back only the named table,
660
+ * leaving a concurrent mutation to another table intact; a
1519
661
  * non-`id` primary key (`posts.slug`) round-trips; a nested-object row
1520
662
  * round-trips structurally (via {@link deepEqual}). The optional surface is
1521
663
  * presence-gated: when `migrate` exists, a `column.remove` plan strips the
@@ -1568,11 +710,16 @@ async function* driverFindings(factory) {
1568
710
  name: "age",
1569
711
  type: "integer",
1570
712
  nullable: true
713
+ },
714
+ {
715
+ name: "meta",
716
+ type: "json",
717
+ nullable: true
1571
718
  }
1572
719
  ],
1573
720
  indexes: []
1574
721
  };
1575
- const CONFORMANCE_SCHEMA = [CONFORMANCE_USERS_SCHEMA, {
722
+ const CONFORMANCE_POSTS_SCHEMA = {
1576
723
  name: "posts",
1577
724
  primary: "slug",
1578
725
  columns: [{
@@ -1585,7 +732,8 @@ async function* driverFindings(factory) {
1585
732
  nullable: false
1586
733
  }],
1587
734
  indexes: []
1588
- }];
735
+ };
736
+ const CONFORMANCE_SCHEMA = [CONFORMANCE_USERS_SCHEMA, CONFORMANCE_POSTS_SCHEMA];
1589
737
  const findingOf = (check, message, context) => ({
1590
738
  check,
1591
739
  message,
@@ -1622,29 +770,33 @@ async function* driverFindings(factory) {
1622
770
  const input = {
1623
771
  id: "u1",
1624
772
  name: "Ada",
1625
- age: 30
773
+ age: 30,
774
+ meta: { tags: ["a"] }
1626
775
  };
1627
776
  await driver.write("users", "u1", input);
1628
777
  input.name = "Mutated after write";
778
+ if (isRecord(input.meta) && Array.isArray(input.meta.tags)) input.meta.tags.push("mutated");
1629
779
  const stored = await driver.read("users", "u1");
1630
780
  const original = {
1631
781
  id: "u1",
1632
782
  name: "Ada",
1633
- age: 30
783
+ age: 30,
784
+ meta: { tags: ["a"] }
1634
785
  };
1635
786
  if (stored === void 0 || !deepEqual(stored, original)) {
1636
787
  await driver.close();
1637
- return findingOf("copy-in", "write must copy the input row rather than store it by reference", {
788
+ return findingOf("copy-in", "write must deep-copy the input row (including nested fields) rather than store it by reference", {
1638
789
  table: "users",
1639
790
  expected: original,
1640
791
  actual: stored
1641
792
  });
1642
793
  }
1643
794
  stored.name = "Mutated after read";
795
+ if (isRecord(stored.meta) && Array.isArray(stored.meta.tags)) stored.meta.tags.push("mutated");
1644
796
  const reread = await driver.read("users", "u1");
1645
797
  if (reread === void 0 || !deepEqual(reread, original)) {
1646
798
  await driver.close();
1647
- return findingOf("copy-out", "read must copy the stored row rather than return it by reference", {
799
+ return findingOf("copy-out", "read must deep-copy the stored row (including nested fields) rather than return it by reference", {
1648
800
  table: "users",
1649
801
  expected: original,
1650
802
  actual: reread
@@ -1807,6 +959,37 @@ async function* driverFindings(factory) {
1807
959
  });
1808
960
  }
1809
961
  },
962
+ {
963
+ check: "snapshot-nested",
964
+ run: async () => {
965
+ const driver = factory();
966
+ await driver.open(CONFORMANCE_SCHEMA);
967
+ const original = {
968
+ id: "u3",
969
+ name: "Nested",
970
+ age: 20,
971
+ meta: { tags: ["a"] }
972
+ };
973
+ await driver.write("users", "u3", original);
974
+ const rollback = await driver.snapshot();
975
+ const before = await driver.read("users", "u3");
976
+ if (isRecord(before) && isRecord(before.meta) && Array.isArray(before.meta.tags)) before.meta.tags.push("mutated-before-restore");
977
+ await driver.write("users", "u3", {
978
+ id: "u3",
979
+ name: "Nested",
980
+ age: 20,
981
+ meta: { tags: ["a", "mutated-after-write"] }
982
+ });
983
+ await rollback();
984
+ const restored = await driver.read("users", "u3");
985
+ await driver.close();
986
+ if (restored === void 0 || !deepEqual(restored, original)) return findingOf("snapshot-nested", "snapshot rollback must restore pre-snapshot nested field values, unaffected by a later in-place mutation of a read-back row", {
987
+ table: "users",
988
+ expected: original,
989
+ actual: restored
990
+ });
991
+ }
992
+ },
1810
993
  {
1811
994
  check: "non-id-primary",
1812
995
  run: async () => {
@@ -1855,21 +1038,22 @@ async function* driverFindings(factory) {
1855
1038
  run: async () => {
1856
1039
  const driver = factory();
1857
1040
  if (driver.migrate === void 0) return void 0;
1858
- await driver.open(CONFORMANCE_SCHEMA);
1041
+ const deployedUsers = {
1042
+ ...CONFORMANCE_USERS_SCHEMA,
1043
+ columns: [...CONFORMANCE_USERS_SCHEMA.columns, {
1044
+ name: "legacy",
1045
+ type: "boolean",
1046
+ nullable: true
1047
+ }]
1048
+ };
1049
+ await driver.open([deployedUsers, CONFORMANCE_POSTS_SCHEMA]);
1859
1050
  await driver.write("users", "u1", {
1860
1051
  id: "u1",
1861
1052
  name: "Ada",
1862
1053
  age: 30,
1863
1054
  legacy: true
1864
1055
  });
1865
- const removePlan = planMigration([{
1866
- ...CONFORMANCE_USERS_SCHEMA,
1867
- columns: [...CONFORMANCE_USERS_SCHEMA.columns, {
1868
- name: "legacy",
1869
- type: "boolean",
1870
- nullable: false
1871
- }]
1872
- }], [CONFORMANCE_USERS_SCHEMA]);
1056
+ const removePlan = planMigration([deployedUsers], [CONFORMANCE_USERS_SCHEMA]);
1873
1057
  await driver.migrate(removePlan);
1874
1058
  const migrated = await driver.read("users", "u1");
1875
1059
  if (migrated === void 0 || "legacy" in migrated) {
@@ -2131,173 +1315,40 @@ async function auditDriver(factory) {
2131
1315
  for await (const finding of driverFindings(factory)) findings.push(finding);
2132
1316
  return findings;
2133
1317
  }
2134
- //#endregion
2135
- //#region node_modules/@orkestrel/emitter/dist/src/core/index.js
2136
1318
  /**
2137
- * Extract the own enumerable keys of a mapped object, typed as its key union.
1319
+ * Generate an RFC 4122 version 4 UUID from a number source no host crypto global.
2138
1320
  *
2139
1321
  * @remarks
2140
- * `Object.keys` widens its result to `string[]`, which breaks the key↔value
2141
- * correlation a mapped type (like `EmitterHooks<TMap>`) otherwise guarantees.
2142
- * A `for…in` push into a `keyof`-typed array narrows the result back,
2143
- * type-safely and with no assertion.
2144
- *
2145
- * @typeParam T - The object shape whose keys are extracted.
2146
- * @param object - The object to read keys from.
2147
- * @returns The object's own enumerable keys, typed as `(keyof T)[]`.
1322
+ * Draws exactly {@link UUID_BYTE_COUNT} values from `random`, one per byte, then
1323
+ * forces the version (`4`) and variant (`10xx`) bits. The default source is
1324
+ * `Math.random` a pure-ECMAScript intrinsic, so generation works on every host;
1325
+ * pass a seeded source (`seededRandom` from `@orkestrel/contract`) and reuse it
1326
+ * across calls for reproducible sequences in tests and fixtures — production
1327
+ * identifiers should keep the default source, whose engine entropy is far larger
1328
+ * than a 32-bit seed. Each byte is floored and masked, so a source straying
1329
+ * outside `[0, 1)` (negative, `>= 1`, `NaN`, `Infinity`) can never yield a
1330
+ * malformed UUID. Suitable as a collision-resistant record identifier — not a
1331
+ * cryptographic token; never use one as a secret.
1332
+ *
1333
+ * @param random - A number source returning values in the half-open range `[0, 1)` (defaults to `Math.random`)
1334
+ * @returns A lowercase RFC 4122 version 4 UUID
2148
1335
  *
2149
1336
  * @example
2150
1337
  * ```ts
2151
- * import { extractKeys } from '@src/core'
1338
+ * import { generateUUID } from '@orkestrel/database'
1339
+ * import { seededRandom } from '@orkestrel/contract'
2152
1340
  *
2153
- * const hooks = { tick: () => {}, done: () => {} }
2154
- * extractKeys(hooks) // ['tick', 'done']
2155
- * extractKeys({}) // []
1341
+ * generateUUID() // e.g. '9b2f7c1e-3d4a-4f6b-8e2d-5a1c0b9f8e7d'
1342
+ * generateUUID(seededRandom(42)) // the same UUID on every run
2156
1343
  * ```
2157
1344
  */
2158
- function extractKeys(object) {
2159
- const collected = [];
2160
- for (const key in object) collected.push(key);
2161
- return collected;
2162
- }
2163
- Object.freeze([
2164
- "null",
2165
- "boolean",
2166
- "object",
2167
- "array",
2168
- "number",
2169
- "integer",
2170
- "string"
2171
- ]);
2172
- /** Determine whether a value is callable. */
2173
- function isFunction(value) {
2174
- return typeof value === "function";
1345
+ function generateUUID(random = Math.random) {
1346
+ const bytes = Array.from({ length: 16 }, () => Math.floor(random() * 256) & 255);
1347
+ bytes[6] = bytes[6] & 15 | 64;
1348
+ bytes[8] = bytes[8] & 63 | 128;
1349
+ const hex = bytes.map((byte) => byte.toString(16).padStart(2, "0"));
1350
+ return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10).join("")}`;
2175
1351
  }
2176
- /**
2177
- * A typed synchronous event emitter — the foundational observable primitive of
2178
- * the codebase (AGENTS §13). Stateful entities OWN one as a `#emitter` field and
2179
- * expose it through `readonly emitter`; they never inherit from it.
2180
- *
2181
- * @typeParam TMap - The event map: each event name to the argument tuple its
2182
- * listeners receive.
2183
- *
2184
- * @remarks
2185
- * - **Synchronous.** `emit` invokes listeners in registration order, in the
2186
- * current tick.
2187
- * - **Listener isolation.** A throwing listener never stops its siblings: every
2188
- * listener runs, and a throw is routed to the `error` handler
2189
- * ({@link EmitterOptions.error}) — never rethrown. Every throwing listener
2190
- * surfaces (not just the first), and with no `error` handler a throw is swallowed
2191
- * silently. The `error` handler runs inside its own try/catch, so a throwing
2192
- * error-handler is swallowed too (anti-recursion — it cannot escape or re-enter).
2193
- * - **Per-event storage.** Listeners live in a per-event `Set`, so every public
2194
- * method is precisely typed with no assertions.
2195
- * - **Destroyed → no-op.** After `destroy()`, `on` / `once` / `emit` do nothing
2196
- * and `destroyed` is `true`.
2197
- *
2198
- * @example
2199
- * ```ts
2200
- * type CounterEventMap = {
2201
- * tick: readonly [count: number]
2202
- * done: readonly []
2203
- * }
2204
- *
2205
- * const emitter = new Emitter<CounterEventMap>({
2206
- * on: { done: () => stop() },
2207
- * error: (error, event) => log(`listener for ${event} threw`, error),
2208
- * })
2209
- * emitter.on('tick', (count) => render(count))
2210
- * emitter.emit('tick', 1)
2211
- * ```
2212
- */
2213
- var Emitter = class {
2214
- #destroyed = false;
2215
- #listeners = {};
2216
- #wrappers = {};
2217
- #error;
2218
- constructor(options) {
2219
- const error = options?.error;
2220
- this.#error = isFunction(error) ? error : void 0;
2221
- const hooks = options?.on;
2222
- if (hooks !== void 0) this.#wire(hooks);
2223
- }
2224
- get destroyed() {
2225
- return this.#destroyed;
2226
- }
2227
- on(event, handler) {
2228
- if (this.#destroyed) return;
2229
- (this.#listeners[event] ??= /* @__PURE__ */ new Set()).add(handler);
2230
- }
2231
- once(event, handler) {
2232
- if (this.#destroyed) return;
2233
- const pending = this.#wrappers[event] ??= /* @__PURE__ */ new Map();
2234
- const wrapper = (...args) => {
2235
- this.#listeners[event]?.delete(wrapper);
2236
- const wrappers = pending.get(handler);
2237
- wrappers?.delete(wrapper);
2238
- if (wrappers !== void 0 && wrappers.size === 0) pending.delete(handler);
2239
- handler(...args);
2240
- };
2241
- const wrappers = pending.get(handler) ?? /* @__PURE__ */ new Set();
2242
- wrappers.add(wrapper);
2243
- pending.set(handler, wrappers);
2244
- this.on(event, wrapper);
2245
- }
2246
- off(event, handler) {
2247
- const listeners = this.#listeners[event];
2248
- const wrappers = this.#wrappers[event];
2249
- const pending = wrappers?.get(handler);
2250
- if (pending !== void 0) {
2251
- for (const wrapper of pending) listeners?.delete(wrapper);
2252
- wrappers?.delete(handler);
2253
- }
2254
- listeners?.delete(handler);
2255
- }
2256
- emit(event, ...args) {
2257
- if (this.#destroyed) return;
2258
- const listeners = this.#listeners[event];
2259
- if (listeners === void 0) return;
2260
- for (const handler of [...listeners]) try {
2261
- handler(...args);
2262
- } catch (error) {
2263
- this.#surface(error, event);
2264
- }
2265
- }
2266
- count(event) {
2267
- if (event !== void 0) return this.#listeners[event]?.size ?? 0;
2268
- let total = 0;
2269
- for (const set of Object.values(this.#listeners)) total += set?.size ?? 0;
2270
- return total;
2271
- }
2272
- clear(event) {
2273
- if (event !== void 0) {
2274
- delete this.#listeners[event];
2275
- delete this.#wrappers[event];
2276
- return;
2277
- }
2278
- this.#listeners = {};
2279
- this.#wrappers = {};
2280
- }
2281
- destroy() {
2282
- this.#listeners = {};
2283
- this.#wrappers = {};
2284
- this.#error = void 0;
2285
- this.#destroyed = true;
2286
- }
2287
- #surface(error, event) {
2288
- const handler = this.#error;
2289
- if (handler === void 0) return;
2290
- try {
2291
- handler(error, String(event));
2292
- } catch {}
2293
- }
2294
- #wire(hooks) {
2295
- for (const event of extractKeys(hooks)) {
2296
- const handler = hooks[event];
2297
- if (isFunction(handler)) this.on(event, handler);
2298
- }
2299
- }
2300
- };
2301
1352
  //#endregion
2302
1353
  //#region src/core/Cursor.ts
2303
1354
  /**
@@ -2669,6 +1720,21 @@ var Table = class {
2669
1720
  for (const row of source) if (this.#guard(row)) rows.push(row);
2670
1721
  return rows;
2671
1722
  }
1723
+ /**
1724
+ * Count rows matching `criteria`'s conditions.
1725
+ *
1726
+ * @remarks
1727
+ * Unlike {@link records}, which narrows every row through the table's
1728
+ * contract guard before returning it, `count` operates on STORED rows
1729
+ * WITHOUT that guard (both the native `driver.count` hook and the
1730
+ * `filterRows`-over-`#collect()` fallback count raw storage) — so it can
1731
+ * exceed `(await records(criteria)).length` when storage holds rows that
1732
+ * no longer conform to the table's contract (legacy or migrated data).
1733
+ *
1734
+ * @param criteria - Optional conditions to filter by (paging is ignored)
1735
+ * @param options - `{ signal }` to abort
1736
+ * @returns The count of matching stored rows
1737
+ */
2672
1738
  async count(criteria, options) {
2673
1739
  checkAbort(options?.signal);
2674
1740
  await this.#ready();
@@ -2677,6 +1743,23 @@ var Table = class {
2677
1743
  if (native !== void 0) return native;
2678
1744
  return filterRows(await this.#collect(), criteria?.conditions ?? []).length;
2679
1745
  }
1746
+ /**
1747
+ * Compute an aggregate over `column` across rows matching `criteria`'s
1748
+ * conditions.
1749
+ *
1750
+ * @remarks
1751
+ * Like {@link count}, `aggregate` operates on STORED rows WITHOUT the
1752
+ * contract guard {@link records} / {@link scan} apply — a non-conforming
1753
+ * stored row still contributes to the computed aggregate when it matches
1754
+ * the conditions, even though it would never appear in `records()`'s
1755
+ * output.
1756
+ *
1757
+ * @param operation - The aggregate to compute
1758
+ * @param column - The column to aggregate
1759
+ * @param criteria - Optional conditions to filter by (paging is ignored)
1760
+ * @param options - `{ signal }` to abort
1761
+ * @returns The aggregate value, or `undefined` when undefined for the inputs
1762
+ */
2680
1763
  async aggregate(operation, column, criteria, options) {
2681
1764
  checkAbort(options?.signal);
2682
1765
  await this.#ready();
@@ -2699,7 +1782,7 @@ var Table = class {
2699
1782
  *
2700
1783
  * @param criteria - Optional conditions plus offset/limit paging
2701
1784
  * @param options - `{ signal }` to abort mid-stream
2702
- * @returns An async generator of matching, guard-conforming rows
1785
+ * @returns An async iterable of matching, guard-conforming rows
2703
1786
  */
2704
1787
  async *scan(criteria, options) {
2705
1788
  checkAbort(options?.signal);
@@ -3118,8 +2201,12 @@ var Database = class Database {
3118
2201
  * @remarks
3119
2202
  * The in-between made concrete: it runs identically in a browser or on a server,
3120
2203
  * so it is the storage behind tests, ephemeral caches, and any code that wants
3121
- * the database API without a persistent backend. Rows are copied in and out so a
3122
- * caller can never mutate stored state by reference (AGENTS §11), and `snapshot`
2204
+ * the database API without a persistent backend. Rows are DEEP-copied (via
2205
+ * `structuredClone`) in and out at `write`, `read`, `scan`, `stream`, and both
2206
+ * snapshot capture and restore — so a caller mutating a nested field of an input
2207
+ * row, a returned row, or a row mutated in place between snapshot and rollback
2208
+ * can never perturb stored state (AGENTS §11); a shallow `{ ...row }` spread
2209
+ * would still share nested object/array references. `snapshot`
3123
2210
  * clones every table to give transactions an exact rollback point. `scan` and
3124
2211
  * `keys` yield in key order — sorted by the core {@link compareValues} total
3125
2212
  * order, the same contract the SQLite (`ORDER BY`) and IndexedDB (key-ordered
@@ -3136,10 +2223,10 @@ var MemoryDriver = class {
3136
2223
  async close() {}
3137
2224
  async read(table, key) {
3138
2225
  const row = this.#store(table).get(key);
3139
- return row === void 0 ? void 0 : { ...row };
2226
+ return row === void 0 ? void 0 : structuredClone(row);
3140
2227
  }
3141
2228
  async write(table, key, row) {
3142
- this.#store(table).set(key, { ...row });
2229
+ this.#store(table).set(key, structuredClone(row));
3143
2230
  }
3144
2231
  async delete(table, key) {
3145
2232
  return this.#store(table).delete(key);
@@ -3151,7 +2238,7 @@ var MemoryDriver = class {
3151
2238
  const store = this.#store(table);
3152
2239
  for (const key of this.#ordered(table)) {
3153
2240
  const row = store.get(key);
3154
- if (row !== void 0) yield { ...row };
2241
+ if (row !== void 0) yield structuredClone(row);
3155
2242
  }
3156
2243
  }
3157
2244
  /**
@@ -3195,7 +2282,7 @@ var MemoryDriver = class {
3195
2282
  skipped += 1;
3196
2283
  continue;
3197
2284
  }
3198
- yield { ...row };
2285
+ yield structuredClone(row);
3199
2286
  yielded += 1;
3200
2287
  }
3201
2288
  }
@@ -3219,12 +2306,16 @@ var MemoryDriver = class {
3219
2306
  const copy = /* @__PURE__ */ new Map();
3220
2307
  for (const [name, store] of this.#tables) {
3221
2308
  const cloned = /* @__PURE__ */ new Map();
3222
- for (const [key, row] of store) cloned.set(key, { ...row });
2309
+ for (const [key, row] of store) cloned.set(key, structuredClone(row));
3223
2310
  copy.set(name, cloned);
3224
2311
  }
3225
2312
  return async () => {
3226
2313
  this.#tables.clear();
3227
- for (const [name, store] of copy) this.#tables.set(name, store);
2314
+ for (const [name, store] of copy) {
2315
+ const restored = /* @__PURE__ */ new Map();
2316
+ for (const [key, row] of store) restored.set(key, structuredClone(row));
2317
+ this.#tables.set(name, restored);
2318
+ }
3228
2319
  };
3229
2320
  }
3230
2321
  const copy = /* @__PURE__ */ new Map();
@@ -3232,11 +2323,15 @@ var MemoryDriver = class {
3232
2323
  const store = this.#tables.get(name);
3233
2324
  if (store === void 0) continue;
3234
2325
  const cloned = /* @__PURE__ */ new Map();
3235
- for (const [key, row] of store) cloned.set(key, { ...row });
2326
+ for (const [key, row] of store) cloned.set(key, structuredClone(row));
3236
2327
  copy.set(name, cloned);
3237
2328
  }
3238
2329
  return async () => {
3239
- for (const [name, store] of copy) this.#tables.set(name, store);
2330
+ for (const [name, store] of copy) {
2331
+ const restored = /* @__PURE__ */ new Map();
2332
+ for (const [key, row] of store) restored.set(key, structuredClone(row));
2333
+ this.#tables.set(name, restored);
2334
+ }
3240
2335
  };
3241
2336
  }
3242
2337
  /**
@@ -3359,6 +2454,6 @@ function createMemoryDriver() {
3359
2454
  return new MemoryDriver();
3360
2455
  }
3361
2456
  //#endregion
3362
- export { Clause, Cursor, DEFAULT_PRIMARY, Database, DatabaseError, MAX_PATTERN_LENGTH, MemoryDriver, Query, Table, applyCriteria, auditDriver, checkAbort, compareValues, computeAggregate, conformDriver, createDatabase, createMemoryDriver, deepEqual, driverFindings, extractKey, filterRows, globMatch, isDatabaseError, likeMatch, matchesCondition, matchesCriteria, migrateRows, planMigration, shapeToColumnType, sortRows, wildcardMatch };
2457
+ export { Clause, Cursor, DEFAULT_PRIMARY, Database, DatabaseError, MAX_PATTERN_LENGTH, MemoryDriver, Query, Table, UUID_BYTE_COUNT, UUID_BYTE_RANGE, applyCriteria, auditDriver, checkAbort, compareValues, computeAggregate, conformDriver, createDatabase, createMemoryDriver, deepEqual, driverFindings, extractKey, filterRows, generateUUID, globMatch, isDatabaseError, isDriverMeta, likeMatch, matchesCondition, matchesCriteria, migrateRows, planMigration, shapeToColumnType, sortRows, wildcardMatch };
3363
2458
 
3364
2459
  //# sourceMappingURL=index.js.map