use super::*;
#[test]
fn executes_create_node_set_remove_delete_detach() {
let plan = Plan {
version: 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!["Person".to_string()],
forbidden_labels: vec![],
graph_ref: None,
est_rows: -1,
selectivity: 1.0,
},
Op::Filter {
input: 0,
predicate: Expr::Cmp {
op: CmpOp::Eq,
lhs: Box::new(Expr::PropAccess {
col: 0,
prop: "name".to_string(),
}),
rhs: Box::new(Expr::StringLiteral("Alice".to_string())),
},
},
Op::CreateNode {
input: 1,
labels: vec!["Person".to_string()],
props: Expr::MapLiteral {
entries: vec![
(
"name".to_string(),
Expr::StringLiteral("Charlie".to_string()),
),
("age".to_string(), Expr::IntLiteral(25)),
],
},
schema: vec![],
out_var: "m".to_string(),
},
Op::SetProperty {
input: 2,
target_col: 1,
key: "city".to_string(),
value_expr: Expr::StringLiteral("SF".to_string()),
schema: vec![],
},
Op::RemoveProperty {
input: 3,
target_col: 0,
key: "age".to_string(),
schema: vec![],
},
Op::Delete {
input: 4,
target_col: 0,
detach: true,
schema: vec![],
},
Op::Return { input: 5 },
],
root_op: 6,
};
let mut engine = InMemoryEngine::new(fixture_graph());
let out = engine.execute_plan(&plan).expect("execute");
assert_eq!(out.rows.len(), 1);
assert_eq!(
engine.graph.nodes.len(),
3,
"create + detach-delete should net to 3 nodes"
);
assert_eq!(
engine.graph.rels.len(),
1,
"detach-delete should remove Alice incident relationships",
);
assert!(engine.graph.nodes.iter().any(|n| n.props.get("name")
== Some(&Value::String("Charlie".to_string()))
&& n.props.get("city") == Some(&Value::String("SF".to_string()))
&& n.labels.contains("Person")));
assert!(!engine
.graph
.nodes
.iter()
.any(|n| n.props.get("name") == Some(&Value::String("Alice".to_string()))));
}
#[test]
fn executes_create_rel_set_property_then_delete_rel() {
let plan = Plan {
version: 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!["Person".to_string()],
forbidden_labels: vec![],
graph_ref: None,
est_rows: -1,
selectivity: 1.0,
},
Op::Filter {
input: 0,
predicate: Expr::Cmp {
op: CmpOp::Eq,
lhs: Box::new(Expr::PropAccess {
col: 0,
prop: "name".to_string(),
}),
rhs: Box::new(Expr::StringLiteral("Bob".to_string())),
},
},
Op::Expand {
input: 1,
src_col: 0,
types: vec!["WORKS_AT".to_string()],
dir: ExpandDir::Out,
schema: vec![],
src_var: "n".to_string(),
rel_var: "r".to_string(),
dst_var: "c".to_string(),
legal_src_labels: vec!["Person".to_string()],
legal_dst_labels: vec!["Company".to_string()],
graph_ref: None,
est_degree: -1.0,
},
Op::CreateRel {
input: 2,
src_col: 0,
dst_col: 2,
rel_type: "MANAGES".to_string(),
props: Expr::NullLiteral,
schema: vec![],
out_var: "r2".to_string(),
},
Op::SetProperty {
input: 3,
target_col: 3,
key: "since".to_string(),
value_expr: Expr::IntLiteral(2026),
schema: vec![],
},
Op::Delete {
input: 4,
target_col: 3,
detach: false,
schema: vec![],
},
Op::Return { input: 5 },
],
root_op: 6,
};
let mut engine = InMemoryEngine::new(fixture_graph());
let initial_rel_count = engine.graph.rels.len();
engine.execute_plan(&plan).expect("execute");
assert_eq!(engine.graph.rels.len(), initial_rel_count);
assert!(!engine.graph.rels.iter().any(|r| r.typ == "MANAGES"));
}
#[test]
fn cypher_dml_bridge_bytes_to_execute_smoke() {
let cypher =
"MATCH (n:Person) WHERE n.name = 'Alice' CREATE (m:Person) SET m.name = 'Charlie' RETURN m";
let query = parse(cypher).expect("parse");
let bytes = lower_to_flatbuffer(&query).expect("bridge lower_to_flatbuffer");
let mut engine = InMemoryEngine::new(fixture_graph());
let out = execute_serialized(&mut engine, &bytes).expect("execute");
assert_eq!(out.rows.len(), 1);
assert_eq!(out.rows[0].len(), 1);
assert!(matches!(out.rows[0][0], Value::NodeRef(_)));
assert!(engine.graph.nodes.iter().any(|n| n.props.get("name")
== Some(&Value::String("Charlie".to_string()))
&& n.labels.contains("Person")));
}
#[test]
fn executes_merge_creates_when_missing() {
let merge_pattern = Expr::MapLiteral {
entries: vec![
(
"kind".to_string(),
Expr::StringLiteral("linear_v1".to_string()),
),
(
"nodes".to_string(),
Expr::ListLiteral {
items: vec![
Expr::MapLiteral {
entries: vec![
("var".to_string(), Expr::StringLiteral("n".to_string())),
(
"labels".to_string(),
Expr::ListLiteral {
items: vec![Expr::StringLiteral("Company".to_string())],
},
),
],
},
Expr::MapLiteral {
entries: vec![
("var".to_string(), Expr::NullLiteral),
(
"labels".to_string(),
Expr::ListLiteral {
items: vec![Expr::StringLiteral("Person".to_string())],
},
),
],
},
],
},
),
(
"rels".to_string(),
Expr::ListLiteral {
items: vec![Expr::MapLiteral {
entries: vec![
("var".to_string(), Expr::NullLiteral),
(
"types".to_string(),
Expr::ListLiteral {
items: vec![Expr::StringLiteral("MANAGES".to_string())],
},
),
("dir".to_string(), Expr::StringLiteral("out".to_string())),
],
}],
},
),
],
};
let plan = Plan {
version: 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!["Person".to_string()],
forbidden_labels: vec![],
graph_ref: None,
est_rows: -1,
selectivity: 1.0,
},
Op::Filter {
input: 0,
predicate: Expr::Cmp {
op: CmpOp::Eq,
lhs: Box::new(Expr::PropAccess {
col: 0,
prop: "name".to_string(),
}),
rhs: Box::new(Expr::StringLiteral("Alice".to_string())),
},
},
Op::Merge {
input: 1,
pattern: merge_pattern,
on_create_props: Expr::NullLiteral,
on_match_props: Expr::NullLiteral,
schema: vec![ColDef {
name: "n".to_string(),
kind: ColKind::Node,
logical_type: plexus_serde::LogicalType::Unknown,
}],
},
Op::Return { input: 2 },
],
root_op: 3,
};
let mut engine = InMemoryEngine::new(fixture_graph());
let initial_nodes = engine.graph.nodes.len();
let initial_rels = engine.graph.rels.len();
engine.execute_plan(&plan).expect("execute");
assert_eq!(engine.graph.nodes.len(), initial_nodes + 1);
assert_eq!(engine.graph.rels.len(), initial_rels + 1);
}
#[test]
fn cypher_merge_bridge_bytes_is_idempotent_across_runs() {
let cypher =
"MATCH (n:Person) WHERE n.name = 'Alice' MERGE (n)-[:MANAGES]->(m:Company) RETURN n";
let query = parse(cypher).expect("parse");
let bytes = lower_to_flatbuffer(&query).expect("bridge lower_to_flatbuffer");
let mut engine = InMemoryEngine::new(fixture_graph());
let initial_nodes = engine.graph.nodes.len();
let initial_rels = engine.graph.rels.len();
execute_serialized(&mut engine, &bytes).expect("execute #1");
assert_eq!(engine.graph.nodes.len(), initial_nodes + 1);
assert_eq!(engine.graph.rels.len(), initial_rels + 1);
execute_serialized(&mut engine, &bytes).expect("execute #2");
assert_eq!(engine.graph.nodes.len(), initial_nodes + 1);
assert_eq!(engine.graph.rels.len(), initial_rels + 1);
}
#[test]
fn executes_merge_applies_on_create_and_on_match_props() {
let merge_pattern = Expr::MapLiteral {
entries: vec![
(
"kind".to_string(),
Expr::StringLiteral("linear_v1".to_string()),
),
(
"nodes".to_string(),
Expr::ListLiteral {
items: vec![
Expr::MapLiteral {
entries: vec![
("var".to_string(), Expr::StringLiteral("n".to_string())),
(
"labels".to_string(),
Expr::ListLiteral {
items: vec![Expr::StringLiteral("Person".to_string())],
},
),
],
},
Expr::MapLiteral {
entries: vec![
("var".to_string(), Expr::NullLiteral),
(
"labels".to_string(),
Expr::ListLiteral {
items: vec![Expr::StringLiteral("Company".to_string())],
},
),
],
},
],
},
),
(
"rels".to_string(),
Expr::ListLiteral {
items: vec![Expr::MapLiteral {
entries: vec![
("var".to_string(), Expr::NullLiteral),
(
"types".to_string(),
Expr::ListLiteral {
items: vec![Expr::StringLiteral("MANAGES".to_string())],
},
),
("dir".to_string(), Expr::StringLiteral("out".to_string())),
],
}],
},
),
],
};
let plan = Plan {
version: 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!["Person".to_string()],
forbidden_labels: vec![],
graph_ref: None,
est_rows: -1,
selectivity: 1.0,
},
Op::Filter {
input: 0,
predicate: Expr::Cmp {
op: CmpOp::Eq,
lhs: Box::new(Expr::PropAccess {
col: 0,
prop: "name".to_string(),
}),
rhs: Box::new(Expr::StringLiteral("Alice".to_string())),
},
},
Op::Merge {
input: 1,
pattern: merge_pattern,
on_create_props: Expr::MapLiteral {
entries: vec![(
"created_flag".to_string(),
Expr::StringLiteral("yes".to_string()),
)],
},
on_match_props: Expr::MapLiteral {
entries: vec![("matched_count".to_string(), Expr::IntLiteral(2))],
},
schema: vec![ColDef {
name: "n".to_string(),
kind: ColKind::Node,
logical_type: plexus_serde::LogicalType::Unknown,
}],
},
Op::Return { input: 2 },
],
root_op: 3,
};
let mut engine = InMemoryEngine::new(fixture_graph());
engine.execute_plan(&plan).expect("execute #1");
let rel = engine
.graph
.rels
.iter()
.find(|r| r.typ == "MANAGES")
.expect("created MANAGES rel");
assert_eq!(
rel.props.get("created_flag"),
Some(&Value::String("yes".to_string()))
);
assert!(!rel.props.contains_key("matched_count"));
engine.execute_plan(&plan).expect("execute #2");
let rel = engine
.graph
.rels
.iter()
.find(|r| r.typ == "MANAGES")
.expect("MANAGES rel persists");
assert_eq!(rel.props.get("matched_count"), Some(&Value::Int(2)));
}
#[test]
fn cypher_merge_optimized_serialized_deserialized_execute() {
let cypher =
"MATCH (n:Person) WHERE n.name = 'Alice' MERGE (n)-[:MANAGES]->(m:Company) RETURN n";
let query = parse(cypher).expect("parse");
let ctx = make_context();
let mut module = lower(&query, &ctx).expect("lower");
optimize(&mut module, &ctx, PIPELINE_CORE).expect("optimize");
let bytes = module_to_flatbuffer(&module).expect("module_to_flatbuffer");
let mut engine = InMemoryEngine::new(fixture_graph());
let initial_nodes = engine.graph.nodes.len();
let initial_rels = engine.graph.rels.len();
execute_serialized(&mut engine, &bytes).expect("execute #1");
assert_eq!(engine.graph.nodes.len(), initial_nodes + 1);
assert_eq!(engine.graph.rels.len(), initial_rels + 1);
execute_serialized(&mut engine, &bytes).expect("execute #2");
assert_eq!(engine.graph.nodes.len(), initial_nodes + 1);
assert_eq!(engine.graph.rels.len(), initial_rels + 1);
}
#[test]
fn cypher_merge_multi_hop_bridge_bytes_is_idempotent_across_runs() {
let cypher = "MATCH (n:Person) WHERE n.name = 'Alice' MERGE (n)-[:MANAGES]->(m:Company)-[:LOCATED_IN]->(c:City) RETURN n";
let query = parse(cypher).expect("parse");
let bytes = lower_to_flatbuffer(&query).expect("bridge lower_to_flatbuffer");
let mut engine = InMemoryEngine::new(fixture_graph());
let initial_nodes = engine.graph.nodes.len();
let initial_rels = engine.graph.rels.len();
execute_serialized(&mut engine, &bytes).expect("execute #1");
assert_eq!(engine.graph.nodes.len(), initial_nodes + 2);
assert_eq!(engine.graph.rels.len(), initial_rels + 2);
execute_serialized(&mut engine, &bytes).expect("execute #2");
assert_eq!(engine.graph.nodes.len(), initial_nodes + 2);
assert_eq!(engine.graph.rels.len(), initial_rels + 2);
}
#[test]
fn cypher_merge_multi_hop_optimized_serialized_deserialized_execute() {
let cypher = "MATCH (n:Person) WHERE n.name = 'Alice' MERGE (n)-[:MANAGES]->(m:Company)-[:LOCATED_IN]->(c:City) RETURN n";
let query = parse(cypher).expect("parse");
let ctx = make_context();
let mut module = lower(&query, &ctx).expect("lower");
optimize(&mut module, &ctx, PIPELINE_CORE).expect("optimize");
let bytes = module_to_flatbuffer(&module).expect("module_to_flatbuffer");
let mut engine = InMemoryEngine::new(fixture_graph());
let initial_nodes = engine.graph.nodes.len();
let initial_rels = engine.graph.rels.len();
execute_serialized(&mut engine, &bytes).expect("execute #1");
assert_eq!(engine.graph.nodes.len(), initial_nodes + 2);
assert_eq!(engine.graph.rels.len(), initial_rels + 2);
execute_serialized(&mut engine, &bytes).expect("execute #2");
assert_eq!(engine.graph.nodes.len(), initial_nodes + 2);
assert_eq!(engine.graph.rels.len(), initial_rels + 2);
}
#[test]
fn cypher_merge_branching_bridge_bytes_is_idempotent_across_runs() {
let cypher = "MATCH (n:Person) WHERE n.name = 'Alice' MERGE (n)-[:MANAGES]->(m:Company), (n)-[:VISITS]->(c:City) RETURN n";
let query = parse(cypher).expect("parse");
let bytes = lower_to_flatbuffer(&query).expect("bridge lower_to_flatbuffer");
let mut engine = InMemoryEngine::new(fixture_graph());
let initial_nodes = engine.graph.nodes.len();
let initial_rels = engine.graph.rels.len();
execute_serialized(&mut engine, &bytes).expect("execute #1");
assert_eq!(engine.graph.nodes.len(), initial_nodes + 2);
assert_eq!(engine.graph.rels.len(), initial_rels + 2);
execute_serialized(&mut engine, &bytes).expect("execute #2");
assert_eq!(engine.graph.nodes.len(), initial_nodes + 2);
assert_eq!(engine.graph.rels.len(), initial_rels + 2);
}
#[test]
fn cypher_merge_branching_optimized_serialized_deserialized_execute() {
let cypher = "MATCH (n:Person) WHERE n.name = 'Alice' MERGE (n)-[:MANAGES]->(m:Company), (n)-[:VISITS]->(c:City) RETURN n";
let query = parse(cypher).expect("parse");
let ctx = make_context();
let mut module = lower(&query, &ctx).expect("lower");
optimize(&mut module, &ctx, PIPELINE_CORE).expect("optimize");
let bytes = module_to_flatbuffer(&module).expect("module_to_flatbuffer");
let mut engine = InMemoryEngine::new(fixture_graph());
let initial_nodes = engine.graph.nodes.len();
let initial_rels = engine.graph.rels.len();
execute_serialized(&mut engine, &bytes).expect("execute #1");
assert_eq!(engine.graph.nodes.len(), initial_nodes + 2);
assert_eq!(engine.graph.rels.len(), initial_rels + 2);
execute_serialized(&mut engine, &bytes).expect("execute #2");
assert_eq!(engine.graph.nodes.len(), initial_nodes + 2);
assert_eq!(engine.graph.rels.len(), initial_rels + 2);
}
#[test]
fn executes_merge_graph_v1_branching_idempotent() {
let graph_pattern = Expr::MapLiteral {
entries: vec![
(
"kind".to_string(),
Expr::StringLiteral("graph_v1".to_string()),
),
(
"nodes".to_string(),
Expr::ListLiteral {
items: vec![
Expr::MapLiteral {
entries: vec![
("var".to_string(), Expr::StringLiteral("n".to_string())),
(
"labels".to_string(),
Expr::ListLiteral {
items: vec![Expr::StringLiteral("Person".to_string())],
},
),
],
},
Expr::MapLiteral {
entries: vec![
("var".to_string(), Expr::NullLiteral),
(
"labels".to_string(),
Expr::ListLiteral {
items: vec![Expr::StringLiteral("Company".to_string())],
},
),
],
},
Expr::MapLiteral {
entries: vec![
("var".to_string(), Expr::NullLiteral),
(
"labels".to_string(),
Expr::ListLiteral {
items: vec![Expr::StringLiteral("City".to_string())],
},
),
],
},
],
},
),
(
"rels".to_string(),
Expr::ListLiteral {
items: vec![
Expr::MapLiteral {
entries: vec![
("src".to_string(), Expr::IntLiteral(0)),
("dst".to_string(), Expr::IntLiteral(1)),
("var".to_string(), Expr::NullLiteral),
(
"types".to_string(),
Expr::ListLiteral {
items: vec![Expr::StringLiteral("MANAGES".to_string())],
},
),
("dir".to_string(), Expr::StringLiteral("out".to_string())),
],
},
Expr::MapLiteral {
entries: vec![
("src".to_string(), Expr::IntLiteral(0)),
("dst".to_string(), Expr::IntLiteral(2)),
("var".to_string(), Expr::NullLiteral),
(
"types".to_string(),
Expr::ListLiteral {
items: vec![Expr::StringLiteral("VISITS".to_string())],
},
),
("dir".to_string(), Expr::StringLiteral("out".to_string())),
],
},
],
},
),
],
};
let plan = Plan {
version: 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!["Person".to_string()],
forbidden_labels: vec![],
graph_ref: None,
est_rows: -1,
selectivity: 1.0,
},
Op::Filter {
input: 0,
predicate: Expr::Cmp {
op: CmpOp::Eq,
lhs: Box::new(Expr::PropAccess {
col: 0,
prop: "name".to_string(),
}),
rhs: Box::new(Expr::StringLiteral("Alice".to_string())),
},
},
Op::Merge {
input: 1,
pattern: graph_pattern,
on_create_props: Expr::NullLiteral,
on_match_props: Expr::NullLiteral,
schema: vec![ColDef {
name: "n".to_string(),
kind: ColKind::Node,
logical_type: plexus_serde::LogicalType::Unknown,
}],
},
Op::Return { input: 2 },
],
root_op: 3,
};
let mut engine = InMemoryEngine::new(fixture_graph());
let initial_nodes = engine.graph.nodes.len();
let initial_rels = engine.graph.rels.len();
engine.execute_plan(&plan).expect("execute #1");
assert_eq!(engine.graph.nodes.len(), initial_nodes + 2);
assert_eq!(engine.graph.rels.len(), initial_rels + 2);
engine.execute_plan(&plan).expect("execute #2");
assert_eq!(engine.graph.nodes.len(), initial_nodes + 2);
assert_eq!(engine.graph.rels.len(), initial_rels + 2);
}