8000 perf: hash-join cluster — O(N²) → O(N) for not/or/join (#202 #203 #204) by adityamukho · Pull Request #247 · project-minigraf/minigraf · GitHub
[go: up one dir, main page]

Skip to content

perf: hash-join cluster — O(N²) → O(N) for not/or/join (#202 #203 #204) - #247

Merged
adityamukho merged 9 commits into
mainfrom
feature/issues-202-203-204-hash-join
May 15, 2026
Merged

adityamukho merged 9 commits into
mainfrom
feature/issues-202-203-204-hash-join

Conversation

@adityamukho
Copy link
Copy Markdown
Collaborator

Summary

Three surgical O(N²) → O(N) fixes in the query engine:

  • perf(post-1.0): hash-join negation inner loop #202 (executor.rs): Pre-compute not/not-join exclusion sets once before the outer binding loop. Each not-body is now evaluated once against filtered facts; the filter loop probes a HashSet in O(1) per binding. Expr-only bodies fall through to the existing slow path. Normalize keyword→Ref for cross-type entity reference equality.
  • perf(post-1.0): hash-join disjunction inner loop #203 (executor.rs): Replace seeded branch evaluation in apply_or_clauses with empty-seed eval + hash-join back onto incoming bindings. Branches are evaluated once from [{}]; results are unioned across branches and hash-joined on shared user-visible variables. Includes defensive guard for or-join variables not bound in outer scope.
  • perf(post-1.0): hash-join with-grouped aggregation cross-product #204 (matcher.rs): Replace nested-loop in join_with_pattern with hash-join when a shared variable is detected. Entity position is checked first (dominant ?e-join), value position as fallback. normalize_join_value handles keyword/Ref asymmetry. Pseudo-attribute patterns and unrelated patterns fall back to nested-loop.

Test Plan

  • cargo test — all 577+ unit tests + all integration suites pass, zero failures
  • cargo clippy — clean
  • cargo fmt --check — clean
  • New tests: not_hash_join_tests (2), or_hash_join_tests (3), hash_join_tests (2)
  • Benchmark smoke-check: negation/not_scale, disjunction/or_scale, aggregation/with_grouped_sum all complete without panics
  • No {:?} debug format on Result/Fact/Value/EdnValue in any new test assert messages

Closes #202, #203, #204.

🤖 Generated with Claude Code

@codecov
codecov Bot commented May 15, 2026
Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 85.96491% with 40 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/query/datalog/executor.rs 83.84% 37 Missing ⚠️
src/query/datalog/matcher.rs 94.64% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

adityamukho and others added 9 commits May 15, 2026 14:44
Replace the per-binding not-filter loop in execute_query with a pre-computed
exclusion set approach: run each not/not-join body once against filtered_facts,
build a HashSet of excluded join-key tuples, then probe O(1) per outer binding.
Includes normalize_value() to reconcile Value::Keyword vs Value::Ref entity
representations across body and outer bindings. Expr-only bodies fall through
to the existing not_body_matches slow path unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Move has_expr out of per-binding filter closure into pre-compute phase;
  store alongside exclusion set as (bool, HashSet) tuple for both not and
  not-join paths, eliminating redundant O(clauses) work per outer binding
- Apply normalize_value consistently on the not (non-join) path when
  building and probing the exclusion set, matching the not-join path;
  prevents misses when entity values differ between Ref and Keyword variants
- Derive key_vars as intersection of join_vars with vars present in ALL
  body binding rows (fold/all approach), not just the first sample row
- Fix not fast path to fall back to not_body_matches when the outer
  binding is underspecified relative to the exclusion set key, instead
  of silently allowing the row through with continue
- Replace unsafe [i] indexing with zip() iteration to satisfy clippy
  (reduces clippy errors from 3 to 0)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
For Or/OrJoin branches that contain only patterns (no Not/NotJoin),
evaluate each branch once from an empty seed to get the full candidate
set, then hash-join the union back onto incoming bindings using shared
user-visible variables. Branches with Not/NotJoin fall back to the
classic seeded evaluation to preserve correctness (those clauses need
bound outer-scope variables). Adds or_hash_join_tests covering the
no-overlap, overlap deduplication, and or-join cases.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the nested-loop in join_with_pattern with a hash-join when a
shared join variable is detected in the entity or value position of the
incoming pattern. Keyword/Ref values are normalized during build and probe
so cross-type entity references (e.g., Value::Keyword stored as a fact
value vs. Value::Ref produced in entity position) compare equal. The
fallback to nested-loop is preserved for pseudo-attribute patterns and
patterns with no shared variable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@adityamukho
adityamukho force-pushed the feature/issues-202-203-204-hash-join branch from e78c4dd to 92b2ce4 Compare May 15, 2026 09:18
@adityamukho
adityamukho merged commit e86a363 into main May 15, 2026
36 checks passed
@adityamukho
adityamukho deleted the feature/issues-202-203-204-hash-join branch May 15, 2026 09:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant

0