Disjunction (OR): Strategy B — materialized union in the query planner - #5
Closed
oflatt-claude wants to merge 4 commits into
Closed
Disjunction (OR): Strategy B — materialized union in the query planner#5oflatt-claude wants to merge 4 commits into
oflatt-claude wants to merge 4 commits into
Conversation
Introduce `(OR (branch) (branch) ...)` in rule queries, where each branch is a parenthesized list of facts (a conjunctive subquery); the rule matches when any branch matches. Only variables bound in every branch may appear in the actions. This commit establishes the surface syntax, semantics, behavioral tests, and a design doc. The current backend is a parse-time rule-splitting prototype that distributes a body into the cartesian product of branch choices; per review it will be replaced by a materialized-union subquery (design doc Strategy B) so the disjunction is executed natively without rule blowup or redundant firing. - src/ast/parse.rs: parse and expand `OR`; handle multi-command `fail` expansion - tests/disjunction.rs: 6 behavioral tests (semantics + rejection cases) - docs/disjunction-design.md: syntax, semantics, and efficient-execution design - CHANGELOG.md: feature entry Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the parse-time rule-splitting prototype with a materialized-union lowering. `OR` is now a real AST fact (`GenericFact::Or`); each disjunction is compiled to an internal relation keyed on the branches' common variables, populated by one auxiliary rule per branch, and the `OR` in the body becomes a single atom over that relation. This avoids rule blowup and redundant firing and reuses seminaive evaluation and the worst-case-optimal join unchanged; the cost is one extra derivation step of latency (run to fixpoint). - egglog-ast: add `GenericFact::Or`; recurse in Display/visit/map/map_symbols - src/ast/parse.rs: parse `(OR (branch)...)` into `Fact::Or` - src/ast/disjunction.rs: lower `OR` to relation + per-branch rules; enforce the common-variable interface rule; look up sorts via `typecheck_facts` - src/typechecking.rs: `OrBranchLocalEscapes` / `EmptyOrBranch` errors - tests/disjunction.rs: strategy-independent behavioral tests (run to fixpoint) - docs/disjunction-design.md, CHANGELOG.md: document the implemented strategy Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Shared scaffolding for implementing OR disjunction natively in the backend: - egglog-ast: GenericFact::Or variant (+ Display/visit/map/map_symbols) - src/ast/parse.rs: parse (OR (branch)...) into Fact::Or - src/typechecking.rs: OrBranchLocalEscapes / EmptyOrBranch error variants - tests/disjunction.rs: strategy-independent behavioral tests (correctness oracle) - docs/disjunction-design.md: strategies Post-typecheck match sites currently `unreachable!` on OR; a real backend implementation replaces these by carrying OR into the query engine. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…(Strategy B) Compile each `(OR (branch) ...)` in a rule body as a materialized bag in the core-relations tree-decomposed plan, keyed on the branches' common variables, so the rest of the query joins against it. No egglog-level relations or rules. - typechecking: `typecheck_rule_with_or` splits conjunctive vs OR facts into one constraint problem (branch-local vars renamed fresh), enforces the interface rule, and produces `ResolvedFact::Or`. - lib/core/bridge: OR groups are pulled out of the flat core query, lowered per branch, and emitted via `RuleBuilder::query_union`; common vars are passed as `extra_bound` for action binding. - core-relations: `QueryBuilder::add_union` + `Plan::UnionPlan`; `plan_union_query` materializes each union as a leading bag and joins the conjunction against it via `FusedIntersectMat`; `run_union_plan_serial` runs branches into a deduped set materialization then fires actions. Unions run naively (no seminaive delta through the disjunction) and are rejected under proofs/term encoding. All 9 disjunction tests and the 747 files tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This was referenced Jul 1, 2026
Owner
Author
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 in rule bodies as Strategy B: materialize the union inside thecore-relationsquery planner via the existing tree-decomposition machinery. Supersedes #3 (which did B as an egglog→egglog transformation — extra internal relations/rules and a fixpoint-iteration of latency; too slow).How it works (planner change, not egglog→egglog)
An
OR's common variablesV = ⋂ᵢ vars(branchᵢ)become the message variables of a leading materialization bag:plan_union_query(core-relations/src/free_join/plan.rs) plans each branch's atoms intoJoinStagesprojecting ontoV; the union occupies materialization slots (Plan::UnionPlan/UnionMat).run_union_plan_serial(execute.rs) runs every branch into oneIndexMap<Vec<Value>, RowBuffer>and dedups it to a set (dedup_union_mat).JoinStage::FusedIntersectMatprologue — the same mechanism hypertree decomposition already uses.No egglog-level relations or auxiliary rules are created. Only variables common to every branch cross the
ORboundary; a branch-local variable used outside is a compile error; branch-locals are renamed to fresh names during typechecking so their types don't conflate.Layers
typecheck_rule_with_or(src/typechecking.rs) →ResolvedFact::Or;src/lib.rsextracts OR groups and binds common vars for actions.RuleBuilder::query_union(egglog-bridge/src/rule.rs).Query.unions+QueryBuilder::add_union(query.rs);Plan::UnionPlan,plan_union_query,build_union_result_block(plan.rs);run_union_plan_serial,dedup_union_mat(execute.rs), incl. the parallel executor path.Tests
tests/disjunction.rs: 9/9.eggharness: 747/747 (incl. desugar, 32-thread, proof treatments)make nits: cleanRestrictions (documented in code + design doc)
ORopts out of seminaive — no delta through the disjunction).ORis rejected under proofs / term encoding.Comparison
Strategy C (a fused single-pass
JoinStage::Unionthat streams branches into the join continuation without materializing) is a separate PR for comparison.🤖 Generated with Claude Code