use regex::{Regex, Captures};
use rusqlite::{Connection, DatabaseName, Error, Result, Row, named_params};
use crate::{SQL_ARRAY_SEPARATOR, config::ConfigOptions, str_to_vec, zettel::Zettel};
use rayon::prelude::*;
impl Zettel {
fn from_db(row: &Row) -> Result<Zettel, rusqlite::Error>
{
let title: String = row.get(0)?;
let project: String = row.get(1)?;
let links: String = row.get(2)?;
let tags: String = row.get(3)?;
let mut z = Zettel::new(&title, &project);
z.links = str_to_vec(&links);
z.tags = str_to_vec(&tags);
Ok(z)
}
}
pub struct Database
{
name: String,
conn: Connection,
}
fn cli_input_to_db_input(inp: &str) -> String
{
let re = Regex::new(r"(\\\\|%|\*|\\\*|_|\.|\\\.|\\)").unwrap();
re.replace_all(inp, |cap: &Captures| {
match &cap[0] {
r"\\" => r"\",
r"%" => r"\%",
r"*" => r"%",
r"\*" => r"*",
r"_" => r"\_",
r"." => r"_",
r"\." => r".",
_ => r"",
}
}).to_string()
}
impl Database
{
pub fn new(name: &str) -> Result<Self, Error>
{
Ok(Database {
name: name.to_string(),
conn: Connection::open(name)?,
})
}
pub fn in_memory(name: &str) -> Result<Self, Error>
{
let uri = &format!("file:{}?mode=memory&cache=shared", name);
Ok(Database {
name: name.to_string(),
conn: Connection::open(uri)?,
})
}
pub fn init(&self) -> Result<(), Error>
{
self.conn.execute(
"CREATE TABLE IF NOT EXISTS zettelkasten (
title TEXT NOT NULL,
project TEXT,
links TEXT,
tags TEXT,
UNIQUE(title, project)
)",
[])?;
Ok(())
}
pub fn write_to(&self, path: &str) -> Result<(), Error>
{
self.conn.backup(DatabaseName::Main, path, None)?;
Ok(())
}
pub fn save(&self, zettel: &Zettel) -> Result<(), Error>
{
let links = crate::vec_to_str(&zettel.links);
let tags = crate::vec_to_str(&zettel.tags);
self.conn.execute(
"INSERT INTO zettelkasten (title, project, links, tags) values (?1, ?2, ?3, ?4)",
&[ &zettel.title, &zettel.project, &links, &tags ])?;
Ok(())
}
pub fn delete(&self, zettel: &Zettel) -> Result<(), Error>
{
self.conn.execute(
"DELETE FROM zettelkasten WHERE title=?1 AND project=?2",
&[&zettel.title, &zettel.project ])?;
Ok(())
}
pub fn all(&self) -> Result<Vec<Zettel>, Error>
{
let mut stmt = self.conn.prepare("SELECT * FROM zettelkasten")?;
let mut rows = stmt.query([])?;
let mut results: Vec<Zettel> = Vec::new();
while let Some(row) = rows.next()? {
let zettel = Zettel::from_db(row)?;
results.push(zettel);
}
Ok(results)
}
pub fn find_by_title(&self, pattern: &str) -> Result<Vec<Zettel>, Error>
{
let req_pattern = cli_input_to_db_input(pattern);
let mut stmt = self.conn.prepare("SELECT * FROM zettelkasten WHERE title LIKE :req_pattern")?;
let mut rows = stmt.query(named_params! {":req_pattern": req_pattern})?;
let mut results: Vec<Zettel> = Vec::new();
while let Some(row) = rows.next()? {
let zettel = Zettel::from_db(row)?;
results.push(zettel);
}
Ok(results)
}
pub fn find_by_tag(&self, pattern: &str) -> Result<Vec<Zettel>, Error>
{
let req_pattern = format!(
"%{}{}{}%",
SQL_ARRAY_SEPARATOR,
cli_input_to_db_input(pattern),
SQL_ARRAY_SEPARATOR,
);
let mut stmt = self.conn.prepare("SELECT * FROM zettelkasten WHERE tags LIKE :req_pattern")?;
let mut rows = stmt.query(named_params! {":req_pattern": req_pattern})?;
let mut results: Vec<Zettel> = Vec::new();
while let Some(row) = rows.next()? {
let zettel = Zettel::from_db(row)?;
results.push(zettel);
}
Ok(results)
}
pub fn list_tags(&self) -> Result<Vec<String>, Error>
{
let mut stmt = self.conn.prepare("SELECT tags FROM zettelkasten")?;
let mut rows = stmt.query([])?;
let mut results: Vec<String> = Vec::new();
while let Some(row) = rows.next()? {
let tags: String = row.get(0)?;
for tag in str_to_vec(&tags) {
results.push(tag);
}
}
results.par_sort();
results.dedup();
Ok(results)
}
pub fn list_projects(&self) -> Result<Vec<String>, Error>
{
let mut stmt = self.conn.prepare("SELECT project FROM zettelkasten")?;
let mut rows = stmt.query([])?;
let mut results: Vec<String> = Vec::new();
while let Some(row) = rows.next()? {
let project: String = row.get(0)?;
if ! project.is_empty() {
results.push(project);
}
}
results.par_sort();
results.dedup();
Ok(results)
}
pub fn find_by_links_to(&self, pattern: &str) -> Result<Vec<Zettel>>
{
let req_pattern = format!(
"%{}{}{}%",
SQL_ARRAY_SEPARATOR,
cli_input_to_db_input(pattern),
SQL_ARRAY_SEPARATOR,
);
let mut stmt = self.conn.prepare("SELECT * FROM zettelkasten WHERE links LIKE :req_pattern")?;
let mut rows = stmt.query(named_params! {":req_pattern": req_pattern})?;
let mut results: Vec<Zettel> = Vec::new();
while let Some(row) = rows.next()? {
let zettel = Zettel::from_db(row)?;
results.push(zettel);
}
Ok(results)
}
pub fn zettel_not_yet_created(&self) -> Result<Vec<String>>
{
let mut stmt = self.conn.prepare("SELECT links FROM zettelkasten")?;
let mut rows = stmt.query([])?;
let mut unique_links: Vec<String> = Vec::new();
while let Some(row) = rows.next()? {
let links_str: String = row.get(0)?;
let links = str_to_vec(&links_str);
unique_links.extend(links);
}
unique_links.par_sort();
unique_links.dedup();
Ok(unique_links.into_iter()
.filter(|link| {
self.find_by_title(link).unwrap().is_empty()
})
.collect())
}
pub fn generate(&self, cfg: &ConfigOptions)
{
let db_name = &self.name;
let mut directories = crate::io::list_subdirectories(&cfg.zettelkasten);
directories.push(cfg.zettelkasten.clone());
directories.par_iter()
.for_each(|dir| {
let notes = crate::io::list_md_files(dir);
notes.par_iter()
.for_each(|note| {
let thread_db = Self::in_memory(db_name).unwrap();
let thread_zettel = Zettel::from_file(cfg, note);
thread_db.save(&thread_zettel).unwrap();
});
});
}
pub fn update(&self, cfg: &ConfigOptions, zettel: &Zettel) -> Result<(), Error>
{
self.delete(zettel)?;
let z = &Zettel::from_file(cfg, &zettel.filename(cfg));
self.save(z)?;
Ok(())
}
pub fn search_text(&self, cfg: &ConfigOptions, text: &str) -> Result<Vec<Zettel>, Error>
{
let zettel = self.all()?;
Ok(zettel.par_iter()
.filter(|z|
z.has_text(cfg, text)
).map(|z| z.clone())
.collect())
}
pub fn change_project(&self, zettel: &Zettel, new_project: &str) -> Result<(), Error>
{
self.conn.execute(
"UPDATE zettelkasten SET project=?1 WHERE title=?2 AND project=?3",
&[ new_project, &zettel.title, &zettel.project ])?;
Ok(())
}
pub fn change_title(&self, zettel: &Zettel, new_title: &str) -> Result<(), Error>
{
self.conn.execute(
"UPDATE zettelkasten SET title=?1 WHERE title=?2 AND project=?3",
&[ new_title, &zettel.title, &zettel.project ])?;
Ok(())
}
}