renox_core/db/model.rs
1use std::future::Future;
2
3use super::{DateTime, Db, DbValue, Executor, ModelKey, Query, ToDbValue, now, quote, sql};
4use crate::{Error, Result};
5use anyhow::anyhow;
6
7/// A struct stored as a row in a table. Derive it with `#[derive(Model)]`.
8///
9/// ```
10/// # use renox::prelude::*;
11/// # use serde::Serialize;
12/// #[derive(Model, Serialize, Default)]
13/// #[model(table = "products", soft_deletes)]
14/// struct Product {
15/// id: i64,
16/// name: String,
17/// price: i64,
18/// created_at: Option<DateTime>,
19/// updated_at: Option<DateTime>,
20/// deleted_at: Option<DateTime>,
21/// }
22///
23/// # async fn demo(db: Db) -> Result {
24/// let mut coffee = Product { name: "Coffee".into(), price: 18_000, ..Default::default() };
25/// coffee.save(&db).await?; // INSERT, sets id and timestamps
26/// let cheap = Product::query().where_op("price", "<", 20_000).get(&db).await?;
27/// # let _ = cheap; Ok(()) }
28/// ```
29///
30/// Implement it with `#[derive(Model)]`: the hidden items it writes may
31/// change in a minor release.
32///
33/// The primary key is the `id` column. Its type is the `id` field's: `i64`
34/// (numbered by the database; `0` means "not saved yet"), or a
35/// [`Ulid`](super::Ulid), a UUID or a `String` (see [`ModelKey`]).
36pub trait Model: super::FromRow + Sized + Send + Sync + Unpin + 'static {
37 /// The table name (the snake_case struct name unless `#[model(table = …)]`).
38 const TABLE: &'static str;
39 /// Every column, including `id`.
40 const COLUMNS: &'static [&'static str];
41 /// `delete()` sets `deleted_at` instead of removing the row, and queries
42 /// skip deleted rows unless asked with `with_trashed()` / `only_trashed()`.
43 const SOFT_DELETES: bool = false;
44 /// Select every column (`*`) instead of `COLUMNS`, so `from_row` also
45 /// sees columns the struct doesn't list, and queries may filter on them.
46 /// The built-in `User` does this to keep the app's own columns.
47 const SELECT_ALL: bool = false;
48 /// The text columns full-text search looks in, most important first
49 /// (they weigh more in the ranking); empty: the model can't be
50 /// searched. Set it with `#[model(search = "title, body")]`; see
51 /// [`renox::db::search`](super::search).
52 const SEARCHABLE: &'static [&'static str] = &[];
53 /// The language full-text search stems words for: `english` (the
54 /// default) or a PostgreSQL text search configuration such as `simple`
55 /// (no stemming) or `spanish`. Set it with
56 /// `#[model(search_language = "simple")]`.
57 const SEARCH_LANGUAGE: &'static str = "english";
58
59 /// The type of the `id` field.
60 type Key: ModelKey;
61
62 /// The primary key.
63 fn id(&self) -> Self::Key;
64 /// Sets the primary key (after an insert, for keys the database numbers).
65 #[doc(hidden)]
66 fn set_id(&mut self, id: Self::Key);
67 /// Values of every column except `id`, in `COLUMNS` order.
68 #[doc(hidden)]
69 fn values(&self) -> Vec<DbValue>;
70 /// Updates `created_at` / `updated_at` if the model has them.
71 #[doc(hidden)]
72 fn touch(&mut self, _now: DateTime, _creating: bool) {}
73 /// Updates `deleted_at` if the model has it.
74 #[doc(hidden)]
75 fn set_deleted_at(&mut self, _at: Option<DateTime>) {}
76 /// Empties `created_at` / `updated_at` when they are `Option`s.
77 #[doc(hidden)]
78 fn forget_timestamps(&mut self) {}
79
80 /// A copy of the model that isn't saved yet (Laravel's `replicate`):
81 /// the same values, with an unsaved id, no `deleted_at` and, when they
82 /// are `Option`s, no timestamps, so `save` inserts a new row. Change
83 /// what must differ (a unique SKU, a name) before saving it, or show it
84 /// in the "new" form for someone to finish ("Duplicate").
85 ///
86 /// ```
87 /// # use renox::prelude::*;
88 /// # #[derive(Model, serde::Serialize, Default, Clone)]
89 /// # #[model(table = "products")]
90 /// # struct Product { id: i64, sku: String, name: String, created_at: Option<DateTime> }
91 /// # async fn demo(db: Db) -> Result {
92 /// let original = Product::find_or_404(&db, 1).await?;
93 /// let mut copy = original.replicate();
94 /// copy.sku = format!("{}-COPY", original.sku);
95 /// copy.save(&db).await?; // a new row, with its own id
96 /// # Ok(()) }
97 /// ```
98 fn replicate(&self) -> Self
99 where
100 Self: Clone,
101 {
102 let mut copy = self.clone();
103 copy.set_id(Self::Key::default());
104 copy.set_deleted_at(None);
105 copy.forget_timestamps();
106 copy
107 }
108
109 /// Conditions every query of this model starts with, e.g. the current
110 /// tenant, read from [`renox::context`](mod@crate::context). `query()`,
111 /// `find`, `all`, `where_eq` and the relation loaders apply it;
112 /// [`Model::unscoped`] doesn't. Saving, deleting and restoring a loaded
113 /// model work by its id. Set it with `#[model(default_scope = "…")]`:
114 ///
115 /// ```
116 /// # use renox::prelude::*;
117 /// #[derive(Clone)]
118 /// struct CurrentTeam(i64);
119 ///
120 /// #[derive(Model, serde::Serialize, Default)]
121 /// #[model(table = "projects", default_scope = "team_only")]
122 /// struct Project { id: i64, team_id: i64, name: String }
123 ///
124 /// fn team_only(query: renox::db::Query<Project>) -> renox::db::Query<Project> {
125 /// match renox::context::get::<CurrentTeam>() {
126 /// Some(team) => query.where_eq("team_id", team.0),
127 /// None => query.none(), // no team, no rows: fail closed
128 /// }
129 /// }
130 /// ```
131 fn default_scope(query: Query<Self>) -> Query<Self> {
132 query
133 }
134
135 /// Runs before the row is written; an error stops the save. Implement
136 /// [`ModelHooks`] and add `#[model(hooks)]` rather than overriding it.
137 fn saving(&mut self, _creating: bool) -> Result {
138 Ok(())
139 }
140
141 /// Runs after the row is written (inside the caller's transaction, if
142 /// any); an error is returned by `save`. See [`ModelHooks`].
143 fn saved(&self, _created: bool) -> impl Future<Output = Result> + Send {
144 async { Ok(()) }
145 }
146
147 /// Runs before `delete`/`force_delete`; an error stops it. See [`ModelHooks`].
148 fn deleting(&self) -> Result {
149 Ok(())
150 }
151
152 /// Runs after `delete`/`force_delete`. See [`ModelHooks`].
153 fn deleted(&self) -> impl Future<Output = Result> + Send {
154 async { Ok(()) }
155 }
156
157 /// A query with the default scope applied (see [`Model::default_scope`]).
158 fn query() -> Query<Self> {
159 Self::default_scope(Query::new())
160 }
161
162 /// A query without the default scope, e.g. for an admin who sees every
163 /// tenant. Soft-deleted rows stay hidden unless asked for.
164 fn unscoped() -> Query<Self> {
165 Query::new()
166 }
167
168 /// Reloads the model's row (e.g. after an `increment` or another request
169 /// changed it); a deleted row is a 404.
170 fn refresh(&mut self, db: &Db) -> impl Future<Output = Result<()>> + Send {
171 async move {
172 let id = self.id();
173 *self = Self::unscoped()
174 .with_trashed()
175 .where_eq("id", id)
176 .first_or_404(db)
177 .await?;
178 Ok(())
179 }
180 }
181
182 /// The rows matching a full-text search, best matches first:
183 /// shorthand for `query().search(words)`. The model needs
184 /// `#[model(search = "…")]` and its index (see
185 /// [`renox::db::search`](super::search)).
186 ///
187 /// ```
188 /// # use renox::prelude::*;
189 /// #[derive(Model, serde::Serialize, Default)]
190 /// #[model(table = "posts", search = "title, body")]
191 /// struct Post { id: i64, title: String, body: String }
192 ///
193 /// # async fn demo(db: Db, q: String) -> Result {
194 /// let posts = Post::search(&q).limit(20).get(&db).await?;
195 /// # let _ = posts; Ok(()) }
196 /// ```
197 fn search(words: &str) -> Query<Self> {
198 Self::query().search(words)
199 }
200
201 /// Shorthand for `query().where_eq(column, value)`.
202 fn where_eq(column: &str, value: impl ToDbValue) -> Query<Self> {
203 Self::query().where_eq(column, value)
204 }
205
206 /// Every row, in id order.
207 fn all<'c, E: Executor<'c>>(db: E) -> impl Future<Output = Result<Vec<Self>>> + Send {
208 Self::query().order_by("id").get(db)
209 }
210
211 /// The row with this id, or `None`.
212 fn find<'c, E: Executor<'c>>(
213 db: E,
214 id: Self::Key,
215 ) -> impl Future<Output = Result<Option<Self>>> + Send {
216 Self::query().where_eq("id", id).first(db)
217 }
218
219 /// The rows with these ids, in id order (missing ids are skipped).
220 fn find_many<'c, E: Executor<'c>>(
221 db: E,
222 ids: impl IntoIterator<Item = Self::Key>,
223 ) -> impl Future<Output = Result<Vec<Self>>> + Send {
224 let ids: Vec<Self::Key> = ids.into_iter().collect();
225 Self::query().where_in("id", ids).order_by("id").get(db)
226 }
227
228 /// Inserts many new models with a few statements (ids aren't returned;
229 /// use `create` when you need them). Timestamps are set; unsaved ULID
230 /// and UUID keys are made, `String` keys must be set, `i64` keys come
231 /// from the database. Returns the number of rows inserted.
232 fn insert_many<'c, E: Executor<'c>>(
233 db: E,
234 models: Vec<Self>,
235 ) -> impl Future<Output = Result<u64>> + Send {
236 async move { write_many::<Self>(db.into_conn(), models, None).await }
237 }
238
239 /// Inserts `models`, or updates the rows they clash with on the
240 /// `unique_by` columns (which need a unique index), setting `update`
241 /// columns (and `updated_at` when the model has it). Returns the rows
242 /// written.
243 ///
244 /// ```
245 /// # use renox::prelude::*;
246 /// # #[derive(Model, serde::Serialize, Default)] struct Stock { id: i64, sku: String, qty: i64 }
247 /// # async fn demo(db: Db) -> Result {
248 /// let feed = vec![Stock { sku: "COFFEE-1".into(), qty: 12, ..Default::default() }];
249 /// Stock::upsert(&db, feed, &["sku"], &["qty"]).await?;
250 /// # Ok(()) }
251 /// ```
252 fn upsert<'c, E: Executor<'c>>(
253 db: E,
254 models: Vec<Self>,
255 unique_by: &[&str],
256 update: &[&str],
257 ) -> impl Future<Output = Result<u64>> + Send {
258 let unique_by: Vec<String> = unique_by.iter().map(|c| (*c).to_owned()).collect();
259 let update: Vec<String> = update.iter().map(|c| (*c).to_owned()).collect();
260 async move {
261 // Keys the app writes (ULID, UUID, String) may be the conflict
262 // target; an `i64` id isn't written, and no key is ever updated.
263 let key_target = !<Self::Key as ModelKey>::AUTO_INCREMENT;
264 for column in unique_by.iter().chain(&update) {
265 let id_allowed =
266 key_target && unique_by.contains(column) && !update.contains(column);
267 if !Self::COLUMNS.contains(&column.as_str()) || (column == "id" && !id_allowed) {
268 return Err(
269 anyhow!("`{}` has no column `{column}` to upsert", Self::TABLE).into(),
270 );
271 }
272 }
273 if unique_by.is_empty() {
274 return Err(anyhow!("upsert needs at least one `unique_by` column").into());
275 }
276 write_many::<Self>(db.into_conn(), models, Some((unique_by, update))).await
277 }
278 }
279
280 /// Like `find`, but a missing row becomes a 404 response.
281 fn find_or_404<'c, E: Executor<'c>>(
282 db: E,
283 id: Self::Key,
284 ) -> impl Future<Output = Result<Self>> + Send {
285 let found = Self::find(db, id);
286 async move { found.await?.ok_or(Error::NotFound) }
287 }
288
289 /// Inserts a new model and returns it with its id and timestamps.
290 fn create<'c, E: Executor<'c>>(
291 db: E,
292 mut model: Self,
293 ) -> impl Future<Output = Result<Self>> + Send {
294 async move {
295 model.insert(db).await?;
296 Ok(model)
297 }
298 }
299
300 /// Inserts the model as a new row, whatever its id: an unsaved id gets a
301 /// new key (from the database for `i64`, a new ULID or UUID v7), a set
302 /// one is written as it is (a `String` key must be set).
303 fn insert<'c, E: Executor<'c>>(&mut self, db: E) -> impl Future<Output = Result> + Send {
304 async move {
305 self.saving(true)?;
306 self.touch(now(), true);
307 if self.id().is_unsaved()
308 && let Some(key) = Self::Key::generate()
309 {
310 self.set_id(key);
311 }
312 let key = self.id();
313 let mut columns: Vec<String> = Self::COLUMNS
314 .iter()
315 .filter(|c| **c != "id")
316 .map(|c| quote(c))
317 .collect();
318 let mut values = self.values();
319 let table = quote(Self::TABLE);
320 if key.is_unsaved() {
321 if !<Self::Key as ModelKey>::AUTO_INCREMENT {
322 return Err(anyhow!(
323 "set the `id` of a new {} row before inserting it",
324 Self::TABLE
325 )
326 .into());
327 }
328 let sql_text = if columns.is_empty() {
329 format!("INSERT INTO {table} DEFAULT VALUES RETURNING id")
330 } else {
331 let marks = vec!["?"; columns.len()].join(", ");
332 format!(
333 "INSERT INTO {table} ({}) VALUES ({marks}) RETURNING id",
334 columns.join(", ")
335 )
336 };
337 let id: Self::Key = sql(sql_text).bind_all(values).scalar(db).await?;
338 self.set_id(id);
339 } else {
340 columns.insert(0, quote("id"));
341 values.insert(0, key.to_db_value());
342 let marks = vec!["?"; columns.len()].join(", ");
343 sql(format!(
344 "INSERT INTO {table} ({}) VALUES ({marks})",
345 columns.join(", ")
346 ))
347 .bind_all(values)
348 .execute(db)
349 .await?;
350 }
351 self.saved(true).await
352 }
353 }
354
355 /// Inserts the model if its id is unsaved (`0`, an empty ULID…),
356 /// otherwise updates its row.
357 fn save<'c, E: Executor<'c>>(&mut self, db: E) -> impl Future<Output = Result> + Send {
358 async move {
359 if self.id().is_unsaved() {
360 return self.insert(db).await;
361 }
362 self.saving(false)?;
363 self.touch(now(), false);
364 let columns: Vec<String> = Self::COLUMNS
365 .iter()
366 .filter(|c| **c != "id")
367 .map(|c| quote(c))
368 .collect();
369 let values = self.values();
370 let table = quote(Self::TABLE);
371 if columns.is_empty() {
372 return self.saved(false).await;
373 }
374 let sets: Vec<String> = columns.iter().map(|c| format!("{c} = ?")).collect();
375 let changed = sql(format!(
376 "UPDATE {table} SET {} WHERE id = ?",
377 sets.join(", ")
378 ))
379 .bind_all(values)
380 .bind(self.id())
381 .execute(db)
382 .await?;
383 if changed == 0 {
384 return Err(Error::NotFound);
385 }
386 self.saved(false).await
387 }
388 }
389
390 /// Updates only `columns` (and `updated_at`, if the model has it) of a
391 /// saved model, so a concurrent change to another column isn't
392 /// overwritten. A column the `saving` hook changes is saved only if
393 /// it's listed.
394 ///
395 /// ```
396 /// # use renox::prelude::*;
397 /// # #[derive(Model, serde::Serialize, Default)]
398 /// # #[model(table = "posts")]
399 /// # struct Post { id: i64, title: String, views: i64 }
400 /// # async fn demo(db: Db, mut post: Post) -> Result {
401 /// post.title = "New title".into();
402 /// post.save_only(&db, &["title"]).await?; // leaves `views` alone
403 /// # Ok(()) }
404 /// ```
405 fn save_only<'c, E: Executor<'c>>(
406 &mut self,
407 db: E,
408 columns: &[&str],
409 ) -> impl Future<Output = Result> + Send {
410 let columns: Vec<String> = columns.iter().map(|c| (*c).to_owned()).collect();
411 async move {
412 self.saving(false)?;
413 update_columns(self, db, columns).await
414 }
415 }
416
417 /// Saves the columns that differ from `original` (the model as it was
418 /// loaded), including those the `saving` hook changes, and returns
419 /// whether anything was written. When nothing changed there's no query
420 /// and no `saved` hook.
421 ///
422 /// ```
423 /// # use renox::prelude::*;
424 /// # #[derive(Model, serde::Serialize, Default, Clone)]
425 /// # #[model(table = "posts")]
426 /// # struct Post { id: i64, title: String, views: i64 }
427 /// # async fn demo(db: Db) -> Result {
428 /// let original = Post::find_or_404(&db, 1).await?;
429 /// let mut post = original.clone();
430 /// post.title = "New title".into();
431 /// post.save_changes(&db, &original).await?; // UPDATE posts SET title = ?
432 /// # Ok(()) }
433 /// ```
434 fn save_changes<'c, E: Executor<'c>>(
435 &mut self,
436 db: E,
437 original: &Self,
438 ) -> impl Future<Output = Result<bool>> + Send {
439 let hooked = self.saving(false);
440 let changed: Vec<String> = Self::COLUMNS
441 .iter()
442 .filter(|c| **c != "id")
443 .zip(self.values().into_iter().zip(original.values()))
444 .filter(|(_, (now, before))| now != before)
445 .map(|(column, _)| (*column).to_owned())
446 .collect();
447 async move {
448 hooked?;
449 if changed.is_empty() {
450 return Ok(false);
451 }
452 update_columns(self, db, changed).await?;
453 Ok(true)
454 }
455 }
456
457 /// Deletes the row, or marks it deleted for models with soft deletes.
458 fn delete<'c, E: Executor<'c>>(&mut self, db: E) -> impl Future<Output = Result> + Send {
459 async move {
460 if !Self::SOFT_DELETES {
461 return self.force_delete(db).await;
462 }
463 self.deleting()?;
464 let at = now();
465 sql(format!(
466 "UPDATE {} SET deleted_at = ? WHERE id = ?",
467 quote(Self::TABLE)
468 ))
469 .bind(at)
470 .bind(self.id())
471 .execute(db)
472 .await?;
473 self.set_deleted_at(Some(at));
474 self.deleted().await
475 }
476 }
477
478 /// Removes the row, even for models with soft deletes.
479 fn force_delete<'c, E: Executor<'c>>(&self, db: E) -> impl Future<Output = Result> + Send {
480 async move {
481 self.deleting()?;
482 sql(format!("DELETE FROM {} WHERE id = ?", quote(Self::TABLE)))
483 .bind(self.id())
484 .execute(db)
485 .await?;
486 self.deleted().await
487 }
488 }
489
490 /// Brings back a soft-deleted row.
491 fn restore<'c, E: Executor<'c>>(&mut self, db: E) -> impl Future<Output = Result> + Send {
492 async move {
493 if !Self::SOFT_DELETES {
494 return Err(anyhow!("{} does not use soft deletes", Self::TABLE).into());
495 }
496 sql(format!(
497 "UPDATE {} SET deleted_at = NULL WHERE id = ?",
498 quote(Self::TABLE)
499 ))
500 .bind(self.id())
501 .execute(db)
502 .await?;
503 self.set_deleted_at(None);
504 Ok(())
505 }
506 }
507}
508
509/// `save_only` after the `saving` hook: writes `columns` and `updated_at`,
510/// then runs `saved`.
511async fn update_columns<'c, M: Model, E: Executor<'c>>(
512 model: &mut M,
513 db: E,
514 columns: Vec<String>,
515) -> Result {
516 if model.id().is_unsaved() {
517 return Err(anyhow!("save_only on an unsaved {} row", M::TABLE).into());
518 }
519 for column in &columns {
520 if column == "id" || !M::COLUMNS.contains(&column.as_str()) {
521 return Err(anyhow!("{} has no column `{column}` to save", M::TABLE).into());
522 }
523 }
524 model.touch(now(), false);
525 let mut sets = Vec::new();
526 let mut binds = Vec::new();
527 for (column, value) in M::COLUMNS
528 .iter()
529 .filter(|c| **c != "id")
530 .zip(model.values())
531 {
532 if columns.iter().any(|c| c == column) || *column == "updated_at" {
533 sets.push(format!("{} = ?", quote(column)));
534 binds.push(value);
535 }
536 }
537 if !sets.is_empty() {
538 let changed = sql(format!(
539 "UPDATE {} SET {} WHERE id = ?",
540 quote(M::TABLE),
541 sets.join(", ")
542 ))
543 .bind_all(binds)
544 .bind(model.id())
545 .execute(db)
546 .await?;
547 if changed == 0 {
548 return Err(Error::NotFound);
549 }
550 }
551 model.saved(false).await
552}
553
554/// Code that runs around a model's writes, like Laravel's model events:
555/// fill a slug, check an invariant, forget a cache key, emit an event. Add
556/// `#[model(hooks)]` and implement the ones you need:
557///
558/// ```
559/// # use renox::prelude::*;
560/// use renox::db::ModelHooks;
561///
562/// #[derive(Model, serde::Serialize, Default)]
563/// #[model(table = "posts", hooks)]
564/// struct Post { id: i64, title: String, slug: String }
565///
566/// impl ModelHooks for Post {
567/// fn saving(&mut self, _creating: bool) -> Result {
568/// self.slug = self.title.to_lowercase().replace(' ', "-");
569/// Ok(())
570/// }
571///
572/// async fn saved(&self, _created: bool) -> Result {
573/// if let Some(state) = renox::context::app() { // the request's or job's app
574/// state.cache.forget("posts.latest").await?;
575/// }
576/// Ok(())
577/// }
578/// }
579/// ```
580///
581/// They run for `save`, `save_only`, `save_changes`, `create`, `insert`, `delete` and
582/// `force_delete`, not for `restore` or bulk `Query::update`/`delete` and
583/// `insert_many`, which write many rows in one statement.
584pub trait ModelHooks {
585 /// Runs before the row is written (`creating` is true for an insert); an error cancels it.
586 fn saving(&mut self, _creating: bool) -> Result {
587 Ok(())
588 }
589
590 /// Runs after the row is written (`created` is true for an insert).
591 fn saved(&self, _created: bool) -> impl Future<Output = Result> + Send {
592 async { Ok(()) }
593 }
594
595 /// Runs before the row is deleted; an error cancels the delete.
596 fn deleting(&self) -> Result {
597 Ok(())
598 }
599
600 /// Runs after the row is deleted.
601 fn deleted(&self) -> impl Future<Output = Result> + Send {
602 async { Ok(()) }
603 }
604}
605
606/// Most parameters one statement binds; both databases allow more
607/// (SQLite 32,766, PostgreSQL 65,535).
608const MAX_BINDS: usize = 30_000;
609
610/// `insert_many` / `upsert`: multi-row INSERTs in chunks under the bind limit.
611async fn write_many<M: Model>(
612 mut conn: super::Conn<'_>,
613 mut models: Vec<M>,
614 upsert: Option<(Vec<String>, Vec<String>)>,
615) -> Result<u64> {
616 let at = now();
617 // `i64` keys come from the database; other keys are written, made here
618 // (ULID, UUID) when unsaved.
619 let with_keys = !<M::Key as super::ModelKey>::AUTO_INCREMENT;
620 for model in &mut models {
621 model.touch(at, true);
622 if with_keys && model.id().is_unsaved() {
623 match <M::Key as super::ModelKey>::generate() {
624 Some(key) => model.set_id(key),
625 None => {
626 return Err(anyhow!(
627 "set the `id` of every new {} row before inserting them",
628 M::TABLE
629 )
630 .into());
631 }
632 }
633 }
634 }
635 let mut columns: Vec<&str> = M::COLUMNS.iter().copied().filter(|c| *c != "id").collect();
636 if with_keys {
637 columns.insert(0, "id");
638 }
639 if columns.is_empty() || models.is_empty() {
640 return Ok(0);
641 }
642 let quoted: Vec<String> = columns.iter().map(|c| quote(c)).collect();
643 let row_marks = format!("({})", vec!["?"; columns.len()].join(", "));
644 let conflict = upsert.map(|(unique_by, update)| {
645 let targets: Vec<String> = unique_by.iter().map(|c| quote(c)).collect();
646 let mut sets: Vec<String> = update
647 .iter()
648 .map(|c| format!("{0} = excluded.{0}", quote(c)))
649 .collect();
650 if columns.contains(&"updated_at") && !update.iter().any(|c| c == "updated_at") {
651 sets.push(format!("{0} = excluded.{0}", quote("updated_at")));
652 }
653 if sets.is_empty() {
654 format!(" ON CONFLICT ({}) DO NOTHING", targets.join(", "))
655 } else {
656 format!(
657 " ON CONFLICT ({}) DO UPDATE SET {}",
658 targets.join(", "),
659 sets.join(", ")
660 )
661 }
662 });
663 let per_statement = (MAX_BINDS / columns.len()).max(1);
664 let mut written = 0;
665 for chunk in models.chunks(per_statement) {
666 let marks = vec![row_marks.as_str(); chunk.len()].join(", ");
667 let statement = format!(
668 "INSERT INTO {} ({}) VALUES {marks}{}",
669 quote(M::TABLE),
670 quoted.join(", "),
671 conflict.as_deref().unwrap_or_default()
672 );
673 let values = chunk.iter().flat_map(|model| {
674 let key = with_keys.then(|| model.id().to_db_value());
675 key.into_iter().chain(model.values())
676 });
677 written += sql(statement)
678 .bind_all(values)
679 .execute(conn.reborrow())
680 .await?;
681 }
682 Ok(written)
683}