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/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)]