// Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html use crate::{ card::{Card, CardID, CardQueue, CardType}, decks::{Deck, DeckID}, err::Result, notes::NoteID, sched::congrats::CongratsInfo, timestamp::{TimestampMillis, TimestampSecs}, types::Usn, }; use rusqlite::params; use rusqlite::{ named_params, types::{FromSql, FromSqlError, ValueRef}, OptionalExtension, Row, NO_PARAMS, }; use std::{collections::HashSet, convert::TryFrom, result}; impl FromSql for CardType { 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) } } } impl FromSql for CardQueue { fn column_result(value: ValueRef<'_>) -> std::result::Result { if let ValueRef::Integer(i) = value { Ok(Self::try_from(i as i8).map_err(|_| FromSqlError::InvalidType)?) } else { Err(FromSqlError::InvalidType) } } } fn row_to_card(row: &Row) -> result::Result { Ok(Card { id: row.get(0)?, nid: row.get(1)?, did: row.get(2)?, ord: row.get(3)?, mtime: row.get(4)?, usn: row.get(5)?, ctype: row.get(6)?, queue: row.get(7)?, due: row.get(8).ok().unwrap_or_default(), ivl: row.get(9)?, factor: row.get(10)?, reps: row.get(11)?, lapses: row.get(12)?, left: row.get(13)?, odue: row.get(14).ok().unwrap_or_default(), odid: row.get(15)?, flags: row.get(16)?, data: row.get(17)?, }) } impl super::SqliteStorage { pub fn get_card(&self, cid: CardID) -> Result> { self.db .prepare_cached(concat!(include_str!("get_card.sql"), " where id = ?"))? .query_row(params![cid], row_to_card) .optional() .map_err(Into::into) } pub(crate) fn update_card(&self, card: &Card) -> Result<()> { let mut stmt = self.db.prepare_cached(include_str!("update_card.sql"))?; stmt.execute(params![ card.nid, card.did, card.ord, card.mtime, card.usn, card.ctype as u8, card.queue as i8, card.due, card.ivl, card.factor, card.reps, card.lapses, card.left, card.odue, card.odid, card.flags, card.data, card.id, ])?; Ok(()) } pub(crate) fn add_card(&self, card: &mut Card) -> Result<()> { let now = TimestampMillis::now().0; let mut stmt = self.db.prepare_cached(include_str!("add_card.sql"))?; stmt.execute(params![ now, card.nid, card.did, card.ord, card.mtime, card.usn, card.ctype as u8, card.queue as i8, card.due, card.ivl, card.factor, card.reps, card.lapses, card.left, card.odue, card.odid, card.flags, card.data, ])?; card.id = CardID(self.db.last_insert_rowid()); Ok(()) } /// Add or update card, using the provided ID. Used when syncing. pub(crate) fn add_or_update_card(&self, card: &Card) -> Result<()> { let mut stmt = self.db.prepare_cached(include_str!("add_or_update.sql"))?; stmt.execute(params![ card.id, card.nid, card.did, card.ord, card.mtime, card.usn, card.ctype as u8, card.queue as i8, card.due, card.ivl, card.factor, card.reps, card.lapses, card.left, card.odue, card.odid, card.flags, card.data, ])?; Ok(()) } pub(crate) fn remove_card(&self, cid: CardID) -> Result<()> { self.db .prepare_cached("delete from cards where id = ?")? .execute(&[cid])?; Ok(()) } /// Fix some invalid card properties, and return number of changed cards. pub(crate) fn fix_card_properties( &self, today: u32, mtime: TimestampSecs, usn: Usn, ) -> Result<(usize, usize)> { let new_cnt = self .db .prepare(include_str!("fix_due_new.sql"))? .execute(params![mtime, usn])?; let mut other_cnt = self .db .prepare(include_str!("fix_due_other.sql"))? .execute(params![today, mtime, usn])?; other_cnt += self .db .prepare(include_str!("fix_odue.sql"))? .execute(params![mtime, usn])?; other_cnt += self .db .prepare(include_str!("fix_ivl.sql"))? .execute(params![mtime, usn])?; Ok((new_cnt, other_cnt)) } pub(crate) fn delete_orphaned_cards(&self) -> Result { self.db .prepare("delete from cards where nid not in (select id from notes)")? .execute(NO_PARAMS) .map_err(Into::into) } pub(crate) fn all_filtered_cards_by_deck(&self) -> Result> { self.db .prepare("select id, did from cards where odid > 0")? .query_and_then(NO_PARAMS, |r| -> Result<_> { Ok((r.get(0)?, r.get(1)?)) })? .collect() } pub(crate) fn max_new_card_position(&self) -> Result { self.db .prepare("select max(due)+1 from cards where type=0")? .query_row(NO_PARAMS, |r| r.get(0)) .map_err(Into::into) } pub(crate) fn get_card_by_ordinal(&self, nid: NoteID, ord: u16) -> Result> { self.db .prepare_cached(concat!( include_str!("get_card.sql"), " where nid = ? and ord = ?" ))? .query_row(params![nid, ord], row_to_card) .optional() .map_err(Into::into) } pub(crate) fn clear_pending_card_usns(&self) -> Result<()> { self.db .prepare("update cards set usn = 0 where usn = -1")? .execute(NO_PARAMS)?; Ok(()) } pub(crate) fn have_at_least_one_card(&self) -> Result { self.db .prepare_cached("select null from cards")? .query(NO_PARAMS)? .next() .map(|o| o.is_none()) .map_err(Into::into) } pub(crate) fn all_cards_of_note(&self, nid: NoteID) -> Result> { self.db .prepare_cached(concat!(include_str!("get_card.sql"), " where nid = ?"))? .query_and_then(&[nid], |r| row_to_card(r).map_err(Into::into))? .collect() } pub(crate) fn all_card_ids_of_note(&self, nid: NoteID) -> Result> { self.db .prepare_cached("select id from cards where nid = ? order by ord")? .query_and_then(&[nid], |r| Ok(CardID(r.get(0)?)))? .collect() } pub(crate) fn note_ids_of_cards(&self, cids: &[CardID]) -> Result> { let mut stmt = self .db .prepare_cached("select nid from cards where id = ?")?; let mut nids = HashSet::new(); for cid in cids { if let Some(nid) = stmt .query_row(&[cid], |r| r.get::<_, NoteID>(0)) .optional()? { nids.insert(nid); } } Ok(nids) } pub(crate) fn all_searched_cards(&self) -> Result> { self.db .prepare_cached(concat!( include_str!("get_card.sql"), " where id in (select id from search_cids)" ))? .query_and_then(NO_PARAMS, |r| row_to_card(r).map_err(Into::into))? .collect() } pub(crate) fn for_each_card_in_search(&self, mut func: F) -> Result<()> where F: FnMut(Card) -> Result<()>, { let mut stmt = self.db.prepare_cached(concat!( include_str!("get_card.sql"), " where id in (select id from search_cids)" ))?; let mut rows = stmt.query(NO_PARAMS)?; while let Some(row) = rows.next()? { let card = row_to_card(row)?; func(card)? } Ok(()) } pub(crate) fn congrats_info(&self, current: &Deck, today: u32) -> Result { self.update_active_decks(current)?; self.db .prepare(include_str!("congrats.sql"))? .query_and_then_named( named_params! { ":review_queue": CardQueue::Review as i8, ":day_learn_queue": CardQueue::DayLearn as i8, ":new_queue": CardQueue::New as i8, ":user_buried_queue": CardQueue::UserBuried as i8, ":sched_buried_queue": CardQueue::SchedBuried as i8, ":learn_queue": CardQueue::Learn as i8, ":today": today, }, |row| { Ok(CongratsInfo { review_remaining: row.get::<_, u32>(0)? > 0, new_remaining: row.get::<_, u32>(1)? > 0, have_sched_buried: row.get::<_, u32>(2)? > 0, have_user_buried: row.get::<_, u32>(3)? > 0, learn_count: row.get(4)?, next_learn_due: row.get(5)?, }) }, )? .next() .unwrap() } } #[cfg(test)] mod test { use crate::{card::Card, i18n::I18n, log, storage::SqliteStorage}; use std::path::Path; #[test] fn add_card() { let i18n = I18n::new(&[""], "", log::terminal()); let storage = SqliteStorage::open_or_create(Path::new(":memory:"), &i18n, false).unwrap(); let mut card = Card::default(); storage.add_card(&mut card).unwrap(); let id1 = card.id; storage.add_card(&mut card).unwrap(); assert_ne!(id1, card.id); } }