Skip to content

Nested Queries#21557

Merged
alice-i-cecile merged 39 commits intobevyengine:mainfrom
chescock:query-multiple-access
Feb 23, 2026
Merged

Nested Queries#21557
alice-i-cecile merged 39 commits intobevyengine:mainfrom
chescock:query-multiple-access

Conversation

@chescock
Copy link
Copy Markdown
Contributor

@chescock chescock commented Oct 15, 2025

Objective

Support queries that soundly access multiple entities.

This can be used to create queries that follow relations, as in #17647.

This can also be used to create queries that perform resource access. This has been supported since #16843, although that approach may become unsound if we do resources-as-components #19731, such as #21346.

Fixes #20315

Solution

Allow a QueryData that wants to access other entities to store a QueryState<D, F> in its WorldQuery::State, so that it can create a nested Query<D, F> during the outer fetch.

NestedQuery type

Introduce a NestedQuery type that implements QueryData by yielding a Query. It is intended to be used inside other implementations of QueryData, either for manual implementations or #[derive(QueryData)]. It is not normally useful to query directly, since it's equivalent to adding another Query parameter to a system.

In theory, we could directly impl QueryData for Query, but this would be too easy to do accidentally. Having to explicitly import and write NestedQuery will make it clear that it's something unusual, and also allows us to remove the need for passing 'static for the 'w and 's lifetimes.

New WorldQuery methods

For it to be sound to create the Query during fetch, we need to register the FilteredAccess of the nested query and check for conflicts with other parameters. Create a WorldQuery::update_external_component_access method for that purpose. For Query as SystemParam, call this during init_access so the access can be combined with the rest of the system access. For loose QueryStates, call it during QueryState::new.

In order to keep the query cache up-to-date, create a WorldQuery::update_archetypes method where it can call QueryState::update_archetypes_unsafe_world_cell, and call it from there.

New QueryData subtraits

Some operations would not be sound with nested queries! In particular, we want a Parent<D> query that reads data from the parent entity by following the ChildOf relation. But many entities may share a parent, so it's not sound to iterate a Query<Parent<&mut C>>.

It is sound to get_mut, though, so we want the query type to exist, just not be iterable. And following the relation in the other direction for a Query<Children<&mut C>> is sound to iterate, since children are unique to a given parent.

So, introduce two new QueryData subtraits:

Note that SingleEntityQueryData: IterQueryData, since single-entity queries never alias data across entities, and ReadOnlyQueryData: IterQueryData, since it's always sound to alias read-only data.

Here is a summary of the traits implemented by some representative QueryData:

Data Iter ReadOnly SingleEntity
&T
&mut T x
Parent<&T> x
Parent<&mut T> x x x
(&mut T, Parent<&U>) x x
Children<&mut T> x x

Alternatives

We could avoid the need for the IterQueryData trait by making it a requirement for all QueryData. That would reduce the number of traits required, at the cost of making it impossible to support Query<Parent<&mut C>>.

Showcase

Here is an implementation of a Related<R, D, F> query using this PR:

Details
pub struct Related<R: Relationship, D: QueryData + 'static, F: QueryFilter + 'static = ()>(
    RelatedInner<R, D, F>,
);

type RelatedInner<R, D, F> = (
    &'static R,
    NestedQuery<D, (F, With<<R as Relationship>::RelationshipTarget>)>,
);

