Skip to main content

supabase_client_query/
upsert.rs

1use std::marker::PhantomData;
2
3use serde::de::DeserializeOwned;
4
5use supabase_client_core::SupabaseResponse;
6
7use crate::backend::QueryBackend;
8use crate::modifier::Modifiable;
9use crate::sql::{ParamStore, SqlParts};
10
11/// Builder for UPSERT (INSERT ... ON CONFLICT DO UPDATE) queries.
12/// Implements Modifiable. Call `.select()` for RETURNING clause.
13pub struct UpsertBuilder<T> {
14    pub(crate) backend: QueryBackend,
15    pub(crate) parts: SqlParts,
16    pub(crate) params: ParamStore,
17    pub(crate) _marker: PhantomData<T>,
18}
19
20impl<T> Modifiable for UpsertBuilder<T> {
21    fn parts_mut(&mut self) -> &mut SqlParts {
22        &mut self.parts
23    }
24}
25
26impl<T> UpsertBuilder<T> {
27    /// Set the conflict columns for ON CONFLICT.
28    pub fn on_conflict(mut self, columns: &[&str]) -> Self {
29        self.parts.conflict_columns = columns.iter().map(|c| c.to_string()).collect();
30        self
31    }
32
33    /// Set a constraint name for ON CONFLICT ON CONSTRAINT.
34    pub fn on_conflict_constraint(mut self, constraint: &str) -> Self {
35        self.parts.conflict_constraint = Some(constraint.to_string());
36        self
37    }
38
39    /// Use ON CONFLICT DO NOTHING instead of DO UPDATE.
40    ///
41    /// When set, duplicate rows are silently ignored instead of updated.
42    pub fn ignore_duplicates(mut self) -> Self {
43        self.parts.ignore_duplicates = true;
44        self
45    }
46
47    /// Override the schema for this query.
48    ///
49    /// Generates `"schema"."table"` instead of the default schema.
50    pub fn schema(mut self, schema: &str) -> Self {
51        self.parts.schema_override = Some(schema.to_string());
52        self
53    }
54
55    /// Add RETURNING * clause.
56    pub fn select(mut self) -> Self {
57        self.parts.returning = Some("*".to_string());
58        self
59    }
60
61    /// Add RETURNING with specific columns.
62    pub fn select_columns(mut self, columns: &str) -> Self {
63        if columns == "*" || columns.is_empty() {
64            self.parts.returning = Some("*".to_string());
65        } else {
66            let quoted = columns
67                .split(',')
68                .map(|c| {
69                    let c = c.trim();
70                    if c.contains('(') || c.contains('*') || c.contains('"') {
71                        c.to_string()
72                    } else {
73                        format!("\"{}\"", c)
74                    }
75                })
76                .collect::<Vec<_>>()
77                .join(", ");
78            self.parts.returning = Some(quoted);
79        }
80        self
81    }
82}
83
84// REST-only mode: only DeserializeOwned + Send needed
85#[cfg(not(feature = "direct-sql"))]
86impl<T> UpsertBuilder<T>
87where
88    T: DeserializeOwned + Send,
89{
90    /// Execute the UPSERT query.
91    pub async fn execute(self) -> SupabaseResponse<T> {
92        let QueryBackend::Rest { ref http, ref base_url, ref api_key, ref schema } = self.backend;
93        let (url, headers, body) = match crate::postgrest::build_postgrest_upsert(
94            base_url, &self.parts, &self.params,
95        ) {
96            Ok(r) => r,
97            Err(e) => return SupabaseResponse::error(
98                supabase_client_core::SupabaseError::QueryBuilder(e),
99            ),
100        };
101        crate::postgrest_execute::execute_rest(
102            http, reqwest::Method::POST, &url, headers, Some(body), api_key, schema, &self.parts,
103        ).await
104    }
105}
106
107// Direct-SQL mode: additional FromRow + Unpin bounds
108#[cfg(feature = "direct-sql")]
109impl<T> UpsertBuilder<T>
110where
111    T: DeserializeOwned + Send + Unpin + for<'r> sqlx::FromRow<'r, sqlx::postgres::PgRow>,
112{
113    /// Execute the UPSERT query.
114    pub async fn execute(self) -> SupabaseResponse<T> {
115        match &self.backend {
116            QueryBackend::Rest { http, base_url, api_key, schema } => {
117                let (url, headers, body) = match crate::postgrest::build_postgrest_upsert(
118                    base_url, &self.parts, &self.params,
119                ) {
120                    Ok(r) => r,
121                    Err(e) => return SupabaseResponse::error(
122                        supabase_client_core::SupabaseError::QueryBuilder(e),
123                    ),
124                };
125                crate::postgrest_execute::execute_rest(
126                    http, reqwest::Method::POST, &url, headers, Some(body), api_key, schema, &self.parts,
127                ).await
128            }
129            QueryBackend::DirectSql { pool } => {
130                crate::execute::execute_typed::<T>(pool, &self.parts, &self.params).await
131            }
132        }
133    }
134}