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. [one] Fixed { $count } invalid card property.
*[other] Fixed { $count } invalid card properties. *[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 = database-check-missing-templates =
{ $count -> { $count ->
[one] Deleted { $count } card with missing template. [one] Deleted { $count } card with missing template.

View file

@ -40,6 +40,7 @@ pub struct CheckDatabaseOutput {
notetypes_recovered: usize, notetypes_recovered: usize,
invalid_utf8: usize, invalid_utf8: usize,
invalid_ids: usize, invalid_ids: usize,
card_last_review_time_empty: usize,
} }
#[derive(Debug, Clone, Copy, Default)] #[derive(Debug, Clone, Copy, Default)]
@ -69,6 +70,11 @@ impl CheckDatabaseOutput {
if self.card_properties_invalid > 0 { if self.card_properties_invalid > 0 {
probs.push(tr.database_check_card_properties(self.card_properties_invalid)); 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 { if self.cards_missing_note > 0 {
probs.push(tr.database_check_card_missing_note(self.cards_missing_note)); 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<()> { fn check_card_properties(&mut self, out: &mut CheckDatabaseOutput) -> Result<()> {
let timing = self.timing_today()?; 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, timing.days_elapsed,
TimestampSecs::now(), TimestampSecs::now(),
self.usn()?, self.usn()?,
@ -166,6 +172,7 @@ impl Collection {
)?; )?;
out.card_position_too_high = new_cnt; out.card_position_too_high = new_cnt;
out.card_properties_invalid += other_cnt; out.card_properties_invalid += other_cnt;
out.card_last_review_time_empty = last_review_time_cnt;
Ok(()) Ok(())
} }

View file

@ -84,6 +84,16 @@ impl RevlogEntry {
}) })
.unwrap() .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 { impl Collection {

View file

@ -306,15 +306,15 @@ pub(crate) fn fsrs_items_for_memory_states(
.collect() .collect()
} }
struct LastRevlogInfo { pub(crate) struct LastRevlogInfo {
/// Used to determine the actual elapsed time between the last time the user /// 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 /// reviewed the card and now, so that we can determine an accurate period
/// when the card has subsequently been rescheduled to a different day. /// 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. /// 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(); let mut out = HashMap::new();
revlogs revlogs
.iter() .iter()
@ -323,7 +323,7 @@ fn get_last_revlog_info(revlogs: &[RevlogEntry]) -> HashMap<CardId, LastRevlogIn
.for_each(|(card_id, group)| { .for_each(|(card_id, group)| {
let mut last_reviewed_at = None; let mut last_reviewed_at = None;
for e in group.into_iter() { 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()); last_reviewed_at = Some(e.id.as_secs());
} }
} }

View file

@ -53,10 +53,7 @@ impl GraphsContext {
self.revlog self.revlog
.iter() .iter()
.filter(|review| { .filter(|review| {
// not rescheduled/set due date/reset review.has_rating_and_affect_scheduling()
review.button_chosen > 0
// not cramming
&& (review.review_kind != RevlogReviewKind::Filtered || review.ease_factor != 0)
// cards with an interval ≥ 1 day // cards with an interval ≥ 1 day
&& (review.review_kind == RevlogReviewKind::Review && (review.review_kind == RevlogReviewKind::Review
|| review.last_interval <= -86400 || review.last_interval <= -86400

View file

@ -33,6 +33,7 @@ use crate::decks::DeckKind;
use crate::error::Result; use crate::error::Result;
use crate::notes::NoteId; use crate::notes::NoteId;
use crate::scheduler::congrats::CongratsInfo; use crate::scheduler::congrats::CongratsInfo;
use crate::scheduler::fsrs::memory_state::get_last_revlog_info;
use crate::scheduler::queue::BuryMode; use crate::scheduler::queue::BuryMode;
use crate::scheduler::queue::DueCard; use crate::scheduler::queue::DueCard;
use crate::scheduler::queue::DueCardKind; use crate::scheduler::queue::DueCardKind;
@ -365,7 +366,7 @@ impl super::SqliteStorage {
mtime: TimestampSecs, mtime: TimestampSecs,
usn: Usn, usn: Usn,
v1_sched: bool, v1_sched: bool,
) -> Result<(usize, usize)> { ) -> Result<(usize, usize, usize)> {
let new_cnt = self let new_cnt = self
.db .db
.prepare(include_str!("fix_due_new.sql"))? .prepare(include_str!("fix_due_new.sql"))?
@ -390,7 +391,20 @@ impl super::SqliteStorage {
.db .db
.prepare(include_str!("fix_ordinal.sql"))? .prepare(include_str!("fix_ordinal.sql"))?
.execute(params![mtime, usn])?; .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> { pub(crate) fn delete_orphaned_cards(&self) -> Result<usize> {