@orkestrel/database 0.0.1 → 0.0.2

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.
@@ -67,946 +69,6 @@ var DatabaseError = class extends Error {
67
69
  function isDatabaseError(value) {
68
70
  return value instanceof DatabaseError;
69
71
  }
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
72
  //#endregion
1011
73
  //#region src/core/helpers.ts
1012
74
  /**
@@ -1153,9 +215,16 @@ function globMatch(value, pattern) {
1153
215
  * @remarks
1154
216
  * Reads the condition's column — a `FieldPath`, resolved with `resolveField` (a
1155
217
  * 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.
218
+ * operator. Range operators (`above` / `below` / `from` / `to` / `between`) use
219
+ * {@link compareValues}, the total order; the equality family (`equals` / `not`
220
+ * / `any` / `none`) uses {@link deepEqual} STRUCTURAL equality, not the total
221
+ * order's rank-5-collapses-all-objects behavior, so `equals` on an object/array
222
+ * operand only matches a structurally-equal value, never every row holding any
223
+ * object. This is a semantics change from ranking: `deepEqual` is SameValueZero
224
+ * on leaves, so `NaN` now equals `NaN` under `equals` / `any` (it never matched
225
+ * anything under the old rank-based comparison). `like` / `glob` / `starts` /
226
+ * `ends` match only strings; `absent` / `present` test nullishness. Total — a
227
+ * type mismatch is simply a non-match.
1159
228
  *
1160
229
  * @param row - The row to test
1161
230
  * @param condition - The condition to apply
@@ -1166,8 +235,8 @@ function matchesCondition(row, condition) {
1166
235
  const first = condition.values[0];
1167
236
  const second = condition.values[1];
1168
237
  switch (condition.operator) {
1169
- case "equals": return compareValues(value, first) === 0;
1170
- case "not": return compareValues(value, first) !== 0;
238
+ case "equals": return deepEqual(value, first);
239
+ case "not": return !deepEqual(value, first);
1171
240
  case "above": return compareValues(value, first) > 0;
1172
241
  case "below": return compareValues(value, first) < 0;
1173
242
  case "from": return compareValues(value, first) >= 0;
@@ -1177,8 +246,8 @@ function matchesCondition(row, condition) {
1177
246
  case "glob": return isString(value) && isString(first) && globMatch(value, first);
1178
247
  case "starts": return isString(value) && isString(first) && value.startsWith(first);
1179
248
  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);
249
+ case "any": return condition.values.some((candidate) => deepEqual(value, candidate));
250
+ case "none": return !condition.values.some((candidate) => deepEqual(value, candidate));
1182
251
  case "absent": return value === void 0 || value === null;
1183
252
  case "present": return value !== void 0 && value !== null;
1184
253
  }
@@ -1363,6 +432,44 @@ function shapeToColumnType(shape) {
1363
432
  }
1364
433
  }
1365
434
  /**
435
+ * Whether a value is a well-formed {@link DriverMeta} — the boundary guard a
436
+ * versioning driver's `meta()` narrows a stored (structured-clone or
437
+ * `JSON.parse`d) value through before trusting it, replacing the per-driver
438
+ * duplicated narrowing every backend used to hand-roll (AGENTS §14: never `as`).
439
+ *
440
+ * @remarks
441
+ * Total and total-recursive over the whole shape: a finite `version`, and a
442
+ * `schema` array of well-formed {@link TableSchema} entries — each a `name` /
443
+ * `primary` string pair, a `columns` array of well-formed {@link ColumnSchema}
444
+ * entries (a `name` string, a {@link ColumnType} literal, a `nullable`
445
+ * boolean), and an `indexes` array of string arrays. Anything off-shape
446
+ * (including a non-record) returns `false` rather than throwing.
447
+ *
448
+ * @param value - The value to test
449
+ * @returns `true` when `value` is a well-formed `DriverMeta`
450
+ *
451
+ * @example
452
+ * ```ts
453
+ * isDriverMeta({ version: 1, schema: [] }) // true
454
+ * isDriverMeta({ version: 1, schema: [{ name: 'users' }] }) // false
455
+ * ```
456
+ */
457
+ function isDriverMeta(value) {
458
+ const COLUMN_TYPES = [
459
+ "text",
460
+ "integer",
461
+ "real",
462
+ "boolean",
463
+ "json",
464
+ "blob"
465
+ ];
466
+ const isColumnType = (candidate) => isString(candidate) && COLUMN_TYPES.some((type) => type === candidate);
467
+ const isColumnSchema = (candidate) => isRecord(candidate) && isString(candidate.name) && isColumnType(candidate.type) && isBoolean(candidate.nullable);
468
+ const isIndexGroup = (candidate) => isArray(candidate) && candidate.every((entry) => isString(entry));
469
+ 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);
470
+ return isRecord(value) && isFiniteNumber(value.version) && isArray(value.schema) && value.schema.every(isTableSchema);
471
+ }
472
+ /**
1366
473
  * Throw when an {@link ReadOptions.signal | AbortSignal} has fired — the shared
1367
474
  * cancellation gate checked at operation boundaries and between streamed rows.
1368
475
  *
@@ -1406,11 +513,22 @@ function checkAbort(signal) {
1406
513
  * plan labels only — version tracking itself is deferred to persistent
1407
514
  * backends.
1408
515
  *
516
+ * A column present in BOTH schemas under the same name but with a different
517
+ * `type` or `nullable` throws a `MIGRATION` {@link DatabaseError} naming the
518
+ * table, the column, and the from→to difference — a name-only diff would
519
+ * otherwise silently produce NO step for the drift, and versioned
520
+ * reconciliation would stamp over it. There is no automatic in-place
521
+ * type-change step: the manual path is to add a new column, copy/convert the
522
+ * data at the application layer, then remove the old column — two separate
523
+ * plans, never a single implicit "alter" step.
524
+ *
1409
525
  * @param deployed - The table schemas currently applied
1410
526
  * @param declared - The table schemas the caller wants applied
1411
527
  * @param from - The plan's source version label (defaults to `0`)
1412
528
  * @param to - The plan's target version label (defaults to `1`)
1413
529
  * @returns The migration plan moving `deployed` toward `declared`
530
+ * @throws A `MIGRATION` {@link DatabaseError} when a shared column's `type` or
531
+ * `nullable` differs between `deployed` and `declared`
1414
532
  *
1415
533
  * @example
1416
534
  * ```ts
@@ -1443,11 +561,29 @@ function planMigration(deployed, declared, from = 0, to = 1) {
1443
561
  table: table.name,
1444
562
  column: column.name
1445
563
  });
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
- });
564
+ for (const column of table.columns) {
565
+ const previous = beforeColumns.get(column.name);
566
+ if (previous === void 0) {
567
+ steps.push({
568
+ operation: "column.add",
569
+ table: table.name,
570
+ column
571
+ });
572
+ continue;
573
+ }
574
+ 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`, {
575
+ table: table.name,
576
+ column: column.name,
577
+ from: {
578
+ type: previous.type,
579
+ nullable: previous.nullable
580
+ },
581
+ to: {
582
+ type: column.type,
583
+ nullable: column.nullable
584
+ }
585
+ });
586
+ }
1451
587
  const sameIndex = (left, right) => left.length === right.length && left.every((column, position) => column === right[position]);
1452
588
  for (const index of before.indexes) if (!table.indexes.some((candidate) => sameIndex(candidate, index))) steps.push({
1453
589
  operation: "index.remove",
@@ -1510,12 +646,14 @@ function migrateRows(rows, steps) {
1510
646
  * default `id`, `posts` keyed by a non-id `slug`) and, calling `factory()`
1511
647
  * fresh for each phase so failures stay isolated, verifies: `open`/`close`;
1512
648
  * `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
649
+ * DEEP copy-in/copy-out isolation (mutating the caller's row including a
650
+ * NESTED field — after `write`, or a row `read` returns, never perturbs
651
+ * stored state) and upsert-overwrite; `delete` returns `true` then `false`;
652
+ * `keys`/`scan` yield in ascending key order; `clear` empties only its target
653
+ * table; `snapshot`'s rollback thunk restores pre-snapshot state, including a
654
+ * NESTED field mutated in place on a read-back row between capture and
655
+ * restore; a scoped `snapshot(['users'])` rolls back only the named table,
656
+ * leaving a concurrent mutation to another table intact; a
1519
657
  * non-`id` primary key (`posts.slug`) round-trips; a nested-object row
1520
658
  * round-trips structurally (via {@link deepEqual}). The optional surface is
1521
659
  * presence-gated: when `migrate` exists, a `column.remove` plan strips the
@@ -1568,11 +706,16 @@ async function* driverFindings(factory) {
1568
706
  name: "age",
1569
707
  type: "integer",
1570
708
  nullable: true
709
+ },
710
+ {
711
+ name: "meta",
712
+ type: "json",
713
+ nullable: true
1571
714
  }
1572
715
  ],
1573
716
  indexes: []
1574
717
  };
1575
- const CONFORMANCE_SCHEMA = [CONFORMANCE_USERS_SCHEMA, {
718
+ const CONFORMANCE_POSTS_SCHEMA = {
1576
719
  name: "posts",
1577
720
  primary: "slug",
1578
721
  columns: [{
@@ -1585,7 +728,8 @@ async function* driverFindings(factory) {
1585
728
  nullable: false
1586
729
  }],
1587
730
  indexes: []
1588
- }];
731
+ };
732
+ const CONFORMANCE_SCHEMA = [CONFORMANCE_USERS_SCHEMA, CONFORMANCE_POSTS_SCHEMA];
1589
733
  const findingOf = (check, message, context) => ({
1590
734
  check,
1591
735
  message,
@@ -1622,29 +766,33 @@ async function* driverFindings(factory) {
1622
766
  const input = {
1623
767
  id: "u1",
1624
768
  name: "Ada",
1625
- age: 30
769
+ age: 30,
770
+ meta: { tags: ["a"] }
1626
771
  };
1627
772
  await driver.write("users", "u1", input);
1628
773
  input.name = "Mutated after write";
774
+ if (isRecord(input.meta) && Array.isArray(input.meta.tags)) input.meta.tags.push("mutated");
1629
775
  const stored = await driver.read("users", "u1");
1630
776
  const original = {
1631
777
  id: "u1",
1632
778
  name: "Ada",
1633
- age: 30
779
+ age: 30,
780
+ meta: { tags: ["a"] }
1634
781
  };
1635
782
  if (stored === void 0 || !deepEqual(stored, original)) {
1636
783
  await driver.close();
1637
- return findingOf("copy-in", "write must copy the input row rather than store it by reference", {
784
+ return findingOf("copy-in", "write must deep-copy the input row (including nested fields) rather than store it by reference", {
1638
785
  table: "users",
1639
786
  expected: original,
1640
787
  actual: stored
1641
788
  });
1642
789
  }
1643
790
  stored.name = "Mutated after read";
791
+ if (isRecord(stored.meta) && Array.isArray(stored.meta.tags)) stored.meta.tags.push("mutated");
1644
792
  const reread = await driver.read("users", "u1");
1645
793
  if (reread === void 0 || !deepEqual(reread, original)) {
1646
794
  await driver.close();
1647
- return findingOf("copy-out", "read must copy the stored row rather than return it by reference", {
795
+ return findingOf("copy-out", "read must deep-copy the stored row (including nested fields) rather than return it by reference", {
1648
796
  table: "users",
1649
797
  expected: original,
1650
798
  actual: reread
@@ -1807,6 +955,37 @@ async function* driverFindings(factory) {
1807
955
  });
1808
956
  }
1809
957
  },
958
+ {
959
+ check: "snapshot-nested",
960
+ run: async () => {
961
+ const driver = factory();
962
+ await driver.open(CONFORMANCE_SCHEMA);
963
+ const original = {
964
+ id: "u3",
965
+ name: "Nested",
966
+ age: 20,
967
+ meta: { tags: ["a"] }
968
+ };
969
+ await driver.write("users", "u3", original);
970
+ const rollback = await driver.snapshot();
971
+ const before = await driver.read("users", "u3");
972
+ if (isRecord(before) && isRecord(before.meta) && Array.isArray(before.meta.tags)) before.meta.tags.push("mutated-before-restore");
973
+ await driver.write("users", "u3", {
974
+ id: "u3",
975
+ name: "Nested",
976
+ age: 20,
977
+ meta: { tags: ["a", "mutated-after-write"] }
978
+ });
979
+ await rollback();
980
+ const restored = await driver.read("users", "u3");
981
+ await driver.close();
982
+ 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", {
983
+ table: "users",
984
+ expected: original,
985
+ actual: restored
986
+ });
987
+ }
988
+ },
1810
989
  {
1811
990
  check: "non-id-primary",
1812
991
  run: async () => {
@@ -1855,21 +1034,22 @@ async function* driverFindings(factory) {
1855
1034
  run: async () => {
1856
1035
  const driver = factory();
1857
1036
  if (driver.migrate === void 0) return void 0;
1858
- await driver.open(CONFORMANCE_SCHEMA);
1037
+ const deployedUsers = {
1038
+ ...CONFORMANCE_USERS_SCHEMA,
1039
+ columns: [...CONFORMANCE_USERS_SCHEMA.columns, {
1040
+ name: "legacy",
1041
+ type: "boolean",
1042
+ nullable: true
1043
+ }]
1044
+ };
1045
+ await driver.open([deployedUsers, CONFORMANCE_POSTS_SCHEMA]);
1859
1046
  await driver.write("users", "u1", {
1860
1047
  id: "u1",
1861
1048
  name: "Ada",
1862
1049
  age: 30,
1863
1050
  legacy: true
1864
1051
  });
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]);
1052
+ const removePlan = planMigration([deployedUsers], [CONFORMANCE_USERS_SCHEMA]);
1873
1053
  await driver.migrate(removePlan);
1874
1054
  const migrated = await driver.read("users", "u1");
1875
1055
  if (migrated === void 0 || "legacy" in migrated) {
@@ -2132,173 +1312,6 @@ async function auditDriver(factory) {
2132
1312
  return findings;
2133
1313
  }
2134
1314
  //#endregion
2135
- //#region node_modules/@orkestrel/emitter/dist/src/core/index.js
2136
- /**
2137
- * Extract the own enumerable keys of a mapped object, typed as its key union.
2138
- *
2139
- * @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)[]`.
2148
- *
2149
- * @example
2150
- * ```ts
2151
- * import { extractKeys } from '@src/core'
2152
- *
2153
- * const hooks = { tick: () => {}, done: () => {} }
2154
- * extractKeys(hooks) // ['tick', 'done']
2155
- * extractKeys({}) // []
2156
- * ```
2157
- */
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";
2175
- }
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
- //#endregion
2302
1315
  //#region src/core/Cursor.ts
2303
1316
  /**
2304
1317
  * A forward row cursor for bulk in-place mutation.
@@ -2669,6 +1682,21 @@ var Table = class {
2669
1682
  for (const row of source) if (this.#guard(row)) rows.push(row);
2670
1683
  return rows;
2671
1684
  }
1685
+ /**
1686
+ * Count rows matching `criteria`'s conditions.
1687
+ *
1688
+ * @remarks
1689
+ * Unlike {@link records}, which narrows every row through the table's
1690
+ * contract guard before returning it, `count` operates on STORED rows
1691
+ * WITHOUT that guard (both the native `driver.count` hook and the
1692
+ * `filterRows`-over-`#collect()` fallback count raw storage) — so it can
1693
+ * exceed `(await records(criteria)).length` when storage holds rows that
1694
+ * no longer conform to the table's contract (legacy or migrated data).
1695
+ *
1696
+ * @param criteria - Optional conditions to filter by (paging is ignored)
1697
+ * @param options - `{ signal }` to abort
1698
+ * @returns The count of matching stored rows
1699
+ */
2672
1700
  async count(criteria, options) {
2673
1701
  checkAbort(options?.signal);
2674
1702
  await this.#ready();
@@ -2677,6 +1705,23 @@ var Table = class {
2677
1705
  if (native !== void 0) return native;
2678
1706
  return filterRows(await this.#collect(), criteria?.conditions ?? []).length;
2679
1707
  }
1708
+ /**
1709
+ * Compute an aggregate over `column` across rows matching `criteria`'s
1710
+ * conditions.
1711
+ *
1712
+ * @remarks
1713
+ * Like {@link count}, `aggregate` operates on STORED rows WITHOUT the
1714
+ * contract guard {@link records} / {@link scan} apply — a non-conforming
1715
+ * stored row still contributes to the computed aggregate when it matches
1716
+ * the conditions, even though it would never appear in `records()`'s
1717
+ * output.
1718
+ *
1719
+ * @param operation - The aggregate to compute
1720
+ * @param column - The column to aggregate
1721
+ * @param criteria - Optional conditions to filter by (paging is ignored)
1722
+ * @param options - `{ signal }` to abort
1723
+ * @returns The aggregate value, or `undefined` when undefined for the inputs
1724
+ */
2680
1725
  async aggregate(operation, column, criteria, options) {
2681
1726
  checkAbort(options?.signal);
2682
1727
  await this.#ready();
@@ -2699,7 +1744,7 @@ var Table = class {
2699
1744
  *
2700
1745
  * @param criteria - Optional conditions plus offset/limit paging
2701
1746
  * @param options - `{ signal }` to abort mid-stream
2702
- * @returns An async generator of matching, guard-conforming rows
1747
+ * @returns An async iterable of matching, guard-conforming rows
2703
1748
  */
2704
1749
  async *scan(criteria, options) {
2705
1750
  checkAbort(options?.signal);
@@ -3118,8 +2163,12 @@ var Database = class Database {
3118
2163
  * @remarks
3119
2164
  * The in-between made concrete: it runs identically in a browser or on a server,
3120
2165
  * 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`
2166
+ * the database API without a persistent backend. Rows are DEEP-copied (via
2167
+ * `structuredClone`) in and out at `write`, `read`, `scan`, `stream`, and both
2168
+ * snapshot capture and restore — so a caller mutating a nested field of an input
2169
+ * row, a returned row, or a row mutated in place between snapshot and rollback
2170
+ * can never perturb stored state (AGENTS §11); a shallow `{ ...row }` spread
2171
+ * would still share nested object/array references. `snapshot`
3123
2172
  * clones every table to give transactions an exact rollback point. `scan` and
3124
2173
  * `keys` yield in key order — sorted by the core {@link compareValues} total
3125
2174
  * order, the same contract the SQLite (`ORDER BY`) and IndexedDB (key-ordered
@@ -3136,10 +2185,10 @@ var MemoryDriver = class {
3136
2185
  async close() {}
3137
2186
  async read(table, key) {
3138
2187
  const row = this.#store(table).get(key);
3139
- return row === void 0 ? void 0 : { ...row };
2188
+ return row === void 0 ? void 0 : structuredClone(row);
3140
2189
  }
3141
2190
  async write(table, key, row) {
3142
- this.#store(table).set(key, { ...row });
2191
+ this.#store(table).set(key, structuredClone(row));
3143
2192
  }
3144
2193
  async delete(table, key) {
3145
2194
  return this.#store(table).delete(key);
@@ -3151,7 +2200,7 @@ var MemoryDriver = class {
3151
2200
  const store = this.#store(table);
3152
2201
  for (const key of this.#ordered(table)) {
3153
2202
  const row = store.get(key);
3154
- if (row !== void 0) yield { ...row };
2203
+ if (row !== void 0) yield structuredClone(row);
3155
2204
  }
3156
2205
  }
3157
2206
  /**
@@ -3195,7 +2244,7 @@ var MemoryDriver = class {
3195
2244
  skipped += 1;
3196
2245
  continue;
3197
2246
  }
3198
- yield { ...row };
2247
+ yield structuredClone(row);
3199
2248
  yielded += 1;
3200
2249
  }
3201
2250
  }
@@ -3219,12 +2268,16 @@ var MemoryDriver = class {
3219
2268
  const copy = /* @__PURE__ */ new Map();
3220
2269
  for (const [name, store] of this.#tables) {
3221
2270
  const cloned = /* @__PURE__ */ new Map();
3222
- for (const [key, row] of store) cloned.set(key, { ...row });
2271
+ for (const [key, row] of store) cloned.set(key, structuredClone(row));
3223
2272
  copy.set(name, cloned);
3224
2273
  }
3225
2274
  return async () => {
3226
2275
  this.#tables.clear();
3227
- for (const [name, store] of copy) this.#tables.set(name, store);
2276
+ for (const [name, store] of copy) {
2277
+ const restored = /* @__PURE__ */ new Map();
2278
+ for (const [key, row] of store) restored.set(key, structuredClone(row));
2279
+ this.#tables.set(name, restored);
2280
+ }
3228
2281
  };
3229
2282
  }
3230
2283
  const copy = /* @__PURE__ */ new Map();
@@ -3232,11 +2285,15 @@ var MemoryDriver = class {
3232
2285
  const store = this.#tables.get(name);
3233
2286
  if (store === void 0) continue;
3234
2287
  const cloned = /* @__PURE__ */ new Map();
3235
- for (const [key, row] of store) cloned.set(key, { ...row });
2288
+ for (const [key, row] of store) cloned.set(key, structuredClone(row));
3236
2289
  copy.set(name, cloned);
3237
2290
  }
3238
2291
  return async () => {
3239
- for (const [name, store] of copy) this.#tables.set(name, store);
2292
+ for (const [name, store] of copy) {
2293
+ const restored = /* @__PURE__ */ new Map();
2294
+ for (const [key, row] of store) restored.set(key, structuredClone(row));
2295
+ this.#tables.set(name, restored);
2296
+ }
3240
2297
  };
3241
2298
  }
3242
2299
  /**
@@ -3359,6 +2416,6 @@ function createMemoryDriver() {
3359
2416
  return new MemoryDriver();
3360
2417
  }
3361
2418
  //#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 };
2419
+ 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, isDriverMeta, likeMatch, matchesCondition, matchesCriteria, migrateRows, planMigration, shapeToColumnType, sortRows, wildcardMatch };
3363
2420
 
3364
2421
  //# sourceMappingURL=index.js.map