toasty_cli/migration/
apply.rs

1use super::HistoryFile;
2use crate::Config;
3use anyhow::Result;
4use clap::Parser;
5use console::style;
6use std::collections::HashSet;
7use std::fs;
8use toasty::Db;
9use toasty::schema::db::Migration;
10
11/// Applies pending migrations to the database.
12///
13/// Reads the migration history file to determine which migrations exist, then
14/// queries the database for already-applied migrations. Any migration present
15/// in the history but not yet applied is executed in order.
16///
17/// If no pending migrations are found, the command prints a message and exits
18/// without modifying the database.
19#[derive(Parser, Debug)]
20pub struct ApplyCommand {}
21
22impl ApplyCommand {
23    pub(crate) async fn run(self, db: &Db, config: &Config) -> Result<()> {
24        println!();
25        println!("  {}", style("Apply Migrations").cyan().bold().underlined());
26        println!();
27        println!(
28            "  {}",
29            style(format!(
30                "Connected to {}",
31                crate::utility::redact_url_password(&db.driver().url())
32            ))
33            .dim()
34        );
35        println!();
36
37        apply_migrations(db, config).await
38    }
39}
40
41pub(crate) async fn apply_migrations(db: &Db, config: &Config) -> Result<()> {
42    let history_path = config.migration.get_history_file_path();
43
44    // Load migration history
45    let history = HistoryFile::load_or_default(&history_path)?;
46
47    if history.migrations().is_empty() {
48        println!(
49            "  {}",
50            style("No migrations found in history file.")
51                .magenta()
52                .dim()
53        );
54        println!();
55        return Ok(());
56    }
57
58    // Get a connection to check which migrations have been applied
59    let mut conn = db.driver().connect().await?;
60
61    // Get list of already applied migrations
62    let applied_migrations = conn.applied_migrations().await?;
63    let applied_ids: HashSet<u64> = applied_migrations.iter().map(|m| m.id()).collect();
64
65    // Find migrations that haven't been applied yet
66    let pending_migrations: Vec<_> = history
67        .migrations()
68        .iter()
69        .filter(|m| !applied_ids.contains(&m.id))
70        .collect();
71
72    if pending_migrations.is_empty() {
73        println!(
74            "  {}",
75            style("All migrations are already applied. Database is up to date.")
76                .green()
77                .dim()
78        );
79        println!();
80        return Ok(());
81    }
82
83    let pending_count = pending_migrations.len();
84    println!(
85        "  {} Found {} pending migration(s) to apply",
86        style("→").cyan(),
87        pending_count
88    );
89    println!();
90
91    // Apply each pending migration
92    for migration_entry in &pending_migrations {
93        let migration_path = config
94            .migration
95            .get_migrations_dir()
96            .join(&migration_entry.name);
97
98        println!(
99            "  {} Applying migration: {}",
100            style("→").cyan(),
101            style(&migration_entry.name).bold()
102        );
103
104        // Load the migration SQL file
105        let sql = fs::read_to_string(&migration_path)?;
106        let migration = Migration::new_sql(sql);
107
108        // Apply the migration
109        conn.apply_migration(migration_entry.id, &migration_entry.name, &migration)
110            .await?;
111
112        println!(
113            "  {} {}",
114            style("✓").green().bold(),
115            style(format!("Applied: {}", migration_entry.name)).dim()
116        );
117    }
118
119    println!();
120    println!(
121        "  {}",
122        style(format!(
123            "Successfully applied {} migration(s)",
124            pending_count
125        ))
126        .green()
127        .bold()
128    );
129    println!();
130
131    Ok(())
132}