Merge remote-tracking branch 'danielelmes/master' into create_actions_for_windows_macos
This commit is contained in:
commit
b07454ca0c
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
##########################################################################
|
||||
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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>,
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -390,7 +390,7 @@ where
|
||||
self.maybe_fire_progress_cb()?;
|
||||
}
|
||||
let nt = note_types
|
||||
.get(¬e.mid)
|
||||
.get(¬e.ntid)
|
||||
.ok_or_else(|| AnkiError::DBError {
|
||||
info: "missing note type".to_string(),
|
||||
kind: DBErrorKind::MissingEntity,
|
||||
|
@ -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(¬e.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,
|
||||
|
@ -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,
|
||||
|
@ -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<_, _>>()?;
|
||||
|
||||
|
@ -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<_, _>>()?;
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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
116
rslib/src/storage/card.rs
Normal 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(())
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
mod card;
|
||||
mod sqlite;
|
||||
|
||||
pub(crate) use sqlite::{SqliteStorage, StorageContext};
|
||||
|
@ -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,
|
||||
|
@ -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))]
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user