use super::*;
use plexus_serde::serialize_plan;
#[test]
fn executes_filter_project_sort_limit() {
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::Gt,
lhs: Box::new(Expr::PropAccess {
col: 0,
prop: "age".to_string(),
}),
rhs: Box::new(Expr::IntLiteral(30)),
},
},
Op::Project {
input: 1,
exprs: vec![Expr::PropAccess {
col: 0,
prop: "name".to_string(),
}],
schema: vec![ColDef {
name: "name".to_string(),
kind: ColKind::Value,
logical_type: plexus_serde::LogicalType::Unknown,
}],
},
Op::Sort {
input: 2,
keys: vec![0],
dirs: vec![SortDir::Asc],
},
Op::Limit {
input: 3,
count: 1,
skip: 0,
},
Op::Return { input: 4 },
],
root_op: 5,
};
let mut engine = InMemoryEngine::new(fixture_graph());
let out = engine.execute_plan(&plan).expect("execute");
assert_eq!(out.rows, vec![vec![Value::String("Bob".to_string())]]);
}
#[test]
fn executes_sort_with_stable_tie_breaking() {
use std::collections::{HashMap, HashSet};
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::Project {
input: 0,
exprs: vec![
Expr::PropAccess {
col: 0,
prop: "age".to_string(),
},
Expr::PropAccess {
col: 0,
prop: "name".to_string(),
},
],
schema: vec![],
},
Op::Sort {
input: 1,
keys: vec![0],
dirs: vec![SortDir::Asc],
},
Op::Return { input: 2 },
],
root_op: 3,
};
let graph = Graph {
nodes: vec![
Node {
id: 1,
labels: HashSet::from(["Person".to_string()]),
props: HashMap::from([
("age".to_string(), Value::Int(30)),
("name".to_string(), Value::String("Zed".to_string())),
]),
},
Node {
id: 2,
labels: HashSet::from(["Person".to_string()]),
props: HashMap::from([
("age".to_string(), Value::Int(30)),
("name".to_string(), Value::String("Amy".to_string())),
]),
},
Node {
id: 3,
labels: HashSet::from(["Person".to_string()]),
props: HashMap::from([
("age".to_string(), Value::Int(31)),
("name".to_string(), Value::String("Bob".to_string())),
]),
},
],
rels: vec![],
};
let mut engine = InMemoryEngine::new(graph);
let out = engine.execute_plan(&plan).expect("execute");
assert_eq!(
out.rows,
vec![
vec![Value::Int(30), Value::String("Zed".to_string())],
vec![Value::Int(30), Value::String("Amy".to_string())],
vec![Value::Int(31), Value::String("Bob".to_string())],
]
);
}
#[test]
fn executes_expand_and_aggregate() {
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::Expand {
input: 0,
src_col: 0,
types: vec!["KNOWS".to_string()],
dir: ExpandDir::Out,
schema: vec![],
src_var: "n".to_string(),
rel_var: "r".to_string(),
dst_var: "m".to_string(),
legal_src_labels: vec!["Person".to_string()],
legal_dst_labels: vec!["Person".to_string()],
graph_ref: None,
est_degree: -1.0,
},
Op::Project {
input: 1,
exprs: vec![Expr::PropAccess {
col: 0,
prop: "name".to_string(),
}],
schema: vec![ColDef {
name: "src_name".to_string(),
kind: ColKind::Value,
logical_type: plexus_serde::LogicalType::Unknown,
}],
},
Op::Aggregate {
input: 2,
keys: vec![0],
aggs: vec![Expr::Agg {
fn_: AggFn::CountStar,
expr: None,
}],
schema: vec![],
},
Op::Sort {
input: 3,
keys: vec![0],
dirs: vec![SortDir::Asc],
},
Op::Return { input: 4 },
],
root_op: 5,
};
let mut engine = InMemoryEngine::new(fixture_graph());
let out = engine.execute_plan(&plan).expect("execute");
assert_eq!(
out.rows,
vec![
vec![Value::String("Alice".to_string()), Value::Int(1)],
vec![Value::String("Bob".to_string()), Value::Int(1)],
]
);
}
#[test]
fn executes_semi_expand_exists_filter() {
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::SemiExpand {
input: 0,
src_col: 0,
types: vec!["WORKS_AT".to_string()],
dir: ExpandDir::Out,
schema: vec![ColDef {
name: "n".to_string(),
kind: ColKind::Node,
logical_type: plexus_serde::LogicalType::Unknown,
}],
legal_src_labels: vec!["Person".to_string()],
legal_dst_labels: vec!["Company".to_string()],
},
Op::Project {
input: 1,
exprs: vec![Expr::PropAccess {
col: 0,
prop: "name".to_string(),
}],
schema: vec![ColDef {
name: "name".to_string(),
kind: ColKind::Value,
logical_type: plexus_serde::LogicalType::Unknown,
}],
},
Op::Return { input: 2 },
],
root_op: 3,
};
let mut engine = InMemoryEngine::new(fixture_graph());
let out = engine.execute_plan(&plan).expect("execute");
assert_eq!(out.rows, vec![vec![Value::String("Bob".to_string())]]);
}
#[test]
fn executes_path_construct() {
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::Expand {
input: 0,
src_col: 0,
types: vec!["KNOWS".to_string()],
dir: ExpandDir::Out,
schema: vec![],
src_var: "n".to_string(),
rel_var: "r".to_string(),
dst_var: "m".to_string(),
legal_src_labels: vec!["Person".to_string()],
legal_dst_labels: vec!["Person".to_string()],
graph_ref: None,
est_degree: -1.0,
},
Op::PathConstruct {
input: 1,
rel_cols: vec![1],
schema: vec![],
},
Op::Project {
input: 2,
exprs: vec![Expr::ColRef { idx: 3 }],
schema: vec![ColDef {
name: "path".to_string(),
kind: ColKind::Value,
logical_type: plexus_serde::LogicalType::Unknown,
}],
},
Op::Return { input: 3 },
],
root_op: 4,
};
let mut engine = InMemoryEngine::new(fixture_graph());
let out = engine.execute_plan(&plan).expect("execute");
assert_eq!(
out.rows,
vec![
vec![Value::List(vec![Value::RelRef(10)])],
vec![Value::List(vec![Value::RelRef(11)])],
]
);
}
#[test]
fn executes_expr_phase2_forms_except_list_comp() {
use std::collections::BTreeMap;
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::Project {
input: 0,
exprs: vec![
Expr::In {
expr: Box::new(Expr::IntLiteral(2)),
items: vec![Expr::IntLiteral(1), Expr::IntLiteral(2)],
},
Expr::ListLiteral {
items: vec![Expr::IntLiteral(1), Expr::StringLiteral("x".to_string())],
},
Expr::MapLiteral {
entries: vec![
("a".to_string(), Expr::IntLiteral(1)),
("b".to_string(), Expr::BoolLiteral(true)),
],
},
Expr::Exists {
expr: Box::new(Expr::PropAccess {
col: 0,
prop: "name".to_string(),
}),
},
],
schema: vec![],
},
Op::Return { input: 1 },
],
root_op: 2,
};
let mut engine = InMemoryEngine::new(fixture_graph());
let out = engine.execute_plan(&plan).expect("execute");
let mut map = BTreeMap::new();
map.insert("a".to_string(), Value::Int(1));
map.insert("b".to_string(), Value::Bool(true));
assert_eq!(
out.rows[0],
vec![
Value::Bool(true),
Value::List(vec![Value::Int(1), Value::String("x".to_string())]),
Value::Map(map),
Value::Bool(true),
]
);
}
#[test]
fn execute_serialized_with_in_memory_engine() {
let plan = Plan {
version: version(),
ops: vec![Op::ScanNodes {
labels: vec!["Company".to_string()],
schema: vec![ColDef {
name: "n".to_string(),
kind: ColKind::Node,
logical_type: plexus_serde::LogicalType::Unknown,
}],
must_labels: vec!["Company".to_string()],
forbidden_labels: vec![],
graph_ref: None,
est_rows: -1,
selectivity: 1.0,
}],
root_op: 0,
};
let bytes = serialize_plan(&plan).expect("serialize");
let mut engine = InMemoryEngine::new(fixture_graph());
let out = execute_serialized(&mut engine, &bytes).expect("execute");
assert_eq!(out.rows, vec![vec![Value::NodeRef(3)]]);
}
#[test]
fn executes_optional_expand() {
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::OptionalExpand {
input: 0,
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: "m".to_string(),
legal_src_labels: vec!["Person".to_string()],
legal_dst_labels: vec!["Company".to_string()],
graph_ref: None,
},
Op::Project {
input: 1,
exprs: vec![Expr::PropAccess {
col: 2,
prop: "name".to_string(),
}],
schema: vec![ColDef {
name: "company".to_string(),
kind: ColKind::Value,
logical_type: plexus_serde::LogicalType::Unknown,
}],
},
Op::Return { input: 2 },
],
root_op: 3,
};
let mut engine = InMemoryEngine::new(fixture_graph());
let out = engine.execute_plan(&plan).expect("execute");
assert_eq!(
out.rows,
vec![vec![Value::Null], vec![Value::String("Acme".to_string())]]
);
}
#[test]
fn executes_expand_var_len() {
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::ExpandVarLen {
input: 1,
src_col: 0,
types: vec!["KNOWS".to_string()],
dir: ExpandDir::Out,
min_hops: 1,
max_hops: 2,
schema: vec![],
src_var: "n".to_string(),
path_var: "p".to_string(),
dst_var: "m".to_string(),
graph_ref: None,
},
Op::Project {
input: 2,
exprs: vec![Expr::ColRef { idx: 2 }, Expr::ColRef { idx: 1 }],
schema: vec![
ColDef {
name: "dst".to_string(),
kind: ColKind::Node,
logical_type: plexus_serde::LogicalType::Unknown,
},
ColDef {
name: "path".to_string(),
kind: ColKind::Value,
logical_type: plexus_serde::LogicalType::Unknown,
},
],
},
Op::Return { input: 3 },
],
root_op: 4,
};
let mut engine = InMemoryEngine::new(fixture_graph());
let out = engine.execute_plan(&plan).expect("execute");
assert_eq!(
out.rows,
vec![
vec![Value::NodeRef(2), Value::List(vec![Value::RelRef(10)])],
vec![
Value::NodeRef(1),
Value::List(vec![Value::RelRef(10), Value::RelRef(11)])
],
]
);
}
#[test]
fn executes_union_distinct() {
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::Project {
input: 0,
exprs: vec![Expr::PropAccess {
col: 0,
prop: "name".to_string(),
}],
schema: vec![ColDef {
name: "name".to_string(),
kind: ColKind::Value,
logical_type: plexus_serde::LogicalType::Unknown,
}],
},
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::Project {
input: 2,
exprs: vec![Expr::PropAccess {
col: 0,
prop: "name".to_string(),
}],
schema: vec![ColDef {
name: "name".to_string(),
kind: ColKind::Value,
logical_type: plexus_serde::LogicalType::Unknown,
}],
},
Op::Union {
lhs: 1,
rhs: 3,
all: false,
schema: vec![ColDef {
name: "name".to_string(),
kind: ColKind::Value,
logical_type: plexus_serde::LogicalType::Unknown,
}],
},
Op::Sort {
input: 4,
keys: vec![0],
dirs: vec![SortDir::Asc],
},
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,
vec![
vec![Value::String("Alice".to_string())],
vec![Value::String("Bob".to_string())],
]
);
}
#[test]
fn executes_unwind() {
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::Project {
input: 0,
exprs: vec![Expr::PropAccess {
col: 0,
prop: "name".to_string(),
}],
schema: vec![ColDef {
name: "name".to_string(),
kind: ColKind::Value,
logical_type: plexus_serde::LogicalType::Unknown,
}],
},
Op::Aggregate {
input: 1,
keys: vec![],
aggs: vec![Expr::Agg {
fn_: AggFn::Collect,
expr: Some(Box::new(Expr::ColRef { idx: 0 })),
}],
schema: vec![ColDef {
name: "names".to_string(),
kind: ColKind::Value,
logical_type: plexus_serde::LogicalType::Unknown,
}],
},
Op::Unwind {
input: 2,
list_expr: Expr::ColRef { idx: 0 },
out_var: "name".to_string(),
schema: vec![],
},
Op::Project {
input: 3,
exprs: vec![Expr::ColRef { idx: 1 }],
schema: vec![ColDef {
name: "name".to_string(),
kind: ColKind::Value,
logical_type: plexus_serde::LogicalType::Unknown,
}],
},
Op::Sort {
input: 4,
keys: vec![0],
dirs: vec![SortDir::Asc],
},
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,
vec![
vec![Value::String("Alice".to_string())],
vec![Value::String("Bob".to_string())],
]
);
}
#[test]
fn executes_project_with_arith_and_params() {
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::Project {
input: 0,
exprs: vec![Expr::Arith {
op: plexus_serde::ArithOp::Mul,
lhs: Box::new(Expr::PropAccess {
col: 0,
prop: "age".to_string(),
}),
rhs: Box::new(Expr::Param {
name: "factor".to_string(),
expected_type: Some("f64".to_string()),
}),
}],
schema: vec![ColDef {
name: "scaled_age".to_string(),
kind: ColKind::Value,
logical_type: plexus_serde::LogicalType::Unknown,
}],
},
Op::Sort {
input: 1,
keys: vec![0],
dirs: vec![SortDir::Asc],
},
Op::Return { input: 2 },
],
root_op: 3,
};
let mut engine = InMemoryEngine::new(fixture_graph());
engine.set_param("factor", Value::Float(1.5));
let out = engine.execute_plan(&plan).expect("execute");
assert_eq!(
out.rows,
vec![vec![Value::Float(45.0)], vec![Value::Float(60.0)]]
);
}
#[test]
fn rejects_unbound_param() {
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::Project {
input: 0,
exprs: vec![Expr::Param {
name: "missing".to_string(),
expected_type: None,
}],
schema: vec![ColDef {
name: "x".to_string(),
kind: ColKind::Value,
logical_type: plexus_serde::LogicalType::Unknown,
}],
},
Op::Return { input: 1 },
],
root_op: 2,
};
let mut engine = InMemoryEngine::new(fixture_graph());
let err = engine
.execute_plan(&plan)
.expect_err("expected missing parameter");
assert!(matches!(err, ExecutionError::UnboundParam(name) if name == "missing"));
}
#[test]
fn executes_case_when_expression() {
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::Project {
input: 0,
exprs: vec![Expr::Case {
arms: vec![
(
Expr::Cmp {
op: CmpOp::Gt,
lhs: Box::new(Expr::PropAccess {
col: 0,
prop: "age".to_string(),
}),
rhs: Box::new(Expr::IntLiteral(35)),
},
Expr::StringLiteral("senior".to_string()),
),
(
Expr::Cmp {
op: CmpOp::Gt,
lhs: Box::new(Expr::PropAccess {
col: 0,
prop: "age".to_string(),
}),
rhs: Box::new(Expr::IntLiteral(20)),
},
Expr::StringLiteral("adult".to_string()),
),
],
else_expr: Some(Box::new(Expr::StringLiteral("junior".to_string()))),
}],
schema: vec![ColDef {
name: "bucket".to_string(),
kind: ColKind::Value,
logical_type: plexus_serde::LogicalType::Unknown,
}],
},
Op::Sort {
input: 1,
keys: vec![0],
dirs: vec![SortDir::Asc],
},
Op::Return { input: 2 },
],
root_op: 3,
};
let mut engine = InMemoryEngine::new(fixture_graph());
let out = engine.execute_plan(&plan).expect("execute");
assert_eq!(
out.rows,
vec![
vec![Value::String("adult".to_string())],
vec![Value::String("senior".to_string())],
]
);
}