Skip to main content

toasty_core/
migration.rs

1use crate::{Error, Result};
2use serde::{Deserialize, Serialize};
3use std::fmt;
4use std::path::Path;
5use std::str::FromStr;
6
7const HISTORY_VERSION: u32 = 1;
8
9/// A TOML-serializable record of all migrations that have been generated.
10///
11/// The history file lives at `<migration_path>/history.toml` and is the
12/// source of truth for which migrations exist and what order they were
13/// created in. Each entry is a [`HistoryEntry`].
14///
15/// The file carries a version number. [`History::load`] and the [`FromStr`]
16/// implementation reject files whose version does not match the current
17/// format.
18///
19/// # Examples
20///
21/// ```
22/// use toasty_core::migration::{History, HistoryEntry};
23///
24/// let mut history = History::new();
25/// assert_eq!(history.next_migration_number(), 0);
26///
27/// history.add_entry(HistoryEntry {
28///     id: 100,
29///     name: "0000_init.sql".to_string(),
30///     snapshot_name: "0000_snapshot.toml".to_string(),
31///     checksum: None,
32/// });
33/// assert_eq!(history.next_migration_number(), 1);
34/// assert_eq!(history.entries().len(), 1);
35///
36/// // Round-trip through TOML serialization
37/// let serialized = history.to_string();
38/// let restored: History = serialized.parse().unwrap();
39/// assert_eq!(restored.entries()[0].id, 100);
40/// ```
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct History {
43    /// History file format version
44    version: u32,
45
46    /// Migration history
47    #[serde(rename = "migrations")]
48    entries: Vec<HistoryEntry>,
49}
50
51/// A single entry in the migration history.
52///
53/// Each entry records the randomly-assigned ID used by the database driver to
54/// track application status, the migration SQL file name, the companion
55/// snapshot file name, and an optional checksum.
56///
57/// # Examples
58///
59/// ```
60/// use toasty_core::migration::HistoryEntry;
61///
62/// let entry = HistoryEntry {
63///     id: 42,
64///     name: "0001_create_users.sql".to_string(),
65///     snapshot_name: "0001_snapshot.toml".to_string(),
66///     checksum: None,
67/// };
68/// assert_eq!(entry.id, 42);
69/// assert_eq!(entry.name, "0001_create_users.sql");
70/// ```
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct HistoryEntry {
73    /// Random unique identifier for this migration.
74    pub id: u64,
75
76    /// Migration name/identifier.
77    pub name: String,
78
79    /// Name of the snapshot generated alongside this migration.
80    pub snapshot_name: String,
81
82    /// Optional checksum of the migration file to detect changes
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub checksum: Option<String>,
85}
86
87impl History {
88    /// Create a new empty history.
89    pub fn new() -> Self {
90        Self {
91            version: HISTORY_VERSION,
92            entries: Vec::new(),
93        }
94    }
95
96    /// Load history from a TOML file.
97    pub fn load(path: impl AsRef<Path>) -> Result<Self> {
98        let contents = std::fs::read_to_string(path.as_ref())?;
99        contents.parse()
100    }
101
102    /// Save the history to a TOML file.
103    pub fn save(&self, path: impl AsRef<Path>) -> Result<()> {
104        std::fs::write(path.as_ref(), self.to_string())?;
105        Ok(())
106    }
107
108    /// Loads the history file, or returns an empty one if it does not exist.
109    pub fn load_or_default(path: impl AsRef<Path>) -> Result<Self> {
110        let path = path.as_ref();
111        if std::fs::exists(path)? {
112            return Self::load(path);
113        }
114        Ok(Self::default())
115    }
116
117    /// Returns the ordered list of entries in this history.
118    ///
119    /// Entries appear in the order they were added. An empty slice means no
120    /// migrations have been recorded yet.
121    ///
122    /// # Examples
123    ///
124    /// ```
125    /// use toasty_core::migration::{History, HistoryEntry};
126    ///
127    /// let mut history = History::new();
128    /// assert!(history.entries().is_empty());
129    ///
130    /// history.add_entry(HistoryEntry {
131    ///     id: 1,
132    ///     name: "0001_init.sql".to_string(),
133    ///     snapshot_name: "0001_snapshot.toml".to_string(),
134    ///     checksum: None,
135    /// });
136    /// assert_eq!(history.entries().len(), 1);
137    /// assert_eq!(history.entries()[0].name, "0001_init.sql");
138    /// ```
139    pub fn entries(&self) -> &[HistoryEntry] {
140        &self.entries
141    }
142
143    /// Get the next migration number by parsing the last entry's name.
144    pub fn next_migration_number(&self) -> u32 {
145        self.entries
146            .last()
147            .and_then(|m| m.name.split('_').next()?.parse::<u32>().ok())
148            .map(|n| n + 1)
149            .unwrap_or(0)
150    }
151
152    /// Add an entry to the history.
153    pub fn add_entry(&mut self, entry: HistoryEntry) {
154        self.entries.push(entry);
155    }
156
157    /// Remove an entry from the history by index.
158    pub fn remove_entry(&mut self, index: usize) {
159        self.entries.remove(index);
160    }
161}
162
163impl Default for History {
164    fn default() -> Self {
165        Self::new()
166    }
167}
168
169impl FromStr for History {
170    type Err = Error;
171
172    fn from_str(s: &str) -> Result<Self> {
173        let history: History =
174            toml::from_str(s).map_err(|err| Error::from_args(format_args!("{err}")))?;
175
176        if history.version != HISTORY_VERSION {
177            return Err(Error::from_args(format_args!(
178                "unsupported history file version: {}. Expected version {}",
179                history.version, HISTORY_VERSION
180            )));
181        }
182
183        Ok(history)
184    }
185}
186
187impl fmt::Display for History {
188    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189        let toml_str = toml::to_string_pretty(self).map_err(|_| fmt::Error)?;
190        write!(f, "{}", toml_str)
191    }
192}