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