@bjornpagen/bumbledb 0.14.0 → 0.17.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 (108) hide show
  1. package/COOKBOOK.md +58 -62
  2. package/README.md +82 -56
  3. package/dist/capacity.d.ts +24 -136
  4. package/dist/capacity.d.ts.map +1 -1
  5. package/dist/capacity.js +18 -40
  6. package/dist/capacity.js.map +1 -1
  7. package/dist/closed.d.ts +0 -156
  8. package/dist/closed.d.ts.map +1 -1
  9. package/dist/closed.js +0 -104
  10. package/dist/closed.js.map +1 -1
  11. package/dist/db.d.ts +93 -290
  12. package/dist/db.d.ts.map +1 -1
  13. package/dist/db.js +713 -556
  14. package/dist/db.js.map +1 -1
  15. package/dist/face.d.ts +0 -133
  16. package/dist/face.d.ts.map +1 -1
  17. package/dist/face.js +0 -33
  18. package/dist/face.js.map +1 -1
  19. package/dist/fields.d.ts +1 -145
  20. package/dist/fields.d.ts.map +1 -1
  21. package/dist/fields.js +2 -91
  22. package/dist/fields.js.map +1 -1
  23. package/dist/index.d.ts +13 -23
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +11 -20
  26. package/dist/index.js.map +1 -1
  27. package/dist/law.d.ts +111 -93
  28. package/dist/law.d.ts.map +1 -1
  29. package/dist/law.js +23 -27
  30. package/dist/law.js.map +1 -1
  31. package/dist/lower.d.ts +9 -35
  32. package/dist/lower.d.ts.map +1 -1
  33. package/dist/lower.js +8 -53
  34. package/dist/lower.js.map +1 -1
  35. package/dist/marshal.d.ts +0 -65
  36. package/dist/marshal.d.ts.map +1 -1
  37. package/dist/marshal.js +0 -72
  38. package/dist/marshal.js.map +1 -1
  39. package/dist/native.d.ts +97 -390
  40. package/dist/native.d.ts.map +1 -1
  41. package/dist/native.js +39 -61
  42. package/dist/native.js.map +1 -1
  43. package/dist/query/atom.d.ts +10 -276
  44. package/dist/query/atom.d.ts.map +1 -1
  45. package/dist/query/atom.js +1 -96
  46. package/dist/query/atom.js.map +1 -1
  47. package/dist/query/find.d.ts +10 -76
  48. package/dist/query/find.d.ts.map +1 -1
  49. package/dist/query/find.js +0 -30
  50. package/dist/query/find.js.map +1 -1
  51. package/dist/query/lower.d.ts +64 -146
  52. package/dist/query/lower.d.ts.map +1 -1
  53. package/dist/query/lower.js +19 -256
  54. package/dist/query/lower.js.map +1 -1
  55. package/dist/query/parse-ir.d.ts +0 -7
  56. package/dist/query/parse-ir.d.ts.map +1 -1
  57. package/dist/query/parse-ir.js +1 -13
  58. package/dist/query/parse-ir.js.map +1 -1
  59. package/dist/query/run.d.ts +0 -36
  60. package/dist/query/run.d.ts.map +1 -1
  61. package/dist/query/run.js +0 -44
  62. package/dist/query/run.js.map +1 -1
  63. package/dist/query/scope.d.ts +24 -180
  64. package/dist/query/scope.d.ts.map +1 -1
  65. package/dist/query/scope.js +2 -66
  66. package/dist/query/scope.js.map +1 -1
  67. package/dist/relation.d.ts +2 -50
  68. package/dist/relation.d.ts.map +1 -1
  69. package/dist/relation.js +2 -37
  70. package/dist/relation.js.map +1 -1
  71. package/dist/schema.d.ts +13 -63
  72. package/dist/schema.d.ts.map +1 -1
  73. package/dist/schema.js +118 -92
  74. package/dist/schema.js.map +1 -1
  75. package/dist/spec.d.ts +1 -140
  76. package/dist/spec.d.ts.map +1 -1
  77. package/dist/spec.js +1 -68
  78. package/dist/spec.js.map +1 -1
  79. package/dist/statements.d.ts +6 -137
  80. package/dist/statements.d.ts.map +1 -1
  81. package/dist/statements.js +16 -119
  82. package/dist/statements.js.map +1 -1
  83. package/package.json +3 -3
  84. package/src/capacity.ts +26 -140
  85. package/src/closed.ts +5 -206
  86. package/src/db.ts +997 -854
  87. package/src/face.ts +0 -142
  88. package/src/fields.ts +4 -172
  89. package/src/index.ts +32 -35
  90. package/src/law.ts +201 -129
  91. package/src/lower.ts +8 -53
  92. package/src/marshal.ts +1 -85
  93. package/src/native.ts +192 -413
  94. package/src/query/atom.ts +26 -313
  95. package/src/query/find.ts +24 -110
  96. package/src/query/lower.ts +132 -377
  97. package/src/query/parse-ir.ts +1 -14
  98. package/src/query/run.ts +0 -45
  99. package/src/query/scope.ts +25 -186
  100. package/src/relation.ts +2 -66
  101. package/src/schema.ts +143 -122
  102. package/src/spec.ts +1 -160
  103. package/src/statements.ts +22 -174
  104. package/dist/exhume.d.ts +0 -143
  105. package/dist/exhume.d.ts.map +0 -1
  106. package/dist/exhume.js +0 -166
  107. package/dist/exhume.js.map +0 -1
  108. package/src/exhume.ts +0 -267
package/COOKBOOK.md CHANGED
@@ -2,11 +2,9 @@
2
2
 
3
3
  The bumbledb engine's 32 cookbook recipes (`bumbledb/docs/cookbook.md`),
4
4
  translated to this SDK's structural API. **This document is illustrative,
5
- never normative**: where a recipe and an engine architecture chapter disagree,
6
- the chapter wins (`docs/architecture/README.md` rule 5) — the SDK is the same
7
- theory in another skin, and the engine cookbook's deference chain
8
- (`10-data-model.md`, `30-dependencies.md`, `20-query-ir.md`, `70-api.md`)
9
- applies here unchanged.
5
+ never normative**: the compiled tests are the authority every block here
6
+ is executed by the sync suitesand the SDK is the same theory in another
7
+ skin.
10
8
 
11
9
  Every `ts` fence below compiles VERBATIM against the current SDK —
12
10
  `ts/test/cookbook-doc.test.ts` extracts each recipe's fences from this
@@ -36,22 +34,17 @@ import {
36
34
  Db,
37
35
  type Infer,
38
36
  abandon,
39
- allen,
40
37
  bool,
41
38
  bytes,
42
39
  capacity,
43
40
  closed,
44
41
  contained,
45
42
  duration,
46
- eq,
47
43
  i64,
48
44
  interval,
49
45
  key,
50
- lt,
51
46
  mirrors,
52
- not,
53
47
  on,
54
- pointIn,
55
48
  query,
56
49
  ref,
57
50
  relation,
@@ -128,7 +121,7 @@ const downAt = query(Uptime).rule((r) => {
128
121
  const { service, window } = v(Outage)
129
122
  return r
130
123
  .match(Outage, { service, window })
131
- .where(pointIn(r.param("t"), window))
124
+ .where(r.pointIn(r.param("t"), window))
132
125
  .find({ service })
133
126
  })
134
127
  // overlapping an incident window (one Allen mask, no operator zoo):
@@ -136,14 +129,11 @@ const overlapping = query(Uptime).rule((r) => {
136
129
  const { service, window } = v(Outage)
137
130
  return r
138
131
  .match(Outage, { service, window })
139
- .where(allen(window, ALLEN.intersects, r.param("incident")))
132
+ .where(r.allen(window, ALLEN.intersects, r.param("incident")))
140
133
  .find({ service, window })
141
134
  })
142
- // total downtime per service (the denotation's one arithmetic):
143
- const downtime = query(Uptime).rule((r) => {
144
- const { service, window } = v(Outage)
145
- return r.match(Outage, { service, window }).find({ service, downtime: r.sum(r.duration(window)) })
146
- })
135
+ // total downtime per service is host arithmetic on the interval
136
+ // endpoints every answer row already carries (`end − start`).
147
137
  ```
148
138
 
149
139
  ## 2. Discriminated unions
@@ -225,7 +215,7 @@ const unaddressed = query(Optionality).rule((r) => {
225
215
  const { id: b } = v(Business)
226
216
  return r
227
217
  .match(Business, { id: b })
228
- .where(not(MailingAddress, { business: b }))
218
+ .where(r.not(MailingAddress, { business: b }))
229
219
  .find({ b })
230
220
  })
231
221
  ```
@@ -507,7 +497,7 @@ const playingAt = query(Playlists).rule((r) => {
507
497
  const { slot, track } = v(Slot)
508
498
  return r
509
499
  .match(Slot, { playlist: r.param("list"), slot, track })
510
- .where(pointIn(r.param("pos"), slot))
500
+ .where(r.pointIn(r.param("pos"), slot))
511
501
  .find({ track })
512
502
  })
513
503
 
@@ -623,7 +613,7 @@ const mutual = query(Graph).rule((r) => {
623
613
  return r
624
614
  .match(Follows, { follower: a, followee: b })
625
615
  .match(Follows, { follower: b, followee: a })
626
- .where(lt(a, b))
616
+ .where(r.lt(a, b))
627
617
  .find({ a, b })
628
618
  })
629
619
  ```
@@ -772,14 +762,14 @@ const roomConflicts = query(Calendar).rule((r) => {
772
762
  const { room, span } = v(Booking)
773
763
  return r
774
764
  .match(Booking, { room, span })
775
- .where(allen(span, ALLEN.intersects, r.param("want")))
765
+ .where(r.allen(span, ALLEN.intersects, r.param("want")))
776
766
  .find({ room, span })
777
767
  })
778
768
  const personLoad = query(Calendar).rule((r) => {
779
769
  const { person, span } = v(Claim)
780
770
  return r
781
771
  .match(Claim, { person, span })
782
- .where(allen(span, ALLEN.intersects, r.param("window")))
772
+ .where(r.allen(span, ALLEN.intersects, r.param("window")))
783
773
  .find({ person, span })
784
774
  })
785
775
  ```
@@ -815,7 +805,7 @@ const inForce = query(Pricing).rule((r) => {
815
805
  const { rate_bps, valid } = v(Version)
816
806
  return r
817
807
  .match(Version, { policy: r.param("p"), rate_bps, valid })
818
- .where(pointIn(r.param("t"), valid))
808
+ .where(r.pointIn(r.param("t"), valid))
819
809
  .find({ rate_bps })
820
810
  })
821
811
  // clean successions (half-open makes MEETS exact, no ±1 fudge):
@@ -825,7 +815,7 @@ const successions = query(Pricing).rule((r) => {
825
815
  return r
826
816
  .match(Version, { policy: p, valid: a })
827
817
  .match(Version, { policy: p, valid: b })
828
- .where(allen(a, ALLEN.meets, b))
818
+ .where(r.allen(a, ALLEN.meets, b))
829
819
  .find({ a, b })
830
820
  })
831
821
  ```
@@ -858,7 +848,7 @@ const holding = query(Payroll).rule((r) => {
858
848
  const { seq, span } = v(PayPeriod)
859
849
  return r
860
850
  .match(PayPeriod, { year: r.param("y"), seq, span })
861
- .where(pointIn(r.param("t"), span))
851
+ .where(r.pointIn(r.param("t"), span))
862
852
  .find({ seq })
863
853
  })
864
854
  ```
@@ -908,7 +898,7 @@ const marginal = query(Tax).rule((r) => {
908
898
  return r
909
899
  .match(Regime, { id: reg, year: r.param("y"), status: r.param("s") })
910
900
  .match(Bracket, { regime: reg, income: b, rate_bps })
911
- .where(pointIn(r.param("taxable"), b))
901
+ .where(r.pointIn(r.param("taxable"), b))
912
902
  .find({ rate_bps })
913
903
  })
914
904
  ```
@@ -939,11 +929,9 @@ const busy = query(FreeTime).rule((r) => {
939
929
  const { person, span } = v(Claim)
940
930
  return r.match(Claim, { person, span }).find({ person, packed: r.pack(span) })
941
931
  })
942
- // raw claimed time (overlaps double-count often the wrong question):
943
- const claimed = query(FreeTime).rule((r) => {
944
- const { person, span } = v(Claim)
945
- return r.match(Claim, { person, span }).find({ person, claimed: r.sum(r.duration(span)) })
946
- })
932
+ // raw claimed time is host arithmetic on the interval endpoints every
933
+ // answer row already carries (`end − start`). Overlaps double-count,
934
+ // often the wrong question.
947
935
  // Coalesced totals = the two-query composition (pack, then a host fold) —
948
936
  // aggregates never nest; free time (gaps) is the two-line host walk over
949
937
  // sorted packed answers (host sorts by person then span start/end) — both
@@ -999,9 +987,9 @@ discipline — snapshot-derived writes detect movement
999
987
  final-state point reads need no earlier witness.
1000
988
 
1001
989
  The generation witness: read the model, propose a delta, commit iff the model
1002
- you read is still the model. The SDK ships one-shot `writeFrom` (must run
1003
- inside the read callback that owns the snapshot). Retry on
1004
- `ErrGenerationMoved` is host policy — a short loop around `db.read` +
990
+ you read is still the model. The SDK ships one-shot `writeFrom` (the witness
991
+ escapes the read callback; the instance does not). Retry on
992
+ `{ tag: "moved" }` is host policy — a short loop around `db.read` +
1005
993
  `writeFrom` if the host wants it. `abandon(payload)`
1006
994
  declines to commit without issuing anything — from `db.write` and
1007
995
  `db.writeFrom` alike (the sentinel's contract is unconditional, and
@@ -1027,18 +1015,22 @@ const stillQueued = query(Jobs).rule((r) => {
1027
1015
  return r.match(Job, { id, state: "Queued", payload }).find({ id, payload })
1028
1016
  })
1029
1017
 
1030
- const db = await Db.create("./jobs.db", Jobs)
1018
+ const created = await Db.create("./jobs.db", Jobs)
1019
+ if (created.tag !== "accepted") {
1020
+ throw new Error("create rejected")
1021
+ }
1022
+ const db = created.value
1031
1023
  const prepared = db.prepare(stillQueued)
1032
1024
 
1033
- // The witnessed write: premise reads via `snap`, the delta via `tx`. On a
1034
- // moved generation `writeFrom` throws `ErrGenerationMoved` — retry is host
1035
- // policy. The other two idioms: insert-select is the same shape (query
1025
+ // The witnessed write: premise reads via `instance`, the delta via `tx`.
1026
+ // On a moved generation `writeFrom` returns `{ tag: "moved" }` — retry is
1027
+ // host policy. The other two idioms: insert-select is the same shape (query
1036
1028
  // source answers, insert the derived facts); key-shaped read-modify-write
1037
1029
  // uses `tx.get`/`tx.contains` — final-state point reads need no earlier
1038
1030
  // witness.
1039
- const outcome = db.read(function attempt(snap) {
1040
- return db.writeFrom(snap, function updateWhere(tx) {
1041
- const queued = snap.execute(prepared, {})
1031
+ const outcome = db.read(function attempt(instance, witness) {
1032
+ return db.writeFrom(witness, function updateWhere(tx) {
1033
+ const queued = instance.execute(prepared, {})
1042
1034
  if (queued.length === 0) {
1043
1035
  return abandon("nothing queued")
1044
1036
  }
@@ -1092,10 +1084,9 @@ const deriving = query(Rollup).rule((r) => {
1092
1084
 
1093
1085
  Guarantee: Lean theorem + represented planner/runtime premise — rule union is
1094
1086
  set-idempotent (`lean/Bumbledb/Query/Denotation.lean: union_idempotent`);
1095
- key-backed DU arms justify a disjointness *witness*
1096
- (`lean/Bumbledb/Exec/Dedup.lean: disjoint_witness_licence`) spent diagnostically
1097
- only — execution always keeps one spanning seen-set
1098
- (`docs/architecture/40-execution.md` § set semantics).
1087
+ key-backed DU arms satisfy the disjoint-arms licence
1088
+ (`lean/Bumbledb/Exec/Dedup.lean: disjoint_witness_licence`); execution
1089
+ always keeps one spanning seen-set.
1099
1090
 
1100
1091
  The whole-DU read is a set of rules: one head, one rule per arm — disjunction
1101
1092
  is data at the top, never an execution node.
@@ -1116,8 +1107,8 @@ const Payments = schema("Payments", { Kind, Payment, Card, Ach }, [
1116
1107
 
1117
1108
  // One query, two rules (set union). The exclusivity theorem (recipe 2) is
1118
1109
  // spent a third time here: rules selecting different `kind` handles are
1119
- // provably disjoint. Plan introspection retains the witness; execution still
1120
- // probes one spanning seen-set — the measured refutation deleted the elision.
1110
+ // provably disjoint. Execution still probes one spanning seen-set — the
1111
+ // measured refutation deleted the elision.
1121
1112
  const wholeDu = query(Payments)
1122
1113
  .rule((r) => {
1123
1114
  const { id } = v(Payment)
@@ -1211,14 +1202,18 @@ The loop (the compiled, driven copy is in `test/cookbook.test.ts`, over a
1211
1202
  three-level forest with the exact reachable set asserted):
1212
1203
 
1213
1204
  ```ts
1214
- const db = await Db.create("./closure.db", Closure)
1205
+ const created = await Db.create("./closure.db", Closure)
1206
+ if (created.tag !== "accepted") {
1207
+ throw new Error("create rejected")
1208
+ }
1209
+ const db = created.value
1215
1210
  const stepPrepared = db.prepare(step)
1216
1211
  const root = 1n // the host's chosen root node id
1217
1212
 
1218
1213
  const seen = new Set<bigint>([root])
1219
1214
  let frontier: readonly bigint[] = [root]
1220
1215
  for (;;) {
1221
- const next = db.execute(stepPrepared, { frontier }) // one set-param query
1216
+ const next = db.read((i) => i.execute(stepPrepared, { frontier })) // one set-param query
1222
1217
  const fresh = next
1223
1218
  .map((row) => row.c)
1224
1219
  .filter((c) => {
@@ -1249,7 +1244,7 @@ const reach = query(Closure)
1249
1244
  const { id: c } = v(Node)
1250
1245
  return r
1251
1246
  .match(Node, { id: c })
1252
- .where(eq(c, r.param("root")))
1247
+ .where(r.eq(c, r.param("root")))
1253
1248
  .find({ c })
1254
1249
  }
1255
1250
  ],
@@ -1281,7 +1276,7 @@ const unreached = query(Closure)
1281
1276
  const { id: c } = v(Node)
1282
1277
  return r
1283
1278
  .match(Node, { id: c })
1284
- .where(eq(c, r.param("root")))
1279
+ .where(r.eq(c, r.param("root")))
1285
1280
  .find({ c })
1286
1281
  }
1287
1282
  ],
@@ -1352,7 +1347,7 @@ const nativeRollup = query(Accounts)
1352
1347
  const { id: a } = v(Account)
1353
1348
  return r
1354
1349
  .match(Account, { id: a })
1355
- .where(eq(a, r.param("root")))
1350
+ .where(r.eq(a, r.param("root")))
1356
1351
  .find({ a })
1357
1352
  }
1358
1353
  ],
@@ -1492,7 +1487,7 @@ const inForceAt = query(Payroll).rule((r) => {
1492
1487
  return r
1493
1488
  .match(Employee, { id: e, name })
1494
1489
  .match(Salary, { employee: e, amount, applies: w })
1495
- .where(pointIn(r.param("at"), w))
1490
+ .where(r.pointIn(r.param("at"), w))
1496
1491
  .find({ name, amount })
1497
1492
  })
1498
1493
  ```
@@ -1595,7 +1590,11 @@ runtime shape check. The primary 2-arg form needs no statement: the fresh
1595
1590
  field IS the primary key.
1596
1591
 
1597
1592
  ```ts
1598
- const db = await Db.create("./courses.db", KeyedRead)
1593
+ const created = await Db.create("./courses.db", KeyedRead)
1594
+ if (created.tag !== "accepted") {
1595
+ throw new Error("create rejected")
1596
+ }
1597
+ const db = created.value
1599
1598
 
1600
1599
  const minted: { grp?: bigint } = {}
1601
1600
  db.write((tx) => {
@@ -1608,10 +1607,10 @@ db.write((tx) => {
1608
1607
  const grp = minted.grp ?? 0n
1609
1608
 
1610
1609
  // db.get — the standalone keyed read through the declared law:
1611
- const byGroup = db.get(Course, courseGrpKey, { grp })
1610
+ const byGroup = db.read((i) => i.get(Course, courseGrpKey, { grp }))
1612
1611
 
1613
1612
  // snap.get — the same spelling inside a read scope:
1614
- const viaSnap = db.read((snap) => snap.get(Course, courseGrpKey, { grp }))
1613
+ const viaSnap = db.read((instance) => instance.get(Course, courseGrpKey, { grp }))
1615
1614
 
1616
1615
  // tx.get — key-shaped read-modify-write, final-state (recipe 20's third
1617
1616
  // idiom): per-fact premises need no earlier snapshot witness.
@@ -1624,7 +1623,7 @@ db.write((tx) => {
1624
1623
  })
1625
1624
 
1626
1625
  // The primary 2-arg form — the fresh field is the primary key:
1627
- const byId = byGroup === undefined ? undefined : db.get(Course, { id: byGroup.id })
1626
+ const byId = byGroup === undefined ? undefined : db.read((i) => i.get(Course, { id: byGroup.id }))
1628
1627
  ```
1629
1628
 
1630
1629
  The anti-pattern this recipe retires: a scan-and-find where a key law
@@ -1717,11 +1716,8 @@ const Rooms = schema("Rooms", { Room, Booking }, [
1717
1716
  )
1718
1717
  ])
1719
1718
 
1720
- // the booked time per room, read back:
1721
- const booked = query(Rooms).rule((r) => {
1722
- const { id, room, booked } = v(Booking)
1723
- return r.match(Booking, { id, room, booked }).find({ room, total: r.sum(r.duration(booked)) })
1724
- })
1719
+ // the booked time per room is host arithmetic on the `booked`
1720
+ // endpoints every answer row already carries (`end − start`).
1725
1721
  ```
1726
1722
 
1727
1723
  Mind the weighted `{0}` and the weighted floor: on a weighted statement
package/README.md CHANGED
@@ -1,16 +1,23 @@
1
1
  # @bjornpagen/bumbledb
2
2
 
3
- Type-theoretic TypeScript SDK for the [bumbledb](https://github.com/bjornpagen/bumbledb) embedded relational engine.
3
+ This package is the TypeScript interface to the
4
+ [Bumbledb](https://github.com/bjornpagen/bumbledb) embedded relational
5
+ database. Schemas and queries are typed TypeScript values rather than SQL
6
+ strings, while storage, admitted instances, transactions, and query execution run in
7
+ the native engine.
4
8
 
5
- bumbledb models data as relations judged by statements (functionality, containment, capacity) and queried as typed IR values — no SQL, no query-string parser. The SDK is a thin, fully typed surface over an in-process native engine (LMDB storage, MVCC snapshots, one-shot `write` / `writeFrom`).
6
-
7
- The surface is structural to the bone. Relation declarations are pure structure — kind, width, element, fresh, nothing else — and domains are never declared anywhere: **the laws type the columns**. `schema()` computes every field's equivalence class from the statement list itself, so the containments and mirrors you already write ARE the typing, at compile time and again at construction. Values stay bare (`bigint`, `string`, …); identity lives in the class the laws compute, not in a wrapper.
8
-
9
- > **Research-grade, one platform.** This is **0.14.0**, a `0.x` release of an embedded engine under active development. It targets a single platform today (below), the API is not yet frozen across `0.x`, and the FFI ABI is pinned exactly per version (`bdb_abi_version()` is **2** — collection insert/delete, `reserve`). Treat it as an early adopter's tool, not a production datastore.
9
+ Relation declarations describe their fields, and the statements passed to
10
+ `schema()` connect those fields into typed keys and references. Values remain
11
+ ordinary `bigint`, `string`, boolean, byte, and interval values; queries infer
12
+ their parameter and result types from how those values are used.
10
13
 
11
14
  ## Platform support
12
15
 
13
- This release targets **darwin-arm64 (macOS Apple Silicon) only**. The native binary ships as the optional platform package `@bjornpagen/bumbledb-darwin-arm64`, resolved automatically at install on a matching host. Installs on other platforms succeed (the main package is pure JS) but throw a typed, actionable error at first load naming the running platform and that only `darwin-arm64` ships today. More targets are pure addition — one more `os`/`cpu`-gated package plus a CI matrix — not a redesign.
16
+ The TypeScript package currently ships a native binary for **darwin-arm64**
17
+ (macOS on Apple Silicon). The optional
18
+ `@bjornpagen/bumbledb-darwin-arm64` package is selected automatically during
19
+ installation. On another platform, importing the package returns an error
20
+ that identifies the running platform and the available binary.
14
21
 
15
22
  ## Install
16
23
 
@@ -20,18 +27,16 @@ pnpm add @bjornpagen/bumbledb
20
27
 
21
28
  ## Quick start
22
29
 
23
- Declare relations as pure structure, let the statement list type every column,
24
- write facts through a transaction, and query with the typed builder.
25
- Everything is typed end to end bare structural values in law-computed
26
- classes, inferred query rows, and rejections that arrive as data rather than
27
- exceptions.
30
+ Declare relations, connect their fields with keys and references, write
31
+ records in a transaction, and query them with the typed builder. Parameters
32
+ and result rows are inferred, and a failed constraint check is returned as
33
+ structured data rather than thrown as an exception.
28
34
 
29
35
  ```ts
30
- import { bool, closed, contained, Db, gt, type Infer, key, on, query, relation, schema, u64, v } from "@bjornpagen/bumbledb"
36
+ import { bool, closed, contained, Db, type Infer, key, on, query, relation, schema, u64, v } from "@bjornpagen/bumbledb"
31
37
 
32
- // A closed relation: a sealed roster of axioms with typed payload columns.
33
- // At the host surface a handle is its NAME the string literal "DirectPass"
34
- // is the one spelling, and closed columns type as the handle union.
38
+ // A fixed set can carry typed columns as well as names.
39
+ // Its ID type is the union "DirectPass" | "JudgedPass" | "Failed".
35
40
  const Kind = closed(
36
41
  "Kind",
37
42
  { mastered: bool, rank: u64 },
@@ -42,14 +47,13 @@ const Kind = closed(
42
47
  }
43
48
  )
44
49
 
45
- // Relations are pure structure — no domain is declared anywhere.
50
+ // Relations describe stored records.
46
51
  // `u64.fresh` marks an engine-minted primary key.
47
52
  const Attempt = relation("Attempt", { id: u64.fresh, kind: Kind.id })
48
53
  const Certificate = relation("Certificate", { attempt: u64, kind: Kind.id })
49
54
 
50
- // THE LAWS TYPE THE COLUMNS: schema() computes every field's class FROM this
51
- // statement list the containments are the typing. The last statement uses
52
- // ψ-selection: a certificate may only ever cite a mastered kind.
55
+ // These statements declare the key and references. The final reference is
56
+ // conditional: a certificate may cite only a mastered kind.
53
57
  const Review = schema("Review", { Kind, Attempt, Certificate }, [
54
58
  contained(on(Attempt, "kind"), on(Kind, "id")),
55
59
  key(Certificate, ["attempt"]),
@@ -57,55 +61,53 @@ const Review = schema("Review", { Kind, Attempt, Certificate }, [
57
61
  contained(on(Certificate, "kind"), on(Kind.where({ mastered: true }), "id"))
58
62
  ])
59
63
 
60
- const db = await Db.create("./review.db", Review)
64
+ const created = await Db.create("./review.db", Review)
65
+ if (created.tag !== "accepted") {
66
+ throw new Error("create rejected")
67
+ }
68
+ const db = created.value
61
69
 
62
- // Write. The delta is judged against every statement at commit. A closed
63
- // column takes the handle name a wrong string is a compile error AND a
64
- // marshal refusal.
70
+ // All writes are checked together before the transaction commits. A fixed-set
71
+ // column takes its name, and a wrong string is rejected by TypeScript and
72
+ // again if an untyped value reaches the native boundary.
65
73
  const result = db.write((tx) => {
66
74
  const id = tx.reserve(Attempt, "id", 1n).at(0n)!
67
75
  tx.insert(Attempt, [{ id, kind: "DirectPass" }])
68
76
  tx.insert(Certificate, [{ attempt: id, kind: "DirectPass" }])
69
77
  })
70
78
 
71
- // Rejection-as-data: no throw a rejected commit is a typed value carrying
72
- // every violated statement, cited once, with its canonical spelling and facts.
73
- if (!result.ok) {
79
+ // A failed constraint check is returned as typed data rather than thrown.
80
+ if (result.tag === "rejected") {
74
81
  for (const v of result.violations) {
75
82
  console.error(v.kind, v.canonical, v.facts)
76
83
  }
77
84
  }
78
85
 
79
- // Query: v(R) mints a fresh variable per column, typed by
80
- // the column's law-class; reusing one by object reference IS the join, and
81
- // rows are typed from the find keys. Params are typed by use.
82
- // `gt` is one of the free comparison exports.
86
+ // v(R) creates a typed variable for each column. Reusing one across records
87
+ // creates the join, result rows follow the find keys, and parameters are typed
88
+ // from where they are used.
83
89
  const certifiedAbove = query(Review).rule((r) => {
84
90
  const { attempt: a, kind: k } = v(Certificate)
85
91
  const { rank } = v(Kind)
86
92
  return r
87
93
  .match(Certificate, { attempt: a, kind: k })
88
- .match(Kind, { id: k, mastered: true, rank }) // k reused at Kind.id that reuse is the join; ψ on the read side too
89
- .where(gt(rank, r.param("floor")))
94
+ .match(Kind, { id: k, mastered: true, rank }) // reusing k at Kind.id creates the join
95
+ .where(r.gt(rank, r.param("floor")))
90
96
  .find({ a, rank })
91
97
  })
92
98
 
93
99
  const prepared = db.prepare(certifiedAbove)
94
- const rows = db.execute(prepared, { floor: 15n }) // rows: { a: bigint; rank: bigint }[]
100
+ const rows = db.read((i) => i.execute(prepared, { floor: 15n })) // rows: { a: bigint; rank: bigint }[]
95
101
  console.log(rows)
96
102
 
97
- // Lifetimes are disposables, never close() (Node 26 explicit resource
98
- // management): a read scope acquired without a callback is released by its
99
- // `using` declaration at scope exit — deterministic, in the language's own
100
- // syntax. `db.read(fn)` remains the callback spelling of the same scope.
101
- {
102
- using snap = db.read()
103
- console.log(snap.generation, snap.execute(prepared, { floor: 15n }))
104
- }
103
+ // A store read is one callback. The instance is invalid when the callback
104
+ // returns; the witness is a clone and may escape.
105
+ db.read((instance) => {
106
+ console.log(instance.generation, instance.execute(prepared, { floor: 15n }))
107
+ })
105
108
 
106
- // Host dispatch over the sealed roster is native `switch` narrowing over
107
- // the handle union ("DirectPass" | "JudgedPass" | "Failed") — exhaustive
108
- // via `satisfies never`; the sealed axioms read back typed.
109
+ // Dispatch over the fixed set uses native `switch` narrowing.
110
+ // `satisfies never` checks that every possible name is handled.
109
111
  function describe(kind: Infer<typeof Kind.id>): string {
110
112
  switch (kind) {
111
113
  case "DirectPass":
@@ -125,23 +127,47 @@ real surface by `test/readme.test.ts` — the examples cannot drift.
125
127
 
126
128
  ## Surface
127
129
 
128
- The drizzle law governs this surface: the SDK's job at the host boundary is translation, not abstraction — every database idiom arrives as the modern TypeScript idiom for that concept, and the SDK never invents an operator where the language already has one.
129
-
130
- - The structural type kernel — fields as pure structure (`bool`, `bytes`, `i64`, `u64`, `str`, `interval`, `span`), `relation()`, and `closed()` sealed rosters with typed axiom payloads. A closed reference's value type IS the handle union (`Infer` speaks it); dispatch is native `switch` narrowing with `satisfies never` exhaustiveness. Domains are never declared: `schema()` computes every field's class from the statement list.
131
- - The statement algebra — `schema()`, `key`, `contained`, `mirrors`, `capacity`; faces via `on` (set membership is a plain array in `.where`); windows via `within` (`within(n)` exact, `within(lo, hi)` range, `within(lo, "*")` floor), measures via `weigh` (`weigh("f")` a u64 field, `weigh(duration("f"))` an interval's measure), dependent bounds via `ref`/`duration` read from the target row; ψ-selection via `.where` on relations and closed rosters.
132
- - The `Db` runtime — `Db.create`/`Db.open` (exclusive-lock stores; a second open of the same path is `EnvironmentLocked`), transactions, typed violations, scoped snapshot reads (`db.read(fn)`, or `using snap = db.read()` — lifetimes are disposables, never `close()`), the write verbs with `abandon` (returning `abandon(payload)` from `write` or `writeFrom` rolls the transaction back; the outcome arm is in the result type). Collection writes: `tx.insert(Rel, [{...}])` / `tx.delete(Rel, facts)` return `MutationReport { submitted, changed }`; mint is `tx.reserve(Rel, field, 1n)` (empty is not a minted id). ETL is a host loop of `write`.
133
- - The query surface `query(S).rule(r => ...)`: `v(R)`-minted vars (identity is the object reference — reusing one across binding positions IS the join), `find({...})` named result heads (renames are real), params typed by use unchanged, negation, aggregates, and the free comparison/connective exports (`eq`, `ne`, `lt`, `le`, `gt`, `ge`, `and`, `or`, `not`, `allen`/`ALLEN`, `pointIn`); set membership at a closed field is a plain array in the match record (`r.match(Ticket, { priority: ["Normal", "Urgent"] })` — closed-only there: an ordinary field's membership is a bound `r.inSet` param); named interiors and one linear rec via `q.interior` / `q.reach`; `db.prepare` as a plain value.
134
- - The exhume surface — `Db.exhume`, the schema-independent read path: a store's self-described shapes and raw facts by name, with typed refusals (`ErrExhumeNoDescriptor`, `ErrExhumeFormatMismatch`, `ErrExhumeCorruption`). A disposable lifetime: `using exhumed = await Db.exhume(path)` releases the store's exclusive lock at scope exit.
130
+ The SDK translates TypeScript values directly into the engine's shared schema
131
+ and query representations.
132
+
133
+ - Fields use `bool`, `bytes`, `i64`, `u64`, `str`, `interval`, and
134
+ `span`. `relation()` declares stored records, while `closed()` declares a
135
+ fixed enum-like set whose values may carry typed columns. `Infer` exposes
136
+ the resulting TypeScript value type.
137
+ - `schema()` accepts `key`, `contained`, `mirrors`, and `capacity`
138
+ statements. `.where` makes a reference conditional, `within` sets a count
139
+ or measurement range, and `weigh` chooses a numeric field or interval
140
+ duration to measure.
141
+ - `Db.create` and `Db.open` manage embedded stores. Create returns an
142
+ `Admission`. Reads are a synchronous callback `db.read((instance, witness) => …)` —
143
+ the instance cannot escape; the witness may. Writes use `write` or
144
+ `writeFrom(witness, …)` and may return `abandon(payload)` to roll back
145
+ explicitly. `insert` and `delete` report how many submitted records
146
+ changed the set, and `reserve` returns never-reused IDs.
147
+ - `query(S).rule(...)` builds typed queries. Reusing a variable created by
148
+ `v(R)` joins records through that value. The builder supports named result
149
+ rows, typed parameters, negation, comparisons, boolean conditions, set
150
+ parameters, interval operations, aggregates, named intermediate results,
151
+ and linear recursive reachability.
152
+ - `Db.exhume` opens a store without its original application schema and
153
+ exposes its stored relation descriptions and records by name. The returned
154
+ handle uses `using` so the exclusive store lock is released at scope exit.
135
155
 
136
156
  ## Cookbook
137
157
 
138
- The engine cookbook's 32 modeling recipes, translated to this SDK's structural API: [COOKBOOK.md](./COOKBOOK.md). Two referees hold it: `test/cookbook-doc.test.ts` extracts the document's own `ts` fences and type-checks them against the real surface (the doc itself cannot drift), and `test/cookbook.test.ts` runs compiled copies of the recipes — each schema admitted by the real engine, its fingerprint asserted against the cross-host goldens the Rust cookbook suite also pins, every query snippet lowered through `db.prepare`.
158
+ The engine cookbook's 32 modeling recipes are translated to the TypeScript API
159
+ in [COOKBOOK.md](./COOKBOOK.md). `test/cookbook-doc.test.ts` extracts and
160
+ type-checks the document's TypeScript examples, while
161
+ `test/cookbook.test.ts` opens every schema and prepares every query. The Rust
162
+ and TypeScript versions are also checked to ensure that they describe the same
163
+ schema.
139
164
 
140
165
  ## Architecture
141
166
 
142
- The SDK is a typed surface over the native engine; the model (relations,
143
- statement-based judgment, Datalog evaluation, MVCC storage, the witnessed
144
- write loop) is documented in the [bumbledb engine repository](https://github.com/bjornpagen/bumbledb).
167
+ The SDK is a typed interface to the native engine. Storage, transactions,
168
+ queries, constraints, performance results, and the Rust implementation are
169
+ documented in the
170
+ [Bumbledb repository](https://github.com/bjornpagen/bumbledb).
145
171
 
146
172
  ## License
147
173