use std::borrow::Cow;
use crate::value::{ToValue, Value};
use crate::writer::{DynExpr, Expression, SqlWriter, dyn_expr};
use super::convert::{IntoExpr, IntoExprList, IntoIdent};
use super::raw::{RawArg, write_template};
#[derive(Debug, Clone)]
pub enum Expr {
Raw(Cow<'static, str>),
Literal(Cow<'static, str>),
Ident(Vec<Cow<'static, str>>),
Arg(Value),
Args(Vec<Value>),
NamedArg(Cow<'static, str>),
Template {
sql: Cow<'static, str>,
args: Vec<RawArg>,
},
Group(Vec<Expr>),
Binary {
lhs: Box<Expr>,
op: &'static str,
rhs: Box<Expr>,
},
Prefix {
op: &'static str,
operand: Box<Expr>,
},
Postfix {
operand: Box<Expr>,
op: &'static str,
},
Join {
exprs: Vec<Expr>,
sep: &'static str,
},
Func {
name: Cow<'static, str>,
args: Vec<Expr>,
over: Option<Box<Expr>>,
},
Case {
whens: Vec<(Expr, Expr)>,
else_: Option<Box<Expr>>,
},
Cast {
expr: Box<Expr>,
type_name: Cow<'static, str>,
},
Custom(DynExpr),
}
impl Expr {
pub fn raw(sql: impl Into<Cow<'static, str>>) -> Expr {
Expr::Raw(sql.into())
}
pub fn template(
sql: impl Into<Cow<'static, str>>,
args: impl IntoIterator<Item = RawArg>,
) -> Expr {
Expr::Template {
sql: sql.into(),
args: args.into_iter().collect(),
}
}
pub fn literal(s: impl Into<Cow<'static, str>>) -> Expr {
Expr::Literal(s.into())
}
pub fn ident(parts: impl IntoIdent) -> Expr {
let mut parts = parts.into_ident_parts();
parts.retain(|p| !p.is_empty());
Expr::Ident(parts)
}
pub fn arg(v: impl ToValue) -> Expr {
Expr::Arg(v.to_value())
}
pub fn args<V: ToValue>(vals: impl IntoIterator<Item = V>) -> Expr {
Expr::Args(vals.into_iter().map(ToValue::to_value).collect())
}
pub fn placeholders(n: usize) -> Expr {
Expr::Args(vec![Value::Null; n])
}
pub fn named_arg(name: impl Into<Cow<'static, str>>) -> Expr {
Expr::NamedArg(name.into())
}
pub fn group(items: impl IntoExprList) -> Expr {
Expr::Group(items.into_expr_list())
}
pub fn binary(lhs: impl IntoExpr, op: &'static str, rhs: impl IntoExpr) -> Expr {
Expr::Binary {
lhs: Box::new(lhs.into_expr()),
op,
rhs: Box::new(rhs.into_expr()),
}
}
pub fn prefix(op: &'static str, operand: impl IntoExpr) -> Expr {
Expr::Prefix {
op,
operand: Box::new(operand.into_expr()),
}
}
pub fn postfix(operand: impl IntoExpr, op: &'static str) -> Expr {
Expr::Postfix {
operand: Box::new(operand.into_expr()),
op,
}
}
pub fn join(items: impl IntoExprList) -> Expr {
Expr::join_with(" ", items)
}
pub fn join_with(sep: &'static str, items: impl IntoExprList) -> Expr {
Expr::Join {
exprs: items.into_expr_list(),
sep,
}
}
pub fn func(name: impl Into<Cow<'static, str>>, args: impl IntoExprList) -> Expr {
Expr::Func {
name: name.into(),
args: args.into_expr_list(),
over: None,
}
}
pub fn cast(expr: impl IntoExpr, type_name: impl Into<Cow<'static, str>>) -> Expr {
Expr::Cast {
expr: Box::new(expr.into_expr()),
type_name: type_name.into(),
}
}
pub fn custom(e: impl Expression + 'static) -> Expr {
Expr::Custom(dyn_expr(e))
}
pub fn is_atomic(&self) -> bool {
matches!(
self,
Expr::Raw(_)
| Expr::Template { .. }
| Expr::Literal(_)
| Expr::Ident(_)
| Expr::Arg(_)
| Expr::Args(_)
| Expr::NamedArg(_)
| Expr::Group(_)
)
}
#[must_use]
pub fn grouped(self) -> Expr {
if self.is_atomic() {
self
} else {
Expr::Group(vec![self])
}
}
}
impl Expression for Expr {
fn write_sql(&self, w: &mut SqlWriter<'_>) {
match self {
Expr::Raw(sql) => w.push_str(sql),
Expr::Literal(s) => {
w.push_str("'");
w.push_str(s);
w.push_str("'");
}
Expr::Ident(parts) => w.push_quoted(parts),
Expr::Arg(v) => w.push_arg(v.clone()),
Expr::Args(vals) => {
if vals.is_empty() {
w.push_str("NULL");
}
for (i, v) in vals.iter().enumerate() {
if i > 0 {
w.push_str(", ");
}
w.push_arg(v.clone());
}
}
Expr::NamedArg(name) => w.push_named_arg(name),
Expr::Template { sql, args } => write_template(w, sql, args),
Expr::Group(items) => {
if items.is_empty() {
w.push_str("(NULL)");
} else {
w.write_slice(items, "(", ", ", ")");
}
}
Expr::Binary { lhs, op, rhs } => {
w.write_expr(&**lhs);
w.push_str(" ");
w.push_str(op);
w.push_str(" ");
w.write_expr(&**rhs);
}
Expr::Prefix { op, operand } => {
w.push_str(op);
w.push_str(" ");
w.write_expr(&**operand);
}
Expr::Postfix { operand, op } => {
w.write_expr(&**operand);
w.push_str(" ");
w.push_str(op);
}
Expr::Join { exprs, sep } => w.write_slice(exprs, "", sep, ""),
Expr::Func { name, args, over } => {
w.push_str(name);
w.push_str("(");
w.write_slice(args, "", ", ", "");
w.push_str(")");
w.write_if_some(over.as_deref(), " OVER (", ")");
}
Expr::Case { whens, else_ } => {
if whens.is_empty() {
w.record_error(crate::Error::Incomplete("a CASE WHEN branch"));
return;
}
w.push_str("CASE");
for (cond, then) in whens {
w.push_str(" WHEN ");
w.write_expr(cond);
w.push_str(" THEN ");
w.write_expr(then);
}
w.write_if_some(else_.as_deref(), " ELSE ", "");
w.push_str(" END");
}
Expr::Cast { expr, type_name } => {
w.push_str("CAST(");
w.write_expr(&**expr);
w.push_str(" AS ");
w.push_str(type_name);
w.push_str(")");
}
Expr::Custom(e) => w.write_expr(&**e),
}
}
}
#[cfg(test)]
mod tests {
use keelson_sqlcheck::testing::assert_frag_sql;
use super::*;
use crate::dialect::testing::{Numbered, TestDialect};
use crate::writer::build;
fn sql(e: &Expr) -> String {
build(&TestDialect, e).expect("render").0
}
fn pg(e: &Expr) -> String {
build(&Numbered, e).expect("render").0
}
const COND: &str = r#"SELECT "id" FROM users WHERE {}"#;
const VALUE: &str = r#"SELECT {} FROM users"#;
const IN_LIST: &str = r#"SELECT "id" FROM users WHERE "id" IN ({})"#;
const IN_GROUP: &str = r#"SELECT "id" FROM users WHERE "id" IN {}"#;
#[test]
fn raw_sql_is_never_parenthesised() {
let e = Expr::raw("age = 1");
assert!(e.is_atomic());
assert_frag_sql(COND, &pg(&e.clone().grouped()), "age = 1");
assert_frag_sql(COND, &pg(&e), "age = 1");
}
#[test]
fn a_template_is_never_parenthesised() {
let e = Expr::template("age = ?", [RawArg::value(1i32)]);
assert!(e.is_atomic());
assert_frag_sql(COND, &pg(&e.clone().grouped()), "age = $1");
assert_eq!(sql(&e.grouped()), "age = ?1");
}
#[test]
fn a_string_literal_is_never_parenthesised() {
let e = Expr::literal("A");
assert!(e.is_atomic());
assert_frag_sql(VALUE, &pg(&e.grouped()), "'A'");
}
#[test]
fn a_quoted_identifier_is_never_parenthesised() {
let e = Expr::ident(("users", "id"));
assert!(e.is_atomic());
assert_frag_sql(VALUE, &pg(&e.grouped()), r#""users"."id""#);
}
#[test]
fn placeholders_are_never_parenthesised() {
for e in [Expr::arg(1i32), Expr::args([1i32, 2]), Expr::named_arg("n")] {
assert!(e.is_atomic(), "{e:?}");
}
assert_frag_sql(IN_LIST, &pg(&Expr::args([1i32, 2]).grouped()), "$1, $2");
}
#[test]
fn a_group_is_not_parenthesised_twice() {
let e = Expr::group(Expr::binary(Expr::ident("age"), "=", Expr::arg(1i32)));
assert!(e.is_atomic());
assert_frag_sql(COND, &pg(&e.grouped()), r#"("age" = $1)"#);
}
#[test]
fn every_operator_shape_is_parenthesised() {
let conditions: Vec<(Expr, &str)> = vec![
(
Expr::binary(Expr::ident("age"), "=", Expr::arg(1i32)),
r#"("age" = $1)"#,
),
(
Expr::prefix("NOT", Expr::ident("is_active")),
r#"(NOT "is_active")"#,
),
(
Expr::postfix(Expr::ident("age"), "IS NULL"),
r#"("age" IS NULL)"#,
),
];
for (e, expected) in conditions {
assert!(!e.is_atomic(), "{e:?} should not be atomic");
assert_frag_sql(COND, &pg(&e.grouped()), expected);
}
let values: Vec<(Expr, &str)> = vec![
(Expr::func("NOW", ()), "(NOW())"),
(
Expr::cast(Expr::ident("age"), "int"),
r#"(CAST("age" AS int))"#,
),
(
Expr::Case {
whens: vec![(Expr::raw("age > 1"), Expr::literal("x"))],
else_: None,
},
"(CASE WHEN age > 1 THEN 'x' END)",
),
];
for (e, expected) in values {
assert!(!e.is_atomic(), "{e:?} should not be atomic");
assert_frag_sql(VALUE, &pg(&e.grouped()), expected);
}
let sort_key = Expr::join([Expr::ident("age"), Expr::raw("DESC")]);
assert!(!sort_key.is_atomic());
assert_eq!(pg(&sort_key.grouped()), r#"("age" DESC)"#);
}
#[test]
fn a_custom_expression_is_parenthesised_because_core_cannot_see_inside_it() {
#[derive(Debug)]
struct Opaque;
impl Expression for Opaque {
fn write_sql(&self, w: &mut SqlWriter<'_>) {
w.push_str("ARRAY[1] <@ ARRAY[1, 2]");
}
}
let e = Expr::custom(Opaque);
assert!(!e.is_atomic());
assert_frag_sql(COND, &pg(&e.grouped()), "(ARRAY[1] <@ ARRAY[1, 2])");
}
#[test]
fn grouping_is_idempotent_which_is_what_keeps_operators_from_nesting_parens() {
let once = Expr::binary(Expr::ident("age"), "=", Expr::arg(1i32)).grouped();
let twice = once.clone().grouped();
assert_frag_sql(COND, &pg(&once), r#"("age" = $1)"#);
assert_eq!(pg(&once), pg(&twice));
}
#[test]
fn an_empty_identifier_renders_nothing_and_empty_parts_are_dropped() {
assert_eq!(sql(&Expr::ident(Vec::<String>::new())), "");
assert_frag_sql(VALUE, &pg(&Expr::ident(["", "id"])), r#""id""#);
assert!(matches!(Expr::ident(["", "id"]), Expr::Ident(p) if p.len() == 1));
}
#[test]
fn an_empty_argument_list_renders_null() {
assert_frag_sql(IN_LIST, &pg(&Expr::args(Vec::<i32>::new())), "NULL");
}
#[test]
fn an_empty_group_renders_a_null_row() {
assert_frag_sql(IN_GROUP, &pg(&Expr::Group(vec![])), "(NULL)");
}
#[test]
fn placeholders_bind_null_and_keep_their_positions() {
let (s, args) = build(&Numbered, &Expr::placeholders(3)).unwrap();
assert_frag_sql(IN_LIST, &s, "$1, $2, $3");
assert!(args.iter().all(Value::is_null));
}
#[test]
fn a_named_argument_binds_nothing_and_fails_where_unsupported() {
let (s, args) = build(&TestDialect, &Expr::named_arg("name")).unwrap();
assert_eq!(s, ":name");
assert!(args.is_empty());
assert!(matches!(
build(&Numbered, &Expr::named_arg("name")),
Err(crate::Error::NoNamedArgs)
));
}
#[test]
fn a_function_call_renders_its_arguments_and_window() {
assert_frag_sql(VALUE, &pg(&Expr::func("NOW", ())), "NOW()");
assert_frag_sql(
"SELECT {} OVER () FROM posts",
&pg(&Expr::func(
"LEAD",
("published_at", 1, Expr::func("NOW", ())),
)),
"LEAD(published_at, 1, NOW())",
);
assert_frag_sql(
VALUE,
&pg(&Expr::Func {
name: "row_number".into(),
args: vec![],
over: Some(Box::new(Expr::raw(""))),
}),
"row_number() OVER ()",
);
}
#[test]
fn case_renders_both_with_and_without_an_else() {
let with_else = Expr::Case {
whens: vec![(
Expr::binary(Expr::ident("id"), "=", Expr::literal("1")).grouped(),
Expr::literal("A"),
)],
else_: Some(Box::new(Expr::literal("B"))),
};
assert_frag_sql(
VALUE,
&pg(&with_else),
r#"CASE WHEN ("id" = '1') THEN 'A' ELSE 'B' END"#,
);
let without = Expr::Case {
whens: vec![(Expr::raw("age > 1"), Expr::literal("A"))],
else_: None,
};
assert_frag_sql(VALUE, &pg(&without), "CASE WHEN age > 1 THEN 'A' END");
}
#[test]
fn a_case_with_no_branches_is_a_recorded_failure_not_a_broken_fragment() {
let empty = Expr::Case {
whens: vec![],
else_: None,
};
let err = build(&TestDialect, &empty).unwrap_err();
assert!(
matches!(&err, crate::Error::Incomplete(what) if what.contains("CASE WHEN")),
"got: {err}"
);
}
#[test]
fn join_uses_its_separator_verbatim() {
let parts = [Expr::raw("a"), Expr::raw("b")];
assert_eq!(sql(&Expr::join(parts.clone())), "a b");
assert_eq!(sql(&Expr::join_with(" || ", parts.clone())), "a || b");
assert_eq!(sql(&Expr::join_with("", parts)), "ab");
}
#[test]
fn an_empty_join_renders_nothing_which_is_how_a_clause_omits_itself() {
assert_eq!(sql(&Expr::join(Vec::<Expr>::new())), "");
}
#[test]
fn nested_arguments_are_numbered_in_write_order() {
let e = Expr::binary(
Expr::group(Expr::args([1i32, 2])),
"IN",
Expr::group([Expr::group(Expr::args([3i32, 4])), Expr::arg(5i32)]),
);
let (s, args) = build(&Numbered, &e).unwrap();
assert_eq!(s, "($1, $2) IN (($3, $4), $5)");
assert_eq!(args.len(), 5);
}
}