use rusqlite::{
OptionalExtension,
Result as SqlResult
};
use greg::{
Point,
Span,
Scale,
Calendar,
Utc,
calendar::zone::Shift
};
use crate::cache::ActivityAffects;
use crate::data::{
Activity,
Category,
Streak,
StreakRule
};
use super::{
Tree,
Zone,
Database
};
pub trait Plan: Database {
fn get_streak(&self, id: u64) -> SqlResult<Streak> {
self.db().query_row(
"SELECT \
id, \
interval, \
start, \
stop, \
added, \
removed, \
rule_type, \
activity_id, \
min_duration, \
category_id \
FROM streak \
WHERE id = ?1",
[id],
|row| row.try_into()
)
}
fn get_active_activity_streak(&self, when: Point, activity: &Activity)
-> SqlResult<Option<Streak>>
{
self.db().query_row(
"SELECT \
id, \
interval, \
start, \
stop, \
added, \
removed, \
rule_type, \
activity_id, \
min_duration, \
category_id \
FROM streak \
WHERE activity_id = ?1 \
AND ( \
(stop IS NULL AND ?2 >= start) \
OR ?2 BETWEEN start AND stop \
) \
ORDER BY start",
(activity.id, when.timestamp),
|row| row.try_into()
).optional()
}
fn get_active_category_streak(&self, when: Point, category: &Category)
-> SqlResult<Option<Streak>>
{
self.db().query_row(
"SELECT \
id, \
interval, \
start, \
stop, \
added, \
removed, \
rule_type, \
activity_id, \
min_duration, \
category_id \
FROM streak \
WHERE category_id = ?1 \
AND ( \
(stop IS NULL AND ?2 >= start) \
OR ?2 BETWEEN start AND stop \
) \
ORDER BY start",
(category.id, when.timestamp),
|row| row.try_into()
).optional()
}
fn begin(
&self,
rule: StreakRule,
begin: Point,
interval: Span)
-> SqlResult<Streak>
{
let begin = calculate_streak_begin(self, begin, interval)?;
let tx = self.db().unchecked_transaction()?;
let mut stmt = self.db().prepare(
"INSERT INTO streak_definition (interval, start, added) \
VALUES (?1, ?2, ?3)"
)?;
let added = Point::now();
let id = stmt.insert((interval, begin, added))? as u64;
match &rule {
StreakRule::Activity {activity_id, min_duration} => {
let mut stmt = self.db().prepare(
"INSERT INTO activity_streak_rule VALUES (?1, ?2, ?3)"
)?;
stmt.insert((id, activity_id, min_duration))?;
}
StreakRule::Category {category_id, min_duration} => {
let mut stmt = self.db().prepare(
"INSERT INTO category_streak_rule VALUES (?1, ?2, ?3)"
)?;
stmt.insert((id, category_id, min_duration))?;
}
}
tx.commit().map(|()| Streak {
id, interval, begin, end: None, added, removed: None, rule
})
}
fn end(&self, streak: &Streak, end: Point) -> SqlResult<bool> {
self.cache().execute(
"UPDATE streakdamage \
SET damage = MIN(damage, ?2) \
WHERE streak_id = ?1",
&(streak.id, end.timestamp)
)?;
self.db().execute(
"UPDATE streak_definition \
SET stop = ?2, removed = ?3 \
WHERE id = ?1",
(streak.id, end, Point::now())
).map(|i| i == 1)
}
fn list_streaks(&self) -> SqlResult<Vec<Streak>> {
let mut streaks = Vec::new();
let mut stmt = self.db().prepare(
"SELECT \
id, \
interval, \
start, \
stop, \
added, \
removed, \
rule_type, \
activity_id, \
min_duration, \
category_id \
FROM streak \
ORDER BY start"
)?;
let mut rows = stmt.query([])?;
while let Some(row) = rows.next()? {
streaks.push(row.try_into()?);
}
Ok(streaks)
}
fn list_active_streaks(&self) -> SqlResult<Vec<Streak>> {
let mut streaks = Vec::new();
let mut stmt = self.db().prepare(
"SELECT \
id, \
interval, \
start, \
stop, \
added, \
removed, \
rule_type, \
activity_id, \
min_duration, \
category_id \
FROM streak \
WHERE (stop IS NULL AND ?1 >= start) \
OR ?1 BETWEEN start AND stop \
ORDER BY start"
)?;
let mut rows = stmt.query([Point::now()])?;
while let Some(row) = rows.next()? {
streaks.push(row.try_into()?);
}
Ok(streaks)
}
fn set_activity_affects(&self, streak: &Streak) -> SqlResult<()> {
match streak.rule {
StreakRule::Activity { activity_id, .. } => {
let affects = ActivityAffects {
activity_id,
streak_id: streak.id
};
self.cache().upsert(&affects)?;
},
StreakRule::Category { category_id, .. } => {
let cat = self.get_category(category_id)?;
let acts = self.get_activities_under(&cat)?;
let mut stmt = self.cache().prepare(
"INSERT OR IGNORE INTO activityaffects VALUES (?1, ?2)",
)?;
for act in acts {
stmt.execute((act.id, streak.id))?;
}
}
}
Ok(())
}
}
impl<Db: Database + ?Sized> Plan for Db {}
fn calculate_streak_begin<Db: Database + ?Sized>(
db: &Db,
begin: Point,
interval: Span)
-> SqlResult<Point>
{
let (scale, _count) = interval.scale_div();
assert!(
scale >= Scale::Days,
"streak intervals shorter than a day are currently not supported"
);
let calendar = db.load_calendar()?;
let begin_utc = calendar.0.apply(begin);
Ok(Calendar(Utc).date_floor(scale, begin_utc))
}
#[test]
fn empty() -> SqlResult<()> {
let nunc = crate::Nunc::testing().unwrap();
assert!(nunc.list_streaks()?.is_empty());
Ok(())
}
#[test]
fn begin_end() -> SqlResult<()> {
let interval = Span::parse("1d");
let duration = Span::parse("10m");
let begin = Point::now() - duration * 2;
let end = Point::now() - duration;
let activity_rule = StreakRule::Activity {
activity_id: 1,
min_duration: duration
};
let category_rule = StreakRule::Category {
category_id: 1,
min_duration: duration
};
for rule in [activity_rule, category_rule] {
let nunc = crate::Nunc::testing().unwrap();
crate::tests::add_example_tree(&nunc)?;
let added = Point::now();
let streak = nunc.begin(rule, begin, interval)?;
assert_eq!(nunc.list_streaks()?[0], streak);
assert_eq!(nunc.list_active_streaks()?[0], streak);
dbg!(&streak);
let removed = Point::now();
dbg!(end);
assert!(nunc.end(&streak, end)?);
let stopped = nunc.list_streaks()?.pop().unwrap();
assert_ne!(stopped, streak);
assert!(nunc.list_active_streaks()?.is_empty());
assert_eq!(stopped.end, Some(end));
assert_eq!(stopped.removed, Some(removed));
assert_eq!(stopped.added, added);
}
Ok(())
}
#[test]
fn activity_affects() -> SqlResult<()> {
let nunc = crate::Nunc::testing().unwrap();
crate::tests::add_example_tree(&nunc)?;
let run_id = nunc.find_activity("Run")?.unwrap().id;
let run = StreakRule::Activity {
activity_id: run_id,
min_duration: Span::parse("10m")
};
let run_streak = nunc.begin(run, Point::now(), Span::DAY)?;
assert!(nunc.cache().get_all::<ActivityAffects>()?.is_empty());
nunc.set_activity_affects(&run_streak)?;
let affects = nunc.cache().get_all::<ActivityAffects>()?;
assert_eq!(affects.len(), 1);
assert_eq!(
affects[0],
ActivityAffects {streak_id: run_streak.id, activity_id: run_id}
);
let beta_id = nunc.find_category("Beta")?.unwrap().id;
let beta = StreakRule::Category {
category_id: beta_id,
min_duration: Span::parse("10m")
};
let beta_streak = nunc.begin(beta, Point::now(), Span::DAY)?;
nunc.set_activity_affects(&beta_streak)?;
let prog_id = nunc.find_activity("Programming")?.unwrap().id;
let fit_id = nunc.find_activity("Fitness")?.unwrap().id;
let all_affects = nunc.cache().get_all::<ActivityAffects>()?;
assert_eq!(all_affects.len(), 3);
let affects = all_affects
.into_iter()
.filter(|a| a.streak_id == beta_streak.id)
.collect::<Vec<_>>();
assert_eq!(affects.len(), 2);
assert!(affects.iter().any(|a| a.activity_id == prog_id));
assert!(affects.iter().any(|a| a.activity_id == fit_id));
let alpha_id = nunc.find_category("Alpha")?.unwrap().id;
let alpha = StreakRule::Category {
category_id: alpha_id,
min_duration: Span::parse("10m")
};
let alpha_streak = nunc.begin(alpha, Point::now(), Span::DAY)?;
nunc.set_activity_affects(&alpha_streak)?;
let russian_id = nunc.find_activity("Russian")?.unwrap().id;
let chess_id = nunc.find_activity("Chess")?.unwrap().id;
let all_affects = nunc.cache().get_all::<ActivityAffects>()?;
assert_eq!(all_affects.len(), 6);
let affects = all_affects
.into_iter()
.filter(|a| a.streak_id == alpha_streak.id)
.collect::<Vec<_>>();
assert_eq!(affects.len(), 3);
assert!(affects.iter().any(|a| a.activity_id == run_id));
assert!(affects.iter().any(|a| a.activity_id == russian_id));
assert!(affects.iter().any(|a| a.activity_id == chess_id));
let run_affects = nunc.cache().get_all::<ActivityAffects>()?
.into_iter()
.filter(|a| a.activity_id == run_id)
.collect::<Vec<_>>();
assert_eq!(run_affects.len(), 2);
assert!(run_affects.iter().any(|a| a.streak_id == run_streak.id));
assert!(run_affects.iter().any(|a| a.streak_id == alpha_streak.id));
Ok(())
}