use std::io::Write;
use std::ops::Not;
use clap::{Parser, Subcommand};
use crate::error::Error;
use crate::migrator::{Migrate, Plan};
#[derive(Parser, Debug)]
pub struct MigrationCommand {
#[command(subcommand)]
sub_command: SubCommand,
}
impl MigrationCommand {
pub async fn parse_and_run<DB, State>(
migrator: Box<dyn Migrate<DB, State>>,
connection: &mut <DB as sqlx::Database>::Connection,
) -> Result<(), Error>
where
DB: sqlx::Database,
State: Send + Sync,
{
let migration_command = Self::parse();
migration_command.run(migrator, connection).await
}
pub async fn run<DB, State>(
&self,
migrator: Box<dyn Migrate<DB, State>>,
connection: &mut <DB as sqlx::Database>::Connection,
) -> Result<(), Error>
where
DB: sqlx::Database,
State: Send + Sync,
{
self.sub_command
.handle_subcommand(migrator, connection)
.await?;
Ok(())
}
}
#[derive(Subcommand, Debug)]
enum SubCommand {
#[command()]
Apply(Apply),
#[command()]
Drop,
#[command()]
List,
#[command()]
Revert(Revert),
}
impl SubCommand {
async fn handle_subcommand<DB, State>(
&self,
migrator: Box<dyn Migrate<DB, State>>,
connection: &mut <DB as sqlx::Database>::Connection,
) -> Result<(), Error>
where
DB: sqlx::Database,
State: Send + Sync,
{
match self {
SubCommand::Apply(apply) => apply.run(migrator, connection).await?,
SubCommand::Drop => drop_migrations(migrator, connection).await?,
SubCommand::List => list_migrations(migrator, connection).await?,
SubCommand::Revert(revert) => revert.run(migrator, connection).await?,
}
Ok(())
}
}
async fn drop_migrations<DB, State>(
migrator: Box<dyn Migrate<DB, State>>,
connection: &mut <DB as sqlx::Database>::Connection,
) -> Result<(), Error>
where
DB: sqlx::Database,
{
migrator.ensure_migration_table_exists(connection).await?;
if migrator
.fetch_applied_migration_from_db(connection)
.await?
.is_empty()
.not()
{
return Err(Error::AppliedMigrationExists);
}
migrator.drop_migration_table_if_exists(connection).await?;
println!("Dropped migrations table");
Ok(())
}
async fn list_migrations<DB, State>(
migrator: Box<dyn Migrate<DB, State>>,
connection: &mut <DB as sqlx::Database>::Connection,
) -> Result<(), Error>
where
DB: sqlx::Database,
State: Send + Sync,
{
let migration_plan = migrator.generate_migration_plan(None, connection).await?;
if !migration_plan.is_empty() {
let applied_migrations = migrator.fetch_applied_migration_from_db(connection).await?;
let apply_plan = migrator
.generate_migration_plan(Some(&Plan::apply_all()), connection)
.await?;
let widths = [5, 10, 50, 10, 40];
let full_width = widths.iter().sum::<usize>() + widths.len() * 3;
let first_width = widths[0];
let second_width = widths[1];
let third_width = widths[2];
let fourth_width = widths[3];
let fifth_width = widths[4];
println!(
"{:^first_width$} | {:^second_width$} | {:^third_width$} | {:^fourth_width$} | \
{:^fifth_width$}",
"ID", "App", "Name", "Status", "Applied time"
);
println!("{:^full_width$}", "-".repeat(full_width));
for migration in migration_plan {
let mut id = String::from("N/A");
let mut status = "\u{2717}";
let mut applied_time = String::from("N/A");
let find_applied_migrations = applied_migrations
.iter()
.find(|&applied_migration| applied_migration == migration);
if let Some(sqlx_migration) = find_applied_migrations {
id = sqlx_migration.id().to_string();
status = "\u{2713}";
applied_time = sqlx_migration.applied_time().to_string();
} else if !apply_plan
.iter()
.any(|&plan_migration| plan_migration == migration)
{
status = "\u{2194}";
}
println!(
"{:^first_width$} | {:^second_width$} | {:^third_width$} | {:^fourth_width$} | \
{:^fifth_width$}",
id,
migration.app(),
migration.name(),
status,
applied_time
);
}
}
Ok(())
}
#[derive(Parser, Debug)]
#[allow(clippy::struct_excessive_bools)]
struct Apply {
#[arg(long)]
app: Option<String>,
#[arg(long)]
check: bool,
#[arg(long, conflicts_with = "app")]
count: Option<usize>,
#[arg(long)]
fake: bool,
#[arg(long)]
force: bool,
#[arg(long, requires = "app")]
migration: Option<String>,
#[arg(long)]
plan: bool,
}
impl Apply {
async fn run<DB, State>(
&self,
migrator: Box<dyn Migrate<DB, State>>,
connection: &mut <DB as sqlx::Database>::Connection,
) -> Result<(), Error>
where
DB: sqlx::Database,
State: Send + Sync,
{
let plan;
if let Some(count) = self.count {
plan = Plan::apply_count(count);
} else if let Some(app) = &self.app {
plan = Plan::apply_name(app, &self.migration);
} else {
plan = Plan::apply_all();
};
let migrations = migrator
.generate_migration_plan(Some(&plan), connection)
.await?;
if self.check && !migrations.is_empty() {
return Err(Error::PendingMigrationPresent);
}
if self.plan {
if migrations.is_empty() {
println!("No migration exists for applying");
} else {
let first_width = 10;
let second_width = 50;
let full_width = first_width + second_width + 3;
println!("{:^first_width$} | {:^second_width$}", "App", "Name");
println!("{:^full_width$}", "-".repeat(full_width));
for migration in migrations {
println!(
"{:^first_width$} | {:^second_width$}",
migration.app(),
migration.name(),
);
}
}
} else if self.fake {
for migration in migrations {
migrator
.add_migration_to_db_table(migration, connection)
.await?;
}
} else {
let destructible_migrations = migrations
.iter()
.filter(|m| m.operations().iter().any(|o| o.is_destructible()))
.collect::<Vec<_>>();
if !self.force && !destructible_migrations.is_empty() {
let mut input = String::new();
println!(
"Do you want to apply destructible migrations {} (y/N)",
destructible_migrations.len()
);
for (position, migration) in destructible_migrations.iter().enumerate() {
println!("{position}. {} : {}", migration.app(), migration.name());
}
std::io::stdout().flush()?;
std::io::stdin().read_line(&mut input)?;
let input_trimmed = input.trim().to_ascii_lowercase();
if !["y", "yes"].contains(&input_trimmed.as_str()) {
return Ok(());
}
}
migrator.run(connection, &plan).await?;
println!("Successfully applied migrations according to plan");
}
Ok(())
}
}
#[derive(Parser, Debug)]
#[allow(clippy::struct_excessive_bools)]
struct Revert {
#[arg(long, conflicts_with = "app")]
all: bool,
#[arg(long)]
app: Option<String>,
#[arg(long, conflicts_with_all = ["all", "app"])]
count: Option<usize>,
#[arg(long)]
fake: bool,
#[arg(long)]
force: bool,
#[arg(long, requires = "app")]
migration: Option<String>,
#[arg(long)]
plan: bool,
}
impl Revert {
async fn run<DB, State>(
&self,
migrator: Box<dyn Migrate<DB, State>>,
connection: &mut <DB as sqlx::Database>::Connection,
) -> Result<(), Error>
where
DB: sqlx::Database,
State: Send + Sync,
{
let plan;
if let Some(count) = self.count {
plan = Plan::revert_count(count);
} else if let Some(app) = &self.app {
plan = Plan::revert_name(app, &self.migration);
} else if self.all {
plan = Plan::revert_all();
} else {
plan = Plan::revert_count(1);
};
let revert_migrations = migrator
.generate_migration_plan(Some(&plan), connection)
.await?;
if self.plan {
if revert_migrations.is_empty() {
println!("No migration exists for reverting");
} else {
let first_width = 10;
let second_width = 50;
let full_width = first_width + second_width + 3;
println!("{:^first_width$} | {:^second_width$}", "App", "Name");
println!("{:^full_width$}", "-".repeat(full_width));
for migration in revert_migrations {
println!(
"{:^first_width$} | {:^second_width$}",
migration.app(),
migration.name(),
);
}
}
} else if self.fake {
for migration in revert_migrations {
migrator
.delete_migration_from_db_table(migration, connection)
.await?;
}
} else {
if !self.force && !revert_migrations.is_empty() {
let mut input = String::new();
println!(
"Do you want to revert {} migrations (y/N)",
revert_migrations.len()
);
for (position, migration) in revert_migrations.iter().enumerate() {
println!("{position}. {} : {}", migration.app(), migration.name());
}
std::io::stdout().flush()?;
std::io::stdin().read_line(&mut input)?;
let input_trimmed = input.trim().to_ascii_lowercase();
if !["y", "yes"].contains(&input_trimmed.as_str()) {
return Ok(());
}
}
migrator.run(connection, &plan).await?;
println!("Successfully reverted migrations according to plan");
}
Ok(())
}
}