优化: 存储层清债与子轨迹来源迁移
- V44 加 ai_conversations.origin 列(子代理轨迹来源标记) - V21 断点续迁改造(逐对话进度判定,崩溃重跑补漏) - 手写事务改 guard.transaction,panic 不残留打开事务 - 软删 CAS 补 deleted_at IS NULL(任务/项目/灵感三处) - 父任务校验拒回收站挂子,删模块先清依赖边 - 列缺失与未知 status 降级补 warn 留痕,limit=0 返空集
This commit is contained in:
@@ -47,7 +47,11 @@ pub fn run(conn: &Connection) -> Result<()> {
|
||||
// 重启后从 DB 恢复 pending_approvals 内存态,使待审批不丢。
|
||||
// V41 = 任务关联工程模块:tasks.module_id 列(工程系统打底,项目多工程下任务落到具体 module)。
|
||||
// V42 = ai_messages 加 is_estimated 列(消息级估算标记,reload 逐条回显「估算」标注)。
|
||||
let steps: [(i32, fn(&Connection) -> Result<()>); 42] = [
|
||||
// V43 = 存量知识一次性晋升(决策单 B4①:candidate + confidence='high' → published,
|
||||
// 纯数据 UPDATE 不经过 promote 函数,不触发嵌入生成)。
|
||||
// V44 = 子代理轨迹来源标记(ai_conversations.origin TEXT,spawn_agent 出口整轨迹落库,
|
||||
// 桌面侧栏/miniapp 列表按 origin 过滤子会话)。
|
||||
let steps: [(i32, fn(&Connection) -> Result<()>); 44] = [
|
||||
(1, migrate_v1),
|
||||
(2, migrate_v2),
|
||||
(3, migrate_v3),
|
||||
@@ -90,6 +94,8 @@ pub fn run(conn: &Connection) -> Result<()> {
|
||||
(40, migrate_v40),
|
||||
(41, migrate_v41),
|
||||
(42, migrate_v42),
|
||||
(43, migrate_v43),
|
||||
(44, migrate_v44),
|
||||
];
|
||||
|
||||
for (version, migrate_fn) in steps {
|
||||
@@ -433,11 +439,13 @@ fn migrate_v20(conn: &Connection) -> Result<()> {
|
||||
/// **一次原子迁移**(决策 V21 合并,不拆 V21a/V21b):
|
||||
/// 1. 建表 ai_messages(IF NOT EXISTS 幂等,新库空表/老库均安全)
|
||||
/// 2. 幂等补 ai_tool_executions.message_id 列(消息级溯源 audit)
|
||||
/// 3. COUNT 探测 ai_messages 已有数据 → 跳过数据迁移(仅写版本号,防重复迁移)
|
||||
/// 4. 遍历 ai_conversations.messages JSON → 逐条提取到 ai_messages(分批 commit)
|
||||
/// 3. 遍历 ai_conversations.messages JSON → 逐条提取到 ai_messages(分批 commit)
|
||||
/// 4. 逐对话进度判定:已落库数 == JSON 消息数 → 跳过该对话(防重复迁移)
|
||||
///
|
||||
/// 设计要点(详见消息拆分存储设计 §4.2):
|
||||
/// - **幂等安全**:COUNT 探测 + INSERT OR IGNORE,中途崩溃重跑跳过已迁移数据
|
||||
/// - **幂等安全**:逐对话进度判定 + INSERT OR IGNORE,中途崩溃重跑只补未迁对话。
|
||||
/// 不用全局 COUNT 判完成——批量迁移每批一事务,批次中途崩溃后 ai_messages 已有
|
||||
/// 数据,COUNT>0 会整段跳过,剩余对话永不再迁而版本号照样补齐,残缺被永久掩盖
|
||||
/// - **分批 commit**:每 50 对话一批,避免长事务持有 SQLite 写锁
|
||||
/// - **迁移期 ID**:`msg_migrated_{conv_id}_{seq}` —— 天然唯一(UNIQUE 是 conv_id+seq)、零依赖
|
||||
/// - **裸 JSON 提取**:用 `serde_json::Value` 而非 ChatMessage(df-storage 不依赖 df-ai-core)
|
||||
@@ -471,20 +479,7 @@ fn migrate_v21(conn: &Connection) -> Result<()> {
|
||||
tracing::info!("v21: 补建 ai_tool_executions.message_id 列(消息级溯源 audit)");
|
||||
}
|
||||
|
||||
// 3. COUNT 探测:ai_messages 已有数据 → 跳过迁移只写版本号(幂等)
|
||||
// INSERT OR IGNORE 防崩溃重跑(schema_version PK 冲突):run() 正常流程只调
|
||||
// 一次 migrate_v21(current_version<21),但崩溃重跑/手动重调时 version=21
|
||||
// 可能已存在,IGNORE 保证幂等不报错。
|
||||
let existing: i64 = conn.query_row(
|
||||
"SELECT COUNT(*) FROM ai_messages", [], |row| row.get(0),
|
||||
)?;
|
||||
if existing > 0 {
|
||||
tracing::info!("v21: ai_messages 已有 {} 条,跳过数据迁移", existing);
|
||||
conn.execute("INSERT OR IGNORE INTO schema_version (version) VALUES (?)", [21])?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 4. 遍历 ai_conversations,反序列化 messages JSON → 逐条写入 ai_messages
|
||||
// 3. 遍历 ai_conversations,反序列化 messages JSON → 逐条写入 ai_messages
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, messages, created_at FROM ai_conversations",
|
||||
)?;
|
||||
@@ -497,13 +492,13 @@ fn migrate_v21(conn: &Connection) -> Result<()> {
|
||||
})?;
|
||||
let all_rows: Vec<(String, String, String)> = rows.collect::<std::result::Result<Vec<_>, _>>()?;
|
||||
|
||||
// 5. 分批 commit(每 50 个对话一批,避免长事务持有写锁)
|
||||
// 4. 分批 commit(每 50 个对话一批,避免长事务持有写锁)
|
||||
const BATCH_SIZE: usize = 50;
|
||||
let mut migrated_count: usize = 0;
|
||||
for (batch_idx, batch) in all_rows.chunks(BATCH_SIZE).enumerate() {
|
||||
let tx = conn.unchecked_transaction()?;
|
||||
for (conv_id, messages_json, conv_created_at) in batch {
|
||||
// 6. 逐对话反序列化 messages JSON → Vec<serde_json::Value>
|
||||
// 5. 逐对话反序列化 messages JSON → Vec<serde_json::Value>
|
||||
// (用裸 JSON 而非 ChatMessage,因 df-storage 不依赖 df-ai-core)
|
||||
let messages: Vec<serde_json::Value> = match serde_json::from_str(messages_json) {
|
||||
Ok(v) => v,
|
||||
@@ -513,8 +508,23 @@ fn migrate_v21(conn: &Connection) -> Result<()> {
|
||||
}
|
||||
};
|
||||
|
||||
// 逐对话进度判定(幂等核心):批事务保证对话级原子——一个对话的全部消息
|
||||
// 在同一事务内写入,要么全落库要么全无,不存在半迁持久态。已落库数等于
|
||||
// JSON 消息数即该对话已完整迁移,跳过;否则(未迁/断点残缺)整对话重跑,
|
||||
// 行级 INSERT OR IGNORE 兜底去重。中途崩溃重跑只补未迁对话,不再被
|
||||
// 全局 COUNT>0 整段短路。
|
||||
let json_count = messages.len() as i64;
|
||||
let db_count: i64 = tx.query_row(
|
||||
"SELECT COUNT(*) FROM ai_messages WHERE conversation_id = ?1",
|
||||
rusqlite::params![conv_id],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
if json_count > 0 && db_count == json_count {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (seq, msg) in messages.iter().enumerate() {
|
||||
// 7. 逐条消息提取字段 → INSERT INTO ai_messages
|
||||
// 6. 逐条消息提取字段 → INSERT INTO ai_messages
|
||||
// 字段名硬编码("role"/"content" 等)——ChatMessage 改名会漏数据!
|
||||
// 迁移期 ID 天然唯一(UNIQUE 是 conv_id+seq),人类可读,零依赖
|
||||
let id = format!("msg_migrated_{}_{}", conv_id, seq);
|
||||
@@ -1275,6 +1285,55 @@ fn migrate_v42(conn: &Connection) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// V43: 存量知识一次性晋升 — candidate 且 confidence='high' 批量转 published
|
||||
///
|
||||
/// 决策单 B4①(docs/02-架构设计/专项设计/aichat上下文管理-派工与决策单-2026-09-29.md ①,
|
||||
/// 2026-09-29)。DB 实测:knowledges 表 candidate 967 条(其中 confidence='high' 854 条)
|
||||
/// 长期滞留,而检索侧(search/vector/top_reused)只认 published(53 条)——AI 提炼的知识
|
||||
/// 从未进入检索面,知识库沉淀近乎零生效。本迁移把存量高置信 candidate 一次性晋升,
|
||||
/// 与代码侧「复用即信任」(KnowledgeRepo::auto_publish_if_reused)互补:前者清历史存量,
|
||||
/// 后者管增量沉淀。
|
||||
///
|
||||
/// 设计要点:
|
||||
/// - **纯数据 UPDATE,不经过 promote 函数**:不触发嵌入生成(854 条不会在启动时批量调
|
||||
/// 嵌入 API);embedding NULL 的条目检索走 LIKE 降级,向量面由后续复用/晋升路径逐步补
|
||||
/// ——这是有意为之,非遗漏。也不写 updated_at(非内容变更,不伪造新近度)。
|
||||
/// - **幂等双保险**:schema_version 版本号保证只跑一次(run() 按 current_version<43 跳过);
|
||||
/// 即便崩溃重跑/版本号回退重调本函数,WHERE 条件天然幂等(已晋升行不再命中,UPDATE
|
||||
/// 影响 0 行),版本号 INSERT OR IGNORE 防 PK 冲突(同 V21/V32 崩溃重跑防御模式)。
|
||||
/// - **可逆方式**(备用,当前不执行):本迁移不落版本特征列,knowledges.updated_at 由业务
|
||||
/// 路径维护、无法精确圈定本次影响集,可逆按晋升前后 confidence 不变这一事实圈定:
|
||||
/// `UPDATE knowledges SET status='candidate' WHERE status='published' AND confidence='high'`
|
||||
/// ——注意此语句会连带回退「非本迁移产生的 published+high」条目(人工放行/复用达标晋升的
|
||||
/// 高置信条目),执行前需以 knowledge_events 审计表核对时间窗。
|
||||
fn migrate_v43(conn: &Connection) -> Result<()> {
|
||||
let affected = conn.execute(
|
||||
"UPDATE knowledges SET status = 'published' \
|
||||
WHERE status = 'candidate' AND confidence = 'high'",
|
||||
[],
|
||||
)?;
|
||||
conn.execute("INSERT OR IGNORE INTO schema_version (version) VALUES (?)", [43])?;
|
||||
tracing::info!("迁移 v43 完成(存量知识 candidate+confidence=high 晋升 published,共 {} 条)", affected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// V44: 子代理轨迹来源标记 — ai_conversations 加 origin TEXT 列。
|
||||
///
|
||||
/// 子代理会话(spawn_agent)从「纯内存零落库」改为出口整轨迹落库(二期池①),落库时
|
||||
/// origin='sub_agent' 随首插写入,桌面侧栏与 miniapp 会话列表按此过滤——对话本体可追溯
|
||||
/// 而不污染主会话列表。主会话 origin 恒为 NULL(业务层 None),无其他写入点。
|
||||
/// - **幂等**:column_exists 守卫(同 V42 模式),崩溃重跑/全链两遍不炸;
|
||||
/// - **可逆**:`ALTER TABLE ai_conversations DROP COLUMN origin`(SQLite 3.35+)。
|
||||
fn migrate_v44(conn: &Connection) -> Result<()> {
|
||||
if !column_exists(conn, "ai_conversations", "origin") {
|
||||
conn.execute("ALTER TABLE ai_conversations ADD COLUMN origin TEXT", [])?;
|
||||
tracing::info!("v44: ai_conversations 加 origin 列(子代理轨迹来源标记)");
|
||||
}
|
||||
conn.execute("INSERT OR IGNORE INTO schema_version (version) VALUES (?)", [44])?;
|
||||
tracing::info!("迁移 v44 完成: ai_conversations 加 origin 列");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// V21 建表 SQL — 消息拆分存储 ai_messages 表
|
||||
///
|
||||
/// 与 V9_SQL 中的 ai_messages 镜像(V9 给新库,此 const 给老库 V21 迁移用 IF NOT EXISTS)。
|
||||
@@ -1742,7 +1801,7 @@ mod tests {
|
||||
assert_eq!(bad_count, 0);
|
||||
}
|
||||
|
||||
/// 幂等重跑:第二次 migrate_v21 不重复迁移(COUNT 探测跳过)
|
||||
/// 幂等重跑:第二次 migrate_v21 按逐对话进度判定跳过已迁对话,不重复迁移
|
||||
#[test]
|
||||
fn v21_idempotent_rerun() {
|
||||
let conn = setup_legacy_db();
|
||||
@@ -1759,7 +1818,7 @@ mod tests {
|
||||
.unwrap();
|
||||
assert_eq!(count_after_first, 1);
|
||||
|
||||
// 第二次跑:COUNT 探测 > 0 → 跳过数据迁移,不重复
|
||||
// 第二次跑:逐对话进度判定命中 → 跳过,不重复
|
||||
migrate_v21(&conn).expect("二次迁移应幂等成功");
|
||||
let count_after_second: i64 = conn
|
||||
.query_row("SELECT COUNT(*) FROM ai_messages", [], |r| r.get(0))
|
||||
@@ -1778,6 +1837,141 @@ mod tests {
|
||||
assert_eq!(v_count, 1, "版本号 21 应只写一次");
|
||||
}
|
||||
|
||||
/// 断点续迁:批次中途崩溃后 ai_messages 已有部分对话数据、版本号未写,
|
||||
/// 重跑必须补齐未迁对话,不得因「表已有数据」整段跳过。
|
||||
/// 旧实现全局 COUNT>0 即跳过全部数据迁移,此场景下 conv_b 永不再迁而版本号照样补齐。
|
||||
#[test]
|
||||
fn v21_crash_midway_resumes_missing_convs() {
|
||||
let conn = setup_legacy_db();
|
||||
let msgs_a = serde_json::json!([
|
||||
{"role": "user", "content": "a1"},
|
||||
{"role": "assistant", "content": "a2"}
|
||||
])
|
||||
.to_string();
|
||||
let msgs_b = serde_json::json!([{"role": "user", "content": "b1"}]).to_string();
|
||||
conn.execute(
|
||||
"INSERT INTO ai_conversations (id, messages, created_at, updated_at) VALUES ('conv_a', ?1, '0', '0')",
|
||||
rusqlite::params![msgs_a],
|
||||
)
|
||||
.unwrap();
|
||||
conn.execute(
|
||||
"INSERT INTO ai_conversations (id, messages, created_at, updated_at) VALUES ('conv_b', ?1, '0', '0')",
|
||||
rusqlite::params![msgs_b],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// 模拟崩溃断点:表已建(V21_SQL,首次跑 migrate_v21 的产物)、conv_a 已按迁移格式
|
||||
// 落库(其批次已 commit),conv_b 未迁,版本号未写
|
||||
conn.execute_batch(V21_SQL).unwrap();
|
||||
for seq in 0..2i64 {
|
||||
conn.execute(
|
||||
"INSERT INTO ai_messages (id, conversation_id, seq, role, content, status, created_at)
|
||||
VALUES (?1, 'conv_a', ?2, 'user', ?3, 'active', '0')",
|
||||
rusqlite::params![
|
||||
format!("msg_migrated_conv_a_{}", seq),
|
||||
seq,
|
||||
format!("a{}", seq)
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
let version_before: i64 = conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM schema_version WHERE version = 21",
|
||||
[],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(version_before, 0, "崩溃断点:版本号尚未写入");
|
||||
|
||||
migrate_v21(&conn).expect("重跑应补齐未迁对话");
|
||||
|
||||
// conv_b 被补迁(旧实现此处为 0 —— 被 COUNT>0 整段跳过)
|
||||
let b_count: i64 = conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM ai_messages WHERE conversation_id = 'conv_b'",
|
||||
[],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(b_count, 1, "未迁对话应被补齐");
|
||||
|
||||
// conv_a 不重复迁移
|
||||
let a_count: i64 = conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM ai_messages WHERE conversation_id = 'conv_a'",
|
||||
[],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(a_count, 2, "已迁对话不应重复插入");
|
||||
|
||||
let v: i64 = conn
|
||||
.query_row("SELECT MAX(version) FROM schema_version", [], |r| r.get(0))
|
||||
.unwrap();
|
||||
assert_eq!(v, 21, "重跑后版本号应补齐");
|
||||
}
|
||||
|
||||
/// 批次边界断点续迁:55 个对话分两批(每批 50),首批已 commit 后崩溃,
|
||||
/// 重跑应只补剩余批次,全部对话迁移完整且无重复。
|
||||
#[test]
|
||||
fn v21_batch_boundary_crash_resumes_remaining_batch() {
|
||||
let conn = setup_legacy_db();
|
||||
let total = 55usize;
|
||||
let batch_done = 50usize;
|
||||
for i in 0..total {
|
||||
let msgs = serde_json::json!([
|
||||
{"role": "user", "content": format!("q{}", i)},
|
||||
{"role": "assistant", "content": format!("r{}", i)}
|
||||
])
|
||||
.to_string();
|
||||
conn.execute(
|
||||
"INSERT INTO ai_conversations (id, messages, created_at, updated_at) VALUES (?1, ?2, '0', '0')",
|
||||
rusqlite::params![format!("conv_{:03}", i), msgs],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
// 模拟崩溃断点:表已建(V21_SQL)、首批 50 对话已按迁移格式落库(批次已 commit),其余未迁
|
||||
conn.execute_batch(V21_SQL).unwrap();
|
||||
for i in 0..batch_done {
|
||||
for seq in 0..2i64 {
|
||||
conn.execute(
|
||||
"INSERT INTO ai_messages (id, conversation_id, seq, role, content, status, created_at)
|
||||
VALUES (?1, ?2, ?3, 'user', '', 'active', '0')",
|
||||
rusqlite::params![
|
||||
format!("msg_migrated_conv_{:03}_{}", i, seq),
|
||||
format!("conv_{:03}", i),
|
||||
seq
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
migrate_v21(&conn).expect("重跑应补齐剩余批次");
|
||||
|
||||
let total_count: i64 = conn
|
||||
.query_row("SELECT COUNT(*) FROM ai_messages", [], |r| r.get(0))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
total_count as usize,
|
||||
total * 2,
|
||||
"55 对话 × 2 条,无重复无缺失"
|
||||
);
|
||||
|
||||
// 逐对话完整性:不存在条数不足的残缺对话
|
||||
let incomplete: i64 = conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM (
|
||||
SELECT conversation_id FROM ai_messages GROUP BY conversation_id HAVING COUNT(*) < 2
|
||||
)",
|
||||
[],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(incomplete, 0, "不应存在残缺对话");
|
||||
}
|
||||
|
||||
/// ai_tool_executions.message_id 列补建(老库已有表无该列)
|
||||
#[test]
|
||||
fn v21_adds_message_id_column_to_tool_executions() {
|
||||
@@ -1962,7 +2156,108 @@ mod tests {
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 全量迁移测试 — 新库从零跑完整 V1-V41 路径
|
||||
// V43 迁移(存量知识一次性晋升)
|
||||
// ============================================================
|
||||
|
||||
/// 构造最小 knowledges 表(schema_version 同建),只含 V43 触及的列。
|
||||
fn setup_knowledges_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 knowledges (
|
||||
id TEXT PRIMARY KEY,
|
||||
status TEXT NOT NULL DEFAULT 'candidate',
|
||||
confidence TEXT
|
||||
);",
|
||||
)
|
||||
.expect("create knowledges table");
|
||||
conn
|
||||
}
|
||||
|
||||
/// V43:仅 candidate + confidence='high' 晋升,其余状态/置信度不动
|
||||
#[test]
|
||||
fn v43_promotes_only_candidate_high_confidence() {
|
||||
let conn = setup_knowledges_db();
|
||||
conn.execute_batch(
|
||||
"INSERT INTO knowledges (id, status, confidence) VALUES
|
||||
('k1', 'candidate', 'high'),
|
||||
('k2', 'candidate', 'high'),
|
||||
('k3', 'candidate', 'medium'),
|
||||
('k4', 'candidate', 'low'),
|
||||
('k5', 'candidate', NULL),
|
||||
('k6', 'pending_review', 'high'),
|
||||
('k7', 'published', 'high'),
|
||||
('k8', 'archived', 'high');",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
migrate_v43(&conn).expect("v43 应成功晋升");
|
||||
|
||||
// k1/k2 转 published
|
||||
let published_high: i64 = conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM knowledges WHERE status = 'published' AND confidence = 'high'",
|
||||
[], |r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(published_high, 3, "2 条存量晋升 + 原 k7 = 3 条 published+high");
|
||||
|
||||
// 非 candidate+high 一律不动
|
||||
let untouched: i64 = conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM knowledges WHERE id IN ('k3','k4','k5','k6','k8') \
|
||||
AND status NOT IN ('candidate','pending_review','archived')",
|
||||
[], |r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(untouched, 0, "medium/low/NULL 置信与 pending_review/archived 不应被晋升");
|
||||
|
||||
// candidate 剩余:k3/k4/k5(2 条 high 已走)
|
||||
let candidates_left: i64 = conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM knowledges WHERE status = 'candidate'",
|
||||
[], |r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(candidates_left, 3, "仅剩 medium/low/NULL 置信的 candidate");
|
||||
|
||||
let v: i64 = conn
|
||||
.query_row("SELECT MAX(version) FROM schema_version", [], |r| r.get(0))
|
||||
.unwrap();
|
||||
assert_eq!(v, 43, "应写入版本号 43");
|
||||
}
|
||||
|
||||
/// V43 幂等:重跑 UPDATE 影响 0 行(WHERE 天然幂等,版本号防重入)
|
||||
#[test]
|
||||
fn v43_idempotent_rerun_zero_affected() {
|
||||
let conn = setup_knowledges_db();
|
||||
conn.execute(
|
||||
"INSERT INTO knowledges (id, status, confidence) VALUES ('k1', 'candidate', 'high')",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
migrate_v43(&conn).expect("首次晋升");
|
||||
migrate_v43(&conn).expect("重跑应幂等成功(UPDATE 0 行)");
|
||||
|
||||
let published: i64 = conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM knowledges WHERE status = 'published'",
|
||||
[], |r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(published, 1, "重跑不应产生重复变更");
|
||||
let v_count: i64 = conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM schema_version WHERE version = 43",
|
||||
[], |r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(v_count, 1, "版本号 43 应只写一次");
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 全量迁移测试 — 新库从零跑完整 V1-V43 路径
|
||||
// ------------------------------------------------------------
|
||||
// 目的:某 migrate_vN 的 SQL 手滑写错(列名/类型/缺索引/缺表)只能等运行时暴露,
|
||||
// 此测试一次性覆盖全部迁移路径。任何一条迁移 SQL 写错、列名拼错、缺建表
|
||||
@@ -1986,7 +2281,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// 全量迁移:新库从零跑完 V1-V41,验证关键表齐全 + 列数 > 0 + 关键列存在。
|
||||
/// 全量迁移:新库从零跑完 V1-V43,验证关键表齐全 + 列数 > 0 + 关键列存在。
|
||||
///
|
||||
/// 覆盖至少:task / ai_conversations / ai_messages / ai_tool_executions /
|
||||
/// conversation_checkpoints / ai_providers / projects / ideas。
|
||||
@@ -2043,7 +2338,7 @@ mod tests {
|
||||
"tasks.module_id 列缺失(V41 加)"
|
||||
);
|
||||
|
||||
// 3. schema_version 应推进到 42(全量迁移成功落版本号)
|
||||
// 3. schema_version 应推进到 43(全量迁移成功落版本号)
|
||||
let max_version: i64 = conn
|
||||
.query_row(
|
||||
"SELECT COALESCE(MAX(version), 0) FROM schema_version",
|
||||
@@ -2052,8 +2347,8 @@ mod tests {
|
||||
)
|
||||
.expect("查 schema_version 应成功");
|
||||
assert_eq!(
|
||||
max_version, 42,
|
||||
"全量迁移后 schema_version 应为 42(实际 {}),说明某条 migrate_vN 链路断在中间",
|
||||
max_version, 44,
|
||||
"全量迁移后 schema_version 应为 44(实际 {}),说明某条 migrate_vN 链路断在中间",
|
||||
max_version
|
||||
);
|
||||
|
||||
@@ -2114,7 +2409,7 @@ mod tests {
|
||||
assert_eq!(v_count, 1, "版本号 32 应只写一次");
|
||||
}
|
||||
|
||||
/// G5.5: 全链幂等不变量——V1-V41 每步执行两遍不抛错。
|
||||
/// G5.5: 全链幂等不变量——V1-V43 每步执行两遍不抛错。
|
||||
///
|
||||
/// 首轮 run() 建全 schema;清空 schema_version 强制下一轮从 V1 重跑每步
|
||||
/// (模拟存量库 + 崩溃重跑/版本号回退)。任何 migrate_vN 的裸 ALTER(无 column_exists
|
||||
@@ -2130,7 +2425,7 @@ mod tests {
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(max_v, 42, "首轮应推进到 42");
|
||||
assert_eq!(max_v, 44, "首轮应推进到 44");
|
||||
|
||||
// 清空版本表强制全链第二遍(每步 execute 第二次)
|
||||
conn.execute("DELETE FROM schema_version", []).unwrap();
|
||||
@@ -2142,7 +2437,7 @@ mod tests {
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(max_v2, 42, "重跑后应重新推进到 42");
|
||||
assert_eq!(max_v2, 44, "重跑后应重新推进到 44");
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
||||
Reference in New Issue
Block a user