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}