pub const INLINE_LIMIT: usize = 12;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct StringView {
length: u32,
payload: [u8; 12],
}
impl StringView {
#[must_use]
pub fn inline(text: &str) -> Self {
assert!(text.len() <= INLINE_LIMIT, "a string of {} bytes is not inline", text.len());
let mut payload = [0u8; 12];
payload[..text.len()].copy_from_slice(text.as_bytes());
Self { length: text.len() as u32, payload }
}
fn indirect(text: &str, block: u32, offset: u32) -> Self {
let mut payload = [0u8; 12];
payload[..4].copy_from_slice(&text.as_bytes()[..4]);
payload[4..8].copy_from_slice(&block.to_le_bytes());
payload[8..].copy_from_slice(&offset.to_le_bytes());
Self { length: text.len() as u32, payload }
}
#[must_use]
pub fn len(&self) -> usize {
self.length as usize
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.length == 0
}
#[must_use]
pub fn is_inline(&self) -> bool {
self.len() <= INLINE_LIMIT
}
#[must_use]
pub fn prefix(&self) -> [u8; 4] {
[self.payload[0], self.payload[1], self.payload[2], self.payload[3]]
}
#[must_use]
pub fn as_inline_str(&self) -> Option<&str> {
if !self.is_inline() {
return None;
}
std::str::from_utf8(&self.payload[..self.len()]).ok()
}
fn block(&self) -> usize {
u32::from_le_bytes([self.payload[4], self.payload[5], self.payload[6], self.payload[7]])
as usize
}
fn offset(&self) -> usize {
u32::from_le_bytes([self.payload[8], self.payload[9], self.payload[10], self.payload[11]])
as usize
}
#[must_use]
pub fn definitely_differs(&self, other: &Self) -> bool {
self.length != other.length || self.prefix() != other.prefix()
}
}
const BLOCK_SIZE: usize = 16 * 1024;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct StringColumn {
views: Vec<StringView>,
blocks: Vec<Vec<u8>>,
}
impl StringColumn {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_capacity(capacity: usize) -> Self {
Self { views: Vec::with_capacity(capacity), blocks: Vec::new() }
}
#[must_use]
pub fn len(&self) -> usize {
self.views.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.views.is_empty()
}
#[must_use]
pub fn views(&self) -> &[StringView] {
&self.views
}
pub fn push(&mut self, text: &str) -> usize {
let view = if text.len() <= INLINE_LIMIT {
StringView::inline(text)
} else {
let (block, offset) = self.append_bytes(text.as_bytes());
StringView::indirect(text, block, offset)
};
self.views.push(view);
self.views.len() - 1
}
#[must_use]
pub fn get(&self, index: usize) -> Option<&str> {
let view = self.views.get(index)?;
if let Some(text) = view.as_inline_str() {
return Some(text);
}
let block = self.blocks.get(view.block())?;
let bytes = block.get(view.offset()..view.offset() + view.len())?;
std::str::from_utf8(bytes).ok()
}
pub fn iter(&self) -> impl Iterator<Item = &str> {
(0..self.len()).filter_map(|index| self.get(index))
}
#[must_use]
pub fn heap_bytes(&self) -> usize {
self.blocks.iter().map(Vec::len).sum()
}
fn append_bytes(&mut self, bytes: &[u8]) -> (u32, u32) {
let fits = self
.blocks
.last()
.is_some_and(|block| block.len() + bytes.len() <= block.capacity().max(BLOCK_SIZE));
if !fits {
self.blocks.push(Vec::with_capacity(BLOCK_SIZE.max(bytes.len())));
}
let block_index = self.blocks.len() - 1;
let block = &mut self.blocks[block_index];
let offset = block.len();
block.extend_from_slice(bytes);
(block_index as u32, offset as u32)
}
}
impl<'a> Extend<&'a str> for StringColumn {
fn extend<T: IntoIterator<Item = &'a str>>(&mut self, iter: T) {
for text in iter {
self.push(text);
}
}
}
impl<'a> FromIterator<&'a str> for StringColumn {
fn from_iter<T: IntoIterator<Item = &'a str>>(iter: T) -> Self {
let mut column = Self::new();
column.extend(iter);
column
}
}
#[cfg(test)]
mod tests {
use super::{INLINE_LIMIT, StringColumn, StringView};
#[test]
fn a_view_is_sixteen_bytes_and_stays_sixteen_bytes() {
assert_eq!(size_of::<StringView>(), 16);
assert_eq!(align_of::<StringView>(), 4);
}
#[test]
fn twelve_bytes_is_inline_and_thirteen_is_not() {
let mut column = StringColumn::new();
column.push("123456789012");
column.push("1234567890123");
assert!(column.views()[0].is_inline());
assert!(!column.views()[1].is_inline());
assert_eq!(column.get(0), Some("123456789012"));
assert_eq!(column.get(1), Some("1234567890123"));
assert_eq!(INLINE_LIMIT, 12);
}
#[test]
fn a_prefix_answers_the_comparison_without_reading_the_payload() {
let mut column = StringColumn::new();
column.push("https://example.com/a");
column.push("https://example.com/b");
column.push("mailto:someone@example.com");
let views = column.views();
assert!(!views[0].definitely_differs(&views[1]));
assert!(views[0].definitely_differs(&views[2]));
}
#[test]
fn a_string_longer_than_a_block_gets_a_block_of_its_own() {
let long = "x".repeat(40 * 1024);
let mut column = StringColumn::new();
column.push("short");
column.push(&long);
column.push("also short");
assert_eq!(column.get(1), Some(long.as_str()));
assert_eq!(column.get(2), Some("also short"));
assert_eq!(column.heap_bytes(), long.len());
}
#[test]
fn blocks_hold_many_strings_and_the_offsets_stay_right() {
let mut column = StringColumn::new();
let strings: Vec<String> =
(0..2000).map(|i| format!("value number {i} padded out")).collect();
for text in &strings {
column.push(text);
}
for (index, text) in strings.iter().enumerate() {
assert_eq!(column.get(index), Some(text.as_str()), "at {index}");
}
assert_eq!(column.len(), 2000);
assert_eq!(column.iter().count(), 2000);
}
#[test]
fn the_empty_string_is_inline_and_reads_back_empty() {
let mut column = StringColumn::new();
column.push("");
assert_eq!(column.get(0), Some(""));
assert!(column.views()[0].is_empty());
assert_eq!(column.heap_bytes(), 0);
}
#[test]
fn multibyte_text_survives_the_inline_boundary() {
let mut column = StringColumn::new();
column.push("héllo wörld");
column.push("🦀🦀🦀🦀");
assert_eq!(column.get(0), Some("héllo wörld"));
assert_eq!(column.get(1), Some("🦀🦀🦀🦀"));
assert!(!column.views()[1].is_inline());
}
#[test]
fn reading_past_the_end_is_none_rather_than_a_panic() {
let column: StringColumn = ["a", "b"].into_iter().collect();
assert_eq!(column.get(2), None);
assert_eq!(column.len(), 2);
}
}