mirror of
https://github.com/ankitects/anki.git
synced 2025-12-31 07:43:02 -05:00
Closes #766 - changes the on-disk representation from % to a multiplier, eg 250 -> 2.5, as this is consistent with the other options - resets deck configs at or below 1.3 to 2.5 - for any cards that were using a reset deck config, reset their current factor if it's at or below 2.0x. The cutoff is arbitrary, and just intended to make sure we catch cards the user has rated Easy on multiple times. The existing due dates are left alone.
334 lines
10 KiB
Rust
334 lines
10 KiB
Rust
// 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},
|
|
deckconf::DeckConfID,
|
|
decks::{Deck, DeckID, DeckKind},
|
|
err::Result,
|
|
notes::NoteID,
|
|
timestamp::{TimestampMillis, TimestampSecs},
|
|
types::Usn,
|
|
};
|
|
use rusqlite::params;
|
|
use rusqlite::{
|
|
types::{FromSql, FromSqlError, ValueRef},
|
|
OptionalExtension, Row, NO_PARAMS,
|
|
};
|
|
use std::{collections::HashSet, convert::TryFrom, result};
|
|
|
|
use super::ids_to_string;
|
|
|
|
impl FromSql for CardType {
|
|
fn column_result(value: ValueRef<'_>) -> std::result::Result<Self, FromSqlError> {
|
|
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<Self, FromSqlError> {
|
|
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<Card, rusqlite::Error> {
|
|
Ok(Card {
|
|
id: row.get(0)?,
|
|
note_id: row.get(1)?,
|
|
deck_id: row.get(2)?,
|
|
template_idx: 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(),
|
|
interval: row.get(9)?,
|
|
ease_factor: row.get(10)?,
|
|
reps: row.get(11)?,
|
|
lapses: row.get(12)?,
|
|
remaining_steps: row.get(13)?,
|
|
original_due: row.get(14).ok().unwrap_or_default(),
|
|
original_deck_id: row.get(15)?,
|
|
flags: row.get(16)?,
|
|
data: row.get(17)?,
|
|
})
|
|
}
|
|
|
|
impl super::SqliteStorage {
|
|
pub fn get_card(&self, cid: CardID) -> Result<Option<Card>> {
|
|
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.note_id,
|
|
card.deck_id,
|
|
card.template_idx,
|
|
card.mtime,
|
|
card.usn,
|
|
card.ctype as u8,
|
|
card.queue as i8,
|
|
card.due,
|
|
card.interval,
|
|
card.ease_factor,
|
|
card.reps,
|
|
card.lapses,
|
|
card.remaining_steps,
|
|
card.original_due,
|
|
card.original_deck_id,
|
|
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.note_id,
|
|
card.deck_id,
|
|
card.template_idx,
|
|
card.mtime,
|
|
card.usn,
|
|
card.ctype as u8,
|
|
card.queue as i8,
|
|
card.due,
|
|
card.interval,
|
|
card.ease_factor,
|
|
card.reps,
|
|
card.lapses,
|
|
card.remaining_steps,
|
|
card.original_due,
|
|
card.original_deck_id,
|
|
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.note_id,
|
|
card.deck_id,
|
|
card.template_idx,
|
|
card.mtime,
|
|
card.usn,
|
|
card.ctype as u8,
|
|
card.queue as i8,
|
|
card.due,
|
|
card.interval,
|
|
card.ease_factor,
|
|
card.reps,
|
|
card.lapses,
|
|
card.remaining_steps,
|
|
card.original_due,
|
|
card.original_deck_id,
|
|
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<usize> {
|
|
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<Vec<(CardID, DeckID)>> {
|
|
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<u32> {
|
|
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<Option<Card>> {
|
|
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<bool> {
|
|
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<Vec<Card>> {
|
|
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 note_ids_of_cards(&self, cids: &[CardID]) -> Result<HashSet<NoteID>> {
|
|
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<Vec<Card>> {
|
|
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<F>(&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(())
|
|
}
|
|
|
|
|
|
/// Fix cards with low eases due to schema 15 bug.
|
|
/// Deck configs were defaulting to 2.5% ease, which was capped to
|
|
/// 130% when the deck options were edited for the first time.
|
|
pub(crate) fn fix_low_card_eases_for_configs(
|
|
&self,
|
|
configs: &[DeckConfID],
|
|
server: bool,
|
|
) -> Result<()> {
|
|
let mut affected_decks = vec![];
|
|
for conf in configs {
|
|
for (deck_id, _name) in self.get_all_deck_names()? {
|
|
if let Some(deck) = self.get_deck(deck_id)? {
|
|
if let DeckKind::Normal(normal) = &deck.kind {
|
|
if normal.config_id == conf.0 {
|
|
affected_decks.push(deck.id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut ids = String::new();
|
|
ids_to_string(&mut ids, &affected_decks);
|
|
let sql = include_str!("fix_low_ease.sql").replace("DECK_IDS", &ids);
|
|
|
|
self.db
|
|
.prepare(&sql)?
|
|
.execute(params![self.usn(server)?, TimestampSecs::now()])?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
}
|
|
|
|
#[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);
|
|
}
|
|
}
|