优化: 存储层清债与子轨迹来源迁移
- V44 加 ai_conversations.origin 列(子代理轨迹来源标记) - V21 断点续迁改造(逐对话进度判定,崩溃重跑补漏) - 手写事务改 guard.transaction,panic 不残留打开事务 - 软删 CAS 补 deleted_at IS NULL(任务/项目/灵感三处) - 父任务校验拒回收站挂子,删模块先清依赖边 - 列缺失与未知 status 降级补 warn 留痕,limit=0 返空集
This commit is contained in:
@@ -104,7 +104,16 @@ fn idea_from_row(row: &Row<'_>) -> std::result::Result<IdeaRecord, rusqlite::Err
|
||||
description: row.get("description")?,
|
||||
status: {
|
||||
let s: String = row.get("status")?;
|
||||
IdeaStatus::from_db_str(&s).unwrap_or_default()
|
||||
match IdeaStatus::from_db_str(&s) {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
// 未知 status 静默降级默认值后,后续 update_full 回写会把库里的未知值
|
||||
// 覆盖成默认值(读→改→写放大静默),warn 留痕让脏数据可发现。
|
||||
let id: String = row.get("id")?;
|
||||
tracing::warn!("ideas.id={id} 含未知 status {s:?},按默认值降级(回写会覆盖原值,请核查)");
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
},
|
||||
priority: row.get("priority")?,
|
||||
score: row.get("score")?,
|
||||
@@ -613,7 +622,7 @@ impl IdeaRepo {
|
||||
///
|
||||
/// 不动 GUI 的无条件 [`update_full`](无 expected 语义,保持现状);本方法仅服务
|
||||
/// 需要乐观锁版本的调用方(df-mcp update_idea / score_idea)。
|
||||
/// `Ok(false)` = id 不存在或版本冲突。
|
||||
/// `Ok(false)` = id 不存在、已进回收站(deleted_at IS NULL 守卫,V28 灵感软删)或版本冲突。
|
||||
pub async fn update_full_cas(
|
||||
&self,
|
||||
record: &IdeaRecord,
|
||||
@@ -626,7 +635,8 @@ impl IdeaRepo {
|
||||
let guard = conn.blocking_lock();
|
||||
let affected = guard
|
||||
.execute(
|
||||
"UPDATE ideas SET title = ?1, description = ?2, status = ?3, priority = ?4, score = ?5, tags = ?6, source = ?7, promoted_to = ?8, ai_analysis = ?9, scores = ?10, related_ids = ?11, updated_at = ?12 WHERE id = ?13 AND updated_at = ?14",
|
||||
// deleted_at IS NULL:回收站灵感不可被 CAS 覆盖(同 tasks/projects 同族守卫)
|
||||
"UPDATE ideas SET title = ?1, description = ?2, status = ?3, priority = ?4, score = ?5, tags = ?6, source = ?7, promoted_to = ?8, ai_analysis = ?9, scores = ?10, related_ids = ?11, updated_at = ?12 WHERE id = ?13 AND updated_at = ?14 AND deleted_at IS NULL",
|
||||
params![
|
||||
rec.title, rec.description, rec.status.as_str(), rec.priority,
|
||||
rec.score, rec.tags, rec.source, rec.promoted_to, rec.ai_analysis,
|
||||
@@ -1992,6 +2002,27 @@ mod tests {
|
||||
assert_eq!(after.updated_at, "1800000000000");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn idea_update_full_cas_skips_soft_deleted() {
|
||||
// 回收站灵感不可被 CAS 命中(deleted_at IS NULL 守卫,V28 软删)
|
||||
let repo = setup_idea_repo().await;
|
||||
repo.insert(irec("i1", "原标题")).await.unwrap();
|
||||
repo.soft_delete("i1").await.unwrap();
|
||||
// soft_delete 刷新了 updated_at,取当前值作 expected(排除版本不匹配干扰)
|
||||
let current = repo.get_by_id("i1").await.unwrap().unwrap();
|
||||
let mut rec = current.clone();
|
||||
rec.title = "回收站里改标题".to_string();
|
||||
rec.updated_at = "1900000000000".to_string();
|
||||
let ok = repo
|
||||
.update_full_cas(&rec, ¤t.updated_at)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(!ok, "软删灵感 CAS 应拒绝(deleted_at 守卫)");
|
||||
let after = repo.get_by_id("i1").await.unwrap().unwrap();
|
||||
assert_eq!(after.title, "原标题", "回收站灵感内容不应被改写");
|
||||
assert_eq!(after.updated_at, current.updated_at, "updated_at 不应抖动");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn idea_update_full_cas_conflict_when_expected_stale() {
|
||||
let repo = setup_idea_repo().await;
|
||||
|
||||
Reference in New Issue
Block a user