Merge remote-tracking branch 'danielelmes/master' into create_actions_for_windows_macos

This commit is contained in:
evandrocoan 2020-03-26 13:41:00 -03:00
commit b07454ca0c
26 changed files with 508 additions and 132 deletions

View file

@ -47,6 +47,8 @@ message BackendInput {
Empty restore_trash = 35; Empty restore_trash = 35;
OpenCollectionIn open_collection = 36; OpenCollectionIn open_collection = 36;
Empty close_collection = 37; Empty close_collection = 37;
int64 get_card = 38;
Card update_card = 39;
} }
} }
@ -77,6 +79,8 @@ message BackendOutput {
Empty restore_trash = 35; Empty restore_trash = 35;
Empty open_collection = 36; Empty open_collection = 36;
Empty close_collection = 37; Empty close_collection = 37;
GetCardOut get_card = 38;
Empty update_card = 39;
BackendError error = 2047; BackendError error = 2047;
} }
@ -367,3 +371,28 @@ enum BuiltinSortKind {
CARD_DECK = 11; CARD_DECK = 11;
CARD_TEMPLATE = 12; CARD_TEMPLATE = 12;
} }
message GetCardOut {
Card card = 1;
}
message Card {
int64 id = 1;
int64 nid = 2;
int64 did = 3;
uint32 ord = 4;
int64 mtime = 5;
sint32 usn = 6;
uint32 ctype = 7;
sint32 queue = 8;
sint32 due = 9;
uint32 ivl = 10;
uint32 factor = 11;
uint32 reps = 12;
uint32 lapses = 13;
uint32 left = 14;
sint32 odue = 15;
int64 odid = 16;
uint32 flags = 17;
string data = 18;
}

View file

@ -12,6 +12,7 @@ from anki import hooks
from anki.consts import * from anki.consts import *
from anki.models import NoteType, Template from anki.models import NoteType, Template
from anki.notes import Note from anki.notes import Note
from anki.rsbackend import BackendCard
from anki.sound import AVTag from anki.sound import AVTag
from anki.utils import intTime, joinFields, timestampID from anki.utils import intTime, joinFields, timestampID
@ -33,9 +34,7 @@ class Card:
lastIvl: int lastIvl: int
ord: int ord: int
def __init__( def __init__(self, col: anki.storage._Collection, id: Optional[int] = None) -> None:
self, col: anki.collection._Collection, id: Optional[int] = None
) -> None:
self.col = col.weakref() self.col = col.weakref()
self.timerStarted = None self.timerStarted = None
self._render_output: Optional[anki.template.TemplateRenderOutput] = None self._render_output: Optional[anki.template.TemplateRenderOutput] = None
@ -61,68 +60,59 @@ class Card:
self.data = "" self.data = ""
def load(self) -> None: def load(self) -> None:
(
self.id,
self.nid,
self.did,
self.ord,
self.mod,
self.usn,
self.type,
self.queue,
self.due,
self.ivl,
self.factor,
self.reps,
self.lapses,
self.left,
self.odue,
self.odid,
self.flags,
self.data,
) = self.col.db.first("select * from cards where id = ?", self.id)
self._render_output = None self._render_output = None
self._note = None self._note = None
c = self.col.backend.get_card(self.id)
assert c
self.nid = c.nid
self.did = c.did
self.ord = c.ord
self.mod = c.mtime
self.usn = c.usn
self.type = c.ctype
self.queue = c.queue
self.due = c.due
self.ivl = c.ivl
self.factor = c.factor
self.reps = c.reps
self.lapses = c.lapses
self.left = c.left
self.odue = c.odue
self.odid = c.odid
self.flags = c.flags
self.data = c.data
def _preFlush(self) -> None: def _bugcheck(self) -> None:
hooks.card_will_flush(self)
self.mod = intTime()
self.usn = self.col.usn()
# bug check
if ( if (
self.queue == QUEUE_TYPE_REV self.queue == QUEUE_TYPE_REV
and self.odue and self.odue
and not self.col.decks.isDyn(self.did) and not self.col.decks.isDyn(self.did)
): ):
hooks.card_odue_was_invalid() hooks.card_odue_was_invalid()
assert self.due < 4294967296
def flush(self) -> None: def flush(self) -> None:
self._preFlush() self._bugcheck()
self.col.db.execute( hooks.card_will_flush(self)
""" # mtime & usn are set by backend
insert or replace into cards values card = BackendCard(
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", id=self.id,
self.id, nid=self.nid,
self.nid, did=self.did,
self.did, ord=self.ord,
self.ord, ctype=self.type,
self.mod, queue=self.queue,
self.usn, due=self.due,
self.type, ivl=self.ivl,
self.queue, factor=self.factor,
self.due, reps=self.reps,
self.ivl, lapses=self.lapses,
self.factor, left=self.left,
self.reps, odue=self.odue,
self.lapses, odid=self.odid,
self.left, flags=self.flags,
self.odue, data=self.data,
self.odid,
self.flags,
self.data,
) )
self.col.log(self) self.col.backend.update_card(card)
def question(self, reload: bool = False, browser: bool = False) -> str: def question(self, reload: bool = False, browser: bool = False) -> str:
return self.css() + self.render_output(reload, browser).question_text return self.css() + self.render_output(reload, browser).question_text

View file

@ -956,7 +956,7 @@ and type=0""",
) )
rowcount = self.db.scalar("select changes()") rowcount = self.db.scalar("select changes()")
if rowcount: if rowcount:
problems.append( syncable_problems.append(
"Found %d new cards with a due number >= 1,000,000 - consider repositioning them in the Browse screen." "Found %d new cards with a due number >= 1,000,000 - consider repositioning them in the Browse screen."
% rowcount % rowcount
) )

View file

