store config in separate DB table

- mtime is tracked on each key individually, which will allow
merging of config changes when syncing in the future
- added col.(get|set|remove)_config()
- in order to support existing code that was mutating returned
values (eg col.conf["something"]["another"] = 5), the returned list/dict
will be automatically wrapped so that when the value is dropped, it
will save the mutated item back to the DB if it's changed. Code that
is fetching lists/dicts from the config like so:

col.conf["foo"]["bar"] = baz
col.setMod()

will continue to work in most case, but should be gradually updated to:

conf = col.get_config("foo")
conf["bar"] = baz
col.set_config("foo", conf)
This commit is contained in:
Damien Elmes 2020-04-05 21:38:58 +10:00
parent 8b76098bc7
commit 676f4e74a8
24 changed files with 539 additions and 125 deletions

View file

@ -61,6 +61,10 @@ message BackendInput {
string canonify_tags = 49;
Empty all_tags = 50;
int32 get_changed_tags = 51;
string get_config_json = 52;
SetConfigJson set_config_json = 53;
string set_all_config = 54;
Empty get_all_config = 55;
}
}
@ -105,6 +109,10 @@ message BackendOutput {
CanonifyTagsOut canonify_tags = 49;
AllTagsOut all_tags = 50;
GetChangedTagsOut get_changed_tags = 51;
string get_config_json = 52;
Empty set_config_json = 53;
Empty set_all_config = 54;
string get_all_config = 55;
BackendError error = 2047;
}
@ -454,3 +462,11 @@ message CanonifyTagsOut {
string tags = 1;
bool tag_list_changed = 2;
}
message SetConfigJson {
string key = 1;
oneof op {
string val = 2;
Empty remove = 3;
}
}

View file

