Skip to main content

akar_storage/
string_dictionary.rs

1//! String dictionary encoding for efficient string storage.
2//!
3//! Dictionary encoding maps strings to integer IDs, storing each unique
4//! string only once in a dictionary. Repeated strings reference the dict
5//! by ID, reducing storage for low-cardinality string columns.
6
7use std::collections::HashMap;
8use std::rc::Rc;
9
10/// A dictionary-encoded string column.
11#[derive(Debug, Clone)]
12pub struct StringDictionary {
13    /// The dictionary: string_id -> string value. Each unique string is stored
14    /// once via `Rc<str>` and shared with the reverse-lookup map key, so the
15    /// bytes of a string have a single heap allocation (P79).
16    strings: Vec<Rc<str>>,
17    /// Reverse lookup: string value -> string_id. Keys share the heap
18    /// allocation of the corresponding `strings` entry.
19    lookup: HashMap<Rc<str>, u32>,
20}
21
22impl StringDictionary {
23    /// Create a new empty dictionary.
24    pub fn new() -> Self {
25        Self {
26            strings: Vec::new(),
27            lookup: HashMap::new(),
28        }
29    }
30
31    /// Encode a batch of strings, returning (dictionary, encoded_ids).
32    ///
33    /// The dictionary contains unique strings. `encoded_ids` is a `Vec<u32>`
34    /// where each entry is the dictionary ID for the corresponding input string.
35    /// Unknown/NULL strings get ID `u32::MAX`.
36    pub fn encode(strings: &[Option<&str>]) -> (Self, Vec<u32>) {
37        let mut dict = Self::new();
38        let mut ids = Vec::with_capacity(strings.len());
39        for s in strings {
40            let id = match s {
41                None => u32::MAX,
42                Some(val) => dict.intern(val),
43            };
44            ids.push(id);
45        }
46        (dict, ids)
47    }
48
49    /// Insert a single string and return its ID.
50    pub fn intern(&mut self, s: &str) -> u32 {
51        if let Some(&id) = self.lookup.get(s) {
52            return id;
53        }
54        let id = self.strings.len() as u32;
55        let owned: Rc<str> = Rc::from(s);
56        self.strings.push(Rc::clone(&owned));
57        self.lookup.insert(owned, id);
58        id
59    }
60
61    /// Look up a string by ID. Returns `None` if ID is out of range.
62    pub fn lookup(&self, id: u32) -> Option<&str> {
63        self.strings.get(id as usize).map(|s| s.as_ref())
64    }
65
66    /// Look up a string value and return its ID. Returns `None` if not found.
67    pub fn lookup_id(&self, s: &str) -> Option<u32> {
68        self.lookup.get(s).copied()
69    }
70
71    /// Return the number of unique strings in the dictionary.
72    pub fn len(&self) -> usize {
73        self.strings.len()
74    }
75
76    /// Check if the dictionary is empty.
77    pub fn is_empty(&self) -> bool {
78        self.strings.is_empty()
79    }
80
81    /// Serialize the dictionary to bytes.
82    ///
83    /// Format: `[num_strings: u32][for each string: [len: u32][bytes...]]`
84    pub fn serialize(&self) -> Vec<u8> {
85        let mut buf = Vec::new();
86        buf.extend_from_slice(&(self.strings.len() as u32).to_le_bytes());
87        for s in &self.strings {
88            buf.extend_from_slice(&(s.len() as u32).to_le_bytes());
89            buf.extend_from_slice(s.as_bytes());
90        }
91        buf
92    }
93
94    /// Deserialize a dictionary from bytes.
95    pub fn deserialize(data: &[u8]) -> std::io::Result<Self> {
96        if data.len() < 4 {
97            return Err(std::io::Error::new(
98                std::io::ErrorKind::UnexpectedEof,
99                "data too short for dictionary header",
100            ));
101        }
102        let num_strings = u32::from_le_bytes(data[..4].try_into().unwrap()) as usize;
103        let mut dict = Self::with_capacity(num_strings);
104        let mut offset = 4usize;
105        for _ in 0..num_strings {
106            if offset + 4 > data.len() {
107                return Err(std::io::Error::new(
108                    std::io::ErrorKind::UnexpectedEof,
109                    "data too short for string length",
110                ));
111            }
112            let len = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize;
113            offset += 4;
114            if offset + len > data.len() {
115                return Err(std::io::Error::new(
116                    std::io::ErrorKind::UnexpectedEof,
117                    "data too short for string content",
118                ));
119            }
120            let s = String::from_utf8(data[offset..offset + len].to_vec())
121                .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
122            offset += len;
123            let id = dict.strings.len() as u32;
124            let owned: Rc<str> = Rc::from(s);
125            dict.strings.push(Rc::clone(&owned));
126            dict.lookup.insert(owned, id);
127        }
128        Ok(dict)
129    }
130
131    /// Compute the memory usage in bytes.
132    ///
133    /// String bytes are counted once: the `Rc<str>` in `strings` and the map key
134    /// share a single heap allocation, so the reverse-lookup keys add only the
135    /// `Rc` pointer/refcount overhead, not a second copy of the data.
136    pub fn memory_usage(&self) -> usize {
137        let mut total = std::mem::size_of::<Self>();
138        total += self.strings.capacity() * std::mem::size_of::<Rc<str>>();
139        for s in &self.strings {
140            total += std::mem::size_of::<Rc<str>>() + s.len();
141        }
142        total += self.lookup.capacity() * std::mem::size_of::<(Rc<str>, u32)>();
143        total
144    }
145
146    fn with_capacity(cap: usize) -> Self {
147        Self {
148            strings: Vec::with_capacity(cap),
149            lookup: HashMap::with_capacity(cap),
150        }
151    }
152}
153
154impl Default for StringDictionary {
155    fn default() -> Self {
156        Self::new()
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    #[test]
165    fn test_encode_decode_roundtrip() {
166        let input = [
167            Some("apple"),
168            Some("banana"),
169            Some("apple"),
170            Some("cherry"),
171            Some("banana"),
172        ];
173        let (dict, ids) = StringDictionary::encode(&input);
174        assert_eq!(dict.len(), 3);
175        for (i, s) in input.iter().enumerate() {
176            let expected = ids[i];
177            if let Some(val) = s {
178                assert_eq!(dict.lookup(expected), Some(*val));
179            }
180        }
181    }
182
183    #[test]
184    fn test_intern_dedup() {
185        let mut dict = StringDictionary::new();
186        let id1 = dict.intern("hello");
187        let id2 = dict.intern("hello");
188        assert_eq!(id1, id2);
189        let id3 = dict.intern("world");
190        assert_ne!(id1, id3);
191        assert_eq!(dict.len(), 2);
192    }
193
194    #[test]
195    fn test_serialize_roundtrip() {
196        let mut dict = StringDictionary::new();
197        dict.intern("alpha");
198        dict.intern("beta");
199        dict.intern("gamma");
200        let bytes = dict.serialize();
201        let deserialized = StringDictionary::deserialize(&bytes).unwrap();
202        assert_eq!(deserialized.len(), dict.len());
203        assert_eq!(deserialized.lookup_id("alpha"), Some(0));
204        assert_eq!(deserialized.lookup_id("beta"), Some(1));
205        assert_eq!(deserialized.lookup_id("gamma"), Some(2));
206        assert_eq!(deserialized.lookup_id("delta"), None);
207    }
208
209    #[test]
210    fn test_empty_input() {
211        let input: &[Option<&str>] = &[];
212        let (dict, ids) = StringDictionary::encode(input);
213        assert!(dict.is_empty());
214        assert!(ids.is_empty());
215    }
216
217    #[test]
218    fn test_null_handling() {
219        let input = [Some("a"), None, Some("b"), None];
220        let (dict, ids) = StringDictionary::encode(&input);
221        assert_eq!(dict.len(), 2);
222        assert_eq!(ids[0], 0);
223        assert_eq!(ids[1], u32::MAX);
224        assert_eq!(ids[2], 1);
225        assert_eq!(ids[3], u32::MAX);
226    }
227
228    #[test]
229    fn test_lookup_miss() {
230        let mut dict = StringDictionary::new();
231        dict.intern("foo");
232        assert_eq!(dict.lookup(0), Some("foo"));
233        assert_eq!(dict.lookup(1), None);
234        assert_eq!(dict.lookup(u32::MAX), None);
235        assert_eq!(dict.lookup_id("bar"), None);
236    }
237
238    #[test]
239    fn test_memory_usage() {
240        let mut dict = StringDictionary::new();
241        dict.intern("short");
242        dict.intern("a longer string value");
243        let usage = dict.memory_usage();
244        assert!(usage > 0);
245        assert!(usage > std::mem::size_of::<StringDictionary>());
246    }
247
248    #[test]
249    fn test_deserialize_empty() {
250        let bytes = 0u32.to_le_bytes().to_vec();
251        let dict = StringDictionary::deserialize(&bytes).unwrap();
252        assert!(dict.is_empty());
253        assert_eq!(dict.len(), 0);
254    }
255
256    #[test]
257    fn test_deserialize_truncated() {
258        let result = StringDictionary::deserialize(&[1, 0, 0, 0]);
259        assert!(result.is_err());
260    }
261
262    #[test]
263    fn test_deserialize_invalid_utf8() {
264        let bytes = [1u8, 0, 0, 0, 1, 0, 0, 0, 0xFF];
265        let result = StringDictionary::deserialize(&bytes);
266        assert!(result.is_err());
267    }
268
269    #[test]
270    fn test_empty_string() {
271        let mut dict = StringDictionary::new();
272        let id = dict.intern("");
273        assert_eq!(dict.lookup(id), Some(""));
274        assert_eq!(dict.lookup_id(""), Some(id));
275    }
276
277    #[test]
278    fn test_compression_integration() {
279        let dict = StringDictionary::new();
280        let serialized = dict.serialize();
281        let chunk = crate::compression::compress(akar_common::enums::CompressionType::StringDictionary, &serialized, 0);
282        let decompressed = crate::compression::decompress(&chunk, serialized.len());
283        let deserialized = StringDictionary::deserialize(&decompressed).unwrap();
284        assert!(deserialized.is_empty());
285    }
286}