8000 perf(query): magic sets rewriting for demand-driven recursive rule evaluation · Issue #289 · project-minigraf/minigraf · GitHub
[go: up one dir, main page]

Skip to content

perf(query): magic sets rewriting for demand-driven recursive rule evaluation #289

Closed

Description

@adityamukho

Problem

The semi-naive bottom-up evaluator in RecursiveEvaluator and StratifiedEvaluator computes the complete fixed-point closure of all recursive rules before any query-level filtering is applied. For a transitive-closure rule like ancestor over a 1,000-commit git history, this means deriving ~500,000 ancestor pairs even for a query like "is commit A an ancestor of tag T?" — which only needs O(N) derivations along the relevant path.

This is the root cause of the limit-exhaustion problem reported in #288. Making the limit configurable (#288) works around the symptom; magic sets rewriting eliminates the cause.

What magic sets rewriting does

Magic sets (Beeri & Ramakrishnan 1991) is a compile-time transformation that pushes the bound variables of the top-level query back into the recursive rules as "magic predicates." The result is that only derivations reachable from the actual query bindings are computed.

Example — without magic sets:

(rule [(ancestor ?a ?b)] [?a :commit/parent ?b])
(rule [(ancestor ?a ?c)] [?a :commit/parent ?b] (ancestor ?b ?c))

;; Query: (ancestor "abc123" ?tag)
;; Evaluator derives ALL ancestor pairs for ALL commits before filtering to ?a = "abc123"

Example — with magic sets:

;; Transformed rules (done at query time, transparent to user):
(rule [(magic-ancestor ?a)] [(seed-ancestor "abc123")])
(rule [(magic-ancestor ?b)] (magic-ancestor ?a) [?a :commit/parent ?b])
(rule [(ancestor ?a ?b)] (magic-ancestor ?a) [?a :commit/parent ?b])
(rule [(ancestor ?a ?c)] (magic-ancestor ?a) [?a :commit/parent ?b] (ancestor ?b ?c))

;; Evaluator now only derives ancestors reachable from "abc123" — O(N) not O(N²)

Performance impact

Query type Without magic sets With magic sets
Point query (bound start) O(N²) derivations O(N) derivations
Full closure query O(N²) derivations O(N²) derivations (same)
Reverse query (bound end) O(N²) derivations O(N) derivations with sideways info passing

For the 986-commit git history from #288, a point query reduces from ~500K derived facts to ~1K.

Implementation sketch

  1. Query adornment — classify each argument in a recursive predicate as bound (b) or free (f) based on the query's ?var bindings at the call site
  2. Magic predicate generation — for each adornment, generate a magic-<pred>/<adornment> predicate and seed facts from the query's bound values
  3. Rule transformation — rewrite each rule to (a) propagate magic facts through recursive calls and (b) guard the rule head with the magic predicate
  4. Integration point — executor.rs execute_query_with_rules, before handing off to StratifiedEvaluator

Scope and complexity

  • Non-trivial: query adornment + rule transformation must handle mutual recursion and negation correctly
  • Well-studied: clean algorithms exist in the literature (Beeri & Ramakrishnan 1991; Bancilhon et al. 1986)
  • Zero user-visible API changes — transformation is internal
  • Should be implemented after v1.x stabilizes; Phase 9 or dedicated query optimization phase

References

Related

Activity

  1. added a commit that references this issue on Jun 4, 2026
  2. adityamukho commented on Jun 4, 2026

    @adityamukho
    CollaboratorAuthor

    Implemented in #291 (merged). Magic sets rewriting is always-on for positive recursive rules with bound query args. 974 tests passing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      0