drizzle_cli/commands/
upgrade.rs1use 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
15pub 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 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
71fn upgrade_snapshots(out_dir: &Path, dialect: Dialect) -> Result<usize, CliError> {
73 let mut upgraded_count = 0;
74
75 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 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
98fn 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
121fn 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
144fn 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 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 return Ok(false);
163 }
164
165 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 let upgraded = upgrade_to_latest(json, dialect);
191
192 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 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 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(); let snapshots = find_legacy_snapshots(&meta_folder).unwrap();
232 assert_eq!(snapshots.len(), 2);
233 }
234}