mirror of
https://github.com/ankitects/anki.git
synced 2025-11-06 20:57:13 -05:00
Support searching for custom data strings (#2634)
* Add extract_custom_data * Add tests for has-cd * Add `prop:cds` query
This commit is contained in:
parent
ff53625408
commit
d3d67c2083
4 changed files with 76 additions and 47 deletions
|
|
@ -105,6 +105,7 @@ pub enum PropertyKind {
|
|||
Position(u32),
|
||||
Rated(i32, RatingKind),
|
||||
CustomDataNumber { key: String, value: f32 },
|
||||
CustomDataString { key: String, value: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
|
|
@ -409,6 +410,7 @@ fn parse_prop(prop_clause: &str) -> ParseResult<SearchNode> {
|
|||
tag("rated"),
|
||||
tag("resched"),
|
||||
recognize(preceded(tag("cdn:"), alphanumeric1)),
|
||||
recognize(preceded(tag("cds:"), alphanumeric1)),
|
||||
))(prop_clause)
|
||||
.map_err(|_| {
|
||||
parse_failure(
|
||||
|
|
@ -448,15 +450,15 @@ fn parse_prop(prop_clause: &str) -> ParseResult<SearchNode> {
|
|||
"reps" => PropertyKind::Reps(parse_u32(num, prop_clause)?),
|
||||
"lapses" => PropertyKind::Lapses(parse_u32(num, prop_clause)?),
|
||||
"pos" => PropertyKind::Position(parse_u32(num, prop_clause)?),
|
||||
other => {
|
||||
let Some(prop) = other.strip_prefix("cdn:") else {
|
||||
unreachable!()
|
||||
};
|
||||
PropertyKind::CustomDataNumber {
|
||||
key: prop.into(),
|
||||
prop if prop.starts_with("cdn:") => PropertyKind::CustomDataNumber {
|
||||
key: prop.strip_prefix("cdn:").unwrap().into(),
|
||||
value: parse_f32(num, prop_clause)?,
|
||||
}
|
||||
}
|
||||
},
|
||||
prop if prop.starts_with("cds:") => PropertyKind::CustomDataString {
|
||||
key: prop.strip_prefix("cds:").unwrap().into(),
|
||||
value: num.into(),
|
||||
},
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
Ok(SearchNode::Property {
|
||||
|
|
@ -927,6 +929,27 @@ mod test {
|
|||
}
|
||||
})]
|
||||
);
|
||||
assert_eq!(
|
||||
parse("prop:cds:abc=foo")?,
|
||||
vec![Search(Property {
|
||||
operator: "=".into(),
|
||||
kind: PropertyKind::CustomDataString {
|
||||
key: "abc".into(),
|
||||
value: "foo".into()
|
||||
}
|
||||
})]
|
||||
);
|
||||
assert_eq!(
|
||||
parse("\"prop:cds:abc=foo bar\"")?,
|
||||
vec![Search(Property {
|
||||
operator: "=".into(),
|
||||
kind: PropertyKind::CustomDataString {
|
||||
key: "abc".into(),
|
||||
value: "foo bar".into()
|
||||
}
|
||||
})]
|
||||
);
|
||||
assert_eq!(parse("has-cd:r")?, vec![Search(CustomData("r".into()))]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1148,6 +1171,18 @@ mod test {
|
|||
provided: "cdn:=5".to_string(),
|
||||
},
|
||||
);
|
||||
assert_err_kind(
|
||||
"prop:cds=s",
|
||||
InvalidPropProperty {
|
||||
provided: "cds=s".to_string(),
|
||||
},
|
||||
);
|
||||
assert_err_kind(
|
||||
"prop:cds:=s",
|
||||
InvalidPropProperty {
|
||||
provided: "cds:=s".to_string(),
|
||||
},
|
||||
);
|
||||
|
||||
assert_err_kind(
|
||||
"prop:lapses",
|
||||
|
|
|
|||
|
|
@ -360,7 +360,14 @@ impl SqlWriter<'_> {
|
|||
PropertyKind::CustomDataNumber { key, value } => {
|
||||
write!(
|
||||
self.sql,
|
||||
"extract_custom_data_number(c.data, '{key}') {op} {value}"
|
||||
"cast(extract_custom_data(c.data, '{key}') as float) {op} {value}"
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
PropertyKind::CustomDataString { key, value } => {
|
||||
write!(
|
||||
self.sql,
|
||||
"extract_custom_data(c.data, '{key}') {op} '{value}'"
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
|
@ -370,7 +377,7 @@ impl SqlWriter<'_> {
|
|||
}
|
||||
|
||||
fn write_custom_data(&mut self, key: &str) -> Result<()> {
|
||||
write!(self.sql, "has_custom_data(c.data, '{key}')").unwrap();
|
||||
write!(self.sql, "extract_custom_data(c.data, '{key}') is not null").unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1187,7 +1194,11 @@ c.odue != 0 then c.odue else c.due end) != {days}) or (c.queue in (1,4) and
|
|||
assert_eq!(s(ctx, "prop:rated>-5:3").0, s(ctx, "rated:5:3").0);
|
||||
assert_eq!(
|
||||
&s(ctx, "prop:cdn:r=1").0,
|
||||
"(extract_custom_data_number(c.data, 'r') = 1)"
|
||||
"(cast(extract_custom_data(c.data, 'r') as float) = 1)"
|
||||
);
|
||||
assert_eq!(
|
||||
&s(ctx, "prop:cds:r=s").0,
|
||||
"(extract_custom_data(c.data, 'r') = 's')"
|
||||
);
|
||||
|
||||
// note types by name
|
||||
|
|
@ -1229,6 +1240,12 @@ c.odue != 0 then c.odue else c.due end) != {days}) or (c.queue in (1,4) and
|
|||
vec![r"(?i)\b.*fo.o.*\b".into()]
|
||||
)
|
||||
);
|
||||
|
||||
// has-cd
|
||||
assert_eq!(
|
||||
&s(ctx, "has-cd:r").0,
|
||||
"(extract_custom_data(c.data, 'r') is not null)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -173,6 +173,9 @@ fn write_property(operator: &str, kind: &PropertyKind) -> String {
|
|||
RatingKind::ManualReschedule => format!("prop:resched{}{}", operator, u),
|
||||
},
|
||||
CustomDataNumber { key, value } => format!("prop:cdn:{key}{operator}{value}"),
|
||||
CustomDataString { key, value } => {
|
||||
maybe_quote(&format!("prop:cds:{key}{operator}{value}",))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -69,8 +69,7 @@ fn open_or_create_collection_db(path: &Path) -> Result<Connection> {
|
|||
add_regexp_tags_function(&db)?;
|
||||
add_without_combining_function(&db)?;
|
||||
add_fnvhash_function(&db)?;
|
||||
add_extract_custom_data_number_function(&db)?;
|
||||
add_has_custom_data_function(&db)?;
|
||||
add_extract_custom_data_function(&db)?;
|
||||
|
||||
db.create_collation("unicase", unicase_compare)?;
|
||||
|
||||
|
|
@ -202,10 +201,10 @@ fn add_regexp_tags_function(db: &Connection) -> rusqlite::Result<()> {
|
|||
)
|
||||
}
|
||||
|
||||
/// eg. extract_custom_data_number(card.data, 'r') -> float | null
|
||||
fn add_extract_custom_data_number_function(db: &Connection) -> rusqlite::Result<()> {
|
||||
/// eg. extract_custom_data(card.data, 'r') -> string | null
|
||||
fn add_extract_custom_data_function(db: &Connection) -> rusqlite::Result<()> {
|
||||
db.create_scalar_function(
|
||||
"extract_custom_data_number",
|
||||
"extract_custom_data",
|
||||
2,
|
||||
FunctionFlags::SQLITE_DETERMINISTIC,
|
||||
move |ctx| {
|
||||
|
|
@ -224,36 +223,11 @@ fn add_extract_custom_data_number_function(db: &Connection) -> rusqlite::Result<
|
|||
let Ok(value) = serde_json::from_str::<Value>(custom_data) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let num = value.get(key).and_then(|v| v.as_f64());
|
||||
Ok(num)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// eg. has_custom_data(card.data, 'r') -> bool
|
||||
fn add_has_custom_data_function(db: &Connection) -> rusqlite::Result<()> {
|
||||
db.create_scalar_function(
|
||||
"has_custom_data",
|
||||
2,
|
||||
FunctionFlags::SQLITE_DETERMINISTIC,
|
||||
move |ctx| {
|
||||
assert_eq!(ctx.len(), 2, "called with unexpected number of arguments");
|
||||
|
||||
let Ok(card_data) = ctx.get_raw(0).as_str() else {
|
||||
return Ok(None);
|
||||
};
|
||||
if card_data.is_empty() {
|
||||
return Ok(Some(false));
|
||||
}
|
||||
let Ok(key) = ctx.get_raw(1).as_str() else {
|
||||
return Ok(Some(false));
|
||||
};
|
||||
let custom_data = &CardData::from_str(card_data).custom_data;
|
||||
let Ok(value) = serde_json::from_str::<Value>(custom_data) else {
|
||||
return Ok(Some(false));
|
||||
};
|
||||
|
||||
Ok(value.get(key).map(|_| true))
|
||||
let v = value.get(key).map(|v| match v {
|
||||
Value::String(s) => s.to_owned(),
|
||||
_ => v.to_string(),
|
||||
});
|
||||
Ok(v)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue