Disjunction (OR): Strategy C — fused union node in the free-join engine - #6
Closed
oflatt-claude wants to merge 1 commit into
Closed
Disjunction (OR): Strategy C — fused union node in the free-join engine#6oflatt-claude wants to merge 1 commit into
oflatt-claude wants to merge 1 commit into
Conversation
…(Strategy C)
Add `(OR (branch) (branch) ...)` to rule bodies, compiled to a single backend
rule with a fused union node in the core-relations free-join engine — the
surrounding conjunction is scanned once and the branches are enumerated
additively (no rule-splitting / no cartesian product of rules).
Backend: a new `JoinStage::Union { branches }` (core-relations/src/free_join/plan.rs,
executed in execute.rs) forms block 0 of a DecomposedPlan; each branch is a
self-contained sub-plan projecting onto the OR's common variables, written into
one materialization keyed on those variables (deduplicated in memory). The result
block is the surrounding conjunction, planned via the existing tree-decomposition
message-passing (`FusedIntersectMat`) so it joins that materialization by index
and fires the action. `Query.union` / `QueryBuilder::set_union` carry the union;
`egglog-bridge` `RuleBuilder::set_union_branches` bridges it.
Frontend: `OR` is a real AST fact (`GenericFact::Or`); typechecking resolves each
branch (branch-local vars renamed to fresh names), enforces that only variables
common to every branch cross the OR boundary, and binds those common variables
for the actions.
Restrictions: OR rules run in naive mode (no seminaive delta through a union);
branch atoms must be tables (primitives allowed in the surrounding conjunction);
OR is rejected under proofs / term encoding.
- egglog-ast, src/ast/parse.rs: `GenericFact::Or` + parsing
- src/typechecking.rs, src/core.rs, src/lib.rs: typecheck + wire OR to the backend
- core-relations: `JoinStage::Union`, `plan_union`, execution
- egglog-bridge: union plumbing
- tests/disjunction.rs (14 tests), docs/disjunction-design.md, CHANGELOG.md
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Owner
Author
|
Superseded by #7, which builds on this fused union node and adds correlated branches + seminaive-through-union (the version needed for efficient rebuilding). |
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.
Implements
ORdisjunction as Strategy C: a fused union node in thecore-relationsfree-join engine. Companion to #5 (Strategy B).How it works
ORcompiles to a single backend rule (no rule-splitting, no cartesian product of rules). A newJoinStage::Union { branches }(core-relations/src/free_join/plan.rs, executed inexecute.rs) is block 0 of aDecomposedPlan:InPlaceMaterializer.C, planned by the ordinaryplan_single_bag, which auto-generates aFusedIntersectMatprologue that iterates the distinct union tuples and probesC's atoms by index — soCis scanned once and joined against the union, never re-scanned per branch.Query.union/QueryBuilder::set_unioncarry the union to the planner;egglog-bridge'sRuleBuilder::set_union_branchesbridges it. Frontend:GenericFact::Or+ typecheck resolves each branch (branch-local vars renamed fresh), enforces the common-variable interface rule, and binds the common vars for actions.Tests
tests/disjunction.rs: 14/14 (incl. a branch with an internal join on a branch-local var, output-var-in-continuation, multi/nestedOR, rewrite:when, union in actions, error cases).eggharness: 747/747make nits: cleanRestrictions
ORrule opts out of seminaive — no delta through a union).B vs C — honest comparison
Both #5 (B) and this (C) end up materializing the union's output tuples keyed on the common variables and joining the surrounding conjunction against that materialization. Materializing is essentially inherent here: to index-join
Cagainst the union you need the union's tuples in an indexable form. So this is not a "materialized vs zero-materialization streaming" contrast.They differ in plan structure / integration:
Plan::UnionPlanwith its own serial+parallel executor (run_union_plan_serial,dedup_union_mat) and union bags as a leading phase.JoinStage::Unionfolded into the existingDecomposedPlan, reusing the tree-decomposition materializer andFusedIntersectMatfor the continuation — fewer new execution paths, more reuse of existing machinery.A truly zero-materialization streaming union (stream each branch match straight into the continuation) would re-probe
Cper branch tuple; since egglog does not dedup action firings for ordinary rules anyway, that is essentially the work rule-splitting does — which is why both strategies materialize the deduped union instead.🤖 Generated with Claude Code