From d71a3c7413708d16cf5562e78fd80af9fcc25316 Mon Sep 17 00:00:00 2001 From: Jarrett Ye Date: Wed, 30 Jul 2025 12:18:49 +0800 Subject: [PATCH] Fix Cards with Missing Last Review Time During Database Check --- ftl/core/database-check.ftl | 5 +++++ rslib/src/dbcheck.rs | 9 ++++++++- rslib/src/revlog/mod.rs | 10 ++++++++++ rslib/src/scheduler/fsrs/memory_state.rs | 8 ++++---- rslib/src/stats/graphs/retention.rs | 5 +---- rslib/src/storage/card/mod.rs | 18 ++++++++++++++++-- 6 files changed, 44 insertions(+), 11 deletions(-) diff --git a/ftl/core/database-check.ftl b/ftl/core/database-check.ftl index 8a9e4e178..b4a12724d 100644 --- a/ftl/core/database-check.ftl +++ b/ftl/core/database-check.ftl @@ -5,6 +5,11 @@ database-check-card-properties = [one] Fixed { $count } invalid card property. *[other] Fixed { $count } invalid card properties. } +database-check-card-last-review-time-empty = + { $count -> + [one] Fixed { $count } card with no last review time. + *[other] Fixed { $count } cards with no last review time. + } database-check-missing-templates = { $count -> [one] Deleted { $count } card with missing template. diff --git a/rslib/src/dbcheck.rs b/rslib/src/dbcheck.rs index f58a2184a..2f4d13760 100644 --- a/rslib/src/dbcheck.rs +++ b/rslib/src/dbcheck.rs @@ -40,6 +40,7 @@ pub struct CheckDatabaseOutput { notetypes_recovered: usize, invalid_utf8: usize, invalid_ids: usize, + card_last_review_time_empty: usize, } #[derive(Debug, Clone, Copy, Default)] @@ -69,6 +70,11 @@ impl CheckDatabaseOutput { if self.card_properties_invalid > 0 { probs.push(tr.database_check_card_properties(self.card_properties_invalid)); } + if self.card_last_review_time_empty > 0 { + probs.push( + tr.database_check_card_last_review_time_empty(self.card_last_review_time_empty), + ); + } if self.cards_missing_note > 0 { probs.push(tr.database_check_card_missing_note(self.cards_missing_note)); } @@ -158,7 +164,7 @@ impl Collection { fn check_card_properties(&mut self, out: &mut CheckDatabaseOutput) -> Result<()> { let timing = self.timing_today()?; - let (new_cnt, other_cnt) = self.storage.fix_card_properties( + let (new_cnt, other_cnt, last_review_time_cnt) = self.storage.fix_card_properties( timing.days_elapsed, TimestampSecs::now(), self.usn()?, @@ -166,6 +172,7 @@ impl Collection { )?; out.card_position_too_high = new_cnt; out.card_properties_invalid += other_cnt; + out.card_last_review_time_empty = last_review_time_cnt; Ok(()) } diff --git a/rslib/src/revlog/mod.rs b/rslib/src/revlog/mod.rs index ad7f30261..7a187ae95 100644 --- a/rslib/src/revlog/mod.rs +++ b/rslib/src/revlog/mod.rs @@ -84,6 +84,16 @@ impl RevlogEntry { }) .unwrap() } + + /// Returns true if the review entry is not manually rescheduled and not + /// cramming. Used to filter out entries that shouldn't be considered + /// for statistics and scheduling. + pub(crate) fn has_rating_and_affect_scheduling(&self) -> bool { + // not rescheduled/set due date/reset + self.button_chosen > 0 + // not cramming + && (self.review_kind != RevlogReviewKind::Filtered || self.ease_factor != 0) + } } impl Collection { diff --git a/rslib/src/scheduler/fsrs/memory_state.rs b/rslib/src/scheduler/fsrs/memory_state.rs index 199b19329..b9d718994 100644 --- a/rslib/src/scheduler/fsrs/memory_state.rs +++ b/rslib/src/scheduler/fsrs/memory_state.rs @@ -306,15 +306,15 @@ pub(crate) fn fsrs_items_for_memory_states( .collect() } -struct LastRevlogInfo { +pub(crate) struct LastRevlogInfo { /// Used to determine the actual elapsed time between the last time the user /// reviewed the card and now, so that we can determine an accurate period /// when the card has subsequently been rescheduled to a different day. - last_reviewed_at: Option, + pub(crate) last_reviewed_at: Option, } /// Return a map of cards to info about last review/reschedule. -fn get_last_revlog_info(revlogs: &[RevlogEntry]) -> HashMap { +pub(crate) fn get_last_revlog_info(revlogs: &[RevlogEntry]) -> HashMap { let mut out = HashMap::new(); revlogs .iter() @@ -323,7 +323,7 @@ fn get_last_revlog_info(revlogs: &[RevlogEntry]) -> HashMap= 1 { + if e.has_rating_and_affect_scheduling() { last_reviewed_at = Some(e.id.as_secs()); } } diff --git a/rslib/src/stats/graphs/retention.rs b/rslib/src/stats/graphs/retention.rs index c21f43301..204733fe5 100644 --- a/rslib/src/stats/graphs/retention.rs +++ b/rslib/src/stats/graphs/retention.rs @@ -53,10 +53,7 @@ impl GraphsContext { self.revlog .iter() .filter(|review| { - // not rescheduled/set due date/reset - review.button_chosen > 0 - // not cramming - && (review.review_kind != RevlogReviewKind::Filtered || review.ease_factor != 0) + review.has_rating_and_affect_scheduling() // cards with an interval ≥ 1 day && (review.review_kind == RevlogReviewKind::Review || review.last_interval <= -86400 diff --git a/rslib/src/storage/card/mod.rs b/rslib/src/storage/card/mod.rs index a1db247c3..678009304 100644 --- a/rslib/src/storage/card/mod.rs +++ b/rslib/src/storage/card/mod.rs @@ -33,6 +33,7 @@ use crate::decks::DeckKind; use crate::error::Result; use crate::notes::NoteId; use crate::scheduler::congrats::CongratsInfo; +use crate::scheduler::fsrs::memory_state::get_last_revlog_info; use crate::scheduler::queue::BuryMode; use crate::scheduler::queue::DueCard; use crate::scheduler::queue::DueCardKind; @@ -365,7 +366,7 @@ impl super::SqliteStorage { mtime: TimestampSecs, usn: Usn, v1_sched: bool, - ) -> Result<(usize, usize)> { + ) -> Result<(usize, usize, usize)> { let new_cnt = self .db .prepare(include_str!("fix_due_new.sql"))? @@ -390,7 +391,20 @@ impl super::SqliteStorage { .db .prepare(include_str!("fix_ordinal.sql"))? .execute(params![mtime, usn])?; - Ok((new_cnt, other_cnt)) + let mut last_review_time_cnt = 0; + let revlog = self.get_all_revlog_entries_in_card_order()?; + let last_revlog_info = get_last_revlog_info(&revlog); + for (card_id, last_revlog_info) in last_revlog_info { + let card = self.get_card(card_id)?; + if let Some(mut card) = card { + if card.ctype != CardType::New && card.last_review_time.is_none() { + card.last_review_time = last_revlog_info.last_reviewed_at; + self.update_card(&mut card)?; + last_review_time_cnt += 1; + } + } + } + Ok((new_cnt, other_cnt, last_review_time_cnt)) } pub(crate) fn delete_orphaned_cards(&self) -> Result {