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 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 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 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 pub fn key_of_historical(&self, id: u32) -> Option<&str> {
58 self.to_key.get(id as usize).map(|s| s.as_str())
59 }
60
61 pub fn rename(&mut self, old: &str, new: &str) -> Result<u32> {
67 if self.to_id.contains_key(new) {
68 return Err(GraphError::DuplicateKey { key: new.into() });
69 }
70 let id = self
71 .to_id
72 .remove(old)
73 .ok_or_else(|| GraphError::KeyNotFound { key: old.into() })?;
74 self.to_id.insert(new.to_string(), id);
75 self.to_key[id as usize] = new.to_string();
76 Ok(id)
77 }
78
79 pub fn delete(&mut self, key: &str) -> Option<u32> {
82 let id = self.to_id.remove(key)?;
83 self.tombstones.insert(id);
84 Some(id)
85 }
86
87 pub fn is_tombstoned(&self, id: u32) -> bool {
89 self.tombstones.contains(&id)
90 }
91
92 pub fn len(&self) -> usize {
95 self.to_key.len()
96 }
97
98 pub(crate) fn all_keys(&self) -> &[String] {
104 &self.to_key
105 }
106
107 pub fn is_empty(&self) -> bool {
108 self.to_key.is_empty()
109 }
110
111 pub fn live_len(&self) -> usize {
113 self.to_id.len()
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 #[test]
122 fn ids_are_dense_and_stable() {
123 let mut m = IdMap::new();
124 assert_eq!(m.get_or_insert("a"), 0);
125 assert_eq!(m.get_or_insert("b"), 1);
126 assert_eq!(m.get_or_insert("a"), 0); assert_eq!(m.get("b"), Some(1));
128 assert_eq!(m.get("zzz"), None);
129 assert_eq!(m.key_of(1), Some("b"));
130 assert_eq!(m.key_of(9), None);
131 assert_eq!(m.len(), 2);
132 }
133
134 #[test]
135 fn survives_serde_roundtrip() {
136 let mut m = IdMap::new();
137 m.get_or_insert("x");
138 let back: IdMap = bincode::deserialize(&bincode::serialize(&m).unwrap()).unwrap();
139 assert_eq!(back.get("x"), Some(0));
140 assert_eq!(back.len(), 1);
141 }
142
143 #[test]
144 fn delete_makes_key_invisible_and_id_tombstoned() {
145 let mut m = IdMap::new();
146 let id = m.get_or_insert("alice");
147 assert_eq!(m.delete("alice"), Some(id));
149 assert_eq!(m.get("alice"), None);
151 assert!(m.is_tombstoned(id));
153 assert_eq!(m.key_of(id), None);
154 assert_eq!(m.delete("nobody"), None);
156 }
157
158 #[test]
159 fn reinsert_after_delete_gets_fresh_id() {
160 let mut m = IdMap::new();
161 let dead_id = m.get_or_insert("alice");
162 m.delete("alice");
163 let new_id = m.get_or_insert("alice");
164 assert_ne!(new_id, dead_id);
165 assert!(m.is_tombstoned(dead_id));
167 assert!(!m.is_tombstoned(new_id));
169 assert_eq!(m.get("alice"), Some(new_id));
170 assert_eq!(m.key_of(new_id), Some("alice"));
171 }
172
173 #[test]
174 fn live_len_tracks_live_entries() {
175 let mut m = IdMap::new();
176 m.get_or_insert("a");
177 m.get_or_insert("b");
178 assert_eq!(m.live_len(), 2);
179 m.delete("a");
180 assert_eq!(m.live_len(), 1);
181 assert_eq!(m.len(), 2);
183 m.get_or_insert("a");
185 assert_eq!(m.live_len(), 2);
186 assert_eq!(m.len(), 3);
187 }
188
189 #[test]
190 fn serde_roundtrip_preserves_tombstones() {
191 let mut m = IdMap::new();
192 m.get_or_insert("x");
193 let dead = m.get_or_insert("y");
194 m.delete("y");
195 let bytes = bincode::serialize(&m).unwrap();
196 let back: IdMap = bincode::deserialize(&bytes).unwrap();
197 assert!(back.is_tombstoned(dead));
198 assert_eq!(back.get("y"), None);
199 assert_eq!(back.key_of(dead), None);
200 assert_eq!(back.live_len(), 1);
201 assert_eq!(back.len(), 2);
202 }
203
204 #[test]
205 fn try_insert_fails_when_u32_space_exhausted() {
206 assert!(dense_id(u32::MAX as usize + 1).is_err());
207 assert_eq!(dense_id(0).unwrap(), 0);
208 assert_eq!(dense_id(u32::MAX as usize).unwrap(), u32::MAX);
209 let mut m = IdMap::new();
210 assert_eq!(m.try_insert("a").unwrap(), 0);
211 assert_eq!(m.try_insert("a").unwrap(), 0);
212 }
213}