@bjornpagen/bumbledb 0.20.2 → 1.0.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 +283 -1611
- package/README.md +156 -118
- package/dist/capacity.d.ts +16 -12
- package/dist/capacity.d.ts.map +1 -1
- package/dist/capacity.js +16 -48
- package/dist/capacity.js.map +1 -1
- package/dist/changes.d.ts +54 -0
- package/dist/changes.d.ts.map +1 -0
- package/dist/changes.js +245 -0
- package/dist/changes.js.map +1 -0
- package/dist/close.d.ts +24 -0
- package/dist/close.d.ts.map +1 -0
- package/dist/close.js +34 -0
- package/dist/close.js.map +1 -0
- package/dist/closed.d.ts +1 -3
- package/dist/closed.d.ts.map +1 -1
- package/dist/closed.js +24 -12
- package/dist/closed.js.map +1 -1
- package/dist/codec.d.ts +56 -0
- package/dist/codec.d.ts.map +1 -0
- package/dist/codec.js +289 -0
- package/dist/codec.js.map +1 -0
- package/dist/compile.d.ts +68 -0
- package/dist/compile.d.ts.map +1 -0
- package/dist/compile.js +75 -0
- package/dist/compile.js.map +1 -0
- package/dist/db-native.d.ts +170 -0
- package/dist/db-native.d.ts.map +1 -0
- package/dist/db-native.js +5 -0
- package/dist/db-native.js.map +1 -0
- package/dist/db.d.ts +101 -233
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +232 -1084
- package/dist/db.js.map +1 -1
- package/dist/errors.d.ts +57 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +32 -0
- package/dist/errors.js.map +1 -0
- package/dist/face.d.ts.map +1 -1
- package/dist/face.js +2 -2
- package/dist/face.js.map +1 -1
- package/dist/fields.d.ts +65 -24
- package/dist/fields.d.ts.map +1 -1
- package/dist/fields.js +94 -24
- package/dist/fields.js.map +1 -1
- package/dist/index.d.ts +37 -32
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -21
- package/dist/index.js.map +1 -1
- package/dist/internal/log.d.ts +24 -0
- package/dist/internal/log.d.ts.map +1 -0
- package/dist/internal/log.js +10 -0
- package/dist/internal/log.js.map +1 -0
- package/dist/law.d.ts +8 -13
- package/dist/law.d.ts.map +1 -1
- package/dist/law.js +6 -59
- package/dist/law.js.map +1 -1
- package/dist/lower.d.ts +0 -13
- package/dist/lower.d.ts.map +1 -1
- package/dist/lower.js +9 -17
- package/dist/lower.js.map +1 -1
- package/dist/migration.d.ts +33 -0
- package/dist/migration.d.ts.map +1 -0
- package/dist/migration.js +48 -0
- package/dist/migration.js.map +1 -0
- package/dist/native.d.ts +256 -301
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js +65 -108
- package/dist/native.js.map +1 -1
- package/dist/query/atom.d.ts +24 -13
- package/dist/query/atom.d.ts.map +1 -1
- package/dist/query/atom.js +7 -3
- package/dist/query/atom.js.map +1 -1
- package/dist/query/compute.d.ts +68 -0
- package/dist/query/compute.d.ts.map +1 -0
- package/dist/query/compute.js +141 -0
- package/dist/query/compute.js.map +1 -0
- package/dist/query/find.d.ts +15 -8
- package/dist/query/find.d.ts.map +1 -1
- package/dist/query/find.js +6 -2
- package/dist/query/find.js.map +1 -1
- package/dist/query/lower.d.ts +23 -2
- package/dist/query/lower.d.ts.map +1 -1
- package/dist/query/lower.js +433 -80
- package/dist/query/lower.js.map +1 -1
- package/dist/query/parse-ir.d.ts.map +1 -1
- package/dist/query/parse-ir.js +17 -8
- package/dist/query/parse-ir.js.map +1 -1
- package/dist/query/run.d.ts +12 -2
- package/dist/query/run.d.ts.map +1 -1
- package/dist/query/run.js +26 -9
- package/dist/query/run.js.map +1 -1
- package/dist/query/scope.d.ts +47 -3
- package/dist/query/scope.d.ts.map +1 -1
- package/dist/query/scope.js +83 -19
- package/dist/query/scope.js.map +1 -1
- package/dist/relation.d.ts +3 -8
- package/dist/relation.d.ts.map +1 -1
- package/dist/relation.js +16 -10
- package/dist/relation.js.map +1 -1
- package/dist/result.d.ts +47 -0
- package/dist/result.d.ts.map +1 -0
- package/dist/result.js +50 -0
- package/dist/result.js.map +1 -0
- package/dist/rows.d.ts +69 -0
- package/dist/rows.d.ts.map +1 -0
- package/dist/rows.js +328 -0
- package/dist/rows.js.map +1 -0
- package/dist/runtime-codes.d.ts +2 -0
- package/dist/runtime-codes.d.ts.map +1 -0
- package/dist/runtime-codes.js +19 -0
- package/dist/runtime-codes.js.map +1 -0
- package/dist/runtime-errors.d.ts +106 -0
- package/dist/runtime-errors.d.ts.map +1 -0
- package/dist/runtime-errors.js +62 -0
- package/dist/runtime-errors.js.map +1 -0
- package/dist/runtime-native.d.ts +171 -0
- package/dist/runtime-native.d.ts.map +1 -0
- package/dist/runtime-native.js +5 -0
- package/dist/runtime-native.js.map +1 -0
- package/dist/runtime.d.ts +73 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +293 -0
- package/dist/runtime.js.map +1 -0
- package/dist/scalar.d.ts +165 -0
- package/dist/scalar.d.ts.map +1 -0
- package/dist/scalar.js +344 -0
- package/dist/scalar.js.map +1 -0
- package/dist/schema.d.ts +1 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +34 -37
- package/dist/schema.js.map +1 -1
- package/dist/shape.d.ts +28 -0
- package/dist/shape.d.ts.map +1 -0
- package/dist/shape.js +2 -0
- package/dist/shape.js.map +1 -0
- package/dist/spec.d.ts +27 -3
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js +22 -1
- package/dist/spec.js.map +1 -1
- package/dist/statements.d.ts +16 -3
- package/dist/statements.d.ts.map +1 -1
- package/dist/statements.js +54 -28
- package/dist/statements.js.map +1 -1
- package/dist/uuid.d.ts +23 -0
- package/dist/uuid.d.ts.map +1 -0
- package/dist/uuid.js +54 -0
- package/dist/uuid.js.map +1 -0
- package/pack-provenance.json +7 -0
- package/package.json +12 -16
- package/src/capacity.ts +34 -54
- package/src/changes.ts +361 -0
- package/src/close.ts +53 -0
- package/src/closed.ts +25 -19
- package/src/codec.ts +379 -0
- package/src/compile.ts +149 -0
- package/src/db-native.ts +247 -0
- package/src/db.ts +461 -1632
- package/src/errors.ts +50 -0
- package/src/face.ts +2 -2
- package/src/fields.ts +182 -55
- package/src/index.ts +62 -126
- package/src/internal/log.ts +51 -0
- package/src/law.ts +13 -28
- package/src/lower.ts +9 -4
- package/src/migration.ts +69 -0
- package/src/native.ts +326 -487
- package/src/query/atom.ts +61 -25
- package/src/query/compute.ts +237 -0
- package/src/query/find.ts +31 -12
- package/src/query/lower.ts +531 -143
- package/src/query/parse-ir.ts +18 -13
- package/src/query/run.ts +32 -16
- package/src/query/scope.ts +140 -8
- package/src/relation.ts +17 -21
- package/src/result.ts +130 -0
- package/src/rows.ts +404 -0
- package/src/runtime-codes.ts +18 -0
- package/src/runtime-errors.ts +69 -0
- package/src/runtime-native.ts +242 -0
- package/src/runtime.ts +440 -0
- package/src/scalar.ts +560 -0
- package/src/schema.ts +35 -37
- package/src/shape.ts +31 -0
- package/src/spec.ts +33 -3
- package/src/statements.ts +75 -68
- package/src/uuid.ts +69 -0
- package/dist/marshal.d.ts +0 -44
- package/dist/marshal.d.ts.map +0 -1
- package/dist/marshal.js +0 -165
- package/dist/marshal.js.map +0 -1
- package/src/marshal.ts +0 -200
package/COOKBOOK.md
CHANGED
|
@@ -1,1729 +1,401 @@
|
|
|
1
|
-
# The
|
|
1
|
+
# The Bumbledb TypeScript cookbook
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
skin.
|
|
3
|
+
Worked recipes for the Effect-native surface: typed schema values,
|
|
4
|
+
declared keys and laws, application-owned `Uuid` identity, one bounded
|
|
5
|
+
native runtime, scoped resources, immutable final-state changes, `Option`
|
|
6
|
+
reads, sealed complete results, and one-shot page streams.
|
|
8
7
|
|
|
9
|
-
Every `ts` fence below
|
|
10
|
-
`
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
edit here whose code stops compiling fails the build. The runtime half of
|
|
14
|
-
the claim is `ts/test/cookbook.test.ts`'s, over compiled copies of the
|
|
15
|
-
recipes: each schema is constructed through the public surface, admitted
|
|
16
|
-
on a real store (the engine's schema validation is the acceptance
|
|
17
|
-
judgment), its fingerprint asserted stable across a reopen AND equal to
|
|
18
|
-
the per-recipe cross-host golden the Rust cookbook suite also pins
|
|
19
|
-
(`fixtures/cookbook-fingerprints.txt` at the repository root — the two
|
|
20
|
-
cookbooks teach one
|
|
21
|
-
theory per recipe number, provably), and every query snippet lowered
|
|
22
|
-
through `db.prepare` (the engine's own IR validation).
|
|
23
|
-
|
|
24
|
-
Guarantee labels that name Lean results cite the checked spec in the engine
|
|
25
|
-
repo's `lean/` by theorem name, exactly as the engine cookbook does
|
|
26
|
-
(`scripts/spec-census.sh` verifies every citation resolves); the label always
|
|
27
|
-
names any additional Rust premise.
|
|
28
|
-
|
|
29
|
-
Everything below imports from the one package entry:
|
|
8
|
+
Every `ts` fence below is extracted and type-checked against the real
|
|
9
|
+
package surface by `test/cookbook-doc.test.ts`; the imports fence here is
|
|
10
|
+
prepended to every recipe. The examples are lazy Effect programs — nothing
|
|
11
|
+
below runs a database at import time, and measured policies are inputs.
|
|
30
12
|
|
|
31
13
|
```ts
|
|
14
|
+
import { Effect, Option, Result, Stream } from "effect"
|
|
32
15
|
import {
|
|
33
|
-
ALLEN,
|
|
34
|
-
Db,
|
|
35
|
-
type Infer,
|
|
36
|
-
abandon,
|
|
37
|
-
bool,
|
|
38
|
-
bytes,
|
|
39
16
|
capacity,
|
|
17
|
+
ChangeSet,
|
|
40
18
|
closed,
|
|
41
19
|
contained,
|
|
20
|
+
Db,
|
|
42
21
|
duration,
|
|
22
|
+
f64,
|
|
43
23
|
i64,
|
|
24
|
+
uuid,
|
|
25
|
+
Uuid,
|
|
44
26
|
interval,
|
|
45
27
|
key,
|
|
46
28
|
mirrors,
|
|
29
|
+
NativeRuntime,
|
|
47
30
|
on,
|
|
48
31
|
query,
|
|
49
32
|
ref,
|
|
50
33
|
relation,
|
|
34
|
+
Scalar,
|
|
51
35
|
schema,
|
|
36
|
+
span,
|
|
52
37
|
str,
|
|
53
38
|
u64,
|
|
54
39
|
v,
|
|
55
40
|
weigh,
|
|
56
41
|
within
|
|
57
42
|
} from "@bjornpagen/bumbledb"
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
included). Three laws govern the classes:
|
|
67
|
-
|
|
68
|
-
1. **A fresh field is a generator** and names its class by its declaration
|
|
69
|
-
coordinate (`"Attempt.id"`); a closed relation's id is a generator named
|
|
70
|
-
the same way (`"Kind.id"`).
|
|
71
|
-
2. **A generator-less class** is named by its least member coordinate in
|
|
72
|
-
relation-declaration × field-declaration order (recipe 5's
|
|
73
|
-
`"Document.payload"`), deterministic and pinned forever.
|
|
74
|
-
3. **A field in no law is bare, and bare pairs only with bare** in queries —
|
|
75
|
-
a deliberate sum-domain pointer stays legal because you simply write no
|
|
76
|
-
law over it.
|
|
77
|
-
|
|
78
|
-
The wall: at most ONE generator per class — a statement list that unifies two
|
|
79
|
-
fresh coordinates is a contradiction (two mints cannot share a carrier), a
|
|
80
|
-
schema-level compile error with a construction-time runtime twin. The
|
|
81
|
-
dividend is that the statements you already write ARE the typing: a query
|
|
82
|
-
variable first bound in one class refuses to join a field of another (compile
|
|
83
|
-
error, with the same refusal thrown at construction), and no label, link, or
|
|
84
|
-
domain declaration exists anywhere on the surface. When a recipe below says a
|
|
85
|
-
statement "types" a column, this machinery is what it means. Classes never
|
|
86
|
-
touch the fingerprint — identity hashes canonical descriptor bytes, never the
|
|
87
|
-
law-computed names.
|
|
88
|
-
|
|
89
|
-
## Foundations
|
|
90
|
-
|
|
91
|
-
## 1. The minimal interval schema
|
|
92
|
-
|
|
93
|
-
Guarantee: Lean theorem + validator/runtime premise — the pointwise key
|
|
94
|
-
enforces per-service disjointness (`lean/Bumbledb/Dependencies.lean:
|
|
95
|
-
pointwise_key_disjoint`); checked intervals supply nonempty values
|
|
96
|
-
(`lean/Bumbledb/Values.lean: interval_nonempty`).
|
|
97
|
-
|
|
98
|
-
One fact per outage window; the pointwise key is the whole temporal design.
|
|
99
|
-
|
|
100
|
-
```ts
|
|
101
|
-
const Service = relation("Service", { id: u64.fresh, name: str })
|
|
102
|
-
// The window is one value, not a (start, end) column pair: the denotation
|
|
103
|
-
// (a set of points, half-open) is what the judgments below read through.
|
|
104
|
-
// The containment below is what types `service` — the class laws.
|
|
105
|
-
const Outage = relation("Outage", { service: u64, window: interval(i64) })
|
|
106
|
-
|
|
107
|
-
const Uptime = schema("Uptime", { Service, Outage }, [
|
|
108
|
-
contained(on(Outage, "service"), on(Service, "id")),
|
|
109
|
-
// The pointwise key: per service, no two outages share a point — every
|
|
110
|
-
// pair satisfies DISJOINT. SQL:2011's WITHOUT OVERLAPS, as a theorem.
|
|
111
|
-
// `key(R, [...])` is the host flavor of the canonical arrow — this one
|
|
112
|
-
// renders `Outage(service, window) -> Outage`: the key projection
|
|
113
|
-
// determines the tuple, and the arrow closing over its own relation is
|
|
114
|
-
// what makes a key a key (the compile-pin asserts the render).
|
|
115
|
-
key(Outage, ["service", "window"])
|
|
116
|
-
])
|
|
117
|
-
|
|
118
|
-
// down at instant t — v(Outage) mints a fresh variable per column (typed by
|
|
119
|
-
// that column's law-class); destructure what you bind, reuse to join:
|
|
120
|
-
const downAt = query(Uptime).rule((r) => {
|
|
121
|
-
const { service, window } = v(Outage)
|
|
122
|
-
return r
|
|
123
|
-
.match(Outage, { service, window })
|
|
124
|
-
.where(r.pointIn(r.param("t"), window))
|
|
125
|
-
.find({ service })
|
|
126
|
-
})
|
|
127
|
-
// overlapping an incident window (one Allen mask, no operator zoo):
|
|
128
|
-
const overlapping = query(Uptime).rule((r) => {
|
|
129
|
-
const { service, window } = v(Outage)
|
|
130
|
-
return r
|
|
131
|
-
.match(Outage, { service, window })
|
|
132
|
-
.where(r.allen(window, ALLEN.intersects, r.param("incident")))
|
|
133
|
-
.find({ service, window })
|
|
134
|
-
})
|
|
135
|
-
// total downtime per service is host arithmetic on the interval
|
|
136
|
-
// endpoints every answer row already carries (`end − start`).
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
## 2. Discriminated unions
|
|
140
|
-
|
|
141
|
-
Guarantee: Lean theorem + validator/runtime premises — key-backed equality
|
|
142
|
-
gives unique source/target correspondence (`lean/Bumbledb/Dependencies.lean:
|
|
143
|
-
keyed_eq_unique_correspondence`); both projections must resolve to declared keys.
|
|
144
|
-
|
|
145
|
-
Sum-typed entities: a closed-relation discriminator plus per-arm child
|
|
146
|
-
relations, glued by bidirectional conditional containments.
|
|
147
|
-
|
|
148
|
-
```ts
|
|
149
|
-
// The discriminator vocabulary is a closed relation: its ground axioms are
|
|
150
|
-
// axioms, and a handle is its NAME — the string literal "Deterministic" is
|
|
151
|
-
// the ONE spelling, on every surface (statements, inserts, queries, rows).
|
|
152
|
-
const Kind = closed("Kind", ["Deterministic", "CustomOperator"])
|
|
153
|
-
const Task = relation("Task", { id: u64.fresh, kind: Kind.id })
|
|
154
|
-
const DeterministicGrading = relation("DeterministicGrading", { task: u64, tolerance: i64 })
|
|
155
|
-
const CustomOperatorGrading = relation("CustomOperatorGrading", { task: u64, operator: str })
|
|
156
|
-
|
|
157
|
-
const Grading = schema("Grading", { Kind, Task, DeterministicGrading, CustomOperatorGrading }, [
|
|
158
|
-
contained(on(Task, "kind"), on(Kind, "id")), // the discriminator resolves
|
|
159
|
-
key(DeterministicGrading, ["task"]), // one arm fact per parent
|
|
160
|
-
key(CustomOperatorGrading, ["task"]),
|
|
161
|
-
// Totality (==, left to right): a Deterministic task HAS its arm fact —
|
|
162
|
-
// same commit, always. Arm validity (right to left): an arm fact's parent
|
|
163
|
-
// exists WITH that kind — composite-FK-plus-CHECK, one statement. These
|
|
164
|
-
// mirrors are also what type `task` on both arms: each lands in the
|
|
165
|
-
// "Task.id" generator class.
|
|
166
|
-
mirrors(on(Task.where({ kind: "Deterministic" }), "id"), on(DeterministicGrading, "task")),
|
|
167
|
-
mirrors(on(Task.where({ kind: "CustomOperator" }), "id"), on(CustomOperatorGrading, "task"))
|
|
168
|
-
// Exclusivity is a theorem, not a statement: one id in two arms would
|
|
169
|
-
// force `kind` to equal two handles against the fresh key on id.
|
|
170
|
-
// The executor spends the same theorem again as a diagnostic witness
|
|
171
|
-
// (recipe 22); multi-rule execution still keeps a spanning seen-set.
|
|
172
|
-
])
|
|
173
|
-
|
|
174
|
-
// Host dispatch over the discriminator is native `switch` narrowing over
|
|
175
|
-
// the handle union (`Infer<typeof Kind.id>` = "Deterministic" |
|
|
176
|
-
// "CustomOperator") — rows already arrive carrying the handle name, and
|
|
177
|
-
// `satisfies never` makes the switch exhaustive: a missing arm is a
|
|
178
|
-
// compile error.
|
|
179
|
-
const gradedBy = (kind: Infer<typeof Kind.id>) => {
|
|
180
|
-
switch (kind) {
|
|
181
|
-
case "Deterministic":
|
|
182
|
-
return "tolerance"
|
|
183
|
-
case "CustomOperator":
|
|
184
|
-
return "operator"
|
|
185
|
-
default:
|
|
186
|
-
return kind satisfies never
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
## 3. 0..1 optional attributes
|
|
192
|
-
|
|
193
|
-
Guarantee: Lean theorem + validator/runtime premises — the child key proves at
|
|
194
|
-
most one fact (`lean/Bumbledb/Dependencies.lean: functionality_unique_witness`)
|
|
195
|
-
and containment requires its parent (`lean/Bumbledb/Dependencies.lean:
|
|
196
|
-
contains_iff_view_subset`); absence remains legal.
|
|
197
|
-
|
|
198
|
-
No nulls, anywhere. Optional data is an absent fact in a child relation; the
|
|
199
|
-
child's key plus a one-way containment *is* "nullable column", done honestly.
|
|
200
|
-
|
|
201
|
-
```ts
|
|
202
|
-
const Business = relation("Business", { id: u64.fresh, name: str })
|
|
203
|
-
const MailingAddress = relation("MailingAddress", { business: u64, line: str, city: str })
|
|
204
|
-
|
|
205
|
-
const Optionality = schema("Optionality", { Business, MailingAddress }, [
|
|
206
|
-
key(MailingAddress, ["business"]), // at most one address...
|
|
207
|
-
contained(on(MailingAddress, "business"), on(Business, "id")) // ...and only for a real business
|
|
208
|
-
// One-way containment on purpose: absence is the fact that isn't. The
|
|
209
|
-
// all-or-nothing column group (line+city together or neither) is
|
|
210
|
-
// unstatable TO VIOLATE — the fact carries both fields or does not exist.
|
|
211
|
-
])
|
|
212
|
-
|
|
213
|
-
// Negation is plain anti-join (no null branch exists in any operator):
|
|
214
|
-
const unaddressed = query(Optionality).rule((r) => {
|
|
215
|
-
const { id: b } = v(Business)
|
|
216
|
-
return r
|
|
217
|
-
.match(Business, { id: b })
|
|
218
|
-
.where(r.not(MailingAddress, { business: b }))
|
|
219
|
-
.find({ b })
|
|
220
|
-
})
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
## 4. Money
|
|
224
|
-
|
|
225
|
-
Guarantee: host discipline + validator premises — fixed-point scale and
|
|
226
|
-
currency grouping live in host domains; containments only resolve references.
|
|
227
|
-
|
|
228
|
-
Fixed-point i64 minor units. Floats are permanently refused (the ledger);
|
|
229
|
-
proration and FX are host arithmetic. `minor` sits in no law — bare, and bare
|
|
230
|
-
pairs only with bare (the class laws), so a stray join against some other
|
|
231
|
-
i64 column refuses unless a statement puts them in one class. Scale and
|
|
232
|
-
currency intent are host discipline; the closed `currency` reference is what
|
|
233
|
-
the schema CAN say, and says.
|
|
234
|
-
|
|
235
|
-
```ts
|
|
236
|
-
const Currency = closed("Currency", ["Usd", "Eur", "Gbp"])
|
|
237
|
-
const Account = relation("Account", { id: u64.fresh, name: str })
|
|
238
|
-
const Posting = relation("Posting", {
|
|
239
|
-
id: u64.fresh,
|
|
240
|
-
account: u64,
|
|
241
|
-
currency: Currency.id,
|
|
242
|
-
minor: i64
|
|
243
|
-
})
|
|
244
|
-
|
|
245
|
-
const Money = schema("Money", { Currency, Account, Posting }, [
|
|
246
|
-
contained(on(Posting, "account"), on(Account, "id")),
|
|
247
|
-
contained(on(Posting, "currency"), on(Currency, "id"))
|
|
248
|
-
])
|
|
43
|
+
import type {
|
|
44
|
+
ApplyOutcome,
|
|
45
|
+
CompleteResult,
|
|
46
|
+
ExecutionPolicy,
|
|
47
|
+
Fact,
|
|
48
|
+
NativeRuntimeOptions,
|
|
49
|
+
QueryReader
|
|
50
|
+
} from "@bjornpagen/bumbledb"
|
|
249
51
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
// (account, currency, minor) postings without it.
|
|
254
|
-
const totals = query(Money).rule((r) => {
|
|
255
|
-
const { id, account, currency, minor } = v(Posting)
|
|
256
|
-
return r.match(Posting, { id, account, currency, minor }).find({ account, currency, total: r.sum(minor) })
|
|
257
|
-
})
|
|
52
|
+
declare const work: ExecutionPolicy
|
|
53
|
+
declare const runtimePolicy: NativeRuntimeOptions
|
|
54
|
+
declare const localPath: string
|
|
258
55
|
```
|
|
259
56
|
|
|
260
|
-
##
|
|
261
|
-
|
|
262
|
-
Guarantee: validator/runtime premises + host discipline — the payload key and
|
|
263
|
-
containments enforce identity/reference shape; hashing and blob durability stay external.
|
|
57
|
+
## 1. One schema, typed twice — relations, declared keys, laws
|
|
264
58
|
|
|
265
|
-
|
|
266
|
-
|
|
59
|
+
Identity fields are ordinary application-owned `Uuid` values: the database
|
|
60
|
+
issues no identity and there is no `fresh` mint. Keys are declared
|
|
61
|
+
statements; references and capacity are laws over the same fields. The same
|
|
62
|
+
declarations, spelled in Rust's `schema!`, produce the same canonical schema
|
|
63
|
+
identity.
|
|
267
64
|
|
|
268
65
|
```ts
|
|
269
|
-
const
|
|
270
|
-
const
|
|
271
|
-
id:
|
|
272
|
-
|
|
273
|
-
|
|
66
|
+
const Student = relation("Student", { id: uuid, name: str, budget: u64 })
|
|
67
|
+
const Attempt = relation("Attempt", {
|
|
68
|
+
id: uuid,
|
|
69
|
+
student: uuid,
|
|
70
|
+
score: f64,
|
|
71
|
+
units: u64,
|
|
72
|
+
active: interval(i64)
|
|
274
73
|
})
|
|
275
|
-
const Replica = relation("Replica", { payload: bytes(32), region: Region.id })
|
|
276
74
|
|
|
277
|
-
const
|
|
278
|
-
key(
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
// facts stay fixed-width; the payload lives in external storage,
|
|
287
|
-
// referenced by identity (the large-object refusal).
|
|
75
|
+
const Learning = schema("Learning", { Student, Attempt }, [
|
|
76
|
+
key(Student, ["id"]),
|
|
77
|
+
key(Attempt, ["id"]),
|
|
78
|
+
contained(on(Attempt, "student"), on(Student, "id")),
|
|
79
|
+
capacity(on(Student, "id"), {
|
|
80
|
+
from: on(Attempt, "student"),
|
|
81
|
+
weight: weigh("units"),
|
|
82
|
+
within: within(0n, ref("budget"))
|
|
83
|
+
})
|
|
288
84
|
])
|
|
289
85
|
|
|
290
|
-
//
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
86
|
+
// Schema construction is pure metadata: the value is inert data, and typing
|
|
87
|
+
// follows it — a Fact<typeof Attempt> is the inferred row object.
|
|
88
|
+
declare const row: Fact<typeof Attempt>
|
|
89
|
+
const scoreIsNumber: number = row.score
|
|
90
|
+
const unitsAreExact: bigint = row.units
|
|
91
|
+
void [Learning, scoreIsNumber, unitsAreExact]
|
|
295
92
|
```
|
|
296
93
|
|
|
297
|
-
##
|
|
298
|
-
|
|
299
|
-
## 6. The vocabulary
|
|
94
|
+
## 2. One runtime layer; explicit create and open
|
|
300
95
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
The enum idiom's replacement, first-class: a vocabulary is a **closed
|
|
307
|
-
relation** — its ground axioms are declared in the schema, sealed at
|
|
308
|
-
validate, frozen by the fingerprint, virtual in storage. The store holds zero
|
|
309
|
-
vocabulary bytes, and handle names are the string literals on every surface.
|
|
96
|
+
`NativeRuntime.layer(options)` acquires the single bounded native runtime
|
|
97
|
+
with scope; reuse ONE layer value so Effect's memoization shares it.
|
|
98
|
+
`Db.open` never creates a missing database; `Db.create` refuses existing
|
|
99
|
+
authority. Both are scoped acquisitions.
|
|
310
100
|
|
|
311
101
|
```ts
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
// vocabulary stays relational (ids = declaration order) and the marshal
|
|
315
|
-
// owns the bijection. Dispatch is native `switch` narrowing (recipe 2).
|
|
316
|
-
const Priority = closed("Priority", ["Low", "Normal", "Urgent"])
|
|
317
|
-
|
|
318
|
-
const Ticket = relation("Ticket", { id: u64.fresh, priority: Priority.id, opened_at: i64 })
|
|
319
|
-
|
|
320
|
-
const Tickets = schema("Tickets", { Priority, Ticket }, [
|
|
321
|
-
// A closed reference is an ordinary u64 under one containment (which also
|
|
322
|
-
// types `priority` into the "Priority.id" generator class); the judgment
|
|
323
|
-
// compiles at validate to a member-set test — one AND, one bit test, no
|
|
324
|
-
// probe (30-dependencies.md).
|
|
325
|
-
contained(on(Ticket, "priority"), on(Priority, "id"))
|
|
326
|
-
])
|
|
327
|
-
|
|
328
|
-
// Handles are literals in queries exactly as in statements, and the
|
|
329
|
-
// renderer prints them back — the round trip runs on names. The boundary
|
|
330
|
-
// law: intrinsic meaning goes here (changing it is a new theory); policy
|
|
331
|
-
// that drifts without a rebuild is an ordinary relation — a vocabulary is
|
|
332
|
-
// never written, only declared.
|
|
333
|
-
const urgent = query(Tickets).rule((r) => {
|
|
334
|
-
const { id: t } = v(Ticket)
|
|
335
|
-
return r.match(Ticket, { id: t, priority: "Urgent" }).find({ t })
|
|
336
|
-
})
|
|
337
|
-
|
|
338
|
-
// Set membership is a plain array — the drizzle law's spelling, closed-only
|
|
339
|
-
// in query match records (an ordinary u64/str field's membership is a bound
|
|
340
|
-
// ∈-set param, `r.inSet`); the array folds to the same wire set the param
|
|
341
|
-
// spelling crosses. In `.where()` selections arrays work at EVERY field kind.
|
|
342
|
-
const actionable = query(Tickets).rule((r) => {
|
|
343
|
-
const { id: t } = v(Ticket)
|
|
344
|
-
return r.match(Ticket, { id: t, priority: ["Normal", "Urgent"] }).find({ t })
|
|
345
|
-
})
|
|
346
|
-
```
|
|
347
|
-
|
|
348
|
-
## 7. The classification
|
|
102
|
+
const Doc = relation("Doc", { id: uuid, text: str })
|
|
103
|
+
const Docs = schema("Docs", { Doc }, [key(Doc, ["id"])])
|
|
349
104
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
columns** — one ground axiom per handle, values sealed with the schema, read
|
|
356
|
-
by ψ-selections. Axioms are declared, never written.
|
|
357
|
-
|
|
358
|
-
```ts
|
|
359
|
-
// Tier 2: payload columns state what each word MEANS, next to the word.
|
|
360
|
-
// A rubric change is a new theory — exactly right for meaning.
|
|
361
|
-
const Kind = closed(
|
|
362
|
-
"Kind",
|
|
363
|
-
["DirectPass", "JudgedPass", "Failed"],
|
|
364
|
-
{ mastered: bool, rank: u64 },
|
|
365
|
-
{
|
|
366
|
-
DirectPass: { mastered: true, rank: 30n },
|
|
367
|
-
JudgedPass: { mastered: true, rank: 20n },
|
|
368
|
-
Failed: { mastered: false, rank: 10n }
|
|
369
|
-
}
|
|
105
|
+
const openExisting = Effect.scoped(
|
|
106
|
+
Effect.gen(function* () {
|
|
107
|
+
const db = yield* Db.open(localPath, Docs, work)
|
|
108
|
+
return db.schemaId
|
|
109
|
+
})
|
|
370
110
|
)
|
|
371
|
-
const Attempt = relation("Attempt", { id: u64.fresh, kind: Kind.id })
|
|
372
|
-
const Certificate = relation("Certificate", { attempt: u64, kind: Kind.id })
|
|
373
|
-
|
|
374
|
-
const Review = schema("Review", { Kind, Attempt, Certificate }, [
|
|
375
|
-
contained(on(Attempt, "kind"), on(Kind, "id")),
|
|
376
|
-
key(Certificate, ["attempt"]),
|
|
377
|
-
contained(on(Certificate, "attempt"), on(Attempt, "id")),
|
|
378
|
-
// ψ reads the payload: certificates carry mastered kinds only — the
|
|
379
|
-
// member set {DirectPass, JudgedPass} compiles at validate and the
|
|
380
|
-
// judgment is O(1) at commit (recipe 8 is this statement's own recipe).
|
|
381
|
-
contained(on(Certificate, "kind"), on(Kind.where({ mastered: true }), "id"))
|
|
382
|
-
])
|
|
383
111
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
const { id: a, kind: k } = v(Attempt)
|
|
390
|
-
return r
|
|
391
|
-
.match(Attempt, { id: a, kind: k })
|
|
392
|
-
.match(Kind, { id: k, mastered: true })
|
|
393
|
-
.find({ a })
|
|
394
|
-
})
|
|
395
|
-
|
|
396
|
-
// Host dispatch on the payload tier is the record-table idiom — a `Record`
|
|
397
|
-
// over the handle union is total by type (a missing or extra entry is a
|
|
398
|
-
// compile error), and each entry reads its sealed axiom row off the typed
|
|
399
|
-
// `Kind.axioms` readback:
|
|
400
|
-
const labels: Record<Infer<typeof Kind.id>, string> = {
|
|
401
|
-
DirectPass: `mastered, rank ${Kind.axioms.DirectPass.rank}`,
|
|
402
|
-
JudgedPass: `mastered, rank ${Kind.axioms.JudgedPass.rank}`,
|
|
403
|
-
Failed: "not mastered"
|
|
404
|
-
}
|
|
405
|
-
const label = (k: Infer<typeof Kind.id>) => labels[k]
|
|
406
|
-
```
|
|
407
|
-
|
|
408
|
-
Two honest boundaries. The fold has limits: payload escaping to the head and
|
|
409
|
-
param-bearing filters don't fold at prepare — the engine falls back to a
|
|
410
|
-
virtual-image join over the sealed extension, and the semantics are identical
|
|
411
|
-
either way. And for an ALREADY-DEPLOYED store, moving from the old
|
|
412
|
-
complement-window workaround to these ψ spellings is a NEW theory — a
|
|
413
|
-
different fingerprint — which is recipe 28's ETL territory: humans own that
|
|
414
|
-
migration.
|
|
415
|
-
|
|
416
|
-
## 8. The sub-vocabulary
|
|
417
|
-
|
|
418
|
-
Guarantee: validator/runtime premise — ψ over the sealed extension compiles
|
|
419
|
-
the exact paging member set; a nonmember write is commit-rejected.
|
|
420
|
-
|
|
421
|
-
The ψ-selected containment: a reference constrained to the facts of a
|
|
422
|
-
vocabulary that satisfy a payload selection. Because the target is closed and
|
|
423
|
-
sealed, the enforcement plan is not a probe strategy — it is **the answer set
|
|
424
|
-
itself**, fixed when the schema is built.
|
|
425
|
-
|
|
426
|
-
```ts
|
|
427
|
-
const Severity = closed(
|
|
428
|
-
"Severity",
|
|
429
|
-
["Info", "Warning", "Critical", "Fatal"],
|
|
430
|
-
{ pages: bool },
|
|
431
|
-
{
|
|
432
|
-
Info: { pages: false },
|
|
433
|
-
Warning: { pages: false },
|
|
434
|
-
Critical: { pages: true },
|
|
435
|
-
Fatal: { pages: true }
|
|
436
|
-
}
|
|
112
|
+
const createOnce = Effect.scoped(
|
|
113
|
+
Effect.gen(function* () {
|
|
114
|
+
const db = yield* Db.create(localPath, Docs, work)
|
|
115
|
+
return db.schemaId
|
|
116
|
+
})
|
|
437
117
|
)
|
|
438
|
-
const Incident = relation("Incident", { id: u64.fresh, severity: Severity.id })
|
|
439
|
-
const Escalation = relation("Escalation", { incident: u64, severity: Severity.id, at: i64 })
|
|
440
|
-
|
|
441
|
-
const Oncall = schema("Oncall", { Severity, Incident, Escalation }, [
|
|
442
|
-
contained(on(Incident, "severity"), on(Severity, "id")),
|
|
443
|
-
contained(on(Escalation, "incident"), on(Incident, "id")),
|
|
444
|
-
// The sub-vocabulary: an escalation carries a PAGING severity, by
|
|
445
|
-
// statement. ψ over the sealed extension compiles to the member set
|
|
446
|
-
// {Critical, Fatal}; the judgment is one bit test per touched fact,
|
|
447
|
-
// and an escalation at "Info" aborts the commit.
|
|
448
|
-
contained(on(Escalation, "severity"), on(Severity.where({ pages: true }), "id"))
|
|
449
|
-
])
|
|
450
|
-
|
|
451
|
-
// who is being paged — the same ψ, on the read side:
|
|
452
|
-
const paged = query(Oncall).rule((r) => {
|
|
453
|
-
const { incident: i, severity: s } = v(Escalation)
|
|
454
|
-
return r
|
|
455
|
-
.match(Escalation, { incident: i, severity: s })
|
|
456
|
-
.match(Severity, { id: s, pages: true })
|
|
457
|
-
.find({ i })
|
|
458
|
-
})
|
|
459
|
-
```
|
|
460
|
-
|
|
461
|
-
## Structure
|
|
462
|
-
|
|
463
|
-
## 9. Ordered collections
|
|
464
|
-
|
|
465
|
-
Guarantee: Lean theorem + validator/runtime premises — mutual point coverage
|
|
466
|
-
plus pointwise keys realizes exact partition
|
|
467
|
-
(`lean/Bumbledb/Dependencies.lean: exact_partition_iff`), and the mixed-width
|
|
468
|
-
interval positions type by element domain
|
|
469
|
-
(`lean/Bumbledb/Schema.lean: Value.points_one_tag_u64`); ordering the result
|
|
470
|
-
remains a host presentation step — the SDK ships the comparator (`by`/`desc`,
|
|
471
|
-
keys as data); the engine never orders.
|
|
472
|
-
|
|
473
|
-
The linked-list verdict: successor pointers are control flow smuggled into
|
|
474
|
-
data. Order is a value. The idiomatic ordered collection is an interval
|
|
475
|
-
partition, spelled as a **triple**: the entity, the extent as a 0..1 child
|
|
476
|
-
(empty lists exist, empty intervals do not — presence of the child IS
|
|
477
|
-
nonemptiness), and the unit-slot sidecar (`interval(u64, 1n)` — the width is
|
|
478
|
-
the type: a wrong-width value is unrepresentable).
|
|
479
|
-
|
|
480
|
-
```ts
|
|
481
|
-
const Playlist = relation("Playlist", { id: u64.fresh, name: str })
|
|
482
|
-
// The extent: a 0..1 child, because empty playlists exist and empty
|
|
483
|
-
// intervals do not — presence of the child IS nonemptiness.
|
|
484
|
-
const Extent = relation("Extent", { playlist: u64, span: interval(u64) })
|
|
485
|
-
// The unit slot: position p occupies [p, p+1) — the width is the type.
|
|
486
|
-
const Slot = relation("Slot", { playlist: u64, slot: interval(u64, 1n), track: str })
|
|
487
|
-
|
|
488
|
-
const Playlists = schema("Playlists", { Playlist, Extent, Slot }, [
|
|
489
|
-
contained(on(Extent, "playlist"), on(Playlist, "id")),
|
|
490
|
-
contained(on(Slot, "playlist"), on(Playlist, "id")),
|
|
491
|
-
key(Extent, ["playlist"]), // 0..1 extent per playlist
|
|
492
|
-
key(Extent, ["playlist", "span"]), // exact target key (recipe 26's note)
|
|
493
|
-
key(Slot, ["playlist", "slot"]), // one occupant per position
|
|
494
|
-
mirrors(on(Extent, ["playlist", "span"]), on(Slot, ["playlist", "slot"])) // slots tile the span exactly
|
|
495
|
-
])
|
|
496
|
-
|
|
497
|
-
// Positional access is membership — "what plays at position ?pos":
|
|
498
|
-
const playingAt = query(Playlists).rule((r) => {
|
|
499
|
-
const { slot, track } = v(Slot)
|
|
500
|
-
return r
|
|
501
|
-
.match(Slot, { playlist: r.param("list"), slot, track })
|
|
502
|
-
.where(r.pointIn(r.param("pos"), slot))
|
|
503
|
-
.find({ track })
|
|
504
|
-
})
|
|
505
|
-
|
|
506
|
-
// Answers are SETS — the host sorts them. Limit is the language's own
|
|
507
|
-
// `.slice(0, n)`.
|
|
508
|
-
const inPlayOrder = [
|
|
509
|
-
{ slot: { start: 1n, end: 2n }, track: "b" },
|
|
510
|
-
{ slot: { start: 0n, end: 1n }, track: "a" }
|
|
511
|
-
].sort(function bySlotThenTrack(left, right) {
|
|
512
|
-
if (left.slot.start < right.slot.start) {
|
|
513
|
-
return -1
|
|
514
|
-
}
|
|
515
|
-
if (left.slot.start > right.slot.start) {
|
|
516
|
-
return 1
|
|
517
|
-
}
|
|
518
|
-
if (left.slot.end < right.slot.end) {
|
|
519
|
-
return -1
|
|
520
|
-
}
|
|
521
|
-
if (left.slot.end > right.slot.end) {
|
|
522
|
-
return 1
|
|
523
|
-
}
|
|
524
|
-
if (left.track < right.track) {
|
|
525
|
-
return -1
|
|
526
|
-
}
|
|
527
|
-
if (left.track > right.track) {
|
|
528
|
-
return 1
|
|
529
|
-
}
|
|
530
|
-
return 0
|
|
531
|
-
})
|
|
532
|
-
```
|
|
533
|
-
|
|
534
|
-
Middle insert is honest about its cost: making room at position `k` shifts
|
|
535
|
-
every later slot and grows the extent — O(k) writes in **one delta**, judged
|
|
536
|
-
once at commit. If middle inserts dominate, the demoted escape hatch is the
|
|
537
|
-
spread slot: a scalar `pos: u64` written in gapped strides under the same
|
|
538
|
-
composite key — bumbledb has no lexicographic fractional indexing, because
|
|
539
|
-
string order is refused: there is no "between two strings" to allocate.
|
|
540
|
-
|
|
541
|
-
## 10. Trees and ASTs
|
|
542
|
-
|
|
543
|
-
Guarantee: Lean theorem + validator/runtime premises for key-backed arms
|
|
544
|
-
(`lean/Bumbledb/Dependencies.lean: keyed_eq_unique_correspondence`); host
|
|
545
|
-
discipline for acyclicity — statements prove arm/edge shape, never a tree theorem.
|
|
546
118
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
```
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
119
|
+
// One boundary; an Effect app provides the layer in its own graph instead.
|
|
120
|
+
const layer = NativeRuntime.layer(runtimePolicy)
|
|
121
|
+
void [openExisting.pipe(Effect.provide(layer)), createOnce.pipe(Effect.provide(layer))]
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## 3. Changes: build lazily, normalize once, apply atomically
|
|
125
|
+
|
|
126
|
+
`ChangeSet.builder` acquires a scoped database-free draft. Ingestion effects
|
|
127
|
+
are lazy and re-runnable while the draft is building — each execution reads
|
|
128
|
+
the then-current iterable and charges work again. Within ONE change set the
|
|
129
|
+
normalization is `(add, remove ∖ add)`: the identical fact's add wins
|
|
130
|
+
independent of call order. `finish()` consumes the draft into an immutable,
|
|
131
|
+
reusable `ChangeSet`.
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
const Task = relation("Task", { id: uuid, title: str, done: u64 })
|
|
135
|
+
const Tasks = schema("Tasks", { Task }, [key(Task, ["id"])])
|
|
136
|
+
|
|
137
|
+
const applyOnce = Effect.scoped(
|
|
138
|
+
Effect.gen(function* () {
|
|
139
|
+
const db = yield* Db.open(localPath, Tasks, work)
|
|
140
|
+
const taskId = yield* Effect.sync(() => crypto.randomUUID())
|
|
141
|
+
const draft = yield* ChangeSet.builder(Tasks, work)
|
|
142
|
+
yield* draft.insert(Task, [{ id: taskId, title: "write the cookbook", done: 0n }])
|
|
143
|
+
const changes = yield* draft.finish()
|
|
144
|
+
const outcome: ApplyOutcome = yield* db.apply(changes, { ...work, expected: { kind: "any" } })
|
|
145
|
+
switch (outcome.kind) {
|
|
146
|
+
case "accepted":
|
|
147
|
+
case "no-change":
|
|
148
|
+
return outcome.witness
|
|
149
|
+
case "invariant-rejected":
|
|
150
|
+
// Complete statement diagnostics, typed data — never a throw.
|
|
151
|
+
return yield* Effect.fail(outcome.violations)
|
|
152
|
+
case "moved":
|
|
153
|
+
return outcome.current
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
)
|
|
157
|
+
void applyOnce
|
|
585
158
|
```
|
|
586
159
|
|
|
587
|
-
##
|
|
588
|
-
|
|
589
|
-
Guarantee: validator/runtime premises — endpoint containments type each edge
|
|
590
|
-
and composite keys deduplicate pairs; no transitive graph property is claimed.
|
|
160
|
+
## 4. Keyed reads are Options
|
|
591
161
|
|
|
592
|
-
|
|
593
|
-
|
|
162
|
+
`get` reads through the relation's primary declared key. A missing key is
|
|
163
|
+
`Option.none` — never a fake I/O error, never a nullable row.
|
|
594
164
|
|
|
595
165
|
```ts
|
|
596
|
-
const Person = relation("Person", { id:
|
|
597
|
-
const
|
|
598
|
-
const Follows = relation("Follows", { follower: u64, followee: u64 })
|
|
599
|
-
const Maintains = relation("Maintains", { person: u64, repo: u64 })
|
|
166
|
+
const Person = relation("Person", { id: uuid, name: str })
|
|
167
|
+
const People = schema("People", { Person }, [key(Person, ["id"])])
|
|
600
168
|
|
|
601
|
-
const
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
key(Maintains, ["person", "repo"])
|
|
608
|
-
])
|
|
609
|
-
|
|
610
|
-
// Mutual follows — joins are explicit var reuse on both ends (both columns
|
|
611
|
-
// live in the "Person.id" class, so the reuse is lawful); `lt` keeps each
|
|
612
|
-
// pair once:
|
|
613
|
-
const mutual = query(Graph).rule((r) => {
|
|
614
|
-
const { follower: a, followee: b } = v(Follows)
|
|
615
|
-
return r
|
|
616
|
-
.match(Follows, { follower: a, followee: b })
|
|
617
|
-
.match(Follows, { follower: b, followee: a })
|
|
618
|
-
.where(r.lt(a, b))
|
|
619
|
-
.find({ a, b })
|
|
620
|
-
})
|
|
621
|
-
```
|
|
622
|
-
|
|
623
|
-
## 12. Entity-component
|
|
624
|
-
|
|
625
|
-
Guarantee: definition + validator/runtime premises — component keys give 0..1
|
|
626
|
-
and containments require the stated entity/archetype facts.
|
|
627
|
-
|
|
628
|
-
The 0..1 idiom (recipe 3) at scale: components are sidecar relations; an
|
|
629
|
-
entity has a component iff the fact exists; a new component kind is a new
|
|
630
|
-
relation, not a wider fact.
|
|
631
|
-
|
|
632
|
-
```ts
|
|
633
|
-
const Entity = relation("Entity", { id: u64.fresh, name: str })
|
|
634
|
-
const Transform = relation("Transform", { entity: u64, x: i64, y: i64 })
|
|
635
|
-
const Velocity = relation("Velocity", { entity: u64, dx: i64, dy: i64 })
|
|
636
|
-
const Renderable = relation("Renderable", { entity: u64, mesh: str })
|
|
637
|
-
|
|
638
|
-
const Ecs = schema("Ecs", { Entity, Transform, Velocity, Renderable }, [
|
|
639
|
-
key(Transform, ["entity"]), // each component 0..1 per entity
|
|
640
|
-
contained(on(Transform, "entity"), on(Entity, "id")),
|
|
641
|
-
key(Velocity, ["entity"]),
|
|
642
|
-
contained(on(Velocity, "entity"), on(Entity, "id")),
|
|
643
|
-
key(Renderable, ["entity"]),
|
|
644
|
-
// An archetype rule is one containment: every Renderable has a Transform
|
|
645
|
-
// (and, through it, an Entity — containment composes, and the class
|
|
646
|
-
// composes with it: every `entity` column lands in "Entity.id").
|
|
647
|
-
contained(on(Renderable, "entity"), on(Transform, "entity"))
|
|
648
|
-
])
|
|
649
|
-
|
|
650
|
-
// The physics join is the component intersection:
|
|
651
|
-
const physics = query(Ecs).rule((r) => {
|
|
652
|
-
const { entity, x, y } = v(Transform)
|
|
653
|
-
const { dx, dy } = v(Velocity)
|
|
654
|
-
return r
|
|
655
|
-
.match(Transform, { entity, x, y })
|
|
656
|
-
.match(Velocity, { entity, dx, dy })
|
|
657
|
-
.find({ entity, x, y, dx, dy })
|
|
658
|
-
})
|
|
169
|
+
const lookup = (reader: QueryReader<typeof People>, personId: Uuid) =>
|
|
170
|
+
Effect.gen(function* () {
|
|
171
|
+
const found = yield* reader.get(Person, { id: personId }, work)
|
|
172
|
+
return Option.isSome(found) ? found.value.name : "unknown"
|
|
173
|
+
})
|
|
174
|
+
void lookup
|
|
659
175
|
```
|
|
660
176
|
|
|
661
|
-
##
|
|
177
|
+
## 5. Queries are reusable typed values
|
|
662
178
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
States are a discriminated union; per-state data lives in arms; and the
|
|
668
|
-
conditional reference target — a reference to "an order *that is shipped*" —
|
|
669
|
-
is one selected statement, the statement SQL cannot write.
|
|
179
|
+
`v(R)` mints typed variables; reusing one across `match` records is the
|
|
180
|
+
join. The `find` record names the answer columns, and parameters infer from
|
|
181
|
+
use. A template contains no tenant rows or live snapshot — execute it
|
|
182
|
+
against any matching-schema reader.
|
|
670
183
|
|
|
671
184
|
```ts
|
|
672
|
-
const
|
|
673
|
-
const
|
|
674
|
-
const
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
contained(on(Order, "state"), on(State, "id")),
|
|
679
|
-
key(Placement, ["order"]),
|
|
680
|
-
key(Shipment, ["order"]),
|
|
681
|
-
// History accretes: a Shipped order keeps its Placement — one-way
|
|
682
|
-
// containment admits arms from earlier states surviving the transition.
|
|
683
|
-
contained(on(Placement, "order"), on(Order, "id")),
|
|
684
|
-
// The conditional target, both ways: every Shipment references an order
|
|
685
|
-
// THAT IS Shipped (validity), and every Shipped order has its Shipment
|
|
686
|
-
// (totality) — the transition and its evidence commit together.
|
|
687
|
-
mirrors(on(Shipment, "order"), on(Order.where({ state: "Shipped" }), "id"))
|
|
688
|
-
// Transition predicates ("only Placed may ship") are host code under the
|
|
689
|
-
// generation witness — recipe 20; the schema pins the states, not the paths.
|
|
185
|
+
const Author = relation("Author", { id: uuid, name: str })
|
|
186
|
+
const Book = relation("Book", { id: uuid, author: uuid, pages: u64 })
|
|
187
|
+
const Library = schema("Library", { Author, Book }, [
|
|
188
|
+
key(Author, ["id"]),
|
|
189
|
+
key(Book, ["id"]),
|
|
190
|
+
contained(on(Book, "author"), on(Author, "id"))
|
|
690
191
|
])
|
|
691
192
|
|
|
692
|
-
const
|
|
693
|
-
const { id } = v(
|
|
694
|
-
const {
|
|
193
|
+
const booksBy = query(Library).rule((r) => {
|
|
194
|
+
const { id, author, pages } = v(Book)
|
|
195
|
+
const { name } = v(Author)
|
|
695
196
|
return r
|
|
696
|
-
.match(
|
|
697
|
-
.match(
|
|
698
|
-
.
|
|
197
|
+
.match(Book, { id, author, pages })
|
|
198
|
+
.match(Author, { id: author, name })
|
|
199
|
+
.where(r.eq(name, r.param("name")))
|
|
200
|
+
.find({ id, pages })
|
|
699
201
|
})
|
|
700
|
-
```
|
|
701
|
-
|
|
702
|
-
## Time and coverage
|
|
703
|
-
|
|
704
|
-
## 14. The calendar core
|
|
705
202
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
it: pairing `source` with `Attendance.id` puts it in the `"Attendance.id"`
|
|
715
|
-
generator class. The class flows from the statement; no link declaration
|
|
716
|
-
exists or is needed.
|
|
717
|
-
|
|
718
|
-
```ts
|
|
719
|
-
const Rsvp = closed("Rsvp", ["Accepted", "Tentative", "Declined"])
|
|
720
|
-
const Arm = closed("Arm", ["Busy", "Ooo"])
|
|
721
|
-
|
|
722
|
-
const Person = relation("Person", { id: u64.fresh, name: str })
|
|
723
|
-
const Room = relation("Room", { id: u64.fresh, name: str })
|
|
724
|
-
const Event = relation("Event", { id: u64.fresh, span: interval(i64) })
|
|
725
|
-
const Attendance = relation("Attendance", {
|
|
726
|
-
id: u64.fresh,
|
|
727
|
-
event: u64,
|
|
728
|
-
person: u64,
|
|
729
|
-
rsvp: Rsvp.id
|
|
730
|
-
})
|
|
731
|
-
const Claim = relation("Claim", {
|
|
732
|
-
source: u64,
|
|
733
|
-
person: u64,
|
|
734
|
-
arm: Arm.id,
|
|
735
|
-
span: interval(i64)
|
|
736
|
-
})
|
|
737
|
-
const Booking = relation("Booking", { room: u64, event: u64, span: interval(i64) })
|
|
738
|
-
const WorkHours = relation("WorkHours", { person: u64, hours: interval(i64) })
|
|
739
|
-
|
|
740
|
-
const Calendar = schema("Calendar", { Rsvp, Arm, Person, Room, Event, Attendance, Claim, Booking, WorkHours }, [
|
|
741
|
-
contained(on(Attendance, "event"), on(Event, "id")),
|
|
742
|
-
contained(on(Attendance, "person"), on(Person, "id")),
|
|
743
|
-
contained(on(Attendance, "rsvp"), on(Rsvp, "id")),
|
|
744
|
-
key(Attendance, ["event", "person"]), // one RSVP per (event, person)
|
|
745
|
-
key(Claim, ["source"]),
|
|
746
|
-
contained(on(Claim, "person"), on(Person, "id")),
|
|
747
|
-
contained(on(Claim, "arm"), on(Arm, "id")),
|
|
748
|
-
// HARD: rooms cannot double-book — the pointwise key (recipe 1's theorem).
|
|
749
|
-
key(Booking, ["room", "span"]),
|
|
750
|
-
// SOFT: people CAN double-book — key(Claim, ["person", "span"]) is simply
|
|
751
|
-
// not declared. Policy is the presence or absence of one statement.
|
|
752
|
-
// Accepting an invitation IS claiming the time (totality + validity) —
|
|
753
|
-
// and this is the statement that types Claim.source:
|
|
754
|
-
mirrors(on(Attendance.where({ rsvp: "Accepted" }), "id"), on(Claim.where({ arm: "Busy" }), "source")),
|
|
755
|
-
// Busy time lies inside working hours, pointwise — coverage rides the
|
|
756
|
-
// target's own key (disjoint + ordered is a theorem, not a request):
|
|
757
|
-
key(WorkHours, ["person", "hours"]),
|
|
758
|
-
contained(on(Claim.where({ arm: "Busy" }), ["person", "span"]), on(WorkHours, ["person", "hours"])),
|
|
759
|
-
contained(on(Booking, "room"), on(Room, "id")),
|
|
760
|
-
contained(on(Booking, "event"), on(Event, "id"))
|
|
761
|
-
])
|
|
762
|
-
|
|
763
|
-
const roomConflicts = query(Calendar).rule((r) => {
|
|
764
|
-
const { room, span } = v(Booking)
|
|
765
|
-
return r
|
|
766
|
-
.match(Booking, { room, span })
|
|
767
|
-
.where(r.allen(span, ALLEN.intersects, r.param("want")))
|
|
768
|
-
.find({ room, span })
|
|
769
|
-
})
|
|
770
|
-
const personLoad = query(Calendar).rule((r) => {
|
|
771
|
-
const { person, span } = v(Claim)
|
|
772
|
-
return r
|
|
773
|
-
.match(Claim, { person, span })
|
|
774
|
-
.where(r.allen(span, ALLEN.intersects, r.param("window")))
|
|
775
|
-
.find({ person, span })
|
|
776
|
-
})
|
|
203
|
+
const readBooks = (reader: QueryReader<typeof Library>, name: string) =>
|
|
204
|
+
Effect.scoped(
|
|
205
|
+
Effect.gen(function* () {
|
|
206
|
+
const result = yield* reader.execute(booksBy, { name }, work)
|
|
207
|
+
return yield* result.collect({ maxBytes: work.resultBytes }, work)
|
|
208
|
+
})
|
|
209
|
+
)
|
|
210
|
+
void readBooks
|
|
777
211
|
```
|
|
778
212
|
|
|
779
|
-
##
|
|
780
|
-
|
|
781
|
-
Guarantee: Lean theorem/countermodel + validator/runtime premise — pointwise
|
|
782
|
-
keys plus one-way support inclusion form a disjoint cover
|
|
783
|
-
(`lean/Bumbledb/Dependencies.lean: pointwise_key_disjoint`,
|
|
784
|
-
`coverage_is_support_inclusion`); target overhang is legal
|
|
785
|
-
(`lean/Bumbledb/Countermodels.lean: one_way_overhang`).
|
|
213
|
+
## 6. Grouped exact aggregates
|
|
786
214
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
215
|
+
Aggregates fold the group's distinct full bindings: keep the identity-bearing
|
|
216
|
+
`id` in the binding set so equal scores in different rows both contribute.
|
|
217
|
+
Exact float `sum`/`mean` are deterministic with one final rounding.
|
|
790
218
|
|
|
791
219
|
```ts
|
|
792
|
-
const
|
|
793
|
-
const
|
|
220
|
+
const Sample = relation("Sample", { id: uuid, series: uuid, value: f64 })
|
|
221
|
+
const Series = schema("Series", { Sample }, [key(Sample, ["id"])])
|
|
794
222
|
|
|
795
|
-
const
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
])
|
|
804
|
-
|
|
805
|
-
// in force on date t — one membership probe:
|
|
806
|
-
const inForce = query(Pricing).rule((r) => {
|
|
807
|
-
const { rate_bps, valid } = v(Version)
|
|
808
|
-
return r
|
|
809
|
-
.match(Version, { policy: r.param("p"), rate_bps, valid })
|
|
810
|
-
.where(r.pointIn(r.param("t"), valid))
|
|
811
|
-
.find({ rate_bps })
|
|
812
|
-
})
|
|
813
|
-
// clean successions (half-open makes MEETS exact, no ±1 fudge):
|
|
814
|
-
const successions = query(Pricing).rule((r) => {
|
|
815
|
-
const { policy: p, valid: a } = v(Version)
|
|
816
|
-
const { valid: b } = v(Version)
|
|
817
|
-
return r
|
|
818
|
-
.match(Version, { policy: p, valid: a })
|
|
819
|
-
.match(Version, { policy: p, valid: b })
|
|
820
|
-
.where(r.allen(a, ALLEN.meets, b))
|
|
821
|
-
.find({ a, b })
|
|
223
|
+
const stats = query(Series).rule((r) => {
|
|
224
|
+
const { id, series, value } = v(Sample)
|
|
225
|
+
return r.match(Sample, { id, series, value }).find({
|
|
226
|
+
series,
|
|
227
|
+
total: r.sum(value),
|
|
228
|
+
mean: r.mean(value),
|
|
229
|
+
points: r.count()
|
|
230
|
+
})
|
|
822
231
|
})
|
|
232
|
+
void stats
|
|
823
233
|
```
|
|
824
234
|
|
|
825
|
-
##
|
|
826
|
-
|
|
827
|
-
Guarantee: Lean theorem/countermodel + validator/runtime premise —
|
|
828
|
-
`lean/Bumbledb/Dependencies.lean: coverage_is_support_inclusion` proves source
|
|
829
|
-
coverage, not exact partition (`lean/Bumbledb/Countermodels.lean:
|
|
830
|
-
one_way_overhang`).
|
|
235
|
+
## 7. Nonrecursive composition — a template is a relation expression
|
|
831
236
|
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
237
|
+
A typed query template of the same schema splices as a derived stage:
|
|
238
|
+
`v(imported)` mints variables for its head columns and `match(imported, …)`
|
|
239
|
+
joins them. Naming materializes nothing.
|
|
835
240
|
|
|
836
241
|
```ts
|
|
837
|
-
const
|
|
838
|
-
const
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
key(PayPeriod, ["year", "span"]), // disjoint: no shared instant
|
|
844
|
-
// Covering: no holes in the fiscal year's span; overhang is legal.
|
|
845
|
-
contained(on(FiscalYear, ["id", "span"]), on(PayPeriod, ["year", "span"]))
|
|
242
|
+
const Reading = relation("Reading", { id: uuid, sensor: uuid, value: f64 })
|
|
243
|
+
const Sensor = relation("Sensor", { id: uuid, label: str })
|
|
244
|
+
const Telemetry = schema("Telemetry", { Reading, Sensor }, [
|
|
245
|
+
key(Reading, ["id"]),
|
|
246
|
+
key(Sensor, ["id"]),
|
|
247
|
+
contained(on(Reading, "sensor"), on(Sensor, "id"))
|
|
846
248
|
])
|
|
847
249
|
|
|
848
|
-
|
|
849
|
-
const
|
|
850
|
-
|
|
851
|
-
return r
|
|
852
|
-
.match(PayPeriod, { year: r.param("y"), seq, span })
|
|
853
|
-
.where(r.pointIn(r.param("t"), span))
|
|
854
|
-
.find({ seq })
|
|
250
|
+
const perSensor = query(Telemetry).rule((r) => {
|
|
251
|
+
const { id, sensor, value } = v(Reading)
|
|
252
|
+
return r.match(Reading, { id, sensor, value }).find({ sensor, mean: r.mean(value) })
|
|
855
253
|
})
|
|
856
|
-
```
|
|
857
|
-
|
|
858
|
-
## 17. Federal income tax
|
|
859
|
-
|
|
860
|
-
Guarantee: validator/runtime premises + host discipline — keys prove bracket
|
|
861
|
-
disjointness and statements prove residency coverage; full bracket coverage and proration are host duties.
|
|
862
|
-
|
|
863
|
-
Brackets are intervals over money; the top bracket is a ray; regimes key on
|
|
864
|
-
(year, status); and proration happens at write time, never at query time.
|
|
865
|
-
|
|
866
|
-
```ts
|
|
867
|
-
const Status = closed("Status", ["Single", "MarriedJoint", "HeadOfHousehold"])
|
|
868
|
-
const Regime = relation("Regime", { id: u64.fresh, year: i64, status: Status.id })
|
|
869
|
-
const Bracket = relation("Bracket", { regime: u64, income: interval(i64), rate_bps: i64 })
|
|
870
|
-
const Residency = relation("Residency", { person: u64, span: interval(i64) })
|
|
871
|
-
// Split at write: an Earned fact never spans a year boundary — writers
|
|
872
|
-
// split (prorate) at the boundary, so no reader ever clips. The
|
|
873
|
-
// representation move that deletes clip-at-query (gravestone, recipe 23).
|
|
874
|
-
const Earned = relation("Earned", { person: u64, regime: u64, span: interval(i64), minor: i64 })
|
|
875
254
|
|
|
876
|
-
const
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
contained(on(Bracket, "regime"), on(Regime, "id")),
|
|
880
|
-
// Brackets are disjoint per regime. Seed data conventionally covers
|
|
881
|
-
// [0, ∞) and the top bracket is a ray, but this key proves disjointness
|
|
882
|
-
// only. end == MAX denotes [s, ∞), an honest value of the representation,
|
|
883
|
-
// not a sentinel (the point-domain law).
|
|
884
|
-
key(Bracket, ["regime", "income"]),
|
|
885
|
-
contained(on(Earned, "regime"), on(Regime, "id")),
|
|
886
|
-
key(Residency, ["person", "span"]),
|
|
887
|
-
// Residency exclusion: income counts only where earned inside a residency
|
|
888
|
-
// period — pointwise coverage, the same judgment as recipe 15's. This
|
|
889
|
-
// pair statement is also what puts the two bare `person` columns in one
|
|
890
|
-
// (generator-less) class: "Residency.person", by least coordinate.
|
|
891
|
-
contained(on(Earned, ["person", "span"]), on(Residency, ["person", "span"]))
|
|
892
|
-
])
|
|
893
|
-
|
|
894
|
-
// the marginal bracket (membership probes the disjoint bracket set). Tax
|
|
895
|
-
// owed is host arithmetic over the bracket walk — arithmetic beyond the
|
|
896
|
-
// measure is refused (the ledger).
|
|
897
|
-
const marginal = query(Tax).rule((r) => {
|
|
898
|
-
const { id: reg } = v(Regime)
|
|
899
|
-
const { income: b, rate_bps } = v(Bracket)
|
|
255
|
+
const labeled = query(Telemetry).rule((r) => {
|
|
256
|
+
const { sensor, mean } = v(perSensor)
|
|
257
|
+
const { label } = v(Sensor)
|
|
900
258
|
return r
|
|
901
|
-
.match(
|
|
902
|
-
.match(
|
|
903
|
-
.
|
|
904
|
-
.find({ rate_bps })
|
|
905
|
-
})
|
|
906
|
-
```
|
|
907
|
-
|
|
908
|
-
## 18. Free time and coalescing
|
|
909
|
-
|
|
910
|
-
Guarantee: Lean theorem + runtime query semantics — `pack` coalesces answer
|
|
911
|
-
intervals (`lean/Bumbledb/Query/Aggregates.lean: pack_canonical`,
|
|
912
|
-
`pack_extensional`); it asserts no stored disjointness, completeness, or
|
|
913
|
-
maintenance behavior.
|
|
914
|
-
|
|
915
|
-
`pack` is Snodgrass's coalesce as an aggregate — maximal disjoint segments per
|
|
916
|
-
group, one answer per (group, segment). Coalescing is never a write rule: the
|
|
917
|
-
engine stores the claims it was given.
|
|
918
|
-
|
|
919
|
-
```ts
|
|
920
|
-
const Person = relation("Person", { id: u64.fresh, name: str })
|
|
921
|
-
const Claim = relation("Claim", { person: u64, span: interval(i64) })
|
|
922
|
-
|
|
923
|
-
const FreeTime = schema("FreeTime", { Person, Claim }, [
|
|
924
|
-
contained(on(Claim, "person"), on(Person, "id"))
|
|
925
|
-
// No pointwise key, on purpose: claims overlap freely and pack coalesces
|
|
926
|
-
// at read time. Wanting them stored-disjoint is recipe 1's key instead.
|
|
927
|
-
])
|
|
928
|
-
|
|
929
|
-
// busy time, coalesced (adjacent segments merge — the half-open law):
|
|
930
|
-
const busy = query(FreeTime).rule((r) => {
|
|
931
|
-
const { person, span } = v(Claim)
|
|
932
|
-
return r.match(Claim, { person, span }).find({ person, packed: r.pack(span) })
|
|
259
|
+
.match(perSensor, { sensor, mean })
|
|
260
|
+
.match(Sensor, { id: sensor, label })
|
|
261
|
+
.find({ label, mean })
|
|
933
262
|
})
|
|
934
|
-
|
|
935
|
-
// answer row already carries (`end − start`). Overlaps double-count,
|
|
936
|
-
// often the wrong question.
|
|
937
|
-
// Coalesced totals = the two-query composition (pack, then a host fold) —
|
|
938
|
-
// aggregates never nest; free time (gaps) is the two-line host walk over
|
|
939
|
-
// sorted packed answers (host sorts by person then span start/end) — both
|
|
940
|
-
// refusals recorded in the ledger.
|
|
263
|
+
void labeled
|
|
941
264
|
```
|
|
942
265
|
|
|
943
|
-
##
|
|
944
|
-
|
|
945
|
-
## 19. The ledger
|
|
946
|
-
|
|
947
|
-
Guarantee: Lean theorem + runtime invariant for bounded sums
|
|
948
|
-
(`lean/Bumbledb/Query/Aggregates.lean: checkedSum_sound`); host discipline
|
|
949
|
-
for double entry — statements resolve posting references, not arithmetic agreement.
|
|
266
|
+
## 8. Bounded results: capped collect, one-shot page stream
|
|
950
267
|
|
|
951
|
-
|
|
268
|
+
`collect({ maxBytes }, work)` is database-enforced total materialization and
|
|
269
|
+
leaves the result available. `pages({ pageBytes }, work)` is a ONE-SHOT consuming
|
|
270
|
+
stream over the completed result: the first run moves the backing into a
|
|
271
|
+
private scoped cursor; a second run refuses. Every element is one owned page
|
|
272
|
+
array — pages, not rows — delivered after complete evaluation.
|
|
952
273
|
|
|
953
274
|
```ts
|
|
954
|
-
const
|
|
955
|
-
const
|
|
956
|
-
const Posting = relation("Posting", {
|
|
957
|
-
id: u64.fresh,
|
|
958
|
-
entry: u64,
|
|
959
|
-
account: u64,
|
|
960
|
-
minor: i64
|
|
961
|
-
})
|
|
962
|
-
|
|
963
|
-
const Ledger = schema("Ledger", { Account, JournalEntry, Posting }, [
|
|
964
|
-
contained(on(Posting, "entry"), on(JournalEntry, "id")),
|
|
965
|
-
contained(on(Posting, "account"), on(Account, "id"))
|
|
966
|
-
// A stored balance column equaling Sum(postings) is the arithmetic-
|
|
967
|
-
// agreement statement — refused (the ledger): statements prove presence
|
|
968
|
-
// and topology, never that a value equals a computation. Balance is host
|
|
969
|
-
// arithmetic over sum; a materialized rollup is recipe 21's shape.
|
|
970
|
-
])
|
|
275
|
+
const Event = relation("Event", { id: uuid, at: i64 })
|
|
276
|
+
const Feed = schema("Feed", { Event }, [key(Event, ["id"])])
|
|
971
277
|
|
|
972
|
-
|
|
973
|
-
const
|
|
974
|
-
|
|
975
|
-
return r.match(Posting, { id, account, minor }).find({ account, balance: r.sum(minor) })
|
|
278
|
+
const everything = query(Feed).rule((r) => {
|
|
279
|
+
const { id, at } = v(Event)
|
|
280
|
+
return r.match(Event, { id, at }).find({ id, at })
|
|
976
281
|
})
|
|
977
|
-
// double-entry audit (host asserts every total is 0 — discipline, not schema):
|
|
978
|
-
const doubleEntry = query(Ledger).rule((r) => {
|
|
979
|
-
const { id, entry, minor } = v(Posting)
|
|
980
|
-
return r.match(Posting, { id, entry, minor }).find({ entry, balance: r.sum(minor) })
|
|
981
|
-
})
|
|
982
|
-
```
|
|
983
|
-
|
|
984
|
-
## 20. Conditional writes
|
|
985
|
-
|
|
986
|
-
Guarantee: Lean theorem + generation-witness/runtime premise + host retry
|
|
987
|
-
discipline — snapshot-derived writes detect movement
|
|
988
|
-
(`lean/Bumbledb/Txn.lean: writeFrom_moved`, `witness_conflict_distinct`);
|
|
989
|
-
final-state point reads need no earlier witness.
|
|
990
|
-
|
|
991
|
-
The generation witness: read the model, propose a delta, commit iff the model
|
|
992
|
-
you read is still the model. The SDK ships one-shot `writeFrom` (the witness
|
|
993
|
-
escapes the read callback; the instance does not). Retry on
|
|
994
|
-
`{ tag: "moved" }` is host policy — a short loop around `db.read` +
|
|
995
|
-
`writeFrom` if the host wants it. `abandon(payload)`
|
|
996
|
-
declines to commit without issuing anything — from `db.write` and
|
|
997
|
-
`db.writeFrom` alike (the sentinel's contract is unconditional, and
|
|
998
|
-
the outcome arm is in the result type exactly when the callback can
|
|
999
|
-
abandon).
|
|
1000
282
|
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
])
|
|
1013
|
-
|
|
1014
|
-
// update-where's premise — "still Queued" is the witness:
|
|
1015
|
-
const stillQueued = query(Jobs).rule((r) => {
|
|
1016
|
-
const { id, payload } = v(Job)
|
|
1017
|
-
return r.match(Job, { id, state: "Queued", payload }).find({ id, payload })
|
|
1018
|
-
})
|
|
1019
|
-
|
|
1020
|
-
const created = await Db.create("./jobs.db", Jobs)
|
|
1021
|
-
if (created.tag !== "accepted") {
|
|
1022
|
-
throw new Error("create rejected")
|
|
1023
|
-
}
|
|
1024
|
-
const db = created.value
|
|
1025
|
-
const prepared = db.prepare(stillQueued)
|
|
1026
|
-
|
|
1027
|
-
// The witnessed write: premise reads via `instance`, the delta via `tx`.
|
|
1028
|
-
// On a moved generation `writeFrom` returns `{ tag: "moved" }` — retry is
|
|
1029
|
-
// host policy. The other two idioms: insert-select is the same shape (query
|
|
1030
|
-
// source answers, insert the derived facts); key-shaped read-modify-write
|
|
1031
|
-
// uses `tx.get`/`tx.contains` — final-state point reads need no earlier
|
|
1032
|
-
// witness.
|
|
1033
|
-
const outcome = db.read(function attempt(instance, witness) {
|
|
1034
|
-
return db.writeFrom(witness, function updateWhere(tx) {
|
|
1035
|
-
const queued = instance.execute(prepared, {})
|
|
1036
|
-
if (queued.length === 0) {
|
|
1037
|
-
return abandon("nothing queued")
|
|
1038
|
-
}
|
|
1039
|
-
for (const row of queued) {
|
|
1040
|
-
tx.delete(Job, [{ id: row.id, state: "Queued", payload: row.payload }])
|
|
1041
|
-
tx.insert(Job, [{ id: row.id, state: "Running", payload: row.payload }])
|
|
1042
|
-
tx.insert(Lease, [{ job: row.id, worker: 7n, until: 60n }])
|
|
1043
|
-
}
|
|
1044
|
-
return undefined
|
|
1045
|
-
})
|
|
1046
|
-
})
|
|
283
|
+
const drain = (result: CompleteResult<{ readonly id: Uuid; readonly at: bigint }>) =>
|
|
284
|
+
result.pages({ pageBytes: 65536n }, work).pipe(
|
|
285
|
+
Stream.runForEach((page) =>
|
|
286
|
+
Effect.sync(() => {
|
|
287
|
+
// One owned page array; caller mutation cannot reach native
|
|
288
|
+
// state or another delivered page.
|
|
289
|
+
return page.length
|
|
290
|
+
})
|
|
291
|
+
)
|
|
292
|
+
)
|
|
293
|
+
void [everything, drain]
|
|
1047
294
|
```
|
|
1048
295
|
|
|
1049
|
-
##
|
|
1050
|
-
|
|
1051
|
-
Guarantee: Lean theorem + validator/runtime premises for soundness
|
|
1052
|
-
(`lean/Bumbledb/Txn.lean: derived_soundness_vs_freshness`); host
|
|
1053
|
-
discipline for completeness — containment rejects unsupported facts but never refreshes omissions.
|
|
296
|
+
## 9. Application identity: generate once, retain, never regenerate
|
|
1054
297
|
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
298
|
+
`Effect.sync(() => crypto.randomUUID())` is effectful cryptographic entropy — run it once for an
|
|
299
|
+
original intent, persist the value with the request, and never regenerate
|
|
300
|
+
inside a retry. `Uuid.parse` is the pure fixed-size parser returning
|
|
301
|
+
`Result`.
|
|
1058
302
|
|
|
1059
303
|
```ts
|
|
1060
|
-
const
|
|
1061
|
-
const
|
|
1062
|
-
const BusySpan = relation("BusySpan", { person: u64, span: interval(i64) })
|
|
304
|
+
const parsed = Uuid.parse("00112233-4455-6677-8899-aabbccddeeff")
|
|
305
|
+
const okOrRefused: boolean = Result.isSuccess(parsed)
|
|
1063
306
|
|
|
1064
|
-
const
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
// Soundness, pointwise: every stored rollup point is covered by busy
|
|
1070
|
-
// claims — an UNSOUND rollup (claiming busy time that isn't, or surviving
|
|
1071
|
-
// its sources' deletion) cannot commit, judged on every touching commit.
|
|
1072
|
-
contained(on(BusySpan, ["person", "span"]), on(Claim.where({ arm: "Busy" }), ["person", "span"]))
|
|
1073
|
-
])
|
|
1074
|
-
|
|
1075
|
-
// Maintenance is the third witness idiom (recipe 20): re-run the deriving
|
|
1076
|
-
// query on a snapshot, diff, commit witnessed — the rollup cannot commit
|
|
1077
|
-
// against sources it didn't actually read. The deriving query (pack IS the
|
|
1078
|
-
// coalesce):
|
|
1079
|
-
const deriving = query(Rollup).rule((r) => {
|
|
1080
|
-
const { person, span } = v(Claim)
|
|
1081
|
-
return r.match(Claim, { person, span, arm: "Busy" }).find({ person, packed: r.pack(span) })
|
|
307
|
+
const mintOnce = Effect.gen(function* () {
|
|
308
|
+
const id = yield* Effect.sync(() => crypto.randomUUID())
|
|
309
|
+
// Persist `id` with the original request BEFORE any database dispatch;
|
|
310
|
+
// a timeout retries the identical intent, never a new identity.
|
|
311
|
+
return id
|
|
1082
312
|
})
|
|
313
|
+
void [okOrRefused, mintOnce]
|
|
1083
314
|
```
|
|
1084
315
|
|
|
1085
|
-
##
|
|
1086
|
-
|
|
1087
|
-
Guarantee: Lean theorem + represented planner/runtime premise — rule union is
|
|
1088
|
-
set-idempotent (`lean/Bumbledb/Query/Denotation.lean: union_idempotent`);
|
|
1089
|
-
key-backed DU arms satisfy the disjoint-arms licence
|
|
1090
|
-
(`lean/Bumbledb/Exec/Dedup.lean: disjoint_witness_licence`); execution
|
|
1091
|
-
always keeps one spanning seen-set.
|
|
316
|
+
## 10. Witnessed correction: exact expected state
|
|
1092
317
|
|
|
1093
|
-
|
|
1094
|
-
|
|
318
|
+
Read under a short scope, keep the copied witness, and apply with
|
|
319
|
+
`expected: { kind: "exact", at: witness }`. An intervening net change moves
|
|
320
|
+
the apply instead of silently overwriting.
|
|
1095
321
|
|
|
1096
322
|
```ts
|
|
1097
|
-
const
|
|
1098
|
-
const
|
|
1099
|
-
const Card = relation("Card", { payment: u64, last4: u64 })
|
|
1100
|
-
const Ach = relation("Ach", { payment: u64, routing: u64 })
|
|
323
|
+
const Account = relation("Account", { id: uuid, balance: i64 })
|
|
324
|
+
const Bank = schema("Bank", { Account }, [key(Account, ["id"])])
|
|
1101
325
|
|
|
1102
|
-
const
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
.
|
|
1121
|
-
.find({ id, n })
|
|
1122
|
-
})
|
|
1123
|
-
.rule((r) => {
|
|
1124
|
-
const { id } = v(Payment)
|
|
1125
|
-
const { routing: n } = v(Ach)
|
|
1126
|
-
return r
|
|
1127
|
-
.match(Payment, { id, kind: "Ach" })
|
|
1128
|
-
.match(Ach, { payment: id, routing: n })
|
|
1129
|
-
.find({ id, n })
|
|
1130
|
-
})
|
|
1131
|
-
```
|
|
1132
|
-
|
|
1133
|
-
## 23. The anti-recipes: five gravestones
|
|
1134
|
-
|
|
1135
|
-
Guarantee: intentionally refused — each gravestone names unsupported vocabulary
|
|
1136
|
-
and its representable replacement; none asserts an engine theorem.
|
|
1137
|
-
|
|
1138
|
-
What not to model. Each gravestone cites its replacement; the block's
|
|
1139
|
-
relations are the replacements, compiled.
|
|
1140
|
-
|
|
1141
|
-
```ts
|
|
1142
|
-
// GRAVESTONE: successor pointers (a `next` column). A linked list inside a
|
|
1143
|
-
// relation is control flow smuggled into data. REPLACEMENT: the ordering
|
|
1144
|
-
// triple (recipe 9).
|
|
1145
|
-
const Step = relation("Step", { flow: u64, pos: u64, action: str })
|
|
1146
|
-
// GRAVESTONE: floats for scores, rates, money. Permanently refused (the
|
|
1147
|
-
// ledger). REPLACEMENT: fixed-point i64 — basis points (recipe 4).
|
|
1148
|
-
const Score = relation("Score", { subject: u64, bps: i64 })
|
|
1149
|
-
// GRAVESTONE: conditional keys ("at most one active run per student") —
|
|
1150
|
-
// rejected as FDs. REPLACEMENT: the relation split, whose ordinary key IS
|
|
1151
|
-
// the invariant (recipe 13's arm shape).
|
|
1152
|
-
const ActiveRun = relation("ActiveRun", { student: u64, run: u64 })
|
|
1153
|
-
// GRAVESTONE: clip-at-query intervals (facts spanning period boundaries,
|
|
1154
|
-
// every reader clipping). REPLACEMENT: split at write (recipe 17).
|
|
1155
|
-
const Usage = relation("Usage", { meter: u64, period: u64, used: interval(i64) })
|
|
1156
|
-
// GRAVESTONE: uuid keys. uuidv7 is identity + clash-avoidance + clock in
|
|
1157
|
-
// one lie. REPLACEMENT: fresh (minted identity) + an explicit i64 time column.
|
|
1158
|
-
const Event = relation("Event", { id: u64.fresh, at: i64 })
|
|
1159
|
-
|
|
1160
|
-
const Gravestones = schema("Gravestones", { Step, Score, ActiveRun, Usage, Event }, [
|
|
1161
|
-
key(Step, ["flow", "pos"]),
|
|
1162
|
-
key(Score, ["subject"]),
|
|
1163
|
-
key(ActiveRun, ["student"]),
|
|
1164
|
-
key(Usage, ["meter", "used"])
|
|
1165
|
-
])
|
|
1166
|
-
```
|
|
1167
|
-
|
|
1168
|
-
## Host-driven closure
|
|
1169
|
-
|
|
1170
|
-
## 24. The closure idiom
|
|
1171
|
-
|
|
1172
|
-
Guarantee: host discipline for the loop — the finite `seen` set proves
|
|
1173
|
-
termination for the host run; the engine-native form beside it executes
|
|
1174
|
-
whole under the linear reach driver, budget-bounded
|
|
1175
|
-
(`lean/Bumbledb/Exec/Reach.lean: evalLinearReach_eq_lfp` /
|
|
1176
|
-
`evalQuery_sound`).
|
|
1177
|
-
|
|
1178
|
-
Reachability, in two dialects. The host-loop idiom remains the depth-bounded
|
|
1179
|
-
answer: the loop runs depth-many rounds and each round is one ∈-set query —
|
|
1180
|
-
an `inSet` probe, microsecond-class. The frontier discipline below *is*
|
|
1181
|
-
semi-naive evaluation's Δ, spent where a loop is a loop: the host. The
|
|
1182
|
-
engine-native form is the same closure as one query with `.reach()`.
|
|
1183
|
-
|
|
1184
|
-
```ts
|
|
1185
|
-
const Node = relation("Node", { id: u64.fresh, name: str })
|
|
1186
|
-
// One parent per child — a forest (recipe 10's edge shape); a root is a
|
|
1187
|
-
// node whose Parent fact is absent (recipe 3's honest 0..1).
|
|
1188
|
-
const Parent = relation("Parent", { child: u64, parent: u64 })
|
|
1189
|
-
|
|
1190
|
-
const Closure = schema("Closure", { Node, Parent }, [
|
|
1191
|
-
key(Parent, ["child"]),
|
|
1192
|
-
contained(on(Parent, "child"), on(Node, "id")),
|
|
1193
|
-
contained(on(Parent, "parent"), on(Node, "id"))
|
|
1194
|
-
])
|
|
1195
|
-
|
|
1196
|
-
// The loop's one query — the frontier's children, one ∈-set probe:
|
|
1197
|
-
const step = query(Closure).rule((r) => {
|
|
1198
|
-
const { child: c } = v(Parent)
|
|
1199
|
-
return r.match(Parent, { child: c, parent: r.inSet("frontier") }).find({ c })
|
|
1200
|
-
})
|
|
1201
|
-
```
|
|
1202
|
-
|
|
1203
|
-
The loop (the compiled, driven copy is in `test/cookbook.test.ts`, over a
|
|
1204
|
-
three-level forest with the exact reachable set asserted):
|
|
1205
|
-
|
|
1206
|
-
```ts
|
|
1207
|
-
const created = await Db.create("./closure.db", Closure)
|
|
1208
|
-
if (created.tag !== "accepted") {
|
|
1209
|
-
throw new Error("create rejected")
|
|
1210
|
-
}
|
|
1211
|
-
const db = created.value
|
|
1212
|
-
const stepPrepared = db.prepare(step)
|
|
1213
|
-
const root = 1n // the host's chosen root node id
|
|
1214
|
-
|
|
1215
|
-
const seen = new Set<bigint>([root])
|
|
1216
|
-
let frontier: readonly bigint[] = [root]
|
|
1217
|
-
for (;;) {
|
|
1218
|
-
const next = db.read((i) => i.execute(stepPrepared, { frontier })) // one set-param query
|
|
1219
|
-
const fresh = next
|
|
1220
|
-
.map((row) => row.c)
|
|
1221
|
-
.filter((c) => {
|
|
1222
|
-
return !seen.has(c)
|
|
326
|
+
const correct = (accountId: Uuid) =>
|
|
327
|
+
Effect.scoped(
|
|
328
|
+
Effect.gen(function* () {
|
|
329
|
+
const db = yield* Db.open(localPath, Bank, work)
|
|
330
|
+
const observed = yield* Effect.scoped(
|
|
331
|
+
Effect.gen(function* () {
|
|
332
|
+
const snapshot = yield* db.snapshot(work)
|
|
333
|
+
const previous = yield* snapshot.get(Account, { id: accountId }, work)
|
|
334
|
+
if (Option.isNone(previous)) {
|
|
335
|
+
return yield* Effect.fail({ missing: accountId })
|
|
336
|
+
}
|
|
337
|
+
return { previous: previous.value, at: snapshot.witness }
|
|
338
|
+
})
|
|
339
|
+
)
|
|
340
|
+
const draft = yield* ChangeSet.builder(Bank, work)
|
|
341
|
+
yield* draft.delete(Account, [observed.previous])
|
|
342
|
+
yield* draft.insert(Account, [{ ...observed.previous, balance: observed.previous.balance + 1n }])
|
|
343
|
+
const changes = yield* draft.finish()
|
|
344
|
+
return yield* db.apply(changes, { ...work, expected: { kind: "exact", at: observed.at } })
|
|
1223
345
|
})
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
}
|
|
1227
|
-
for (const c of fresh) {
|
|
1228
|
-
seen.add(c)
|
|
1229
|
-
}
|
|
1230
|
-
frontier = fresh
|
|
1231
|
-
}
|
|
1232
|
-
```
|
|
1233
|
-
|
|
1234
|
-
Termination is the host's theorem: `seen` grows strictly or the loop breaks,
|
|
1235
|
-
inside a finite node set. When the idiom's costs bite — **unbounded or large
|
|
1236
|
-
depth**, or **closure composed into a larger plan** — write the engine-native
|
|
1237
|
-
form instead: `?root` seeds the rec, and the main rule is the finished
|
|
1238
|
-
set's own identity projection (an interior atom is a positive occurrence, so it
|
|
1239
|
-
grounds its variables — no re-grounding join over a domain relation exists):
|
|
1240
|
-
|
|
1241
|
-
```ts
|
|
1242
|
-
const reach = query(Closure)
|
|
1243
|
-
.reach("reach", {
|
|
1244
|
-
base: [
|
|
1245
|
-
(r) => {
|
|
1246
|
-
const { id: c } = v(Node)
|
|
1247
|
-
return r
|
|
1248
|
-
.match(Node, { id: c })
|
|
1249
|
-
.where(r.eq(c, r.param("root")))
|
|
1250
|
-
.find({ c })
|
|
1251
|
-
}
|
|
1252
|
-
],
|
|
1253
|
-
rec: [
|
|
1254
|
-
(r) => {
|
|
1255
|
-
const { child: c, parent } = v(Parent)
|
|
1256
|
-
return r.match(Parent, { child: c, parent }).interior("reach", { c: parent }).find({ c })
|
|
1257
|
-
}
|
|
1258
|
-
]
|
|
1259
|
-
})
|
|
1260
|
-
.rule((r) => {
|
|
1261
|
-
const { id: c } = v(Node)
|
|
1262
|
-
return r.interior("reach", { c }).find({ c })
|
|
1263
|
-
})
|
|
1264
|
-
const reachPrepared = db.prepare(reach)
|
|
1265
|
-
```
|
|
1266
|
-
|
|
1267
|
-
The complement is one `r.not` away — negation **of** a finished rec is
|
|
1268
|
-
engine-legal (negation in rec is refused), so
|
|
1269
|
-
"every node the closure never reached" runs in-plan through the engine's
|
|
1270
|
-
anti-probe, never as a host-side set difference — the same query with the
|
|
1271
|
-
main rule:
|
|
1272
|
-
|
|
1273
|
-
```ts
|
|
1274
|
-
const unreached = query(Closure)
|
|
1275
|
-
.reach("reach", {
|
|
1276
|
-
base: [
|
|
1277
|
-
(r) => {
|
|
1278
|
-
const { id: c } = v(Node)
|
|
1279
|
-
return r
|
|
1280
|
-
.match(Node, { id: c })
|
|
1281
|
-
.where(r.eq(c, r.param("root")))
|
|
1282
|
-
.find({ c })
|
|
1283
|
-
}
|
|
1284
|
-
],
|
|
1285
|
-
rec: [
|
|
1286
|
-
(r) => {
|
|
1287
|
-
const { child: c, parent } = v(Parent)
|
|
1288
|
-
return r.match(Parent, { child: c, parent }).interior("reach", { c: parent }).find({ c })
|
|
1289
|
-
}
|
|
1290
|
-
]
|
|
1291
|
-
})
|
|
1292
|
-
.rule((r) => {
|
|
1293
|
-
const { id: c } = v(Node)
|
|
1294
|
-
return r.match(Node, { id: c }).where(r.not("reach", { c })).find({ c })
|
|
1295
|
-
})
|
|
1296
|
-
```
|
|
1297
|
-
|
|
1298
|
-
(the test drives both dialects and asserts the same reachable sets, root for
|
|
1299
|
-
root). What stays host-side is the **chain-window class** — interval
|
|
1300
|
-
intersection along paths — which the recursion surface fences out: the idiom
|
|
1301
|
-
carries the window in the host's frontier, one intersection per hop, and that
|
|
1302
|
-
composition has no engine form.
|
|
1303
|
-
|
|
1304
|
-
## 25. The chart of accounts
|
|
1305
|
-
|
|
1306
|
-
Guarantee: host discipline + runtime aggregate semantics — the host computes
|
|
1307
|
-
closure, then one checked `sum` (`lean/Bumbledb/Query/Aggregates.lean:
|
|
1308
|
-
checkedSum_sound`); the engine-native form folds over a finished rec,
|
|
1309
|
-
the one aggregation shape the rec roster admits.
|
|
1310
|
-
|
|
1311
|
-
The ledger workload's real recursion case, in the same two dialects: a
|
|
1312
|
-
hierarchical chart of accounts and a subtree rollup. The host composition —
|
|
1313
|
-
recipe 24's loop accumulates the subtree's ∈-set, then **one `sum` query over
|
|
1314
|
-
the accumulated set** folds the postings. The engine aggregates, the host
|
|
1315
|
-
composes (aggregates never nest). The engine-native form is one query:
|
|
1316
|
-
aggregation *through* a cycle is refused, but a fold over a finished rec
|
|
1317
|
-
from main reads a finished set and is ordinary.
|
|
1318
|
-
|
|
1319
|
-
```ts
|
|
1320
|
-
const Account = relation("Account", { id: u64.fresh, name: str })
|
|
1321
|
-
const AccountParent = relation("AccountParent", { child: u64, parent: u64 })
|
|
1322
|
-
const Posting = relation("Posting", { id: u64.fresh, account: u64, minor: i64 })
|
|
1323
|
-
|
|
1324
|
-
const Accounts = schema("Accounts", { Account, AccountParent, Posting }, [
|
|
1325
|
-
key(AccountParent, ["child"]), // one parent per account
|
|
1326
|
-
contained(on(AccountParent, "child"), on(Account, "id")),
|
|
1327
|
-
contained(on(AccountParent, "parent"), on(Account, "id")),
|
|
1328
|
-
contained(on(Posting, "account"), on(Account, "id"))
|
|
1329
|
-
])
|
|
1330
|
-
|
|
1331
|
-
// The two queries the host rollup composes:
|
|
1332
|
-
// the frontier step (recipe 24's loop, verbatim):
|
|
1333
|
-
const frontierStep = query(Accounts).rule((r) => {
|
|
1334
|
-
const { child: c } = v(AccountParent)
|
|
1335
|
-
return r.match(AccountParent, { child: c, parent: r.inSet("frontier") }).find({ c })
|
|
1336
|
-
})
|
|
1337
|
-
// the rollup over the accumulated subtree (bind the fresh id — recipe
|
|
1338
|
-
// 19's discipline, spent again; equal postings to one account both count):
|
|
1339
|
-
const subtreeRollup = query(Accounts).rule((r) => {
|
|
1340
|
-
const { id, minor } = v(Posting)
|
|
1341
|
-
return r.match(Posting, { id, account: r.inSet("subtree"), minor }).find({ total: r.sum(minor) })
|
|
1342
|
-
})
|
|
1343
|
-
// The engine-native form: the rec converges first, then the
|
|
1344
|
-
// main fold runs once over the finished subtree.
|
|
1345
|
-
const nativeRollup = query(Accounts)
|
|
1346
|
-
.reach("sub", {
|
|
1347
|
-
base: [
|
|
1348
|
-
(r) => {
|
|
1349
|
-
const { id: a } = v(Account)
|
|
1350
|
-
return r
|
|
1351
|
-
.match(Account, { id: a })
|
|
1352
|
-
.where(r.eq(a, r.param("root")))
|
|
1353
|
-
.find({ a })
|
|
1354
|
-
}
|
|
1355
|
-
],
|
|
1356
|
-
rec: [
|
|
1357
|
-
(r) => {
|
|
1358
|
-
const { child: a, parent } = v(AccountParent)
|
|
1359
|
-
return r.match(AccountParent, { child: a, parent }).interior("sub", { a: parent }).find({ a })
|
|
1360
|
-
}
|
|
1361
|
-
]
|
|
1362
|
-
})
|
|
1363
|
-
.rule((r) => {
|
|
1364
|
-
const { id, account: a, minor } = v(Posting)
|
|
1365
|
-
return r
|
|
1366
|
-
.match(Posting, { id, account: a, minor })
|
|
1367
|
-
.interior("sub", { a })
|
|
1368
|
-
.find({ total: r.sum(minor) })
|
|
1369
|
-
})
|
|
1370
|
-
```
|
|
1371
|
-
|
|
1372
|
-
## 26. Exact partition
|
|
1373
|
-
|
|
1374
|
-
Guarantee: Lean theorem + validator/runtime premises — mutual point coverage
|
|
1375
|
-
plus pointwise keys realizes exact partition
|
|
1376
|
-
(`lean/Bumbledb/Dependencies.lean: exact_partition_iff`).
|
|
1377
|
-
|
|
1378
|
-
An exact partition needs both coverage directions. The first containment below
|
|
1379
|
-
is the intent-level reference; the two pointwise keys make each side disjoint;
|
|
1380
|
-
the final pair proves equal point supports per policy — forward coverage
|
|
1381
|
-
forbids gaps and reverse coverage forbids overhang.
|
|
1382
|
-
|
|
1383
|
-
The explicit `key(Policy, ["id", "live"])` is load-bearing. Containment
|
|
1384
|
-
targets resolve by their exact projected field set, so the fresh `{id}` key
|
|
1385
|
-
cannot serve the `{id, live}` target and the engine infers no key closure.
|
|
1386
|
-
|
|
1387
|
-
```ts
|
|
1388
|
-
const Policy = relation("Policy", { id: u64.fresh, live: interval(i64) })
|
|
1389
|
-
const Version = relation("Version", { policy: u64, valid: interval(i64) })
|
|
1390
|
-
|
|
1391
|
-
const ExactPartition = schema("ExactPartition", { Policy, Version }, [
|
|
1392
|
-
contained(on(Version, "policy"), on(Policy, "id")), // reference intent
|
|
1393
|
-
key(Version, ["policy", "valid"]), // disjoint versions
|
|
1394
|
-
key(Policy, ["id", "live"]), // exact target key, not implied by {id}
|
|
1395
|
-
contained(on(Policy, ["id", "live"]), on(Version, ["policy", "valid"])), // no gaps in the policy source span
|
|
1396
|
-
contained(on(Version, ["policy", "valid"]), on(Policy, ["id", "live"])) // no version overhang
|
|
1397
|
-
])
|
|
1398
|
-
```
|
|
1399
|
-
|
|
1400
|
-
Together the mutual containments prove equal point supports for each policy;
|
|
1401
|
-
the pointwise keys make those supports genuine partitions rather than
|
|
1402
|
-
overlapping covers. Touching half-open segments remain legal, and the same
|
|
1403
|
-
construction works with any scalar-prefix arity before the final interval
|
|
1404
|
-
position.
|
|
1405
|
-
|
|
1406
|
-
## 27. Derived facts, maintained
|
|
1407
|
-
|
|
1408
|
-
Guarantee: host discipline + validator/runtime premises — freshness comes from
|
|
1409
|
-
the generation witness; containment proves surviving rollup facts sound only
|
|
1410
|
-
(`lean/Bumbledb/Txn.lean: derived_soundness_vs_freshness`).
|
|
1411
|
-
|
|
1412
|
-
A stored rollup is an ordinary relation with an ordinary soundness statement.
|
|
1413
|
-
Here `pack` derives maximal busy spans, while containment prevents any stored
|
|
1414
|
-
`BusySpan` point that has no busy claim behind it. That is soundness, not a
|
|
1415
|
-
refresh theorem: a missing span remains representable until the host
|
|
1416
|
-
maintenance loop fills it.
|
|
1417
|
-
|
|
1418
|
-
```ts
|
|
1419
|
-
const Arm = closed("Arm", ["Busy", "Ooo"])
|
|
1420
|
-
const Claim = relation("Claim", { source: u64, person: u64, arm: Arm.id, span: interval(i64) })
|
|
1421
|
-
const BusySpan = relation("BusySpan", { person: u64, span: interval(i64) })
|
|
1422
|
-
|
|
1423
|
-
const MaintainedRollup = schema("MaintainedRollup", { Arm, Claim, BusySpan }, [
|
|
1424
|
-
contained(on(Claim, "arm"), on(Arm, "id")),
|
|
1425
|
-
key(Claim, ["source"]),
|
|
1426
|
-
key(Claim, ["person", "span"]),
|
|
1427
|
-
key(BusySpan, ["person", "span"]),
|
|
1428
|
-
contained(on(BusySpan, ["person", "span"]), on(Claim.where({ arm: "Busy" }), ["person", "span"]))
|
|
1429
|
-
])
|
|
1430
|
-
|
|
1431
|
-
// Derive the desired rollup on the maintenance snapshot:
|
|
1432
|
-
const deriving = query(MaintainedRollup).rule((r) => {
|
|
1433
|
-
const { source, person, span } = v(Claim)
|
|
1434
|
-
return r.match(Claim, { source, person, arm: "Busy", span }).find({ person, packed: r.pack(span) })
|
|
1435
|
-
})
|
|
1436
|
-
```
|
|
1437
|
-
|
|
1438
|
-
The host loop is `db.read` + `writeFrom`: derive on the attempt's snapshot, diff,
|
|
1439
|
-
build the delta — recipe 20's third idiom. On a moved generation the host
|
|
1440
|
-
throws away the attempt and retries on a fresh snapshot; it
|
|
1441
|
-
never retries a stale diff. Dependencies prove every surviving stored span
|
|
1442
|
-
sound, while the witness proves which source state the derivation saw;
|
|
1443
|
-
neither mechanism proves completeness. The engine's compiled copy
|
|
1444
|
-
(`maintain_busy_spans` in `cookbook.rs`) drives the retry-and-repack loop.
|
|
1445
|
-
|
|
1446
|
-
## Operating the store
|
|
1447
|
-
|
|
1448
|
-
## 28. Migration is ETL
|
|
1449
|
-
|
|
1450
|
-
Guarantee: Lean theorem + validator/runtime premises + host discipline —
|
|
1451
|
-
fingerprints refuse reinterpretation, final-state judgments validate each load
|
|
1452
|
-
(`lean/Bumbledb/Txn.lean: etl_lands_valid`), and the host owns
|
|
1453
|
-
the semantic transform and dependency-safe load order.
|
|
1454
|
-
|
|
1455
|
-
There is no in-place migration and never will be: a schema is a theory, the
|
|
1456
|
-
store records the theory's fingerprint, and `Db.open` under a changed theory
|
|
1457
|
-
is a hard fingerprint mismatch — the engine refuses to reinterpret facts it
|
|
1458
|
-
judged under different laws. Migration is extract, transform, load: `scan`
|
|
1459
|
-
exports every fact of a relation as typed values under one snapshot, the host
|
|
1460
|
-
transforms, and inserts (fresh ids resupplied — identity survives) land into
|
|
1461
|
-
a store created under the new theory, judged whole by the ordinary
|
|
1462
|
-
final-state judgment: load containment targets first, and a chunk that lands
|
|
1463
|
-
is already valid.
|
|
1464
|
-
|
|
1465
|
-
The v2 theory below adds what v1 never recorded — *when* a salary applied —
|
|
1466
|
-
as an interval with a pointwise key: one salary per employee per instant. The
|
|
1467
|
-
transform supplies the missing dimension (a ray from the migration epoch).
|
|
1468
|
-
|
|
1469
|
-
```ts
|
|
1470
|
-
// The old theory, judged and fingerprinted:
|
|
1471
|
-
const EmployeeV1 = relation("Employee", { id: u64.fresh, name: str })
|
|
1472
|
-
const SalaryV1 = relation("Salary", { employee: u64, amount: i64 })
|
|
1473
|
-
const PayrollV1 = schema("PayrollV1", { Employee: EmployeeV1, Salary: SalaryV1 }, [
|
|
1474
|
-
contained(on(SalaryV1, "employee"), on(EmployeeV1, "id"))
|
|
1475
|
-
])
|
|
1476
|
-
|
|
1477
|
-
// The new theory adds what v1 never recorded:
|
|
1478
|
-
const Employee = relation("Employee", { id: u64.fresh, name: str })
|
|
1479
|
-
const Salary = relation("Salary", { employee: u64, amount: i64, applies: interval(i64) })
|
|
1480
|
-
const Payroll = schema("Payroll", { Employee, Salary }, [
|
|
1481
|
-
contained(on(Salary, "employee"), on(Employee, "id")),
|
|
1482
|
-
key(Salary, ["employee", "applies"]) // one salary per instant
|
|
1483
|
-
])
|
|
1484
|
-
|
|
1485
|
-
// The post-migration read — salaries in force at an instant:
|
|
1486
|
-
const inForceAt = query(Payroll).rule((r) => {
|
|
1487
|
-
const { id: e, name } = v(Employee)
|
|
1488
|
-
const { amount, applies: w } = v(Salary)
|
|
1489
|
-
return r
|
|
1490
|
-
.match(Employee, { id: e, name })
|
|
1491
|
-
.match(Salary, { employee: e, amount, applies: w })
|
|
1492
|
-
.where(r.pointIn(r.param("at"), w))
|
|
1493
|
-
.find({ name, amount })
|
|
1494
|
-
})
|
|
1495
|
-
```
|
|
1496
|
-
|
|
1497
|
-
The engine's compiled test drives the whole loop (export under one snapshot,
|
|
1498
|
-
the fingerprint refusal, load order, identity, mint catch-up, judgment); the
|
|
1499
|
-
SDK pin asserts the two theories carry two distinct fingerprints — the
|
|
1500
|
-
refusal's premise. For stores whose creating schema is gone, `Db.exhume`
|
|
1501
|
-
reads the store's own persisted descriptor — the one schema-independent read
|
|
1502
|
-
path, the E half of ETL. Its lifetime is a `using` scope (`using exhumed =
|
|
1503
|
-
await Db.exhume(oldPath)`): disposal releases the store's exclusive lock at
|
|
1504
|
-
scope exit, so the rebirth loop's same-path retry never waits on GC.
|
|
1505
|
-
|
|
1506
|
-
## Composition
|
|
1507
|
-
|
|
1508
|
-
## 29. The zone ledger
|
|
1509
|
-
|
|
1510
|
-
Guarantee: Lean theorem + validator/runtime premises — per-kind mutual point
|
|
1511
|
-
coverage realizes each arm's exact partition
|
|
1512
|
-
(`lean/Bumbledb/Dependencies.lean: exact_partition_iff`) over one
|
|
1513
|
-
disjointness witness, and the mixed-width `==` positions type by element
|
|
1514
|
-
domain (`lean/Bumbledb/Schema.lean: Value.points_one_tag_u64`); witness
|
|
1515
|
-
segmentation is host discipline (the honesty note below).
|
|
1516
|
-
|
|
1517
|
-
Recipe 9's sidecar, composed: a ledger whose timeline divides into zones of
|
|
1518
|
-
two kinds — unit zones (`interval(u64, 1n)`) and pair zones
|
|
1519
|
-
(`interval(u64, 2n)`), each kind carrying its own payload sidecar. The
|
|
1520
|
-
discriminated-union pattern (recipe 2) applied at interval positions: a
|
|
1521
|
-
kind-discriminated `Zone` witness relation owns **cross-sidecar disjointness**
|
|
1522
|
-
through its one pointwise key, and since each sidecar's point support equals
|
|
1523
|
-
its kind's zone support (the per-kind `mirrors`), a unit slot can never
|
|
1524
|
-
overlap a pair slot even though they live in different relations. The arm
|
|
1525
|
-
widths are enforced **by type**: a `UnitSlot` value is width 1 or does not
|
|
1526
|
-
exist — no runtime width check, nothing to enforce at commit.
|
|
1527
|
-
|
|
1528
|
-
```ts
|
|
1529
|
-
const Kind = closed("Kind", ["Unit", "Pair"])
|
|
1530
|
-
const Ledger = relation("Ledger", { id: u64.fresh, name: str })
|
|
1531
|
-
// The witness: every zone of the ledger, kind-discriminated; its one
|
|
1532
|
-
// pointwise key is the cross-sidecar disjointness proof.
|
|
1533
|
-
const Zone = relation("Zone", { ledger: u64, kind: Kind.id, at: interval(u64) })
|
|
1534
|
-
const UnitSlot = relation("UnitSlot", { ledger: u64, at: interval(u64, 1n), entry: u64 })
|
|
1535
|
-
const PairSlot = relation("PairSlot", { ledger: u64, at: interval(u64, 2n), entry: u64 })
|
|
1536
|
-
|
|
1537
|
-
const ZoneLedger = schema("ZoneLedger", { Kind, Ledger, Zone, UnitSlot, PairSlot }, [
|
|
1538
|
-
contained(on(Zone, "ledger"), on(Ledger, "id")),
|
|
1539
|
-
contained(on(Zone, "kind"), on(Kind, "id")),
|
|
1540
|
-
key(Zone, ["ledger", "at"]), // all zones disjoint, whatever the kind
|
|
1541
|
-
key(UnitSlot, ["ledger", "at"]),
|
|
1542
|
-
key(PairSlot, ["ledger", "at"]),
|
|
1543
|
-
// Each kind's zones carry exactly its sidecar's points — mixed widths,
|
|
1544
|
-
// one element domain:
|
|
1545
|
-
mirrors(on(Zone.where({ kind: "Unit" }), ["ledger", "at"]), on(UnitSlot, ["ledger", "at"])),
|
|
1546
|
-
mirrors(on(Zone.where({ kind: "Pair" }), ["ledger", "at"]), on(PairSlot, ["ledger", "at"]))
|
|
1547
|
-
])
|
|
346
|
+
)
|
|
347
|
+
void correct
|
|
1548
348
|
```
|
|
1549
349
|
|
|
1550
|
-
|
|
1551
|
-
compare point supports, not rows. A single Unit-kind zone `[4,6)` beside two
|
|
1552
|
-
unit slots `[4,5)`, `[5,6)` satisfies both directions, because nothing forces
|
|
1553
|
-
the witness rows to mirror the sidecar's segmentation — only its points. If
|
|
1554
|
-
per-row correspondence matters, the host writes zones at slot granularity;
|
|
1555
|
-
the schema proves disjointness and coverage either way.
|
|
1556
|
-
|
|
1557
|
-
## Point reads
|
|
350
|
+
## 11. Exact floats and dense intervals
|
|
1558
351
|
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
keyed point read answers exactly that fact or nothing, on every scope
|
|
1564
|
-
(`ts/test/keyed-get.test.ts`; the engine half is
|
|
1565
|
-
`crates/bumbledb/tests/keyed_get.rs`).
|
|
1566
|
-
|
|
1567
|
-
The key is a **law**, and the read surface is that law made callable. The
|
|
1568
|
-
schema says `key(Course, ["grp"])` — one course per group — so "the
|
|
1569
|
-
course of a group" is a well-posed question with at most one answer, and
|
|
1570
|
-
the store already enforces that on every commit. Hold the statement VALUE:
|
|
1571
|
-
it is the read's selector below (statement identity is the membership rule).
|
|
1572
|
-
|
|
1573
|
-
```ts
|
|
1574
|
-
const Grp = relation("Grp", { id: u64.fresh, label: str })
|
|
1575
|
-
const Course = relation("Course", { id: u64.fresh, grp: u64, title: str })
|
|
1576
|
-
// The law: one course per group — the callable key.
|
|
1577
|
-
const courseGrpKey = key(Course, ["grp"])
|
|
1578
|
-
|
|
1579
|
-
const KeyedRead = schema("KeyedRead", { Grp, Course }, [
|
|
1580
|
-
contained(on(Course, "grp"), on(Grp, "id")),
|
|
1581
|
-
courseGrpKey
|
|
1582
|
-
])
|
|
1583
|
-
```
|
|
1584
|
-
|
|
1585
|
-
The point read is the statement handed back to `get` — one spelling on
|
|
1586
|
-
every scope (the symmetry rule): `db.get` standalone, `snap.get` inside a
|
|
1587
|
-
read scope, `tx.get` inside a write transaction, where the transaction side
|
|
1588
|
-
answers the FINAL state (base plus pending delta: read-your-writes, a
|
|
1589
|
-
pending delete answers `undefined`). The key object is typed by the
|
|
1590
|
-
statement's own projection — a wrong field name is a compile error, never a
|
|
1591
|
-
runtime shape check. The primary 2-arg form needs no statement: the fresh
|
|
1592
|
-
field IS the primary key.
|
|
352
|
+
`f64` is a real schema scalar: NaN canonicalizes to the one quiet NaN,
|
|
353
|
+
`-0` to `+0`, and the relational order is total. `interval(f64)` is the
|
|
354
|
+
parameterized dense interval — half-open, NaN-free, strictly ordered.
|
|
355
|
+
`span` builds checked interval values as `Result`s.
|
|
1593
356
|
|
|
1594
357
|
```ts
|
|
1595
|
-
const
|
|
1596
|
-
|
|
1597
|
-
throw new Error("create rejected")
|
|
1598
|
-
}
|
|
1599
|
-
const db = created.value
|
|
1600
|
-
|
|
1601
|
-
const minted: { grp?: bigint } = {}
|
|
1602
|
-
db.write((tx) => {
|
|
1603
|
-
const g = tx.reserve(Grp, "id", 1n).at(0n)!
|
|
1604
|
-
tx.insert(Grp, [{ id: g, label: "algebra" }])
|
|
1605
|
-
const course = tx.reserve(Course, "id", 1n).at(0n)!
|
|
1606
|
-
tx.insert(Course, [{ id: course, grp: g, title: "linear equations" }])
|
|
1607
|
-
minted.grp = g
|
|
1608
|
-
})
|
|
1609
|
-
const grp = minted.grp ?? 0n
|
|
358
|
+
const Window = relation("Window", { id: uuid, confidence: interval(f64), during: interval(i64) })
|
|
359
|
+
const Windows = schema("Windows", { Window }, [key(Window, ["id"])])
|
|
1610
360
|
|
|
1611
|
-
|
|
1612
|
-
const
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
const viaSnap = db.read((instance) => instance.get(Course, courseGrpKey, { grp }))
|
|
1616
|
-
|
|
1617
|
-
// tx.get — key-shaped read-modify-write, final-state (recipe 20's third
|
|
1618
|
-
// idiom): per-fact premises need no earlier snapshot witness.
|
|
1619
|
-
db.write((tx) => {
|
|
1620
|
-
const current = tx.get(Course, courseGrpKey, { grp })
|
|
1621
|
-
if (current !== undefined) {
|
|
1622
|
-
tx.delete(Course, [current])
|
|
1623
|
-
tx.insert(Course, [{ id: current.id, grp: current.grp, title: "linear equations II" }])
|
|
1624
|
-
}
|
|
1625
|
-
})
|
|
1626
|
-
|
|
1627
|
-
// The primary 2-arg form — the fresh field is the primary key:
|
|
1628
|
-
const byId = byGroup === undefined ? undefined : db.read((i) => i.get(Course, { id: byGroup.id }))
|
|
361
|
+
const discrete = span(0n, 60n)
|
|
362
|
+
const dense = span(0.25, 1.5)
|
|
363
|
+
const bothChecked: boolean = Result.isSuccess(discrete) && Result.isSuccess(dense)
|
|
364
|
+
void [Windows, bothChecked]
|
|
1629
365
|
```
|
|
1630
366
|
|
|
1631
|
-
|
|
1632
|
-
exists — `snap.scan(Course).find((row) => row.grp === grp)` — re-derives
|
|
1633
|
-
in the host what the store already enforces. The uniqueness the fold
|
|
1634
|
-
quietly assumes IS the declared key statement; spell the law and the point
|
|
1635
|
-
read comes with it.
|
|
1636
|
-
|
|
1637
|
-
## Capacity laws
|
|
1638
|
-
|
|
1639
|
-
## 31. The power budget
|
|
367
|
+
## 12. Scoped ownership and honest close
|
|
1640
368
|
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
capacity_plan_decides`); the pinned-column containment proves a device's
|
|
1646
|
-
watts equals its model's at every commit.
|
|
1647
|
-
|
|
1648
|
-
Per-group capacity is one statement — the `capacity` builder mirrors the
|
|
1649
|
-
operator positionally (target, weight, window, source): `weigh` names the
|
|
1650
|
-
measure on the SOURCE row, and `ref` reads each group's ceiling from the
|
|
1651
|
-
TARGET row (hi slot only — ruled 2026-07-24, C6; bound names resolve
|
|
1652
|
-
against the target's full roster, ruled 2026-07-24, C1).
|
|
369
|
+
Every native resource is scoped; early `close()` is itself an Effect
|
|
370
|
+
returning the honest `CloseReport`. A scope finalizer that cannot complete
|
|
371
|
+
teardown surfaces a structured `CloseFailure` DEFECT in the Cause — never a
|
|
372
|
+
silently swallowed failure, never falsely reclaimed resources.
|
|
1653
373
|
|
|
1654
374
|
```ts
|
|
1655
|
-
const
|
|
1656
|
-
const
|
|
1657
|
-
const Device = relation("Device", {
|
|
1658
|
-
id: u64.fresh,
|
|
1659
|
-
pool: u64,
|
|
1660
|
-
model: u64,
|
|
1661
|
-
watts: u64
|
|
1662
|
-
})
|
|
375
|
+
const Item = relation("Item", { id: uuid, label: str })
|
|
376
|
+
const Items = schema("Items", { Item }, [key(Item, ["id"])])
|
|
1663
377
|
|
|
1664
|
-
const
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
// utilization is a query, never a column (the ledger's law, recipe 19):
|
|
1676
|
-
const draw = query(Racks).rule((r) => {
|
|
1677
|
-
const { id, pool, watts } = v(Device)
|
|
1678
|
-
return r.match(Device, { id, pool, watts }).find({ pool, total: r.sum(watts) })
|
|
1679
|
-
})
|
|
378
|
+
const explicitClose = Effect.scoped(
|
|
379
|
+
Effect.gen(function* () {
|
|
380
|
+
const db = yield* Db.open(localPath, Items, work)
|
|
381
|
+
const report = yield* db.close()
|
|
382
|
+
// `closed` releases this capability's obligations; `incomplete` and
|
|
383
|
+
// `failed` retain native Closing accounting — they are never
|
|
384
|
+
// counted as reclaimed.
|
|
385
|
+
return report.kind
|
|
386
|
+
})
|
|
387
|
+
)
|
|
388
|
+
void explicitClose
|
|
1680
389
|
```
|
|
1681
390
|
|
|
1682
|
-
|
|
1683
|
-
names exactly this pair — the weight vocabulary is closed at the row (ruled
|
|
1684
|
-
2026-07-24, ruling 6): a weight read through a reference would be a
|
|
1685
|
-
maintained copy of another relation's field, and a catalog edit would
|
|
1686
|
-
silently re-weigh deployed fleets. Pinned, the inconsistent commit refuses
|
|
1687
|
-
at the device site and the migration is explicit.
|
|
1688
|
-
|
|
1689
|
-
## 32. Calendar capacity
|
|
391
|
+
## 13. Unresolved field arithmetic is authoring metadata
|
|
1690
392
|
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
Duration-capable bounds, ruled 2026-07-24, C18; a ray-valued weight or
|
|
1695
|
-
bound refuses typed at the law site, ruled 2026-07-24, C10).
|
|
1696
|
-
|
|
1697
|
-
"Total booked time per room stays within the room's span" — one statement.
|
|
1698
|
-
The interval enters through the measure argument, never the group key.
|
|
393
|
+
`Scalar.field("units")` is not a typed program. Builders accept it inside
|
|
394
|
+
arithmetic. Native compilation binds it against the verified source
|
|
395
|
+
schema — including empty input — before any manifest write or freeze.
|
|
1699
396
|
|
|
1700
397
|
```ts
|
|
1701
|
-
const
|
|
1702
|
-
const
|
|
1703
|
-
|
|
1704
|
-
room: u64,
|
|
1705
|
-
booked: interval(i64)
|
|
1706
|
-
})
|
|
1707
|
-
|
|
1708
|
-
const Rooms = schema("Rooms", { Room, Booking }, [
|
|
1709
|
-
contained(on(Booking, "room"), on(Room, "id")),
|
|
1710
|
-
// The pointwise key forbids double-booking (recipe 1); the capacity law
|
|
1711
|
-
// bounds the TOTAL. Different laws — a schema usually wants both.
|
|
1712
|
-
key(Booking, ["room", "booked"]),
|
|
1713
|
-
capacity(
|
|
1714
|
-
on(Room, "id"),
|
|
1715
|
-
weigh(duration("booked")),
|
|
1716
|
-
within(0n, duration("span")),
|
|
1717
|
-
on(Booking, "room")
|
|
1718
|
-
)
|
|
1719
|
-
])
|
|
1720
|
-
|
|
1721
|
-
// the booked time per room is host arithmetic on the `booked`
|
|
1722
|
-
// endpoints every answer row already carries (`end − start`).
|
|
398
|
+
const incrementUnits = Scalar.add(Scalar.field("units"), Scalar.u64(1n))
|
|
399
|
+
const asFloat = Scalar.toF64(Scalar.add(Scalar.field("units"), Scalar.u64(1n)))
|
|
400
|
+
void [incrementUnits, asFloat]
|
|
1723
401
|
```
|
|
1724
|
-
|
|
1725
|
-
Mind the weighted `{0}` and the weighted floor: on a weighted statement
|
|
1726
|
-
`within(0n)` says "the group's total is zero" (zero-measure rows may exist
|
|
1727
|
-
— the weaker law than the unit exclusion), and `within(1n, "*")` under a
|
|
1728
|
-
weight ("positive total") is not "at least one booking" — that intent is
|
|
1729
|
-
the bare `contained`. Choose by what you mean.
|