重构: crud.rs按表拆分(SMELL-P1-9) + F-09批1 PerConvState数据结构
- SMELL-P1-9: crud.rs 2212行→crud/6文件(mod/settings/project_repo/task_repo/conversation_repo/idea_repo) re-export pub use *_repo::* 零调用方改动,宏 pub(crate) use + 子模块 use super::impl_repo 基线测试锁12表白名单+13Repo构造 - F-09 批1: PerConvState struct(9字段对齐AiSession::new)+AiSession.per_conv HashMap+conv()/conv_read()访问器+3单测 纯新增无行为变化,b-1主代自主裁决采纳,批2迁移承接 主代统一兜底: cargo check --workspace 0 + df-storage 35+11 + devflow 96 passed
This commit is contained in:
277
crates/df-storage/src/crud/mod.rs
Normal file
277
crates/df-storage/src/crud/mod.rs
Normal file
@@ -0,0 +1,277 @@
|
||||
//! Repository 模式的 CRUD 操作
|
||||
//!
|
||||
//! 为 7 张表各提供一个 Repo 结构体,统一实现 insert / get_by_id / list_all / query / update_field / delete。
|
||||
//!
|
||||
//! 按表域拆分(SMELL-P1-9,对齐 SMELL-P0-2 拆分思路降 2212 行单文件复杂度):
|
||||
//! - [`mod@settings`]:SettingsRepo + 列白名单(allowed_columns_for/validate_column_name/is_allowed_column)
|
||||
//! - [`mod@project_repo`]:ProjectRepo/BranchRepo/ReleaseRepo/WorkflowRepo/NodeExecutionRepo
|
||||
//! - [`mod@task_repo`]:TaskRepo(含 advance_status_atomic 状态机收口)
|
||||
//! - [`mod@conversation_repo`]:AiProviderRepo/AiConversationRepo/AiToolExecutionRepo
|
||||
//! - [`mod@idea_repo`]:IdeaRepo/KnowledgeRepo/KnowledgeEventsRepo + 向量工具
|
||||
//!
|
||||
//! re-export(`pub use ...::*`)保持 `df_storage::crud::XxxRepo` /
|
||||
//! `df_storage::crud::is_allowed_column` 路径不变,**调用方零改动**。
|
||||
|
||||
mod conversation_repo;
|
||||
mod idea_repo;
|
||||
mod project_repo;
|
||||
mod settings;
|
||||
mod task_repo;
|
||||
|
||||
pub use conversation_repo::*;
|
||||
pub use idea_repo::*;
|
||||
pub use project_repo::*;
|
||||
pub use settings::*;
|
||||
pub use task_repo::*;
|
||||
|
||||
// ============================================================
|
||||
// 辅助宏 — 消除多个 Repo 的重复样板
|
||||
// ============================================================
|
||||
|
||||
/// 一次性为一张表生成完整的 Repo 结构体及其 CRUD 实现。
|
||||
///
|
||||
/// 用法: `impl_repo!(RepoName, ModelType, "table_name", from_row_body, insert_body, update_body);`
|
||||
///
|
||||
/// 约束:`$record` 须实现 `Clone`(`update_full` 内部 clone 后丢给 spawn_blocking)。
|
||||
///
|
||||
/// 宏通过 `#[macro_use]`(见本文件末 `mod` 声明上方的文本注入)对各子模块可见,
|
||||
/// 子模块 `impl_repo!(...)` 直接调用。`from_row`/`insert`/`update` 体内引用的 helper
|
||||
/// (storage_err/now_millis_str/validate_column_name 及各 from_row)须在调用处可见。
|
||||
macro_rules! impl_repo {
|
||||
(
|
||||
$(#[$meta:meta])*
|
||||
$name:ident, $record:ident, $table:literal,
|
||||
from_row => |$row:ident| $from_body:expr,
|
||||
insert => |$conn:ident, $rec:ident| $insert_body:expr,
|
||||
update => |$u_conn:ident, $u_rec:ident| $update_body:expr
|
||||
) => {
|
||||
$(#[$meta])*
|
||||
pub struct $name {
|
||||
conn: Arc<Mutex<Connection>>,
|
||||
}
|
||||
|
||||
impl $name {
|
||||
pub fn new(db: &Database) -> Self {
|
||||
Self { conn: db.conn() }
|
||||
}
|
||||
|
||||
pub async fn insert(&self, record: $record) -> Result<String> {
|
||||
let conn = self.conn.clone();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let guard = conn.blocking_lock();
|
||||
let id = record.id.clone();
|
||||
let $conn = &*guard;
|
||||
let $rec = &record;
|
||||
$insert_body.map_err(storage_err)?;
|
||||
Ok(id)
|
||||
})
|
||||
.await
|
||||
.map_err(storage_err)?
|
||||
}
|
||||
|
||||
pub async fn get_by_id(&self, id: &str) -> Result<Option<$record>> {
|
||||
let conn = self.conn.clone();
|
||||
let id = id.to_owned();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let guard = conn.blocking_lock();
|
||||
let mut stmt = guard
|
||||
.prepare(&format!("SELECT * FROM {} WHERE id = ?1", $table))
|
||||
.map_err(storage_err)?;
|
||||
let row = stmt
|
||||
.query_row(params![id], |$row| $from_body)
|
||||
.optional()
|
||||
.map_err(storage_err)?;
|
||||
Ok(row)
|
||||
})
|
||||
.await
|
||||
.map_err(storage_err)?
|
||||
}
|
||||
|
||||
pub async fn list_all(&self) -> Result<Vec<$record>> {
|
||||
let conn = self.conn.clone();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let guard = conn.blocking_lock();
|
||||
let mut stmt = guard
|
||||
.prepare(&format!("SELECT * FROM {} ORDER BY created_at DESC", $table))
|
||||
.map_err(storage_err)?;
|
||||
let rows = stmt
|
||||
.query_map([], |$row| $from_body)
|
||||
.map_err(storage_err)?;
|
||||
let mut results = Vec::new();
|
||||
for r in rows {
|
||||
results.push(
|
||||
r.map_err(storage_err)?,
|
||||
);
|
||||
}
|
||||
Ok(results)
|
||||
})
|
||||
.await
|
||||
.map_err(storage_err)?
|
||||
}
|
||||
|
||||
pub async fn query(&self, field: &str, value: &str) -> Result<Vec<$record>> {
|
||||
// 白名单校验,防止 SQL 注入
|
||||
validate_column_name(field, $table)?;
|
||||
let conn = self.conn.clone();
|
||||
let sql = format!("SELECT * FROM {} WHERE {} = ?1 ORDER BY created_at DESC", $table, field);
|
||||
let value = value.to_owned();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let guard = conn.blocking_lock();
|
||||
let mut stmt = guard
|
||||
.prepare(&sql)
|
||||
.map_err(storage_err)?;
|
||||
let rows = stmt
|
||||
.query_map(params![value], |$row| $from_body)
|
||||
.map_err(storage_err)?;
|
||||
let mut results = Vec::new();
|
||||
for r in rows {
|
||||
results.push(
|
||||
r.map_err(storage_err)?,
|
||||
);
|
||||
}
|
||||
Ok(results)
|
||||
})
|
||||
.await
|
||||
.map_err(storage_err)?
|
||||
}
|
||||
|
||||
pub async fn update_field(&self, id: &str, field: &str, value: &str) -> Result<bool> {
|
||||
validate_column_name(field, $table)?;
|
||||
let conn = self.conn.clone();
|
||||
let sql = format!("UPDATE {} SET {} = ?1, updated_at = ?2 WHERE id = ?3", $table, field);
|
||||
let id = id.to_owned();
|
||||
let value = value.to_owned();
|
||||
let now = now_millis_str();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let guard = conn.blocking_lock();
|
||||
let affected = guard
|
||||
.execute(&sql, params![value, now, id])
|
||||
.map_err(storage_err)?;
|
||||
Ok(affected > 0)
|
||||
})
|
||||
.await
|
||||
.map_err(storage_err)?
|
||||
}
|
||||
|
||||
/// 整体更新记录(全可变字段,保留 id 与 created_at),单次原子写
|
||||
pub async fn update_full(&self, record: &$record) -> Result<bool> {
|
||||
let conn = self.conn.clone();
|
||||
let rec = record.clone();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let guard = conn.blocking_lock();
|
||||
let $u_conn = &*guard;
|
||||
let $u_rec = &rec;
|
||||
let affected =
|
||||
$update_body.map_err(storage_err)?;
|
||||
Ok(affected > 0)
|
||||
})
|
||||
.await
|
||||
.map_err(storage_err)?
|
||||
}
|
||||
|
||||
pub async fn delete(&self, id: &str) -> Result<bool> {
|
||||
let conn = self.conn.clone();
|
||||
let id = id.to_owned();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let guard = conn.blocking_lock();
|
||||
let affected = guard
|
||||
.execute(&format!("DELETE FROM {} WHERE id = ?1", $table), params![id])
|
||||
.map_err(storage_err)?;
|
||||
Ok(affected > 0)
|
||||
})
|
||||
.await
|
||||
.map_err(storage_err)?
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 让 impl_repo! 宏对下方声明的子模块可见(textual scope 注入)。
|
||||
// 必须放在宏定义之后、`mod` 声明之前——顺序错了子模块拿不到宏。
|
||||
pub(crate) use impl_repo;
|
||||
|
||||
// ============================================================
|
||||
// 公共工具(各子模块 use super::* 复用)
|
||||
// ============================================================
|
||||
|
||||
/// rusqlite 错误 → df-types `Error::Storage` 统一包装(R-PD-10 DRY,
|
||||
/// 替代散落的 `.map_err(storage_err)`)。
|
||||
pub(crate) fn storage_err<E: std::string::ToString>(e: E) -> df_types::error::Error {
|
||||
df_types::error::Error::Storage(e.to_string())
|
||||
}
|
||||
|
||||
/// 规范化路径用于比较:canonicalize 解析绝对规范路径(失败降级),
|
||||
/// 统一正斜杠 + 小写。与 `df_project::scan::normalize_path` **同算法镜像**(df-storage
|
||||
/// 不依赖 df-project,故独立实现;改动须同步)。防 `C:\a\b` vs `C:/a/b/` 绕过。
|
||||
pub(crate) fn normalize_stored_path(p: &str) -> String {
|
||||
match std::path::Path::new(p).canonicalize() {
|
||||
Ok(abs) => abs.to_string_lossy().replace('\\', "/").to_lowercase(),
|
||||
Err(_) => p
|
||||
.trim_end_matches(['\\', '/'])
|
||||
.replace('\\', "/")
|
||||
.to_lowercase(),
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 时间工具
|
||||
// ============================================================
|
||||
|
||||
pub(crate) fn now_millis_str() -> String {
|
||||
// 转发 df_types::now_millis(DRY:时间获取统一入口在 df-types,避免本 crate 与 commands 层各写一份 SystemTime 调用)
|
||||
df_types::now_millis().to_string()
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 基线测试 — 锁 Repo 数/表名防回归(SMELL-P1-9 对齐 SMELL-P0-2)
|
||||
// ============================================================
|
||||
//
|
||||
// 拆分后,各 Repo 散落 5 个子模块。此基线测试确保:
|
||||
// ① 所有 Repo 类型经 mod.rs `pub use` re-export 后在 `df_storage::crud::` 路径下仍可统一访问
|
||||
// (调用方 `df_storage::crud::XxxRepo::new(&db)` 路径不破,降回归)。
|
||||
// ② 各 Repo::new 在内存 DB 上构造不 panic + 列白名单覆盖每张表
|
||||
// (拆分时误删 Repo / 漏 re-export / 表名漂移会被此测试立即捕获)。
|
||||
|
||||
#[cfg(test)]
|
||||
mod baseline_tests {
|
||||
use super::*;
|
||||
use crate::db::Database;
|
||||
|
||||
/// 每张表都登记了列白名单(拆分后白名单仍在 settings.rs 单一来源,无表遗漏)。
|
||||
/// 新增表须同步登记白名单,否则此测试失败(防遗漏)。
|
||||
#[test]
|
||||
fn all_known_tables_have_column_whitelist() {
|
||||
let tables = [
|
||||
"ideas", "projects", "tasks", "releases", "branches", "workflow_executions",
|
||||
"node_executions", "ai_providers", "ai_conversations", "ai_tool_executions",
|
||||
"knowledges", "knowledge_events",
|
||||
];
|
||||
for t in tables {
|
||||
assert!(
|
||||
allowed_columns_for(t).is_some(),
|
||||
"表 {t} 应登记列白名单(防 SQL 注入 + 按表隔离);拆分后白名单单一来源在 settings.rs"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// re-export 完整性:所有 Repo 类型经 `df_storage::crud::XxxRepo` 路径可访问 + new 不 panic。
|
||||
/// 若拆分时漏 `pub use` 某子模块,对应 Repo 名在此处编译失败(立即暴露)。
|
||||
#[tokio::test]
|
||||
async fn all_repos_constructible_in_memory() {
|
||||
let db = Database::open_in_memory().await.expect("open_in_memory");
|
||||
// 各子模块 Repo:new 仅取 conn 句柄,不触 DB IO,构造成功即证明类型可见 + Database 路径通。
|
||||
let _ = SettingsRepo::new(&db);
|
||||
let _ = IdeaRepo::new(&db);
|
||||
let _ = ProjectRepo::new(&db);
|
||||
let _ = TaskRepo::new(&db);
|
||||
let _ = BranchRepo::new(&db);
|
||||
let _ = ReleaseRepo::new(&db);
|
||||
let _ = WorkflowRepo::new(&db);
|
||||
let _ = NodeExecutionRepo::new(&db);
|
||||
let _ = AiProviderRepo::new(&db);
|
||||
let _ = AiConversationRepo::new(&db);
|
||||
let _ = AiToolExecutionRepo::new(&db);
|
||||
let _ = KnowledgeRepo::new(&db);
|
||||
let _ = KnowledgeEventsRepo::new(&db);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user