收敛攻坚(81/400 失败点根因与实测,见 docs/failed81_cno_seed_popzer_dpsilg_2026_08_18.md): - runner: 新增 seed_step_stab 策略链——同物理族(同 Teff/logg/logHe)CNO 邻居种子 + DPSILG=3.0 λ 算子欠松弛 + POPZER=1E-10 微布居置零(联动 POPZR2/RADZER 同值 + NITZER=1),针对 20kK He 富大气 He I/II 电离前沿布居极限环 - runner: 三级链内延拓回退(主链与 nl_direct 全败后自动触发): · 固定 ladder 步进——plan_ladder_steps 按归一化间隔选轴,Δlogg≤0.25/ΔTeff≤2.5kK,≤4 步 · 自适应 Teff 延拓——步长 1250K 起、成功 ×1.5 恢复、失败二分至 25K 折叠墙,≤48 步 · C/N 丰度轴延拓——高温域(Teff>30kK)专用,严格同族种子沿 C(优先)/N 轴 0.2 dex 起步 延拓阶段 NITER 下限提至 300(慢收敛 waypoint 迭代饥饿误判修复) - runner: 稳定化多档回退(DPSILG/POPZER 三档互补,联合回收 41/65); 域门控 Teff≤30kK——高温高金属域实测旋钮致散(17 拍爆至 4e16),域外跳过 - 梯级种子持久化: 收敛中间模型登记 ladder_seeds 随上报落 server seeds 表 (任务失败也上传,合成名 _ladder 与真实网格点零冲突),簇内相邻失败点自动复用 调度与执行: - scheduler: 策略解析新增 seed_step_stab 臂——find_exact_family_seed_from_db 严格 同物理族判定(不做 global 退化,防 ladder 中间种子 ΔTeff≤5000K 误命中), 排除本点历史已用种子实现重试轮换;链在 stab 耗尽时轮换未试过同族邻居重派 - executor: seed_step_stab 补种子下载(漏列曾致 78 任务假失败,seed_nc 无 fort.8 崩溃); Teff>30kK 域外自动降级普通种子链 收敛判据: - conv_check: 热启动豁免——首拍 max_relc<1(种子已近解)时首末比 1e3 判据数学上 不可达,豁免后交五重物理硬门槛裁决(修复 nl_ladder 0.038→6e-4 物理全过被误杀); 冷启动仍受判据门控 workflow 生命周期: - workflows/tasks: 完成 flip 增加「未消费回退链」阻塞子句——failed 点策略链未耗尽 或链尾 seed_step_stab 尚有未试过同族种子时不得置 completed(修复最后活跃点 cold 失败上报抢先 flip、still_running 守卫拦截后续策略永不派发);按 failed_stage 归因 (synspec 失败行只看 synspec 链,防 stale 审计副本永久卡死)+ json_valid 脏行防护 统计与前端: - 统计新增权威口径 tlusty_converged(不按策略拆)与 seed_step_stab_converged 分项, 前端详情页色带/概览卡消费权威总数并新增稳定化青色段(修复 stab 收敛点漏计, 生产 9137/9216 差额);M14 迁移回填历史 completed 点的 tlusty_status 文档: - 新增 failed81 POPZER/DPSILG 制胜配方根因分析、Windows 节点经跳板 RDP 运维手册; failed400 增补 ladder 生产化实现与第二轮 121 残点实测矩阵
642 lines
31 KiB
Rust
642 lines
31 KiB
Rust
//! 主库版本化迁移基础设施(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"],
|
||
},
|
||
// M14(2026-08-25,大气收敛统计补漏):Phase 5b 阶段列(tlusty_status)上线前完成的历史
|
||
// 点该列为 NULL——但 tlusty_success_method 仅在整管线成功(tlusty_enabled=1 且收敛)时
|
||
// 写入(record_task_report 成功分支),故 completed + method 非空 ⟹ 大气必已收敛。
|
||
// 回填 'converged',否则 stats 的 CASE WHEN tlusty_status='converged' 漏计
|
||
// (生产实证:sdB_cno 9216 点中 9 个此形态,前端 9137/9216 差额的一部分)。
|
||
Migration {
|
||
version: 14,
|
||
name: "backfill-tlusty-status-for-completed",
|
||
detect: |c| {
|
||
let n: i64 = c.query_row(
|
||
"SELECT COUNT(*) FROM grid_points
|
||
WHERE status = 'completed'
|
||
AND COALESCE(tlusty_success_method, '') != ''
|
||
AND COALESCE(tlusty_status, '') = ''",
|
||
[],
|
||
|r| r.get(0),
|
||
)?;
|
||
Ok(n == 0)
|
||
},
|
||
up: &[
|
||
"UPDATE grid_points SET tlusty_status = 'converged' \
|
||
WHERE status = 'completed' \
|
||
AND COALESCE(tlusty_success_method, '') != '' \
|
||
AND COALESCE(tlusty_status, '') = ''",
|
||
],
|
||
},
|
||
];
|
||
|
||
/// 当前 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);
|
||
}
|
||
|
||
/// M14(2026-08-25):completed + tlusty_success_method 非空 + tlusty_status 空
|
||
/// 的历史点回填 'converged'(method 仅在整管线成功时写入);其它形态不动。
|
||
#[test]
|
||
fn m14_backfills_tlusty_status_for_legacy_completed() {
|
||
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',
|
||
tlusty_success_method TEXT,
|
||
synspec_success_method TEXT,
|
||
tlusty_status TEXT,
|
||
synspec_status TEXT,
|
||
pending_strategies TEXT
|
||
);
|
||
INSERT INTO grid_points (name, workflow_name, status, tlusty_success_method, tlusty_status) VALUES
|
||
('legacy_ok', 'wf', 'completed', 'cold_run', NULL),
|
||
('legacy_seed', 'wf', 'completed', 'seed_step', ''),
|
||
('already_failed', 'wf', 'failed', 'cold_run', 'failed'),
|
||
('already_conv', 'wf', 'completed', 'cold_run', 'converged'),
|
||
('no_method', 'wf', 'completed', NULL, NULL);",
|
||
)
|
||
.unwrap();
|
||
|
||
// 仅跑 M14(全量 MIGRATIONS 含 M1 的 tasks 迁移,本夹具只建 grid_points)。
|
||
let m14 = MIGRATIONS.iter().find(|m| m.version == 14).unwrap();
|
||
apply_migrations_with(&mut conn, &[*m14]).unwrap();
|
||
|
||
let read = |c: &Connection, name: &str| -> Option<String> {
|
||
c.query_row(
|
||
"SELECT tlusty_status FROM grid_points WHERE name = ?1",
|
||
rusqlite::params![name],
|
||
|r| r.get(0),
|
||
)
|
||
.unwrap()
|
||
};
|
||
assert_eq!(read(&conn, "legacy_ok").as_deref(), Some("converged"));
|
||
assert_eq!(read(&conn, "legacy_seed").as_deref(), Some("converged"));
|
||
// failed 点 / 已有状态 / 无 method 的点不受影响。
|
||
assert_eq!(read(&conn, "already_failed").as_deref(), Some("failed"));
|
||
assert_eq!(read(&conn, "already_conv").as_deref(), Some("converged"));
|
||
assert_eq!(read(&conn, "no_method"), None);
|
||
assert_eq!(current_version(&conn).unwrap(), 14);
|
||
|
||
// 幂等:再跑一遍 detect 命中(无待回填行)→ 仅推进版本,不改数据。
|
||
apply_migrations_with(&mut conn, &[*m14]).unwrap();
|
||
assert_eq!(read(&conn, "no_method"), None);
|
||
}
|
||
}
|