feat(saas): add down migrations for all incremental schema changes (AUD3-DB-01)
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled

- 16 down SQL files in migrations/down/ for each incremental migration
- db::run_down_migrations() executes rollback files in reverse order
- migrate_down CLI task: task=migrate_down timestamp=20260402
- Initial schema and seed data excluded (would be destructive)
This commit is contained in:
iven
2026-04-05 01:35:33 +08:00
parent 3b0ab1a7b7
commit 745c2fd754
18 changed files with 123 additions and 2 deletions

View File

@@ -0,0 +1,2 @@
-- Down: Drop scheduled_tasks table
DROP TABLE IF EXISTS scheduled_tasks;

View File

@@ -0,0 +1,2 @@
-- Down: Remove llm_routing column from accounts
ALTER TABLE accounts DROP COLUMN IF EXISTS llm_routing;

View File

@@ -0,0 +1,11 @@
-- Down: Remove extension columns from agent_templates
DROP INDEX IF EXISTS idx_agent_templates_source_id;
ALTER TABLE agent_templates DROP COLUMN IF EXISTS source_id;
ALTER TABLE agent_templates DROP COLUMN IF EXISTS version;
ALTER TABLE agent_templates DROP COLUMN IF EXISTS emoji;
ALTER TABLE agent_templates DROP COLUMN IF EXISTS communication_style;
ALTER TABLE agent_templates DROP COLUMN IF EXISTS personality;
ALTER TABLE agent_templates DROP COLUMN IF EXISTS quick_commands;
ALTER TABLE agent_templates DROP COLUMN IF EXISTS welcome_message;
ALTER TABLE agent_templates DROP COLUMN IF EXISTS scenarios;
ALTER TABLE agent_templates DROP COLUMN IF EXISTS soul_content;

View File

@@ -0,0 +1,2 @@
-- Down: Remove last_used_at from provider_keys
ALTER TABLE provider_keys DROP COLUMN IF EXISTS last_used_at;

View File

@@ -0,0 +1,2 @@
-- Down: Re-add quota_reset_interval to provider_keys
ALTER TABLE provider_keys ADD COLUMN IF NOT EXISTS quota_reset_interval TEXT;

View File

@@ -0,0 +1,5 @@
-- Down: Remove is_embedding and model_type from models
DROP INDEX IF EXISTS idx_models_model_type;
DROP INDEX IF EXISTS idx_models_is_embedding;
ALTER TABLE models DROP COLUMN IF EXISTS model_type;
ALTER TABLE models DROP COLUMN IF EXISTS is_embedding;

View File

@@ -0,0 +1,4 @@
-- Down: Remove password security columns from accounts
ALTER TABLE accounts DROP COLUMN IF EXISTS locked_until;
ALTER TABLE accounts DROP COLUMN IF EXISTS failed_login_count;
ALTER TABLE accounts DROP COLUMN IF EXISTS password_version;

View File

@@ -0,0 +1,2 @@
-- Down: Drop rate_limit_events table
DROP TABLE IF EXISTS rate_limit_events;

View File

@@ -0,0 +1,6 @@
-- Down: Drop billing tables (reverse creation order due to FK)
DROP TABLE IF EXISTS billing_usage_quotas;
DROP TABLE IF EXISTS billing_payments;
DROP TABLE IF EXISTS billing_invoices;
DROP TABLE IF EXISTS billing_subscriptions;
DROP TABLE IF EXISTS billing_plans;

View File

@@ -0,0 +1,7 @@
-- Down: Drop knowledge base tables (reverse creation order due to FK)
-- Note: This does NOT reverse the pgvector extension installation or role permission updates.
DROP TABLE IF EXISTS knowledge_usage;
DROP TABLE IF EXISTS knowledge_versions;
DROP TABLE IF EXISTS knowledge_chunks;
DROP TABLE IF EXISTS knowledge_items;
DROP TABLE IF EXISTS knowledge_categories;

View File

@@ -0,0 +1,3 @@
-- Down: Remove result columns from scheduled_tasks
ALTER TABLE scheduled_tasks DROP COLUMN IF EXISTS last_duration_ms;
ALTER TABLE scheduled_tasks DROP COLUMN IF EXISTS last_result;

View File

@@ -0,0 +1,2 @@
-- Down: Remove assigned_template_id from accounts
ALTER TABLE accounts DROP COLUMN IF EXISTS assigned_template_id;

