@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.
- package/COOKBOOK.md +35 -21
- package/README.md +82 -55
- package/dist/db.d.ts +173 -130
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +782 -371
- package/dist/db.js.map +1 -1
- package/dist/index.d.ts +7 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -9
- package/dist/index.js.map +1 -1
- package/dist/marshal.d.ts +5 -27
- package/dist/marshal.d.ts.map +1 -1
- package/dist/marshal.js +4 -21
- package/dist/marshal.js.map +1 -1
- package/dist/native.d.ts +157 -168
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js +46 -8
- package/dist/native.js.map +1 -1
- package/dist/query/find.d.ts +14 -9
- package/dist/query/find.d.ts.map +1 -1
- package/dist/query/find.js +2 -2
- package/dist/query/find.js.map +1 -1
- package/dist/query/lower.d.ts.map +1 -1
- package/dist/query/lower.js +5 -4
- package/dist/query/lower.js.map +1 -1
- package/dist/query/parse-ir.d.ts.map +1 -1
- package/dist/query/parse-ir.js +11 -9
- package/dist/query/parse-ir.js.map +1 -1
- package/dist/relation.d.ts +4 -25
- package/dist/relation.d.ts.map +1 -1
- package/dist/relation.js +3 -4
- package/dist/relation.js.map +1 -1
- package/package.json +3 -3
- package/src/db.ts +1151 -500
- package/src/index.ts +28 -24
- package/src/marshal.ts +5 -35
- package/src/native.ts +248 -175
- package/src/query/find.ts +19 -14
- package/src/query/lower.ts +7 -6
- package/src/query/parse-ir.ts +11 -9
- package/src/relation.ts +3 -28
- package/dist/exhume.d.ts +0 -143
- package/dist/exhume.d.ts.map +0 -1
- package/dist/exhume.js +0 -166
- package/dist/exhume.js.map +0 -1
- 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` (
|
|
1003
|
-
|
|
1004
|
-
`
|
|
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
|
|
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 `
|
|
1034
|
-
// moved generation `writeFrom`
|
|
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(
|
|
1040
|
-
return db.writeFrom(
|
|
1041
|
-
const queued =
|
|
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
|
|
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
|
|
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.
|
|
1603
|
-
tx.insert(
|
|
1604
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
33
|
-
//
|
|
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
|
|
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
|
-
//
|
|
51
|
-
//
|
|
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
|
|
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
|
-
//
|
|
63
|
-
// column takes
|
|
64
|
-
//
|
|
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
|
|
67
|
-
tx.insert(
|
|
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
|
-
//
|
|
71
|
-
|
|
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
|
-
//
|
|
79
|
-
// the
|
|
80
|
-
//
|
|
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
|
|
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
|
-
//
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
//
|
|
106
|
-
//
|
|
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
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
-
|
|
131
|
-
|
|
132
|
-
-
|
|
133
|
-
|
|
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
|
|
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
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|