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_index;
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    fn get_type_ids(&self) -> Vec<TypeId>;
147
148    /// Returns the `TypeId` of the multi-property having the properties of this query, if any.
149    fn multi_property_id(&self) -> Option<usize> {
150        // This trick allows us to cache the multi-property ID so we don't have to allocate every
151        // time.
152        static REGISTRY: OnceLock<Mutex<HashMap<TypeId, &'static Option<usize>>>> = OnceLock::new();
153
154        let map = REGISTRY.get_or_init(|| Mutex::new(HashMap::default()));
155        let mut map = map.lock().unwrap();
156        let type_id = TypeId::of::<Self>();
157        let entry = *map.entry(type_id).or_insert_with(|| {
158            let mut types = self.get_type_ids();
159            types.sort_unstable();
160            Box::leak(Box::new(type_ids_to_multi_property_index(types.as_slice())))
161        });
162
163        *entry
164    }
165
166    /// Indicates whether this query matches the entire population for `E`.
167    fn is_empty_query(&self) -> bool {
168        false
169    }
170
171    /// Exposes the query parts without allocating.
172    fn query_parts(&self) -> Self::QueryParts<'_>;
173
174    /// Creates a new query result as an `EntitySet`.
175    fn new_query_result<'c>(&self, context: &'c Context) -> EntitySet<'c, E>;
176
177    /// Creates a new `EntitySetIterator`.
178    fn new_query_result_iterator<'c>(&self, context: &'c Context) -> EntitySetIterator<'c, E> {
179        self.new_query_result(context).into_iter()
180    }
181
182    /// Determines if the given person matches this query.
183    fn match_entity(&self, entity_id: EntityId<E>, context: &Context) -> bool;
184
185    /// Removes all `EntityId`s from the given vector that do not match this query.
186    fn filter_entities(&self, entities: &mut Vec<EntityId<E>>, context: &Context);
187}
188
189/// Values accepted by user-facing query APIs such as
190/// [`ContextEntitiesExt::query`](crate::entity::context_extension::ContextEntitiesExt::query)
191/// and
192/// [`ContextEntitiesExt::sample_entity`](crate::entity::context_extension::ContextEntitiesExt::sample_entity).
193///
194/// Use [`with!`](crate::with) to query for specific property values, or pass the entity type
195/// directly to work with the entire population.
196pub trait Query<E: Entity>: QueryInternal<E> {}
197
198impl<E: Entity, QI: QueryInternal<E>> Query<E> for EntityPropertyTuple<E, QI> {}
199impl<E: Entity> Query<E> for E {}
200
201#[cfg(test)]
202mod tests {
203
204    use crate::prelude::*;
205    use crate::{
206        define_derived_property, define_entity, define_multi_property, define_property, Context,
207    };
208
209    define_entity!(Person);
210
211    define_property!(struct Age(u8), Person, default_const = Age(0));
212    define_property!(struct County(u32), Person, default_const = County(0));
213    define_property!(struct Height(u32), Person, default_const = Height(0));
214    define_property!(
215        enum RiskCategory {
216            High,
217            Low,
218        },
219        Person
220    );
221
222    define_multi_property!((Age, County), Person);
223
224    #[test]
225    fn with_query_results() {
226        let mut context = Context::new();
227        let _ = context
228            .add_entity(with!(Person, RiskCategory::High))
229            .unwrap();
230
231        context.with_query_results(with!(Person, RiskCategory::High), &mut |people| {
232            assert_eq!(people.into_iter().count(), 1);
233        });
234    }
235
236    #[test]
237    fn with_query_results_empty() {
238        let context = Context::new();
239
240        context.with_query_results(with!(Person, RiskCategory::High), &mut |people| {
241            assert_eq!(people.into_iter().count(), 0);
242        });
243    }
244
245    #[test]
246    fn query_entity_count() {
247        let mut context = Context::new();
248        let _ = context
249            .add_entity(with!(Person, RiskCategory::High))
250            .unwrap();
251
252        assert_eq!(
253            context.query_entity_count(with!(Person, RiskCategory::High)),
254            1
255        );
256    }
257
258    #[test]
259    fn query_entity_count_empty() {
260        let context = Context::new();
261
262        assert_eq!(
263            context.query_entity_count(with!(Person, RiskCategory::High)),
264            0
265        );
266    }
267
268    #[test]
269    fn with_query_results_macro_index_first() {
270        let mut context = Context::new();
271        let _ = context
272            .add_entity(with!(Person, RiskCategory::High))
273            .unwrap();
274        context.index_property::<_, RiskCategory>();
275        assert!(context.is_property_indexed::<Person, RiskCategory>());
276
277        context.with_query_results(with!(Person, RiskCategory::High), &mut |people| {
278            assert_eq!(people.into_iter().count(), 1);
279        });
280    }
281
282    #[test]
283    fn with_query_results_macro_index_second() {
284        let mut context = Context::new();
285        let _ = context.add_entity(with!(Person, RiskCategory::High));
286
287        context.with_query_results(with!(Person, RiskCategory::High), &mut |people| {
288            assert_eq!(people.into_iter().count(), 1);
289        });
290        assert!(!context.is_property_indexed::<Person, RiskCategory>());
291
292        context.index_property::<Person, RiskCategory>();
293        assert!(context.is_property_indexed::<Person, RiskCategory>());
294
295        context.with_query_results(with!(Person, RiskCategory::High), &mut |people| {
296            assert_eq!(people.into_iter().count(), 1);
297        });
298    }
299
300    #[test]
301    fn with_query_results_macro_change() {
302        let mut context = Context::new();
303        let person1 = context
304            .add_entity(with!(Person, RiskCategory::High))
305            .unwrap();
306
307        context.with_query_results(with!(Person, RiskCategory::High), &mut |people| {
308            assert_eq!(people.into_iter().count(), 1);
309        });
310
311        context.with_query_results(with!(Person, RiskCategory::Low), &mut |people| {
312            assert_eq!(people.into_iter().count(), 0);
313        });
314
315        context.set_property(person1, RiskCategory::Low);
316        context.with_query_results(with!(Person, RiskCategory::High), &mut |people| {
317            assert_eq!(people.into_iter().count(), 0);
318        });
319
320        context.with_query_results(with!(Person, RiskCategory::Low), &mut |people| {
321            assert_eq!(people.into_iter().count(), 1);
322        });
323    }
324
325    #[test]
326    fn with_query_results_index_after_add() {
327        let mut context = Context::new();
328        let _ = context
329            .add_entity(with!(Person, RiskCategory::High))
330            .unwrap();
331        context.index_property::<Person, RiskCategory>();
332        assert!(context.is_property_indexed::<Person, RiskCategory>());
333        context.with_query_results(with!(Person, RiskCategory::High), &mut |people| {
334            assert_eq!(people.into_iter().count(), 1);
335        });
336    }
337
338    #[test]
339    fn with_query_results_add_after_index() {
340        let mut context = Context::new();
341        let _ = context
342            .add_entity(with!(Person, RiskCategory::High))
343            .unwrap();
344        context.index_property::<Person, RiskCategory>();
345        assert!(context.is_property_indexed::<Person, RiskCategory>());
346        context.with_query_results(with!(Person, RiskCategory::High), &mut |people| {
347            assert_eq!(people.into_iter().count(), 1);
348        });
349
350        let _ = context
351            .add_entity(with!(Person, RiskCategory::High))
352            .unwrap();
353        context.with_query_results(with!(Person, RiskCategory::High), &mut |people| {
354            assert_eq!(people.into_iter().count(), 2);
355        });
356    }
357
358    #[test]
359    fn with_query_results_cast_value() {
360        let mut context = Context::new();
361        let _ = context
362            .add_entity(with!(Person, Age(42), RiskCategory::High))
363            .unwrap();
364
365        context.with_query_results(with!(Person, Age(42)), &mut |people| {
366            assert_eq!(people.into_iter().count(), 1);
367        });
368    }
369
370    #[test]
371    fn with_query_results_intersection() {
372        let mut context = Context::new();
373        let _ = context
374            .add_entity(with!(Person, Age(42), RiskCategory::High))
375            .unwrap();
376        let _ = context
377            .add_entity(with!(Person, Age(42), RiskCategory::Low))
378            .unwrap();
379        let _ = context
380            .add_entity(with!(Person, Age(40), RiskCategory::Low))
381            .unwrap();
382
383        context.with_query_results(with!(Person, Age(42), RiskCategory::High), &mut |people| {
384            assert_eq!(people.into_iter().count(), 1);
385        });
386    }
387
388    #[test]
389    fn with_query_results_intersection_non_macro() {
390        let mut context = Context::new();
391        let _ = context
392            .add_entity(with!(Person, Age(42), RiskCategory::High))
393            .unwrap();
394        let _ = context
395            .add_entity(with!(Person, Age(42), RiskCategory::Low))
396            .unwrap();
397        let _ = context
398            .add_entity(with!(Person, Age(40), RiskCategory::Low))
399            .unwrap();
400
401        context.with_query_results(with!(Person, Age(42), RiskCategory::High), &mut |people| {
402            assert_eq!(people.into_iter().count(), 1);
403        });
404    }
405
406    #[test]
407    fn with_query_results_intersection_one_indexed() {
408        let mut context = Context::new();
409        let _ = context
410            .add_entity(with!(Person, Age(42), RiskCategory::High))
411            .unwrap();
412        let _ = context
413            .add_entity(with!(Person, Age(42), RiskCategory::Low))
414            .unwrap();
415        let _ = context
416            .add_entity(with!(Person, Age(40), RiskCategory::Low))
417            .unwrap();
418
419        context.index_property::<Person, Age>();
420        context.with_query_results(with!(Person, Age(42), RiskCategory::High), &mut |people| {
421            assert_eq!(people.into_iter().count(), 1);
422        });
423    }
424
425    #[test]
426    fn query_derived_prop() {
427        let mut context = Context::new();
428        define_derived_property!(struct Senior(bool), Person, [Age], |age| Senior(age.0 >= 65));
429
430        let person = context
431            .add_entity(with!(Person, Age(64), RiskCategory::High))
432            .unwrap();
433        context
434            .add_entity(with!(Person, Age(88), RiskCategory::High))
435            .unwrap();
436
437        let mut not_seniors = Vec::new();
438        context.with_query_results(with!(Person, Senior(false)), &mut |people| {
439            not_seniors = people.to_owned_vec();
440        });
441        let mut seniors = Vec::new();
442        context.with_query_results(with!(Person, Senior(true)), &mut |people| {
443            seniors = people.to_owned_vec();
444        });
445        assert_eq!(seniors.len(), 1, "One senior");
446        assert_eq!(not_seniors.len(), 1, "One non-senior");
447
448        context.set_property(person, Age(65));
449
450        context.with_query_results(with!(Person, Senior(false)), &mut |people| {
451            not_seniors = people.to_owned_vec()
452        });
453        context.with_query_results(with!(Person, Senior(true)), &mut |people| {
454            seniors = people.to_owned_vec()
455        });
456
457        assert_eq!(seniors.len(), 2, "Two seniors");
458        assert_eq!(not_seniors.len(), 0, "No non-seniors");
459    }
460
461    #[test]
462    fn query_derived_prop_with_index() {
463        let mut context = Context::new();
464        define_derived_property!(struct Senior(bool), Person, [Age], |age| Senior(age.0 >= 65));
465
466        context.index_property::<Person, Senior>();
467        let person = context
468            .add_entity(with!(Person, Age(64), RiskCategory::Low))
469            .unwrap();
470        let _ = context.add_entity(with!(Person, Age(88), RiskCategory::Low));
471
472        let mut not_seniors = Vec::new();
473        context.with_query_results(with!(Person, Senior(false)), &mut |people| {
474            not_seniors = people.to_owned_vec()
475        });
476        let mut seniors = Vec::new();
477        context.with_query_results(with!(Person, Senior(true)), &mut |people| {
478            seniors = people.to_owned_vec()
479        });
480        assert_eq!(seniors.len(), 1, "One senior");
481        assert_eq!(not_seniors.len(), 1, "One non-senior");
482
483        context.set_property(person, Age(65));
484
485        context.with_query_results(with!(Person, Senior(false)), &mut |people| {
486            not_seniors = people.to_owned_vec()
487        });
488        context.with_query_results(with!(Person, Senior(true)), &mut |people| {
489            seniors = people.to_owned_vec()
490        });
491
492        assert_eq!(seniors.len(), 2, "Two seniors");
493        assert_eq!(not_seniors.len(), 0, "No non-seniors");
494    }
495
496    // create a multi-property index
497    define_multi_property!((Age, County, Height), Person);
498    define_multi_property!((County, Height), Person);
499
500    #[test]
501    fn query_derived_prop_with_optimized_index() {
502        let mut context = Context::new();
503        // create a 'regular' derived property
504        define_derived_property!(
505            struct Ach(u8, u32, u32),
506            Person,
507            [Age, County, Height],
508            [],
509            |age, county, height| Ach(age.0, county.0, height.0)
510        );
511
512        // add some people
513        let _ = context.add_entity(with!(
514            Person,
515            Age(64),
516            County(2),
517            Height(120),
518            RiskCategory::Low
519        ));
520        let _ = context.add_entity(with!(
521            Person,
522            Age(88),
523            County(2),
524            Height(130),
525            RiskCategory::Low
526        ));
527        let p2 = context
528            .add_entity(with!(
529                Person,
530                Age(8),
531                County(1),
532                Height(140),
533                RiskCategory::Low
534            ))
535            .unwrap();
536        let p3 = context
537            .add_entity(with!(
538                Person,
539                Age(28),
540                County(1),
541                Height(140),
542                RiskCategory::Low
543            ))
544            .unwrap();
545        let p4 = context
546            .add_entity(with!(
547                Person,
548                Age(28),
549                County(2),
550                Height(160),
551                RiskCategory::Low
552            ))
553            .unwrap();
554        let p5 = context
555            .add_entity(with!(
556                Person,
557                Age(28),
558                County(2),
559                Height(160),
560                RiskCategory::Low
561            ))
562            .unwrap();
563
564        // 'regular' derived property
565        context.with_query_results(with!(Person, Ach(28, 2, 160)), &mut |people| {
566            assert!(people.contains(p4));
567            assert!(people.contains(p5));
568            assert_eq!(people.into_iter().count(), 2, "Should have 2 matches");
569        });
570
571        // multi-property index
572        context.with_query_results(
573            with!(Person, Age(28), County(2), Height(160)),
574            &mut |people| {
575                assert!(people.contains(p4));
576                assert!(people.contains(p5));
577                assert_eq!(people.into_iter().count(), 2, "Should have 2 matches");
578            },
579        );
580
581        // multi-property index with different order
582        context.with_query_results(
583            with!(Person, County(2), Height(160), Age(28)),
584            &mut |people| {
585                assert!(people.contains(p4));
586                assert!(people.contains(p5));
587                assert_eq!(people.into_iter().count(), 2, "Should have 2 matches");
588            },
589        );
590
591        // multi-property index with different order
592        context.with_query_results(
593            with!(Person, Height(160), County(2), Age(28)),
594            &mut |people| {
595                assert!(people.contains(p4));
596                assert!(people.contains(p5));
597                assert_eq!(people.into_iter().count(), 2, "Should have 2 matches");
598            },
599        );
600
601        // multi-property index with different order and different value
602        context.with_query_results(
603            with!(Person, Height(140), County(1), Age(28)),
604            &mut |people| {
605                assert!(people.contains(p3));
606                assert_eq!(people.into_iter().count(), 1, "Should have 1 matches");
607            },
608        );
609
610        context.set_property(p2, Age(28));
611        // multi-property index again after changing the value
612        context.with_query_results(
613            with!(Person, Height(140), County(1), Age(28)),
614            &mut |people| {
615                assert!(people.contains(p2));
616                assert!(people.contains(p3));
617                assert_eq!(people.into_iter().count(), 2, "Should have 2 matches");
618            },
619        );
620
621        context.with_query_results(with!(Person, Height(140), County(1)), &mut |people| {
622            assert!(people.contains(p2));
623            assert!(people.contains(p3));
624            assert_eq!(people.into_iter().count(), 2, "Should have 2 matches");
625        });
626    }
627
628    #[test]
629    fn test_match_entity() {
630        let mut context = Context::new();
631        let person = context
632            .add_entity(with!(
633                Person,
634                Age(28),
635                County(2),
636                Height(160),
637                RiskCategory::Low
638            ))
639            .unwrap();
640        assert!(context.match_entity(person, with!(Person, Age(28), County(2), Height(160))));
641        assert!(!context.match_entity(person, with!(Person, Age(13), County(2), Height(160))));
642        assert!(!context.match_entity(person, with!(Person, Age(28), County(33), Height(160))));
643        assert!(!context.match_entity(person, with!(Person, Age(28), County(2), Height(9))));
644    }
645
646    #[test]
647    fn filter_entities_for_unindexed_query() {
648        let mut context = Context::new();
649        let mut people = Vec::new();
650
651        for idx in 0..10 {
652            let person = context
653                .add_entity(with!(
654                    Person,
655                    Age(28),
656                    County(idx % 2),
657                    Height(160),
658                    RiskCategory::Low
659                ))
660                .unwrap();
661            people.push(person);
662        }
663
664        context.filter_entities(
665            &mut people,
666            with!(Person, Age(28), County(0), Height(160), RiskCategory::Low),
667        );
668
669        let expected = (0..5)
670            .map(|idx| PersonId::new(idx * 2))
671            .collect::<Vec<PersonId>>();
672        assert_eq!(people, expected);
673    }
674
675    #[test]
676    fn filter_entities_for_indexed_query() {
677        let mut context = Context::new();
678        let mut people = Vec::new();
679
680        context.index_property::<Person, (Age, County)>();
681
682        for idx in 0..10 {
683            let person = context
684                .add_entity(with!(
685                    Person,
686                    Age(28),
687                    County(idx % 2),
688                    Height(160),
689                    RiskCategory::Low
690                ))
691                .unwrap();
692            people.push(person);
693        }
694
695        context.filter_entities(&mut people, with!(Person, County(0), Age(28)));
696
697        let expected = (0..5)
698            .map(|idx| PersonId::new(idx * 2))
699            .collect::<Vec<PersonId>>();
700        assert_eq!(people, expected);
701    }
702
703    #[test]
704    fn entity_property_tuple_basic() {
705        use super::EntityPropertyTuple;
706
707        let mut context = Context::new();
708        let p1 = context
709            .add_entity(with!(Person, Age(42), RiskCategory::High))
710            .unwrap();
711        let _ = context
712            .add_entity(with!(Person, Age(42), RiskCategory::Low))
713            .unwrap();
714        let _ = context
715            .add_entity(with!(Person, Age(30), RiskCategory::High))
716            .unwrap();
717
718        // Create query using EntityPropertyTuple
719        let query: EntityPropertyTuple<Person, _> =
720            EntityPropertyTuple::new((Age(42), RiskCategory::High));
721
722        context.with_query_results(query, &mut |people| {
723            assert!(people.contains(p1));
724            assert_eq!(people.into_iter().count(), 1);
725        });
726
727        // Test match_entity
728        assert!(context.match_entity(p1, query));
729
730        // Test query_entity_count
731        assert_eq!(context.query_entity_count(query), 1);
732    }
733
734    #[test]
735    fn entity_property_tuple_empty_query() {
736        use super::EntityPropertyTuple;
737
738        let mut context = Context::new();
739        let _ = context
740            .add_entity(with!(Person, Age(42), RiskCategory::High))
741            .unwrap();
742        let _ = context
743            .add_entity(with!(Person, Age(30), RiskCategory::Low))
744            .unwrap();
745
746        // Empty query matches all entities
747        let query: EntityPropertyTuple<Person, _> = EntityPropertyTuple::new(());
748
749        assert_eq!(context.query_entity_count(query), 2);
750    }
751
752    #[test]
753    fn entity_property_tuple_singleton() {
754        use super::EntityPropertyTuple;
755
756        let mut context = Context::new();
757        let _ = context
758            .add_entity(with!(Person, Age(42), RiskCategory::High))
759            .unwrap();
760        let _ = context
761            .add_entity(with!(Person, Age(42), RiskCategory::Low))
762            .unwrap();
763        let _ = context
764            .add_entity(with!(Person, Age(30), RiskCategory::High))
765            .unwrap();
766
767        // Single property query
768        let query: EntityPropertyTuple<Person, _> = EntityPropertyTuple::new((Age(42),));
769
770        assert_eq!(context.query_entity_count(query), 2);
771    }
772
773    #[test]
774    fn entity_property_tuple_inner_access() {
775        use super::EntityPropertyTuple;
776
777        let query: EntityPropertyTuple<Person, _> =
778            EntityPropertyTuple::new((Age(42), RiskCategory::High));
779
780        // Test inner() accessor
781        let inner = query.inner();
782        assert_eq!(inner.0, Age(42));
783        assert_eq!(inner.1, RiskCategory::High);
784
785        // Test into_inner()
786        let (age, risk) = query.into_inner();
787        assert_eq!(age, Age(42));
788        assert_eq!(risk, RiskCategory::High);
789    }
790
791    #[test]
792    fn all_macro_no_properties() {
793        use crate::with;
794
795        let mut context = Context::new();
796        let _ = context
797            .add_entity(with!(Person, Age(42), RiskCategory::High))
798            .unwrap();
799        let _ = context
800            .add_entity(with!(Person, Age(30), RiskCategory::Low))
801            .unwrap();
802
803        // with!(Person) should match all Person entities
804        let query = with!(Person);
805        assert_eq!(context.query_entity_count(query), 2);
806    }
807
808    #[test]
809    fn all_macro_single_property() {
810        use crate::with;
811
812        let mut context = Context::new();
813        let _ = context
814            .add_entity(with!(Person, Age(42), RiskCategory::High))
815            .unwrap();
816        let _ = context
817            .add_entity(with!(Person, Age(42), RiskCategory::Low))
818            .unwrap();
819        let _ = context
820            .add_entity(with!(Person, Age(30), RiskCategory::High))
821            .unwrap();
822
823        // with!(Person, Age(42)) should match entities with Age = 42
824        let query = with!(Person, Age(42));
825        assert_eq!(context.query_entity_count(query), 2);
826    }
827
828    #[test]
829    fn all_macro_multiple_properties() {
830        use crate::with;
831
832        let mut context = Context::new();
833        let p1 = context
834            .add_entity(with!(Person, Age(42), RiskCategory::High))
835            .unwrap();
836        let _ = context
837            .add_entity(with!(Person, Age(42), RiskCategory::Low))
838            .unwrap();
839        let _ = context
840            .add_entity(with!(Person, Age(30), RiskCategory::High))
841            .unwrap();
842
843        // with!(Person, Age(42), RiskCategory::High) should match one entity
844        let query = with!(Person, Age(42), RiskCategory::High);
845        assert_eq!(context.query_entity_count(query), 1);
846
847        context.with_query_results(query, &mut |people| {
848            assert!(people.contains(p1));
849        });
850    }
851
852    #[test]
853    fn all_macro_with_trailing_comma() {
854        use crate::with;
855
856        let mut context = Context::new();
857        let _ = context
858            .add_entity(with!(Person, Age(42), RiskCategory::High))
859            .unwrap();
860
861        // Trailing comma should work
862        let query = with!(Person, Age(42));
863        assert_eq!(context.query_entity_count(query), 1);
864
865        let query = with!(Person, Age(42), RiskCategory::High);
866        assert_eq!(context.query_entity_count(query), 1);
867    }
868
869    #[test]
870    fn entity_property_tuple_as_property_list() {
871        use super::EntityPropertyTuple;
872        use crate::entity::property_list::PropertyList;
873
874        // Test validate
875        assert!(EntityPropertyTuple::<Person, (Age,)>::validate().is_ok());
876        assert!(EntityPropertyTuple::<Person, (Age, RiskCategory)>::validate().is_ok());
877
878        // Test contains_properties
879        assert!(EntityPropertyTuple::<Person, (Age,)>::contains_properties(
880            &[Age::type_id()]
881        ));
882        assert!(
883            EntityPropertyTuple::<Person, (Age, RiskCategory)>::contains_properties(&[
884                Age::type_id()
885            ])
886        );
887        assert!(
888            EntityPropertyTuple::<Person, (Age, RiskCategory)>::contains_properties(&[
889                Age::type_id(),
890                RiskCategory::type_id()
891            ])
892        );
893    }
894
895    #[test]
896    fn all_macro_as_property_list_for_add_entity() {
897        use crate::with;
898
899        let mut context = Context::new();
900
901        // Use with! macro result to add an entity
902        let props = with!(Person, Age(42), RiskCategory::High);
903        let person = context.add_entity(props).unwrap();
904
905        // Verify the entity was created with the correct properties
906        assert_eq!(context.get_property::<Person, Age>(person), Age(42));
907        assert_eq!(
908            context.get_property::<Person, RiskCategory>(person),
909            RiskCategory::High
910        );
911    }
912}