use super::collection::SQLiteDDL;
use super::ddl::SqliteEntity;
use super::snapshot::{SQLiteSnapshot, SQLiteSnapshotV6};
use std::path::Path;
#[derive(Debug, Clone)]
pub struct SerializerError {
pub message: String,
pub path: Option<String>,
}
impl std::fmt::Display for SerializerError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(path) = &self.path {
write!(f, "Serialization error in '{}': {}", path, self.message)
} else {
write!(f, "Serialization error: {}", self.message)
}
}
}
impl std::error::Error for SerializerError {}
pub type SerializerResult<T> = Result<T, SerializerError>;
#[derive(Debug, Clone)]
pub struct PreparedSnapshots {
pub ddl_prev: SQLiteDDL,
pub ddl_cur: SQLiteDDL,
pub snapshot: SQLiteSnapshot,
pub snapshot_prev: SQLiteSnapshot,
}
pub fn load_snapshot(path: &Path) -> SerializerResult<SQLiteSnapshot> {
let contents = std::fs::read_to_string(path).map_err(|e| SerializerError {
message: format!("Failed to read snapshot file: {}", e),
path: Some(path.display().to_string()),
})?;
if let Ok(snapshot) = serde_json::from_str::<SQLiteSnapshot>(&contents) {
return Ok(snapshot);
}
if let Ok(v6) = serde_json::from_str::<SQLiteSnapshotV6>(&contents) {
return Ok(upgrade_v6_to_v7(v6));
}
Err(SerializerError {
message: "Failed to parse snapshot as v6 or v7 format".to_string(),
path: Some(path.display().to_string()),
})
}
pub fn save_snapshot(snapshot: &SQLiteSnapshot, path: &Path) -> SerializerResult<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|e| SerializerError {
message: format!("Failed to create directory: {}", e),
path: Some(parent.display().to_string()),
})?;
}
let json = serde_json::to_string_pretty(snapshot).map_err(|e| SerializerError {
message: format!("Failed to serialize snapshot: {}", e),
path: Some(path.display().to_string()),
})?;
std::fs::write(path, json).map_err(|e| SerializerError {
message: format!("Failed to write snapshot file: {}", e),
path: Some(path.display().to_string()),
})?;
Ok(())
}
fn upgrade_v6_to_v7(v6: SQLiteSnapshotV6) -> SQLiteSnapshot {
let mut snapshot = SQLiteSnapshot::with_prev_ids(vec![v6.prev_id]);
snapshot.id = v6.id;
snapshot
}
pub fn load_latest_snapshot(drizzle_folder: &Path) -> SerializerResult<Option<SQLiteSnapshot>> {
let meta_folder = drizzle_folder.join("meta");
let journal_path = meta_folder.join("_journal.json");
if !journal_path.exists() {
return Ok(None);
}
let journal_contents = std::fs::read_to_string(&journal_path).map_err(|e| SerializerError {
message: format!("Failed to read journal: {}", e),
path: Some(journal_path.display().to_string()),
})?;
let journal: serde_json::Value =
serde_json::from_str(&journal_contents).map_err(|e| SerializerError {
message: format!("Failed to parse journal: {}", e),
path: Some(journal_path.display().to_string()),
})?;
let entries = journal["entries"]
.as_array()
.ok_or_else(|| SerializerError {
message: "Invalid journal format: missing entries array".to_string(),
path: Some(journal_path.display().to_string()),
})?;
if entries.is_empty() {
return Ok(None);
}
let last_entry = entries.last().unwrap();
let tag = last_entry["tag"].as_str().ok_or_else(|| SerializerError {
message: "Invalid journal entry: missing tag".to_string(),
path: Some(journal_path.display().to_string()),
})?;
let snapshot_path = meta_folder.join(format!("{}_snapshot.json", tag));
load_snapshot(&snapshot_path).map(Some)
}
pub fn find_snapshot_files(drizzle_folder: &Path) -> SerializerResult<Vec<std::path::PathBuf>> {
let meta_folder = drizzle_folder.join("meta");
if !meta_folder.exists() {
return Ok(Vec::new());
}
let mut snapshots = Vec::new();
let entries = std::fs::read_dir(&meta_folder).map_err(|e| SerializerError {
message: format!("Failed to read meta folder: {}", e),
path: Some(meta_folder.display().to_string()),
})?;
for entry in entries.flatten() {
let path = entry.path();
if let Some(name) = path.file_name().and_then(|n| n.to_str())
&& name.ends_with("_snapshot.json")
&& name != "_journal.json"
{
snapshots.push(path);
}
}
snapshots.sort();
Ok(snapshots)
}
pub fn prepare_snapshots(
drizzle_folder: &Path,
current_ddl: SQLiteDDL,
) -> SerializerResult<PreparedSnapshots> {
let snapshot_prev = load_latest_snapshot(drizzle_folder)?.unwrap_or_else(SQLiteSnapshot::new);
let ddl_prev = SQLiteDDL::from_entities(snapshot_prev.ddl.clone());
let mut snapshot = SQLiteSnapshot::with_prev_ids(vec![snapshot_prev.id.clone()]);
snapshot.ddl = current_ddl.to_entities();
Ok(PreparedSnapshots {
ddl_prev,
ddl_cur: current_ddl,
snapshot,
snapshot_prev,
})
}
pub fn empty_snapshot() -> SQLiteSnapshot {
SQLiteSnapshot::new()
}
pub fn ddl_from_entities(entities: Vec<SqliteEntity>) -> SQLiteDDL {
SQLiteDDL::from_entities(entities)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::TempDir;
#[test]
fn test_empty_snapshot() {
let snapshot = empty_snapshot();
assert!(snapshot.ddl.is_empty());
assert_eq!(snapshot.version, "7");
}
#[test]
fn test_save_and_load_snapshot() {
let temp_dir = TempDir::new().unwrap();
let snapshot_path = temp_dir.path().join("test_snapshot.json");
let snapshot = SQLiteSnapshot::new();
save_snapshot(&snapshot, &snapshot_path).unwrap();
let loaded = load_snapshot(&snapshot_path).unwrap();
assert_eq!(loaded.version, snapshot.version);
assert_eq!(loaded.id, snapshot.id);
}
#[test]
fn test_find_snapshot_files() {
let temp_dir = TempDir::new().unwrap();
let meta_dir = temp_dir.path().join("meta");
std::fs::create_dir_all(&meta_dir).unwrap();
let mut f1 = std::fs::File::create(meta_dir.join("0001_snapshot.json")).unwrap();
f1.write_all(b"{}").unwrap();
let mut f2 = std::fs::File::create(meta_dir.join("0002_snapshot.json")).unwrap();
f2.write_all(b"{}").unwrap();
let snapshots = find_snapshot_files(temp_dir.path()).unwrap();
assert_eq!(snapshots.len(), 2);
}
}