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}