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::{DEFAULT_K, Sketch};
use rudb_stats::{Order, STRIPE_K, 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 stripes: 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> {
build_summary_for(reader, column, false)
}
pub fn build_summary_for(reader: &Reader, column: usize, per_stripe: bool) -> 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 blind = || {
invalid(&format!(
"column {} of {} holds a form with no hash rule, so it has no sketch",
field.name,
reader.table().name()
))
};
let mut whole = Counts::new(1);
let mut stripes = Vec::new();
let mut pass = Pass::new(&field.ty, reader.table().generation());
for stripe in reader.stripe_parts() {
pass.open_stripe();
let mut counted = per_stripe.then(|| Counts::new(1));
for part in stripe {
let chunk = reader.read(part, &[column])?;
match counted.as_mut() {
Some(counted) => counted.add(&chunk),
None => whole.add(&chunk),
}
pass.scan(chunk.column(0)?);
}
pass.close_stripe();
if let Some(counted) = counted {
stripes.push(counted.sketch(0).ok_or_else(blind)?);
}
}
if !per_stripe {
return Ok(pass.finish(whole.sketch(0).ok_or_else(blind)?, Vec::new()));
}
let mut merged = Sketch::new(DEFAULT_K)?;
for stripe in &stripes {
merged = merged.union(stripe)?;
}
let narrowed =
stripes.iter().map(|stripe| stripe.narrowed(STRIPE_K)).collect::<Result<Vec<_>>>()?;
Ok(pass.finish(merged, narrowed))
}
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_low: Option<Bound>,
stripe_high: Option<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_low: None,
stripe_high: 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) {
self.bytes_value(bytes);
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.measure(width);
let ordering = self.previous.as_ref().map(|previous| previous.order(&bound));
self.run(ordering);
if takes(&self.low, &bound, Ordering::Less) {
self.low = Some(bound.clone());
}
if takes(&self.high, &bound, Ordering::Greater) {
self.high = Some(bound.clone());
}
if takes(&self.stripe_low, &bound, Ordering::Less) {
self.stripe_low = Some(bound.clone());
}
if takes(&self.stripe_high, &bound, Ordering::Greater) {
self.stripe_high = Some(bound.clone());
}
self.previous = Some(bound);
}
fn bytes_value(&mut self, bytes: &[u8]) {
self.measure(bytes.len() as u64);
let ordering = match &self.previous {
None => None,
Some(Bound::Bytes(previous)) => Some(Some(previous.as_slice().cmp(bytes))),
Some(_) => Some(None),
};
self.run(ordering);
if takes_bytes(&self.low, bytes, Ordering::Less) {
fill(&mut self.low, bytes);
}
if takes_bytes(&self.high, bytes, Ordering::Greater) {
fill(&mut self.high, bytes);
}
if takes_bytes(&self.stripe_low, bytes, Ordering::Less) {
fill(&mut self.stripe_low, bytes);
}
if takes_bytes(&self.stripe_high, bytes, Ordering::Greater) {
fill(&mut self.stripe_high, bytes);
}
fill(&mut self.previous, bytes);
}
fn measure(&mut self, width: u64) {
self.bytes = self.bytes.saturating_add(width);
self.widest = self.widest.max(width);
}
fn run(&mut self, ordering: Option<Option<Ordering>>) {
match ordering {
None => self.runs = 1,
Some(Some(Ordering::Less)) => self.descending = false,
Some(Some(Ordering::Greater)) => {
self.ascending = false;
self.runs += 1;
}
Some(Some(Ordering::Equal)) => {}
Some(None) => {
self.ascending = false;
self.descending = false;
}
}
}
fn open_stripe(&mut self) {
self.stripe_low = None;
self.stripe_high = None;
}
fn close_stripe(&mut self) {
if let (Some(low), Some(high)) = (self.stripe_low.take(), self.stripe_high.take()) {
self.stripes.push((low, high));
}
}
fn finish(self, sketch: Sketch, stripes: Vec<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,
};
let sketches = match Sketches::new(sketch.clone(), stripes) {
Ok(sketches) => sketches,
Err(_) => Sketches::merged(sketch),
};
Stats { summary, sketches }
}
}
fn takes(held: &Option<Bound>, bound: &Bound, want: Ordering) -> bool {
match held {
None => true,
Some(held) => bound.order(held) == Some(want),
}
}
fn takes_bytes(held: &Option<Bound>, bytes: &[u8], want: Ordering) -> bool {
match held {
None => true,
Some(Bound::Bytes(held)) => bytes.cmp(held.as_slice()) == want,
Some(_) => false,
}
}
fn fill(held: &mut Option<Bound>, bytes: &[u8]) {
match held {
Some(Bound::Bytes(held)) => {
held.clear();
held.extend_from_slice(bytes);
}
held => *held = Some(Bound::Bytes(bytes.to_vec())),
}
}
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)
}
#[must_use]
pub fn read_columns(reader: &Reader) -> Vec<usize> {
let generation = reader.table().generation();
let mut promoted = reader
.table()
.sections()
.iter()
.filter(|held| held.among(section::GRAPH_KINDS) && held.usable(generation))
.filter_map(|held| usize::try_from(held.id).ok())
.collect::<Vec<_>>();
promoted.sort_unstable();
promoted.dedup();
promoted
}
pub fn build_stats_within(
path: &Path,
table: &str,
columns: &[usize],
share: u64,
) -> Result<Vec<Built>> {
let promoted = read_columns(&Catalog::open(path)?.table(table)?);
build_stats_for(path, table, columns, &promoted, share)
}
pub fn build_stats_for(
path: &Path,
table: &str,
columns: &[usize],
per_stripe: &[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_for(&reader, column, per_stripe.contains(&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(),
stripes: stats.sketches.stripes.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() {
if !held.among(section::STATISTICS_KINDS) {
continue;
}
let replaced = 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 table_of_parts(label: &str, values: &[Option<i64>], per_part: usize) -> 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(per_part) {
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 rows_of(reader: &Reader) -> Vec<Value> {
let mut out = Vec::new();
for part in 0..reader.parts() {
let chunk = reader.read(part, &[0]).expect("a part reads back");
for row in 0..chunk.len() {
out.push(chunk.value_at(0, row));
}
}
out
}
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 a_column_something_declared_a_key_over_is_sketched_per_stripe_and_a_plain_one_is_not() {
let values = (1..=19_200_i64).map(Some).collect::<Vec<_>>();
let path = table_of_parts("promoted", &values, 100);
let plain = build_stats(&path, "t", &[0]).expect("build");
assert_eq!(plain[0].stripes, 0, "nothing has declared anything over this column yet");
crate::graph::build_key_maps(&path, "t", &[0]).expect("a key map declares it a key");
let promoted = build_stats(&path, "t", &[0]).expect("rebuild");
assert!(promoted[0].stripes > 1, "{} stripes, wanted more than one", promoted[0].stripes);
assert!(promoted[0].built, "and they fit");
assert_eq!(promoted[0].distinct, plain[0].distinct, "the merged count did not move");
let reader = reopen(&path);
let sketches = sketches(&reader, 0).expect("the sketches came back");
assert_eq!(sketches.stripes.len(), promoted[0].stripes);
assert!(
sketches.stripes.iter().all(|stripe| stripe.k() == STRIPE_K),
"a stripe sketch is written down at the smaller k"
);
let floor = sketches.floor(0, sketches.stripes.len()).expect("a floor over every stripe");
let actual = 19_200.0;
assert!(
(floor - actual).abs() / actual < 0.25,
"{floor:.0} over every stripe against {actual:.0}"
);
drop(reader);
fs::remove_file(&path).expect("clean up");
}
#[test]
fn a_file_from_before_the_section_table_opens_and_every_statistic_is_unknown() {
let values = (1..=3000_i64).map(Some).collect::<Vec<_>>();
let current = table_of("with_sections", &values);
let older = table_of("before_sections", &values);
build_stats(¤t, "t", &[0]).expect("this build states what its columns hold");
let file = fs::OpenOptions::new().write(true).open(&older).expect("reopen to patch");
crate::write_at(&file, 8, &22_u32.to_le_bytes()).expect("stamp the older format");
drop(file);
let new = reopen(¤t);
assert!(summary(&new, 0).is_some(), "the file this build wrote says what it holds");
let old = reopen(&older);
assert!(old.table().sections().is_empty(), "an older file names no sections");
assert!(summary(&old, 0).is_none(), "and so says nothing about its columns");
assert!(sketches(&old, 0).is_none());
assert!(read_columns(&old).is_empty(), "nor promotes any of them");
assert_eq!(rows_of(&old), rows_of(&new), "and answers what the newer file answers");
drop(new);
drop(old);
fs::remove_file(¤t).expect("clean up");
fs::remove_file(&older).expect("clean up");
}
#[test]
fn the_stripe_ends_say_whether_a_scan_can_skip_and_a_shuffle_says_it_cannot() {
let sorted = (1..=19_200_i64).map(Some).collect::<Vec<_>>();
let ordered = table_of_parts("stripes_sorted", &sorted, 100);
build_stats(&ordered, "t", &[0]).expect("build");
let reader = reopen(&ordered);
let ordered_summary = summary(&reader, 0).expect("the summary");
assert!(!ordered_summary.overlapping, "a sorted column's stripes are disjoint");
assert_eq!(ordered_summary.low, Some(Bound::Int(1)));
assert_eq!(ordered_summary.high, Some(Bound::Int(19_200)));
drop(reader);
let shuffled = (0..19_200_i64).map(|at| Some(1 + at * 7919 % 19_200)).collect::<Vec<_>>();
let mixed = table_of_parts("stripes_shuffled", &shuffled, 100);
build_stats(&mixed, "t", &[0]).expect("build");
let reader = reopen(&mixed);
let mixed_summary = summary(&reader, 0).expect("the summary");
assert!(mixed_summary.overlapping, "a shuffled column's stripes all span it");
assert_eq!(mixed_summary.low, Some(Bound::Int(1)), "the same values in a different order");
assert_eq!(mixed_summary.high, Some(Bound::Int(19_200)));
drop(reader);
fs::remove_file(&ordered).expect("clean up");
fs::remove_file(&mixed).expect("clean up");
}
#[test]
fn the_graph_sections_do_not_count_against_the_statistics_budget() {
let values = (1..=3000_i64).map(Some).collect::<Vec<_>>();
let path = table_of("apart", &values);
crate::graph::build_key_maps(&path, "t", &[0]).expect("a key map first");
let reader = reopen(&path);
let graph = reader
.table()
.sections()
.iter()
.filter(|held| held.among(section::GRAPH_KINDS))
.count();
assert_eq!(graph, 1, "the key map is in the file");
assert_eq!(held_bytes(&reader, &[0]).expect("held"), 0, "and it is not the statistics'");
drop(reader);
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);
}
}