1use reqwest::{Method, Response};
2use serde::{de::DeserializeOwned, Serialize};
3
4use crate::Supabase;
5
6#[must_use = "a QueryBuilder does nothing until .execute() or .execute_and_parse() is called"]
13pub struct QueryBuilder<'a> {
14 client: &'a Supabase,
15 table: String,
16 query_params: Vec<(String, String)>,
17 method: Method,
18 body: Option<String>,
19}
20
21impl<'a> QueryBuilder<'a> {
22 pub(crate) fn new(client: &'a Supabase, table: impl Into<String>) -> Self {
24 Self {
25 client,
26 table: table.into(),
27 query_params: Vec::new(),
28 method: Method::GET,
29 body: None,
30 }
31 }
32
33 pub fn select(mut self, columns: impl Into<String>) -> Self {
37 self.query_params.push(("select".into(), columns.into()));
38 self.method = Method::GET;
39 self
40 }
41
42 pub fn insert<T: Serialize>(mut self, data: &T) -> Result<Self, crate::Error> {
46 self.method = Method::POST;
47 self.body = Some(serde_json::to_string(data)?);
48 Ok(self)
49 }
50
51 pub fn update<T: Serialize>(mut self, data: &T) -> Result<Self, crate::Error> {
55 self.method = Method::PATCH;
56 self.body = Some(serde_json::to_string(data)?);
57 Ok(self)
58 }
59
60 pub fn delete(mut self) -> Self {
64 self.method = Method::DELETE;
65 self
66 }
67
68 pub fn eq(self, column: impl Into<String>, value: impl Into<String>) -> Self {
70 self.add_filter(column, "eq", value)
71 }
72
73 pub fn neq(self, column: impl Into<String>, value: impl Into<String>) -> Self {
75 self.add_filter(column, "neq", value)
76 }
77
78 pub fn gt(self, column: impl Into<String>, value: impl Into<String>) -> Self {
80 self.add_filter(column, "gt", value)
81 }
82
83 pub fn gte(self, column: impl Into<String>, value: impl Into<String>) -> Self {
85 self.add_filter(column, "gte", value)
86 }
87
88 pub fn lt(self, column: impl Into<String>, value: impl Into<String>) -> Self {
90 self.add_filter(column, "lt", value)
91 }
92
93 pub fn lte(self, column: impl Into<String>, value: impl Into<String>) -> Self {
95 self.add_filter(column, "lte", value)
96 }
97
98 pub fn like(self, column: impl Into<String>, pattern: impl Into<String>) -> Self {
102 self.add_filter(column, "like", pattern)
103 }
104
105 pub fn ilike(self, column: impl Into<String>, pattern: impl Into<String>) -> Self {
109 self.add_filter(column, "ilike", pattern)
110 }
111
112 pub fn in_<I, S>(mut self, column: impl Into<String>, values: I) -> Self
114 where
115 I: IntoIterator<Item = S>,
116 S: AsRef<str>,
117 {
118 let values_str: Vec<_> = values.into_iter().map(|s| s.as_ref().to_string()).collect();
119 self.query_params
120 .push((column.into(), format!("in.({})", values_str.join(","))));
121 self
122 }
123
124 pub fn is_null(mut self, column: impl Into<String>) -> Self {
126 self.query_params.push((column.into(), "is.null".into()));
127 self
128 }
129
130 pub fn not_null(mut self, column: impl Into<String>) -> Self {
132 self.query_params.push((column.into(), "not.is.null".into()));
133 self
134 }
135
136 pub fn order(mut self, column: impl Into<String>) -> Self {
140 self.query_params.push(("order".into(), column.into()));
141 self
142 }
143
144 pub fn limit(mut self, count: usize) -> Self {
146 self.query_params.push(("limit".into(), count.to_string()));
147 self
148 }
149
150 pub fn offset(mut self, count: usize) -> Self {
152 self.query_params.push(("offset".into(), count.to_string()));
153 self
154 }
155
156 pub async fn execute(self) -> Result<Response, crate::Error> {
160 let url = format!("{}/rest/v1/{}", self.client.url, self.table);
161
162 let mut request = self
163 .client
164 .client
165 .request(self.method, &url)
166 .header("apikey", &self.client.api_key)
167 .header("Content-Type", "application/json");
168
169 if let Some(ref token) = self.client.bearer_token {
170 request = request.bearer_auth(token);
171 }
172
173 if !self.query_params.is_empty() {
174 request = request.query(&self.query_params);
175 }
176
177 if let Some(body) = self.body {
178 request = request.body(body);
179 }
180
181 let resp = request.send().await?;
182
183 let status = resp.status().as_u16();
184 if !(200..300).contains(&status) {
185 let message = resp.text().await.unwrap_or_default();
186 return Err(crate::Error::Api { status, message });
187 }
188
189 Ok(resp)
190 }
191
192 pub async fn execute_and_parse<T: DeserializeOwned>(self) -> Result<T, crate::Error> {
197 let resp = self.execute().await?;
198 let body = resp.text().await?;
199 let parsed: T = serde_json::from_str(&body)?;
200 Ok(parsed)
201 }
202
203 fn add_filter(
204 mut self,
205 column: impl Into<String>,
206 op: &str,
207 value: impl Into<String>,
208 ) -> Self {
209 self.query_params
210 .push((column.into(), format!("{op}.{}", value.into())));
211 self
212 }
213}
214
215impl Supabase {
216 pub fn from(&self, table: impl Into<String>) -> QueryBuilder<'_> {
239 QueryBuilder::new(self, table)
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246 use serde::{Deserialize, Serialize};
247
248 fn client() -> Supabase {
249 Supabase::new(None, None, None).unwrap_or_else(|_| {
250 Supabase::new(
251 Some("https://example.supabase.co"),
252 Some("test-key"),
253 None,
254 )
255 .unwrap()
256 })
257 }
258
259 fn is_acceptable_error(err: &crate::Error) -> bool {
262 matches!(
263 err,
264 crate::Error::Request(_) | crate::Error::Api { status: 401, .. }
265 )
266 }
267
268 #[derive(Debug, Serialize, Deserialize)]
269 struct TestItem {
270 name: String,
271 value: i32,
272 }
273
274 #[tokio::test]
275 async fn test_select() {
276 let client = client();
277
278 match client.from("test_items").select("*").execute().await {
279 Ok(_resp) => {}
280 Err(e) if is_acceptable_error(&e) => {
281 println!("Test skipped: {e}");
282 }
283 Err(e) => panic!("unexpected error: {e}"),
284 }
285 }
286
287 #[tokio::test]
288 async fn test_select_columns() {
289 let client = client();
290
291 match client.from("test_items").select("id,name").execute().await {
292 Ok(_resp) => {}
293 Err(e) if is_acceptable_error(&e) => {
294 println!("Test skipped: {e}");
295 }
296 Err(e) => panic!("unexpected error: {e}"),
297 }
298 }
299
300 #[tokio::test]
301 async fn test_select_with_filter() {
302 let client = client();
303
304 match client
305 .from("test_items")
306 .select("*")
307 .eq("name", "test")
308 .execute()
309 .await
310 {
311 Ok(_resp) => {}
312 Err(e) if is_acceptable_error(&e) => {
313 println!("Test skipped: {e}");
314 }
315 Err(e) => panic!("unexpected error: {e}"),
316 }
317 }
318
319 #[tokio::test]
320 async fn test_insert() {
321 let client = client();
322
323 let item = TestItem {
324 name: "test_item".into(),
325 value: 42,
326 };
327
328 match client
329 .from("test_items")
330 .insert(&item)
331 .expect("serialization should succeed")
332 .execute()
333 .await
334 {
335 Ok(_resp) => {}
336 Err(e) if is_acceptable_error(&e) => {
337 println!("Test skipped: {e}");
338 }
339 Err(e) => panic!("unexpected error: {e}"),
340 }
341 }
342
343 #[tokio::test]
344 async fn test_update() {
345 let client = client();
346
347 let updates = serde_json::json!({ "value": 100 });
348
349 match client
350 .from("test_items")
351 .update(&updates)
352 .expect("serialization should succeed")
353 .eq("name", "test_item")
354 .execute()
355 .await
356 {
357 Ok(_resp) => {}
358 Err(e) if is_acceptable_error(&e) => {
359 println!("Test skipped: {e}");
360 }
361 Err(e) => panic!("unexpected error: {e}"),
362 }
363 }
364
365 #[tokio::test]
366 async fn test_delete() {
367 let client = client();
368
369 match client
370 .from("test_items")
371 .delete()
372 .eq("name", "test_item")
373 .execute()
374 .await
375 {
376 Ok(_resp) => {}
377 Err(e) if is_acceptable_error(&e) => {
378 println!("Test skipped: {e}");
379 }
380 Err(e) => panic!("unexpected error: {e}"),
381 }
382 }
383
384 #[tokio::test]
385 async fn test_select_with_order_and_limit() {
386 let client = client();
387
388 match client
389 .from("test_items")
390 .select("*")
391 .order("id.desc")
392 .limit(10)
393 .execute()
394 .await
395 {
396 Ok(_resp) => {}
397 Err(e) if is_acceptable_error(&e) => {
398 println!("Test skipped: {e}");
399 }
400 Err(e) => panic!("unexpected error: {e}"),
401 }
402 }
403
404 #[tokio::test]
405 async fn test_select_with_multiple_filters() {
406 let client = client();
407
408 match client
409 .from("test_items")
410 .select("*")
411 .gte("value", "10")
412 .lte("value", "100")
413 .execute()
414 .await
415 {
416 Ok(_resp) => {}
417 Err(e) if is_acceptable_error(&e) => {
418 println!("Test skipped: {e}");
419 }
420 Err(e) => panic!("unexpected error: {e}"),
421 }
422 }
423
424 #[tokio::test]
425 async fn test_in_filter() {
426 let client = client();
427
428 match client
429 .from("test_items")
430 .select("*")
431 .in_("id", ["1", "2", "3"])
432 .execute()
433 .await
434 {
435 Ok(_resp) => {}
436 Err(e) if is_acceptable_error(&e) => {
437 println!("Test skipped: {e}");
438 }
439 Err(e) => panic!("unexpected error: {e}"),
440 }
441 }
442
443 #[test]
444 fn test_error_display() {
445 let err = crate::Error::Api {
446 status: 400,
447 message: "bad request".into(),
448 };
449 assert!(format!("{err}").contains("400"));
450 }
451}