use std::cmp::Ordering;
use arrow_array::{Array, RecordBatch};
use arrow_schema::{DataType, Schema};
use crate::value::{compare_literal, literal_fits, value_reader, Kind, Value, ValueFn};
use crate::DataError;
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum Scalar {
Int(i64),
UInt(u64),
Float(f64),
Bool(bool),
Str(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum CompareOp {
Eq,
Ne,
Lt,
Le,
Gt,
Ge,
}
impl CompareOp {
fn holds(self, order: Ordering) -> bool {
match self {
Self::Eq => order.is_eq(),
Self::Ne => order.is_ne(),
Self::Lt => order.is_lt(),
Self::Le => order.is_le(),
Self::Gt => order.is_gt(),
Self::Ge => order.is_ge(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum Filter {
Compare {
column: String,
op: CompareOp,
value: Scalar,
},
IsNull(String),
Contains {
column: String,
text: String,
},
All(Vec<Filter>),
Any(Vec<Filter>),
Not(Box<Filter>),
}
impl Filter {
pub fn compare(column: impl Into<String>, op: CompareOp, value: Scalar) -> Self {
Self::Compare {
column: column.into(),
op,
value,
}
}
pub fn is_null(column: impl Into<String>) -> Self {
Self::IsNull(column.into())
}
pub fn is_not_null(column: impl Into<String>) -> Self {
Self::not(Self::is_null(column))
}
pub fn contains(column: impl Into<String>, text: impl Into<String>) -> Self {
Self::Contains {
column: column.into(),
text: text.into(),
}
}
pub fn all(filters: impl IntoIterator<Item = Filter>) -> Self {
Self::All(filters.into_iter().collect())
}
pub fn any(filters: impl IntoIterator<Item = Filter>) -> Self {
Self::Any(filters.into_iter().collect())
}
#[expect(
clippy::should_implement_trait,
reason = "a constructor, not negation of a value"
)]
pub fn not(filter: Filter) -> Self {
Self::Not(Box::new(filter))
}
}
#[derive(Debug, Clone)]
pub(crate) enum Predicate {
Compare {
column: usize,
kind: Kind,
read: ValueFn,
op: CompareOp,
value: Scalar,
},
IsNull {
column: usize,
read: Option<ValueFn>,
},
Contains {
column: usize,
read: ValueFn,
text: String,
},
All(Vec<Predicate>),
Any(Vec<Predicate>),
Not(Box<Predicate>),
}
pub(crate) fn resolve(schema: &Schema, name: &str) -> Result<(usize, Kind, ValueFn), DataError> {
let (index, field) = schema
.column_with_name(name)
.ok_or_else(|| DataError::UnknownColumn(name.to_owned()))?;
let (kind, read) = value_reader(field.data_type())
.ok_or_else(|| DataError::UnsupportedColumn(name.to_owned()))?;
Ok((index, kind, read))
}
impl Predicate {
pub(crate) fn compile(filter: &Filter, schema: &Schema) -> Result<Self, DataError> {
Ok(match filter {
Filter::Compare { column, op, value } => {
let (index, kind, read) = resolve(schema, column)?;
if !literal_fits(kind, value) {
return Err(DataError::LiteralMismatch(column.clone()));
}
Self::Compare {
column: index,
kind,
read,
op: *op,
value: value.clone(),
}
}
Filter::IsNull(column) => {
let (index, field) = schema
.column_with_name(column)
.ok_or_else(|| DataError::UnknownColumn(column.clone()))?;
Self::IsNull {
column: index,
read: value_reader(field.data_type()).map(|(_, read)| read),
}
}
Filter::Contains { column, text } => {
let (index, kind, read) = resolve(schema, column)?;
if kind != Kind::Str {
return Err(DataError::LiteralMismatch(column.clone()));
}
Self::Contains {
column: index,
read,
text: text.clone(),
}
}
Filter::All(filters) => Self::All(compile_all(filters, schema)?),
Filter::Any(filters) => Self::Any(compile_all(filters, schema)?),
Filter::Not(filter) => Self::Not(Box::new(Self::compile(filter, schema)?)),
})
}
pub(crate) fn eval(&self, batch: &RecordBatch, row: usize) -> Option<bool> {
let value = |column: usize, read: ValueFn| read(batch.column(column).as_ref(), row);
match self {
Self::Compare {
column,
kind,
read,
op,
value: literal,
} => compare_literal(*kind, value(*column, *read), literal).map(|o| op.holds(o)),
Self::IsNull { column, read } => Some(match read {
Some(read) => value(*column, *read) == Value::Null,
None => {
let array = batch.column(*column);
*array.data_type() == DataType::Null || array.is_null(row)
}
}),
Self::Contains { column, read, text } => match value(*column, *read) {
Value::Str(s) => Some(s.contains(text.as_str())),
_ => None,
},
Self::All(predicates) => kleene_all(predicates, batch, row),
Self::Any(predicates) => kleene_any(predicates, batch, row),
Self::Not(predicate) => predicate.eval(batch, row).map(|b| !b),
}
}
}
fn compile_all(filters: &[Filter], schema: &Schema) -> Result<Vec<Predicate>, DataError> {
filters
.iter()
.map(|f| Predicate::compile(f, schema))
.collect()
}
fn kleene_all(predicates: &[Predicate], batch: &RecordBatch, row: usize) -> Option<bool> {
let mut unknown = false;
for predicate in predicates {
match predicate.eval(batch, row) {
Some(false) => return Some(false),
None => unknown = true,
Some(true) => {}
}
}
(!unknown).then_some(true)
}
fn kleene_any(predicates: &[Predicate], batch: &RecordBatch, row: usize) -> Option<bool> {
let mut unknown = false;
for predicate in predicates {
match predicate.eval(batch, row) {
Some(true) => return Some(true),
None => unknown = true,
Some(false) => {}
}
}
(!unknown).then_some(false)
}