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}