Skip to main content

persistant_relational/
migrate.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Checksummed, ordered, resumable schema migrations as a **capability**: the types a driver speaks and
3//! the [`Migrates`] trait it implements. The driver crate owns the ledger (`persistant-sqlx` keeps sqlx's
4//! own `_sqlx_migrations` table, so a database can be handled by this crate or by `sqlx migrate run`
5//! without a conversion). What the ledger promises is decided by
6//! [`migrate_conformance`](crate::migrate_conformance), not asserted here.
7
8use crate::{RelError, RelationalBackend};
9use std::fmt;
10use std::future::Future;
11
12/// One migration: a version, a description and the SQL text. The text is hashed; changing a
13/// migration that has been applied is an error, never a silent re-run.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct Migration {
16    /// Positive, strictly increasing across a program's migrations (sqlx uses the timestamp or
17    /// sequence in the file name).
18    pub version: i64,
19    /// A short description, stored in the ledger.
20    pub description: String,
21    /// The statements, run in one transaction.
22    pub sql: String,
23}
24
25impl Migration {
26    /// A migration.
27    pub fn new(version: i64, description: impl Into<String>, sql: impl Into<String>) -> Self {
28        Migration {
29            version,
30            description: description.into(),
31            sql: sql.into(),
32        }
33    }
34}
35
36/// Why a migration run stopped.
37#[derive(Debug, Clone, PartialEq, Eq)]
38#[non_exhaustive]
39pub enum MigrateError {
40    /// A migration already applied has different text now (its checksum differs).
41    ChecksumMismatch {
42        /// The version whose text changed.
43        version: i64,
44    },
45    /// The database has applied a version this program does not know: it is older than the
46    /// database it is pointed at. The program refuses rather than run against a newer schema.
47    NewerThanProgram {
48        /// The unknown applied version.
49        version: i64,
50    },
51    /// A migration is recorded as partially applied (`success = false`); a person must fix it.
52    Dirty {
53        /// The version.
54        version: i64,
55    },
56    /// A migration's SQL failed; its transaction was rolled back.
57    Failed {
58        /// The version.
59        version: i64,
60        /// The database's classification of the failure.
61        error: RelError,
62    },
63    /// Anything else (connection, ledger access).
64    Backend(String),
65}
66
67impl fmt::Display for MigrateError {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        match self {
70            MigrateError::ChecksumMismatch { version } => {
71                write!(f, "migration {version} was applied and has been modified")
72            }
73            MigrateError::NewerThanProgram { version } => write!(
74                f,
75                "the database has applied migration {version}, which this program does not know"
76            ),
77            MigrateError::Dirty { version } => {
78                write!(
79                    f,
80                    "migration {version} is partially applied; fix it by hand"
81                )
82            }
83            MigrateError::Failed { version, error } => {
84                write!(f, "migration {version} failed: {error}")
85            }
86            MigrateError::Backend(m) => write!(f, "migration: {m}"),
87        }
88    }
89}
90
91impl std::error::Error for MigrateError {}
92
93/// What a successful run did.
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub struct MigrateReport {
96    /// Versions applied by this run, ascending.
97    pub applied: Vec<i64>,
98    /// Versions that were already applied and still match.
99    pub already_applied: Vec<i64>,
100    /// The checksum the ledger holds for every version applied after this run (both lists above), as
101    /// `(version, checksum)` ascending. A backend whose ledger keeps none leaves this empty.
102    pub checksums: Vec<(i64, Vec<u8>)>,
103}
104
105/// A relational backend that keeps a migration ledger.
106///
107/// The ledger lives in the connection's current schema (or database): point this at a database a
108/// program owns, never at one whose ledger belongs to something else.
109pub trait Migrates: RelationalBackend {
110    /// Apply `migrations` in version order, skipping those already applied. Each migration runs in
111    /// its own transaction where the database can; refuses (applying nothing further) if an applied
112    /// migration's text has changed, if the database has applied a version the program does not
113    /// know, or if a migration is recorded as dirty.
114    fn migrate(
115        &self,
116        migrations: &[Migration],
117    ) -> impl Future<Output = Result<MigrateReport, MigrateError>> + Send;
118
119    /// The versions the ledger says succeeded, ascending; empty if there is no ledger yet.
120    fn applied_versions(&self) -> impl Future<Output = Result<Vec<i64>, MigrateError>> + Send;
121}