drizzle_cli/commands/
upgrade.rs1use 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 #[arg(long)]
19 pub dialect: Option<CliDialect>,
20
21 #[arg(long)]
23 pub out: Option<PathBuf>,
24}
25
26pub fn run(config: &Config, db_name: Option<&str>, opts: &UpgradeOptions) -> Result<(), CliError> {
34 let db = config.database(db_name)?;
35
36 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
83fn upgrade_snapshots(out_dir: &Path, dialect: Dialect) -> Result<usize, CliError> {
85 let mut upgraded_count = 0;
86
87 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 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
110fn 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
133fn 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
156fn 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 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 return Ok(false);
175 }
176
177 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 let upgraded = upgrade_to_latest(json, dialect);
203
204 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 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 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(); let snapshots = find_legacy_snapshots(&meta_folder).unwrap();
244 assert_eq!(snapshots.len(), 2);
245 }
246}