ixa/people/
query.rs

1use crate::hashing::{one_shot_128, HashMap};
2use crate::people::multi_property::{static_reorder_by_keys, type_ids_to_multi_property_id};
3use crate::{people::HashValueType, Context, ContextPeopleExt, PersonProperty};
4use seq_macro::seq;
5use std::any::TypeId;
6use std::sync::{Mutex, OnceLock};
7
8/// Encapsulates a person query.
9///
10/// [`Context::query_people`] actually takes an instance of [`Query`], but because
11/// we implement Query for tuples of up to size 20, that's invisible
12/// to the caller. Do not use this trait directly.
13pub trait Query: Copy + 'static {
14    fn setup(&self, context: &Context);
15    /// Returns a list of `(type_id, hash)` pairs where `hash` is the hash of a value of type
16    /// `Property::Value` and `type_id` is `Property.type_id()` (NOT the type ID of the value).
17    fn get_query(&self) -> Vec<(TypeId, HashValueType)>;
18
19    /// Returns an unordered list of type IDs of the properties in this query.
20    fn get_type_ids(&self) -> Vec<TypeId>;
21
22    /// Returns the `TypeId` of the multi-property having the properties of this query, if any.
23    fn multi_property_type_id(&self) -> Option<TypeId> {
24        // This trick allows us to cache the multi-property ID so we don't have to allocate every
25        // time.
26        static REGISTRY: OnceLock<Mutex<HashMap<TypeId, &'static Option<TypeId>>>> =
27            OnceLock::new();
28
29        let map = REGISTRY.get_or_init(|| Mutex::new(HashMap::default()));
30        let mut map = map.lock().unwrap();
31        let type_id = TypeId::of::<Self>();
32        let entry = *map.entry(type_id).or_insert_with(|| {
33            let mut types = self.get_type_ids();
34            types.sort_unstable();
35            Box::leak(Box::new(type_ids_to_multi_property_id(types.as_slice())))
36        });
37
38        *entry
39    }
40
41    fn multi_property_value_hash(&self) -> HashValueType;
42}
43
44impl Query for () {
45    fn setup(&self, _: &Context) {}
46
47    fn get_query(&self) -> Vec<(TypeId, HashValueType)> {
48        Vec::new()
49    }
50
51    fn get_type_ids(&self) -> Vec<TypeId> {
52        Vec::new()
53    }
54
55    fn multi_property_type_id(&self) -> Option<TypeId> {
56        None
57    }
58
59    fn multi_property_value_hash(&self) -> HashValueType {
60        let empty: &[u128] = &[];
61        one_shot_128(&empty)
62    }
63}
64
65// Implement the query version with one parameter.
66impl<T1: PersonProperty> Query for (T1, T1::Value) {
67    fn setup(&self, context: &Context) {
68        context.register_property::<T1>();
69    }
70
71    fn get_query(&self) -> Vec<(TypeId, HashValueType)> {
72        let value = T1::make_canonical(self.1);
73        vec![(T1::type_id(), T1::hash_property_value(&value))]
74    }
75
76    fn get_type_ids(&self) -> Vec<TypeId> {
77        vec![T1::type_id()]
78    }
79
80    fn multi_property_type_id(&self) -> Option<TypeId> {
81        // While not a "true" multi-property, it is convenient to have this method return the
82        // `TypeId` of the singleton property.
83        Some(T1::type_id())
84    }
85
86    fn multi_property_value_hash(&self) -> HashValueType {
87        T1::hash_property_value(&T1::make_canonical(self.1))
88    }
89}
90
91// Implement the query version with one parameter as a singleton tuple. We split this out from the
92// `impl_query` macro to avoid applying the `SortedTuple` machinery to such a simple case and so
93// that `multi_property_type_id()` can just return `Some(T1::type_id())`.
94impl<T1: PersonProperty> Query for ((T1, T1::Value),) {
95    fn setup(&self, context: &Context) {
96        context.register_property::<T1>();
97    }
98
99    fn get_query(&self) -> Vec<(TypeId, HashValueType)> {
100        let value = T1::make_canonical(self.0 .1);
101        vec![(T1::type_id(), T1::hash_property_value(&value))]
102    }
103
104    fn get_type_ids(&self) -> Vec<TypeId> {
105        vec![T1::type_id()]
106    }
107
108    fn multi_property_type_id(&self) -> Option<TypeId> {
109        // While not a "true" multi-property, it is convenient to have this method return the
110        // `TypeId` of the singleton property.
111        Some(T1::type_id())
112    }
113
114    fn multi_property_value_hash(&self) -> HashValueType {
115        T1::hash_property_value(&T1::make_canonical(self.0 .1))
116    }
117}
118
119macro_rules! impl_query {
120    ($ct:expr) => {
121        seq!(N in 0..$ct {
122            impl<
123                #(
124                    T~N : PersonProperty,
125                )*
126            > Query for (
127                #(
128                    (T~N, T~N::Value),
129                )*
130            )
131            {
132                fn setup(&self, context: &Context) {
133                    #(
134                        context.register_property::<T~N>();
135                    )*
136                }
137
138                fn get_query(&self) -> Vec<(TypeId, HashValueType)> {
139                    let mut ordered_items = vec![
140                    #(
141                        (T~N::type_id(), T~N::hash_property_value(&T~N::make_canonical(self.N.1))),
142                    )*
143                    ];
144                    ordered_items.sort_by(|a, b| a.0.cmp(&b.0));
145                    ordered_items
146                }
147
148                fn get_type_ids(&self) -> Vec<TypeId> {
149                    vec![
150                        #(
151                            T~N::type_id(),
152                        )*
153                    ]
154                }
155
156                fn multi_property_value_hash(&self) -> HashValueType {
157                    // This needs to be kept in sync with how multi-properties compute their hash. We are
158                    // exploiting the fact that `bincode` encodes tuples as the concatenation of their
159                    // elements. Unfortunately, `bincode` allocates, but we avoid more allocations by
160                    // using staticly allocated arrays.
161
162                    // Multi-properties order their values by lexicographic order of the component
163                    // properties, not `TypeId` order.
164                    // let type_ids: [TypeId; $ct] = [
165                    //     #(
166                    //         T~N::type_id(),
167                    //     )*
168                    // ];
169                    let keys: [&str; $ct] = [
170                        #(
171                            T~N::name(),
172                        )*
173                    ];
174                    // It is convenient to have the elements of the array to be `Copy` in the `static_apply_reordering`
175                    // function. Since references are trivially copyable, we construct `values` below to be an array
176                    // of _references_ to the `Vec`s returned from `encode_to_vec`. (The compiler is smart enough to
177                    // keep the referenced value in scope.)
178                    let mut values: [&Vec<u8>; $ct] = [
179                        #(
180                            &$crate::bincode::serde::encode_to_vec(self.N.1, bincode::config::standard()).unwrap(),
181                        )*
182                    ];
183                    static_reorder_by_keys(&keys, &mut values);
184
185                    let data = values.into_iter().flatten().copied().collect::<Vec<u8>>();
186                    one_shot_128(&data.as_slice())
187                }
188            }
189        });
190    }
191}
192
193// Implement the versions with 2..10 parameters. (The 1 case is implemented above.)
194seq!(Z in 2..10 {
195    impl_query!(Z);
196});
197
198#[cfg(test)]
199mod tests {
200    #![allow(dead_code)]
201    use crate::people::PeoplePlugin;
202    use crate::{
203        define_derived_property, define_multi_property, define_person_property, Context,
204        ContextPeopleExt, HashSetExt, PersonProperty,
205    };
206    use serde_derive::Serialize;
207
208    define_person_property!(Age, u8);
209    define_person_property!(County, u32);
210    define_person_property!(Height, u32);
211
212    #[derive(Serialize, Copy, Clone, PartialEq, Eq, Debug)]
213    pub enum RiskCategoryValue {
214        High,
215        Low,
216    }
217
218    define_person_property!(RiskCategory, RiskCategoryValue);
219
220    #[test]
221    fn with_query_results() {
222        let mut context = Context::new();
223        let _ = context
224            .add_person((RiskCategory, RiskCategoryValue::High))
225            .unwrap();
226
227        context.with_query_results((RiskCategory, RiskCategoryValue::High), &mut |people| {
228            assert_eq!(people.len(), 1);
229        });
230    }
231
232    #[test]
233    fn with_query_results_empty() {
234        let context = Context::new();
235
236        context.with_query_results((RiskCategory, RiskCategoryValue::High), &mut |people| {
237            assert_eq!(people.len(), 0);
238        });
239    }
240
241    #[test]
242    fn query_people_count() {
243        let mut context = Context::new();
244        let _ = context
245            .add_person((RiskCategory, RiskCategoryValue::High))
246            .unwrap();
247
248        assert_eq!(
249            context.query_people_count((RiskCategory, RiskCategoryValue::High)),
250            1
251        );
252    }
253
254    #[test]
255    fn query_people_count_empty() {
256        let context = Context::new();
257
258        assert_eq!(
259            context.query_people_count((RiskCategory, RiskCategoryValue::High)),
260            0
261        );
262    }
263
264    #[test]
265    fn with_query_results_macro_index_first() {
266        let mut context = Context::new();
267        let _ = context
268            .add_person((RiskCategory, RiskCategoryValue::High))
269            .unwrap();
270        context.index_property(RiskCategory);
271        assert!(is_property_indexed::<RiskCategory>(&context));
272
273        context.with_query_results((RiskCategory, RiskCategoryValue::High), &mut |people| {
274            assert_eq!(people.len(), 1);
275        });
276    }
277
278    fn is_property_indexed<T: PersonProperty>(context: &Context) -> bool {
279        let container = context.get_data(PeoplePlugin);
280        container
281            .property_indexes
282            .borrow()
283            .get(&T::type_id())
284            .map(|index| index.is_indexed())
285            .unwrap_or(false)
286    }
287
288    #[test]
289    fn with_query_results_macro_index_second() {
290        let mut context = Context::new();
291        let _ = context.add_person((RiskCategory, RiskCategoryValue::High));
292
293        context.with_query_results((RiskCategory, RiskCategoryValue::High), &mut |people| {
294            assert_eq!(people.len(), 1);
295        });
296        assert!(!is_property_indexed::<RiskCategory>(&context));
297
298        context.index_property(RiskCategory);
299        assert!(is_property_indexed::<RiskCategory>(&context));
300
301        context.with_query_results((RiskCategory, RiskCategoryValue::High), &mut |people| {
302            assert_eq!(people.len(), 1);
303        });
304    }
305
306    #[test]
307    fn with_query_results_macro_change() {
308        let mut context = Context::new();
309        let person1 = context
310            .add_person((RiskCategory, RiskCategoryValue::High))
311            .unwrap();
312
313        context.with_query_results((RiskCategory, RiskCategoryValue::High), &mut |people| {
314            assert_eq!(people.len(), 1);
315        });
316
317        context.with_query_results((RiskCategory, RiskCategoryValue::Low), &mut |people| {
318            assert_eq!(people.len(), 0);
319        });
320
321        context.set_person_property(person1, RiskCategory, RiskCategoryValue::Low);
322        context.with_query_results((RiskCategory, RiskCategoryValue::High), &mut |people| {
323            assert_eq!(people.len(), 0);
324        });
325
326        context.with_query_results((RiskCategory, RiskCategoryValue::Low), &mut |people| {
327            assert_eq!(people.len(), 1);
328        });
329    }
330
331    #[test]
332    fn with_query_results_index_after_add() {
333        let mut context = Context::new();
334        let _ = context
335            .add_person((RiskCategory, RiskCategoryValue::High))
336            .unwrap();
337        context.index_property(RiskCategory);
338        assert!(is_property_indexed::<RiskCategory>(&context));
339        context.with_query_results((RiskCategory, RiskCategoryValue::High), &mut |people| {
340            assert_eq!(people.len(), 1);
341        });
342    }
343
344    #[test]
345    fn with_query_results_add_after_index() {
346        let mut context = Context::new();
347        let _ = context
348            .add_person((RiskCategory, RiskCategoryValue::High))
349            .unwrap();
350        context.index_property(RiskCategory);
351        assert!(is_property_indexed::<RiskCategory>(&context));
352        context.with_query_results((RiskCategory, RiskCategoryValue::High), &mut |people| {
353            assert_eq!(people.len(), 1);
354        });
355
356        let _ = context
357            .add_person((RiskCategory, RiskCategoryValue::High))
358            .unwrap();
359        context.with_query_results((RiskCategory, RiskCategoryValue::High), &mut |people| {
360            assert_eq!(people.len(), 2);
361        });
362    }
363
364    #[test]
365    // This is safe because we reindex only when someone queries.
366    fn add_after_index_without_query() {
367        let mut context = Context::new();
368        let _ = context.add_person(()).unwrap();
369        context.index_property(RiskCategory);
370    }
371
372    #[test]
373    #[should_panic(expected = "Property not initialized")]
374    // This will panic when we query.
375    fn with_query_results_add_after_index_panic() {
376        let mut context = Context::new();
377        context.add_person(()).unwrap();
378        context.index_property(RiskCategory);
379        context.with_query_results((RiskCategory, RiskCategoryValue::High), &mut |_people| {});
380    }
381
382    #[test]
383    fn with_query_results_cast_value() {
384        let mut context = Context::new();
385        let _ = context.add_person((Age, 42)).unwrap();
386
387        // Age is a u8, by default integer literals are i32; the macro should cast it.
388        context.with_query_results((Age, 42), &mut |people| {
389            assert_eq!(people.len(), 1);
390        });
391    }
392
393    #[test]
394    fn with_query_results_intersection() {
395        let mut context = Context::new();
396        let _ = context
397            .add_person(((Age, 42), (RiskCategory, RiskCategoryValue::High)))
398            .unwrap();
399        let _ = context
400            .add_person(((Age, 42), (RiskCategory, RiskCategoryValue::Low)))
401            .unwrap();
402        let _ = context
403            .add_person(((Age, 40), (RiskCategory, RiskCategoryValue::Low)))
404            .unwrap();
405
406        context.with_query_results(
407            ((Age, 42), (RiskCategory, RiskCategoryValue::High)),
408            &mut |people| {
409                assert_eq!(people.len(), 1);
410            },
411        );
412    }
413
414    #[test]
415    fn with_query_results_intersection_non_macro() {
416        let mut context = Context::new();
417        let _ = context
418            .add_person(((Age, 42), (RiskCategory, RiskCategoryValue::High)))
419            .unwrap();
420        let _ = context
421            .add_person(((Age, 42), (RiskCategory, RiskCategoryValue::Low)))
422            .unwrap();
423        let _ = context
424            .add_person(((Age, 40), (RiskCategory, RiskCategoryValue::Low)))
425            .unwrap();
426
427        context.with_query_results(
428            ((Age, 42), (RiskCategory, RiskCategoryValue::High)),
429            &mut |people| {
430                assert_eq!(people.len(), 1);
431            },
432        );
433    }
434
435    #[test]
436    fn with_query_results_intersection_one_indexed() {
437        let mut context = Context::new();
438        let _ = context
439            .add_person(((Age, 42), (RiskCategory, RiskCategoryValue::High)))
440            .unwrap();
441        let _ = context
442            .add_person(((Age, 42), (RiskCategory, RiskCategoryValue::Low)))
443            .unwrap();
444        let _ = context
445            .add_person(((Age, 40), (RiskCategory, RiskCategoryValue::Low)))
446            .unwrap();
447
448        context.index_property(Age);
449        context.with_query_results(
450            ((Age, 42), (RiskCategory, RiskCategoryValue::High)),
451            &mut |people| {
452                assert_eq!(people.len(), 1);
453            },
454        );
455    }
456
457    #[test]
458    fn query_derived_prop() {
459        let mut context = Context::new();
460        define_derived_property!(Senior, bool, [Age], |age| age >= 65);
461
462        let person = context.add_person((Age, 64)).unwrap();
463        let _ = context.add_person((Age, 88));
464
465        let mut not_seniors = Vec::new();
466        context.with_query_results((Senior, false), &mut |people| {
467            not_seniors = people.to_owned_vec()
468        });
469        let mut seniors = Vec::new();
470        context.with_query_results((Senior, true), &mut |people| {
471            seniors = people.to_owned_vec();
472        });
473        assert_eq!(seniors.len(), 1, "One senior");
474        assert_eq!(not_seniors.len(), 1, "One non-senior");
475
476        context.set_person_property(person, Age, 65);
477
478        context.with_query_results((Senior, false), &mut |people| {
479            not_seniors = people.to_owned_vec()
480        });
481        context.with_query_results((Senior, true), &mut |people| {
482            seniors = people.to_owned_vec()
483        });
484
485        assert_eq!(seniors.len(), 2, "Two seniors");
486        assert_eq!(not_seniors.len(), 0, "No non-seniors");
487    }
488
489    #[test]
490    fn query_derived_prop_with_index() {
491        let mut context = Context::new();
492        define_derived_property!(Senior, bool, [Age], |age| age >= 65);
493
494        context.index_property(Senior);
495        let person = context.add_person((Age, 64)).unwrap();
496        let _ = context.add_person((Age, 88));
497
498        // Age is a u8, by default integer literals are i32; the macro should cast it.
499        let mut not_seniors = Vec::new();
500        context.with_query_results((Senior, false), &mut |people| {
501            not_seniors = people.to_owned_vec()
502        });
503        let mut seniors = Vec::new();
504        context.with_query_results((Senior, true), &mut |people| {
505            seniors = people.to_owned_vec()
506        });
507        assert_eq!(seniors.len(), 1, "One senior");
508        assert_eq!(not_seniors.len(), 1, "One non-senior");
509
510        context.set_person_property(person, Age, 65);
511
512        context.with_query_results((Senior, false), &mut |people| {
513            not_seniors = people.to_owned_vec()
514        });
515        context.with_query_results((Senior, true), &mut |people| {
516            seniors = people.to_owned_vec()
517        });
518
519        assert_eq!(seniors.len(), 2, "Two seniors");
520        assert_eq!(not_seniors.len(), 0, "No non-seniors");
521    }
522
523    // create a multi-property index
524    define_multi_property!(ACH, (Age, County, Height));
525    define_multi_property!(CH, (County, Height));
526
527    #[test]
528    fn query_derived_prop_with_optimized_index() {
529        let mut context = Context::new();
530        // create a 'regular' derived property
531        define_derived_property!(
532            Ach,
533            (u8, u32, u32),
534            [Age, County, Height],
535            |age, county, height| { (age, county, height) }
536        );
537
538        // add some people
539        let _ = context.add_person(((Age, 64), (County, 2), (Height, 120)));
540        let _ = context.add_person(((Age, 88), (County, 2), (Height, 130)));
541        let p2 = context
542            .add_person(((Age, 8), (County, 1), (Height, 140)))
543            .unwrap();
544        let p3 = context
545            .add_person(((Age, 28), (County, 1), (Height, 140)))
546            .unwrap();
547        let p4 = context
548            .add_person(((Age, 28), (County, 2), (Height, 160)))
549            .unwrap();
550        let p5 = context
551            .add_person(((Age, 28), (County, 2), (Height, 160)))
552            .unwrap();
553
554        // 'regular' derived property
555        context.with_query_results((Ach, (28, 2, 160)), &mut |people| {
556            assert_eq!(people.len(), 2, "Should have 2 matches");
557            assert!(people.contains(&p4));
558            assert!(people.contains(&p5));
559        });
560
561        // multi-property index
562        context.with_query_results(((Age, 28), (County, 2), (Height, 160)), &mut |people| {
563            assert_eq!(people.len(), 2, "Should have 2 matches");
564            assert!(people.contains(&p4));
565            assert!(people.contains(&p5));
566        });
567
568        // multi-property index with different order
569        context.with_query_results(((County, 2), (Height, 160), (Age, 28)), &mut |people| {
570            assert_eq!(people.len(), 2, "Should have 2 matches");
571            assert!(people.contains(&p4));
572            assert!(people.contains(&p5));
573        });
574
575        // multi-property index with different order
576        context.with_query_results(((Height, 160), (County, 2), (Age, 28)), &mut |people| {
577            assert_eq!(people.len(), 2, "Should have 2 matches");
578            assert!(people.contains(&p4));
579            assert!(people.contains(&p5));
580        });
581
582        // multi-property index with different order and different value
583        context.with_query_results(((Height, 140), (County, 1), (Age, 28)), &mut |people| {
584            assert_eq!(people.len(), 1, "Should have 1 matches");
585            assert!(people.contains(&p3));
586        });
587
588        context.set_person_property(p2, Age, 28);
589        // multi-property index again after changing the value
590        context.with_query_results(((Height, 140), (County, 1), (Age, 28)), &mut |people| {
591            assert_eq!(people.len(), 2, "Should have 2 matches");
592            assert!(people.contains(&p2));
593            assert!(people.contains(&p3));
594        });
595
596        context.with_query_results(((Height, 140), (County, 1)), &mut |people| {
597            assert_eq!(people.len(), 2, "Should have 2 matches");
598            assert!(people.contains(&p2));
599            assert!(people.contains(&p3));
600        });
601    }
602}