ixa/entity/
property_store.rs

1/*!
2
3A [`PropertyStore`] implements the registry pattern for property value stores: A [`PropertyStore`]
4wraps a vector of `PropertyValueStore`s, one for each concrete property type. The implementor
5of [`crate::entity::property::Property`] is the value type. Since there's a 1-1 correspondence between property types
6and their value stores, we implement the `index` method for each property type to make
7property lookup fast. The [`PropertyStore`] stores a list of all properties in the form of
8boxed `PropertyValueStore` instances, which provide a type-erased interface to the backing
9storage (including index) of the property. Storage is only allocated as-needed, so the
10instantiation of a `PropertyValueStore` for a property that is never used is negligible.
11There's no need, then, for lazy initialization of the `PropertyValueStore`s themselves.
12
13This module also implements the initialization of "static" data associated with a property,
14that is, data that is the same across all [`crate::context::Context`] instances, which is computed before `main()`
15using `ctor` magic. (Each property implements a ctor that calls [`add_to_property_registry()`].)
16For simplicity, a property's ctor implementation, supplied by a macro, just calls
17`add_to_property_registry<E: Entity, P: Property<E>>()`, which does all the work. The
18`add_to_property_registry` function adds the following metadata to global metadata stores:
19
20Metadata stored on `PROPERTY_METADATA`, which for each property stores:
21- a list of dependent (derived) properties, and
22- a constructor function to create a new `PropertyValueStore` instance for the property.
23
24Metadata stored on `ENTITY_METADATA`, which for each entity stores:
25- a list of properties associated with the entity, and
26- a list of _required_ properties for the entity. These are properties for
27  which values must be supplied to `add_entity` when creating a new entity.
28
29*/
30
31use std::any::{Any, TypeId};
32use std::collections::HashMap;
33use std::sync::atomic::{AtomicUsize, Ordering};
34use std::sync::{LazyLock, Mutex, OnceLock};
35
36use crate::entity::entity::Entity;
37use crate::entity::entity_store::register_property_with_entity;
38use crate::entity::events::PartialPropertyChangeEventBox;
39use crate::entity::index::{IndexCountResult, IndexSetResult};
40use crate::entity::property::Property;
41use crate::entity::property_list::PropertyList;
42use crate::entity::property_value_store::PropertyValueStore;
43use crate::entity::property_value_store_core::PropertyValueStoreCore;
44use crate::entity::value_change_counter::StratifiedValueChangeCounter;
45use crate::entity::{EntityId, PropertyIndexType};
46use crate::Context;
47
48/// A map from Entity ID to a count of the properties already associated with the entity. The value for the key is
49/// equivalent to the next property ID that will be assigned to the next property that requests an ID. Each `Entity`
50/// type has its own series of increasing property IDs.
51///
52/// Note: The mechanism to assign property IDs needs to be distinct from the rest of property registration, because
53/// properties often need to have an ID assigned _before_ its registration proper so that it can be recorded as a
54/// dependency of some other property.
55static NEXT_PROPERTY_ID: LazyLock<Mutex<HashMap<usize, usize>>> =
56    LazyLock::new(|| Mutex::new(HashMap::default()));
57
58/// A container struct to hold the (global) metadata for a single property.
59///
60/// At program startup (before `main()`, using ctors) we compute metadata for all properties
61/// that are linked into the binary, and this data remains unchanged for the life of the program.
62#[derive(Default)]
63pub(super) struct PropertyMetadata<E: Entity> {
64    /// The (derived) properties that depend on this property, as represented by their
65    /// `Property::index` value. This list is used to update the index (if applicable)
66    /// and emit change events for these properties when this property changes.
67    pub dependents: Vec<usize>,
68    /// A function that constructs a new `PropertyValueStoreCore<E, P>` instance in a type-erased
69    /// way, used in the constructor of `PropertyStore`. This is an `Option` because this
70    /// function pointer is recorded possibly out-of-order from when the `PropertyMetadata`
71    /// instance for this property needs to exist (when its dependents are recorded).
72    #[allow(clippy::type_complexity)]
73    pub value_store_constructor: Option<fn() -> Box<dyn PropertyValueStore<E>>>,
74}
75
76/// This maps `(entity_type_id, property_type_index)` to `PropertyMetadata<E>`, which holds a vector of dependents (as IDs)
77/// and a function pointer to the constructor that constucts a `PropertyValueStoreCore<E, P>` type erased as
78/// a `Box<dyn PropertyValueStore<E>>`. This data is actually written by the property `ctor`s with a call to [`crate::entity::entity_store::register_property_with_entity`()].
79#[allow(clippy::type_complexity)]
80static PROPERTY_METADATA_BUILDER: LazyLock<
81    Mutex<HashMap<(usize, usize), Box<dyn Any + Send + Sync>>>,
82> = LazyLock::new(|| Mutex::new(HashMap::default()));
83
84/// The frozen property metadata registry, created exactly once on first read.
85///
86/// This is derived from `PROPERTY_METADATA_BUILDER` by moving the builder `HashMap` out. After this point,
87/// registration is no longer allowed.
88#[allow(clippy::type_complexity)]
89static PROPERTY_METADATA: OnceLock<HashMap<(usize, usize), Box<dyn Any + Send + Sync>>> =
90    OnceLock::new();
91
92/// Private helper to fetch or initialize the frozen metadata.
93fn property_metadata() -> &'static HashMap<(usize, usize), Box<dyn Any + Send + Sync>> {
94    PROPERTY_METADATA.get_or_init(|| {
95        let mut builder = PROPERTY_METADATA_BUILDER.lock().unwrap();
96        std::mem::take(&mut *builder)
97    })
98}
99
100/// The public getter for the dependents of a property with index `property_index` (as stored in
101/// `PROPERTY_METADATA`). The `Property<E: Entity>::dependents()` method defers to this.
102///
103/// This function should only be called once `main()` starts, that is, not in `ctors` constructors,
104/// as it assumes `PROPERTY_METADATA` has been correctly initialized. Hence, the "static" suffix.
105#[must_use]
106pub(super) fn get_property_dependents_static<E: Entity>(property_index: usize) -> &'static [usize] {
107    let map = property_metadata();
108    let property_metadata = map
109        .get(&(E::id(), property_index))
110                               .unwrap_or_else(|| panic!("No registered property found with index = {property_index:?}. You must use the `define_property!` macro to create a registered property."));
111    let property_metadata: &PropertyMetadata<E> = property_metadata.downcast_ref().unwrap_or_else(
112        || panic!(
113            "Property type at index {:?} does not match registered property type. You must use the `define_property!` macro to create a registered property.",
114            property_index
115        )
116    );
117
118    property_metadata.dependents.as_slice()
119}
120
121/// Adds a new item to the registry. The job of this method is to create whatever "singleton"
122/// data/metadata is associated with the [`crate::entity::property::Property`] if it doesn't already exist. In
123/// our use case, this method is called in the `ctor` function of each `Property<E>` type.
124pub fn add_to_property_registry<E: Entity, P: Property<E>>() {
125    // Ensure the ID of the property type is initialized.
126    let property_index = P::id();
127
128    // Registers the property with the entity type.
129    register_property_with_entity(
130        <E as Entity>::type_id(),
131        <P as Property<E>>::type_id(),
132        P::is_required(),
133    );
134
135    let mut property_metadata = PROPERTY_METADATA_BUILDER.lock().unwrap();
136    if PROPERTY_METADATA.get().is_some() {
137        panic!(
138            "`add_to_property_registry()` called after property metadata was frozen; registration must occur during startup/ctors."
139        );
140    }
141
142    // Register the `PropertyValueStoreCore<E, P>` constructor.
143    {
144        let metadata = property_metadata
145            .entry((E::id(), property_index))
146            .or_insert_with(|| Box::new(PropertyMetadata::<E>::default()));
147        let metadata: &mut PropertyMetadata<E> = metadata.downcast_mut().unwrap();
148        metadata
149            .value_store_constructor
150            .get_or_insert(PropertyValueStoreCore::<E, P>::new_boxed);
151    }
152
153    // Construct the dependency graph
154    for dependency in P::non_derived_dependencies() {
155        // Add `property_index` as a dependent of the dependency
156        let dependency_meta = property_metadata
157            .entry((E::id(), dependency))
158            .or_insert_with(|| Box::new(PropertyMetadata::<E>::default()));
159        let dependency_meta: &mut PropertyMetadata<E> = dependency_meta.downcast_mut().unwrap();
160        dependency_meta.dependents.push(property_index);
161    }
162}
163
164/// A convenience getter for `NEXT_ENTITY_INDEX`.
165pub fn get_registered_property_count<E: Entity>() -> usize {
166    let map = NEXT_PROPERTY_ID.lock().unwrap();
167    *map.get(&E::id()).unwrap_or(&0)
168}
169
170/// Encapsulates the synchronization logic for initializing an item's index.
171///
172/// Acquires a global lock on the next available property ID, but only increments
173/// it if we successfully initialize the provided ID. The ID of a property is
174/// assigned at runtime but only once per type. It's possible for a single
175/// type to attempt to initialize its index multiple times from different threads,
176/// which is why all this synchronization is required. However, the overhead
177/// is negligible, as this initialization only happens once upon first access.
178///
179/// In fact, for our use case we know we are calling this function
180/// once for each type in each `Property`'s `ctor` function, which
181/// should be the only time this method is ever called for the type.
182pub fn initialize_property_id<E: Entity>(property_id: &AtomicUsize) -> usize {
183    // Acquire a global lock.
184    let mut guard = NEXT_PROPERTY_ID.lock().unwrap();
185    let candidate = guard.entry(E::id()).or_insert_with(|| 0);
186
187    // Try to claim the candidate index. Here we guard against the potential race condition that
188    // another instance of this plugin in another thread just initialized the index prior to us
189    // obtaining the lock. If the index has been initialized beneath us, we do not update
190    // NEXT_PROPERTY_INDEX, we just return the value `index` was initialized to.
191    // For a justification of the data ordering, see:
192    //     https://github.com/CDCgov/ixa/pull/477#discussion_r2244302872
193    match property_id.compare_exchange(usize::MAX, *candidate, Ordering::AcqRel, Ordering::Acquire)
194    {
195        Ok(_) => {
196            // We won the race — increment the global next plugin index and return the new index
197            *candidate += 1;
198            *candidate - 1
199        }
200        Err(existing) => {
201            // Another thread beat us — don’t increment the global next plugin index,
202            // just return existing
203            existing
204        }
205    }
206}
207
208/// A wrapper around a vector of property value stores.
209pub struct PropertyStore<E: Entity> {
210    /// A vector of `Box<PropertyValueStoreCore<E, P>>`, type-erased to `Box<dyn PropertyValueStore<E>>`
211    items: Vec<Box<dyn PropertyValueStore<E>>>,
212}
213
214impl<E: Entity> Default for PropertyStore<E> {
215    fn default() -> Self {
216        PropertyStore::new()
217    }
218}
219
220impl<E: Entity> PropertyStore<E> {
221    /// Creates a new [`PropertyStore`].
222    pub fn new() -> Self {
223        let num_items = get_registered_property_count::<E>();
224        // The constructors for each `PropertyValueStoreCore<E, P>` are stored in the `PROPERTY_METADATA` global.
225        let property_metadata = property_metadata();
226
227        // We construct the correct concrete `PropertyValueStoreCore<E, P>` value for each index (=`P::index()`).
228        let items = (0..num_items)
229            .map(|idx| {
230                let metadata = property_metadata
231                    .get(&(E::id(), idx))
232                    .unwrap_or_else(|| panic!("No property metadata entry for index {idx}"))
233                    .downcast_ref::<PropertyMetadata<E>>()
234                    .unwrap_or_else(|| {
235                        panic!(
236                            "Property metadata entry for index {idx} does not match expected type"
237                        )
238                    });
239                let constructor = metadata
240                    .value_store_constructor
241                    .unwrap_or_else(|| panic!("No PropertyValueStore constructor for index {idx}"));
242                constructor()
243            })
244            .collect();
245
246        Self { items }
247    }
248
249    /// Fetches an immutable reference to the `PropertyValueStoreCore<E, P>`.
250    #[must_use]
251    pub fn get<P: Property<E>>(&self) -> &PropertyValueStoreCore<E, P> {
252        let index = P::id();
253        let property_value_store =
254            self.items
255                .get(index)
256                .unwrap_or_else(||
257                    panic!(
258                        "No registered property found with index = {:?} while trying to get property {}. You must use the `define_property!` macro to create a registered property.",
259                        index,
260                        P::name()
261                    )
262                );
263        let property_value_store: &PropertyValueStoreCore<E, P> = property_value_store
264            .as_any()
265            .downcast_ref::<PropertyValueStoreCore<E, P>>()
266            .unwrap_or_else(||
267                {
268                    panic!(
269                        "Property type at index {:?} does not match registered property type. Found type_id {:?} while getting type_id {:?}. You must use the `define_property!` macro to create a registered property.",
270                        index,
271                        (**property_value_store).type_id(),
272                        TypeId::of::<PropertyValueStoreCore<E, P>>()
273                    )
274                }
275            );
276        property_value_store
277    }
278
279    /// Fetches a mutable reference to the `PropertyValueStoreCore<E, P>`.
280    #[must_use]
281    pub fn get_mut<P: Property<E>>(&mut self) -> &mut PropertyValueStoreCore<E, P> {
282        let index = P::id();
283        let property_value_store =
284            self.items
285                .get_mut(index)
286                .unwrap_or_else(||
287                    panic!(
288                        "No registered property found with index = {:?} while trying to get property {}. You must use the `define_property!` macro to create a registered property.",
289                        index,
290                        P::name()
291                    )
292                );
293        let type_id = (**property_value_store).type_id(); // Only used for error message if error occurs.
294        let property_value_store: &mut PropertyValueStoreCore<E, P> = property_value_store
295            .as_any_mut()
296            .downcast_mut::<PropertyValueStoreCore<E, P>>()
297            .unwrap_or_else(||
298                {
299                    panic!(
300                        "Property type at index {:?} does not match registered property type. Found type_id {:?} while getting type_id {:?}. You must use the `define_property!` macro to create a registered property.",
301                        index,
302                        type_id,
303                        TypeId::of::<PropertyValueStoreCore<E, P>>()
304                    )
305                }
306            );
307        property_value_store
308    }
309
310    /// Creates a `PartialPropertyChangeEvent` instance for the `entity_id` and `property_index`. This method is only
311    /// called for derived dependents of some property that has changed (one of `P`'s non-derived dependencies).
312    pub(crate) fn create_partial_property_change(
313        &self,
314        property_index: usize,
315        entity_id: EntityId<E>,
316        context: &Context,
317    ) -> PartialPropertyChangeEventBox {
318        let property_value_store = self.items
319                                       .get(property_index)
320            .unwrap_or_else(|| panic!("No registered property found with index = {property_index:?}. You must use the `define_property!` macro to create a registered property."));
321
322        property_value_store.create_partial_property_change(entity_id, context)
323    }
324
325    /// Returns whether the property with `property_index` needs partial change-event processing.
326    pub(crate) fn should_create_partial_property_change(
327        &self,
328        property_index: usize,
329        context: &Context,
330    ) -> bool {
331        let property_value_store = self.items
332                                       .get(property_index)
333            .unwrap_or_else(|| panic!("No registered property found with index = {property_index:?}. You must use the `define_property!` macro to create a registered property."));
334
335        property_value_store.should_create_partial_change(context)
336    }
337
338    /// Returns whether or not the property `P` is indexed.
339    ///
340    /// This method can return `true` even if `context.index_property::<P>()` has never been called. For example,
341    /// if a multi-property is indexed, all equivalent multi-properties are automatically also indexed, as they
342    /// share a single index.
343    #[cfg(test)]
344    pub fn is_property_indexed<P: Property<E>>(&self) -> bool {
345        self.items
346            .get(P::index_id())
347            .unwrap_or_else(|| panic!("No registered property {} found with index = {:?}. You must use the `define_property!` macro to create a registered property.", P::name(), P::index_id()))
348            .index_type()
349            != PropertyIndexType::Unindexed
350    }
351
352    /// Sets the index type for `P`. Passing `PropertyIndexType::Unindexed` removes any existing index for `P`.
353    ///
354    /// Note that the index might not live in the `PropertyValueStore` associated with `P` itself, as in the case
355    /// of multi-properties which share a single index among all equivalent multi-properties.
356    pub fn set_property_indexed<P: Property<E>>(&mut self, index_type: PropertyIndexType) {
357        let property_value_store = self.items
358            .get_mut(P::index_id())
359            .unwrap_or_else(|| panic!("No registered property {} found with index = {:?}. You must use the `define_property!` macro to create a registered property.", P::name(), P::index_id()));
360        property_value_store.set_indexed(index_type);
361    }
362
363    /// Creates a stratified value change counter for tracked property `P` with strata `PL`.
364    ///
365    /// Returns the counter ID.
366    pub fn create_value_change_counter<PL, P>(&mut self) -> usize
367    where
368        PL: PropertyList<E> + Eq + std::hash::Hash,
369        P: Property<E> + Eq + std::hash::Hash,
370    {
371        let property_value_store = self.get_mut::<P>();
372        property_value_store.add_value_change_counter(Box::new(StratifiedValueChangeCounter::<
373            E,
374            PL,
375            P,
376        >::new()))
377    }
378
379    /// Updates the index of the property having the given ID for any entities that have been added to the context
380    /// since the last time the index was updated.
381    pub fn index_unindexed_entities_for_property_id(
382        &mut self,
383        context: &Context,
384        property_id: usize,
385    ) {
386        self.items[property_id].index_unindexed_entities(context)
387    }
388
389    /// Updates all indexed properties for any entities that have been added since the last update.
390    pub fn index_unindexed_entities_for_all_properties(&mut self, context: &Context) {
391        for store in &mut self.items {
392            store.index_unindexed_entities(context);
393        }
394    }
395
396    pub fn get_index_set_for_query_parts(
397        &self,
398        property_id: usize,
399        query_parts: &[&dyn Any],
400    ) -> IndexSetResult<'_, E> {
401        self.items[property_id].get_index_set_for_query_parts(query_parts)
402    }
403
404    pub fn get_index_count_for_query_parts(
405        &self,
406        property_id: usize,
407        query_parts: &[&dyn Any],
408    ) -> IndexCountResult {
409        self.items[property_id].get_index_count_for_query_parts(query_parts)
410    }
411}
412
413#[cfg(test)]
414mod tests {
415    #![allow(dead_code)]
416    use std::any::Any;
417
418    use super::*;
419    use crate::entity::index::{IndexCountResult, IndexSetResult};
420    use crate::prelude::*;
421    use crate::{define_entity, define_property, with, Context};
422
423    define_entity!(Person);
424
425    define_property!(struct Age(u8), Person);
426    define_property!(
427        enum InfectionStatus {
428            Susceptible,
429            Infected,
430            Recovered,
431        },
432        Person,
433        default_const = InfectionStatus::Susceptible
434    );
435    define_property!(struct Vaccinated(bool), Person, default_const = Vaccinated(false));
436
437    #[test]
438    fn test_get_property_store() {
439        let mut property_store = PropertyStore::new();
440
441        {
442            let ages: &mut PropertyValueStoreCore<_, Age> = property_store.get_mut();
443            ages.set(EntityId::<Person>::new(0), Age(12));
444            ages.set(EntityId::<Person>::new(1), Age(33));
445            ages.set(EntityId::<Person>::new(2), Age(44));
446
447            let infection_statuses: &mut PropertyValueStoreCore<_, InfectionStatus> =
448                property_store.get_mut();
449            infection_statuses.set(EntityId::<Person>::new(0), InfectionStatus::Susceptible);
450            infection_statuses.set(EntityId::<Person>::new(1), InfectionStatus::Susceptible);
451            infection_statuses.set(EntityId::<Person>::new(2), InfectionStatus::Infected);
452
453            let vaccine_status: &mut PropertyValueStoreCore<_, Vaccinated> =
454                property_store.get_mut();
455            vaccine_status.set(EntityId::<Person>::new(0), Vaccinated(true));
456            vaccine_status.set(EntityId::<Person>::new(1), Vaccinated(false));
457            vaccine_status.set(EntityId::<Person>::new(2), Vaccinated(true));
458        }
459
460        // Verify that `get` returns the expected values
461        {
462            let ages: &PropertyValueStoreCore<_, Age> = property_store.get();
463            assert_eq!(ages.get(EntityId::<Person>::new(0)), Age(12));
464            assert_eq!(ages.get(EntityId::<Person>::new(1)), Age(33));
465            assert_eq!(ages.get(EntityId::<Person>::new(2)), Age(44));
466
467            let infection_statuses: &PropertyValueStoreCore<_, InfectionStatus> =
468                property_store.get();
469            assert_eq!(
470                infection_statuses.get(EntityId::<Person>::new(0)),
471                InfectionStatus::Susceptible
472            );
473            assert_eq!(
474                infection_statuses.get(EntityId::<Person>::new(1)),
475                InfectionStatus::Susceptible
476            );
477            assert_eq!(
478                infection_statuses.get(EntityId::<Person>::new(2)),
479                InfectionStatus::Infected
480            );
481
482            let vaccine_status: &PropertyValueStoreCore<_, Vaccinated> = property_store.get();
483            assert_eq!(
484                vaccine_status.get(EntityId::<Person>::new(0)),
485                Vaccinated(true)
486            );
487            assert_eq!(
488                vaccine_status.get(EntityId::<Person>::new(1)),
489                Vaccinated(false)
490            );
491            assert_eq!(
492                vaccine_status.get(EntityId::<Person>::new(2)),
493                Vaccinated(true)
494            );
495        }
496    }
497
498    #[test]
499    fn test_index_query_results_for_property_store() {
500        let mut context = Context::new();
501        context.index_property::<Person, Age>();
502
503        let existing_value = Age(12);
504        let missing_value = Age(99);
505        let existing_query_parts = [&existing_value as &dyn Any];
506        let missing_query_parts = [&missing_value as &dyn Any];
507
508        let _ = context.add_entity(with!(Person, existing_value)).unwrap();
509        let _ = context.add_entity(with!(Person, existing_value)).unwrap();
510
511        let property_store = context.entity_store.get_property_store::<Person>();
512
513        // FullIndex + count
514        assert_eq!(
515            property_store.get_index_count_for_query_parts(Age::index_id(), &missing_query_parts,),
516            IndexCountResult::Count(0)
517        );
518        assert_eq!(
519            property_store.get_index_count_for_query_parts(Age::index_id(), &existing_query_parts,),
520            IndexCountResult::Count(2)
521        );
522
523        // FullIndex + set
524        assert!(matches!(
525            property_store.get_index_set_for_query_parts(Age::index_id(), &missing_query_parts,),
526            IndexSetResult::Empty
527        ));
528        assert!(matches!(
529            property_store.get_index_set_for_query_parts(
530                Age::index_id(),
531                &existing_query_parts,
532            ),
533            IndexSetResult::Set(set) if set.len() == 2
534        ));
535    }
536
537    #[test]
538    fn test_index_query_results_for_property_store_value_count_index() {
539        let mut context = Context::new();
540        context.index_property_counts::<Person, Age>();
541
542        let existing_value = Age(12);
543        let missing_value = Age(99);
544        let existing_query_parts = [&existing_value as &dyn Any];
545        let missing_query_parts = [&missing_value as &dyn Any];
546
547        let _ = context.add_entity(with!(Person, existing_value)).unwrap();
548        let _ = context.add_entity(with!(Person, existing_value)).unwrap();
549
550        let property_store = context.entity_store.get_property_store::<Person>();
551
552        // ValueCountIndex + count
553        assert_eq!(
554            property_store.get_index_count_for_query_parts(Age::index_id(), &missing_query_parts,),
555            IndexCountResult::Count(0)
556        );
557        assert_eq!(
558            property_store.get_index_count_for_query_parts(Age::index_id(), &existing_query_parts,),
559            IndexCountResult::Count(2)
560        );
561
562        // ValueCountIndex + set (unsupported)
563        assert!(matches!(
564            property_store.get_index_set_for_query_parts(Age::index_id(), &missing_query_parts,),
565            IndexSetResult::Unsupported
566        ));
567        assert!(matches!(
568            property_store.get_index_set_for_query_parts(Age::index_id(), &existing_query_parts,),
569            IndexSetResult::Unsupported
570        ));
571    }
572
573    #[test]
574    fn test_index_query_results_for_property_store_unindexed() {
575        let mut context = Context::new();
576        let existing_value = Age(12);
577        let missing_value = Age(99);
578        let existing_query_parts = [&existing_value as &dyn Any];
579        let missing_query_parts = [&missing_value as &dyn Any];
580
581        let _ = context.add_entity(with!(Person, existing_value)).unwrap();
582        let _ = context.add_entity(with!(Person, existing_value)).unwrap();
583
584        let property_store = context.entity_store.get_property_store::<Person>();
585
586        // Unindexed + count
587        assert_eq!(
588            property_store.get_index_count_for_query_parts(Age::index_id(), &missing_query_parts,),
589            IndexCountResult::Unsupported
590        );
591        assert_eq!(
592            property_store.get_index_count_for_query_parts(Age::index_id(), &existing_query_parts,),
593            IndexCountResult::Unsupported
594        );
595
596        // Unindexed + set
597        assert!(matches!(
598            property_store.get_index_set_for_query_parts(Age::index_id(), &missing_query_parts,),
599            IndexSetResult::Unsupported
600        ));
601        assert!(matches!(
602            property_store.get_index_set_for_query_parts(Age::index_id(), &existing_query_parts,),
603            IndexSetResult::Unsupported
604        ));
605    }
606}