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

View File

@ -1,7 +1,6 @@
use crate::addressing::Address;
use crate::database::entry::EntryValue;
use crate::database::inner::schema::data;
use anyhow::{anyhow, Result};
use diesel::expression::operators::{And, Not, Or};
use diesel::sql_types::Bool;
use diesel::sqlite::Sqlite;
@ -11,10 +10,19 @@ use std::borrow::Borrow;
use std::convert::TryFrom;
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)]
pub enum QueryComponent<T>
where
T: FromStr,
T: TryFrom<lexpr::Value>,
{
Exact(T),
In(Vec<T>),
@ -25,10 +33,66 @@ where
#[derive(Debug, PartialEq)]
pub struct EntryQuery {
pub entity: QueryComponent<Address>,
pub attribute: QueryComponent<String>,
pub attribute: QueryComponent<Attribute>,
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)]
pub enum QueryPart {
Matches(EntryQuery),
@ -54,15 +118,28 @@ pub enum Query {
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>;
impl TryFrom<&lexpr::Value> for Query {
type Error = anyhow::Error;
type Error = QueryParseError;
fn try_from(expression: &lexpr::Value) -> Result<Self> {
fn parse_component<T: FromStr>(value: &lexpr::Value) -> Result<QueryComponent<T>>
fn try_from(expression: &lexpr::Value) -> Result<Self, Self::Error> {
fn parse_component<T: TryFrom<lexpr::Value>>(
value: &lexpr::Value,
) -> Result<QueryComponent<T>, QueryParseError>
where
<T as FromStr>::Err: std::fmt::Debug,
QueryParseError: From<<T as TryFrom<lexpr::Value>>::Error>,
{
match value {
lexpr::Value::Cons(cons) => {
@ -72,22 +149,16 @@ impl TryFrom<&lexpr::Value> for Query {
let (cons_vec, _) = cons.clone().into_vec();
if let Some(split) = cons_vec.split_first() {
let args = split.1;
let values: Result<Vec<T>, _> = args.iter().map(|value| {
if let lexpr::Value::String(str) = value {
match T::from_str(str.borrow()) {
Ok(value) => Ok(value),
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();
let values: Result<Vec<T>, _> = args
.iter()
.map(|value| T::try_from(value.clone()))
.collect();
Ok(QueryComponent::In(values?))
} else {
Err(anyhow!(
"Malformed expression: Inner value cannot be empty."
))
Err(QueryParseError(
"Malformed expression: Inner value cannot be empty.".into(),
))
}
}
"contains" => {
@ -98,43 +169,28 @@ impl TryFrom<&lexpr::Value> for Query {
if let lexpr::Value::String(str) = value {
Ok(QueryComponent::Contains(str.into_string()))
} else {
Err(anyhow!("Malformed expression: 'Contains' argument must be a string."))
Err(QueryParseError("Malformed expression: 'Contains' argument must be a string.".into()))
}
}
_ => Err(anyhow!(
"Malformed expression: 'Contains' requires a single argument."
_ => Err(QueryParseError(
"Malformed expression: 'Contains' requires a single argument.".into()
)),
}
}
_ => Err(anyhow!(format!(
"Malformed expression: Unknown symbol {}",
symbol
))),
_ => Err(QueryParseError(format!(
"Malformed expression: Unknowne symbol {}",
symbol
))),
}
} else {
Err(anyhow!(format!(
"Malformed expression: Inner value '{:?}' is not a symbol.",
value
)))
Err(QueryParseError(format!(
"Malformed expression: Inner value '{:?}' is not a symbol.",
value
)))
}
}
lexpr::Value::String(str) => match T::from_str(str.borrow()) {
Ok(value) => Ok(QueryComponent::Exact(value)),
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 '?'."
)),
lexpr::Value::Symbol(symbol) if symbol.as_ref() == "?" => Ok(QueryComponent::Any),
_ => Ok(QueryComponent::Exact(T::try_from(value.clone())?)),
}
}
@ -145,7 +201,7 @@ impl TryFrom<&lexpr::Value> for Query {
let (cons_vec, _) = value.clone().into_vec();
if let [_, entity, attribute, value] = &cons_vec[..] {
let entity = parse_component::<Address>(entity)?;
let attribute = parse_component::<String>(attribute)?;
let attribute = parse_component::<Attribute>(attribute)?;
let value = parse_component::<EntryValue>(value)?;
Ok(Query::SingleQuery(QueryPart::Matches(EntryQuery {
entity,
@ -153,8 +209,9 @@ impl TryFrom<&lexpr::Value> for Query {
value,
})))
} else {
Err(anyhow!(
Err(QueryParseError(
"Malformed expression: Wrong number of arguments to 'matches'."
.into(),
))
}
}
@ -166,13 +223,14 @@ impl TryFrom<&lexpr::Value> for Query {
type_name_str.to_string(),
)))
} else {
Err(anyhow!(
Err(QueryParseError(
"Malformed expression: Type must be specified as a string."
.into(),
))
}
} else {
Err(anyhow!(
"Malformed expression: Wrong number of arguments to 'type'."
Err(QueryParseError(
"Malformed expression: Wrong number of arguments to 'type'.".into(),
))
}
}
@ -182,7 +240,7 @@ impl TryFrom<&lexpr::Value> for Query {
let values = sub_expressions
.iter()
.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) {
Ok(Query::MultiQuery(MultiQuery {
@ -193,8 +251,8 @@ impl TryFrom<&lexpr::Value> for Query {
queries,
}))
} else {
Err(anyhow!(
"Malformed expression: sub-query list cannot be empty.",
Err(QueryParseError(
"Malformed expression: sub-query list cannot be empty.".into(),
))
}
}
@ -204,7 +262,7 @@ impl TryFrom<&lexpr::Value> for Query {
let values = sub_expressions
.iter()
.map(|value| Ok(Box::new(Query::try_from(value)?)))
.collect::<Result<Vec<Box<Query>>>>()?;
.collect::<Result<Vec<Box<Query>>, QueryParseError>>()?;
if values.len() == 1 {
Ok(Query::MultiQuery(MultiQuery {
@ -212,85 +270,96 @@ impl TryFrom<&lexpr::Value> for Query {
queries: NonEmpty::from_vec(values).unwrap(),
}))
} else {
Err(anyhow!(
"Malformed expression: NOT takes exactly one parameter."
Err(QueryParseError(
"Malformed expression: NOT takes exactly one parameter.".into(),
))
}
}
_ => Err(anyhow!(format!(
_ => Err(QueryParseError(format!(
"Malformed expression: Unknown symbol '{}'.",
symbol
))),
}
} else {
Err(anyhow!(format!(
Err(QueryParseError(format!(
"Malformed expression: Value '{:?}' is not a symbol.",
value
)))
}
} else {
Err(anyhow!("Malformed expression: Not a list."))
Err(QueryParseError("Malformed expression: Not a list.".into()))
}
}
}
impl FromStr for Query {
type Err = anyhow::Error;
type Err = QueryParseError;
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)
}
}
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 {
Query::SingleQuery(qp) => {
match qp {
QueryPart::Matches(eq) => {
let mut subqueries: Vec<Box<Predicate>> = vec![];
Query::SingleQuery(qp) => match qp {
QueryPart::Matches(eq) => {
let mut subqueries: Vec<Box<Predicate>> = vec![];
match &eq.entity {
QueryComponent::Exact(q_entity) => {
subqueries.push(Box::new(data::entity.eq(q_entity.encode()?)))
match &eq.entity {
QueryComponent::Exact(q_entity) => subqueries.push(Box::new(
data::entity.eq(q_entity
.encode()
.map_err(|_| QueryParseError("???".into()))?),
)),
QueryComponent::In(q_entities) => {
let entities: Result<Vec<_>, _> =
q_entities.iter().map(|t| t.encode()).collect();
subqueries.push(Box::new(
data::entity
.eq_any(entities.map_err(|_| QueryParseError("???".into()))?),
))
}
QueryComponent::Contains(q_entity) => subqueries.push(Box::new(
data::entity_searchable.like(format!("%{}%", q_entity)),
)),
QueryComponent::Any => {}
};
match &eq.attribute {
QueryComponent::Exact(q_attribute) => {
subqueries.push(Box::new(data::attribute.eq(q_attribute.0.clone())))
}
QueryComponent::In(q_attributes) => subqueries.push(Box::new(
data::attribute.eq_any(q_attributes.iter().map(|a| &a.0).cloned()),
)),
QueryComponent::Contains(q_attribute) => subqueries
.push(Box::new(data::attribute.like(format!("%{}%", q_attribute)))),
QueryComponent::Any => {}
};
match &eq.value {
QueryComponent::Exact(q_value) => match q_value {
EntryValue::Number(n) => {
subqueries.push(Box::new(data::value_num.eq(*n)))
}
QueryComponent::In(q_entities) => {
let entities: Result<Vec<_>, _> =
q_entities.iter().map(|t| t.encode()).collect();
subqueries.push(Box::new(data::entity.eq_any(entities?)))
}
QueryComponent::Contains(q_entity) => subqueries.push(Box::new(
data::entity_searchable.like(format!("%{}%", q_entity)),
_ => subqueries.push(Box::new(
data::value_str.eq(q_value
.to_string()
.map_err(|_| QueryParseError("???".into()))?),
)),
QueryComponent::Any => {}
};
},
QueryComponent::In(q_values) => {
let first = q_values.first().ok_or_else(|| {
QueryParseError(
"Malformed expression: Inner value cannot be empty.".into(),
)
})?;
match &eq.attribute {
QueryComponent::Exact(q_attribute) => {
subqueries.push(Box::new(data::attribute.eq(q_attribute.clone())))
}
QueryComponent::In(q_attributes) => subqueries
.push(Box::new(data::attribute.eq_any(q_attributes.clone()))),
QueryComponent::Contains(q_attribute) => subqueries
.push(Box::new(data::attribute.like(format!("%{}%", q_attribute)))),
QueryComponent::Any => {}
};
match &eq.value {
QueryComponent::Exact(q_value) => match q_value {
EntryValue::Number(n) => {
subqueries.push(Box::new(data::value_num.eq(*n)))
}
_ => subqueries
.push(Box::new(data::value_str.eq(q_value.to_string()?))),
},
QueryComponent::In(q_values) => {
let first = q_values.first().ok_or(anyhow!(
"Malformed expression: Inner value cannot be empty."
))?;
match first {
match first {
EntryValue::Number(_) => subqueries.push(Box::new(
data::value_num.eq_any(
q_values
@ -299,10 +368,10 @@ impl Query {
if let EntryValue::Number(n) = v {
Ok(*n)
} 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(
@ -311,39 +380,38 @@ impl Query {
.iter()
.map(|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 {
v.to_string()
v.to_string().map_err(|_| QueryParseError("???".into()))
}
})
.collect::<Result<Vec<String>>>()?,
.collect::<Result<Vec<String>, QueryParseError>>()?,
),
)),
}
}
QueryComponent::Contains(q_value) => subqueries
.push(Box::new(data::value_str.like(format!("S%{}%", q_value)))),
QueryComponent::Any => {}
};
}
QueryComponent::Contains(q_value) => subqueries
.push(Box::new(data::value_str.like(format!("S%{}%", q_value)))),
QueryComponent::Any => {}
};
match subqueries.len() {
0 => Ok(Box::new(true.into_sql::<Bool>())),
1 => Ok(subqueries.remove(0)),
_ => {
let mut result: Box<And<Box<Predicate>, Box<Predicate>>> =
Box::new(And::new(subqueries.remove(0), subqueries.remove(0)));
while !subqueries.is_empty() {
result = Box::new(And::new(result, subqueries.remove(0)));
}
Ok(Box::new(result))
match subqueries.len() {
0 => Ok(Box::new(true.into_sql::<Bool>())),
1 => Ok(subqueries.remove(0)),
_ => {
let mut result: Box<And<Box<Predicate>, Box<Predicate>>> =
Box::new(And::new(subqueries.remove(0), subqueries.remove(0)));
while !subqueries.is_empty() {
result = Box::new(And::new(result, subqueries.remove(0)));
}
Ok(Box::new(result))
}
}
QueryPart::Type(_) => unimplemented!("Type queries are not yet implemented."),
}
}
QueryPart::Type(_) => unimplemented!("Type queries are not yet implemented."),
},
Query::MultiQuery(mq) => {
let subqueries: Result<Vec<Box<Predicate>>> = mq
let subqueries: Result<Vec<Box<Predicate>>, QueryParseError> = mq
.queries
.iter()
.map(|sq| sq.to_sqlite_predicates())
@ -375,7 +443,9 @@ impl Query {
}
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)]
mod test {
use super::*;
use anyhow::Result;
use anyhow::{anyhow, Result};
#[test]
fn test_matches() -> Result<()> {
@ -402,7 +472,7 @@ mod test {
query.to_sqlite_predicates()?;
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!(
query,
Query::SingleQuery(QueryPart::Matches(EntryQuery {
@ -413,19 +483,19 @@ mod test {
);
query.to_sqlite_predicates()?;
let query = "(matches ? \"FOO\" ?)".parse::<Query>()?;
let query = r#"(matches ? "FOO" ?)"#.parse::<Query>()?;
assert_eq!(
query,
Query::SingleQuery(QueryPart::Matches(EntryQuery {
entity: QueryComponent::Any,
attribute: QueryComponent::Exact(String::from("FOO")),
attribute: QueryComponent::Exact("FOO".into()),
value: QueryComponent::Any
}))
);
query.to_sqlite_predicates()?;
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!(
query,
Query::SingleQuery(QueryPart::Matches(EntryQuery {
@ -441,27 +511,19 @@ mod test {
#[test]
fn test_in_parse() -> Result<()> {
let query = "(matches ? (in \"FOO\" \"BAR\") ?)".parse::<Query>()?;
let query = r#"(matches ? (in "FOO" "BAR") ?)"#.parse::<Query>()?;
assert_eq!(
query,
Query::SingleQuery(QueryPart::Matches(EntryQuery {
entity: QueryComponent::Any,
attribute: QueryComponent::In(vec!("FOO".to_string(), "BAR".to_string())),
attribute: QueryComponent::In(vec!("FOO".into(), "BAR".into())),
value: QueryComponent::Any
}))
);
query.to_sqlite_predicates()?;
let values: Vec<EntryValue> = vec!["FOO".into(), "BAR".into()];
let query = format!(
"(matches ? ? (in {}))",
values
.iter()
.map(|v| format!("\"{}\"", v.to_string().unwrap()))
.collect::<Vec<String>>()
.join(" ")
)
.parse::<Query>()?;
let query = r#"(matches ? ? (in "FOO" "BAR"))"#.parse::<Query>()?;
assert_eq!(
query,
Query::SingleQuery(QueryPart::Matches(EntryQuery {
@ -473,15 +535,7 @@ mod test {
query.to_sqlite_predicates()?;
let values = vec![EntryValue::Number(1337.93), EntryValue::Number(1968.12)];
let query = format!(
"(matches ? ? (in {}))",
values
.iter()
.map(|v| format!("\"{}\"", v.to_string().unwrap()))
.collect::<Vec<String>>()
.join(" ")
)
.parse::<Query>()?;
let query = r#"(matches ? ? (in 1337.93 1968.12))"#.parse::<Query>()?;
assert_eq!(
query,
Query::SingleQuery(QueryPart::Matches(EntryQuery {
@ -494,15 +548,8 @@ mod test {
// Invalid queries
let values: Vec<EntryValue> = vec!["FOO".into(), EntryValue::Number(1337.93)];
let query = format!(
"(matches ? ? (in {}))",
values
.iter()
.map(|v| format!("\"{}\"", v.to_string().unwrap()))
.collect::<Vec<String>>()
.join(" ")
)
.parse::<Query>()?;
let query = r#"(matches ? ? (in "FOO" 1337.93))"#.parse::<Query>()?;
assert_eq!(
query,
Query::SingleQuery(QueryPart::Matches(EntryQuery {
@ -517,15 +564,8 @@ mod test {
.ok_or(anyhow!("Failed to reject mixed query."))?;
let values = vec![EntryValue::Number(1337.93), "FOO".into()];
let query = format!(
"(matches ? ? (in {}))",
values
.iter()
.map(|v| format!("\"{}\"", v.to_string().unwrap()))
.collect::<Vec<String>>()
.join(" ")
)
.parse::<Query>()?;
let query = r#"(matches ? ? (in 1337.93 "FOO"))"#.parse::<Query>()?;
assert_eq!(
query,
Query::SingleQuery(QueryPart::Matches(EntryQuery {
@ -544,7 +584,7 @@ mod test {
#[test]
fn test_contains() -> Result<()> {
let query = "(matches (contains \"foo\") ? ?)".parse::<Query>()?;
let query = r#"(matches (contains "foo") ? ?)"#.parse::<Query>()?;
assert_eq!(
query,
Query::SingleQuery(QueryPart::Matches(EntryQuery {
@ -555,7 +595,7 @@ mod test {
);
query.to_sqlite_predicates()?;
let query = "(matches ? (contains \"foo\") ?)".parse::<Query>()?;
let query = r#"(matches ? (contains "foo") ?)"#.parse::<Query>()?;
assert_eq!(
query,
Query::SingleQuery(QueryPart::Matches(EntryQuery {
@ -566,7 +606,7 @@ mod test {
);
query.to_sqlite_predicates()?;
let query = "(matches ? ? (contains \"foo\"))".parse::<Query>()?;
let query = r#"(matches ? ? (contains "foo"))"#.parse::<Query>()?;
assert_eq!(
query,
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, "FLAVOUR", "STRANGE").unwrap();
let query = format!(r#"(matches "{random_entity}" ? ?)"#)
let query = format!(r#"(matches @{random_entity} ? ?)"#)
.parse()
.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, "CHARGE", "POSITIVE").unwrap();
let query = format!(r#"(matches (in "{random_entity}" "{other_entity}") ? ?)"#)
let query = format!(r#"(matches (in @{random_entity} @{other_entity}) ? ?)"#)
.parse()
.unwrap();
let result = connection.query(query).unwrap();
@ -478,11 +478,11 @@ mod test {
let result = connection.query(query).unwrap();
assert_eq!(result.len(), 2);
// let query = format!("(matches ? \"{LABEL_ATTR}\" (in \"FOOBAR\" \"BAZQUX\"))")
// .parse()
// .unwrap();
// let result = connection.query(query).unwrap();
// assert_eq!(result.len(), 2);
let query = format!(r#"(matches ? "{LABEL_ATTR}" (in "FOOBAR" "BAZQUX"))"#)
.parse()
.unwrap();
let result = connection.query(query).unwrap();
assert_eq!(result.len(), 2);
let query = format!(r#"(matches ? "{LABEL_ATTR}" (contains "OOBA"))"#)
.parse()

View File

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

View File

@ -576,7 +576,7 @@ pub async fn put_object_attribute(
let new_address = web::block(move || {
connection.transaction::<_, anyhow::Error, _>(|| {
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 {
let _ = connection.remove_object(eae.address()?)?;

View File

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

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;
}

View File

@ -38,7 +38,7 @@
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(
(labelListing) => {
exactHits = labelListing.entries