use std::collections::BTreeSet;
use std::sync::Arc;
use arrow::datatypes::{DataType, Schema};
use datafusion::common::ScalarValue;
use datafusion::logical_expr::Operator;
use datafusion::physical_expr::PhysicalExpr;
use datafusion::physical_expr::expressions::{
BinaryExpr, Column, InListExpr, IsNotNullExpr, IsNullExpr, Literal,
};
use crate::metadata_provider::DuckLakeTableColumn;
use crate::stats_encode;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum StatKind {
MinValue,
MaxValue,
NullCount,
ValueCount,
ContainsNan,
}
impl StatKind {
pub fn column_name(self) -> &'static str {
match self {
Self::MinValue => "min_value",
Self::MaxValue => "max_value",
Self::NullCount => "null_count",
Self::ValueCount => "value_count",
Self::ContainsNan => "contains_nan",
}
}
fn is_bound(self) -> bool {
matches!(self, Self::MinValue | Self::MaxValue)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StatsLiteral {
text: String,
cast: Option<DataType>,
unquoted: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BoundOp {
Lt,
LtEq,
Gt,
GtEq,
}
impl BoundOp {
fn sql(self) -> &'static str {
match self {
Self::Lt => "<",
Self::LtEq => "<=",
Self::Gt => ">",
Self::GtEq => ">=",
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum StatsExpr {
And(Vec<StatsExpr>),
Or(Vec<StatsExpr>),
LiteralWithinBounds(StatsLiteral),
NotEveryRowEqual(StatsLiteral),
BoundCompare {
stat: StatKind,
op: BoundOp,
literal: StatsLiteral,
},
CountPositive(StatKind),
FloatBoundsUnusable,
}
#[derive(Debug, Clone, PartialEq)]
pub struct StatsColumnFilter {
pub column_id: i64,
pub referenced_stats: BTreeSet<StatKind>,
pub needs_value_count_guard: bool,
pub condition: StatsExpr,
}
impl StatsColumnFilter {
pub fn cte_stats(&self) -> BTreeSet<StatKind> {
let mut stats = self.referenced_stats.clone();
if self.needs_value_count_guard {
stats.insert(StatKind::ValueCount);
}
stats
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct StatsFilter {
pub columns: Vec<StatsColumnFilter>,
}
pub trait StatsSqlDialect {
fn try_cast(&self, expr: &str, literal: &StatsLiteral, data_type: &DataType) -> Option<String>;
fn collate_binary(&self, expr: &str) -> String;
fn boolean_is_not_false(&self, expr: &str) -> String;
fn quote_literal(&self, text: &str) -> String {
format!("'{}'", text.replace('\'', "''"))
}
fn keep_when_unknown(&self, condition: &str) -> String {
format!("({condition}) IS NOT FALSE")
}
}
fn requires_value_comparison(data_type: &DataType) -> bool {
data_type.is_numeric() || data_type.is_temporal() || matches!(data_type, DataType::Boolean)
}
fn renders_unquoted(value: &ScalarValue, data_type: &DataType) -> bool {
if !data_type.is_numeric() {
return false;
}
match value {
ScalarValue::Float16(Some(v)) => v.is_finite(),
ScalarValue::Float32(Some(v)) => v.is_finite(),
ScalarValue::Float64(Some(v)) => v.is_finite(),
_ => true,
}
}
impl StatsLiteral {
fn new(value: &ScalarValue) -> Option<Self> {
if value.is_null() {
return None;
}
let data_type = value.data_type();
if matches!(
data_type,
DataType::Binary
| DataType::LargeBinary
| DataType::BinaryView
| DataType::FixedSizeBinary(_)
) {
return None;
}
if matches!(
value,
ScalarValue::Float16(Some(_))
| ScalarValue::Float32(Some(_))
| ScalarValue::Float64(Some(_))
) && !renders_unquoted(value, &data_type)
{
return None;
}
let text = stats_encode::encode_scalar(value)?;
if text.contains('\0') {
return None;
}
let cast = requires_value_comparison(&data_type).then_some(data_type.clone());
Some(Self {
text,
cast: cast.clone(),
unquoted: renders_unquoted(value, &data_type),
})
}
pub fn text(&self) -> &str {
&self.text
}
fn render(&self, ctx: &RenderContext<'_>) -> String {
if self.unquoted {
self.text.clone()
} else {
ctx.dialect.quote_literal(&self.text)
}
}
fn render_stat(&self, stat: StatKind, ctx: &RenderContext<'_>) -> Option<String> {
let raw = ctx.qualify(stat);
match &self.cast {
Some(data_type) => ctx.dialect.try_cast(&raw, self, data_type),
None => Some(ctx.dialect.collate_binary(&raw)),
}
}
}
struct RenderContext<'a> {
dialect: &'a dyn StatsSqlDialect,
alias: String,
}
impl RenderContext<'_> {
fn qualify(&self, stat: StatKind) -> String {
format!("{}.{}", self.alias, stat.column_name())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RenderedColumnFilter {
pub alias: String,
pub column_id: i64,
pub stats: Vec<&'static str>,
pub condition: String,
}
impl StatsFilter {
pub fn render(&self, dialect: &dyn StatsSqlDialect) -> Option<Vec<RenderedColumnFilter>> {
let rendered: Vec<_> = self
.columns
.iter()
.filter_map(|column| Self::render_column(column, dialect))
.collect();
(!rendered.is_empty()).then_some(rendered)
}
fn render_column(
column: &StatsColumnFilter,
dialect: &dyn StatsSqlDialect,
) -> Option<RenderedColumnFilter> {
let context = RenderContext {
dialect,
alias: format!("col_{}_stats", column.column_id),
};
let condition = render_expr(&column.condition, &context)?;
let null_checks: String = column
.referenced_stats
.iter()
.map(|stat| format!("{} IS NULL OR ", context.qualify(*stat)))
.collect();
let body = if column.needs_value_count_guard {
let value_count = context.qualify(StatKind::ValueCount);
format!("({value_count} IS NULL OR {value_count} > 0) AND ({null_checks}{condition})")
} else {
format!("{null_checks}{condition}")
};
let data_file_id = format!("{}.data_file_id", context.alias);
let condition = format!("({data_file_id} IS NULL OR ({body}))");
Some(RenderedColumnFilter {
alias: context.alias.clone(),
column_id: column.column_id,
stats: column
.cte_stats()
.iter()
.map(|stat| stat.column_name())
.collect(),
condition: dialect.keep_when_unknown(&condition),
})
}
}
fn render_expr(expr: &StatsExpr, ctx: &RenderContext<'_>) -> Option<String> {
match expr {
StatsExpr::And(children) => {
let parts: Vec<_> = children
.iter()
.filter_map(|child| render_expr(child, ctx))
.map(|part| format!("({part})"))
.collect();
(!parts.is_empty()).then(|| parts.join(" AND "))
},
StatsExpr::Or(children) => {
let mut parts = Vec::with_capacity(children.len());
for child in children {
parts.push(format!("({})", render_expr(child, ctx)?));
}
(!parts.is_empty()).then(|| parts.join(" OR "))
},
StatsExpr::LiteralWithinBounds(literal) => {
let min = literal.render_stat(StatKind::MinValue, ctx)?;
let max = literal.render_stat(StatKind::MaxValue, ctx)?;
Some(format!("{} BETWEEN {min} AND {max}", literal.render(ctx)))
},
StatsExpr::NotEveryRowEqual(literal) => {
let min = literal.render_stat(StatKind::MinValue, ctx)?;
let max = literal.render_stat(StatKind::MaxValue, ctx)?;
let value = literal.render(ctx);
Some(format!("NOT ({min} = {value} AND {max} = {value})"))
},
StatsExpr::BoundCompare {
stat,
op,
literal,
} => {
let bound = literal.render_stat(*stat, ctx)?;
Some(format!("{bound} {} {}", op.sql(), literal.render(ctx)))
},
StatsExpr::CountPositive(stat) => Some(format!("{} > 0", ctx.qualify(*stat))),
StatsExpr::FloatBoundsUnusable => Some(
ctx.dialect
.boolean_is_not_false(&ctx.qualify(StatKind::ContainsNan)),
),
}
}
struct Lowered {
condition: StatsExpr,
referenced_stats: BTreeSet<StatKind>,
}
pub fn lower_predicate(
predicate: &Arc<dyn PhysicalExpr>,
schema: &Schema,
columns: &[DuckLakeTableColumn],
) -> Option<StatsFilter> {
let mut by_column: Vec<(i64, Lowered)> = Vec::new();
for conjunct in datafusion::physical_expr::split_conjunction(predicate) {
let Some((column_id, lowered)) = lower_conjunct(conjunct, schema, columns) else {
continue;
};
match by_column.iter_mut().find(|(id, _)| *id == column_id) {
Some((_, existing)) => {
let previous =
std::mem::replace(&mut existing.condition, StatsExpr::And(Vec::new()));
existing.condition = match previous {
StatsExpr::And(mut parts) => {
parts.push(lowered.condition);
StatsExpr::And(parts)
},
other => StatsExpr::And(vec![other, lowered.condition]),
};
existing.referenced_stats.extend(lowered.referenced_stats);
},
None => by_column.push((column_id, lowered)),
}
}
if by_column.is_empty() {
return None;
}
by_column.sort_by_key(|(column_id, _)| *column_id);
let columns = by_column
.into_iter()
.map(|(column_id, lowered)| {
let reads_null_count = lowered.referenced_stats.contains(&StatKind::NullCount);
let reads_bound = lowered.referenced_stats.iter().any(|stat| stat.is_bound());
StatsColumnFilter {
column_id,
needs_value_count_guard: !reads_null_count && reads_bound,
referenced_stats: lowered.referenced_stats,
condition: lowered.condition,
}
})
.collect();
Some(StatsFilter {
columns,
})
}
fn lower_conjunct(
expr: &Arc<dyn PhysicalExpr>,
schema: &Schema,
columns: &[DuckLakeTableColumn],
) -> Option<(i64, Lowered)> {
let index = sole_column_index(expr)?;
let field = schema.fields().get(index)?;
let column = columns.get(index)?;
let mut referenced_stats = BTreeSet::new();
let mut condition = lower_node(expr, index, &mut referenced_stats)?;
if needs_float_gate(&condition) && is_float(field.data_type()) {
referenced_stats.insert(StatKind::ContainsNan);
condition = StatsExpr::Or(vec![StatsExpr::FloatBoundsUnusable, condition]);
}
Some((
column.column_id,
Lowered {
condition,
referenced_stats,
},
))
}
fn needs_float_gate(expr: &StatsExpr) -> bool {
match expr {
StatsExpr::And(children) | StatsExpr::Or(children) => children.iter().any(needs_float_gate),
StatsExpr::BoundCompare {
..
}
| StatsExpr::NotEveryRowEqual(_) => true,
StatsExpr::LiteralWithinBounds(_)
| StatsExpr::CountPositive(_)
| StatsExpr::FloatBoundsUnusable => false,
}
}
fn is_float(data_type: &DataType) -> bool {
matches!(
data_type,
DataType::Float16 | DataType::Float32 | DataType::Float64
)
}
fn sole_column_index(expr: &Arc<dyn PhysicalExpr>) -> Option<usize> {
use datafusion::common::tree_node::{TreeNode, TreeNodeRecursion};
let mut found: Option<usize> = None;
let mut conflicting = false;
expr.apply(|node| {
if let Some(column) = node.downcast_ref::<Column>() {
match found {
Some(index) if index != column.index() => {
conflicting = true;
return Ok(TreeNodeRecursion::Stop);
},
Some(_) => {},
None => found = Some(column.index()),
}
}
Ok(TreeNodeRecursion::Continue)
})
.ok()?;
(!conflicting).then_some(found).flatten()
}
fn lower_node(
expr: &Arc<dyn PhysicalExpr>,
column_index: usize,
referenced_stats: &mut BTreeSet<StatKind>,
) -> Option<StatsExpr> {
if let Some(binary) = expr.downcast_ref::<BinaryExpr>() {
return match binary.op() {
Operator::And => {
let mut parts = Vec::new();
let mut stats = BTreeSet::new();
for child in [binary.left(), binary.right()] {
let mut child_stats = BTreeSet::new();
if let Some(part) = lower_node(child, column_index, &mut child_stats) {
parts.push(part);
stats.extend(child_stats);
}
}
if parts.is_empty() {
return None;
}
referenced_stats.extend(stats);
Some(if parts.len() == 1 {
parts.remove(0)
} else {
StatsExpr::And(parts)
})
},
Operator::Or => {
let mut parts = Vec::new();
let mut stats = BTreeSet::new();
for child in [binary.left(), binary.right()] {
let mut child_stats = BTreeSet::new();
parts.push(lower_node(child, column_index, &mut child_stats)?);
stats.extend(child_stats);
}
referenced_stats.extend(stats);
Some(StatsExpr::Or(parts))
},
operator => lower_comparison(binary, *operator, column_index, referenced_stats),
};
}
if let Some(is_null) = expr.downcast_ref::<IsNullExpr>() {
subject_column(is_null.arg(), column_index)?;
referenced_stats.insert(StatKind::NullCount);
return Some(StatsExpr::CountPositive(StatKind::NullCount));
}
if let Some(is_not_null) = expr.downcast_ref::<IsNotNullExpr>() {
subject_column(is_not_null.arg(), column_index)?;
referenced_stats.insert(StatKind::ValueCount);
return Some(StatsExpr::CountPositive(StatKind::ValueCount));
}
if let Some(in_list) = expr.downcast_ref::<InListExpr>() {
if in_list.negated() {
return None;
}
subject_column(in_list.expr(), column_index)?;
if in_list.list().is_empty() {
return None;
}
let mut parts = Vec::with_capacity(in_list.list().len());
for element in in_list.list() {
let literal = element.downcast_ref::<Literal>()?;
parts.push(StatsExpr::LiteralWithinBounds(StatsLiteral::new(
literal.value(),
)?));
}
referenced_stats.insert(StatKind::MinValue);
referenced_stats.insert(StatKind::MaxValue);
return Some(if parts.len() == 1 {
parts.remove(0)
} else {
StatsExpr::Or(parts)
});
}
None
}
fn lower_comparison(
binary: &BinaryExpr,
operator: Operator,
column_index: usize,
referenced_stats: &mut BTreeSet<StatKind>,
) -> Option<StatsExpr> {
let (literal, operator) = if subject_column(binary.left(), column_index).is_some() {
(binary.right().downcast_ref::<Literal>()?, operator)
} else if subject_column(binary.right(), column_index).is_some() {
(
binary.left().downcast_ref::<Literal>()?,
flip_operator(operator)?,
)
} else {
return None;
};
let literal = StatsLiteral::new(literal.value())?;
let (stats, condition): (&[StatKind], StatsExpr) = match operator {
Operator::Eq => (
&[StatKind::MinValue, StatKind::MaxValue],
StatsExpr::LiteralWithinBounds(literal),
),
Operator::NotEq => (
&[StatKind::MinValue, StatKind::MaxValue],
StatsExpr::NotEveryRowEqual(literal),
),
Operator::Lt => (
&[StatKind::MinValue],
StatsExpr::BoundCompare {
stat: StatKind::MinValue,
op: BoundOp::Lt,
literal,
},
),
Operator::LtEq => (
&[StatKind::MinValue],
StatsExpr::BoundCompare {
stat: StatKind::MinValue,
op: BoundOp::LtEq,
literal,
},
),
Operator::Gt => (
&[StatKind::MaxValue],
StatsExpr::BoundCompare {
stat: StatKind::MaxValue,
op: BoundOp::Gt,
literal,
},
),
Operator::GtEq => (
&[StatKind::MaxValue],
StatsExpr::BoundCompare {
stat: StatKind::MaxValue,
op: BoundOp::GtEq,
literal,
},
),
_ => return None,
};
referenced_stats.extend(stats.iter().copied());
Some(condition)
}
fn subject_column(expr: &Arc<dyn PhysicalExpr>, column_index: usize) -> Option<()> {
let column = expr.downcast_ref::<Column>()?;
(column.index() == column_index).then_some(())
}
fn flip_operator(operator: Operator) -> Option<Operator> {
Some(match operator {
Operator::Eq => Operator::Eq,
Operator::NotEq => Operator::NotEq,
Operator::Lt => Operator::Gt,
Operator::LtEq => Operator::GtEq,
Operator::Gt => Operator::Lt,
Operator::GtEq => Operator::LtEq,
_ => return None,
})
}
#[cfg(test)]
mod tests {
use super::*;
use arrow::datatypes::{ArrowPrimitiveType, Field, TimeUnit};
use datafusion::physical_expr::expressions::{CastExpr, NotExpr, in_list, lit};
fn duckdb_type_name(data_type: &DataType) -> Option<String> {
Some(match data_type {
DataType::Boolean => "BOOLEAN".to_string(),
DataType::Int8 => "TINYINT".to_string(),
DataType::Int16 => "SMALLINT".to_string(),
DataType::Int32 => "INTEGER".to_string(),
DataType::Int64 => "BIGINT".to_string(),
DataType::UInt8 => "UTINYINT".to_string(),
DataType::UInt16 => "USMALLINT".to_string(),
DataType::UInt32 => "UINTEGER".to_string(),
DataType::UInt64 => "UBIGINT".to_string(),
DataType::Float32 => "FLOAT".to_string(),
DataType::Float64 => "DOUBLE".to_string(),
DataType::Date32 => "DATE".to_string(),
DataType::Decimal128(precision, scale) => {
format!("DECIMAL({precision},{scale})")
},
DataType::Timestamp(TimeUnit::Microsecond, None) => "TIMESTAMP".to_string(),
_ => return None,
})
}
struct Duck;
impl StatsSqlDialect for Duck {
fn try_cast(
&self,
expr: &str,
_literal: &StatsLiteral,
data_type: &DataType,
) -> Option<String> {
Some(format!(
"TRY_CAST({expr} AS {})",
duckdb_type_name(data_type)?
))
}
fn collate_binary(&self, expr: &str) -> String {
expr.to_string()
}
fn boolean_is_not_false(&self, expr: &str) -> String {
format!("{expr} IS NULL OR {expr} <> false")
}
}
struct NoBigint;
impl StatsSqlDialect for NoBigint {
fn try_cast(
&self,
expr: &str,
literal: &StatsLiteral,
data_type: &DataType,
) -> Option<String> {
if matches!(data_type, DataType::Int64) {
return None;
}
Duck.try_cast(expr, literal, data_type)
}
fn collate_binary(&self, expr: &str) -> String {
Duck.collate_binary(expr)
}
fn boolean_is_not_false(&self, expr: &str) -> String {
Duck.boolean_is_not_false(expr)
}
}
struct Fixture {
schema: Schema,
columns: Vec<DuckLakeTableColumn>,
}
impl Fixture {
fn new(spec: &[(&str, DataType, i64)]) -> Self {
let schema = Schema::new(
spec.iter()
.map(|(name, data_type, _)| Field::new(*name, data_type.clone(), true))
.collect::<Vec<_>>(),
);
let columns = spec
.iter()
.map(|(name, data_type, column_id)| {
DuckLakeTableColumn::new(
*column_id,
(*name).to_string(),
duckdb_type_name(data_type).unwrap_or_else(|| "VARCHAR".to_string()),
true,
)
})
.collect();
Self {
schema,
columns,
}
}
fn column(&self, name: &str) -> Arc<dyn PhysicalExpr> {
let index = self
.schema
.fields()
.iter()
.position(|field| field.name() == name)
.expect("column in fixture");
Arc::new(Column::new(name, index))
}
fn lower(&self, predicate: &Arc<dyn PhysicalExpr>) -> Option<StatsFilter> {
lower_predicate(predicate, &self.schema, &self.columns)
}
fn render_with(
&self,
predicate: &Arc<dyn PhysicalExpr>,
dialect: &dyn StatsSqlDialect,
) -> Option<Vec<RenderedColumnFilter>> {
self.lower(predicate)?.render(dialect)
}
fn render(&self, predicate: &Arc<dyn PhysicalExpr>) -> Vec<RenderedColumnFilter> {
self.render_with(predicate, &Duck).expect("rendered")
}
fn only(&self, predicate: &Arc<dyn PhysicalExpr>) -> RenderedColumnFilter {
let mut rendered = self.render(predicate);
assert_eq!(rendered.len(), 1, "expected exactly one column filter");
rendered.remove(0)
}
fn sql(&self, predicate: &Arc<dyn PhysicalExpr>) -> String {
self.only(predicate).condition
}
fn cte_stats(&self, predicate: &Arc<dyn PhysicalExpr>) -> Vec<&'static str> {
self.only(predicate).stats
}
fn lowers_to_nothing(&self, predicate: &Arc<dyn PhysicalExpr>) -> bool {
self.lower(predicate).is_none()
}
}
fn ints() -> Fixture {
Fixture::new(&[("a", DataType::Int32, 1), ("b", DataType::Int32, 2)])
}
fn bin(
left: Arc<dyn PhysicalExpr>,
op: Operator,
right: Arc<dyn PhysicalExpr>,
) -> Arc<dyn PhysicalExpr> {
Arc::new(BinaryExpr::new(left, op, right))
}
#[test]
fn eq_renders_literal_between_bounds() {
let table = ints();
let predicate = bin(table.column("a"), Operator::Eq, lit(5i32));
assert_eq!(
table.sql(&predicate),
"((col_1_stats.data_file_id IS NULL OR \
((col_1_stats.value_count IS NULL OR col_1_stats.value_count > 0) AND \
(col_1_stats.min_value IS NULL OR col_1_stats.max_value IS NULL OR \
5 BETWEEN TRY_CAST(col_1_stats.min_value AS INTEGER) AND \
TRY_CAST(col_1_stats.max_value AS INTEGER))))) IS NOT FALSE"
);
assert_eq!(
table.cte_stats(&predicate),
vec!["min_value", "max_value", "value_count"]
);
}
#[test]
fn not_eq_renders_not_every_row_equal() {
let table = ints();
let predicate = bin(table.column("a"), Operator::NotEq, lit(5i32));
assert_eq!(
table.sql(&predicate),
"((col_1_stats.data_file_id IS NULL OR \
((col_1_stats.value_count IS NULL OR col_1_stats.value_count > 0) AND \
(col_1_stats.min_value IS NULL OR col_1_stats.max_value IS NULL OR \
NOT (TRY_CAST(col_1_stats.min_value AS INTEGER) = 5 AND \
TRY_CAST(col_1_stats.max_value AS INTEGER) = 5))))) IS NOT FALSE"
);
}
#[test]
fn gt_uses_max_only() {
let table = ints();
let predicate = bin(table.column("a"), Operator::Gt, lit(5i32));
assert_eq!(
table.sql(&predicate),
"((col_1_stats.data_file_id IS NULL OR \
((col_1_stats.value_count IS NULL OR col_1_stats.value_count > 0) AND \
(col_1_stats.max_value IS NULL OR \
TRY_CAST(col_1_stats.max_value AS INTEGER) > 5)))) IS NOT FALSE"
);
assert_eq!(
table.cte_stats(&predicate),
vec!["max_value", "value_count"]
);
}
#[test]
fn gt_eq_uses_max_only() {
let table = ints();
let predicate = bin(table.column("a"), Operator::GtEq, lit(5i32));
assert_eq!(
table.sql(&predicate),
"((col_1_stats.data_file_id IS NULL OR \
((col_1_stats.value_count IS NULL OR col_1_stats.value_count > 0) AND \
(col_1_stats.max_value IS NULL OR \
TRY_CAST(col_1_stats.max_value AS INTEGER) >= 5)))) IS NOT FALSE"
);
}
#[test]
fn lt_uses_min_only() {
let table = ints();
let predicate = bin(table.column("a"), Operator::Lt, lit(5i32));
assert_eq!(
table.sql(&predicate),
"((col_1_stats.data_file_id IS NULL OR \
((col_1_stats.value_count IS NULL OR col_1_stats.value_count > 0) AND \
(col_1_stats.min_value IS NULL OR \
TRY_CAST(col_1_stats.min_value AS INTEGER) < 5)))) IS NOT FALSE"
);
assert_eq!(
table.cte_stats(&predicate),
vec!["min_value", "value_count"]
);
}
#[test]
fn lt_eq_uses_min_only() {
let table = ints();
let predicate = bin(table.column("a"), Operator::LtEq, lit(5i32));
assert_eq!(
table.sql(&predicate),
"((col_1_stats.data_file_id IS NULL OR \
((col_1_stats.value_count IS NULL OR col_1_stats.value_count > 0) AND \
(col_1_stats.min_value IS NULL OR \
TRY_CAST(col_1_stats.min_value AS INTEGER) <= 5)))) IS NOT FALSE"
);
}
#[test]
fn is_null_uses_null_count() {
let table = ints();
let predicate = Arc::new(IsNullExpr::new(table.column("a"))) as Arc<dyn PhysicalExpr>;
assert_eq!(
table.sql(&predicate),
"((col_1_stats.data_file_id IS NULL OR \
(col_1_stats.null_count IS NULL OR col_1_stats.null_count > 0))) IS NOT FALSE"
);
assert_eq!(table.cte_stats(&predicate), vec!["null_count"]);
}
#[test]
fn is_not_null_uses_value_count() {
let table = ints();
let predicate = Arc::new(IsNotNullExpr::new(table.column("a"))) as Arc<dyn PhysicalExpr>;
assert_eq!(
table.sql(&predicate),
"((col_1_stats.data_file_id IS NULL OR \
(col_1_stats.value_count IS NULL OR col_1_stats.value_count > 0))) IS NOT FALSE"
);
assert_eq!(table.cte_stats(&predicate), vec!["value_count"]);
}
#[test]
fn in_list_is_an_or_of_equalities() {
let table = ints();
let predicate = in_list(
table.column("a"),
vec![lit(1i32), lit(2i32)],
&false,
&table.schema,
)
.expect("in list");
assert_eq!(
table.sql(&predicate),
"((col_1_stats.data_file_id IS NULL OR \
((col_1_stats.value_count IS NULL OR col_1_stats.value_count > 0) AND \
(col_1_stats.min_value IS NULL OR col_1_stats.max_value IS NULL OR \
(1 BETWEEN TRY_CAST(col_1_stats.min_value AS INTEGER) AND \
TRY_CAST(col_1_stats.max_value AS INTEGER)) OR \
(2 BETWEEN TRY_CAST(col_1_stats.min_value AS INTEGER) AND \
TRY_CAST(col_1_stats.max_value AS INTEGER)))))) IS NOT FALSE"
);
assert_eq!(
table.cte_stats(&predicate),
vec!["min_value", "max_value", "value_count"]
);
}
#[test]
fn string_comparison_is_raw_and_collated() {
let table = Fixture::new(&[("s", DataType::Utf8, 4)]);
let predicate = bin(table.column("s"), Operator::Eq, lit("x'y"));
assert_eq!(
table.sql(&predicate),
"((col_4_stats.data_file_id IS NULL OR \
((col_4_stats.value_count IS NULL OR col_4_stats.value_count > 0) AND \
(col_4_stats.min_value IS NULL OR col_4_stats.max_value IS NULL OR \
'x''y' BETWEEN col_4_stats.min_value AND col_4_stats.max_value)))) IS NOT FALSE"
);
}
#[test]
fn g1_every_condition_is_wrapped_in_the_no_stats_row_guard() {
let table = ints();
let a = table.column("a");
let predicates: Vec<Arc<dyn PhysicalExpr>> = vec![
bin(Arc::clone(&a), Operator::Gt, lit(5i32)),
bin(Arc::clone(&a), Operator::Eq, lit(5i32)),
Arc::new(IsNullExpr::new(Arc::clone(&a))),
Arc::new(IsNotNullExpr::new(Arc::clone(&a))),
];
for predicate in &predicates {
let sql = table.sql(predicate);
assert!(
sql.starts_with("((col_1_stats.data_file_id IS NULL OR (")
&& sql.ends_with(")) IS NOT FALSE"),
"missing G1 wrapper: {sql}"
);
}
}
#[test]
fn g2_one_is_null_disjunct_per_referenced_stat() {
let table = ints();
let a = table.column("a");
let predicate = bin(
bin(Arc::clone(&a), Operator::Gt, lit(5i32)),
Operator::Or,
Arc::new(IsNullExpr::new(a)),
);
assert_eq!(
table.sql(&predicate),
"((col_1_stats.data_file_id IS NULL OR \
(col_1_stats.max_value IS NULL OR col_1_stats.null_count IS NULL OR \
(TRY_CAST(col_1_stats.max_value AS INTEGER) > 5) OR \
(col_1_stats.null_count > 0)))) IS NOT FALSE"
);
assert_eq!(table.cte_stats(&predicate), vec!["max_value", "null_count"]);
}
#[test]
fn g2_guard_only_value_count_is_selected_but_never_null_checked() {
let table = ints();
let predicate = bin(table.column("a"), Operator::Gt, lit(5i32));
let rendered = table.only(&predicate);
assert!(
rendered.stats.contains(&"value_count"),
"{:?}",
rendered.stats
);
assert_eq!(
rendered.condition,
"((col_1_stats.data_file_id IS NULL OR \
((col_1_stats.value_count IS NULL OR col_1_stats.value_count > 0) AND \
(col_1_stats.max_value IS NULL OR \
TRY_CAST(col_1_stats.max_value AS INTEGER) > 5)))) IS NOT FALSE"
);
assert_eq!(
rendered.condition.matches("value_count IS NULL").count(),
1,
"{}",
rendered.condition
);
assert!(
!rendered.condition.contains(
"col_1_stats.value_count IS NULL OR \
col_1_stats.max_value"
),
"{}",
rendered.condition
);
let lowered = table.lower(&predicate).expect("lowered");
let column = &lowered.columns[0];
assert!(column.needs_value_count_guard);
assert!(!column.referenced_stats.contains(&StatKind::ValueCount));
assert!(column.cte_stats().contains(&StatKind::ValueCount));
}
#[test]
fn g2_is_not_null_value_count_is_null_checked() {
let table = ints();
let predicate = Arc::new(IsNotNullExpr::new(table.column("a"))) as Arc<dyn PhysicalExpr>;
let rendered = table.only(&predicate);
assert_eq!(
rendered.condition,
"((col_1_stats.data_file_id IS NULL OR \
(col_1_stats.value_count IS NULL OR col_1_stats.value_count > 0))) IS NOT FALSE"
);
assert_eq!(
rendered.condition.matches("value_count IS NULL").count(),
1,
"{}",
rendered.condition
);
let lowered = table.lower(&predicate).expect("lowered");
let column = &lowered.columns[0];
assert!(!column.needs_value_count_guard);
assert!(column.referenced_stats.contains(&StatKind::ValueCount));
}
#[test]
fn g3_value_count_guard_present_for_every_value_based_comparison() {
let table = ints();
let a = table.column("a");
for op in [
Operator::Eq,
Operator::NotEq,
Operator::Lt,
Operator::LtEq,
Operator::Gt,
Operator::GtEq,
] {
let predicate = bin(Arc::clone(&a), op, lit(5i32));
let lowered = table.lower(&predicate).expect("lowered");
assert!(
lowered.columns[0].needs_value_count_guard,
"missing guard for {op:?}"
);
assert!(
table.sql(&predicate).contains(
"(col_1_stats.value_count IS NULL OR col_1_stats.value_count > 0) AND"
),
"missing guard SQL for {op:?}"
);
}
let in_predicate =
in_list(Arc::clone(&a), vec![lit(1i32)], &false, &table.schema).expect("in list");
assert!(table.lower(&in_predicate).expect("lowered").columns[0].needs_value_count_guard);
}
#[test]
fn g3_value_count_guard_absent_for_is_null() {
let table = ints();
let predicate = Arc::new(IsNullExpr::new(table.column("a"))) as Arc<dyn PhysicalExpr>;
let lowered = table.lower(&predicate).expect("lowered");
assert!(!lowered.columns[0].needs_value_count_guard);
assert!(!table.sql(&predicate).contains("value_count > 0"));
}
#[test]
fn g3_value_count_guard_absent_for_is_not_null() {
let table = ints();
let predicate = Arc::new(IsNotNullExpr::new(table.column("a"))) as Arc<dyn PhysicalExpr>;
let lowered = table.lower(&predicate).expect("lowered");
assert!(!lowered.columns[0].needs_value_count_guard);
}
#[test]
fn g3_value_count_guard_absent_when_is_null_appears_anywhere() {
let table = ints();
let a = table.column("a");
let predicate = bin(
bin(Arc::clone(&a), Operator::Gt, lit(5i32)),
Operator::Or,
Arc::new(IsNullExpr::new(a)),
);
let lowered = table.lower(&predicate).expect("lowered");
assert!(!lowered.columns[0].needs_value_count_guard);
let sql = table.sql(&predicate);
assert!(
!sql.contains("col_1_stats.value_count"),
"value_count must not appear at all: {sql}"
);
assert!(!table.cte_stats(&predicate).contains(&"value_count"));
}
#[test]
fn g3_value_count_guard_absent_when_is_null_is_a_separate_conjunct() {
let table = ints();
let a = table.column("a");
let predicate = bin(
bin(Arc::clone(&a), Operator::Gt, lit(5i32)),
Operator::And,
Arc::new(IsNullExpr::new(a)),
);
let lowered = table.lower(&predicate).expect("lowered");
assert!(!lowered.columns[0].needs_value_count_guard);
assert_eq!(
table.sql(&predicate),
"((col_1_stats.data_file_id IS NULL OR \
(col_1_stats.max_value IS NULL OR col_1_stats.null_count IS NULL OR \
(TRY_CAST(col_1_stats.max_value AS INTEGER) > 5) AND \
(col_1_stats.null_count > 0)))) IS NOT FALSE"
);
}
#[test]
fn g4_column_lowering_to_nothing_contributes_no_entry() {
let table = ints();
let cast_b = Arc::new(CastExpr::new(table.column("b"), DataType::Int64, None));
let predicate = bin(
bin(table.column("a"), Operator::Gt, lit(5i32)),
Operator::And,
bin(cast_b, Operator::Gt, lit(5i64)),
);
let rendered = table.render(&predicate);
assert_eq!(rendered.len(), 1);
assert_eq!(rendered[0].column_id, 1);
assert_eq!(rendered[0].alias, "col_1_stats");
}
#[test]
fn g8_cast_subject_pushes_down_nothing() {
let table = ints();
let cast_a = Arc::new(CastExpr::new(table.column("a"), DataType::Int64, None));
let predicate = bin(cast_a, Operator::Gt, lit(5i64));
assert!(table.lowers_to_nothing(&predicate));
}
#[test]
fn g8_column_to_column_comparison_pushes_down_nothing() {
let table = ints();
let predicate = bin(table.column("a"), Operator::Gt, table.column("b"));
assert!(table.lowers_to_nothing(&predicate));
}
#[test]
fn g9_constant_on_the_left_flips_the_operator() {
let table = ints();
let flipped = bin(lit(5i32), Operator::Lt, table.column("a"));
let direct = bin(table.column("a"), Operator::Gt, lit(5i32));
assert_eq!(table.sql(&flipped), table.sql(&direct));
assert_eq!(
table.sql(&flipped),
"((col_1_stats.data_file_id IS NULL OR \
((col_1_stats.value_count IS NULL OR col_1_stats.value_count > 0) AND \
(col_1_stats.max_value IS NULL OR \
TRY_CAST(col_1_stats.max_value AS INTEGER) > 5)))) IS NOT FALSE"
);
for (constant_op, column_op) in [
(Operator::Eq, Operator::Eq),
(Operator::NotEq, Operator::NotEq),
(Operator::Lt, Operator::Gt),
(Operator::LtEq, Operator::GtEq),
(Operator::Gt, Operator::Lt),
(Operator::GtEq, Operator::LtEq),
] {
let flipped = bin(lit(5i32), constant_op, table.column("a"));
let direct = bin(table.column("a"), column_op, lit(5i32));
assert_eq!(
table.sql(&flipped),
table.sql(&direct),
"{constant_op:?} should flip to {column_op:?}"
);
}
}
#[test]
fn g10_and_drops_an_unlowerable_child_and_keeps_going() {
let table = ints();
let a = table.column("a");
let cast_a = Arc::new(CastExpr::new(Arc::clone(&a), DataType::Int64, None));
let predicate = bin(
bin(
bin(Arc::clone(&a), Operator::Gt, lit(5i32)),
Operator::And,
bin(cast_a, Operator::Gt, lit(5i64)),
),
Operator::Or,
bin(Arc::clone(&a), Operator::Lt, lit(2i32)),
);
assert_eq!(
table.sql(&predicate),
"((col_1_stats.data_file_id IS NULL OR \
((col_1_stats.value_count IS NULL OR col_1_stats.value_count > 0) AND \
(col_1_stats.min_value IS NULL OR col_1_stats.max_value IS NULL OR \
(TRY_CAST(col_1_stats.max_value AS INTEGER) > 5) OR \
(TRY_CAST(col_1_stats.min_value AS INTEGER) < 2))))) IS NOT FALSE"
);
}
#[test]
fn g10_or_with_any_unlowerable_child_lowers_to_nothing() {
let table = ints();
let a = table.column("a");
let cast_a = Arc::new(CastExpr::new(Arc::clone(&a), DataType::Int64, None));
let predicate = bin(
bin(Arc::clone(&a), Operator::Gt, lit(5i32)),
Operator::Or,
bin(cast_a, Operator::Gt, lit(5i64)),
);
assert!(
table.lowers_to_nothing(&predicate),
"an OR with an unlowerable branch must not prune: {:?}",
table.lower(&predicate)
);
}
#[test]
fn g10_top_level_conjunct_that_cannot_lower_is_skipped() {
let table = ints();
let a = table.column("a");
let cast_a = Arc::new(CastExpr::new(Arc::clone(&a), DataType::Int64, None));
let predicate = bin(
bin(cast_a, Operator::Gt, lit(5i64)),
Operator::And,
bin(a, Operator::Lt, lit(2i32)),
);
assert_eq!(
table.sql(&predicate),
"((col_1_stats.data_file_id IS NULL OR \
((col_1_stats.value_count IS NULL OR col_1_stats.value_count > 0) AND \
(col_1_stats.min_value IS NULL OR \
TRY_CAST(col_1_stats.min_value AS INTEGER) < 2)))) IS NOT FALSE"
);
}
#[test]
fn g11_cast_target_type_comes_from_the_constant_not_the_column() {
let table = ints();
let predicate = bin(table.column("a"), Operator::Gt, lit(5i64));
let sql = table.sql(&predicate);
assert!(
sql.contains("TRY_CAST(col_1_stats.max_value AS BIGINT)"),
"{sql}"
);
assert!(!sql.contains("INTEGER"), "{sql}");
assert_eq!(
sql,
"((col_1_stats.data_file_id IS NULL OR \
((col_1_stats.value_count IS NULL OR col_1_stats.value_count > 0) AND \
(col_1_stats.max_value IS NULL OR \
TRY_CAST(col_1_stats.max_value AS BIGINT) > 5)))) IS NOT FALSE"
);
}
#[test]
fn null_constant_pushes_down_nothing() {
let table = ints();
let predicate = bin(
table.column("a"),
Operator::Eq,
lit(ScalarValue::Int32(None)),
);
assert!(table.lowers_to_nothing(&predicate));
}
#[test]
fn binary_constants_push_down_nothing() {
let table = Fixture::new(&[("blob", DataType::Binary, 1)]);
let values = [
ScalarValue::Binary(Some(vec![1, 2, 3])),
ScalarValue::LargeBinary(Some(vec![1, 2, 3])),
ScalarValue::BinaryView(Some(vec![1, 2, 3])),
ScalarValue::FixedSizeBinary(3, Some(vec![1, 2, 3])),
];
for value in values {
let predicate = bin(table.column("blob"), Operator::Eq, lit(value.clone()));
assert!(
table.lowers_to_nothing(&predicate),
"BLOB must never push down: {value:?}"
);
}
}
#[test]
fn over_long_string_constant_pushes_down_nothing() {
let table = Fixture::new(&[("s", DataType::Utf8, 1)]);
let long = "x".repeat(stats_encode::MAX_STRING_STAT_BYTES + 1);
let predicate = bin(table.column("s"), Operator::Eq, lit(long));
assert!(table.lowers_to_nothing(&predicate));
let exact = "x".repeat(stats_encode::MAX_STRING_STAT_BYTES);
let predicate = bin(table.column("s"), Operator::Eq, lit(exact));
assert!(!table.lowers_to_nothing(&predicate));
}
#[test]
fn string_constant_with_nul_byte_pushes_down_nothing() {
let table = Fixture::new(&[("s", DataType::Utf8, 1)]);
let predicate = bin(table.column("s"), Operator::Eq, lit("ab\0cd"));
assert!(table.lowers_to_nothing(&predicate));
}
#[test]
fn nan_constant_pushes_down_nothing() {
let table = Fixture::new(&[("f", DataType::Float64, 1)]);
for op in [
Operator::Eq,
Operator::NotEq,
Operator::Lt,
Operator::LtEq,
Operator::Gt,
Operator::GtEq,
] {
let predicate = bin(table.column("f"), op, lit(f64::NAN));
assert!(
table.lowers_to_nothing(&predicate),
"NaN constant must not prune for {op:?}"
);
}
let predicate = bin(table.column("f"), Operator::Gt, lit(f32::NAN));
assert!(table.lowers_to_nothing(&predicate));
}
#[test]
fn infinite_constant_pushes_down_nothing() {
let table = Fixture::new(&[("f", DataType::Float64, 1)]);
for value in [f64::INFINITY, f64::NEG_INFINITY] {
for op in [
Operator::Eq,
Operator::NotEq,
Operator::Lt,
Operator::LtEq,
Operator::Gt,
Operator::GtEq,
] {
let predicate = bin(table.column("f"), op, lit(value));
assert!(
table.lowers_to_nothing(&predicate),
"{value} must not prune for {op:?}"
);
}
}
let floats = Fixture::new(&[("g", DataType::Float32, 1)]);
let predicate = bin(floats.column("g"), Operator::Lt, lit(f32::NEG_INFINITY));
assert!(floats.lowers_to_nothing(&predicate));
let predicate = in_list(
table.column("f"),
vec![lit(1.0f64), lit(f64::INFINITY)],
&false,
&table.schema,
)
.expect("in list");
assert!(table.lowers_to_nothing(&predicate));
}
#[test]
fn in_list_with_a_null_element_kills_the_whole_list() {
let table = ints();
let predicate = in_list(
table.column("a"),
vec![lit(1i32), lit(ScalarValue::Int32(None)), lit(3i32)],
&false,
&table.schema,
)
.expect("in list");
assert!(table.lowers_to_nothing(&predicate));
}
#[test]
fn in_list_with_a_non_literal_element_kills_the_whole_list() {
let table = ints();
let computed = bin(lit(2i32), Operator::Plus, lit(3i32));
let predicate = in_list(
table.column("a"),
vec![lit(1i32), computed],
&false,
&table.schema,
)
.expect("in list");
assert!(table.lowers_to_nothing(&predicate));
}
#[test]
fn in_list_with_an_unencodable_element_kills_the_whole_list() {
let table = Fixture::new(&[("f", DataType::Float64, 1)]);
let predicate = in_list(
table.column("f"),
vec![lit(1.0f64), lit(f64::NAN)],
&false,
&table.schema,
)
.expect("in list");
assert!(table.lowers_to_nothing(&predicate));
}
#[test]
fn not_in_pushes_down_nothing() {
let table = ints();
let predicate = in_list(
table.column("a"),
vec![lit(1i32), lit(2i32)],
&true,
&table.schema,
)
.expect("not in list");
assert!(table.lowers_to_nothing(&predicate));
}
#[test]
fn empty_in_list_pushes_down_nothing() {
let table = ints();
let predicate =
in_list(table.column("a"), vec![], &false, &table.schema).expect("empty in list");
assert!(table.lowers_to_nothing(&predicate));
}
#[test]
fn conjunct_spanning_two_columns_pushes_down_nothing() {
let table = ints();
let predicate = bin(
bin(table.column("a"), Operator::Gt, lit(5i32)),
Operator::Or,
bin(table.column("b"), Operator::Lt, lit(3i32)),
);
assert!(table.lowers_to_nothing(&predicate));
}
#[test]
fn two_conjuncts_on_two_columns_render_in_column_id_order() {
let table = Fixture::new(&[("a", DataType::Int32, 9), ("b", DataType::Int32, 2)]);
let predicate = bin(
bin(table.column("a"), Operator::Gt, lit(5i32)),
Operator::And,
bin(table.column("b"), Operator::Lt, lit(3i32)),
);
let rendered = table.render(&predicate);
assert_eq!(
rendered
.iter()
.map(|filter| filter.column_id)
.collect::<Vec<_>>(),
vec![2, 9]
);
assert_eq!(
rendered
.iter()
.map(|filter| filter.alias.as_str())
.collect::<Vec<_>>(),
vec!["col_2_stats", "col_9_stats"]
);
assert_eq!(
rendered[0].condition,
"((col_2_stats.data_file_id IS NULL OR \
((col_2_stats.value_count IS NULL OR col_2_stats.value_count > 0) AND \
(col_2_stats.min_value IS NULL OR \
TRY_CAST(col_2_stats.min_value AS INTEGER) < 3)))) IS NOT FALSE"
);
assert_eq!(
rendered[1].condition,
"((col_9_stats.data_file_id IS NULL OR \
((col_9_stats.value_count IS NULL OR col_9_stats.value_count > 0) AND \
(col_9_stats.max_value IS NULL OR \
TRY_CAST(col_9_stats.max_value AS INTEGER) > 5)))) IS NOT FALSE"
);
}
#[test]
fn two_conjuncts_on_one_column_intersect_into_one_entry() {
let table = ints();
let a = table.column("a");
let predicate = bin(
bin(Arc::clone(&a), Operator::Gt, lit(5i32)),
Operator::And,
bin(a, Operator::Lt, lit(10i32)),
);
assert_eq!(
table.sql(&predicate),
"((col_1_stats.data_file_id IS NULL OR \
((col_1_stats.value_count IS NULL OR col_1_stats.value_count > 0) AND \
(col_1_stats.min_value IS NULL OR col_1_stats.max_value IS NULL OR \
(TRY_CAST(col_1_stats.max_value AS INTEGER) > 5) AND \
(TRY_CAST(col_1_stats.min_value AS INTEGER) < 10))))) IS NOT FALSE"
);
assert_eq!(
table.cte_stats(&predicate),
vec!["min_value", "max_value", "value_count"]
);
}
#[test]
fn three_conjuncts_on_one_column_intersect_flat() {
let table = ints();
let a = table.column("a");
let predicate = bin(
bin(
bin(Arc::clone(&a), Operator::Gt, lit(5i32)),
Operator::And,
bin(Arc::clone(&a), Operator::Lt, lit(10i32)),
),
Operator::And,
bin(a, Operator::NotEq, lit(7i32)),
);
assert_eq!(
table.sql(&predicate),
"((col_1_stats.data_file_id IS NULL OR \
((col_1_stats.value_count IS NULL OR col_1_stats.value_count > 0) AND \
(col_1_stats.min_value IS NULL OR col_1_stats.max_value IS NULL OR \
(TRY_CAST(col_1_stats.max_value AS INTEGER) > 5) AND \
(TRY_CAST(col_1_stats.min_value AS INTEGER) < 10) AND \
(NOT (TRY_CAST(col_1_stats.min_value AS INTEGER) = 7 AND \
TRY_CAST(col_1_stats.max_value AS INTEGER) = 7)))))) IS NOT FALSE"
);
}
#[test]
fn float_bound_condition_is_gated_on_contains_nan() {
let table = Fixture::new(&[("f", DataType::Float64, 3)]);
let predicate = bin(table.column("f"), Operator::Lt, lit(5.0f64));
assert_eq!(
table.sql(&predicate),
"((col_3_stats.data_file_id IS NULL OR \
((col_3_stats.value_count IS NULL OR col_3_stats.value_count > 0) AND \
(col_3_stats.min_value IS NULL OR col_3_stats.contains_nan IS NULL OR \
(col_3_stats.contains_nan IS NULL OR col_3_stats.contains_nan <> false) OR \
(TRY_CAST(col_3_stats.min_value AS DOUBLE) < 5.0))))) IS NOT FALSE"
);
assert_eq!(
table.cte_stats(&predicate),
vec!["min_value", "value_count", "contains_nan"]
);
}
#[test]
fn float32_bound_condition_is_gated_too() {
let table = Fixture::new(&[("f", DataType::Float32, 3)]);
let predicate = bin(table.column("f"), Operator::Gt, lit(5.0f32));
assert_eq!(
table.sql(&predicate),
"((col_3_stats.data_file_id IS NULL OR \
((col_3_stats.value_count IS NULL OR col_3_stats.value_count > 0) AND \
(col_3_stats.max_value IS NULL OR col_3_stats.contains_nan IS NULL OR \
(col_3_stats.contains_nan IS NULL OR col_3_stats.contains_nan <> false) OR \
(TRY_CAST(col_3_stats.max_value AS FLOAT) > 5.0))))) IS NOT FALSE"
);
}
#[test]
fn float_is_null_is_not_gated() {
let table = Fixture::new(&[("f", DataType::Float64, 3)]);
let predicate = Arc::new(IsNullExpr::new(table.column("f"))) as Arc<dyn PhysicalExpr>;
assert_eq!(
table.sql(&predicate),
"((col_3_stats.data_file_id IS NULL OR \
(col_3_stats.null_count IS NULL OR col_3_stats.null_count > 0))) IS NOT FALSE"
);
assert_eq!(table.cte_stats(&predicate), vec!["null_count"]);
}
#[test]
fn float_is_not_null_is_not_gated() {
let table = Fixture::new(&[("f", DataType::Float64, 3)]);
let predicate = Arc::new(IsNotNullExpr::new(table.column("f"))) as Arc<dyn PhysicalExpr>;
assert_eq!(
table.sql(&predicate),
"((col_3_stats.data_file_id IS NULL OR \
(col_3_stats.value_count IS NULL OR col_3_stats.value_count > 0))) IS NOT FALSE"
);
assert!(!table.cte_stats(&predicate).contains(&"contains_nan"));
}
#[test]
fn non_float_column_is_never_gated() {
let table = ints();
for predicate in [
bin(table.column("a"), Operator::Gt, lit(5i32)),
bin(table.column("a"), Operator::Eq, lit(5i32)),
bin(table.column("a"), Operator::NotEq, lit(5i32)),
] {
let sql = table.sql(&predicate);
assert!(!sql.contains("contains_nan"), "{sql}");
assert!(!table.cte_stats(&predicate).contains(&"contains_nan"));
}
let dates = Fixture::new(&[("d", DataType::Date32, 1)]);
let predicate = bin(
dates.column("d"),
Operator::Gt,
lit(ScalarValue::Date32(Some(1))),
);
assert!(!dates.sql(&predicate).contains("contains_nan"));
}
#[test]
fn float_equals_nan_lowers_to_nothing() {
let table = Fixture::new(&[("f", DataType::Float64, 3)]);
let predicate = bin(table.column("f"), Operator::Eq, lit(f64::NAN));
assert!(table.lowers_to_nothing(&predicate));
}
fn or3(left: Option<bool>, right: Option<bool>) -> Option<bool> {
match (left, right) {
(Some(true), _) | (_, Some(true)) => Some(true),
(Some(false), Some(false)) => Some(false),
_ => None,
}
}
fn and3(left: Option<bool>, right: Option<bool>) -> Option<bool> {
match (left, right) {
(Some(false), _) | (_, Some(false)) => Some(false),
(Some(true), Some(true)) => Some(true),
_ => None,
}
}
#[test]
fn float_gate_admits_exactly_the_same_files_as_official_for_gt_and_gt_eq() {
let constant = 5.0_f64;
for contains_nan in [None, Some(true), Some(false)] {
for max_value in [None, Some(1.0_f64), Some(9.0_f64)] {
for gt_eq in [false, true] {
let bound_matches = max_value.map(|max| {
if gt_eq {
max >= constant
} else {
max > constant
}
});
let official = or3(
or3(Some(max_value.is_none()), Some(contains_nan.is_none())),
or3(bound_matches, contains_nan),
);
let gate = or3(Some(contains_nan.is_none()), contains_nan);
let ours = or3(
or3(Some(max_value.is_none()), Some(contains_nan.is_none())),
or3(gate, bound_matches),
);
assert_eq!(
official == Some(true),
ours == Some(true),
"diverged for contains_nan={contains_nan:?} \
max_value={max_value:?} gt_eq={gt_eq}"
);
assert_ne!(ours, None, "condition should not be unknown here");
assert_ne!(official, None, "condition should not be unknown here");
}
}
}
for value_count in [None, Some(0_i64), Some(3_i64)] {
let guard = or3(
Some(value_count.is_none()),
value_count.map(|count| count > 0),
);
assert_eq!(
and3(guard, Some(true)) == Some(true),
value_count != Some(0),
"value_count guard model wrong for {value_count:?}"
);
}
}
#[test]
fn rendering_is_byte_identical_across_runs() {
let table = Fixture::new(&[
("a", DataType::Int32, 9),
("b", DataType::Utf8, 2),
("f", DataType::Float64, 5),
]);
let build = || {
bin(
bin(
bin(table.column("a"), Operator::Gt, lit(5i32)),
Operator::And,
bin(table.column("b"), Operator::Eq, lit("x")),
),
Operator::And,
bin(table.column("f"), Operator::Lt, lit(2.5f64)),
)
};
let first = table.render(&build());
for _ in 0..8 {
assert_eq!(table.render(&build()), first);
}
assert_eq!(
first
.iter()
.map(|filter| filter.column_id)
.collect::<Vec<_>>(),
vec![2, 5, 9],
"columns must come out sorted by column_id"
);
}
#[test]
fn declined_cast_drops_only_that_comparison_inside_an_and() {
let table = ints();
let a = table.column("a");
let predicate = bin(
bin(Arc::clone(&a), Operator::Gt, lit(5i32)),
Operator::And,
bin(a, Operator::Gt, lit(6i64)),
);
assert_eq!(
table.sql(&predicate),
"((col_1_stats.data_file_id IS NULL OR \
((col_1_stats.value_count IS NULL OR col_1_stats.value_count > 0) AND \
(col_1_stats.max_value IS NULL OR \
(TRY_CAST(col_1_stats.max_value AS INTEGER) > 5) AND \
(TRY_CAST(col_1_stats.max_value AS BIGINT) > 6))))) IS NOT FALSE"
);
let rendered = table
.render_with(&predicate, &NoBigint)
.expect("the INTEGER comparison still renders");
assert_eq!(rendered.len(), 1);
assert_eq!(
rendered[0].condition,
"((col_1_stats.data_file_id IS NULL OR \
((col_1_stats.value_count IS NULL OR col_1_stats.value_count > 0) AND \
(col_1_stats.max_value IS NULL OR \
(TRY_CAST(col_1_stats.max_value AS INTEGER) > 5))))) IS NOT FALSE"
);
}
#[test]
fn declined_cast_collapses_a_whole_or_but_leaves_other_columns() {
let table = ints();
let a = table.column("a");
let predicate = bin(
bin(
bin(Arc::clone(&a), Operator::Gt, lit(5i32)),
Operator::Or,
bin(a, Operator::Gt, lit(6i64)),
),
Operator::And,
bin(table.column("b"), Operator::Lt, lit(3i32)),
);
assert_eq!(
table
.render(&predicate)
.iter()
.map(|filter| filter.column_id)
.collect::<Vec<_>>(),
vec![1, 2]
);
let rendered = table
.render_with(&predicate, &NoBigint)
.expect("column b still renders");
assert_eq!(rendered.len(), 1);
assert_eq!(rendered[0].column_id, 2);
assert_eq!(
rendered[0].condition,
"((col_2_stats.data_file_id IS NULL OR \
((col_2_stats.value_count IS NULL OR col_2_stats.value_count > 0) AND \
(col_2_stats.min_value IS NULL OR \
TRY_CAST(col_2_stats.min_value AS INTEGER) < 3)))) IS NOT FALSE"
);
}
#[test]
fn declining_every_cast_renders_nothing_at_all() {
let table = ints();
let predicate = bin(table.column("a"), Operator::Gt, lit(6i64));
assert!(table.render_with(&predicate, &NoBigint).is_none());
}
#[test]
fn not_expression_pushes_down_nothing() {
let table = ints();
let inner = bin(table.column("a"), Operator::Gt, lit(5i32));
let predicate = Arc::new(NotExpr::new(inner)) as Arc<dyn PhysicalExpr>;
assert!(table.lowers_to_nothing(&predicate));
}
#[test]
fn non_comparison_operators_push_down_nothing() {
let table = ints();
for op in
[Operator::IsDistinctFrom, Operator::IsNotDistinctFrom, Operator::Plus, Operator::Minus]
{
let predicate = bin(table.column("a"), op, lit(5i32));
assert!(table.lowers_to_nothing(&predicate), "{op:?} must not prune");
}
}
#[test]
fn is_null_on_a_cast_subject_pushes_down_nothing() {
let table = ints();
let cast_a = Arc::new(CastExpr::new(table.column("a"), DataType::Int64, None));
let predicate = Arc::new(IsNullExpr::new(cast_a)) as Arc<dyn PhysicalExpr>;
assert!(table.lowers_to_nothing(&predicate));
}
#[test]
fn is_not_null_on_a_cast_subject_pushes_down_nothing() {
let table = ints();
let cast_a = Arc::new(CastExpr::new(table.column("a"), DataType::Int64, None));
let predicate = Arc::new(IsNotNullExpr::new(cast_a)) as Arc<dyn PhysicalExpr>;
assert!(table.lowers_to_nothing(&predicate));
}
#[test]
fn in_list_with_a_cast_subject_pushes_down_nothing() {
let table = ints();
let cast_a = Arc::new(CastExpr::new(table.column("a"), DataType::Int64, None));
let predicate =
in_list(cast_a, vec![lit(1i64), lit(2i64)], &false, &table.schema).expect("in list");
assert!(table.lowers_to_nothing(&predicate));
}
#[test]
fn predicate_with_no_column_reference_pushes_down_nothing() {
let table = ints();
let predicate = bin(lit(1i32), Operator::Gt, lit(0i32));
assert!(table.lowers_to_nothing(&predicate));
}
#[test]
fn and_with_both_children_unlowerable_collapses_its_or_parent() {
let table = ints();
let a = table.column("a");
let cast_a = || Arc::new(CastExpr::new(table.column("a"), DataType::Int64, None));
let predicate = bin(
bin(
bin(cast_a(), Operator::Gt, lit(5i64)),
Operator::And,
bin(cast_a(), Operator::Lt, lit(9i64)),
),
Operator::Or,
bin(a, Operator::Lt, lit(2i32)),
);
assert!(table.lowers_to_nothing(&predicate));
}
#[test]
fn boolean_constant_is_cast_but_quoted() {
let table = Fixture::new(&[("flag", DataType::Boolean, 1)]);
let predicate = bin(table.column("flag"), Operator::Eq, lit(true));
assert_eq!(
table.sql(&predicate),
"((col_1_stats.data_file_id IS NULL OR \
((col_1_stats.value_count IS NULL OR col_1_stats.value_count > 0) AND \
(col_1_stats.min_value IS NULL OR col_1_stats.max_value IS NULL OR \
'true' BETWEEN TRY_CAST(col_1_stats.min_value AS BOOLEAN) AND \
TRY_CAST(col_1_stats.max_value AS BOOLEAN))))) IS NOT FALSE"
);
}
#[test]
fn decimal_constant_is_cast_and_unquoted() {
let table = Fixture::new(&[("d", DataType::Decimal128(5, 2), 1)]);
let predicate = bin(
table.column("d"),
Operator::Gt,
lit(ScalarValue::Decimal128(Some(525), 5, 2)),
);
assert_eq!(
table.sql(&predicate),
"((col_1_stats.data_file_id IS NULL OR \
((col_1_stats.value_count IS NULL OR col_1_stats.value_count > 0) AND \
(col_1_stats.max_value IS NULL OR \
TRY_CAST(col_1_stats.max_value AS DECIMAL(5,2)) > 5.25)))) IS NOT FALSE"
);
}
#[test]
fn date_constant_is_cast_and_quoted() {
let table = Fixture::new(&[("d", DataType::Date32, 1)]);
let predicate = bin(
table.column("d"),
Operator::GtEq,
lit(ScalarValue::Date32(Some(0))),
);
assert_eq!(
table.sql(&predicate),
"((col_1_stats.data_file_id IS NULL OR \
((col_1_stats.value_count IS NULL OR col_1_stats.value_count > 0) AND \
(col_1_stats.max_value IS NULL OR \
TRY_CAST(col_1_stats.max_value AS DATE) >= '1970-01-01')))) IS NOT FALSE"
);
}
#[test]
fn timestamp_constant_is_cast_and_quoted() {
let table = Fixture::new(&[("t", DataType::Timestamp(TimeUnit::Microsecond, None), 1)]);
let value = ScalarValue::TimestampMicrosecond(Some(0), None);
let encoded = stats_encode::encode_scalar(&value).expect("encodable");
let predicate = bin(table.column("t"), Operator::Lt, lit(value));
assert_eq!(
table.sql(&predicate),
format!(
"((col_1_stats.data_file_id IS NULL OR \
((col_1_stats.value_count IS NULL OR col_1_stats.value_count > 0) AND \
(col_1_stats.min_value IS NULL OR \
TRY_CAST(col_1_stats.min_value AS TIMESTAMP) < '{encoded}')))) IS NOT FALSE"
)
);
}
#[test]
fn negative_constant_renders_with_its_sign() {
let table = ints();
let predicate = bin(table.column("a"), Operator::Gt, lit(-5i32));
assert!(
table
.sql(&predicate)
.contains("TRY_CAST(col_1_stats.max_value AS INTEGER) > -5"),
"{}",
table.sql(&predicate)
);
}
#[test]
fn nested_or_inside_and_inside_or_keeps_its_brackets() {
let table = ints();
let a = table.column("a");
let eq = |value: i32| bin(Arc::clone(&a), Operator::Eq, lit(value));
let predicate = bin(
bin(
bin(eq(1), Operator::Or, eq(2)),
Operator::And,
bin(eq(3), Operator::Or, eq(4)),
),
Operator::Or,
eq(5),
);
let between = |value: i32| {
format!(
"{value} BETWEEN TRY_CAST(col_1_stats.min_value AS INTEGER) AND \
TRY_CAST(col_1_stats.max_value AS INTEGER)"
)
};
assert_eq!(
table.sql(&predicate),
format!(
"((col_1_stats.data_file_id IS NULL OR \
((col_1_stats.value_count IS NULL OR col_1_stats.value_count > 0) AND \
(col_1_stats.min_value IS NULL OR col_1_stats.max_value IS NULL OR \
((({}) OR ({})) AND (({}) OR ({}))) OR ({}))))) IS NOT FALSE",
between(1),
between(2),
between(3),
between(4),
between(5),
)
);
}
#[test]
fn float_eq_is_not_gated() {
let table = Fixture::new(&[("f", DataType::Float64, 3)]);
let predicate = bin(table.column("f"), Operator::Eq, lit(1.5f64));
assert_eq!(
table.sql(&predicate),
"((col_3_stats.data_file_id IS NULL OR \
((col_3_stats.value_count IS NULL OR col_3_stats.value_count > 0) AND \
(col_3_stats.min_value IS NULL OR col_3_stats.max_value IS NULL OR \
1.5 BETWEEN TRY_CAST(col_3_stats.min_value AS DOUBLE) AND \
TRY_CAST(col_3_stats.max_value AS DOUBLE))))) IS NOT FALSE"
);
assert_eq!(
table.cte_stats(&predicate),
vec!["min_value", "max_value", "value_count"],
"an ungated equality must not drag contains_nan into the CTE"
);
}
#[test]
fn float_not_eq_is_still_gated() {
let table = Fixture::new(&[("f", DataType::Float64, 3)]);
let predicate = bin(table.column("f"), Operator::NotEq, lit(1.5f64));
assert_eq!(
table.sql(&predicate),
"((col_3_stats.data_file_id IS NULL OR \
((col_3_stats.value_count IS NULL OR col_3_stats.value_count > 0) AND \
(col_3_stats.min_value IS NULL OR col_3_stats.max_value IS NULL OR \
col_3_stats.contains_nan IS NULL OR \
(col_3_stats.contains_nan IS NULL OR col_3_stats.contains_nan <> false) OR \
(NOT (TRY_CAST(col_3_stats.min_value AS DOUBLE) = 1.5 AND \
TRY_CAST(col_3_stats.max_value AS DOUBLE) = 1.5)))))) IS NOT FALSE"
);
assert_eq!(
table.cte_stats(&predicate),
vec!["min_value", "max_value", "value_count", "contains_nan"]
);
}
#[test]
fn float_in_list_is_not_gated() {
let table = Fixture::new(&[("f", DataType::Float64, 3)]);
let predicate = in_list(
table.column("f"),
vec![lit(1.0f64), lit(2.0f64)],
&false,
&table.schema,
)
.expect("in list");
let sql = table.sql(&predicate);
assert!(!sql.contains("contains_nan"), "{sql}");
assert_eq!(
sql,
"((col_3_stats.data_file_id IS NULL OR \
((col_3_stats.value_count IS NULL OR col_3_stats.value_count > 0) AND \
(col_3_stats.min_value IS NULL OR col_3_stats.max_value IS NULL OR \
(1.0 BETWEEN TRY_CAST(col_3_stats.min_value AS DOUBLE) AND \
TRY_CAST(col_3_stats.max_value AS DOUBLE)) OR \
(2.0 BETWEEN TRY_CAST(col_3_stats.min_value AS DOUBLE) AND \
TRY_CAST(col_3_stats.max_value AS DOUBLE)))))) IS NOT FALSE"
);
assert_eq!(
table.cte_stats(&predicate),
vec!["min_value", "max_value", "value_count"]
);
}
#[test]
fn float_column_casts_to_the_constant_type_and_is_still_gated() {
let table = Fixture::new(&[("f", DataType::Float64, 3)]);
let predicate = bin(table.column("f"), Operator::Gt, lit(5i32));
let sql = table.sql(&predicate);
assert!(
sql.contains("TRY_CAST(col_3_stats.max_value AS INTEGER) > 5"),
"{sql}"
);
assert!(sql.contains("contains_nan <> false"), "{sql}");
}
#[test]
fn integer_column_with_a_float_constant_is_not_gated() {
let table = ints();
let predicate = bin(table.column("a"), Operator::Gt, lit(5.0f64));
let sql = table.sql(&predicate);
assert!(!sql.contains("contains_nan"), "{sql}");
assert!(
sql.contains("TRY_CAST(col_1_stats.max_value AS DOUBLE) > 5.0"),
"{sql}"
);
}
#[test]
fn a_dialect_declining_the_float_cast_drops_the_column_gate_and_all() {
struct NoDouble;
impl StatsSqlDialect for NoDouble {
fn try_cast(
&self,
expr: &str,
literal: &StatsLiteral,
data_type: &DataType,
) -> Option<String> {
if matches!(data_type, DataType::Float64) {
return None;
}
Duck.try_cast(expr, literal, data_type)
}
fn collate_binary(&self, expr: &str) -> String {
Duck.collate_binary(expr)
}
fn boolean_is_not_false(&self, expr: &str) -> String {
Duck.boolean_is_not_false(expr)
}
}
let table = Fixture::new(&[("f", DataType::Float64, 3)]);
let predicate = bin(table.column("f"), Operator::Gt, lit(5.0f64));
assert!(table.render_with(&predicate, &Duck).is_some());
assert!(table.render_with(&predicate, &NoDouble).is_none());
}
#[test]
fn a_dropped_branch_leaves_no_stat_behind() {
let table = ints();
let a = table.column("a");
let cast_a = Arc::new(CastExpr::new(table.column("a"), DataType::Int64, None));
let predicate = bin(
bin(
Arc::new(IsNullExpr::new(Arc::clone(&a))) as Arc<dyn PhysicalExpr>,
Operator::Or,
bin(cast_a, Operator::Gt, lit(5i64)),
),
Operator::And,
bin(a, Operator::Gt, lit(5i32)),
);
let lowered = table.lower(&predicate).expect("lowered");
assert_eq!(
lowered.columns[0]
.referenced_stats
.iter()
.copied()
.collect::<Vec<_>>(),
vec![StatKind::MaxValue]
);
assert!(lowered.columns[0].needs_value_count_guard);
assert_eq!(
table.sql(&predicate),
"((col_1_stats.data_file_id IS NULL OR \
((col_1_stats.value_count IS NULL OR col_1_stats.value_count > 0) AND \
(col_1_stats.max_value IS NULL OR \
TRY_CAST(col_1_stats.max_value AS INTEGER) > 5)))) IS NOT FALSE"
);
}
#[test]
fn a_render_time_drop_keeps_the_stat_it_no_longer_reads() {
let table = ints();
let a = table.column("a");
let predicate = bin(
bin(Arc::clone(&a), Operator::Gt, lit(5i32)),
Operator::And,
bin(a, Operator::Lt, lit(10i64)),
);
let rendered = table
.render_with(&predicate, &NoBigint)
.expect("the INTEGER comparison survives");
assert_eq!(rendered.len(), 1);
assert_eq!(
rendered[0].stats,
vec!["min_value", "max_value", "value_count"]
);
assert_eq!(
rendered[0].condition,
"((col_1_stats.data_file_id IS NULL OR \
((col_1_stats.value_count IS NULL OR col_1_stats.value_count > 0) AND \
(col_1_stats.min_value IS NULL OR col_1_stats.max_value IS NULL OR \
(TRY_CAST(col_1_stats.max_value AS INTEGER) > 5))))) IS NOT FALSE"
);
}
#[test]
fn float16_constant_pushes_down_nothing() {
type F16 = <arrow::datatypes::Float16Type as ArrowPrimitiveType>::Native;
let table = Fixture::new(&[("h", DataType::Float16, 1)]);
let predicate = bin(
table.column("h"),
Operator::Gt,
lit(ScalarValue::Float16(Some(F16::from_f32(1.5)))),
);
assert!(table.lowers_to_nothing(&predicate));
}
fn not3(value: Option<bool>) -> Option<bool> {
value.map(|inner| !inner)
}
fn float_stats_states() -> Vec<(Option<bool>, Option<f64>, Option<f64>)> {
let mut states = Vec::new();
for contains_nan in [None, Some(true), Some(false)] {
for min_value in [None, Some(1.0_f64), Some(5.0_f64)] {
for max_value in [None, Some(5.0_f64), Some(9.0_f64)] {
states.push((contains_nan, min_value, max_value));
}
}
}
states
}
fn gate(contains_nan: Option<bool>) -> Option<bool> {
or3(Some(contains_nan.is_none()), contains_nan)
}
#[test]
fn float_gate_admits_exactly_official_set_for_not_eq_too() {
let constant = 5.0_f64;
for (contains_nan, min_value, max_value) in float_stats_states() {
let body = not3(and3(
min_value.map(|min| min == constant),
max_value.map(|max| max == constant),
));
let null_checks = or3(
or3(Some(min_value.is_none()), Some(max_value.is_none())),
Some(contains_nan.is_none()),
);
let official = or3(null_checks, or3(body, contains_nan));
let ours = or3(null_checks, or3(gate(contains_nan), body));
assert_eq!(
official == Some(true),
ours == Some(true),
"diverged for contains_nan={contains_nan:?} min={min_value:?} max={max_value:?}"
);
assert_ne!(ours, None);
assert_ne!(official, None);
}
}
#[test]
fn float_gate_is_strictly_more_conservative_for_lt_and_lt_eq() {
let constant = 5.0_f64;
let mut strictly_more = false;
for (contains_nan, min_value, _) in float_stats_states() {
for strict in [true, false] {
let body = min_value.map(|min| {
if strict {
min < constant
} else {
min <= constant
}
});
let official = or3(Some(min_value.is_none()), body);
let ours = or3(
or3(Some(min_value.is_none()), Some(contains_nan.is_none())),
or3(gate(contains_nan), body),
);
assert!(
official != Some(true) || ours == Some(true),
"the gate must never prune what official keeps: \
contains_nan={contains_nan:?} min={min_value:?} strict={strict}"
);
if ours == Some(true) && official != Some(true) {
strictly_more = true;
}
}
}
assert!(
strictly_more,
"the gate should keep at least one file official prunes"
);
}
#[test]
fn float_gate_applies_to_exactly_the_bound_reading_operators() {
let table = Fixture::new(&[("f", DataType::Float64, 3)]);
for (op, gated) in [
(Operator::Eq, false),
(Operator::NotEq, true),
(Operator::Lt, true),
(Operator::LtEq, true),
(Operator::Gt, true),
(Operator::GtEq, true),
] {
let predicate = bin(table.column("f"), op, lit(1.5f64));
let sql = table.sql(&predicate);
assert_eq!(
sql.contains("contains_nan"),
gated,
"wrong gate decision for {op:?}: {sql}"
);
assert_eq!(
table.cte_stats(&predicate).contains(&"contains_nan"),
gated,
"wrong CTE stats for {op:?}"
);
}
let predicate = in_list(
table.column("f"),
vec![lit(1.0f64), lit(2.0f64)],
&false,
&table.schema,
)
.expect("in list");
assert!(!table.sql(&predicate).contains("contains_nan"));
let predicate = Arc::new(IsNullExpr::new(table.column("f"))) as Arc<dyn PhysicalExpr>;
assert!(!table.sql(&predicate).contains("contains_nan"));
let predicate = Arc::new(IsNotNullExpr::new(table.column("f"))) as Arc<dyn PhysicalExpr>;
assert!(!table.sql(&predicate).contains("contains_nan"));
}
#[test]
fn float_eq_or_lt_in_one_conjunct_is_still_gated() {
let table = Fixture::new(&[("f", DataType::Float64, 3)]);
let f = table.column("f");
let predicate = bin(
bin(Arc::clone(&f), Operator::Eq, lit(1.5f64)),
Operator::Or,
bin(f, Operator::Lt, lit(3.0f64)),
);
assert_eq!(
table.sql(&predicate),
"((col_3_stats.data_file_id IS NULL OR \
((col_3_stats.value_count IS NULL OR col_3_stats.value_count > 0) AND \
(col_3_stats.min_value IS NULL OR col_3_stats.max_value IS NULL OR \
col_3_stats.contains_nan IS NULL OR \
(col_3_stats.contains_nan IS NULL OR col_3_stats.contains_nan <> false) OR \
((1.5 BETWEEN TRY_CAST(col_3_stats.min_value AS DOUBLE) AND \
TRY_CAST(col_3_stats.max_value AS DOUBLE)) OR \
(TRY_CAST(col_3_stats.min_value AS DOUBLE) < 3.0)))))) IS NOT FALSE"
);
assert_eq!(
table.cte_stats(&predicate),
vec!["min_value", "max_value", "value_count", "contains_nan"]
);
let f = table.column("f");
let predicate = bin(
bin(Arc::clone(&f), Operator::Eq, lit(1.5f64)),
Operator::And,
bin(f, Operator::Lt, lit(3.0f64)),
);
assert!(table.sql(&predicate).contains("contains_nan <> false"));
}
#[test]
fn ungated_equality_admits_exactly_official_set_and_recovers_pruning() {
let constant = 5.0_f64;
let mut recovered_unknown_nan = 0;
let mut recovered_known_nan = 0;
for contains_nan in [None, Some(true), Some(false)] {
for (min_value, max_value) in [
(None, None),
(None, Some(9.0_f64)),
(Some(1.0_f64), None),
(Some(1.0_f64), Some(9.0_f64)), (Some(1.0_f64), Some(3.0_f64)), (Some(7.0_f64), Some(9.0_f64)), (Some(5.0_f64), Some(5.0_f64)), ] {
let between = and3(
min_value.map(|min| min <= constant),
max_value.map(|max| max >= constant),
);
let null_checks = or3(Some(min_value.is_none()), Some(max_value.is_none()));
let official = or3(null_checks, between);
let ours = or3(null_checks, between);
let old_gated = or3(
or3(null_checks, Some(contains_nan.is_none())),
or3(gate(contains_nan), between),
);
assert_ne!(ours, None);
assert_eq!(
official == Some(true),
ours != Some(false),
"diverged from official for contains_nan={contains_nan:?} \
min={min_value:?} max={max_value:?}"
);
assert!(
ours != Some(true) || old_gated == Some(true),
"ungating must not admit anything the gate rejected"
);
if old_gated == Some(true) && ours == Some(false) {
match contains_nan {
None => recovered_unknown_nan += 1,
Some(true) => recovered_known_nan += 1,
Some(false) => {
panic!("contains_nan = false never gated, so nothing to recover")
},
}
}
}
}
assert_eq!(
recovered_unknown_nan, 2,
"a file with contains_nan NULL whose bounds exclude the constant \
must now be pruned"
);
assert_eq!(
recovered_known_nan, 2,
"a file known to contain a NaN is prunable too: a NaN row cannot \
satisfy an equality"
);
for contains_nan in [None, Some(true), Some(false)] {
for (min_value, max_value) in
[(Some(1.0_f64), Some(3.0_f64)), (Some(1.0_f64), Some(9.0_f64))]
{
let null_checks = or3(Some(min_value.is_none()), Some(max_value.is_none()));
let between =
|c: f64| and3(min_value.map(|min| min <= c), max_value.map(|max| max >= c));
let body = or3(between(5.0), between(8.0));
let official = or3(null_checks, body);
let ours = or3(null_checks, body);
let old_gated = or3(
or3(null_checks, Some(contains_nan.is_none())),
or3(gate(contains_nan), body),
);
assert_eq!(official == Some(true), ours != Some(false));
assert!(ours != Some(true) || old_gated == Some(true));
}
}
let null_checks = or3(Some(false), Some(false));
let body = or3(
and3(Some(1.0 <= 5.0), Some(3.0 >= 5.0)),
and3(Some(1.0 <= 8.0), Some(3.0 >= 8.0)),
);
assert_eq!(or3(null_checks, body), Some(false), "IN now prunes it");
assert_eq!(
or3(or3(null_checks, Some(true)), or3(gate(None), body)),
Some(true),
"the gated form used to keep it"
);
}
#[test]
fn bare_boolean_column_predicate_pushes_down_nothing() {
let table = Fixture::new(&[("flag", DataType::Boolean, 1)]);
let predicate = table.column("flag");
assert!(table.lowers_to_nothing(&predicate));
}
#[test]
fn each_float_conjunct_carries_its_own_gate() {
let table = Fixture::new(&[("f", DataType::Float64, 3)]);
let f = table.column("f");
let predicate = bin(
bin(Arc::clone(&f), Operator::Gt, lit(5.0f64)),
Operator::And,
bin(f, Operator::Lt, lit(10.0f64)),
);
assert_eq!(
table.sql(&predicate),
"((col_3_stats.data_file_id IS NULL OR \
((col_3_stats.value_count IS NULL OR col_3_stats.value_count > 0) AND \
(col_3_stats.min_value IS NULL OR col_3_stats.max_value IS NULL OR \
col_3_stats.contains_nan IS NULL OR \
((col_3_stats.contains_nan IS NULL OR col_3_stats.contains_nan <> false) OR \
(TRY_CAST(col_3_stats.max_value AS DOUBLE) > 5.0)) AND \
((col_3_stats.contains_nan IS NULL OR col_3_stats.contains_nan <> false) OR \
(TRY_CAST(col_3_stats.min_value AS DOUBLE) < 10.0)))))) IS NOT FALSE"
);
assert_eq!(
table.cte_stats(&predicate),
vec!["min_value", "max_value", "value_count", "contains_nan"]
);
}
#[test]
fn keep_when_unknown_wraps_outside_the_no_stats_row_guard() {
let table = ints();
let predicate = bin(table.column("a"), Operator::Gt, lit(5i32));
let sql = table.sql(&predicate);
assert_eq!(
sql,
"((col_1_stats.data_file_id IS NULL OR \
((col_1_stats.value_count IS NULL OR col_1_stats.value_count > 0) AND \
(col_1_stats.max_value IS NULL OR \
TRY_CAST(col_1_stats.max_value AS INTEGER) > 5)))) IS NOT FALSE"
);
assert!(
sql.starts_with("((col_1_stats.data_file_id IS NULL OR "),
"{sql}"
);
assert!(sql.ends_with(") IS NOT FALSE"), "{sql}");
assert!(
!sql.contains("IS NOT FALSE)"),
"the wrapper must be outermost: {sql}"
);
assert_eq!(sql.matches("IS NOT FALSE").count(), 1, "{sql}");
}
#[test]
fn keep_when_unknown_wraps_every_column_exactly_once() {
let table = ints();
let predicate = bin(
bin(table.column("a"), Operator::Gt, lit(5i32)),
Operator::And,
bin(table.column("b"), Operator::Lt, lit(3i32)),
);
let rendered = table.render(&predicate);
assert_eq!(rendered.len(), 2);
for filter in &rendered {
assert_eq!(
filter.condition.matches("IS NOT FALSE").count(),
1,
"{}",
filter.condition
);
assert!(filter.condition.ends_with(") IS NOT FALSE"));
}
}
#[test]
fn keep_when_unknown_is_honoured_when_a_dialect_overrides_it() {
struct Coalescing;
impl StatsSqlDialect for Coalescing {
fn try_cast(
&self,
expr: &str,
literal: &StatsLiteral,
data_type: &DataType,
) -> Option<String> {
Duck.try_cast(expr, literal, data_type)
}
fn collate_binary(&self, expr: &str) -> String {
Duck.collate_binary(expr)
}
fn boolean_is_not_false(&self, expr: &str) -> String {
Duck.boolean_is_not_false(expr)
}
fn keep_when_unknown(&self, condition: &str) -> String {
format!("COALESCE({condition}, TRUE)")
}
}
let table = ints();
let predicate = bin(table.column("a"), Operator::Gt, lit(5i32));
let rendered = table
.render_with(&predicate, &Coalescing)
.expect("rendered");
assert_eq!(rendered.len(), 1);
assert_eq!(
rendered[0].condition,
"COALESCE((col_1_stats.data_file_id IS NULL OR \
((col_1_stats.value_count IS NULL OR col_1_stats.value_count > 0) AND \
(col_1_stats.max_value IS NULL OR \
TRY_CAST(col_1_stats.max_value AS INTEGER) > 5))), TRUE)"
);
assert!(!rendered[0].condition.contains("IS NOT FALSE"));
}
type StoredBound = Option<Option<f64>>;
#[test]
fn keep_when_unknown_rescues_a_malformed_stat() {
let constant = 5.0_f64;
let mut rescued = 0;
for stored in [
None, Some(None), Some(Some(1.0_f64)), Some(Some(9.0_f64)), ] as [StoredBound; 4]
{
let is_null = Some(stored.is_none());
let compared = stored.flatten().map(|value| value > constant);
let inner = or3(is_null, compared);
let official_admits = inner == Some(true);
let ours_admits = inner != Some(false);
assert!(
!official_admits || ours_admits,
"the wrapper must never prune what official keeps: {stored:?}"
);
if ours_admits && !official_admits {
assert_eq!(stored, Some(None), "only a malformed stat should differ");
rescued += 1;
}
}
assert_eq!(
rescued, 1,
"the malformed stat must be the one file rescued"
);
let stored: StoredBound = Some(Some(1.0_f64));
let inner = or3(
Some(stored.is_none()),
stored.flatten().map(|value| value > constant),
);
assert_eq!(inner, Some(false), "a well-formed miss must still prune");
}
#[test]
fn try_cast_receives_the_literal_the_stat_is_compared_against() {
struct LiteralAware;
impl StatsSqlDialect for LiteralAware {
fn try_cast(
&self,
expr: &str,
literal: &StatsLiteral,
data_type: &DataType,
) -> Option<String> {
if literal.text().starts_with('-') {
return None;
}
Some(format!(
"TRY_CAST({expr} AS {}) /* {} */",
duckdb_type_name(data_type)?,
literal.text()
))
}
fn collate_binary(&self, expr: &str) -> String {
Duck.collate_binary(expr)
}
fn boolean_is_not_false(&self, expr: &str) -> String {
Duck.boolean_is_not_false(expr)
}
}
let table = ints();
let predicate = bin(table.column("a"), Operator::Gt, lit(5i32));
let rendered = table
.render_with(&predicate, &LiteralAware)
.expect("rendered");
assert_eq!(
rendered[0].condition,
"((col_1_stats.data_file_id IS NULL OR \
((col_1_stats.value_count IS NULL OR col_1_stats.value_count > 0) AND \
(col_1_stats.max_value IS NULL OR \
TRY_CAST(col_1_stats.max_value AS INTEGER) /* 5 */ > 5)))) IS NOT FALSE"
);
let predicate = bin(table.column("a"), Operator::Eq, lit(7i32));
let rendered = table
.render_with(&predicate, &LiteralAware)
.expect("rendered");
assert_eq!(rendered[0].condition.matches("/* 7 */").count(), 2);
let a = table.column("a");
let predicate = bin(
bin(Arc::clone(&a), Operator::Gt, lit(5i32)),
Operator::And,
bin(Arc::clone(&a), Operator::Lt, lit(-3i32)),
);
let rendered = table
.render_with(&predicate, &LiteralAware)
.expect("the positive constant still renders");
assert_eq!(
rendered[0].condition,
"((col_1_stats.data_file_id IS NULL OR \
((col_1_stats.value_count IS NULL OR col_1_stats.value_count > 0) AND \
(col_1_stats.min_value IS NULL OR col_1_stats.max_value IS NULL OR \
(TRY_CAST(col_1_stats.max_value AS INTEGER) /* 5 */ > 5))))) IS NOT FALSE"
);
let predicate = bin(a, Operator::Lt, lit(-3i32));
assert!(table.render_with(&predicate, &LiteralAware).is_none());
}
#[test]
fn quote_literal_default_doubles_embedded_quotes() {
let table = Fixture::new(&[("s", DataType::Utf8, 1)]);
let predicate = bin(table.column("s"), Operator::Eq, lit("it's 'quoted'"));
assert_eq!(
table.sql(&predicate),
"((col_1_stats.data_file_id IS NULL OR \
((col_1_stats.value_count IS NULL OR col_1_stats.value_count > 0) AND \
(col_1_stats.min_value IS NULL OR col_1_stats.max_value IS NULL OR \
'it''s ''quoted''' BETWEEN col_1_stats.min_value AND \
col_1_stats.max_value)))) IS NOT FALSE"
);
}
#[test]
fn quote_literal_override_is_honoured() {
struct BackslashEscaping;
impl StatsSqlDialect for BackslashEscaping {
fn try_cast(
&self,
expr: &str,
literal: &StatsLiteral,
data_type: &DataType,
) -> Option<String> {
Duck.try_cast(expr, literal, data_type)
}
fn collate_binary(&self, expr: &str) -> String {
Duck.collate_binary(expr)
}
fn boolean_is_not_false(&self, expr: &str) -> String {
Duck.boolean_is_not_false(expr)
}
fn quote_literal(&self, text: &str) -> String {
format!("'{}'", text.replace('\\', "\\\\").replace('\'', "''"))
}
}
let table = Fixture::new(&[("s", DataType::Utf8, 1)]);
let predicate = bin(table.column("s"), Operator::Eq, lit("a\\b'c"));
assert_eq!(
table.sql(&predicate),
"((col_1_stats.data_file_id IS NULL OR \
((col_1_stats.value_count IS NULL OR col_1_stats.value_count > 0) AND \
(col_1_stats.min_value IS NULL OR col_1_stats.max_value IS NULL OR \
'a\\b''c' BETWEEN col_1_stats.min_value AND \
col_1_stats.max_value)))) IS NOT FALSE"
);
let rendered = table
.render_with(&predicate, &BackslashEscaping)
.expect("rendered");
assert_eq!(
rendered[0].condition,
"((col_1_stats.data_file_id IS NULL OR \
((col_1_stats.value_count IS NULL OR col_1_stats.value_count > 0) AND \
(col_1_stats.min_value IS NULL OR col_1_stats.max_value IS NULL OR \
'a\\\\b''c' BETWEEN col_1_stats.min_value AND \
col_1_stats.max_value)))) IS NOT FALSE"
);
}
#[test]
fn quote_literal_is_not_used_for_unquoted_numerics() {
struct Marking;
impl StatsSqlDialect for Marking {
fn try_cast(
&self,
expr: &str,
literal: &StatsLiteral,
data_type: &DataType,
) -> Option<String> {
Duck.try_cast(expr, literal, data_type)
}
fn collate_binary(&self, expr: &str) -> String {
Duck.collate_binary(expr)
}
fn boolean_is_not_false(&self, expr: &str) -> String {
Duck.boolean_is_not_false(expr)
}
fn quote_literal(&self, text: &str) -> String {
format!("Q<{text}>")
}
}
let strings = Fixture::new(&[("s", DataType::Utf8, 1)]);
let predicate = bin(strings.column("s"), Operator::Eq, lit("x"));
let rendered = strings.render_with(&predicate, &Marking).expect("rendered");
assert!(
rendered[0].condition.contains("Q<x> BETWEEN"),
"{}",
rendered[0].condition
);
let ints = ints();
let predicate = bin(ints.column("a"), Operator::Gt, lit(5i32));
let rendered = ints.render_with(&predicate, &Marking).expect("rendered");
assert_eq!(
rendered[0].condition,
"((col_1_stats.data_file_id IS NULL OR \
((col_1_stats.value_count IS NULL OR col_1_stats.value_count > 0) AND \
(col_1_stats.max_value IS NULL OR \
TRY_CAST(col_1_stats.max_value AS INTEGER) > 5)))) IS NOT FALSE"
);
let flags = Fixture::new(&[("flag", DataType::Boolean, 1)]);
let predicate = bin(flags.column("flag"), Operator::Eq, lit(true));
let rendered = flags.render_with(&predicate, &Marking).expect("rendered");
assert!(
rendered[0].condition.contains("Q<true> BETWEEN"),
"{}",
rendered[0].condition
);
}
}