From a6eeb6f4ad089579e7749f2cdb2aa7fca5cc7b76 Mon Sep 17 00:00:00 2001 From: Chris Russell <8494645+chescock@users.noreply.github.com> Date: Tue, 24 Feb 2026 16:02:46 -0500 Subject: [PATCH 1/6] Require `IterQueryData` for `ContiguousQueryData`, since it yields multiple items concurrently. --- crates/bevy_ecs/src/query/fetch.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index f52f8b8076358..44aac109fd11f 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -406,7 +406,7 @@ pub unsafe trait QueryData: WorldQuery { label = "invalid contiguous `Query` data", note = "if `{Self}` is a custom query type, using `QueryData` derive macro, ensure that the `#[query_data(contiguous(target))]` attribute is added" )] -pub trait ContiguousQueryData: ArchetypeQueryData { +pub trait ContiguousQueryData: ArchetypeQueryData + IterQueryData { /// Item returned by [`ContiguousQueryData::fetch_contiguous`]. /// Represents a contiguous chunk of memory. type Contiguous<'w, 's>; From eee21fb03c4e17cc1c8254ead74abb0efbfb1962 Mon Sep 17 00:00:00 2001 From: Chris Russell <8494645+chescock@users.noreply.github.com> Date: Tue, 24 Feb 2026 16:03:37 -0500 Subject: [PATCH 2/6] Require `IterQueryData` for `QueryCombinationsIter`, since it yields multiple items concurrently. --- crates/bevy_ecs/src/query/iter.rs | 15 +++++++++++---- crates/bevy_ecs/src/query/state.rs | 10 ++++++++-- crates/bevy_ecs/src/system/query.rs | 17 ++++++++++++----- .../migration-guides/nested_queries.md | 3 ++- 4 files changed, 33 insertions(+), 12 deletions(-) diff --git a/crates/bevy_ecs/src/query/iter.rs b/crates/bevy_ecs/src/query/iter.rs index 9c620dc977cfa..9e0dcdc0b314e 100644 --- a/crates/bevy_ecs/src/query/iter.rs +++ b/crates/bevy_ecs/src/query/iter.rs @@ -2386,14 +2386,16 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> Debug /// [`Query`]: crate::system::Query /// [`Query::iter_combinations`]: crate::system::Query::iter_combinations /// [`Query::iter_combinations_mut`]: crate::system::Query::iter_combinations_mut -pub struct QueryCombinationIter<'w, 's, D: QueryData, F: QueryFilter, const K: usize> { +pub struct QueryCombinationIter<'w, 's, D: IterQueryData, F: QueryFilter, const K: usize> { tables: &'w Tables, archetypes: &'w Archetypes, query_state: &'s QueryState, cursors: [QueryIterationCursor<'w, 's, D, F>; K], } -impl<'w, 's, D: QueryData, F: QueryFilter, const K: usize> QueryCombinationIter<'w, 's, D, F, K> { +impl<'w, 's, D: IterQueryData, F: QueryFilter, const K: usize> + QueryCombinationIter<'w, 's, D, F, K> +{ /// # Safety /// - `world` must have permission to access any of the components registered in `query_state`. /// - `world` must be the same one used to initialize `query_state`. @@ -2552,7 +2554,7 @@ impl<'w, 's, D: ReadOnlyQueryData, F: QueryFilter, const K: usize> FusedIterator { } -impl<'w, 's, D: QueryData, F: QueryFilter, const K: usize> Debug +impl<'w, 's, D: IterQueryData, F: QueryFilter, const K: usize> Debug for QueryCombinationIter<'w, 's, D, F, K> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { @@ -2647,7 +2649,10 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { /// The result of `next` and any previous calls to `peek_last` with this row must have been /// dropped to prevent aliasing mutable references. #[inline] - unsafe fn peek_last(&mut self, query_state: &'s QueryState) -> Option> { + unsafe fn peek_last(&mut self, query_state: &'s QueryState) -> Option> + where + D: IterQueryData, + { if self.current_row > 0 { let index = self.current_row - 1; if self.is_dense { @@ -2656,6 +2661,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { // SAFETY: // - `set_table` must have been called previously either in `next` or before it. // - `*entity` and `index` are in the current table. + // - `D: IterQueryData` unsafe { D::fetch( &query_state.fetch_state, @@ -2672,6 +2678,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { // SAFETY: // - `set_archetype` must have been called previously either in `next` or before it. // - `archetype_entity.id()` and `archetype_entity.table_row()` are in the current archetype. + // - `D: IterQueryData` unsafe { D::fetch( &query_state.fetch_state, diff --git a/crates/bevy_ecs/src/query/state.rs b/crates/bevy_ecs/src/query/state.rs index f41769ce9a8c7..e6f52a5251bbe 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -1254,7 +1254,10 @@ impl QueryState { pub fn iter_combinations_mut<'w, 's, const K: usize>( &'s mut self, world: &'w mut World, - ) -> QueryCombinationIter<'w, 's, D, F, K> { + ) -> QueryCombinationIter<'w, 's, D, F, K> + where + D: IterQueryData, + { self.query_mut(world).iter_combinations_inner() } @@ -1406,7 +1409,10 @@ impl QueryState { pub unsafe fn iter_combinations_unchecked<'w, 's, const K: usize>( &'s mut self, world: UnsafeWorldCell<'w>, - ) -> QueryCombinationIter<'w, 's, D, F, K> { + ) -> QueryCombinationIter<'w, 's, D, F, K> + where + D: IterQueryData, + { // SAFETY: Upheld by caller unsafe { self.query_unchecked(world) }.iter_combinations_inner() } diff --git a/crates/bevy_ecs/src/system/query.rs b/crates/bevy_ecs/src/system/query.rs index b455cb8e08549..2f61da3b55f46 100644 --- a/crates/bevy_ecs/src/system/query.rs +++ b/crates/bevy_ecs/src/system/query.rs @@ -767,9 +767,10 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// - [`iter_combinations`](Self::iter_combinations) for read-only query item combinations. /// - [`iter_combinations_inner`](Self::iter_combinations_inner) for mutable query item combinations with the full `'world` lifetime. #[inline] - pub fn iter_combinations_mut( - &mut self, - ) -> QueryCombinationIter<'_, 's, D, F, K> { + pub fn iter_combinations_mut(&mut self) -> QueryCombinationIter<'_, 's, D, F, K> + where + D: IterQueryData, + { self.reborrow().iter_combinations_inner() } @@ -798,7 +799,10 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// - [`iter_combinations`](Self::iter_combinations) for read-only query item combinations. /// - [`iter_combinations_mut`](Self::iter_combinations_mut) for mutable query item combinations. #[inline] - pub fn iter_combinations_inner(self) -> QueryCombinationIter<'w, 's, D, F, K> { + pub fn iter_combinations_inner(self) -> QueryCombinationIter<'w, 's, D, F, K> + where + D: IterQueryData, + { // SAFETY: `self.world` has permission to access the required components. unsafe { QueryCombinationIter::new(self.world, self.state, self.last_run, self.this_run) } } @@ -1139,7 +1143,10 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { #[inline] pub unsafe fn iter_combinations_unsafe( &self, - ) -> QueryCombinationIter<'_, 's, D, F, K> { + ) -> QueryCombinationIter<'_, 's, D, F, K> + where + D: IterQueryData, + { // SAFETY: The caller promises that this will not result in multiple mutable references. unsafe { self.reborrow_unsafe() }.iter_combinations_inner() } diff --git a/release-content/migration-guides/nested_queries.md b/release-content/migration-guides/nested_queries.md index d23a98db85e9b..42cb7e59d1fb0 100644 --- a/release-content/migration-guides/nested_queries.md +++ b/release-content/migration-guides/nested_queries.md @@ -17,8 +17,9 @@ An `IterQueryData` bound has been added to iteration methods on `Query`: * `get_many_mut` / `get_many_inner` / `get_many_unique_mut` / `get_many_unique_inner` * `par_iter_mut` / `par_iter_inner` / `par_iter_many_unique_mut` * `single_mut` / `single_inner` +* `iter_combinations_mut` / `iter_combinations_inner` / `iter_combinations_unsafe` -`iter`, `iter_many`, `par_iter`, and `single` have no extra bounds, +`iter`, `iter_many`, `par_iter`, `single`, and `iter_combinations` have no extra bounds, since read-only queries are always sound to iterate. `iter_many_mut` and `iter_many_inner` methods have no extra bounds, either, since they already prohibit concurrent access to multiple entities. From 27b9dc6a625e81f8cfaf55502ea35dcb20b10f5f Mon Sep 17 00:00:00 2001 From: Chris Russell <8494645+chescock@users.noreply.github.com> Date: Wed, 25 Feb 2026 13:08:28 -0500 Subject: [PATCH 3/6] Fix up some incorrect or missing safety comments. --- crates/bevy_ecs/src/query/iter.rs | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/crates/bevy_ecs/src/query/iter.rs b/crates/bevy_ecs/src/query/iter.rs index 9e0dcdc0b314e..01317524f50af 100644 --- a/crates/bevy_ecs/src/query/iter.rs +++ b/crates/bevy_ecs/src/query/iter.rs @@ -956,8 +956,9 @@ impl<'w, 's, D: IterQueryData, F: QueryFilter> Iterator for QueryIter<'w, 's, D, #[inline(always)] fn next(&mut self) -> Option { // SAFETY: - // `tables` and `archetypes` belong to the same world that the cursor was initialized for. - // `query_state` is the state that was passed to `QueryIterationCursor::init`. + // - `tables` and `archetypes` belong to the same world that the cursor was initialized for. + // - `query_state` is the state that was passed to `QueryIterationCursor::init`. + // - `D: IterQueryData` unsafe { self.cursor .next(self.tables, self.archetypes, self.query_state) @@ -1173,8 +1174,8 @@ where /// # Safety /// /// - `entity` must stem from `self.entity_iter` - /// - If `Self` does not impl `ReadOnlyQueryData`, then there must not be any other `Item`s alive for the current entity - /// - If `Self` does not impl `IterQueryData`, then there must not be any other `Item`s alive for *any* entity + /// - If `D` does not impl `ReadOnlyQueryData`, then there must not be any other `Item`s alive for the current entity + /// - If `D` does not impl `IterQueryData`, then there must not be any other `Item`s alive for *any* entity #[inline(always)] unsafe fn fetch_next(&mut self, entity: Entity) -> Option> { let (location, archetype, table); @@ -1345,8 +1346,8 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> /// # Safety /// /// - All arguments must stem from the same valid `QueryManyIter`. - /// - If `Self` does not impl `ReadOnlyQueryData`, then there must not be any other `Item`s alive for the current entity - /// - If `Self` does not impl `IterQueryData`, then there must not be any other `Item`s alive for *any* entity + /// - If `D` does not impl `ReadOnlyQueryData`, then there must not be any other `Item`s alive for the current entity + /// - If `D` does not impl `IterQueryData`, then there must not be any other `Item`s alive for *any* entity #[inline(always)] unsafe fn fetch_next_aliased_unchecked( entity_iter: impl Iterator, @@ -2181,8 +2182,8 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> /// # Safety /// /// - `entity` must stem from `self.entity_iter` - /// - If `Self` does not impl `ReadOnlyQueryData`, then there must not be any other `Item`s alive for the current entity - /// - If `Self` does not impl `IterQueryData`, then there must not be any other `Item`s alive for *any* entity + /// - If `D` does not impl `ReadOnlyQueryData`, then there must not be any other `Item`s alive for the current entity + /// - If `D` does not impl `IterQueryData`, then there must not be any other `Item`s alive for *any* entity #[inline(always)] unsafe fn fetch_next_aliased_unchecked(&mut self, entity: Entity) -> Option> { let (location, archetype, table); @@ -2714,9 +2715,11 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { // QueryState::par_fold_init_unchecked_manual, QueryState::par_many_fold_init_unchecked_manual, // QueryState::par_many_unique_fold_init_unchecked_manual, QueryContiguousIter::next /// # Safety - /// `tables` and `archetypes` must belong to the same world that the [`QueryIterationCursor`] - /// was initialized for. - /// `query_state` must be the same [`QueryState`] that was passed to `init` or `init_empty`. + /// - `tables` and `archetypes` must belong to the same world that the [`QueryIterationCursor`] + /// was initialized for. + /// - `query_state` must be the same [`QueryState`] that was passed to `init` or `init_empty`. + /// - If `D` does not impl `ReadOnlyQueryData`, then there must not be any other `Item`s alive for the current entity + /// - If `D` does not impl `IterQueryData`, then there must not be any other `Item`s alive for *any* entity #[inline(always)] unsafe fn next( &mut self, @@ -2763,6 +2766,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { // - `current_row` must be a table row in range of the current table, // because if it was not, then the above would have been executed. // - fetch is only called once for each `entity`. + // - caller ensures no conflicting `Item`s are alive let item = unsafe { D::fetch(&query_state.fetch_state, &mut self.fetch, *entity, row) }; if let Some(item) = item { @@ -2821,6 +2825,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { // - `current_row` must be an archetype index row in range of the current archetype, // because if it was not, then the if above would have been executed. // - fetch is only called once for each `archetype_entity`. + // - caller ensures no conflicting `Item`s are alive let item = unsafe { D::fetch( &query_state.fetch_state, From 66d8fd26bc3f9d286fd6fd52f7755e46509bd135 Mon Sep 17 00:00:00 2001 From: Chris Russell <8494645+chescock@users.noreply.github.com> Date: Tue, 24 Feb 2026 16:30:57 -0500 Subject: [PATCH 4/6] Add `fetch_next` to `QueryIter` to support streaming iteration where necessary. --- crates/bevy_ecs/src/query/fetch.rs | 3 + crates/bevy_ecs/src/query/iter.rs | 32 +++++++++ crates/bevy_ecs/src/query/state.rs | 16 ++--- crates/bevy_ecs/src/system/query.rs | 72 +++++++++---------- .../migration-guides/nested_queries.md | 11 ++- 5 files changed, 82 insertions(+), 52 deletions(-) diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 44aac109fd11f..5224c3379d7bf 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -434,6 +434,9 @@ pub trait ContiguousQueryData: ArchetypeQueryData + IterQueryData { /// This is how methods like [`Iterator::collect`] work. /// It is therefore unsound to offer an [`Iterator`] for a [`QueryData`] for which only one instance may be alive concurrently. /// +/// To iterate over a [`QueryData`] that does not implement [`IterQueryData`], +/// use the [`QueryIter::fetch_next()`](crate::query::QueryIter::fetch_next) method. +/// /// For `QueryData` that implement this trait, [`QueryData::fetch`] may be called for one entity while an item is still alive for a different entity. /// /// All [`SingleEntityQueryData`] types are [`IterQueryData`]. diff --git a/crates/bevy_ecs/src/query/iter.rs b/crates/bevy_ecs/src/query/iter.rs index 01317524f50af..00881c53d8d00 100644 --- a/crates/bevy_ecs/src/query/iter.rs +++ b/crates/bevy_ecs/src/query/iter.rs @@ -128,6 +128,38 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { cursor: self.cursor.reborrow(), } } + + /// Get next result from the query. + /// + /// This can be used to iterate over queries that do not implement [`IterQueryData`]. + /// Most queries do implement that trait, and can use the ordinary [`Iterator::next`] + /// method or a `for` loop. + /// + /// # Example + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # #[derive(Component)] + /// # struct C; + /// fn system(mut query: Query<&mut C>) { + /// let mut iter = query.iter_mut(); + /// while let Some(mut c) = iter.fetch_next() { + /// // + /// } + /// } + /// # bevy_ecs::system::assert_is_system(system); + /// ``` + pub fn fetch_next(&mut self) -> Option> { + // SAFETY: + // - `tables` and `archetypes` belong to the same world that the cursor was initialized for. + // - `query_state` is the state that was passed to `QueryIterationCursor::init`. + // - `self` is mutably borrowed, so there are no other items alive for any entity. + unsafe { + self.cursor + .next(self.tables, self.archetypes, self.query_state) + .map(D::shrink) + } + } } impl<'w, 's, D: IterQueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { diff --git a/crates/bevy_ecs/src/query/state.rs b/crates/bevy_ecs/src/query/state.rs index e6f52a5251bbe..9f44cd8ecfb10 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -1174,7 +1174,7 @@ impl QueryState { /// consider using [`Self::update_archetypes`] followed by multiple [`Self::iter_manual`] calls. #[inline] pub fn iter<'w, 's>(&'s mut self, world: &'w World) -> QueryIter<'w, 's, D::ReadOnly, F> { - self.query(world).into_iter() + self.query(world).iter_inner() } /// Returns an [`Iterator`] over the query results for the given [`World`]. @@ -1182,11 +1182,8 @@ impl QueryState { /// This iterator is always guaranteed to return results from each matching entity once and only once. /// Iteration order is not guaranteed. #[inline] - pub fn iter_mut<'w, 's>(&'s mut self, world: &'w mut World) -> QueryIter<'w, 's, D, F> - where - D: IterQueryData, - { - self.query_mut(world).into_iter() + pub fn iter_mut<'w, 's>(&'s mut self, world: &'w mut World) -> QueryIter<'w, 's, D, F> { + self.query_mut(world).iter_inner() } /// Returns an [`Iterator`] over the query results for the given [`World`] without updating the query's archetypes. @@ -1386,12 +1383,9 @@ impl QueryState { pub unsafe fn iter_unchecked<'w, 's>( &'s mut self, world: UnsafeWorldCell<'w>, - ) -> QueryIter<'w, 's, D, F> - where - D: IterQueryData, - { + ) -> QueryIter<'w, 's, D, F> { // SAFETY: Upheld by caller - unsafe { self.query_unchecked(world) }.into_iter() + unsafe { self.query_unchecked(world) }.iter_inner() } /// Returns an [`Iterator`] over all possible combinations of `K` query results for the diff --git a/crates/bevy_ecs/src/system/query.rs b/crates/bevy_ecs/src/system/query.rs index 2f61da3b55f46..8df49ff6dcb1b 100644 --- a/crates/bevy_ecs/src/system/query.rs +++ b/crates/bevy_ecs/src/system/query.rs @@ -706,11 +706,37 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// [`iter`](Self::iter) for read-only query items. #[inline] - pub fn iter_mut(&mut self) -> QueryIter<'_, 's, D, F> - where - D: IterQueryData, - { - self.reborrow().into_iter() + pub fn iter_mut(&mut self) -> QueryIter<'_, 's, D, F> { + self.reborrow().iter_inner() + } + + /// Returns an [`Iterator`] over the query items, with the actual "inner" world lifetime. + /// + /// # Example + /// + /// Here, the `report_names_system` iterates over the `Player` component of every entity + /// that contains it: + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # + /// # #[derive(Component)] + /// # struct Player { name: String } + /// # + /// fn report_names_system(query: Query<&Player>) { + /// for player in &query { + /// println!("Say hello to {}!", player.name); + /// } + /// } + /// # bevy_ecs::system::assert_is_system(report_names_system); + /// ``` + #[inline] + pub fn iter_inner(self) -> QueryIter<'w, 's, D, F> { + // SAFETY: + // - `self.world` has permission to access the required components. + // - We consume the query, so mutable queries cannot alias. + // Read-only queries are `Copy`, but may alias themselves. + unsafe { QueryIter::new(self.world, self.state, self.last_run, self.this_run) } } /// Returns a [`QueryCombinationIter`] over all combinations of `K` read-only query items without repetition. @@ -2675,11 +2701,7 @@ impl<'w, 's, D: IterQueryData, F: QueryFilter> IntoIterator for Query<'w, 's, D, type IntoIter = QueryIter<'w, 's, D, F>; fn into_iter(self) -> Self::IntoIter { - // SAFETY: - // - `self.world` has permission to access the required components. - // - We consume the query, so mutable queries cannot alias. - // Read-only queries are `Copy`, but may alias themselves. - unsafe { QueryIter::new(self.world, self.state, self.last_run, self.this_run) } + self.iter_inner() } } @@ -2701,36 +2723,6 @@ impl<'w, 's, D: IterQueryData, F: QueryFilter> IntoIterator for &'w mut Query<'_ } } -impl<'w, 's, D: ReadOnlyQueryData, F: QueryFilter> Query<'w, 's, D, F> { - /// Returns an [`Iterator`] over the query items, with the actual "inner" world lifetime. - /// - /// This can only return immutable data (mutable data will be cast to an immutable form). - /// See [`Self::iter_mut`] for queries that contain at least one mutable component. - /// - /// # Example - /// - /// Here, the `report_names_system` iterates over the `Player` component of every entity - /// that contains it: - /// - /// ``` - /// # use bevy_ecs::prelude::*; - /// # - /// # #[derive(Component)] - /// # struct Player { name: String } - /// # - /// fn report_names_system(query: Query<&Player>) { - /// for player in &query { - /// println!("Say hello to {}!", player.name); - /// } - /// } - /// # bevy_ecs::system::assert_is_system(report_names_system); - /// ``` - #[inline] - pub fn iter_inner(&self) -> QueryIter<'w, 's, D::ReadOnly, F> { - (*self).into_iter() - } -} - /// Type returned from [`Query::transmute_lens`] containing the new [`QueryState`]. /// /// Call [`query`](QueryLens::query) or [`into`](Into::into) to construct the resulting [`Query`] diff --git a/release-content/migration-guides/nested_queries.md b/release-content/migration-guides/nested_queries.md index 42cb7e59d1fb0..db42d69f39db4 100644 --- a/release-content/migration-guides/nested_queries.md +++ b/release-content/migration-guides/nested_queries.md @@ -12,13 +12,17 @@ and need additional trait bounds to ensure they are only used soundly. An `IterQueryData` bound has been added to iteration methods on `Query`: -* `iter_mut`/ `iter_unsafe` / `into_iter` +* `into_iter` * `iter_many_unique_mut` / `iter_many_unique_unsafe` / `iter_many_unique_inner` * `get_many_mut` / `get_many_inner` / `get_many_unique_mut` / `get_many_unique_inner` * `par_iter_mut` / `par_iter_inner` / `par_iter_many_unique_mut` * `single_mut` / `single_inner` * `iter_combinations_mut` / `iter_combinations_inner` / `iter_combinations_unsafe` +`iter_mut`, `iter_unsafe`, and `iter_inner` may be called with non-iterable data, +but the resulting `QueryIter` will not `impl Iterator`. +It may be iterated using streaming or lending iteration by calling the new `fetch_next` method. + `iter`, `iter_many`, `par_iter`, `single`, and `iter_combinations` have no extra bounds, since read-only queries are always sound to iterate. `iter_many_mut` and `iter_many_inner` methods have no extra bounds, either, @@ -42,6 +46,11 @@ fn generic_func(query: Query) { fn generic_func(query: Query) { for item in &mut query { ... } } +// 0.18, but with support for non-iterable query types +fn generic_func(mut query: Query) { + let mut iter = query.iter_mut(); + while let Some(item) = iter.fetch_next() { ... } +} ``` Conversely, manual implementations of `QueryData` may want to implement `IterQueryData` and `SingleEntityQueryData` if appropriate. From bf5e4c2364ed3641524ce831cb1b1a72b8f79be3 Mon Sep 17 00:00:00 2001 From: Chris Russell <8494645+chescock@users.noreply.github.com> Date: Tue, 24 Feb 2026 16:42:11 -0500 Subject: [PATCH 5/6] Add `fetch_next` to `QuerySortedIter` to support streaming iteration where necessary. --- crates/bevy_ecs/src/query/iter.rs | 35 ++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/crates/bevy_ecs/src/query/iter.rs b/crates/bevy_ecs/src/query/iter.rs index 00881c53d8d00..7705eac6ae5cd 100644 --- a/crates/bevy_ecs/src/query/iter.rs +++ b/crates/bevy_ecs/src/query/iter.rs @@ -1203,13 +1203,42 @@ where } } + /// Get next result from the query + pub fn fetch_next(&mut self) -> Option> { + while let Some(entity) = self.entity_iter.next() { + // SAFETY: + // - `entity` is passed from `entity_iter` the first time. + // - `self` is mutably borrowed, so there are no other items alive for any entity. + if let Some(item) = unsafe { self.fetch_next_impl(entity) } { + return Some(D::shrink(item)); + } + } + None + } + + /// Get next result from the back of the query + pub fn fetch_next_back(&mut self) -> Option> + where + I: DoubleEndedIterator, + { + while let Some(entity) = self.entity_iter.next_back() { + // SAFETY: + // - `entity` is passed from `entity_iter` the first time. + // - `self` is mutably borrowed, so there are no other items alive for any entity. + if let Some(item) = unsafe { self.fetch_next_impl(entity) } { + return Some(D::shrink(item)); + } + } + None + } + /// # Safety /// /// - `entity` must stem from `self.entity_iter` /// - If `D` does not impl `ReadOnlyQueryData`, then there must not be any other `Item`s alive for the current entity /// - If `D` does not impl `IterQueryData`, then there must not be any other `Item`s alive for *any* entity #[inline(always)] - unsafe fn fetch_next(&mut self, entity: Entity) -> Option> { + unsafe fn fetch_next_impl(&mut self, entity: Entity) -> Option> { let (location, archetype, table); // SAFETY: // `tables` and `archetypes` belong to the same world that the [`QueryIter`] @@ -1262,7 +1291,7 @@ where // SAFETY: // - `entity` is passed from `entity_iter` the first time. // - `D: IterQueryData` - if let Some(item) = unsafe { self.fetch_next(entity) } { + if let Some(item) = unsafe { self.fetch_next_impl(entity) } { return Some(item); } } @@ -1288,7 +1317,7 @@ where // SAFETY: // - `entity` is passed from `entity_iter` the first time. // - `D: IterQueryData` - if let Some(item) = unsafe { self.fetch_next(entity) } { + if let Some(item) = unsafe { self.fetch_next_impl(entity) } { return Some(item); } } From d95c97f545dcee8176479106462628ccf4b03558 Mon Sep 17 00:00:00 2001 From: Chris Russell <8494645+chescock@users.noreply.github.com> Date: Wed, 11 Mar 2026 11:10:03 -0400 Subject: [PATCH 6/6] Better docs. --- crates/bevy_ecs/src/query/iter.rs | 115 +++++++++++++++++++++++++--- crates/bevy_ecs/src/system/query.rs | 24 ++++++ 2 files changed, 130 insertions(+), 9 deletions(-) diff --git a/crates/bevy_ecs/src/query/iter.rs b/crates/bevy_ecs/src/query/iter.rs index 7705eac6ae5cd..da9de877dc20e 100644 --- a/crates/bevy_ecs/src/query/iter.rs +++ b/crates/bevy_ecs/src/query/iter.rs @@ -129,10 +129,16 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { } } - /// Get next result from the query. + /// Get the next result from the query. /// - /// This can be used to iterate over queries that do not implement [`IterQueryData`]. - /// Most queries do implement that trait, and can use the ordinary [`Iterator::next`] + /// If the [`QueryData`] does not implement [`IterQueryData`], + /// then it is not sound to yield multiple items concurrently + /// and the resulting [`QueryIter`] will not implement [`Iterator`]. + /// In that case, this method can be used to iterate over the items + /// while ensuring only one is alive at a time. + /// + /// Most queries do implement [`IterQueryData`], + /// and and can use the ordinary [`Iterator::next`] /// method or a `for` loop. /// /// # Example @@ -460,6 +466,13 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { /// /// The sort is not cached across system runs. /// + /// If the [`QueryData`] does not implement [`IterQueryData`], + /// then it is not sound to yield multiple items concurrently + /// and the resulting [`QuerySortedIter`] will not implement [`Iterator`]. + /// To iterate over the items in that case, + /// use the [`QuerySortedIter::fetch_next()`](crate::query::QuerySortedIter::fetch_next) method, + /// which ensures only one item is alive at a time. + /// /// [allowed transmutes]: crate::system::Query#allowed-transmutes /// /// # Panics @@ -591,6 +604,13 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { /// /// The sort is not cached across system runs. /// + /// If the [`QueryData`] does not implement [`IterQueryData`], + /// then it is not sound to yield multiple items concurrently + /// and the resulting [`QuerySortedIter`] will not implement [`Iterator`]. + /// To iterate over the items in that case, + /// use the [`QuerySortedIter::fetch_next()`](crate::query::QuerySortedIter::fetch_next) method, + /// which ensures only one item is alive at a time. + /// /// [allowed transmutes]: crate::system::Query#allowed-transmutes /// /// # Panics @@ -652,6 +672,13 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { /// /// The sort is not cached across system runs. /// + /// If the [`QueryData`] does not implement [`IterQueryData`], + /// then it is not sound to yield multiple items concurrently + /// and the resulting [`QuerySortedIter`] will not implement [`Iterator`]. + /// To iterate over the items in that case, + /// use the [`QuerySortedIter::fetch_next()`](crate::query::QuerySortedIter::fetch_next) method, + /// which ensures only one item is alive at a time. + /// /// [allowed transmutes]: crate::system::Query#allowed-transmutes /// /// # Panics @@ -720,6 +747,13 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { /// /// The sort is not cached across system runs. /// + /// If the [`QueryData`] does not implement [`IterQueryData`], + /// then it is not sound to yield multiple items concurrently + /// and the resulting [`QuerySortedIter`] will not implement [`Iterator`]. + /// To iterate over the items in that case, + /// use the [`QuerySortedIter::fetch_next()`](crate::query::QuerySortedIter::fetch_next) method, + /// which ensures only one item is alive at a time. + /// /// [allowed transmutes]: crate::system::Query#allowed-transmutes /// /// # Panics @@ -756,6 +790,13 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { /// /// The sort is not cached across system runs. /// + /// If the [`QueryData`] does not implement [`IterQueryData`], + /// then it is not sound to yield multiple items concurrently + /// and the resulting [`QuerySortedIter`] will not implement [`Iterator`]. + /// To iterate over the items in that case, + /// use the [`QuerySortedIter::fetch_next()`](crate::query::QuerySortedIter::fetch_next) method, + /// which ensures only one item is alive at a time. + /// /// [allowed transmutes]: crate::system::Query#allowed-transmutes /// /// # Panics @@ -853,6 +894,13 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { /// /// The sort is not cached across system runs. /// + /// If the [`QueryData`] does not implement [`IterQueryData`], + /// then it is not sound to yield multiple items concurrently + /// and the resulting [`QuerySortedIter`] will not implement [`Iterator`]. + /// To iterate over the items in that case, + /// use the [`QuerySortedIter::fetch_next()`](crate::query::QuerySortedIter::fetch_next) method, + /// which ensures only one item is alive at a time. + /// /// [allowed transmutes]: crate::system::Query#allowed-transmutes /// /// # Panics @@ -892,6 +940,13 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { /// /// The sort is not cached across system runs. /// + /// If the [`QueryData`] does not implement [`IterQueryData`], + /// then it is not sound to yield multiple items concurrently + /// and the resulting [`QuerySortedIter`] will not implement [`Iterator`]. + /// To iterate over the items in that case, + /// use the [`QuerySortedIter::fetch_next()`](crate::query::QuerySortedIter::fetch_next) method, + /// which ensures only one item is alive at a time. + /// /// [allowed transmutes]: crate::system::Query#allowed-transmutes /// /// # Panics @@ -926,6 +981,13 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { /// /// The sort is not cached across system runs. /// + /// If the [`QueryData`] does not implement [`IterQueryData`], + /// then it is not sound to yield multiple items concurrently + /// and the resulting [`QuerySortedIter`] will not implement [`Iterator`]. + /// To iterate over the items in that case, + /// use the [`QuerySortedIter::fetch_next()`](crate::query::QuerySortedIter::fetch_next) method, + /// which ensures only one item is alive at a time. + /// /// [allowed transmutes]: crate::system::Query#allowed-transmutes /// /// # Panics @@ -1203,7 +1265,32 @@ where } } - /// Get next result from the query + /// Get the next result from the query. + /// + /// If the [`QueryData`] does not implement [`IterQueryData`], + /// then it is not sound to yield multiple items concurrently + /// and the resulting [`QuerySortedIter`] will not implement [`Iterator`]. + /// In that case, this method can be used to iterate over the items + /// while ensuring only one is alive at a time. + /// + /// Most queries do implement [`IterQueryData`], + /// and and can use the ordinary [`Iterator::next`] + /// method or a `for` loop. + /// + /// # Example + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # #[derive(Component, Ord, PartialOrd, Eq, PartialEq)] + /// # struct C; + /// fn system(mut query: Query<&mut C>) { + /// let mut iter = query.iter_mut().sort::<&C>(); + /// while let Some(mut c) = iter.fetch_next() { + /// // + /// } + /// } + /// # bevy_ecs::system::assert_is_system(system); + /// ``` pub fn fetch_next(&mut self) -> Option> { while let Some(entity) = self.entity_iter.next() { // SAFETY: @@ -1216,7 +1303,17 @@ where None } - /// Get next result from the back of the query + /// Get the next result from the back of the query. + /// + /// If the [`QueryData`] does not implement [`IterQueryData`], + /// then it is not sound to yield multiple items concurrently + /// and the resulting [`QuerySortedIter`] will not implement [`Iterator`]. + /// In that case, this method can be used to iterate over the items + /// while ensuring only one is alive at a time. + /// + /// Most queries do implement [`IterQueryData`], + /// and and can use the ordinary [`Iterator::next`] + /// method or a `for` loop. pub fn fetch_next_back(&mut self) -> Option> where I: DoubleEndedIterator, @@ -1470,7 +1567,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> None } - /// Get next result from the query + /// Get the next result from the query #[inline(always)] pub fn fetch_next(&mut self) -> Option> { // SAFETY: @@ -2011,7 +2108,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> impl<'w, 's, D: QueryData, F: QueryFilter, I: DoubleEndedIterator> QueryManyIter<'w, 's, D, F, I> { - /// Get next result from the back of the query + /// Get the next result from the back of the query #[inline(always)] pub fn fetch_next_back(&mut self) -> Option> { // SAFETY: @@ -2285,7 +2382,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> } } - /// Get next result from the query + /// Get the next result from the query #[inline(always)] pub fn fetch_next(&mut self) -> Option> { while let Some(entity) = self.entity_iter.next() { @@ -2307,7 +2404,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> impl<'w, 's, D: QueryData, F: QueryFilter, I: DoubleEndedIterator> QuerySortedManyIter<'w, 's, D, F, I> { - /// Get next result from the query + /// Get the next result from the query #[inline(always)] pub fn fetch_next_back(&mut self) -> Option> { while let Some(entity) = self.entity_iter.next_back() { diff --git a/crates/bevy_ecs/src/system/query.rs b/crates/bevy_ecs/src/system/query.rs index 8df49ff6dcb1b..eff324be204d3 100644 --- a/crates/bevy_ecs/src/system/query.rs +++ b/crates/bevy_ecs/src/system/query.rs @@ -684,6 +684,13 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// This iterator is always guaranteed to return results from each matching entity once and only once. /// Iteration order is not guaranteed. /// + /// If the [`QueryData`] does not implement [`IterQueryData`], + /// then it is not sound to yield multiple items concurrently + /// and the resulting [`QueryIter`] will not implement [`Iterator`]. + /// To iterate over the items in that case, + /// use the [`QueryIter::fetch_next()`](crate::query::QueryIter::fetch_next) method, + /// which ensures only one item is alive at a time. + /// /// # Example /// /// Here, the `gravity_system` updates the `Velocity` component of every entity that contains it: @@ -712,6 +719,16 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// Returns an [`Iterator`] over the query items, with the actual "inner" world lifetime. /// + /// This iterator is always guaranteed to return results from each matching entity once and only once. + /// Iteration order is not guaranteed. + /// + /// If the [`QueryData`] does not implement [`IterQueryData`], + /// then it is not sound to yield multiple items concurrently + /// and the resulting [`QueryIter`] will not implement [`Iterator`]. + /// To iterate over the items in that case, + /// use the [`QueryIter::fetch_next()`](crate::query::QueryIter::fetch_next) method, + /// which ensures only one item is alive at a time. + /// /// # Example /// /// Here, the `report_names_system` iterates over the `Player` component of every entity @@ -1136,6 +1153,13 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// This iterator is always guaranteed to return results from each matching entity once and only once. /// Iteration order is not guaranteed. /// + /// If the [`QueryData`] does not implement [`IterQueryData`], + /// then it is not sound to yield multiple items concurrently + /// and the resulting [`QueryIter`] will not implement [`Iterator`]. + /// To iterate over the items in that case, + /// use the [`QueryIter::fetch_next()`](crate::query::QueryIter::fetch_next) method, + /// which ensures only one item is alive at a time. + /// /// # Safety /// /// This function makes it possible to violate Rust's aliasing guarantees.