Skip to main content

supabase_client_query/
builder.rs

1use std::marker::PhantomData;
2
3use serde_json::Value as JsonValue;
4
5use supabase_client_core::Row;
6
7use crate::backend::QueryBackend;
8use crate::delete::DeleteBuilder;
9use crate::insert::InsertBuilder;
10use crate::select::SelectBuilder;
11use crate::sql::{ParamStore, SqlOperation, SqlParts};
12use crate::table::Table;
13use crate::update::UpdateBuilder;
14use crate::upsert::UpsertBuilder;
15
16/// Entry point query builder created by `client.from("table")`.
17///
18/// Call `.select()`, `.insert()`, `.update()`, `.delete()`, or `.upsert()` to
19/// specialize into the appropriate builder type.
20pub struct QueryBuilder {
21    backend: QueryBackend,
22    schema: String,
23    table: String,
24}
25
26impl QueryBuilder {
27    pub fn new(backend: QueryBackend, schema: String, table: String) -> Self {
28        Self {
29            backend,
30            schema,
31            table,
32        }
33    }
34
35    /// Start a SELECT query.
36    /// Pass column expressions like "name, country_id" or "*".
37    pub fn select(self, columns: &str) -> SelectBuilder<Row> {
38        let mut parts = SqlParts::new(SqlOperation::Select, &self.schema, &self.table);
39
40        // Parse and quote column names
41        if columns == "*" || columns.is_empty() {
42            parts.select_columns = None; // will become SELECT *
43        } else {
44            let quoted = columns
45                .split(',')
46                .map(|c| {
47                    let c = c.trim();
48                    if c.contains('(') || c.contains('*') || c.contains('"') || c.contains(' ') {
49                        // Already complex expression, pass through
50                        c.to_string()
51                    } else {
52                        format!("\"{}\"", c)
53                    }
54                })
55                .collect::<Vec<_>>()
56                .join(", ");
57            parts.select_columns = Some(quoted);
58        }
59
60        SelectBuilder {
61            backend: self.backend,
62            parts,
63            params: ParamStore::new(),
64            _marker: PhantomData,
65        }
66    }
67
68    /// Start an INSERT query with a single row.
69    pub fn insert(self, row: Row) -> InsertBuilder<Row> {
70        let mut parts = SqlParts::new(SqlOperation::Insert, &self.schema, &self.table);
71        let mut params = ParamStore::new();
72
73        let mut entries: Vec<_> = row.into_inner().into_iter().collect();
74        entries.sort_by(|a, b| a.0.cmp(&b.0));
75        for (col, val) in entries {
76            let idx = params.push(json_to_sql_param(val));
77            parts.set_clauses.push((col, idx));
78        }
79
80        InsertBuilder {
81            backend: self.backend,
82            parts,
83            params,
84            _marker: PhantomData,
85        }
86    }
87
88    /// Start an INSERT query with multiple rows.
89    pub fn insert_many(self, rows: Vec<Row>) -> InsertBuilder<Row> {
90        let mut parts = SqlParts::new(SqlOperation::Insert, &self.schema, &self.table);
91        let mut params = ParamStore::new();
92
93        // Determine canonical column order from the first row
94        let column_order: Vec<String> = if let Some(first) = rows.first() {
95            let mut cols: Vec<String> = first.columns().iter().map(|c| c.to_string()).collect();
96            cols.sort();
97            cols
98        } else {
99            Vec::new()
100        };
101
102        for row in rows {
103            let inner = row.into_inner();
104            let mut row_pairs = Vec::new();
105            for col in &column_order {
106                let val = inner.get(col).cloned().unwrap_or(serde_json::Value::Null);
107                let idx = params.push(json_to_sql_param(val));
108                row_pairs.push((col.clone(), idx));
109            }
110            parts.many_rows.push(row_pairs);
111        }
112
113        InsertBuilder {
114            backend: self.backend,
115            parts,
116            params,
117            _marker: PhantomData,
118        }
119    }
120
121    /// Start an UPDATE query.
122    pub fn update(self, row: Row) -> UpdateBuilder<Row> {
123        let mut parts = SqlParts::new(SqlOperation::Update, &self.schema, &self.table);
124        let mut params = ParamStore::new();
125
126        let mut entries: Vec<_> = row.into_inner().into_iter().collect();
127        entries.sort_by(|a, b| a.0.cmp(&b.0));
128        for (col, val) in entries {
129            let idx = params.push(json_to_sql_param(val));
130            parts.set_clauses.push((col, idx));
131        }
132
133        UpdateBuilder {
134            backend: self.backend,
135            parts,
136            params,
137            _marker: PhantomData,
138        }
139    }
140
141    /// Start a DELETE query.
142    pub fn delete(self) -> DeleteBuilder<Row> {
143        let parts = SqlParts::new(SqlOperation::Delete, &self.schema, &self.table);
144        DeleteBuilder {
145            backend: self.backend,
146            parts,
147            params: ParamStore::new(),
148            _marker: PhantomData,
149        }
150    }
151
152    /// Start an UPSERT (INSERT ... ON CONFLICT DO UPDATE) query with a single row.
153    pub fn upsert(self, row: Row) -> UpsertBuilder<Row> {
154        let mut parts = SqlParts::new(SqlOperation::Upsert, &self.schema, &self.table);
155        let mut params = ParamStore::new();
156
157        let mut entries: Vec<_> = row.into_inner().into_iter().collect();
158        entries.sort_by(|a, b| a.0.cmp(&b.0));
159        for (col, val) in entries {
160            let idx = params.push(json_to_sql_param(val));
161            parts.set_clauses.push((col, idx));
162        }
163
164        UpsertBuilder {
165            backend: self.backend,
166            parts,
167            params,
168            _marker: PhantomData,
169        }
170    }
171
172    /// Start an UPSERT query with multiple rows.
173    pub fn upsert_many(self, rows: Vec<Row>) -> UpsertBuilder<Row> {
174        let mut parts = SqlParts::new(SqlOperation::Upsert, &self.schema, &self.table);
175        let mut params = ParamStore::new();
176
177        let column_order: Vec<String> = if let Some(first) = rows.first() {
178            let mut cols: Vec<String> = first.columns().iter().map(|c| c.to_string()).collect();
179            cols.sort();
180            cols
181        } else {
182            Vec::new()
183        };
184
185        for row in rows {
186            let inner = row.into_inner();
187            let mut row_pairs = Vec::new();
188            for col in &column_order {
189                let val = inner.get(col).cloned().unwrap_or(serde_json::Value::Null);
190                let idx = params.push(json_to_sql_param(val));
191                row_pairs.push((col.clone(), idx));
192            }
193            parts.many_rows.push(row_pairs);
194        }
195
196        UpsertBuilder {
197            backend: self.backend,
198            parts,
199            params,
200            _marker: PhantomData,
201        }
202    }
203}
204
205/// Entry point for typed queries created by `client.from_typed::<T>()`.
206pub struct TypedQueryBuilder<T: Table> {
207    backend: QueryBackend,
208    schema: String,
209    _marker: PhantomData<T>,
210}
211
212impl<T: Table> TypedQueryBuilder<T> {
213    pub fn new(backend: QueryBackend, schema: String) -> Self {
214        Self {
215            backend,
216            schema,
217            _marker: PhantomData,
218        }
219    }
220
221    /// Start a typed SELECT query (selects all columns by default).
222    pub fn select(self) -> SelectBuilder<T> {
223        let parts = SqlParts::new(SqlOperation::Select, &self.schema, T::table_name());
224        SelectBuilder {
225            backend: self.backend,
226            parts,
227            params: ParamStore::new(),
228            _marker: PhantomData,
229        }
230    }
231
232    /// Start a typed SELECT with specific columns.
233    pub fn select_columns(self, columns: &str) -> SelectBuilder<T> {
234        let mut parts = SqlParts::new(SqlOperation::Select, &self.schema, T::table_name());
235        if columns != "*" && !columns.is_empty() {
236            let quoted = columns
237                .split(',')
238                .map(|c| {
239                    let c = c.trim();
240                    if c.contains('(') || c.contains('*') || c.contains('"') || c.contains(' ') {
241                        c.to_string()
242                    } else {
243                        format!("\"{}\"", c)
244                    }
245                })
246                .collect::<Vec<_>>()
247                .join(", ");
248            parts.select_columns = Some(quoted);
249        }
250        SelectBuilder {
251            backend: self.backend,
252            parts,
253            params: ParamStore::new(),
254            _marker: PhantomData,
255        }
256    }
257
258    /// Start a typed INSERT from a struct instance.
259    pub fn insert(self, value: &T) -> InsertBuilder<T> {
260        let mut parts = SqlParts::new(SqlOperation::Insert, &self.schema, T::table_name());
261        let mut params = ParamStore::new();
262
263        let columns = T::insertable_columns();
264        let values = value.bind_insert();
265
266        for (col, val) in columns.iter().zip(values.into_iter()) {
267            let idx = params.push(val);
268            parts.set_clauses.push((col.to_string(), idx));
269        }
270
271        InsertBuilder {
272            backend: self.backend,
273            parts,
274            params,
275            _marker: PhantomData,
276        }
277    }
278
279    /// Start a typed UPDATE from a struct instance (updates non-PK columns).
280    pub fn update(self, value: &T) -> UpdateBuilder<T> {
281        let mut parts = SqlParts::new(SqlOperation::Update, &self.schema, T::table_name());
282        let mut params = ParamStore::new();
283
284        // SET clauses: all non-PK columns
285        let pk_cols = T::primary_key_columns();
286        let all_cols = T::column_names();
287        let update_vals = value.bind_update();
288
289        let update_cols: Vec<&&str> = all_cols
290            .iter()
291            .filter(|c| !pk_cols.contains(c))
292            .collect();
293
294        for (col, val) in update_cols.iter().zip(update_vals.into_iter()) {
295            let idx = params.push(val);
296            parts.set_clauses.push((col.to_string(), idx));
297        }
298
299        // WHERE clause: primary key match
300        let pk_vals = value.bind_primary_key();
301        for (col, val) in pk_cols.iter().zip(pk_vals.into_iter()) {
302            let idx = params.push(val);
303            parts.filters.push(crate::sql::FilterCondition::Comparison {
304                column: col.to_string(),
305                operator: crate::sql::FilterOperator::Eq,
306                param_index: idx,
307            });
308        }
309
310        UpdateBuilder {
311            backend: self.backend,
312            parts,
313            params,
314            _marker: PhantomData,
315        }
316    }
317
318    /// Start a typed DELETE.
319    pub fn delete(self) -> DeleteBuilder<T> {
320        let parts = SqlParts::new(SqlOperation::Delete, &self.schema, T::table_name());
321        DeleteBuilder {
322            backend: self.backend,
323            parts,
324            params: ParamStore::new(),
325            _marker: PhantomData,
326        }
327    }
328
329    /// Start a typed UPSERT from a struct instance.
330    pub fn upsert(self, value: &T) -> UpsertBuilder<T> {
331        let mut parts = SqlParts::new(SqlOperation::Upsert, &self.schema, T::table_name());
332        let mut params = ParamStore::new();
333
334        let pk_cols = T::primary_key_columns();
335        let insertable_cols = T::insertable_columns();
336
337        // First add PK columns
338        let pk_vals = value.bind_primary_key();
339        for (col, val) in pk_cols.iter().zip(pk_vals.into_iter()) {
340            let idx = params.push(val);
341            parts.set_clauses.push((col.to_string(), idx));
342        }
343
344        // Then add insertable columns
345        let insert_vals = value.bind_insert();
346        for (col, val) in insertable_cols.iter().zip(insert_vals.into_iter()) {
347            let idx = params.push(val);
348            parts.set_clauses.push((col.to_string(), idx));
349        }
350
351        parts.conflict_columns = pk_cols.iter().map(|c| c.to_string()).collect();
352
353        UpsertBuilder {
354            backend: self.backend,
355            parts,
356            params,
357            _marker: PhantomData,
358        }
359    }
360}
361
362/// Convert a serde_json::Value into an SqlParam.
363fn json_to_sql_param(value: JsonValue) -> crate::sql::SqlParam {
364    match value {
365        JsonValue::Null => crate::sql::SqlParam::Null,
366        JsonValue::Bool(b) => crate::sql::SqlParam::Bool(b),
367        JsonValue::Number(n) => {
368            if let Some(i) = n.as_i64() {
369                if i >= i32::MIN as i64 && i <= i32::MAX as i64 {
370                    crate::sql::SqlParam::I32(i as i32)
371                } else {
372                    crate::sql::SqlParam::I64(i)
373                }
374            } else if let Some(f) = n.as_f64() {
375                crate::sql::SqlParam::F64(f)
376            } else {
377                crate::sql::SqlParam::Text(n.to_string())
378            }
379        }
380        JsonValue::String(s) => {
381            // Try to parse as UUID
382            if let Ok(uuid) = uuid::Uuid::parse_str(&s) {
383                crate::sql::SqlParam::Uuid(uuid)
384            } else {
385                crate::sql::SqlParam::Text(s)
386            }
387        }
388        other => crate::sql::SqlParam::Json(other),
389    }
390}