@bjornpagen/bumbledb 0.5.0 → 0.7.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 +140 -136
- package/README.md +24 -11
- package/dist/closed.d.ts +7 -5
- package/dist/closed.d.ts.map +1 -1
- package/dist/closed.js +4 -1
- package/dist/closed.js.map +1 -1
- package/dist/count.js +1 -1
- package/dist/count.js.map +1 -1
- package/dist/db.d.ts +110 -73
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +172 -116
- package/dist/db.js.map +1 -1
- package/dist/exhume.d.ts +22 -10
- package/dist/exhume.d.ts.map +1 -1
- package/dist/exhume.js +42 -9
- package/dist/exhume.js.map +1 -1
- package/dist/face.d.ts +10 -6
- package/dist/face.d.ts.map +1 -1
- package/dist/face.js.map +1 -1
- package/dist/fields.d.ts +38 -16
- package/dist/fields.d.ts.map +1 -1
- package/dist/fields.js +33 -8
- package/dist/fields.js.map +1 -1
- package/dist/index.d.ts +9 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/lower.d.ts +9 -9
- package/dist/lower.d.ts.map +1 -1
- package/dist/lower.js +17 -12
- package/dist/lower.js.map +1 -1
- package/dist/marshal.d.ts +17 -7
- package/dist/marshal.d.ts.map +1 -1
- package/dist/marshal.js +32 -10
- package/dist/marshal.js.map +1 -1
- package/dist/native.d.ts +60 -9
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js.map +1 -1
- package/dist/query/atom.d.ts +199 -203
- package/dist/query/atom.d.ts.map +1 -1
- package/dist/query/atom.js +39 -48
- 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 +131 -158
- package/dist/query/lower.d.ts.map +1 -1
- package/dist/query/lower.js +505 -494
- 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 +35 -42
- package/dist/query/predicate.js.map +1 -1
- package/dist/query/run.d.ts +8 -8
- package/dist/query/run.d.ts.map +1 -1
- package/dist/query/run.js +16 -21
- package/dist/query/run.js.map +1 -1
- package/dist/query/scope.d.ts +139 -74
- package/dist/query/scope.d.ts.map +1 -1
- package/dist/query/scope.js +80 -33
- package/dist/query/scope.js.map +1 -1
- package/dist/relation.d.ts.map +1 -1
- package/dist/relation.js +3 -1
- package/dist/relation.js.map +1 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +12 -2
- package/dist/schema.js.map +1 -1
- package/dist/spec.d.ts +20 -10
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js.map +1 -1
- package/dist/statements.d.ts +22 -2
- package/dist/statements.d.ts.map +1 -1
- package/dist/statements.js +26 -5
- package/dist/statements.js.map +1 -1
- package/package.json +3 -3
- package/src/closed.ts +13 -7
- package/src/count.ts +1 -1
- package/src/db.ts +293 -177
- package/src/exhume.ts +48 -11
- package/src/face.ts +13 -6
- package/src/fields.ts +51 -15
- package/src/index.ts +12 -8
- package/src/lower.ts +17 -12
- package/src/marshal.ts +38 -13
- package/src/native.ts +59 -8
- package/src/query/atom.ts +297 -275
- package/src/query/find.ts +212 -0
- package/src/query/lower.ts +692 -724
- package/src/query/predicate.ts +37 -45
- package/src/query/run.ts +17 -22
- package/src/query/scope.ts +184 -90
- package/src/relation.ts +3 -1
- package/src/schema.ts +14 -2
- package/src/spec.ts +21 -9
- package/src/statements.ts +30 -6
- 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
|
@@ -56,7 +56,8 @@ import {
|
|
|
56
56
|
relation,
|
|
57
57
|
schema,
|
|
58
58
|
str,
|
|
59
|
-
u64
|
|
59
|
+
u64,
|
|
60
|
+
v
|
|
60
61
|
} from "@bjornpagen/bumbledb"
|
|
61
62
|
```
|
|
62
63
|
|
|
@@ -118,30 +119,27 @@ const Uptime = schema("Uptime", { Service, Outage }, [
|
|
|
118
119
|
key(Outage, ["service", "window"])
|
|
119
120
|
])
|
|
120
121
|
|
|
121
|
-
// down at instant t —
|
|
122
|
-
//
|
|
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:
|
|
123
124
|
const downAt = query(Uptime).rule((r) => {
|
|
124
|
-
const service =
|
|
125
|
-
const window = r.var("window")
|
|
125
|
+
const { service, window } = v(Outage)
|
|
126
126
|
return r
|
|
127
127
|
.match(Outage, { service, window })
|
|
128
128
|
.where(pointIn(r.param("t"), window))
|
|
129
|
-
.
|
|
129
|
+
.find({ service })
|
|
130
130
|
})
|
|
131
131
|
// overlapping an incident window (one Allen mask, no operator zoo):
|
|
132
132
|
const overlapping = query(Uptime).rule((r) => {
|
|
133
|
-
const service =
|
|
134
|
-
const window = r.var("window")
|
|
133
|
+
const { service, window } = v(Outage)
|
|
135
134
|
return r
|
|
136
135
|
.match(Outage, { service, window })
|
|
137
136
|
.where(allen(window, ALLEN.intersects, r.param("incident")))
|
|
138
|
-
.
|
|
137
|
+
.find({ service, window })
|
|
139
138
|
})
|
|
140
139
|
// total downtime per service (the denotation's one arithmetic):
|
|
141
140
|
const downtime = query(Uptime).rule((r) => {
|
|
142
|
-
const service =
|
|
143
|
-
|
|
144
|
-
return r.match(Outage, { service, window }).select("service", r.sum(r.duration("window")))
|
|
141
|
+
const { service, window } = v(Outage)
|
|
142
|
+
return r.match(Outage, { service, window }).find({ service, downtime: r.sum(r.duration(window)) })
|
|
145
143
|
})
|
|
146
144
|
```
|
|
147
145
|
|
|
@@ -220,11 +218,11 @@ const Optionality = schema("Optionality", { Business, MailingAddress }, [
|
|
|
220
218
|
|
|
221
219
|
// Negation is plain anti-join (no null branch exists in any operator):
|
|
222
220
|
const unaddressed = query(Optionality).rule((r) => {
|
|
223
|
-
const b =
|
|
221
|
+
const { id: b } = v(Business)
|
|
224
222
|
return r
|
|
225
223
|
.match(Business, { id: b })
|
|
226
224
|
.where(not(MailingAddress, { business: b }))
|
|
227
|
-
.
|
|
225
|
+
.find({ b })
|
|
228
226
|
})
|
|
229
227
|
```
|
|
230
228
|
|
|
@@ -260,11 +258,8 @@ const Money = schema("Money", { Currency, Account, Posting }, [
|
|
|
260
258
|
// silently. Bind the fresh id: set semantics would collapse two equal
|
|
261
259
|
// (account, currency, minor) postings without it.
|
|
262
260
|
const totals = query(Money).rule((r) => {
|
|
263
|
-
const id =
|
|
264
|
-
|
|
265
|
-
const currency = r.var("currency")
|
|
266
|
-
const minor = r.var("minor")
|
|
267
|
-
return r.match(Posting, { id, account, currency, minor }).select("account", "currency", r.sum("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) })
|
|
268
263
|
})
|
|
269
264
|
```
|
|
270
265
|
|
|
@@ -300,8 +295,8 @@ const Content = schema("Content", { Region, Document, Replica }, [
|
|
|
300
295
|
|
|
301
296
|
// a bytes param self-encodes (Uint8Array by inference):
|
|
302
297
|
const byDigest = query(Content).rule((r) => {
|
|
303
|
-
const id =
|
|
304
|
-
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 })
|
|
305
300
|
})
|
|
306
301
|
```
|
|
307
302
|
|
|
@@ -342,8 +337,8 @@ const Tickets = schema("Tickets", { Priority, Ticket }, [
|
|
|
342
337
|
// that drifts without a rebuild is an ordinary relation — a vocabulary is
|
|
343
338
|
// never written, only declared.
|
|
344
339
|
const urgent = query(Tickets).rule((r) => {
|
|
345
|
-
const t =
|
|
346
|
-
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 })
|
|
347
342
|
})
|
|
348
343
|
|
|
349
344
|
// Set membership is a plain array — the drizzle law's spelling, closed-only
|
|
@@ -351,8 +346,8 @@ const urgent = query(Tickets).rule((r) => {
|
|
|
351
346
|
// ∈-set param, `r.inSet`); the array folds to the same wire set the param
|
|
352
347
|
// spelling crosses. In `.where()` selections arrays work at EVERY field kind.
|
|
353
348
|
const actionable = query(Tickets).rule((r) => {
|
|
354
|
-
const t =
|
|
355
|
-
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 })
|
|
356
351
|
})
|
|
357
352
|
```
|
|
358
353
|
|
|
@@ -396,12 +391,11 @@ const Review = schema("Review", { Kind, Attempt, Certificate }, [
|
|
|
396
391
|
// exactly like an ordinary one, and the atom folds at prepare into a
|
|
397
392
|
// plan-constant handle set on its sibling.
|
|
398
393
|
const masteredAttempts = query(Review).rule((r) => {
|
|
399
|
-
const a =
|
|
400
|
-
const k = r.var("k")
|
|
394
|
+
const { id: a, kind: k } = v(Attempt)
|
|
401
395
|
return r
|
|
402
396
|
.match(Attempt, { id: a, kind: k })
|
|
403
397
|
.match(Kind, { id: k, mastered: true })
|
|
404
|
-
.
|
|
398
|
+
.find({ a })
|
|
405
399
|
})
|
|
406
400
|
|
|
407
401
|
// Host dispatch on the payload tier is the record-table idiom — a `Record`
|
|
@@ -460,12 +454,11 @@ const Oncall = schema("Oncall", { Severity, Incident, Escalation }, [
|
|
|
460
454
|
|
|
461
455
|
// who is being paged — the same ψ, on the read side:
|
|
462
456
|
const paged = query(Oncall).rule((r) => {
|
|
463
|
-
const i =
|
|
464
|
-
const s = r.var("s")
|
|
457
|
+
const { incident: i, severity: s } = v(Escalation)
|
|
465
458
|
return r
|
|
466
459
|
.match(Escalation, { incident: i, severity: s })
|
|
467
460
|
.match(Severity, { id: s, pages: true })
|
|
468
|
-
.
|
|
461
|
+
.find({ i })
|
|
469
462
|
})
|
|
470
463
|
```
|
|
471
464
|
|
|
@@ -507,12 +500,11 @@ const Playlists = schema("Playlists", { Playlist, Extent, Slot }, [
|
|
|
507
500
|
|
|
508
501
|
// Positional access is membership — "what plays at position ?pos":
|
|
509
502
|
const playingAt = query(Playlists).rule((r) => {
|
|
510
|
-
const slot =
|
|
511
|
-
const track = r.var("track")
|
|
503
|
+
const { slot, track } = v(Slot)
|
|
512
504
|
return r
|
|
513
505
|
.match(Slot, { playlist: r.param("list"), slot, track })
|
|
514
506
|
.where(pointIn(r.param("pos"), slot))
|
|
515
|
-
.
|
|
507
|
+
.find({ track })
|
|
516
508
|
})
|
|
517
509
|
|
|
518
510
|
// Answers are SETS — the host sorts them, and the SDK ships the comparator:
|
|
@@ -568,12 +560,12 @@ const Ast = schema("Ast", { Kind, Node, Lit, Add, Parent }, [
|
|
|
568
560
|
])
|
|
569
561
|
|
|
570
562
|
const lhsLiteral = query(Ast).rule((r) => {
|
|
571
|
-
const l =
|
|
572
|
-
const
|
|
563
|
+
const { lhs: l } = v(Add)
|
|
564
|
+
const { value } = v(Lit)
|
|
573
565
|
return r
|
|
574
566
|
.match(Add, { node: r.param("n"), lhs: l })
|
|
575
|
-
.match(Lit, { node: l, value
|
|
576
|
-
.
|
|
567
|
+
.match(Lit, { node: l, value })
|
|
568
|
+
.find({ value })
|
|
577
569
|
})
|
|
578
570
|
```
|
|
579
571
|
|
|
@@ -604,13 +596,12 @@ const Graph = schema("Graph", { Person, Repo, Follows, Maintains }, [
|
|
|
604
596
|
// live in the "Person.id" class, so the reuse is lawful); `lt` keeps each
|
|
605
597
|
// pair once:
|
|
606
598
|
const mutual = query(Graph).rule((r) => {
|
|
607
|
-
const a =
|
|
608
|
-
const b = r.var("b")
|
|
599
|
+
const { follower: a, followee: b } = v(Follows)
|
|
609
600
|
return r
|
|
610
601
|
.match(Follows, { follower: a, followee: b })
|
|
611
602
|
.match(Follows, { follower: b, followee: a })
|
|
612
603
|
.where(lt(a, b))
|
|
613
|
-
.
|
|
604
|
+
.find({ a, b })
|
|
614
605
|
})
|
|
615
606
|
```
|
|
616
607
|
|
|
@@ -643,15 +634,12 @@ const Ecs = schema("Ecs", { Entity, Transform, Velocity, Renderable }, [
|
|
|
643
634
|
|
|
644
635
|
// The physics join is the component intersection:
|
|
645
636
|
const physics = query(Ecs).rule((r) => {
|
|
646
|
-
const entity =
|
|
647
|
-
const
|
|
648
|
-
const y = r.var("y")
|
|
649
|
-
const dx = r.var("dx")
|
|
650
|
-
const dy = r.var("dy")
|
|
637
|
+
const { entity, x, y } = v(Transform)
|
|
638
|
+
const { dx, dy } = v(Velocity)
|
|
651
639
|
return r
|
|
652
640
|
.match(Transform, { entity, x, y })
|
|
653
641
|
.match(Velocity, { entity, dx, dy })
|
|
654
|
-
.
|
|
642
|
+
.find({ entity, x, y, dx, dy })
|
|
655
643
|
})
|
|
656
644
|
```
|
|
657
645
|
|
|
@@ -687,12 +675,12 @@ const Orders = schema("Orders", { State, Order, Placement, Shipment }, [
|
|
|
687
675
|
])
|
|
688
676
|
|
|
689
677
|
const shipped = query(Orders).rule((r) => {
|
|
690
|
-
const id =
|
|
691
|
-
const carrier =
|
|
678
|
+
const { id } = v(Order)
|
|
679
|
+
const { carrier } = v(Shipment)
|
|
692
680
|
return r
|
|
693
681
|
.match(Order, { id, state: "Shipped" })
|
|
694
682
|
.match(Shipment, { order: id, carrier })
|
|
695
|
-
.
|
|
683
|
+
.find({ id, carrier })
|
|
696
684
|
})
|
|
697
685
|
```
|
|
698
686
|
|
|
@@ -758,20 +746,18 @@ const Calendar = schema("Calendar", { Rsvp, Arm, Person, Room, Event, Attendance
|
|
|
758
746
|
])
|
|
759
747
|
|
|
760
748
|
const roomConflicts = query(Calendar).rule((r) => {
|
|
761
|
-
const room =
|
|
762
|
-
const span = r.var("span")
|
|
749
|
+
const { room, span } = v(Booking)
|
|
763
750
|
return r
|
|
764
751
|
.match(Booking, { room, span })
|
|
765
752
|
.where(allen(span, ALLEN.intersects, r.param("want")))
|
|
766
|
-
.
|
|
753
|
+
.find({ room, span })
|
|
767
754
|
})
|
|
768
755
|
const personLoad = query(Calendar).rule((r) => {
|
|
769
|
-
const person =
|
|
770
|
-
const span = r.var("span")
|
|
756
|
+
const { person, span } = v(Claim)
|
|
771
757
|
return r
|
|
772
758
|
.match(Claim, { person, span })
|
|
773
759
|
.where(allen(span, ALLEN.intersects, r.param("window")))
|
|
774
|
-
.
|
|
760
|
+
.find({ person, span })
|
|
775
761
|
})
|
|
776
762
|
```
|
|
777
763
|
|
|
@@ -803,23 +789,21 @@ const Pricing = schema("Pricing", { Policy, Version }, [
|
|
|
803
789
|
|
|
804
790
|
// in force on date t — one membership probe:
|
|
805
791
|
const inForce = query(Pricing).rule((r) => {
|
|
806
|
-
const rate_bps =
|
|
807
|
-
const valid = r.var("valid")
|
|
792
|
+
const { rate_bps, valid } = v(Version)
|
|
808
793
|
return r
|
|
809
794
|
.match(Version, { policy: r.param("p"), rate_bps, valid })
|
|
810
795
|
.where(pointIn(r.param("t"), valid))
|
|
811
|
-
.
|
|
796
|
+
.find({ rate_bps })
|
|
812
797
|
})
|
|
813
798
|
// clean successions (half-open makes MEETS exact, no ±1 fudge):
|
|
814
799
|
const successions = query(Pricing).rule((r) => {
|
|
815
|
-
const p =
|
|
816
|
-
const
|
|
817
|
-
const b = r.var("b")
|
|
800
|
+
const { policy: p, valid: a } = v(Version)
|
|
801
|
+
const { valid: b } = v(Version)
|
|
818
802
|
return r
|
|
819
803
|
.match(Version, { policy: p, valid: a })
|
|
820
804
|
.match(Version, { policy: p, valid: b })
|
|
821
805
|
.where(allen(a, ALLEN.meets, b))
|
|
822
|
-
.
|
|
806
|
+
.find({ a, b })
|
|
823
807
|
})
|
|
824
808
|
```
|
|
825
809
|
|
|
@@ -848,12 +832,11 @@ const Payroll = schema("Payroll", { FiscalYear, PayPeriod }, [
|
|
|
848
832
|
|
|
849
833
|
// the period holding date t:
|
|
850
834
|
const holding = query(Payroll).rule((r) => {
|
|
851
|
-
const seq =
|
|
852
|
-
const span = r.var("span")
|
|
835
|
+
const { seq, span } = v(PayPeriod)
|
|
853
836
|
return r
|
|
854
837
|
.match(PayPeriod, { year: r.param("y"), seq, span })
|
|
855
838
|
.where(pointIn(r.param("t"), span))
|
|
856
|
-
.
|
|
839
|
+
.find({ seq })
|
|
857
840
|
})
|
|
858
841
|
```
|
|
859
842
|
|
|
@@ -897,14 +880,13 @@ const Tax = schema("Tax", { Status, Regime, Bracket, Residency, Earned }, [
|
|
|
897
880
|
// owed is host arithmetic over the bracket walk — arithmetic beyond the
|
|
898
881
|
// measure is refused (the ledger).
|
|
899
882
|
const marginal = query(Tax).rule((r) => {
|
|
900
|
-
const reg =
|
|
901
|
-
const b =
|
|
902
|
-
const rate_bps = r.var("rate_bps")
|
|
883
|
+
const { id: reg } = v(Regime)
|
|
884
|
+
const { income: b, rate_bps } = v(Bracket)
|
|
903
885
|
return r
|
|
904
886
|
.match(Regime, { id: reg, year: r.param("y"), status: r.param("s") })
|
|
905
887
|
.match(Bracket, { regime: reg, income: b, rate_bps })
|
|
906
888
|
.where(pointIn(r.param("taxable"), b))
|
|
907
|
-
.
|
|
889
|
+
.find({ rate_bps })
|
|
908
890
|
})
|
|
909
891
|
```
|
|
910
892
|
|
|
@@ -931,15 +913,13 @@ const FreeTime = schema("FreeTime", { Person, Claim }, [
|
|
|
931
913
|
|
|
932
914
|
// busy time, coalesced (adjacent segments merge — the half-open law):
|
|
933
915
|
const busy = query(FreeTime).rule((r) => {
|
|
934
|
-
const person =
|
|
935
|
-
|
|
936
|
-
return r.match(Claim, { person, span }).select("person", r.pack("span"))
|
|
916
|
+
const { person, span } = v(Claim)
|
|
917
|
+
return r.match(Claim, { person, span }).find({ person, packed: r.pack(span) })
|
|
937
918
|
})
|
|
938
919
|
// raw claimed time (overlaps double-count — often the wrong question):
|
|
939
920
|
const claimed = query(FreeTime).rule((r) => {
|
|
940
|
-
const person =
|
|
941
|
-
|
|
942
|
-
return r.match(Claim, { person, span }).select("person", r.sum(r.duration("span")))
|
|
921
|
+
const { person, span } = v(Claim)
|
|
922
|
+
return r.match(Claim, { person, span }).find({ person, claimed: r.sum(r.duration(span)) })
|
|
943
923
|
})
|
|
944
924
|
// Coalesced totals = the two-query composition (pack, then a host fold) —
|
|
945
925
|
// aggregates never nest; free time (gaps) is the two-line host walk over
|
|
@@ -979,17 +959,13 @@ const Ledger = schema("Ledger", { Account, JournalEntry, Posting }, [
|
|
|
979
959
|
|
|
980
960
|
// balances (bind the fresh id — set semantics collapses duplicates):
|
|
981
961
|
const balances = query(Ledger).rule((r) => {
|
|
982
|
-
const id =
|
|
983
|
-
|
|
984
|
-
const minor = r.var("minor")
|
|
985
|
-
return r.match(Posting, { id, account, minor }).select("account", r.sum("minor"))
|
|
962
|
+
const { id, account, minor } = v(Posting)
|
|
963
|
+
return r.match(Posting, { id, account, minor }).find({ account, balance: r.sum(minor) })
|
|
986
964
|
})
|
|
987
965
|
// double-entry audit (host asserts every total is 0 — discipline, not schema):
|
|
988
966
|
const doubleEntry = query(Ledger).rule((r) => {
|
|
989
|
-
const id =
|
|
990
|
-
|
|
991
|
-
const minor = r.var("minor")
|
|
992
|
-
return r.match(Posting, { id, entry, minor }).select("entry", r.sum("minor"))
|
|
967
|
+
const { id, entry, minor } = v(Posting)
|
|
968
|
+
return r.match(Posting, { id, entry, minor }).find({ entry, balance: r.sum(minor) })
|
|
993
969
|
})
|
|
994
970
|
```
|
|
995
971
|
|
|
@@ -1007,7 +983,10 @@ the host's own interleaved writes), capped at 64 attempts: a callback that
|
|
|
1007
983
|
itself issues a plain `db.write` each try re-moves the generation it is about
|
|
1008
984
|
to witness, and past the cap that interleave hazard throws the typed
|
|
1009
985
|
`ErrWitnessedLivelock` instead of spinning forever. `abandon(payload)`
|
|
1010
|
-
declines to commit without issuing anything.
|
|
986
|
+
declines to commit without issuing anything — from `db.write` and
|
|
987
|
+
`db.writeWitnessed` alike (the sentinel's contract is unconditional, and
|
|
988
|
+
the outcome arm is in the result type exactly when the callback can
|
|
989
|
+
abandon).
|
|
1011
990
|
|
|
1012
991
|
```ts
|
|
1013
992
|
const State = closed("State", ["Queued", "Running", "Done"])
|
|
@@ -1024,9 +1003,8 @@ const Jobs = schema("Jobs", { State, Job, Lease }, [
|
|
|
1024
1003
|
|
|
1025
1004
|
// update-where's premise — "still Queued" is the witness:
|
|
1026
1005
|
const stillQueued = query(Jobs).rule((r) => {
|
|
1027
|
-
const id =
|
|
1028
|
-
|
|
1029
|
-
return r.match(Job, { id, state: "Queued", payload }).select("id", "payload")
|
|
1006
|
+
const { id, payload } = v(Job)
|
|
1007
|
+
return r.match(Job, { id, state: "Queued", payload }).find({ id, payload })
|
|
1030
1008
|
})
|
|
1031
1009
|
|
|
1032
1010
|
const db = await Db.create("./jobs.db", Jobs)
|
|
@@ -1082,9 +1060,8 @@ const Rollup = schema("Rollup", { Arm, Claim, BusySpan }, [
|
|
|
1082
1060
|
// against sources it didn't actually read. The deriving query (pack IS the
|
|
1083
1061
|
// coalesce):
|
|
1084
1062
|
const deriving = query(Rollup).rule((r) => {
|
|
1085
|
-
const person =
|
|
1086
|
-
|
|
1087
|
-
return r.match(Claim, { person, span, arm: "Busy" }).select("person", r.pack("span"))
|
|
1063
|
+
const { person, span } = v(Claim)
|
|
1064
|
+
return r.match(Claim, { person, span, arm: "Busy" }).find({ person, packed: r.pack(span) })
|
|
1088
1065
|
})
|
|
1089
1066
|
```
|
|
1090
1067
|
|
|
@@ -1117,20 +1094,20 @@ const Payments = schema("Payments", { Kind, Payment, Card, Ach }, [
|
|
|
1117
1094
|
// provably disjoint, so the executor elides cross-rule dedup — the free lunch.
|
|
1118
1095
|
const wholeDu = query(Payments)
|
|
1119
1096
|
.rule((r) => {
|
|
1120
|
-
const id =
|
|
1121
|
-
const n =
|
|
1097
|
+
const { id } = v(Payment)
|
|
1098
|
+
const { last4: n } = v(Card)
|
|
1122
1099
|
return r
|
|
1123
1100
|
.match(Payment, { id, kind: "Card" })
|
|
1124
1101
|
.match(Card, { payment: id, last4: n })
|
|
1125
|
-
.
|
|
1102
|
+
.find({ id, n })
|
|
1126
1103
|
})
|
|
1127
1104
|
.rule((r) => {
|
|
1128
|
-
const id =
|
|
1129
|
-
const n =
|
|
1105
|
+
const { id } = v(Payment)
|
|
1106
|
+
const { routing: n } = v(Ach)
|
|
1130
1107
|
return r
|
|
1131
1108
|
.match(Payment, { id, kind: "Ach" })
|
|
1132
1109
|
.match(Ach, { payment: id, routing: n })
|
|
1133
|
-
.
|
|
1110
|
+
.find({ id, n })
|
|
1134
1111
|
})
|
|
1135
1112
|
```
|
|
1136
1113
|
|
|
@@ -1198,8 +1175,8 @@ const Closure = schema("Closure", { Node, Parent }, [
|
|
|
1198
1175
|
|
|
1199
1176
|
// The loop's one query — the frontier's children, one ∈-set probe:
|
|
1200
1177
|
const step = query(Closure).rule((r) => {
|
|
1201
|
-
const c =
|
|
1202
|
-
return r.match(Parent, { child: c, parent: r.inSet("frontier") }).
|
|
1178
|
+
const { child: c } = v(Parent)
|
|
1179
|
+
return r.match(Parent, { child: c, parent: r.inSet("frontier") }).find({ c })
|
|
1203
1180
|
})
|
|
1204
1181
|
```
|
|
1205
1182
|
|
|
@@ -1233,37 +1210,70 @@ for (;;) {
|
|
|
1233
1210
|
Termination is the host's theorem: `seen` grows strictly or the loop breaks,
|
|
1234
1211
|
inside a finite node set. When the idiom's costs bite — **unbounded or large
|
|
1235
1212
|
depth**, or **closure composed into a larger plan** — write the engine-native
|
|
1236
|
-
form instead: `?root` seeds the predicate, and the output
|
|
1237
|
-
set
|
|
1238
|
-
|
|
1213
|
+
form instead: `?root` seeds the predicate, and the output is the finished
|
|
1214
|
+
set's own identity projection (an `idb` atom is a positive occurrence, so it
|
|
1215
|
+
grounds its variables — no re-grounding join over a domain relation exists):
|
|
1239
1216
|
|
|
1240
1217
|
```ts
|
|
1241
1218
|
const reach = program(Closure, (p) => {
|
|
1242
1219
|
const rec = p.rec("reach")
|
|
1243
1220
|
const seeded = rec
|
|
1244
1221
|
.rule((r) => {
|
|
1245
|
-
const c =
|
|
1222
|
+
const { id: c } = v(Node)
|
|
1246
1223
|
return r
|
|
1247
1224
|
.match(Node, { id: c })
|
|
1248
1225
|
.where(eq(c, r.param("root")))
|
|
1249
|
-
.
|
|
1226
|
+
.find({ c })
|
|
1250
1227
|
})
|
|
1251
1228
|
.rule((r) => {
|
|
1252
|
-
const c =
|
|
1253
|
-
const parent = r.var("parent")
|
|
1229
|
+
const { child: c, parent } = v(Parent)
|
|
1254
1230
|
return r
|
|
1255
1231
|
.match(Parent, { child: c, parent })
|
|
1256
|
-
.idb(rec, parent)
|
|
1257
|
-
.
|
|
1232
|
+
.idb(rec, { c: parent })
|
|
1233
|
+
.find({ c })
|
|
1258
1234
|
})
|
|
1259
1235
|
return p.output((r) => {
|
|
1260
|
-
const c =
|
|
1261
|
-
return r.
|
|
1236
|
+
const { id: c } = v(Node)
|
|
1237
|
+
return r.idb(seeded, { c }).find({ c })
|
|
1262
1238
|
})
|
|
1263
1239
|
})
|
|
1264
1240
|
const reachPrepared = db.prepare(reach)
|
|
1265
1241
|
```
|
|
1266
1242
|
|
|
1243
|
+
The complement is one `r.not` away — negation **of** a finished stratum is
|
|
1244
|
+
engine-legal (the strata judge refuses only negation *through* a cycle), so
|
|
1245
|
+
"every node the closure never reached" runs in-plan through the engine's
|
|
1246
|
+
anti-probe, never as a host-side set difference — the same program with the
|
|
1247
|
+
output rule:
|
|
1248
|
+
|
|
1249
|
+
```ts
|
|
1250
|
+
const unreached = program(Closure, (p) => {
|
|
1251
|
+
const rec = p.rec("reach")
|
|
1252
|
+
const seeded = rec
|
|
1253
|
+
.rule((r) => {
|
|
1254
|
+
const { id: c } = v(Node)
|
|
1255
|
+
return r
|
|
1256
|
+
.match(Node, { id: c })
|
|
1257
|
+
.where(eq(c, r.param("root")))
|
|
1258
|
+
.find({ c })
|
|
1259
|
+
})
|
|
1260
|
+
.rule((r) => {
|
|
1261
|
+
const { child: c, parent } = v(Parent)
|
|
1262
|
+
return r
|
|
1263
|
+
.match(Parent, { child: c, parent })
|
|
1264
|
+
.idb(rec, { c: parent })
|
|
1265
|
+
.find({ c })
|
|
1266
|
+
})
|
|
1267
|
+
return p.output((r) => {
|
|
1268
|
+
const { id: c } = v(Node)
|
|
1269
|
+
return r
|
|
1270
|
+
.match(Node, { id: c })
|
|
1271
|
+
.where(r.not(seeded, { c }))
|
|
1272
|
+
.find({ c })
|
|
1273
|
+
})
|
|
1274
|
+
})
|
|
1275
|
+
```
|
|
1276
|
+
|
|
1267
1277
|
(the test drives both dialects and asserts the same reachable sets, root for
|
|
1268
1278
|
root). What stays host-side is the **chain-window class** — interval
|
|
1269
1279
|
intersection along paths — which the recursion surface fences out: the idiom
|
|
@@ -1300,15 +1310,14 @@ const Accounts = schema("Accounts", { Account, AccountParent, Posting }, [
|
|
|
1300
1310
|
// The two queries the host rollup composes:
|
|
1301
1311
|
// the frontier step (recipe 24's loop, verbatim):
|
|
1302
1312
|
const frontierStep = query(Accounts).rule((r) => {
|
|
1303
|
-
const c =
|
|
1304
|
-
return r.match(AccountParent, { child: c, parent: r.inSet("frontier") }).
|
|
1313
|
+
const { child: c } = v(AccountParent)
|
|
1314
|
+
return r.match(AccountParent, { child: c, parent: r.inSet("frontier") }).find({ c })
|
|
1305
1315
|
})
|
|
1306
1316
|
// the rollup over the accumulated subtree (bind the fresh id — recipe
|
|
1307
1317
|
// 19's discipline, spent again; equal postings to one account both count):
|
|
1308
1318
|
const subtreeRollup = query(Accounts).rule((r) => {
|
|
1309
|
-
const id =
|
|
1310
|
-
|
|
1311
|
-
return r.match(Posting, { id, account: r.inSet("subtree"), minor }).select(r.sum("minor"))
|
|
1319
|
+
const { id, minor } = v(Posting)
|
|
1320
|
+
return r.match(Posting, { id, account: r.inSet("subtree"), minor }).find({ total: r.sum(minor) })
|
|
1312
1321
|
})
|
|
1313
1322
|
// The engine-native form: the closure stratum converges first, then the
|
|
1314
1323
|
// output's fold runs once over the finished subtree.
|
|
@@ -1316,28 +1325,25 @@ const nativeRollup = program(Accounts, (p) => {
|
|
|
1316
1325
|
const sub = p.rec("sub")
|
|
1317
1326
|
const seeded = sub
|
|
1318
1327
|
.rule((r) => {
|
|
1319
|
-
const a =
|
|
1328
|
+
const { id: a } = v(Account)
|
|
1320
1329
|
return r
|
|
1321
1330
|
.match(Account, { id: a })
|
|
1322
1331
|
.where(eq(a, r.param("root")))
|
|
1323
|
-
.
|
|
1332
|
+
.find({ a })
|
|
1324
1333
|
})
|
|
1325
1334
|
.rule((r) => {
|
|
1326
|
-
const a =
|
|
1327
|
-
const parent = r.var("parent")
|
|
1335
|
+
const { child: a, parent } = v(AccountParent)
|
|
1328
1336
|
return r
|
|
1329
1337
|
.match(AccountParent, { child: a, parent })
|
|
1330
|
-
.idb(sub, parent)
|
|
1331
|
-
.
|
|
1338
|
+
.idb(sub, { a: parent })
|
|
1339
|
+
.find({ a })
|
|
1332
1340
|
})
|
|
1333
1341
|
return p.output((r) => {
|
|
1334
|
-
const id =
|
|
1335
|
-
const a = r.var("a")
|
|
1336
|
-
const minor = r.var("minor")
|
|
1342
|
+
const { id, account: a, minor } = v(Posting)
|
|
1337
1343
|
return r
|
|
1338
1344
|
.match(Posting, { id, account: a, minor })
|
|
1339
|
-
.idb(seeded, a)
|
|
1340
|
-
.
|
|
1345
|
+
.idb(seeded, { a })
|
|
1346
|
+
.find({ total: r.sum(minor) })
|
|
1341
1347
|
})
|
|
1342
1348
|
})
|
|
1343
1349
|
```
|
|
@@ -1403,10 +1409,8 @@ const MaintainedRollup = schema("MaintainedRollup", { Arm, Claim, BusySpan }, [
|
|
|
1403
1409
|
|
|
1404
1410
|
// Derive the desired rollup on the maintenance snapshot:
|
|
1405
1411
|
const deriving = query(MaintainedRollup).rule((r) => {
|
|
1406
|
-
const source =
|
|
1407
|
-
|
|
1408
|
-
const span = r.var("span")
|
|
1409
|
-
return r.match(Claim, { source, person, arm: "Busy", span }).select("person", r.pack("span"))
|
|
1412
|
+
const { source, person, span } = v(Claim)
|
|
1413
|
+
return r.match(Claim, { source, person, arm: "Busy", span }).find({ person, packed: r.pack(span) })
|
|
1410
1414
|
})
|
|
1411
1415
|
```
|
|
1412
1416
|
|
|
@@ -1459,15 +1463,13 @@ const Payroll = schema("Payroll", { Employee, Salary }, [
|
|
|
1459
1463
|
|
|
1460
1464
|
// The post-migration read — salaries in force at an instant:
|
|
1461
1465
|
const inForceAt = query(Payroll).rule((r) => {
|
|
1462
|
-
const e =
|
|
1463
|
-
const
|
|
1464
|
-
const amount = r.var("amount")
|
|
1465
|
-
const w = r.var("w")
|
|
1466
|
+
const { id: e, name } = v(Employee)
|
|
1467
|
+
const { amount, applies: w } = v(Salary)
|
|
1466
1468
|
return r
|
|
1467
1469
|
.match(Employee, { id: e, name })
|
|
1468
1470
|
.match(Salary, { employee: e, amount, applies: w })
|
|
1469
1471
|
.where(pointIn(r.param("at"), w))
|
|
1470
|
-
.
|
|
1472
|
+
.find({ name, amount })
|
|
1471
1473
|
})
|
|
1472
1474
|
```
|
|
1473
1475
|
|
|
@@ -1476,7 +1478,9 @@ the fingerprint refusal, load order, identity, mint catch-up, judgment); the
|
|
|
1476
1478
|
SDK pin asserts the two theories carry two distinct fingerprints — the
|
|
1477
1479
|
refusal's premise. For stores whose creating schema is gone, `Db.exhume`
|
|
1478
1480
|
reads the store's own persisted descriptor — the one schema-independent read
|
|
1479
|
-
path, the E half of ETL.
|
|
1481
|
+
path, the E half of ETL. Its lifetime is a `using` scope (`using exhumed =
|
|
1482
|
+
await Db.exhume(oldPath)`): disposal releases the store's exclusive lock at
|
|
1483
|
+
scope exit, so the rebirth loop's same-path retry never waits on GC.
|
|
1480
1484
|
|
|
1481
1485
|
## Composition
|
|
1482
1486
|
|