use std::fmt::Write;
use crate::column::AnyColumn;
use crate::value::Value;
#[derive(Debug, Clone)]
pub enum Expr {
Eq(AnyColumn, Value),
Ne(AnyColumn, Value),
Gt(AnyColumn, Value),
Gte(AnyColumn, Value),
Lt(AnyColumn, Value),
Lte(AnyColumn, Value),
Between(AnyColumn, Value, Value),
InList(AnyColumn, Vec<Value>),
NotIn(AnyColumn, Vec<Value>),
Like(AnyColumn, Value),
ILike(AnyColumn, Value),
IsNull(AnyColumn),
IsNotNull(AnyColumn),
And(Box<Expr>, Box<Expr>),
Or(Box<Expr>, Box<Expr>),
Not(Box<Expr>),
}
impl Expr {
pub fn and(self, other: Expr) -> Expr {
Expr::And(Box::new(self), Box::new(other))
}
pub fn or(self, other: Expr) -> Expr {
Expr::Or(Box::new(self), Box::new(other))
}
pub fn not(self) -> Expr {
Expr::Not(Box::new(self))
}
pub fn to_sql(&self, sql: &mut String, params: &mut Vec<Value>, idx: &mut usize) {
match self {
Expr::Eq(col, val) => {
*idx += 1;
write!(sql, "{} = ${}", col.name(), idx).unwrap();
params.push(val.clone());
}
Expr::Ne(col, val) => {
*idx += 1;
write!(sql, "{} != ${}", col.name(), idx).unwrap();
params.push(val.clone());
}
Expr::Gt(col, val) => {
*idx += 1;
write!(sql, "{} > ${}", col.name(), idx).unwrap();
params.push(val.clone());
}
Expr::Gte(col, val) => {
*idx += 1;
write!(sql, "{} >= ${}", col.name(), idx).unwrap();
params.push(val.clone());
}
Expr::Lt(col, val) => {
*idx += 1;
write!(sql, "{} < ${}", col.name(), idx).unwrap();
params.push(val.clone());
}
Expr::Lte(col, val) => {
*idx += 1;
write!(sql, "{} <= ${}", col.name(), idx).unwrap();
params.push(val.clone());
}
Expr::Between(col, low, high) => {
*idx += 1;
let low_idx = *idx;
*idx += 1;
let high_idx = *idx;
write!(sql, "{} BETWEEN ${} AND ${}", col.name(), low_idx, high_idx).unwrap();
params.push(low.clone());
params.push(high.clone());
}
Expr::InList(col, values) => {
if values.is_empty() {
sql.push_str("1=0");
} else {
write!(sql, "{} IN (", col.name()).unwrap();
for (i, val) in values.iter().enumerate() {
if i > 0 {
sql.push_str(", ");
}
*idx += 1;
write!(sql, "${}", idx).unwrap();
params.push(val.clone());
}
sql.push(')');
}
}
Expr::NotIn(col, values) => {
if values.is_empty() {
sql.push_str("1=1");
} else {
write!(sql, "{} NOT IN (", col.name()).unwrap();
for (i, val) in values.iter().enumerate() {
if i > 0 {
sql.push_str(", ");
}
*idx += 1;
write!(sql, "${}", idx).unwrap();
params.push(val.clone());
}
sql.push(')');
}
}
Expr::Like(col, val) => {
*idx += 1;
write!(sql, "{} LIKE ${}", col.name(), idx).unwrap();
params.push(val.clone());
}
Expr::ILike(col, val) => {
*idx += 1;
write!(sql, "{} ILIKE ${}", col.name(), idx).unwrap();
params.push(val.clone());
}
Expr::IsNull(col) => {
write!(sql, "{} IS NULL", col.name()).unwrap();
}
Expr::IsNotNull(col) => {
write!(sql, "{} IS NOT NULL", col.name()).unwrap();
}
Expr::And(left, right) => {
sql.push('(');
left.to_sql(sql, params, idx);
sql.push_str(" AND ");
right.to_sql(sql, params, idx);
sql.push(')');
}
Expr::Or(left, right) => {
sql.push('(');
left.to_sql(sql, params, idx);
sql.push_str(" OR ");
right.to_sql(sql, params, idx);
sql.push(')');
}
Expr::Not(inner) => {
sql.push_str("NOT (");
inner.to_sql(sql, params, idx);
sql.push(')');
}
}
}
pub fn render(&self) -> (String, Vec<Value>) {
let mut sql = String::new();
let mut params = Vec::new();
let mut idx = 0usize;
self.to_sql(&mut sql, &mut params, &mut idx);
(sql, params)
}
}
#[derive(Debug, Clone)]
pub struct OrderExpr {
pub(crate) column: AnyColumn,
pub(crate) direction: OrderDirection,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum OrderDirection {
Asc,
Desc,
AscNullsLast,
DescNullsFirst,
}
impl OrderExpr {
pub fn new(column: AnyColumn, direction: OrderDirection) -> Self {
Self { column, direction }
}
pub fn to_sql(&self, sql: &mut String) {
sql.push_str(self.column.name());
match self.direction {
OrderDirection::Asc => sql.push_str(" ASC"),
OrderDirection::Desc => sql.push_str(" DESC"),
OrderDirection::AscNullsLast => sql.push_str(" ASC NULLS LAST"),
OrderDirection::DescNullsFirst => sql.push_str(" DESC NULLS FIRST"),
}
}
pub fn render(&self) -> String {
let mut sql = String::new();
self.to_sql(&mut sql);
sql
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::column::Column;
fn col_id() -> Column<i32> { Column::new("id", "users") }
fn col_age() -> Column<i32> { Column::new("age", "users") }
fn col_name() -> Column<String> { Column::new("name", "users") }
fn col_email() -> Column<String> { Column::new("email", "users") }
#[test]
fn render_eq() {
let (sql, params) = col_age().eq(25).render();
assert_eq!(sql, "age = $1");
assert_eq!(params, vec![Value::Int(25)]);
}
#[test]
fn render_ne() {
let (sql, params) = col_age().ne(0).render();
assert_eq!(sql, "age != $1");
assert_eq!(params, vec![Value::Int(0)]);
}
#[test]
fn render_gt() {
let (sql, params) = col_age().gt(25).render();
assert_eq!(sql, "age > $1");
assert_eq!(params, vec![Value::Int(25)]);
}
#[test]
fn render_gte() {
let (sql, params) = col_age().gte(18).render();
assert_eq!(sql, "age >= $1");
assert_eq!(params, vec![Value::Int(18)]);
}
#[test]
fn render_lt() {
let (sql, params) = col_age().lt(65).render();
assert_eq!(sql, "age < $1");
assert_eq!(params, vec![Value::Int(65)]);
}
#[test]
fn render_lte() {
let (sql, params) = col_age().lte(100).render();
assert_eq!(sql, "age <= $1");
assert_eq!(params, vec![Value::Int(100)]);
}
#[test]
fn render_between() {
let (sql, params) = col_age().between(18, 65).render();
assert_eq!(sql, "age BETWEEN $1 AND $2");
assert_eq!(params, vec![Value::Int(18), Value::Int(65)]);
}
#[test]
fn render_in_list() {
let (sql, params) = col_id().in_list(vec![1, 2, 3]).render();
assert_eq!(sql, "id IN ($1, $2, $3)");
assert_eq!(params, vec![Value::Int(1), Value::Int(2), Value::Int(3)]);
}
#[test]
fn render_in_list_single() {
let (sql, params) = col_id().in_list(vec![42]).render();
assert_eq!(sql, "id IN ($1)");
assert_eq!(params, vec![Value::Int(42)]);
}
#[test]
fn render_in_list_empty() {
let (sql, params) = col_id().in_list(Vec::<i32>::new()).render();
assert_eq!(sql, "1=0");
assert!(params.is_empty());
}
#[test]
fn render_not_in() {
let (sql, params) = col_id().not_in(vec![4, 5]).render();
assert_eq!(sql, "id NOT IN ($1, $2)");
assert_eq!(params, vec![Value::Int(4), Value::Int(5)]);
}
#[test]
fn render_not_in_empty() {
let (sql, params) = col_id().not_in(Vec::<i32>::new()).render();
assert_eq!(sql, "1=1");
assert!(params.is_empty());
}
#[test]
fn render_like() {
let (sql, params) = col_name().like("A%").render();
assert_eq!(sql, "name LIKE $1");
assert_eq!(params, vec![Value::String("A%".into())]);
}
#[test]
fn render_ilike() {
let (sql, params) = col_name().ilike("alice%").render();
assert_eq!(sql, "name ILIKE $1");
assert_eq!(params, vec![Value::String("alice%".into())]);
}
#[test]
fn render_contains() {
let (sql, params) = col_name().contains("foo").render();
assert_eq!(sql, "name LIKE $1");
assert_eq!(params, vec![Value::String("%foo%".into())]);
}
#[test]
fn render_starts_with() {
let (sql, params) = col_name().starts_with("A").render();
assert_eq!(sql, "name LIKE $1");
assert_eq!(params, vec![Value::String("A%".into())]);
}
#[test]
fn render_ends_with() {
let (sql, params) = col_name().ends_with("z").render();
assert_eq!(sql, "name LIKE $1");
assert_eq!(params, vec![Value::String("%z".into())]);
}
#[test]
fn render_is_null() {
let (sql, params) = col_email().is_null().render();
assert_eq!(sql, "email IS NULL");
assert!(params.is_empty());
}
#[test]
fn render_is_not_null() {
let (sql, params) = col_email().is_not_null().render();
assert_eq!(sql, "email IS NOT NULL");
assert!(params.is_empty());
}
#[test]
fn render_and() {
let (sql, params) = col_age().gt(25).and(col_name().eq("Alice")).render();
assert_eq!(sql, "(age > $1 AND name = $2)");
assert_eq!(params, vec![Value::Int(25), Value::String("Alice".into())]);
}
#[test]
fn render_or() {
let (sql, params) = col_age().lt(18).or(col_age().gt(65)).render();
assert_eq!(sql, "(age < $1 OR age > $2)");
assert_eq!(params, vec![Value::Int(18), Value::Int(65)]);
}
#[test]
fn render_not() {
let (sql, params) = col_email().is_null().not().render();
assert_eq!(sql, "NOT (email IS NULL)");
assert!(params.is_empty());
}
#[test]
fn render_nested_and_or() {
let expr = col_age().gt(25).and(
col_name().eq("Alice").or(col_name().eq("Bob")),
);
let (sql, params) = expr.render();
assert_eq!(sql, "(age > $1 AND (name = $2 OR name = $3))");
assert_eq!(params, vec![
Value::Int(25),
Value::String("Alice".into()),
Value::String("Bob".into()),
]);
}
#[test]
fn render_deep_nesting() {
let expr = col_age().lt(18).or(col_age().gt(65)).not();
let (sql, params) = expr.render();
assert_eq!(sql, "NOT ((age < $1 OR age > $2))");
assert_eq!(params, vec![Value::Int(18), Value::Int(65)]);
}
#[test]
fn render_complex_filter() {
let expr = col_age()
.between(18, 65)
.and(col_email().is_not_null())
.or(col_name().eq("admin"));
let (sql, params) = expr.render();
assert_eq!(
sql,
"((age BETWEEN $1 AND $2 AND email IS NOT NULL) OR name = $3)"
);
assert_eq!(params, vec![
Value::Int(18),
Value::Int(65),
Value::String("admin".into()),
]);
}
#[test]
fn render_shared_counter() {
let mut sql = String::new();
let mut params = Vec::new();
let mut idx = 0usize;
col_age().gt(25).to_sql(&mut sql, &mut params, &mut idx);
sql.push_str(" AND ");
col_name().eq("Alice").to_sql(&mut sql, &mut params, &mut idx);
assert_eq!(sql, "age > $1 AND name = $2");
assert_eq!(idx, 2);
assert_eq!(params.len(), 2);
}
#[test]
fn render_many_params() {
let mut sql = String::new();
let mut params = Vec::new();
let mut idx = 0usize;
col_id().in_list(vec![1, 2, 3, 4, 5]).to_sql(&mut sql, &mut params, &mut idx);
sql.push_str(" AND ");
col_name().eq("test").to_sql(&mut sql, &mut params, &mut idx);
assert_eq!(sql, "id IN ($1, $2, $3, $4, $5) AND name = $6");
assert_eq!(params.len(), 6);
assert_eq!(idx, 6);
}
#[test]
fn render_order_asc() {
assert_eq!(col_name().asc().render(), "name ASC");
}
#[test]
fn render_order_desc() {
assert_eq!(col_age().desc().render(), "age DESC");
}
#[test]
fn render_order_asc_nulls_last() {
assert_eq!(col_age().asc_nulls_last().render(), "age ASC NULLS LAST");
}
#[test]
fn render_order_desc_nulls_first() {
assert_eq!(col_age().desc_nulls_first().render(), "age DESC NULLS FIRST");
}
#[test]
fn render_is_null_no_params() {
let (_, params) = col_email().is_null().render();
assert!(params.is_empty(), "IS NULL should not produce parameters");
}
#[test]
fn render_and_with_null() {
let (sql, params) = col_age().gt(25).and(col_email().is_null()).render();
assert_eq!(sql, "(age > $1 AND email IS NULL)");
assert_eq!(params.len(), 1); }
}