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}