From de302fc2367342218ebd29c446b7c3cad67bdd90 Mon Sep 17 00:00:00 2001 From: giles Date: Fri, 8 May 2026 08:45:59 +0000 Subject: [PATCH] datalog: rose-ash demo programs (Phase 10 syntactic, 153/153) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- lib/datalog/conformance.conf | 2 + lib/datalog/demo.sx | 75 +++++++++++++ lib/datalog/scoreboard.json | 9 +- lib/datalog/scoreboard.md | 3 +- lib/datalog/tests/demo.sx | 202 +++++++++++++++++++++++++++++++++++ plans/datalog-on-sx.md | 38 +++++-- 6 files changed, 315 insertions(+), 14 deletions(-) create mode 100644 lib/datalog/demo.sx create mode 100644 lib/datalog/tests/demo.sx diff --git a/lib/datalog/conformance.conf b/lib/datalog/conformance.conf index e80e8b7a..f56276af 100644 --- a/lib/datalog/conformance.conf +++ b/lib/datalog/conformance.conf @@ -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!)" ) diff --git a/lib/datalog/demo.sx b/lib/datalog/demo.sx new file mode 100644 index 00000000..7bbdc3b2 --- /dev/null +++ b/lib/datalog/demo.sx @@ -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))) diff --git a/lib/datalog/scoreboard.json b/lib/datalog/scoreboard.json index 1bd89123..c6eba8b4 100644 --- a/lib/datalog/scoreboard.json +++ b/lib/datalog/scoreboard.json @@ -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" } diff --git a/lib/datalog/scoreboard.md b/lib/datalog/scoreboard.md index 66ade082..2b5b5d7e 100644 --- a/lib/datalog/scoreboard.md +++ b/lib/datalog/scoreboard.md @@ -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 | diff --git a/lib/datalog/tests/demo.sx b/lib/datalog/tests/demo.sx new file mode 100644 index 00000000..d8218266 --- /dev/null +++ b/lib/datalog/tests/demo.sx @@ -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}))) diff --git a/plans/datalog-on-sx.md b/plans/datalog-on-sx.md index b9990c17..57edf541 100644 --- a/plans/datalog-on-sx.md +++ b/plans/datalog-on-sx.md @@ -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 {: {: true}}` index alongside `:facts`. `dl-tuple-key` derives a stable string