Skip to content

fix(engine): multi-clause rules sharing a derived relation (#91) - #123

Merged
jsam merged 2 commits into
mainfrom
fix/multi-clause-rules
Oct 1, 2026
Merged

jsam merged 2 commits into
mainfrom
fix/multi-clause-rules

Conversation

@jsam

@jsam jsam commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

Fixes #91.

Root cause: JoinPlanner::plan_joins (src/join_planning/mod.rs) treated a multi-clause rule's Union as one join block. Scans from different clauses landed in one join graph, and same-named variables across clauses became join edges. When that graph was connected, the planner replaced the Union with one cross-clause join, dropping each clause's projections and filters: empty results, or a raw cross join with extra columns. Disjoint variable names left the graph disconnected, which is why only some shapes failed.

Fix

  • Plan each union branch on its own; a union nested under a join is one opaque input.
  • Rule catalog uses a BTreeMap so persistent rule order is deterministic (mutual-recursion results were hash-order dependent once the fix landed).

Tests: tests/multi_clause_rule_tests.rs (8, via Handler: both issue repros, retroactive empty-on-new-clause, session rules, consistency-core conflict as one 4-clause rule, negation, bound recursion, aggregate; 6 fail on main). 2 planner unit tests. New snapshot 18_advanced_patterns/98_multi_clause_shared_derived; 5 snapshots that recorded the bug as expected output updated. cargo test: 3,286 passed; snapshots: 1,145 passed, stable over 5 runs.

Found, not fixed here

  • Server ignores [optimization] config (IQLEngine::new() always), so the flags in the issue comment did nothing.
  • String Var < Var is always false: ColumnsLt (src/code_generator/mod.rs:1821) only compares numbers.
  • Mutual recursion stops at base cases: only self-recursive rules reach fixpoint (src/lib.rs:1252).

consistency-core's one-clause-per-relation workaround can now be collapsed.

jsam added 2 commits October 1, 2026 17:28
RuleCatalog stored definitions in a HashMap, so all_rules() emitted
rules in hash order. The topological sort keeps that order for rules in
a cycle, so mutually recursive rules ran in a different order per
process and produced different results. Store definitions in a BTreeMap
so iteration (and catalog.json) is ordered by rule name.
Root cause: JoinPlanner::plan_joins treated a multi-clause rule's Union
as one conjunctive join block. extract_scans descended into every Union
branch, so scans from different clauses landed in one join graph, and
variables with the same name in different clauses (E1/E2 in the bridge
and solo clauses) became join edges. When that graph was connected, the
planner rebuilt a single join tree across clauses and
preserve_top_operations returned it in place of the Union, dropping
each branch's Map/Compute/Filter. Result: constant filters of one
clause applied to the other (silently empty), or the raw cross-clause
join leaked out (4 columns for a 3-column head). Clauses with disjoint
variable names left the graph disconnected, which is why only some
shapes failed. Planner is on by default and the server ignores
[optimization] in config, so disabling it there had no effect.

Fix: plan each Union branch as its own join block, and treat a Union
nested under a join as an opaque leaf. extract_scans now returns the
leaf IR nodes directly.

Snapshots that recorded the bug are updated: the two-hop session rule
(malformed 3-column output), the recursive debug plan (union merged
into a join), and the mutual recursion cases (empty). Mutual recursion
now returns base cases but still does not iterate across relations;
only self-recursive heads run to fixpoint. That is a separate defect.
@jsam
jsam merged commit d9cb6e3 into main Oct 1, 2026
13 checks passed
@jsam
jsam deleted the fix/multi-clause-rules branch October 1, 2026 16:49
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.

Engine: multi-clause rules silently return empty or malformed results when clauses share a derived relation

1 participant