diff --git a/proto/backend.proto b/proto/backend.proto index 543524f35..fe678e7f4 100644 --- a/proto/backend.proto +++ b/proto/backend.proto @@ -71,7 +71,7 @@ message BackendInput { int32 get_changed_notetypes = 56; AddOrUpdateNotetypeIn add_or_update_notetype = 57; Empty get_all_decks = 58; -// bytes set_all_decks = 59; + Empty check_database = 59; Empty all_stock_notetypes = 60; int64 get_notetype_legacy = 61; Empty get_notetype_names = 62; @@ -140,7 +140,7 @@ message BackendOutput { bytes get_changed_notetypes = 56; int64 add_or_update_notetype = 57; bytes get_all_decks = 58; -// Empty set_all_decks = 59; + Empty check_database = 59; bytes get_notetype_legacy = 61; NoteTypeNames get_notetype_names = 62; NoteTypeUseCounts get_notetype_names_and_counts = 63; diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index 33ea1cc97..4ade3e6fb 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -958,6 +958,8 @@ and type=0 and queue!=4""", # models if self.models.ensureNotEmpty(): problems.append("Added missing note type.") + # misc other + self.backend.check_database() # and finally, optimize self.optimize() newSize = os.stat(self.path)[stat.ST_SIZE] diff --git a/pylib/anki/decks.py b/pylib/anki/decks.py index c3656e268..8aa5f7420 100644 --- a/pylib/anki/decks.py +++ b/pylib/anki/decks.py @@ -5,7 +5,7 @@ from __future__ import annotations import copy import unicodedata -from typing import Any, Dict, List, Optional, Set, Tuple, Union +from typing import Any, Dict, List, Optional, Tuple, Union import anki # pylint: disable=unused-import import anki.backend_pb2 as pb @@ -524,37 +524,8 @@ class DeckManager: # ) # self.col.db.mod = mod - def _checkDeckTree(self) -> None: - decks = self.all() - decks.sort(key=self.key) - names: Set[str] = set() - - for deck in decks: - # two decks with the same name? - if deck["name"] in names: - self.col.log("fix duplicate deck name", deck["name"]) - deck["name"] += "%d" % intTime(1000) - self.save(deck) - - # ensure no sections are blank - if not all(self.path(deck["name"])): - self.col.log("fix deck with missing sections", deck["name"]) - deck["name"] = "recovered%d" % intTime(1000) - self.save(deck) - - # immediate parent must exist - if "::" in deck["name"]: - immediateParent = self.immediate_parent(deck["name"]) - if immediateParent not in names: - self.col.log("fix deck with missing parent", deck["name"]) - self._ensureParents(deck["name"]) - names.add(immediateParent) - - names.add(deck["name"]) - def checkIntegrity(self) -> None: self._recoverOrphans() - self._checkDeckTree() def should_deck_be_displayed( self, deck, force_default: bool = True, assume_no_child: bool = False diff --git a/pylib/anki/rsbackend.py b/pylib/anki/rsbackend.py index 34b3286c1..223ba9b47 100644 --- a/pylib/anki/rsbackend.py +++ b/pylib/anki/rsbackend.py @@ -735,6 +735,9 @@ class RustBackend: pb.BackendInput(deck_tree=pb.DeckTreeIn(include_counts=include_counts)) ).deck_tree + def check_database(self) -> None: + self._run_command(pb.BackendInput(check_database=pb.Empty())) + def translate_string_in( key: TR, **kwargs: Union[str, int, float] diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index f9d05021f..01ef2ab85 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -356,6 +356,10 @@ impl Backend { self.remove_deck(did)?; pb::Empty {} }), + Value::CheckDatabase(_) => { + self.check_database()?; + OValue::CheckDatabase(pb::Empty {}) + } }) } @@ -1046,6 +1050,10 @@ impl Backend { fn remove_deck(&self, did: i64) -> Result<()> { self.with_col(|col| col.remove_deck_and_child_decks(DeckID(did))) } + + fn check_database(&self) -> Result<()> { + self.with_col(|col| col.transact(None, |col| col.check_database())) + } } fn translate_arg_to_fluent_val(arg: &pb::TranslateArgValue) -> FluentValue { diff --git a/rslib/src/dbcheck.rs b/rslib/src/dbcheck.rs new file mode 100644 index 000000000..1d316ae36 --- /dev/null +++ b/rslib/src/dbcheck.rs @@ -0,0 +1,12 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use crate::{collection::Collection, err::Result}; + +impl Collection { + pub(crate) fn check_database(&mut self) -> Result<()> { + let names = self.storage.get_all_deck_names()?; + self.add_missing_decks(&names)?; + Ok(()) + } +} diff --git a/rslib/src/decks/tree.rs b/rslib/src/decks/tree.rs index c0097d8e3..da25a6660 100644 --- a/rslib/src/decks/tree.rs +++ b/rslib/src/decks/tree.rs @@ -3,7 +3,11 @@ use super::Deck; use crate::{backend_proto::DeckTreeNode, collection::Collection, decks::DeckID, err::Result}; -use std::{collections::HashMap, iter::Peekable}; +use std::{ + collections::{HashMap, HashSet}, + iter::Peekable, +}; +use unicase::UniCase; // fixme: handle mixed case of parents @@ -80,6 +84,21 @@ impl Collection { Ok(tree) } + + pub(crate) fn add_missing_decks(&mut self, names: &[(DeckID, String)]) -> Result<()> { + let mut parents = HashSet::new(); + for (_id, name) in names { + parents.insert(UniCase::new(name.as_str())); + if let Some(immediate_parent) = name.rsplitn(2, "::").nth(1) { + let immediate_parent_uni = UniCase::new(immediate_parent); + if !parents.contains(&immediate_parent_uni) { + self.get_or_create_normal_deck(immediate_parent)?; + parents.insert(immediate_parent_uni); + } + } + } + Ok(()) + } } #[cfg(test)] diff --git a/rslib/src/lib.rs b/rslib/src/lib.rs index 362db2987..0d4062afc 100644 --- a/rslib/src/lib.rs +++ b/rslib/src/lib.rs @@ -14,6 +14,7 @@ pub mod card; pub mod cloze; pub mod collection; pub mod config; +pub mod dbcheck; pub mod deckconf; pub mod decks; pub mod err;