sqlx-models-cli 0.1.1

Command-line utility for SQLx, the Rust SQL toolkit.
Documentation
use anyhow::{bail, Context};
use console::style;
use std::fs::{self, File};
use std::io::{Read, Write};

const MIGRATION_FOLDER: &str = "migrations";

pub struct Migration {
    pub name: String,
    pub sql: String,
}

pub fn add_file(name: &str) -> anyhow::Result<()> {
    use chrono::prelude::*;
    use std::path::PathBuf;

    fs::create_dir_all(MIGRATION_FOLDER).context("Unable to create migrations directory")?;

    let dt = Utc::now();
    let mut file_name = dt.format("%Y-%m-%d_%H-%M-%S").to_string();
    file_name.push_str("_");
    file_name.push_str(name);
    file_name.push_str(".sql");

    let mut path = PathBuf::new();
    path.push(MIGRATION_FOLDER);
    path.push(&file_name);

    let mut file = File::create(path).context("Failed to create file")?;
    file.write_all(b"-- Add migration script here")
        .context("Could not write to file")?;

    println!("Created migration: '{}'", file_name);
    Ok(())
}

pub async fn run() -> anyhow::Result<()> {
    let migrator = crate::migrator::get()?;

    if !migrator.can_migrate_database() {
        bail!(
            "Database migrations not supported for {}",
            migrator.database_type()
        );
    }

    migrator.create_migration_table().await?;

    let migrations = load_migrations()?;

    for mig in migrations.iter() {
        let mut tx = migrator.begin_migration().await?;

        if tx.check_if_applied(&mig.name).await? {
            println!("Already applied migration: '{}'", mig.name);
            continue;
        }
        println!("Applying migration: '{}'", mig.name);

        tx.execute_migration(&mig.sql)
            .await
            .with_context(|| format!("Failed to run migration {:?}", &mig.name))?;

        tx.save_applied_migration(&mig.name)
            .await
            .context("Failed to insert migration")?;

        tx.commit().await.context("Failed")?;
    }

    Ok(())
}

pub async fn list() -> anyhow::Result<()> {
    let migrator = crate::migrator::get()?;

    if !migrator.can_migrate_database() {
        bail!(
            "Database migrations not supported for {}",
            migrator.database_type()
        );
    }

    let file_migrations = load_migrations()?;

    if migrator
        .check_if_database_exists(&migrator.get_database_name()?)
        .await?
    {
        let applied_migrations = migrator.get_migrations().await.unwrap_or_else(|_| {
            println!("Could not retrive data from migration table");
            Vec::new()
        });

        let mut width = 0;
        for mig in file_migrations.iter() {
            width = std::cmp::max(width, mig.name.len());
        }
        for mig in file_migrations.iter() {
            let status = if applied_migrations
                .iter()
                .find(|&m| mig.name == *m)
                .is_some()
            {
                style("Applied").green()
            } else {
                style("Not Applied").yellow()
            };

            println!("{:width$}\t{}", mig.name, status, width = width);
        }

        let orphans = check_for_orphans(file_migrations, applied_migrations);

        if let Some(orphans) = orphans {
            println!("\nFound migrations applied in the database that does not have a corresponding migration file:");
            for name in orphans {
                println!("{:width$}\t{}", name, style("Orphan").red(), width = width);
            }
        }
    } else {
        println!("No database found, listing migrations");

        for mig in file_migrations {
            println!("{}", mig.name);
        }
    }

    Ok(())
}

fn load_migrations() -> anyhow::Result<Vec<Migration>> {
    let entries = fs::read_dir(&MIGRATION_FOLDER).context("Could not find 'migrations' dir")?;

    let mut migrations = Vec::new();

    for e in entries {
        if let Ok(e) = e {
            if let Ok(meta) = e.metadata() {
                if !meta.is_file() {
                    continue;
                }

                if let Some(ext) = e.path().extension() {
                    if ext != "sql" {
                        println!("Wrong ext: {:?}", ext);
                        continue;
                    }
                } else {
                    continue;
                }

                let mut file = File::open(e.path())
                    .with_context(|| format!("Failed to open: '{:?}'", e.file_name()))?;
                let mut contents = String::new();
                file.read_to_string(&mut contents)
                    .with_context(|| format!("Failed to read: '{:?}'", e.file_name()))?;

                migrations.push(Migration {
                    name: e.file_name().to_str().unwrap().to_string(),
                    sql: contents,
                });
            }
        }
    }

    migrations.sort_by(|a, b| a.name.partial_cmp(&b.name).unwrap());

    Ok(migrations)
}

fn check_for_orphans(
    file_migrations: Vec<Migration>,
    applied_migrations: Vec<String>,
) -> Option<Vec<String>> {
    let orphans: Vec<String> = applied_migrations
        .iter()
        .filter(|m| !file_migrations.iter().any(|fm| fm.name == **m))
        .cloned()
        .collect();

    if orphans.len() > 0 {
        Some(orphans)
    } else {
        None
    }
}