Skip to main content

supabase_client_query/
lib.rs

1//! # supabase-client-query
2//!
3//! Query builder, filters, modifiers, and SQL/PostgREST execution for
4//! the `supabase-client` crate family.
5//!
6//! Provides a fluent API for SELECT, INSERT, UPDATE, DELETE, UPSERT, and RPC
7//! queries against Supabase, with 20+ filter methods and full modifier support.
8//!
9//! **Most users should depend on [`supabase-client-sdk`](https://crates.io/crates/supabase-client-sdk)
10//! instead** and enable the `query` feature (on by default), which re-exports
11//! this crate.
12//!
13//! ## Features
14//!
15//! - `direct-sql` — Execute queries directly against PostgreSQL via sqlx
16//!   instead of going through PostgREST.
17
18pub mod sql;
19pub mod table;
20pub mod filter;
21pub mod modifier;
22pub mod backend;
23pub mod postgrest;
24pub mod postgrest_execute;
25pub mod builder;
26pub mod select;
27pub mod csv_select;
28pub mod geojson_select;
29pub mod insert;
30pub mod update;
31pub mod delete;
32pub mod upsert;
33pub mod rpc;
34
35#[cfg(feature = "direct-sql")]
36pub mod generate;
37#[cfg(feature = "direct-sql")]
38pub mod execute;
39
40pub use sql::*;
41pub use table::Table;
42pub use filter::{Filterable, FilterCollector};
43pub use modifier::Modifiable;
44pub use backend::QueryBackend;
45pub use builder::{QueryBuilder, TypedQueryBuilder};
46pub use select::SelectBuilder;
47pub use insert::InsertBuilder;
48pub use update::UpdateBuilder;
49pub use delete::DeleteBuilder;
50pub use upsert::UpsertBuilder;
51pub use rpc::{RpcBuilder, TypedRpcBuilder};
52pub use csv_select::CsvSelectBuilder;
53pub use geojson_select::GeoJsonSelectBuilder;
54
55// Re-export Phase 10 types for convenience
56pub use sql::{ExplainOptions, ExplainFormat, CountOption};
57
58use std::sync::Arc;
59use serde::de::DeserializeOwned;
60use serde_json::Value as JsonValue;
61use supabase_client_core::SupabaseClient;
62
63/// Extension trait adding query builder methods to SupabaseClient.
64pub trait SupabaseClientQueryExt {
65    /// Start a dynamic (string-based) query on a table.
66    fn from(&self, table: &str) -> QueryBuilder;
67
68    /// Start a typed query on a table using the Table trait.
69    fn from_typed<T: Table>(&self) -> TypedQueryBuilder<T>;
70
71    /// Call a stored procedure/function with dynamic return.
72    fn rpc(&self, function: &str, args: JsonValue) -> Result<RpcBuilder, supabase_client_core::SupabaseError>;
73
74    /// Call a stored procedure/function with typed return.
75    fn rpc_typed<T>(&self, function: &str, args: JsonValue) -> Result<TypedRpcBuilder<T>, supabase_client_core::SupabaseError>
76    where
77        T: DeserializeOwned + Send;
78}
79
80impl SupabaseClientQueryExt for SupabaseClient {
81    fn from(&self, table: &str) -> QueryBuilder {
82        let backend = make_backend(self);
83        QueryBuilder::new(backend, self.schema().to_string(), table.to_string())
84    }
85
86    fn from_typed<T: Table>(&self) -> TypedQueryBuilder<T> {
87        let backend = make_backend(self);
88        let schema = if T::schema_name() != "public" {
89            T::schema_name().to_string()
90        } else {
91            self.schema().to_string()
92        };
93        TypedQueryBuilder::new(backend, schema)
94    }
95
96    fn rpc(&self, function: &str, args: JsonValue) -> Result<RpcBuilder, supabase_client_core::SupabaseError> {
97        let backend = make_backend(self);
98        RpcBuilder::new(backend, self.schema().to_string(), function.to_string(), args)
99    }
100
101    fn rpc_typed<T>(&self, function: &str, args: JsonValue) -> Result<TypedRpcBuilder<T>, supabase_client_core::SupabaseError>
102    where
103        T: DeserializeOwned + Send,
104    {
105        let backend = make_backend(self);
106        TypedRpcBuilder::new(backend, self.schema().to_string(), function.to_string(), args)
107    }
108}
109
110/// Create a QueryBackend from a SupabaseClient.
111///
112/// If the `direct-sql` feature is enabled and a pool is available, uses DirectSql.
113/// Otherwise, uses the REST backend (PostgREST).
114fn make_backend(client: &SupabaseClient) -> QueryBackend {
115    #[cfg(feature = "direct-sql")]
116    {
117        if let Some(pool) = client.pool_arc() {
118            return QueryBackend::DirectSql { pool };
119        }
120    }
121
122    QueryBackend::Rest {
123        http: client.http().clone(),
124        base_url: Arc::from(client.supabase_url()),
125        api_key: Arc::from(client.api_key()),
126        schema: client.schema().to_string(),
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133    use supabase_client_core::SupabaseConfig;
134
135    fn make_client() -> SupabaseClient {
136        let config = SupabaseConfig {
137            supabase_url: "http://localhost:54321".to_string(),
138            supabase_key: "test-key".to_string(),
139            schema: "public".to_string(),
140            #[cfg(feature = "direct-sql")]
141            database_url: None,
142            #[cfg(feature = "direct-sql")]
143            pool: Default::default(),
144        };
145        SupabaseClient::new(config).unwrap()
146    }
147
148    #[test]
149    fn test_from_returns_query_builder() {
150        let client = make_client();
151        // client.from() should return a QueryBuilder which we can call select on
152        let select_builder = client.from("users").select("*");
153        assert_eq!(select_builder.parts.table, "users");
154        assert_eq!(select_builder.parts.schema, "public");
155        assert!(select_builder.parts.select_columns.is_none());
156    }
157
158    #[test]
159    fn test_from_typed_returns_typed_query_builder() {
160        // Minimal Table implementation for testing
161        #[derive(Debug, Clone, serde::Deserialize)]
162        struct MyTable {
163            id: i32,
164        }
165
166        impl crate::table::Table for MyTable {
167            fn table_name() -> &'static str {
168                "my_table"
169            }
170
171            fn primary_key_columns() -> &'static [&'static str] {
172                &["id"]
173            }
174
175            fn column_names() -> &'static [&'static str] {
176                &["id"]
177            }
178
179            fn insertable_columns() -> &'static [&'static str] {
180                &[]
181            }
182
183            fn field_to_column(field: &str) -> Option<&'static str> {
184                match field {
185                    "id" => Some("id"),
186                    _ => None,
187                }
188            }
189
190            fn column_to_field(column: &str) -> Option<&'static str> {
191                match column {
192                    "id" => Some("id"),
193                    _ => None,
194                }
195            }
196
197            fn bind_insert(&self) -> Vec<SqlParam> {
198                vec![]
199            }
200
201            fn bind_update(&self) -> Vec<SqlParam> {
202                vec![]
203            }
204
205            fn bind_primary_key(&self) -> Vec<SqlParam> {
206                vec![SqlParam::I32(self.id)]
207            }
208        }
209
210        let client = make_client();
211        let select_builder = client.from_typed::<MyTable>().select();
212        assert_eq!(select_builder.parts.table, "my_table");
213        assert_eq!(select_builder.parts.schema, "public");
214    }
215
216    #[test]
217    fn test_make_backend_creates_rest() {
218        let client = make_client();
219        let backend = make_backend(&client);
220        match &backend {
221            QueryBackend::Rest { base_url, schema, .. } => {
222                assert_eq!(base_url.as_ref(), "http://localhost:54321");
223                assert_eq!(schema, "public");
224            }
225            #[cfg(feature = "direct-sql")]
226            _ => panic!("expected Rest backend"),
227        }
228    }
229}