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
11pub 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 pub fn schema(mut self, schema: &str) -> Self {
31 self.parts.schema_override = Some(schema.to_string());
32 self
33 }
34
35 pub fn select(mut self) -> Self {
37 self.parts.returning = Some("*".to_string());
38 self
39 }
40
41 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 #[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 #[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 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#[cfg(not(feature = "direct-sql"))]
249impl<T> InsertBuilder<T>
250where
251 T: DeserializeOwned + Send,
252{
253 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#[cfg(feature = "direct-sql")]
272impl<T> InsertBuilder<T>
273where
274 T: DeserializeOwned + Send + Unpin + for<'r> sqlx::FromRow<'r, sqlx::postgres::PgRow>,
275{
276 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}