Skip to content

Support empty antecedent rules in coln-query - #158

Open
lstwn wants to merge 2 commits into
mainfrom
coln-query/empty-antecedent-rules
Open

lstwn wants to merge 2 commits into
mainfrom
coln-query/empty-antecedent-rules

Conversation

@lstwn

@lstwn lstwn commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Empty Antecedent Rules

Issue

Rules can have an empty consequents, in which case they assert nothing and can
be ignored. However, rules can also have an empty antecedents, in which case
they demand the consequents to evaluate to true. That means the conjunctive
query defined by the consequents must be non-empty.

Solution

Antijoin Approach

$antijoin(R, S)$

Whenever there is an Atom with no variable binding at all (neither row id nor
values) the FLIR expects a 0-tuple (a tuple with 0 fields; the unit tuple
hereafter). We can translate that into projecting away all columns of the
Atom's underlying relation $S$. This results in as many unit tuples as there
are rows in $S$ (let's say $n$ rows). $n$ is usually zero or one row because
rules of this kind often operate on Atoms (in the consequent) which contain at
most one row.

For the antijoin approach to work the left input relation $R$ (given by the
empty antecedent) must somehow come into existence, as there is no table to
provide it. The obvious candidate is a relation consisting of only the unit
tuple, as any quantification over the unit tuple is true by definition (its a
neutral element). An empty antecedent then signals such a "neutral relation".
Such a "unit relation" can now be constructed using the new ConstantExpr leaf.

The upside of this approach is that it aligns nicely with non-empty antecedent
rules and requires no special treatment except for the creation of a neutral
relation.

A downside of this rule checking is that for consequents with more than one
Atom we cannot tell the user which Atoms are non-empty.

The Counting Approach

Another query shape ensuring that every consequent's atom's underlying relation
is non-empty, is to count its entries with a count() aggregation.

The upside is that we can tell which base table violates the non-emptiness
assertion. The downside is it requires aggregation (which isn't a thing yet in
coln-query's IR; but we want to have it at some point anyways). Furthermore, an
empty antecedent rule with $n$ consequent atoms turns into $n$ queries which all
need checking if their output is $> 0$.

…IR to

support having a relation with the empty tuple in it for empty antecedent rules
@lstwn
lstwn requested a review from szlangini September 9, 2026 15:37
@lstwn
lstwn marked this pull request as ready for review September 9, 2026 16:00
@ept

ept commented Sep 9, 2026

Copy link
Copy Markdown
Member

If you have a rule with empty antecedent and multiple atoms in the consequent, I don't think it's correct to count the number of rows in each of those atoms' tables separately. I think you have to evaluate the consequent as a conjunctive query and check whether the results of that query are nonempty.

For example, you could define a graph that must always contain at least one triangle. That would be a rule with an empty antecedent, and a 3-atom consequent.

@lstwn

lstwn commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator Author

True! But this PR implements the anti join approach, so we’re good here.

Reflecting upon a call with @olynch on Monday, the example rule true => edge(a, b), edge(b, c), edge(c, a) cannot be constructed like this because:

  1. Every variable a rule mentions, must be covered (i.e. used or bound to a variable) in its antecedent (@olynch told me this rule).

To express all triangles we need at least the three join variables a, b, and c but the empty antecedent permits zero variables. So I guess the compiler must handle this differently somehow. Maybe all triangles become a derived view, and then there is an empty antecedent rule with the consequent being a single atom of that derived view (and not binding any variable)?! I guess we have to ask @olynch here.

So as long as (1) is indeed the case, the counting approach is an alternative option. Anyways, I continue with the anti join approach in here but still I'm interested to know how the example from @ept works out with the compiler.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants