新增: F-260619-01 任务可关联灵感(tasks.idea_id 1对1 单向)
- df-storage V20 迁移:tasks 加 idea_id TEXT REFERENCES ideas(id)(填预留空位, 幂等 column_exists 探测)+ V9_SQL 同步 + TaskRecord.idea_id + task_repo SQL/SELECT + 白名单 - AI 工具 create_task 加 idea_id 参数;update_task 经白名单自动支持 - IPC commands/task.rs CreateTaskInput 加 idea_id - 前端 types TaskRecord/CreateTaskInput idea_id + TaskDetail.vue 关联灵感展示 (router-link 友好 title 非裸 id)+ i18n - 补 idea_id 字段:df-mcp tools / df-nodes 测试 helper - 单向 1对1,Option 可空,复用 projects.idea_id 模式,老任务 None 兼容 自验: df-storage 47 passed(含 V20 2)+ workspace EXIT 0 + vue-tsc EXIT 0
This commit is contained in:
@@ -23,8 +23,8 @@ pub fn run(conn: &Connection) -> Result<()> {
|
||||
|
||||
// 迁移步骤链: 顺序执行,跳过已应用的版本(current_version < N 才跑)。
|
||||
// 新增版本时,在此数组追加一项 (N, migrate_vN) 即可,无需改逻辑。
|
||||
// V20 预留给 F-260619-01(任务关联灵感),跳过;V21 = 消息拆分存储 + audit message_id。
|
||||
let steps: [(i32, fn(&Connection) -> Result<()>); 20] = [
|
||||
// V20 = F-260619-01(任务关联灵感 idea_id);V21 = 消息拆分存储 + audit message_id。
|
||||
let steps: [(i32, fn(&Connection) -> Result<()>); 21] = [
|
||||
(1, migrate_v1),
|
||||
(2, migrate_v2),
|
||||
(3, migrate_v3),
|
||||
@@ -44,6 +44,7 @@ pub fn run(conn: &Connection) -> Result<()> {
|
||||
(17, migrate_v17),
|
||||
(18, migrate_v18),
|
||||
(19, migrate_v19),
|
||||
(20, migrate_v20),
|
||||
(21, migrate_v21),
|
||||
];
|
||||
|
||||
@@ -350,6 +351,26 @@ fn migrate_v19(conn: &Connection) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// V20:幂等补 tasks.idea_id 列(F-260619-01 任务关联灵感,1对1 单向)
|
||||
///
|
||||
/// 任务可关联到一条灵感(任务→灵感单向),复用 projects.idea_id 模式
|
||||
/// (REFERENCES ideas(id) 外键)。TEXT NULL 向后兼容:老库行默认 NULL,TaskRecord
|
||||
/// 字段为 Option<String>(未关联灵感的任务为 None)。
|
||||
/// 用 PRAGMA 探测列存在性,缺失才 ALTER(同 v11/v14/v17 模式),对新库/老库均安全。
|
||||
/// 新库已在 V9_SQL(tasks 建表)直接带 idea_id 列,此处只补老库。
|
||||
fn migrate_v20(conn: &Connection) -> Result<()> {
|
||||
if !column_exists(conn, "tasks", "idea_id") {
|
||||
conn.execute(
|
||||
"ALTER TABLE tasks ADD COLUMN idea_id TEXT REFERENCES ideas(id)",
|
||||
[],
|
||||
)?;
|
||||
tracing::info!("v20: 补建 tasks.idea_id 列(任务关联灵感,F-260619-01)");
|
||||
}
|
||||
conn.execute("INSERT INTO schema_version (version) VALUES (?)", [20])?;
|
||||
tracing::info!("迁移 v20 完成");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// V21:消息拆分存储(ai_messages 表 + 全量迁移)+ ai_tool_executions.message_id 列
|
||||
///
|
||||
/// **一次原子迁移**(决策 V21 合并,不拆 V21a/V21b):
|
||||
@@ -554,6 +575,8 @@ CREATE TABLE IF NOT EXISTS tasks (
|
||||
priority INTEGER NOT NULL DEFAULT 2,
|
||||
branch_name TEXT,
|
||||
assignee TEXT,
|
||||
-- F-260619-01 任务关联灵感(1对1 单向,复用 projects.idea_id 模式)。老库由 V20 迁移补列。
|
||||
idea_id TEXT REFERENCES ideas(id),
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
@@ -997,4 +1020,72 @@ mod tests {
|
||||
"迁移后应有 message_id 列"
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// V20 迁移幂等安全(F-260619-01 任务关联灵感)
|
||||
// ============================================================
|
||||
|
||||
/// 构造最小老库 schema:tasks 表(无 idea_id 列,模拟 V1 建表老形态)+ schema_version。
|
||||
fn setup_legacy_tasks_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 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 tasks table");
|
||||
conn
|
||||
}
|
||||
|
||||
/// 老库无 idea_id 列:迁移应补建 + 写版本号 20
|
||||
#[test]
|
||||
fn v20_legacy_db_adds_idea_id_column() {
|
||||
let conn = setup_legacy_tasks_db();
|
||||
assert!(
|
||||
!column_exists(&conn, "tasks", "idea_id"),
|
||||
"迁移前应无 idea_id 列"
|
||||
);
|
||||
|
||||
migrate_v20(&conn).expect("v20 应在老库补建 idea_id 列");
|
||||
|
||||
assert!(
|
||||
column_exists(&conn, "tasks", "idea_id"),
|
||||
"迁移后应有 idea_id 列"
|
||||
);
|
||||
let v: i64 = conn
|
||||
.query_row("SELECT MAX(version) FROM schema_version", [], |r| r.get(0))
|
||||
.unwrap();
|
||||
assert_eq!(v, 20, "应写入版本号 20");
|
||||
}
|
||||
|
||||
/// 幂等重跑:列已存在时跳过 ALTER,版本号不重复写(PK 冲突防)
|
||||
/// 注:migrate_v20 用普通 INSERT(非 IGNORE),重跑会因 PK 冲突报错——这是预期行为,
|
||||
/// run() 正常流程下 current_version<20 只调一次;此处验证列存在时 ALTER 被短路(不报 duplicate column)。
|
||||
#[test]
|
||||
fn v20_column_exists_skips_alter() {
|
||||
let conn = setup_legacy_tasks_db();
|
||||
// 先跑一次补列
|
||||
migrate_v20(&conn).expect("首次迁移");
|
||||
assert!(column_exists(&conn, "tasks", "idea_id"));
|
||||
|
||||
// 手动回退版本号模拟「列已存在但版本号未写」场景,验证 ALTER 被短路不报 duplicate column
|
||||
conn.execute("DELETE FROM schema_version WHERE version = 20", [])
|
||||
.unwrap();
|
||||
// 此时列存在但版本号 20 缺失 → migrate_v20 应跳过 ALTER 只补版本号
|
||||
migrate_v20(&conn).expect("列存在时应跳过 ALTER 不报错");
|
||||
let v: i64 = conn
|
||||
.query_row("SELECT MAX(version) FROM schema_version", [], |r| r.get(0))
|
||||
.unwrap();
|
||||
assert_eq!(v, 20);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user