新增: 任务关联工程(module_id 数据层V41 + AI module 写工具6个 + 任务构造补字段)
This commit is contained in:
@@ -45,7 +45,8 @@ pub fn run(conn: &Connection) -> Result<()> {
|
||||
// 什么数据库、Redis 在哪、有没有 MQ"的基础设施上下文。
|
||||
// V33 = 审批重启恢复:ai_conversations 加 pending_approvals TEXT 列,持久化挂起审批快照,
|
||||
// 重启后从 DB 恢复 pending_approvals 内存态,使待审批不丢。
|
||||
let steps: [(i32, fn(&Connection) -> Result<()>); 40] = [
|
||||
// V41 = 任务关联工程模块:tasks.module_id 列(工程系统打底,项目多工程下任务落到具体 module)。
|
||||
let steps: [(i32, fn(&Connection) -> Result<()>); 41] = [
|
||||
(1, migrate_v1),
|
||||
(2, migrate_v2),
|
||||
(3, migrate_v3),
|
||||
@@ -86,6 +87,7 @@ pub fn run(conn: &Connection) -> Result<()> {
|
||||
(38, migrate_v38),
|
||||
(39, migrate_v39),
|
||||
(40, migrate_v40),
|
||||
(41, migrate_v41),
|
||||
];
|
||||
|
||||
for (version, migrate_fn) in steps {
|
||||
@@ -1230,6 +1232,31 @@ fn migrate_v40(conn: &Connection) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// V41: 幂等补 tasks.module_id 列(任务关联到具体工程 module)
|
||||
///
|
||||
/// 工程系统打底(V34 project_modules 表):一个项目可含多个工程(Monorepo 多仓库 /
|
||||
/// 微服务 / 前后端分离),任务此前只能关联到项目(project_id),本列让任务落到具体
|
||||
/// 工程,是后续 IPC / 前端 / AI 按工程筛选任务的数据基础。TEXT NULL 向后兼容:
|
||||
/// 老任务无关联 → None(TaskRecord 字段为 Option<String>)。
|
||||
///
|
||||
/// 外键 REFERENCES project_modules(id) ON DELETE SET NULL:module 被删除时该任务
|
||||
/// module_id 自动置 NULL 解关联(不阻塞删除、不留悬挂引用)。新列默认值 NULL,
|
||||
/// 存量行全为 NULL,foreign_keys=ON 下 ALTER ADD COLUMN 不报错(SQLite 要求
|
||||
/// 带 REFERENCES 的新列默认值为 NULL)。用 PRAGMA 探测列存在性,缺失才 ALTER
|
||||
/// (同 v20/v29 模式),对新库/老库/坏库均安全。
|
||||
fn migrate_v41(conn: &Connection) -> Result<()> {
|
||||
if !column_exists(conn, "tasks", "module_id") {
|
||||
conn.execute(
|
||||
"ALTER TABLE tasks ADD COLUMN module_id TEXT REFERENCES project_modules(id) ON DELETE SET NULL",
|
||||
[],
|
||||
)?;
|
||||
tracing::info!("v41: 补建 tasks.module_id 列(任务关联工程)");
|
||||
}
|
||||
conn.execute("INSERT INTO schema_version (version) VALUES (?)", [41])?;
|
||||
tracing::info!("迁移 v41 完成");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// V21 建表 SQL — 消息拆分存储 ai_messages 表
|
||||
///
|
||||
/// 与 V9_SQL 中的 ai_messages 镜像(V9 给新库,此 const 给老库 V21 迁移用 IF NOT EXISTS)。
|
||||
@@ -1915,7 +1942,7 @@ mod tests {
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 全量迁移测试 — 新库从零跑完整 V1-V40 路径
|
||||
// 全量迁移测试 — 新库从零跑完整 V1-V41 路径
|
||||
// ------------------------------------------------------------
|
||||
// 目的:某 migrate_vN 的 SQL 手滑写错(列名/类型/缺索引/缺表)只能等运行时暴露,
|
||||
// 此测试一次性覆盖全部迁移路径。任何一条迁移 SQL 写错、列名拼错、缺建表
|
||||
@@ -1939,12 +1966,13 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// 全量迁移:新库从零跑完 V1-V40,验证关键表齐全 + 列数 > 0 + 关键列存在。
|
||||
/// 全量迁移:新库从零跑完 V1-V41,验证关键表齐全 + 列数 > 0 + 关键列存在。
|
||||
///
|
||||
/// 覆盖至少:task / ai_conversations / ai_messages / ai_tool_executions /
|
||||
/// conversation_checkpoints / ai_providers / projects / ideas。
|
||||
/// 抽查关键列:ai_providers.enabled/weight、conversation_checkpoints.snapshot、
|
||||
/// tasks.idea_id、project_modules.description/status(这些列由不同 vN 加,任一漏加此处失败)。
|
||||
/// tasks.idea_id、tasks.module_id、project_modules.description/status(这些列由
|
||||
/// 不同 vN 加,任一漏加此处失败)。
|
||||
#[tokio::test]
|
||||
async fn test_full_migration_on_fresh_db() {
|
||||
// 用 Database::open_in_memory 打开新库,内部自动跑 migrations::run() 全量迁移
|
||||
@@ -1989,8 +2017,13 @@ mod tests {
|
||||
column_exists(&conn, "tasks", "idea_id"),
|
||||
"tasks.idea_id 列缺失(V1 建表已带)"
|
||||
);
|
||||
// tasks.module_id(V41 老库 ALTER 补,新库也走 V41——新库应有)
|
||||
assert!(
|
||||
column_exists(&conn, "tasks", "module_id"),
|
||||
"tasks.module_id 列缺失(V41 加)"
|
||||
);
|
||||
|
||||
// 3. schema_version 应推进到 40(全量迁移成功落版本号)
|
||||
// 3. schema_version 应推进到 41(全量迁移成功落版本号)
|
||||
let max_version: i64 = conn
|
||||
.query_row(
|
||||
"SELECT COALESCE(MAX(version), 0) FROM schema_version",
|
||||
@@ -1999,8 +2032,8 @@ mod tests {
|
||||
)
|
||||
.expect("查 schema_version 应成功");
|
||||
assert_eq!(
|
||||
max_version, 40,
|
||||
"全量迁移后 schema_version 应为 40(实际 {}),说明某条 migrate_vN 链路断在中间",
|
||||
max_version, 41,
|
||||
"全量迁移后 schema_version 应为 41(实际 {}),说明某条 migrate_vN 链路断在中间",
|
||||
max_version
|
||||
);
|
||||
|
||||
@@ -2056,13 +2089,13 @@ mod tests {
|
||||
assert_eq!(v_count, 1, "版本号 32 应只写一次");
|
||||
}
|
||||
|
||||
/// G5.5: 全链幂等不变量——V1-V40 每步执行两遍不抛错。
|
||||
/// G5.5: 全链幂等不变量——V1-V41 每步执行两遍不抛错。
|
||||
///
|
||||
/// 首轮 run() 建全 schema;清空 schema_version 强制下一轮从 V1 重跑每步
|
||||
/// (模拟存量库 + 崩溃重跑/版本号回退)。任何 migrate_vN 的裸 ALTER(无 column_exists
|
||||
/// 守卫,如 v32 修前形态)都会在第二遍报 duplicate column 被此测试捕获。
|
||||
#[test]
|
||||
fn v1_to_v40_full_chain_rerun_idempotent() {
|
||||
fn v1_to_v41_full_chain_rerun_idempotent() {
|
||||
let conn = Connection::open_in_memory().expect("open in-memory db");
|
||||
run(&conn).expect("首轮全量迁移应成功");
|
||||
let max_v: i64 = conn
|
||||
@@ -2072,7 +2105,7 @@ mod tests {
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(max_v, 40, "首轮应推进到 40");
|
||||
assert_eq!(max_v, 41, "首轮应推进到 41");
|
||||
|
||||
// 清空版本表强制全链第二遍(每步 execute 第二次)
|
||||
conn.execute("DELETE FROM schema_version", []).unwrap();
|
||||
@@ -2084,6 +2117,77 @@ mod tests {
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(max_v2, 40, "重跑后应重新推进到 40");
|
||||
assert_eq!(max_v2, 41, "重跑后应重新推进到 41");
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// V41 迁移幂等安全(任务关联工程 module)
|
||||
// ============================================================
|
||||
|
||||
/// 构造最小老库 schema:tasks 表(无 module_id 列,模拟 V40 前老形态)+ project_modules
|
||||
/// 表(V41 外键引用目标,工程系统 V34)+ schema_version。
|
||||
fn setup_legacy_tasks_no_module_db() -> Connection {
|
||||
let conn = Connection::open_in_memory().expect("open in-memory db");
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE schema_version (version INTEGER PRIMARY KEY);
|
||||
CREATE TABLE project_modules (
|
||||
id TEXT PRIMARY KEY,
|
||||
project_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
path TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE tasks (
|
||||
id TEXT PRIMARY KEY,
|
||||
project_id TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
status TEXT NOT NULL DEFAULT 'todo',
|
||||
priority INTEGER NOT NULL DEFAULT 2,
|
||||
branch_name TEXT,
|
||||
assignee TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);",
|
||||
)
|
||||
.expect("create legacy tables");
|
||||
conn
|
||||
}
|
||||
|
||||
/// 老库无 module_id 列:迁移应补建 + 写版本号 41
|
||||
#[test]
|
||||
fn v41_legacy_db_adds_module_id_column() {
|
||||
let conn = setup_legacy_tasks_no_module_db();
|
||||
assert!(
|
||||
!column_exists(&conn, "tasks", "module_id"),
|
||||
"迁移前应无 module_id 列"
|
||||
);
|
||||
|
||||
migrate_v41(&conn).expect("v41 应在老库补建 module_id 列");
|
||||
|
||||
assert!(
|
||||
column_exists(&conn, "tasks", "module_id"),
|
||||
"迁移后应有 module_id 列"
|
||||
);
|
||||
let v: i64 = conn
|
||||
.query_row("SELECT MAX(version) FROM schema_version", [], |r| r.get(0))
|
||||
.unwrap();
|
||||
assert_eq!(v, 41, "应写入版本号 41");
|
||||
}
|
||||
|
||||
/// 幂等重跑:列已存在时跳过 ALTER,不报 duplicate column(对齐 v20/v32 守卫模式)
|
||||
#[test]
|
||||
fn v41_column_exists_skips_alter() {
|
||||
let conn = setup_legacy_tasks_no_module_db();
|
||||
migrate_v41(&conn).expect("首次迁移");
|
||||
assert!(column_exists(&conn, "tasks", "module_id"));
|
||||
|
||||
// 手动回退版本号模拟「列已存在但版本号未写」场景,验证 ALTER 被短路不报 duplicate column
|
||||
conn.execute("DELETE FROM schema_version WHERE version = 41", [])
|
||||
.unwrap();
|
||||
migrate_v41(&conn).expect("列存在时应跳过 ALTER 不报错");
|
||||
let v: i64 = conn
|
||||
.query_row("SELECT MAX(version) FROM schema_version", [], |r| r.get(0))
|
||||
.unwrap();
|
||||
assert_eq!(v, 41);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user