use crate::core::condition::{Condition, JoinOp, OrderDir as CoreOrderDir, SqlValue};
use crate::core::model::Model;
use crate::core::query::QueryBuilder;
use crate::dsl::column::{NullsOrder, OrderDir as DslOrderDir, OrderExpr};
use crate::dsl::expr::Expr;
use crate::dsl::SelectBuilder;
pub fn expr_to_condition(expr: Expr) -> Condition {
match expr {
Expr::Eq(col, val) => Condition::Eq(col, val),
Expr::Ne(col, val) => Condition::Ne(col, val),
Expr::Gt(col, val) => Condition::Gt(col, val),
Expr::Gte(col, val) => Condition::Gte(col, val),
Expr::Lt(col, val) => Condition::Lt(col, val),
Expr::Lte(col, val) => Condition::Lte(col, val),
Expr::Like(col, val) => Condition::Like(col, sql_value_to_str(val)),
Expr::NotLike(col, val) => Condition::NotLike(col, sql_value_to_str(val)),
Expr::ILike(col, val) => Condition::ILike(col, sql_value_to_str(val)),
Expr::In(col, vals) => Condition::In(col, vals),
Expr::NotIn(col, vals) => Condition::NotIn(col, vals),
Expr::IsNull(col) => Condition::IsNull(col),
Expr::IsNotNull(col) => Condition::IsNotNull(col),
Expr::Between(col, lo, hi) => Condition::Between(col, lo, hi),
Expr::NotBetween(col, lo, hi) => Condition::NotBetween(col, lo, hi),
Expr::And(l, r) => Condition::Group(vec![
(JoinOp::And, expr_to_condition(*l)),
(JoinOp::And, expr_to_condition(*r)),
]),
Expr::Or(l, r) => Condition::Group(vec![
(JoinOp::And, expr_to_condition(*l)),
(JoinOp::Or, expr_to_condition(*r)),
]),
Expr::Not(inner) => Condition::Raw(format!("NOT ({})", expr_to_literal(&inner))),
Expr::Raw(sql) => Condition::Raw(sql),
Expr::Exists(sql) => Condition::Raw(format!("EXISTS ({sql})")),
Expr::NotExists(sql) => Condition::Raw(format!("NOT EXISTS ({sql})")),
Expr::InSubquery(col, sql) => Condition::Raw(format!("{col} IN ({sql})")),
Expr::NotInSubquery(col, sql) => Condition::Raw(format!("{col} NOT IN ({sql})")),
Expr::ColEq(a, b) => Condition::Raw(format!("{a} = {b}")),
Expr::AggCmp(agg, op, val) => {
Condition::Raw(format!("{agg} {op} {}", val.to_sql_literal()))
}
Expr::Case(case_expr) => {
Condition::Raw(case_expr_to_literal(&case_expr))
}
#[allow(unreachable_patterns)]
_ => Condition::Raw("1=1".into()),
}
}
pub fn model_query_into_select<M: Model>(builder: QueryBuilder<M>) -> SelectBuilder {
let mut sel = SelectBuilder::new().from_table_name(builder.table_name_str());
for (op, cond) in builder.conditions() {
let expr = condition_to_expr(cond.clone());
match op {
JoinOp::And => sel = sel.where_(expr),
JoinOp::Or => sel = sel.or_where(expr),
}
}
for (col, dir) in builder.order_clauses() {
let dsl_dir = match dir {
CoreOrderDir::Asc => DslOrderDir::Asc,
CoreOrderDir::Desc => DslOrderDir::Desc,
};
let ord = OrderExpr {
col: col.clone(),
dir: dsl_dir,
nulls: NullsOrder::Default,
};
sel = sel.order_by(ord);
}
if let Some(n) = builder.limit_value() {
sel = sel.limit(n as u64);
}
if let Some(n) = builder.offset_value() {
sel = sel.offset(n as u64);
}
sel
}
fn condition_to_expr(cond: Condition) -> Expr {
match cond {
Condition::Eq(col, val) => Expr::Eq(col, val),
Condition::Ne(col, val) => Expr::Ne(col, val),
Condition::Gt(col, val) => Expr::Gt(col, val),
Condition::Gte(col, val) => Expr::Gte(col, val),
Condition::Lt(col, val) => Expr::Lt(col, val),
Condition::Lte(col, val) => Expr::Lte(col, val),
Condition::Like(col, pat) => Expr::Like(col, SqlValue::Text(pat)),
Condition::NotLike(col, pat) => Expr::NotLike(col, SqlValue::Text(pat)),
Condition::ILike(col, pat) => Expr::ILike(col, SqlValue::Text(pat)),
Condition::In(col, vals) => Expr::In(col, vals),
Condition::NotIn(col, vals) => Expr::NotIn(col, vals),
Condition::IsNull(col) => Expr::IsNull(col),
Condition::IsNotNull(col) => Expr::IsNotNull(col),
Condition::Between(col, lo, hi) => Expr::Between(col, lo, hi),
Condition::NotBetween(col, lo, hi) => Expr::NotBetween(col, lo, hi),
Condition::Raw(sql) => Expr::Raw(sql),
Condition::Group(inner) => {
let pairs: Vec<(JoinOp, Expr)> = inner
.into_iter()
.map(|(op, c)| (op, condition_to_expr(c)))
.collect();
group_to_expr(pairs)
}
other => Expr::Raw(other.to_literal_sql()),
}
}
fn group_to_expr(mut pairs: Vec<(JoinOp, Expr)>) -> Expr {
if pairs.is_empty() {
return Expr::Raw("1=1".into());
}
let (_, first) = pairs.remove(0);
pairs.into_iter().fold(first, |acc, (op, expr)| match op {
JoinOp::And => acc.and(expr),
JoinOp::Or => acc.or(expr),
})
}
fn sql_value_to_str(v: SqlValue) -> String {
match v {
SqlValue::Text(s) => s,
other => other.to_sql_literal(),
}
}
fn expr_to_literal(expr: &Expr) -> String {
let (sql, params) = expr.to_sql_pg(1);
let mut result = sql;
for i in (0..params.len()).rev() {
result = result.replace(&format!("${}", i + 1), ¶ms[i].to_sql_literal());
}
result
}
fn case_expr_to_literal(case_expr: &crate::dsl::expr::CaseExpr) -> String {
let (sql, params) = case_expr.render(1, '$');
let mut result = sql;
for i in (0..params.len()).rev() {
result = result.replace(&format!("${}", i + 1), ¶ms[i].to_sql_literal());
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::condition::SqlValue;
use crate::core::model::Model;
use crate::core::query::QueryBuilder;
use crate::dsl::expr::Expr;
struct Stub;
impl Model for Stub {
fn table_name() -> &'static str {
"posts"
}
fn columns() -> &'static [&'static str] {
&["id", "user_id", "published"]
}
}
#[test]
fn expr_eq_roundtrip() {
let expr = Expr::Eq("user_id".into(), SqlValue::Integer(42));
let cond = expr_to_condition(expr);
let (sql, params) = cond.to_param_sql(1);
assert_eq!(sql, "user_id = $1");
assert_eq!(params, vec![SqlValue::Integer(42)]);
}
#[test]
fn expr_and_roundtrip() {
let expr = Expr::Eq("user_id".into(), SqlValue::Integer(1))
.and(Expr::Eq("published".into(), SqlValue::Bool(true)));
let cond = expr_to_condition(expr);
let (sql, params) = cond.to_param_sql(1);
assert_eq!(sql, "(user_id = $1 AND published = $2)");
assert_eq!(params.len(), 2);
}
#[test]
fn expr_or_roundtrip() {
let expr = Expr::Eq("status".into(), SqlValue::Text("active".into()))
.or(Expr::Eq("status".into(), SqlValue::Text("pending".into())));
let cond = expr_to_condition(expr);
let (sql, params) = cond.to_param_sql(1);
assert_eq!(sql, "(status = $1 OR status = $2)");
assert_eq!(params.len(), 2);
}
#[test]
fn expr_in_roundtrip() {
let expr = Expr::In(
"id".into(),
vec![
SqlValue::Integer(1),
SqlValue::Integer(2),
SqlValue::Integer(3),
],
);
let cond = expr_to_condition(expr);
let (sql, params) = cond.to_param_sql(1);
assert_eq!(sql, "id IN ($1, $2, $3)");
assert_eq!(params.len(), 3);
}
#[test]
fn into_dsl_preserves_table_name() {
let builder: QueryBuilder<Stub> = QueryBuilder::new("posts");
let sel = model_query_into_select(builder);
let (sql, _) = sel.to_sql_pg();
assert!(sql.contains("FROM \"posts\""), "sql: {sql}");
}
#[test]
fn into_dsl_preserves_eq_condition() {
let builder: QueryBuilder<Stub> = QueryBuilder::new("posts").where_eq("user_id", 7_i64);
let sel = model_query_into_select(builder);
let (sql, params) = sel.to_sql_pg();
assert!(sql.contains("WHERE"), "sql: {sql}");
assert_eq!(params.len(), 1);
assert_eq!(params[0], SqlValue::Integer(7));
}
#[test]
fn into_dsl_preserves_limit_offset() {
let builder: QueryBuilder<Stub> = QueryBuilder::new("posts").limit(10).offset(20);
let sel = model_query_into_select(builder);
let (sql, _) = sel.to_sql_pg();
assert!(sql.contains("LIMIT 10"), "sql: {sql}");
assert!(sql.contains("OFFSET 20"), "sql: {sql}");
}
}