Skip to main content

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 assign an ID to 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::data_structures::bit_set::BitSet;
37use crate::entity::entity::Entity;
38use crate::entity::entity_store::register_property_with_entity;
39use crate::entity::events::PartialPropertyChangeEventBox;
40use crate::entity::index::{IndexCountResult, IndexSetResult, PropertyIndex};
41use crate::entity::property::{IndexableProperty, Property};
42use crate::entity::property_list::PropertyList;
43use crate::entity::property_value_store::PropertyValueStore;
44use crate::entity::property_value_store_core::PropertyValueStoreCore;
45use crate::entity::value_change_counter::StratifiedValueChangeCounter;
46use crate::entity::EntityId;
47use crate::{Context, ContextEntitiesExt};
48
49pub(in crate::entity) type IndexNewEntityFn<E> = fn(&mut Context, EntityId<E>);
50
51fn index_new_entity<E, P>(context: &mut Context, entity_id: EntityId<E>)
52where
53    E: Entity,
54    P: IndexableProperty<E>,
55{
56    // This may compute a derived or multi-property. `P` is copied, so no reference into Context
57    // survives into the subsequent mutable borrow.
58    let value: P = context.get_property(entity_id);
59
60    let property_value_store = context.get_property_value_store_mut::<E, P>();
61    let index = property_value_store
62        .index
63        .as_mut()
64        .expect("Ixa internal error: index_new_entity dispatch invoked for an unindexed property");
65
66    index.add_entity(&value, entity_id);
67}
68
69/// A map from Entity ID to a count of the properties already associated with the entity. The value for the key is
70/// equivalent to the next property ID that will be assigned to the next property that requests an ID. Each `Entity`
71/// type has its own series of increasing property IDs.
72///
73/// Note: The mechanism to assign property IDs needs to be distinct from the rest of property registration, because
74/// properties often need to have an ID assigned _before_ its registration proper so that it can be recorded as a
75/// dependency of some other property.
76static NEXT_PROPERTY_ID: LazyLock<Mutex<HashMap<usize, usize>>> =
77    LazyLock::new(|| Mutex::new(HashMap::default()));
78
79/// Entity-scoped names for logical property identities, used to construct
80/// query-profiling labels without query-specific name collection.
81#[cfg(feature = "profiling")]
82static PROPERTY_NAMES: LazyLock<Mutex<HashMap<(usize, TypeId), &'static str>>> =
83    LazyLock::new(|| Mutex::new(HashMap::default()));
84
85#[cfg(feature = "profiling")]
86pub(crate) fn registered_property_name(entity_id: usize, property_type_id: TypeId) -> &'static str {
87    PROPERTY_NAMES
88        .lock()
89        .unwrap()
90        .get(&(entity_id, property_type_id))
91        .unwrap_or_else(|| {
92            panic!(
93                "No registered property name for entity ID {entity_id} and logical property type ID {property_type_id:?}"
94            )
95        })
96}
97
98/// A container struct to hold the (global) metadata for a single property.
99///
100/// At program startup (before `main()`, using ctors) we compute metadata for all properties
101/// that are linked into the binary, and this data remains unchanged for the life of the program.
102#[derive(Default)]
103pub(super) struct PropertyMetadata<E: Entity> {
104    /// The (derived) properties that depend on this property, as represented by their
105    /// `Property::id` value. This list is used to update the index (if applicable)
106    /// and emit change events for these properties when this property changes.
107    pub dependents: Vec<usize>,
108    /// A function that constructs a new `PropertyValueStoreCore<E, P>` instance in a type-erased
109    /// way, used in the constructor of `PropertyStore`. This is an `Option` because this
110    /// function pointer is recorded possibly out-of-order from when the `PropertyMetadata`
111    /// instance for this property needs to exist (when its dependents are recorded).
112    #[allow(clippy::type_complexity)]
113    pub value_store_constructor: Option<fn() -> Box<dyn PropertyValueStore<E>>>,
114}
115
116/// This maps `(entity_type_id, property_type_index)` to `PropertyMetadata<E>`, which holds a vector of dependents (as IDs)
117/// and a function pointer to the constructor that constucts a `PropertyValueStoreCore<E, P>` type erased as
118/// 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`()].
119#[allow(clippy::type_complexity)]
120static PROPERTY_METADATA_BUILDER: LazyLock<
121    Mutex<HashMap<(usize, usize), Box<dyn Any + Send + Sync>>>,
122> = LazyLock::new(|| Mutex::new(HashMap::default()));
123
124/// The frozen property metadata registry, created exactly once on first read.
125///
126/// This is derived from `PROPERTY_METADATA_BUILDER` by moving the builder `HashMap` out. After this point,
127/// registration is no longer allowed.
128static PROPERTY_METADATA: OnceLock<HashMap<(usize, usize), Box<dyn Any + Send + Sync>>> =
129    OnceLock::new();
130
131/// Private helper to fetch or initialize the frozen metadata.
132fn property_metadata() -> &'static HashMap<(usize, usize), Box<dyn Any + Send + Sync>> {
133    PROPERTY_METADATA.get_or_init(|| {
134        let mut builder = PROPERTY_METADATA_BUILDER.lock().unwrap();
135        std::mem::take(&mut *builder)
136    })
137}
138
139/// The public getter for the dependents of a property with index `property_index` (as stored in
140/// `PROPERTY_METADATA`). The `Property<E: Entity>::dependents()` method defers to this.
141///
142/// This function should only be called once `main()` starts, that is, not in `ctors` constructors,
143/// as it assumes `PROPERTY_METADATA` has been correctly initialized. Hence, the "static" suffix.
144#[must_use]
145pub(super) fn get_property_dependents_static<E: Entity>(property_index: usize) -> &'static [usize] {
146    let map = property_metadata();
147    let property_metadata = map
148        .get(&(E::id(), property_index))
149                               .unwrap_or_else(|| panic!("No registered property found with index = {property_index:?}. You must use the `define_property!` macro to create a registered property."));
150    let property_metadata: &PropertyMetadata<E> = property_metadata.downcast_ref().unwrap_or_else(
151        || panic!(
152            "Property type at index {:?} does not match registered property type. You must use the `define_property!` macro to create a registered property.",
153            property_index
154        )
155    );
156
157    property_metadata.dependents.as_slice()
158}
159
160/// Adds a new item to the registry. The job of this method is to create whatever "singleton"
161/// data/metadata is associated with the [`crate::entity::property::Property`] if it doesn't already exist. In
162/// our use case, this method is called in the `ctor` function of each `Property<E>` type.
163pub fn add_to_property_registry<E: Entity, P: Property<E>>() {
164    // Ensure the ID of the property type is initialized.
165    let property_index = P::id();
166
167    #[cfg(feature = "profiling")]
168    {
169        let key = (E::id(), P::type_id());
170        let mut names = PROPERTY_NAMES.lock().unwrap();
171        if let Some(existing) = names.insert(key, P::name()) {
172            assert_eq!(
173                existing,
174                P::name(),
175                "conflicting names for one logical property identity"
176            );
177        }
178    }
179
180    // Registers the property with the entity type.
181    register_property_with_entity(
182        <E as Entity>::type_id(),
183        <P as Property<E>>::type_id(),
184        P::is_required(),
185    );
186
187    let mut property_metadata = PROPERTY_METADATA_BUILDER.lock().unwrap();
188    if PROPERTY_METADATA.get().is_some() {
189        panic!(
190            "`add_to_property_registry()` called after property metadata was frozen; registration must occur during startup/ctors."
191        );
192    }
193
194    // Register the `PropertyValueStoreCore<E, P>` constructor.
195    {
196        let metadata = property_metadata
197            .entry((E::id(), property_index))
198            .or_insert_with(|| Box::new(PropertyMetadata::<E>::default()));
199        let metadata: &mut PropertyMetadata<E> = metadata
200            .downcast_mut()
201            .expect("Ixa internal error: property metadata has the wrong entity type");
202        metadata
203            .value_store_constructor
204            .get_or_insert(PropertyValueStoreCore::<E, P>::new_boxed);
205    }
206
207    // Construct the dependency graph
208    for dependency in P::non_derived_dependencies() {
209        // Add `property_index` as a dependent of the dependency
210        let dependency_meta = property_metadata
211            .entry((E::id(), dependency))
212            .or_insert_with(|| Box::new(PropertyMetadata::<E>::default()));
213        let dependency_meta: &mut PropertyMetadata<E> = dependency_meta
214            .downcast_mut()
215            .expect("Ixa internal error: dependency metadata has the wrong entity type");
216        dependency_meta.dependents.push(property_index);
217    }
218}
219
220/// A convenience getter for `NEXT_ENTITY_INDEX`.
221pub fn get_registered_property_count<E: Entity>() -> usize {
222    let map = NEXT_PROPERTY_ID.lock().unwrap();
223    *map.get(&E::id()).unwrap_or(&0)
224}
225
226/// Encapsulates the synchronization logic for initializing an item's index.
227///
228/// Acquires a global lock on the next available property ID, but only increments
229/// it if we successfully initialize the provided ID. The ID of a property is
230/// assigned at runtime but only once per type. It's possible for a single
231/// type to attempt to initialize its index multiple times from different threads,
232/// which is why all this synchronization is required. However, the overhead
233/// is negligible, as this initialization only happens once upon first access.
234///
235/// In fact, for our use case we know we are calling this function
236/// once for each type in each `Property`'s `ctor` function, which
237/// should be the only time this method is ever called for the type.
238pub fn initialize_property_id<E: Entity>(property_id: &AtomicUsize) -> usize {
239    // Acquire a global lock.
240    let mut guard = NEXT_PROPERTY_ID.lock().unwrap();
241    let candidate = guard.entry(E::id()).or_insert_with(|| 0);
242
243    // The property may have been initialized while this call waited for the lock.
244    let existing = property_id.load(Ordering::Acquire);
245    if existing != usize::MAX {
246        return existing;
247    }
248
249    // Try to claim the candidate index. Here we guard against the potential race condition that
250    // another instance of this plugin in another thread just initialized the index prior to us
251    // obtaining the lock. If the index has been initialized beneath us, we do not update
252    // NEXT_PROPERTY_INDEX, we just return the value `index` was initialized to.
253    // For a justification of the data ordering, see:
254    //     https://github.com/CDCgov/ixa/pull/477#discussion_r2244302872
255    match property_id.compare_exchange(usize::MAX, *candidate, Ordering::AcqRel, Ordering::Acquire)
256    {
257        Ok(_) => {
258            // We won the race — increment the global next plugin index and return the new index
259            *candidate += 1;
260            *candidate - 1
261        }
262        Err(existing) => {
263            // Another thread beat us — don’t increment the global next plugin index,
264            // just return existing
265            existing
266        }
267    }
268}
269
270/// A wrapper around a vector of property value stores.
271pub struct PropertyStore<E: Entity> {
272    /// A vector of `Box<PropertyValueStoreCore<E, P>>`, type-erased to `Box<dyn PropertyValueStore<E>>`
273    items: Vec<Box<dyn PropertyValueStore<E>>>,
274
275    /// Set of properties that currently have `PropertyInitializedEvent` subscribers.
276    ///
277    /// `PropertyList::emit_initialized_events` reads this in the hot path to skip
278    /// per-property initialization-event work unless a handler is registered as a performance
279    /// optimization.
280    pub(in crate::entity) property_initialized_event_subscriptions: BitSet,
281
282    /// One entry for every property whose `PropertyValueStoreCore` currently has an index.
283    /// The property ID supports removal without relying on deduplicable function addresses.
284    pub(in crate::entity) index_new_entity_fns: Vec<(usize, IndexNewEntityFn<E>)>,
285}
286
287impl<E: Entity> Default for PropertyStore<E> {
288    fn default() -> Self {
289        PropertyStore::new()
290    }
291}
292
293impl<E: Entity> PropertyStore<E> {
294    /// Creates a new [`PropertyStore`].
295    #[must_use]
296    pub fn new() -> Self {
297        let num_items = get_registered_property_count::<E>();
298        // The constructors for each `PropertyValueStoreCore<E, P>` are stored in the `PROPERTY_METADATA` global.
299        let property_metadata = property_metadata();
300
301        // We construct the correct concrete `PropertyValueStoreCore<E, P>` value for each ID.
302        let items = (0..num_items)
303            .map(|idx| {
304                let metadata = property_metadata
305                    .get(&(E::id(), idx))
306                    .unwrap_or_else(|| {
307                        panic!("Ixa internal error: no property metadata entry for index {idx}")
308                    })
309                    .downcast_ref::<PropertyMetadata<E>>()
310                    .unwrap_or_else(|| {
311                        panic!(
312                            "Ixa internal error: property metadata entry for index {idx} does not \
313                             match the expected type"
314                        )
315                    });
316                let constructor = metadata.value_store_constructor.unwrap_or_else(|| {
317                    panic!("Ixa internal error: no PropertyValueStore constructor for index {idx}")
318                });
319                constructor()
320            })
321            .collect();
322
323        Self {
324            items,
325            property_initialized_event_subscriptions: BitSet::new(num_items),
326            index_new_entity_fns: Vec::new(),
327        }
328    }
329
330    /// Fetches an immutable reference to the `PropertyValueStoreCore<E, P>`.
331    #[must_use]
332    pub fn get<P: Property<E>>(&self) -> &PropertyValueStoreCore<E, P> {
333        let index = P::id();
334        let property_value_store =
335            self.items
336                .get(index)
337                .unwrap_or_else(||
338                    panic!(
339                        "No registered property found with index = {:?} while trying to get property {}. You must use the `define_property!` macro to create a registered property.",
340                        index,
341                        P::name()
342                    )
343                );
344        let property_value_store: &PropertyValueStoreCore<E, P> = property_value_store
345            .as_any()
346            .downcast_ref::<PropertyValueStoreCore<E, P>>()
347            .unwrap_or_else(||
348                {
349                    panic!(
350                        "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.",
351                        index,
352                        (**property_value_store).type_id(),
353                        TypeId::of::<PropertyValueStoreCore<E, P>>()
354                    )
355                }
356            );
357        property_value_store
358    }
359
360    /// Fetches a mutable reference to the `PropertyValueStoreCore<E, P>`.
361    #[must_use]
362    pub fn get_mut<P: Property<E>>(&mut self) -> &mut PropertyValueStoreCore<E, P> {
363        let index = P::id();
364        let property_value_store =
365            self.items
366                .get_mut(index)
367                .unwrap_or_else(||
368                    panic!(
369                        "No registered property found with index = {:?} while trying to get property {}. You must use the `define_property!` macro to create a registered property.",
370                        index,
371                        P::name()
372                    )
373                );
374        let type_id = (**property_value_store).type_id(); // Only used for error message if error occurs.
375        let property_value_store: &mut PropertyValueStoreCore<E, P> = property_value_store
376            .as_any_mut()
377            .downcast_mut::<PropertyValueStoreCore<E, P>>()
378            .unwrap_or_else(||
379                {
380                    panic!(
381                        "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.",
382                        index,
383                        type_id,
384                        TypeId::of::<PropertyValueStoreCore<E, P>>()
385                    )
386                }
387            );
388        property_value_store
389    }
390
391    /// Creates a `PartialPropertyChangeEvent` instance for the `entity_id` and `property_index`. This method is only
392    /// called for derived dependents of some property that has changed (one of `P`'s non-derived dependencies).
393    #[must_use]
394    pub(crate) fn create_partial_property_change(
395        &self,
396        property_index: usize,
397        entity_id: EntityId<E>,
398        context: &Context,
399    ) -> PartialPropertyChangeEventBox {
400        let property_value_store = self.items.get(property_index).unwrap_or_else(|| {
401            panic!(
402                "Ixa internal error: dependent property index {property_index:?} is not registered"
403            )
404        });
405
406        property_value_store.create_partial_property_change(entity_id, context)
407    }
408
409    /// Returns whether the property with `property_index` needs partial change-event processing.
410    #[must_use]
411    pub(crate) fn should_create_partial_property_change(
412        &self,
413        property_index: usize,
414        context: &Context,
415    ) -> bool {
416        let property_value_store = self.items.get(property_index).unwrap_or_else(|| {
417            panic!(
418                "Ixa internal error: dependent property index {property_index:?} is not registered"
419            )
420        });
421
422        property_value_store.should_create_partial_change(context)
423    }
424
425    /// Returns whether or not the property `P` is indexed.
426    ///
427    #[cfg(test)]
428    #[must_use]
429    pub fn is_property_indexed<P: Property<E>>(&self) -> bool {
430        self.get::<P>().index.is_some()
431    }
432
433    pub(in crate::entity) fn install_property_index<P>(
434        &mut self,
435        new_index: Option<Box<dyn PropertyIndex<E, P>>>,
436    ) where
437        P: IndexableProperty<E>,
438    {
439        let property_id = P::id();
440        let was_indexed = self.get::<P>().index.is_some();
441        let will_be_indexed = new_index.is_some();
442
443        // Property IDs are stable dispatcher identities; function-pointer addresses are not,
444        // because the linker may deduplicate distinct monomorphizations.
445        let dispatcher_position = self
446            .index_new_entity_fns
447            .iter()
448            .position(|(id, _)| *id == property_id);
449
450        if was_indexed {
451            assert!(
452                dispatcher_position.is_some(),
453                "Ixa internal error: indexed property is missing its index_new_entity dispatcher",
454            );
455        } else {
456            debug_assert!(
457                dispatcher_position.is_none(),
458                "Ixa internal error: unindexed property unexpectedly has an index_new_entity dispatcher",
459            );
460        }
461
462        // Reserve before replacing a valid index so allocation failure cannot leave the index
463        // installed without its dispatcher.
464        if !was_indexed && will_be_indexed {
465            self.index_new_entity_fns.reserve(1);
466        }
467
468        // All invariant checks and potentially allocating preparation are complete. Replacing the
469        // installed index is the commit point.
470        self.get_mut::<P>().index = new_index;
471
472        match (was_indexed, will_be_indexed) {
473            (false, true) => {
474                self.index_new_entity_fns
475                    .push((property_id, index_new_entity::<E, P> as IndexNewEntityFn<E>));
476            }
477            (true, false) => {
478                // The invariant check above proves that an indexed property has a dispatcher.
479                self.index_new_entity_fns
480                    .swap_remove(dispatcher_position.unwrap());
481            }
482            _ => {}
483        }
484    }
485
486    /// Creates a stratified value change counter for tracked property `P` with strata `PL`.
487    ///
488    /// Returns the counter ID.
489    #[must_use]
490    pub fn create_value_change_counter<PL, P>(&mut self) -> usize
491    where
492        PL: PropertyList<E> + Eq + std::hash::Hash,
493        P: Property<E> + Eq + std::hash::Hash,
494    {
495        let property_value_store = self.get_mut::<P>();
496        property_value_store.add_value_change_counter(Box::new(StratifiedValueChangeCounter::<
497            E,
498            PL,
499            P,
500        >::new()))
501    }
502
503    #[must_use]
504    pub fn get_index_set_for_query_parts(
505        &self,
506        property_id: usize,
507        query_parts: &[&dyn Any],
508    ) -> IndexSetResult<'_, E> {
509        self.items[property_id].get_index_set_for_query_parts(query_parts)
510    }
511
512    #[must_use]
513    pub fn get_index_count_for_query_parts(
514        &self,
515        property_id: usize,
516        query_parts: &[&dyn Any],
517    ) -> IndexCountResult {
518        self.items[property_id].get_index_count_for_query_parts(query_parts)
519    }
520}
521
522#[cfg(test)]
523mod tests {
524    #![allow(dead_code)]
525    use std::any::Any;
526    use std::panic::{catch_unwind, AssertUnwindSafe};
527
528    use super::*;
529    use crate::entity::index::{FullIndex, IndexCountResult, IndexSetResult, ValueCountIndex};
530    use crate::entity::PropertyIndexType;
531    use crate::prelude::*;
532    use crate::{define_derived_property, define_entity, define_property, with, Context};
533
534    define_entity!(Person);
535
536    define_property!(struct Age(u8), Person);
537    define_property!(
538        enum InfectionStatus {
539            Susceptible,
540            Infected,
541            Recovered,
542        },
543        Person,
544        default_const = InfectionStatus::Susceptible
545    );
546    define_property!(struct Vaccinated(bool), Person, default_const = Vaccinated(false));
547    define_property!(struct PanicDependency(u8), Person, default_const = PanicDependency(0));
548
549    define_derived_property!(
550        struct PanickingDerived(u8),
551        Person,
552        [PanicDependency],
553        [],
554        |dependency| {
555            let dependency: PanicDependency = dependency;
556            assert_ne!(dependency, PanicDependency(255), "sentinel property value");
557            PanickingDerived(dependency.0)
558        }
559    );
560
561    #[test]
562    fn property_store_default_matches_new() {
563        let property_store = PropertyStore::<Person>::default();
564        assert_eq!(
565            property_store.items.len(),
566            get_registered_property_count::<Person>()
567        );
568    }
569
570    #[test]
571    fn install_property_index_maintains_active_dispatchers() {
572        let mut context = Context::new();
573
574        {
575            let property_store = context.entity_store.get_property_store_mut::<Person>();
576            assert_eq!(property_store.index_new_entity_fns.len(), 0);
577
578            property_store.install_property_index::<Age>(Some(Box::new(ValueCountIndex::new())));
579            assert_eq!(property_store.index_new_entity_fns.len(), 1);
580
581            property_store.install_property_index::<Age>(Some(Box::new(ValueCountIndex::new())));
582            assert_eq!(property_store.index_new_entity_fns.len(), 1);
583
584            property_store.install_property_index::<Age>(Some(Box::new(FullIndex::new())));
585            assert_eq!(property_store.index_new_entity_fns.len(), 1);
586
587            property_store.install_property_index::<Age>(None);
588            assert_eq!(property_store.index_new_entity_fns.len(), 0);
589
590            property_store.install_property_index::<Age>(Some(Box::new(ValueCountIndex::new())));
591            property_store.install_property_index::<Vaccinated>(Some(Box::new(FullIndex::new())));
592            property_store.install_property_index::<Age>(None);
593
594            assert_eq!(property_store.index_new_entity_fns.len(), 1);
595            assert_eq!(property_store.index_new_entity_fns[0].0, Vaccinated::id());
596        }
597
598        context.add_entity(with!(Person, Age(10))).unwrap();
599        let property_store = context.entity_store.get_property_store::<Person>();
600        assert_eq!(
601            property_store.get::<Age>().index_type(),
602            PropertyIndexType::Unindexed
603        );
604        assert_eq!(
605            property_store.get_index_count_for_query_parts(
606                Vaccinated::id(),
607                &[&Vaccinated(false) as &dyn Any],
608            ),
609            IndexCountResult::Count(1),
610        );
611    }
612
613    #[test]
614    #[should_panic(
615        expected = "Ixa internal error: indexed property is missing its index_new_entity dispatcher"
616    )]
617    fn removing_index_without_dispatcher_panics() {
618        let mut property_store = PropertyStore::<Person>::new();
619        property_store.install_property_index::<Age>(Some(Box::new(FullIndex::new())));
620        property_store.index_new_entity_fns.clear();
621
622        property_store.install_property_index::<Age>(None);
623    }
624
625    #[test]
626    #[should_panic(
627        expected = "Ixa internal error: unindexed property unexpectedly has an index_new_entity dispatcher"
628    )]
629    fn dispatcher_without_index_panics() {
630        let mut property_store = PropertyStore::<Person>::new();
631        property_store.install_property_index::<Age>(Some(Box::new(FullIndex::new())));
632        property_store.get_mut::<Age>().index = None;
633
634        property_store.install_property_index::<Age>(None);
635    }
636
637    #[test]
638    fn failed_replacement_keeps_old_index_and_dispatcher() {
639        let mut context = Context::new();
640        let ordinary = context
641            .add_entity(with!(Person, Age(10), PanicDependency(10)))
642            .unwrap();
643        let sentinel = context
644            .add_entity(with!(Person, Age(20), PanicDependency(255)))
645            .unwrap();
646
647        let mut old_index = ValueCountIndex::<Person, PanickingDerived>::new();
648        old_index.add_entity(&PanickingDerived(10), ordinary);
649        old_index.add_entity(&PanickingDerived(255), sentinel);
650        context
651            .entity_store
652            .get_property_store_mut::<Person>()
653            .install_property_index::<PanickingDerived>(Some(Box::new(old_index)));
654
655        let (old_type, old_dispatcher_count, old_dispatcher_property_id) = {
656            let property_store = context.entity_store.get_property_store::<Person>();
657            (
658                property_store.get::<PanickingDerived>().index_type(),
659                property_store.index_new_entity_fns.len(),
660                property_store.index_new_entity_fns[0].0,
661            )
662        };
663        assert_eq!(old_type, PropertyIndexType::ValueCountIndex);
664        assert_eq!(
665            context.query_entity_count(with!(Person, PanickingDerived(10))),
666            1
667        );
668        assert_eq!(
669            context.query_entity_count(with!(Person, PanickingDerived(255))),
670            1
671        );
672
673        let result = catch_unwind(AssertUnwindSafe(|| {
674            context.index_property::<Person, PanickingDerived>();
675        }));
676        assert!(result.is_err());
677
678        let property_store = context.entity_store.get_property_store::<Person>();
679        assert_eq!(
680            property_store.get::<PanickingDerived>().index_type(),
681            old_type
682        );
683        assert_eq!(
684            property_store.index_new_entity_fns.len(),
685            old_dispatcher_count
686        );
687        assert_eq!(
688            property_store.index_new_entity_fns[0].0,
689            old_dispatcher_property_id
690        );
691        assert_eq!(
692            context.query_entity_count(with!(Person, PanickingDerived(10))),
693            1
694        );
695        assert_eq!(
696            context.query_entity_count(with!(Person, PanickingDerived(255))),
697            1
698        );
699
700        context
701            .add_entity(with!(Person, Age(30), PanicDependency(10)))
702            .unwrap();
703        assert_eq!(
704            context.query_entity_count(with!(Person, PanickingDerived(10))),
705            2
706        );
707    }
708
709    #[test]
710    fn test_get_property_store() {
711        let mut property_store = PropertyStore::new();
712
713        {
714            let ages: &mut PropertyValueStoreCore<_, Age> = property_store.get_mut();
715            ages.set(EntityId::<Person>::new(0), Age(12));
716            ages.set(EntityId::<Person>::new(1), Age(33));
717            ages.set(EntityId::<Person>::new(2), Age(44));
718
719            let infection_statuses: &mut PropertyValueStoreCore<_, InfectionStatus> =
720                property_store.get_mut();
721            infection_statuses.set(EntityId::<Person>::new(0), InfectionStatus::Susceptible);
722            infection_statuses.set(EntityId::<Person>::new(1), InfectionStatus::Susceptible);
723            infection_statuses.set(EntityId::<Person>::new(2), InfectionStatus::Infected);
724
725            let vaccine_status: &mut PropertyValueStoreCore<_, Vaccinated> =
726                property_store.get_mut();
727            vaccine_status.set(EntityId::<Person>::new(0), Vaccinated(true));
728            vaccine_status.set(EntityId::<Person>::new(1), Vaccinated(false));
729            vaccine_status.set(EntityId::<Person>::new(2), Vaccinated(true));
730        }
731
732        // Verify that `get` returns the expected values
733        {
734            let ages: &PropertyValueStoreCore<_, Age> = property_store.get();
735            assert_eq!(ages.get(EntityId::<Person>::new(0)), Age(12));
736            assert_eq!(ages.get(EntityId::<Person>::new(1)), Age(33));
737            assert_eq!(ages.get(EntityId::<Person>::new(2)), Age(44));
738
739            let infection_statuses: &PropertyValueStoreCore<_, InfectionStatus> =
740                property_store.get();
741            assert_eq!(
742                infection_statuses.get(EntityId::<Person>::new(0)),
743                InfectionStatus::Susceptible
744            );
745            assert_eq!(
746                infection_statuses.get(EntityId::<Person>::new(1)),
747                InfectionStatus::Susceptible
748            );
749            assert_eq!(
750                infection_statuses.get(EntityId::<Person>::new(2)),
751                InfectionStatus::Infected
752            );
753
754            let vaccine_status: &PropertyValueStoreCore<_, Vaccinated> = property_store.get();
755            assert_eq!(
756                vaccine_status.get(EntityId::<Person>::new(0)),
757                Vaccinated(true)
758            );
759            assert_eq!(
760                vaccine_status.get(EntityId::<Person>::new(1)),
761                Vaccinated(false)
762            );
763            assert_eq!(
764                vaccine_status.get(EntityId::<Person>::new(2)),
765                Vaccinated(true)
766            );
767        }
768    }
769
770    #[test]
771    fn test_index_query_results_for_property_store() {
772        let mut context = Context::new();
773        context.index_property::<Person, Age>();
774
775        let existing_value = Age(12);
776        let missing_value = Age(99);
777        let existing_query_parts = [&existing_value as &dyn Any];
778        let missing_query_parts = [&missing_value as &dyn Any];
779
780        let _ = context.add_entity(with!(Person, existing_value)).unwrap();
781        let _ = context.add_entity(with!(Person, existing_value)).unwrap();
782
783        let property_store = context.entity_store.get_property_store::<Person>();
784
785        // FullIndex + count
786        assert_eq!(
787            property_store.get_index_count_for_query_parts(Age::id(), &missing_query_parts,),
788            IndexCountResult::Count(0)
789        );
790        assert_eq!(
791            property_store.get_index_count_for_query_parts(Age::id(), &existing_query_parts,),
792            IndexCountResult::Count(2)
793        );
794
795        // FullIndex + set
796        assert!(matches!(
797            property_store.get_index_set_for_query_parts(Age::id(), &missing_query_parts,),
798            IndexSetResult::Empty
799        ));
800        assert!(matches!(
801            property_store.get_index_set_for_query_parts(
802                Age::id(),
803                &existing_query_parts,
804            ),
805            IndexSetResult::Set(set) if set.len() == 2
806        ));
807    }
808
809    #[test]
810    fn test_index_query_results_for_property_store_value_count_index() {
811        let mut context = Context::new();
812        context.index_property_counts::<Person, Age>();
813
814        let existing_value = Age(12);
815        let missing_value = Age(99);
816        let existing_query_parts = [&existing_value as &dyn Any];
817        let missing_query_parts = [&missing_value as &dyn Any];
818
819        let _ = context.add_entity(with!(Person, existing_value)).unwrap();
820        let _ = context.add_entity(with!(Person, existing_value)).unwrap();
821
822        let property_store = context.entity_store.get_property_store::<Person>();
823
824        // ValueCountIndex + count
825        assert_eq!(
826            property_store.get_index_count_for_query_parts(Age::id(), &missing_query_parts,),
827            IndexCountResult::Count(0)
828        );
829        assert_eq!(
830            property_store.get_index_count_for_query_parts(Age::id(), &existing_query_parts,),
831            IndexCountResult::Count(2)
832        );
833
834        // ValueCountIndex + set (unsupported)
835        assert!(matches!(
836            property_store.get_index_set_for_query_parts(Age::id(), &missing_query_parts,),
837            IndexSetResult::Unsupported
838        ));
839        assert!(matches!(
840            property_store.get_index_set_for_query_parts(Age::id(), &existing_query_parts,),
841            IndexSetResult::Unsupported
842        ));
843    }
844
845    #[test]
846    fn test_index_query_results_for_property_store_unindexed() {
847        let mut context = Context::new();
848        let existing_value = Age(12);
849        let missing_value = Age(99);
850        let existing_query_parts = [&existing_value as &dyn Any];
851        let missing_query_parts = [&missing_value as &dyn Any];
852
853        let _ = context.add_entity(with!(Person, existing_value)).unwrap();
854        let _ = context.add_entity(with!(Person, existing_value)).unwrap();
855
856        let property_store = context.entity_store.get_property_store::<Person>();
857
858        // Unindexed + count
859        assert_eq!(
860            property_store.get_index_count_for_query_parts(Age::id(), &missing_query_parts,),
861            IndexCountResult::Unsupported
862        );
863        assert_eq!(
864            property_store.get_index_count_for_query_parts(Age::id(), &existing_query_parts,),
865            IndexCountResult::Unsupported
866        );
867
868        // Unindexed + set
869        assert!(matches!(
870            property_store.get_index_set_for_query_parts(Age::id(), &missing_query_parts,),
871            IndexSetResult::Unsupported
872        ));
873        assert!(matches!(
874            property_store.get_index_set_for_query_parts(Age::id(), &existing_query_parts,),
875            IndexSetResult::Unsupported
876        ));
877    }
878}