@bjornpagen/bumbledb 0.14.0 → 0.15.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.
package/src/db.ts CHANGED
@@ -2,18 +2,16 @@
2
2
  * `Db` — the living half of the SDK (PRD-07): open/create a store from a
3
3
  * `Schema`, write typed facts through delta transactions with race-free
4
4
  * final-state point reads, receive rejections as typed violation VALUES
5
- * keyed to statements, and read through scoped snapshots — all typed by
6
- * the schema's relations record.
5
+ * keyed to statements, and read through a synchronous instance callback —
6
+ * all typed by the schema's relations record.
7
7
  *
8
- * LIFETIMES ARE DISPOSABLES, never `close()` (ruled 2026-07-23, R12): no
9
- * value this module returns carries a close spelling — release is
10
- * deterministic and scope-shaped in the language's own syntax. Snapshots
11
- * are scope-shaped both ways: `read(fn)` opens one before `fn` and closes
12
- * it after unconditionally (the {@link ReadScope} handed to `fn` is
13
- * invalidated the moment `fn` returns), and `using snap = db.read()` hands
14
- * the caller the lifetime, released by the scope's own `Symbol.dispose`
15
- * at scope exit. Prepared plans are plain values whose engine-side half
16
- * is reclaimed by a GC finalizer — reclamation only, never correctness.
8
+ * A store read is one callback: `db.read((instance, witness) => …)`. The
9
+ * instance is invalid the moment the callback returns; the witness is a
10
+ * cloneable token and may escape. There is no handle-shaped read and no
11
+ * `using snap = db.read()`. Builder, owned instance, and witness
12
+ * implement `Symbol.dispose`. Prepared plans are plain values whose
13
+ * engine-side half is reclaimed by a GC finalizer — reclamation only,
14
+ * never correctness.
17
15
  *
18
16
  * PROCESS MODEL: one process, one exclusive-lock handle per store. The
19
17
  * `Db` value owns the LMDB environment's exclusive lock until process
@@ -24,33 +22,38 @@
24
22
  * policy.
25
23
  *
26
24
  * REJECTION IS DATA: a rejected commit is a domain outcome (it becomes the
27
- * LLM repair prompt downstream), returned as a {@link WriteResult} carrying
28
- * {@link Violation} values. Genuine failures — I/O, used-after-scope,
29
- * marshal shape, a moved generation on {@link Db.writeFrom} — throw
30
- * `@superbuilders/errors` wrapped errors instead.
25
+ * LLM repair prompt downstream), returned as a {@link WriteOutcome}
26
+ * carrying {@link Violation} values. A moved generation on
27
+ * {@link Db.writeFrom} is the `{ tag: "moved" }` arm, not an exception.
28
+ * Genuine failures — I/O, used-after-scope, spent handle, marshal shape —
29
+ * throw `@superbuilders/errors` wrapped errors instead.
31
30
  */
32
31
 
33
32
  import * as path from "node:path"
34
33
  import * as errors from "@superbuilders/errors"
35
34
  import { isClosedMember, sealedFieldsOf } from "#closed.ts"
36
- import type { Exhumed } from "#exhume.ts"
37
- import { exhumeStore } from "#exhume.ts"
38
35
  import { rosterOf } from "#fields.ts"
39
36
  import { lower } from "#lower.ts"
40
- import { factOf, handleOf, isFreshField, type KeyFact, keyRowOf, recordOf, rowOf } from "#marshal.ts"
37
+ import { cellOf, factOf, handleOf, isFreshField, type KeyFact, keyRowOf, recordOf, rowOf } from "#marshal.ts"
41
38
 
42
39
  import type {
40
+ AdmitResult,
41
+ BuilderHandle,
43
42
  DbHandle,
44
43
  FactValue,
44
+ InstanceHandle,
45
45
  Manifest,
46
+ NativeWriteOutcome,
47
+ OwnedHandle,
46
48
  PreparedHandle,
47
- SnapshotHandle,
48
49
  TxHandle,
49
50
  WireFreshRange,
50
51
  Violation as WireViolation,
51
- ViolationFact as WireViolationFact
52
+ ViolationFact as WireViolationFact,
53
+ WireMutationReport,
54
+ WitnessHandle
52
55
  } from "#native.ts"
53
- import { bridged, native } from "#native.ts"
56
+ import { bridged, bridgedAsync, errorFromThrow, native } from "#native.ts"
54
57
  import type { FindColumn } from "#query/atom.ts"
55
58
  import type { Query } from "#query/lower.ts"
56
59
  import { lowerQuery } from "#query/lower.ts"
@@ -76,6 +79,66 @@ interface MutationReport {
76
79
  readonly changed: bigint
77
80
  }
78
81
 
82
+ /**
83
+ * Column-major collection write: one array per declared field, every
84
+ * column the same length. The second transport of `load` / `insert` —
85
+ * objects and columns are two ways to spell the same batch.
86
+ */
87
+ type ColumnBatch<R extends AnyRelation> = {
88
+ readonly [K in keyof Fact<R> & string]: readonly Fact<R>[K][]
89
+ }
90
+
91
+ type CollectionWrite<R extends AnyRelation> = Iterable<Fact<R>> | ColumnBatch<R>
92
+
93
+ function isColumnBatch(value: object): boolean {
94
+ return !(Symbol.iterator in value)
95
+ }
96
+
97
+ function rowsOf<R extends AnyRelation>(relation: R, facts: Iterable<Fact<R>>): FactValue[][] {
98
+ const rows: FactValue[][] = []
99
+ for (const fact of facts) {
100
+ rows.push(rowOf(relation.data, recordOf(fact)))
101
+ }
102
+ return rows
103
+ }
104
+
105
+ /**
106
+ * Lowers a column batch to per-field wire arrays in sealed field order.
107
+ * Allocates one array per field — never a JS array per row.
108
+ */
109
+ function columnsOf(relation: AnyRelation, batch: object): FactValue[][] {
110
+ const record = recordOf(batch)
111
+ let count: number | undefined
112
+ return relation.data.fields.map(function marshalColumn(declared) {
113
+ const raw = record[declared.name]
114
+ if (!Array.isArray(raw)) {
115
+ throw errors.new(`relation ${relation.name}: column ${declared.name} is not an array`)
116
+ }
117
+ if (count === undefined) {
118
+ count = raw.length
119
+ } else if (raw.length !== count) {
120
+ throw errors.new(
121
+ `relation ${relation.name}: column ${declared.name} has length ${raw.length}, expected ${count}`
122
+ )
123
+ }
124
+ return raw.map(function marshalCell(value: unknown) {
125
+ return cellOf(`relation ${relation.name} field ${declared.name}`, declared.field, value)
126
+ })
127
+ })
128
+ }
129
+
130
+ function mutateCollection<R extends AnyRelation>(
131
+ relation: R,
132
+ facts: CollectionWrite<R>,
133
+ applyRows: (rows: readonly FactValue[][]) => WireMutationReport,
134
+ applyColumns: (columns: readonly FactValue[][]) => WireMutationReport
135
+ ): MutationReport {
136
+ const report = isColumnBatch(facts)
137
+ ? applyColumns(columnsOf(relation, facts))
138
+ : applyRows(rowsOf(relation, facts as Iterable<Fact<R>>))
139
+ return Object.freeze({ submitted: report.submitted, changed: report.changed })
140
+ }
141
+
79
142
  /**
80
143
  * Half-open fresh-id range from one `reserve`. Empty cannot yield a
81
144
  * minted id — `start` exists only on the nonempty arm.
@@ -245,29 +308,40 @@ type Violation<Rels extends SchemaRelations> =
245
308
  * `never` and the arm vanishes from the sum. The outcome is in the type;
246
309
  * a dead arm is never handled.
247
310
  */
248
- type AbandonedArm<R> = R extends Abandon<infer P> ? { readonly ok: false; readonly abandoned: P } : never
311
+ type AbandonedArm<R> = R extends Abandon<infer P> ? { readonly tag: "abandoned"; readonly abandoned: P } : never
312
+
313
+ /** A callback return that is not Promise-like. TypeScript `never` is not a runtime boundary. */
314
+ type SyncResult<R> = R extends PromiseLike<unknown> ? never : R
315
+
316
+ interface Committed<T> {
317
+ readonly value: T
318
+ readonly generation: bigint
319
+ }
320
+
321
+ type Admission<Rels extends SchemaRelations, T> =
322
+ | { readonly tag: "accepted"; readonly value: T }
323
+ | { readonly tag: "rejected"; readonly violations: readonly Violation<Rels>[] }
249
324
 
250
325
  /**
251
- * A write's domain outcome, one sum for BOTH verbs (ruled 2026-07-23,
252
- * R10): the committed generation; the COMPLETE violation set (every
253
- * violated statement cited once, per direction for a containment, in
254
- * materialized statement order); or the callback's own abandon payload —
255
- * commit-vs-abandon is in the type, so a caller's explicit decline to
256
- * commit can never be silently discarded. Narrows on `.ok`, then (when the
257
- * callback can abandon) on `"violations" in result`.
326
+ * A write's domain outcome. One discriminant: narrow on `tag`. The
327
+ * abandoned arm is present exactly when the callback can abandon.
258
328
  */
259
- type WriteResult<Rels extends SchemaRelations, R = void> =
260
- | { readonly ok: true; readonly generation: bigint }
261
- | { readonly ok: false; readonly violations: readonly Violation<Rels>[] }
329
+ type WriteOutcome<Rels extends SchemaRelations, R> =
330
+ | { readonly tag: "accepted"; readonly value: Committed<Exclude<R, Abandon<unknown>>> }
331
+ | { readonly tag: "rejected"; readonly violations: readonly Violation<Rels>[] }
262
332
  | AbandonedArm<R>
263
333
 
334
+ type WriteFromOutcome<Rels extends SchemaRelations, R> =
335
+ | WriteOutcome<Rels, R>
336
+ | { readonly tag: "moved"; readonly witnessed: bigint; readonly current: bigint }
337
+
264
338
  /**
265
339
  * The delta-building callback of a write: runs synchronously against the
266
340
  * live transaction. Returning {@link abandon}`(payload)` rolls the
267
341
  * transaction back (R10) — the result type carries the payload arm exactly
268
342
  * then.
269
343
  */
270
- type DeltaBuild<Rels extends SchemaRelations, R = void> = (tx: Tx<Rels>) => R
344
+ type DeltaBuild<Rels extends SchemaRelations, R = void> = (tx: WriteTx<Rels>) => R
271
345
 
272
346
  /**
273
347
  * The runtime discriminant of {@link Abandon} values — a property probe is
@@ -280,7 +354,7 @@ const abandonMark: unique symbol = Symbol("bumbledb.abandon")
280
354
  * The abandon sentinel {@link abandon} builds: returning one from a `write`
281
355
  * or `writeFrom` callback rolls the transaction back WITHOUT
282
356
  * committing (no empty commit is ever issued) and surfaces the payload as
283
- * `{ ok: false, abandoned: payload }` (ruled 2026-07-23, R10 — the
357
+ * `{ tag: "abandoned", abandoned: payload }` (ruled 2026-07-23, R10 — the
284
358
  * sentinel's contract is unconditional, whichever write verb received it).
285
359
  */
