@bjornpagen/bumbledb 0.12.2 → 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.
Files changed (46) hide show
  1. package/COOKBOOK.md +35 -21
  2. package/README.md +82 -55
  3. package/dist/db.d.ts +173 -130
  4. package/dist/db.d.ts.map +1 -1
  5. package/dist/db.js +782 -371
  6. package/dist/db.js.map +1 -1
  7. package/dist/index.d.ts +7 -13
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +4 -9
  10. package/dist/index.js.map +1 -1
  11. package/dist/marshal.d.ts +5 -27
  12. package/dist/marshal.d.ts.map +1 -1
  13. package/dist/marshal.js +4 -21
  14. package/dist/marshal.js.map +1 -1
  15. package/dist/native.d.ts +157 -168
  16. package/dist/native.d.ts.map +1 -1
  17. package/dist/native.js +46 -8
  18. package/dist/native.js.map +1 -1
  19. package/dist/query/find.d.ts +14 -9
  20. package/dist/query/find.d.ts.map +1 -1
  21. package/dist/query/find.js +2 -2
  22. package/dist/query/find.js.map +1 -1
  23. package/dist/query/lower.d.ts.map +1 -1
  24. package/dist/query/lower.js +5 -4
  25. package/dist/query/lower.js.map +1 -1
  26. package/dist/query/parse-ir.d.ts.map +1 -1
  27. package/dist/query/parse-ir.js +11 -9
  28. package/dist/query/parse-ir.js.map +1 -1
  29. package/dist/relation.d.ts +4 -25
  30. package/dist/relation.d.ts.map +1 -1
  31. package/dist/relation.js +3 -4
  32. package/dist/relation.js.map +1 -1
  33. package/package.json +3 -3
  34. package/src/db.ts +1151 -500
  35. package/src/index.ts +28 -24
  36. package/src/marshal.ts +5 -35
  37. package/src/native.ts +248 -175
  38. package/src/query/find.ts +19 -14
  39. package/src/query/lower.ts +7 -6
  40. package/src/query/parse-ir.ts +11 -9
  41. package/src/relation.ts +3 -28
  42. package/dist/exhume.d.ts +0 -143
  43. package/dist/exhume.d.ts.map +0 -1
  44. package/dist/exhume.js +0 -166
  45. package/dist/exhume.js.map +0 -1
  46. package/src/exhume.ts +0 -267
package/src/native.ts CHANGED
@@ -22,28 +22,46 @@ import type { SchemaSpec, ValueSpec, ValueTypeSpec } from "#spec.ts"
22
22
  /** The opaque database handle (owns the LMDB environment + exclusive lock). */
23
23
  type DbHandle = { readonly __brand: "bumbledb.db" }
24
24
 
25
- /** One live MVCC read snapshot. */
26
- type SnapshotHandle = { readonly __brand: "bumbledb.snapshot" }
25
+ /** One live borrowed instance valid only inside a read callback. */
26
+ type InstanceHandle = { readonly __brand: "bumbledb.instance" }
27
27
 
28
- /**
29
- * One exhumed store — the read-only, theory-less open (engine 70-api.md
30
- * § exhume). Lifetimes are disposables (ruled 2026-07-23, R12):
31
- * `exhumeClose` is the deterministic teardown the SDK's `Symbol.dispose`
32
- * rides — releasing the environment (and the store's exclusive lock)
33
- * scope-shaped, never a GC race; the engine-side drop remains the
34
- * reclamation-only backstop for a collected-but-undisposed handle.
35
- */
36
- type ExhumeHandle = { readonly __brand: "bumbledb.exhume" }
28
+ /** One cloneable generation witness. May outlive the read that minted it. */
29
+ type WitnessHandle = { readonly __brand: "bumbledb.witness" }
30
+
31
+ /** One unproved heap builder. Spent by `instanceBuilderAdmit` / close. */
32
+ type BuilderHandle = { readonly __brand: "bumbledb.builder" }
33
+
34
+ /** One admitted heap instance. */
35
+ type OwnedHandle = { readonly __brand: "bumbledb.owned" }
37
36
 
38
37
  /**
39
38
  * One live write transaction — the submitted delta with the engine's
40
- * final-state point-read view. Spent by `txCommit`/`txAbort`.
39
+ * final-state point-read view. Valid only inside a write callback.
41
40
  */
42
41
  type TxHandle = { readonly __brand: "bumbledb.tx" }
43
42
 
44
43
  /** One prepared query (plan pinned at prepare). */
45
44
  type PreparedHandle = { readonly __brand: "bumbledb.prepared" }
