diff --git a/lib/sparopt/src/algebra.rs b/lib/sparopt/src/algebra.rs index 65c4c618..c7d7c82a 100644 --- a/lib/sparopt/src/algebra.rs +++ b/lib/sparopt/src/algebra.rs @@ -660,6 +660,7 @@ pub enum GraphPattern { left: Box, right: Box, expression: Expression, + algorithm: LeftJoinAlgorithm, }, /// Lateral join i.e. evaluate right for all result row of left #[cfg(feature = "sep-0006")] @@ -678,7 +679,11 @@ pub enum GraphPattern { expression: Expression, }, /// [Minus](https://www.w3.org/TR/sparql11-query/#defn_algMinus). - Minus { left: Box, right: Box }, + Minus { + left: Box, + right: Box, + algorithm: MinusAlgorithm, + }, /// A table used to provide inline values Values { variables: Vec, @@ -784,7 +789,12 @@ impl GraphPattern { } } - pub fn left_join(left: Self, right: Self, expression: Expression) -> Self { + pub fn left_join( + left: Self, + right: Self, + expression: Expression, + algorithm: LeftJoinAlgorithm, + ) -> Self { let expression_ebv = expression.effective_boolean_value(); if left.is_empty() || right.is_empty() @@ -801,10 +811,11 @@ impl GraphPattern { } else { expression }, + algorithm, } } - pub fn minus(left: Self, right: Self) -> Self { + pub fn minus(left: Self, right: Self, algorithm: MinusAlgorithm) -> Self { if left.is_empty() { return Self::empty(); } @@ -814,6 +825,7 @@ impl GraphPattern { Self::Minus { left: Box::new(left), right: Box::new(right), + algorithm, } } @@ -1046,7 +1058,7 @@ impl GraphPattern { child.lookup_used_variables(callback); } } - Self::Join { left, right, .. } | Self::Minus { left, right } => { + Self::Join { left, right, .. } | Self::Minus { left, right, .. } => { left.lookup_used_variables(callback); right.lookup_used_variables(callback); } @@ -1059,6 +1071,7 @@ impl GraphPattern { left, right, expression, + .. } => { expression.lookup_used_variables(callback); left.lookup_used_variables(callback); @@ -1148,6 +1161,7 @@ impl GraphPattern { || true.into(), |e| Expression::from_sparql_algebra(e, graph_name), ), + algorithm: LeftJoinAlgorithm::default(), }, #[cfg(feature = "sep-0006")] AlGraphPattern::Lateral { left, right } => Self::Lateral { @@ -1179,6 +1193,7 @@ impl GraphPattern { AlGraphPattern::Minus { left, right } => Self::Minus { left: Box::new(Self::from_sparql_algebra(left, graph_name, blank_nodes)), right: Box::new(Self::from_sparql_algebra(right, graph_name, blank_nodes)), + algorithm: MinusAlgorithm::default(), }, AlGraphPattern::Values { variables, @@ -1365,6 +1380,7 @@ impl From<&GraphPattern> for AlGraphPattern { left, right, expression, + .. } => { let empty_expr = if let Expression::Literal(l) = expression { l.datatype() == xsd::BOOLEAN && l.value() == "true" @@ -1418,7 +1434,7 @@ impl From<&GraphPattern> for AlGraphPattern { expression: expression.into(), variable: variable.clone(), }, - GraphPattern::Minus { left, right } => Self::Minus { + GraphPattern::Minus { left, right, .. } => Self::Minus { left: Box::new(left.as_ref().into()), right: Box::new(right.as_ref().into()), }, @@ -1478,14 +1494,44 @@ impl From<&GraphPattern> for AlGraphPattern { } /// The join algorithm used (c.f. [`GraphPattern::Join`]). -#[derive(Eq, PartialEq, Debug, Clone, Copy, Hash)] +#[derive(Eq, PartialEq, Debug, Clone, Hash)] pub enum JoinAlgorithm { - HashBuildLeftProbeRight, + HashBuildLeftProbeRight { keys: Vec }, } impl Default for JoinAlgorithm { fn default() -> Self { - Self::HashBuildLeftProbeRight + Self::HashBuildLeftProbeRight { + keys: Vec::default(), + } + } +} + +/// The left join algorithm used (c.f. [`GraphPattern::LeftJoin`]). +#[derive(Eq, PartialEq, Debug, Clone, Hash)] +pub enum LeftJoinAlgorithm { + HashBuildRightProbeLeft { keys: Vec }, +} + +impl Default for LeftJoinAlgorithm { + fn default() -> Self { + Self::HashBuildRightProbeLeft { + keys: Vec::default(), + } + } +} + +/// The left join algorithm used (c.f. [`GraphPattern::Minus`]). +#[derive(Eq, PartialEq, Debug, Clone, Hash)] +pub enum MinusAlgorithm { + HashBuildRightProbeLeft { keys: Vec }, +} + +impl Default for MinusAlgorithm { + fn default() -> Self { + Self::HashBuildRightProbeLeft { + keys: Vec::default(), + } } } diff --git a/lib/sparopt/src/optimizer.rs b/lib/sparopt/src/optimizer.rs index 91ff65d0..a141bd63 100644 --- a/lib/sparopt/src/optimizer.rs +++ b/lib/sparopt/src/optimizer.rs @@ -1,7 +1,10 @@ -use crate::algebra::{Expression, GraphPattern, JoinAlgorithm, OrderExpression}; +use crate::algebra::{ + Expression, GraphPattern, JoinAlgorithm, LeftJoinAlgorithm, MinusAlgorithm, OrderExpression, +}; use crate::type_inference::{ infer_expression_type, infer_graph_pattern_types, VariableType, VariableTypes, }; +use oxrdf::Variable; use spargebra::algebra::PropertyPathExpression; use spargebra::term::{GroundTermPattern, NamedNodePattern}; use std::cmp::{max, min}; @@ -53,6 +56,7 @@ impl Optimizer { left, right, expression, + algorithm, } => { let left = Self::normalize_pattern(*left, input_types); let right = Self::normalize_pattern(*right, input_types); @@ -62,6 +66,7 @@ impl Optimizer { left, right, Self::normalize_expression(expression, &inner_types), + algorithm, ) } #[cfg(feature = "sep-0006")] @@ -103,9 +108,14 @@ impl Optimizer { GraphPattern::extend(inner, variable, expression) } } - GraphPattern::Minus { left, right } => GraphPattern::minus( + GraphPattern::Minus { + left, + right, + algorithm, + } => GraphPattern::minus( Self::normalize_pattern(*left, input_types), Self::normalize_pattern(*right, input_types), + algorithm, ), GraphPattern::Values { variables, @@ -336,6 +346,7 @@ impl Optimizer { left, right, expression, + algorithm, } => { let left_types = infer_graph_pattern_types(&left, input_types.clone()); let right_types = infer_graph_pattern_types(&right, input_types.clone()); @@ -364,13 +375,19 @@ impl Optimizer { Self::push_filters(*left, left_filters, input_types), Self::push_filters(*right, right_filters, input_types), expression, + algorithm, ), Expression::and_all(final_filters), ) } - GraphPattern::Minus { left, right } => GraphPattern::minus( + GraphPattern::Minus { + left, + right, + algorithm, + } => GraphPattern::minus( Self::push_filters(*left, filters, input_types), Self::push_filters(*right, Vec::new(), input_types), + algorithm, ), GraphPattern::Extend { inner, @@ -503,11 +520,7 @@ impl Optimizer { .enumerate() .filter_map(|(i, v)| v.then(|| i)) .filter(|i| { - count_common_variables( - &output_types, - &to_reorder_types[*i], - input_types, - ) > 0 + has_common_variables(&output_types, &to_reorder_types[*i], input_types) }) .min_by_key(|i| { // Estimation of the join cost @@ -527,10 +540,14 @@ impl Optimizer { } else { estimate_join_cost( &output, - &output_types, &to_reorder[*i], - &to_reorder_types[*i], - JoinAlgorithm::HashBuildLeftProbeRight, + &JoinAlgorithm::HashBuildLeftProbeRight { + keys: join_key_variables( + &output_types, + &to_reorder_types[*i], + input_types, + ), + }, input_types, ) } @@ -547,7 +564,13 @@ impl Optimizer { GraphPattern::join( output, next, - JoinAlgorithm::HashBuildLeftProbeRight, + JoinAlgorithm::HashBuildLeftProbeRight { + keys: join_key_variables( + &output_types, + &to_reorder_types[next_id], + input_types, + ), + }, ) }; } @@ -556,7 +579,13 @@ impl Optimizer { output = GraphPattern::join( output, next, - JoinAlgorithm::HashBuildLeftProbeRight, + JoinAlgorithm::HashBuildLeftProbeRight { + keys: join_key_variables( + &output_types, + &to_reorder_types[next_id], + input_types, + ), + }, ); } output_types.intersect_with(to_reorder_types[next_id].clone()); @@ -566,12 +595,25 @@ impl Optimizer { output_cartesian_product_joins .into_iter() .reduce(|left, right| { + let keys = join_key_variables( + &infer_graph_pattern_types(&left, input_types.clone()), + &infer_graph_pattern_types(&right, input_types.clone()), + input_types, + ); if estimate_graph_pattern_size(&left, input_types) <= estimate_graph_pattern_size(&right, input_types) { - GraphPattern::join(left, right, JoinAlgorithm::HashBuildLeftProbeRight) + GraphPattern::join( + left, + right, + JoinAlgorithm::HashBuildLeftProbeRight { keys }, + ) } else { - GraphPattern::join(right, left, JoinAlgorithm::HashBuildLeftProbeRight) + GraphPattern::join( + right, + left, + JoinAlgorithm::HashBuildLeftProbeRight { keys }, + ) } }) .unwrap() @@ -588,15 +630,16 @@ impl Optimizer { left, right, expression, + .. } => { let left = Self::reorder_joins(*left, input_types); + let left_types = infer_graph_pattern_types(&left, input_types.clone()); let right = Self::reorder_joins(*right, input_types); + let right_types = infer_graph_pattern_types(&right, input_types.clone()); #[cfg(feature = "sep-0006")] { - let left_types = infer_graph_pattern_types(&left, input_types.clone()); - let right_types = infer_graph_pattern_types(&right, input_types.clone()); if is_fit_for_for_loop_join(&right, input_types, &left_types) - && count_common_variables(&left_types, &right_types, input_types) > 0 + && has_common_variables(&left_types, &right_types, input_types) { return GraphPattern::lateral( left, @@ -604,16 +647,33 @@ impl Optimizer { GraphPattern::empty_singleton(), right, expression, + LeftJoinAlgorithm::HashBuildRightProbeLeft { keys: Vec::new() }, ), ); } } - GraphPattern::left_join(left, right, expression) + GraphPattern::left_join( + left, + right, + expression, + LeftJoinAlgorithm::HashBuildRightProbeLeft { + keys: join_key_variables(&left_types, &right_types, input_types), + }, + ) + } + GraphPattern::Minus { left, right, .. } => { + let left = Self::reorder_joins(*left, input_types); + let left_types = infer_graph_pattern_types(&left, input_types.clone()); + let right = Self::reorder_joins(*right, input_types); + let right_types = infer_graph_pattern_types(&right, input_types.clone()); + GraphPattern::minus( + left, + right, + MinusAlgorithm::HashBuildRightProbeLeft { + keys: join_key_variables(&left_types, &right_types, input_types), + }, + ) } - GraphPattern::Minus { left, right } => GraphPattern::minus( - Self::reorder_joins(*left, input_types), - Self::reorder_joins(*right, input_types), - ), GraphPattern::Extend { inner, expression, @@ -685,6 +745,7 @@ fn is_fit_for_for_loop_join( left, right, expression, + .. } => { if !is_fit_for_for_loop_join(left, global_input_types, entry_types) { return false; @@ -802,17 +863,28 @@ fn is_expression_fit_for_for_loop_join( } } -fn count_common_variables( +fn has_common_variables( left: &VariableTypes, right: &VariableTypes, input_types: &VariableTypes, -) -> usize { +) -> bool { // TODO: we should be smart and count as shared variables FILTER(?a = ?b) + left.iter().any(|(variable, left_type)| { + !left_type.undef && !right.get(variable).undef && input_types.get(variable).undef + }) +} + +fn join_key_variables( + left: &VariableTypes, + right: &VariableTypes, + input_types: &VariableTypes, +) -> Vec { left.iter() .filter(|(variable, left_type)| { !left_type.undef && !right.get(variable).undef && input_types.get(variable).undef }) - .count() + .map(|(variable, _)| variable.clone()) + .collect() } fn estimate_graph_pattern_size(pattern: &GraphPattern, input_types: &VariableTypes) -> usize { @@ -842,35 +914,26 @@ fn estimate_graph_pattern_size(pattern: &GraphPattern, input_types: &VariableTyp left, right, algorithm, - } => { - let left_types = infer_graph_pattern_types(left, input_types.clone()); - let right_types = infer_graph_pattern_types(right, input_types.clone()); - estimate_join_cost( - left, - &left_types, - right, - &right_types, - *algorithm, - input_types, - ) - } - GraphPattern::LeftJoin { left, right, .. } => { - let left_size = estimate_graph_pattern_size(left, input_types); - let left_types = infer_graph_pattern_types(left, input_types.clone()); - let right_types = infer_graph_pattern_types(right, input_types.clone()); - max( - left_size, - left_size - .saturating_mul(estimate_graph_pattern_size(right, &right_types)) - .saturating_div( - 1_000_usize.saturating_pow( - count_common_variables(&left_types, &right_types, input_types) - .try_into() - .unwrap(), - ), - ), - ) - } + } => estimate_join_cost(left, right, algorithm, input_types), + GraphPattern::LeftJoin { + left, + right, + algorithm, + .. + } => match algorithm { + LeftJoinAlgorithm::HashBuildRightProbeLeft { keys } => { + let left_size = estimate_graph_pattern_size(left, input_types); + max( + left_size, + left_size + .saturating_mul(estimate_graph_pattern_size( + right, + &infer_graph_pattern_types(right, input_types.clone()), + )) + .saturating_div(1_000_usize.saturating_pow(keys.len().try_into().unwrap())), + ) + } + }, #[cfg(feature = "sep-0006")] GraphPattern::Lateral { left, right } => estimate_lateral_cost( left, @@ -908,22 +971,16 @@ fn estimate_graph_pattern_size(pattern: &GraphPattern, input_types: &VariableTyp fn estimate_join_cost( left: &GraphPattern, - left_types: &VariableTypes, right: &GraphPattern, - right_types: &VariableTypes, - algorithm: JoinAlgorithm, + algorithm: &JoinAlgorithm, input_types: &VariableTypes, ) -> usize { match algorithm { - JoinAlgorithm::HashBuildLeftProbeRight => estimate_graph_pattern_size(left, input_types) - .saturating_mul(estimate_graph_pattern_size(right, input_types)) - .saturating_div( - 1_000_usize.saturating_pow( - count_common_variables(left_types, right_types, input_types) - .try_into() - .unwrap(), - ), - ), + JoinAlgorithm::HashBuildLeftProbeRight { keys } => { + estimate_graph_pattern_size(left, input_types) + .saturating_mul(estimate_graph_pattern_size(right, input_types)) + .saturating_div(1_000_usize.saturating_pow(keys.len().try_into().unwrap())) + } } } fn estimate_lateral_cost( diff --git a/lib/sparopt/src/type_inference.rs b/lib/sparopt/src/type_inference.rs index 421fd756..03960adb 100644 --- a/lib/sparopt/src/type_inference.rs +++ b/lib/sparopt/src/type_inference.rs @@ -124,11 +124,20 @@ pub fn infer_graph_pattern_types( } types } - GraphPattern::Service { name, inner, .. } => { + GraphPattern::Service { + name, + inner, + silent, + } => { + let parent_types = types.clone(); let mut types = infer_graph_pattern_types(inner, types); if let NamedNodePattern::Variable(v) = name { types.intersect_variable_with(v.clone(), VariableType::NAMED_NODE) } + if *silent { + // On failure, single empty solution + types.union_with(parent_types); + } types } } diff --git a/lib/src/sparql/eval.rs b/lib/src/sparql/eval.rs index 8ab0d8ba..06372e25 100644 --- a/lib/src/sparql/eval.rs +++ b/lib/src/sparql/eval.rs @@ -380,12 +380,9 @@ impl SimpleEvaluator { PlanNode::HashJoin { probe_child, build_child, + keys, } => { - let join_keys: Vec<_> = probe_child - .always_bound_variables() - .intersection(&build_child.always_bound_variables()) - .copied() - .collect(); + let join_keys = keys.iter().map(|v| v.encoded).collect::>(); let (probe, probe_stats) = self.plan_evaluator(probe_child); stat_children.push(probe_stats); let (build, build_stats) = self.plan_evaluator(build_child); @@ -444,12 +441,8 @@ impl SimpleEvaluator { })) }) } - PlanNode::AntiJoin { left, right } => { - let join_keys: Vec<_> = left - .always_bound_variables() - .intersection(&right.always_bound_variables()) - .copied() - .collect(); + PlanNode::AntiJoin { left, right, keys } => { + let join_keys = keys.iter().map(|v| v.encoded).collect::>(); let (left, left_stats) = self.plan_evaluator(left); stat_children.push(left_stats); let (right, right_stats) = self.plan_evaluator(right); @@ -487,12 +480,9 @@ impl SimpleEvaluator { left, right, expression, + keys, } => { - let join_keys: Vec<_> = left - .always_bound_variables() - .intersection(&right.always_bound_variables()) - .copied() - .collect(); + let join_keys = keys.iter().map(|v| v.encoded).collect::>(); let (left, left_stats) = self.plan_evaluator(left); stat_children.push(left_stats); let (right, right_stats) = self.plan_evaluator(right); diff --git a/lib/src/sparql/plan.rs b/lib/src/sparql/plan.rs index a447fb01..fd9fb989 100644 --- a/lib/src/sparql/plan.rs +++ b/lib/src/sparql/plan.rs @@ -4,9 +4,7 @@ use crate::storage::numeric_encoder::EncodedTerm; use regex::Regex; use spargebra::algebra::GraphPattern; use spargebra::term::GroundTerm; -use std::cmp::max; -use std::collections::btree_map::Entry; -use std::collections::{BTreeMap, BTreeSet}; +use std::collections::BTreeSet; use std::fmt; use std::rc::Rc; @@ -40,6 +38,7 @@ pub enum PlanNode { HashJoin { probe_child: Rc, build_child: Rc, + keys: Vec, }, /// Right nested in left loop ForLoopJoin { @@ -50,6 +49,7 @@ pub enum PlanNode { AntiJoin { left: Rc, right: Rc, + keys: Vec, }, Filter { child: Rc, @@ -63,6 +63,7 @@ pub enum PlanNode { left: Rc, right: Rc, expression: Box, + keys: Vec, }, /// right nested in left loop ForLoopLeftJoin { @@ -159,9 +160,10 @@ impl PlanNode { Self::HashJoin { probe_child: left, build_child: right, + .. } | Self::ForLoopJoin { left, right, .. } - | Self::AntiJoin { left, right } + | Self::AntiJoin { left, right, .. } | Self::ForLoopLeftJoin { left, right, .. } => { left.lookup_used_variables(callback); right.lookup_used_variables(callback); @@ -170,6 +172,7 @@ impl PlanNode { left, right, expression, + .. } => { left.lookup_used_variables(callback); right.lookup_used_variables(callback); @@ -219,134 +222,6 @@ impl PlanNode { } } } - - /// Returns subset of the set of variables that are always bound in the result set - /// - /// (subset because this function is not perfect yet) - pub fn always_bound_variables(&self) -> BTreeSet { - let mut set = BTreeSet::default(); - self.lookup_always_bound_variables(&mut |v| { - set.insert(v); - }); - set - } - - pub fn lookup_always_bound_variables(&self, callback: &mut impl FnMut(usize)) { - match self { - Self::StaticBindings { encoded_tuples, .. } => { - let mut variables = BTreeMap::default(); // value true iff always bound - let max_tuple_length = encoded_tuples - .iter() - .map(EncodedTuple::capacity) - .fold(0, max); - for tuple in encoded_tuples { - for key in 0..max_tuple_length { - match variables.entry(key) { - Entry::Vacant(e) => { - e.insert(tuple.contains(key)); - } - Entry::Occupied(mut e) => { - if !tuple.contains(key) { - e.insert(false); - } - } - } - } - } - for (k, v) in variables { - if v { - callback(k); - } - } - } - Self::QuadPattern { - subject, - predicate, - object, - graph_name, - } => { - subject.lookup_variables(callback); - predicate.lookup_variables(callback); - object.lookup_variables(callback); - graph_name.lookup_variables(callback); - } - Self::PathPattern { - subject, - object, - graph_name, - .. - } => { - subject.lookup_variables(callback); - object.lookup_variables(callback); - graph_name.lookup_variables(callback); - } - Self::Filter { child, .. } => { - //TODO: have a look at the expression to know if it filters out unbound variables - child.lookup_always_bound_variables(callback); - } - Self::Union { children } => { - if let Some(vars) = children - .iter() - .map(|c| c.always_bound_variables()) - .reduce(|a, b| a.intersection(&b).copied().collect()) - { - for v in vars { - callback(v); - } - } - } - Self::HashJoin { - probe_child: left, - build_child: right, - } - | Self::ForLoopJoin { left, right, .. } => { - left.lookup_always_bound_variables(callback); - right.lookup_always_bound_variables(callback); - } - Self::AntiJoin { left, .. } - | Self::HashLeftJoin { left, .. } - | Self::ForLoopLeftJoin { left, .. } => { - left.lookup_always_bound_variables(callback); - } - Self::Extend { - child, - variable, - expression, - } => { - if matches!( - expression.as_ref(), - PlanExpression::NamedNode(_) | PlanExpression::Literal(_) - ) { - // TODO: more cases? - callback(variable.encoded); - } - child.lookup_always_bound_variables(callback); - } - Self::Sort { child, .. } - | Self::HashDeduplicate { child } - | Self::Reduced { child } - | Self::Skip { child, .. } - | Self::Limit { child, .. } => child.lookup_always_bound_variables(callback), - Self::Service { child, silent, .. } => { - if *silent { - // none, might return a null tuple - } else { - child.lookup_always_bound_variables(callback) - } - } - Self::Project { mapping, child } => { - let child_bound = child.always_bound_variables(); - for (child_i, output_i) in mapping.iter() { - if child_bound.contains(&child_i.encoded) { - callback(output_i.encoded); - } - } - } - Self::Aggregate { .. } => { - //TODO - } - } - } } #[derive(Debug, Clone)] diff --git a/lib/src/sparql/plan_builder.rs b/lib/src/sparql/plan_builder.rs index ec93bb7f..12faeb6c 100644 --- a/lib/src/sparql/plan_builder.rs +++ b/lib/src/sparql/plan_builder.rs @@ -106,25 +106,37 @@ impl<'a> PlanBuilder<'a> { right, algorithm, } => match algorithm { - JoinAlgorithm::HashBuildLeftProbeRight => PlanNode::HashJoin { + JoinAlgorithm::HashBuildLeftProbeRight { keys } => PlanNode::HashJoin { build_child: Rc::new(self.build_for_graph_pattern(left, variables)?), probe_child: Rc::new(self.build_for_graph_pattern(right, variables)?), + keys: keys + .iter() + .map(|v| build_plan_variable(variables, v)) + .collect(), }, }, GraphPattern::LeftJoin { left, right, expression, - } => PlanNode::HashLeftJoin { - left: Rc::new(self.build_for_graph_pattern(left, variables)?), - right: Rc::new(self.build_for_graph_pattern(right, variables)?), - expression: Box::new(self.build_for_expression(expression, variables)?), + algorithm, + } => match algorithm { + LeftJoinAlgorithm::HashBuildRightProbeLeft { keys } => PlanNode::HashLeftJoin { + left: Rc::new(self.build_for_graph_pattern(left, variables)?), + right: Rc::new(self.build_for_graph_pattern(right, variables)?), + expression: Box::new(self.build_for_expression(expression, variables)?), + keys: keys + .iter() + .map(|v| build_plan_variable(variables, v)) + .collect(), + }, }, GraphPattern::Lateral { left, right } => { if let GraphPattern::LeftJoin { left: nested_left, right: nested_right, expression, + .. } = right.as_ref() { if nested_left.is_empty_singleton() { @@ -167,9 +179,19 @@ impl<'a> PlanBuilder<'a> { variable: build_plan_variable(variables, variable), expression: Box::new(self.build_for_expression(expression, variables)?), }, - GraphPattern::Minus { left, right } => PlanNode::AntiJoin { - left: Rc::new(self.build_for_graph_pattern(left, variables)?), - right: Rc::new(self.build_for_graph_pattern(right, variables)?), + GraphPattern::Minus { + left, + right, + algorithm, + } => match algorithm { + MinusAlgorithm::HashBuildRightProbeLeft { keys } => PlanNode::AntiJoin { + left: Rc::new(self.build_for_graph_pattern(left, variables)?), + right: Rc::new(self.build_for_graph_pattern(right, variables)?), + keys: keys + .iter() + .map(|v| build_plan_variable(variables, v)) + .collect(), + }, }, GraphPattern::Service { name,