Skip to main content

keelson_core/clause/
where_.rs

1use crate::expr::{Expr, IntoExpr};
2use crate::writer::{Expression, SqlWriter};
3
4/// `WHERE a AND b AND c`
5///
6/// Conditions accumulate and are always joined with `AND`. `OR` is built inside a
7/// single condition — [`expr::or`](crate::expr::or) — because a clause that could
8/// switch its own connective would make `append_where` mean something different
9/// depending on call order.
10#[derive(Debug, Clone, Default)]
11pub struct Where {
12    /// The conditions, in the order they were appended.
13    pub conditions: Vec<Expr>,
14}
15
16impl Where {
17    /// Append one condition.
18    pub fn append_where(&mut self, condition: impl IntoExpr) {
19        self.conditions.push(condition.into_expr());
20    }
21
22    /// Whether the clause is absent.
23    pub fn is_empty(&self) -> bool {
24        self.conditions.is_empty()
25    }
26}
27
28impl Expression for Where {
29    fn write_sql(&self, w: &mut SqlWriter<'_>) {
30        w.write_slice(&self.conditions, "WHERE ", " AND ", "");
31    }
32}
33
34/// Anything with a `WHERE` clause.
35///
36/// Implemented by [`Where`] itself, and — because `ON CONFLICT` nests two of them
37/// — by [`ConflictClause`](super::ConflictClause) and
38/// [`ConflictTarget`](super::ConflictTarget), so that a `where_` mod written once
39/// applies in all three places.
40pub trait HasWhere {
41    /// The `WHERE` clause to modify.
42    fn where_mut(&mut self) -> &mut Where;
43}
44
45impl HasWhere for Where {
46    fn where_mut(&mut self) -> &mut Where {
47        self
48    }
49}
50
51#[cfg(test)]
52mod tests {
53    use keelson_sqlcheck::testing::assert_frag_sql;
54
55    use super::*;
56    use crate::dialect::testing::Numbered;
57    use crate::expr::{Chain, arg, or, quote};
58    use crate::value::Value;
59    use crate::writer::build;
60
61    /// A `WHERE` clause is not a statement, so every case below is judged inside
62    /// the statement it belongs to. The names are `tests/schema/psql.sql`'s,
63    /// because a real engine resolves them.
64    const FRAME: &str = r#"SELECT "id" FROM users {}"#;
65
66    fn sql(e: &impl Expression) -> String {
67        build(&Numbered, e).expect("render").0
68    }
69
70    #[test]
71    fn an_empty_where_writes_nothing_not_even_the_keyword() {
72        // PostgreSQL 17 sql-select: `[ WHERE condition ]`. The keyword is inside
73        // the brackets, so an absent clause takes it along.
74        let (sql, args) = build(&Numbered, &Where::default()).unwrap();
75        assert_frag_sql(FRAME, &sql, "");
76        assert!(args.is_empty());
77        assert!(Where::default().is_empty());
78    }
79
80    #[test]
81    fn conditions_are_and_joined_and_numbered_in_order() {
82        let mut wh = Where::default();
83        wh.append_where(quote("age").eq(arg(30i32)));
84        wh.append_where(quote("name").eq(arg("kubo")));
85        // Progressive enhancement: a hand-written fragment is a condition too.
86        wh.append_where("email IS NULL");
87
88        let (sql, args) = build(&Numbered, &wh).unwrap();
89        assert_frag_sql(
90            FRAME,
91            &sql,
92            r#"WHERE ("age" = $1) AND ("name" = $2) AND email IS NULL"#,
93        );
94        assert_eq!(args, vec![Value::I32(30), Value::Text("kubo".into())]);
95    }
96
97    #[test]
98    fn a_disjunction_is_one_condition() {
99        let mut wh = Where::default();
100        wh.append_where(or((
101            quote("age").eq(arg(30i32)),
102            quote("name").eq(arg("kubo")),
103        )));
104        assert_frag_sql(FRAME, &sql(&wh), r#"WHERE (("age" = $1) OR ("name" = $2))"#);
105    }
106}