unsafe impl<R: Relationship, D: QueryData + 'static, F: QueryFilter + 'static> WorldQuery
    for Related<R, D, F>
{
    type Fetch<'w> = <RelatedInner<R, D, F> as WorldQuery>::Fetch<'w>;
    type State = <RelatedInner<R, D, F> as WorldQuery>::State;

    fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> {
        <RelatedInner<R, D, F> as WorldQuery>::shrink_fetch(fetch)
    }

    unsafe fn init_fetch<'w, 's>(
        world: UnsafeWorldCell<'w>,
        state: &'s Self::State,
        last_run: Tick,
        this_run: Tick,
    ) -> Self::Fetch<'w> {
        unsafe {
            <RelatedInner<R, D, F> as WorldQuery>::init_fetch(world, state, last_run, this_run)
        }
    }

    const IS_DENSE: bool = <RelatedInner<R, D, F> as WorldQuery>::IS_DENSE;

    unsafe fn set_archetype<'w, 's>(
        fetch: &mut Self::Fetch<'w>,
        state: &'s Self::State,
        archetype: &'w Archetype,
        table: &'w Table,
    ) {
        unsafe {
            <RelatedInner<R, D, F> as WorldQuery>::set_archetype(fetch, state, archetype, table)
        };
    }

    unsafe fn set_table<'w, 's>(
        fetch: &mut Self::Fetch<'w>,
        state: &'s Self::State,
        table: &'w Table,
    ) {
        unsafe { <RelatedInner<R, D, F> as WorldQuery>::set_table(fetch, state, table) };
    }

    fn update_component_access(state: &Self::State, access: &mut FilteredAccess) {
        <RelatedInner<R, D, F> as WorldQuery>::update_component_access(state, access);
    }

    fn init_nested_access(
        state: &Self::State,
        system_name: Option<&str>,
        component_access_set: &mut FilteredAccessSet,
        world: UnsafeWorldCell,
    ) {
        <RelatedInner<R, D, F> as WorldQuery>::init_nested_access(state, system_name, component_access_set, world);
    }

    fn init_state(world: &mut World) -> Self::State {
        <RelatedInner<R, D, F> as WorldQuery>::init_state(world)
    }

    fn get_state(components: &Components) -> Option<Self::State> {
        <RelatedInner<R, D, F> as WorldQuery>::get_state(components)
    }

    fn matches_component_set(
        state: &Self::State,
        set_contains_id: &impl Fn(ComponentId) -> bool,
    ) -> bool {
        <RelatedInner<R, D, F> as WorldQuery>::matches_component_set(state, set_contains_id)
    }

    fn update_archetypes(state: &mut Self::State, world: UnsafeWorldCell) {
        <RelatedInner<R, D, F> as WorldQuery>::update_archetypes(state, world);
    }
}

unsafe impl<R: Relationship, D: QueryData + 'static, F: QueryFilter + 'static> QueryData
    for Related<R, D, F>
{
    const IS_READ_ONLY: bool = D::IS_READ_ONLY;
    type ReadOnly = Related<R, D::ReadOnly, F>;
    type Item<'w, 's> = Option<D::Item<'w, 's>>;

    fn shrink<'wlong: 'wshort, 'wshort, 's>(
        item: Self::Item<'wlong, 's>,
    ) -> Self::Item<'wshort, 's> {
        item.map(D::shrink)
    }

    unsafe fn fetch<'w, 's>(
        state: &'s Self::State,
        fetch: &mut Self::Fetch<'w>,
        entity: Entity,
        table_row: TableRow,
    ) -> Self::Item<'w, 's> {
        let (relationship, query) =
            unsafe { <RelatedInner<R, D, F> as QueryData>::fetch(state, fetch, entity, table_row) };
        query.get_inner(relationship.get()).ok()
    }
}

unsafe impl<R: Relationship, D: ReadOnlyQueryData + 'static, F: QueryFilter + 'static> ReadOnlyQueryData for Related<R, D, F> { }

// Note that we require `D: ReadOnlyQueryData` for `Related: IterQueryData`
unsafe impl<R: Relationship, D: ReadOnlyQueryData + 'static, F: QueryFilter + 'static> IterQueryData for Related<R, D, F> { }

I'd like to leave that to a follow-up PR to allow bikeshedding the API, and to take advantage of #21581 to remove the Option, but I think it works!

Future Work

There is more to do here, but this PR is already pretty big. Future work includes:

  • WorldQuery types for working with relationships #17647
  • Following Store resources as components on singleton entities (v2) #21346, update AssetChanged to use nested queries for resource access, and stop tracking resource access separately in Access
  • Implement get_state for NestedQuery. This is difficult because constructing a QueryState requires reading the DefaultQueryFilters resource, but get_state can be called from transmute with no access.
  • Relax the SingleEntityQueryData bound on transmutes and joins. This will require checking that the nested query access is also a subset of the original access. Although unless we also solve the problem of implementing get_state, transmuting to a query with nested queries won't work anyway.
  • Support streaming iteration for QueryIter by offering a fn fetch_next(&self) -> D::Item<'_> method and relaxing the IterQueryData bound on Query::into_iter and Query::iter_mut. This would work similar to iter_many_mut and iter_many_inner.
  • Relax the IterQueryData bound on Query::single_inner, Query::single_mut, and Single<D, F>. This seems like it should be straightforward, because the method only returns a single item. But the way it checks that there is only one item is by fetching the second one!

