sqlx_utils/traits/repository/
insert.rs

1//! Trait for adding insert capabilities to a repository
2
3use crate::prelude::Database;
4use crate::traits::{Model, Repository};
5use crate::types::Query;
6use crate::utils::{BatchOperator, DEFAULT_BATCH_SIZE};
7use sqlx::Executor;
8
9/// Trait for repositories that can insert new records into the database.
10///
11/// The `InsertableRepository` trait extends the base [`Repository`] trait with methods
12/// for inserting new records. It provides standardized ways to insert both individual models
13/// and batches of models, optimizing database interactions for performance while maintaining
14/// data integrity.
15///
16/// # Type Parameters
17///
18/// * `M` - The model type that this repository inserts. Must implement the [`Model`] trait.
19///
20/// # Examples
21///
22/// Basic implementation:
23/// ```rust
24/// # use sqlx_utils::traits::{Model, Repository, InsertableRepository};
25/// # use sqlx_utils::types::{Pool, Query};
26/// # struct User { id: i32, name: String }
27/// # impl Model for User {
28/// #     type Id = i32;
29/// #     fn get_id(&self) -> Option<Self::Id> { Some(self.id) }
30/// # }
31/// # struct UserRepository { pool: Pool }
32/// # impl Repository<User> for UserRepository {
33/// #     fn pool(&self) -> &Pool { &self.pool }
34/// # }
35///
36/// impl InsertableRepository<User> for UserRepository {
37///     fn insert_query(user: &User) -> Query<'_> {
38///         sqlx::query("INSERT INTO users (name) VALUES ($1)")
39///             .bind(&user.name)
40///     }
41/// }
42///
43/// // Usage
44/// # async fn example(repo: &UserRepository, user: &User) -> sqlx_utils::Result<()> {
45/// // Insert a single user via a reference
46/// repo.insert_ref(user).await?;
47///
48/// // Insert multiple users
49/// let users = vec![
50///     User { id: 1, name: String::from("Alice") },
51///     User { id: 2, name: String::from("Bob") }
52/// ];
53/// repo.insert_many(users).await?;
54/// # Ok(())
55/// # }
56/// ```
57///
58/// Using the macro for simpler implementation:
59/// ```rust
60/// # use sqlx_utils::{repository, repository_insert};
61/// # use sqlx_utils::traits::Model;
62/// # use sqlx_utils::types::Query;
63/// # struct User { id: i32, name: String }
64/// # impl Model for User {
65/// #     type Id = i32;
66/// #     fn get_id(&self) -> Option<Self::Id> { Some(self.id) }
67/// # }
68///
69/// repository! {
70///     UserRepository<User>;
71///
72///     // if you need to override any method other than `Repository::pool` they will go here
73/// }
74///
75/// repository_insert! {
76///     UserRepository<User>;
77///
78///     insert_query(user) {
79///         sqlx::query("INSERT INTO users (name) VALUES ($1)")
80///             .bind(&user.name)
81///     }
82/// }
83/// ```
84///
85/// # Implementation Notes
86///
87/// 1. Required method: [`insert_query`](InsertableRepository::insert_query) - Defines how a model is translated into an INSERT statement
88/// 2. Provided methods:
89///    - [`insert_with_executor`](InsertableRepository::insert_with_executor) - Inserts a single model using any [`Executor`]
90///    - [`insert`](InsertableRepository::insert) - Inserts a single model
91///    - [`insert_many`](InsertableRepository::insert_many) - Inserts multiple models using the default batch size
92///    - [`insert_batch`](InsertableRepository::insert_batch) - Inserts multiple models with a custom batch size
93/// 3. All batch operations use transactions to ensure data consistency
94/// 4. Performance is optimized through batching and connection pooling
95#[diagnostic::on_unimplemented(
96    note = "Type `{Self}` does not implement the `InsertableRepository<{M}>` trait",
97    label = "this type does not implement `InsertableRepository` for model type `{M}`",
98    message = "`{Self}` must implement `InsertableRepository<{M}>` to insert `{M}` records"
99)]
100#[async_trait::async_trait]
101pub trait InsertableRepository<M: Model>: Repository<M> {
102    /// Creates a SQL query to insert a single model instance into the database.
103    ///
104    /// This method defines how a model should be persisted in the database as a new record.
105    /// It constructs a parameterized query that maps the model's fields to database columns.
106    /// The query is returned without being executed, allowing for transactions management
107    /// and error handling at a higher level.
108    ///
109    /// # Parameters
110    ///
111    /// * `model` - A reference to the model instance to be inserted
112    ///
113    /// # Returns
114    ///
115    /// * [`Query`] - A prepared SQL query ready for execution
116    ///
117    /// # Implementation Notes
118    ///
119    /// The implementing repository should:
120    /// 1. Handle all model fields appropriately
121    /// 2. Use proper SQL parameter binding for safety
122    /// 3. Return an appropriate error if the model is invalid
123    fn insert_query(model: &M) -> Query<'_>;
124
125    /// Persists a new model instance to the database.
126    ///
127    /// This method executes the insertion query generated by [`insert_query`](InsertableRepository::insert_query) with the [`Executor`] `tx`. It handles
128    /// the actual database interaction and provides a simple interface for creating new records.
129    ///
130    /// # Parameters
131    ///
132    /// * `tx` - The executor to use for the query
133    /// * `model` - The model instance to insert
134    ///
135    /// # Returns
136    ///
137    /// * [`crate::Result<M>`](crate::Result) - Success if the insertion was executed, or an error if the operation failed
138    ///
139    /// # Example
140    ///
141    /// ```no_compile
142    /// async fn create_user(repo: &UserRepository, user: &User) -> crate::Result<()> {
143    ///     repo.insert_with_executor(repo.pool(), user).await
144    /// }
145    /// ```
146    ///
147    /// # Panics
148    ///
149    /// The method will panic if an ID is present, but it will only do so in debug mode to avoid
150    /// performance issues. This is so that we don't insert a duplicate key, if this is the desired behavior you want you can enable the feature `insert_duplicate`
151    #[inline(always)]
152    #[cfg_attr(feature = "log_err", tracing::instrument(skip_all, level = "debug", parent = &(Self::repository_span()), name = "insert", err))]
153    #[cfg_attr(not(feature = "log_err"), tracing::instrument(skip_all, level = "debug", parent = &(Self::repository_span()), name = "insert"))]
154    async fn insert_with_executor<'c, E>(&self, tx: E, model: M) -> crate::Result<M>
155    where
156        M: 'async_trait,
157        E: Executor<'c, Database = Database> + Send,
158    {
159        #[cfg(not(feature = "insert_duplicate"))]
160        debug_assert!(model.get_id().is_none());
161
162        Self::insert_query(&model).execute(tx).await?;
163        Ok(model)
164    }
165
166    /// Persists a new model instance to the database.
167    ///
168    /// This method executes the insertion query generated by [`insert_query`](InsertableRepository::insert_query) with the [`Executor`] `tx`. It handles
169    /// the actual database interaction and provides a simple interface for creating new records.
170    ///
171    /// # Parameters
172    ///
173    /// * `tx` - The executor to use for the query
174    /// * `model` - A reference to the model instance to insert
175    ///
176    /// # Returns
177    ///
178    /// * [`crate::Result<()>`](crate::Result) - Success if the insertion was executed, or an error if the operation failed
179    ///
180    /// # Example
181    ///
182    /// ```no_compile
183    /// async fn create_user(repo: &UserRepository, user: &User) -> crate::Result<()> {
184    ///     repo.insert_with_executor(repo.pool(), user).await
185    /// }
186    /// ```
187    ///
188    /// # Panics
189    ///
190    /// The method will panic if an ID is present, but it will only do so in debug mode to avoid
191    /// performance issues. This is so that we don't insert a duplicate key, if this is the desired behavior you want you can enable the feature `insert_duplicate`
192    #[inline(always)]
193    #[cfg_attr(feature = "log_err", tracing::instrument(skip_all, level = "debug", parent = &(Self::repository_span()), name = "insert", err))]
194    #[cfg_attr(not(feature = "log_err"), tracing::instrument(skip_all, level = "debug", parent = &(Self::repository_span()), name = "insert"))]
195    async fn insert_ref_with_executor<'c, E>(&self, tx: E, model: &M) -> crate::Result<()>
196    where
197        E: Executor<'c, Database = Database> + Send,
198    {
199        #[cfg(not(feature = "insert_duplicate"))]
200        debug_assert!(model.get_id().is_none());
201
202        Self::insert_query(model).execute(tx).await?;
203        Ok(())
204    }
205
206    /// Persists a new model instance to the database.
207    ///
208    /// This method executes the insertion query generated by [`insert_query`](InsertableRepository::insert_query). It handles
209    /// the actual database interaction and provides a simple interface for creating
210    /// new records.
211    ///
212    /// # Parameters
213    ///
214    /// * `model` - A reference to the model instance to insert
215    ///
216    /// # Returns
217    ///
218    /// * [`crate::Result<()>`](crate::Result) - Success if the insertion was executed, or an error if the operation failed
219    ///
220    /// # Example
221    ///
222    /// ```no_compile
223    /// async fn create_user(repo: &UserRepository, user: &User) -> crate::Result<()> {
224    ///     repo.insert(user).await
225    /// }
226    /// ```
227    ///
228    /// # Panics
229    ///
230    /// The method will panic if an ID is present, but it will only do so in debug mode to avoid
231    /// performance issues. This is so that we don't insert a duplicate key, if this is the desired behavior you want you can enable the feature `insert_duplicate`
232    #[inline(always)]
233    async fn insert(&self, model: M) -> crate::Result<M>
234    where
235        M: 'async_trait,
236    {
237        self.insert_with_executor(self.pool(), model).await
238    }
239
240    /// Persists a new model instance to the database.
241    ///
242    /// This method executes the insertion query generated by [`insert_query`](InsertableRepository::insert_query). It handles
243    /// the actual database interaction and provides a simple interface for creating
244    /// new records.
245    ///
246    /// # Parameters
247    ///
248    /// * `model` - A reference to the model instance to insert
249    ///
250    /// # Returns
251    ///
252    /// * [`crate::Result<()>`](crate::Result) - Success if the insertion was executed, or an error if the operation failed
253    ///
254    /// # Example
255    ///
256    /// ```no_compile
257    /// async fn create_user(repo: &UserRepository, user: &User) -> crate::Result<()> {
258    ///     repo.insert(user).await
259    /// }
260    /// ```
261    ///
262    /// # Panics
263    ///
264    /// The method will panic if an ID is present, but it will only do so in debug mode to avoid
265    /// performance issues. This is so that we don't insert a duplicate key, if this is the desired behavior you want you can enable the feature `insert_duplicate`
266    #[inline(always)]
267    async fn insert_ref(&self, model: &M) -> crate::Result<()>
268    where
269        M: 'async_trait,
270    {
271        self.insert_ref_with_executor(self.pool(), model).await
272    }
273
274    /// Inserts multiple models using the default batch size.
275    ///
276    /// This is a convenience wrapper around [`insert_batch`](InsertableRepository::insert_batch) that uses [`DEFAULT_BATCH_SIZE`].
277    /// It provides a simpler interface for bulk insertions when the default batch size
278    /// is appropriate for the use case.
279    ///
280    /// # Parameters
281    ///
282    /// * `models` - An iterator yielding model instances to insert
283    ///
284    /// # Returns
285    ///
286    /// * [`crate::Result<()>`](crate::Result) - Success if all insertions were executed, or an error if any operation failed
287    ///
288    /// # Example
289    ///
290    /// ```no_compile
291    /// async fn create_users(repo: &UserRepository, users: Vec<User>) -> crate::Result<()> {
292    ///     repo.insert_many(users).await
293    /// }
294    /// ```
295    #[inline(always)]
296    async fn insert_many<I>(&self, models: I) -> crate::Result<()>
297    where
298        I: IntoIterator<Item = M> + Send + 'async_trait,
299        I::IntoIter: Send,
300    {
301        <Self as InsertableRepository<M>>::insert_batch::<DEFAULT_BATCH_SIZE, I>(self, models).await
302    }
303
304    /// Performs a batched insertion operation with a specified batch size.
305    ///
306    /// This method uses [`BatchOperator`] to efficiently process large numbers of insertions
307    /// in chunks. It helps prevent memory overflow and maintains optimal database performance
308    /// by limiting the number of records processed at once.
309    ///
310    /// # Type Parameters
311    ///
312    /// * `N` - The size of each batch to process
313    ///
314    /// # Parameters
315    ///
316    /// * `models` - An iterator yielding model instances to insert
317    ///
318    /// # Returns
319    ///
320    /// * [`crate::Result<()>`](crate::Result) - Success if all batches were processed, or an error if any operation failed
321    ///
322    /// # Implementation Details
323    ///
324    /// The method:
325    /// 1. Chunks the input into batches of size N
326    /// 2. Processes each batch in a transactions
327    /// 3. Uses the [`insert_query`](InsertableRepository::insert_query) query for each model
328    /// 4. Maintains ACID properties within each batch
329    ///
330    /// # Performance Considerations
331    ///
332    /// Consider batch size carefully:
333    /// - Too small: More overhead from multiple transactions
334    /// - Too large: Higher memory usage and longer transactions times
335    #[cfg_attr(feature = "log_err", tracing::instrument(skip_all, level = "debug", parent = &(Self::repository_span()), name = "insert_batch", err))]
336    #[cfg_attr(not(feature = "log_err"), tracing::instrument(skip_all, level = "debug", parent = &(Self::repository_span()), name = "insert_batch"))]
337    #[inline(always)]
338    async fn insert_batch<const N: usize, I>(&self, models: I) -> crate::Result<()>
339    where
340        I: IntoIterator<Item = M> + Send + 'async_trait,
341        I::IntoIter: Send,
342    {
343        let span = tracing::Span::current();
344        span.record("BATCH_SIZE", N);
345
346        BatchOperator::<M, N>::execute_query(models, self.pool(), Self::insert_query).await
347    }
348}