From 8f9037cf0ff50720547d0202e42d6d601a84a966 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Wed, 2 Sep 2020 12:54:33 +1000 Subject: [PATCH] move filtered deck empty/fill to backend emptying of individual card ids still to be done --- proto/backend.proto | 2 + pylib/anki/sched.py | 72 +----------- pylib/anki/schedv2.py | 91 ++------------- rslib/src/backend/mod.rs | 8 ++ rslib/src/card.rs | 66 ++++++++--- rslib/src/decks/filtered.rs | 167 ++++++++++++++++++++++++++++ rslib/src/decks/mod.rs | 45 +------- rslib/src/sched/bury_and_suspend.rs | 7 +- rslib/src/search/cards.rs | 38 ++++--- rslib/src/stats/graphs.rs | 4 +- 10 files changed, 269 insertions(+), 231 deletions(-) create mode 100644 rslib/src/decks/filtered.rs diff --git a/proto/backend.proto b/proto/backend.proto index ac5340e32..bac9d8a9e 100644 --- a/proto/backend.proto +++ b/proto/backend.proto @@ -103,6 +103,8 @@ service BackendService { rpc RestoreBuriedAndSuspendedCards (CardIDs) returns (Empty); rpc UnburyCardsInCurrentDeck (UnburyCardsInCurrentDeckIn) returns (Empty); rpc BuryOrSuspendCards (BuryOrSuspendCardsIn) returns (Empty); + rpc EmptyFilteredDeck (DeckID) returns (Empty); + rpc RebuildFilteredDeck (DeckID) returns (UInt32); // stats diff --git a/pylib/anki/sched.py b/pylib/anki/sched.py index 924972b34..333c4d8e0 100644 --- a/pylib/anki/sched.py +++ b/pylib/anki/sched.py @@ -6,7 +6,7 @@ from __future__ import annotations import random import time from heapq import * -from typing import Any, List, Optional, Sequence, Tuple, Union +from typing import Any, List, Optional, Tuple, Union import anki from anki import hooks @@ -599,77 +599,9 @@ did = ? and queue = {QUEUE_TYPE_REV} and due <= ? limit ?""", idealIvl = self._fuzzedIvl(idealIvl) return idealIvl - # Dynamic deck handling + # Filtered deck handling ########################################################################## - def rebuildDyn(self, did: Optional[int] = None) -> Optional[Sequence[int]]: # type: ignore[override] - "Rebuild a dynamic deck." - did = did or self.col.decks.selected() - deck = self.col.decks.get(did) - assert deck["dyn"] - # move any existing cards back first, then fill - self.emptyDyn(did) - ids = self._fillDyn(deck) - if not ids: - return None - # and change to our new deck - self.col.decks.select(did) - return ids - - def _fillDyn(self, deck: Deck) -> Sequence[int]: # type: ignore[override] - search, limit, order = deck["terms"][0] - orderlimit = self._dynOrder(order, limit) - if search.strip(): - search = "(%s)" % search - search = "%s -is:suspended -is:buried -deck:filtered -is:learn" % search - try: - ids = self.col.findCards(search, order=orderlimit) - except: - ids = [] - return ids - # move the cards over - self.col.log(deck["id"], ids) - self._moveToDyn(deck["id"], ids) - return ids - - def emptyDyn(self, did: Optional[int], lim: Optional[str] = None) -> None: - if not lim: - lim = "did = %s" % did - self.col.log(self.col.db.list("select id from cards where %s" % lim)) - # move out of cram queue - self.col.db.execute( - f""" -update cards set did = odid, queue = (case when type = {CARD_TYPE_LRN} then {QUEUE_TYPE_NEW} -else type end), type = (case when type = {CARD_TYPE_LRN} then {CARD_TYPE_NEW} else type end), -due = odue, odue = 0, odid = 0, usn = ? where %s""" - % lim, - self.col.usn(), - ) - - def _moveToDyn(self, did: int, ids: Sequence[int]) -> None: # type: ignore[override] - deck = self.col.decks.get(did) - data = [] - t = intTime() - u = self.col.usn() - for c, id in enumerate(ids): - # start at -100000 so that reviews are all due - data.append((did, -100000 + c, u, id)) - # due reviews stay in the review queue. careful: can't use - # "odid or did", as sqlite converts to boolean - queue = f""" -(case when type={CARD_TYPE_REV} and (case when odue then odue <= %d else due <= %d end) - then {QUEUE_TYPE_REV} else {QUEUE_TYPE_NEW} end)""" - queue %= (self.today, self.today) - self.col.db.executemany( - """ -update cards set -odid = (case when odid then odid else did end), -odue = (case when odue then odue else due end), -did = ?, queue = %s, due = ?, usn = ? where id = ?""" - % queue, - data, - ) - def _dynIvlBoost(self, card: Card) -> int: assert card.odid and card.type == CARD_TYPE_REV assert card.factor diff --git a/pylib/anki/schedv2.py b/pylib/anki/schedv2.py index 7ca006b83..53ab369fb 100644 --- a/pylib/anki/schedv2.py +++ b/pylib/anki/schedv2.py @@ -25,7 +25,7 @@ import anki.backend_pb2 as pb from anki import hooks from anki.cards import Card from anki.consts import * -from anki.decks import Deck, DeckConfig, DeckManager, FilteredDeck, QueueConfig +from anki.decks import Deck, DeckConfig, DeckManager, QueueConfig from anki.lang import _ from anki.notes import Note from anki.rsbackend import ( @@ -1062,7 +1062,7 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l return ivl - # Dynamic deck handling + # Filtered deck handling ########################################################################## _restoreQueueWhenEmptyingSnippet = f""" @@ -1076,41 +1076,19 @@ end) """ def rebuildDyn(self, did: Optional[int] = None) -> Optional[int]: - "Rebuild a dynamic deck." + "Rebuild a filtered deck." did = did or self.col.decks.selected() - deck = self.col.decks.get(did) - assert deck["dyn"] - # move any existing cards back first, then fill - self.emptyDyn(did) - cnt = self._fillDyn(deck) - if not cnt: + count = self.col.backend.rebuild_filtered_deck(did) or None + if not count: return None # and change to our new deck self.col.decks.select(did) - return cnt - - def _fillDyn(self, deck: FilteredDeck) -> int: - start = -100000 - total = 0 - for search, limit, order in deck["terms"]: - orderlimit = self._dynOrder(order, limit) - if search.strip(): - search = "(%s)" % search - search = "%s -is:suspended -is:buried -deck:filtered" % search - try: - ids = self.col.findCards(search, order=orderlimit) - except: - return total - # move the cards over - self.col.log(deck["id"], ids) - self._moveToDyn(deck["id"], ids, start=start + total) - total += len(ids) - return total + return count def emptyDyn(self, did: Optional[int], lim: Optional[str] = None) -> None: - if not lim: - lim = "did = %s" % did - self.col.log(self.col.db.list("select id from cards where %s" % lim)) + if lim is None: + self.col.backend.empty_filtered_deck(did) + return self.col.db.execute( """ @@ -1123,57 +1101,6 @@ due = (case when odue>0 then odue else due end), odue = 0, odid = 0, usn = ? whe def remFromDyn(self, cids: List[int]) -> None: self.emptyDyn(None, "id in %s and odid" % ids2str(cids)) - def _dynOrder(self, o: int, l: int) -> str: - if o == DYN_OLDEST: - t = "(select max(id) from revlog where cid=c.id)" - elif o == DYN_RANDOM: - t = "random()" - elif o == DYN_SMALLINT: - t = "ivl" - elif o == DYN_BIGINT: - t = "ivl desc" - elif o == DYN_LAPSES: - t = "lapses desc" - elif o == DYN_ADDED: - t = "n.id" - elif o == DYN_REVADDED: - t = "n.id desc" - elif o == DYN_DUEPRIORITY: - t = ( - f"(case when queue={QUEUE_TYPE_REV} and due <= %d then (ivl / cast(%d-due+0.001 as real)) else 100000+due end)" - % (self.today, self.today) - ) - else: # DYN_DUE or unknown - t = "c.due, c.ord" - return t + " limit %d" % l - - def _moveToDyn(self, did: int, ids: Sequence[int], start: int = -100000) -> None: - deck = self.col.decks.get(did) - data = [] - u = self.col.usn() - due = start - for id in ids: - data.append((did, due, u, id)) - due += 1 - - queue = "" - if not deck["resched"]: - queue = f",queue={QUEUE_TYPE_REV}" - - query = ( - """ -update cards set -odid = did, odue = due, -did = ?, -due = (case when due <= 0 then due else ? end), -usn = ? -%s -where id = ? -""" - % queue - ) - self.col.db.executemany(query, data) - def _removeFromFiltered(self, card: Card) -> None: if card.odid: card.did = card.odid diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 72be90733..977a1dce2 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -539,6 +539,14 @@ impl BackendService for Backend { }) } + fn empty_filtered_deck(&mut self, input: pb::DeckId) -> BackendResult { + self.with_col(|col| col.empty_filtered_deck(input.did.into()).map(Into::into)) + } + + fn rebuild_filtered_deck(&mut self, input: pb::DeckId) -> BackendResult { + self.with_col(|col| col.rebuild_filtered_deck(input.did.into()).map(Into::into)) + } + // statistics //----------------------------------------------- diff --git a/rslib/src/card.rs b/rslib/src/card.rs index 9baf69c8a..65ada96c5 100644 --- a/rslib/src/card.rs +++ b/rslib/src/card.rs @@ -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::decks::DeckID; +use crate::decks::{DeckFilterContext, DeckID}; use crate::define_newtype; use crate::err::{AnkiError, Result}; use crate::notes::NoteID; @@ -102,7 +102,42 @@ impl Card { self.usn = usn; } - pub(crate) fn return_home(&mut self, sched: SchedulerVersion) { + pub(crate) fn move_into_filtered_deck(&mut self, ctx: &DeckFilterContext, position: i32) { + // filtered and v1 learning cards are excluded, so odue should be guaranteed to be zero + if self.original_due != 0 { + println!("bug: odue was set"); + return; + } + + self.original_deck_id = self.deck_id; + self.deck_id = ctx.target_deck; + + self.original_due = self.due; + + if ctx.scheduler == SchedulerVersion::V1 { + if self.ctype == CardType::Review && self.due <= ctx.today as i32 { + // review cards that are due are left in the review queue + } else { + // new + non-due go into new queue + self.queue = CardQueue::New; + } + if self.due != 0 { + self.due = position; + } + } else { + // if rescheduling is disabled, all cards go in the review queue + if !ctx.config.reschedule { + self.queue = CardQueue::Review; + } + // fixme: can we unify this with v1 scheduler in the future? + // https://anki.tenderapp.com/discussions/ankidesktop/35978-rebuilding-filtered-deck-on-experimental-v2-empties-deck-and-reschedules-to-the-year-1745 + if self.due > 0 { + self.due = position; + } + } + } + + pub(crate) fn remove_from_filtered_deck(&mut self, sched: SchedulerVersion) { if self.original_deck_id.0 == 0 { // not in a filtered deck return; @@ -110,14 +145,11 @@ impl Card { self.deck_id = self.original_deck_id; self.original_deck_id.0 = 0; - if self.original_due > 0 { - self.due = self.original_due; - } - self.original_due = 0; - self.queue = match sched { + match sched { SchedulerVersion::V1 => { - match self.ctype { + self.due = self.original_due; + self.queue = match self.ctype { CardType::New => CardQueue::New, CardType::Learn => CardQueue::New, CardType::Review => CardQueue::Review, @@ -126,11 +158,19 @@ impl Card { println!("did not expect relearn type in v1 for card {}", self.id); CardQueue::New } + }; + if self.ctype == CardType::Learn { + self.ctype = CardType::New; } } SchedulerVersion::V2 => { + // original_due is cleared if card answered in filtered deck + if self.original_due > 0 { + self.due = self.original_due; + } + if (self.queue as i8) >= 0 { - match self.ctype { + self.queue = match self.ctype { CardType::Learn | CardType::Relearn => { if self.due > 1_000_000_000 { // unix timestamp @@ -143,15 +183,11 @@ impl Card { CardType::New => CardQueue::New, CardType::Review => CardQueue::Review, } - } else { - self.queue } } - }; - - if sched == SchedulerVersion::V1 && self.ctype == CardType::Learn { - self.ctype = CardType::New; } + + self.original_due = 0; } /// Remove the card from the (re)learning queue. diff --git a/rslib/src/decks/filtered.rs b/rslib/src/decks/filtered.rs new file mode 100644 index 000000000..c93de02ce --- /dev/null +++ b/rslib/src/decks/filtered.rs @@ -0,0 +1,167 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use super::{Deck, DeckID}; +pub use crate::backend_proto::{ + deck_kind::Kind as DeckKind, filtered_search_term::FilteredSearchOrder, Deck as DeckProto, + DeckCommon, DeckKind as DeckKindProto, FilteredDeck, FilteredSearchTerm, NormalDeck, +}; +use crate::{ + card::{CardID, CardQueue}, + collection::Collection, + config::SchedulerVersion, + err::Result, + prelude::AnkiError, + search::SortMode, + timestamp::TimestampSecs, + types::Usn, +}; + +impl Deck { + pub fn new_filtered() -> Deck { + let mut filt = FilteredDeck::default(); + filt.search_terms.push(FilteredSearchTerm { + search: "".into(), + limit: 100, + order: 0, + }); + filt.preview_delay = 10; + filt.reschedule = true; + Deck { + id: DeckID(0), + name: "".into(), + mtime_secs: TimestampSecs(0), + usn: Usn(0), + common: DeckCommon::default(), + kind: DeckKind::Filtered(filt), + } + } + + pub(crate) fn is_filtered(&self) -> bool { + matches!(self.kind, DeckKind::Filtered(_)) + } +} + +pub(crate) struct DeckFilterContext<'a> { + pub target_deck: DeckID, + pub config: &'a FilteredDeck, + pub scheduler: SchedulerVersion, + pub usn: Usn, + pub today: u32, +} + +impl Collection { + pub fn empty_filtered_deck(&mut self, did: DeckID) -> Result<()> { + self.transact(None, |col| col.return_all_cards_in_filtered_deck(did)) + } + pub(super) fn return_all_cards_in_filtered_deck(&mut self, did: DeckID) -> Result<()> { + let cids = self.storage.all_cards_in_single_deck(did)?; + self.return_cards_to_home_deck(&cids) + } + + // Unlike the old Python code, this also marks the cards as modified. + fn return_cards_to_home_deck(&mut self, cids: &[CardID]) -> Result<()> { + let sched = self.sched_ver(); + let usn = self.usn()?; + for cid in cids { + if let Some(mut card) = self.storage.get_card(*cid)? { + let original = card.clone(); + card.remove_from_filtered_deck(sched); + self.update_card(&mut card, &original, usn)?; + } + } + Ok(()) + } + + // Unlike the old Python code, this also marks the cards as modified. + pub fn rebuild_filtered_deck(&mut self, did: DeckID) -> Result { + let deck = self.get_deck(did)?.ok_or(AnkiError::NotFound)?; + let config = if let DeckKind::Filtered(kind) = &deck.kind { + kind + } else { + return Err(AnkiError::invalid_input("not filtered")); + }; + let ctx = DeckFilterContext { + target_deck: did, + config, + scheduler: self.sched_ver(), + usn: self.usn()?, + today: self.timing_today()?.days_elapsed, + }; + + self.transact(None, |col| { + col.return_all_cards_in_filtered_deck(did)?; + col.build_filtered_deck(ctx) + }) + } + + fn build_filtered_deck(&mut self, ctx: DeckFilterContext) -> Result { + let start = -100_000; + let mut position = start; + for term in &ctx.config.search_terms { + position = self.move_cards_matching_term(&ctx, term, position)?; + } + + Ok((position - start) as u32) + } + + /// Move matching cards into filtered deck. + /// Returns the new starting position. + fn move_cards_matching_term( + &mut self, + ctx: &DeckFilterContext, + term: &FilteredSearchTerm, + mut position: i32, + ) -> Result { + let search = format!( + "{} -is:suspended -is:buried -deck:filtered {}", + if term.search.trim().is_empty() { + "".to_string() + } else { + format!("({})", term.search) + }, + if ctx.scheduler == SchedulerVersion::V1 { + "-is:learn" + } else { + "" + } + ); + let order = order_and_limit_for_search(term, ctx.today); + + self.search_cards_into_table(&search, SortMode::Custom(order))?; + for mut card in self.storage.all_searched_cards()? { + let original = card.clone(); + card.move_into_filtered_deck(ctx, position); + self.update_card(&mut card, &original, ctx.usn)?; + position += 1; + } + + Ok(position) + } +} + +fn order_and_limit_for_search(term: &FilteredSearchTerm, today: u32) -> String { + let temp_string; + let order = match term.order() { + FilteredSearchOrder::OldestFirst => "(select max(id) from revlog where cid=c.id)", + FilteredSearchOrder::Random => "random()", + FilteredSearchOrder::IntervalsAscending => "ivl", + FilteredSearchOrder::IntervalsDescending => "ivl desc", + FilteredSearchOrder::Lapses => "lapses desc", + FilteredSearchOrder::Added => "n.id", + FilteredSearchOrder::ReverseAdded => "n.id desc", + FilteredSearchOrder::Due => "c.due, c.ord", + FilteredSearchOrder::DuePriority => { + temp_string = format!( + " +(case when queue={rev_queue} and due <= {today} +then (ivl / cast({today}-due+0.001 as real)) else 100000+due end)", + rev_queue = CardQueue::Review as i8, + today = today + ); + &temp_string + } + }; + + format!("{} limit {}", order, term.limit) +} diff --git a/rslib/src/decks/mod.rs b/rslib/src/decks/mod.rs index f1968bb4e..af7db1845 100644 --- a/rslib/src/decks/mod.rs +++ b/rslib/src/decks/mod.rs @@ -7,7 +7,6 @@ pub use crate::backend_proto::{ DeckCommon, DeckKind as DeckKindProto, FilteredDeck, FilteredSearchTerm, NormalDeck, }; use crate::{ - card::CardID, collection::Collection, deckconf::DeckConfID, define_newtype, @@ -18,9 +17,11 @@ use crate::{ types::Usn, }; mod counts; +mod filtered; mod schema11; mod tree; pub(crate) use counts::DueCounts; +pub(crate) use filtered::DeckFilterContext; pub use schema11::DeckSchema11; use std::{borrow::Cow, sync::Arc}; @@ -51,25 +52,6 @@ impl Deck { } } - pub fn new_filtered() -> Deck { - let mut filt = FilteredDeck::default(); - filt.search_terms.push(FilteredSearchTerm { - search: "".into(), - limit: 100, - order: 0, - }); - filt.preview_delay = 10; - filt.reschedule = true; - Deck { - id: DeckID(0), - name: "".into(), - mtime_secs: TimestampSecs(0), - usn: Usn(0), - common: DeckCommon::default(), - kind: DeckKind::Filtered(filt), - } - } - fn reset_stats_if_day_changed(&mut self, today: u32) { let c = &mut self.common; if c.last_day_studied != today { @@ -80,12 +62,6 @@ impl Deck { c.last_day_studied = today; } } -} - -impl Deck { - pub(crate) fn is_filtered(&self) -> bool { - matches!(self.kind, DeckKind::Filtered(_)) - } /// Returns deck config ID if deck is a normal deck. pub(crate) fn config_id(&self) -> Option { @@ -434,23 +410,6 @@ impl Collection { self.remove_cards_and_orphaned_notes(&cids) } - fn return_all_cards_in_filtered_deck(&mut self, did: DeckID) -> Result<()> { - let cids = self.storage.all_cards_in_single_deck(did)?; - self.return_cards_to_home_deck(&cids) - } - - fn return_cards_to_home_deck(&mut self, cids: &[CardID]) -> Result<()> { - let sched = self.sched_ver(); - for cid in cids { - if let Some(mut card) = self.storage.get_card(*cid)? { - // fixme: undo - card.return_home(sched); - self.storage.update_card(&card)?; - } - } - Ok(()) - } - pub fn get_all_deck_names(&self, skip_empty_default: bool) -> Result> { if skip_empty_default && self.default_deck_is_empty()? { Ok(self diff --git a/rslib/src/sched/bury_and_suspend.rs b/rslib/src/sched/bury_and_suspend.rs index fe445f580..a8201ead9 100644 --- a/rslib/src/sched/bury_and_suspend.rs +++ b/rslib/src/sched/bury_and_suspend.rs @@ -7,6 +7,7 @@ use crate::{ collection::Collection, config::SchedulerVersion, err::Result, + search::SortMode, }; use super::cutoff::SchedTimingToday; @@ -59,7 +60,7 @@ impl Collection { /// Unbury cards from the previous day. /// Done automatically, and does not mark the cards as modified. fn unbury_on_day_rollover(&mut self) -> Result<()> { - self.search_cards_into_table("is:buried")?; + self.search_cards_into_table("is:buried", SortMode::NoOrder)?; self.storage.for_each_card_in_search(|mut card| { card.restore_queue_after_bury_or_suspend(); self.storage.update_card(&card) @@ -94,7 +95,7 @@ impl Collection { UnburyDeckMode::SchedOnly => "is:buried-sibling", }; self.transact(None, |col| { - col.search_cards_into_table(&format!("deck:current {}", search))?; + col.search_cards_into_table(&format!("deck:current {}", search), SortMode::NoOrder)?; col.unsuspend_or_unbury_searched_cards() }) } @@ -125,7 +126,7 @@ impl Collection { }; if card.queue != desired_queue { if sched == SchedulerVersion::V1 { - card.return_home(sched); + card.remove_from_filtered_deck(sched); card.remove_from_learning(); } card.queue = desired_queue; diff --git a/rslib/src/search/cards.rs b/rslib/src/search/cards.rs index 4be6d3648..351abc89b 100644 --- a/rslib/src/search/cards.rs +++ b/rslib/src/search/cards.rs @@ -63,20 +63,7 @@ impl Collection { let writer = SqlWriter::new(self); let (mut sql, args) = writer.build_cards_query(&top_node, mode.required_table())?; - - match mode { - SortMode::NoOrder => (), - SortMode::FromConfig => unreachable!(), - SortMode::Builtin { kind, reverse } => { - prepare_sort(self, kind)?; - sql.push_str(" order by "); - write_order(&mut sql, kind, reverse)?; - } - SortMode::Custom(order_clause) => { - sql.push_str(" order by "); - sql.push_str(&order_clause); - } - } + self.add_order(&mut sql, mode)?; let mut stmt = self.storage.db.prepare(&sql)?; let ids: Vec<_> = stmt @@ -86,13 +73,32 @@ impl Collection { Ok(ids) } + fn add_order(&mut self, sql: &mut String, mode: SortMode) -> Result<()> { + match mode { + SortMode::NoOrder => (), + SortMode::FromConfig => unreachable!(), + SortMode::Builtin { kind, reverse } => { + prepare_sort(self, kind)?; + sql.push_str(" order by "); + write_order(sql, kind, reverse)?; + } + SortMode::Custom(order_clause) => { + sql.push_str(" order by "); + sql.push_str(&order_clause); + } + } + Ok(()) + } + /// Place the matched card ids into a temporary 'search_cids' table /// instead of returning them. Use clear_searched_cards() to remove it. - pub(crate) fn search_cards_into_table(&mut self, search: &str) -> Result<()> { + pub(crate) fn search_cards_into_table(&mut self, search: &str, mode: SortMode) -> Result<()> { let top_node = Node::Group(parse(search)?); let writer = SqlWriter::new(self); - let (sql, args) = writer.build_cards_query(&top_node, RequiredTable::Cards)?; + let (mut sql, args) = writer.build_cards_query(&top_node, mode.required_table())?; + self.add_order(&mut sql, mode)?; + self.storage .db .execute_batch(include_str!("search_cids_setup.sql"))?; diff --git a/rslib/src/stats/graphs.rs b/rslib/src/stats/graphs.rs index 601de79fe..8a2432315 100644 --- a/rslib/src/stats/graphs.rs +++ b/rslib/src/stats/graphs.rs @@ -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::{backend_proto as pb, prelude::*, revlog::RevlogEntry}; +use crate::{backend_proto as pb, prelude::*, revlog::RevlogEntry, search::SortMode}; impl Collection { pub(crate) fn graph_data_for_search( @@ -9,7 +9,7 @@ impl Collection { search: &str, days: u32, ) -> Result { - self.search_cards_into_table(search)?; + self.search_cards_into_table(search, SortMode::NoOrder)?; let all = search.trim().is_empty(); self.graph_data(all, days) }