query lang - addresses are denoted by @, values need not match db deserialization format

e.g.` (matches @address ? ?)`, and `(matches ? ? "foo")` instead of "Sfoo"
feat/vaults
Tomáš Mládek 2022-03-30 10:20:27 +02:00
parent fd72034571
commit 8c60a617a2
No known key found for this signature in database
GPG Key ID: 65E225C8B3E2ED8A
9 changed files with 233 additions and 193 deletions

View File

@ -97,7 +97,7 @@ pub fn list_roots(connection: &UpEndConnection) -> Result<Vec<Address>> {
let all_directories: Vec<Entry> = let all_directories: Vec<Entry> =
connection.query(Query::SingleQuery(QueryPart::Matches(EntryQuery { connection.query(Query::SingleQuery(QueryPart::Matches(EntryQuery {
entity: QueryComponent::Any, entity: QueryComponent::Any,
attribute: QueryComponent::Exact(IS_OF_TYPE_ATTR.to_string()), attribute: QueryComponent::Exact(IS_OF_TYPE_ATTR.into()),
value: QueryComponent::Exact(HIER_ADDR.clone().into()), value: QueryComponent::Exact(HIER_ADDR.clone().into()),
})))?; })))?;
@ -105,7 +105,7 @@ pub fn list_roots(connection: &UpEndConnection) -> Result<Vec<Address>> {
let directories_with_parents: Vec<Address> = connection let directories_with_parents: Vec<Address> = connection
.query(Query::SingleQuery(QueryPart::Matches(EntryQuery { .query(Query::SingleQuery(QueryPart::Matches(EntryQuery {
entity: QueryComponent::Any, entity: QueryComponent::Any,
attribute: QueryComponent::Exact(HIER_HAS_ATTR.to_string()), attribute: QueryComponent::Exact(HIER_HAS_ATTR.into()),
value: QueryComponent::Any, value: QueryComponent::Any,
})))? })))?
.extract_pointers() .extract_pointers()
@ -143,7 +143,7 @@ pub fn fetch_or_create_dir(
let matching_directories = connection let matching_directories = connection
.query(Query::SingleQuery(QueryPart::Matches(EntryQuery { .query(Query::SingleQuery(QueryPart::Matches(EntryQuery {
entity: QueryComponent::Any, entity: QueryComponent::Any,
attribute: QueryComponent::Exact(String::from(LABEL_ATTR)), attribute: QueryComponent::Exact(LABEL_ATTR.into()),
value: QueryComponent::Exact(directory.as_ref().clone().into()), value: QueryComponent::Exact(directory.as_ref().clone().into()),
})))? })))?
.into_iter() .into_iter()
@ -153,7 +153,7 @@ pub fn fetch_or_create_dir(
Some(parent) => connection Some(parent) => connection
.query(Query::SingleQuery(QueryPart::Matches(EntryQuery { .query(Query::SingleQuery(QueryPart::Matches(EntryQuery {
entity: QueryComponent::Exact(parent), entity: QueryComponent::Exact(parent),
attribute: QueryComponent::Exact(String::from(HIER_HAS_ATTR)), attribute: QueryComponent::Exact(HIER_HAS_ATTR.into()),
value: QueryComponent::Any, value: QueryComponent::Any,
})))? })))?
.extract_pointers() .extract_pointers()

View File

@ -1,7 +1,6 @@
use crate::addressing::Address; use crate::addressing::Address;
use crate::database::entry::EntryValue; use crate::database::entry::EntryValue;
use crate::database::inner::schema::data; use crate::database::inner::schema::data;
use anyhow::{anyhow, Result};
use diesel::expression::operators::{And, Not, Or}; use diesel::expression::operators::{And, Not, Or};
use diesel::sql_types::Bool; use diesel::sql_types::Bool;
use diesel::sqlite::Sqlite; use diesel::sqlite::Sqlite;
@ -11,10 +10,19 @@ use std::borrow::Borrow;
use std::convert::TryFrom; use std::convert::TryFrom;
use std::str::FromStr; use std::str::FromStr;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Attribute(String);
impl From<&str> for Attribute {
fn from(str: &str) -> Self {
Self(str.to_string())
}
}
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub enum QueryComponent<T> pub enum QueryComponent<T>
where where
T: FromStr, T: TryFrom<lexpr::Value>,
{ {
Exact(T), Exact(T),
In(Vec<T>), In(Vec<T>),
@ -25,10 +33,66 @@ where
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub struct EntryQuery { pub struct EntryQuery {
pub entity: QueryComponent<Address>, pub entity: QueryComponent<Address>,
pub attribute: QueryComponent<String>, pub attribute: QueryComponent<Attribute>,
pub value: QueryComponent<EntryValue>, pub value: QueryComponent<EntryValue>,
} }
impl TryFrom<lexpr::Value> for Address {
type Error = QueryParseError;
fn try_from(value: lexpr::Value) -> Result<Self, Self::Error> {
match value {
lexpr::Value::Symbol(str) => {
if let Some(address_str) = str.strip_prefix('@') {
address_str
.parse()
.map_err(|e: anyhow::Error| QueryParseError(e.to_string()))
} else {
Err(QueryParseError(
"Incorrect address format (use @address).".into(),
))
}
}
_ => Err(QueryParseError(
"Incorrect type for address (use @address).".into(),
)),
}
}
}
impl TryFrom<lexpr::Value> for Attribute {
type Error = QueryParseError;
fn try_from(value: lexpr::Value) -> Result<Self, Self::Error> {
match value {
lexpr::Value::String(str) => Ok(Attribute(str.to_string())),
_ => Err(QueryParseError(
"Can only convert to attribute from string.".into(),
)),
}
}
}
impl TryFrom<lexpr::Value> for EntryValue {
type Error = QueryParseError;
fn try_from(value: lexpr::Value) -> Result<Self, Self::Error> {
match value {
lexpr::Value::Number(num) => {
Ok(EntryValue::Number(num.as_f64().ok_or_else(|| {
QueryParseError(format!("Error processing number ({num:?})."))
})?))
}
lexpr::Value::Char(chr) => Ok(EntryValue::String(chr.to_string())),
lexpr::Value::String(str) => Ok(EntryValue::String(str.to_string())),
lexpr::Value::Symbol(_) => Ok(EntryValue::Address(Address::try_from(value.clone())?)),
_ => Err(QueryParseError(
"Value can only be a string, number or address.".into(),
)),
}
}
}
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub enum QueryPart { pub enum QueryPart {
Matches(EntryQuery), Matches(EntryQuery),
@ -54,15 +118,28 @@ pub enum Query {
MultiQuery(MultiQuery), MultiQuery(MultiQuery),
} }
#[derive(Debug, Clone)]
pub struct QueryParseError(String);
impl std::fmt::Display for QueryParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::error::Error for QueryParseError {}
type Predicate = dyn BoxableExpression<data::table, Sqlite, SqlType = Bool>; type Predicate = dyn BoxableExpression<data::table, Sqlite, SqlType = Bool>;
impl TryFrom<&lexpr::Value> for Query { impl TryFrom<&lexpr::Value> for Query {
type Error = anyhow::Error; type Error = QueryParseError;
fn try_from(expression: &lexpr::Value) -> Result<Self> { fn try_from(expression: &lexpr::Value) -> Result<Self, Self::Error> {
fn parse_component<T: FromStr>(value: &lexpr::Value) -> Result<QueryComponent<T>> fn parse_component<T: TryFrom<lexpr::Value>>(
value: &lexpr::Value,
) -> Result<QueryComponent<T>, QueryParseError>
where where
<T as FromStr>::Err: std::fmt::Debug, QueryParseError: From<<T as TryFrom<lexpr::Value>>::Error>,
{ {
match value { match value {
lexpr::Value::Cons(cons) => { lexpr::Value::Cons(cons) => {
@ -72,21 +149,15 @@ impl TryFrom<&lexpr::Value> for Query {
let (cons_vec, _) = cons.clone().into_vec(); let (cons_vec, _) = cons.clone().into_vec();
if let Some(split) = cons_vec.split_first() { if let Some(split) = cons_vec.split_first() {
let args = split.1; let args = split.1;
let values: Result<Vec<T>, _> = args.iter().map(|value| { let values: Result<Vec<T>, _> = args
if let lexpr::Value::String(str) = value { .iter()
match T::from_str(str.borrow()) { .map(|value| T::try_from(value.clone()))
Ok(value) => Ok(value), .collect();
Err(error) => Err(anyhow!(format!("Malformed expression: Conversion of inner value '{}' from string failed: {:#?}",str, error))),
}
} else {
Err(anyhow!("Malformed expression: Inner value list must be comprised of strings."))
}
}).collect();
Ok(QueryComponent::In(values?)) Ok(QueryComponent::In(values?))
} else { } else {
Err(anyhow!( Err(QueryParseError(
"Malformed expression: Inner value cannot be empty." "Malformed expression: Inner value cannot be empty.".into(),
)) ))
} }
} }
@ -98,43 +169,28 @@ impl TryFrom<&lexpr::Value> for Query {
if let lexpr::Value::String(str) = value { if let lexpr::Value::String(str) = value {
Ok(QueryComponent::Contains(str.into_string())) Ok(QueryComponent::Contains(str.into_string()))
} else { } else {
Err(anyhow!("Malformed expression: 'Contains' argument must be a string.")) Err(QueryParseError("Malformed expression: 'Contains' argument must be a string.".into()))
} }
} }
_ => Err(anyhow!( _ => Err(QueryParseError(
"Malformed expression: 'Contains' requires a single argument." "Malformed expression: 'Contains' requires a single argument.".into()
)), )),
} }
} }
_ => Err(anyhow!(format!( _ => Err(QueryParseError(format!(
"Malformed expression: Unknown symbol {}", "Malformed expression: Unknowne symbol {}",
symbol symbol
))), ))),
} }
} else { } else {
Err(anyhow!(format!( Err(QueryParseError(format!(
"Malformed expression: Inner value '{:?}' is not a symbol.", "Malformed expression: Inner value '{:?}' is not a symbol.",
value value
))) )))
} }
} }
lexpr::Value::String(str) => match T::from_str(str.borrow()) { lexpr::Value::Symbol(symbol) if symbol.as_ref() == "?" => Ok(QueryComponent::Any),
Ok(value) => Ok(QueryComponent::Exact(value)), _ => Ok(QueryComponent::Exact(T::try_from(value.clone())?)),
Err(error) => Err(anyhow!(format!(
"Malformed expression: Conversion of inner value '{}' from string failed: {:#?}",
str, error
))),
},
lexpr::Value::Symbol(symbol) => match symbol.borrow() {
"?" => Ok(QueryComponent::Any),
_ => Err(anyhow!(format!(
"Malformed expression: Unknown symbol {}",
symbol
))),
},
_ => Err(anyhow!(
"Malformed expression: Inner value not a string, list or '?'."
)),
} }
} }
@ -145,7 +201,7 @@ impl TryFrom<&lexpr::Value> for Query {
let (cons_vec, _) = value.clone().into_vec(); let (cons_vec, _) = value.clone().into_vec();
if let [_, entity, attribute, value] = &cons_vec[..] { if let [_, entity, attribute, value] = &cons_vec[..] {
let entity = parse_component::<Address>(entity)?; let entity = parse_component::<Address>(entity)?;
let attribute = parse_component::<String>(attribute)?; let attribute = parse_component::<Attribute>(attribute)?;
let value = parse_component::<EntryValue>(value)?; let value = parse_component::<EntryValue>(value)?;
Ok(Query::SingleQuery(QueryPart::Matches(EntryQuery { Ok(Query::SingleQuery(QueryPart::Matches(EntryQuery {
entity, entity,
@ -153,8 +209,9 @@ impl TryFrom<&lexpr::Value> for Query {
value, value,
}))) })))
} else { } else {
Err(anyhow!( Err(QueryParseError(
"Malformed expression: Wrong number of arguments to 'matches'." "Malformed expression: Wrong number of arguments to 'matches'."
.into(),
)) ))
} }
} }
@ -166,13 +223,14 @@ impl TryFrom<&lexpr::Value> for Query {
type_name_str.to_string(), type_name_str.to_string(),
))) )))
} else { } else {
Err(anyhow!( Err(QueryParseError(
"Malformed expression: Type must be specified as a string." "Malformed expression: Type must be specified as a string."
.into(),
)) ))
} }
} else { } else {
Err(anyhow!( Err(QueryParseError(
"Malformed expression: Wrong number of arguments to 'type'." "Malformed expression: Wrong number of arguments to 'type'.".into(),
)) ))
} }
} }
@ -182,7 +240,7 @@ impl TryFrom<&lexpr::Value> for Query {
let values = sub_expressions let values = sub_expressions
.iter() .iter()
.map(|value| Ok(Box::new(Query::try_from(value)?))) .map(|value| Ok(Box::new(Query::try_from(value)?)))
.collect::<Result<Vec<Box<Query>>>>()?; .collect::<Result<Vec<Box<Query>>, QueryParseError>>()?;
if let Some(queries) = NonEmpty::from_vec(values) { if let Some(queries) = NonEmpty::from_vec(values) {
Ok(Query::MultiQuery(MultiQuery { Ok(Query::MultiQuery(MultiQuery {
@ -193,8 +251,8 @@ impl TryFrom<&lexpr::Value> for Query {
queries, queries,
})) }))
} else { } else {
Err(anyhow!( Err(QueryParseError(
"Malformed expression: sub-query list cannot be empty.", "Malformed expression: sub-query list cannot be empty.".into(),
)) ))
} }
} }
@ -204,7 +262,7 @@ impl TryFrom<&lexpr::Value> for Query {
let values = sub_expressions let values = sub_expressions
.iter() .iter()
.map(|value| Ok(Box::new(Query::try_from(value)?))) .map(|value| Ok(Box::new(Query::try_from(value)?)))
.collect::<Result<Vec<Box<Query>>>>()?; .collect::<Result<Vec<Box<Query>>, QueryParseError>>()?;
if values.len() == 1 { if values.len() == 1 {
Ok(Query::MultiQuery(MultiQuery { Ok(Query::MultiQuery(MultiQuery {
@ -212,53 +270,58 @@ impl TryFrom<&lexpr::Value> for Query {
queries: NonEmpty::from_vec(values).unwrap(), queries: NonEmpty::from_vec(values).unwrap(),
})) }))
} else { } else {
Err(anyhow!( Err(QueryParseError(
"Malformed expression: NOT takes exactly one parameter." "Malformed expression: NOT takes exactly one parameter.".into(),
)) ))
} }
} }
_ => Err(anyhow!(format!( _ => Err(QueryParseError(format!(
"Malformed expression: Unknown symbol '{}'.", "Malformed expression: Unknown symbol '{}'.",
symbol symbol
))), ))),
} }
} else { } else {
Err(anyhow!(format!( Err(QueryParseError(format!(
"Malformed expression: Value '{:?}' is not a symbol.", "Malformed expression: Value '{:?}' is not a symbol.",
value value
))) )))
} }
} else { } else {
Err(anyhow!("Malformed expression: Not a list.")) Err(QueryParseError("Malformed expression: Not a list.".into()))
} }
} }
} }
impl FromStr for Query { impl FromStr for Query {
type Err = anyhow::Error; type Err = QueryParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
let sexp = lexpr::from_str_custom(s, lexpr::parse::Options::new())?; let sexp = lexpr::from_str_custom(s, lexpr::parse::Options::new())
.map_err(|_| QueryParseError("???".into()))?;
Query::try_from(&sexp) Query::try_from(&sexp)
} }
} }
impl Query { impl Query {
pub(crate) fn to_sqlite_predicates(&self) -> Result<Box<Predicate>> { pub(crate) fn to_sqlite_predicates(&self) -> Result<Box<Predicate>, QueryParseError> {
match self { match self {
Query::SingleQuery(qp) => { Query::SingleQuery(qp) => match qp {
match qp {
QueryPart::Matches(eq) => { QueryPart::Matches(eq) => {
let mut subqueries: Vec<Box<Predicate>> = vec![]; let mut subqueries: Vec<Box<Predicate>> = vec![];
match &eq.entity { match &eq.entity {
QueryComponent::Exact(q_entity) => { QueryComponent::Exact(q_entity) => subqueries.push(Box::new(
subqueries.push(Box::new(data::entity.eq(q_entity.encode()?))) data::entity.eq(q_entity
} .encode()
.map_err(|_| QueryParseError("???".into()))?),
)),
QueryComponent::In(q_entities) => { QueryComponent::In(q_entities) => {
let entities: Result<Vec<_>, _> = let entities: Result<Vec<_>, _> =
q_entities.iter().map(|t| t.encode()).collect(); q_entities.iter().map(|t| t.encode()).collect();
subqueries.push(Box::new(data::entity.eq_any(entities?))) subqueries.push(Box::new(
data::entity
.eq_any(entities.map_err(|_| QueryParseError("???".into()))?),
))
} }
QueryComponent::Contains(q_entity) => subqueries.push(Box::new( QueryComponent::Contains(q_entity) => subqueries.push(Box::new(
data::entity_searchable.like(format!("%{}%", q_entity)), data::entity_searchable.like(format!("%{}%", q_entity)),
@ -268,10 +331,11 @@ impl Query {
match &eq.attribute { match &eq.attribute {
QueryComponent::Exact(q_attribute) => { QueryComponent::Exact(q_attribute) => {
subqueries.push(Box::new(data::attribute.eq(q_attribute.clone()))) subqueries.push(Box::new(data::attribute.eq(q_attribute.0.clone())))
} }
QueryComponent::In(q_attributes) => subqueries QueryComponent::In(q_attributes) => subqueries.push(Box::new(
.push(Box::new(data::attribute.eq_any(q_attributes.clone()))), data::attribute.eq_any(q_attributes.iter().map(|a| &a.0).cloned()),
)),
QueryComponent::Contains(q_attribute) => subqueries QueryComponent::Contains(q_attribute) => subqueries
.push(Box::new(data::attribute.like(format!("%{}%", q_attribute)))), .push(Box::new(data::attribute.like(format!("%{}%", q_attribute)))),
QueryComponent::Any => {} QueryComponent::Any => {}
@ -282,13 +346,18 @@ impl Query {
EntryValue::Number(n) => { EntryValue::Number(n) => {
subqueries.push(Box::new(data::value_num.eq(*n))) subqueries.push(Box::new(data::value_num.eq(*n)))
} }
_ => subqueries _ => subqueries.push(Box::new(
.push(Box::new(data::value_str.eq(q_value.to_string()?))), data::value_str.eq(q_value
.to_string()
.map_err(|_| QueryParseError("???".into()))?),
)),
}, },
QueryComponent::In(q_values) => { QueryComponent::In(q_values) => {
let first = q_values.first().ok_or(anyhow!( let first = q_values.first().ok_or_else(|| {
"Malformed expression: Inner value cannot be empty." QueryParseError(
))?; "Malformed expression: Inner value cannot be empty.".into(),
)
})?;
match first { match first {
EntryValue::Number(_) => subqueries.push(Box::new( EntryValue::Number(_) => subqueries.push(Box::new(
@ -299,10 +368,10 @@ impl Query {
if let EntryValue::Number(n) = v { if let EntryValue::Number(n) = v {
Ok(*n) Ok(*n)
} else { } else {
Err(anyhow!("IN queries must not combine numeric and string values! ({v} is not a number)")) Err(QueryParseError(format!("IN queries must not combine numeric and string values! ({v} is not a number)")))
} }
}) })
.collect::<Result<Vec<f64>>>()?, .collect::<Result<Vec<f64>, QueryParseError>>()?,
), ),
)), )),
_ => subqueries.push(Box::new( _ => subqueries.push(Box::new(
@ -311,12 +380,12 @@ impl Query {
.iter() .iter()
.map(|v| { .map(|v| {
if let EntryValue::Number(_) = v { if let EntryValue::Number(_) = v {
Err(anyhow!("IN queries must not combine numeric and string values! (Found {v})")) Err(QueryParseError(format!("IN queries must not combine numeric and string values! (Found {v})")))
} else { } else {
v.to_string() v.to_string().map_err(|_| QueryParseError("???".into()))
} }
}) })
.collect::<Result<Vec<String>>>()?, .collect::<Result<Vec<String>, QueryParseError>>()?,
), ),
)), )),
} }
@ -340,10 +409,9 @@ impl Query {
} }
} }
QueryPart::Type(_) => unimplemented!("Type queries are not yet implemented."), QueryPart::Type(_) => unimplemented!("Type queries are not yet implemented."),
} },
}
Query::MultiQuery(mq) => { Query::MultiQuery(mq) => {
let subqueries: Result<Vec<Box<Predicate>>> = mq let subqueries: Result<Vec<Box<Predicate>>, QueryParseError> = mq
.queries .queries
.iter() .iter()
.map(|sq| sq.to_sqlite_predicates()) .map(|sq| sq.to_sqlite_predicates())
@ -375,7 +443,9 @@ impl Query {
} }
Ok(Box::new(result)) Ok(Box::new(result))
} }
QueryQualifier::Not => Err(anyhow!("NOT only takes one subquery.")), QueryQualifier::Not => {
Err(QueryParseError("NOT only takes one subquery.".into()))
}
}, },
} }
} }
@ -386,7 +456,7 @@ impl Query {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use anyhow::Result; use anyhow::{anyhow, Result};
#[test] #[test]
fn test_matches() -> Result<()> { fn test_matches() -> Result<()> {
@ -402,7 +472,7 @@ mod test {
query.to_sqlite_predicates()?; query.to_sqlite_predicates()?;
let address = Address::Url(String::from("https://upendproject.net")); let address = Address::Url(String::from("https://upendproject.net"));
let query = format!("(matches \"{address}\" ? ?)").parse::<Query>()?; let query = format!("(matches @{address} ? ?)").parse::<Query>()?;
assert_eq!( assert_eq!(
query, query,
Query::SingleQuery(QueryPart::Matches(EntryQuery { Query::SingleQuery(QueryPart::Matches(EntryQuery {
@ -413,19 +483,19 @@ mod test {
); );
query.to_sqlite_predicates()?; query.to_sqlite_predicates()?;
let query = "(matches ? \"FOO\" ?)".parse::<Query>()?; let query = r#"(matches ? "FOO" ?)"#.parse::<Query>()?;
assert_eq!( assert_eq!(
query, query,
Query::SingleQuery(QueryPart::Matches(EntryQuery { Query::SingleQuery(QueryPart::Matches(EntryQuery {
entity: QueryComponent::Any, entity: QueryComponent::Any,
attribute: QueryComponent::Exact(String::from("FOO")), attribute: QueryComponent::Exact("FOO".into()),
value: QueryComponent::Any value: QueryComponent::Any
})) }))
); );
query.to_sqlite_predicates()?; query.to_sqlite_predicates()?;
let value = EntryValue::Number(1337.93); let value = EntryValue::Number(1337.93);
let query = format!("(matches ? ? \"{}\")", value.to_string().unwrap()).parse::<Query>()?; let query = "(matches ? ? 1337.93)".parse::<Query>()?;
assert_eq!( assert_eq!(
query, query,
Query::SingleQuery(QueryPart::Matches(EntryQuery { Query::SingleQuery(QueryPart::Matches(EntryQuery {
@ -441,27 +511,19 @@ mod test {
#[test] #[test]
fn test_in_parse() -> Result<()> { fn test_in_parse() -> Result<()> {
let query = "(matches ? (in \"FOO\" \"BAR\") ?)".parse::<Query>()?; let query = r#"(matches ? (in "FOO" "BAR") ?)"#.parse::<Query>()?;
assert_eq!( assert_eq!(
query, query,
Query::SingleQuery(QueryPart::Matches(EntryQuery { Query::SingleQuery(QueryPart::Matches(EntryQuery {
entity: QueryComponent::Any, entity: QueryComponent::Any,
attribute: QueryComponent::In(vec!("FOO".to_string(), "BAR".to_string())), attribute: QueryComponent::In(vec!("FOO".into(), "BAR".into())),
value: QueryComponent::Any value: QueryComponent::Any
})) }))
); );
query.to_sqlite_predicates()?; query.to_sqlite_predicates()?;
let values: Vec<EntryValue> = vec!["FOO".into(), "BAR".into()]; let values: Vec<EntryValue> = vec!["FOO".into(), "BAR".into()];
let query = format!( let query = r#"(matches ? ? (in "FOO" "BAR"))"#.parse::<Query>()?;
"(matches ? ? (in {}))",
values
.iter()
.map(|v| format!("\"{}\"", v.to_string().unwrap()))
.collect::<Vec<String>>()
.join(" ")
)
.parse::<Query>()?;
assert_eq!( assert_eq!(
query, query,
Query::SingleQuery(QueryPart::Matches(EntryQuery { Query::SingleQuery(QueryPart::Matches(EntryQuery {
@ -473,15 +535,7 @@ mod test {
query.to_sqlite_predicates()?; query.to_sqlite_predicates()?;
let values = vec![EntryValue::Number(1337.93), EntryValue::Number(1968.12)]; let values = vec![EntryValue::Number(1337.93), EntryValue::Number(1968.12)];
let query = format!( let query = r#"(matches ? ? (in 1337.93 1968.12))"#.parse::<Query>()?;
"(matches ? ? (in {}))",
values
.iter()
.map(|v| format!("\"{}\"", v.to_string().unwrap()))
.collect::<Vec<String>>()
.join(" ")
)
.parse::<Query>()?;
assert_eq!( assert_eq!(
query, query,
Query::SingleQuery(QueryPart::Matches(EntryQuery { Query::SingleQuery(QueryPart::Matches(EntryQuery {
@ -494,15 +548,8 @@ mod test {
// Invalid queries // Invalid queries
let values: Vec<EntryValue> = vec!["FOO".into(), EntryValue::Number(1337.93)]; let values: Vec<EntryValue> = vec!["FOO".into(), EntryValue::Number(1337.93)];
let query = format!( let query = r#"(matches ? ? (in "FOO" 1337.93))"#.parse::<Query>()?;
"(matches ? ? (in {}))",
values
.iter()
.map(|v| format!("\"{}\"", v.to_string().unwrap()))
.collect::<Vec<String>>()
.join(" ")
)
.parse::<Query>()?;
assert_eq!( assert_eq!(
query, query,
Query::SingleQuery(QueryPart::Matches(EntryQuery { Query::SingleQuery(QueryPart::Matches(EntryQuery {
@ -517,15 +564,8 @@ mod test {
.ok_or(anyhow!("Failed to reject mixed query."))?; .ok_or(anyhow!("Failed to reject mixed query."))?;
let values = vec![EntryValue::Number(1337.93), "FOO".into()]; let values = vec![EntryValue::Number(1337.93), "FOO".into()];
let query = format!( let query = r#"(matches ? ? (in 1337.93 "FOO"))"#.parse::<Query>()?;
"(matches ? ? (in {}))",
values
.iter()
.map(|v| format!("\"{}\"", v.to_string().unwrap()))
.collect::<Vec<String>>()
.join(" ")
)
.parse::<Query>()?;
assert_eq!( assert_eq!(
query, query,
Query::SingleQuery(QueryPart::Matches(EntryQuery { Query::SingleQuery(QueryPart::Matches(EntryQuery {
@ -544,7 +584,7 @@ mod test {
#[test] #[test]
fn test_contains() -> Result<()> { fn test_contains() -> Result<()> {
let query = "(matches (contains \"foo\") ? ?)".parse::<Query>()?; let query = r#"(matches (contains "foo") ? ?)"#.parse::<Query>()?;
assert_eq!( assert_eq!(
query, query,
Query::SingleQuery(QueryPart::Matches(EntryQuery { Query::SingleQuery(QueryPart::Matches(EntryQuery {
@ -555,7 +595,7 @@ mod test {
); );
query.to_sqlite_predicates()?; query.to_sqlite_predicates()?;
let query = "(matches ? (contains \"foo\") ?)".parse::<Query>()?; let query = r#"(matches ? (contains "foo") ?)"#.parse::<Query>()?;
assert_eq!( assert_eq!(
query, query,
Query::SingleQuery(QueryPart::Matches(EntryQuery { Query::SingleQuery(QueryPart::Matches(EntryQuery {
@ -566,7 +606,7 @@ mod test {
); );
query.to_sqlite_predicates()?; query.to_sqlite_predicates()?;
let query = "(matches ? ? (contains \"foo\"))".parse::<Query>()?; let query = r#"(matches ? ? (contains "foo"))"#.parse::<Query>()?;
assert_eq!( assert_eq!(
query, query,
Query::SingleQuery(QueryPart::Matches(EntryQuery { Query::SingleQuery(QueryPart::Matches(EntryQuery {

View File

@ -458,7 +458,7 @@ mod test {
upend_insert_val!(connection, random_entity, LABEL_ATTR, "FOOBAR").unwrap(); upend_insert_val!(connection, random_entity, LABEL_ATTR, "FOOBAR").unwrap();
upend_insert_val!(connection, random_entity, "FLAVOUR", "STRANGE").unwrap(); upend_insert_val!(connection, random_entity, "FLAVOUR", "STRANGE").unwrap();
let query = format!(r#"(matches "{random_entity}" ? ?)"#) let query = format!(r#"(matches @{random_entity} ? ?)"#)
.parse() .parse()
.unwrap(); .unwrap();
let result = connection.query(query).unwrap(); let result = connection.query(query).unwrap();
@ -468,7 +468,7 @@ mod test {
upend_insert_val!(connection, random_entity, LABEL_ATTR, "BAZQUX").unwrap(); upend_insert_val!(connection, random_entity, LABEL_ATTR, "BAZQUX").unwrap();
upend_insert_val!(connection, random_entity, "CHARGE", "POSITIVE").unwrap(); upend_insert_val!(connection, random_entity, "CHARGE", "POSITIVE").unwrap();
let query = format!(r#"(matches (in "{random_entity}" "{other_entity}") ? ?)"#) let query = format!(r#"(matches (in @{random_entity} @{other_entity}) ? ?)"#)
.parse() .parse()
.unwrap(); .unwrap();
let result = connection.query(query).unwrap(); let result = connection.query(query).unwrap();
@ -478,11 +478,11 @@ mod test {
let result = connection.query(query).unwrap(); let result = connection.query(query).unwrap();
assert_eq!(result.len(), 2); assert_eq!(result.len(), 2);
// let query = format!("(matches ? \"{LABEL_ATTR}\" (in \"FOOBAR\" \"BAZQUX\"))") let query = format!(r#"(matches ? "{LABEL_ATTR}" (in "FOOBAR" "BAZQUX"))"#)
// .parse() .parse()
// .unwrap(); .unwrap();
// let result = connection.query(query).unwrap(); let result = connection.query(query).unwrap();
// assert_eq!(result.len(), 2); assert_eq!(result.len(), 2);
let query = format!(r#"(matches ? "{LABEL_ATTR}" (contains "OOBA"))"#) let query = format!(r#"(matches ? "{LABEL_ATTR}" (contains "OOBA"))"#)
.parse() .parse()

View File

@ -72,7 +72,7 @@ impl Extractor for WebExtractor {
) -> Result<bool> { ) -> Result<bool> {
Ok(connection Ok(connection
.query( .query(
format!("(matches \"{address}\" (in \"HTML_TITLE\" \"HTML_DESCRIPTION\") ?)") format!(r#"(matches @{address} (in "HTML_TITLE" "HTML_DESCRIPTION") ?)"#)
.parse()?, .parse()?,
)? )?
.is_empty()) .is_empty())

View File

@ -576,7 +576,7 @@ pub async fn put_object_attribute(
let new_address = web::block(move || { let new_address = web::block(move || {
connection.transaction::<_, anyhow::Error, _>(|| { connection.transaction::<_, anyhow::Error, _>(|| {
let existing_attr_entries = let existing_attr_entries =
connection.query(format!("(matches \"{address}\" \"{attribute}\" ?)").parse()?)?; connection.query(format!(r#"(matches @{address} "{attribute}" ?)"#).parse()?)?;
for eae in existing_attr_entries { for eae in existing_attr_entries {
let _ = connection.remove_object(eae.address()?)?; let _ = connection.remove_object(eae.address()?)?;

View File

@ -45,7 +45,7 @@
$: allTypeEntries = query( $: allTypeEntries = query(
`(matches (in ${allTypeAddresses `(matches (in ${allTypeAddresses
.map((addr) => `"${addr}"`) .map((addr) => `@${addr}`)
.join(" ")}) ? ?)` .join(" ")}) ? ?)`
).result; ).result;

View File

@ -45,7 +45,7 @@
} }
}); });
const addressesString = addresses.map((addr) => `"${addr}"`).join(" "); const addressesString = addresses.map((addr) => `@${addr}`).join(" ");
labelListing = query(`(matches (in ${addressesString}) "LBL" ? )`).result; labelListing = query(`(matches (in ${addressesString}) "LBL" ? )`).result;
} }

View File

@ -79,7 +79,7 @@
} }
}); });
const addressesString = addresses.map((addr) => `"${addr}"`).join(" "); const addressesString = addresses.map((addr) => `@${addr}`).join(" ");
labelListing = query(`(matches (in ${addressesString}) "LBL" ? )`).result; labelListing = query(`(matches (in ${addressesString}) "LBL" ? )`).result;
} }

View File

@ -38,7 +38,7 @@
let exactHits: string[] = []; let exactHits: string[] = [];
$: { $: {
const addressesString = objects.map((e) => `"${e.entity}"`).join(" "); const addressesString = objects.map((e) => `@${e.entity}`).join(" ");
queryOnce(`(matches (in ${addressesString}) "LBL" ? )`).then( queryOnce(`(matches (in ${addressesString}) "LBL" ? )`).then(
(labelListing) => { (labelListing) => {
exactHits = labelListing.entries exactHits = labelListing.entries