drizzle_postgres/builder/
insert.rs

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