Skip to main content

supabase_rust/
db.rs

1use reqwest::{Method, Response};
2use serde::{de::DeserializeOwned, Serialize};
3
4use crate::Supabase;
5
6/// Query builder for PostgREST database operations.
7///
8/// Provides a fluent API for constructing and executing database queries.
9///
10/// Use [`execute()`](Self::execute) to get the raw response, or
11/// [`execute_and_parse()`](Self::execute_and_parse) to deserialize the JSON body.
12#[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    /// Creates a new QueryBuilder for the specified table.
23    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    /// Specifies which columns to select.
34    ///
35    /// Pass `"*"` to select all columns, or a comma-separated list of column names.
36    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    /// Prepares an insert operation with the provided data.
43    ///
44    /// Data will be serialized to JSON. Call `execute()` to run the query.
45    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    /// Prepares an update operation with the provided data.
52    ///
53    /// Should be combined with filter methods to target specific rows.
54    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    /// Prepares a delete operation.
61    ///
62    /// Should be combined with filter methods to target specific rows.
63    pub fn delete(mut self) -> Self {
64        self.method = Method::DELETE;
65        self
66    }
67
68    /// Filter: column equals value (`col=eq.val`).
69    pub fn eq(self, column: impl Into<String>, value: impl Into<String>) -> Self {
70        self.add_filter(column, "eq", value)
71    }
72
73    /// Filter: column not equals value (`col=neq.val`).
74    pub fn neq(self, column: impl Into<String>, value: impl Into<String>) -> Self {
75        self.add_filter(column, "neq", value)
76    }
77
78    /// Filter: column greater than value (`col=gt.val`).
79    pub fn gt(self, column: impl Into<String>, value: impl Into<String>) -> Self {
80        self.add_filter(column, "gt", value)
81    }
82
83    /// Filter: column greater than or equal to value (`col=gte.val`).
84    pub fn gte(self, column: impl Into<String>, value: impl Into<String>) -> Self {
85        self.add_filter(column, "gte", value)
86    }
87
88    /// Filter: column less than value (`col=lt.val`).
89    pub fn lt(self, column: impl Into<String>, value: impl Into<String>) -> Self {
90        self.add_filter(column, "lt", value)
91    }
92
93    /// Filter: column less than or equal to value (`col=lte.val`).
94    pub fn lte(self, column: impl Into<String>, value: impl Into<String>) -> Self {
95        self.add_filter(column, "lte", value)
96    }
97
98    /// Filter: column matches pattern (`col=like.pattern`).
99    ///
100    /// Use `*` as wildcard character.
101    pub fn like(self, column: impl Into<String>, pattern: impl Into<String>) -> Self {
102        self.add_filter(column, "like", pattern)
103    }
104
105    /// Filter: column matches pattern case-insensitively (`col=ilike.pattern`).
106    ///
107    /// Use `*` as wildcard character.
108    pub fn ilike(self, column: impl Into<String>, pattern: impl Into<String>) -> Self {
109        self.add_filter(column, "ilike", pattern)
110    }
111
112    /// Filter: column value is in the provided list (`col=in.(v1,v2,...)`).
113    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    /// Filter: column is null (`col=is.null`).
125    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    /// Filter: column is not null (`col=not.is.null`).
131    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    /// Orders results by the specified column.
137    ///
138    /// Use `"column"` for ascending or `"column.desc"` for descending.
139    pub fn order(mut self, column: impl Into<String>) -> Self {
140        self.query_params.push(("order".into(), column.into()));
141        self
142    }
143
144    /// Limits the number of rows returned.
145    pub fn limit(mut self, count: usize) -> Self {
146        self.query_params.push(("limit".into(), count.to_string()));
147        self
148    }
149
150    /// Offsets the results by the specified number of rows.
151    pub fn offset(mut self, count: usize) -> Self {
152        self.query_params.push(("offset".into(), count.to_string()));
153        self
154    }
155
156    /// Executes the query and returns the raw response.
157    ///
158    /// Returns `Error::Api` if the server responds with a non-2xx status code.
159    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    /// Executes the query and deserializes the JSON response body into `T`.
193    ///
194    /// This is a convenience wrapper around [`execute()`](Self::execute) that
195    /// also parses the response body.
196    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    /// Creates a QueryBuilder for the specified table.
217    ///
218    /// This is the entry point for all database operations.
219    ///
220    /// # Examples
221    ///
222    /// ```ignore
223    /// // Select all from users
224    /// client.from("users").select("*").execute().await?;
225    ///
226    /// // Select with filter
227    /// client.from("users").select("id,name").eq("status", "active").execute().await?;
228    ///
229    /// // Insert
230    /// client.from("users").insert(&user_data)?.execute().await?;
231    ///
232    /// // Update
233    /// client.from("users").update(&updates)?.eq("id", "123").execute().await?;
234    ///
235    /// // Delete
236    /// client.from("users").delete().eq("id", "123").execute().await?;
237    /// ```
238    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    /// Helper: returns true if the error is acceptable for tests running without
260    /// a real Supabase backend (network errors or 401 API errors).
261    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}