Files
mixvideo-v2/apps/desktop/src-tauri/src/infrastructure/database/migrations.rs
imeepos 4291230acb fix: 修复React渲染错误和数据库迁移问题
- 修复ModelDetail.tsx中直接渲染Gender枚举导致的React错误
- 修复TemplateDetailModal.tsx中直接渲染TemplateMaterialType和TrackType枚举的问题
- 添加枚举到中文文本的转换函数(getGenderText, getMaterialTypeText, getTrackTypeText)
- 实现完整的数据库迁移系统,支持版本化迁移
- 添加迁移v9修复template_materials表file_size字段允许NULL
- 改进数据库迁移执行逻辑,使用execute_batch方法
- 添加数据库集成测试和迁移测试
- 修复template_materials表约束问题,解决模板导入失败

主要变更:
- 新增数据库迁移系统(migrations.rs)
- 新增9个数据库迁移文件(v1-v9)
- 修复前端枚举渲染问题
- 完善数据库测试覆盖
2025-07-23 18:30:52 +08:00

467 lines
17 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use rusqlite::Connection;
use anyhow::{Result, anyhow};
use std::collections::HashMap;
/// 数据库迁移版本号
pub type MigrationVersion = u32;
/// 单个迁移脚本
#[derive(Debug, Clone)]
pub struct Migration {
/// 迁移版本号
pub version: MigrationVersion,
/// 迁移描述
pub description: String,
/// 向前迁移SQL
pub up_sql: String,
/// 回滚迁移SQL可选
pub down_sql: Option<String>,
}
/// 迁移管理器
pub struct MigrationManager {
migrations: HashMap<MigrationVersion, Migration>,
}
impl MigrationManager {
/// 创建新的迁移管理器
pub fn new() -> Self {
let mut manager = Self {
migrations: HashMap::new(),
};
// 注册所有迁移
manager.register_migrations();
manager
}
/// 注册所有迁移脚本
fn register_migrations(&mut self) {
// 迁移 1: 初始化数据库表结构
self.add_migration(Migration {
version: 1,
description: "初始化数据库表结构".to_string(),
up_sql: include_str!("migrations/001_initial_schema.sql").to_string(),
down_sql: Some(include_str!("migrations/001_initial_schema_down.sql").to_string()),
});
// 迁移 2: 添加模特ID字段到素材表
self.add_migration(Migration {
version: 2,
description: "添加模特ID字段到素材表".to_string(),
up_sql: include_str!("migrations/002_add_model_id_to_materials.sql").to_string(),
down_sql: Some(include_str!("migrations/002_add_model_id_to_materials_down.sql").to_string()),
});
// 迁移 3: 添加文件存在状态字段到模板素材表
self.add_migration(Migration {
version: 3,
description: "添加文件存在状态字段到模板素材表".to_string(),
up_sql: include_str!("migrations/003_add_file_status_to_template_materials.sql").to_string(),
down_sql: Some(include_str!("migrations/003_add_file_status_to_template_materials_down.sql").to_string()),
});
// 迁移 4: 移除模板表的project_id字段
self.add_migration(Migration {
version: 4,
description: "移除模板表的project_id字段".to_string(),
up_sql: include_str!("migrations/004_remove_project_id_from_templates.sql").to_string(),
down_sql: None, // 这个迁移不支持回滚,因为涉及数据丢失
});
// 迁移 5: 修复AI分类表的is_active字段类型
self.add_migration(Migration {
version: 5,
description: "修复AI分类表的is_active字段类型".to_string(),
up_sql: include_str!("migrations/005_fix_ai_classifications_is_active.sql").to_string(),
down_sql: None, // 类型修复不支持回滚
});
// 迁移 6: 添加导出状态字段到模板匹配结果表
self.add_migration(Migration {
version: 6,
description: "添加导出状态字段到模板匹配结果表".to_string(),
up_sql: include_str!("migrations/006_add_export_status_to_template_matching.sql").to_string(),
down_sql: Some(include_str!("migrations/006_add_export_status_to_template_matching_down.sql").to_string()),
});
// 迁移 7: 修复项目表的is_active字段数据
self.add_migration(Migration {
version: 7,
description: "修复项目表的is_active字段数据".to_string(),
up_sql: include_str!("migrations/007_fix_projects_is_active_data.sql").to_string(),
down_sql: None, // 数据修复不支持回滚
});
// 迁移 8: 添加缩略图和使用跟踪字段
self.add_migration(Migration {
version: 8,
description: "添加缩略图和使用跟踪字段".to_string(),
up_sql: include_str!("migrations/008_add_thumbnail_and_usage_tracking.sql").to_string(),
down_sql: Some(include_str!("migrations/008_add_thumbnail_and_usage_tracking_down.sql").to_string()),
});
// 迁移 9: 修复template_materials表的file_size字段允许为NULL
self.add_migration(Migration {
version: 9,
description: "修复template_materials表的file_size字段允许为NULL".to_string(),
up_sql: include_str!("migrations/009_fix_template_materials_file_size_nullable.sql").to_string(),
down_sql: Some(include_str!("migrations/009_fix_template_materials_file_size_nullable_down.sql").to_string()),
});
}
/// 添加迁移
fn add_migration(&mut self, migration: Migration) {
self.migrations.insert(migration.version, migration);
}
/// 获取当前数据库版本
pub fn get_current_version(&self, conn: &Connection) -> Result<MigrationVersion> {
// 确保迁移历史表存在
self.ensure_migration_table(conn)?;
let version = conn.query_row(
"SELECT MAX(version) FROM schema_migrations WHERE success = 1",
[],
|row| row.get::<_, Option<u32>>(0)
).unwrap_or(Some(0)).unwrap_or(0);
Ok(version)
}
/// 获取最新版本号
pub fn get_latest_version(&self) -> MigrationVersion {
self.migrations.keys().max().copied().unwrap_or(0)
}
/// 确保迁移历史表存在
fn ensure_migration_table(&self, conn: &Connection) -> Result<()> {
conn.execute(
"CREATE TABLE IF NOT EXISTS schema_migrations (
version INTEGER PRIMARY KEY,
description TEXT NOT NULL,
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP,
success INTEGER NOT NULL DEFAULT 0,
error_message TEXT
)",
[],
)?;
Ok(())
}
/// 运行所有待执行的迁移
pub fn migrate(&self, conn: &Connection) -> Result<()> {
let current_version = self.get_current_version(conn)?;
let latest_version = self.get_latest_version();
if current_version >= latest_version {
println!("数据库已是最新版本 v{}", current_version);
return Ok(());
}
println!("开始数据库迁移: v{} -> v{}", current_version, latest_version);
// 按版本号排序执行迁移
let mut versions: Vec<_> = self.migrations.keys().collect();
versions.sort();
for &version in versions {
if version > current_version {
self.apply_migration(conn, version)?;
}
}
println!("数据库迁移完成");
Ok(())
}
/// 应用单个迁移
fn apply_migration(&self, conn: &Connection, version: MigrationVersion) -> Result<()> {
let migration = self.migrations.get(&version)
.ok_or_else(|| anyhow!("迁移版本 {} 不存在", version))?;
println!("应用迁移 v{}: {}", version, migration.description);
// 开始事务
let tx = conn.unchecked_transaction()?;
// 记录迁移开始
tx.execute(
"INSERT OR REPLACE INTO schema_migrations (version, description, success) VALUES (?1, ?2, 0)",
[&version.to_string(), &migration.description],
)?;
// 执行迁移SQL
match self.execute_migration_sql(&tx, &migration.up_sql) {
Ok(_) => {
// 标记迁移成功
tx.execute(
"UPDATE schema_migrations SET success = 1, applied_at = CURRENT_TIMESTAMP WHERE version = ?1",
[&version.to_string()],
)?;
tx.commit()?;
println!("迁移 v{} 应用成功", version);
Ok(())
}
Err(e) => {
// 记录错误信息
let _ = tx.execute(
"UPDATE schema_migrations SET error_message = ?1 WHERE version = ?2",
[&e.to_string(), &version.to_string()],
);
tx.rollback()?;
Err(anyhow!("迁移 v{} 失败: {}", version, e))
}
}
}
/// 执行迁移SQL支持多条语句
fn execute_migration_sql(&self, conn: &Connection, sql: &str) -> Result<()> {
// 使用SQLite的execute_batch方法来执行多条SQL语句
// 这个方法能正确处理SQL语句的分割包括注释和字符串中的分号
conn.execute_batch(sql)?;
Ok(())
}
/// 回滚到指定版本
pub fn rollback_to(&self, conn: &Connection, target_version: MigrationVersion) -> Result<()> {
let current_version = self.get_current_version(conn)?;
if target_version >= current_version {
return Err(anyhow!("目标版本 {} 不能大于等于当前版本 {}", target_version, current_version));
}
println!("开始回滚数据库: v{} -> v{}", current_version, target_version);
// 按版本号倒序回滚
let mut versions: Vec<_> = self.migrations.keys().collect();
versions.sort_by(|a, b| b.cmp(a)); // 倒序
for &version in versions {
if version > target_version && version <= current_version {
self.rollback_migration(conn, version)?;
}
}
println!("数据库回滚完成");
Ok(())
}
/// 回滚单个迁移
fn rollback_migration(&self, conn: &Connection, version: MigrationVersion) -> Result<()> {
let migration = self.migrations.get(&version)
.ok_or_else(|| anyhow!("迁移版本 {} 不存在", version))?;
let down_sql = migration.down_sql.as_ref()
.ok_or_else(|| anyhow!("迁移 v{} 不支持回滚", version))?;
println!("回滚迁移 v{}: {}", version, migration.description);
// 开始事务
let tx = conn.unchecked_transaction()?;
// 执行回滚SQL
match self.execute_migration_sql(&tx, down_sql) {
Ok(_) => {
// 删除迁移记录
tx.execute(
"DELETE FROM schema_migrations WHERE version = ?1",
[&version.to_string()],
)?;
tx.commit()?;
println!("迁移 v{} 回滚成功", version);
Ok(())
}
Err(e) => {
tx.rollback()?;
Err(anyhow!("迁移 v{} 回滚失败: {}", version, e))
}
}
}
/// 获取迁移历史
pub fn get_migration_history(&self, conn: &Connection) -> Result<Vec<(MigrationVersion, String, String, bool)>> {
self.ensure_migration_table(conn)?;
let mut stmt = conn.prepare(
"SELECT version, description, applied_at, success FROM schema_migrations ORDER BY version"
)?;
let rows = stmt.query_map([], |row| {
Ok((
row.get::<_, u32>(0)?,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
row.get::<_, i32>(3)? == 1,
))
})?;
let mut history = Vec::new();
for row in rows {
history.push(row?);
}
Ok(history)
}
}
#[cfg(test)]
mod tests {
use super::*;
use rusqlite::Connection;
use tempfile::NamedTempFile;
fn create_test_connection() -> Connection {
let temp_file = NamedTempFile::new().unwrap();
let conn = Connection::open(temp_file.path()).unwrap();
// 配置数据库设置
conn.pragma_update(None, "foreign_keys", "ON").unwrap();
conn.pragma_update(None, "journal_mode", "WAL").unwrap();
conn
}
#[test]
fn test_migration_manager_creation() {
let manager = MigrationManager::new();
let latest_version = manager.get_latest_version();
// 应该有至少8个迁移版本
assert!(latest_version >= 8, "应该有至少8个迁移版本实际: {}", latest_version);
}
#[test]
fn test_initial_version_is_zero() {
let conn = create_test_connection();
let manager = MigrationManager::new();
let current_version = manager.get_current_version(&conn).unwrap();
assert_eq!(current_version, 0, "新数据库的版本应该是0");
}
#[test]
fn test_migration_table_creation() {
let conn = create_test_connection();
let manager = MigrationManager::new();
// 确保迁移表存在
manager.ensure_migration_table(&conn).unwrap();
// 验证表是否存在
let table_exists: bool = conn.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='schema_migrations'",
[],
|row| row.get::<_, i32>(0)
).unwrap() > 0;
assert!(table_exists, "schema_migrations表应该存在");
}
#[test]
fn test_full_migration() {
let conn = create_test_connection();
let manager = MigrationManager::new();
// 执行所有迁移
manager.migrate(&conn).unwrap();
// 验证版本已更新
let current_version = manager.get_current_version(&conn).unwrap();
let latest_version = manager.get_latest_version();
assert_eq!(current_version, latest_version, "迁移后版本应该是最新版本");
// 验证一些关键表是否存在
let tables = vec![
"projects", "materials", "material_segments", "models",
"ai_classifications", "templates", "template_materials",
"tracks", "track_segments", "video_classification_records"
];
for table in tables {
let table_exists: bool = conn.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?1",
[table],
|row| row.get::<_, i32>(0)
).unwrap() > 0;
assert!(table_exists, "表 {} 应该存在", table);
}
}
#[test]
fn test_migration_history() {
let conn = create_test_connection();
let manager = MigrationManager::new();
// 执行迁移
manager.migrate(&conn).unwrap();
// 获取迁移历史
let history = manager.get_migration_history(&conn).unwrap();
// 应该有迁移记录
assert!(!history.is_empty(), "应该有迁移历史记录");
// 所有迁移都应该成功
for (version, description, _applied_at, success) in history {
assert!(success, "迁移 v{}: {} 应该成功", version, description);
}
}
#[test]
fn test_idempotent_migration() {
let conn = create_test_connection();
let manager = MigrationManager::new();
// 第一次迁移
manager.migrate(&conn).unwrap();
let version_after_first = manager.get_current_version(&conn).unwrap();
// 第二次迁移(应该是幂等的)
manager.migrate(&conn).unwrap();
let version_after_second = manager.get_current_version(&conn).unwrap();
assert_eq!(version_after_first, version_after_second, "重复迁移应该是幂等的");
}
#[test]
fn test_specific_migration_features() {
let conn = create_test_connection();
let manager = MigrationManager::new();
// 执行迁移
manager.migrate(&conn).unwrap();
// 测试特定的迁移功能
// 1. 检查materials表是否有model_id字段迁移2
let has_model_id = conn.prepare("SELECT model_id FROM materials LIMIT 1").is_ok();
assert!(has_model_id, "materials表应该有model_id字段");
// 2. 检查template_materials表是否有file_exists字段迁移3
let has_file_exists = conn.prepare("SELECT file_exists FROM template_materials LIMIT 1").is_ok();
assert!(has_file_exists, "template_materials表应该有file_exists字段");
// 3. 检查materials表是否有thumbnail_path字段迁移8
let has_materials_thumbnail_path = conn.prepare("SELECT thumbnail_path FROM materials LIMIT 1").is_ok();
assert!(has_materials_thumbnail_path, "materials表应该有thumbnail_path字段");
// 4. 检查material_segments表是否有usage_count字段迁移8
let has_usage_count = conn.prepare("SELECT usage_count FROM material_segments LIMIT 1").is_ok();
assert!(has_usage_count, "material_segments表应该有usage_count字段");
// 5. 检查template_matching_results表是否有export_count字段迁移8
let has_export_count = conn.prepare("SELECT export_count FROM template_matching_results LIMIT 1").is_ok();
assert!(has_export_count, "template_matching_results表应该有export_count字段");
// 6. 验证初始化时就存在的字段
let has_matching_rule = conn.prepare("SELECT matching_rule FROM track_segments LIMIT 1").is_ok();
assert!(has_matching_rule, "track_segments表应该有matching_rule字段初始化时创建");
let has_segments_thumbnail_path = conn.prepare("SELECT thumbnail_path FROM material_segments LIMIT 1").is_ok();
assert!(has_segments_thumbnail_path, "material_segments表应该有thumbnail_path字段初始化时创建");
}
}