Skip to main content

keelson_core/clause/
set.rs

1use crate::expr::{Expr, IntoExpr, IntoExprList};
2use crate::writer::{Expression, SqlWriter};
3
4/// The assignment list of an `UPDATE`, or of `DO UPDATE` / `ON DUPLICATE KEY
5/// UPDATE`.
6///
7/// **The `SET` keyword is not written here**, and this is the one clause in the
8/// module that does not write its own. The reason is grammatical: MySQL's
9/// `INSERT … ON DUPLICATE KEY UPDATE col = val` takes exactly this list with no
10/// `SET` in front of it, while `UPDATE t SET …` and PostgreSQL's
11/// `DO UPDATE SET …` both need one. Whatever contains the list therefore supplies
12/// the keyword.
13///
14/// An assignment is a whole expression rather than a column/value pair, because
15/// PostgreSQL's multi-column form `(a, b) = (SELECT x, y FROM …)` is one
16/// assignment with a row on each side, and a pair could not hold it.
17#[derive(Debug, Clone, Default)]
18pub struct Set {
19    /// The assignments, in order.
20    pub exprs: Vec<Expr>,
21}
22
23impl Set {
24    /// Append one assignment.
25    pub fn append_set(&mut self, expr: impl IntoExpr) {
26        self.exprs.push(expr.into_expr());
27    }
28
29    /// Append several assignments.
30    pub fn append_sets(&mut self, exprs: impl IntoExprList) {
31        self.exprs.extend(exprs.into_expr_list());
32    }
33
34    /// Whether the clause is absent.
35    pub fn is_empty(&self) -> bool {
36        self.exprs.is_empty()
37    }
38}
39
40impl Expression for Set {
41    fn write_sql(&self, w: &mut SqlWriter<'_>) {
42        w.write_slice(&self.exprs, "", ", ", "");
43    }
44}
45
46/// Anything with an assignment list: an `UPDATE`, or a
47/// [`ConflictClause`](super::ConflictClause).
48pub trait HasSet {
49    /// The assignment list to modify.
50    fn set_mut(&mut self) -> &mut Set;
51}
52
53impl HasSet for Set {
54    fn set_mut(&mut self) -> &mut Set {
55        self
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use keelson_sqlcheck::testing::assert_frag_sql;
62
63    use super::*;
64    use crate::dialect::testing::Numbered;
65    use crate::expr::{Chain, arg, quote};
66    use crate::value::Value;
67    use crate::writer::build;
68
69    /// The assignment list carries no `SET` keyword, so the frame supplies it.
70    const FRAME: &str = "UPDATE users SET {} WHERE \"id\" = 1";
71
72    #[test]
73    fn an_empty_set_writes_nothing() {
74        // Not framed: `UPDATE users SET` with nothing after it is not a statement
75        // in any dialect, so there is no statement for an empty assignment list to
76        // be judged inside. What it renders is all there is to check.
77        assert_eq!(build(&Numbered, &Set::default()).unwrap().0, "");
78        assert!(Set::default().is_empty());
79    }
80
81    #[test]
82    fn assignments_are_comma_separated_and_carry_no_keyword() {
83        let mut s = Set::default();
84        s.append_set(quote("name").eq(arg("kubo")));
85        s.append_set(quote("age").eq(arg(3i32)));
86
87        let (sql, args) = build(&Numbered, &s).unwrap();
88        // The parentheses come from the comparison chain, which is what makes an
89        // assignment and a condition the same kind of thing. That also means this
90        // one cannot be framed as an `UPDATE`: PostgreSQL's `set_clause` is
91        // `column_name = expression`, and `("name" = $1)` is a parenthesised
92        // *comparison*, so a dialect's `set` mod builds an unparenthesised
93        // `"a" = $1` instead. Judged as the select list it *is* valid in.
94        assert_frag_sql(
95            r#"SELECT {} FROM users"#,
96            &sql,
97            r#"("name" = $1), ("age" = $2)"#,
98        );
99        assert_eq!(args, vec![Value::Text("kubo".into()), Value::I32(3)]);
100    }
101
102    #[test]
103    fn a_multi_column_assignment_is_one_assignment() {
104        // PostgreSQL 17 sql-update:
105        //   ( column_name [, ...] ) = ( { expression } [, ...] )
106        let mut s = Set::default();
107        s.append_sets((
108            Expr::binary(
109                Expr::group((quote("name"), quote("email"))),
110                "=",
111                Expr::group((arg("kubo"), arg("kubo@example.com"))),
112            ),
113            Expr::raw(r#""age" = DEFAULT"#),
114        ));
115        let (sql, args) = build(&Numbered, &s).unwrap();
116        assert_frag_sql(
117            FRAME,
118            &sql,
119            r#"("name", "email") = ($1, $2), "age" = DEFAULT"#,
120        );
121        assert_eq!(args.len(), 2);
122    }
123}