Skip to content

Introduce methods on QueryState to obtain a Query #15858

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Feb 5, 2025

Conversation

chescock
Copy link
Contributor

Objective

Simplify and expand the API for QueryState.

QueryState has a lot of methods that mirror those on Query. These are then multiplied by variants that take &World, &mut World, and UnsafeWorldCell. In addition, many of them have _manual variants that take &QueryState and avoid calling update_archetypes(). Not all of the combinations exist, however, so some operations are not possible.

Solution

Introduce methods to get a Query from a QueryState. That will reduce duplication between the types, and ensure that the full Query API is always available for QueryState.

Introduce methods on Query that consume the query to return types with the full 'w lifetime. This avoids issues with borrowing where things like query_state.query(&world).get(entity) don't work because they borrow from the temporary Query.

Finally, implement Copy for read-only Querys. get_inner and iter_inner currently take &self, so changing them to consume self would be a breaking change. By making Query: Copy, they can consume a copy of self and continue to work.

The consuming methods also let us simplify the implementation of methods on Query, by doing fn foo(&self) { self.as_readonly().foo_inner() } and fn foo_mut(&mut self) { self.reborrow().foo_inner() }. That structure makes it more difficult to accidentally extend lifetimes, since the safe as_readonly() and reborrow() methods shrink them appropriately. The optimizer is able to see that they are both identity functions and inline them, so there should be no performance cost.

Note that this change would conflict with #15848. If QueryState is stored as a Cow, then the consuming methods cannot be implemented, and Copy cannot be implemented.

Future Work

The next step is to mark the methods on QueryState as #[deprecated], and move the implementations into Query.

Migration Guide

Query::to_readonly has been renamed to Query::as_readonly.

…ry to consume Self and return the original lifetime.
@alice-i-cecile alice-i-cecile added A-ECS Entities, components, systems, and events C-Usability A targeted quality-of-life change that makes Bevy easier to use M-Needs-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Oct 11, 2024
@alice-i-cecile alice-i-cecile added this to the 0.16 milestone Oct 11, 2024
@alice-i-cecile alice-i-cecile added M-Needs-Release-Note Work that should be called out in the blog due to impact X-Contentious There are nontrivial implications that should be thought through C-Code-Quality A section of code that is hard to understand or change labels Oct 11, 2024
@Trashtalk217
Copy link
Contributor

I agree that there's a lot of duplication between QueryState's and Query's API and it's good that this is lessened, but just out of curiosity (just to offer a potentially dumb idea), would it perhaps be better if QueryState and Query were just one single struct instead of two?

@Trashtalk217 Trashtalk217 self-assigned this Oct 16, 2024
@chescock
Copy link
Contributor Author

I agree that there's a lot of duplication between QueryState's and Query's API and it's good that this is lessened, but just out of curiosity (just to offer a potentially dumb idea), would it perhaps be better if QueryState and Query were just one single struct instead of two?

I think the idea is that you need access to the world to actually do querying, but that we want to cache some state that lives longer than that borrow. So a Query is a &QueryState for the state plus UnsafeWorldCell<'w> and Ticks from the world.

I don't see a clean way to get rid of either. If you don't have Query then you'd need to pass a world to every method on QueryState, which is terrible ergonomics for even the simplest system. If you don't have QueryState then you need to look up ComponentIds and match archetypes every time you run a system, which is terrible performance.

@chescock
Copy link
Contributor Author

I realized I could split this into two independent PRs: One for the methods on QueryState to obtain a Query, and one for the consuming methods on Query. The consuming methods aren't all that useful by themselves, but the methods on QueryState should be, and those shouldn't be as contentious. Would anyone like me to split it up like that?

@BenjaminBrienen
Copy link
Contributor

BenjaminBrienen commented Oct 31, 2024

I say go big! Go ahead and remove the to-be-deprecated methods on QueryState! 😆

You can split it up if you think it would help, but I don't mind bigger PRs.

@@ -1542,24 +1542,6 @@ mod tests {
});
}

#[test]
#[should_panic = "Encountered a mismatched World."]
fn query_validates_world_id() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why remove this test? Is it no longer applicable?

