diff --git a/Cargo.toml b/Cargo.toml index 2420d28..9f24b40 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ edition = "2024" [dependencies] csv = "1.4.0" +egg = "0.10.0" libc = "0.2" oxrdf = "0.3.3" oxttl = "0.2.3" diff --git a/build.rs b/build.rs index 475046e..efec3c5 100644 --- a/build.rs +++ b/build.rs @@ -74,6 +74,8 @@ fn regenerate_bindings() { .allowlist_function("GrB_Vector_extractTuples_BOOL") .allowlist_function("GrB_vxm") .allowlist_item("LAGRAPH_MSG_LEN") + .allowlist_item("RPQMatrixOp") + .allowlist_type("RPQMatrixPlan") .allowlist_type("LAGraph_Graph") .allowlist_type("LAGraph_Kind") .allowlist_function("LAGraph_CheckGraph") @@ -83,6 +85,8 @@ fn regenerate_bindings() { .allowlist_function("LAGraph_Delete") .allowlist_function("LAGraph_Cached_AT") .allowlist_function("LAGraph_MMRead") + .allowlist_function("LAGraph_RPQMatrix") + .allowlist_function("LAGraph_DestroyRpqMatrixPlan") .default_enum_style(bindgen::EnumVariation::Rust { non_exhaustive: false, }) diff --git a/src/graph/mod.rs b/src/graph/mod.rs index d096b6f..05b0125 100644 --- a/src/graph/mod.rs +++ b/src/graph/mod.rs @@ -125,6 +125,7 @@ impl Drop for LagraphGraph { unsafe impl Send for LagraphGraph {} unsafe impl Sync for LagraphGraph {} +#[derive(Debug)] pub struct GraphblasVector { pub inner: GrB_Vector, } diff --git a/src/lagraph_sys_generated.rs b/src/lagraph_sys_generated.rs index 3201d28..6be0310 100644 --- a/src/lagraph_sys_generated.rs +++ b/src/lagraph_sys_generated.rs @@ -261,3 +261,32 @@ unsafe extern "C" { msg: *mut ::std::os::raw::c_char, ) -> ::std::os::raw::c_int; } +#[repr(u32)] +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] +pub enum RPQMatrixOp { + RPQ_MATRIX_OP_LABEL = 0, + RPQ_MATRIX_OP_LOR = 1, + RPQ_MATRIX_OP_CONCAT = 2, + RPQ_MATRIX_OP_KLEENE = 3, + RPQ_MATRIX_OP_KLEENE_L = 4, + RPQ_MATRIX_OP_KLEENE_R = 5, +} +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct RPQMatrixPlan { + pub op: RPQMatrixOp, + pub lhs: *mut RPQMatrixPlan, + pub rhs: *mut RPQMatrixPlan, + pub mat: GrB_Matrix, + pub res_mat: GrB_Matrix, +} +unsafe extern "C" { + pub fn LAGraph_RPQMatrix( + nnz: *mut GrB_Index, + plan: *mut RPQMatrixPlan, + msg: *mut ::std::os::raw::c_char, + ) -> GrB_Info; +} +unsafe extern "C" { + pub fn LAGraph_DestroyRpqMatrixPlan(plan: *mut RPQMatrixPlan) -> GrB_Info; +} diff --git a/src/lib.rs b/src/lib.rs index 0d11b1f..0f89008 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,8 @@ pub mod formats; pub mod graph; +pub mod rpq; pub mod sparql; #[allow(unused_unsafe, dead_code)] -pub(crate) mod utils; +pub mod utils; pub mod lagraph_sys; diff --git a/src/rpq/mod.rs b/src/rpq/mod.rs new file mode 100644 index 0000000..8180612 --- /dev/null +++ b/src/rpq/mod.rs @@ -0,0 +1,54 @@ +//! Regular Path Query (RPQ) evaluation over edge-labeled graphs. +//! ```rust,ignore +//! use pathrex::sparql::parse_rpq; +//! use pathrex::rpq::{RpqEvaluator, nfarpq::NfaRpqEvaluator}; +//! +//! let triple = parse_rpq("SELECT ?x ?y WHERE { ?x /* ?y . }")?; +//! let result = NfaRpqEvaluator.evaluate(&triple.subject, &triple.path, &triple.object, &graph)?; +//! ``` + +pub mod rpqmatrix; + +use crate::graph::GraphDecomposition; +use crate::graph::GraphblasVector; +use crate::sparql::ExtractError; +use spargebra::SparqlSyntaxError; +use spargebra::algebra::PropertyPathExpression; +use spargebra::term::TermPattern; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum RpqError { + #[error("SPARQL syntax error: {0}")] + Parse(#[from] SparqlSyntaxError), + + #[error("query extraction error: {0}")] + Extract(#[from] ExtractError), + + #[error("unsupported path expression: {0}")] + UnsupportedPath(String), + + #[error("label not found in graph: '{0}'")] + LabelNotFound(String), + + #[error("vertex not found in graph: '{0}'")] + VertexNotFound(String), + + #[error("GraphBLAS/LAGraph error: {0}")] + GraphBlas(String), +} + +#[derive(Debug)] +pub struct RpqResult { + pub reachable: GraphblasVector, +} + +pub trait RpqEvaluator { + fn evaluate( + &self, + subject: &TermPattern, + path: &PropertyPathExpression, + object: &TermPattern, + graph: &G, + ) -> Result; +} diff --git a/src/rpq/rpqmatrix.rs b/src/rpq/rpqmatrix.rs new file mode 100644 index 0000000..e551e34 --- /dev/null +++ b/src/rpq/rpqmatrix.rs @@ -0,0 +1,214 @@ +//! Plan-based RPQ evaluation using `LAGraph_RPQMatrix`. + +use std::ptr::null_mut; + +use egg::{Id, RecExpr, define_language}; +use spargebra::algebra::PropertyPathExpression; +use spargebra::term::TermPattern; + +use crate::graph::{GraphDecomposition, GraphblasVector, ensure_grb_init}; +use crate::lagraph_sys::*; +use crate::rpq::{RpqError, RpqEvaluator, RpqResult}; +use crate::{grb_ok, la_ok}; + +unsafe impl Send for RPQMatrixPlan {} + +define_language! { + pub enum RpqPlan { + Label(String), + "/" = Seq([Id; 2]), + "|" = Alt([Id; 2]), + "*" = Star([Id; 1]), + } +} + +/// Compile a [`PropertyPathExpression`] into [`RecExpr`]. +pub fn to_expr(path: &PropertyPathExpression) -> Result, RpqError> { + let mut expr = RecExpr::default(); + to_expr_aux(path, &mut expr)?; + Ok(expr) +} + +fn to_expr_aux( + path: &PropertyPathExpression, + expr: &mut RecExpr, +) -> Result { + match path { + PropertyPathExpression::NamedNode(nn) => { + Ok(expr.add(RpqPlan::Label(nn.as_str().to_owned()))) + } + + PropertyPathExpression::Sequence(lhs, rhs) => { + let l = to_expr_aux(lhs, expr)?; + let r = to_expr_aux(rhs, expr)?; + Ok(expr.add(RpqPlan::Seq([l, r]))) + } + + PropertyPathExpression::Alternative(lhs, rhs) => { + let l = to_expr_aux(lhs, expr)?; + let r = to_expr_aux(rhs, expr)?; + Ok(expr.add(RpqPlan::Alt([l, r]))) + } + + PropertyPathExpression::ZeroOrMore(inner) => { + let i = to_expr_aux(inner, expr)?; + Ok(expr.add(RpqPlan::Star([i]))) + } + + PropertyPathExpression::OneOrMore(inner) => { + let e = to_expr_aux(inner, expr)?; + let s = expr.add(RpqPlan::Star([e])); + Ok(expr.add(RpqPlan::Seq([e, s]))) + } + + PropertyPathExpression::ZeroOrOne(_) => Err(RpqError::UnsupportedPath( + "ZeroOrOne (?) is not supported by RPQMatrix".into(), + )), + + PropertyPathExpression::Reverse(_) => Err(RpqError::UnsupportedPath( + "Reverse paths are not supported".into(), + )), + + PropertyPathExpression::NegatedPropertySet(_) => Err(RpqError::UnsupportedPath( + "NegatedPropertySet paths are not supported".into(), + )), + } +} + +/// Convert a [`RecExpr`] into the flat [`RPQMatrixPlan`] array that +/// `LAGraph_RPQMatrix` expects. +pub fn materialize( + expr: &RecExpr, + graph: &G, +) -> Result, RpqError> { + let null_plan = RPQMatrixPlan { + op: RPQMatrixOp::RPQ_MATRIX_OP_LABEL, + lhs: null_mut(), + rhs: null_mut(), + mat: null_mut(), + res_mat: null_mut(), + }; + let mut plans = vec![null_plan; expr.len()]; + + for (id, node) in expr.as_ref().iter().enumerate() { + plans[id] = match node { + RpqPlan::Label(label) => { + let lg = graph + .get_graph(label) + .map_err(|_| RpqError::LabelNotFound(label.clone()))?; + let mat = unsafe { (*lg.inner).A }; + RPQMatrixPlan { + op: RPQMatrixOp::RPQ_MATRIX_OP_LABEL, + lhs: null_mut(), + rhs: null_mut(), + mat, + res_mat: null_mut(), + } + } + + RpqPlan::Seq([l, r]) => RPQMatrixPlan { + op: RPQMatrixOp::RPQ_MATRIX_OP_CONCAT, + lhs: unsafe { plans.as_mut_ptr().add(usize::from(*l)) }, + rhs: unsafe { plans.as_mut_ptr().add(usize::from(*r)) }, + mat: null_mut(), + res_mat: null_mut(), + }, + + RpqPlan::Alt([l, r]) => RPQMatrixPlan { + op: RPQMatrixOp::RPQ_MATRIX_OP_LOR, + lhs: unsafe { plans.as_mut_ptr().add(usize::from(*l)) }, + rhs: unsafe { plans.as_mut_ptr().add(usize::from(*r)) }, + mat: null_mut(), + res_mat: null_mut(), + }, + + RpqPlan::Star([i]) => RPQMatrixPlan { + op: RPQMatrixOp::RPQ_MATRIX_OP_KLEENE, + lhs: null_mut(), + rhs: unsafe { plans.as_mut_ptr().add(usize::from(*i)) }, + mat: null_mut(), + res_mat: null_mut(), + }, + }; + } + + Ok(plans) +} + +/// RPQ evaluator backed by `LAGraph_RPQMatrix`. +pub struct RpqMatrixEvaluator; + +impl RpqEvaluator for RpqMatrixEvaluator { + fn evaluate( + &self, + subject: &TermPattern, + path: &PropertyPathExpression, + object: &TermPattern, + graph: &G, + ) -> Result { + if !matches!(object, TermPattern::Variable(_)) { + return Err(RpqError::UnsupportedPath( + "bound object term is not yet supported by RpqMatrixEvaluator".into(), + )); + } + + ensure_grb_init().map_err(|e| RpqError::GraphBlas(e.to_string()))?; + + let n = graph.num_nodes() as GrB_Index; + + let expr = to_expr(path)?; + + let mut plans = materialize(&expr, graph)?; + let root_ptr = unsafe { plans.as_mut_ptr().add(plans.len() - 1) }; + + let mut nnz: GrB_Index = 0; + la_ok!(LAGraph_RPQMatrix(&mut nnz, root_ptr)) + .map_err(|e| RpqError::GraphBlas(e.to_string()))?; + + let res_mat = unsafe { (*root_ptr).res_mat }; + + let src = unsafe { + GraphblasVector::new_bool(n).map_err(|e| RpqError::GraphBlas(e.to_string()))? + }; + match subject { + TermPattern::NamedNode(nn) => { + let id = graph + .get_node_id(nn.as_str()) + .ok_or_else(|| RpqError::VertexNotFound(nn.as_str().to_owned()))? + as GrB_Index; + grb_ok!(GrB_Vector_setElement_BOOL(src.inner, true, id)) + .map_err(|e| RpqError::GraphBlas(e.to_string()))?; + } + TermPattern::Variable(_) => { + for i in 0..n { + grb_ok!(GrB_Vector_setElement_BOOL(src.inner, true, i)) + .map_err(|e| RpqError::GraphBlas(e.to_string()))?; + } + } + _ => { + return Err(RpqError::UnsupportedPath( + "subject must be a variable or named node".into(), + )); + } + } + + let result = unsafe { + GraphblasVector::new_bool(n).map_err(|e| RpqError::GraphBlas(e.to_string()))? + }; + grb_ok!(GrB_vxm( + result.inner, + null_mut(), + null_mut(), + GrB_LOR_LAND_SEMIRING_BOOL, + src.inner, + res_mat, + null_mut(), + )) + .map_err(|e| RpqError::GraphBlas(e.to_string()))?; + + grb_ok!(LAGraph_DestroyRpqMatrixPlan(root_ptr)) + .map_err(|e| RpqError::GraphBlas(e.to_string()))?; + + Ok(RpqResult { reachable: result }) + } +} diff --git a/src/utils.rs b/src/utils.rs index 7cb37d3..92846ca 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,6 +1,26 @@ use crate::{graph::*, lagraph_sys::*}; use std::{fmt::Display, sync::Arc}; +pub fn build_graph(edges: &[(&str, &str, &str)]) -> ::Graph { + let builder = InMemoryBuilder::new(); + let edges = edges + .iter() + .cloned() + .map(|(s, t, l)| { + Ok(Edge { + source: s.to_string(), + label: l.to_string(), + target: t.to_string(), + }) + }) + .collect::>>(); + builder + .with_stream(edges.into_iter()) + .expect("Should insert edges stream") + .build() + .expect("build must succeed") +} + pub struct CountOutput(pub usize, std::marker::PhantomData); impl CountOutput { diff --git a/tests/rpqmatrix_tests.rs b/tests/rpqmatrix_tests.rs new file mode 100644 index 0000000..ab4f766 --- /dev/null +++ b/tests/rpqmatrix_tests.rs @@ -0,0 +1,326 @@ +use pathrex::graph::GraphDecomposition; +use pathrex::lagraph_sys::{GrB_Index, GrB_Vector_extractTuples_BOOL, GrB_Vector_nvals}; +use pathrex::rpq::rpqmatrix::RpqMatrixEvaluator; +use pathrex::rpq::{RpqError, RpqEvaluator, RpqResult}; +use pathrex::utils::build_graph; +use spargebra::algebra::PropertyPathExpression; +use spargebra::term::{NamedNode, TermPattern, Variable}; + +fn named(iri: &str) -> PropertyPathExpression { + PropertyPathExpression::NamedNode(NamedNode::new_unchecked(iri)) +} + +fn var(name: &str) -> TermPattern { + TermPattern::Variable(Variable::new_unchecked(name)) +} + +fn named_term(iri: &str) -> TermPattern { + TermPattern::NamedNode(NamedNode::new_unchecked(iri)) +} + +fn reachable_indices(result: &RpqResult) -> Vec { + unsafe { + let mut nvals: GrB_Index = 0; + GrB_Vector_nvals(&mut nvals, result.reachable.inner); + if nvals == 0 { + return Vec::new(); + } + let mut indices = vec![0u64; nvals as usize]; + let mut values = vec![false; nvals as usize]; + GrB_Vector_extractTuples_BOOL( + indices.as_mut_ptr(), + values.as_mut_ptr(), + &mut nvals, + result.reachable.inner, + ); + indices.truncate(nvals as usize); + indices + } +} + +fn reachable_count(result: &RpqResult) -> u64 { + unsafe { + let mut nvals: GrB_Index = 0; + GrB_Vector_nvals(&mut nvals, result.reachable.inner); + nvals + } +} + +/// Graph: A --knows--> B --knows--> C +/// Query: ?x ?y +#[test] +fn test_single_label_variable_variable() { + let graph = build_graph(&[("A", "B", "knows"), ("B", "C", "knows")]); + let evaluator = RpqMatrixEvaluator; + + let result = evaluator + .evaluate(&var("x"), &named("knows"), &var("y"), &graph) + .expect("evaluate should succeed"); + + let count = reachable_count(&result); + assert_eq!(count, 2); +} + +/// Graph: A --knows--> B --knows--> C +/// Query: ?y +#[test] +fn test_single_label_named_source() { + let graph = build_graph(&[("A", "B", "knows"), ("B", "C", "knows")]); + let evaluator = RpqMatrixEvaluator; + + let result = evaluator + .evaluate(&named_term("A"), &named("knows"), &var("y"), &graph) + .expect("evaluate should succeed"); + + let indices = reachable_indices(&result); + let b_id = graph.get_node_id("B").expect("B should exist"); + assert!( + indices.contains(&(b_id as GrB_Index)), + "B (id={b_id}) should be reachable from A via 'knows', got indices: {indices:?}" + ); +} + +/// Graph: A --knows--> B --likes--> C +/// Query: ?x / ?y (two-hop sequence) +#[test] +fn test_sequence_path() { + let graph = build_graph(&[("A", "B", "knows"), ("B", "C", "likes")]); + let evaluator = RpqMatrixEvaluator; + + let path = PropertyPathExpression::Sequence(Box::new(named("knows")), Box::new(named("likes"))); + + let result = evaluator + .evaluate(&var("x"), &path, &var("y"), &graph) + .expect("evaluate should succeed"); + + let count = reachable_count(&result); + assert_eq!(count, 1); +} + +/// Graph: A --knows--> B --likes--> C +/// Query: / ?y +#[test] +fn test_sequence_path_named_source() { + let graph = build_graph(&[("A", "B", "knows"), ("B", "C", "likes")]); + let evaluator = RpqMatrixEvaluator; + + let path = PropertyPathExpression::Sequence(Box::new(named("knows")), Box::new(named("likes"))); + + let result = evaluator + .evaluate(&named_term("A"), &path, &var("y"), &graph) + .expect("evaluate should succeed"); + + let indices = reachable_indices(&result); + let c_id = graph.get_node_id("C").expect("C should exist"); + assert!( + indices.contains(&(c_id as GrB_Index)), + "C (id={c_id}) should be reachable from A via knows/likes, got indices: {indices:?}" + ); +} + +/// Graph: A --knows--> B, A --likes--> C +/// Query: | ?y +#[test] +fn test_alternative_path() { + let graph = build_graph(&[("A", "B", "knows"), ("A", "C", "likes")]); + let evaluator = RpqMatrixEvaluator; + + let path = + PropertyPathExpression::Alternative(Box::new(named("knows")), Box::new(named("likes"))); + + let result = evaluator + .evaluate(&named_term("A"), &path, &var("y"), &graph) + .expect("evaluate should succeed"); + + let indices = reachable_indices(&result); + let b_id = graph.get_node_id("B").expect("B should exist"); + let c_id = graph.get_node_id("C").expect("C should exist"); + assert!( + indices.contains(&(b_id as GrB_Index)), + "B should be reachable via knows|likes" + ); + assert!( + indices.contains(&(c_id as GrB_Index)), + "C should be reachable via knows|likes" + ); +} + +/// Graph: A --knows--> B --knows--> C +/// Query: * ?y +#[test] +fn test_zero_or_more_path() { + let graph = build_graph(&[("A", "B", "knows"), ("B", "C", "knows")]); + let evaluator = RpqMatrixEvaluator; + + let path = PropertyPathExpression::ZeroOrMore(Box::new(named("knows"))); + + let result = evaluator + .evaluate(&named_term("A"), &path, &var("y"), &graph) + .expect("evaluate should succeed"); + + let indices = reachable_indices(&result); + let a_id = graph.get_node_id("A").expect("A should exist"); + let b_id = graph.get_node_id("B").expect("B should exist"); + let c_id = graph.get_node_id("C").expect("C should exist"); + + assert!(indices.contains(&(a_id as GrB_Index)), "A should be reachable (zero hops)"); + assert!(indices.contains(&(b_id as GrB_Index)), "B should be reachable (one hop)"); + assert!(indices.contains(&(c_id as GrB_Index)), "C should be reachable (two hops)"); +} + +/// Graph: A --knows--> B --knows--> C +/// Query: + ?y +#[test] +fn test_one_or_more_path() { + let graph = build_graph(&[("A", "B", "knows"), ("B", "C", "knows")]); + let evaluator = RpqMatrixEvaluator; + + let path = PropertyPathExpression::OneOrMore(Box::new(named("knows"))); + + let result = evaluator + .evaluate(&named_term("A"), &path, &var("y"), &graph) + .expect("evaluate should succeed"); + + let indices = reachable_indices(&result); + let a_id = graph.get_node_id("A").expect("A should exist"); + let b_id = graph.get_node_id("B").expect("B should exist"); + let c_id = graph.get_node_id("C").expect("C should exist"); + + assert!(!indices.contains(&(a_id as GrB_Index)), "A shouldn't be reachable"); + assert!(indices.contains(&(b_id as GrB_Index)), "B should be reachable (one hop)"); + assert!(indices.contains(&(c_id as GrB_Index)), "C should be reachable (two hops)"); +} + +#[test] +fn test_zero_or_one_unsupported() { + let graph = build_graph(&[("A", "B", "knows")]); + let evaluator = RpqMatrixEvaluator; + + let path = PropertyPathExpression::ZeroOrOne(Box::new(named("knows"))); + let result = evaluator.evaluate(&var("x"), &path, &var("y"), &graph); + + assert!( + matches!(result, Err(RpqError::UnsupportedPath(_))), + "expected UnsupportedPath for ZeroOrOne, got: {result:?}" + ); +} + +#[test] +fn test_label_not_found() { + let graph = build_graph(&[("A", "B", "knows")]); + let evaluator = RpqMatrixEvaluator; + + let result = evaluator.evaluate(&var("x"), &named("nonexistent"), &var("y"), &graph); + + assert!( + matches!(result, Err(RpqError::LabelNotFound(ref l)) if l == "nonexistent"), + "expected LabelNotFound error, got: {result:?}" + ); +} + +#[test] +fn test_vertex_not_found() { + let graph = build_graph(&[("A", "B", "knows")]); + let evaluator = RpqMatrixEvaluator; + + let result = evaluator.evaluate(&named_term("Z"), &named("knows"), &var("y"), &graph); + + assert!( + matches!(result, Err(RpqError::VertexNotFound(ref v)) if v == "Z"), + "expected VertexNotFound error, got: {result:?}" + ); +} + +#[test] +fn test_bound_object_unsupported() { + let graph = build_graph(&[("A", "B", "knows")]); + let evaluator = RpqMatrixEvaluator; + + let result = evaluator.evaluate(&var("x"), &named("knows"), &named_term("B"), &graph); + + assert!( + matches!(result, Err(RpqError::UnsupportedPath(_))), + "expected UnsupportedPath for bound object, got: {result:?}" + ); +} + +#[test] +fn test_reverse_path_unsupported() { + let graph = build_graph(&[("A", "B", "knows")]); + let evaluator = RpqMatrixEvaluator; + + let path = PropertyPathExpression::Reverse(Box::new(named("knows"))); + let result = evaluator.evaluate(&var("x"), &path, &var("y"), &graph); + + assert!( + matches!(result, Err(RpqError::UnsupportedPath(_))), + "expected UnsupportedPath error, got: {result:?}" + ); +} + +/// Graph: A --knows--> B --knows--> C --knows--> A (cycle) +/// Query: * ?y +#[test] +fn test_cycle_graph_star() { + let graph = build_graph(&[ + ("A", "B", "knows"), + ("B", "C", "knows"), + ("C", "A", "knows"), + ]); + let evaluator = RpqMatrixEvaluator; + + let path = PropertyPathExpression::ZeroOrMore(Box::new(named("knows"))); + + let result = evaluator + .evaluate(&named_term("A"), &path, &var("y"), &graph) + .expect("evaluate should succeed"); + + let count = reachable_count(&result); + assert_eq!(count, 3, "all 3 nodes should be reachable in a cycle"); +} + +/// Graph: A --knows--> B --likes--> C --knows--> D +/// Query: /*/ ?y +#[test] +fn test_complex_path() { + let graph = build_graph(&[ + ("A", "B", "knows"), + ("B", "C", "likes"), + ("C", "D", "knows"), + ]); + let evaluator = RpqMatrixEvaluator; + + let path = PropertyPathExpression::Sequence( + Box::new(PropertyPathExpression::Sequence( + Box::new(named("knows")), + Box::new(PropertyPathExpression::ZeroOrMore(Box::new(named("likes")))), + )), + Box::new(named("knows")), + ); + + let result = evaluator + .evaluate(&named_term("A"), &path, &var("y"), &graph) + .expect("evaluate should succeed"); + + let indices = reachable_indices(&result); + let d_id = graph.get_node_id("D").expect("D should exist"); + assert!( + indices.contains(&(d_id as GrB_Index)), + "D should be reachable via knows/likes*/knows, got indices: {indices:?}" + ); +} + +#[test] +fn test_no_matching_path() { + let graph = build_graph(&[("A", "B", "knows")]); + let evaluator = RpqMatrixEvaluator; + + let path = PropertyPathExpression::Sequence(Box::new(named("knows")), Box::new(named("likes"))); + + let result = evaluator.evaluate(&var("x"), &path, &var("y"), &graph); + + assert!( + matches!(result, Err(RpqError::LabelNotFound(ref l)) if l == "likes"), + "expected LabelNotFound for 'likes', got: {result:?}" + ); +}