@chescock chescock added C-Feature A new feature, making something new possible A-ECS Entities, components, systems, and events M-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide D-Unsafe Touches with unsafe code in some way S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Oct 15, 2025
@alice-i-cecile alice-i-cecile added D-Complex Quite challenging from either a design or technical perspective. Ask for help! M-Release-Note Work that should be called out in the blog due to impact labels Oct 15, 2025
@alice-i-cecile alice-i-cecile added this to the 0.18 milestone Oct 15, 2025
@Freyja-moth
Copy link
Copy Markdown
Contributor

so it's not sound to iterate a Query<Parent<&mut C>>.

I'm not certain it's the best way of going about it but couldn't we implement this by storing the entity of the parent instead of the data and then resolving the data from the entity when it is needed?

@chescock
Copy link
Copy Markdown
Contributor Author

couldn't we implement this by storing the entity of the parent instead of the data and then resolving the data from the entity when it is needed?

I'm not sure what you mean. Like, instead of yielding a D::Item<'w, 's>, we could yield some type Foo with a fn get_mut(&mut self) -> D::Item<'_, '_>? That wouldn't help, since it would still be possible to collect the Foo values and then call get_mut on several of them concurrently, like:

let mut items = query.iter_mut().collect::<Vec<_>>();
let mapped = items.iter_mut().map(|item| item.get_mut()).collect::<Vec<_>>();

@cBournhonesque
Copy link
Copy Markdown
Contributor

I think these changes are a great idea; I guess I would like to know how this contrasts to how flecs queries for relation data. Maybe @james-j-obrien knows?

@james-j-obrien
Copy link
Copy Markdown
Contributor

I think these changes are a great idea; I guess I would like to know how this contrasts to how flecs queries for relation data. Maybe @james-j-obrien knows?

That's a quite involved question to answer (although a very interesting one).

The big difference between the bevy and flecs queries are tied to one being defined in the type system and the other being defined dynamically. Due to this bevy queries are fundamentally based on nesting, you have tuples of query terms that each store their own state and generate their own code for managing that state. In flecs all the query terms are just stored in a flat array.

For example in this PR we express querying our parent as creating a query term that traverses the relationship and then nested in that is the set of components we want to access on the target, whereas in flecs you would have a set of instructions that said: "get me any entity A with relationship of the form (ChildOf, B) and store B as a variable", "get me component Y on entity B", "get me component Z on entity B".

This structure allows flecs to optimize/batch/reorder terms since they can be evaluated in the full context of the rest of the query, but for simple queries it's mostly a different path to the same goal.

Since flecs also has fragmenting relations they can do stuff like cache the tables for B since you know that entities in A's table will always have parent B.

All that being said, with bevy's queries as they exist today this PR seems like the shortest path to querying on multiple entities so seems like a positive step.

@cBournhonesque
Copy link
Copy Markdown
Contributor

cBournhonesque commented Oct 19, 2025

I think these changes are a great idea; I guess I would like to know how this contrasts to how flecs queries for relation data. Maybe @james-j-obrien knows?

That's a quite involved question to answer (although a very interesting one).

The big difference between the bevy and flecs queries are tied to one being defined in the type system and the other being defined dynamically. Due to this bevy queries are fundamentally based on nesting, you have tuples of query terms that each store their own state and generate their own code for managing that state. In flecs all the query terms are just stored in a flat array.

For example in this PR we express querying our parent as creating a query term that traverses the relationship and then nested in that is the set of components we want to access on the target, whereas in flecs you would have a set of instructions that said: "get me any entity A with relationship of the form (ChildOf, B) and store B as a variable", "get me component Y on entity B", "get me component Z on entity B".

This structure allows flecs to optimize/batch/reorder terms since they can be evaluated in the full context of the rest of the query, but for simple queries it's mostly a different path to the same goal.

Since flecs also has fragmenting relations they can do stuff like cache the tables for B since you know that entities in A's table will always have parent B.

All that being said, with bevy's queries as they exist today this PR seems like the shortest path to querying on multiple entities so seems like a positive step.

Thanks for the answer!
I guess bevy can also use the dynamic QueryBuilder, but the entire design is heavily influenced by primarily using types.
After reading https://ajmmertens.medium.com/building-games-in-ecs-with-entity-relationships-657275ba2c6c, it sounds like flecs creates some kind of data structure (node graph) that allow it to efficiently match entities.

