Files
DCTS/crates/server/src/migrations.rs
T
fmq f2700031ce feat(all): seed_step_stab 稳定化种子链与同族种子轮换、ladder/自适应 Teff/丰度轴延拓三级回退与梯级种子入库复用、热启动首末比豁免、workflow 完成翻转回退链阻塞修复、大气收敛权威统计与 M14 回填
收敛攻坚(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 残点实测矩阵
2026-09-02 19:37:25 +08:00

642 lines
31 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.
//! 主库版本化迁移基础设施(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] = &[
// M1Phase 1P1):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",
],
},
// M2Phase 2P2):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)"],
},
// M4Phase 4P5):清除 node_credentials 死列 revoked。
// 新代码不读写它;registration_secret 保留在 nodes(审查 CRITICAL#1/#2pending 节点无
// 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"],
},
// M6Phase 6P8):删除 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"],
},
// M7Phase 5aP6):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"],
},
// M8Phase 5bP6):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",
],
},
// M9Phase 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'"],
},
// M10Phase 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"],
},
// M11H1 活锁修复):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"],
},
// M12P9 命名拆分):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_methodTLUSTY 阶段策略(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_methodM12 已回填 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"],
},
// M142026-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 bootstraptasks 是含全部迁移引用列的最新形态(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());
// M11grid_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);
}
/// M9Phase 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()]
);
// M10snapshots 列改名。
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());
// 重跑:M1version 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_methodTLUSTY 阶段策略)+ 既有 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);
}
/// M142026-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);
}
}