Skip to main content

reddb_types/
key_dictionary.rs

1//! Per-collection field-name key dictionary (PRD-1398, ADR-0063).
2//!
3//! Homogeneous document collections repeat the same handful of field-name
4//! strings in every document.  This dictionary **interns** those common field
5//! names into a per-collection, **append-only** name↔id catalogue so the
6//! binary body can store a compact key-id (see [`document_body_codec`]) instead
7//! of repeating the string.
8//!
9//! ## Append-only
10//!
11//! Ids are assigned in insertion order and never change or get reused once
12//! assigned, so a key-id baked into a stored document body is stable for the
13//! life of the collection.  This is what lets older document versions (ADR
14//! 0014 MVCC) share the same dictionary as newer ones.
15//!
16//! ## Inline-key fallback
17//!
18//! The dictionary deliberately interns **only common keys**.  Rare or unique
19//! field names (e.g. a heterogeneous collection where almost every document
20//! carries a distinct key) are stored *inline* in the body and never enter the
21//! catalogue, so an adversarial write pattern cannot bloat the shared
22//! dictionary.  The common/rare classification policy lives at the write path;
23//! this module only provides the catalogue primitive.
24//!
25//! ## Flag-dark
26//!
27//! Like the body codec, this is compiled and tested but not yet wired into any
28//! storage path.  Persistence in the collection catalogue happens in a later
29//! PRD-1398 slice.
30//!
31//! [`document_body_codec`]: crate::document_body_codec
32
33use crate::types::{read_varint, write_varint, ValueError};
34use std::collections::HashMap;
35
36/// Magic bytes at the start of a serialized key dictionary.
37pub const MAGIC: &[u8; 4] = b"RKDX";
38
39/// Serialization format version for the dictionary.
40pub const VERSION: u8 = 0x01;
41
42/// Errors produced when (de)serializing a [`KeyDictionary`].
43#[derive(Debug, PartialEq)]
44pub enum KeyDictError {
45    /// Buffer is too short to hold the header or a declared name.
46    TruncatedData,
47    /// First 4 bytes do not match `b"RKDX"`.
48    BadMagic,
49    /// Version byte is not [`VERSION`].
50    UnsupportedVersion(u8),
51    /// A field name is not valid UTF-8.
52    InvalidName,
53    /// A varint length prefix could not be decoded.
54    Varint(ValueError),
55}
56
57impl From<ValueError> for KeyDictError {
58    fn from(e: ValueError) -> Self {
59        KeyDictError::Varint(e)
60    }
61}
62
63impl std::fmt::Display for KeyDictError {
64    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65        match self {
66            Self::TruncatedData => write!(f, "key dictionary: truncated data"),
67            Self::BadMagic => write!(f, "key dictionary: bad magic bytes (expected RKDX)"),
68            Self::UnsupportedVersion(v) => write!(f, "key dictionary: unsupported version {v}"),
69            Self::InvalidName => write!(f, "key dictionary: name is not valid UTF-8"),
70            Self::Varint(e) => write!(f, "key dictionary: varint error: {e}"),
71        }
72    }
73}
74
75impl std::error::Error for KeyDictError {}
76
77/// An append-only field-name↔id catalogue for one document collection.
78///
79/// Ids are dense, assigned `0, 1, 2, …` in interning order, and never reused.
80#[derive(Debug, Clone, Default, PartialEq)]
81pub struct KeyDictionary {
82    /// Field names indexed by their id (`names[id] == name`).
83    names: Vec<String>,
84    /// Reverse lookup: name → id.
85    ids: HashMap<String, u32>,
86}
87
88impl KeyDictionary {
89    /// Create an empty dictionary.
90    pub fn new() -> Self {
91        Self::default()
92    }
93
94    /// Number of interned field names.
95    pub fn len(&self) -> usize {
96        self.names.len()
97    }
98
99    /// Whether the dictionary holds no names.
100    pub fn is_empty(&self) -> bool {
101        self.names.is_empty()
102    }
103
104    /// Intern `name`, returning its (stable) id.
105    ///
106    /// If `name` is already present its existing id is returned and nothing is
107    /// appended; otherwise a new id is assigned at the end of the catalogue.
108    /// This is the **transactional append** used by the write path when it
109    /// classifies a key as common.
110    pub fn intern(&mut self, name: &str) -> u32 {
111        if let Some(&id) = self.ids.get(name) {
112            return id;
113        }
114        let id = self.names.len() as u32;
115        self.names.push(name.to_string());
116        self.ids.insert(name.to_string(), id);
117        id
118    }
119
120    /// Look up the id for an already-interned `name`, if any.
121    ///
122    /// Unlike [`intern`](Self::intern) this never mutates the dictionary, so a
123    /// caller can ask "is this key already common?" without appending.
124    pub fn id_of(&self, name: &str) -> Option<u32> {
125        self.ids.get(name).copied()
126    }
127
128    /// Resolve an id back to its field name, if the id is in range.
129    pub fn name_of(&self, id: u32) -> Option<&str> {
130        self.names.get(id as usize).map(String::as_str)
131    }
132
133    /// Serialize the dictionary into `out` for persistence in the collection
134    /// catalogue.
135    ///
136    /// Names are written in id order, so a decode reconstructs the exact same
137    /// id assignment.
138    pub fn encode(&self, out: &mut Vec<u8>) {
139        out.extend_from_slice(MAGIC);
140        out.push(VERSION);
141        write_varint(out, self.names.len() as u64);
142        for name in &self.names {
143            let bytes = name.as_bytes();
144            write_varint(out, bytes.len() as u64);
145            out.extend_from_slice(bytes);
146        }
147    }
148
149    /// Deserialize a dictionary previously produced by [`encode`](Self::encode).
150    pub fn decode(data: &[u8]) -> Result<Self, KeyDictError> {
151        if data.len() < 5 {
152            return Err(KeyDictError::TruncatedData);
153        }
154        if &data[0..4] != MAGIC.as_slice() {
155            return Err(KeyDictError::BadMagic);
156        }
157        if data[4] != VERSION {
158            return Err(KeyDictError::UnsupportedVersion(data[4]));
159        }
160
161        let mut cursor = 5;
162        let (count, n) = read_varint(&data[cursor..])?;
163        cursor += n;
164
165        let mut dict = KeyDictionary::new();
166        for _ in 0..count {
167            let (len, n) = read_varint(&data[cursor..])?;
168            cursor += n;
169            let end = cursor
170                .checked_add(len as usize)
171                .ok_or(KeyDictError::TruncatedData)?;
172            if end > data.len() {
173                return Err(KeyDictError::TruncatedData);
174            }
175            let name =
176                std::str::from_utf8(&data[cursor..end]).map_err(|_| KeyDictError::InvalidName)?;
177            dict.intern(name);
178            cursor = end;
179        }
180
181        Ok(dict)
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn empty_dictionary() {
191        let dict = KeyDictionary::new();
192        assert!(dict.is_empty());
193        assert_eq!(dict.len(), 0);
194        assert_eq!(dict.id_of("anything"), None);
195        assert_eq!(dict.name_of(0), None);
196    }
197
198    #[test]
199    fn intern_assigns_dense_ids_in_order() {
200        let mut dict = KeyDictionary::new();
201        assert_eq!(dict.intern("name"), 0);
202        assert_eq!(dict.intern("email"), 1);
203        assert_eq!(dict.intern("active"), 2);
204        assert_eq!(dict.len(), 3);
205    }
206
207    #[test]
208    fn intern_is_idempotent_and_append_only() {
209        let mut dict = KeyDictionary::new();
210        let first = dict.intern("status");
211        dict.intern("createdAt");
212        // Re-interning an existing key returns the same id and appends nothing.
213        let again = dict.intern("status");
214        assert_eq!(first, again);
215        assert_eq!(dict.len(), 2);
216    }
217
218    #[test]
219    fn id_of_does_not_mutate() {
220        let mut dict = KeyDictionary::new();
221        dict.intern("a");
222        assert_eq!(dict.id_of("a"), Some(0));
223        // A key that was never interned stays absent — id_of never appends.
224        assert_eq!(dict.id_of("b"), None);
225        assert_eq!(dict.len(), 1);
226    }
227
228    #[test]
229    fn name_of_round_trips_ids() {
230        let mut dict = KeyDictionary::new();
231        dict.intern("first");
232        dict.intern("second");
233        assert_eq!(dict.name_of(0), Some("first"));
234        assert_eq!(dict.name_of(1), Some("second"));
235        assert_eq!(dict.name_of(2), None);
236    }
237
238    #[test]
239    fn encode_decode_round_trip() {
240        let mut dict = KeyDictionary::new();
241        for k in ["id", "name", "email", "país", "🔑"] {
242            dict.intern(k);
243        }
244        let mut buf = Vec::new();
245        dict.encode(&mut buf);
246        let restored = KeyDictionary::decode(&buf).expect("decode");
247        assert_eq!(restored, dict);
248        // Ids are preserved exactly.
249        assert_eq!(restored.id_of("país"), Some(3));
250        assert_eq!(restored.name_of(4), Some("🔑"));
251    }
252
253    #[test]
254    fn empty_dictionary_round_trips() {
255        let dict = KeyDictionary::new();
256        let mut buf = Vec::new();
257        dict.encode(&mut buf);
258        let restored = KeyDictionary::decode(&buf).expect("decode");
259        assert!(restored.is_empty());
260    }
261
262    #[test]
263    fn decode_rejects_bad_magic_and_version() {
264        let mut buf = Vec::new();
265        KeyDictionary::new().encode(&mut buf);
266        let mut bad = buf.clone();
267        bad[0] = b'X';
268        assert_eq!(KeyDictionary::decode(&bad), Err(KeyDictError::BadMagic));
269        let mut bad = buf.clone();
270        bad[4] = 0x99;
271        assert_eq!(
272            KeyDictionary::decode(&bad),
273            Err(KeyDictError::UnsupportedVersion(0x99))
274        );
275    }
276
277    #[test]
278    fn decode_rejects_truncated() {
279        assert_eq!(KeyDictionary::decode(&[]), Err(KeyDictError::TruncatedData));
280        assert_eq!(
281            KeyDictionary::decode(b"RKDX"),
282            Err(KeyDictError::TruncatedData)
283        );
284    }
285}