@ -36,6 +36,7 @@ assert ankirspy.buildhash() == anki.buildinfo.buildhash
SchedTimingToday = pb.SchedTimingTodayOut SchedTimingToday = pb.SchedTimingTodayOut
BuiltinSortKind = pb.BuiltinSortKind BuiltinSortKind = pb.BuiltinSortKind
BackendCard = pb.Card
try: try:
import orjson import orjson
@ -480,6 +481,12 @@ class RustBackend:
pb.BackendInput(search_notes=pb.SearchNotesIn(search=search)) pb.BackendInput(search_notes=pb.SearchNotesIn(search=search))
).search_notes.note_ids ).search_notes.note_ids
def get_card(self, cid: int) -> Optional[pb.Card]:
return self._run_command(pb.BackendInput(get_card=cid)).get_card.card
def update_card(self, card: BackendCard) -> None:
self._run_command(pb.BackendInput(update_card=card))
def translate_string_in( def translate_string_in(
key: TR, **kwargs: Union[str, int, float] key: TR, **kwargs: Union[str, int, float]

View file

@ -341,7 +341,7 @@ limit %d"""
resched = self._resched(card) resched = self._resched(card)
if "mult" in conf and resched: if "mult" in conf and resched:
# review that's lapsed # review that's lapsed
card.ivl = max(1, conf["minInt"], card.ivl * conf["mult"]) card.ivl = max(1, conf["minInt"], int(card.ivl * conf["mult"]))
else: else:
# new card; no ivl adjustment # new card; no ivl adjustment
pass pass

View file

@ -40,14 +40,15 @@ class CardStats:
self.addLine(_("First Review"), self.date(first / 1000)) self.addLine(_("First Review"), self.date(first / 1000))
self.addLine(_("Latest Review"), self.date(last / 1000)) self.addLine(_("Latest Review"), self.date(last / 1000))
if c.type in (CARD_TYPE_LRN, CARD_TYPE_REV): if c.type in (CARD_TYPE_LRN, CARD_TYPE_REV):
next: Optional[str] = None
if c.odid or c.queue < QUEUE_TYPE_NEW: if c.odid or c.queue < QUEUE_TYPE_NEW:
next = None pass
else: else:
if c.queue in (QUEUE_TYPE_REV, QUEUE_TYPE_DAY_LEARN_RELEARN): if c.queue in (QUEUE_TYPE_REV, QUEUE_TYPE_DAY_LEARN_RELEARN):
next = time.time() + ((c.due - self.col.sched.today) * 86400) n = time.time() + ((c.due - self.col.sched.today) * 86400)
else: else:
next = c.due n = c.due
next = self.date(next) next = self.date(n)
if next: if next:
self.addLine(self.col.tr(TR.STATISTICS_DUE_DATE), next) self.addLine(self.col.tr(TR.STATISTICS_DUE_DATE), next)
if c.queue == QUEUE_TYPE_REV: if c.queue == QUEUE_TYPE_REV:

View file

@ -318,7 +318,7 @@ def test_modelChange():
try: try:
c1.load() c1.load()
assert 0 assert 0
except TypeError: except AssertionError:
pass pass
# but we have two cards, as a new one was generated # but we have two cards, as a new one was generated
assert len(f.cards()) == 2 assert len(f.cards()) == 2

View file

@ -968,7 +968,7 @@ def test_timing():
c2 = d.sched.getCard() c2 = d.sched.getCard()
assert c2.queue == QUEUE_TYPE_REV assert c2.queue == QUEUE_TYPE_REV
# if the failed card becomes due, it should show first # if the failed card becomes due, it should show first
c.due = time.time() - 1 c.due = intTime() - 1
c.flush() c.flush()
d.reset() d.reset()
c = d.sched.getCard() c = d.sched.getCard()

View file

@ -442,7 +442,7 @@ close the profile or restart Anki."""
def loadCollection(self) -> bool: def loadCollection(self) -> bool:
try: try:
return self._loadCollection() self._loadCollection()
except Exception as e: except Exception as e:
showWarning( showWarning(
tr(TR.ERRORS_UNABLE_OPEN_COLLECTION) + "\n" + traceback.format_exc() tr(TR.ERRORS_UNABLE_OPEN_COLLECTION) + "\n" + traceback.format_exc()
@ -460,15 +460,22 @@ close the profile or restart Anki."""
self.showProfileManager() self.showProfileManager()
return False return False
def _loadCollection(self) -> bool: # make sure we don't get into an inconsistent state if an add-on
# has broken the deck browser or the did_load hook
try:
self.maybeEnableUndo()
gui_hooks.collection_did_load(self.col)
self.moveToState("deckBrowser")
except Exception as e:
# dump error to stderr so it gets picked up by errors.py
traceback.print_exc()
return True
def _loadCollection(self):
cpath = self.pm.collectionPath() cpath = self.pm.collectionPath()
self.col = Collection(cpath, backend=self.backend) self.col = Collection(cpath, backend=self.backend)
self.setEnabled(True) self.setEnabled(True)
self.maybeEnableUndo()
gui_hooks.collection_did_load(self.col)
self.moveToState("deckBrowser")
return True
def reopen(self): def reopen(self):
cpath = self.pm.collectionPath() cpath = self.pm.collectionPath()
@ -1241,7 +1248,10 @@ and if the problem comes up again, please ask on the support site."""
########################################################################## ##########################################################################
def onSchemaMod(self, arg): def onSchemaMod(self, arg):
return askUser( progress_shown = self.progress.busy()
if progress_shown:
self.progress.finish()
ret = askUser(
_( _(
"""\ """\
The requested change will require a full upload of the database when \ The requested change will require a full upload of the database when \
@ -1250,6 +1260,9 @@ waiting on another device that haven't been synchronized here yet, they \
will be lost. Continue?""" will be lost. Continue?"""
) )
) )
if progress_shown:
self.progress.start()
return ret
# Advanced features # Advanced features
########################################################################## ##########################################################################

View file

@ -4,8 +4,11 @@
use crate::backend::dbproxy::db_command_bytes; use crate::backend::dbproxy::db_command_bytes;
use crate::backend_proto::backend_input::Value; use crate::backend_proto::backend_input::Value;
use crate::backend_proto::{BuiltinSortKind, Empty, RenderedTemplateReplacement, SyncMediaIn}; use crate::backend_proto::{BuiltinSortKind, Empty, RenderedTemplateReplacement, SyncMediaIn};
use crate::card::{Card, CardID};
use crate::card::{CardQueue, CardType};
use crate::collection::{open_collection, Collection}; use crate::collection::{open_collection, Collection};
use crate::config::SortKind; use crate::config::SortKind;
use crate::decks::DeckID;
use crate::err::{AnkiError, NetworkErrorKind, Result, SyncErrorKind}; use crate::err::{AnkiError, NetworkErrorKind, Result, SyncErrorKind};
use crate::i18n::{tr_args, FString, I18n}; use crate::i18n::{tr_args, FString, I18n};
use crate::latex::{extract_latex, extract_latex_expanding_clozes, ExtractedLatex}; use crate::latex::{extract_latex, extract_latex_expanding_clozes, ExtractedLatex};
@ -13,6 +16,7 @@ use crate::log::{default_logger, Logger};
use crate::media::check::MediaChecker; use crate::media::check::MediaChecker;
use crate::media::sync::MediaSyncProgress; use crate::media::sync::MediaSyncProgress;
use crate::media::MediaManager; use crate::media::MediaManager;
use crate::notes::NoteID;
use crate::sched::cutoff::{local_minutes_west_for_stamp, sched_timing_today}; use crate::sched::cutoff::{local_minutes_west_for_stamp, sched_timing_today};
use crate::sched::timespan::{answer_button_time, learning_congrats, studied_today, time_span}; use crate::sched::timespan::{answer_button_time, learning_congrats, studied_today, time_span};
use crate::search::{search_cards, search_notes, SortMode}; use crate::search::{search_cards, search_notes, SortMode};
@ -21,10 +25,13 @@ use crate::template::{
RenderedNode, RenderedNode,
}; };
use crate::text::{extract_av_tags, strip_av_tags, AVTag}; use crate::text::{extract_av_tags, strip_av_tags, AVTag};
use crate::timestamp::TimestampSecs;
use crate::types::Usn;
use crate::{backend_proto as pb, log}; use crate::{backend_proto as pb, log};
use fluent::FluentValue; use fluent::FluentValue;
use prost::Message; use prost::Message;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::convert::TryFrom;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use tokio::runtime::Runtime; use tokio::runtime::Runtime;
@ -248,6 +255,11 @@ impl Backend {
} }
Value::SearchCards(input) => OValue::SearchCards(self.search_cards(input)?), Value::SearchCards(input) => OValue::SearchCards(self.search_cards(input)?),
Value::SearchNotes(input) => OValue::SearchNotes(self.search_notes(input)?), Value::SearchNotes(input) => OValue::SearchNotes(self.search_notes(input)?),
Value::GetCard(cid) => OValue::GetCard(self.get_card(cid)?),
Value::UpdateCard(card) => {
self.update_card(card)?;
OValue::UpdateCard(pb::Empty {})
}
}) })
} }
@ -599,7 +611,9 @@ impl Backend {
SortMode::FromConfig SortMode::FromConfig
}; };
let cids = search_cards(ctx, &input.search, order)?; let cids = search_cards(ctx, &input.search, order)?;
Ok(pb::SearchCardsOut { card_ids: cids }) Ok(pb::SearchCardsOut {
card_ids: cids.into_iter().map(|v| v.0).collect(),
})
}) })
}) })
} }
@ -608,10 +622,24 @@ impl Backend {
self.with_col(|col| { self.with_col(|col| {
col.with_ctx(|ctx| { col.with_ctx(|ctx| {
let nids = search_notes(ctx, &input.search)?; let nids = search_notes(ctx, &input.search)?;
Ok(pb::SearchNotesOut { note_ids: nids }) Ok(pb::SearchNotesOut {
note_ids: nids.into_iter().map(|v| v.0).collect(),
})
}) })
}) })
} }
fn get_card(&self, cid: i64) -> Result<pb::GetCardOut> {
let card = self.with_col(|col| col.with_ctx(|ctx| ctx.storage.get_card(CardID(cid))))?;
Ok(pb::GetCardOut {
card: card.map(card_to_pb),
})
}
fn update_card(&self, pbcard: pb::Card) -> Result<()> {
let mut card = pbcard_to_native(pbcard)?;
self.with_col(|col| col.with_ctx(|ctx| ctx.update_card(&mut card)))
}
} }
fn translate_arg_to_fluent_val(arg: &pb::TranslateArgValue) -> FluentValue { fn translate_arg_to_fluent_val(arg: &pb::TranslateArgValue) -> FluentValue {
@ -704,3 +732,53 @@ fn sort_kind_from_pb(kind: i32) -> SortKind {
_ => SortKind::NoteCreation, _ => SortKind::NoteCreation,
} }
} }
fn card_to_pb(c: Card) -> pb::Card {
pb::Card {
id: c.id.0,
nid: c.nid.0,
did: c.did.0,
ord: c.ord as u32,
mtime: c.mtime.0,
usn: c.usn.0,
ctype: c.ctype as u32,
queue: c.queue as i32,
due: c.due,
ivl: c.ivl,
factor: c.factor as u32,
reps: c.reps,
lapses: c.lapses,
left: c.left,
odue: c.odue,
odid: c.odid.0,
flags: c.flags as u32,
data: c.data,
}
}
fn pbcard_to_native(c: pb::Card) -> Result<Card> {
let ctype = CardType::try_from(c.ctype as u8)
.map_err(|_| AnkiError::invalid_input("invalid card type"))?;
let queue = CardQueue::try_from(c.queue as i8)
.map_err(|_| AnkiError::invalid_input("invalid card queue"))?;
Ok(Card {
id: CardID(c.id),
nid: NoteID(c.nid),
did: DeckID(c.did),
ord: c.ord as u16,
mtime: TimestampSecs(c.mtime),
usn: Usn(c.usn),
ctype,
queue,
due: c.due,
ivl: c.ivl,
factor: c.factor as u16,
reps: c.reps,
lapses: c.lapses,
left: c.left,
odue: c.odue,
odid: DeckID(c.odid),
flags: c.flags as u8,
data: c.data,
})
}

