Skip to main content

boarddown_db/
fts.rs

1use boarddown_core::{TaskId, BoardId, Error, SearchResult};
2use rusqlite::Connection;
3
4#[derive(Debug)]
5pub struct FullTextSearch {
6    enabled: bool,
7}
8
9impl FullTextSearch {
10    pub fn new(enabled: bool) -> Self {
11        Self { enabled }
12    }
13
14    pub fn enabled(&self) -> bool {
15        self.enabled
16    }
17
18    pub fn init(&self, conn: &Connection) -> Result<(), Error> {
19        if !self.enabled {
20            return Ok(());
21        }
22        
23        conn.execute(
24            "CREATE VIRTUAL TABLE IF NOT EXISTS tasks_fts USING fts5(
25                board_id,
26                id,
27                title,
28                description,
29                tags
30            )",
31            [],
32        ).map_err(|e| Error::Database(e.to_string()))?;
33        
34        Ok(())
35    }
36
37    pub fn index_task(&self, conn: &Connection, board_id: &BoardId, task_id: &TaskId, title: &str, description: &str, tags: &[String]) -> Result<(), Error> {
38        if !self.enabled {
39            return Ok(());
40        }
41        
42        let tags_str = tags.join(" ");
43        
44        conn.execute(
45            "INSERT OR REPLACE INTO tasks_fts (board_id, id, title, description, tags)
46             VALUES (?1, ?2, ?3, ?4, ?5)",
47            rusqlite::params![
48                board_id.as_ref(),
49                task_id.to_string(),
50                title,
51                description,
52                tags_str
53            ],
54        ).map_err(|e| Error::Database(e.to_string()))?;
55        
56        Ok(())
57    }
58
59    pub fn remove_task(&self, conn: &Connection, board_id: &BoardId, task_id: &TaskId) -> Result<(), Error> {
60        if !self.enabled {
61            return Ok(());
62        }
63        
64        conn.execute(
65            "DELETE FROM tasks_fts WHERE board_id = ?1 AND id = ?2",
66            rusqlite::params![board_id.as_ref(), task_id.to_string()],
67        ).map_err(|e| Error::Database(e.to_string()))?;
68        
69        Ok(())
70    }
71
72    pub fn search(&self, conn: &Connection, query: &str) -> Result<Vec<SearchResult>, Error> {
73        if !self.enabled {
74            return Ok(Vec::new());
75        }
76        
77        let mut stmt = conn.prepare(
78            "SELECT id, title, snippet(tasks_fts, 2, '<mark>', '</mark>', '...', 32) as snippet, rank
79             FROM tasks_fts WHERE tasks_fts MATCH ? ORDER BY rank"
80        ).map_err(|e| Error::Database(e.to_string()))?;
81        
82        let rows = stmt.query_map([query], |row| {
83            Ok(SearchResult {
84                task_id: TaskId::parse(&row.get::<_, String>(0)?).unwrap_or_else(|_| TaskId::new("TASK", 0)),
85                title: row.get(1)?,
86                snippet: row.get(2)?,
87                rank: row.get(3)?,
88            })
89        }).map_err(|e| Error::Database(e.to_string()))?;
90        
91        let mut results = Vec::new();
92        for row in rows {
93            results.push(row.map_err(|e| Error::Database(e.to_string()))?);
94        }
95        
96        Ok(results)
97    }
98
99    pub fn search_in_board(&self, conn: &Connection, board_id: &BoardId, query: &str) -> Result<Vec<SearchResult>, Error> {
100        if !self.enabled {
101            return Ok(Vec::new());
102        }
103        
104        let mut stmt = conn.prepare(
105            "SELECT id, title, snippet(tasks_fts, 2, '<mark>', '</mark>', '...', 32) as snippet, rank
106             FROM tasks_fts WHERE board_id = ?1 AND tasks_fts MATCH ?2 ORDER BY rank"
107        ).map_err(|e| Error::Database(e.to_string()))?;
108        
109        let rows = stmt.query_map(rusqlite::params![board_id.as_ref(), query], |row| {
110            Ok(SearchResult {
111                task_id: TaskId::parse(&row.get::<_, String>(0)?).unwrap_or_else(|_| TaskId::new("TASK", 0)),
112                title: row.get(1)?,
113                snippet: row.get(2)?,
114                rank: row.get(3)?,
115            })
116        }).map_err(|e| Error::Database(e.to_string()))?;
117        
118        let mut results = Vec::new();
119        for row in rows {
120            results.push(row.map_err(|e| Error::Database(e.to_string()))?);
121        }
122        
123        Ok(results)
124    }
125}