Skip to main content

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}