Skip to main content

orbit_orm/
migrate.rs

1//! Migrations are SQL folders that change the schema. Each folder is named `<timestamp>_<name>`
2//! and holds `up.sql` and `down.sql`. The applied ones are recorded in Diesel's
3//! `__diesel_schema_migrations` table with a batch number, and a rollback reverts the last batch.
4//! The table has the same name and columns on every database.
5
6#[cfg(feature = "sqlite")]
7use std::collections::BTreeSet;
8use std::collections::{HashMap, HashSet};
9use std::future::Future;
10use std::path::Path;
11#[cfg(feature = "sqlite")]
12use std::sync::{Mutex, PoisonError};
13use std::time::Duration;
14#[cfg(any(feature = "postgres", feature = "sqlite"))]
15use std::time::Instant;
16
17use crate::backend;
18use crate::database::Pool;
19use crate::transaction::{Target, Transaction};
20use crate::{Database, Driver, Error, Value};
21
22/// One migration: its name, and the SQL that applies it (`up`) and reverts it (`down`).
23#[derive(Debug, Clone)]
24pub struct Migration {
25    /// The folder name, such as `20261108000000_create_users`.
26    pub name: String,
27    /// The contents of `up.sql`.
28    pub up: String,
29    /// The contents of `down.sql`.
30    pub down: String,
31}
32
33impl Migration {
34    /// A migration from its name and SQL, for code that builds migrations itself.
35    pub fn new(name: impl Into<String>, up: impl Into<String>, down: impl Into<String>) -> Self {
36        Self {
37            name: name.into(),
38            up: up.into(),
39            down: down.into(),
40        }
41    }
42}
43
44/// Whether a migration has run. `Applied` carries the batch it ran in.
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum State {
47    Applied { batch: i64 },
48    Pending,
49}
50
51/// One line of [`Migrator::status`].
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct Status {
54    pub name: String,
55    pub state: State,
56}
57
58/// The migrations of one app, sorted by name. Build it with [`Migrator::from_dir`] or
59/// [`migrations!`](crate::migrations).
60#[derive(Debug, Clone)]
61pub struct Migrator {
62    migrations: Vec<Migration>,
63    lock_timeout: Duration,
64}
65
66/// The lock that serialises `run` and `rollback` across processes is named after the database,
67/// so two databases on one server do not wait for each other.
68const LOCK_PREFIX: &str = "orbit_migrations";
69
70/// How long `run` and `rollback` wait for another process's lock before giving up.
71const DEFAULT_LOCK_TIMEOUT: Duration = Duration::from_secs(10);
72
73/// How often a waiting lock is tried again, where the database has no blocking lock call.
74#[cfg(any(feature = "postgres", feature = "sqlite"))]
75const LOCK_POLL: Duration = Duration::from_millis(50);
76
77/// The table that records the applied migrations, on every database.
78const TABLE: &str = "__diesel_schema_migrations";
79
80/// The migrations that are held in this process, by lock name. Used by SQLite, which has no
81/// named lock. See `sqlite_lock`.
82#[cfg(feature = "sqlite")]
83static SQLITE_HELD: Mutex<BTreeSet<String>> = Mutex::new(BTreeSet::new());
84
85impl Migrator {
86    /// Sorts the migrations by name. A name must start with a 14-digit timestamp and an
87    /// underscore, as `20261108000000_create_users` does. Two migrations with the same name, or
88    /// the same timestamp, are an error.
89    pub fn new(mut migrations: Vec<Migration>) -> Result<Self, Error> {
90        for migration in &migrations {
91            if !has_timestamp(&migration.name) {
92                return Err(Error::InvalidMigrationName {
93                    name: migration.name.clone(),
94                });
95            }
96        }
97        migrations.sort_by(|a, b| a.name.cmp(&b.name));
98        for pair in migrations.windows(2) {
99            if pair[0].name == pair[1].name {
100                return Err(Error::DuplicateMigration {
101                    name: pair[0].name.clone(),
102                });
103            }
104            if timestamp(&pair[0].name) == timestamp(&pair[1].name) {
105                return Err(Error::DuplicateTimestamp {
106                    first: pair[0].name.clone(),
107                    second: pair[1].name.clone(),
108                });
109            }
110        }
111        Ok(Self {
112            migrations,
113            lock_timeout: DEFAULT_LOCK_TIMEOUT,
114        })
115    }
116
117    /// Sets how long `run` and `rollback` wait for another process to finish its migrations.
118    pub fn with_lock_timeout(mut self, timeout: Duration) -> Self {
119        self.lock_timeout = timeout;
120        self
121    }
122
123    /// The migrations in name order.
124    pub fn migrations(&self) -> &[Migration] {
125        &self.migrations
126    }
127
128    /// Reads every folder in `dir` as a migration, such as `migrations/20261108000000_create_users`.
129    /// Each folder needs `up.sql` and `down.sql`. Hidden entries are skipped.
130    pub fn from_dir(dir: impl AsRef<Path>) -> Result<Self, Error> {
131        let dir = dir.as_ref();
132        let entries = std::fs::read_dir(dir).map_err(|source| Error::Read {
133            path: dir.to_path_buf(),
134            source,
135        })?;
136        let mut migrations = Vec::new();
137        for entry in entries {
138            let entry = entry.map_err(|source| Error::Read {
139                path: dir.to_path_buf(),
140                source,
141            })?;
142            let name = entry.file_name().to_string_lossy().into_owned();
143            let path = entry.path();
144            if name.starts_with('.') || !path.is_dir() {
145                continue;
146            }
147            let up = read_file(&path.join("up.sql"))?;
148            let down = read_file(&path.join("down.sql"))?;
149            migrations.push(Migration { name, up, down });
150        }
151        Self::new(migrations)
152    }
153
154    /// Applies every pending migration, in name order, as one new batch. Each migration runs in
155    /// its own transaction. The run stops at the first failure, and the error names it. The
156    /// migrations before it stay applied in this batch.
157    ///
158    /// MySQL and SQLite do not roll back DDL, and PostgreSQL does. On MySQL a failing migration can
159    /// leave the DDL it ran before the failure in place; only its DML is rolled back. Returns the
160    /// names that were applied.
161    ///
162    /// Only one process runs migrations at a time. A second process waits up to the lock timeout,
163    /// then fails with [`Error::MigrationsLocked`]. The lock is held differently on each database;
164    /// see `docs/laravel-differences.md`.
165    pub async fn run(&self, db: &Database) -> Result<Vec<String>, Error> {
166        self.with_lock(db, self.run_unlocked(db)).await
167    }
168
169    async fn run_unlocked(&self, db: &Database) -> Result<Vec<String>, Error> {
170        ensure_table(db).await?;
171        let applied = applied(db).await?;
172        let pending: Vec<&Migration> = self
173            .migrations
174            .iter()
175            .filter(|m| !applied.contains_key(timestamp(&m.name)))
176            .collect();
177        if pending.is_empty() {
178            return Ok(Vec::new());
179        }
180
181        let max: Option<i32> = backend::scalar(
182            Target::Pool(&db.pool),
183            &format!("SELECT MAX(batch) FROM {TABLE}"),
184            &[],
185        )
186        .await?;
187        let batch = i64::from(max.unwrap_or(0)) + 1;
188        let mut names = Vec::with_capacity(pending.len());
189        for migration in pending {
190            let mut tx = Transaction::begin(&db.pool).await?;
191            backend::run_script(Target::Tx(&mut tx.inner), &migration.up)
192                .await
193                .map_err(|source| Error::MigrationFailed {
194                    name: migration.name.clone(),
195                    source,
196                })?;
197            let (sql, values) = prepared(
198                db.driver(),
199                &format!("INSERT INTO {TABLE} (version, batch) VALUES (?, ?)"),
200                vec![
201                    Value::Text(timestamp(&migration.name).to_owned()),
202                    Value::Int(batch),
203                ],
204            );
205            backend::execute(Target::Tx(&mut tx.inner), &sql, &values)
206                .await
207                .map_err(|source| Error::MigrationFailed {
208                    name: migration.name.clone(),
209                    source,
210                })?;
211            tx.commit().await?;
212            names.push(migration.name.clone());
213        }
214        Ok(names)
215    }
216
217    /// Lists every migration in this migrator as applied (with its batch) or pending.
218    /// Applied migrations without files here are not listed.
219    pub async fn status(&self, db: &Database) -> Result<Vec<Status>, Error> {
220        ensure_table(db).await?;
221        let applied = applied(db).await?;
222        Ok(self
223            .migrations
224            .iter()
225            .map(|m| Status {
226                name: m.name.clone(),
227                state: match applied.get(timestamp(&m.name)) {
228                    Some(&batch) => State::Applied { batch },
229                    None => State::Pending,
230                },
231            })
232            .collect())
233    }
234
235    /// Reverts the last batch, running each `down.sql` in reverse name order, each in its own
236    /// transaction. It checks that every migration in the batch has files here before it reverts
237    /// anything. Returns the names that were reverted, or nothing when no migration has run.
238    ///
239    /// Batch 0 holds the history that Diesel recorded, so a rollback that reaches batch 0 reverts
240    /// those migrations and removes their rows from Diesel's table too.
241    ///
242    /// As with [`run`](Self::run), DDL in a `down.sql` is only rolled back on PostgreSQL.
243    pub async fn rollback(&self, db: &Database) -> Result<Vec<String>, Error> {
244        self.with_lock(db, self.rollback_unlocked(db)).await
245    }
246
247    async fn rollback_unlocked(&self, db: &Database) -> Result<Vec<String>, Error> {
248        ensure_table(db).await?;
249        let last: Option<i32> = backend::scalar(
250            Target::Pool(&db.pool),
251            &format!("SELECT MAX(batch) FROM {TABLE}"),
252            &[],
253        )
254        .await?;
255        let Some(batch) = last else {
256            return Ok(Vec::new());
257        };
258        let (sql, values) = prepared(
259            db.driver(),
260            &format!("SELECT version FROM {TABLE} WHERE batch = ? ORDER BY version DESC"),
261            vec![Value::Int(i64::from(batch))],
262        );
263        let versions: Vec<(String,)> =
264            backend::fetch_all(Target::Pool(&db.pool), &sql, &values).await?;
265
266        let mut to_revert = Vec::with_capacity(versions.len());
267        for (version,) in &versions {
268            let migration = self
269                .migrations
270                .iter()
271                .find(|m| timestamp(&m.name) == version.as_str())
272                .ok_or_else(|| Error::UnknownMigration {
273                    name: version.clone(),
274                })?;
275            to_revert.push(migration);
276        }
277        let names = to_revert.iter().map(|m| m.name.clone()).collect();
278        for migration in to_revert {
279            let mut tx = Transaction::begin(&db.pool).await?;
280            backend::run_script(Target::Tx(&mut tx.inner), &migration.down)
281                .await
282                .map_err(|source| Error::RollbackFailed {
283                    name: migration.name.clone(),
284                    source,
285                })?;
286            let (sql, values) = prepared(
287                db.driver(),
288                &format!("DELETE FROM {TABLE} WHERE version = ?"),
289                vec![Value::Text(timestamp(&migration.name).to_owned())],
290            );
291            backend::execute(Target::Tx(&mut tx.inner), &sql, &values)
292                .await
293                .map_err(|source| Error::RollbackFailed {
294                    name: migration.name.clone(),
295                    source,
296                })?;
297            tx.commit().await?;
298        }
299        Ok(names)
300    }
301}
302
303impl Migrator {
304    /// Brings Diesel's history into this migrator's table. Orbit and Diesel share
305    /// `__diesel_schema_migrations`, so the rows Diesel wrote are already the applied history, in
306    /// batch 0. This call does what is still needed for them: it adds the `batch` column to a
307    /// table that has only Diesel's columns. No `up.sql` runs, and no row is added or changed.
308    /// It returns nothing, because there is nothing new to record. It is safe to call again.
309    #[deprecated(
310        since = "0.1.1",
311        note = "not needed any more: run, status and rollback upgrade the migrations table \
312                themselves. This returns an empty list and will be removed in a later release"
313    )]
314    pub async fn import_diesel_history(&self, db: &Database) -> Result<Vec<String>, Error> {
315        self.with_lock(db, import_unlocked(db)).await
316    }
317
318    /// The versions in the migrations table that have no migration in this migrator, in order.
319    /// They include versions that Orbit recorded when a folder has since been removed. Empty when
320    /// the table does not exist. Only reads the table.
321    pub async fn unmatched_diesel_versions(&self, db: &Database) -> Result<Vec<String>, Error> {
322        let Some(versions) = recorded_versions(db).await? else {
323            return Ok(Vec::new());
324        };
325        let mut unmatched: Vec<String> = versions
326            .into_iter()
327            .filter(|version| {
328                !self
329                    .migrations
330                    .iter()
331                    .any(|m| timestamp(&m.name) == version.as_str())
332            })
333            .collect();
334        unmatched.sort();
335        Ok(unmatched)
336    }
337}
338
339/// Embeds migrations in the binary at build time. `dir` is relative to the crate root, and each
340/// name is a folder in it with `up.sql` and `down.sql`. The names are listed by hand, and a
341/// missing file is a build error. Returns a `Result<Migrator, Error>`.
342///
343/// ```ignore
344/// let migrator = orbit_orm::migrations!("migrations", "20261108000000_create_users")?;
345/// ```
346#[macro_export]
347macro_rules! migrations {
348    ($dir:literal, $($name:literal),* $(,)?) => {
349        $crate::Migrator::new(vec![
350            $(
351                $crate::Migration::new(
352                    $name,
353                    include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/", $dir, "/", $name, "/up.sql")),
354                    include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/", $dir, "/", $name, "/down.sql")),
355                ),
356            )*
357        ])
358    };
359}
360
361impl Migrator {
362    /// Runs `body` while holding the migrations lock for the database. See each driver's lock
363    /// function for how it is held.
364    async fn with_lock<T>(
365        &self,
366        db: &Database,
367        body: impl Future<Output = Result<T, Error>>,
368    ) -> Result<T, Error> {
369        match &db.pool {
370            #[cfg(feature = "mysql")]
371            Pool::MySql(pool) => mysql_lock(pool, self.lock_timeout, body).await,
372            #[cfg(feature = "postgres")]
373            Pool::Postgres(pool) => postgres_lock(pool, self.lock_timeout, body).await,
374            #[cfg(feature = "sqlite")]
375            Pool::Sqlite(pool) => sqlite_lock(pool, self.lock_timeout, body).await,
376        }
377    }
378}
379
380/// MySQL: a named lock from `GET_LOCK`, held by one pooled connection, so it lasts for the whole
381/// run and is released when the run ends.
382#[cfg(feature = "mysql")]
383async fn mysql_lock<T>(
384    pool: &sqlx::MySqlPool,
385    timeout: Duration,
386    body: impl Future<Output = Result<T, Error>>,
387) -> Result<T, Error> {
388    let name = format!(
389        "{LOCK_PREFIX}:{}",
390        pool.connect_options().get_database().unwrap_or("")
391    );
392    let mut conn = pool.acquire().await?;
393    let got: Option<i64> = sqlx::query_scalar("SELECT GET_LOCK(?, ?)")
394        .bind(&name)
395        .bind(timeout.as_secs() as i64)
396        .fetch_one(&mut *conn)
397        .await?;
398    if got != Some(1) {
399        return Err(Error::MigrationsLocked {
400            seconds: timeout.as_secs(),
401        });
402    }
403
404    let result = body.await;
405    let released = sqlx::query("SELECT RELEASE_LOCK(?)")
406        .bind(&name)
407        .execute(&mut *conn)
408        .await;
409    if released.is_err() {
410        // Closing the connection drops the lock on the server, so it cannot stay in the pool.
411        drop(conn.detach());
412    }
413    let value = result?;
414    released?;
415    Ok(value)
416}
417
418/// PostgreSQL: a session advisory lock, keyed by a stable hash of the database name. It is held by
419/// one pooled connection, and waiting is a `pg_try_advisory_lock` retried until the timeout.
420#[cfg(feature = "postgres")]
421async fn postgres_lock<T>(
422    pool: &sqlx::PgPool,
423    timeout: Duration,
424    body: impl Future<Output = Result<T, Error>>,
425) -> Result<T, Error> {
426    let name = format!(
427        "{LOCK_PREFIX}:{}",
428        pool.connect_options().get_database().unwrap_or("")
429    );
430    let key = advisory_key(&name);
431    let mut conn = pool.acquire().await?;
432    let deadline = Instant::now() + timeout;
433    loop {
434        let got: bool = sqlx::query_scalar("SELECT pg_try_advisory_lock($1)")
435            .bind(key)
436            .fetch_one(&mut *conn)
437            .await?;
438        if got {
439            break;
440        }
441        if Instant::now() >= deadline {
442            return Err(Error::MigrationsLocked {
443                seconds: timeout.as_secs(),
444            });
445        }
446        tokio::time::sleep(LOCK_POLL).await;
447    }
448
449    let result = body.await;
450    let released = sqlx::query_scalar::<_, bool>("SELECT pg_advisory_unlock($1)")
451        .bind(key)
452        .fetch_one(&mut *conn)
453        .await;
454    if released.is_err() {
455        // Closing the connection ends the session, which drops its advisory locks.
456        drop(conn.detach());
457    }
458    let value = result?;
459    released?;
460    Ok(value)
461}
462
463/// SQLite has no named lock, so this lock is held by the process: the name is kept in a set
464/// while the run lasts, and a second run in the same process waits for it to leave the set.
465/// Two processes are not held apart by this lock. SQLite serialises their writes, so they can
466/// both reach the migrations table, and one of them may fail on a duplicate version. See
467/// `docs/laravel-differences.md`.
468#[cfg(feature = "sqlite")]
469async fn sqlite_lock<T>(
470    pool: &sqlx::SqlitePool,
471    timeout: Duration,
472    body: impl Future<Output = Result<T, Error>>,
473) -> Result<T, Error> {
474    let name = format!(
475        "{LOCK_PREFIX}:{}",
476        pool.connect_options().get_filename().display()
477    );
478    let deadline = Instant::now() + timeout;
479    loop {
480        let taken = SQLITE_HELD
481            .lock()
482            .unwrap_or_else(PoisonError::into_inner)
483            .insert(name.clone());
484        if taken {
485            break;
486        }
487        if Instant::now() >= deadline {
488            return Err(Error::MigrationsLocked {
489                seconds: timeout.as_secs(),
490            });
491        }
492        tokio::time::sleep(LOCK_POLL).await;
493    }
494
495    let result = body.await;
496    SQLITE_HELD
497        .lock()
498        .unwrap_or_else(PoisonError::into_inner)
499        .remove(&name);
500    result
501}
502
503/// A stable 64-bit key for a PostgreSQL advisory lock: FNV-1a of the lock name. It does not change
504/// between releases, so two versions of the app agree on the lock.
505#[cfg(feature = "postgres")]
506fn advisory_key(name: &str) -> i64 {
507    let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
508    for byte in name.bytes() {
509        hash ^= u64::from(byte);
510        hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
511    }
512    hash as i64
513}
514
515/// The body of [`Migrator::import_diesel_history`], run under the lock.
516async fn import_unlocked(db: &Database) -> Result<Vec<String>, Error> {
517    ensure_table(db).await?;
518    Ok(Vec::new())
519}
520
521/// Whether the name starts with a 14-digit timestamp and an underscore, followed by a name.
522fn has_timestamp(name: &str) -> bool {
523    let bytes = name.as_bytes();
524    bytes.len() > 15 && bytes[..14].iter().all(u8::is_ascii_digit) && bytes[14] == b'_'
525}
526
527/// The timestamp of a name that passed `has_timestamp`. It is the `version` in the table.
528fn timestamp(name: &str) -> &str {
529    &name[..14]
530}
531
532/// The statement and values for the driver: `?` placeholders are numbered on PostgreSQL.
533fn prepared(driver: Driver, sql: &str, values: Vec<Value>) -> (String, Vec<Value>) {
534    driver.prepare(sql, &values)
535}
536
537/// Picks the SQL for the driver. Each argument is the same statement written for one database.
538#[cfg_attr(
539    not(all(feature = "mysql", feature = "postgres", feature = "sqlite")),
540    allow(unused_variables)
541)]
542fn pick<'a>(driver: Driver, mysql: &'a str, postgres: &'a str, sqlite: &'a str) -> &'a str {
543    match driver {
544        #[cfg(feature = "mysql")]
545        Driver::MySql => mysql,
546        #[cfg(feature = "postgres")]
547        Driver::Postgres => postgres,
548        #[cfg(feature = "sqlite")]
549        Driver::Sqlite => sqlite,
550    }
551}
552
553/// Creates the migrations table in Diesel's layout if it does not exist, with Orbit's `batch`
554/// column. A table that Diesel created has `version` and `run_on` only. For that table the `batch`
555/// column is added, and its existing rows get batch 0, so they count as one batch that was
556/// imported from Diesel. Nothing is dropped or renamed. Safe to run on every call.
557async fn ensure_table(db: &Database) -> Result<(), Error> {
558    let driver = db.driver();
559    let create = pick(
560        driver,
561        "CREATE TABLE IF NOT EXISTS __diesel_schema_migrations (\
562         version VARCHAR(50) NOT NULL PRIMARY KEY, \
563         run_on TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, \
564         batch INT NOT NULL DEFAULT 0)",
565        "CREATE TABLE IF NOT EXISTS __diesel_schema_migrations (\
566         version VARCHAR(50) NOT NULL PRIMARY KEY, \
567         run_on TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, \
568         batch INT NOT NULL DEFAULT 0)",
569        "CREATE TABLE IF NOT EXISTS __diesel_schema_migrations (\
570         version TEXT NOT NULL PRIMARY KEY, \
571         run_on TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, \
572         batch INTEGER NOT NULL DEFAULT 0)",
573    );
574    backend::execute(Target::Pool(&db.pool), create, &[]).await?;
575    if has_batch_column(db).await? {
576        return Ok(());
577    }
578    let added = backend::execute(
579        Target::Pool(&db.pool),
580        "ALTER TABLE __diesel_schema_migrations ADD COLUMN batch INT NOT NULL DEFAULT 0",
581        &[],
582    )
583    .await;
584    match added {
585        Ok(_) => Ok(()),
586        // Another process added the column after the check above.
587        Err(err) if is_duplicate_column(&err) => Ok(()),
588        Err(err) => Err(err.into()),
589    }
590}
591
592/// Whether the migrations table has a `batch` column.
593async fn has_batch_column(db: &Database) -> Result<bool, Error> {
594    let sql = pick(
595        db.driver(),
596        "SELECT COUNT(*) FROM information_schema.columns \
597         WHERE table_schema = DATABASE() AND table_name = '__diesel_schema_migrations' \
598         AND column_name = 'batch'",
599        "SELECT COUNT(*) FROM information_schema.columns \
600         WHERE table_schema = current_schema() AND table_name = '__diesel_schema_migrations' \
601         AND column_name = 'batch'",
602        "SELECT COUNT(*) FROM pragma_table_info('__diesel_schema_migrations') \
603         WHERE name = 'batch'",
604    );
605    let found: i64 = backend::scalar(Target::Pool(&db.pool), sql, &[]).await?;
606    Ok(found > 0)
607}
608
609/// Whether the error says that the column already exists: MySQL 1060, PostgreSQL 42701, or
610/// SQLite's "duplicate column name".
611fn is_duplicate_column(err: &sqlx::Error) -> bool {
612    err.as_database_error().is_some_and(|db_err| {
613        matches!(db_err.code().as_deref(), Some("1060" | "42701"))
614            || db_err.message().contains("duplicate column name")
615    })
616}
617
618/// The applied migrations, keyed by version, with their batch numbers.
619async fn applied(db: &Database) -> Result<HashMap<String, i64>, Error> {
620    let rows: Vec<(String, i32)> = backend::fetch_all(
621        Target::Pool(&db.pool),
622        &format!("SELECT version, batch FROM {TABLE}"),
623        &[],
624    )
625    .await?;
626    Ok(rows
627        .into_iter()
628        .map(|(version, batch)| (version, i64::from(batch)))
629        .collect())
630}
631
632/// The versions in the migrations table, or `None` when the table does not exist. Only reads the
633/// table.
634async fn recorded_versions(db: &Database) -> Result<Option<HashSet<String>>, Error> {
635    let sql = pick(
636        db.driver(),
637        "SELECT COUNT(*) FROM information_schema.tables \
638         WHERE table_schema = DATABASE() AND table_name = '__diesel_schema_migrations'",
639        "SELECT COUNT(*) FROM information_schema.tables \
640         WHERE table_schema = current_schema() AND table_name = '__diesel_schema_migrations'",
641        "SELECT COUNT(*) FROM sqlite_master \
642         WHERE type = 'table' AND name = '__diesel_schema_migrations'",
643    );
644    let found: i64 = backend::scalar(Target::Pool(&db.pool), sql, &[]).await?;
645    if found == 0 {
646        return Ok(None);
647    }
648    let versions: Vec<(String,)> = backend::fetch_all(
649        Target::Pool(&db.pool),
650        &format!("SELECT version FROM {TABLE}"),
651        &[],
652    )
653    .await?;
654    Ok(Some(versions.into_iter().map(|(v,)| v).collect()))
655}
656
657/// Reads a migration file. The error names the file.
658pub(crate) fn read_file(path: &Path) -> Result<String, Error> {
659    std::fs::read_to_string(path).map_err(|source| Error::Read {
660        path: path.to_path_buf(),
661        source,
662    })
663}