@ -5,7 +5,6 @@ from __future__ import annotations
import copy
import datetime
import json
import os
import pprint
import random
@ -22,6 +21,7 @@ import anki.latex # sets up hook
import anki.template
from anki import hooks
from anki.cards import Card
from anki.config import ConfigManager
from anki.consts import *
from anki.dbproxy import DBProxy
from anki.decks import DeckManager
@ -45,25 +45,6 @@ from anki.utils import (
stripHTMLMedia,
)
defaultConf = {
# review options
"activeDecks": [1],
"curDeck": 1,
"newSpread": NEW_CARDS_DISTRIBUTE,
"collapseTime": 1200,
"timeLim": 0,
"estTimes": True,
"dueCounts": True,
# other config
"curModel": None,
"nextPos": 1,
"sortType": "noteFld",
"sortBackwards": False,
"addToCur": True, # add new to currently selected deck?
"dayLearnFirst": False,
"schedVer": 1,
}
# this is initialized by storage.Collection
class _Collection:
@ -75,7 +56,6 @@ class _Collection:
dty: bool # no longer used
_usn: int
ls: int
conf: Dict[str, Any]
_undo: List[Any]
def __init__(
@ -97,6 +77,7 @@ class _Collection:
self.models = ModelManager(self)
self.decks = DeckManager(self)
self.tags = TagManager(self)
self.conf = ConfigManager(self)
self.load()
if not self.crt:
d = datetime.datetime.today()
@ -179,23 +160,21 @@ class _Collection:
self.dty, # no longer used
self._usn,
self.ls,
conf,
models,
decks,
) = self.db.first(
"""
select crt, mod, scm, dty, usn, ls,
conf, models, decks from col"""
models, decks from col"""
)
self.conf = json.loads(conf)
self.models.load(models)
self.decks.load(decks)
def setMod(self) -> None:
"""Mark DB modified.
DB operations and the deck/tag/model managers do this automatically, so this
is only necessary if you modify properties of this object or the conf dict."""
DB operations and the deck/model managers do this automatically, so this
is only necessary if you modify properties of this object."""
self.db.mod = True
def flush(self, mod: Optional[int] = None) -> None:
@ -203,14 +182,13 @@ is only necessary if you modify properties of this object or the conf dict."""
self.mod = intTime(1000) if mod is None else mod
self.db.execute(
"""update col set
crt=?, mod=?, scm=?, dty=?, usn=?, ls=?, conf=?""",
crt=?, mod=?, scm=?, dty=?, usn=?, ls=?""",
self.crt,
self.mod,
self.scm,
self.dty,
self._usn,
self.ls,
json.dumps(self.conf),
)
def flush_all_changes(self, mod: Optional[int] = None):
@ -651,6 +629,23 @@ where c.nid = n.id and c.id in %s group by nid"""
findCards = find_cards
findNotes = find_notes
# Config
##########################################################################
def get_config(self, key: str, default: Any = None) -> Any:
try:
return self.conf.get_immutable(key)
except KeyError:
return default
def set_config(self, key: str, val: Any):
self.setMod()
self.conf.set(key, val)
def remove_config(self, key):
self.setMod()
self.conf.remove(key)
# Stats
##########################################################################

121
pylib/anki/config.py Normal file
View file

@ -0,0 +1,121 @@
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
"""
Config handling
- To set a config value, use col.set_config(key, val).
- To get a config value, use col.get_config(key, default=None). In
the case of lists and dictionaries, any changes you make to the returned
value will not be saved unless you call set_config().
- To remove a config value, use col.remove_config(key).
For legacy reasons, the config is also exposed as a dict interface
as col.conf. To support old code that was mutating inner values,
using col.conf["key"] needs to wrap lists and dicts when returning them.
As this is less efficient, please use the col.*_config() API in new code.
"""
from __future__ import annotations
import copy
import json
import weakref
from typing import Any
import anki
class ConfigManager:
def __init__(self, col: anki.storage._Collection):
self.col = col.weakref()
def get_immutable(self, key: str) -> Any:
s = self.col.backend.get_config_json(key)
if not s:
raise KeyError
return json.loads(s)
def set(self, key: str, val: Any) -> None:
self.col.backend.set_config_json(key, val)
def remove(self, key: str) -> None:
self.col.backend.remove_config(key)
# Legacy dict interface
#########################
def __getitem__(self, key):
val = self.get_immutable(key)
if isinstance(val, list):
print(
f"conf key {key} should be fetched with col.get_config(), and saved with col.set_config()"
)
return WrappedList(weakref.ref(self), key, val)
elif isinstance(val, dict):
print(
f"conf key {key} should be fetched with col.get_config(), and saved with col.set_config()"
)
return WrappedDict(weakref.ref(self), key, val)
else:
return val
def __setitem__(self, key, value):
self.set(key, value)
def get(self, key, default=None):
try:
return self[key]
except KeyError:
return default
def setdefault(self, key, default):
if key not in self:
self[key] = default
return self[key]
def __contains__(self, key):
try:
self.get_immutable(key)
return True
except KeyError:
return False
def __delitem__(self, key):
self.remove(key)
# Tracking changes to mutable objects
#########################################
# Because we previously allowed mutation of the conf
# structure directly, to allow col.conf["foo"]["bar"] = xx
# to continue to function, we apply changes as the object
# is dropped.
class WrappedList(list):
def __init__(self, conf, key, val):
self.key = key
self.conf = conf
self.orig = copy.deepcopy(val)
super().__init__(val)
def __del__(self):
cur = list(self)
conf = self.conf()
if conf and self.orig != cur:
conf[self.key] = cur
class WrappedDict(dict):
def __init__(self, conf, key, val):
self.key = key
self.conf = conf
self.orig = copy.deepcopy(val)
super().__init__(val)
def __del__(self):
cur = dict(self)
conf = self.conf()
if conf and self.orig != cur:
conf[self.key] = cur

View file

@ -524,8 +524,8 @@ class DeckManager:
#############################################################
def active(self) -> Any:
"The currrently active dids. Make sure to copy before modifying."
return self.col.conf["activeDecks"]
"The currrently active dids."
return self.col.get_config("activeDecks", [1])
def selected(self) -> Any:
"The currently selected did."

View file

@ -577,6 +577,32 @@ class RustBackend:
).get_changed_tags.tags
)
def get_config_json(self, key: str) -> str:
return self._run_command(pb.BackendInput(get_config_json=key)).get_config_json
def set_config_json(self, key: str, val: Any):
self._run_command(
pb.BackendInput(
set_config_json=pb.SetConfigJson(key=key, val=json.dumps(val))
)
)
def remove_config(self, key: str):
self._run_command(
pb.BackendInput(
set_config_json=pb.SetConfigJson(key=key, remove=pb.Empty())
)
)
def get_all_config(self) -> Dict[str, Any]:
jstr = self._run_command(
pb.BackendInput(get_all_config=pb.Empty())
).get_all_config
return json.loads(jstr)
def set_all_config(self, conf: Dict[str, Any]):
self._run_command(pb.BackendInput(set_all_config=json.dumps(conf)))
def translate_string_in(
key: TR, **kwargs: Union[str, int, float]

View file

@ -6,10 +6,9 @@ import json
import os
import weakref
from dataclasses import dataclass
from typing import Any, Dict, Optional, Tuple
from typing import Optional
from anki.collection import _Collection
from anki.consts import *
from anki.dbproxy import DBProxy
from anki.lang import _
from anki.media import media_paths_from_col_path
@ -74,26 +73,18 @@ def Collection(
def initial_db_setup(db: DBProxy) -> None:
db.begin()
_addColVars(db, *_getColVars(db))
def _getColVars(db: DBProxy) -> Tuple[Any, Dict[str, Any]]:
import anki.collection
import anki.decks
db.begin()
g = copy.deepcopy(anki.decks.defaultDeck)
g["id"] = 1
g["name"] = _("Default")
g["conf"] = 1
g["mod"] = intTime()
return g, anki.collection.defaultConf.copy()
def _addColVars(db: DBProxy, g: Dict[str, Any], c: Dict[str, Any]) -> None:
db.execute(
"""
update col set conf = ?, decks = ?""",
json.dumps(c),
update col set decks = ?""",
json.dumps({"1": g}),
)

