Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 50s
New lib/datalog/aggregates.sx: (count R V Goal), (sum R V Goal), (min R V Goal), (max R V Goal). dl-eval-aggregate runs dl-find-bindings on the goal under the outer subst, collects distinct values of V, applies the operator, binds R. Empty input: count/sum return 0; min/max produce no binding (rule fails). Group-by emerges naturally from outer-subst substitution into the goal — `popular(P) :- post(P), count(N, U, liked(U, P)), >=(N, 3).` counts per-post. Stratifier extended: dl-aggregate-dep-edge contributes a negation-like edge so the aggregate's goal relation is fully derived before the aggregate fires (non-monotonicity respected). Safety relaxed for aggregates: goal-internal vars are existentials, only the result var becomes bound.
224 lines
5.6 KiB
Plaintext
224 lines
5.6 KiB
Plaintext
;; lib/datalog/tests/aggregates.sx — count / sum / min / max.
|
|
|
|
(define dl-at-pass 0)
|
|
(define dl-at-fail 0)
|
|
(define dl-at-failures (list))
|
|
|
|
(define
|
|
dl-at-deep=?
|
|
(fn
|
|
(a b)
|
|
(cond
|
|
((and (list? a) (list? b))
|
|
(and (= (len a) (len b)) (dl-at-deq-l? a b 0)))
|
|
((and (dict? a) (dict? b))
|
|
(let ((ka (keys a)) (kb (keys b)))
|
|
(and (= (len ka) (len kb)) (dl-at-deq-d? a b ka 0))))
|
|
((and (number? a) (number? b)) (= a b))
|
|
(else (equal? a b)))))
|
|
|
|
(define
|
|
dl-at-deq-l?
|
|
(fn
|
|
(a b i)
|
|
(cond
|
|
((>= i (len a)) true)
|
|
((not (dl-at-deep=? (nth a i) (nth b i))) false)
|
|
(else (dl-at-deq-l? a b (+ i 1))))))
|
|
|
|
(define
|
|
dl-at-deq-d?
|
|
(fn
|
|
(a b ka i)
|
|
(cond
|
|
((>= i (len ka)) true)
|
|
((let ((k (nth ka i)))
|
|
(not (dl-at-deep=? (get a k) (get b k))))
|
|
false)
|
|
(else (dl-at-deq-d? a b ka (+ i 1))))))
|
|
|
|
(define
|
|
dl-at-set=?
|
|
(fn
|
|
(a b)
|
|
(and
|
|
(= (len a) (len b))
|
|
(dl-at-subset? a b)
|
|
(dl-at-subset? b a))))
|
|
|
|
(define
|
|
dl-at-subset?
|
|
(fn
|
|
(xs ys)
|
|
(cond
|
|
((= (len xs) 0) true)
|
|
((not (dl-at-contains? ys (first xs))) false)
|
|
(else (dl-at-subset? (rest xs) ys)))))
|
|
|
|
(define
|
|
dl-at-contains?
|
|
(fn
|
|
(xs target)
|
|
(cond
|
|
((= (len xs) 0) false)
|
|
((dl-at-deep=? (first xs) target) true)
|
|
(else (dl-at-contains? (rest xs) target)))))
|
|
|
|
(define
|
|
dl-at-test!
|
|
(fn
|
|
(name got expected)
|
|
(if
|
|
(dl-at-deep=? got expected)
|
|
(set! dl-at-pass (+ dl-at-pass 1))
|
|
(do
|
|
(set! dl-at-fail (+ dl-at-fail 1))
|
|
(append!
|
|
dl-at-failures
|
|
(str
|
|
name
|
|
"\n expected: " expected
|
|
"\n got: " got))))))
|
|
|
|
(define
|
|
dl-at-test-set!
|
|
(fn
|
|
(name got expected)
|
|
(if
|
|
(dl-at-set=? got expected)
|
|
(set! dl-at-pass (+ dl-at-pass 1))
|
|
(do
|
|
(set! dl-at-fail (+ dl-at-fail 1))
|
|
(append!
|
|
dl-at-failures
|
|
(str
|
|
name
|
|
"\n expected (set): " expected
|
|
"\n got: " got))))))
|
|
|
|
(define
|
|
dl-at-throws?
|
|
(fn
|
|
(thunk)
|
|
(let
|
|
((threw false))
|
|
(do
|
|
(guard
|
|
(e (#t (set! threw true)))
|
|
(thunk))
|
|
threw))))
|
|
|
|
(define
|
|
dl-at-run-all!
|
|
(fn
|
|
()
|
|
(do
|
|
;; count
|
|
(dl-at-test-set! "count siblings"
|
|
(dl-query
|
|
(dl-program
|
|
"parent(p, bob). parent(p, alice). parent(p, charlie).
|
|
sibling(X, Y) :- parent(P, X), parent(P, Y), !=(X, Y).
|
|
sib_count(N) :- count(N, S, sibling(bob, S)).")
|
|
(list (quote sib_count) (quote N)))
|
|
(list {:N 2}))
|
|
|
|
;; sum
|
|
(dl-at-test-set! "sum prices"
|
|
(dl-query
|
|
(dl-program
|
|
"price(apple, 5). price(pear, 7). price(plum, 3).
|
|
total(T) :- sum(T, X, price(F, X)).")
|
|
(list (quote total) (quote T)))
|
|
(list {:T 15}))
|
|
|
|
;; min
|
|
(dl-at-test-set! "min score"
|
|
(dl-query
|
|
(dl-program
|
|
"score(alice, 80). score(bob, 65). score(carol, 92).
|
|
lo(M) :- min(M, S, score(P, S)).")
|
|
(list (quote lo) (quote M)))
|
|
(list {:M 65}))
|
|
|
|
;; max
|
|
(dl-at-test-set! "max score"
|
|
(dl-query
|
|
(dl-program
|
|
"score(alice, 80). score(bob, 65). score(carol, 92).
|
|
hi(M) :- max(M, S, score(P, S)).")
|
|
(list (quote hi) (quote M)))
|
|
(list {:M 92}))
|
|
|
|
;; count over derived relation (stratification needed).
|
|
(dl-at-test-set! "count over derived"
|
|
(dl-query
|
|
(dl-program
|
|
"parent(a, b). parent(a, c). parent(b, d). parent(c, e).
|
|
ancestor(X, Y) :- parent(X, Y).
|
|
ancestor(X, Z) :- parent(X, Y), ancestor(Y, Z).
|
|
num_ancestors(N) :- count(N, X, ancestor(a, X)).")
|
|
(list (quote num_ancestors) (quote N)))
|
|
(list {:N 4}))
|
|
|
|
;; count with no matches → 0.
|
|
(dl-at-test-set! "count empty"
|
|
(dl-query
|
|
(dl-program
|
|
"p(1). p(2).
|
|
zero(N) :- count(N, X, q(X)).")
|
|
(list (quote zero) (quote N)))
|
|
(list {:N 0}))
|
|
|
|
;; sum with no matches → 0.
|
|
(dl-at-test-set! "sum empty"
|
|
(dl-query
|
|
(dl-program
|
|
"p(1). p(2).
|
|
total(T) :- sum(T, X, q(X)).")
|
|
(list (quote total) (quote T)))
|
|
(list {:T 0}))
|
|
|
|
;; min with no matches → rule does not fire.
|
|
(dl-at-test-set! "min empty"
|
|
(dl-query
|
|
(dl-program
|
|
"p(1). p(2).
|
|
lo(M) :- min(M, X, q(X)).")
|
|
(list (quote lo) (quote M)))
|
|
(list))
|
|
|
|
;; Aggregate with comparison filter on result.
|
|
(dl-at-test-set! "popularity threshold"
|
|
(dl-query
|
|
(dl-program
|
|
"post(p1). post(p2).
|
|
liked(u1, p1). liked(u2, p1). liked(u3, p1).
|
|
liked(u1, p2). liked(u2, p2).
|
|
popular(P) :- post(P), count(N, U, liked(U, P)), >=(N, 3).")
|
|
(list (quote popular) (quote P)))
|
|
(list {:P (quote p1)}))
|
|
|
|
;; Aggregate vs single distinct.
|
|
(dl-at-test-set! "distinct counted once"
|
|
(dl-query
|
|
(dl-program
|
|
"rated(alice, x). rated(alice, y). rated(bob, x).
|
|
rater_count(N) :- count(N, U, rated(U, F)).")
|
|
(list (quote rater_count) (quote N)))
|
|
(list {:N 2})))))
|
|
|
|
(define
|
|
dl-aggregates-tests-run!
|
|
(fn
|
|
()
|
|
(do
|
|
(set! dl-at-pass 0)
|
|
(set! dl-at-fail 0)
|
|
(set! dl-at-failures (list))
|
|
(dl-at-run-all!)
|
|
{:passed dl-at-pass
|
|
:failed dl-at-fail
|
|
:total (+ dl-at-pass dl-at-fail)
|
|
:failures dl-at-failures})))
|