Skip to main content

toasty_cli/migration/
apply.rs

1use crate::Config;
2use anyhow::Result;
3use clap::Parser;
4use console::style;
5use hashbrown::HashSet;
6use std::fs;
7use toasty::Db;
8use toasty::migration::History;
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 = History::load_or_default(&history_path)?;
46
47    if history.entries().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
60        .driver()
61        .connect(&toasty::db::ConnectContext::default())
62        .await?;
63
64    // Get list of already applied migrations
65    let applied_migrations = conn.applied_migrations().await?;
66    let applied_ids: HashSet<u64> = applied_migrations.iter().map(|m| m.id()).collect();
67
68    // Find migrations that haven't been applied yet
69    let pending_migrations: Vec<_> = history
70        .entries()
71        .iter()
72        .filter(|m| !applied_ids.contains(&m.id))
73        .collect();
74
75    if pending_migrations.is_empty() {
76        println!(
77            "  {}",
78            style("All migrations are already applied. Database is up to date.")
79                .green()
80                .dim()
81        );
82        println!();
83        return Ok(());
84    }
85
86    let pending_count = pending_migrations.len();
87    println!(
88        "  {} Found {} pending migration(s) to apply",
89        style("→").cyan(),
90        pending_count
91    );
92    println!();
93
94    // Apply each pending migration
95    for migration_entry in &pending_migrations {
96        let migration_path = config
97            .migration
98            .get_migrations_dir()
99            .join(&migration_entry.name);
100
101        println!(
102            "  {} Applying migration: {}",
103            style("→").cyan(),
104            style(&migration_entry.name).bold()
105        );
106
107        // Load the migration SQL file
108        let sql = fs::read_to_string(&migration_path)?;
109        let migration = Migration::new_sql(sql);
110
111        // Apply the migration
112        conn.apply_migration(migration_entry.id, &migration_entry.name, &migration)
113            .await?;
114
115        println!(
116            "  {} {}",
117            style("✓").green().bold(),
118            style(format!("Applied: {}", migration_entry.name)).dim()
119        );
120    }
121
122    println!();
123    println!(
124        "  {}",
125        style(format!(
126            "Successfully applied {} migration(s)",
127            pending_count
128        ))
129        .green()
130        .bold()
131    );
132    println!();
133
134    Ok(())
135}