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
12pub 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 pub fn schema(mut self, schema: &str) -> Self {
41 self.parts.schema_override = Some(schema.to_string());
42 self
43 }
44
45 pub fn select(mut self) -> Self {
47 self.parts.returning = Some("*".to_string());
48 self
49 }
50
51 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 #[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 #[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#[cfg(not(feature = "direct-sql"))]
244impl<T> DeleteBuilder<T>
245where
246 T: DeserializeOwned + Send,
247{
248 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#[cfg(feature = "direct-sql")]
267impl<T> DeleteBuilder<T>
268where
269 T: DeserializeOwned + Send + Unpin + for<'r> sqlx::FromRow<'r, sqlx::postgres::PgRow>,
270{
271 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}