Aggregation

count, sum and list over groups, filtering groups with having, and an aggregate in a rule's head.

An aggregate in the projection reduces many rows to one per group. The plain variables in the projection are the group, and the aggregate is computed over each group’s rows. This counts what each package imports:

imports(?p, ?q) => ?p, count(?q)
?pcount(?q)
api4
app3
auth3
cache2
config1
db2
legacy1
log1
metrics1
Facts: the deps dataset

util has no row because it imports nothing, so there’s no group for it, which is worth remembering when a count you expected to be zero is just missing. Several aggregates can share a group, which is a fairly common shape of question. Here’s each team’s package count and total lines:

owner(?p, ?t), loc(?p, ?n) => ?t, count(?p), sum(?n)
?tcount(?p)sum(?n)
data21900
platform4700
security1800
web32400
Facts: the deps dataset

The aggregates are count, sum, min, max and list.

Counting rows or values

count(?t) counts the rows in a group, and count(distinct ?p) counts the different values. They differ when a value repeats. The security team has two tests, both of auth:

test(?t, ?p), owner(?p, ?team) => ?team, count(?t), count(distinct ?p)
?teamcount(?t)count(distinct ?p)
data11
platform11
security21
web11
Facts: the deps dataset

list gathers a group’s values into a list, in the answer’s usual order, which a later query can read again with the list.* functions (reference):

owner(?p, ?t) => ?t, list(?p)
?tlist(?p)
data["cache","db"]
platform["config","log","metrics","util"]
security["auth"]
web["api","app","legacy"]
Facts: the deps dataset

Filtering groups

A comparison in the goal filters rows before they’re grouped. To filter the groups themselves, by what an aggregate came to, add having after the projection. These are the packages at least three others import:

imports(?p, ?q) => ?q, count(?p) having count(?p) >= 3
?qcount(?p)
config4
log5
util3
Facts: the deps dataset

An aggregate in a rule

A rule’s head can aggregate, which gives the result a name that other rules and goals can use like any relation. fan_in is how many packages import each one, and the goal then reads it with an ordinary comparison:

fan_in(?q, count(?p)) :- imports(?p, ?q);
fan_in(?q, ?n), ?n >= 3 => ?q, ?n
?q?n
config4
log5
util3
Facts: the deps dataset

A rule that aggregates has to be its relation’s only rule. Two rules each producing a count for the same package would leave it with two counts, and it’s not clear which one you’d want. jaala derives everything the rule’s body reads before it computes the aggregate, the same way it handles not, so the count is never taken over a relation that’s still growing.

A group with no rows

With no plain variables in the projection, the whole answer is one group, and an aggregate over no rows still answers. legacy imports nothing the security team owns, so the count is zero rather than missing:

imports("legacy", ?q), owner(?q, "security") => count(?q)
count(?q)
0
Facts: the deps dataset

That’s the end of the language tutorials. Everything here also works from Go, which the next tutorial covers.