From cd77219617e411ff8b7fb5e2fdf8cbf8fe856ba2 Mon Sep 17 00:00:00 2001 From: Josh Pschorr Date: Tue, 25 Mar 2025 10:28:16 -0700 Subject: [PATCH 1/3] Add lowering from AST to logical plan for graph MATCH --- partiql-ast/src/ast.rs | 1 + partiql-eval/src/eval/expr/graph_match.rs | 56 +++- partiql-eval/src/eval/graph/evaluator.rs | 19 +- partiql-eval/src/eval/graph/mod.rs | 1 - partiql-eval/src/eval/graph/plan.rs | 74 ----- partiql-eval/src/plan.rs | 50 +-- partiql-logical-planner/src/graph.rs | 294 ++++++++++++++++++ partiql-logical-planner/src/lib.rs | 1 + partiql-logical-planner/src/lower.rs | 25 +- .../src}/graph/bind_name.rs | 5 +- .../src/{graph.rs => graph/mod.rs} | 8 +- partiql-logical/src/lib.rs | 5 +- 12 files changed, 419 insertions(+), 120 deletions(-) create mode 100644 partiql-logical-planner/src/graph.rs rename {partiql-eval/src/eval => partiql-logical/src}/graph/bind_name.rs (74%) rename partiql-logical/src/{graph.rs => graph/mod.rs} (96%) diff --git a/partiql-ast/src/ast.rs b/partiql-ast/src/ast.rs index 2c26f56a..4539d812 100644 --- a/partiql-ast/src/ast.rs +++ b/partiql-ast/src/ast.rs @@ -945,6 +945,7 @@ pub struct GraphMatchEdge { #[derive(Visit, Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct GraphMatchPattern { + /// an optional restrictor for the entire pattern match #[visit(skip)] pub restrictor: Option, /// an optional quantifier for the entire pattern match diff --git a/partiql-eval/src/eval/expr/graph_match.rs b/partiql-eval/src/eval/expr/graph_match.rs index 82e7f95b..58ba2d14 100644 --- a/partiql-eval/src/eval/expr/graph_match.rs +++ b/partiql-eval/src/eval/expr/graph_match.rs @@ -46,19 +46,67 @@ impl BindEvalExpr for EvalGraphMatch { #[cfg(test)] mod tests { use crate::eval::expr::{BindEvalExpr, EvalGlobalVarRef, EvalGraphMatch}; - use crate::eval::graph::bind_name::FreshBinder; use crate::eval::graph::plan::{ - BindSpec, DirectionFilter, EdgeFilter, ElementFilterBuilder, NodeFilter, NodeMatch, - PathMatch, PathPatternMatch, StepFilter, TripleFilter, + BindSpec, DirectionFilter, EdgeFilter, LabelFilter, NodeFilter, NodeMatch, PathMatch, + PathPatternMatch, StepFilter, TripleFilter, ValueFilter, }; use crate::eval::graph::string_graph::StringGraphTypes; + use crate::eval::graph::types::GraphTypes; use crate::eval::{BasicContext, MapBindings}; use crate::test_value::TestValue; use partiql_catalog::context::SystemContext; use partiql_common::pretty::ToPretty; - + use partiql_logical::graph::bind_name::FreshBinder; use partiql_value::{tuple, BindingsName, DateTime, Value}; + impl From> for PathPatternMatch { + fn from(value: PathMatch) -> Self { + Self::Match(value) + } + } + + impl From> for PathPatternMatch { + fn from(value: NodeMatch) -> Self { + Self::Node(value) + } + } + + pub trait ElementFilterBuilder { + fn any() -> Self; + fn labeled(label: GT::Label) -> Self; + } + + impl ElementFilterBuilder for NodeFilter { + fn any() -> Self { + Self { + label: LabelFilter::Always, + filter: ValueFilter::Always, + } + } + + fn labeled(label: GT::Label) -> Self { + Self { + label: LabelFilter::Named(label), + filter: ValueFilter::Always, + } + } + } + + impl ElementFilterBuilder for EdgeFilter { + fn any() -> Self { + Self { + label: LabelFilter::Always, + filter: ValueFilter::Always, + } + } + fn labeled(label: GT::Label) -> Self { + Self { + label: LabelFilter::Named(label), + filter: ValueFilter::Always, + } + } + } + /* A simple 3-node, 3-edge graph which is intended to be able to be exactly matched by: ```(graph MATCH diff --git a/partiql-eval/src/eval/graph/evaluator.rs b/partiql-eval/src/eval/graph/evaluator.rs index ebbfd0dc..6fd1c6f6 100644 --- a/partiql-eval/src/eval/graph/evaluator.rs +++ b/partiql-eval/src/eval/graph/evaluator.rs @@ -1,8 +1,8 @@ -use crate::eval::graph::bind_name::BindNameExt; use crate::eval::graph::engine::GraphEngine; use crate::eval::graph::result::{ - GraphElement, NodeBinding, PathBinding, PathPatternBinding, PathPatternNodes, + GraphElement, NodeBinding, PathBinding, PathPatternBinding, PathPatternNodes, Triple, }; +use partiql_logical::graph::bind_name::BindNameExt; use fxhash::FxBuildHasher; use indexmap::IndexMap; @@ -92,7 +92,20 @@ impl> GraphEvaluator { fn eval_path_pattern(&self, matcher: PathPatternMatch) -> PathPatternBinding { match matcher { PathPatternMatch::Node(n) => self.eval_node(n).into(), - PathPatternMatch::Match(m) => self.eval_path(m).into(), + PathPatternMatch::Match(m) => { + let PathBinding { + matcher, + mut bindings, + } = self.eval_path(m); + + // if edge is cyclic, filter triples + let (n1, _, n2) = &matcher.binders; + if n1 == n2 { + bindings.retain(|Triple { lhs, e: _, rhs }| lhs == rhs) + } + + PathBinding { matcher, bindings }.into() + } PathPatternMatch::Concat(ms) => ms .into_iter() .map(|p| self.eval_path_pattern(p)) diff --git a/partiql-eval/src/eval/graph/mod.rs b/partiql-eval/src/eval/graph/mod.rs index 1fc838a7..377be62f 100644 --- a/partiql-eval/src/eval/graph/mod.rs +++ b/partiql-eval/src/eval/graph/mod.rs @@ -1,4 +1,3 @@ -pub(crate) mod bind_name; pub(crate) mod engine; pub(crate) mod evaluator; pub(crate) mod plan; diff --git a/partiql-eval/src/eval/graph/plan.rs b/partiql-eval/src/eval/graph/plan.rs index 47bcb2c6..b3fa5afc 100644 --- a/partiql-eval/src/eval/graph/plan.rs +++ b/partiql-eval/src/eval/graph/plan.rs @@ -3,7 +3,6 @@ use std::fmt::Debug; use std::hash::Hash; /// A plan specification for an edge's direction filtering. -#[allow(dead_code)] // TODO remove once graph planning is implemented #[allow(clippy::upper_case_acronyms)] #[derive(Debug, Clone, Copy)] pub enum DirectionFilter { @@ -66,14 +65,6 @@ pub struct StepFilter { pub triple: TripleFilter, } -/// A plan specification for 'path patterns' (i.e., sequences of 'node edge node's) matching. -#[allow(dead_code)] // TODO remove once graph planning is implemented -#[derive(Debug, Clone)] -pub struct PathPatternFilter { - pub head: NodeFilter, - pub tail: Vec<(DirectionFilter, EdgeFilter, NodeFilter)>, -} - /// A plan specification for node matching. #[derive(Debug, Clone)] pub struct NodeMatch { @@ -81,14 +72,6 @@ pub struct NodeMatch { pub spec: NodeFilter, } -/// A plan specification for edge matching. -#[allow(dead_code)] // TODO remove once graph planning is implemented -#[derive(Debug, Clone)] -pub struct EdgeMatch { - pub binder: BindSpec, - pub spec: EdgeFilter, -} - /// A plan specification for path (i.e., node, edge, node) matching. #[derive(Debug, Clone)] pub struct PathMatch { @@ -97,7 +80,6 @@ pub struct PathMatch { } /// A plan specification for path patterns (i.e., sequences of [`PathMatch`]s) matching. -#[allow(dead_code)] // TODO remove once graph planning is implemented #[derive(Debug, Clone)] pub enum PathPatternMatch { Node(NodeMatch), @@ -105,55 +87,6 @@ pub enum PathPatternMatch { Concat(Vec>), } -impl From> for PathPatternMatch { - fn from(value: PathMatch) -> Self { - Self::Match(value) - } -} - -impl From> for PathPatternMatch { - fn from(value: NodeMatch) -> Self { - Self::Node(value) - } -} - -#[allow(dead_code)] // TODO remove once graph planning is implemented -pub trait ElementFilterBuilder { - fn any() -> Self; - fn labeled(label: GT::Label) -> Self; -} - -impl ElementFilterBuilder for NodeFilter { - fn any() -> Self { - Self { - label: LabelFilter::Always, - filter: ValueFilter::Always, - } - } - - fn labeled(label: GT::Label) -> Self { - Self { - label: LabelFilter::Named(label), - filter: ValueFilter::Always, - } - } -} - -impl ElementFilterBuilder for EdgeFilter { - fn any() -> Self { - Self { - label: LabelFilter::Always, - filter: ValueFilter::Always, - } - } - fn labeled(label: GT::Label) -> Self { - Self { - label: LabelFilter::Named(label), - filter: ValueFilter::Always, - } - } -} - /// A trait for converting between plans parameterized by different [`GraphTypes`] pub trait GraphPlanConvert: Debug { fn convert_pathpattern_match(&self, matcher: &PathPatternMatch) -> PathPatternMatch { @@ -211,13 +144,6 @@ pub trait GraphPlanConvert: Debug { } } - #[allow(dead_code)] // TODO remove once graph planning is implemented - fn convert_edge_match(&self, edge: &EdgeMatch) -> EdgeMatch { - EdgeMatch { - binder: self.convert_binder(&edge.binder), - spec: self.convert_edge_filter(&edge.spec), - } - } fn convert_label_filter(&self, node: &LabelFilter) -> LabelFilter; fn convert_binder(&self, binder: &BindSpec) -> BindSpec; } diff --git a/partiql-eval/src/plan.rs b/partiql-eval/src/plan.rs index 22d70b85..9e0fd870 100644 --- a/partiql-eval/src/plan.rs +++ b/partiql-eval/src/plan.rs @@ -794,37 +794,37 @@ impl<'c> EvaluatorPlanner<'c> { } fn plan_graph_plan( - pattern: &partiql_logical::PathPatternMatch, + pattern: &logical::graph::PathPatternMatch, ) -> Result, PlanningError> { use eval::graph::plan as physical; use partiql_logical as logical; fn plan_bind_spec( - pattern: &logical::BindSpec, + pattern: &logical::graph::BindSpec, ) -> Result, PlanningError> { Ok(physical::BindSpec(pattern.0.clone())) } fn plan_label_filter( - pattern: &logical::LabelFilter, + pattern: &logical::graph::LabelFilter, ) -> Result, PlanningError> { Ok(match pattern { - logical::LabelFilter::Always => physical::LabelFilter::Always, - logical::LabelFilter::Never => physical::LabelFilter::Never, - logical::LabelFilter::Named(n) => physical::LabelFilter::Named(n.clone()), + logical::graph::LabelFilter::Always => physical::LabelFilter::Always, + logical::graph::LabelFilter::Never => physical::LabelFilter::Never, + logical::graph::LabelFilter::Named(n) => physical::LabelFilter::Named(n.clone()), }) } fn plan_value_filter( - pattern: &logical::ValueFilter, + pattern: &logical::graph::ValueFilter, ) -> Result { Ok(match pattern { - logical::ValueFilter::Always => physical::ValueFilter::Always, + logical::graph::ValueFilter::Always => physical::ValueFilter::Always, }) } fn plan_node_filter( - pattern: &logical::NodeFilter, + pattern: &logical::graph::NodeFilter, ) -> Result, PlanningError> { Ok(physical::NodeFilter { label: plan_label_filter(&pattern.label)?, @@ -833,7 +833,7 @@ fn plan_graph_plan( } fn plan_edge_filter( - pattern: &logical::EdgeFilter, + pattern: &logical::graph::EdgeFilter, ) -> Result, PlanningError> { Ok(physical::EdgeFilter { label: plan_label_filter(&pattern.label)?, @@ -842,16 +842,16 @@ fn plan_graph_plan( } fn plan_step_filter( - pattern: &logical::StepFilter, + pattern: &logical::graph::StepFilter, ) -> Result, PlanningError> { let dir = match pattern.dir { - logical::DirectionFilter::L => physical::DirectionFilter::L, - logical::DirectionFilter::R => physical::DirectionFilter::R, - logical::DirectionFilter::U => physical::DirectionFilter::U, - logical::DirectionFilter::LU => physical::DirectionFilter::LU, - logical::DirectionFilter::UR => physical::DirectionFilter::UR, - logical::DirectionFilter::LR => physical::DirectionFilter::LR, - logical::DirectionFilter::LUR => physical::DirectionFilter::LUR, + logical::graph::DirectionFilter::L => physical::DirectionFilter::L, + logical::graph::DirectionFilter::R => physical::DirectionFilter::R, + logical::graph::DirectionFilter::U => physical::DirectionFilter::U, + logical::graph::DirectionFilter::LU => physical::DirectionFilter::LU, + logical::graph::DirectionFilter::UR => physical::DirectionFilter::UR, + logical::graph::DirectionFilter::LR => physical::DirectionFilter::LR, + logical::graph::DirectionFilter::LUR => physical::DirectionFilter::LUR, }; Ok(physical::StepFilter { dir, @@ -860,7 +860,7 @@ fn plan_graph_plan( } fn plan_triple_filter( - pattern: &logical::TripleFilter, + pattern: &logical::graph::TripleFilter, ) -> Result, PlanningError> { Ok(physical::TripleFilter { lhs: plan_node_filter(&pattern.lhs)?, @@ -870,7 +870,7 @@ fn plan_graph_plan( } fn plan_node_match( - pattern: &logical::NodeMatch, + pattern: &logical::graph::NodeMatch, ) -> Result, PlanningError> { Ok(physical::NodeMatch { binder: plan_bind_spec(&pattern.binder)?, @@ -879,7 +879,7 @@ fn plan_graph_plan( } fn plan_path_match( - pattern: &logical::PathMatch, + pattern: &logical::graph::PathMatch, ) -> Result, PlanningError> { let (l, m, r) = &pattern.binders; let binders = (plan_bind_spec(l)?, plan_bind_spec(m)?, plan_bind_spec(r)?); @@ -890,16 +890,16 @@ fn plan_graph_plan( } fn plan_path_pattern_match( - pattern: &logical::PathPatternMatch, + pattern: &logical::graph::PathPatternMatch, ) -> Result, PlanningError> { Ok(match pattern { - logical::PathPatternMatch::Node(n) => { + logical::graph::PathPatternMatch::Node(n) => { physical::PathPatternMatch::Node(plan_node_match(n)?) } - logical::PathPatternMatch::Match(m) => { + logical::graph::PathPatternMatch::Match(m) => { physical::PathPatternMatch::Match(plan_path_match(m)?) } - logical::PathPatternMatch::Concat(ms) => { + logical::graph::PathPatternMatch::Concat(ms) => { let ms: Result, _> = ms.iter().map(plan_path_pattern_match).collect(); physical::PathPatternMatch::Concat(ms?) } diff --git a/partiql-logical-planner/src/graph.rs b/partiql-logical-planner/src/graph.rs new file mode 100644 index 00000000..d620bffb --- /dev/null +++ b/partiql-logical-planner/src/graph.rs @@ -0,0 +1,294 @@ +use num::Integer; +use partiql_ast::ast; +use partiql_ast::ast::{GraphMatchDirection, GraphMatchPatternPart}; +use partiql_logical::graph::bind_name::FreshBinder; +use partiql_logical::graph::{ + BindSpec, DirectionFilter, EdgeFilter, EdgeMatch, LabelFilter, NodeFilter, NodeMatch, + PathMatch, PathPattern, PathPatternMatch, StepFilter, TripleFilter, ValueFilter, +}; +use std::mem::take; + +#[macro_export] +macro_rules! not_yet_implemented_result { + ($msg:expr) => { + return std::result::Result::Err($msg.to_string()); + }; +} + +#[derive(Debug, Default)] +pub(crate) struct GraphToLogical { + graph_id: FreshBinder, +} + +#[derive(Debug, Clone)] +enum MatchElement { + Node(NodeMatch), + Edge(DirectionFilter, EdgeMatch), + #[allow(dead_code)] // TODO with sub-graphs in graph MATCH + Pattern(Vec), +} + +#[derive(Debug)] +struct Normalize<'a> { + head: Option, + tail: Vec<(DirectionFilter, EdgeMatch, Option)>, + fresh_binder: &'a FreshBinder, +} + +impl<'a> Normalize<'a> { + pub fn new(fresh_binder: &'a FreshBinder) -> Self { + Self { + head: Default::default(), + tail: Default::default(), + fresh_binder, + } + } + + pub fn normalize(mut self, elements: Vec) -> Result, String> { + self.do_normalize(elements) + } + + fn fresh_node(&mut self) -> NodeMatch { + NodeMatch { + binder: BindSpec(self.fresh_binder.node()), + spec: NodeFilter { + label: LabelFilter::Always, + filter: ValueFilter::Always, + }, + } + } + + fn flush(&mut self) -> Option { + if self.head.is_none() { + debug_assert!(self.tail.is_empty()); + return None; + } + + if !self.tail.is_empty() && self.tail.last().unwrap().2.is_none() { + let fresh = self.fresh_node(); + self.tail.last_mut().unwrap().2 = Some(fresh); + } + + let head = self.head.take().unwrap(); + let tail = take(&mut self.tail) + .into_iter() + .map(|(d, e, n)| (d, e, n.unwrap())) + .collect::>(); + + Some(PathPattern { head, tail }) + } + + fn do_normalize(&mut self, elements: Vec) -> Result, String> { + let mut path_patterns = vec![]; + + for elt in elements { + match elt { + MatchElement::Node(n) => { + if self.head.is_none() { + self.head = Some(n); + } else { + match self.tail.last_mut() { + Some((_td, _te, tn)) => { + if tn.is_none() { + *tn = Some(n); + } else { + todo!("Deal with adjacent nodes") + } + } + None => { + todo!("Deal with adjacent nodes") + } + } + } + } + MatchElement::Edge(d, e) => { + if self.head.is_none() { + self.head = Some(self.fresh_node()); + } + self.tail.push((d, e, None)); + } + MatchElement::Pattern(p) => { + path_patterns.extend(self.flush()); + path_patterns.extend(self.do_normalize(p)?); + } + } + } + + path_patterns.extend(self.flush()); + + Ok(path_patterns) + } +} + +impl GraphToLogical { + fn normalize(&self, elements: Vec) -> Result, String> { + Normalize::new(&self.graph_id).normalize(elements) + } + + fn expand(&self, paths: Vec) -> Result, String> { + // TODO handle expansion as described in 6.3 of https://arxiv.org/pdf/2112.06217 + // TODO this will enable alternation and quantifiers + Ok(paths) + } + + fn plan(&self, paths: Vec) -> Result { + debug_assert!(paths.len() == 1); + let path_pattern = &paths[0]; + // pattern at this point should be a node, or a series of node-[edge-node]+ + debug_assert!((1 + (2 * path_pattern.tail.len())).is_odd()); + + if path_pattern.tail.is_empty() { + Ok(PathPatternMatch::Node(path_pattern.head.clone())) + } else { + let mut paths = vec![]; + let mut head = &path_pattern.head; + for (d, e, n) in &path_pattern.tail { + let binders = (head.binder.clone(), e.binder.clone(), n.binder.clone()); + let spec = StepFilter { + dir: d.clone(), + triple: TripleFilter { + lhs: head.spec.clone(), + e: e.spec.clone(), + rhs: n.spec.clone(), + }, + }; + paths.push(PathPatternMatch::Match(PathMatch { binders, spec })); + head = n; + } + if paths.len() == 1 { + let path = paths.into_iter().next().unwrap(); + Ok(path) + } else { + Ok(PathPatternMatch::Concat(paths)) + } + } + } + pub(crate) fn plan_graph_match( + &self, + match_expr: &ast::GraphMatchExpr, + ) -> Result { + if match_expr.selector.is_some() { + not_yet_implemented_result!("MATCH expression selectors are not yet supported."); + } + + if match_expr.patterns.len() != 1 { + not_yet_implemented_result!( + "MATCH expression with multiple patterns are not yet supported." + ); + } + + let first_pattern = &match_expr.patterns[0].node; + let pattern = self.plan_graph_pattern(first_pattern)?; + let normalized = self.normalize(pattern)?; + let expanded = self.expand(normalized)?; + self.plan(expanded) + } + + fn plan_graph_pattern( + &self, + pattern: &ast::GraphMatchPattern, + ) -> Result, String> { + if pattern.restrictor.is_some() { + not_yet_implemented_result!("MATCH pattern restrictors are not yet supported."); + } + if pattern.quantifier.is_some() { + not_yet_implemented_result!("MATCH pattern quantifiers are not yet supported."); + } + if pattern.prefilter.is_some() { + not_yet_implemented_result!("MATCH pattern prefilters are not yet supported."); + } + if pattern.variable.is_some() { + not_yet_implemented_result!("MATCH pattern path variables are not yet supported."); + } + + let parts = pattern + .parts + .iter() + .map(|p| self.plan_graph_pattern_part(p)); + let result: Result, _> = parts.collect(); + + result.map(|r| r.into_iter().flatten().collect()) + } + + fn plan_graph_pattern_part( + &self, + part: &ast::GraphMatchPatternPart, + ) -> Result, String> { + match part { + GraphMatchPatternPart::Node(n) => self.plan_graph_pattern_part_node(&n.node), + GraphMatchPatternPart::Edge(e) => self.plan_graph_pattern_part_edge(&e.node), + GraphMatchPatternPart::Pattern(pattern) => self.plan_graph_pattern(&pattern.node), + } + } + + fn plan_graph_pattern_label( + &self, + label: &Option>, + ) -> Result { + match label { + None => Ok(LabelFilter::Always), + Some(labels) => { + if labels.len() != 1 { + not_yet_implemented_result!( + "MATCH expression with multiple patterns are not yet supported." + ); + } + Ok(LabelFilter::Named(labels[0].value.clone())) + } + } + } + + fn plan_graph_pattern_part_node( + &self, + node: &ast::GraphMatchNode, + ) -> Result, String> { + if node.prefilter.is_some() { + not_yet_implemented_result!("MATCH node prefilters are not yet supported."); + } + let binder = match &node.variable { + None => self.graph_id.node(), + Some(v) => v.value.clone(), + }; + let node_match = NodeMatch { + binder: BindSpec(binder), + spec: NodeFilter { + label: self.plan_graph_pattern_label(&node.label)?, + filter: ValueFilter::Always, + }, + }; + Ok(vec![MatchElement::Node(node_match)]) + } + + fn plan_graph_pattern_part_edge( + &self, + edge: &ast::GraphMatchEdge, + ) -> Result, String> { + if edge.quantifier.is_some() { + not_yet_implemented_result!("MATCH edge quantifiers are not yet supported."); + } + if edge.prefilter.is_some() { + not_yet_implemented_result!("MATCH edge prefilters are not yet supported."); + } + let direction = match &edge.direction { + GraphMatchDirection::Left => DirectionFilter::L, + GraphMatchDirection::Undirected => DirectionFilter::U, + GraphMatchDirection::Right => DirectionFilter::R, + GraphMatchDirection::LeftOrUndirected => DirectionFilter::LU, + GraphMatchDirection::UndirectedOrRight => DirectionFilter::UR, + GraphMatchDirection::LeftOrRight => DirectionFilter::LR, + GraphMatchDirection::LeftOrUndirectedOrRight => DirectionFilter::LUR, + }; + let binder = match &edge.variable { + None => self.graph_id.node(), + Some(v) => v.value.clone(), + }; + let edge_match = EdgeMatch { + binder: BindSpec(binder), + spec: EdgeFilter { + label: self.plan_graph_pattern_label(&edge.label)?, + filter: ValueFilter::Always, + }, + }; + Ok(vec![MatchElement::Edge(direction, edge_match)]) + } +} diff --git a/partiql-logical-planner/src/lib.rs b/partiql-logical-planner/src/lib.rs index a3313dcd..6851a051 100644 --- a/partiql-logical-planner/src/lib.rs +++ b/partiql-logical-planner/src/lib.rs @@ -12,6 +12,7 @@ use partiql_catalog::catalog::{Catalog, PartiqlCatalog}; mod builtins; mod functions; +mod graph; mod lower; mod typer; diff --git a/partiql-logical-planner/src/lower.rs b/partiql-logical-planner/src/lower.rs index a89c34e6..ab9dd8a4 100644 --- a/partiql-logical-planner/src/lower.rs +++ b/partiql-logical-planner/src/lower.rs @@ -15,8 +15,8 @@ use partiql_ast::ast::{ use partiql_ast::visit::{Traverse, Visit, Visitor}; use partiql_logical as logical; use partiql_logical::{ - AggregateExpression, BagExpr, BagOp, BetweenExpr, BindingsOp, IsTypeExpr, LikeMatch, - LikeNonStringNonLiteralMatch, ListExpr, LogicalPlan, OpId, PathComponent, Pattern, + AggregateExpression, BagExpr, BagOp, BetweenExpr, BindingsOp, GraphMatchExpr, IsTypeExpr, + LikeMatch, LikeNonStringNonLiteralMatch, ListExpr, LogicalPlan, OpId, PathComponent, Pattern, PatternMatchExpr, SortSpecOrder, TupleExpr, ValueExpr, VarRefType, }; use std::borrow::Cow; @@ -1896,7 +1896,26 @@ impl<'ast> Visitor<'ast> for AstToLogical<'_> { } fn enter_graph_match(&mut self, _graph_pattern: &'ast GraphMatch) -> Traverse { - not_yet_implemented_fault!(self, "MATCH expression"); + self.enter_env(); + Traverse::Continue + } + fn exit_graph_match(&mut self, graph_pattern: &'ast GraphMatch) -> Traverse { + let mut env = self.exit_env(); + true_or_fault!(self, env.len() == 1, "env.len() is not 1"); + + let value = Box::new(env.pop().unwrap()); + let graph_planner = crate::graph::GraphToLogical::default(); + match graph_planner.plan_graph_match(&graph_pattern.graph_expr.node) { + Ok(pattern) => { + self.push_vexpr(ValueExpr::GraphMatch(GraphMatchExpr { value, pattern })); + + Traverse::Continue + } + Err(e) => { + not_yet_implemented_err!(self, e); + Traverse::Stop + } + } } } diff --git a/partiql-eval/src/eval/graph/bind_name.rs b/partiql-logical/src/graph/bind_name.rs similarity index 74% rename from partiql-eval/src/eval/graph/bind_name.rs rename to partiql-logical/src/graph/bind_name.rs index 0efa47b1..7d4c8909 100644 --- a/partiql-eval/src/eval/graph/bind_name.rs +++ b/partiql-logical/src/graph/bind_name.rs @@ -15,11 +15,10 @@ impl> BindNameExt for S { } /// Creates 'fresh' bind names +#[derive(Debug)] pub struct FreshBinder { - #[allow(dead_code)] // TODO remove once graph planning is implemented node: AtomicU32, - #[allow(dead_code)] // TODO remove once graph planning is implemented edge: AtomicU32, } @@ -33,12 +32,10 @@ impl Default for FreshBinder { } impl FreshBinder { - #[allow(dead_code)] // TODO remove once graph planning is implemented pub fn node(&self) -> String { format!("{ANON_PREFIX}🞎{}", self.node.fetch_add(1, Relaxed)) } - #[allow(dead_code)] // TODO remove once graph planning is implemented pub fn edge(&self) -> String { format!("{ANON_PREFIX}⁃{}", self.edge.fetch_add(1, Relaxed)) } diff --git a/partiql-logical/src/graph.rs b/partiql-logical/src/graph/mod.rs similarity index 96% rename from partiql-logical/src/graph.rs rename to partiql-logical/src/graph/mod.rs index f0603d31..9bf84192 100644 --- a/partiql-logical/src/graph.rs +++ b/partiql-logical/src/graph/mod.rs @@ -2,6 +2,8 @@ use serde::{Deserialize, Serialize}; use std::fmt::Debug; +pub mod bind_name; + /// A plan specification for an edge's direction filtering. #[allow(clippy::upper_case_acronyms)] #[derive(Debug, Clone, Eq, PartialEq)] @@ -76,9 +78,9 @@ pub struct StepFilter { /// A plan specification for 'path patterns' (i.e., sequences of 'node edge node's) matching. #[derive(Debug, Clone, Eq, PartialEq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct PathPatternFilter { - pub head: NodeFilter, - pub tail: Vec<(DirectionFilter, EdgeFilter, NodeFilter)>, +pub struct PathPattern { + pub head: NodeMatch, + pub tail: Vec<(DirectionFilter, EdgeMatch, NodeMatch)>, } /// A plan specification for node matching. diff --git a/partiql-logical/src/lib.rs b/partiql-logical/src/lib.rs index 3e44bae7..a24c66c4 100644 --- a/partiql-logical/src/lib.rs +++ b/partiql-logical/src/lib.rs @@ -15,8 +15,7 @@ mod util; -mod graph; -pub use graph::*; +pub mod graph; use ordered_float::OrderedFloat; use partiql_common::catalog::ObjectId; @@ -609,7 +608,7 @@ pub struct LikeNonStringNonLiteralMatch { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct GraphMatchExpr { pub value: Box, - pub pattern: PathPatternMatch, + pub pattern: graph::PathPatternMatch, } #[derive(Debug, Clone, Eq, PartialEq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] From 024006de450c8cec153edb0e6c8b0e98ad3dcacd Mon Sep 17 00:00:00 2001 From: Josh Pschorr Date: Tue, 25 Mar 2025 11:05:04 -0700 Subject: [PATCH 2/3] Enable graph conformance tests --- .github/workflows/ci_build_test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci_build_test.yml b/.github/workflows/ci_build_test.yml index fd4120ce..0da7bdee 100644 --- a/.github/workflows/ci_build_test.yml +++ b/.github/workflows/ci_build_test.yml @@ -103,7 +103,7 @@ jobs: # to format and use the output. - name: Cargo Test of the conformance tests (can fail) and save to json file continue-on-error: true - run: cargo test --verbose --package partiql-conformance-tests --features "conformance_test" --release -- -Z unstable-options --format json > ${{ env.CARGO_TEST_RESULT_NAME }} + run: cargo test --verbose --package partiql-conformance-tests --features "conformance_test, experimental" --release -- -Z unstable-options --format json > ${{ env.CARGO_TEST_RESULT_NAME }} # Create a conformance report from the `cargo test` json file - run: cargo run --features report_tool --bin generate_cts_report ${{ env.CARGO_TEST_RESULT_NAME }} ${GITHUB_SHA} ${{ env.CONFORMANCE_REPORT_NAME }} # Upload conformance report for comparison with future runs From 12eab14ea4752b2f8b61c92d56b8c50329bca94e Mon Sep 17 00:00:00 2001 From: Josh Pschorr Date: Fri, 4 Apr 2025 21:21:46 -0700 Subject: [PATCH 3/3] Cleanup/Clippy --- partiql-catalog/src/scalar_fn.rs | 2 +- partiql-eval/src/eval/evaluable.rs | 2 +- partiql-logical-planner/src/builtins.rs | 4 +--- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/partiql-catalog/src/scalar_fn.rs b/partiql-catalog/src/scalar_fn.rs index d7904b43..54bd979f 100644 --- a/partiql-catalog/src/scalar_fn.rs +++ b/partiql-catalog/src/scalar_fn.rs @@ -75,7 +75,7 @@ impl ScalarFunction { pub fn vararg_scalar_fn_overloads(scalar_fn_expr: Box) -> Vec { (1..=FN_VAR_ARG_MAX) .map(|n| ScalarFnCallSpec { - input: std::iter::repeat(CallSpecArg::Positional).take(n).collect(), + input: std::iter::repeat_n(CallSpecArg::Positional, n).collect(), output: scalar_fn_expr.clone(), }) .collect() diff --git a/partiql-eval/src/eval/evaluable.rs b/partiql-eval/src/eval/evaluable.rs index 44cb3b6d..6135b1d4 100644 --- a/partiql-eval/src/eval/evaluable.rs +++ b/partiql-eval/src/eval/evaluable.rs @@ -665,7 +665,7 @@ impl Evaluable for EvalGroupBy { } EvalGroupingStrategy::GroupFull => { let mut grouped: FxHashMap = FxHashMap::default(); - let state = std::iter::repeat(None).take(self.aggs.len()).collect_vec(); + let state = std::iter::repeat_n(None, self.aggs.len()).collect_vec(); let distinct_state = std::iter::repeat_with(|| (None, FxHashMap::default())) .take(self.distinct_aggs.len()) .collect_vec(); diff --git a/partiql-logical-planner/src/builtins.rs b/partiql-logical-planner/src/builtins.rs index ce577895..9e84187d 100644 --- a/partiql-logical-planner/src/builtins.rs +++ b/partiql-logical-planner/src/builtins.rs @@ -280,9 +280,7 @@ fn function_call_def_coalesce() -> CallDef { names: vec!["coalesce"], overloads: (0..15) .map(|n| CallSpec { - input: std::iter::repeat(CallSpecArg::Positional) - .take(n) - .collect_vec(), + input: std::iter::repeat_n(CallSpecArg::Positional, n).collect_vec(), output: Box::new(|args| { logical::ValueExpr::CoalesceExpr(logical::CoalesceExpr { elements: args }) }),