Add regex tag search (#1707)
This commit is contained in:
parent
4066518808
commit
5426164ebf
@ -134,7 +134,10 @@ impl SearchNode {
|
|||||||
|
|
||||||
/// Construct [SearchNode] from an unescaped tag name.
|
/// Construct [SearchNode] from an unescaped tag name.
|
||||||
pub fn from_tag_name(name: &str) -> Self {
|
pub fn from_tag_name(name: &str) -> Self {
|
||||||
Self::Tag(escape_anki_wildcards_for_search_node(name))
|
Self::Tag {
|
||||||
|
tag: escape_anki_wildcards_for_search_node(name),
|
||||||
|
is_re: false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Construct [SearchNode] from an unescaped notetype name.
|
/// Construct [SearchNode] from an unescaped notetype name.
|
||||||
|
@ -64,7 +64,10 @@ pub enum SearchNode {
|
|||||||
days: u32,
|
days: u32,
|
||||||
ease: RatingKind,
|
ease: RatingKind,
|
||||||
},
|
},
|
||||||
Tag(String),
|
Tag {
|
||||||
|
tag: String,
|
||||||
|
is_re: bool,
|
||||||
|
},
|
||||||
Duplicates {
|
Duplicates {
|
||||||
notetype_id: NotetypeId,
|
notetype_id: NotetypeId,
|
||||||
text: String,
|
text: String,
|
||||||
@ -311,7 +314,7 @@ fn search_node_for_text_with_argument<'a>(
|
|||||||
Ok(match key.to_ascii_lowercase().as_str() {
|
Ok(match key.to_ascii_lowercase().as_str() {
|
||||||
"deck" => SearchNode::Deck(unescape(val)?),
|
"deck" => SearchNode::Deck(unescape(val)?),
|
||||||
"note" => SearchNode::Notetype(unescape(val)?),
|
"note" => SearchNode::Notetype(unescape(val)?),
|
||||||
"tag" => SearchNode::Tag(unescape(val)?),
|
"tag" => parse_tag(val)?,
|
||||||
"card" => parse_template(val)?,
|
"card" => parse_template(val)?,
|
||||||
"flag" => parse_flag(val)?,
|
"flag" => parse_flag(val)?,
|
||||||
"resched" => parse_resched(val)?,
|
"resched" => parse_resched(val)?,
|
||||||
@ -334,6 +337,20 @@ fn search_node_for_text_with_argument<'a>(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_tag(s: &str) -> ParseResult<SearchNode> {
|
||||||
|
Ok(if let Some(re) = s.strip_prefix("re:") {
|
||||||
|
SearchNode::Tag {
|
||||||
|
tag: unescape_quotes(re),
|
||||||
|
is_re: true,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
SearchNode::Tag {
|
||||||
|
tag: unescape(s)?,
|
||||||
|
is_re: false,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_template(s: &str) -> ParseResult<SearchNode> {
|
fn parse_template(s: &str) -> ParseResult<SearchNode> {
|
||||||
Ok(SearchNode::CardTemplate(match s.parse::<u16>() {
|
Ok(SearchNode::CardTemplate(match s.parse::<u16>() {
|
||||||
Ok(n) => TemplateKind::Ordinal(n.max(1) - 1),
|
Ok(n) => TemplateKind::Ordinal(n.max(1) - 1),
|
||||||
@ -820,7 +837,20 @@ mod test {
|
|||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(parse("note:basic")?, vec![Search(Notetype("basic".into()))]);
|
assert_eq!(parse("note:basic")?, vec![Search(Notetype("basic".into()))]);
|
||||||
assert_eq!(parse("tag:hard")?, vec![Search(Tag("hard".into()))]);
|
assert_eq!(
|
||||||
|
parse("tag:hard")?,
|
||||||
|
vec![Search(Tag {
|
||||||
|
tag: "hard".into(),
|
||||||
|
is_re: false
|
||||||
|
})]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse(r"tag:re:\\")?,
|
||||||
|
vec![Search(Tag {
|
||||||
|
tag: r"\\".into(),
|
||||||
|
is_re: true
|
||||||
|
})]
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse("nid:1237123712,2,3")?,
|
parse("nid:1237123712,2,3")?,
|
||||||
vec![Search(NoteIds("1237123712,2,3".into()))]
|
vec![Search(NoteIds("1237123712,2,3".into()))]
|
||||||
|
@ -156,7 +156,7 @@ impl SqlWriter<'_> {
|
|||||||
SearchNode::Notetype(notetype) => self.write_notetype(&norm(notetype)),
|
SearchNode::Notetype(notetype) => self.write_notetype(&norm(notetype)),
|
||||||
SearchNode::Rated { days, ease } => self.write_rated(">", -i64::from(*days), ease)?,
|
SearchNode::Rated { days, ease } => self.write_rated(">", -i64::from(*days), ease)?,
|
||||||
|
|
||||||
SearchNode::Tag(tag) => self.write_tag(&norm(tag)),
|
SearchNode::Tag { tag, is_re } => self.write_tag(&norm(tag), *is_re),
|
||||||
SearchNode::State(state) => self.write_state(state)?,
|
SearchNode::State(state) => self.write_state(state)?,
|
||||||
SearchNode::Flag(flag) => {
|
SearchNode::Flag(flag) => {
|
||||||
write!(self.sql, "(c.flags & 7) == {}", flag).unwrap();
|
write!(self.sql, "(c.flags & 7) == {}", flag).unwrap();
|
||||||
@ -199,17 +199,19 @@ impl SqlWriter<'_> {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_tag(&mut self, text: &str) {
|
fn write_tag(&mut self, tag: &str, is_re: bool) {
|
||||||
if text.contains(' ') {
|
if is_re {
|
||||||
write!(self.sql, "false").unwrap();
|
self.args.push(format!("(?i){tag}"));
|
||||||
|
write!(self.sql, "regexp_tags(?{}, n.tags)", self.args.len()).unwrap();
|
||||||
} else {
|
} else {
|
||||||
match text {
|
match tag {
|
||||||
"none" => {
|
"none" => {
|
||||||
write!(self.sql, "n.tags = ''").unwrap();
|
write!(self.sql, "n.tags = ''").unwrap();
|
||||||
}
|
}
|
||||||
"*" => {
|
"*" => {
|
||||||
write!(self.sql, "true").unwrap();
|
write!(self.sql, "true").unwrap();
|
||||||
}
|
}
|
||||||
|
s if s.contains(' ') => write!(self.sql, "false").unwrap(),
|
||||||
text => {
|
text => {
|
||||||
write!(self.sql, "n.tags regexp ?").unwrap();
|
write!(self.sql, "n.tags regexp ?").unwrap();
|
||||||
let re = &to_custom_re(text, r"\S");
|
let re = &to_custom_re(text, r"\S");
|
||||||
@ -660,7 +662,7 @@ impl SearchNode {
|
|||||||
|
|
||||||
SearchNode::UnqualifiedText(_) => RequiredTable::Notes,
|
SearchNode::UnqualifiedText(_) => RequiredTable::Notes,
|
||||||
SearchNode::SingleField { .. } => RequiredTable::Notes,
|
SearchNode::SingleField { .. } => RequiredTable::Notes,
|
||||||
SearchNode::Tag(_) => RequiredTable::Notes,
|
SearchNode::Tag { .. } => RequiredTable::Notes,
|
||||||
SearchNode::Duplicates { .. } => RequiredTable::Notes,
|
SearchNode::Duplicates { .. } => RequiredTable::Notes,
|
||||||
SearchNode::Regex(_) => RequiredTable::Notes,
|
SearchNode::Regex(_) => RequiredTable::Notes,
|
||||||
SearchNode::NoCombining(_) => RequiredTable::Notes,
|
SearchNode::NoCombining(_) => RequiredTable::Notes,
|
||||||
@ -848,6 +850,13 @@ mod test {
|
|||||||
);
|
);
|
||||||
assert_eq!(s(ctx, "tag:none"), ("(n.tags = '')".into(), vec![]));
|
assert_eq!(s(ctx, "tag:none"), ("(n.tags = '')".into(), vec![]));
|
||||||
assert_eq!(s(ctx, "tag:*"), ("(true)".into(), vec![]));
|
assert_eq!(s(ctx, "tag:*"), ("(true)".into(), vec![]));
|
||||||
|
assert_eq!(
|
||||||
|
s(ctx, "tag:re:.ne|tw."),
|
||||||
|
(
|
||||||
|
"(regexp_tags(?1, n.tags))".into(),
|
||||||
|
vec!["(?i).ne|tw.".into()]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
// state
|
// state
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -70,7 +70,7 @@ fn write_search_node(node: &SearchNode) -> String {
|
|||||||
NotetypeId(NotetypeIdType(i)) => format!("mid:{}", i),
|
NotetypeId(NotetypeIdType(i)) => format!("mid:{}", i),
|
||||||
Notetype(s) => maybe_quote(&format!("note:{}", s)),
|
Notetype(s) => maybe_quote(&format!("note:{}", s)),
|
||||||
Rated { days, ease } => write_rated(days, ease),
|
Rated { days, ease } => write_rated(days, ease),
|
||||||
Tag(s) => maybe_quote(&format!("tag:{}", s)),
|
Tag { tag, is_re } => write_single_field("tag", tag, *is_re),
|
||||||
Duplicates { notetype_id, text } => write_dupe(notetype_id, text),
|
Duplicates { notetype_id, text } => write_dupe(notetype_id, text),
|
||||||
State(k) => write_state(k),
|
State(k) => write_state(k),
|
||||||
Flag(u) => format!("flag:{}", u),
|
Flag(u) => format!("flag:{}", u),
|
||||||
@ -102,6 +102,7 @@ fn needs_quotation(txt: &str) -> bool {
|
|||||||
RE.is_match(txt)
|
RE.is_match(txt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Also used by tag search, which has the same syntax.
|
||||||
fn write_single_field(field: &str, text: &str, is_re: bool) -> String {
|
fn write_single_field(field: &str, text: &str, is_re: bool) -> String {
|
||||||
let re = if is_re { "re:" } else { "" };
|
let re = if is_re { "re:" } else { "" };
|
||||||
let text = if !is_re && text.starts_with("re:") {
|
let text = if !is_re && text.starts_with("re:") {
|
||||||
|
@ -52,6 +52,7 @@ fn open_or_create_collection_db(path: &Path) -> Result<Connection> {
|
|||||||
add_field_index_function(&db)?;
|
add_field_index_function(&db)?;
|
||||||
add_regexp_function(&db)?;
|
add_regexp_function(&db)?;
|
||||||
add_regexp_fields_function(&db)?;
|
add_regexp_fields_function(&db)?;
|
||||||
|
add_regexp_tags_function(&db)?;
|
||||||
add_without_combining_function(&db)?;
|
add_without_combining_function(&db)?;
|
||||||
add_fnvhash_function(&db)?;
|
add_fnvhash_function(&db)?;
|
||||||
|
|
||||||
@ -157,6 +158,26 @@ fn add_regexp_fields_function(db: &Connection) -> rusqlite::Result<()> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Adds sql function `regexp_tags(regex, tags) -> is_match`.
|
||||||
|
fn add_regexp_tags_function(db: &Connection) -> rusqlite::Result<()> {
|
||||||
|
db.create_scalar_function(
|
||||||
|
"regexp_tags",
|
||||||
|
2,
|
||||||
|
FunctionFlags::SQLITE_DETERMINISTIC,
|
||||||
|
move |ctx| {
|
||||||
|
assert_eq!(ctx.len(), 2, "called with unexpected number of arguments");
|
||||||
|
|
||||||
|
let re: Arc<Regex> = ctx
|
||||||
|
.get_or_create_aux(0, |vr| -> std::result::Result<_, BoxError> {
|
||||||
|
Ok(Regex::new(vr.as_str()?)?)
|
||||||
|
})?;
|
||||||
|
let mut tags = ctx.get_raw(1).as_str()?.split(' ');
|
||||||
|
|
||||||
|
Ok(tags.any(|tag| re.is_match(tag)))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Fetch schema version from database.
|
/// Fetch schema version from database.
|
||||||
/// Return (must_create, version)
|
/// Return (must_create, version)
|
||||||
fn schema_version(db: &Connection) -> Result<(bool, u8)> {
|
fn schema_version(db: &Connection) -> Result<(bool, u8)> {
|
||||||
|
Loading…
Reference in New Issue
Block a user