use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
use std::borrow::Cow;
use tracing::debug;
pub type ValueList = Vec<FilterValue>;
pub type SmallValueList = SmallVec<[FilterValue; 8]>;
pub type LargeValueList = SmallVec<[FilterValue; 32]>;
pub type FieldName = Cow<'static, str>;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum FilterValue {
Null,
Bool(bool),
Int(i64),
Float(f64),
String(String),
Json(serde_json::Value),
List(Vec<FilterValue>),
}
impl FilterValue {
pub fn is_null(&self) -> bool {
matches!(self, Self::Null)
}
}
impl From<bool> for FilterValue {
fn from(v: bool) -> Self {
Self::Bool(v)
}
}
impl From<i32> for FilterValue {
fn from(v: i32) -> Self {
Self::Int(v as i64)
}
}
impl From<i64> for FilterValue {
fn from(v: i64) -> Self {
Self::Int(v)
}
}
impl From<f64> for FilterValue {
fn from(v: f64) -> Self {
Self::Float(v)
}
}
impl From<String> for FilterValue {
fn from(v: String) -> Self {
Self::String(v)
}
}
impl From<&str> for FilterValue {
fn from(v: &str) -> Self {
Self::String(v.to_string())
}
}
impl<T: Into<FilterValue>> From<Vec<T>> for FilterValue {
fn from(v: Vec<T>) -> Self {
Self::List(v.into_iter().map(Into::into).collect())
}
}
impl<T: Into<FilterValue>> From<Option<T>> for FilterValue {
fn from(v: Option<T>) -> Self {
match v {
Some(v) => v.into(),
None => Self::Null,
}
}
}
impl From<i8> for FilterValue {
fn from(v: i8) -> Self {
Self::Int(v as i64)
}
}
impl From<i16> for FilterValue {
fn from(v: i16) -> Self {
Self::Int(v as i64)
}
}
impl From<u8> for FilterValue {
fn from(v: u8) -> Self {
Self::Int(v as i64)
}
}
impl From<u16> for FilterValue {
fn from(v: u16) -> Self {
Self::Int(v as i64)
}
}
impl From<u32> for FilterValue {
fn from(v: u32) -> Self {
Self::Int(v as i64)
}
}
impl From<u64> for FilterValue {
fn from(v: u64) -> Self {
let v = i64::try_from(v).expect(
"u64 value exceeds i64::MAX; cast explicitly to i64 or use FilterValue::String",
);
Self::Int(v)
}
}
impl From<f32> for FilterValue {
fn from(v: f32) -> Self {
Self::Float(f64::from(v))
}
}
impl From<chrono::DateTime<chrono::Utc>> for FilterValue {
fn from(v: chrono::DateTime<chrono::Utc>) -> Self {
Self::String(v.to_rfc3339_opts(chrono::SecondsFormat::Micros, true))
}
}
impl From<chrono::NaiveDateTime> for FilterValue {
fn from(v: chrono::NaiveDateTime) -> Self {
Self::String(v.format("%Y-%m-%dT%H:%M:%S%.6f").to_string())
}
}
impl From<chrono::NaiveDate> for FilterValue {
fn from(v: chrono::NaiveDate) -> Self {
Self::String(v.format("%Y-%m-%d").to_string())
}
}
impl From<chrono::NaiveTime> for FilterValue {
fn from(v: chrono::NaiveTime) -> Self {
Self::String(v.format("%H:%M:%S%.6f").to_string())
}
}
impl From<uuid::Uuid> for FilterValue {
fn from(v: uuid::Uuid) -> Self {
Self::String(v.to_string())
}
}
impl From<rust_decimal::Decimal> for FilterValue {
fn from(v: rust_decimal::Decimal) -> Self {
Self::String(v.to_string())
}
}
impl From<serde_json::Value> for FilterValue {
fn from(v: serde_json::Value) -> Self {
Self::Json(v)
}
}
pub trait ToFilterValue {
fn to_filter_value(&self) -> FilterValue;
}
impl ToFilterValue for i8 {
fn to_filter_value(&self) -> FilterValue {
FilterValue::Int(*self as i64)
}
}
impl ToFilterValue for i16 {
fn to_filter_value(&self) -> FilterValue {
FilterValue::Int(*self as i64)
}
}
impl ToFilterValue for i32 {
fn to_filter_value(&self) -> FilterValue {
FilterValue::Int(*self as i64)
}
}
impl ToFilterValue for i64 {
fn to_filter_value(&self) -> FilterValue {
FilterValue::Int(*self)
}
}
impl ToFilterValue for u8 {
fn to_filter_value(&self) -> FilterValue {
FilterValue::Int(*self as i64)
}
}
impl ToFilterValue for u16 {
fn to_filter_value(&self) -> FilterValue {
FilterValue::Int(*self as i64)
}
}
impl ToFilterValue for u32 {
fn to_filter_value(&self) -> FilterValue {
FilterValue::Int(*self as i64)
}
}
impl ToFilterValue for f32 {
fn to_filter_value(&self) -> FilterValue {
FilterValue::Float(f64::from(*self))
}
}
impl ToFilterValue for f64 {
fn to_filter_value(&self) -> FilterValue {
FilterValue::Float(*self)
}
}
impl ToFilterValue for bool {
fn to_filter_value(&self) -> FilterValue {
FilterValue::Bool(*self)
}
}
impl ToFilterValue for String {
fn to_filter_value(&self) -> FilterValue {
FilterValue::String(self.clone())
}
}
impl ToFilterValue for str {
fn to_filter_value(&self) -> FilterValue {
FilterValue::String(self.to_string())
}
}
impl ToFilterValue for uuid::Uuid {
fn to_filter_value(&self) -> FilterValue {
FilterValue::String(self.to_string())
}
}
impl ToFilterValue for rust_decimal::Decimal {
fn to_filter_value(&self) -> FilterValue {
FilterValue::String(self.to_string())
}
}
impl ToFilterValue for chrono::DateTime<chrono::Utc> {
fn to_filter_value(&self) -> FilterValue {
FilterValue::String(self.to_rfc3339_opts(chrono::SecondsFormat::Micros, true))
}
}
impl ToFilterValue for chrono::NaiveDateTime {
fn to_filter_value(&self) -> FilterValue {
FilterValue::String(self.format("%Y-%m-%dT%H:%M:%S%.6f").to_string())
}
}
impl ToFilterValue for chrono::NaiveDate {
fn to_filter_value(&self) -> FilterValue {
FilterValue::String(self.format("%Y-%m-%d").to_string())
}
}
impl ToFilterValue for chrono::NaiveTime {
fn to_filter_value(&self) -> FilterValue {
FilterValue::String(self.format("%H:%M:%S%.6f").to_string())
}
}
impl ToFilterValue for serde_json::Value {
fn to_filter_value(&self) -> FilterValue {
FilterValue::Json(self.clone())
}
}
impl ToFilterValue for Vec<u8> {
fn to_filter_value(&self) -> FilterValue {
FilterValue::List(self.iter().map(|b| FilterValue::Int(*b as i64)).collect())
}
}
impl<T: ToFilterValue> ToFilterValue for Option<T> {
fn to_filter_value(&self) -> FilterValue {
self.as_ref()
.map(T::to_filter_value)
.unwrap_or(FilterValue::Null)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ScalarFilter<T> {
Equals(T),
Not(Box<T>),
In(Vec<T>),
NotIn(Vec<T>),
Lt(T),
Lte(T),
Gt(T),
Gte(T),
Contains(T),
StartsWith(T),
EndsWith(T),
IsNull,
IsNotNull,
}
impl<T: Into<FilterValue>> ScalarFilter<T> {
pub fn into_filter(self, column: impl Into<FieldName>) -> Filter {
let column = column.into();
match self {
Self::Equals(v) => Filter::Equals(column, v.into()),
Self::Not(v) => Filter::NotEquals(column, (*v).into()),
Self::In(values) => Filter::In(column, values.into_iter().map(Into::into).collect()),
Self::NotIn(values) => {
Filter::NotIn(column, values.into_iter().map(Into::into).collect())
}
Self::Lt(v) => Filter::Lt(column, v.into()),
Self::Lte(v) => Filter::Lte(column, v.into()),
Self::Gt(v) => Filter::Gt(column, v.into()),
Self::Gte(v) => Filter::Gte(column, v.into()),
Self::Contains(v) => Filter::Contains(column, v.into()),
Self::StartsWith(v) => Filter::StartsWith(column, v.into()),
Self::EndsWith(v) => Filter::EndsWith(column, v.into()),
Self::IsNull => Filter::IsNull(column),
Self::IsNotNull => Filter::IsNotNull(column),
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[repr(C)] #[derive(Default)]
pub enum Filter {
#[default]
None,
Equals(FieldName, FilterValue),
NotEquals(FieldName, FilterValue),
Lt(FieldName, FilterValue),
Lte(FieldName, FilterValue),
Gt(FieldName, FilterValue),
Gte(FieldName, FilterValue),
In(FieldName, ValueList),
NotIn(FieldName, ValueList),
Contains(FieldName, FilterValue),
StartsWith(FieldName, FilterValue),
EndsWith(FieldName, FilterValue),
IsNull(FieldName),
IsNotNull(FieldName),
And(Box<[Filter]>),
Or(Box<[Filter]>),
Not(Box<Filter>),
}
impl Filter {
#[inline(always)]
pub fn none() -> Self {
Self::None
}
#[inline(always)]
pub fn is_none(&self) -> bool {
matches!(self, Self::None)
}
#[inline]
pub fn and(filters: impl IntoIterator<Item = Filter>) -> Self {
let filters: Vec<_> = filters.into_iter().filter(|f| !f.is_none()).collect();
let count = filters.len();
let result = match count {
0 => Self::None,
1 => filters.into_iter().next().unwrap(),
_ => Self::And(filters.into_boxed_slice()),
};
debug!(count, "Filter::and() created");
result
}
#[inline(always)]
pub fn and2(a: Filter, b: Filter) -> Self {
match (a.is_none(), b.is_none()) {
(true, true) => Self::None,
(true, false) => b,
(false, true) => a,
(false, false) => Self::And(Box::new([a, b])),
}
}
#[inline]
pub fn or(filters: impl IntoIterator<Item = Filter>) -> Self {
let filters: Vec<_> = filters.into_iter().filter(|f| !f.is_none()).collect();
let count = filters.len();
let result = match count {
0 => Self::None,
1 => filters.into_iter().next().unwrap(),
_ => Self::Or(filters.into_boxed_slice()),
};
debug!(count, "Filter::or() created");
result
}
#[inline(always)]
pub fn or2(a: Filter, b: Filter) -> Self {
match (a.is_none(), b.is_none()) {
(true, true) => Self::None,
(true, false) => b,
(false, true) => a,
(false, false) => Self::Or(Box::new([a, b])),
}
}
#[inline(always)]
pub fn and_n<const N: usize>(filters: [Filter; N]) -> Self {
Self::And(Box::new(filters))
}
#[inline(always)]
pub fn or_n<const N: usize>(filters: [Filter; N]) -> Self {
Self::Or(Box::new(filters))
}
#[inline(always)]
pub fn and3(a: Filter, b: Filter, c: Filter) -> Self {
Self::And(Box::new([a, b, c]))
}
#[inline(always)]
pub fn and4(a: Filter, b: Filter, c: Filter, d: Filter) -> Self {
Self::And(Box::new([a, b, c, d]))
}
#[inline(always)]
pub fn and5(a: Filter, b: Filter, c: Filter, d: Filter, e: Filter) -> Self {
Self::And(Box::new([a, b, c, d, e]))
}
#[inline(always)]
pub fn or3(a: Filter, b: Filter, c: Filter) -> Self {
Self::Or(Box::new([a, b, c]))
}
#[inline(always)]
pub fn or4(a: Filter, b: Filter, c: Filter, d: Filter) -> Self {
Self::Or(Box::new([a, b, c, d]))
}
#[inline(always)]
pub fn or5(a: Filter, b: Filter, c: Filter, d: Filter, e: Filter) -> Self {
Self::Or(Box::new([a, b, c, d, e]))
}
#[inline]
pub fn in_i64(field: impl Into<FieldName>, values: impl IntoIterator<Item = i64>) -> Self {
let list: ValueList = values.into_iter().map(FilterValue::Int).collect();
Self::In(field.into(), list)
}
#[inline]
pub fn in_i32(field: impl Into<FieldName>, values: impl IntoIterator<Item = i32>) -> Self {
let list: ValueList = values
.into_iter()
.map(|v| FilterValue::Int(v as i64))
.collect();
Self::In(field.into(), list)
}
#[inline]
pub fn in_strings(
field: impl Into<FieldName>,
values: impl IntoIterator<Item = String>,
) -> Self {
let list: ValueList = values.into_iter().map(FilterValue::String).collect();
Self::In(field.into(), list)
}
#[inline]
pub fn in_values(field: impl Into<FieldName>, values: ValueList) -> Self {
Self::In(field.into(), values)
}
#[inline]
pub fn in_range(field: impl Into<FieldName>, range: std::ops::Range<i64>) -> Self {
let list: ValueList = range.map(FilterValue::Int).collect();
Self::In(field.into(), list)
}
#[inline(always)]
pub fn in_i64_slice(field: impl Into<FieldName>, values: &[i64]) -> Self {
let mut list = Vec::with_capacity(values.len());
for &v in values {
list.push(FilterValue::Int(v));
}
Self::In(field.into(), list)
}
#[inline(always)]
pub fn in_i32_slice(field: impl Into<FieldName>, values: &[i32]) -> Self {
let mut list = Vec::with_capacity(values.len());
for &v in values {
list.push(FilterValue::Int(v as i64));
}
Self::In(field.into(), list)
}
#[inline(always)]
pub fn in_str_slice(field: impl Into<FieldName>, values: &[&str]) -> Self {
let mut list = Vec::with_capacity(values.len());
for &v in values {
list.push(FilterValue::String(v.to_string()));
}
Self::In(field.into(), list)
}
#[inline]
#[allow(clippy::should_implement_trait)]
pub fn not(filter: Filter) -> Self {
if filter.is_none() {
return Self::None;
}
Self::Not(Box::new(filter))
}
#[inline]
pub fn in_slice<T: Into<FilterValue> + Clone>(
field: impl Into<FieldName>,
values: &[T],
) -> Self {
let list: ValueList = values.iter().map(|v| v.clone().into()).collect();
Self::In(field.into(), list)
}
#[inline]
pub fn not_in_slice<T: Into<FilterValue> + Clone>(
field: impl Into<FieldName>,
values: &[T],
) -> Self {
let list: ValueList = values.iter().map(|v| v.clone().into()).collect();
Self::NotIn(field.into(), list)
}
#[inline]
pub fn in_array<T: Into<FilterValue>, const N: usize>(
field: impl Into<FieldName>,
values: [T; N],
) -> Self {
let list: ValueList = values.into_iter().map(Into::into).collect();
Self::In(field.into(), list)
}
#[inline]
pub fn not_in_array<T: Into<FilterValue>, const N: usize>(
field: impl Into<FieldName>,
values: [T; N],
) -> Self {
let list: ValueList = values.into_iter().map(Into::into).collect();
Self::NotIn(field.into(), list)
}
pub fn and_then(self, other: Filter) -> Self {
if self.is_none() {
return other;
}
if other.is_none() {
return self;
}
match self {
Self::And(filters) => {
let mut vec: Vec<_> = filters.into_vec();
vec.push(other);
Self::And(vec.into_boxed_slice())
}
_ => Self::And(Box::new([self, other])),
}
}
pub fn or_else(self, other: Filter) -> Self {
if self.is_none() {
return other;
}
if other.is_none() {
return self;
}
match self {
Self::Or(filters) => {
let mut vec: Vec<_> = filters.into_vec();
vec.push(other);
Self::Or(vec.into_boxed_slice())
}
_ => Self::Or(Box::new([self, other])),
}
}
pub fn to_sql(
&self,
param_offset: usize,
dialect: &dyn crate::dialect::SqlDialect,
) -> (String, Vec<FilterValue>) {
let mut params = Vec::new();
let sql = self.to_sql_with_params(param_offset, &mut params, dialect);
(sql, params)
}
fn to_sql_with_params(
&self,
mut param_idx: usize,
params: &mut Vec<FilterValue>,
dialect: &dyn crate::dialect::SqlDialect,
) -> String {
match self {
Self::None => "TRUE".to_string(),
Self::Equals(col, val) => {
let c = dialect.quote_ident(col);
if val.is_null() {
format!("{} IS NULL", c)
} else {
params.push(val.clone());
param_idx += params.len();
format!("{} = {}", c, dialect.placeholder(param_idx))
}
}
Self::NotEquals(col, val) => {
let c = dialect.quote_ident(col);
if val.is_null() {
format!("{} IS NOT NULL", c)
} else {
params.push(val.clone());
param_idx += params.len();
format!("{} != {}", c, dialect.placeholder(param_idx))
}
}
Self::Lt(col, val) => {
let c = dialect.quote_ident(col);
params.push(val.clone());
param_idx += params.len();
format!("{} < {}", c, dialect.placeholder(param_idx))
}
Self::Lte(col, val) => {
let c = dialect.quote_ident(col);
params.push(val.clone());
param_idx += params.len();
format!("{} <= {}", c, dialect.placeholder(param_idx))
}
Self::Gt(col, val) => {
let c = dialect.quote_ident(col);
params.push(val.clone());
param_idx += params.len();
format!("{} > {}", c, dialect.placeholder(param_idx))
}
Self::Gte(col, val) => {
let c = dialect.quote_ident(col);
params.push(val.clone());
param_idx += params.len();
format!("{} >= {}", c, dialect.placeholder(param_idx))
}
Self::In(col, values) => {
if values.is_empty() {
return "FALSE".to_string();
}
let c = dialect.quote_ident(col);
let placeholders: Vec<_> = values
.iter()
.map(|v| {
params.push(v.clone());
param_idx += params.len();
dialect.placeholder(param_idx)
})
.collect();
format!("{} IN ({})", c, placeholders.join(", "))
}
Self::NotIn(col, values) => {
if values.is_empty() {
return "TRUE".to_string();
}
let c = dialect.quote_ident(col);
let placeholders: Vec<_> = values
.iter()
.map(|v| {
params.push(v.clone());
param_idx += params.len();
dialect.placeholder(param_idx)
})
.collect();
format!("{} NOT IN ({})", c, placeholders.join(", "))
}
Self::Contains(col, val) => {
let c = dialect.quote_ident(col);
if let FilterValue::String(s) = val {
params.push(FilterValue::String(format!("%{}%", s)));
} else {
params.push(val.clone());
}
param_idx += params.len();
format!("{} LIKE {}", c, dialect.placeholder(param_idx))
}
Self::StartsWith(col, val) => {
let c = dialect.quote_ident(col);
if let FilterValue::String(s) = val {
params.push(FilterValue::String(format!("{}%", s)));
} else {
params.push(val.clone());
}
param_idx += params.len();
format!("{} LIKE {}", c, dialect.placeholder(param_idx))
}
Self::EndsWith(col, val) => {
let c = dialect.quote_ident(col);
if let FilterValue::String(s) = val {
params.push(FilterValue::String(format!("%{}", s)));
} else {
params.push(val.clone());
}
param_idx += params.len();
format!("{} LIKE {}", c, dialect.placeholder(param_idx))
}
Self::IsNull(col) => {
let c = dialect.quote_ident(col);
format!("{} IS NULL", c)
}
Self::IsNotNull(col) => {
let c = dialect.quote_ident(col);
format!("{} IS NOT NULL", c)
}
Self::And(filters) => {
if filters.is_empty() {
return "TRUE".to_string();
}
let parts: Vec<_> = filters
.iter()
.map(|f| f.to_sql_with_params(param_idx + params.len(), params, dialect))
.collect();
format!("({})", parts.join(" AND "))
}
Self::Or(filters) => {
if filters.is_empty() {
return "FALSE".to_string();
}
let parts: Vec<_> = filters
.iter()
.map(|f| f.to_sql_with_params(param_idx + params.len(), params, dialect))
.collect();
format!("({})", parts.join(" OR "))
}
Self::Not(filter) => {
let inner = filter.to_sql_with_params(param_idx, params, dialect);
format!("NOT ({})", inner)
}
}
}
#[inline]
pub fn and_builder(capacity: usize) -> AndFilterBuilder {
AndFilterBuilder::with_capacity(capacity)
}
#[inline]
pub fn or_builder(capacity: usize) -> OrFilterBuilder {
OrFilterBuilder::with_capacity(capacity)
}
#[inline]
pub fn builder() -> FluentFilterBuilder {
FluentFilterBuilder::new()
}
}
#[derive(Debug, Clone)]
pub struct AndFilterBuilder {
filters: Vec<Filter>,
}
impl AndFilterBuilder {
#[inline]
pub fn new() -> Self {
Self {
filters: Vec::new(),
}
}
#[inline]
pub fn with_capacity(capacity: usize) -> Self {
Self {
filters: Vec::with_capacity(capacity),
}
}
#[inline]
pub fn push(mut self, filter: Filter) -> Self {
if !filter.is_none() {
self.filters.push(filter);
}
self
}
#[inline]
pub fn extend(mut self, filters: impl IntoIterator<Item = Filter>) -> Self {
self.filters
.extend(filters.into_iter().filter(|f| !f.is_none()));
self
}
#[inline]
pub fn push_if(self, condition: bool, filter: Filter) -> Self {
if condition { self.push(filter) } else { self }
}
#[inline]
pub fn push_if_some<F>(self, opt: Option<F>) -> Self
where
F: Into<Filter>,
{
match opt {
Some(f) => self.push(f.into()),
None => self,
}
}
#[inline]
pub fn build(self) -> Filter {
match self.filters.len() {
0 => Filter::None,
1 => self.filters.into_iter().next().unwrap(),
_ => Filter::And(self.filters.into_boxed_slice()),
}
}
#[inline]
pub fn len(&self) -> usize {
self.filters.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.filters.is_empty()
}
}
impl Default for AndFilterBuilder {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct OrFilterBuilder {
filters: Vec<Filter>,
}
impl OrFilterBuilder {
#[inline]
pub fn new() -> Self {
Self {
filters: Vec::new(),
}
}
#[inline]
pub fn with_capacity(capacity: usize) -> Self {
Self {
filters: Vec::with_capacity(capacity),
}
}
#[inline]
pub fn push(mut self, filter: Filter) -> Self {
if !filter.is_none() {
self.filters.push(filter);
}
self
}
#[inline]
pub fn extend(mut self, filters: impl IntoIterator<Item = Filter>) -> Self {
self.filters
.extend(filters.into_iter().filter(|f| !f.is_none()));
self
}
#[inline]
pub fn push_if(self, condition: bool, filter: Filter) -> Self {
if condition { self.push(filter) } else { self }
}
#[inline]
pub fn push_if_some<F>(self, opt: Option<F>) -> Self
where
F: Into<Filter>,
{
match opt {
Some(f) => self.push(f.into()),
None => self,
}
}
#[inline]
pub fn build(self) -> Filter {
match self.filters.len() {
0 => Filter::None,
1 => self.filters.into_iter().next().unwrap(),
_ => Filter::Or(self.filters.into_boxed_slice()),
}
}
#[inline]
pub fn len(&self) -> usize {
self.filters.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.filters.is_empty()
}
}
impl Default for OrFilterBuilder {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct FluentFilterBuilder {
filters: Vec<Filter>,
}
impl FluentFilterBuilder {
#[inline]
pub fn new() -> Self {
Self {
filters: Vec::new(),
}
}
#[inline]
pub fn with_capacity(mut self, capacity: usize) -> Self {
self.filters.reserve(capacity);
self
}
#[inline]
pub fn eq<F, V>(mut self, field: F, value: V) -> Self
where
F: Into<FieldName>,
V: Into<FilterValue>,
{
self.filters
.push(Filter::Equals(field.into(), value.into()));
self
}
#[inline]
pub fn ne<F, V>(mut self, field: F, value: V) -> Self
where
F: Into<FieldName>,
V: Into<FilterValue>,
{
self.filters
.push(Filter::NotEquals(field.into(), value.into()));
self
}
#[inline]
pub fn lt<F, V>(mut self, field: F, value: V) -> Self
where
F: Into<FieldName>,
V: Into<FilterValue>,
{
self.filters.push(Filter::Lt(field.into(), value.into()));
self
}
#[inline]
pub fn lte<F, V>(mut self, field: F, value: V) -> Self
where
F: Into<FieldName>,
V: Into<FilterValue>,
{
self.filters.push(Filter::Lte(field.into(), value.into()));
self
}
#[inline]
pub fn gt<F, V>(mut self, field: F, value: V) -> Self
where
F: Into<FieldName>,
V: Into<FilterValue>,
{
self.filters.push(Filter::Gt(field.into(), value.into()));
self
}
#[inline]
pub fn gte<F, V>(mut self, field: F, value: V) -> Self
where
F: Into<FieldName>,
V: Into<FilterValue>,
{
self.filters.push(Filter::Gte(field.into(), value.into()));
self
}
#[inline]
pub fn is_in<F, I, V>(mut self, field: F, values: I) -> Self
where
F: Into<FieldName>,
I: IntoIterator<Item = V>,
V: Into<FilterValue>,
{
self.filters.push(Filter::In(
field.into(),
values.into_iter().map(Into::into).collect(),
));
self
}
#[inline]
pub fn not_in<F, I, V>(mut self, field: F, values: I) -> Self
where
F: Into<FieldName>,
I: IntoIterator<Item = V>,
V: Into<FilterValue>,
{
self.filters.push(Filter::NotIn(
field.into(),
values.into_iter().map(Into::into).collect(),
));
self
}
#[inline]
pub fn contains<F, V>(mut self, field: F, value: V) -> Self
where
F: Into<FieldName>,
V: Into<FilterValue>,
{
self.filters
.push(Filter::Contains(field.into(), value.into()));
self
}
#[inline]
pub fn starts_with<F, V>(mut self, field: F, value: V) -> Self
where
F: Into<FieldName>,
V: Into<FilterValue>,
{
self.filters
.push(Filter::StartsWith(field.into(), value.into()));
self
}
#[inline]
pub fn ends_with<F, V>(mut self, field: F, value: V) -> Self
where
F: Into<FieldName>,
V: Into<FilterValue>,
{
self.filters
.push(Filter::EndsWith(field.into(), value.into()));
self
}
#[inline]
pub fn is_null<F>(mut self, field: F) -> Self
where
F: Into<FieldName>,
{
self.filters.push(Filter::IsNull(field.into()));
self
}
#[inline]
pub fn is_not_null<F>(mut self, field: F) -> Self
where
F: Into<FieldName>,
{
self.filters.push(Filter::IsNotNull(field.into()));
self
}
#[inline]
pub fn filter(mut self, filter: Filter) -> Self {
if !filter.is_none() {
self.filters.push(filter);
}
self
}
#[inline]
pub fn filter_if(self, condition: bool, filter: Filter) -> Self {
if condition { self.filter(filter) } else { self }
}
#[inline]
pub fn filter_if_some<F>(self, opt: Option<F>) -> Self
where
F: Into<Filter>,
{
match opt {
Some(f) => self.filter(f.into()),
None => self,
}
}
#[inline]
pub fn build_and(self) -> Filter {
let filters: Vec<_> = self.filters.into_iter().filter(|f| !f.is_none()).collect();
match filters.len() {
0 => Filter::None,
1 => filters.into_iter().next().unwrap(),
_ => Filter::And(filters.into_boxed_slice()),
}
}
#[inline]
pub fn build_or(self) -> Filter {
let filters: Vec<_> = self.filters.into_iter().filter(|f| !f.is_none()).collect();
match filters.len() {
0 => Filter::None,
1 => filters.into_iter().next().unwrap(),
_ => Filter::Or(filters.into_boxed_slice()),
}
}
#[inline]
pub fn len(&self) -> usize {
self.filters.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.filters.is_empty()
}
}
impl Default for FluentFilterBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_filter_value_from() {
assert_eq!(FilterValue::from(42i32), FilterValue::Int(42));
assert_eq!(
FilterValue::from("hello"),
FilterValue::String("hello".to_string())
);
assert_eq!(FilterValue::from(true), FilterValue::Bool(true));
}
#[test]
fn test_scalar_filter_equals() {
let filter = ScalarFilter::Equals("test@example.com".to_string()).into_filter("email");
let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
assert_eq!(sql, r#""email" = $1"#);
assert_eq!(params.len(), 1);
}
#[test]
fn test_filter_and() {
let f1 = Filter::Equals("name".into(), "Alice".into());
let f2 = Filter::Gt("age".into(), FilterValue::Int(18));
let combined = Filter::and([f1, f2]);
let (sql, params) = combined.to_sql(0, &crate::dialect::Postgres);
assert!(sql.contains("AND"));
assert_eq!(params.len(), 2);
}
#[test]
fn test_filter_or() {
let f1 = Filter::Equals("status".into(), "active".into());
let f2 = Filter::Equals("status".into(), "pending".into());
let combined = Filter::or([f1, f2]);
let (sql, _) = combined.to_sql(0, &crate::dialect::Postgres);
assert!(sql.contains("OR"));
}
#[test]
fn test_filter_not() {
let filter = Filter::not(Filter::Equals("deleted".into(), FilterValue::Bool(true)));
let (sql, _) = filter.to_sql(0, &crate::dialect::Postgres);
assert!(sql.contains("NOT"));
}
#[test]
fn test_filter_is_null() {
let filter = Filter::IsNull("deleted_at".into());
let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
assert_eq!(sql, r#""deleted_at" IS NULL"#);
assert!(params.is_empty());
}
#[test]
fn test_filter_in() {
let filter = Filter::In("status".into(), vec!["active".into(), "pending".into()]);
let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
assert!(sql.contains("IN"));
assert_eq!(params.len(), 2);
}
#[test]
fn test_filter_contains() {
let filter = Filter::Contains("email".into(), "example".into());
let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
assert!(sql.contains("LIKE"));
assert_eq!(params.len(), 1);
if let FilterValue::String(s) = ¶ms[0] {
assert!(s.contains("%example%"));
}
}
#[test]
fn test_filter_value_is_null() {
assert!(FilterValue::Null.is_null());
assert!(!FilterValue::Bool(false).is_null());
assert!(!FilterValue::Int(0).is_null());
assert!(!FilterValue::Float(0.0).is_null());
assert!(!FilterValue::String("".to_string()).is_null());
}
#[test]
fn test_filter_value_from_i64() {
assert_eq!(FilterValue::from(42i64), FilterValue::Int(42));
assert_eq!(FilterValue::from(-100i64), FilterValue::Int(-100));
}
#[test]
#[allow(clippy::approx_constant)]
fn test_filter_value_from_f64() {
assert_eq!(FilterValue::from(3.14f64), FilterValue::Float(3.14));
}
#[test]
fn test_filter_value_from_string() {
assert_eq!(
FilterValue::from("hello".to_string()),
FilterValue::String("hello".to_string())
);
}
#[test]
fn test_filter_value_from_vec() {
let values: Vec<i32> = vec![1, 2, 3];
let filter_val: FilterValue = values.into();
if let FilterValue::List(list) = filter_val {
assert_eq!(list.len(), 3);
assert_eq!(list[0], FilterValue::Int(1));
assert_eq!(list[1], FilterValue::Int(2));
assert_eq!(list[2], FilterValue::Int(3));
} else {
panic!("Expected List");
}
}
#[test]
fn test_filter_value_from_option_some() {
let val: FilterValue = Some(42i32).into();
assert_eq!(val, FilterValue::Int(42));
}
#[test]
fn test_filter_value_from_option_none() {
let val: FilterValue = Option::<i32>::None.into();
assert_eq!(val, FilterValue::Null);
}
#[test]
fn test_scalar_filter_not() {
let filter = ScalarFilter::Not(Box::new("test".to_string())).into_filter("name");
let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
assert_eq!(sql, r#""name" != $1"#);
assert_eq!(params.len(), 1);
}
#[test]
fn test_scalar_filter_in() {
let filter = ScalarFilter::In(vec!["a".to_string(), "b".to_string()]).into_filter("status");
let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
assert!(sql.contains("IN"));
assert_eq!(params.len(), 2);
}
#[test]
fn test_scalar_filter_not_in() {
let filter = ScalarFilter::NotIn(vec!["x".to_string()]).into_filter("status");
let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
assert!(sql.contains("NOT IN"));
assert_eq!(params.len(), 1);
}
#[test]
fn test_scalar_filter_lt() {
let filter = ScalarFilter::Lt(100i32).into_filter("price");
let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
assert_eq!(sql, r#""price" < $1"#);
assert_eq!(params.len(), 1);
}
#[test]
fn test_scalar_filter_lte() {
let filter = ScalarFilter::Lte(100i32).into_filter("price");
let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
assert_eq!(sql, r#""price" <= $1"#);
assert_eq!(params.len(), 1);
}
#[test]
fn test_scalar_filter_gt() {
let filter = ScalarFilter::Gt(0i32).into_filter("quantity");
let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
assert_eq!(sql, r#""quantity" > $1"#);
assert_eq!(params.len(), 1);
}
#[test]
fn test_scalar_filter_gte() {
let filter = ScalarFilter::Gte(0i32).into_filter("quantity");
let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
assert_eq!(sql, r#""quantity" >= $1"#);
assert_eq!(params.len(), 1);
}
#[test]
fn test_scalar_filter_starts_with() {
let filter = ScalarFilter::StartsWith("prefix".to_string()).into_filter("name");
let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
assert!(sql.contains("LIKE"));
assert_eq!(params.len(), 1);
if let FilterValue::String(s) = ¶ms[0] {
assert!(s.starts_with("prefix"));
assert!(s.ends_with("%"));
}
}
#[test]
fn test_scalar_filter_ends_with() {
let filter = ScalarFilter::EndsWith("suffix".to_string()).into_filter("name");
let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
assert!(sql.contains("LIKE"));
assert_eq!(params.len(), 1);
if let FilterValue::String(s) = ¶ms[0] {
assert!(s.starts_with("%"));
assert!(s.ends_with("suffix"));
}
}
#[test]
fn test_scalar_filter_is_null() {
let filter = ScalarFilter::<String>::IsNull.into_filter("deleted_at");
let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
assert_eq!(sql, r#""deleted_at" IS NULL"#);
assert!(params.is_empty());
}
#[test]
fn test_scalar_filter_is_not_null() {
let filter = ScalarFilter::<String>::IsNotNull.into_filter("name");
let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
assert_eq!(sql, r#""name" IS NOT NULL"#);
assert!(params.is_empty());
}
#[test]
fn test_filter_none() {
let filter = Filter::none();
assert!(filter.is_none());
let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
assert_eq!(sql, "TRUE"); assert!(params.is_empty());
}
#[test]
fn test_filter_not_equals() {
let filter = Filter::NotEquals("status".into(), "deleted".into());
let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
assert_eq!(sql, r#""status" != $1"#);
assert_eq!(params.len(), 1);
}
#[test]
fn test_filter_lte() {
let filter = Filter::Lte("price".into(), FilterValue::Int(100));
let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
assert_eq!(sql, r#""price" <= $1"#);
assert_eq!(params.len(), 1);
}
#[test]
fn test_filter_gte() {
let filter = Filter::Gte("quantity".into(), FilterValue::Int(0));
let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
assert_eq!(sql, r#""quantity" >= $1"#);
assert_eq!(params.len(), 1);
}
#[test]
fn test_filter_not_in() {
let filter = Filter::NotIn("status".into(), vec!["deleted".into(), "archived".into()]);
let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
assert!(sql.contains("NOT IN"));
assert_eq!(params.len(), 2);
}
#[test]
fn test_filter_starts_with() {
let filter = Filter::StartsWith("email".into(), "admin".into());
let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
assert!(sql.contains("LIKE"));
assert_eq!(params.len(), 1);
}
#[test]
fn test_filter_ends_with() {
let filter = Filter::EndsWith("email".into(), "@example.com".into());
let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
assert!(sql.contains("LIKE"));
assert_eq!(params.len(), 1);
}
#[test]
fn test_filter_is_not_null() {
let filter = Filter::IsNotNull("name".into());
let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
assert_eq!(sql, r#""name" IS NOT NULL"#);
assert!(params.is_empty());
}
#[test]
fn test_filter_and_empty() {
let filter = Filter::and([]);
assert!(filter.is_none());
}
#[test]
fn test_filter_and_single() {
let f = Filter::Equals("name".into(), "Alice".into());
let combined = Filter::and([f.clone()]);
assert_eq!(combined, f);
}
#[test]
fn test_filter_and_with_none() {
let f1 = Filter::Equals("name".into(), "Alice".into());
let f2 = Filter::None;
let combined = Filter::and([f1.clone(), f2]);
assert_eq!(combined, f1);
}
#[test]
fn test_filter_or_empty() {
let filter = Filter::or([]);
assert!(filter.is_none());
}
#[test]
fn test_filter_or_single() {
let f = Filter::Equals("status".into(), "active".into());
let combined = Filter::or([f.clone()]);
assert_eq!(combined, f);
}
#[test]
fn test_filter_or_with_none() {
let f1 = Filter::Equals("status".into(), "active".into());
let f2 = Filter::None;
let combined = Filter::or([f1.clone(), f2]);
assert_eq!(combined, f1);
}
#[test]
fn test_filter_not_none() {
let filter = Filter::not(Filter::None);
assert!(filter.is_none());
}
#[test]
fn test_filter_and_then() {
let f1 = Filter::Equals("name".into(), "Alice".into());
let f2 = Filter::Gt("age".into(), FilterValue::Int(18));
let combined = f1.and_then(f2);
let (sql, params) = combined.to_sql(0, &crate::dialect::Postgres);
assert!(sql.contains("AND"));
assert_eq!(params.len(), 2);
}
#[test]
fn test_filter_and_then_with_none_first() {
let f1 = Filter::None;
let f2 = Filter::Equals("name".into(), "Bob".into());
let combined = f1.and_then(f2.clone());
assert_eq!(combined, f2);
}
#[test]
fn test_filter_and_then_with_none_second() {
let f1 = Filter::Equals("name".into(), "Alice".into());
let f2 = Filter::None;
let combined = f1.clone().and_then(f2);
assert_eq!(combined, f1);
}
#[test]
fn test_filter_and_then_chained() {
let f1 = Filter::Equals("a".into(), "1".into());
let f2 = Filter::Equals("b".into(), "2".into());
let f3 = Filter::Equals("c".into(), "3".into());
let combined = f1.and_then(f2).and_then(f3);
let (sql, params) = combined.to_sql(0, &crate::dialect::Postgres);
assert!(sql.contains("AND"));
assert_eq!(params.len(), 3);
}
#[test]
fn test_filter_or_else() {
let f1 = Filter::Equals("status".into(), "active".into());
let f2 = Filter::Equals("status".into(), "pending".into());
let combined = f1.or_else(f2);
let (sql, _) = combined.to_sql(0, &crate::dialect::Postgres);
assert!(sql.contains("OR"));
}
#[test]
fn test_filter_or_else_with_none_first() {
let f1 = Filter::None;
let f2 = Filter::Equals("name".into(), "Bob".into());
let combined = f1.or_else(f2.clone());
assert_eq!(combined, f2);
}
#[test]
fn test_filter_or_else_with_none_second() {
let f1 = Filter::Equals("name".into(), "Alice".into());
let f2 = Filter::None;
let combined = f1.clone().or_else(f2);
assert_eq!(combined, f1);
}
#[test]
fn test_filter_nested_and_or() {
let f1 = Filter::Equals("status".into(), "active".into());
let f2 = Filter::and([
Filter::Gt("age".into(), FilterValue::Int(18)),
Filter::Lt("age".into(), FilterValue::Int(65)),
]);
let combined = Filter::and([f1, f2]);
let (sql, params) = combined.to_sql(0, &crate::dialect::Postgres);
assert!(sql.contains("AND"));
assert_eq!(params.len(), 3);
}
#[test]
fn test_filter_nested_not() {
let inner = Filter::and([
Filter::Equals("status".into(), "deleted".into()),
Filter::Equals("archived".into(), FilterValue::Bool(true)),
]);
let filter = Filter::not(inner);
let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
assert!(sql.contains("NOT"));
assert!(sql.contains("AND"));
assert_eq!(params.len(), 2);
}
#[test]
fn test_filter_with_json_value() {
let json_val = serde_json::json!({"key": "value"});
let filter = Filter::Equals("metadata".into(), FilterValue::Json(json_val));
let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
assert_eq!(sql, r#""metadata" = $1"#);
assert_eq!(params.len(), 1);
}
#[test]
fn test_filter_in_empty_list() {
let filter = Filter::In("status".into(), vec![]);
let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
assert!(
sql.contains("FALSE")
|| sql.contains("1=0")
|| sql.is_empty()
|| sql.contains("status")
);
assert!(params.is_empty());
}
#[test]
fn test_filter_with_null_value() {
let filter = Filter::IsNull("deleted_at".into());
let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
assert!(sql.contains("deleted_at"));
assert!(sql.contains("IS NULL"));
assert!(params.is_empty());
}
#[test]
fn test_and_builder_basic() {
let filter = Filter::and_builder(3)
.push(Filter::Equals("active".into(), FilterValue::Bool(true)))
.push(Filter::Gt("score".into(), FilterValue::Int(100)))
.push(Filter::IsNotNull("email".into()))
.build();
let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
assert!(sql.contains("AND"));
assert_eq!(params.len(), 2); }
#[test]
fn test_and_builder_empty() {
let filter = Filter::and_builder(0).build();
assert!(filter.is_none());
}
#[test]
fn test_and_builder_single() {
let filter = Filter::and_builder(1)
.push(Filter::Equals("id".into(), FilterValue::Int(42)))
.build();
assert!(matches!(filter, Filter::Equals(_, _)));
}
#[test]
fn test_and_builder_filters_none() {
let filter = Filter::and_builder(3)
.push(Filter::None)
.push(Filter::Equals("id".into(), FilterValue::Int(1)))
.push(Filter::None)
.build();
assert!(matches!(filter, Filter::Equals(_, _)));
}
#[test]
fn test_and_builder_push_if() {
let include_deleted = false;
let filter = Filter::and_builder(2)
.push(Filter::Equals("active".into(), FilterValue::Bool(true)))
.push_if(include_deleted, Filter::IsNull("deleted_at".into()))
.build();
assert!(matches!(filter, Filter::Equals(_, _)));
}
#[test]
fn test_or_builder_basic() {
let filter = Filter::or_builder(2)
.push(Filter::Equals(
"role".into(),
FilterValue::String("admin".into()),
))
.push(Filter::Equals(
"role".into(),
FilterValue::String("moderator".into()),
))
.build();
let (sql, _) = filter.to_sql(0, &crate::dialect::Postgres);
assert!(sql.contains("OR"));
}
#[test]
fn test_or_builder_empty() {
let filter = Filter::or_builder(0).build();
assert!(filter.is_none());
}
#[test]
fn test_or_builder_single() {
let filter = Filter::or_builder(1)
.push(Filter::Equals("id".into(), FilterValue::Int(42)))
.build();
assert!(matches!(filter, Filter::Equals(_, _)));
}
#[test]
fn test_fluent_builder_and() {
let filter = Filter::builder()
.eq("status", "active")
.gt("age", 18)
.is_not_null("email")
.build_and();
let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
assert!(sql.contains("AND"));
assert_eq!(params.len(), 2);
}
#[test]
fn test_fluent_builder_or() {
let filter = Filter::builder()
.eq("role", "admin")
.eq("role", "moderator")
.build_or();
let (sql, _) = filter.to_sql(0, &crate::dialect::Postgres);
assert!(sql.contains("OR"));
}
#[test]
fn test_fluent_builder_with_capacity() {
let filter = Filter::builder()
.with_capacity(5)
.eq("a", 1)
.ne("b", 2)
.lt("c", 3)
.lte("d", 4)
.gte("e", 5)
.build_and();
let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
assert!(sql.contains("AND"));
assert_eq!(params.len(), 5);
}
#[test]
fn test_fluent_builder_string_operations() {
let filter = Filter::builder()
.contains("name", "john")
.starts_with("email", "admin")
.ends_with("domain", ".com")
.build_and();
let (sql, _) = filter.to_sql(0, &crate::dialect::Postgres);
assert!(sql.contains("LIKE"));
}
#[test]
fn test_fluent_builder_null_operations() {
let filter = Filter::builder()
.is_null("deleted_at")
.is_not_null("created_at")
.build_and();
let (sql, _) = filter.to_sql(0, &crate::dialect::Postgres);
assert!(sql.contains("IS NULL"));
assert!(sql.contains("IS NOT NULL"));
}
#[test]
fn test_fluent_builder_in_operations() {
let filter = Filter::builder()
.is_in("status", vec!["pending", "processing"])
.not_in("role", vec!["banned", "suspended"])
.build_and();
let (sql, _) = filter.to_sql(0, &crate::dialect::Postgres);
assert!(sql.contains("IN"));
assert!(sql.contains("NOT IN"));
}
#[test]
fn test_fluent_builder_filter_if() {
let include_archived = false;
let filter = Filter::builder()
.eq("active", true)
.filter_if(
include_archived,
Filter::Equals("archived".into(), FilterValue::Bool(true)),
)
.build_and();
assert!(matches!(filter, Filter::Equals(_, _)));
}
#[test]
fn test_fluent_builder_filter_if_some() {
let maybe_status: Option<Filter> = Some(Filter::Equals("status".into(), "active".into()));
let filter = Filter::builder()
.eq("id", 1)
.filter_if_some(maybe_status)
.build_and();
assert!(matches!(filter, Filter::And(_)));
}
#[test]
fn test_and_builder_extend() {
let extra_filters = vec![
Filter::Gt("score".into(), FilterValue::Int(100)),
Filter::Lt("score".into(), FilterValue::Int(1000)),
];
let filter = Filter::and_builder(3)
.push(Filter::Equals("active".into(), FilterValue::Bool(true)))
.extend(extra_filters)
.build();
let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
assert!(sql.contains("AND"));
assert_eq!(params.len(), 3);
}
#[test]
fn test_builder_len_and_is_empty() {
let mut builder = AndFilterBuilder::new();
assert!(builder.is_empty());
assert_eq!(builder.len(), 0);
builder = builder.push(Filter::Equals("id".into(), FilterValue::Int(1)));
assert!(!builder.is_empty());
assert_eq!(builder.len(), 1);
}
#[test]
fn test_and2_both_valid() {
let a = Filter::Equals("id".into(), FilterValue::Int(1));
let b = Filter::Equals("active".into(), FilterValue::Bool(true));
let filter = Filter::and2(a, b);
assert!(matches!(filter, Filter::And(_)));
let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
assert!(sql.contains("AND"));
assert_eq!(params.len(), 2);
}
#[test]
fn test_and2_first_none() {
let a = Filter::None;
let b = Filter::Equals("active".into(), FilterValue::Bool(true));
let filter = Filter::and2(a, b.clone());
assert_eq!(filter, b);
}
#[test]
fn test_and2_second_none() {
let a = Filter::Equals("id".into(), FilterValue::Int(1));
let b = Filter::None;
let filter = Filter::and2(a.clone(), b);
assert_eq!(filter, a);
}
#[test]
fn test_and2_both_none() {
let filter = Filter::and2(Filter::None, Filter::None);
assert!(filter.is_none());
}
#[test]
fn test_or2_both_valid() {
let a = Filter::Equals("role".into(), FilterValue::String("admin".into()));
let b = Filter::Equals("role".into(), FilterValue::String("mod".into()));
let filter = Filter::or2(a, b);
assert!(matches!(filter, Filter::Or(_)));
let (sql, _) = filter.to_sql(0, &crate::dialect::Postgres);
assert!(sql.contains("OR"));
}
#[test]
fn test_or2_first_none() {
let a = Filter::None;
let b = Filter::Equals("active".into(), FilterValue::Bool(true));
let filter = Filter::or2(a, b.clone());
assert_eq!(filter, b);
}
#[test]
fn test_or2_second_none() {
let a = Filter::Equals("id".into(), FilterValue::Int(1));
let b = Filter::None;
let filter = Filter::or2(a.clone(), b);
assert_eq!(filter, a);
}
#[test]
fn test_or2_both_none() {
let filter = Filter::or2(Filter::None, Filter::None);
assert!(filter.is_none());
}
#[test]
fn to_sql_quotes_column_names_against_injection() {
use crate::dialect::{Mssql, Mysql, Postgres};
let filter = Filter::Equals(r#"id" OR 1=1--"#.into(), FilterValue::Int(1));
let (sql_pg, _) = filter.to_sql(0, &Postgres);
assert!(
sql_pg.starts_with(r#""id"" OR 1=1--" ="#),
"postgres did not quote col; got: {sql_pg}"
);
let (sql_my, _) = filter.to_sql(0, &Mysql);
assert!(
sql_my.starts_with(r#"`id" OR 1=1--` ="#),
"mysql did not quote col; got: {sql_my}"
);
let (sql_ms, _) = filter.to_sql(0, &Mssql);
assert!(
sql_ms.starts_with(r#"[id" OR 1=1--] ="#),
"mssql did not quote col; got: {sql_ms}"
);
}
#[test]
fn to_sql_quotes_in_list_column_names() {
use crate::dialect::Postgres;
let filter = Filter::In("id".into(), vec![FilterValue::Int(1), FilterValue::Int(2)]);
let (sql, _) = filter.to_sql(0, &Postgres);
assert!(
sql.starts_with(r#""id" IN ("#),
"expected quoted id on IN, got: {sql}"
);
}
#[test]
fn to_sql_quotes_null_checks() {
use crate::dialect::Postgres;
let filter = Filter::IsNull("deleted_at".into());
let (sql, _) = filter.to_sql(0, &Postgres);
assert_eq!(sql, r#""deleted_at" IS NULL"#);
}
#[test]
fn to_sql_quotes_comparison_operators() {
use crate::dialect::Postgres;
let filter = Filter::Lt("age".into(), FilterValue::Int(18));
let (sql, _) = filter.to_sql(0, &Postgres);
assert!(sql.starts_with(r#""age" < "#), "Lt not quoted: {sql}");
let filter = Filter::Lte("price".into(), FilterValue::Int(100));
let (sql, _) = filter.to_sql(0, &Postgres);
assert!(sql.starts_with(r#""price" <= "#), "Lte not quoted: {sql}");
let filter = Filter::Gt("score".into(), FilterValue::Int(0));
let (sql, _) = filter.to_sql(0, &Postgres);
assert!(sql.starts_with(r#""score" > "#), "Gt not quoted: {sql}");
let filter = Filter::Gte("quantity".into(), FilterValue::Int(1));
let (sql, _) = filter.to_sql(0, &Postgres);
assert!(
sql.starts_with(r#""quantity" >= "#),
"Gte not quoted: {sql}"
);
let filter = Filter::NotEquals("status".into(), "deleted".into());
let (sql, _) = filter.to_sql(0, &Postgres);
assert!(
sql.starts_with(r#""status" != "#),
"NotEquals not quoted: {sql}"
);
}
#[test]
fn to_sql_quotes_like_operators() {
use crate::dialect::Postgres;
let filter = Filter::Contains("email".into(), "example".into());
let (sql, _) = filter.to_sql(0, &Postgres);
assert!(
sql.starts_with(r#""email" LIKE "#),
"Contains not quoted: {sql}"
);
let filter = Filter::StartsWith("name".into(), "admin".into());
let (sql, _) = filter.to_sql(0, &Postgres);
assert!(
sql.starts_with(r#""name" LIKE "#),
"StartsWith not quoted: {sql}"
);
let filter = Filter::EndsWith("domain".into(), ".com".into());
let (sql, _) = filter.to_sql(0, &Postgres);
assert!(
sql.starts_with(r#""domain" LIKE "#),
"EndsWith not quoted: {sql}"
);
}
#[test]
fn to_sql_quotes_not_in() {
use crate::dialect::Postgres;
let filter = Filter::NotIn("status".into(), vec!["deleted".into(), "archived".into()]);
let (sql, _) = filter.to_sql(0, &Postgres);
assert!(
sql.starts_with(r#""status" NOT IN ("#),
"NotIn not quoted: {sql}"
);
}
#[test]
fn to_sql_quotes_is_not_null() {
use crate::dialect::Postgres;
let filter = Filter::IsNotNull("verified_at".into());
let (sql, _) = filter.to_sql(0, &Postgres);
assert_eq!(sql, r#""verified_at" IS NOT NULL"#);
}
#[test]
fn filter_value_from_u64_in_range() {
assert_eq!(FilterValue::from(42u64), FilterValue::Int(42));
assert_eq!(FilterValue::from(0u64), FilterValue::Int(0));
let max_safe = i64::MAX as u64;
assert_eq!(FilterValue::from(max_safe), FilterValue::Int(i64::MAX));
}
#[test]
#[should_panic(expected = "u64 value exceeds i64::MAX")]
fn filter_value_from_u64_overflow_panics() {
let _ = FilterValue::from(u64::MAX);
}
#[test]
fn filter_value_from_chrono_datetime_utc_rfc3339() {
use chrono::{TimeZone, Utc};
let dt = Utc.with_ymd_and_hms(2020, 1, 15, 10, 30, 45).unwrap();
let fv = FilterValue::from(dt);
assert_eq!(
fv,
FilterValue::String("2020-01-15T10:30:45.000000Z".to_string())
);
}
#[test]
fn filter_value_from_chrono_naive_datetime_iso() {
use chrono::NaiveDate;
let dt = NaiveDate::from_ymd_opt(2020, 1, 15)
.unwrap()
.and_hms_opt(10, 30, 45)
.unwrap();
let fv = FilterValue::from(dt);
assert_eq!(
fv,
FilterValue::String("2020-01-15T10:30:45.000000".to_string())
);
}
#[test]
fn filter_value_from_chrono_naive_date() {
use chrono::NaiveDate;
let d = NaiveDate::from_ymd_opt(2020, 1, 15).unwrap();
assert_eq!(
FilterValue::from(d),
FilterValue::String("2020-01-15".to_string())
);
}
#[test]
fn filter_value_from_chrono_naive_time() {
use chrono::NaiveTime;
let t = NaiveTime::from_hms_opt(10, 30, 45).unwrap();
assert_eq!(
FilterValue::from(t),
FilterValue::String("10:30:45.000000".to_string())
);
}
#[test]
fn filter_value_from_uuid_is_lowercase_hyphenated() {
use uuid::Uuid;
let u = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
match FilterValue::from(u) {
FilterValue::String(ref s) => {
assert_eq!(s, "550e8400-e29b-41d4-a716-446655440000");
assert_eq!(s, &u.to_string());
}
other => panic!("expected FilterValue::String, got {other:?}"),
}
}
#[test]
fn filter_value_from_uuid_nil_round_trips() {
use uuid::Uuid;
let u = Uuid::nil();
assert_eq!(
FilterValue::from(u),
FilterValue::String("00000000-0000-0000-0000-000000000000".to_string())
);
}
#[test]
fn filter_value_from_decimal_uses_to_string_not_f64() {
use rust_decimal::Decimal;
use std::str::FromStr;
let d = Decimal::from_str("3.14").unwrap();
assert_eq!(
FilterValue::from(d),
FilterValue::String("3.14".to_string())
);
}
#[test]
fn filter_value_from_decimal_high_precision_preserved() {
use rust_decimal::Decimal;
use std::str::FromStr;
let d = Decimal::from_str("1234567890.1234567890").unwrap();
match FilterValue::from(d) {
FilterValue::String(ref s) => {
assert_eq!(s, "1234567890.1234567890");
}
other => panic!("expected FilterValue::String, got {other:?}"),
}
}
#[test]
fn filter_value_from_serde_json_value_keeps_json_variant() {
let v = serde_json::json!({"key": "value", "nested": [1, 2, 3]});
match FilterValue::from(v.clone()) {
FilterValue::Json(inner) => {
assert_eq!(inner, v);
}
other => panic!("expected FilterValue::Json, got {other:?}"),
}
}
#[test]
fn filter_value_from_serde_json_null_keeps_json_variant() {
let v = serde_json::Value::Null;
match FilterValue::from(v) {
FilterValue::Json(serde_json::Value::Null) => {}
other => panic!("expected FilterValue::Json(Null), got {other:?}"),
}
}
#[test]
fn filter_value_from_option_none_maps_to_null() {
let none_i32: Option<i32> = None;
assert_eq!(FilterValue::from(none_i32), FilterValue::Null);
let none_string: Option<String> = None;
assert_eq!(FilterValue::from(none_string), FilterValue::Null);
}
#[test]
fn filter_value_from_signed_integer_extremes() {
assert_eq!(FilterValue::from(i8::MIN), FilterValue::Int(i8::MIN as i64));
assert_eq!(FilterValue::from(i8::MAX), FilterValue::Int(i8::MAX as i64));
assert_eq!(
FilterValue::from(i16::MIN),
FilterValue::Int(i16::MIN as i64)
);
assert_eq!(
FilterValue::from(i16::MAX),
FilterValue::Int(i16::MAX as i64)
);
}
#[test]
fn filter_value_from_unsigned_integer_extremes() {
assert_eq!(FilterValue::from(u8::MAX), FilterValue::Int(u8::MAX as i64));
assert_eq!(
FilterValue::from(u16::MAX),
FilterValue::Int(u16::MAX as i64)
);
assert_eq!(
FilterValue::from(u32::MAX),
FilterValue::Int(u32::MAX as i64)
);
assert_eq!(FilterValue::from(u32::MAX), FilterValue::Int(4_294_967_295));
}
#[test]
fn filter_value_from_f32_widens_to_f64() {
let v: f32 = 1.5;
assert_eq!(FilterValue::from(v), FilterValue::Float(1.5));
}
#[test]
fn to_filter_value_option_some_some() {
let v: Option<i32> = Some(42);
assert_eq!(v.to_filter_value(), FilterValue::Int(42));
}
#[test]
fn to_filter_value_option_none_is_null() {
let v: Option<i32> = None;
assert_eq!(v.to_filter_value(), FilterValue::Null);
}
#[test]
fn to_filter_value_uuid_is_string() {
let id = uuid::Uuid::nil();
assert_eq!(id.to_filter_value(), FilterValue::String(id.to_string()));
}
#[test]
fn to_filter_value_bool_is_bool() {
assert_eq!(true.to_filter_value(), FilterValue::Bool(true));
}
}