Skip to main content

supabase_client_query/
delete.rs

1use std::marker::PhantomData;
2
3use serde::de::DeserializeOwned;
4
5use supabase_client_core::SupabaseResponse;
6
7use crate::backend::QueryBackend;
8use crate::filter::Filterable;
9use crate::modifier::Modifiable;
10use crate::sql::{FilterCondition, ParamStore, SqlParts};
11
12/// Builder for DELETE queries. Implements Filterable and Modifiable.
13/// Call `.select()` to add RETURNING clause.
14pub struct DeleteBuilder<T> {
15    pub(crate) backend: QueryBackend,
16    pub(crate) parts: SqlParts,
17    pub(crate) params: ParamStore,
18    pub(crate) _marker: PhantomData<T>,
19}
20
21impl<T> Filterable for DeleteBuilder<T> {
22    fn filters_mut(&mut self) -> &mut Vec<FilterCondition> {
23        &mut self.parts.filters
24    }
25    fn params_mut(&mut self) -> &mut ParamStore {
26        &mut self.params
27    }
28}
29
30impl<T> Modifiable for DeleteBuilder<T> {
31    fn parts_mut(&mut self) -> &mut SqlParts {
32        &mut self.parts
33    }
34}
35
36impl<T> DeleteBuilder<T> {
37    /// Override the schema for this query.
38    ///
39    /// Generates `"schema"."table"` instead of the default schema.
40    pub fn schema(mut self, schema: &str) -> Self {
41        self.parts.schema_override = Some(schema.to_string());
42        self
43    }
44
45    /// Add RETURNING * clause.
46    pub fn select(mut self) -> Self {
47        self.parts.returning = Some("*".to_string());
48        self
49    }
50
51    /// Add RETURNING with specific columns.
52    pub fn select_columns(mut self, columns: &str) -> Self {
53        if columns == "*" || columns.is_empty() {
54            self.parts.returning = Some("*".to_string());
55        } else {
56            let quoted = columns
57                .split(',')
58                .map(|c| {
59                    let c = c.trim();
60                    if c.contains('(') || c.contains('*') || c.contains('"') {
61                        c.to_string()
62                    } else {
63                        format!("\"{}\"", c)
64                    }
65                })
66                .collect::<Vec<_>>()
67                .join(", ");
68            self.parts.returning = Some(quoted);
69        }
70        self
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77    use crate::backend::QueryBackend;
78    use crate::sql::{ParamStore, SqlOperation, SqlParts};
79    use serde_json::Value as JsonValue;
80    use std::marker::PhantomData;
81    use std::sync::Arc;
82    use wiremock::matchers::{method, path};
83    use wiremock::{Mock, MockServer, ResponseTemplate};
84
85    fn make_delete_builder() -> DeleteBuilder<JsonValue> {
86        DeleteBuilder {
87            backend: QueryBackend::Rest {
88                http: reqwest::Client::new(),
89                base_url: Arc::from("http://localhost"),
90                api_key: Arc::from("test-key"),
91                schema: "public".to_string(),
92            },
93            parts: SqlParts::new(SqlOperation::Delete, "public", "users"),
94            params: ParamStore::new(),
95            _marker: PhantomData,
96        }
97    }
98
99    // ---- Builder method tests ----
100
101    #[test]
102    fn test_schema_sets_override() {
103        let builder = make_delete_builder().schema("custom");
104        assert_eq!(builder.parts.schema_override.as_deref(), Some("custom"));
105    }
106
107    #[test]
108    fn test_select_sets_returning_star() {
109        let builder = make_delete_builder().select();
110        assert_eq!(builder.parts.returning.as_deref(), Some("*"));
111    }
112
113    #[test]
114    fn test_select_columns_star() {
115        let builder = make_delete_builder().select_columns("*");
116        assert_eq!(builder.parts.returning.as_deref(), Some("*"));
117    }
118
119    #[test]
120    fn test_select_columns_empty() {
121        let builder = make_delete_builder().select_columns("");
122        assert_eq!(builder.parts.returning.as_deref(), Some("*"));
123    }
124
125    #[test]
126    fn test_select_columns_specific() {
127        let builder = make_delete_builder().select_columns("id, name");
128        assert_eq!(builder.parts.returning.as_deref(), Some("\"id\", \"name\""));
129    }
130
131    #[test]
132    fn test_select_columns_complex_expression() {
133        let builder = make_delete_builder().select_columns("count(*)");
134        assert_eq!(builder.parts.returning.as_deref(), Some("count(*)"));
135    }
136
137    // ---- execute() via wiremock ----
138
139    #[tokio::test]
140    async fn test_execute_delete_success_with_returning() {
141        let mock_server = MockServer::start().await;
142        Mock::given(method("DELETE"))
143            .and(path("/rest/v1/users"))
144            .respond_with(
145                ResponseTemplate::new(200)
146                    .set_body_json(serde_json::json!([{"id": 1, "name": "Alice"}])),
147            )
148            .mount(&mock_server)
149            .await;
150
151        let mut parts = SqlParts::new(SqlOperation::Delete, "public", "users");
152        parts.returning = Some("*".to_string());
153
154        let builder: DeleteBuilder<JsonValue> = DeleteBuilder {
155            backend: QueryBackend::Rest {
156                http: reqwest::Client::new(),
157                base_url: Arc::from(mock_server.uri().as_str()),
158                api_key: Arc::from("test-key"),
159                schema: "public".to_string(),
160            },
161            parts,
162            params: ParamStore::new(),
163            _marker: PhantomData,
164        };
165
166        let resp = builder.execute().await;
167        assert!(resp.is_ok());
168        assert_eq!(resp.data.len(), 1);
169        assert_eq!(resp.data[0]["id"], 1);
170    }
171
172    #[tokio::test]
173    async fn test_execute_delete_success_no_returning() {
174        let mock_server = MockServer::start().await;
175        Mock::given(method("DELETE"))
176            .and(path("/rest/v1/users"))
177            .respond_with(ResponseTemplate::new(204))
178            .mount(&mock_server)
179            .await;
180
181        let parts = SqlParts::new(SqlOperation::Delete, "public", "users");
182
183        let builder: DeleteBuilder<JsonValue> = DeleteBuilder {
184            backend: QueryBackend::Rest {
185                http: reqwest::Client::new(),
186                base_url: Arc::from(mock_server.uri().as_str()),
187                api_key: Arc::from("test-key"),
188                schema: "public".to_string(),
189            },
190            parts,
191            params: ParamStore::new(),
192            _marker: PhantomData,
193        };
194
195        let resp = builder.execute().await;
196        assert!(resp.is_ok());
197        assert!(resp.data.is_empty());
198    }
199
200    #[tokio::test]
201    async fn test_execute_delete_error() {
202        let mock_server = MockServer::start().await;
203        Mock::given(method("DELETE"))
204            .and(path("/rest/v1/users"))
205            .respond_with(
206                ResponseTemplate::new(403)
207                    .set_body_json(serde_json::json!({
208                        "message": "Permission denied",
209                        "code": "42501"
210                    })),
211            )
212            .mount(&mock_server)
213            .await;
214
215        let parts = SqlParts::new(SqlOperation::Delete, "public", "users");
216
217        let builder: DeleteBuilder<JsonValue> = DeleteBuilder {
218            backend: QueryBackend::Rest {
219                http: reqwest::Client::new(),
220                base_url: Arc::from(mock_server.uri().as_str()),
221                api_key: Arc::from("test-key"),
222                schema: "public".to_string(),
223            },
224            parts,
225            params: ParamStore::new(),
226            _marker: PhantomData,
227        };
228
229        let resp = builder.execute().await;
230        assert!(resp.is_err());
231        match resp.error.as_ref().unwrap() {
232            supabase_client_core::SupabaseError::PostgRest { status, message, code } => {
233                assert_eq!(*status, 403);
234                assert_eq!(message, "Permission denied");
235                assert_eq!(code.as_deref(), Some("42501"));
236            }
237            other => panic!("Expected PostgRest error, got {:?}", other),
238        }
239    }
240}
241
242// REST-only mode: only DeserializeOwned + Send needed
243#[cfg(not(feature = "direct-sql"))]
244impl<T> DeleteBuilder<T>
245where
246    T: DeserializeOwned + Send,
247{
248    /// Execute the DELETE query.
249    pub async fn execute(self) -> SupabaseResponse<T> {
250        let QueryBackend::Rest { ref http, ref base_url, ref api_key, ref schema } = self.backend;
251        let (url, headers) = match crate::postgrest::build_postgrest_delete(
252            base_url, &self.parts, &self.params,
253        ) {
254            Ok(r) => r,
255            Err(e) => return SupabaseResponse::error(
256                supabase_client_core::SupabaseError::QueryBuilder(e),
257            ),
258        };
259        crate::postgrest_execute::execute_rest(
260            http, reqwest::Method::DELETE, &url, headers, None, api_key, schema, &self.parts,
261        ).await
262    }
263}
264
265// Direct-SQL mode: additional FromRow + Unpin bounds
266#[cfg(feature = "direct-sql")]
267impl<T> DeleteBuilder<T>
268where
269    T: DeserializeOwned + Send + Unpin + for<'r> sqlx::FromRow<'r, sqlx::postgres::PgRow>,
270{
271    /// Execute the DELETE query.
272    pub async fn execute(self) -> SupabaseResponse<T> {
273        match &self.backend {
274            QueryBackend::Rest { http, base_url, api_key, schema } => {
275                let (url, headers) = match crate::postgrest::build_postgrest_delete(
276                    base_url, &self.parts, &self.params,
277                ) {
278                    Ok(r) => r,
279                    Err(e) => return SupabaseResponse::error(
280                        supabase_client_core::SupabaseError::QueryBuilder(e),
281                    ),
282                };
283                crate::postgrest_execute::execute_rest(
284                    http, reqwest::Method::DELETE, &url, headers, None, api_key, schema, &self.parts,
285                ).await
286            }
287            QueryBackend::DirectSql { pool } => {
288                crate::execute::execute_typed::<T>(pool, &self.parts, &self.params).await
289            }
290        }
291    }
292}