View file

@ -1,9 +1,16 @@
// Copyright: Ankitects Pty Ltd and contributors // Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use crate::decks::DeckID;
use crate::define_newtype;
use crate::err::Result;
use crate::notes::NoteID;
use crate::{collection::RequestContext, timestamp::TimestampSecs, types::Usn};
use num_enum::TryFromPrimitive; use num_enum::TryFromPrimitive;
use serde_repr::{Deserialize_repr, Serialize_repr}; use serde_repr::{Deserialize_repr, Serialize_repr};
define_newtype!(CardID, i64);
#[derive(Serialize_repr, Deserialize_repr, Debug, PartialEq, TryFromPrimitive, Clone, Copy)] #[derive(Serialize_repr, Deserialize_repr, Debug, PartialEq, TryFromPrimitive, Clone, Copy)]
#[repr(u8)] #[repr(u8)]
pub enum CardType { pub enum CardType {
@ -31,3 +38,58 @@ pub enum CardQueue {
UserBuried = -2, UserBuried = -2,
SchedBuried = -3, SchedBuried = -3,
} }
#[derive(Debug, Clone)]
pub struct Card {
pub(crate) id: CardID,
pub(crate) nid: NoteID,
pub(crate) did: DeckID,
pub(crate) ord: u16,
pub(crate) mtime: TimestampSecs,
pub(crate) usn: Usn,
pub(crate) ctype: CardType,
pub(crate) queue: CardQueue,
pub(crate) due: i32,
pub(crate) ivl: u32,
pub(crate) factor: u16,
pub(crate) reps: u32,
pub(crate) lapses: u32,
pub(crate) left: u32,
pub(crate) odue: i32,
pub(crate) odid: DeckID,
pub(crate) flags: u8,
pub(crate) data: String,
}
impl Default for Card {
fn default() -> Self {
Self {
id: CardID(0),
nid: NoteID(0),
did: DeckID(0),
ord: 0,
mtime: TimestampSecs(0),
usn: Usn(0),
ctype: CardType::New,
queue: CardQueue::New,
due: 0,
ivl: 0,
factor: 0,
reps: 0,
lapses: 0,
left: 0,
odue: 0,
odid: DeckID(0),
flags: 0,
data: "".to_string(),
}
}
}
impl RequestContext<'_> {
pub fn update_card(&mut self, card: &mut Card) -> Result<()> {
card.mtime = TimestampSecs::now();
card.usn = self.storage.usn()?;
self.storage.update_card(card)
}
}

