use std::collections::BTreeMap;
use rudb_common::bounds::Test;
use rudb_common::stat::{Class, Direction, Provenance, Stat};
use rudb_plan::{
ColumnBinding, CompareOp, ConjunctionOp, Expr, ExprRef, JoinKind, Node, NodeRef, Plan,
SetOpKind, Slice,
};
use crate::{bounds, walk};
const KEPT_BY_A_CONDITION: f64 = 0.2;
const KEPT_BY_A_GROUP_BY: f64 = 0.1;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Statistics {
tables: BTreeMap<(String, String, String), u64>,
columns: BTreeMap<(String, String, String, String), u64>,
}
impl Statistics {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn record(&mut self, catalog: &str, schema: &str, table: &str, rows: u64) {
self.tables.insert((catalog.to_owned(), schema.to_owned(), table.to_owned()), rows);
}
#[must_use]
pub fn rows_in(&self, catalog: &str, schema: &str, table: &str) -> Option<u64> {
self.tables.get(&(catalog.to_owned(), schema.to_owned(), table.to_owned())).copied()
}
pub fn record_distinct(
&mut self,
catalog: &str,
schema: &str,
table: &str,
column: &str,
distinct: u64,
) {
let key = (catalog.to_owned(), schema.to_owned(), table.to_owned(), column.to_owned());
self.columns.insert(key, distinct);
}
#[must_use]
pub fn distinct_in(
&self,
catalog: &str,
schema: &str,
table: &str,
column: &str,
) -> Option<u64> {
let key = (catalog.to_owned(), schema.to_owned(), table.to_owned(), column.to_owned());
self.columns.get(&key).copied()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.tables.is_empty() && self.columns.is_empty()
}
}
const GUESSED: Class = Class::Estimated;
const FROM_A_CONSTANT: Provenance = Provenance::Default;
const CEILING: Class = Class::Certified { bound: 1.0, direction: Direction::AtMost };
#[must_use]
pub fn rows(plan: &Plan, node: NodeRef, stats: &Statistics) -> Option<u64> {
rows_stat(plan, node, stats).decide().copied()
}
#[must_use]
pub fn rows_stat(plan: &Plan, node: NodeRef, stats: &Statistics) -> Stat<u64> {
let of = |child: NodeRef| rows_stat(plan, child, stats);
match *plan.node(node) {
Node::Dummy => Stat::exact(1, Provenance::RowCount),
Node::Get { catalog, schema, table, .. } => {
match stats.rows_in(plan.string(catalog), plan.string(schema), plan.string(table)) {
Some(rows) => Stat::exact(rows, Provenance::RowCount),
None => Stat::Unknown,
}
}
Node::Values { rows: list, .. } => u64::try_from(plan.row_list(list).len())
.map_or(Stat::Unknown, |rows| Stat::exact(rows, Provenance::RowCount)),
Node::TableFunction { index, .. } => plan.measured(index),
Node::LateralFunction { .. } => Stat::Unknown,
Node::Filter { input, predicate } => {
let (kept, from) = kept(plan, predicate, stats);
match surviving(plan, input, predicate) {
Some(0) => Stat::exact(0, Provenance::ZoneMap),
Some(ceiling) => capped(guess_from(of(input), kept, from), ceiling),
None => guess_from(of(input), kept, from),
}
}
Node::Project { input, .. }
| Node::Window { input, .. }
| Node::Sort { input, .. }
| Node::Fetch { input, .. }
| Node::TableFetch { input, .. } => of(input),
Node::Aggregate { input, groups, .. } => {
if plan.expr_list(groups).is_empty() {
return Stat::exact(1, Provenance::RowCount);
}
guess(of(input), KEPT_BY_A_GROUP_BY)
}
Node::Distinct { input, .. } => guess(of(input), KEPT_BY_A_GROUP_BY),
Node::Limit { input, count, offset } => {
let input = of(input);
match count {
None => input.map(|n| n.saturating_sub(offset)),
Some(count) => match input {
Stat::Unknown => {
Stat::Known { value: count, class: CEILING, provenance: FROM_A_CONSTANT }
}
known => known.map(|n| n.saturating_sub(offset).min(count)),
},
}
}
Node::TopN { input, count, offset, .. } => match of(input) {
Stat::Unknown => {
Stat::Known { value: count, class: CEILING, provenance: FROM_A_CONSTANT }
}
known => known.map(|n| n.saturating_sub(offset).min(count)),
},
Node::Join { left, right, kind, conditions, .. } => join(
of(left),
of(right),
kind,
plan.expr_list(conditions).len(),
keyspace(plan, conditions, stats),
),
Node::DependentJoin { .. } => Stat::Unknown,
Node::CrossProduct { left, right } => of(left).zip(of(right), u64::saturating_mul),
Node::MaterializedCte { body, .. } => of(body),
Node::CteScan { .. } => Stat::Unknown,
Node::SetOp { left, right, kind, all, .. } => {
let total = of(left).zip(of(right), u64::saturating_add);
match (kind, all) {
(SetOpKind::Union, true) => total,
_ => ceiling(total),
}
}
}
}
fn guess(input: Stat<u64>, kept: f64) -> Stat<u64> {
guess_from(input, kept, FROM_A_CONSTANT)
}
fn guess_from(input: Stat<u64>, kept: f64, from: Provenance) -> Stat<u64> {
match input {
Stat::Unknown => Stat::Unknown,
Stat::Known { value, class, .. } => Stat::Known {
value: scale(value, kept).max(1),
class: class.combine(GUESSED),
provenance: from,
},
}
}
fn capped(guessed: Stat<u64>, ceiling: u64) -> Stat<u64> {
match guessed {
Stat::Unknown => {
Stat::Known { value: ceiling, class: CEILING, provenance: Provenance::ZoneMap }
}
Stat::Known { value, .. } if ceiling < value => {
Stat::Known { value: ceiling, class: CEILING, provenance: Provenance::ZoneMap }
}
known => known,
}
}
fn ceiling(stat: Stat<u64>) -> Stat<u64> {
match stat {
Stat::Unknown => Stat::Unknown,
Stat::Known { value, class, provenance } => {
Stat::Known { value, class: class.combine(CEILING), provenance }
}
}
}
fn kept(plan: &Plan, predicate: ExprRef, stats: &Statistics) -> (f64, Provenance) {
let mut fraction = 1.0;
let (mut counted, mut guessed) = (0_u32, 0_u32);
for conjunct in conjuncts(plan, predicate) {
match values(plan, conjunct, stats) {
Some(values) => {
fraction /= widened(values);
counted += 1;
}
None => {
fraction *= KEPT_BY_A_CONDITION;
guessed += 1;
}
}
}
let from = match (counted, guessed) {
(0, _) => FROM_A_CONSTANT,
(_, 0) => Provenance::Sketch,
_ => Provenance::Propagation,
};
(fraction, from)
}
fn conjuncts(plan: &Plan, predicate: ExprRef) -> Vec<ExprRef> {
let parts = match *plan.expr(predicate) {
Expr::Conjunction { op: ConjunctionOp::And, children } => plan.expr_list(children).to_vec(),
_ => vec![predicate],
};
parts.into_iter().filter(|&part| !walk::constant(plan, part)).take(8).collect()
}
fn values(plan: &Plan, conjunct: ExprRef, stats: &Statistics) -> Option<u64> {
let Expr::Compare { op: CompareOp::Equal, left, right } = *plan.expr(conjunct) else {
return None;
};
let binding = match (plan.expr(left), plan.expr(right)) {
(&Expr::Column(binding), _) if walk::constant(plan, right) => binding,
(_, &Expr::Column(binding)) if walk::constant(plan, left) => binding,
_ => return None,
};
stated(plan, binding, stats).filter(|&values| values > 0)
}
fn surviving(plan: &Plan, input: NodeRef, predicate: ExprRef) -> Option<u64> {
let index = bounds::scanned(plan, input)?;
let zones = plan.zones(index)?;
let names = match *plan.node(input) {
Node::Get { columns, .. } | Node::TableFunction { columns, .. } => plan.field_list(columns),
_ => return None,
};
let read = bounds::of(plan, input, predicate);
if read.is_empty() {
return None;
}
let mut tests = Vec::with_capacity(read.len());
for (position, op, value) in read {
let name = &names.get(position)?.name;
tests.push(Test { column: zones.column(name)?, op, value });
}
zones.surviving(&tests)
}
fn distinct(plan: &Plan, binding: ColumnBinding, stats: &Statistics) -> Option<u64> {
follow(plan, binding, stats, Missing::Rows, 16)
}
fn stated(plan: &Plan, binding: ColumnBinding, stats: &Statistics) -> Option<u64> {
follow(plan, binding, stats, Missing::Nothing, 16)
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Missing {
Rows,
Nothing,
}
fn follow(
plan: &Plan,
binding: ColumnBinding,
stats: &Statistics,
missing: Missing,
depth: u32,
) -> Option<u64> {
let depth = depth.checked_sub(1)?;
let position = binding.column as usize;
let rows = missing == Missing::Rows;
for at in 0..u32::try_from(plan.node_count()).unwrap_or(u32::MAX) {
match *plan.node(at) {
Node::Get { catalog, schema, table, index, columns, .. } if index == binding.table => {
let name = &plan.field_list(columns).get(position)?.name;
let catalog = plan.string(catalog);
let schema = plan.string(schema);
let table = plan.string(table);
if let Some(distinct) = stats.distinct_in(catalog, schema, table, name) {
return Some(distinct);
}
return if rows { stats.rows_in(catalog, schema, table) } else { None };
}
Node::TableFunction { index, columns, .. } if index == binding.table => {
let name = &plan.field_list(columns).get(position)?.name;
if let Some(distinct) = plan.distinct_measured(index, name) {
return Some(distinct);
}
return if rows { plan.measured(index).decide().copied() } else { None };
}
Node::Project { index, exprs, .. } if index == binding.table => {
let &carried = plan.expr_list(exprs).get(position)?;
let &Expr::Column(carried) = plan.expr(carried) else {
return None;
};
return follow(plan, carried, stats, missing, depth);
}
_ => {}
}
}
None
}
fn keyspace(plan: &Plan, conditions: Slice, stats: &Statistics) -> Option<u64> {
keyspace_of(plan, plan.expr_list(conditions), stats)
}
#[must_use]
pub fn keyspace_of(plan: &Plan, conditions: &[ExprRef], stats: &Statistics) -> Option<u64> {
if conditions.is_empty() {
return None;
}
let mut product: u64 = 1;
for &condition in conditions {
let Expr::Compare { op: CompareOp::Equal | CompareOp::NotDistinctFrom, left, right } =
*plan.expr(condition)
else {
return None;
};
let (&Expr::Column(left), &Expr::Column(right)) = (plan.expr(left), plan.expr(right))
else {
return None;
};
let pair = distinct(plan, left, stats)?.max(distinct(plan, right, stats)?);
product = product.checked_mul(pair)?;
}
(product > 0).then_some(product)
}
#[must_use]
pub fn matched(left: u64, right: u64, keys: Option<u64>) -> u64 {
let counted = keys.map_or(0, |keys| left.saturating_mul(right) / keys);
left.max(right).max(counted)
}
fn join(
left: Stat<u64>,
right: Stat<u64>,
kind: JoinKind,
conditions: usize,
keys: Option<u64>,
) -> Stat<u64> {
match kind {
JoinKind::Semi => guess(left, KEPT_BY_A_CONDITION),
JoinKind::Anti => guess(left, 1.0 - KEPT_BY_A_CONDITION),
JoinKind::Single | JoinKind::Mark => left,
JoinKind::Positional => left.zip(right, u64::min),
_ => {
let (
Stat::Known { value: left, class: left_class, provenance: left_from },
Stat::Known { value: right, class: right_class, provenance: right_from },
) = (left, right)
else {
return Stat::Unknown;
};
let both = left_class.combine(right_class);
let from = if left_from == right_from { left_from } else { Provenance::Propagation };
if conditions == 0 {
return Stat::Known {
value: left.saturating_mul(right),
class: both,
provenance: from,
};
}
let matched = matched(left, right, keys);
let value = match kind {
JoinKind::Left => matched.max(left),
JoinKind::Right => matched.max(right),
JoinKind::Full => matched.max(left).max(right),
_ => matched,
};
Stat::Known { value, class: both.combine(GUESSED), provenance: FROM_A_CONSTANT }
}
}
}
#[expect(
clippy::cast_precision_loss,
reason = "a count past two to the fifty third is not a count anybody measured"
)]
fn widened(count: u64) -> f64 {
(count as f64).max(1.0)
}
#[expect(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "an estimate going through f64 is the point, and the result is clamped"
)]
fn scale(rows: u64, by: f64) -> u64 {
let scaled = rows as f64 * by;
if scaled.is_finite() && scaled >= 0.0 { scaled.min(u64::MAX as f64) as u64 } else { 0 }
}
#[cfg(test)]
mod tests {
use std::sync::{Arc, Mutex};
use rudb_common::bounds::{Op, Test, Zones};
use rudb_common::stat::{Class, Direction, Provenance, Stat};
use rudb_plan::Plan;
use super::{Statistics, rows, rows_stat};
fn scan(table: &str, index: u32) -> String {
format!("Get memory.main.{table} AS {table} #{index} [a::INTEGER]\n")
}
fn statistics(tables: &[(&str, u64)]) -> Statistics {
let mut stats = Statistics::new();
for (table, count) in tables {
stats.record("memory", "main", table, *count);
}
stats
}
fn estimate(text: &str, tables: &[(&str, u64)]) -> Option<u64> {
let stats = statistics(tables);
let plan =
Plan::parse(text).unwrap_or_else(|error| panic!("{text} did not parse: {error}"));
rows(&plan, plan.root(), &stats)
}
fn stat(text: &str, tables: &[(&str, u64)]) -> Stat<u64> {
let stats = statistics(tables);
let plan =
Plan::parse(text).unwrap_or_else(|error| panic!("{text} did not parse: {error}"));
rows_stat(&plan, plan.root(), &stats)
}
fn counted(text: &str, tables: &[(&str, u64)], columns: &[(&str, &str, u64)]) -> Option<u64> {
let mut stats = statistics(tables);
for (table, column, distinct) in columns {
stats.record_distinct("memory", "main", table, column, *distinct);
}
let plan =
Plan::parse(text).unwrap_or_else(|error| panic!("{text} did not parse: {error}"));
rows(&plan, plan.root(), &stats)
}
fn counted_stat(
text: &str,
tables: &[(&str, u64)],
columns: &[(&str, &str, u64)],
) -> Stat<u64> {
let mut stats = statistics(tables);
for (table, column, distinct) in columns {
stats.record_distinct("memory", "main", table, column, *distinct);
}
let plan =
Plan::parse(text).unwrap_or_else(|error| panic!("{text} did not parse: {error}"));
rows_stat(&plan, plan.root(), &stats)
}
fn filtered(predicate: &str) -> String {
format!("Filter {predicate}\n Get memory.main.t AS t #0 [a::INTEGER, b::INTEGER]\n")
}
fn joined(left: &str, right: &str) -> String {
format!(
"Join INNER on=[(#0.0::INTEGER = #1.0::INTEGER)::BOOLEAN]\n {} {}",
scan(left, 0),
scan(right, 1)
)
}
const GUESSED: Class = Class::Estimated;
#[test]
fn a_scan_is_what_the_catalog_said_and_nothing_when_nobody_said() {
let text = scan("t", 0);
assert_eq!(estimate(&text, &[("t", 5000)]), Some(5000));
assert_eq!(estimate(&text, &[]), None);
assert_eq!(estimate(&text, &[("t", 0)]), Some(0));
}
#[test]
fn not_knowing_travels_up_rather_than_being_rounded_away() {
let text = format!("Filter (#0.0::INTEGER > 1::INTEGER)::BOOLEAN\n {}", scan("t", 0));
assert_eq!(estimate(&text, &[]), None);
assert!(estimate(&text, &[("t", 1000)]).is_some());
}
#[test]
fn an_ungrouped_aggregate_is_one_row_whatever_is_under_it() {
let text = format!("Aggregate #1 groups=[] aggregates=[]\n {}", scan("t", 0));
assert_eq!(estimate(&text, &[]), Some(1));
assert_eq!(estimate(&text, &[("t", 9_000_000)]), Some(1));
}
#[test]
fn a_group_by_collapses_its_input_and_a_scan_under_it_still_decides_whether_it_can() {
let text = format!("Aggregate #1 groups=[#0.0::INTEGER] aggregates=[]\n {}", scan("t", 0));
assert_eq!(estimate(&text, &[("t", 1000)]), Some(100));
assert_eq!(estimate(&text, &[]), None);
}
#[test]
fn a_limit_is_a_ceiling_even_over_an_input_nobody_measured() {
let text = format!("Limit 10 offset 0\n {}", scan("t", 0));
assert_eq!(estimate(&text, &[]), Some(10));
assert_eq!(estimate(&text, &[("t", 3)]), Some(3));
assert_eq!(estimate(&text, &[("t", 3_000_000)]), Some(10));
}
#[test]
fn an_offset_with_no_limit_takes_rows_away_and_cannot_add_any() {
let text = format!("Limit ALL offset 5\n {}", scan("t", 0));
assert_eq!(estimate(&text, &[("t", 12)]), Some(7));
assert_eq!(estimate(&text, &[("t", 2)]), Some(0));
assert_eq!(estimate(&text, &[]), None);
}
#[test]
fn a_filter_never_estimates_a_relation_away_entirely() {
let and = "(#0.0::INTEGER > 1::INTEGER)::BOOLEAN AND (#0.0::INTEGER > 2::INTEGER)::BOOLEAN \
AND (#0.0::INTEGER > 3::INTEGER)::BOOLEAN AND \
(#0.0::INTEGER > 4::INTEGER)::BOOLEAN AND (#0.0::INTEGER > 5::INTEGER)::BOOLEAN \
AND (#0.0::INTEGER > 6::INTEGER)::BOOLEAN";
let text = format!("Filter ({and})::BOOLEAN\n {}", scan("t", 0));
assert_eq!(estimate(&text, &[("t", 10)]), Some(1));
let one = format!("Filter (#0.0::INTEGER > 1::INTEGER)::BOOLEAN\n {}", scan("t", 0));
assert_eq!(estimate(&one, &[("t", 1_000_000)]), Some(200_000));
}
#[test]
fn a_condition_that_reads_no_column_is_not_counted_as_a_condition() {
let both = format!(
"Filter ((#0.0::INTEGER > 1::INTEGER)::BOOLEAN AND (1::INTEGER > 2::INTEGER)::BOOLEAN)::BOOLEAN\n {}",
scan("t", 0)
);
assert_eq!(estimate(&both, &[("t", 1_000_000)]), Some(200_000));
let alone = format!("Filter (1::INTEGER > 2::INTEGER)::BOOLEAN\n {}", scan("t", 0));
assert_eq!(estimate(&alone, &[("t", 1_000_000)]), Some(1_000_000));
}
#[test]
fn an_inner_join_comes_out_the_size_of_its_larger_side() {
let text = format!(
"Join INNER on=[(#0.0::INTEGER = #1.0::INTEGER)::BOOLEAN]\n {} {}",
scan("small", 0),
scan("big", 1)
);
assert_eq!(estimate(&text, &[("small", 10_000), ("big", 50_000)]), Some(50_000));
assert_eq!(estimate(&text, &[("small", 10_000)]), None);
}
#[test]
fn a_join_with_no_condition_is_the_product_and_says_so() {
let text = format!("Join INNER on=[]\n {} {}", scan("small", 0), scan("big", 1));
assert_eq!(estimate(&text, &[("small", 1000), ("big", 1000)]), Some(1_000_000));
}
#[test]
fn an_outer_join_never_estimates_below_the_side_it_preserves() {
let text = format!(
"Join LEFT on=[(#0.0::INTEGER = #1.0::INTEGER)::BOOLEAN]\n {} {}",
scan("big", 0),
scan("small", 1)
);
assert_eq!(estimate(&text, &[("big", 50_000), ("small", 10)]), Some(50_000));
}
#[test]
fn a_semi_join_is_bounded_by_its_left_side_and_ignores_the_right() {
let text = format!(
"Join SEMI on=[(#0.0::INTEGER = #1.0::INTEGER)::BOOLEAN]\n {} {}",
scan("small", 0),
scan("big", 1)
);
let estimated =
estimate(&text, &[("small", 1000), ("big", 9_000_000)]).expect("both sides known");
assert!(estimated <= 1000, "a semi join produced {estimated} out of 1000 left rows");
}
#[test]
fn a_cross_product_of_two_enormous_sides_saturates_rather_than_wrapping() {
let text = format!("CrossProduct\n {} {}", scan("a", 0), scan("b", 1));
assert_eq!(estimate(&text, &[("a", u64::MAX), ("b", 2)]), Some(u64::MAX));
}
#[test]
fn a_union_all_is_both_sides_and_so_is_the_bound_on_the_rest_of_them() {
let text = format!("SetOp UNION ALL #2\n {} {}", scan("a", 0), scan("b", 1));
assert_eq!(estimate(&text, &[("a", 30), ("b", 12)]), Some(42));
}
#[test]
fn a_count_that_came_from_the_catalog_says_it_is_exact() {
assert_eq!(stat(&scan("t", 0), &[("t", 5000)]).class(), Some(Class::Exact));
assert_eq!(stat(&scan("t", 0), &[]).class(), None);
}
#[test]
fn every_number_here_says_where_it_came_from() {
assert_eq!(stat(&scan("t", 0), &[("t", 5000)]).provenance(), Some(Provenance::RowCount));
let text = format!("Filter (#0.0::INTEGER > 1::INTEGER)::BOOLEAN\n {}", scan("t", 0));
assert_eq!(stat(&text, &[("t", 1000)]).provenance(), Some(Provenance::Default));
}
#[test]
fn a_cardinality_is_for_deciding_and_answers_nothing() {
let text = format!("Filter (#0.0::INTEGER > 1::INTEGER)::BOOLEAN\n {}", scan("t", 0));
let guessed = stat(&text, &[("t", 1000)]);
assert_eq!(guessed.decide(), Some(&200));
assert_eq!(guessed.answer(), None);
assert_eq!(guessed.enable(), None);
let counted = stat(&scan("t", 0), &[("t", 5000)]);
assert_eq!(counted.answer(), Some(&5000));
assert_eq!(counted.enable(), Some(&5000));
let nothing = stat(&scan("t", 0), &[]);
assert_eq!(nothing.decide(), None);
assert_eq!(nothing.answer(), None);
assert_eq!(nothing.enable(), None);
}
#[test]
fn one_guess_anywhere_under_a_node_makes_the_node_a_guess() {
let text = format!("Filter (#0.0::INTEGER > 1::INTEGER)::BOOLEAN\n {}", scan("t", 0));
assert_eq!(stat(&text, &[("t", 1000)]).class(), Some(GUESSED));
let twice = format!(
"Aggregate #1 groups=[#0.0::INTEGER] aggregates=[]\n Filter (#0.0::INTEGER > \
1::INTEGER)::BOOLEAN\n {}",
scan("t", 0)
);
assert_eq!(stat(&twice, &[("t", 1000)]).class(), Some(GUESSED));
}
#[test]
fn an_ungrouped_aggregate_is_exact_because_one_row_is_a_fact() {
let text = format!("Aggregate #1 groups=[] aggregates=[]\n {}", scan("t", 0));
assert_eq!(stat(&text, &[]).class(), Some(Class::Exact));
}
#[test]
fn a_limit_over_an_unmeasured_input_is_certified_rather_than_estimated() {
let text = format!("Limit 10 offset 0\n {}", scan("t", 0));
assert_eq!(
stat(&text, &[]).class(),
Some(Class::Certified { bound: 1.0, direction: Direction::AtMost })
);
assert_eq!(stat(&text, &[("t", 3)]).class(), Some(Class::Exact));
}
#[test]
fn a_join_with_no_condition_is_a_product_and_the_product_is_exact() {
let product = format!("Join INNER on=[]\n {} {}", scan("a", 0), scan("b", 1));
assert_eq!(stat(&product, &[("a", 1000), ("b", 1000)]).class(), Some(Class::Exact));
let equi = format!(
"Join INNER on=[(#0.0::INTEGER = #1.0::INTEGER)::BOOLEAN]\n {} {}",
scan("a", 0),
scan("b", 1)
);
assert_eq!(stat(&equi, &[("a", 1000), ("b", 1000)]).class(), Some(GUESSED));
}
#[test]
fn a_table_function_nobody_measured_is_unknown_and_stays_unknown_over_it() {
let text = "TableFunction range args=[] #0 [a::BIGINT]\n";
assert_eq!(stat(text, &[]), Stat::Unknown);
}
#[test]
fn a_table_function_the_binder_measured_is_as_tall_as_the_binder_said() {
let text = "TableFunction read_parquet args=[] #0 [a::BIGINT]\n";
let mut plan = Plan::parse(text).expect("a table function");
plan.measure(0, Stat::exact(6_001_215, Provenance::RowCount));
assert_eq!(
rows_stat(&plan, plan.root(), &Statistics::new()),
Stat::exact(6_001_215, Provenance::RowCount)
);
}
#[test]
fn a_table_function_is_measured_against_its_index_and_not_against_its_name() {
let text = concat!(
"Join INNER on=[]\n",
" TableFunction read_parquet args=[] #0 [a::BIGINT]\n",
" TableFunction read_parquet args=[] #1 [b::BIGINT]\n"
);
let mut plan = Plan::parse(text).expect("two table functions");
plan.measure(0, Stat::exact(3, Provenance::RowCount));
plan.measure(1, Stat::exact(5, Provenance::RowCount));
assert_eq!(
rows_stat(&plan, plan.root(), &Statistics::new()),
Stat::exact(15, Provenance::RowCount)
);
}
#[test]
fn a_table_function_nobody_measured_is_unknown_even_beside_one_that_was() {
let text = concat!(
"Join INNER on=[]\n",
" TableFunction read_parquet args=[] #0 [a::BIGINT]\n",
" TableFunction read_csv args=[] #1 [b::BIGINT]\n"
);
let mut plan = Plan::parse(text).expect("two table functions");
plan.measure(0, Stat::exact(3, Provenance::RowCount));
assert_eq!(rows_stat(&plan, plan.root(), &Statistics::new()), Stat::Unknown);
}
#[test]
fn a_guess_over_a_measured_file_is_a_guess_with_a_number_under_it() {
let text = concat!(
"Filter (#0.0::BIGINT > 5::BIGINT)::BOOLEAN\n",
" TableFunction read_parquet args=[] #0 [a::BIGINT]\n"
);
let mut plan = Plan::parse(text).expect("a filter over a table function");
assert_eq!(rows_stat(&plan, plan.root(), &Statistics::new()), Stat::Unknown);
plan.measure(0, Stat::exact(1000, Provenance::RowCount));
let over = rows_stat(&plan, plan.root(), &Statistics::new());
assert_eq!(over.value().copied(), Some(200));
assert_eq!(over.class(), Some(Class::Estimated));
assert_eq!(over.provenance(), Some(Provenance::Default));
}
#[test]
fn a_lateral_function_is_unknown_however_well_the_file_beside_it_is_measured() {
let text = concat!(
"LateralFunction range args=[#0.0::INTEGER] #1 [a::BIGINT]\n",
" Get memory.main.t AS t #0 [a::INTEGER]\n"
);
let mut plan = Plan::parse(text).expect("a lateral function");
plan.measure(1, Stat::exact(4096, Provenance::RowCount));
assert_eq!(rows_stat(&plan, plan.root(), &Statistics::new()), Stat::Unknown);
}
#[test]
fn statistics_that_nobody_filled_in_say_so() {
let mut stats = Statistics::new();
assert!(stats.is_empty());
stats.record("memory", "main", "t", 7);
assert!(!stats.is_empty());
assert_eq!(stats.rows_in("memory", "main", "t"), Some(7));
assert_eq!(stats.rows_in("memory", "other", "t"), None);
}
#[test]
fn a_join_on_a_column_with_few_values_in_it_produces_more_rows_than_its_larger_side() {
let text = joined("customer", "supplier");
let tables = &[("customer", 150_000), ("supplier", 10_000)];
assert_eq!(counted(&text, tables, &[]), Some(150_000));
let counts = &[("customer", "a", 25), ("supplier", "a", 25)];
assert_eq!(counted(&text, tables, counts), Some(60_000_000));
}
#[test]
fn a_join_on_a_key_is_the_containment_assumption_and_a_count_does_not_change_it() {
let text = joined("customer", "orders");
let tables = &[("customer", 150_000), ("orders", 1_500_000)];
assert_eq!(counted(&text, tables, &[]), Some(1_500_000));
let counts = &[("customer", "a", 150_000), ("orders", "a", 150_000)];
assert_eq!(counted(&text, tables, counts), Some(1_500_000));
}
#[test]
fn a_count_larger_than_the_rows_on_the_smaller_side_does_not_shrink_the_estimate() {
let text = joined("small", "big");
let tables = &[("small", 10), ("big", 1_000)];
let counts = &[("small", "a", 1_000_000), ("big", "a", 1_000_000)];
assert_eq!(counted(&text, tables, counts), Some(1_000));
}
#[test]
fn two_conditions_match_on_the_pairs_of_values_and_not_on_either_column() {
let text = concat!(
"Join INNER on=[(#0.0::INTEGER = #1.0::INTEGER)::BOOLEAN, ",
"(#0.1::INTEGER = #1.1::INTEGER)::BOOLEAN]\n",
" Get memory.main.l AS l #0 [a::INTEGER, b::INTEGER]\n",
" Get memory.main.r AS r #1 [a::INTEGER, b::INTEGER]\n"
);
let tables = &[("l", 1_000_000), ("r", 1_000_000)];
let counts = &[("l", "a", 10), ("l", "b", 10), ("r", "a", 10), ("r", "b", 10)];
assert_eq!(counted(text, tables, counts), Some(10_000_000_000));
}
#[test]
fn a_condition_that_is_not_an_equality_between_two_columns_leaves_the_counts_unread() {
let text = concat!(
"Join INNER on=[(#0.0::INTEGER < #1.0::INTEGER)::BOOLEAN]\n",
" Get memory.main.l AS l #0 [a::INTEGER]\n",
" Get memory.main.r AS r #1 [a::INTEGER]\n"
);
let tables = &[("l", 150_000), ("r", 10_000)];
let counts = &[("l", "a", 25), ("r", "a", 25)];
assert_eq!(counted(text, tables, counts), Some(150_000));
}
#[test]
fn a_projection_that_carries_a_column_through_carries_its_count_through_as_well() {
let text = concat!(
"Join INNER on=[(#1.0::INTEGER = #3.0::INTEGER)::BOOLEAN]\n",
" Project #1 [#0.0::INTEGER AS a]\n",
" Get memory.main.customer AS customer #0 [a::INTEGER]\n",
" Project #3 [#2.0::INTEGER AS a]\n",
" Get memory.main.supplier AS supplier #2 [a::INTEGER]\n"
);
let tables = &[("customer", 150_000), ("supplier", 10_000)];
let counts = &[("customer", "a", 25), ("supplier", "a", 25)];
assert_eq!(counted(text, tables, counts), Some(60_000_000));
}
#[test]
fn a_projection_that_computes_something_is_where_the_count_stops() {
let text = concat!(
"Join INNER on=[(#1.0::INTEGER = #3.0::INTEGER)::BOOLEAN]\n",
" Project #1 [\"+\"(#0.0::INTEGER, 1::INTEGER)::INTEGER AS a]\n",
" Get memory.main.customer AS customer #0 [a::INTEGER]\n",
" Project #3 [#2.0::INTEGER AS a]\n",
" Get memory.main.supplier AS supplier #2 [a::INTEGER]\n"
);
let tables = &[("customer", 150_000), ("supplier", 10_000)];
let counts = &[("customer", "a", 25), ("supplier", "a", 25)];
assert_eq!(counted(text, tables, counts), Some(150_000));
}
#[test]
fn a_column_with_no_distinct_values_at_all_is_not_divided_by() {
let text = joined("l", "r");
let tables = &[("l", 1_000), ("r", 1_000)];
let counts = &[("l", "a", 0), ("r", "a", 0)];
assert_eq!(counted(&text, tables, counts), Some(1_000));
}
#[test]
fn an_equality_against_a_constant_keeps_one_value_out_of_the_count() {
let text = filtered("(#0.0::INTEGER = 3::INTEGER)::BOOLEAN");
let tables = &[("t", 300_000)];
assert_eq!(counted(&text, tables, &[]), Some(60_000));
assert_eq!(counted(&text, tables, &[("t", "a", 200)]), Some(1_500));
}
#[test]
fn a_count_that_says_more_rows_than_the_constant_did_is_still_the_count() {
let text = filtered("(#0.0::INTEGER = 3::INTEGER)::BOOLEAN");
assert_eq!(counted(&text, &[("t", 900_000)], &[("t", "a", 3)]), Some(300_000));
}
#[test]
fn two_counted_equalities_divide_by_both_counts() {
let text = filtered(
"((#0.0::INTEGER = 3::INTEGER)::BOOLEAN AND (#0.1::INTEGER = 4::INTEGER)::BOOLEAN)::BOOLEAN",
);
let tables = &[("t", 200_000)];
let counts = &[("t", "a", 25), ("t", "b", 40)];
assert_eq!(counted(&text, tables, counts), Some(200));
}
#[test]
fn a_column_nobody_counted_keeps_the_constant_rather_than_becoming_one_row() {
let text = filtered(
"((#0.0::INTEGER = 3::INTEGER)::BOOLEAN AND (#0.1::INTEGER = 4::INTEGER)::BOOLEAN)::BOOLEAN",
);
let tables = &[("t", 1_000_000)];
assert_eq!(counted(&text, tables, &[]), Some(40_000));
assert_eq!(counted(&text, tables, &[("t", "a", 50)]), Some(4_000));
}
#[test]
fn only_an_equality_against_a_constant_reads_the_count() {
let tables = &[("t", 1_000_000)];
let counts = &[("t", "a", 50), ("t", "b", 50)];
let above = filtered("(#0.0::INTEGER > 3::INTEGER)::BOOLEAN");
assert_eq!(counted(&above, tables, counts), Some(200_000));
let other = filtered("(#0.0::INTEGER <> 3::INTEGER)::BOOLEAN");
assert_eq!(counted(&other, tables, counts), Some(200_000));
let columns = filtered("(#0.0::INTEGER = #0.1::INTEGER)::BOOLEAN");
assert_eq!(counted(&columns, tables, counts), Some(200_000));
}
#[test]
fn an_equality_reads_the_count_whichever_side_the_constant_is_on() {
let text = filtered("(3::INTEGER = #0.0::INTEGER)::BOOLEAN");
assert_eq!(counted(&text, &[("t", 100_000)], &[("t", "a", 50)]), Some(2_000));
}
#[test]
fn a_projection_carries_a_count_up_to_a_filter_as_well() {
let text = concat!(
"Filter (#1.0::INTEGER = 3::INTEGER)::BOOLEAN\n",
" Project #1 [#0.0::INTEGER AS a]\n",
" Get memory.main.t AS t #0 [a::INTEGER]\n"
);
assert_eq!(counted(text, &[("t", 100_000)], &[("t", "a", 50)]), Some(2_000));
}
#[test]
fn a_count_bigger_than_the_table_still_leaves_a_row() {
let text = filtered("(#0.0::INTEGER = 3::INTEGER)::BOOLEAN");
assert_eq!(counted(&text, &[("t", 10)], &[("t", "a", 1_000_000)]), Some(1));
}
#[test]
fn where_a_filter_got_its_fraction_from_is_printed() {
let tables = &[("t", 1_000_000)];
let one = filtered("(#0.0::INTEGER = 3::INTEGER)::BOOLEAN");
assert_eq!(
counted_stat(&one, tables, &[("t", "a", 50)]).provenance(),
Some(Provenance::Sketch)
);
assert_eq!(counted_stat(&one, tables, &[]).provenance(), Some(Provenance::Default));
let both = filtered(
"((#0.0::INTEGER = 3::INTEGER)::BOOLEAN AND (#0.1::INTEGER > 4::INTEGER)::BOOLEAN)::BOOLEAN",
);
assert_eq!(
counted_stat(&both, tables, &[("t", "a", 50)]).provenance(),
Some(Provenance::Propagation)
);
}
#[test]
fn a_counted_filter_is_still_a_guess() {
let text = filtered("(#0.0::INTEGER = 3::INTEGER)::BOOLEAN");
let stat = counted_stat(&text, &[("t", 1_000_000)], &[("t", "a", 50)]);
assert_eq!(stat.decide(), Some(&20_000));
assert_eq!(stat.enable(), None);
assert_eq!(stat.answer(), None);
}
#[derive(Debug)]
struct Stub {
surviving: Option<u64>,
asked: Mutex<Vec<Test>>,
}
impl Stub {
fn new(surviving: Option<u64>) -> Arc<Self> {
Arc::new(Self { surviving, asked: Mutex::new(Vec::new()) })
}
}
impl Zones for Stub {
fn column(&self, name: &str) -> Option<usize> {
match name {
"b" => Some(0),
"a" => Some(1),
_ => None,
}
}
fn surviving(&self, tests: &[Test]) -> Option<u64> {
self.asked.lock().expect("no test panics while holding this").extend_from_slice(tests);
self.surviving
}
}
fn bounded_scan() -> String {
"Get memory.main.t AS t #0 [a::INTEGER, b::INTEGER]\n".to_string()
}
fn zoned(text: &str, rows: u64, zones: &Arc<Stub>) -> Stat<u64> {
let stats = statistics(&[("t", rows)]);
let mut plan =
Plan::parse(text).unwrap_or_else(|error| panic!("{text} did not parse: {error}"));
plan.set_zones(0, Arc::clone(zones) as Arc<dyn Zones>);
rows_stat(&plan, plan.root(), &stats)
}
#[test]
fn a_filter_the_bounds_rule_out_entirely_is_exactly_no_rows() {
let text = format!("Filter (#0.0::INTEGER > 9::INTEGER)::BOOLEAN\n {}", bounded_scan());
let zones = Stub::new(Some(0));
assert_eq!(zoned(&text, 10_000_000, &zones), Stat::exact(0, Provenance::ZoneMap));
}
#[test]
fn a_ceiling_below_the_guess_replaces_it_and_says_it_is_a_ceiling() {
let text = format!("Filter (#0.0::INTEGER < 9::INTEGER)::BOOLEAN\n {}", bounded_scan());
let zones = Stub::new(Some(100_000));
let capped = zoned(&text, 10_000_000, &zones);
assert_eq!(capped.value(), Some(&100_000));
assert_eq!(
capped.class(),
Some(Class::Certified { bound: 1.0, direction: Direction::AtMost })
);
assert_eq!(capped.provenance(), Some(Provenance::ZoneMap));
let tiny = Stub::new(Some(1));
assert_eq!(zoned(&text, 10_000_000, &tiny).value(), Some(&1));
}
#[test]
fn a_ceiling_above_the_guess_leaves_the_guess_alone() {
let text = format!("Filter (#0.0::INTEGER < 9::INTEGER)::BOOLEAN\n {}", bounded_scan());
let wide = Stub::new(Some(10_000_000));
assert_eq!(
zoned(&text, 10_000_000, &wide),
Stat::estimated(2_000_000, Provenance::Default)
);
}
#[test]
fn a_store_that_cannot_answer_leaves_the_estimate_exactly_as_it_was() {
let text = format!("Filter (#0.0::INTEGER < 9::INTEGER)::BOOLEAN\n {}", bounded_scan());
let quiet = Stub::new(None);
assert_eq!(zoned(&text, 1_000_000, &quiet), stat(&text, &[("t", 1_000_000)]));
assert_eq!(zoned(&text, 1_000_000, &quiet), Stat::estimated(200_000, Provenance::Default));
}
#[test]
fn a_filter_that_reads_as_no_test_at_all_does_not_ask_the_store() {
let text = format!("Filter (#0.0::INTEGER = #0.1::INTEGER)::BOOLEAN\n {}", bounded_scan());
let zones = Stub::new(Some(7));
assert_eq!(zoned(&text, 1_000_000, &zones), Stat::estimated(200_000, Provenance::Default));
assert!(zones.asked.lock().expect("not poisoned").is_empty(), "it was never asked");
}
#[test]
fn the_column_the_store_is_asked_about_is_the_one_the_plan_named_and_not_the_position() {
let text = format!("Filter (#0.0::INTEGER < 9::INTEGER)::BOOLEAN\n {}", bounded_scan());
let zones = Stub::new(Some(100));
zoned(&text, 1_000_000, &zones);
let asked = zones.asked.lock().expect("not poisoned");
assert_eq!(asked.len(), 1);
assert_eq!(asked[0].column, 1, "`a` is the store's column one");
assert_eq!(asked[0].op, Op::Less);
}
#[test]
fn a_table_with_no_store_recorded_is_estimated_the_way_it_always_was() {
let text = format!("Filter (#0.0::INTEGER < 9::INTEGER)::BOOLEAN\n {}", bounded_scan());
assert_eq!(stat(&text, &[("t", 1_000_000)]), Stat::estimated(200_000, Provenance::Default));
}
}