use chrono::{Local, Utc};
use list::{Error, List};
use query::ast::{Expression, AST};
use query::token::Operator;
use rusqlite::types::ToSql;
use rusqlite::{Connection, Error as SQLError, OpenFlags, Row, NO_PARAMS};
use std::path::Path;
use task::{Note, Task};
impl From<SQLError> for Error {
fn from(e: SQLError) -> Error {
Error::BadRequest(e.to_string())
}
}
const CREATE_TASK_TABLE: &str = "CREATE TABLE IF NOT EXISTS tasks(
id text PRIMARY KEY,
title text,
body text,
context text,
priority integer,
created_date integer,
completed_date integer
)";
const CREATE_NOTE_TABLE: &str = "CREATE TABLE IF NOT EXISTS notes(
task_id text,
id text,
body text,
created_date integer,
FOREIGN KEY(task_id) REFERENCES tasks(id)
)";
pub struct SQLiteList {
conn: Connection,
}
impl SQLiteList {
pub fn new(file_name: &Path) -> SQLiteList {
SQLiteList::open(file_name).unwrap()
}
pub fn create_tables(&mut self) -> Result<(), rusqlite::Error> {
self.conn.execute(CREATE_TASK_TABLE, NO_PARAMS)?;
self.conn.execute(CREATE_NOTE_TABLE, NO_PARAMS).map(|_| ())
}
pub fn open(file_name: &Path) -> Result<SQLiteList, rusqlite::Error> {
Connection::open_with_flags(
file_name,
OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE,
).map(|conn| SQLiteList { conn })
}
fn eval(exp: Expression) -> String {
match exp {
Expression::Infix(left, op, right) => SQLiteList::eval_infix(&left, op, &right),
Expression::String(s) => format!(
"(title LIKE \"%{}%\" OR body LIKE \"%{}%\")",
s.clone(),
s.clone()
),
_ => "".to_string(),
}
}
fn eval_literal(exp: Expression) -> String {
match exp {
Expression::Date(d) => format!("\"{}\"", d.with_timezone(&Utc).to_rfc3339()),
Expression::String(s) => format!("\"{}\"", s),
x => format!("{}", x),
}
}
fn eval_infix(left: &Expression, op: Operator, right: &Expression) -> String {
match op {
Operator::AND => {
return format!(
"({} AND {})",
SQLiteList::eval(left.clone()),
SQLiteList::eval(right.clone()),
)
}
Operator::OR => {
return format!(
"({} OR {})",
SQLiteList::eval(left.clone()),
SQLiteList::eval(right.clone()),
)
}
_ => (),
};
let ident = match left {
Expression::String(s) => match s.as_ref() {
"completed" => "completed_date",
s => s,
},
_ => "",
};
let val = match right {
Expression::Bool(true) => return format!("({} is not null)", ident),
Expression::Bool(false) => return format!("({} is null)", ident),
x => SQLiteList::eval_literal(x.to_owned()),
};
match op {
Operator::LIKE => format!("({} LIKE \"%{}%\")", ident, val),
Operator::NLIKE => format!("({} NOT LIKE \"%{}%\")", ident, val),
x => format!("({} {} {})", ident, x, val),
}
}
fn populate_notes(&mut self, task: &mut Task) -> Result<(), Error> {
let mut stmt = self
.conn
.prepare("SELECT id, body, created_date FROM notes WHERE task_id = ?")?;
let iter = stmt.query_map(&[&task.id], SQLiteList::row_to_note)?;
for note in iter {
match note {
Ok(n) => task.notes.push(n),
Err(e) => return Err(Error::from(e)),
}
}
Ok(())
}
fn row_to_task(row: &Row) -> Task {
Task {
id: row.get(0),
title: row.get(1),
context: row.get(2),
created_date: row.get(3),
priority: row.get(4),
completed_date: row.get(5),
body: row.get(6),
notes: vec![],
}
}
fn row_to_note(row: &Row) -> Note {
Note {
id: row.get(0),
body: row.get(1),
created_date: row.get(2),
}
}
}
impl List for SQLiteList {
fn add(&mut self, task: Task) -> Result<(), Error> {
self.conn
.execute(
"
INSERT INTO tasks (
id,
title,
context,
created_date,
priority,
completed_date,
body
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
&[
&task.id as &ToSql,
&task.title as &ToSql,
&task.context as &ToSql,
&task.created_date,
&task.priority as &ToSql,
&task.completed_date,
&task.body as &ToSql,
],
).map(|_| ())
.map_err(Error::from)
}
fn add_multiple(&mut self, task: Vec<Task>) -> Result<(), Error> {
for task in task.iter() {
self.add(task.clone())?;
}
Ok(())
}
fn find_by_id(&mut self, id: String) -> Result<Task, Error> {
let mut task = self
.conn
.query_row(
"SELECT id, title, context, created_date, priority,
completed_date, body
FROM tasks
WHERE id = ?1",
&[&id],
SQLiteList::row_to_task,
).map_err(Error::from)?;
self.populate_notes(&mut task)?;
Ok(task)
}
fn current(&mut self) -> Result<Task, Error> {
self.conn
.query_row(
"SELECT id, title, context, created_date, priority,
completed_date, body
FROM tasks
WHERE completed_date is null
ORDER BY priority DESC, created_date ASC
LIMIT 1",
NO_PARAMS,
SQLiteList::row_to_task,
).map_err(Error::from)
}
fn contexts(&mut self) -> Result<Vec<String>, Error> {
let mut stmt = self.conn.prepare("SELECT context FROM tasks")?;
let iter = stmt.query_map(NO_PARAMS, |row| row.get(0))?;
let mut contexts = Vec::new();
for context in iter {
match context {
Ok(c) => contexts.push(c),
Err(e) => return Err(Error::from(e)),
}
}
Ok(contexts)
}
fn complete(&mut self, id: String) -> Result<(), Error> {
self.conn
.execute(
"UPDATE tasks SET completed_date = ?2 WHERE id = ?1",
&[&id as &ToSql, &Local::now()],
).map(|_| ())
.map_err(Error::from)
}
fn update(&mut self, task: Task) -> Result<(), Error> {
self.conn
.execute(
"
UPDATE tasks
SET title = ?2,
context = ?3,
priority = ?4,
body = ?5
WHERE id = ?1",
&[
&task.id as &ToSql,
&task.title as &ToSql,
&task.context as &ToSql,
&task.priority as &ToSql,
&task.body as &ToSql,
],
).map(|_| ())
.map_err(Error::from)
}
fn add_note(&mut self, id: String, note: Note) -> Result<(), Error> {
self.conn
.execute(
"INSERT INTO notes (task_id, id, body, created_date) VALUES (?1, ?2, ?3, ?4)",
&[
&id as &ToSql,
¬e.id as &ToSql,
¬e.body as &ToSql,
¬e.created_date,
],
).map(|_| ())
.map_err(Error::from)
}
fn search(&mut self, ast: AST) -> Result<Vec<Task>, Error> {
let where_clause = match ast.expression {
infix => SQLiteList::eval(infix),
};
let sql_query = if where_clause != "" {
format!(
"SELECT id, title, context, created_date,
priority, completed_date, body
FROM tasks
WHERE {}",
where_clause
)
} else {
"SELECT id, title, context, created_date, priority,
completed_date, body
FROM tasks"
.to_string()
};
let mut tasks: Vec<Task> = Vec::new();
{
let mut stmt = self.conn.prepare(&sql_query)?;
let iter = stmt
.query_map(NO_PARAMS, SQLiteList::row_to_task)
.map_err(Error::from)?;
for task in iter {
match task {
Ok(t) => tasks.push(t),
Err(e) => return Err(Error::from(e)),
}
}
}
for task in tasks.iter_mut() {
self.populate_notes(task)?;
}
Ok(tasks)
}
}
#[cfg(test)]
pub mod test {
use super::SQLiteList;
use list::List;
use rand::distributions::Alphanumeric;
use rand::Rng;
use std::fs;
use std::path::PathBuf;
const TEST_DIR: &str = ".sqlite_test_tmp";
fn setup_sqlite() -> SQLiteList {
match fs::create_dir(TEST_DIR) {
Ok(_) => (),
Err(_) => (),
};
let s = rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(10)
.collect::<String>();
let path = PathBuf::from(format!("{}/{}.sqlite", TEST_DIR, s));
let mut list = SQLiteList::new(&path);
list.create_tables().unwrap();
list
}
test_list!(setup_sqlite);
}