View file

@ -1,7 +1,7 @@
// Copyright: Ankitects Pty Ltd and contributors // Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use crate::types::ObjID; use crate::decks::DeckID;
use serde::Deserialize as DeTrait; use serde::Deserialize as DeTrait;
use serde_aux::field_attributes::deserialize_number_from_string; use serde_aux::field_attributes::deserialize_number_from_string;
use serde_derive::Deserialize; use serde_derive::Deserialize;
@ -22,7 +22,7 @@ pub struct Config {
rename = "curDeck", rename = "curDeck",
deserialize_with = "deserialize_number_from_string" deserialize_with = "deserialize_number_from_string"
)] )]
pub(crate) current_deck_id: ObjID, pub(crate) current_deck_id: DeckID,
pub(crate) rollover: Option<i8>, pub(crate) rollover: Option<i8>,
pub(crate) creation_offset: Option<i32>, pub(crate) creation_offset: Option<i32>,
pub(crate) local_offset: Option<i32>, pub(crate) local_offset: Option<i32>,

View file

@ -1,18 +1,21 @@
// Copyright: Ankitects Pty Ltd and contributors // Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use crate::types::ObjID; use crate::define_newtype;
use serde_aux::field_attributes::deserialize_number_from_string; use serde_aux::field_attributes::deserialize_number_from_string;
use serde_derive::Deserialize; use serde_derive::Deserialize;
define_newtype!(DeckID, i64);
define_newtype!(DeckConfID, i64);
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct Deck { pub struct Deck {
#[serde(deserialize_with = "deserialize_number_from_string")] #[serde(deserialize_with = "deserialize_number_from_string")]
pub(crate) id: ObjID, pub(crate) id: DeckID,
pub(crate) name: String, pub(crate) name: String,
} }
pub(crate) fn child_ids<'a>(decks: &'a [Deck], name: &str) -> impl Iterator<Item = ObjID> + 'a { pub(crate) fn child_ids<'a>(decks: &'a [Deck], name: &str) -> impl Iterator<Item = DeckID> + 'a {
let prefix = format!("{}::", name.to_ascii_lowercase()); let prefix = format!("{}::", name.to_ascii_lowercase());
decks decks
.iter() .iter()
@ -20,7 +23,7 @@ pub(crate) fn child_ids<'a>(decks: &'a [Deck], name: &str) -> impl Iterator<Item
.map(|d| d.id) .map(|d| d.id)
} }
pub(crate) fn get_deck(decks: &[Deck], id: ObjID) -> Option<&Deck> { pub(crate) fn get_deck(decks: &[Deck], id: DeckID) -> Option<&Deck> {
for d in decks { for d in decks {
if d.id == id { if d.id == id {
return Some(d); return Some(d);

View file

@ -28,5 +28,5 @@ pub mod storage;
pub mod template; pub mod template;
pub mod template_filters; pub mod template_filters;
pub mod text; pub mod text;
pub mod time; pub mod timestamp;
pub mod types; pub mod types;

View file

@ -390,7 +390,7 @@ where
self.maybe_fire_progress_cb()?; self.maybe_fire_progress_cb()?;
} }
let nt = note_types let nt = note_types
.get(&note.mid) .get(&note.ntid)
.ok_or_else(|| AnkiError::DBError { .ok_or_else(|| AnkiError::DBError {
info: "missing note type".to_string(), info: "missing note type".to_string(),
kind: DBErrorKind::MissingEntity, kind: DBErrorKind::MissingEntity,

View file

@ -4,20 +4,20 @@
/// At the moment, this is just basic note reading/updating functionality for /// At the moment, this is just basic note reading/updating functionality for
/// the media DB check. /// the media DB check.
use crate::err::{AnkiError, DBErrorKind, Result}; use crate::err::{AnkiError, DBErrorKind, Result};
use crate::notetypes::NoteTypeID;
use crate::text::strip_html_preserving_image_filenames; use crate::text::strip_html_preserving_image_filenames;
use crate::time::i64_unix_secs; use crate::timestamp::TimestampSecs;
use crate::{ use crate::{define_newtype, notetypes::NoteType, types::Usn};
notetypes::NoteType,
types::{ObjID, Timestamp, Usn},
};
use rusqlite::{params, Connection, Row, NO_PARAMS}; use rusqlite::{params, Connection, Row, NO_PARAMS};
use std::convert::TryInto; use std::convert::TryInto;
define_newtype!(NoteID, i64);
#[derive(Debug)] #[derive(Debug)]
pub(super) struct Note { pub(super) struct Note {
pub id: ObjID, pub id: NoteID,
pub mid: ObjID, pub ntid: NoteTypeID,
pub mtime_secs: Timestamp, pub mtime: TimestampSecs,
pub usn: Usn, pub usn: Usn,
fields: Vec<String>, fields: Vec<String>,
} }
@ -48,7 +48,7 @@ pub(crate) fn field_checksum(text: &str) -> u32 {
} }
#[allow(dead_code)] #[allow(dead_code)]
fn get_note(db: &Connection, nid: ObjID) -> Result<Option<Note>> { fn get_note(db: &Connection, nid: NoteID) -> Result<Option<Note>> {
let mut stmt = db.prepare_cached("select id, mid, mod, usn, flds from notes where id=?")?; let mut stmt = db.prepare_cached("select id, mid, mod, usn, flds from notes where id=?")?;
let note = stmt.query_and_then(params![nid], row_to_note)?.next(); let note = stmt.query_and_then(params![nid], row_to_note)?.next();
@ -72,8 +72,8 @@ pub(super) fn for_every_note<F: FnMut(&mut Note) -> Result<()>>(
fn row_to_note(row: &Row) -> Result<Note> { fn row_to_note(row: &Row) -> Result<Note> {
Ok(Note { Ok(Note {
id: row.get(0)?, id: row.get(0)?,
mid: row.get(1)?, ntid: row.get(1)?,
mtime_secs: row.get(2)?, mtime: row.get(2)?,
usn: row.get(3)?, usn: row.get(3)?,
fields: row fields: row
.get_raw(4) .get_raw(4)
@ -85,9 +85,9 @@ fn row_to_note(row: &Row) -> Result<Note> {
} }
pub(super) fn set_note(db: &Connection, note: &mut Note, note_type: &NoteType) -> Result<()> { pub(super) fn set_note(db: &Connection, note: &mut Note, note_type: &NoteType) -> Result<()> {
note.mtime_secs = i64_unix_secs(); note.mtime = TimestampSecs::now();
// hard-coded for now // hard-coded for now
note.usn = -1; note.usn = Usn(-1);
let field1_nohtml = strip_html_preserving_image_filenames(&note.fields()[0]); let field1_nohtml = strip_html_preserving_image_filenames(&note.fields()[0]);
let csum = field_checksum(field1_nohtml.as_ref()); let csum = field_checksum(field1_nohtml.as_ref());
let sort_field = if note_type.sort_field_idx == 0 { let sort_field = if note_type.sort_field_idx == 0 {
@ -106,7 +106,7 @@ pub(super) fn set_note(db: &Connection, note: &mut Note, note_type: &NoteType) -
let mut stmt = let mut stmt =
db.prepare_cached("update notes set mod=?,usn=?,flds=?,sfld=?,csum=? where id=?")?; db.prepare_cached("update notes set mod=?,usn=?,flds=?,sfld=?,csum=? where id=?")?;
stmt.execute(params![ stmt.execute(params![
note.mtime_secs, note.mtime,
note.usn, note.usn,
note.fields().join("\x1f"), note.fields().join("\x1f"),
sort_field, sort_field,

View file

@ -1,14 +1,16 @@
// Copyright: Ankitects Pty Ltd and contributors // Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use crate::types::ObjID; use crate::define_newtype;
use serde_aux::field_attributes::deserialize_number_from_string; use serde_aux::field_attributes::deserialize_number_from_string;
use serde_derive::Deserialize; use serde_derive::Deserialize;
define_newtype!(NoteTypeID, i64);
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub(crate) struct NoteType { pub(crate) struct NoteType {
#[serde(deserialize_with = "deserialize_number_from_string")] #[serde(deserialize_with = "deserialize_number_from_string")]
pub id: ObjID, pub id: NoteTypeID,
pub name: String, pub name: String,
#[serde(rename = "sortf")] #[serde(rename = "sortf")]
pub sort_field_idx: u16, pub sort_field_idx: u16,

View file

@ -2,12 +2,12 @@
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use super::{parser::Node, sqlwriter::node_to_sql}; use super::{parser::Node, sqlwriter::node_to_sql};
use crate::card::CardID;
use crate::card::CardType; use crate::card::CardType;
use crate::collection::RequestContext; use crate::collection::RequestContext;
use crate::config::SortKind; use crate::config::SortKind;
use crate::err::Result; use crate::err::Result;
use crate::search::parser::parse; use crate::search::parser::parse;
use crate::types::ObjID;
use rusqlite::params; use rusqlite::params;
pub(crate) enum SortMode { pub(crate) enum SortMode {
@ -21,7 +21,7 @@ pub(crate) fn search_cards<'a, 'b>(
req: &'a mut RequestContext<'b>, req: &'a mut RequestContext<'b>,
search: &'a str, search: &'a str,
order: SortMode, order: SortMode,
) -> Result<Vec<ObjID>> { ) -> Result<Vec<CardID>> {
let top_node = Node::Group(parse(search)?); let top_node = Node::Group(parse(search)?);
let (sql, args) = node_to_sql(req, &top_node)?; let (sql, args) = node_to_sql(req, &top_node)?;
@ -50,7 +50,7 @@ pub(crate) fn search_cards<'a, 'b>(
} }
let mut stmt = req.storage.db.prepare(&sql)?; let mut stmt = req.storage.db.prepare(&sql)?;
let ids: Vec<i64> = stmt let ids: Vec<_> = stmt
.query_map(&args, |row| row.get(0))? .query_map(&args, |row| row.get(0))?
.collect::<std::result::Result<_, _>>()?; .collect::<std::result::Result<_, _>>()?;

View file

@ -4,13 +4,13 @@
use super::{parser::Node, sqlwriter::node_to_sql}; use super::{parser::Node, sqlwriter::node_to_sql};
use crate::collection::RequestContext; use crate::collection::RequestContext;
use crate::err::Result; use crate::err::Result;
use crate::notes::NoteID;
use crate::search::parser::parse; use crate::search::parser::parse;
use crate::types::ObjID;
pub(crate) fn search_notes<'a, 'b>( pub(crate) fn search_notes<'a, 'b>(
req: &'a mut RequestContext<'b>, req: &'a mut RequestContext<'b>,
search: &'a str, search: &'a str,
) -> Result<Vec<ObjID>> { ) -> Result<Vec<NoteID>> {
let top_node = Node::Group(parse(search)?); let top_node = Node::Group(parse(search)?);
let (sql, args) = node_to_sql(req, &top_node)?; let (sql, args) = node_to_sql(req, &top_node)?;
@ -20,7 +20,7 @@ pub(crate) fn search_notes<'a, 'b>(
); );
let mut stmt = req.storage.db.prepare(&sql)?; let mut stmt = req.storage.db.prepare(&sql)?;
let ids: Vec<i64> = stmt let ids: Vec<_> = stmt
.query_map(&args, |row| row.get(0))? .query_map(&args, |row| row.get(0))?
.collect::<std::result::Result<_, _>>()?; .collect::<std::result::Result<_, _>>()?;

View file

@ -2,7 +2,7 @@
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use crate::err::{AnkiError, Result}; use crate::err::{AnkiError, Result};
use crate::types::ObjID; use crate::notetypes::NoteTypeID;
use nom::branch::alt; use nom::branch::alt;
use nom::bytes::complete::{escaped, is_not, tag, take_while1}; use nom::bytes::complete::{escaped, is_not, tag, take_while1};
use nom::character::complete::{anychar, char, one_of}; use nom::character::complete::{anychar, char, one_of};
@ -58,7 +58,7 @@ pub(super) enum SearchNode<'a> {
AddedInDays(u32), AddedInDays(u32),
CardTemplate(TemplateKind), CardTemplate(TemplateKind),
Deck(Cow<'a, str>), Deck(Cow<'a, str>),
NoteTypeID(ObjID), NoteTypeID(NoteTypeID),
NoteType(Cow<'a, str>), NoteType(Cow<'a, str>),
Rated { Rated {
days: u32, days: u32,
@ -66,7 +66,7 @@ pub(super) enum SearchNode<'a> {
}, },
Tag(Cow<'a, str>), Tag(Cow<'a, str>),
Duplicates { Duplicates {
note_type_id: ObjID, note_type_id: NoteTypeID,
text: String, text: String,
}, },
State(StateKind), State(StateKind),
@ -339,7 +339,7 @@ fn parse_rated(val: &str) -> ParseResult<SearchNode<'static>> {
/// eg dupes:1231,hello /// eg dupes:1231,hello
fn parse_dupes(val: &str) -> ParseResult<SearchNode<'static>> { fn parse_dupes(val: &str) -> ParseResult<SearchNode<'static>> {
let mut it = val.splitn(2, ','); let mut it = val.splitn(2, ',');
let mid: ObjID = it.next().unwrap().parse()?; let mid: NoteTypeID = it.next().unwrap().parse()?;
let text = it.next().ok_or(ParseError {})?; let text = it.next().ok_or(ParseError {})?;
Ok(SearchNode::Duplicates { Ok(SearchNode::Duplicates {
note_type_id: mid, note_type_id: mid,

View file

@ -7,11 +7,10 @@ use crate::decks::child_ids;
use crate::decks::get_deck; use crate::decks::get_deck;
use crate::err::{AnkiError, Result}; use crate::err::{AnkiError, Result};
use crate::notes::field_checksum; use crate::notes::field_checksum;
use crate::notetypes::NoteTypeID;
use crate::text::matches_wildcard; use crate::text::matches_wildcard;
use crate::text::without_combining; use crate::text::without_combining;
use crate::{ use crate::{collection::RequestContext, text::strip_html_preserving_image_filenames};
collection::RequestContext, text::strip_html_preserving_image_filenames, types::ObjID,
};
use std::fmt::Write; use std::fmt::Write;
struct SqlWriter<'a, 'b> { struct SqlWriter<'a, 'b> {
@ -342,7 +341,7 @@ impl SqlWriter<'_, '_> {
Ok(()) Ok(())
} }
fn write_dupes(&mut self, ntid: ObjID, text: &str) { fn write_dupes(&mut self, ntid: NoteTypeID, text: &str) {
let text_nohtml = strip_html_preserving_image_filenames(text); let text_nohtml = strip_html_preserving_image_filenames(text);
let csum = field_checksum(text_nohtml.as_ref()); let csum = field_checksum(text_nohtml.as_ref());
write!( write!(

116
rslib/src/storage/card.rs Normal file
View file

@ -0,0 +1,116 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use crate::cached_sql;
use crate::card::{Card, CardID, CardQueue, CardType};
use crate::err::{AnkiError, Result};
use rusqlite::params;
use rusqlite::{
types::{FromSql, FromSqlError, ValueRef},
OptionalExtension,
};
use std::convert::TryFrom;
impl FromSql for CardType {
fn column_result(value: ValueRef<'_>) -> std::result::Result<Self, FromSqlError> {
if let ValueRef::Integer(i) = value {
Ok(Self::try_from(i as u8).map_err(|_| FromSqlError::InvalidType)?)
} else {
Err(FromSqlError::InvalidType)
}
}
}
impl FromSql for CardQueue {
fn column_result(value: ValueRef<'_>) -> std::result::Result<Self, FromSqlError> {
if let ValueRef::Integer(i) = value {
Ok(Self::try_from(i as i8).map_err(|_| FromSqlError::InvalidType)?)
} else {
Err(FromSqlError::InvalidType)
}
}
}
impl super::StorageContext<'_> {
pub fn get_card(&mut self, cid: CardID) -> Result<Option<Card>> {
// the casts are required as Anki didn't prevent add-ons from
// storing strings or floats in columns before
let stmt = cached_sql!(
self.get_card_stmt,
self.db,
"
select nid, did, ord, cast(mod as integer), usn, type, queue, due,
cast(ivl as integer), factor, reps, lapses, left, odue, odid,
flags, data from cards where id=?"
);
stmt.query_row(params![cid], |row| {
Ok(Card {
id: cid,
nid: row.get(0)?,
did: row.get(1)?,
ord: row.get(2)?,
mtime: row.get(3)?,
usn: row.get(4)?,
ctype: row.get(5)?,
queue: row.get(6)?,
due: row.get(7)?,
ivl: row.get(8)?,
factor: row.get(9)?,
reps: row.get(10)?,
lapses: row.get(11)?,
left: row.get(12)?,
odue: row.get(13)?,
odid: row.get(14)?,
flags: row.get(15)?,
data: row.get(16)?,
})
})
.optional()
.map_err(Into::into)
}
pub(crate) fn update_card(&mut self, card: &Card) -> Result<()> {
if card.id.0 == 0 {
return Err(AnkiError::invalid_input("card id not set"));
}
self.flush_card(card)
}
fn flush_card(&mut self, card: &Card) -> Result<()> {
let stmt = cached_sql!(
self.update_card_stmt,
self.db,
"
insert or replace into cards
(id, nid, did, ord, mod, usn, type, queue, due, ivl, factor,
reps, lapses, left, odue, odid, flags, data)
values
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"
);
stmt.execute(params![
card.id,
card.nid,
card.did,
card.ord,
card.mtime,
card.usn,
card.ctype as u8,
card.queue as i8,
card.due,
card.ivl,
card.factor,
card.reps,
card.lapses,
card.left,
card.odue,
card.odid,
card.flags,
card.data,
])?;
Ok(())
}
}

View file

@ -1,3 +1,4 @@
mod card;
mod sqlite; mod sqlite;
pub(crate) use sqlite::{SqliteStorage, StorageContext}; pub(crate) use sqlite::{SqliteStorage, StorageContext};

View file

@ -3,15 +3,17 @@
use crate::collection::CollectionOp; use crate::collection::CollectionOp;
use crate::config::Config; use crate::config::Config;
use crate::decks::DeckID;
use crate::err::Result; use crate::err::Result;
use crate::err::{AnkiError, DBErrorKind}; use crate::err::{AnkiError, DBErrorKind};
use crate::time::{i64_unix_millis, i64_unix_secs}; use crate::notetypes::NoteTypeID;
use crate::timestamp::{TimestampMillis, TimestampSecs};
use crate::{ use crate::{
decks::Deck, decks::Deck,
notetypes::NoteType, notetypes::NoteType,
sched::cutoff::{sched_timing_today, SchedTimingToday}, sched::cutoff::{sched_timing_today, SchedTimingToday},
text::without_combining, text::without_combining,
types::{ObjID, Usn}, types::Usn,
}; };
use regex::Regex; use regex::Regex;
use rusqlite::{functions::FunctionFlags, params, Connection, NO_PARAMS}; use rusqlite::{functions::FunctionFlags, params, Connection, NO_PARAMS};
@ -40,6 +42,16 @@ pub struct SqliteStorage {
path: PathBuf, path: PathBuf,
} }
#[macro_export]
macro_rules! cached_sql {
( $label:expr, $db:expr, $sql:expr ) => {{
if $label.is_none() {
$label = Some($db.prepare_cached($sql)?);
}
$label.as_mut().unwrap()
}};
}
fn open_or_create_collection_db(path: &Path) -> Result<Connection> { fn open_or_create_collection_db(path: &Path) -> Result<Connection> {
let mut db = Connection::open(path)?; let mut db = Connection::open(path)?;
@ -168,7 +180,10 @@ impl SqliteStorage {
if create { if create {
db.prepare_cached("begin exclusive")?.execute(NO_PARAMS)?; db.prepare_cached("begin exclusive")?.execute(NO_PARAMS)?;
db.execute_batch(include_str!("schema11.sql"))?; db.execute_batch(include_str!("schema11.sql"))?;
db.execute("update col set crt=?, ver=?", params![i64_unix_secs(), ver])?; db.execute(
"update col set crt=?, ver=?",
params![TimestampSecs::now(), ver],
)?;
db.prepare_cached("commit")?.execute(NO_PARAMS)?; db.prepare_cached("commit")?.execute(NO_PARAMS)?;
} else { } else {
if ver > SCHEMA_MAX_VERSION { if ver > SCHEMA_MAX_VERSION {
@ -200,12 +215,14 @@ impl SqliteStorage {
pub(crate) struct StorageContext<'a> { pub(crate) struct StorageContext<'a> {
pub(crate) db: &'a Connection, pub(crate) db: &'a Connection,
#[allow(dead_code)]
server: bool, server: bool,
#[allow(dead_code)]
usn: Option<Usn>, usn: Option<Usn>,
timing_today: Option<SchedTimingToday>, timing_today: Option<SchedTimingToday>,
// cards
pub(super) get_card_stmt: Option<rusqlite::CachedStatement<'a>>,
pub(super) update_card_stmt: Option<rusqlite::CachedStatement<'a>>,
} }
impl StorageContext<'_> { impl StorageContext<'_> {
@ -215,6 +232,8 @@ impl StorageContext<'_> {
server, server,
usn: None, usn: None,
timing_today: None, timing_today: None,
get_card_stmt: None,
update_card_stmt: None,
} }
} }
@ -278,11 +297,10 @@ impl StorageContext<'_> {
pub(crate) fn mark_modified(&self) -> Result<()> { pub(crate) fn mark_modified(&self) -> Result<()> {
self.db self.db
.prepare_cached("update col set mod=?")? .prepare_cached("update col set mod=?")?
.execute(params![i64_unix_millis()])?; .execute(params![TimestampMillis::now()])?;
Ok(()) Ok(())
} }
#[allow(dead_code)]
pub(crate) fn usn(&mut self) -> Result<Usn> { pub(crate) fn usn(&mut self) -> Result<Usn> {
if self.server { if self.server {
if self.usn.is_none() { if self.usn.is_none() {
@ -292,13 +310,13 @@ impl StorageContext<'_> {
.query_row(NO_PARAMS, |row| row.get(0))?, .query_row(NO_PARAMS, |row| row.get(0))?,
); );
} }
Ok(*self.usn.as_ref().unwrap()) Ok(self.usn.clone().unwrap())
} else { } else {
Ok(-1) Ok(Usn(-1))
} }
} }
pub(crate) fn all_decks(&self) -> Result<HashMap<ObjID, Deck>> { pub(crate) fn all_decks(&self) -> Result<HashMap<DeckID, Deck>> {
self.db self.db
.query_row_and_then("select decks from col", NO_PARAMS, |row| -> Result<_> { .query_row_and_then("select decks from col", NO_PARAMS, |row| -> Result<_> {
Ok(serde_json::from_str(row.get_raw(0).as_str()?)?) Ok(serde_json::from_str(row.get_raw(0).as_str()?)?)
@ -312,11 +330,12 @@ impl StorageContext<'_> {
}) })
} }
pub(crate) fn all_note_types(&self) -> Result<HashMap<ObjID, NoteType>> { pub(crate) fn all_note_types(&self) -> Result<HashMap<NoteTypeID, NoteType>> {
let mut stmt = self.db.prepare("select models from col")?; let mut stmt = self.db.prepare("select models from col")?;
let note_types = stmt let note_types = stmt
.query_and_then(NO_PARAMS, |row| -> Result<HashMap<ObjID, NoteType>> { .query_and_then(NO_PARAMS, |row| -> Result<HashMap<NoteTypeID, NoteType>> {
let v: HashMap<ObjID, NoteType> = serde_json::from_str(row.get_raw(0).as_str()?)?; let v: HashMap<NoteTypeID, NoteType> =
serde_json::from_str(row.get_raw(0).as_str()?)?;
Ok(v) Ok(v)
})? })?
.next() .next()
@ -339,7 +358,7 @@ impl StorageContext<'_> {
self.timing_today = Some(sched_timing_today( self.timing_today = Some(sched_timing_today(
crt, crt,
i64_unix_secs(), TimestampSecs::now().0,
conf.creation_offset, conf.creation_offset,
now_offset, now_offset,
conf.rollover, conf.rollover,

View file

@ -1,14 +1,22 @@
// Copyright: Ankitects Pty Ltd and contributors // Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use crate::define_newtype;
use std::time; use std::time;
pub(crate) fn i64_unix_secs() -> i64 { define_newtype!(TimestampSecs, i64);
elapsed().as_secs() as i64 define_newtype!(TimestampMillis, i64);
impl TimestampSecs {
pub fn now() -> Self {
Self(elapsed().as_secs() as i64)
}
} }
pub(crate) fn i64_unix_millis() -> i64 { impl TimestampMillis {
elapsed().as_millis() as i64 pub fn now() -> Self {
Self(elapsed().as_millis() as i64)
}
} }
#[cfg(not(test))] #[cfg(not(test))]

View file

@ -1,9 +1,57 @@
// Copyright: Ankitects Pty Ltd and contributors // Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
// while Anki tends to only use positive numbers, sqlite only supports #[macro_export]
// signed integers, so these numbers are signed as well. macro_rules! define_newtype {
( $name:ident, $type:ident ) => {
#[repr(transparent)]
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
serde::Serialize,
serde::Deserialize,
)]
pub struct $name(pub $type);
pub type ObjID = i64; impl std::fmt::Display for $name {
pub type Usn = i32; fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
pub type Timestamp = i64; self.0.fmt(f)
}
}
impl std::str::FromStr for $name {
type Err = std::num::ParseIntError;
fn from_str(s: &std::primitive::str) -> std::result::Result<Self, Self::Err> {
$type::from_str(s).map($name)
}
}
impl rusqlite::types::FromSql for $name {
fn column_result(
value: rusqlite::types::ValueRef<'_>,
) -> std::result::Result<Self, rusqlite::types::FromSqlError> {
if let rusqlite::types::ValueRef::Integer(i) = value {
Ok(Self(i as $type))
} else {
Err(rusqlite::types::FromSqlError::InvalidType)
}
}
}
impl rusqlite::ToSql for $name {
fn to_sql(&self) -> ::rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
Ok(rusqlite::types::ToSqlOutput::Owned(
rusqlite::types::Value::Integer(self.0 as i64),
))
}
}
};
}
define_newtype!(Usn, i32);