mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
Fix invalid ids on db check (#2445)
* Move open_test_collection into Collection test impl * Fix invalid ids when checking database * Report fixed invalid ids * Improve message when trying to export invalid ids Also move ImportError due to namespace conflicts with snafu macro. * Take a human name in DeckAdder::new * Mention timestamps in the db check message (dae) Will help to correlate the fix with the message shown when importing/ exporting.
This commit is contained in:
parent
19cf375152
commit
039ebfeed6
47 changed files with 330 additions and 162 deletions
|
@ -45,6 +45,11 @@ database-check-notes-with-invalid-utf8 =
|
||||||
[one] Fixed { $count } note with invalid utf8 characters.
|
[one] Fixed { $count } note with invalid utf8 characters.
|
||||||
*[other] Fixed { $count } notes with invalid utf8 characters.
|
*[other] Fixed { $count } notes with invalid utf8 characters.
|
||||||
}
|
}
|
||||||
|
database-check-fixed-invalid-ids =
|
||||||
|
{ $count ->
|
||||||
|
[one] Fixed { $count } object with timestamps in the future.
|
||||||
|
*[other] Fixed { $count } objects with timestamps in the future.
|
||||||
|
}
|
||||||
# "db-check" is always in English
|
# "db-check" is always in English
|
||||||
database-check-notetypes-recovered = One or more notetypes were missing. The notes that used them have been given new notetypes starting with "db-check", but field names and card design have been lost, so you may be better off restoring from an automatic backup.
|
database-check-notetypes-recovered = One or more notetypes were missing. The notes that used them have been given new notetypes starting with "db-check", but field names and card design have been lost, so you may be better off restoring from an automatic backup.
|
||||||
|
|
||||||
|
|
|
@ -282,7 +282,6 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::backend::ankidroid::db::select_slice_of_size;
|
use crate::backend::ankidroid::db::select_slice_of_size;
|
||||||
use crate::backend::ankidroid::db::Sizable;
|
use crate::backend::ankidroid::db::Sizable;
|
||||||
use crate::collection::open_test_collection;
|
|
||||||
use crate::pb::ankidroid::sql_value;
|
use crate::pb::ankidroid::sql_value;
|
||||||
use crate::pb::ankidroid::Row;
|
use crate::pb::ankidroid::Row;
|
||||||
use crate::pb::ankidroid::SqlValue;
|
use crate::pb::ankidroid::SqlValue;
|
||||||
|
@ -382,7 +381,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn integration_test() {
|
fn integration_test() {
|
||||||
let col = open_test_collection();
|
let col = Collection::new();
|
||||||
|
|
||||||
let row = Row { fields: gen_data() };
|
let row = Row { fields: gen_data() };
|
||||||
|
|
||||||
|
|
|
@ -110,11 +110,6 @@ impl CollectionBuilder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub fn open_test_collection() -> Collection {
|
|
||||||
CollectionBuilder::default().build().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct CollectionState {
|
pub struct CollectionState {
|
||||||
pub(crate) undo: UndoManager,
|
pub(crate) undo: UndoManager,
|
||||||
|
|
|
@ -310,18 +310,17 @@ pub(crate) enum Weekday {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use crate::collection::open_test_collection;
|
use super::*;
|
||||||
use crate::decks::DeckId;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn defaults() {
|
fn defaults() {
|
||||||
let col = open_test_collection();
|
let col = Collection::new();
|
||||||
assert_eq!(col.get_current_deck_id(), DeckId(1));
|
assert_eq!(col.get_current_deck_id(), DeckId(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn get_set() {
|
fn get_set() {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
|
|
||||||
// missing key
|
// missing key
|
||||||
assert_eq!(col.get_config_optional::<Vec<i64>, _>("test"), None);
|
assert_eq!(col.get_config_optional::<Vec<i64>, _>("test"), None);
|
||||||
|
|
|
@ -71,11 +71,10 @@ impl Collection {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::collection::open_test_collection;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn undo() -> Result<()> {
|
fn undo() -> Result<()> {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
// the op kind doesn't matter, we just need undo enabled
|
// the op kind doesn't matter, we just need undo enabled
|
||||||
let op = Op::Bury;
|
let op = Op::Bury;
|
||||||
// test key
|
// test key
|
||||||
|
|
|
@ -36,6 +36,7 @@ pub struct CheckDatabaseOutput {
|
||||||
field_count_mismatch: usize,
|
field_count_mismatch: usize,
|
||||||
notetypes_recovered: usize,
|
notetypes_recovered: usize,
|
||||||
invalid_utf8: usize,
|
invalid_utf8: usize,
|
||||||
|
invalid_ids: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
@ -82,6 +83,9 @@ impl CheckDatabaseOutput {
|
||||||
if self.invalid_utf8 > 0 {
|
if self.invalid_utf8 > 0 {
|
||||||
probs.push(tr.database_check_notes_with_invalid_utf8(self.invalid_utf8));
|
probs.push(tr.database_check_notes_with_invalid_utf8(self.invalid_utf8));
|
||||||
}
|
}
|
||||||
|
if self.invalid_ids > 0 {
|
||||||
|
probs.push(tr.database_check_fixed_invalid_ids(self.invalid_ids));
|
||||||
|
}
|
||||||
|
|
||||||
probs.into_iter().map(Into::into).collect()
|
probs.into_iter().map(Into::into).collect()
|
||||||
}
|
}
|
||||||
|
@ -139,6 +143,9 @@ impl Collection {
|
||||||
|
|
||||||
self.update_next_new_position()?;
|
self.update_next_new_position()?;
|
||||||
|
|
||||||
|
debug!("invalid ids");
|
||||||
|
self.maybe_fix_invalid_ids(&mut out)?;
|
||||||
|
|
||||||
debug!("db check finished: {:#?}", out);
|
debug!("db check finished: {:#?}", out);
|
||||||
|
|
||||||
Ok(out)
|
Ok(out)
|
||||||
|
@ -408,12 +415,22 @@ impl Collection {
|
||||||
let pos = self.storage.max_new_card_position().unwrap_or(0);
|
let pos = self.storage.max_new_card_position().unwrap_or(0);
|
||||||
self.set_next_card_position(pos)
|
self.set_next_card_position(pos)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn maybe_fix_invalid_ids(&mut self, out: &mut CheckDatabaseOutput) -> Result<()> {
|
||||||
|
let now = TimestampMillis::now();
|
||||||
|
let tomorrow = now.adding_secs(24 * 60 * 60).0;
|
||||||
|
out.invalid_ids = self.storage.invalid_ids(tomorrow)?;
|
||||||
|
if out.invalid_ids > 0 {
|
||||||
|
self.storage.fix_invalid_ids(tomorrow, now.0)?;
|
||||||
|
self.set_schema_modified()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::collection::open_test_collection;
|
|
||||||
use crate::decks::DeckId;
|
use crate::decks::DeckId;
|
||||||
use crate::search::SortMode;
|
use crate::search::SortMode;
|
||||||
|
|
||||||
|
@ -421,7 +438,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cards() -> Result<()> {
|
fn cards() -> Result<()> {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
let nt = col.get_notetype_by_name("Basic")?.unwrap();
|
let nt = col.get_notetype_by_name("Basic")?.unwrap();
|
||||||
let mut note = nt.new_note();
|
let mut note = nt.new_note();
|
||||||
col.add_note(&mut note, DeckId(1))?;
|
col.add_note(&mut note, DeckId(1))?;
|
||||||
|
@ -483,7 +500,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn revlog() -> Result<()> {
|
fn revlog() -> Result<()> {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
|
|
||||||
col.storage.db.execute_batch(
|
col.storage.db.execute_batch(
|
||||||
"
|
"
|
||||||
|
@ -508,7 +525,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn note_card_link() -> Result<()> {
|
fn note_card_link() -> Result<()> {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
let nt = col.get_notetype_by_name("Basic")?.unwrap();
|
let nt = col.get_notetype_by_name("Basic")?.unwrap();
|
||||||
let mut note = nt.new_note();
|
let mut note = nt.new_note();
|
||||||
col.add_note(&mut note, DeckId(1))?;
|
col.add_note(&mut note, DeckId(1))?;
|
||||||
|
@ -557,7 +574,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn note_fields() -> Result<()> {
|
fn note_fields() -> Result<()> {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
let nt = col.get_notetype_by_name("Basic")?.unwrap();
|
let nt = col.get_notetype_by_name("Basic")?.unwrap();
|
||||||
let mut note = nt.new_note();
|
let mut note = nt.new_note();
|
||||||
col.add_note(&mut note, DeckId(1))?;
|
col.add_note(&mut note, DeckId(1))?;
|
||||||
|
@ -597,7 +614,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn deck_names() -> Result<()> {
|
fn deck_names() -> Result<()> {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
|
|
||||||
let deck = col.get_or_create_normal_deck("foo::bar::baz")?;
|
let deck = col.get_or_create_normal_deck("foo::bar::baz")?;
|
||||||
// includes default
|
// includes default
|
||||||
|
@ -631,7 +648,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tags() -> Result<()> {
|
fn tags() -> Result<()> {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
let nt = col.get_notetype_by_name("Basic")?.unwrap();
|
let nt = col.get_notetype_by_name("Basic")?.unwrap();
|
||||||
let mut note = nt.new_note();
|
let mut note = nt.new_note();
|
||||||
note.tags.push("one".into());
|
note.tags.push("one".into());
|
||||||
|
|
|
@ -275,14 +275,13 @@ fn update_day_limit(day_limit: &mut Option<DayLimit>, new_limit: Option<u32>, to
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::collection::open_test_collection;
|
|
||||||
use crate::deckconfig::NewCardInsertOrder;
|
use crate::deckconfig::NewCardInsertOrder;
|
||||||
use crate::tests::open_test_collection_with_learning_card;
|
use crate::tests::open_test_collection_with_learning_card;
|
||||||
use crate::tests::open_test_collection_with_relearning_card;
|
use crate::tests::open_test_collection_with_relearning_card;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn updating() -> Result<()> {
|
fn updating() -> Result<()> {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
let nt = col.get_notetype_by_name("Basic")?.unwrap();
|
let nt = col.get_notetype_by_name("Basic")?.unwrap();
|
||||||
let mut note1 = nt.new_note();
|
let mut note1 = nt.new_note();
|
||||||
col.add_note(&mut note1, DeckId(1))?;
|
col.add_note(&mut note1, DeckId(1))?;
|
||||||
|
|
|
@ -185,7 +185,6 @@ impl Collection {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use crate::collection::open_test_collection;
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::search::SortMode;
|
use crate::search::SortMode;
|
||||||
|
|
||||||
|
@ -200,7 +199,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn adding_updating() -> Result<()> {
|
fn adding_updating() -> Result<()> {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
|
|
||||||
let deck1 = col.get_or_create_normal_deck("foo")?;
|
let deck1 = col.get_or_create_normal_deck("foo")?;
|
||||||
let deck2 = col.get_or_create_normal_deck("FOO")?;
|
let deck2 = col.get_or_create_normal_deck("FOO")?;
|
||||||
|
@ -220,7 +219,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn renaming() -> Result<()> {
|
fn renaming() -> Result<()> {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
|
|
||||||
let _ = col.get_or_create_normal_deck("foo::bar::baz")?;
|
let _ = col.get_or_create_normal_deck("foo::bar::baz")?;
|
||||||
let mut top_deck = col.get_or_create_normal_deck("foo")?;
|
let mut top_deck = col.get_or_create_normal_deck("foo")?;
|
||||||
|
@ -291,7 +290,7 @@ mod test {
|
||||||
fn default() -> Result<()> {
|
fn default() -> Result<()> {
|
||||||
// deleting the default deck will remove cards, but bring the deck back
|
// deleting the default deck will remove cards, but bring the deck back
|
||||||
// as a top level deck
|
// as a top level deck
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
|
|
||||||
let mut default = col.get_or_create_normal_deck("default")?;
|
let mut default = col.get_or_create_normal_deck("default")?;
|
||||||
default.name = NativeDeckName::from_native_str("one\x1ftwo");
|
default.name = NativeDeckName::from_native_str("one\x1ftwo");
|
||||||
|
|
|
@ -12,9 +12,10 @@ pub struct NativeDeckName(String);
|
||||||
|
|
||||||
impl NativeDeckName {
|
impl NativeDeckName {
|
||||||
/// Create from a '::'-separated string
|
/// Create from a '::'-separated string
|
||||||
pub fn from_human_name(name: &str) -> Self {
|
pub fn from_human_name(name: impl AsRef<str>) -> Self {
|
||||||
NativeDeckName(
|
NativeDeckName(
|
||||||
name.split("::")
|
name.as_ref()
|
||||||
|
.split("::")
|
||||||
.map(normalized_deck_name_component)
|
.map(normalized_deck_name_component)
|
||||||
.join("\x1f"),
|
.join("\x1f"),
|
||||||
)
|
)
|
||||||
|
|
|
@ -406,13 +406,12 @@ impl Collection {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::collection::open_test_collection;
|
|
||||||
use crate::deckconfig::DeckConfigId;
|
use crate::deckconfig::DeckConfigId;
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn wellformed() -> Result<()> {
|
fn wellformed() -> Result<()> {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
|
|
||||||
col.get_or_create_normal_deck("1")?;
|
col.get_or_create_normal_deck("1")?;
|
||||||
col.get_or_create_normal_deck("2")?;
|
col.get_or_create_normal_deck("2")?;
|
||||||
|
@ -436,7 +435,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn malformed() -> Result<()> {
|
fn malformed() -> Result<()> {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
|
|
||||||
col.get_or_create_normal_deck("1")?;
|
col.get_or_create_normal_deck("1")?;
|
||||||
col.get_or_create_normal_deck("2::3::4")?;
|
col.get_or_create_normal_deck("2::3::4")?;
|
||||||
|
@ -453,7 +452,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn counts() -> Result<()> {
|
fn counts() -> Result<()> {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
|
|
||||||
let mut parent_deck = col.get_or_create_normal_deck("Default")?;
|
let mut parent_deck = col.get_or_create_normal_deck("Default")?;
|
||||||
let mut child_deck = col.get_or_create_normal_deck("Default::one")?;
|
let mut child_deck = col.get_or_create_normal_deck("Default::one")?;
|
||||||
|
@ -503,7 +502,7 @@ mod test {
|
||||||
deck
|
deck
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
col.set_config_bool(BoolKey::Sched2021, true, false)?;
|
col.set_config_bool(BoolKey::Sched2021, true, false)?;
|
||||||
|
|
||||||
let parent_deck = create_deck_with_new_limit(&mut col, "Default", 8);
|
let parent_deck = create_deck_with_new_limit(&mut col, "Default", 8);
|
||||||
|
|
|
@ -31,6 +31,7 @@ pub use self::invalid_input::OrInvalid;
|
||||||
pub use self::not_found::NotFoundError;
|
pub use self::not_found::NotFoundError;
|
||||||
pub use self::not_found::OrNotFound;
|
pub use self::not_found::OrNotFound;
|
||||||
use crate::i18n::I18n;
|
use crate::i18n::I18n;
|
||||||
|
use crate::import_export::ImportError;
|
||||||
use crate::links::HelpPage;
|
use crate::links::HelpPage;
|
||||||
|
|
||||||
pub type Result<T, E = AnkiError> = std::result::Result<T, E>;
|
pub type Result<T, E = AnkiError> = std::result::Result<T, E>;
|
||||||
|
@ -150,7 +151,7 @@ impl AnkiError {
|
||||||
AnkiError::CustomStudyError { source } => source.message(tr),
|
AnkiError::CustomStudyError { source } => source.message(tr),
|
||||||
AnkiError::ImportError { source } => source.message(tr),
|
AnkiError::ImportError { source } => source.message(tr),
|
||||||
AnkiError::Deleted => tr.browsing_row_deleted().into(),
|
AnkiError::Deleted => tr.browsing_row_deleted().into(),
|
||||||
AnkiError::InvalidId => tr.errors_invalid_ids().into(),
|
AnkiError::InvalidId => tr.errors_please_check_database().into(),
|
||||||
AnkiError::JsonError { .. }
|
AnkiError::JsonError { .. }
|
||||||
| AnkiError::ProtoError { .. }
|
| AnkiError::ProtoError { .. }
|
||||||
| AnkiError::Interrupted
|
| AnkiError::Interrupted
|
||||||
|
@ -299,25 +300,3 @@ pub enum CardTypeErrorDetails {
|
||||||
MissingCloze,
|
MissingCloze,
|
||||||
ExtraneousCloze,
|
ExtraneousCloze,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Snafu)]
|
|
||||||
pub enum ImportError {
|
|
||||||
Corrupt,
|
|
||||||
TooNew,
|
|
||||||
MediaImportFailed { info: String },
|
|
||||||
NoFieldColumn,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ImportError {
|
|
||||||
fn message(&self, tr: &I18n) -> String {
|
|
||||||
match self {
|
|
||||||
ImportError::Corrupt => tr.importing_the_provided_file_is_not_a(),
|
|
||||||
ImportError::TooNew => tr.errors_collection_too_new(),
|
|
||||||
ImportError::MediaImportFailed { info } => {
|
|
||||||
tr.importing_failed_to_import_media_file(info)
|
|
||||||
}
|
|
||||||
ImportError::NoFieldColumn => tr.importing_file_must_contain_field_column(),
|
|
||||||
}
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -114,12 +114,11 @@ impl Collection {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::collection::open_test_collection;
|
|
||||||
use crate::decks::DeckId;
|
use crate::decks::DeckId;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn findreplace() -> Result<()> {
|
fn findreplace() -> Result<()> {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
|
|
||||||
let nt = col.get_notetype_by_name("Basic")?.unwrap();
|
let nt = col.get_notetype_by_name("Basic")?.unwrap();
|
||||||
let mut note = nt.new_note();
|
let mut note = nt.new_note();
|
||||||
|
|
|
@ -270,13 +270,12 @@ impl Collection {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::collection::open_test_collection;
|
|
||||||
use crate::search::SearchNode;
|
use crate::search::SearchNode;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_gather_valid_notes() {
|
fn should_gather_valid_notes() {
|
||||||
let mut data = ExchangeData::default();
|
let mut data = ExchangeData::default();
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
|
|
||||||
let note = NoteAdder::basic(&mut col).add(&mut col);
|
let note = NoteAdder::basic(&mut col).add(&mut col);
|
||||||
data.gather_data(&mut col, SearchNode::WholeCollection, true)
|
data.gather_data(&mut col, SearchNode::WholeCollection, true)
|
||||||
|
@ -288,7 +287,7 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn should_err_if_note_has_invalid_id() {
|
fn should_err_if_note_has_invalid_id() {
|
||||||
let mut data = ExchangeData::default();
|
let mut data = ExchangeData::default();
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
let now_micros = TimestampMillis::now().0 * 1000;
|
let now_micros = TimestampMillis::now().0 * 1000;
|
||||||
|
|
||||||
let mut note = NoteAdder::basic(&mut col).add(&mut col);
|
let mut note = NoteAdder::basic(&mut col).add(&mut col);
|
||||||
|
|
|
@ -8,6 +8,8 @@ pub mod text;
|
||||||
|
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
|
use snafu::Snafu;
|
||||||
|
|
||||||
pub use crate::pb::import_export::import_response::Log as NoteLog;
|
pub use crate::pb::import_export::import_response::Log as NoteLog;
|
||||||
pub use crate::pb::import_export::import_response::Note as LogNote;
|
pub use crate::pb::import_export::import_response::Note as LogNote;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
@ -128,3 +130,27 @@ impl Note {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Snafu)]
|
||||||
|
pub enum ImportError {
|
||||||
|
Corrupt,
|
||||||
|
TooNew,
|
||||||
|
MediaImportFailed { info: String },
|
||||||
|
NoFieldColumn,
|
||||||
|
InvalidId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImportError {
|
||||||
|
pub(crate) fn message(&self, tr: &I18n) -> String {
|
||||||
|
match self {
|
||||||
|
ImportError::Corrupt => tr.importing_the_provided_file_is_not_a(),
|
||||||
|
ImportError::TooNew => tr.errors_collection_too_new(),
|
||||||
|
ImportError::MediaImportFailed { info } => {
|
||||||
|
tr.importing_failed_to_import_media_file(info)
|
||||||
|
}
|
||||||
|
ImportError::NoFieldColumn => tr.importing_file_must_contain_field_column(),
|
||||||
|
ImportError::InvalidId => tr.errors_invalid_ids(),
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -209,11 +209,10 @@ mod test {
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::collection::open_test_collection;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parents() {
|
fn parents() {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
|
|
||||||
DeckAdder::new("filtered").filtered(true).add(&mut col);
|
DeckAdder::new("filtered").filtered(true).add(&mut col);
|
||||||
DeckAdder::new("PARENT").add(&mut col);
|
DeckAdder::new("PARENT").add(&mut col);
|
||||||
|
@ -222,10 +221,10 @@ mod test {
|
||||||
ctx.unique_suffix = "★".to_string();
|
ctx.unique_suffix = "★".to_string();
|
||||||
|
|
||||||
let imports = vec![
|
let imports = vec![
|
||||||
DeckAdder::new("unknown parent\x1fchild").deck(),
|
DeckAdder::new("unknown parent::child").deck(),
|
||||||
DeckAdder::new("filtered\x1fchild").deck(),
|
DeckAdder::new("filtered::child").deck(),
|
||||||
DeckAdder::new("parent\x1fchild").deck(),
|
DeckAdder::new("parent::child").deck(),
|
||||||
DeckAdder::new("NEW PARENT\x1fchild").deck(),
|
DeckAdder::new("NEW PARENT::child").deck(),
|
||||||
DeckAdder::new("new parent").deck(),
|
DeckAdder::new("new parent").deck(),
|
||||||
];
|
];
|
||||||
ctx.import_decks(imports, false, false).unwrap();
|
ctx.import_decks(imports, false, false).unwrap();
|
||||||
|
|
|
@ -20,6 +20,7 @@ use crate::error::FileIoSnafu;
|
||||||
use crate::error::FileOp;
|
use crate::error::FileOp;
|
||||||
use crate::import_export::gather::ExchangeData;
|
use crate::import_export::gather::ExchangeData;
|
||||||
use crate::import_export::package::Meta;
|
use crate::import_export::package::Meta;
|
||||||
|
use crate::import_export::ImportError;
|
||||||
use crate::import_export::ImportProgress;
|
use crate::import_export::ImportProgress;
|
||||||
use crate::import_export::IncrementableProgress;
|
use crate::import_export::IncrementableProgress;
|
||||||
use crate::import_export::NoteLog;
|
use crate::import_export::NoteLog;
|
||||||
|
@ -110,7 +111,11 @@ impl ExchangeData {
|
||||||
|
|
||||||
progress.call(ImportProgress::Gathering)?;
|
progress.call(ImportProgress::Gathering)?;
|
||||||
let mut data = ExchangeData::default();
|
let mut data = ExchangeData::default();
|
||||||
data.gather_data(&mut col, search, with_scheduling)?;
|
data.gather_data(&mut col, search, with_scheduling)
|
||||||
|
.map_err(|error| match error {
|
||||||
|
AnkiError::InvalidId => ImportError::InvalidId.into(),
|
||||||
|
error => error,
|
||||||
|
})?;
|
||||||
|
|
||||||
Ok(data)
|
Ok(data)
|
||||||
}
|
}
|
||||||
|
|
|
@ -292,7 +292,6 @@ impl Notetype {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::collection::open_test_collection;
|
|
||||||
use crate::import_export::package::media::SafeMediaEntry;
|
use crate::import_export::package::media::SafeMediaEntry;
|
||||||
|
|
||||||
/// Import [Note] into [Collection], optionally taking a [MediaUseMap],
|
/// Import [Note] into [Collection], optionally taking a [MediaUseMap],
|
||||||
|
@ -341,7 +340,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_add_note_with_new_id_if_guid_is_unique_and_id_is_not() {
|
fn should_add_note_with_new_id_if_guid_is_unique_and_id_is_not() {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
let mut note = NoteAdder::basic(&mut col).add(&mut col);
|
let mut note = NoteAdder::basic(&mut col).add(&mut col);
|
||||||
note.guid = "other".to_string();
|
note.guid = "other".to_string();
|
||||||
let original_id = note.id;
|
let original_id = note.id;
|
||||||
|
@ -353,7 +352,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_skip_note_if_guid_already_exists_with_newer_mtime() {
|
fn should_skip_note_if_guid_already_exists_with_newer_mtime() {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
let mut note = NoteAdder::basic(&mut col).add(&mut col);
|
let mut note = NoteAdder::basic(&mut col).add(&mut col);
|
||||||
note.mtime.0 -= 1;
|
note.mtime.0 -= 1;
|
||||||
note.fields_mut()[0] = "outdated".to_string();
|
note.fields_mut()[0] = "outdated".to_string();
|
||||||
|
@ -365,7 +364,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_update_note_if_guid_already_exists_with_different_id() {
|
fn should_update_note_if_guid_already_exists_with_different_id() {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
let mut note = NoteAdder::basic(&mut col).add(&mut col);
|
let mut note = NoteAdder::basic(&mut col).add(&mut col);
|
||||||
note.id.0 = 42;
|
note.id.0 = 42;
|
||||||
note.mtime.0 += 1;
|
note.mtime.0 += 1;
|
||||||
|
@ -378,7 +377,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_ignore_note_if_guid_already_exists_with_different_notetype() {
|
fn should_ignore_note_if_guid_already_exists_with_different_notetype() {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
let mut note = NoteAdder::basic(&mut col).add(&mut col);
|
let mut note = NoteAdder::basic(&mut col).add(&mut col);
|
||||||
note.notetype_id.0 = 42;
|
note.notetype_id.0 = 42;
|
||||||
note.mtime.0 += 1;
|
note.mtime.0 += 1;
|
||||||
|
@ -391,7 +390,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_add_note_with_remapped_notetype_if_in_notetype_map() {
|
fn should_add_note_with_remapped_notetype_if_in_notetype_map() {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
let basic_ntid = col.get_notetype_by_name("basic").unwrap().unwrap().id;
|
let basic_ntid = col.get_notetype_by_name("basic").unwrap().unwrap().id;
|
||||||
let mut note = NoteAdder::basic(&mut col).note();
|
let mut note = NoteAdder::basic(&mut col).note();
|
||||||
note.notetype_id.0 = 123;
|
note.notetype_id.0 = 123;
|
||||||
|
@ -403,7 +402,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_ignore_note_if_guid_already_exists_and_notetype_is_remapped() {
|
fn should_ignore_note_if_guid_already_exists_and_notetype_is_remapped() {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
let basic_ntid = col.get_notetype_by_name("basic").unwrap().unwrap().id;
|
let basic_ntid = col.get_notetype_by_name("basic").unwrap().unwrap().id;
|
||||||
let mut note = NoteAdder::basic(&mut col).add(&mut col);
|
let mut note = NoteAdder::basic(&mut col).add(&mut col);
|
||||||
note.notetype_id.0 = 123;
|
note.notetype_id.0 = 123;
|
||||||
|
@ -417,7 +416,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_add_note_with_remapped_media_reference_in_field_if_in_media_map() {
|
fn should_add_note_with_remapped_media_reference_in_field_if_in_media_map() {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
let mut note = NoteAdder::basic(&mut col).note();
|
let mut note = NoteAdder::basic(&mut col).note();
|
||||||
note.fields_mut()[0] = "<img src='foo.jpg'>".to_string();
|
note.fields_mut()[0] = "<img src='foo.jpg'>".to_string();
|
||||||
|
|
||||||
|
|
|
@ -14,10 +14,10 @@ use zstd::stream::copy_decode;
|
||||||
use crate::collection::CollectionBuilder;
|
use crate::collection::CollectionBuilder;
|
||||||
use crate::error::FileIoSnafu;
|
use crate::error::FileIoSnafu;
|
||||||
use crate::error::FileOp;
|
use crate::error::FileOp;
|
||||||
use crate::error::ImportError;
|
|
||||||
use crate::import_export::package::media::extract_media_entries;
|
use crate::import_export::package::media::extract_media_entries;
|
||||||
use crate::import_export::package::media::SafeMediaEntry;
|
use crate::import_export::package::media::SafeMediaEntry;
|
||||||
use crate::import_export::package::Meta;
|
use crate::import_export::package::Meta;
|
||||||
|
use crate::import_export::ImportError;
|
||||||
use crate::import_export::ImportProgress;
|
use crate::import_export::ImportProgress;
|
||||||
use crate::import_export::IncrementableProgress;
|
use crate::import_export::IncrementableProgress;
|
||||||
use crate::io::atomic_rename;
|
use crate::io::atomic_rename;
|
||||||
|
|
|
@ -25,9 +25,9 @@ use super::MediaEntry;
|
||||||
use super::Meta;
|
use super::Meta;
|
||||||
use crate::error::FileIoError;
|
use crate::error::FileIoError;
|
||||||
use crate::error::FileOp;
|
use crate::error::FileOp;
|
||||||
use crate::error::ImportError;
|
|
||||||
use crate::error::InvalidInputError;
|
use crate::error::InvalidInputError;
|
||||||
use crate::import_export::package::colpkg::export::MaybeEncodedWriter;
|
use crate::import_export::package::colpkg::export::MaybeEncodedWriter;
|
||||||
|
use crate::import_export::ImportError;
|
||||||
use crate::io::atomic_rename;
|
use crate::io::atomic_rename;
|
||||||
use crate::io::filename_is_safe;
|
use crate::io::filename_is_safe;
|
||||||
use crate::io::new_tempfile_in;
|
use crate::io::new_tempfile_in;
|
||||||
|
|
|
@ -9,7 +9,7 @@ use prost::Message;
|
||||||
use zip::ZipArchive;
|
use zip::ZipArchive;
|
||||||
use zstd::stream::copy_decode;
|
use zstd::stream::copy_decode;
|
||||||
|
|
||||||
use crate::error::ImportError;
|
use crate::import_export::ImportError;
|
||||||
pub(super) use crate::pb::import_export::package_metadata::Version;
|
pub(super) use crate::pb::import_export::package_metadata::Version;
|
||||||
pub(super) use crate::pb::import_export::PackageMetadata as Meta;
|
pub(super) use crate::pb::import_export::PackageMetadata as Meta;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
|
@ -14,8 +14,8 @@ use strum::IntoEnumIterator;
|
||||||
|
|
||||||
use super::import::build_csv_reader;
|
use super::import::build_csv_reader;
|
||||||
use crate::config::I32ConfigKey;
|
use crate::config::I32ConfigKey;
|
||||||
use crate::error::ImportError;
|
|
||||||
use crate::import_export::text::NameOrId;
|
use crate::import_export::text::NameOrId;
|
||||||
|
use crate::import_export::ImportError;
|
||||||
use crate::io::open_file;
|
use crate::io::open_file;
|
||||||
use crate::notetype::NoteField;
|
use crate::notetype::NoteField;
|
||||||
use crate::pb::generic::StringList;
|
use crate::pb::generic::StringList;
|
||||||
|
@ -574,7 +574,6 @@ mod test {
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::collection::open_test_collection;
|
|
||||||
|
|
||||||
macro_rules! metadata {
|
macro_rules! metadata {
|
||||||
($col:expr,$csv:expr) => {
|
($col:expr,$csv:expr) => {
|
||||||
|
@ -604,7 +603,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_detect_deck_by_name_or_id() {
|
fn should_detect_deck_by_name_or_id() {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
let deck_id = col.get_or_create_normal_deck("my deck").unwrap().id.0;
|
let deck_id = col.get_or_create_normal_deck("my deck").unwrap().id.0;
|
||||||
assert_eq!(metadata!(col, "#deck:my deck\n").unwrap_deck_id(), deck_id);
|
assert_eq!(metadata!(col, "#deck:my deck\n").unwrap_deck_id(), deck_id);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -618,7 +617,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_detect_notetype_by_name_or_id() {
|
fn should_detect_notetype_by_name_or_id() {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
let basic_id = col.get_notetype_by_name("Basic").unwrap().unwrap().id.0;
|
let basic_id = col.get_notetype_by_name("Basic").unwrap().unwrap().id.0;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
metadata!(col, "#notetype:Basic\n").unwrap_notetype_id(),
|
metadata!(col, "#notetype:Basic\n").unwrap_notetype_id(),
|
||||||
|
@ -632,7 +631,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_detect_valid_delimiters() {
|
fn should_detect_valid_delimiters() {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
metadata!(col, "#separator:comma\n").delimiter(),
|
metadata!(col, "#separator:comma\n").delimiter(),
|
||||||
Delimiter::Comma
|
Delimiter::Comma
|
||||||
|
@ -661,7 +660,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_enforce_valid_html_flag() {
|
fn should_enforce_valid_html_flag() {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
|
|
||||||
let meta = metadata!(col, "#html:true\n");
|
let meta = metadata!(col, "#html:true\n");
|
||||||
assert!(meta.is_html);
|
assert!(meta.is_html);
|
||||||
|
@ -676,7 +675,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_set_missing_html_flag_by_first_line() {
|
fn should_set_missing_html_flag_by_first_line() {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
|
|
||||||
let meta = metadata!(col, "<br/>\n");
|
let meta = metadata!(col, "<br/>\n");
|
||||||
assert!(meta.is_html);
|
assert!(meta.is_html);
|
||||||
|
@ -690,7 +689,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_detect_old_and_new_style_tags() {
|
fn should_detect_old_and_new_style_tags() {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
assert_eq!(metadata!(col, "tags:foo bar\n").global_tags, ["foo", "bar"]);
|
assert_eq!(metadata!(col, "tags:foo bar\n").global_tags, ["foo", "bar"]);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
metadata!(col, "#tags:foo bar\n").global_tags,
|
metadata!(col, "#tags:foo bar\n").global_tags,
|
||||||
|
@ -708,7 +707,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_detect_column_number_and_names() {
|
fn should_detect_column_number_and_names() {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
// detect from line
|
// detect from line
|
||||||
assert_eq!(metadata!(col, "foo;bar\n").column_labels.len(), 2);
|
assert_eq!(metadata!(col, "foo;bar\n").column_labels.len(), 2);
|
||||||
// detect encoded
|
// detect encoded
|
||||||
|
@ -745,7 +744,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_detect_column_number_despite_escaped_line_breaks() {
|
fn should_detect_column_number_despite_escaped_line_breaks() {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
metadata!(col, "\"foo|\nbar\"\tfoo\tbar\n")
|
metadata!(col, "\"foo|\nbar\"\tfoo\tbar\n")
|
||||||
.column_labels
|
.column_labels
|
||||||
|
@ -765,21 +764,21 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_map_default_notetype_fields_by_index_if_no_column_names() {
|
fn should_map_default_notetype_fields_by_index_if_no_column_names() {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
let meta = metadata!(col, "#deck column:1\nfoo,bar,baz\n");
|
let meta = metadata!(col, "#deck column:1\nfoo,bar,baz\n");
|
||||||
assert_eq!(meta.unwrap_notetype_map(), &[2, 3]);
|
assert_eq!(meta.unwrap_notetype_map(), &[2, 3]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_map_default_notetype_fields_by_given_column_names() {
|
fn should_map_default_notetype_fields_by_given_column_names() {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
let meta = metadata!(col, "#columns:Back\tFront\nfoo,bar,baz\n");
|
let meta = metadata!(col, "#columns:Back\tFront\nfoo,bar,baz\n");
|
||||||
assert_eq!(meta.unwrap_notetype_map(), &[2, 1]);
|
assert_eq!(meta.unwrap_notetype_map(), &[2, 1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_gather_first_lines_into_preview() {
|
fn should_gather_first_lines_into_preview() {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
let meta = metadata!(col, "#separator: \nfoo bar\nbaz<br>\n");
|
let meta = metadata!(col, "#separator: \nfoo bar\nbaz<br>\n");
|
||||||
assert_eq!(meta.preview[0].vals, ["foo", "bar"]);
|
assert_eq!(meta.preview[0].vals, ["foo", "bar"]);
|
||||||
// html is stripped
|
// html is stripped
|
||||||
|
@ -788,7 +787,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_parse_first_first_line_despite_bom() {
|
fn should_parse_first_first_line_despite_bom() {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
metadata!(col, "\u{feff}#separator:tab\n").delimiter(),
|
metadata!(col, "\u{feff}#separator:tab\n").delimiter(),
|
||||||
Delimiter::Tab
|
Delimiter::Tab
|
||||||
|
|
|
@ -630,7 +630,6 @@ impl ForeignTemplate {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::collection::open_test_collection;
|
|
||||||
use crate::tests::DeckAdder;
|
use crate::tests::DeckAdder;
|
||||||
use crate::tests::NoteAdder;
|
use crate::tests::NoteAdder;
|
||||||
|
|
||||||
|
@ -653,7 +652,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_always_add_note_if_dupe_mode_is_add() {
|
fn should_always_add_note_if_dupe_mode_is_add() {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
let mut data = ForeignData::with_defaults();
|
let mut data = ForeignData::with_defaults();
|
||||||
data.add_note(&["same", "old"]);
|
data.add_note(&["same", "old"]);
|
||||||
data.dupe_resolution = DupeResolution::Duplicate;
|
data.dupe_resolution = DupeResolution::Duplicate;
|
||||||
|
@ -665,7 +664,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_add_or_ignore_note_if_dupe_mode_is_ignore() {
|
fn should_add_or_ignore_note_if_dupe_mode_is_ignore() {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
let mut data = ForeignData::with_defaults();
|
let mut data = ForeignData::with_defaults();
|
||||||
data.add_note(&["same", "old"]);
|
data.add_note(&["same", "old"]);
|
||||||
data.dupe_resolution = DupeResolution::Preserve;
|
data.dupe_resolution = DupeResolution::Preserve;
|
||||||
|
@ -682,7 +681,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_update_or_add_note_if_dupe_mode_is_update() {
|
fn should_update_or_add_note_if_dupe_mode_is_update() {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
let mut data = ForeignData::with_defaults();
|
let mut data = ForeignData::with_defaults();
|
||||||
data.add_note(&["same", "old"]);
|
data.add_note(&["same", "old"]);
|
||||||
data.dupe_resolution = DupeResolution::Update;
|
data.dupe_resolution = DupeResolution::Update;
|
||||||
|
@ -697,7 +696,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_keep_old_field_content_if_no_new_one_is_supplied() {
|
fn should_keep_old_field_content_if_no_new_one_is_supplied() {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
let mut data = ForeignData::with_defaults();
|
let mut data = ForeignData::with_defaults();
|
||||||
data.add_note(&["same", "unchanged"]);
|
data.add_note(&["same", "unchanged"]);
|
||||||
data.add_note(&["same", "unchanged"]);
|
data.add_note(&["same", "unchanged"]);
|
||||||
|
@ -716,7 +715,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_recognize_normalized_duplicate_only_if_normalization_is_enabled() {
|
fn should_recognize_normalized_duplicate_only_if_normalization_is_enabled() {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
NoteAdder::basic(&mut col)
|
NoteAdder::basic(&mut col)
|
||||||
.fields(&["神", "old"])
|
.fields(&["神", "old"])
|
||||||
.add(&mut col);
|
.add(&mut col);
|
||||||
|
@ -737,7 +736,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_add_global_tags() {
|
fn should_add_global_tags() {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
let mut data = ForeignData::with_defaults();
|
let mut data = ForeignData::with_defaults();
|
||||||
data.add_note(&["foo"]);
|
data.add_note(&["foo"]);
|
||||||
data.notes[0].tags.replace(vec![String::from("bar")]);
|
data.notes[0].tags.replace(vec![String::from("bar")]);
|
||||||
|
@ -749,7 +748,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_match_note_with_same_guid() {
|
fn should_match_note_with_same_guid() {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
let mut data = ForeignData::with_defaults();
|
let mut data = ForeignData::with_defaults();
|
||||||
data.add_note(&["foo"]);
|
data.add_note(&["foo"]);
|
||||||
data.notes[0].tags.replace(vec![String::from("bar")]);
|
data.notes[0].tags.replace(vec![String::from("bar")]);
|
||||||
|
@ -761,7 +760,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_only_update_duplicates_in_same_deck_if_limit_is_enabled() {
|
fn should_only_update_duplicates_in_same_deck_if_limit_is_enabled() {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
let other_deck_id = DeckAdder::new("other").add(&mut col).id;
|
let other_deck_id = DeckAdder::new("other").add(&mut col).id;
|
||||||
NoteAdder::basic(&mut col)
|
NoteAdder::basic(&mut col)
|
||||||
.fields(&["foo", "old"])
|
.fields(&["foo", "old"])
|
||||||
|
|
|
@ -631,7 +631,6 @@ fn note_differs_from_db(existing_note: &mut Note, note: &mut Note) -> bool {
|
||||||
mod test {
|
mod test {
|
||||||
use super::anki_base91;
|
use super::anki_base91;
|
||||||
use super::field_checksum;
|
use super::field_checksum;
|
||||||
use crate::collection::open_test_collection;
|
|
||||||
use crate::config::BoolKey;
|
use crate::config::BoolKey;
|
||||||
use crate::decks::DeckId;
|
use crate::decks::DeckId;
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
|
@ -655,7 +654,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn adding_cards() -> Result<()> {
|
fn adding_cards() -> Result<()> {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
let nt = col
|
let nt = col
|
||||||
.get_notetype_by_name("basic (and reversed card)")?
|
.get_notetype_by_name("basic (and reversed card)")?
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -703,7 +702,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn normalization() -> Result<()> {
|
fn normalization() -> Result<()> {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
|
|
||||||
let nt = col.get_notetype_by_name("Basic")?.unwrap();
|
let nt = col.get_notetype_by_name("Basic")?.unwrap();
|
||||||
let mut note = nt.new_note();
|
let mut note = nt.new_note();
|
||||||
|
@ -735,7 +734,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn undo() -> Result<()> {
|
fn undo() -> Result<()> {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
let nt = col
|
let nt = col
|
||||||
.get_notetype_by_name("basic (and reversed card)")?
|
.get_notetype_by_name("basic (and reversed card)")?
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
|
@ -374,12 +374,11 @@ fn remap_fields(fields: &mut Vec<String>, new_fields: &[Option<usize>]) {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::collection::open_test_collection;
|
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn field_map() -> Result<()> {
|
fn field_map() -> Result<()> {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
let mut basic = col
|
let mut basic = col
|
||||||
.storage
|
.storage
|
||||||
.get_notetype(col.get_current_notetype_id().unwrap())?
|
.get_notetype(col.get_current_notetype_id().unwrap())?
|
||||||
|
@ -446,7 +445,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn basic() -> Result<()> {
|
fn basic() -> Result<()> {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
let basic = col.get_notetype_by_name("Basic")?.unwrap();
|
let basic = col.get_notetype_by_name("Basic")?.unwrap();
|
||||||
let mut note = basic.new_note();
|
let mut note = basic.new_note();
|
||||||
note.set_field(0, "1")?;
|
note.set_field(0, "1")?;
|
||||||
|
@ -482,7 +481,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn field_count_change() -> Result<()> {
|
fn field_count_change() -> Result<()> {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
let basic = col.get_notetype_by_name("Basic")?.unwrap();
|
let basic = col.get_notetype_by_name("Basic")?.unwrap();
|
||||||
let mut note = basic.new_note();
|
let mut note = basic.new_note();
|
||||||
note.set_field(0, "1")?;
|
note.set_field(0, "1")?;
|
||||||
|
@ -503,7 +502,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cloze() -> Result<()> {
|
fn cloze() -> Result<()> {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
let basic = col
|
let basic = col
|
||||||
.get_notetype_by_name("Basic (and reversed card)")?
|
.get_notetype_by_name("Basic (and reversed card)")?
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
|
@ -194,11 +194,7 @@ impl Notetype {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::ords_changed;
|
use super::*;
|
||||||
use super::TemplateOrdChanges;
|
|
||||||
use crate::collection::open_test_collection;
|
|
||||||
use crate::decks::DeckId;
|
|
||||||
use crate::error::Result;
|
|
||||||
use crate::search::SortMode;
|
use crate::search::SortMode;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -258,7 +254,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn fields() -> Result<()> {
|
fn fields() -> Result<()> {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
let mut nt = col
|
let mut nt = col
|
||||||
.storage
|
.storage
|
||||||
.get_notetype(col.get_current_notetype_id().unwrap())?
|
.get_notetype(col.get_current_notetype_id().unwrap())?
|
||||||
|
@ -286,7 +282,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn field_renaming_and_deleting() -> Result<()> {
|
fn field_renaming_and_deleting() -> Result<()> {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
let mut nt = col
|
let mut nt = col
|
||||||
.storage
|
.storage
|
||||||
.get_notetype(col.get_current_notetype_id().unwrap())?
|
.get_notetype(col.get_current_notetype_id().unwrap())?
|
||||||
|
@ -307,7 +303,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cards() -> Result<()> {
|
fn cards() -> Result<()> {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
let mut nt = col
|
let mut nt = col
|
||||||
.storage
|
.storage
|
||||||
.get_notetype(col.get_current_notetype_id().unwrap())?
|
.get_notetype(col.get_current_notetype_id().unwrap())?
|
||||||
|
|
|
@ -447,7 +447,6 @@ fn get_fuzz_factor(seed: Option<u64>) -> Option<f32> {
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::card::CardType;
|
use crate::card::CardType;
|
||||||
use crate::collection::open_test_collection;
|
|
||||||
use crate::deckconfig::ReviewMix;
|
use crate::deckconfig::ReviewMix;
|
||||||
use crate::search::SortMode;
|
use crate::search::SortMode;
|
||||||
|
|
||||||
|
@ -459,7 +458,7 @@ mod test {
|
||||||
// state we applied to it
|
// state we applied to it
|
||||||
#[test]
|
#[test]
|
||||||
fn state_application() -> Result<()> {
|
fn state_application() -> Result<()> {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
if col.timing_today()?.near_cutoff() {
|
if col.timing_today()?.near_cutoff() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
@ -574,7 +573,7 @@ mod test {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn v3_test_collection(cards: usize) -> Result<(Collection, Vec<CardId>)> {
|
fn v3_test_collection(cards: usize) -> Result<(Collection, Vec<CardId>)> {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
let nt = col.get_notetype_by_name("Basic")?.unwrap();
|
let nt = col.get_notetype_by_name("Basic")?.unwrap();
|
||||||
for _ in 0..cards {
|
for _ in 0..cards {
|
||||||
let mut note = Note::new(&nt);
|
let mut note = Note::new(&nt);
|
||||||
|
|
|
@ -42,7 +42,6 @@ impl CardStateUpdater {
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::card::CardType;
|
use crate::card::CardType;
|
||||||
use crate::collection::open_test_collection;
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::scheduler::answering::CardAnswer;
|
use crate::scheduler::answering::CardAnswer;
|
||||||
use crate::scheduler::answering::Rating;
|
use crate::scheduler::answering::Rating;
|
||||||
|
@ -52,7 +51,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn preview() -> Result<()> {
|
fn preview() -> Result<()> {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
let mut c = Card {
|
let mut c = Card {
|
||||||
deck_id: DeckId(1),
|
deck_id: DeckId(1),
|
||||||
ctype: CardType::Learn,
|
ctype: CardType::Learn,
|
||||||
|
|
|
@ -175,14 +175,13 @@ impl CardQueue {
|
||||||
mod test {
|
mod test {
|
||||||
use crate::card::Card;
|
use crate::card::Card;
|
||||||
use crate::card::CardQueue;
|
use crate::card::CardQueue;
|
||||||
use crate::collection::open_test_collection;
|
|
||||||
use crate::collection::Collection;
|
use crate::collection::Collection;
|
||||||
use crate::search::SortMode;
|
use crate::search::SortMode;
|
||||||
use crate::search::StateKind;
|
use crate::search::StateKind;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn unbury() {
|
fn unbury() {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
let mut card = Card {
|
let mut card = Card {
|
||||||
queue: CardQueue::UserBuried,
|
queue: CardQueue::UserBuried,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|
|
@ -44,11 +44,11 @@ impl Collection {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use crate::collection::open_test_collection;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty() {
|
fn empty() {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
let info = col.congrats_info().unwrap();
|
let info = col.congrats_info().unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
info,
|
info,
|
||||||
|
|
|
@ -299,7 +299,6 @@ fn tags_to_nodes(tags_to_include: &[String], tags_to_exclude: &[String]) -> Sear
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::collection::open_test_collection;
|
|
||||||
use crate::pb::scheduler::custom_study_request::cram::CramKind;
|
use crate::pb::scheduler::custom_study_request::cram::CramKind;
|
||||||
use crate::pb::scheduler::custom_study_request::Cram;
|
use crate::pb::scheduler::custom_study_request::Cram;
|
||||||
use crate::pb::scheduler::custom_study_request::Value;
|
use crate::pb::scheduler::custom_study_request::Value;
|
||||||
|
@ -307,7 +306,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tag_remembering() -> Result<()> {
|
fn tag_remembering() -> Result<()> {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
|
|
||||||
let nt = col.get_notetype_by_name("Basic")?.unwrap();
|
let nt = col.get_notetype_by_name("Basic")?.unwrap();
|
||||||
let mut note = nt.new_note();
|
let mut note = nt.new_note();
|
||||||
|
|
|
@ -270,7 +270,6 @@ mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::card::CardQueue;
|
use crate::card::CardQueue;
|
||||||
use crate::card::CardType;
|
use crate::card::CardType;
|
||||||
use crate::collection::open_test_collection;
|
|
||||||
use crate::pb::deckconfig::deck_config::config::NewCardGatherPriority;
|
use crate::pb::deckconfig::deck_config::config::NewCardGatherPriority;
|
||||||
use crate::pb::deckconfig::deck_config::config::NewCardSortOrder;
|
use crate::pb::deckconfig::deck_config::config::NewCardSortOrder;
|
||||||
|
|
||||||
|
@ -346,9 +345,9 @@ mod test {
|
||||||
// ┣━━child━━grandchild
|
// ┣━━child━━grandchild
|
||||||
// ┗━━child_2
|
// ┗━━child_2
|
||||||
let mut parent = DeckAdder::new("parent").add(&mut col);
|
let mut parent = DeckAdder::new("parent").add(&mut col);
|
||||||
let mut child = DeckAdder::new("parent\x1fchild").add(&mut col);
|
let mut child = DeckAdder::new("parent::child").add(&mut col);
|
||||||
let child_2 = DeckAdder::new("parent\x1fchild_2").add(&mut col);
|
let child_2 = DeckAdder::new("parent::child_2").add(&mut col);
|
||||||
let grandchild = DeckAdder::new("parent\x1fchild\x1fgrandchild").add(&mut col);
|
let grandchild = DeckAdder::new("parent::child::grandchild").add(&mut col);
|
||||||
|
|
||||||
// add 2 new cards to each deck
|
// add 2 new cards to each deck
|
||||||
for deck in [&parent, &child, &child_2, &grandchild] {
|
for deck in [&parent, &child, &child_2, &grandchild] {
|
||||||
|
@ -402,7 +401,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn review_queue_building() -> Result<()> {
|
fn review_queue_building() -> Result<()> {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
col.set_config_bool(BoolKey::Sched2021, true, false)?;
|
col.set_config_bool(BoolKey::Sched2021, true, false)?;
|
||||||
|
|
||||||
let mut deck = col.get_or_create_normal_deck("Default").unwrap();
|
let mut deck = col.get_or_create_normal_deck("Default").unwrap();
|
||||||
|
|
|
@ -66,7 +66,6 @@ impl Collection {
|
||||||
mod test {
|
mod test {
|
||||||
use crate::card::CardQueue;
|
use crate::card::CardQueue;
|
||||||
use crate::card::CardType;
|
use crate::card::CardType;
|
||||||
use crate::collection::open_test_collection;
|
|
||||||
use crate::deckconfig::LeechAction;
|
use crate::deckconfig::LeechAction;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
@ -86,7 +85,7 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn undo() -> Result<()> {
|
fn undo() -> Result<()> {
|
||||||
// add a note
|
// add a note
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
let nid = add_note(&mut col, true)?;
|
let nid = add_note(&mut col, true)?;
|
||||||
|
|
||||||
// turn burying and leech suspension on
|
// turn burying and leech suspension on
|
||||||
|
@ -193,7 +192,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn undo_counts() -> Result<()> {
|
fn undo_counts() -> Result<()> {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
if col.timing_today()?.near_cutoff() {
|
if col.timing_today()?.near_cutoff() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
@ -247,7 +246,7 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn redo_after_queue_invalidation_bug() -> Result<()> {
|
fn redo_after_queue_invalidation_bug() -> Result<()> {
|
||||||
// add a note to the default deck
|
// add a note to the default deck
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
let _nid = add_note(&mut col, true)?;
|
let _nid = add_note(&mut col, true)?;
|
||||||
|
|
||||||
// add a deck and select it
|
// add a deck and select it
|
||||||
|
|
|
@ -375,7 +375,7 @@ impl SqlWriter<'_> {
|
||||||
.as_native_str(),
|
.as_native_str(),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
NativeDeckName::from_human_name(&to_re(deck))
|
NativeDeckName::from_human_name(to_re(deck))
|
||||||
.as_native_str()
|
.as_native_str()
|
||||||
.to_string()
|
.to_string()
|
||||||
};
|
};
|
||||||
|
|
|
@ -106,12 +106,11 @@ fn stats_revlog_entry(entry: &RevlogEntry) -> pb::stats::card_stats_response::St
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::collection::open_test_collection;
|
|
||||||
use crate::search::SortMode;
|
use crate::search::SortMode;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn stats() -> Result<()> {
|
fn stats() -> Result<()> {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
|
|
||||||
let nt = col.get_notetype_by_name("Basic")?.unwrap();
|
let nt = col.get_notetype_by_name("Basic")?.unwrap();
|
||||||
let mut note = nt.new_note();
|
let mut note = nt.new_note();
|
||||||
|
|
13
rslib/src/storage/dbcheck/invalid_ids_count.sql
Normal file
13
rslib/src/storage/dbcheck/invalid_ids_count.sql
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
SELECT (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM notes
|
||||||
|
WHERE id > :cutoff
|
||||||
|
) + (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM cards
|
||||||
|
WHERE id > :cutoff
|
||||||
|
) + (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM revlog
|
||||||
|
WHERE id > :cutoff
|
||||||
|
);
|
27
rslib/src/storage/dbcheck/invalid_ids_create.sql
Normal file
27
rslib/src/storage/dbcheck/invalid_ids_create.sql
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
DROP TABLE IF EXISTS invalid_ids;
|
||||||
|
CREATE TEMPORARY TABLE invalid_ids AS WITH max_existing_valid_id AS (
|
||||||
|
SELECT coalesce(max(id), 0) AS max_id
|
||||||
|
FROM "{source_table}"
|
||||||
|
WHERE id <= "{max_valid_id}"
|
||||||
|
),
|
||||||
|
first_new_id AS (
|
||||||
|
SELECT CASE
|
||||||
|
WHEN "{new_id}" > (
|
||||||
|
SELECT max_id
|
||||||
|
FROM max_existing_valid_id
|
||||||
|
) THEN "{new_id}"
|
||||||
|
ELSE (
|
||||||
|
SELECT max_id
|
||||||
|
FROM max_existing_valid_id
|
||||||
|
) + 1
|
||||||
|
END AS id
|
||||||
|
)
|
||||||
|
SELECT id,
|
||||||
|
(
|
||||||
|
SELECT id
|
||||||
|
FROM first_new_id
|
||||||
|
) + row_number() OVER (
|
||||||
|
ORDER BY id
|
||||||
|
) - 1 AS new_id
|
||||||
|
FROM "{source_table}"
|
||||||
|
WHERE id > "{max_valid_id}";
|
1
rslib/src/storage/dbcheck/invalid_ids_drop.sql
Normal file
1
rslib/src/storage/dbcheck/invalid_ids_drop.sql
Normal file
|
@ -0,0 +1 @@
|
||||||
|
DROP TABLE IF EXISTS invalid_ids;
|
11
rslib/src/storage/dbcheck/invalid_ids_update.sql
Normal file
11
rslib/src/storage/dbcheck/invalid_ids_update.sql
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
UPDATE "{target_table}"
|
||||||
|
SET "{id_column}" = (
|
||||||
|
SELECT invalid_ids.new_id
|
||||||
|
FROM invalid_ids
|
||||||
|
WHERE invalid_ids.id = "{target_table}"."{id_column}"
|
||||||
|
LIMIT 1
|
||||||
|
)
|
||||||
|
WHERE "{target_table}"."{id_column}" IN (
|
||||||
|
SELECT invalid_ids.id
|
||||||
|
FROM invalid_ids
|
||||||
|
);
|
115
rslib/src/storage/dbcheck/mod.rs
Normal file
115
rslib/src/storage/dbcheck/mod.rs
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
impl super::SqliteStorage {
|
||||||
|
/// True if any ids used as timestamps are larger than `cutoff`.
|
||||||
|
pub(crate) fn invalid_ids(&self, cutoff: i64) -> Result<usize> {
|
||||||
|
Ok(self
|
||||||
|
.db
|
||||||
|
.query_row_and_then(include_str!("invalid_ids_count.sql"), [cutoff], |r| {
|
||||||
|
r.get(0)
|
||||||
|
})?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensures all ids used as timestamps are `max_valid_id` or lower.
|
||||||
|
/// If not, new ids will be assigned starting at whichever is larger,
|
||||||
|
/// `new_id` or the next free valid id.
|
||||||
|
/// `new_id` must be a valid id, i.e. lower or equal to `max_valid_id`.
|
||||||
|
pub(crate) fn fix_invalid_ids(&self, max_valid_id: i64, new_id: i64) -> Result<()> {
|
||||||
|
require!(new_id <= max_valid_id, "new_id is invalid");
|
||||||
|
for (source_table, foreign_table) in [
|
||||||
|
("notes", Some(("cards", "nid"))),
|
||||||
|
("cards", Some(("revlog", "cid"))),
|
||||||
|
("revlog", None),
|
||||||
|
] {
|
||||||
|
self.setup_invalid_ids_table(source_table, max_valid_id, new_id)?;
|
||||||
|
self.update_invalid_ids_from_table(source_table, "id")?;
|
||||||
|
if let Some((target_table, id_column)) = foreign_table {
|
||||||
|
self.update_invalid_ids_from_table(target_table, id_column)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.db.execute(include_str!("invalid_ids_drop.sql"), [])?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_invalid_ids_table(
|
||||||
|
&self,
|
||||||
|
source_table: &str,
|
||||||
|
max_valid_id: i64,
|
||||||
|
new_id: i64,
|
||||||
|
) -> Result<()> {
|
||||||
|
self.db.execute_batch(&format!(
|
||||||
|
include_str!("invalid_ids_create.sql"),
|
||||||
|
source_table = source_table,
|
||||||
|
max_valid_id = max_valid_id,
|
||||||
|
new_id = new_id,
|
||||||
|
))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fix the invalid ids in `id_column` of `target_table` using the map from
|
||||||
|
/// the invalid ids temporary table.
|
||||||
|
fn update_invalid_ids_from_table(&self, target_table: &str, id_column: &str) -> Result<()> {
|
||||||
|
self.db.execute_batch(&format!(
|
||||||
|
include_str!("invalid_ids_update.sql"),
|
||||||
|
target_table = target_table,
|
||||||
|
id_column = id_column,
|
||||||
|
))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn any_invalid_ids() {
|
||||||
|
let mut col = Collection::new();
|
||||||
|
assert_eq!(col.storage.invalid_ids(0).unwrap(), 0);
|
||||||
|
NoteAdder::basic(&mut col).add(&mut col);
|
||||||
|
// 1 card and 1 note
|
||||||
|
assert_eq!(col.storage.invalid_ids(0).unwrap(), 2);
|
||||||
|
assert_eq!(
|
||||||
|
col.storage.invalid_ids(TimestampMillis::now().0).unwrap(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fix_invalid_note_ids_only_and_update_cards() {
|
||||||
|
let mut col = Collection::new();
|
||||||
|
let valid = NoteAdder::basic(&mut col).add(&mut col);
|
||||||
|
NoteAdder::basic(&mut col).add(&mut col);
|
||||||
|
col.storage.fix_invalid_ids(valid.id.0, 42).unwrap();
|
||||||
|
assert_eq!(col.storage.all_cards_of_note(valid.id).unwrap().len(), 1);
|
||||||
|
assert_eq!(col.storage.all_cards_of_note(NoteId(42)).unwrap().len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fix_invalid_card_ids_only() {
|
||||||
|
let mut col = Collection::new();
|
||||||
|
let mut cards = CardAdder::new().siblings(3).add(&mut col);
|
||||||
|
col.storage.fix_invalid_ids(cards[0].id.0, 42).unwrap();
|
||||||
|
cards.sort_by(|c1, c2| c1.id.cmp(&c2.id));
|
||||||
|
cards[1].id.0 = 42;
|
||||||
|
cards[2].id.0 = 43;
|
||||||
|
let old_first_card = cards.remove(0);
|
||||||
|
cards.push(old_first_card);
|
||||||
|
let mut new_cards = col.storage.get_all_cards();
|
||||||
|
new_cards.sort_by(|c1, c2| c1.id.cmp(&c2.id));
|
||||||
|
assert_eq!(new_cards, cards);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn update_revlog_when_fixing_card_ids() {
|
||||||
|
let mut col = Collection::new();
|
||||||
|
CardAdder::new().due_dates(["7"]).add(&mut col);
|
||||||
|
col.storage.fix_invalid_ids(42, 42).unwrap();
|
||||||
|
// revlog id was also reset to 42
|
||||||
|
let revlog_entry = col.storage.get_revlog_entry(RevlogId(42)).unwrap().unwrap();
|
||||||
|
assert_eq!(revlog_entry.cid.0, 42);
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,7 @@
|
||||||
pub(crate) mod card;
|
pub(crate) mod card;
|
||||||
mod collection_timestamps;
|
mod collection_timestamps;
|
||||||
mod config;
|
mod config;
|
||||||
|
mod dbcheck;
|
||||||
mod deck;
|
mod deck;
|
||||||
mod deckconfig;
|
mod deckconfig;
|
||||||
mod graves;
|
mod graves;
|
||||||
|
|
|
@ -101,12 +101,11 @@ where
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::collection::open_test_collection;
|
|
||||||
use crate::decks::DeckId;
|
use crate::decks::DeckId;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn find_replace() -> Result<()> {
|
fn find_replace() -> Result<()> {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
let nt = col.get_notetype_by_name("Basic")?.unwrap();
|
let nt = col.get_notetype_by_name("Basic")?.unwrap();
|
||||||
let mut note = nt.new_note();
|
let mut note = nt.new_note();
|
||||||
note.tags.push("test".into());
|
note.tags.push("test".into());
|
||||||
|
|
|
@ -195,12 +195,11 @@ pub(super) fn normalize_tag_name(name: &str) -> Result<Cow<str>> {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::collection::open_test_collection;
|
|
||||||
use crate::decks::DeckId;
|
use crate::decks::DeckId;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tags() -> Result<()> {
|
fn tags() -> Result<()> {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
let nt = col.get_notetype_by_name("Basic")?.unwrap();
|
let nt = col.get_notetype_by_name("Basic")?.unwrap();
|
||||||
let mut note = nt.new_note();
|
let mut note = nt.new_note();
|
||||||
col.add_note(&mut note, DeckId(1))?;
|
col.add_note(&mut note, DeckId(1))?;
|
||||||
|
|
|
@ -97,12 +97,11 @@ impl Collection {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::collection::open_test_collection;
|
|
||||||
use crate::tags::Tag;
|
use crate::tags::Tag;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn clearing() -> Result<()> {
|
fn clearing() -> Result<()> {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
let nt = col.get_notetype_by_name("Basic")?.unwrap();
|
let nt = col.get_notetype_by_name("Basic")?.unwrap();
|
||||||
let mut note = nt.new_note();
|
let mut note = nt.new_note();
|
||||||
note.tags.push("one".into());
|
note.tags.push("one".into());
|
||||||
|
|
|
@ -114,7 +114,6 @@ fn reparented_name(existing_name: &str, new_parent: Option<&str>) -> Option<Stri
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::collection::open_test_collection;
|
|
||||||
|
|
||||||
fn alltags(col: &Collection) -> Vec<String> {
|
fn alltags(col: &Collection) -> Vec<String> {
|
||||||
col.storage
|
col.storage
|
||||||
|
@ -127,7 +126,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dragdrop() -> Result<()> {
|
fn dragdrop() -> Result<()> {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
let nt = col.get_notetype_by_name("Basic")?.unwrap();
|
let nt = col.get_notetype_by_name("Basic")?.unwrap();
|
||||||
for tag in &[
|
for tag in &[
|
||||||
"a",
|
"a",
|
||||||
|
|
|
@ -124,7 +124,6 @@ fn add_tag_and_missing_parents<'a, 'b>(
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::collection::open_test_collection;
|
|
||||||
|
|
||||||
fn node(name: &str, level: u32, children: Vec<TagTreeNode>) -> TagTreeNode {
|
fn node(name: &str, level: u32, children: Vec<TagTreeNode>) -> TagTreeNode {
|
||||||
TagTreeNode {
|
TagTreeNode {
|
||||||
|
@ -141,7 +140,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tree() -> Result<()> {
|
fn tree() -> Result<()> {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
let nt = col.get_notetype_by_name("Basic")?.unwrap();
|
let nt = col.get_notetype_by_name("Basic")?.unwrap();
|
||||||
let mut note = nt.new_note();
|
let mut note = nt.new_note();
|
||||||
note.tags.push("foo::bar::a".into());
|
note.tags.push("foo::bar::a".into());
|
||||||
|
|
|
@ -8,7 +8,6 @@ use itertools::Itertools;
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
use crate::collection::open_test_collection;
|
|
||||||
use crate::collection::CollectionBuilder;
|
use crate::collection::CollectionBuilder;
|
||||||
use crate::deckconfig::DeckConfigInner;
|
use crate::deckconfig::DeckConfigInner;
|
||||||
use crate::io::create_dir;
|
use crate::io::create_dir;
|
||||||
|
@ -28,14 +27,14 @@ pub(crate) fn open_fs_test_collection(name: &str) -> (Collection, TempDir) {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn open_test_collection_with_learning_card() -> Collection {
|
pub(crate) fn open_test_collection_with_learning_card() -> Collection {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
NoteAdder::basic(&mut col).add(&mut col);
|
NoteAdder::basic(&mut col).add(&mut col);
|
||||||
col.answer_again();
|
col.answer_again();
|
||||||
col
|
col
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn open_test_collection_with_relearning_card() -> Collection {
|
pub(crate) fn open_test_collection_with_relearning_card() -> Collection {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
NoteAdder::basic(&mut col).add(&mut col);
|
NoteAdder::basic(&mut col).add(&mut col);
|
||||||
col.answer_easy();
|
col.answer_easy();
|
||||||
col.storage
|
col.storage
|
||||||
|
@ -48,8 +47,12 @@ pub(crate) fn open_test_collection_with_relearning_card() -> Collection {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Collection {
|
impl Collection {
|
||||||
|
pub(crate) fn new() -> Collection {
|
||||||
|
CollectionBuilder::default().build().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn new_v3() -> Collection {
|
pub(crate) fn new_v3() -> Collection {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
col.set_config_bool(BoolKey::Sched2021, true, false)
|
col.set_config_bool(BoolKey::Sched2021, true, false)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
col
|
col
|
||||||
|
@ -109,9 +112,9 @@ pub(crate) struct DeckAdder {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DeckAdder {
|
impl DeckAdder {
|
||||||
pub(crate) fn new(machine_name: impl Into<String>) -> Self {
|
pub(crate) fn new(human_name: impl AsRef<str>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
name: NativeDeckName::from_native_str(machine_name),
|
name: NativeDeckName::from_human_name(human_name),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -332,12 +332,11 @@ impl From<&[UndoableChange]> for StateChanges {
|
||||||
mod test {
|
mod test {
|
||||||
use super::UndoableChange;
|
use super::UndoableChange;
|
||||||
use crate::card::Card;
|
use crate::card::Card;
|
||||||
use crate::collection::open_test_collection;
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn undo() -> Result<()> {
|
fn undo() -> Result<()> {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
|
|
||||||
let mut card = Card {
|
let mut card = Card {
|
||||||
interval: 1,
|
interval: 1,
|
||||||
|
@ -445,7 +444,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn custom() -> Result<()> {
|
fn custom() -> Result<()> {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
|
|
||||||
// perform some actions in separate steps
|
// perform some actions in separate steps
|
||||||
let nt = col.get_notetype_by_name("Basic")?.unwrap();
|
let nt = col.get_notetype_by_name("Basic")?.unwrap();
|
||||||
|
@ -509,7 +508,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn undo_mtime_bump() -> Result<()> {
|
fn undo_mtime_bump() -> Result<()> {
|
||||||
let mut col = open_test_collection();
|
let mut col = Collection::new();
|
||||||
col.storage.db.execute_batch("update col set mod = 0")?;
|
col.storage.db.execute_batch("update col set mod = 0")?;
|
||||||
|
|
||||||
// a no-op change should not bump mtime
|
// a no-op change should not bump mtime
|
||||||
|
|
Loading…
Reference in a new issue