@bjornpagen/bumbledb 0.4.0 → 0.6.0

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.
Files changed (103) hide show
  1. package/COOKBOOK.md +191 -92
  2. package/README.md +9 -7
  3. package/dist/closed.d.ts +30 -2
  4. package/dist/closed.d.ts.map +1 -1
  5. package/dist/closed.js +66 -20
  6. package/dist/closed.js.map +1 -1
  7. package/dist/db.d.ts +33 -6
  8. package/dist/db.d.ts.map +1 -1
  9. package/dist/db.js +89 -72
  10. package/dist/db.js.map +1 -1
  11. package/dist/exhume.d.ts.map +1 -1
  12. package/dist/exhume.js +1 -14
  13. package/dist/exhume.js.map +1 -1
  14. package/dist/face.d.ts +1 -1
  15. package/dist/face.d.ts.map +1 -1
  16. package/dist/face.js +2 -1
  17. package/dist/face.js.map +1 -1
  18. package/dist/fields.d.ts +18 -1
  19. package/dist/fields.d.ts.map +1 -1
  20. package/dist/fields.js +44 -16
  21. package/dist/fields.js.map +1 -1
  22. package/dist/index.d.ts +18 -10
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +13 -6
  25. package/dist/index.js.map +1 -1
  26. package/dist/law.d.ts +2 -1
  27. package/dist/law.d.ts.map +1 -1
  28. package/dist/law.js +15 -14
  29. package/dist/law.js.map +1 -1
  30. package/dist/lower.d.ts.map +1 -1
  31. package/dist/lower.js +1 -7
  32. package/dist/lower.js.map +1 -1
  33. package/dist/marshal.d.ts.map +1 -1
  34. package/dist/marshal.js +15 -27
  35. package/dist/marshal.js.map +1 -1
  36. package/dist/native.d.ts +21 -2
  37. package/dist/native.d.ts.map +1 -1
  38. package/dist/native.js +20 -3
  39. package/dist/native.js.map +1 -1
  40. package/dist/order.d.ts +36 -0
  41. package/dist/order.d.ts.map +1 -0
  42. package/dist/order.js +135 -0
  43. package/dist/order.js.map +1 -0
  44. package/dist/query/atom.d.ts +140 -206
  45. package/dist/query/atom.d.ts.map +1 -1
  46. package/dist/query/atom.js +33 -52
  47. package/dist/query/atom.js.map +1 -1
  48. package/dist/query/find.d.ts +116 -0
  49. package/dist/query/find.d.ts.map +1 -0
  50. package/dist/query/{select.js → find.js} +22 -22
  51. package/dist/query/find.js.map +1 -0
  52. package/dist/query/lower.d.ts +124 -162
  53. package/dist/query/lower.d.ts.map +1 -1
  54. package/dist/query/lower.js +462 -507
  55. package/dist/query/lower.js.map +1 -1
  56. package/dist/query/predicate.d.ts +22 -14
  57. package/dist/query/predicate.d.ts.map +1 -1
  58. package/dist/query/predicate.js +45 -20
  59. package/dist/query/predicate.js.map +1 -1
  60. package/dist/query/run.d.ts +3 -3
  61. package/dist/query/run.d.ts.map +1 -1
  62. package/dist/query/run.js +10 -11
  63. package/dist/query/run.js.map +1 -1
  64. package/dist/query/scope.d.ts +124 -83
  65. package/dist/query/scope.d.ts.map +1 -1
  66. package/dist/query/scope.js +77 -70
  67. package/dist/query/scope.js.map +1 -1
  68. package/dist/relation.d.ts +9 -22
  69. package/dist/relation.d.ts.map +1 -1
  70. package/dist/relation.js +9 -28
  71. package/dist/relation.js.map +1 -1
  72. package/dist/schema.d.ts.map +1 -1
  73. package/dist/schema.js +7 -31
  74. package/dist/schema.js.map +1 -1
  75. package/dist/statements.d.ts +5 -2
  76. package/dist/statements.d.ts.map +1 -1
  77. package/dist/statements.js +30 -35
  78. package/dist/statements.js.map +1 -1
  79. package/package.json +2 -5
  80. package/src/closed.ts +73 -28
  81. package/src/db.ts +126 -88
  82. package/src/exhume.ts +1 -15
  83. package/src/face.ts +4 -3
  84. package/src/fields.ts +58 -16
  85. package/src/index.ts +19 -13
  86. package/src/law.ts +15 -14
  87. package/src/lower.ts +2 -9
  88. package/src/marshal.ts +15 -31
  89. package/src/native.ts +22 -4
  90. package/src/order.ts +156 -0
  91. package/src/query/atom.ts +186 -276
  92. package/src/query/find.ts +212 -0
  93. package/src/query/lower.ts +614 -749
  94. package/src/query/predicate.ts +47 -20
  95. package/src/query/run.ts +11 -13
  96. package/src/query/scope.ts +165 -132
  97. package/src/relation.ts +9 -51
  98. package/src/schema.ts +7 -33
  99. package/src/statements.ts +33 -38
  100. package/dist/query/select.d.ts +0 -128
  101. package/dist/query/select.d.ts.map +0 -1
  102. package/dist/query/select.js.map +0 -1
  103. package/src/query/select.ts +0 -215
