use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering as Atomic};
use std::sync::{Arc, Mutex};
use rudb_common::{Error, LogicalType, Result};
use rudb_metrics::{LoadProfile, Stage};
use rudb_storage::Range;
use rudb_vector::{Bitmap, Chunk, Data, StringColumn, Validity, Vector};
use super::{
ColumnStripe, DICTIONARY_CHECK_SEED, DICTIONARY_DECIDE_ROWS, DICTIONARY_DISTINCT_IN_TEN,
GlobalDictionary, MAX_ENCODE_WORKERS, MAX_PAGE, Part, PendingChunk, STRIPE_PARTS, Spread,
Writer, checksum, coded_page, invalid, push_validity, seeded_checksum, stats, unique_codes,
weight,
};
static BUSY: AtomicUsize = AtomicUsize::new(0);
struct Share(usize);
impl Share {
fn take(columns: usize, parts: usize) -> Self {
let busy = BUSY.fetch_add(1, Atomic::Relaxed) + 1;
let cores =
std::thread::available_parallelism().map_or(1, usize::from).min(MAX_ENCODE_WORKERS);
let workers = if parts <= 1 { 1 } else { (cores / busy).clamp(1, columns.max(1)) };
Self(workers)
}
}
impl Drop for Share {
fn drop(&mut self) {
BUSY.fetch_sub(1, Atomic::Relaxed);
}
}
#[derive(Debug, Clone)]
pub struct Preparer {
types: Vec<LogicalType>,
coded: Arc<[AtomicBool]>,
profile: Option<Arc<LoadProfile>>,
}
#[derive(Debug)]
pub struct Prepared {
parts: Vec<Part>,
types: Vec<LogicalType>,
columns: Vec<Column>,
gathers: Vec<Option<stats::Gather>>,
profile: Option<Arc<LoadProfile>>,
}
#[derive(Debug)]
pub struct Merged {
parts: Vec<Part>,
columns: Vec<Merge>,
profile: Option<Arc<LoadProfile>>,
}
#[derive(Debug)]
pub struct Paged {
parts: Vec<Part>,
columns: Vec<ColumnStripe>,
}
#[derive(Debug)]
enum Column {
Pages(ColumnStripe),
Coded(Local),
}
#[derive(Debug)]
enum Merge {
Pages(ColumnStripe),
Codes {
parts: Vec<LocalPart>,
global: Vec<u32>,
},
Plain(Local),
}
const END: u32 = u32::MAX;
#[derive(Debug, Default)]
struct Local {
first: HashMap<u64, u32, Spread>,
next: Vec<u32>,
hashes: Vec<u64>,
checks: Vec<u64>,
bytes: Vec<u8>,
ends: Vec<usize>,
counts: Vec<u64>,
nulls: u64,
parts: Vec<LocalPart>,
}
#[derive(Debug)]
struct LocalPart {
codes: Vec<u32>,
validity: Vec<u8>,
range: Range,
}
impl Local {
fn code_column(index: usize, held: &[PendingChunk]) -> Result<Self> {
let mut local = Self::default();
for pending in held {
let column = pending.chunk.column(index)?;
let flat = column.flatten()?;
let mut codes = Vec::with_capacity(flat.len());
let mut last = None;
for row in 0..flat.len() {
let text = flat.text_at(row).unwrap_or("").as_bytes();
let code = match last {
Some(code) if local.value(code) == text => code,
_ => local.code(text)?,
};
last = Some(code);
if flat.is_null_at(row) {
local.nulls += 1;
} else {
local.counts[code as usize] += 1;
}
codes.push(code);
}
let mut validity = Vec::new();
push_validity(&mut validity, &flat);
local.parts.push(LocalPart { codes, validity, range: Range::of(column) });
}
local.first = HashMap::default();
local.next = Vec::new();
Ok(local)
}
fn rows(&self) -> Result<Vec<Vector>> {
self.parts
.iter()
.map(|part| {
let len = part.codes.len();
let mut column = StringColumn::with_capacity(len);
for &code in &part.codes {
column.push_bytes(self.value(code));
}
let validity = match part.validity.split_first() {
Some((0, _)) => Validity::AllValid,
Some((1, _)) => Validity::AllInvalid,
Some((2, bits)) => {
let mut mask = Bitmap::all_valid(len);
for row in (0..len).filter(|row| bits[row / 8] & (1 << (row % 8)) == 0) {
mask.set(row, false);
}
Validity::Mask(mask)
}
_ => return Err(Error::internal("a coded part has no validity")),
};
Ok(Vector::flat(LogicalType::Varchar, Data::Varlen(column))?
.with_validity(validity))
})
.collect()
}
fn values(&self) -> usize {
self.ends.len()
}
fn value(&self, code: u32) -> &[u8] {
let code = code as usize;
let from = if code == 0 { 0 } else { self.ends[code - 1] };
&self.bytes[from..self.ends[code]]
}
fn code(&mut self, text: &[u8]) -> Result<u32> {
let hash = checksum(text);
let Some(&first) = self.first.get(&hash) else {
let code = self.push(text, hash)?;
self.first.insert(hash, code);
return Ok(code);
};
let mut at = first;
loop {
if self.value(at) == text {
return Ok(at);
}
match self.next[at as usize] {
END => break,
next => at = next,
}
}
let code = self.push(text, hash)?;
self.next[at as usize] = code;
Ok(code)
}
fn push(&mut self, text: &[u8], hash: u64) -> Result<u32> {
let code = u32::try_from(self.ends.len())
.ok()
.filter(|&code| code != END)
.ok_or_else(|| invalid("a stripe has too many values in one column"))?;
self.bytes.extend_from_slice(text);
self.ends.push(self.bytes.len());
self.next.push(END);
self.hashes.push(hash);
self.checks.push(seeded_checksum(text, DICTIONARY_CHECK_SEED));
self.counts.push(0);
Ok(code)
}
fn merge_into(&self, dictionary: &mut GlobalDictionary) -> Result<Vec<u32>> {
let mut global = Vec::with_capacity(self.values());
for (code, (&hash, &check)) in self.hashes.iter().zip(&self.checks).enumerate() {
let text = self.value(code as u32);
let at = dictionary.code_hashed(text, hash, check)?;
let count = dictionary
.counts
.get_mut(at as usize)
.ok_or_else(|| invalid("global dictionary count code is out of range"))?;
*count = count.saturating_add(self.counts[code]);
global.push(at);
}
dictionary.nulls = dictionary.nulls.saturating_add(self.nulls);
Ok(global)
}
}
fn drops_dictionary(rows: usize, distinct: usize) -> bool {
rows >= DICTIONARY_DECIDE_ROWS
&& distinct.saturating_mul(10) > rows.saturating_mul(DICTIONARY_DISTINCT_IN_TEN)
}
fn fan_out<T: Send>(
jobs: Vec<usize>,
workers: usize,
profile: Option<&LoadProfile>,
work: impl Fn(usize) -> Result<T> + Sync,
) -> Result<Vec<(usize, T)>> {
if workers <= 1 || jobs.len() <= 1 {
let _span = profile.map(|profile| profile.span(Stage::Pages));
return jobs.into_iter().map(|index| Ok((index, work(index)?))).collect();
}
let workers = workers.min(jobs.len());
let queue = Mutex::new(jobs);
let pieces = std::thread::scope(|scope| {
(0..workers)
.map(|_| {
scope.spawn(|| {
let _span = profile.map(|profile| profile.span(Stage::Pages));
let mut mine = Vec::new();
loop {
let taken = queue
.lock()
.map_err(|_| Error::internal("a native encode worker panicked"))?
.pop();
let Some(index) = taken else { break };
mine.push((index, work(index)?));
}
Ok(mine)
})
})
.collect::<Vec<_>>()
.into_iter()
.map(|handle| {
handle.join().map_err(|_| Error::internal("a native encode worker panicked"))?
})
.collect::<Result<Vec<Vec<_>>>>()
})?;
Ok(pieces.into_iter().flatten().collect())
}
fn column_of(held: &[PendingChunk], index: usize) -> Result<Vec<&Vector>> {
held.iter().map(|pending| pending.chunk.column(index)).collect()
}
fn in_order<T>(width: usize, done: Vec<(usize, T)>) -> Result<Vec<T>> {
let mut slots: Vec<Option<T>> = (0..width).map(|_| None).collect();
for (index, one) in done {
slots[index] = Some(one);
}
slots
.into_iter()
.map(|slot| slot.ok_or_else(|| Error::internal("a column was never encoded")))
.collect()
}
impl Preparer {
pub fn prepare(&self, parts: Vec<((u64, u64), Chunk)>) -> Result<Prepared> {
if parts.len() > STRIPE_PARTS {
return Err(invalid("a stripe was handed more parts than it holds"));
}
let held = parts
.into_iter()
.filter(|(_, chunk)| !chunk.is_empty())
.map(|(order, chunk)| PendingChunk { order, chunk })
.collect::<Vec<_>>();
for pending in &held {
self.fits(&pending.chunk)?;
}
self.prepare_held(held)
}
fn fits(&self, chunk: &Chunk) -> Result<()> {
if chunk.width() != self.types.len() {
return Err(invalid("chunk width differs from table schema"));
}
for (index, ty) in self.types.iter().enumerate() {
if chunk.column(index)?.logical_type() != ty {
return Err(invalid("chunk type differs from table schema"));
}
}
Ok(())
}
pub(crate) fn prepare_held(&self, held: Vec<PendingChunk>) -> Result<Prepared> {
let width = self.types.len();
let key = held.first().map_or((0, 0), |pending| pending.order);
let share = Share::take(width, held.len());
let mut jobs = (0..width).collect::<Vec<_>>();
jobs.sort_by_key(|&index| weight(&self.types[index]));
let done = fan_out(jobs, share.0, self.profile.as_deref(), |index| {
let gather = stats::Gather::new(&self.types[index], 0)
.filter(|_| !held.is_empty())
.map(|mut gather| {
gather.stripe(
key,
held.iter().filter_map(|pending| pending.chunk.column(index).ok()),
);
gather
});
let column = if self.coded[index].load(Atomic::Relaxed) {
Column::Coded(Local::code_column(index, &held)?)
} else {
Column::Pages(Writer::encode_pages(&column_of(&held, index)?)?)
};
Ok((column, gather))
})?;
drop(share);
let (columns, gathers) = in_order(width, done)?.into_iter().unzip();
let parts = held.iter().map(Part::of).collect();
drop(held);
Ok(Prepared {
parts,
types: self.types.clone(),
columns,
gathers,
profile: self.profile.clone(),
})
}
}
impl Merged {
pub fn pages(self) -> Result<Paged> {
let Self { parts, columns, profile } = self;
let width = columns.len();
let mut jobs = (0..width)
.filter(|&index| !matches!(columns[index], Merge::Pages(_)))
.collect::<Vec<_>>();
jobs.sort_by_key(|&index| matches!(columns[index], Merge::Plain(_)));
let share = Share::take(jobs.len(), parts.len());
let built = fan_out(jobs, share.0, profile.as_deref(), |index| match &columns[index] {
Merge::Codes { parts, global } => code_pages(parts, global),
Merge::Plain(local) => Writer::encode_pages(&local.rows()?.iter().collect::<Vec<_>>()),
Merge::Pages(_) => Err(Error::internal("a finished column was queued to be built")),
})?;
drop(share);
let mut slots: Vec<Option<ColumnStripe>> = (0..width).map(|_| None).collect();
for (index, stripe) in built {
slots[index] = Some(stripe);
}
let columns = columns
.into_iter()
.zip(slots)
.map(|(column, slot)| match (column, slot) {
(Merge::Pages(stripe), _) | (_, Some(stripe)) => Ok(stripe),
_ => Err(Error::internal("a column was never encoded")),
})
.collect::<Result<Vec<_>>>()?;
Ok(Paged { parts, columns })
}
}
fn code_pages(parts: &[LocalPart], global: &[u32]) -> Result<ColumnStripe> {
let mut stripe = ColumnStripe {
pages: Vec::with_capacity(parts.len()),
codes: Vec::with_capacity(parts.len()),
sieves: Vec::with_capacity(parts.len()),
ranges: Vec::with_capacity(parts.len()),
};
for part in parts {
let codes = part
.codes
.iter()
.map(|&code| global.get(code as usize).copied())
.collect::<Option<Vec<_>>>()
.ok_or_else(|| Error::internal("a stripe's code has no global code"))?;
let bytes = coded_page(&codes, &part.validity)?;
if bytes.len() > MAX_PAGE {
return Err(invalid("column page exceeds the configured bound"));
}
stripe.pages.push(bytes);
stripe.codes.push(Some(unique_codes(&codes)));
stripe.sieves.push(None);
stripe.ranges.push(part.range.clone());
}
Ok(stripe)
}
impl Writer {
#[must_use]
pub fn preparer(&self) -> Preparer {
Preparer {
types: self.table.fields.iter().map(|field| field.ty.clone()).collect(),
coded: Arc::clone(&self.coded),
profile: self.profile.clone(),
}
}
pub fn merge(&mut self, prepared: Prepared) -> Result<Merged> {
self.flush_pending()?;
if prepared.columns.len() != self.table.fields.len()
|| prepared.types.iter().ne(self.table.fields.iter().map(|field| &field.ty))
{
return Err(invalid("a stripe was prepared for a table of other columns"));
}
self.table.rows = prepared
.parts
.iter()
.try_fold(self.table.rows, |rows, part| rows.checked_add(part.rows))
.ok_or_else(|| invalid("row count overflow"))?;
self.merge_held(prepared)
}
pub(crate) fn merge_held(&mut self, prepared: Prepared) -> Result<Merged> {
let Prepared { parts, columns, gathers, profile, .. } = prepared;
let timing = profile.as_deref().map(|profile| profile.span(Stage::Dictionary));
for (mine, stripe) in self.gathers.iter_mut().zip(gathers) {
if let (Some(mine), Some(stripe)) = (mine, stripe) {
mine.absorb(stripe);
}
}
let rows: usize = parts.iter().map(|part| part.rows).sum();
let mut merged = Vec::with_capacity(columns.len());
for (index, column) in columns.into_iter().enumerate() {
let dictionary = &mut self.dictionaries[index];
merged.push(match (column, dictionary.as_mut()) {
(Column::Pages(stripe), None) => Merge::Pages(stripe),
(Column::Pages(_), Some(_)) => {
return Err(Error::internal(
"a column with a global dictionary was prepared without one",
));
}
(Column::Coded(local), None) => Merge::Plain(local),
(Column::Coded(local), Some(global)) => {
if global.values() == 0 && drops_dictionary(rows, local.values()) {
*dictionary = None;
self.coded[index].store(false, Atomic::Relaxed);
Merge::Plain(local)
} else {
let global = local.merge_into(global)?;
Merge::Codes { parts: local.parts, global }
}
}
});
}
drop(timing);
Ok(Merged { parts, columns: merged, profile })
}
pub fn write(&mut self, paged: Paged) -> Result<()> {
self.write_paged(paged)
}
pub(crate) fn write_paged(&mut self, paged: Paged) -> Result<()> {
if paged.parts.is_empty() {
return Ok(());
}
self.write_stripe(&paged.parts, paged.columns)
}
pub fn append_prepared(&mut self, prepared: Prepared) -> Result<()> {
let merged = self.merge(prepared)?;
let paged = merged.pages()?;
self.write(paged)
}
}
#[cfg(test)]
mod tests {
use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
use rudb_common::{Field, Value};
use rudb_vector::Vector;
use super::*;
use crate::Reader;
const PART: usize = 1_000;
fn path(label: &str) -> PathBuf {
let stamp = SystemTime::now().duration_since(UNIX_EPOCH).expect("time advances").as_nanos();
std::env::temp_dir()
.join(format!("rudb-prepare-{label}-{}-{stamp}.rdb", std::process::id()))
}
fn fields() -> Vec<Field> {
vec![
Field::required("id", LogicalType::BigInt),
Field::new("city", LogicalType::Varchar),
Field::new("note", LogicalType::Varchar),
]
}
fn row(id: usize) -> [Value; 3] {
let city = if id % 11 == 0 {
Value::Null
} else {
Value::Varchar(format!("city {}", (id / 7) % 13))
};
let note = if id % 17 == 0 { Value::Null } else { Value::Varchar(format!("note {id}")) };
[Value::BigInt(id as i64), city, note]
}
fn stripe(first: usize, parts: usize) -> Vec<((u64, u64), Chunk)> {
(first..first + parts)
.map(|part| {
let rows = (part * PART..(part + 1) * PART).map(row).collect::<Vec<_>>();
let column = |at: usize| {
let values = rows.iter().map(|row| row[at].clone()).collect::<Vec<_>>();
Vector::from_values(fields()[at].ty.clone(), &values).expect("a column")
};
let chunk = Chunk::new(vec![column(0), column(1), column(2)]).expect("a chunk");
((part as u64, 0), chunk)
})
.collect()
}
fn runs() -> Vec<Vec<((u64, u64), Chunk)>> {
vec![stripe(5, 5), stripe(0, 5), stripe(10, 3)]
}
fn check(path: &PathBuf) {
let reader = Reader::open(path).expect("reopen");
assert_eq!(reader.parts(), 13);
for part in 0..13 {
let chunk = reader.read(part, &[0, 1, 2]).expect("a part");
for at in [0, 17, PART - 1] {
let want = row(part * PART + at);
for (column, value) in want.iter().enumerate() {
assert_eq!(&chunk.value_at(at, column), value, "part {part} row {at}");
}
}
}
}
#[test]
fn stripes_prepared_before_any_is_merged_write_the_same_bytes_as_one_at_a_time() {
let alone = path("alone");
let mut writer = Writer::create(&alone, "t", fields()).expect("a file");
for run in runs() {
writer.append_stripe(run).expect("a stripe");
}
writer.finish().expect("commit");
let split = path("split");
let mut writer = Writer::create(&split, "t", fields()).expect("a file");
let preparer = writer.preparer();
let prepared = runs()
.into_iter()
.map(|run| preparer.prepare(run).expect("prepared"))
.collect::<Vec<_>>();
for one in prepared {
writer.append_prepared(one).expect("a stripe");
}
assert!(!preparer.coded[2].load(Atomic::Relaxed), "note lost its dictionary");
assert!(preparer.coded[1].load(Atomic::Relaxed), "city kept its dictionary");
writer.finish().expect("commit");
assert_eq!(fs::read(&alone).expect("read"), fs::read(&split).expect("read"));
check(&split);
fs::remove_file(alone).expect("remove");
fs::remove_file(split).expect("remove");
}
#[test]
fn stripes_written_in_another_order_than_they_were_merged_read_back() {
let path = path("crossed");
let mut writer = Writer::create(&path, "t", fields()).expect("a file");
let preparer = writer.preparer();
let mut merged = runs()
.into_iter()
.map(|run| writer.merge(preparer.prepare(run).expect("prepared")).expect("merged"))
.map(|merged| merged.pages().expect("paged"))
.collect::<Vec<_>>();
merged.reverse();
for paged in merged {
writer.write(paged).expect("written");
}
writer.finish().expect("commit");
check(&path);
fs::remove_file(path).expect("remove");
}
#[test]
fn a_stripe_of_another_table_is_refused_at_the_merge() {
let path = path("refused");
let mut writer = Writer::create(&path, "t", fields()).expect("a file");
let other = Writer::create(path.with_extension("other"), "u", vec![fields().remove(0)])
.expect("a file");
let prepared = other.preparer().prepare(vec![]).expect("nothing to prepare");
assert!(writer.merge(prepared).is_err());
assert_eq!(writer.table.rows, 0);
drop(other);
fs::remove_file(path.with_extension("other")).expect("remove");
fs::remove_file(path).expect("remove");
}
}