286
360
  interface Abandon<P> {
@@ -292,7 +366,7 @@ interface Abandon<P> {
292
366
  * Wraps a payload in the {@link Abandon} sentinel — the one way a write
293
367
  * callback declines to commit: `return abandon(payload)` aborts the delta
294
368
  * (nothing is committed, not even an empty commit) and the write resolves
295
- * to `{ ok: false, abandoned: payload }`, from `write` and `writeFrom`
369
+ * to `{ tag: "abandoned", abandoned: payload }`, from `write` and `writeFrom`
296
370
  * alike (R10).
297
371
  */
298
372
  function abandon<P>(payload: P): Abandon<P> {
@@ -326,17 +400,17 @@ function isAbandon<R>(value: R): value is R & Abandon<AbandonedPayload<R>> {
326
400
  * cannot resolve over an open `R`.
327
401
  */
328
402
  function isAbandonedOutcome<Rels extends SchemaRelations, R>(
329
- outcome: { readonly ok: false; readonly abandoned: AbandonedPayload<R> },
403
+ outcome: { readonly tag: "abandoned"; readonly abandoned: AbandonedPayload<R> },
330
404
  sentinel: Abandon<AbandonedPayload<R>>
331
- ): outcome is { readonly ok: false; readonly abandoned: AbandonedPayload<R> } & WriteResult<Rels, R> {
405
+ ): outcome is { readonly tag: "abandoned"; readonly abandoned: AbandonedPayload<R> } & WriteOutcome<Rels, R> {
332
406
  return isAbandon(sentinel) && outcome.abandoned === sentinel.payload
333
407
  }
334
408
 
335
409
  /** Builds the abandoned write outcome from the callback's own sentinel (the R10 arm's one mint). */
336
410
  function abandonedOutcome<Rels extends SchemaRelations, R>(
337
411
  sentinel: Abandon<AbandonedPayload<R>>
338
- ): WriteResult<Rels, R> {
339
- const outcome = Object.freeze({ ok: false as const, abandoned: sentinel.payload })
412
+ ): WriteOutcome<Rels, R> {
413
+ const outcome = Object.freeze({ tag: "abandoned" as const, abandoned: sentinel.payload })
340
414
  if (!isAbandonedOutcome<Rels, R>(outcome, sentinel)) {
341
415
  throw errors.new("bumbledb abandon outcome construction incomplete")
342
416
  }
@@ -350,14 +424,14 @@ function abandonedOutcome<Rels extends SchemaRelations, R>(
350
424
  * Spent when its owning `write`/`writeFrom` call resolves the attempt;
351
425
  * any later use throws.
352
426
  */
353
- interface Tx<Rels extends SchemaRelations> {
427
+ interface WriteTx<Rels extends SchemaRelations> {
354
428
  /**
355
429
  * Records a collection of inserts. Singleton is `[fact]`. Empty is
356
430
  * lawful. Returns how many facts were consumed and how many changed
357
431
  * the in-memory final-state view. Every fact is complete — omitted
358
- * fresh cells are a type error; mint first with {@link Tx.reserve}.
432
+ * fresh cells are a type error; mint first with {@link WriteTx.reserve}.
359
433
  */
360
- insert<R extends MemberRelation<Rels>>(relation: R, facts: Iterable<Fact<R>>): MutationReport
434
+ insert<R extends MemberRelation<Rels>>(relation: R, facts: CollectionWrite<R>): MutationReport
361
435
  /**
362
436
  * Records a collection of deletes. Singleton is `[fact]`. Returns
363
437
  * how many facts were consumed and how many changed the view.
@@ -387,21 +461,27 @@ interface Tx<Rels extends SchemaRelations> {
387
461
  ): Fact<R> | undefined
388
462
  }
389
463
 
464
+ type Tx<Rels extends SchemaRelations> = WriteTx<Rels>
465
+
466
+ const witnessTypes: unique symbol = Symbol("bumbledb.witness.types")
467
+
390
468
  /**
391
- * The read view one `db.read(fn)` call scopes — or one `using snap =
392
- * db.read()` acquisition owns (ruled 2026-07-23, R12: lifetimes are
393
- * disposables, never `close()`). The callback form invalidates the value
394
- * when `fn` returns; the `using` form releases it at scope exit through
395
- * `Symbol.dispose` — either way the release is deterministic and
396
- * scope-shaped, every later verb call throws a typed used-after-scope
397
- * error, and the underlying snapshot (with its LMDB reader slot) is
398
- * already closed.
469
+ * Cloneable generation evidence from one store read. May cross `await`.
470
+ * Disposal is idempotent; later use throws {@link ErrSpentHandle}.
399
471
  */
400
- interface ReadScope<Rels extends SchemaRelations> extends Disposable {
472
+ interface Witness<Rels extends SchemaRelations> extends Disposable {
473
+ readonly [witnessTypes]?: Rels
474
+ }
475
+
476
+ /**
477
+ * The borrowed instance one `db.read((instance, witness) => …)` callback
478
+ * receives. Invalid the moment the callback returns. A stashed value
479
+ * throws {@link ErrUseAfterScope}. Not a handle: there is no `db.read()`.
480
+ */
481
+ interface ReadInstance<Rels extends SchemaRelations> {
401
482
  /**
402
- * The committed generation this scope witnessed — carried by the
403
- * snapshot open itself (one crossing, inside the snapshot's own
404
- * transaction), so it is atomic with the snapshot by construction.
483
+ * The committed generation this instance witnessed — read inside the
484
+ * lease's own transaction.
405
485
  */
406
486
  readonly generation: bigint
407
487
  /** Full-relation export in row-id order, decoded to bare structural facts. */
@@ -424,12 +504,13 @@ interface ReadScope<Rels extends SchemaRelations> extends Disposable {
424
504
  /** Committed-state membership of one complete fact. */
425
505
  contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
426
506
  /**
427
- * Executes a prepared query against this scope's snapshot with the
428
- * typed params object; returns the answer SET as plain rows with
429
- * bare structural values (no order — the host sorts). This is the ONE
507
+ * Executes a prepared query against this instance with the typed
508
+ * params object; returns the answer SET as plain rows with bare
509
+ * structural values (no order — the host sorts). This is the ONE
430
510
  * execution spelling ({@link Prepared} carries no `execute`).
431
511
  */
432
512
  execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[]
513
+ prepare<Row, Params extends ParamsRecord>(q: Query<Rels, Row, Params>): Prepared<Rels, Row, Params>
433
514
  }
434
515
 
435
516
  /**
@@ -446,7 +527,7 @@ const preparedTypes: unique symbol = Symbol("bumbledb.prepared.types")
446
527
  * One prepared query as a plain VALUE: explicit visible compilation
447
528
  * (`db.prepare(q)` lowers, pins the plan, and surfaces every engine roster
448
529
  * refusal), no lifecycle. Execution happens ONLY through
449
- * `snap.execute(prepared, params)` / `db.execute(prepared, params)` — the
530
+ * `instance.execute(prepared, params)` / `db.execute(prepared, params)` — the
450
531
  * symmetry rule's one spelling. The engine-side plan is reclaimed by a GC
451
532
  * finalizer when this value becomes unreachable (reclamation only, never
452
533
  * correctness — an unreclaimed plan is idle memory, and process exit frees
@@ -467,53 +548,42 @@ interface Db<Rels extends SchemaRelations> {
467
548
  /** The theory this store was opened with (fingerprint-verified by the engine). */
468
549
  readonly schema: Schema<Rels>
469
550
  /**
470
- * One scoped snapshot read: opens an MVCC snapshot, runs `fn`
471
- * SYNCHRONOUSLY against it, and closes the snapshot unconditionally
472
- * before returning `fn`'s result. The {@link ReadScope} is invalidated
473
- * when `fn` returns — a used-after-scope call throws a typed error.
474
- */
475
- read<T>(fn: (snap: ReadScope<Rels>) => T): T
476
- /**
477
- * The `using` acquisition (ruled 2026-07-23, R12): `using snap =
478
- * db.read()` — the caller owns the scope's lifetime, and the scope's
479
- * `Symbol.dispose` releases the snapshot deterministically at scope
480
- * exit, in the language's own syntax. Lifetimes are disposables, never
481
- * `close()`.
551
+ * One store read: runs `body` SYNCHRONOUSLY inside the engine lease
552
+ * and returns its result. The {@link ReadInstance} is invalidated
553
+ * when `body` returns — a stashed use throws {@link ErrUseAfterScope}.
554
+ * The {@link Witness} is a clone and may escape. A thenable return
555
+ * throws {@link ErrAsyncCallback}.
482
556
  */
483
- read(): ReadScope<Rels>
484
- /** `db.scan(r)` === `db.read(snap => snap.scan(r))` — the symmetry rule. */
557
+ read<R>(body: (instance: ReadInstance<Rels>, witness: Witness<Rels>) => SyncResult<R>): SyncResult<R>
558
+ /** `db.scan(r)` === `db.read(instance => instance.scan(r))` — the symmetry rule. */
485
559
  scan<R extends MemberRelation<Rels>>(relation: R): Fact<R>[]
486
- /** `db.get(r, k)` === `db.read(snap => snap.get(r, k))` — the symmetry rule. */
560
+ /** `db.get(r, k)` === `db.read(instance => instance.get(r, k))` — the symmetry rule. */
487
561
  get<R extends MemberRelation<Rels>>(relation: R, key: KeyFact<R>): Fact<R> | undefined
488
- /** `db.get(r, s, k)` === `db.read(snap => snap.get(r, s, k))` — the symmetry rule, keyed form. */
562
+ /** `db.get(r, s, k)` === `db.read(instance => instance.get(r, s, k))` — the symmetry rule, keyed form. */
489
563
  get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
490
564
  relation: R,
491
565
  keyStatement: KeyStatement<R, P>,
492
566
  key: DeclaredKeyFact<R, P>
493
567
  ): Fact<R> | undefined
494
- /** `db.contains(r, f)` === `db.read(snap => snap.contains(r, f))` — the symmetry rule. */
568
+ /** `db.contains(r, f)` === `db.read(instance => instance.contains(r, f))` — the symmetry rule. */
495
569
  contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
496
- /** `db.execute(p, params)` === `db.read(snap => snap.execute(p, params))` — the symmetry rule. */
570
+ /** `db.execute(p, params)` === `db.read(instance => instance.execute(p, params))` — the symmetry rule. */
497
571
  execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[]
498
572
  /**
499
573
  * One delta transaction: builds the delta synchronously through `fn`,
500
574
  * commits, and returns the domain outcome. A throw from `fn` aborts
501
575
  * the delta (LMDB untouched) and rethrows wrapped. `fn` may decline to
502
- * commit by returning {@link abandon}`(payload)` (ruled 2026-07-23,
503
- * R10): the transaction rolls back — nothing is committed, not even an
504
- * empty commit — and the outcome is `{ ok: false, abandoned: payload }`,
505
- * an arm the result type carries exactly when the callback can abandon.
576
+ * commit by returning {@link abandon}`(payload)`: the transaction rolls
577
+ * back — nothing is committed, not even an empty commit — and the
578
+ * outcome is `{ tag: "abandoned", abandoned: payload }`.
506
579
  */
507
- write<R = void>(fn: DeltaBuild<Rels, R>): WriteResult<Rels, R>
580
+ write<R>(fn: (tx: WriteTx<Rels>) => SyncResult<R>): WriteOutcome<Rels, SyncResult<R>>
508
581
  /**
509
- * One-shot witnessed write from a live read snapshot: begins a
510
- * transaction that commits only if no state-changing commit landed
511
- * since `snap` was taken. Must run inside the read callback that owns
512
- * `snap`. A moved generation is the typed {@link ErrGenerationMoved}
513
- * — retry is host policy; this method never loops. `fn` may decline to
514
- * commit by returning {@link abandon}`(payload)`.
582
+ * Witnessed write: commits only if no state-changing commit landed
583
+ * since `witness` was minted. A moved generation is
584
+ * `{ tag: "moved" }` — retry is host policy; this method never loops.
515
585
  */
516
- writeFrom<R>(snap: ReadScope<Rels>, fn: DeltaBuild<Rels, R>): WriteResult<Rels, R>
586
+ writeFrom<R>(witness: Witness<Rels>, fn: (tx: WriteTx<Rels>) => SyncResult<R>): WriteFromOutcome<Rels, SyncResult<R>>
517
587
  /**
518
588
  * Prepares a query value built against THIS schema (identity is the
519
589
  * membership rule): lowers it to the engine IR, pins the plan, and
@@ -780,6 +850,29 @@ function tablesOf(theory: AnySchema, manifest: Manifest): Tables {
780
850
  return Object.freeze({ relations, statements: Object.freeze(entries) })
781
851
  }
782
852
 
853
+ function tablesFromTheory(theory: AnySchema): Tables {
854
+ const entries = materializedEntries(theory)
855
+ const relations = new Map<string, RelationEntry>()
856
+ Object.keys(theory.relations).forEach(function byOrdinal(name, ordinal) {
857
+ const member = theory.relations[name]
858
+ if (member === undefined) {
859
+ throw errors.new(`bumbledb theory has no relation ${name}`)
860
+ }
861
+ const fieldIds = new Map<string, number>()
862
+ sealedFieldsOf(member).forEach(function byField(declared, fieldOrdinal) {
863
+ fieldIds.set(declared.name, fieldOrdinal)
864
+ })
865
+ let primaryKey: PrimaryKey | undefined
866
+ entries.forEach(function firstOwnedKey(entry, index) {
867
+ if (primaryKey === undefined && entry.kind === "functionality" && entry.owner === name) {
868
+ primaryKey = Object.freeze({ statementId: index, projection: entry.projection })
869
+ }
870
+ })
871
+ relations.set(name, Object.freeze({ id: ordinal, member, fieldIds, primaryKey }))
872
+ })
873
+ return Object.freeze({ relations, statements: Object.freeze(entries) })
874
+ }
875
+
783
876
  /** The point-read half a transaction and a read scope share, over their own handle. */
784
877
  interface PointReads {
785
878
  contains(relationId: number, row: readonly FactValue[]): boolean
@@ -787,25 +880,33 @@ interface PointReads {
787
880
  }
788
881
 
789
882
  /**
790
- * One read scope's PRIVATE lifetime record: its live snapshot handle, the
791
- * generation it witnessed (carried by the snapshot open itself — one
792
- * crossing, finding 016), its liveness flag (flipped when the owning
793
- * `read` callback returns, or by the scope's own
794
- * `Symbol.dispose`), its close latch (`closed` — the snapshot closes
795
- * exactly once, whichever of the owner and the dispose gets there first),
796
- * and its owning store's identity token. Held in {@link scopeStates} —
797
- * the snapshot handle is never a public value.
883
+ * One borrowed instance's PRIVATE lifetime record. Held in
884
+ * {@link instanceStates} — the native handle is never a public value.
798
885
  */
799
- interface ScopeState {
800
- readonly handle: SnapshotHandle
801
- readonly generation: bigint
886
+ interface InstanceState {
887
+ readonly handle: InstanceHandle
802
888
  live: boolean
803
- closed: boolean
804
889
  readonly owner: object
805
890
  }
806
891
 
807
- /** The private lifetime records of this module's read scopes. */
808
- const scopeStates = new WeakMap<object, ScopeState>()
892
+ const instanceStates = new WeakMap<object, InstanceState>()
893
+
894
+ interface WitnessState {
895
+ readonly handle: WitnessHandle
896
+ spent: boolean
897
+ readonly owner: object
898
+ }
899
+
900
+ const witnessStates = new WeakMap<object, WitnessState>()
901
+
902
+ const witnessReclaimer = new FinalizationRegistry<WitnessHandle>(function reclaimWitness(handle) {
903
+ const closed = errors.trySync(function closeWitness() {
904
+ native.witnessClose(handle)
905
+ })
906
+ if (closed.error) {
907
+ return
908
+ }
909
+ })
809
910
 
810
911
  /**
811
912
  * One prepared value's PRIVATE engine half: the pinned plan handle, the
@@ -838,14 +939,305 @@ const planReclaimer = new FinalizationRegistry<PreparedHandle>(function reclaimP
838
939
  }
839
940
  })
840
941
 
942
+ const ErrAsyncCallback = errors.new(
943
+ "bumbledb asyncCallback: a read or write callback returned a thenable — the callback is synchronous"
944
+ )
945
+ const ErrSpentHandle = errors.new("bumbledb spentHandle: a consumed builder, instance, or witness was used")
946
+ const ErrUseAfterScope = errors.new(
947
+ "bumbledb useAfterScope: a stashed read instance or write transaction was used after its callback returned"
948
+ )
949
+ const ErrForeignPrepared = errors.new("bumbledb foreignPrepared: a prepared query met a foreign instance")
950
+ const ErrForeignWitness = errors.new("bumbledb foreignWitness: a witness met a foreign store")
951
+
841
952
  /**
842
- * The typed generation-moved refusal `writeFrom` throws when a
843
- * state-changing commit landed since the witness snapshot: retry is host
844
- * policy. Match with `errors.is`.
953
+ * The shared typed read surface: store leases and owned instances both
954
+ * expose scan/get/contains/execute/prepare. The native ops are the only
955
+ * difference — one way to read, two handle kinds.
845
956
  */
846
- const ErrGenerationMoved = errors.new(
847
- "bumbledb generationMoved: a state-changing commit landed since the witness snapshot"
848
- )
957
+ interface CatalogNative {
958
+ scan(relationId: number): FactValue[][]
959
+ contains(relationId: number, values: readonly FactValue[]): boolean
960
+ get(relationId: number, statementId: number, keyValues: readonly FactValue[]): FactValue[] | null
961
+ prepare(query: ReturnType<typeof lowerQuery>): ReturnType<typeof native.instancePrepare>
962
+ execute(prepared: PreparedHandle, params: ReturnType<typeof wireParams>): FactValue[][]
963
+ }
964
+
965
+ function catalogMethods<Rels extends SchemaRelations>(
966
+ theory: Schema<Rels>,
967
+ tables: Tables,
968
+ owner: object,
969
+ assertLive: () => void,
970
+ ops: CatalogNative
971
+ ): Pick<ReadInstance<Rels>, "scan" | "get" | "contains" | "execute" | "prepare"> {
972
+ function resolveOrdinary(relation: AnyRelation): RelationEntry {
973
+ const entry = tables.relations.get(relation.name)
974
+ if (entry === undefined || entry.member !== relation) {
975
+ throw errors.new(`relation ${relation.name} is not a member of schema ${theory.name}`)
976
+ }
977
+ if (isClosedMember(relation)) {
978
+ throw errors.new(
979
+ `relation ${relation.name} is closed — its extension is schema data (axioms), never scanned or written`
980
+ )
981
+ }
982
+ return entry
983
+ }
984
+ function declaredKeyOf(relation: AnyRelation, statement: Statement): PrimaryKey {
985
+ const statementId = tables.statements.findIndex(function byIdentity(candidate) {
986
+ return "statement" in candidate && candidate.statement === statement
987
+ })
988
+ const entry = tables.statements[statementId]
989
+ if (entry === undefined) {
990
+ throw errors.new(
991
+ `keyed get statement is not a declared statement of schema ${theory.name} — statement identity is the membership rule`
992
+ )
993
+ }
994
+ if (entry.kind !== "functionality") {
995
+ throw errors.new("keyed get takes a key() statement — containments and capacity statements key nothing")
996
+ }
997
+ if (entry.owner !== relation.name) {
998
+ throw errors.new(
999
+ `keyed get statement keys ${entry.owner}, not ${relation.name} — the statement must be a declared key of the relation it reads`
1000
+ )
1001
+ }
1002
+ return Object.freeze({ statementId, projection: entry.projection })
1003
+ }
1004
+ function planOf(prepared: object): PreparedPlan {
1005
+ const plan = preparedPlans.get(prepared)
1006
+ if (plan === undefined) {
1007
+ throw errors.wrap(ErrForeignPrepared, "bumbledb execute target is not a prepared value of this SDK")
1008
+ }
1009
+ if (plan.owner !== owner) {
1010
+ throw errors.wrap(
1011
+ ErrForeignPrepared,
1012
+ `bumbledb prepared value was prepared by a different store than this one (schema ${theory.name})`
1013
+ )
1014
+ }
1015
+ return plan
1016
+ }
1017
+ function contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean {
1018
+ assertLive()
1019
+ const entry = resolveOrdinary(relation)
1020
+ return bridged("bumbledb instance contains", function readContains() {
1021
+ return ops.contains(entry.id, rowOf(relation.data, recordOf(fact)))
1022
+ })
1023
+ }
1024
+ function get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
1025
+ relation: R,
1026
+ keyOrStatement: KeyFact<R> | KeyStatement<R, P>,
1027
+ declaredKey?: DeclaredKeyFact<R, P>
1028
+ ): Fact<R> | undefined {
1029
+ assertLive()
1030
+ const entry = resolveOrdinary(relation)
1031
+ return selectKeyRead(
1032
+ keyOrStatement,
1033
+ declaredKey,
1034
+ function byStatement(statement, key) {
1035
+ const selected = declaredKeyOf(relation, statement)
1036
+ const row = bridged("bumbledb instance get", function readGet() {
1037
+ return ops.get(entry.id, selected.statementId, keyRowOf(relation.data, selected.projection, recordOf(key)))
1038
+ })
1039
+ return row === null ? undefined : factOf(relation, row)
1040
+ },
1041
+ function byPrimary(key) {
1042
+ const primaryKey = entry.primaryKey
1043
+ if (primaryKey === undefined) {
1044
+ throw errors.new(
1045
+ `relation ${relation.name} has no candidate key — keyed get requires a fresh field or a declared key statement`
1046
+ )
1047
+ }
1048
+ const row = bridged("bumbledb instance get", function readGet() {
1049
+ return ops.get(
1050
+ entry.id,
1051
+ primaryKey.statementId,
1052
+ keyRowOf(relation.data, primaryKey.projection, recordOf(key))
1053
+ )
1054
+ })
1055
+ return row === null ? undefined : factOf(relation, row)
1056
+ }
1057
+ )
1058
+ }
1059
+ function scan<R extends MemberRelation<Rels>>(relation: R): Fact<R>[] {
1060
+ assertLive()
1061
+ const entry = resolveOrdinary(relation)
1062
+ const rows = bridged("bumbledb instance scan", function readScan() {
1063
+ return ops.scan(entry.id)
1064
+ })
1065
+ return rows.map(function decodeRow(row) {
1066
+ return factOf(relation, row)
1067
+ })
1068
+ }
1069
+ function execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[] {
1070
+ assertLive()
1071
+ const plan = planOf(prepared)
1072
+ const wire = wireParams(plan.params, recordOf(params))
1073
+ const rows = bridged("execute bumbledb prepared query", function callExecute() {
1074
+ return ops.execute(plan.handle, wire)
1075
+ })
1076
+ return decodeAnswers<Row>(plan.finds, rows)
1077
+ }
1078
+ function prepare<Row, Params extends ParamsRecord>(q: Query<Rels, Row, Params>): Prepared<Rels, Row, Params> {
1079
+ assertLive()
1080
+ if (q.schema !== theory) {
1081
+ throw errors.new(
1082
+ `query was built against schema ${q.schema.name}, not the identical schema value this store opened with — schema identity is the membership rule`
1083
+ )
1084
+ }
1085
+ const queryIr = lowerQuery(q)
1086
+ const outcome = bridged("prepare bumbledb query", function callPrepare() {
1087
+ return ops.prepare(queryIr)
1088
+ })
1089
+ if (!outcome.ok) {
1090
+ throwPrepareRefusal(outcome.message)
1091
+ }
1092
+ const prepared: Prepared<Rels, Row, Params> = Object.freeze({})
1093
+ preparedPlans.set(
1094
+ prepared,
1095
+ Object.freeze({
1096
+ handle: outcome.prepared,
1097
+ owner,
1098
+ params: q.data.params,
1099
+ finds: q.data.finds
1100
+ })
1101
+ )
1102
+ planReclaimer.register(prepared, outcome.prepared)
1103
+ return prepared
1104
+ }
1105
+ return { scan, get, contains, execute, prepare }
1106
+ }
1107
+
1108
+ function ordinaryEntry(tables: Tables, theory: AnySchema, relation: AnyRelation): RelationEntry {
1109
+ const entry = tables.relations.get(relation.name)
1110
+ if (entry === undefined || entry.member !== relation) {
1111
+ throw errors.new(`relation ${relation.name} is not a member of schema ${theory.name}`)
1112
+ }
1113
+ if (isClosedMember(relation)) {
1114
+ throw errors.new(
1115
+ `relation ${relation.name} is closed — its extension is schema data (axioms), never scanned or written`
1116
+ )
1117
+ }
1118
+ return entry
1119
+ }
1120
+
1121
+ function overlayMethods<Rels extends SchemaRelations>(
1122
+ theory: Schema<Rels>,
1123
+ tables: Tables,
1124
+ assertLive: () => void,
1125
+ reads: PointReads
1126
+ ): Pick<WriteTx<Rels>, "contains" | "get"> {
1127
+ function declaredKeyOf(relation: AnyRelation, statement: Statement): PrimaryKey {
1128
+ const statementId = tables.statements.findIndex(function byIdentity(candidate) {
1129
+ return "statement" in candidate && candidate.statement === statement
1130
+ })
1131
+ const entry = tables.statements[statementId]
1132
+ if (entry === undefined) {
1133
+ throw errors.new(
1134
+ `keyed get statement is not a declared statement of schema ${theory.name} — statement identity is the membership rule`
1135
+ )
1136
+ }
1137
+ if (entry.kind !== "functionality") {
1138
+ throw errors.new("keyed get takes a key() statement — containments and capacity statements key nothing")
1139
+ }
1140
+ if (entry.owner !== relation.name) {
1141
+ throw errors.new(
1142
+ `keyed get statement keys ${entry.owner}, not ${relation.name} — the statement must be a declared key of the relation it reads`
1143
+ )
1144
+ }
1145
+ return Object.freeze({ statementId, projection: entry.projection })
1146
+ }
1147
+ function contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean {
1148
+ assertLive()
1149
+ const entry = ordinaryEntry(tables, theory, relation)
1150
+ return reads.contains(entry.id, rowOf(relation.data, recordOf(fact)))
1151
+ }
1152
+ function readThroughKey<R extends MemberRelation<Rels>>(
1153
+ relation: R,
1154
+ entry: RelationEntry,
1155
+ selected: PrimaryKey,
1156
+ key: Readonly<Record<string, unknown>>
1157
+ ): Fact<R> | undefined {
1158
+ const row = reads.get(entry.id, selected.statementId, keyRowOf(relation.data, selected.projection, key))
1159
+ if (row === null) {
1160
+ return undefined
1161
+ }
1162
+ return factOf(relation, row)
1163
+ }
1164
+ function get<R extends MemberRelation<Rels>>(relation: R, key: KeyFact<R>): Fact<R> | undefined
1165
+ function get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
1166
+ relation: R,
1167
+ keyStatement: KeyStatement<R, P>,
1168
+ key: DeclaredKeyFact<R, P>
1169
+ ): Fact<R> | undefined
1170
+ function get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
1171
+ relation: R,
1172
+ keyOrStatement: KeyFact<R> | KeyStatement<R, P>,
1173
+ declaredKey?: DeclaredKeyFact<R, P>
1174
+ ): Fact<R> | undefined {
1175
+ assertLive()
1176
+ const entry = ordinaryEntry(tables, theory, relation)
1177
+ return selectKeyRead(
1178
+ keyOrStatement,
1179
+ declaredKey,
1180
+ function byStatement(statement, key) {
1181
+ return readThroughKey(relation, entry, declaredKeyOf(relation, statement), recordOf(key))
1182
+ },
1183
+ function byPrimary(key) {
1184
+ const primaryKey = entry.primaryKey
1185
+ if (primaryKey === undefined) {
1186
+ throw errors.new(
1187
+ `relation ${relation.name} has no candidate key — keyed get requires a fresh field or a declared key statement`
1188
+ )
1189
+ }
1190
+ return readThroughKey(relation, entry, primaryKey, recordOf(key))
1191
+ }
1192
+ )
1193
+ }
1194
+ return { contains, get }
1195
+ }
1196
+
1197
+ function createReadInstance<Rels extends SchemaRelations>(
1198
+ nativeHandle: InstanceHandle,
1199
+ theory: Schema<Rels>,
1200
+ tables: Tables,
1201
+ owner: object
1202
+ ): ReadInstance<Rels> {
1203
+ const state: InstanceState = { handle: nativeHandle, live: true, owner }
1204
+ function assertLive(): void {
1205
+ if (!state.live) {
1206
+ throw errors.wrap(
1207
+ ErrUseAfterScope,
1208
+ "bumbledb read instance is invalidated — its owning callback already returned"
1209
+ )
1210
+ }
1211
+ }
1212
+ const methods = catalogMethods(theory, tables, owner, assertLive, {
1213
+ scan(relationId) {
1214
+ return native.instanceScan(state.handle, relationId)
1215
+ },
1216
+ contains(relationId, values) {
1217
+ return native.instanceContains(state.handle, relationId, values)
1218
+ },
1219
+ get(relationId, statementId, keyValues) {
1220
+ return native.instanceGet(state.handle, relationId, statementId, keyValues)
1221
+ },
1222
+ prepare(query) {
1223
+ return native.instancePrepare(state.handle, query)
1224
+ },
1225
+ execute(prepared, params) {
1226
+ return native.preparedExecute(prepared, state.handle, params)
1227
+ }
1228
+ })
1229
+ const instance: ReadInstance<Rels> = Object.freeze({
1230
+ get generation() {
1231
+ assertLive()
1232
+ return bridged("bumbledb instance generation", function readGeneration() {
1233
+ return native.instanceGeneration(state.handle)
1234
+ })
1235
+ },
1236
+ ...methods
1237
+ })
1238
+ instanceStates.set(instance, state)
1239
+ return instance
1240
+ }
849
1241
 
850
1242
  /**
851
1243
  * Constructs one open `Db` over an already-admitted handle: builds the
@@ -1022,152 +1414,70 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1022
1414
  return { contains, get }
1023
1415
  }
1024
1416
 
1025
- /**
1026
- * Resolves a prepared value's private plan, refusing foreign objects
1027
- * and prepared values of other stores as typed errors.
1028
- */
1029
- function planOf(prepared: object): PreparedPlan {
1030
- const plan = preparedPlans.get(prepared)
1031
- if (plan === undefined) {
1032
- throw errors.new("bumbledb execute target is not a prepared value of this SDK")
1033
- }
1034
- if (plan.owner !== owner) {
1035
- throw errors.new(
1036
- `bumbledb prepared value was prepared by a different store than this one (schema ${theory.name})`
1037
- )
1038
- }
1039
- return plan
1417
+ function pinPrepared<Row, Params extends ParamsRecord>(
1418
+ preparedHandle: PreparedHandle,
1419
+ q: Query<Rels, Row, Params>
1420
+ ): Prepared<Rels, Row, Params> {
1421
+ const prepared: Prepared<Rels, Row, Params> = Object.freeze({})
1422
+ preparedPlans.set(
1423
+ prepared,
1424
+ Object.freeze({
1425
+ handle: preparedHandle,
1426
+ owner,
1427
+ params: q.data.params,
1428
+ finds: q.data.finds
1429
+ })
1430
+ )
1431
+ planReclaimer.register(prepared, preparedHandle)
1432
+ return prepared
1040
1433
  }
1041
1434
 
1042
- /**
1043
- * Builds one {@link ReadScope} over a live scope state. Every verb
1044
- * asserts liveness first: the owning call flips `state.live` the moment
1045
- * its callback returns (or the scope's own `Symbol.dispose` does, for a
1046
- * `using`-acquired scope), so a leaked scope is a typed refusal forever
1047
- * after.
1048
- */
1049
- function makeScope(state: ScopeState): ReadScope<Rels> {
1050
- function assertLive(): void {
1051
- if (!state.live) {
1052
- throw errors.new("bumbledb read scope is invalidated — its owning read callback already returned")
1053
- }
1054
- }
1055
- const reads = pointReadsOf(assertLive, {
1056
- contains(relationId, row) {
1057
- return bridged("bumbledb snapshot contains", function readContains() {
1058
- return native.snapshotContains(state.handle, relationId, row)
1059
- })
1060
- },
1061
- get(relationId, statementId, key) {
1062
- return bridged("bumbledb snapshot get", function readGet() {
1063
- return native.snapshotGet(state.handle, relationId, statementId, key)
1435
+ function makeWitness(nativeHandle: WitnessHandle): Witness<Rels> {
1436
+ const state: WitnessState = { handle: nativeHandle, spent: false, owner }
1437
+ const witness: Witness<Rels> = Object.freeze({
1438
+ [Symbol.dispose](): void {
1439
+ if (state.spent) {
1440
+ return
1441
+ }
1442
+ state.spent = true
1443
+ bridged("close bumbledb witness", function closeWitness() {
1444
+ native.witnessClose(nativeHandle)
1064
1445
  })
1065
1446
  }
1066
1447
  })
1067
- function scan<R extends MemberRelation<Rels>>(relation: R): Fact<R>[] {
1068
- assertLive()
1069
- const entry = resolveOrdinary(relation)
1070
- const rows = bridged("bumbledb snapshot scan", function readScan() {
1071
- return native.snapshotScan(state.handle, entry.id)
1072
- })
1073
- return rows.map(function decodeRow(row) {
1074
- return factOf(relation, row)
1075
- })
1076
- }
1077
- function execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[] {
1078
- assertLive()
1079
- const plan = planOf(prepared)
1080
- const wire = wireParams(plan.params, recordOf(params))
1081
- const rows = bridged("execute bumbledb prepared query", function callExecute() {
1082
- return native.preparedExecute(plan.handle, state.handle, wire)
1083
- })
1084
- return decodeAnswers<Row>(plan.finds, rows)
1085
- }
1086
- /** The R12 teardown: invalidate, then close — idempotent through the state's close latch. */
1087
- function dispose(): void {
1088
- state.live = false
1089
- closeScopeState(state)
1090
- }
1091
- const scope: ReadScope<Rels> = Object.freeze({
1092
- generation: state.generation,
1093
- scan,
1094
- get: reads.get,
1095
- contains: reads.contains,
1096
- execute,
1097
- [Symbol.dispose]: dispose
1098
- })
1099
- scopeStates.set(scope, state)
1100
- return scope
1448
+ witnessStates.set(witness, state)
1449
+ witnessReclaimer.register(witness, nativeHandle)
1450
+ return witness
1101
1451
  }
1102
1452
 
1103
- /**
1104
- * Live-handle accounting (diagnostic law, prod EINVAL 2026-07-17): every
1105
- * snapshot open/close is counted so a write-begin failure can report how
1106
- * many read handles were live at the fault — a leaked scope is invisible
1107
- * until the exact moment it matters, so the failure carries the census.
1108
- */
1109
- let liveSnapshots = 0
1110
-
1111
- /**
1112
- * Opens one snapshot and its scope state (live until the owner flips
1113
- * it). The witnessed generation rides the snapshot open itself — one
1114
- * crossing carries both (finding 016), so the fault-pairing close
1115
- * branch a second `dbGeneration` call needed is structurally gone.
1116
- */
1117
- function openScopeState(): ScopeState {
1118
- const opened = bridged("open bumbledb snapshot", function openSnapshot() {
1119
- return native.dbSnapshot(handle)
1120
- })
1121
- liveSnapshots += 1
1122
- return { handle: opened.snapshot, generation: opened.generation, live: true, closed: false, owner }
1453
+ function makeInstance(nativeHandle: InstanceHandle): ReadInstance<Rels> {
1454
+ return createReadInstance(nativeHandle, theory, tables, owner)
1123
1455
  }
1124
1456
 
1125
- /**
1126
- * Closes a scope's snapshot after the owner invalidated it — a LATCH:
1127
- * the snapshot closes exactly once, whichever of the owning call and
1128
- * the scope's own `Symbol.dispose` gets there first, so an early
1129
- * in-callback disposal never double-closes (and never double-counts
1130
- * the census).
1131
- */
1132
- function closeScopeState(state: ScopeState): void {
1133
- if (state.closed) {
1134
- return
1135
- }
1136
- state.closed = true
1137
- bridged("close bumbledb snapshot", function closeSnapshot() {
1138
- native.snapshotClose(state.handle)
1139
- })
1140
- liveSnapshots -= 1
1141
- }
1142
-
1143
- function read<T>(fn: (snap: ReadScope<Rels>) => T): T
1144
- function read(): ReadScope<Rels>
1145
- function read<T>(fn?: (snap: ReadScope<Rels>) => T): T | ReadScope<Rels> {
1146
- const state = openScopeState()
1147
- const scope = makeScope(state)
1148
- if (fn === undefined) {
1149
- /**
1150
- * The `using` acquisition (R12): the caller owns the lifetime —
1151
- * `using snap = db.read()` — and the scope's `Symbol.dispose`
1152
- * is the deterministic release, scope-shaped in the language's
1153
- * own syntax.
1154
- */
1155
- return scope
1156
- }
1157
- const result = errors.trySync(function runRead() {
1158
- return fn(scope)
1457
+ function read<R>(body: (instance: ReadInstance<Rels>, witness: Witness<Rels>) => SyncResult<R>): SyncResult<R> {
1458
+ let captured: R | undefined
1459
+ const result = bridged("bumbledb read", function runRead() {
1460
+ return native.dbRead(handle, function onRead(nativeInstance, nativeWitness) {
1461
+ const instance = makeInstance(nativeInstance)
1462
+ const witness = makeWitness(nativeWitness)
1463
+ const value = body(instance, witness)
1464
+ const state = instanceStates.get(instance)
1465
+ if (state !== undefined) {
1466
+ state.live = false
1467
+ }
1468
+ if (isThenable(value)) {
1469
+ throw errors.wrap(ErrAsyncCallback, "bumbledb read callback returned a thenable")
1470
+ }
1471
+ captured = value
1472
+ return value
1473
+ })
1159
1474
  })
1160
- state.live = false
1161
- closeScopeState(state)
1162
- if (result.error) {
1163
- throw errors.wrap(result.error, "bumbledb read")
1164
- }
1165
- return result.data
1475
+ return (captured ?? result) as SyncResult<R>
1166
1476
  }
1167
1477
 
1168
1478
  function scan<R extends MemberRelation<Rels>>(relation: R): Fact<R>[] {
1169
- return read(function scanInScope(snap) {
1170
- return snap.scan(relation)
1479
+ return read(function scanInScope(instance) {
1480
+ return instance.scan(relation)
1171
1481
  })
1172
1482
  }
1173
1483
 
@@ -1182,41 +1492,39 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1182
1492
  keyOrStatement: KeyFact<R> | KeyStatement<R, P>,
1183
1493
  declaredKey?: DeclaredKeyFact<R, P>
1184
1494
  ): Fact<R> | undefined {
1185
- return read(function getInScope(snap) {
1186
- return selectKeyRead(
1495
+ let found: Fact<R> | undefined
1496
+ read(function getInScope(instance) {
1497
+ found = selectKeyRead(
1187
1498
  keyOrStatement,
1188
1499
  declaredKey,
1189
1500
  function byStatement(statement, key) {
1190
- return snap.get(relation, statement, key)
1501
+ return instance.get(relation, statement, key)
1191
1502
  },
1192
1503
  function byPrimary(key) {
1193
- return snap.get(relation, key)
1504
+ return instance.get(relation, key)
1194
1505
  }
1195
1506
  )
1196
1507
  })
1508
+ return found
1197
1509
  }
1198
1510
 
1199
1511
  function contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean {
1200
- return read(function containsInScope(snap) {
1201
- return snap.contains(relation, fact)
1512
+ return read(function containsInScope(instance) {
1513
+ return instance.contains(relation, fact)
1202
1514
  })
1203
1515
  }
1204
1516
 
1205
1517
  function execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[] {
1206
- return read(function executeInScope(snap) {
1207
- return snap.execute(prepared, params)
1518
+ return read(function executeInScope(instance) {
1519
+ return instance.execute(prepared, params)
1208
1520
  })
1209
1521
  }
1210
1522
 
1211
- /**
1212
- * Builds one {@link Tx} over a transaction-handle thunk: `write` and
1213
- * `writeFrom` pass an already-begun handle.
1214
- */
1215
- function makeTx(resolveTx: () => TxHandle): { readonly tx: Tx<Rels>; spend(): void } {
1523
+ function makeTx(resolveTx: () => TxHandle): { readonly tx: WriteTx<Rels>; spend(): void } {
1216
1524
  const txState = { spent: false }
1217
1525
  function assertLive(): void {
1218
1526
  if (txState.spent) {
1219
- throw errors.new("bumbledb write transaction is spent")
1527
+ throw errors.wrap(ErrUseAfterScope, "bumbledb write transaction is spent")
1220
1528
  }
1221
1529
  }
1222
1530
  const reads = pointReadsOf(assertLive, {
@@ -1233,29 +1541,31 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1233
1541
  })
1234
1542
  }
1235
1543
  })
1236
- function insert<R extends MemberRelation<Rels>>(relation: R, facts: Iterable<Fact<R>>): MutationReport {
1544
+ function insert<R extends MemberRelation<Rels>>(relation: R, facts: CollectionWrite<R>): MutationReport {
1237
1545
  assertLive()
1238
- const rows: FactValue[][] = []
1239
- for (const fact of facts) {
1240
- rows.push(rowOf(relation.data, recordOf(fact)))
1241
- }
1242
1546
  const entry = resolveOrdinary(relation)
1243
1547
  const txHandle = resolveTx()
1244
- const report = bridged("bumbledb tx insert", function record() {
1245
- return native.txInsert(txHandle, entry.id, rows)
1246
- })
1247
- return Object.freeze({ submitted: report.submitted, changed: report.changed })
1548
+ return mutateCollection(
1549
+ relation,
1550
+ facts,
1551
+ function applyRows(rows) {
1552
+ return bridged("bumbledb tx insert", function record() {
1553
+ return native.txInsert(txHandle, entry.id, rows)
1554
+ })
1555
+ },
1556
+ function applyColumns(columns) {
1557
+ return bridged("bumbledb tx insert", function recordColumns() {
1558
+ return native.txInsertColumns(txHandle, entry.id, columns)
1559
+ })
1560
+ }
1561
+ )
1248
1562
  }
1249
1563
  function remove<R extends MemberRelation<Rels>>(relation: R, facts: Iterable<Fact<R>>): MutationReport {
1250
1564
  assertLive()
1251
- const rows: FactValue[][] = []
1252
- for (const fact of facts) {
1253
- rows.push(rowOf(relation.data, recordOf(fact)))
1254
- }
1255
1565
  const entry = resolveOrdinary(relation)
1256
1566
  const txHandle = resolveTx()
1257
1567
  const report = bridged("bumbledb tx delete", function record() {
1258
- return native.txDelete(txHandle, entry.id, rows)
1568
+ return native.txDelete(txHandle, entry.id, rowsOf(relation, facts))
1259
1569
  })
1260
1570
  return Object.freeze({ submitted: report.submitted, changed: report.changed })
1261
1571
  }
@@ -1282,7 +1592,7 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1282
1592
  })
1283
1593
  return freshRangeOf(range)
1284
1594
  }
1285
- const tx: Tx<Rels> = Object.freeze({
1595
+ const tx: WriteTx<Rels> = Object.freeze({
1286
1596
  insert,
1287
1597
  delete: remove,
1288
1598
  reserve,
@@ -1295,111 +1605,92 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1295
1605
  return { tx, spend }
1296
1606
  }
1297
1607
 
1298
- function runDelta<R>(txHandle: TxHandle, fn: DeltaBuild<Rels, R>): WriteResult<Rels, R> {
1299
- const made = makeTx(function resolveTx() {
1300
- return txHandle
1301
- })
1302
- const built = errors.trySync(function buildDelta() {
1303
- return fn(made.tx)
1304
- })
1305
- made.spend()
1306
- if (built.error) {
1307
- bridged("abort bumbledb write transaction", function abort() {
1308
- native.txAbort(txHandle)
1309
- })
1310
- throw errors.wrap(built.error, "build write delta")
1311
- }
1312
- if (isThenable(built.data)) {
1313
- /**
1314
- * An `async` callback TYPECHECKS (Promise<void> is assignable where
1315
- * a `void` return is expected) but its body runs after the tx is
1316
- * spent: committing here would be a silent EMPTY commit reported
1317
- * ok while the callback's real inserts throw "spent" as unhandled
1318
- * rejections. Refused typed instead — abort, nothing committed
1319
- * (the same one-writer law as the thrown-callback path).
1320
- */
1321
- bridged("abort bumbledb write transaction", function abort() {
1322
- native.txAbort(txHandle)
1608
+ function mapNativeWrite<R>(nativeOutcome: NativeWriteOutcome, built: R | undefined): WriteFromOutcome<Rels, R> {
1609
+ if (nativeOutcome.tag === "moved") {
1610
+ return Object.freeze({
1611
+ tag: "moved" as const,
1612
+ witnessed: nativeOutcome.witnessed,
1613
+ current: nativeOutcome.current
1323
1614
  })
1324
- throw errors.new(
1325
- "bumbledb write callback returned a thenable — the delta build is synchronous; an async callback is refused, nothing was committed"
1326
- )
1327
1615
  }
1328
- if (isAbandon(built.data)) {
1329
- /**
1330
- * The caller's explicit decline to commit (R10): the sentinel's
1331
- * contract is unconditional — roll back, nothing committed, not
1332
- * even an empty commit; commit is unreachable for a sentinel
1333
- * result.
1334
- */
1335
- bridged("abort bumbledb write transaction", function abort() {
1336
- native.txAbort(txHandle)
1616
+ if (nativeOutcome.tag === "rejected") {
1617
+ return Object.freeze({
1618
+ tag: "rejected" as const,
1619
+ violations: Object.freeze(nativeOutcome.violations.map(violationOf))
1337
1620
  })
1338
- return abandonedOutcome<Rels, R>(built.data)
1339
1621
  }
1340
- const committed = errors.trySync(function commitDelta() {
1341
- return bridged("commit bumbledb write transaction", function commit() {
1342
- return native.txCommit(txHandle)
1343
- })
1344
- })
1345
- if (committed.error) {
1346
- /**
1347
- * A THROWN commit (engine I/O failure, bridge fault) must never
1348
- * leave the write transaction live: LMDB holds one writer per
1349
- * environment, and a leaked handle turns every later begin into
1350
- * EINVAL for the process's lifetime. The abort is best-effort —
1351
- * the native side may already have consumed the handle.
1352
- */
1353
- const aborted = errors.trySync(function abortAfterFailedCommit() {
1354
- native.txAbort(txHandle)
1355
- })
1356
- if (aborted.error) {
1622
+ if (nativeOutcome.tag === "abandoned") {
1623
+ if (built === undefined || !isAbandon(built)) {
1624
+ throw errors.new("bumbledb write abandoned without an abandon sentinel")
1357
1625
  }
1358
- throw errors.wrap(committed.error, "commit bumbledb write transaction")
1359
- }
1360
- const outcome = committed.data
1361
- if (outcome.ok) {
1362
- return Object.freeze({ ok: true, generation: outcome.generation })
1626
+ return abandonedOutcome<Rels, R>(built)
1363
1627
  }
1364
1628
  return Object.freeze({
1365
- ok: false,
1366
- violations: Object.freeze(outcome.violations.map(violationOf))
1629
+ tag: "accepted" as const,
1630
+ value: Object.freeze({
1631
+ value: built as Exclude<R, Abandon<unknown>>,
1632
+ generation: nativeOutcome.generation
1633
+ })
1367
1634
  })
1368
1635
  }
1369
1636
 
1370
- function write<R = void>(fn: DeltaBuild<Rels, R>): WriteResult<Rels, R> {
1371
- const txHandle = bridged(`begin bumbledb write transaction (live snapshots: ${liveSnapshots})`, function begin() {
1372
- return native.dbWriteBegin(handle)
1637
+ function runWrite<R>(
1638
+ invoke: (callback: (tx: TxHandle) => boolean) => NativeWriteOutcome,
1639
+ fn: (tx: WriteTx<Rels>) => SyncResult<R>
1640
+ ): WriteFromOutcome<Rels, SyncResult<R>> {
1641
+ let built: SyncResult<R> | undefined
1642
+ const nativeOutcome = bridged("bumbledb write", function callWrite() {
1643
+ return invoke(function onWrite(txHandle) {
1644
+ const made = makeTx(function resolveTx() {
1645
+ return txHandle
1646
+ })
1647
+ const result = errors.trySync(function buildDelta() {
1648
+ return fn(made.tx)
1649
+ })
1650
+ made.spend()
1651
+ if (result.error) {
1652
+ throw errors.wrap(result.error, "build write delta")
1653
+ }
1654
+ if (isThenable(result.data)) {
1655
+ throw errors.wrap(ErrAsyncCallback, "bumbledb write callback returned a thenable")
1656
+ }
1657
+ built = result.data
1658
+ return !isAbandon(result.data)
1659
+ })
1373
1660
  })
1374
- return runDelta(txHandle, fn)
1661
+ return mapNativeWrite(nativeOutcome, built)
1375
1662
  }
1376
1663
 
1377
- /**
1378
- * One-shot write from a live snapshot this store owns. Begins immediately;
1379
- * a moved generation throws {@link ErrGenerationMoved} and `fn` never
1380
- * runs. The snapshot stays owned by the caller's read callback.
1381
- */
1382
- function writeFrom<R>(snap: ReadScope<Rels>, fn: DeltaBuild<Rels, R>): WriteResult<Rels, R> {
1383
- const snapState = scopeStates.get(snap)
1384
- if (snapState === undefined) {
1385
- throw errors.new("bumbledb writeFrom witness is not a read scope of this SDK")
1386
- }
1387
- if (snapState.owner !== owner) {
1388
- throw errors.new(`bumbledb writeFrom snapshot belongs to a different store (schema ${theory.name})`)
1664
+ function write<R>(fn: (tx: WriteTx<Rels>) => SyncResult<R>): WriteOutcome<Rels, SyncResult<R>> {
1665
+ const outcome = runWrite(function invoke(callback) {
1666
+ return native.dbWrite(handle, callback)
1667
+ }, fn)
1668
+ if (outcome.tag === "moved") {
1669
+ throw errors.new("bumbledb write reported moved — unconditional writes cannot move")
1389
1670
  }
1390
- if (!snapState.live) {
1391
- throw errors.new("bumbledb writeFrom snapshot is invalidated — its owning read callback already returned")
1671
+ return outcome
1672
+ }
1673
+
1674
+ function writeFrom<R>(
1675
+ witness: Witness<Rels>,
1676
+ fn: (tx: WriteTx<Rels>) => SyncResult<R>
1677
+ ): WriteFromOutcome<Rels, SyncResult<R>> {
1678
+ const state = witnessStates.get(witness)
1679
+ if (state === undefined) {
1680
+ throw errors.wrap(ErrForeignWitness, "bumbledb writeFrom witness is not a witness of this SDK")
1392
1681
  }
1393
- const witnessed = bridged("begin witnessed bumbledb write transaction", function begin() {
1394
- return native.dbWriteFrom(handle, snapState.handle)
1395
- })
1396
- if (!witnessed.ok) {
1682
+ if (state.owner !== owner) {
1397
1683
  throw errors.wrap(
1398
- ErrGenerationMoved,
1399
- `writeFrom: generation moved (witnessed ${witnessed.witnessed} current ${witnessed.current}) against schema ${theory.name}`
1684
+ ErrForeignWitness,
1685
+ `bumbledb writeFrom witness belongs to a different store (schema ${theory.name})`
1400
1686
  )
1401
1687
  }
1402
- return runDelta(witnessed.tx, fn)
1688
+ if (state.spent) {
1689
+ throw errors.wrap(ErrSpentHandle, "bumbledb writeFrom witness has been disposed")
1690
+ }
1691
+ return runWrite(function invoke(callback) {
1692
+ return native.dbWriteFrom(handle, state.handle, callback)
1693
+ }, fn)
1403
1694
  }
1404
1695
 
1405
1696
  function prepare<Row, Params extends ParamsRecord>(q: Query<Rels, Row, Params>): Prepared<Rels, Row, Params> {
@@ -1413,21 +1704,9 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1413
1704
  return native.dbPrepare(handle, queryIr)
1414
1705
  })
1415
1706
  if (!outcome.ok) {
1416
- throw errors.new(`bumbledb ${outcome.kind} (prepare): ${outcome.message}`)
1707
+ throwPrepareRefusal(outcome.message)
1417
1708
  }
1418
- const preparedHandle = outcome.prepared
1419
- const prepared: Prepared<Rels, Row, Params> = Object.freeze({})
1420
- preparedPlans.set(
1421
- prepared,
1422
- Object.freeze({
1423
- handle: preparedHandle,
1424
- owner,
1425
- params: q.data.params,
1426
- finds: q.data.finds
1427
- })
1428
- )
1429
- planReclaimer.register(prepared, preparedHandle)
1430
- return prepared
1709
+ return pinPrepared(outcome.prepared, q)
1431
1710
  }
1432
1711
 
1433
1712
  return Object.freeze({
@@ -1456,90 +1735,425 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1456
1735
  const ErrNewtypeMismatch = errors.new(
1457
1736
  "bumbledb newtypeMismatch: a statement pairs faces whose newtypes disagree — the faces of a dependency agree on their newtype, or neither carries one"
1458
1737
  )
1738
+ const ErrSchemaError = errors.new("bumbledb schemaError: the declaration failed validation")
1739
+ const ErrFingerprintMismatch = errors.new(
1740
+ "bumbledb fingerprintMismatch: the store's schema does not match this theory"
1741
+ )
1742
+ const ErrIrError = errors.new("bumbledb irError: the query failed validation")
1743
+
1744
+ function throwOpenRefusal(
1745
+ verb: string,
1746
+ canonical: string,
1747
+ kind: "schemaError" | "newtypeMismatch" | "fingerprintMismatch",
1748
+ message: string
1749
+ ): never {
1750
+ const detail = `${verb} ${canonical}: ${message}`
1751
+ if (kind === "newtypeMismatch") {
1752
+ throw errors.wrap(ErrNewtypeMismatch, detail)
1753
+ }
1754
+ if (kind === "schemaError") {
1755
+ throw errors.wrap(ErrSchemaError, detail)
1756
+ }
1757
+ throw errors.wrap(ErrFingerprintMismatch, detail)
1758
+ }
1459
1759
 
1460
- /**
1461
- * The one admission path both verbs share: lower the theory, run one
1462
- * bridge call, and wrap the domain refusals — `schemaError` (spec
1463
- * resolution + schema validation, every issue in one message),
1464
- * `newtypeMismatch` (the coherence wall, {@link ErrNewtypeMismatch}),
1465
- * and `fingerprintMismatch` (a different theory cannot open the store)
1466
- * — into typed errors carrying the engine's message intact.
1467
- * Environment failures (a second live writer on the same path, IO)
1468
- * throw from the bridge; the engine's `EnvironmentLocked` message is
1469
- * "another live handle holds this environment's lock".
1470
- */
1471
- function admit<Rels extends SchemaRelations>(
1472
- verb: "create" | "open",
1760
+ function throwPrepareRefusal(message: string): never {
1761
+ throw errors.wrap(ErrIrError, `prepare: ${message}`)
1762
+ }
1763
+
1764
+ function openFromHandle<Rels extends SchemaRelations>(dbHandle: DbHandle, theory: Schema<Rels>): Db<Rels> {
1765
+ const manifest = bridged("fetch bumbledb manifest", function fetchManifest() {
1766
+ return native.dbManifest(dbHandle)
1767
+ })
1768
+ return openDb(dbHandle, theory, manifest)
1769
+ }
1770
+
1771
+ async function createStore<Rels extends SchemaRelations>(
1473
1772
  storePath: string,
1474
1773
  theory: Schema<Rels>
1475
- ): Db<Rels> {
1774
+ ): Promise<Admission<Rels, Db<Rels>>> {
1476
1775
  const canonical = path.resolve(storePath)
1477
1776
  const spec = lower(theory)
1478
- const opened = bridged(`${verb} bumbledb store at ${canonical}`, function callBridge() {
1479
- if (verb === "create") {
1480
- return native.dbCreate(canonical, spec)
1777
+ const created = await bridgedAsync(`create bumbledb store at ${canonical}`, function callBridge() {
1778
+ return native.dbCreate(canonical, spec)
1779
+ })
1780
+ if (created.tag === "schemaError" || created.tag === "newtypeMismatch") {
1781
+ throwOpenRefusal("create", canonical, created.tag, created.message)
1782
+ }
1783
+ if (created.tag === "rejected") {
1784
+ return Object.freeze({
1785
+ tag: "rejected" as const,
1786
+ violations: Object.freeze(
1787
+ created.violations.map(function mapWire(wire) {
1788
+ return mapViolationWithoutStore<Rels>(theory, wire)
1789
+ })
1790
+ )
1791
+ })
1792
+ }
1793
+ return Object.freeze({ tag: "accepted" as const, value: openFromHandle(created.db, theory) })
1794
+ }
1795
+
1796
+ function mapViolationWithoutStore<Rels extends SchemaRelations>(
1797
+ theory: Schema<Rels>,
1798
+ wire: WireViolation
1799
+ ): Violation<Rels> {
1800
+ const entries = materializedEntries(theory)
1801
+ const entry = entries[wire.statementId]
1802
+ if (entry === undefined) {
1803
+ throw errors.new(`bumbledb violation cites unknown statement id ${wire.statementId}`)
1804
+ }
1805
+ function offending(fact: WireViolationFact): OffendingFact<Rels> {
1806
+ const member = theory.relations[fact.relation]
1807
+ if (member === undefined || !(fact.relation in theory.relations)) {
1808
+ throw errors.new(`bumbledb violation cites unknown relation ${fact.relation}`)
1481
1809
  }
1810
+ const declared = sealedFieldsOf(member)
1811
+ const decoded: Record<string, FactValue> = {}
1812
+ for (const cell of fact.fields) {
1813
+ const cited = declared.find(function byName(candidate) {
1814
+ return candidate.name === cell.name
1815
+ })
1816
+ const roster = rosterOf(cited?.field)
1817
+ decoded[cell.name] =
1818
+ roster !== undefined
1819
+ ? handleOf(`violation fact ${fact.relation} field ${cell.name}`, roster, cell.value)
1820
+ : cell.value
1821
+ }
1822
+ return Object.freeze({ relation: fact.relation as keyof Rels & string, fact: Object.freeze(decoded) })
1823
+ }
1824
+ const facts = Object.freeze(wire.facts.map(offending))
1825
+ const canonical = wire.canonical
1826
+ if (entry.kind === "functionality") {
1827
+ if (!("statement" in entry)) {
1828
+ return Object.freeze({ kind: "functionality", statement: undefined, canonical, facts })
1829
+ }
1830
+ return Object.freeze({ kind: "functionality", statement: entry.statement, canonical, facts })
1831
+ }
1832
+ if (entry.kind === "capacity") {
1833
+ if (wire.kind !== "capacity") {
1834
+ throw errors.new(`bumbledb violation ${wire.statementId} is a capacity slot without a measure`)
1835
+ }
1836
+ return Object.freeze({
1837
+ kind: "capacity",
1838
+ statement: entry.statement,
1839
+ canonical,
1840
+ measure: wire.measure,
1841
+ facts
1842
+ })
1843
+ }
1844
+ if (wire.kind !== "containment") {
1845
+ throw errors.new(`bumbledb violation ${wire.statementId} is a containment slot without a direction`)
1846
+ }
1847
+ if (entry.kind === "mirrors") {
1848
+ return Object.freeze({
1849
+ kind: "containment",
1850
+ statement: entry.statement,
1851
+ canonical,
1852
+ direction: wire.direction,
1853
+ orientation: entry.orientation,
1854
+ facts
1855
+ })
1856
+ }
1857
+ return Object.freeze({
1858
+ kind: "containment",
1859
+ statement: entry.statement,
1860
+ canonical,
1861
+ direction: wire.direction,
1862
+ facts
1863
+ })
1864
+ }
1865
+
1866
+ async function openStore<Rels extends SchemaRelations>(
1867
+ storePath: string,
1868
+ theory: Schema<Rels>
1869
+ ): Promise<Db<Rels>> {
1870
+ const canonical = path.resolve(storePath)
1871
+ const spec = lower(theory)
1872
+ const opened = await bridgedAsync(`open bumbledb store at ${canonical}`, function callBridge() {
1482
1873
  return native.dbOpen(canonical, spec)
1483
1874
  })
1484
1875
  if (!opened.ok) {
1485
- if (opened.kind === "newtypeMismatch") {
1486
- throw errors.wrap(ErrNewtypeMismatch, `${verb} ${canonical}: ${opened.message}`)
1876
+ throwOpenRefusal("open", canonical, opened.kind, opened.message)
1877
+ }
1878
+ return openFromHandle(opened.db, theory)
1879
+ }
1880
+
1881
+ interface OwnedInstance<Rels extends SchemaRelations> extends Disposable {
1882
+ prepare<Row, Params extends ParamsRecord>(q: Query<Rels, Row, Params>): Prepared<Rels, Row, Params>
1883
+ execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[]
1884
+ scan<R extends MemberRelation<Rels>>(relation: R): Fact<R>[]
1885
+ contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
1886
+ get<R extends MemberRelation<Rels>>(relation: R, key: KeyFact<R>): Fact<R> | undefined
1887
+ get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
1888
+ relation: R,
1889
+ keyStatement: KeyStatement<R, P>,
1890
+ key: DeclaredKeyFact<R, P>
1891
+ ): Fact<R> | undefined
1892
+ }
1893
+
1894
+ interface InstanceBuilder<Rels extends SchemaRelations> extends Disposable {
1895
+ load<R extends MemberRelation<Rels>>(relation: R, facts: CollectionWrite<R>): MutationReport
1896
+ delete<R extends MemberRelation<Rels>>(relation: R, facts: Iterable<Fact<R>>): MutationReport
1897
+ reserve<R extends MemberRelation<Rels>>(relation: R, field: FreshKeys<R> & string, count: bigint): FreshRange
1898
+ contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
1899
+ get<R extends MemberRelation<Rels>>(relation: R, key: KeyFact<R>): Fact<R> | undefined
1900
+ get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
1901
+ relation: R,
1902
+ keyStatement: KeyStatement<R, P>,
1903
+ key: DeclaredKeyFact<R, P>
1904
+ ): Fact<R> | undefined
1905
+ admit(): Promise<Admission<Rels, OwnedInstance<Rels>>>
1906
+ }
1907
+
1908
+ const ownedRecords = new WeakMap<object, { handle: OwnedHandle; theory: AnySchema; spent: boolean; owner: object }>()
1909
+ const builderRecords = new WeakMap<object, { handle: BuilderHandle; theory: AnySchema; spent: boolean }>()
1910
+
1911
+ const ownedReclaimer = new FinalizationRegistry<OwnedHandle>(function reclaimOwned(handle) {
1912
+ const closed = errors.trySync(function closeOwned() {
1913
+ native.ownedInstanceClose(handle)
1914
+ })
1915
+ if (closed.error) {
1916
+ return
1917
+ }
1918
+ })
1919
+
1920
+ const builderReclaimer = new FinalizationRegistry<BuilderHandle>(function reclaimBuilder(handle) {
1921
+ const closed = errors.trySync(function closeBuilder() {
1922
+ native.instanceBuilderClose(handle)
1923
+ })
1924
+ if (closed.error) {
1925
+ return
1926
+ }
1927
+ })
1928
+
1929
+ function wrapOwned<Rels extends SchemaRelations>(nativeHandle: OwnedHandle, theory: Schema<Rels>): OwnedInstance<Rels> {
1930
+ const owner = Object.freeze({})
1931
+ const rec = { handle: nativeHandle, theory, spent: false, owner }
1932
+ const tables = tablesFromTheory(theory)
1933
+ function assertLive(): void {
1934
+ if (rec.spent) {
1935
+ throw errors.wrap(ErrSpentHandle, "bumbledb owned instance has been disposed")
1487
1936
  }
1488
- throw errors.new(`bumbledb ${opened.kind} (${verb} ${canonical}): ${opened.message}`)
1489
1937
  }
1490
- const manifest = bridged("fetch bumbledb manifest", function fetchManifest() {
1491
- return native.dbManifest(opened.db)
1938
+ const methods = catalogMethods(theory, tables, owner, assertLive, {
1939
+ scan(relationId) {
1940
+ return native.ownedScan(nativeHandle, relationId)
1941
+ },
1942
+ contains(relationId, values) {
1943
+ return native.ownedContains(nativeHandle, relationId, values)
1944
+ },
1945
+ get(relationId, statementId, keyValues) {
1946
+ return native.ownedGet(nativeHandle, relationId, statementId, keyValues)
1947
+ },
1948
+ prepare(query) {
1949
+ return native.ownedPrepare(nativeHandle, query)
1950
+ },
1951
+ execute(prepared, params) {
1952
+ return native.ownedExecute(prepared, nativeHandle, params)
1953
+ }
1492
1954
  })
1493
- return openDb(opened.db, theory, manifest)
1955
+ const instance: OwnedInstance<Rels> = Object.freeze({
1956
+ ...methods,
1957
+ [Symbol.dispose](): void {
1958
+ if (rec.spent) {
1959
+ return
1960
+ }
1961
+ try {
1962
+ native.ownedInstanceClose(nativeHandle)
1963
+ } catch (caught) {
1964
+ const error = errorFromThrow(caught)
1965
+ if (/leased for publish/.test(error.message)) {
1966
+ throw errors.wrap(ErrSpentHandle, "bumbledb owned instance is leased for publish")
1967
+ }
1968
+ throw errors.wrap(error, "close bumbledb owned instance")
1969
+ }
1970
+ rec.spent = true
1971
+ ownedReclaimer.unregister(instance)
1972
+ }
1973
+ })
1974
+ ownedRecords.set(instance, rec)
1975
+ ownedReclaimer.register(instance, nativeHandle, instance)
1976
+ return instance
1977
+ }
1978
+
1979
+ function wrapBuilder<Rels extends SchemaRelations>(
1980
+ nativeHandle: BuilderHandle,
1981
+ theory: Schema<Rels>
1982
+ ): InstanceBuilder<Rels> {
1983
+ const rec = { handle: nativeHandle, theory, spent: false }
1984
+ const tables = tablesFromTheory(theory)
1985
+ function assertLive(): void {
1986
+ if (rec.spent) {
1987
+ throw errors.wrap(ErrSpentHandle, "bumbledb instance builder has been spent")
1988
+ }
1989
+ }
1990
+ const overlay = overlayMethods(theory, tables, assertLive, {
1991
+ contains(relationId, row) {
1992
+ return bridged("bumbledb builder contains", function readContains() {
1993
+ return native.instanceBuilderContains(nativeHandle, relationId, row)
1994
+ })
1995
+ },
1996
+ get(relationId, statementId, key) {
1997
+ return bridged("bumbledb builder get", function readGet() {
1998
+ return native.instanceBuilderGet(nativeHandle, relationId, statementId, key)
1999
+ })
2000
+ }
2001
+ })
2002
+ const builder: InstanceBuilder<Rels> = Object.freeze({
2003
+ load<R extends MemberRelation<Rels>>(relation: R, facts: CollectionWrite<R>): MutationReport {
2004
+ assertLive()
2005
+ const entry = ordinaryEntry(tables, theory, relation)
2006
+ return mutateCollection(
2007
+ relation,
2008
+ facts,
2009
+ function applyRows(rows) {
2010
+ return bridged("bumbledb builder load", function loadRows() {
2011
+ return native.instanceBuilderLoad(nativeHandle, entry.id, rows)
2012
+ })
2013
+ },
2014
+ function applyColumns(columns) {
2015
+ return bridged("bumbledb builder load", function loadColumns() {
2016
+ return native.instanceBuilderLoadColumns(nativeHandle, entry.id, columns)
2017
+ })
2018
+ }
2019
+ )
2020
+ },
2021
+ delete<R extends MemberRelation<Rels>>(relation: R, facts: Iterable<Fact<R>>): MutationReport {
2022
+ assertLive()
2023
+ const entry = ordinaryEntry(tables, theory, relation)
2024
+ const report = bridged("bumbledb builder delete", function remove() {
2025
+ return native.instanceBuilderDelete(nativeHandle, entry.id, rowsOf(relation, facts))
2026
+ })
2027
+ return Object.freeze({ submitted: report.submitted, changed: report.changed })
2028
+ },
2029
+ reserve<R extends MemberRelation<Rels>>(
2030
+ relation: R,
2031
+ field: FreshKeys<R> & string,
2032
+ count: bigint
2033
+ ): FreshRange {
2034
+ assertLive()
2035
+ const entry = ordinaryEntry(tables, theory, relation)
2036
+ const declared = relation.data.fields.find(function byName(candidate) {
2037
+ return candidate.name === field
2038
+ })
2039
+ if (declared === undefined || !isFreshField(declared.field)) {
2040
+ throw errors.new(`relation ${relation.name}: field ${field} is not a fresh cell`)
2041
+ }
2042
+ const fieldId = entry.fieldIds.get(field)
2043
+ if (fieldId === undefined) {
2044
+ throw errors.new(`bumbledb manifest drift: relation ${relation.name} has no field id for ${field}`)
2045
+ }
2046
+ const range = bridged("bumbledb builder reserve", function mint() {
2047
+ return native.instanceBuilderReserve(nativeHandle, entry.id, fieldId, count)
2048
+ })
2049
+ return freshRangeOf(range)
2050
+ },
2051
+ contains: overlay.contains,
2052
+ get: overlay.get,
2053
+ async admit(): Promise<Admission<Rels, OwnedInstance<Rels>>> {
2054
+ if (rec.spent) {
2055
+ throw errors.wrap(ErrSpentHandle, "bumbledb instance builder has been spent")
2056
+ }
2057
+ rec.spent = true
2058
+ builderReclaimer.unregister(builder)
2059
+ let outcome: AdmitResult
2060
+ try {
2061
+ outcome = await native.instanceBuilderAdmit(nativeHandle)
2062
+ } catch (caught) {
2063
+ throw errors.wrap(errorFromThrow(caught), "admit bumbledb instance")
2064
+ }
2065
+ if (outcome.tag === "rejected") {
2066
+ return Object.freeze({
2067
+ tag: "rejected" as const,
2068
+ violations: Object.freeze(
2069
+ outcome.violations.map(function mapWire(wire) {
2070
+ return mapViolationWithoutStore<Rels>(theory, wire)
2071
+ })
2072
+ )
2073
+ })
2074
+ }
2075
+ return Object.freeze({
2076
+ tag: "accepted" as const,
2077
+ value: wrapOwned(outcome.value, theory)
2078
+ })
2079
+ },
2080
+ [Symbol.dispose](): void {
2081
+ if (rec.spent) {
2082
+ return
2083
+ }
2084
+ rec.spent = true
2085
+ builderReclaimer.unregister(builder)
2086
+ bridged("close bumbledb instance builder", function closeBuilder() {
2087
+ native.instanceBuilderClose(nativeHandle)
2088
+ })
2089
+ }
2090
+ })
2091
+ builderRecords.set(builder, rec)
2092
+ builderReclaimer.register(builder, nativeHandle, builder)
2093
+ return builder
1494
2094
  }
1495
2095
 
2096
+ const InstanceBuilder = Object.freeze({
2097
+ create<Rels extends SchemaRelations>(theory: Schema<Rels>): InstanceBuilder<Rels> {
2098
+ const spec = lower(theory)
2099
+ const handle = bridged("create bumbledb instance builder", function make() {
2100
+ return native.instanceBuilderNew(spec)
2101
+ })
2102
+ return wrapBuilder(handle, theory)
2103
+ }
2104
+ })
2105
+
1496
2106
  /**
1497
2107
  * The store lifecycle — `Db.create(path, schema)` / `Db.open(path, schema)`.
1498
2108
  * Create refuses an already-initialized directory; open verifies format
1499
- * version, store kind, and the schema fingerprint. A second live handle
1500
- * on the same path is the engine's `EnvironmentLocked`. There is no close
1501
- * anywhere: the process owns the environment until GC/exit (durability is
1502
- * the engine's per-commit fsync). One store kind exists: durable —
1503
- * resume = reopen in a fresh process, or hold the `Db` this process opened.
2109
+ * version and the schema fingerprint. A second live handle on the same
2110
+ * path is the engine's `EnvironmentLocked`. There is no close anywhere:
2111
+ * the process owns the environment until GC/exit (durability is the
2112
+ * engine's per-commit fsync). Resume = reopen in a fresh process, or
2113
+ * hold the `Db` this process opened.
1504
2114
  */
1505
2115
  const Db = Object.freeze({
1506
2116
  /** Creates a fresh durable store at `path` from the schema. */
1507
- async create<Rels extends SchemaRelations>(path: string, theory: Schema<Rels>): Promise<Db<Rels>> {
1508
- return admit("create", path, theory)
2117
+ async create<Rels extends SchemaRelations>(
2118
+ storePath: string,
2119
+ theory: Schema<Rels>
2120
+ ): Promise<Admission<Rels, Db<Rels>>> {
2121
+ return createStore(storePath, theory)
1509
2122
  },
1510
2123
  /**
1511
2124
  * Opens an existing durable store at `path` with the same theory.
1512
- * A fingerprint-matching open also BACK-FILLS the store's persisted
1513
- * schema descriptor when it is absent (self-describing stores, engine
1514
- * 50-storage.md § the `_meta` block), so a legacy store becomes
1515
- * exhumable after one ordinary open — adoption is automatic, never a
1516
- * separate verb. A second open of a still-live path is
1517
- * `EnvironmentLocked`.
2125
+ * Format 8 open never back-fills a descriptor. A second open of a
2126
+ * still-live path is `EnvironmentLocked`.
1518
2127
  */
1519
- async open<Rels extends SchemaRelations>(path: string, theory: Schema<Rels>): Promise<Db<Rels>> {
1520
- return admit("open", path, theory)
2128
+ async open<Rels extends SchemaRelations>(storePath: string, theory: Schema<Rels>): Promise<Db<Rels>> {
2129
+ return openStore(storePath, theory)
1521
2130
  },
1522
- /**
1523
- * Opens a store READ-ONLY from its own persisted descriptor — the SDK's
1524
- * one schema-independent read path (no theory, no fingerprint check; the
1525
- * store rebirth tool's entry). Lives beside `open`/`create` so the path
1526
- * law stays in one place: the same `node:path.resolve` canonicalization,
1527
- * applied here. The value is NOT cached and is a DISPOSABLE lifetime
1528
- * (R12) — `using exhumed = await Db.exhume(path)` releases the engine
1529
- * handle and the store's exclusive lock at scope exit, so a same-path
1530
- * reopen never waits on GC. A store not yet adopted rejects with the
1531
- * typed `ErrExhumeNoDescriptor` (the remedy: one fingerprint-matching
1532
- * `Db.open` under the creating schema back-fills the descriptor).
1533
- */
1534
- async exhume(storePath: string): Promise<Exhumed> {
1535
- return exhumeStore(path.resolve(storePath))
2131
+ async fromInstance<Rels extends SchemaRelations>(
2132
+ storePath: string,
2133
+ instance: OwnedInstance<Rels>
2134
+ ): Promise<Db<Rels>> {
2135
+ const rec = ownedRecords.get(instance)
2136
+ if (rec === undefined) {
2137
+ throw errors.wrap(ErrSpentHandle, "bumbledb fromInstance target is not an owned instance of this SDK")
2138
+ }
2139
+ if (rec.spent) {
2140
+ throw errors.wrap(ErrSpentHandle, "bumbledb fromInstance target has been disposed")
2141
+ }
2142
+ const canonical = path.resolve(storePath)
2143
+ const dbHandle = await bridgedAsync(`publish bumbledb instance at ${canonical}`, function publish() {
2144
+ return native.dbFromInstance(canonical, rec.handle)
2145
+ })
2146
+ return openFromHandle(dbHandle, rec.theory as Schema<Rels>)
1536
2147
  }
1537
2148
  })
1538
2149
 
1539
2150
  export type {
1540
2151
  Abandon,
1541
2152
  AbandonedArm,
2153
+ Admission,
1542
2154
  CapacityViolation,
2155
+ ColumnBatch,
2156
+ Committed,
1543
2157
  ContainmentViolation,
1544
2158
  DeclaredKeyFact,
1545
2159
  DeclaredKeyViolation,
@@ -1550,10 +2164,28 @@ export type {
1550
2164
  MirrorViolation,
1551
2165
  MutationReport,
1552
2166
  OffendingFact,
2167
+ OwnedInstance,
1553
2168
  Prepared,
1554
- ReadScope,
2169
+ ReadInstance,
2170
+ SyncResult,
1555
2171
  Tx,
1556
2172
  Violation,
1557
- WriteResult
2173
+ Witness,
2174
+ WriteFromOutcome,
2175
+ WriteOutcome,
2176
+ WriteTx
2177
+ }
2178
+ export {
2179
+ abandon,
2180
+ Db,
2181
+ ErrAsyncCallback,
2182
+ ErrForeignPrepared,
2183
+ ErrForeignWitness,
2184
+ ErrFingerprintMismatch,
2185
+ ErrIrError,
2186
+ ErrNewtypeMismatch,
2187
+ ErrSchemaError,
2188
+ ErrSpentHandle,
2189
+ ErrUseAfterScope,
2190
+ InstanceBuilder
1558
2191
  }
1559
- export { abandon, Db, ErrGenerationMoved, ErrNewtypeMismatch }