use super::*;
use plexus_serde::{
current_plan_version, CmpOp, ColDef, ColKind, LogicalType, Op, Plan, VectorMetric,
PLAN_FORMAT_MAJOR, PLAN_FORMAT_MINOR, PLAN_FORMAT_PATCH,
};
use std::collections::BTreeSet;
fn test_version() -> plexus_serde::Version {
current_plan_version("test")
}
fn current_semver() -> PlanSemver {
PlanSemver::new(PLAN_FORMAT_MAJOR, PLAN_FORMAT_MINOR, PLAN_FORMAT_PATCH)
}
fn current_range() -> VersionRange {
VersionRange::new(current_semver(), current_semver())
}
#[test]
fn version_range_inclusive_check() {
let range = VersionRange::new(PlanSemver::new(0, 1, 0), PlanSemver::new(0, 3, 9));
assert!(range.supports(PlanSemver::new(0, 1, 0)));
assert!(range.supports(PlanSemver::new(0, 2, 5)));
assert!(range.supports(PlanSemver::new(0, 3, 9)));
assert!(!range.supports(PlanSemver::new(0, 4, 0)));
assert!(!range.supports(PlanSemver::new(0, 0, 9)));
}
#[test]
fn required_capabilities_collects_ops_and_nested_exprs() {
use plexus_serde::Expr;
let plan = Plan {
version: test_version(),
ops: vec![
Op::ScanNodes {
labels: vec!["Person".to_string()],
schema: vec![],
must_labels: vec![],
forbidden_labels: vec![],
graph_ref: None,
est_rows: -1,
selectivity: 1.0,
},
Op::Filter {
input: 0,
predicate: Expr::And {
lhs: Box::new(Expr::Cmp {
op: CmpOp::Gt,
lhs: Box::new(Expr::PropAccess {
col: 0,
prop: "age".to_string(),
}),
rhs: Box::new(Expr::IntLiteral(30)),
}),
rhs: Box::new(Expr::Exists {
expr: Box::new(Expr::PropAccess {
col: 0,
prop: "name".to_string(),
}),
}),
},
},
Op::Return { input: 1 },
],
root_op: 2,
};
let req = required_capabilities(&plan);
assert!(req.required_ops.contains(&OpKind::ScanNodes));
assert!(req.required_ops.contains(&OpKind::Filter));
assert!(req.required_ops.contains(&OpKind::Return));
assert!(req.required_exprs.contains(&ExprKind::And));
assert!(req.required_exprs.contains(&ExprKind::Cmp));
assert!(req.required_exprs.contains(&ExprKind::PropAccess));
assert!(req.required_exprs.contains(&ExprKind::IntLiteral));
assert!(req.required_exprs.contains(&ExprKind::Exists));
}
#[test]
fn validate_reports_missing_feature_support() {
use plexus_serde::Expr;
let plan = Plan {
version: test_version(),
ops: vec![
Op::ScanNodes {
labels: vec![],
schema: vec![],
must_labels: vec![],
forbidden_labels: vec![],
graph_ref: None,
est_rows: -1,
selectivity: 1.0,
},
Op::Filter {
input: 0,
predicate: Expr::Contains {
expr: Box::new(Expr::PropAccess {
col: 0,
prop: "name".to_string(),
}),
pattern: "A".to_string(),
},
},
Op::Return { input: 1 },
],
root_op: 2,
};
let capabilities = EngineCapabilities {
version_range: current_range(),
supported_ops: BTreeSet::from([OpKind::ScanNodes, OpKind::Return]),
supported_exprs: BTreeSet::from([ExprKind::PropAccess]),
supports_graph_ref: false,
supports_multi_graph: false,
supports_graph_params: false,
};
let err =
validate_plan_against_capabilities(&plan, &capabilities).expect_err("expected mismatch");
match err {
CapabilityError::MissingFeatureSupport {
missing_ops,
missing_exprs,
} => {
assert!(missing_ops.contains(&OpKind::Filter));
assert!(missing_exprs.contains(&ExprKind::Contains));
}
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn validate_rejects_unsupported_version() {
let plan = Plan {
version: plexus_serde::Version {
major: 1,
minor: 0,
patch: 0,
producer: "future".to_string(),
},
ops: vec![Op::Return { input: 0 }],
root_op: 0,
};
let capabilities = EngineCapabilities::full(VersionRange::new(
PlanSemver::new(0, 1, 0),
PlanSemver::new(0, 9, 99),
));
let err = validate_plan_against_capabilities(&plan, &capabilities)
.expect_err("expected version rejection");
assert!(matches!(
err,
CapabilityError::UnsupportedPlanVersion { .. }
));
}
#[test]
fn capability_json_roundtrip() {
let caps = EngineCapabilities {
version_range: VersionRange::new(PlanSemver::new(0, 1, 0), PlanSemver::new(0, 3, 0)),
supported_ops: BTreeSet::from([OpKind::ScanNodes, OpKind::Filter, OpKind::Return]),
supported_exprs: BTreeSet::from([
ExprKind::PropAccess,
ExprKind::IntLiteral,
ExprKind::Cmp,
]),
supports_graph_ref: false,
supports_multi_graph: false,
supports_graph_params: false,
};
let json = caps.to_json_pretty().expect("serialize json");
let round = EngineCapabilities::from_json(&json).expect("deserialize json");
assert_eq!(round, caps);
}
#[test]
fn capability_flatbuffer_roundtrip() {
let caps = EngineCapabilities {
version_range: VersionRange::new(PlanSemver::new(0, 1, 0), PlanSemver::new(0, 3, 0)),
supported_ops: BTreeSet::from([OpKind::ScanNodes, OpKind::Sort, OpKind::Return]),
supported_exprs: BTreeSet::from([
ExprKind::PropAccess,
ExprKind::IntLiteral,
ExprKind::Cmp,
]),
supports_graph_ref: false,
supports_multi_graph: false,
supports_graph_params: false,
};
let bytes = caps.to_flatbuffer_bytes().expect("serialize flatbuffer");
let round = EngineCapabilities::from_flatbuffer_bytes(&bytes).expect("deserialize flatbuffer");
assert_eq!(round, caps);
}
#[test]
fn capability_json_flatbuffer_parity() {
let caps = EngineCapabilities {
version_range: VersionRange::new(PlanSemver::new(0, 1, 0), PlanSemver::new(0, 2, 5)),
supported_ops: BTreeSet::from([OpKind::ScanNodes, OpKind::Filter, OpKind::Sort]),
supported_exprs: BTreeSet::from([ExprKind::PropAccess, ExprKind::Cmp]),
supports_graph_ref: false,
supports_multi_graph: false,
supports_graph_params: false,
};
let json = caps.to_json_pretty().expect("serialize json");
let from_json = EngineCapabilities::from_json(&json).expect("deserialize json");
let bytes = caps.to_flatbuffer_bytes().expect("serialize flatbuffer");
let from_bytes =
EngineCapabilities::from_flatbuffer_bytes(&bytes).expect("deserialize flatbuffer");
assert_eq!(from_json, from_bytes);
assert_eq!(from_bytes, caps);
}
#[test]
fn capability_json_rejects_ordering_mismatch() {
let doc = EngineCapabilityDocument {
version_range: VersionRange::new(PlanSemver::new(0, 1, 0), PlanSemver::new(0, 2, 0)),
supported_ops: vec!["Sort".to_string()],
supported_exprs: vec![],
op_ordering_contracts: vec![OpOrderingDocument {
op: "Sort".to_string(),
contract: OpOrderingContract::UnspecifiedWithoutSort,
}],
supports_graph_ref: false,
supports_multi_graph: false,
supports_graph_params: false,
};
let err = EngineCapabilities::from_document(doc).expect_err("expected mismatch");
assert!(matches!(
err,
CapabilityError::OrderingContractMismatch { .. }
));
}
#[test]
fn capability_json_rejects_partial_ordering_declarations() {
let doc = EngineCapabilityDocument {
version_range: VersionRange::new(PlanSemver::new(0, 1, 0), PlanSemver::new(0, 2, 0)),
supported_ops: vec!["Filter".to_string(), "Return".to_string()],
supported_exprs: vec![],
op_ordering_contracts: vec![OpOrderingDocument {
op: "Filter".to_string(),
contract: OpOrderingContract::StablePassThrough,
}],
supports_graph_ref: false,
supports_multi_graph: false,
supports_graph_params: false,
};
let err = EngineCapabilities::from_document(doc).expect_err("expected missing declaration");
assert!(matches!(
err,
CapabilityError::MissingOrderingDeclaration { .. }
));
}
#[test]
fn ordering_contract_declares_union_non_distinct_mode() {
assert_eq!(
op_ordering_contract(OpKind::Union),
OpOrderingContract::StableConcatOrUnspecifiedDistinct
);
}
#[test]
fn ordering_contract_declares_rerank_score_desc_stable_ties() {
assert_eq!(
op_ordering_contract(OpKind::Rerank),
OpOrderingContract::ScoreDescStableTies
);
}
#[test]
fn ordering_contract_declares_vector_scan_unspecified_without_sort() {
assert_eq!(
op_ordering_contract(OpKind::VectorScan),
OpOrderingContract::UnspecifiedWithoutSort
);
}
#[test]
fn capability_json_rejects_rerank_ordering_mismatch() {
let doc = EngineCapabilityDocument {
version_range: current_range(),
supported_ops: vec!["Rerank".to_string()],
supported_exprs: vec![],
op_ordering_contracts: vec![OpOrderingDocument {
op: "Rerank".to_string(),
contract: OpOrderingContract::StablePassThrough,
}],
supports_graph_ref: false,
supports_multi_graph: false,
supports_graph_params: false,
};
let err = EngineCapabilities::from_document(doc).expect_err("expected ordering mismatch");
assert!(matches!(
err,
CapabilityError::OrderingContractMismatch { .. }
));
}
#[test]
fn capability_json_rejects_vector_scan_ordering_mismatch() {
let doc = EngineCapabilityDocument {
version_range: current_range(),
supported_ops: vec!["VectorScan".to_string()],
supported_exprs: vec![],
op_ordering_contracts: vec![OpOrderingDocument {
op: "VectorScan".to_string(),
contract: OpOrderingContract::StablePassThrough,
}],
supports_graph_ref: false,
supports_multi_graph: false,
supports_graph_params: false,
};
let err = EngineCapabilities::from_document(doc).expect_err("expected ordering mismatch");
assert!(matches!(
err,
CapabilityError::OrderingContractMismatch { .. }
));
}
#[test]
fn vector_scan_rejected_by_engine_without_vector_capability() {
let plan = Plan {
version: test_version(),
ops: vec![
Op::ScanNodes {
labels: vec!["Person".to_string()],
schema: vec![ColDef {
name: "n".to_string(),
kind: ColKind::Node,
logical_type: plexus_serde::LogicalType::Unknown,
}],
must_labels: vec![],
forbidden_labels: vec![],
graph_ref: None,
est_rows: -1,
selectivity: 1.0,
},
Op::VectorScan {
input: 0,
collection: "embeddings".to_string(),
query_vector: plexus_serde::Expr::FloatLiteral(0.5),
metric: VectorMetric::Cosine,
top_k: 10,
approx_hint: true,
schema: vec![
ColDef {
name: "n".to_string(),
kind: ColKind::Node,
logical_type: plexus_serde::LogicalType::Unknown,
},
ColDef {
name: "score".to_string(),
kind: ColKind::Value,
logical_type: plexus_serde::LogicalType::Unknown,
},
],
},
Op::Return { input: 1 },
],
root_op: 2,
};
let capabilities = EngineCapabilities {
version_range: current_range(),
supported_ops: BTreeSet::from([OpKind::ScanNodes, OpKind::Return]),
supported_exprs: BTreeSet::from([ExprKind::FloatLiteral]),
supports_graph_ref: false,
supports_multi_graph: false,
supports_graph_params: false,
};
let err =
validate_plan_against_capabilities(&plan, &capabilities).expect_err("expected rejection");
match err {
CapabilityError::MissingFeatureSupport { missing_ops, .. } => {
assert!(missing_ops.contains(&OpKind::VectorScan));
}
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn vector_scan_accepted_by_engine_with_vector_capability() {
let plan = Plan {
version: test_version(),
ops: vec![
Op::ScanNodes {
labels: vec![],
schema: vec![ColDef {
name: "n".to_string(),
kind: ColKind::Node,
logical_type: plexus_serde::LogicalType::Unknown,
}],
must_labels: vec![],
forbidden_labels: vec![],
graph_ref: None,
est_rows: -1,
selectivity: 1.0,
},
Op::VectorScan {
input: 0,
collection: "embeddings".to_string(),
query_vector: plexus_serde::Expr::FloatLiteral(0.5),
metric: VectorMetric::Cosine,
top_k: 10,
approx_hint: true,
schema: vec![
ColDef {
name: "n".to_string(),
kind: ColKind::Node,
logical_type: plexus_serde::LogicalType::Unknown,
},
ColDef {
name: "score".to_string(),
kind: ColKind::Value,
logical_type: plexus_serde::LogicalType::Unknown,
},
],
},
Op::Return { input: 1 },
],
root_op: 2,
};
let capabilities = EngineCapabilities {
version_range: current_range(),
supported_ops: BTreeSet::from([OpKind::ScanNodes, OpKind::VectorScan, OpKind::Return]),
supported_exprs: BTreeSet::from([ExprKind::FloatLiteral]),
supports_graph_ref: false,
supports_multi_graph: false,
supports_graph_params: false,
};
assert!(validate_plan_against_capabilities(&plan, &capabilities).is_ok());
}
#[test]
fn rerank_rejected_by_engine_without_rerank_capability() {
let plan = Plan {
version: test_version(),
ops: vec![
Op::ScanNodes {
labels: vec![],
schema: vec![ColDef {
name: "n".to_string(),
kind: ColKind::Node,
logical_type: plexus_serde::LogicalType::Unknown,
}],
must_labels: vec![],
forbidden_labels: vec![],
graph_ref: None,
est_rows: -1,
selectivity: 1.0,
},
Op::Rerank {
input: 0,
score_expr: plexus_serde::Expr::FloatLiteral(1.0),
top_k: 5,
schema: vec![ColDef {
name: "n".to_string(),
kind: ColKind::Node,
logical_type: plexus_serde::LogicalType::Unknown,
}],
},
Op::Return { input: 1 },
],
root_op: 2,
};
let capabilities = EngineCapabilities {
version_range: current_range(),
supported_ops: BTreeSet::from([OpKind::ScanNodes, OpKind::Return]),
supported_exprs: BTreeSet::from([ExprKind::FloatLiteral]),
supports_graph_ref: false,
supports_multi_graph: false,
supports_graph_params: false,
};
let err =
validate_plan_against_capabilities(&plan, &capabilities).expect_err("expected rejection");
match err {
CapabilityError::MissingFeatureSupport { missing_ops, .. } => {
assert!(missing_ops.contains(&OpKind::Rerank));
}
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn rerank_accepted_by_engine_with_rerank_capability() {
let plan = Plan {
version: test_version(),
ops: vec![
Op::ScanNodes {
labels: vec![],
schema: vec![ColDef {
name: "n".to_string(),
kind: ColKind::Node,
logical_type: plexus_serde::LogicalType::Unknown,
}],
must_labels: vec![],
forbidden_labels: vec![],
graph_ref: None,
est_rows: -1,
selectivity: 1.0,
},
Op::Rerank {
input: 0,
score_expr: plexus_serde::Expr::FloatLiteral(1.0),
top_k: 5,
schema: vec![ColDef {
name: "n".to_string(),
kind: ColKind::Node,
logical_type: plexus_serde::LogicalType::Unknown,
}],
},
Op::Return { input: 1 },
],
root_op: 2,
};
let capabilities = EngineCapabilities {
version_range: current_range(),
supported_ops: BTreeSet::from([OpKind::ScanNodes, OpKind::Rerank, OpKind::Return]),
supported_exprs: BTreeSet::from([ExprKind::FloatLiteral]),
supports_graph_ref: false,
supports_multi_graph: false,
supports_graph_params: false,
};
assert!(validate_plan_against_capabilities(&plan, &capabilities).is_ok());
}
#[test]
fn vector_similarity_expr_rejected_without_expr_capability() {
let similarity_expr = plexus_serde::Expr::VectorSimilarity {
metric: VectorMetric::Cosine,
lhs: Box::new(plexus_serde::Expr::FloatLiteral(0.1)),
rhs: Box::new(plexus_serde::Expr::FloatLiteral(0.2)),
};
let plan = Plan {
version: test_version(),
ops: vec![
Op::ScanNodes {
labels: vec![],
schema: vec![ColDef {
name: "n".to_string(),
kind: ColKind::Node,
logical_type: plexus_serde::LogicalType::Unknown,
}],
must_labels: vec![],
forbidden_labels: vec![],
graph_ref: None,
est_rows: -1,
selectivity: 1.0,
},
Op::Rerank {
input: 0,
score_expr: similarity_expr,
top_k: 5,
schema: vec![ColDef {
name: "n".to_string(),
kind: ColKind::Node,
logical_type: plexus_serde::LogicalType::Unknown,
}],
},
Op::Return { input: 1 },
],
root_op: 2,
};
let capabilities = EngineCapabilities {
version_range: current_range(),
supported_ops: BTreeSet::from([OpKind::ScanNodes, OpKind::Rerank, OpKind::Return]),
supported_exprs: BTreeSet::from([ExprKind::FloatLiteral]),
supports_graph_ref: false,
supports_multi_graph: false,
supports_graph_params: false,
};
let err =
validate_plan_against_capabilities(&plan, &capabilities).expect_err("expected rejection");
match err {
CapabilityError::MissingFeatureSupport { missing_exprs, .. } => {
assert!(missing_exprs.contains(&ExprKind::VectorSimilarity));
}
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn vector_similarity_expr_accepted_with_expr_capability() {
let similarity_expr = plexus_serde::Expr::VectorSimilarity {
metric: VectorMetric::Cosine,
lhs: Box::new(plexus_serde::Expr::FloatLiteral(0.1)),
rhs: Box::new(plexus_serde::Expr::FloatLiteral(0.2)),
};
let plan = Plan {
version: test_version(),
ops: vec![
Op::ScanNodes {
labels: vec![],
schema: vec![ColDef {
name: "n".to_string(),
kind: ColKind::Node,
logical_type: plexus_serde::LogicalType::Unknown,
}],
must_labels: vec![],
forbidden_labels: vec![],
graph_ref: None,
est_rows: -1,
selectivity: 1.0,
},
Op::Rerank {
input: 0,
score_expr: similarity_expr,
top_k: 5,
schema: vec![ColDef {
name: "n".to_string(),
kind: ColKind::Node,
logical_type: plexus_serde::LogicalType::Unknown,
}],
},
Op::Return { input: 1 },
],
root_op: 2,
};
let capabilities = EngineCapabilities {
version_range: current_range(),
supported_ops: BTreeSet::from([OpKind::ScanNodes, OpKind::Rerank, OpKind::Return]),
supported_exprs: BTreeSet::from([ExprKind::FloatLiteral, ExprKind::VectorSimilarity]),
supports_graph_ref: false,
supports_multi_graph: false,
supports_graph_params: false,
};
assert!(validate_plan_against_capabilities(&plan, &capabilities).is_ok());
}
#[test]
fn hybrid_vector_pipeline_rejected_without_vector_similarity_expr_capability() {
let similarity_expr = plexus_serde::Expr::VectorSimilarity {
metric: VectorMetric::L2,
lhs: Box::new(plexus_serde::Expr::ColRef { idx: 1 }),
rhs: Box::new(plexus_serde::Expr::FloatLiteral(0.2)),
};
let plan = Plan {
version: test_version(),
ops: vec![
Op::ScanNodes {
labels: vec![],
schema: vec![ColDef {
name: "d".to_string(),
kind: ColKind::Node,
logical_type: LogicalType::Unknown,
}],
must_labels: vec![],
forbidden_labels: vec![],
graph_ref: None,
est_rows: -1,
selectivity: 1.0,
},
Op::VectorScan {
input: 0,
collection: "docs".to_string(),
query_vector: plexus_serde::Expr::FloatLiteral(0.1),
metric: VectorMetric::Cosine,
top_k: 25,
approx_hint: true,
schema: vec![
ColDef {
name: "d".to_string(),
kind: ColKind::Node,
logical_type: LogicalType::Unknown,
},
ColDef {
name: "score".to_string(),
kind: ColKind::Value,
logical_type: LogicalType::Unknown,
},
],
},
Op::Rerank {
input: 1,
score_expr: similarity_expr,
top_k: 5,
schema: vec![
ColDef {
name: "d".to_string(),
kind: ColKind::Node,
logical_type: LogicalType::Unknown,
},
ColDef {
name: "score".to_string(),
kind: ColKind::Value,
logical_type: LogicalType::Unknown,
},
],
},
Op::Return { input: 2 },
],
root_op: 3,
};
let capabilities = EngineCapabilities {
version_range: current_range(),
supported_ops: BTreeSet::from([
OpKind::ScanNodes,
OpKind::VectorScan,
OpKind::Rerank,
OpKind::Return,
]),
supported_exprs: BTreeSet::from([ExprKind::FloatLiteral, ExprKind::ColRef]),
supports_graph_ref: false,
supports_multi_graph: false,
supports_graph_params: false,
};
let err =
validate_plan_against_capabilities(&plan, &capabilities).expect_err("expected rejection");
match err {
CapabilityError::MissingFeatureSupport {
missing_ops,
missing_exprs,
..
} => {
assert!(
missing_ops.is_empty(),
"unexpected missing ops: {missing_ops:?}"
);
assert!(missing_exprs.contains(&ExprKind::VectorSimilarity));
}
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn arith_and_param_exprs_require_capability_flags() {
let plan = Plan {
version: test_version(),
ops: vec![
Op::ScanNodes {
labels: vec![],
schema: vec![ColDef {
name: "x".to_string(),
kind: ColKind::Value,
logical_type: plexus_serde::LogicalType::Unknown,
}],
must_labels: vec![],
forbidden_labels: vec![],
graph_ref: None,
est_rows: -1,
selectivity: 1.0,
},
Op::Project {
input: 0,
exprs: vec![plexus_serde::Expr::Case {
arms: vec![(
plexus_serde::Expr::Cmp {
op: CmpOp::Gt,
lhs: Box::new(plexus_serde::Expr::Arith {
op: plexus_serde::ArithOp::Add,
lhs: Box::new(plexus_serde::Expr::ColRef { idx: 0 }),
rhs: Box::new(plexus_serde::Expr::Param {
name: "delta".to_string(),
expected_type: None,
}),
}),
rhs: Box::new(plexus_serde::Expr::IntLiteral(10)),
},
plexus_serde::Expr::StringLiteral("large".to_string()),
)],
else_expr: Some(Box::new(plexus_serde::Expr::StringLiteral(
"small".to_string(),
))),
}],
schema: vec![ColDef {
name: "y".to_string(),
kind: ColKind::Value,
logical_type: plexus_serde::LogicalType::Unknown,
}],
},
Op::Return { input: 1 },
],
root_op: 2,
};
let capabilities = EngineCapabilities {
version_range: VersionRange::new(PlanSemver::new(0, 1, 0), PlanSemver::new(0, 3, 0)),
supported_ops: BTreeSet::from([OpKind::ScanNodes, OpKind::Project, OpKind::Return]),
supported_exprs: BTreeSet::from([ExprKind::ColRef]),
supports_graph_ref: false,
supports_multi_graph: false,
supports_graph_params: false,
};
let err =
validate_plan_against_capabilities(&plan, &capabilities).expect_err("expected rejection");
match err {
CapabilityError::MissingFeatureSupport { missing_exprs, .. } => {
assert!(missing_exprs.contains(&ExprKind::Arith));
assert!(missing_exprs.contains(&ExprKind::Param));
assert!(missing_exprs.contains(&ExprKind::Case));
}
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn ordering_contract_declares_sort_deterministic() {
assert_eq!(
op_ordering_contract(OpKind::Sort),
OpOrderingContract::DeterministicSortStableTies
);
}
#[test]
fn graph_ref_rejected_when_engine_lacks_graph_ref_capability() {
let plan = Plan {
version: test_version(),
ops: vec![
Op::ScanNodes {
labels: vec![],
schema: vec![],
must_labels: vec![],
forbidden_labels: vec![],
graph_ref: Some("social".to_string()),
est_rows: -1,
selectivity: 1.0,
},
Op::Return { input: 0 },
],
root_op: 1,
};
let capabilities = EngineCapabilities {
version_range: current_range(),
supported_ops: BTreeSet::from([OpKind::ScanNodes, OpKind::Return]),
supported_exprs: BTreeSet::new(),
supports_graph_ref: false,
supports_multi_graph: false,
supports_graph_params: false,
};
let err =
validate_plan_against_capabilities(&plan, &capabilities).expect_err("expected rejection");
assert!(matches!(err, CapabilityError::GraphRefUnsupported));
}
#[test]
fn multi_graph_ref_rejected_when_engine_lacks_multi_graph_capability() {
let plan = Plan {
version: test_version(),
ops: vec![
Op::ScanNodes {
labels: vec![],
schema: vec![],
must_labels: vec![],
forbidden_labels: vec![],
graph_ref: Some("g1".to_string()),
est_rows: -1,
selectivity: 1.0,
},
Op::Expand {
input: 0,
src_col: 0,
types: vec!["KNOWS".to_string()],
dir: plexus_serde::ExpandDir::Out,
schema: vec![],
src_var: "n".to_string(),
rel_var: "r".to_string(),
dst_var: "m".to_string(),
legal_src_labels: vec![],
legal_dst_labels: vec![],
est_degree: -1.0,
graph_ref: Some("g2".to_string()),
},
Op::Return { input: 1 },
],
root_op: 2,
};
let capabilities = EngineCapabilities {
version_range: current_range(),
supported_ops: BTreeSet::from([OpKind::ScanNodes, OpKind::Expand, OpKind::Return]),
supported_exprs: BTreeSet::new(),
supports_graph_ref: true,
supports_multi_graph: false,
supports_graph_params: false,
};
let err =
validate_plan_against_capabilities(&plan, &capabilities).expect_err("expected rejection");
assert!(matches!(err, CapabilityError::MultiGraphUnsupported));
}
#[test]
fn multi_graph_ref_allowed_when_engine_declares_multi_graph_capability() {
let plan = Plan {
version: test_version(),
ops: vec![
Op::ScanNodes {
labels: vec![],
schema: vec![],
must_labels: vec![],
forbidden_labels: vec![],
graph_ref: Some("g1".to_string()),
est_rows: -1,
selectivity: 1.0,
},
Op::Expand {
input: 0,
src_col: 0,
types: vec!["KNOWS".to_string()],
dir: plexus_serde::ExpandDir::Out,
schema: vec![],
src_var: "n".to_string(),
rel_var: "r".to_string(),
dst_var: "m".to_string(),
legal_src_labels: vec![],
legal_dst_labels: vec![],
est_degree: -1.0,
graph_ref: Some("g2".to_string()),
},
Op::Return { input: 1 },
],
root_op: 2,
};
let capabilities = EngineCapabilities {
version_range: current_range(),
supported_ops: BTreeSet::from([OpKind::ScanNodes, OpKind::Expand, OpKind::Return]),
supported_exprs: BTreeSet::new(),
supports_graph_ref: true,
supports_multi_graph: true,
supports_graph_params: false,
};
assert!(validate_plan_against_capabilities(&plan, &capabilities).is_ok());
}