#25797·bevy

Structured dynamic query builders

Author: chescockCreated Sep 15, 2026Updated Sep 15, 2026
LabelsC-FeatureS-Needs-Triage

What problem does this solve or what need does it fill?

Querying for dynamic components in Bevy is possible today, but is usually awkward.

One use case for dynamic components is scripting. If we have a Lua script interpreter, we might want to store LuaValues as components on entities. The set of components will be determined by the scripts, so we don't know the full list at compile time, but we do know that they are each exposed to Rust as a shared LuaValue type.

Another use case is inspectors. We might want to be able to define a query using a UI, and then browse the resulting components. In that case, we want to query existing components, but we won't know the types and will want to expose them using reflection as &dyn Reflect.

How do we do this today?

The current solution is FilteredEntityRef and FilteredEntityMut. These are types that implement QueryData but that can be used to query a dynamically defined set of components. In practice, they can be awkward to use:

Need to track the ComponentId separately

For a static query, we can provide Query<&T> and the user can immediately obtain &Ts. But Query<FilteredEntityRef> isn't enough to get a &LuaValue or &dyn Reflect. We need to pass additional data that identifies the component, such as a ComponentId or a ReflectComponent value. This means a Query<FilteredEntityRef> isn't enough to encapsulate a full query.

Can't express non-archetypal filters

For a static query, we can write Query<&T, Changed<T>>, and the query itself will filter out entities whose T hasn't changed. There is no way to make a dynamic version of this that filters based on a ComponentId being changed. It can be checked manually using get_change_ticks_by_id(), but this is another case where Query<FilteredEntityRef> isn't enough to encapsulate a full query.

Hard to get multiple mutable components

For a static query, we can write Query<(&mut T, &mut U)> to get multiple borrows, with Bevy handling all of the checking for aliasing. But FilteredEntityMut::get_mut_by_id() will borrow the entire FilteredEntityMut, so we need to use complex or unsafe workarounds like get_mut_unchecked() to get multiple mutable components.

Less efficient

For a static query, Bevy will cache pointers to the current table in the WorldQuery::Fetch type, so that fetching a component from each entity is a simple pointer addition. For FilteredEntityRef, we have nowhere to store that cache, and methods like FilteredEntityRef::get_by_id will need to look up the index of the column in the table for each entity iterated.

What solution would you like?

I want to be able express dynamic queries with types like Query<Vec<&mut dyn Reflect>> to query an unknown number of dynamic components, or Query<(&mut LuaValue, &mut LuaValue, &mut Transform)> to query a known set of scripting components with runtime ComponentIds.

This solves the issues above: By building the ComponentIds into the query, we avoid needing to track them separately. Non-archetypal filters could be expressed using buildable QueryFilter types. By declaring the structure up front, we can do the access checking at build time and then offer multiple mutable references during iteration. And we know what is being queried when changing tables, so we have the ability to cache it like we do today (sometimes - see below).

This isn't possible with the current QueryBuilder because it only constructs a FilteredAccess. There is no way to identify which access should go with which &dyn Reflect, and no way to pass the reflection metadata to convert from a pointer to a dyn Reflect.

So, introduce structured query builders, using the same pattern as SystemParamBuilder. A query builder will contain the data required to create the WorldQuery::State for a specific query type. And tuples would be built from tuples of builders, so we can compose the builders into more complex structures.

Examples of buildable query types

  • &T, &mut T, Ref<T>, and Mut<T> could be built using unsafe code from a ComponentId. This would allow multiple dynamic components to be registered using the same Rust type, such as a hypothetical LuaValue.
  • Ptr, PtrMut, and MutUntyped could be built from a ComponentId, exposing the raw pointer directly.
  • &dyn Reflect, &mut dyn Reflect, Ref<dyn Reflect>, and Mut<dyn Reflect> could be built from a ReflectComponent and a ReflectFromPtr. We could offer helper methods that take a TypeId and look them up in the type registry, or from a static type.
  • Any ordinary WorldQuery can be built with no extra data by calling its init_state method. We could represent this either with () or with some special builder type like DefaultQueryBuilder.
  • Vec<T> could be built using a Vec<B> of builders. This can be used to express queries with dynamic numbers of components like Query<Vec<&mut dyn Reflect>>.
  • Tuples could be built using tuples of builders. This can be used to express queries that have both static and dynamic parts, like Query<(&dyn Reflect, &mut Transform)>.
  • Dynamic versions of Added<T> and Changed<T> filters could be built from a ComponentId.
  • Once we have relationship queries, dynamic versions could be built from the ComponentId of the relationship.
  • A dyn QueryFilter filter could be built from any concrete QueryFilter. That would let us use Changed<T> or AssetChanged<A> dynamically.
  • A dyn QueryData data is a little trickier, since we'd need a way to specify the output type. But it should be possible to have a Query<DynQuery<&T>> built from a concrete QueryData that abstracts over Query<&T> and Query<Parent<&T>>.
  • FilteredEntityRef and FilteredEntityMut could be built from an arbitrary Access.

Example of usage

// Create a query builder for `Vec<&dyn Reflect>` as a `Vec<ReflectQueryBuilder>` from a list of `TypeId`s
let type_ids: Vec<TypeId> = ...;
let query_builder: Vec<ReflectQueryBuilder> = type_ids.map(|type_id| ReflectQueryBuilder::new(type_id)).collect();
// Create a query builder for a tuple of static and dynamic parts by creating a tuple of builders
let query_builder = (DefaultQueryBuilder, query_builder);
// And either build a `QueryState` directly
let query_state: QueryState<(NameOrEntity, Vec<&dyn Reflect>)> = query_builder.build(&mut world);
// Or use the query builder to build a system
let system = (QueryParamBuilder::new(query_builder),)
    .build_state(&mut world)
    .build_system(|query: Query<(NameOrEntity, Vec<&dyn Reflect>)>| {
        for (name, values) in query {
            let debug_values = values.into_iter().map(|c| format!("{c:?}"));
            let debug_string = debug_values.collect::<Vec<_>>().join(", ");
            println!("{name}: {debug_string}");
        }
    });

But still less efficient

There are a few places where we won't be able to recapture the full efficiency of static queries.

One is dynamically-sized WorldQuery::Fetch values for dyn QueryData and Vec<D>. Since the size of Fetch depends on the runtime type or size, we can't create it directly on the stack. We can avoid needing to allocate it on the heap, though, by giving up the efficiency gains from caching data on each archetype or table: Vec<D> can yield items that store an UnsafeEntityCell and the &'state [D::State] and only create the D::Item on-demand. So I don't expect that to be any worse than FilteredEntityMut, but it won't be better, either.

The other is StorageType. A static query knows whether each component is a table component or sparse set component, and can do compile-time branching to only compile the correct lookup. Even when using FilteredEntityMut, the get_components method will be specialized to the component time and only need to emit code for one storage type. But a type-erased Ptr query or Changed filter will have to include runtime branches. Note that &dyn Reflect will already need to call a function pointer somewhere to convert the raw pointer to a trait object, so we may be able to include the storage type branching in there.