things_mcp/core/reader/
fts.rs1use rusqlite::Connection;
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct FtsCapability {
13 pub table: String,
15 pub columns: Vec<String>,
17}
18
19pub fn detect(conn: &Connection) -> rusqlite::Result<Option<FtsCapability>> {
28 let mut stmt = conn.prepare(
29 "SELECT name FROM sqlite_master \
30 WHERE type = 'table' \
31 AND sql IS NOT NULL \
32 AND sql LIKE '%USING fts5%' \
33 AND (name LIKE 'TMTask%' OR name LIKE 'TMSearchInfo%') \
34 ORDER BY name \
35 LIMIT 1",
36 )?;
37 let mut rows = stmt.query([])?;
38 let Some(row) = rows.next()? else {
39 return Ok(None);
40 };
41 let table: String = row.get(0)?;
42 let columns = list_columns(conn, &table)?;
43 Ok(Some(FtsCapability { table, columns }))
44}
45
46fn list_columns(conn: &Connection, table: &str) -> rusqlite::Result<Vec<String>> {
47 let mut stmt = conn.prepare(&format!("PRAGMA table_info(\"{}\")", table))?;
48 let cols: Result<Vec<String>, _> = stmt
49 .query_map([], |r| r.get::<_, String>(1))?
50 .collect();
51 cols
52}
53
54#[cfg(test)]
55mod tests {
56 use super::*;
57 use rusqlite::Connection;
58
59 #[test]
60 fn detect_returns_none_on_empty_db() {
61 let c = Connection::open_in_memory().unwrap();
62 c.execute_batch("CREATE TABLE TMTask (uuid TEXT);").unwrap();
63 assert_eq!(detect(&c).unwrap(), None);
64 }
65
66 #[test]
67 fn detect_finds_fts5_virtual_table() {
68 let c = Connection::open_in_memory().unwrap();
69 c.execute_batch(
70 "CREATE TABLE TMTask (uuid TEXT);
71 CREATE VIRTUAL TABLE TMTask_searchstr USING fts5(title, notes, content='');",
72 )
73 .unwrap();
74 let cap = detect(&c).unwrap().expect("FTS5 table should be detected");
75 assert_eq!(cap.table, "TMTask_searchstr");
76 assert!(cap.columns.iter().any(|c| c == "title"));
77 assert!(cap.columns.iter().any(|c| c == "notes"));
78 }
79
80 #[test]
81 fn detect_ignores_non_fts_virtual_tables() {
82 let c = Connection::open_in_memory().unwrap();
83 c.execute_batch(
85 "CREATE TABLE TMTask (uuid TEXT);
86 CREATE VIRTUAL TABLE TMTask_rtree USING rtree(id, minX, maxX);",
87 )
88 .unwrap();
89 assert_eq!(detect(&c).unwrap(), None);
90 }
91
92 #[test]
93 fn detect_ignores_unrelated_fts_tables() {
94 let c = Connection::open_in_memory().unwrap();
95 c.execute_batch(
97 "CREATE TABLE TMTask (uuid TEXT);
98 CREATE VIRTUAL TABLE Whatever_fts USING fts5(stuff);",
99 )
100 .unwrap();
101 assert_eq!(detect(&c).unwrap(), None);
102 }
103}