use reqwest::header::HeaderValue;
use supabase_client_core::SupabaseError;
use crate::backend::QueryBackend;
use crate::filter::Filterable;
use crate::modifier::Modifiable;
use crate::sql::{FilterCondition, ParamStore, SqlParts};
pub struct CsvSelectBuilder {
pub(crate) backend: QueryBackend,
pub(crate) parts: SqlParts,
pub(crate) params: ParamStore,
}
impl Filterable for CsvSelectBuilder {
fn filters_mut(&mut self) -> &mut Vec<FilterCondition> {
&mut self.parts.filters
}
fn params_mut(&mut self) -> &mut ParamStore {
&mut self.params
}
}
impl Modifiable for CsvSelectBuilder {
fn parts_mut(&mut self) -> &mut SqlParts {
&mut self.parts
}
}
impl CsvSelectBuilder {
pub fn schema(mut self, schema: &str) -> Self {
self.parts.schema_override = Some(schema.to_string());
self
}
pub async fn execute(self) -> Result<String, SupabaseError> {
match &self.backend {
QueryBackend::Rest { http, base_url, api_key, schema } => {
let (url, mut headers) = crate::postgrest::build_postgrest_select(
base_url, &self.parts, &self.params,
)
.map_err(SupabaseError::QueryBuilder)?;
headers.insert("Accept", HeaderValue::from_static("text/csv"));
headers.insert("apikey", HeaderValue::from_str(api_key).unwrap());
headers.insert(
"Authorization",
HeaderValue::from_str(&format!("Bearer {}", api_key)).unwrap(),
);
if let Some(ref so) = self.parts.schema_override {
headers.insert(
"Accept-Profile",
HeaderValue::from_str(so).unwrap(),
);
} else if schema != "public" {
headers.entry("Accept-Profile")
.or_insert_with(|| HeaderValue::from_str(schema).unwrap());
}
let response = http
.get(&url)
.headers(headers)
.send()
.await
.map_err(|e| SupabaseError::Http(e.to_string()))?;
let status = response.status().as_u16();
let body = response
.text()
.await
.map_err(|e| SupabaseError::Http(e.to_string()))?;
if status >= 400 {
return Err(SupabaseError::postgrest(status, body, None));
}
Ok(body)
}
#[cfg(feature = "direct-sql")]
QueryBackend::DirectSql { .. } => {
Err(SupabaseError::query_builder(
"CSV output is only supported with the REST (PostgREST) backend",
))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sql::{SqlOperation, SqlParts, ParamStore};
#[test]
fn test_csv_builder_modifiable() {
let mut builder = CsvSelectBuilder {
backend: QueryBackend::Rest {
http: reqwest::Client::new(),
base_url: "http://localhost".into(),
api_key: "key".into(),
schema: "public".to_string(),
},
parts: SqlParts::new(SqlOperation::Select, "public", "cities"),
params: ParamStore::new(),
};
builder = builder.limit(10);
assert_eq!(builder.parts.limit, Some(10));
}
#[test]
fn test_csv_builder_filterable() {
let builder = CsvSelectBuilder {
backend: QueryBackend::Rest {
http: reqwest::Client::new(),
base_url: "http://localhost".into(),
api_key: "key".into(),
schema: "public".to_string(),
},
parts: SqlParts::new(SqlOperation::Select, "public", "cities"),
params: ParamStore::new(),
};
let builder = builder.eq("name", "Auckland");
assert_eq!(builder.parts.filters.len(), 1);
}
#[test]
fn test_csv_accept_header() {
let parts = SqlParts::new(SqlOperation::Select, "public", "cities");
let params = ParamStore::new();
let (_, mut headers) = crate::postgrest::build_postgrest_select(
"http://localhost:64321", &parts, ¶ms,
).unwrap();
headers.insert("Accept", HeaderValue::from_static("text/csv"));
assert_eq!(headers.get("Accept").unwrap(), "text/csv");
}
#[test]
fn test_csv_schema_sets_override() {
let builder = CsvSelectBuilder {
backend: QueryBackend::Rest {
http: reqwest::Client::new(),
base_url: "http://localhost".into(),
api_key: "key".into(),
schema: "public".to_string(),
},
parts: SqlParts::new(SqlOperation::Select, "public", "cities"),
params: ParamStore::new(),
};
let builder = builder.schema("custom");
assert_eq!(builder.parts.schema_override.as_deref(), Some("custom"));
}
#[tokio::test]
async fn test_csv_execute_success() {
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
let mock_server = MockServer::start().await;
let csv_body = "id,name\n1,Alice\n2,Bob\n";
Mock::given(method("GET"))
.and(path("/rest/v1/users"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string(csv_body),
)
.mount(&mock_server)
.await;
let builder = CsvSelectBuilder {
backend: QueryBackend::Rest {
http: reqwest::Client::new(),
base_url: mock_server.uri().into(),
api_key: "test-key".into(),
schema: "public".to_string(),
},
parts: SqlParts::new(SqlOperation::Select, "public", "users"),
params: ParamStore::new(),
};
let result = builder.execute().await;
assert!(result.is_ok());
let csv = result.unwrap();
assert!(csv.contains("id,name"));
assert!(csv.contains("1,Alice"));
assert!(csv.contains("2,Bob"));
}
#[tokio::test]
async fn test_csv_execute_error() {
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/rest/v1/nonexistent"))
.respond_with(
ResponseTemplate::new(404)
.set_body_string("Relation not found"),
)
.mount(&mock_server)
.await;
let builder = CsvSelectBuilder {
backend: QueryBackend::Rest {
http: reqwest::Client::new(),
base_url: mock_server.uri().into(),
api_key: "test-key".into(),
schema: "public".to_string(),
},
parts: SqlParts::new(SqlOperation::Select, "public", "nonexistent"),
params: ParamStore::new(),
};
let result = builder.execute().await;
assert!(result.is_err());
match result.unwrap_err() {
SupabaseError::PostgRest { status, .. } => {
assert_eq!(status, 404);
}
other => panic!("Expected PostgRest error, got {:?}", other),
}
}
#[tokio::test]
async fn test_csv_execute_empty_result() {
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/rest/v1/users"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string("id,name\n"),
)
.mount(&mock_server)
.await;
let builder = CsvSelectBuilder {
backend: QueryBackend::Rest {
http: reqwest::Client::new(),
base_url: mock_server.uri().into(),
api_key: "test-key".into(),
schema: "public".to_string(),
},
parts: SqlParts::new(SqlOperation::Select, "public", "users"),
params: ParamStore::new(),
};
let result = builder.execute().await;
assert!(result.is_ok());
let csv = result.unwrap();
assert!(csv.contains("id,name"));
}
}