mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00

* Add card meta for persisting custom scheduling state * Rename meta -> custom_data * Enforce limits on size of custom data Large values will slow down table scans of the cards table, and it's easier to be strict now and possibly relax things in the future than the opposite. * Pack card states and customData into a single message + default customData to empty if it can't be parsed Co-authored-by: Damien Elmes <gpg@ankiweb.net>
772 lines
25 KiB
Rust
772 lines
25 KiB
Rust
// Copyright: Ankitects Pty Ltd and contributors
|
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
pub(crate) mod data;
|
|
pub(crate) mod filtered;
|
|
|
|
use std::{collections::HashSet, convert::TryFrom, fmt, result};
|
|
|
|
use rusqlite::{
|
|
named_params, params,
|
|
types::{FromSql, FromSqlError, ValueRef},
|
|
OptionalExtension, Row,
|
|
};
|
|
|
|
use self::data::CardData;
|
|
use super::ids_to_string;
|
|
use crate::{
|
|
card::{Card, CardId, CardQueue, CardType},
|
|
deckconfig::{DeckConfigId, ReviewCardOrder},
|
|
decks::{Deck, DeckId, DeckKind},
|
|
error::Result,
|
|
notes::NoteId,
|
|
scheduler::{
|
|
congrats::CongratsInfo,
|
|
queue::{DueCard, DueCardKind, NewCard},
|
|
},
|
|
timestamp::{TimestampMillis, TimestampSecs},
|
|
types::Usn,
|
|
};
|
|
|
|
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> {
|
|
let data: CardData = row.get(17)?;
|
|
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)?,
|
|
original_position: data.original_position,
|
|
custom_data: data.custom_data,
|
|
})
|
|
}
|
|
|
|
fn row_to_new_card(row: &Row) -> result::Result<NewCard, rusqlite::Error> {
|
|
Ok(NewCard {
|
|
id: row.get(0)?,
|
|
note_id: row.get(1)?,
|
|
template_index: row.get(2)?,
|
|
mtime: row.get(3)?,
|
|
current_deck_id: row.get(4)?,
|
|
original_deck_id: row.get(5)?,
|
|
hash: 0,
|
|
})
|
|
}
|
|
|
|
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,
|
|
CardData::from_card(card),
|
|
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,
|
|
CardData::from_card(card),
|
|
])?;
|
|
card.id = CardId(self.db.last_insert_rowid());
|
|
Ok(())
|
|
}
|
|
|
|
/// Add card if id is unique. True if card was added.
|
|
pub(crate) fn add_card_if_unique(&self, card: &Card) -> Result<bool> {
|
|
self.db
|
|
.prepare_cached(include_str!("add_card_if_unique.sql"))?
|
|
.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,
|
|
CardData::from_card(card),
|
|
])
|
|
.map(|n_rows| n_rows == 1)
|
|
.map_err(Into::into)
|
|
}
|
|
|
|
/// Add or update card, using the provided ID. Used for syncing & undoing.
|
|
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,
|
|
CardData::from_card(card),
|
|
])?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn remove_card(&self, cid: CardId) -> Result<()> {
|
|
self.db
|
|
.prepare_cached("delete from cards where id = ?")?
|
|
.execute([cid])?;
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn for_each_intraday_card_in_active_decks<F>(
|
|
&self,
|
|
learn_cutoff: TimestampSecs,
|
|
mut func: F,
|
|
) -> Result<()>
|
|
where
|
|
F: FnMut(DueCard),
|
|
{
|
|
let mut stmt = self.db.prepare_cached(include_str!("intraday_due.sql"))?;
|
|
let mut rows = stmt.query(params![learn_cutoff])?;
|
|
while let Some(row) = rows.next()? {
|
|
func(DueCard {
|
|
id: row.get(0)?,
|
|
note_id: row.get(1)?,
|
|
due: row.get(2).ok().unwrap_or_default(),
|
|
mtime: row.get(3)?,
|
|
current_deck_id: row.get(4)?,
|
|
original_deck_id: row.get(5)?,
|
|
kind: DueCardKind::Learning,
|
|
})
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Call func() for each review card or interday learning card, stopping
|
|
/// when it returns false or no more cards found.
|
|
pub(crate) fn for_each_due_card_in_active_decks<F>(
|
|
&self,
|
|
day_cutoff: u32,
|
|
order: ReviewCardOrder,
|
|
kind: DueCardKind,
|
|
mut func: F,
|
|
) -> Result<()>
|
|
where
|
|
F: FnMut(DueCard) -> bool,
|
|
{
|
|
let order_clause = review_order_sql(order, day_cutoff);
|
|
let mut stmt = self.db.prepare_cached(&format!(
|
|
"{} order by {}",
|
|
include_str!("due_cards.sql"),
|
|
order_clause
|
|
))?;
|
|
let queue = match kind {
|
|
DueCardKind::Review => CardQueue::Review,
|
|
DueCardKind::Learning => CardQueue::DayLearn,
|
|
};
|
|
let mut rows = stmt.query(params![queue as i8, day_cutoff])?;
|
|
while let Some(row) = rows.next()? {
|
|
if !func(DueCard {
|
|
id: row.get(0)?,
|
|
note_id: row.get(1)?,
|
|
due: row.get(2).ok().unwrap_or_default(),
|
|
mtime: row.get(4)?,
|
|
current_deck_id: row.get(5)?,
|
|
original_deck_id: row.get(6)?,
|
|
kind,
|
|
}) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Call func() for each new card in the provided deck, stopping when it
|
|
/// returns or no more cards found.
|
|
pub(crate) fn for_each_new_card_in_deck<F>(&self, deck: DeckId, mut func: F) -> Result<()>
|
|
where
|
|
F: FnMut(NewCard) -> bool,
|
|
{
|
|
let mut stmt = self.db.prepare_cached(&format!(
|
|
"{} ORDER BY due, ord ASC",
|
|
include_str!("new_cards.sql")
|
|
))?;
|
|
let mut rows = stmt.query(params![deck])?;
|
|
while let Some(row) = rows.next()? {
|
|
if !func(row_to_new_card(row)?) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Call func() for each new card in the active decks, stopping when it
|
|
/// returns false or no more cards found.
|
|
pub(crate) fn for_each_new_card_in_active_decks<F>(
|
|
&self,
|
|
order: NewCardSorting,
|
|
mut func: F,
|
|
) -> Result<()>
|
|
where
|
|
F: FnMut(NewCard) -> bool,
|
|
{
|
|
let mut stmt = self.db.prepare_cached(&format!(
|
|
"{} ORDER BY {}",
|
|
include_str!("active_new_cards.sql"),
|
|
order.write(),
|
|
))?;
|
|
let mut rows = stmt.query(params![])?;
|
|
while let Some(row) = rows.next()? {
|
|
if !func(row_to_new_card(row)?) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
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,
|
|
v1_sched: bool,
|
|
) -> 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, v1_sched])?;
|
|
other_cnt += self
|
|
.db
|
|
.prepare(include_str!("fix_ivl.sql"))?
|
|
.execute(params![mtime, usn])?;
|
|
other_cnt += self
|
|
.db
|
|
.prepare(include_str!("fix_ordinal.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([])
|
|
.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([], |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([], |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([])?;
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn have_at_least_one_card(&self) -> Result<bool> {
|
|
self.db
|
|
.prepare_cached("select null from cards")?
|
|
.query([])?
|
|
.next()
|
|
.map(|o| o.is_some())
|
|
.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 all_cards_of_notes_above_ordinal(
|
|
&mut self,
|
|
note_ids: &[NoteId],
|
|
ordinal: usize,
|
|
) -> Result<Vec<Card>> {
|
|
self.with_ids_in_searched_notes_table(note_ids, || {
|
|
self.db
|
|
.prepare_cached(concat!(
|
|
include_str!("get_card.sql"),
|
|
" where nid in (select nid from search_nids) and ord > ?"
|
|
))?
|
|
.query_and_then([ordinal as i64], |r| row_to_card(r).map_err(Into::into))?
|
|
.collect()
|
|
})
|
|
}
|
|
|
|
pub(crate) fn all_card_ids_of_note_in_template_order(
|
|
&self,
|
|
nid: NoteId,
|
|
) -> Result<Vec<CardId>> {
|
|
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 get_all_card_ids(&self) -> Result<HashSet<CardId>> {
|
|
self.db
|
|
.prepare("SELECT id FROM cards")?
|
|
.query_and_then([], |row| Ok(row.get(0)?))?
|
|
.collect()
|
|
}
|
|
|
|
pub(crate) fn all_cards_as_nid_and_ord(&self) -> Result<HashSet<(NoteId, u16)>> {
|
|
self.db
|
|
.prepare("SELECT nid, ord FROM cards")?
|
|
.query_and_then([], |r| Ok((NoteId(r.get(0)?), r.get(1)?)))?
|
|
.collect()
|
|
}
|
|
|
|
pub(crate) fn card_ids_of_notes(&self, nids: &[NoteId]) -> Result<Vec<CardId>> {
|
|
let mut stmt = self
|
|
.db
|
|
.prepare_cached("select id from cards where nid = ?")?;
|
|
let mut cids = vec![];
|
|
for nid in nids {
|
|
for cid in stmt.query_map([nid], |row| row.get(0))? {
|
|
cids.push(cid?);
|
|
}
|
|
}
|
|
Ok(cids)
|
|
}
|
|
|
|
pub(crate) fn all_siblings_for_bury(
|
|
&self,
|
|
cid: CardId,
|
|
nid: NoteId,
|
|
include_new: bool,
|
|
include_reviews: bool,
|
|
include_day_learn: bool,
|
|
) -> Result<Vec<Card>> {
|
|
let params = named_params! {
|
|
":card_id": cid,
|
|
":note_id": nid,
|
|
":include_new": include_new,
|
|
":include_reviews": include_reviews,
|
|
":include_day_learn": include_day_learn,
|
|
":new_queue": CardQueue::New as i8,
|
|
":review_queue": CardQueue::Review as i8,
|
|
":daylearn_queue": CardQueue::DayLearn as i8,
|
|
};
|
|
self.with_searched_cards_table(false, || {
|
|
self.db
|
|
.prepare_cached(include_str!("siblings_for_bury.sql"))?
|
|
.execute(params)?;
|
|
self.all_searched_cards()
|
|
})
|
|
}
|
|
|
|
pub(crate) fn with_searched_cards_table<T>(
|
|
&self,
|
|
preserve_order: bool,
|
|
func: impl FnOnce() -> Result<T>,
|
|
) -> Result<T> {
|
|
if preserve_order {
|
|
self.setup_searched_cards_table_to_preserve_order()?;
|
|
} else {
|
|
self.setup_searched_cards_table()?;
|
|
}
|
|
let result = func();
|
|
self.clear_searched_cards_table()?;
|
|
result
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
/// Place the ids of cards with notes in 'search_nids' into 'search_cids'.
|
|
/// Returns number of added cards.
|
|
pub(crate) fn search_cards_of_notes_into_table(&self) -> Result<usize> {
|
|
self.db
|
|
.prepare(include_str!("search_cards_of_notes_into_table.sql"))?
|
|
.execute([])
|
|
.map_err(Into::into)
|
|
}
|
|
|
|
pub(crate) fn all_searched_cards(&self) -> Result<Vec<Card>> {
|
|
self.db
|
|
.prepare_cached(concat!(
|
|
include_str!("get_card.sql"),
|
|
" where id in (select cid from search_cids)"
|
|
))?
|
|
.query_and_then([], |r| row_to_card(r).map_err(Into::into))?
|
|
.collect()
|
|
}
|
|
|
|
pub(crate) fn all_searched_cards_in_search_order(&self) -> Result<Vec<Card>> {
|
|
self.db
|
|
.prepare_cached(concat!(
|
|
include_str!("get_card.sql"),
|
|
", search_cids where cards.id = search_cids.cid order by search_cids.rowid"
|
|
))?
|
|
.query_and_then([], |r| row_to_card(r).map_err(Into::into))?
|
|
.collect()
|
|
}
|
|
|
|
/// Cards will arrive in card id order, not search order.
|
|
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 cid from search_cids)"
|
|
))?;
|
|
let mut rows = stmt.query([])?;
|
|
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<CongratsInfo> {
|
|
// FIXME: when v1/v2 are dropped, this line will become obsolete, as it's run
|
|
// on queue build by v3
|
|
self.update_active_decks(current)?;
|
|
self.db
|
|
.prepare(include_str!("congrats.sql"))?
|
|
.query_and_then(
|
|
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()
|
|
}
|
|
|
|
pub(crate) fn all_cards_at_or_above_position(&self, start: u32) -> Result<Vec<Card>> {
|
|
self.with_searched_cards_table(false, || {
|
|
self.db
|
|
.prepare(include_str!("at_or_above_position.sql"))?
|
|
.execute([start, CardType::New as u32])?;
|
|
self.all_searched_cards()
|
|
})
|
|
}
|
|
|
|
pub(crate) fn setup_searched_cards_table(&self) -> Result<()> {
|
|
self.db
|
|
.execute_batch(include_str!("search_cids_setup.sql"))?;
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn setup_searched_cards_table_to_preserve_order(&self) -> Result<()> {
|
|
self.db
|
|
.execute_batch(include_str!("search_cids_setup_ordered.sql"))?;
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn clear_searched_cards_table(&self) -> Result<()> {
|
|
self.db.execute("drop table if exists search_cids", [])?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Injects the provided card IDs into the search_cids table, for
|
|
/// when ids have arrived outside of a search.
|
|
pub(crate) fn set_search_table_to_card_ids(&self, cards: &[CardId]) -> Result<()> {
|
|
let mut stmt = self
|
|
.db
|
|
.prepare_cached("insert into search_cids values (?)")?;
|
|
for cid in cards {
|
|
stmt.execute([cid])?;
|
|
}
|
|
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: &[DeckConfigId],
|
|
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)?])?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(crate) fn get_all_cards(&self) -> Vec<Card> {
|
|
self.db
|
|
.prepare("SELECT * FROM cards")
|
|
.unwrap()
|
|
.query_and_then([], row_to_card)
|
|
.unwrap()
|
|
.collect::<rusqlite::Result<_>>()
|
|
.unwrap()
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
enum ReviewOrderSubclause {
|
|
Day,
|
|
Deck,
|
|
Random,
|
|
IntervalsAscending,
|
|
IntervalsDescending,
|
|
EaseAscending,
|
|
EaseDescending,
|
|
RelativeOverdueness { today: u32 },
|
|
}
|
|
|
|
impl fmt::Display for ReviewOrderSubclause {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
let temp_string;
|
|
let clause = match self {
|
|
ReviewOrderSubclause::Day => "due",
|
|
ReviewOrderSubclause::Deck => "(select rowid from active_decks ad where ad.id = did)",
|
|
ReviewOrderSubclause::Random => "fnvhash(id, mod)",
|
|
ReviewOrderSubclause::IntervalsAscending => "ivl asc",
|
|
ReviewOrderSubclause::IntervalsDescending => "ivl desc",
|
|
ReviewOrderSubclause::EaseAscending => "factor asc",
|
|
ReviewOrderSubclause::EaseDescending => "factor desc",
|
|
ReviewOrderSubclause::RelativeOverdueness { today } => {
|
|
temp_string = format!("ivl / cast({today}-due+0.001 as real)", today = today);
|
|
&temp_string
|
|
}
|
|
};
|
|
write!(f, "{}", clause)
|
|
}
|
|
}
|
|
|
|
fn review_order_sql(order: ReviewCardOrder, today: u32) -> String {
|
|
let mut subclauses = match order {
|
|
ReviewCardOrder::Day => vec![ReviewOrderSubclause::Day],
|
|
ReviewCardOrder::DayThenDeck => vec![ReviewOrderSubclause::Day, ReviewOrderSubclause::Deck],
|
|
ReviewCardOrder::DeckThenDay => vec![ReviewOrderSubclause::Deck, ReviewOrderSubclause::Day],
|
|
ReviewCardOrder::IntervalsAscending => vec![ReviewOrderSubclause::IntervalsAscending],
|
|
ReviewCardOrder::IntervalsDescending => vec![ReviewOrderSubclause::IntervalsDescending],
|
|
ReviewCardOrder::EaseAscending => vec![ReviewOrderSubclause::EaseAscending],
|
|
ReviewCardOrder::EaseDescending => vec![ReviewOrderSubclause::EaseDescending],
|
|
ReviewCardOrder::RelativeOverdueness => {
|
|
vec![ReviewOrderSubclause::RelativeOverdueness { today }]
|
|
}
|
|
};
|
|
subclauses.push(ReviewOrderSubclause::Random);
|
|
|
|
let v: Vec<_> = subclauses
|
|
.iter()
|
|
.map(ReviewOrderSubclause::to_string)
|
|
.collect();
|
|
v.join(", ")
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub(crate) enum NewCardSorting {
|
|
/// Ascending position, consecutive siblings,
|
|
/// provided they have the same position.
|
|
LowestPosition,
|
|
/// Descending position, consecutive siblings,
|
|
/// provided they have the same position.
|
|
HighestPosition,
|
|
/// Random, but with consecutive siblings.
|
|
/// For some given salt the order is stable.
|
|
RandomNotes(u32),
|
|
/// Fully random.
|
|
/// For some given salt the order is stable.
|
|
RandomCards(u32),
|
|
}
|
|
|
|
impl NewCardSorting {
|
|
fn write(self) -> String {
|
|
match self {
|
|
NewCardSorting::LowestPosition => "due ASC, ord ASC".to_string(),
|
|
NewCardSorting::HighestPosition => "due DESC, ord ASC".to_string(),
|
|
NewCardSorting::RandomNotes(salt) => format!("fnvhash(nid, {salt}), ord ASC"),
|
|
NewCardSorting::RandomCards(salt) => format!("fnvhash(id, {salt})"),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use std::path::Path;
|
|
|
|
use crate::{card::Card, i18n::I18n, storage::SqliteStorage};
|
|
|
|
#[test]
|
|
fn add_card() {
|
|
let tr = I18n::template_only();
|
|
let storage = SqliteStorage::open_or_create(Path::new(":memory:"), &tr, 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);
|
|
}
|
|
}
|