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 (visible for testing).
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}
391
392#[cfg(test)]
393mod tests {
394    use super::*;
395    use crate::backend::QueryBackend;
396    use crate::sql::*;
397    use serde_json::json;
398    use std::sync::Arc;
399    use supabase_client_core::Row;
400
401    fn make_backend() -> QueryBackend {
402        QueryBackend::Rest {
403            http: reqwest::Client::new(),
404            base_url: Arc::from("http://localhost"),
405            api_key: Arc::from("key"),
406            schema: "public".to_string(),
407        }
408    }
409
410    fn make_query_builder() -> QueryBuilder {
411        QueryBuilder::new(make_backend(), "public".to_string(), "test".to_string())
412    }
413
414    // ---- select() ----
415
416    #[test]
417    fn test_select_star() {
418        let builder = make_query_builder().select("*");
419        assert!(builder.parts.select_columns.is_none());
420    }
421
422    #[test]
423    fn test_select_empty() {
424        let builder = make_query_builder().select("");
425        assert!(builder.parts.select_columns.is_none());
426    }
427
428    #[test]
429    fn test_select_named_columns() {
430        let builder = make_query_builder().select("name, age");
431        let cols = builder.parts.select_columns.unwrap();
432        assert_eq!(cols, "\"name\", \"age\"");
433    }
434
435    #[test]
436    fn test_select_count_expression_passes_through() {
437        let builder = make_query_builder().select("count(*)");
438        let cols = builder.parts.select_columns.unwrap();
439        assert_eq!(cols, "count(*)");
440    }
441
442    #[test]
443    fn test_select_quoted_column_passes_through() {
444        let builder = make_query_builder().select("\"my_col\"");
445        let cols = builder.parts.select_columns.unwrap();
446        // Contains quote, passed through unchanged
447        assert_eq!(cols, "\"my_col\"");
448    }
449
450    // ---- insert() ----
451
452    #[test]
453    fn test_insert_sets_up_clauses() {
454        let mut row = Row::new();
455        row.set("name", json!("Alice"));
456        row.set("age", json!(30));
457
458        let builder = make_query_builder().insert(row);
459        assert_eq!(builder.parts.operation, SqlOperation::Insert);
460        assert_eq!(builder.parts.set_clauses.len(), 2);
461        // Sorted by column name: "age" before "name"
462        assert_eq!(builder.parts.set_clauses[0].0, "age");
463        assert_eq!(builder.parts.set_clauses[1].0, "name");
464    }
465
466    // ---- insert_many() ----
467
468    #[test]
469    fn test_insert_many_sets_up_many_rows() {
470        let mut row1 = Row::new();
471        row1.set("name", json!("Alice"));
472        row1.set("age", json!(30));
473
474        let mut row2 = Row::new();
475        row2.set("name", json!("Bob"));
476        row2.set("age", json!(25));
477
478        let builder = make_query_builder().insert_many(vec![row1, row2]);
479        assert_eq!(builder.parts.operation, SqlOperation::Insert);
480        assert_eq!(builder.parts.many_rows.len(), 2);
481        // Each row should have 2 column entries
482        assert_eq!(builder.parts.many_rows[0].len(), 2);
483        assert_eq!(builder.parts.many_rows[1].len(), 2);
484    }
485
486    #[test]
487    fn test_insert_many_empty() {
488        let builder = make_query_builder().insert_many(vec![]);
489        assert!(builder.parts.many_rows.is_empty());
490    }
491
492    // ---- delete() ----
493
494    #[test]
495    fn test_delete_creates_builder() {
496        let builder = make_query_builder().delete();
497        assert_eq!(builder.parts.operation, SqlOperation::Delete);
498        assert!(builder.params.is_empty());
499        assert!(builder.parts.filters.is_empty());
500    }
501
502    // ---- update() ----
503
504    #[test]
505    fn test_update_sets_up_clauses() {
506        let mut row = Row::new();
507        row.set("name", json!("Updated"));
508
509        let builder = make_query_builder().update(row);
510        assert_eq!(builder.parts.operation, SqlOperation::Update);
511        assert_eq!(builder.parts.set_clauses.len(), 1);
512        assert_eq!(builder.parts.set_clauses[0].0, "name");
513    }
514
515    // ---- upsert() ----
516
517    #[test]
518    fn test_upsert_sets_up_clauses() {
519        let mut row = Row::new();
520        row.set("id", json!(1));
521        row.set("name", json!("Alice"));
522
523        let builder = make_query_builder().upsert(row);
524        assert_eq!(builder.parts.operation, SqlOperation::Upsert);
525        assert_eq!(builder.parts.set_clauses.len(), 2);
526    }
527
528    // ---- upsert_many() ----
529
530    #[test]
531    fn test_upsert_many_sets_up_many_rows() {
532        let mut row1 = Row::new();
533        row1.set("id", json!(1));
534        row1.set("name", json!("Alice"));
535
536        let mut row2 = Row::new();
537        row2.set("id", json!(2));
538        row2.set("name", json!("Bob"));
539
540        let builder = make_query_builder().upsert_many(vec![row1, row2]);
541        assert_eq!(builder.parts.operation, SqlOperation::Upsert);
542        assert_eq!(builder.parts.many_rows.len(), 2);
543    }
544
545    // ---- json_to_sql_param (tested through insert) ----
546
547    #[test]
548    fn test_json_to_sql_param_null() {
549        let param = json_to_sql_param(json!(null));
550        assert!(matches!(param, SqlParam::Null));
551    }
552
553    #[test]
554    fn test_json_to_sql_param_bool() {
555        let param = json_to_sql_param(json!(true));
556        assert!(matches!(param, SqlParam::Bool(true)));
557        let param = json_to_sql_param(json!(false));
558        assert!(matches!(param, SqlParam::Bool(false)));
559    }
560
561    #[test]
562    fn test_json_to_sql_param_int_small() {
563        // Small integer fits in i32
564        let param = json_to_sql_param(json!(42));
565        assert!(matches!(param, SqlParam::I32(42)));
566    }
567
568    #[test]
569    fn test_json_to_sql_param_int_large() {
570        // Large integer that exceeds i32 range
571        let big = i64::MAX;
572        let param = json_to_sql_param(json!(big));
573        assert!(matches!(param, SqlParam::I64(_)));
574    }
575
576    #[test]
577    fn test_json_to_sql_param_float() {
578        let param = json_to_sql_param(json!(3.14));
579        match param {
580            SqlParam::F64(v) => assert!((v - 3.14).abs() < 0.001),
581            _ => panic!("expected F64"),
582        }
583    }
584
585    #[test]
586    fn test_json_to_sql_param_string() {
587        let param = json_to_sql_param(json!("hello world"));
588        match param {
589            SqlParam::Text(s) => assert_eq!(s, "hello world"),
590            _ => panic!("expected Text"),
591        }
592    }
593
594    #[test]
595    fn test_json_to_sql_param_uuid_string() {
596        let param = json_to_sql_param(json!("550e8400-e29b-41d4-a716-446655440000"));
597        match param {
598            SqlParam::Uuid(u) => assert_eq!(u.to_string(), "550e8400-e29b-41d4-a716-446655440000"),
599            _ => panic!("expected Uuid, got {:?}", param),
600        }
601    }
602
603    #[test]
604    fn test_json_to_sql_param_json_object() {
605        let param = json_to_sql_param(json!({"key": "value"}));
606        assert!(matches!(param, SqlParam::Json(_)));
607    }
608
609    #[test]
610    fn test_json_to_sql_param_json_array() {
611        let param = json_to_sql_param(json!([1, 2, 3]));
612        assert!(matches!(param, SqlParam::Json(_)));
613    }
614
615    // ---- TypedQueryBuilder ----
616
617    // Minimal Table implementation for testing
618    #[derive(Debug, Clone, serde::Deserialize)]
619    struct TestTable {
620        id: i32,
621        name: String,
622    }
623
624    impl crate::table::Table for TestTable {
625        fn table_name() -> &'static str {
626            "test_table"
627        }
628
629        fn primary_key_columns() -> &'static [&'static str] {
630            &["id"]
631        }
632
633        fn column_names() -> &'static [&'static str] {
634            &["id", "name"]
635        }
636
637        fn insertable_columns() -> &'static [&'static str] {
638            &["name"]
639        }
640
641        fn field_to_column(field: &str) -> Option<&'static str> {
642            match field {
643                "id" => Some("id"),
644                "name" => Some("name"),
645                _ => None,
646            }
647        }
648
649        fn column_to_field(column: &str) -> Option<&'static str> {
650            match column {
651                "id" => Some("id"),
652                "name" => Some("name"),
653                _ => None,
654            }
655        }
656
657        fn bind_insert(&self) -> Vec<SqlParam> {
658            vec![SqlParam::Text(self.name.clone())]
659        }
660
661        fn bind_update(&self) -> Vec<SqlParam> {
662            vec![SqlParam::Text(self.name.clone())]
663        }
664
665        fn bind_primary_key(&self) -> Vec<SqlParam> {
666            vec![SqlParam::I32(self.id)]
667        }
668    }
669
670    #[test]
671    fn test_typed_query_builder_select() {
672        let typed_builder: TypedQueryBuilder<TestTable> =
673            TypedQueryBuilder::new(make_backend(), "public".to_string());
674        let select_builder = typed_builder.select();
675        assert_eq!(select_builder.parts.table, "test_table");
676        assert!(select_builder.parts.select_columns.is_none());
677    }
678
679    #[test]
680    fn test_typed_query_builder_select_columns() {
681        let typed_builder: TypedQueryBuilder<TestTable> =
682            TypedQueryBuilder::new(make_backend(), "public".to_string());
683        let select_builder = typed_builder.select_columns("id, name");
684        let cols = select_builder.parts.select_columns.unwrap();
685        assert_eq!(cols, "\"id\", \"name\"");
686    }
687
688    #[test]
689    fn test_typed_query_builder_delete() {
690        let typed_builder: TypedQueryBuilder<TestTable> =
691            TypedQueryBuilder::new(make_backend(), "public".to_string());
692        let delete_builder = typed_builder.delete();
693        assert_eq!(delete_builder.parts.operation, SqlOperation::Delete);
694        assert_eq!(delete_builder.parts.table, "test_table");
695    }
696
697    #[test]
698    fn test_typed_query_builder_insert() {
699        let typed_builder: TypedQueryBuilder<TestTable> =
700            TypedQueryBuilder::new(make_backend(), "public".to_string());
701        let value = TestTable {
702            id: 1,
703            name: "Alice".to_string(),
704        };
705        let insert_builder = typed_builder.insert(&value);
706        assert_eq!(insert_builder.parts.operation, SqlOperation::Insert);
707        assert_eq!(insert_builder.parts.set_clauses.len(), 1);
708        assert_eq!(insert_builder.parts.set_clauses[0].0, "name");
709    }
710
711    #[test]
712    fn test_typed_query_builder_update() {
713        let typed_builder: TypedQueryBuilder<TestTable> =
714            TypedQueryBuilder::new(make_backend(), "public".to_string());
715        let value = TestTable {
716            id: 1,
717            name: "Updated".to_string(),
718        };
719        let update_builder = typed_builder.update(&value);
720        assert_eq!(update_builder.parts.operation, SqlOperation::Update);
721        // Should have 1 set clause (name, excluding PK)
722        assert_eq!(update_builder.parts.set_clauses.len(), 1);
723        // Should have 1 filter for PK
724        assert_eq!(update_builder.parts.filters.len(), 1);
725    }
726
727    #[test]
728    fn test_typed_query_builder_upsert() {
729        let typed_builder: TypedQueryBuilder<TestTable> =
730            TypedQueryBuilder::new(make_backend(), "public".to_string());
731        let value = TestTable {
732            id: 1,
733            name: "Alice".to_string(),
734        };
735        let upsert_builder = typed_builder.upsert(&value);
736        assert_eq!(upsert_builder.parts.operation, SqlOperation::Upsert);
737        // Should have PK + insertable columns in set_clauses
738        assert_eq!(upsert_builder.parts.set_clauses.len(), 2);
739        // Conflict columns should be set to PK
740        assert_eq!(upsert_builder.parts.conflict_columns, vec!["id"]);
741    }
742}