dmsc/database/
migration.rs

1//! Copyright © 2025-2026 Wenze Wei. All Rights Reserved.
2//!
3//! This file is part of DMSC.
4//! The DMSC project belongs to the Dunimd Team.
5//!
6//! Licensed under the Apache License, Version 2.0 (the "License");
7//! You may not use this file except in compliance with the License.
8//! You may obtain a copy of the License at
9//!
10//!     http://www.apache.org/licenses/LICENSE-2.0
11//!
12//! Unless required by applicable law or agreed to in writing, software
13//! distributed under the License is distributed on an "AS IS" BASIS,
14//! WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15//! See the License for the specific language governing permissions and
16//! limitations under the License.
17
18//! # Database Migration
19//!
20//! This module provides database migration and schema management functionality for DMSC.
21//! It handles version-controlled schema changes with support for up and down migrations.
22//!
23//! ## Key Components
24//!
25//! - **Migration**: Struct representing a single migration with version, name, and SQL statements
26//! - **MigrationManager**: Manages migration lifecycle and execution
27//!
28//! ## Design Principles
29//!
30//! 1. **Version Control**: Each migration has a unique version number
31//! 2. **Idempotent Operations**: Migrations can be safely run multiple times
32//! 3. **Transactional Safety**: Migrations execute within transactions
33//! 4. **Bidirectional Changes**: Support for both up and down migrations
34//! 5. **History Tracking**: Maintain migration execution history
35//!
36//! ## Usage Example
37//!
38//! ```rust,ignore
39//! use dmsc::database::migration::{Migration, MigrationManager};
40//!
41//! let migration = Migration::new(
42//!     1,
43//!     "create_users_table",
44//!     "CREATE TABLE users (id INT PRIMARY KEY, name TEXT)",
45//!     "DROP TABLE users",
46//! );
47//!
48//! let manager = MigrationManager::new(migrations);
49//! manager.migrate().await?;
50//! ```
51
52use 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}