Fix 'Expression tree is too large' when user has many decks

+ Allow 'did:' searches to match multiple decks at once
This commit is contained in:
Damien Elmes 2023-10-09 16:14:59 +11:00
parent 67acdc3034
commit 0e6104a96b
7 changed files with 23 additions and 24 deletions

View file

@ -19,6 +19,7 @@ use crate::prelude::*;
use crate::scheduler::fsrs::memory_state::UpdateMemoryStateRequest; use crate::scheduler::fsrs::memory_state::UpdateMemoryStateRequest;
use crate::search::JoinSearches; use crate::search::JoinSearches;
use crate::search::SearchNode; use crate::search::SearchNode;
use crate::storage::comma_separated_ids;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct UpdateDeckConfigsRequest { pub struct UpdateDeckConfigsRequest {
@ -164,7 +165,7 @@ impl Collection {
let usn = self.usn()?; let usn = self.usn()?;
let today = self.timing_today()?.days_elapsed; let today = self.timing_today()?.days_elapsed;
let selected_config = req.configs.last().unwrap(); let selected_config = req.configs.last().unwrap();
let mut decks_needing_memory_recompute: HashMap<DeckConfigId, Vec<SearchNode>> = let mut decks_needing_memory_recompute: HashMap<DeckConfigId, Vec<DeckId>> =
Default::default(); Default::default();
let fsrs_toggled = self.get_config_bool(BoolKey::Fsrs) != req.fsrs; let fsrs_toggled = self.get_config_bool(BoolKey::Fsrs) != req.fsrs;
if fsrs_toggled { if fsrs_toggled {
@ -216,7 +217,7 @@ impl Collection {
decks_needing_memory_recompute decks_needing_memory_recompute
.entry(current_config_id) .entry(current_config_id)
.or_default() .or_default()
.push(SearchNode::DeckIdWithoutChildren(deck_id)); .push(deck_id);
} }
self.adjust_remaining_steps_in_deck(deck_id, previous_config, current_config, usn)?; self.adjust_remaining_steps_in_deck(deck_id, previous_config, current_config, usn)?;
@ -224,7 +225,7 @@ impl Collection {
} }
if !decks_needing_memory_recompute.is_empty() { if !decks_needing_memory_recompute.is_empty() {
let input: Vec<(Option<UpdateMemoryStateRequest>, Vec<SearchNode>)> = let input: Vec<(Option<UpdateMemoryStateRequest>, SearchNode)> =
decks_needing_memory_recompute decks_needing_memory_recompute
.into_iter() .into_iter()
.map(|(conf_id, search)| { .map(|(conf_id, search)| {
@ -240,7 +241,10 @@ impl Collection {
None None
} }
}); });
Ok((weights, search)) Ok((
weights,
SearchNode::DeckIdsWithoutChildren(comma_separated_ids(&search)),
))
}) })
.collect::<Result<_>>()?; .collect::<Result<_>>()?;
self.update_memory_state(input)?; self.update_memory_state(input)?;

View file

