switch search parser to using owned values

I was a bit too enthusiastic with using borrowed values in structs
earlier on in the Rust porting. In this case any performance gains are
dwarfed by the cost of querying the DB, and using owned values here
simplifies the code, and will make it easier to parse a fragment in
the From<SearchTerm> impl.
This commit is contained in:
Damien Elmes 2021-02-11 12:19:36 +10:00
parent 8852359fa9
commit 242b4ea505
5 changed files with 52 additions and 59 deletions

View File

@ -296,41 +296,32 @@ impl From<pb::DeckConfigId> for DeckConfID {
}
}
impl From<pb::SearchTerm> for Node<'_> {
impl From<pb::SearchTerm> for Node {
fn from(msg: pb::SearchTerm) -> Self {
use pb::search_term::group::Operator;
use pb::search_term::Filter;
use pb::search_term::Flag;
if let Some(filter) = msg.filter {
match filter {
Filter::Tag(s) => Node::Search(SearchNode::Tag(
escape_anki_wildcards(&s).into_owned().into(),
)),
Filter::Deck(s) => Node::Search(SearchNode::Deck(
if s == "*" {
s
} else {
escape_anki_wildcards(&s).into_owned()
}
.into(),
)),
Filter::Note(s) => Node::Search(SearchNode::NoteType(
escape_anki_wildcards(&s).into_owned().into(),
)),
Filter::Tag(s) => Node::Search(SearchNode::Tag(escape_anki_wildcards(&s))),
Filter::Deck(s) => Node::Search(SearchNode::Deck(if s == "*" {
s
} else {
escape_anki_wildcards(&s)
})),
Filter::Note(s) => Node::Search(SearchNode::NoteType(escape_anki_wildcards(&s))),
Filter::Template(u) => {
Node::Search(SearchNode::CardTemplate(TemplateKind::Ordinal(u as u16)))
}
Filter::Nid(nid) => Node::Search(SearchNode::NoteIDs(nid.to_string().into())),
Filter::Nids(nids) => {
Node::Search(SearchNode::NoteIDs(nids.into_id_string().into()))
}
Filter::Nid(nid) => Node::Search(SearchNode::NoteIDs(nid.to_string())),
Filter::Nids(nids) => Node::Search(SearchNode::NoteIDs(nids.into_id_string())),
Filter::Dupe(dupe) => Node::Search(SearchNode::Duplicates {
note_type_id: dupe.notetype_id.into(),
text: dupe.first_field.into(),
text: dupe.first_field,
}),
Filter::FieldName(s) => Node::Search(SearchNode::SingleField {
field: escape_anki_wildcards(&s).into_owned().into(),
text: "*".to_string().into(),
field: escape_anki_wildcards(&s),
text: "*".to_string(),
is_re: false,
}),
Filter::Rated(rated) => Node::Search(SearchNode::Rated {

View File

@ -17,7 +17,6 @@ use nom::{
sequence::{preceded, separated_pair},
};
use regex::{Captures, Regex};
use std::borrow::Cow;
type IResult<'a, O> = std::result::Result<(&'a str, O), nom::Err<ParseError<'a>>>;
type ParseResult<'a, O> = std::result::Result<O, nom::Err<ParseError<'a>>>;
@ -31,52 +30,52 @@ fn parse_error(input: &str) -> nom::Err<ParseError<'_>> {
}
#[derive(Debug, PartialEq, Clone)]
pub enum Node<'a> {
pub enum Node {
And,
Or,
Not(Box<Node<'a>>),
Group(Vec<Node<'a>>),
Search(SearchNode<'a>),
Not(Box<Node>),
Group(Vec<Node>),
Search(SearchNode),
}
#[derive(Debug, PartialEq, Clone)]
pub enum SearchNode<'a> {
pub enum SearchNode {
// text without a colon
UnqualifiedText(Cow<'a, str>),
UnqualifiedText(String),
// foo:bar, where foo doesn't match a term below
SingleField {
field: Cow<'a, str>,
text: Cow<'a, str>,
field: String,
text: String,
is_re: bool,
},
AddedInDays(u32),
EditedInDays(u32),
CardTemplate(TemplateKind<'a>),
Deck(Cow<'a, str>),
CardTemplate(TemplateKind),
Deck(String),
DeckID(DeckID),
NoteTypeID(NoteTypeID),
NoteType(Cow<'a, str>),
NoteType(String),
Rated {
days: u32,
ease: RatingKind,
},
Tag(Cow<'a, str>),
Tag(String),
Duplicates {
note_type_id: NoteTypeID,
text: Cow<'a, str>,
text: String,
},
State(StateKind),
Flag(u8),
NoteIDs(Cow<'a, str>),
CardIDs(&'a str),
NoteIDs(String),
CardIDs(String),
Property {
operator: String,
kind: PropertyKind,
},
WholeCollection,
Regex(Cow<'a, str>),
NoCombining(Cow<'a, str>),
WordBoundary(Cow<'a, str>),
Regex(String),
NoCombining(String),
WordBoundary(String),
}
#[derive(Debug, PartialEq, Clone)]
@ -103,9 +102,9 @@ pub enum StateKind {
}
#[derive(Debug, PartialEq, Clone)]
pub enum TemplateKind<'a> {
pub enum TemplateKind {
Ordinal(u16),
Name(Cow<'a, str>),
Name(String),
}
#[derive(Debug, PartialEq, Clone)]
@ -303,7 +302,7 @@ fn search_node_for_text(s: &str) -> ParseResult<SearchNode> {
fn search_node_for_text_with_argument<'a>(
key: &'a str,
val: &'a str,
) -> ParseResult<'a, SearchNode<'a>> {
) -> ParseResult<'a, SearchNode> {
Ok(match key.to_ascii_lowercase().as_str() {
"deck" => SearchNode::Deck(unescape(val)?),
"note" => SearchNode::NoteType(unescape(val)?),
@ -319,7 +318,7 @@ fn search_node_for_text_with_argument<'a>(
"did" => parse_did(val)?,
"mid" => parse_mid(val)?,
"nid" => SearchNode::NoteIDs(check_id_list(val, key)?.into()),
"cid" => SearchNode::CardIDs(check_id_list(val, key)?),
"cid" => SearchNode::CardIDs(check_id_list(val, key)?.into()),
"re" => SearchNode::Regex(unescape_quotes(val)),
"nc" => SearchNode::NoCombining(unescape(val)?),
"w" => SearchNode::WordBoundary(unescape(val)?),
@ -579,7 +578,7 @@ fn parse_dupe(s: &str) -> ParseResult<SearchNode> {
}
}
fn parse_single_field<'a>(key: &'a str, val: &'a str) -> ParseResult<'a, SearchNode<'a>> {
fn parse_single_field<'a>(key: &'a str, val: &'a str) -> ParseResult<'a, SearchNode> {
Ok(if let Some(stripped) = val.strip_prefix("re:") {
SearchNode::SingleField {
field: unescape(key)?,
@ -596,25 +595,25 @@ fn parse_single_field<'a>(key: &'a str, val: &'a str) -> ParseResult<'a, SearchN
}
/// For strings without unescaped ", convert \" to "
fn unescape_quotes(s: &str) -> Cow<str> {
fn unescape_quotes(s: &str) -> String {
if s.contains('"') {
s.replace(r#"\""#, "\"").into()
s.replace(r#"\""#, "\"")
} else {
s.into()
}
}
/// For non-globs like dupe text without any assumption about the content
fn unescape_quotes_and_backslashes(s: &str) -> Cow<str> {
fn unescape_quotes_and_backslashes(s: &str) -> String {
if s.contains('"') || s.contains('\\') {
s.replace(r#"\""#, "\"").replace(r"\\", r"\").into()
s.replace(r#"\""#, "\"").replace(r"\\", r"\")
} else {
s.into()
}
}
/// Unescape chars with special meaning to the parser.
fn unescape(txt: &str) -> ParseResult<Cow<str>> {
fn unescape(txt: &str) -> ParseResult<String> {
if let Some(seq) = invalid_escape_sequence(txt) {
Err(parse_failure(txt, FailKind::UnknownEscape(seq)))
} else {
@ -631,6 +630,7 @@ fn unescape(txt: &str) -> ParseResult<Cow<str>> {
r"\-" => "-",
_ => unreachable!(),
})
.into()
} else {
txt.into()
})

View File

@ -134,7 +134,9 @@ impl SqlWriter<'_> {
SearchNode::EditedInDays(days) => self.write_edited(*days)?,
SearchNode::CardTemplate(template) => match template {
TemplateKind::Ordinal(_) => self.write_template(template)?,
TemplateKind::Name(name) => self.write_template(&TemplateKind::Name(norm(name)))?,
TemplateKind::Name(name) => {
self.write_template(&TemplateKind::Name(norm(name).into()))?
}
},
SearchNode::Deck(deck) => self.write_deck(&norm(deck))?,
SearchNode::NoteTypeID(ntid) => {
@ -532,7 +534,7 @@ impl RequiredTable {
}
}
impl Node<'_> {
impl Node {
fn required_table(&self) -> RequiredTable {
match self {
Node::And => RequiredTable::CardsOrNotes,
@ -546,7 +548,7 @@ impl Node<'_> {
}
}
impl SearchNode<'_> {
impl SearchNode {
fn required_table(&self) -> RequiredTable {
match self {
SearchNode::AddedInDays(_) => RequiredTable::Cards,

View File

@ -67,8 +67,8 @@ pub fn replace_search_term(search: &str, replacement: &str) -> Result<String> {
let mut nodes = parse(search)?;
let new = parse(replacement)?;
if let [Node::Search(search_node)] = &new[..] {
fn update_node_vec<'a>(old_nodes: &mut [Node<'a>], new_node: &SearchNode<'a>) {
fn update_node<'a>(old_node: &mut Node<'a>, new_node: &SearchNode<'a>) {
fn update_node_vec(old_nodes: &mut [Node], new_node: &SearchNode) {
fn update_node(old_node: &mut Node, new_node: &SearchNode) {
match old_node {
Node::Not(n) => update_node(n, new_node),
Node::Group(ns) => update_node_vec(ns, new_node),
@ -89,7 +89,7 @@ pub fn replace_search_term(search: &str, replacement: &str) -> Result<String> {
pub fn write_nodes<'a, I>(nodes: I) -> String
where
I: IntoIterator<Item = &'a Node<'a>>,
I: IntoIterator<Item = &'a Node>,
{
nodes.into_iter().map(|node| write_node(node)).collect()
}

View File

@ -336,11 +336,11 @@ pub(crate) fn to_text(txt: &str) -> Cow<str> {
}
/// Escape Anki wildcards and the backslash for escaping them: \*_
pub(crate) fn escape_anki_wildcards(txt: &str) -> Cow<str> {
pub(crate) fn escape_anki_wildcards(txt: &str) -> String {
lazy_static! {
static ref RE: Regex = Regex::new(r"[\\*_]").unwrap();
}
RE.replace_all(&txt, r"\$0")
RE.replace_all(&txt, r"\$0").into()
}
/// Compare text with a possible glob, folding case.