nunc 0.2.0

WIP: time tracking application
Documentation
use std::collections::BTreeSet;

use rusqlite::Result as SqlResult;

use greg::{
	Point,
	Frame
};

use crate::data::{
	Activity,
	Entry,
	SetEntry
};

use super::{
	Database,
	Measure
};

pub trait Log: Database {
	fn get_entry(&self, id: u64) -> SqlResult<Entry> {
		self.db().query_row(
			"SELECT id, activity_id, start, stop FROM entry WHERE id = ?1",
			[id],
			|row| row.try_into()
		)
	}
	fn get_last_entries(&self, n: usize) -> SqlResult<Vec<Entry>> {
		let mut stmt = self.db().prepare(
			"SELECT id, activity_id, start, stop \
			FROM entry \
			ORDER BY id DESC \
			LIMIT ?1"
		)?;
		let mut entries = Vec::new();
		let mut rows = stmt.query([n])?;
		while let Some(row) = rows.next()? {
			entries.push(row.try_into()?);
		}
		Ok(entries)
	}

	/// Start a new entry for the activity
	fn start(&self, activity: &Activity, when: Point)
		-> SqlResult<u64>
	{
		self.damage_at(activity.id, when)?;
		let mut stmt = self.db().prepare(
			"INSERT INTO entry (activity_id, start) \
			VALUES (?1, ?2)"
		)?;
		stmt.insert((activity.id, when)).map(|i| i as u64)
	}
	fn stop(&self, entry: &Entry, when: Point) -> SqlResult<bool> {
		self.damage_at(entry.activity_id, when)?;
		let mut stmt = self.db().prepare(
			"UPDATE entry SET stop = ?2 WHERE id = ?1 AND stop IS NULL"
		)?;
		stmt.execute((entry.id, when)).map(|i| i == 1)
	}

	fn update(&self, entry: &Entry) -> SqlResult<bool> {
		self.damage_at(entry.activity_id, entry.start)?;
		let mut stmt = self.db().prepare(
			"UPDATE entry SET \
				activity_id = ?2, \
				start = ?3, \
				stop = ?4 \
			WHERE id = ?1"
		)?;
		let Entry {id, activity_id, start, stop} = entry;
		stmt.execute((id, activity_id, start, stop)).map(|i| i == 1)
	}

	fn delete(&self, entry: &Entry) -> SqlResult<bool> {
		self.damage_at(entry.activity_id, entry.start)?;
		let mut stmt = self.db().prepare("DELETE from entry WHERE id = ?1")?;
		stmt.execute([entry.id]).map(|i| i == 1)
	}

	fn insert(&self, entry: &Entry) -> SqlResult<bool> {
		self.damage_at(entry.activity_id, entry.start)?;
		self.db().execute(
			"INSERT INTO entry VALUES (?1, ?2, ?3, ?4)",
			(entry.id, entry.activity_id,  entry.start,  entry.stop)
		).map(|i| i == 1)
	}
	fn create(
		&self,
		activity: &Activity,
		start: Point,
		stop: Point)
		-> SqlResult<u64>
	{
		self.damage_at(activity.id, start)?;
		let mut stmt = self.db().prepare(
			"INSERT INTO entry (activity_id, start, stop) VALUES (?1, ?2, ?3)"
		)?;
		stmt.insert((activity.id, start, stop)).map(|i| i as u64)
	}

	fn recap(&self, frame: Frame) -> SqlResult<Vec<Entry>> {
		let mut stmt = self.db().prepare(
			// "inclusive" timespan selection with special 0s timespan case
			"SELECT \
				id, activity_id, start, stop \
			FROM entry \
			WHERE ( \
				(?1 <= start AND start < ?2) \
				OR (?1 < stop AND stop <= ?2) \
				OR (start < ?1 AND ?2 < stop) \
				OR (start <= ?1 AND stop IS NULL) \
				OR (?1 == ?2 AND ?1 == start) \
			) \
			ORDER BY start"
		)?;
		let mut entries = Vec::new();
		let mut rows = stmt.query((frame.start, frame.stop))?;
		while let Some(row) = rows.next()? {
			entries.push(row.try_into()?);
		}
		Ok(entries)
	}

	fn get_active_entries(&self) -> SqlResult<Vec<Entry>> {
		let mut stmt = self.db().prepare(
			"SELECT id, activity_id, start, stop \
			FROM entry \
			WHERE stop IS NULL \
			ORDER BY start"
		)?;
		let mut entries = Vec::new();
		let mut rows = stmt.query([])?;
		while let Some(row) = rows.next()? {
			entries.push(row.try_into()?);
		}
		Ok(entries)
	}

	fn load_set(&self) -> SqlResult<BTreeSet<SetEntry>> {
		let mut set = BTreeSet::new();
		// We trust the DB consistency constraints
		let mut stmt = self.db().prepare(
			"SELECT activity_id, start, stop FROM entry ORDER BY start"
		)?;
		let mut rows = stmt.query([])?;
		while let Some(row) = rows.next()? {
			let (activity_id, start, stop) = row.try_into()?;
			set.insert(SetEntry {activity_id, start, stop});
		}
		Ok(set)
	}

}

impl<Db: Database + ?Sized> Log for Db {}


#[test]
fn empty() -> SqlResult<()> {
	let db = crate::Nunc::testing().unwrap();

	assert!(db.get_active_entries()?.is_empty());

	let jan_1970 = Point::from_epoch(0);
	let may_2033 = Point::from_epoch(2000000000);

	assert!(db.recap(Frame::new(jan_1970..jan_1970))?.is_empty());
	assert!(db.recap(Frame::new(jan_1970..may_2033))?.is_empty());
	assert!(db.recap(Frame::new(may_2033..may_2033))?.is_empty());
	assert!(db.recap(Frame::new(may_2033..jan_1970))?.is_empty());
	Ok(())
}


#[test]
fn start_stop() -> SqlResult<()> {
	use super::Tree;
	let db = crate::Nunc::testing().unwrap();
	crate::tests::add_example_tree(&db)?;
	db.get_active_entries()?;
	let run = db.find_activity("Run")?.unwrap();

	// Valid
	let ent_id = db.start(&run, Point::now())?;
	let ent = db.get_entry(ent_id)?;
	assert_eq!(ent, db.get_active_entries()?[0]);
	db.stop(&ent, Point::now())?;

	// Invalid
	let ent_id = db.start(&run, Point::from_epoch(1000))?;
	let ent = db.get_entry(ent_id)?;
	db.stop(&ent, Point::from_epoch(999)).unwrap_err();
	Ok(())
}

#[test]
fn create_insert() -> SqlResult<()> {
	use super::Tree;
	let db = crate::Nunc::testing().unwrap();
	crate::tests::add_example_tree(&db)?;

	db.get_active_entries()?;
	let run = db.find_activity("Run")?.unwrap();
	let now = Point::now();

	// create
	let ent_id = db.create(&run, now, now)?;
	let ent = db.get_entry(ent_id)?;
	assert!(!db.stop(&ent, Point::now())?);

	// insert
	let ent = Entry {
		id: 100,
		activity_id: run.id,
		start: now,
		stop: Some(now)
	};
	assert!(db.insert(&ent)?);
	assert_eq!(ent, db.get_entry(ent.id)?);
	assert!(!db.stop(&ent, Point::now())?);
	Ok(())
}

#[test]
fn update() -> SqlResult<()> {
	use super::Tree;
	let db = crate::Nunc::testing().unwrap();
	crate::tests::add_example_tree(&db)?;

	db.get_active_entries()?;
	let run = db.find_activity("Run")?.unwrap();
	let chess = db.find_activity("Chess")?.unwrap();
	let now = Point::now();

	// create
	let ent_id = db.create(&run, now, now)?;
	let mut ent = db.get_entry(ent_id)?;

	ent.stop = Some(Point::now() - greg::Span::MINUTE);
	assert!(db.update(&ent).is_err());

	ent.activity_id = chess.id;
	assert!(db.update(&ent).is_err());

	ent.stop = None;
	assert!(db.update(&ent)?);

	ent.stop = Some(Point::now() + greg::Span::MINUTE);
	assert!(db.update(&ent)?);

	ent.stop = None;
	assert!(db.update(&ent)?);

	ent.start = Point::now() - greg::Span::MINUTE;
	ent.stop = Some(Point::now() + greg::Span::MINUTE);
	assert!(db.update(&ent)?);


	Ok(())
}