1pub const INLINE_LIMIT: usize = 12;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
31pub struct StringView {
32 length: u32,
33 payload: [u8; 12],
34}
35
36impl StringView {
37 #[must_use]
44 pub fn inline(text: &str) -> Self {
45 assert!(text.len() <= INLINE_LIMIT, "a string of {} bytes is not inline", text.len());
46 let mut payload = [0u8; 12];
47 payload[..text.len()].copy_from_slice(text.as_bytes());
48 Self { length: text.len() as u32, payload }
49 }
50
51 fn indirect(text: &str, block: u32, offset: u32) -> Self {
53 let mut payload = [0u8; 12];
54 payload[..4].copy_from_slice(&text.as_bytes()[..4]);
55 payload[4..8].copy_from_slice(&block.to_le_bytes());
56 payload[8..].copy_from_slice(&offset.to_le_bytes());
57 Self { length: text.len() as u32, payload }
58 }
59
60 #[must_use]
62 pub fn len(&self) -> usize {
63 self.length as usize
64 }
65
66 #[must_use]
68 pub fn is_empty(&self) -> bool {
69 self.length == 0
70 }
71
72 #[must_use]
74 pub fn is_inline(&self) -> bool {
75 self.len() <= INLINE_LIMIT
76 }
77
78 #[must_use]
84 pub fn prefix(&self) -> [u8; 4] {
85 [self.payload[0], self.payload[1], self.payload[2], self.payload[3]]
86 }
87
88 #[must_use]
90 pub fn as_inline_str(&self) -> Option<&str> {
91 if !self.is_inline() {
92 return None;
93 }
94 std::str::from_utf8(&self.payload[..self.len()]).ok()
97 }
98
99 fn block(&self) -> usize {
100 u32::from_le_bytes([self.payload[4], self.payload[5], self.payload[6], self.payload[7]])
101 as usize
102 }
103
104 fn offset(&self) -> usize {
105 u32::from_le_bytes([self.payload[8], self.payload[9], self.payload[10], self.payload[11]])
106 as usize
107 }
108
109 #[must_use]
114 pub fn definitely_differs(&self, other: &Self) -> bool {
115 self.length != other.length || self.prefix() != other.prefix()
116 }
117}
118
119const BLOCK_SIZE: usize = 16 * 1024;
124
125#[derive(Debug, Clone, Default, PartialEq, Eq)]
132pub struct StringColumn {
133 views: Vec<StringView>,
134 blocks: Vec<Vec<u8>>,
135}
136
137impl StringColumn {
138 #[must_use]
140 pub fn new() -> Self {
141 Self::default()
142 }
143
144 #[must_use]
146 pub fn with_capacity(capacity: usize) -> Self {
147 Self { views: Vec::with_capacity(capacity), blocks: Vec::new() }
148 }
149
150 #[must_use]
152 pub fn len(&self) -> usize {
153 self.views.len()
154 }
155
156 #[must_use]
158 pub fn is_empty(&self) -> bool {
159 self.views.is_empty()
160 }
161
162 #[must_use]
164 pub fn views(&self) -> &[StringView] {
165 &self.views
166 }
167
168 pub fn push(&mut self, text: &str) -> usize {
170 let view = if text.len() <= INLINE_LIMIT {
171 StringView::inline(text)
172 } else {
173 let (block, offset) = self.append_bytes(text.as_bytes());
174 StringView::indirect(text, block, offset)
175 };
176 self.views.push(view);
177 self.views.len() - 1
178 }
179
180 #[must_use]
182 pub fn get(&self, index: usize) -> Option<&str> {
183 let view = self.views.get(index)?;
184 if let Some(text) = view.as_inline_str() {
185 return Some(text);
186 }
187 let block = self.blocks.get(view.block())?;
188 let bytes = block.get(view.offset()..view.offset() + view.len())?;
189 std::str::from_utf8(bytes).ok()
191 }
192
193 pub fn iter(&self) -> impl Iterator<Item = &str> {
195 (0..self.len()).filter_map(|index| self.get(index))
196 }
197
198 #[must_use]
200 pub fn heap_bytes(&self) -> usize {
201 self.blocks.iter().map(Vec::len).sum()
202 }
203
204 fn append_bytes(&mut self, bytes: &[u8]) -> (u32, u32) {
205 let fits = self
206 .blocks
207 .last()
208 .is_some_and(|block| block.len() + bytes.len() <= block.capacity().max(BLOCK_SIZE));
209 if !fits {
210 self.blocks.push(Vec::with_capacity(BLOCK_SIZE.max(bytes.len())));
211 }
212 let block_index = self.blocks.len() - 1;
213 let block = &mut self.blocks[block_index];
214 let offset = block.len();
215 block.extend_from_slice(bytes);
216 (block_index as u32, offset as u32)
219 }
220}
221
222impl<'a> Extend<&'a str> for StringColumn {
223 fn extend<T: IntoIterator<Item = &'a str>>(&mut self, iter: T) {
224 for text in iter {
225 self.push(text);
226 }
227 }
228}
229
230impl<'a> FromIterator<&'a str> for StringColumn {
231 fn from_iter<T: IntoIterator<Item = &'a str>>(iter: T) -> Self {
232 let mut column = Self::new();
233 column.extend(iter);
234 column
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use super::{INLINE_LIMIT, StringColumn, StringView};
241
242 #[test]
243 fn a_view_is_sixteen_bytes_and_stays_sixteen_bytes() {
244 assert_eq!(size_of::<StringView>(), 16);
247 assert_eq!(align_of::<StringView>(), 4);
248 }
249
250 #[test]
251 fn twelve_bytes_is_inline_and_thirteen_is_not() {
252 let mut column = StringColumn::new();
253 column.push("123456789012");
254 column.push("1234567890123");
255 assert!(column.views()[0].is_inline());
256 assert!(!column.views()[1].is_inline());
257 assert_eq!(column.get(0), Some("123456789012"));
258 assert_eq!(column.get(1), Some("1234567890123"));
259 assert_eq!(INLINE_LIMIT, 12);
260 }
261
262 #[test]
263 fn a_prefix_answers_the_comparison_without_reading_the_payload() {
264 let mut column = StringColumn::new();
265 column.push("https://example.com/a");
266 column.push("https://example.com/b");
267 column.push("mailto:someone@example.com");
268 let views = column.views();
269 assert!(!views[0].definitely_differs(&views[1]));
273 assert!(views[0].definitely_differs(&views[2]));
275 }
276
277 #[test]
278 fn a_string_longer_than_a_block_gets_a_block_of_its_own() {
279 let long = "x".repeat(40 * 1024);
280 let mut column = StringColumn::new();
281 column.push("short");
282 column.push(&long);
283 column.push("also short");
284 assert_eq!(column.get(1), Some(long.as_str()));
285 assert_eq!(column.get(2), Some("also short"));
286 assert_eq!(column.heap_bytes(), long.len());
287 }
288
289 #[test]
290 fn blocks_hold_many_strings_and_the_offsets_stay_right() {
291 let mut column = StringColumn::new();
292 let strings: Vec<String> =
293 (0..2000).map(|i| format!("value number {i} padded out")).collect();
294 for text in &strings {
295 column.push(text);
296 }
297 for (index, text) in strings.iter().enumerate() {
298 assert_eq!(column.get(index), Some(text.as_str()), "at {index}");
299 }
300 assert_eq!(column.len(), 2000);
301 assert_eq!(column.iter().count(), 2000);
302 }
303
304 #[test]
305 fn the_empty_string_is_inline_and_reads_back_empty() {
306 let mut column = StringColumn::new();
307 column.push("");
308 assert_eq!(column.get(0), Some(""));
309 assert!(column.views()[0].is_empty());
310 assert_eq!(column.heap_bytes(), 0);
311 }
312
313 #[test]
314 fn multibyte_text_survives_the_inline_boundary() {
315 let mut column = StringColumn::new();
318 column.push("héllo wörld");
319 column.push("🦀🦀🦀🦀");
320 assert_eq!(column.get(0), Some("héllo wörld"));
321 assert_eq!(column.get(1), Some("🦀🦀🦀🦀"));
322 assert!(!column.views()[1].is_inline());
323 }
324
325 #[test]
326 fn reading_past_the_end_is_none_rather_than_a_panic() {
327 let column: StringColumn = ["a", "b"].into_iter().collect();
328 assert_eq!(column.get(2), None);
329 assert_eq!(column.len(), 2);
330 }
331}