Skip to main content

qraft_core/builder/
delete.rs

1//! Delete statement builders.
2
3use crate::{
4    HasDialect, ModelQueryPolicy, Qrafting, Query, QueryOf,
5    builder::Update,
6    cte::IntoCtes,
7    emitter::Emitter,
8    query::{LowerFilter, Table, TypedCompiled, rewrite_params},
9};
10
11type SoftDeleteFn<T> = fn(Query) -> Option<Update<T>>;
12
13/// A typed delete statement under construction.
14pub struct Delete<T> {
15    /// Delete target table.
16    pub table: Table<T>,
17    /// Shared query state used for filters, CTEs, and parameters.
18    pub query: Query,
19    soft_delete: Option<SoftDeleteFn<T>>,
20}
21
22/// Starts a delete statement for the given table.
23///
24/// # Examples
25///
26/// ```rust
27/// use qraft_core::{BigInt, Sqlite, builder::delete::delete_from, expression::col, query::Table};
28///
29/// let users = Table::<()>::new("users");
30/// let sql = delete_from(users)
31///     .filter(col::<BigInt>("id").eq(3_i64))
32///     .to_debug_sql::<Sqlite>();
33///
34/// assert_eq!(sql, r#"delete from "users" where "id" = ?; params=[3]"#);
35/// ```
36pub fn delete_from<T>(table: Table<T>) -> Delete<T> {
37    Delete {
38        table,
39        query: Query::from(table),
40        soft_delete: None,
41    }
42}
43
44impl<T> Delete<T> {
45    /// Attaches a common table expression to the delete.
46    pub fn with<C>(mut self, ctes: C) -> Self
47    where
48        C: IntoCtes,
49    {
50        self.query = self.query.with(ctes);
51        self
52    }
53
54    /// Attaches a recursive common table expression to the delete.
55    pub fn with_recursive<C>(mut self, ctes: C) -> Self
56    where
57        C: IntoCtes,
58    {
59        self.query = self.query.with_recursive(ctes);
60        self
61    }
62
63    /// Adds a `where` predicate combined with `and`.
64    pub fn filter<F>(mut self, clause: F) -> Self
65    where
66        F: LowerFilter,
67    {
68        self.query = self.query.filter(clause);
69        self
70    }
71
72    /// Adds a `where` predicate combined with `or`.
73    pub fn or_filter<F>(mut self, clause: F) -> Self
74    where
75        F: LowerFilter,
76    {
77        self.query = self.query.or_filter(clause);
78        self
79    }
80
81    /// Compiles the delete into the typed representation used by executors.
82    pub fn into_compiled<D: HasDialect>(mut self) -> TypedCompiled<T> {
83        if let Some(soft_delete) = self.soft_delete
84            && let Some(update) = soft_delete(self.query.clone())
85        {
86            return update.into_compiled::<D>();
87        }
88
89        let sql = self.to_sql::<D>();
90
91        TypedCompiled {
92            sql,
93            params: self.query.params,
94            data: self.query.data,
95            marker: std::marker::PhantomData,
96        }
97    }
98
99    /// Emits SQL with appended debug parameter output.
100    pub fn to_debug_sql<D: HasDialect>(&mut self) -> String {
101        if let Some(soft_delete) = self.soft_delete
102            && let Some(mut update) = soft_delete(self.query.clone())
103        {
104            return update.to_debug_sql::<D>();
105        }
106
107        let mut sql = self.to_sql::<D>();
108        self.query.debug_params(&mut sql).unwrap();
109        sql
110    }
111
112    /// Emits SQL for the requested dialect.
113    pub fn to_sql<D: HasDialect>(&mut self) -> String {
114        if let Some(soft_delete) = self.soft_delete
115            && let Some(mut update) = soft_delete(self.query.clone())
116        {
117            return update.to_sql::<D>();
118        }
119
120        let mut writer = String::new();
121        let mut directives = Vec::new();
122        let mut indexes = Vec::new();
123
124        let mut emitter = Emitter::new(
125            &mut writer,
126            &self.query.data,
127            D::DIALECT,
128            &mut directives,
129            &mut indexes,
130        );
131
132        emitter.emit_delete(self).unwrap();
133
134        rewrite_params(&indexes, &mut self.query.params);
135
136        writer
137    }
138}
139
140impl<M> QueryOf<M>
141where
142    M: Qrafting,
143{
144    pub fn delete_query(self) -> Delete<M> {
145        let table = Table::new(M::TABLE);
146        Delete {
147            table,
148            query: self.into(),
149            soft_delete: Some(<M::QueryPolicy as ModelQueryPolicy<M>>::soft_delete),
150        }
151    }
152
153    pub async fn delete<E>(self, exec: E) -> quex::Result<quex::ExecResult>
154    where
155        E: quex::Executor,
156    {
157        self.delete_query().execute(exec).await
158    }
159
160    pub fn force_delete_query(self) -> Delete<M> {
161        let table = Table::new(M::TABLE);
162        Delete {
163            table,
164            query: self.into(),
165            soft_delete: None,
166        }
167    }
168
169    pub async fn force_delete<E>(self, exec: E) -> quex::Result<quex::ExecResult>
170    where
171        E: quex::Executor,
172    {
173        self.force_delete_query().execute(exec).await
174    }
175
176    pub fn restore_query(self) -> Update<M> {
177        <M::QueryPolicy as ModelQueryPolicy<M>>::restore(self.into())
178            .expect("QueryOf::restore_query() is only available for soft-deletable qraft models")
179    }
180
181    pub async fn restore<E>(self, exec: E) -> quex::Result<quex::ExecResult>
182    where
183        E: quex::Executor,
184    {
185        self.restore_query().execute(exec).await
186    }
187}
188
189impl<T> Delete<T> {
190    pub fn force(mut self) -> Self {
191        self.soft_delete = None;
192        self
193    }
194}
195
196impl<T> Delete<T> {
197    fn from_query(table: Table<T>, query: Query) -> Self {
198        Self {
199            table,
200            query,
201            soft_delete: None,
202        }
203    }
204}
205
206impl Query {
207    pub fn delete_query(self) -> Delete<()> {
208        let table = self
209            .base_table_static()
210            .expect("Query::delete() requires a base table created from a static table source");
211        Delete::from_query(Table::new(table), self)
212    }
213
214    pub async fn delete<E>(self, exec: E) -> quex::Result<quex::ExecResult>
215    where
216        E: quex::Executor,
217    {
218        self.delete_query().execute(exec).await
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225    use crate::{
226        Sqlite,
227        expression::{Col, PredicateExt},
228        tests::{User, id, table},
229    };
230
231    #[test]
232    fn test_simple_delete_stmt() {
233        let stmt = delete_from(table).to_debug_sql::<Sqlite>();
234        assert_eq!(stmt, r#"delete from "users"; params=[]"#);
235    }
236
237    #[test]
238    fn test_filter_delete_stmt() {
239        let stmt = delete_from(table)
240            .filter("id".bigint().eq(10))
241            .to_debug_sql::<Sqlite>();
242        assert_eq!(stmt, r#"delete from "users" where "id" = ?; params=[10]"#);
243    }
244
245    #[test]
246    fn test_filter_complex_delete_stmt() {
247        let stmt = delete_from(table)
248            .filter("id".bigint().eq(10).or("name".bigint().eq(1)))
249            .filter("username".bigint().eq(10))
250            .to_debug_sql::<Sqlite>();
251        assert_eq!(
252            stmt,
253            r#"delete from "users" where ("id" = ? or "name" = ?) and "username" = ?; params=[10, 1, 10]"#
254        );
255    }
256
257    #[test]
258    fn test_delete_query() {
259        let stmt = Query::from("users")
260            .filter(id.eq(1))
261            .typed::<User>()
262            .delete_query()
263            .to_debug_sql::<Sqlite>();
264
265        assert_eq!(
266            stmt,
267            r#"delete from "users" where "users"."id" = ?; params=[1]"#
268        );
269    }
270
271    #[test]
272    fn test_delete_or_filter_combines_with_existing_where_clause() {
273        let stmt = delete_from(table)
274            .filter(id.eq(1))
275            .or_filter(id.eq(2))
276            .to_debug_sql::<Sqlite>();
277
278        assert_eq!(
279            stmt,
280            r#"delete from "users" where "users"."id" = ? or "users"."id" = ?; params=[1, 2]"#
281        );
282    }
283}