Skip to main content

ixa/entity/query/
mod.rs

1mod query_impls;
2
3use std::any::TypeId;
4use std::marker::PhantomData;
5use std::sync::{Mutex, OnceLock};
6
7use crate::entity::entity_set::{EntitySet, EntitySetIterator};
8use crate::entity::multi_property::type_ids_to_multi_property_id;
9use crate::entity::property_list::{PropertyInitializationList, PropertyList};
10use crate::entity::property_store::PropertyStore;
11use crate::entity::Entity;
12use crate::hashing::HashMap;
13use crate::prelude::EntityId;
14use crate::{Context, IxaError};
15
16/// A newtype wrapper that associates a tuple of property values with an entity type.
17///
18/// This is not meant to be used directly, but rather as a backing for the with! macro/
19/// a replacement for the query tuple.
20///
21/// # Example
22/// ```ignore
23/// use ixa::{define_entity, define_property, with};
24///
25/// define_entity!(Person);
26/// define_property!(struct Age(u8), Person, default_const = Age(0));
27///
28/// // Build a query for people with Age(42).
29/// let query = with!(Person, Age(42));
30/// ```
31pub struct EntityPropertyTuple<E: Entity, T> {
32    inner: T,
33    _marker: PhantomData<E>,
34}
35
36// Manual implementations to avoid requiring E: Copy/Clone
37impl<E: Entity, T: Copy> Copy for EntityPropertyTuple<E, T> {}
38
39impl<E: Entity, T: Clone> Clone for EntityPropertyTuple<E, T> {
40    fn clone(&self) -> Self {
41        Self {
42            inner: self.inner.clone(),
43            _marker: PhantomData,
44        }
45    }
46}
47
48impl<E: Entity, T: std::fmt::Debug> std::fmt::Debug for EntityPropertyTuple<E, T> {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        f.debug_struct("EntityPropertyTuple")
51            .field("inner", &self.inner)
52            .finish()
53    }
54}
55
56impl<E: Entity, T> EntityPropertyTuple<E, T> {
57    /// Create a new `EntityPropertyTuple` wrapping the given tuple.
58    pub fn new(inner: T) -> Self {
59        Self {
60            inner,
61            _marker: PhantomData,
62        }
63    }
64
65    /// Returns a reference to the inner tuple.
66    pub fn inner(&self) -> &T {
67        &self.inner
68    }
69
70    /// Consumes self and returns the inner tuple.
71    pub fn into_inner(self) -> T {
72        self.inner
73    }
74}
75
76impl<E: Entity, T: QueryInternal<E>> QueryInternal<E> for EntityPropertyTuple<E, T> {
77    type QueryParts<'a>
78        = T::QueryParts<'a>
79    where
80        Self: 'a;
81
82    fn get_type_ids(&self) -> Vec<TypeId> {
83        self.inner.get_type_ids()
84    }
85
86    fn multi_property_id(&self) -> Option<usize> {
87        self.inner.multi_property_id()
88    }
89
90    fn is_empty_query(&self) -> bool {
91        self.inner.is_empty_query()
92    }
93
94    fn query_parts(&self) -> Self::QueryParts<'_> {
95        self.inner.query_parts()
96    }
97
98    fn new_query_result<'c>(&self, context: &'c Context) -> EntitySet<'c, E> {
99        self.inner.new_query_result(context)
100    }
101
102    fn match_entity(&self, entity_id: EntityId<E>, context: &Context) -> bool {
103        self.inner.match_entity(entity_id, context)
104    }
105
106    fn filter_entities(&self, entities: &mut Vec<EntityId<E>>, context: &Context) {
107        self.inner.filter_entities(entities, context)
108    }
109}
110
111impl<E: Entity, T: PropertyList<E>> PropertyList<E> for EntityPropertyTuple<E, T> {
112    fn validate() -> Result<(), IxaError> {
113        T::validate()
114    }
115
116    fn contains_properties(property_type_ids: &[TypeId]) -> bool {
117        T::contains_properties(property_type_ids)
118    }
119
120    fn set_values_for_new_entity(
121        &self,
122        entity_id: EntityId<E>,
123        property_store: &mut PropertyStore<E>,
124    ) {
125        let tuple = *self;
126        tuple
127            .into_inner()
128            .set_values_for_new_entity(entity_id, property_store)
129    }
130
131    fn get_values_for_entity(context: &Context, entity_id: EntityId<E>) -> Self {
132        EntityPropertyTuple::new(T::get_values_for_entity(context, entity_id))
133    }
134}
135
136impl<E: Entity, PL: PropertyList<E>> PropertyInitializationList<E> for EntityPropertyTuple<E, PL> {}
137
138/// Internal query machinery.
139pub trait QueryInternal<E: Entity>: 'static {
140    /// Allocation-free representation of the query parts exposed by this query.
141    type QueryParts<'a>: AsRef<[&'a dyn std::any::Any]>
142    where
143        Self: 'a;
144
145    /// Returns an unordered list of type IDs of the properties in this query.
146    #[must_use]
147    fn get_type_ids(&self) -> Vec<TypeId>;
148
149    /// Returns the property ID of the representative multi-property having the properties of
150    /// this query, if any.
151    #[must_use]
152    fn multi_property_id(&self) -> Option<usize> {
153        #[allow(clippy::type_complexity)]
154        static REGISTRY: OnceLock<Mutex<HashMap<(usize, TypeId), &'static Option<usize>>>> =
155            OnceLock::new();
156
157        let map = REGISTRY.get_or_init(|| Mutex::new(HashMap::default()));
158        let mut map = map.lock().unwrap();
159        let key = (E::id(), TypeId::of::<Self>());
160        let entry = *map.entry(key).or_insert_with(|| {
161            let mut types = self.get_type_ids();
162            types.sort_unstable();
163            Box::leak(Box::new(type_ids_to_multi_property_id(
164                E::id(),
165                types.as_slice(),
166            )))
167        });
168
169        *entry
170    }
171
172    /// Indicates whether this query matches the entire population for `E`.
173    #[must_use]
174    fn is_empty_query(&self) -> bool {
175        false
176    }
177
178    /// Exposes the query parts without allocating.
179    #[must_use]
180    fn query_parts(&self) -> Self::QueryParts<'_>;
181
182    /// Creates a new query result as an `EntitySet`.
183    #[must_use]
184    fn new_query_result<'c>(&self, context: &'c Context) -> EntitySet<'c, E>;
185
186    /// Creates a new `EntitySetIterator`.
187    #[must_use]
188    fn new_query_result_iterator<'c>(&self, context: &'c Context) -> EntitySetIterator<'c, E> {
189        self.new_query_result(context).into_iter()
190    }
191
192    /// Determines if the given person matches this query.
193    #[must_use]
194    fn match_entity(&self, entity_id: EntityId<E>, context: &Context) -> bool;
195
196    /// Removes all `EntityId`s from the given vector that do not match this query.
197    fn filter_entities(&self, entities: &mut Vec<EntityId<E>>, context: &Context);
198}
199
200/// Values accepted by user-facing query APIs such as
201/// [`ContextEntitiesExt::query`](crate::entity::context_extension::ContextEntitiesExt::query)
202/// and
203/// [`ContextEntitiesExt::sample_entity`](crate::entity::context_extension::ContextEntitiesExt::sample_entity).
204///
205/// Use [`with!`](crate::with) to query for specific property values, or pass the entity type
206/// directly to work with the entire population.
207pub trait Query<E: Entity>: QueryInternal<E> {}
208
209impl<E: Entity, QI: QueryInternal<E>> Query<E> for EntityPropertyTuple<E, QI> {}
210impl<E: Entity> Query<E> for E {}
211
212#[cfg(test)]
213mod tests {
214
215    use super::QueryInternal;
216    use crate::prelude::*;
217    use crate::{
218        define_derived_property, define_entity, define_multi_property, define_property, Context,
219    };
220
221    define_entity!(Person);
222
223    define_property!(struct Age(u8), Person, default_const = Age(0));
224    define_property!(struct County(u32), Person, default_const = County(0));
225    define_property!(struct Height(u32), Person, default_const = Height(0));
226    define_property!(
227        enum RiskCategory {
228            High,
229            Low,
230        },
231        Person
232    );
233
234    define_multi_property!(Person, (Age, County));
235
236    #[test]
237    fn empty_tuple_query_internal_matches_all_entities() {
238        let mut context = Context::new();
239        let person1 = context
240            .add_entity(with!(Person, Age(42), RiskCategory::High))
241            .unwrap();
242        let person2 = context
243            .add_entity(with!(Person, Age(30), RiskCategory::Low))
244            .unwrap();
245
246        assert_eq!(<() as QueryInternal<Person>>::get_type_ids(&()), Vec::new());
247        assert!(<() as QueryInternal<Person>>::is_empty_query(&()));
248        assert!(<() as QueryInternal<Person>>::query_parts(&())
249            .as_ref()
250            .is_empty());
251
252        let people = <() as QueryInternal<Person>>::new_query_result_iterator(&(), &context)
253            .collect::<Vec<_>>();
254        assert_eq!(people, vec![person1, person2]);
255        assert!(<() as QueryInternal<Person>>::match_entity(
256            &(),
257            person1,
258            &context
259        ));
260
261        let mut ids = vec![person1, person2];
262        <() as QueryInternal<Person>>::filter_entities(&(), &mut ids, &context);
263        assert_eq!(ids, vec![person1, person2]);
264    }
265
266    #[test]
267    fn entity_query_internal_has_no_type_ids() {
268        let mut context = Context::new();
269        let person1 = context
270            .add_entity(with!(Person, Age(42), RiskCategory::High))
271            .unwrap();
272        let person2 = context
273            .add_entity(with!(Person, Age(30), RiskCategory::Low))
274            .unwrap();
275
276        assert_eq!(
277            <Person as QueryInternal<Person>>::get_type_ids(&Person),
278            Vec::new()
279        );
280        assert_eq!(
281            <Person as QueryInternal<Person>>::multi_property_id(&Person),
282            None
283        );
284        assert!(<Person as QueryInternal<Person>>::query_parts(&Person)
285            .as_ref()
286            .is_empty());
287
288        let people = <Person as QueryInternal<Person>>::new_query_result(&Person, &context)
289            .into_iter()
290            .collect::<Vec<_>>();
291        assert_eq!(people, vec![person1, person2]);
292        assert!(<Person as QueryInternal<Person>>::match_entity(
293            &Person, person1, &context
294        ));
295
296        let mut ids = vec![person1, person2];
297        <Person as QueryInternal<Person>>::filter_entities(&Person, &mut ids, &context);
298        assert_eq!(ids, vec![person1, person2]);
299    }
300
301    #[test]
302    fn singleton_query_result_iterator_uses_indexed_and_unindexed_paths() {
303        let mut context = Context::new();
304        let high = context
305            .add_entity(with!(Person, RiskCategory::High))
306            .unwrap();
307        let _low = context
308            .add_entity(with!(Person, RiskCategory::Low))
309            .unwrap();
310
311        let people = <(RiskCategory,) as QueryInternal<Person>>::new_query_result_iterator(
312            &(RiskCategory::High,),
313            &context,
314        )
315        .collect::<Vec<_>>();
316        assert_eq!(people, vec![high]);
317
318        let mut indexed_context = Context::new();
319        indexed_context.index_property::<Person, RiskCategory>();
320        let indexed_high = indexed_context
321            .add_entity(with!(Person, RiskCategory::High))
322            .unwrap();
323        let _indexed_low = indexed_context
324            .add_entity(with!(Person, RiskCategory::Low))
325            .unwrap();
326
327        let people = <(RiskCategory,) as QueryInternal<Person>>::new_query_result_iterator(
328            &(RiskCategory::High,),
329            &indexed_context,
330        )
331        .collect::<Vec<_>>();
332        assert_eq!(people, vec![indexed_high]);
333
334        let mut empty_index_context = Context::new();
335        empty_index_context.index_property::<Person, Age>();
336        let _ = empty_index_context
337            .add_entity(with!(Person, Age(42), RiskCategory::Low))
338            .unwrap();
339
340        let people = <(Age,) as QueryInternal<Person>>::new_query_result_iterator(
341            &(Age(99),),
342            &empty_index_context,
343        )
344        .collect::<Vec<_>>();
345        assert!(people.is_empty());
346    }
347
348    #[test]
349    fn tuple_query_result_iterator_uses_indexed_and_unindexed_paths() {
350        let mut context = Context::new();
351        let matching1 = context
352            .add_entity(with!(Person, Age(28), County(0), RiskCategory::High))
353            .unwrap();
354        let _wrong_county = context
355            .add_entity(with!(Person, Age(28), County(1), RiskCategory::Low))
356            .unwrap();
357        let _wrong_age = context
358            .add_entity(with!(Person, Age(30), County(0), RiskCategory::Low))
359            .unwrap();
360        let matching2 = context
361            .add_entity(with!(Person, Age(28), County(0), RiskCategory::Low))
362            .unwrap();
363
364        let people = <(Age, County) as QueryInternal<Person>>::new_query_result_iterator(
365            &(Age(28), County(0)),
366            &context,
367        )
368        .collect::<Vec<_>>();
369        assert_eq!(people, vec![matching1, matching2]);
370
371        let mut indexed_context = Context::new();
372        indexed_context.index_property::<Person, (Age, County)>();
373        let indexed_matching = indexed_context
374            .add_entity(with!(Person, Age(28), County(0), RiskCategory::High))
375            .unwrap();
376        let _indexed_nonmatching = indexed_context
377            .add_entity(with!(Person, Age(28), County(1), RiskCategory::Low))
378            .unwrap();
379
380        let people = <(Age, County) as QueryInternal<Person>>::new_query_result_iterator(
381            &(Age(28), County(0)),
382            &indexed_context,
383        )
384        .collect::<Vec<_>>();
385        assert_eq!(people, vec![indexed_matching]);
386
387        let people = <(Age, County) as QueryInternal<Person>>::new_query_result_iterator(
388            &(Age(99), County(99)),
389            &indexed_context,
390        )
391        .collect::<Vec<_>>();
392        assert!(people.is_empty());
393    }
394
395    #[test]
396    fn singleton_filter_entities_keeps_matching_entities() {
397        let mut context = Context::new();
398        let high1 = context
399            .add_entity(with!(Person, RiskCategory::High))
400            .unwrap();
401        let low = context
402            .add_entity(with!(Person, RiskCategory::Low))
403            .unwrap();
404        let high2 = context
405            .add_entity(with!(Person, RiskCategory::High))
406            .unwrap();
407
408        let mut people = vec![high1, low, high2];
409        context.filter_entities(&mut people, with!(Person, RiskCategory::High));
410
411        assert_eq!(people, vec![high1, high2]);
412    }
413
414    #[test]
415    fn tuple_filter_entities_falls_through_after_unsupported_multi_index_lookup() {
416        let mut context = Context::new();
417        let matching1 = context
418            .add_entity(with!(Person, Age(28), County(0), RiskCategory::High))
419            .unwrap();
420        let wrong_county = context
421            .add_entity(with!(Person, Age(28), County(1), RiskCategory::Low))
422            .unwrap();
423        let wrong_age = context
424            .add_entity(with!(Person, Age(30), County(0), RiskCategory::Low))
425            .unwrap();
426        let matching2 = context
427            .add_entity(with!(Person, Age(28), County(0), RiskCategory::Low))
428            .unwrap();
429
430        let mut people = vec![matching1, wrong_county, wrong_age, matching2];
431        context.filter_entities(&mut people, with!(Person, County(0), Age(28)));
432
433        assert_eq!(people, vec![matching1, matching2]);
434    }
435
436    #[test]
437    fn with_query_results() {
438        let mut context = Context::new();
439        let _ = context
440            .add_entity(with!(Person, RiskCategory::High))
441            .unwrap();
442
443        context.with_query_results(with!(Person, RiskCategory::High), &mut |people| {
444            assert_eq!(people.into_iter().count(), 1);
445        });
446    }
447
448    #[test]
449    fn with_query_results_empty() {
450        let context = Context::new();
451
452        context.with_query_results(with!(Person, RiskCategory::High), &mut |people| {
453            assert_eq!(people.into_iter().count(), 0);
454        });
455    }
456
457    #[test]
458    fn query_entity_count() {
459        let mut context = Context::new();
460        let _ = context
461            .add_entity(with!(Person, RiskCategory::High))
462            .unwrap();
463
464        assert_eq!(
465            context.query_entity_count(with!(Person, RiskCategory::High)),
466            1
467        );
468    }
469
470    #[test]
471    fn query_entity_count_empty() {
472        let context = Context::new();
473
474        assert_eq!(
475            context.query_entity_count(with!(Person, RiskCategory::High)),
476            0
477        );
478    }
479
480    #[test]
481    fn with_query_results_macro_index_first() {
482        let mut context = Context::new();
483        let _ = context
484            .add_entity(with!(Person, RiskCategory::High))
485            .unwrap();
486        context.index_property::<_, RiskCategory>();
487        assert!(context.is_property_indexed::<Person, RiskCategory>());
488
489        context.with_query_results(with!(Person, RiskCategory::High), &mut |people| {
490            assert_eq!(people.into_iter().count(), 1);
491        });
492    }
493
494    #[test]
495    fn with_query_results_macro_index_second() {
496        let mut context = Context::new();
497        let _ = context.add_entity(with!(Person, RiskCategory::High));
498
499        context.with_query_results(with!(Person, RiskCategory::High), &mut |people| {
500            assert_eq!(people.into_iter().count(), 1);
501        });
502        assert!(!context.is_property_indexed::<Person, RiskCategory>());
503
504        context.index_property::<Person, RiskCategory>();
505        assert!(context.is_property_indexed::<Person, RiskCategory>());
506
507        context.with_query_results(with!(Person, RiskCategory::High), &mut |people| {
508            assert_eq!(people.into_iter().count(), 1);
509        });
510    }
511
512    #[test]
513    fn with_query_results_macro_change() {
514        let mut context = Context::new();
515        let person1 = context
516            .add_entity(with!(Person, RiskCategory::High))
517            .unwrap();
518
519        context.with_query_results(with!(Person, RiskCategory::High), &mut |people| {
520            assert_eq!(people.into_iter().count(), 1);
521        });
522
523        context.with_query_results(with!(Person, RiskCategory::Low), &mut |people| {
524            assert_eq!(people.into_iter().count(), 0);
525        });
526
527        context.set_property(person1, RiskCategory::Low);
528        context.with_query_results(with!(Person, RiskCategory::High), &mut |people| {
529            assert_eq!(people.into_iter().count(), 0);
530        });
531
532        context.with_query_results(with!(Person, RiskCategory::Low), &mut |people| {
533            assert_eq!(people.into_iter().count(), 1);
534        });
535    }
536
537    #[test]
538    fn with_query_results_index_after_add() {
539        let mut context = Context::new();
540        let _ = context
541            .add_entity(with!(Person, RiskCategory::High))
542            .unwrap();
543        context.index_property::<Person, RiskCategory>();
544        assert!(context.is_property_indexed::<Person, RiskCategory>());
545        context.with_query_results(with!(Person, RiskCategory::High), &mut |people| {
546            assert_eq!(people.into_iter().count(), 1);
547        });
548    }
549
550    #[test]
551    fn with_query_results_add_after_index() {
552        let mut context = Context::new();
553        let _ = context
554            .add_entity(with!(Person, RiskCategory::High))
555            .unwrap();
556        context.index_property::<Person, RiskCategory>();
557        assert!(context.is_property_indexed::<Person, RiskCategory>());
558        context.with_query_results(with!(Person, RiskCategory::High), &mut |people| {
559            assert_eq!(people.into_iter().count(), 1);
560        });
561
562        let _ = context
563            .add_entity(with!(Person, RiskCategory::High))
564            .unwrap();
565        context.with_query_results(with!(Person, RiskCategory::High), &mut |people| {
566            assert_eq!(people.into_iter().count(), 2);
567        });
568    }
569
570    #[test]
571    fn with_query_results_cast_value() {
572        let mut context = Context::new();
573        let _ = context
574            .add_entity(with!(Person, Age(42), RiskCategory::High))
575            .unwrap();
576
577        context.with_query_results(with!(Person, Age(42)), &mut |people| {
578            assert_eq!(people.into_iter().count(), 1);
579        });
580    }
581
582    #[test]
583    fn with_query_results_intersection() {
584        let mut context = Context::new();
585        let _ = context
586            .add_entity(with!(Person, Age(42), RiskCategory::High))
587            .unwrap();
588        let _ = context
589            .add_entity(with!(Person, Age(42), RiskCategory::Low))
590            .unwrap();
591        let _ = context
592            .add_entity(with!(Person, Age(40), RiskCategory::Low))
593            .unwrap();
594
595        context.with_query_results(with!(Person, Age(42), RiskCategory::High), &mut |people| {
596            assert_eq!(people.into_iter().count(), 1);
597        });
598    }
599
600    #[test]
601    fn with_query_results_intersection_non_macro() {
602        let mut context = Context::new();
603        let _ = context
604            .add_entity(with!(Person, Age(42), RiskCategory::High))
605            .unwrap();
606        let _ = context
607            .add_entity(with!(Person, Age(42), RiskCategory::Low))
608            .unwrap();
609        let _ = context
610            .add_entity(with!(Person, Age(40), RiskCategory::Low))
611            .unwrap();
612
613        context.with_query_results(with!(Person, Age(42), RiskCategory::High), &mut |people| {
614            assert_eq!(people.into_iter().count(), 1);
615        });
616    }
617
618    #[test]
619    fn with_query_results_intersection_one_indexed() {
620        let mut context = Context::new();
621        let _ = context
622            .add_entity(with!(Person, Age(42), RiskCategory::High))
623            .unwrap();
624        let _ = context
625            .add_entity(with!(Person, Age(42), RiskCategory::Low))
626            .unwrap();
627        let _ = context
628            .add_entity(with!(Person, Age(40), RiskCategory::Low))
629            .unwrap();
630
631        context.index_property::<Person, Age>();
632        context.with_query_results(with!(Person, Age(42), RiskCategory::High), &mut |people| {
633            assert_eq!(people.into_iter().count(), 1);
634        });
635    }
636
637    #[test]
638    fn query_derived_prop() {
639        let mut context = Context::new();
640        define_derived_property!(struct Senior(bool), Person, [Age], |age| Senior(age.0 >= 65));
641
642        let person = context
643            .add_entity(with!(Person, Age(64), RiskCategory::High))
644            .unwrap();
645        context
646            .add_entity(with!(Person, Age(88), RiskCategory::High))
647            .unwrap();
648
649        let mut not_seniors = Vec::new();
650        context.with_query_results(with!(Person, Senior(false)), &mut |people| {
651            not_seniors = people.to_owned_vec();
652        });
653        let mut seniors = Vec::new();
654        context.with_query_results(with!(Person, Senior(true)), &mut |people| {
655            seniors = people.to_owned_vec();
656        });
657        assert_eq!(seniors.len(), 1, "One senior");
658        assert_eq!(not_seniors.len(), 1, "One non-senior");
659
660        context.set_property(person, Age(65));
661
662        context.with_query_results(with!(Person, Senior(false)), &mut |people| {
663            not_seniors = people.to_owned_vec()
664        });
665        context.with_query_results(with!(Person, Senior(true)), &mut |people| {
666            seniors = people.to_owned_vec()
667        });
668
669        assert_eq!(seniors.len(), 2, "Two seniors");
670        assert_eq!(not_seniors.len(), 0, "No non-seniors");
671    }
672
673    #[test]
674    fn query_derived_prop_with_index() {
675        let mut context = Context::new();
676        define_derived_property!(struct Senior(bool), Person, [Age], |age| Senior(age.0 >= 65));
677
678        context.index_property::<Person, Senior>();
679        let person = context
680            .add_entity(with!(Person, Age(64), RiskCategory::Low))
681            .unwrap();
682        let _ = context.add_entity(with!(Person, Age(88), RiskCategory::Low));
683
684        let mut not_seniors = Vec::new();
685        context.with_query_results(with!(Person, Senior(false)), &mut |people| {
686            not_seniors = people.to_owned_vec()
687        });
688        let mut seniors = Vec::new();
689        context.with_query_results(with!(Person, Senior(true)), &mut |people| {
690            seniors = people.to_owned_vec()
691        });
692        assert_eq!(seniors.len(), 1, "One senior");
693        assert_eq!(not_seniors.len(), 1, "One non-senior");
694
695        context.set_property(person, Age(65));
696
697        context.with_query_results(with!(Person, Senior(false)), &mut |people| {
698            not_seniors = people.to_owned_vec()
699        });
700        context.with_query_results(with!(Person, Senior(true)), &mut |people| {
701            seniors = people.to_owned_vec()
702        });
703
704        assert_eq!(seniors.len(), 2, "Two seniors");
705        assert_eq!(not_seniors.len(), 0, "No non-seniors");
706    }
707
708    // create a multi-property index
709    define_multi_property!(Person, (Age, County, Height));
710    define_multi_property!(Person, (County, Height));
711
712    #[test]
713    fn query_derived_prop_with_optimized_index() {
714        let mut context = Context::new();
715        // create a 'regular' derived property
716        define_derived_property!(
717            struct Ach(u8, u32, u32),
718            Person,
719            [Age, County, Height],
720            [],
721            |age, county, height| Ach(age.0, county.0, height.0)
722        );
723
724        // add some people
725        let _ = context.add_entity(with!(
726            Person,
727            Age(64),
728            County(2),
729            Height(120),
730            RiskCategory::Low
731        ));
732        let _ = context.add_entity(with!(
733            Person,
734            Age(88),
735            County(2),
736            Height(130),
737            RiskCategory::Low
738        ));
739        let p2 = context
740            .add_entity(with!(
741                Person,
742                Age(8),
743                County(1),
744                Height(140),
745                RiskCategory::Low
746            ))
747            .unwrap();
748        let p3 = context
749            .add_entity(with!(
750                Person,
751                Age(28),
752                County(1),
753                Height(140),
754                RiskCategory::Low
755            ))
756            .unwrap();
757        let p4 = context
758            .add_entity(with!(
759                Person,
760                Age(28),
761                County(2),
762                Height(160),
763                RiskCategory::Low
764            ))
765            .unwrap();
766        let p5 = context
767            .add_entity(with!(
768                Person,
769                Age(28),
770                County(2),
771                Height(160),
772                RiskCategory::Low
773            ))
774            .unwrap();
775
776        // 'regular' derived property
777        context.with_query_results(with!(Person, Ach(28, 2, 160)), &mut |people| {
778            assert!(people.contains(p4));
779            assert!(people.contains(p5));
780            assert_eq!(people.into_iter().count(), 2, "Should have 2 matches");
781        });
782
783        // multi-property index
784        context.with_query_results(
785            with!(Person, Age(28), County(2), Height(160)),
786            &mut |people| {
787                assert!(people.contains(p4));
788                assert!(people.contains(p5));
789                assert_eq!(people.into_iter().count(), 2, "Should have 2 matches");
790            },
791        );
792
793        // multi-property index with different order
794        context.with_query_results(
795            with!(Person, County(2), Height(160), Age(28)),
796            &mut |people| {
797                assert!(people.contains(p4));
798                assert!(people.contains(p5));
799                assert_eq!(people.into_iter().count(), 2, "Should have 2 matches");
800            },
801        );
802
803        // multi-property index with different order
804        context.with_query_results(
805            with!(Person, Height(160), County(2), Age(28)),
806            &mut |people| {
807                assert!(people.contains(p4));
808                assert!(people.contains(p5));
809                assert_eq!(people.into_iter().count(), 2, "Should have 2 matches");
810            },
811        );
812
813        // multi-property index with different order and different value
814        context.with_query_results(
815            with!(Person, Height(140), County(1), Age(28)),
816            &mut |people| {
817                assert!(people.contains(p3));
818                assert_eq!(people.into_iter().count(), 1, "Should have 1 matches");
819            },
820        );
821
822        context.set_property(p2, Age(28));
823        // multi-property index again after changing the value
824        context.with_query_results(
825            with!(Person, Height(140), County(1), Age(28)),
826            &mut |people| {
827                assert!(people.contains(p2));
828                assert!(people.contains(p3));
829                assert_eq!(people.into_iter().count(), 2, "Should have 2 matches");
830            },
831        );
832
833        context.with_query_results(with!(Person, Height(140), County(1)), &mut |people| {
834            assert!(people.contains(p2));
835            assert!(people.contains(p3));
836            assert_eq!(people.into_iter().count(), 2, "Should have 2 matches");
837        });
838    }
839
840    #[test]
841    fn test_match_entity() {
842        let mut context = Context::new();
843        let person = context
844            .add_entity(with!(
845                Person,
846                Age(28),
847                County(2),
848                Height(160),
849                RiskCategory::Low
850            ))
851            .unwrap();
852        assert!(context.match_entity(person, with!(Person, Age(28), County(2), Height(160))));
853        assert!(!context.match_entity(person, with!(Person, Age(13), County(2), Height(160))));
854        assert!(!context.match_entity(person, with!(Person, Age(28), County(33), Height(160))));
855        assert!(!context.match_entity(person, with!(Person, Age(28), County(2), Height(9))));
856    }
857
858    #[test]
859    fn filter_entities_for_unindexed_query() {
860        let mut context = Context::new();
861        let mut people = Vec::new();
862
863        for idx in 0..10 {
864            let person = context
865                .add_entity(with!(
866                    Person,
867                    Age(28),
868                    County(idx % 2),
869                    Height(160),
870                    RiskCategory::Low
871                ))
872                .unwrap();
873            people.push(person);
874        }
875
876        context.filter_entities(
877            &mut people,
878            with!(Person, Age(28), County(0), Height(160), RiskCategory::Low),
879        );
880
881        let expected = (0..5)
882            .map(|idx| PersonId::new(idx * 2))
883            .collect::<Vec<PersonId>>();
884        assert_eq!(people, expected);
885    }
886
887    #[test]
888    fn filter_entities_for_indexed_query() {
889        let mut context = Context::new();
890        let mut people = Vec::new();
891
892        context.index_property::<Person, (Age, County)>();
893
894        for idx in 0..10 {
895            let person = context
896                .add_entity(with!(
897                    Person,
898                    Age(28),
899                    County(idx % 2),
900                    Height(160),
901                    RiskCategory::Low
902                ))
903                .unwrap();
904            people.push(person);
905        }
906
907        context.filter_entities(&mut people, with!(Person, County(0), Age(28)));
908
909        let expected = (0..5)
910            .map(|idx| PersonId::new(idx * 2))
911            .collect::<Vec<PersonId>>();
912        assert_eq!(people, expected);
913    }
914
915    #[test]
916    fn entity_property_tuple_basic() {
917        use super::EntityPropertyTuple;
918
919        let mut context = Context::new();
920        let p1 = context
921            .add_entity(with!(Person, Age(42), RiskCategory::High))
922            .unwrap();
923        let _ = context
924            .add_entity(with!(Person, Age(42), RiskCategory::Low))
925            .unwrap();
926        let _ = context
927            .add_entity(with!(Person, Age(30), RiskCategory::High))
928            .unwrap();
929
930        // Create query using EntityPropertyTuple
931        let query: EntityPropertyTuple<Person, _> =
932            EntityPropertyTuple::new((Age(42), RiskCategory::High));
933
934        context.with_query_results(query, &mut |people| {
935            assert!(people.contains(p1));
936            assert_eq!(people.into_iter().count(), 1);
937        });
938
939        // Test match_entity
940        assert!(context.match_entity(p1, query));
941
942        // Test query_entity_count
943        assert_eq!(context.query_entity_count(query), 1);
944    }
945
946    #[test]
947    fn entity_property_tuple_empty_query() {
948        use super::EntityPropertyTuple;
949
950        let mut context = Context::new();
951        let _ = context
952            .add_entity(with!(Person, Age(42), RiskCategory::High))
953            .unwrap();
954        let _ = context
955            .add_entity(with!(Person, Age(30), RiskCategory::Low))
956            .unwrap();
957
958        // Empty query matches all entities
959        let query: EntityPropertyTuple<Person, _> = EntityPropertyTuple::new(());
960
961        assert_eq!(context.query_entity_count(query), 2);
962    }
963
964    #[test]
965    fn entity_property_tuple_singleton() {
966        use super::EntityPropertyTuple;
967
968        let mut context = Context::new();
969        let _ = context
970            .add_entity(with!(Person, Age(42), RiskCategory::High))
971            .unwrap();
972        let _ = context
973            .add_entity(with!(Person, Age(42), RiskCategory::Low))
974            .unwrap();
975        let _ = context
976            .add_entity(with!(Person, Age(30), RiskCategory::High))
977            .unwrap();
978
979        // Single property query
980        let query: EntityPropertyTuple<Person, _> = EntityPropertyTuple::new((Age(42),));
981
982        assert_eq!(context.query_entity_count(query), 2);
983    }
984
985    #[test]
986    fn entity_property_tuple_inner_access() {
987        use super::EntityPropertyTuple;
988
989        let query: EntityPropertyTuple<Person, _> =
990            EntityPropertyTuple::new((Age(42), RiskCategory::High));
991
992        // Test inner() accessor
993        let inner = query.inner();
994        assert_eq!(inner.0, Age(42));
995        assert_eq!(inner.1, RiskCategory::High);
996
997        // Test into_inner()
998        let (age, risk) = query.into_inner();
999        assert_eq!(age, Age(42));
1000        assert_eq!(risk, RiskCategory::High);
1001    }
1002
1003    #[test]
1004    fn all_macro_no_properties() {
1005        use crate::with;
1006
1007        let mut context = Context::new();
1008        let _ = context
1009            .add_entity(with!(Person, Age(42), RiskCategory::High))
1010            .unwrap();
1011        let _ = context
1012            .add_entity(with!(Person, Age(30), RiskCategory::Low))
1013            .unwrap();
1014
1015        // with!(Person) should match all Person entities
1016        let query = with!(Person);
1017        assert_eq!(context.query_entity_count(query), 2);
1018    }
1019
1020    #[test]
1021    fn all_macro_single_property() {
1022        use crate::with;
1023
1024        let mut context = Context::new();
1025        let _ = context
1026            .add_entity(with!(Person, Age(42), RiskCategory::High))
1027            .unwrap();
1028        let _ = context
1029            .add_entity(with!(Person, Age(42), RiskCategory::Low))
1030            .unwrap();
1031        let _ = context
1032            .add_entity(with!(Person, Age(30), RiskCategory::High))
1033            .unwrap();
1034
1035        // with!(Person, Age(42)) should match entities with Age = 42
1036        let query = with!(Person, Age(42));
1037        assert_eq!(context.query_entity_count(query), 2);
1038    }
1039
1040    #[test]
1041    fn all_macro_multiple_properties() {
1042        use crate::with;
1043
1044        let mut context = Context::new();
1045        let p1 = context
1046            .add_entity(with!(Person, Age(42), RiskCategory::High))
1047            .unwrap();
1048        let _ = context
1049            .add_entity(with!(Person, Age(42), RiskCategory::Low))
1050            .unwrap();
1051        let _ = context
1052            .add_entity(with!(Person, Age(30), RiskCategory::High))
1053            .unwrap();
1054
1055        // with!(Person, Age(42), RiskCategory::High) should match one entity
1056        let query = with!(Person, Age(42), RiskCategory::High);
1057        assert_eq!(context.query_entity_count(query), 1);
1058
1059        context.with_query_results(query, &mut |people| {
1060            assert!(people.contains(p1));
1061        });
1062    }
1063
1064    #[test]
1065    fn all_macro_with_trailing_comma() {
1066        use crate::with;
1067
1068        let mut context = Context::new();
1069        let _ = context
1070            .add_entity(with!(Person, Age(42), RiskCategory::High))
1071            .unwrap();
1072
1073        // Trailing comma should work
1074        let query = with!(Person, Age(42));
1075        assert_eq!(context.query_entity_count(query), 1);
1076
1077        let query = with!(Person, Age(42), RiskCategory::High);
1078        assert_eq!(context.query_entity_count(query), 1);
1079    }
1080
1081    #[test]
1082    fn entity_property_tuple_as_property_list() {
1083        use super::EntityPropertyTuple;
1084        use crate::entity::property_list::PropertyList;
1085
1086        // Test validate
1087        assert!(EntityPropertyTuple::<Person, (Age,)>::validate().is_ok());
1088        assert!(EntityPropertyTuple::<Person, (Age, RiskCategory)>::validate().is_ok());
1089
1090        // Test contains_properties
1091        assert!(EntityPropertyTuple::<Person, (Age,)>::contains_properties(
1092            &[Age::type_id()]
1093        ));
1094        assert!(
1095            EntityPropertyTuple::<Person, (Age, RiskCategory)>::contains_properties(&[
1096                Age::type_id()
1097            ])
1098        );
1099        assert!(
1100            EntityPropertyTuple::<Person, (Age, RiskCategory)>::contains_properties(&[
1101                Age::type_id(),
1102                RiskCategory::type_id()
1103            ])
1104        );
1105    }
1106
1107    #[test]
1108    fn all_macro_as_property_list_for_add_entity() {
1109        use crate::with;
1110
1111        let mut context = Context::new();
1112
1113        // Use with! macro result to add an entity
1114        let props = with!(Person, Age(42), RiskCategory::High);
1115        let person = context.add_entity(props).unwrap();
1116
1117        // Verify the entity was created with the correct properties
1118        assert_eq!(context.get_property::<Person, Age>(person), Age(42));
1119        assert_eq!(
1120            context.get_property::<Person, RiskCategory>(person),
1121            RiskCategory::High
1122        );
1123    }
1124}