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 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 pub fn is_tombstoned(&self, id: u32) -> bool {
62 self.tombstones.contains(&id)
63 }
64
65 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 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); 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 assert_eq!(m.delete("alice"), Some(id));
113 assert_eq!(m.get("alice"), None);
115 assert!(m.is_tombstoned(id));
117 assert_eq!(m.key_of(id), None);
118 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 assert!(m.is_tombstoned(dead_id));
131 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 assert_eq!(m.len(), 2);
147 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}