46
45
 
46
+ /**
47
+ * Engine mutation report as it crosses napi: both counts are engine
48
+ * values, never reconstructed from JS length.
49
+ */
50
+ interface WireMutationReport {
51
+ readonly submitted: bigint
52
+ readonly changed: bigint
53
+ }
54
+
55
+ /**
56
+ * Engine fresh-id range as it crosses napi. Empty cannot yield a start —
57
+ * `start` is a minted id only on the nonempty arm. (C wires empty as
58
+ * `BDB_FRESH_RANGE_TAG_EMPTY`. The JS wire is `{ empty: true }`, not that
59
+ * C sentinel.)
60
+ */
61
+ type WireFreshRange =
62
+ | { readonly empty: true }
63
+ | { readonly empty: false; readonly start: bigint; readonly endExclusive: bigint }
64
+
47
65
  /** A half-open interval `[start, end)` as it crosses the boundary. */
48
66
  interface IntervalValue {
49
67
  readonly start: bigint
@@ -118,15 +136,14 @@ interface RuleIr {
118
136
  readonly conditions: readonly ConditionTreeIr[]
119
137
  }
120
138
 
121
- /** One find term (mirrors `ir::FindTerm`). Count carries no `over`; folds require it. */
139
+ /** One find term (mirrors `ir::FindTerm`). Count is nullary; pack and folds carry `over`. */
122
140
  type FoldOpIr = { readonly kind: "sum" } | { readonly kind: "min" } | { readonly kind: "max" }
123
141
 
124
- type ArgOpIr = FoldOpIr | { readonly kind: "pack" }
125
-
126
142
  type FindTermIr =
127
143
  | { readonly kind: "var"; readonly var: number }
128
- | { readonly kind: "aggregate"; readonly op: { readonly kind: "count" } }
129
- | { readonly kind: "aggregate"; readonly op: ArgOpIr; readonly over: number }
144
+ | { readonly kind: "count" }
145
+ | { readonly kind: "aggregate"; readonly op: FoldOpIr; readonly over: number }
146
+ | { readonly kind: "pack"; readonly over: number }
130
147
  | { readonly kind: "measure"; readonly var: number }
131
148
  | { readonly kind: "aggregateMeasure"; readonly op: FoldOpIr; readonly over: number }
132
149
 
@@ -278,14 +295,22 @@ type Violation =
278
295
  }
279
296
 
280
297
  /**
281
- * `dbCreate`/`dbOpen`'s domain outcome. `schemaError` spans both spec
298
+ * `dbCreate`'s domain outcome. Admission is `accepted` / `rejected`.
299
+ * Declaration-boundary refusals ride as their own tags (not theory
300
+ * admission) and the SDK throws them.
301
+ */
302
+ type CreateResult =
303
+ | { readonly tag: "accepted"; readonly db: DbHandle }
304
+ | { readonly tag: "rejected"; readonly violations: readonly Violation[] }
305
+ | { readonly tag: "schemaError"; readonly message: string }
306
+ | { readonly tag: "newtypeMismatch"; readonly message: string }
307
+
308
+ /**
309
+ * `dbOpen`'s domain outcome. `schemaError` spans both spec
282
310
  * resolution (unresolvable names, banned spellings — every issue in one
283
311
  * message) and schema validation at the declaration boundary;
284
- * `newtypeMismatch` is the coherence wall's own kind — a spec whose
285
- * statement pairs faces with disagreeing newtype labels (the engine twin
286
- * of the schema-level class wall; unreachable through the typed builder,
287
- * which computes every label from the laws, so only a raw spec can reach
288
- * it); `fingerprintMismatch` is `dbOpen`'s stored-theory refusal.
312
+ * `newtypeMismatch` is the coherence wall's own kind; `fingerprintMismatch`
313
+ * is `dbOpen`'s stored-theory refusal.
289
314
  */
290
315
  type DbOpenResult =
291
316
  | { readonly ok: true; readonly db: DbHandle }
@@ -296,46 +321,23 @@ type DbOpenResult =
296
321
  }
297
322
 
