#![warn(missing_docs)]
#![warn(clippy::all)]
#![deny(unsafe_code)]
use std::collections::{BTreeMap, HashMap};
use std::marker::PhantomData;
use chrono::{SecondsFormat, Utc};
use serde::{Deserialize, Serialize};
pub use crate::query_generator::*;
pub use helix_dsl_macros::register;
#[doc(hidden)]
pub mod __private {
use std::collections::BTreeMap;
pub use inventory;
pub fn dynamic_query_value_from_property_value(
value: crate::PropertyValue,
path: impl Into<String>,
) -> Result<crate::DynamicQueryValue, crate::DynamicQueryError> {
fn convert(
value: crate::PropertyValue,
path: String,
) -> Result<crate::DynamicQueryValue, crate::DynamicQueryError> {
Ok(match value {
crate::PropertyValue::Null => crate::DynamicQueryValue::Null,
crate::PropertyValue::Bool(value) => crate::DynamicQueryValue::Bool(value),
crate::PropertyValue::I64(value) => crate::DynamicQueryValue::I64(value),
crate::PropertyValue::DateTime(value) => crate::DynamicQueryValue::String(
crate::DateTime::from_millis(value)
.to_rfc3339()
.ok_or_else(|| crate::DynamicQueryError::invalid_datetime(path, value))?,
),
crate::PropertyValue::F64(value) => crate::DynamicQueryValue::F64(value),
crate::PropertyValue::F32(value) => crate::DynamicQueryValue::F32(value),
crate::PropertyValue::String(value) => crate::DynamicQueryValue::String(value),
crate::PropertyValue::Bytes(_) => {
return Err(crate::DynamicQueryError::unsupported_bytes(path));
}
crate::PropertyValue::I64Array(values) => crate::DynamicQueryValue::Array(
values
.into_iter()
.map(crate::DynamicQueryValue::I64)
.collect(),
),
crate::PropertyValue::F64Array(values) => crate::DynamicQueryValue::Array(
values
.into_iter()
.map(crate::DynamicQueryValue::F64)
.collect(),
),
crate::PropertyValue::F32Array(values) => crate::DynamicQueryValue::Array(
values
.into_iter()
.map(crate::DynamicQueryValue::F32)
.collect(),
),
crate::PropertyValue::StringArray(values) => crate::DynamicQueryValue::Array(
values
.into_iter()
.map(crate::DynamicQueryValue::String)
.collect(),
),
crate::PropertyValue::Array(values) => crate::DynamicQueryValue::Array(
values
.into_iter()
.enumerate()
.map(|(index, value)| convert(value, format!("{}[{}]", path, index)))
.collect::<Result<Vec<_>, _>>()?,
),
crate::PropertyValue::Object(values) => crate::DynamicQueryValue::Object(
values
.into_iter()
.map(|(key, value)| {
let entry_path = format!("{}.{}", path, key);
Ok((key, convert(value, entry_path)?))
})
.collect::<Result<BTreeMap<_, _>, crate::DynamicQueryError>>()?,
),
})
}
convert(value, path.into())
}
}
pub type NodeId = u64;
pub type EdgeId = u64;
pub type ParamValue = PropertyValue;
pub type ParamObject = BTreeMap<String, PropertyValue>;
#[doc(hidden)]
pub trait TraversalState: private::Sealed {}
mod private {
pub trait Sealed {}
impl Sealed for super::Empty {}
impl Sealed for super::OnNodes {}
impl Sealed for super::OnEdges {}
impl Sealed for super::Terminal {}
impl Sealed for super::ReadOnly {}
impl Sealed for super::WriteEnabled {}
}
#[doc(hidden)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Empty;
#[doc(hidden)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OnNodes;
#[doc(hidden)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OnEdges;
#[doc(hidden)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Terminal;
impl TraversalState for Empty {}
impl TraversalState for OnNodes {}
impl TraversalState for OnEdges {}
impl TraversalState for Terminal {}
#[doc(hidden)]
pub trait MutationMode: private::Sealed {}
#[doc(hidden)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ReadOnly;
#[doc(hidden)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WriteEnabled;
impl MutationMode for ReadOnly {}
impl MutationMode for WriteEnabled {}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum PropertyValue {
Null,
Bool(bool),
I64(i64),
DateTime(i64),
F64(f64),
F32(f32),
String(String),
Bytes(Vec<u8>),
I64Array(Vec<i64>),
F64Array(Vec<f64>),
F32Array(Vec<f32>),
StringArray(Vec<String>),
Array(Vec<PropertyValue>),
Object(BTreeMap<String, PropertyValue>),
}
impl PropertyValue {
pub fn as_str(&self) -> Option<&str> {
match self {
PropertyValue::String(s) => Some(s),
_ => None,
}
}
pub fn as_i64(&self) -> Option<i64> {
match self {
PropertyValue::I64(n) => Some(*n),
_ => None,
}
}
pub fn datetime_millis(millis: i64) -> Self {
Self::DateTime(millis)
}
pub fn as_datetime_millis(&self) -> Option<i64> {
match self {
PropertyValue::DateTime(millis) => Some(*millis),
_ => None,
}
}
pub fn as_f64(&self) -> Option<f64> {
match self {
PropertyValue::F64(n) => Some(*n),
PropertyValue::F32(n) => Some(*n as f64),
_ => None,
}
}
pub fn as_bool(&self) -> Option<bool> {
match self {
PropertyValue::Bool(b) => Some(*b),
_ => None,
}
}
pub fn as_array(&self) -> Option<&[PropertyValue]> {
match self {
PropertyValue::Array(values) => Some(values),
_ => None,
}
}
pub fn as_object(&self) -> Option<&BTreeMap<String, PropertyValue>> {
match self {
PropertyValue::Object(values) => Some(values),
_ => None,
}
}
}
impl From<&str> for PropertyValue {
fn from(s: &str) -> Self {
PropertyValue::String(s.to_string())
}
}
impl From<String> for PropertyValue {
fn from(s: String) -> Self {
PropertyValue::String(s)
}
}
impl From<i64> for PropertyValue {
fn from(n: i64) -> Self {
PropertyValue::I64(n)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct DateTime(i64);
impl DateTime {
pub fn from_millis(millis: i64) -> Self {
Self(millis)
}
pub fn parse_rfc3339(input: &str) -> Result<Self, chrono::ParseError> {
Ok(Self(
chrono::DateTime::parse_from_rfc3339(input)?
.with_timezone(&Utc)
.timestamp_millis(),
))
}
pub fn millis(self) -> i64 {
self.0
}
pub fn to_rfc3339(self) -> Option<String> {
chrono::DateTime::<Utc>::from_timestamp_millis(self.0)
.map(|dt| dt.to_rfc3339_opts(SecondsFormat::Millis, true))
}
}
impl From<DateTime> for PropertyValue {
fn from(value: DateTime) -> Self {
PropertyValue::DateTime(value.millis())
}
}
impl From<i32> for PropertyValue {
fn from(n: i32) -> Self {
PropertyValue::I64(n as i64)
}
}
impl From<f64> for PropertyValue {
fn from(n: f64) -> Self {
PropertyValue::F64(n)
}
}
impl From<f32> for PropertyValue {
fn from(n: f32) -> Self {
PropertyValue::F32(n)
}
}
impl From<bool> for PropertyValue {
fn from(b: bool) -> Self {
PropertyValue::Bool(b)
}
}
impl From<Vec<u8>> for PropertyValue {
fn from(bytes: Vec<u8>) -> Self {
PropertyValue::Bytes(bytes)
}
}
impl From<Vec<i64>> for PropertyValue {
fn from(values: Vec<i64>) -> Self {
PropertyValue::I64Array(values)
}
}
impl From<Vec<f64>> for PropertyValue {
fn from(values: Vec<f64>) -> Self {
PropertyValue::F64Array(values)
}
}
impl From<Vec<f32>> for PropertyValue {
fn from(values: Vec<f32>) -> Self {
PropertyValue::F32Array(values)
}
}
impl From<Vec<String>> for PropertyValue {
fn from(values: Vec<String>) -> Self {
PropertyValue::StringArray(values)
}
}
impl From<Vec<PropertyValue>> for PropertyValue {
fn from(values: Vec<PropertyValue>) -> Self {
PropertyValue::Array(values)
}
}
impl From<BTreeMap<String, PropertyValue>> for PropertyValue {
fn from(values: BTreeMap<String, PropertyValue>) -> Self {
PropertyValue::Object(values)
}
}
impl From<HashMap<String, PropertyValue>> for PropertyValue {
fn from(values: HashMap<String, PropertyValue>) -> Self {
PropertyValue::Object(values.into_iter().collect())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum PropertyInput {
Value(PropertyValue),
Expr(Expr),
}
impl PropertyInput {
pub fn param(name: impl Into<String>) -> Self {
Self::Expr(Expr::param(name))
}
#[doc(hidden)]
pub fn into_expr(self) -> Expr {
match self {
PropertyInput::Value(v) => Expr::Constant(v),
PropertyInput::Expr(e) => e,
}
}
}
impl<T> From<T> for PropertyInput
where
PropertyValue: From<T>,
{
fn from(value: T) -> Self {
Self::Value(value.into())
}
}
impl From<Expr> for PropertyInput {
fn from(value: Expr) -> Self {
Self::Expr(value)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum NodeRef {
All,
Ids(Vec<NodeId>),
Var(String),
Param(String),
}
impl NodeRef {
pub fn all() -> Self {
NodeRef::All
}
pub fn id(id: NodeId) -> Self {
NodeRef::Ids(vec![id])
}
pub fn ids(ids: impl IntoIterator<Item = NodeId>) -> Self {
NodeRef::Ids(ids.into_iter().collect())
}
pub fn var(name: impl Into<String>) -> Self {
NodeRef::Var(name.into())
}
pub fn param(name: impl Into<String>) -> Self {
NodeRef::Param(name.into())
}
}
impl From<NodeId> for NodeRef {
fn from(id: NodeId) -> Self {
NodeRef::Ids(vec![id])
}
}
impl From<Vec<NodeId>> for NodeRef {
fn from(ids: Vec<NodeId>) -> Self {
NodeRef::Ids(ids)
}
}
impl<const N: usize> From<[NodeId; N]> for NodeRef {
fn from(ids: [NodeId; N]) -> Self {
NodeRef::Ids(ids.to_vec())
}
}
impl From<&str> for NodeRef {
fn from(var_name: &str) -> Self {
NodeRef::Var(var_name.to_string())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum EdgeRef {
Ids(Vec<EdgeId>),
Var(String),
Param(String),
}
impl EdgeRef {
pub fn id(id: EdgeId) -> Self {
EdgeRef::Ids(vec![id])
}
pub fn ids(ids: impl IntoIterator<Item = EdgeId>) -> Self {
EdgeRef::Ids(ids.into_iter().collect())
}
pub fn var(name: impl Into<String>) -> Self {
EdgeRef::Var(name.into())
}
pub fn param(name: impl Into<String>) -> Self {
EdgeRef::Param(name.into())
}
}
impl From<EdgeId> for EdgeRef {
fn from(id: EdgeId) -> Self {
EdgeRef::Ids(vec![id])
}
}
impl From<Vec<EdgeId>> for EdgeRef {
fn from(ids: Vec<EdgeId>) -> Self {
EdgeRef::Ids(ids)
}
}
impl<const N: usize> From<[EdgeId; N]> for EdgeRef {
fn from(ids: [EdgeId; N]) -> Self {
EdgeRef::Ids(ids.to_vec())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Expr {
Property(String),
Id,
Timestamp,
DateTimeNow,
Constant(PropertyValue),
Param(String),
Add(Box<Expr>, Box<Expr>),
Sub(Box<Expr>, Box<Expr>),
Mul(Box<Expr>, Box<Expr>),
Div(Box<Expr>, Box<Expr>),
Mod(Box<Expr>, Box<Expr>),
Neg(Box<Expr>),
Case {
when_then: Vec<(Predicate, Expr)>,
else_expr: Option<Box<Expr>>,
},
}
impl Expr {
pub fn prop(name: impl Into<String>) -> Self {
Expr::Property(name.into())
}
pub fn val(value: impl Into<PropertyValue>) -> Self {
Expr::Constant(value.into())
}
pub fn id() -> Self {
Expr::Id
}
pub fn timestamp() -> Self {
Expr::Timestamp
}
pub fn datetime() -> Self {
Expr::DateTimeNow
}
pub fn param(name: impl Into<String>) -> Self {
Expr::Param(name.into())
}
pub fn add(self, other: Expr) -> Self {
Expr::Add(Box::new(self), Box::new(other))
}
pub fn sub(self, other: Expr) -> Self {
Expr::Sub(Box::new(self), Box::new(other))
}
pub fn mul(self, other: Expr) -> Self {
Expr::Mul(Box::new(self), Box::new(other))
}
pub fn div(self, other: Expr) -> Self {
Expr::Div(Box::new(self), Box::new(other))
}
pub fn modulo(self, other: Expr) -> Self {
Expr::Mod(Box::new(self), Box::new(other))
}
pub fn neg(self) -> Self {
Expr::Neg(Box::new(self))
}
pub fn case(when_then: Vec<(Predicate, Expr)>, else_expr: Option<Expr>) -> Self {
Expr::Case {
when_then,
else_expr: else_expr.map(Box::new),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum StreamBound {
Literal(usize),
Expr(Expr),
}
impl StreamBound {
pub fn literal(value: usize) -> Self {
Self::Literal(value)
}
pub fn expr(expr: Expr) -> Self {
Self::Expr(expr)
}
}
impl From<usize> for StreamBound {
fn from(value: usize) -> Self {
Self::Literal(value)
}
}
impl From<u32> for StreamBound {
fn from(value: u32) -> Self {
Self::Literal(value as usize)
}
}
impl From<u16> for StreamBound {
fn from(value: u16) -> Self {
Self::Literal(value as usize)
}
}
impl From<u8> for StreamBound {
fn from(value: u8) -> Self {
Self::Literal(value as usize)
}
}
impl From<i64> for StreamBound {
fn from(value: i64) -> Self {
if value >= 0 {
Self::Literal(value as usize)
} else {
Self::Expr(Expr::val(value))
}
}
}
impl From<i32> for StreamBound {
fn from(value: i32) -> Self {
if value >= 0 {
Self::Literal(value as usize)
} else {
Self::Expr(Expr::val(value))
}
}
}
impl From<Expr> for StreamBound {
fn from(value: Expr) -> Self {
Self::Expr(value)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CompareOp {
Eq,
Neq,
Gt,
Gte,
Lt,
Lte,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Predicate {
Eq(String, PropertyValue),
Neq(String, PropertyValue),
Gt(String, PropertyValue),
Gte(String, PropertyValue),
Lt(String, PropertyValue),
Lte(String, PropertyValue),
Between(String, PropertyValue, PropertyValue),
HasKey(String),
IsNull(String),
IsNotNull(String),
StartsWith(String, String),
EndsWith(String, String),
Contains(String, String),
ContainsExpr(String, Expr),
IsIn(String, PropertyValue),
IsInExpr(String, Expr),
And(Vec<Predicate>),
Or(Vec<Predicate>),
Not(Box<Predicate>),
Compare {
left: Expr,
op: CompareOp,
right: Expr,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum SourcePredicate {
Eq(String, PropertyValue),
Neq(String, PropertyValue),
Gt(String, PropertyValue),
Gte(String, PropertyValue),
Lt(String, PropertyValue),
Lte(String, PropertyValue),
Between(String, PropertyValue, PropertyValue),
HasKey(String),
StartsWith(String, String),
And(Vec<SourcePredicate>),
Or(Vec<SourcePredicate>),
EqExpr(String, Expr),
NeqExpr(String, Expr),
GtExpr(String, Expr),
GteExpr(String, Expr),
LtExpr(String, Expr),
LteExpr(String, Expr),
BetweenExpr(String, Expr, Expr),
}
impl SourcePredicate {
pub fn eq(property: impl Into<String>, value: impl Into<PropertyInput>) -> Self {
match value.into() {
PropertyInput::Value(v) => SourcePredicate::Eq(property.into(), v),
PropertyInput::Expr(e) => SourcePredicate::EqExpr(property.into(), e),
}
}
pub fn neq(property: impl Into<String>, value: impl Into<PropertyInput>) -> Self {
match value.into() {
PropertyInput::Value(v) => SourcePredicate::Neq(property.into(), v),
PropertyInput::Expr(e) => SourcePredicate::NeqExpr(property.into(), e),
}
}
pub fn gt(property: impl Into<String>, value: impl Into<PropertyInput>) -> Self {
match value.into() {
PropertyInput::Value(v) => SourcePredicate::Gt(property.into(), v),
PropertyInput::Expr(e) => SourcePredicate::GtExpr(property.into(), e),
}
}
pub fn gte(property: impl Into<String>, value: impl Into<PropertyInput>) -> Self {
match value.into() {
PropertyInput::Value(v) => SourcePredicate::Gte(property.into(), v),
PropertyInput::Expr(e) => SourcePredicate::GteExpr(property.into(), e),
}
}
pub fn lt(property: impl Into<String>, value: impl Into<PropertyInput>) -> Self {
match value.into() {
PropertyInput::Value(v) => SourcePredicate::Lt(property.into(), v),
PropertyInput::Expr(e) => SourcePredicate::LtExpr(property.into(), e),
}
}
pub fn lte(property: impl Into<String>, value: impl Into<PropertyInput>) -> Self {
match value.into() {
PropertyInput::Value(v) => SourcePredicate::Lte(property.into(), v),
PropertyInput::Expr(e) => SourcePredicate::LteExpr(property.into(), e),
}
}
pub fn between(
property: impl Into<String>,
min: impl Into<PropertyInput>,
max: impl Into<PropertyInput>,
) -> Self {
let prop = property.into();
match (min.into(), max.into()) {
(PropertyInput::Value(a), PropertyInput::Value(b)) => {
SourcePredicate::Between(prop, a, b)
}
(min, max) => SourcePredicate::BetweenExpr(prop, min.into_expr(), max.into_expr()),
}
}
pub fn has_key(property: impl Into<String>) -> Self {
SourcePredicate::HasKey(property.into())
}
pub fn starts_with(property: impl Into<String>, prefix: impl Into<String>) -> Self {
SourcePredicate::StartsWith(property.into(), prefix.into())
}
pub fn and(predicates: Vec<SourcePredicate>) -> Self {
SourcePredicate::And(predicates)
}
pub fn or(predicates: Vec<SourcePredicate>) -> Self {
SourcePredicate::Or(predicates)
}
}
impl From<SourcePredicate> for Predicate {
fn from(predicate: SourcePredicate) -> Self {
match predicate {
SourcePredicate::Eq(prop, val) => Predicate::Eq(prop, val),
SourcePredicate::Neq(prop, val) => Predicate::Neq(prop, val),
SourcePredicate::Gt(prop, val) => Predicate::Gt(prop, val),
SourcePredicate::Gte(prop, val) => Predicate::Gte(prop, val),
SourcePredicate::Lt(prop, val) => Predicate::Lt(prop, val),
SourcePredicate::Lte(prop, val) => Predicate::Lte(prop, val),
SourcePredicate::Between(prop, min, max) => Predicate::Between(prop, min, max),
SourcePredicate::HasKey(prop) => Predicate::HasKey(prop),
SourcePredicate::StartsWith(prop, prefix) => Predicate::StartsWith(prop, prefix),
SourcePredicate::And(predicates) => {
Predicate::And(predicates.into_iter().map(Predicate::from).collect())
}
SourcePredicate::Or(predicates) => {
Predicate::Or(predicates.into_iter().map(Predicate::from).collect())
}
SourcePredicate::EqExpr(prop, e) => Predicate::Compare {
left: Expr::Property(prop),
op: CompareOp::Eq,
right: e,
},
SourcePredicate::NeqExpr(prop, e) => Predicate::Compare {
left: Expr::Property(prop),
op: CompareOp::Neq,
right: e,
},
SourcePredicate::GtExpr(prop, e) => Predicate::Compare {
left: Expr::Property(prop),
op: CompareOp::Gt,
right: e,
},
SourcePredicate::GteExpr(prop, e) => Predicate::Compare {
left: Expr::Property(prop),
op: CompareOp::Gte,
right: e,
},
SourcePredicate::LtExpr(prop, e) => Predicate::Compare {
left: Expr::Property(prop),
op: CompareOp::Lt,
right: e,
},
SourcePredicate::LteExpr(prop, e) => Predicate::Compare {
left: Expr::Property(prop),
op: CompareOp::Lte,
right: e,
},
SourcePredicate::BetweenExpr(prop, min, max) => Predicate::And(vec![
Predicate::Compare {
left: Expr::Property(prop.clone()),
op: CompareOp::Gte,
right: min,
},
Predicate::Compare {
left: Expr::Property(prop),
op: CompareOp::Lte,
right: max,
},
]),
}
}
}
impl Predicate {
pub fn eq(property: impl Into<String>, value: impl Into<PropertyValue>) -> Self {
Predicate::Eq(property.into(), value.into())
}
pub fn neq(property: impl Into<String>, value: impl Into<PropertyValue>) -> Self {
Predicate::Neq(property.into(), value.into())
}
pub fn gt(property: impl Into<String>, value: impl Into<PropertyValue>) -> Self {
Predicate::Gt(property.into(), value.into())
}
pub fn gte(property: impl Into<String>, value: impl Into<PropertyValue>) -> Self {
Predicate::Gte(property.into(), value.into())
}
pub fn lt(property: impl Into<String>, value: impl Into<PropertyValue>) -> Self {
Predicate::Lt(property.into(), value.into())
}
pub fn lte(property: impl Into<String>, value: impl Into<PropertyValue>) -> Self {
Predicate::Lte(property.into(), value.into())
}
pub fn between(
property: impl Into<String>,
min: impl Into<PropertyValue>,
max: impl Into<PropertyValue>,
) -> Self {
Predicate::Between(property.into(), min.into(), max.into())
}
pub fn has_key(property: impl Into<String>) -> Self {
Predicate::HasKey(property.into())
}
pub fn is_null(property: impl Into<String>) -> Self {
Predicate::IsNull(property.into())
}
pub fn is_not_null(property: impl Into<String>) -> Self {
Predicate::IsNotNull(property.into())
}
pub fn starts_with(property: impl Into<String>, prefix: impl Into<String>) -> Self {
Predicate::StartsWith(property.into(), prefix.into())
}
pub fn ends_with(property: impl Into<String>, suffix: impl Into<String>) -> Self {
Predicate::EndsWith(property.into(), suffix.into())
}
pub fn contains(property: impl Into<String>, substring: impl Into<String>) -> Self {
Predicate::Contains(property.into(), substring.into())
}
pub fn contains_param(property: impl Into<String>, param_name: impl Into<String>) -> Self {
Predicate::ContainsExpr(property.into(), Expr::Param(param_name.into()))
}
pub fn is_in(property: impl Into<String>, values: impl Into<PropertyValue>) -> Self {
Predicate::IsIn(property.into(), values.into())
}
pub fn is_in_expr(property: impl Into<String>, values: Expr) -> Self {
Predicate::IsInExpr(property.into(), values)
}
pub fn is_in_param(property: impl Into<String>, param_name: impl Into<String>) -> Self {
Predicate::IsInExpr(property.into(), Expr::Param(param_name.into()))
}
pub fn and(predicates: Vec<Predicate>) -> Self {
Predicate::And(predicates)
}
pub fn or(predicates: Vec<Predicate>) -> Self {
Predicate::Or(predicates)
}
pub fn not(predicate: Predicate) -> Self {
Predicate::Not(Box::new(predicate))
}
pub fn compare(left: Expr, op: CompareOp, right: Expr) -> Self {
Predicate::Compare { left, op, right }
}
pub fn eq_param(property: impl Into<String>, param_name: impl Into<String>) -> Self {
Predicate::Compare {
left: Expr::Property(property.into()),
op: CompareOp::Eq,
right: Expr::Param(param_name.into()),
}
}
pub fn neq_param(property: impl Into<String>, param_name: impl Into<String>) -> Self {
Predicate::Compare {
left: Expr::Property(property.into()),
op: CompareOp::Neq,
right: Expr::Param(param_name.into()),
}
}
pub fn gt_param(property: impl Into<String>, param_name: impl Into<String>) -> Self {
Predicate::Compare {
left: Expr::Property(property.into()),
op: CompareOp::Gt,
right: Expr::Param(param_name.into()),
}
}
pub fn gte_param(property: impl Into<String>, param_name: impl Into<String>) -> Self {
Predicate::Compare {
left: Expr::Property(property.into()),
op: CompareOp::Gte,
right: Expr::Param(param_name.into()),
}
}
pub fn lt_param(property: impl Into<String>, param_name: impl Into<String>) -> Self {
Predicate::Compare {
left: Expr::Property(property.into()),
op: CompareOp::Lt,
right: Expr::Param(param_name.into()),
}
}
pub fn lte_param(property: impl Into<String>, param_name: impl Into<String>) -> Self {
Predicate::Compare {
left: Expr::Property(property.into()),
op: CompareOp::Lte,
right: Expr::Param(param_name.into()),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PropertyProjection {
pub source: String,
pub alias: String,
}
impl PropertyProjection {
pub fn new(name: impl Into<String>) -> Self {
let n = name.into();
Self {
source: n.clone(),
alias: n,
}
}
pub fn renamed(source: impl Into<String>, alias: impl Into<String>) -> Self {
Self {
source: source.into(),
alias: alias.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ExprProjection {
pub alias: String,
pub expr: Expr,
}
impl ExprProjection {
pub fn new(alias: impl Into<String>, expr: Expr) -> Self {
Self {
alias: alias.into(),
expr,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Projection {
Property(PropertyProjection),
Expr(ExprProjection),
}
impl Projection {
pub fn property(source: impl Into<String>, alias: impl Into<String>) -> Self {
Self::Property(PropertyProjection::renamed(source, alias))
}
pub fn expr(alias: impl Into<String>, expr: Expr) -> Self {
Self::Expr(ExprProjection::new(alias, expr))
}
}
impl From<PropertyProjection> for Projection {
fn from(value: PropertyProjection) -> Self {
Self::Property(value)
}
}
impl From<ExprProjection> for Projection {
fn from(value: ExprProjection) -> Self {
Self::Expr(value)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Order {
Asc,
Desc,
}
impl Default for Order {
fn default() -> Self {
Order::Asc
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum EmitBehavior {
None,
Before,
After,
All,
}
impl Default for EmitBehavior {
fn default() -> Self {
EmitBehavior::None
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum AggregateFunction {
Count,
Sum,
Min,
Max,
Mean,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct SubTraversal {
pub steps: Vec<Step>,
}
impl SubTraversal {
pub fn new() -> Self {
Self { steps: Vec::new() }
}
pub fn out(mut self, label: Option<impl Into<String>>) -> Self {
self.steps.push(Step::Out(label.map(|l| l.into())));
self
}
pub fn in_(mut self, label: Option<impl Into<String>>) -> Self {
self.steps.push(Step::In(label.map(|l| l.into())));
self
}
pub fn both(mut self, label: Option<impl Into<String>>) -> Self {
self.steps.push(Step::Both(label.map(|l| l.into())));
self
}
pub fn out_e(mut self, label: Option<impl Into<String>>) -> Self {
self.steps.push(Step::OutE(label.map(|l| l.into())));
self
}
pub fn in_e(mut self, label: Option<impl Into<String>>) -> Self {
self.steps.push(Step::InE(label.map(|l| l.into())));
self
}
pub fn both_e(mut self, label: Option<impl Into<String>>) -> Self {
self.steps.push(Step::BothE(label.map(|l| l.into())));
self
}
pub fn out_n(mut self) -> Self {
self.steps.push(Step::OutN);
self
}
pub fn in_n(mut self) -> Self {
self.steps.push(Step::InN);
self
}
pub fn other_n(mut self) -> Self {
self.steps.push(Step::OtherN);
self
}
pub fn has(mut self, property: impl Into<String>, value: impl Into<PropertyValue>) -> Self {
self.steps.push(Step::Has(property.into(), value.into()));
self
}
pub fn has_label(mut self, label: impl Into<String>) -> Self {
self.steps.push(Step::HasLabel(label.into()));
self
}
pub fn has_key(mut self, property: impl Into<String>) -> Self {
self.steps.push(Step::HasKey(property.into()));
self
}
pub fn where_(mut self, predicate: Predicate) -> Self {
self.steps.push(Step::Where(predicate));
self
}
pub fn dedup(mut self) -> Self {
self.steps.push(Step::Dedup);
self
}
pub fn within(mut self, var_name: impl Into<String>) -> Self {
self.steps.push(Step::Within(var_name.into()));
self
}
pub fn without(mut self, var_name: impl Into<String>) -> Self {
self.steps.push(Step::Without(var_name.into()));
self
}
pub fn edge_has(
mut self,
property: impl Into<String>,
value: impl Into<PropertyInput>,
) -> Self {
self.steps
.push(Step::EdgeHas(property.into(), value.into()));
self
}
pub fn edge_has_label(mut self, label: impl Into<String>) -> Self {
self.steps.push(Step::EdgeHasLabel(label.into()));
self
}
pub fn limit(mut self, n: impl Into<StreamBound>) -> Self {
self.steps.push(limit_step(n));
self
}
pub fn skip(mut self, n: impl Into<StreamBound>) -> Self {
self.steps.push(skip_step(n));
self
}
pub fn range(mut self, start: impl Into<StreamBound>, end: impl Into<StreamBound>) -> Self {
self.steps.push(range_step(start, end));
self
}
pub fn as_(mut self, name: impl Into<String>) -> Self {
self.steps.push(Step::As(name.into()));
self
}
pub fn store(mut self, name: impl Into<String>) -> Self {
self.steps.push(Step::Store(name.into()));
self
}
pub fn select(mut self, name: impl Into<String>) -> Self {
self.steps.push(Step::Select(name.into()));
self
}
pub fn order_by(mut self, property: impl Into<String>, order: Order) -> Self {
self.steps.push(Step::OrderBy(property.into(), order));
self
}
pub fn order_by_multiple(mut self, orderings: Vec<(impl Into<String>, Order)>) -> Self {
let orderings: Vec<(String, Order)> =
orderings.into_iter().map(|(p, o)| (p.into(), o)).collect();
self.steps.push(Step::OrderByMultiple(orderings));
self
}
pub fn path(mut self) -> Self {
self.steps.push(Step::Path);
self
}
pub fn simple_path(mut self) -> Self {
self.steps.push(Step::SimplePath);
self
}
}
pub fn sub() -> SubTraversal {
SubTraversal::new()
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RepeatConfig {
pub traversal: SubTraversal,
pub times: Option<usize>,
pub until: Option<Predicate>,
pub emit: EmitBehavior,
pub emit_predicate: Option<Predicate>,
pub max_depth: usize,
}
impl RepeatConfig {
pub fn new(traversal: SubTraversal) -> Self {
Self {
traversal,
times: None,
until: None,
emit: EmitBehavior::None,
emit_predicate: None,
max_depth: 100,
}
}
pub fn times(mut self, n: usize) -> Self {
self.times = Some(n);
self
}
pub fn until(mut self, predicate: Predicate) -> Self {
self.until = Some(predicate);
self
}
pub fn emit_all(mut self) -> Self {
self.emit = EmitBehavior::All;
self
}
pub fn emit_before(mut self) -> Self {
self.emit = EmitBehavior::Before;
self
}
pub fn emit_after(mut self) -> Self {
self.emit = EmitBehavior::After;
self
}
pub fn emit_if(mut self, predicate: Predicate) -> Self {
self.emit = EmitBehavior::After;
self.emit_predicate = Some(predicate);
self
}
pub fn max_depth(mut self, depth: usize) -> Self {
self.max_depth = depth;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum IndexSpec {
NodeEquality {
label: String,
property: String,
#[serde(default)]
unique: bool,
},
NodeRange {
label: String,
property: String,
},
EdgeEquality {
label: String,
property: String,
},
EdgeRange {
label: String,
property: String,
},
NodeVector {
label: String,
property: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
tenant_property: Option<String>,
},
NodeText {
label: String,
property: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
tenant_property: Option<String>,
},
EdgeVector {
label: String,
property: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
tenant_property: Option<String>,
},
EdgeText {
label: String,
property: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
tenant_property: Option<String>,
},
}
impl IndexSpec {
pub fn node_equality(label: impl Into<String>, property: impl Into<String>) -> Self {
Self::NodeEquality {
label: label.into(),
property: property.into(),
unique: false,
}
}
pub fn node_unique_equality(label: impl Into<String>, property: impl Into<String>) -> Self {
Self::NodeEquality {
label: label.into(),
property: property.into(),
unique: true,
}
}
pub fn node_range(label: impl Into<String>, property: impl Into<String>) -> Self {
Self::NodeRange {
label: label.into(),
property: property.into(),
}
}
pub fn edge_equality(label: impl Into<String>, property: impl Into<String>) -> Self {
Self::EdgeEquality {
label: label.into(),
property: property.into(),
}
}
pub fn edge_range(label: impl Into<String>, property: impl Into<String>) -> Self {
Self::EdgeRange {
label: label.into(),
property: property.into(),
}
}
pub fn node_vector(
label: impl Into<String>,
property: impl Into<String>,
tenant_property: Option<impl Into<String>>,
) -> Self {
Self::NodeVector {
label: label.into(),
property: property.into(),
tenant_property: tenant_property.map(|value| value.into()),
}
}
pub fn node_text(
label: impl Into<String>,
property: impl Into<String>,
tenant_property: Option<impl Into<String>>,
) -> Self {
Self::NodeText {
label: label.into(),
property: property.into(),
tenant_property: tenant_property.map(|value| value.into()),
}
}
pub fn edge_vector(
label: impl Into<String>,
property: impl Into<String>,
tenant_property: Option<impl Into<String>>,
) -> Self {
Self::EdgeVector {
label: label.into(),
property: property.into(),
tenant_property: tenant_property.map(|value| value.into()),
}
}
pub fn edge_text(
label: impl Into<String>,
property: impl Into<String>,
tenant_property: Option<impl Into<String>>,
) -> Self {
Self::EdgeText {
label: label.into(),
property: property.into(),
tenant_property: tenant_property.map(|value| value.into()),
}
}
}
#[doc(hidden)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Step {
N(NodeRef),
NWhere(SourcePredicate),
E(EdgeRef),
EWhere(SourcePredicate),
VectorSearchNodes {
label: String,
property: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
tenant_value: Option<PropertyInput>,
query_vector: PropertyInput,
k: StreamBound,
},
TextSearchNodes {
label: String,
property: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
tenant_value: Option<PropertyInput>,
query_text: PropertyInput,
k: StreamBound,
},
VectorSearchEdges {
label: String,
property: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
tenant_value: Option<PropertyInput>,
query_vector: PropertyInput,
k: StreamBound,
},
TextSearchEdges {
label: String,
property: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
tenant_value: Option<PropertyInput>,
query_text: PropertyInput,
k: StreamBound,
},
Out(Option<String>),
In(Option<String>),
Both(Option<String>),
OutE(Option<String>),
InE(Option<String>),
BothE(Option<String>),
OutN,
InN,
OtherN,
Has(String, PropertyValue),
HasLabel(String),
HasKey(String),
Where(Predicate),
Dedup,
Within(String),
Without(String),
EdgeHas(String, PropertyInput),
EdgeHasLabel(String),
Limit(usize),
LimitBy(Expr),
Skip(usize),
SkipBy(Expr),
Range(usize, usize),
RangeBy(StreamBound, StreamBound),
As(String),
Store(String),
Select(String),
Count,
Exists,
Id,
Label,
Values(Vec<String>),
ValueMap(Option<Vec<String>>),
Project(Vec<Projection>),
EdgeProperties,
CreateIndex {
spec: IndexSpec,
if_not_exists: bool,
},
DropIndex {
spec: IndexSpec,
},
CreateVectorIndexNodes {
label: String,
property: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
tenant_property: Option<String>,
},
CreateVectorIndexEdges {
label: String,
property: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
tenant_property: Option<String>,
},
CreateTextIndexNodes {
label: String,
property: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
tenant_property: Option<String>,
},
CreateTextIndexEdges {
label: String,
property: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
tenant_property: Option<String>,
},
AddN {
label: String,
properties: Vec<(String, PropertyInput)>,
},
AddE {
label: String,
to: NodeRef,
properties: Vec<(String, PropertyInput)>,
},
SetProperty(String, PropertyInput),
RemoveProperty(String),
Drop,
DropEdge(NodeRef),
DropEdgeLabeled {
to: NodeRef,
label: String,
},
DropEdgeById(EdgeRef),
OrderBy(String, Order),
OrderByMultiple(Vec<(String, Order)>),
Repeat(RepeatConfig),
Union(Vec<SubTraversal>),
Choose {
condition: Predicate,
then_traversal: SubTraversal,
else_traversal: Option<SubTraversal>,
},
Coalesce(Vec<SubTraversal>),
Optional(SubTraversal),
Group(String),
GroupCount(String),
AggregateBy(AggregateFunction, String),
Fold,
Unfold,
Path,
SimplePath,
WithSack(PropertyValue),
SackSet(String),
SackAdd(String),
SackGet,
Inject(String),
}
fn limit_step(bound: impl Into<StreamBound>) -> Step {
match bound.into() {
StreamBound::Literal(n) => Step::Limit(n),
StreamBound::Expr(expr) => Step::LimitBy(expr),
}
}
fn skip_step(bound: impl Into<StreamBound>) -> Step {
match bound.into() {
StreamBound::Literal(n) => Step::Skip(n),
StreamBound::Expr(expr) => Step::SkipBy(expr),
}
}
fn range_step(start: impl Into<StreamBound>, end: impl Into<StreamBound>) -> Step {
let start = start.into();
let end = end.into();
match (&start, &end) {
(StreamBound::Literal(start), StreamBound::Literal(end)) => Step::Range(*start, *end),
_ => Step::RangeBy(start, end),
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Traversal<S: TraversalState = OnNodes, M: MutationMode = ReadOnly> {
#[doc(hidden)]
pub steps: Vec<Step>,
_state: PhantomData<S>,
_mode: PhantomData<M>,
}
impl<S: TraversalState, M: MutationMode> Default for Traversal<S, M> {
fn default() -> Self {
Self {
steps: Vec::new(),
_state: PhantomData,
_mode: PhantomData,
}
}
}
impl<S: TraversalState, M: MutationMode> Traversal<S, M> {
#[doc(hidden)]
pub fn into_steps(self) -> Vec<Step> {
self.steps
}
#[doc(hidden)]
pub fn has_terminal(&self) -> bool {
self.steps.iter().any(|s| {
matches!(
s,
Step::Count
| Step::Exists
| Step::Id
| Step::Label
| Step::Values(_)
| Step::ValueMap(_)
| Step::Project(_)
| Step::EdgeProperties
| Step::CreateIndex { .. }
| Step::DropIndex { .. }
| Step::CreateVectorIndexNodes { .. }
| Step::CreateVectorIndexEdges { .. }
| Step::CreateTextIndexNodes { .. }
| Step::CreateTextIndexEdges { .. }
)
})
}
#[doc(hidden)]
pub fn from_steps(steps: Vec<Step>) -> Self {
Self {
steps,
_state: PhantomData,
_mode: PhantomData,
}
}
fn push_step<T: TraversalState>(mut self, step: Step) -> Traversal<T, M> {
self.steps.push(step);
Traversal::from_steps(self.steps)
}
fn push_mutation_step<T: TraversalState>(mut self, step: Step) -> Traversal<T, WriteEnabled> {
self.steps.push(step);
Traversal::from_steps(self.steps)
}
}
impl Traversal<Empty, ReadOnly> {
#[doc(hidden)]
pub fn new() -> Self {
Self {
steps: Vec::new(),
_state: PhantomData,
_mode: PhantomData,
}
}
pub fn n(self, nodes: impl Into<NodeRef>) -> Traversal<OnNodes> {
self.push_step(Step::N(nodes.into()))
}
pub fn n_where(self, predicate: SourcePredicate) -> Traversal<OnNodes> {
self.push_step(Step::NWhere(predicate))
}
pub fn n_with_label(self, label: impl Into<String>) -> Traversal<OnNodes> {
self.n_where(SourcePredicate::Eq(
"$label".to_string(),
PropertyValue::String(label.into()),
))
}
pub fn n_with_label_where(
self,
label: impl Into<String>,
predicate: SourcePredicate,
) -> Traversal<OnNodes> {
self.n_where(SourcePredicate::And(vec![
SourcePredicate::Eq("$label".to_string(), PropertyValue::String(label.into())),
predicate,
]))
}
pub fn vector_search_nodes(
self,
label: impl Into<String>,
property: impl Into<String>,
query_vector: Vec<f32>,
k: usize,
tenant_value: Option<PropertyValue>,
) -> Traversal<OnNodes> {
self.vector_search_nodes_with(
label,
property,
query_vector,
k,
tenant_value.map(PropertyInput::from),
)
}
pub fn vector_search_nodes_with(
self,
label: impl Into<String>,
property: impl Into<String>,
query_vector: impl Into<PropertyInput>,
k: impl Into<StreamBound>,
tenant_value: Option<PropertyInput>,
) -> Traversal<OnNodes> {
self.push_step(Step::VectorSearchNodes {
label: label.into(),
property: property.into(),
tenant_value,
query_vector: query_vector.into(),
k: k.into(),
})
}
pub fn text_search_nodes(
self,
label: impl Into<String>,
property: impl Into<String>,
query_text: impl Into<String>,
k: usize,
tenant_value: Option<PropertyValue>,
) -> Traversal<OnNodes> {
self.text_search_nodes_with(
label,
property,
PropertyInput::from(query_text.into()),
k,
tenant_value.map(PropertyInput::from),
)
}
pub fn text_search_nodes_with(
self,
label: impl Into<String>,
property: impl Into<String>,
query_text: impl Into<PropertyInput>,
k: impl Into<StreamBound>,
tenant_value: Option<PropertyInput>,
) -> Traversal<OnNodes> {
self.push_step(Step::TextSearchNodes {
label: label.into(),
property: property.into(),
tenant_value,
query_text: query_text.into(),
k: k.into(),
})
}
pub fn e(self, edges: impl Into<EdgeRef>) -> Traversal<OnEdges> {
self.push_step(Step::E(edges.into()))
}
pub fn e_where(self, predicate: SourcePredicate) -> Traversal<OnEdges> {
self.push_step(Step::EWhere(predicate))
}
pub fn e_with_label(self, label: impl Into<String>) -> Traversal<OnEdges> {
self.e_where(SourcePredicate::Eq(
"$label".to_string(),
PropertyValue::String(label.into()),
))
}
pub fn e_with_label_where(
self,
label: impl Into<String>,
predicate: SourcePredicate,
) -> Traversal<OnEdges> {
self.e_where(SourcePredicate::And(vec![
SourcePredicate::Eq("$label".to_string(), PropertyValue::String(label.into())),
predicate,
]))
}
pub fn vector_search_edges(
self,
label: impl Into<String>,
property: impl Into<String>,
query_vector: Vec<f32>,
k: usize,
tenant_value: Option<PropertyValue>,
) -> Traversal<OnEdges> {
self.vector_search_edges_with(
label,
property,
query_vector,
k,
tenant_value.map(PropertyInput::from),
)
}
pub fn vector_search_edges_with(
self,
label: impl Into<String>,
property: impl Into<String>,
query_vector: impl Into<PropertyInput>,
k: impl Into<StreamBound>,
tenant_value: Option<PropertyInput>,
) -> Traversal<OnEdges> {
self.push_step(Step::VectorSearchEdges {
label: label.into(),
property: property.into(),
tenant_value,
query_vector: query_vector.into(),
k: k.into(),
})
}
pub fn text_search_edges(
self,
label: impl Into<String>,
property: impl Into<String>,
query_text: impl Into<String>,
k: usize,
tenant_value: Option<PropertyValue>,
) -> Traversal<OnEdges> {
self.text_search_edges_with(
label,
property,
PropertyInput::from(query_text.into()),
k,
tenant_value.map(PropertyInput::from),
)
}
pub fn text_search_edges_with(
self,
label: impl Into<String>,
property: impl Into<String>,
query_text: impl Into<PropertyInput>,
k: impl Into<StreamBound>,
tenant_value: Option<PropertyInput>,
) -> Traversal<OnEdges> {
self.push_step(Step::TextSearchEdges {
label: label.into(),
property: property.into(),
tenant_value,
query_text: query_text.into(),
k: k.into(),
})
}
pub fn create_index_if_not_exists(self, spec: IndexSpec) -> Traversal<Terminal, WriteEnabled> {
self.push_mutation_step(Step::CreateIndex {
spec,
if_not_exists: true,
})
}
pub fn drop_index(self, spec: IndexSpec) -> Traversal<Terminal, WriteEnabled> {
self.push_mutation_step(Step::DropIndex { spec })
}
pub fn create_vector_index_nodes(
self,
label: impl Into<String>,
property: impl Into<String>,
tenant_property: Option<impl Into<String>>,
) -> Traversal<Terminal, WriteEnabled> {
self.create_index_if_not_exists(IndexSpec::node_vector(label, property, tenant_property))
}
pub fn create_vector_index_edges(
self,
label: impl Into<String>,
property: impl Into<String>,
tenant_property: Option<impl Into<String>>,
) -> Traversal<Terminal, WriteEnabled> {
self.create_index_if_not_exists(IndexSpec::edge_vector(label, property, tenant_property))
}
pub fn create_text_index_nodes(
self,
label: impl Into<String>,
property: impl Into<String>,
tenant_property: Option<impl Into<String>>,
) -> Traversal<Terminal, WriteEnabled> {
self.create_index_if_not_exists(IndexSpec::node_text(label, property, tenant_property))
}
pub fn create_text_index_edges(
self,
label: impl Into<String>,
property: impl Into<String>,
tenant_property: Option<impl Into<String>>,
) -> Traversal<Terminal, WriteEnabled> {
self.create_index_if_not_exists(IndexSpec::edge_text(label, property, tenant_property))
}
pub fn add_n<K, V>(
self,
label: impl Into<String>,
properties: Vec<(K, V)>,
) -> Traversal<OnNodes, WriteEnabled>
where
K: Into<String>,
V: Into<PropertyInput>,
{
let props: Vec<(String, PropertyInput)> = properties
.into_iter()
.map(|(k, v)| (k.into(), v.into()))
.collect();
self.push_mutation_step(Step::AddN {
label: label.into(),
properties: props,
})
}
pub fn inject(self, var_name: impl Into<String>) -> Traversal<OnNodes, ReadOnly> {
self.push_step(Step::Inject(var_name.into()))
}
pub fn drop_edge_by_id(self, edges: impl Into<EdgeRef>) -> Traversal<OnNodes, WriteEnabled> {
self.push_mutation_step(Step::DropEdgeById(edges.into()))
}
}
impl<M: MutationMode> Traversal<OnNodes, M> {
pub fn out(self, label: Option<impl Into<String>>) -> Traversal<OnNodes, M> {
self.push_step(Step::Out(label.map(|l| l.into())))
}
pub fn in_(self, label: Option<impl Into<String>>) -> Traversal<OnNodes, M> {
self.push_step(Step::In(label.map(|l| l.into())))
}
pub fn both(self, label: Option<impl Into<String>>) -> Traversal<OnNodes, M> {
self.push_step(Step::Both(label.map(|l| l.into())))
}
pub fn out_e(self, label: Option<impl Into<String>>) -> Traversal<OnEdges, M> {
self.push_step(Step::OutE(label.map(|l| l.into())))
}
pub fn in_e(self, label: Option<impl Into<String>>) -> Traversal<OnEdges, M> {
self.push_step(Step::InE(label.map(|l| l.into())))
}
pub fn both_e(self, label: Option<impl Into<String>>) -> Traversal<OnEdges, M> {
self.push_step(Step::BothE(label.map(|l| l.into())))
}
pub fn has(self, property: impl Into<String>, value: impl Into<PropertyValue>) -> Self {
self.push_step(Step::Has(property.into(), value.into()))
}
pub fn has_label(self, label: impl Into<String>) -> Self {
self.push_step(Step::HasLabel(label.into()))
}
pub fn has_key(self, property: impl Into<String>) -> Self {
self.push_step(Step::HasKey(property.into()))
}
pub fn where_(self, predicate: Predicate) -> Self {
self.push_step(Step::Where(predicate))
}
pub fn dedup(self) -> Self {
self.push_step(Step::Dedup)
}
pub fn within(self, var_name: impl Into<String>) -> Self {
self.push_step(Step::Within(var_name.into()))
}
pub fn without(self, var_name: impl Into<String>) -> Self {
self.push_step(Step::Without(var_name.into()))
}
pub fn limit(self, n: impl Into<StreamBound>) -> Self {
self.push_step(limit_step(n))
}
pub fn skip(self, n: impl Into<StreamBound>) -> Self {
self.push_step(skip_step(n))
}
pub fn range(self, start: impl Into<StreamBound>, end: impl Into<StreamBound>) -> Self {
self.push_step(range_step(start, end))
}
pub fn as_(self, name: impl Into<String>) -> Self {
self.push_step(Step::As(name.into()))
}
pub fn store(self, name: impl Into<String>) -> Self {
self.push_step(Step::Store(name.into()))
}
pub fn select(self, name: impl Into<String>) -> Self {
self.push_step(Step::Select(name.into()))
}
pub fn inject(self, var_name: impl Into<String>) -> Self {
self.push_step(Step::Inject(var_name.into()))
}
pub fn count(self) -> Traversal<Terminal, M> {
self.push_step(Step::Count)
}
pub fn exists(self) -> Traversal<Terminal, M> {
self.push_step(Step::Exists)
}
pub fn id(self) -> Traversal<Terminal, M> {
self.push_step(Step::Id)
}
pub fn label(self) -> Traversal<Terminal, M> {
self.push_step(Step::Label)
}
pub fn values(self, properties: Vec<impl Into<String>>) -> Traversal<Terminal, M> {
self.push_step(Step::Values(
properties.into_iter().map(|p| p.into()).collect(),
))
}
pub fn value_map(self, properties: Option<Vec<impl Into<String>>>) -> Traversal<Terminal, M> {
self.push_step(Step::ValueMap(
properties.map(|ps| ps.into_iter().map(|p| p.into()).collect()),
))
}
pub fn project<P>(self, projections: Vec<P>) -> Traversal<Terminal, M>
where
P: Into<Projection>,
{
self.push_step(Step::Project(
projections.into_iter().map(Into::into).collect(),
))
}
pub fn order_by(self, property: impl Into<String>, order: Order) -> Self {
self.push_step(Step::OrderBy(property.into(), order))
}
pub fn order_by_multiple(self, orderings: Vec<(impl Into<String>, Order)>) -> Self {
let orderings: Vec<(String, Order)> =
orderings.into_iter().map(|(p, o)| (p.into(), o)).collect();
self.push_step(Step::OrderByMultiple(orderings))
}
pub fn repeat(self, config: RepeatConfig) -> Self {
self.push_step(Step::Repeat(config))
}
pub fn union(self, traversals: Vec<SubTraversal>) -> Self {
self.push_step(Step::Union(traversals))
}
pub fn choose(
self,
condition: Predicate,
then_traversal: SubTraversal,
else_traversal: Option<SubTraversal>,
) -> Self {
self.push_step(Step::Choose {
condition,
then_traversal,
else_traversal,
})
}
pub fn coalesce(self, traversals: Vec<SubTraversal>) -> Self {
self.push_step(Step::Coalesce(traversals))
}
pub fn optional(self, traversal: SubTraversal) -> Self {
self.push_step(Step::Optional(traversal))
}
pub fn group(self, property: impl Into<String>) -> Traversal<Terminal, M> {
self.push_step(Step::Group(property.into()))
}
pub fn group_count(self, property: impl Into<String>) -> Traversal<Terminal, M> {
self.push_step(Step::GroupCount(property.into()))
}
pub fn aggregate_by(
self,
function: AggregateFunction,
property: impl Into<String>,
) -> Traversal<Terminal, M> {
self.push_step(Step::AggregateBy(function, property.into()))
}
pub fn fold(self) -> Self {
self.push_step(Step::Fold)
}
pub fn unfold(self) -> Self {
self.push_step(Step::Unfold)
}
pub fn path(self) -> Self {
self.push_step(Step::Path)
}
pub fn simple_path(self) -> Self {
self.push_step(Step::SimplePath)
}
pub fn with_sack(self, initial: PropertyValue) -> Self {
self.push_step(Step::WithSack(initial))
}
pub fn sack_set(self, property: impl Into<String>) -> Self {
self.push_step(Step::SackSet(property.into()))
}
pub fn sack_add(self, property: impl Into<String>) -> Self {
self.push_step(Step::SackAdd(property.into()))
}
pub fn sack_get(self) -> Self {
self.push_step(Step::SackGet)
}
pub fn add_n<K, V>(
self,
label: impl Into<String>,
properties: Vec<(K, V)>,
) -> Traversal<OnNodes, WriteEnabled>
where
K: Into<String>,
V: Into<PropertyInput>,
{
let props: Vec<(String, PropertyInput)> = properties
.into_iter()
.map(|(k, v)| (k.into(), v.into()))
.collect();
self.push_mutation_step(Step::AddN {
label: label.into(),
properties: props,
})
}
pub fn add_e<K, V>(
self,
label: impl Into<String>,
to: impl Into<NodeRef>,
properties: Vec<(K, V)>,
) -> Traversal<OnNodes, WriteEnabled>
where
K: Into<String>,
V: Into<PropertyInput>,
{
let props: Vec<(String, PropertyInput)> = properties
.into_iter()
.map(|(k, v)| (k.into(), v.into()))
.collect();
self.push_mutation_step(Step::AddE {
label: label.into(),
to: to.into(),
properties: props,
})
}
pub fn set_property(
self,
name: impl Into<String>,
value: impl Into<PropertyInput>,
) -> Traversal<OnNodes, WriteEnabled> {
self.push_mutation_step(Step::SetProperty(name.into(), value.into()))
}
pub fn remove_property(self, name: impl Into<String>) -> Traversal<OnNodes, WriteEnabled> {
self.push_mutation_step(Step::RemoveProperty(name.into()))
}
pub fn drop(self) -> Traversal<OnNodes, WriteEnabled> {
self.push_mutation_step(Step::Drop)
}
pub fn drop_edge(self, to: impl Into<NodeRef>) -> Traversal<OnNodes, WriteEnabled> {
self.push_mutation_step(Step::DropEdge(to.into()))
}
pub fn drop_edge_labeled(
self,
to: impl Into<NodeRef>,
label: impl Into<String>,
) -> Traversal<OnNodes, WriteEnabled> {
self.push_mutation_step(Step::DropEdgeLabeled {
to: to.into(),
label: label.into(),
})
}
pub fn drop_edge_by_id(self, edges: impl Into<EdgeRef>) -> Traversal<OnNodes, WriteEnabled> {
self.push_mutation_step(Step::DropEdgeById(edges.into()))
}
}
impl<M: MutationMode> Traversal<OnEdges, M> {
pub fn out_n(self) -> Traversal<OnNodes, M> {
self.push_step(Step::OutN)
}
pub fn in_n(self) -> Traversal<OnNodes, M> {
self.push_step(Step::InN)
}
pub fn other_n(self) -> Traversal<OnNodes, M> {
self.push_step(Step::OtherN)
}
pub fn edge_has(self, property: impl Into<String>, value: impl Into<PropertyInput>) -> Self {
self.push_step(Step::EdgeHas(property.into(), value.into()))
}
pub fn edge_has_label(self, label: impl Into<String>) -> Self {
self.push_step(Step::EdgeHasLabel(label.into()))
}
pub fn dedup(self) -> Self {
self.push_step(Step::Dedup)
}
pub fn limit(self, n: impl Into<StreamBound>) -> Self {
self.push_step(limit_step(n))
}
pub fn skip(self, n: impl Into<StreamBound>) -> Self {
self.push_step(skip_step(n))
}
pub fn range(self, start: impl Into<StreamBound>, end: impl Into<StreamBound>) -> Self {
self.push_step(range_step(start, end))
}
pub fn as_(self, name: impl Into<String>) -> Self {
self.push_step(Step::As(name.into()))
}
pub fn store(self, name: impl Into<String>) -> Self {
self.push_step(Step::Store(name.into()))
}
pub fn count(self) -> Traversal<Terminal, M> {
self.push_step(Step::Count)
}
pub fn exists(self) -> Traversal<Terminal, M> {
self.push_step(Step::Exists)
}
pub fn id(self) -> Traversal<Terminal, M> {
self.push_step(Step::Id)
}
pub fn label(self) -> Traversal<Terminal, M> {
self.push_step(Step::Label)
}
pub fn edge_properties(self) -> Traversal<Terminal, M> {
self.push_step(Step::EdgeProperties)
}
pub fn order_by(self, property: impl Into<String>, order: Order) -> Self {
self.push_step(Step::OrderBy(property.into(), order))
}
}
pub fn g() -> Traversal<Empty> {
Traversal::new()
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum BatchCondition {
VarNotEmpty(String),
VarEmpty(String),
VarMinSize(String, usize),
PrevNotEmpty,
}
#[doc(hidden)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct NamedQuery {
pub name: Option<String>,
pub steps: Vec<Step>,
pub condition: Option<BatchCondition>,
}
#[doc(hidden)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum BatchEntry {
Query(NamedQuery),
ForEach {
param: String,
body: Vec<BatchEntry>,
},
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct ReadBatch {
#[doc(hidden)]
pub queries: Vec<BatchEntry>,
#[doc(hidden)]
pub returns: Vec<String>,
}
impl ReadBatch {
#[doc(hidden)]
pub fn new() -> Self {
Self {
queries: Vec::new(),
returns: Vec::new(),
}
}
pub fn var_as<S: TraversalState>(
mut self,
name: &str,
traversal: Traversal<S, ReadOnly>,
) -> Self {
self.queries.push(BatchEntry::Query(NamedQuery {
name: Some(name.to_string()),
steps: traversal.into_steps(),
condition: None,
}));
self
}
pub fn var_as_if<S: TraversalState>(
mut self,
name: &str,
condition: BatchCondition,
traversal: Traversal<S, ReadOnly>,
) -> Self {
self.queries.push(BatchEntry::Query(NamedQuery {
name: Some(name.to_string()),
steps: traversal.into_steps(),
condition: Some(condition),
}));
self
}
pub fn for_each_param(mut self, param: &str, body: ReadBatch) -> Self {
self.queries.push(BatchEntry::ForEach {
param: param.to_string(),
body: body.queries,
});
self
}
pub fn returning<I, S>(mut self, vars: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.returns = vars.into_iter().map(|s| s.into()).collect();
self
}
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct WriteBatch {
#[doc(hidden)]
pub queries: Vec<BatchEntry>,
#[doc(hidden)]
pub returns: Vec<String>,
}
impl WriteBatch {
#[doc(hidden)]
pub fn new() -> Self {
Self {
queries: Vec::new(),
returns: Vec::new(),
}
}
pub fn var_as<S: TraversalState, M: MutationMode>(
mut self,
name: &str,
traversal: Traversal<S, M>,
) -> Self {
self.queries.push(BatchEntry::Query(NamedQuery {
name: Some(name.to_string()),
steps: traversal.into_steps(),
condition: None,
}));
self
}
pub fn var_as_if<S: TraversalState, M: MutationMode>(
mut self,
name: &str,
condition: BatchCondition,
traversal: Traversal<S, M>,
) -> Self {
self.queries.push(BatchEntry::Query(NamedQuery {
name: Some(name.to_string()),
steps: traversal.into_steps(),
condition: Some(condition),
}));
self
}
pub fn for_each_param(mut self, param: &str, body: WriteBatch) -> Self {
self.queries.push(BatchEntry::ForEach {
param: param.to_string(),
body: body.queries,
});
self
}
pub fn returning<I, S>(mut self, vars: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.returns = vars.into_iter().map(|s| s.into()).collect();
self
}
}
#[doc(hidden)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum BatchQuery {
Read(ReadBatch),
Write(WriteBatch),
}
#[derive(Debug)]
pub enum DynamicQueryError {
Serialize(sonic_rs::Error),
Utf8(std::string::FromUtf8Error),
UnsupportedBytesParameter(String),
InvalidDateTimeParameter {
path: String,
millis: i64,
},
}
impl DynamicQueryError {
pub fn unsupported_bytes(path: impl Into<String>) -> Self {
Self::UnsupportedBytesParameter(path.into())
}
pub fn invalid_datetime(path: impl Into<String>, millis: i64) -> Self {
Self::InvalidDateTimeParameter {
path: path.into(),
millis,
}
}
}
impl std::fmt::Display for DynamicQueryError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Serialize(err) => write!(f, "json serialization error: {err}"),
Self::Utf8(err) => write!(f, "utf8 conversion error: {err}"),
Self::UnsupportedBytesParameter(path) => write!(
f,
"parameter '{path}' uses bytes, which the dynamic query JSON route cannot represent"
),
Self::InvalidDateTimeParameter { path, millis } => write!(
f,
"parameter '{path}' uses datetime millis '{millis}', which cannot be rendered as RFC3339"
),
}
}
}
impl std::error::Error for DynamicQueryError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Serialize(err) => Some(err),
Self::Utf8(err) => Some(err),
Self::UnsupportedBytesParameter(_) => None,
Self::InvalidDateTimeParameter { .. } => None,
}
}
}
impl From<sonic_rs::Error> for DynamicQueryError {
fn from(value: sonic_rs::Error) -> Self {
Self::Serialize(value)
}
}
impl From<std::string::FromUtf8Error> for DynamicQueryError {
fn from(value: std::string::FromUtf8Error) -> Self {
Self::Utf8(value)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum DynamicQueryRequestType {
Read,
Write,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum DynamicQueryValue {
Null,
Bool(bool),
I64(i64),
F64(f64),
F32(f32),
String(String),
Array(Vec<DynamicQueryValue>),
Object(BTreeMap<String, DynamicQueryValue>),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DynamicQueryRequest {
#[serde(rename = "request_type")]
pub request_type: DynamicQueryRequestType,
pub query: BatchQuery,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parameters: Option<BTreeMap<String, DynamicQueryValue>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parameter_types: Option<BTreeMap<String, QueryParamType>>,
}
impl DynamicQueryRequest {
fn new(request_type: DynamicQueryRequestType, query: BatchQuery) -> Self {
Self {
request_type,
query,
parameters: None,
parameter_types: None,
}
}
pub fn read(query: ReadBatch) -> Self {
Self::new(DynamicQueryRequestType::Read, BatchQuery::Read(query))
}
pub fn write(query: WriteBatch) -> Self {
Self::new(DynamicQueryRequestType::Write, BatchQuery::Write(query))
}
pub fn insert_parameter_value(&mut self, name: impl Into<String>, value: DynamicQueryValue) {
self.parameters
.get_or_insert_with(BTreeMap::new)
.insert(name.into(), value);
}
pub fn insert_parameter_type(&mut self, name: impl Into<String>, ty: QueryParamType) {
self.parameter_types
.get_or_insert_with(BTreeMap::new)
.insert(name.into(), ty);
}
pub fn with_parameter_value(
mut self,
name: impl Into<String>,
value: DynamicQueryValue,
) -> Self {
self.insert_parameter_value(name, value);
self
}
pub fn with_parameter_type(mut self, name: impl Into<String>, ty: QueryParamType) -> Self {
self.insert_parameter_type(name, ty);
self
}
pub fn to_json_bytes(&self) -> Result<Vec<u8>, DynamicQueryError> {
Ok(sonic_rs::to_vec(self)?)
}
pub fn to_json_string(&self) -> Result<String, DynamicQueryError> {
Ok(String::from_utf8(self.to_json_bytes()?)?)
}
}
pub fn read_batch() -> ReadBatch {
ReadBatch::new()
}
pub fn write_batch() -> WriteBatch {
WriteBatch::new()
}
#[allow(missing_docs)]
pub mod prelude {
pub use crate::{
g, read_batch, register, sub, write_batch, AggregateFunction, BatchCondition, BatchEntry,
CompareOp, DateTime, DynamicQueryError, DynamicQueryRequest, DynamicQueryRequestType,
DynamicQueryValue, EdgeId, EdgeRef, EmitBehavior, Expr, ExprProjection, IndexSpec, NodeId,
NodeRef, Order, ParamObject, ParamValue, Predicate, Projection, PropertyInput,
PropertyProjection, PropertyValue, ReadBatch, RepeatConfig, SourcePredicate, StreamBound,
SubTraversal, Traversal, WriteBatch,
generate, generate_to_path, GenerateError, QueryBundle, QueryParameter, QueryParamType,
};
}
#[doc(hidden)]
pub type PropertyMap = HashMap<String, PropertyValue>;
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use crate::query_generator::{
deserialize_query_bundle, serialize_query_bundle, GenerateError, QueryBundle,
QueryParamType, QueryParameter, QUERY_BUNDLE_VERSION,
};
use super::*;
fn query_entry(entry: &BatchEntry) -> &NamedQuery {
match entry {
BatchEntry::Query(query) => query,
other => panic!("expected query entry, got {other:?}"),
}
}
#[test]
fn query_bundle_roundtrip_with_bincode_fixint() {
let mut bundle = QueryBundle::default();
bundle.read_routes.insert(
"read_users".to_string(),
read_batch().var_as(
"count",
g().n_with_label("User")
.where_(Predicate::is_in(
"status",
vec!["active".to_string(), "pending".to_string()],
))
.count(),
),
);
bundle.read_parameters.insert(
"read_users".to_string(),
vec![QueryParameter {
name: "filters".to_string(),
ty: QueryParamType::Object,
}],
);
bundle.write_routes.insert(
"create_user".to_string(),
write_batch().var_as("created", g().add_n("User", vec![("name", "Alice")])),
);
bundle.write_parameters.insert(
"create_user".to_string(),
vec![QueryParameter {
name: "data".to_string(),
ty: QueryParamType::Array(Box::new(QueryParamType::Object)),
}],
);
let bytes = serialize_query_bundle(&bundle).expect("serialize query bundle");
let decoded = deserialize_query_bundle(&bytes).expect("deserialize query bundle");
assert_eq!(decoded.version, QUERY_BUNDLE_VERSION);
assert_eq!(decoded.read_routes.len(), 1);
assert_eq!(decoded.write_routes.len(), 1);
assert!(decoded.read_routes.contains_key("read_users"));
assert!(decoded.write_routes.contains_key("create_user"));
assert_eq!(
decoded.read_parameters.get("read_users"),
Some(&vec![QueryParameter {
name: "filters".to_string(),
ty: QueryParamType::Object,
}])
);
assert_eq!(
decoded.write_parameters.get("create_user"),
Some(&vec![QueryParameter {
name: "data".to_string(),
ty: QueryParamType::Array(Box::new(QueryParamType::Object)),
}])
);
}
#[test]
fn query_bundle_rejects_unsupported_version() {
let mut bundle = QueryBundle::default();
bundle.version = QUERY_BUNDLE_VERSION + 1;
let bytes = serialize_query_bundle(&bundle).expect("serialize query bundle");
let err = deserialize_query_bundle(&bytes).expect_err("version should fail");
assert!(matches!(
err,
GenerateError::UnsupportedVersion {
found: _,
expected: QUERY_BUNDLE_VERSION,
}
));
}
#[test]
fn test_traversal_builder() {
let t = g()
.n([1u64, 2, 3])
.out(Some("FOLLOWS"))
.has("active", "true")
.limit(10);
assert_eq!(t.steps.len(), 4);
assert!(matches!(&t.steps[0], Step::N(NodeRef::Ids(ids)) if ids == &vec![1, 2, 3]));
assert!(matches!(&t.steps[1], Step::Out(Some(label)) if label == "FOLLOWS"));
}
#[test]
fn test_variable_steps() {
let t = g()
.n([1u64])
.out(None::<String>)
.as_("neighbors")
.out(None::<String>)
.within("neighbors");
assert_eq!(t.steps.len(), 5);
assert!(matches!(&t.steps[2], Step::As(name) if name == "neighbors"));
assert!(matches!(&t.steps[4], Step::Within(name) if name == "neighbors"));
}
#[test]
fn test_terminal_detection() {
let t1 = g().n([1u64]).out(None::<String>);
assert!(!t1.has_terminal());
let t2 = g().n([1u64]).count();
assert!(t2.has_terminal());
let t3 = g().n([1u64]).exists();
assert!(t3.has_terminal());
}
#[test]
fn test_node_ref_from_impls() {
let all = NodeRef::all();
assert!(matches!(all, NodeRef::All));
let r1: NodeRef = 42u64.into();
assert!(matches!(r1, NodeRef::Ids(ids) if ids == vec![42]));
let r2: NodeRef = vec![1u64, 2, 3].into();
assert!(matches!(r2, NodeRef::Ids(ids) if ids == vec![1, 2, 3]));
let r3: NodeRef = "my_var".into();
assert!(matches!(r3, NodeRef::Var(name) if name == "my_var"));
let r4 = NodeRef::param("node_id");
assert!(matches!(r4, NodeRef::Param(name) if name == "node_id"));
}
#[test]
fn test_edge_ref_param_constructor() {
let r = EdgeRef::param("edge_id");
assert!(matches!(r, EdgeRef::Param(name) if name == "edge_id"));
}
#[test]
fn test_add_n_and_add_e() {
let t = g()
.add_n("User", vec![("name", "Alice")])
.as_("alice")
.add_n("User", vec![("name", "Bob")])
.add_e("KNOWS", NodeRef::var("alice"), vec![("since", "2024")]);
assert_eq!(t.steps.len(), 4);
assert!(
matches!(&t.steps[0], Step::AddN { label, properties } if label == "User" && properties.len() == 1)
);
assert!(
matches!(&t.steps[3], Step::AddE { label, to: NodeRef::Var(name), .. } if label == "KNOWS" && name == "alice")
);
}
#[test]
fn test_predicate_builder() {
let p1 = Predicate::eq("name", "Alice");
assert!(
matches!(p1, Predicate::Eq(prop, PropertyValue::String(val)) if prop == "name" && val == "Alice")
);
let p_in = Predicate::is_in("status", vec!["active".to_string(), "pending".to_string()]);
assert!(matches!(
p_in,
Predicate::IsIn(prop, PropertyValue::StringArray(values))
if prop == "status" && values == vec!["active".to_string(), "pending".to_string()]
));
let p2 = Predicate::and(vec![
Predicate::eq("status", "active"),
Predicate::gt("age", "18"),
]);
assert!(matches!(p2, Predicate::And(preds) if preds.len() == 2));
}
#[test]
fn test_edge_traversal() {
let t = g()
.n([1u64])
.out_e(Some("FOLLOWS"))
.edge_has("weight", 1i64)
.out_n()
.has_label("User");
assert_eq!(t.steps.len(), 5);
}
#[test]
fn test_sub_traversal() {
let t = g()
.n([1u64])
.union(vec![sub().out(Some("FOLLOWS")), sub().out(Some("LIKES"))]);
assert_eq!(t.steps.len(), 2);
if let Step::Union(subs) = &t.steps[1] {
assert_eq!(subs.len(), 2);
} else {
panic!("Expected Union step");
}
}
#[test]
fn test_repeat_with_sub_traversal() {
let t = g()
.n([1u64])
.repeat(RepeatConfig::new(sub().out(None::<&str>)).times(3));
assert_eq!(t.steps.len(), 2);
}
#[test]
fn test_read_batch_construction() {
let b = read_batch()
.var_as(
"user",
g().n_where(SourcePredicate::eq("username", "alice")),
)
.var_as("friends", g().n(NodeRef::var("user")).out(Some("FOLLOWS")))
.returning(["user", "friends"]);
assert_eq!(b.queries.len(), 2);
assert_eq!(b.returns, vec!["user", "friends"]);
let first = query_entry(&b.queries[0]);
let second = query_entry(&b.queries[1]);
assert_eq!(first.name, Some("user".to_string()));
assert!(first.condition.is_none());
assert_eq!(first.steps.len(), 1);
assert_eq!(second.name, Some("friends".to_string()));
assert_eq!(second.steps.len(), 2); }
#[test]
fn test_read_batch_conditional() {
let b = read_batch()
.var_as("user", g().n_where(SourcePredicate::eq("id", 1i64)))
.var_as_if(
"posts",
BatchCondition::VarNotEmpty("user".to_string()),
g().n(NodeRef::var("user")).out(Some("POSTED")),
);
assert_eq!(b.queries.len(), 2);
assert!(matches!(
&query_entry(&b.queries[1]).condition,
Some(BatchCondition::VarNotEmpty(name)) if name == "user"
));
}
#[test]
fn test_read_batch_with_terminal() {
let b = read_batch()
.var_as("user", g().n([1u64]).value_map(None::<Vec<&str>>))
.var_as("friend_count", g().n([1u64]).out(Some("FOLLOWS")).count())
.returning(["user", "friend_count"]);
assert_eq!(b.queries.len(), 2);
assert!(matches!(
query_entry(&b.queries[0]).steps.last(),
Some(Step::ValueMap(_))
));
assert!(matches!(
query_entry(&b.queries[1]).steps.last(),
Some(Step::Count)
));
}
#[test]
fn test_write_batch_construction() {
let b = write_batch()
.var_as("user", g().add_n("User", vec![("name", "Alice")]))
.var_as("post", g().add_n("Post", vec![("title", "Hello")]))
.returning(["user", "post"]);
assert_eq!(b.queries.len(), 2);
assert_eq!(b.returns, vec!["user", "post"]);
}
#[test]
fn test_property_input_from_expr() {
let traversal = g().add_n(
"User",
vec![
("name", PropertyInput::param("name")),
("age", PropertyInput::param("age")),
],
);
assert!(matches!(
&traversal.steps[0],
Step::AddN { properties, .. }
if matches!(&properties[0].1, PropertyInput::Expr(Expr::Param(name)) if name == "name")
&& matches!(&properties[1].1, PropertyInput::Expr(Expr::Param(name)) if name == "age")
));
}
#[test]
fn test_edge_has_accepts_param_input() {
let traversal = g()
.e([1u64])
.edge_has("targetExternalId", PropertyInput::param("targetExternalId"));
assert!(matches!(
&traversal.steps[1],
Step::EdgeHas(property, PropertyInput::Expr(Expr::Param(name)))
if property == "targetExternalId" && name == "targetExternalId"
));
}
#[test]
fn test_write_batch_for_each_param() {
let body = write_batch()
.var_as(
"existing",
g().n_where(SourcePredicate::eq("$label", "User")),
)
.var_as(
"created",
g().add_n("User", vec![("name", PropertyInput::param("name"))]),
);
let batch = write_batch().for_each_param("data", body);
assert_eq!(batch.queries.len(), 1);
assert!(matches!(
&batch.queries[0],
BatchEntry::ForEach { param, body }
if param == "data" && matches!(&body[1], BatchEntry::Query(NamedQuery { name: Some(name), .. }) if name == "created")
));
}
#[test]
fn test_property_value_nested_payload_variants() {
let mut row = BTreeMap::new();
row.insert("externalId".to_string(), PropertyValue::from("u-1"));
row.insert("active".to_string(), PropertyValue::from(true));
let payload = PropertyValue::from(vec![PropertyValue::from(row.clone())]);
assert!(matches!(payload.as_array(), Some(values) if values.len() == 1));
assert_eq!(
payload
.as_array()
.and_then(|values| values[0].as_object())
.and_then(|map| map.get("externalId"))
.and_then(PropertyValue::as_str),
Some("u-1")
);
}
#[test]
fn test_vector_search_steps() {
let embedding = vec![0.1f32; 4];
let t = g().vector_search_nodes("Doc", "embedding", embedding.clone(), 5, None);
assert!(matches!(
&t.steps[0],
Step::VectorSearchNodes {
label,
property,
query_vector: PropertyInput::Value(PropertyValue::F32Array(values)),
k: StreamBound::Literal(k),
tenant_value,
}
if label == "Doc"
&& property == "embedding"
&& values == &embedding
&& *k == 5
&& tenant_value.is_none()
));
let t2 = g().vector_search_edges("SIMILAR", "embedding", embedding.clone(), 3, None);
assert!(matches!(
&t2.steps[0],
Step::VectorSearchEdges {
label,
property,
query_vector: PropertyInput::Value(PropertyValue::F32Array(values)),
k: StreamBound::Literal(k),
tenant_value,
}
if label == "SIMILAR"
&& property == "embedding"
&& values == &embedding
&& *k == 3
&& tenant_value.is_none()
));
let t3 = g().vector_search_nodes(
"Doc",
"embedding",
vec![0.1f32; 4],
5,
Some(PropertyValue::from("tenant-a")),
);
assert!(matches!(
&t3.steps[0],
Step::VectorSearchNodes {
label,
property,
k: StreamBound::Literal(k),
tenant_value: Some(PropertyInput::Value(PropertyValue::String(value))),
..
} if label == "Doc" && property == "embedding" && *k == 5 && value == "tenant-a"
));
}
#[test]
fn test_parameterized_vector_search_steps() {
let t = g().vector_search_nodes_with(
"Doc",
"embedding",
PropertyInput::param("queryVector"),
Expr::param("limit"),
Some(PropertyInput::param("firmId")),
);
assert!(matches!(
&t.steps[0],
Step::VectorSearchNodes {
label,
property,
query_vector: PropertyInput::Expr(Expr::Param(query_vector)),
k: StreamBound::Expr(Expr::Param(limit)),
tenant_value: Some(PropertyInput::Expr(Expr::Param(firm_id))),
}
if label == "Doc"
&& property == "embedding"
&& query_vector == "queryVector"
&& limit == "limit"
&& firm_id == "firmId"
));
}
#[test]
fn test_text_search_steps() {
let t = g().text_search_nodes("Doc", "body", "alice search", 5, None);
assert!(matches!(
&t.steps[0],
Step::TextSearchNodes {
label,
property,
query_text: PropertyInput::Value(PropertyValue::String(query)),
k: StreamBound::Literal(k),
tenant_value,
}
if label == "Doc"
&& property == "body"
&& query == "alice search"
&& *k == 5
&& tenant_value.is_none()
));
let t2 = g().text_search_edges(
"REL",
"body",
"alice edge",
3,
Some(PropertyValue::from("tenant-a")),
);
assert!(matches!(
&t2.steps[0],
Step::TextSearchEdges {
label,
property,
query_text: PropertyInput::Value(PropertyValue::String(query)),
k: StreamBound::Literal(k),
tenant_value: Some(PropertyInput::Value(PropertyValue::String(tenant))),
}
if label == "REL"
&& property == "body"
&& query == "alice edge"
&& *k == 3
&& tenant == "tenant-a"
));
}
#[test]
fn test_parameterized_text_search_steps() {
let t = g().text_search_nodes_with(
"Doc",
"body",
PropertyInput::param("queryText"),
Expr::param("limit"),
Some(PropertyInput::param("tenantId")),
);
assert!(matches!(
&t.steps[0],
Step::TextSearchNodes {
label,
property,
query_text: PropertyInput::Expr(Expr::Param(query_text)),
k: StreamBound::Expr(Expr::Param(limit)),
tenant_value: Some(PropertyInput::Expr(Expr::Param(tenant_id))),
}
if label == "Doc"
&& property == "body"
&& query_text == "queryText"
&& limit == "limit"
&& tenant_id == "tenantId"
));
}
#[test]
fn test_parameterized_stream_bounds() {
let range = g()
.n_with_label("User")
.range(Expr::param("start"), Expr::param("end"));
assert!(matches!(
range.steps.as_slice(),
[
Step::NWhere(_),
Step::RangeBy(StreamBound::Expr(Expr::Param(start)), StreamBound::Expr(Expr::Param(end))),
] if start == "start" && end == "end"
));
let ordered = g()
.n_with_label("User")
.order_by("age", Order::Desc)
.limit(Expr::param("limit"))
.skip(Expr::param("offset"));
assert!(matches!(
ordered.steps.as_slice(),
[
Step::NWhere(_),
Step::OrderBy(property, Order::Desc),
Step::LimitBy(Expr::Param(limit)),
Step::SkipBy(Expr::Param(offset)),
] if property == "age" && limit == "limit" && offset == "offset"
));
}
#[test]
fn test_contains_param_predicate() {
assert!(matches!(
Predicate::contains_param("location", "city"),
Predicate::ContainsExpr(property, Expr::Param(param))
if property == "location" && param == "city"
));
}
#[test]
fn test_is_in_param_predicate() {
assert!(matches!(
Predicate::is_in_param("location", "cities"),
Predicate::IsInExpr(property, Expr::Param(param))
if property == "location" && param == "cities"
));
}
#[test]
fn source_predicate_literal_stays_literal() {
assert!(matches!(
SourcePredicate::eq("username", "alice"),
SourcePredicate::Eq(prop, PropertyValue::String(v))
if prop == "username" && v == "alice"
));
assert!(matches!(
SourcePredicate::gt("score", 10i64),
SourcePredicate::Gt(_, PropertyValue::I64(10))
));
}
#[test]
fn source_predicate_param_routes_to_expr_variant() {
assert!(matches!(
SourcePredicate::eq("username", Expr::param("name")),
SourcePredicate::EqExpr(prop, Expr::Param(p))
if prop == "username" && p == "name"
));
assert!(matches!(
SourcePredicate::lte("score", Expr::param("max")),
SourcePredicate::LteExpr(_, Expr::Param(p)) if p == "max"
));
}
#[test]
fn source_predicate_between_dispatch() {
assert!(matches!(
SourcePredicate::between("age", 18i64, 65i64),
SourcePredicate::Between(_, PropertyValue::I64(18), PropertyValue::I64(65))
));
assert!(matches!(
SourcePredicate::between("age", Expr::param("lo"), 65i64),
SourcePredicate::BetweenExpr(_, Expr::Param(lo), Expr::Constant(PropertyValue::I64(65)))
if lo == "lo"
));
}
#[test]
fn source_predicate_expr_converts_to_compare() {
let pred: Predicate = SourcePredicate::eq("username", Expr::param("name")).into();
assert!(matches!(
pred,
Predicate::Compare { left: Expr::Property(prop), op: CompareOp::Eq, right: Expr::Param(p) }
if prop == "username" && p == "name"
));
}
#[test]
fn test_create_vector_index_steps() {
let t = g().create_vector_index_nodes("Doc", "embedding", None::<&str>);
assert!(t.has_terminal());
assert!(matches!(
&t.steps[0],
Step::CreateIndex {
spec: IndexSpec::NodeVector {
label,
property,
tenant_property,
},
if_not_exists,
} if label == "Doc"
&& property == "embedding"
&& tenant_property.is_none()
&& *if_not_exists
));
let t2 = g().create_vector_index_edges("REL", "embedding", None::<&str>);
assert!(matches!(
&t2.steps[0],
Step::CreateIndex {
spec: IndexSpec::EdgeVector {
label,
property,
tenant_property,
},
if_not_exists,
} if label == "REL"
&& property == "embedding"
&& tenant_property.is_none()
&& *if_not_exists
));
let t3 = g().create_vector_index_nodes("Doc", "embedding", Some("tenant_id"));
assert!(matches!(
&t3.steps[0],
Step::CreateIndex {
spec: IndexSpec::NodeVector {
tenant_property: Some(tenant_property),
..
},
if_not_exists,
} if tenant_property == "tenant_id" && *if_not_exists
));
}
#[test]
fn test_create_text_index_steps() {
let t = g().create_text_index_nodes("Doc", "body", None::<&str>);
assert!(matches!(
&t.steps[0],
Step::CreateIndex {
spec: IndexSpec::NodeText {
label,
property,
tenant_property,
},
if_not_exists,
} if label == "Doc"
&& property == "body"
&& tenant_property.is_none()
&& *if_not_exists
));
let t2 = g().create_text_index_edges("REL", "body", Some("tenant_id"));
assert!(matches!(
&t2.steps[0],
Step::CreateIndex {
spec: IndexSpec::EdgeText {
label,
property,
tenant_property: Some(tenant_property),
},
if_not_exists,
} if label == "REL"
&& property == "body"
&& tenant_property == "tenant_id"
&& *if_not_exists
));
}
#[test]
fn test_generic_index_steps() {
let create = g().create_index_if_not_exists(IndexSpec::node_equality("User", "status"));
assert!(create.has_terminal());
assert!(matches!(
&create.steps[0],
Step::CreateIndex {
spec: IndexSpec::NodeEquality {
label,
property,
unique,
},
if_not_exists,
} if label == "User" && property == "status" && !unique && *if_not_exists
));
let drop = g().drop_index(IndexSpec::edge_range("FOLLOWS", "weight"));
assert!(drop.has_terminal());
assert!(matches!(
&drop.steps[0],
Step::DropIndex {
spec: IndexSpec::EdgeRange { label, property },
} if label == "FOLLOWS" && property == "weight"
));
}
#[test]
fn test_unique_node_equality_constructor() {
assert_eq!(
IndexSpec::node_unique_equality("User", "email"),
IndexSpec::NodeEquality {
label: "User".to_string(),
property: "email".to_string(),
unique: true,
}
);
}
#[test]
fn test_node_equality_deserializes_unique_default_false() {
let decoded: IndexSpec =
sonic_rs::from_str(r#"{"NodeEquality":{"label":"User","property":"status"}}"#)
.expect("deserialize old node equality payload");
assert_eq!(
decoded,
IndexSpec::NodeEquality {
label: "User".to_string(),
property: "status".to_string(),
unique: false,
}
);
}
#[test]
fn test_node_unique_equality_serializes_unique_flag() {
let encoded = sonic_rs::to_string(&IndexSpec::node_unique_equality("User", "status"))
.expect("serialize unique node equality");
assert!(encoded.contains(r#""NodeEquality""#));
assert!(encoded.contains(r#""unique":true"#));
}
}