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;
OpenCollectionIn open_collection = 36;
Empty close_collection = 37;
int64 get_card = 38;
Card update_card = 39;
}
}
@ -77,6 +79,8 @@ message BackendOutput {
Empty restore_trash = 35;
Empty open_collection = 36;
Empty close_collection = 37;
GetCardOut get_card = 38;
Empty update_card = 39;
BackendError error = 2047;
}
@ -367,3 +371,28 @@ enum BuiltinSortKind {
CARD_DECK = 11;
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.models import NoteType, Template
from anki.notes import Note
from anki.rsbackend import BackendCard
from anki.sound import AVTag
from anki.utils import intTime, joinFields, timestampID
@ -33,9 +34,7 @@ class Card:
lastIvl: int
ord: int
def __init__(
self, col: anki.collection._Collection, id: Optional[int] = None
) -> None:
def __init__(self, col: anki.storage._Collection, id: Optional[int] = None) -> None:
self.col = col.weakref()
self.timerStarted = None
self._render_output: Optional[anki.template.TemplateRenderOutput] = None
@ -61,68 +60,59 @@ class Card:
self.data = ""
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._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:
hooks.card_will_flush(self)
self.mod = intTime()
self.usn = self.col.usn()
# bug check
def _bugcheck(self) -> None:
if (
self.queue == QUEUE_TYPE_REV
and self.odue
and not self.col.decks.isDyn(self.did)
):
hooks.card_odue_was_invalid()
assert self.due < 4294967296
def flush(self) -> None:
self._preFlush()
self.col.db.execute(
"""
insert or replace into cards values
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
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._bugcheck()
hooks.card_will_flush(self)
# mtime & usn are set by backend
card = BackendCard(
id=self.id,
nid=self.nid,
did=self.did,
ord=self.ord,
ctype=self.type,
queue=self.queue,
due=self.due,
ivl=self.ivl,
factor=self.factor,
reps=self.reps,
lapses=self.lapses,
left=self.left,
odue=self.odue,
odid=self.odid,
flags=self.flags,
data=self.data,
)
self.col.log(self)
self.col.backend.update_card(card)
def question(self, reload: bool = False, browser: bool = False) -> str:
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()")
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."
% rowcount
)

View File

@ -36,6 +36,7 @@ assert ankirspy.buildhash() == anki.buildinfo.buildhash
SchedTimingToday = pb.SchedTimingTodayOut
BuiltinSortKind = pb.BuiltinSortKind
BackendCard = pb.Card
try:
import orjson
@ -480,6 +481,12 @@ class RustBackend:
pb.BackendInput(search_notes=pb.SearchNotesIn(search=search))
).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(
key: TR, **kwargs: Union[str, int, float]

View File

@ -341,7 +341,7 @@ limit %d"""
resched = self._resched(card)
if "mult" in conf and resched:
# 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:
# new card; no ivl adjustment
pass

View File

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

View File

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

View File

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

View File

@ -442,7 +442,7 @@ close the profile or restart Anki."""
def loadCollection(self) -> bool:
try:
return self._loadCollection()
self._loadCollection()
except Exception as e:
showWarning(
tr(TR.ERRORS_UNABLE_OPEN_COLLECTION) + "\n" + traceback.format_exc()
@ -460,15 +460,22 @@ close the profile or restart Anki."""
self.showProfileManager()
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()
self.col = Collection(cpath, backend=self.backend)
self.setEnabled(True)
self.maybeEnableUndo()
gui_hooks.collection_did_load(self.col)
self.moveToState("deckBrowser")
return True
def reopen(self):
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):
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 \
@ -1250,6 +1260,9 @@ waiting on another device that haven't been synchronized here yet, they \
will be lost. Continue?"""
)
)
if progress_shown:
self.progress.start()
return ret
# Advanced features
##########################################################################

View File

