Fix Cards with Missing Last Review Time During Database Check

This commit is contained in:
Jarrett Ye 2025-07-30 12:18:49 +08:00
parent d13c117e80
commit d71a3c7413
No known key found for this signature in database
GPG key ID: EBFC55E0C1A352BB
6 changed files with 44 additions and 11 deletions

View file

@ -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.

View file

@ -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(())
}

View file

@ -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 {

View file

@ -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<TimestampSecs>,
pub(crate) last_reviewed_at: Option<TimestampSecs>,
}
/// Return a map of cards to info about last review/reschedule.
fn get_last_revlog_info(revlogs: &[RevlogEntry]) -> HashMap<CardId, LastRevlogInfo> {
pub(crate) fn get_last_revlog_info(revlogs: &[RevlogEntry]) -> HashMap<CardId, LastRevlogInfo> {
let mut out = HashMap::new();
revlogs
.iter()
@ -323,7 +323,7 @@ fn get_last_revlog_info(revlogs: &[RevlogEntry]) -> HashMap<CardId, LastRevlogIn
.for_each(|(card_id, group)| {
let mut last_reviewed_at = None;
for e in group.into_iter() {
if e.button_chosen >= 1 {
if e.has_rating_and_affect_scheduling() {
last_reviewed_at = Some(e.id.as_secs());
}
}

View file

@ -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

View file

@ -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<usize> {