View file

@ -449,11 +449,11 @@ from notes where %s"""
# Col config
##########################################################################
def getConf(self) -> Any:
return self.col.conf
def getConf(self) -> Dict[str, Any]:
return self.col.backend.get_all_config()
def mergeConf(self, conf) -> None:
self.col.conf = conf
def mergeConf(self, conf: Dict[str, Any]) -> None:
self.col.backend.set_all_config(conf)
# HTTP syncing tools

View file

@ -80,7 +80,7 @@ class DataModel(QAbstractTableModel):
self.browser = browser
self.col = browser.col
self.sortKey = None
self.activeCols = self.col.conf.get(
self.activeCols = self.col.get_config(
"activeCols", ["noteFld", "template", "cardDue", "deck"]
)
self.cards: Sequence[int] = []
@ -1121,7 +1121,7 @@ QTableView {{ gridline-color: {grid} }}
def _favTree(self, root) -> None:
assert self.col
saved = self.col.conf.get("savedFilters", {})
saved = self.col.get_config("savedFilters", {})
for name, filt in sorted(saved.items()):
item = SidebarItem(
name,
@ -1360,7 +1360,7 @@ QTableView {{ gridline-color: {grid} }}
ml = MenuList()
# make sure exists
if "savedFilters" not in self.col.conf:
self.col.conf["savedFilters"] = {}
self.col.set_config("savedFilters", {})
ml.addSeparator()
@ -1369,7 +1369,7 @@ QTableView {{ gridline-color: {grid} }}
else:
ml.addItem(_("Save Current Filter..."), self._onSaveFilter)
saved = self.col.conf["savedFilters"]
saved = self.col.get_config("savedFilters")
if not saved:
return ml
@ -1384,8 +1384,9 @@ QTableView {{ gridline-color: {grid} }}
if not name:
return
filt = self.form.searchEdit.lineEdit().text()
self.col.conf["savedFilters"][name] = filt
self.col.setMod()
conf = self.col.get_config("savedFilters")
conf[name] = filt
self.col.save_config("savedFilters", conf)
self.maybeRefreshSidebar()
def _onRemoveFilter(self):
@ -1399,7 +1400,7 @@ QTableView {{ gridline-color: {grid} }}
# returns name if found
def _currentFilterIsSaved(self):
filt = self.form.searchEdit.lineEdit().text()
for k, v in self.col.conf["savedFilters"].items():
for k, v in self.col.get_config("savedFilters").items():
if filt == v:
return k
return None

View file

@ -2,7 +2,6 @@
# -*- coding: utf-8 -*-
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from operator import itemgetter
from typing import Union
import aqt

View file

@ -2,7 +2,6 @@
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use crate::backend::dbproxy::db_command_bytes;
use crate::backend_proto::backend_input::Value;
use crate::backend_proto::{
AddOrUpdateDeckConfigIn, BuiltinSortKind, Empty, RenderedTemplateReplacement, SyncMediaIn,
};
@ -34,7 +33,9 @@ use crate::{backend_proto as pb, log};
use fluent::FluentValue;
use futures::future::{AbortHandle, Abortable};
use log::error;
use pb::backend_input::Value;
use prost::Message;
use serde_json::Value as JsonValue;
use std::collections::{HashMap, HashSet};
use std::convert::TryFrom;
use std::path::PathBuf;
@ -291,6 +292,17 @@ impl Backend {
Value::AllTags(_) => OValue::AllTags(self.all_tags()?),
Value::RegisterTags(input) => OValue::RegisterTags(self.register_tags(input)?),
Value::GetChangedTags(usn) => OValue::GetChangedTags(self.get_changed_tags(usn)?),
Value::GetConfigJson(key) => OValue::GetConfigJson(self.get_config_json(&key)?),
Value::SetConfigJson(input) => OValue::SetConfigJson({
self.set_config_json(input)?;
pb::Empty {}
}),
Value::SetAllConfig(input) => OValue::SetConfigJson({
self.set_all_config(&input)?;
pb::Empty {}
}),
Value::GetAllConfig(_) => OValue::GetAllConfig(self.get_all_config()?),
})
}
@ -400,8 +412,8 @@ impl Backend {
fn sched_timing_today(&self, input: pb::SchedTimingTodayIn) -> pb::SchedTimingTodayOut {
let today = sched_timing_today(
input.created_secs as i64,
input.now_secs as i64,
TimestampSecs(input.created_secs),
TimestampSecs(input.now_secs),
input.created_mins_west.map(|v| v.val),
input.now_mins_west.map(|v| v.val),
input.rollover_hour.map(|v| v.val as i8),
@ -783,6 +795,52 @@ impl Backend {
})
})
}
fn get_config_json(&self, key: &str) -> Result<String> {
self.with_col(|col| {
let val: Option<JsonValue> = col.get_config_optional(key);
match val {
None => Ok("".to_string()),
Some(val) => Ok(serde_json::to_string(&val)?),
}
})
}
fn set_config_json(&self, input: pb::SetConfigJson) -> Result<()> {
self.with_col(|col| {
col.transact(None, |col| {
if let Some(op) = input.op {
match op {
pb::set_config_json::Op::Val(val) => {
// ensure it's a well-formed object
let val: JsonValue = serde_json::from_str(&val)?;
col.set_config(&input.key, &val)
}
pb::set_config_json::Op::Remove(_) => col.remove_config(&input.key),
}
} else {
Err(AnkiError::invalid_input("no op received"))
}
})
})
}
fn set_all_config(&self, conf: &str) -> Result<()> {
let val: HashMap<String, JsonValue> = serde_json::from_str(conf)?;
self.with_col(|col| {
col.transact(None, |col| {
col.storage
.set_all_config(val, col.usn()?, TimestampSecs::now())
})
})
}
fn get_all_config(&self) -> Result<String> {
self.with_col(|col| {
let conf = col.storage.get_all_config()?;
serde_json::to_string(&conf).map_err(Into::into)
})
}
}
fn translate_arg_to_fluent_val(arg: &pb::TranslateArgValue) -> FluentValue {

View file

@ -6,7 +6,11 @@ use crate::i18n::I18n;
use crate::log::Logger;
use crate::timestamp::TimestampSecs;
use crate::types::Usn;
use crate::{sched::cutoff::SchedTimingToday, storage::SqliteStorage, undo::UndoManager};
use crate::{
sched::cutoff::{sched_timing_today, SchedTimingToday},
storage::SqliteStorage,
undo::UndoManager,
};
use std::path::PathBuf;
pub fn open_collection<P: Into<PathBuf>>(
@ -141,8 +145,24 @@ impl Collection {
return Ok(*timing);
}
}
self.state.timing_today = Some(self.storage.timing_today(self.server)?);
Ok(self.state.timing_today.clone().unwrap())
let local_offset = if self.server {
self.get_local_mins_west()
} else {
None
};
let timing = sched_timing_today(
self.storage.creation_stamp()?,
TimestampSecs::now(),
self.get_creation_mins_west(),
local_offset,
self.get_rollover(),
);
self.state.timing_today = Some(timing);
Ok(timing)
}
pub(crate) fn usn(&self) -> Result<Usn> {

View file

@ -1,26 +1,111 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use crate::collection::Collection;
use crate::decks::DeckID;
use crate::serde::default_on_invalid;
use serde_aux::field_attributes::deserialize_number_from_string;
use crate::err::Result;
use crate::timestamp::TimestampSecs;
use serde::{de::DeserializeOwned, Serialize};
use serde_derive::Deserialize;
use serde_json::json;
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Config {
#[serde(
rename = "curDeck",
deserialize_with = "deserialize_number_from_string"
)]
pub(crate) current_deck_id: DeckID,
pub(crate) rollover: Option<i8>,
pub(crate) creation_offset: Option<i32>,
pub(crate) local_offset: Option<i32>,
#[serde(rename = "sortType", deserialize_with = "default_on_invalid")]
pub(crate) browser_sort_kind: SortKind,
#[serde(rename = "sortBackwards", deserialize_with = "default_on_invalid")]
pub(crate) browser_sort_reverse: bool,
pub(crate) fn schema11_config_as_string() -> String {
let obj = json!({
"activeDecks": [1],
"curDeck": 1,
"newSpread": 0,
"collapseTime": 1200,
"timeLim": 0,
"estTimes": true,
"dueCounts": true,
"curModel": null,
"nextPos": 1,
"sortType": "noteFld",
"sortBackwards": false,
"addToCur": true,
"dayLearnFirst": false,
"schedVer": 1,
});
serde_json::to_string(&obj).unwrap()
}
pub(crate) enum ConfigKey {
BrowserSortKind,
BrowserSortReverse,
CurrentDeckID,
CreationOffset,
Rollover,
LocalOffset,
}
impl From<ConfigKey> for &'static str {
fn from(c: ConfigKey) -> Self {
match c {
ConfigKey::BrowserSortKind => "sortType",
ConfigKey::BrowserSortReverse => "sortBackwards",
ConfigKey::CurrentDeckID => "curDeck",
ConfigKey::CreationOffset => "creationOffset",
ConfigKey::Rollover => "rollover",
ConfigKey::LocalOffset => "localOffset",
}
}
}
impl Collection {
/// Get config item, returning None if missing/invalid.
pub(crate) fn get_config_optional<'a, T, K>(&self, key: K) -> Option<T>
where
T: DeserializeOwned,
K: Into<&'a str>,
{
match self.storage.get_config_value(key.into()) {
Ok(Some(val)) => val,
_ => None,
}
}
// /// Get config item, returning default value if missing/invalid.
pub(crate) fn get_config_default<T, K>(&self, key: K) -> T
where
T: DeserializeOwned + Default,
K: Into<&'static str>,
{
self.get_config_optional(key).unwrap_or_default()
}
pub(crate) fn set_config<T: Serialize>(&self, key: &str, val: &T) -> Result<()> {
self.storage
.set_config_value(key, val, self.usn()?, TimestampSecs::now())
}
pub(crate) fn remove_config(&self, key: &str) -> Result<()> {
self.storage.remove_config(key)
}
pub(crate) fn get_browser_sort_kind(&self) -> SortKind {
self.get_config_default(ConfigKey::BrowserSortKind)
}
pub(crate) fn get_browser_sort_reverse(&self) -> bool {
self.get_config_default(ConfigKey::BrowserSortReverse)
}
pub(crate) fn get_current_deck_id(&self) -> DeckID {
self.get_config_optional(ConfigKey::CurrentDeckID)
.unwrap_or(DeckID(1))
}
pub(crate) fn get_creation_mins_west(&self) -> Option<i32> {
self.get_config_optional(ConfigKey::CreationOffset)
}
pub(crate) fn get_local_mins_west(&self) -> Option<i32> {
self.get_config_optional(ConfigKey::LocalOffset)
}
pub(crate) fn get_rollover(&self) -> Option<i8> {
self.get_config_optional(ConfigKey::Rollover)
}
}
#[derive(Deserialize, PartialEq, Debug)]

View file

@ -1,6 +1,7 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use crate::timestamp::TimestampSecs;
use chrono::{Date, Duration, FixedOffset, Local, TimeZone};
#[derive(Debug, PartialEq, Clone, Copy)]
@ -138,25 +139,25 @@ fn sched_timing_today_v2_legacy(
/// Based on provided input, get timing info from the relevant function.
pub(crate) fn sched_timing_today(
created_secs: i64,
now_secs: i64,
created_secs: TimestampSecs,
now_secs: TimestampSecs,
created_mins_west: Option<i32>,
now_mins_west: Option<i32>,
rollover_hour: Option<i8>,
) -> SchedTimingToday {
let now_west = now_mins_west.unwrap_or_else(|| local_minutes_west_for_stamp(now_secs));
let now_west = now_mins_west.unwrap_or_else(|| local_minutes_west_for_stamp(now_secs.0));
match (rollover_hour, created_mins_west) {
(None, _) => {
// if rollover unset, v1 scheduler
sched_timing_today_v1(created_secs, now_secs)
sched_timing_today_v1(created_secs.0, now_secs.0)
}
(Some(roll), None) => {
// if creationOffset unset, v2 scheduler with legacy cutoff handling
sched_timing_today_v2_legacy(created_secs, roll, now_secs, now_west)
sched_timing_today_v2_legacy(created_secs.0, roll, now_secs.0, now_west)
}
(Some(roll), Some(crt_west)) => {
// v2 scheduler, new cutoff handling
sched_timing_today_v2_new(created_secs, crt_west, now_secs, now_west, roll)
sched_timing_today_v2_new(created_secs.0, crt_west, now_secs.0, now_west, roll)
}
}
}

View file

@ -33,10 +33,10 @@ pub(crate) fn search_cards<'a, 'b>(
match order {
SortMode::NoOrder => (),
SortMode::FromConfig => {
let conf = req.storage.all_config()?;
prepare_sort(req, &conf.browser_sort_kind)?;
let kind = req.get_browser_sort_kind();
prepare_sort(req, &kind)?;
sql.push_str(" order by ");
write_order(&mut sql, &conf.browser_sort_kind, conf.browser_sort_reverse)?;
write_order(&mut sql, &kind, req.get_browser_sort_reverse())?;
}
SortMode::Builtin { kind, reverse } => {
prepare_sort(req, &kind)?;

View file

@ -231,9 +231,9 @@ impl SqlWriter<'_> {
.map(|(_, v)| v)
.collect();
let dids_with_children = if deck == "current" {
let config = self.col.storage.all_config()?;
let mut dids_with_children = vec![config.current_deck_id];
let current = get_deck(&all_decks, config.current_deck_id)
let current_id = self.col.get_current_deck_id();
let mut dids_with_children = vec![current_id];
let current = get_deck(&all_decks, current_id)
.ok_or_else(|| AnkiError::invalid_input("invalid current deck"))?;
for child_did in child_ids(&all_decks, &current.name) {
dids_with_children.push(child_did);

View file

@ -0,0 +1,4 @@
insert
or replace into config (key, usn, mtime_secs, val)
values
(?, ?, ?, ?)

View file

@ -0,0 +1,5 @@
select
val
from config
where
key = ?

View file

@ -0,0 +1,88 @@
// 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::{err::Result, timestamp::TimestampSecs, types::Usn};
use rusqlite::{params, NO_PARAMS};
use serde::{de::DeserializeOwned, Serialize};
use serde_json::Value;
use std::collections::HashMap;
impl SqliteStorage {
pub(crate) fn set_config_value<T: Serialize>(
&self,
key: &str,
val: &T,
usn: Usn,
mtime: TimestampSecs,
) -> Result<()> {
let json = serde_json::to_string(val)?;
self.db
.prepare_cached(include_str!("add.sql"))?
.execute(params![key, usn, mtime, json])?;
Ok(())
}
pub(crate) fn remove_config(&self, key: &str) -> Result<()> {
self.db
.prepare_cached("delete from config where key=?")?
.execute(&[key])?;
Ok(())
}
pub(crate) fn get_config_value<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>> {
self.db
.prepare_cached(include_str!("get.sql"))?
.query_and_then(&[key], |row| {
serde_json::from_str(row.get_raw(0).as_str()?).map_err(Into::into)
})?
.next()
.transpose()
}
pub(crate) fn get_all_config(&self) -> Result<HashMap<String, Value>> {
self.db
.prepare("select key, val from config")?
.query_and_then(NO_PARAMS, |row| {
let val: Value = serde_json::from_str(row.get_raw(1).as_str()?)?;
Ok((row.get::<usize, String>(0)?, val))
})?
.collect()
}
pub(crate) fn set_all_config(
&self,
conf: HashMap<String, Value>,
usn: Usn,
mtime: TimestampSecs,
) -> Result<()> {
self.db.execute("delete from config", NO_PARAMS)?;
for (key, val) in conf.iter() {
self.set_config_value(key, val, usn, mtime)?;
}
Ok(())
}
// Upgrading/downgrading
pub(super) fn upgrade_config_to_schema14(&self) -> Result<()> {
let conf = self
.db
.query_row_and_then("select conf from col", NO_PARAMS, |row| {
let conf: Result<HashMap<String, Value>> =
serde_json::from_str(row.get_raw(0).as_str()?).map_err(Into::into);
conf
})?;
self.set_all_config(conf, Usn(0), TimestampSecs(0))?;
self.db.execute_batch("update col set conf=''")?;
Ok(())
}
pub(super) fn downgrade_config_from_schema14(&self) -> Result<()> {
let allconf = self.get_all_config()?;
self.db
.execute("update col set conf=?", &[serde_json::to_string(&allconf)?])?;
Ok(())
}
}

View file

@ -2,6 +2,7 @@
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
mod card;
mod config;
mod deckconf;
mod sqlite;
mod tag;

View file

@ -1,20 +1,13 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use crate::config::Config;
use crate::config::schema11_config_as_string;
use crate::decks::DeckID;
use crate::err::Result;
use crate::err::{AnkiError, DBErrorKind};
use crate::notetypes::NoteTypeID;
use crate::timestamp::{TimestampMillis, TimestampSecs};
use crate::{
decks::Deck,
i18n::I18n,
notetypes::NoteType,
sched::cutoff::{sched_timing_today, SchedTimingToday},
text::without_combining,
types::Usn,
};
use crate::{decks::Deck, i18n::I18n, notetypes::NoteType, text::without_combining, types::Usn};
use regex::Regex;
use rusqlite::{functions::FunctionFlags, params, Connection, NO_PARAMS};
use std::cmp::Ordering;
@ -23,7 +16,7 @@ use unicase::UniCase;
const SCHEMA_MIN_VERSION: u8 = 11;
const SCHEMA_STARTING_VERSION: u8 = 11;
const SCHEMA_MAX_VERSION: u8 = 13;
const SCHEMA_MAX_VERSION: u8 = 14;
fn unicase_compare(s1: &str, s2: &str) -> Ordering {
UniCase::new(s1).cmp(&UniCase::new(s2))
@ -182,8 +175,12 @@ impl SqliteStorage {
db.execute_batch(include_str!("schema11.sql"))?;
// start at schema 11, then upgrade below
db.execute(
"update col set crt=?, ver=?",
params![TimestampSecs::now(), SCHEMA_STARTING_VERSION],
"update col set crt=?, ver=?, conf=?",
params![
TimestampSecs::now(),
SCHEMA_STARTING_VERSION,
&schema11_config_as_string()
],
)?;
}
@ -290,13 +287,6 @@ impl SqliteStorage {
})
}
pub(crate) fn all_config(&self) -> Result<Config> {
self.db
.query_row_and_then("select conf from col", NO_PARAMS, |row| -> Result<_> {
Ok(serde_json::from_str(row.get_raw(0).as_str()?)?)
})
}
pub(crate) fn all_note_types(&self) -> Result<HashMap<NoteTypeID, NoteType>> {
let mut stmt = self.db.prepare("select models from col")?;
let note_types = stmt
@ -313,21 +303,11 @@ impl SqliteStorage {
Ok(note_types)
}
pub(crate) fn timing_today(&self, server: bool) -> Result<SchedTimingToday> {
let crt: i64 = self
.db
pub(crate) fn creation_stamp(&self) -> Result<TimestampSecs> {
self.db
.prepare_cached("select crt from col")?
.query_row(NO_PARAMS, |row| row.get(0))?;
let conf = self.all_config()?;
let now_offset = if server { conf.local_offset } else { None };
Ok(sched_timing_today(
crt,
TimestampSecs::now().0,
conf.creation_offset,
now_offset,
conf.rollover,
))
.query_row(NO_PARAMS, |row| row.get(0))
.map_err(Into::into)
}
pub(crate) fn schema_modified(&self) -> Result<bool> {

View file

@ -58,7 +58,7 @@ impl SqliteStorage {
// Upgrading/downgrading
pub(super) fn upgrade_tags_to_schema12(&self) -> Result<()> {
pub(super) fn upgrade_tags_to_schema13(&self) -> Result<()> {
let tags = self
.db
.query_row_and_then("select tags from col", NO_PARAMS, |row| {

View file

@ -14,14 +14,24 @@ impl SqliteStorage {
if ver < 13 {
self.db
.execute_batch(include_str!("schema13_upgrade.sql"))?;
self.upgrade_tags_to_schema12()?;
self.upgrade_tags_to_schema13()?;
}
if ver < 14 {
self.db
.execute_batch(include_str!("schema14_upgrade.sql"))?;
self.upgrade_config_to_schema14()?;
}
Ok(())
}
pub(super) fn downgrade_to_schema_11(&self) -> Result<()> {
self.begin_trx()?;
self.downgrade_config_from_schema14()?;
self.db
.execute_batch(include_str!("schema14_downgrade.sql"))?;
self.downgrade_tags_from_schema13()?;
self.db
.execute_batch(include_str!("schema13_downgrade.sql"))?;

View file

@ -0,0 +1,4 @@
drop table config;
update col
set
ver = 13;

View file

@ -0,0 +1,9 @@
create table config (
key text not null primary key,
usn integer not null,
mtime_secs integer not null,
val text not null
) without rowid;
update col
set
ver = 14;