persistant_sqlx/
migrate.rs1use crate::{PgBackend, classify};
14use persistant_relational::RelError;
15use sqlx::SqlSafeStr;
16use sqlx::migrate::{Migration as SqlxMigration, MigrationType, Migrator};
17use std::fmt;
18
19#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct Migration {
23 pub version: i64,
26 pub description: String,
28 pub sql: String,
30}
31
32impl Migration {
33 pub fn new(version: i64, description: impl Into<String>, sql: impl Into<String>) -> Self {
35 Migration {
36 version,
37 description: description.into(),
38 sql: sql.into(),
39 }
40 }
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
45#[non_exhaustive]
46pub enum MigrateError {
47 ChecksumMismatch {
49 version: i64,
51 },
52 NewerThanProgram {
55 version: i64,
57 },
58 Dirty {
60 version: i64,
62 },
63 Failed {
65 version: i64,
67 error: RelError,
69 },
70 Backend(String),
72}
73
74impl fmt::Display for MigrateError {
75 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76 match self {
77 MigrateError::ChecksumMismatch { version } => {
78 write!(f, "migration {version} was applied and has been modified")
79 }
80 MigrateError::NewerThanProgram { version } => write!(
81 f,
82 "the database has applied migration {version}, which this program does not know"
83 ),
84 MigrateError::Dirty { version } => {
85 write!(
86 f,
87 "migration {version} is partially applied; fix it by hand"
88 )
89 }
90 MigrateError::Failed { version, error } => {
91 write!(f, "migration {version} failed: {error}")
92 }
93 MigrateError::Backend(m) => write!(f, "migration: {m}"),
94 }
95 }
96}
97
98impl std::error::Error for MigrateError {}
99
100#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct MigrateReport {
103 pub applied: Vec<i64>,
105 pub already_applied: Vec<i64>,
107}
108
109impl PgBackend {
110 pub async fn migrate(&self, migrations: &[Migration]) -> Result<MigrateReport, MigrateError> {
117 let sqlx_migrations: Vec<SqlxMigration> = migrations
118 .iter()
119 .map(|m| {
120 SqlxMigration::new(
121 m.version,
122 m.description.clone().into(),
123 MigrationType::Simple,
124 sqlx::AssertSqlSafe(m.sql.clone()).into_sql_str(),
125 false,
126 )
127 })
128 .collect();
129 let before = self.applied_versions().await?;
130 let migrator = Migrator::with_migrations(sqlx_migrations);
131 migrator.run(&self.pool).await.map_err(|e| match e {
132 sqlx::migrate::MigrateError::VersionMismatch(version) => {
133 MigrateError::ChecksumMismatch { version }
134 }
135 sqlx::migrate::MigrateError::VersionMissing(version) => {
136 MigrateError::NewerThanProgram { version }
137 }
138 sqlx::migrate::MigrateError::Dirty(version) => MigrateError::Dirty { version },
139 sqlx::migrate::MigrateError::ExecuteMigration(e, version) => MigrateError::Failed {
140 version,
141 error: classify(e),
142 },
143 other => MigrateError::Backend(other.to_string()),
144 })?;
145 let after = self.applied_versions().await?;
146 let applied = after
147 .iter()
148 .copied()
149 .filter(|v| !before.contains(v))
150 .collect();
151 Ok(MigrateReport {
152 applied,
153 already_applied: before,
154 })
155 }
156
157 pub async fn applied_versions(&self) -> Result<Vec<i64>, MigrateError> {
159 let exists: bool = sqlx::query_scalar("SELECT to_regclass('_sqlx_migrations') IS NOT NULL")
160 .fetch_one(&self.pool)
161 .await
162 .map_err(|e| MigrateError::Backend(e.to_string()))?;
163 if !exists {
164 return Ok(Vec::new());
165 }
166 sqlx::query_scalar("SELECT version FROM _sqlx_migrations WHERE success ORDER BY version")
167 .fetch_all(&self.pool)
168 .await
169 .map_err(|e| MigrateError::Backend(e.to_string()))
170 }
171}