Skip to content

Add GPML logical plan and plan -> evaluator. #549

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 26, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion partiql-eval/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,11 @@ partiql-logical-planner = { path = "../partiql-logical-planner", version = "0.11
default = []
serde = [
"dep:serde",
"rust_decimal/serde-with-str",
"partiql-common/serde",
"partiql-logical/serde",
"partiql-value/serde",
"partiql-extension-ion/serde",
"rust_decimal/serde-with-str",
]

[[bench]]
Expand Down
343 changes: 343 additions & 0 deletions partiql-eval/src/eval/expr/graph_match.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,343 @@
use crate::eval::eval_expr_wrapper::UnaryValueExpr;
use crate::eval::expr::{BindError, BindEvalExpr, EvalExpr};
use crate::eval::graph::evaluator::GraphEvaluator;
use crate::eval::graph::simple_graph::engine::SimpleGraphEngine;

use crate::eval::graph::plan::PathPatternMatch;
use crate::eval::graph::string_graph::StringGraphTypes;
use partiql_types::{type_graph, PartiqlNoIdShapeBuilder};
use partiql_value::Value::Missing;
use partiql_value::{Graph, Value};

/// Represents an evaluation `MATCH` operator, e.g. in `graph MATCH () -> ()'`.
#[derive(Debug)]
pub(crate) struct EvalGraphMatch {
pub(crate) pattern: PathPatternMatch<StringGraphTypes>,
}

impl EvalGraphMatch {
pub(crate) fn new(pattern: PathPatternMatch<StringGraphTypes>) -> Self {
EvalGraphMatch { pattern }
}
}

impl BindEvalExpr for EvalGraphMatch {
fn bind<const STRICT: bool>(
self,
args: Vec<Box<dyn EvalExpr>>,
) -> Result<Box<dyn EvalExpr>, BindError> {
// use DummyShapeBuilder, as we don't care about shape Ids for evaluation dispatch
let mut bld = PartiqlNoIdShapeBuilder::default();
UnaryValueExpr::create_typed::<{ STRICT }, _>([type_graph!(bld)], args, move |value| {
match value {
Value::Graph(graph) => match graph.as_ref() {
Graph::Simple(g) => {
let engine = SimpleGraphEngine::new(g.clone());
let ge = GraphEvaluator::new(engine);
ge.eval(&self.pattern)
}
},
_ => Missing,
}
})
}
}

#[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,
};
use crate::eval::graph::string_graph::StringGraphTypes;
use crate::eval::{BasicContext, MapBindings};
use crate::test_value::TestValue;
use partiql_catalog::context::SystemContext;
use partiql_common::pretty::ToPretty;

use partiql_value::{tuple, BindingsName, DateTime, Value};

/*
A simple 3-node, 3-edge graph which is intended to be able to be exactly matched by:
```(graph MATCH
(n1:a WHERE n1 == 1) -[e12:e WHERE e12 == 1.2]-> (n2),
(n2:b WHERE n2 == 2) -[e23:d WHERE e23 == 2.3]-> (n3),
(n3:a WHERE n3 == 3) ~[e_u:self WHERE e_u == <<>>]~ (n3)
)```
*/
fn graph() -> Value {
let graph = r##"
$graph::{
nodes: [ {id: n1, labels: ["a"], payload: 1},
{id: n2, labels: ["b"], payload: 2},
{id: n3, labels: ["a"], payload: 3} ],
edges: [ {id: e12, labels: ["e"], payload: 1.2, ends: (n1 -> n2) },
{id: e23, labels: ["d"], payload: 2.3, ends: (n2 -> n3) },
{id: e_u, labels: ["self"], payload: $bag::[] , ends: (n3 -- n3) } ]
}
"##;
TestValue::from(graph).value
}

fn bindings() -> MapBindings<Value> {
let mut bindings: MapBindings<Value> = MapBindings::default();
bindings.insert("graph", graph());
bindings
}

fn context() -> BasicContext<'static> {
let sys = SystemContext {
now: DateTime::from_system_now_utc(),
};
let ctx = BasicContext::new(bindings(), sys);
ctx
}

fn graph_reference() -> Box<EvalGlobalVarRef> {
Box::new(EvalGlobalVarRef {
name: BindingsName::CaseInsensitive("graph".to_string().into()),
})
}

