@bjornpagen/bumbledb 0.10.0 → 0.12.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 +155 -136
- package/README.md +5 -9
- package/dist/db.d.ts +77 -109
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +121 -339
- package/dist/db.js.map +1 -1
- package/dist/index.d.ts +12 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -11
- package/dist/index.js.map +1 -1
- package/dist/lower.d.ts.map +1 -1
- package/dist/lower.js +8 -1
- package/dist/lower.js.map +1 -1
- package/dist/native.d.ts +77 -51
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js.map +1 -1
- package/dist/query/atom.d.ts +94 -112
- package/dist/query/atom.d.ts.map +1 -1
- package/dist/query/atom.js +8 -17
- package/dist/query/atom.js.map +1 -1
- package/dist/query/find.d.ts +18 -35
- package/dist/query/find.d.ts.map +1 -1
- package/dist/query/find.js +13 -32
- package/dist/query/find.js.map +1 -1
- package/dist/query/lower.d.ts +128 -124
- package/dist/query/lower.d.ts.map +1 -1
- package/dist/query/lower.js +366 -258
- package/dist/query/lower.js.map +1 -1
- package/dist/query/parse-ir.d.ts +12 -0
- package/dist/query/parse-ir.d.ts.map +1 -0
- package/dist/query/parse-ir.js +71 -0
- package/dist/query/parse-ir.js.map +1 -0
- package/dist/query/run.d.ts +2 -2
- package/dist/query/run.d.ts.map +1 -1
- package/dist/query/run.js +2 -13
- package/dist/query/run.js.map +1 -1
- package/dist/query/scope.d.ts +4 -16
- package/dist/query/scope.d.ts.map +1 -1
- package/dist/query/scope.js +1 -6
- package/dist/query/scope.js.map +1 -1
- package/dist/schema.js +2 -2
- package/dist/schema.js.map +1 -1
- package/dist/statements.d.ts +8 -4
- package/dist/statements.d.ts.map +1 -1
- package/dist/statements.js +7 -9
- package/dist/statements.js.map +1 -1
- package/package.json +2 -2
- package/src/db.ts +182 -443
- package/src/index.ts +11 -24
- package/src/lower.ts +8 -1
- package/src/native.ts +79 -43
- package/src/query/atom.ts +105 -138
- package/src/query/find.ts +39 -80
- package/src/query/lower.ts +626 -434
- package/src/query/parse-ir.ts +82 -0
- package/src/query/run.ts +2 -14
- package/src/query/scope.ts +3 -21
- package/src/schema.ts +2 -2
- package/src/statements.ts +17 -13
- package/dist/order.d.ts +0 -87
- package/dist/order.d.ts.map +0 -1
- package/dist/order.js +0 -153
- package/dist/order.js.map +0 -1
- package/dist/query/predicate.d.ts +0 -91
- package/dist/query/predicate.d.ts.map +0 -1
- package/dist/query/predicate.js +0 -156
- package/dist/query/predicate.js.map +0 -1
- package/src/order.ts +0 -234
- package/src/query/predicate.ts +0 -269
package/COOKBOOK.md
CHANGED
|
@@ -18,7 +18,8 @@ recipes: each schema is constructed through the public surface, admitted
|
|
|
18
18
|
on a real store (the engine's schema validation is the acceptance
|
|
19
19
|
judgment), its fingerprint asserted stable across a reopen AND equal to
|
|
20
20
|
the per-recipe cross-host golden the Rust cookbook suite also pins
|
|
21
|
-
(`
|
|
21
|
+
(`fixtures/cookbook-fingerprints.txt` at the repository root — the two
|
|
22
|
+
cookbooks teach one
|
|
22
23
|
theory per recipe number, provably), and every query snippet lowered
|
|
23
24
|
through `db.prepare` (the engine's own IR validation).
|
|
24
25
|
|
|
@@ -37,12 +38,10 @@ import {
|
|
|
37
38
|
abandon,
|
|
38
39
|
allen,
|
|
39
40
|
bool,
|
|
40
|
-
by,
|
|
41
41
|
bytes,
|
|
42
42
|
capacity,
|
|
43
43
|
closed,
|
|
44
44
|
contained,
|
|
45
|
-
desc,
|
|
46
45
|
duration,
|
|
47
46
|
eq,
|
|
48
47
|
i64,
|
|
@@ -53,7 +52,6 @@ import {
|
|
|
53
52
|
not,
|
|
54
53
|
on,
|
|
55
54
|
pointIn,
|
|
56
|
-
program,
|
|
57
55
|
query,
|
|
58
56
|
ref,
|
|
59
57
|
relation,
|
|
@@ -179,7 +177,8 @@ const Grading = schema("Grading", { Kind, Task, DeterministicGrading, CustomOper
|
|
|
179
177
|
mirrors(on(Task.where({ kind: "CustomOperator" }), "id"), on(CustomOperatorGrading, "task"))
|
|
180
178
|
// Exclusivity is a theorem, not a statement: one id in two arms would
|
|
181
179
|
// force `kind` to equal two handles against the fresh key on id.
|
|
182
|
-
// The executor spends the same theorem again
|
|
180
|
+
// The executor spends the same theorem again as a diagnostic witness
|
|
181
|
+
// (recipe 22); multi-rule execution still keeps a spanning seen-set.
|
|
183
182
|
])
|
|
184
183
|
|
|
185
184
|
// Host dispatch over the discriminator is native `switch` narrowing over
|
|
@@ -512,13 +511,32 @@ const playingAt = query(Playlists).rule((r) => {
|
|
|
512
511
|
.find({ track })
|
|
513
512
|
})
|
|
514
513
|
|
|
515
|
-
// Answers are SETS — the host sorts them
|
|
516
|
-
//
|
|
517
|
-
// order by (start, end). Limit is the language's own `.slice(0, n)`.
|
|
514
|
+
// Answers are SETS — the host sorts them. Limit is the language's own
|
|
515
|
+
// `.slice(0, n)`.
|
|
518
516
|
const inPlayOrder = [
|
|
519
517
|
{ slot: { start: 1n, end: 2n }, track: "b" },
|
|
520
518
|
{ slot: { start: 0n, end: 1n }, track: "a" }
|
|
521
|
-
].sort(
|
|
519
|
+
].sort(function bySlotThenTrack(left, right) {
|
|
520
|
+
if (left.slot.start < right.slot.start) {
|
|
521
|
+
return -1
|
|
522
|
+
}
|
|
523
|
+
if (left.slot.start > right.slot.start) {
|
|
524
|
+
return 1
|
|
525
|
+
}
|
|
526
|
+
if (left.slot.end < right.slot.end) {
|
|
527
|
+
return -1
|
|
528
|
+
}
|
|
529
|
+
if (left.slot.end > right.slot.end) {
|
|
530
|
+
return 1
|
|
531
|
+
}
|
|
532
|
+
if (left.track < right.track) {
|
|
533
|
+
return -1
|
|
534
|
+
}
|
|
535
|
+
if (left.track > right.track) {
|
|
536
|
+
return 1
|
|
537
|
+
}
|
|
538
|
+
return 0
|
|
539
|
+
})
|
|
522
540
|
```
|
|
523
541
|
|
|
524
542
|
Middle insert is honest about its cost: making room at position `k` shifts
|
|
@@ -928,8 +946,7 @@ const claimed = query(FreeTime).rule((r) => {
|
|
|
928
946
|
})
|
|
929
947
|
// Coalesced totals = the two-query composition (pack, then a host fold) —
|
|
930
948
|
// aggregates never nest; free time (gaps) is the two-line host walk over
|
|
931
|
-
// sorted packed answers (
|
|
932
|
-
// keys-as-data comparator; limit is the language's own `.slice`) — both
|
|
949
|
+
// sorted packed answers (host sorts by person then span start/end) — both
|
|
933
950
|
// refusals recorded in the ledger.
|
|
934
951
|
```
|
|
935
952
|
|
|
@@ -982,14 +999,12 @@ discipline — snapshot-derived writes detect movement
|
|
|
982
999
|
final-state point reads need no earlier witness.
|
|
983
1000
|
|
|
984
1001
|
The generation witness: read the model, propose a delta, commit iff the model
|
|
985
|
-
you read is still the model.
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
to witness, and past the cap that interleave hazard throws the typed
|
|
990
|
-
`ErrWitnessedLivelock` instead of spinning forever. `abandon(payload)`
|
|
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` +
|
|
1005
|
+
`writeFrom` if the host wants it. `abandon(payload)`
|
|
991
1006
|
declines to commit without issuing anything — from `db.write` and
|
|
992
|
-
`db.
|
|
1007
|
+
`db.writeFrom` alike (the sentinel's contract is unconditional, and
|
|
993
1008
|
the outcome arm is in the result type exactly when the callback can
|
|
994
1009
|
abandon).
|
|
995
1010
|
|
|
@@ -1015,22 +1030,25 @@ const stillQueued = query(Jobs).rule((r) => {
|
|
|
1015
1030
|
const db = await Db.create("./jobs.db", Jobs)
|
|
1016
1031
|
const prepared = db.prepare(stillQueued)
|
|
1017
1032
|
|
|
1018
|
-
// The witnessed
|
|
1019
|
-
// moved generation
|
|
1020
|
-
// two idioms: insert-select is the same shape (query
|
|
1021
|
-
// the derived facts); key-shaped read-modify-write
|
|
1022
|
-
// — final-state point reads need no earlier
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
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
|
|
1036
|
+
// source answers, insert the derived facts); key-shaped read-modify-write
|
|
1037
|
+
// uses `tx.get`/`tx.contains` — final-state point reads need no earlier
|
|
1038
|
+
// witness.
|
|
1039
|
+
const outcome = db.read(function attempt(snap) {
|
|
1040
|
+
return db.writeFrom(snap, function updateWhere(tx) {
|
|
1041
|
+
const queued = snap.execute(prepared, {})
|
|
1042
|
+
if (queued.length === 0) {
|
|
1043
|
+
return abandon("nothing queued")
|
|
1044
|
+
}
|
|
1045
|
+
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 })
|
|
1049
|
+
}
|
|
1050
|
+
return undefined
|
|
1051
|
+
})
|
|
1034
1052
|
})
|
|
1035
1053
|
```
|
|
1036
1054
|
|
|
@@ -1074,8 +1092,10 @@ const deriving = query(Rollup).rule((r) => {
|
|
|
1074
1092
|
|
|
1075
1093
|
Guarantee: Lean theorem + represented planner/runtime premise — rule union is
|
|
1076
1094
|
set-idempotent (`lean/Bumbledb/Query/Denotation.lean: union_idempotent`);
|
|
1077
|
-
key-backed DU arms justify
|
|
1078
|
-
(`lean/Bumbledb/Exec/Dedup.lean: disjoint_witness_licence`)
|
|
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).
|
|
1079
1099
|
|
|
1080
1100
|
The whole-DU read is a set of rules: one head, one rule per arm — disjunction
|
|
1081
1101
|
is data at the top, never an execution node.
|
|
@@ -1096,7 +1116,8 @@ const Payments = schema("Payments", { Kind, Payment, Card, Ach }, [
|
|
|
1096
1116
|
|
|
1097
1117
|
// One query, two rules (set union). The exclusivity theorem (recipe 2) is
|
|
1098
1118
|
// spent a third time here: rules selecting different `kind` handles are
|
|
1099
|
-
// provably disjoint
|
|
1119
|
+
// provably disjoint. Plan introspection retains the witness; execution still
|
|
1120
|
+
// probes one spanning seen-set — the measured refutation deleted the elision.
|
|
1100
1121
|
const wholeDu = query(Payments)
|
|
1101
1122
|
.rule((r) => {
|
|
1102
1123
|
const { id } = v(Payment)
|
|
@@ -1157,14 +1178,15 @@ const Gravestones = schema("Gravestones", { Step, Score, ActiveRun, Usage, Event
|
|
|
1157
1178
|
|
|
1158
1179
|
Guarantee: host discipline for the loop — the finite `seen` set proves
|
|
1159
1180
|
termination for the host run; the engine-native form beside it executes
|
|
1160
|
-
whole under the
|
|
1161
|
-
(`lean/Bumbledb/Exec/
|
|
1181
|
+
whole under the linear reach driver, budget-bounded
|
|
1182
|
+
(`lean/Bumbledb/Exec/Reach.lean: evalLinearReach_eq_lfp` /
|
|
1183
|
+
`evalQuery_sound`).
|
|
1162
1184
|
|
|
1163
1185
|
Reachability, in two dialects. The host-loop idiom remains the depth-bounded
|
|
1164
1186
|
answer: the loop runs depth-many rounds and each round is one ∈-set query —
|
|
1165
1187
|
an `inSet` probe, microsecond-class. The frontier discipline below *is*
|
|
1166
1188
|
semi-naive evaluation's Δ, spent where a loop is a loop: the host. The
|
|
1167
|
-
engine-native form is the same closure as one
|
|
1189
|
+
engine-native form is the same closure as one query with `.reach()`.
|
|
1168
1190
|
|
|
1169
1191
|
```ts
|
|
1170
1192
|
const Node = relation("Node", { id: u64.fresh, name: str })
|
|
@@ -1215,68 +1237,65 @@ for (;;) {
|
|
|
1215
1237
|
Termination is the host's theorem: `seen` grows strictly or the loop breaks,
|
|
1216
1238
|
inside a finite node set. When the idiom's costs bite — **unbounded or large
|
|
1217
1239
|
depth**, or **closure composed into a larger plan** — write the engine-native
|
|
1218
|
-
form instead: `?root` seeds the
|
|
1219
|
-
set's own identity projection (an
|
|
1240
|
+
form instead: `?root` seeds the rec, and the main rule is the finished
|
|
1241
|
+
set's own identity projection (an interior atom is a positive occurrence, so it
|
|
1220
1242
|
grounds its variables — no re-grounding join over a domain relation exists):
|
|
1221
1243
|
|
|
1222
1244
|
```ts
|
|
1223
|
-
const reach =
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
.
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1245
|
+
const reach = query(Closure)
|
|
1246
|
+
.reach("reach", {
|
|
1247
|
+
base: [
|
|
1248
|
+
(r) => {
|
|
1249
|
+
const { id: c } = v(Node)
|
|
1250
|
+
return r
|
|
1251
|
+
.match(Node, { id: c })
|
|
1252
|
+
.where(eq(c, r.param("root")))
|
|
1253
|
+
.find({ c })
|
|
1254
|
+
}
|
|
1255
|
+
],
|
|
1256
|
+
rec: [
|
|
1257
|
+
(r) => {
|
|
1258
|
+
const { child: c, parent } = v(Parent)
|
|
1259
|
+
return r.match(Parent, { child: c, parent }).interior("reach", { c: parent }).find({ c })
|
|
1260
|
+
}
|
|
1261
|
+
]
|
|
1262
|
+
})
|
|
1263
|
+
.rule((r) => {
|
|
1241
1264
|
const { id: c } = v(Node)
|
|
1242
|
-
return r.
|
|
1265
|
+
return r.interior("reach", { c }).find({ c })
|
|
1243
1266
|
})
|
|
1244
|
-
})
|
|
1245
1267
|
const reachPrepared = db.prepare(reach)
|
|
1246
1268
|
```
|
|
1247
1269
|
|
|
1248
|
-
The complement is one `r.not` away — negation **of** a finished
|
|
1249
|
-
engine-legal (
|
|
1270
|
+
The complement is one `r.not` away — negation **of** a finished rec is
|
|
1271
|
+
engine-legal (negation in rec is refused), so
|
|
1250
1272
|
"every node the closure never reached" runs in-plan through the engine's
|
|
1251
|
-
anti-probe, never as a host-side set difference — the same
|
|
1252
|
-
|
|
1273
|
+
anti-probe, never as a host-side set difference — the same query with the
|
|
1274
|
+
main rule:
|
|
1253
1275
|
|
|
1254
1276
|
```ts
|
|
1255
|
-
const unreached =
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
.
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1277
|
+
const unreached = query(Closure)
|
|
1278
|
+
.reach("reach", {
|
|
1279
|
+
base: [
|
|
1280
|
+
(r) => {
|
|
1281
|
+
const { id: c } = v(Node)
|
|
1282
|
+
return r
|
|
1283
|
+
.match(Node, { id: c })
|
|
1284
|
+
.where(eq(c, r.param("root")))
|
|
1285
|
+
.find({ c })
|
|
1286
|
+
}
|
|
1287
|
+
],
|
|
1288
|
+
rec: [
|
|
1289
|
+
(r) => {
|
|
1290
|
+
const { child: c, parent } = v(Parent)
|
|
1291
|
+
return r.match(Parent, { child: c, parent }).interior("reach", { c: parent }).find({ c })
|
|
1292
|
+
}
|
|
1293
|
+
]
|
|
1294
|
+
})
|
|
1295
|
+
.rule((r) => {
|
|
1273
1296
|
const { id: c } = v(Node)
|
|
1274
|
-
return r
|
|
1275
|
-
.match(Node, { id: c })
|
|
1276
|
-
.where(r.not(seeded, { c }))
|
|
1277
|
-
.find({ c })
|
|
1297
|
+
return r.match(Node, { id: c }).where(r.not("reach", { c })).find({ c })
|
|
1278
1298
|
})
|
|
1279
|
-
})
|
|
1280
1299
|
```
|
|
1281
1300
|
|
|
1282
1301
|
(the test drives both dialects and asserts the same reachable sets, root for
|
|
@@ -1289,16 +1308,16 @@ composition has no engine form.
|
|
|
1289
1308
|
|
|
1290
1309
|
Guarantee: host discipline + runtime aggregate semantics — the host computes
|
|
1291
1310
|
closure, then one checked `sum` (`lean/Bumbledb/Query/Aggregates.lean:
|
|
1292
|
-
checkedSum_sound`); the engine-native form folds over a
|
|
1293
|
-
|
|
1311
|
+
checkedSum_sound`); the engine-native form folds over a finished rec,
|
|
1312
|
+
the one aggregation shape the rec roster admits.
|
|
1294
1313
|
|
|
1295
1314
|
The ledger workload's real recursion case, in the same two dialects: a
|
|
1296
1315
|
hierarchical chart of accounts and a subtree rollup. The host composition —
|
|
1297
1316
|
recipe 24's loop accumulates the subtree's ∈-set, then **one `sum` query over
|
|
1298
1317
|
the accumulated set** folds the postings. The engine aggregates, the host
|
|
1299
|
-
composes (aggregates never nest). The engine-native form is one
|
|
1300
|
-
aggregation *through* a cycle is refused, but a fold over a
|
|
1301
|
-
|
|
1318
|
+
composes (aggregates never nest). The engine-native form is one query:
|
|
1319
|
+
aggregation *through* a cycle is refused, but a fold over a finished rec
|
|
1320
|
+
from main reads a finished set and is ordinary.
|
|
1302
1321
|
|
|
1303
1322
|
```ts
|
|
1304
1323
|
const Account = relation("Account", { id: u64.fresh, name: str })
|
|
@@ -1324,33 +1343,33 @@ const subtreeRollup = query(Accounts).rule((r) => {
|
|
|
1324
1343
|
const { id, minor } = v(Posting)
|
|
1325
1344
|
return r.match(Posting, { id, account: r.inSet("subtree"), minor }).find({ total: r.sum(minor) })
|
|
1326
1345
|
})
|
|
1327
|
-
// The engine-native form: the
|
|
1328
|
-
//
|
|
1329
|
-
const nativeRollup =
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
.
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1346
|
+
// The engine-native form: the rec converges first, then the
|
|
1347
|
+
// main fold runs once over the finished subtree.
|
|
1348
|
+
const nativeRollup = query(Accounts)
|
|
1349
|
+
.reach("sub", {
|
|
1350
|
+
base: [
|
|
1351
|
+
(r) => {
|
|
1352
|
+
const { id: a } = v(Account)
|
|
1353
|
+
return r
|
|
1354
|
+
.match(Account, { id: a })
|
|
1355
|
+
.where(eq(a, r.param("root")))
|
|
1356
|
+
.find({ a })
|
|
1357
|
+
}
|
|
1358
|
+
],
|
|
1359
|
+
rec: [
|
|
1360
|
+
(r) => {
|
|
1361
|
+
const { child: a, parent } = v(AccountParent)
|
|
1362
|
+
return r.match(AccountParent, { child: a, parent }).interior("sub", { a: parent }).find({ a })
|
|
1363
|
+
}
|
|
1364
|
+
]
|
|
1365
|
+
})
|
|
1366
|
+
.rule((r) => {
|
|
1347
1367
|
const { id, account: a, minor } = v(Posting)
|
|
1348
1368
|
return r
|
|
1349
1369
|
.match(Posting, { id, account: a, minor })
|
|
1350
|
-
.
|
|
1370
|
+
.interior("sub", { a })
|
|
1351
1371
|
.find({ total: r.sum(minor) })
|
|
1352
1372
|
})
|
|
1353
|
-
})
|
|
1354
1373
|
```
|
|
1355
1374
|
|
|
1356
1375
|
## 26. Exact partition
|
|
@@ -1419,9 +1438,9 @@ const deriving = query(MaintainedRollup).rule((r) => {
|
|
|
1419
1438
|
})
|
|
1420
1439
|
```
|
|
1421
1440
|
|
|
1422
|
-
The host loop is `db.
|
|
1423
|
-
build the delta — recipe 20's third idiom. On a moved generation the
|
|
1424
|
-
throws away the attempt and
|
|
1441
|
+
The host loop is `db.read` + `writeFrom`: derive on the attempt's snapshot, diff,
|
|
1442
|
+
build the delta — recipe 20's third idiom. On a moved generation the host
|
|
1443
|
+
throws away the attempt and retries on a fresh snapshot; it
|
|
1425
1444
|
never retries a stale diff. Dependencies prove every surviving stored span
|
|
1426
1445
|
sound, while the witness proves which source state the derivation saw;
|
|
1427
1446
|
neither mechanism proves completeness. The engine's compiled copy
|
|
@@ -1549,20 +1568,20 @@ keyed point read answers exactly that fact or nothing, on every scope
|
|
|
1549
1568
|
`crates/bumbledb/tests/keyed_get.rs`).
|
|
1550
1569
|
|
|
1551
1570
|
The key is a **law**, and the read surface is that law made callable. The
|
|
1552
|
-
schema says `key(
|
|
1553
|
-
|
|
1571
|
+
schema says `key(Course, ["grp"])` — one course per group — so "the
|
|
1572
|
+
course of a group" is a well-posed question with at most one answer, and
|
|
1554
1573
|
the store already enforces that on every commit. Hold the statement VALUE:
|
|
1555
1574
|
it is the read's selector below (statement identity is the membership rule).
|
|
1556
1575
|
|
|
1557
1576
|
```ts
|
|
1558
1577
|
const Grp = relation("Grp", { id: u64.fresh, label: str })
|
|
1559
|
-
const
|
|
1560
|
-
// The law: one
|
|
1561
|
-
const
|
|
1578
|
+
const Course = relation("Course", { id: u64.fresh, grp: u64, title: str })
|
|
1579
|
+
// The law: one course per group — the callable key.
|
|
1580
|
+
const courseGrpKey = key(Course, ["grp"])
|
|
1562
1581
|
|
|
1563
|
-
const KeyedRead = schema("KeyedRead", { Grp,
|
|
1564
|
-
contained(on(
|
|
1565
|
-
|
|
1582
|
+
const KeyedRead = schema("KeyedRead", { Grp, Course }, [
|
|
1583
|
+
contained(on(Course, "grp"), on(Grp, "id")),
|
|
1584
|
+
courseGrpKey
|
|
1566
1585
|
])
|
|
1567
1586
|
```
|
|
1568
1587
|
|
|
@@ -1576,38 +1595,38 @@ runtime shape check. The primary 2-arg form needs no statement: the fresh
|
|
|
1576
1595
|
field IS the primary key.
|
|
1577
1596
|
|
|
1578
1597
|
```ts
|
|
1579
|
-
const db = await Db.create("./
|
|
1598
|
+
const db = await Db.create("./courses.db", KeyedRead)
|
|
1580
1599
|
|
|
1581
1600
|
const minted: { grp?: bigint } = {}
|
|
1582
1601
|
db.write((tx) => {
|
|
1583
1602
|
const g = tx.insert(Grp, { label: "algebra" })
|
|
1584
|
-
tx.insert(
|
|
1603
|
+
tx.insert(Course, { grp: g.id, title: "linear equations" })
|
|
1585
1604
|
minted.grp = g.id
|
|
1586
1605
|
})
|
|
1587
1606
|
const grp = minted.grp ?? 0n
|
|
1588
1607
|
|
|
1589
1608
|
// db.get — the standalone keyed read through the declared law:
|
|
1590
|
-
const byGroup = db.get(
|
|
1609
|
+
const byGroup = db.get(Course, courseGrpKey, { grp })
|
|
1591
1610
|
|
|
1592
1611
|
// snap.get — the same spelling inside a read scope:
|
|
1593
|
-
const viaSnap = db.read((snap) => snap.get(
|
|
1612
|
+
const viaSnap = db.read((snap) => snap.get(Course, courseGrpKey, { grp }))
|
|
1594
1613
|
|
|
1595
1614
|
// tx.get — key-shaped read-modify-write, final-state (recipe 20's third
|
|
1596
1615
|
// idiom): per-fact premises need no earlier snapshot witness.
|
|
1597
1616
|
db.write((tx) => {
|
|
1598
|
-
const current = tx.get(
|
|
1617
|
+
const current = tx.get(Course, courseGrpKey, { grp })
|
|
1599
1618
|
if (current !== undefined) {
|
|
1600
|
-
tx.delete(
|
|
1601
|
-
tx.insert(
|
|
1619
|
+
tx.delete(Course, current)
|
|
1620
|
+
tx.insert(Course, { id: current.id, grp: current.grp, title: "linear equations II" })
|
|
1602
1621
|
}
|
|
1603
1622
|
})
|
|
1604
1623
|
|
|
1605
1624
|
// The primary 2-arg form — the fresh field is the primary key:
|
|
1606
|
-
const byId = byGroup === undefined ? undefined : db.get(
|
|
1625
|
+
const byId = byGroup === undefined ? undefined : db.get(Course, { id: byGroup.id })
|
|
1607
1626
|
```
|
|
1608
1627
|
|
|
1609
1628
|
The anti-pattern this recipe retires: a scan-and-find where a key law
|
|
1610
|
-
exists — `snap.scan(
|
|
1629
|
+
exists — `snap.scan(Course).find((row) => row.grp === grp)` — re-derives
|
|
1611
1630
|
in the host what the store already enforces. The uniqueness the fold
|
|
1612
1631
|
quietly assumes IS the declared key statement; spell the law and the point
|
|
1613
1632
|
read comes with it.
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Type-theoretic TypeScript SDK for the [bumbledb](https://github.com/bjornpagen/bumbledb) embedded relational engine.
|
|
4
4
|
|
|
5
|
-
bumbledb models data as relations judged by statements (functionality, containment, capacity) and queried
|
|
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
6
|
|
|
7
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
8
|
|
|
@@ -21,7 +21,7 @@ pnpm add @bjornpagen/bumbledb
|
|
|
21
21
|
## Quick start
|
|
22
22
|
|
|
23
23
|
Declare relations as pure structure, let the statement list type every column,
|
|
24
|
-
write facts through a transaction, and query with
|
|
24
|
+
write facts through a transaction, and query with the typed builder.
|
|
25
25
|
Everything is typed end to end — bare structural values in law-computed
|
|
26
26
|
classes, inferred query rows, and rejections that arrive as data rather than
|
|
27
27
|
exceptions.
|
|
@@ -75,7 +75,7 @@ if (!result.ok) {
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
// Query:
|
|
78
|
+
// Query: v(R) mints a fresh variable per column, typed by
|
|
79
79
|
// the column's law-class; reusing one by object reference IS the join, and
|
|
80
80
|
// rows are typed from the find keys. Params are typed by use.
|
|
81
81
|
// `gt` is one of the free comparison exports.
|
|
@@ -102,10 +102,6 @@ console.log(rows)
|
|
|
102
102
|
console.log(snap.generation, snap.execute(prepared, { floor: 15n }))
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
// explain(): the plan as data — what the engine did with the query, plan
|
|
106
|
-
// sections and counters as plain values (a diagnostic surface, unfrozen).
|
|
107
|
-
console.log(db.explain(prepared, { floor: 15n }).emits)
|
|
108
|
-
|
|
109
105
|
// Host dispatch over the sealed roster is native `switch` narrowing over
|
|
110
106
|
// the handle union ("DirectPass" | "JudgedPass" | "Failed") — exhaustive
|
|
111
107
|
// via `satisfies never`; the sealed axioms read back typed.
|
|
@@ -132,8 +128,8 @@ The drizzle law governs this surface: the SDK's job at the host boundary is tran
|
|
|
132
128
|
|
|
133
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.
|
|
134
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.
|
|
135
|
-
- The `Db` runtime — `Db.create`/`Db.open
|
|
136
|
-
- The query surface —
|
|
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.
|
|
137
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.
|
|
138
134
|
|
|
139
135
|
## Cookbook
|