ixa/people/
query.rs

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