#[track_caller]
fn test_graph(matcher: PathPatternMatch<StringGraphTypes>, expected: &'static str) {
let eval = EvalGraphMatch::new(matcher)
.bind::<false>(vec![graph_reference()])
.expect("graph match bind");

let bindings = tuple![("graph", graph())];
let ctx = context();
let res = eval.evaluate(&bindings, &ctx);
let expected = crate::test_value::parse_partiql_value_str(expected);

let pretty = |v: &Value| v.to_pretty_string(80).unwrap();

assert_eq!(pretty(&expected), pretty(res.as_ref()));
}

#[test]
fn node() {
// Query: (graph MATCH (x:a))
let binder = BindSpec("x".to_string());
let spec = NodeFilter::labeled("a".to_string());
let matcher = NodeMatch { binder, spec };

test_graph(matcher.into(), "<< { 'x': 1 }, { 'x': 3 } >>")
}

#[test]
fn no_edge_matches() {
let fresh = FreshBinder::default();

// Query: (graph MATCH () -[e:foo]- ())
let binders = (
BindSpec(fresh.node()),
BindSpec("e".to_string()),
BindSpec(fresh.node()),
);
let spec = StepFilter {
dir: DirectionFilter::LUR,
triple: TripleFilter {
lhs: NodeFilter::any(),
e: EdgeFilter::labeled("foo".to_string()),
rhs: NodeFilter::any(),
},
};

let matcher: PathMatch<StringGraphTypes> = PathMatch { binders, spec };

test_graph(matcher.into(), "<< >>")
}

#[test]
fn no_node_matches() {
let fresh = FreshBinder::default();

// Query: (graph MATCH (:foo) -[]- ())
let binders = (
BindSpec(fresh.node()),
BindSpec(fresh.node()),
BindSpec(fresh.node()),
);
let spec = StepFilter {
dir: DirectionFilter::LUR,
triple: TripleFilter {
lhs: NodeFilter::labeled("foo".to_string()),
e: EdgeFilter::any(),
rhs: NodeFilter::any(),
},
};

let matcher: PathMatch<StringGraphTypes> = PathMatch { binders, spec };

test_graph(matcher.into(), "<< >>")
}

#[test]
fn node_edge_node() {
// Query: (graph MATCH (x)<-[z:e]-(y))
let binders = (
BindSpec("x".to_string()),
BindSpec("z".to_string()),
BindSpec("y".to_string()),
);
let spec = StepFilter {
dir: DirectionFilter::L,
triple: TripleFilter {
lhs: NodeFilter::any(),
e: EdgeFilter::labeled("e".to_string()),
rhs: NodeFilter::any(),
},
};

let matcher: PathMatch<StringGraphTypes> = PathMatch { binders, spec };

test_graph(matcher.into(), "<< {'x': 2, 'z': 1.2, 'y': 1} >>")
}

#[test]
fn edge() {
let fresh = FreshBinder::default();

// Query: (graph MATCH -> )
let binders = (
BindSpec(fresh.node()),
BindSpec(fresh.edge()),
BindSpec(fresh.node()),
);
let spec = StepFilter {
dir: DirectionFilter::R,
triple: TripleFilter {
lhs: NodeFilter::any(),
e: EdgeFilter::any(),
rhs: NodeFilter::any(),
},
};

let matcher: PathMatch<StringGraphTypes> = PathMatch { binders, spec };

test_graph(matcher.into(), "<< { }, { } >>")
}

#[test]
fn edge_outgoing() {
let fresh = FreshBinder::default();

// Query: (graph MATCH <-[z]-> )
let binders = (
BindSpec(fresh.node()),
BindSpec("z".to_string()),
BindSpec(fresh.node()),
);
let spec = StepFilter {
dir: DirectionFilter::LR,
triple: TripleFilter {
lhs: NodeFilter::any(),
e: EdgeFilter::any(),
rhs: NodeFilter::any(),
},
};

let matcher: PathMatch<StringGraphTypes> = PathMatch { binders, spec };

test_graph(
matcher.into(),
"<< { 'z': 1.2 }, { 'z': 1.2 }, { 'z': 2.3 }, { 'z': 2.3 } >>",
)
}