View File

@@ -0,0 +1,3 @@
-- Down: Drop webhook tables (reverse creation order due to FK)
DROP TABLE IF EXISTS webhook_deliveries;
DROP TABLE IF EXISTS webhook_subscriptions;

View File

@@ -0,0 +1,3 @@
-- Down: Drop model group tables (reverse creation order due to FK)
DROP TABLE IF EXISTS model_group_members;
DROP TABLE IF EXISTS model_groups;

View File

@@ -0,0 +1,6 @@
-- Down: Remove industry agent template seed data
DELETE FROM agent_templates WHERE id IN (
'edu-teacher-001',
'healthcare-admin-001',
'design-shantou-001'
);

View File

@@ -123,6 +123,50 @@ async fn run_migration_files(pool: &PgPool, dir: &std::path::Path) -> SaasResult
Ok(()) Ok(())
} }
/// 从 down/ 目录加载并执行回滚迁移文件(按文件名倒序)
///
/// 用法: `task=migrate_down timestamp=20260402000001`
/// 会回滚 >= 该时间戳的所有增量迁移。不包含初始 schema 和 seed data。
pub async fn run_down_migrations(pool: &PgPool, target: &str) -> SaasResult<()> {
let down_dir = std::path::Path::new("crates/zclaw-saas/migrations/down");
let alt_dir = std::path::Path::new("migrations/down");
let dir = if down_dir.exists() { down_dir } else if alt_dir.exists() { alt_dir } else {
tracing::warn!("No down migrations directory found");
return Ok(());
};
let mut entries: Vec<std::path::PathBuf> = std::fs::read_dir(dir)?
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| {
p.extension().map(|ext| ext == "sql").unwrap_or(false)
&& p.file_name()
.map(|n| n.to_string_lossy().starts_with(target))
.unwrap_or(false)
})
.collect();
entries.sort_by(|a, b| b.cmp(a)); // Reverse order (newest first)
if entries.is_empty() {
tracing::info!("No down migrations match prefix '{}'", target);
return Ok(());
}
for path in &entries {
let filename = path.file_name().unwrap_or_default().to_string_lossy();
tracing::info!("Running down migration: {}", filename);
let content = std::fs::read_to_string(path)?;
for stmt in split_sql_statements(&content) {
let trimmed = stmt.trim();
if !trimmed.is_empty() && !trimmed.starts_with("--") {
sqlx::query(trimmed).execute(pool).await?;
}
}
}
tracing::info!("Down migrations complete (prefix={})", target);
Ok(())
}
/// 按语句分割 SQL 文件内容,正确处理: /// 按语句分割 SQL 文件内容,正确处理:
/// - 单引号字符串 `'...'` /// - 单引号字符串 `'...'`
/// - 双引号标识符 `"..."` /// - 双引号标识符 `"..."`

View File

@@ -3,7 +3,7 @@
//! 提供可手动执行的运维命令: //! 提供可手动执行的运维命令:
//! - seed_admin — 创建管理员账号 //! - seed_admin — 创建管理员账号
//! - cleanup_devices — 清理不活跃设备 //! - cleanup_devices — 清理不活跃设备
//! - migrate_schema — 手动触发 schema 迁移 //! - migrate_down — 回滚指定迁移timestamp=20260402 回滚所有 >= 该前缀的迁移
use std::collections::HashMap; use std::collections::HashMap;
use sqlx::PgPool; use sqlx::PgPool;
@@ -27,6 +27,7 @@ pub fn builtin_tasks() -> Vec<Box<dyn Task>> {
vec![ vec![
Box::new(SeedAdminTask), Box::new(SeedAdminTask),
Box::new(CleanupDevicesTask), Box::new(CleanupDevicesTask),
Box::new(MigrateDownTask),
] ]
} }
@@ -86,3 +87,18 @@ impl Task for CleanupDevicesTask {
Ok(()) Ok(())
} }
} }
/// 回滚数据库迁移
struct MigrateDownTask;
#[async_trait::async_trait]
impl Task for MigrateDownTask {
fn name(&self) -> &str { "migrate_down" }
fn description(&self) -> &str { "回滚指定时间戳之后的迁移timestamp=20260402 回滚 >= 该前缀的所有增量迁移)" }
async fn run(&self, db: &PgPool, args: &HashMap<String, String>) -> SaasResult<()> {
let timestamp = args.get("timestamp")
.ok_or_else(|| crate::error::SaasError::InvalidInput("Missing 'timestamp' argument (e.g. 20260402)".into()))?;
crate::db::run_down_migrations(db, timestamp).await
}
}

