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    /// Convenience wrapper around [`Self::try_insert`] for call-sites that do not
25    /// return `Result`.  Panics only when the u32 id space is exhausted (> 4 billion
26    /// distinct node keys inserted without restart).  Converting this to return
27    /// `Result<u32>` requires a public API change — tracked as a TODO-0.4.2 task.
28    pub fn get_or_insert(&mut self, key: &str) -> u32 {
29        self.try_insert(key).expect("id space exhausted")
30    }
31
32    /// Allocate a dense id for `key`, or return the existing live id.
33    /// Fails before wrap when the next id would not fit in `u32`.
34    pub fn try_insert(&mut self, key: &str) -> Result<u32> {
35        if let Some(&id) = self.to_id.get(key) {
36            return Ok(id);
37        }
38        let id = dense_id(self.to_key.len())?;
39        self.to_id.insert(key.to_string(), id);
40        self.to_key.push(key.to_string());
41        Ok(id)
42    }
43
44    pub fn get(&self, key: &str) -> Option<u32> {
45        // to_id is cleared on delete so this naturally returns None for deleted keys.
46        self.to_id.get(key).copied()
47    }
48
49    pub fn key_of(&self, id: u32) -> Option<&str> {
50        if self.tombstones.contains(&id) {
51            return None;
52        }
53        self.to_key.get(id as usize).map(|s| s.as_str())
54    }
55
56    /// Like `key_of`, but also resolves tombstoned ids.
57    ///
58    /// Use only for historical WAL scan paths (e.g. `edge_history`) where the
59    /// goal is to reconstruct what existed in the past, not the current live
60    /// state. All other callers should use `key_of`.
61    pub fn key_of_historical(&self, id: u32) -> Option<&str> {
62        self.to_key.get(id as usize).map(|s| s.as_str())
63    }
64
65    /// Rename a live key, keeping its dense id stable.
66    ///
67    /// Returns `Err(KeyNotFound)` if `old` is unknown or tombstoned.
68    /// Returns `Err(DuplicateKey)` if `new` is already a live key.
69    /// On success returns the stable id shared by both names.
70    pub fn rename(&mut self, old: &str, new: &str) -> Result<u32> {
71        if self.to_id.contains_key(new) {
72            return Err(GraphError::DuplicateKey { key: new.into() });
73        }
74        let id = self
75            .to_id
76            .remove(old)
77            .ok_or_else(|| GraphError::KeyNotFound { key: old.into() })?;
78        self.to_id.insert(new.to_string(), id);
79        self.to_key[id as usize] = new.to_string();
80        Ok(id)
81    }
82
83    /// Remove `key` from the live map, permanently tombstone its dense id, and
84    /// return that id. Returns `None` if the key is not present.
85    pub fn delete(&mut self, key: &str) -> Option<u32> {
86        let id = self.to_id.remove(key)?;
87        self.tombstones.insert(id);
88        Some(id)
89    }
90
91    /// Returns `true` if `id` has been retired by a prior `delete` call.
92    pub fn is_tombstoned(&self, id: u32) -> bool {
93        self.tombstones.contains(&id)
94    }
95
96    /// Number of total id slots ever allocated (live + tombstoned). Stable across
97    /// deletes and re-inserts — use `live_len` for the live count.
98    pub fn len(&self) -> usize {
99        self.to_key.len()
100    }
101
102    /// All allocated key slots in dense-id order (index = id).
103    ///
104    /// Tombstoned slots retain their original key string so the V8 encoder
105    /// can round-trip the full allocation history.  Callers must check
106    /// `is_tombstoned(id)` to distinguish live from retired slots.
107    pub(crate) fn all_keys(&self) -> &[String] {
108        &self.to_key
109    }
110
111    pub fn is_empty(&self) -> bool {
112        self.to_key.is_empty()
113    }
114
115    /// Number of currently live (non-tombstoned) entries.
116    pub fn live_len(&self) -> usize {
117        self.to_id.len()
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn ids_are_dense_and_stable() {
127        let mut m = IdMap::new();
128        assert_eq!(m.get_or_insert("a"), 0);
129        assert_eq!(m.get_or_insert("b"), 1);
130        assert_eq!(m.get_or_insert("a"), 0); // idempotent
131        assert_eq!(m.get("b"), Some(1));
132        assert_eq!(m.get("zzz"), None);
133        assert_eq!(m.key_of(1), Some("b"));
134        assert_eq!(m.key_of(9), None);
135        assert_eq!(m.len(), 2);
136    }
137
138    #[test]
139    fn survives_serde_roundtrip() {
140        let mut m = IdMap::new();
141        m.get_or_insert("x");
142        let back: IdMap = bincode::deserialize(&bincode::serialize(&m).unwrap()).unwrap();
143        assert_eq!(back.get("x"), Some(0));
144        assert_eq!(back.len(), 1);
145    }
146
147    #[test]
148    fn delete_makes_key_invisible_and_id_tombstoned() {
149        let mut m = IdMap::new();
150        let id = m.get_or_insert("alice");
151        // delete returns the dead id
152        assert_eq!(m.delete("alice"), Some(id));
153        // key is gone
154        assert_eq!(m.get("alice"), None);
155        // id is tombstoned
156        assert!(m.is_tombstoned(id));
157        assert_eq!(m.key_of(id), None);
158        // deleting absent key → None
159        assert_eq!(m.delete("nobody"), None);
160    }
161
162    #[test]
163    fn reinsert_after_delete_gets_fresh_id() {
164        let mut m = IdMap::new();
165        let dead_id = m.get_or_insert("alice");
166        m.delete("alice");
167        let new_id = m.get_or_insert("alice");
168        assert_ne!(new_id, dead_id);
169        // old id still tombstoned
170        assert!(m.is_tombstoned(dead_id));
171        // new id is live
172        assert!(!m.is_tombstoned(new_id));
173        assert_eq!(m.get("alice"), Some(new_id));
174        assert_eq!(m.key_of(new_id), Some("alice"));
175    }
176
177    #[test]
178    fn live_len_tracks_live_entries() {
179        let mut m = IdMap::new();
180        m.get_or_insert("a");
181        m.get_or_insert("b");
182        assert_eq!(m.live_len(), 2);
183        m.delete("a");
184        assert_eq!(m.live_len(), 1);
185        // len() is total slots ever allocated
186        assert_eq!(m.len(), 2);
187        // re-insert "a" → new slot, live_len back to 2, len = 3
188        m.get_or_insert("a");
189        assert_eq!(m.live_len(), 2);
190        assert_eq!(m.len(), 3);
191    }
192
193    #[test]
194    fn serde_roundtrip_preserves_tombstones() {
195        let mut m = IdMap::new();
196        m.get_or_insert("x");
197        let dead = m.get_or_insert("y");
198        m.delete("y");
199        let bytes = bincode::serialize(&m).unwrap();
200        let back: IdMap = bincode::deserialize(&bytes).unwrap();
201        assert!(back.is_tombstoned(dead));
202        assert_eq!(back.get("y"), None);
203        assert_eq!(back.key_of(dead), None);
204        assert_eq!(back.live_len(), 1);
205        assert_eq!(back.len(), 2);
206    }
207
208    #[test]
209    fn try_insert_fails_when_u32_space_exhausted() {
210        assert!(dense_id(u32::MAX as usize + 1).is_err());
211        assert_eq!(dense_id(0).unwrap(), 0);
212        assert_eq!(dense_id(u32::MAX as usize).unwrap(), u32::MAX);
213        let mut m = IdMap::new();
214        assert_eq!(m.try_insert("a").unwrap(), 0);
215        assert_eq!(m.try_insert("a").unwrap(), 0);
216    }
217}