use std::cmp::Ordering;
use std::path::Path;
use std::time::{Duration, Instant};
use rudb_common::bounds::{self, Bound};
use rudb_common::stat::Class;
use rudb_common::{LogicalType, Result, Value};
use rudb_encoding::sketch::Sketch;
use rudb_stats::{Order, Sketches, Summary, sketches::HEADER_BYTES as SKETCH_HEADER};
use rudb_storage::count::{Counts, countable};
use rudb_vector::Vector;
use crate::section::{self, Attachment};
use crate::{Catalog, Reader, invalid};
pub const BUDGET_SHARE: u64 = 2;
pub const BUDGET_FLOOR: u64 = 64 * 1024;
#[derive(Debug, Clone)]
pub struct Built {
pub column: usize,
pub rows: u64,
pub distinct: u64,
pub exact: bool,
pub order: Order,
pub summary_bytes: usize,
pub sketch_bytes: usize,
pub column_bytes: u64,
pub built: bool,
pub build: Duration,
}
impl Built {
#[must_use]
pub fn bytes(&self) -> usize {
self.summary_bytes + self.sketch_bytes
}
}
#[derive(Debug, Clone)]
pub struct Stats {
pub summary: Summary,
pub sketches: Sketches,
}
pub fn build_summary(reader: &Reader, column: usize) -> Result<Stats> {
let fields = reader.table().fields();
let Some(field) = fields.get(column) else {
return Err(invalid(&format!(
"column {column} is past the {} of table {}",
fields.len(),
reader.table().name()
)));
};
if !countable(&field.ty) {
return Err(invalid(&format!(
"a summary of {} needs a hash rule, and {} has none",
field.name, field.ty
)));
}
let mut counts = Counts::new(1);
let mut pass = Pass::new(&field.ty, reader.table().generation());
for stripe in reader.stripe_parts() {
pass.open_stripe();
for part in stripe {
let chunk = reader.read(part, &[column])?;
counts.add(&chunk);
pass.scan(chunk.column(0)?);
}
pass.close_stripe();
}
let Some(sketch) = counts.sketch(0) else {
return Err(invalid(&format!(
"column {} of {} holds a form with no hash rule, so it has no sketch",
field.name,
reader.table().name()
)));
};
Ok(pass.finish(sketch))
}
struct Pass {
rows: u64,
nulls: u64,
low: Option<Bound>,
high: Option<Bound>,
bounded: bool,
ascending: bool,
descending: bool,
runs: u64,
previous: Option<Bound>,
bytes: u64,
widest: u64,
generation: u64,
stripe: Option<(Bound, Bound)>,
stripes: Vec<(Bound, Bound)>,
fixed: Option<u64>,
scale: Option<u8>,
}
impl Pass {
fn new(ty: &LogicalType, generation: u64) -> Self {
Self {
rows: 0,
nulls: 0,
low: None,
high: None,
bounded: true,
ascending: true,
descending: true,
runs: 0,
previous: None,
bytes: 0,
widest: 0,
generation,
stripe: None,
stripes: Vec::new(),
fixed: fixed_width(ty),
scale: bounds::scale_of(ty),
}
}
fn scan(&mut self, vector: &Vector) {
for row in 0..vector.len() {
self.rows += 1;
if vector.is_null_at(row) {
self.nulls += 1;
continue;
}
if let Some(signed) = vector.signed_at(row) {
let bound = match self.scale {
Some(scale) => Bound::Scaled { unscaled: signed, scale },
None => Bound::Int(signed),
};
self.value(bound, self.fixed.unwrap_or(8));
continue;
}
if let Some(bytes) = vector.bytes_at(row) {
let width = bytes.len() as u64;
self.value(Bound::Bytes(bytes.to_vec()), width);
continue;
}
let value = vector.value_at(row);
let width = self.fixed.unwrap_or_else(|| width(&value));
match Bound::of_value(&value) {
Some(bound) => self.value(bound, width),
None => {
self.bytes = self.bytes.saturating_add(width);
self.widest = self.widest.max(width);
self.bounded = false;
self.ascending = false;
self.descending = false;
}
}
}
}
fn value(&mut self, bound: Bound, width: u64) {
self.bytes = self.bytes.saturating_add(width);
self.widest = self.widest.max(width);
match &self.previous {
None => self.runs = 1,
Some(previous) => match previous.order(&bound) {
Some(Ordering::Less) => self.descending = false,
Some(Ordering::Greater) => {
self.ascending = false;
self.runs += 1;
}
Some(Ordering::Equal) => {}
None => {
self.ascending = false;
self.descending = false;
}
},
}
self.low = Some(match self.low.take() {
Some(held) => held.smaller(bound.clone()),
None => bound.clone(),
});
self.high = Some(match self.high.take() {
Some(held) => held.larger(bound.clone()),
None => bound.clone(),
});
self.stripe = Some(match self.stripe.take() {
Some((low, high)) => (low.smaller(bound.clone()), high.larger(bound.clone())),
None => (bound.clone(), bound.clone()),
});
self.previous = Some(bound);
}
fn open_stripe(&mut self) {
self.stripe = None;
}
fn close_stripe(&mut self) {
if let Some(range) = self.stripe.take() {
self.stripes.push(range);
}
}
fn finish(self, sketch: Sketch) -> Stats {
let present = self.rows - self.nulls;
let exact = sketch.is_exact();
let distinct = if exact {
sketch.len() as u64
} else {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let estimate = sketch.distinct().round().max(0.0) as u64;
estimate.min(present)
};
let summary = Summary {
rows: self.rows,
nulls: self.nulls,
low: if self.bounded { self.low } else { None },
high: if self.bounded { self.high } else { None },
ends_exact: self.bounded,
distinct,
distinct_class: if exact { Class::Exact } else { Class::Estimated },
unique: exact && distinct == present,
order: if present == 0 {
Order::Neither
} else if self.ascending {
Order::Ascending
} else if self.descending {
Order::Descending
} else {
Order::Neither
},
runs: self.runs,
overlapping: overlapping(&self.stripes),
bytes: self.bytes,
widest: self.widest,
newest: self.generation,
};
Stats { summary, sketches: Sketches::merged(sketch) }
}
}
fn overlapping(stripes: &[(Bound, Bound)]) -> bool {
let mut order = (0..stripes.len()).collect::<Vec<_>>();
order
.sort_by(|&one, &other| stripes[one].0.order(&stripes[other].0).unwrap_or(Ordering::Equal));
order.windows(2).any(|pair| {
let before = &stripes[pair[0]].1;
let after = &stripes[pair[1]].0;
before.order(after) != Some(Ordering::Less)
})
}
fn fixed_width(ty: &LogicalType) -> Option<u64> {
Some(match ty {
LogicalType::Boolean | LogicalType::TinyInt | LogicalType::UTinyInt => 1,
LogicalType::SmallInt | LogicalType::USmallInt => 2,
LogicalType::Integer | LogicalType::UInteger | LogicalType::Float | LogicalType::Date => 4,
LogicalType::HugeInt | LogicalType::UHugeInt | LogicalType::Decimal { .. } => 16,
LogicalType::Varchar | LogicalType::Blob => return None,
_ => 8,
})
}
fn width(value: &Value) -> u64 {
match value {
Value::Null => 0,
Value::Boolean(_) | Value::TinyInt(_) | Value::UTinyInt(_) => 1,
Value::SmallInt(_) | Value::USmallInt(_) => 2,
Value::Integer(_) | Value::UInteger(_) | Value::Float(_) | Value::Date(_) => 4,
Value::HugeInt(_) | Value::UHugeInt(_) | Value::Decimal { .. } => 16,
Value::Varchar(text) => text.len() as u64,
Value::Blob(bytes) => bytes.len() as u64,
_ => 8,
}
}
pub fn build_stats(path: &Path, table: &str, columns: &[usize]) -> Result<Vec<Built>> {
build_stats_within(path, table, columns, BUDGET_SHARE)
}
pub fn build_stats_within(
path: &Path,
table: &str,
columns: &[usize],
share: u64,
) -> Result<Vec<Built>> {
let reader = Catalog::open(path)?.table(table)?;
let column_bytes = reader.layout().columns_total();
let allowance = (column_bytes.saturating_mul(share) / 100).max(BUDGET_FLOOR);
let mut spent = held_bytes(&reader, columns)?;
let mut report = Vec::with_capacity(columns.len());
let mut payloads = Vec::with_capacity(columns.len());
for &column in columns {
let start = Instant::now();
let stats = build_summary(&reader, column)?;
let mut summary = Vec::new();
stats.summary.encode(&mut summary)?;
let mut sketches = Vec::new();
stats.sketches.encode(&mut sketches)?;
report.push(Built {
column,
rows: stats.summary.rows,
distinct: stats.summary.distinct,
exact: stats.summary.distinct_class == Class::Exact,
order: stats.summary.order,
summary_bytes: summary.len(),
sketch_bytes: sketches.len(),
column_bytes,
built: false,
build: start.elapsed(),
});
payloads.push((column, summary, sketches));
}
let mut order = (0..payloads.len()).collect::<Vec<_>>();
order.sort_by_key(|&at| report[at].bytes());
let mut keep = vec![false; payloads.len()];
for at in order {
let cost = report[at].bytes() as u64;
if spent.saturating_add(cost) <= allowance {
spent += cost;
keep[at] = true;
report[at].built = true;
}
}
drop(reader);
let mut attachments = Vec::with_capacity(payloads.len() * 2);
for ((column, summary, sketches), _) in payloads.iter().zip(&keep).filter(|&(_, &keep)| keep) {
let id = u64::try_from(*column).map_err(|_| invalid("column index overflow"))?;
attachments.push(Attachment {
kind: *section::SUMMARY,
id,
flags: 0,
header_bytes: u32::try_from(summary.len())
.map_err(|_| invalid("a summary longer than a u32 can count"))?,
bytes: summary,
});
attachments.push(Attachment {
kind: *section::SKETCHES,
id,
flags: 0,
header_bytes: SKETCH_HEADER,
bytes: sketches,
});
}
crate::attach(path, table, &attachments)?;
Ok(report)
}
fn held_bytes(reader: &Reader, replacing: &[usize]) -> Result<u64> {
let mut total = 0;
for held in reader.table().sections() {
let mine = held.kind == *section::SUMMARY || held.kind == *section::SKETCHES;
let replaced = mine && replacing.iter().any(|&column| u64::try_from(column) == Ok(held.id));
if replaced || !held.usable(reader.table().generation()) {
continue;
}
let Ok(extents) = reader.extents(held) else { continue };
total += extents.iter().map(|extent| u64::from(extent.length)).sum::<u64>();
}
Ok(total)
}
#[must_use]
pub fn summary(reader: &Reader, column: usize) -> Option<Summary> {
let bytes = payload(reader, column, section::SUMMARY)?;
Summary::decode(&bytes).ok()
}
#[must_use]
pub fn sketches(reader: &Reader, column: usize) -> Option<Sketches> {
let bytes = payload(reader, column, section::SKETCHES)?;
Sketches::decode(&bytes).ok()
}
fn payload(reader: &Reader, column: usize, kind: &[u8; 8]) -> Option<Vec<u8>> {
let table = reader.table();
let id = u64::try_from(column).ok()?;
let held = table.sections().iter().find(|section| section.kind == *kind && section.id == id)?;
if !held.usable(table.generation()) {
return None;
}
reader.payload(held).ok()
}
#[must_use]
pub fn summarizable(ty: &LogicalType) -> bool {
countable(ty)
}
#[cfg(test)]
mod tests {
use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
use rudb_common::Field;
use rudb_encoding::sketch::hash64;
use rudb_storage::count::hash_value;
use rudb_vector::{Chunk, Vector};
use super::*;
use crate::Writer;
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-stats-{label}-{}-{stamp}.rdb", std::process::id()))
}
fn table_of(label: &str, values: &[Option<i64>]) -> PathBuf {
let path = path(label);
let mut writer =
Writer::create(&path, "t", vec![Field::new("v", LogicalType::BigInt)]).expect("new");
for part in values.chunks(1000) {
let held =
part.iter().map(|v| v.map_or(Value::Null, Value::BigInt)).collect::<Vec<_>>();
let chunk =
Chunk::new(vec![Vector::from_values(LogicalType::BigInt, &held).expect("values")])
.expect("one column");
writer.append(&chunk).expect("a part");
}
writer.finish().expect("commit");
path
}
fn reopen(path: &PathBuf) -> Reader {
Catalog::open(path).expect("reopen").table("t").expect("the table")
}
#[test]
fn a_summary_built_over_a_file_says_what_the_column_holds() {
let values = (1..=3000_i64).map(Some).collect::<Vec<_>>();
let path = table_of("sorted", &values);
let built = build_stats(&path, "t", &[0]).expect("build");
assert_eq!(built.len(), 1);
assert!(built[0].built, "a one column table is nowhere near the budget");
assert_eq!(built[0].rows, 3000);
assert_eq!(built[0].distinct, 3000);
assert!(built[0].exact, "three thousand values is under the default k");
assert_eq!(built[0].order, Order::Ascending);
let reader = reopen(&path);
let summary = summary(&reader, 0).expect("the summary is in the file");
assert_eq!(summary.rows, 3000);
assert_eq!(summary.nulls, 0);
assert_eq!(summary.low, Some(Bound::Int(1)));
assert_eq!(summary.high, Some(Bound::Int(3000)));
assert!(summary.ends_exact);
assert!(summary.unique, "a sorted run of distinct values is a key candidate");
assert_eq!(summary.runs, 1, "one ascending run");
assert_eq!(summary.distinct_class, Class::Exact);
assert_eq!(summary.newest, reader.table().generation());
let sketches = sketches(&reader, 0).expect("the sketches are in the file");
assert!(sketches.merged.is_exact());
assert!(sketches.stripes.is_empty(), "the per stripe rule gives this column none");
fs::remove_file(&path).expect("clean up");
}
#[test]
fn nulls_are_counted_and_do_not_reach_the_ends_or_the_sketch() {
let values: Vec<Option<i64>> =
(0..2000).map(|at| if at % 3 == 0 { None } else { Some(at) }).collect();
let path = table_of("nulls", &values);
build_stats(&path, "t", &[0]).expect("build");
let reader = reopen(&path);
let summary = summary(&reader, 0).expect("the summary");
let nulls = values.iter().filter(|v| v.is_none()).count() as u64;
assert_eq!(summary.rows, 2000);
assert_eq!(summary.nulls, nulls);
assert_eq!(summary.present(), 2000 - nulls);
assert_eq!(summary.distinct, 2000 - nulls, "a null is not a distinct value");
assert_eq!(summary.low, Some(Bound::Int(1)), "zero is null here");
assert!(summary.unique);
fs::remove_file(&path).expect("clean up");
}
#[test]
fn a_column_that_repeats_is_not_reported_unique_and_a_descending_one_is_seen() {
let values = (0..2000_i64).map(|at| Some(-(at / 2))).collect::<Vec<_>>();
let path = table_of("repeats", &values);
build_stats(&path, "t", &[0]).expect("build");
let reader = reopen(&path);
let summary = summary(&reader, 0).expect("the summary");
assert_eq!(summary.distinct, 1000);
assert!(!summary.unique, "every value appears twice");
assert_eq!(summary.order, Order::Descending);
assert_eq!(summary.runs, 1000, "a descending column is a run per distinct value");
fs::remove_file(&path).expect("clean up");
}
#[test]
fn a_column_past_the_default_k_is_estimated_and_says_so() {
let values = (0..20_000_i64).map(Some).collect::<Vec<_>>();
let path = table_of("estimated", &values);
let built = build_stats(&path, "t", &[0]).expect("build");
assert!(!built[0].exact, "twenty thousand values is past the default k");
let reader = reopen(&path);
let summary = summary(&reader, 0).expect("the summary");
assert_eq!(summary.distinct_class, Class::Estimated);
assert!(!summary.unique, "uniqueness is never claimed off an estimate");
assert!(summary.distinct > 17_000 && summary.distinct <= 20_000, "{}", summary.distinct);
assert!(summary.distinct <= summary.present(), "more distinct values than rows");
fs::remove_file(&path).expect("clean up");
}
#[test]
fn a_shuffled_column_is_neither_ordered_nor_one_run() {
let values = (0..2000_i64).map(|at| Some((at * 7919) % 2000)).collect::<Vec<_>>();
let path = table_of("shuffled", &values);
build_stats(&path, "t", &[0]).expect("build");
let reader = reopen(&path);
let summary = summary(&reader, 0).expect("the summary");
assert_eq!(summary.order, Order::Neither);
assert!(summary.runs > 100, "a shuffle is many runs, not one: {}", summary.runs);
assert_eq!(summary.low, Some(Bound::Int(0)));
assert_eq!(summary.high, Some(Bound::Int(1999)));
fs::remove_file(&path).expect("clean up");
}
#[test]
fn deleting_the_sections_changes_nothing_but_whether_they_are_there() {
let values = (1..=1500_i64).map(Some).collect::<Vec<_>>();
let path = table_of("invariant", &values);
build_stats(&path, "t", &[0]).expect("build");
let reader = reopen(&path);
assert!(summary(&reader, 0).is_some());
let generation = reader.table().generation();
let held: Vec<_> = reader
.table()
.sections()
.iter()
.filter(|s| s.kind == *section::SUMMARY || s.kind == *section::SKETCHES)
.copied()
.collect();
assert_eq!(held.len(), 2, "a summary and a sketch section");
for section in &held {
assert!(section.usable(generation));
assert!(!section.usable(generation + 1), "a rewrite invalidates rather than corrupts");
}
let rows: usize =
(0..reader.parts()).map(|part| reader.read(part, &[0]).expect("a part").len()).sum();
assert_eq!(rows, 1500, "the scan is the scan whether the sections are read or not");
fs::remove_file(&path).expect("clean up");
}
#[test]
fn a_string_column_is_read_through_the_typed_path_and_measured_by_its_bytes() {
let path = path("strings");
let mut writer =
Writer::create(&path, "t", vec![Field::new("v", LogicalType::Varchar)]).expect("new");
let words = ["alpha", "bravo", "charlie", "delta", "alpha"];
let held = words.iter().map(|w| Value::Varchar((*w).into())).collect::<Vec<_>>();
let chunk =
Chunk::new(vec![Vector::from_values(LogicalType::Varchar, &held).expect("words")])
.expect("one column");
writer.append(&chunk).expect("a part");
writer.finish().expect("commit");
build_stats(&path, "t", &[0]).expect("build");
let reader = reopen(&path);
let summary = summary(&reader, 0).expect("the summary");
assert_eq!(summary.rows, 5);
assert_eq!(summary.distinct, 4, "alpha twice");
assert!(!summary.unique);
assert_eq!(summary.bytes, words.iter().map(|w| w.len() as u64).sum::<u64>());
assert_eq!(summary.widest, 7, "charlie");
assert_eq!(summary.low, Some(Bound::Bytes(b"alpha".to_vec())));
assert_eq!(summary.high, Some(Bound::Bytes(b"delta".to_vec())));
drop(reader);
fs::remove_file(&path).expect("clean up");
}
#[test]
fn a_type_with_no_hash_rule_is_refused_by_name_rather_than_summarized_as_empty() {
let path = table_of("refused", &[Some(1)]);
let reader = reopen(&path);
assert!(summarizable(&LogicalType::BigInt));
assert!(!summarizable(&LogicalType::Interval));
assert!(build_summary(&reader, 1).is_err(), "a column past the end");
drop(reader);
fs::remove_file(&path).expect("clean up");
}
#[test]
fn the_stored_sketch_depends_on_the_value_rule_and_not_only_on_the_hash() {
assert_eq!(hash_value(&Value::BigInt(1)), Some(hash64(&1_u128.to_le_bytes())));
assert_eq!(hash_value(&Value::Integer(1)), hash_value(&Value::BigInt(1)));
assert_eq!(hash_value(&Value::Varchar("a".into())), Some(hash64(b"a")));
assert_eq!(hash_value(&Value::Null), None);
}
}