@ -16,7 +16,6 @@ use crate::revlog::RevlogReviewKind;
use crate::scheduler::fsrs::weights::single_card_revlog_to_items; use crate::scheduler::fsrs::weights::single_card_revlog_to_items;
use crate::scheduler::fsrs::weights::Weights; use crate::scheduler::fsrs::weights::Weights;
use crate::scheduler::states::fuzz::with_review_fuzz; use crate::scheduler::states::fuzz::with_review_fuzz;
use crate::search::JoinSearches;
use crate::search::Negated; use crate::search::Negated;
use crate::search::SearchNode; use crate::search::SearchNode;
use crate::search::StateKind; use crate::search::StateKind;
@ -43,13 +42,13 @@ impl Collection {
/// memory state should be removed. /// memory state should be removed.
pub(crate) fn update_memory_state( pub(crate) fn update_memory_state(
&mut self, &mut self,
entries: Vec<(Option<UpdateMemoryStateRequest>, Vec<SearchNode>)>, entries: Vec<(Option<UpdateMemoryStateRequest>, SearchNode)>,
) -> Result<()> { ) -> Result<()> {
let timing = self.timing_today()?; let timing = self.timing_today()?;
let usn = self.usn()?; let usn = self.usn()?;
for (req, search) in entries { for (req, search) in entries {
let search = SearchBuilder::any(search.into_iter()) let search =
.and(SearchNode::State(StateKind::New).negated()); SearchBuilder::all([search.into(), SearchNode::State(StateKind::New).negated()]);
let revlog = self.revlog_for_srs(search)?; let revlog = self.revlog_for_srs(search)?;
let reschedule = req.as_ref().map(|e| e.reschedule).unwrap_or_default(); let reschedule = req.as_ref().map(|e| e.reschedule).unwrap_or_default();
let last_reviews = if reschedule { let last_reviews = if reschedule {

View file

@ -281,7 +281,7 @@ impl Collection {
usn: Usn, usn: Usn,
) -> Result<usize> { ) -> Result<usize> {
let cids = self.search_cards( let cids = self.search_cards(
SearchNode::DeckIdWithoutChildren(deck).and(StateKind::New), SearchNode::DeckIdsWithoutChildren(deck.to_string()).and(StateKind::New),
SortMode::NoOrder, SortMode::NoOrder,
)?; )?;
self.sort_cards_inner(&cids, 1, 1, order.into(), false, usn) self.sort_cards_inner(&cids, 1, 1, order.into(), false, usn)

View file

@ -11,6 +11,7 @@ use super::SearchNode;
use super::StateKind; use super::StateKind;
use super::TemplateKind; use super::TemplateKind;
use crate::prelude::*; use crate::prelude::*;
use crate::storage::comma_separated_ids;
use crate::text::escape_anki_wildcards_for_search_node; use crate::text::escape_anki_wildcards_for_search_node;
pub trait Negated { pub trait Negated {
@ -123,7 +124,7 @@ impl SearchBuilder {
/// Construct [SearchBuilder] matching any given deck, excluding children. /// Construct [SearchBuilder] matching any given deck, excluding children.
pub fn from_decks(decks: &[DeckId]) -> Self { pub fn from_decks(decks: &[DeckId]) -> Self {
Self::any(decks.iter().copied().map(SearchNode::DeckIdWithoutChildren)) SearchNode::DeckIdsWithoutChildren(comma_separated_ids(decks)).into()
} }
/// Construct [SearchBuilder] matching learning, but not relearning cards. /// Construct [SearchBuilder] matching learning, but not relearning cards.
@ -160,7 +161,7 @@ impl SearchNode {
if with_children { if with_children {
Self::DeckIdWithChildren(did.into()) Self::DeckIdWithChildren(did.into())
} else { } else {
Self::DeckIdWithoutChildren(did.into()) Self::DeckIdsWithoutChildren(did.into().to_string())
} }
} }

View file

@ -60,8 +60,8 @@ pub enum SearchNode {
EditedInDays(u32), EditedInDays(u32),
CardTemplate(TemplateKind), CardTemplate(TemplateKind),
Deck(String), Deck(String),
/// Matches cards in a single deck (original_deck_id is not checked). /// Matches cards in a list of decks (original_deck_id is not checked).
DeckIdWithoutChildren(DeckId), DeckIdsWithoutChildren(String),
/// Matches cards in a deck or its children (original_deck_id is not /// Matches cards in a deck or its children (original_deck_id is not
/// checked). /// checked).
DeckIdWithChildren(DeckId), DeckIdWithChildren(DeckId),
@ -346,7 +346,7 @@ fn search_node_for_text_with_argument<'a>(
"introduced" => parse_introduced(val)?, "introduced" => parse_introduced(val)?,
"rated" => parse_rated(val)?, "rated" => parse_rated(val)?,
"is" => parse_state(val)?, "is" => parse_state(val)?,
"did" => parse_did(val)?, "did" => SearchNode::DeckIdsWithoutChildren(check_id_list(val, key)?.into()),
"mid" => parse_mid(val)?, "mid" => parse_mid(val)?,
"nid" => SearchNode::NoteIds(check_id_list(val, key)?.into()), "nid" => SearchNode::NoteIds(check_id_list(val, key)?.into()),
"cid" => SearchNode::CardIds(check_id_list(val, key)?.into()), "cid" => SearchNode::CardIds(check_id_list(val, key)?.into()),
@ -614,10 +614,6 @@ fn parse_state(s: &str) -> ParseResult<SearchNode> {
})) }))
} }
fn parse_did(s: &str) -> ParseResult<SearchNode> {
parse_i64(s, "did:").map(|n| SearchNode::DeckIdWithoutChildren(n.into()))
}
fn parse_mid(s: &str) -> ParseResult<SearchNode> { fn parse_mid(s: &str) -> ParseResult<SearchNode> {
parse_i64(s, "mid:").map(|n| SearchNode::NotetypeId(n.into())) parse_i64(s, "mid:").map(|n| SearchNode::NotetypeId(n.into()))
} }

View file

@ -160,8 +160,8 @@ impl SqlWriter<'_> {
SearchNode::NotetypeId(ntid) => { SearchNode::NotetypeId(ntid) => {
write!(self.sql, "n.mid = {}", ntid).unwrap(); write!(self.sql, "n.mid = {}", ntid).unwrap();
} }
SearchNode::DeckIdWithoutChildren(did) => { SearchNode::DeckIdsWithoutChildren(dids) => {
write!(self.sql, "c.did = {}", did).unwrap(); write!(self.sql, "c.did in ({})", dids).unwrap();
} }
SearchNode::DeckIdWithChildren(did) => self.write_deck_id_with_children(*did)?, SearchNode::DeckIdWithChildren(did) => self.write_deck_id_with_children(*did)?,
SearchNode::Notetype(notetype) => self.write_notetype(&norm(notetype)), SearchNode::Notetype(notetype) => self.write_notetype(&norm(notetype)),
@ -965,7 +965,7 @@ impl SearchNode {
SearchNode::AddedInDays(_) => RequiredTable::Cards, SearchNode::AddedInDays(_) => RequiredTable::Cards,
SearchNode::IntroducedInDays(_) => RequiredTable::Cards, SearchNode::IntroducedInDays(_) => RequiredTable::Cards,
SearchNode::Deck(_) => RequiredTable::Cards, SearchNode::Deck(_) => RequiredTable::Cards,
SearchNode::DeckIdWithoutChildren(_) => RequiredTable::Cards, SearchNode::DeckIdsWithoutChildren(_) => RequiredTable::Cards,
SearchNode::DeckIdWithChildren(_) => RequiredTable::Cards, SearchNode::DeckIdWithChildren(_) => RequiredTable::Cards,
SearchNode::Rated { .. } => RequiredTable::Cards, SearchNode::Rated { .. } => RequiredTable::Cards,
SearchNode::State(_) => RequiredTable::Cards, SearchNode::State(_) => RequiredTable::Cards,
@ -973,6 +973,7 @@ impl SearchNode {
SearchNode::CardIds(_) => RequiredTable::Cards, SearchNode::CardIds(_) => RequiredTable::Cards,
SearchNode::Property { .. } => RequiredTable::Cards, SearchNode::Property { .. } => RequiredTable::Cards,
SearchNode::CustomData { .. } => RequiredTable::Cards, SearchNode::CustomData { .. } => RequiredTable::Cards,
SearchNode::Preset(_) => RequiredTable::Cards,
SearchNode::UnqualifiedText(_) => RequiredTable::Notes, SearchNode::UnqualifiedText(_) => RequiredTable::Notes,
SearchNode::SingleField { .. } => RequiredTable::Notes, SearchNode::SingleField { .. } => RequiredTable::Notes,
@ -989,7 +990,6 @@ impl SearchNode {
SearchNode::WholeCollection => RequiredTable::CardsOrNotes, SearchNode::WholeCollection => RequiredTable::CardsOrNotes,
SearchNode::CardTemplate(_) => RequiredTable::CardsAndNotes, SearchNode::CardTemplate(_) => RequiredTable::CardsAndNotes,
SearchNode::Preset(_) => RequiredTable::Cards,
} }
} }
} }

View file

@ -6,7 +6,6 @@ use std::mem;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use regex::Regex; use regex::Regex;
use crate::decks::DeckId as DeckIdType;
use crate::notetype::NotetypeId as NotetypeIdType; use crate::notetype::NotetypeId as NotetypeIdType;
use crate::prelude::*; use crate::prelude::*;
use crate::search::parser::parse; use crate::search::parser::parse;
@ -69,7 +68,7 @@ fn write_search_node(node: &SearchNode) -> String {
IntroducedInDays(u) => format!("introduced:{}", u), IntroducedInDays(u) => format!("introduced:{}", u),
CardTemplate(t) => write_template(t), CardTemplate(t) => write_template(t),
Deck(s) => maybe_quote(&format!("deck:{}", s)), Deck(s) => maybe_quote(&format!("deck:{}", s)),
DeckIdWithoutChildren(DeckIdType(i)) => format!("did:{}", i), DeckIdsWithoutChildren(s) => format!("did:{}", s),
// not exposed on the GUI end // not exposed on the GUI end
DeckIdWithChildren(_) => "".to_string(), DeckIdWithChildren(_) => "".to_string(),
NotetypeId(NotetypeIdType(i)) => format!("mid:{}", i), NotetypeId(NotetypeIdType(i)) => format!("mid:{}", i),