@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/COOKBOOK.md CHANGED
@@ -999,9 +999,9 @@ discipline — snapshot-derived writes detect movement
999
999
  final-state point reads need no earlier witness.
1000
1000
 
1001
1001
  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` +
1002
+ you read is still the model. The SDK ships one-shot `writeFrom` (the witness
1003
+ escapes the read callback; the instance does not). Retry on
1004
+ `{ tag: "moved" }` is host policy — a short loop around `db.read` +
1005
1005
  `writeFrom` if the host wants it. `abandon(payload)`
1006
1006
  declines to commit without issuing anything — from `db.write` and
1007
1007
  `db.writeFrom` alike (the sentinel's contract is unconditional, and
@@ -1027,25 +1027,29 @@ const stillQueued = query(Jobs).rule((r) => {
1027
1027
  return r.match(Job, { id, state: "Queued", payload }).find({ id, payload })
1028
1028
  })
1029
1029
 
1030
- const db = await Db.create("./jobs.db", Jobs)
1030
+ const created = await Db.create("./jobs.db", Jobs)
1031
+ if (created.tag !== "accepted") {
1032
+ throw new Error("create rejected")
1033
+ }
1034
+ const db = created.value
1031
1035
  const prepared = db.prepare(stillQueued)
1032
1036
 
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
1037
+ // The witnessed write: premise reads via `instance`, the delta via `tx`.
1038
+ // On a moved generation `writeFrom` returns `{ tag: "moved" }` — retry is
1039
+ // host policy. The other two idioms: insert-select is the same shape (query
1036
1040
  // source answers, insert the derived facts); key-shaped read-modify-write
1037
1041
  // uses `tx.get`/`tx.contains` — final-state point reads need no earlier
1038
1042
  // witness.
1039
- const outcome = db.read(function attempt(snap) {
1040
- return db.writeFrom(snap, function updateWhere(tx) {
1041
- const queued = snap.execute(prepared, {})
1043
+ const outcome = db.read(function attempt(instance, witness) {
1044
+ return db.writeFrom(witness, function updateWhere(tx) {
1045
+ const queued = instance.execute(prepared, {})
1042
1046
  if (queued.length === 0) {
1043
1047
  return abandon("nothing queued")
1044
1048
  }
1045
1049
  for (const row of queued) {
1046
- tx.delete(Job, { id: row.id, state: "Queued", payload: row.payload })
1047
- tx.insert(Job, { id: row.id, state: "Running", payload: row.payload })
1048
- tx.insert(Lease, { job: row.id, worker: 7n, until: 60n })
1050
+ tx.delete(Job, [{ id: row.id, state: "Queued", payload: row.payload }])
1051
+ tx.insert(Job, [{ id: row.id, state: "Running", payload: row.payload }])
1052
+ tx.insert(Lease, [{ job: row.id, worker: 7n, until: 60n }])
1049
1053
  }
1050
1054
  return undefined
1051
1055
  })
@@ -1211,7 +1215,11 @@ The loop (the compiled, driven copy is in `test/cookbook.test.ts`, over a
1211
1215
  three-level forest with the exact reachable set asserted):
1212
1216
 
1213
1217
  ```ts
1214
- const db = await Db.create("./closure.db", Closure)
1218
+ const created = await Db.create("./closure.db", Closure)
1219
+ if (created.tag !== "accepted") {
1220
+ throw new Error("create rejected")
1221
+ }
1222
+ const db = created.value
1215
1223
  const stepPrepared = db.prepare(step)
1216
1224
  const root = 1n // the host's chosen root node id
1217
1225
 
@@ -1595,13 +1603,19 @@ runtime shape check. The primary 2-arg form needs no statement: the fresh
1595
1603
  field IS the primary key.
1596
1604
 
1597
1605
  ```ts
1598
- const db = await Db.create("./courses.db", KeyedRead)
1606
+ const created = await Db.create("./courses.db", KeyedRead)
1607
+ if (created.tag !== "accepted") {
1608
+ throw new Error("create rejected")
1609
+ }
1610
+ const db = created.value
1599
1611
 
1600
1612
  const minted: { grp?: bigint } = {}
1601
1613
  db.write((tx) => {
1602
- const g = tx.insert(Grp, { label: "algebra" })
1603
- tx.insert(Course, { grp: g.id, title: "linear equations" })
1604
- minted.grp = g.id
1614
+ const g = tx.reserve(Grp, "id", 1n).at(0n)!
1615
+ tx.insert(Grp, [{ id: g, label: "algebra" }])
1616
+ const course = tx.reserve(Course, "id", 1n).at(0n)!
1617
+ tx.insert(Course, [{ id: course, grp: g, title: "linear equations" }])
1618
+ minted.grp = g
1605
1619
  })
1606
1620
  const grp = minted.grp ?? 0n
1607
1621
 
@@ -1609,15 +1623,15 @@ const grp = minted.grp ?? 0n
1609
1623
  const byGroup = db.get(Course, courseGrpKey, { grp })
1610
1624
 
1611
1625
  // snap.get — the same spelling inside a read scope:
1612
- const viaSnap = db.read((snap) => snap.get(Course, courseGrpKey, { grp }))
1626
+ const viaSnap = db.read((instance) => instance.get(Course, courseGrpKey, { grp }))
1613
1627
 
1614
1628
  // tx.get — key-shaped read-modify-write, final-state (recipe 20's third
1615
1629
  // idiom): per-fact premises need no earlier snapshot witness.
1616
1630
  db.write((tx) => {
1617
1631
  const current = tx.get(Course, courseGrpKey, { grp })
1618
1632
  if (current !== undefined) {
1619
- tx.delete(Course, current)
1620
- tx.insert(Course, { id: current.id, grp: current.grp, title: "linear equations II" })
1633
+ tx.delete(Course, [current])
1634
+ tx.insert(Course, [{ id: current.id, grp: current.grp, title: "linear equations II" }])
1621
1635
  }
1622
1636
  })
1623
1637
 
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 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. 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
36
  import { bool, closed, contained, Db, gt, 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,34 +61,37 @@ 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
- const attempt = tx.insert(Attempt, { kind: "DirectPass" }) // attempt.id minted, a bare bigint
67
- tx.insert(Certificate, { attempt: attempt.id, kind: "DirectPass" })
74
+ const id = tx.reserve(Attempt, "id", 1n).at(0n)!
75
+ tx.insert(Attempt, [{ id, kind: "DirectPass" }])
76
+ tx.insert(Certificate, [{ attempt: id, kind: "DirectPass" }])
68
77
  })
69
78
 
70
- // Rejection-as-data: no throw a rejected commit is a typed value carrying
71
- // every violated statement, cited once, with its canonical spelling and facts.
72
- if (!result.ok) {
79
+ // A failed constraint check is returned as typed data rather than thrown.
80
+ if (result.tag === "rejected") {
73
81
  for (const v of result.violations) {
74
82
  console.error(v.kind, v.canonical, v.facts)
75
83
  }
76
84
  }
77
85
 
78
- // Query: v(R) mints a fresh variable per column, typed by
79
- // the column's law-class; reusing one by object reference IS the join, and
80
- // rows are typed from the find keys. Params are typed by use.
81
- // `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.
82
89
  const certifiedAbove = query(Review).rule((r) => {
83
90
  const { attempt: a, kind: k } = v(Certificate)
84
91
  const { rank } = v(Kind)
85
92
  return r
86
93
  .match(Certificate, { attempt: a, kind: k })
87
- .match(Kind, { id: k, mastered: true, rank }) // k reused at Kind.id that reuse is the join; ψ on the read side too
94
+ .match(Kind, { id: k, mastered: true, rank }) // reusing k at Kind.id creates the join
88
95
  .where(gt(rank, r.param("floor")))
89
96
  .find({ a, rank })
90
97
  })
@@ -93,18 +100,14 @@ const prepared = db.prepare(certifiedAbove)
93
100
  const rows = db.execute(prepared, { floor: 15n }) // rows: { a: bigint; rank: bigint }[]
94
101
  console.log(rows)
95
102
 
96
- // Lifetimes are disposables, never close() (Node 26 explicit resource
97
- // management): a read scope acquired without a callback is released by its
98
- // `using` declaration at scope exit — deterministic, in the language's own
99
- // syntax. `db.read(fn)` remains the callback spelling of the same scope.
100
- {
101
- using snap = db.read()
102
- console.log(snap.generation, snap.execute(prepared, { floor: 15n }))
103
- }
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
+ })
104
108
 
105
- // Host dispatch over the sealed roster is native `switch` narrowing over
106
- // the handle union ("DirectPass" | "JudgedPass" | "Failed") — exhaustive
107
- // 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.
108
111
  function describe(kind: Infer<typeof Kind.id>): string {
109
112
  switch (kind) {
110
113
  case "DirectPass":
@@ -124,23 +127,47 @@ real surface by `test/readme.test.ts` — the examples cannot drift.
124
127
 
125
128
  ## Surface
126
129
 
127
- 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.
128
-
129
- - 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.
130
- - 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.
131
- - 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).
132
- - 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.
133
- - 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.
134
155
 
135
156
  ## Cookbook
136
157
 
137
- 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.
138
164
 
139
165
  ## Architecture
140
166
 
141
- The SDK is a typed surface over the native engine; the model (relations,
142
- statement-based judgment, Datalog evaluation, MVCC storage, the witnessed
143
- 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).
144
171
 
145
172
  ## License
146
173