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 fn a_full_table_refuses_rather_than_failing_the_write() {
182 let mut keys = Keys::with_capacity(KEYS_MAX);
183 for i in 0..KEYS_MAX {
184 let name = format!("f{i}");
185 assert_eq!(keys.intern(name.as_bytes()), Some(i as u16));
186 }
187 assert!(keys.is_full());
188 assert_eq!(keys.intern(b"one too many"), None);
189 assert_eq!(
190 keys.id(b"f0"),
191 Some(0),
192 "a full table still answers for what is in it"
193 );
194 }
195}