use std::collections::HashMap;
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 const fn empty() -> Self {
Self { length: 0, payload: [0; 12] }
}
#[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 }
}
#[must_use]
pub 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
}
pub(crate) fn shifted(self, by: u64) -> Self {
if self.is_inline() {
return self;
}
let mut shifted = self;
shifted.payload[4..].copy_from_slice(&(self.offset() as u64 + by).to_le_bytes());
shifted
}
#[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()
}
#[must_use]
pub fn bytes_in<'a>(&'a self, arena: &'a [u8]) -> Option<&'a [u8]> {
if let Some(inline) = self.inline_bytes() {
return Some(inline);
}
arena.get(self.offset()..self.offset() + self.len())
}
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: Buffer<StringView>,
arena: Buffer<u8>,
}
impl StringColumn {
#[must_use]
pub fn footprint(&self) -> usize {
self.views.footprint() + self.arena.footprint()
}
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_capacity(capacity: usize) -> Self {
Self { views: Buffer::with_capacity(capacity), arena: Buffer::new() }
}
#[must_use]
pub fn over(arena: Buffer<u8>) -> Self {
Self { views: Buffer::new(), arena }
}
#[must_use]
pub fn into_page(self) -> Self {
Self { views: self.views.into_page(), arena: self.arena.into_page() }
}
#[must_use]
pub fn from_parts(views: Vec<StringView>, arena: Buffer<u8>) -> Self {
Self { views: Buffer::from_vec(views), arena }
}
#[must_use]
pub fn viewing(&self, at: impl Iterator<Item = usize>) -> Option<Self> {
if !self.arena.is_shared() {
return None;
}
let views = at
.map(|index| self.views.get(index).copied().unwrap_or_else(StringView::empty))
.collect();
Some(Self { views, arena: self.arena.clone() })
}
#[must_use]
pub fn window(&self, from: usize, to: usize) -> Option<Self> {
if !self.arena.is_shared() || from > to || to > self.views.len() {
return None;
}
Some(Self { views: self.views.slice(from, to - from), arena: self.arena.clone() })
}
#[must_use]
pub fn is_paged(&self) -> bool {
self.views.is_shared() && self.arena.is_shared()
}
#[must_use]
pub fn joined(&self, next: &Self) -> Option<Self> {
if !self.arena.same_window(&next.arena) {
return None;
}
Some(Self { views: self.views.joined(&next.views)?, arena: self.arena.clone() })
}
#[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(crate) fn push_column(&mut self, source: &Self, arenas: &mut Arenas) {
self.views.reserve(source.views.len());
let key = Arenas::key(source);
let (live, share) = match arenas.counted(source) {
Some(live) => (live, true),
None => (live_bytes(source), false),
};
let base = match arenas.placed.get(&key) {
Some(&base) => Some(base),
None if Arenas::mostly_read(source.arena.len(), live) => {
let base = self.arena.len() as u64;
self.arena.extend_from_slice(source.arena());
if share {
arenas.placed.insert(key, base);
}
Some(base)
}
None => None,
};
if let Some(base) = base {
self.views.to_mut().extend(source.views.iter().map(|view| view.shifted(base)));
return;
}
self.arena.reserve(live_bytes(source));
for index in 0..source.len() {
self.push_from(source, index);
}
}
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()
))
})?;
if !rudb_common::utf8::valid(bytes) {
return Err(Error::internal(format!("the bytes at {offset} are not valid UTF-8")));
}
self.views.push(StringView::over(bytes, offset as u64));
Ok(self.views.len() - 1)
}
pub fn push_run_in_place(&mut self, start: usize, ends: &[usize]) -> Result<()> {
let last = ends.last().copied().unwrap_or(start);
let run = self.arena.get(start..last).ok_or_else(|| {
Error::internal(format!(
"strings from {start} to {last} are not inside a {} byte arena",
self.arena.len()
))
})?;
let mut from = start;
for &end in ends {
if end < from {
return Err(Error::internal(format!("a string ends at {end} before {from}")));
}
from = end;
}
let cut = |at: usize| run.get(at - start).is_some_and(|&byte| byte & 0xC0 == 0x80);
if !rudb_common::utf8::valid(run) || ends.iter().any(|&end| cut(end)) {
return Err(Error::internal(format!("the bytes from {start} are not valid UTF-8")));
}
self.views.reserve(ends.len());
let mut from = start;
for &end in ends {
self.views.push(StringView::over(&self.arena[from..end], from as u64));
from = end;
}
Ok(())
}
pub fn push_bytes_in_place(&mut self, offset: usize, len: usize) -> Result<usize> {
let end = offset.checked_add(len).ok_or_else(|| {
Error::internal(format!(
"a value 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 value at {offset} of {len} bytes is not inside a {} byte arena",
self.arena.len()
))
})?;
self.views.push(StringView::over(bytes, offset as u64));
Ok(self.views.len() - 1)
}
#[must_use]
pub fn arena(&self) -> &[u8] {
&self.arena
}
pub(crate) fn mostly_read(&self) -> bool {
Arenas::mostly_read(self.arena.len(), live_bytes(self))
}
#[must_use]
pub fn into_parts(self) -> (Vec<StringView>, Buffer<u8>) {
(self.views.into_vec(), self.arena)
}
#[must_use]
pub fn bytes(&self, index: usize) -> Option<&[u8]> {
self.views.get(index)?.bytes_in(&self.arena)
}
#[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);
}
pub fn reserve_views(&mut self, count: usize) {
self.views.reserve(count);
}
}
#[derive(Debug, Default)]
pub(crate) struct Arenas {
live: HashMap<(usize, usize), usize>,
placed: HashMap<(usize, usize), u64>,
}
impl Arenas {
pub(crate) fn count(&mut self, column: &StringColumn) {
*self.live.entry(Self::key(column)).or_default() += live_bytes(column);
}
pub(crate) fn bytes(&self) -> usize {
self.live
.iter()
.map(|(&(_, len), &live)| if Self::mostly_read(len, live) { len } else { live })
.sum()
}
pub(crate) fn mostly_read(arena: usize, live: usize) -> bool {
arena <= live.saturating_add(live / 4)
}
fn key(column: &StringColumn) -> (usize, usize) {
(column.arena.as_ptr() as usize, column.arena.len())
}
fn counted(&self, column: &StringColumn) -> Option<usize> {
self.live.get(&Self::key(column)).copied()
}
}
fn live_bytes(column: &StringColumn) -> usize {
column.views.iter().filter(|view| !view.is_inline()).map(StringView::len).sum()
}
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 std::sync::Arc;
use super::{Arenas, INLINE_LIMIT, StringColumn, StringView};
use crate::buffer::Buffer;
#[test]
fn cuts_of_one_page_lay_the_page_once_and_a_sparse_cut_lays_its_strings() {
let strings =
["the first string past the inline limit", "short", "a second string past the limit"];
let mut bytes = Vec::new();
let mut at = Vec::new();
for text in strings {
at.push((bytes.len(), text.len()));
bytes.extend_from_slice(text.as_bytes());
}
let page = Arc::new(bytes);
let cut = |rows: &[usize]| {
let mut column = StringColumn::over(Buffer::from_arc(Arc::clone(&page)));
for &row in rows {
column.push_in_place(at[row].0, at[row].1).expect("inside the page");
}
column
};
let (first, second) = (cut(&[0, 1]), cut(&[2]));
let mut arenas = Arenas::default();
arenas.count(&first);
arenas.count(&second);
assert_eq!(arenas.bytes(), page.len(), "what the lay below takes, reserved up front");
let mut laid = StringColumn::from_iter(["a string already there, past the limit"]);
let before = laid.arena().len();
laid.push_column(&first, &mut arenas);
laid.push_column(&second, &mut arenas);
assert_eq!(laid.arena().len(), before + page.len(), "the page is laid once");
let expected =
["a string already there, past the limit", strings[0], strings[1], strings[2]];
assert_eq!(laid.iter().collect::<Vec<_>>(), expected);
let mut sparse = StringColumn::new();
let mut alone = Arenas::default();
alone.count(&second);
assert_eq!(alone.bytes(), strings[2].len(), "a sliver reserves only its own bytes");
sparse.push_column(&second, &mut alone);
assert_eq!(sparse.arena(), strings[2].as_bytes(), "a sliver of a page is copied alone");
assert_eq!(sparse.get(0), Some(strings[2]));
}
#[test]
fn an_arena_that_nobody_counted_is_not_remembered_by_its_address() {
let text = "a string built on the way past, well over the inline limit";
let built = StringColumn::from_iter([text]);
let mut laid = StringColumn::new();
let mut arenas = Arenas::default();
laid.push_column(&built, &mut arenas);
assert!(arenas.placed.is_empty(), "an uncounted arena was recorded by its address");
assert_eq!(laid.get(0), Some(text));
}
#[test]
fn an_arena_that_nobody_counted_is_still_laid_in_one_copy() {
let text = "a string two views point at, well over the inline limit";
let page = Arc::new(text.as_bytes().to_vec());
let mut twice = StringColumn::over(Buffer::from_arc(Arc::clone(&page)));
twice.push_in_place(0, text.len()).expect("inside the page");
twice.push_in_place(0, text.len()).expect("inside the page");
let mut laid = StringColumn::new();
let mut arenas = Arenas::default();
laid.push_column(&twice, &mut arenas);
assert!(arenas.placed.is_empty(), "an uncounted arena was recorded by its address");
assert_eq!(laid.arena().len(), text.len(), "the arena was laid once and not once a view");
assert_eq!(laid.get(0), Some(text));
assert_eq!(laid.get(1), Some(text));
}
#[test]
fn an_arena_that_was_counted_is_still_copied_whole() {
let text = "a string on a page the caller holds, well over the inline limit";
let page = StringColumn::from_iter([text]);
let mut laid = StringColumn::new();
let mut arenas = Arenas::default();
arenas.count(&page);
laid.push_column(&page, &mut arenas);
assert_eq!(arenas.placed.len(), 1, "a counted arena is copied whole and written down");
assert_eq!(laid.get(0), Some(text));
}
#[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 a_run_of_strings_is_checked_for_text_once_and_as_strictly() {
let page = "ab\u{e9}t\u{e9} and a string well past the inline limit".as_bytes().to_vec();
let mut column = StringColumn::over(Buffer::from_vec(page.clone()));
column.push_run_in_place(0, &[2, 2, 7, page.len()]).expect("text");
assert_eq!(column.get(0), Some("ab"));
assert_eq!(column.get(1), Some(""));
assert_eq!(column.get(2), Some("\u{e9}t\u{e9}"));
assert_eq!(column.get(3), Some(" and a string well past the inline limit"));
let mut split = StringColumn::over(Buffer::from_vec(page.clone()));
assert!(split.push_run_in_place(0, &[3, page.len()]).is_err(), "a cut inside a character");
assert_eq!(split.len(), 0);
let mut bad = StringColumn::over(Buffer::from_vec(vec![b'a', 0xff, b'b']));
assert!(bad.push_run_in_place(0, &[1, 3]).is_err(), "bytes that are not text");
let mut back = StringColumn::over(Buffer::from_vec(page.clone()));
assert!(back.push_run_in_place(0, &[5, 4, page.len()]).is_err(), "an end before its start");
let mut past = StringColumn::over(Buffer::from_vec(page));
assert!(past.push_run_in_place(0, &[4, 400]).is_err(), "an end past the page");
}
#[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);
let mut page = b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_vec();
page.push(0x80);
let len = page.len();
let mut column = StringColumn::over(Buffer::from_vec(page));
assert!(column.push_in_place(0, len).is_err());
assert!(column.push_in_place(0, len - 1).is_ok());
let page = "søk på nettet".as_bytes().to_vec();
let len = page.len();
let mut column = StringColumn::over(Buffer::from_vec(page));
column.push_in_place(0, len).expect("valid text that is not ASCII");
assert_eq!(column.get(0), Some("søk på nettet"));
}
#[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);
}
}