feat(all): 数据库模块化拆分与版本化迁移、任务引擎命名体系收敛、物理输出校验加固与用户配置接通
- server/db: 拆 4929 行 db.rs 单体为 db/ 目录,migrations.rs 引入 PRAGMA user_version
版本化迁移运行器(M1~M13)
- 任务引擎 Phase 6/7b/7c 改名收敛:EngineStageConfig→PhaseConfig、StagePolicy→ResumePolicy、
Converged→Completed、删除 task_type 列、success_method 拆 tlusty_/synspec_ 双列、
新增 tlusty_status/synspec_status 半失败阶段守卫
- 科学正确性加固:conv_check 任意行 NaN/Inf/溢出判无效(0 行容忍)、新增 spec_is_valid
校验 SYNSPEC 脏谱、itek_history 逐次迭代全量保真、fmt_abn powf 溢出饱和
- 用户配置真正接通:tlusty_chain/tlusty_input 由死字段经 调度器→TaskSpec→executor→runner
透传生效;config 加载期 validate + deny_unknown_fields + 解析失败记 warn
- 调度修复:H1 活锁(pending_strategies 跳过已失败策略)、种子查找错误不再静默降级冷启动
- dashboard: 阶段配置面板 tlusty_stage/synspec_stage、"已完成"标签、迭代诊断展示
- docs: 新增 database_refactor_design.md,同步 database/api/PIPELINE/workflow_detail
This commit is contained in:
@@ -0,0 +1,565 @@
|
||||
//! 主库版本化迁移基础设施(Phase 0,见 docs/database_refactor_design.md §2)。
|
||||
//!
|
||||
//! 背景:init_tables 内持续堆积手写幂等 ALTER 块,无版本追踪,风险随 schema 演进累积。
|
||||
//! 本模块引入 `PRAGMA user_version` 驱动的版本化迁移,为后续各 Phase 的结构变更
|
||||
//! (P1/P2/P4/P5a/P6)提供统一、可检测、事务化、可中断恢复的迁移通道。
|
||||
//!
|
||||
//! 约定:
|
||||
//! - **V0 = 0**(`PRAGMA user_version` 对全新库的默认值);后续编号迁移从 1 开始。
|
||||
//! - **新库**:bootstrap 的 CREATE TABLE 始终是最新形态(含各 Phase 新增列)→ 置 V0 →
|
||||
//! 顺序应用 V0+1..N。每个迁移自带 detect 守卫,已存在的列/索引直接跳过 → 新库上所有迁移为 no-op。
|
||||
//! - **旧库**(user_version=0 但表已存在):bootstrap 幂等补全既有列 → 置 V0 → 应用后续迁移,
|
||||
//! detect 守卫保证只补缺的列/索引,数据零搬运。
|
||||
//! - **幂等性关键**:SQLite 无 `ADD COLUMN IF NOT EXISTS`,迁移必须靠 detect 守卫而非裸 SQL
|
||||
//! 数组实现幂等(审查 CRITICAL#3——否则全新库上 bootstrap 已建新列,迁移再 ADD 会报
|
||||
//! duplicate column 崩启动)。
|
||||
//!
|
||||
//! 队列库(dcts_queue.db)无版本迁移,维持现状;本模块**仅主库**引入版本号。
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use rusqlite::{params, Connection, TransactionBehavior};
|
||||
|
||||
/// 一个版本化迁移。
|
||||
///
|
||||
/// - `version`:> V0(=0) 的顺序号(1..N),`PRAGMA user_version = version` 即代表已应用。
|
||||
/// - `name`:便于日志与审计。
|
||||
/// - `detect`:该迁移是否已应用(列/索引存在性检测)。为 true 时跳过 `up`,仅推进版本号。
|
||||
/// - `up`:未应用时才执行,同一事务内顺序执行。
|
||||
///
|
||||
/// `Clone + Copy`:fn 指针与 `&'static str` 均 Copy,迁移定义可原地复用(测试重跑场景)。
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Migration {
|
||||
pub version: u32,
|
||||
pub name: &'static str,
|
||||
pub detect: fn(&Connection) -> Result<bool>,
|
||||
pub up: &'static [&'static str],
|
||||
}
|
||||
|
||||
/// 全部迁移。随各 Phase 追加(Phase 0 交付基础设施,Phase 1 起逐个加入)。
|
||||
pub const MIGRATIONS: &[Migration] = &[
|
||||
// M1(Phase 1,P1):tasks 阶段信息补全。failed_stage = 失败阶段归因,
|
||||
// summary_json = ModelSummary 全保真 JSON。两列均在线 ADD COLUMN,旧节点上报不破坏结算。
|
||||
Migration {
|
||||
version: 1,
|
||||
name: "tasks-stage-info",
|
||||
detect: |c| {
|
||||
Ok(has_column(c, "tasks", "failed_stage")?
|
||||
&& has_column(c, "tasks", "summary_json")?)
|
||||
},
|
||||
up: &[
|
||||
"ALTER TABLE tasks ADD COLUMN failed_stage TEXT",
|
||||
"ALTER TABLE tasks ADD COLUMN summary_json TEXT",
|
||||
],
|
||||
},
|
||||
// M2(Phase 2,P2):tasks 单列 workflow_name 查询的覆盖索引。
|
||||
// 覆盖 `COUNT(*) WHERE workflow_name=?` 及详情页按工作流统计;用户决策不做 tasks 清理。
|
||||
// CREATE INDEX IF NOT EXISTS 天然幂等,新库/旧库统一由此迁移建立(无需进 init_tables)。
|
||||
Migration {
|
||||
version: 2,
|
||||
name: "tasks-wf-status-created-index",
|
||||
detect: |c| has_index(c, "idx_tasks_wf_status_created"),
|
||||
up: &["CREATE INDEX IF NOT EXISTS idx_tasks_wf_status_created ON tasks(workflow_name, status, created_at)"],
|
||||
},
|
||||
// M4(Phase 4,P5):清除 node_credentials 死列 revoked。
|
||||
// 新代码不读写它;registration_secret 保留在 nodes(审查 CRITICAL#1/#2:pending 节点无
|
||||
// node_credentials 行,迁移会静默丢凭据;token_hash NOT NULL + 唯一索引塞不下空占位)。
|
||||
// DROP COLUMN 涉及表重建(bundled SQLite 3.45+),部署走低峰窗口 + 手动备份(§11)。
|
||||
Migration {
|
||||
version: 4,
|
||||
name: "drop-revoked-dead-column",
|
||||
detect: |c| Ok(!has_column(c, "node_credentials", "revoked")?),
|
||||
up: &["ALTER TABLE node_credentials DROP COLUMN revoked"],
|
||||
},
|
||||
// M6(Phase 6,P8):删除 tasks 冗余列 task_type。
|
||||
// 该列与 tlusty_strategies[0] 恒等、synspec-only 场景为"假值",执行链已改由 strategies[0]
|
||||
// 推导(executor.rs),归因/过滤全部改派生口径。前提:集群无历史节点(用户决策)。
|
||||
// DROP COLUMN 涉及表重建,部署走低峰窗口 + 手动备份(§11);升级前确认队列为空(§8.8)。
|
||||
Migration {
|
||||
version: 6,
|
||||
name: "drop-task-type-column",
|
||||
detect: |c| Ok(!has_column(c, "tasks", "task_type")?),
|
||||
up: &["ALTER TABLE tasks DROP COLUMN task_type"],
|
||||
},
|
||||
// M7(Phase 5a,P6):grid_points 补 synspec 收敛归因列。
|
||||
// 与 success_method 镜像的 synspec 分支(光谱以什么策略收敛),解锁「光谱以 standard 等
|
||||
// 策略收敛了多少点」的 SQL 统计;TLUSTY-only 成功保持 NULL。在线 ADD COLUMN,无停写窗口。
|
||||
//
|
||||
// 版本号 = 7(而非 5):**迁移版本必须与部署顺序单调一致**——§11 部署顺序是 6 → 5a,
|
||||
// 若 5a 编号为 5,则已升到 v6 的库会因 `version <= current` 跳过它,synspec 列永不创建。
|
||||
//(设计 §2.2 的 "M5" 标签是早期命名,此处按部署序改号 M7。)
|
||||
Migration {
|
||||
version: 7,
|
||||
name: "synspec-success-method",
|
||||
detect: |c| has_column(c, "grid_points", "synspec_success_method"),
|
||||
up: &["ALTER TABLE grid_points ADD COLUMN synspec_success_method TEXT"],
|
||||
},
|
||||
// M8(Phase 5b,P6):grid_points 阶段状态列。
|
||||
// 解除点级单值 status 掩盖两阶段管线:半失败点(大气收敛+光谱失败)可查
|
||||
// tlusty_status='converged' + synspec_status='failed'。NULL = 阶段不适用(tlusty_enabled=0
|
||||
// 或 synspec_enabled=0)。整体 grid_points.status 仍是权威状态,阶段列是补充可查信息。
|
||||
// 在线 ADD COLUMN;同步触点见 docs/database_refactor_design.md §7.3 5b。
|
||||
Migration {
|
||||
version: 8,
|
||||
name: "grid-point-stage-status",
|
||||
detect: |c| {
|
||||
Ok(has_column(c, "grid_points", "tlusty_status")?
|
||||
&& has_column(c, "grid_points", "synspec_status")?)
|
||||
},
|
||||
up: &[
|
||||
"ALTER TABLE grid_points ADD COLUMN tlusty_status TEXT",
|
||||
"ALTER TABLE grid_points ADD COLUMN synspec_status TEXT",
|
||||
],
|
||||
},
|
||||
// M9(Phase 7c):grid_points.status 值 'converged' → 'completed'。
|
||||
// TLUSTY-first 残留:'converged' 暗示"大气收敛",实为"管线完成"(大气+光谱)。
|
||||
// 数据迁移 + 全链 SQL 字面量同步(见 db.rs/scheduler.rs,值全部改 'completed')。
|
||||
Migration {
|
||||
version: 9,
|
||||
name: "grid-status-converged-to-completed",
|
||||
detect: |c| {
|
||||
// 已迁移 = 不再存在旧值 'converged'(detect 检查数据而非列)。
|
||||
// 安全性:apply_migrations_with 以 user_version 闸控——M9 只在 version<9 时评估,
|
||||
// 一旦版本推进到 9 即永不再走此 detect,故即便后续代码意外再写入 'converged'
|
||||
// 也不会触发本迁移重放(user_version 不会回退)。
|
||||
let mut stmt = c.prepare("SELECT 1 FROM grid_points WHERE status = 'converged' LIMIT 1")?;
|
||||
Ok(!stmt.exists([])?)
|
||||
},
|
||||
up: &["UPDATE grid_points SET status = 'completed' WHERE status = 'converged'"],
|
||||
},
|
||||
// M10(Phase 7c):workflow_progress_snapshots 列名 converged → completed。
|
||||
// 该列存"管线完成点数",列名随状态值改名保持一致(§9.5 耦合项)。RENAME COLUMN 在线。
|
||||
Migration {
|
||||
version: 10,
|
||||
name: "snapshots-converged-column-rename",
|
||||
detect: |c| has_column(c, "workflow_progress_snapshots", "completed"),
|
||||
up: &["ALTER TABLE workflow_progress_snapshots RENAME COLUMN converged TO completed"],
|
||||
},
|
||||
// M11(H1 活锁修复):grid_points 补 pending_strategies 列。
|
||||
// 运行时回退(trigger_strategy_fallback)把点打回 pending 等种子时,记录「剩余策略链」
|
||||
// (JSON 数组),调度路径据此用剩余链重派、避免重跑已失败策略导致的无界失败重试活锁。
|
||||
// 在线 ADD COLUMN,无停写窗口。detect 幂等(全新库 bootstrap 已含该列 → 跳过)。
|
||||
Migration {
|
||||
version: 11,
|
||||
name: "grid-point-pending-strategies",
|
||||
detect: |c| has_column(c, "grid_points", "pending_strategies"),
|
||||
up: &["ALTER TABLE grid_points ADD COLUMN pending_strategies TEXT"],
|
||||
},
|
||||
// M12(P9 命名拆分):rid_points.success_method 值域混用列拆为阶段列 tlusty_success_method。
|
||||
// success_method 原是 TLUSTY-first 整体归因:TLUSTY 任务存 tlusty_strategies[0]
|
||||
// (cold_run/seed_step),synspec-only 任务却存 synspec_strategies[0](standard)——
|
||||
// 同一列两个值域,前端需猜策略名区分。拆后:
|
||||
// - tlusty_success_method:TLUSTY 阶段策略(tlusty 禁用为 NULL)
|
||||
// - synspec_success_method(既有):光谱阶段策略(synspec 禁用为 NULL)
|
||||
// 整体归因改由消费方派生(前端 tlusty ?? synspec)。
|
||||
//
|
||||
// 回填判别:旧数据里 synspec-only 点 success_method 与 synspec_success_method 同值
|
||||
// (正是被清理的冗余);故命中该等式的点不写 tlusty(保持 NULL),其余(正常双阶段
|
||||
// cold_run/seed_step)→ tlusty = 原 success_method。局限:极端情形下 TLUSTY 点
|
||||
// tlusty_strategies[0]==synspec_strategies[0](如都叫 standard)会被误判为 NULL;
|
||||
// 实际 sdB_cno 中大气策略与光谱策略不冲突,可接受。在线 ADD + UPDATE,无停写窗口。
|
||||
Migration {
|
||||
version: 12,
|
||||
name: "tlusty-success-method",
|
||||
detect: |c| has_column(c, "grid_points", "tlusty_success_method"),
|
||||
up: &[
|
||||
"ALTER TABLE grid_points ADD COLUMN tlusty_success_method TEXT",
|
||||
"UPDATE grid_points SET tlusty_success_method = success_method \
|
||||
WHERE success_method IS NOT NULL \
|
||||
AND NOT (success_method = synspec_success_method AND synspec_success_method IS NOT NULL)",
|
||||
],
|
||||
},
|
||||
// M13(P9 命名拆分):删除值域混用列 success_method(M12 已回填 tlusty_success_method)。
|
||||
// DROP COLUMN 涉及表重建(bundled SQLite 3.45+),部署走低峰窗口 + 手动备份(§11)。
|
||||
Migration {
|
||||
version: 13,
|
||||
name: "drop-success-method-column",
|
||||
detect: |c| Ok(!has_column(c, "grid_points", "success_method")?),
|
||||
up: &["ALTER TABLE grid_points DROP COLUMN success_method"],
|
||||
},
|
||||
];
|
||||
|
||||
/// 当前 schema 版本(`PRAGMA user_version`)。
|
||||
pub fn current_version(conn: &Connection) -> Result<u32> {
|
||||
Ok(conn.pragma_query_value(None, "user_version", |r| r.get(0))?)
|
||||
}
|
||||
|
||||
/// 从当前版本顺序应用常量表 `MIGRATIONS` 中尚未执行的迁移。
|
||||
pub fn apply_migrations(conn: &mut Connection) -> Result<()> {
|
||||
apply_migrations_with(conn, MIGRATIONS)
|
||||
}
|
||||
|
||||
/// 应用给定迁移列表中尚未执行的部分,每个迁移独立事务(供测试传入自定义列表)。
|
||||
///
|
||||
/// 对每个 `version > current` 的迁移:
|
||||
/// - `detect = true`(已应用,如新库 bootstrap 已建列)→ 仅推进 user_version,不执行 `up`;
|
||||
/// - `detect = false` → `BEGIN IMMEDIATE` → 执行 `up` SQL → `PRAGMA user_version = V` → `COMMIT`。
|
||||
///
|
||||
/// 中途失败不推进版本(进程启动时重试):当前迁移所在事务回滚,之前迁移的版本号已持久化。
|
||||
fn apply_migrations_with(conn: &mut Connection, migrations: &[Migration]) -> Result<()> {
|
||||
let mut current = current_version(conn)?;
|
||||
for m in migrations {
|
||||
if m.version <= current {
|
||||
continue;
|
||||
}
|
||||
if (m.detect)(conn)? {
|
||||
// 已应用(detect 命中,如全新库 bootstrap 已建列)→ 仅推进版本号,不执行 up。
|
||||
conn.pragma_update(None, "user_version", m.version)?;
|
||||
tracing::info!(
|
||||
version = m.version,
|
||||
name = m.name,
|
||||
"迁移已应用(detect 跳过)"
|
||||
);
|
||||
current = m.version;
|
||||
continue;
|
||||
}
|
||||
let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
|
||||
for sql in m.up {
|
||||
tx.execute_batch(sql)
|
||||
.with_context(|| format!("迁移 M{} ({}) 失败:{}", m.version, m.name, sql))?;
|
||||
}
|
||||
tx.pragma_update(None, "user_version", m.version)?;
|
||||
tx.commit()?;
|
||||
tracing::info!(version = m.version, name = m.name, "迁移已应用");
|
||||
current = m.version;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 检测表是否已含指定列(PRAGMA table_info)。`table` 必须是 MIGRATIONS 内写死的常量表名,
|
||||
/// 绝不来自外部输入(表名不参与任何用户数据路径)。
|
||||
fn has_column(conn: &Connection, table: &str, column: &str) -> Result<bool> {
|
||||
let mut stmt = conn.prepare(&format!("PRAGMA table_info({table})"))?;
|
||||
let rows = stmt.query_map([], |r| r.get::<_, String>(1))?;
|
||||
for r in rows {
|
||||
if r.map(|name| name == column).unwrap_or(false) {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// 检测索引是否已存在(sqlite_master)。
|
||||
fn has_index(conn: &Connection, index: &str) -> Result<bool> {
|
||||
let mut stmt = conn.prepare("SELECT 1 FROM sqlite_master WHERE type='index' AND name=?1")?;
|
||||
Ok(stmt.exists(params![index])?)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn mem_conn() -> Connection {
|
||||
Connection::open_in_memory().unwrap()
|
||||
}
|
||||
|
||||
/// 新库全流程(对齐 Database::new):bootstrap 已建出最新形态 schema(含 M1 新增列)
|
||||
/// → 版本 0 → apply_migrations 应**无报错**地把版本推进到最新(已存在的列/索引经 detect
|
||||
/// 跳过,不因 duplicate column 崩溃——审查 CRITICAL#3 回归;索引类迁移在最新列上正常建立)。
|
||||
#[test]
|
||||
fn fresh_db_bootstrap_then_migrations_advance_version() {
|
||||
let mut conn = mem_conn();
|
||||
// 模拟 init_tables bootstrap:tasks 是含全部迁移引用列的最新形态(M1 列已存在、
|
||||
// M2 索引目标列已存在但索引本身未建)。
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE tasks (
|
||||
task_id TEXT PRIMARY KEY,
|
||||
point_name TEXT NOT NULL,
|
||||
node_id TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
created_at DATETIME NOT NULL,
|
||||
completed_at DATETIME,
|
||||
workflow_name TEXT,
|
||||
failed_stage TEXT,
|
||||
summary_json TEXT
|
||||
);
|
||||
CREATE TABLE grid_points (
|
||||
name TEXT NOT NULL,
|
||||
workflow_name TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
tlusty_success_method TEXT,
|
||||
synspec_success_method TEXT,
|
||||
tlusty_status TEXT,
|
||||
synspec_status TEXT,
|
||||
pending_strategies TEXT
|
||||
);
|
||||
CREATE TABLE workflow_progress_snapshots (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
workflow_name TEXT NOT NULL,
|
||||
ts DATETIME NOT NULL,
|
||||
total INTEGER NOT NULL,
|
||||
pending INTEGER NOT NULL,
|
||||
queued INTEGER NOT NULL,
|
||||
running INTEGER NOT NULL,
|
||||
completed INTEGER NOT NULL,
|
||||
failed INTEGER NOT NULL
|
||||
)",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(current_version(&conn).unwrap(), 0);
|
||||
apply_migrations(&mut conn).unwrap();
|
||||
let latest = MIGRATIONS.last().map(|m| m.version).unwrap_or(0);
|
||||
assert_eq!(current_version(&conn).unwrap(), latest);
|
||||
}
|
||||
|
||||
/// 正常迁移:up 顺序执行,版本推进。
|
||||
#[test]
|
||||
fn migration_applies_and_advances_version() {
|
||||
let mut conn = mem_conn();
|
||||
let m = Migration {
|
||||
version: 1,
|
||||
name: "test-add-col",
|
||||
detect: |c| has_column(c, "t", "c"),
|
||||
up: &[
|
||||
"CREATE TABLE t(id INTEGER PRIMARY KEY)",
|
||||
"ALTER TABLE t ADD COLUMN c TEXT",
|
||||
],
|
||||
};
|
||||
apply_migrations_with(&mut conn, &[m]).unwrap();
|
||||
assert_eq!(current_version(&conn).unwrap(), 1);
|
||||
assert!(has_column(&conn, "t", "c").unwrap());
|
||||
}
|
||||
|
||||
/// detect=true(已应用)→ 跳过 up(若执行会 duplicate column 崩),仅推进版本。
|
||||
#[test]
|
||||
fn detect_skip_advances_version_without_running_up() {
|
||||
let mut conn = mem_conn();
|
||||
conn.execute_batch("CREATE TABLE t(id INTEGER PRIMARY KEY, c TEXT)")
|
||||
.unwrap();
|
||||
let m = Migration {
|
||||
version: 1,
|
||||
name: "test-add-col",
|
||||
detect: |c| has_column(c, "t", "c"),
|
||||
up: &["ALTER TABLE t ADD COLUMN c TEXT"],
|
||||
};
|
||||
apply_migrations_with(&mut conn, &[m]).unwrap();
|
||||
assert_eq!(current_version(&conn).unwrap(), 1);
|
||||
}
|
||||
|
||||
/// M4 专项:旧库 node_credentials 含 revoked 死列 → apply_migrations 后列消失、版本推进。
|
||||
/// 同时验证 M1(列已存在跳过)+ M2(索引缺失建立)+ M4(DROP COLUMN)在同库顺序生效。
|
||||
#[test]
|
||||
fn m4_drops_revoked_dead_column_on_old_db() {
|
||||
let mut conn = mem_conn();
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE tasks (
|
||||
task_id TEXT PRIMARY KEY,
|
||||
point_name TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
created_at DATETIME NOT NULL,
|
||||
workflow_name TEXT,
|
||||
failed_stage TEXT,
|
||||
summary_json TEXT
|
||||
);
|
||||
CREATE TABLE grid_points (
|
||||
name TEXT NOT NULL,
|
||||
workflow_name TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
success_method TEXT,
|
||||
synspec_success_method TEXT
|
||||
);
|
||||
CREATE TABLE workflow_progress_snapshots (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
workflow_name TEXT NOT NULL,
|
||||
ts DATETIME NOT NULL,
|
||||
total INTEGER NOT NULL,
|
||||
pending INTEGER NOT NULL,
|
||||
queued INTEGER NOT NULL,
|
||||
running INTEGER NOT NULL,
|
||||
converged INTEGER NOT NULL,
|
||||
failed INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE node_credentials (
|
||||
node_id TEXT PRIMARY KEY,
|
||||
token_hash TEXT NOT NULL,
|
||||
issued_at DATETIME NOT NULL,
|
||||
revoked INTEGER NOT NULL DEFAULT 0,
|
||||
raw_token_pending TEXT
|
||||
);",
|
||||
)
|
||||
.unwrap();
|
||||
apply_migrations(&mut conn).unwrap();
|
||||
assert!(
|
||||
!has_column(&conn, "node_credentials", "revoked").unwrap(),
|
||||
"revoked 死列应被 M4 清除"
|
||||
);
|
||||
assert!(
|
||||
has_index(&conn, "idx_tasks_wf_status_created").unwrap(),
|
||||
"M2 索引应建立"
|
||||
);
|
||||
// M10:旧 snapshots 的 converged 列应被重命名为 completed。
|
||||
assert!(has_column(&conn, "workflow_progress_snapshots", "completed").unwrap());
|
||||
assert!(!has_column(&conn, "workflow_progress_snapshots", "converged").unwrap());
|
||||
// M11:grid_points 的 pending_strategies 列应被补齐(H1 活锁修复标记)。
|
||||
assert!(has_column(&conn, "grid_points", "pending_strategies").unwrap());
|
||||
let latest = MIGRATIONS.last().map(|m| m.version).unwrap_or(0);
|
||||
assert_eq!(current_version(&conn).unwrap(), latest);
|
||||
}
|
||||
|
||||
/// M9(Phase 7c):grid_points.status 值 'converged' → 'completed' 数据迁移。
|
||||
/// 旧库残留 'converged' 值 → apply_migrations 后全部转为 'completed'。
|
||||
#[test]
|
||||
fn m9_converged_status_value_migrated() {
|
||||
let mut conn = mem_conn();
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE tasks (
|
||||
task_id TEXT PRIMARY KEY,
|
||||
point_name TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
created_at DATETIME NOT NULL,
|
||||
workflow_name TEXT
|
||||
);
|
||||
CREATE TABLE grid_points (
|
||||
name TEXT NOT NULL,
|
||||
workflow_name TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
success_method TEXT,
|
||||
synspec_success_method TEXT
|
||||
);
|
||||
INSERT INTO grid_points (name, workflow_name, status) VALUES ('p1','wf_a','converged');
|
||||
INSERT INTO grid_points (name, workflow_name, status) VALUES ('p2','wf_a','failed');
|
||||
CREATE TABLE workflow_progress_snapshots (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
workflow_name TEXT NOT NULL,
|
||||
ts DATETIME NOT NULL,
|
||||
total INTEGER NOT NULL,
|
||||
pending INTEGER NOT NULL,
|
||||
queued INTEGER NOT NULL,
|
||||
running INTEGER NOT NULL,
|
||||
converged INTEGER NOT NULL,
|
||||
failed INTEGER NOT NULL
|
||||
);",
|
||||
)
|
||||
.unwrap();
|
||||
apply_migrations(&mut conn).unwrap();
|
||||
// 旧值迁为 'completed';'failed' 不受影响。
|
||||
let statuses: Vec<String> = {
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT status FROM grid_points ORDER BY name")
|
||||
.unwrap();
|
||||
stmt.query_map([], |r| r.get::<_, String>(0))
|
||||
.unwrap()
|
||||
.filter_map(Result::ok)
|
||||
.collect()
|
||||
};
|
||||
assert_eq!(
|
||||
statuses,
|
||||
vec!["completed".to_string(), "failed".to_string()]
|
||||
);
|
||||
// M10:snapshots 列改名。
|
||||
assert!(has_column(&conn, "workflow_progress_snapshots", "completed").unwrap());
|
||||
}
|
||||
|
||||
/// 中断恢复:M1 成功(版本 1),M2 中途失败(事务回滚,版本停在 1);
|
||||
/// 修正 M2 后重跑,M1 跳过、M2 成功。
|
||||
#[test]
|
||||
fn interrupted_migration_rolls_back_and_retries() {
|
||||
let mut conn = mem_conn();
|
||||
let m1 = Migration {
|
||||
version: 1,
|
||||
name: "m1",
|
||||
detect: |c| has_column(c, "t", "c1"),
|
||||
up: &[
|
||||
"CREATE TABLE t(id INTEGER PRIMARY KEY)",
|
||||
"ALTER TABLE t ADD COLUMN c1 TEXT",
|
||||
],
|
||||
};
|
||||
let m2_bad = Migration {
|
||||
version: 2,
|
||||
name: "m2-bad",
|
||||
detect: |c| has_column(c, "t", "c2"),
|
||||
up: &["ALTER TABLE t ADD COLUMN c2 TEXT", "THIS IS NOT VALID SQL"],
|
||||
};
|
||||
let err = apply_migrations_with(&mut conn, &[m1, m2_bad]).unwrap_err();
|
||||
assert!(err.to_string().contains("M2 (m2-bad) 失败"));
|
||||
// M1 已提交、版本停在 1;M2 回滚(c2 未建)。
|
||||
assert_eq!(current_version(&conn).unwrap(), 1);
|
||||
assert!(!has_column(&conn, "t", "c2").unwrap());
|
||||
// 重跑:M1(version 1)跳过,M2 修正后成功。
|
||||
let m2_good = Migration {
|
||||
version: 2,
|
||||
name: "m2-good",
|
||||
detect: |c| has_column(c, "t", "c2"),
|
||||
up: &["ALTER TABLE t ADD COLUMN c2 TEXT"],
|
||||
};
|
||||
apply_migrations_with(&mut conn, &[m1, m2_good]).unwrap();
|
||||
assert_eq!(current_version(&conn).unwrap(), 2);
|
||||
assert!(has_column(&conn, "t", "c2").unwrap());
|
||||
}
|
||||
|
||||
/// M12/M13 专项(P9 命名拆分):旧库 success_method 值域混用列 → 拆为
|
||||
/// tlusty_success_method(TLUSTY 阶段策略)+ 既有 synspec_success_method,再删 success_method。
|
||||
/// 回填判别:synspec-only 点 success_method 与 synspec_success_method 同值(值域混用冗余),
|
||||
/// 命中该等式 → tlusty 保持 NULL;其余(正常双阶段 cold_run/seed_step)→ tlusty = 原值。
|
||||
#[test]
|
||||
fn m12_m13_split_success_method_backfills_and_drops() {
|
||||
let mut conn = mem_conn();
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE grid_points (
|
||||
name TEXT NOT NULL,
|
||||
workflow_name TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
success_method TEXT,
|
||||
synspec_success_method TEXT
|
||||
);
|
||||
-- 正常双阶段点:success_method = 大气策略 cold_run(≠ 光谱归因 standard)
|
||||
INSERT INTO grid_points (name, workflow_name, status, success_method, synspec_success_method)
|
||||
VALUES ('p_tlusty', 'wf', 'completed', 'cold_run', 'standard');
|
||||
-- 种子步进点
|
||||
INSERT INTO grid_points (name, workflow_name, status, success_method, synspec_success_method)
|
||||
VALUES ('p_seed', 'wf', 'completed', 'seed_step', 'standard');
|
||||
-- synspec-only 点:success_method == synspec_success_method(值域混用冗余)
|
||||
INSERT INTO grid_points (name, workflow_name, status, success_method, synspec_success_method)
|
||||
VALUES ('p_syn', 'wf', 'completed', 'standard', 'standard');
|
||||
-- 失败点:归因为 NULL
|
||||
INSERT INTO grid_points (name, workflow_name, status)
|
||||
VALUES ('p_failed', 'wf', 'failed')",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// M12 单跑:ADD tlusty_success_method + 回填。
|
||||
let m12 = MIGRATIONS.iter().find(|m| m.version == 12).unwrap();
|
||||
apply_migrations_with(&mut conn, &[*m12]).unwrap();
|
||||
let read = |conn: &Connection, name: &str, col: &str| -> Option<String> {
|
||||
// `col` 为测试内写死的列名常量(非用户输入)。
|
||||
conn.query_row(
|
||||
&format!("SELECT {col} FROM grid_points WHERE name = ?1"),
|
||||
rusqlite::params![name],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.ok()
|
||||
};
|
||||
assert_eq!(
|
||||
read(&conn, "p_tlusty", "tlusty_success_method").as_deref(),
|
||||
Some("cold_run"),
|
||||
"双阶段点回填大气策略"
|
||||
);
|
||||
assert_eq!(
|
||||
read(&conn, "p_seed", "tlusty_success_method").as_deref(),
|
||||
Some("seed_step"),
|
||||
"种子步进点回填 seed_step"
|
||||
);
|
||||
assert_eq!(
|
||||
read(&conn, "p_syn", "tlusty_success_method"),
|
||||
None,
|
||||
"synspec-only 点(两列同值)tlusty 保持 NULL"
|
||||
);
|
||||
assert_eq!(read(&conn, "p_failed", "tlusty_success_method"), None);
|
||||
assert_eq!(current_version(&conn).unwrap(), 12);
|
||||
|
||||
// M13 单跑:DROP success_method 列。
|
||||
let m13 = MIGRATIONS.iter().find(|m| m.version == 13).unwrap();
|
||||
apply_migrations_with(&mut conn, &[*m13]).unwrap();
|
||||
assert!(
|
||||
!has_column(&conn, "grid_points", "success_method").unwrap(),
|
||||
"success_method 值域混用列应被删除"
|
||||
);
|
||||
assert!(has_column(&conn, "grid_points", "tlusty_success_method").unwrap());
|
||||
assert!(has_column(&conn, "grid_points", "synspec_success_method").unwrap());
|
||||
// 回填数据在 DROP 后仍保留(tlusty_success_method 是独立列)。
|
||||
assert_eq!(
|
||||
read(&conn, "p_tlusty", "tlusty_success_method").as_deref(),
|
||||
Some("cold_run")
|
||||
);
|
||||
assert_eq!(current_version(&conn).unwrap(), 13);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user