toasty_cli/migration/
history_file.rs

1use anyhow::{Result, bail};
2use serde::{Deserialize, Serialize};
3use std::fmt;
4use std::path::Path;
5use std::str::FromStr;
6
7const HISTORY_FILE_VERSION: u32 = 1;
8
9/// History file containing the record of all applied migrations
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct HistoryFile {
12    /// History file format version
13    version: u32,
14
15    /// Migration history
16    migrations: Vec<HistoryFileMigration>,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct HistoryFileMigration {
21    /// Random unique identifier for this migration.
22    pub id: u64,
23
24    /// Migration name/identifier.
25    pub name: String,
26
27    /// Name of the snapshot generated alongside this migration.
28    pub snapshot_name: String,
29
30    /// Optional checksum of the migration file to detect changes
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub checksum: Option<String>,
33}
34
35impl HistoryFile {
36    /// Create a new empty history file
37    pub fn new() -> Self {
38        Self {
39            version: HISTORY_FILE_VERSION,
40            migrations: Vec::new(),
41        }
42    }
43
44    /// Load a history file from a TOML file
45    pub fn load(path: impl AsRef<Path>) -> Result<Self> {
46        let contents = std::fs::read_to_string(path.as_ref())?;
47        contents.parse()
48    }
49
50    /// Save the history file to a TOML file
51    pub fn save(&self, path: impl AsRef<Path>) -> Result<()> {
52        std::fs::write(path.as_ref(), self.to_string())?;
53        Ok(())
54    }
55
56    /// Loads the history file, or returns an empty one if it does not exist
57    pub fn load_or_default(path: impl AsRef<Path>) -> Result<Self> {
58        if std::fs::exists(&path)? {
59            return Self::load(path);
60        }
61        Ok(Self::default())
62    }
63
64    pub fn migrations(&self) -> &[HistoryFileMigration] {
65        &self.migrations
66    }
67
68    /// Get the next migration number by parsing the last migration's name
69    pub fn next_migration_number(&self) -> u32 {
70        self.migrations
71            .last()
72            .and_then(|m| {
73                // Extract the first 4 digits from the migration name (e.g., "0001_migration.sql" -> 1)
74                m.name.split('_').next()?.parse::<u32>().ok()
75            })
76            .map(|n| n + 1)
77            .unwrap_or(0)
78    }
79
80    /// Add a migration to the history
81    pub fn add_migration(&mut self, migration: HistoryFileMigration) {
82        self.migrations.push(migration);
83    }
84
85    /// Remove a migration from the history by index
86    pub fn remove_migration(&mut self, index: usize) {
87        self.migrations.remove(index);
88    }
89}
90
91impl Default for HistoryFile {
92    fn default() -> Self {
93        Self::new()
94    }
95}
96
97impl FromStr for HistoryFile {
98    type Err = anyhow::Error;
99
100    fn from_str(s: &str) -> Result<Self> {
101        let file: HistoryFile = toml::from_str(s)?;
102
103        // Validate version
104        if file.version != HISTORY_FILE_VERSION {
105            bail!(
106                "Unsupported history file version: {}. Expected version {}",
107                file.version,
108                HISTORY_FILE_VERSION
109            );
110        }
111
112        Ok(file)
113    }
114}
115
116impl fmt::Display for HistoryFile {
117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118        let toml_str = toml::to_string_pretty(self).map_err(|_| fmt::Error)?;
119        write!(f, "{}", toml_str)
120    }
121}