1#![allow(dead_code)]
2use std::collections::HashMap;
9use std::fmt;
10
11#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
13pub struct SchemaVersion {
14 pub major: u32,
16 pub minor: u32,
18 pub patch: u32,
20}
21
22impl SchemaVersion {
23 pub fn new(major: u32, minor: u32, patch: u32) -> Self {
25 Self {
26 major,
27 minor,
28 patch,
29 }
30 }
31
32 pub fn is_compatible_with(&self, other: &Self) -> bool {
34 self.major == other.major
35 }
36
37 pub fn is_newer_than(&self, other: &Self) -> bool {
39 self > other
40 }
41}
42
43impl fmt::Display for SchemaVersion {
44 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
46 }
47}
48
49#[derive(Debug, Clone, PartialEq)]
51pub enum MigrationError {
52 NoPath {
54 from: SchemaVersion,
56 to: SchemaVersion,
58 },
59 StepFailed {
61 step: String,
63 reason: String,
65 },
66 AlreadyCurrent,
68 DowngradeNotSupported,
70 ValidationFailed(String),
72}
73
74impl fmt::Display for MigrationError {
75 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76 match self {
77 Self::NoPath { from, to } => {
78 write!(f, "no migration path from {from} to {to}")
79 }
80 Self::StepFailed { step, reason } => {
81 write!(f, "migration step '{step}' failed: {reason}")
82 }
83 Self::AlreadyCurrent => write!(f, "already at target version"),
84 Self::DowngradeNotSupported => write!(f, "downgrade not supported"),
85 Self::ValidationFailed(msg) => write!(f, "validation failed: {msg}"),
86 }
87 }
88}
89
90#[derive(Debug, Clone, PartialEq)]
92pub enum FieldChange {
93 AddField {
95 name: String,
97 default: String,
99 },
100 RemoveField {
102 name: String,
104 },
105 RenameField {
107 from: String,
109 to: String,
111 },
112 ChangeType {
114 name: String,
116 description: String,
118 },
119}
120
121#[derive(Debug, Clone)]
123pub struct MigrationStep {
124 pub from: SchemaVersion,
126 pub to: SchemaVersion,
128 pub description: String,
130 pub changes: Vec<FieldChange>,
132}
133
134impl MigrationStep {
135 pub fn new(from: SchemaVersion, to: SchemaVersion, description: impl Into<String>) -> Self {
137 Self {
138 from,
139 to,
140 description: description.into(),
141 changes: Vec::new(),
142 }
143 }
144
145 pub fn add_change(&mut self, change: FieldChange) {
147 self.changes.push(change);
148 }
149
150 pub fn apply(&self, definition: &mut HashMap<String, String>) -> Result<(), MigrationError> {
152 for change in &self.changes {
153 match change {
154 FieldChange::AddField { name, default } => {
155 definition
156 .entry(name.clone())
157 .or_insert_with(|| default.clone());
158 }
159 FieldChange::RemoveField { name } => {
160 definition.remove(name);
161 }
162 FieldChange::RenameField { from, to } => {
163 if let Some(value) = definition.remove(from) {
164 definition.insert(to.clone(), value);
165 }
166 }
167 FieldChange::ChangeType {
168 name,
169 description: _,
170 } => {
171 if !definition.contains_key(name) {
172 return Err(MigrationError::StepFailed {
173 step: self.description.clone(),
174 reason: format!("field '{name}' not found for type change"),
175 });
176 }
177 }
178 }
179 }
180 definition.insert("_schema_version".to_string(), self.to.to_string());
182 Ok(())
183 }
184}
185
186#[derive(Debug, Clone)]
188pub struct MigrationRegistry {
189 steps: Vec<MigrationStep>,
191}
192
193impl MigrationRegistry {
194 pub fn new() -> Self {
196 Self { steps: Vec::new() }
197 }
198
199 pub fn register(&mut self, step: MigrationStep) {
201 self.steps.push(step);
202 }
203
204 pub fn find_path(
206 &self,
207 from: &SchemaVersion,
208 to: &SchemaVersion,
209 ) -> Result<Vec<&MigrationStep>, MigrationError> {
210 if from == to {
211 return Err(MigrationError::AlreadyCurrent);
212 }
213 if from > to {
214 return Err(MigrationError::DowngradeNotSupported);
215 }
216
217 let mut path = Vec::new();
218 let mut current = from.clone();
219
220 while current != *to {
221 let step = self
222 .steps
223 .iter()
224 .find(|s| s.from == current)
225 .ok_or_else(|| MigrationError::NoPath {
226 from: current.clone(),
227 to: to.clone(),
228 })?;
229 if step.to > *to {
230 return Err(MigrationError::NoPath {
231 from: from.clone(),
232 to: to.clone(),
233 });
234 }
235 current = step.to.clone();
236 path.push(step);
237 }
238
239 Ok(path)
240 }
241
242 pub fn migrate(
244 &self,
245 definition: &mut HashMap<String, String>,
246 from: &SchemaVersion,
247 to: &SchemaVersion,
248 ) -> Result<Vec<String>, MigrationError> {
249 let path = self.find_path(from, to)?;
250 let mut descriptions = Vec::new();
251 for step in path {
252 step.apply(definition)?;
253 descriptions.push(step.description.clone());
254 }
255 Ok(descriptions)
256 }
257
258 pub fn step_count(&self) -> usize {
260 self.steps.len()
261 }
262
263 pub fn known_versions(&self) -> Vec<SchemaVersion> {
265 let mut versions: Vec<SchemaVersion> = self
266 .steps
267 .iter()
268 .flat_map(|s| vec![s.from.clone(), s.to.clone()])
269 .collect();
270 versions.sort();
271 versions.dedup();
272 versions
273 }
274}
275
276impl Default for MigrationRegistry {
277 fn default() -> Self {
278 Self::new()
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285
286 fn v(major: u32, minor: u32, patch: u32) -> SchemaVersion {
287 SchemaVersion::new(major, minor, patch)
288 }
289
290 #[test]
291 fn test_version_display() {
292 assert_eq!(v(1, 2, 3).to_string(), "1.2.3");
293 assert_eq!(v(0, 0, 1).to_string(), "0.0.1");
294 }
295
296 #[test]
297 fn test_version_compatibility() {
298 assert!(v(1, 0, 0).is_compatible_with(&v(1, 5, 3)));
299 assert!(!v(1, 0, 0).is_compatible_with(&v(2, 0, 0)));
300 }
301
302 #[test]
303 fn test_version_ordering() {
304 assert!(v(1, 0, 0).is_newer_than(&v(0, 9, 9)));
305 assert!(v(1, 1, 0).is_newer_than(&v(1, 0, 9)));
306 assert!(!v(1, 0, 0).is_newer_than(&v(1, 0, 0)));
307 }
308
309 #[test]
310 fn test_migration_step_add_field() {
311 let mut step = MigrationStep::new(v(1, 0, 0), v(1, 1, 0), "add priority");
312 step.add_change(FieldChange::AddField {
313 name: "priority".into(),
314 default: "normal".into(),
315 });
316
317 let mut def = HashMap::new();
318 def.insert("name".into(), "test".into());
319 step.apply(&mut def).expect("should succeed in test");
320 assert_eq!(
321 def.get("priority").expect("should succeed in test"),
322 "normal"
323 );
324 assert_eq!(
325 def.get("_schema_version").expect("should succeed in test"),
326 "1.1.0"
327 );
328 }
329
330 #[test]
331 fn test_migration_step_remove_field() {
332 let mut step = MigrationStep::new(v(1, 0, 0), v(1, 1, 0), "remove legacy");
333 step.add_change(FieldChange::RemoveField {
334 name: "legacy_flag".into(),
335 });
336
337 let mut def = HashMap::new();
338 def.insert("legacy_flag".into(), "true".into());
339 step.apply(&mut def).expect("should succeed in test");
340 assert!(!def.contains_key("legacy_flag"));
341 }
342
343 #[test]
344 fn test_migration_step_rename_field() {
345 let mut step = MigrationStep::new(v(1, 0, 0), v(1, 1, 0), "rename field");
346 step.add_change(FieldChange::RenameField {
347 from: "old_name".into(),
348 to: "new_name".into(),
349 });
350
351 let mut def = HashMap::new();
352 def.insert("old_name".into(), "value".into());
353 step.apply(&mut def).expect("should succeed in test");
354 assert!(!def.contains_key("old_name"));
355 assert_eq!(
356 def.get("new_name").expect("should succeed in test"),
357 "value"
358 );
359 }
360
361 #[test]
362 fn test_change_type_missing_field() {
363 let mut step = MigrationStep::new(v(1, 0, 0), v(1, 1, 0), "change type");
364 step.add_change(FieldChange::ChangeType {
365 name: "missing".into(),
366 description: "int -> str".into(),
367 });
368
369 let mut def = HashMap::new();
370 let result = step.apply(&mut def);
371 assert!(matches!(result, Err(MigrationError::StepFailed { .. })));
372 }
373
374 #[test]
375 fn test_registry_find_path() {
376 let mut reg = MigrationRegistry::new();
377 reg.register(MigrationStep::new(v(1, 0, 0), v(1, 1, 0), "step1"));
378 reg.register(MigrationStep::new(v(1, 1, 0), v(1, 2, 0), "step2"));
379
380 let path = reg
381 .find_path(&v(1, 0, 0), &v(1, 2, 0))
382 .expect("should succeed in test");
383 assert_eq!(path.len(), 2);
384 assert_eq!(path[0].description, "step1");
385 assert_eq!(path[1].description, "step2");
386 }
387
388 #[test]
389 fn test_registry_already_current() {
390 let reg = MigrationRegistry::new();
391 let result = reg.find_path(&v(1, 0, 0), &v(1, 0, 0));
392 assert!(matches!(result, Err(MigrationError::AlreadyCurrent)));
393 }
394
395 #[test]
396 fn test_registry_downgrade_not_supported() {
397 let reg = MigrationRegistry::new();
398 let result = reg.find_path(&v(2, 0, 0), &v(1, 0, 0));
399 assert!(matches!(result, Err(MigrationError::DowngradeNotSupported)));
400 }
401
402 #[test]
403 fn test_registry_no_path() {
404 let mut reg = MigrationRegistry::new();
405 reg.register(MigrationStep::new(v(1, 0, 0), v(1, 1, 0), "step1"));
406 let result = reg.find_path(&v(1, 0, 0), &v(1, 3, 0));
407 assert!(matches!(result, Err(MigrationError::NoPath { .. })));
408 }
409
410 #[test]
411 fn test_full_migration() {
412 let mut reg = MigrationRegistry::new();
413 let mut s1 = MigrationStep::new(v(1, 0, 0), v(1, 1, 0), "add timeout");
414 s1.add_change(FieldChange::AddField {
415 name: "timeout".into(),
416 default: "30".into(),
417 });
418 let mut s2 = MigrationStep::new(v(1, 1, 0), v(1, 2, 0), "rename output");
419 s2.add_change(FieldChange::RenameField {
420 from: "dest".into(),
421 to: "output_path".into(),
422 });
423 reg.register(s1);
424 reg.register(s2);
425
426 let mut def = HashMap::new();
427 def.insert("name".into(), "wf1".into());
428 def.insert("dest".into(), "/out".into());
429
430 let descs = reg
431 .migrate(&mut def, &v(1, 0, 0), &v(1, 2, 0))
432 .expect("should succeed in test");
433 assert_eq!(descs.len(), 2);
434 assert_eq!(def.get("timeout").expect("should succeed in test"), "30");
435 assert_eq!(
436 def.get("output_path").expect("should succeed in test"),
437 "/out"
438 );
439 assert!(!def.contains_key("dest"));
440 }
441
442 #[test]
443 fn test_known_versions() {
444 let mut reg = MigrationRegistry::new();
445 reg.register(MigrationStep::new(v(1, 0, 0), v(1, 1, 0), "s1"));
446 reg.register(MigrationStep::new(v(1, 1, 0), v(2, 0, 0), "s2"));
447
448 let versions = reg.known_versions();
449 assert_eq!(versions.len(), 3);
450 assert_eq!(versions[0], v(1, 0, 0));
451 assert_eq!(versions[1], v(1, 1, 0));
452 assert_eq!(versions[2], v(2, 0, 0));
453 }
454
455 #[test]
456 fn test_step_count() {
457 let mut reg = MigrationRegistry::new();
458 assert_eq!(reg.step_count(), 0);
459 reg.register(MigrationStep::new(v(1, 0, 0), v(1, 1, 0), "s1"));
460 assert_eq!(reg.step_count(), 1);
461 }
462}