Skip to main content

floz_orm/query/
delete.rs

1//! DELETE query builder.
2//!
3//! ```ignore
4//! let (sql, params) = DeleteQuery::new("users")
5//!     .where_(col_id.eq(1))
6//!     .to_sql()?;
7//! // "DELETE FROM users WHERE id = $1"
8//! ```
9
10use std::fmt::Write;
11
12use crate::error::FlozError;
13use crate::expr::Expr;
14use crate::value::Value;
15use super::quote_table;
16
17/// A DELETE query builder.
18///
19/// Safety: `to_sql()` returns `FlozError::UnguardedDelete` if neither `.where_()` nor
20/// `.all()` is called, to prevent accidental table-wide deletes.
21#[derive(Debug, Clone)]
22pub struct DeleteQuery {
23    table: String,
24    where_clause: Option<Expr>,
25    all: bool,
26    returning: ReturningClause,
27}
28
29#[derive(Debug, Clone)]
30enum ReturningClause {
31    None,
32    All,
33    Cols(Vec<String>),
34}
35
36impl DeleteQuery {
37    /// Create a new DELETE query for the given table.
38    pub fn new(table: &str) -> Self {
39        Self {
40            table: table.to_string(),
41            where_clause: None,
42            all: false,
43            returning: ReturningClause::None,
44        }
45    }
46
47    /// Add a WHERE clause.
48    pub fn where_(mut self, expr: Expr) -> Self {
49        self.where_clause = Some(expr);
50        self
51    }
52
53    /// Explicitly permit deleting all rows (without WHERE).
54    /// This is a safety mechanism — you must explicitly opt in.
55    pub fn all(mut self) -> Self {
56        self.all = true;
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::UnguardedDelete` if neither `.where_()` nor `.all()` is set.
77    pub fn to_sql(&self) -> Result<(String, Vec<Value>), FlozError> {
78        if self.where_clause.is_none() && !self.all {
79            return Err(FlozError::UnguardedDelete);
80        }
81
82        let mut sql = String::with_capacity(64);
83        let mut params = Vec::new();
84        let mut idx = 0usize;
85
86        // DELETE FROM table
87        write!(sql, "DELETE FROM {}", quote_table(&self.table)).unwrap();
88
89        // WHERE
90        if let Some(ref expr) = self.where_clause {
91            sql.push_str(" WHERE ");
92            expr.to_sql(&mut sql, &mut params, &mut idx);
93        }
94
95        // RETURNING
96        match &self.returning {
97            ReturningClause::None => {}
98            ReturningClause::All => sql.push_str(" RETURNING *"),
99            ReturningClause::Cols(cols) => {
100                sql.push_str(" RETURNING ");
101                for (i, col) in cols.iter().enumerate() {
102                    if i > 0 { sql.push_str(", "); }
103                    sql.push_str(col);
104                }
105            }
106        }
107
108        Ok((sql, params))
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115    use crate::column::Column;
116
117    fn col_id() -> Column<i32> { Column::new("id", "users") }
118    fn col_age() -> Column<i32> { Column::new("age", "users") }
119
120    #[test]
121    fn delete_with_where() {
122        let (sql, params) = DeleteQuery::new("users")
123            .where_(col_id().eq(1))
124            .to_sql()
125            .unwrap();
126        assert_eq!(sql, "DELETE FROM users WHERE id = $1");
127        assert_eq!(params, vec![Value::Int(1)]);
128    }
129
130    #[test]
131    fn delete_complex_where() {
132        let (sql, params) = DeleteQuery::new("users")
133            .where_(col_age().lt(18).or(col_age().gt(100)))
134            .to_sql()
135            .unwrap();
136        assert_eq!(sql, "DELETE FROM users WHERE (age < $1 OR age > $2)");
137        assert_eq!(params.len(), 2);
138    }
139
140    #[test]
141    fn delete_all() {
142        let (sql, params) = DeleteQuery::new("users")
143            .all()
144            .to_sql()
145            .unwrap();
146        assert_eq!(sql, "DELETE FROM users");
147        assert!(params.is_empty());
148    }
149
150    #[test]
151    fn delete_unguarded_error() {
152        let result = DeleteQuery::new("users").to_sql();
153        assert!(matches!(result, Err(FlozError::UnguardedDelete)));
154    }
155
156    #[test]
157    fn delete_returning() {
158        let (sql, _) = DeleteQuery::new("users")
159            .where_(col_id().eq(1))
160            .returning_all()
161            .to_sql()
162            .unwrap();
163        assert!(sql.ends_with("RETURNING *"));
164    }
165
166    #[test]
167    fn delete_returning_cols() {
168        let (sql, _) = DeleteQuery::new("users")
169            .where_(col_id().eq(1))
170            .returning_cols(&["id", "name"])
171            .to_sql()
172            .unwrap();
173        assert!(sql.ends_with("RETURNING id, name"));
174    }
175
176    #[test]
177    fn delete_schema_qualified() {
178        let (sql, _) = DeleteQuery::new("auth.users")
179            .all()
180            .to_sql()
181            .unwrap();
182        assert_eq!(sql, r#"DELETE FROM "auth"."users""#);
183    }
184}