fix: keep filters when pushing projections into imports (#792) - #793
Merged
Conversation
`push_projections_and_filters` pushes a rule's projection *and* its filters
into the import it reads from. Two things there could separate a filter from
the column it needs.
First, the projection decided which columns to read by looking at the head
variables plus the variables of operations that bind a variable
(`variable_assignment()`). A variable used only by a boolean filter matched
neither, so its column was marked SKIP while the filter that reads it was
pushed into the same import. The filter then referenced a column that had
never been read:
p(?s) :- triple(?s, <http://example.org/label>, ?v), STRSTARTS(?v, "al") .
thread 'main' panicked at nemo-physical/src/function/evaluation.rs:220:36:
index out of bounds: the len is 2 but the index is 2
Now every variable an operation mentions keeps its column alive. This is
conservative — a filtered column is read even though it is not projected —
but a filter that cannot see its input is not an optimisation.
Second, a rule whose head and body atoms are all variables was treated as a
pure projection and removed, on the grounds that the import can do the
projection itself. The check never asked whether the rule carried any
operations, so the rule's filters were discarded with it and the rule matched
everything:
p(?v) :- triple(?s, ?q, ?v), STRSTARTS(?v, "al") . % returned "beta" too
A rule with body operations is no longer considered obsolete.
Also fixes an adjacent defect in the same block: `obsolete_rules` is collected
in ascending order and was drained with `rules.remove(idx)` in that same order,
so every removal after the first took the wrong element. It is now drained in
reverse. No test covered this because the surrounding conditions rarely make
two rules obsolete at once.
Adds regression tests under
resources/testcases/regression/filter_import_projection/ — one rule per case,
deliberately: several rules with different head predicates disable the import
pushdown entirely and hide the bug. Four of the five fail before this change
(two by returning unfiltered rows, one by returning everything for an
unsatisfiable filter, one by panicking); `kept` is a control that passed
before and still passes.
mmarx
force-pushed
the
fix/792-filter-import-projection
branch
from
July 31, 2026 19:59
7a0acf9 to
2149d1c
Compare
mmarx
enabled auto-merge
July 31, 2026 21:01
mmarx
approved these changes
Jul 31, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #792.
Summary
Both symptoms in #792 — a filter silently doing nothing, and an index-out-of-bounds panic — come from one place:
push_projections_and_filtersinnemo/src/rule_model/pipeline/transformations/filter_imports.rs, which pushes arule's projection and its filters into the import it reads from. Two separate mistakes there could separate a filter from the column it needs.
My report guessed the fault was in
StackProgram::evaluate/ the reference map. That was wrong — those are downstream victims, andnemo-physicalis not touched by this PR.1. The projection did not count filter-only variables
Columns to read were chosen from the head variables plus the variables of operations that bind something (
variable_assignment()). A variable used only by a boolean filter matches neither, so its column was markedSKIP— while the filter that reads it was pushed into that same import. The filter then referred to a column that had never been read:Index 2 was the skipped column. Every variable an operation mentions now keeps its
column alive.
2. Rules carrying filters were removed as pure projections
A rule whose head and body atoms are all variables was treated as a projection the import can perform itself, and dropped. The check never asked whether the rule carried any operations, so its filters were discarded with it and the rule matched everything:
A rule with body operations is no longer considered obsolete. This is also why the filter never appeared in the execution plan: by planning time the rule was gone.
3. Adjacent:
obsolete_ruleswas drained in the wrong orderobsolete_rulesis collected in ascending index order and was drained withrules.remove(idx)in that same order, so every removal after the first took the wrong element. Now drained in reverse. Unnoticed because the surrounding conditions rarely make two rules obsolete at once; found while reading, not by a failing test.Behaviour
Filter
STRSTARTS(?v, "al")over two imported triples, expecting only"alpha":triple(?s, <const>, ?v)triple(?s, <const>, ?v)triple(?s, ?q, ?v)"alpha","beta"triple(?s, ?q, ?v)An unsatisfiable filter (
STRSTARTS(?v, "zz")) returned every row before and returns nothing now.Tests
resources/testcases/regression/filter_import_projection/— five cases, verified to fail onv0.10.0and onmainbefore this change (three wrong answers, one panic).keptis a control that passed before and still passes.One rule per case, deliberately: several rules with different head predicates trip
same_head_predicateand disable the import pushdown altogether, which hides thebug. My first attempt at these tests passed on an unpatched build for exactly that reason.
cargo test --workspace: 458 passed, 0 failed.cargo fmt --checkandcargo clippy -p nemoclean.Questions
This fix is conservative: a filtered column is now read even though it is not projected, so the pushdown optimises slightly less in precisely those cases. I took correctness over the optimisation on the grounds that a filter which cannot see its input is not an optimisation.
The alternative is to keep the skip and remap the filter's references onto the post-skip layout —
with_skipsalready does this renaming, so the machinery exists; it would need the projection and the filter to agree on which columnssurvive. That is a larger change and a design call that is yours, so I did not attempt it.
Happy to rework this if you would prefer that direction.