mirror of
https://github.com/ankitects/anki.git
synced 2025-09-20 15:02:21 -04:00
start on making deck config and schema/mod changes undoable
+ move timestamps into a struct in a separate file for convenience
This commit is contained in:
parent
3abe6168aa
commit
262b50445c
21 changed files with 363 additions and 176 deletions
|
@ -1,10 +1,11 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
use crate::storage::SqliteStorage;
|
||||
use crate::{collection::Collection, error::Result};
|
||||
use rusqlite::types::{FromSql, FromSqlError, ToSql, ToSqlOutput, ValueRef};
|
||||
use rusqlite::OptionalExtension;
|
||||
use crate::{prelude::*, storage::SqliteStorage};
|
||||
use rusqlite::{
|
||||
types::{FromSql, FromSqlError, ToSql, ToSqlOutput, ValueRef},
|
||||
OptionalExtension,
|
||||
};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
@ -88,7 +89,7 @@ pub(super) fn db_command_bytes(col: &mut Collection, input: &[u8]) -> Result<Vec
|
|||
}
|
||||
DbRequest::Commit => {
|
||||
if col.state.modified_by_dbproxy {
|
||||
col.storage.set_modified()?;
|
||||
col.storage.set_modified_time(TimestampMillis::now())?;
|
||||
col.state.modified_by_dbproxy = false;
|
||||
}
|
||||
col.storage.commit_trx()?;
|
||||
|
|
|
@ -1,17 +1,20 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
pub(crate) mod timestamps;
|
||||
mod transact;
|
||||
pub(crate) mod undo;
|
||||
|
||||
use crate::i18n::I18n;
|
||||
use crate::types::Usn;
|
||||
use crate::{
|
||||
browser_table,
|
||||
decks::{Deck, DeckId},
|
||||
notetype::{Notetype, NotetypeId},
|
||||
prelude::*,
|
||||
storage::SqliteStorage,
|
||||
undo::UndoManager,
|
||||
};
|
||||
use crate::{error::Result, scheduler::queue::CardQueues};
|
||||
use crate::{i18n::I18n, ops::StateChanges};
|
||||
use crate::{log::Logger, scheduler::SchedulerInfo};
|
||||
use std::{collections::HashMap, path::PathBuf, sync::Arc};
|
||||
|
||||
|
@ -86,68 +89,6 @@ pub struct Collection {
|
|||
}
|
||||
|
||||
impl Collection {
|
||||
fn transact_inner<F, R>(&mut self, op: Option<Op>, func: F) -> Result<OpOutput<R>>
|
||||
where
|
||||
F: FnOnce(&mut Collection) -> Result<R>,
|
||||
{
|
||||
self.storage.begin_rust_trx()?;
|
||||
self.begin_undoable_operation(op);
|
||||
|
||||
let mut res = func(self);
|
||||
|
||||
if res.is_ok() {
|
||||
if let Err(e) = self.storage.set_modified() {
|
||||
res = Err(e);
|
||||
} else if let Err(e) = self.storage.commit_rust_trx() {
|
||||
res = Err(e);
|
||||
}
|
||||
}
|
||||
|
||||
match res {
|
||||
Ok(output) => {
|
||||
let changes = if op.is_some() {
|
||||
let changes = self.op_changes();
|
||||
self.maybe_clear_study_queues_after_op(changes);
|
||||
self.maybe_coalesce_note_undo_entry(changes);
|
||||
changes
|
||||
} else {
|
||||
self.clear_study_queues();
|
||||
// dummy value, not used by transact_no_undo(). only required
|
||||
// until we can migrate all the code to undoable ops
|
||||
OpChanges {
|
||||
op: Op::SetFlag,
|
||||
changes: StateChanges::default(),
|
||||
}
|
||||
};
|
||||
self.end_undoable_operation();
|
||||
Ok(OpOutput { output, changes })
|
||||
}
|
||||
Err(err) => {
|
||||
self.discard_undo_and_study_queues();
|
||||
self.storage.rollback_rust_trx()?;
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute the provided closure in a transaction, rolling back if
|
||||
/// an error is returned. Records undo state, and returns changes.
|
||||
pub(crate) fn transact<F, R>(&mut self, op: Op, func: F) -> Result<OpOutput<R>>
|
||||
where
|
||||
F: FnOnce(&mut Collection) -> Result<R>,
|
||||
{
|
||||
self.transact_inner(Some(op), func)
|
||||
}
|
||||
|
||||
/// Execute the provided closure in a transaction, rolling back if
|
||||
/// an error is returned.
|
||||
pub(crate) fn transact_no_undo<F, R>(&mut self, func: F) -> Result<R>
|
||||
where
|
||||
F: FnOnce(&mut Collection) -> Result<R>,
|
||||
{
|
||||
self.transact_inner(None, func).map(|out| out.output)
|
||||
}
|
||||
|
||||
pub(crate) fn close(self, downgrade: bool) -> Result<()> {
|
||||
self.storage.close(downgrade)
|
||||
}
|
||||
|
@ -169,8 +110,9 @@ impl Collection {
|
|||
col.storage.clear_deck_usns()?;
|
||||
col.storage.clear_notetype_usns()?;
|
||||
col.storage.increment_usn()?;
|
||||
col.storage.set_schema_modified()?;
|
||||
col.storage.set_last_sync(col.storage.get_schema_mtime()?)
|
||||
col.set_schema_modified()?;
|
||||
col.storage
|
||||
.set_last_sync(col.storage.get_collection_timestamps()?.schema_change)
|
||||
})?;
|
||||
self.storage.optimize()
|
||||
}
|
32
rslib/src/collection/timestamps.rs
Normal file
32
rslib/src/collection/timestamps.rs
Normal file
|
@ -0,0 +1,32 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
pub(crate) struct CollectionTimestamps {
|
||||
pub collection_change: TimestampMillis,
|
||||
pub schema_change: TimestampMillis,
|
||||
pub last_sync: TimestampMillis,
|
||||
}
|
||||
|
||||
impl CollectionTimestamps {
|
||||
pub fn collection_changed_since_sync(&self) -> bool {
|
||||
self.collection_change > self.last_sync
|
||||
}
|
||||
|
||||
pub fn schema_changed_since_sync(&self) -> bool {
|
||||
self.schema_change > self.last_sync
|
||||
}
|
||||
}
|
||||
|
||||
impl Collection {
|
||||
pub(crate) fn set_modified(&mut self) -> Result<()> {
|
||||
let stamps = self.storage.get_collection_timestamps()?;
|
||||
self.set_modified_time_undoable(TimestampMillis::now(), stamps.collection_change)
|
||||
}
|
||||
|
||||
pub(crate) fn set_schema_modified(&mut self) -> Result<()> {
|
||||
let stamps = self.storage.get_collection_timestamps()?;
|
||||
self.set_schema_modified_time_undoable(TimestampMillis::now(), stamps.schema_change)
|
||||
}
|
||||
}
|
68
rslib/src/collection/transact.rs
Normal file
68
rslib/src/collection/transact.rs
Normal file
|
@ -0,0 +1,68 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
use crate::{ops::StateChanges, prelude::*};
|
||||
|
||||
impl Collection {
|
||||
fn transact_inner<F, R>(&mut self, op: Option<Op>, func: F) -> Result<OpOutput<R>>
|
||||
where
|
||||
F: FnOnce(&mut Collection) -> Result<R>,
|
||||
{
|
||||
self.storage.begin_rust_trx()?;
|
||||
self.begin_undoable_operation(op);
|
||||
|
||||
let mut res = func(self);
|
||||
|
||||
if res.is_ok() {
|
||||
if let Err(e) = self.set_modified() {
|
||||
res = Err(e);
|
||||
} else if let Err(e) = self.storage.commit_rust_trx() {
|
||||
res = Err(e);
|
||||
}
|
||||
}
|
||||
|
||||
match res {
|
||||
Ok(output) => {
|
||||
let changes = if op.is_some() {
|
||||
let changes = self.op_changes();
|
||||
self.maybe_clear_study_queues_after_op(changes);
|
||||
self.maybe_coalesce_note_undo_entry(changes);
|
||||
changes
|
||||
} else {
|
||||
self.clear_study_queues();
|
||||
// dummy value, not used by transact_no_undo(). only required
|
||||
// until we can migrate all the code to undoable ops
|
||||
OpChanges {
|
||||
op: Op::SetFlag,
|
||||
changes: StateChanges::default(),
|
||||
}
|
||||
};
|
||||
self.end_undoable_operation();
|
||||
Ok(OpOutput { output, changes })
|
||||
}
|
||||
Err(err) => {
|
||||
self.discard_undo_and_study_queues();
|
||||
self.storage.rollback_rust_trx()?;
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute the provided closure in a transaction, rolling back if
|
||||
/// an error is returned. Records undo state, and returns changes.
|
||||
pub(crate) fn transact<F, R>(&mut self, op: Op, func: F) -> Result<OpOutput<R>>
|
||||
where
|
||||
F: FnOnce(&mut Collection) -> Result<R>,
|
||||
{
|
||||
self.transact_inner(Some(op), func)
|
||||
}
|
||||
|
||||
/// Execute the provided closure in a transaction, rolling back if
|
||||
/// an error is returned.
|
||||
pub(crate) fn transact_no_undo<F, R>(&mut self, func: F) -> Result<R>
|
||||
where
|
||||
F: FnOnce(&mut Collection) -> Result<R>,
|
||||
{
|
||||
self.transact_inner(None, func).map(|out| out.output)
|
||||
}
|
||||
}
|
46
rslib/src/collection/undo.rs
Normal file
46
rslib/src/collection/undo.rs
Normal file
|
@ -0,0 +1,46 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum UndoableCollectionChange {
|
||||
Schema(TimestampMillis),
|
||||
Modified(TimestampMillis),
|
||||
}
|
||||
|
||||
impl Collection {
|
||||
pub(crate) fn undo_collection_change(
|
||||
&mut self,
|
||||
change: UndoableCollectionChange,
|
||||
) -> Result<()> {
|
||||
match change {
|
||||
UndoableCollectionChange::Schema(schema) => {
|
||||
let current = self.storage.get_collection_timestamps()?.schema_change;
|
||||
self.set_schema_modified_time_undoable(schema, current)
|
||||
}
|
||||
UndoableCollectionChange::Modified(modified) => {
|
||||
let current = self.storage.get_collection_timestamps()?.collection_change;
|
||||
self.set_schema_modified_time_undoable(modified, current)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn set_modified_time_undoable(
|
||||
&mut self,
|
||||
modified: TimestampMillis,
|
||||
original: TimestampMillis,
|
||||
) -> Result<()> {
|
||||
self.save_undo(UndoableCollectionChange::Modified(original));
|
||||
self.storage.set_modified_time(modified)
|
||||
}
|
||||
|
||||
pub(super) fn set_schema_modified_time_undoable(
|
||||
&mut self,
|
||||
schema: TimestampMillis,
|
||||
original: TimestampMillis,
|
||||
) -> Result<()> {
|
||||
self.save_undo(UndoableCollectionChange::Schema(original));
|
||||
self.storage.set_schema_modified_time(schema)
|
||||
}
|
||||
}
|
|
@ -153,7 +153,7 @@ impl Collection {
|
|||
fn check_orphaned_cards(&mut self, out: &mut CheckDatabaseOutput) -> Result<()> {
|
||||
let cnt = self.storage.delete_orphaned_cards()?;
|
||||
if cnt > 0 {
|
||||
self.storage.set_schema_modified()?;
|
||||
self.set_schema_modified()?;
|
||||
out.cards_missing_note = cnt;
|
||||
}
|
||||
Ok(())
|
||||
|
@ -186,7 +186,7 @@ impl Collection {
|
|||
}
|
||||
|
||||
if wrong > 0 {
|
||||
self.storage.set_schema_modified()?;
|
||||
self.set_schema_modified()?;
|
||||
out.card_properties_invalid += wrong;
|
||||
}
|
||||
|
||||
|
@ -275,7 +275,7 @@ impl Collection {
|
|||
|| out.templates_missing > 0
|
||||
|| out.notetypes_recovered > 0
|
||||
{
|
||||
self.storage.set_schema_modified()?;
|
||||
self.set_schema_modified()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -365,10 +365,10 @@ impl Collection {
|
|||
Ok(Arc::new(basic))
|
||||
}
|
||||
|
||||
fn check_revlog(&self, out: &mut CheckDatabaseOutput) -> Result<()> {
|
||||
fn check_revlog(&mut self, out: &mut CheckDatabaseOutput) -> Result<()> {
|
||||
let cnt = self.storage.fix_revlog_properties()?;
|
||||
if cnt > 0 {
|
||||
self.storage.set_schema_modified()?;
|
||||
self.set_schema_modified()?;
|
||||
out.revlog_properties_invalid = cnt;
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
mod schema11;
|
||||
pub(crate) mod undo;
|
||||
mod update;
|
||||
|
||||
pub use {
|
||||
|
@ -114,11 +115,15 @@ impl Collection {
|
|||
}
|
||||
|
||||
/// Remove a deck configuration. This will force a full sync.
|
||||
pub(crate) fn remove_deck_config(&self, dcid: DeckConfId) -> Result<()> {
|
||||
pub(crate) fn remove_deck_config(&mut 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)
|
||||
let conf = self
|
||||
.storage
|
||||
.get_deck_config(dcid)?
|
||||
.ok_or(AnkiError::NotFound)?;
|
||||
self.set_schema_modified()?;
|
||||
self.remove_deck_config_undoable(conf)
|
||||
}
|
||||
}
|
||||
|
|
76
rslib/src/deckconf/undo.rs
Normal file
76
rslib/src/deckconf/undo.rs
Normal file
|
@ -0,0 +1,76 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
|
||||
pub(crate) enum UndoableDeckConfigChange {
|
||||
Added(Box<DeckConf>),
|
||||
Updated(Box<DeckConf>),
|
||||
Removed(Box<DeckConf>),
|
||||
}
|
||||
|
||||
// fixme: schema mod
|
||||
|
||||
impl Collection {
|
||||
pub(crate) fn undo_deck_config_change(
|
||||
&mut self,
|
||||
change: UndoableDeckConfigChange,
|
||||
) -> Result<()> {
|
||||
match change {
|
||||
UndoableDeckConfigChange::Added(config) => self.remove_deck_config_undoable(*config),
|
||||
UndoableDeckConfigChange::Updated(mut config) => {
|
||||
let current = self
|
||||
.storage
|
||||
.get_deck_config(config.id)?
|
||||
.ok_or_else(|| AnkiError::invalid_input("deck config disappeared"))?;
|
||||
self.update_single_deck_config_undoable(&mut *config, current)
|
||||
}
|
||||
UndoableDeckConfigChange::Removed(config) => self.restore_deleted_deck_config(*config),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn remove_deck_config_undoable(&mut self, config: DeckConf) -> Result<()> {
|
||||
self.storage.remove_deck_conf(config.id)?;
|
||||
self.save_undo(UndoableDeckConfigChange::Removed(Box::new(config)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(super) fn add_deck_config_undoable(
|
||||
&mut self,
|
||||
config: &mut DeckConf,
|
||||
) -> Result<(), AnkiError> {
|
||||
self.storage.add_deck_conf(config)?;
|
||||
self.save_undo(UndoableDeckConfigChange::Added(Box::new(config.clone())));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn update_single_deck_config_undoable(
|
||||
&mut self,
|
||||
config: &mut DeckConf,
|
||||
original: DeckConf,
|
||||
) -> Result<()> {
|
||||
self.save_undo(UndoableDeckConfigChange::Updated(Box::new(original)));
|
||||
self.storage.update_deck_conf(config)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn add_or_update_deck_config_with_existing_id_undoable(
|
||||
&mut self,
|
||||
config: &mut DeckConf,
|
||||
) -> Result<(), AnkiError> {
|
||||
self.storage
|
||||
.add_or_update_deck_config_with_existing_id(config)?;
|
||||
self.save_undo(UndoableDeckConfigChange::Added(Box::new(config.clone())));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn restore_deleted_deck_config(&mut self, config: DeckConf) -> Result<()> {
|
||||
self.storage
|
||||
.add_or_update_deck_config_with_existing_id(&config)?;
|
||||
self.save_undo(UndoableDeckConfigChange::Added(Box::new(config)));
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -11,7 +11,7 @@ pub struct UpdateDeckConfigsIn {
|
|||
pub target_deck_id: DeckId,
|
||||
/// Deck will be set to last provided deck config.
|
||||
pub configs: Vec<DeckConf>,
|
||||
pub removed_config_ids: Vec<DeckId>,
|
||||
pub removed_config_ids: Vec<DeckConfId>,
|
||||
pub apply_to_children: bool,
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,10 @@ impl Collection {
|
|||
all_config: self.get_deck_config_with_extra_for_update()?,
|
||||
current_deck: Some(self.get_current_deck_for_update(deck)?),
|
||||
defaults: Some(DeckConf::default().into()),
|
||||
schema_modified: self.storage.schema_modified()?,
|
||||
schema_modified: self
|
||||
.storage
|
||||
.get_collection_timestamps()?
|
||||
.schema_changed_since_sync(),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -98,6 +101,10 @@ impl Collection {
|
|||
return Err(AnkiError::invalid_input("config not provided"));
|
||||
}
|
||||
|
||||
for dcid in &input.removed_config_ids {
|
||||
self.remove_deck_config(*dcid)?;
|
||||
}
|
||||
|
||||
todo!();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -494,7 +494,7 @@ impl Collection {
|
|||
// fixme: currently the storage layer is taking care of removing the notes and cards,
|
||||
// but we need to do it in this layer in the future for undo handling
|
||||
self.transact_no_undo(|col| {
|
||||
col.storage.set_schema_modified()?;
|
||||
col.set_schema_modified()?;
|
||||
col.state.notetype_cache.remove(&ntid);
|
||||
col.clear_aux_config_for_notetype(ntid)?;
|
||||
col.storage.remove_notetype(ntid)?;
|
||||
|
|
|
@ -74,7 +74,7 @@ impl Collection {
|
|||
}
|
||||
}
|
||||
|
||||
self.storage.set_schema_modified()?;
|
||||
self.set_schema_modified()?;
|
||||
|
||||
let nids = self.search_notes(&format!("mid:{}", nt.id))?;
|
||||
let usn = self.usn()?;
|
||||
|
@ -115,7 +115,7 @@ impl Collection {
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
self.storage.set_schema_modified()?;
|
||||
self.set_schema_modified()?;
|
||||
|
||||
let changes = TemplateOrdChanges::new(ords, previous_template_count as u32);
|
||||
if !changes.removed.is_empty() {
|
||||
|
|
|
@ -85,6 +85,9 @@ pub struct StateChanges {
|
|||
pub tag: bool,
|
||||
pub notetype: bool,
|
||||
pub config: bool,
|
||||
pub deck_config: bool,
|
||||
/// covers schema change (and last_sync in the future), but not mtime
|
||||
pub collection: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
|
|
|
@ -127,7 +127,7 @@ impl Collection {
|
|||
}
|
||||
|
||||
// force full sync
|
||||
self.storage.set_schema_modified()
|
||||
self.set_schema_modified()
|
||||
}
|
||||
|
||||
fn upgrade_cards_to_v2(&mut self) -> Result<()> {
|
||||
|
|
58
rslib/src/storage/collection_timestamps.rs
Normal file
58
rslib/src/storage/collection_timestamps.rs
Normal file
|
@ -0,0 +1,58 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
use super::SqliteStorage;
|
||||
use crate::{collection::timestamps::CollectionTimestamps, prelude::*};
|
||||
use rusqlite::{params, NO_PARAMS};
|
||||
|
||||
impl SqliteStorage {
|
||||
pub(crate) fn get_collection_timestamps(&self) -> Result<CollectionTimestamps> {
|
||||
self.db
|
||||
.prepare_cached("select mod, scm, ls from col")?
|
||||
.query_row(NO_PARAMS, |row| {
|
||||
Ok(CollectionTimestamps {
|
||||
collection_change: row.get(0)?,
|
||||
schema_change: row.get(1)?,
|
||||
last_sync: row.get(2)?,
|
||||
})
|
||||
})
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub(crate) fn set_schema_modified_time(&self, stamp: TimestampMillis) -> Result<()> {
|
||||
self.db
|
||||
.prepare_cached("update col set scm = ?")?
|
||||
.execute(&[stamp])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn set_last_sync(&self, stamp: TimestampMillis) -> Result<()> {
|
||||
self.db
|
||||
.prepare("update col set ls = ?")?
|
||||
.execute(&[stamp])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn set_modified_time(&self, stamp: TimestampMillis) -> Result<()> {
|
||||
self.db
|
||||
.prepare_cached("update col set mod=?")?
|
||||
.execute(params![stamp])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Creation timestamp is used less frequently, and has separate accessor
|
||||
|
||||
pub(crate) fn creation_stamp(&self) -> Result<TimestampSecs> {
|
||||
self.db
|
||||
.prepare_cached("select crt from col")?
|
||||
.query_row(NO_PARAMS, |row| row.get(0))
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub(crate) fn set_creation_stamp(&self, stamp: TimestampSecs) -> Result<()> {
|
||||
self.db
|
||||
.prepare("update col set crt = ?")?
|
||||
.execute(&[stamp])?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -4,8 +4,7 @@
|
|||
use super::SqliteStorage;
|
||||
use crate::{
|
||||
deckconf::{DeckConf, DeckConfId, DeckConfSchema11, DeckConfigInner},
|
||||
error::Result,
|
||||
i18n::I18n,
|
||||
prelude::*,
|
||||
};
|
||||
use prost::Message;
|
||||
use rusqlite::{params, Row, NO_PARAMS};
|
||||
|
@ -81,8 +80,12 @@ impl SqliteStorage {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Used for syncing.
|
||||
pub(crate) fn add_or_update_deck_config(&self, conf: &DeckConf) -> Result<()> {
|
||||
/// Used for syncing&undo; will keep provided ID. Shouldn't be used to add
|
||||
/// new config normally, since it does not allocate an id.
|
||||
pub(crate) fn add_or_update_deck_config_with_existing_id(&self, conf: &DeckConf) -> Result<()> {
|
||||
if conf.id.0 == 0 {
|
||||
return Err(AnkiError::invalid_input("deck with id 0"));
|
||||
}
|
||||
let mut conf_bytes = vec![];
|
||||
conf.inner.encode(&mut conf_bytes)?;
|
||||
self.db
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
pub(crate) mod card;
|
||||
mod collection_timestamps;
|
||||
mod config;
|
||||
mod deck;
|
||||
mod deckconf;
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
use crate::config::schema11::schema11_config_as_string;
|
||||
use crate::error::Result;
|
||||
use crate::error::{AnkiError, DbErrorKind};
|
||||
use crate::timestamp::{TimestampMillis, TimestampSecs};
|
||||
use crate::timestamp::TimestampMillis;
|
||||
use crate::{i18n::I18n, scheduler::timing::v1_creation_date, text::without_combining};
|
||||
use regex::Regex;
|
||||
use rusqlite::{functions::FunctionFlags, params, Connection, NO_PARAMS};
|
||||
|
@ -256,81 +256,6 @@ impl SqliteStorage {
|
|||
|
||||
//////////////////////////////////////////
|
||||
|
||||
pub(crate) fn set_modified(&self) -> Result<()> {
|
||||
self.set_modified_time(TimestampMillis::now())
|
||||
}
|
||||
|
||||
pub(crate) fn set_modified_time(&self, stamp: TimestampMillis) -> Result<()> {
|
||||
self.db
|
||||
.prepare_cached("update col set mod=?")?
|
||||
.execute(params![stamp])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn get_modified_time(&self) -> Result<TimestampMillis> {
|
||||
self.db
|
||||
.prepare_cached("select mod from col")?
|
||||
.query_and_then(NO_PARAMS, |r| r.get(0))?
|
||||
.next()
|
||||
.ok_or_else(|| AnkiError::invalid_input("missing col"))?
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub(crate) fn creation_stamp(&self) -> Result<TimestampSecs> {
|
||||
self.db
|
||||
.prepare_cached("select crt from col")?
|
||||
.query_row(NO_PARAMS, |row| row.get(0))
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub(crate) fn set_creation_stamp(&self, stamp: TimestampSecs) -> Result<()> {
|
||||
self.db
|
||||
.prepare("update col set crt = ?")?
|
||||
.execute(&[stamp])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn set_schema_modified(&self) -> Result<()> {
|
||||
self.db
|
||||
.prepare_cached("update col set scm = ?")?
|
||||
.execute(&[TimestampMillis::now()])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn schema_modified(&self) -> Result<bool> {
|
||||
self.db
|
||||
.prepare_cached("select scm > ls from col")?
|
||||
.query_row(NO_PARAMS, |r| r.get(0))
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub(crate) fn get_schema_mtime(&self) -> Result<TimestampMillis> {
|
||||
self.db
|
||||
.prepare_cached("select scm from col")?
|
||||
.query_and_then(NO_PARAMS, |r| r.get(0))?
|
||||
.next()
|
||||
.ok_or_else(|| AnkiError::invalid_input("missing col"))?
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub(crate) fn get_last_sync(&self) -> Result<TimestampMillis> {
|
||||
self.db
|
||||
.prepare_cached("select ls from col")?
|
||||
.query_and_then(NO_PARAMS, |r| r.get(0))?
|
||||
.next()
|
||||
.ok_or_else(|| AnkiError::invalid_input("missing col"))?
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub(crate) fn set_last_sync(&self, stamp: TimestampMillis) -> Result<()> {
|
||||
self.db
|
||||
.prepare("update col set ls = ?")?
|
||||
.execute(&[stamp])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
//////////////////////////////////////////
|
||||
|
||||
/// true if corrupt/can't access
|
||||
pub(crate) fn quick_check_corrupt(&self) -> bool {
|
||||
match self.db.pragma_query_value(None, "quick_check", |row| {
|
||||
|
|
|
@ -610,12 +610,10 @@ pub(crate) async fn get_remote_sync_meta(auth: SyncAuth) -> Result<SyncMeta> {
|
|||
|
||||
impl Collection {
|
||||
pub fn get_local_sync_status(&mut self) -> Result<sync_status_out::Required> {
|
||||
let last_sync = self.storage.get_last_sync()?;
|
||||
let schema_mod = self.storage.get_schema_mtime()?;
|
||||
let normal_mod = self.storage.get_modified_time()?;
|
||||
let required = if schema_mod > last_sync {
|
||||
let stamps = self.storage.get_collection_timestamps()?;
|
||||
let required = if stamps.schema_changed_since_sync() {
|
||||
sync_status_out::Required::FullSync
|
||||
} else if normal_mod > last_sync {
|
||||
} else if stamps.collection_changed_since_sync() {
|
||||
sync_status_out::Required::NormalSync
|
||||
} else {
|
||||
sync_status_out::Required::NoChanges
|
||||
|
@ -687,9 +685,10 @@ impl Collection {
|
|||
}
|
||||
|
||||
fn sync_meta(&self) -> Result<SyncMeta> {
|
||||
let stamps = self.storage.get_collection_timestamps()?;
|
||||
Ok(SyncMeta {
|
||||
modified: self.storage.get_modified_time()?,
|
||||
schema: self.storage.get_schema_mtime()?,
|
||||
modified: stamps.collection_change,
|
||||
schema: stamps.schema_change,
|
||||
usn: self.storage.usn(true)?,
|
||||
current_time: TimestampSecs::now(),
|
||||
server_message: "".into(),
|
||||
|
@ -894,7 +893,8 @@ impl Collection {
|
|||
};
|
||||
if proceed {
|
||||
let conf = conf.into();
|
||||
self.storage.add_or_update_deck_config(&conf)?;
|
||||
self.storage
|
||||
.add_or_update_deck_config_with_existing_id(&conf)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
|
|
@ -80,9 +80,10 @@ impl LocalServer {
|
|||
#[async_trait(?Send)]
|
||||
impl SyncServer for LocalServer {
|
||||
async fn meta(&self) -> Result<SyncMeta> {
|
||||
let stamps = self.col.storage.get_collection_timestamps()?;
|
||||
Ok(SyncMeta {
|
||||
modified: self.col.storage.get_modified_time()?,
|
||||
schema: self.col.storage.get_schema_mtime()?,
|
||||
modified: stamps.collection_change,
|
||||
schema: stamps.schema_change,
|
||||
usn: self.col.storage.usn(true)?,
|
||||
current_time: TimestampSecs::now(),
|
||||
server_message: String::new(),
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
use crate::{
|
||||
card::undo::UndoableCardChange, config::undo::UndoableConfigChange,
|
||||
card::undo::UndoableCardChange, collection::undo::UndoableCollectionChange,
|
||||
config::undo::UndoableConfigChange, deckconf::undo::UndoableDeckConfigChange,
|
||||
decks::undo::UndoableDeckChange, notes::undo::UndoableNoteChange, prelude::*,
|
||||
revlog::undo::UndoableRevlogChange, scheduler::queue::undo::UndoableQueueChange,
|
||||
tags::undo::UndoableTagChange,
|
||||
|
@ -13,10 +14,12 @@ pub(crate) enum UndoableChange {
|
|||
Card(UndoableCardChange),
|
||||
Note(UndoableNoteChange),
|
||||
Deck(UndoableDeckChange),
|
||||
DeckConfig(UndoableDeckConfigChange),
|
||||
Tag(UndoableTagChange),
|
||||
Revlog(UndoableRevlogChange),
|
||||
Queue(UndoableQueueChange),
|
||||
Config(UndoableConfigChange),
|
||||
Collection(UndoableCollectionChange),
|
||||
}
|
||||
|
||||
impl UndoableChange {
|
||||
|
@ -29,6 +32,8 @@ impl UndoableChange {
|
|||
UndoableChange::Revlog(c) => col.undo_revlog_change(c),
|
||||
UndoableChange::Queue(c) => col.undo_queue_change(c),
|
||||
UndoableChange::Config(c) => col.undo_config_change(c),
|
||||
UndoableChange::DeckConfig(c) => col.undo_deck_config_change(c),
|
||||
UndoableChange::Collection(c) => col.undo_collection_change(c),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -51,6 +56,12 @@ impl From<UndoableDeckChange> for UndoableChange {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<UndoableDeckConfigChange> for UndoableChange {
|
||||
fn from(c: UndoableDeckConfigChange) -> Self {
|
||||
UndoableChange::DeckConfig(c)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UndoableTagChange> for UndoableChange {
|
||||
fn from(c: UndoableTagChange) -> Self {
|
||||
UndoableChange::Tag(c)
|
||||
|
@ -74,3 +85,9 @@ impl From<UndoableConfigChange> for UndoableChange {
|
|||
UndoableChange::Config(c)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UndoableCollectionChange> for UndoableChange {
|
||||
fn from(c: UndoableCollectionChange) -> Self {
|
||||
UndoableChange::Collection(c)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -127,6 +127,8 @@ impl UndoManager {
|
|||
UndoableChange::Revlog(_) => {}
|
||||
UndoableChange::Queue(_) => {}
|
||||
UndoableChange::Config(_) => changes.config = true,
|
||||
UndoableChange::DeckConfig(_) => changes.deck_config = true,
|
||||
UndoableChange::Collection(_) => changes.collection = true,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue