mirror of
https://github.com/ankitects/anki.git
synced 2025-09-24 16:56:36 -04:00
Use temporary tables for gathering
export_apkg() now accepts a search instead of a deck id. Decks are gathered according to the matched notes' cards.
This commit is contained in:
parent
d1dd0586bd
commit
97c9dd4c18
9 changed files with 123 additions and 104 deletions
|
@ -82,6 +82,10 @@ impl NativeDeckName {
|
|||
.chain(self.components().skip(old_parent.components().count()))
|
||||
.join("\x1f")
|
||||
}
|
||||
|
||||
pub(crate) fn immediate_parent_name(&self) -> Option<&str> {
|
||||
immediate_parent_name(self.as_native_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for NativeDeckName {
|
||||
|
|
|
@ -15,8 +15,6 @@ use crate::{
|
|||
latex::extract_latex,
|
||||
prelude::*,
|
||||
revlog::RevlogEntry,
|
||||
search::{Negated, SearchNode, SortMode},
|
||||
storage::ids_to_string,
|
||||
text::{extract_media_refs, extract_underscored_css_imports, extract_underscored_references},
|
||||
};
|
||||
|
||||
|
@ -31,43 +29,27 @@ pub(super) struct ExportData {
|
|||
pub(super) media_paths: HashSet<PathBuf>,
|
||||
}
|
||||
|
||||
fn sibling_search(notes: &[Note], cards: &[Card]) -> SearchBuilder {
|
||||
let mut nids = String::new();
|
||||
ids_to_string(&mut nids, notes.iter().map(|note| note.id));
|
||||
let mut cids = String::new();
|
||||
ids_to_string(&mut cids, cards.iter().map(|card| card.id));
|
||||
SearchBuilder::from(SearchNode::NoteIds(nids)).and(SearchNode::CardIds(cids).negated())
|
||||
}
|
||||
|
||||
fn optional_deck_search(deck_id: Option<DeckId>) -> SearchNode {
|
||||
if let Some(did) = deck_id {
|
||||
SearchNode::from_deck_id(did, true)
|
||||
} else {
|
||||
SearchNode::WholeCollection
|
||||
}
|
||||
}
|
||||
|
||||
impl ExportData {
|
||||
pub(super) fn gather_data(
|
||||
&mut self,
|
||||
col: &mut Collection,
|
||||
deck_id: Option<DeckId>,
|
||||
search: impl TryIntoSearch,
|
||||
with_scheduling: bool,
|
||||
) -> Result<()> {
|
||||
self.decks = col.gather_decks(deck_id)?;
|
||||
let search = optional_deck_search(deck_id);
|
||||
self.notes = col.gather_notes(search.clone())?;
|
||||
self.cards = col.gather_cards(search, &self.notes, deck_id)?;
|
||||
self.notetypes = col.gather_notetypes(&self.notes)?;
|
||||
self.notes = col.gather_notes(search)?;
|
||||
self.cards = col.gather_cards()?;
|
||||
self.decks = col.gather_decks()?;
|
||||
self.notetypes = col.gather_notetypes()?;
|
||||
|
||||
if with_scheduling {
|
||||
self.revlog = col.gather_revlog(&self.cards)?;
|
||||
self.revlog = col.gather_revlog()?;
|
||||
self.decks_configs = col.gather_deck_configs(&self.decks)?;
|
||||
} else {
|
||||
self.remove_scheduling_information(col);
|
||||
};
|
||||
|
||||
Ok(())
|
||||
col.storage.clear_searched_notes_table()?;
|
||||
col.storage.clear_searched_cards_table()
|
||||
}
|
||||
|
||||
pub(super) fn gather_media_paths(&mut self) {
|
||||
|
@ -176,81 +158,50 @@ fn svg_getter(notetypes: &[Notetype]) -> impl Fn(NotetypeId) -> bool {
|
|||
}
|
||||
|
||||
impl Collection {
|
||||
fn gather_decks(&mut self, deck_id: Option<DeckId>) -> Result<Vec<Deck>> {
|
||||
if let Some(did) = deck_id {
|
||||
let deck = self.get_deck(did)?.ok_or(AnkiError::NotFound)?;
|
||||
self.storage
|
||||
.deck_id_with_children(&deck)?
|
||||
.iter()
|
||||
.filter(|did| **did != DeckId(1))
|
||||
.map(|did| self.storage.get_deck(*did)?.ok_or(AnkiError::NotFound))
|
||||
.collect()
|
||||
} else {
|
||||
Ok(self
|
||||
.storage
|
||||
.get_all_decks()?
|
||||
.into_iter()
|
||||
.filter(|deck| deck.id != DeckId(1))
|
||||
.collect())
|
||||
fn gather_notes(&mut self, search: impl TryIntoSearch) -> Result<Vec<Note>> {
|
||||
self.search_notes_into_table(search)?;
|
||||
self.storage.all_searched_notes()
|
||||
}
|
||||
|
||||
fn gather_cards(&mut self) -> Result<Vec<Card>> {
|
||||
self.storage.search_notes_cards_into_table()?;
|
||||
self.storage.all_searched_cards()
|
||||
}
|
||||
|
||||
fn gather_decks(&mut self) -> Result<Vec<Deck>> {
|
||||
let decks = self.storage.get_decks_for_search_cards()?;
|
||||
let parents = self.get_parent_decks(&decks)?;
|
||||
Ok(decks
|
||||
.into_iter()
|
||||
.filter(|deck| deck.id != DeckId(1))
|
||||
.chain(parents)
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn get_parent_decks(&mut self, decks: &[Deck]) -> Result<Vec<Deck>> {
|
||||
let mut parent_names: HashSet<&str> =
|
||||
decks.iter().map(|deck| deck.name.as_native_str()).collect();
|
||||
let mut parents = Vec::new();
|
||||
for deck in decks {
|
||||
while let Some(parent_name) = deck.name.immediate_parent_name() {
|
||||
if parent_names.insert(parent_name) {
|
||||
parents.push(
|
||||
self.storage
|
||||
.get_deck_by_name(parent_name)?
|
||||
.ok_or(AnkiError::DatabaseCheckRequired)?,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(parents)
|
||||
}
|
||||
|
||||
fn gather_notes(&mut self, search: SearchNode) -> Result<Vec<Note>> {
|
||||
self.search_notes(search, SortMode::NoOrder)?
|
||||
.iter()
|
||||
.map(|nid| self.storage.get_note(*nid)?.ok_or(AnkiError::NotFound))
|
||||
.collect()
|
||||
fn gather_notetypes(&mut self) -> Result<Vec<Notetype>> {
|
||||
self.storage.get_notetypes_for_search_notes()
|
||||
}
|
||||
|
||||
fn gather_cards(
|
||||
&mut self,
|
||||
search: SearchNode,
|
||||
notes: &[Note],
|
||||
deck_id: Option<DeckId>,
|
||||
) -> Result<Vec<Card>> {
|
||||
let mut cards: Vec<_> = self
|
||||
.search_cards(search, SortMode::NoOrder)?
|
||||
.iter()
|
||||
.map(|cid| self.storage.get_card(*cid)?.ok_or(AnkiError::NotFound))
|
||||
.collect::<Result<_>>()?;
|
||||
|
||||
if let Some(did) = deck_id {
|
||||
let mut siblings = self.gather_siblings(notes, &cards, did)?;
|
||||
cards.append(&mut siblings);
|
||||
}
|
||||
|
||||
Ok(cards)
|
||||
}
|
||||
|
||||
fn gather_siblings(
|
||||
&mut self,
|
||||
notes: &[Note],
|
||||
cards: &[Card],
|
||||
deck_id: DeckId,
|
||||
) -> Result<Vec<Card>> {
|
||||
self.search_cards(sibling_search(notes, cards), SortMode::NoOrder)?
|
||||
.iter()
|
||||
.map(|cid| {
|
||||
let mut card = self.storage.get_card(*cid)?.ok_or(AnkiError::NotFound)?;
|
||||
card.deck_id = deck_id;
|
||||
Ok(card)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn gather_notetypes(&mut self, notes: &[Note]) -> Result<Vec<Notetype>> {
|
||||
notes
|
||||
.iter()
|
||||
.map(|note| note.notetype_id)
|
||||
.unique()
|
||||
.map(|ntid| self.storage.get_notetype(ntid)?.ok_or(AnkiError::NotFound))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn gather_revlog(&mut self, cards: &[Card]) -> Result<Vec<RevlogEntry>> {
|
||||
let mut cids = String::new();
|
||||
ids_to_string(&mut cids, cards.iter().map(|card| card.id));
|
||||
self.storage.get_revlog_entries_for_card_ids(cids)
|
||||
fn gather_revlog(&mut self) -> Result<Vec<RevlogEntry>> {
|
||||
self.storage.get_revlog_entries_for_searched_cards()
|
||||
}
|
||||
|
||||
fn gather_deck_configs(&mut self, decks: &[Deck]) -> Result<Vec<DeckConfig>> {
|
||||
|
|
|
@ -26,7 +26,7 @@ impl Collection {
|
|||
pub fn export_apkg(
|
||||
&mut self,
|
||||
out_path: impl AsRef<Path>,
|
||||
deck_id: Option<DeckId>,
|
||||
search: impl TryIntoSearch,
|
||||
with_scheduling: bool,
|
||||
with_media: bool,
|
||||
media_fn: Option<impl FnOnce(HashSet<PathBuf>) -> MediaIter>,
|
||||
|
@ -40,7 +40,7 @@ impl Collection {
|
|||
.ok_or_else(|| AnkiError::IoError("tempfile with non-unicode name".into()))?;
|
||||
let media = self.export_collection_extracting_media(
|
||||
temp_col_path,
|
||||
deck_id,
|
||||
search,
|
||||
with_scheduling,
|
||||
with_media,
|
||||
)?;
|
||||
|
@ -66,12 +66,12 @@ impl Collection {
|
|||
fn export_collection_extracting_media(
|
||||
&mut self,
|
||||
path: &str,
|
||||
deck_id: Option<DeckId>,
|
||||
search: impl TryIntoSearch,
|
||||
with_scheduling: bool,
|
||||
with_media: bool,
|
||||
) -> Result<HashSet<PathBuf>> {
|
||||
let mut data = ExportData::default();
|
||||
data.gather_data(self, deck_id, with_scheduling)?;
|
||||
data.gather_data(self, search, with_scheduling)?;
|
||||
if with_media {
|
||||
data.gather_media_paths();
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ impl Collection {
|
|||
self.storage.get_all_revlog_entries(revlog_start)?
|
||||
} else {
|
||||
self.storage
|
||||
.get_revlog_entries_for_searched_cards(revlog_start)?
|
||||
.get_pb_revlog_entries_for_searched_cards(revlog_start)?
|
||||
};
|
||||
|
||||
self.storage.clear_searched_cards_table()?;
|
||||
|
|
|
@ -455,6 +455,20 @@ impl super::SqliteStorage {
|
|||
Ok(nids)
|
||||
}
|
||||
|
||||
/// Place the ids of cards with notes in 'search_nids' into 'search_cids'.
|
||||
/// Returns number of added cards.
|
||||
pub(crate) fn search_notes_cards_into_table(&self) -> Result<usize> {
|
||||
self.setup_searched_cards_table()?;
|
||||
self.db
|
||||
.prepare(concat!(
|
||||
"INSERT INTO search_cids",
|
||||
" (SELECT id FROM cards WHERE nid IN",
|
||||
" (SELECT nid FROM search_nids))",
|
||||
))?
|
||||
.execute([])
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub(crate) fn all_searched_cards(&self) -> Result<Vec<Card>> {
|
||||
self.db
|
||||
.prepare_cached(concat!(
|
||||
|
|
|
@ -74,6 +74,15 @@ impl SqliteStorage {
|
|||
.transpose()
|
||||
}
|
||||
|
||||
pub(crate) fn get_deck_by_name(&self, machine_name: &str) -> Result<Option<Deck>> {
|
||||
self.db
|
||||
.prepare_cached(concat!(include_str!("get_deck.sql"), " WHERE name = ?"))?
|
||||
.query_and_then([machine_name], row_to_deck)?
|
||||
.next()
|
||||
.transpose()
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub(crate) fn get_all_decks(&self) -> Result<Vec<Deck>> {
|
||||
self.db
|
||||
.prepare(include_str!("get_deck.sql"))?
|
||||
|
@ -111,6 +120,17 @@ impl SqliteStorage {
|
|||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub(crate) fn get_decks_for_search_cards(&self) -> Result<Vec<Deck>> {
|
||||
self.db
|
||||
.prepare_cached(concat!(
|
||||
include_str!("get_deck.sql"),
|
||||
" WHERE id IN (SELECT DISTINCT did FROM cards WHERE id IN",
|
||||
" (SELECT cid FROM search_cids))",
|
||||
))?
|
||||
.query_and_then([], row_to_deck)?
|
||||
.collect()
|
||||
}
|
||||
|
||||
// caller should ensure name unique
|
||||
pub(crate) fn add_deck(&self, deck: &mut Deck) -> Result<()> {
|
||||
assert!(deck.id.0 == 0);
|
||||
|
|
|
@ -210,6 +210,16 @@ impl super::SqliteStorage {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn all_searched_notes(&self) -> Result<Vec<Note>> {
|
||||
self.db
|
||||
.prepare_cached(concat!(
|
||||
include_str!("get.sql"),
|
||||
" WHERE id IN (SELECT nid FROM search_nids)"
|
||||
))?
|
||||
.query_and_then([], |r| row_to_note(r).map_err(Into::into))?
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn get_note_tags_by_predicate<F>(&mut self, want: F) -> Result<Vec<NoteTags>>
|
||||
where
|
||||
F: Fn(&str) -> bool,
|
||||
|
|
|
@ -99,6 +99,23 @@ impl SqliteStorage {
|
|||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub(crate) fn get_notetypes_for_search_notes(&self) -> Result<Vec<Notetype>> {
|
||||
self.db
|
||||
.prepare_cached(concat!(
|
||||
include_str!("get_notetype.sql"),
|
||||
" WHERE id IN (SELECT DISTINCT mid FROM notes WHERE id IN",
|
||||
" (SELECT nid FROM search_nids))",
|
||||
))?
|
||||
.query_and_then([], |r| {
|
||||
row_to_notetype_core(r).and_then(|mut nt| {
|
||||
nt.fields = self.get_notetype_fields(nt.id)?;
|
||||
nt.templates = self.get_notetype_templates(nt.id)?;
|
||||
Ok(nt)
|
||||
})
|
||||
})?
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn get_all_notetype_names(&self) -> Result<Vec<(NotetypeId, String)>> {
|
||||
self.db
|
||||
.prepare_cached(include_str!("get_notetype_names.sql"))?
|
||||
|
|
|
@ -107,7 +107,7 @@ impl SqliteStorage {
|
|||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn get_revlog_entries_for_searched_cards(
|
||||
pub(crate) fn get_pb_revlog_entries_for_searched_cards(
|
||||
&self,
|
||||
after: TimestampSecs,
|
||||
) -> Result<Vec<pb::RevlogEntry>> {
|
||||
|
@ -120,9 +120,12 @@ impl SqliteStorage {
|
|||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn get_revlog_entries_for_card_ids(&self, cids: String) -> Result<Vec<RevlogEntry>> {
|
||||
pub(crate) fn get_revlog_entries_for_searched_cards(&self) -> Result<Vec<RevlogEntry>> {
|
||||
self.db
|
||||
.prepare_cached(&format!("{} where cid in {cids}", include_str!("get.sql"),))?
|
||||
.prepare_cached(concat!(
|
||||
include_str!("get.sql"),
|
||||
" where cid in (select cid from search_cids)"
|
||||
))?
|
||||
.query_and_then([], row_to_revlog_entry)?
|
||||
.collect()
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue