use std::collections::BTreeMap;
use std::sync::Arc;
use rudb_common::bounds::{Bound, Frequencies, Spread, Test, Zones};
use rudb_common::stat::{Class, Direction, Provenance, Stat, Use};
use rudb_common::{Field, Value};
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, PartialEq, Eq, PartialOrd, Ord)]
pub enum Key<'a> {
Rows {
catalog: &'a str,
schema: &'a str,
table: &'a str,
},
Distinct {
catalog: &'a str,
schema: &'a str,
table: &'a str,
column: &'a str,
},
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Facts {
tables: BTreeMap<(String, String, String), u64>,
columns: BTreeMap<(String, String, String, String), (u64, Provenance)>,
generation: u64,
}
impl Facts {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn at(generation: u64) -> Self {
Self { generation, ..Self::default() }
}
#[must_use]
pub const fn generation(&self) -> u64 {
self.generation
}
#[must_use]
pub fn get(&self, key: &Key<'_>) -> Stat<u64> {
let (found, provenance) = match *key {
Key::Rows { catalog, schema, table } => {
(self.rows_in(catalog, schema, table), Provenance::RowCount)
}
Key::Distinct { catalog, schema, table, column } => {
match self.distinct_in(catalog, schema, table, column) {
Some((value, provenance)) => (Some(value), provenance),
None => (None, Provenance::Dictionary),
}
}
};
found.map_or(Stat::Unknown, |value| Stat::exact(value, provenance))
}
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);
}
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,
provenance: Provenance,
) {
let key = (catalog.to_owned(), schema.to_owned(), table.to_owned(), column.to_owned());
self.columns.insert(key, (distinct, provenance));
}
#[must_use]
pub fn without_distincts(&self) -> Self {
Self { tables: self.tables.clone(), columns: BTreeMap::new(), generation: self.generation }
}
fn distinct_in(
&self,
catalog: &str,
schema: &str,
table: &str,
column: &str,
) -> Option<(u64, Provenance)> {
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;
pub(crate) const FROM_A_CONSTANT: Provenance = Provenance::Default;
const CEILING: Class = Class::Certified { bound: 1.0, direction: Direction::AtMost };
pub const CARDINALITY: Use = Use::Decide;
pub const DISTINCT: Use = Use::Decide;
const COMMON: Use = Use::Decide;
const NULLS: Use = Use::Decide;
#[must_use]
pub fn rows(plan: &Plan, node: NodeRef, stats: &Facts) -> Option<u64> {
rows_stat(plan, node, stats).read(CARDINALITY).copied()
}
#[must_use]
pub fn unfiltered(plan: &Plan, node: NodeRef, stats: &Facts) -> Stat<u64> {
match *plan.node(node) {
Node::Filter { input, .. } => unfiltered(plan, input, stats),
Node::Project { input, .. }
| Node::Window { input, .. }
| Node::Sort { input, .. }
| Node::Fetch { input, .. }
| Node::TableFetch { input, .. } => unfiltered(plan, input, stats),
Node::Join { left, kind: JoinKind::Semi | JoinKind::Anti, .. } => {
unfiltered(plan, left, stats)
}
Node::Join { left, right, kind: kind @ JoinKind::Inner, conditions, .. } => join(
both(unfiltered(plan, left, stats)),
both(unfiltered(plan, right, stats)),
kind,
plan.expr_list(conditions).len(),
keyspace(plan, conditions, stats, &mut Vec::new()),
),
Node::CrossProduct { left, right } => {
unfiltered(plan, left, stats).zip(unfiltered(plan, right, stats), u64::saturating_mul)
}
_ => rows_stat(plan, node, stats),
}
}
#[must_use]
pub fn side(plan: &Plan, node: NodeRef, stats: &Facts) -> Option<Side> {
let rows = *rows_stat(plan, node, stats).read(CARDINALITY)?;
let base = *unfiltered(plan, node, stats).read(CARDINALITY)?;
Some(Side { rows, base: base.max(rows) })
}
#[must_use]
pub fn rows_stat(plan: &Plan, node: NodeRef, stats: &Facts) -> Stat<u64> {
rows_stat_into(plan, node, stats, &mut Vec::new())
}
#[must_use]
pub fn rows_stat_into(
plan: &Plan,
node: NodeRef,
stats: &Facts,
reads: &mut Vec<Stat<u64>>,
) -> 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, .. } => stats.get(&Key::Rows {
catalog: plan.string(catalog),
schema: plan.string(schema),
table: plan.string(table),
}),
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, input, predicate, stats, reads);
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);
}
collapsed(plan, input, of(input), keyed(plan, plan.expr_list(groups)), stats, reads)
}
Node::Distinct { input, on } => {
let keys = plan.expr_list(on);
let keys = if keys.is_empty() { produced(plan, input) } else { keyed(plan, keys) };
collapsed(plan, input, of(input), keys, stats, reads)
}
Node::Limit { input, count, offset } => {
let input = of(input);
let offset = offset.rows().unwrap_or(0);
match count.rows() {
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::LimitPercent { input, percent, offset } => of(input).map(|n| {
let share = percent.percent().unwrap_or(100.0) / 100.0 * n as f64;
(share as u64).saturating_sub(offset.rows().unwrap_or(0))
}),
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(
Both { rows: of(left), base: unfiltered(plan, left, stats) },
Both { rows: of(right), base: unfiltered(plan, right, stats) },
kind,
plan.expr_list(conditions).len(),
keyspace(plan, conditions, stats, reads),
),
Node::LinkJoin { child, kind: JoinKind::Left, .. } => of(child),
Node::LinkJoin { child, .. } => ceiling(of(child)),
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,
input: NodeRef,
predicate: ExprRef,
stats: &Facts,
reads: &mut Vec<Stat<u64>>,
) -> (f64, Provenance) {
let mut fraction = 1.0;
let mut counted = 0;
let mut source: Option<Provenance> = None;
let mut pending = Vec::new();
for conjunct in conjuncts(plan, predicate) {
match counted_by(plan, input, conjunct, stats, reads) {
Some((share, from)) => {
fraction *= share;
counted += 1;
source = match source {
None => Some(from),
Some(one) if one == from => Some(one),
Some(_) => Some(Provenance::Propagation),
};
}
None => pending.push(conjunct),
}
}
let interpolated = spread(plan, input, &pending).map_or(0, |spread| {
fraction *= spread.fraction;
spread.read
});
let guessed = pending.len().saturating_sub(interpolated);
for _ in 0..guessed {
fraction *= KEPT_BY_A_CONDITION;
}
let from = match (counted, interpolated, guessed) {
(0, 0, _) => FROM_A_CONSTANT,
(_, 0, 0) => source.unwrap_or(FROM_A_CONSTANT),
(0, _, 0) => Provenance::ZoneMap,
_ => Provenance::Propagation,
};
(fraction, from)
}
fn spread(plan: &Plan, input: NodeRef, conjuncts: &[ExprRef]) -> Option<Spread> {
let (zones, tests) = asked(plan, input, conjuncts)?;
zones.spread(&tests)
}
fn counted_by(
plan: &Plan,
input: NodeRef,
conjunct: ExprRef,
stats: &Facts,
reads: &mut Vec<Stat<u64>>,
) -> Option<(f64, Provenance)> {
missing(plan, input, conjunct, reads)
.or_else(|| common(plan, input, conjunct, stats, reads))
.or_else(|| values(plan, conjunct, stats, reads).map(|(v, from)| (1.0 / widened(v), from)))
}
pub(crate) fn kept_by(
plan: &Plan,
input: NodeRef,
conjunct: ExprRef,
stats: &Facts,
) -> (f64, Provenance) {
let mut reads = Vec::new();
if let Some(answer) = counted_by(plan, input, conjunct, stats, &mut reads) {
return answer;
}
match spread(plan, input, std::slice::from_ref(&conjunct)) {
Some(spread) if spread.read > 0 => (spread.fraction, Provenance::ZoneMap),
_ => (KEPT_BY_A_CONDITION, FROM_A_CONSTANT),
}
}
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()
}
pub(crate) fn keyed(plan: &Plan, keys: &[ExprRef]) -> Option<Vec<ColumnBinding>> {
keys.iter()
.map(|&key| match *plan.expr(key) {
Expr::Column(binding) => Some(binding),
_ => None,
})
.collect()
}
fn produced(plan: &Plan, input: NodeRef) -> Option<Vec<ColumnBinding>> {
let Node::Project { index, exprs, .. } = *plan.node(input) else {
return None;
};
let width = u32::try_from(plan.expr_list(exprs).len()).ok()?;
Some((0..width).map(|column| ColumnBinding { table: index, column }).collect())
}
fn collapsed(
plan: &Plan,
node: NodeRef,
input: Stat<u64>,
keys: Option<Vec<ColumnBinding>>,
stats: &Facts,
reads: &mut Vec<Stat<u64>>,
) -> Stat<u64> {
let (Some(keys), Stat::Known { value: rows, .. }) = (keys, input) else {
return guess(input, KEPT_BY_A_GROUP_BY);
};
let Some(total) = scanned_rows(plan, node, stats).filter(|&total| total > 0) else {
return guess(input, KEPT_BY_A_GROUP_BY);
};
if keys.is_empty() || rows == 0 {
return guess(input, KEPT_BY_A_GROUP_BY);
}
let mut values: u64 = 1;
let mut source: Option<Provenance> = None;
for binding in keys {
let stat = stated(plan, binding, stats);
reads.push(stat);
let Some(counted) = stat.read(DISTINCT).copied().filter(|&counted| counted > 0) else {
return guess(input, KEPT_BY_A_GROUP_BY);
};
values = values.saturating_mul(counted);
let from = stat.provenance().unwrap_or(FROM_A_CONSTANT);
source = match source {
None => Some(from),
Some(one) if one == from => Some(one),
Some(_) => Some(Provenance::Propagation),
};
}
let groups = landed_on(values, rows, total);
guess_from(input, groups as f64 / rows as f64, source.unwrap_or(FROM_A_CONSTANT))
}
fn scanned_rows(plan: &Plan, node: NodeRef, stats: &Facts) -> Option<u64> {
let mut at = node;
for _ in 0..16 {
if matches!(*plan.node(at), Node::Get { .. }) {
return rows_stat(plan, at, stats).value().copied();
}
match plan.node(at).children() {
[Some(input), None] => at = input,
_ => return None,
}
}
None
}
#[expect(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "a count of groups is a weight here and not an identity"
)]
fn landed_on(values: u64, rows: u64, total: u64) -> u64 {
let ceiling = values.min(rows).max(1);
if rows >= total {
return ceiling;
}
let kept = rows as f64 / total as f64;
let each = total as f64 / values as f64;
let survives = 1.0 - (1.0 - kept).powf(each);
((values as f64 * survives).round() as u64).clamp(1, ceiling)
}
fn scanned_fields(plan: &Plan, input: NodeRef) -> Option<&[Field]> {
match *plan.node(input) {
Node::Get { columns, .. } | Node::TableFunction { columns, .. } => {
Some(plan.field_list(columns))
}
_ => None,
}
}
fn nulls_at(plan: &Plan, input: NodeRef, position: usize) -> Option<Stat<u64>> {
let index = bounds::scanned(plan, input)?;
let zones = plan.zones(index)?;
let name = &scanned_fields(plan, input)?.get(position)?.name;
Some(zones.nulls(zones.column(name)?))
}
pub(crate) const NO_NULLS: Use = Use::Enable;
pub(crate) fn never_null(plan: &Plan, input: NodeRef, binding: ColumnBinding) -> bool {
let Some(at) = walk::scan_of(plan, input, binding.table) else {
return false;
};
let Some(stat) = nulls_at(plan, at, binding.column as usize) else { return false };
stat.read(NO_NULLS) == Some(&0)
}
fn compared(plan: &Plan, index: u32, conjunct: ExprRef) -> Option<(CompareOp, usize, Bound)> {
let Expr::Compare { op, left, right } = *plan.expr(conjunct) else {
return None;
};
if !matches!(op, CompareOp::Equal | CompareOp::NotEqual) {
return None;
}
let (binding, value) = match (plan.expr(left), plan.expr(right)) {
(&Expr::Column(binding), &Expr::Constant(value))
| (&Expr::Constant(value), &Expr::Column(binding)) => (binding, value),
_ => return None,
};
if binding.table != index {
return None;
}
Some((op, binding.column as usize, Bound::of_value(plan.value(value))?))
}
fn missing(
plan: &Plan,
input: NodeRef,
conjunct: ExprRef,
reads: &mut Vec<Stat<u64>>,
) -> Option<(f64, Provenance)> {
let Expr::Compare { op, left, right } = *plan.expr(conjunct) else {
return None;
};
let wants_null = match op {
CompareOp::NotDistinctFrom => true,
CompareOp::DistinctFrom => false,
_ => return None,
};
let binding = match (plan.expr(left), plan.expr(right)) {
(&Expr::Column(binding), &Expr::Constant(value))
| (&Expr::Constant(value), &Expr::Column(binding))
if matches!(plan.value(value), Value::Null) =>
{
binding
}
_ => return None,
};
let index = bounds::scanned(plan, input)?;
if binding.table != index {
return None;
}
let stat = nulls_at(plan, input, binding.column as usize)?;
reads.push(stat);
let nulls = stat.read(NULLS).copied()?;
let rows = plan.zones(index)?.surviving(&[])?;
if rows == 0 {
return None;
}
let held = if wants_null { nulls.min(rows) } else { rows.saturating_sub(nulls) };
Some((share(held, rows), Provenance::NullCount))
}
fn common(
plan: &Plan,
input: NodeRef,
conjunct: ExprRef,
stats: &Facts,
reads: &mut Vec<Stat<u64>>,
) -> Option<(f64, Provenance)> {
let index = bounds::scanned(plan, input)?;
let frequencies = plan.frequencies(index)?;
let fields = scanned_fields(plan, input)?;
let rows = frequencies.rows();
if rows == 0 {
return None;
}
let mut blended = false;
let mut counted = |position: usize, value: &Bound| {
let column = frequencies.column(&fields.get(position)?.name)?;
let stat = frequencies.rows_with(column, value);
reads.push(stat);
if let Some(count) = stat.read(COMMON).copied() {
return Some(count);
}
let binding = ColumnBinding { table: index, column: u32::try_from(position).ok()? };
let held = unlisted(plan, frequencies, column, binding, stats, reads)?;
blended = true;
Some(held)
};
let held = match *plan.expr(conjunct) {
Expr::Conjunction { op: ConjunctionOp::Or, children } => {
let branches = plan.expr_list(children);
if branches.is_empty() || branches.len() > 32 {
return None;
}
let mut column = None;
let mut total: u64 = 0;
for &branch in branches {
let (CompareOp::Equal, position, value) = compared(plan, index, branch)? else {
return None;
};
if *column.get_or_insert(position) != position {
return None;
}
total = total.checked_add(counted(position, &value)?)?;
}
total.min(rows)
}
_ => {
let (op, position, value) = compared(plan, index, conjunct)?;
let counted = counted(position, &value)?;
match op {
CompareOp::Equal => counted,
_ => {
let nulls = nulls_at(plan, input, position)?.read(NULLS).copied()?;
rows.saturating_sub(nulls).saturating_sub(counted)
}
}
}
};
let from = if blended { Provenance::Propagation } else { Provenance::FrequencySynopsis };
Some((share(held, rows), from))
}
fn unlisted(
plan: &Plan,
frequencies: &Arc<dyn Frequencies>,
column: usize,
binding: ColumnBinding,
stats: &Facts,
reads: &mut Vec<Stat<u64>>,
) -> Option<u64> {
let remainder = frequencies.remainder(column)?;
let stat = stated(plan, binding, stats);
reads.push(stat);
let counted = match stat {
Stat::Known { value, class: Class::Exact, .. } => Some(value),
_ => None,
};
let divided = counted
.and_then(|values| values.checked_sub(remainder.listed))
.filter(|&rest| rest > 0)
.map(|rest| remainder.rows / rest);
Some(divided.unwrap_or(remainder.most).clamp(1, remainder.most.max(1)))
}
fn values(
plan: &Plan,
conjunct: ExprRef,
stats: &Facts,
reads: &mut Vec<Stat<u64>>,
) -> Option<(u64, Provenance)> {
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,
};
let stat = stated(plan, binding, stats);
reads.push(stat);
let values = stat.read(DISTINCT).copied().filter(|&values| values > 0)?;
Some((values, stat.provenance().unwrap_or(FROM_A_CONSTANT)))
}
fn surviving(plan: &Plan, input: NodeRef, predicate: ExprRef) -> Option<u64> {
let (zones, tests) = asked(plan, input, &[predicate])?;
zones.surviving(&tests)
}
fn asked<'a>(
plan: &'a Plan,
input: NodeRef,
predicates: &[ExprRef],
) -> Option<(&'a Arc<dyn Zones>, Vec<Test>)> {
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 mut tests = Vec::new();
for predicate in predicates {
for (position, op, value) in bounds::of(plan, input, *predicate) {
let name = &names.get(position)?.name;
tests.push(Test { column: zones.column(name)?, op, value });
}
}
(!tests.is_empty()).then_some((zones, tests))
}
fn distinct(plan: &Plan, binding: ColumnBinding, stats: &Facts) -> Stat<u64> {
follow(plan, binding, stats, Missing::Rows, 16)
}
pub(crate) fn stated(plan: &Plan, binding: ColumnBinding, stats: &Facts) -> Stat<u64> {
follow(plan, binding, stats, Missing::Nothing, 16)
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Missing {
Rows,
Nothing,
}
fn follow(
plan: &Plan,
binding: ColumnBinding,
stats: &Facts,
missing: Missing,
depth: u32,
) -> Stat<u64> {
let Some(depth) = depth.checked_sub(1) else {
return Stat::Unknown;
};
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 Some(field) = plan.field_list(columns).get(position) else {
return Stat::Unknown;
};
let catalog = plan.string(catalog);
let schema = plan.string(schema);
let table = plan.string(table);
let distinct =
stats.get(&Key::Distinct { catalog, schema, table, column: &field.name });
if matches!(distinct, Stat::Known { .. }) {
return distinct;
}
let measured = plan.distinct_measured(index, &field.name);
if matches!(measured, Stat::Known { .. }) {
return measured;
}
if !rows {
return Stat::Unknown;
}
return ceiling(stats.get(&Key::Rows { catalog, schema, table }));
}
Node::TableFunction { index, columns, .. } if index == binding.table => {
let Some(field) = plan.field_list(columns).get(position) else {
return Stat::Unknown;
};
let distinct = plan.distinct_measured(index, &field.name);
if matches!(distinct, Stat::Known { .. }) {
return distinct;
}
return if rows { ceiling(plan.measured(index)) } else { Stat::Unknown };
}
Node::Project { index, exprs, .. } if index == binding.table => {
let Some(&carried) = plan.expr_list(exprs).get(position) else {
return Stat::Unknown;
};
let &Expr::Column(carried) = plan.expr(carried) else {
return Stat::Unknown;
};
return follow(plan, carried, stats, missing, depth);
}
_ => {}
}
}
Stat::Unknown
}
fn keyspace(
plan: &Plan,
conditions: Slice,
stats: &Facts,
reads: &mut Vec<Stat<u64>>,
) -> Option<u64> {
keyspace_into(plan, plan.expr_list(conditions), stats, reads)
}
#[must_use]
pub fn keyspace_of(plan: &Plan, conditions: &[ExprRef], stats: &Facts) -> Option<u64> {
keyspace_into(plan, conditions, stats, &mut Vec::new())
}
fn keyspace_into(
plan: &Plan,
conditions: &[ExprRef],
stats: &Facts,
reads: &mut Vec<Stat<u64>>,
) -> 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 (left, right) = (distinct(plan, left, stats), distinct(plan, right, stats));
reads.push(left);
reads.push(right);
let pair = (*left.read(DISTINCT)?).max(*right.read(DISTINCT)?);
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)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Side {
pub rows: u64,
pub base: u64,
}
impl Side {
#[must_use]
pub const fn whole(rows: u64) -> Self {
Self { rows, base: rows }
}
fn share(self) -> f64 {
(widened(self.rows) / widened(self.base)).min(1.0)
}
}
#[must_use]
pub fn matched_sides(left: Side, right: Side, keys: Option<u64>) -> Side {
let base = matched(left.base, right.base, keys);
let rows = scale(base, left.share() * right.share()).max(1).min(base);
Side { rows, base }
}
#[derive(Clone, Copy)]
struct Both {
rows: Stat<u64>,
base: Stat<u64>,
}
const fn both(stat: Stat<u64>) -> Both {
Both { rows: stat, base: stat }
}
fn join(
left: Both,
right: Both,
kind: JoinKind,
conditions: usize,
keys: Option<u64>,
) -> Stat<u64> {
let (left, right, bases) = (left.rows, right.rows, (left.base, right.base));
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 sides = (
Side { rows: left, base: bases.0.value().copied().unwrap_or(left).max(left) },
Side { rows: right, base: bases.1.value().copied().unwrap_or(right).max(right) },
);
let matched = matched_sides(sides.0, sides.1, keys).rows;
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 row count is a weight here and not an identity")]
fn share(counted: u64, rows: u64) -> f64 {
(counted as f64 / rows as f64).clamp(0.0, 1.0)
}
#[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::{Bound, End, Frequencies, Op, Remainder, Spread, Test, Zones};
use rudb_common::stat::{Class, Direction, Provenance, Stat};
use rudb_plan::Plan;
use super::{Facts, Key, Side, matched_sides, rows, rows_stat, unfiltered};
fn scan(table: &str, index: u32) -> String {
format!("Get memory.main.{table} AS {table} #{index} [a::INTEGER]\n")
}
fn facts(tables: &[(&str, u64)]) -> Facts {
let mut stats = Facts::new();
for (table, count) in tables {
stats.record("memory", "main", table, *count);
}
stats
}
fn estimate(text: &str, tables: &[(&str, u64)]) -> Option<u64> {
let stats = facts(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 = facts(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 = facts(tables);
for (table, column, distinct) in columns {
stats.record_distinct(
"memory",
"main",
table,
column,
*distinct,
Provenance::Dictionary,
);
}
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 = facts(tables);
for (table, column, distinct) in columns {
stats.record_distinct(
"memory",
"main",
table,
column,
*distinct,
Provenance::Dictionary,
);
}
let plan =
Plan::parse(text).unwrap_or_else(|error| panic!("{text} did not parse: {error}"));
rows_stat(&plan, plan.root(), &stats)
}
fn whole(text: &str, tables: &[(&str, u64)]) -> Option<u64> {
let stats = facts(tables);
let plan =
Plan::parse(text).unwrap_or_else(|error| panic!("{text} did not parse: {error}"));
unfiltered(&plan, plan.root(), &stats).value().copied()
}
fn sourced_stat(
text: &str,
tables: &[(&str, u64)],
columns: &[(&str, &str, u64, Provenance)],
) -> Stat<u64> {
let mut stats = facts(tables);
for (table, column, distinct, provenance) in columns {
stats.record_distinct("memory", "main", table, column, *distinct, *provenance);
}
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);
}
fn wide_scan(table: &str, index: u32) -> String {
format!("Get memory.main.{table} AS {table} #{index} [a::INTEGER, b::INTEGER]\n")
}
#[test]
fn a_group_by_on_a_counted_column_produces_as_many_groups_as_the_column_has_values() {
let text = format!("Aggregate #1 groups=[#0.0::INTEGER] aggregates=[]\n {}", scan("t", 0));
assert_eq!(
counted_stat(&text, &[("t", 1000)], &[("t", "a", 25)]),
Stat::estimated(25, Provenance::Dictionary)
);
assert_eq!(counted(&text, &[("t", 1000)], &[("t", "a", 1000)]), Some(1000));
}
#[test]
fn two_group_keys_multiply_and_the_rows_going_in_cap_the_product() {
let text = format!(
"Aggregate #1 groups=[#0.0::INTEGER, #0.1::INTEGER] aggregates=[]\n {}",
wide_scan("t", 0)
);
let keys = [("t", "a", 25), ("t", "b", 5)];
assert_eq!(counted(&text, &[("t", 100_000)], &keys), Some(125));
assert_eq!(counted(&text, &[("t", 50)], &keys), Some(50));
}
#[test]
fn a_group_by_under_a_filter_gets_the_values_the_filter_left_rather_than_all_of_them() {
let text = format!(
"Aggregate #1 groups=[#0.0::INTEGER] aggregates=[]\n Filter (#0.0::INTEGER > \
1::INTEGER)::BOOLEAN\n {}",
scan("t", 0)
);
assert_eq!(counted(&text, &[("t", 1000)], &[("t", "a", 100)]), Some(89));
}
#[test]
fn a_group_by_on_a_column_nobody_counted_is_the_constant_it_always_was() {
let text = format!("Aggregate #1 groups=[#0.0::INTEGER] aggregates=[]\n {}", scan("t", 0));
assert_eq!(
counted_stat(&text, &[("t", 1000)], &[]),
Stat::estimated(100, Provenance::Default)
);
}
#[test]
fn a_plain_distinct_groups_by_every_column_the_projection_under_it_produces() {
let text = format!(
"Distinct on=[]\n Project #1 [#0.0::INTEGER AS a, #0.1::INTEGER AS b]\n {}",
wide_scan("t", 0)
);
let keys = [("t", "a", 25), ("t", "b", 5)];
assert_eq!(counted(&text, &[("t", 100_000)], &keys), Some(125));
}
#[test]
fn a_distinct_on_some_columns_reads_the_columns_it_names() {
let text = format!(
"Distinct on=[#1.0::INTEGER]\n Project #1 [#0.0::INTEGER AS a, #0.1::INTEGER AS \
b]\n {}",
wide_scan("t", 0)
);
let keys = [("t", "a", 25), ("t", "b", 5)];
assert_eq!(counted(&text, &[("t", 100_000)], &keys), Some(25));
}
#[test]
fn a_group_by_over_a_join_keeps_the_constant_because_two_tables_have_two_row_counts() {
let text = format!(
"Aggregate #1 groups=[#0.0::INTEGER] aggregates=[]\n Join Inner on=[]\n {} {}",
scan("t", 0),
scan("u", 1)
);
assert_eq!(
counted_stat(&text, &[("t", 1000), ("u", 1000)], &[("t", "a", 25)]).provenance(),
Some(Provenance::Default)
);
}
#[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(), &Facts::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(), &Facts::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(), &Facts::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(), &Facts::new()), Stat::Unknown);
plan.measure(0, Stat::exact(1000, Provenance::RowCount));
let over = rows_stat(&plan, plan.root(), &Facts::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(), &Facts::new()), Stat::Unknown);
}
#[test]
fn facts_that_nobody_filled_in_say_so() {
let mut stats = Facts::new();
assert!(stats.is_empty());
stats.record("memory", "main", "t", 7);
assert!(!stats.is_empty());
assert_eq!(
stats.get(&Key::Rows { catalog: "memory", schema: "main", table: "t" }),
Stat::exact(7, Provenance::RowCount)
);
assert_eq!(
stats.get(&Key::Rows { catalog: "memory", schema: "other", table: "t" }),
Stat::Unknown
);
}
#[test]
fn a_distinct_count_says_where_it_came_from_and_a_missing_one_says_nothing() {
let mut stats = Facts::new();
stats.record_distinct("memory", "main", "t", "a", 25, Provenance::Dictionary);
let key = |column| Key::Distinct { catalog: "memory", schema: "main", table: "t", column };
assert_eq!(stats.get(&key("a")), Stat::exact(25, Provenance::Dictionary));
assert_eq!(stats.get(&key("b")), Stat::Unknown, "a column nobody counted");
}
#[test]
fn a_set_built_by_hand_can_never_be_mistaken_for_a_catalog_that_is_current() {
assert_eq!(Facts::new().generation(), 0);
assert_eq!(Facts::at(12).generation(), 12);
}
#[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));
}
fn joined_to_a_filtered_part(filter: bool) -> String {
let part = if filter {
concat!(
" Filter (#1.0::INTEGER = 3::INTEGER)::BOOLEAN\n",
" Get memory.main.part AS part #1 [a::INTEGER]\n"
)
} else {
" Get memory.main.part AS part #1 [a::INTEGER]\n"
};
format!(
"Join INNER on=[(#0.0::INTEGER = #1.0::INTEGER)::BOOLEAN]\n{}{part}",
" Get memory.main.lineitem AS lineitem #0 [a::INTEGER]\n"
)
}
#[test]
fn the_unfiltered_size_of_a_node_is_the_rows_the_filters_under_it_were_given() {
let tables = &[("lineitem", 6_000_000), ("part", 200_000)];
let text = joined_to_a_filtered_part(true);
assert_eq!(estimate(&text, tables), Some(1_200_000));
assert_eq!(whole(&text, tables), Some(6_000_000));
let plain = joined_to_a_filtered_part(false);
assert_eq!(estimate(&plain, tables), whole(&plain, tables));
}
#[test]
fn a_filter_under_one_side_makes_the_join_smaller_than_the_side_it_contains() {
let tables = &[("lineitem", 6_000_000), ("part", 200_000)];
assert_eq!(estimate(&joined_to_a_filtered_part(false), tables), Some(6_000_000));
assert_eq!(estimate(&joined_to_a_filtered_part(true), tables), Some(1_200_000));
}
#[test]
fn a_filter_is_charged_once_however_many_joins_sit_above_it() {
let text = concat!(
"Join INNER on=[(#0.0::INTEGER = #2.0::INTEGER)::BOOLEAN]\n",
" Join INNER on=[(#0.0::INTEGER = #1.0::INTEGER)::BOOLEAN]\n",
" Get memory.main.lineitem AS lineitem #0 [a::INTEGER]\n",
" Filter (#1.0::INTEGER = 3::INTEGER)::BOOLEAN\n",
" Get memory.main.part AS part #1 [a::INTEGER]\n",
" Get memory.main.supplier AS supplier #2 [a::INTEGER]\n"
);
let tables = &[("lineitem", 6_000_000), ("part", 200_000), ("supplier", 10_000)];
assert_eq!(estimate(text, tables), Some(1_200_000));
assert_eq!(whole(text, tables), Some(6_000_000));
}
#[test]
fn a_semi_join_is_walked_through_the_way_a_filter_over_the_same_side_is() {
let tables = &[("t", 1_000), ("u", 100)];
let before = filtered("(#0.0::INTEGER = 3::INTEGER)::BOOLEAN");
let after = concat!(
"Join SEMI on=[(#1.0::INTEGER = #0.0::INTEGER)::BOOLEAN]\n",
" Filter (#0.0::INTEGER = 3::INTEGER)::BOOLEAN\n",
" Get memory.main.t AS t #0 [a::INTEGER, b::INTEGER]\n",
" Get memory.main.u AS u #1 [a::INTEGER]\n"
);
assert_eq!(whole(&before, tables), Some(1_000));
assert_eq!(whole(after, tables), Some(1_000));
}
#[test]
fn a_side_with_nothing_under_it_is_whole_and_the_arithmetic_is_the_containment_reading() {
let sides = matched_sides(Side::whole(1_500_000), Side::whole(150_000), Some(150_000));
assert_eq!(sides, Side { rows: 1_500_000, base: 1_500_000 });
let cut = Side { rows: 15_000, base: 150_000 };
assert_eq!(
matched_sides(Side::whole(1_500_000), cut, Some(150_000)),
Side { rows: 150_000, base: 1_500_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::Dictionary)
);
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_filter_says_which_of_the_three_places_its_count_came_from() {
let tables = &[("t", 1_000_000)];
let one = filtered("(#0.0::INTEGER = 3::INTEGER)::BOOLEAN");
assert_eq!(
sourced_stat(&one, tables, &[("t", "a", 50, Provenance::Sketch)]).provenance(),
Some(Provenance::Sketch)
);
assert_eq!(
sourced_stat(&one, tables, &[("t", "a", 50, Provenance::Dictionary)]).provenance(),
Some(Provenance::Dictionary)
);
let both = filtered(
"((#0.0::INTEGER = 3::INTEGER)::BOOLEAN AND (#0.1::INTEGER = 4::INTEGER)::BOOLEAN)::BOOLEAN",
);
assert_eq!(
sourced_stat(
&both,
tables,
&[("t", "a", 50, Provenance::Sketch), ("t", "b", 40, Provenance::Dictionary)]
)
.provenance(),
Some(Provenance::Propagation)
);
assert_eq!(
sourced_stat(
&both,
tables,
&[("t", "a", 50, Provenance::Sketch), ("t", "b", 40, Provenance::Sketch)]
)
.provenance(),
Some(Provenance::Sketch)
);
}
#[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>,
spread: Option<f64>,
asked: Mutex<Vec<Test>>,
nulls: Stat<u64>,
}
impl Stub {
fn new(surviving: Option<u64>) -> Arc<Self> {
Arc::new(Self {
surviving,
spread: None,
asked: Mutex::new(Vec::new()),
nulls: Stat::Unknown,
})
}
fn spreading(spread: f64) -> Arc<Self> {
Arc::new(Self {
surviving: None,
spread: Some(spread),
asked: Mutex::new(Vec::new()),
nulls: Stat::Unknown,
})
}
fn counting(rows: u64, nulls: u64) -> Arc<Self> {
Arc::new(Self {
surviving: Some(rows),
spread: None,
asked: Mutex::new(Vec::new()),
nulls: Stat::exact(nulls, Provenance::NullCount),
})
}
}
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 spread(&self, tests: &[Test]) -> Option<Spread> {
self.asked.lock().expect("no test panics while holding this").extend_from_slice(tests);
self.spread.map(|fraction| Spread { fraction, read: tests.len() })
}
fn extreme(&self, _column: usize, _end: End) -> Stat<Bound> {
Stat::Unknown
}
fn nulls(&self, _column: usize) -> Stat<u64> {
self.nulls
}
}
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 = facts(&[("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 a_range_the_store_can_interpolate_takes_its_fraction_rather_than_the_constant() {
let text = format!("Filter (#0.0::INTEGER < 9::INTEGER)::BOOLEAN\n {}", bounded_scan());
let zones = Stub::spreading(0.98);
assert_eq!(zoned(&text, 1_000_000, &zones), Stat::estimated(980_000, Provenance::ZoneMap));
}
#[test]
fn a_fraction_above_the_constant_is_taken_as_readily_as_one_below_it() {
let text = format!("Filter (#0.0::INTEGER < 9::INTEGER)::BOOLEAN\n {}", bounded_scan());
let narrow = Stub::spreading(0.01);
assert_eq!(zoned(&text, 1_000_000, &narrow), Stat::estimated(10_000, Provenance::ZoneMap));
}
#[test]
fn a_fraction_of_nothing_is_still_a_row_and_is_still_a_guess() {
let text = format!("Filter (#0.0::INTEGER < 9::INTEGER)::BOOLEAN\n {}", bounded_scan());
let empty = Stub::spreading(0.0);
assert_eq!(zoned(&text, 1_000_000, &empty), Stat::estimated(1, Provenance::ZoneMap));
}
#[test]
fn a_counted_condition_and_an_interpolated_one_report_the_arithmetic_over_both() {
let text = format!(
"Filter ((#0.0::INTEGER = 5::INTEGER)::BOOLEAN AND (#0.1::INTEGER < 9::INTEGER)::BOOLEAN)::BOOLEAN\n {}",
bounded_scan()
);
let mut stats = facts(&[("t", 1_000_000)]);
stats.record_distinct("memory", "main", "t", "a", 10, Provenance::Dictionary);
let mut plan = Plan::parse(&text).expect("parses");
let zones = Stub::spreading(0.5);
plan.set_zones(0, Arc::clone(&zones) as Arc<dyn Zones>);
let stat = rows_stat(&plan, plan.root(), &stats);
assert_eq!(stat, Stat::estimated(50_000, Provenance::Propagation));
}
#[test]
fn a_condition_the_store_could_not_read_still_costs_the_constant() {
let text = format!(
"Filter ((#0.0::INTEGER < 9::INTEGER)::BOOLEAN AND (#0.0::INTEGER = #0.1::INTEGER)::BOOLEAN)::BOOLEAN\n {}",
bounded_scan()
);
let zones = Stub::spreading(0.5);
assert_eq!(
zoned(&text, 1_000_000, &zones),
Stat::estimated(100_000, Provenance::Propagation)
);
}
#[test]
fn the_count_answers_an_equality_before_the_bounds_are_asked_to_interpolate_it() {
let text = format!("Filter (#0.0::INTEGER = 5::INTEGER)::BOOLEAN\n {}", bounded_scan());
let mut stats = facts(&[("t", 1_000_000)]);
stats.record_distinct("memory", "main", "t", "a", 8, Provenance::Dictionary);
let mut plan = Plan::parse(&text).expect("parses");
plan.set_zones(0, Stub::spreading(0.5) as Arc<dyn Zones>);
assert_eq!(
rows_stat(&plan, plan.root(), &stats),
Stat::estimated(125_000, Provenance::Dictionary)
);
}
#[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!(!asked.is_empty(), "it was asked");
for test in asked.iter() {
assert_eq!(test.column, 1, "`a` is the store's column one");
assert_eq!(test.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));
}
#[test]
fn a_null_test_on_a_column_the_store_counted_takes_the_count_over_the_constant() {
let null = format!(
"Filter (#0.0::INTEGER IS NOT DISTINCT FROM NULL::INTEGER)::BOOLEAN\n {}",
bounded_scan()
);
let counted = Stub::counting(100_000, 10_000);
assert_eq!(zoned(&null, 100_000, &counted), Stat::estimated(10_000, Provenance::NullCount));
let present = format!(
"Filter (#0.0::INTEGER IS DISTINCT FROM NULL::INTEGER)::BOOLEAN\n {}",
bounded_scan()
);
assert_eq!(
zoned(&present, 100_000, &counted),
Stat::estimated(90_000, Provenance::NullCount)
);
}
#[test]
fn a_store_that_states_no_null_count_gets_the_constant_it_always_got() {
let text = format!(
"Filter (#0.0::INTEGER IS NOT DISTINCT FROM NULL::INTEGER)::BOOLEAN\n {}",
bounded_scan()
);
let quiet = Stub::new(Some(100_000));
assert_eq!(zoned(&text, 100_000, &quiet), Stat::estimated(20_000, Provenance::Default));
}
#[test]
fn a_distinctness_test_between_two_columns_is_not_a_null_test() {
let text = format!(
"Filter (#0.0::INTEGER IS DISTINCT FROM #0.1::INTEGER)::BOOLEAN\n {}",
bounded_scan()
);
let counted = Stub::counting(100_000, 10_000);
assert_eq!(zoned(&text, 100_000, &counted), Stat::estimated(20_000, Provenance::Default));
}
#[derive(Debug)]
struct Counted {
rows: u64,
held: Vec<Option<Vec<(i128, u64)>>>,
tail: Option<Remainder>,
asked: Mutex<Vec<usize>>,
}
impl Counted {
fn of(held: Option<Vec<(i128, u64)>>) -> Arc<Self> {
Arc::new(Self {
rows: 1_500_000,
held: vec![None, held],
tail: None,
asked: Mutex::new(Vec::new()),
})
}
fn prefixed(held: Vec<(i128, u64)>, tail: Remainder) -> Arc<Self> {
Arc::new(Self {
rows: 1_500_000,
held: vec![None, Some(held)],
tail: Some(tail),
asked: Mutex::new(Vec::new()),
})
}
}
impl Frequencies for Counted {
fn column(&self, name: &str) -> Option<usize> {
match name {
"b" => Some(0),
"a" => Some(1),
_ => None,
}
}
fn rows(&self) -> u64 {
self.rows
}
fn rows_with(&self, column: usize, value: &Bound) -> Stat<u64> {
self.asked.lock().expect("no test panics while holding this").push(column);
let (Some(Some(list)), Bound::Int(wanted)) = (self.held.get(column), value) else {
return Stat::Unknown;
};
match list.iter().find(|(held, _)| held == wanted) {
Some((_, of)) => Stat::exact(*of, Provenance::FrequencySynopsis),
None if self.tail.is_some() => Stat::Unknown,
None => Stat::exact(0, Provenance::FrequencySynopsis),
}
}
fn remainder(&self, column: usize) -> Option<Remainder> {
self.tail.filter(|_| column == 1)
}
}
fn common_stat(text: &str, distinct: u64, held: &Arc<Counted>) -> Stat<u64> {
let stats = facts(&[("t", 1_500_000)]);
let mut plan =
Plan::parse(text).unwrap_or_else(|error| panic!("{text} did not parse: {error}"));
plan.set_frequencies(0, Arc::clone(held) as Arc<dyn Frequencies>);
plan.measure_distinct(0, "a", Stat::exact(distinct, Provenance::Dictionary));
rows_stat(&plan, plan.root(), &stats)
}
#[test]
fn an_equality_on_a_counted_column_takes_the_count_over_the_uniform_guess() {
let text = format!("Filter (#0.0::INTEGER = 3::INTEGER)::BOOLEAN\n {}", bounded_scan());
let held = Counted::of(Some(vec![(3, 729_413), (4, 732_044), (5, 38_543)]));
assert_eq!(
common_stat(&text, 3, &held),
Stat::estimated(729_413, Provenance::FrequencySynopsis)
);
assert_eq!(*held.asked.lock().expect("not poisoned"), vec![1]);
}
#[test]
fn a_value_a_complete_synopsis_does_not_list_is_as_close_to_no_rows_as_the_guess_goes() {
let text = format!("Filter (#0.0::INTEGER = 9::INTEGER)::BOOLEAN\n {}", bounded_scan());
let held = Counted::of(Some(vec![(3, 729_413), (4, 732_044), (5, 38_543)]));
assert_eq!(common_stat(&text, 3, &held).value(), Some(&1));
}
fn dropped(most: u64) -> Arc<Counted> {
let held = vec![(3, 750_000), (4, 200_000)];
Counted::prefixed(held, Remainder { rows: 550_000, listed: 2, most })
}
#[test]
fn a_value_an_incomplete_synopsis_left_out_is_the_tail_spread_over_the_values_in_it() {
let text = format!("Filter {}\n {}", equals(0, 9), bounded_scan());
assert_eq!(
common_stat(&text, 1002, &dropped(5_000)),
Stat::estimated(550, Provenance::Propagation)
);
}
#[test]
fn the_bound_the_writer_recorded_caps_what_the_tail_is_spread_into() {
let text = format!("Filter {}\n {}", equals(0, 9), bounded_scan());
assert_eq!(common_stat(&text, 1002, &dropped(100)).value(), Some(&100));
}
#[test]
fn a_tail_too_small_to_divide_is_still_one_row_rather_than_none() {
let text = format!("Filter {}\n {}", equals(0, 9), bounded_scan());
let held = Counted::prefixed(
vec![(3, 750_000), (4, 749_995)],
Remainder { rows: 5, listed: 2, most: 10 },
);
assert_eq!(common_stat(&text, 1002, &held).value(), Some(&1));
}
#[test]
fn a_value_an_incomplete_synopsis_does_list_is_still_the_count_it_listed() {
let text = format!("Filter {}\n {}", equals(0, 3), bounded_scan());
assert_eq!(
common_stat(&text, 1002, &dropped(5_000)),
Stat::estimated(750_000, Provenance::FrequencySynopsis)
);
}
#[test]
fn an_in_list_over_a_prefix_adds_the_counts_it_has_to_the_tail_it_guesses() {
let text = one_of(0, &[3, 9]);
assert_eq!(
common_stat(&text, 1002, &dropped(5_000)),
Stat::estimated(750_549, Provenance::Propagation)
);
}
#[test]
fn a_distinct_count_below_what_the_synopsis_listed_falls_back_to_the_bound() {
let text = format!("Filter {}\n {}", equals(0, 9), bounded_scan());
assert_eq!(
common_stat(&text, 2, &dropped(5_000)),
Stat::estimated(5_000, Provenance::Propagation)
);
}
fn ceilinged(text: &str, distinct: u64, held: &Arc<Counted>) -> Stat<u64> {
let stats = facts(&[("t", 1_500_000)]);
let mut plan =
Plan::parse(text).unwrap_or_else(|error| panic!("{text} did not parse: {error}"));
plan.set_frequencies(0, Arc::clone(held) as Arc<dyn Frequencies>);
plan.measure_distinct(
0,
"a",
Stat::certified(distinct, 1.0, Direction::AtMost, Provenance::ZoneMap),
);
rows_stat(&plan, plan.root(), &stats)
}
#[test]
fn a_ceiling_on_the_values_is_not_a_count_and_does_not_divide_the_tail() {
let text = format!("Filter {}\n {}", equals(0, 9), bounded_scan());
assert_eq!(
ceilinged(&text, 10_002, &dropped(5_000)),
Stat::estimated(5_000, Provenance::Propagation)
);
assert_eq!(common_stat(&text, 1002, &dropped(5_000)).value(), Some(&550));
}
fn common_stat_of(text: &str, held: &Arc<Counted>, nulls: u64) -> Stat<u64> {
let stats = facts(&[("t", 1_500_000)]);
let mut plan =
Plan::parse(text).unwrap_or_else(|error| panic!("{text} did not parse: {error}"));
plan.set_frequencies(0, Arc::clone(held) as Arc<dyn Frequencies>);
plan.set_zones(0, Stub::counting(1_500_000, nulls) as Arc<dyn Zones>);
plan.measure_distinct(0, "a", Stat::exact(3, Provenance::Dictionary));
rows_stat(&plan, plan.root(), &stats)
}
fn equals(column: u32, value: i64) -> String {
format!("(#0.{column}::INTEGER = {value}::INTEGER)::BOOLEAN")
}
fn any_of(branches: &[String]) -> String {
format!("Filter ({})::BOOLEAN\n {}", branches.join(" OR "), bounded_scan())
}
fn one_of(column: u32, values: &[i64]) -> String {
let branches: Vec<String> = values.iter().map(|value| equals(column, *value)).collect();
any_of(&branches)
}
#[test]
fn an_inequality_on_a_counted_column_is_the_rows_less_that_value_and_less_the_nulls() {
let text = format!("Filter (#0.0::INTEGER <> 3::INTEGER)::BOOLEAN\n {}", bounded_scan());
let held = Counted::of(Some(vec![(3, 729_413), (4, 732_044), (5, 26_543)]));
assert_eq!(
common_stat_of(&text, &held, 12_000),
Stat::estimated(758_587, Provenance::FrequencySynopsis)
);
}
#[test]
fn an_inequality_gives_up_where_the_store_states_no_null_count() {
let text = format!("Filter (#0.0::INTEGER <> 3::INTEGER)::BOOLEAN\n {}", bounded_scan());
let held = Counted::of(Some(vec![(3, 729_413), (4, 732_044), (5, 38_543)]));
assert_eq!(common_stat(&text, 3, &held), Stat::estimated(300_000, Provenance::Default));
}
#[test]
fn a_list_of_values_on_a_counted_column_is_the_counts_added_up() {
let text = one_of(0, &[3, 5]);
let held = Counted::of(Some(vec![(3, 729_413), (4, 732_044), (5, 38_543)]));
assert_eq!(
common_stat_of(&text, &held, 0),
Stat::estimated(767_956, Provenance::FrequencySynopsis)
);
}
#[test]
fn a_disjunction_across_two_columns_is_not_a_list_and_is_not_added_up() {
let text = any_of(&[equals(0, 3), equals(1, 4)]);
let held = Counted::of(Some(vec![(3, 729_413), (4, 732_044), (5, 38_543)]));
assert_eq!(common_stat_of(&text, &held, 0), Stat::estimated(300_000, Provenance::Default));
}
#[test]
fn a_list_longer_than_the_cap_is_left_to_the_constant() {
let values: Vec<i64> = (0..33).collect();
let held = Counted::of(Some(vec![(3, 729_413), (4, 732_044), (5, 38_543)]));
assert_eq!(
common_stat_of(&one_of(0, &values), &held, 0),
Stat::estimated(300_000, Provenance::Default)
);
}
#[test]
fn a_column_with_no_synopsis_is_divided_by_its_distinct_count_the_way_it_always_was() {
let text = format!("Filter (#0.0::INTEGER = 3::INTEGER)::BOOLEAN\n {}", bounded_scan());
assert_eq!(
common_stat(&text, 3, &Counted::of(None)),
Stat::estimated(500_000, Provenance::Dictionary)
);
}
}