Skip to main content

supabase_client_query/
insert.rs

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