Skip to main content

drizzle_core/builder/
conflict.rs

1//! Shared `ON CONFLICT` builder machinery.
2
3use core::marker::PhantomData;
4
5use crate::expr::Expr;
6use crate::prelude::Box;
7use crate::sql::{SQL, Token};
8use crate::traits::{SQLParam, ToSQL};
9use crate::types::BooleanLike;
10
11/// Converts a dialect conflict target into an `ON CONFLICT ...` SQL fragment.
12pub trait ConflictTargetSql<'a, V: SQLParam> {
13    fn into_target_sql(self, target_where: Option<SQL<'a, V>>) -> SQL<'a, V>;
14}
15
16/// Shared column-list conflict target: `ON CONFLICT (col1, col2)`.
17#[derive(Debug, Clone)]
18pub struct ConflictColumnsTarget<'a, V: SQLParam> {
19    columns: SQL<'a, V>,
20}
21
22impl<'a, V: SQLParam> ConflictColumnsTarget<'a, V> {
23    #[inline]
24    #[must_use]
25    pub fn new(columns: SQL<'a, V>) -> Self {
26        Self { columns }
27    }
28}
29
30impl<'a, V: SQLParam> ConflictTargetSql<'a, V> for ConflictColumnsTarget<'a, V> {
31    fn into_target_sql(self, target_where: Option<SQL<'a, V>>) -> SQL<'a, V> {
32        let mut target = SQL::from_iter([Token::ON, Token::CONFLICT, Token::LPAREN])
33            .append(self.columns)
34            .push(Token::RPAREN);
35        if let Some(target_where) = target_where {
36            target = target.push(Token::WHERE).append(target_where);
37        }
38        target
39    }
40}
41
42/// PostgreSQL conflict target, including `ON CONSTRAINT`.
43#[derive(Debug, Clone)]
44pub enum PostgresConflictTarget<'a, V: SQLParam> {
45    Columns(Box<ConflictColumnsTarget<'a, V>>),
46    Constraint(&'static str),
47}
48
49impl<'a, V: SQLParam> PostgresConflictTarget<'a, V> {
50    #[inline]
51    #[must_use]
52    pub fn columns(columns: SQL<'a, V>) -> Self {
53        Self::Columns(Box::new(ConflictColumnsTarget::new(columns)))
54    }
55
56    #[inline]
57    #[must_use]
58    pub const fn constraint(name: &'static str) -> Self {
59        Self::Constraint(name)
60    }
61}
62
63impl<'a, V: SQLParam> ConflictTargetSql<'a, V> for PostgresConflictTarget<'a, V> {
64    fn into_target_sql(self, target_where: Option<SQL<'a, V>>) -> SQL<'a, V> {
65        match self {
66            Self::Columns(columns) => (*columns).into_target_sql(target_where),
67            Self::Constraint(name) => {
68                SQL::from_iter([Token::ON, Token::CONFLICT, Token::ON, Token::CONSTRAINT])
69                    .append(SQL::ident(name))
70            }
71        }
72    }
73}
74
75/// Dialect adapter for producing the concrete insert builder type.
76pub trait OnConflictOutput<'a, V: SQLParam, Schema, Table> {
77    type OnConflictSet;
78    type DoUpdateSet;
79
80    fn on_conflict(sql: SQL<'a, V>) -> Self::OnConflictSet;
81    fn do_update(sql: SQL<'a, V>) -> Self::DoUpdateSet;
82}
83
84/// Intermediate builder for typed `ON CONFLICT` clause construction.
85#[derive(Debug, Clone)]
86pub struct OnConflictBuilder<'a, V, Schema, Table, Target, Output>
87where
88    V: SQLParam,
89{
90    sql: SQL<'a, V>,
91    target: Target,
92    target_where: Option<SQL<'a, V>>,
93    schema: PhantomData<Schema>,
94    table: PhantomData<Table>,
95    output: PhantomData<Output>,
96}
97
98impl<'a, V, Schema, Table, Target, Output> OnConflictBuilder<'a, V, Schema, Table, Target, Output>
99where
100    V: SQLParam,
101    Target: ConflictTargetSql<'a, V>,
102    Output: OnConflictOutput<'a, V, Schema, Table>,
103{
104    #[inline]
105    #[must_use]
106    pub fn new(sql: SQL<'a, V>, target: Target) -> Self {
107        Self {
108            sql,
109            target,
110            target_where: None,
111            schema: PhantomData,
112            table: PhantomData,
113            output: PhantomData,
114        }
115    }
116
117    /// Seeds a partial-index predicate supplied by a typed conflict target.
118    #[doc(hidden)]
119    #[must_use]
120    pub fn with_target_where_sql(mut self, target_where: Option<SQL<'a, V>>) -> Self {
121        self.target_where = target_where;
122        self
123    }
124
125    /// Adds a WHERE clause to the conflict target for partial index matching.
126    ///
127    /// A typed partial-index target supplies its declared predicate automatically.
128    /// Calling this method after selecting such a target replaces that predicate,
129    /// so the replacement must still identify the same unique index.
130    #[must_use]
131    pub fn r#where<E>(mut self, condition: E) -> Self
132    where
133        E: Expr<'a, V>,
134        E::SQLType: BooleanLike,
135    {
136        self.target_where = Some(condition.into_expr_sql());
137        self
138    }
139
140    fn into_parts(self) -> (SQL<'a, V>, SQL<'a, V>) {
141        (self.sql, self.target.into_target_sql(self.target_where))
142    }
143
144    /// Resolves the conflict by doing nothing.
145    #[must_use]
146    pub fn do_nothing(self) -> Output::OnConflictSet {
147        let (sql, target) = self.into_parts();
148        Output::on_conflict(sql.append(target.push(Token::DO).push(Token::NOTHING)))
149    }
150
151    /// Resolves the conflict by updating the existing row.
152    pub fn do_update(self, set: impl ToSQL<'a, V>) -> Output::DoUpdateSet {
153        let (sql, target) = self.into_parts();
154        let conflict = target
155            .push(Token::DO)
156            .push(Token::UPDATE)
157            .push(Token::SET)
158            .append(set.into_sql());
159        Output::do_update(sql.append(conflict))
160    }
161}