@bjornpagen/bumbledb 0.3.0 → 0.5.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 +246 -92
- package/README.md +31 -16
- package/dist/closed.d.ts +80 -75
- package/dist/closed.d.ts.map +1 -1
- package/dist/closed.js +102 -127
- package/dist/closed.js.map +1 -1
- package/dist/db.d.ts +37 -7
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +95 -55
- package/dist/db.js.map +1 -1
- package/dist/exhume.d.ts.map +1 -1
- package/dist/exhume.js +1 -14
- package/dist/exhume.js.map +1 -1
- package/dist/face.d.ts +40 -40
- package/dist/face.d.ts.map +1 -1
- package/dist/face.js +9 -17
- package/dist/face.js.map +1 -1
- package/dist/fields.d.ts +46 -15
- package/dist/fields.d.ts.map +1 -1
- package/dist/fields.js +58 -29
- package/dist/fields.js.map +1 -1
- package/dist/index.d.ts +13 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -5
- package/dist/index.js.map +1 -1
- package/dist/law.d.ts +2 -1
- package/dist/law.d.ts.map +1 -1
- package/dist/law.js +15 -14
- package/dist/law.js.map +1 -1
- package/dist/lower.d.ts.map +1 -1
- package/dist/lower.js +1 -7
- package/dist/lower.js.map +1 -1
- package/dist/marshal.d.ts +33 -6
- package/dist/marshal.d.ts.map +1 -1
- package/dist/marshal.js +75 -26
- package/dist/marshal.js.map +1 -1
- package/dist/native.d.ts +21 -2
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js +20 -3
- package/dist/native.js.map +1 -1
- package/dist/order.d.ts +36 -0
- package/dist/order.d.ts.map +1 -0
- package/dist/order.js +135 -0
- package/dist/order.js.map +1 -0
- package/dist/query/atom.d.ts +76 -28
- package/dist/query/atom.d.ts.map +1 -1
- package/dist/query/atom.js +12 -16
- package/dist/query/atom.js.map +1 -1
- package/dist/query/lower.d.ts +5 -8
- package/dist/query/lower.d.ts.map +1 -1
- package/dist/query/lower.js +308 -72
- package/dist/query/lower.js.map +1 -1
- package/dist/query/predicate.d.ts.map +1 -1
- package/dist/query/predicate.js +34 -2
- package/dist/query/predicate.js.map +1 -1
- package/dist/query/run.d.ts +15 -5
- package/dist/query/run.d.ts.map +1 -1
- package/dist/query/run.js +27 -8
- package/dist/query/run.js.map +1 -1
- package/dist/query/scope.d.ts +37 -29
- package/dist/query/scope.d.ts.map +1 -1
- package/dist/query/scope.js +19 -47
- package/dist/query/scope.js.map +1 -1
- package/dist/relation.d.ts +17 -29
- package/dist/relation.d.ts.map +1 -1
- package/dist/relation.js +41 -38
- package/dist/relation.js.map +1 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +7 -31
- package/dist/schema.js.map +1 -1
- package/dist/spec.d.ts +3 -2
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js.map +1 -1
- package/dist/statements.d.ts +13 -4
- package/dist/statements.d.ts.map +1 -1
- package/dist/statements.js +75 -8
- package/dist/statements.js.map +1 -1
- package/package.json +2 -5
- package/src/closed.ts +144 -206
- package/src/db.ts +143 -68
- package/src/exhume.ts +1 -15
- package/src/face.ts +38 -48
- package/src/fields.ts +103 -49
- package/src/index.ts +11 -10
- package/src/law.ts +15 -14
- package/src/lower.ts +2 -9
- package/src/marshal.ts +82 -31
- package/src/native.ts +22 -4
- package/src/order.ts +156 -0
- package/src/query/atom.ts +70 -35
- package/src/query/lower.ts +354 -82
- package/src/query/predicate.ts +39 -4
- package/src/query/run.ts +27 -9
- package/src/query/scope.ts +52 -70
- package/src/relation.ts +54 -68
- package/src/schema.ts +7 -33
- package/src/spec.ts +3 -2
- package/src/statements.ts +82 -8
package/COOKBOOK.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# The cookbook — modeling intuition as schemas, in TypeScript
|
|
2
2
|
|
|
3
|
-
The bumbledb engine's
|
|
3
|
+
The bumbledb engine's 30 cookbook recipes (`bumbledb/docs/cookbook.md`),
|
|
4
4
|
translated to this SDK's structural API. **This document is illustrative,
|
|
5
5
|
never normative**: where a recipe and an engine architecture chapter disagree,
|
|
6
6
|
the chapter wins (`docs/architecture/README.md` rule 5) — the SDK is the same
|
|
@@ -33,12 +33,15 @@ Everything below imports from the one package entry:
|
|
|
33
33
|
import {
|
|
34
34
|
ALLEN,
|
|
35
35
|
Db,
|
|
36
|
+
type Infer,
|
|
36
37
|
abandon,
|
|
37
38
|
allen,
|
|
38
39
|
bool,
|
|
40
|
+
by,
|
|
39
41
|
bytes,
|
|
40
42
|
closed,
|
|
41
43
|
contained,
|
|
44
|
+
desc,
|
|
42
45
|
eq,
|
|
43
46
|
i64,
|
|
44
47
|
interval,
|
|
@@ -115,10 +118,11 @@ const Uptime = schema("Uptime", { Service, Outage }, [
|
|
|
115
118
|
key(Outage, ["service", "window"])
|
|
116
119
|
])
|
|
117
120
|
|
|
118
|
-
// down at instant t — `r.
|
|
119
|
-
//
|
|
121
|
+
// down at instant t — `r.var` names each variable (typed by the field it
|
|
122
|
+
// first binds), and shorthand punning binds same-named columns:
|
|
120
123
|
const downAt = query(Uptime).rule((r) => {
|
|
121
|
-
const
|
|
124
|
+
const service = r.var("service")
|
|
125
|
+
const window = r.var("window")
|
|
122
126
|
return r
|
|
123
127
|
.match(Outage, { service, window })
|
|
124
128
|
.where(pointIn(r.param("t"), window))
|
|
@@ -126,7 +130,8 @@ const downAt = query(Uptime).rule((r) => {
|
|
|
126
130
|
})
|
|
127
131
|
// overlapping an incident window (one Allen mask, no operator zoo):
|
|
128
132
|
const overlapping = query(Uptime).rule((r) => {
|
|
129
|
-
const
|
|
133
|
+
const service = r.var("service")
|
|
134
|
+
const window = r.var("window")
|
|
130
135
|
return r
|
|
131
136
|
.match(Outage, { service, window })
|
|
132
137
|
.where(allen(window, ALLEN.intersects, r.param("incident")))
|
|
@@ -134,7 +139,8 @@ const overlapping = query(Uptime).rule((r) => {
|
|
|
134
139
|
})
|
|
135
140
|
// total downtime per service (the denotation's one arithmetic):
|
|
136
141
|
const downtime = query(Uptime).rule((r) => {
|
|
137
|
-
const
|
|
142
|
+
const service = r.var("service")
|
|
143
|
+
const window = r.var("window")
|
|
138
144
|
return r.match(Outage, { service, window }).select("service", r.sum(r.duration("window")))
|
|
139
145
|
})
|
|
140
146
|
```
|
|
@@ -150,8 +156,8 @@ relations, glued by bidirectional conditional containments.
|
|
|
150
156
|
|
|
151
157
|
```ts
|
|
152
158
|
// The discriminator vocabulary is a closed relation: its ground axioms are
|
|
153
|
-
// axioms, and
|
|
154
|
-
// the
|
|
159
|
+
// axioms, and a handle is its NAME — the string literal "Deterministic" is
|
|
160
|
+
// the ONE spelling, on every surface (statements, inserts, queries, rows).
|
|
155
161
|
const Kind = closed("Kind", ["Deterministic", "CustomOperator"])
|
|
156
162
|
const Task = relation("Task", { id: u64.fresh, kind: Kind.id })
|
|
157
163
|
const DeterministicGrading = relation("DeterministicGrading", { task: u64, tolerance: i64 })
|
|
@@ -166,21 +172,28 @@ const Grading = schema("Grading", { Kind, Task, DeterministicGrading, CustomOper
|
|
|
166
172
|
// exists WITH that kind — composite-FK-plus-CHECK, one statement. These
|
|
167
173
|
// mirrors are also what type `task` on both arms: each lands in the
|
|
168
174
|
// "Task.id" generator class.
|
|
169
|
-
mirrors(on(Task.where({ kind:
|
|
170
|
-
mirrors(on(Task.where({ kind:
|
|
175
|
+
mirrors(on(Task.where({ kind: "Deterministic" }), "id"), on(DeterministicGrading, "task")),
|
|
176
|
+
mirrors(on(Task.where({ kind: "CustomOperator" }), "id"), on(CustomOperatorGrading, "task"))
|
|
171
177
|
// Exclusivity is a theorem, not a statement: one id in two arms would
|
|
172
178
|
// force `kind` to equal two handles against the fresh key on id.
|
|
173
179
|
// The executor spends the same theorem again — recipe 22's free lunch.
|
|
174
180
|
])
|
|
175
181
|
|
|
176
|
-
// Host dispatch over the discriminator is `
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
182
|
+
// Host dispatch over the discriminator is native `switch` narrowing over
|
|
183
|
+
// the handle union (`Infer<typeof Kind.id>` = "Deterministic" |
|
|
184
|
+
// "CustomOperator") — rows already arrive carrying the handle name, and
|
|
185
|
+
// `satisfies never` makes the switch exhaustive: a missing arm is a
|
|
186
|
+
// compile error.
|
|
187
|
+
const gradedBy = (kind: Infer<typeof Kind.id>) => {
|
|
188
|
+
switch (kind) {
|
|
189
|
+
case "Deterministic":
|
|
190
|
+
return "tolerance"
|
|
191
|
+
case "CustomOperator":
|
|
192
|
+
return "operator"
|
|
193
|
+
default:
|
|
194
|
+
return kind satisfies never
|
|
195
|
+
}
|
|
196
|
+
}
|
|
184
197
|
```
|
|
185
198
|
|
|
186
199
|
## 3. 0..1 optional attributes
|
|
@@ -207,7 +220,7 @@ const Optionality = schema("Optionality", { Business, MailingAddress }, [
|
|
|
207
220
|
|
|
208
221
|
// Negation is plain anti-join (no null branch exists in any operator):
|
|
209
222
|
const unaddressed = query(Optionality).rule((r) => {
|
|
210
|
-
const
|
|
223
|
+
const b = r.var("b")
|
|
211
224
|
return r
|
|
212
225
|
.match(Business, { id: b })
|
|
213
226
|
.where(not(MailingAddress, { business: b }))
|
|
@@ -247,7 +260,10 @@ const Money = schema("Money", { Currency, Account, Posting }, [
|
|
|
247
260
|
// silently. Bind the fresh id: set semantics would collapse two equal
|
|
248
261
|
// (account, currency, minor) postings without it.
|
|
249
262
|
const totals = query(Money).rule((r) => {
|
|
250
|
-
const
|
|
263
|
+
const id = r.var("id")
|
|
264
|
+
const account = r.var("account")
|
|
265
|
+
const currency = r.var("currency")
|
|
266
|
+
const minor = r.var("minor")
|
|
251
267
|
return r.match(Posting, { id, account, currency, minor }).select("account", "currency", r.sum("minor"))
|
|
252
268
|
})
|
|
253
269
|
```
|
|
@@ -284,7 +300,7 @@ const Content = schema("Content", { Region, Document, Replica }, [
|
|
|
284
300
|
|
|
285
301
|
// a bytes param self-encodes (Uint8Array by inference):
|
|
286
302
|
const byDigest = query(Content).rule((r) => {
|
|
287
|
-
const
|
|
303
|
+
const id = r.var("id")
|
|
288
304
|
return r.match(Document, { id, payload: r.param("digest") }).select("id")
|
|
289
305
|
})
|
|
290
306
|
```
|
|
@@ -301,13 +317,13 @@ declared priority handles.
|
|
|
301
317
|
The enum idiom's replacement, first-class: a vocabulary is a **closed
|
|
302
318
|
relation** — its ground axioms are declared in the schema, sealed at
|
|
303
319
|
validate, frozen by the fingerprint, virtual in storage. The store holds zero
|
|
304
|
-
vocabulary bytes, and
|
|
320
|
+
vocabulary bytes, and handle names are the string literals on every surface.
|
|
305
321
|
|
|
306
322
|
```ts
|
|
307
|
-
// Tier 1: handles only.
|
|
308
|
-
//
|
|
309
|
-
// vocabulary stays relational
|
|
310
|
-
//
|
|
323
|
+
// Tier 1: handles only. At the host surface a handle is its NAME — a string
|
|
324
|
+
// literal of the roster's union ("Low" | "Normal" | "Urgent"); the engine's
|
|
325
|
+
// vocabulary stays relational (ids = declaration order) and the marshal
|
|
326
|
+
// owns the bijection. Dispatch is native `switch` narrowing (recipe 2).
|
|
311
327
|
const Priority = closed("Priority", ["Low", "Normal", "Urgent"])
|
|
312
328
|
|
|
313
329
|
const Ticket = relation("Ticket", { id: u64.fresh, priority: Priority.id, opened_at: i64 })
|
|
@@ -326,8 +342,17 @@ const Tickets = schema("Tickets", { Priority, Ticket }, [
|
|
|
326
342
|
// that drifts without a rebuild is an ordinary relation — a vocabulary is
|
|
327
343
|
// never written, only declared.
|
|
328
344
|
const urgent = query(Tickets).rule((r) => {
|
|
329
|
-
const
|
|
330
|
-
return r.match(Ticket, { id: t, priority:
|
|
345
|
+
const t = r.var("t")
|
|
346
|
+
return r.match(Ticket, { id: t, priority: "Urgent" }).select("t")
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
// Set membership is a plain array — the drizzle law's spelling, closed-only
|
|
350
|
+
// in query match records (an ordinary u64/str field's membership is a bound
|
|
351
|
+
// ∈-set param, `r.inSet`); the array folds to the same wire set the param
|
|
352
|
+
// spelling crosses. In `.where()` selections arrays work at EVERY field kind.
|
|
353
|
+
const actionable = query(Tickets).rule((r) => {
|
|
354
|
+
const t = r.var("t")
|
|
355
|
+
return r.match(Ticket, { id: t, priority: ["Normal", "Urgent"] }).select("t")
|
|
331
356
|
})
|
|
332
357
|
```
|
|
333
358
|
|
|
@@ -371,20 +396,24 @@ const Review = schema("Review", { Kind, Attempt, Certificate }, [
|
|
|
371
396
|
// exactly like an ordinary one, and the atom folds at prepare into a
|
|
372
397
|
// plan-constant handle set on its sibling.
|
|
373
398
|
const masteredAttempts = query(Review).rule((r) => {
|
|
374
|
-
const
|
|
399
|
+
const a = r.var("a")
|
|
400
|
+
const k = r.var("k")
|
|
375
401
|
return r
|
|
376
402
|
.match(Attempt, { id: a, kind: k })
|
|
377
403
|
.match(Kind, { id: k, mastered: true })
|
|
378
404
|
.select("a")
|
|
379
405
|
})
|
|
380
406
|
|
|
381
|
-
// Host dispatch on the payload tier
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
}
|
|
407
|
+
// Host dispatch on the payload tier is the record-table idiom — a `Record`
|
|
408
|
+
// over the handle union is total by type (a missing or extra entry is a
|
|
409
|
+
// compile error), and each entry reads its sealed axiom row off the typed
|
|
410
|
+
// `Kind.axioms` readback:
|
|
411
|
+
const labels: Record<Infer<typeof Kind.id>, string> = {
|
|
412
|
+
DirectPass: `mastered, rank ${Kind.axioms.DirectPass.rank}`,
|
|
413
|
+
JudgedPass: `mastered, rank ${Kind.axioms.JudgedPass.rank}`,
|
|
414
|
+
Failed: "not mastered"
|
|
415
|
+
}
|
|
416
|
+
const label = (k: Infer<typeof Kind.id>) => labels[k]
|
|
388
417
|
```
|
|
389
418
|
|
|
390
419
|
Two honest boundaries. The fold has limits: payload escaping to the head and
|
|
@@ -425,13 +454,14 @@ const Oncall = schema("Oncall", { Severity, Incident, Escalation }, [
|
|
|
425
454
|
// The sub-vocabulary: an escalation carries a PAGING severity, by
|
|
426
455
|
// statement. ψ over the sealed extension compiles to the member set
|
|
427
456
|
// {Critical, Fatal}; the judgment is one bit test per touched fact,
|
|
428
|
-
// and an escalation at
|
|
457
|
+
// and an escalation at "Info" aborts the commit.
|
|
429
458
|
contained(on(Escalation, "severity"), on(Severity.where({ pages: true }), "id"))
|
|
430
459
|
])
|
|
431
460
|
|
|
432
461
|
// who is being paged — the same ψ, on the read side:
|
|
433
462
|
const paged = query(Oncall).rule((r) => {
|
|
434
|
-
const
|
|
463
|
+
const i = r.var("i")
|
|
464
|
+
const s = r.var("s")
|
|
435
465
|
return r
|
|
436
466
|
.match(Escalation, { incident: i, severity: s })
|
|
437
467
|
.match(Severity, { id: s, pages: true })
|
|
@@ -448,7 +478,8 @@ plus pointwise keys realizes exact partition
|
|
|
448
478
|
(`lean/Bumbledb/Dependencies.lean: exact_partition_iff`), and the mixed-width
|
|
449
479
|
interval positions type by element domain
|
|
450
480
|
(`lean/Bumbledb/Schema.lean: Value.points_one_tag_u64`); ordering the result
|
|
451
|
-
remains a host presentation step
|
|
481
|
+
remains a host presentation step — the SDK ships the comparator (`by`/`desc`,
|
|
482
|
+
keys as data); the engine never orders.
|
|
452
483
|
|
|
453
484
|
The linked-list verdict: successor pointers are control flow smuggled into
|
|
454
485
|
data. Order is a value. The idiomatic ordered collection is an interval
|
|
@@ -476,12 +507,21 @@ const Playlists = schema("Playlists", { Playlist, Extent, Slot }, [
|
|
|
476
507
|
|
|
477
508
|
// Positional access is membership — "what plays at position ?pos":
|
|
478
509
|
const playingAt = query(Playlists).rule((r) => {
|
|
479
|
-
const
|
|
510
|
+
const slot = r.var("slot")
|
|
511
|
+
const track = r.var("track")
|
|
480
512
|
return r
|
|
481
513
|
.match(Slot, { playlist: r.param("list"), slot, track })
|
|
482
514
|
.where(pointIn(r.param("pos"), slot))
|
|
483
515
|
.select("track")
|
|
484
516
|
})
|
|
517
|
+
|
|
518
|
+
// Answers are SETS — the host sorts them, and the SDK ships the comparator:
|
|
519
|
+
// sort keys as data, a bare name ascending, `desc(...)` the flip; intervals
|
|
520
|
+
// order by (start, end). Limit is the language's own `.slice(0, n)`.
|
|
521
|
+
const inPlayOrder = [
|
|
522
|
+
{ slot: { start: 1n, end: 2n }, track: "b" },
|
|
523
|
+
{ slot: { start: 0n, end: 1n }, track: "a" }
|
|
524
|
+
].sort(by("slot", "track"))
|
|
485
525
|
```
|
|
486
526
|
|
|
487
527
|
Middle insert is honest about its cost: making room at position `k` shifts
|
|
@@ -512,8 +552,8 @@ const Ast = schema("Ast", { Kind, Node, Lit, Add, Parent }, [
|
|
|
512
552
|
key(Lit, ["node"]),
|
|
513
553
|
key(Add, ["node"]),
|
|
514
554
|
// Every node's arm is total, valid, and exclusive (recipe 2's theorems):
|
|
515
|
-
mirrors(on(Node.where({ kind:
|
|
516
|
-
mirrors(on(Node.where({ kind:
|
|
555
|
+
mirrors(on(Node.where({ kind: "Lit" }), "id"), on(Lit, "node")),
|
|
556
|
+
mirrors(on(Node.where({ kind: "Add" }), "id"), on(Add, "node")),
|
|
517
557
|
// Every child edge resolves — no dangling subtrees, judged at commit
|
|
518
558
|
// (these containments also put lhs/rhs in the "Node.id" class, which is
|
|
519
559
|
// exactly what lets the query below join lhs against Lit.node):
|
|
@@ -528,7 +568,8 @@ const Ast = schema("Ast", { Kind, Node, Lit, Add, Parent }, [
|
|
|
528
568
|
])
|
|
529
569
|
|
|
530
570
|
const lhsLiteral = query(Ast).rule((r) => {
|
|
531
|
-
const
|
|
571
|
+
const l = r.var("l")
|
|
572
|
+
const v = r.var("v")
|
|
532
573
|
return r
|
|
533
574
|
.match(Add, { node: r.param("n"), lhs: l })
|
|
534
575
|
.match(Lit, { node: l, value: v })
|
|
@@ -563,7 +604,8 @@ const Graph = schema("Graph", { Person, Repo, Follows, Maintains }, [
|
|
|
563
604
|
// live in the "Person.id" class, so the reuse is lawful); `lt` keeps each
|
|
564
605
|
// pair once:
|
|
565
606
|
const mutual = query(Graph).rule((r) => {
|
|
566
|
-
const
|
|
607
|
+
const a = r.var("a")
|
|
608
|
+
const b = r.var("b")
|
|
567
609
|
return r
|
|
568
610
|
.match(Follows, { follower: a, followee: b })
|
|
569
611
|
.match(Follows, { follower: b, followee: a })
|
|
@@ -601,7 +643,11 @@ const Ecs = schema("Ecs", { Entity, Transform, Velocity, Renderable }, [
|
|
|
601
643
|
|
|
602
644
|
// The physics join is the component intersection:
|
|
603
645
|
const physics = query(Ecs).rule((r) => {
|
|
604
|
-
const
|
|
646
|
+
const entity = r.var("entity")
|
|
647
|
+
const x = r.var("x")
|
|
648
|
+
const y = r.var("y")
|
|
649
|
+
const dx = r.var("dx")
|
|
650
|
+
const dy = r.var("dy")
|
|
605
651
|
return r
|
|
606
652
|
.match(Transform, { entity, x, y })
|
|
607
653
|
.match(Velocity, { entity, dx, dy })
|
|
@@ -635,15 +681,16 @@ const Orders = schema("Orders", { State, Order, Placement, Shipment }, [
|
|
|
635
681
|
// The conditional target, both ways: every Shipment references an order
|
|
636
682
|
// THAT IS Shipped (validity), and every Shipped order has its Shipment
|
|
637
683
|
// (totality) — the transition and its evidence commit together.
|
|
638
|
-
mirrors(on(Shipment, "order"), on(Order.where({ state:
|
|
684
|
+
mirrors(on(Shipment, "order"), on(Order.where({ state: "Shipped" }), "id"))
|
|
639
685
|
// Transition predicates ("only Placed may ship") are host code under the
|
|
640
686
|
// generation witness — recipe 20; the schema pins the states, not the paths.
|
|
641
687
|
])
|
|
642
688
|
|
|
643
689
|
const shipped = query(Orders).rule((r) => {
|
|
644
|
-
const
|
|
690
|
+
const id = r.var("id")
|
|
691
|
+
const carrier = r.var("carrier")
|
|
645
692
|
return r
|
|
646
|
-
.match(Order, { id, state:
|
|
693
|
+
.match(Order, { id, state: "Shipped" })
|
|
647
694
|
.match(Shipment, { order: id, carrier })
|
|
648
695
|
.select("id", "carrier")
|
|
649
696
|
})
|
|
@@ -701,24 +748,26 @@ const Calendar = schema("Calendar", { Rsvp, Arm, Person, Room, Event, Attendance
|
|
|
701
748
|
// not declared. Policy is the presence or absence of one statement.
|
|
702
749
|
// Accepting an invitation IS claiming the time (totality + validity) —
|
|
703
750
|
// and this is the statement that types Claim.source:
|
|
704
|
-
mirrors(on(Attendance.where({ rsvp:
|
|
751
|
+
mirrors(on(Attendance.where({ rsvp: "Accepted" }), "id"), on(Claim.where({ arm: "Busy" }), "source")),
|
|
705
752
|
// Busy time lies inside working hours, pointwise — coverage rides the
|
|
706
753
|
// target's own key (disjoint + ordered is a theorem, not a request):
|
|
707
754
|
key(WorkHours, ["person", "hours"]),
|
|
708
|
-
contained(on(Claim.where({ arm:
|
|
755
|
+
contained(on(Claim.where({ arm: "Busy" }), ["person", "span"]), on(WorkHours, ["person", "hours"])),
|
|
709
756
|
contained(on(Booking, "room"), on(Room, "id")),
|
|
710
757
|
contained(on(Booking, "event"), on(Event, "id"))
|
|
711
758
|
])
|
|
712
759
|
|
|
713
760
|
const roomConflicts = query(Calendar).rule((r) => {
|
|
714
|
-
const
|
|
761
|
+
const room = r.var("room")
|
|
762
|
+
const span = r.var("span")
|
|
715
763
|
return r
|
|
716
764
|
.match(Booking, { room, span })
|
|
717
765
|
.where(allen(span, ALLEN.intersects, r.param("want")))
|
|
718
766
|
.select("room", "span")
|
|
719
767
|
})
|
|
720
768
|
const personLoad = query(Calendar).rule((r) => {
|
|
721
|
-
const
|
|
769
|
+
const person = r.var("person")
|
|
770
|
+
const span = r.var("span")
|
|
722
771
|
return r
|
|
723
772
|
.match(Claim, { person, span })
|
|
724
773
|
.where(allen(span, ALLEN.intersects, r.param("window")))
|
|
@@ -754,7 +803,8 @@ const Pricing = schema("Pricing", { Policy, Version }, [
|
|
|
754
803
|
|
|
755
804
|
// in force on date t — one membership probe:
|
|
756
805
|
const inForce = query(Pricing).rule((r) => {
|
|
757
|
-
const
|
|
806
|
+
const rate_bps = r.var("rate_bps")
|
|
807
|
+
const valid = r.var("valid")
|
|
758
808
|
return r
|
|
759
809
|
.match(Version, { policy: r.param("p"), rate_bps, valid })
|
|
760
810
|
.where(pointIn(r.param("t"), valid))
|
|
@@ -762,7 +812,9 @@ const inForce = query(Pricing).rule((r) => {
|
|
|
762
812
|
})
|
|
763
813
|
// clean successions (half-open makes MEETS exact, no ±1 fudge):
|
|
764
814
|
const successions = query(Pricing).rule((r) => {
|
|
765
|
-
const
|
|
815
|
+
const p = r.var("p")
|
|
816
|
+
const a = r.var("a")
|
|
817
|
+
const b = r.var("b")
|
|
766
818
|
return r
|
|
767
819
|
.match(Version, { policy: p, valid: a })
|
|
768
820
|
.match(Version, { policy: p, valid: b })
|
|
@@ -796,7 +848,8 @@ const Payroll = schema("Payroll", { FiscalYear, PayPeriod }, [
|
|
|
796
848
|
|
|
797
849
|
// the period holding date t:
|
|
798
850
|
const holding = query(Payroll).rule((r) => {
|
|
799
|
-
const
|
|
851
|
+
const seq = r.var("seq")
|
|
852
|
+
const span = r.var("span")
|
|
800
853
|
return r
|
|
801
854
|
.match(PayPeriod, { year: r.param("y"), seq, span })
|
|
802
855
|
.where(pointIn(r.param("t"), span))
|
|
@@ -844,7 +897,9 @@ const Tax = schema("Tax", { Status, Regime, Bracket, Residency, Earned }, [
|
|
|
844
897
|
// owed is host arithmetic over the bracket walk — arithmetic beyond the
|
|
845
898
|
// measure is refused (the ledger).
|
|
846
899
|
const marginal = query(Tax).rule((r) => {
|
|
847
|
-
const
|
|
900
|
+
const reg = r.var("reg")
|
|
901
|
+
const b = r.var("b")
|
|
902
|
+
const rate_bps = r.var("rate_bps")
|
|
848
903
|
return r
|
|
849
904
|
.match(Regime, { id: reg, year: r.param("y"), status: r.param("s") })
|
|
850
905
|
.match(Bracket, { regime: reg, income: b, rate_bps })
|
|
@@ -876,17 +931,21 @@ const FreeTime = schema("FreeTime", { Person, Claim }, [
|
|
|
876
931
|
|
|
877
932
|
// busy time, coalesced (adjacent segments merge — the half-open law):
|
|
878
933
|
const busy = query(FreeTime).rule((r) => {
|
|
879
|
-
const
|
|
934
|
+
const person = r.var("person")
|
|
935
|
+
const span = r.var("span")
|
|
880
936
|
return r.match(Claim, { person, span }).select("person", r.pack("span"))
|
|
881
937
|
})
|
|
882
938
|
// raw claimed time (overlaps double-count — often the wrong question):
|
|
883
939
|
const claimed = query(FreeTime).rule((r) => {
|
|
884
|
-
const
|
|
940
|
+
const person = r.var("person")
|
|
941
|
+
const span = r.var("span")
|
|
885
942
|
return r.match(Claim, { person, span }).select("person", r.sum(r.duration("span")))
|
|
886
943
|
})
|
|
887
944
|
// Coalesced totals = the two-query composition (pack, then a host fold) —
|
|
888
945
|
// aggregates never nest; free time (gaps) is the two-line host walk over
|
|
889
|
-
// sorted packed answers
|
|
946
|
+
// sorted packed answers (`rows.sort(by("person", "span"))` — the
|
|
947
|
+
// keys-as-data comparator; limit is the language's own `.slice`) — both
|
|
948
|
+
// refusals recorded in the ledger.
|
|
890
949
|
```
|
|
891
950
|
|
|
892
951
|
## The write side
|
|
@@ -920,12 +979,16 @@ const Ledger = schema("Ledger", { Account, JournalEntry, Posting }, [
|
|
|
920
979
|
|
|
921
980
|
// balances (bind the fresh id — set semantics collapses duplicates):
|
|
922
981
|
const balances = query(Ledger).rule((r) => {
|
|
923
|
-
const
|
|
982
|
+
const id = r.var("id")
|
|
983
|
+
const account = r.var("account")
|
|
984
|
+
const minor = r.var("minor")
|
|
924
985
|
return r.match(Posting, { id, account, minor }).select("account", r.sum("minor"))
|
|
925
986
|
})
|
|
926
987
|
// double-entry audit (host asserts every total is 0 — discipline, not schema):
|
|
927
988
|
const doubleEntry = query(Ledger).rule((r) => {
|
|
928
|
-
const
|
|
989
|
+
const id = r.var("id")
|
|
990
|
+
const entry = r.var("entry")
|
|
991
|
+
const minor = r.var("minor")
|
|
929
992
|
return r.match(Posting, { id, entry, minor }).select("entry", r.sum("minor"))
|
|
930
993
|
})
|
|
931
994
|
```
|
|
@@ -940,8 +1003,11 @@ final-state point reads need no earlier witness.
|
|
|
940
1003
|
The generation witness: read the model, propose a delta, commit iff the model
|
|
941
1004
|
you read is still the model. In the SDK the whole loop is `db.writeWitnessed`
|
|
942
1005
|
— retry on movement is built in (every generation move is self-inflicted by
|
|
943
|
-
the host's own interleaved writes),
|
|
944
|
-
|
|
1006
|
+
the host's own interleaved writes), capped at 64 attempts: a callback that
|
|
1007
|
+
itself issues a plain `db.write` each try re-moves the generation it is about
|
|
1008
|
+
to witness, and past the cap that interleave hazard throws the typed
|
|
1009
|
+
`ErrWitnessedLivelock` instead of spinning forever. `abandon(payload)`
|
|
1010
|
+
declines to commit without issuing anything.
|
|
945
1011
|
|
|
946
1012
|
```ts
|
|
947
1013
|
const State = closed("State", ["Queued", "Running", "Done"])
|
|
@@ -953,13 +1019,14 @@ const Jobs = schema("Jobs", { State, Job, Lease }, [
|
|
|
953
1019
|
key(Lease, ["job"]),
|
|
954
1020
|
// A lease exists iff its job is Running (recipe 13's conditional target):
|
|
955
1021
|
// claiming a job and leasing it commit together or not at all.
|
|
956
|
-
mirrors(on(Lease, "job"), on(Job.where({ state:
|
|
1022
|
+
mirrors(on(Lease, "job"), on(Job.where({ state: "Running" }), "id"))
|
|
957
1023
|
])
|
|
958
1024
|
|
|
959
1025
|
// update-where's premise — "still Queued" is the witness:
|
|
960
1026
|
const stillQueued = query(Jobs).rule((r) => {
|
|
961
|
-
const
|
|
962
|
-
|
|
1027
|
+
const id = r.var("id")
|
|
1028
|
+
const payload = r.var("payload")
|
|
1029
|
+
return r.match(Job, { id, state: "Queued", payload }).select("id", "payload")
|
|
963
1030
|
})
|
|
964
1031
|
|
|
965
1032
|
const db = await Db.create("./jobs.db", Jobs)
|
|
@@ -976,8 +1043,8 @@ const outcome = db.writeWitnessed(function updateWhere(snap, tx) {
|
|
|
976
1043
|
return abandon("nothing queued")
|
|
977
1044
|
}
|
|
978
1045
|
for (const row of queued) {
|
|
979
|
-
tx.delete(Job, { id: row.id, state:
|
|
980
|
-
tx.insert(Job, { id: row.id, state:
|
|
1046
|
+
tx.delete(Job, { id: row.id, state: "Queued", payload: row.payload })
|
|
1047
|
+
tx.insert(Job, { id: row.id, state: "Running", payload: row.payload })
|
|
981
1048
|
tx.insert(Lease, { job: row.id, worker: 7n, until: 60n })
|
|
982
1049
|
}
|
|
983
1050
|
return undefined
|
|
@@ -1007,7 +1074,7 @@ const Rollup = schema("Rollup", { Arm, Claim, BusySpan }, [
|
|
|
1007
1074
|
// Soundness, pointwise: every stored rollup point is covered by busy
|
|
1008
1075
|
// claims — an UNSOUND rollup (claiming busy time that isn't, or surviving
|
|
1009
1076
|
// its sources' deletion) cannot commit, judged on every touching commit.
|
|
1010
|
-
contained(on(BusySpan, ["person", "span"]), on(Claim.where({ arm:
|
|
1077
|
+
contained(on(BusySpan, ["person", "span"]), on(Claim.where({ arm: "Busy" }), ["person", "span"]))
|
|
1011
1078
|
])
|
|
1012
1079
|
|
|
1013
1080
|
// Maintenance is the third witness idiom (recipe 20): re-run the deriving
|
|
@@ -1015,8 +1082,9 @@ const Rollup = schema("Rollup", { Arm, Claim, BusySpan }, [
|
|
|
1015
1082
|
// against sources it didn't actually read. The deriving query (pack IS the
|
|
1016
1083
|
// coalesce):
|
|
1017
1084
|
const deriving = query(Rollup).rule((r) => {
|
|
1018
|
-
const
|
|
1019
|
-
|
|
1085
|
+
const person = r.var("person")
|
|
1086
|
+
const span = r.var("span")
|
|
1087
|
+
return r.match(Claim, { person, span, arm: "Busy" }).select("person", r.pack("span"))
|
|
1020
1088
|
})
|
|
1021
1089
|
```
|
|
1022
1090
|
|
|
@@ -1040,8 +1108,8 @@ const Payments = schema("Payments", { Kind, Payment, Card, Ach }, [
|
|
|
1040
1108
|
contained(on(Payment, "kind"), on(Kind, "id")),
|
|
1041
1109
|
key(Card, ["payment"]),
|
|
1042
1110
|
key(Ach, ["payment"]),
|
|
1043
|
-
mirrors(on(Payment.where({ kind:
|
|
1044
|
-
mirrors(on(Payment.where({ kind:
|
|
1111
|
+
mirrors(on(Payment.where({ kind: "Card" }), "id"), on(Card, "payment")),
|
|
1112
|
+
mirrors(on(Payment.where({ kind: "Ach" }), "id"), on(Ach, "payment"))
|
|
1045
1113
|
])
|
|
1046
1114
|
|
|
1047
1115
|
// One query, two rules (set union). The exclusivity theorem (recipe 2) is
|
|
@@ -1049,16 +1117,18 @@ const Payments = schema("Payments", { Kind, Payment, Card, Ach }, [
|
|
|
1049
1117
|
// provably disjoint, so the executor elides cross-rule dedup — the free lunch.
|
|
1050
1118
|
const wholeDu = query(Payments)
|
|
1051
1119
|
.rule((r) => {
|
|
1052
|
-
const
|
|
1120
|
+
const id = r.var("id")
|
|
1121
|
+
const n = r.var("n")
|
|
1053
1122
|
return r
|
|
1054
|
-
.match(Payment, { id, kind:
|
|
1123
|
+
.match(Payment, { id, kind: "Card" })
|
|
1055
1124
|
.match(Card, { payment: id, last4: n })
|
|
1056
1125
|
.select("id", "n")
|
|
1057
1126
|
})
|
|
1058
1127
|
.rule((r) => {
|
|
1059
|
-
const
|
|
1128
|
+
const id = r.var("id")
|
|
1129
|
+
const n = r.var("n")
|
|
1060
1130
|
return r
|
|
1061
|
-
.match(Payment, { id, kind:
|
|
1131
|
+
.match(Payment, { id, kind: "Ach" })
|
|
1062
1132
|
.match(Ach, { payment: id, routing: n })
|
|
1063
1133
|
.select("id", "n")
|
|
1064
1134
|
})
|
|
@@ -1128,7 +1198,7 @@ const Closure = schema("Closure", { Node, Parent }, [
|
|
|
1128
1198
|
|
|
1129
1199
|
// The loop's one query — the frontier's children, one ∈-set probe:
|
|
1130
1200
|
const step = query(Closure).rule((r) => {
|
|
1131
|
-
const
|
|
1201
|
+
const c = r.var("c")
|
|
1132
1202
|
return r.match(Parent, { child: c, parent: r.inSet("frontier") }).select("c")
|
|
1133
1203
|
})
|
|
1134
1204
|
```
|
|
@@ -1172,21 +1242,22 @@ const reach = program(Closure, (p) => {
|
|
|
1172
1242
|
const rec = p.rec("reach")
|
|
1173
1243
|
const seeded = rec
|
|
1174
1244
|
.rule((r) => {
|
|
1175
|
-
const
|
|
1245
|
+
const c = r.var("c")
|
|
1176
1246
|
return r
|
|
1177
1247
|
.match(Node, { id: c })
|
|
1178
1248
|
.where(eq(c, r.param("root")))
|
|
1179
1249
|
.select("c")
|
|
1180
1250
|
})
|
|
1181
1251
|
.rule((r) => {
|
|
1182
|
-
const
|
|
1252
|
+
const c = r.var("c")
|
|
1253
|
+
const parent = r.var("parent")
|
|
1183
1254
|
return r
|
|
1184
1255
|
.match(Parent, { child: c, parent })
|
|
1185
1256
|
.idb(rec, parent)
|
|
1186
1257
|
.select("c")
|
|
1187
1258
|
})
|
|
1188
1259
|
return p.output((r) => {
|
|
1189
|
-
const
|
|
1260
|
+
const c = r.var("c")
|
|
1190
1261
|
return r.match(Node, { id: c }).idb(seeded, c).select("c")
|
|
1191
1262
|
})
|
|
1192
1263
|
})
|
|
@@ -1229,13 +1300,14 @@ const Accounts = schema("Accounts", { Account, AccountParent, Posting }, [
|
|
|
1229
1300
|
// The two queries the host rollup composes:
|
|
1230
1301
|
// the frontier step (recipe 24's loop, verbatim):
|
|
1231
1302
|
const frontierStep = query(Accounts).rule((r) => {
|
|
1232
|
-
const
|
|
1303
|
+
const c = r.var("c")
|
|
1233
1304
|
return r.match(AccountParent, { child: c, parent: r.inSet("frontier") }).select("c")
|
|
1234
1305
|
})
|
|
1235
1306
|
// the rollup over the accumulated subtree (bind the fresh id — recipe
|
|
1236
1307
|
// 19's discipline, spent again; equal postings to one account both count):
|
|
1237
1308
|
const subtreeRollup = query(Accounts).rule((r) => {
|
|
1238
|
-
const
|
|
1309
|
+
const id = r.var("id")
|
|
1310
|
+
const minor = r.var("minor")
|
|
1239
1311
|
return r.match(Posting, { id, account: r.inSet("subtree"), minor }).select(r.sum("minor"))
|
|
1240
1312
|
})
|
|
1241
1313
|
// The engine-native form: the closure stratum converges first, then the
|
|
@@ -1244,21 +1316,24 @@ const nativeRollup = program(Accounts, (p) => {
|
|
|
1244
1316
|
const sub = p.rec("sub")
|
|
1245
1317
|
const seeded = sub
|
|
1246
1318
|
.rule((r) => {
|
|
1247
|
-
const
|
|
1319
|
+
const a = r.var("a")
|
|
1248
1320
|
return r
|
|
1249
1321
|
.match(Account, { id: a })
|
|
1250
1322
|
.where(eq(a, r.param("root")))
|
|
1251
1323
|
.select("a")
|
|
1252
1324
|
})
|
|
1253
1325
|
.rule((r) => {
|
|
1254
|
-
const
|
|
1326
|
+
const a = r.var("a")
|
|
1327
|
+
const parent = r.var("parent")
|
|
1255
1328
|
return r
|
|
1256
1329
|
.match(AccountParent, { child: a, parent })
|
|
1257
1330
|
.idb(sub, parent)
|
|
1258
1331
|
.select("a")
|
|
1259
1332
|
})
|
|
1260
1333
|
return p.output((r) => {
|
|
1261
|
-
const
|
|
1334
|
+
const id = r.var("id")
|
|
1335
|
+
const a = r.var("a")
|
|
1336
|
+
const minor = r.var("minor")
|
|
1262
1337
|
return r
|
|
1263
1338
|
.match(Posting, { id, account: a, minor })
|
|
1264
1339
|
.idb(seeded, a)
|
|
@@ -1323,13 +1398,15 @@ const MaintainedRollup = schema("MaintainedRollup", { Arm, Claim, BusySpan }, [
|
|
|
1323
1398
|
key(Claim, ["source"]),
|
|
1324
1399
|
key(Claim, ["person", "span"]),
|
|
1325
1400
|
key(BusySpan, ["person", "span"]),
|
|
1326
|
-
contained(on(BusySpan, ["person", "span"]), on(Claim.where({ arm:
|
|
1401
|
+
contained(on(BusySpan, ["person", "span"]), on(Claim.where({ arm: "Busy" }), ["person", "span"]))
|
|
1327
1402
|
])
|
|
1328
1403
|
|
|
1329
1404
|
// Derive the desired rollup on the maintenance snapshot:
|
|
1330
1405
|
const deriving = query(MaintainedRollup).rule((r) => {
|
|
1331
|
-
const
|
|
1332
|
-
|
|
1406
|
+
const source = r.var("source")
|
|
1407
|
+
const person = r.var("person")
|
|
1408
|
+
const span = r.var("span")
|
|
1409
|
+
return r.match(Claim, { source, person, arm: "Busy", span }).select("person", r.pack("span"))
|
|
1333
1410
|
})
|
|
1334
1411
|
```
|
|
1335
1412
|
|
|
@@ -1382,7 +1459,10 @@ const Payroll = schema("Payroll", { Employee, Salary }, [
|
|
|
1382
1459
|
|
|
1383
1460
|
// The post-migration read — salaries in force at an instant:
|
|
1384
1461
|
const inForceAt = query(Payroll).rule((r) => {
|
|
1385
|
-
const
|
|
1462
|
+
const e = r.var("e")
|
|
1463
|
+
const name = r.var("name")
|
|
1464
|
+
const amount = r.var("amount")
|
|
1465
|
+
const w = r.var("w")
|
|
1386
1466
|
return r
|
|
1387
1467
|
.match(Employee, { id: e, name })
|
|
1388
1468
|
.match(Salary, { employee: e, amount, applies: w })
|
|
@@ -1437,8 +1517,8 @@ const ZoneLedger = schema("ZoneLedger", { Kind, Ledger, Zone, UnitSlot, PairSlot
|
|
|
1437
1517
|
key(PairSlot, ["ledger", "at"]),
|
|
1438
1518
|
// Each kind's zones carry exactly its sidecar's points — mixed widths,
|
|
1439
1519
|
// one element domain:
|
|
1440
|
-
mirrors(on(Zone.where({ kind:
|
|
1441
|
-
mirrors(on(Zone.where({ kind:
|
|
1520
|
+
mirrors(on(Zone.where({ kind: "Unit" }), ["ledger", "at"]), on(UnitSlot, ["ledger", "at"])),
|
|
1521
|
+
mirrors(on(Zone.where({ kind: "Pair" }), ["ledger", "at"]), on(PairSlot, ["ledger", "at"]))
|
|
1442
1522
|
])
|
|
1443
1523
|
```
|
|
1444
1524
|
|
|
@@ -1448,3 +1528,77 @@ unit slots `[4,5)`, `[5,6)` satisfies both directions, because nothing forces
|
|
|
1448
1528
|
the witness rows to mirror the sidecar's segmentation — only its points. If
|
|
1449
1529
|
per-row correspondence matters, the host writes zones at slot granularity;
|
|
1450
1530
|
the schema proves disjointness and coverage either way.
|
|
1531
|
+
|
|
1532
|
+
## Point reads
|
|
1533
|
+
|
|
1534
|
+
## 30. The keyed read
|
|
1535
|
+
|
|
1536
|
+
Guarantee: validator/runtime premises — a declared key FD admits at most one
|
|
1537
|
+
fact per determinant tuple (the key phase of the commit judgment), and every
|
|
1538
|
+
keyed point read answers exactly that fact or nothing, on every scope
|
|
1539
|
+
(`ts/test/keyed-get.test.ts`; the engine half is
|
|
1540
|
+
`crates/bumbledb/tests/keyed_get.rs`).
|
|
1541
|
+
|
|
1542
|
+
The key is a **law**, and the read surface is that law made callable. The
|
|
1543
|
+
schema says `key(Program, ["grp"])` — one program per group — so "the
|
|
1544
|
+
program of a group" is a well-posed question with at most one answer, and
|
|
1545
|
+
the store already enforces that on every commit. Hold the statement VALUE:
|
|
1546
|
+
it is the read's selector below (statement identity is the membership rule).
|
|
1547
|
+
|
|
1548
|
+
```ts
|
|
1549
|
+
const Grp = relation("Grp", { id: u64.fresh, label: str })
|
|
1550
|
+
const Program = relation("Program", { id: u64.fresh, grp: u64, title: str })
|
|
1551
|
+
// The law: one program per group — the callable key.
|
|
1552
|
+
const programGrpKey = key(Program, ["grp"])
|
|
1553
|
+
|
|
1554
|
+
const KeyedRead = schema("KeyedRead", { Grp, Program }, [
|
|
1555
|
+
contained(on(Program, "grp"), on(Grp, "id")),
|
|
1556
|
+
programGrpKey
|
|
1557
|
+
])
|
|
1558
|
+
```
|
|
1559
|
+
|
|
1560
|
+
The point read is the statement handed back to `get` — one spelling on
|
|
1561
|
+
every scope (the symmetry rule): `db.get` standalone, `snap.get` inside a
|
|
1562
|
+
read scope, `tx.get` inside a write transaction, where the transaction side
|
|
1563
|
+
answers the FINAL state (base plus pending delta: read-your-writes, a
|
|
1564
|
+
pending delete answers `undefined`). The key object is typed by the
|
|
1565
|
+
statement's own projection — a wrong field name is a compile error, never a
|
|
1566
|
+
runtime shape check. The primary 2-arg form needs no statement: the fresh
|
|
1567
|
+
field IS the primary key.
|
|
1568
|
+
|
|
1569
|
+
```ts
|
|
1570
|
+
const db = await Db.create("./programs.db", KeyedRead)
|
|
1571
|
+
|
|
1572
|
+
const minted: { grp?: bigint } = {}
|
|
1573
|
+
db.write((tx) => {
|
|
1574
|
+
const g = tx.insert(Grp, { label: "algebra" })
|
|
1575
|
+
tx.insert(Program, { grp: g.id, title: "linear equations" })
|
|
1576
|
+
minted.grp = g.id
|
|
1577
|
+
})
|
|
1578
|
+
const grp = minted.grp ?? 0n
|
|
1579
|
+
|
|
1580
|
+
// db.get — the standalone keyed read through the declared law:
|
|
1581
|
+
const byGroup = db.get(Program, programGrpKey, { grp })
|
|
1582
|
+
|
|
1583
|
+
// snap.get — the same spelling inside a read scope:
|
|
1584
|
+
const viaSnap = db.read((snap) => snap.get(Program, programGrpKey, { grp }))
|
|
1585
|
+
|
|
1586
|
+
// tx.get — key-shaped read-modify-write, final-state (recipe 20's third
|
|
1587
|
+
// idiom): per-fact premises need no earlier snapshot witness.
|
|
1588
|
+
db.write((tx) => {
|
|
1589
|
+
const current = tx.get(Program, programGrpKey, { grp })
|
|
1590
|
+
if (current !== undefined) {
|
|
1591
|
+
tx.delete(Program, current)
|
|
1592
|
+
tx.insert(Program, { id: current.id, grp: current.grp, title: "linear equations II" })
|
|
1593
|
+
}
|
|
1594
|
+
})
|
|
1595
|
+
|
|
1596
|
+
// The primary 2-arg form — the fresh field is the primary key:
|
|
1597
|
+
const byId = byGroup === undefined ? undefined : db.get(Program, { id: byGroup.id })
|
|
1598
|
+
```
|
|
1599
|
+
|
|
1600
|
+
The anti-pattern this recipe retires: a scan-and-find where a key law
|
|
1601
|
+
exists — `snap.scan(Program).find((row) => row.grp === grp)` — re-derives
|
|
1602
|
+
in the host what the store already enforces. The uniqueness the fold
|
|
1603
|
+
quietly assumes IS the declared key statement; spell the law and the point
|
|
1604
|
+
read comes with it.
|