1use crate::{PgBackend, classify};
16use persistant_relational::{MigrateError, MigrateReport, Migrates, Migration, RelError};
17use sqlx::SqlSafeStr;
18use sqlx::migrate::{Migration as SqlxMigration, MigrationType, Migrator};
19
20pub(crate) fn migrator(migrations: &[Migration]) -> Migrator {
22 let sqlx_migrations: Vec<SqlxMigration> = migrations
23 .iter()
24 .map(|m| {
25 SqlxMigration::new(
26 m.version,
27 m.description.clone().into(),
28 MigrationType::Simple,
29 sqlx::AssertSqlSafe(m.sql.clone()).into_sql_str(),
30 false,
31 )
32 })
33 .collect();
34 Migrator::with_migrations(sqlx_migrations)
35}
36
37#[derive(Debug, Clone, PartialEq, Eq)]
42#[non_exhaustive]
43pub enum FromSqlxError {
44 NoTransaction {
47 version: i64,
49 },
50 LedgerTable {
53 table: String,
55 },
56 Setting(&'static str),
59 Checksum {
62 version: i64,
64 },
65}
66
67impl std::fmt::Display for FromSqlxError {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 match self {
70 FromSqlxError::NoTransaction { version } => {
71 write!(f, "migration {version} runs outside a transaction")
72 }
73 FromSqlxError::LedgerTable { table } => {
74 write!(
75 f,
76 "the migrator keeps its ledger in `{table}`, not `_sqlx_migrations`"
77 )
78 }
79 FromSqlxError::Setting(s) => write!(f, "the migrator sets {s}, which is not supported"),
80 FromSqlxError::Checksum { version } => {
81 write!(
82 f,
83 "migration {version}: checksum is not the SHA-384 of its text"
84 )
85 }
86 }
87 }
88}
89
90impl std::error::Error for FromSqlxError {}
91
92pub fn migrations_from_sqlx(migrator: &Migrator) -> Result<Vec<Migration>, FromSqlxError> {
101 if migrator.table_name != "_sqlx_migrations" {
102 return Err(FromSqlxError::LedgerTable {
103 table: migrator.table_name.to_string(),
104 });
105 }
106 if migrator.no_tx {
107 return Err(FromSqlxError::Setting("no_tx"));
108 }
109 if !migrator.locking {
110 return Err(FromSqlxError::Setting("locking = false"));
111 }
112 let mut out = Vec::new();
113 for m in migrator
114 .iter()
115 .filter(|m| !m.migration_type.is_down_migration())
116 {
117 if m.no_tx {
118 return Err(FromSqlxError::NoTransaction { version: m.version });
119 }
120 let sql = sqlx::SqlStr::as_str(&m.sql);
121 let ours = Migration::new(m.version, m.description.to_string(), sql);
122 if migrator_checksum(&ours) != m.checksum.as_ref() {
123 return Err(FromSqlxError::Checksum { version: m.version });
124 }
125 out.push(ours);
126 }
127 Ok(out)
128}
129
130fn migrator_checksum(m: &Migration) -> Vec<u8> {
132 migrator(std::slice::from_ref(m))
133 .iter()
134 .next()
135 .map(|x| x.checksum.to_vec())
136 .unwrap_or_default()
137}
138
139pub(crate) fn map_error(
141 e: sqlx::migrate::MigrateError,
142 classify: fn(sqlx::Error) -> RelError,
143) -> MigrateError {
144 match e {
145 sqlx::migrate::MigrateError::VersionMismatch(version) => {
146 MigrateError::ChecksumMismatch { version }
147 }
148 sqlx::migrate::MigrateError::VersionMissing(version) => {
149 MigrateError::NewerThanProgram { version }
150 }
151 sqlx::migrate::MigrateError::Dirty(version) => MigrateError::Dirty { version },
152 sqlx::migrate::MigrateError::ExecuteMigration(e, version) => MigrateError::Failed {
153 version,
154 error: classify(e),
155 },
156 other => MigrateError::Backend(other.to_string()),
157 }
158}
159
160pub(crate) fn backend_error(e: sqlx::Error) -> MigrateError {
161 MigrateError::Backend(e.to_string())
162}
163
164macro_rules! run_on_own_connection {
180 ($pool:expr, $migrations:expr, $classify:expr, $exists:expr, $guard:expr) => {{
181 let mut conn = $pool
182 .acquire()
183 .await
184 .map_err(crate::migrate::backend_error)?;
185 let mut failure = None;
186 let mut before = Vec::new();
187 let mut after = Vec::new();
188 let guard: Option<(&str, &str)> = $guard;
189 'run: {
190 if let Some((take, _)) = guard {
191 match sqlx::query_scalar::<_, Option<i64>>(sqlx::AssertSqlSafe(take.to_string()))
192 .fetch_one(&mut *conn)
193 .await
194 {
195 Ok(Some(1)) => {}
196 Ok(other) => {
197 failure = Some(persistant_relational::MigrateError::Backend(format!(
198 "the migration lock was not granted: {other:?}"
199 )));
200 break 'run;
201 }
202 Err(e) => {
203 failure = Some(crate::migrate::backend_error(e));
204 break 'run;
205 }
206 }
207 }
208 if let Err(e) = sqlx::migrate::Migrate::lock(&mut *conn).await {
209 failure = Some(persistant_relational::MigrateError::Backend(e.to_string()));
210 break 'run;
211 }
212 match crate::migrate::ledger(&mut *conn, $exists).await {
213 Ok(v) => before = v.into_iter().map(|(version, _)| version).collect(),
214 Err(e) => {
215 failure = Some(e);
216 break 'run;
217 }
218 }
219 if let Err(e) = crate::migrate::migrator($migrations)
221 .run_direct(None, &mut *conn, false)
222 .await
223 {
224 failure = Some(crate::migrate::map_error(e, $classify));
225 break 'run;
226 }
227 match crate::migrate::ledger(&mut *conn, $exists).await {
228 Ok(v) => after = v,
229 Err(e) => {
230 failure = Some(e);
231 break 'run;
232 }
233 }
234 if let Err(e) = sqlx::migrate::Migrate::unlock(&mut *conn).await {
235 failure = Some(persistant_relational::MigrateError::Backend(e.to_string()));
236 }
237 if let Some((_, release)) = guard {
238 if let Err(e) = sqlx::query(sqlx::AssertSqlSafe(release.to_string()))
239 .execute(&mut *conn)
240 .await
241 {
242 failure = Some(crate::migrate::backend_error(e));
243 }
244 }
245 }
246 match failure {
247 None => {
248 let applied = after
249 .iter()
250 .map(|(version, _)| *version)
251 .filter(|v| !before.contains(v))
252 .collect();
253 let checksums = after;
254 Ok(persistant_relational::MigrateReport {
255 applied,
256 already_applied: before,
257 checksums,
258 })
259 }
260 Some(e) => {
261 let _ = sqlx::Connection::close(conn.detach()).await;
262 Err(e)
263 }
264 }
265 }};
266}
267#[cfg(any(feature = "mysql", feature = "sqlite"))]
268pub(crate) use run_on_own_connection;
269
270pub(crate) async fn ledger<C>(
273 conn: &mut C,
274 exists: &str,
275) -> Result<Vec<(i64, Vec<u8>)>, MigrateError>
276where
277 C: sqlx::Connection,
278 for<'c> &'c mut C: sqlx::Executor<'c, Database = C::Database>,
279 for<'q> i64: sqlx::Decode<'q, C::Database> + sqlx::Type<C::Database>,
280 for<'q> Vec<u8>: sqlx::Decode<'q, C::Database> + sqlx::Type<C::Database>,
281 <C::Database as sqlx::Database>::Arguments: sqlx::IntoArguments<C::Database>,
282 usize: sqlx::ColumnIndex<<C::Database as sqlx::Database>::Row>,
283{
284 let there: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(exists.to_string()))
285 .fetch_one(&mut *conn)
286 .await
287 .map_err(backend_error)?;
288 if there == 0 {
289 return Ok(Vec::new());
290 }
291 sqlx::query_as(sqlx::AssertSqlSafe(
292 "SELECT version, checksum FROM _sqlx_migrations WHERE success ORDER BY version".to_string(),
293 ))
294 .fetch_all(&mut *conn)
295 .await
296 .map_err(backend_error)
297}
298
299impl Migrates for PgBackend {
300 async fn migrate(&self, migrations: &[Migration]) -> Result<MigrateReport, MigrateError> {
301 run_on_own_connection!(
302 self.pool,
303 migrations,
304 classify,
305 "SELECT CASE WHEN to_regclass('_sqlx_migrations') IS NULL THEN 0::int8 ELSE 1::int8 END",
306 None
307 )
308 }
309
310 async fn applied_versions(&self) -> Result<Vec<i64>, MigrateError> {
311 let exists: bool = sqlx::query_scalar("SELECT to_regclass('_sqlx_migrations') IS NOT NULL")
312 .fetch_one(&self.pool)
313 .await
314 .map_err(backend_error)?;
315 if !exists {
316 return Ok(Vec::new());
317 }
318 sqlx::query_scalar("SELECT version FROM _sqlx_migrations WHERE success ORDER BY version")
319 .fetch_all(&self.pool)
320 .await
321 .map_err(backend_error)
322 }
323}
324
325impl PgBackend {
326 pub async fn migrate(&self, migrations: &[Migration]) -> Result<MigrateReport, MigrateError> {
329 Migrates::migrate(self, migrations).await
330 }
331
332 pub async fn verify_fresh(
344 &self,
345 migrations: &[Migration],
346 ) -> Result<MigrateReport, MigrateError> {
347 self.fresh_run(migrations, false).await.map(|(r, _)| r)
348 }
349
350 pub async fn fresh_catalog(
354 &self,
355 migrations: &[Migration],
356 ) -> Result<persistant_relational::catalog::Snapshot, MigrateError> {
357 let (_, catalog) = self.fresh_run(migrations, true).await?;
358 Ok(catalog.expect("a catalog was asked for"))
359 }
360
361 async fn fresh_run(
362 &self,
363 migrations: &[Migration],
364 want_catalog: bool,
365 ) -> Result<
366 (
367 MigrateReport,
368 Option<persistant_relational::catalog::Snapshot>,
369 ),
370 MigrateError,
371 > {
372 use std::sync::atomic::{AtomicU64, Ordering};
373 static N: AtomicU64 = AtomicU64::new(0);
374 let nanos = std::time::SystemTime::now()
375 .duration_since(std::time::UNIX_EPOCH)
376 .map_or(0, |d| d.as_nanos());
377 let schema = format!(
378 "persistant_fresh_{}_{}_{}",
379 std::process::id(),
380 nanos,
381 N.fetch_add(1, Ordering::Relaxed)
382 );
383 sqlx::query(sqlx::AssertSqlSafe(format!("CREATE SCHEMA {schema}")))
384 .execute(&self.pool)
385 .await
386 .map_err(backend_error)?;
387 let search = format!("SET search_path TO {schema}");
388 let scratch = sqlx::postgres::PgPoolOptions::new()
389 .max_connections(1)
390 .after_connect(move |conn, _| {
391 let search = search.clone();
392 Box::pin(async move {
393 sqlx::query(sqlx::AssertSqlSafe(search))
394 .execute(&mut *conn)
395 .await
396 .map(|_| ())
397 })
398 })
399 .connect_with((*self.pool.connect_options()).clone())
400 .await;
401 let result = match scratch {
402 Ok(pool) => {
403 let r = match PgBackend::from_pool(pool.clone()).await {
404 Ok(b) => scratch_run(&b, migrations, want_catalog, b.applied_checksums()).await,
405 Err(e) => Err(MigrateError::Backend(e.to_string())),
406 };
407 pool.close().await;
408 r
409 }
410 Err(e) => Err(backend_error(e)),
411 };
412 let dropped = sqlx::query(sqlx::AssertSqlSafe(format!("DROP SCHEMA {schema} CASCADE")))
413 .execute(&self.pool)
414 .await
415 .map_err(backend_error);
416 let report = result?;
417 dropped?;
418 Ok(report)
419 }
420
421 pub async fn applied_versions(&self) -> Result<Vec<i64>, MigrateError> {
423 Migrates::applied_versions(self).await
424 }
425
426 pub async fn applied_checksums(&self) -> Result<Vec<(i64, Vec<u8>)>, MigrateError> {
430 let mut conn = self.pool.acquire().await.map_err(backend_error)?;
431 ledger(
432 &mut *conn,
433 "SELECT CASE WHEN to_regclass('_sqlx_migrations') IS NULL THEN 0::int8 ELSE 1::int8 END",
434 )
435 .await
436 }
437}
438
439pub(crate) async fn scratch_run<B, L>(
442 b: &B,
443 migrations: &[Migration],
444 want_catalog: bool,
445 ledger: L,
446) -> Result<
447 (
448 MigrateReport,
449 Option<persistant_relational::catalog::Snapshot>,
450 ),
451 MigrateError,
452>
453where
454 B: persistant_relational::RelationalBackend + Migrates,
455 L: std::future::Future<Output = Result<Vec<(i64, Vec<u8>)>, MigrateError>>,
456{
457 let report = Migrates::migrate(b, migrations).await?;
458 if !want_catalog {
459 return Ok((report, None));
460 }
461 let meta = persistant_relational::catalog::migration_meta(&ledger.await?);
462 let catalog = persistant_relational::catalog::read_catalog(b, meta)
463 .await
464 .map_err(|e| MigrateError::Backend(e.to_string()))?;
465 Ok((report, Some(catalog)))
466}