#[inline]
pub(crate) unsafe fn new(
world: UnsafeWorldCell<'w>,
state: &'s QueryState<D, F>,
last_run: Tick,
this_run: Tick,
) -> Self {
state.validate_world(world.id());
Copy link
Contributor

Choose a reason for hiding this comment

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

Why remove this check?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh, right, that was subtle and I should have pointed it out!

The reason is that I changed all of the &self and &mut self methods on Query to call self.as_readonly().actual_method() and self.reborrow().actual_method(), and as_readonly() and reborrow() are implemented in terms of Query::new(). I wanted to make sure that change was zero-cost, but checking the assembly with cargo-show-asm showed that it was adding a call to validate_world to every existing method on Query! Since we always had a world that was already validated, I changed it to be a safety requirement, and now the assembly appears to be unchanged.

(Then I removed the test you asked about above because it's no longer true that it panics.)

@BenjaminBrienen BenjaminBrienen added D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes D-Unsafe Touches with unsafe code in some way labels Oct 31, 2024
@BenjaminBrienen
Copy link
Contributor

BenjaminBrienen commented Oct 31, 2024

I think it would be good to add more examples/tests that might help miri find unsafe mistakes, but it seems pretty clean.

Copy link
Contributor

@13ros27 13ros27 left a comment

Choose a reason for hiding this comment

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

This looks good to me, mostly just nits although I do have one question about that IntoIterator for Query?

@@ -381,6 +381,14 @@ pub struct Query<'world, 'state, D: QueryData, F: QueryFilter = ()> {
this_run: Tick,
}

impl<D: ReadOnlyQueryData, F: QueryFilter> Clone for Query<'_, '_, D, F> {
fn clone(&self) -> Self {
*self
Copy link
Contributor

Choose a reason for hiding this comment

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

sorry, noob question: how does this work?
The &QueryState inside the Query is simply copied? i.e. we create a new Query that points to the same QueryState?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yup! Everything in Query is Copy, which means it can be copied by memcpy. That includes references like &QueryState, which just makes a new reference to the same underlying QueryState. So we can make Query be Copy, and then implement Clone in terms of it.

@alice-i-cecile alice-i-cecile 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 5, 2025
/// Creates a [`Query`] from the given [`QueryState`] and [`World`].
///
/// This will create read-only queries, see [`Self::query_mut`] for mutable queries.
pub fn query<'w, 's>(&'s mut self, world: &'w World) -> Query<'w, 's, D::ReadOnly, F> {
Copy link
Member

Choose a reason for hiding this comment

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

Oh my gosh finally.

@alice-i-cecile alice-i-cecile added this pull request to the merge queue Feb 5, 2025
self.state
.iter_unchecked_manual(self.world, self.last_run, self.this_run)
}
self.reborrow().into_iter()
Copy link
Contributor

@cBournhonesque cBournhonesque Feb 5, 2025

Choose a reason for hiding this comment

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

Sorry noob question again could you please explain again why we need to re-borrow?

self.reborrow().into_iter() creates a new Query with reborrow() and then borrows from that with into_iter()
self.into_iter() would simply create a QueryIter that borrows from the initital &'a mut Query so the lifetime is constrained by `a; is that the issue?

The lifetimes are elided on reborrow so I don't get exactly what is happening

Copy link
Contributor Author

Choose a reason for hiding this comment

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

reborrow() goes from &'a mut Query<'w> to Query<'a>, turning a borrowed Query into an owned one with a shorter lifetime.

into_iter() takes self instead of &mut self, so it actually consumes the Query.

We have an &mut Query, so we first reborrow() to get an owned Query with a shorter lifetime, and then use into_iter() to consume the owned query and create a QueryIter - still with the shorter lifetime.

It's more complex when looking at one method, but across the whole type it means we can re-use the safety proofs in reborrow() and as_readonly(). That lets us implement this method without unsafe code, and means the compiler would catch it if we tried to return QueryIter<'w, ...> instead of QueryIter<'_, ...>.

Merged via the queue into bevyengine:main with commit 6f39e44 Feb 5, 2025
29 checks passed
Copy link
Contributor

@Victoronz Victoronz left a comment

Choose a reason for hiding this comment

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

Sorry to block this (the merge queue got there first), but I don't think Query being Copy is sound?
IIUC you can go from a mutable Query to a read-only QueryLens, make a Query out of that lens again, then copy that Query, transmute_lens it again, and get a Query from it for a last time. Then you'd have a read-only query that is no longer bound to the &mut self of the original mutable Query.
Did I overlook something?

@alice-i-cecile
Copy link
Member

Discuss in #17693.

@Victoronz
Copy link
Contributor

Victoronz commented Feb 7, 2025

Something else I'd like to note is that this PR implements IntoIterator for Query, however that impl is not restricted to ReadOnlyQueryData. This means that a simple for loop over a mutable Query will consume it, even if that loop doesn't actually mutate anything!
I'd think this to be quite confusing for users. Especially since there is no "owned" way of iterating Query, we're still just borrowing the data in the end.
Implementing it only for ReadOnlyQueryData would be confusing as well, since that'd mean that writing mutable for loops now diverges from writing immutable ones!
Is that impl necessary?

@chescock
Copy link
Contributor Author

chescock commented Feb 7, 2025

Something else I'd like to note is that this PR implements IntoIterator for Query, however that impl is not restricted to ReadOnlyQueryData. This means that a simple for loop over a mutable Query will consume it, even if that loop doesn't actually mutate anything!

Yup! That's the generalization of iter_inner() to mutable data, and lets you write query_state.query_mut(world).into_iter() without needing to promote the temporary query to a variable, which may be impossible if you're returning data. That's necessary to completely replace query_state.iter_mut(world).

I'm hoping it will be a nicer experience for users: You can write the obvious for data in query, and in the common case where you only iterate once it will finally just work! If you need to iterate multiple times, you'll need to for data in &mut query, but that's true for other common collection types like Vec, so it should be familiar.

And because read-only queries also impl Copy, you can do multiple for data in query loops over them, and everything will work!

@chescock
Copy link
Contributor Author

chescock commented Feb 7, 2025

And while I'd been focusing on QueryState when writing this, the IntoIter impl should also make it nicer to work with QueryLens, since you won't always have to put the Query in a variable.

For example:

let mut lens: QueryLens<_> = ...;

// Fails with "error[E0716]: temporary value dropped while borrowed"
let first = lens.query().iter_mut().next();
assert!(first.is_some());

// Works!
let first = lens.query().into_iter().next();
assert!(first.is_some());

github-merge-queue bot pushed a commit that referenced this pull request Feb 10, 2025
# Objective

Restore the behavior of `Query::get_many` prior to #15858.  

When passed duplicate `Entity`s, `get_many` is supposed to return
results for all of them, since read-only queries don't alias. However,
#15858 merged the implementation with `get_many_mut` and caused it to
return `QueryEntityError::AliasedMutability`.

## Solution

Introduce a new `Query::get_many_readonly` method that consumes the
`Query` like `get_many_inner`, but that is constrained to `D:
ReadOnlyQueryData` so that it can skip the aliasing check. Implement
`Query::get_many` in terms of that new method. Add a test, and a comment
explaining why it doesn't match the pattern of the other `&self`
methods.
github-merge-queue bot pushed a commit that referenced this pull request Feb 12, 2025
# Objective

Fix unsoundness introduced by #15858. `QueryLens::query()` would hand
out a `Query` with the full `'w` lifetime, and the new `_inner` methods
would let the results outlive the `Query`. This could be used to create
aliasing mutable references, like

```rust
fn bad<'w>(mut lens: QueryLens<'w, EntityMut>, entity: Entity) {
    let one: EntityMut<'w> = lens.query().get_inner(entity).unwrap();
    let two: EntityMut<'w> = lens.query().get_inner(entity).unwrap();
    assert!(one.entity() == two.entity());
}
```

Fixes #17693 

## Solution

Restrict the `'world` lifetime in the `Query` returned by
`QueryLens::query()` to `'_`, the lifetime of the borrow of the
`QueryLens`.

The model here is that `Query<'w, 's, D, F>` and `QueryLens<'w, D, F>`
have permission to access their components for the lifetime `'w`. So
going from `&'a mut QueryLens<'w>` to `Query<'w, 'a>` would borrow the
permission only for the `'a` lifetime, but incorrectly give it out for
the full `'w` lifetime.

To handle any cases where users were calling `get_inner()` or
`iter_inner()` on the `Query` and expecting the full `'w` lifetime, we
introduce a new `QueryLens::query_inner()` method. This is only valid
for `ReadOnlyQueryData`, so it may safely hand out a copy of the
permission for the full `'w` lifetime. Since `get_inner()` and
`iter_inner()` were only valid on `ReadOnlyQueryData` prior to #15858,
that should cover any uses that relied on the longer lifetime.

## Migration Guide

Users of `QueryLens::query()` who were calling `get_inner()` or
`iter_inner()` will need to replace the call with
`QueryLens::query_inner()`.
@Victoronz
Copy link
Contributor

Victoronz commented Feb 12, 2025

The reasoning suggests that this is mainly to work around Query always borrowing QueryState.
The reason why QueryState has the fetching methods at all I think was because we couldn't directly create Query from World, also because it borrows QueryState.
The reason we have QueryLens is once again the same.

I have some thoughts on Query as a whole, but I hadn't quite put them together yet, so I'll just pick this place to state them.
I'd like to make an issue out of this where we can discuss, but it also serves as a response here:

It feels like we are stacking features/implementation logic on this detail, when really the proper solution should be to make a Query that owns its QueryState (what we basically want to allow is at least Box<QueryState> but preferably Deref<QueryState>).
The reason this was not done previously was to avoid an additional generic or branching in Query.
That doesn't mean it can't be done!
Instead of placing the generic in Query itself, we could abstract over multiple query types that can produce the necessary parts for fetching, and then have the fetch methods (the various gets/iters) be generic. And/Or we could even avoid duplication by deriving the fetch methods from a base trait impl, like Iterator does with next/size_hint.
Looking forward, when querying becomes more complex, it is very likely that we won't get away with a single Query type in the first place. #13607 or the uncached query efforts are examples.
Within this, QueryState itself should no longer be the place where call fetch methods, like you state in "Future Work".

Ideally, consuming methods will not be needed, but more likely will still have their place.
However, I don't think we should be making them the default:
Almost everywhere, an owned into_iter call will yield owned items, but here it instead means "longer-lived" borrowed items, which is quite inconsistent with this notion.
IIUC, then #15396 will mean that the consuming methods will not work for what is not ReleaseStateQueryData, meaning that the default way of iterating would not work for some specific types, because they don't only rely on the 'w lifetime.

All in all, I'm struggling to keep a mental model of all this Query/QueryState logic myself, and would like to expose the least amount of that as necessary to the users. Let me know what you think!

github-merge-queue bot pushed a commit that referenced this pull request Feb 16, 2025
…#17822)

# Objective

Simplify the API surface by removing duplicated functionality between
`Query` and `QueryState`.

Reduce the amount of `unsafe` code required in `QueryState`.  

This is a follow-up to #15858.

## Solution

Move implementations of `Query` methods from `QueryState` to `Query`.
Instead of the original methods being on `QueryState`, with `Query`
methods calling them by passing the individual parameters, the original
methods are now on `Query`, with `QueryState` methods calling them by
constructing a `Query`.

This also adds two `_inner` methods that were missed in #15858:
`iter_many_unique_inner` and `single_inner`.

One goal here is to be able to deprecate and eventually remove many of
the methods on `QueryState`, reducing the overall API surface. (I
expected to do that in this PR, but this change was large enough on its
own!) Now that the `QueryState` methods each consist of a simple
expression like `self.query(world).get_inner(entity)`, a future PR can
deprecate some or all of them with simple migration instructions.

The other goal is to reduce the amount of `unsafe` code. The current
implementation of a read-only method like `QueryState::get` directly
calls the `unsafe fn get_unchecked_manual` and needs to repeat the proof
that `&World` has enough access. With this change, `QueryState::get` is
entirely safe code, with the proof that `&World` has enough access done
by the `query()` method and shared across all read-only operations.

## Future Work

The next step will be to mark the `QueryState` methods as
`#[deprecated]` and migrate callers to the methods on `Query`.
mrchantey pushed a commit to mrchantey/bevy that referenced this pull request Feb 17, 2025
# Objective

Simplify and expand the API for `QueryState`.  

`QueryState` has a lot of methods that mirror those on `Query`. These
are then multiplied by variants that take `&World`, `&mut World`, and
`UnsafeWorldCell`. In addition, many of them have `_manual` variants
that take `&QueryState` and avoid calling `update_archetypes()`. Not all
of the combinations exist, however, so some operations are not possible.

## Solution

Introduce methods to get a `Query` from a `QueryState`. That will reduce
duplication between the types, and ensure that the full `Query` API is
always available for `QueryState`.

Introduce methods on `Query` that consume the query to return types with
the full `'w` lifetime. This avoids issues with borrowing where things
like `query_state.query(&world).get(entity)` don't work because they
borrow from the temporary `Query`.

Finally, implement `Copy` for read-only `Query`s. `get_inner` and
`iter_inner` currently take `&self`, so changing them to consume `self`
would be a breaking change. By making `Query: Copy`, they can consume a
copy of `self` and continue to work.

The consuming methods also let us simplify the implementation of methods
on `Query`, by doing `fn foo(&self) { self.as_readonly().foo_inner() }`
and `fn foo_mut(&mut self) { self.reborrow().foo_inner() }`. That
structure makes it more difficult to accidentally extend lifetimes,
since the safe `as_readonly()` and `reborrow()` methods shrink them
appropriately. The optimizer is able to see that they are both identity
functions and inline them, so there should be no performance cost.

Note that this change would conflict with bevyengine#15848. If `QueryState` is
stored as a `Cow`, then the consuming methods cannot be implemented, and
`Copy` cannot be implemented.

## Future Work

The next step is to mark the methods on `QueryState` as `#[deprecated]`,
and move the implementations into `Query`.

## Migration Guide

`Query::to_readonly` has been renamed to `Query::as_readonly`.
@alice-i-cecile
Copy link
Member

Thank you to everyone involved with the authoring or reviewing of this PR! This work is relatively important and needs release notes! Head over to bevyengine/bevy-website#1990 if you'd like to help out.

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-Code-Quality A section of code that is hard to understand or change C-Usability A targeted quality-of-life change that makes Bevy easier to use D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes D-Unsafe Touches with unsafe code in some way M-Needs-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide M-Needs-Release-Note Work that should be called out in the blog due to impact S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it X-Contentious There are nontrivial implications that should be thought through
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants