1use reqwest::header::HeaderValue;
2
3use supabase_client_core::SupabaseError;
4
5use crate::backend::QueryBackend;
6use crate::filter::Filterable;
7use crate::modifier::Modifiable;
8use crate::sql::{FilterCondition, ParamStore, SqlParts};
9
10pub struct CsvSelectBuilder {
15 pub(crate) backend: QueryBackend,
16 pub(crate) parts: SqlParts,
17 pub(crate) params: ParamStore,
18}
19
20impl Filterable for CsvSelectBuilder {
21 fn filters_mut(&mut self) -> &mut Vec<FilterCondition> {
22 &mut self.parts.filters
23 }
24 fn params_mut(&mut self) -> &mut ParamStore {
25 &mut self.params
26 }
27}
28
29impl Modifiable for CsvSelectBuilder {
30 fn parts_mut(&mut self) -> &mut SqlParts {
31 &mut self.parts
32 }
33}
34
35impl CsvSelectBuilder {
36 pub fn schema(mut self, schema: &str) -> Self {
38 self.parts.schema_override = Some(schema.to_string());
39 self
40 }
41
42 pub async fn execute(self) -> Result<String, SupabaseError> {
44 match &self.backend {
45 QueryBackend::Rest { http, base_url, api_key, schema } => {
46 let (url, mut headers) = crate::postgrest::build_postgrest_select(
47 base_url, &self.parts, &self.params,
48 )
49 .map_err(SupabaseError::QueryBuilder)?;
50
51 headers.insert("Accept", HeaderValue::from_static("text/csv"));
53
54 headers.insert("apikey", HeaderValue::from_str(api_key).unwrap());
56 headers.insert(
57 "Authorization",
58 HeaderValue::from_str(&format!("Bearer {}", api_key)).unwrap(),
59 );
60
61 if let Some(ref so) = self.parts.schema_override {
63 headers.insert(
64 "Accept-Profile",
65 HeaderValue::from_str(so).unwrap(),
66 );
67 } else if schema != "public" {
68 headers.entry("Accept-Profile")
69 .or_insert_with(|| HeaderValue::from_str(schema).unwrap());
70 }
71
72 let response = http
73 .get(&url)
74 .headers(headers)
75 .send()
76 .await
77 .map_err(|e| SupabaseError::Http(e.to_string()))?;
78
79 let status = response.status().as_u16();
80 let body = response
81 .text()
82 .await
83 .map_err(|e| SupabaseError::Http(e.to_string()))?;
84
85 if status >= 400 {
86 return Err(SupabaseError::postgrest(status, body, None));
87 }
88
89 Ok(body)
90 }
91 #[cfg(feature = "direct-sql")]
92 QueryBackend::DirectSql { .. } => {
93 Err(SupabaseError::query_builder(
94 "CSV output is only supported with the REST (PostgREST) backend",
95 ))
96 }
97 }
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104 use crate::sql::{SqlOperation, SqlParts, ParamStore};
105
106 #[test]
107 fn test_csv_builder_modifiable() {
108 let mut builder = CsvSelectBuilder {
109 backend: QueryBackend::Rest {
110 http: reqwest::Client::new(),
111 base_url: "http://localhost".into(),
112 api_key: "key".into(),
113 schema: "public".to_string(),
114 },
115 parts: SqlParts::new(SqlOperation::Select, "public", "cities"),
116 params: ParamStore::new(),
117 };
118 builder = builder.limit(10);
119 assert_eq!(builder.parts.limit, Some(10));
120 }
121
122 #[test]
123 fn test_csv_builder_filterable() {
124 let builder = CsvSelectBuilder {
125 backend: QueryBackend::Rest {
126 http: reqwest::Client::new(),
127 base_url: "http://localhost".into(),
128 api_key: "key".into(),
129 schema: "public".to_string(),
130 },
131 parts: SqlParts::new(SqlOperation::Select, "public", "cities"),
132 params: ParamStore::new(),
133 };
134 let builder = builder.eq("name", "Auckland");
135 assert_eq!(builder.parts.filters.len(), 1);
136 }
137
138 #[test]
139 fn test_csv_accept_header() {
140 let parts = SqlParts::new(SqlOperation::Select, "public", "cities");
141 let params = ParamStore::new();
142 let (_, mut headers) = crate::postgrest::build_postgrest_select(
143 "http://localhost:64321", &parts, ¶ms,
144 ).unwrap();
145 headers.insert("Accept", HeaderValue::from_static("text/csv"));
147 assert_eq!(headers.get("Accept").unwrap(), "text/csv");
148 }
149
150 #[test]
151 fn test_csv_schema_sets_override() {
152 let builder = CsvSelectBuilder {
153 backend: QueryBackend::Rest {
154 http: reqwest::Client::new(),
155 base_url: "http://localhost".into(),
156 api_key: "key".into(),
157 schema: "public".to_string(),
158 },
159 parts: SqlParts::new(SqlOperation::Select, "public", "cities"),
160 params: ParamStore::new(),
161 };
162 let builder = builder.schema("custom");
163 assert_eq!(builder.parts.schema_override.as_deref(), Some("custom"));
164 }
165
166 #[tokio::test]
169 async fn test_csv_execute_success() {
170 use wiremock::matchers::{method, path};
171 use wiremock::{Mock, MockServer, ResponseTemplate};
172
173 let mock_server = MockServer::start().await;
174 let csv_body = "id,name\n1,Alice\n2,Bob\n";
175 Mock::given(method("GET"))
176 .and(path("/rest/v1/users"))
177 .respond_with(
178 ResponseTemplate::new(200)
179 .set_body_string(csv_body),
180 )
181 .mount(&mock_server)
182 .await;
183
184 let builder = CsvSelectBuilder {
185 backend: QueryBackend::Rest {
186 http: reqwest::Client::new(),
187 base_url: mock_server.uri().into(),
188 api_key: "test-key".into(),
189 schema: "public".to_string(),
190 },
191 parts: SqlParts::new(SqlOperation::Select, "public", "users"),
192 params: ParamStore::new(),
193 };
194
195 let result = builder.execute().await;
196 assert!(result.is_ok());
197 let csv = result.unwrap();
198 assert!(csv.contains("id,name"));
199 assert!(csv.contains("1,Alice"));
200 assert!(csv.contains("2,Bob"));
201 }
202
203 #[tokio::test]
204 async fn test_csv_execute_error() {
205 use wiremock::matchers::{method, path};
206 use wiremock::{Mock, MockServer, ResponseTemplate};
207
208 let mock_server = MockServer::start().await;
209 Mock::given(method("GET"))
210 .and(path("/rest/v1/nonexistent"))
211 .respond_with(
212 ResponseTemplate::new(404)
213 .set_body_string("Relation not found"),
214 )
215 .mount(&mock_server)
216 .await;
217
218 let builder = CsvSelectBuilder {
219 backend: QueryBackend::Rest {
220 http: reqwest::Client::new(),
221 base_url: mock_server.uri().into(),
222 api_key: "test-key".into(),
223 schema: "public".to_string(),
224 },
225 parts: SqlParts::new(SqlOperation::Select, "public", "nonexistent"),
226 params: ParamStore::new(),
227 };
228
229 let result = builder.execute().await;
230 assert!(result.is_err());
231 match result.unwrap_err() {
232 SupabaseError::PostgRest { status, .. } => {
233 assert_eq!(status, 404);
234 }
235 other => panic!("Expected PostgRest error, got {:?}", other),
236 }
237 }
238
239 #[tokio::test]
240 async fn test_csv_execute_empty_result() {
241 use wiremock::matchers::{method, path};
242 use wiremock::{Mock, MockServer, ResponseTemplate};
243
244 let mock_server = MockServer::start().await;
245 Mock::given(method("GET"))
246 .and(path("/rest/v1/users"))
247 .respond_with(
248 ResponseTemplate::new(200)
249 .set_body_string("id,name\n"),
250 )
251 .mount(&mock_server)
252 .await;
253
254 let builder = CsvSelectBuilder {
255 backend: QueryBackend::Rest {
256 http: reqwest::Client::new(),
257 base_url: mock_server.uri().into(),
258 api_key: "test-key".into(),
259 schema: "public".to_string(),
260 },
261 parts: SqlParts::new(SqlOperation::Select, "public", "users"),
262 params: ParamStore::new(),
263 };
264
265 let result = builder.execute().await;
266 assert!(result.is_ok());
267 let csv = result.unwrap();
268 assert!(csv.contains("id,name"));
269 }
270}