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::schema::Statement;
20use crate::transaction::{Target, Transaction};
21use crate::{Database, Driver, Error, Value};
22
23#[derive(Debug, Clone)]
26pub struct Migration {
27 pub name: String,
29 pub up: String,
32 pub down: String,
34 schema: Option<SchemaBody>,
36}
37
38#[derive(Debug, Clone)]
39struct SchemaBody {
40 up: Vec<Statement>,
41 down: Vec<Statement>,
42}
43
44impl Migration {
45 pub fn new(name: impl Into<String>, up: impl Into<String>, down: impl Into<String>) -> Self {
47 Self {
48 name: name.into(),
49 up: up.into(),
50 down: down.into(),
51 schema: None,
52 }
53 }
54
55 pub fn schema(name: impl Into<String>, up: Vec<Statement>, down: Vec<Statement>) -> Self {
64 Self {
65 name: name.into(),
66 up: String::new(),
67 down: String::new(),
68 schema: Some(SchemaBody { up, down }),
69 }
70 }
71
72 fn up_scripts(&self, driver: Driver) -> Result<Vec<String>, Error> {
74 match &self.schema {
75 Some(body) => render(&body.up, driver),
76 None => Ok(vec![self.up.clone()]),
77 }
78 }
79
80 fn down_scripts(&self, driver: Driver) -> Result<Vec<String>, Error> {
82 match &self.schema {
83 Some(body) => render(&body.down, driver),
84 None => Ok(vec![self.down.clone()]),
85 }
86 }
87}
88
89fn render(statements: &[Statement], driver: Driver) -> Result<Vec<String>, Error> {
90 let mut sql = Vec::new();
91 for statement in statements {
92 sql.extend(statement.try_to_sql(driver)?);
93 }
94 Ok(sql)
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub enum State {
100 Applied { batch: i64 },
101 Pending,
102}
103
104#[derive(Debug, Clone, PartialEq, Eq)]
106pub struct Status {
107 pub name: String,
108 pub state: State,
109}
110
111#[derive(Debug, Clone)]
114pub struct Migrator {
115 migrations: Vec<Migration>,
116 lock_timeout: Duration,
117}
118
119const LOCK_PREFIX: &str = "orbit_migrations";
122
123const DEFAULT_LOCK_TIMEOUT: Duration = Duration::from_secs(10);
125
126#[cfg(any(feature = "postgres", feature = "sqlite"))]
128const LOCK_POLL: Duration = Duration::from_millis(50);
129
130const TABLE: &str = "__diesel_schema_migrations";
132
133#[cfg(feature = "sqlite")]
136static SQLITE_HELD: Mutex<BTreeSet<String>> = Mutex::new(BTreeSet::new());
137
138impl Migrator {
139 pub fn new(mut migrations: Vec<Migration>) -> Result<Self, Error> {
143 for migration in &migrations {
144 if !has_timestamp(&migration.name) {
145 return Err(Error::InvalidMigrationName {
146 name: migration.name.clone(),
147 });
148 }
149 }
150 migrations.sort_by(|a, b| a.name.cmp(&b.name));
151 for pair in migrations.windows(2) {
152 if pair[0].name == pair[1].name {
153 return Err(Error::DuplicateMigration {
154 name: pair[0].name.clone(),
155 });
156 }
157 if timestamp(&pair[0].name) == timestamp(&pair[1].name) {
158 return Err(Error::DuplicateTimestamp {
159 first: pair[0].name.clone(),
160 second: pair[1].name.clone(),
161 });
162 }
163 }
164 Ok(Self {
165 migrations,
166 lock_timeout: DEFAULT_LOCK_TIMEOUT,
167 })
168 }
169
170 pub fn with_lock_timeout(mut self, timeout: Duration) -> Self {
172 self.lock_timeout = timeout;
173 self
174 }
175
176 pub fn migrations(&self) -> &[Migration] {
178 &self.migrations
179 }
180
181 pub fn from_dir(dir: impl AsRef<Path>) -> Result<Self, Error> {
184 let dir = dir.as_ref();
185 let entries = std::fs::read_dir(dir).map_err(|source| Error::Read {
186 path: dir.to_path_buf(),
187 source,
188 })?;
189 let mut migrations = Vec::new();
190 for entry in entries {
191 let entry = entry.map_err(|source| Error::Read {
192 path: dir.to_path_buf(),
193 source,
194 })?;
195 let name = entry.file_name().to_string_lossy().into_owned();
196 let path = entry.path();
197 if name.starts_with('.') || !path.is_dir() {
198 continue;
199 }
200 let up = read_file(&path.join("up.sql"))?;
201 let down = read_file(&path.join("down.sql"))?;
202 migrations.push(Migration::new(name, up, down));
203 }
204 Self::new(migrations)
205 }
206
207 pub async fn run(&self, db: &Database) -> Result<Vec<String>, Error> {
219 self.with_lock(db, self.run_unlocked(db)).await
220 }
221
222 async fn run_unlocked(&self, db: &Database) -> Result<Vec<String>, Error> {
223 ensure_table(db).await?;
224 let applied = applied(db).await?;
225 let pending: Vec<&Migration> = self
226 .migrations
227 .iter()
228 .filter(|m| !applied.contains_key(timestamp(&m.name)))
229 .collect();
230 if pending.is_empty() {
231 return Ok(Vec::new());
232 }
233
234 let max: Option<i32> = backend::scalar(
235 Target::Pool(&db.pool),
236 &format!("SELECT MAX(batch) FROM {TABLE}"),
237 &[],
238 )
239 .await?;
240 let batch = i64::from(max.unwrap_or(0)) + 1;
241 let mut names = Vec::with_capacity(pending.len());
242 for migration in pending {
243 let scripts = migration.up_scripts(db.driver())?;
244 let mut tx = Transaction::begin(&db.pool).await?;
245 for script in &scripts {
246 backend::run_script(Target::Tx(&mut tx.inner), script)
247 .await
248 .map_err(|source| Error::MigrationFailed {
249 name: migration.name.clone(),
250 source,
251 })?;
252 }
253 let (sql, values) = prepared(
254 db.driver(),
255 &format!("INSERT INTO {TABLE} (version, batch) VALUES (?, ?)"),
256 vec![
257 Value::Text(timestamp(&migration.name).to_owned()),
258 Value::Int(batch),
259 ],
260 );
261 backend::execute(Target::Tx(&mut tx.inner), &sql, &values)
262 .await
263 .map_err(|source| Error::MigrationFailed {
264 name: migration.name.clone(),
265 source,
266 })?;
267 tx.commit().await?;
268 names.push(migration.name.clone());
269 }
270 Ok(names)
271 }
272
273 pub async fn status(&self, db: &Database) -> Result<Vec<Status>, Error> {
276 ensure_table(db).await?;
277 let applied = applied(db).await?;
278 Ok(self
279 .migrations
280 .iter()
281 .map(|m| Status {
282 name: m.name.clone(),
283 state: match applied.get(timestamp(&m.name)) {
284 Some(&batch) => State::Applied { batch },
285 None => State::Pending,
286 },
287 })
288 .collect())
289 }
290
291 pub async fn rollback(&self, db: &Database) -> Result<Vec<String>, Error> {
300 self.with_lock(db, self.rollback_unlocked(db)).await
301 }
302
303 async fn rollback_unlocked(&self, db: &Database) -> Result<Vec<String>, Error> {
304 ensure_table(db).await?;
305 let last: Option<i32> = backend::scalar(
306 Target::Pool(&db.pool),
307 &format!("SELECT MAX(batch) FROM {TABLE}"),
308 &[],
309 )
310 .await?;
311 let Some(batch) = last else {
312 return Ok(Vec::new());
313 };
314 let (sql, values) = prepared(
315 db.driver(),
316 &format!("SELECT version FROM {TABLE} WHERE batch = ? ORDER BY version DESC"),
317 vec![Value::Int(i64::from(batch))],
318 );
319 let versions: Vec<(String,)> =
320 backend::fetch_all(Target::Pool(&db.pool), &sql, &values).await?;
321
322 let mut to_revert = Vec::with_capacity(versions.len());
323 for (version,) in &versions {
324 let migration = self
325 .migrations
326 .iter()
327 .find(|m| timestamp(&m.name) == version.as_str())
328 .ok_or_else(|| Error::UnknownMigration {
329 name: version.clone(),
330 })?;
331 to_revert.push(migration);
332 }
333 let names = to_revert.iter().map(|m| m.name.clone()).collect();
334 for migration in to_revert {
335 let scripts = migration.down_scripts(db.driver())?;
336 let mut tx = Transaction::begin(&db.pool).await?;
337 for script in &scripts {
338 backend::run_script(Target::Tx(&mut tx.inner), script)
339 .await
340 .map_err(|source| Error::RollbackFailed {
341 name: migration.name.clone(),
342 source,
343 })?;
344 }
345 let (sql, values) = prepared(
346 db.driver(),
347 &format!("DELETE FROM {TABLE} WHERE version = ?"),
348 vec![Value::Text(timestamp(&migration.name).to_owned())],
349 );
350 backend::execute(Target::Tx(&mut tx.inner), &sql, &values)
351 .await
352 .map_err(|source| Error::RollbackFailed {
353 name: migration.name.clone(),
354 source,
355 })?;
356 tx.commit().await?;
357 }
358 Ok(names)
359 }
360}
361
362impl Migrator {
363 #[deprecated(
369 since = "0.1.1",
370 note = "not needed any more: run, status and rollback upgrade the migrations table \
371 themselves. This returns an empty list and will be removed in a later release"
372 )]
373 pub async fn import_diesel_history(&self, db: &Database) -> Result<Vec<String>, Error> {
374 self.with_lock(db, import_unlocked(db)).await
375 }
376
377 pub async fn unmatched_diesel_versions(&self, db: &Database) -> Result<Vec<String>, Error> {
381 let Some(versions) = recorded_versions(db).await? else {
382 return Ok(Vec::new());
383 };
384 let mut unmatched: Vec<String> = versions
385 .into_iter()
386 .filter(|version| {
387 !self
388 .migrations
389 .iter()
390 .any(|m| timestamp(&m.name) == version.as_str())
391 })
392 .collect();
393 unmatched.sort();
394 Ok(unmatched)
395 }
396}
397
398#[macro_export]
406macro_rules! migrations {
407 ($dir:literal, $($name:literal),* $(,)?) => {
408 $crate::Migrator::new(vec![
409 $(
410 $crate::Migration::new(
411 $name,
412 include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/", $dir, "/", $name, "/up.sql")),
413 include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/", $dir, "/", $name, "/down.sql")),
414 ),
415 )*
416 ])
417 };
418}
419
420impl Migrator {
421 async fn with_lock<T>(
424 &self,
425 db: &Database,
426 body: impl Future<Output = Result<T, Error>>,
427 ) -> Result<T, Error> {
428 match &db.pool {
429 #[cfg(feature = "mysql")]
430 Pool::MySql(pool) => mysql_lock(pool, self.lock_timeout, body).await,
431 #[cfg(feature = "postgres")]
432 Pool::Postgres(pool) => postgres_lock(pool, self.lock_timeout, body).await,
433 #[cfg(feature = "sqlite")]
434 Pool::Sqlite(pool) => sqlite_lock(pool, self.lock_timeout, body).await,
435 }
436 }
437}
438
439#[cfg(feature = "mysql")]
442async fn mysql_lock<T>(
443 pool: &sqlx::MySqlPool,
444 timeout: Duration,
445 body: impl Future<Output = Result<T, Error>>,
446) -> Result<T, Error> {
447 let name = format!(
448 "{LOCK_PREFIX}:{}",
449 pool.connect_options().get_database().unwrap_or("")
450 );
451 let mut conn = pool.acquire().await?;
452 let got: Option<i64> = sqlx::query_scalar("SELECT GET_LOCK(?, ?)")
453 .bind(&name)
454 .bind(timeout.as_secs() as i64)
455 .fetch_one(&mut *conn)
456 .await?;
457 if got != Some(1) {
458 return Err(Error::MigrationsLocked {
459 seconds: timeout.as_secs(),
460 });
461 }
462
463 let result = body.await;
464 let released = sqlx::query("SELECT RELEASE_LOCK(?)")
465 .bind(&name)
466 .execute(&mut *conn)
467 .await;
468 if released.is_err() {
469 drop(conn.detach());
471 }
472 let value = result?;
473 released?;
474 Ok(value)
475}
476
477#[cfg(feature = "postgres")]
480async fn postgres_lock<T>(
481 pool: &sqlx::PgPool,
482 timeout: Duration,
483 body: impl Future<Output = Result<T, Error>>,
484) -> Result<T, Error> {
485 let name = format!(
486 "{LOCK_PREFIX}:{}",
487 pool.connect_options().get_database().unwrap_or("")
488 );
489 let key = advisory_key(&name);
490 let mut conn = pool.acquire().await?;
491 let deadline = Instant::now() + timeout;
492 loop {
493 let got: bool = sqlx::query_scalar("SELECT pg_try_advisory_lock($1)")
494 .bind(key)
495 .fetch_one(&mut *conn)
496 .await?;
497 if got {
498 break;
499 }
500 if Instant::now() >= deadline {
501 return Err(Error::MigrationsLocked {
502 seconds: timeout.as_secs(),
503 });
504 }
505 tokio::time::sleep(LOCK_POLL).await;
506 }
507
508 let result = body.await;
509 let released = sqlx::query_scalar::<_, bool>("SELECT pg_advisory_unlock($1)")
510 .bind(key)
511 .fetch_one(&mut *conn)
512 .await;
513 if released.is_err() {
514 drop(conn.detach());
516 }
517 let value = result?;
518 released?;
519 Ok(value)
520}
521
522#[cfg(feature = "sqlite")]
528async fn sqlite_lock<T>(
529 pool: &sqlx::SqlitePool,
530 timeout: Duration,
531 body: impl Future<Output = Result<T, Error>>,
532) -> Result<T, Error> {
533 let name = format!(
534 "{LOCK_PREFIX}:{}",
535 pool.connect_options().get_filename().display()
536 );
537 let deadline = Instant::now() + timeout;
538 loop {
539 let taken = SQLITE_HELD
540 .lock()
541 .unwrap_or_else(PoisonError::into_inner)
542 .insert(name.clone());
543 if taken {
544 break;
545 }
546 if Instant::now() >= deadline {
547 return Err(Error::MigrationsLocked {
548 seconds: timeout.as_secs(),
549 });
550 }
551 tokio::time::sleep(LOCK_POLL).await;
552 }
553
554 let result = body.await;
555 SQLITE_HELD
556 .lock()
557 .unwrap_or_else(PoisonError::into_inner)
558 .remove(&name);
559 result
560}
561
562#[cfg(feature = "postgres")]
565fn advisory_key(name: &str) -> i64 {
566 let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
567 for byte in name.bytes() {
568 hash ^= u64::from(byte);
569 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
570 }
571 hash as i64
572}
573
574async fn import_unlocked(db: &Database) -> Result<Vec<String>, Error> {
576 ensure_table(db).await?;
577 Ok(Vec::new())
578}
579
580fn has_timestamp(name: &str) -> bool {
582 let bytes = name.as_bytes();
583 bytes.len() > 15 && bytes[..14].iter().all(u8::is_ascii_digit) && bytes[14] == b'_'
584}
585
586fn timestamp(name: &str) -> &str {
588 &name[..14]
589}
590
591fn prepared(driver: Driver, sql: &str, values: Vec<Value>) -> (String, Vec<Value>) {
593 driver.prepare(sql, &values)
594}
595
596#[cfg_attr(
598 not(all(feature = "mysql", feature = "postgres", feature = "sqlite")),
599 allow(unused_variables)
600)]
601fn pick<'a>(driver: Driver, mysql: &'a str, postgres: &'a str, sqlite: &'a str) -> &'a str {
602 match driver {
603 #[cfg(feature = "mysql")]
604 Driver::MySql => mysql,
605 #[cfg(feature = "postgres")]
606 Driver::Postgres => postgres,
607 #[cfg(feature = "sqlite")]
608 Driver::Sqlite => sqlite,
609 }
610}
611
612async fn ensure_table(db: &Database) -> Result<(), Error> {
617 let driver = db.driver();
618 let create = pick(
619 driver,
620 "CREATE TABLE IF NOT EXISTS __diesel_schema_migrations (\
621 version VARCHAR(50) NOT NULL PRIMARY KEY, \
622 run_on TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, \
623 batch INT NOT NULL DEFAULT 0)",
624 "CREATE TABLE IF NOT EXISTS __diesel_schema_migrations (\
625 version VARCHAR(50) NOT NULL PRIMARY KEY, \
626 run_on TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, \
627 batch INT NOT NULL DEFAULT 0)",
628 "CREATE TABLE IF NOT EXISTS __diesel_schema_migrations (\
629 version TEXT NOT NULL PRIMARY KEY, \
630 run_on TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, \
631 batch INTEGER NOT NULL DEFAULT 0)",
632 );
633 backend::execute(Target::Pool(&db.pool), create, &[]).await?;
634 if has_batch_column(db).await? {
635 return Ok(());
636 }
637 let added = backend::execute(
638 Target::Pool(&db.pool),
639 "ALTER TABLE __diesel_schema_migrations ADD COLUMN batch INT NOT NULL DEFAULT 0",
640 &[],
641 )
642 .await;
643 match added {
644 Ok(_) => Ok(()),
645 Err(err) if is_duplicate_column(&err) => Ok(()),
647 Err(err) => Err(err.into()),
648 }
649}
650
651async fn has_batch_column(db: &Database) -> Result<bool, Error> {
653 let sql = pick(
654 db.driver(),
655 "SELECT COUNT(*) FROM information_schema.columns \
656 WHERE table_schema = DATABASE() AND table_name = '__diesel_schema_migrations' \
657 AND column_name = 'batch'",
658 "SELECT COUNT(*) FROM information_schema.columns \
659 WHERE table_schema = current_schema() AND table_name = '__diesel_schema_migrations' \
660 AND column_name = 'batch'",
661 "SELECT COUNT(*) FROM pragma_table_info('__diesel_schema_migrations') \
662 WHERE name = 'batch'",
663 );
664 let found: i64 = backend::scalar(Target::Pool(&db.pool), sql, &[]).await?;
665 Ok(found > 0)
666}
667
668fn is_duplicate_column(err: &sqlx::Error) -> bool {
671 err.as_database_error().is_some_and(|db_err| {
672 matches!(db_err.code().as_deref(), Some("1060" | "42701"))
673 || db_err.message().contains("duplicate column name")
674 })
675}
676
677async fn applied(db: &Database) -> Result<HashMap<String, i64>, Error> {
679 let rows: Vec<(String, i32)> = backend::fetch_all(
680 Target::Pool(&db.pool),
681 &format!("SELECT version, batch FROM {TABLE}"),
682 &[],
683 )
684 .await?;
685 Ok(rows
686 .into_iter()
687 .map(|(version, batch)| (version, i64::from(batch)))
688 .collect())
689}
690
691async fn recorded_versions(db: &Database) -> Result<Option<HashSet<String>>, Error> {
694 let sql = pick(
695 db.driver(),
696 "SELECT COUNT(*) FROM information_schema.tables \
697 WHERE table_schema = DATABASE() AND table_name = '__diesel_schema_migrations'",
698 "SELECT COUNT(*) FROM information_schema.tables \
699 WHERE table_schema = current_schema() AND table_name = '__diesel_schema_migrations'",
700 "SELECT COUNT(*) FROM sqlite_master \
701 WHERE type = 'table' AND name = '__diesel_schema_migrations'",
702 );
703 let found: i64 = backend::scalar(Target::Pool(&db.pool), sql, &[]).await?;
704 if found == 0 {
705 return Ok(None);
706 }
707 let versions: Vec<(String,)> = backend::fetch_all(
708 Target::Pool(&db.pool),
709 &format!("SELECT version FROM {TABLE}"),
710 &[],
711 )
712 .await?;
713 Ok(Some(versions.into_iter().map(|(v,)| v).collect()))
714}
715
716pub(crate) fn read_file(path: &Path) -> Result<String, Error> {
718 std::fs::read_to_string(path).map_err(|source| Error::Read {
719 path: path.to_path_buf(),
720 source,
721 })
722}