|
| 1 | +//! 知识概念数据层(v0.13.1 REQ-202;db_* 拆分模式同款)。 |
| 2 | +//! |
| 3 | +//! @ai-context: knowledge_concepts 表 CRUD + 按名查找。概念是全库唯一身份的 |
| 4 | +//! 知识单元(三问本质/边界/联系),name UNIQUE(spec §二 不可变约束) |
| 5 | +//! 是交叉点判定的前提——重复名冲突由 rusqlite 约束错误上抛(不过滤)。 |
| 6 | +//! @ai-context: 锁访问统一走 Db::with_conn(中毒锁恢复而非 panic)。 |
| 7 | +//! |
| 8 | +//! @ai-context: M1 数据层 API 供 M2 command 层接入用(机制先行);接入后移除本 allow。 |
| 9 | +#![allow(dead_code)] |
| 10 | + |
| 11 | +use rusqlite::params; |
| 12 | + |
| 13 | +use crate::db::{unix_seconds, Db}; |
| 14 | +use crate::error::Result; |
| 15 | +use crate::types::{KnowledgeConcept, NewKnowledgeConcept}; |
| 16 | + |
| 17 | +/// knowledge_concepts 表统一查询列(列顺序与 row_to_concept 严格对应)。 |
| 18 | +const CONCEPT_COLUMNS: &str = |
| 19 | + "id, system_id, name, essence, boundary, relation, status, last_applied_at, created_at, updated_at"; |
| 20 | + |
| 21 | +impl Db { |
| 22 | + /// 新建概念,返回完整记录。 |
| 23 | + /// |
| 24 | + /// @ai-context: name 全局 UNIQUE——同名概念重复创建时 rusqlite 约束错误自然上抛 |
| 25 | + /// (command 层预查给友好错误;名称归一化在 command 层执行后落库)。 |
| 26 | + pub fn add_knowledge_concept(&self, new: &NewKnowledgeConcept) -> Result<KnowledgeConcept> { |
| 27 | + let now = unix_seconds(); |
| 28 | + self.with_conn(|conn| { |
| 29 | + conn.execute( |
| 30 | + "INSERT INTO knowledge_concepts (system_id, name, essence, boundary, relation, status, last_applied_at, created_at, updated_at) |
| 31 | + VALUES (?1, ?2, ?3, ?4, ?5, 'core', NULL, ?6, ?6)", |
| 32 | + params![new.system_id, new.name, new.essence, new.boundary, new.relation, now], |
| 33 | + )?; |
| 34 | + let id = conn.last_insert_rowid(); |
| 35 | + Ok(KnowledgeConcept { |
| 36 | + id, |
| 37 | + system_id: new.system_id, |
| 38 | + name: new.name.clone(), |
| 39 | + essence: new.essence.clone(), |
| 40 | + boundary: new.boundary.clone(), |
| 41 | + relation: new.relation.clone(), |
| 42 | + status: "core".to_string(), |
| 43 | + last_applied_at: None, |
| 44 | + created_at: now, |
| 45 | + updated_at: now, |
| 46 | + }) |
| 47 | + }) |
| 48 | + } |
| 49 | + |
| 50 | + /// 按 id 读取概念;不存在返回 None。 |
| 51 | + pub fn get_knowledge_concept(&self, id: i64) -> Result<Option<KnowledgeConcept>> { |
| 52 | + self.with_conn(|conn| { |
| 53 | + let mut stmt = conn.prepare(&format!( |
| 54 | + "SELECT {} FROM knowledge_concepts WHERE id = ?1", |
| 55 | + CONCEPT_COLUMNS |
| 56 | + ))?; |
| 57 | + let mut rows = stmt.query_map(params![id], row_to_concept)?; |
| 58 | + match rows.next() { |
| 59 | + Some(Ok(c)) => Ok(Some(c)), |
| 60 | + Some(Err(e)) => Err(e.into()), |
| 61 | + None => Ok(None), |
| 62 | + } |
| 63 | + }) |
| 64 | + } |
| 65 | + |
| 66 | + /// 按名称查找概念(全局唯一——命中至多一条;名称已归一化由调用方保证)。 |
| 67 | + pub fn find_concept_by_name(&self, name: &str) -> Result<Option<KnowledgeConcept>> { |
| 68 | + self.with_conn(|conn| { |
| 69 | + let mut stmt = conn.prepare(&format!( |
| 70 | + "SELECT {} FROM knowledge_concepts WHERE name = ?1 LIMIT 1", |
| 71 | + CONCEPT_COLUMNS |
| 72 | + ))?; |
| 73 | + let mut rows = stmt.query_map(params![name], row_to_concept)?; |
| 74 | + match rows.next() { |
| 75 | + Some(Ok(c)) => Ok(Some(c)), |
| 76 | + Some(Err(e)) => Err(e.into()), |
| 77 | + None => Ok(None), |
| 78 | + } |
| 79 | + }) |
| 80 | + } |
| 81 | + |
| 82 | + /// 更新概念可选字段。 |
| 83 | + /// |
| 84 | + /// @ai-context: 三问 essence/boundary/relation 为可空列(两层 Option:外层 None=不改, |
| 85 | + /// 内层 None=清空为 NULL);name 为 NOT NULL(单层 Option:None=不改)。 |
| 86 | + pub fn update_knowledge_concept( |
| 87 | + &self, |
| 88 | + id: i64, |
| 89 | + name: Option<&str>, |
| 90 | + essence: Option<Option<&str>>, |
| 91 | + boundary: Option<Option<&str>>, |
| 92 | + relation: Option<Option<&str>>, |
| 93 | + status: Option<&str>, |
| 94 | + ) -> Result<bool> { |
| 95 | + self.with_conn(|conn| { |
| 96 | + let mut sets: Vec<String> = Vec::new(); |
| 97 | + let mut params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new(); |
| 98 | + if let Some(v) = name { |
| 99 | + sets.push(format!("name = ?{}", params.len() + 1)); |
| 100 | + params.push(Box::new(v.to_string())); |
| 101 | + } |
| 102 | + if let Some(v) = essence { |
| 103 | + sets.push(format!("essence = ?{}", params.len() + 1)); |
| 104 | + params.push(Box::new(v.map(str::to_string))); |
| 105 | + } |
| 106 | + if let Some(v) = boundary { |
| 107 | + sets.push(format!("boundary = ?{}", params.len() + 1)); |
| 108 | + params.push(Box::new(v.map(str::to_string))); |
| 109 | + } |
| 110 | + if let Some(v) = relation { |
| 111 | + sets.push(format!("relation = ?{}", params.len() + 1)); |
| 112 | + params.push(Box::new(v.map(str::to_string))); |
| 113 | + } |
| 114 | + if let Some(v) = status { |
| 115 | + sets.push(format!("status = ?{}", params.len() + 1)); |
| 116 | + params.push(Box::new(v.to_string())); |
| 117 | + } |
| 118 | + if sets.is_empty() { |
| 119 | + return Ok(false); |
| 120 | + } |
| 121 | + sets.push(format!("updated_at = ?{}", params.len() + 1)); |
| 122 | + params.push(Box::new(unix_seconds())); |
| 123 | + let sql = format!( |
| 124 | + "UPDATE knowledge_concepts SET {} WHERE id = ?{}", |
| 125 | + sets.join(", "), |
| 126 | + params.len() + 1 |
| 127 | + ); |
| 128 | + params.push(Box::new(id)); |
| 129 | + let affected = conn.execute(&sql, rusqlite::params_from_iter(params.iter().map(|b| b.as_ref())))?; |
| 130 | + Ok(affected > 0) |
| 131 | + }) |
| 132 | + } |
| 133 | + |
| 134 | + /// 列出概念(体系过滤可选、状态过滤可选;动态 WHERE)。 |
| 135 | + pub fn list_knowledge_concepts( |
| 136 | + &self, |
| 137 | + system_id: Option<i64>, |
| 138 | + status: Option<&str>, |
| 139 | + ) -> Result<Vec<KnowledgeConcept>> { |
| 140 | + self.with_conn(|conn| { |
| 141 | + let mut clauses: Vec<String> = Vec::new(); |
| 142 | + let mut params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new(); |
| 143 | + if let Some(sid) = system_id { |
| 144 | + clauses.push(format!("system_id = ?{}", params.len() + 1)); |
| 145 | + params.push(Box::new(sid)); |
| 146 | + } |
| 147 | + if let Some(st) = status { |
| 148 | + clauses.push(format!("status = ?{}", params.len() + 1)); |
| 149 | + params.push(Box::new(st.to_string())); |
| 150 | + } |
| 151 | + let where_sql = if clauses.is_empty() { |
| 152 | + String::new() |
| 153 | + } else { |
| 154 | + format!("WHERE {}", clauses.join(" AND ")) |
| 155 | + }; |
| 156 | + let sql = format!( |
| 157 | + "SELECT {} FROM knowledge_concepts {} ORDER BY id ASC", |
| 158 | + CONCEPT_COLUMNS, where_sql |
| 159 | + ); |
| 160 | + let mut stmt = conn.prepare(&sql)?; |
| 161 | + let rows = stmt.query_map(rusqlite::params_from_iter(params.iter().map(|b| b.as_ref())), row_to_concept)?; |
| 162 | + rows.collect::<rusqlite::Result<Vec<_>>>().map_err(Into::into) |
| 163 | + }) |
| 164 | + } |
| 165 | +} |
| 166 | + |
| 167 | +/// 把 rusqlite 行映射为 KnowledgeConcept。 |
| 168 | +fn row_to_concept(row: &rusqlite::Row<'_>) -> rusqlite::Result<KnowledgeConcept> { |
| 169 | + Ok(KnowledgeConcept { |
| 170 | + id: row.get(0)?, |
| 171 | + system_id: row.get(1)?, |
| 172 | + name: row.get(2)?, |
| 173 | + essence: row.get(3)?, |
| 174 | + boundary: row.get(4)?, |
| 175 | + relation: row.get(5)?, |
| 176 | + status: row.get(6)?, |
| 177 | + last_applied_at: row.get(7)?, |
| 178 | + created_at: row.get(8)?, |
| 179 | + updated_at: row.get(9)?, |
| 180 | + }) |
| 181 | +} |
| 182 | + |
| 183 | +/// 单测独立文件(保持本文件 ≤300 行,AGENTS.md §3)。 |
| 184 | +#[cfg(test)] |
| 185 | +#[path = "db_knowledge_concepts_tests.rs"] |
| 186 | +mod tests; |
0 commit comments