Skip to main content

drizzle_cli/commands/
upgrade.rs

1//! Upgrade command - upgrades migration snapshots to the latest version
2//!
3//! This command scans the migrations folder and upgrades any old snapshot
4//! versions to the latest format, matching drizzle-kit's `up` command.
5
6use crate::config::{Config, Dialect as CliDialect};
7use crate::error::CliError;
8use crate::output;
9use drizzle_migrations::upgrade::upgrade_to_latest;
10use drizzle_migrations::version::{is_supported_version, snapshot_version};
11use drizzle_types::Dialect;
12use std::fs;
13use std::path::{Path, PathBuf};
14
15#[derive(clap::Args, Debug, Clone, Default)]
16pub struct UpgradeOptions {
17    /// Override dialect from config
18    #[arg(long)]
19    pub dialect: Option<CliDialect>,
20
21    /// Override output directory
22    #[arg(long)]
23    pub out: Option<PathBuf>,
24}
25
26/// Run the upgrade command.
27///
28/// # Errors
29///
30/// Returns [`CliError`] if the database cannot be resolved, the migration
31/// directory is unreadable, the legacy journal cannot be parsed or migrated
32/// to the v3 format, or writing the upgraded snapshot files to disk fails.
33pub fn run(config: &Config, db_name: Option<&str>, opts: &UpgradeOptions) -> Result<(), CliError> {
34    let db = config.database(db_name)?;
35
36    // CLI flags override config
37    let dialect = opts.dialect.unwrap_or(db.dialect).to_base();
38    let out_dir = opts.out.as_deref().unwrap_or_else(|| db.migrations_dir());
39
40    println!(
41        "{}",
42        output::heading(&format!(
43            "Checking for snapshots to upgrade in {}",
44            out_dir.display()
45        ))
46    );
47
48    if !out_dir.exists() {
49        println!(
50            "{}",
51            output::warning(&format!(
52                "No migrations folder found at {}",
53                out_dir.display()
54            ))
55        );
56        return Ok(());
57    }
58
59    let upgraded = upgrade_snapshots(out_dir, dialect)?;
60
61    if upgraded == 0 {
62        println!(
63            "{}",
64            output::success(&format!(
65                "All snapshots are already at the latest version ({})",
66                snapshot_version(dialect)
67            ))
68        );
69    } else {
70        println!(
71            "{}",
72            output::success(&format!(
73                "Upgraded {} snapshot(s) to version {}",
74                upgraded,
75                snapshot_version(dialect)
76            ))
77        );
78    }
79
80    Ok(())
81}
82
83/// Upgrade all snapshots in a migrations folder
84fn upgrade_snapshots(out_dir: &Path, dialect: Dialect) -> Result<usize, CliError> {
85    let mut upgraded_count = 0;
86
87    // Check for V3 folder-based migrations (each folder has snapshot.json)
88    let v3_snapshots = find_v3_snapshots(out_dir)?;
89
90    for snapshot_path in v3_snapshots {
91        if upgrade_snapshot_file(&snapshot_path, dialect)? {
92            upgraded_count += 1;
93        }
94    }
95
96    // Also check for legacy meta/ folder snapshots
97    let meta_folder = out_dir.join("meta");
98    if meta_folder.exists() {
99        let legacy_snapshots = find_legacy_snapshots(&meta_folder)?;
100        for snapshot_path in legacy_snapshots {
101            if upgrade_snapshot_file(&snapshot_path, dialect)? {
102                upgraded_count += 1;
103            }
104        }
105    }
106
107    Ok(upgraded_count)
108}
109
110/// Find V3 format snapshots (folder/snapshot.json)
111fn find_v3_snapshots(out_dir: &Path) -> Result<Vec<std::path::PathBuf>, CliError> {
112    let mut snapshots = Vec::new();
113
114    if !out_dir.exists() {
115        return Ok(snapshots);
116    }
117
118    for entry in fs::read_dir(out_dir).map_err(|e| CliError::IoError(e.to_string()))? {
119        let entry = entry.map_err(|e| CliError::IoError(e.to_string()))?;
120        let path = entry.path();
121
122        if path.is_dir() {
123            let snapshot_path = path.join("snapshot.json");
124            if snapshot_path.exists() {
125                snapshots.push(snapshot_path);
126            }
127        }
128    }
129
130    Ok(snapshots)
131}
132
133/// Find legacy format snapshots (meta/*_snapshot.json)
134fn find_legacy_snapshots(meta_folder: &Path) -> Result<Vec<std::path::PathBuf>, CliError> {
135    let mut snapshots = Vec::new();
136
137    if !meta_folder.exists() {
138        return Ok(snapshots);
139    }
140
141    for entry in fs::read_dir(meta_folder).map_err(|e| CliError::IoError(e.to_string()))? {
142        let entry = entry.map_err(|e| CliError::IoError(e.to_string()))?;
143        let path = entry.path();
144
145        if path.is_file()
146            && let Some(name) = path.file_name().and_then(|n| n.to_str())
147            && name.ends_with("_snapshot.json")
148        {
149            snapshots.push(path);
150        }
151    }
152
153    Ok(snapshots)
154}
155
156/// Upgrade a single snapshot file if needed
157/// Returns true if the file was upgraded, false if already at latest version
158fn upgrade_snapshot_file(path: &Path, dialect: Dialect) -> Result<bool, CliError> {
159    let contents = fs::read_to_string(path).map_err(|e| CliError::IoError(e.to_string()))?;
160
161    let json: serde_json::Value = serde_json::from_str(&contents)
162        .map_err(|e| CliError::Other(format!("Invalid JSON in {}: {}", path.display(), e)))?;
163
164    // Get current version
165    let version = json
166        .get("version")
167        .and_then(|v| v.as_str())
168        .unwrap_or("unknown");
169
170    let latest_version = snapshot_version(dialect);
171
172    if version == latest_version {
173        // Already at latest version
174        return Ok(false);
175    }
176
177    // Check if version is supported for upgrade
178    let version_num: u32 = version.parse().unwrap_or(0);
179    if !is_supported_version(dialect, version) && version_num > 0 {
180        println!(
181            "{}",
182            output::warning(&format!(
183                "Skipping {}: version {} is not supported for upgrade",
184                path.display(),
185                version
186            ))
187        );
188        return Ok(false);
189    }
190
191    println!(
192        "{}",
193        output::info(&format!(
194            "Upgrading {} from version {} to {}",
195            path.display(),
196            version,
197            latest_version
198        ))
199    );
200
201    // Upgrade the snapshot
202    let upgraded = upgrade_to_latest(json, dialect);
203
204    // Write back
205    let upgraded_json = serde_json::to_string_pretty(&upgraded)
206        .map_err(|e| CliError::Other(format!("Failed to serialize upgraded snapshot: {e}")))?;
207
208    fs::write(path, upgraded_json).map_err(|e| CliError::IoError(e.to_string()))?;
209
210    Ok(true)
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216    use tempfile::TempDir;
217
218    #[test]
219    fn test_find_v3_snapshots() {
220        let temp_dir = TempDir::new().unwrap();
221
222        // Create a V3 migration folder
223        let migration_folder = temp_dir.path().join("20231220_initial");
224        fs::create_dir_all(&migration_folder).unwrap();
225        fs::write(migration_folder.join("snapshot.json"), "{}").unwrap();
226        fs::write(migration_folder.join("migration.sql"), "").unwrap();
227
228        let snapshots = find_v3_snapshots(temp_dir.path()).unwrap();
229        assert_eq!(snapshots.len(), 1);
230    }
231
232    #[test]
233    fn test_find_legacy_snapshots() {
234        let temp_dir = TempDir::new().unwrap();
235
236        // Create a legacy meta folder
237        let meta_folder = temp_dir.path().join("meta");
238        fs::create_dir_all(&meta_folder).unwrap();
239        fs::write(meta_folder.join("0000_initial_snapshot.json"), "{}").unwrap();
240        fs::write(meta_folder.join("0001_add_users_snapshot.json"), "{}").unwrap();
241        fs::write(meta_folder.join("_journal.json"), "{}").unwrap(); // Should not be included
242
243        let snapshots = find_legacy_snapshots(&meta_folder).unwrap();
244        assert_eq!(snapshots.len(), 2);
245    }
246}