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