Fluent Query API & Predicate Parsing
Archetype-rooted fluent query builder that parses C# lambdas into index-driven plans — structural constraints, indexed/opaque predicates, OR disjunction, and FK navigation joins, all off one entry point.
Status: ✅ Implemented · Visibility: Public · Level: 🟢 Start Here · Category: Querying
🎯 What it solves
Application code needs a single, type-safe entry point for finding entities — "has Inventory but not Frozen", "Level ≥ 50", "players in guilds with Level ≥ 10" — without ever hand-rolling a B+Tree scan, an expression-tree walker, or a ring-buffer registration. tx.Query<TArchetype>() is that entry point: archetype/component constraints, enabled-bit checks, field predicates (indexed or opaque), OR disjunction, FK joins, ordering, pagination, and reactive materialization all compose off the same fluent builder, rooted at the transaction that will see the results.
⚙️ How it works (in brief)
tx.Query<TArchetype>() returns a mutable ref-friendly struct that borrows the Transaction and resolves the archetype's polymorphic subtree (the archetype plus all descendants, by default — use tx.QueryExact<TArchetype>() to skip descendants) into a bitmask at construction. Fluent calls narrow that mask (With<T>/Without<T>/Exclude<TArchetype>) or add per-entity constraints (Enabled<T>/Disabled<T>, field predicates, spatial predicates). WhereField<T>(Expression<Func<T,bool>>) walks the lambda's expression tree and decomposes it into FieldPredicate structs — flattening && chains and, when || is present, normalizing to Disjunctive Normal Form (DNF). A one-shot terminal (Execute/ExecuteOrdered/Count/Any) consumes the parsed predicate immediately; ToView() instead compiles it into a FieldEvaluator[] array and wires up a persistent, incrementally-refreshed EcsView<TArchetype>.
Sub-features
| Sub-feature | Use it when... |
|---|---|
| Indexed Field Predicates (WhereField) | The field has [Index] and you want the fastest scan path, ordering/pagination, or a reactive ToView() |
| Opaque Post-Filter Predicates (Where) | The condition needs arbitrary C# logic, multiple fields at once, or a non-indexed field |
| OR Disjunction (DNF Predicates) | Your predicate needs \|\| — across an indexed field comparison or a mixed AND/OR group |
| Foreign-Key Navigation Joins (L4) | You need to filter or join across an entity-reference field (e.g. Player.GuildId → Guild) |
⚠️ Guarantees & limits
- Queries are polymorphic by default —
tx.Query<TArchetype>()matches the archetype and every descendant;tx.QueryExact<TArchetype>()is exact-archetype-only. With<T>/Without<T>/Exclude<TArchetype>filter at the archetype mask level, not per-entity component presence;Enabled<T>/Disabled<T>test per-entity enabled bitmaps (at most 4 of each, backed by aushortbitmap — exceeding it throwsInvalidOperationException).- At most one spatial predicate (
WhereNearby/WhereInAABB/WhereRay) per query; composes withWhereField/Wherebut not with a second spatial call. Execute()returns an unorderedHashSet<EntityId>;ExecuteOrdered()requiresOrderByField/OrderByFieldDescendingon an indexed field (itself requiringWhereField) and returns aList<EntityId>.Count()/Any()ignoreSkip/Takeand throw if either is set.WhereFieldpredicates must reference[Index]-carrying fields; unsupported syntax (method calls, arithmetic, field-to-field comparisons) throwsNotSupportedExceptionimmediately insideWhereField, while a resolvable-but-unindexed field throwsInvalidOperationExceptionat the first terminal call.- Supported operators:
==,!=,>,<,>=,<=, combined with&&,||,!(De Morgan's law applied at parse time). String-typed fields (String64) aren't supported inWhereField/OrderByField— use.Where(lambda)instead. ToView()rejectsOrderBy/Skip/Take(a view is unordered) and rejects mixingWhereFieldwith a spatial predicate or a chained.Where(...).NavigateField<TSource, TTarget>(fkSelector)starts a separate FK-join query path (EcsNavigationQueryBuilder) — its predicates compose with the source archetype mask but not with the OR/spatial forms above.- Parsing is a one-time cold-path cost (~100µs) per query/view construction; the compiled
FieldEvaluator[]is what runs on the hot path.
🧪 Tests
- EcsQueryTests — archetype-mask With/Without/Exclude, Enabled/Disabled bitmaps, Count/Any, foreach, and unsupported-clause-combo guards
🔗 Related
- Sibling: Query System (EcsQuery) — the Ecs-category entry point this whole category is the deep-dive extension of
- Source: src/Typhon.Engine/Ecs/public/EcsQuery.cs
- Sub-features: Indexed Field Predicates (WhereField), Opaque Post-Filter Predicates (Where), OR Disjunction (DNF Predicates), Foreign-Key Navigation Joins (L4)