use rudb_common::{Error, Result};
use crate::buffer::Buffer;
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, offset: u64) -> Self {
let mut payload = [0u8; 12];
payload[..4].copy_from_slice(&text.as_bytes()[..4]);
payload[4..].copy_from_slice(&offset.to_le_bytes());
Self { length: text.len() as u32, payload }
}
fn over(bytes: &[u8], offset: u64) -> Self {
let mut payload = [0u8; 12];
if bytes.len() <= INLINE_LIMIT {
payload[..bytes.len()].copy_from_slice(bytes);
} else {
payload[..4].copy_from_slice(&bytes[..4]);
payload[4..].copy_from_slice(&offset.to_le_bytes());
}
Self { length: bytes.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 inline_bytes(&self) -> Option<&[u8]> {
if self.is_inline() { Some(&self.payload[..self.len()]) } else { None }
}
#[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 offset(&self) -> usize {
u64::from_le_bytes([
self.payload[4],
self.payload[5],
self.payload[6],
self.payload[7],
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()
}
}
#[derive(Debug, Clone, Default, Eq)]
pub struct StringColumn {
views: Vec<StringView>,
arena: Buffer<u8>,
}
impl StringColumn {
#[must_use]
pub fn footprint(&self) -> usize {
self.views.capacity() * size_of::<StringView>() + self.arena.footprint()
}
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_capacity(capacity: usize) -> Self {
Self { views: Vec::with_capacity(capacity), arena: Buffer::new() }
}
#[must_use]
pub fn over(arena: Buffer<u8>) -> Self {
Self { views: Vec::new(), arena }
}
#[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 offset = self.arena.len() as u64;
self.arena.extend_from_slice(text.as_bytes());
StringView::indirect(text, offset)
};
self.views.push(view);
self.views.len() - 1
}
pub fn push_from(&mut self, source: &Self, index: usize) -> usize {
self.push_bytes(source.bytes(index).unwrap_or(b""))
}
pub fn push_bytes(&mut self, bytes: &[u8]) -> usize {
let offset = self.arena.len() as u64;
if bytes.len() > INLINE_LIMIT {
self.arena.extend_from_slice(bytes);
}
self.views.push(StringView::over(bytes, offset));
self.views.len() - 1
}
pub fn push_in_place(&mut self, offset: usize, len: usize) -> Result<usize> {
let end = offset.checked_add(len).ok_or_else(|| {
Error::internal(format!(
"a string at {offset} of {len} bytes runs off the end of memory"
))
})?;
let bytes = self.arena.get(offset..end).ok_or_else(|| {
Error::internal(format!(
"a string at {offset} of {len} bytes is not inside a {} byte arena",
self.arena.len()
))
})?;
let text = std::str::from_utf8(bytes)
.map_err(|_| Error::internal(format!("the bytes at {offset} are not valid UTF-8")))?;
let view = if len <= INLINE_LIMIT {
StringView::inline(text)
} else {
StringView::indirect(text, offset as u64)
};
self.views.push(view);
Ok(self.views.len() - 1)
}
#[must_use]
pub fn arena(&self) -> &[u8] {
&self.arena
}
#[must_use]
pub fn bytes(&self, index: usize) -> Option<&[u8]> {
let view = self.views.get(index)?;
if let Some(inline) = view.inline_bytes() {
return Some(inline);
}
self.arena.get(view.offset()..view.offset() + view.len())
}
#[must_use]
pub fn get(&self, index: usize) -> Option<&str> {
std::str::from_utf8(self.bytes(index)?).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.arena.len()
}
pub fn reserve_bytes(&mut self, bytes: usize) {
self.arena.reserve(bytes);
}
}
impl PartialEq for StringColumn {
fn eq(&self, other: &Self) -> bool {
self.views.len() == other.views.len()
&& (0..self.views.len()).all(|index| {
let mine = self.views[index];
let theirs = other.views[index];
if mine.definitely_differs(&theirs) {
return false;
}
if mine.is_inline() {
return mine == theirs;
}
self.bytes(index) == other.bytes(index)
})
}
}
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};
use crate::buffer::Buffer;
#[test]
fn a_column_over_a_page_records_the_strings_without_moving_them() {
let page =
b"HEADER..a string well past the inline limit!!a second one past the limit".to_vec();
let mut column = StringColumn::over(Buffer::from_vec(page.clone()));
assert_eq!(column.push_in_place(8, 37).expect("inside the page"), 0);
assert_eq!(column.push_in_place(45, 27).expect("inside the page"), 1);
assert_eq!(column.get(0), Some("a string well past the inline limit!!"));
assert_eq!(column.get(1), Some("a second one past the limit"));
assert_eq!(column.arena(), page.as_slice());
assert_eq!(column.heap_bytes(), page.len());
assert_eq!(column.len(), 2);
}
#[test]
fn copying_from_another_column_takes_the_strings_and_not_the_page_they_were_in() {
let page = b"HEADER..a string well past the inline limit!!short".to_vec();
let mut source = StringColumn::over(Buffer::from_vec(page.clone()));
source.push_in_place(8, 37).expect("inside the page");
source.push_in_place(45, 5).expect("inside the page");
let mut out = StringColumn::new();
assert_eq!(out.push_from(&source, 1), 0);
assert_eq!(out.push_from(&source, 0), 1);
assert_eq!(out.push_from(&source, 9), 2, "a position that is not there");
assert_eq!(out.get(0), Some("short"));
assert_eq!(out.get(1), Some("a string well past the inline limit!!"));
assert_eq!(out.get(2), Some(""));
assert!(out.views()[0].is_inline(), "a short string stays in its view");
assert!(!out.views()[1].is_inline());
assert_eq!(out.views()[1].prefix(), *b"a st", "the prefix is the string's own");
assert_eq!(
out.arena(),
b"a string well past the inline limit!!",
"the arena is the long strings and not the page"
);
}
#[test]
fn a_column_holds_bytes_that_are_not_a_string() {
let long = b"\xff\xfe and a good deal more than twelve bytes of it";
let mut column = StringColumn::new();
assert_eq!(column.push_bytes(b"a\xffb"), 0);
assert_eq!(column.push_bytes(long), 1);
assert_eq!(column.push_bytes(b""), 2);
assert_eq!(column.bytes(0), Some(b"a\xffb".as_slice()));
assert_eq!(column.bytes(1), Some(long.as_slice()));
assert_eq!(column.bytes(2), Some(b"".as_slice()));
assert_eq!(column.get(0), None, "a stray 0xff is not a character");
assert_eq!(column.get(1), None);
assert!(column.views()[0].is_inline());
assert!(!column.views()[1].is_inline());
assert_eq!(column.arena(), long, "only the long one needed the arena");
}
#[test]
fn copying_from_a_column_that_was_itself_copied_reads_the_same_strings() {
let mut first = StringColumn::new();
for text in ["a string well past the inline limit", "short", "another long one past it"] {
first.push(text);
}
let mut second = StringColumn::new();
for index in (0..first.len()).rev() {
second.push_from(&first, index);
}
let mut third = StringColumn::new();
for index in 0..second.len() {
third.push_from(&second, index);
}
assert_eq!(
third.iter().collect::<Vec<_>>(),
["another long one past it", "short", "a string well past the inline limit"]
);
}
#[test]
fn a_short_string_in_a_page_is_copied_into_its_view() {
let mut column = StringColumn::over(Buffer::from_vec(b"one.two".to_vec()));
column.push_in_place(0, 3).expect("inside the page");
column.push_in_place(4, 3).expect("inside the page");
assert!(column.views()[0].is_inline());
assert_eq!(column.get(0), Some("one"));
assert_eq!(column.get(1), Some("two"));
assert_eq!(column.arena(), b"one.two");
}
#[test]
fn a_range_outside_the_page_or_bytes_that_are_not_text_are_refused() {
let mut column = StringColumn::over(Buffer::from_vec(vec![0xff, 0xfe, 0xfd]));
assert!(column.push_in_place(2, 4).is_err());
assert!(column.push_in_place(usize::MAX, 1).is_err());
assert!(column.push_in_place(0, 3).is_err());
assert_eq!(column.len(), 0);
}
#[test]
fn the_same_strings_over_different_arenas_are_the_same_column() {
let copied: StringColumn =
["the first string past the limit", "the second string past the limit"]
.into_iter()
.collect();
let page =
b"gap!the second string past the limit....the first string past the limit".to_vec();
let mut over = StringColumn::over(Buffer::from_vec(page));
over.push_in_place(40, 31).expect("inside the page");
over.push_in_place(4, 32).expect("inside the page");
assert_ne!(copied.arena(), over.arena());
assert_eq!(copied, over);
let mut different: StringColumn = copied.clone();
different.push("a third one past the inline limit");
assert_ne!(copied, different);
}
#[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_far_larger_than_any_block_would_have_been_goes_in_whole() {
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 the_arena_moving_underneath_does_not_move_what_the_views_point_at() {
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 reserving_bytes_changes_nothing_but_where_the_allocation_happens() {
let mut column = StringColumn::with_capacity(3);
column.reserve_bytes(128);
for text in ["a string past the limit", "another one past it", "short"] {
column.push(text);
}
assert_eq!(column.get(0), Some("a string past the limit"));
assert_eq!(column.get(1), Some("another one past it"));
assert_eq!(column.get(2), Some("short"));
assert_eq!(column.heap_bytes(), 42);
}
#[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);
}
#[test]
fn the_bytes_and_the_string_are_the_same_string() {
let long = "x".repeat(9000);
let words = ["", "a", "twelve bytes", "thirteen bytes", "π is two bytes", &long];
let column: StringColumn = words.into_iter().collect();
for (index, text) in words.iter().enumerate() {
assert_eq!(column.bytes(index), Some(text.as_bytes()), "at {index}");
assert_eq!(column.get(index), Some(*text), "at {index}");
}
assert_eq!(column.bytes(words.len()), None);
}
}