#![forbid(unsafe_code)]
#![doc(html_root_url = "https://docs.rs/cyrs-plan/0.0.1")]
pub mod error;
pub mod lower;
pub mod pretty;
#[cfg(feature = "serde")]
pub mod ser;
pub use error::PlanLowerError;
use smol_str::SmolStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct VarId(pub u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct OpId(pub u32);
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct LabelSet(pub Vec<SmolStr>);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NodeSpec {
pub labels: LabelSet,
pub properties: Option<Expr>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RelSpec {
pub types: Vec<SmolStr>,
pub direction: Direction,
pub length: RelLength,
pub properties: Option<Expr>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Direction {
Outgoing,
Incoming,
Undirected,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum RelLength {
Single,
Variable {
min: Option<u64>,
max: Option<u64>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum UnionKind {
All,
Distinct,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Projection {
pub expr: Expr,
pub alias: SmolStr,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OrderKey {
pub expr: Expr,
pub dir: SortDir,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum SortDir {
Asc,
Desc,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AggExpr {
pub func: SmolStr,
pub args: Vec<Expr>,
pub distinct: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ReadOp {
Source {
label: Option<LabelSet>,
bind: VarId,
},
Expand {
input: OpId,
from: VarId,
rel: RelSpec,
to: NodeSpec,
bind_rel: VarId,
bind_to: VarId,
},
Filter {
input: OpId,
predicate: Expr,
},
Project {
input: OpId,
items: Vec<Projection>,
},
Aggregate {
input: OpId,
keys: Vec<Expr>,
aggs: Vec<AggExpr>,
},
OrderBy {
input: OpId,
keys: Vec<OrderKey>,
},
Skip {
input: OpId,
count: Expr,
},
Limit {
input: OpId,
count: Expr,
},
Distinct {
input: OpId,
},
Unwind {
input: OpId,
list: Expr,
bind: VarId,
},
Union {
left: OpId,
right: OpId,
kind: UnionKind,
},
With {
input: OpId,
items: Vec<Projection>,
filter: Option<Expr>,
},
OptionalJoin {
input: OpId,
pattern: Box<ReadOp>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum WriteOp {
CreateNode {
labels: Vec<SmolStr>,
props: Expr,
bind: Option<VarId>,
},
CreateRel {
from: VarId,
to: VarId,
rel_type: SmolStr,
props: Expr,
bind: Option<VarId>,
},
MergeNode {
labels: Vec<SmolStr>,
props: Expr,
on_create: Vec<WriteOp>,
on_match: Vec<WriteOp>,
bind: Option<VarId>,
},
MergeRel {
from: VarId,
to: VarId,
rel_type: SmolStr,
props: Expr,
on_create: Vec<WriteOp>,
on_match: Vec<WriteOp>,
bind: Option<VarId>,
},
SetProperty {
target: VarId,
prop: SmolStr,
value: Expr,
},
SetLabels {
target: VarId,
labels: Vec<SmolStr>,
},
RemoveProperty {
target: VarId,
prop: SmolStr,
},
RemoveLabels {
target: VarId,
labels: Vec<SmolStr>,
},
Delete {
targets: Vec<Expr>,
detach: bool,
},
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum Expr {
Null,
Bool(bool),
Int(i64),
Float(f64),
String(SmolStr),
Var(VarId),
Prop {
target: Box<Expr>,
prop: SmolStr,
},
Index {
target: Box<Expr>,
index: Box<Expr>,
},
Slice {
target: Box<Expr>,
start: Option<Box<Expr>>,
end: Option<Box<Expr>>,
},
List(Vec<Expr>),
Map(Vec<(SmolStr, Expr)>),
Call {
func: SmolStr,
args: Vec<Expr>,
},
BinOp {
op: BinOp,
lhs: Box<Expr>,
rhs: Box<Expr>,
},
UnaryOp {
op: UnaryOp,
operand: Box<Expr>,
},
Case {
scrutinee: Option<Box<Expr>>,
arms: Vec<(Expr, Expr)>,
otherwise: Option<Box<Expr>>,
},
IsNull {
operand: Box<Expr>,
negated: bool,
},
InList {
operand: Box<Expr>,
list: Box<Expr>,
},
ListPredicate {
kind: ListPredKind,
var: VarId,
iterable: Box<Expr>,
predicate: Option<Box<Expr>>,
},
Param {
name: SmolStr,
},
Exists {
pattern: Box<ReadOp>,
},
}
impl Eq for Expr {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum BinOp {
Add,
Sub,
Mul,
Div,
Mod,
Pow,
Eq,
Neq,
Lt,
Le,
Gt,
Ge,
And,
Or,
Xor,
In,
StartsWith,
EndsWith,
Contains,
RegexMatch,
Concat,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum UnaryOp {
Neg,
Not,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ListPredKind {
Any,
All,
None,
Single,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn read_op_source_all_nodes() {
let op = ReadOp::Source {
label: None,
bind: VarId(0),
};
assert_eq!(
op,
ReadOp::Source {
label: None,
bind: VarId(0)
}
);
let d1 = format!("{op:?}");
let d2 = format!("{op:?}");
assert_eq!(d1, d2);
}
#[test]
fn read_op_source_with_label() {
let op = ReadOp::Source {
label: Some(LabelSet(vec!["Person".into()])),
bind: VarId(0),
};
let debug = format!("{op:?}");
assert!(
debug.contains("Person"),
"label must appear in debug: {debug}"
);
}
#[test]
fn read_op_expand_composes() {
let expand = ReadOp::Expand {
input: OpId(0),
from: VarId(0),
rel: RelSpec {
types: vec!["KNOWS".into()],
direction: Direction::Outgoing,
length: RelLength::Single,
properties: None,
},
to: NodeSpec {
labels: LabelSet(vec![]),
properties: None,
},
bind_rel: VarId(1),
bind_to: VarId(2),
};
assert!(format!("{expand:?}").contains("KNOWS"));
}
#[test]
fn read_op_filter_predicate() {
let filter = ReadOp::Filter {
input: OpId(1),
predicate: Expr::Bool(true),
};
assert_eq!(
filter,
ReadOp::Filter {
input: OpId(1),
predicate: Expr::Bool(true)
}
);
}
#[test]
fn read_op_project_items() {
let project = ReadOp::Project {
input: OpId(0),
items: vec![Projection {
expr: Expr::Prop {
target: Box::new(Expr::Var(VarId(0))),
prop: "name".into(),
},
alias: "name".into(),
}],
};
let debug = format!("{project:?}");
assert!(debug.contains("name"));
}
#[test]
fn read_op_aggregate() {
let agg = ReadOp::Aggregate {
input: OpId(0),
keys: vec![Expr::Var(VarId(0))],
aggs: vec![AggExpr {
func: "count".into(),
args: vec![Expr::Var(VarId(0))],
distinct: false,
}],
};
let debug = format!("{agg:?}");
assert!(debug.contains("count"));
}
#[test]
fn read_op_order_by() {
let op = ReadOp::OrderBy {
input: OpId(0),
keys: vec![OrderKey {
expr: Expr::Var(VarId(0)),
dir: SortDir::Desc,
}],
};
assert!(format!("{op:?}").contains("Desc"));
}
#[test]
fn read_op_skip_limit() {
let skip = ReadOp::Skip {
input: OpId(0),
count: Expr::Int(10),
};
let limit = ReadOp::Limit {
input: OpId(0),
count: Expr::Int(5),
};
assert!(format!("{skip:?}").contains("10"));
assert!(format!("{limit:?}").contains('5'));
}
#[test]
fn read_op_distinct() {
let op = ReadOp::Distinct { input: OpId(0) };
assert_eq!(op, ReadOp::Distinct { input: OpId(0) });
}
#[test]
fn read_op_unwind() {
let op = ReadOp::Unwind {
input: OpId(0),
list: Expr::Var(VarId(0)),
bind: VarId(1),
};
assert!(format!("{op:?}").contains("VarId(1)"));
}
#[test]
fn read_op_union_all_and_distinct() {
let all = ReadOp::Union {
left: OpId(0),
right: OpId(1),
kind: UnionKind::All,
};
let distinct = ReadOp::Union {
left: OpId(0),
right: OpId(1),
kind: UnionKind::Distinct,
};
assert_ne!(all, distinct);
}
#[test]
fn read_op_with_filter() {
let with = ReadOp::With {
input: OpId(0),
items: vec![Projection {
expr: Expr::Var(VarId(0)),
alias: "n".into(),
}],
filter: Some(Expr::Bool(true)),
};
assert!(format!("{with:?}").contains("Bool(true)"));
}
#[test]
fn read_op_optional_join_boxed_subtree() {
let inner = ReadOp::Source {
label: None,
bind: VarId(1),
};
let op = ReadOp::OptionalJoin {
input: OpId(0),
pattern: Box::new(inner.clone()),
};
assert_eq!(
**match &op {
ReadOp::OptionalJoin { pattern, .. } => pattern,
_ => panic!(),
},
inner
);
}
#[test]
fn write_op_create_node() {
let op = WriteOp::CreateNode {
labels: vec!["Person".into()],
props: Expr::Map(vec![("name".into(), Expr::String("Alice".into()))]),
bind: Some(VarId(0)),
};
assert!(format!("{op:?}").contains("Person"));
}
#[test]
fn write_op_create_rel() {
let op = WriteOp::CreateRel {
from: VarId(0),
to: VarId(1),
rel_type: "KNOWS".into(),
props: Expr::Map(vec![]),
bind: None,
};
assert!(format!("{op:?}").contains("KNOWS"));
}
#[test]
fn write_op_merge_node_on_create_and_on_match() {
let on_create = vec![WriteOp::SetProperty {
target: VarId(0),
prop: "created".into(),
value: Expr::Bool(true),
}];
let on_match = vec![WriteOp::SetProperty {
target: VarId(0),
prop: "updated".into(),
value: Expr::Bool(true),
}];
let op = WriteOp::MergeNode {
labels: vec!["Person".into()],
props: Expr::Map(vec![]),
on_create,
on_match,
bind: Some(VarId(0)),
};
let debug = format!("{op:?}");
assert!(debug.contains("created"));
assert!(debug.contains("updated"));
}
#[test]
fn write_op_merge_rel() {
let op = WriteOp::MergeRel {
from: VarId(0),
to: VarId(1),
rel_type: "FOLLOWS".into(),
props: Expr::Map(vec![]),
on_create: vec![],
on_match: vec![],
bind: None,
};
assert!(format!("{op:?}").contains("FOLLOWS"));
}
#[test]
fn write_op_set_and_remove() {
let set_prop = WriteOp::SetProperty {
target: VarId(0),
prop: "age".into(),
value: Expr::Int(30),
};
let set_labels = WriteOp::SetLabels {
target: VarId(0),
labels: vec!["Admin".into()],
};
let rm_prop = WriteOp::RemoveProperty {
target: VarId(0),
prop: "age".into(),
};
let rm_labels = WriteOp::RemoveLabels {
target: VarId(0),
labels: vec!["Admin".into()],
};
assert!(format!("{set_prop:?}").contains("30"));
assert!(format!("{set_labels:?}").contains("Admin"));
assert!(format!("{rm_prop:?}").contains("age"));
assert!(format!("{rm_labels:?}").contains("Admin"));
}
#[test]
fn write_op_delete_and_detach_delete() {
let del = WriteOp::Delete {
targets: vec![Expr::Var(VarId(0))],
detach: false,
};
let detach = WriteOp::Delete {
targets: vec![Expr::Var(VarId(0))],
detach: true,
};
assert_ne!(del, detach);
assert!(format!("{detach:?}").contains("true"));
}
#[test]
fn expr_literals_and_var() {
assert_eq!(Expr::Null, Expr::Null);
assert_eq!(Expr::Bool(true), Expr::Bool(true));
assert_ne!(Expr::Bool(true), Expr::Bool(false));
assert_eq!(Expr::Int(42), Expr::Int(42));
assert_eq!(Expr::String("hi".into()), Expr::String("hi".into()));
assert_eq!(Expr::Var(VarId(3)), Expr::Var(VarId(3)));
}
#[test]
fn expr_float_structural_eq() {
assert_eq!(Expr::Float(1.5), Expr::Float(1.5));
assert_ne!(Expr::Float(1.5), Expr::Float(2.0));
let _ = Expr::Float(f64::NAN); }
#[test]
fn expr_prop_and_index() {
let prop = Expr::Prop {
target: Box::new(Expr::Var(VarId(0))),
prop: "name".into(),
};
let index = Expr::Index {
target: Box::new(Expr::Var(VarId(0))),
index: Box::new(Expr::Int(0)),
};
assert!(format!("{prop:?}").contains("name"));
assert!(format!("{index:?}").contains("Int(0)"));
}
#[test]
fn expr_list_and_map() {
let list = Expr::List(vec![Expr::Int(1), Expr::Int(2)]);
let map = Expr::Map(vec![("key".into(), Expr::String("val".into()))]);
assert!(format!("{list:?}").contains("Int(1)"));
assert!(format!("{map:?}").contains("key"));
}
#[test]
fn expr_call() {
let call = Expr::Call {
func: "toLower".into(),
args: vec![Expr::Var(VarId(0))],
};
assert!(format!("{call:?}").contains("toLower"));
}
#[test]
fn expr_bin_op_all_variants_are_debug() {
for op in [
BinOp::Add,
BinOp::Sub,
BinOp::Mul,
BinOp::Div,
BinOp::Mod,
BinOp::Pow,
BinOp::Eq,
BinOp::Neq,
BinOp::Lt,
BinOp::Le,
BinOp::Gt,
BinOp::Ge,
BinOp::And,
BinOp::Or,
BinOp::Xor,
BinOp::In,
BinOp::StartsWith,
BinOp::EndsWith,
BinOp::Contains,
BinOp::RegexMatch,
BinOp::Concat,
] {
let _ = format!("{op:?}");
}
}
#[test]
fn expr_unary_op() {
let neg = Expr::UnaryOp {
op: UnaryOp::Neg,
operand: Box::new(Expr::Int(1)),
};
let not = Expr::UnaryOp {
op: UnaryOp::Not,
operand: Box::new(Expr::Bool(false)),
};
assert!(format!("{neg:?}").contains("Neg"));
assert!(format!("{not:?}").contains("Not"));
}
#[test]
fn expr_case() {
let case = Expr::Case {
scrutinee: None,
arms: vec![(Expr::Bool(true), Expr::Int(1))],
otherwise: Some(Box::new(Expr::Null)),
};
assert!(format!("{case:?}").contains("Int(1)"));
}
#[test]
fn expr_is_null_and_in_list() {
let is_null = Expr::IsNull {
operand: Box::new(Expr::Var(VarId(0))),
negated: false,
};
let in_list = Expr::InList {
operand: Box::new(Expr::Var(VarId(0))),
list: Box::new(Expr::List(vec![])),
};
assert!(format!("{is_null:?}").contains("negated: false"));
let _ = format!("{in_list:?}");
}
#[test]
fn expr_param() {
let p = Expr::Param {
name: "userId".into(),
};
assert!(format!("{p:?}").contains("userId"));
}
#[test]
fn debug_output_is_deterministic() {
let plan = ReadOp::Project {
input: OpId(0),
items: vec![
Projection {
expr: Expr::Var(VarId(1)),
alias: "a".into(),
},
Projection {
expr: Expr::Var(VarId(2)),
alias: "b".into(),
},
],
};
let first = format!("{plan:?}");
let second = format!("{plan:?}");
assert_eq!(first, second);
}
#[test]
fn build_simple_read_plan() {
let source = ReadOp::Source {
label: Some(LabelSet(vec!["Person".into()])),
bind: VarId(0),
};
let project = ReadOp::Project {
input: OpId(0),
items: vec![Projection {
expr: Expr::Prop {
target: Box::new(Expr::Var(VarId(0))),
prop: "name".into(),
},
alias: "name".into(),
}],
};
let _ = (source, project);
}
#[test]
fn var_id_and_op_id_are_copy_and_hash() {
use std::collections::HashSet;
let mut ids: HashSet<VarId> = HashSet::new();
ids.insert(VarId(0));
ids.insert(VarId(1));
ids.insert(VarId(0)); assert_eq!(ids.len(), 2);
let v = VarId(7);
let v2 = v; assert_eq!(v, v2);
let mut ops: HashSet<OpId> = HashSet::new();
ops.insert(OpId(0));
assert_eq!(ops.len(), 1);
}
}