Skip to main content

drizzle_migrations/sqlite/
serializer.rs

1//! `SQLite` schema serialization
2//!
3//! This module provides functionality to serialize Drizzle schema definitions
4//! into DDL entities and snapshots.
5
6use super::collection::SQLiteDDL;
7use super::ddl::SqliteEntity;
8use super::snapshot::SQLiteSnapshot;
9use std::path::Path;
10
11/// Error type for serialization operations
12#[derive(Debug, Clone)]
13pub struct SerializerError {
14    pub message: String,
15    pub path: Option<String>,
16}
17
18impl std::fmt::Display for SerializerError {
19    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20        if let Some(path) = &self.path {
21            write!(f, "Serialization error in '{}': {}", path, self.message)
22        } else {
23            write!(f, "Serialization error: {}", self.message)
24        }
25    }
26}
27
28impl std::error::Error for SerializerError {}
29
30/// Result type for serialization
31pub type SerializerResult<T> = Result<T, SerializerError>;
32
33/// Result of preparing `SQLite` snapshots for migration
34#[derive(Debug, Clone)]
35pub struct PreparedSnapshots {
36    /// Previous DDL state
37    pub ddl_prev: SQLiteDDL,
38    /// Current DDL state
39    pub ddl_cur: SQLiteDDL,
40    /// Current snapshot to be written
41    pub snapshot: SQLiteSnapshot,
42    /// Previous snapshot (read from file)
43    pub snapshot_prev: SQLiteSnapshot,
44}
45
46/// Load a snapshot from a JSON file.
47///
48/// # Errors
49///
50/// Returns a [`SerializerError`] if the file cannot be read or the contents
51/// cannot be parsed as a v7 [`SQLiteSnapshot`], either directly or after
52/// running the legacy (v5/v6) structural upgrade chain.
53pub fn load_snapshot(path: &Path) -> SerializerResult<SQLiteSnapshot> {
54    let contents = std::fs::read_to_string(path).map_err(|e| SerializerError {
55        message: format!("Failed to read snapshot file: {e}"),
56        path: Some(path.display().to_string()),
57    })?;
58
59    // Try parsing as v7 first
60    if let Ok(snapshot) = serde_json::from_str::<SQLiteSnapshot>(&contents) {
61        return Ok(snapshot);
62    }
63
64    // Legacy object-format snapshot (v5/v6): run the structural upgrade
65    // chain and parse the result.
66    if let Ok(legacy) = serde_json::from_str::<serde_json::Value>(&contents) {
67        let upgraded = crate::upgrade::upgrade_to_latest(legacy, drizzle_types::Dialect::SQLite);
68        if let Ok(snapshot) = serde_json::from_value::<SQLiteSnapshot>(upgraded) {
69            return Ok(snapshot);
70        }
71    }
72
73    Err(SerializerError {
74        message:
75            "Failed to parse snapshot as a v7 document or upgrade it from the legacy v5/v6 format"
76                .to_string(),
77        path: Some(path.display().to_string()),
78    })
79}
80
81/// Save a snapshot to a JSON file.
82///
83/// # Errors
84///
85/// Returns a [`SerializerError`] if the parent directory cannot be created,
86/// the snapshot cannot be serialized, or the file cannot be written.
87pub fn save_snapshot(snapshot: &SQLiteSnapshot, path: &Path) -> SerializerResult<()> {
88    // Create parent directories if needed
89    if let Some(parent) = path.parent() {
90        std::fs::create_dir_all(parent).map_err(|e| SerializerError {
91            message: format!("Failed to create directory: {e}"),
92            path: Some(parent.display().to_string()),
93        })?;
94    }
95
96    let json = serde_json::to_string_pretty(snapshot).map_err(|e| SerializerError {
97        message: format!("Failed to serialize snapshot: {e}"),
98        path: Some(path.display().to_string()),
99    })?;
100
101    std::fs::write(path, json).map_err(|e| SerializerError {
102        message: format!("Failed to write snapshot file: {e}"),
103        path: Some(path.display().to_string()),
104    })?;
105
106    Ok(())
107}
108
109/// Load the latest snapshot from a drizzle folder.
110///
111/// # Errors
112///
113/// Returns a [`SerializerError`] if the folder cannot be scanned or the
114/// latest snapshot file cannot be read/parsed.
115pub fn load_latest_snapshot(drizzle_folder: &Path) -> SerializerResult<Option<SQLiteSnapshot>> {
116    let snapshots = find_snapshot_files(drizzle_folder)?;
117    snapshots.last().map(|path| load_snapshot(path)).transpose()
118}
119
120/// Find all snapshot files in a drizzle folder.
121///
122/// # Errors
123///
124/// Returns a [`SerializerError`] if the folder exists but cannot be read.
125pub fn find_snapshot_files(drizzle_folder: &Path) -> SerializerResult<Vec<std::path::PathBuf>> {
126    if !drizzle_folder.exists() {
127        return Ok(Vec::new());
128    }
129
130    let mut snapshots = Vec::new();
131
132    let entries = std::fs::read_dir(drizzle_folder).map_err(|e| SerializerError {
133        message: format!("Failed to read migrations folder: {e}"),
134        path: Some(drizzle_folder.display().to_string()),
135    })?;
136
137    for entry in entries.flatten() {
138        let path = entry.path();
139        if !entry.file_type().is_ok_and(|t| t.is_dir()) {
140            continue;
141        }
142
143        let snapshot_path = path.join("snapshot.json");
144        if snapshot_path.exists() {
145            snapshots.push(snapshot_path);
146        }
147    }
148
149    // Sort by parent folder name (which includes timestamp)
150    snapshots.sort();
151
152    Ok(snapshots)
153}
154
155/// Prepare snapshots for migration generation.
156///
157/// # Errors
158///
159/// Returns a [`SerializerError`] if loading the previous snapshot from
160/// `drizzle_folder` fails.
161pub fn prepare_snapshots(
162    drizzle_folder: &Path,
163    current_ddl: SQLiteDDL,
164) -> SerializerResult<PreparedSnapshots> {
165    // Load previous snapshot if exists
166    let snapshot_prev = load_latest_snapshot(drizzle_folder)?.unwrap_or_else(SQLiteSnapshot::new);
167
168    // Build DDL from previous snapshot
169    let ddl_prev = SQLiteDDL::from_entities(snapshot_prev.ddl.clone());
170
171    // Create new snapshot from current DDL
172    let mut snapshot = SQLiteSnapshot::with_prev_ids(vec![snapshot_prev.id.clone()]);
173    snapshot.ddl = current_ddl.to_entities();
174
175    Ok(PreparedSnapshots {
176        ddl_prev,
177        ddl_cur: current_ddl,
178        snapshot,
179        snapshot_prev,
180    })
181}
182
183/// Create an empty/dry snapshot (for initial migrations)
184#[must_use]
185pub fn empty_snapshot() -> SQLiteSnapshot {
186    SQLiteSnapshot::new()
187}
188
189/// Create a DDL from a list of entities
190#[must_use]
191pub fn ddl_from_entities(entities: Vec<SqliteEntity>) -> SQLiteDDL {
192    SQLiteDDL::from_entities(entities)
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198    use std::io::Write;
199    use tempfile::TempDir;
200
201    #[test]
202    fn test_empty_snapshot() {
203        let snapshot = empty_snapshot();
204        assert!(snapshot.ddl.is_empty());
205        assert_eq!(snapshot.version, "7");
206    }
207
208    #[test]
209    fn test_save_and_load_snapshot() {
210        let temp_dir = TempDir::new().unwrap();
211        let snapshot_path = temp_dir.path().join("test_snapshot.json");
212
213        let snapshot = SQLiteSnapshot::new();
214        save_snapshot(&snapshot, &snapshot_path).unwrap();
215
216        let loaded = load_snapshot(&snapshot_path).unwrap();
217        assert_eq!(loaded.version, snapshot.version);
218        assert_eq!(loaded.id, snapshot.id);
219    }
220
221    #[test]
222    fn test_find_snapshot_files() {
223        let temp_dir = TempDir::new().unwrap();
224        let mig1 = temp_dir.path().join("0001_first");
225        let mig2 = temp_dir.path().join("0002_second");
226        std::fs::create_dir_all(&mig1).unwrap();
227        std::fs::create_dir_all(&mig2).unwrap();
228
229        // Create some snapshot files
230        let mut f1 = std::fs::File::create(mig1.join("snapshot.json")).unwrap();
231        f1.write_all(b"{}").unwrap();
232
233        let mut f2 = std::fs::File::create(mig2.join("snapshot.json")).unwrap();
234        f2.write_all(b"{}").unwrap();
235
236        let snapshots = find_snapshot_files(temp_dir.path()).unwrap();
237        assert_eq!(snapshots.len(), 2);
238    }
239}