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