Skip to main content

core_storage/
idmap.rs

1use crate::types::{GraphError, Result};
2use serde::{Deserialize, Serialize};
3use std::collections::{BTreeSet, HashMap};
4
5fn dense_id(len: usize) -> Result<u32> {
6    u32::try_from(len).map_err(|_| GraphError::Corrupt {
7        detail: "id space exhausted".into(),
8    })
9}
10
11#[derive(Debug, Default, Clone, Serialize, Deserialize)]
12pub struct IdMap {
13    to_id: HashMap<String, u32>,
14    to_key: Vec<String>,
15    /// Dense ids permanently retired by `delete`. Never reused.
16    tombstones: BTreeSet<u32>,
17}
18
19impl IdMap {
20    pub fn new() -> Self {
21        Self::default()
22    }
23
24    pub fn get_or_insert(&mut self, key: &str) -> u32 {
25        self.try_insert(key).expect("id space exhausted")
26    }
27
28    /// Allocate a dense id for `key`, or return the existing live id.
29    /// Fails before wrap when the next id would not fit in `u32`.
30    pub fn try_insert(&mut self, key: &str) -> Result<u32> {
31        if let Some(&id) = self.to_id.get(key) {
32            return Ok(id);
33        }
34        let id = dense_id(self.to_key.len())?;
35        self.to_id.insert(key.to_string(), id);
36        self.to_key.push(key.to_string());
37        Ok(id)
38    }
39
40    pub fn get(&self, key: &str) -> Option<u32> {
41        // to_id is cleared on delete so this naturally returns None for deleted keys.
42        self.to_id.get(key).copied()
43    }
44
45    pub fn key_of(&self, id: u32) -> Option<&str> {
46        if self.tombstones.contains(&id) {
47            return None;
48        }
49        self.to_key.get(id as usize).map(|s| s.as_str())
50    }
51
52    /// Remove `key` from the live map, permanently tombstone its dense id, and
53    /// return that id. Returns `None` if the key is not present.
54    pub fn delete(&mut self, key: &str) -> Option<u32> {
55        let id = self.to_id.remove(key)?;
56        self.tombstones.insert(id);
57        Some(id)
58    }
59
60    /// Returns `true` if `id` has been retired by a prior `delete` call.
61    pub fn is_tombstoned(&self, id: u32) -> bool {
62        self.tombstones.contains(&id)
63    }
64
65    /// Number of total id slots ever allocated (live + tombstoned). Stable across
66    /// deletes and re-inserts — use `live_len` for the live count.
67    pub fn len(&self) -> usize {
68        self.to_key.len()
69    }
70
71    pub fn is_empty(&self) -> bool {
72        self.to_key.is_empty()
73    }
74
75    /// Number of currently live (non-tombstoned) entries.
76    pub fn live_len(&self) -> usize {
77        self.to_id.len()
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn ids_are_dense_and_stable() {
87        let mut m = IdMap::new();
88        assert_eq!(m.get_or_insert("a"), 0);
89        assert_eq!(m.get_or_insert("b"), 1);
90        assert_eq!(m.get_or_insert("a"), 0); // idempotent
91        assert_eq!(m.get("b"), Some(1));
92        assert_eq!(m.get("zzz"), None);
93        assert_eq!(m.key_of(1), Some("b"));
94        assert_eq!(m.key_of(9), None);
95        assert_eq!(m.len(), 2);
96    }
97
98    #[test]
99    fn survives_serde_roundtrip() {
100        let mut m = IdMap::new();
101        m.get_or_insert("x");
102        let back: IdMap = bincode::deserialize(&bincode::serialize(&m).unwrap()).unwrap();
103        assert_eq!(back.get("x"), Some(0));
104        assert_eq!(back.len(), 1);
105    }
106
107    #[test]
108    fn delete_makes_key_invisible_and_id_tombstoned() {
109        let mut m = IdMap::new();
110        let id = m.get_or_insert("alice");
111        // delete returns the dead id
112        assert_eq!(m.delete("alice"), Some(id));
113        // key is gone
114        assert_eq!(m.get("alice"), None);
115        // id is tombstoned
116        assert!(m.is_tombstoned(id));
117        assert_eq!(m.key_of(id), None);
118        // deleting absent key → None
119        assert_eq!(m.delete("nobody"), None);
120    }
121
122    #[test]
123    fn reinsert_after_delete_gets_fresh_id() {
124        let mut m = IdMap::new();
125        let dead_id = m.get_or_insert("alice");
126        m.delete("alice");
127        let new_id = m.get_or_insert("alice");
128        assert_ne!(new_id, dead_id);
129        // old id still tombstoned
130        assert!(m.is_tombstoned(dead_id));
131        // new id is live
132        assert!(!m.is_tombstoned(new_id));
133        assert_eq!(m.get("alice"), Some(new_id));
134        assert_eq!(m.key_of(new_id), Some("alice"));
135    }
136
137    #[test]
138    fn live_len_tracks_live_entries() {
139        let mut m = IdMap::new();
140        m.get_or_insert("a");
141        m.get_or_insert("b");
142        assert_eq!(m.live_len(), 2);
143        m.delete("a");
144        assert_eq!(m.live_len(), 1);
145        // len() is total slots ever allocated
146        assert_eq!(m.len(), 2);
147        // re-insert "a" → new slot, live_len back to 2, len = 3
148        m.get_or_insert("a");
149        assert_eq!(m.live_len(), 2);
150        assert_eq!(m.len(), 3);
151    }
152
153    #[test]
154    fn serde_roundtrip_preserves_tombstones() {
155        let mut m = IdMap::new();
156        m.get_or_insert("x");
157        let dead = m.get_or_insert("y");
158        m.delete("y");
159        let bytes = bincode::serialize(&m).unwrap();
160        let back: IdMap = bincode::deserialize(&bytes).unwrap();
161        assert!(back.is_tombstoned(dead));
162        assert_eq!(back.get("y"), None);
163        assert_eq!(back.key_of(dead), None);
164        assert_eq!(back.live_len(), 1);
165        assert_eq!(back.len(), 2);
166    }
167
168    #[test]
169    fn try_insert_fails_when_u32_space_exhausted() {
170        assert!(dense_id(u32::MAX as usize + 1).is_err());
171        assert_eq!(dense_id(0).unwrap(), 0);
172        assert_eq!(dense_id(u32::MAX as usize).unwrap(), u32::MAX);
173        let mut m = IdMap::new();
174        assert_eq!(m.try_insert("a").unwrap(), 0);
175        assert_eq!(m.try_insert("a").unwrap(), 0);
176    }
177}