@ -4,8 +4,11 @@
use crate::backend::dbproxy::db_command_bytes;
use crate::backend_proto::backend_input::Value;
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::config::SortKind;
use crate::decks::DeckID;
use crate::err::{AnkiError, NetworkErrorKind, Result, SyncErrorKind};
use crate::i18n::{tr_args, FString, I18n};
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::sync::MediaSyncProgress;
use crate::media::MediaManager;
use crate::notes::NoteID;
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::search::{search_cards, search_notes, SortMode};
@ -21,10 +25,13 @@ use crate::template::{
RenderedNode,
};
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 fluent::FluentValue;
use prost::Message;
use std::collections::{HashMap, HashSet};
use std::convert::TryFrom;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use tokio::runtime::Runtime;
@ -248,6 +255,11 @@ impl Backend {
}
Value::SearchCards(input) => OValue::SearchCards(self.search_cards(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
};
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| {
col.with_ctx(|ctx| {
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 {
@ -704,3 +732,53 @@ fn sort_kind_from_pb(kind: i32) -> SortKind {
_ => 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
// 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 serde_repr::{Deserialize_repr, Serialize_repr};
define_newtype!(CardID, i64);
#[derive(Serialize_repr, Deserialize_repr, Debug, PartialEq, TryFromPrimitive, Clone, Copy)]
#[repr(u8)]
pub enum CardType {
@ -31,3 +38,58 @@ pub enum CardQueue {
UserBuried = -2,
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
// 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_aux::field_attributes::deserialize_number_from_string;
use serde_derive::Deserialize;
@ -22,7 +22,7 @@ pub struct Config {
rename = "curDeck",
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) creation_offset: Option<i32>,
pub(crate) local_offset: Option<i32>,

View File

@ -1,18 +1,21 @@
// Copyright: Ankitects Pty Ltd and contributors
// 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_derive::Deserialize;
define_newtype!(DeckID, i64);
define_newtype!(DeckConfID, i64);
#[derive(Deserialize)]
pub struct Deck {
#[serde(deserialize_with = "deserialize_number_from_string")]
pub(crate) id: ObjID,
pub(crate) id: DeckID,
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());
decks
.iter()
@ -20,7 +23,7 @@ pub(crate) fn child_ids<'a>(decks: &'a [Deck], name: &str) -> impl Iterator<Item
.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 {
if d.id == id {
return Some(d);

View File

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

View File

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

View File

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

View File

@ -1,14 +1,16 @@
// Copyright: Ankitects Pty Ltd and contributors
// 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_derive::Deserialize;
define_newtype!(NoteTypeID, i64);
#[derive(Deserialize, Debug)]
pub(crate) struct NoteType {
#[serde(deserialize_with = "deserialize_number_from_string")]
pub id: ObjID,
pub id: NoteTypeID,
pub name: String,
#[serde(rename = "sortf")]
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
use super::{parser::Node, sqlwriter::node_to_sql};
use crate::card::CardID;
use crate::card::CardType;
use crate::collection::RequestContext;
use crate::config::SortKind;
use crate::err::Result;
use crate::search::parser::parse;
use crate::types::ObjID;
use rusqlite::params;
pub(crate) enum SortMode {
@ -21,7 +21,7 @@ pub(crate) fn search_cards<'a, 'b>(
req: &'a mut RequestContext<'b>,
search: &'a str,
order: SortMode,
) -> Result<Vec<ObjID>> {
) -> Result<Vec<CardID>> {
let top_node = Node::Group(parse(search)?);
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 ids: Vec<i64> = stmt
let ids: Vec<_> = stmt
.query_map(&args, |row| row.get(0))?
.collect::<std::result::Result<_, _>>()?;

View File

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

View File

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

View File

@ -7,11 +7,10 @@ use crate::decks::child_ids;
use crate::decks::get_deck;
use crate::err::{AnkiError, Result};
use crate::notes::field_checksum;
use crate::notetypes::NoteTypeID;
use crate::text::matches_wildcard;
use crate::text::without_combining;
use crate::{
collection::RequestContext, text::strip_html_preserving_image_filenames, types::ObjID,
};
use crate::{collection::RequestContext, text::strip_html_preserving_image_filenames};
use std::fmt::Write;
struct SqlWriter<'a, 'b> {
@ -342,7 +341,7 @@ impl SqlWriter<'_, '_> {
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 csum = field_checksum(text_nohtml.as_ref());
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;
pub(crate) use sqlite::{SqliteStorage, StorageContext};

View File

@ -3,15 +3,17 @@
use crate::collection::CollectionOp;
use crate::config::Config;
use crate::decks::DeckID;
use crate::err::Result;
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::{
decks::Deck,
notetypes::NoteType,
sched::cutoff::{sched_timing_today, SchedTimingToday},
text::without_combining,
types::{ObjID, Usn},
types::Usn,
};
use regex::Regex;
use rusqlite::{functions::FunctionFlags, params, Connection, NO_PARAMS};
@ -40,6 +42,16 @@ pub struct SqliteStorage {
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> {
let mut db = Connection::open(path)?;
@ -168,7 +180,10 @@ impl SqliteStorage {
if create {
db.prepare_cached("begin exclusive")?.execute(NO_PARAMS)?;
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)?;
} else {
if ver > SCHEMA_MAX_VERSION {
@ -200,12 +215,14 @@ impl SqliteStorage {
pub(crate) struct StorageContext<'a> {
pub(crate) db: &'a Connection,
#[allow(dead_code)]
server: bool,
#[allow(dead_code)]
usn: Option<Usn>,
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<'_> {
@ -215,6 +232,8 @@ impl StorageContext<'_> {
server,
usn: 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<()> {
self.db
.prepare_cached("update col set mod=?")?
.execute(params![i64_unix_millis()])?;
.execute(params![TimestampMillis::now()])?;
Ok(())
}
#[allow(dead_code)]
pub(crate) fn usn(&mut self) -> Result<Usn> {
if self.server {
if self.usn.is_none() {
@ -292,13 +310,13 @@ impl StorageContext<'_> {
.query_row(NO_PARAMS, |row| row.get(0))?,
);
}
Ok(*self.usn.as_ref().unwrap())
Ok(self.usn.clone().unwrap())
} 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
.query_row_and_then("select decks from col", NO_PARAMS, |row| -> Result<_> {
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 note_types = stmt
.query_and_then(NO_PARAMS, |row| -> Result<HashMap<ObjID, NoteType>> {
let v: HashMap<ObjID, NoteType> = serde_json::from_str(row.get_raw(0).as_str()?)?;
.query_and_then(NO_PARAMS, |row| -> Result<HashMap<NoteTypeID, NoteType>> {
let v: HashMap<NoteTypeID, NoteType> =
serde_json::from_str(row.get_raw(0).as_str()?)?;
Ok(v)
})?
.next()
@ -339,7 +358,7 @@ impl StorageContext<'_> {
self.timing_today = Some(sched_timing_today(
crt,
i64_unix_secs(),
TimestampSecs::now().0,
conf.creation_offset,
now_offset,
conf.rollover,

View File

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

View File

@ -1,9 +1,57 @@
// Copyright: Ankitects Pty Ltd and contributors
// 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
// signed integers, so these numbers are signed as well.
#[macro_export]
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;
pub type Usn = i32;
pub type Timestamp = i64;
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
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);