@bjornpagen/bumbledb 0.5.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.
Files changed (45) hide show
  1. package/COOKBOOK.md +96 -131
  2. package/README.md +9 -9
  3. package/dist/db.js +2 -2
  4. package/dist/db.js.map +1 -1
  5. package/dist/index.d.ts +8 -5
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +5 -2
  8. package/dist/index.js.map +1 -1
  9. package/dist/query/atom.d.ts +132 -201
  10. package/dist/query/atom.d.ts.map +1 -1
  11. package/dist/query/atom.js +30 -42
  12. package/dist/query/atom.js.map +1 -1
  13. package/dist/query/find.d.ts +116 -0
  14. package/dist/query/find.d.ts.map +1 -0
  15. package/dist/query/{select.js → find.js} +22 -22
  16. package/dist/query/find.js.map +1 -0
  17. package/dist/query/lower.d.ts +123 -158
  18. package/dist/query/lower.d.ts.map +1 -1
  19. package/dist/query/lower.js +437 -493
  20. package/dist/query/lower.js.map +1 -1
  21. package/dist/query/predicate.d.ts +22 -14
  22. package/dist/query/predicate.d.ts.map +1 -1
  23. package/dist/query/predicate.js +35 -42
  24. package/dist/query/predicate.js.map +1 -1
  25. package/dist/query/run.d.ts +3 -3
  26. package/dist/query/run.d.ts.map +1 -1
  27. package/dist/query/run.js +9 -9
  28. package/dist/query/run.js.map +1 -1
  29. package/dist/query/scope.d.ts +127 -72
  30. package/dist/query/scope.d.ts.map +1 -1
  31. package/dist/query/scope.js +80 -33
  32. package/dist/query/scope.js.map +1 -1
  33. package/package.json +2 -2
  34. package/src/db.ts +4 -4
  35. package/src/index.ts +11 -7
  36. package/src/query/atom.ts +181 -263
  37. package/src/query/find.ts +212 -0
  38. package/src/query/lower.ts +588 -722
  39. package/src/query/predicate.ts +37 -45
  40. package/src/query/run.ts +10 -10
  41. package/src/query/scope.ts +172 -87
  42. package/dist/query/select.d.ts +0 -128
  43. package/dist/query/select.d.ts.map +0 -1
  44. package/dist/query/select.js.map +0 -1
  45. 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 — `r.var` names each variable (typed by the field it
122
- // first binds), and shorthand punning binds same-named columns:
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 = r.var("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
- .select("service")
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 = r.var("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
- .select("service", "window")
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 = r.var("service")
143
- const window = r.var("window")
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 = r.var("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
- .select("b")
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 = r.var("id")
264
- const account = r.var("account")
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 = r.var("id")
304
- return r.match(Document, { id, payload: r.param("digest") }).select("id")
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 = r.var("t")
346
- return r.match(Ticket, { id: t, priority: "Urgent" }).select("t")
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 = r.var("t")
355
- return r.match(Ticket, { id: t, priority: ["Normal", "Urgent"] }).select("t")
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 = r.var("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
- .select("a")
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 = r.var("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
- .select("i")
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 = r.var("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
- .select("track")
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 = r.var("l")
572
- const v = r.var("v")
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: v })
576
- .select("v")
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 = r.var("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
- .select("a", "b")
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 = 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")
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
- .select("entity", "x", "y", "dx", "dy")
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 = r.var("id")
691
- const carrier = r.var("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
- .select("id", "carrier")
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 = r.var("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
- .select("room", "span")
753
+ .find({ room, span })
767
754
  })
768
755
  const personLoad = query(Calendar).rule((r) => {
769
- const person = r.var("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
- .select("person", "span")
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 = r.var("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
- .select("rate_bps")
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 = r.var("p")
816
- const a = r.var("a")
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
- .select("a", "b")
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 = r.var("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
- .select("seq")
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 = r.var("reg")
901
- const b = r.var("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
- .select("rate_bps")
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 = r.var("person")
935
- const span = r.var("span")
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 = r.var("person")
941
- const span = r.var("span")
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 = r.var("id")
983
- const account = r.var("account")
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 = r.var("id")
990
- const entry = r.var("entry")
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
 
@@ -1024,9 +1000,8 @@ const Jobs = schema("Jobs", { State, Job, Lease }, [
1024
1000
 
1025
1001
  // update-where's premise — "still Queued" is the witness:
1026
1002
  const stillQueued = query(Jobs).rule((r) => {
1027
- const id = r.var("id")
1028
- const payload = r.var("payload")
1029
- return r.match(Job, { id, state: "Queued", payload }).select("id", "payload")
1003
+ const { id, payload } = v(Job)
1004
+ return r.match(Job, { id, state: "Queued", payload }).find({ id, payload })
1030
1005
  })
1031
1006
 
1032
1007
  const db = await Db.create("./jobs.db", Jobs)
@@ -1082,9 +1057,8 @@ const Rollup = schema("Rollup", { Arm, Claim, BusySpan }, [
1082
1057
  // against sources it didn't actually read. The deriving query (pack IS the
1083
1058
  // coalesce):
1084
1059
  const deriving = query(Rollup).rule((r) => {
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"))
1060
+ const { person, span } = v(Claim)
1061
+ return r.match(Claim, { person, span, arm: "Busy" }).find({ person, packed: r.pack(span) })
1088
1062
  })
1089
1063
  ```
1090
1064
 
@@ -1117,20 +1091,20 @@ const Payments = schema("Payments", { Kind, Payment, Card, Ach }, [
1117
1091
  // provably disjoint, so the executor elides cross-rule dedup — the free lunch.
1118
1092
  const wholeDu = query(Payments)
1119
1093
  .rule((r) => {
1120
- const id = r.var("id")
1121
- const n = r.var("n")
1094
+ const { id } = v(Payment)
1095
+ const { last4: n } = v(Card)
1122
1096
  return r
1123
1097
  .match(Payment, { id, kind: "Card" })
1124
1098
  .match(Card, { payment: id, last4: n })
1125
- .select("id", "n")
1099
+ .find({ id, n })
1126
1100
  })
1127
1101
  .rule((r) => {
1128
- const id = r.var("id")
1129
- const n = r.var("n")
1102
+ const { id } = v(Payment)
1103
+ const { routing: n } = v(Ach)
1130
1104
  return r
1131
1105
  .match(Payment, { id, kind: "Ach" })
1132
1106
  .match(Ach, { payment: id, routing: n })
1133
- .select("id", "n")
1107
+ .find({ id, n })
1134
1108
  })
1135
1109
  ```
1136
1110
 
@@ -1198,8 +1172,8 @@ const Closure = schema("Closure", { Node, Parent }, [
1198
1172
 
1199
1173
  // The loop's one query — the frontier's children, one ∈-set probe:
1200
1174
  const step = query(Closure).rule((r) => {
1201
- const c = r.var("c")
1202
- return r.match(Parent, { child: c, parent: r.inSet("frontier") }).select("c")
1175
+ const { child: c } = v(Parent)
1176
+ return r.match(Parent, { child: c, parent: r.inSet("frontier") }).find({ c })
1203
1177
  })
1204
1178
  ```
1205
1179
 
@@ -1242,23 +1216,22 @@ const reach = program(Closure, (p) => {
1242
1216
  const rec = p.rec("reach")
1243
1217
  const seeded = rec
1244
1218
  .rule((r) => {
1245
- const c = r.var("c")
1219
+ const { id: c } = v(Node)
1246
1220
  return r
1247
1221
  .match(Node, { id: c })
1248
1222
  .where(eq(c, r.param("root")))
1249
- .select("c")
1223
+ .find({ c })
1250
1224
  })
1251
1225
  .rule((r) => {
1252
- const c = r.var("c")
1253
- const parent = r.var("parent")
1226
+ const { child: c, parent } = v(Parent)
1254
1227
  return r
1255
1228
  .match(Parent, { child: c, parent })
1256
- .idb(rec, parent)
1257
- .select("c")
1229
+ .idb(rec, { c: parent })
1230
+ .find({ c })
1258
1231
  })
1259
1232
  return p.output((r) => {
1260
- const c = r.var("c")
1261
- return r.match(Node, { id: c }).idb(seeded, c).select("c")
1233
+ const { id: c } = v(Node)
1234
+ return r.match(Node, { id: c }).idb(seeded, { c }).find({ c })
1262
1235
  })
1263
1236
  })
1264
1237
  const reachPrepared = db.prepare(reach)
@@ -1300,15 +1273,14 @@ const Accounts = schema("Accounts", { Account, AccountParent, Posting }, [
1300
1273
  // The two queries the host rollup composes:
1301
1274
  // the frontier step (recipe 24's loop, verbatim):
1302
1275
  const frontierStep = query(Accounts).rule((r) => {
1303
- const c = r.var("c")
1304
- return r.match(AccountParent, { child: c, parent: r.inSet("frontier") }).select("c")
1276
+ const { child: c } = v(AccountParent)
1277
+ return r.match(AccountParent, { child: c, parent: r.inSet("frontier") }).find({ c })
1305
1278
  })
1306
1279
  // the rollup over the accumulated subtree (bind the fresh id — recipe
1307
1280
  // 19's discipline, spent again; equal postings to one account both count):
1308
1281
  const subtreeRollup = query(Accounts).rule((r) => {
1309
- const id = r.var("id")
1310
- const minor = r.var("minor")
1311
- return r.match(Posting, { id, account: r.inSet("subtree"), minor }).select(r.sum("minor"))
1282
+ const { id, minor } = v(Posting)
1283
+ return r.match(Posting, { id, account: r.inSet("subtree"), minor }).find({ total: r.sum(minor) })
1312
1284
  })
1313
1285
  // The engine-native form: the closure stratum converges first, then the
1314
1286
  // output's fold runs once over the finished subtree.
@@ -1316,28 +1288,25 @@ const nativeRollup = program(Accounts, (p) => {
1316
1288
  const sub = p.rec("sub")
1317
1289
  const seeded = sub
1318
1290
  .rule((r) => {
1319
- const a = r.var("a")
1291
+ const { id: a } = v(Account)
1320
1292
  return r
1321
1293
  .match(Account, { id: a })
1322
1294
  .where(eq(a, r.param("root")))
1323
- .select("a")
1295
+ .find({ a })
1324
1296
  })
1325
1297
  .rule((r) => {
1326
- const a = r.var("a")
1327
- const parent = r.var("parent")
1298
+ const { child: a, parent } = v(AccountParent)
1328
1299
  return r
1329
1300
  .match(AccountParent, { child: a, parent })
1330
- .idb(sub, parent)
1331
- .select("a")
1301
+ .idb(sub, { a: parent })
1302
+ .find({ a })
1332
1303
  })
1333
1304
  return p.output((r) => {
1334
- const id = r.var("id")
1335
- const a = r.var("a")
1336
- const minor = r.var("minor")
1305
+ const { id, account: a, minor } = v(Posting)
1337
1306
  return r
1338
1307
  .match(Posting, { id, account: a, minor })
1339
- .idb(seeded, a)
1340
- .select(r.sum("minor"))
1308
+ .idb(seeded, { a })
1309
+ .find({ total: r.sum(minor) })
1341
1310
  })
1342
1311
  })
1343
1312
  ```
@@ -1403,10 +1372,8 @@ const MaintainedRollup = schema("MaintainedRollup", { Arm, Claim, BusySpan }, [
1403
1372
 
1404
1373
  // Derive the desired rollup on the maintenance snapshot:
1405
1374
  const deriving = query(MaintainedRollup).rule((r) => {
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"))
1375
+ const { source, person, span } = v(Claim)
1376
+ return r.match(Claim, { source, person, arm: "Busy", span }).find({ person, packed: r.pack(span) })
1410
1377
  })
1411
1378
  ```
1412
1379
 
@@ -1459,15 +1426,13 @@ const Payroll = schema("Payroll", { Employee, Salary }, [
1459
1426
 
1460
1427
  // The post-migration read — salaries in force at an instant:
1461
1428
  const inForceAt = query(Payroll).rule((r) => {
1462
- const e = r.var("e")
1463
- const name = r.var("name")
1464
- const amount = r.var("amount")
1465
- const w = r.var("w")
1429
+ const { id: e, name } = v(Employee)
1430
+ const { amount, applies: w } = v(Salary)
1466
1431
  return r
1467
1432
  .match(Employee, { id: e, name })
1468
1433
  .match(Salary, { employee: e, amount, applies: w })
1469
1434
  .where(pointIn(r.param("at"), w))
1470
- .select("name", "amount")
1435
+ .find({ name, amount })
1471
1436
  })
1472
1437
  ```
1473
1438
 
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,18 +75,18 @@ if (!result.ok) {
75
75
  }
76
76
  }
77
77
 
78
- // Query: Datalog as values. Vars are named and typed by the class of their
79
- // first binding; params are typed by use; rows are typed from the select.
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 = r.var("a")
83
- const k = r.var("k")
84
- const rank = r.var("rank")
83
+ const { attempt: a, kind: k } = v(Certificate)
84
+ const { rank } = v(Kind)
85
85
  return r
86
86
  .match(Certificate, { attempt: a, kind: k })
87
- .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
88
88
  .where(gt(rank, r.param("floor")))
89
- .select("a", "rank")
89
+ .find({ a, rank })
90
90
  })
91
91
 
92
92
  const prepared = db.prepare(certifiedAbove)
@@ -120,7 +120,7 @@ The drizzle law governs this surface: the SDK's job at the host boundary is tran
120
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.
121
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.
122
122
  - The `Db` runtime — `Db.create`/`Db.open`, path-cached stores, transactions, typed violations, scoped snapshot reads, the witnessed write loop with `abandon`.
123
- - The query surface — Datalog as values, `query(S).rule(r => ...)`: named vars, params typed by use, 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.
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.
124
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`).
125
125
 
126
126
  ## Cookbook
package/dist/db.js CHANGED
@@ -492,7 +492,7 @@ function openDb(handle, theory, manifest) {
492
492
  const rows = bridged("execute bumbledb prepared query", function callExecute() {
493
493
  return native.preparedExecute(plan.handle, state.handle, wire);
494
494
  });
495
- return decodeAnswers(plan.select, rows);
495
+ return decodeAnswers(plan.finds, rows);
496
496
  }
497
497
  const scope = Object.freeze({
498
498
  generation,
@@ -927,7 +927,7 @@ function openDb(handle, theory, manifest) {
927
927
  handle: preparedHandle,
928
928
  owner,
929
929
  params: q.data.params,
930
- select: q.data.select
930
+ finds: q.data.finds
931
931
  }));
932
932
  planReclaimer.register(prepared, preparedHandle);
933
933
  return prepared;