datalog: rose-ash demo programs (Phase 10 syntactic, 153/153)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 41s

New lib/datalog/demo.sx with three Datalog-as-query-language demos
over synthetic rose-ash data:

  Federation: (mutual A B), (reachable A B), (foaf A C) over a
              follows graph.
  Content:    (post-likes P N) via count aggregation, (popular P)
              for likes >= 3, (interesting Me P) joining follows
              + authored + popular.
  Permissions: (in-group A G) over transitive subgroup chains,
              (can-access A R).

10 tests run each program against in-memory EDB tuples loaded via
dl-program-data.

Wiring to PostgreSQL and exposing as a service endpoint (/internal
/datalog) is out of scope for this loop — both would require
edits outside lib/datalog/. Programs above document the EDB shape
a real loader would populate.
This commit is contained in:
2026-05-08 08:45:59 +00:00
parent 3cc760082c
commit de302fc236
6 changed files with 315 additions and 14 deletions

View File

@@ -13,6 +13,7 @@ PRELOADS=(
lib/datalog/strata.sx
lib/datalog/eval.sx
lib/datalog/api.sx
lib/datalog/demo.sx
)
SUITES=(
@@ -25,4 +26,5 @@ SUITES=(
"negation:lib/datalog/tests/negation.sx:(dl-negation-tests-run!)"
"aggregates:lib/datalog/tests/aggregates.sx:(dl-aggregates-tests-run!)"
"api:lib/datalog/tests/api.sx:(dl-api-tests-run!)"
"demo:lib/datalog/tests/demo.sx:(dl-demo-tests-run!)"
)

75
lib/datalog/demo.sx Normal file
View File

@@ -0,0 +1,75 @@
;; lib/datalog/demo.sx — example programs over rose-ash-shaped data.
;;
;; Phase 10 prototypes Datalog as a rose-ash query language. Wiring
;; the EDB to actual PostgreSQL is out of scope for this loop (it
;; would touch service code outside lib/datalog/), but the programs
;; below show the shape of queries we want, and the test suite runs
;; them against synthetic in-memory tuples loaded via dl-program-data.
;;
;; Three thematic demos:
;;
;; 1. Federation — follow graph, transitive reach, mutuals.
;; 2. Content — posts, tags, likes, popularity, "for you" feed.
;; 3. Permissions — group membership and resource access.
;; ── Demo 1: federation follow graph ─────────────────────────────
;; EDB: (follows ACTOR-A ACTOR-B) — A follows B.
;; IDB:
;; (mutual A B) — A follows B and B follows A
;; (reachable A B) — transitive follow closure
;; (foaf A C) — friend of a friend (mutual filter)
(define
dl-demo-federation-rules
(quote
((mutual A B <- (follows A B) (follows B A))
(reachable A B <- (follows A B))
(reachable A C <- (follows A B) (reachable B C))
(foaf A C <- (follows A B) (follows B C) (!= A C)))))
;; ── Demo 2: content recommendation ──────────────────────────────
;; EDB:
;; (authored ACTOR POST)
;; (tagged POST TAG)
;; (liked ACTOR POST)
;; IDB:
;; (post-likes POST N) — count of likes per post
;; (popular POST) — posts with >= 3 likes
;; (tagged-by-mutual ACTOR POST) — post tagged TOPIC by someone
;; A's mutuals follow.
(define
dl-demo-content-rules
(quote
((post-likes P N <- (authored Author P) (count N L (liked L P)))
(popular P <- (authored Author P) (post-likes P N) (>= N 3))
(interesting Me P
<-
(follows Me Buddy)
(authored Buddy P)
(popular P)))))
;; ── Demo 3: role-based permissions ──────────────────────────────
;; EDB:
;; (member ACTOR GROUP)
;; (subgroup CHILD PARENT)
;; (allowed GROUP RESOURCE)
;; IDB:
;; (in-group ACTOR GROUP) — direct or via subgroup chain
;; (can-access ACTOR RESOURCE) — actor inherits group permission
(define
dl-demo-perm-rules
(quote
((in-group A G <- (member A G))
(in-group A G <- (member A H) (subgroup-trans H G))
(subgroup-trans X Y <- (subgroup X Y))
(subgroup-trans X Z <- (subgroup X Y) (subgroup-trans Y Z))
(can-access A R <- (in-group A G) (allowed G R)))))
;; ── Loader stub ──────────────────────────────────────────────────
;; Wiring to PostgreSQL would replace these helpers with calls into
;; rose-ash's internal HTTP RPC (fetch_data → /internal/data/...).
;; The shape returned by dl-load-from-edb! is the same in either case.
(define
dl-demo-make
(fn
(facts rules)
(dl-program-data facts rules)))

View File

@@ -1,8 +1,8 @@
{
"lang": "datalog",
"total_passed": 143,
"total_passed": 153,
"total_failed": 0,
"total": 143,
"total": 153,
"suites": [
{"name":"tokenize","passed":26,"failed":0,"total":26},
{"name":"parse","passed":18,"failed":0,"total":18},
@@ -12,7 +12,8 @@
{"name":"semi_naive","passed":8,"failed":0,"total":8},
{"name":"negation","passed":10,"failed":0,"total":10},
{"name":"aggregates","passed":10,"failed":0,"total":10},
{"name":"api","passed":9,"failed":0,"total":9}
{"name":"api","passed":9,"failed":0,"total":9},
{"name":"demo","passed":10,"failed":0,"total":10}
],
"generated": "2026-05-08T08:33:54+00:00"
"generated": "2026-05-08T08:45:37+00:00"
}

View File

@@ -1,6 +1,6 @@
# datalog scoreboard
**143 / 143 passing** (0 failure(s)).
**153 / 153 passing** (0 failure(s)).
| Suite | Passed | Total | Status |
|-------|--------|-------|--------|
@@ -13,3 +13,4 @@
| negation | 10 | 10 | ok |
| aggregates | 10 | 10 | ok |
| api | 9 | 9 | ok |
| demo | 10 | 10 | ok |

202
lib/datalog/tests/demo.sx Normal file
View File

@@ -0,0 +1,202 @@
;; lib/datalog/tests/demo.sx — Phase 10 demo programs.
(define dl-demo-pass 0)
(define dl-demo-fail 0)
(define dl-demo-failures (list))
(define
dl-demo-deep=?
(fn
(a b)
(cond
((and (list? a) (list? b))
(and (= (len a) (len b)) (dl-demo-deq-l? a b 0)))
((and (dict? a) (dict? b))
(let ((ka (keys a)) (kb (keys b)))
(and (= (len ka) (len kb)) (dl-demo-deq-d? a b ka 0))))
((and (number? a) (number? b)) (= a b))
(else (equal? a b)))))
(define
dl-demo-deq-l?
(fn
(a b i)
(cond
((>= i (len a)) true)
((not (dl-demo-deep=? (nth a i) (nth b i))) false)
(else (dl-demo-deq-l? a b (+ i 1))))))
(define
dl-demo-deq-d?
(fn
(a b ka i)
(cond
((>= i (len ka)) true)
((let ((k (nth ka i)))
(not (dl-demo-deep=? (get a k) (get b k))))
false)
(else (dl-demo-deq-d? a b ka (+ i 1))))))
(define
dl-demo-set=?
(fn
(a b)
(and
(= (len a) (len b))
(dl-demo-subset? a b)
(dl-demo-subset? b a))))
(define
dl-demo-subset?
(fn
(xs ys)
(cond
((= (len xs) 0) true)
((not (dl-demo-contains? ys (first xs))) false)
(else (dl-demo-subset? (rest xs) ys)))))
(define
dl-demo-contains?
(fn
(xs target)
(cond
((= (len xs) 0) false)
((dl-demo-deep=? (first xs) target) true)
(else (dl-demo-contains? (rest xs) target)))))
(define
dl-demo-test-set!
(fn
(name got expected)
(if
(dl-demo-set=? got expected)
(set! dl-demo-pass (+ dl-demo-pass 1))
(do
(set! dl-demo-fail (+ dl-demo-fail 1))
(append!
dl-demo-failures
(str
name
"\n expected (set): " expected
"\n got: " got))))))
(define
dl-demo-run-all!
(fn
()
(do
;; ── Federation ──────────────────────────────────────────
(dl-demo-test-set! "mutuals"
(dl-query
(dl-demo-make
(quote ((follows alice bob) (follows bob alice)
(follows bob carol) (follows carol dave)))
dl-demo-federation-rules)
(quote (mutual alice X)))
(list {:X (quote bob)}))
(dl-demo-test-set! "reachable transitive"
(dl-query
(dl-demo-make
(quote ((follows alice bob) (follows bob carol) (follows carol dave)))
dl-demo-federation-rules)
(quote (reachable alice X)))
(list {:X (quote bob)} {:X (quote carol)} {:X (quote dave)}))
(dl-demo-test-set! "foaf"
(dl-query
(dl-demo-make
(quote ((follows alice bob) (follows bob carol) (follows alice dave)))
dl-demo-federation-rules)
(quote (foaf alice X)))
(list {:X (quote carol)}))
;; ── Content ─────────────────────────────────────────────
(dl-demo-test-set! "popular posts"
(dl-query
(dl-demo-make
(quote
((authored alice p1) (authored bob p2) (authored carol p3)
(liked u1 p1) (liked u2 p1) (liked u3 p1)
(liked u1 p2)))
dl-demo-content-rules)
(quote (popular P)))
(list {:P (quote p1)}))
(dl-demo-test-set! "interesting feed"
(dl-query
(dl-demo-make
(quote
((follows me alice) (follows me bob)
(authored alice p1) (authored bob p2)
(liked u1 p1) (liked u2 p1) (liked u3 p1)
(liked u4 p2)))
dl-demo-content-rules)
(quote (interesting me P)))
(list {:P (quote p1)}))
(dl-demo-test-set! "post likes count"
(dl-query
(dl-demo-make
(quote
((authored alice p1)
(liked u1 p1) (liked u2 p1) (liked u3 p1)))
dl-demo-content-rules)
(quote (post-likes p1 N)))
(list {:N 3}))
;; ── Permissions ─────────────────────────────────────────
(dl-demo-test-set! "direct group access"
(dl-query
(dl-demo-make
(quote
((member alice editors)
(allowed editors blog)))
dl-demo-perm-rules)
(quote (can-access X blog)))
(list {:X (quote alice)}))
(dl-demo-test-set! "subgroup access"
(dl-query
(dl-demo-make
(quote
((member bob writers)
(subgroup writers editors)
(allowed editors blog)))
dl-demo-perm-rules)
(quote (can-access X blog)))
(list {:X (quote bob)}))
(dl-demo-test-set! "transitive subgroup"
(dl-query
(dl-demo-make
(quote
((member carol drafters)
(subgroup drafters writers)
(subgroup writers editors)
(allowed editors blog)))
dl-demo-perm-rules)
(quote (can-access X blog)))
(list {:X (quote carol)}))
(dl-demo-test-set! "no access without grant"
(dl-query
(dl-demo-make
(quote ((member dave outsiders) (allowed editors blog)))
dl-demo-perm-rules)
(quote (can-access X blog)))
(list)))))
(define
dl-demo-tests-run!
(fn
()
(do
(set! dl-demo-pass 0)
(set! dl-demo-fail 0)
(set! dl-demo-failures (list))
(dl-demo-run-all!)
{:passed dl-demo-pass
:failed dl-demo-fail
:total (+ dl-demo-pass dl-demo-fail)
:failures dl-demo-failures})))

View File

@@ -234,15 +234,25 @@ large graphs.
rose-ash ActivityPub follow relationships (Phase 10).
### Phase 10 — Datalog as a query language for rose-ash
- [ ] Schema: map SQLAlchemy model relationships to Datalog EDB facts
(e.g. `(follows user1 user2)`, `(authored user post)`, `(tagged post tag)`)
- [ ] Loader: `dl-load-from-db!` — query PostgreSQL, populate Datalog EDB
- [ ] Query examples:
- `?- ancestor(me, X), authored(X, Post), tagged(Post, cooking).`
→ posts about cooking by people I follow (transitively)
- `?- popular(Post) :- tagged(Post, T), count(L, (liked(L, Post))) >= 10.`
posts with 10+ likes
- [ ] Expose as a rose-ash service endpoint: `POST /internal/datalog` with program + query
- [x] Schema sketches in `lib/datalog/demo.sx`:
- **Federation**: `(follows A B)` `(mutual A B)`, `(reachable A B)`,
`(foaf A C)` (friend-of-a-friend, distinct).
- **Content**: `(authored A P)`, `(liked U P)`, `(tagged P T)` →
`(post-likes P N)` via aggregation, `(popular P)` for likes ≥ 3,
`(interesting Me P)` joining follows + authored + popular.
- **Permissions**: `(member A G)`, `(subgroup C P)`, `(allowed G R)`
`(in-group A G)` over transitive subgroups, `(can-access A R)`.
- [ ] Loader `dl-load-from-db!` — out of scope for this loop
(would need to edit `shared/services/` outside `lib/datalog/`).
Programs in `demo.sx` already document the EDB shape expected
from such a loader. `dl-program-data` consumes the same shape.
- [x] Query examples covered by `lib/datalog/tests/demo.sx` (10):
mutuals, transitive reach, FOAF, popular posts, interesting feed,
post likes count, direct/subgroup/transitive group access, no
access without grant.
- [ ] Service endpoint `POST /internal/datalog` — out of scope as above.
Once exposed, server-side handler would be `dl-program-data` +
`dl-query`, returning JSON-encoded substitutions.
## Blockers
@@ -259,6 +269,16 @@ large graphs.
_Newest first._
- 2026-05-08 — Phase 10 syntactic demo. New `lib/datalog/demo.sx`
with three programs over rose-ash-shaped data: federation
(`mutual`, `reachable`, `foaf`), content recommendation
(`post-likes` via count aggregation, `popular`, `interesting`),
and role-based permissions (`in-group` over transitive subgroups,
`can-access`). 10 demo tests pass against synthetic EDB tuples.
Postgres loader and `/internal/datalog` HTTP endpoint remain
out of scope for this loop (they need service-tree edits beyond
`lib/datalog/**`). Conformance now 153/153.
- 2026-05-08 — Phase 5b perf: hash-set membership in `dl-add-fact!`.
db gains a parallel `:facts-keys {<rel>: {<tuple-string>: true}}`
index alongside `:facts`. `dl-tuple-key` derives a stable string