/ src / database.rs
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  }