drizzle_postgres/builder/insert.rs
1#[cfg(not(feature = "std"))]
2use crate::prelude::*;
3use crate::traits::PostgresTable;
4use crate::values::PostgresValue;
5use core::fmt::Debug;
6use core::marker::PhantomData;
7use drizzle_core::{ConflictTarget, NamedConstraint, SQL, ToSQL, Token};
8
9// Import the ExecutableState trait
10use super::ExecutableState;
11
12//------------------------------------------------------------------------------
13// Type State Markers
14//------------------------------------------------------------------------------
15
16/// Marker for the initial state of `InsertBuilder`.
17#[derive(Debug, Clone, Copy, Default)]
18pub struct InsertInitial;
19
20/// Marker for the state after VALUES are set.
21#[derive(Debug, Clone, Copy, Default)]
22pub struct InsertValuesSet;
23
24/// Marker for the state after RETURNING clause is added.
25#[derive(Debug, Clone, Copy, Default)]
26pub struct InsertReturningSet;
27
28/// Marker for the state after ON CONFLICT is set.
29#[derive(Debug, Clone, Copy, Default)]
30pub struct InsertOnConflictSet;
31
32/// Marker for the state after DO UPDATE SET (before optional WHERE).
33#[derive(Debug, Clone, Copy, Default)]
34pub struct InsertDoUpdateSet;
35
36// Mark states that can execute insert queries
37impl ExecutableState for InsertValuesSet {}
38impl ExecutableState for InsertReturningSet {}
39impl ExecutableState for InsertOnConflictSet {}
40impl ExecutableState for InsertDoUpdateSet {}
41
42//------------------------------------------------------------------------------
43// OnConflictBuilder
44//------------------------------------------------------------------------------
45
46/// Internal: how the conflict target was specified.
47#[derive(Debug, Clone)]
48enum ConflictTargetKind<'a> {
49 /// ON CONFLICT (col1, col2)
50 Columns(Box<SQL<'a, PostgresValue<'a>>>),
51 /// ON CONFLICT ON CONSTRAINT "`constraint_name`"
52 Constraint(&'static str),
53}
54
55/// Intermediate builder for typed ON CONFLICT clause construction (`PostgreSQL`).
56///
57/// Created by [`InsertBuilder::on_conflict()`] or
58/// [`InsertBuilder::on_conflict_on_constraint()`].
59/// Call [`do_nothing()`](Self::do_nothing) or [`do_update()`](Self::do_update)
60/// to complete the clause.
61#[derive(Debug, Clone)]
62pub struct OnConflictBuilder<'a, S, T> {
63 sql: SQL<'a, PostgresValue<'a>>,
64 target: ConflictTargetKind<'a>,
65 target_where: Option<SQL<'a, PostgresValue<'a>>>,
66 schema: PhantomData<S>,
67 table: PhantomData<T>,
68}
69
70impl<'a, S, T> OnConflictBuilder<'a, S, T> {
71 /// Adds a WHERE clause to the conflict target for partial index matching.
72 ///
73 /// Generates: `ON CONFLICT (col) WHERE condition DO ...`
74 ///
75 /// Note: WHERE is only meaningful for column-based targets, not for
76 /// `ON CONFLICT ON CONSTRAINT` targets.
77 #[must_use]
78 pub fn r#where<E>(mut self, condition: E) -> Self
79 where
80 E: drizzle_core::expr::Expr<'a, PostgresValue<'a>>,
81 E::SQLType: drizzle_core::types::BooleanLike,
82 {
83 self.target_where = Some(condition.to_sql());
84 self
85 }
86
87 /// Splits into (base insert SQL, conflict target SQL prefix).
88 fn into_parts(self) -> (SQL<'a, PostgresValue<'a>>, SQL<'a, PostgresValue<'a>>) {
89 let target = match self.target {
90 ConflictTargetKind::Columns(cols) => {
91 let mut t = SQL::from_iter([Token::ON, Token::CONFLICT, Token::LPAREN])
92 .append(*cols)
93 .push(Token::RPAREN);
94 if let Some(tw) = self.target_where {
95 t = t.push(Token::WHERE).append(tw);
96 }
97 t
98 }
99 ConflictTargetKind::Constraint(name) => {
100 SQL::from_iter([Token::ON, Token::CONFLICT, Token::ON, Token::CONSTRAINT])
101 .append(SQL::ident(name))
102 }
103 };
104 (self.sql, target)
105 }
106
107 /// Resolves the conflict by doing nothing (ignoring the conflicting row).
108 ///
109 /// Generates: `ON CONFLICT (col1, col2) DO NOTHING`
110 #[must_use]
111 pub fn do_nothing(self) -> InsertBuilder<'a, S, InsertOnConflictSet, T> {
112 let (sql, target) = self.into_parts();
113 InsertBuilder {
114 sql: sql.append(target.push(Token::DO).push(Token::NOTHING)),
115 schema: PhantomData,
116 state: PhantomData,
117 table: PhantomData,
118 marker: PhantomData,
119 row: PhantomData,
120 grouped: PhantomData,
121 }
122 }
123
124 /// Resolves the conflict by updating the existing row.
125 ///
126 /// The `set` parameter accepts any `ToSQL` value, typically an `UpdateModel`
127 /// which generates the SET clause assignments. Use `EXCLUDED.column` to
128 /// reference the proposed insert values.
129 ///
130 /// Generates: `ON CONFLICT (col1, col2) DO UPDATE SET ...`
131 ///
132 /// Chain `.r#where(condition)` to add a conditional update filter.
133 pub fn do_update(
134 self,
135 set: impl ToSQL<'a, PostgresValue<'a>>,
136 ) -> InsertBuilder<'a, S, InsertDoUpdateSet, T> {
137 let (sql, target) = self.into_parts();
138 let conflict = target
139 .push(Token::DO)
140 .push(Token::UPDATE)
141 .push(Token::SET)
142 .append(set.to_sql());
143 InsertBuilder {
144 sql: sql.append(conflict),
145 schema: PhantomData,
146 state: PhantomData,
147 table: PhantomData,
148 marker: PhantomData,
149 row: PhantomData,
150 grouped: PhantomData,
151 }
152 }
153}
154
155//------------------------------------------------------------------------------
156// InsertBuilder Definition
157//------------------------------------------------------------------------------
158
159/// Builds an INSERT query specifically for `PostgreSQL`.
160///
161/// Provides a type-safe, fluent API for constructing INSERT statements
162/// with support for typed conflict resolution, batch inserts, and returning clauses.
163///
164/// ## Type Parameters
165///
166/// - `Schema`: The database schema type, ensuring only valid tables can be referenced
167/// - `State`: The current builder state, enforcing proper query construction order
168/// - `Table`: The table being inserted into
169pub type InsertBuilder<'a, Schema, State, Table, Marker = (), Row = ()> =
170 super::QueryBuilder<'a, Schema, State, Table, Marker, Row>;
171
172type ReturningMarker<Table, Columns> = drizzle_core::Scoped<
173 <Columns as drizzle_core::IntoSelectTarget>::Marker,
174 drizzle_core::Cons<Table, drizzle_core::Nil>,
175>;
176
177type ReturningRow<Table, Columns> =
178 <<Columns as drizzle_core::IntoSelectTarget>::Marker as drizzle_core::ResolveRow<Table>>::Row;
179
180type ReturningBuilder<'a, S, T, Columns> = InsertBuilder<
181 'a,
182 S,
183 InsertReturningSet,
184 T,
185 ReturningMarker<T, Columns>,
186 ReturningRow<T, Columns>,
187>;
188
189//------------------------------------------------------------------------------
190// Initial State Implementation
191//------------------------------------------------------------------------------
192
193impl<'a, Schema, Table> InsertBuilder<'a, Schema, InsertInitial, Table>
194where
195 Table: PostgresTable<'a>,
196{
197 /// Specifies a single row to insert. Shorthand for `.values([row])`.
198 #[inline]
199 pub fn value<T>(
200 self,
201 value: Table::Insert<T>,
202 ) -> InsertBuilder<'a, Schema, InsertValuesSet, Table> {
203 self.values([value])
204 }
205
206 /// Specifies multiple rows to insert.
207 #[inline]
208 pub fn values<I, T>(self, values: I) -> InsertBuilder<'a, Schema, InsertValuesSet, Table>
209 where
210 I: IntoIterator<Item = Table::Insert<T>>,
211 {
212 let sql = crate::helpers::values::<'a, Table, T>(values);
213 InsertBuilder {
214 sql: self.sql.append(sql),
215 schema: PhantomData,
216 state: PhantomData,
217 table: PhantomData,
218 marker: PhantomData,
219 row: PhantomData,
220 grouped: PhantomData,
221 }
222 }
223}
224
225//------------------------------------------------------------------------------
226// Post-VALUES Implementation
227//------------------------------------------------------------------------------
228
229impl<'a, S, T> InsertBuilder<'a, S, InsertValuesSet, T> {
230 /// Begins a typed ON CONFLICT clause targeting specific columns.
231 ///
232 /// The target must implement `ConflictTarget<T>`, which is auto-generated for
233 /// primary key columns, unique columns, and unique indexes.
234 ///
235 /// Returns an [`OnConflictBuilder`] to specify `do_nothing()` or `do_update()`.
236 ///
237 /// # Examples
238 ///
239 /// ```rust
240 /// # extern crate self as drizzle;
241 /// # mod _drizzle {
242 /// # pub mod core { pub use drizzle_core::*; }
243 /// # pub mod error { pub use drizzle_core::error::*; }
244 /// # pub mod types { pub use drizzle_types::*; }
245 /// # pub mod migrations { pub use drizzle_migrations::*; }
246 /// # pub use drizzle_types::Dialect;
247 /// # pub use drizzle_types as ddl;
248 /// # pub mod postgres {
249 /// # pub mod values { pub use drizzle_postgres::values::*; }
250 /// # pub mod traits { pub use drizzle_postgres::traits::*; }
251 /// # pub mod common { pub use drizzle_postgres::common::*; }
252 /// # pub mod attrs { pub use drizzle_postgres::attrs::*; }
253 /// # pub mod builder { pub use drizzle_postgres::builder::*; }
254 /// # pub mod helpers { pub use drizzle_postgres::helpers::*; }
255 /// # pub mod expr { pub use drizzle_postgres::expr::*; }
256 /// # pub mod types { pub use drizzle_postgres::types::*; }
257 /// # pub struct Row;
258 /// # impl Row {
259 /// # pub fn get<'a, I, T>(&'a self, _: I) -> T { unimplemented!() }
260 /// # pub fn try_get<'a, I, T>(&'a self, _: I) -> Result<T, Box<dyn std::error::Error + Sync + Send>> { unimplemented!() }
261 /// # }
262 /// # pub mod prelude {
263 /// # pub use drizzle_macros::{PostgresTable, PostgresSchema, PostgresIndex};
264 /// # pub use drizzle_postgres::attrs::*;
265 /// # pub use drizzle_postgres::common::PostgresSchemaType;
266 /// # pub use drizzle_postgres::traits::{PostgresColumn, PostgresTable};
267 /// # pub use drizzle_postgres::values::{PostgresInsertValue, PostgresUpdateValue, PostgresValue};
268 /// # pub use drizzle_core::*;
269 /// # }
270 /// # }
271 /// # }
272 /// # pub use _drizzle::*;
273 /// # pub use const_format;
274 /// fn main() {
275 /// use drizzle::postgres::prelude::*;
276 /// use drizzle::postgres::builder::QueryBuilder;
277 ///
278 /// #[PostgresTable(name = "users")]
279 /// struct User {
280 /// #[column(serial, primary)]
281 /// id: i32,
282 /// name: String,
283 /// #[column(unique)]
284 /// email: Option<String>,
285 /// }
286 ///
287 /// #[PostgresIndex(unique)]
288 /// struct UserEmailIdx(User::email);
289 ///
290 /// #[derive(PostgresSchema)]
291 /// struct Schema {
292 /// user: User,
293 /// user_email_idx: UserEmailIdx,
294 /// }
295 ///
296 /// let builder = QueryBuilder::new::<Schema>();
297 /// let schema = Schema::new();
298 /// let user = schema.user;
299 ///
300 /// // Target a specific column
301 /// builder.insert(user).values([InsertUser::new("Alice")])
302 /// .on_conflict(user.id).do_nothing();
303 ///
304 /// // Target with DO UPDATE using EXCLUDED
305 /// builder.insert(user).values([InsertUser::new("Alice")])
306 /// .on_conflict(user.email).do_update(UpdateUser::default().with_name("updated"));
307 ///
308 /// // Target a unique index
309 /// builder.insert(user).values([InsertUser::new("Alice")])
310 /// .on_conflict(schema.user_email_idx).do_nothing();
311 /// }
312 /// ```
313 pub fn on_conflict<C: ConflictTarget<T>>(self, target: C) -> OnConflictBuilder<'a, S, T> {
314 let columns = target.conflict_columns();
315 let target_sql = SQL::join(columns.iter().map(|c| SQL::ident(*c)), Token::COMMA);
316 OnConflictBuilder {
317 sql: self.sql,
318 target: ConflictTargetKind::Columns(Box::new(target_sql)),
319 target_where: None,
320 schema: PhantomData,
321 table: PhantomData,
322 }
323 }
324
325 /// Begins a typed ON CONFLICT ON CONSTRAINT clause (PostgreSQL-only).
326 ///
327 /// The target must implement `NamedConstraint<T>`, which is auto-generated
328 /// for unique indexes.
329 ///
330 /// Returns an [`OnConflictBuilder`] to specify `do_nothing()` or `do_update()`.
331 ///
332 /// # Examples
333 ///
334 /// ```rust
335 /// # extern crate self as drizzle;
336 /// # mod _drizzle {
337 /// # pub mod core { pub use drizzle_core::*; }
338 /// # pub mod error { pub use drizzle_core::error::*; }
339 /// # pub mod types { pub use drizzle_types::*; }
340 /// # pub mod migrations { pub use drizzle_migrations::*; }
341 /// # pub use drizzle_types::Dialect;
342 /// # pub use drizzle_types as ddl;
343 /// # pub mod postgres {
344 /// # pub mod values { pub use drizzle_postgres::values::*; }
345 /// # pub mod traits { pub use drizzle_postgres::traits::*; }
346 /// # pub mod common { pub use drizzle_postgres::common::*; }
347 /// # pub mod attrs { pub use drizzle_postgres::attrs::*; }
348 /// # pub mod builder { pub use drizzle_postgres::builder::*; }
349 /// # pub mod helpers { pub use drizzle_postgres::helpers::*; }
350 /// # pub mod expr { pub use drizzle_postgres::expr::*; }
351 /// # pub mod types { pub use drizzle_postgres::types::*; }
352 /// # pub struct Row;
353 /// # impl Row {
354 /// # pub fn get<'a, I, T>(&'a self, _: I) -> T { unimplemented!() }
355 /// # pub fn try_get<'a, I, T>(&'a self, _: I) -> Result<T, Box<dyn std::error::Error + Sync + Send>> { unimplemented!() }
356 /// # }
357 /// # pub mod prelude {
358 /// # pub use drizzle_macros::{PostgresTable, PostgresSchema, PostgresIndex};
359 /// # pub use drizzle_postgres::attrs::*;
360 /// # pub use drizzle_postgres::common::PostgresSchemaType;
361 /// # pub use drizzle_postgres::traits::{PostgresColumn, PostgresTable};
362 /// # pub use drizzle_postgres::values::{PostgresInsertValue, PostgresUpdateValue, PostgresValue};
363 /// # pub use drizzle_core::*;
364 /// # }
365 /// # }
366 /// # }
367 /// # pub use _drizzle::*;
368 /// # pub use const_format;
369 /// fn main() {
370 /// use drizzle::postgres::prelude::*;
371 /// use drizzle::postgres::builder::QueryBuilder;
372 ///
373 /// #[PostgresTable(name = "users")]
374 /// struct User {
375 /// #[column(serial, primary)]
376 /// id: i32,
377 /// name: String,
378 /// #[column(unique)]
379 /// email: Option<String>,
380 /// }
381 ///
382 /// #[PostgresIndex(unique)]
383 /// struct UserEmailIdx(User::email);
384 ///
385 /// #[derive(PostgresSchema)]
386 /// struct Schema {
387 /// user: User,
388 /// user_email_idx: UserEmailIdx,
389 /// }
390 ///
391 /// let builder = QueryBuilder::new::<Schema>();
392 /// let schema = Schema::new();
393 ///
394 /// builder.insert(schema.user).values([InsertUser::new("Alice")])
395 /// .on_conflict_on_constraint(schema.user_email_idx).do_nothing();
396 /// }
397 /// ```
398 pub fn on_conflict_on_constraint<C: NamedConstraint<T>>(
399 self,
400 target: C,
401 ) -> OnConflictBuilder<'a, S, T> {
402 OnConflictBuilder {
403 sql: self.sql,
404 target: ConflictTargetKind::Constraint(target.constraint_name()),
405 target_where: None,
406 schema: PhantomData,
407 table: PhantomData,
408 }
409 }
410
411 /// Shorthand for `ON CONFLICT DO NOTHING` without specifying a target.
412 ///
413 /// This matches any constraint violation.
414 #[must_use]
415 pub fn on_conflict_do_nothing(self) -> InsertBuilder<'a, S, InsertOnConflictSet, T> {
416 let conflict_sql = SQL::from_iter([Token::ON, Token::CONFLICT, Token::DO, Token::NOTHING]);
417 InsertBuilder {
418 sql: self.sql.append(conflict_sql),
419 schema: PhantomData,
420 state: PhantomData,
421 table: PhantomData,
422 marker: PhantomData,
423 row: PhantomData,
424 grouped: PhantomData,
425 }
426 }
427
428 /// Adds a RETURNING clause and transitions to `ReturningSet` state
429 #[inline]
430 pub fn returning<Columns>(self, columns: Columns) -> ReturningBuilder<'a, S, T, Columns>
431 where
432 Columns: ToSQL<'a, PostgresValue<'a>> + drizzle_core::IntoSelectTarget,
433 Columns::Marker: drizzle_core::ResolveRow<T>,
434 {
435 let returning_sql = crate::helpers::returning(columns);
436 InsertBuilder {
437 sql: self.sql.append(returning_sql),
438 schema: PhantomData,
439 state: PhantomData,
440 table: PhantomData,
441 marker: PhantomData,
442 row: PhantomData,
443 grouped: PhantomData,
444 }
445 }
446}
447
448//------------------------------------------------------------------------------
449// Post-ON CONFLICT Implementation
450//------------------------------------------------------------------------------
451
452impl<'a, S, T> InsertBuilder<'a, S, InsertOnConflictSet, T> {
453 /// Adds a RETURNING clause after ON CONFLICT
454 #[inline]
455 pub fn returning<Columns>(self, columns: Columns) -> ReturningBuilder<'a, S, T, Columns>
456 where
457 Columns: ToSQL<'a, PostgresValue<'a>> + drizzle_core::IntoSelectTarget,
458 Columns::Marker: drizzle_core::ResolveRow<T>,
459 {
460 let returning_sql = crate::helpers::returning(columns);
461 InsertBuilder {
462 sql: self.sql.append(returning_sql),
463 schema: PhantomData,
464 state: PhantomData,
465 table: PhantomData,
466 marker: PhantomData,
467 row: PhantomData,
468 grouped: PhantomData,
469 }
470 }
471}
472
473//------------------------------------------------------------------------------
474// Post-DO UPDATE SET Implementation
475//------------------------------------------------------------------------------
476
477impl<'a, S, T> InsertBuilder<'a, S, InsertDoUpdateSet, T> {
478 /// Adds a WHERE clause to the DO UPDATE SET clause.
479 ///
480 /// Generates: `ON CONFLICT (col) DO UPDATE SET ... WHERE condition`
481 pub fn r#where<E>(self, condition: E) -> InsertBuilder<'a, S, InsertOnConflictSet, T>
482 where
483 E: drizzle_core::expr::Expr<'a, PostgresValue<'a>>,
484 E::SQLType: drizzle_core::types::BooleanLike,
485 {
486 let sql = self.sql.push(Token::WHERE).append(condition.to_sql());
487 InsertBuilder {
488 sql,
489 schema: PhantomData,
490 state: PhantomData,
491 table: PhantomData,
492 marker: PhantomData,
493 row: PhantomData,
494 grouped: PhantomData,
495 }
496 }
497
498 /// Adds a RETURNING clause after DO UPDATE SET
499 #[inline]
500 pub fn returning<Columns>(self, columns: Columns) -> ReturningBuilder<'a, S, T, Columns>
501 where
502 Columns: ToSQL<'a, PostgresValue<'a>> + drizzle_core::IntoSelectTarget,
503 Columns::Marker: drizzle_core::ResolveRow<T>,
504 {
505 let returning_sql = crate::helpers::returning(columns);
506 InsertBuilder {
507 sql: self.sql.append(returning_sql),
508 schema: PhantomData,
509 state: PhantomData,
510 table: PhantomData,
511 marker: PhantomData,
512 row: PhantomData,
513 grouped: PhantomData,
514 }
515 }
516}
517
518#[cfg(test)]
519mod tests {
520 use super::*;
521 use drizzle_core::{SQL, ToSQL};
522
523 #[test]
524 fn test_insert_builder_creation() {
525 let builder = InsertBuilder::<(), InsertInitial, ()> {
526 sql: SQL::raw("INSERT INTO test"),
527 schema: PhantomData,
528 state: PhantomData,
529 table: PhantomData,
530 marker: PhantomData,
531 row: PhantomData,
532 grouped: PhantomData,
533 };
534
535 assert_eq!(builder.to_sql().sql(), "INSERT INTO test");
536 }
537}