Skip to content

Disjunction (OR): Strategy B — materialized union in the query planner - #5

Closed
oflatt-claude wants to merge 4 commits into
mainfrom
disjunction-planner-materialized
Closed

Disjunction (OR): Strategy B — materialized union in the query planner#5
oflatt-claude wants to merge 4 commits into
mainfrom
disjunction-planner-materialized

Conversation

@oflatt-claude

Copy link
Copy Markdown
Owner

Implements OR disjunction in rule bodies as Strategy B: materialize the union inside the core-relations query 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).

(rule ((OR ((edge x y))
           ((edge y x))))       ; symmetric closure
      ((connected x y)))

How it works (planner change, not egglog→egglog)

An OR's common variables V = ⋂ᵢ 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 into JoinStages projecting onto V; the union occupies materialization slots (Plan::UnionPlan / UnionMat).
  • At runtime run_union_plan_serial (execute.rs) runs every branch into one IndexMap<Vec<Value>, RowBuffer> and dedups it to a set (dedup_union_mat).
  • The surrounding conjunction is an ordinary bag that joins against the union materialization via the existing JoinStage::FusedIntersectMat prologue — the same mechanism hypertree decomposition already uses.

No egglog-level relations or auxiliary rules are created. Only variables common to every branch cross the OR boundary; 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

  • Frontend: typecheck_rule_with_or (src/typechecking.rs) → ResolvedFact::Or; src/lib.rs extracts OR groups and binds common vars for actions.
  • Bridge: RuleBuilder::query_union (egglog-bridge/src/rule.rs).
  • Backend: 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
  • full .egg harness: 747/747 (incl. desugar, 32-thread, proof treatments)
  • make nits: clean

Restrictions (documented in code + design doc)

  • Unions run naively (a rule with OR opts out of seminaive — no delta through the disjunction).
  • Union bags are forced to be leading (not folded into the decomposition cost model).
  • Branch atoms must be tables (no primitives inside a branch — clear error otherwise).
  • OR is rejected under proofs / term encoding.

Comparison

Strategy C (a fused single-pass JoinStage::Union that streams branches into the join continuation without materializing) is a separate PR for comparison.

🤖 Generated with Claude Code

oflatt and others added 4 commits July 1, 2026 20:38
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>
@oflatt-claude

Copy link
Copy Markdown
Owner Author

Superseded by #7. Strategy B (planner tree-decomposition materialization) was an alternative backend; #7 (fused correlated + seminaive union) is the chosen implementation and the one the rebuild benchmark validated.

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