mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 06:22:22 -04:00
add note/card removal to backend
This commit is contained in:
parent
e1b0fe1832
commit
4a69b55a90
17 changed files with 157 additions and 47 deletions
|
@ -127,6 +127,7 @@ service BackendService {
|
||||||
rpc GetCard (CardID) returns (Card);
|
rpc GetCard (CardID) returns (Card);
|
||||||
rpc UpdateCard (Card) returns (Empty);
|
rpc UpdateCard (Card) returns (Empty);
|
||||||
rpc AddCard (Card) returns (CardID);
|
rpc AddCard (Card) returns (CardID);
|
||||||
|
rpc RemoveCards (RemoveCardsIn) returns (Empty);
|
||||||
|
|
||||||
// notes
|
// notes
|
||||||
|
|
||||||
|
@ -134,6 +135,7 @@ service BackendService {
|
||||||
rpc AddNote (AddNoteIn) returns (NoteID);
|
rpc AddNote (AddNoteIn) returns (NoteID);
|
||||||
rpc UpdateNote (Note) returns (Empty);
|
rpc UpdateNote (Note) returns (Empty);
|
||||||
rpc GetNote (NoteID) returns (Note);
|
rpc GetNote (NoteID) returns (Note);
|
||||||
|
rpc RemoveNotes (RemoveNotesIn) returns (Empty);
|
||||||
rpc AddNoteTags (AddNoteTagsIn) returns (UInt32);
|
rpc AddNoteTags (AddNoteTagsIn) returns (UInt32);
|
||||||
rpc UpdateNoteTags (UpdateNoteTagsIn) returns (UInt32);
|
rpc UpdateNoteTags (UpdateNoteTagsIn) returns (UInt32);
|
||||||
rpc ClozeNumbersInNote (Note) returns (ClozeNumbersInNoteOut);
|
rpc ClozeNumbersInNote (Note) returns (ClozeNumbersInNoteOut);
|
||||||
|
@ -933,3 +935,12 @@ message SyncAuth {
|
||||||
string hkey = 1;
|
string hkey = 1;
|
||||||
uint32 host_number = 2;
|
uint32 host_number = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message RemoveNotesIn {
|
||||||
|
repeated int64 note_ids = 1;
|
||||||
|
repeated int64 card_ids = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RemoveCardsIn {
|
||||||
|
repeated int64 card_ids = 1;
|
||||||
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import re
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import weakref
|
import weakref
|
||||||
from typing import TYPE_CHECKING, Any, Iterable, List, Optional, Sequence, Tuple, Union
|
from typing import TYPE_CHECKING, Any, List, Optional, Sequence, Tuple, Union
|
||||||
|
|
||||||
import anki.find
|
import anki.find
|
||||||
import anki.latex # sets up hook
|
import anki.latex # sets up hook
|
||||||
|
@ -343,24 +343,29 @@ class Collection:
|
||||||
def add_note(self, note: Note, deck_id: int) -> None:
|
def add_note(self, note: Note, deck_id: int) -> None:
|
||||||
note.id = self.backend.add_note(note=note.to_backend_note(), deck_id=deck_id)
|
note.id = self.backend.add_note(note=note.to_backend_note(), deck_id=deck_id)
|
||||||
|
|
||||||
|
def remove_notes(self, note_ids: Sequence[int]) -> None:
|
||||||
|
hooks.notes_will_be_deleted(self, note_ids)
|
||||||
|
self.backend.remove_notes(note_ids=note_ids, card_ids=[])
|
||||||
|
|
||||||
|
def remove_notes_by_card(self, card_ids: List[int]) -> None:
|
||||||
|
if hooks.notes_will_be_deleted.count():
|
||||||
|
nids = self.db.list(
|
||||||
|
"select nid from cards where id in " + ids2str(card_ids)
|
||||||
|
)
|
||||||
|
hooks.notes_will_be_deleted(self, nids)
|
||||||
|
self.backend.remove_notes(note_ids=[], card_ids=card_ids)
|
||||||
|
|
||||||
|
# legacy
|
||||||
|
|
||||||
def addNote(self, note: Note) -> int:
|
def addNote(self, note: Note) -> int:
|
||||||
self.add_note(note, note.model()["did"])
|
self.add_note(note, note.model()["did"])
|
||||||
return len(note.cards())
|
return len(note.cards())
|
||||||
|
|
||||||
def remNotes(self, ids: Iterable[int]) -> None:
|
def remNotes(self, ids: Sequence[int]) -> None:
|
||||||
"""Deletes notes with the given IDs."""
|
self.remove_notes(ids)
|
||||||
self.remCards(self.db.list("select id from cards where nid in " + ids2str(ids)))
|
|
||||||
|
|
||||||
def _remNotes(self, ids: List[int]) -> None:
|
def _remNotes(self, ids: List[int]) -> None:
|
||||||
"""Bulk delete notes by ID. Don't call this directly."""
|
pass
|
||||||
if not ids:
|
|
||||||
return
|
|
||||||
strids = ids2str(ids)
|
|
||||||
# we need to log these independently of cards, as one side may have
|
|
||||||
# more card templates
|
|
||||||
hooks.notes_will_be_deleted(self, ids)
|
|
||||||
self._logRem(ids, REM_NOTE)
|
|
||||||
self.db.execute("delete from notes where id in %s" % strids)
|
|
||||||
|
|
||||||
# Cards
|
# Cards
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
@ -371,24 +376,14 @@ class Collection:
|
||||||
def cardCount(self) -> Any:
|
def cardCount(self) -> Any:
|
||||||
return self.db.scalar("select count() from cards")
|
return self.db.scalar("select count() from cards")
|
||||||
|
|
||||||
|
def remove_cards_and_orphaned_notes(self, card_ids: Sequence[int]):
|
||||||
|
"You probably want .remove_notes_by_card() instead."
|
||||||
|
self.backend.remove_cards(card_ids=card_ids)
|
||||||
|
|
||||||
|
# legacy
|
||||||
|
|
||||||
def remCards(self, ids: List[int], notes: bool = True) -> None:
|
def remCards(self, ids: List[int], notes: bool = True) -> None:
|
||||||
"Bulk delete cards by ID."
|
self.remove_cards_and_orphaned_notes(ids)
|
||||||
if not ids:
|
|
||||||
return
|
|
||||||
sids = ids2str(ids)
|
|
||||||
nids = self.db.list("select nid from cards where id in " + sids)
|
|
||||||
# remove cards
|
|
||||||
self._logRem(ids, REM_CARD)
|
|
||||||
self.db.execute("delete from cards where id in " + sids)
|
|
||||||
# then notes
|
|
||||||
if not notes:
|
|
||||||
return
|
|
||||||
nids = self.db.list(
|
|
||||||
"""
|
|
||||||
select id from notes where id in %s and id not in (select nid from cards)"""
|
|
||||||
% ids2str(nids)
|
|
||||||
)
|
|
||||||
self._remNotes(nids)
|
|
||||||
|
|
||||||
def emptyCids(self) -> List[int]:
|
def emptyCids(self) -> List[int]:
|
||||||
print("emptyCids() will go away")
|
print("emptyCids() will go away")
|
||||||
|
|
|
@ -466,7 +466,7 @@ and notes.mid = ? and cards.ord = ?""",
|
||||||
else:
|
else:
|
||||||
deleted.append(cid)
|
deleted.append(cid)
|
||||||
self.col.db.executemany("update cards set ord=?,usn=?,mod=? where id=?", d)
|
self.col.db.executemany("update cards set ord=?,usn=?,mod=? where id=?", d)
|
||||||
self.col.remCards(deleted)
|
self.col.remove_cards_and_orphaned_notes(deleted)
|
||||||
|
|
||||||
# Schema hash
|
# Schema hash
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
|
@ -12,7 +12,7 @@ def test_delete():
|
||||||
cid = f.cards()[0].id
|
cid = f.cards()[0].id
|
||||||
deck.reset()
|
deck.reset()
|
||||||
deck.sched.answerCard(deck.sched.getCard(), 2)
|
deck.sched.answerCard(deck.sched.getCard(), 2)
|
||||||
deck.remCards([cid])
|
deck.remove_cards_and_orphaned_notes([cid])
|
||||||
assert deck.cardCount() == 0
|
assert deck.cardCount() == 0
|
||||||
assert deck.noteCount() == 0
|
assert deck.noteCount() == 0
|
||||||
assert deck.db.scalar("select count() from notes") == 0
|
assert deck.db.scalar("select count() from notes") == 0
|
||||||
|
@ -53,7 +53,7 @@ def test_genrem():
|
||||||
mm.save(m, templates=True)
|
mm.save(m, templates=True)
|
||||||
rep = d.backend.get_empty_cards()
|
rep = d.backend.get_empty_cards()
|
||||||
for note in rep.notes:
|
for note in rep.notes:
|
||||||
d.remCards(note.card_ids)
|
d.remove_cards_and_orphaned_notes(note.card_ids)
|
||||||
assert len(f.cards()) == 1
|
assert len(f.cards()) == 1
|
||||||
# if we add to the note, a card should be automatically generated
|
# if we add to the note, a card should be automatically generated
|
||||||
f.load()
|
f.load()
|
||||||
|
|
|
@ -46,7 +46,7 @@ def test_anki2_mediadupes():
|
||||||
imp.run()
|
imp.run()
|
||||||
assert os.listdir(empty.media.dir()) == ["foo.mp3"]
|
assert os.listdir(empty.media.dir()) == ["foo.mp3"]
|
||||||
# and importing again will not duplicate, as the file content matches
|
# and importing again will not duplicate, as the file content matches
|
||||||
empty.remCards(empty.db.list("select id from cards"))
|
empty.remove_cards_and_orphaned_notes(empty.db.list("select id from cards"))
|
||||||
imp = Anki2Importer(empty, tmp.path)
|
imp = Anki2Importer(empty, tmp.path)
|
||||||
imp.run()
|
imp.run()
|
||||||
assert os.listdir(empty.media.dir()) == ["foo.mp3"]
|
assert os.listdir(empty.media.dir()) == ["foo.mp3"]
|
||||||
|
@ -54,7 +54,7 @@ def test_anki2_mediadupes():
|
||||||
assert "foo.mp3" in n.fields[0]
|
assert "foo.mp3" in n.fields[0]
|
||||||
# if the local file content is different, and import should trigger a
|
# if the local file content is different, and import should trigger a
|
||||||
# rename
|
# rename
|
||||||
empty.remCards(empty.db.list("select id from cards"))
|
empty.remove_cards_and_orphaned_notes(empty.db.list("select id from cards"))
|
||||||
with open(os.path.join(empty.media.dir(), "foo.mp3"), "w") as f:
|
with open(os.path.join(empty.media.dir(), "foo.mp3"), "w") as f:
|
||||||
f.write("bar")
|
f.write("bar")
|
||||||
imp = Anki2Importer(empty, tmp.path)
|
imp = Anki2Importer(empty, tmp.path)
|
||||||
|
@ -64,7 +64,7 @@ def test_anki2_mediadupes():
|
||||||
assert "_" in n.fields[0]
|
assert "_" in n.fields[0]
|
||||||
# if the localized media file already exists, we rewrite the note and
|
# if the localized media file already exists, we rewrite the note and
|
||||||
# media
|
# media
|
||||||
empty.remCards(empty.db.list("select id from cards"))
|
empty.remove_cards_and_orphaned_notes(empty.db.list("select id from cards"))
|
||||||
with open(os.path.join(empty.media.dir(), "foo.mp3"), "w") as f:
|
with open(os.path.join(empty.media.dir(), "foo.mp3"), "w") as f:
|
||||||
f.write("bar")
|
f.write("bar")
|
||||||
imp = Anki2Importer(empty, tmp.path)
|
imp = Anki2Importer(empty, tmp.path)
|
||||||
|
@ -83,12 +83,12 @@ def test_apkg():
|
||||||
imp.run()
|
imp.run()
|
||||||
assert os.listdir(tmp.media.dir()) == ["foo.wav"]
|
assert os.listdir(tmp.media.dir()) == ["foo.wav"]
|
||||||
# importing again should be idempotent in terms of media
|
# importing again should be idempotent in terms of media
|
||||||
tmp.remCards(tmp.db.list("select id from cards"))
|
tmp.remove_cards_and_orphaned_notes(tmp.db.list("select id from cards"))
|
||||||
imp = AnkiPackageImporter(tmp, apkg)
|
imp = AnkiPackageImporter(tmp, apkg)
|
||||||
imp.run()
|
imp.run()
|
||||||
assert os.listdir(tmp.media.dir()) == ["foo.wav"]
|
assert os.listdir(tmp.media.dir()) == ["foo.wav"]
|
||||||
# but if the local file has different data, it will rename
|
# but if the local file has different data, it will rename
|
||||||
tmp.remCards(tmp.db.list("select id from cards"))
|
tmp.remove_cards_and_orphaned_notes(tmp.db.list("select id from cards"))
|
||||||
with open(os.path.join(tmp.media.dir(), "foo.wav"), "w") as f:
|
with open(os.path.join(tmp.media.dir(), "foo.wav"), "w") as f:
|
||||||
f.write("xyz")
|
f.write("xyz")
|
||||||
imp = AnkiPackageImporter(tmp, apkg)
|
imp = AnkiPackageImporter(tmp, apkg)
|
||||||
|
|
|
@ -1611,7 +1611,7 @@ where id in %s"""
|
||||||
else:
|
else:
|
||||||
# last selection at top; place one above topmost selection
|
# last selection at top; place one above topmost selection
|
||||||
newRow = min(selectedRows) - 1
|
newRow = min(selectedRows) - 1
|
||||||
self.col.remNotes(nids)
|
self.col.remove_notes(nids)
|
||||||
self.search()
|
self.search()
|
||||||
if len(self.model.cards):
|
if len(self.model.cards):
|
||||||
newRow = min(newRow, len(self.model.cards) - 1)
|
newRow = min(newRow, len(self.model.cards) - 1)
|
||||||
|
|
|
@ -95,5 +95,5 @@ class EmptyCardsDialog(QDialog):
|
||||||
else:
|
else:
|
||||||
to_delete.extend(note.card_ids)
|
to_delete.extend(note.card_ids)
|
||||||
|
|
||||||
self.mw.col.remCards(to_delete)
|
self.mw.col.remove_cards_and_orphaned_notes(to_delete)
|
||||||
return len(to_delete)
|
return len(to_delete)
|
||||||
|
|
|
@ -1279,7 +1279,7 @@ and if the problem comes up again, please ask on the support site."""
|
||||||
# Log note deletion
|
# Log note deletion
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def onRemNotes(self, col: Collection, nids: List[int]) -> None:
|
def onRemNotes(self, col: Collection, nids: Sequence[int]) -> None:
|
||||||
path = os.path.join(self.pm.profileFolder(), "deleted.txt")
|
path = os.path.join(self.pm.profileFolder(), "deleted.txt")
|
||||||
existed = os.path.exists(path)
|
existed = os.path.exists(path)
|
||||||
with open(path, "ab") as f:
|
with open(path, "ab") as f:
|
||||||
|
|
|
@ -791,7 +791,7 @@ time = %(time)d;
|
||||||
return
|
return
|
||||||
self.mw.checkpoint(_("Delete"))
|
self.mw.checkpoint(_("Delete"))
|
||||||
cnt = len(self.card.note().cards())
|
cnt = len(self.card.note().cards())
|
||||||
self.mw.col.remNotes([self.card.note().id])
|
self.mw.col.remove_notes([self.card.note().id])
|
||||||
self.mw.reset()
|
self.mw.reset()
|
||||||
tooltip(
|
tooltip(
|
||||||
ngettext(
|
ngettext(
|
||||||
|
|
|
@ -653,6 +653,21 @@ impl BackendService for Backend {
|
||||||
Ok(pb::CardId { cid: card.id.0 })
|
Ok(pb::CardId { cid: card.id.0 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn remove_cards(&mut self, input: pb::RemoveCardsIn) -> BackendResult<Empty> {
|
||||||
|
self.with_col(|col| {
|
||||||
|
col.transact(None, |col| {
|
||||||
|
col.remove_cards_and_orphaned_notes(
|
||||||
|
&input
|
||||||
|
.card_ids
|
||||||
|
.into_iter()
|
||||||
|
.map(Into::into)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
)?;
|
||||||
|
Ok(().into())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// notes
|
// notes
|
||||||
//-------------------------------------------------------------------
|
//-------------------------------------------------------------------
|
||||||
|
|
||||||
|
@ -688,6 +703,31 @@ impl BackendService for Backend {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn remove_notes(&mut self, input: pb::RemoveNotesIn) -> BackendResult<Empty> {
|
||||||
|
self.with_col(|col| {
|
||||||
|
if !input.note_ids.is_empty() {
|
||||||
|
col.remove_notes(
|
||||||
|
&input
|
||||||
|
.note_ids
|
||||||
|
.into_iter()
|
||||||
|
.map(Into::into)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
if !input.card_ids.is_empty() {
|
||||||
|
let nids = col.storage.note_ids_of_cards(
|
||||||
|
&input
|
||||||
|
.card_ids
|
||||||
|
.into_iter()
|
||||||
|
.map(Into::into)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
)?;
|
||||||
|
col.remove_notes(&nids.into_iter().collect::<Vec<_>>())?
|
||||||
|
}
|
||||||
|
Ok(().into())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn add_note_tags(&mut self, input: pb::AddNoteTagsIn) -> BackendResult<pb::UInt32> {
|
fn add_note_tags(&mut self, input: pb::AddNoteTagsIn) -> BackendResult<pb::UInt32> {
|
||||||
self.with_col(|col| {
|
self.with_col(|col| {
|
||||||
col.add_tags_for_notes(&to_nids(input.nids), &input.tags)
|
col.add_tags_for_notes(&to_nids(input.nids), &input.tags)
|
||||||
|
|
|
@ -206,7 +206,7 @@ impl Collection {
|
||||||
|
|
||||||
/// Remove cards and any resulting orphaned notes.
|
/// Remove cards and any resulting orphaned notes.
|
||||||
/// Expects a transaction.
|
/// Expects a transaction.
|
||||||
pub(crate) fn remove_cards_inner(&mut self, cids: &[CardID]) -> Result<()> {
|
pub(crate) fn remove_cards_and_orphaned_notes(&mut self, cids: &[CardID]) -> Result<()> {
|
||||||
let usn = self.usn()?;
|
let usn = self.usn()?;
|
||||||
let mut nids = HashSet::new();
|
let mut nids = HashSet::new();
|
||||||
for cid in cids {
|
for cid in cids {
|
||||||
|
@ -225,6 +225,14 @@ impl Collection {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn remove_card_only(&mut self, card: Card, usn: Usn) -> Result<()> {
|
||||||
|
// fixme: undo
|
||||||
|
self.storage.remove_card(card.id)?;
|
||||||
|
self.storage.add_card_grave(card.id, usn)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -420,7 +420,7 @@ impl Collection {
|
||||||
|
|
||||||
fn delete_all_cards_in_normal_deck(&mut self, did: DeckID) -> Result<()> {
|
fn delete_all_cards_in_normal_deck(&mut self, did: DeckID) -> Result<()> {
|
||||||
let cids = self.storage.all_cards_in_single_deck(did)?;
|
let cids = self.storage.all_cards_in_single_deck(did)?;
|
||||||
self.remove_cards_inner(&cids)
|
self.remove_cards_and_orphaned_notes(&cids)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn return_all_cards_in_filtered_deck(&mut self, did: DeckID) -> Result<()> {
|
fn return_all_cards_in_filtered_deck(&mut self, did: DeckID) -> Result<()> {
|
||||||
|
|
|
@ -321,6 +321,25 @@ impl Collection {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Remove provided notes, and any cards that use them.
|
||||||
|
pub(crate) fn remove_notes(&mut self, nids: &[NoteID]) -> Result<()> {
|
||||||
|
let usn = self.usn()?;
|
||||||
|
self.transact(None, |col| {
|
||||||
|
for nid in nids {
|
||||||
|
let nid = *nid;
|
||||||
|
if let Some(_existing_note) = col.storage.get_note(nid)? {
|
||||||
|
// fixme: undo
|
||||||
|
for card in col.storage.all_cards_of_note(nid)? {
|
||||||
|
col.remove_card_only(card, usn)?;
|
||||||
|
}
|
||||||
|
col.remove_note_only(nid, usn)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Update cards and field cache after notes modified externally.
|
/// Update cards and field cache after notes modified externally.
|
||||||
/// If gencards is false, skip card generation.
|
/// If gencards is false, skip card generation.
|
||||||
pub(crate) fn after_note_updates(
|
pub(crate) fn after_note_updates(
|
||||||
|
@ -501,7 +520,7 @@ mod test {
|
||||||
1
|
1
|
||||||
);
|
);
|
||||||
let cids = col.search_cards("", SortMode::NoOrder)?;
|
let cids = col.search_cards("", SortMode::NoOrder)?;
|
||||||
col.remove_cards_inner(&cids)?;
|
col.remove_cards_and_orphaned_notes(&cids)?;
|
||||||
|
|
||||||
// if normalization turned off, note text is entered as-is
|
// if normalization turned off, note text is entered as-is
|
||||||
let mut note = nt.new_note();
|
let mut note = nt.new_note();
|
||||||
|
|
|
@ -14,7 +14,7 @@ use rusqlite::{
|
||||||
types::{FromSql, FromSqlError, ValueRef},
|
types::{FromSql, FromSqlError, ValueRef},
|
||||||
OptionalExtension, Row, NO_PARAMS,
|
OptionalExtension, Row, NO_PARAMS,
|
||||||
};
|
};
|
||||||
use std::{convert::TryFrom, result};
|
use std::{collections::HashSet, convert::TryFrom, result};
|
||||||
|
|
||||||
impl FromSql for CardType {
|
impl FromSql for CardType {
|
||||||
fn column_result(value: ValueRef<'_>) -> std::result::Result<Self, FromSqlError> {
|
fn column_result(value: ValueRef<'_>) -> std::result::Result<Self, FromSqlError> {
|
||||||
|
@ -227,6 +227,29 @@ impl super::SqliteStorage {
|
||||||
.map(|o| o.is_none())
|
.map(|o| o.is_none())
|
||||||
.map_err(Into::into)
|
.map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn all_cards_of_note(&self, nid: NoteID) -> Result<Vec<Card>> {
|
||||||
|
self.db
|
||||||
|
.prepare_cached(concat!(include_str!("get_card.sql"), " where nid = ?"))?
|
||||||
|
.query_and_then(&[nid], |r| row_to_card(r).map_err(Into::into))?
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn note_ids_of_cards(&self, cids: &[CardID]) -> Result<HashSet<NoteID>> {
|
||||||
|
let mut stmt = self
|
||||||
|
.db
|
||||||
|
.prepare_cached("select nid from cards where id = ?")?;
|
||||||
|
let mut nids = HashSet::new();
|
||||||
|
for cid in cids {
|
||||||
|
if let Some(nid) = stmt
|
||||||
|
.query_row(&[cid], |r| r.get::<_, NoteID>(0))
|
||||||
|
.optional()?
|
||||||
|
{
|
||||||
|
nids.insert(nid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(nids)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -1400,7 +1400,7 @@ mod test {
|
||||||
}
|
}
|
||||||
|
|
||||||
// fixme: inconsistent usn arg
|
// fixme: inconsistent usn arg
|
||||||
col1.remove_cards_inner(&[cardid])?;
|
col1.remove_cards_and_orphaned_notes(&[cardid])?;
|
||||||
col1.remove_note_only(noteid, col1.usn()?)?;
|
col1.remove_note_only(noteid, col1.usn()?)?;
|
||||||
col1.remove_deck_and_child_decks(deckid)?;
|
col1.remove_deck_and_child_decks(deckid)?;
|
||||||
|
|
||||||
|
|
|
@ -52,6 +52,18 @@ macro_rules! define_newtype {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<$type> for $name {
|
||||||
|
fn from(t: $type) -> $name {
|
||||||
|
$name(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<$name> for $type {
|
||||||
|
fn from(n: $name) -> $type {
|
||||||
|
n.0
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -113,6 +113,8 @@ fn want_release_gil(method: u32) -> bool {
|
||||||
BackendMethod::SyncStatus => true,
|
BackendMethod::SyncStatus => true,
|
||||||
BackendMethod::FullUpload => true,
|
BackendMethod::FullUpload => true,
|
||||||
BackendMethod::FullDownload => true,
|
BackendMethod::FullDownload => true,
|
||||||
|
BackendMethod::RemoveNotes => true,
|
||||||
|
BackendMethod::RemoveCards => true,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
|
|
Loading…
Reference in a new issue