Skip to content

feat: add filter expression storage to query arena#8

Merged
nnunley merged 8 commits into
forest-rs:mainfrom
nnunley:filter-expression-storage
Apr 11, 2026
Merged

feat: add filter expression storage to query arena#8
nnunley merged 8 commits into
forest-rs:mainfrom
nnunley:filter-expression-storage

Conversation

@nnunley
Copy link
Copy Markdown
Collaborator

@nnunley nnunley commented Apr 6, 2026

Summary

  • Add FilterSlotId, FilterEvaluator<Id: EntityId> trait, and NoFilter to leit_core
  • Add FilterPredicate, FilterValue enums and QueryNode::Filter/ExternalFilter variants to leit_query
  • Add ExecutionPlan::wrap_external_filter() for post-planning filter attachment
  • Thread FilterEvaluator<u32> through ExecutionWorkspace::{execute, search} and all internal execution methods
  • Add ExecutionWorkspace::plan_filtered() as primary API for attaching filters to string queries
  • ExternalFilter nodes are immediately usable; Filter (structured predicates) returns IndexError::UnsupportedFilterPredicate until columnar storage (Phase 3)
  • Mark filter expression storage as decided in the handover decision register

Design

Filtering is node-mediated: ExternalFilter nodes in the query plan dispatch to the FilterEvaluator at execution time. NoFilter is a zero-cost abstraction — the compiler eliminates all filter checks via monomorphization. The FilterEvaluator trait is generic over Id: EntityId in leit_core to avoid a future breaking change when the pipeline becomes generic.

Spec: docs/superpowers/specs/2026-04-05-filter-expression-storage-design.md

Test plan

  • Unit tests for Filter/ExternalFilter validation, children(), FilterPredicate combinators
  • Unit tests for wrap_external_filter metadata (root, depth, cost, selectivity, features)
  • Integration tests: accept-all, reject-all, selective, chained multi-slot, structured predicate error
  • All existing tests pass with &NoFilter parameter
  • cargo clippy --workspace --all-features --all-targets clean
  • cargo test --workspace --all-features — 177 tests pass

@nnunley nnunley force-pushed the filter-expression-storage branch 2 times, most recently from aad80df to 7603726 Compare April 6, 2026 22:06
… plan()

refactor: make FilterEvaluator self-describing with slots() and unify plan()

FilterEvaluator now requires a slots() method declaring which filter slots
the evaluator handles. plan() queries the evaluator's slots and wraps the
plan with ExternalFilter nodes. This eliminates the separate plan_filtered()
method and ensures search() always applies filters correctly.

Addresses PR review feedback: the old search(filter) API silently ignored
the filter because plan() never attached ExternalFilter nodes.

fix: address review findings — AcceptAll test gap and plan/execute mismatch guard

- Give AcceptAll test filter a slot so evaluate() is actually dispatched
  through the ExternalFilter node path
- Add debug_assert in try_execute_root and try_execute_root_unscored
  that filter.slots() is empty when the Term fast path fires, catching
  plan/execute filter mismatches in debug builds

fix: broken intra-doc link for ExternalFilter in leit_core
@nnunley nnunley force-pushed the filter-expression-storage branch from 7603726 to 4fbb37c Compare April 6, 2026 23:16
@nnunley nnunley requested a review from waywardmonkeys April 9, 2026 13:53
@nnunley nnunley merged commit e8c1194 into forest-rs:main Apr 11, 2026
14 checks passed
@nnunley nnunley deleted the filter-expression-storage branch April 18, 2026 17:55
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.

1 participant