@bjornpagen/bumbledb 0.4.0 → 0.6.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 +191 -92
- package/README.md +9 -7
- package/dist/closed.d.ts +30 -2
- package/dist/closed.d.ts.map +1 -1
- package/dist/closed.js +66 -20
- package/dist/closed.js.map +1 -1
- package/dist/db.d.ts +33 -6
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +89 -72
- 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 +1 -1
- package/dist/face.d.ts.map +1 -1
- package/dist/face.js +2 -1
- package/dist/face.js.map +1 -1
- package/dist/fields.d.ts +18 -1
- package/dist/fields.d.ts.map +1 -1
- package/dist/fields.js +44 -16
- package/dist/fields.js.map +1 -1
- package/dist/index.d.ts +18 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -6
- 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.map +1 -1
- package/dist/marshal.js +15 -27
- 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 +140 -206
- package/dist/query/atom.d.ts.map +1 -1
- package/dist/query/atom.js +33 -52
- package/dist/query/atom.js.map +1 -1
- package/dist/query/find.d.ts +116 -0
- package/dist/query/find.d.ts.map +1 -0
- package/dist/query/{select.js → find.js} +22 -22
- package/dist/query/find.js.map +1 -0
- package/dist/query/lower.d.ts +124 -162
- package/dist/query/lower.d.ts.map +1 -1
- package/dist/query/lower.js +462 -507
- package/dist/query/lower.js.map +1 -1
- package/dist/query/predicate.d.ts +22 -14
- package/dist/query/predicate.d.ts.map +1 -1
- package/dist/query/predicate.js +45 -20
- package/dist/query/predicate.js.map +1 -1
- package/dist/query/run.d.ts +3 -3
- package/dist/query/run.d.ts.map +1 -1
- package/dist/query/run.js +10 -11
- package/dist/query/run.js.map +1 -1
- package/dist/query/scope.d.ts +124 -83
- package/dist/query/scope.d.ts.map +1 -1
- package/dist/query/scope.js +77 -70
- package/dist/query/scope.js.map +1 -1
- package/dist/relation.d.ts +9 -22
- package/dist/relation.d.ts.map +1 -1
- package/dist/relation.js +9 -28
- 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/statements.d.ts +5 -2
- package/dist/statements.d.ts.map +1 -1
- package/dist/statements.js +30 -35
- package/dist/statements.js.map +1 -1
- package/package.json +2 -5
- package/src/closed.ts +73 -28
- package/src/db.ts +126 -88
- package/src/exhume.ts +1 -15
- package/src/face.ts +4 -3
- package/src/fields.ts +58 -16
- package/src/index.ts +19 -13
- package/src/law.ts +15 -14
- package/src/lower.ts +2 -9
- package/src/marshal.ts +15 -31
- package/src/native.ts +22 -4
- package/src/order.ts +156 -0
- package/src/query/atom.ts +186 -276
- package/src/query/find.ts +212 -0
- package/src/query/lower.ts +614 -749
- package/src/query/predicate.ts +47 -20
- package/src/query/run.ts +11 -13
- package/src/query/scope.ts +165 -132
- package/src/relation.ts +9 -51
- package/src/schema.ts +7 -33
- package/src/statements.ts +33 -38
- package/dist/query/select.d.ts +0 -128
- package/dist/query/select.d.ts.map +0 -1
- package/dist/query/select.js.map +0 -1
- package/src/query/select.ts +0 -215
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
|
|
@@ -37,9 +37,11 @@ import {
|
|
|
37
37
|
abandon,
|
|
38
38
|
allen,
|
|
39
39
|
bool,
|
|
40
|
+
by,
|
|
40
41
|
bytes,
|
|
41
42
|
closed,
|
|
42
43
|
contained,
|
|
44
|
+
desc,
|
|
43
45
|
eq,
|
|
44
46
|
i64,
|
|
45
47
|
interval,
|
|
@@ -54,7 +56,8 @@ import {
|
|
|
54
56
|
relation,
|
|
55
57
|
schema,
|
|
56
58
|
str,
|
|
57
|
-
u64
|
|
59
|
+
u64,
|
|
60
|
+
v
|
|
58
61
|
} from "@bjornpagen/bumbledb"
|
|
59
62
|
```
|
|
60
63
|
|
|
@@ -116,27 +119,27 @@ const Uptime = schema("Uptime", { Service, Outage }, [
|
|
|
116
119
|
key(Outage, ["service", "window"])
|
|
117
120
|
])
|
|
118
121
|
|
|
119
|
-
// down at instant t —
|
|
120
|
-
//
|
|
122
|
+
// down at instant t — v(Outage) mints a fresh variable per column (typed by
|
|
123
|
+
// that column's law-class); destructure what you bind, reuse to join:
|
|
121
124
|
const downAt = query(Uptime).rule((r) => {
|
|
122
|
-
const { service, window } =
|
|
125
|
+
const { service, window } = v(Outage)
|
|
123
126
|
return r
|
|
124
127
|
.match(Outage, { service, window })
|
|
125
128
|
.where(pointIn(r.param("t"), window))
|
|
126
|
-
.
|
|
129
|
+
.find({ service })
|
|
127
130
|
})
|
|
128
131
|
// overlapping an incident window (one Allen mask, no operator zoo):
|
|
129
132
|
const overlapping = query(Uptime).rule((r) => {
|
|
130
|
-
const { service, window } =
|
|
133
|
+
const { service, window } = v(Outage)
|
|
131
134
|
return r
|
|
132
135
|
.match(Outage, { service, window })
|
|
133
136
|
.where(allen(window, ALLEN.intersects, r.param("incident")))
|
|
134
|
-
.
|
|
137
|
+
.find({ service, window })
|
|
135
138
|
})
|
|
136
139
|
// total downtime per service (the denotation's one arithmetic):
|
|
137
140
|
const downtime = query(Uptime).rule((r) => {
|
|
138
|
-
const { service, window } =
|
|
139
|
-
return r.match(Outage, { service, window }).
|
|
141
|
+
const { service, window } = v(Outage)
|
|
142
|
+
return r.match(Outage, { service, window }).find({ service, downtime: r.sum(r.duration(window)) })
|
|
140
143
|
})
|
|
141
144
|
```
|
|
142
145
|
|
|
@@ -215,11 +218,11 @@ const Optionality = schema("Optionality", { Business, MailingAddress }, [
|
|
|
215
218
|
|
|
216
219
|
// Negation is plain anti-join (no null branch exists in any operator):
|
|
217
220
|
const unaddressed = query(Optionality).rule((r) => {
|
|
218
|
-
const { b } =
|
|
221
|
+
const { id: b } = v(Business)
|
|
219
222
|
return r
|
|
220
223
|
.match(Business, { id: b })
|
|
221
224
|
.where(not(MailingAddress, { business: b }))
|
|
222
|
-
.
|
|
225
|
+
.find({ b })
|
|
223
226
|
})
|
|
224
227
|
```
|
|
225
228
|
|
|
@@ -255,8 +258,8 @@ const Money = schema("Money", { Currency, Account, Posting }, [
|
|
|
255
258
|
// silently. Bind the fresh id: set semantics would collapse two equal
|
|
256
259
|
// (account, currency, minor) postings without it.
|
|
257
260
|
const totals = query(Money).rule((r) => {
|
|
258
|
-
const { id, account, currency, minor } =
|
|
259
|
-
return r.match(Posting, { id, account, currency, minor }).
|
|
261
|
+
const { id, account, currency, minor } = v(Posting)
|
|
262
|
+
return r.match(Posting, { id, account, currency, minor }).find({ account, currency, total: r.sum(minor) })
|
|
260
263
|
})
|
|
261
264
|
```
|
|
262
265
|
|
|
@@ -292,8 +295,8 @@ const Content = schema("Content", { Region, Document, Replica }, [
|
|
|
292
295
|
|
|
293
296
|
// a bytes param self-encodes (Uint8Array by inference):
|
|
294
297
|
const byDigest = query(Content).rule((r) => {
|
|
295
|
-
const { id } =
|
|
296
|
-
return r.match(Document, { id, payload: r.param("digest") }).
|
|
298
|
+
const { id } = v(Document)
|
|
299
|
+
return r.match(Document, { id, payload: r.param("digest") }).find({ id })
|
|
297
300
|
})
|
|
298
301
|
```
|
|
299
302
|
|
|
@@ -334,8 +337,8 @@ const Tickets = schema("Tickets", { Priority, Ticket }, [
|
|
|
334
337
|
// that drifts without a rebuild is an ordinary relation — a vocabulary is
|
|
335
338
|
// never written, only declared.
|
|
336
339
|
const urgent = query(Tickets).rule((r) => {
|
|
337
|
-
const { t } =
|
|
338
|
-
return r.match(Ticket, { id: t, priority: "Urgent" }).
|
|
340
|
+
const { id: t } = v(Ticket)
|
|
341
|
+
return r.match(Ticket, { id: t, priority: "Urgent" }).find({ t })
|
|
339
342
|
})
|
|
340
343
|
|
|
341
344
|
// Set membership is a plain array — the drizzle law's spelling, closed-only
|
|
@@ -343,8 +346,8 @@ const urgent = query(Tickets).rule((r) => {
|
|
|
343
346
|
// ∈-set param, `r.inSet`); the array folds to the same wire set the param
|
|
344
347
|
// spelling crosses. In `.where()` selections arrays work at EVERY field kind.
|
|
345
348
|
const actionable = query(Tickets).rule((r) => {
|
|
346
|
-
const { t } =
|
|
347
|
-
return r.match(Ticket, { id: t, priority: ["Normal", "Urgent"] }).
|
|
349
|
+
const { id: t } = v(Ticket)
|
|
350
|
+
return r.match(Ticket, { id: t, priority: ["Normal", "Urgent"] }).find({ t })
|
|
348
351
|
})
|
|
349
352
|
```
|
|
350
353
|
|
|
@@ -388,11 +391,11 @@ const Review = schema("Review", { Kind, Attempt, Certificate }, [
|
|
|
388
391
|
// exactly like an ordinary one, and the atom folds at prepare into a
|
|
389
392
|
// plan-constant handle set on its sibling.
|
|
390
393
|
const masteredAttempts = query(Review).rule((r) => {
|
|
391
|
-
const { a, k } =
|
|
394
|
+
const { id: a, kind: k } = v(Attempt)
|
|
392
395
|
return r
|
|
393
396
|
.match(Attempt, { id: a, kind: k })
|
|
394
397
|
.match(Kind, { id: k, mastered: true })
|
|
395
|
-
.
|
|
398
|
+
.find({ a })
|
|
396
399
|
})
|
|
397
400
|
|
|
398
401
|
// Host dispatch on the payload tier is the record-table idiom — a `Record`
|
|
@@ -451,11 +454,11 @@ const Oncall = schema("Oncall", { Severity, Incident, Escalation }, [
|
|
|
451
454
|
|
|
452
455
|
// who is being paged — the same ψ, on the read side:
|
|
453
456
|
const paged = query(Oncall).rule((r) => {
|
|
454
|
-
const { i, s } =
|
|
457
|
+
const { incident: i, severity: s } = v(Escalation)
|
|
455
458
|
return r
|
|
456
459
|
.match(Escalation, { incident: i, severity: s })
|
|
457
460
|
.match(Severity, { id: s, pages: true })
|
|
458
|
-
.
|
|
461
|
+
.find({ i })
|
|
459
462
|
})
|
|
460
463
|
```
|
|
461
464
|
|
|
@@ -468,7 +471,8 @@ plus pointwise keys realizes exact partition
|
|
|
468
471
|
(`lean/Bumbledb/Dependencies.lean: exact_partition_iff`), and the mixed-width
|
|
469
472
|
interval positions type by element domain
|
|
470
473
|
(`lean/Bumbledb/Schema.lean: Value.points_one_tag_u64`); ordering the result
|
|
471
|
-
remains a host presentation step
|
|
474
|
+
remains a host presentation step — the SDK ships the comparator (`by`/`desc`,
|
|
475
|
+
keys as data); the engine never orders.
|
|
472
476
|
|
|
473
477
|
The linked-list verdict: successor pointers are control flow smuggled into
|
|
474
478
|
data. Order is a value. The idiomatic ordered collection is an interval
|
|
@@ -496,12 +500,20 @@ const Playlists = schema("Playlists", { Playlist, Extent, Slot }, [
|
|
|
496
500
|
|
|
497
501
|
// Positional access is membership — "what plays at position ?pos":
|
|
498
502
|
const playingAt = query(Playlists).rule((r) => {
|
|
499
|
-
const { slot, track } =
|
|
503
|
+
const { slot, track } = v(Slot)
|
|
500
504
|
return r
|
|
501
505
|
.match(Slot, { playlist: r.param("list"), slot, track })
|
|
502
506
|
.where(pointIn(r.param("pos"), slot))
|
|
503
|
-
.
|
|
507
|
+
.find({ track })
|
|
504
508
|
})
|
|
509
|
+
|
|
510
|
+
// Answers are SETS — the host sorts them, and the SDK ships the comparator:
|
|
511
|
+
// sort keys as data, a bare name ascending, `desc(...)` the flip; intervals
|
|
512
|
+
// order by (start, end). Limit is the language's own `.slice(0, n)`.
|
|
513
|
+
const inPlayOrder = [
|
|
514
|
+
{ slot: { start: 1n, end: 2n }, track: "b" },
|
|
515
|
+
{ slot: { start: 0n, end: 1n }, track: "a" }
|
|
516
|
+
].sort(by("slot", "track"))
|
|
505
517
|
```
|
|
506
518
|
|
|
507
519
|
Middle insert is honest about its cost: making room at position `k` shifts
|
|
@@ -548,11 +560,12 @@ const Ast = schema("Ast", { Kind, Node, Lit, Add, Parent }, [
|
|
|
548
560
|
])
|
|
549
561
|
|
|
550
562
|
const lhsLiteral = query(Ast).rule((r) => {
|
|
551
|
-
const { l
|
|
563
|
+
const { lhs: l } = v(Add)
|
|
564
|
+
const { value } = v(Lit)
|
|
552
565
|
return r
|
|
553
566
|
.match(Add, { node: r.param("n"), lhs: l })
|
|
554
|
-
.match(Lit, { node: l, value
|
|
555
|
-
.
|
|
567
|
+
.match(Lit, { node: l, value })
|
|
568
|
+
.find({ value })
|
|
556
569
|
})
|
|
557
570
|
```
|
|
558
571
|
|
|
@@ -583,12 +596,12 @@ const Graph = schema("Graph", { Person, Repo, Follows, Maintains }, [
|
|
|
583
596
|
// live in the "Person.id" class, so the reuse is lawful); `lt` keeps each
|
|
584
597
|
// pair once:
|
|
585
598
|
const mutual = query(Graph).rule((r) => {
|
|
586
|
-
const { a, b } =
|
|
599
|
+
const { follower: a, followee: b } = v(Follows)
|
|
587
600
|
return r
|
|
588
601
|
.match(Follows, { follower: a, followee: b })
|
|
589
602
|
.match(Follows, { follower: b, followee: a })
|
|
590
603
|
.where(lt(a, b))
|
|
591
|
-
.
|
|
604
|
+
.find({ a, b })
|
|
592
605
|
})
|
|
593
606
|
```
|
|
594
607
|
|
|
@@ -621,11 +634,12 @@ const Ecs = schema("Ecs", { Entity, Transform, Velocity, Renderable }, [
|
|
|
621
634
|
|
|
622
635
|
// The physics join is the component intersection:
|
|
623
636
|
const physics = query(Ecs).rule((r) => {
|
|
624
|
-
const { entity, x, y
|
|
637
|
+
const { entity, x, y } = v(Transform)
|
|
638
|
+
const { dx, dy } = v(Velocity)
|
|
625
639
|
return r
|
|
626
640
|
.match(Transform, { entity, x, y })
|
|
627
641
|
.match(Velocity, { entity, dx, dy })
|
|
628
|
-
.
|
|
642
|
+
.find({ entity, x, y, dx, dy })
|
|
629
643
|
})
|
|
630
644
|
```
|
|
631
645
|
|
|
@@ -661,11 +675,12 @@ const Orders = schema("Orders", { State, Order, Placement, Shipment }, [
|
|
|
661
675
|
])
|
|
662
676
|
|
|
663
677
|
const shipped = query(Orders).rule((r) => {
|
|
664
|
-
const { id
|
|
678
|
+
const { id } = v(Order)
|
|
679
|
+
const { carrier } = v(Shipment)
|
|
665
680
|
return r
|
|
666
681
|
.match(Order, { id, state: "Shipped" })
|
|
667
682
|
.match(Shipment, { order: id, carrier })
|
|
668
|
-
.
|
|
683
|
+
.find({ id, carrier })
|
|
669
684
|
})
|
|
670
685
|
```
|
|
671
686
|
|
|
@@ -731,18 +746,18 @@ const Calendar = schema("Calendar", { Rsvp, Arm, Person, Room, Event, Attendance
|
|
|
731
746
|
])
|
|
732
747
|
|
|
733
748
|
const roomConflicts = query(Calendar).rule((r) => {
|
|
734
|
-
const { room, span } =
|
|
749
|
+
const { room, span } = v(Booking)
|
|
735
750
|
return r
|
|
736
751
|
.match(Booking, { room, span })
|
|
737
752
|
.where(allen(span, ALLEN.intersects, r.param("want")))
|
|
738
|
-
.
|
|
753
|
+
.find({ room, span })
|
|
739
754
|
})
|
|
740
755
|
const personLoad = query(Calendar).rule((r) => {
|
|
741
|
-
const { person, span } =
|
|
756
|
+
const { person, span } = v(Claim)
|
|
742
757
|
return r
|
|
743
758
|
.match(Claim, { person, span })
|
|
744
759
|
.where(allen(span, ALLEN.intersects, r.param("window")))
|
|
745
|
-
.
|
|
760
|
+
.find({ person, span })
|
|
746
761
|
})
|
|
747
762
|
```
|
|
748
763
|
|
|
@@ -774,20 +789,21 @@ const Pricing = schema("Pricing", { Policy, Version }, [
|
|
|
774
789
|
|
|
775
790
|
// in force on date t — one membership probe:
|
|
776
791
|
const inForce = query(Pricing).rule((r) => {
|
|
777
|
-
const { rate_bps, valid } =
|
|
792
|
+
const { rate_bps, valid } = v(Version)
|
|
778
793
|
return r
|
|
779
794
|
.match(Version, { policy: r.param("p"), rate_bps, valid })
|
|
780
795
|
.where(pointIn(r.param("t"), valid))
|
|
781
|
-
.
|
|
796
|
+
.find({ rate_bps })
|
|
782
797
|
})
|
|
783
798
|
// clean successions (half-open makes MEETS exact, no ±1 fudge):
|
|
784
799
|
const successions = query(Pricing).rule((r) => {
|
|
785
|
-
const { p, a
|
|
800
|
+
const { policy: p, valid: a } = v(Version)
|
|
801
|
+
const { valid: b } = v(Version)
|
|
786
802
|
return r
|
|
787
803
|
.match(Version, { policy: p, valid: a })
|
|
788
804
|
.match(Version, { policy: p, valid: b })
|
|
789
805
|
.where(allen(a, ALLEN.meets, b))
|
|
790
|
-
.
|
|
806
|
+
.find({ a, b })
|
|
791
807
|
})
|
|
792
808
|
```
|
|
793
809
|
|
|
@@ -816,11 +832,11 @@ const Payroll = schema("Payroll", { FiscalYear, PayPeriod }, [
|
|
|
816
832
|
|
|
817
833
|
// the period holding date t:
|
|
818
834
|
const holding = query(Payroll).rule((r) => {
|
|
819
|
-
const { seq, span } =
|
|
835
|
+
const { seq, span } = v(PayPeriod)
|
|
820
836
|
return r
|
|
821
837
|
.match(PayPeriod, { year: r.param("y"), seq, span })
|
|
822
838
|
.where(pointIn(r.param("t"), span))
|
|
823
|
-
.
|
|
839
|
+
.find({ seq })
|
|
824
840
|
})
|
|
825
841
|
```
|
|
826
842
|
|
|
@@ -864,12 +880,13 @@ const Tax = schema("Tax", { Status, Regime, Bracket, Residency, Earned }, [
|
|
|
864
880
|
// owed is host arithmetic over the bracket walk — arithmetic beyond the
|
|
865
881
|
// measure is refused (the ledger).
|
|
866
882
|
const marginal = query(Tax).rule((r) => {
|
|
867
|
-
const { reg
|
|
883
|
+
const { id: reg } = v(Regime)
|
|
884
|
+
const { income: b, rate_bps } = v(Bracket)
|
|
868
885
|
return r
|
|
869
886
|
.match(Regime, { id: reg, year: r.param("y"), status: r.param("s") })
|
|
870
887
|
.match(Bracket, { regime: reg, income: b, rate_bps })
|
|
871
888
|
.where(pointIn(r.param("taxable"), b))
|
|
872
|
-
.
|
|
889
|
+
.find({ rate_bps })
|
|
873
890
|
})
|
|
874
891
|
```
|
|
875
892
|
|
|
@@ -896,17 +913,19 @@ const FreeTime = schema("FreeTime", { Person, Claim }, [
|
|
|
896
913
|
|
|
897
914
|
// busy time, coalesced (adjacent segments merge — the half-open law):
|
|
898
915
|
const busy = query(FreeTime).rule((r) => {
|
|
899
|
-
const { person, span } =
|
|
900
|
-
return r.match(Claim, { person, span }).
|
|
916
|
+
const { person, span } = v(Claim)
|
|
917
|
+
return r.match(Claim, { person, span }).find({ person, packed: r.pack(span) })
|
|
901
918
|
})
|
|
902
919
|
// raw claimed time (overlaps double-count — often the wrong question):
|
|
903
920
|
const claimed = query(FreeTime).rule((r) => {
|
|
904
|
-
const { person, span } =
|
|
905
|
-
return r.match(Claim, { person, span }).
|
|
921
|
+
const { person, span } = v(Claim)
|
|
922
|
+
return r.match(Claim, { person, span }).find({ person, claimed: r.sum(r.duration(span)) })
|
|
906
923
|
})
|
|
907
924
|
// Coalesced totals = the two-query composition (pack, then a host fold) —
|
|
908
925
|
// aggregates never nest; free time (gaps) is the two-line host walk over
|
|
909
|
-
// sorted packed answers
|
|
926
|
+
// sorted packed answers (`rows.sort(by("person", "span"))` — the
|
|
927
|
+
// keys-as-data comparator; limit is the language's own `.slice`) — both
|
|
928
|
+
// refusals recorded in the ledger.
|
|
910
929
|
```
|
|
911
930
|
|
|
912
931
|
## The write side
|
|
@@ -940,13 +959,13 @@ const Ledger = schema("Ledger", { Account, JournalEntry, Posting }, [
|
|
|
940
959
|
|
|
941
960
|
// balances (bind the fresh id — set semantics collapses duplicates):
|
|
942
961
|
const balances = query(Ledger).rule((r) => {
|
|
943
|
-
const { id, account, minor } =
|
|
944
|
-
return r.match(Posting, { id, account, minor }).
|
|
962
|
+
const { id, account, minor } = v(Posting)
|
|
963
|
+
return r.match(Posting, { id, account, minor }).find({ account, balance: r.sum(minor) })
|
|
945
964
|
})
|
|
946
965
|
// double-entry audit (host asserts every total is 0 — discipline, not schema):
|
|
947
966
|
const doubleEntry = query(Ledger).rule((r) => {
|
|
948
|
-
const { id, entry, minor } =
|
|
949
|
-
return r.match(Posting, { id, entry, minor }).
|
|
967
|
+
const { id, entry, minor } = v(Posting)
|
|
968
|
+
return r.match(Posting, { id, entry, minor }).find({ entry, balance: r.sum(minor) })
|
|
950
969
|
})
|
|
951
970
|
```
|
|
952
971
|
|
|
@@ -960,8 +979,11 @@ final-state point reads need no earlier witness.
|
|
|
960
979
|
The generation witness: read the model, propose a delta, commit iff the model
|
|
961
980
|
you read is still the model. In the SDK the whole loop is `db.writeWitnessed`
|
|
962
981
|
— retry on movement is built in (every generation move is self-inflicted by
|
|
963
|
-
the host's own interleaved writes),
|
|
964
|
-
|
|
982
|
+
the host's own interleaved writes), capped at 64 attempts: a callback that
|
|
983
|
+
itself issues a plain `db.write` each try re-moves the generation it is about
|
|
984
|
+
to witness, and past the cap that interleave hazard throws the typed
|
|
985
|
+
`ErrWitnessedLivelock` instead of spinning forever. `abandon(payload)`
|
|
986
|
+
declines to commit without issuing anything.
|
|
965
987
|
|
|
966
988
|
```ts
|
|
967
989
|
const State = closed("State", ["Queued", "Running", "Done"])
|
|
@@ -978,8 +1000,8 @@ const Jobs = schema("Jobs", { State, Job, Lease }, [
|
|
|
978
1000
|
|
|
979
1001
|
// update-where's premise — "still Queued" is the witness:
|
|
980
1002
|
const stillQueued = query(Jobs).rule((r) => {
|
|
981
|
-
const { id, payload } =
|
|
982
|
-
return r.match(Job, { id, state: "Queued", payload }).
|
|
1003
|
+
const { id, payload } = v(Job)
|
|
1004
|
+
return r.match(Job, { id, state: "Queued", payload }).find({ id, payload })
|
|
983
1005
|
})
|
|
984
1006
|
|
|
985
1007
|
const db = await Db.create("./jobs.db", Jobs)
|
|
@@ -1035,8 +1057,8 @@ const Rollup = schema("Rollup", { Arm, Claim, BusySpan }, [
|
|
|
1035
1057
|
// against sources it didn't actually read. The deriving query (pack IS the
|
|
1036
1058
|
// coalesce):
|
|
1037
1059
|
const deriving = query(Rollup).rule((r) => {
|
|
1038
|
-
const { person, span } =
|
|
1039
|
-
return r.match(Claim, { person, span, arm: "Busy" }).
|
|
1060
|
+
const { person, span } = v(Claim)
|
|
1061
|
+
return r.match(Claim, { person, span, arm: "Busy" }).find({ person, packed: r.pack(span) })
|
|
1040
1062
|
})
|
|
1041
1063
|
```
|
|
1042
1064
|
|
|
@@ -1069,18 +1091,20 @@ const Payments = schema("Payments", { Kind, Payment, Card, Ach }, [
|
|
|
1069
1091
|
// provably disjoint, so the executor elides cross-rule dedup — the free lunch.
|
|
1070
1092
|
const wholeDu = query(Payments)
|
|
1071
1093
|
.rule((r) => {
|
|
1072
|
-
const { id
|
|
1094
|
+
const { id } = v(Payment)
|
|
1095
|
+
const { last4: n } = v(Card)
|
|
1073
1096
|
return r
|
|
1074
1097
|
.match(Payment, { id, kind: "Card" })
|
|
1075
1098
|
.match(Card, { payment: id, last4: n })
|
|
1076
|
-
.
|
|
1099
|
+
.find({ id, n })
|
|
1077
1100
|
})
|
|
1078
1101
|
.rule((r) => {
|
|
1079
|
-
const { id
|
|
1102
|
+
const { id } = v(Payment)
|
|
1103
|
+
const { routing: n } = v(Ach)
|
|
1080
1104
|
return r
|
|
1081
1105
|
.match(Payment, { id, kind: "Ach" })
|
|
1082
1106
|
.match(Ach, { payment: id, routing: n })
|
|
1083
|
-
.
|
|
1107
|
+
.find({ id, n })
|
|
1084
1108
|
})
|
|
1085
1109
|
```
|
|
1086
1110
|
|
|
@@ -1148,8 +1172,8 @@ const Closure = schema("Closure", { Node, Parent }, [
|
|
|
1148
1172
|
|
|
1149
1173
|
// The loop's one query — the frontier's children, one ∈-set probe:
|
|
1150
1174
|
const step = query(Closure).rule((r) => {
|
|
1151
|
-
const { c } =
|
|
1152
|
-
return r.match(Parent, { child: c, parent: r.inSet("frontier") }).
|
|
1175
|
+
const { child: c } = v(Parent)
|
|
1176
|
+
return r.match(Parent, { child: c, parent: r.inSet("frontier") }).find({ c })
|
|
1153
1177
|
})
|
|
1154
1178
|
```
|
|
1155
1179
|
|
|
@@ -1192,22 +1216,22 @@ const reach = program(Closure, (p) => {
|
|
|
1192
1216
|
const rec = p.rec("reach")
|
|
1193
1217
|
const seeded = rec
|
|
1194
1218
|
.rule((r) => {
|
|
1195
|
-
const { c } =
|
|
1219
|
+
const { id: c } = v(Node)
|
|
1196
1220
|
return r
|
|
1197
1221
|
.match(Node, { id: c })
|
|
1198
1222
|
.where(eq(c, r.param("root")))
|
|
1199
|
-
.
|
|
1223
|
+
.find({ c })
|
|
1200
1224
|
})
|
|
1201
1225
|
.rule((r) => {
|
|
1202
|
-
const { c, parent } =
|
|
1226
|
+
const { child: c, parent } = v(Parent)
|
|
1203
1227
|
return r
|
|
1204
1228
|
.match(Parent, { child: c, parent })
|
|
1205
|
-
.idb(rec, parent)
|
|
1206
|
-
.
|
|
1229
|
+
.idb(rec, { c: parent })
|
|
1230
|
+
.find({ c })
|
|
1207
1231
|
})
|
|
1208
1232
|
return p.output((r) => {
|
|
1209
|
-
const { c } =
|
|
1210
|
-
return r.match(Node, { id: c }).idb(seeded, c).
|
|
1233
|
+
const { id: c } = v(Node)
|
|
1234
|
+
return r.match(Node, { id: c }).idb(seeded, { c }).find({ c })
|
|
1211
1235
|
})
|
|
1212
1236
|
})
|
|
1213
1237
|
const reachPrepared = db.prepare(reach)
|
|
@@ -1249,14 +1273,14 @@ const Accounts = schema("Accounts", { Account, AccountParent, Posting }, [
|
|
|
1249
1273
|
// The two queries the host rollup composes:
|
|
1250
1274
|
// the frontier step (recipe 24's loop, verbatim):
|
|
1251
1275
|
const frontierStep = query(Accounts).rule((r) => {
|
|
1252
|
-
const { c } =
|
|
1253
|
-
return r.match(AccountParent, { child: c, parent: r.inSet("frontier") }).
|
|
1276
|
+
const { child: c } = v(AccountParent)
|
|
1277
|
+
return r.match(AccountParent, { child: c, parent: r.inSet("frontier") }).find({ c })
|
|
1254
1278
|
})
|
|
1255
1279
|
// the rollup over the accumulated subtree (bind the fresh id — recipe
|
|
1256
1280
|
// 19's discipline, spent again; equal postings to one account both count):
|
|
1257
1281
|
const subtreeRollup = query(Accounts).rule((r) => {
|
|
1258
|
-
const { id, minor } =
|
|
1259
|
-
return r.match(Posting, { id, account: r.inSet("subtree"), minor }).
|
|
1282
|
+
const { id, minor } = v(Posting)
|
|
1283
|
+
return r.match(Posting, { id, account: r.inSet("subtree"), minor }).find({ total: r.sum(minor) })
|
|
1260
1284
|
})
|
|
1261
1285
|
// The engine-native form: the closure stratum converges first, then the
|
|
1262
1286
|
// output's fold runs once over the finished subtree.
|
|
@@ -1264,25 +1288,25 @@ const nativeRollup = program(Accounts, (p) => {
|
|
|
1264
1288
|
const sub = p.rec("sub")
|
|
1265
1289
|
const seeded = sub
|
|
1266
1290
|
.rule((r) => {
|
|
1267
|
-
const { a } =
|
|
1291
|
+
const { id: a } = v(Account)
|
|
1268
1292
|
return r
|
|
1269
1293
|
.match(Account, { id: a })
|
|
1270
1294
|
.where(eq(a, r.param("root")))
|
|
1271
|
-
.
|
|
1295
|
+
.find({ a })
|
|
1272
1296
|
})
|
|
1273
1297
|
.rule((r) => {
|
|
1274
|
-
const { a, parent } =
|
|
1298
|
+
const { child: a, parent } = v(AccountParent)
|
|
1275
1299
|
return r
|
|
1276
1300
|
.match(AccountParent, { child: a, parent })
|
|
1277
|
-
.idb(sub, parent)
|
|
1278
|
-
.
|
|
1301
|
+
.idb(sub, { a: parent })
|
|
1302
|
+
.find({ a })
|
|
1279
1303
|
})
|
|
1280
1304
|
return p.output((r) => {
|
|
1281
|
-
const { id, a, minor } =
|
|
1305
|
+
const { id, account: a, minor } = v(Posting)
|
|
1282
1306
|
return r
|
|
1283
1307
|
.match(Posting, { id, account: a, minor })
|
|
1284
|
-
.idb(seeded, a)
|
|
1285
|
-
.
|
|
1308
|
+
.idb(seeded, { a })
|
|
1309
|
+
.find({ total: r.sum(minor) })
|
|
1286
1310
|
})
|
|
1287
1311
|
})
|
|
1288
1312
|
```
|
|
@@ -1348,8 +1372,8 @@ const MaintainedRollup = schema("MaintainedRollup", { Arm, Claim, BusySpan }, [
|
|
|
1348
1372
|
|
|
1349
1373
|
// Derive the desired rollup on the maintenance snapshot:
|
|
1350
1374
|
const deriving = query(MaintainedRollup).rule((r) => {
|
|
1351
|
-
const { source, person, span } =
|
|
1352
|
-
return r.match(Claim, { source, person, arm: "Busy", span }).
|
|
1375
|
+
const { source, person, span } = v(Claim)
|
|
1376
|
+
return r.match(Claim, { source, person, arm: "Busy", span }).find({ person, packed: r.pack(span) })
|
|
1353
1377
|
})
|
|
1354
1378
|
```
|
|
1355
1379
|
|
|
@@ -1402,12 +1426,13 @@ const Payroll = schema("Payroll", { Employee, Salary }, [
|
|
|
1402
1426
|
|
|
1403
1427
|
// The post-migration read — salaries in force at an instant:
|
|
1404
1428
|
const inForceAt = query(Payroll).rule((r) => {
|
|
1405
|
-
const { e, name
|
|
1429
|
+
const { id: e, name } = v(Employee)
|
|
1430
|
+
const { amount, applies: w } = v(Salary)
|
|
1406
1431
|
return r
|
|
1407
1432
|
.match(Employee, { id: e, name })
|
|
1408
1433
|
.match(Salary, { employee: e, amount, applies: w })
|
|
1409
1434
|
.where(pointIn(r.param("at"), w))
|
|
1410
|
-
.
|
|
1435
|
+
.find({ name, amount })
|
|
1411
1436
|
})
|
|
1412
1437
|
```
|
|
1413
1438
|
|
|
@@ -1468,3 +1493,77 @@ unit slots `[4,5)`, `[5,6)` satisfies both directions, because nothing forces
|
|
|
1468
1493
|
the witness rows to mirror the sidecar's segmentation — only its points. If
|
|
1469
1494
|
per-row correspondence matters, the host writes zones at slot granularity;
|
|
1470
1495
|
the schema proves disjointness and coverage either way.
|
|
1496
|
+
|
|
1497
|
+
## Point reads
|
|
1498
|
+
|
|
1499
|
+
## 30. The keyed read
|
|
1500
|
+
|
|
1501
|
+
Guarantee: validator/runtime premises — a declared key FD admits at most one
|
|
1502
|
+
fact per determinant tuple (the key phase of the commit judgment), and every
|
|
1503
|
+
keyed point read answers exactly that fact or nothing, on every scope
|
|
1504
|
+
(`ts/test/keyed-get.test.ts`; the engine half is
|
|
1505
|
+
`crates/bumbledb/tests/keyed_get.rs`).
|
|
1506
|
+
|
|
1507
|
+
The key is a **law**, and the read surface is that law made callable. The
|
|
1508
|
+
schema says `key(Program, ["grp"])` — one program per group — so "the
|
|
1509
|
+
program of a group" is a well-posed question with at most one answer, and
|
|
1510
|
+
the store already enforces that on every commit. Hold the statement VALUE:
|
|
1511
|
+
it is the read's selector below (statement identity is the membership rule).
|
|
1512
|
+
|
|
1513
|
+
```ts
|
|
1514
|
+
const Grp = relation("Grp", { id: u64.fresh, label: str })
|
|
1515
|
+
const Program = relation("Program", { id: u64.fresh, grp: u64, title: str })
|
|
1516
|
+
// The law: one program per group — the callable key.
|
|
1517
|
+
const programGrpKey = key(Program, ["grp"])
|
|
1518
|
+
|
|
1519
|
+
const KeyedRead = schema("KeyedRead", { Grp, Program }, [
|
|
1520
|
+
contained(on(Program, "grp"), on(Grp, "id")),
|
|
1521
|
+
programGrpKey
|
|
1522
|
+
])
|
|
1523
|
+
```
|
|
1524
|
+
|
|
1525
|
+
The point read is the statement handed back to `get` — one spelling on
|
|
1526
|
+
every scope (the symmetry rule): `db.get` standalone, `snap.get` inside a
|
|
1527
|
+
read scope, `tx.get` inside a write transaction, where the transaction side
|
|
1528
|
+
answers the FINAL state (base plus pending delta: read-your-writes, a
|
|
1529
|
+
pending delete answers `undefined`). The key object is typed by the
|
|
1530
|
+
statement's own projection — a wrong field name is a compile error, never a
|
|
1531
|
+
runtime shape check. The primary 2-arg form needs no statement: the fresh
|
|
1532
|
+
field IS the primary key.
|
|
1533
|
+
|
|
1534
|
+
```ts
|
|
1535
|
+
const db = await Db.create("./programs.db", KeyedRead)
|
|
1536
|
+
|
|
1537
|
+
const minted: { grp?: bigint } = {}
|
|
1538
|
+
db.write((tx) => {
|
|
1539
|
+
const g = tx.insert(Grp, { label: "algebra" })
|
|
1540
|
+
tx.insert(Program, { grp: g.id, title: "linear equations" })
|
|
1541
|
+
minted.grp = g.id
|
|
1542
|
+
})
|
|
1543
|
+
const grp = minted.grp ?? 0n
|
|
1544
|
+
|
|
1545
|
+
// db.get — the standalone keyed read through the declared law:
|
|
1546
|
+
const byGroup = db.get(Program, programGrpKey, { grp })
|
|
1547
|
+
|
|
1548
|
+
// snap.get — the same spelling inside a read scope:
|
|
1549
|
+
const viaSnap = db.read((snap) => snap.get(Program, programGrpKey, { grp }))
|
|
1550
|
+
|
|
1551
|
+
// tx.get — key-shaped read-modify-write, final-state (recipe 20's third
|
|
1552
|
+
// idiom): per-fact premises need no earlier snapshot witness.
|
|
1553
|
+
db.write((tx) => {
|
|
1554
|
+
const current = tx.get(Program, programGrpKey, { grp })
|
|
1555
|
+
if (current !== undefined) {
|
|
1556
|
+
tx.delete(Program, current)
|
|
1557
|
+
tx.insert(Program, { id: current.id, grp: current.grp, title: "linear equations II" })
|
|
1558
|
+
}
|
|
1559
|
+
})
|
|
1560
|
+
|
|
1561
|
+
// The primary 2-arg form — the fresh field is the primary key:
|
|
1562
|
+
const byId = byGroup === undefined ? undefined : db.get(Program, { id: byGroup.id })
|
|
1563
|
+
```
|
|
1564
|
+
|
|
1565
|
+
The anti-pattern this recipe retires: a scan-and-find where a key law
|
|
1566
|
+
exists — `snap.scan(Program).find((row) => row.grp === grp)` — re-derives
|
|
1567
|
+
in the host what the store already enforces. The uniqueness the fold
|
|
1568
|
+
quietly assumes IS the declared key statement; spell the law and the point
|
|
1569
|
+
read comes with it.
|
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ classes, inferred query rows, and rejections that arrive as data rather than
|
|
|
27
27
|
exceptions.
|
|
28
28
|
|
|
29
29
|
```ts
|
|
30
|
-
import { bool, closed, contained, Db, gt, type Infer, key, on, query, relation, schema, u64 } from "@bjornpagen/bumbledb"
|
|
30
|
+
import { bool, closed, contained, Db, gt, type Infer, key, on, query, relation, schema, u64, v } from "@bjornpagen/bumbledb"
|
|
31
31
|
|
|
32
32
|
// A closed relation: a sealed roster of axioms with typed payload columns.
|
|
33
33
|
// At the host surface a handle is its NAME — the string literal "DirectPass"
|
|
@@ -75,16 +75,18 @@ if (!result.ok) {
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
// Query: Datalog as values.
|
|
79
|
-
//
|
|
78
|
+
// Query: Datalog as values. v(R) mints a fresh variable per column, typed by
|
|
79
|
+
// the column's law-class; reusing one by object reference IS the join, and
|
|
80
|
+
// rows are typed from the find keys. Params are typed by use.
|
|
80
81
|
// `gt` is one of the free comparison exports.
|
|
81
82
|
const certifiedAbove = query(Review).rule((r) => {
|
|
82
|
-
const { a, k
|
|
83
|
+
const { attempt: a, kind: k } = v(Certificate)
|
|
84
|
+
const { rank } = v(Kind)
|
|
83
85
|
return r
|
|
84
86
|
.match(Certificate, { attempt: a, kind: k })
|
|
85
|
-
.match(Kind, { id: k, mastered: true, rank }) // ψ on the read side too
|
|
87
|
+
.match(Kind, { id: k, mastered: true, rank }) // k reused at Kind.id — that reuse is the join; ψ on the read side too
|
|
86
88
|
.where(gt(rank, r.param("floor")))
|
|
87
|
-
.
|
|
89
|
+
.find({ a, rank })
|
|
88
90
|
})
|
|
89
91
|
|
|
90
92
|
const prepared = db.prepare(certifiedAbove)
|
|
@@ -118,7 +120,7 @@ The drizzle law governs this surface: the SDK's job at the host boundary is tran
|
|
|
118
120
|
- The structural type kernel — fields as pure structure (`bool`, `bytes`, `i64`, `u64`, `str`, `interval`, `span`), `relation()`, and `closed()` sealed rosters with typed axiom payloads. A closed reference's value type IS the handle union (`Infer` speaks it); dispatch is native `switch` narrowing with `satisfies never` exhaustiveness. Domains are never declared: `schema()` computes every field's class from the statement list.
|
|
119
121
|
- The statement algebra — `schema()`, `key`, `contained`, `mirrors`, `window`; faces via `on` (set membership is a plain array in `.where`); counts via `exactly`, `atLeast`, `atMost`, `between`, `none`; ψ-selection via `.where` on relations and closed rosters.
|
|
120
122
|
- The `Db` runtime — `Db.create`/`Db.open`, path-cached stores, transactions, typed violations, scoped snapshot reads, the witnessed write loop with `abandon`.
|
|
121
|
-
- The query surface — Datalog as values, `query(S).rule(r => ...)`:
|
|
123
|
+
- The query surface — Datalog as values, `query(S).rule(r => ...)`: `v(R)`-minted vars (identity is the object reference — reusing one across binding positions IS the join), `find({...})` named result heads (renames are real), params typed by use unchanged, negation, aggregates, and the free comparison/connective exports (`eq`, `ne`, `lt`, `le`, `gt`, `ge`, `and`, `or`, `not`, `allen`/`ALLEN`, `pointIn`); set membership at a closed field is a plain array in the match record (`r.match(Ticket, { priority: ["Normal", "Urgent"] })` — closed-only there: an ordinary field's membership is a bound `r.inSet` param); stratified recursion via `program()`; `db.prepare` as a plain value.
|
|
122
124
|
- The exhume surface — `Db.exhume`, the schema-independent read path: a store's self-described shapes and raw facts by name, with typed refusals (`ErrExhumeNoDescriptor`, `ErrExhumeFormatMismatch`, `ErrExhumeCorruption`).
|
|
123
125
|
|
|
124
126
|
## Cookbook
|