// Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html use std::convert::TryFrom; use rusqlite::params; use rusqlite::types::FromSql; use rusqlite::types::FromSqlError; use rusqlite::types::ValueRef; use rusqlite::OptionalExtension; use rusqlite::Row; use super::SqliteStorage; use crate::error::Result; use crate::prelude::*; use crate::revlog::RevlogEntry; use crate::revlog::RevlogReviewKind; pub(crate) struct StudiedToday { pub cards: u32, pub seconds: f64, } impl FromSql for RevlogReviewKind { fn column_result(value: ValueRef<'_>) -> std::result::Result { if let ValueRef::Integer(i) = value { Ok(Self::try_from(i as u8).map_err(|_| FromSqlError::InvalidType)?) } else { Err(FromSqlError::InvalidType) } } } fn row_to_revlog_entry(row: &Row) -> Result { Ok(RevlogEntry { id: row.get(0)?, cid: row.get(1)?, usn: row.get(2)?, button_chosen: row.get(3)?, interval: row.get(4)?, last_interval: row.get(5)?, ease_factor: row.get(6)?, taken_millis: row.get(7).unwrap_or_default(), review_kind: row.get(8).unwrap_or_default(), }) } impl SqliteStorage { pub(crate) fn fix_revlog_properties(&self) -> Result { self.db .prepare(include_str!("fix_props.sql"))? .execute([]) .map_err(Into::into) } pub(crate) fn clear_pending_revlog_usns(&self) -> Result<()> { self.db .prepare("update revlog set usn = 0 where usn = -1")? .execute([])?; Ok(()) } /// Adds the entry, if its id is unique. If it is not, and `uniquify` is /// true, adds it with a new id. Returns the added id. /// (I.e., the option is safe to unwrap, if `uniquify` is true.) pub(crate) fn add_revlog_entry( &self, entry: &RevlogEntry, uniquify: bool, ) -> Result> { let added = self .db .prepare_cached(include_str!("add.sql"))? .execute(params![ uniquify, entry.id, entry.cid, entry.usn, entry.button_chosen, entry.interval, entry.last_interval, entry.ease_factor, entry.taken_millis, entry.review_kind as u8 ])?; Ok((added > 0).then(|| RevlogId(self.db.last_insert_rowid()))) } pub(crate) fn get_revlog_entry(&self, id: RevlogId) -> Result> { self.db .prepare_cached(concat!(include_str!("get.sql"), " where id=?"))? .query_and_then([id], row_to_revlog_entry)? .next() .transpose() } /// Determine the the last review time based on the revlog. pub(crate) fn time_of_last_review(&self, card_id: CardId) -> Result> { self.db .prepare_cached(include_str!("time_of_last_review.sql"))? .query_row([card_id], |row| row.get(0)) .optional() .map_err(Into::into) } /// Only intended to be used by the undo code, as Anki can not sync revlog /// deletions. pub(crate) fn remove_revlog_entry(&self, id: RevlogId) -> Result<()> { self.db .prepare_cached("delete from revlog where id = ?")? .execute([id])?; Ok(()) } pub(crate) fn get_revlog_entries_for_card(&self, cid: CardId) -> Result> { self.db .prepare_cached(concat!(include_str!("get.sql"), " where cid=?"))? .query_and_then([cid], row_to_revlog_entry)? .collect() } pub(crate) fn get_revlog_entries_for_searched_cards_after_stamp( &self, after: TimestampSecs, ) -> Result> { self.db .prepare_cached(concat!( include_str!("get.sql"), " where cid in (select cid from search_cids) and id >= ?" ))? .query_and_then([after.0 * 1000], row_to_revlog_entry)? .collect() } pub(crate) fn get_revlog_entries_for_searched_cards(&self) -> Result> { self.db .prepare_cached(concat!( include_str!("get.sql"), " where cid in (select cid from search_cids)" ))? .query_and_then([], row_to_revlog_entry)? .collect() } pub(crate) fn get_revlog_entries_for_searched_cards_in_card_order( &self, ) -> Result> { self.db .prepare_cached(concat!( include_str!("get.sql"), " where cid in (select cid from search_cids) order by cid, id" ))? .query_and_then([], row_to_revlog_entry)? .collect() } pub(crate) fn get_revlog_entries_for_export_dataset(&self) -> Result> { self.db .prepare_cached(concat!( include_str!("get.sql"), " where ease between 1 and 4", " order by cid, id" ))? .query_and_then([], row_to_revlog_entry)? .collect() } pub(crate) fn get_all_revlog_entries_in_card_order(&self) -> Result> { self.db .prepare_cached(concat!(include_str!("get.sql"), " order by cid, id"))? .query_and_then([], row_to_revlog_entry)? .collect() } pub(crate) fn get_all_revlog_entries(&self, after: TimestampSecs) -> Result> { self.db .prepare_cached(concat!(include_str!("get.sql"), " where id >= ?"))? .query_and_then([after.0 * 1000], row_to_revlog_entry)? .collect() } pub(crate) fn studied_today(&self, day_cutoff: TimestampSecs) -> Result { let start = day_cutoff.adding_secs(-86_400).as_millis(); self.db .prepare_cached(include_str!("studied_today.sql"))? .query_map( [ start.0, RevlogReviewKind::Manual as i64, RevlogReviewKind::Rescheduled as i64, ], |row| { Ok(StudiedToday { cards: row.get(0)?, seconds: row.get(1)?, }) }, )? .next() .unwrap() .map_err(Into::into) } pub(crate) fn upgrade_revlog_to_v2(&self) -> Result<()> { self.db .execute_batch(include_str!("v2_upgrade.sql")) .map_err(Into::into) } }