use std::collections::HashSet;
use std::path::Path;
use crate::core::{inventory, ModelEntry, ModelSchema};
use crate::sql::sqlx;
#[cfg(feature = "postgres")]
use crate::sql::sqlx::Row;
#[cfg(feature = "postgres")]
use crate::sql::sqlx::PgPool;
use super::file::{self, Migration, Operation};
use super::invert::invert;
use super::progress::{emit, MigrationEvent, MigrationObserver, Outcome};
use super::snapshot::SchemaSnapshot;
use super::{ddl, MigrateError};
pub const LEDGER_TABLE: &str = "__rustango_migrations__";
pub const SYSTEM_LEDGER_TABLE: &str = "__rustango_system_migrations__";
#[derive(Debug, Clone, Copy)]
pub struct Builder {
ledger: &'static str,
}
impl Default for Builder {
fn default() -> Self {
Self {
ledger: LEDGER_TABLE,
}
}
}
impl Builder {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn ledger(mut self, name: &'static str) -> Self {
validate_ledger_name(name);
self.ledger = name;
self
}
#[must_use]
pub fn ledger_name(&self) -> &'static str {
self.ledger
}
#[cfg(feature = "postgres")]
pub async fn migrate(&self, pool: &PgPool, dir: &Path) -> Result<Vec<Migration>, MigrateError> {
migrate_with_ledger(pool, dir, self.ledger, None).await
}
#[cfg(feature = "postgres")]
pub async fn migrate_to(
&self,
pool: &PgPool,
dir: &Path,
target: &str,
) -> Result<Vec<Migration>, MigrateError> {
migrate_to_with_ledger(pool, dir, target, self.ledger).await
}
#[cfg(feature = "postgres")]
pub async fn migrate_embedded(
&self,
pool: &PgPool,
embedded: &[(&str, &str)],
) -> Result<Vec<Migration>, MigrateError> {
migrate_embedded_with_ledger(pool, embedded, self.ledger).await
}
#[cfg(feature = "postgres")]
pub async fn migrate_dry_run(
&self,
pool: &PgPool,
dir: &Path,
) -> Result<Vec<MigrationPreview>, MigrateError> {
migrate_dry_run_with_ledger(pool, dir, self.ledger).await
}
#[cfg(feature = "postgres")]
pub async fn downgrade(
&self,
pool: &PgPool,
dir: &Path,
steps: usize,
) -> Result<Vec<Migration>, MigrateError> {
downgrade_with_ledger(pool, dir, steps, self.ledger).await
}
#[cfg(feature = "postgres")]
pub async fn unapply(
&self,
pool: &PgPool,
dir: &Path,
name: &str,
) -> Result<Migration, MigrateError> {
unapply_with_ledger(pool, dir, name, self.ledger).await
}
#[cfg(feature = "postgres")]
pub async fn unapply_force(
&self,
pool: &PgPool,
dir: &Path,
name: &str,
) -> Result<Migration, MigrateError> {
unapply_force_with_ledger(pool, dir, name, self.ledger).await
}
#[cfg(feature = "postgres")]
pub async fn applied_set(&self, pool: &PgPool) -> Result<HashSet<String>, MigrateError> {
applied_set_for(pool, self.ledger).await
}
#[cfg(feature = "postgres")]
pub async fn ensure_ledger(&self, pool: &PgPool) -> Result<(), MigrateError> {
ensure_ledger_for(pool, self.ledger).await
}
}
fn validate_ledger_name(name: &str) {
let bytes = name.as_bytes();
let valid = !bytes.is_empty()
&& bytes.len() <= 63
&& (bytes[0].is_ascii_alphabetic() || bytes[0] == b'_')
&& bytes
.iter()
.all(|b| b.is_ascii_alphanumeric() || *b == b'_');
assert!(
valid,
"Builder::ledger({name:?}) is not a valid SQL identifier — \
must match [A-Za-z_][A-Za-z0-9_]* and be ≤ 63 bytes"
);
}
#[cfg(any(feature = "postgres", feature = "mysql"))]
const MIGRATE_LOCK_KEY: i64 = 0x5255_5354_4d49_4754;
#[must_use]
pub fn registered_models() -> Vec<&'static ModelSchema> {
inventory::iter::<ModelEntry>
.into_iter()
.map(|e| e.schema)
.collect()
}
fn bootstrap_models() -> Vec<&'static ModelSchema> {
registered_models()
.into_iter()
.filter(|m| m.managed)
.collect()
}
#[cfg(feature = "postgres")]
pub async fn apply_all(pool: &PgPool) -> Result<(), MigrateError> {
#[cfg(feature = "signals")]
use crate::signals::migrate::{
send_post_migrate, send_pre_migrate, PostMigrateContext, PreMigrateContext,
};
#[cfg(feature = "signals")]
send_pre_migrate(PreMigrateContext {
source: "apply_all",
})
.await;
let models = bootstrap_models();
for model in &models {
let sql = ddl::create_table_sql(model);
sqlx::query(&sql).execute(pool).await?;
}
for model in &models {
for sql in ddl::create_constraints_sql(model) {
sqlx::query(&sql).execute(pool).await?;
}
}
#[cfg(feature = "signals")]
send_post_migrate(PostMigrateContext {
source: "apply_all",
applied: Vec::new(),
})
.await;
Ok(())
}
#[cfg(feature = "postgres")]
pub async fn drop_all(pool: &PgPool) -> Result<(), MigrateError> {
for model in bootstrap_models() {
let sql = ddl::drop_table_sql(model, true, true);
sqlx::query(&sql).execute(pool).await?;
}
Ok(())
}
pub async fn apply_all_pool(pool: &crate::sql::Pool) -> Result<(), MigrateError> {
#[cfg(feature = "signals")]
use crate::signals::migrate::{
send_post_migrate, send_pre_migrate, PostMigrateContext, PreMigrateContext,
};
#[cfg(feature = "signals")]
send_pre_migrate(PreMigrateContext {
source: "apply_all_pool",
})
.await;
let dialect = pool.dialect();
let models = bootstrap_models();
for model in &models {
let sql = ddl::create_table_sql_with_dialect(dialect, model);
crate::sql::raw_execute_pool(pool, &sql, ::std::vec::Vec::new()).await?;
}
for model in &models {
for sql in ddl::create_constraints_sql_with_dialect(dialect, model) {
crate::sql::raw_execute_pool(pool, &sql, ::std::vec::Vec::new()).await?;
}
}
for model in &models {
for sql in ddl::column_comment_statements_with_dialect(dialect, model) {
crate::sql::raw_execute_pool(pool, &sql, ::std::vec::Vec::new()).await?;
}
}
for model in &models {
for sql in ddl::table_comment_statements_with_dialect(dialect, model) {
crate::sql::raw_execute_pool(pool, &sql, ::std::vec::Vec::new()).await?;
}
}
#[cfg(feature = "signals")]
send_post_migrate(PostMigrateContext {
source: "apply_all_pool",
applied: Vec::new(),
})
.await;
Ok(())
}
pub async fn drop_all_pool(pool: &crate::sql::Pool) -> Result<(), MigrateError> {
let dialect = pool.dialect();
let cascade = dialect.name() == "postgres";
let models = bootstrap_models();
for model in &models {
for sql in ddl::drop_constraints_sql_with_dialect(dialect, model) {
let _ = crate::sql::raw_execute_pool(pool, &sql, ::std::vec::Vec::new()).await;
}
}
for model in &models {
let sql =
ddl::drop_table_sql_with_dialect(dialect, model, true, cascade);
crate::sql::raw_execute_pool(pool, &sql, ::std::vec::Vec::new()).await?;
}
Ok(())
}
#[cfg(feature = "postgres")]
pub async fn migrate(pool: &PgPool, dir: &Path) -> Result<Vec<Migration>, MigrateError> {
Builder::default().migrate(pool, dir).await
}
#[cfg(feature = "postgres")]
pub async fn migrate_with_progress(
pool: &PgPool,
dir: &Path,
observer: &dyn MigrationObserver,
) -> Result<Vec<Migration>, MigrateError> {
migrate_with_ledger(pool, dir, LEDGER_TABLE, Some(observer)).await
}
#[cfg(feature = "postgres")]
async fn migrate_with_ledger(
pool: &PgPool,
dir: &Path,
ledger: &str,
observer: Option<&dyn MigrationObserver>,
) -> Result<Vec<Migration>, MigrateError> {
with_migrate_signals(
async {
ensure_ledger_for(pool, ledger).await?;
with_migrate_lock(pool, migrate_with_ledger_body(pool, dir, ledger, observer)).await
},
|newly| newly.iter().map(|m| m.name.clone()).collect(),
)
.await
}
#[cfg(feature = "postgres")]
#[cfg_attr(not(any(feature = "manage", feature = "tenancy")), allow(dead_code))]
pub(crate) async fn migrate_locked(
_: LockHeld,
pool: &PgPool,
dir: &Path,
observer: Option<&dyn MigrationObserver>,
) -> Result<Vec<Migration>, MigrateError> {
let enum_pool = crate::sql::Pool::Postgres(pool.clone());
create_ledger_locked(&enum_pool, LEDGER_TABLE).await?;
migrate_with_ledger_body(pool, dir, LEDGER_TABLE, observer).await
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum Signals {
#[cfg_attr(
not(all(feature = "postgres", any(feature = "manage", feature = "tenancy"))),
allow(dead_code)
)]
Fire,
Skip,
}
pub(crate) async fn with_migrate_signals<T>(
run: impl std::future::Future<Output = Result<T, MigrateError>>,
applied: impl FnOnce(&T) -> Vec<String>,
) -> Result<T, MigrateError> {
#[cfg(feature = "signals")]
use crate::signals::migrate::{
send_post_migrate, send_pre_migrate, PostMigrateContext, PreMigrateContext,
};
#[cfg(feature = "signals")]
send_pre_migrate(PreMigrateContext { source: "migrate" }).await;
let out = run.await?;
#[cfg(feature = "signals")]
send_post_migrate(PostMigrateContext {
source: "migrate",
applied: applied(&out),
})
.await;
#[cfg(not(feature = "signals"))]
let _ = applied;
Ok(out)
}
#[cfg(feature = "postgres")]
async fn migrate_with_ledger_body(
pool: &PgPool,
dir: &Path,
ledger: &str,
observer: Option<&dyn MigrationObserver>,
) -> Result<Vec<Migration>, MigrateError> {
let all = file::list_dir(dir)?;
let applied = applied_set_for(pool, ledger).await?;
let pending = pending_migrations(&all, &applied);
let total = pending.len();
emit(observer, || MigrationEvent::Planned { total });
let mut newly = Vec::with_capacity(total);
let enum_pool = crate::sql::Pool::Postgres(pool.clone());
for (i, mig) in pending.into_iter().enumerate() {
let index = i + 1;
emit(observer, || MigrationEvent::Started {
name: mig.name.clone(),
index,
total,
});
let began = std::time::Instant::now();
let step = async {
Ok::<_, MigrateError>(match reconcile(&enum_pool, &mig, &applied, false).await? {
ReconcileAction::Fake => {
fake_apply_pool(&enum_pool, &mig, ledger).await?;
Outcome::Faked
}
ReconcileAction::Run | ReconcileAction::RunPartial(_) => {
apply_one(pool, &mig, ledger).await?;
Outcome::Ran
}
ReconcileAction::RunOutside(existing) => {
apply_one(pool, &outside_tables(&mig, &existing), ledger).await?;
Outcome::RanPartial { skipped: existing }
}
})
}
.await;
let outcome = match step {
Ok(outcome) => outcome,
Err(e) => {
emit(observer, || MigrationEvent::Failed {
name: mig.name.clone(),
index,
total,
error: e.to_string(),
});
return Err(e);
}
};
emit(observer, || MigrationEvent::Finished {
name: mig.name.clone(),
index,
total,
outcome,
elapsed: began.elapsed(),
});
newly.push(mig);
}
Ok(newly)
}
#[cfg(feature = "postgres")]
async fn with_migrate_lock<F, R>(pool: &PgPool, body: F) -> Result<R, MigrateError>
where
F: std::future::Future<Output = Result<R, MigrateError>>,
{
hold_migrate_lock(&crate::sql::Pool::Postgres(pool.clone()), body).await
}
#[cfg(feature = "postgres")]
async fn pg_creation_schema(pool: &PgPool) -> Result<Option<String>, MigrateError> {
let pool = crate::sql::Pool::Postgres(pool.clone());
Ok(super::ensure::creation_schema(&pool).await?)
}
#[cfg(feature = "postgres")]
pub async fn applied_set(pool: &PgPool) -> Result<HashSet<String>, MigrateError> {
applied_set_for(pool, LEDGER_TABLE).await
}
#[cfg(feature = "postgres")]
async fn applied_set_for(pool: &PgPool, ledger: &str) -> Result<HashSet<String>, MigrateError> {
let rows = sqlx::query(&format!("SELECT name FROM {ledger}"))
.fetch_all(pool)
.await?;
let mut out = HashSet::with_capacity(rows.len());
for row in rows {
out.insert(row.try_get::<String, _>("name")?);
}
Ok(out)
}
#[cfg(feature = "postgres")]
pub async fn ensure_ledger(pool: &PgPool) -> Result<(), MigrateError> {
ensure_ledger_for(pool, LEDGER_TABLE).await
}
#[cfg(feature = "postgres")]
async fn ensure_ledger_for(pool: &PgPool, ledger: &str) -> Result<(), MigrateError> {
use crate::sql::{Dialect as _, Postgres};
let dialect = Postgres;
let mut tx = pool.begin().await?;
if let Some(xact_lock_sql) = dialect.acquire_xact_lock_sql() {
sqlx::query(&xact_lock_sql)
.bind(MIGRATE_LOCK_KEY)
.execute(&mut *tx)
.await?;
}
let create_sql = format!(
"CREATE TABLE IF NOT EXISTS {ledger} (\
name TEXT PRIMARY KEY, \
applied_at {})",
dialect.timestamp_now_column()
);
sqlx::query(&create_sql).execute(&mut *tx).await?;
tx.commit().await?;
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MigrationPreview {
pub name: String,
pub atomic: bool,
pub statements: Vec<String>,
}
#[cfg(feature = "postgres")]
pub async fn migrate_dry_run(
pool: &PgPool,
dir: &Path,
) -> Result<Vec<MigrationPreview>, MigrateError> {
Builder::default().migrate_dry_run(pool, dir).await
}
#[cfg(feature = "postgres")]
async fn migrate_dry_run_with_ledger(
pool: &PgPool,
dir: &Path,
ledger: &str,
) -> Result<Vec<MigrationPreview>, MigrateError> {
ensure_ledger_for(pool, ledger).await?;
let all = file::list_dir(dir)?;
let applied = applied_set_for(pool, ledger).await?;
let pending = all.iter().filter(|m| !applied.contains(&m.name));
let mut out = Vec::new();
for mig in pending {
let before = prev_snapshot(&all, mig, dir)?;
out.push(preview_migration(
mig,
&before,
&crate::sql::Postgres,
ledger,
)?);
}
Ok(out)
}
pub fn sqlmigrate_one(
dir: &Path,
name: &str,
dialect: &dyn crate::sql::Dialect,
) -> Result<MigrationPreview, MigrateError> {
let all = file::list_dir(dir)?;
let mig = all.iter().find(|m| m.name == name).ok_or_else(|| {
MigrateError::Validation(format!("migration `{name}` not found in {}", dir.display()))
})?;
let before = prev_snapshot(&all, mig, dir)?;
preview_migration(mig, &before, dialect, LEDGER_TABLE)
}
async fn invoke_migration_callback(
op: &crate::migrate::file::CallbackOp,
pool: crate::sql::Pool,
) -> Result<(), MigrateError> {
let cb = crate::migrate::callbacks::find(&op.name).ok_or_else(|| {
MigrateError::Validation(format!(
"migration callback `{}` is not registered — \
call `rustango::register_migration_callback!(\"{0}\", …)` \
at startup",
op.name,
))
})?;
(cb.forward)(pool).await
}
fn callback_in_atomic(mig: &str, op: &crate::migrate::file::CallbackOp) -> MigrateError {
MigrateError::Validation(format!(
"{mig}: callback `{}` needs `\"atomic\": false` (#1626)",
op.name,
))
}
fn prev_snapshot(
all: &[Migration],
mig: &Migration,
dir: &Path,
) -> Result<SchemaSnapshot, MigrateError> {
let Some(prev_name) = &mig.prev else {
return Ok(SchemaSnapshot::default());
};
all.iter()
.find(|m| &m.name == prev_name)
.map(|m| m.snapshot.clone())
.ok_or_else(|| {
MigrateError::Validation(format!(
"migration `{}` declares prev=`{prev_name}` but that file is missing in {}",
mig.name,
dir.display()
))
})
}
fn preview_migration(
mig: &Migration,
before: &SchemaSnapshot,
dialect: &dyn crate::sql::Dialect,
ledger: &str,
) -> Result<MigrationPreview, MigrateError> {
let mut statements = Vec::new();
let mut deferred_fks: Vec<String> = Vec::new();
if mig.atomic {
statements.push("BEGIN".to_string());
}
for op in &mig.forward {
match op {
Operation::Schema(change) => {
let step = render_step(change, &mig.forward, &mig.snapshot, dialect, None)?;
if let (Some(fk), Some(_)) = (&step.drop_fks, dialect.foreign_key_names_sql()) {
let has_fk = before
.table(&fk.table)
.and_then(|t| t.field(&fk.column))
.is_some_and(|f| f.fk.is_some());
if has_fk {
let name = ddl::fk_constraint_name(&fk.table, &fk.column);
statements.extend(dialect.drop_foreign_key_sql(&fk.table, &name));
}
}
if let (Some(u), Some(_)) = (&step.drop_unique, dialect.unique_index_names_sql()) {
let name = ddl::unique_constraint_name(&u.table, &u.column);
statements.extend(dialect.drop_unique_index_sql(&u.table, &name));
}
statements.extend(step.batch.immediate);
if let Some(rebuild) = &step.batch.rebuild {
statements.extend(rebuild.statements(dialect));
statements.push(format!(
"-- re-create the indexes and triggers of {}",
rebuild.table()
));
}
deferred_fks.extend(step.batch.deferred_fks);
}
Operation::Data(d) => {
statements.push(d.sql.clone());
}
Operation::Callback(c) => {
statements.push(format!("-- RunPython: {}", c.name));
}
}
}
statements.extend(deferred_fks);
statements.push(format!(
"INSERT INTO {ledger} (name, applied_at) VALUES ('{}', <now>)",
mig.name.replace('\'', "''")
));
if mig.atomic {
statements.push("COMMIT".to_string());
}
Ok(MigrationPreview {
name: mig.name.clone(),
atomic: mig.atomic,
statements,
})
}
#[cfg(feature = "postgres")]
async fn apply_atomic(pool: &PgPool, mig: &Migration, ledger: &str) -> Result<(), MigrateError> {
tracing::info!(migration = %mig.name, "applying (atomic)");
let schema = pg_creation_schema(pool).await?;
let mut tx = pool.begin().await?;
let mut deferred_fks: Vec<String> = Vec::new();
for op in &mig.forward {
match op {
Operation::Schema(change) => {
let step = render_step(
change,
&mig.forward,
&mig.snapshot,
&crate::sql::Postgres,
schema.as_deref(),
)?;
for stmt in pg_statements(&mut tx, &step).await? {
sqlx::query(&stmt).execute(&mut *tx).await?;
}
deferred_fks.extend(step.batch.deferred_fks);
}
Operation::Data(d) => {
sqlx::query(&d.sql).execute(&mut *tx).await?;
}
Operation::Callback(c) => return Err(callback_in_atomic(&mig.name, c)),
}
}
for stmt in deferred_fks {
sqlx::query(&stmt).execute(&mut *tx).await?;
}
sqlx::query(&ledger_insert_sql(&crate::sql::Postgres, ledger))
.bind(&mig.name)
.bind(chrono::Utc::now())
.execute(&mut *tx)
.await?;
tx.commit().await?;
Ok(())
}
#[cfg(feature = "postgres")]
pub async fn migrate_to(
pool: &PgPool,
dir: &Path,
target: &str,
) -> Result<Vec<Migration>, MigrateError> {
Builder::default().migrate_to(pool, dir, target).await
}
#[cfg(feature = "postgres")]
async fn migrate_to_with_ledger(
pool: &PgPool,
dir: &Path,
target: &str,
ledger: &str,
) -> Result<Vec<Migration>, MigrateError> {
ensure_ledger_for(pool, ledger).await?;
with_migrate_lock(pool, async {
let all = file::list_dir(dir)?;
let applied = applied_set_for(pool, ledger).await?;
if target == "zero" {
return unapply_all_in_order(pool, dir, &all, &applied, ledger).await;
}
if !all.iter().any(|m| m.name == target) {
return Err(MigrateError::Validation(format!(
"target migration `{target}` not found in {}",
dir.display()
)));
}
let head = all
.iter()
.rev()
.find(|m| applied.contains(&m.name))
.map(|m| m.name.clone());
let mut touched = Vec::new();
match head {
None => {
for mig in all.into_iter().filter(|m| m.name.as_str() <= target) {
apply_one(pool, &mig, ledger).await?;
touched.push(mig);
}
}
Some(h) => {
use std::cmp::Ordering;
match target.cmp(h.as_str()) {
Ordering::Equal => {}
Ordering::Greater => {
for mig in all.into_iter().filter(|m| {
m.name.as_str() > h.as_str()
&& m.name.as_str() <= target
&& !applied.contains(&m.name)
}) {
apply_one(pool, &mig, ledger).await?;
touched.push(mig);
}
}
Ordering::Less => {
let mut to_unapply: Vec<Migration> = all
.into_iter()
.filter(|m| {
m.name.as_str() > target
&& m.name.as_str() <= h.as_str()
&& applied.contains(&m.name)
})
.collect();
to_unapply.reverse();
for mig in to_unapply {
unapply_locked(pool, dir, &mig.name, ledger).await?;
touched.push(mig);
}
}
}
}
}
Ok(touched)
})
.await
}
#[cfg(feature = "postgres")]
pub async fn migrate_embedded(
pool: &PgPool,
embedded: &[(&str, &str)],
) -> Result<Vec<Migration>, MigrateError> {
Builder::default().migrate_embedded(pool, embedded).await
}
#[cfg(feature = "postgres")]
async fn migrate_embedded_with_ledger(
pool: &PgPool,
embedded: &[(&str, &str)],
ledger: &str,
) -> Result<Vec<Migration>, MigrateError> {
ensure_ledger_for(pool, ledger).await?;
with_migrate_lock(pool, async {
let mut all: Vec<Migration> = Vec::with_capacity(embedded.len());
for (name, json) in embedded {
let mig = file::parse(json)?;
if mig.name != *name {
return Err(MigrateError::Validation(format!(
"embedded entry key `{name}` doesn't match migration `name` field `{}`",
mig.name,
)));
}
all.push(mig);
}
all.sort_by(|a, b| a.name.cmp(&b.name));
file::validate_chain(&all, "embedded slice")?;
let applied = applied_set_for(pool, ledger).await?;
let pending: Vec<Migration> = all
.into_iter()
.filter(|m| !applied.contains(&m.name))
.collect();
let mut newly = Vec::with_capacity(pending.len());
for mig in pending {
apply_one(pool, &mig, ledger).await?;
newly.push(mig);
}
Ok(newly)
})
.await
}
#[cfg(feature = "postgres")]
pub async fn downgrade(
pool: &PgPool,
dir: &Path,
steps: usize,
) -> Result<Vec<Migration>, MigrateError> {
Builder::default().downgrade(pool, dir, steps).await
}
#[cfg(feature = "postgres")]
async fn downgrade_with_ledger(
pool: &PgPool,
dir: &Path,
steps: usize,
ledger: &str,
) -> Result<Vec<Migration>, MigrateError> {
if steps == 0 {
return Ok(Vec::new());
}
ensure_ledger_for(pool, ledger).await?;
with_migrate_lock(pool, async {
let all = file::list_dir(dir)?;
let applied = applied_set_for(pool, ledger).await?;
let applied_in_order: Vec<Migration> = all
.into_iter()
.filter(|m| applied.contains(&m.name))
.collect();
if applied_in_order.is_empty() {
return Ok(Vec::new());
}
let n = steps.min(applied_in_order.len());
let to_unapply: Vec<Migration> = applied_in_order.into_iter().rev().take(n).collect();
let mut touched = Vec::with_capacity(to_unapply.len());
for mig in to_unapply {
unapply_locked(pool, dir, &mig.name, ledger).await?;
touched.push(mig);
}
Ok(touched)
})
.await
}
#[cfg(feature = "postgres")]
async fn apply_one(pool: &PgPool, mig: &Migration, ledger: &str) -> Result<(), MigrateError> {
if mig.atomic {
apply_atomic(pool, mig, ledger).await
} else {
apply_loose(pool, mig, ledger).await
}
}
#[cfg(feature = "postgres")]
async fn unapply_all_in_order(
pool: &PgPool,
dir: &Path,
all: &[Migration],
applied: &HashSet<String>,
ledger: &str,
) -> Result<Vec<Migration>, MigrateError> {
let mut to_unapply: Vec<Migration> = all
.iter()
.filter(|m| applied.contains(&m.name))
.cloned()
.collect();
to_unapply.reverse();
let mut touched = Vec::with_capacity(to_unapply.len());
for mig in to_unapply {
unapply_locked(pool, dir, &mig.name, ledger).await?;
touched.push(mig);
}
Ok(touched)
}
#[cfg(feature = "postgres")]
pub async fn unapply(pool: &PgPool, dir: &Path, name: &str) -> Result<Migration, MigrateError> {
Builder::default().unapply(pool, dir, name).await
}
#[cfg(feature = "postgres")]
async fn unapply_with_ledger(
pool: &PgPool,
dir: &Path,
name: &str,
ledger: &str,
) -> Result<Migration, MigrateError> {
ensure_ledger_for(pool, ledger).await?;
with_migrate_lock(pool, async {
check_is_head(pool, dir, name, ledger).await?;
unapply_locked(pool, dir, name, ledger).await
})
.await
}
#[cfg(feature = "postgres")]
pub async fn unapply_force(
pool: &PgPool,
dir: &Path,
name: &str,
) -> Result<Migration, MigrateError> {
Builder::default().unapply_force(pool, dir, name).await
}
#[cfg(feature = "postgres")]
async fn unapply_force_with_ledger(
pool: &PgPool,
dir: &Path,
name: &str,
ledger: &str,
) -> Result<Migration, MigrateError> {
ensure_ledger_for(pool, ledger).await?;
with_migrate_lock(pool, unapply_locked(pool, dir, name, ledger)).await
}
#[cfg(feature = "postgres")]
async fn check_is_head(
pool: &PgPool,
dir: &Path,
name: &str,
ledger: &str,
) -> Result<(), MigrateError> {
let applied = applied_set_for(pool, ledger).await?;
if !applied.contains(name) {
return Ok(());
}
let all = file::list_dir(dir)?;
let head = all
.iter()
.rev()
.find(|m| applied.contains(&m.name))
.map(|m| m.name.as_str());
match head {
Some(h) if h == name => Ok(()),
Some(h) => Err(MigrateError::Validation(format!(
"refusing to unapply `{name}` out of order: current head is `{h}`. \
Use `downgrade(pool, dir, n)` / `migrate_to(pool, dir, target)` for \
ordered rollback, or `unapply_force` to bypass.",
))),
None => Ok(()),
}
}
#[cfg(feature = "postgres")]
async fn unapply_locked(
pool: &PgPool,
dir: &Path,
name: &str,
ledger: &str,
) -> Result<Migration, MigrateError> {
let all = file::list_dir(dir)?;
let target = all
.iter()
.find(|m| m.name == name)
.cloned()
.ok_or_else(|| {
MigrateError::Validation(format!("migration `{name}` not found in {}", dir.display()))
})?;
let prev_snapshot = prev_snapshot(&all, &target, dir)?;
let inverted = invert(&target.forward, &prev_snapshot)?;
if target.atomic {
unapply_atomic(pool, &target, &inverted, &prev_snapshot, ledger).await?;
} else {
unapply_loose(pool, &target, &inverted, &prev_snapshot, ledger).await?;
}
Ok(target)
}
#[cfg(feature = "postgres")]
async fn unapply_atomic(
pool: &PgPool,
target: &Migration,
inverted: &[Operation],
snapshot: &SchemaSnapshot,
ledger: &str,
) -> Result<(), MigrateError> {
tracing::info!(migration = %target.name, "unapplying (atomic)");
let schema = pg_creation_schema(pool).await?;
let mut tx = pool.begin().await?;
let mut deferred_fks: Vec<String> = Vec::new();
for op in inverted {
match op {
Operation::Schema(change) => {
let step = render_step(
change,
inverted,
snapshot,
&crate::sql::Postgres,
schema.as_deref(),
)?;
for stmt in pg_statements(&mut tx, &step).await? {
sqlx::query(&stmt).execute(&mut *tx).await?;
}
deferred_fks.extend(step.batch.deferred_fks);
}
Operation::Data(d) => {
sqlx::query(&d.sql).execute(&mut *tx).await?;
}
Operation::Callback(c) => return Err(callback_in_atomic(&target.name, c)),
}
}
for stmt in deferred_fks {
sqlx::query(&stmt).execute(&mut *tx).await?;
}
sqlx::query(&format!("DELETE FROM {ledger} WHERE name = $1"))
.bind(&target.name)
.execute(&mut *tx)
.await?;
tx.commit().await?;
Ok(())
}
#[cfg(feature = "postgres")]
async fn unapply_loose(
pool: &PgPool,
target: &Migration,
inverted: &[Operation],
snapshot: &SchemaSnapshot,
ledger: &str,
) -> Result<(), MigrateError> {
tracing::info!(migration = %target.name, "unapplying (non-atomic)");
let schema = pg_creation_schema(pool).await?;
let mut deferred_fks: Vec<String> = Vec::new();
for op in inverted {
match op {
Operation::Schema(change) => {
let step = render_step(
change,
inverted,
snapshot,
&crate::sql::Postgres,
schema.as_deref(),
)?;
let mut conn = pool.acquire().await?;
for stmt in pg_statements(&mut conn, &step).await? {
sqlx::query(&stmt).execute(&mut *conn).await?;
}
deferred_fks.extend(step.batch.deferred_fks);
}
Operation::Data(d) => {
sqlx::query(&d.sql).execute(pool).await?;
}
Operation::Callback(c) => {
invoke_migration_callback(c, pool.clone().into()).await?;
}
}
}
for stmt in deferred_fks {
sqlx::query(&stmt).execute(pool).await?;
}
sqlx::query(&format!("DELETE FROM {ledger} WHERE name = $1"))
.bind(&target.name)
.execute(pool)
.await?;
Ok(())
}
#[cfg(feature = "postgres")]
async fn apply_loose(pool: &PgPool, mig: &Migration, ledger: &str) -> Result<(), MigrateError> {
tracing::info!(migration = %mig.name, "applying (non-atomic)");
let schema = pg_creation_schema(pool).await?;
let mut deferred_fks: Vec<String> = Vec::new();
for op in &mig.forward {
match op {
Operation::Schema(change) => {
let step = render_step(
change,
&mig.forward,
&mig.snapshot,
&crate::sql::Postgres,
schema.as_deref(),
)?;
let mut conn = pool.acquire().await?;
for stmt in pg_statements(&mut conn, &step).await? {
sqlx::query(&stmt).execute(&mut *conn).await?;
}
deferred_fks.extend(step.batch.deferred_fks);
}
Operation::Data(d) => {
sqlx::query(&d.sql).execute(pool).await?;
}
Operation::Callback(c) => {
invoke_migration_callback(c, pool.clone().into()).await?;
}
}
}
for stmt in deferred_fks {
sqlx::query(&stmt).execute(pool).await?;
}
sqlx::query(&ledger_insert_sql(&crate::sql::Postgres, ledger))
.bind(&mig.name)
.bind(chrono::Utc::now())
.execute(pool)
.await?;
Ok(())
}
pub async fn ensure_ledger_pool(pool: &crate::sql::Pool) -> Result<(), MigrateError> {
ensure_ledger_pool_with_ledger(pool, LEDGER_TABLE).await
}
pub async fn ensure_ledger_pool_with_ledger(
pool: &crate::sql::Pool,
ledger: &str,
) -> Result<(), MigrateError> {
with_migrate_lock_pool(pool, ledger, async { Ok(()) }).await
}
async fn create_ledger_locked(pool: &crate::sql::Pool, ledger: &str) -> Result<(), MigrateError> {
let timestamp_col = pool.dialect().timestamp_now_column();
let create_sql = format!(
"CREATE TABLE IF NOT EXISTS {ledger} (\
name VARCHAR(255) PRIMARY KEY, \
applied_at {timestamp_col})"
);
crate::sql::raw_execute_pool(pool, &create_sql, ::std::vec::Vec::new()).await?;
Ok(())
}
pub(crate) async fn ledger_names(
_: LockHeld,
pool: &crate::sql::Pool,
ledger: &str,
) -> Result<HashSet<String>, MigrateError> {
create_ledger_locked(pool, ledger).await?;
applied_set_pool_with_ledger(pool, ledger).await
}
fn ledger_insert_sql(dialect: &dyn crate::sql::Dialect, ledger: &str) -> String {
let (p1, p2) = (dialect.placeholder(1), dialect.placeholder(2));
format!("INSERT INTO {ledger} (name, applied_at) VALUES ({p1}, {p2})")
}
pub async fn applied_set_pool(pool: &crate::sql::Pool) -> Result<HashSet<String>, MigrateError> {
applied_set_pool_with_ledger(pool, LEDGER_TABLE).await
}
pub async fn applied_set_pool_with_ledger(
pool: &crate::sql::Pool,
ledger: &str,
) -> Result<HashSet<String>, MigrateError> {
let sql = format!("SELECT name FROM {ledger}");
let rows: Vec<(String,)> = crate::sql::raw_query_pool(&sql, Vec::new(), pool).await?;
Ok(rows.into_iter().map(|(name,)| name).collect())
}
pub async fn migrate_pool(
pool: &crate::sql::Pool,
dir: &Path,
) -> Result<Vec<Migration>, MigrateError> {
migrate_pool_with_ledger(pool, dir, LEDGER_TABLE).await
}
pub async fn migrate_pool_with_progress(
pool: &crate::sql::Pool,
dir: &Path,
observer: &dyn MigrationObserver,
) -> Result<Vec<Migration>, MigrateError> {
migrate_pool_with_ledger_opts(
pool,
dir,
LEDGER_TABLE,
false,
ChainOrigin::OnDisk,
Some(observer),
)
.await
}
pub async fn migrate_pool_with_ledger_fake_initial_with_progress(
pool: &crate::sql::Pool,
dir: &Path,
ledger: &str,
observer: &dyn MigrationObserver,
) -> Result<Vec<Migration>, MigrateError> {
migrate_pool_with_ledger_opts(pool, dir, ledger, true, ChainOrigin::OnDisk, Some(observer))
.await
}
pub async fn migrate_pool_with_ledger(
pool: &crate::sql::Pool,
dir: &Path,
ledger: &str,
) -> Result<Vec<Migration>, MigrateError> {
migrate_pool_with_ledger_opts(pool, dir, ledger, false, ChainOrigin::OnDisk, None).await
}
pub async fn migrate_pool_with_ledger_fake_initial(
pool: &crate::sql::Pool,
dir: &Path,
ledger: &str,
) -> Result<Vec<Migration>, MigrateError> {
migrate_pool_with_ledger_opts(pool, dir, ledger, true, ChainOrigin::OnDisk, None).await
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ChainOrigin {
OnDisk,
Regenerated,
}
pub(crate) async fn migrate_system_chain(
_: LockHeld,
pool: &crate::sql::Pool,
chain: &super::make::SystemChain,
run_dir: &Path,
observer: Option<&dyn MigrationObserver>,
) -> Result<Vec<Migration>, MigrateError> {
let origin = chain.origin();
create_ledger_locked(pool, SYSTEM_LEDGER_TABLE).await?;
migrate_pool_body(pool, run_dir, SYSTEM_LEDGER_TABLE, true, origin, observer).await
}
pub(crate) async fn migrate_pool_locked(
_: LockHeld,
pool: &crate::sql::Pool,
dir: &Path,
observer: Option<&dyn MigrationObserver>,
) -> Result<Vec<Migration>, MigrateError> {
create_ledger_locked(pool, LEDGER_TABLE).await?;
migrate_pool_body(
pool,
dir,
LEDGER_TABLE,
false,
ChainOrigin::OnDisk,
observer,
)
.await
}
async fn migrate_pool_with_ledger_opts(
pool: &crate::sql::Pool,
dir: &Path,
ledger: &str,
fake_initial: bool,
origin: ChainOrigin,
observer: Option<&dyn MigrationObserver>,
) -> Result<Vec<Migration>, MigrateError> {
with_migrate_lock_pool(
pool,
ledger,
migrate_pool_body(pool, dir, ledger, fake_initial, origin, observer),
)
.await
}
async fn migrate_pool_body(
pool: &crate::sql::Pool,
dir: &Path,
ledger: &str,
fake_initial: bool,
origin: ChainOrigin,
observer: Option<&dyn MigrationObserver>,
) -> Result<Vec<Migration>, MigrateError> {
let all = file::list_dir(dir)?;
let mut applied = applied_set_pool_with_ledger(pool, ledger).await?;
if origin == ChainOrigin::Regenerated && !applied.is_empty() {
let mut unfixed = Vec::new();
for mig in &all {
let failed = converge_regenerated(pool, mig).await?;
if failed.is_empty() {
if applied.insert(mig.name.clone()) {
fake_apply_pool(pool, mig, ledger).await?;
}
} else {
unfixed.extend(failed);
}
}
if !unfixed.is_empty() {
return Err(MigrateError::Validation(format!(
"the framework schema is missing objects `migrate` cannot add; \
add them by hand, then rerun `migrate`:\n - {}",
unfixed.join("\n - ")
)));
}
}
let pending = pending_migrations(&all, &applied);
let total = pending.len();
emit(observer, || MigrationEvent::Planned { total });
let mut newly = Vec::with_capacity(total);
for (i, mig) in pending.into_iter().enumerate() {
let index = i + 1;
emit(observer, || MigrationEvent::Started {
name: mig.name.clone(),
index,
total,
});
let began = std::time::Instant::now();
let outcome = reconcile_and_apply(pool, &mig, &applied, ledger, fake_initial).await;
let outcome = match outcome {
Ok(outcome) => outcome,
Err(e) => {
emit(observer, || MigrationEvent::Failed {
name: mig.name.clone(),
index,
total,
error: e.to_string(),
});
return Err(e);
}
};
emit(observer, || MigrationEvent::Finished {
name: mig.name.clone(),
index,
total,
outcome,
elapsed: began.elapsed(),
});
newly.push(mig);
}
Ok(newly)
}
async fn reconcile_and_apply(
pool: &crate::sql::Pool,
mig: &Migration,
applied: &HashSet<String>,
ledger: &str,
fake_initial: bool,
) -> Result<Outcome, MigrateError> {
match reconcile(pool, mig, applied, fake_initial).await? {
ReconcileAction::Fake => {
fake_apply_pool(pool, mig, ledger).await?;
Ok(Outcome::Faked)
}
other => {
let (effective, outcome) = match other {
ReconcileAction::RunPartial(existing) => (
std::borrow::Cow::Owned(without_tables(mig, &existing)),
Outcome::RanPartial { skipped: existing },
),
ReconcileAction::RunOutside(existing) => (
std::borrow::Cow::Owned(outside_tables(mig, &existing)),
Outcome::RanPartial { skipped: existing },
),
_ => (std::borrow::Cow::Borrowed(mig), Outcome::Ran),
};
apply_one_pool(pool, &effective, ledger).await?;
Ok(outcome)
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum ReconcileAction {
Run,
Fake,
RunPartial(Vec<String>),
RunOutside(Vec<String>),
}
async fn reconcile(
pool: &crate::sql::Pool,
mig: &Migration,
applied: &HashSet<String>,
fake_initial: bool,
) -> Result<ReconcileAction, MigrateError> {
if !mig.replaces.is_empty() {
let present = mig.replaces.iter().filter(|n| applied.contains(*n)).count();
if present == mig.replaces.len() {
tracing::info!(
migration = %mig.name,
replaces = ?mig.replaces,
"reconciling squash — every replaced migration is already applied; \
recording it and tombstoning them without running DDL"
);
return Ok(ReconcileAction::Fake);
}
if present > 0 {
let missing: Vec<&str> = mig
.replaces
.iter()
.filter(|n| !applied.contains(*n))
.map(String::as_str)
.collect();
return Err(MigrateError::Validation(format!(
"cannot reconcile squash `{}`: it replaces {} migration(s) but only {} are \
recorded as applied (missing: {}). The database is in a partial state — \
resolve it by hand (see `migrate --fake <name>`) rather than guessing.",
mig.name,
mig.replaces.len(),
present,
missing.join(", "),
)));
}
let targets = create_table_targets(mig);
if targets.is_empty() {
return Ok(ReconcileAction::Run);
}
let existing = count_existing_tables(pool, &targets).await;
if existing == targets.len() {
let outside: Vec<&super::SchemaChange> = mig
.forward
.iter()
.filter_map(|op| match op {
Operation::Schema(c) if !targets.iter().any(|t| t == c.table()) => Some(c),
_ => None,
})
.collect();
let (present, absent) = applied_counts(pool, &outside).await?;
if present > 0 && absent > 0 {
return Err(MigrateError::Validation(format!(
"cannot reconcile squash `{}`: its tables exist and {present} of its other \
changes are applied, {absent} are not. The database is in a partial \
state — resolve it by hand (see `migrate --fake <name>`).",
mig.name
)));
}
if absent > 0 {
if mig
.forward
.iter()
.any(|op| !matches!(op, Operation::Schema(_)))
{
return Err(MigrateError::Validation(format!(
"cannot reconcile squash `{}`: its tables exist but its other changes \
do not, and whether its data operations ran is unknown. Resolve it \
by hand (see `migrate --fake <name>`).",
mig.name
)));
}
tracing::info!(
migration = %mig.name,
tables = %targets.join(", "),
"reconciling squash — its tables already exist (cross-ledger); \
running only its changes to other tables"
);
return Ok(ReconcileAction::RunOutside(targets));
}
tracing::info!(
migration = %mig.name,
tables = %targets.join(", "),
"reconciling squash — its tables already exist (cross-ledger \
fake-initial); recording it without running DDL"
);
return Ok(ReconcileAction::Fake);
}
if existing > 0 {
return Err(MigrateError::Validation(format!(
"cannot reconcile squash `{}`: {} of its {} tables already exist but the rest \
do not. The database is in a partial state — resolve it by hand (see \
`migrate --fake <name>`) rather than guessing.",
mig.name,
existing,
targets.len(),
)));
}
return Ok(ReconcileAction::Run);
}
if fake_initial {
if let Some(tables) = create_only_tables(mig) {
let existing: Vec<String> = {
let mut present = Vec::new();
for t in &tables {
if count_existing_tables(pool, std::slice::from_ref(t)).await == 1 {
present.push(t.clone());
}
}
present
};
if existing.len() == tables.len() {
tracing::warn!(
migration = %mig.name,
tables = %tables.join(", "),
"fake-initial: tables already exist (pre-migration \
`ensure_table` era) — recorded as applied without \
running its CREATE TABLE; existing data left intact"
);
return Ok(ReconcileAction::Fake);
}
if !existing.is_empty() {
tracing::warn!(
migration = %mig.name,
existing = %existing.join(", "),
"fake-initial: some framework tables already exist — creating \
only the missing ones and leaving the existing untouched"
);
return Ok(ReconcileAction::RunPartial(existing));
}
}
}
Ok(ReconcileAction::Run)
}
pub(crate) fn without_tables(mig: &Migration, existing: &[String]) -> Migration {
use super::diff::SchemaChange as SC;
let skip = |t: &String| existing.iter().any(|e| e == t);
let forward = mig
.forward
.iter()
.filter(|op| match op {
Operation::Schema(SC::CreateTable(t)) => !skip(t),
Operation::Schema(SC::CreateIndex { table, .. }) => !skip(table),
Operation::Schema(SC::CreateM2MTable { through, .. }) => !skip(through),
_ => true,
})
.cloned()
.collect();
Migration {
forward,
..mig.clone()
}
}
async fn applied_counts(
pool: &crate::sql::Pool,
changes: &[&super::SchemaChange],
) -> Result<(usize, usize), MigrateError> {
use super::SchemaChange as SC;
let schema = super::ensure::creation_schema(pool)
.await?
.unwrap_or_default();
let (mut present, mut absent) = (0, 0);
for change in changes {
let applied = match change {
SC::CreateTable(t) | SC::CreateM2MTable { through: t, .. } => {
Some(table_exists_here(pool, t).await)
}
SC::DropTable(t) | SC::DropM2MTable { through: t } => {
Some(!table_exists_here(pool, t).await)
}
SC::AddColumn { table, column } => Some(
super::ensure::live_columns(pool, table)
.await?
.contains(column),
),
SC::DropColumn { table, column } => Some(
!super::ensure::live_columns(pool, table)
.await?
.contains(column),
),
SC::CreateIndex { name, table, .. } => Some(
!super::inspectdb::index_columns(pool, &schema, table, name)
.await?
.is_empty(),
),
SC::DropIndex { name, table } => Some(
super::inspectdb::index_columns(pool, &schema, table, name)
.await?
.is_empty(),
),
_ => None,
};
match applied {
Some(true) => present += 1,
Some(false) => absent += 1,
None => {}
}
}
Ok((present, absent))
}
fn outside_tables(mig: &Migration, existing: &[String]) -> Migration {
let forward = mig
.forward
.iter()
.filter(|op| match op {
Operation::Schema(c) => !existing.iter().any(|e| e == c.table()),
_ => false,
})
.cloned()
.collect();
Migration {
forward,
..mig.clone()
}
}
fn pending_migrations(all: &[Migration], applied: &HashSet<String>) -> Vec<Migration> {
let superseded: HashSet<&str> = all
.iter()
.filter(|m| applied.contains(&m.name))
.flat_map(|m| m.replaces.iter().map(String::as_str))
.collect();
all.iter()
.filter(|m| !applied.contains(&m.name) && !superseded.contains(m.name.as_str()))
.cloned()
.collect()
}
fn create_table_targets(mig: &Migration) -> Vec<String> {
use super::diff::SchemaChange as SC;
mig.forward
.iter()
.filter_map(|op| match op {
Operation::Schema(SC::CreateTable(t) | SC::CreateM2MTable { through: t, .. }) => {
Some(t.clone())
}
_ => None,
})
.collect()
}
fn create_only_tables(mig: &Migration) -> Option<Vec<String>> {
use super::diff::SchemaChange as SC;
let mut tables: Vec<String> = Vec::new();
let mut targeted: Vec<&str> = Vec::new();
for op in &mig.forward {
match op {
Operation::Schema(SC::CreateTable(t)) => tables.push(t.clone()),
Operation::Schema(SC::CreateIndex { table, .. }) => targeted.push(table.as_str()),
Operation::Schema(SC::CreateM2MTable { through, .. }) => tables.push(through.clone()),
_ => return None,
}
}
if tables.is_empty() {
return None;
}
if targeted
.iter()
.any(|t| !tables.iter().any(|created| created == t))
{
return None;
}
Some(tables)
}
async fn converge_regenerated(
pool: &crate::sql::Pool,
mig: &Migration,
) -> Result<Vec<String>, MigrateError> {
use super::diff::SchemaChange as SC;
let mut groups: Vec<Vec<SC>> = Vec::new();
for op in &mig.forward {
let Operation::Schema(change) = op else {
continue;
};
let table = match change {
SC::CreateTable(t) => t,
SC::CreateM2MTable { through, .. } => through,
_ => continue,
};
if !table_exists_here(pool, table).await {
let indexes = mig.forward.iter().filter_map(|op| match op {
Operation::Schema(c @ SC::CreateIndex { table: t, .. }) if t == table => {
Some(c.clone())
}
_ => None,
});
groups.push(std::iter::once(change.clone()).chain(indexes).collect());
continue;
}
let (SC::CreateTable(_), Some(snap)) = (change, mig.snapshot.table(table)) else {
continue;
};
let live = super::ensure::live_columns(pool, table).await?;
groups.extend(
snap.fields
.iter()
.filter(|f| !live.contains(&f.column))
.map(|f| {
vec![SC::AddColumn {
table: table.clone(),
column: f.column.clone(),
}]
}),
);
groups.extend(
mig.snapshot
.indexes
.iter()
.filter(|i| &i.table == table)
.map(|i| vec![super::diff::create_index(i)]),
);
for column in live.iter().filter(|c| snap.field(c).is_none()) {
tracing::warn!(
target: "rustango::migrate",
"`{table}.{column}` is not in the framework schema; drop it by hand \
if it is NOT NULL with no default, or inserts fail"
);
}
}
let failed = super::ensure::converge_groups(pool, &mig.snapshot, &groups).await?;
warn_on_index_clash(pool, mig).await?;
Ok(failed)
}
async fn warn_on_index_clash(pool: &crate::sql::Pool, mig: &Migration) -> Result<(), MigrateError> {
let schema = super::ensure::creation_schema(pool)
.await?
.unwrap_or_default();
for idx in &mig.snapshot.indexes {
let live = super::inspectdb::index_columns(pool, &schema, &idx.table, &idx.name).await?;
let Some((table, _)) = live.first() else {
continue;
};
let columns: Vec<&str> = live.iter().map(|(_, c)| c.as_str()).collect();
if table != &idx.table || columns != idx.columns {
tracing::warn!(
target: "rustango::migrate",
"index `{}` is on `{table}` ({}), not `{}` ({}); drop or rename it by hand",
idx.name,
columns.join(", "),
idx.table,
idx.columns.join(", "),
);
}
}
Ok(())
}
async fn count_existing_tables(pool: &crate::sql::Pool, tables: &[String]) -> usize {
let mut n = 0;
for t in tables {
if table_exists_here(pool, t).await {
n += 1;
}
}
n
}
pub(crate) async fn table_exists_here(pool: &crate::sql::Pool, table: &str) -> bool {
match pool {
#[cfg(feature = "postgres")]
crate::sql::Pool::Postgres(pg) => sqlx::query_scalar::<_, i64>(
"SELECT COUNT(*) FROM information_schema.tables \
WHERE table_schema = current_schema() AND table_name = $1",
)
.bind(table)
.fetch_one(pg)
.await
.map(|n| n > 0)
.unwrap_or(false),
#[cfg(feature = "mysql")]
crate::sql::Pool::Mysql(my) => sqlx::query_scalar::<_, i64>(
"SELECT COUNT(*) FROM information_schema.tables \
WHERE table_schema = DATABASE() AND table_name = ?",
)
.bind(table)
.fetch_one(my)
.await
.map(|n| n > 0)
.unwrap_or(false),
#[cfg(feature = "sqlite")]
crate::sql::Pool::Sqlite(sq) => sqlx::query_scalar::<_, i64>(
"SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = ?",
)
.bind(table)
.fetch_one(sq)
.await
.map(|n| n > 0)
.unwrap_or(false),
}
}
async fn fake_apply_pool(
pool: &crate::sql::Pool,
mig: &Migration,
ledger: &str,
) -> Result<(), MigrateError> {
let (insert, delete) = {
let dialect = pool.dialect();
let placeholder = dialect.placeholder(1);
let table = dialect.quote_ident(ledger);
let name_col = dialect.quote_ident("name");
let conflict_tail = dialect.insert_on_conflict_skip(&[&name_col]);
(
format!("INSERT INTO {table} ({name_col}) VALUES ({placeholder}) {conflict_tail}"),
format!("DELETE FROM {table} WHERE {name_col} = {placeholder}"),
)
};
crate::sql::raw_execute_pool(
pool,
&insert,
vec![crate::core::SqlValue::String(mig.name.clone())],
)
.await
.map_err(|e| {
MigrateError::Validation(format!(
"fake-apply: ledger insert for `{}` failed: {e}",
mig.name
))
})?;
if mig.replaces.is_empty() {
return Ok(());
}
for replaced in &mig.replaces {
crate::sql::raw_execute_pool(
pool,
&delete,
vec![crate::core::SqlValue::String(replaced.clone())],
)
.await
.map_err(|e| {
MigrateError::Validation(format!(
"fake-apply: tombstoning `{replaced}` (replaced by `{}`) failed: {e}",
mig.name
))
})?;
}
Ok(())
}
async fn with_migrate_lock_pool<F, R>(
pool: &crate::sql::Pool,
ledger: &str,
body: F,
) -> Result<R, MigrateError>
where
F: std::future::Future<Output = Result<R, MigrateError>>,
{
hold_migrate_lock(pool, async {
create_ledger_locked(pool, ledger).await?;
body.await
})
.await
}
#[derive(Clone, Copy)]
pub(crate) struct LockHeld(());
pub(crate) async fn with_migrate_lock_held<F, Fut, R>(
pool: &crate::sql::Pool,
body: F,
) -> Result<R, MigrateError>
where
F: FnOnce(LockHeld) -> Fut,
Fut: std::future::Future<Output = Result<R, MigrateError>>,
{
hold_migrate_lock(pool, body(LockHeld(()))).await
}
tokio::task_local! {
static LOCK_HELD: ();
static LOCK_TIMEOUT: std::time::Duration;
}
pub async fn with_lock_timeout<F: std::future::Future>(
timeout: std::time::Duration,
fut: F,
) -> F::Output {
LOCK_TIMEOUT.scope(timeout, fut).await
}
#[cfg_attr(not(feature = "mysql"), allow(dead_code))]
fn mysql_lock_taken(got: Option<i64>) -> Result<bool, sqlx::Error> {
got.map(|n| n == 1).ok_or_else(|| {
sqlx::Error::Protocol("GET_LOCK returned NULL: the server could not take the lock".into())
})
}
fn refuse_nested_lock() -> Result<(), MigrateError> {
if LOCK_HELD.try_with(|()| ()).is_ok() {
return Err(MigrateError::Validation(
"migrate called while this task holds the migrate lock; \
run it after the outer migrate returns"
.into(),
));
}
Ok(())
}
async fn hold_migrate_lock<F, R>(pool: &crate::sql::Pool, body: F) -> Result<R, MigrateError>
where
F: std::future::Future<Output = Result<R, MigrateError>>,
{
refuse_nested_lock()?;
let body = LOCK_HELD.scope((), body);
let dialect = pool.dialect();
let (Some(acquire), Some(release)) = (
dialect.acquire_session_lock_sql(),
dialect.release_session_lock_sql(),
) else {
return body.await;
};
match pool {
#[cfg(feature = "postgres")]
crate::sql::Pool::Postgres(pg) => {
let mut held = LockSession::wait(pg, |conn| {
let sql = acquire.clone();
Box::pin(async move {
sqlx::query_scalar::<_, bool>(&sql)
.bind(MIGRATE_LOCK_KEY)
.fetch_one(&mut **conn)
.await
})
})
.await?;
let result = body.await;
let _ = sqlx::query(&release)
.bind(MIGRATE_LOCK_KEY)
.execute(&mut **held.conn())
.await;
result
}
#[cfg(feature = "mysql")]
crate::sql::Pool::Mysql(my) => {
let name = format!("rustango_migrate_{:x}", MIGRATE_LOCK_KEY);
let mut held = LockSession::wait(my, |conn| {
let (sql, name) = (acquire.clone(), name.clone());
Box::pin(async move {
let got: Option<i64> = sqlx::query_scalar(&sql)
.bind(name)
.fetch_one(&mut **conn)
.await?;
mysql_lock_taken(got)
})
})
.await?;
let result = body.await;
let _ = sqlx::query(&release)
.bind(&name)
.execute(&mut **held.conn())
.await;
result
}
#[allow(unreachable_patterns)]
_ => {
let _ = (acquire, release);
body.await
}
}
}
#[cfg(any(feature = "postgres", feature = "mysql"))]
struct LockSession<DB: sqlx::Database>(Option<sqlx::pool::PoolConnection<DB>>);
#[cfg(any(feature = "postgres", feature = "mysql"))]
type TryLock<'c> =
std::pin::Pin<Box<dyn std::future::Future<Output = Result<bool, sqlx::Error>> + Send + 'c>>;
#[cfg(any(feature = "postgres", feature = "mysql"))]
impl<DB: sqlx::Database> LockSession<DB> {
async fn wait<F>(pool: &sqlx::Pool<DB>, try_lock: F) -> Result<Self, MigrateError>
where
F: for<'c> Fn(&'c mut sqlx::pool::PoolConnection<DB>) -> TryLock<'c>,
{
use std::hash::{BuildHasher as _, Hasher as _};
use std::time::{Duration, Instant};
let limit = LOCK_TIMEOUT.try_with(|d| *d).ok();
let start = Instant::now();
let mut pause = Duration::from_millis(10);
let mut logged = false;
loop {
let mut session = Self(Some(pool.acquire().await?));
if try_lock(session.conn()).await? {
return Ok(session);
}
drop(session.0.take());
if !std::mem::replace(&mut logged, true) {
tracing::info!(target: "rustango::migrate", "waiting for another run's migrate lock");
}
let rand = std::collections::hash_map::RandomState::new()
.build_hasher()
.finish();
let sleep = pause / 2 + pause.mul_f64((rand % 1000) as f64 / 2000.0);
if let Some(limit) = limit.filter(|l| start.elapsed() + sleep > *l) {
return Err(MigrateError::LockTimeout(limit));
}
tokio::time::sleep(sleep).await;
pause = (pause * 2).min(Duration::from_millis(500));
}
}
fn conn(&mut self) -> &mut sqlx::pool::PoolConnection<DB> {
self.0.as_mut().expect("lock session connection")
}
}
#[cfg(any(feature = "postgres", feature = "mysql"))]
impl<DB: sqlx::Database> Drop for LockSession<DB> {
fn drop(&mut self) {
if let Some(conn) = self.0.as_mut() {
conn.close_on_drop();
}
}
}
struct Step {
batch: super::diff::RenderedBatch,
#[cfg_attr(not(feature = "sqlite"), allow(dead_code))]
retry: Option<super::ensure::FilledTableRetry>,
#[cfg_attr(not(any(feature = "mysql", feature = "postgres")), allow(dead_code))]
drop_fks: Option<ColumnRef>,
#[cfg_attr(not(feature = "mysql"), allow(dead_code))]
drop_unique: Option<ColumnRef>,
}
#[cfg_attr(not(any(feature = "mysql", feature = "postgres")), allow(dead_code))]
struct ColumnRef {
table: String,
column: String,
keep: Vec<String>,
}
fn render_step(
change: &super::SchemaChange,
ops: &[Operation],
after: &SchemaSnapshot,
dialect: &dyn crate::sql::Dialect,
schema: Option<&str>,
) -> Result<Step, MigrateError> {
let changes = std::slice::from_ref(change);
let render = |snap: &SchemaSnapshot| {
super::diff::render_changes_split_in_schema(changes, snap, dialect, schema)
};
let retry = super::ensure::filled_table_retry(dialect, after, changes, render)
.transpose()
.map_err(MigrateError::Validation)?;
use super::SchemaChange as SC;
let at_column = |table: &str, column: &str| ColumnRef {
table: table.to_owned(),
column: column.to_owned(),
keep: super::diff::declared_indexes(after, table),
};
let has_fk = |table: &str, column: &str| {
after
.table(table)
.and_then(|t| t.field(column))
.is_some_and(|f| f.fk.is_some())
};
let drop_fks = match change {
SC::DropColumn { table, column } | SC::AlterFkOnDelete { table, column, .. } => {
Some(at_column(table, column))
}
SC::AlterColumnType { table, column, .. }
| SC::AlterColumnMaxLength { table, column, .. }
| SC::AlterColumnUnique {
table,
column,
unique: false,
} if dialect.modifies_whole_column() && has_fk(table, column) => {
Some(at_column(table, column))
}
_ => None,
};
let drop_unique = match change {
SC::AlterColumnUnique {
table,
column,
unique: false,
} => Some(at_column(table, column)),
_ => None,
};
let rebuilt = match change {
SC::DropColumn { table, .. }
| SC::AlterFkOnDelete { table, .. }
| SC::AlterColumnType { table, .. }
| SC::AlterColumnNullable { table, .. }
| SC::AlterColumnDefault { table, .. }
| SC::AlterColumnMaxLength { table, .. }
| SC::AlterColumnUnique { table, .. }
| SC::AddCheckConstraint { table, .. }
| SC::DropCheckConstraint { table, .. }
| SC::AddCompositeFk { table, .. }
| SC::DropCompositeFk { table, .. } => Some(table.as_str()),
_ => None,
};
let later = ops
.iter()
.position(|op| matches!(op, Operation::Schema(c) if std::ptr::eq(c, change)))
.map_or(&[][..], |i| &ops[i + 1..]);
let at = match rebuilt.filter(|_| dialect.alters_by_rebuild()) {
Some(table) if later.iter().any(|op| touches_table(op, table)) => {
let mut at = after.clone();
let shape =
super::rebuild::shape_at(table, later, after).map_err(MigrateError::Validation)?;
at.tables.retain(|t| t.name != table);
at.tables.push(shape);
at.checks.retain(|c| {
!later.iter().any(|op| {
matches!(op, Operation::Schema(SC::AddCheckConstraint { name, .. }) if *name == c.name)
})
});
Some(at)
}
_ => None,
};
let mut batch = render(at.as_ref().unwrap_or(after)).map_err(MigrateError::Validation)?;
batch.rebuild = batch.rebuild.map(|r| {
let dropped: Vec<&str> = later
.iter()
.filter_map(|op| match op {
Operation::Schema(super::SchemaChange::DropColumn { table, column })
if table == r.table() =>
{
Some(column.as_str())
}
_ => None,
})
.collect();
dropped.into_iter().fold(r, |r, c| r.dropping(c))
});
Ok(Step {
batch,
retry,
drop_fks,
drop_unique,
})
}
fn touches_table(op: &Operation, table: &str) -> bool {
matches!(op, Operation::Schema(c) if c.table() == table)
}
#[cfg(feature = "sqlite")]
fn rebuilds(ops: &[Operation], after: &SchemaSnapshot) -> Result<bool, MigrateError> {
for op in ops {
if let Operation::Schema(change) = op {
if render_step(change, ops, after, &crate::sql::Sqlite, None)?
.batch
.rebuild
.is_some()
{
return Ok(true);
}
}
}
Ok(false)
}
#[cfg(feature = "sqlite")]
async fn run_step_sqlite(
tx: &mut super::rebuild::RebuildTx<'_>,
step: Step,
) -> Result<Vec<String>, MigrateError> {
let Step { batch, retry, .. } = step;
for (i, stmt) in batch.immediate.iter().enumerate() {
let Err(e) = sqlx::query(stmt).execute(&mut **tx).await else {
continue;
};
let Some(retry) = retry.filter(|_| i == 0 && super::ensure::is_non_constant_default(&e))
else {
return Err(e.into());
};
tracing::warn!(target: "rustango::migrate", "column {}", retry.warning);
for stmt in &retry.batch.immediate {
sqlx::query(stmt).execute(&mut **tx).await?;
}
return Ok(retry.batch.deferred_fks);
}
if let Some(rebuild) = &batch.rebuild {
rebuild.run(tx).await?;
}
Ok(batch.deferred_fks)
}
#[cfg(any(feature = "mysql", feature = "postgres"))]
macro_rules! step_statements {
($conn:expr, $step:expr, $dialect:expr) => {{
use crate::sql::Dialect as _;
let (step, dialect): (&Step, _) = ($step, $dialect);
let mut out = Vec::new();
if let (Some(fk), Some(sql)) = (&step.drop_fks, dialect.foreign_key_names_sql()) {
let names: Vec<String> = sqlx::query_scalar(sql)
.bind(&fk.table)
.bind(&fk.column)
.fetch_all(&mut *$conn)
.await?;
out.extend(
names
.iter()
.filter_map(|n| dialect.drop_foreign_key_sql(&fk.table, n)),
);
}
if let (Some(u), Some(sql)) = (&step.drop_unique, dialect.unique_index_names_sql()) {
let names: Vec<String> = sqlx::query_scalar(sql)
.bind(&u.table)
.bind(&u.column)
.fetch_all(&mut *$conn)
.await?;
out.extend(
names
.iter()
.filter(|n| !u.keep.contains(n))
.filter_map(|n| dialect.drop_unique_index_sql(&u.table, n)),
);
}
out.extend(step.batch.immediate.iter().cloned());
Ok(out)
}};
}
#[cfg(feature = "mysql")]
async fn mysql_statements(
conn: &mut sqlx::MySqlConnection,
step: &Step,
) -> Result<Vec<String>, sqlx::Error> {
step_statements!(conn, step, crate::sql::MySql)
}
#[cfg(feature = "postgres")]
async fn pg_statements(
conn: &mut sqlx::PgConnection,
step: &Step,
) -> Result<Vec<String>, sqlx::Error> {
step_statements!(conn, step, crate::sql::Postgres)
}
#[cfg(feature = "sqlite")]
#[derive(Clone, Copy)]
enum LedgerWrite<'a> {
Insert(&'a str),
Delete(&'a str),
}
#[cfg(feature = "sqlite")]
async fn atomic_sqlite(
sq: &sqlx::SqlitePool,
name: &str,
ops: &[Operation],
after: &SchemaSnapshot,
ledger: LedgerWrite<'_>,
) -> Result<(), MigrateError> {
let rebuilds = rebuilds(ops, after)?;
if rebuilds && ops.iter().any(|op| matches!(op, Operation::Data(_))) {
return Err(MigrateError::Validation(format!(
"migration `{name}` rebuilds a SQLite table, which runs with FOREIGN KEY \
enforcement off; move its RunSQL to a migration of its own or set `atomic: false`"
)));
}
let mut conn = super::rebuild::RebuildConn::acquire(sq, rebuilds).await?;
let result: Result<(), MigrateError> = async {
let mut tx = conn.begin().await?;
let mut deferred_fks: Vec<String> = Vec::new();
for op in ops {
match op {
Operation::Schema(change) => {
let step = render_step(change, ops, after, &crate::sql::Sqlite, None)?;
deferred_fks.extend(run_step_sqlite(&mut tx, step).await?);
}
Operation::Data(d) => {
sqlx::query(&d.sql).execute(&mut *tx).await?;
}
Operation::Callback(c) => return Err(callback_in_atomic(name, c)),
}
}
for stmt in deferred_fks {
sqlx::query(&stmt).execute(&mut *tx).await?;
}
match ledger {
LedgerWrite::Insert(ledger) => {
sqlx::query(&ledger_insert_sql(&crate::sql::Sqlite, ledger))
.bind(name)
.bind(crate::sql::encode_datetime(chrono::Utc::now()))
.execute(&mut *tx)
.await?;
}
LedgerWrite::Delete(ledger) => {
sqlx::query(&format!("DELETE FROM {ledger} WHERE name = ?"))
.bind(name)
.execute(&mut *tx)
.await?;
}
}
tx.commit().await
}
.await;
let finished = conn.finish().await;
result?;
Ok(finished?)
}
async fn run_step_pool(pool: &crate::sql::Pool, step: Step) -> Result<Vec<String>, MigrateError> {
match pool {
#[cfg(feature = "sqlite")]
crate::sql::Pool::Sqlite(sq) => {
let mut conn =
super::rebuild::RebuildConn::acquire(sq, step.batch.rebuild.is_some()).await?;
let result: Result<Vec<String>, MigrateError> = async {
let mut tx = conn.begin().await?;
let deferred = run_step_sqlite(&mut tx, step).await?;
tx.commit().await?;
Ok(deferred)
}
.await;
let finished = conn.finish().await;
let deferred = result?;
finished?;
Ok(deferred)
}
#[cfg(feature = "mysql")]
crate::sql::Pool::Mysql(my) => {
let mut conn = my.acquire().await?;
for stmt in mysql_statements(&mut conn, &step).await? {
sqlx::query(&stmt).execute(&mut *conn).await?;
}
Ok(step.batch.deferred_fks)
}
#[cfg(feature = "postgres")]
crate::sql::Pool::Postgres(pg) => {
let mut conn = pg.acquire().await?;
for stmt in pg_statements(&mut conn, &step).await? {
sqlx::query(&stmt).execute(&mut *conn).await?;
}
Ok(step.batch.deferred_fks)
}
}
}
async fn apply_atomic_pool(
pool: &crate::sql::Pool,
mig: &Migration,
ledger: &str,
) -> Result<(), MigrateError> {
tracing::info!(migration = %mig.name, "applying (atomic, _pool)");
#[cfg_attr(
not(any(feature = "postgres", feature = "mysql")),
allow(unused_variables)
)]
let schema = super::ensure::creation_schema(pool).await?;
#[cfg_attr(
not(any(feature = "postgres", feature = "mysql")),
allow(unused_variables)
)]
let dialect = pool.dialect();
match pool {
#[cfg(feature = "postgres")]
crate::sql::Pool::Postgres(pg) => {
let mut tx = pg.begin().await?;
let mut deferred_fks: Vec<String> = Vec::new();
for op in &mig.forward {
match op {
Operation::Schema(change) => {
let step = render_step(
change,
&mig.forward,
&mig.snapshot,
dialect,
schema.as_deref(),
)?;
for stmt in pg_statements(&mut tx, &step).await? {
sqlx::query(&stmt).execute(&mut *tx).await?;
}
deferred_fks.extend(step.batch.deferred_fks);
}
Operation::Data(d) => {
sqlx::query(&d.sql).execute(&mut *tx).await?;
}
Operation::Callback(c) => return Err(callback_in_atomic(&mig.name, c)),
}
}
for stmt in deferred_fks {
sqlx::query(&stmt).execute(&mut *tx).await?;
}
sqlx::query(&ledger_insert_sql(&crate::sql::Postgres, ledger))
.bind(&mig.name)
.bind(chrono::Utc::now())
.execute(&mut *tx)
.await?;
tx.commit().await?;
}
#[cfg(feature = "mysql")]
crate::sql::Pool::Mysql(my) => {
tracing::warn!(
migration = %mig.name,
"MySQL silently auto-commits on every DDL statement (CREATE/ALTER/DROP/INDEX); \
the atomic-migration wrapper only protects RunSQL/RunPython operations \
between DDL ops. A failure mid-DDL leaves the migration partially applied \
and requires manual recovery. See migrate/runner.rs::apply_atomic_pool for \
details. Tracked in #559."
);
let mut tx = my.begin().await?;
let mut deferred_fks: Vec<String> = Vec::new();
let total = mig.forward.len();
let mut applied = 0usize;
let mut ddl_applied = 0usize;
macro_rules! stuck {
($e:expr) => {{
let e: sqlx::Error = $e;
if ddl_applied == 0 {
MigrateError::Driver(e)
} else {
MigrateError::PartiallyApplied {
migration: mig.name.clone(),
applied,
total,
ddl_applied,
source: Box::new(e),
}
}
}};
}
for op in &mig.forward {
match op {
Operation::Schema(change) => {
let step = render_step(
change,
&mig.forward,
&mig.snapshot,
dialect,
schema.as_deref(),
)?;
let stmts = mysql_statements(&mut tx, &step)
.await
.map_err(|e| stuck!(e))?;
for stmt in stmts {
sqlx::query(&stmt)
.execute(&mut *tx)
.await
.map_err(|e| stuck!(e))?;
ddl_applied += 1;
}
deferred_fks.extend(step.batch.deferred_fks);
}
Operation::Data(d) => {
sqlx::query(&d.sql)
.execute(&mut *tx)
.await
.map_err(|e| stuck!(e))?;
}
Operation::Callback(c) => return Err(callback_in_atomic(&mig.name, c)),
}
applied += 1;
}
for stmt in deferred_fks {
sqlx::query(&stmt)
.execute(&mut *tx)
.await
.map_err(|e| stuck!(e))?;
}
sqlx::query(&ledger_insert_sql(&crate::sql::MySql, ledger))
.bind(&mig.name)
.bind(chrono::Utc::now())
.execute(&mut *tx)
.await
.map_err(|e| stuck!(e))?;
tx.commit().await.map_err(|e| stuck!(e))?;
}
#[cfg(feature = "sqlite")]
crate::sql::Pool::Sqlite(sq) => {
atomic_sqlite(
sq,
&mig.name,
&mig.forward,
&mig.snapshot,
LedgerWrite::Insert(ledger),
)
.await?;
}
}
Ok(())
}
async fn apply_nonatomic_pool(
pool: &crate::sql::Pool,
mig: &Migration,
ledger: &str,
) -> Result<(), MigrateError> {
tracing::info!(migration = %mig.name, "applying (non-atomic, _pool)");
let schema = super::ensure::creation_schema(pool).await?;
let mut deferred_fks: Vec<String> = Vec::new();
for op in &mig.forward {
match op {
Operation::Schema(change) => {
let step = render_step(
change,
&mig.forward,
&mig.snapshot,
pool.dialect(),
schema.as_deref(),
)?;
deferred_fks.extend(run_step_pool(pool, step).await?);
}
Operation::Data(d) => {
crate::sql::raw_execute_pool(pool, &d.sql, ::std::vec::Vec::new()).await?;
}
Operation::Callback(c) => {
invoke_migration_callback(c, pool.clone()).await?;
}
}
}
for stmt in deferred_fks {
crate::sql::raw_execute_pool(pool, &stmt, ::std::vec::Vec::new()).await?;
}
let insert_sql = ledger_insert_sql(pool.dialect(), ledger);
crate::sql::raw_execute_pool(
pool,
&insert_sql,
::std::vec![
crate::core::SqlValue::String(mig.name.clone()),
crate::core::SqlValue::DateTime(chrono::Utc::now()),
],
)
.await?;
Ok(())
}
pub async fn migrate_to_pool(
pool: &crate::sql::Pool,
dir: &Path,
target: &str,
) -> Result<Vec<Migration>, MigrateError> {
migrate_to_pool_with_ledger(pool, dir, target, LEDGER_TABLE).await
}
pub async fn migrate_to_pool_with_ledger(
pool: &crate::sql::Pool,
dir: &Path,
target: &str,
ledger: &str,
) -> Result<Vec<Migration>, MigrateError> {
with_migrate_lock_pool(pool, ledger, async {
let all = file::list_dir(dir)?;
let applied = applied_set_pool_with_ledger(pool, ledger).await?;
if target == "zero" {
return unapply_all_in_order_pool(pool, dir, &all, &applied, ledger).await;
}
if !all.iter().any(|m| m.name == target) {
return Err(MigrateError::Validation(format!(
"target migration `{target}` not found in {}",
dir.display()
)));
}
let head = all
.iter()
.rev()
.find(|m| applied.contains(&m.name))
.map(|m| m.name.clone());
let mut touched = Vec::new();
match head {
None => {
for mig in all.iter().filter(|m| m.name.as_str() <= target) {
apply_one_pool(pool, mig, ledger).await?;
let mig = mig.clone();
touched.push(mig);
}
}
Some(h) => {
use std::cmp::Ordering;
match target.cmp(h.as_str()) {
Ordering::Equal => {}
Ordering::Greater => {
for mig in all.iter().filter(|m| {
m.name.as_str() > h.as_str()
&& m.name.as_str() <= target
&& !applied.contains(&m.name)
}) {
apply_one_pool(pool, mig, ledger).await?;
let mig = mig.clone();
touched.push(mig);
}
}
Ordering::Less => {
let mut to_unapply: Vec<Migration> = all
.into_iter()
.filter(|m| {
m.name.as_str() > target
&& m.name.as_str() <= h.as_str()
&& applied.contains(&m.name)
})
.collect();
to_unapply.reverse();
for mig in to_unapply {
unapply_locked_pool(pool, dir, &mig.name, ledger).await?;
touched.push(mig);
}
}
}
}
}
Ok(touched)
})
.await
}
pub async fn downgrade_pool(
pool: &crate::sql::Pool,
dir: &Path,
steps: usize,
) -> Result<Vec<Migration>, MigrateError> {
downgrade_pool_with_ledger(pool, dir, steps, LEDGER_TABLE).await
}
pub async fn downgrade_pool_with_ledger(
pool: &crate::sql::Pool,
dir: &Path,
steps: usize,
ledger: &str,
) -> Result<Vec<Migration>, MigrateError> {
if steps == 0 {
return Ok(Vec::new());
}
with_migrate_lock_pool(pool, ledger, async {
let all = file::list_dir(dir)?;
let applied = applied_set_pool_with_ledger(pool, ledger).await?;
let applied_in_order: Vec<Migration> = all
.into_iter()
.filter(|m| applied.contains(&m.name))
.collect();
if applied_in_order.is_empty() {
return Ok(Vec::new());
}
let n = steps.min(applied_in_order.len());
let to_unapply: Vec<Migration> = applied_in_order.into_iter().rev().take(n).collect();
let mut touched = Vec::with_capacity(to_unapply.len());
for mig in to_unapply {
unapply_locked_pool(pool, dir, &mig.name, ledger).await?;
touched.push(mig);
}
Ok(touched)
})
.await
}
pub async fn unapply_pool(
pool: &crate::sql::Pool,
dir: &Path,
name: &str,
) -> Result<Migration, MigrateError> {
unapply_pool_with_ledger(pool, dir, name, LEDGER_TABLE).await
}
pub async fn unapply_pool_with_ledger(
pool: &crate::sql::Pool,
dir: &Path,
name: &str,
ledger: &str,
) -> Result<Migration, MigrateError> {
with_migrate_lock_pool(pool, ledger, async {
check_is_head_pool(pool, dir, name, ledger).await?;
unapply_locked_pool(pool, dir, name, ledger).await
})
.await
}
pub async fn unapply_force_pool(
pool: &crate::sql::Pool,
dir: &Path,
name: &str,
) -> Result<Migration, MigrateError> {
unapply_force_pool_with_ledger(pool, dir, name, LEDGER_TABLE).await
}
async fn unapply_force_pool_with_ledger(
pool: &crate::sql::Pool,
dir: &Path,
name: &str,
ledger: &str,
) -> Result<Migration, MigrateError> {
with_migrate_lock_pool(pool, ledger, unapply_locked_pool(pool, dir, name, ledger)).await
}
pub async fn migrate_dry_run_pool(
pool: &crate::sql::Pool,
dir: &Path,
) -> Result<Vec<MigrationPreview>, MigrateError> {
migrate_dry_run_pool_with_ledger(pool, dir, LEDGER_TABLE).await
}
pub async fn migrate_dry_run_pool_with_ledger(
pool: &crate::sql::Pool,
dir: &Path,
ledger: &str,
) -> Result<Vec<MigrationPreview>, MigrateError> {
ensure_ledger_pool_with_ledger(pool, ledger).await?;
let all = file::list_dir(dir)?;
let applied = applied_set_pool_with_ledger(pool, ledger).await?;
let pending = all.iter().filter(|m| !applied.contains(&m.name));
let mut out = Vec::new();
for mig in pending {
let before = prev_snapshot(&all, mig, dir)?;
out.push(preview_migration(mig, &before, pool.dialect(), ledger)?);
}
Ok(out)
}
async fn apply_one_pool(
pool: &crate::sql::Pool,
mig: &Migration,
ledger: &str,
) -> Result<(), MigrateError> {
if mig.atomic {
apply_atomic_pool(pool, mig, ledger).await
} else {
apply_nonatomic_pool(pool, mig, ledger).await
}
}
async fn unapply_all_in_order_pool(
pool: &crate::sql::Pool,
dir: &Path,
all: &[Migration],
applied: &HashSet<String>,
ledger: &str,
) -> Result<Vec<Migration>, MigrateError> {
let mut to_unapply: Vec<Migration> = all
.iter()
.filter(|m| applied.contains(&m.name))
.cloned()
.collect();
to_unapply.reverse();
let mut touched = Vec::with_capacity(to_unapply.len());
for mig in to_unapply {
unapply_locked_pool(pool, dir, &mig.name, ledger).await?;
touched.push(mig);
}
Ok(touched)
}
async fn unapply_locked_pool(
pool: &crate::sql::Pool,
dir: &Path,
name: &str,
ledger: &str,
) -> Result<Migration, MigrateError> {
let all = file::list_dir(dir)?;
let target = all
.iter()
.find(|m| m.name == name)
.cloned()
.ok_or_else(|| {
MigrateError::Validation(format!("migration `{name}` not found in {}", dir.display()))
})?;
let prev_snapshot = prev_snapshot(&all, &target, dir)?;
let inverted = invert(&target.forward, &prev_snapshot)?;
if target.atomic {
unapply_atomic_pool(pool, &target, &inverted, &prev_snapshot, ledger).await?;
} else {
unapply_nonatomic_pool(pool, &target, &inverted, &prev_snapshot, ledger).await?;
}
Ok(target)
}
async fn check_is_head_pool(
pool: &crate::sql::Pool,
dir: &Path,
name: &str,
ledger: &str,
) -> Result<(), MigrateError> {
let applied = applied_set_pool_with_ledger(pool, ledger).await?;
if !applied.contains(name) {
return Ok(());
}
let all = file::list_dir(dir)?;
let head = all
.iter()
.rev()
.find(|m| applied.contains(&m.name))
.map(|m| m.name.as_str());
match head {
Some(h) if h == name => Ok(()),
Some(h) => Err(MigrateError::Validation(format!(
"refusing to unapply `{name}` out of order: current head is `{h}`. \
Use `downgrade_pool(pool, dir, n)` / `migrate_to_pool(pool, dir, target)` for \
ordered rollback, or `unapply_force_pool` to bypass.",
))),
None => Ok(()),
}
}
async fn unapply_atomic_pool(
pool: &crate::sql::Pool,
target: &Migration,
inverted: &[Operation],
snapshot: &SchemaSnapshot,
ledger: &str,
) -> Result<(), MigrateError> {
tracing::info!(migration = %target.name, "unapplying (atomic, _pool)");
#[cfg_attr(
not(any(feature = "postgres", feature = "mysql")),
allow(unused_variables)
)]
let schema = super::ensure::creation_schema(pool).await?;
match pool {
#[cfg(feature = "postgres")]
crate::sql::Pool::Postgres(pg) => {
let mut tx = pg.begin().await?;
let mut deferred_fks: Vec<String> = Vec::new();
for op in inverted {
match op {
Operation::Schema(change) => {
let step = render_step(
change,
inverted,
snapshot,
pool.dialect(),
schema.as_deref(),
)?;
for stmt in pg_statements(&mut tx, &step).await? {
sqlx::query(&stmt).execute(&mut *tx).await?;
}
deferred_fks.extend(step.batch.deferred_fks);
}
Operation::Data(d) => {
sqlx::query(&d.sql).execute(&mut *tx).await?;
}
Operation::Callback(c) => return Err(callback_in_atomic(&target.name, c)),
}
}
for stmt in deferred_fks {
sqlx::query(&stmt).execute(&mut *tx).await?;
}
sqlx::query(&format!("DELETE FROM {ledger} WHERE name = $1"))
.bind(&target.name)
.execute(&mut *tx)
.await?;
tx.commit().await?;
}
#[cfg(feature = "mysql")]
crate::sql::Pool::Mysql(my) => {
tracing::warn!(
migration = %target.name,
"MySQL silently auto-commits on every DDL statement; the atomic-unapply \
wrapper only protects RunSQL/RunPython between DDL ops. A failure mid-unapply \
leaves the schema half-reverted and requires manual recovery."
);
let mut tx = my.begin().await?;
let mut deferred_fks: Vec<String> = Vec::new();
for op in inverted {
match op {
Operation::Schema(change) => {
let step = render_step(
change,
inverted,
snapshot,
pool.dialect(),
schema.as_deref(),
)?;
for stmt in mysql_statements(&mut tx, &step).await? {
sqlx::query(&stmt).execute(&mut *tx).await?;
}
deferred_fks.extend(step.batch.deferred_fks);
}
Operation::Data(d) => {
sqlx::query(&d.sql).execute(&mut *tx).await?;
}
Operation::Callback(c) => return Err(callback_in_atomic(&target.name, c)),
}
}
for stmt in deferred_fks {
sqlx::query(&stmt).execute(&mut *tx).await?;
}
sqlx::query(&format!("DELETE FROM {ledger} WHERE name = ?"))
.bind(&target.name)
.execute(&mut *tx)
.await?;
tx.commit().await?;
}
#[cfg(feature = "sqlite")]
crate::sql::Pool::Sqlite(sq) => {
atomic_sqlite(
sq,
&target.name,
inverted,
snapshot,
LedgerWrite::Delete(ledger),
)
.await?;
}
}
Ok(())
}
pub async fn migrate_embedded_pool(
pool: &crate::sql::Pool,
embedded: &[(&str, &str)],
) -> Result<Vec<Migration>, MigrateError> {
migrate_embedded_pool_with_ledger(pool, embedded, LEDGER_TABLE).await
}
async fn migrate_embedded_pool_with_ledger(
pool: &crate::sql::Pool,
embedded: &[(&str, &str)],
ledger: &str,
) -> Result<Vec<Migration>, MigrateError> {
with_migrate_lock_pool(pool, ledger, async {
let mut all: Vec<Migration> = Vec::with_capacity(embedded.len());
for (name, json) in embedded {
let mig = file::parse(json)?;
if mig.name != *name {
return Err(MigrateError::Validation(format!(
"embedded entry key `{name}` doesn't match migration `name` field `{}`",
mig.name,
)));
}
all.push(mig);
}
all.sort_by(|a, b| a.name.cmp(&b.name));
file::validate_chain(&all, "embedded slice")?;
let applied = applied_set_pool_with_ledger(pool, ledger).await?;
let pending: Vec<Migration> = all
.iter()
.filter(|m| !applied.contains(&m.name))
.cloned()
.collect();
let mut newly = Vec::with_capacity(pending.len());
for mig in pending {
apply_one_pool(pool, &mig, ledger).await?;
newly.push(mig);
}
Ok(newly)
})
.await
}
async fn unapply_nonatomic_pool(
pool: &crate::sql::Pool,
target: &Migration,
inverted: &[Operation],
snapshot: &SchemaSnapshot,
ledger: &str,
) -> Result<(), MigrateError> {
tracing::info!(migration = %target.name, "unapplying (non-atomic, _pool)");
let schema = super::ensure::creation_schema(pool).await?;
let mut deferred_fks: Vec<String> = Vec::new();
for op in inverted {
match op {
Operation::Schema(change) => {
let step = render_step(
change,
inverted,
snapshot,
pool.dialect(),
schema.as_deref(),
)?;
deferred_fks.extend(run_step_pool(pool, step).await?);
}
Operation::Data(d) => {
crate::sql::raw_execute_pool(pool, &d.sql, ::std::vec::Vec::new()).await?;
}
Operation::Callback(c) => {
invoke_migration_callback(c, pool.clone()).await?;
}
}
}
for stmt in deferred_fks {
crate::sql::raw_execute_pool(pool, &stmt, ::std::vec::Vec::new()).await?;
}
let placeholder = pool.dialect().placeholder(1);
let delete_sql = format!("DELETE FROM {ledger} WHERE name = {placeholder}");
crate::sql::raw_execute_pool(
pool,
&delete_sql,
::std::vec![crate::core::SqlValue::String(target.name.clone())],
)
.await?;
Ok(())
}
#[cfg(test)]
mod tests {
#[test]
fn mysql_get_lock_null_is_an_error() {
assert!(super::mysql_lock_taken(Some(1)).unwrap());
assert!(!super::mysql_lock_taken(Some(0)).unwrap());
assert!(super::mysql_lock_taken(None).is_err());
}
}