@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.
- package/COOKBOOK.md +58 -62
- package/README.md +82 -56
- package/dist/capacity.d.ts +24 -136
- package/dist/capacity.d.ts.map +1 -1
- package/dist/capacity.js +18 -40
- package/dist/capacity.js.map +1 -1
- package/dist/closed.d.ts +0 -156
- package/dist/closed.d.ts.map +1 -1
- package/dist/closed.js +0 -104
- package/dist/closed.js.map +1 -1
- package/dist/db.d.ts +93 -290
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +713 -556
- package/dist/db.js.map +1 -1
- package/dist/face.d.ts +0 -133
- package/dist/face.d.ts.map +1 -1
- package/dist/face.js +0 -33
- package/dist/face.js.map +1 -1
- package/dist/fields.d.ts +1 -145
- package/dist/fields.d.ts.map +1 -1
- package/dist/fields.js +2 -91
- package/dist/fields.js.map +1 -1
- package/dist/index.d.ts +13 -23
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -20
- package/dist/index.js.map +1 -1
- package/dist/law.d.ts +111 -93
- package/dist/law.d.ts.map +1 -1
- package/dist/law.js +23 -27
- package/dist/law.js.map +1 -1
- package/dist/lower.d.ts +9 -35
- package/dist/lower.d.ts.map +1 -1
- package/dist/lower.js +8 -53
- package/dist/lower.js.map +1 -1
- package/dist/marshal.d.ts +0 -65
- package/dist/marshal.d.ts.map +1 -1
- package/dist/marshal.js +0 -72
- package/dist/marshal.js.map +1 -1
- package/dist/native.d.ts +97 -390
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js +39 -61
- package/dist/native.js.map +1 -1
- package/dist/query/atom.d.ts +10 -276
- package/dist/query/atom.d.ts.map +1 -1
- package/dist/query/atom.js +1 -96
- package/dist/query/atom.js.map +1 -1
- package/dist/query/find.d.ts +10 -76
- package/dist/query/find.d.ts.map +1 -1
- package/dist/query/find.js +0 -30
- package/dist/query/find.js.map +1 -1
- package/dist/query/lower.d.ts +64 -146
- package/dist/query/lower.d.ts.map +1 -1
- package/dist/query/lower.js +19 -256
- package/dist/query/lower.js.map +1 -1
- package/dist/query/parse-ir.d.ts +0 -7
- package/dist/query/parse-ir.d.ts.map +1 -1
- package/dist/query/parse-ir.js +1 -13
- package/dist/query/parse-ir.js.map +1 -1
- package/dist/query/run.d.ts +0 -36
- package/dist/query/run.d.ts.map +1 -1
- package/dist/query/run.js +0 -44
- package/dist/query/run.js.map +1 -1
- package/dist/query/scope.d.ts +24 -180
- package/dist/query/scope.d.ts.map +1 -1
- package/dist/query/scope.js +2 -66
- package/dist/query/scope.js.map +1 -1
- package/dist/relation.d.ts +2 -50
- package/dist/relation.d.ts.map +1 -1
- package/dist/relation.js +2 -37
- package/dist/relation.js.map +1 -1
- package/dist/schema.d.ts +13 -63
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +118 -92
- package/dist/schema.js.map +1 -1
- package/dist/spec.d.ts +1 -140
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js +1 -68
- package/dist/spec.js.map +1 -1
- package/dist/statements.d.ts +6 -137
- package/dist/statements.d.ts.map +1 -1
- package/dist/statements.js +16 -119
- package/dist/statements.js.map +1 -1
- package/package.json +3 -3
- package/src/capacity.ts +26 -140
- package/src/closed.ts +5 -206
- package/src/db.ts +997 -854
- package/src/face.ts +0 -142
- package/src/fields.ts +4 -172
- package/src/index.ts +32 -35
- package/src/law.ts +201 -129
- package/src/lower.ts +8 -53
- package/src/marshal.ts +1 -85
- package/src/native.ts +192 -413
- package/src/query/atom.ts +26 -313
- package/src/query/find.ts +24 -110
- package/src/query/lower.ts +132 -377
- package/src/query/parse-ir.ts +1 -14
- package/src/query/run.ts +0 -45
- package/src/query/scope.ts +25 -186
- package/src/relation.ts +2 -66
- package/src/schema.ts +143 -122
- package/src/spec.ts +1 -160
- package/src/statements.ts +22 -174
- 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
|
@@ -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**:
|
|
6
|
-
|
|
7
|
-
|
|
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 suites — and 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
|
|
143
|
-
|
|
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
|
|
943
|
-
|
|
944
|
-
|
|
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` (
|
|
1003
|
-
|
|
1004
|
-
`
|
|
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
|
|
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 `
|
|
1034
|
-
// moved generation `writeFrom`
|
|
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(
|
|
1040
|
-
return db.writeFrom(
|
|
1041
|
-
const queued =
|
|
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
|
|
1096
|
-
(`lean/Bumbledb/Exec/Dedup.lean: disjoint_witness_licence`)
|
|
1097
|
-
|
|
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.
|
|
1120
|
-
//
|
|
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
|
|
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
|
|
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((
|
|
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
|
|
1721
|
-
|
|
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
|
-
|
|
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 **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
|
-
|
|
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
|
-
import { bool, closed, contained, Db,
|
|
36
|
+
import { bool, closed, contained, Db, 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,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
|
|
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
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
|
-
//
|
|
72
|
-
|
|
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
|
-
//
|
|
80
|
-
// the
|
|
81
|
-
//
|
|
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
|
|
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
|
-
//
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
//
|
|
107
|
-
//
|
|
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
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
-
|
|
132
|
-
|
|
133
|
-
-
|
|
134
|
-
|
|
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
|
|
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
|
|
143
|
-
|
|
144
|
-
|
|
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
|
|