drizzle_postgres/builder/
insert.rs

1use crate::traits::PostgresTable;
2use crate::values::PostgresValue;
3use drizzle_core::{SQL, ToSQL};
4use std::fmt::Debug;
5use std::marker::PhantomData;
6
7// Import the ExecutableState trait
8use super::ExecutableState;
9
10//------------------------------------------------------------------------------
11// Type State Markers
12//------------------------------------------------------------------------------
13
14/// Marker for the initial state of InsertBuilder.
15#[derive(Debug, Clone, Copy, Default)]
16pub struct InsertInitial;
17
18/// Marker for the state after VALUES are set.
19#[derive(Debug, Clone, Copy, Default)]
20pub struct InsertValuesSet;
21
22/// Marker for the state after RETURNING clause is added.
23#[derive(Debug, Clone, Copy, Default)]
24pub struct InsertReturningSet;
25
26/// Marker for the state after ON CONFLICT is set.
27#[derive(Debug, Clone, Copy, Default)]
28pub struct InsertOnConflictSet;
29
30// Const constructors for insert marker types
31impl InsertInitial {
32    #[inline]
33    pub const fn new() -> Self {
34        Self
35    }
36}
37impl InsertValuesSet {
38    #[inline]
39    pub const fn new() -> Self {
40        Self
41    }
42}
43impl InsertReturningSet {
44    #[inline]
45    pub const fn new() -> Self {
46        Self
47    }
48}
49impl InsertOnConflictSet {
50    #[inline]
51    pub const fn new() -> Self {
52        Self
53    }
54}
55
56/// Conflict target specification for PostgreSQL ON CONFLICT clause
57#[derive(Debug, Clone)]
58#[allow(clippy::large_enum_variant)]
59pub enum ConflictTarget<'a> {
60    /// Target specific columns: ON CONFLICT (column1, column2, ...)
61    Columns(SQL<'a, PostgresValue<'a>>),
62    /// Target specific columns with WHERE clause for partial unique indexes
63    /// ON CONFLICT (column1, column2, ...) WHERE condition
64    ColumnsWhere {
65        columns: SQL<'a, PostgresValue<'a>>,
66        where_clause: SQL<'a, PostgresValue<'a>>,
67    },
68    /// Target a specific constraint by name: ON CONFLICT ON CONSTRAINT constraint_name
69    Constraint(String),
70}
71
72/// Conflict resolution strategies for PostgreSQL
73#[derive(Debug, Clone)]
74#[allow(clippy::large_enum_variant)]
75pub enum Conflict<'a> {
76    /// Do nothing on conflict - ON CONFLICT DO NOTHING or ON CONFLICT (target) DO NOTHING
77    DoNothing {
78        /// Optional target specification. If None, matches any conflict
79        target: Option<ConflictTarget<'a>>,
80    },
81    /// Update on conflict - ON CONFLICT (target) DO UPDATE SET ...
82    DoUpdate {
83        /// Required target specification for DO UPDATE
84        target: ConflictTarget<'a>,
85        /// SET clause assignments (can use EXCLUDED.column to reference proposed values)
86        set: SQL<'a, PostgresValue<'a>>,
87        /// Optional WHERE clause for conditional updates
88        /// Applied after SET: DO UPDATE SET ... WHERE condition
89        where_clause: Option<SQL<'a, PostgresValue<'a>>>,
90    },
91}
92
93impl<'a> Default for Conflict<'a> {
94    fn default() -> Self {
95        Self::DoNothing { target: None }
96    }
97}
98
99impl<'a> Conflict<'a> {
100    /// Create a DO NOTHING conflict resolution with specific columns
101    pub fn do_nothing_on_columns<T>(columns: T) -> Self
102    where
103        T: ToSQL<'a, PostgresValue<'a>>,
104    {
105        Conflict::DoNothing {
106            target: Some(ConflictTarget::Columns(columns.to_sql())),
107        }
108    }
109
110    /// Create a DO NOTHING conflict resolution with a constraint name
111    pub fn do_nothing_on_constraint(constraint_name: String) -> Self {
112        Conflict::DoNothing {
113            target: Some(ConflictTarget::Constraint(constraint_name)),
114        }
115    }
116
117    /// Create a DO UPDATE conflict resolution
118    pub fn do_update<S, W>(target: ConflictTarget<'a>, set: S, where_clause: Option<W>) -> Self
119    where
120        S: ToSQL<'a, PostgresValue<'a>>,
121        W: ToSQL<'a, PostgresValue<'a>>,
122    {
123        Conflict::DoUpdate {
124            target,
125            set: set.to_sql(),
126            where_clause: where_clause.map(|w| w.to_sql()),
127        }
128    }
129}
130
131impl<'a> ConflictTarget<'a> {
132    /// Create a column target
133    pub fn columns<T>(columns: T) -> Self
134    where
135        T: ToSQL<'a, PostgresValue<'a>>,
136    {
137        ConflictTarget::Columns(columns.to_sql())
138    }
139
140    /// Create a column target with WHERE clause for partial unique indexes
141    pub fn columns_where<T, W>(columns: T, where_clause: W) -> Self
142    where
143        T: ToSQL<'a, PostgresValue<'a>>,
144        W: ToSQL<'a, PostgresValue<'a>>,
145    {
146        ConflictTarget::ColumnsWhere {
147            columns: columns.to_sql(),
148            where_clause: where_clause.to_sql(),
149        }
150    }
151
152    /// Create a constraint target
153    pub fn constraint(constraint_name: String) -> Self {
154        ConflictTarget::Constraint(constraint_name)
155    }
156}
157
158// Mark states that can execute insert queries
159impl ExecutableState for InsertValuesSet {}
160impl ExecutableState for InsertReturningSet {}
161impl ExecutableState for InsertOnConflictSet {}
162
163//------------------------------------------------------------------------------
164// InsertBuilder Definition
165//------------------------------------------------------------------------------
166
167/// Builds an INSERT query specifically for PostgreSQL
168pub type InsertBuilder<'a, Schema, State, Table> = super::QueryBuilder<'a, Schema, State, Table>;
169
170//------------------------------------------------------------------------------
171// Initial State Implementation
172//------------------------------------------------------------------------------
173
174impl<'a, Schema, Table> InsertBuilder<'a, Schema, InsertInitial, Table>
175where
176    Table: PostgresTable<'a>,
177{
178    /// Sets values to insert and transitions to ValuesSet state
179    #[inline]
180    pub fn values<I, T>(self, values: I) -> InsertBuilder<'a, Schema, InsertValuesSet, Table>
181    where
182        I: IntoIterator<Item = Table::Insert<T>>,
183    {
184        let sql = crate::helpers::values::<'a, Table, T>(values);
185        InsertBuilder {
186            sql: self.sql.append(sql),
187            schema: PhantomData,
188            state: PhantomData,
189            table: PhantomData,
190        }
191    }
192}
193
194//------------------------------------------------------------------------------
195// Post-VALUES Implementation
196//------------------------------------------------------------------------------
197
198impl<'a, S, T> InsertBuilder<'a, S, InsertValuesSet, T> {
199    /// Adds conflict resolution clause following PostgreSQL ON CONFLICT syntax
200    pub fn on_conflict(
201        self,
202        conflict: Conflict<'a>,
203    ) -> InsertBuilder<'a, S, InsertOnConflictSet, T> {
204        let conflict_sql = match conflict {
205            Conflict::DoNothing { target } => {
206                let mut sql = SQL::raw("ON CONFLICT");
207
208                if let Some(target) = target {
209                    sql = sql.append(Self::build_conflict_target(target));
210                }
211
212                sql.append(SQL::raw(" DO NOTHING"))
213            }
214            Conflict::DoUpdate {
215                target,
216                set,
217                where_clause,
218            } => {
219                let mut sql = SQL::raw("ON CONFLICT")
220                    .append(Self::build_conflict_target(target))
221                    .append(SQL::raw(" DO UPDATE SET "))
222                    .append(set);
223
224                // Add optional WHERE clause for conditional updates
225                if let Some(where_clause) = where_clause {
226                    sql = sql.append(SQL::raw(" WHERE ")).append(where_clause);
227                }
228
229                sql
230            }
231        };
232
233        InsertBuilder {
234            sql: self.sql.append(conflict_sql),
235            schema: PhantomData,
236            state: PhantomData,
237            table: PhantomData,
238        }
239    }
240
241    /// Helper method to build the conflict target portion of ON CONFLICT
242    fn build_conflict_target(target: ConflictTarget<'a>) -> SQL<'a, PostgresValue<'a>> {
243        match target {
244            ConflictTarget::Columns(columns) => {
245                SQL::raw(" (").append(columns).append(SQL::raw(")"))
246            }
247            ConflictTarget::ColumnsWhere {
248                columns,
249                where_clause,
250            } => SQL::raw(" (")
251                .append(columns)
252                .append(SQL::raw(") WHERE "))
253                .append(where_clause),
254            ConflictTarget::Constraint(constraint_name) => {
255                SQL::raw(" ON CONSTRAINT ").append(SQL::raw(constraint_name))
256            }
257        }
258    }
259
260    /// Shorthand for ON CONFLICT DO NOTHING (matches any conflict)
261    pub fn on_conflict_do_nothing(self) -> InsertBuilder<'a, S, InsertOnConflictSet, T> {
262        self.on_conflict(Conflict::default())
263    }
264
265    /// Shorthand for ON CONFLICT (columns...) DO NOTHING
266    pub fn on_conflict_do_nothing_on<C>(
267        self,
268        columns: C,
269    ) -> InsertBuilder<'a, S, InsertOnConflictSet, T>
270    where
271        C: ToSQL<'a, PostgresValue<'a>>,
272    {
273        self.on_conflict(Conflict::do_nothing_on_columns(columns))
274    }
275
276    /// Adds a RETURNING clause and transitions to ReturningSet state
277    #[inline]
278    pub fn returning(
279        self,
280        columns: impl ToSQL<'a, PostgresValue<'a>>,
281    ) -> InsertBuilder<'a, S, InsertReturningSet, T> {
282        let returning_sql = crate::helpers::returning(columns);
283        InsertBuilder {
284            sql: self.sql.append(returning_sql),
285            schema: PhantomData,
286            state: PhantomData,
287            table: PhantomData,
288        }
289    }
290}
291
292//------------------------------------------------------------------------------
293// Post-ON CONFLICT Implementation
294//------------------------------------------------------------------------------
295
296impl<'a, S, T> InsertBuilder<'a, S, InsertOnConflictSet, T> {
297    /// Adds a RETURNING clause after ON CONFLICT
298    #[inline]
299    pub fn returning(
300        self,
301        columns: impl ToSQL<'a, PostgresValue<'a>>,
302    ) -> InsertBuilder<'a, S, InsertReturningSet, T> {
303        let returning_sql = crate::helpers::returning(columns);
304        InsertBuilder {
305            sql: self.sql.append(returning_sql),
306            schema: PhantomData,
307            state: PhantomData,
308            table: PhantomData,
309        }
310    }
311}
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316    use drizzle_core::{SQL, ToSQL};
317
318    #[test]
319    fn test_insert_builder_creation() {
320        let builder = InsertBuilder::<(), InsertInitial, ()> {
321            sql: SQL::raw("INSERT INTO test"),
322            schema: PhantomData,
323            state: PhantomData,
324            table: PhantomData,
325        };
326
327        assert_eq!(builder.to_sql().sql(), "INSERT INTO test");
328    }
329
330    #[test]
331    fn test_conflict_types() {
332        let do_nothing = Conflict::DoNothing { target: None };
333        let do_update = Conflict::DoUpdate {
334            target: ConflictTarget::Columns(SQL::raw("id")),
335            set: SQL::raw("name = EXCLUDED.name"),
336            where_clause: None,
337        };
338
339        match do_nothing {
340            Conflict::DoNothing { .. } => (),
341            _ => panic!("Expected DoNothing"),
342        }
343
344        match do_update {
345            Conflict::DoUpdate { .. } => (),
346            _ => panic!("Expected DoUpdate"),
347        }
348    }
349}