move filtered deck empty/fill to backend

emptying of individual card ids still to be done
This commit is contained in:
Damien Elmes 2020-09-02 12:54:33 +10:00
parent 8d2867aa2d
commit 8f9037cf0f
10 changed files with 269 additions and 231 deletions

View file

@ -103,6 +103,8 @@ service BackendService {
rpc RestoreBuriedAndSuspendedCards (CardIDs) returns (Empty); rpc RestoreBuriedAndSuspendedCards (CardIDs) returns (Empty);
rpc UnburyCardsInCurrentDeck (UnburyCardsInCurrentDeckIn) returns (Empty); rpc UnburyCardsInCurrentDeck (UnburyCardsInCurrentDeckIn) returns (Empty);
rpc BuryOrSuspendCards (BuryOrSuspendCardsIn) returns (Empty); rpc BuryOrSuspendCards (BuryOrSuspendCardsIn) returns (Empty);
rpc EmptyFilteredDeck (DeckID) returns (Empty);
rpc RebuildFilteredDeck (DeckID) returns (UInt32);
// stats // stats

View file

@ -6,7 +6,7 @@ from __future__ import annotations
import random import random
import time import time
from heapq import * from heapq import *
from typing import Any, List, Optional, Sequence, Tuple, Union from typing import Any, List, Optional, Tuple, Union
import anki import anki
from anki import hooks from anki import hooks
@ -599,77 +599,9 @@ did = ? and queue = {QUEUE_TYPE_REV} and due <= ? limit ?""",
idealIvl = self._fuzzedIvl(idealIvl) idealIvl = self._fuzzedIvl(idealIvl)
return 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: def _dynIvlBoost(self, card: Card) -> int:
assert card.odid and card.type == CARD_TYPE_REV assert card.odid and card.type == CARD_TYPE_REV
assert card.factor assert card.factor

View file

@ -25,7 +25,7 @@ import anki.backend_pb2 as pb
from anki import hooks from anki import hooks
from anki.cards import Card from anki.cards import Card
from anki.consts import * 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.lang import _
from anki.notes import Note from anki.notes import Note
from anki.rsbackend import ( 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 return ivl
# Dynamic deck handling # Filtered deck handling
########################################################################## ##########################################################################
_restoreQueueWhenEmptyingSnippet = f""" _restoreQueueWhenEmptyingSnippet = f"""
@ -1076,41 +1076,19 @@ end)
""" """
def rebuildDyn(self, did: Optional[int] = None) -> Optional[int]: def rebuildDyn(self, did: Optional[int] = None) -> Optional[int]:
"Rebuild a dynamic deck." "Rebuild a filtered deck."
did = did or self.col.decks.selected() did = did or self.col.decks.selected()
deck = self.col.decks.get(did) count = self.col.backend.rebuild_filtered_deck(did) or None
assert deck["dyn"] if not count:
# move any existing cards back first, then fill
self.emptyDyn(did)
cnt = self._fillDyn(deck)
if not cnt:
return None return None
# and change to our new deck # and change to our new deck
self.col.decks.select(did) self.col.decks.select(did)
return cnt return count
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
def emptyDyn(self, did: Optional[int], lim: Optional[str] = None) -> None: def emptyDyn(self, did: Optional[int], lim: Optional[str] = None) -> None:
if not lim: if lim is None:
lim = "did = %s" % did self.col.backend.empty_filtered_deck(did)
self.col.log(self.col.db.list("select id from cards where %s" % lim)) return
self.col.db.execute( 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: def remFromDyn(self, cids: List[int]) -> None:
self.emptyDyn(None, "id in %s and odid" % ids2str(cids)) 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: def _removeFromFiltered(self, card: Card) -> None:
if card.odid: if card.odid:
card.did = card.odid card.did = card.odid

View file

@ -539,6 +539,14 @@ impl BackendService for Backend {
}) })
} }
fn empty_filtered_deck(&mut self, input: pb::DeckId) -> BackendResult<Empty> {
self.with_col(|col| col.empty_filtered_deck(input.did.into()).map(Into::into))
}
fn rebuild_filtered_deck(&mut self, input: pb::DeckId) -> BackendResult<pb::UInt32> {
self.with_col(|col| col.rebuild_filtered_deck(input.did.into()).map(Into::into))
}
// statistics // statistics
//----------------------------------------------- //-----------------------------------------------

View file

@ -1,7 +1,7 @@
// Copyright: Ankitects Pty Ltd and contributors // Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // 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::define_newtype;
use crate::err::{AnkiError, Result}; use crate::err::{AnkiError, Result};
use crate::notes::NoteID; use crate::notes::NoteID;
@ -102,7 +102,42 @@ impl Card {
self.usn = usn; 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 { if self.original_deck_id.0 == 0 {
// not in a filtered deck // not in a filtered deck
return; return;
@ -110,14 +145,11 @@ impl Card {
self.deck_id = self.original_deck_id; self.deck_id = self.original_deck_id;
self.original_deck_id.0 = 0; 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 => { SchedulerVersion::V1 => {
match self.ctype { self.due = self.original_due;
self.queue = match self.ctype {
CardType::New => CardQueue::New, CardType::New => CardQueue::New,
CardType::Learn => CardQueue::New, CardType::Learn => CardQueue::New,
CardType::Review => CardQueue::Review, CardType::Review => CardQueue::Review,
@ -126,11 +158,19 @@ impl Card {
println!("did not expect relearn type in v1 for card {}", self.id); println!("did not expect relearn type in v1 for card {}", self.id);
CardQueue::New CardQueue::New
} }
};
if self.ctype == CardType::Learn {
self.ctype = CardType::New;
} }
} }
SchedulerVersion::V2 => { 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 { if (self.queue as i8) >= 0 {
match self.ctype { self.queue = match self.ctype {
CardType::Learn | CardType::Relearn => { CardType::Learn | CardType::Relearn => {
if self.due > 1_000_000_000 { if self.due > 1_000_000_000 {
// unix timestamp // unix timestamp
@ -143,15 +183,11 @@ impl Card {
CardType::New => CardQueue::New, CardType::New => CardQueue::New,
CardType::Review => CardQueue::Review, CardType::Review => CardQueue::Review,
} }
} else {
self.queue
} }
} }
}; }
if sched == SchedulerVersion::V1 && self.ctype == CardType::Learn { self.original_due = 0;
self.ctype = CardType::New;
}
} }
/// Remove the card from the (re)learning queue. /// Remove the card from the (re)learning queue.

167
rslib/src/decks/filtered.rs Normal file
View file

@ -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<u32> {
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<u32> {
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<i32> {
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)
}

View file

@ -7,7 +7,6 @@ pub use crate::backend_proto::{
DeckCommon, DeckKind as DeckKindProto, FilteredDeck, FilteredSearchTerm, NormalDeck, DeckCommon, DeckKind as DeckKindProto, FilteredDeck, FilteredSearchTerm, NormalDeck,
}; };
use crate::{ use crate::{
card::CardID,
collection::Collection, collection::Collection,
deckconf::DeckConfID, deckconf::DeckConfID,
define_newtype, define_newtype,
@ -18,9 +17,11 @@ use crate::{
types::Usn, types::Usn,
}; };
mod counts; mod counts;
mod filtered;
mod schema11; mod schema11;
mod tree; mod tree;
pub(crate) use counts::DueCounts; pub(crate) use counts::DueCounts;
pub(crate) use filtered::DeckFilterContext;
pub use schema11::DeckSchema11; pub use schema11::DeckSchema11;
use std::{borrow::Cow, sync::Arc}; 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) { fn reset_stats_if_day_changed(&mut self, today: u32) {
let c = &mut self.common; let c = &mut self.common;
if c.last_day_studied != today { if c.last_day_studied != today {
@ -80,12 +62,6 @@ impl Deck {
c.last_day_studied = today; 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. /// Returns deck config ID if deck is a normal deck.
pub(crate) fn config_id(&self) -> Option<DeckConfID> { pub(crate) fn config_id(&self) -> Option<DeckConfID> {
@ -434,23 +410,6 @@ impl Collection {
self.remove_cards_and_orphaned_notes(&cids) 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<Vec<(DeckID, String)>> { pub fn get_all_deck_names(&self, skip_empty_default: bool) -> Result<Vec<(DeckID, String)>> {
if skip_empty_default && self.default_deck_is_empty()? { if skip_empty_default && self.default_deck_is_empty()? {
Ok(self Ok(self

View file

@ -7,6 +7,7 @@ use crate::{
collection::Collection, collection::Collection,
config::SchedulerVersion, config::SchedulerVersion,
err::Result, err::Result,
search::SortMode,
}; };
use super::cutoff::SchedTimingToday; use super::cutoff::SchedTimingToday;
@ -59,7 +60,7 @@ impl Collection {
/// Unbury cards from the previous day. /// Unbury cards from the previous day.
/// Done automatically, and does not mark the cards as modified. /// Done automatically, and does not mark the cards as modified.
fn unbury_on_day_rollover(&mut self) -> Result<()> { 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| { self.storage.for_each_card_in_search(|mut card| {
card.restore_queue_after_bury_or_suspend(); card.restore_queue_after_bury_or_suspend();
self.storage.update_card(&card) self.storage.update_card(&card)
@ -94,7 +95,7 @@ impl Collection {
UnburyDeckMode::SchedOnly => "is:buried-sibling", UnburyDeckMode::SchedOnly => "is:buried-sibling",
}; };
self.transact(None, |col| { 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() col.unsuspend_or_unbury_searched_cards()
}) })
} }
@ -125,7 +126,7 @@ impl Collection {
}; };
if card.queue != desired_queue { if card.queue != desired_queue {
if sched == SchedulerVersion::V1 { if sched == SchedulerVersion::V1 {
card.return_home(sched); card.remove_from_filtered_deck(sched);
card.remove_from_learning(); card.remove_from_learning();
} }
card.queue = desired_queue; card.queue = desired_queue;

View file

@ -63,20 +63,7 @@ impl Collection {
let writer = SqlWriter::new(self); let writer = SqlWriter::new(self);
let (mut sql, args) = writer.build_cards_query(&top_node, mode.required_table())?; let (mut sql, args) = writer.build_cards_query(&top_node, mode.required_table())?;
self.add_order(&mut sql, mode)?;
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);
}
}
let mut stmt = self.storage.db.prepare(&sql)?; let mut stmt = self.storage.db.prepare(&sql)?;
let ids: Vec<_> = stmt let ids: Vec<_> = stmt
@ -86,13 +73,32 @@ impl Collection {
Ok(ids) 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 /// Place the matched card ids into a temporary 'search_cids' table
/// instead of returning them. Use clear_searched_cards() to remove it. /// 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 top_node = Node::Group(parse(search)?);
let writer = SqlWriter::new(self); 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 self.storage
.db .db
.execute_batch(include_str!("search_cids_setup.sql"))?; .execute_batch(include_str!("search_cids_setup.sql"))?;

View file

@ -1,7 +1,7 @@
// Copyright: Ankitects Pty Ltd and contributors // Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // 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 { impl Collection {
pub(crate) fn graph_data_for_search( pub(crate) fn graph_data_for_search(
@ -9,7 +9,7 @@ impl Collection {
search: &str, search: &str,
days: u32, days: u32,
) -> Result<pb::GraphsOut> { ) -> Result<pb::GraphsOut> {
self.search_cards_into_table(search)?; self.search_cards_into_table(search, SortMode::NoOrder)?;
let all = search.trim().is_empty(); let all = search.trim().is_empty();
self.graph_data(all, days) self.graph_data(all, days)
} }