diff --git a/contracts/axone-cognitarium/src/msg.rs b/contracts/axone-cognitarium/src/msg.rs index 6bd049e1..666f70c4 100644 --- a/contracts/axone-cognitarium/src/msg.rs +++ b/contracts/axone-cognitarium/src/msg.rs @@ -474,6 +474,24 @@ pub enum WhereClause { /// # LateralJoin /// Evaluates right for all result row of left LateralJoin { left: Box, right: Box }, + + /// # Filter + /// Filters the inner clause matching the expression. + Filter { expr: Expression, inner: Box }, +} + +#[cw_serde] +pub enum Expression { + NamedNode(IRI), + Literal(Literal), + Variable(String), + And(Vec), + Or(Vec), + Equal(Box, Box), + Greater(Box, Box), + GreaterOrEqual(Box, Box), + Less(Box, Box), + LessOrEqual(Box, Box), } /// # TripleDeleteTemplate diff --git a/contracts/axone-cognitarium/src/querier/engine.rs b/contracts/axone-cognitarium/src/querier/engine.rs index 6b95d18f..f84abbf4 100644 --- a/contracts/axone-cognitarium/src/querier/engine.rs +++ b/contracts/axone-cognitarium/src/querier/engine.rs @@ -1,6 +1,7 @@ use crate::msg::{ Node, SelectItem, VarOrNamedNode, VarOrNamedNodeOrLiteral, VarOrNode, VarOrNodeOrLiteral, }; +use crate::querier::expression::Expression; use crate::querier::mapper::{iri_as_node, literal_as_object}; use crate::querier::plan::{PatternValue, QueryNode, QueryPlan}; use crate::querier::variable::{ResolvedVariable, ResolvedVariables}; @@ -159,6 +160,17 @@ impl<'a> QueryEngine<'a> { Box::new(ForLoopJoinIterator::new(left(vars), right)) }) } + QueryNode::Filter { expr, inner } => { + let inner = self.eval_node(*inner); + Rc::new(move |vars| { + Box::new(FilterIterator::new( + self.storage, + inner(vars), + expr.clone(), + self.ns_cache.clone(), + )) + }) + } QueryNode::Skip { child, first } => { let upstream = self.eval_node(*child); Rc::new(move |vars| Box::new(upstream(vars).skip(first))) @@ -173,6 +185,27 @@ impl<'a> QueryEngine<'a> { type ResolvedVariablesIterator<'a> = Box> + 'a>; +struct FilterIterator<'a> { + upstream: ResolvedVariablesIterator<'a>, + expr: Expression, + ns_resolver: NamespaceResolver<'a>, +} + +impl<'a> FilterIterator<'a> { + fn new( + storage: &'a dyn Storage, + upstream: ResolvedVariablesIterator<'a>, + expr: Expression, + ns_cache: Vec, + ) -> Self { + Self { + upstream, + expr, + ns_resolver: NamespaceResolver::new(storage, ns_cache.into()), + } + } +} + impl<'a> Iterator for FilterIterator<'a> { type Item = StdResult; diff --git a/contracts/axone-cognitarium/src/querier/expression.rs b/contracts/axone-cognitarium/src/querier/expression.rs new file mode 100644 index 00000000..a53147a2 --- /dev/null +++ b/contracts/axone-cognitarium/src/querier/expression.rs @@ -0,0 +1,153 @@ +use crate::msg; +use crate::querier::mapper::iri_as_string; +use crate::querier::ResolvedVariables; +use crate::state::NamespaceSolver; +use cosmwasm_std::{StdError, StdResult}; +use std::cmp::Ordering; +use std::collections::{BTreeSet, HashMap}; + +#[derive(Eq, PartialEq, Debug, Clone)] +pub enum Expression { + Constant(Term), + Variable(usize), + And(Vec), + Or(Vec), + Equal(Box, Box), + Greater(Box, Box), + GreaterOrEqual(Box, Box), + Less(Box, Box), + LessOrEqual(Box, Box), +} + +impl Expression { + pub fn bound_variables(&self) -> BTreeSet { + let mut vars = BTreeSet::new(); + self.lookup_bound_variables(&mut |v| { + vars.insert(v); + }); + vars + } + + pub fn lookup_bound_variables(&self, callback: &mut impl FnMut(usize)) { + match self { + Expression::Constant(_) => {} + Expression::Variable(v) => { + callback(*v); + } + Expression::And(exprs) | Expression::Or(exprs) => { + exprs + .iter() + .for_each(|e| e.lookup_bound_variables(callback)); + } + Expression::Equal(left, right) + | Expression::Greater(left, right) + | Expression::GreaterOrEqual(left, right) + | Expression::Less(left, right) + | Expression::LessOrEqual(left, right) => { + left.lookup_bound_variables(callback); + right.lookup_bound_variables(callback); + } + } + } + + pub fn evaluate<'a>( + &self, + vars: &'a ResolvedVariables, + ns_solver: &mut dyn NamespaceSolver, + ) -> StdResult { + match self { + Expression::Constant(term) => Ok(term.clone()), + Expression::Variable(v) => vars + .get(*v) + .clone() + .ok_or(StdError::generic_err("Unbound filter variable")) + .and_then(|v| v.as_term(ns_solver)), + Expression::And(exprs) => { + for expr in exprs { + if !expr.evaluate(vars, ns_solver)?.as_bool() { + return Ok(Term::Boolean(false)); + } + } + return Ok(Term::Boolean(true)); + } + Expression::Or(exprs) => { + for expr in exprs { + if expr.evaluate(vars, ns_solver)?.as_bool() { + return Ok(Term::Boolean(true)); + } + } + return Ok(Term::Boolean(false)); + } + Expression::Equal(left, right) => Ok(Term::Boolean( + left.evaluate(vars, ns_solver)? == right.evaluate(vars, ns_solver)?, + )), + Expression::Greater(left, right) => Ok(Term::Boolean( + left.evaluate(vars, ns_solver)? > right.evaluate(vars, ns_solver)?, + )), + Expression::GreaterOrEqual(left, right) => Ok(Term::Boolean( + left.evaluate(vars, ns_solver)? >= right.evaluate(vars, ns_solver)?, + )), + Expression::Less(left, right) => Ok(Term::Boolean( + left.evaluate(vars, ns_solver)? < right.evaluate(vars, ns_solver)?, + )), + Expression::LessOrEqual(left, right) => Ok(Term::Boolean( + left.evaluate(vars, ns_solver)? <= right.evaluate(vars, ns_solver)?, + )), + } + } +} + +#[derive(Eq, PartialEq, Debug, Clone)] +pub enum Term { + String(String), + Boolean(bool), +} + +impl Term { + pub fn from_iri(iri: msg::IRI, prefixes: &HashMap) -> StdResult { + Ok(Term::String(iri_as_string(iri, prefixes)?)) + } + + pub fn from_literal( + literal: msg::Literal, + prefixes: &HashMap, + ) -> StdResult { + Ok(Term::String(match literal { + msg::Literal::Simple(value) => value, + msg::Literal::LanguageTaggedString { value, language } => { + format!("{}{}", value, language).to_string() + } + msg::Literal::TypedValue { value, datatype } => { + format!("{}{}", value, iri_as_string(datatype, prefixes)?).to_string() + } + })) + } + + pub fn as_string(&self) -> String { + match self { + Term::String(t) => t.clone(), + Term::Boolean(b) => b.to_string(), + } + } + + pub fn as_bool(&self) -> bool { + match self { + Term::String(s) => !s.is_empty(), + Term::Boolean(b) => *b, + } + } +} + +impl PartialOrd for Term { + fn partial_cmp(&self, other: &Term) -> Option { + if self == other { + return Some(Ordering::Equal); + } + + match (self, other) { + (Term::String(left), Term::String(right)) => Some(left.cmp(right)), + (Term::Boolean(left), Term::Boolean(right)) => Some(left.cmp(right)), + _ => None, + } + } +} diff --git a/contracts/axone-cognitarium/src/querier/mod.rs b/contracts/axone-cognitarium/src/querier/mod.rs index b9ecd6ff..84e4d6e7 100644 --- a/contracts/axone-cognitarium/src/querier/mod.rs +++ b/contracts/axone-cognitarium/src/querier/mod.rs @@ -1,4 +1,5 @@ mod engine; +mod expression; mod mapper; mod plan; mod plan_builder; diff --git a/contracts/axone-cognitarium/src/querier/plan.rs b/contracts/axone-cognitarium/src/querier/plan.rs index 6feafd07..9dd28371 100644 --- a/contracts/axone-cognitarium/src/querier/plan.rs +++ b/contracts/axone-cognitarium/src/querier/plan.rs @@ -1,3 +1,4 @@ +use crate::querier::expression::Expression; use crate::state::{Object, Predicate, Subject}; use std::collections::BTreeSet; @@ -58,26 +59,45 @@ pub enum QueryNode { /// Results in no solutions, this special node is used when we know before plan execution that a node /// will end up with no possible solutions. For example, using a triple pattern filtering with a constant /// named node containing a non-existing namespace. - Noop { bound_variables: Vec }, + Noop { + bound_variables: Vec, + }, /// Join two nodes by applying the cartesian product of the nodes variables. /// /// This should be used when the nodes don't have variables in common, and can be seen as a /// full join of disjoint datasets. - CartesianProductJoin { left: Box, right: Box }, + CartesianProductJoin { + left: Box, + right: Box, + }, /// Join two nodes by using the variables values from the left node as replacement in the right /// node. /// /// This results to an inner join, but the underlying processing stream the variables from the /// left node to use them as right node values. - ForLoopJoin { left: Box, right: Box }, + ForLoopJoin { + left: Box, + right: Box, + }, + + Filter { + expr: Expression, + inner: Box, + }, /// Skip the specified first elements from the child node. - Skip { child: Box, first: usize }, + Skip { + child: Box, + first: usize, + }, /// Limit to the specified first elements from the child node. - Limit { child: Box, first: usize }, + Limit { + child: Box, + first: usize, + }, } impl QueryNode { @@ -114,6 +134,10 @@ impl QueryNode { left.lookup_bound_variables(callback); right.lookup_bound_variables(callback); } + QueryNode::Filter { expr, inner } => { + expr.lookup_bound_variables(callback); + inner.lookup_bound_variables(callback); + } QueryNode::Skip { child, .. } | QueryNode::Limit { child, .. } => { child.lookup_bound_variables(callback); } diff --git a/contracts/axone-cognitarium/src/querier/plan_builder.rs b/contracts/axone-cognitarium/src/querier/plan_builder.rs index fae1bfb9..6ecca6ef 100644 --- a/contracts/axone-cognitarium/src/querier/plan_builder.rs +++ b/contracts/axone-cognitarium/src/querier/plan_builder.rs @@ -1,10 +1,12 @@ +use crate::msg; use crate::msg::{Node, TriplePattern, VarOrNamedNode, VarOrNode, VarOrNodeOrLiteral, WhereClause}; +use crate::querier::expression::{Expression, Term}; use crate::querier::mapper::{iri_as_node, literal_as_object}; use crate::querier::plan::{PatternValue, PlanVariable, QueryNode, QueryPlan}; use crate::state::{ HasCachedNamespaces, Namespace, NamespaceQuerier, NamespaceResolver, Object, Predicate, Subject, }; -use cosmwasm_std::{StdResult, Storage}; +use cosmwasm_std::{StdError, StdResult, Storage}; use std::collections::HashMap; pub struct PlanBuilder<'a> { @@ -69,6 +71,18 @@ impl<'a> PlanBuilder<'a> { left: Box::new(self.build_node(left)?), right: Box::new(self.build_node(right)?), }), + WhereClause::Filter { expr, inner } => { + let inner = Box::new(self.build_node(inner)?); + let expr = self.build_expression(expr)?; + + if !expr.bound_variables().is_subset(&inner.bound_variables()) { + return Err(StdError::generic_err( + "Unbound variable in filter expression", + )); + } + + Ok(QueryNode::Filter { expr, inner }) + } } } @@ -101,6 +115,50 @@ impl<'a> PlanBuilder<'a> { .unwrap_or(Ok(QueryNode::noop())) } + fn build_expression(&mut self, expr: &msg::Expression) -> StdResult { + match expr { + msg::Expression::NamedNode(iri) => { + Term::from_iri(iri.clone(), self.prefixes).map(Expression::Constant) + } + msg::Expression::Literal(literal) => { + Term::from_literal(literal.clone(), self.prefixes).map(Expression::Constant) + } + msg::Expression::Variable(v) => Ok(Expression::Variable( + self.resolve_basic_variable(v.to_string()), + )), + msg::Expression::And(exprs) => exprs + .iter() + .map(|e| self.build_expression(e)) + .collect::>>() + .map(Expression::And), + msg::Expression::Or(exprs) => exprs + .iter() + .map(|e| self.build_expression(e)) + .collect::>>() + .map(Expression::Or), + msg::Expression::Equal(left, right) => Ok(Expression::Equal( + Box::new(self.build_expression(left)?), + Box::new(self.build_expression(right)?), + )), + msg::Expression::Greater(left, right) => Ok(Expression::Greater( + Box::new(self.build_expression(left)?), + Box::new(self.build_expression(right)?), + )), + msg::Expression::GreaterOrEqual(left, right) => Ok(Expression::GreaterOrEqual( + Box::new(self.build_expression(left)?), + Box::new(self.build_expression(right)?), + )), + msg::Expression::Less(left, right) => Ok(Expression::Less( + Box::new(self.build_expression(left)?), + Box::new(self.build_expression(right)?), + )), + msg::Expression::LessOrEqual(left, right) => Ok(Expression::LessOrEqual( + Box::new(self.build_expression(left)?), + Box::new(self.build_expression(right)?), + )), + } + } + fn build_triple_pattern(&mut self, pattern: &TriplePattern) -> StdResult { let subject_res = self.build_subject_pattern(pattern.subject.clone()); let predicate_res = self.build_predicate_pattern(pattern.predicate.clone()); diff --git a/contracts/axone-cognitarium/src/querier/variable.rs b/contracts/axone-cognitarium/src/querier/variable.rs index d3458d91..511d13be 100644 --- a/contracts/axone-cognitarium/src/querier/variable.rs +++ b/contracts/axone-cognitarium/src/querier/variable.rs @@ -1,4 +1,5 @@ use crate::msg::{Value, IRI}; +use crate::querier::expression::Term; use crate::state::{Literal, NamespaceSolver, Object, Predicate, Subject}; use axone_rdf::normalize::IdentifierIssuer; use cosmwasm_std::StdResult; @@ -95,6 +96,31 @@ impl ResolvedVariable { }, }) } + + pub fn as_term(&self, ns_solver: &mut dyn NamespaceSolver) -> StdResult { + Ok(match self { + ResolvedVariable::Subject(subject) => match subject { + Subject::Named(named) => named.as_iri(ns_solver).map(Term::String)?, + Subject::Blank(blank) => Term::String(format!("_:{}", blank)), + }, + ResolvedVariable::Predicate(predicate) => { + predicate.as_iri(ns_solver).map(Term::String)? + } + ResolvedVariable::Object(object) => match object { + Object::Named(named) => named.as_iri(ns_solver).map(Term::String)?, + Object::Blank(blank) => Term::String(format!("_:{}", blank)), + Object::Literal(literal) => Term::String(match literal { + Literal::Simple { value } => value.clone(), + Literal::I18NString { value, language } => { + format!("{}{}", value, language).to_string() + } + Literal::Typed { value, datatype } => { + format!("{}{}", value, datatype.as_iri(ns_solver)?).to_string() + } + }), + }, + }) + } } #[derive(Eq, PartialEq, Debug, Clone)]