spacetimedb_table/
pointer_map.rs

1//! Provides [`PointerMap`] that deals with the
2//! association of a [`RowHash`] to a [`RowPointer`]
3//! through operations [`insert`](self::PointerMap::insert)
4//! and [`delete`](PointerMap::delete).
5//!
6//! These associations can then be queried through
7//! `map.pointers_for(hash)` and `map.pointers_for_mut(hash)`.
8//! In most cases, this will result in a `1:1` mapping
9//! and so a direct hit in a hash map.
10//! If however multiple pointers collide to a single hash,
11//! all of these pointers will be returned, in an arbitrary unstable order.
12//! Pointers are returned as a slice, which does not require an allocation.
13//! In this highly unlikely event of a collision,
14//! retrieval is probably no more than 100% slower.
15
16use super::indexes::{PageIndex, PageOffset, RowHash, RowPointer, SquashedOffset};
17use crate::{static_assert_size, MemoryUsage};
18use core::{hint, slice};
19use spacetimedb_data_structures::map::{
20    Entry,
21    IntMap, // No need to hash a hash.
22};
23
24/// An index to the outer layer of `colliders` in `PointerMap`.
25#[derive(Clone, Copy, PartialEq, Eq, Debug)]
26struct ColliderSlotIndex(u32);
27
28impl MemoryUsage for ColliderSlotIndex {}
29
30impl ColliderSlotIndex {
31    /// Returns a new slot index based on `idx`.
32    fn new(idx: usize) -> Self {
33        Self(idx as u32)
34    }
35
36    /// Returns the index as a `usize`.
37    fn idx(self) -> usize {
38        self.0 as usize
39    }
40}
41
42/// A pointer into the `pages` of a table
43/// or, for any `RowHash` collisions in `map`,
44/// the index in `colliders` to a list of `RowPointer`s.
45#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
46struct PtrOrCollider(RowPointer);
47
48impl MemoryUsage for PtrOrCollider {}
49
50/// An unpacked representation of [`&mut PtrOrCollider`](PtrOrCollider).
51enum MapSlotRef<'map> {
52    /// The hash has no collision and is associated to a single row pointer.
53    Pointer(&'map RowPointer),
54    /// The hash has collisions
55    /// and all of the associated row pointers can be found at `map.colliders[idx]`.
56    Collider(ColliderSlotIndex),
57}
58
59/// An unpacked representation of [`&PtrOrCollider`](PtrOrCollider).
60enum MapSlotMut<'map> {
61    /// The hash has no collision and is associated to a single row pointer.
62    Pointer(&'map mut RowPointer),
63    /// The hash has collisions
64    /// and all of the associated row pointers can be found at `map.colliders[idx]`.
65    Collider(ColliderSlotIndex),
66}
67
68/// Ensures `rp` is treated as a `RowPointer` by the map, and not as a collider.
69/// This is achieved by setting the reserved bit,
70/// used by [`PtrOrCollider::is_ptr`], to `false`.
71#[inline]
72const fn ensure_ptr(rp: RowPointer) -> RowPointer {
73    rp.with_reserved_bit(false)
74}
75
76impl PtrOrCollider {
77    /// Returns a pointer.
78    const fn ptr(rp: RowPointer) -> Self {
79        Self(ensure_ptr(rp))
80    }
81
82    /// Returns a collider.
83    const fn collider(c: ColliderSlotIndex) -> Self {
84        // Pack the `ColliderSlotIndex` into the page index bits.
85        let pi = PageIndex(c.0 as u64);
86        Self(RowPointer::new(
87            true,
88            pi,
89            PageOffset::VAR_LEN_NULL,
90            SquashedOffset::COMMITTED_STATE,
91        ))
92    }
93
94    /// Returns whether this is a pointer or not.
95    const fn is_ptr(&self) -> bool {
96        !self.0.reserved_bit()
97    }
98
99    /// Assumes that `self` is a `ColliderSlotIndex` and returns it as such.
100    const fn as_collider(&self) -> ColliderSlotIndex {
101        ColliderSlotIndex(self.0.page_index().0 as u32)
102    }
103
104    /// Convert the packed representation into an unpacked one.
105    const fn unpack(&self) -> MapSlotRef<'_> {
106        if self.is_ptr() {
107            MapSlotRef::Pointer(&self.0)
108        } else {
109            MapSlotRef::Collider(self.as_collider())
110        }
111    }
112
113    /// Convert the packed representation into an unpacked one.
114    fn unpack_mut(&mut self) -> MapSlotMut<'_> {
115        if self.is_ptr() {
116            MapSlotMut::Pointer(&mut self.0)
117        } else {
118            MapSlotMut::Collider(self.as_collider())
119        }
120    }
121}
122
123impl From<ColliderSlotIndex> for PtrOrCollider {
124    fn from(index: ColliderSlotIndex) -> Self {
125        Self::collider(index)
126    }
127}
128
129/// An pointer map `RowHash -> [RowPointer]`.
130#[derive(Default, Clone, PartialEq, Eq, Debug)]
131pub struct PointerMap {
132    /// The pointer map from row hashes to row pointer(s).
133    ///
134    /// Invariant: `self.maintains_map_invariant()`.
135    map: IntMap<RowHash, PtrOrCollider>,
136    /// The inner vector is a list ("slot") of row pointers that share a row hash.
137    /// The outer is indexed by [`ColliderSlotIndex`].
138    ///
139    /// This indirect approach is used,
140    /// rather than storing a list of [`RowPointer`]s,
141    /// to reduce the cost for the more common case (fewer collisions).
142    ///
143    /// This list is append-only as `ColliderSlotIndex` have to be stable.
144    /// When removing a row pointer causes a slot to become empty,
145    /// the index is added to `emptied_collider_slots` and it can be reused.
146    /// This is done to avoid a linear scan of `colliders` for the first empty slot.
147    ///
148    /// Invariant: `self.maintains_colliders_invariant()`.
149    // TODO(centril,perf): Use a `SatsBuffer<T>` with `len/capacity: u32` to reduce size.
150    colliders: Vec<Vec<RowPointer>>,
151    /// Stack of emptied collider slots.
152    // TODO(centril,perf): Use a `SatsBuffer<T>` with `len/capacity: u32` to reduce size.
153    emptied_collider_slots: Vec<ColliderSlotIndex>,
154}
155
156impl MemoryUsage for PointerMap {
157    fn heap_usage(&self) -> usize {
158        let Self {
159            map,
160            colliders,
161            emptied_collider_slots,
162        } = self;
163        map.heap_usage() + colliders.heap_usage() + emptied_collider_slots.heap_usage()
164    }
165}
166
167static_assert_size!(PointerMap, 80);
168
169// Provides the public API.
170impl PointerMap {
171    /// The number of colliding hashes in the map.
172    ///
173    /// If two hashes collide then this counts as 2.
174    pub fn num_collisions(&self) -> usize {
175        self.colliders.iter().map(|a| a.len()).sum()
176    }
177
178    /// The number hashes that do not collide.
179    pub fn num_non_collisions(&self) -> usize {
180        self.map.len() - (self.colliders.len() - self.emptied_collider_slots.len())
181    }
182
183    /// The number of pointers in the map. This is equal to the number of non-colliding hashes
184    /// plus the number of colliding hashes.
185    pub fn len(&self) -> usize {
186        self.num_collisions() + self.num_non_collisions()
187    }
188
189    /// Returns true if there are no pointers in the map.
190    pub fn is_empty(&self) -> bool {
191        self.map.is_empty()
192    }
193
194    /// Returns the row pointers associated with the given row `hash`.
195    pub fn pointers_for(&self, hash: RowHash) -> &[RowPointer] {
196        self.map.get(&hash).map_or(&[], |poc| match poc.unpack() {
197            MapSlotRef::Pointer(ro) => slice::from_ref(ro),
198            MapSlotRef::Collider(ci) => &self.colliders[ci.idx()],
199        })
200    }
201
202    /// Returns the row pointers associated with the given row `hash`.
203    ///
204    /// Take care not to change the reserved bit of any row pointer
205    /// or this will mess up the internal state of the [`PointerMap`].
206    pub fn pointers_for_mut(&mut self, hash: RowHash) -> &mut [RowPointer] {
207        self.map.get_mut(&hash).map_or(&mut [], |poc| match poc.unpack_mut() {
208            MapSlotMut::Pointer(ro) => slice::from_mut(ro),
209            MapSlotMut::Collider(ci) => &mut self.colliders[ci.idx()],
210        })
211    }
212
213    /// Associates row `hash` with row `ptr`.
214    /// Returns whether `hash` was already associated with `ptr`
215    ///
216    /// Handles any hash conflicts for `hash`.
217    pub fn insert(&mut self, hash: RowHash, ptr: RowPointer) -> bool {
218        let mut was_in_map = false;
219
220        self.map
221            .entry(hash)
222            .and_modify(|v| match v.unpack() {
223                // Already in map; bail for idempotence.
224                MapSlotRef::Pointer(existing) if *existing == ptr => was_in_map = true,
225                // Stored inline => colliders list.
226                MapSlotRef::Pointer(existing) => {
227                    let ptrs = [*existing, ptr].map(ensure_ptr);
228                    let ci = match self.emptied_collider_slots.pop() {
229                        // Allocate a new colliders slot.
230                        None => {
231                            let ci = ColliderSlotIndex::new(self.colliders.len());
232                            self.colliders.push(ptrs.into());
233                            ci
234                        }
235                        // Reuse an empty slot.
236                        Some(ci) => {
237                            self.colliders[ci.idx()].extend(ptrs);
238                            ci
239                        }
240                    };
241                    *v = PtrOrCollider::collider(ci);
242                }
243                // Already using a list; add to it.
244                MapSlotRef::Collider(ci) => {
245                    let ptr = ensure_ptr(ptr);
246                    let colliders = &mut self.colliders[ci.idx()];
247                    if colliders.contains(&ptr) {
248                        // Already in map; bail for idempotence.
249                        //
250                        // O(n) check, but that's OK,
251                        // as we only regress perf in case we have > 5_000
252                        // collisions for this `hash`.
253                        //
254                        // Let `n` be the number of bits (`64`)
255                        // and `k` be the number of hashes.
256                        // The average number of collisions, `avg`,
257                        // according to the birthday problem is:
258                        // `avg = 2^(-n) * combinations(k, 2)`.
259                        // (Caveat: our hash function is not truly random.)
260                        //
261                        // Solving for `avg = 5000`, we get `k ≈ 5 * 10^11`.
262                        // That is, we need around half a trillion hashes before,
263                        // on average, getting 5_000 collisions.
264                        // So we can safely ignore this in terms of perf.
265                        return was_in_map = true;
266                    }
267                    colliders.push(ptr);
268                }
269            })
270            // 0 hashes so far.
271            .or_insert(PtrOrCollider::ptr(ptr));
272
273        was_in_map
274    }
275
276    /// Removes the association `hash -> ptr`.
277    ///
278    /// Returns whether the association was deleted.
279    pub fn remove(&mut self, hash: RowHash, ptr: RowPointer) -> bool {
280        let ret = 'fun: {
281            let Entry::Occupied(mut entry) = self.map.entry(hash) else {
282                break 'fun false;
283            };
284
285            match entry.get().unpack() {
286                // Remove entry on `hash -> [ptr]`.
287                MapSlotRef::Pointer(o) if *o == ptr => drop(entry.remove()),
288                MapSlotRef::Pointer(_) => break 'fun false,
289                MapSlotRef::Collider(ci) => {
290                    // Find `ptr` in slot and remove.
291                    let slot = &mut self.colliders[ci.idx()];
292                    let Some(idx) = slot.iter().position(|o| *o == ptr) else {
293                        break 'fun false;
294                    };
295                    slot.swap_remove(idx);
296
297                    match slot.len() {
298                        // SAFETY: This never happens per `self.maintains_collider_invariant()`.
299                        0 => unsafe { hint::unreachable_unchecked() },
300                        // Simplify; don't use collider list since `hash -> [a_ptr]`.
301                        1 => *entry.get_mut() = PtrOrCollider::ptr(slot.pop().unwrap()),
302                        _ => break 'fun true,
303                    }
304
305                    // Slot is now empty; reuse later.
306                    self.emptied_collider_slots.push(ci);
307                }
308            }
309
310            true
311        };
312
313        ret
314    }
315}
316
317impl FromIterator<(RowHash, RowPointer)> for PointerMap {
318    fn from_iter<T: IntoIterator<Item = (RowHash, RowPointer)>>(iter: T) -> Self {
319        let mut map = PointerMap::default();
320        for (h, o) in iter {
321            let _ = map.insert(h, o);
322        }
323        map
324    }
325}
326
327#[cfg(test)]
328mod tests {
329    use super::*;
330    use core::hash::Hash;
331    use core::mem;
332    use itertools::Itertools;
333    use proptest::collection::vec;
334    use proptest::prelude::*;
335
336    type R = Result<(), TestCaseError>;
337
338    fn gen_row_pointer() -> impl Strategy<Value = RowPointer> {
339        (any::<PageOffset>(), any::<PageIndex>()).prop_map(|(po, pi)| RowPointer::new(false, pi, po, SquashedOffset(0)))
340    }
341
342    fn collect_entries(map: &PointerMap) -> Vec<(RowHash, PtrOrCollider)> {
343        map.map.iter().map(|(h, o)| (*h, *o)).collect::<Vec<_>>()
344    }
345
346    fn entry(hash: RowHash, ptr: RowPointer) -> (RowHash, PtrOrCollider) {
347        (hash, PtrOrCollider(ptr))
348    }
349
350    fn sorted<T: Ord + Copy>(xs: &[T]) -> Vec<T> {
351        xs.iter().copied().sorted().collect()
352    }
353
354    fn assert_ptrs_are(map: &mut PointerMap, hash: RowHash, ptrs: &[RowPointer]) -> R {
355        let ptrs = sorted(ptrs);
356        prop_assert_eq!(sorted(map.pointers_for(hash)), &*ptrs);
357        prop_assert_eq!(sorted(map.pointers_for_mut(hash)), ptrs);
358        Ok(())
359    }
360
361    fn assert_ptrs_and_len(map: &mut PointerMap, hash: RowHash, ptrs: &[RowPointer]) -> R {
362        assert_ptrs_are(map, hash, ptrs)?;
363        prop_assert_eq!(map.len(), ptrs.len());
364        prop_assert_eq!(map.is_empty(), ptrs.is_empty());
365        Ok(())
366    }
367
368    fn assert_collisions(map: &PointerMap, num_collisions: usize, num_not: usize) -> R {
369        prop_assert_eq!(map.num_collisions(), num_collisions);
370        prop_assert_eq!(map.num_non_collisions(), num_not);
371        Ok(())
372    }
373
374    fn ensure_unique<T: Eq + Hash>(xs: &[T]) -> R {
375        if !xs.iter().all_unique() {
376            return Err(TestCaseError::reject("all elements must be unique"));
377        }
378        Ok(())
379    }
380
381    proptest! {
382        #[test]
383        fn insert_same_twice_idempotence(
384            (hash, ptrs) in (
385                any::<RowHash>(),
386                vec(gen_row_pointer(), 3..10)
387            )
388         ) {
389            ensure_unique(&ptrs)?;
390
391            let mut map = PointerMap::default();
392
393            // Test the inline case.
394            let ptr = ptrs[0];
395            prop_assert_eq!(map.insert(hash, ptr), false);
396            let old_map = map.clone(); // Savepoint
397            prop_assert_eq!(map.insert(hash, ptr), true); // Insert again.
398            prop_assert_eq!(&map, &old_map); // No change!
399            // Check API state:
400            assert_ptrs_and_len(&mut map, hash, &[ptr])?;
401            assert_collisions(&map, 0, 1)?;
402            // Check internal state.
403            prop_assert_eq!(collect_entries(&map), [entry(hash, ptr)]);
404            prop_assert!(map.colliders.is_empty());
405            prop_assert!(map.emptied_collider_slots.is_empty());
406
407            // Test the colliders case.
408            // First insert the rest of the `ptrs`.
409            for ptr in &ptrs[1..] {
410                prop_assert_eq!(map.insert(hash, *ptr), false);
411            }
412            assert_ptrs_and_len(&mut map, hash, &ptrs)?;
413            assert_collisions(&map, ptrs.len(), 0)?;
414            // Now try inserting `ptr` again.
415            let old_map = map.clone(); // Savepoint
416            prop_assert_eq!(map.insert(hash, ptr), true); // Insert again.
417            prop_assert_eq!(&map, &old_map); // No change!
418            // Check API state:
419            assert_ptrs_and_len(&mut map, hash, &ptrs)?;
420            assert_collisions(&map, ptrs.len(), 0)?;
421            // Check internal state.
422            prop_assert_eq!(collect_entries(&map), [(hash, ColliderSlotIndex::new(0).into())]);
423            prop_assert_eq!(map.colliders, [ptrs.to_owned()]);
424            prop_assert!(map.emptied_collider_slots.is_empty());
425        }
426
427        #[test]
428        fn insert_same_ptr_under_diff_hash(
429            (hashes, ptr) in (vec(any::<RowHash>(), 2..10), gen_row_pointer())
430        ) {
431            ensure_unique(&hashes)?;
432
433            // Insert `ptr` under all `hashes`.
434            let mut map = PointerMap::default();
435            for hash in &hashes {
436                prop_assert_eq!(map.insert(*hash, ptr), false);
437            }
438            // Check API state:
439            for hash in &hashes {
440                assert_ptrs_are(&mut map, *hash, &[ptr])?;
441            }
442            prop_assert_eq!(map.len(), hashes.len());
443            prop_assert_eq!(map.is_empty(), false);
444            assert_collisions(&map, 0, hashes.len())?;
445            // Check internal state.
446            let mut entries = collect_entries(&map);
447            entries.sort();
448            prop_assert_eq!(
449                entries,
450                hashes.iter().copied().sorted().map(|hash| entry(hash, ptr)).collect::<Vec<_>>()
451            );
452            prop_assert!(map.colliders.is_empty());
453            prop_assert!(map.emptied_collider_slots.is_empty());
454        }
455
456        #[test]
457        fn insert_different_for_same_hash_handles_collision(
458            (hash, ptrs) in (any::<RowHash>(), vec(gen_row_pointer(), 3..10))
459        ) {
460            ensure_unique(&ptrs)?;
461
462            let mut map = PointerMap::default();
463
464            // Insert `0` -> no collision.
465            prop_assert_eq!(map.insert(hash, ptrs[0]), false);
466            // Check API state:
467            assert_ptrs_and_len(&mut map, hash, &ptrs[..1])?;
468            assert_collisions(&map, 0, 1)?;
469            // Check internal state.
470            prop_assert_eq!(collect_entries(&map), [entry(hash, ptrs[0])]);
471            prop_assert!(map.colliders.is_empty());
472            prop_assert!(map.emptied_collider_slots.is_empty());
473
474            // Insert `1` => `0` and `1` collide.
475            // This exercises "make new collider slot".
476            prop_assert_eq!(map.insert(hash, ptrs[1]), false);
477            // Check API state:
478            assert_ptrs_and_len(&mut map, hash, &ptrs[..2])?;
479            assert_collisions(&map, 2, 0)?;
480            // Check internal state.
481            let first_collider_idx = ColliderSlotIndex::new(0);
482            let one_collider_entry = [(hash, first_collider_idx.into())];
483            prop_assert_eq!(collect_entries(&map), one_collider_entry);
484            prop_assert_eq!(&*map.colliders, [ptrs[..2].to_owned()]);
485            prop_assert!(map.emptied_collider_slots.is_empty());
486
487            // This exercises "reuse collider slot".
488            for (ptr, i) in ptrs[2..].iter().copied().zip(2..) {
489                // Insert `i = 2..`
490                prop_assert_eq!(map.insert(hash, ptr), false);
491                // Check API state:
492                assert_ptrs_and_len(&mut map, hash, &ptrs[..=i])?;
493                assert_collisions(&map, i + 1, 0)?;
494                // Check internal state.
495                prop_assert_eq!(collect_entries(&map), one_collider_entry);
496                prop_assert_eq!(&*map.colliders, [ptrs[..=i].to_owned()]);
497                prop_assert!(map.emptied_collider_slots.is_empty());
498            }
499
500            // Remove all but the last one.
501            let last = ptrs.len() - 1;
502            for ptr in &ptrs[..last] {
503                prop_assert!(map.remove(hash, *ptr));
504            }
505            // Check API state:
506            assert_ptrs_and_len(&mut map, hash, &ptrs[last..])?;
507            assert_collisions(&map, 0, 1)?;
508            // Check internal state.
509            prop_assert_eq!(collect_entries(&map), [entry(hash, ptrs[last])]);
510            prop_assert_eq!(&*map.colliders, [vec![]]);
511            prop_assert_eq!(&*map.emptied_collider_slots, [first_collider_idx]);
512
513            // Insert `pennultimate` => `last` and `pennultimate` collide.
514            // This exercises "reuse collider slot".
515            let penultimate = last - 1;
516            prop_assert_eq!(map.insert(hash, ptrs[penultimate]), false);
517            // Check API state:
518            let pointers = ptrs[penultimate..].iter().copied().rev().collect::<Vec<_>>();
519            assert_ptrs_and_len(&mut map, hash, &pointers)?;
520            assert_collisions(&map, 2, 0)?;
521            // Check internal state.
522            prop_assert_eq!(collect_entries(&map), one_collider_entry);
523            prop_assert_eq!(&*map.colliders, [pointers]);
524            prop_assert!(map.emptied_collider_slots.is_empty());
525        }
526
527        #[test]
528        fn remove_non_existing_fails((hash, ptr) in (any::<RowHash>(), gen_row_pointer())) {
529            let mut map = PointerMap::default();
530            prop_assert_eq!(map.remove(hash, ptr), false);
531        }
532
533        #[test]
534        fn remove_uncollided_hash_works((hash, ptr) in (any::<RowHash>(), gen_row_pointer())) {
535            let mut map = PointerMap::default();
536
537            // Insert and then remove.
538            prop_assert_eq!(map.insert(hash, ptr), false);
539            prop_assert_eq!(map.remove(hash, ptr), true);
540            // Check API state:
541            assert_ptrs_and_len(&mut map, hash, &[])?;
542            assert_collisions(&map, 0, 0)?;
543            // Check internal state.
544            prop_assert_eq!(collect_entries(&map), []);
545            prop_assert!(map.colliders.is_empty());
546            prop_assert!(map.emptied_collider_slots.is_empty());
547        }
548
549        #[test]
550        fn remove_same_hash_wrong_ptr_fails(
551            (hash, ptr_a, ptr_b) in (
552                any::<RowHash>(),
553                gen_row_pointer(),
554                gen_row_pointer(),
555            )
556        ) {
557            ensure_unique(&[ptr_a, ptr_b])?;
558
559            let mut map = PointerMap::default();
560
561            // Insert `ptr_a` and then remove `ptr_b`.
562            prop_assert_eq!(map.insert(hash, ptr_a), false);
563            prop_assert_eq!(map.remove(hash, ptr_b), false);
564            // Check API state:
565            assert_ptrs_and_len(&mut map, hash, &[ptr_a])?;
566            assert_collisions(&map, 0, 1)?;
567            // Check internal state.
568            prop_assert_eq!(collect_entries(&map), [entry(hash, ptr_a)]);
569            prop_assert!(map.colliders.is_empty());
570            prop_assert!(map.emptied_collider_slots.is_empty());
571        }
572
573        #[test]
574        fn remove_collided_hash_wrong_ptr_fails(
575            (hash, ptrs) in (any::<RowHash>(), vec(gen_row_pointer(), 3..10))
576        ) {
577            ensure_unique(&ptrs)?;
578
579            let mut map = PointerMap::default();
580
581            // Insert `ptrs[0..last]` and then remove `ptrs[last]`.
582            let last = ptrs.len() - 1;
583            let but_last = &ptrs[0..last];
584            for ptr in but_last {
585                prop_assert_eq!(map.insert(hash, *ptr), false);
586            }
587            prop_assert_eq!(map.remove(hash, ptrs[last]), false);
588            // Check API state:
589            assert_ptrs_and_len(&mut map, hash, but_last)?;
590            assert_collisions(&map, but_last.len(), 0)?;
591            // Check internal state.
592            prop_assert_eq!(collect_entries(&map), [(hash, ColliderSlotIndex::new(0).into())]);
593            prop_assert_eq!(&*map.colliders, [but_last.to_owned()]);
594            prop_assert!(map.emptied_collider_slots.is_empty());
595        }
596
597        #[test]
598        fn remove_collided_hash_reduction_works(
599            (hash, mut ptr_a, mut ptr_b, pick_b) in (
600                any::<RowHash>(),
601                gen_row_pointer(),
602                gen_row_pointer(),
603                any::<bool>(),
604            )
605        ) {
606            ensure_unique(&[ptr_a, ptr_b])?;
607
608            // Insert `ptr_a` and `ptr_b`.
609            let mut map = PointerMap::default();
610            prop_assert_eq!(map.insert(hash, ptr_a), false);
611            prop_assert_eq!(map.insert(hash, ptr_b), false);
612            assert_collisions(&map, 2, 0)?;
613
614            // Now remove `ptr_a` or `ptr_b`.
615            if pick_b {
616                mem::swap(&mut ptr_a, &mut ptr_b);
617            }
618            prop_assert_eq!(map.remove(hash, ptr_b), true);
619            assert_ptrs_and_len(&mut map, hash, &[ptr_a])?;
620            assert_collisions(&map, 0, 1)?;
621            prop_assert_eq!(map.emptied_collider_slots, [ColliderSlotIndex(0)]);
622        }
623
624        #[test]
625        fn remove_collided_hash_works(
626            (hash, mut ptrs, pick_remove_idx) in (
627                any::<RowHash>(),
628                vec(gen_row_pointer(), 3..10),
629                0..10usize,
630            )
631        ) {
632            ensure_unique(&ptrs)?;
633
634            let pick_remove_idx = pick_remove_idx.min(ptrs.len() - 1);
635
636            // Insert all in `ptrs`.
637            let mut map = PointerMap::default();
638            for ptr in &ptrs {
639                prop_assert_eq!(map.insert(hash, *ptr), false);
640            }
641            assert_collisions(&map, ptrs.len(), 0)?;
642
643            // Now remove `ptrs[pick_remove_idx]`.
644            let ptr_to_remove = ptrs.remove(pick_remove_idx);
645            prop_assert_eq!(map.remove(hash, ptr_to_remove), true);
646            assert_ptrs_and_len(&mut map, hash, &ptrs)?;
647            assert_collisions(&map, ptrs.len(), 0)?;
648            prop_assert_eq!(sorted(&map.colliders[0]), sorted(&ptrs));
649            prop_assert_eq!(map.emptied_collider_slots, []);
650        }
651    }
652}