Skip to main content

floz_orm/query/
update.rs

1//! UPDATE query builder.
2//!
3//! ```ignore
4//! let (sql, params) = UpdateQuery::new("users")
5//!     .set("age", Value::Short(31))
6//!     .where_(col_id.eq(1))
7//!     .to_sql()?;
8//! // "UPDATE users SET age = $1 WHERE id = $2"
9//! ```
10
11use std::fmt::Write;
12
13use crate::error::FlozError;
14use crate::expr::Expr;
15use crate::value::Value;
16use super::quote_table;
17
18/// An UPDATE query builder.
19///
20/// Safety: `to_sql()` returns `FlozError::UnguardedUpdate` if no WHERE clause is set,
21/// to prevent accidental table-wide updates.
22#[derive(Debug, Clone)]
23pub struct UpdateQuery {
24    table: String,
25    sets: Vec<(String, Value)>,
26    where_clause: Option<Expr>,
27    returning: ReturningClause,
28}
29
30#[derive(Debug, Clone)]
31enum ReturningClause {
32    None,
33    All,
34    Cols(Vec<String>),
35}
36
37impl UpdateQuery {
38    /// Create a new UPDATE query for the given table.
39    pub fn new(table: &str) -> Self {
40        Self {
41            table: table.to_string(),
42            sets: Vec::new(),
43            where_clause: None,
44            returning: ReturningClause::None,
45        }
46    }
47
48    /// Set a column to a value. Chainable.
49    pub fn set(mut self, column: &str, value: impl Into<Value>) -> Self {
50        self.sets.push((column.to_string(), value.into()));
51        self
52    }
53
54    /// Add a WHERE clause.
55    pub fn where_(mut self, expr: Expr) -> Self {
56        self.where_clause = Some(expr);
57        self
58    }
59
60    /// Add RETURNING *.
61    pub fn returning_all(mut self) -> Self {
62        self.returning = ReturningClause::All;
63        self
64    }
65
66    /// Add RETURNING for specific columns.
67    pub fn returning_cols(mut self, columns: &[&str]) -> Self {
68        self.returning = ReturningClause::Cols(
69            columns.iter().map(|c| c.to_string()).collect(),
70        );
71        self
72    }
73
74    /// Render to SQL string + parameters.
75    ///
76    /// Returns `FlozError::UnguardedUpdate` if no WHERE clause is set.
77    pub fn to_sql(&self) -> Result<(String, Vec<Value>), FlozError> {
78        if self.where_clause.is_none() {
79            return Err(FlozError::UnguardedUpdate);
80        }
81
82        let mut sql = String::with_capacity(128);
83        let mut params = Vec::new();
84        let mut idx = 0usize;
85
86        // UPDATE table SET
87        write!(sql, "UPDATE {} SET ", quote_table(&self.table)).unwrap();
88
89        // SET col = $N, ...
90        for (i, (col, val)) in self.sets.iter().enumerate() {
91            if i > 0 { sql.push_str(", "); }
92            idx += 1;
93            write!(sql, "{} = ${}", col, idx).unwrap();
94            params.push(val.clone());
95        }
96
97        // WHERE
98        if let Some(ref expr) = self.where_clause {
99            sql.push_str(" WHERE ");
100            expr.to_sql(&mut sql, &mut params, &mut idx);
101        }
102
103        // RETURNING
104        match &self.returning {
105            ReturningClause::None => {}
106            ReturningClause::All => sql.push_str(" RETURNING *"),
107            ReturningClause::Cols(cols) => {
108                sql.push_str(" RETURNING ");
109                for (i, col) in cols.iter().enumerate() {
110                    if i > 0 { sql.push_str(", "); }
111                    sql.push_str(col);
112                }
113            }
114        }
115
116        Ok((sql, params))
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use crate::column::Column;
124
125    fn col_id() -> Column<i32> { Column::new("id", "users") }
126    fn col_age() -> Column<i32> { Column::new("age", "users") }
127
128    #[test]
129    fn update_basic() {
130        let (sql, params) = UpdateQuery::new("users")
131            .set("age", 31i16)
132            .where_(col_id().eq(1))
133            .to_sql()
134            .unwrap();
135        assert_eq!(sql, "UPDATE users SET age = $1 WHERE id = $2");
136        assert_eq!(params, vec![Value::Short(31), Value::Int(1)]);
137    }
138
139    #[test]
140    fn update_multiple_sets() {
141        let (sql, params) = UpdateQuery::new("users")
142            .set("name", "Updated")
143            .set("age", 31i16)
144            .where_(col_id().eq(1))
145            .to_sql()
146            .unwrap();
147        assert_eq!(sql, "UPDATE users SET name = $1, age = $2 WHERE id = $3");
148        assert_eq!(params.len(), 3);
149    }
150
151    #[test]
152    fn update_returning() {
153        let (sql, _) = UpdateQuery::new("users")
154            .set("age", 31i16)
155            .where_(col_id().eq(1))
156            .returning_all()
157            .to_sql()
158            .unwrap();
159        assert!(sql.ends_with("RETURNING *"));
160    }
161
162    #[test]
163    fn update_returning_cols() {
164        let (sql, _) = UpdateQuery::new("users")
165            .set("age", 31i16)
166            .where_(col_id().eq(1))
167            .returning_cols(&["id", "age"])
168            .to_sql()
169            .unwrap();
170        assert!(sql.ends_with("RETURNING id, age"));
171    }
172
173    #[test]
174    fn update_unguarded_error() {
175        let result = UpdateQuery::new("users")
176            .set("age", 31i16)
177            .to_sql();
178        assert!(matches!(result, Err(FlozError::UnguardedUpdate)));
179    }
180
181    #[test]
182    fn update_complex_where() {
183        let (sql, params) = UpdateQuery::new("users")
184            .set("is_active", false)
185            .where_(col_age().lt(18).or(col_age().gt(65)))
186            .to_sql()
187            .unwrap();
188        assert_eq!(
189            sql,
190            "UPDATE users SET is_active = $1 WHERE (age < $2 OR age > $3)"
191        );
192        assert_eq!(params.len(), 3);
193    }
194
195    #[test]
196    fn update_schema_qualified() {
197        let (sql, _) = UpdateQuery::new("auth.users")
198            .set("age", 31i16)
199            .where_(col_id().eq(1))
200            .to_sql()
201            .unwrap();
202        assert!(sql.starts_with("UPDATE \"auth\".\"users\""));
203    }
204}