Skip to main content

yo_doc/
keys.rs

1//! The per collection key table, which is what makes an interned object's keys
2//! two bytes each (`09` section 4).
3//!
4//! A document collection stores the same field names on every document it
5//! holds. A collection of a million orders with twenty fields each holds twenty
6//! million copies of twenty strings, and that is most of what the collection
7//! costs before anything useful is in it. Interning turns the twenty million
8//! copies into twenty strings and twenty million two byte ids, and it turns a
9//! member lookup from a comparison of bytes into a comparison of integers.
10//!
11//! The table is an [`Elements`] with nothing stored against a name, which is
12//! the same structure a set is, and an id is a row index in it. That works
13//! because a name is never taken out: see [`Keys`] for why not.
14//!
15//! ```
16//! use yo_doc::Keys;
17//!
18//! let mut keys = Keys::new();
19//! let id = keys.intern(b"customer").expect("room");
20//! assert_eq!(keys.id(b"customer"), Some(id));
21//! assert_eq!(keys.name(id), Some(&b"customer"[..]));
22//! assert_eq!(keys.intern(b"customer"), Some(id), "a name gets one id, ever");
23//! ```
24
25use yo_kv::Elements;
26
27/// How many names one collection can intern.
28///
29/// An id is two bytes because that is what an interned object's key entry is,
30/// so the table stops at 65536 names. A collection that reaches it is not a
31/// collection of documents any more, it is a collection of a schema per row,
32/// and [`Keys::intern`] answers `None` so the writer can store that document
33/// with its keys as bytes rather than fail the write.
34pub const KEYS_MAX: usize = 1 << 16;
35
36/// The names one collection has interned, and the ids it gave them.
37///
38/// # Why nothing is ever removed
39///
40/// An id is the row index the name sits at, which costs the table nothing at
41/// all: no second array from id to row, no free list, no generation counter. It
42/// holds only while the rows do not move, and [`Elements`] moves its last row
43/// into the hole when something is taken out, so a removal here would silently
44/// repoint every document that used the moved name.
45///
46/// Never removing is the right answer rather than a limitation being tolerated.
47/// A field name that no document uses any more costs its bytes once and two
48/// bytes of nothing in the row array, and the alternative is either an
49/// indirection on every lookup forever or a scan of the whole collection to
50/// find out whether a name is still wanted. The table is capped at
51/// [`KEYS_MAX`] names, so the worst case is bounded and small.
52#[derive(Debug, Clone, Default)]
53pub struct Keys {
54    names: Elements<()>,
55}
56
57impl Keys {
58    /// An empty table that has not allocated anything yet.
59    #[must_use]
60    pub fn new() -> Keys {
61        Keys {
62            names: Elements::new(),
63        }
64    }
65
66    /// An empty table with room for `n` names already taken.
67    #[must_use]
68    pub fn with_capacity(n: usize) -> Keys {
69        Keys {
70            names: Elements::with_capacity(n.min(KEYS_MAX)),
71        }
72    }
73
74    /// The id of `name`, giving it one if it does not have one yet.
75    ///
76    /// `None` means the table is full or the name is longer than a name may be,
77    /// and in both cases the caller writes the document with its keys as bytes
78    /// instead. That is always safe, because the interned flag is per container
79    /// and not per collection, so a collection can hold both kinds at once and
80    /// everything already written stays readable.
81    pub fn intern(&mut self, name: &[u8]) -> Option<u16> {
82        if let Some(id) = self.id(name) {
83            return Some(id);
84        }
85        if self.names.len() >= KEYS_MAX {
86            return None;
87        }
88        let id = u16::try_from(self.names.len()).expect("the length is under KEYS_MAX");
89        self.names.insert(name, ()).ok()?;
90        Some(id)
91    }
92
93    /// The id `name` already has, without giving it one.
94    ///
95    /// This is the read path: a lookup by name against an interned document
96    /// resolves the name here once and then searches the document by id.
97    #[must_use]
98    pub fn id(&self, name: &[u8]) -> Option<u16> {
99        let at = self.names.index_of(name)?;
100        u16::try_from(at).ok()
101    }
102
103    /// The name behind an id.
104    #[must_use]
105    pub fn name(&self, id: u16) -> Option<&[u8]> {
106        self.names.at(usize::from(id)).map(|(name, ())| name)
107    }
108
109    /// How many names are interned.
110    #[must_use]
111    pub fn len(&self) -> usize {
112        self.names.len()
113    }
114
115    /// Whether nothing has been interned yet.
116    #[must_use]
117    pub fn is_empty(&self) -> bool {
118        self.names.is_empty()
119    }
120
121    /// Whether the next new name would be refused.
122    #[must_use]
123    pub fn is_full(&self) -> bool {
124        self.names.len() >= KEYS_MAX
125    }
126
127    /// Every name and its id, in id order.
128    pub fn iter(&self) -> impl Iterator<Item = (&[u8], u16)> {
129        self.names
130            .iter()
131            .enumerate()
132            .map(|(at, (name, ()))| (name, at as u16))
133    }
134
135    /// What the table costs.
136    #[must_use]
137    pub fn memory_bytes(&self) -> usize {
138        self.names.memory_bytes()
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn a_name_keeps_the_id_it_was_given() {
148        let mut keys = Keys::new();
149        let a = keys.intern(b"alpha").expect("room");
150        let b = keys.intern(b"beta").expect("room");
151        assert_ne!(a, b);
152        assert_eq!(keys.intern(b"alpha"), Some(a));
153        assert_eq!(keys.intern(b"beta"), Some(b));
154        assert_eq!(keys.len(), 2);
155    }
156
157    #[test]
158    fn ids_come_out_in_the_order_they_were_handed_out() {
159        let mut keys = Keys::new();
160        for i in 0..64u16 {
161            let name = format!("field{i}");
162            assert_eq!(keys.intern(name.as_bytes()), Some(i));
163        }
164        for (i, (name, id)) in keys.iter().enumerate() {
165            assert_eq!(id, i as u16);
166            assert_eq!(name, format!("field{i}").as_bytes());
167            assert_eq!(keys.name(id), Some(name));
168        }
169    }
170
171    #[test]
172    fn a_name_nobody_interned_has_no_id() {
173        let mut keys = Keys::new();
174        keys.intern(b"here").expect("room");
175        assert_eq!(keys.id(b"not here"), None);
176        assert_eq!(keys.name(1), None);
177        assert_eq!(keys.len(), 1, "asking did not add it");
178    }
179
180    #[test]
181    #[cfg_attr(
182        miri,
183        ignore = "a full table is the claim and a full table is 65536 names"
184    )]
185    fn a_full_table_refuses_rather_than_failing_the_write() {
186        let mut keys = Keys::with_capacity(KEYS_MAX);
187        for i in 0..KEYS_MAX {
188            let name = format!("f{i}");
189            assert_eq!(keys.intern(name.as_bytes()), Some(i as u16));
190        }
191        assert!(keys.is_full());
192        assert_eq!(keys.intern(b"one too many"), None);
193        assert_eq!(
194            keys.id(b"f0"),
195            Some(0),
196            "a full table still answers for what is in it"
197        );
198    }
199}