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::PropertyList;
10use crate::entity::property_store::PropertyStore;
11use crate::entity::{Entity, HashValueType};
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::{EntityPropertyTuple, define_entity, define_property};
24///
25/// define_entity!(Person);
26/// define_property!(struct Age(u8), Person, default_const = Age(0));
27///
28/// // Use the all macro
29/// let query = with!(Person, Age(42));
30/// // Under the hood this is:
31/// // EntityPropertyTuple::<Person>::new((Age(42),));
32/// ```
33pub struct EntityPropertyTuple<E: Entity, T> {
34    inner: T,
35    _marker: PhantomData<E>,
36}
37
38// Manual implementations to avoid requiring E: Copy/Clone
39impl<E: Entity, T: Copy> Copy for EntityPropertyTuple<E, T> {}
40
41impl<E: Entity, T: Clone> Clone for EntityPropertyTuple<E, T> {
42    fn clone(&self) -> Self {
43        Self {
44            inner: self.inner.clone(),
45            _marker: PhantomData,
46        }
47    }
48}
49
50impl<E: Entity, T: std::fmt::Debug> std::fmt::Debug for EntityPropertyTuple<E, T> {
51    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        f.debug_struct("EntityPropertyTuple")
53            .field("inner", &self.inner)
54            .finish()
55    }
56}
57
58impl<E: Entity, T> EntityPropertyTuple<E, T> {
59    /// Create a new `EntityPropertyTuple` wrapping the given tuple.
60    pub fn new(inner: T) -> Self {
61        Self {
62            inner,
63            _marker: PhantomData,
64        }
65    }
66
67    /// Returns a reference to the inner tuple.
68    pub fn inner(&self) -> &T {
69        &self.inner
70    }
71
72    /// Consumes self and returns the inner tuple.
73    pub fn into_inner(self) -> T {
74        self.inner
75    }
76}
77
78impl<E: Entity, T: Query<E>> Query<E> for EntityPropertyTuple<E, T> {
79    fn get_query(&self) -> Vec<(usize, HashValueType)> {
80        self.inner.get_query()
81    }
82
83    fn get_type_ids(&self) -> Vec<TypeId> {
84        self.inner.get_type_ids()
85    }
86
87    fn multi_property_id(&self) -> Option<usize> {
88        self.inner.multi_property_id()
89    }
90
91    fn multi_property_value_hash(&self) -> HashValueType {
92        self.inner.multi_property_value_hash()
93    }
94
95    fn new_query_result<'c>(&self, context: &'c Context) -> EntitySet<'c, E> {
96        self.inner.new_query_result(context)
97    }
98
99    fn match_entity(&self, entity_id: EntityId<E>, context: &Context) -> bool {
100        self.inner.match_entity(entity_id, context)
101    }
102
103    fn filter_entities(&self, entities: &mut Vec<EntityId<E>>, context: &Context) {
104        self.inner.filter_entities(entities, context)
105    }
106}
107
108impl<E: Entity, T: PropertyList<E>> PropertyList<E> for EntityPropertyTuple<E, T> {
109    fn validate() -> Result<(), IxaError> {
110        T::validate()
111    }
112
113    fn contains_properties(property_type_ids: &[TypeId]) -> bool {
114        T::contains_properties(property_type_ids)
115    }
116
117    fn set_values_for_new_entity(
118        &self,
119        entity_id: EntityId<E>,
120        property_store: &mut PropertyStore<E>,
121    ) {
122        let tuple = *self;
123        tuple
124            .into_inner()
125            .set_values_for_new_entity(entity_id, property_store)
126    }
127
128    fn get_values_for_entity(context: &Context, entity_id: EntityId<E>) -> Self {
129        EntityPropertyTuple::new(T::get_values_for_entity(context, entity_id))
130    }
131}
132
133/// Encapsulates a query.
134///
135/// [`ContextEntitiesExt::query_result_iterator`](crate::entity::context_extension::ContextEntitiesExt::query_result_iterator)
136/// actually takes an instance of [`Query`], but because
137/// we implement Query for tuples of up to size 20, that's invisible
138/// to the caller. Do not use this trait directly.
139pub trait Query<E: Entity>: Copy + 'static {
140    /// Returns a list of `(type_id, hash)` pairs where `hash` is the hash of the property value
141    /// and `type_id` is `Property.type_id()`.
142    fn get_query(&self) -> Vec<(usize, HashValueType)>;
143
144    /// Returns an unordered list of type IDs of the properties in this query.
145    fn get_type_ids(&self) -> Vec<TypeId>;
146
147    /// Returns the `TypeId` of the multi-property having the properties of this query, if any.
148    fn multi_property_id(&self) -> Option<usize> {
149        // This trick allows us to cache the multi-property ID so we don't have to allocate every
150        // time.
151        static REGISTRY: OnceLock<Mutex<HashMap<TypeId, &'static Option<usize>>>> = OnceLock::new();
152
153        let map = REGISTRY.get_or_init(|| Mutex::new(HashMap::default()));
154        let mut map = map.lock().unwrap();
155        let type_id = TypeId::of::<Self>();
156        let entry = *map.entry(type_id).or_insert_with(|| {
157            let mut types = self.get_type_ids();
158            types.sort_unstable();
159            Box::leak(Box::new(type_ids_to_multi_property_index(types.as_slice())))
160        });
161
162        *entry
163    }
164
165    /// If this query is a multi-property query, this method computes the hash of the
166    /// multi-property value.
167    fn multi_property_value_hash(&self) -> HashValueType;
168
169    /// Creates a new query result as an `EntitySet`.
170    fn new_query_result<'c>(&self, context: &'c Context) -> EntitySet<'c, E>;
171
172    /// Creates a new `EntitySetIterator`.
173    fn new_query_result_iterator<'c>(&self, context: &'c Context) -> EntitySetIterator<'c, E> {
174        self.new_query_result(context).into_iter()
175    }
176
177    /// Determines if the given person matches this query.
178    fn match_entity(&self, entity_id: EntityId<E>, context: &Context) -> bool;
179
180    /// Removes all `EntityId`s from the given vector that do not match this query.
181    fn filter_entities(&self, entities: &mut Vec<EntityId<E>>, context: &Context);
182}
183
184#[cfg(test)]
185mod tests {
186
187    use crate::prelude::*;
188    use crate::{
189        define_derived_property, define_entity, define_multi_property, define_property, Context,
190    };
191
192    define_entity!(Person);
193
194    define_property!(struct Age(u8), Person, default_const = Age(0));
195    define_property!(struct County(u32), Person, default_const = County(0));
196    define_property!(struct Height(u32), Person, default_const = Height(0));
197    define_property!(
198        enum RiskCategory {
199            High,
200            Low,
201        },
202        Person
203    );
204
205    define_multi_property!((Age, County), Person);
206
207    #[test]
208    fn with_query_results() {
209        let mut context = Context::new();
210        let _ = context.add_entity((RiskCategory::High,)).unwrap();
211
212        context.with_query_results((RiskCategory::High,), &mut |people| {
213            assert_eq!(people.into_iter().count(), 1);
214        });
215    }
216
217    #[test]
218    fn with_query_results_empty() {
219        let context = Context::new();
220
221        context.with_query_results((RiskCategory::High,), &mut |people| {
222            assert_eq!(people.into_iter().count(), 0);
223        });
224    }
225
226    #[test]
227    fn query_entity_count() {
228        let mut context = Context::new();
229        let _ = context.add_entity((RiskCategory::High,)).unwrap();
230
231        assert_eq!(context.query_entity_count((RiskCategory::High,)), 1);
232    }
233
234    #[test]
235    fn query_entity_count_empty() {
236        let context = Context::new();
237
238        assert_eq!(context.query_entity_count((RiskCategory::High,)), 0);
239    }
240
241    #[test]
242    fn with_query_results_macro_index_first() {
243        let mut context = Context::new();
244        let _ = context.add_entity((RiskCategory::High,)).unwrap();
245        context.index_property::<_, RiskCategory>();
246        assert!(context.is_property_indexed::<Person, RiskCategory>());
247
248        context.with_query_results((RiskCategory::High,), &mut |people| {
249            assert_eq!(people.into_iter().count(), 1);
250        });
251    }
252
253    #[test]
254    fn with_query_results_macro_index_second() {
255        let mut context = Context::new();
256        let _ = context.add_entity((RiskCategory::High,));
257
258        context.with_query_results((RiskCategory::High,), &mut |people| {
259            assert_eq!(people.into_iter().count(), 1);
260        });
261        assert!(!context.is_property_indexed::<Person, RiskCategory>());
262
263        context.index_property::<Person, RiskCategory>();
264        assert!(context.is_property_indexed::<Person, RiskCategory>());
265
266        context.with_query_results((RiskCategory::High,), &mut |people| {
267            assert_eq!(people.into_iter().count(), 1);
268        });
269    }
270
271    #[test]
272    fn with_query_results_macro_change() {
273        let mut context = Context::new();
274        let person1 = context.add_entity((RiskCategory::High,)).unwrap();
275
276        context.with_query_results((RiskCategory::High,), &mut |people| {
277            assert_eq!(people.into_iter().count(), 1);
278        });
279
280        context.with_query_results((RiskCategory::Low,), &mut |people| {
281            assert_eq!(people.into_iter().count(), 0);
282        });
283
284        context.set_property(person1, RiskCategory::Low);
285        context.with_query_results((RiskCategory::High,), &mut |people| {
286            assert_eq!(people.into_iter().count(), 0);
287        });
288
289        context.with_query_results((RiskCategory::Low,), &mut |people| {
290            assert_eq!(people.into_iter().count(), 1);
291        });
292    }
293
294    #[test]
295    fn with_query_results_index_after_add() {
296        let mut context = Context::new();
297        let _ = context.add_entity((RiskCategory::High,)).unwrap();
298        context.index_property::<Person, RiskCategory>();
299        assert!(context.is_property_indexed::<Person, RiskCategory>());
300        context.with_query_results((RiskCategory::High,), &mut |people| {
301            assert_eq!(people.into_iter().count(), 1);
302        });
303    }
304
305    #[test]
306    fn with_query_results_add_after_index() {
307        let mut context = Context::new();
308        let _ = context.add_entity((RiskCategory::High,)).unwrap();
309        context.index_property::<Person, RiskCategory>();
310        assert!(context.is_property_indexed::<Person, RiskCategory>());
311        context.with_query_results((RiskCategory::High,), &mut |people| {
312            assert_eq!(people.into_iter().count(), 1);
313        });
314
315        let _ = context.add_entity((RiskCategory::High,)).unwrap();
316        context.with_query_results((RiskCategory::High,), &mut |people| {
317            assert_eq!(people.into_iter().count(), 2);
318        });
319    }
320
321    #[test]
322    fn with_query_results_cast_value() {
323        let mut context = Context::new();
324        let _ = context.add_entity((Age(42), RiskCategory::High)).unwrap();
325
326        context.with_query_results((Age(42),), &mut |people| {
327            assert_eq!(people.into_iter().count(), 1);
328        });
329    }
330
331    #[test]
332    fn with_query_results_intersection() {
333        let mut context = Context::new();
334        let _ = context.add_entity((Age(42), RiskCategory::High)).unwrap();
335        let _ = context.add_entity((Age(42), RiskCategory::Low)).unwrap();
336        let _ = context.add_entity((Age(40), RiskCategory::Low)).unwrap();
337
338        context.with_query_results((Age(42), RiskCategory::High), &mut |people| {
339            assert_eq!(people.into_iter().count(), 1);
340        });
341    }
342
343    #[test]
344    fn with_query_results_intersection_non_macro() {
345        let mut context = Context::new();
346        let _ = context.add_entity((Age(42), RiskCategory::High)).unwrap();
347        let _ = context.add_entity((Age(42), RiskCategory::Low)).unwrap();
348        let _ = context.add_entity((Age(40), RiskCategory::Low)).unwrap();
349
350        context.with_query_results((Age(42), RiskCategory::High), &mut |people| {
351            assert_eq!(people.into_iter().count(), 1);
352        });
353    }
354
355    #[test]
356    fn with_query_results_intersection_one_indexed() {
357        let mut context = Context::new();
358        let _ = context.add_entity((Age(42), RiskCategory::High)).unwrap();
359        let _ = context.add_entity((Age(42), RiskCategory::Low)).unwrap();
360        let _ = context.add_entity((Age(40), RiskCategory::Low)).unwrap();
361
362        context.index_property::<Person, Age>();
363        context.with_query_results((Age(42), RiskCategory::High), &mut |people| {
364            assert_eq!(people.into_iter().count(), 1);
365        });
366    }
367
368    #[test]
369    fn query_derived_prop() {
370        let mut context = Context::new();
371        define_derived_property!(struct Senior(bool), Person, [Age], |age| Senior(age.0 >= 65));
372
373        let person = context.add_entity((Age(64), RiskCategory::High)).unwrap();
374        context.add_entity((Age(88), RiskCategory::High)).unwrap();
375
376        let mut not_seniors = Vec::new();
377        context.with_query_results((Senior(false),), &mut |people| {
378            not_seniors = people.to_owned_vec();
379        });
380        let mut seniors = Vec::new();
381        context.with_query_results((Senior(true),), &mut |people| {
382            seniors = people.to_owned_vec();
383        });
384        assert_eq!(seniors.len(), 1, "One senior");
385        assert_eq!(not_seniors.len(), 1, "One non-senior");
386
387        context.set_property(person, Age(65));
388
389        context.with_query_results((Senior(false),), &mut |people| {
390            not_seniors = people.to_owned_vec()
391        });
392        context.with_query_results((Senior(true),), &mut |people| {
393            seniors = people.to_owned_vec()
394        });
395
396        assert_eq!(seniors.len(), 2, "Two seniors");
397        assert_eq!(not_seniors.len(), 0, "No non-seniors");
398    }
399
400    #[test]
401    fn query_derived_prop_with_index() {
402        let mut context = Context::new();
403        define_derived_property!(struct Senior(bool), Person, [Age], |age| Senior(age.0 >= 65));
404
405        context.index_property::<Person, Senior>();
406        let person = context.add_entity((Age(64), RiskCategory::Low)).unwrap();
407        let _ = context.add_entity((Age(88), RiskCategory::Low));
408
409        let mut not_seniors = Vec::new();
410        context.with_query_results((Senior(false),), &mut |people| {
411            not_seniors = people.to_owned_vec()
412        });
413        let mut seniors = Vec::new();
414        context.with_query_results((Senior(true),), &mut |people| {
415            seniors = people.to_owned_vec()
416        });
417        assert_eq!(seniors.len(), 1, "One senior");
418        assert_eq!(not_seniors.len(), 1, "One non-senior");
419
420        context.set_property(person, Age(65));
421
422        context.with_query_results((Senior(false),), &mut |people| {
423            not_seniors = people.to_owned_vec()
424        });
425        context.with_query_results((Senior(true),), &mut |people| {
426            seniors = people.to_owned_vec()
427        });
428
429        assert_eq!(seniors.len(), 2, "Two seniors");
430        assert_eq!(not_seniors.len(), 0, "No non-seniors");
431    }
432
433    // create a multi-property index
434    define_multi_property!((Age, County, Height), Person);
435    define_multi_property!((County, Height), Person);
436
437    #[test]
438    fn query_derived_prop_with_optimized_index() {
439        let mut context = Context::new();
440        // create a 'regular' derived property
441        define_derived_property!(
442            struct Ach(u8, u32, u32),
443            Person,
444            [Age, County, Height],
445            [],
446            |age, county, height| Ach(age.0, county.0, height.0)
447        );
448
449        // add some people
450        let _ = context.add_entity((Age(64), County(2), Height(120), RiskCategory::Low));
451        let _ = context.add_entity((Age(88), County(2), Height(130), RiskCategory::Low));
452        let p2 = context
453            .add_entity((Age(8), County(1), Height(140), RiskCategory::Low))
454            .unwrap();
455        let p3 = context
456            .add_entity((Age(28), County(1), Height(140), RiskCategory::Low))
457            .unwrap();
458        let p4 = context
459            .add_entity((Age(28), County(2), Height(160), RiskCategory::Low))
460            .unwrap();
461        let p5 = context
462            .add_entity((Age(28), County(2), Height(160), RiskCategory::Low))
463            .unwrap();
464
465        // 'regular' derived property
466        context.with_query_results((Ach(28, 2, 160),), &mut |people| {
467            assert!(people.contains(p4));
468            assert!(people.contains(p5));
469            assert_eq!(people.into_iter().count(), 2, "Should have 2 matches");
470        });
471
472        // multi-property index
473        context.with_query_results((Age(28), County(2), Height(160)), &mut |people| {
474            assert!(people.contains(p4));
475            assert!(people.contains(p5));
476            assert_eq!(people.into_iter().count(), 2, "Should have 2 matches");
477        });
478
479        // multi-property index with different order
480        context.with_query_results((County(2), Height(160), Age(28)), &mut |people| {
481            assert!(people.contains(p4));
482            assert!(people.contains(p5));
483            assert_eq!(people.into_iter().count(), 2, "Should have 2 matches");
484        });
485
486        // multi-property index with different order
487        context.with_query_results((Height(160), County(2), Age(28)), &mut |people| {
488            assert!(people.contains(p4));
489            assert!(people.contains(p5));
490            assert_eq!(people.into_iter().count(), 2, "Should have 2 matches");
491        });
492
493        // multi-property index with different order and different value
494        context.with_query_results((Height(140), County(1), Age(28)), &mut |people| {
495            assert!(people.contains(p3));
496            assert_eq!(people.into_iter().count(), 1, "Should have 1 matches");
497        });
498
499        context.set_property(p2, Age(28));
500        // multi-property index again after changing the value
501        context.with_query_results((Height(140), County(1), Age(28)), &mut |people| {
502            assert!(people.contains(p2));
503            assert!(people.contains(p3));
504            assert_eq!(people.into_iter().count(), 2, "Should have 2 matches");
505        });
506
507        context.with_query_results((Height(140), County(1)), &mut |people| {
508            assert!(people.contains(p2));
509            assert!(people.contains(p3));
510            assert_eq!(people.into_iter().count(), 2, "Should have 2 matches");
511        });
512    }
513
514    #[test]
515    fn test_match_entity() {
516        let mut context = Context::new();
517        let person = context
518            .add_entity((Age(28), County(2), Height(160), RiskCategory::Low))
519            .unwrap();
520        assert!(context.match_entity(person, (Age(28), County(2), Height(160))));
521        assert!(!context.match_entity(person, (Age(13), County(2), Height(160))));
522        assert!(!context.match_entity(person, (Age(28), County(33), Height(160))));
523        assert!(!context.match_entity(person, (Age(28), County(2), Height(9))));
524    }
525
526    #[test]
527    fn filter_entities_for_unindexed_query() {
528        let mut context = Context::new();
529        let mut people = Vec::new();
530
531        for idx in 0..10 {
532            let person = context
533                .add_entity((Age(28), County(idx % 2), Height(160), RiskCategory::Low))
534                .unwrap();
535            people.push(person);
536        }
537
538        context.filter_entities(
539            &mut people,
540            (Age(28), County(0), Height(160), RiskCategory::Low),
541        );
542
543        let expected = (0..5)
544            .map(|idx| PersonId::new(idx * 2))
545            .collect::<Vec<PersonId>>();
546        assert_eq!(people, expected);
547    }
548
549    #[test]
550    fn filter_entities_for_indexed_query() {
551        let mut context = Context::new();
552        let mut people = Vec::new();
553
554        context.index_property::<Person, (Age, County)>();
555
556        for idx in 0..10 {
557            let person = context
558                .add_entity((Age(28), County(idx % 2), Height(160), RiskCategory::Low))
559                .unwrap();
560            people.push(person);
561        }
562
563        context.filter_entities(&mut people, (County(0), Age(28)));
564
565        let expected = (0..5)
566            .map(|idx| PersonId::new(idx * 2))
567            .collect::<Vec<PersonId>>();
568        assert_eq!(people, expected);
569    }
570
571    #[test]
572    fn entity_property_tuple_basic() {
573        use super::EntityPropertyTuple;
574
575        let mut context = Context::new();
576        let p1 = context.add_entity((Age(42), RiskCategory::High)).unwrap();
577        let _ = context.add_entity((Age(42), RiskCategory::Low)).unwrap();
578        let _ = context.add_entity((Age(30), RiskCategory::High)).unwrap();
579
580        // Create query using EntityPropertyTuple
581        let query: EntityPropertyTuple<Person, _> =
582            EntityPropertyTuple::new((Age(42), RiskCategory::High));
583
584        context.with_query_results(query, &mut |people| {
585            assert!(people.contains(p1));
586            assert_eq!(people.into_iter().count(), 1);
587        });
588
589        // Test match_entity
590        assert!(context.match_entity(p1, query));
591
592        // Test query_entity_count
593        assert_eq!(context.query_entity_count(query), 1);
594    }
595
596    #[test]
597    fn entity_property_tuple_empty_query() {
598        use super::EntityPropertyTuple;
599
600        let mut context = Context::new();
601        let _ = context.add_entity((Age(42), RiskCategory::High)).unwrap();
602        let _ = context.add_entity((Age(30), RiskCategory::Low)).unwrap();
603
604        // Empty query matches all entities
605        let query: EntityPropertyTuple<Person, _> = EntityPropertyTuple::new(());
606
607        assert_eq!(context.query_entity_count(query), 2);
608    }
609
610    #[test]
611    fn entity_property_tuple_singleton() {
612        use super::EntityPropertyTuple;
613
614        let mut context = Context::new();
615        let _ = context.add_entity((Age(42), RiskCategory::High)).unwrap();
616        let _ = context.add_entity((Age(42), RiskCategory::Low)).unwrap();
617        let _ = context.add_entity((Age(30), RiskCategory::High)).unwrap();
618
619        // Single property query
620        let query: EntityPropertyTuple<Person, _> = EntityPropertyTuple::new((Age(42),));
621
622        assert_eq!(context.query_entity_count(query), 2);
623    }
624
625    #[test]
626    fn entity_property_tuple_inner_access() {
627        use super::EntityPropertyTuple;
628
629        let query: EntityPropertyTuple<Person, _> =
630            EntityPropertyTuple::new((Age(42), RiskCategory::High));
631
632        // Test inner() accessor
633        let inner = query.inner();
634        assert_eq!(inner.0, Age(42));
635        assert_eq!(inner.1, RiskCategory::High);
636
637        // Test into_inner()
638        let (age, risk) = query.into_inner();
639        assert_eq!(age, Age(42));
640        assert_eq!(risk, RiskCategory::High);
641    }
642
643    #[test]
644    fn all_macro_no_properties() {
645        use crate::with;
646
647        let mut context = Context::new();
648        let _ = context.add_entity((Age(42), RiskCategory::High)).unwrap();
649        let _ = context.add_entity((Age(30), RiskCategory::Low)).unwrap();
650
651        // with!(Person) should match all Person entities
652        let query = with!(Person);
653        assert_eq!(context.query_entity_count(query), 2);
654    }
655
656    #[test]
657    fn all_macro_single_property() {
658        use crate::with;
659
660        let mut context = Context::new();
661        let _ = context.add_entity((Age(42), RiskCategory::High)).unwrap();
662        let _ = context.add_entity((Age(42), RiskCategory::Low)).unwrap();
663        let _ = context.add_entity((Age(30), RiskCategory::High)).unwrap();
664
665        // with!(Person, Age(42)) should match entities with Age = 42
666        let query = with!(Person, Age(42));
667        assert_eq!(context.query_entity_count(query), 2);
668    }
669
670    #[test]
671    fn all_macro_multiple_properties() {
672        use crate::with;
673
674        let mut context = Context::new();
675        let p1 = context.add_entity((Age(42), RiskCategory::High)).unwrap();
676        let _ = context.add_entity((Age(42), RiskCategory::Low)).unwrap();
677        let _ = context.add_entity((Age(30), RiskCategory::High)).unwrap();
678
679        // with!(Person, Age(42), RiskCategory::High) should match one entity
680        let query = with!(Person, Age(42), RiskCategory::High);
681        assert_eq!(context.query_entity_count(query), 1);
682
683        context.with_query_results(query, &mut |people| {
684            assert!(people.contains(p1));
685        });
686    }
687
688    #[test]
689    fn all_macro_with_trailing_comma() {
690        use crate::with;
691
692        let mut context = Context::new();
693        let _ = context.add_entity((Age(42), RiskCategory::High)).unwrap();
694
695        // Trailing comma should work
696        let query = with!(Person, Age(42));
697        assert_eq!(context.query_entity_count(query), 1);
698
699        let query = with!(Person, Age(42), RiskCategory::High);
700        assert_eq!(context.query_entity_count(query), 1);
701    }
702
703    #[test]
704    fn entity_property_tuple_as_property_list() {
705        use super::EntityPropertyTuple;
706        use crate::entity::property_list::PropertyList;
707
708        // Test validate
709        assert!(EntityPropertyTuple::<Person, (Age,)>::validate().is_ok());
710        assert!(EntityPropertyTuple::<Person, (Age, RiskCategory)>::validate().is_ok());
711
712        // Test contains_properties
713        assert!(EntityPropertyTuple::<Person, (Age,)>::contains_properties(
714            &[Age::type_id()]
715        ));
716        assert!(
717            EntityPropertyTuple::<Person, (Age, RiskCategory)>::contains_properties(&[
718                Age::type_id()
719            ])
720        );
721        assert!(
722            EntityPropertyTuple::<Person, (Age, RiskCategory)>::contains_properties(&[
723                Age::type_id(),
724                RiskCategory::type_id()
725            ])
726        );
727    }
728
729    #[test]
730    fn all_macro_as_property_list_for_add_entity() {
731        use crate::with;
732
733        let mut context = Context::new();
734
735        // Use with! macro result to add an entity
736        let props = with!(Person, Age(42), RiskCategory::High);
737        let person = context.add_entity(props).unwrap();
738
739        // Verify the entity was created with the correct properties
740        assert_eq!(context.get_property::<Person, Age>(person), Age(42));
741        assert_eq!(
742            context.get_property::<Person, RiskCategory>(person),
743            RiskCategory::High
744        );
745    }
746}