package/src/closed.ts CHANGED
@@ -39,6 +39,7 @@ import {
39
39
  type Infer,
40
40
  literalOf
41
41
  } from "#fields.ts"
42
+ import type { AnyRelation, RelationField } from "#relation.ts"
42
43
  import { resolveSelection, type SelectionBinding, type SelectionInput } from "#relation.ts"
43
44
  import type { LiteralSpec } from "#spec.ts"
44
45
 
@@ -53,7 +54,7 @@ type PayloadField = Exclude<AnyField, { readonly fresh: true }>
53
54
  * unspellable — the sealed shape mints the synthetic `id` itself (ordinal
54
55
  * 0 of the matchable fields), so a declared column named `id` would be
55
56
  * shadowed by the synthetic slot everywhere the shape resolves by name
56
- * (`matchFieldsOf`, the projected face, `spec.rs`'s resolver). The wall is
57
+ * (`sealedFieldsOf`, the projected face, `spec.rs`'s resolver). The wall is
57
58
  * typed here and judged again at construction in {@link mintClosed} — the
58
59
  * runtime twin for untyped callers, warmer and earlier than the engine's
59
60
  * `DuplicateFieldName` at `Db.create`.
@@ -193,6 +194,48 @@ interface AnyClosed {
193
194
  readonly columns: Readonly<Record<string, PayloadField>>
194
195
  }
195
196
 
197
+ /**
198
+ * THE relation-kind discriminant — the ONE spelling of "is this schema
199
+ * member closed?" (the type tier's twin is the `AnyClosed` conditional
200
+ * arms). A closed relation's runtime description carries its handle
201
+ * roster; an ordinary relation's never does. Every runtime closed/ordinary
202
+ * fork in the SDK judges through this predicate — never a re-spelled
203
+ * structural probe.
204
+ */
205
+ function isClosedMember(member: AnyRelation | AnyClosed): member is AnyClosed {
206
+ return "handles" in member.data
207
+ }
208
+
209
+ /**
210
+ * The SEALED field list of a schema member — THE one reader of "what
211
+ * fields does this owner expose": an ordinary relation's declared fields;
212
+ * a closed relation's sealed shape — the synthetic `id` (the value's own
213
+ * roster-carrying descriptor, by identity) at ordinal 0, then the declared
214
+ * payload columns at declared index + 1 (the sealed shift, mirroring the
215
+ * engine's `SchemaDescriptor::sealed_fields`). A `ClosedColumn` is
216
+ * structurally a `RelationField`, so both kinds read uniformly.
217
+ */
218
+ function sealedFieldsOf(member: AnyRelation | AnyClosed): readonly RelationField[] {
219
+ if (isClosedMember(member)) {
220
+ return Object.freeze([Object.freeze({ name: "id", field: member.id }), ...member.data.columns])
221
+ }
222
+ return member.data.fields
223
+ }
224
+
225
+ /**
226
+ * One sealed field by name — derived from {@link sealedFieldsOf}, so the
227
+ * closed synthetic `id` resolves everywhere a name is looked up (no reader
228
+ * can silently lack the `id` arm). `undefined` when the name is foreign
229
+ * (the type tiers make that unwritable; the engine re-judges at
230
+ * `Db.create`).
231
+ */
232
+ function sealedFieldOf(member: AnyRelation | AnyClosed, fieldName: string): AnyField | undefined {
233
+ const declared = sealedFieldsOf(member).find(function byName(candidate) {
234
+ return candidate.name === fieldName
235
+ })
236
+ return declared?.field
237
+ }
238
+
196
239
  /** Narrows the two-tier second argument: a handle tuple (bare tier) or a column block (payload tier). */
197
240
  function isHandleTuple(
198
241
  shape: readonly [string, ...string[]] | Record<string, PayloadField>
@@ -219,7 +262,8 @@ function handleKeysOwn<Handles extends string>(
219
262
  * The trusted seam of the axiom-readback mint: every handle carries an own
220
263
  * frozen row and every row carries every declared column as an own
221
264
  * property — verified before the record is admitted as the typed
222
- * {@link Axioms} (the `refsComplete` analog of `relation()`).
265
+ * {@link Axioms} (the trusted-admission-seam pattern its home is
266
+ * `isTypedScope` in query/lower.ts).
223
267
  */
224
268
  function axiomsMinted<Handles extends string, Cols extends Record<string, PayloadField>>(
225
269
  record: Readonly<Record<string, object>>,
@@ -237,38 +281,22 @@ function axiomsMinted<Handles extends string, Cols extends Record<string, Payloa
237
281
  })
238
282
  }
239
283
 
240
- /**
241
- * Reads one handle's ground axiom row for lowering. The typed payload
242
- * surface makes absence unrepresentable ({@link Axioms} carries every
243
- * handle's row); the refusal below guards the one ill-typed path — payload
244
- * columns with the bare tier's absent axioms — which no public spelling
245
- * reaches.
246
- */
247
- function groundRow<Handles extends string, Cols extends Record<string, PayloadField>>(
248
- name: string,
249
- axioms: Axioms<Handles, Cols> | undefined,
250
- handle: Handles
251
- ): Readonly<Record<string, unknown>> {
252
- if (axioms === undefined) {
253
- throw errors.new(`closed relation ${name}: payload columns declared without ground axioms`)
254
- }
255
- return axioms[handle]
256
- }
257
-
258
284
  /**
259
285
  * Mints the axiom-readback record: one own frozen row per handle (the bare
260
286
  * tier's rows are empty — it declares no columns), each row a fresh copy of
261
- * its ground axiom.
287
+ * its ground axiom. Both tiers supply a REAL axioms record ({@link
288
+ * closedBare} mints its empty rows), so the columns-without-axioms state is
289
+ * unrepresentable here — no undefined arm exists to guard.
262
290
  */
263
291
  function mintAxioms<Handles extends string, Cols extends Record<string, PayloadField>>(
264
292
  name: string,
265
293
  handles: readonly Handles[],
266
294
  cols: readonly ClosedColumn[],
267
- axioms: Axioms<Handles, Cols> | undefined
295
+ axioms: Axioms<Handles, Cols>
268
296
  ): Axioms<Handles, Cols> {
269
297
  const out: Record<string, object> = {}
270
298
  for (const handle of handles) {
271
- const row = axioms === undefined ? Object.freeze({}) : Object.freeze({ ...groundRow(name, axioms, handle) })
299
+ const row = Object.freeze({ ...axioms[handle] })
272
300
  Object.defineProperty(out, handle, { value: row, enumerable: true })
273
301
  }
274
302
  Object.freeze(out)
@@ -321,12 +349,29 @@ function closed<const Name extends string, const Cols extends PayloadColumns, Ha
321
349
  return closedPayload(name, shape, axioms)
322
350
  }
323
351
 
324
- /** The bare tier's precisely-typed builder: no columns, no axioms. */
352
+ /**
353
+ * The bare tier's precisely-typed builder: no columns, and the axioms
354
+ * record is the EMPTY-ROW record over the handle roster (one own frozen
355
+ * `{}` per handle, `__proto__`-safe own-property definition) — the same
356
+ * representation the payload tier carries, so `mintClosed` never sees a
357
+ * tier fork and the columns-without-axioms state stops being spellable.
358
+ */
325
359
  function closedBare<Name extends string, Handles extends string>(
326
360
  name: Name,
327
361
  handles: readonly [Handles, ...Handles[]]
328
362
  ): Closed<Name, Handles, Record<never, never>> {
329
- return mintClosed<Name, Handles, Record<never, never>>(name, handles, {}, undefined)
363
+ const empty: Record<string, object> = {}
364
+ for (const handle of handles) {
365
+ /** A duplicated name mints one row; the roster's own duplicate refusal in {@link mintClosed} stays the judge. */
366
+ if (!Object.hasOwn(empty, handle)) {
367
+ Object.defineProperty(empty, handle, { value: Object.freeze({}), enumerable: true })
368
+ }
369
+ }
370
+ Object.freeze(empty)
371
+ if (!axiomsMinted<Handles, Record<never, never>>(empty, handles, [])) {
372
+ throw errors.new(`closed relation ${name}: bare-tier axiom-row minting incomplete`)
373
+ }
374
+ return mintClosed<Name, Handles, Record<never, never>>(name, handles, {}, empty)
330
375
  }
331
376
 
332
377
  /**
@@ -379,7 +424,7 @@ function mintClosed<Name extends string, Handles extends string, Cols extends Re
379
424
  name: Name,
380
425
  handles: readonly Handles[],
381
426
  columns: Cols,
382
- axioms: Axioms<Handles, Cols> | undefined
427
+ axioms: Axioms<Handles, Cols>
383
428
  ): Closed<Name, Handles, Cols> {
384
429
  if (handles.length === 0) {
385
430
  throw errors.new(`closed relation ${name}: at least one handle is required (an empty vocabulary declares nothing)`)
@@ -405,8 +450,8 @@ function mintClosed<Name extends string, Handles extends string, Cols extends Re
405
450
  }
406
451
  Object.freeze(cols)
407
452
  const rows: ClosedRow[] = handleList.map(function lowerRow(handle) {
453
+ const row: Readonly<Record<string, unknown>> = axioms[handle]
408
454
  const values = cols.map(function lowerAxiomLiteral(column) {
409
- const row = groundRow(name, axioms, handle)
410
455
  return Object.freeze(literalOf(column.field, row[column.name]))
411
456
  })
412
457
  return Object.freeze({ handle, values: Object.freeze(values) })
@@ -471,4 +516,4 @@ export type {
471
516
  PayloadField,
472
517
  SelectedClosed
473
518
  }
474
- export { closed }
519
+ export { closed, isClosedMember, sealedFieldOf, sealedFieldsOf }
package/src/db.ts CHANGED
@@ -32,8 +32,10 @@
32
32
 
33
33
  import * as path from "node:path"
34
34
  import * as errors from "@superbuilders/errors"
35
+ import { isClosedMember, sealedFieldsOf } from "#closed.ts"
35
36
  import type { Exhumed } from "#exhume.ts"
36
37
  import { exhumeStore } from "#exhume.ts"
38
+ import { rosterOf } from "#fields.ts"
37
39
  import { lower } from "#lower.ts"
38
40
  import {
39
41
  factOf,
@@ -59,13 +61,13 @@ import type {
59
61
  Violation as WireViolation,
60
62
  ViolationFact as WireViolationFact
61
63
  } from "#native.ts"
62
- import { native } from "#native.ts"
63
- import type { SelectColumn } from "#query/atom.ts"
64
+ import { bridged, native } from "#native.ts"
65
+ import type { FindColumn } from "#query/atom.ts"
64
66
  import type { Query } from "#query/lower.ts"
65
67
  import { lowerQuery } from "#query/lower.ts"
66
68
  import { decodeAnswers, wireParams } from "#query/run.ts"
67
69
  import type { ParamEntry, ParamsRecord } from "#query/scope.ts"
68
- import type { AnyRelation, Fact, InsertFact, RelationField } from "#relation.ts"
70
+ import type { AnyRelation, Fact, InsertFact } from "#relation.ts"
69
71
  import type { AnySchema, Schema, SchemaRelation, SchemaRelations } from "#schema.ts"
70
72
  import type { KeyStatement, Statement } from "#statements.ts"
71
73
 
@@ -348,11 +350,16 @@ interface Db<Rels extends SchemaRelations> {
348
350
  * The ONE witnessed-write form: snapshot → `fn` (premise reads via
349
351
  * `snap`, delta via `tx`) → witnessed commit, which lands only if no
350
352
  * state-changing commit intervened since the snapshot. On a moved
351
- * generation the WHOLE `fn` reruns on a fresh snapshot, unbounded: this
352
- * process holds the store's only write handle, so every generation move
353
- * is self-inflicted by the host's own interleaved writes contention
354
- * is bounded by the host's own progress, and any retry cap would be an
355
- * invented limit (the house no-limits law). `fn` may decline to commit
353
+ * generation the WHOLE `fn` reruns on a fresh snapshot: this process
354
+ * holds the store's only write handle, so every generation move is
355
+ * self-inflicted by the host's own interleaved writes, and each rerun
356
+ * witnesses a strictly newer generation the benign race converges.
357
+ * The loop's honesty bound is {@link WITNESSED_ATTEMPT_CAP}: a callback
358
+ * that moves the generation on EVERY attempt (a plain `db.write` before
359
+ * its first tx verb) would spin forever, so past the cap the typed
360
+ * {@link ErrWitnessedLivelock} is thrown instead of a silent loop (the
361
+ * engine's ruling: the error, never a loop — retry is host policy, and
362
+ * the cap is that policy's own diagnostic). `fn` may decline to commit
356
363
  * by returning {@link abandon}`(payload)` — the outcome is then
357
364
  * `{ ok: false, abandoned: payload }` and NO commit (not even an empty
358
365
  * one) is issued.
@@ -368,19 +375,6 @@ interface Db<Rels extends SchemaRelations> {
368
375
  prepare<Row, Params extends ParamsRecord>(q: Query<Rels, Row, Params>): Prepared<Rels, Row, Params>
369
376
  }
370
377
 
371
- /**
372
- * The bridge guard: runs one native call and wraps anything it throws —
373
- * marshal-shape refusals and handle-lifecycle refusals cross as genuine
374
- * typed failures, never bare foreign errors.
375
- */
376
- function bridged<T>(context: string, run: () => T): T {
377
- const result = errors.trySync(run)
378
- if (result.error) {
379
- throw errors.wrap(result.error, context)
380
- }
381
- return result.data
382
- }
383
-
384
378
  /** One relation's runtime tables: engine id, the identical schema member, field ids, primary key. */
385
379
  interface RelationEntry {
386
380
  readonly id: number
@@ -443,7 +437,7 @@ function materializedEntries(theory: AnySchema): StatementEntry[] {
443
437
  function impliedKeyEntries(theory: AnySchema): StatementEntry[] {
444
438
  const entries: StatementEntry[] = []
445
439
  for (const member of Object.values(theory.relations)) {
446
- if ("handles" in member.data) {
440
+ if (isClosedMember(member)) {
447
441
  continue
448
442
  }
449
443
  for (const declared of member.data.fields) {
@@ -457,7 +451,7 @@ function impliedKeyEntries(theory: AnySchema): StatementEntry[] {
457
451
  }
458
452
  }
459
453
  for (const member of Object.values(theory.relations)) {
460
- if ("handles" in member.data) {
454
+ if (isClosedMember(member)) {
461
455
  entries.push({
462
456
  kind: "functionality",
463
457
  statement: undefined,
@@ -527,6 +521,33 @@ function isStatementValue<R extends AnyRelation, P extends readonly string[]>(
527
521
  return typeof data === "object" && data !== null && "kind" in data
528
522
  }
529
523
 
524
+ /**
525
+ * THE one selector dispatch of the `get` overload pair (primary-key vs
526
+ * key-statement, `docs/architecture/70-api.md` § the freeze): judges the
527
+ * middle argument once and hands the narrowed pieces to the chosen
528
+ * continuation. `Db.get` and the read scope's `get` both dispatch through
529
+ * here, so the two mismatch refusals speak with one voice and the symmetry
530
+ * rule (`db.get(...) === db.read(snap => snap.get(...))`) holds by
531
+ * construction.
532
+ */
533
+ function selectKeyRead<R extends AnyRelation, P extends readonly string[], T>(
534
+ keyOrStatement: KeyFact<R> | KeyStatement<R, P>,
535
+ declaredKey: DeclaredKeyFact<R, P> | undefined,
536
+ byStatement: (statement: KeyStatement<R, P>, key: DeclaredKeyFact<R, P>) => T,
537
+ byPrimary: (key: KeyFact<R>) => T
538
+ ): T {
539
+ if (declaredKey !== undefined) {
540
+ if (!isStatementValue(keyOrStatement)) {
541
+ throw errors.new("keyed get takes a key() statement value as its second argument")
542
+ }
543
+ return byStatement(keyOrStatement, declaredKey)
544
+ }
545
+ if (isStatementValue(keyOrStatement)) {
546
+ throw errors.new("keyed get with a statement selector also takes the key object — get(relation, keyStatement, key)")
547
+ }
548
+ return byPrimary(keyOrStatement)
549
+ }
550
+
530
551
  /** Maps a slot's reversal flag to the violation's `orientation` payload. */
531
552
  function orientationOf(reversed: boolean | undefined): "written" | "mirrored" | undefined {
532
553
  if (reversed === undefined) {
@@ -538,22 +559,6 @@ function orientationOf(reversed: boolean | undefined): "written" | "mirrored" |
538
559
  return "written"
539
560
  }
540
561
 
541
- /**
542
- * The declared field descriptors a violation's offending cells decode
543
- * through: an ordinary relation's declared fields; a closed relation's
544
- * SEALED shape — the roster-carrying synthetic `id` (the member's own
545
- * reference descriptor) plus its payload columns (a `ClosedColumn` is
546
- * structurally a `RelationField`). This is how {@link openDb}'s
547
- * `offendingFactOf` reaches the open-time descriptors: the cited relation
548
- * name resolves to the schema member, and the member carries them.
549
- */
550
- function declaredFieldsOf(member: SchemaRelation): readonly RelationField[] {
551
- if ("axioms" in member) {
552
- return [{ name: "id", field: member.id }, ...member.data.columns]
553
- }
554
- return member.data.fields
555
- }
556
-
557
562
  /** The id-resolution tables one open builds: relation entries by name, statement slots by id. */
558
563
  interface Tables {
559
564
  readonly relations: ReadonlyMap<string, RelationEntry>
@@ -638,7 +643,7 @@ interface PreparedPlan {
638
643
  readonly handle: PreparedHandle
639
644
  readonly owner: object
640
645
  readonly params: readonly ParamEntry[]
641
- readonly select: readonly SelectColumn[]
646
+ readonly finds: readonly FindColumn[]
642
647
  }
643
648
 
644
649
  /** The private engine halves of this module's prepared values. */
@@ -667,6 +672,32 @@ const planReclaimer = new FinalizationRegistry<PreparedHandle>(function reclaimP
667
672
  */
668
673
  const generationMovedSignal = errors.new("bumbledb witnessed generation moved")
669
674
 
675
+ /**
676
+ * The witnessed loop's attempt cap — a generous power of two. Benign
677
+ * self-inflicted contention (the host's own commits landing between an
678
+ * attempt's snapshot and its witnessed begin) converges in a handful of
679
+ * retries because each rerun reads a FRESHER snapshot; a workload that moves
680
+ * the generation on EVERY one of this many consecutive attempts is not
681
+ * converging and never will (see {@link ErrWitnessedLivelock}).
682
+ */
683
+ const WITNESSED_ATTEMPT_CAP = 64
684
+
685
+ /**
686
+ * The typed livelock refusal `writeWitnessed` throws past
687
+ * {@link WITNESSED_ATTEMPT_CAP} attempts: every attempt found the generation
688
+ * moved, which is only sustainable when the callback ITSELF (even
689
+ * indirectly) issues an interleaved plain `db.write` before its first tx
690
+ * verb on every attempt — each rerun then re-moves the generation it is
691
+ * about to witness, forever. That is host-policy pathology, not engine
692
+ * judgment (the engine ships the error, never a loop), so it THROWS rather
693
+ * than returning a result arm. Match with `errors.is`; the remedy is to
694
+ * move the interleaved write out of the callback (or make it first-attempt
695
+ * only — the delta belongs on `tx`, premise reads on `snap`).
696
+ */
697
+ const ErrWitnessedLivelock = errors.new(
698
+ "bumbledb writeWitnessed livelock: the generation moved on every attempt — the callback itself commits an interleaved write each try, so no snapshot can ever stay current"
699
+ )
700
+
670
701
  /**
671
702
  * Fills one insert's omitted fresh cells through the engine's
672
703
  * alloc-then-insert dyn lane (there is no insert-with-omitted-fields wire
@@ -725,7 +756,7 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
725
756
  if (entry === undefined || entry.member !== relation) {
726
757
  throw errors.new(`relation ${relation.name} is not a member of schema ${theory.name}`)
727
758
  }
728
- if ("handles" in relation.data) {
759
+ if (isClosedMember(relation)) {
729
760
  throw errors.new(
730
761
  `relation ${relation.name} is closed — its extension is schema data (axioms), never scanned or written`
731
762
  )
@@ -738,15 +769,16 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
738
769
  if (entry === undefined || !isMemberName(fact.relation)) {
739
770
  throw errors.new(`bumbledb violation cites unknown relation ${fact.relation}`)
740
771
  }
741
- const declared = declaredFieldsOf(entry.member)
772
+ const declared = sealedFieldsOf(entry.member)
742
773
  const decoded: Record<string, FactValue> = {}
743
774
  for (const cell of fact.fields) {
744
775
  const cited = declared.find(function byName(candidate) {
745
776
  return candidate.name === cell.name
746
777
  })
778
+ const roster = rosterOf(cited?.field)
747
779
  decoded[cell.name] =
748
- cited !== undefined && "closed" in cited.field
749
- ? handleOf(`violation fact ${fact.relation} field ${cell.name}`, cited.field.closed, cell.value)
780
+ roster !== undefined
781
+ ? handleOf(`violation fact ${fact.relation} field ${cell.name}`, roster, cell.value)
750
782
  : cell.value
751
783
  }
752
784
  return Object.freeze({ relation: fact.relation, fact: Object.freeze(decoded) })
@@ -828,25 +860,22 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
828
860
  ): Fact<R> | undefined {
829
861
  assertLive()
830
862
  const entry = resolveOrdinary(relation)
831
- if (declaredKey !== undefined) {
832
- if (!isStatementValue(keyOrStatement)) {
833
- throw errors.new("keyed get takes a key() statement value as its second argument")
863
+ return selectKeyRead(
864
+ keyOrStatement,
865
+ declaredKey,
866
+ function byStatement(statement, key) {
867
+ return readThroughKey(relation, entry, declaredKeyOf(relation, statement), recordOf(key))
868
+ },
869
+ function byPrimary(key) {
870
+ const primaryKey = entry.primaryKey
871
+ if (primaryKey === undefined) {
872
+ throw errors.new(
873
+ `relation ${relation.name} has no candidate key — keyed get requires a fresh field or a declared key statement`
874
+ )
875
+ }
876
+ return readThroughKey(relation, entry, primaryKey, recordOf(key))
834
877
  }
835
- const selected = declaredKeyOf(relation, keyOrStatement)
836
- return readThroughKey(relation, entry, selected, recordOf(declaredKey))
837
- }
838
- if (isStatementValue(keyOrStatement)) {
839
- throw errors.new(
840
- "keyed get with a statement selector also takes the key object — get(relation, keyStatement, key)"
841
- )
842
- }
843
- const primaryKey = entry.primaryKey
844
- if (primaryKey === undefined) {
845
- throw errors.new(
846
- `relation ${relation.name} has no candidate key — keyed get requires a fresh field or a declared key statement`
847
- )
848
- }
849
- return readThroughKey(relation, entry, primaryKey, recordOf(keyOrStatement))
878
+ )
850
879
  }
851
880
  return { contains, get }
852
881
  }
@@ -909,7 +938,7 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
909
938
  const rows = bridged("execute bumbledb prepared query", function callExecute() {
910
939
  return native.preparedExecute(plan.handle, state.handle, wire)
911
940
  })
912
- return decodeAnswers<Row>(plan.select, rows)
941
+ return decodeAnswers<Row>(plan.finds, rows)
913
942
  }
914
943
  const scope: ReadScope<Rels> = Object.freeze({
915
944
  generation,
@@ -1003,18 +1032,16 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1003
1032
  declaredKey?: DeclaredKeyFact<R, P>
1004
1033
  ): Fact<R> | undefined {
1005
1034
  return read(function getInScope(snap) {
1006
- if (declaredKey !== undefined) {
1007
- if (!isStatementValue(keyOrStatement)) {
1008
- throw errors.new("keyed get takes a key() statement value as its second argument")
1035
+ return selectKeyRead(
1036
+ keyOrStatement,
1037
+ declaredKey,
1038
+ function byStatement(statement, key) {
1039
+ return snap.get(relation, statement, key)
1040
+ },
1041
+ function byPrimary(key) {
1042
+ return snap.get(relation, key)
1009
1043
  }
1010
- return snap.get(relation, keyOrStatement, declaredKey)
1011
- }
1012
- if (isStatementValue(keyOrStatement)) {
1013
- throw errors.new(
1014
- "keyed get with a statement selector also takes the key object — get(relation, keyStatement, key)"
1015
- )
1016
- }
1017
- return snap.get(relation, keyOrStatement)
1044
+ )
1018
1045
  })
1019
1046
  }
1020
1047
 
@@ -1298,13 +1325,32 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1298
1325
  return commitWitnessed(state, txHandle)
1299
1326
  }
1300
1327
 
1328
+ /**
1329
+ * The witnessed retry loop. What it retries: the benign race — the
1330
+ * host's OWN interleaved commit landing between an attempt's snapshot
1331
+ * and its witnessed begin (every writer shares this handle, so a move
1332
+ * is always self-inflicted) — by rerunning the WHOLE callback on a
1333
+ * fresh snapshot, which converges because each rerun witnesses a
1334
+ * strictly newer generation. What it refuses: the pathology where the
1335
+ * callback itself (even indirectly) issues a plain `db.write` before
1336
+ * its first tx verb, moving the generation on EVERY attempt — an
1337
+ * unbounded loop would spin forever with no diagnostic, so past
1338
+ * {@link WITNESSED_ATTEMPT_CAP} attempts the loop throws the typed
1339
+ * {@link ErrWitnessedLivelock} instead (the engine's ruling: the
1340
+ * error, never a loop — retry is host policy, and this is the host
1341
+ * policy's own honesty bound).
1342
+ */
1301
1343
  function writeWitnessed<R>(fn: (snap: ReadScope<Rels>, tx: Tx<Rels>) => R): WitnessedWriteResult<Rels, R> {
1302
- for (;;) {
1344
+ for (let attempts = 0; attempts < WITNESSED_ATTEMPT_CAP; attempts += 1) {
1303
1345
  const attempt = witnessedAttempt(fn)
1304
1346
  if (attempt !== undefined) {
1305
1347
  return attempt
1306
1348
  }
1307
1349
  }
1350
+ throw errors.wrap(
1351
+ ErrWitnessedLivelock,
1352
+ `writeWitnessed livelock: the generation moved on all ${WITNESSED_ATTEMPT_CAP} attempts against schema ${theory.name}`
1353
+ )
1308
1354
  }
1309
1355
 
1310
1356
  /**
@@ -1326,17 +1372,9 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1326
1372
  if (member === undefined) {
1327
1373
  throw errors.new(`bumbledb manifest drift: schema ${theory.name} lost relation ${name}`)
1328
1374
  }
1329
- const sealed =
1330
- "handles" in member.data
1331
- ? [
1332
- "id",
1333
- ...member.data.columns.map(function columnName(column) {
1334
- return column.name
1335
- })
1336
- ]
1337
- : member.data.fields.map(function fieldName(declared) {
1338
- return declared.name
1339
- })
1375
+ const sealed = sealedFieldsOf(member).map(function fieldName(declared) {
1376
+ return declared.name
1377
+ })
1340
1378
  sealed.forEach(function verifyField(fieldName, fieldOrdinal) {
1341
1379
  if (entry.fieldIds.get(fieldName) !== fieldOrdinal) {
1342
1380
  throw errors.new(
@@ -1386,7 +1424,7 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1386
1424
  handle: preparedHandle,
1387
1425
  owner,
1388
1426
  params: q.data.params,
1389
- select: q.data.select
1427
+ finds: q.data.finds
1390
1428
  })
1391
1429
  )
1392
1430
  planReclaimer.register(prepared, preparedHandle)
@@ -1581,4 +1619,4 @@ export type {
1581
1619
  WitnessedWriteResult,
1582
1620
  WriteResult
1583
1621
  }
1584
- export { abandon, Db, ErrNewtypeMismatch }
1622
+ export { abandon, Db, ErrNewtypeMismatch, ErrWitnessedLivelock, WITNESSED_ATTEMPT_CAP }
package/src/exhume.ts CHANGED
@@ -26,7 +26,7 @@
26
26
 
27
27
  import * as errors from "@superbuilders/errors"
28
28
  import type { FactValue, Manifest } from "#native.ts"
29
- import { native } from "#native.ts"
29
+ import { bridged, native } from "#native.ts"
30
30
  import type { ValueTypeSpec } from "#spec.ts"
31
31
 
32
32
  /**
@@ -132,20 +132,6 @@ interface Exhumed {
132
132
  scan(relation: string): readonly ExhumedFact[]
133
133
  }
134
134
 
135
- /**
136
- * The bridge guard (db.ts's twin over this module's calls): runs one
137
- * native call and wraps anything it throws, so marshal-shape and
138
- * handle-lifecycle refusals cross as genuine typed failures, never bare
139
- * foreign errors.
140
- */
141
- function bridged<T>(context: string, run: () => T): T {
142
- const result = errors.trySync(run)
143
- if (result.error) {
144
- throw errors.wrap(result.error, context)
145
- }
146
- return result.data
147
- }
148
-
149
135
  /**
150
136
  * Shapes the bridge's manifest rendering of the stored descriptor into the
151
137
  * SDK's {@link ExhumedDescriptor}: same relations in the same engine-id
package/src/face.ts CHANGED
@@ -101,11 +101,11 @@ type FaceSource = AnyRelation | AnyClosed | AnySelected | AnySelectedClosed
101
101
  * names address the sealed shape).
102
102
  */
103
103
  type FaceFields<S extends FaceSource> = S extends AnySelected
104
- ? keyof S["relation"]["fields"] & string
104
+ ? keyof RelationFields<S["relation"]> & string
105
105
  : S extends AnySelectedClosed
106
106
  ? "id" | (keyof S["relation"]["columns"] & string)
107
107
  : S extends AnyRelation
108
- ? keyof S["fields"] & string
108
+ ? keyof RelationFields<S> & string
109
109
  : S extends { readonly axioms: Readonly<Record<string, infer Row>> }
110
110
  ? "id" | (keyof Row & string)
111
111
  : never
@@ -263,7 +263,8 @@ function on<S extends FaceSource>(source: S, fields: string | readonly string[])
263
263
  }
264
264
 
265
265
  /**
266
- * The trusted seam of the face mint (the `refsComplete` pattern): the
266
+ * The trusted admission seam of the face mint (the pattern's home is
267
+ * `isTypedScope` in query/lower.ts): the
267
268
  * checkable facts — the value carries exactly the source and projection it
268
269
  * was built from, and `data.owner` is exactly the owner {@link faceParts}
269
270
  * resolves for that source — are verified before the wide construction is