修复: gen_stream判重+重试分类+状态机最短路径+既有测试修复
This commit is contained in:
@@ -391,12 +391,17 @@ impl TaskRepo {
|
||||
params_vec.push(Box::new(mid.clone()));
|
||||
}
|
||||
// keyword: title/description LIKE %kw%(P2,对齐知识库 search 的 LIKE 模式)
|
||||
// ESCAPE 字符用 |(管道符,任务标题/描述几乎不含),不用反斜杠——反斜杠在
|
||||
// Rust format! → rusqlite 绑定 → SQLite 多层转义里极易出错(SQLite 报
|
||||
// "ESCAPE expression must be a single character"),改 | 一劳永逸。
|
||||
// 注意:ESCAPE 只能跟单个 LIKE,不能跟括号分组(实测 `(a OR b) ESCAPE 'x'`
|
||||
// 报 near "ESCAPE" syntax error),故每个 LIKE 各自 ESCAPE。
|
||||
if let Some(kw) = &keyword {
|
||||
let escaped = kw.replace('%', "\\%").replace('_', "\\_");
|
||||
let escaped = kw.replace('|', "||").replace('%', "|%").replace('_', "|_");
|
||||
let pat = format!("%{escaped}%");
|
||||
let p1 = params_vec.len() + 1;
|
||||
let p2 = p1 + 1;
|
||||
where_clauses.push(format!("(title LIKE ?{p1} OR description LIKE ?{p2}) ESCAPE '\\'"));
|
||||
where_clauses.push(format!("(title LIKE ?{p1} ESCAPE '|' OR description LIKE ?{p2} ESCAPE '|')"));
|
||||
params_vec.push(Box::new(pat.clone()));
|
||||
params_vec.push(Box::new(pat));
|
||||
}
|
||||
@@ -494,11 +499,11 @@ impl TaskRepo {
|
||||
params_vec.push(Box::new(a.clone()));
|
||||
}
|
||||
if let Some(ref kw) = keyword {
|
||||
let escaped = kw.replace('%', "\\%").replace('_', "\\_");
|
||||
let escaped = kw.replace('|', "||").replace('%', "|%").replace('_', "|_");
|
||||
let pat = format!("%{escaped}%");
|
||||
let p1 = params_vec.len() + 1;
|
||||
let p2 = p1 + 1;
|
||||
where_clauses.push(format!("(title LIKE ?{p1} OR description LIKE ?{p2}) ESCAPE '\\'"));
|
||||
where_clauses.push(format!("(title LIKE ?{p1} ESCAPE '|' OR description LIKE ?{p2} ESCAPE '|')"));
|
||||
params_vec.push(Box::new(pat.clone()));
|
||||
params_vec.push(Box::new(pat));
|
||||
}
|
||||
@@ -1358,4 +1363,45 @@ mod tests {
|
||||
let after = repo.get_by_id("t1").await.unwrap().unwrap();
|
||||
assert_eq!(after.title, "新标题", "软删后字段不应被改动");
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// keyword LIKE 查询(ESCAPE 转义)—— 防 2026-08-11 语法回归
|
||||
// ============================================================
|
||||
// 背景:旧写法 `(a LIKE ?1 OR b LIKE ?2) ESCAPE '|'`(ESCAPE 跟括号分组)在 SQLite
|
||||
// 报 near "ESCAPE" syntax error,keyword 查询全挂。正确写法:ESCAPE 跟每个 LIKE。
|
||||
// 本测试锁两种语义:普通子串匹配 + 含 %/_ 通配符字面匹配(转义生效)。
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_by_query_keyword_matches_substring() {
|
||||
let repo = setup().await;
|
||||
repo.insert(trec("t1", "todo", None)).await.unwrap();
|
||||
repo.insert(trec("t2", "todo", None)).await.unwrap();
|
||||
// 定制 title:t1 含「支付」,t2 不含
|
||||
repo.update_field_active("t1", "title", "海外支付集成").await.unwrap();
|
||||
|
||||
let rows = repo
|
||||
.list_by_query(&TaskQuery { keyword: Some("支付".into()), ..Default::default() })
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(rows.len(), 1, "keyword 子串应只命中 t1");
|
||||
assert_eq!(rows[0].id, "t1");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_by_query_keyword_escapes_wildcards() {
|
||||
let repo = setup().await;
|
||||
repo.insert(trec("t1", "todo", None)).await.unwrap();
|
||||
repo.insert(trec("t2", "todo", None)).await.unwrap();
|
||||
// t1 标题含字面 % 与 _(通配符需转义,按字面匹配)
|
||||
repo.update_field_active("t1", "title", "比率 100%_cache").await.unwrap();
|
||||
repo.update_field_active("t2", "title", "比率 100x_cache").await.unwrap();
|
||||
|
||||
// 查询字面 "%_"(含两个通配符,转义后应按字面匹配 t1;t2 的 x 不匹配 %)
|
||||
let rows = repo
|
||||
.list_by_query(&TaskQuery { keyword: Some("100%_".into()), ..Default::default() })
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(rows.len(), 1, "% 与 _ 应被转义为字面,只命中 t1");
|
||||
assert_eq!(rows[0].id, "t1");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user