Skip to main content

supabase_client_query/
select.rs

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
14/// Builder for SELECT queries. Implements both Filterable and Modifiable.
15pub 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    /// Override the schema for this query.
39    pub fn schema(mut self, schema: &str) -> Self {
40        self.parts.schema_override = Some(schema.to_string());
41        self
42    }
43
44    /// Wrap the SELECT in `EXPLAIN (ANALYZE, FORMAT JSON)`.
45    pub fn explain(mut self) -> Self {
46        self.parts.explain = Some(ExplainOptions::default());
47        self
48    }
49
50    /// Wrap the SELECT in EXPLAIN with custom options.
51    pub fn explain_with(mut self, options: ExplainOptions) -> Self {
52        self.parts.explain = Some(options);
53        self
54    }
55
56    /// Switch to head/count-only mode.
57    pub fn head(mut self) -> Self {
58        self.parts.head = true;
59        self
60    }
61
62    /// Switch to CSV output mode. Returns a `CsvSelectBuilder` that
63    /// executes the query and returns the raw CSV text.
64    pub fn csv(self) -> CsvSelectBuilder {
65        CsvSelectBuilder {
66            backend: self.backend,
67            parts: self.parts,
68            params: self.params,
69        }
70    }
71
72    /// Switch to GeoJSON output mode. Returns a `GeoJsonSelectBuilder` that
73    /// executes the query and returns a `serde_json::Value` (GeoJSON FeatureCollection).
74    pub fn geojson(self) -> GeoJsonSelectBuilder {
75        GeoJsonSelectBuilder {
76            backend: self.backend,
77            parts: self.parts,
78            params: self.params,
79        }
80    }
81}
82
83// REST-only mode: only DeserializeOwned + Send needed
84#[cfg(not(feature = "direct-sql"))]
85impl<T> SelectBuilder<T>
86where
87    T: DeserializeOwned + Send,
88{
89    /// Execute the SELECT query and return results.
90    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    // ---- Builder method tests ----
137
138    #[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    // ---- execute() via wiremock ----
188
189    #[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// Direct-SQL mode: additional FromRow + Unpin bounds
327#[cfg(feature = "direct-sql")]
328impl<T> SelectBuilder<T>
329where
330    T: DeserializeOwned + Send + Unpin + for<'r> sqlx::FromRow<'r, sqlx::postgres::PgRow>,
331{
332    /// Execute the SELECT query and return results.
333    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}