database.rs
1 // This file is part of digest. 2 // 3 // digest is free software: you can redistribute it and/or modify it under the terms of the GNU 4 // General Public License as published by the Free Software Foundation, either version 3 of the 5 // License, or (at your option) any later version. 6 // 7 // digest is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even 8 // the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General 9 // Public License for more details. 10 // 11 // You should have received a copy of the GNU General Public License along with Foobar. If not, 12 // see <https://www.gnu.org/licenses/>. 13 14 use rusqlite::{Connection, Error, DatabaseName}; 15 use std::path::{Path}; 16 use std::collections::BTreeSet; 17 use fuzzy_matcher::FuzzyMatcher; 18 19 #[derive(Debug, PartialEq, Clone)] 20 pub struct Zettel { 21 pub id: String, 22 pub title: String, 23 pub path: String, 24 } 25 26 impl Zettel { 27 pub fn new(id: &str, title: &str, path: &str) -> Self { 28 Zettel { id: id.to_string(), title: title.to_string(), path: path.to_string() } 29 } 30 } 31 32 #[derive(Debug, PartialEq, Clone)] 33 pub struct Task { 34 pub id: String, 35 pub description: String, 36 } 37 38 impl Task { 39 pub fn new(id: &str, d: &str) -> Self { 40 Self { id: id.to_string(), description: d.to_string() } 41 } 42 } 43 44 #[derive(Debug, PartialEq, Clone)] 45 pub struct ZettelLink { 46 pub from: String, 47 pub to: String, 48 } 49 50 impl ZettelLink { 51 pub fn new(from: &str, to: &str) -> Self { 52 Self { from: from.to_string(), to: to.to_string() } 53 } 54 } 55 56 pub struct Database 57 { 58 conn: Connection, 59 matcher: fuzzy_matcher::skim::SkimMatcherV2, 60 } 61 62 impl Database 63 { 64 pub fn from_memory() -> Result<Self, Error> { 65 Self::from_conn(Connection::open_in_memory()?) 66 } 67 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, Error> { 68 Self::from_conn(Connection::open(path.as_ref())?) 69 } 70 71 fn from_conn(conn: Connection) -> Result<Self, Error> { 72 73 conn.execute("CREATE TABLE IF NOT EXISTS zettel ( 74 id TEXT NOT NULL PRIMARY KEY, 75 title TEXT NOT NULL, 76 path TEXT NOT NULL, 77 UNIQUE(id) 78 )", 79 [], 80 )?; 81 conn.execute("CREATE TABLE IF NOT EXISTS zettel_link ( 82 id TEXT NOT NULL, 83 dst TEXT NOT NULL, 84 UNIQUE(id, dst) 85 )", 86 [], 87 )?; 88 89 conn.execute("CREATE TABLE IF NOT EXISTS task ( 90 id TEXT NOT NULL, 91 description TEXT NOT NULL, 92 UNIQUE(id, description) 93 )", 94 [], 95 )?; 96 97 Ok(Database { conn, matcher: fuzzy_matcher::skim::SkimMatcherV2::default() }) 98 } 99 100 101 pub fn backup<P: AsRef<Path>>(&self, path: P) -> Result<(), Error> 102 { 103 self.conn.backup(DatabaseName::Main, path, None)?; 104 Ok(()) 105 } 106 107 108 pub fn insert_zettel(&self, zettel: &Zettel) -> Result<(), Error> 109 { 110 self.conn.execute( 111 "INSERT OR REPLACE INTO zettel(id, title, path) values (?1, ?2, ?3)", 112 &[&zettel.id, &zettel.title, &zettel.path], 113 )?; 114 Ok(()) 115 } 116 117 pub fn insert_task(&self, id: &str, desc: &str) -> Result<(), Error> 118 { 119 self.conn.execute( 120 "INSERT OR REPLACE INTO task(id, description) values (?1, ?2)", 121 &[&id, &desc], 122 )?; 123 Ok(()) 124 } 125 126 pub fn remove_zettel(&self, id: &str) -> Result<(), Error> 127 { 128 self.conn 129 .execute("DELETE FROM zettel WHERE id=?1", 130 &[id])?; 131 Ok(()) 132 } 133 134 pub fn search(&self, q: &str) -> Result<Vec<Zettel>, Error> 135 { 136 let mut res = Vec::new(); 137 for z in self.all_zettels()? { 138 139 let mut q = q.trim().to_string(); 140 q.make_ascii_lowercase(); 141 142 if let Some(score) = self.matcher.fuzzy_match(&z.title, &q) { 143 if score > 3 { 144 res.push(z); 145 } 146 } 147 } 148 Ok(res) 149 } 150 151 pub fn all_zettels(&self) -> Result<Vec<Zettel>, Error> 152 { 153 let mut stmt = self.conn.prepare("SELECT id, title, path FROM zettel ORDER BY title COLLATE NOCASE ASC")?; 154 let mut rows = stmt.query([])?; 155 156 let mut results: Vec<Zettel> = Vec::new(); 157 while let Some(row) = rows.next()? { 158 let id: String = row.get(0)?; 159 let title: String = row.get(1)?; 160 let path: String = row.get(2)?; 161 results.push(Zettel { id, title, path }); 162 } 163 164 Ok(results) 165 } 166 167 pub fn all_tasks(&self) -> Result<Vec<Task>, Error> 168 { 169 let mut stmt = self.conn.prepare("SELECT id, description FROM task")?; 170 let mut rows = stmt.query([])?; 171 172 let mut results: Vec<Task> = Vec::new(); 173 while let Some(row) = rows.next()? { 174 let id: String = row.get(0)?; 175 let description: String = row.get(1)?; 176 results.push(Task { id, description }); 177 } 178 179 Ok(results) 180 } 181 182 pub fn link_zettel(&self, id: &str, dst: &str) -> Result<(), Error> { 183 self.conn.execute( 184 "INSERT OR REPLACE INTO zettel_link(id, dst) values (?1, ?2)", 185 &[id, dst], 186 )?; 187 Ok(()) 188 } 189 190 /// Delete links in the given zettel 191 pub fn delete_zettel_links(&self, from: &str) -> Result<(), Error> { 192 self.conn.execute( 193 "DELETE FROM zettel_link WHERE id=?1", 194 &[from], 195 )?; 196 Ok(()) 197 } 198 199 /// Delete links in the given zettel 200 pub fn delete_tasks(&self, doc_id: &str) -> Result<(), Error> { 201 self.conn.execute( 202 "DELETE FROM task WHERE id=?1", 203 &[doc_id], 204 )?; 205 Ok(()) 206 } 207 208 /// Find links that point nowhere 209 pub fn missing_zettels(&self) -> Result<BTreeSet<String>, Error> { 210 let mut stmt = self.conn.prepare("SELECT DISTINCT dst FROM zettel_link")?; 211 let mut rows = stmt.query([])?; 212 213 let mut out = BTreeSet::new(); 214 215 while let Some(row) = rows.next()? { 216 let dst = row.get(0)?; 217 out.insert(dst); 218 } 219 220 Ok(out) 221 } 222 223 pub fn all_links(&self) -> Result<Vec<ZettelLink>, Error> { 224 let mut stmt = self.conn.prepare("SELECT DISTINCT id,dst FROM zettel_link")?; 225 let mut rows = stmt.query([])?; 226 227 let mut out = Vec::new(); 228 229 while let Some(row) = rows.next()? { 230 let from = row.get(0)?; 231 let dst = row.get(1)?; 232 out.push(ZettelLink { from, to: dst }); 233 } 234 Ok(out) 235 } 236 }