I guess we could do something similar: for tuple queries, build such a node graph and use it to match entities. I guess we do already create a data structure that helps us find matching entities; that data structure is the QueryState.
The main difference that our state simply has:

  • matched archetypes (from QueryData::matches_component_set)
  • uses types to filter out entities (F::filter)

And the main difference is that the flecs "QueryState" is more elaborate since it can contain sources, relationships, etc.
So this PR's NestedQueries is one way to add more complexity to our QueryState. But we still have a simple 'combined query' since our Tuple QueryState combines the inner WorldQueries' QueryState in a very simple manner. In flecs it would combine them by adding them into a dynamic graph that can then be optimized. Our equivalent would be to add a WorldQuery::add_to_query_plan method so that we would also be able to optimize a query that contains multiple terms

@alice-i-cecile
Copy link
Copy Markdown
Member

@eugineerd, I liked your work over in #21601; can I get your review here in turn?

…e-access

# Conflicts:
#	crates/bevy_ecs/src/world/unsafe_world_cell.rs
This reverts commit 0660ceb.

Resources are entities now!
Copy link
Copy Markdown
Contributor

@Trashtalk217 Trashtalk217 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The query system is starting to feel like trait soup and SingleEntityQueryData is not a great name (one word to many and Single puts me on the wrong foot). Still, I approve this because it enables some really usefull possibilities, including fixing AssetChanged (#23057). I also can't think of a better name than SingleEntityQueryData...

Really well done @chescock.

@Trashtalk217 Trashtalk217 added S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Feb 20, 2026
@github-project-automation github-project-automation bot moved this to Needs SME Triage in ECS Feb 20, 2026
@alice-i-cecile alice-i-cecile moved this from Needs SME Triage to SME Triaged in ECS Feb 20, 2026
@alice-i-cecile alice-i-cecile removed the M-Release-Note Work that should be called out in the blog due to impact label Feb 23, 2026
Copy link
Copy Markdown
Member

@alice-i-cecile alice-i-cecile left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've done a careful review of this now, and think it's both well-architected and carefully constructed.

I was afraid this would be dramatically more complex, but the new traits in query/fetch.rs really are the heart of it.

Merging; I'm excited to see the follow-up work!

@alice-i-cecile alice-i-cecile added this pull request to the merge queue Feb 23, 2026
Merged via the queue into bevyengine:main with commit fc69a76 Feb 23, 2026
40 checks passed
@github-project-automation github-project-automation bot moved this from SME Triaged to Done in ECS Feb 23, 2026
ChristopherBiscardi added a commit to ChristopherBiscardi/bevy_seedling that referenced this pull request Feb 24, 2026
github-merge-queue bot pushed a commit that referenced this pull request Mar 1, 2026
# Objective

Fixes #23057.

## Solution

Uses Nested Queries from #21557 to also register the resource entity to
the access set.

## Testing

Added an extra test.

---------

Co-authored-by: Chris Russell <8494645+chescock@users.noreply.github.com>
github-merge-queue bot pushed a commit that referenced this pull request Mar 11, 2026
# Objective

Follow-up to #21557

Support streaming iteration (a.k.a. lending iteration) for `QueryIter`
and `QuerySortedIter` so that it's possible to iterate
`Query<Parent<&mut T>>`.

Double-check that all the `IterQueryData` bounds are correct and that
the safety comments are correct. A few were not!

## Solution

Offer a `fetch_next` method on `QueryIter` and `QuerySortedIter` that
borrows the entire iterator to prevent aliasing, similar to
`QueryManyIter::QueryManyIter`.

Offer a `Query::iter_inner` method to produce `QueryIter` even when it
is not an `Iterator`. We cannot rely on `IntoIterator` in that case, and
Clippy objects to an inherent method called `into_iter()` that is not
the standard trait method. This supersedes the existing `iter_inner`
method that only worked on `ReadOnlyQueryData`.

Add a missing `IterQueryData` bound to `iter_combinations_mut`. It
yields multiple entities concurrently, so is never sound to use with
non-`IterQueryData`. I think the reason I missed this originally is that
it already has a `fetch_next` method and conditional `Iterator` impl.
But even `fetch_next` yields multiple entities at once for
`QueryCombinationIter`!

Add a `ContiguousQueryData: IterQueryData` supertrait bound.
`QueryContiguousIter` also yields multiple entities concurrently, so it
does not make sense on non-iterable data. This was not actually unsound,
though, as `QueryContiguousIter` does not call `fetch`.

Finally, update some missing or incorrect safety comments.  

### Verify bounds on the other iterator types

To verify I didn't miss any bounds on iterator types, here is a list of
every `Query*Iter` type, and whether they support non-`IterQueryData`:

| Type | Supports non-`Iter`? |
| --- | --- |
|`CombinationIter`|No, added missing bound|
|`ContiguousIter`|No, added missing bound|
|`Iter`|Yes, added support|
|`ManyIter`|Yes, existing support|
|`ManyUniqueIter`|No, and existing bound*|
|`ParIter`|No, and existing bound|
|`ParManyIter`|No, and existing bound|
|`ParManyUniqueIter`|No, and existing bound|
|`SortedIter`|Yes, added support|
|`SortedManyIter`|Yes, existing support|

`Iter` and `SortedIter` were changed in this PR to support streaming
iteration for all `QueryData`, while only implementing `Iterator` for
`IterQueryData`.

`ManyIter` and `SortedManyIter` already had streaming iteration, and
only implement `Iterator` with a stricter `ReadOnlyQueryData` bound,
since they may yield the same entity multiple times.

`ManyUniqueIter` could theoretically be used with non-`IterQueryData`...
but if you need to do streaming iteration anyway then you can call
`iter_many_mut()` instead of `iter_many_unique_mut()`.

`CombinationIter`, `ContiguousIter`, and the `Par*Iter` types are all
fundamentally about accessing multiple entities concurrently, and should
always require `IterQueryData`. `CombinationIter` and `ContiguousIter`
did not have `IterQueryData` bounds, so fix that!

## Showcase

This is cheating a bit because we don't yet have `Parent<&mut T>`, but
once we do it would look like:

```rust
fn system(query: Query<Parent<&mut Component>>) {
    let mut iter = query.iter_mut();
    while let Some(mut parent_component) = iter.fetch_next() {
        // Use `parent_component` here
    }
}
```
ChristopherBiscardi added a commit to ChristopherBiscardi/bevy_seedling that referenced this pull request Mar 17, 2026
CorvusPrudens pushed a commit to CorvusPrudens/bevy_seedling that referenced this pull request Mar 17, 2026
* 0.19.0-dev

* with patches for running local examples

* replace became discard

* reflection reorg changes

* glam 32

* nested queries have landed: bevyengine/bevy#21557
github-merge-queue bot pushed a commit that referenced this pull request Apr 9, 2026
…a` impl (#23686)

# Objective

Demonstrate how to use `NestedQuery` to implement relational queries.  

There is more design work required before making first-party relational
queries, but third-party implementations are already possible using
`NestedQuery`. The documentation does not make it clear how to actually
implement them, though, and the only real example is buried in the PR
description of #21557.

## Solution

Expand the examples for `NestedQuery` to include the implementation of a
`Parent<D>` relational query. Keep one example that uses
`#[derive(QueryData)]` with a method on the generated `Item` type, but
also add an example with a manual `QueryData` impl that delegates
everything.

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: Kevin Chen <chen.kevin.f@gmail.com>
viridia pushed a commit to viridia/bevy that referenced this pull request Apr 10, 2026
…a` impl (bevyengine#23686)

# Objective

Demonstrate how to use `NestedQuery` to implement relational queries.  

There is more design work required before making first-party relational
queries, but third-party implementations are already possible using
`NestedQuery`. The documentation does not make it clear how to actually
implement them, though, and the only real example is buried in the PR
description of bevyengine#21557.

## Solution

Expand the examples for `NestedQuery` to include the implementation of a
`Parent<D>` relational query. Keep one example that uses
`#[derive(QueryData)]` with a method on the generated `Item` type, but
also add an example with a manual `QueryData` impl that delegates
everything.

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: Kevin Chen <chen.kevin.f@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-ECS Entities, components, systems, and events C-Feature A new feature, making something new possible D-Complex Quite challenging from either a design or technical perspective. Ask for help! D-Unsafe Touches with unsafe code in some way M-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Unsound to call EntityRef::get_components with a QueryData that performs resource access

9 participants