Skip to main content

moirai/query/
cursor.rs

1//! Change-detection cursor bound to one query spec and world owner.
2//!
3//! Records the last [`ChangeTick`] observed by a successful
4//! added/changed traversal. Typed iterators commit the cursor only after full exhaustion.
5
6use crate::query::QueryError;
7use crate::time::ChangeTick;
8use crate::world::WorldOwner;
9
10/// Last-observed change tick for one owner-bound added/changed query window.
11pub struct QueryCursor {
12    owner: WorldOwner,
13    fingerprint: u64,
14    last_observed: ChangeTick,
15}
16
17impl QueryCursor {
18    /// Opens a window from the start of change history for a single-component query.
19    pub fn from_spec_start<T: 'static>(
20        world: &mut crate::world::World,
21        spec: &crate::query::QuerySpec,
22    ) -> Result<Self, QueryError> {
23        let fingerprint = world.query_fingerprint::<T>(spec)?;
24        Ok(Self::from_start(world, fingerprint))
25    }
26
27    /// Opens a window ending at the world's current change tick for a single-component query.
28    pub fn from_spec_now<T: 'static>(
29        world: &mut crate::world::World,
30        spec: &crate::query::QuerySpec,
31    ) -> Result<Self, QueryError> {
32        let fingerprint = world.query_fingerprint::<T>(spec)?;
33        Self::from_now(world, fingerprint)
34    }
35
36    /// Opens a window from the start of change history for a two-component query.
37    pub fn from_spec2_start<A: 'static, B: 'static>(
38        world: &mut crate::world::World,
39        spec: &crate::query::QuerySpec,
40    ) -> Result<Self, QueryError> {
41        let (plan, _, _) = world.resolve_query2_plan::<A, B>(spec)?;
42        Ok(Self::from_start(world, plan.fingerprint))
43    }
44
45    /// Opens a window ending at the world's current change tick for a two-component query.
46    pub fn from_spec2_now<A: 'static, B: 'static>(
47        world: &mut crate::world::World,
48        spec: &crate::query::QuerySpec,
49    ) -> Result<Self, QueryError> {
50        let (plan, _, _) = world.resolve_query2_plan::<A, B>(spec)?;
51        Self::from_now(world, plan.fingerprint)
52    }
53
54    #[cfg(test)]
55    pub(crate) fn for_entities_from_start(
56        world: &mut crate::world::World,
57        spec: &crate::query::QuerySpec,
58    ) -> Result<Self, QueryError> {
59        let fingerprint = world.entity_query_fingerprint(spec)?;
60        Ok(Self::from_start(world, fingerprint))
61    }
62
63    /// Copies the cursor without advancing its observed tick.
64    pub fn fork(&self) -> Self {
65        Self {
66            owner: self.owner.clone(),
67            fingerprint: self.fingerprint,
68            last_observed: self.last_observed,
69        }
70    }
71
72    pub(crate) fn from_start(world: &crate::world::World, fingerprint: u64) -> Self {
73        Self {
74            owner: world.owner_token(),
75            fingerprint,
76            last_observed: ChangeTick::ZERO,
77        }
78    }
79
80    pub(crate) fn from_now(
81        world: &crate::world::World,
82        fingerprint: u64,
83    ) -> Result<Self, QueryError> {
84        Ok(Self {
85            owner: world.owner_token(),
86            fingerprint,
87            last_observed: world.change_tick(),
88        })
89    }
90
91    pub(crate) fn validate(
92        &self,
93        world: &crate::world::World,
94        fingerprint: u64,
95    ) -> Result<(), QueryError> {
96        if !self.owner.same(&world.owner_token()) {
97            return Err(QueryError::WrongOwner);
98        }
99        if self.fingerprint != fingerprint {
100            return Err(QueryError::WrongQuery {
101                detail: alloc::string::String::from("cursor fingerprint does not match query spec"),
102            });
103        }
104        Ok(())
105    }
106
107    /// Lower bound of the active half-open change window `(since, captured_now]`.
108    pub fn since(&self) -> ChangeTick {
109        self.last_observed
110    }
111
112    pub(crate) fn commit(&mut self, captured_now: ChangeTick) {
113        self.last_observed = captured_now;
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120    use crate::component::ComponentOptions;
121    use crate::query::{QueryParams, QuerySpec};
122    use crate::world::WorldBuilder;
123
124    #[derive(Clone, Copy)]
125    struct Position(#[allow(dead_code)] i32);
126
127    #[derive(Clone, Copy)]
128    struct Velocity(#[allow(dead_code)] i32);
129
130    #[test]
131    fn query_cursor_commits_on_exhaustion() {
132        let mut builder = WorldBuilder::new();
133        builder
134            .register_component::<Position>(ComponentOptions::sparse())
135            .expect("register");
136        let mut world = builder.build().expect("build");
137        let entity = world.spawn().expect("spawn");
138        world.insert(entity, Position(1)).expect("insert");
139
140        let spec = QuerySpec::new().added::<Position>();
141        let mut cursor =
142            QueryCursor::from_spec_start::<Position>(&mut world, &spec).expect("cursor");
143        let params = QueryParams::new().cursor(&mut cursor);
144        let mut query = world.query::<Position>(&spec, params).expect("query");
145        assert!(query.next().is_some());
146        assert!(query.next().is_none());
147        drop(query);
148        assert!(cursor.since().raw() > 0);
149    }
150
151    #[test]
152    fn query_cursor_skips_commit_on_partial_iteration() {
153        let mut builder = WorldBuilder::new();
154        builder
155            .register_component::<Position>(ComponentOptions::sparse())
156            .expect("register");
157        let mut world = builder.build().expect("build");
158        let a = world.spawn().expect("spawn");
159        let b = world.spawn().expect("spawn");
160        world.insert(a, Position(1)).expect("insert");
161        world.insert(b, Position(2)).expect("insert");
162
163        let spec = QuerySpec::new().added::<Position>();
164        let mut cursor =
165            QueryCursor::from_spec_start::<Position>(&mut world, &spec).expect("cursor");
166        let before = cursor.since();
167        let params = QueryParams::new().cursor(&mut cursor);
168        let mut query = world.query::<Position>(&spec, params).expect("query");
169        let _ = query.next();
170        drop(query);
171        assert_eq!(cursor.since(), before);
172    }
173
174    #[test]
175    fn from_spec_now_and_from_now_capture_change_tick() {
176        let mut builder = WorldBuilder::new();
177        builder
178            .register_component::<Position>(ComponentOptions::sparse())
179            .expect("register");
180        let mut world = builder.build().expect("build");
181        let entity = world.spawn().expect("spawn");
182        world.insert(entity, Position(1)).expect("insert");
183        let spec = QuerySpec::new();
184        let fingerprint = world.query_fingerprint::<Position>(&spec).expect("fp");
185        let cursor = QueryCursor::from_spec_now::<Position>(&mut world, &spec).expect("now");
186        assert_eq!(cursor.since(), world.change_tick());
187        let direct = QueryCursor::from_now(&world, fingerprint).expect("direct");
188        assert_eq!(direct.since(), world.change_tick());
189    }
190
191    #[test]
192    fn query2_now_and_fork_preserve_fingerprint_and_tick() {
193        let mut builder = WorldBuilder::new();
194        builder
195            .register_component::<Position>(ComponentOptions::sparse())
196            .expect("position");
197        builder
198            .register_component::<Velocity>(ComponentOptions::sparse())
199            .expect("velocity");
200        let mut world = builder.build().expect("world");
201        let entity = world.spawn().expect("spawn");
202        world.insert(entity, Position(1)).expect("position");
203        world.insert(entity, Velocity(2)).expect("velocity");
204        let spec = QuerySpec::new().changed::<Position>();
205
206        let cursor = QueryCursor::from_spec2_now::<Position, Velocity>(&mut world, &spec)
207            .expect("query2 now");
208        let fork = cursor.fork();
209        assert_eq!(cursor.since(), world.change_tick());
210        assert_eq!(fork.since(), cursor.since());
211
212        let (plan, _, _) = world
213            .resolve_query2_plan::<Position, Velocity>(&spec)
214            .expect("plan");
215        assert!(fork.validate(&world, plan.fingerprint).is_ok());
216    }
217
218    #[test]
219    fn validate_rejects_foreign_owner() {
220        let mut builder_a = WorldBuilder::new();
221        builder_a
222            .register_component::<Position>(ComponentOptions::sparse())
223            .expect("register");
224        let mut world_a = builder_a.build().expect("a");
225        let spec = QuerySpec::new();
226        let mut cursor =
227            QueryCursor::from_spec_start::<Position>(&mut world_a, &spec).expect("cursor");
228        let mut builder_b = WorldBuilder::new();
229        builder_b
230            .register_component::<Position>(ComponentOptions::sparse())
231            .expect("register");
232        let mut world_b = builder_b.build().expect("b");
233        let fingerprint = world_b.query_fingerprint::<Position>(&spec).expect("fp");
234        assert!(matches!(
235            cursor.validate(&world_b, fingerprint),
236            Err(QueryError::WrongOwner)
237        ));
238        let _ = &mut cursor;
239    }
240}