use anyhow::{Context, Ok, Result};
use parking_lot::Mutex;
use rusqlite::{Connection, params};
use rusqlite_migration::{M, Migrations};
use ustr::Ustr;
use crate::{data::ExerciseDelta, error::PracticeDeltasError, utils};
pub trait PracticeDeltas {
fn get_deltas(
&self,
exercise_id: Ustr,
num_deltas: u32,
) -> Result<Vec<ExerciseDelta>, PracticeDeltasError>;
fn record_exercise_deltas(
&mut self,
deltas: &[ExerciseDelta],
) -> Result<(), PracticeDeltasError>;
fn trim_deltas(&mut self, num_deltas: u32) -> Result<(), PracticeDeltasError>;
fn remove_deltas_with_prefix(&mut self, prefix: &str) -> Result<(), PracticeDeltasError>;
}
pub struct LocalPracticeDeltas {
connection: Mutex<Connection>,
}
impl LocalPracticeDeltas {
fn migrations() -> Migrations<'static> {
Migrations::new(vec![
M::up("CREATE TABLE uids(unit_uid INTEGER PRIMARY KEY, unit_id TEXT NOT NULL UNIQUE);")
.down("DROP TABLE uids;"),
M::up(
"CREATE TABLE practice_deltas(
id INTEGER PRIMARY KEY,
unit_uid INTEGER NOT NULL REFERENCES uids(unit_uid),
delta REAL, timestamp INTEGER);",
)
.down("DROP TABLE practice_deltas"),
M::up("CREATE INDEX deltas_trials ON practice_deltas (unit_uid, timestamp);")
.down("DROP INDEX deltas_trials"),
])
}
fn init(&mut self) -> Result<()> {
let migrations = Self::migrations();
let mut connection = self.connection.lock();
migrations
.to_latest(&mut connection)
.context("failed to initialize practice deltas DB")
}
fn new(connection: Connection) -> Result<LocalPracticeDeltas> {
let mut deltas = LocalPracticeDeltas {
connection: Mutex::new(connection),
};
deltas.init()?;
Ok(deltas)
}
pub fn new_from_disk(db_path: &str) -> Result<LocalPracticeDeltas> {
Self::new(utils::new_connection(db_path)?)
}
fn get_deltas_helper(&self, exercise_id: Ustr, num_deltas: u32) -> Result<Vec<ExerciseDelta>> {
let connection = self.connection.lock();
let mut stmt = connection.prepare_cached(
"SELECT delta, timestamp from practice_deltas WHERE unit_uid = (
SELECT unit_uid FROM uids WHERE unit_id = $1)
ORDER BY timestamp DESC LIMIT ?2;",
)?;
#[allow(clippy::let_and_return)]
let rows = stmt
.query_map(params![exercise_id.as_str(), num_deltas], |row| {
let delta = row.get(0)?;
let timestamp = row.get(1)?;
rusqlite::Result::Ok(ExerciseDelta {
exercise_id,
delta,
timestamp,
})
})?
.map(|r| r.context("failed to retrieve deltas from practice deltas DB"))
.collect::<Result<Vec<ExerciseDelta>, _>>()?;
Ok(rows)
}
fn record_exercise_deltas_helper(&mut self, deltas: &[ExerciseDelta]) -> Result<()> {
let mut connection = self.connection.lock();
let tx = connection.transaction()?;
{
let mut uid_stmt =
tx.prepare_cached("INSERT OR IGNORE INTO uids(unit_id) VALUES ($1);")?;
let mut stmt = tx.prepare_cached(
"INSERT INTO practice_deltas (unit_uid, delta, timestamp) VALUES (
(SELECT unit_uid FROM uids WHERE unit_id = $1), $2, $3);",
)?;
for delta in deltas {
uid_stmt.execute(params![delta.exercise_id.as_str()])?;
stmt.execute(params![
delta.exercise_id.as_str(),
delta.delta,
delta.timestamp
])?;
}
}
tx.commit()?;
Ok(())
}
fn trim_deltas_helper(&mut self, num_deltas: u32) -> Result<()> {
let connection = self.connection.lock();
let mut uid_stmt = connection.prepare_cached("SELECT unit_uid from uids")?;
let uids = uid_stmt
.query_map([], |row| row.get(0))?
.map(|r| r.context("failed to retrieve UIDs from practice deltas DB"))
.collect::<Result<Vec<i64>, _>>()?;
for uid in uids {
let mut stmt = connection.prepare_cached(
"DELETE FROM practice_deltas WHERE unit_uid = $1 AND timestamp NOT IN (
SELECT timestamp FROM practice_deltas WHERE unit_uid = $1
ORDER BY timestamp DESC LIMIT ?2);",
)?;
let _ = stmt.execute(params![uid, num_deltas])?;
}
connection.execute_batch("VACUUM;")?;
Ok(())
}
fn remove_deltas_with_prefix_helper(&mut self, prefix: &str) -> Result<()> {
let connection = self.connection.lock();
let mut uid_stmt =
connection.prepare_cached("SELECT unit_uid FROM uids WHERE unit_id LIKE $1;")?;
let uids = uid_stmt
.query_map(params![format!("{}%", prefix)], |row| row.get(0))?
.map(|r| r.context("failed to retrieve UIDs from practice deltas DB"))
.collect::<Result<Vec<i64>, _>>()?;
for uid in uids {
let mut stmt =
connection.prepare_cached("DELETE FROM practice_deltas WHERE unit_uid = $1;")?;
let _ = stmt.execute(params![uid])?;
}
connection.execute_batch("VACUUM;")?;
Ok(())
}
}
impl PracticeDeltas for LocalPracticeDeltas {
fn get_deltas(
&self,
exercise_id: Ustr,
num_deltas: u32,
) -> Result<Vec<ExerciseDelta>, PracticeDeltasError> {
self.get_deltas_helper(exercise_id, num_deltas)
.map_err(|e| PracticeDeltasError::GetDeltas(exercise_id, e))
}
fn record_exercise_deltas(
&mut self,
deltas: &[ExerciseDelta],
) -> Result<(), PracticeDeltasError> {
self.record_exercise_deltas_helper(deltas)
.map_err(PracticeDeltasError::RecordDelta)
}
fn trim_deltas(&mut self, num_deltas: u32) -> Result<(), PracticeDeltasError> {
self.trim_deltas_helper(num_deltas)
.map_err(PracticeDeltasError::TrimDeltas)
}
fn remove_deltas_with_prefix(&mut self, prefix: &str) -> Result<(), PracticeDeltasError> {
self.remove_deltas_with_prefix_helper(prefix)
.map_err(|e| PracticeDeltasError::RemovePrefix(prefix.to_string(), e))
}
}
#[cfg(test)]
#[cfg_attr(coverage, coverage(off))]
mod test {
use anyhow::{Ok, Result};
use rusqlite::Connection;
use ustr::Ustr;
use crate::{
data::ExerciseDelta,
practice_deltas::{LocalPracticeDeltas, PracticeDeltas},
};
fn delta(exercise_id: Ustr, value: f32, timestamp: i64) -> ExerciseDelta {
ExerciseDelta {
exercise_id,
delta: value,
timestamp,
}
}
fn new_test_deltas() -> Result<Box<dyn PracticeDeltas>> {
let practice_deltas = LocalPracticeDeltas::new(Connection::open_in_memory()?)?;
Ok(Box::new(practice_deltas))
}
fn assert_deltas(expected: &[f32], actual: &[ExerciseDelta]) {
let only_deltas: Vec<f32> = actual.iter().map(|t| t.delta).collect();
assert_eq!(expected, only_deltas);
let timestamp_sorted = actual.iter().enumerate().all(|(i, _)| {
if i == 0 {
return true;
}
actual[i - 1].timestamp >= actual[i].timestamp
});
assert!(timestamp_sorted);
}
#[test]
fn basic() -> Result<()> {
let mut deltas = new_test_deltas()?;
let exercise_id = Ustr::from("ex_123");
deltas.record_exercise_deltas(&[delta(exercise_id, 0.5, 1)])?;
let results = deltas.get_deltas(exercise_id, 1)?;
assert_deltas(&[0.5], &results);
Ok(())
}
#[test]
fn multiple_records() -> Result<()> {
let mut deltas = new_test_deltas()?;
let exercise_id = Ustr::from("ex_123");
deltas.record_exercise_deltas(&[
delta(exercise_id, 0.1, 1),
delta(exercise_id, 0.3, 2),
delta(exercise_id, 0.5, 3),
])?;
let one_delta = deltas.get_deltas(exercise_id, 1)?;
assert_deltas(&[0.5], &one_delta);
let three_deltas = deltas.get_deltas(exercise_id, 3)?;
assert_deltas(&[0.5, 0.3, 0.1], &three_deltas);
let more_deltas = deltas.get_deltas(exercise_id, 10)?;
assert_deltas(&[0.5, 0.3, 0.1], &more_deltas);
Ok(())
}
#[test]
fn no_records() -> Result<()> {
let deltas = new_test_deltas()?;
let results = deltas.get_deltas(Ustr::from("ex_123"), 10)?;
assert_deltas(&[], &results);
Ok(())
}
#[test]
fn trim_deltas_some_removed() -> Result<()> {
let mut deltas = new_test_deltas()?;
let exercise1_id = Ustr::from("exercise1");
deltas.record_exercise_deltas(&[
delta(exercise1_id, 0.1, 1),
delta(exercise1_id, 0.2, 2),
delta(exercise1_id, 0.3, 3),
])?;
let exercise2_id = Ustr::from("exercise2");
deltas.record_exercise_deltas(&[
delta(exercise2_id, -0.1, 1),
delta(exercise2_id, -0.2, 2),
delta(exercise2_id, -0.3, 3),
])?;
deltas.trim_deltas(2)?;
let results = deltas.get_deltas(exercise1_id, 10)?;
assert_deltas(&[0.3, 0.2], &results);
let results = deltas.get_deltas(exercise2_id, 10)?;
assert_deltas(&[-0.3, -0.2], &results);
Ok(())
}
#[test]
fn trim_deltas_none_removed() -> Result<()> {
let mut deltas = new_test_deltas()?;
let exercise1_id = Ustr::from("exercise1");
deltas.record_exercise_deltas(&[
delta(exercise1_id, 0.1, 1),
delta(exercise1_id, 0.2, 2),
delta(exercise1_id, 0.3, 3),
])?;
let exercise2_id = Ustr::from("exercise2");
deltas.record_exercise_deltas(&[
delta(exercise2_id, -0.1, 1),
delta(exercise2_id, -0.2, 2),
delta(exercise2_id, -0.3, 3),
])?;
deltas.trim_deltas(10)?;
let results = deltas.get_deltas(exercise1_id, 10)?;
assert_deltas(&[0.3, 0.2, 0.1], &results);
let results = deltas.get_deltas(exercise2_id, 10)?;
assert_deltas(&[-0.3, -0.2, -0.1], &results);
Ok(())
}
#[test]
fn remove_deltas_with_prefix() -> Result<()> {
let mut deltas = new_test_deltas()?;
let exercise1_id = Ustr::from("exercise1");
deltas.record_exercise_deltas(&[
delta(exercise1_id, 0.1, 1),
delta(exercise1_id, 0.2, 2),
delta(exercise1_id, 0.3, 3),
])?;
let exercise2_id = Ustr::from("exercise2");
deltas.record_exercise_deltas(&[
delta(exercise2_id, -0.1, 1),
delta(exercise2_id, -0.2, 2),
delta(exercise2_id, -0.3, 3),
])?;
let exercise3_id = Ustr::from("exercise3");
deltas.record_exercise_deltas(&[
delta(exercise3_id, 0.4, 1),
delta(exercise3_id, 0.5, 2),
delta(exercise3_id, 0.6, 3),
])?;
deltas.remove_deltas_with_prefix("exercise1")?;
let results = deltas.get_deltas(exercise1_id, 10)?;
assert_deltas(&[], &results);
let results = deltas.get_deltas(exercise2_id, 10)?;
assert_deltas(&[-0.3, -0.2, -0.1], &results);
let results = deltas.get_deltas(exercise3_id, 10)?;
assert_deltas(&[0.6, 0.5, 0.4], &results);
deltas.remove_deltas_with_prefix("exercise")?;
let results = deltas.get_deltas(exercise1_id, 10)?;
assert_deltas(&[], &results);
let results = deltas.get_deltas(exercise2_id, 10)?;
assert_deltas(&[], &results);
let results = deltas.get_deltas(exercise3_id, 10)?;
assert_deltas(&[], &results);
Ok(())
}
}