diff --git a/proto/backend.proto b/proto/backend.proto index 66bcebf25..f285feb83 100644 --- a/proto/backend.proto +++ b/proto/backend.proto @@ -47,6 +47,7 @@ message BackendInput { Empty restore_trash = 35; OpenCollectionIn open_collection = 36; Empty close_collection = 37; + int64 get_card = 38; } } @@ -77,6 +78,7 @@ message BackendOutput { Empty restore_trash = 35; Empty open_collection = 36; Empty close_collection = 37; + GetCardOut get_card = 38; BackendError error = 2047; } @@ -367,3 +369,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; + int64 due = 9; + int64 ivl = 10; + uint32 factor = 11; + int64 reps = 12; + int64 lapses = 13; + int64 left = 14; + int64 odue = 15; + int64 odid = 16; + int64 flags = 17; + string data = 18; +} diff --git a/pylib/anki/cards.py b/pylib/anki/cards.py index f8ac57314..e856fc0ba 100644 --- a/pylib/anki/cards.py +++ b/pylib/anki/cards.py @@ -34,7 +34,7 @@ class Card: ord: int def __init__( - self, col: anki.collection._Collection, id: Optional[int] = None + self, col: anki.storage._Collection, id: Optional[int] = None ) -> None: self.col = col.weakref() self.timerStarted = None @@ -61,28 +61,27 @@ 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) diff --git a/pylib/anki/rsbackend.py b/pylib/anki/rsbackend.py index 630d74368..d6bf3fab0 100644 --- a/pylib/anki/rsbackend.py +++ b/pylib/anki/rsbackend.py @@ -480,6 +480,10 @@ 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 translate_string_in( key: TR, **kwargs: Union[str, int, float] diff --git a/pylib/tests/test_models.py b/pylib/tests/test_models.py index 8f6ceecee..ebcc3d0fa 100644 --- a/pylib/tests/test_models.py +++ b/pylib/tests/test_models.py @@ -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 diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 470fc579d..f3c862248 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -4,6 +4,7 @@ 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::collection::{open_collection, Collection}; use crate::config::SortKind; use crate::err::{AnkiError, NetworkErrorKind, Result, SyncErrorKind}; @@ -248,6 +249,7 @@ 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)?), }) } @@ -616,6 +618,13 @@ impl Backend { }) }) } + + fn get_card(&self, cid: i64) -> Result { + 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 translate_arg_to_fluent_val(arg: &pb::TranslateArgValue) -> FluentValue { @@ -708,3 +717,26 @@ 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, + data: c.data, + } +} diff --git a/rslib/src/card.rs b/rslib/src/card.rs index c79d0958c..d1267fc87 100644 --- a/rslib/src/card.rs +++ b/rslib/src/card.rs @@ -1,11 +1,15 @@ // 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::notes::NoteID; +use crate::{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 { @@ -33,3 +37,50 @@ 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: i64, + pub(crate) ivl: i64, + pub(crate) factor: u16, + pub(crate) reps: i64, + pub(crate) lapses: i64, + pub(crate) left: i64, + pub(crate) odue: i64, + pub(crate) odid: DeckID, + pub(crate) flags: i64, + 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(), + } + } +} diff --git a/rslib/src/storage/card.rs b/rslib/src/storage/card.rs new file mode 100644 index 000000000..28232b75e --- /dev/null +++ b/rslib/src/storage/card.rs @@ -0,0 +1,72 @@ +// 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::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 { + 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 { + 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> { + // 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) + } +} diff --git a/rslib/src/storage/mod.rs b/rslib/src/storage/mod.rs index 2ed04892c..7a21f0336 100644 --- a/rslib/src/storage/mod.rs +++ b/rslib/src/storage/mod.rs @@ -1,3 +1,4 @@ +mod card; mod sqlite; pub(crate) use sqlite::{SqliteStorage, StorageContext}; diff --git a/rslib/src/storage/sqlite.rs b/rslib/src/storage/sqlite.rs index 32a9d0524..28d3de18b 100644 --- a/rslib/src/storage/sqlite.rs +++ b/rslib/src/storage/sqlite.rs @@ -42,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 { let mut db = Connection::open(path)?; @@ -211,6 +221,9 @@ pub(crate) struct StorageContext<'a> { usn: Option, timing_today: Option, + + // cards + pub(super) get_card_stmt: Option>, } impl StorageContext<'_> { @@ -220,6 +233,7 @@ impl StorageContext<'_> { server, usn: None, timing_today: None, + get_card_stmt: None, } }