diff --git a/proto/backend.proto b/proto/backend.proto index b66b47db8..7be3903ea 100644 --- a/proto/backend.proto +++ b/proto/backend.proto @@ -793,3 +793,50 @@ message GetDeckNamesIn { // if unset, implies skip_empty_default bool include_filtered = 2; } + +message DeckConfigInner { + enum NewCardOrder { + NEW_CARD_ORDER_DUE = 0; + NEW_CARD_ORDER_RANDOM = 1; + } + + enum LeechAction { + LEECH_ACTION_SUSPEND = 0; + LEECH_ACTION_TAG_ONLY = 1; + } + + repeated float learn_steps = 1; + repeated float relearn_steps = 2; + + reserved 3 to 8; + + uint32 new_per_day = 9; + uint32 reviews_per_day = 10; + + float initial_ease = 11; + float easy_multiplier = 12; + float hard_multiplier = 13; + float lapse_multiplier = 14; + float interval_multiplier = 15; + + uint32 maximum_review_interval = 16; + uint32 minimum_review_interval = 17; + + uint32 graduating_interval_good = 18; + uint32 graduating_interval_easy = 19; + + NewCardOrder new_card_order = 20; + + LeechAction leech_action = 21; + uint32 leech_threshold = 22; + + bool disable_autoplay = 23; + uint32 cap_answer_time_to_secs = 24; + uint32 visible_timer_secs = 25; + bool skip_question_when_replaying_answer = 26; + + bool bury_new = 27; + bool bury_reviews = 28; + + bytes other = 255; +} diff --git a/pylib/anki/rsbackend.py b/pylib/anki/rsbackend.py index 8bde0a458..b95be4f33 100644 --- a/pylib/anki/rsbackend.py +++ b/pylib/anki/rsbackend.py @@ -730,7 +730,11 @@ class RustBackend: def deck_tree(self, include_counts: bool, top_deck_id: int = 0) -> DeckTreeNode: return self._run_command( - pb.BackendInput(deck_tree=pb.DeckTreeIn(include_counts=include_counts, top_deck_id=top_deck_id)) + pb.BackendInput( + deck_tree=pb.DeckTreeIn( + include_counts=include_counts, top_deck_id=top_deck_id + ) + ) ).deck_tree def check_database(self) -> List[str]: diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 2a98e2316..13b77052f 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -11,7 +11,7 @@ use crate::{ cloze::add_cloze_numbers_in_string, collection::{open_collection, Collection}, config::SortKind, - deckconf::{DeckConf, DeckConfID}, + deckconf::{DeckConf, DeckConfID, DeckConfSchema11}, decks::{Deck, DeckID, DeckSchema11}, err::{AnkiError, NetworkErrorKind, Result, SyncErrorKind}, i18n::{tr_args, I18n, TR}, @@ -758,12 +758,14 @@ impl Backend { fn get_deck_config(&self, dcid: i64) -> Result> { self.with_col(|col| { let conf = col.get_deck_config(DeckConfID(dcid), true)?.unwrap(); + let conf: DeckConfSchema11 = conf.into(); Ok(serde_json::to_vec(&conf)?) }) } fn add_or_update_deck_config(&self, input: AddOrUpdateDeckConfigIn) -> Result { - let mut conf: DeckConf = serde_json::from_slice(&input.config)?; + let conf: DeckConfSchema11 = serde_json::from_slice(&input.config)?; + let mut conf: DeckConf = conf.into(); self.with_col(|col| { col.transact(None, |col| { col.add_or_update_deck_config(&mut conf, input.preserve_usn_and_mtime)?; @@ -773,11 +775,19 @@ impl Backend { } fn all_deck_config(&self) -> Result> { - self.with_col(|col| serde_json::to_vec(&col.storage.all_deck_config()?).map_err(Into::into)) + self.with_col(|col| { + let conf: Vec = col + .storage + .all_deck_config()? + .into_iter() + .map(Into::into) + .collect(); + serde_json::to_vec(&conf).map_err(Into::into) + }) } fn new_deck_config(&self) -> Result> { - serde_json::to_vec(&DeckConf::default()).map_err(Into::into) + serde_json::to_vec(&DeckConfSchema11::default()).map_err(Into::into) } fn remove_deck_config(&self, dcid: i64) -> Result<()> { diff --git a/rslib/src/deckconf.rs b/rslib/src/deckconf.rs deleted file mode 100644 index a7a6bd754..000000000 --- a/rslib/src/deckconf.rs +++ /dev/null @@ -1,246 +0,0 @@ -// Copyright: Ankitects Pty Ltd and contributors -// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - -use crate::{ - collection::Collection, - define_newtype, - err::{AnkiError, Result}, - serde::default_on_invalid, - timestamp::{TimestampMillis, TimestampSecs}, - types::Usn, -}; -use serde_aux::field_attributes::deserialize_number_from_string; -use serde_derive::{Deserialize, Serialize}; -use serde_json::Value; -use serde_repr::{Deserialize_repr, Serialize_repr}; -use serde_tuple::Serialize_tuple; -use std::collections::HashMap; - -define_newtype!(DeckConfID, i64); - -#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] -#[serde(rename_all = "camelCase")] -pub struct DeckConf { - #[serde(deserialize_with = "deserialize_number_from_string")] - pub(crate) id: DeckConfID, - #[serde(rename = "mod", deserialize_with = "deserialize_number_from_string")] - pub(crate) mtime: TimestampSecs, - pub(crate) name: String, - pub(crate) usn: Usn, - max_taken: i32, - autoplay: bool, - #[serde(deserialize_with = "default_on_invalid")] - timer: u8, - #[serde(default)] - replayq: bool, - pub(crate) new: NewConf, - pub(crate) rev: RevConf, - pub(crate) lapse: LapseConf, - #[serde(flatten)] - other: HashMap, -} - -#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] -#[serde(rename_all = "camelCase")] -pub struct NewConf { - #[serde(default)] - bury: bool, - #[serde(deserialize_with = "default_on_invalid")] - delays: Vec, - initial_factor: u16, - #[serde(deserialize_with = "default_on_invalid")] - ints: NewCardIntervals, - #[serde(deserialize_with = "default_on_invalid")] - pub(crate) order: NewCardOrder, - #[serde(deserialize_with = "default_on_invalid")] - pub(crate) per_day: u32, - - #[serde(flatten)] - other: HashMap, -} - -#[derive(Serialize_tuple, Deserialize, Debug, PartialEq, Clone)] -pub struct NewCardIntervals { - good: u16, - easy: u16, - _unused: u16, -} - -impl Default for NewCardIntervals { - fn default() -> Self { - Self { - good: 1, - easy: 4, - _unused: 7, - } - } -} - -#[derive(Serialize_repr, Deserialize_repr, Debug, PartialEq, Clone)] -#[repr(u8)] -pub enum NewCardOrder { - Random = 0, - Due = 1, -} - -impl Default for NewCardOrder { - fn default() -> Self { - Self::Due - } -} - -fn hard_factor_default() -> f32 { - 1.2 -} - -#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] -#[serde(rename_all = "camelCase")] -pub struct RevConf { - #[serde(default)] - bury: bool, - ease4: f32, - ivl_fct: f32, - max_ivl: u32, - #[serde(deserialize_with = "default_on_invalid")] - pub(crate) per_day: u32, - #[serde(default = "hard_factor_default")] - hard_factor: f32, - - #[serde(flatten)] - other: HashMap, -} - -#[derive(Serialize_repr, Deserialize_repr, Debug, PartialEq, Clone)] -#[repr(u8)] -pub enum LeechAction { - Suspend = 0, - TagOnly = 1, -} - -#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] -#[serde(rename_all = "camelCase")] -pub struct LapseConf { - #[serde(deserialize_with = "default_on_invalid")] - delays: Vec, - #[serde(deserialize_with = "default_on_invalid")] - leech_action: LeechAction, - leech_fails: u32, - min_int: u32, - mult: f32, - - #[serde(flatten)] - other: HashMap, -} - -impl Default for LeechAction { - fn default() -> Self { - LeechAction::Suspend - } -} - -impl Default for RevConf { - fn default() -> Self { - RevConf { - bury: false, - ease4: 1.3, - ivl_fct: 1.0, - max_ivl: 36500, - per_day: 200, - hard_factor: 1.2, - other: Default::default(), - } - } -} - -impl Default for NewConf { - fn default() -> Self { - NewConf { - bury: false, - delays: vec![1.0, 10.0], - initial_factor: 2500, - ints: NewCardIntervals::default(), - order: NewCardOrder::default(), - per_day: 20, - other: Default::default(), - } - } -} - -impl Default for LapseConf { - fn default() -> Self { - LapseConf { - delays: vec![10.0], - leech_action: LeechAction::default(), - leech_fails: 8, - min_int: 1, - mult: 0.0, - other: Default::default(), - } - } -} - -impl Default for DeckConf { - fn default() -> Self { - DeckConf { - id: DeckConfID(0), - mtime: TimestampSecs(0), - name: "Default".to_string(), - usn: Usn(0), - max_taken: 60, - autoplay: true, - timer: 0, - replayq: true, - new: Default::default(), - rev: Default::default(), - lapse: Default::default(), - other: Default::default(), - } - } -} - -impl Collection { - /// If fallback is true, guaranteed to return a deck config. - pub fn get_deck_config(&self, dcid: DeckConfID, fallback: bool) -> Result> { - if let Some(conf) = self.storage.get_deck_config(dcid)? { - return Ok(Some(conf)); - } - if fallback { - if let Some(conf) = self.storage.get_deck_config(DeckConfID(1))? { - return Ok(Some(conf)); - } - // if even the default deck config is missing, just return the defaults - Ok(Some(DeckConf::default())) - } else { - Ok(None) - } - } - - pub(crate) fn add_or_update_deck_config( - &self, - conf: &mut DeckConf, - preserve_usn_and_mtime: bool, - ) -> Result<()> { - if !preserve_usn_and_mtime { - conf.mtime = TimestampSecs::now(); - conf.usn = self.usn()?; - } - let orig = self.storage.get_deck_config(conf.id)?; - if let Some(_orig) = orig { - self.storage.update_deck_conf(&conf) - } else { - if conf.id.0 == 0 { - conf.id.0 = TimestampMillis::now().0; - } - self.storage.add_deck_conf(conf) - } - } - - /// Remove a deck configuration. This will force a full sync. - pub(crate) fn remove_deck_config(&self, dcid: DeckConfID) -> Result<()> { - if dcid.0 == 1 { - return Err(AnkiError::invalid_input("can't delete default conf")); - } - self.storage.set_schema_modified()?; - self.storage.remove_deck_conf(dcid) - } -} diff --git a/rslib/src/deckconf/mod.rs b/rslib/src/deckconf/mod.rs new file mode 100644 index 000000000..459afe538 --- /dev/null +++ b/rslib/src/deckconf/mod.rs @@ -0,0 +1,112 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use crate::{ + collection::Collection, + define_newtype, + err::{AnkiError, Result}, + timestamp::{TimestampMillis, TimestampSecs}, + types::Usn, +}; + +pub use crate::backend_proto::{ + deck_config_inner::{LeechAction, NewCardOrder}, + DeckConfigInner, +}; +pub use schema11::{DeckConfSchema11, NewCardOrderSchema11}; + +mod schema11; + +define_newtype!(DeckConfID, i64); + +#[derive(Debug)] +pub struct DeckConf { + pub id: DeckConfID, + pub name: String, + pub mtime_secs: TimestampSecs, + pub usn: Usn, + pub inner: DeckConfigInner, +} + +impl Default for DeckConf { + fn default() -> Self { + DeckConf { + id: DeckConfID(0), + name: "".to_string(), + mtime_secs: Default::default(), + usn: Default::default(), + inner: DeckConfigInner { + learn_steps: vec![1.0, 10.0], + relearn_steps: vec![10.0], + disable_autoplay: false, + cap_answer_time_to_secs: 60, + visible_timer_secs: 0, + skip_question_when_replaying_answer: false, + new_per_day: 20, + reviews_per_day: 200, + bury_new: false, + bury_reviews: false, + initial_ease: 2.5, + easy_multiplier: 1.3, + hard_multiplier: 1.2, + lapse_multiplier: 0.0, + interval_multiplier: 1.0, + maximum_review_interval: 36_500, + minimum_review_interval: 1, + graduating_interval_good: 1, + graduating_interval_easy: 4, + new_card_order: NewCardOrder::Due as i32, + leech_action: LeechAction::Suspend as i32, + leech_threshold: 8, + other: vec![], + }, + } + } +} + +impl Collection { + /// If fallback is true, guaranteed to return a deck config. + pub fn get_deck_config(&self, dcid: DeckConfID, fallback: bool) -> Result> { + if let Some(conf) = self.storage.get_deck_config(dcid)? { + return Ok(Some(conf)); + } + if fallback { + if let Some(conf) = self.storage.get_deck_config(DeckConfID(1))? { + return Ok(Some(conf)); + } + // if even the default deck config is missing, just return the defaults + Ok(Some(DeckConf::default())) + } else { + Ok(None) + } + } + + pub(crate) fn add_or_update_deck_config( + &self, + conf: &mut DeckConf, + preserve_usn_and_mtime: bool, + ) -> Result<()> { + if !preserve_usn_and_mtime { + conf.mtime_secs = TimestampSecs::now(); + conf.usn = self.usn()?; + } + let orig = self.storage.get_deck_config(conf.id)?; + if let Some(_orig) = orig { + self.storage.update_deck_conf(&conf) + } else { + if conf.id.0 == 0 { + conf.id.0 = TimestampMillis::now().0; + } + self.storage.add_deck_conf(conf) + } + } + + /// Remove a deck configuration. This will force a full sync. + pub(crate) fn remove_deck_config(&self, dcid: DeckConfID) -> Result<()> { + if dcid.0 == 1 { + return Err(AnkiError::invalid_input("can't delete default conf")); + } + self.storage.set_schema_modified()?; + self.storage.remove_deck_conf(dcid) + } +} diff --git a/rslib/src/deckconf/schema11.rs b/rslib/src/deckconf/schema11.rs new file mode 100644 index 000000000..cf7a1abfe --- /dev/null +++ b/rslib/src/deckconf/schema11.rs @@ -0,0 +1,329 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use super::{DeckConf, DeckConfID}; +use crate::backend_proto::deck_config_inner::NewCardOrder; +use crate::backend_proto::DeckConfigInner; +use crate::{serde::default_on_invalid, timestamp::TimestampSecs, types::Usn}; +use serde_aux::field_attributes::deserialize_number_from_string; +use serde_derive::{Deserialize, Serialize}; +use serde_json::Value; +use serde_repr::{Deserialize_repr, Serialize_repr}; +use serde_tuple::Serialize_tuple; +use std::collections::HashMap; + +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct DeckConfSchema11 { + #[serde(deserialize_with = "deserialize_number_from_string")] + pub(crate) id: DeckConfID, + #[serde(rename = "mod", deserialize_with = "deserialize_number_from_string")] + pub(crate) mtime: TimestampSecs, + pub(crate) name: String, + pub(crate) usn: Usn, + max_taken: i32, + autoplay: bool, + #[serde(deserialize_with = "default_on_invalid")] + timer: u8, + #[serde(default)] + replayq: bool, + pub(crate) new: NewConfSchema11, + pub(crate) rev: RevConfSchema11, + pub(crate) lapse: LapseConfSchema11, + #[serde(flatten)] + other: HashMap, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct NewConfSchema11 { + #[serde(default)] + bury: bool, + #[serde(deserialize_with = "default_on_invalid")] + delays: Vec, + initial_factor: u16, + #[serde(deserialize_with = "default_on_invalid")] + ints: NewCardIntervals, + #[serde(deserialize_with = "default_on_invalid")] + pub(crate) order: NewCardOrderSchema11, + #[serde(deserialize_with = "default_on_invalid")] + pub(crate) per_day: u32, + + #[serde(flatten)] + other: HashMap, +} + +#[derive(Serialize_tuple, Deserialize, Debug, PartialEq, Clone)] +pub struct NewCardIntervals { + good: u16, + easy: u16, + _unused: u16, +} + +impl Default for NewCardIntervals { + fn default() -> Self { + Self { + good: 1, + easy: 4, + _unused: 7, + } + } +} + +#[derive(Serialize_repr, Deserialize_repr, Debug, PartialEq, Clone)] +#[repr(u8)] +pub enum NewCardOrderSchema11 { + Random = 0, + Due = 1, +} + +impl Default for NewCardOrderSchema11 { + fn default() -> Self { + Self::Due + } +} + +fn hard_factor_default() -> f32 { + 1.2 +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct RevConfSchema11 { + #[serde(default)] + bury: bool, + ease4: f32, + ivl_fct: f32, + max_ivl: u32, + #[serde(deserialize_with = "default_on_invalid")] + pub(crate) per_day: u32, + #[serde(default = "hard_factor_default")] + hard_factor: f32, + + #[serde(flatten)] + other: HashMap, +} + +#[derive(Serialize_repr, Deserialize_repr, Debug, PartialEq, Clone)] +#[repr(u8)] +pub enum LeechAction { + Suspend = 0, + TagOnly = 1, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct LapseConfSchema11 { + #[serde(deserialize_with = "default_on_invalid")] + delays: Vec, + #[serde(deserialize_with = "default_on_invalid")] + leech_action: LeechAction, + leech_fails: u32, + min_int: u32, + mult: f32, + + #[serde(flatten)] + other: HashMap, +} + +impl Default for LeechAction { + fn default() -> Self { + LeechAction::Suspend + } +} + +impl Default for RevConfSchema11 { + fn default() -> Self { + RevConfSchema11 { + bury: false, + ease4: 1.3, + ivl_fct: 1.0, + max_ivl: 36500, + per_day: 200, + hard_factor: 1.2, + other: Default::default(), + } + } +} + +impl Default for NewConfSchema11 { + fn default() -> Self { + NewConfSchema11 { + bury: false, + delays: vec![1.0, 10.0], + initial_factor: 2500, + ints: NewCardIntervals::default(), + order: NewCardOrderSchema11::default(), + per_day: 20, + other: Default::default(), + } + } +} + +impl Default for LapseConfSchema11 { + fn default() -> Self { + LapseConfSchema11 { + delays: vec![10.0], + leech_action: LeechAction::default(), + leech_fails: 8, + min_int: 1, + mult: 0.0, + other: Default::default(), + } + } +} + +impl Default for DeckConfSchema11 { + fn default() -> Self { + DeckConfSchema11 { + id: DeckConfID(0), + mtime: TimestampSecs(0), + name: "Default".to_string(), + usn: Usn(0), + max_taken: 60, + autoplay: true, + timer: 0, + replayq: true, + new: Default::default(), + rev: Default::default(), + lapse: Default::default(), + other: Default::default(), + } + } +} + +// schema11 -> schema15 + +impl From for DeckConf { + fn from(mut c: DeckConfSchema11) -> DeckConf { + // merge any json stored in new/rev/lapse into top level + if !c.new.other.is_empty() { + if let Ok(val) = serde_json::to_value(c.new.other) { + c.other.insert("new".into(), val); + } + } + if !c.rev.other.is_empty() { + if let Ok(val) = serde_json::to_value(c.rev.other) { + c.other.insert("rev".into(), val); + } + } + if !c.lapse.other.is_empty() { + if let Ok(val) = serde_json::to_value(c.lapse.other) { + c.other.insert("lapse".into(), val); + } + } + let other_bytes = serde_json::to_vec(&c.other).unwrap_or_default(); + + DeckConf { + id: c.id, + name: c.name, + mtime_secs: c.mtime, + usn: c.usn, + inner: DeckConfigInner { + learn_steps: c.new.delays, + relearn_steps: c.lapse.delays, + disable_autoplay: !c.autoplay, + cap_answer_time_to_secs: c.max_taken.max(0) as u32, + visible_timer_secs: c.timer as u32, + skip_question_when_replaying_answer: !c.replayq, + new_per_day: c.new.per_day, + reviews_per_day: c.rev.per_day, + bury_new: c.new.bury, + bury_reviews: c.rev.bury, + initial_ease: (c.new.initial_factor as f32) / 10.0, + easy_multiplier: c.rev.ease4, + hard_multiplier: c.rev.hard_factor, + lapse_multiplier: c.lapse.mult, + interval_multiplier: c.rev.ivl_fct, + maximum_review_interval: c.rev.max_ivl, + minimum_review_interval: c.lapse.min_int, + graduating_interval_good: c.new.ints.good as u32, + graduating_interval_easy: c.new.ints.easy as u32, + new_card_order: match c.new.order { + NewCardOrderSchema11::Random => NewCardOrder::Random, + NewCardOrderSchema11::Due => NewCardOrder::Due, + } as i32, + leech_action: c.lapse.leech_action as i32, + leech_threshold: c.lapse.leech_fails, + other: other_bytes, + }, + } + } +} + +// schema 15 -> schema 11 +impl From for DeckConfSchema11 { + fn from(c: DeckConf) -> DeckConfSchema11 { + // split extra json up + let mut top_other: HashMap; + let mut new_other = Default::default(); + let mut rev_other = Default::default(); + let mut lapse_other = Default::default(); + if c.inner.other.is_empty() { + top_other = Default::default(); + } else { + top_other = serde_json::from_slice(&c.inner.other).unwrap_or_default(); + if let Some(new) = top_other.remove("new") { + let val: HashMap = serde_json::from_value(new).unwrap_or_default(); + new_other = val; + } + if let Some(rev) = top_other.remove("rev") { + let val: HashMap = serde_json::from_value(rev).unwrap_or_default(); + rev_other = val; + } + if let Some(lapse) = top_other.remove("lapse") { + let val: HashMap = serde_json::from_value(lapse).unwrap_or_default(); + lapse_other = val; + } + } + let i = c.inner; + let new_order = i.new_card_order(); + DeckConfSchema11 { + id: c.id, + mtime: c.mtime_secs, + name: c.name, + usn: c.usn, + max_taken: i.cap_answer_time_to_secs as i32, + autoplay: !i.disable_autoplay, + timer: i.visible_timer_secs as u8, + replayq: !i.skip_question_when_replaying_answer, + new: NewConfSchema11 { + bury: i.bury_new, + delays: i.learn_steps, + initial_factor: (i.initial_ease * 10.0) as u16, + ints: NewCardIntervals { + good: i.graduating_interval_good as u16, + easy: i.graduating_interval_easy as u16, + _unused: 0, + }, + order: match new_order { + NewCardOrder::Random => NewCardOrderSchema11::Random, + NewCardOrder::Due => NewCardOrderSchema11::Due, + }, + per_day: i.new_per_day, + other: new_other, + }, + rev: RevConfSchema11 { + bury: i.bury_reviews, + ease4: i.easy_multiplier, + ivl_fct: i.interval_multiplier, + max_ivl: i.maximum_review_interval, + per_day: i.reviews_per_day, + hard_factor: i.hard_multiplier, + other: rev_other, + }, + lapse: LapseConfSchema11 { + delays: i.relearn_steps, + leech_action: match i.leech_action { + 1 => LeechAction::TagOnly, + _ => LeechAction::Suspend, + }, + leech_fails: i.leech_threshold, + min_int: i.minimum_review_interval, + mult: i.lapse_multiplier, + other: lapse_other, + }, + other: top_other, + } + } +} diff --git a/rslib/src/decks/tree.rs b/rslib/src/decks/tree.rs index 1ff8c009e..206c197eb 100644 --- a/rslib/src/decks/tree.rs +++ b/rslib/src/decks/tree.rs @@ -137,8 +137,12 @@ fn remaining_counts_for_deck( .get(&DeckConfID(norm.config_id)) .or_else(|| dconf.get(&DeckConfID(1))) { - let new = (conf.new.per_day as i32).saturating_sub(new_today).max(0); - let rev = (conf.rev.per_day as i32).saturating_sub(rev_today).max(0); + let new = (conf.inner.new_per_day as i32) + .saturating_sub(new_today) + .max(0); + let rev = (conf.inner.reviews_per_day as i32) + .saturating_sub(rev_today) + .max(0); (new as u32, rev as u32) } else { // missing dconf and fallback @@ -342,7 +346,7 @@ mod test { // set the limit to 4, which should mean 3 are left let mut conf = col.get_deck_config(DeckConfID(1), false)?.unwrap(); - conf.new.per_day = 4; + conf.inner.new_per_day = 4; col.add_or_update_deck_config(&mut conf, false)?; let tree = col.deck_tree(true, None)?; diff --git a/rslib/src/notetype/cardgen.rs b/rslib/src/notetype/cardgen.rs index 697419c09..2e1993794 100644 --- a/rslib/src/notetype/cardgen.rs +++ b/rslib/src/notetype/cardgen.rs @@ -310,7 +310,7 @@ impl Collection { } let next_pos = cache.next_position.unwrap(); - match cache.deck_configs.get(&did).unwrap().new.order { + match cache.deck_configs.get(&did).unwrap().inner.new_card_order() { crate::deckconf::NewCardOrder::Random => Ok(random_position(next_pos)), crate::deckconf::NewCardOrder::Due => Ok(next_pos), } diff --git a/rslib/src/storage/deckconf/get.sql b/rslib/src/storage/deckconf/get.sql index 4950e30a3..18eb27c2c 100644 --- a/rslib/src/storage/deckconf/get.sql +++ b/rslib/src/storage/deckconf/get.sql @@ -1,5 +1,7 @@ select + id, + name, + mtime_secs, + usn, config from deck_config -where - id = ?; \ No newline at end of file diff --git a/rslib/src/storage/deckconf/mod.rs b/rslib/src/storage/deckconf/mod.rs index a54e7f140..c126947ed 100644 --- a/rslib/src/storage/deckconf/mod.rs +++ b/rslib/src/storage/deckconf/mod.rs @@ -3,61 +3,70 @@ use super::SqliteStorage; use crate::{ - deckconf::{DeckConf, DeckConfID}, + deckconf::{DeckConf, DeckConfID, DeckConfSchema11, DeckConfigInner}, err::Result, i18n::{I18n, TR}, }; -use rusqlite::{params, NO_PARAMS}; +use prost::Message; +use rusqlite::{params, Row, NO_PARAMS}; use std::collections::HashMap; +fn row_to_deckconf(row: &Row) -> Result { + let config = DeckConfigInner::decode(row.get_raw(4).as_blob()?)?; + Ok(DeckConf { + id: row.get(0)?, + name: row.get(1)?, + mtime_secs: row.get(2)?, + usn: row.get(3)?, + inner: config, + }) +} + impl SqliteStorage { pub(crate) fn all_deck_config(&self) -> Result> { self.db - .prepare_cached("select config from deck_config")? - .query_and_then(NO_PARAMS, |row| -> Result<_> { - Ok(serde_json::from_slice(row.get_raw(0).as_blob()?)?) - })? + .prepare_cached(include_str!("get.sql"))? + .query_and_then(NO_PARAMS, row_to_deckconf)? .collect() } pub(crate) fn get_deck_config(&self, dcid: DeckConfID) -> Result> { self.db - .prepare_cached(include_str!("get.sql"))? - .query_and_then(params![dcid], |row| -> Result<_> { - Ok(serde_json::from_slice(row.get_raw(0).as_blob()?)?) - })? + .prepare_cached(concat!(include_str!("get.sql"), " where id = ?"))? + .query_and_then(params![dcid], row_to_deckconf)? .next() .transpose() } pub(crate) fn add_deck_conf(&self, conf: &mut DeckConf) -> Result<()> { + let mut conf_bytes = vec![]; + conf.inner.encode(&mut conf_bytes)?; self.db .prepare_cached(include_str!("add.sql"))? .execute(params![ conf.id, conf.name, - conf.mtime, + conf.mtime_secs, conf.usn, - &serde_json::to_vec(conf)?, + conf_bytes, ])?; let id = self.db.last_insert_rowid(); if conf.id.0 != id { - // if the initial ID conflicted, make sure the json is up to date - // as well conf.id.0 = id; - self.update_deck_conf(conf)?; } Ok(()) } pub(crate) fn update_deck_conf(&self, conf: &DeckConf) -> Result<()> { + let mut conf_bytes = vec![]; + conf.inner.encode(&mut conf_bytes)?; self.db .prepare_cached(include_str!("update.sql"))? .execute(params![ conf.name, - conf.mtime, + conf.mtime_secs, conf.usn, - &serde_json::to_vec(conf)?, + conf_bytes, conf.id, ])?; Ok(()) @@ -90,26 +99,70 @@ impl SqliteStorage { self.add_deck_conf(&mut conf) } + // schema 11->14 + + fn add_deck_conf_schema14(&self, conf: &mut DeckConfSchema11) -> Result<()> { + self.db + .prepare_cached(include_str!("add.sql"))? + .execute(params![ + conf.id, + conf.name, + conf.mtime, + conf.usn, + &serde_json::to_vec(conf)?, + ])?; + let id = self.db.last_insert_rowid(); + if conf.id.0 != id { + conf.id.0 = id; + } + Ok(()) + } + pub(super) fn upgrade_deck_conf_to_schema14(&self) -> Result<()> { let conf = self .db .query_row_and_then("select dconf from col", NO_PARAMS, |row| { - let conf: Result> = + let conf: Result> = serde_json::from_str(row.get_raw(0).as_str()?).map_err(Into::into); conf })?; for (_, mut conf) in conf.into_iter() { - self.add_deck_conf(&mut conf)?; + self.add_deck_conf_schema14(&mut conf)?; } self.db.execute_batch("update col set dconf=''")?; Ok(()) } - pub(super) fn downgrade_deck_conf_from_schema14(&self) -> Result<()> { + // schema 14->15 + + fn all_deck_config_schema14(&self) -> Result> { + self.db + .prepare_cached("select config from deck_config")? + .query_and_then(NO_PARAMS, |row| -> Result<_> { + Ok(serde_json::from_slice(row.get_raw(0).as_blob()?)?) + })? + .collect() + } + + pub(super) fn upgrade_deck_conf_to_schema15(&self) -> Result<()> { + for conf in self.all_deck_config_schema14()? { + let conf: DeckConf = conf.into(); + self.update_deck_conf(&conf)?; + } + + Ok(()) + } + + // schema 15->11 + + pub(super) fn downgrade_deck_conf_from_schema15(&self) -> Result<()> { let allconf = self.all_deck_config()?; - let confmap: HashMap = - allconf.into_iter().map(|c| (c.id, c)).collect(); + let confmap: HashMap = allconf + .into_iter() + .map(|c| -> DeckConfSchema11 { c.into() }) + .map(|c| (c.id, c)) + .collect(); self.db.execute( "update col set dconf=?", params![serde_json::to_string(&confmap)?], diff --git a/rslib/src/storage/upgrades/mod.rs b/rslib/src/storage/upgrades/mod.rs index f8e816f22..c05d9c8c9 100644 --- a/rslib/src/storage/upgrades/mod.rs +++ b/rslib/src/storage/upgrades/mod.rs @@ -18,6 +18,7 @@ impl SqliteStorage { .execute_batch(include_str!("schema15_upgrade.sql"))?; self.upgrade_notetypes_to_schema15()?; self.upgrade_decks_to_schema15()?; + self.upgrade_deck_conf_to_schema15()?; } Ok(()) @@ -26,11 +27,11 @@ impl SqliteStorage { pub(super) fn downgrade_to_schema_11(&self) -> Result<()> { self.begin_trx()?; + self.downgrade_deck_conf_from_schema15()?; self.downgrade_decks_from_schema15()?; self.downgrade_notetypes_from_schema15()?; self.downgrade_config_from_schema14()?; self.downgrade_tags_from_schema14()?; - self.downgrade_deck_conf_from_schema14()?; self.db .execute_batch(include_str!("schema11_downgrade.sql"))?;