use std::sync::Arc;
use rainier_orm::{Blueprint, Dialect, Entity, Row, TableChanges};
use rainier_support::{Error, Result};
use crate::connection::Database;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Down {
Statements(Vec<String>),
Irreversible(String),
}
impl Down {
pub fn statements(statements: impl IntoIterator<Item = String>) -> Self {
Self::Statements(statements.into_iter().collect())
}
pub fn irreversible(reason: impl Into<String>) -> Self {
Self::Irreversible(reason.into())
}
pub fn sql(&self, migration: &str) -> Result<&[String]> {
match self {
Down::Statements(statements) => Ok(statements),
Down::Irreversible(reason) => {
Err(Error::internal(format!("`{migration}` cannot be rolled back: {reason}")))
}
}
}
pub fn is_reversible(&self) -> bool {
matches!(self, Down::Statements(_))
}
}
pub trait Migration: Send + Sync + 'static {
fn name(&self) -> &str;
fn up(&self, dialect: Dialect) -> Vec<String>;
fn down(&self, dialect: Dialect) -> Down;
}
pub struct Step {
name: String,
up: Box<dyn Fn(Dialect) -> Vec<String> + Send + Sync>,
down: Box<dyn Fn(Dialect) -> Down + Send + Sync>,
}
impl Step {
pub fn new(
name: impl Into<String>,
up: impl Fn(Dialect) -> Vec<String> + Send + Sync + 'static,
down: impl Fn(Dialect) -> Down + Send + Sync + 'static,
) -> Self {
Self { name: name.into(), up: Box::new(up), down: Box::new(down) }
}
pub fn raw(name: impl Into<String>, up: Vec<String>, down: Vec<String>) -> Self {
Self::new(name, move |_| up.clone(), move |_| Down::Statements(down.clone()))
}
pub fn raw_irreversible(
name: impl Into<String>,
up: Vec<String>,
reason: impl Into<String>,
) -> Self {
let reason = reason.into();
Self::new(name, move |_| up.clone(), move |_| Down::irreversible(reason.clone()))
}
pub fn create_table<E: Entity>(name: impl Into<String>) -> Self {
Self::new(
name,
|dialect| rainier_orm::schema::schema_ddl::<E>(dialect),
|_| Down::Statements(vec![format!("DROP TABLE IF EXISTS {}", E::table())]),
)
}
pub fn create(
name: impl Into<String>,
table: impl Into<String>,
build: impl FnOnce(&mut Blueprint),
) -> Self {
let blueprint = Arc::new(Blueprint::create(table, build));
let reverse = Arc::clone(&blueprint);
Self::new(
name,
move |dialect| blueprint.to_sql(dialect),
move |dialect| Down::Statements(reverse.to_reverse_sql(dialect)),
)
}
pub fn table(
name: impl Into<String>,
table: impl Into<String>,
build: impl FnOnce(&mut TableChanges),
) -> Self {
let changes = Arc::new(TableChanges::to(table, build));
let reverse = Arc::clone(&changes);
Self::new(
name,
move |dialect| changes.to_sql(dialect),
move |dialect| match reverse.irreversible_because() {
Some(reason) => Down::irreversible(reason),
None => Down::Statements(reverse.to_reverse_sql(dialect)),
},
)
}
pub fn drop_table(name: impl Into<String>, table: impl Into<String>) -> Self {
let table = table.into();
let reason = format!("`{table}` was dropped, along with everything in it");
Self::new(
name,
move |dialect| {
let mut stmt = rainier_orm::sea_query::Table::drop();
stmt.table(rainier_orm::sea_query::Alias::new(table.clone())).if_exists();
vec![dialect.build_schema(&stmt)]
},
move |_| Down::irreversible(reason.clone()),
)
}
pub fn rename_table(
name: impl Into<String>,
from: impl Into<String>,
to: impl Into<String>,
) -> Self {
let (from, to) = (from.into(), to.into());
let (back_from, back_to) = (to.clone(), from.clone());
Self::new(
name,
move |dialect| vec![rename_table_sql(dialect, &from, &to)],
move |dialect| Down::Statements(vec![rename_table_sql(dialect, &back_from, &back_to)]),
)
}
}
fn rename_table_sql(dialect: Dialect, from: &str, to: &str) -> String {
let mut stmt = rainier_orm::sea_query::Table::rename();
stmt.table(
rainier_orm::sea_query::Alias::new(from.to_string()),
rainier_orm::sea_query::Alias::new(to.to_string()),
);
dialect.build_schema(&stmt)
}
impl Migration for Step {
fn name(&self) -> &str {
&self.name
}
fn up(&self, dialect: Dialect) -> Vec<String> {
(self.up)(dialect)
}
fn down(&self, dialect: Dialect) -> Down {
(self.down)(dialect)
}
}
impl std::fmt::Debug for Step {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Step").field("name", &self.name).finish()
}
}
const LEDGER: &str = "rainier_migrations";
#[derive(Default)]
pub struct Migrator {
migrations: Vec<Arc<dyn Migration>>,
}
impl Migrator {
pub fn new() -> Self {
Self::default()
}
#[allow(clippy::should_implement_trait, reason = "a builder step, not arithmetic")]
pub fn add(mut self, migration: impl Migration) -> Self {
self.migrations.push(Arc::new(migration));
self
}
pub fn add_arc(mut self, migration: Arc<dyn Migration>) -> Self {
self.migrations.push(migration);
self
}
pub fn create_table<E: Entity>(self, name: impl Into<String>) -> Self {
self.add(Step::create_table::<E>(name))
}
pub fn step(
self,
name: impl Into<String>,
up: impl Fn(Dialect) -> Vec<String> + Send + Sync + 'static,
down: impl Fn(Dialect) -> Down + Send + Sync + 'static,
) -> Self {
self.add(Step::new(name, up, down))
}
pub fn raw(self, name: impl Into<String>, up: Vec<String>, down: Vec<String>) -> Self {
self.add(Step::raw(name, up, down))
}
pub fn raw_irreversible(
self,
name: impl Into<String>,
up: Vec<String>,
reason: impl Into<String>,
) -> Self {
self.add(Step::raw_irreversible(name, up, reason))
}
pub fn merge(mut self, other: Migrator) -> Self {
self.migrations.extend(other.migrations);
self
}
pub fn names(&self) -> Vec<&str> {
self.migrations.iter().map(|m| m.name()).collect()
}
pub fn len(&self) -> usize {
self.migrations.len()
}
pub fn is_empty(&self) -> bool {
self.migrations.is_empty()
}
pub fn irreversible(&self, dialect: Dialect) -> Vec<&str> {
self.migrations
.iter()
.filter(|m| !m.down(dialect).is_reversible())
.map(|m| m.name())
.collect()
}
pub async fn run(&self, db: &Database) -> Result<Vec<String>> {
self.ensure_ledger(db).await?;
let ledger = self.ledger_rows(db).await?;
let batch = ledger.iter().map(|(_, batch)| *batch).max().unwrap_or(0) + 1;
let mut ran = Vec::new();
for migration in &self.migrations {
let name = migration.name();
if ledger.iter().any(|(applied, _)| applied == name) {
continue;
}
for sql in migration.up(db.dialect()) {
self.execute(db, name, &sql, "migration").await?;
}
self.record(db, name, batch).await?;
tracing::info!(migration = %name, batch, "migrated");
ran.push(name.to_string());
}
Ok(ran)
}
pub async fn rollback(&self, db: &Database, batches: u32) -> Result<Vec<String>> {
let targets = self.rollback_targets(db, batches).await?;
if targets.is_empty() {
return Ok(Vec::new());
}
for name in &targets {
self.find(name)?.down(db.dialect()).sql(name)?;
}
let mut rolled_back = Vec::new();
for name in &targets {
let down = self.find(name)?.down(db.dialect());
for sql in down.sql(name)? {
self.execute(db, name, sql, "rollback").await?;
}
self.forget(db, name).await?;
tracing::info!(migration = %name, "rolled back");
rolled_back.push(name.clone());
}
Ok(rolled_back)
}
pub async fn rollback_targets(&self, db: &Database, batches: u32) -> Result<Vec<String>> {
if batches == 0 {
return Ok(Vec::new());
}
let mut rows = self.ledger_rows(db).await?;
if rows.is_empty() {
return Ok(Vec::new());
}
let mut numbers: Vec<i64> = rows.iter().map(|(_, batch)| *batch).collect();
numbers.sort_unstable();
numbers.dedup();
let cutoff = numbers.iter().rev().take(batches as usize).copied().min().unwrap_or(i64::MAX);
rows.retain(|(_, batch)| *batch >= cutoff);
let declared = self.names();
rows.sort_by_key(|(name, batch)| {
let position = declared.iter().position(|n| n == name).unwrap_or(usize::MAX);
(std::cmp::Reverse(*batch), std::cmp::Reverse(position))
});
Ok(rows.into_iter().map(|(name, _)| name).collect())
}
pub async fn applied(&self, db: &Database) -> Result<Vec<String>> {
Ok(self.ledger_rows(db).await?.into_iter().map(|(name, _)| name).collect())
}
pub async fn pending(&self, db: &Database) -> Result<Vec<&str>> {
let applied = self.applied(db).await?;
Ok(self.names().into_iter().filter(|name| !applied.iter().any(|a| a == name)).collect())
}
fn find(&self, name: &str) -> Result<&Arc<dyn Migration>> {
self.migrations.iter().find(|m| m.name() == name).ok_or_else(|| {
Error::internal(format!(
"`{name}` is in the ledger but not in this migrator, so there is nothing to \
run to undo it. It was probably deleted from the code while still applied."
))
})
}
async fn execute(&self, db: &Database, name: &str, sql: &str, what: &str) -> Result<()> {
db.statement(sql).await.map(|_| ()).map_err(|e| {
Error::internal(format!(
"{what} `{name}` failed on `{}`: {e}",
rainier_support::str::limit(sql, 80, "…")
))
})
}
async fn ledger_rows(&self, db: &Database) -> Result<Vec<(String, i64)>> {
self.ensure_ledger(db).await?;
let prepared = crate::statement::Prepared {
sql: format!("SELECT name, batch FROM {LEDGER}"),
params: Vec::new(),
route: rainier_orm::ShardRoute::Global,
};
let columns = vec![
crate::row::ColumnRequest::new("name", rainier_orm::ColumnType::Text),
crate::row::ColumnRequest::new("batch", rainier_orm::ColumnType::BigInt),
];
let rows = db.fetch(prepared, columns).await?;
rows.iter()
.map(|row| {
let name = row
.get_string("name")
.map_err(Error::from)?
.ok_or_else(|| Error::internal("a migration ledger row had a NULL name"))?;
let batch = row.get_i64("batch").map_err(Error::from)?.unwrap_or(1);
Ok((name, batch))
})
.collect()
}
async fn ensure_ledger(&self, db: &Database) -> Result<()> {
db.statement(&format!(
"CREATE TABLE IF NOT EXISTS {LEDGER} (\
name VARCHAR(255) NOT NULL PRIMARY KEY, \
batch BIGINT NOT NULL DEFAULT 1)"
))
.await?;
Ok(())
}
async fn record(&self, db: &Database, name: &str, batch: i64) -> Result<()> {
let values = if db.dialect() == Dialect::Postgres { "($1, $2)" } else { "(?, ?)" };
let prepared = crate::statement::Prepared {
sql: format!("INSERT INTO {LEDGER} (name, batch) VALUES {values}"),
params: vec![name.into(), batch.into()],
route: rainier_orm::ShardRoute::Global,
};
db.execute(prepared).await?;
Ok(())
}
async fn forget(&self, db: &Database, name: &str) -> Result<()> {
let placeholder = if db.dialect() == Dialect::Postgres { "$1" } else { "?" };
let prepared = crate::statement::Prepared {
sql: format!("DELETE FROM {LEDGER} WHERE name = {placeholder}"),
params: vec![name.into()],
route: rainier_orm::ShardRoute::Global,
};
db.execute(prepared).await?;
Ok(())
}
}
impl std::fmt::Debug for Migrator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Migrator").field("migrations", &self.names()).finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::row::OwnedRow;
use crate::testing::{fake_database, MemoryConnection};
#[derive(rainier_orm::Entity, Clone, Debug)]
#[orm(table = "users")]
struct User {
#[orm(pk, auto_increment)]
id: u64,
#[orm(unique)]
email: String,
}
fn ledger(name: &str, batch: i64) -> OwnedRow {
OwnedRow::new().with("name", name).with("batch", batch)
}
#[test]
fn a_migrator_run_is_send() {
fn assert_send<T: Send>(_: T) {}
let (db, _) = fake_database(MemoryConnection::new(Dialect::Sqlite));
let migrator = Migrator::new().create_table::<User>("0001_users");
assert_send(async move { migrator.run(&db).await });
}
#[test]
fn a_rollback_is_send_too() {
fn assert_send<T: Send>(_: T) {}
let (db, _) = fake_database(MemoryConnection::new(Dialect::Sqlite));
let migrator = Migrator::new().create_table::<User>("0001_users");
assert_send(async move { migrator.rollback(&db, 1).await });
}
#[test]
fn create_table_renders_the_entitys_ddl() {
let step = Step::create_table::<User>("0001_users");
let statements = step.up(Dialect::Sqlite);
assert_eq!(step.name(), "0001_users");
assert!(statements[0].contains("users"), "{:?}", statements);
assert!(statements[0].contains("IF NOT EXISTS"), "{:?}", statements);
}
#[test]
fn create_table_undoes_itself_by_dropping_the_table() {
let step = Step::create_table::<User>("0001_users");
assert_eq!(
step.down(Dialect::Sqlite),
Down::Statements(vec!["DROP TABLE IF EXISTS users".to_string()])
);
}
#[test]
fn ddl_differs_per_dialect() {
let step = Step::create_table::<User>("0001_users");
let sqlite = step.up(Dialect::Sqlite).join("");
let mysql = step.up(Dialect::MySql).join("");
assert_ne!(sqlite, mysql, "each dialect should render its own DDL");
}
#[test]
fn an_irreversible_step_says_why_rather_than_doing_nothing() {
let step = Step::raw_irreversible(
"0003_backfill",
vec!["UPDATE users SET email = lower(email)".into()],
"the original casing is not recoverable",
);
let down = step.down(Dialect::Sqlite);
assert!(!down.is_reversible());
let err = down.sql("0003_backfill").unwrap_err();
assert!(err.message().contains("cannot be rolled back"), "{}", err.message());
assert!(err.message().contains("not recoverable"), "{}", err.message());
}
#[test]
fn a_custom_type_can_implement_the_contract() {
struct Citext;
impl Migration for Citext {
fn name(&self) -> &str {
"0004_citext"
}
fn up(&self, dialect: Dialect) -> Vec<String> {
match dialect {
Dialect::Postgres => vec!["CREATE EXTENSION IF NOT EXISTS citext".into()],
_ => Vec::new(),
}
}
fn down(&self, dialect: Dialect) -> Down {
match dialect {
Dialect::Postgres => Down::statements(["DROP EXTENSION citext".to_string()]),
_ => Down::statements([]),
}
}
}
let migrator = Migrator::new().add(Citext);
assert_eq!(migrator.names(), vec!["0004_citext"]);
assert!(migrator.irreversible(Dialect::Postgres).is_empty());
}
#[test]
fn irreversible_steps_can_be_listed_before_a_deploy() {
let migrator = Migrator::new().create_table::<User>("0001_users").raw_irreversible(
"0002_drop_legacy",
vec!["ALTER TABLE users DROP COLUMN legacy".into()],
"the column's data is gone",
);
assert_eq!(migrator.irreversible(Dialect::Sqlite), vec!["0002_drop_legacy"]);
}
#[tokio::test]
async fn runs_pending_migrations_in_order() {
let (db, connection) = fake_database(MemoryConnection::new(Dialect::Sqlite));
let ran = Migrator::new()
.raw("0001_a", vec!["CREATE TABLE a (id INT)".into()], vec!["DROP TABLE a".into()])
.raw("0002_b", vec!["CREATE TABLE b (id INT)".into()], vec!["DROP TABLE b".into()])
.run(&db)
.await
.unwrap();
assert_eq!(ran, vec!["0001_a", "0002_b"]);
let statements = connection.statements();
assert!(statements.iter().any(|s| s.contains("CREATE TABLE a")));
assert!(statements.iter().any(|s| s.contains("CREATE TABLE b")));
assert!(statements.iter().any(|s| s.contains("rainier_migrations")));
}
#[tokio::test]
async fn an_already_applied_migration_is_skipped() {
let connection = MemoryConnection::new(Dialect::Sqlite).returning([ledger("0001_a", 1)]);
let (db, handle) = fake_database(connection);
let ran = Migrator::new()
.raw("0001_a", vec!["CREATE TABLE a (id INT)".into()], vec!["DROP TABLE a".into()])
.raw("0002_b", vec!["CREATE TABLE b (id INT)".into()], vec!["DROP TABLE b".into()])
.run(&db)
.await
.unwrap();
assert_eq!(ran, vec!["0002_b"]);
assert!(
!handle.statements().iter().any(|s| s.contains("CREATE TABLE a")),
"the applied migration must not run again"
);
}
#[tokio::test]
async fn a_new_run_gets_the_next_batch_number() {
let (db, connection) =
fake_database(MemoryConnection::new(Dialect::Sqlite).returning([ledger("0001_a", 7)]));
Migrator::new()
.raw("0001_a", vec!["SELECT 1".into()], vec!["SELECT 1".into()])
.raw("0002_b", vec!["SELECT 2".into()], vec!["SELECT 2".into()])
.run(&db)
.await
.unwrap();
let inserts: Vec<_> = connection
.recorded()
.into_iter()
.filter(|r| r.sql.starts_with("INSERT INTO rainier_migrations"))
.collect();
assert_eq!(inserts.len(), 1, "only the pending step should be recorded");
assert_eq!(
inserts[0].params[1],
8_i64.into(),
"one past the highest batch in the ledger, not one past its row count"
);
}
#[tokio::test]
async fn a_failing_step_names_itself_and_its_statement() {
let (db, _) = fake_database(MemoryConnection::new(Dialect::Sqlite).failing("syntax error"));
let err = Migrator::new()
.raw("0001_broken", vec!["CREATE TABLE (".into()], vec!["SELECT 1".into()])
.run(&db)
.await
.unwrap_err();
assert!(err.message().contains("syntax error"), "{}", err.message());
}
#[tokio::test]
async fn the_ledger_is_created_before_anything_is_read() {
let (db, connection) = fake_database(MemoryConnection::new(Dialect::Sqlite));
Migrator::new().run(&db).await.unwrap();
let statements = connection.statements();
assert!(statements[0].contains("CREATE TABLE IF NOT EXISTS rainier_migrations"));
assert!(statements[0].contains("batch"), "{}", statements[0]);
}
#[test]
fn the_migrator_lists_its_steps() {
let migrator = Migrator::new().create_table::<User>("0001_users").raw(
"0002_seed",
vec!["INSERT INTO users DEFAULT VALUES".into()],
vec!["DELETE FROM users".into()],
);
assert_eq!(migrator.names(), vec!["0001_users", "0002_seed"]);
assert_eq!(migrator.len(), 2);
assert!(!migrator.is_empty());
}
#[test]
fn merging_appends_the_other_migrators_steps_in_order() {
let component = Migrator::new()
.raw("component_0001", vec!["SELECT 1".into()], vec!["SELECT 1".into()])
.raw("component_0002", vec!["SELECT 2".into()], vec!["SELECT 2".into()]);
let migrator = Migrator::new()
.raw("app_0001", vec!["SELECT 3".into()], vec!["SELECT 3".into()])
.merge(component)
.raw("app_0002", vec!["SELECT 4".into()], vec!["SELECT 4".into()]);
assert_eq!(
migrator.names(),
vec!["app_0001", "component_0001", "component_0002", "app_0002"],
"merged steps keep their order, at the point they were merged in"
);
}
#[tokio::test]
async fn postgres_gets_numbered_placeholders() {
let (db, connection) = fake_database(MemoryConnection::new(Dialect::Postgres));
Migrator::new()
.raw("0001_a", vec!["SELECT 1".into()], vec!["SELECT 1".into()])
.run(&db)
.await
.unwrap();
let insert = connection
.statements()
.into_iter()
.find(|s| s.starts_with("INSERT INTO rainier_migrations"))
.expect("the ledger row should have been written");
assert!(insert.contains("$1") && insert.contains("$2"), "{insert}");
}
#[tokio::test]
async fn rolling_back_an_empty_ledger_is_not_an_error() {
let (db, _) = fake_database(MemoryConnection::new(Dialect::Sqlite));
let migrator = Migrator::new().create_table::<User>("0001_users");
assert!(migrator.rollback(&db, 1).await.unwrap().is_empty());
}
#[tokio::test]
async fn rolling_back_zero_batches_does_nothing() {
let (db, connection) =
fake_database(MemoryConnection::new(Dialect::Sqlite).returning([ledger("0001_a", 1)]));
let migrator =
Migrator::new().raw("0001_a", vec!["SELECT 1".into()], vec!["DROP TABLE a".into()]);
assert!(migrator.rollback(&db, 0).await.unwrap().is_empty());
assert!(
!connection.statements().iter().any(|s| s.contains("DROP TABLE a")),
"nothing should have been undone"
);
}
#[tokio::test]
async fn a_rollback_undoes_the_last_batch_in_reverse() {
let (db, connection) = fake_database(MemoryConnection::new(Dialect::Sqlite).returning([
ledger("0001_a", 1),
ledger("0002_b", 2),
ledger("0003_c", 2),
]));
let migrator = Migrator::new()
.raw("0001_a", vec!["SELECT 1".into()], vec!["DROP TABLE a".into()])
.raw("0002_b", vec!["SELECT 2".into()], vec!["DROP TABLE b".into()])
.raw("0003_c", vec!["SELECT 3".into()], vec!["DROP TABLE c".into()]);
let rolled_back = migrator.rollback(&db, 1).await.unwrap();
assert_eq!(
rolled_back,
vec!["0003_c", "0002_b"],
"the newest batch only, and dependants first"
);
let statements = connection.statements();
assert!(statements.iter().any(|s| s == "DROP TABLE c"));
assert!(statements.iter().any(|s| s == "DROP TABLE b"));
assert!(!statements.iter().any(|s| s == "DROP TABLE a"), "batch 1 was not in range");
assert!(statements.iter().any(|s| s.starts_with("DELETE FROM rainier_migrations")));
}
#[tokio::test]
async fn a_rollback_can_take_several_batches() {
let (db, _) = fake_database(
MemoryConnection::new(Dialect::Sqlite)
.returning([ledger("0001_a", 1), ledger("0002_b", 2)]),
);
let migrator = Migrator::new()
.raw("0001_a", vec!["SELECT 1".into()], vec!["DROP TABLE a".into()])
.raw("0002_b", vec!["SELECT 2".into()], vec!["DROP TABLE b".into()]);
assert_eq!(migrator.rollback(&db, 2).await.unwrap(), vec!["0002_b", "0001_a"]);
}
#[tokio::test]
async fn an_irreversible_step_stops_the_whole_rollback_before_it_starts() {
let (db, connection) = fake_database(
MemoryConnection::new(Dialect::Sqlite)
.returning([ledger("0001_a", 1), ledger("0002_b", 1)]),
);
let migrator = Migrator::new()
.raw_irreversible("0001_a", vec!["SELECT 1".into()], "the rows are gone")
.raw("0002_b", vec!["SELECT 2".into()], vec!["DROP TABLE b".into()]);
let err = migrator.rollback(&db, 1).await.unwrap_err();
assert!(err.message().contains("0001_a"), "{}", err.message());
assert!(err.message().contains("the rows are gone"), "{}", err.message());
assert!(
!connection.statements().iter().any(|s| s == "DROP TABLE b"),
"no step should have run"
);
}
#[tokio::test]
async fn rolling_back_a_migration_that_was_deleted_from_the_code_says_so() {
let (db, _) =
fake_database(MemoryConnection::new(Dialect::Sqlite).returning([ledger("0001_a", 1)]));
let err = Migrator::new().rollback(&db, 1).await.unwrap_err();
assert!(err.message().contains("0001_a"), "{}", err.message());
assert!(err.message().contains("not in this migrator"), "{}", err.message());
}
#[tokio::test]
async fn pending_is_what_has_not_run() {
let (db, _) =
fake_database(MemoryConnection::new(Dialect::Sqlite).returning([ledger("0001_a", 1)]));
let migrator = Migrator::new()
.raw("0001_a", vec!["SELECT 1".into()], vec!["SELECT 1".into()])
.raw("0002_b", vec!["SELECT 2".into()], vec!["SELECT 2".into()]);
assert_eq!(migrator.pending(&db).await.unwrap(), vec!["0002_b"]);
}
}