#[test]
fn n_e_n_e_n() {
// Query: (graph MATCH (x:b)-[z1]-(y1:a)-[z2]-(y2:b) )
let binders = (
BindSpec("x".to_string()),
BindSpec("z1".to_string()),
BindSpec("y1".to_string()),
);
let spec = StepFilter {
dir: DirectionFilter::LUR,
triple: TripleFilter {
lhs: NodeFilter::labeled("b".to_string()),
e: EdgeFilter::any(),
rhs: NodeFilter::labeled("a".to_string()),
},
};
let matcher1: PathMatch<StringGraphTypes> = PathMatch { binders, spec };

let binders = (
BindSpec("y1".to_string()),
BindSpec("z2".to_string()),
BindSpec("y2".to_string()),
);
let spec = StepFilter {
dir: DirectionFilter::LUR,
triple: TripleFilter {
lhs: NodeFilter::labeled("a".to_string()),
e: EdgeFilter::any(),
rhs: NodeFilter::labeled("b".to_string()),
},
};
let matcher2: PathMatch<StringGraphTypes> = PathMatch { binders, spec };

let pattern_match = PathPatternMatch::Concat(vec![
PathPatternMatch::Match(matcher1),
PathPatternMatch::Match(matcher2),
]);

test_graph(
pattern_match,
"<< { 'x': 2, 'z1': 1.2, 'y1': 1, 'z2': 1.2, 'y2': 2 }, \
{ 'x': 2, 'z1': 2.3, 'y1': 3, 'z2': 2.3, 'y2': 2 } >>",
)
}
#[test]
fn cycle() {
let fresh = FreshBinder::default();

// Query: (graph MATCH (x1) - (x2) - (x1))
let binders = (
BindSpec("x1".to_string()),
BindSpec(fresh.edge()),
BindSpec("x2".to_string()),
);
let spec = StepFilter {
dir: DirectionFilter::LUR,
triple: TripleFilter {
lhs: NodeFilter::any(),
e: EdgeFilter::any(),
rhs: NodeFilter::any(),
},
};
let matcher1: PathMatch<StringGraphTypes> = PathMatch { binders, spec };

let binders = (
BindSpec("x2".to_string()),
BindSpec(fresh.edge()),
BindSpec("x1".to_string()),
);
let spec = StepFilter {
dir: DirectionFilter::LUR,
triple: TripleFilter {
lhs: NodeFilter::any(),
e: EdgeFilter::any(),
rhs: NodeFilter::any(),
},
};
let matcher2: PathMatch<StringGraphTypes> = PathMatch { binders, spec };

let pattern_match = PathPatternMatch::Concat(vec![
PathPatternMatch::Match(matcher1),
PathPatternMatch::Match(matcher2),
]);
test_graph(
pattern_match,
"<< { 'x1': 3, 'x2': 3 }, \
{ 'x1': 1, 'x2': 2 }, \
{ 'x1': 2, 'x2': 1 }, \
{ 'x1': 2, 'x2': 3 }, \
{ 'x1': 3, 'x2': 2 } >>",
)
}
}
3 changes: 3 additions & 0 deletions partiql-eval/src/eval/expr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ mod path;
pub(crate) use path::*;
mod pattern_match;
pub(crate) use pattern_match::*;

mod graph_match;
pub(crate) use graph_match::*;
mod functions;
mod operators;

Expand Down
2 changes: 1 addition & 1 deletion partiql-eval/src/eval/graph/bind_name.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::sync::atomic::AtomicU32;
use std::sync::atomic::Ordering::Relaxed;

/// A unicode non-character prefixed onto 'anonymous' bind names
/// A Unicode non-character prefixed onto 'anonymous' bind names
const ANON_PREFIX: char = '\u{FDD0}';

pub trait BindNameExt {
Expand Down
2 changes: 1 addition & 1 deletion partiql-eval/src/eval/graph/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::eval::graph::plan::{
DirectionFilter, GraphPlanConvert, NodeFilter, StepFilter, TripleFilter,
};
use crate::eval::graph::result::Triple;
use crate::eval::graph::string_graph::types::StringGraphTypes;
use crate::eval::graph::string_graph::StringGraphTypes;
use crate::eval::graph::types::GraphTypes;
use partiql_value::Value;

Expand Down
Loading
Loading