View File

@@ -228,7 +228,7 @@
| AUD3-FE-04 | window 全局变量存 interval | **FALSE_POSITIVE** | App.tsx 已使用 useRef非 window 全局变量 | | AUD3-FE-04 | window 全局变量存 interval | **FALSE_POSITIVE** | App.tsx 已使用 useRef非 window 全局变量 |
| AUD3-FE-05 | 25+ 处 mixin `as any` | **FIXED** | gateway-heartbeat.ts 删除(死代码) + kernel-*.ts 改为 Record<string, unknown> + chatStore 改为 Record; as any 38→3 | | AUD3-FE-05 | 25+ 处 mixin `as any` | **FIXED** | gateway-heartbeat.ts 删除(死代码) + kernel-*.ts 改为 Record<string, unknown> + chatStore 改为 Record; as any 38→3 |
| AUD3-FE-06 | PropertyPanel 17 处 `as any` | **FIXED** | 13 处 (data as any) → 类型安全 d 访问器 (Record<string, unknown>) | | AUD3-FE-06 | PropertyPanel 17 处 `as any` | **FIXED** | 13 处 (data as any) → 类型安全 d 访问器 (Record<string, unknown>) |
| AUD3-DB-01 | 无 down migration | OPEN | crates/zclaw-saas/migrations/ | | AUD3-DB-01 | 无 down migration | **FIXED** | 16 个 down SQL + run_down_migrations() + migrate_down task |
| AUD3-DB-02 | format! SQL 模式 | **FALSE_POSITIVE** | 同 SEC2-P2-08白名单+防御性验证已到位 | | AUD3-DB-02 | format! SQL 模式 | **FALSE_POSITIVE** | 同 SEC2-P2-08白名单+防御性验证已到位 |
| AUD3-API-02 | 前端错误处理不统一 | **DOCUMENTED** | error-handling.ts 基础设施已完善100+ 文件渐进式迁移至 reportError/reportApiError | | AUD3-API-02 | 前端错误处理不统一 | **DOCUMENTED** | error-handling.ts 基础设施已完善100+ 文件渐进式迁移至 reportError/reportApiError |
| AUD3-CONC-02 | ~15 处 fire-and-forget tokio::spawn | **FALSE_POSITIVE** | 与 SEC2-P2-05 相同,已添加 NOTE(fire-and-forget) 注释 | | AUD3-CONC-02 | ~15 处 fire-and-forget tokio::spawn | **FALSE_POSITIVE** | 与 SEC2-P2-05 相同,已添加 NOTE(fire-and-forget) 注释 |
@@ -273,6 +273,7 @@
| 2026-04-05 | SEC2-P3-02 | OPEN → DOCUMENTED | P4 级差异admin 独立类型系统 | | 2026-04-05 | SEC2-P3-02 | OPEN → DOCUMENTED | P4 级差异admin 独立类型系统 |
| 2026-04-05 | V11-P2-05 | 部分关闭 → FIXED | 完整审计: 160 @connected + 16 @reserved + 1 未注册 = 177 总命令 | | 2026-04-05 | V11-P2-05 | 部分关闭 → FIXED | 完整审计: 160 @connected + 16 @reserved + 1 未注册 = 177 总命令 |
| 2026-04-05 | TYPE-01 | OPEN → FIXED | ConnectionState 统一(含 handshaking); saas-types PromptTemplate/PromptVariable union literal; CreateRoleRequest optional; PropertyPanel as any→类型安全; chatStore window cast | | 2026-04-05 | TYPE-01 | OPEN → FIXED | ConnectionState 统一(含 handshaking); saas-types PromptTemplate/PromptVariable union literal; CreateRoleRequest optional; PropertyPanel as any→类型安全; chatStore window cast |
| 2026-04-05 | AUD3-DB-01 | OPEN → FIXED | 16 个 down migration SQL + db::run_down_migrations() + migrate_down task (timestamp=20260402) |
## V12 模块化端到端审计修复 (2026-04-04) ## V12 模块化端到端审计修复 (2026-04-04)