Skip to main content

moirai/query/
iter.rs

1//! Typed query iterators backing [`Query1`] and [`Query2`].
2//!
3//! Iterators walk sparse dense slots, archetype tables, exact id lists, or materialized caches.
4//! Change-detection cursors commit on exhaustion, not on partial prefix iteration.
5
6use alloc::rc::Rc;
7use alloc::vec::Vec;
8
9use crate::entity::EntityId;
10use crate::world::query::cached_source::QueryCachedSource;
11
12/// Lazy read iterator over one component type and its resolved query plan.
13pub struct Query1<'w, 'c, T: 'static> {
14    pub(crate) world: &'w crate::world::World,
15    pub(crate) plan: Rc<crate::world::query::plan::ResolvedPlan>,
16    pub(crate) params_fingerprint: u64,
17    pub(crate) captured_now: crate::time::ChangeTick,
18    pub(crate) since: crate::time::ChangeTick,
19    pub(crate) cursor_committed: bool,
20    pub(crate) cursor: Option<&'c mut crate::query::QueryCursor>,
21    pub(crate) additional_covered_required: Option<usize>,
22    pub(crate) state: Query1State<'w, T>,
23}
24
25/// Active traversal source for a [`Query1`] iterator.
26pub(crate) enum Query1State<'w, T: 'static> {
27    /// Dense iteration over one sparse component population.
28    Sparse {
29        store: &'w crate::storage::TypedSparseStorage<T>,
30        index: usize,
31    },
32    /// Row scan across archetypes that contain the driver table component.
33    Table {
34        archetypes: &'w [usize],
35        archetype_index: usize,
36        row: usize,
37    },
38    /// Caller-ordered exact entity id list.
39    Exact { ids: Vec<EntityId>, index: usize },
40    /// Membership or result cache lookup with optional temporal re-filtering.
41    Cached {
42        source: QueryCachedSource,
43        index: usize,
44    },
45    /// Prepared-query materialized ids, optionally re-filtered for added/changed windows.
46    Borrowed {
47        ids: &'w [EntityId],
48        index: usize,
49        apply_temporal: bool,
50    },
51    /// Iterator exhausted; cursor may commit on drop.
52    Done,
53}
54
55/// Lazy read iterator over two component types and their resolved query plan.
56pub struct Query2<'w, 'c, A: 'static, B: 'static> {
57    pub(crate) world: &'w crate::world::World,
58    pub(crate) plan: Rc<crate::world::query::plan::ResolvedPlan>,
59    pub(crate) params_fingerprint: u64,
60    pub(crate) captured_now: crate::time::ChangeTick,
61    pub(crate) since: crate::time::ChangeTick,
62    pub(crate) cursor_committed: bool,
63    pub(crate) cursor: Option<&'c mut crate::query::QueryCursor>,
64    pub(crate) state: Query2State<'w>,
65    pub(crate) second_index: usize,
66    pub(crate) second_is_table: bool,
67    pub(crate) marker: core::marker::PhantomData<fn() -> (A, B)>,
68}
69
70/// Active traversal source for a [`Query2`] iterator.
71pub(crate) enum Query2State<'w> {
72    /// Dense iteration over the smaller sparse driver population.
73    Sparse { slots: &'w [u32], index: usize },
74    /// Row scan across archetypes that contain the driver table component.
75    Table {
76        archetypes: &'w [usize],
77        archetype_index: usize,
78        row: usize,
79    },
80    /// Caller-ordered exact entity id list.
81    Exact { ids: Vec<EntityId>, index: usize },
82    /// Membership or result cache lookup.
83    Cached {
84        source: QueryCachedSource,
85        index: usize,
86    },
87    /// Prepared-query materialized ids, optionally re-filtered for added/changed windows.
88    Borrowed {
89        ids: &'w [EntityId],
90        index: usize,
91        apply_temporal: bool,
92    },
93    /// Iterator exhausted; cursor may commit on drop.
94    Done,
95}
96
97impl<'w, 'c, T: 'static> Query1<'w, 'c, T> {
98    #[allow(clippy::too_many_arguments, dead_code)]
99    pub(crate) fn new(
100        world: &'w crate::world::World,
101        plan: Rc<crate::world::query::plan::ResolvedPlan>,
102        since: crate::time::ChangeTick,
103        captured_now: crate::time::ChangeTick,
104        cursor: Option<&'c mut crate::query::QueryCursor>,
105        cached: Option<QueryCachedSource>,
106        table_archetypes: Option<&'w [usize]>,
107        additional_covered_required: Option<usize>,
108    ) -> Result<Self, crate::query::QueryError> {
109        let state = world.query1_state::<T>(&plan, cached, table_archetypes)?;
110        Ok(Self {
111            world,
112            params_fingerprint: plan.fingerprint,
113            plan,
114            captured_now,
115            since,
116            cursor_committed: false,
117            cursor,
118            additional_covered_required,
119            state,
120        })
121    }
122
123    #[allow(clippy::too_many_arguments)]
124    pub(crate) fn new_prepared(
125        world: &'w crate::world::World,
126        plan: Rc<crate::world::query::plan::ResolvedPlan>,
127        since: crate::time::ChangeTick,
128        captured_now: crate::time::ChangeTick,
129        cursor: Option<&'c mut crate::query::QueryCursor>,
130        materialized: Option<(&'w [EntityId], bool)>,
131        table_archetypes: Option<&'w [usize]>,
132    ) -> Result<Self, crate::query::QueryError> {
133        let state = if let Some((ids, apply_temporal)) = materialized {
134            Query1State::Borrowed {
135                ids,
136                index: 0,
137                apply_temporal,
138            }
139        } else {
140            world.query1_state::<T>(&plan, None, table_archetypes)?
141        };
142        Ok(Self {
143            world,
144            params_fingerprint: plan.fingerprint,
145            plan,
146            captured_now,
147            since,
148            cursor_committed: false,
149            cursor,
150            additional_covered_required: None,
151            state,
152        })
153    }
154
155    fn commit_cursor_if_needed(&mut self) {
156        if self.cursor_committed {
157            return;
158        }
159        let world = self.world;
160        let fingerprint = self.params_fingerprint;
161        let cursor = self
162            .cursor
163            .as_mut()
164            .filter(|cursor| cursor.validate(world, fingerprint).is_ok());
165        if let Some(cursor) = cursor {
166            cursor.commit(self.captured_now);
167        }
168        self.cursor_committed = true;
169    }
170}
171
172impl<'w, 'c, T: 'static> Iterator for Query1<'w, 'c, T> {
173    type Item = (EntityId, &'w T);
174
175    fn next(&mut self) -> Option<Self::Item> {
176        loop {
177            match &mut self.state {
178                Query1State::Done => {
179                    self.commit_cursor_if_needed();
180                    return None;
181                }
182                Query1State::Sparse { store, index } => {
183                    let slots = store.dense_slots();
184                    while *index < slots.len() {
185                        let dense_index = *index;
186                        let slot = slots[dense_index];
187                        *index += 1;
188                        let entity = self.world.entity_from_slot(slot);
189                        if let Some(additional) = self.additional_covered_required {
190                            if !self.world.query1_accept_source_covered(
191                                entity,
192                                &self.plan,
193                                self.since,
194                                self.captured_now,
195                                additional,
196                            ) {
197                                continue;
198                            }
199                            let value = store
200                                .dense_value(dense_index)
201                                .expect("sparse dense slot and value vectors stay aligned");
202                            return Some((entity, value));
203                        }
204                        if let Some(value) = self.world.query1_match_sparse::<T>(
205                            entity,
206                            &self.plan,
207                            self.since,
208                            self.captured_now,
209                            store,
210                        ) {
211                            return Some((entity, value));
212                        }
213                    }
214                    self.state = Query1State::Done;
215                }
216                Query1State::Table {
217                    archetypes,
218                    archetype_index,
219                    row,
220                } => {
221                    while *archetype_index < archetypes.len() {
222                        let archetype = archetypes[*archetype_index];
223                        let slots = self.world.archetype_entity_slots(archetype);
224                        while *row < slots.len() {
225                            let slot = slots[*row];
226                            *row += 1;
227                            let entity = self.world.entity_from_slot(slot);
228                            if let Some(value) = self.world.query1_match_table::<T>(
229                                entity,
230                                &self.plan,
231                                self.since,
232                                self.captured_now,
233                                self.additional_covered_required,
234                            ) {
235                                return Some((entity, value));
236                            }
237                        }
238                        *archetype_index += 1;
239                        *row = 0;
240                    }
241                    self.state = Query1State::Done;
242                }
243                Query1State::Exact { ids, index } => {
244                    while *index < ids.len() {
245                        let entity = ids[*index];
246                        *index += 1;
247                        if let Some(value) = self.world.query1_match_any_storage::<T>(
248                            entity,
249                            &self.plan,
250                            self.since,
251                            self.captured_now,
252                        ) {
253                            return Some((entity, value));
254                        }
255                    }
256                    self.state = Query1State::Done;
257                }
258                Query1State::Cached { source, index } => {
259                    let ids = match self
260                        .world
261                        .cached_query_entities(source, self.params_fingerprint)
262                    {
263                        Ok(ids) => ids,
264                        Err(_) => {
265                            self.state = Query1State::Done;
266                            continue;
267                        }
268                    };
269                    while *index < ids.len() {
270                        let entity = ids[*index];
271                        *index += 1;
272                        let value = if !self.plan.added_indices.is_empty()
273                            || !self.plan.changed_indices.is_empty()
274                        {
275                            self.world.query1_match_any_storage::<T>(
276                                entity,
277                                &self.plan,
278                                self.since,
279                                self.captured_now,
280                            )
281                        } else {
282                            self.world.query1_match_cached::<T>(entity, &self.plan)
283                        };
284                        let value = match value {
285                            Some(value) => value,
286                            None => continue,
287                        };
288                        return Some((entity, value));
289                    }
290                    self.state = Query1State::Done;
291                }
292                Query1State::Borrowed {
293                    ids,
294                    index,
295                    apply_temporal,
296                } => {
297                    while *index < ids.len() {
298                        let entity = ids[*index];
299                        *index += 1;
300                        if *apply_temporal
301                            && !crate::world::query::filter::entity_matches_temporal(
302                                self.world,
303                                entity,
304                                &self.plan,
305                                self.since,
306                                self.captured_now,
307                            )
308                        {
309                            continue;
310                        }
311                        let value = self.world.query1_match_cached::<T>(entity, &self.plan);
312                        let value = match value {
313                            Some(value) => value,
314                            None => continue,
315                        };
316                        return Some((entity, value));
317                    }
318                    self.state = Query1State::Done;
319                }
320            }
321        }
322    }
323}
324
325impl<'w, 'c, T: 'static> Drop for Query1<'w, 'c, T> {
326    fn drop(&mut self) {
327        if matches!(self.state, Query1State::Done) {
328            self.commit_cursor_if_needed();
329        }
330    }
331}
332
333impl<'w, 'c, A: 'static, B: 'static> Query2<'w, 'c, A, B> {
334    #[allow(clippy::too_many_arguments, dead_code)]
335    pub(crate) fn new(
336        world: &'w crate::world::World,
337        plan: Rc<crate::world::query::plan::ResolvedPlan>,
338        since: crate::time::ChangeTick,
339        captured_now: crate::time::ChangeTick,
340        cursor: Option<&'c mut crate::query::QueryCursor>,
341        cached: Option<QueryCachedSource>,
342        table_archetypes: Option<&'w [usize]>,
343        second_index: usize,
344        second_is_table: bool,
345    ) -> Result<Self, crate::query::QueryError> {
346        let state = Self::state(world, &plan, cached, None, table_archetypes)?;
347        Ok(Self {
348            world,
349            params_fingerprint: plan.fingerprint,
350            plan,
351            captured_now,
352            since,
353            cursor_committed: false,
354            cursor,
355            state,
356            second_index,
357            second_is_table,
358            marker: core::marker::PhantomData,
359        })
360    }
361
362    #[allow(clippy::too_many_arguments)]
363    pub(crate) fn new_prepared(
364        world: &'w crate::world::World,
365        plan: Rc<crate::world::query::plan::ResolvedPlan>,
366        since: crate::time::ChangeTick,
367        captured_now: crate::time::ChangeTick,
368        cursor: Option<&'c mut crate::query::QueryCursor>,
369        materialized: Option<(&'w [EntityId], bool)>,
370        table_archetypes: Option<&'w [usize]>,
371        second_index: usize,
372        second_is_table: bool,
373    ) -> Result<Self, crate::query::QueryError> {
374        let state = Self::state(world, &plan, None, materialized, table_archetypes)?;
375        Ok(Self {
376            world,
377            params_fingerprint: plan.fingerprint,
378            plan,
379            captured_now,
380            since,
381            cursor_committed: false,
382            cursor,
383            state,
384            second_index,
385            second_is_table,
386            marker: core::marker::PhantomData,
387        })
388    }
389
390    fn state(
391        world: &'w crate::world::World,
392        plan: &crate::world::query::plan::ResolvedPlan,
393        cached: Option<QueryCachedSource>,
394        materialized: Option<(&'w [EntityId], bool)>,
395        table_archetypes: Option<&'w [usize]>,
396    ) -> Result<Query2State<'w>, crate::query::QueryError> {
397        if let Some((ids, apply_temporal)) = materialized {
398            return Ok(Query2State::Borrowed {
399                ids,
400                index: 0,
401                apply_temporal,
402            });
403        }
404        if let Some(source) = cached {
405            return Ok(Query2State::Cached { source, index: 0 });
406        }
407        match &plan.traversal {
408            crate::world::query::plan::TraversalSource::All => {
409                Err(crate::query::QueryError::WrongQuery {
410                    detail: alloc::string::String::from(
411                        "entity-only plan cannot back a typed query",
412                    ),
413                })
414            }
415            crate::world::query::plan::TraversalSource::Sparse { component_index } => {
416                let slots = world.sparse_dense_slots(*component_index).ok_or_else(|| {
417                    crate::query::QueryError::WrongStorageKind {
418                        name: alloc::format!("component {component_index}"),
419                    }
420                })?;
421                Ok(Query2State::Sparse { slots, index: 0 })
422            }
423            crate::world::query::plan::TraversalSource::Table { .. } => Ok(Query2State::Table {
424                archetypes: table_archetypes.expect("table archetypes prepared"),
425                archetype_index: 0,
426                row: 0,
427            }),
428            crate::world::query::plan::TraversalSource::Exact { ids } => Ok(Query2State::Exact {
429                ids: ids.clone(),
430                index: 0,
431            }),
432        }
433    }
434
435    fn commit_cursor_if_needed(&mut self) {
436        if self.cursor_committed {
437            return;
438        }
439        let world = self.world;
440        let fingerprint = self.params_fingerprint;
441        let cursor = self
442            .cursor
443            .as_mut()
444            .filter(|cursor| cursor.validate(world, fingerprint).is_ok());
445        if let Some(cursor) = cursor {
446            cursor.commit(self.captured_now);
447        }
448        self.cursor_committed = true;
449    }
450
451    fn match_entity(&self, entity: EntityId, filter: CandidateFilter) -> Option<(&'w A, &'w B)> {
452        let matches = match filter {
453            CandidateFilter::Full => crate::world::query::filter::entity_matches(
454                self.world,
455                entity,
456                &self.plan,
457                self.since,
458                self.captured_now,
459            ),
460            CandidateFilter::Temporal => crate::world::query::filter::entity_matches_temporal(
461                self.world,
462                entity,
463                &self.plan,
464                self.since,
465                self.captured_now,
466            ),
467            CandidateFilter::Trusted => true,
468        };
469        if !matches {
470            return None;
471        }
472        let first = self.world.query_component::<A>(
473            entity,
474            self.plan.primary_index,
475            self.plan.primary_is_table,
476        )?;
477        let second =
478            self.world
479                .query_component::<B>(entity, self.second_index, self.second_is_table)?;
480        Some((first, second))
481    }
482}
483
484impl<'w, 'c, A: 'static, B: 'static> Iterator for Query2<'w, 'c, A, B> {
485    type Item = (EntityId, &'w A, &'w B);
486
487    fn next(&mut self) -> Option<Self::Item> {
488        loop {
489            let candidate = match &mut self.state {
490                Query2State::Done => {
491                    self.commit_cursor_if_needed();
492                    return None;
493                }
494                Query2State::Sparse { slots, index } => {
495                    let entity = slots
496                        .get(*index)
497                        .copied()
498                        .map(|slot| self.world.entity_from_slot(slot));
499                    *index += usize::from(entity.is_some());
500                    entity.map(|entity| (entity, CandidateFilter::Full))
501                }
502                Query2State::Table {
503                    archetypes,
504                    archetype_index,
505                    row,
506                } => {
507                    let mut entity = None;
508                    while *archetype_index < archetypes.len() && entity.is_none() {
509                        let slots = self
510                            .world
511                            .archetype_entity_slots(archetypes[*archetype_index]);
512                        if let Some(slot) = slots.get(*row).copied() {
513                            *row += 1;
514                            entity = Some(self.world.entity_from_slot(slot));
515                        } else {
516                            *archetype_index += 1;
517                            *row = 0;
518                        }
519                    }
520                    entity.map(|entity| (entity, CandidateFilter::Full))
521                }
522                Query2State::Exact { ids, index } => {
523                    let entity = ids.get(*index).copied();
524                    *index += usize::from(entity.is_some());
525                    entity.map(|entity| (entity, CandidateFilter::Full))
526                }
527                Query2State::Cached { source, index } => {
528                    let ids = match self
529                        .world
530                        .cached_query_entities(source, self.params_fingerprint)
531                    {
532                        Ok(ids) => ids,
533                        Err(_) => {
534                            self.state = Query2State::Done;
535                            continue;
536                        }
537                    };
538                    let entity = ids.get(*index).copied();
539                    *index += usize::from(entity.is_some());
540                    entity.map(|entity| (entity, CandidateFilter::Full))
541                }
542                Query2State::Borrowed {
543                    ids,
544                    index,
545                    apply_temporal,
546                } => {
547                    let entity = ids.get(*index).copied();
548                    *index += usize::from(entity.is_some());
549                    let filter = if *apply_temporal {
550                        CandidateFilter::Temporal
551                    } else {
552                        CandidateFilter::Trusted
553                    };
554                    entity.map(|entity| (entity, filter))
555                }
556            };
557            let Some((entity, filter)) = candidate else {
558                self.state = Query2State::Done;
559                continue;
560            };
561            if let Some((first, second)) = self.match_entity(entity, filter) {
562                return Some((entity, first, second));
563            }
564        }
565    }
566}
567
568#[derive(Clone, Copy)]
569enum CandidateFilter {
570    Full,
571    Temporal,
572    Trusted,
573}
574
575impl<'w, 'c, A: 'static, B: 'static> Drop for Query2<'w, 'c, A, B> {
576    fn drop(&mut self) {
577        if let Query2State::Done = self.state {
578            self.commit_cursor_if_needed();
579        }
580    }
581}
582
583#[cfg(test)]
584mod tests {
585    use super::*;
586    use crate::component::ComponentOptions;
587    use crate::query::{ExactIdPolicy, QueryError, QueryParams, QuerySpec};
588    use crate::world::query::plan::{ResolvedPlan, TraversalSource};
589    use crate::world::WorldBuilder;
590    use alloc::rc::Rc;
591
592    #[derive(Clone, Copy)]
593    struct Pos(i32);
594
595    #[derive(Clone, Copy)]
596    struct Vel(#[allow(dead_code)] i32);
597
598    fn sparse_world() -> crate::world::World {
599        let mut builder = WorldBuilder::new();
600        builder
601            .register_component::<Pos>(ComponentOptions::sparse())
602            .expect("pos");
603        builder
604            .register_component::<Vel>(ComponentOptions::sparse())
605            .expect("vel");
606        builder.build().expect("build")
607    }
608
609    #[test]
610    fn cached_iterator_stops_when_cache_lookup_fails_mid_iteration() {
611        use crate::world::query::cached_source::QueryCachedSource;
612
613        let mut world = sparse_world();
614        let entity = world.spawn().expect("spawn");
615        world.insert(entity, Pos(1)).expect("insert");
616        let plan = world
617            .resolve_query1_plan::<Pos>(&QuerySpec::new())
618            .expect("plan");
619        let cache = world
620            .build_query_cache::<Pos>(QuerySpec::new())
621            .expect("cache");
622        let stale = cache.clone();
623        world.invalidate_query_cache(&cache);
624        let mut iter = Query1::<Pos> {
625            world: &world,
626            plan: plan.clone(),
627            params_fingerprint: plan.fingerprint,
628            captured_now: world.change_tick(),
629            since: crate::time::ChangeTick::ZERO,
630            cursor_committed: false,
631            cursor: None,
632            additional_covered_required: None,
633            state: Query1State::Cached {
634                source: QueryCachedSource::Membership(stale),
635                index: 0,
636            },
637        };
638        assert!(iter.next().is_none());
639        assert!(matches!(iter.state, Query1State::Done));
640    }
641
642    #[test]
643    fn query2_new_propagates_query1_resolution_errors() {
644        let world = sparse_world();
645        let plan = Rc::new(crate::world::query::plan::ResolvedPlan {
646            fingerprint: 1,
647            primary_index: 0,
648            primary_is_table: false,
649            traversal: crate::world::query::plan::TraversalSource::Sparse {
650                component_index: 99,
651            },
652            required_indices: alloc::vec![99],
653            without_indices: alloc::vec![],
654            with_tag_indices: alloc::vec![],
655            without_tag_indices: alloc::vec![],
656            added_indices: alloc::vec![],
657            changed_indices: alloc::vec![],
658            exact_id_policy: None,
659        });
660        assert!(matches!(
661            Query2::<Pos, Vel>::new(
662                &world,
663                plan,
664                crate::time::ChangeTick::ZERO,
665                crate::time::ChangeTick::ZERO,
666                None,
667                None,
668                None,
669                1,
670                false,
671            ),
672            Err(QueryError::WrongStorageKind { .. })
673        ));
674    }
675
676    #[test]
677    fn query2_iterator_skips_entities_missing_second_component() {
678        let mut world = sparse_world();
679        let partial = world.spawn().expect("partial");
680        let matched = world.spawn().expect("matched");
681        world.insert(partial, Pos(1)).expect("partial");
682        world.insert(matched, Pos(2)).expect("matched pos");
683        world.insert(matched, Vel(9)).expect("matched vel");
684        let plan = Rc::new(ResolvedPlan {
685            fingerprint: 1,
686            primary_index: 0,
687            primary_is_table: false,
688            traversal: TraversalSource::Sparse { component_index: 0 },
689            required_indices: alloc::vec![0],
690            without_indices: alloc::vec![],
691            with_tag_indices: alloc::vec![],
692            without_tag_indices: alloc::vec![],
693            added_indices: alloc::vec![],
694            changed_indices: alloc::vec![],
695            exact_id_policy: None,
696        });
697        let mut iter = Query2::<Pos, Vel>::new(
698            &world,
699            plan,
700            crate::time::ChangeTick::ZERO,
701            world.change_tick(),
702            None,
703            None,
704            None,
705            1,
706            false,
707        )
708        .expect("query2");
709        assert_eq!(iter.next().map(|(_, pos, _)| pos.0), Some(2));
710        assert!(iter.next().is_none());
711    }
712
713    #[test]
714    fn query1_source_covered_path_applies_remaining_filters() {
715        let mut world = sparse_world();
716        let partial = world.spawn().expect("partial");
717        let matched = world.spawn().expect("matched");
718        world.insert(partial, Pos(1)).expect("partial pos");
719        world.insert(partial, Vel(3)).expect("partial vel");
720        world.insert(matched, Pos(2)).expect("matched pos");
721        let plan = world
722            .resolve_query1_plan::<Pos>(&QuerySpec::new().without::<Vel>())
723            .expect("plan");
724        let pos_index = world.component_index::<Pos>().expect("pos index");
725        let mut iter: Query1<'_, '_, Pos> = Query1::new(
726            &world,
727            plan,
728            crate::time::ChangeTick::ZERO,
729            world.change_tick(),
730            None,
731            None,
732            None,
733            Some(pos_index),
734        )
735        .expect("query1");
736
737        assert_eq!(
738            iter.next().map(|(entity, pos)| (entity, pos.0)),
739            Some((matched, 2))
740        );
741        assert!(iter.next().is_none());
742    }
743
744    #[test]
745    fn exact_query1_skips_ids_missing_the_component() {
746        let mut world = sparse_world();
747        let missing = world.spawn().expect("missing");
748        let matched = world.spawn().expect("matched");
749        world.insert(matched, Pos(4)).expect("matched pos");
750        let spec = QuerySpec::new().exact_ids(
751            alloc::vec![missing, matched],
752            ExactIdPolicy::SkipUnavailable,
753        );
754        let values: alloc::vec::Vec<_> = world
755            .query::<Pos>(&spec, QueryParams::new())
756            .expect("query")
757            .map(|(_, pos)| pos.0)
758            .collect();
759        assert_eq!(values, alloc::vec![4]);
760    }
761
762    #[test]
763    fn cached_query1_applies_temporal_filter_on_valid_membership() {
764        let mut world = sparse_world();
765        let entity = world.spawn().expect("entity");
766        world.insert(entity, Pos(1)).expect("pos");
767        let since = world.change_tick();
768        world.get_mut::<Pos>(entity).expect("get").expect("pos").0 = 2;
769        let spec = QuerySpec::new().changed::<Pos>();
770        let cache = world.build_query_cache::<Pos>(spec.clone()).expect("cache");
771
772        assert_eq!(
773            world
774                .query::<Pos>(
775                    &spec,
776                    QueryParams::new().since(since).membership_cache(&cache),
777                )
778                .expect("cached query")
779                .count(),
780            1
781        );
782    }
783
784    #[test]
785    fn cached_and_borrowed_query1_return_valid_members() {
786        use crate::world::query::cached_source::QueryCachedSource;
787
788        let mut world = sparse_world();
789        let entity = world.spawn().expect("entity");
790        let stale = world.spawn().expect("stale");
791        let missing = world.spawn().expect("missing");
792        world.insert(entity, Pos(7)).expect("pos");
793        world.insert(stale, Pos(8)).expect("stale pos");
794        let spec = QuerySpec::new();
795        let plan = world.resolve_query1_plan::<Pos>(&spec).expect("plan");
796        let cache = world.build_query_cache::<Pos>(spec).expect("cache");
797        world.remove::<Pos>(stale).expect("remove stale pos");
798        let mut cached: Query1<'_, '_, Pos> = Query1::new(
799            &world,
800            plan.clone(),
801            crate::time::ChangeTick::ZERO,
802            world.change_tick(),
803            None,
804            Some(QueryCachedSource::Membership(cache)),
805            None,
806            None,
807        )
808        .expect("cached");
809        assert_eq!(
810            cached.next().map(|(id, value)| (id, value.0)),
811            Some((entity, 7))
812        );
813        assert!(cached.next().is_none());
814        drop(cached);
815
816        let ids = [missing, entity];
817        let mut borrowed: Query1<'_, '_, Pos> = Query1::new_prepared(
818            &world,
819            plan,
820            crate::time::ChangeTick::ZERO,
821            world.change_tick(),
822            None,
823            Some((&ids, false)),
824            None,
825        )
826        .expect("borrowed");
827        assert_eq!(
828            borrowed.next().map(|(id, value)| (id, value.0)),
829            Some((entity, 7))
830        );
831        assert!(borrowed.next().is_none());
832    }
833
834    #[test]
835    fn exhausted_typed_iterators_commit_their_cursors_and_drop_done() {
836        let mut world = sparse_world();
837        let entity = world.spawn().expect("entity");
838        world.insert(entity, Pos(1)).expect("pos");
839        world.insert(entity, Vel(2)).expect("vel");
840
841        let q1_spec = QuerySpec::new().changed::<Pos>();
842        let mut q1_cursor = crate::query::QueryCursor::from_spec_start::<Pos>(&mut world, &q1_spec)
843            .expect("Q1 cursor");
844        let q1_before = q1_cursor.since();
845        {
846            let mut query = world
847                .query::<Pos>(&q1_spec, QueryParams::new().cursor(&mut q1_cursor))
848                .expect("Q1");
849            assert!(query.next().is_some());
850            assert!(query.next().is_none());
851        }
852        assert!(q1_cursor.since() > q1_before);
853
854        let q2_spec = QuerySpec::new().changed::<Pos>();
855        let mut q2_cursor =
856            crate::query::QueryCursor::from_spec2_start::<Pos, Vel>(&mut world, &q2_spec)
857                .expect("Q2 cursor");
858        let q2_before = q2_cursor.since();
859        {
860            let mut query = world
861                .query2::<Pos, Vel>(&q2_spec, QueryParams::new().cursor(&mut q2_cursor))
862                .expect("Q2");
863            assert!(query.next().is_some());
864            assert!(query.next().is_none());
865        }
866        assert!(q2_cursor.since() > q2_before);
867
868        let mut partial_cursor =
869            crate::query::QueryCursor::from_spec2_start::<Pos, Vel>(&mut world, &q2_spec)
870                .expect("partial Q2 cursor");
871        let partial_before = partial_cursor.since();
872        {
873            let mut query = world
874                .query2::<Pos, Vel>(&q2_spec, QueryParams::new().cursor(&mut partial_cursor))
875                .expect("partial Q2");
876            assert!(query.next().is_some());
877        }
878        assert_eq!(partial_cursor.since(), partial_before);
879    }
880
881    #[test]
882    fn query2_rejects_entity_only_plan_and_stale_cache() {
883        use crate::world::query::cached_source::QueryCachedSource;
884
885        let mut world = sparse_world();
886        let all_plan = Rc::new(ResolvedPlan {
887            fingerprint: 9,
888            primary_index: 0,
889            primary_is_table: false,
890            traversal: TraversalSource::All,
891            required_indices: alloc::vec![],
892            without_indices: alloc::vec![],
893            with_tag_indices: alloc::vec![],
894            without_tag_indices: alloc::vec![],
895            added_indices: alloc::vec![],
896            changed_indices: alloc::vec![],
897            exact_id_policy: None,
898        });
899        assert!(matches!(
900            Query2::<Pos, Vel>::new(
901                &world,
902                all_plan,
903                crate::time::ChangeTick::ZERO,
904                world.change_tick(),
905                None,
906                None,
907                None,
908                1,
909                false,
910            ),
911            Err(QueryError::WrongQuery { .. })
912        ));
913
914        let entity = world.spawn().expect("entity");
915        world.insert(entity, Pos(1)).expect("pos");
916        world.insert(entity, Vel(2)).expect("vel");
917        let spec = QuerySpec::new();
918        let (plan, second_index, second_is_table) =
919            world.resolve_query2_plan::<Pos, Vel>(&spec).expect("plan");
920        let cache = world.build_query2_cache::<Pos, Vel>(spec).expect("cache");
921        {
922            let mut cached = Query2::<Pos, Vel>::new(
923                &world,
924                plan.clone(),
925                crate::time::ChangeTick::ZERO,
926                world.change_tick(),
927                None,
928                Some(QueryCachedSource::Membership(cache.clone())),
929                None,
930                second_index,
931                second_is_table,
932            )
933            .expect("cached iterator");
934            assert_eq!(cached.next().map(|(id, _, _)| id), Some(entity));
935            assert!(cached.next().is_none());
936        }
937        let stale = cache.clone();
938        world.invalidate_query_cache(&cache);
939        let mut iter = Query2::<Pos, Vel>::new(
940            &world,
941            plan,
942            crate::time::ChangeTick::ZERO,
943            world.change_tick(),
944            None,
945            Some(QueryCachedSource::Membership(stale)),
946            None,
947            second_index,
948            second_is_table,
949        )
950        .expect("iterator");
951        assert!(iter.next().is_none());
952        assert!(matches!(iter.state, Query2State::Done));
953    }
954
955    #[test]
956    fn borrowed_query2_selects_temporal_and_trusted_filters() {
957        let mut world = sparse_world();
958        let matched = world.spawn().expect("matched");
959        world.insert(matched, Pos(1)).expect("pos");
960        world.insert(matched, Vel(2)).expect("vel");
961        let since = world.change_tick();
962        world.get_mut::<Pos>(matched).expect("get").expect("pos").0 = 3;
963        let spec = QuerySpec::new().changed::<Pos>();
964        let (plan, second_index, second_is_table) =
965            world.resolve_query2_plan::<Pos, Vel>(&spec).expect("plan");
966        let temporal_ids = [matched];
967        let mut temporal = Query2::<Pos, Vel>::new_prepared(
968            &world,
969            plan.clone(),
970            since,
971            world.change_tick(),
972            None,
973            Some((&temporal_ids, true)),
974            None,
975            second_index,
976            second_is_table,
977        )
978        .expect("temporal iterator");
979        assert_eq!(temporal.next().map(|(id, _, _)| id), Some(matched));
980        assert!(temporal.next().is_none());
981        drop(temporal);
982
983        let missing_primary = world.spawn().expect("missing primary");
984        world
985            .insert(missing_primary, Vel(4))
986            .expect("secondary only");
987        let trusted_ids = [missing_primary];
988        let mut trusted = Query2::<Pos, Vel>::new_prepared(
989            &world,
990            plan,
991            crate::time::ChangeTick::ZERO,
992            world.change_tick(),
993            None,
994            Some((&trusted_ids, false)),
995            None,
996            second_index,
997            second_is_table,
998        )
999        .expect("trusted iterator");
1000        assert!(trusted.next().is_none());
1001    }
1002}