298
323
  /**
299
- * `dbExhume`'s domain outcome: the live exhume handle, or one of the three
300
- * adoption-era refusals as data — `descriptorMissing` (the store predates
301
- * self-describing stores and has not been adopted; the remedy is one
302
- * fingerprint-matching `dbOpen` under the creating schema),
303
- * `formatMismatch`, and `corruption` (the persisted descriptor fails its
304
- * integrity gates). Genuine failures — a missing path, a held exclusive
305
- * lock — throw.
306
- */
307
- type ExhumeResult =
308
- | { readonly ok: true; readonly exhume: ExhumeHandle }
309
- | {
310
- readonly ok: false
311
- readonly kind: "descriptorMissing" | "formatMismatch" | "corruption"
312
- readonly message: string
313
- }
314
-
315
- /**
316
- * `dbWriteFrom`'s domain outcome: the live witnessed transaction, or the
317
- * typed stale-premise verdict (a state-changing commit landed after the
318
- * witness snapshot; retry policy is host-side).
324
+ * `dbWrite` / `dbWriteFrom` native outcome. The SDK attaches the callback
325
+ * return onto the accepted arm. Moved is data, never an error kind.
319
326
  */
320
- type WriteFromResult =
321
- | { readonly ok: true; readonly tx: TxHandle }
322
- | {
323
- readonly ok: false
324
- readonly kind: "generationMoved"
325
- readonly witnessed: bigint
326
- readonly current: bigint
327
- }
327
+ type NativeWriteOutcome =
328
+ | { readonly tag: "accepted"; readonly generation: bigint }
329
+ | { readonly tag: "rejected"; readonly violations: readonly Violation[] }
330
+ | { readonly tag: "abandoned" }
331
+ | { readonly tag: "moved"; readonly witnessed: bigint; readonly current: bigint }
328
332
 
329
333
  /**
330
- * `txCommit`'s domain outcome: the committed generation, or the COMPLETE
331
- * violation set (every violated statement cited once, per direction for a
332
- * containment, in materialized statement order).
334
+ * Builder `admit` native outcome.
333
335
  */
334
- type CommitResult =
335
- | { readonly ok: true; readonly generation: bigint }
336
- | { readonly ok: false; readonly violations: readonly Violation[] }
336
+ type AdmitResult =
337
+ | { readonly tag: "accepted"; readonly value: OwnedHandle }
338
+ | { readonly tag: "rejected"; readonly violations: readonly Violation[] }
337
339
 
338
- /** `dbPrepare`'s domain outcome (IR roster errors are data). */
340
+ /** `dbPrepare`/`instancePrepare`'s domain outcome (IR roster errors are data). */
339
341
  type PrepareResult =
340
342
  | { readonly ok: true; readonly prepared: PreparedHandle }
341
343
  | { readonly ok: false; readonly kind: "irError"; readonly message: string }
@@ -357,13 +359,37 @@ interface Staleness {
357
359
  readonly maxRatio: number
358
360
  }
359
361
 
360
- /**
361
- * `dbSnapshot`'s reply: the live handle WITH its witnessed generation —
362
- * one crossing carries both (read inside the snapshot's own transaction,
363
- * the race-closing rule of 50-storage.md), so no second `dbGeneration`
364
- * call exists to pay or defend (finding 016's bridge shape).
365
- */
366
- type SnapshotOpened = { readonly ok: true; readonly snapshot: SnapshotHandle; readonly generation: bigint }
362
+ type ErrorFamilyKind =
363
+ | "formatMismatch"
364
+ | "schemaMismatch"
365
+ | "alreadyInitialized"
366
+ | "destinationExists"
367
+ | "publishedButUnsynced"
368
+ | "environmentLocked"
369
+ | "io"
370
+ | "lmdb"
371
+ | "readersFull"
372
+ | "schema"
373
+ | "validation"
374
+ | "factShape"
375
+ | "freshExhausted"
376
+ | "closedRelationWrite"
377
+ | "commitSync"
378
+ | "transactionPoisoned"
379
+ | "foreignPrepared"
380
+ | "foreignWitness"
381
+ | "param"
382
+ | "measureOfRay"
383
+ | "capacityRayMeasure"
384
+ | "derivedBudgetExceeded"
385
+ | "overflow"
386
+ | "resultBytesOverflow"
387
+ | "corruption"
388
+
389
+ type AdmissionTag = "accepted" | "rejected"
390
+ type WriteTag = "accepted" | "rejected" | "abandoned" | "moved"
391
+ type OpenKind = "schemaError" | "newtypeMismatch" | "fingerprintMismatch"
392
+ type PrepareKind = "irError"
367
393
 
368
394
  /**
369
395
  * The plan-as-data report (ruled 2026-07-23, R13): the engine's
@@ -394,16 +420,15 @@ interface Native {
394
420
  engineVersion(): string
395
421
 
396
422
  /**
397
- * Creates a fresh DURABLE store at `path` (frozen ruling 3: no ephemeral
398
- * kind crosses this bridge). Refuses an already-initialized directory
399
- * (throws); schema failures return as data.
423
+ * Creates a fresh durable store at `path`. Refuses an already-initialized
424
+ * directory (throws); schema failures return as data.
400
425
  */
401
- dbCreate(path: string, spec: SchemaSpec): DbOpenResult
426
+ dbCreate(path: string, spec: SchemaSpec): Promise<CreateResult>
402
427
  /**
403
- * Opens an existing durable store, verifying format version, store
404
- * kind, and schema fingerprint (`fingerprintMismatch` as data).
428
+ * Opens an existing durable store, verifying format version and
429
+ * schema fingerprint (`fingerprintMismatch` as data).
405
430
  */
406
- dbOpen(path: string, spec: SchemaSpec): DbOpenResult
431
+ dbOpen(path: string, spec: SchemaSpec): Promise<DbOpenResult>
407
432
  /**
408
433
  * Closes the handle. Dependent handles each hold the engine alive; the
409
434
  * environment (and its exclusive lock) releases when the last closes.
@@ -422,87 +447,59 @@ interface Native {
422
447
  dbFingerprint(db: DbHandle): string
423
448
  /**
424
449
  * The current committed generation — diagnostics only. The write-side
425
- * witness is always the SNAPSHOT handle (`dbWriteFrom`), never this
426
- * integer: an integer witness would be a claim a caller could fabricate
427
- * or stale-cache (the engine's recorded refusal).
450
+ * witness is always a {@link WitnessHandle}, never this integer.
428
451
  */
429
452
  dbGeneration(db: DbHandle): bigint
453
+ /** Publishes an admitted heap instance at `path` without re-judgment. */
454
+ dbFromInstance(path: string, instance: OwnedHandle): Promise<DbHandle>
430
455
 
431
456
  /**
432
- * Opens a store FROM ITS OWN PERSISTED DESCRIPTOR (the read-only,
433
- * theory-less open; engine 70-api.md § exhume) — no schema crosses in.
434
- * The three adoption-era refusals return as data ({@link ExhumeResult});
435
- * genuine failures throw. The handle's deterministic teardown is
436
- * `exhumeClose` (R12); GC reclamation remains the backstop only.
437
- */
438
- dbExhume(path: string): ExhumeResult
439
- /**
440
- * Closes the exhume handle, releasing its environment (and the store's
441
- * exclusive lock) deterministically — the native teardown under the
442
- * SDK's `Symbol.dispose` (ruled 2026-07-23, R12: lifetimes are
443
- * disposables, never `close()` methods to remember).
444
- */
445
- exhumeClose(exhume: ExhumeHandle): void
446
- /**
447
- * The exhumed store's persisted schema as manifest-shaped data — the
448
- * engine's own manifest rendering of the STORED descriptor: relations
449
- * in engine-id order, sealed field lists (a closed relation opens with
450
- * the synthetic (`id`, u64) handle field) with structural value types,
451
- * and closed-relation rosters.
457
+ * Runs `callback` synchronously inside the engine read lease. The
458
+ * instance handle is invalid after the callback returns; the witness
459
+ * handle is a clone and may escape.
452
460
  */
453
- exhumeDescriptor(exhume: ExhumeHandle): Manifest
454
- /**
455
- * Full-relation export by NAME in row-id order, values marshaled per
456
- * the STORED descriptor (str already resolved through `_dict` inside
457
- * the engine; a closed relation scans its sealed roster). Each call is
458
- * one self-contained snapshot read; an unknown relation name throws.
459
- */
460
- exhumeScan(exhume: ExhumeHandle, relationName: string): FactValue[][]
461
-
462
- /**
463
- * Opens one MVCC read snapshot as a live handle, returned WITH its
464
- * witnessed generation — one crossing carries both (finding 016), so
465
- * no separate `dbGeneration` call (with its own transient read
466
- * transaction and fault-pairing close branch) exists on this path.
467
- */
468
- dbSnapshot(db: DbHandle): SnapshotOpened
469
- /** Closes the snapshot, releasing its LMDB reader slot. */
470
- snapshotClose(snap: SnapshotHandle): void
471
- /** Full-relation export in row-id order (one row per fact). */
472
- snapshotScan(snap: SnapshotHandle, relationId: number): FactValue[][]
473
- /** Committed-state membership of one fact (sealed field order). */
474
- snapshotContains(snap: SnapshotHandle, relationId: number, values: readonly FactValue[]): boolean
475
- /**
476
- * Committed-state point lookup through a key statement (`keyValues` in
477
- * the statement's projection order); `null` on a miss.
478
- */
479
- snapshotGet(
480
- snap: SnapshotHandle,
461
+ dbRead<R>(db: DbHandle, callback: (instance: InstanceHandle, witness: WitnessHandle) => R): R
462
+ instanceGeneration(instance: InstanceHandle): bigint
463
+ instanceScan(instance: InstanceHandle, relationId: number): FactValue[][]
464
+ instanceContains(instance: InstanceHandle, relationId: number, values: readonly FactValue[]): boolean
465
+ instanceGet(
466
+ instance: InstanceHandle,
481
467
  relationId: number,
482
468
  keyStatementId: number,
483
469
  keyValues: readonly FactValue[]
484
470
  ): FactValue[] | null
471
+ instancePrepare(instance: InstanceHandle, query: ParsedQuery): PrepareResult
472
+ witnessClose(witness: WitnessHandle): void
485
473
 
486
474
  /**
487
- * Begins a write transaction: the submitted delta. One write
488
- * transaction may be open per db handle at a time (single-writer
489
- * engine; a second begin throws rather than deadlocking the process).
475
+ * Runs `callback` synchronously inside the engine write region.
476
+ * Return `true` to commit, `false` to abandon. Nested writes throw.
490
477
  */
491
- dbWriteBegin(db: DbHandle): TxHandle
478
+ dbWrite(db: DbHandle, callback: (tx: TxHandle) => boolean): NativeWriteOutcome
492
479
  /**
493
- * Begins a WITNESSED write transaction: commits only if no
494
- * state-changing commit landed since `snap` was taken —
495
- * `generationMoved` as data otherwise (the optimistic
496
- * read-compute-write loop's entry; retry policy stays host-side).
480
+ * Witnessed write: `moved` is data when the store advanced since the
481
+ * witness was minted. The callback does not run on that arm.
497
482
  */
498
- dbWriteFrom(db: DbHandle, snap: SnapshotHandle): WriteFromResult
483
+ dbWriteFrom(db: DbHandle, witness: WitnessHandle, callback: (tx: TxHandle) => boolean): NativeWriteOutcome
499
484
  /**
500
- * Records an insert into the delta; `true` iff the final state changed.
501
- * Nothing is judged until commit; shape violations throw typed.
485
+ * Records a collection of inserts into the delta; returns the engine
486
+ * `{ submitted, changed }` report. `rows` is an array of value-arrays
487
+ * in sealed field order. Empty is lawful and still a mutation (poison
488
+ * is observed). Nothing is judged until commit; shape violations throw typed.
502
489
  */
503
- txInsert(tx: TxHandle, relationId: number, values: readonly FactValue[]): boolean
504
- /** Records a delete into the delta; `true` iff the final state changed. */
505
- txDelete(tx: TxHandle, relationId: number, values: readonly FactValue[]): boolean
490
+ txInsert(tx: TxHandle, relationId: number, rows: readonly (readonly FactValue[])[]): WireMutationReport
491
+ /**
492
+ * Records a collection of inserts from per-column arrays in sealed
493
+ * field order — the column transport, same parse-all-first batch as
494
+ * {@link Native.txInsert}.
495
+ */
496
+ txInsertColumns(
497
+ tx: TxHandle,
498
+ relationId: number,
499
+ columns: readonly (readonly FactValue[])[]
500
+ ): WireMutationReport
501
+ /** Records a collection of deletes; returns the engine `{ submitted, changed }` report. */
502
+ txDelete(tx: TxHandle, relationId: number, rows: readonly (readonly FactValue[])[]): WireMutationReport
506
503
  /**
507
504
  * Final-state membership (base + pending delta — the exact view the
508
505
  * commit judgment judges; check-then-act is race-free by construction).
@@ -511,20 +508,10 @@ interface Native {
511
508
  /** Final-state point lookup through a key statement; `null` on a miss. */
512
509
  txGet(tx: TxHandle, relationId: number, keyStatementId: number, keyValues: readonly FactValue[]): FactValue[] | null
513
510
  /**
514
- * Mints the next fresh value for `(relationId, fieldId)` and returns it
515
- * — the engine's alloc-then-insert dyn-lane mint (there is no
516
- * insert-with-omitted-fields spelling; include the minted id in the
517
- * full row).
518
- */
519
- txAlloc(tx: TxHandle, relationId: number, fieldId: number): bigint
520
- /**
521
- * Commits the delta: every dependency statement judged against the
522
- * final state; a rejection carries the complete violation rendering.
523
- * The handle is spent either way.
511
+ * Mints `count` consecutive fresh values for `(relationId, fieldId)`.
512
+ * `count === 0n` is empty and does not yield a start.
524
513
  */
525
- txCommit(tx: TxHandle): CommitResult
526
- /** Aborts the delta (LMDB was never touched). The handle is spent. */
527
- txAbort(tx: TxHandle): void
514
+ txReserve(tx: TxHandle, relationId: number, fieldId: number, count: bigint): WireFreshRange
528
515
 
529
516
  /**
530
517
  * Prepares a query (IR as data, ids only; plan pinned at prepare).
@@ -532,24 +519,64 @@ interface Native {
532
519
  */
533
520
  dbPrepare(db: DbHandle, query: ParsedQuery): PrepareResult
534
521
  /**
535
- * Executes against a snapshot with positional params. One-copy owned
522
+ * Executes against a live instance with positional params. One-copy owned
536
523
  * rows out, column order = the query's head order; answers are a set
537
524
  * — the host sorts.
538
525
  */
539
- preparedExecute(prepared: PreparedHandle, snap: SnapshotHandle, params: readonly QueryParam[]): FactValue[][]
526
+ preparedExecute(prepared: PreparedHandle, instance: InstanceHandle, params: readonly QueryParam[]): FactValue[][]
540
527
  /**
541
528
  * Plan introspection as data (ruled 2026-07-23, R13): runs the prepared
542
- * query against the snapshot with counting instrumentation (the
543
- * engine's `Snapshot::profile`, ANALYZE semantics) and returns the
544
- * structured stats — plan sections and counters as plain values.
545
- * Scalar params only (the engine's profile entry has no param-set
546
- * spelling).
529
+ * query against a store read with counting instrumentation and returns
530
+ * the structured stats. Store-read only.
547
531
  */
548
- preparedExplain(prepared: PreparedHandle, snap: SnapshotHandle, params: readonly QueryParam[]): Explain
549
- /** The pull-based plan-drift signal against a snapshot. */
550
- preparedStaleness(prepared: PreparedHandle, snap: SnapshotHandle): Staleness
532
+ preparedExplain(prepared: PreparedHandle, instance: InstanceHandle, params: readonly QueryParam[]): Explain
533
+ /** The pull-based plan-drift signal against a store read. */
534
+ preparedStaleness(prepared: PreparedHandle, instance: InstanceHandle): Staleness
551
535
  /** Releases the prepared query. */
552
536
  preparedClose(prepared: PreparedHandle): void
537
+
538
+ instanceBuilderNew(spec: SchemaSpec): BuilderHandle
539
+ instanceBuilderLoad(
540
+ builder: BuilderHandle,
541
+ relationId: number,
542
+ rows: readonly (readonly FactValue[])[]
543
+ ): WireMutationReport
544
+ instanceBuilderLoadColumns(
545
+ builder: BuilderHandle,
546
+ relationId: number,
547
+ columns: readonly (readonly FactValue[])[]
548
+ ): WireMutationReport
549
+ instanceBuilderDelete(
550
+ builder: BuilderHandle,
551
+ relationId: number,
552
+ rows: readonly (readonly FactValue[])[]
553
+ ): WireMutationReport
554
+ instanceBuilderReserve(
555
+ builder: BuilderHandle,
556
+ relationId: number,
557
+ fieldId: number,
558
+ count: bigint
559
+ ): WireFreshRange
560
+ instanceBuilderContains(builder: BuilderHandle, relationId: number, values: readonly FactValue[]): boolean
561
+ instanceBuilderGet(
562
+ builder: BuilderHandle,
563
+ relationId: number,
564
+ keyStatementId: number,
565
+ keyValues: readonly FactValue[]
566
+ ): FactValue[] | null
567
+ instanceBuilderClose(builder: BuilderHandle): void
568
+ instanceBuilderAdmit(builder: BuilderHandle): Promise<AdmitResult>
569
+ ownedInstanceClose(instance: OwnedHandle): void
570
+ ownedScan(instance: OwnedHandle, relationId: number): FactValue[][]
571
+ ownedContains(instance: OwnedHandle, relationId: number, values: readonly FactValue[]): boolean
572
+ ownedGet(
573
+ instance: OwnedHandle,
574
+ relationId: number,
575
+ keyStatementId: number,
576
+ keyValues: readonly FactValue[]
577
+ ): FactValue[] | null
578
+ ownedPrepare(instance: OwnedHandle, query: ParsedQuery): PrepareResult
579
+ ownedExecute(prepared: PreparedHandle, instance: OwnedHandle, params: readonly QueryParam[]): FactValue[][]
553
580
  }
554
581
 
555
582
  /**
@@ -626,38 +653,79 @@ function loadNativeBinding(platform: string, arch: string): Native {
626
653
  const native: Native = loadNativeBinding(process.platform, process.arch)
627
654
 
628
655
  /**
629
- * The bridge guard — THE one wrapper every native call crosses (db.ts and
630
- * exhume.ts both import it): runs one native call and wraps anything it
656
+ * Engine throw identity: a real `Error` carrying `kind` from the
657
+ * `ErrorFamily` table, or a leftover `{ kind, message }` object.
658
+ */
659
+ function isEngineThrow(value: unknown): value is { kind: ErrorFamilyKind; message: string } {
660
+ if (typeof value !== "object" || value === null) {
661
+ return false
662
+ }
663
+ const rec = value as { kind?: unknown; message?: unknown }
664
+ return typeof rec.kind === "string" && typeof rec.message === "string"
665
+ }
666
+
667
+ function errorFromThrow(caught: unknown): Error {
668
+ if (caught instanceof Error) {
669
+ return caught
670
+ }
671
+ if (isEngineThrow(caught)) {
672
+ const error = errors.new(`bumbledb ${caught.kind}: ${caught.message}`)
673
+ Object.defineProperty(error, "kind", { value: caught.kind, enumerable: true })
674
+ return error
675
+ }
676
+ return errors.new(String(caught))
677
+ }
678
+
679
+ /**
680
+ * The bridge guard — THE one wrapper every native call crosses (db.ts
681
+ * imports it): runs one native call and wraps anything it
631
682
  * throws, so marshal-shape refusals and handle-lifecycle refusals cross as
632
- * genuine typed failures, never bare foreign errors.
683
+ * genuine typed failures, never bare foreign errors. Engine throws keep
684
+ * their forced kind.
633
685
  */
634
686
  function bridged<T>(context: string, run: () => T): T {
635
- const result = errors.trySync(run)
636
- if (result.error) {
637
- throw errors.wrap(result.error, context)
687
+ try {
688
+ return run()
689
+ } catch (caught) {
690
+ const inner = errorFromThrow(caught)
691
+ throw errors.wrap(inner, `${context}: ${inner.message}`)
692
+ }
693
+ }
694
+
695
+ /**
696
+ * The async twin of {@link bridged}: every control-plane native is an
697
+ * `AsyncTask` Promise, and this is the one wrapper those awaits cross.
698
+ */
699
+ async function bridgedAsync<T>(context: string, run: () => Promise<T>): Promise<T> {
700
+ try {
701
+ return await run()
702
+ } catch (caught) {
703
+ const inner = errorFromThrow(caught)
704
+ throw errors.wrap(inner, `${context}: ${inner.message}`)
638
705
  }
639
- return result.data
640
706
  }
641
707
 
642
708
  export type {
709
+ AdmissionTag,
710
+ AdmitResult,
643
711
  AggOpIr,
644
- ArgOpIr,
645
712
  AtomIr,
646
713
  AtomSourceIr,
714
+ BuilderHandle,
647
715
  CmpOpIr,
648
- CommitResult,
649
716
  ComparisonIr,
650
717
  ConditionTreeIr,
718
+ CreateResult,
651
719
  DbHandle,
652
720
  DbOpenResult,
653
- ExhumeHandle,
654
- ExhumeResult,
721
+ ErrorFamilyKind,
655
722
  Explain,
656
723
  FactValue,
657
724
  FindTermIr,
658
725
  FoldOpIr,
659
726
  HeadOpIr,
660
727
  HeadTermIr,
728
+ InstanceHandle,
661
729
  InteriorIr,
662
730
  IntervalValue,
663
731
  Manifest,
@@ -666,16 +734,18 @@ export type {
666
734
  ManifestRow,
667
735
  ManifestStatement,
668
736
  Native,
737
+ NativeWriteOutcome,
669
738
  OccurrenceDrift,
739
+ OpenKind,
740
+ OwnedHandle,
670
741
  ParsedQuery,
671
742
  PreparedHandle,
743
+ PrepareKind,
672
744
  PrepareResult,
673
745
  QueryIr,
674
746
  QueryParam,
675
747
  RecIr,
676
748
  RuleIr,
677
- SnapshotHandle,
678
- SnapshotOpened,
679
749
  Staleness,
680
750
  StatementKindTag,
681
751
  TaggedValue,
@@ -683,6 +753,9 @@ export type {
683
753
  TxHandle,
684
754
  Violation,
685
755
  ViolationFact,
686
- WriteFromResult
756
+ WireFreshRange,
757
+ WireMutationReport,
758
+ WitnessHandle,
759
+ WriteTag
687
760
  }
688
- export { bridged, loadNativeBinding, native, SHIPPED_PLATFORMS }
761
+ export { bridged, bridgedAsync, errorFromThrow, loadNativeBinding, native, SHIPPED_PLATFORMS }
package/src/query/find.ts CHANGED
@@ -24,19 +24,25 @@ import type { IntervalVarOk, NumericVarOk, OrderVarOk } from "#query/atom.ts"
24
24
  import type { AnyVar, Duration, MintSlotOf } from "#query/scope.ts"
25
25
 
26
26
  /** One aggregate operator name of the find vocabulary. */
27
- type AggOpName = "count" | "sum" | "min" | "max" | "pack"
27
+ type FoldOpName = "sum" | "min" | "max" | "pack"
28
+ type AggOpName = "count" | FoldOpName
29
+
30
+ /** Nullary count: no `over` exists to inhabit. */
31
+ interface CountAgg {
32
+ readonly agg: "count"
33
+ }
28
34
 
29
35
  /**
30
- * One aggregate find VALUE: the op and the variable (or measure) it folds
31
- * BY REFERENCE. The variable's own descriptor types the result.
36
+ * One fold aggregate: the op and the variable (or measure) it folds
37
+ * BY REFERENCE. Count is [`CountAgg`], not this type with `undefined`.
32
38
  */
33
- interface Agg<Op extends AggOpName, Over extends AnyVar | Duration | undefined> {
39
+ interface Agg<Op extends FoldOpName, Over extends AnyVar | Duration> {
34
40
  readonly agg: Op
35
41
  readonly over: Over
36
42
  }
37
43
 
38
44
  /** Any aggregate find value. */
39
- type AnyAgg = Agg<AggOpName, AnyVar | Duration | undefined>
45
+ type AnyAgg = CountAgg | Agg<FoldOpName, AnyVar | Duration>
40
46
 
41
47
  /** One find entry: a projected variable, the measure, or an aggregate. */
42
48
  type FindEntry = AnyVar | Duration | AnyAgg
@@ -44,17 +50,14 @@ type FindEntry = AnyVar | Duration | AnyAgg
44
50
  /** The `find` record: column name → find entry. Keys ARE the answer columns. */
45
51
  type FindShape = Readonly<Record<string, FindEntry>>
46
52
 
47
- /** Builds one aggregate value. */
48
- function aggregate<Op extends AggOpName, Over extends AnyVar | Duration | undefined>(
49
- op: Op,
50
- over: Over
51
- ): Agg<Op, Over> {
53
+ /** Builds one fold aggregate value. */
54
+ function aggregate<Op extends FoldOpName, Over extends AnyVar | Duration>(op: Op, over: Over): Agg<Op, Over> {
52
55
  return Object.freeze({ agg: op, over })
53
56
  }
54
57
 
55
58
  /** Nullary count: |the group's set of distinct full bindings|, `bigint`. */
56
- function count(): Agg<"count", undefined> {
57
- return aggregate("count", undefined)
59
+ function count(): CountAgg {
60
+ return Object.freeze({ agg: "count" })
58
61
  }
59
62
 
60
63
  /**
@@ -119,7 +122,7 @@ type FindEntryOk<E> = E extends AnyVar
119
122
  ? true
120
123
  : E extends Duration<infer V extends AnyVar>
121
124
  ? IntervalVarOk<V>
122
- : E extends Agg<"count", undefined>
125
+ : E extends CountAgg
123
126
  ? true
124
127
  : E extends Agg<"sum", infer O>
125
128
  ? SumOverOk<O>
@@ -151,7 +154,7 @@ type FindValue<E> = E extends AnyVar
151
154
  ? Infer<E["field"]>
152
155
  : E extends Duration<AnyVar>
153
156
  ? bigint
154
- : E extends Agg<"count", undefined>
157
+ : E extends CountAgg
155
158
  ? bigint
156
159
  : E extends Agg<"sum" | "min" | "max", infer O>
157
160
  ? O extends AnyVar
@@ -179,10 +182,12 @@ export type {
179
182
  AnyAgg,
180
183
  CheckFind,
181
184
  CheckRecFind,
185
+ CountAgg,
182
186
  FindEntry,
183
187
  FindEntryOk,
184
188
  FindShape,
185
189
  FindValue,
190
+ FoldOpName,
186
191
  HeadRecordOf,
187
192
  RowOfFind
188
193
  }