2020-05-03 04:24:18 +02:00
|
|
|
// Copyright: Ankitects Pty Ltd and contributors
|
|
|
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
|
2021-04-18 10:29:20 +02:00
|
|
|
use std::{collections::HashSet, sync::Arc};
|
|
|
|
|
|
|
|
use itertools::Itertools;
|
|
|
|
use slog::debug;
|
|
|
|
|
2020-05-10 05:50:04 +02:00
|
|
|
use crate::{
|
|
|
|
collection::Collection,
|
2021-03-19 13:57:43 +01:00
|
|
|
config::SchedulerVersion,
|
2021-04-01 09:34:03 +02:00
|
|
|
error::{AnkiError, DbError, DbErrorKind, Result},
|
2021-03-27 01:39:53 +01:00
|
|
|
i18n::I18n,
|
2020-05-10 05:50:04 +02:00
|
|
|
notetype::{
|
2021-03-27 13:03:19 +01:00
|
|
|
all_stock_notetypes, AlreadyGeneratedCardInfo, CardGenContext, Notetype, NotetypeId,
|
|
|
|
NotetypeKind,
|
2020-05-10 05:50:04 +02:00
|
|
|
},
|
2020-11-06 01:21:51 +01:00
|
|
|
prelude::*,
|
2020-05-10 05:50:04 +02:00
|
|
|
timestamp::{TimestampMillis, TimestampSecs},
|
|
|
|
};
|
2020-05-03 04:24:18 +02:00
|
|
|
|
2020-05-10 10:09:18 +02:00
|
|
|
#[derive(Debug, Default, PartialEq)]
|
|
|
|
pub struct CheckDatabaseOutput {
|
|
|
|
card_properties_invalid: usize,
|
|
|
|
card_position_too_high: usize,
|
|
|
|
cards_missing_note: usize,
|
2020-05-10 11:51:18 +02:00
|
|
|
decks_missing: usize,
|
2020-05-10 10:09:18 +02:00
|
|
|
revlog_properties_invalid: usize,
|
|
|
|
templates_missing: usize,
|
|
|
|
card_ords_duplicated: usize,
|
|
|
|
field_count_mismatch: usize,
|
2020-08-10 05:42:37 +02:00
|
|
|
notetypes_recovered: usize,
|
2020-11-06 01:21:51 +01:00
|
|
|
invalid_utf8: usize,
|
2020-05-10 10:09:18 +02:00
|
|
|
}
|
|
|
|
|
2020-06-08 12:28:11 +02:00
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
|
|
pub(crate) enum DatabaseCheckProgress {
|
|
|
|
Integrity,
|
|
|
|
Optimize,
|
|
|
|
Cards,
|
|
|
|
Notes { current: u32, total: u32 },
|
|
|
|
History,
|
|
|
|
}
|
|
|
|
|
2020-05-10 10:09:18 +02:00
|
|
|
impl CheckDatabaseOutput {
|
2021-03-27 03:09:51 +01:00
|
|
|
pub fn to_i18n_strings(&self, tr: &I18n) -> Vec<String> {
|
2020-05-10 10:09:18 +02:00
|
|
|
let mut probs = Vec::new();
|
|
|
|
|
2020-08-10 05:42:37 +02:00
|
|
|
if self.notetypes_recovered > 0 {
|
2021-03-27 03:09:51 +01:00
|
|
|
probs.push(tr.database_check_notetypes_recovered());
|
2020-08-10 05:42:37 +02:00
|
|
|
}
|
|
|
|
|
2020-05-10 10:09:18 +02:00
|
|
|
if self.card_position_too_high > 0 {
|
2021-03-27 03:09:51 +01:00
|
|
|
probs.push(tr.database_check_new_card_high_due(self.card_position_too_high));
|
2020-05-10 10:09:18 +02:00
|
|
|
}
|
|
|
|
if self.card_properties_invalid > 0 {
|
2021-03-27 03:09:51 +01:00
|
|
|
probs.push(tr.database_check_card_properties(self.card_properties_invalid));
|
2020-05-10 10:09:18 +02:00
|
|
|
}
|
|
|
|
if self.cards_missing_note > 0 {
|
2021-03-27 03:09:51 +01:00
|
|
|
probs.push(tr.database_check_card_missing_note(self.cards_missing_note));
|
2020-05-10 10:09:18 +02:00
|
|
|
}
|
2020-05-10 11:51:18 +02:00
|
|
|
if self.decks_missing > 0 {
|
2021-03-27 03:09:51 +01:00
|
|
|
probs.push(tr.database_check_missing_decks(self.decks_missing));
|
2020-05-10 10:09:18 +02:00
|
|
|
}
|
|
|
|
if self.field_count_mismatch > 0 {
|
2021-03-27 03:09:51 +01:00
|
|
|
probs.push(tr.database_check_field_count(self.field_count_mismatch));
|
2020-05-10 10:09:18 +02:00
|
|
|
}
|
|
|
|
if self.card_ords_duplicated > 0 {
|
2021-03-27 03:09:51 +01:00
|
|
|
probs.push(tr.database_check_duplicate_card_ords(self.card_ords_duplicated));
|
2020-05-10 10:09:18 +02:00
|
|
|
}
|
|
|
|
if self.templates_missing > 0 {
|
2021-03-27 03:09:51 +01:00
|
|
|
probs.push(tr.database_check_missing_templates(self.templates_missing));
|
2020-05-10 10:09:18 +02:00
|
|
|
}
|
|
|
|
if self.revlog_properties_invalid > 0 {
|
2021-03-27 03:09:51 +01:00
|
|
|
probs.push(tr.database_check_revlog_properties(self.revlog_properties_invalid));
|
2020-05-10 10:09:18 +02:00
|
|
|
}
|
2020-11-06 01:21:51 +01:00
|
|
|
if self.invalid_utf8 > 0 {
|
2021-03-27 03:09:51 +01:00
|
|
|
probs.push(tr.database_check_notes_with_invalid_utf8(self.invalid_utf8));
|
2020-11-06 01:21:51 +01:00
|
|
|
}
|
2020-05-10 10:09:18 +02:00
|
|
|
|
2021-03-27 01:39:53 +01:00
|
|
|
probs.into_iter().map(Into::into).collect()
|
2020-05-10 10:09:18 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-03 04:24:18 +02:00
|
|
|
impl Collection {
|
2020-05-10 05:50:04 +02:00
|
|
|
/// Check the database, returning a list of problems that were fixed.
|
2020-06-08 12:28:11 +02:00
|
|
|
pub(crate) fn check_database<F>(&mut self, mut progress_fn: F) -> Result<CheckDatabaseOutput>
|
|
|
|
where
|
|
|
|
F: FnMut(DatabaseCheckProgress, bool),
|
|
|
|
{
|
|
|
|
progress_fn(DatabaseCheckProgress::Integrity, false);
|
2020-05-10 05:50:04 +02:00
|
|
|
debug!(self.log, "quick check");
|
|
|
|
if self.storage.quick_check_corrupt() {
|
|
|
|
debug!(self.log, "quick check failed");
|
2021-04-01 09:34:03 +02:00
|
|
|
return Err(AnkiError::db_error(
|
|
|
|
self.tr.database_check_corrupt(),
|
|
|
|
DbErrorKind::Corrupt,
|
|
|
|
));
|
2020-05-10 05:50:04 +02:00
|
|
|
}
|
|
|
|
|
2020-06-08 12:28:11 +02:00
|
|
|
progress_fn(DatabaseCheckProgress::Optimize, false);
|
2020-05-10 05:50:04 +02:00
|
|
|
debug!(self.log, "optimize");
|
|
|
|
self.storage.optimize()?;
|
|
|
|
|
undoable ops now return changes directly; add new *_ops.py files
- Introduced a new transact() method that wraps the return value
in a separate struct that describes the changes that were made.
- Changes are now gathered from the undo log, so we don't need to
guess at what was changed - eg if update_note() is called with identical
note contents, no changes are returned. Card changes will only be set
if cards were actually generated by the update_note() call, and tag
will only be set if a new tag was added.
- mw.perform_op() has been updated to expect the op to return the changes,
or a structure with the changes in it, and it will use them to fire the
change hook, instead of fetching the changes from undo_status(), so there
is no risk of race conditions.
- the various calls to mw.perform_op() have been split into separate
files like card_ops.py. Aside from making the code cleaner, this works
around a rather annoying issue with mypy. Because we run it with
no_strict_optional, mypy is happy to accept an operation that returns None,
despite the type signature saying it requires changes to be returned.
Turning no_strict_optional on for the whole codebase is not practical
at the moment, but we can enable it for individual files.
Still todo:
- The cursor keeps moving back to the start of a field when typing -
we need to ignore the refresh hook when we are the initiator.
- The busy cursor icon should probably be delayed a few hundreds ms.
- Still need to think about a nicer way of handling saveNow()
- op_made_changes(), op_affects_study_queue() might be better embedded
as properties in the object instead
2021-03-16 05:26:42 +01:00
|
|
|
self.transact_no_undo(|col| col.check_database_inner(progress_fn))
|
2020-05-10 05:50:04 +02:00
|
|
|
}
|
|
|
|
|
2020-06-08 12:28:11 +02:00
|
|
|
fn check_database_inner<F>(&mut self, mut progress_fn: F) -> Result<CheckDatabaseOutput>
|
|
|
|
where
|
|
|
|
F: FnMut(DatabaseCheckProgress, bool),
|
|
|
|
{
|
2020-05-10 10:09:18 +02:00
|
|
|
let mut out = CheckDatabaseOutput::default();
|
2020-05-10 05:50:04 +02:00
|
|
|
|
|
|
|
// cards first, as we need to be able to read them to process notes
|
2020-06-08 12:28:11 +02:00
|
|
|
progress_fn(DatabaseCheckProgress::Cards, false);
|
2020-05-10 05:50:04 +02:00
|
|
|
debug!(self.log, "check cards");
|
2020-05-10 10:09:18 +02:00
|
|
|
self.check_card_properties(&mut out)?;
|
|
|
|
self.check_orphaned_cards(&mut out)?;
|
2020-05-10 05:50:04 +02:00
|
|
|
|
|
|
|
debug!(self.log, "check decks");
|
2020-05-10 10:09:18 +02:00
|
|
|
self.check_missing_deck_ids(&mut out)?;
|
|
|
|
self.check_filtered_cards(&mut out)?;
|
2020-05-10 05:50:04 +02:00
|
|
|
|
|
|
|
debug!(self.log, "check notetypes");
|
2020-06-08 12:28:11 +02:00
|
|
|
self.check_notetypes(&mut out, &mut progress_fn)?;
|
|
|
|
|
|
|
|
progress_fn(DatabaseCheckProgress::History, false);
|
2020-05-10 05:50:04 +02:00
|
|
|
|
|
|
|
debug!(self.log, "check review log");
|
2020-05-10 10:09:18 +02:00
|
|
|
self.check_revlog(&mut out)?;
|
2020-05-10 05:50:04 +02:00
|
|
|
|
|
|
|
debug!(self.log, "missing decks");
|
2020-05-10 11:51:18 +02:00
|
|
|
self.check_missing_deck_names(&mut out)?;
|
2020-05-10 05:50:04 +02:00
|
|
|
|
|
|
|
self.update_next_new_position()?;
|
|
|
|
|
2020-05-10 10:09:18 +02:00
|
|
|
debug!(self.log, "db check finished: {:#?}", out);
|
2020-05-10 05:50:04 +02:00
|
|
|
|
2020-05-10 10:09:18 +02:00
|
|
|
Ok(out)
|
2020-05-10 05:50:04 +02:00
|
|
|
}
|
|
|
|
|
2020-05-10 10:09:18 +02:00
|
|
|
fn check_card_properties(&mut self, out: &mut CheckDatabaseOutput) -> Result<()> {
|
2020-05-10 05:50:04 +02:00
|
|
|
let timing = self.timing_today()?;
|
|
|
|
let (new_cnt, other_cnt) = self.storage.fix_card_properties(
|
|
|
|
timing.days_elapsed,
|
|
|
|
TimestampSecs::now(),
|
|
|
|
self.usn()?,
|
2021-03-19 13:57:43 +01:00
|
|
|
self.scheduler_version() == SchedulerVersion::V1,
|
2020-05-10 05:50:04 +02:00
|
|
|
)?;
|
2020-05-10 10:09:18 +02:00
|
|
|
out.card_position_too_high = new_cnt;
|
|
|
|
out.card_properties_invalid += other_cnt;
|
2020-05-10 05:50:04 +02:00
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2020-05-10 10:09:18 +02:00
|
|
|
fn check_orphaned_cards(&mut self, out: &mut CheckDatabaseOutput) -> Result<()> {
|
|
|
|
let cnt = self.storage.delete_orphaned_cards()?;
|
|
|
|
if cnt > 0 {
|
2021-04-18 09:30:02 +02:00
|
|
|
self.set_schema_modified()?;
|
2020-05-10 10:09:18 +02:00
|
|
|
out.cards_missing_note = cnt;
|
2020-05-10 05:50:04 +02:00
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2020-05-10 10:09:18 +02:00
|
|
|
fn check_missing_deck_ids(&mut self, out: &mut CheckDatabaseOutput) -> Result<()> {
|
2020-05-17 11:07:15 +02:00
|
|
|
let usn = self.usn()?;
|
2020-05-10 05:50:04 +02:00
|
|
|
for did in self.storage.missing_decks()? {
|
2020-05-17 11:07:15 +02:00
|
|
|
self.recover_missing_deck(did, usn)?;
|
2020-05-10 11:51:18 +02:00
|
|
|
out.decks_missing += 1;
|
2020-05-10 05:50:04 +02:00
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2020-05-10 10:09:18 +02:00
|
|
|
fn check_filtered_cards(&mut self, out: &mut CheckDatabaseOutput) -> Result<()> {
|
2021-02-21 06:50:41 +01:00
|
|
|
let decks = self.storage.get_decks_map()?;
|
2020-05-10 05:50:04 +02:00
|
|
|
|
|
|
|
let mut wrong = 0;
|
|
|
|
for (cid, did) in self.storage.all_filtered_cards_by_deck()? {
|
|
|
|
// we expect calling code to ensure all decks already exist
|
|
|
|
if let Some(deck) = decks.get(&did) {
|
|
|
|
if !deck.is_filtered() {
|
|
|
|
let mut card = self.storage.get_card(cid)?.unwrap();
|
2020-08-31 09:09:49 +02:00
|
|
|
card.original_deck_id.0 = 0;
|
|
|
|
card.original_due = 0;
|
2020-05-10 05:50:04 +02:00
|
|
|
self.storage.update_card(&card)?;
|
|
|
|
wrong += 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if wrong > 0 {
|
2021-04-18 09:30:02 +02:00
|
|
|
self.set_schema_modified()?;
|
2020-05-10 10:09:18 +02:00
|
|
|
out.card_properties_invalid += wrong;
|
2020-05-10 05:50:04 +02:00
|
|
|
}
|
|
|
|
|
2020-05-03 04:24:18 +02:00
|
|
|
Ok(())
|
|
|
|
}
|
2020-05-10 05:50:04 +02:00
|
|
|
|
2020-06-08 12:28:11 +02:00
|
|
|
fn check_notetypes<F>(
|
|
|
|
&mut self,
|
|
|
|
out: &mut CheckDatabaseOutput,
|
|
|
|
mut progress_fn: F,
|
|
|
|
) -> Result<()>
|
|
|
|
where
|
|
|
|
F: FnMut(DatabaseCheckProgress, bool),
|
|
|
|
{
|
2020-05-10 05:50:04 +02:00
|
|
|
let nids_by_notetype = self.storage.all_note_ids_by_notetype()?;
|
2021-05-21 09:50:41 +02:00
|
|
|
let norm = self.get_config_bool(BoolKey::NormalizeNoteText);
|
2020-05-10 05:50:04 +02:00
|
|
|
let usn = self.usn()?;
|
|
|
|
let stamp = TimestampMillis::now();
|
|
|
|
|
2021-02-02 09:12:50 +01:00
|
|
|
let expanded_tags = self.storage.expanded_tags()?;
|
2021-03-02 10:02:00 +01:00
|
|
|
self.storage.clear_all_tags()?;
|
2020-05-10 05:50:04 +02:00
|
|
|
|
2020-06-08 12:28:11 +02:00
|
|
|
let total_notes = self.storage.total_notes()?;
|
|
|
|
let mut checked_notes = 0;
|
|
|
|
|
2020-05-10 05:50:04 +02:00
|
|
|
for (ntid, group) in &nids_by_notetype.into_iter().group_by(|tup| tup.0) {
|
|
|
|
debug!(self.log, "check notetype: {}", ntid);
|
|
|
|
let mut group = group.peekable();
|
|
|
|
let nt = match self.get_notetype(ntid)? {
|
|
|
|
None => {
|
|
|
|
let first_note = self.storage.get_note(group.peek().unwrap().1)?.unwrap();
|
2020-08-10 05:42:37 +02:00
|
|
|
out.notetypes_recovered += 1;
|
2021-03-02 10:02:00 +01:00
|
|
|
self.recover_notetype(stamp, first_note.fields().len(), ntid)?
|
2020-05-10 05:50:04 +02:00
|
|
|
}
|
|
|
|
Some(nt) => nt,
|
|
|
|
};
|
|
|
|
|
|
|
|
let mut genctx = None;
|
|
|
|
for (_, nid) in group {
|
2020-06-08 12:28:11 +02:00
|
|
|
progress_fn(
|
|
|
|
DatabaseCheckProgress::Notes {
|
|
|
|
current: checked_notes,
|
|
|
|
total: total_notes,
|
|
|
|
},
|
|
|
|
true,
|
|
|
|
);
|
|
|
|
checked_notes += 1;
|
|
|
|
|
2020-11-06 01:21:51 +01:00
|
|
|
let mut note = self.get_note_fixing_invalid_utf8(nid, out)?;
|
2021-03-02 10:02:00 +01:00
|
|
|
let original = note.clone();
|
2020-05-10 05:50:04 +02:00
|
|
|
|
|
|
|
let cards = self.storage.existing_cards_for_note(nid)?;
|
2020-05-10 10:09:18 +02:00
|
|
|
|
|
|
|
out.card_ords_duplicated += self.remove_duplicate_card_ordinals(&cards)?;
|
|
|
|
out.templates_missing += self.remove_cards_without_template(&nt, &cards)?;
|
2020-05-10 05:50:04 +02:00
|
|
|
|
|
|
|
// fix fields
|
2021-03-02 10:02:00 +01:00
|
|
|
if note.fields().len() != nt.fields.len() {
|
2020-05-10 05:50:04 +02:00
|
|
|
note.fix_field_count(&nt);
|
|
|
|
note.tags.push("db-check".into());
|
2020-05-10 10:09:18 +02:00
|
|
|
out.field_count_mismatch += 1;
|
2020-05-10 05:50:04 +02:00
|
|
|
}
|
|
|
|
|
2020-08-08 04:26:00 +02:00
|
|
|
// note type ID may have changed if we created a recovery notetype
|
2020-08-31 09:09:49 +02:00
|
|
|
note.notetype_id = nt.id;
|
2020-08-08 04:26:00 +02:00
|
|
|
|
2020-05-10 05:50:04 +02:00
|
|
|
// write note, updating tags and generating missing cards
|
2021-04-29 10:48:22 +02:00
|
|
|
let ctx = genctx.get_or_insert_with(|| {
|
|
|
|
CardGenContext::new(&nt, self.get_last_deck_added_to_for_notetype(nt.id), usn)
|
|
|
|
});
|
2021-04-29 15:28:42 +02:00
|
|
|
self.update_note_inner_generating_cards(
|
|
|
|
&ctx, &mut note, &original, false, norm, true,
|
|
|
|
)?;
|
2020-05-10 05:50:04 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-16 11:38:16 +01:00
|
|
|
// the note rebuilding process took care of adding tags back, so we just need
|
|
|
|
// to ensure to restore the collapse state
|
2021-02-02 09:12:50 +01:00
|
|
|
self.storage.restore_expanded_tags(&expanded_tags)?;
|
2021-01-12 21:12:35 +01:00
|
|
|
|
2020-05-10 05:50:04 +02:00
|
|
|
// if the collection is empty and the user has deleted all note types, ensure at least
|
|
|
|
// one note type exists
|
|
|
|
if self.storage.get_all_notetype_names()?.is_empty() {
|
2021-03-27 03:09:51 +01:00
|
|
|
let mut nt = all_stock_notetypes(&self.tr).remove(0);
|
2020-05-17 11:41:47 +02:00
|
|
|
self.add_notetype_inner(&mut nt, usn)?;
|
2020-05-10 05:50:04 +02:00
|
|
|
}
|
|
|
|
|
2020-08-10 05:42:37 +02:00
|
|
|
if out.card_ords_duplicated > 0
|
|
|
|
|| out.field_count_mismatch > 0
|
|
|
|
|| out.templates_missing > 0
|
|
|
|
|| out.notetypes_recovered > 0
|
2020-05-10 10:09:18 +02:00
|
|
|
{
|
2021-04-18 09:30:02 +02:00
|
|
|
self.set_schema_modified()?;
|
2020-05-10 05:50:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2020-11-06 01:21:51 +01:00
|
|
|
fn get_note_fixing_invalid_utf8(
|
|
|
|
&self,
|
2021-03-27 10:53:33 +01:00
|
|
|
nid: NoteId,
|
2020-11-06 01:21:51 +01:00
|
|
|
out: &mut CheckDatabaseOutput,
|
|
|
|
) -> Result<Note> {
|
|
|
|
match self.storage.get_note(nid) {
|
|
|
|
Ok(note) => Ok(note.unwrap()),
|
|
|
|
Err(err) => match err {
|
2021-04-01 09:34:03 +02:00
|
|
|
AnkiError::DbError(DbError {
|
2021-03-27 10:53:33 +01:00
|
|
|
kind: DbErrorKind::Utf8,
|
2020-11-06 01:21:51 +01:00
|
|
|
..
|
2021-04-01 09:34:03 +02:00
|
|
|
}) => {
|
2020-11-06 01:21:51 +01:00
|
|
|
// fix note then fetch again
|
|
|
|
self.storage.fix_invalid_utf8_in_note(nid)?;
|
|
|
|
out.invalid_utf8 += 1;
|
|
|
|
Ok(self.storage.get_note(nid)?.unwrap())
|
|
|
|
}
|
|
|
|
// other errors are unhandled
|
2020-11-24 11:13:05 +01:00
|
|
|
_ => Err(err),
|
2020-11-06 01:21:51 +01:00
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-10 05:50:04 +02:00
|
|
|
fn remove_duplicate_card_ordinals(
|
|
|
|
&mut self,
|
|
|
|
cards: &[AlreadyGeneratedCardInfo],
|
|
|
|
) -> Result<usize> {
|
|
|
|
let mut ords = HashSet::new();
|
|
|
|
let mut removed = 0;
|
|
|
|
for card in cards {
|
|
|
|
if !ords.insert(card.ord) {
|
|
|
|
self.storage.remove_card(card.id)?;
|
|
|
|
removed += 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(removed)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn remove_cards_without_template(
|
|
|
|
&mut self,
|
2021-03-27 13:03:19 +01:00
|
|
|
nt: &Notetype,
|
2020-05-10 05:50:04 +02:00
|
|
|
cards: &[AlreadyGeneratedCardInfo],
|
|
|
|
) -> Result<usize> {
|
2021-03-27 13:03:19 +01:00
|
|
|
if nt.config.kind() == NotetypeKind::Cloze {
|
2020-05-10 05:50:04 +02:00
|
|
|
return Ok(0);
|
|
|
|
}
|
|
|
|
let mut removed = 0;
|
|
|
|
for card in cards {
|
|
|
|
if card.ord as usize >= nt.templates.len() {
|
|
|
|
self.storage.remove_card(card.id)?;
|
|
|
|
removed += 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(removed)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn recover_notetype(
|
|
|
|
&mut self,
|
|
|
|
stamp: TimestampMillis,
|
|
|
|
field_count: usize,
|
2021-03-27 13:03:19 +01:00
|
|
|
previous_id: NotetypeId,
|
|
|
|
) -> Result<Arc<Notetype>> {
|
2020-05-10 05:50:04 +02:00
|
|
|
debug!(self.log, "create recovery notetype");
|
2020-08-10 05:42:37 +02:00
|
|
|
let extra_cards_required = self
|
|
|
|
.storage
|
|
|
|
.highest_card_ordinal_for_notetype(previous_id)?;
|
2021-03-27 03:09:51 +01:00
|
|
|
let mut basic = all_stock_notetypes(&self.tr).remove(0);
|
2020-05-10 05:50:04 +02:00
|
|
|
let mut field = 3;
|
|
|
|
while basic.fields.len() < field_count {
|
|
|
|
basic.add_field(format!("{}", field));
|
|
|
|
field += 1;
|
|
|
|
}
|
|
|
|
basic.name = format!("db-check-{}-{}", stamp, field_count);
|
2020-08-10 05:42:37 +02:00
|
|
|
let qfmt = basic.templates[0].config.q_format.clone();
|
|
|
|
let afmt = basic.templates[0].config.a_format.clone();
|
|
|
|
for n in 0..extra_cards_required {
|
|
|
|
basic.add_template(&format!("Card {}", n + 2), &qfmt, &afmt);
|
|
|
|
}
|
2020-05-10 05:50:04 +02:00
|
|
|
self.add_notetype(&mut basic)?;
|
|
|
|
Ok(Arc::new(basic))
|
|
|
|
}
|
|
|
|
|
2021-04-18 09:30:02 +02:00
|
|
|
fn check_revlog(&mut self, out: &mut CheckDatabaseOutput) -> Result<()> {
|
2020-05-10 05:50:04 +02:00
|
|
|
let cnt = self.storage.fix_revlog_properties()?;
|
|
|
|
if cnt > 0 {
|
2021-04-18 09:30:02 +02:00
|
|
|
self.set_schema_modified()?;
|
2020-05-10 10:09:18 +02:00
|
|
|
out.revlog_properties_invalid = cnt;
|
2020-05-10 05:50:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2020-05-10 11:51:18 +02:00
|
|
|
fn check_missing_deck_names(&mut self, out: &mut CheckDatabaseOutput) -> Result<()> {
|
|
|
|
let names = self.storage.get_all_deck_names()?;
|
|
|
|
out.decks_missing += self.add_missing_deck_names(&names)?;
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2021-03-09 01:58:55 +01:00
|
|
|
fn update_next_new_position(&mut self) -> Result<()> {
|
2020-05-10 05:50:04 +02:00
|
|
|
let pos = self.storage.max_new_card_position().unwrap_or(0);
|
|
|
|
self.set_next_card_position(pos)
|
|
|
|
}
|
2020-05-03 04:24:18 +02:00
|
|
|
}
|
2020-05-10 10:09:18 +02:00
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod test {
|
|
|
|
use super::*;
|
2021-03-27 10:53:33 +01:00
|
|
|
use crate::{collection::open_test_collection, decks::DeckId, search::SortMode};
|
2020-05-10 10:09:18 +02:00
|
|
|
|
2020-06-08 12:28:11 +02:00
|
|
|
fn progress_fn(_progress: DatabaseCheckProgress, _throttle: bool) {}
|
|
|
|
|
2020-05-10 10:09:18 +02:00
|
|
|
#[test]
|
|
|
|
fn cards() -> Result<()> {
|
|
|
|
let mut col = open_test_collection();
|
|
|
|
let nt = col.get_notetype_by_name("Basic")?.unwrap();
|
|
|
|
let mut note = nt.new_note();
|
2021-03-27 10:53:33 +01:00
|
|
|
col.add_note(&mut note, DeckId(1))?;
|
2020-05-10 10:09:18 +02:00
|
|
|
|
|
|
|
// card properties
|
|
|
|
col.storage
|
|
|
|
.db
|
|
|
|
.execute_batch("update cards set ivl=1.5,due=2000000,odue=1.5")?;
|
|
|
|
|
2020-06-08 12:28:11 +02:00
|
|
|
let out = col.check_database(progress_fn)?;
|
2020-05-10 10:09:18 +02:00
|
|
|
assert_eq!(
|
|
|
|
out,
|
|
|
|
CheckDatabaseOutput {
|
|
|
|
card_properties_invalid: 2,
|
|
|
|
card_position_too_high: 1,
|
|
|
|
..Default::default()
|
|
|
|
}
|
|
|
|
);
|
|
|
|
// should be idempotent
|
2020-06-08 12:28:11 +02:00
|
|
|
assert_eq!(col.check_database(progress_fn)?, Default::default());
|
2020-05-10 10:09:18 +02:00
|
|
|
|
|
|
|
// missing deck
|
|
|
|
col.storage.db.execute_batch("update cards set did=123")?;
|
|
|
|
|
2020-06-08 12:28:11 +02:00
|
|
|
let out = col.check_database(progress_fn)?;
|
2020-05-10 10:09:18 +02:00
|
|
|
assert_eq!(
|
|
|
|
out,
|
|
|
|
CheckDatabaseOutput {
|
2020-05-10 11:51:18 +02:00
|
|
|
decks_missing: 1,
|
2020-05-10 10:09:18 +02:00
|
|
|
..Default::default()
|
|
|
|
}
|
|
|
|
);
|
|
|
|
assert_eq!(
|
2021-04-18 01:33:39 +02:00
|
|
|
col.storage
|
|
|
|
.get_deck(DeckId(123))?
|
|
|
|
.unwrap()
|
|
|
|
.name
|
|
|
|
.as_native_str(),
|
2020-05-10 10:09:18 +02:00
|
|
|
"recovered123"
|
|
|
|
);
|
|
|
|
|
2020-05-10 11:51:18 +02:00
|
|
|
// missing note
|
|
|
|
col.storage.remove_note(note.id)?;
|
2020-06-08 12:28:11 +02:00
|
|
|
let out = col.check_database(progress_fn)?;
|
2020-05-10 11:51:18 +02:00
|
|
|
assert_eq!(
|
|
|
|
out,
|
|
|
|
CheckDatabaseOutput {
|
|
|
|
cards_missing_note: 1,
|
|
|
|
..Default::default()
|
|
|
|
}
|
|
|
|
);
|
|
|
|
assert_eq!(
|
|
|
|
col.storage.db_scalar::<u32>("select count(*) from cards")?,
|
|
|
|
0
|
|
|
|
);
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn revlog() -> Result<()> {
|
|
|
|
let mut col = open_test_collection();
|
|
|
|
|
|
|
|
col.storage.db.execute_batch(
|
|
|
|
"
|
|
|
|
insert into revlog (id,cid,usn,ease,ivl,lastIvl,factor,time,type)
|
|
|
|
values (0,0,0,0,1.5,1.5,0,0,0)",
|
|
|
|
)?;
|
|
|
|
|
2020-06-08 12:28:11 +02:00
|
|
|
let out = col.check_database(progress_fn)?;
|
2020-05-10 11:51:18 +02:00
|
|
|
assert_eq!(
|
|
|
|
out,
|
|
|
|
CheckDatabaseOutput {
|
|
|
|
revlog_properties_invalid: 1,
|
|
|
|
..Default::default()
|
|
|
|
}
|
|
|
|
);
|
|
|
|
assert_eq!(
|
|
|
|
col.storage
|
|
|
|
.db_scalar::<bool>("select ivl = lastIvl = 1 from revlog")?,
|
|
|
|
true
|
|
|
|
);
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn note_card_link() -> Result<()> {
|
|
|
|
let mut col = open_test_collection();
|
|
|
|
let nt = col.get_notetype_by_name("Basic")?.unwrap();
|
|
|
|
let mut note = nt.new_note();
|
2021-03-27 10:53:33 +01:00
|
|
|
col.add_note(&mut note, DeckId(1))?;
|
2020-05-10 11:51:18 +02:00
|
|
|
|
2020-05-10 10:09:18 +02:00
|
|
|
// duplicate ordinals
|
|
|
|
let cid = col.search_cards("", SortMode::NoOrder)?[0];
|
|
|
|
let mut card = col.storage.get_card(cid)?.unwrap();
|
|
|
|
card.id.0 += 1;
|
|
|
|
col.storage.add_card(&mut card)?;
|
|
|
|
|
2020-06-08 12:28:11 +02:00
|
|
|
let out = col.check_database(progress_fn)?;
|
2020-05-10 10:09:18 +02:00
|
|
|
assert_eq!(
|
|
|
|
out,
|
|
|
|
CheckDatabaseOutput {
|
|
|
|
card_ords_duplicated: 1,
|
|
|
|
..Default::default()
|
|
|
|
}
|
|
|
|
);
|
|
|
|
assert_eq!(
|
|
|
|
col.storage.db_scalar::<u32>("select count(*) from cards")?,
|
|
|
|
1
|
|
|
|
);
|
|
|
|
|
|
|
|
// missing templates
|
|
|
|
let cid = col.search_cards("", SortMode::NoOrder)?[0];
|
|
|
|
let mut card = col.storage.get_card(cid)?.unwrap();
|
|
|
|
card.id.0 += 1;
|
2020-08-31 09:09:49 +02:00
|
|
|
card.template_idx = 10;
|
2020-05-10 10:09:18 +02:00
|
|
|
col.storage.add_card(&mut card)?;
|
|
|
|
|
2020-06-08 12:28:11 +02:00
|
|
|
let out = col.check_database(progress_fn)?;
|
2020-05-10 10:09:18 +02:00
|
|
|
assert_eq!(
|
|
|
|
out,
|
|
|
|
CheckDatabaseOutput {
|
|
|
|
templates_missing: 1,
|
|
|
|
..Default::default()
|
|
|
|
}
|
|
|
|
);
|
|
|
|
assert_eq!(
|
|
|
|
col.storage.db_scalar::<u32>("select count(*) from cards")?,
|
|
|
|
1
|
|
|
|
);
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
2020-05-10 11:51:18 +02:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn note_fields() -> Result<()> {
|
|
|
|
let mut col = open_test_collection();
|
|
|
|
let nt = col.get_notetype_by_name("Basic")?.unwrap();
|
|
|
|
let mut note = nt.new_note();
|
2021-03-27 10:53:33 +01:00
|
|
|
col.add_note(&mut note, DeckId(1))?;
|
2020-05-10 11:51:18 +02:00
|
|
|
|
|
|
|
// excess fields get joined into the last one
|
|
|
|
col.storage
|
|
|
|
.db
|
|
|
|
.execute_batch("update notes set flds = 'a\x1fb\x1fc\x1fd'")?;
|
2020-06-08 12:28:11 +02:00
|
|
|
let out = col.check_database(progress_fn)?;
|
2020-05-10 11:51:18 +02:00
|
|
|
assert_eq!(
|
|
|
|
out,
|
|
|
|
CheckDatabaseOutput {
|
|
|
|
field_count_mismatch: 1,
|
|
|
|
..Default::default()
|
|
|
|
}
|
|
|
|
);
|
|
|
|
let note = col.storage.get_note(note.id)?.unwrap();
|
2021-03-02 10:02:00 +01:00
|
|
|
assert_eq!(¬e.fields()[..], &["a", "b; c; d"]);
|
2020-05-10 11:51:18 +02:00
|
|
|
|
|
|
|
// missing fields get filled with blanks
|
|
|
|
col.storage
|
|
|
|
.db
|
|
|
|
.execute_batch("update notes set flds = 'a'")?;
|
2020-06-08 12:28:11 +02:00
|
|
|
let out = col.check_database(progress_fn)?;
|
2020-05-10 11:51:18 +02:00
|
|
|
assert_eq!(
|
|
|
|
out,
|
|
|
|
CheckDatabaseOutput {
|
|
|
|
field_count_mismatch: 1,
|
|
|
|
..Default::default()
|
|
|
|
}
|
|
|
|
);
|
|
|
|
let note = col.storage.get_note(note.id)?.unwrap();
|
2021-03-02 10:02:00 +01:00
|
|
|
assert_eq!(¬e.fields()[..], &["a", ""]);
|
2020-05-10 11:51:18 +02:00
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn deck_names() -> Result<()> {
|
|
|
|
let mut col = open_test_collection();
|
|
|
|
|
|
|
|
let deck = col.get_or_create_normal_deck("foo::bar::baz")?;
|
|
|
|
// includes default
|
|
|
|
assert_eq!(col.storage.get_all_deck_names()?.len(), 4);
|
|
|
|
|
|
|
|
col.storage
|
|
|
|
.db
|
|
|
|
.prepare("delete from decks where id != ? and id != 1")?
|
|
|
|
.execute(&[deck.id])?;
|
|
|
|
assert_eq!(col.storage.get_all_deck_names()?.len(), 2);
|
|
|
|
|
2020-06-08 12:28:11 +02:00
|
|
|
let out = col.check_database(progress_fn)?;
|
2020-05-10 11:51:18 +02:00
|
|
|
assert_eq!(
|
|
|
|
out,
|
|
|
|
CheckDatabaseOutput {
|
|
|
|
decks_missing: 1, // only counts the immediate parent that was missing
|
|
|
|
..Default::default()
|
|
|
|
}
|
|
|
|
);
|
|
|
|
assert_eq!(
|
|
|
|
&col.storage
|
|
|
|
.get_all_deck_names()?
|
|
|
|
.iter()
|
|
|
|
.map(|(_, name)| name)
|
|
|
|
.collect::<Vec<_>>(),
|
|
|
|
&["Default", "foo", "foo::bar", "foo::bar::baz"]
|
|
|
|
);
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
2021-01-16 11:38:16 +01:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn tags() -> Result<()> {
|
|
|
|
let mut col = open_test_collection();
|
|
|
|
let nt = col.get_notetype_by_name("Basic")?.unwrap();
|
|
|
|
let mut note = nt.new_note();
|
|
|
|
note.tags.push("one".into());
|
|
|
|
note.tags.push("two".into());
|
2021-03-27 10:53:33 +01:00
|
|
|
col.add_note(&mut note, DeckId(1))?;
|
2021-01-16 11:38:16 +01:00
|
|
|
|
2021-04-05 03:41:53 +02:00
|
|
|
col.set_tag_collapsed("one", false)?;
|
2021-01-16 11:38:16 +01:00
|
|
|
|
|
|
|
col.check_database(progress_fn)?;
|
|
|
|
|
2021-02-02 09:49:34 +01:00
|
|
|
assert_eq!(col.storage.get_tag("one")?.unwrap().expanded, true);
|
|
|
|
assert_eq!(col.storage.get_tag("two")?.unwrap().expanded, false);
|
2021-01-16 11:38:16 +01:00
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
2020-05-10 10:09:18 +02:00
|
|
|
}
|