1use std::marker::PhantomData;
2
3use serde::de::DeserializeOwned;
4
5use supabase_client_core::SupabaseResponse;
6
7use crate::backend::QueryBackend;
8use crate::csv_select::CsvSelectBuilder;
9use crate::filter::Filterable;
10use crate::geojson_select::GeoJsonSelectBuilder;
11use crate::modifier::Modifiable;
12use crate::sql::{ExplainOptions, FilterCondition, ParamStore, SqlParts};
13
14pub struct SelectBuilder<T> {
16 pub(crate) backend: QueryBackend,
17 pub(crate) parts: SqlParts,
18 pub(crate) params: ParamStore,
19 pub(crate) _marker: PhantomData<T>,
20}
21
22impl<T> Filterable for SelectBuilder<T> {
23 fn filters_mut(&mut self) -> &mut Vec<FilterCondition> {
24 &mut self.parts.filters
25 }
26 fn params_mut(&mut self) -> &mut ParamStore {
27 &mut self.params
28 }
29}
30
31impl<T> Modifiable for SelectBuilder<T> {
32 fn parts_mut(&mut self) -> &mut SqlParts {
33 &mut self.parts
34 }
35}
36
37impl<T> SelectBuilder<T> {
38 pub fn schema(mut self, schema: &str) -> Self {
40 self.parts.schema_override = Some(schema.to_string());
41 self
42 }
43
44 pub fn explain(mut self) -> Self {
46 self.parts.explain = Some(ExplainOptions::default());
47 self
48 }
49
50 pub fn explain_with(mut self, options: ExplainOptions) -> Self {
52 self.parts.explain = Some(options);
53 self
54 }
55
56 pub fn head(mut self) -> Self {
58 self.parts.head = true;
59 self
60 }
61
62 pub fn csv(self) -> CsvSelectBuilder {
65 CsvSelectBuilder {
66 backend: self.backend,
67 parts: self.parts,
68 params: self.params,
69 }
70 }
71
72 pub fn geojson(self) -> GeoJsonSelectBuilder {
75 GeoJsonSelectBuilder {
76 backend: self.backend,
77 parts: self.parts,
78 params: self.params,
79 }
80 }
81}
82
83#[cfg(not(feature = "direct-sql"))]
85impl<T> SelectBuilder<T>
86where
87 T: DeserializeOwned + Send,
88{
89 pub async fn execute(self) -> SupabaseResponse<T> {
91 let QueryBackend::Rest { ref http, ref base_url, ref api_key, ref schema } = self.backend;
92 let method = if self.parts.head {
93 reqwest::Method::HEAD
94 } else {
95 reqwest::Method::GET
96 };
97 let (url, headers) = match crate::postgrest::build_postgrest_select(
98 base_url, &self.parts, &self.params,
99 ) {
100 Ok(r) => r,
101 Err(e) => return SupabaseResponse::error(
102 supabase_client_core::SupabaseError::QueryBuilder(e),
103 ),
104 };
105 crate::postgrest_execute::execute_rest(
106 http, method, &url, headers, None, api_key, schema, &self.parts,
107 ).await
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114 use crate::backend::QueryBackend;
115 use crate::sql::{ExplainFormat, ParamStore, SqlOperation, SqlParts};
116 use serde_json::Value as JsonValue;
117 use std::marker::PhantomData;
118 use std::sync::Arc;
119 use wiremock::matchers::{method, path};
120 use wiremock::{Mock, MockServer, ResponseTemplate};
121
122 fn make_select_builder() -> SelectBuilder<JsonValue> {
123 SelectBuilder {
124 backend: QueryBackend::Rest {
125 http: reqwest::Client::new(),
126 base_url: Arc::from("http://localhost"),
127 api_key: Arc::from("test-key"),
128 schema: "public".to_string(),
129 },
130 parts: SqlParts::new(SqlOperation::Select, "public", "users"),
131 params: ParamStore::new(),
132 _marker: PhantomData,
133 }
134 }
135
136 #[test]
139 fn test_schema_sets_override() {
140 let builder = make_select_builder().schema("custom_schema");
141 assert_eq!(builder.parts.schema_override.as_deref(), Some("custom_schema"));
142 }
143
144 #[test]
145 fn test_explain_sets_default_options() {
146 let builder = make_select_builder().explain();
147 let opts = builder.parts.explain.as_ref().unwrap();
148 assert!(opts.analyze);
149 assert!(!opts.verbose);
150 assert_eq!(opts.format, ExplainFormat::Json);
151 }
152
153 #[test]
154 fn test_explain_with_custom_options() {
155 let opts = crate::sql::ExplainOptions {
156 analyze: false,
157 verbose: true,
158 format: ExplainFormat::Text,
159 };
160 let builder = make_select_builder().explain_with(opts);
161 let actual = builder.parts.explain.as_ref().unwrap();
162 assert!(!actual.analyze);
163 assert!(actual.verbose);
164 assert_eq!(actual.format, ExplainFormat::Text);
165 }
166
167 #[test]
168 fn test_head_sets_head_mode() {
169 let builder = make_select_builder().head();
170 assert!(builder.parts.head);
171 }
172
173 #[test]
174 fn test_csv_returns_csv_builder() {
175 let builder = make_select_builder();
176 let csv = builder.csv();
177 assert_eq!(csv.parts.table, "users");
178 }
179
180 #[test]
181 fn test_geojson_returns_geojson_builder() {
182 let builder = make_select_builder();
183 let geo = builder.geojson();
184 assert_eq!(geo.parts.table, "users");
185 }
186
187 #[tokio::test]
190 async fn test_execute_success_json_array() {
191 let mock_server = MockServer::start().await;
192 Mock::given(method("GET"))
193 .and(path("/rest/v1/users"))
194 .respond_with(
195 ResponseTemplate::new(200)
196 .set_body_json(serde_json::json!([
197 {"id": 1, "name": "Alice"},
198 {"id": 2, "name": "Bob"}
199 ])),
200 )
201 .mount(&mock_server)
202 .await;
203
204 let builder: SelectBuilder<JsonValue> = SelectBuilder {
205 backend: QueryBackend::Rest {
206 http: reqwest::Client::new(),
207 base_url: Arc::from(mock_server.uri().as_str()),
208 api_key: Arc::from("test-key"),
209 schema: "public".to_string(),
210 },
211 parts: SqlParts::new(SqlOperation::Select, "public", "users"),
212 params: ParamStore::new(),
213 _marker: PhantomData,
214 };
215
216 let resp = builder.execute().await;
217 assert!(resp.is_ok());
218 assert_eq!(resp.data.len(), 2);
219 assert_eq!(resp.data[0]["name"], "Alice");
220 assert_eq!(resp.data[1]["name"], "Bob");
221 }
222
223 #[tokio::test]
224 async fn test_execute_empty_result() {
225 let mock_server = MockServer::start().await;
226 Mock::given(method("GET"))
227 .and(path("/rest/v1/users"))
228 .respond_with(
229 ResponseTemplate::new(200)
230 .set_body_json(serde_json::json!([])),
231 )
232 .mount(&mock_server)
233 .await;
234
235 let builder: SelectBuilder<JsonValue> = SelectBuilder {
236 backend: QueryBackend::Rest {
237 http: reqwest::Client::new(),
238 base_url: Arc::from(mock_server.uri().as_str()),
239 api_key: Arc::from("test-key"),
240 schema: "public".to_string(),
241 },
242 parts: SqlParts::new(SqlOperation::Select, "public", "users"),
243 params: ParamStore::new(),
244 _marker: PhantomData,
245 };
246
247 let resp = builder.execute().await;
248 assert!(resp.is_ok());
249 assert!(resp.data.is_empty());
250 }
251
252 #[tokio::test]
253 async fn test_execute_error_4xx() {
254 let mock_server = MockServer::start().await;
255 Mock::given(method("GET"))
256 .and(path("/rest/v1/nonexistent"))
257 .respond_with(
258 ResponseTemplate::new(404)
259 .set_body_json(serde_json::json!({
260 "message": "Relation not found",
261 "code": "42P01"
262 })),
263 )
264 .mount(&mock_server)
265 .await;
266
267 let builder: SelectBuilder<JsonValue> = SelectBuilder {
268 backend: QueryBackend::Rest {
269 http: reqwest::Client::new(),
270 base_url: Arc::from(mock_server.uri().as_str()),
271 api_key: Arc::from("test-key"),
272 schema: "public".to_string(),
273 },
274 parts: SqlParts::new(SqlOperation::Select, "public", "nonexistent"),
275 params: ParamStore::new(),
276 _marker: PhantomData,
277 };
278
279 let resp = builder.execute().await;
280 assert!(resp.is_err());
281 match resp.error.as_ref().unwrap() {
282 supabase_client_core::SupabaseError::PostgRest { status, message, code } => {
283 assert_eq!(*status, 404);
284 assert_eq!(message, "Relation not found");
285 assert_eq!(code.as_deref(), Some("42P01"));
286 }
287 other => panic!("Expected PostgRest error, got {:?}", other),
288 }
289 }
290
291 #[tokio::test]
292 async fn test_execute_head_mode() {
293 let mock_server = MockServer::start().await;
294 Mock::given(method("HEAD"))
295 .and(path("/rest/v1/users"))
296 .respond_with(
297 ResponseTemplate::new(200)
298 .insert_header("content-range", "0-9/55"),
299 )
300 .mount(&mock_server)
301 .await;
302
303 let builder: SelectBuilder<JsonValue> = SelectBuilder {
304 backend: QueryBackend::Rest {
305 http: reqwest::Client::new(),
306 base_url: Arc::from(mock_server.uri().as_str()),
307 api_key: Arc::from("test-key"),
308 schema: "public".to_string(),
309 },
310 parts: {
311 let mut p = SqlParts::new(SqlOperation::Select, "public", "users");
312 p.head = true;
313 p
314 },
315 params: ParamStore::new(),
316 _marker: PhantomData,
317 };
318
319 let resp = builder.execute().await;
320 assert!(resp.is_ok());
321 assert!(resp.data.is_empty());
322 assert_eq!(resp.count, Some(55));
323 }
324}
325
326#[cfg(feature = "direct-sql")]
328impl<T> SelectBuilder<T>
329where
330 T: DeserializeOwned + Send + Unpin + for<'r> sqlx::FromRow<'r, sqlx::postgres::PgRow>,
331{
332 pub async fn execute(self) -> SupabaseResponse<T> {
334 match &self.backend {
335 QueryBackend::Rest { http, base_url, api_key, schema } => {
336 let method = if self.parts.head {
337 reqwest::Method::HEAD
338 } else {
339 reqwest::Method::GET
340 };
341 let (url, headers) = match crate::postgrest::build_postgrest_select(
342 base_url, &self.parts, &self.params,
343 ) {
344 Ok(r) => r,
345 Err(e) => return SupabaseResponse::error(
346 supabase_client_core::SupabaseError::QueryBuilder(e),
347 ),
348 };
349 crate::postgrest_execute::execute_rest(
350 http, method, &url, headers, None, api_key, schema, &self.parts,
351 ).await
352 }
353 QueryBackend::DirectSql { pool } => {
354 crate::execute::execute_typed::<T>(pool, &self.parts, &self.params).await
355 }
356 }
357 }
358}