use std::str::FromStr;
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 links: String = row.get(1)?;
let tags: String = row.get(2)?;
let inbox_row: String = row.get(3)?;
let inbox: bool = FromStr::from_str(&inbox_row).unwrap_or(false);
let mut z = Zettel::new(&title, inbox);
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,
links TEXT,
tags TEXT,
inbox INTEGER,
UNIQUE(title, inbox)
)",
[])?;
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, links, tags, inbox) values (?1, ?2, ?3, ?4)",
&[&zettel.title, &links, &tags, &zettel.inbox.to_string() ])?;
Ok(())
}
pub fn delete(&self, zettel: &Zettel) -> Result<(), Error>
{
self.conn.execute(
"DELETE FROM zettelkasten WHERE title=?1 AND inbox=?2",
&[&zettel.title, &zettel.inbox.to_string() ])?;
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 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 mut files = crate::io::list_md_files(&cfg.zettelkasten);
files.append(&mut crate::io::list_md_files(&format!("{}/inbox", &cfg.zettelkasten)));
let name = &self.name;
files.par_iter()
.for_each(|f| {
let thread_db = Self::in_memory(name).unwrap();
let thread_zettel = Zettel::from_file(f);
thread_db.save(&thread_zettel).unwrap();
});
}
pub fn update(&self, cfg: &ConfigOptions, zettel: &Zettel) -> Result<(), Error>
{
self.delete(zettel)?;
let z = &Zettel::from_file(&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())
}
}