Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Aggregates: counting inside a join

Area: Rules, aggregates & recursion Teaches: a count { B } whose body B is a join — a relation atom together with a type-membership atom (p in Person) — so the count ranges only over partners that satisfy both. The membership filter and the relation join are the same body; reordering them must not change the count. Prerequisites: aggregates (count / exists), and first-class relations. Run: ox build examples/aggregate_membership_join && ox derive examples/aggregate_membership_join <predicate>

count { Person(p) } counts a population; count { R(a, p) } counts edges. The interesting case is the conjunction — count the R-partners of a that are also Persons — because the membership atom and the relation atom must be solved together, and a bug that drops the membership test silently under-joins to zero.

What to read in root.ar

a0 relates via R to three things, but only two of them (p1, p2) are Persons — p3 is a plain Thing:

pub fact R(a0, p1);   // p1 in Person
pub fact R(a0, p2);   // p2 in Person
pub fact R(a0, p3);   // p3 in Thing, NOT Person

Relation-only count ranges over all three R-partners:

pub derive rel_count_is_three(a) :- Thing(a), count { R(a, p) } >= 3;

Count inside a join intersects the relation with type membership, so only the Person-typed partners count — two, not three:

pub derive member_count_at_least_two(a)   :- Thing(a), count { p in Person, R(a, p) } >= 2;
pub derive member_count_at_least_three(a) :- Thing(a), count { p in Person, R(a, p) } >= 3;

Order-independence — swapping the two body atoms is the same join, so the same count of 2:

pub derive member_count_reordered_two(a) :- Thing(a), count { R(a, p), p in Person } >= 2;

Running it

ox derive <predicate> prints the head’s tuples; each firing head returns the bound a (here the individual a0):

derive(rel_count_is_three):           1 tuple(s)   (a0)   — count{R(a,p)} = 3, so >= 3 holds
derive(member_count_at_least_two):    1 tuple(s)   (a0)   — count{p in Person, R(a,p)} = 2, so >= 2 holds
derive(member_count_at_least_three):  0 tuple(s)          — that count is 2, so >= 3 fails
derive(member_count_reordered_two):   1 tuple(s)   (a0)   — reordered body, same count of 2

The decisive contrast is rel_count_is_three vs member_count_at_least_three: both ask >= 3 of a count over a0’s R-partners, but the membership atom drops p3 (a non-Person), so the count falls from 3 to 2 and the second head does not fire. The member_count_reordered_two head proves the count is independent of atom order — both arrangements solve the same join.

Honest caveats (what runs today)

  • The join inside the aggregate is the ordinary conjunctive body: p in Person is a type-membership atom and R(a, p) is a relation atom, solved together over the shared variable p.
  • The count ranges over distinct solution bindings of the body, evaluated against the current state.

This example is compiled and run in CI; its join-count behaviour is pinned by a corpus test (oxc-runtime/tests/examples_corpus.rs::corpus_aggregate_membership_join_count), so the membership-inside-aggregate semantics can’t regress to the silent under-count it was written to guard against.