1#[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#[derive(Debug, Clone)]
24pub struct Migration {
25 pub name: String,
27 pub up: String,
29 pub down: String,
31}
32
33impl Migration {
34 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum State {
47 Applied { batch: i64 },
48 Pending,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct Status {
54 pub name: String,
55 pub state: State,
56}
57
58#[derive(Debug, Clone)]
61pub struct Migrator {
62 migrations: Vec<Migration>,
63 lock_timeout: Duration,
64}
65
66const LOCK_PREFIX: &str = "orbit_migrations";
69
70const DEFAULT_LOCK_TIMEOUT: Duration = Duration::from_secs(10);
72
73#[cfg(any(feature = "postgres", feature = "sqlite"))]
75const LOCK_POLL: Duration = Duration::from_millis(50);
76
77const TABLE: &str = "__diesel_schema_migrations";
79
80#[cfg(feature = "sqlite")]
83static SQLITE_HELD: Mutex<BTreeSet<String>> = Mutex::new(BTreeSet::new());
84
85impl Migrator {
86 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 pub fn with_lock_timeout(mut self, timeout: Duration) -> Self {
119 self.lock_timeout = timeout;
120 self
121 }
122
123 pub fn migrations(&self) -> &[Migration] {
125 &self.migrations
126 }
127
128 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 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 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 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 #[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 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#[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 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#[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 drop(conn.detach());
412 }
413 let value = result?;
414 released?;
415 Ok(value)
416}
417
418#[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 drop(conn.detach());
457 }
458 let value = result?;
459 released?;
460 Ok(value)
461}
462
463#[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#[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
515async fn import_unlocked(db: &Database) -> Result<Vec<String>, Error> {
517 ensure_table(db).await?;
518 Ok(Vec::new())
519}
520
521fn 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
527fn timestamp(name: &str) -> &str {
529 &name[..14]
530}
531
532fn prepared(driver: Driver, sql: &str, values: Vec<Value>) -> (String, Vec<Value>) {
534 driver.prepare(sql, &values)
535}
536
537#[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
553async 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 Err(err) if is_duplicate_column(&err) => Ok(()),
588 Err(err) => Err(err.into()),
589 }
590}
591
592async 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
609fn 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
618async 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
632async 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
657pub(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}