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::PartialPropertyChangeEvent;
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, HashValueType, 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 type-erased `PropertyValueStore<E>`.
250    #[cfg(test)]
251    pub(crate) fn get_with_id(&self, property_id: usize) -> &dyn PropertyValueStore<E> {
252        self.items[property_id].as_ref()
253    }
254
255    /// Fetches an immutable reference to the `PropertyValueStoreCore<E, P>`.
256    #[must_use]
257    pub fn get<P: Property<E>>(&self) -> &PropertyValueStoreCore<E, P> {
258        let index = P::id();
259        let property_value_store =
260            self.items
261                .get(index)
262                .unwrap_or_else(||
263                    panic!(
264                        "No registered property found with index = {:?} while trying to get property {}. You must use the `define_property!` macro to create a registered property.",
265                        index,
266                        P::name()
267                    )
268                );
269        let property_value_store: &PropertyValueStoreCore<E, P> = property_value_store
270            .as_any()
271            .downcast_ref::<PropertyValueStoreCore<E, P>>()
272            .unwrap_or_else(||
273                {
274                    panic!(
275                        "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.",
276                        index,
277                        (**property_value_store).type_id(),
278                        TypeId::of::<PropertyValueStoreCore<E, P>>()
279                    )
280                }
281            );
282        property_value_store
283    }
284
285    /// Fetches a mutable reference to the `PropertyValueStoreCore<E, P>`.
286    #[must_use]
287    pub fn get_mut<P: Property<E>>(&mut self) -> &mut PropertyValueStoreCore<E, P> {
288        let index = P::id();
289        let property_value_store =
290            self.items
291                .get_mut(index)
292                .unwrap_or_else(||
293                    panic!(
294                        "No registered property found with index = {:?} while trying to get property {}. You must use the `define_property!` macro to create a registered property.",
295                        index,
296                        P::name()
297                    )
298                );
299        let type_id = (**property_value_store).type_id(); // Only used for error message if error occurs.
300        let property_value_store: &mut PropertyValueStoreCore<E, P> = property_value_store
301            .as_any_mut()
302            .downcast_mut::<PropertyValueStoreCore<E, P>>()
303            .unwrap_or_else(||
304                {
305                    panic!(
306                        "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.",
307                        index,
308                        type_id,
309                        TypeId::of::<PropertyValueStoreCore<E, P>>()
310                    )
311                }
312            );
313        property_value_store
314    }
315
316    /// Creates a `PartialPropertyChangeEvent` instance for the `entity_id` and `property_index`. This method is only
317    /// called for derived dependents of some property that has changed (one of `P`'s non-derived dependencies).
318    pub(crate) fn create_partial_property_change(
319        &self,
320        property_index: usize,
321        entity_id: EntityId<E>,
322        context: &Context,
323    ) -> Box<dyn PartialPropertyChangeEvent> {
324        let property_value_store = self.items
325                                       .get(property_index)
326            .unwrap_or_else(|| panic!("No registered property found with index = {property_index:?}. You must use the `define_property!` macro to create a registered property."));
327
328        property_value_store.create_partial_property_change(entity_id, context)
329    }
330
331    /// Returns whether or not the property `P` is indexed.
332    ///
333    /// This method can return `true` even if `context.index_property::<P>()` has never been called. For example,
334    /// if a multi-property is indexed, all equivalent multi-properties are automatically also indexed, as they
335    /// share a single index.
336    #[cfg(test)]
337    pub fn is_property_indexed<P: Property<E>>(&self) -> bool {
338        self.items
339            .get(P::index_id())
340            .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()))
341            .index_type()
342            != PropertyIndexType::Unindexed
343    }
344
345    /// Sets the index type for `P`. Passing `PropertyIndexType::Unindexed` removes any existing index for `P`.
346    ///
347    /// Note that the index might not live in the `PropertyValueStore` associated with `P` itself, as in the case
348    /// of multi-properties which share a single index among all equivalent multi-properties.
349    pub fn set_property_indexed<P: Property<E>>(&mut self, index_type: PropertyIndexType) {
350        let property_value_store = self.items
351            .get_mut(P::index_id())
352            .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()));
353        property_value_store.set_indexed(index_type);
354    }
355
356    /// Creates a stratified value change counter for tracked property `P` with strata `PL`.
357    ///
358    /// Returns the counter ID.
359    pub fn create_value_change_counter<PL, P>(&mut self) -> usize
360    where
361        PL: PropertyList<E> + Eq + std::hash::Hash,
362        P: Property<E> + Eq + std::hash::Hash,
363    {
364        let property_value_store = self.get_mut::<P>();
365        property_value_store.add_value_change_counter(Box::new(StratifiedValueChangeCounter::<
366            E,
367            PL,
368            P,
369        >::new()))
370    }
371
372    /// Updates the index of the property having the given ID for any entities that have been added to the context
373    /// since the last time the index was updated.
374    pub fn index_unindexed_entities_for_property_id(
375        &mut self,
376        context: &Context,
377        property_id: usize,
378    ) {
379        self.items[property_id].index_unindexed_entities(context)
380    }
381
382    /// Updates all indexed properties for any entities that have been added since the last update.
383    pub fn index_unindexed_entities_for_all_properties(&mut self, context: &Context) {
384        for store in &mut self.items {
385            store.index_unindexed_entities(context);
386        }
387    }
388
389    pub fn get_index_set_with_hash_for_property_id(
390        &self,
391        property_id: usize,
392        hash: HashValueType,
393    ) -> IndexSetResult<'_, E> {
394        self.items[property_id].get_index_set_with_hash_result(hash)
395    }
396
397    pub fn get_index_count_with_hash_for_property_id(
398        &self,
399        property_id: usize,
400        hash: HashValueType,
401    ) -> IndexCountResult {
402        self.items[property_id].get_index_count_with_hash_result(hash)
403    }
404}
405
406#[cfg(test)]
407mod tests {
408    #![allow(dead_code)]
409    use super::*;
410    use crate::entity::index::{IndexCountResult, IndexSetResult};
411    use crate::prelude::*;
412    use crate::{define_entity, define_property, Context};
413
414    define_entity!(Person);
415
416    define_property!(struct Age(u8), Person);
417    define_property!(
418        enum InfectionStatus {
419            Susceptible,
420            Infected,
421            Recovered,
422        },
423        Person,
424        default_const = InfectionStatus::Susceptible
425    );
426    define_property!(struct Vaccinated(bool), Person, default_const = Vaccinated(false));
427
428    #[test]
429    fn test_get_property_store() {
430        let property_store = PropertyStore::new();
431
432        {
433            let ages: &PropertyValueStoreCore<_, Age> = property_store.get();
434            ages.set(EntityId::<Person>::new(0), Age(12));
435            ages.set(EntityId::<Person>::new(1), Age(33));
436            ages.set(EntityId::<Person>::new(2), Age(44));
437
438            let infection_statuses: &PropertyValueStoreCore<_, InfectionStatus> =
439                property_store.get();
440            infection_statuses.set(EntityId::<Person>::new(0), InfectionStatus::Susceptible);
441            infection_statuses.set(EntityId::<Person>::new(1), InfectionStatus::Susceptible);
442            infection_statuses.set(EntityId::<Person>::new(2), InfectionStatus::Infected);
443
444            let vaccine_status: &PropertyValueStoreCore<_, Vaccinated> = property_store.get();
445            vaccine_status.set(EntityId::<Person>::new(0), Vaccinated(true));
446            vaccine_status.set(EntityId::<Person>::new(1), Vaccinated(false));
447            vaccine_status.set(EntityId::<Person>::new(2), Vaccinated(true));
448        }
449
450        // Verify that `get` returns the expected values
451        {
452            let ages: &PropertyValueStoreCore<_, Age> = property_store.get();
453            assert_eq!(ages.get(EntityId::<Person>::new(0)), Age(12));
454            assert_eq!(ages.get(EntityId::<Person>::new(1)), Age(33));
455            assert_eq!(ages.get(EntityId::<Person>::new(2)), Age(44));
456
457            let infection_statuses: &PropertyValueStoreCore<_, InfectionStatus> =
458                property_store.get();
459            assert_eq!(
460                infection_statuses.get(EntityId::<Person>::new(0)),
461                InfectionStatus::Susceptible
462            );
463            assert_eq!(
464                infection_statuses.get(EntityId::<Person>::new(1)),
465                InfectionStatus::Susceptible
466            );
467            assert_eq!(
468                infection_statuses.get(EntityId::<Person>::new(2)),
469                InfectionStatus::Infected
470            );
471
472            let vaccine_status: &PropertyValueStoreCore<_, Vaccinated> = property_store.get();
473            assert_eq!(
474                vaccine_status.get(EntityId::<Person>::new(0)),
475                Vaccinated(true)
476            );
477            assert_eq!(
478                vaccine_status.get(EntityId::<Person>::new(1)),
479                Vaccinated(false)
480            );
481            assert_eq!(
482                vaccine_status.get(EntityId::<Person>::new(2)),
483                Vaccinated(true)
484            );
485        }
486    }
487
488    #[test]
489    fn test_index_query_results_for_property_store() {
490        let mut context = Context::new();
491        context.index_property::<Person, Age>();
492
493        let existing_value = Age(12);
494        let missing_value = Age(99);
495        let existing_hash = <Age as Property<Person>>::hash_property_value(&existing_value);
496        let missing_hash = <Age as Property<Person>>::hash_property_value(&missing_value);
497
498        let _ = context.add_entity((existing_value,)).unwrap();
499        let _ = context.add_entity((existing_value,)).unwrap();
500
501        let property_store = context.entity_store.get_property_store::<Person>();
502
503        // FullIndex + count
504        assert_eq!(
505            property_store
506                .get_index_count_with_hash_for_property_id(Age::index_id(), missing_hash,),
507            IndexCountResult::Count(0)
508        );
509        assert_eq!(
510            property_store
511                .get_index_count_with_hash_for_property_id(Age::index_id(), existing_hash,),
512            IndexCountResult::Count(2)
513        );
514
515        // FullIndex + set
516        assert!(matches!(
517            property_store.get_index_set_with_hash_for_property_id(Age::index_id(), missing_hash,),
518            IndexSetResult::Empty
519        ));
520        assert!(matches!(
521            property_store.get_index_set_with_hash_for_property_id(
522                Age::index_id(),
523                existing_hash,
524            ),
525            IndexSetResult::Set(set) if set.len() == 2
526        ));
527    }
528
529    #[test]
530    fn test_index_query_results_for_property_store_value_count_index() {
531        let mut context = Context::new();
532        context.index_property_counts::<Person, Age>();
533
534        let existing_value = Age(12);
535        let missing_value = Age(99);
536        let existing_hash = <Age as Property<Person>>::hash_property_value(&existing_value);
537        let missing_hash = <Age as Property<Person>>::hash_property_value(&missing_value);
538
539        let _ = context.add_entity((existing_value,)).unwrap();
540        let _ = context.add_entity((existing_value,)).unwrap();
541
542        let property_store = context.entity_store.get_property_store::<Person>();
543
544        // ValueCountIndex + count
545        assert_eq!(
546            property_store
547                .get_index_count_with_hash_for_property_id(Age::index_id(), missing_hash,),
548            IndexCountResult::Count(0)
549        );
550        assert_eq!(
551            property_store
552                .get_index_count_with_hash_for_property_id(Age::index_id(), existing_hash,),
553            IndexCountResult::Count(2)
554        );
555
556        // ValueCountIndex + set (unsupported)
557        assert!(matches!(
558            property_store.get_index_set_with_hash_for_property_id(Age::index_id(), missing_hash,),
559            IndexSetResult::Unsupported
560        ));
561        assert!(matches!(
562            property_store.get_index_set_with_hash_for_property_id(Age::index_id(), existing_hash,),
563            IndexSetResult::Unsupported
564        ));
565    }
566
567    #[test]
568    fn test_index_query_results_for_property_store_unindexed() {
569        let mut context = Context::new();
570        let existing_value = Age(12);
571        let missing_value = Age(99);
572        let existing_hash = <Age as Property<Person>>::hash_property_value(&existing_value);
573        let missing_hash = <Age as Property<Person>>::hash_property_value(&missing_value);
574
575        let _ = context.add_entity((existing_value,)).unwrap();
576        let _ = context.add_entity((existing_value,)).unwrap();
577
578        let property_store = context.entity_store.get_property_store::<Person>();
579
580        // Unindexed + count
581        assert_eq!(
582            property_store
583                .get_index_count_with_hash_for_property_id(Age::index_id(), missing_hash,),
584            IndexCountResult::Unsupported
585        );
586        assert_eq!(
587            property_store
588                .get_index_count_with_hash_for_property_id(Age::index_id(), existing_hash,),
589            IndexCountResult::Unsupported
590        );
591
592        // Unindexed + set
593        assert!(matches!(
594            property_store.get_index_set_with_hash_for_property_id(Age::index_id(), missing_hash,),
595            IndexSetResult::Unsupported
596        ));
597        assert!(matches!(
598            property_store.get_index_set_with_hash_for_property_id(Age::index_id(), existing_hash,),
599            IndexSetResult::Unsupported
600        ));
601    }
602}