dmsc/database/
migration.rs1use serde::{Deserialize, Serialize};
53use std::path::PathBuf;
54
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
56#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
57pub struct DMSCDatabaseMigration {
58 pub version: u32,
59 pub name: String,
60 pub sql_up: String,
61 pub sql_down: Option<String>,
62 pub timestamp: chrono::DateTime<chrono::Utc>,
63}
64
65impl DMSCDatabaseMigration {
66 pub fn new(version: u32, name: &str, sql_up: &str, sql_down: Option<&str>) -> Self {
67 Self {
68 version,
69 name: name.to_string(),
70 sql_up: sql_up.to_string(),
71 sql_down: sql_down.map(|s| s.to_string()),
72 timestamp: chrono::Utc::now(),
73 }
74 }
75}
76
77#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
78pub struct DMSCMigrationHistory {
79 pub version: u32,
80 pub name: String,
81 pub applied_at: chrono::DateTime<chrono::Utc>,
82 pub checksum: String,
83}
84
85impl DMSCMigrationHistory {
86 pub fn new(version: u32, name: &str, checksum: &str) -> Self {
87 Self {
88 version,
89 name: name.to_string(),
90 applied_at: chrono::Utc::now(),
91 checksum: checksum.to_string(),
92 }
93 }
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct DMSCDatabaseMigrator {
98 migrations: Vec<DMSCDatabaseMigration>,
99 migrations_dir: Option<PathBuf>,
100}
101
102impl DMSCDatabaseMigrator {
103 pub fn new() -> Self {
104 Self {
105 migrations: Vec::new(),
106 migrations_dir: None,
107 }
108 }
109
110 pub fn with_migrations_dir(mut self, dir: PathBuf) -> Self {
111 self.migrations_dir = Some(dir);
112 self
113 }
114
115 pub fn add_migration(&mut self, migration: DMSCDatabaseMigration) {
116 self.migrations.push(migration);
117 self.migrations.sort_by(|a, b| a.version.cmp(&b.version));
118 }
119
120 pub fn add_migrations(&mut self, migrations: Vec<DMSCDatabaseMigration>) {
121 self.migrations.extend(migrations);
122 self.migrations.sort_by(|a, b| a.version.cmp(&b.version));
123 }
124
125 pub fn get_migrations(&self) -> &[DMSCDatabaseMigration] {
126 &self.migrations
127 }
128
129 pub fn get_migration(&self, version: u32) -> Option<&DMSCDatabaseMigration> {
130 self.migrations.iter().find(|m| m.version == version)
131 }
132
133 pub fn get_pending_migrations(&self, applied: &[DMSCMigrationHistory]) -> Vec<&DMSCDatabaseMigration> {
134 let applied_versions: std::collections::HashSet<u32> = applied.iter().map(|h| h.version).collect();
135 self.migrations.iter()
136 .filter(|m| !applied_versions.contains(&m.version))
137 .collect()
138 }
139
140 pub fn get_applied_version(&self, applied: &[DMSCMigrationHistory]) -> Option<u32> {
141 applied.iter()
142 .map(|h| h.version)
143 .max()
144 }
145
146 pub fn calculate_checksum(_sql: &str) -> String {
147 use std::collections::hash_map::DefaultHasher;
148 use std::hash::{Hash, Hasher};
149 let mut hasher = DefaultHasher::new();
150 _sql.hash(&mut hasher);
151 format!("{:x}", hasher.finish())
152 }
153
154 pub fn load_migrations_from_dir(&mut self, dir: &str) -> std::io::Result<()> {
155 let path = PathBuf::from(dir);
156 if !path.exists() {
157 return Ok(());
158 }
159
160 let entries = std::fs::read_dir(&path)?;
161 for entry in entries {
162 let entry = entry?;
163 let path = entry.path();
164 if path.is_file() && path.extension().map(|e| e.to_str()) == Some(Some("sql")) {
165 if let Some(file_name) = path.file_stem().and_then(|n| n.to_str()) {
166 let sql_content = std::fs::read_to_string(&path)?;
167 let version: u32 = file_name.split('_').next().unwrap_or("0").parse().unwrap_or(0);
168 let name = file_name.splitn(2, '_').nth(1).unwrap_or(file_name).to_string();
169 self.add_migration(DMSCDatabaseMigration::new(version, &name, &sql_content, None));
170 }
171 }
172 }
173 Ok(())
174 }
175}
176
177impl Default for DMSCDatabaseMigrator {
178 fn default() -> Self {
179 Self::new()
180 }
181}