use std::collections::HashMap;
use crate::graph::GraphStorage;
use crate::*;
pub(crate) mod bulk;
pub mod cancel;
pub(crate) mod context;
pub mod execution;
pub(crate) mod memory_budget;
pub mod metrics;
pub(crate) mod optimizer;
pub(crate) mod permission_check;
pub mod render;
pub(crate) mod side_effect;
pub mod source;
pub mod spelling;
pub mod step;
pub mod table;
pub(crate) mod traverser;
pub(crate) mod tree_render;
#[cfg(test)]
mod bulk_tests;
#[cfg(all(test, feature = "script"))]
pub(crate) mod test_utils;
#[cfg(test)]
pub(crate) mod unwind_probe;
pub use cancel::CancelToken;
pub use context::*;
pub use execution::{ExecOptions, Execution};
pub use metrics::{
LoopMetrics, MemoryMetrics, MemorySource, Metrics, StepCounts, TimingMetrics, TraversalMetrics,
};
pub use render::{RenderOptions, RenderedOutput, Truncation};
pub use spelling::Spelling;
pub use step::flatmap_steps::step_glob_path::GlobPathOptions;
pub use step::sideeffect_steps::SideEffectFn;
pub use step::*;
pub(crate) use traverser::Traverser;
#[doc(hidden)]
pub fn optimizer_rule_names() -> Vec<&'static str> {
optimizer::TraversalOptimizer::<crate::TraversalGraph>::registry()
.into_iter()
.map(|(name, _, _)| name)
.collect()
}
#[derive(Debug, Clone)]
pub struct GraphSource {}
#[derive(Clone)]
pub struct GraphTraversalSource<'g, S: GraphStorage + 'g> {
pub(crate) storage: S,
pub(crate) strategy: TraversalStrategy,
pub(crate) steps: Vec<GraphStep<'g, S>>,
pub(crate) side_effect: HashMap<CompactString, side_effect::SideEffectValue<S>>,
pub(crate) options: HashMap<CompactString, ElementProperty>,
pub(crate) profile_type: ProfileType,
pub(crate) sack: Option<crate::value::SackSpec>,
pub(crate) restrictions: Option<std::sync::Arc<permission_check::Restrictions>>,
}
pub struct __;
pub struct AnonymousTraversal<'g, S: GraphStorage + 'g> {
pub(crate) steps: Vec<GraphStep<'g, S>>,
pub(crate) level: Option<std::sync::Arc<crate::traversal::optimizer::LevelPlan<'g, S>>>,
pub(crate) optimized: bool,
pub(crate) path_modes: Vec<crate::traversal::optimizer::path_analysis::PathMode>,
pub(crate) effects: crate::traversal::optimizer::path_analysis::PlanEffects,
pub(crate) child_index: usize,
}
impl<'g, S: GraphStorage + 'g> Clone for AnonymousTraversal<'g, S> {
fn clone(&self) -> Self {
Self {
steps: self.steps.clone(),
level: self.level.clone(),
optimized: self.optimized,
path_modes: self.path_modes.clone(),
effects: self.effects,
child_index: self.child_index,
}
}
}
impl<'g, S: GraphStorage + 'g> Default for AnonymousTraversal<'g, S> {
fn default() -> Self {
Self {
steps: Vec::new(),
level: None,
optimized: false,
path_modes: Vec::new(),
effects: Default::default(),
child_index: 0,
}
}
}
impl<'g, S: GraphStorage + 'g> AnonymousTraversal<'g, S> {
pub fn new() -> Self {
Self {
steps: Vec::new(),
level: None,
optimized: false,
path_modes: Vec::new(),
effects: Default::default(),
child_index: 0,
}
}
pub(crate) fn push_step(mut self, step: GraphStep<'g, S>) -> Self {
push_step_into(&mut self.steps, step, "__");
self
}
pub fn len(&self) -> usize {
self.steps.len()
}
pub(crate) fn has_barrier(&self) -> bool {
self.steps.iter().any(|step| match step {
GraphStep::Barrier(..) | GraphStep::Count(..) => true,
GraphStep::FlatBarrier(name, step) => {
!matches!(*name, "repeat" | "barrier" | "discard") || step.holds_loop_state()
}
GraphStep::Init(_, init) => init.is_count_only(),
_ => false,
})
}
pub fn is_empty(&self) -> bool {
self.steps.is_empty()
}
pub(crate) fn optimize(
&mut self,
optimizer: &crate::traversal::optimizer::TraversalOptimizer<'g, S>,
) {
if self.optimized {
return;
}
self.optimized = true;
self.level = optimizer.apply(&mut self.steps).map(std::sync::Arc::new);
}
pub(crate) fn analyze_paths(
&mut self,
analysis: &crate::traversal::optimizer::path_analysis::PathAnalysis,
downstream: crate::traversal::optimizer::path_analysis::PathRequirement,
) -> crate::traversal::optimizer::path_analysis::PathRequirement {
let saved = analysis.take_effects();
let (modes, entry) = analysis.analyze_level(&mut self.steps, downstream);
self.path_modes = modes;
self.effects = analysis.take_effects();
analysis.restore_effects(saved, self.effects);
entry
}
pub(crate) fn to_list(
&self,
context: &mut TraverserContext<'_, S>,
source: Vec<Traverser<S>>,
) -> TraverserResult<Vec<Traverser<S>>> {
GraphTraversalSource::<'g, S>::execute_optimized_steps(
&self.steps,
source,
context,
crate::traversal::source::LevelAt::child(self.child_index),
self.level.as_deref(),
&self.path_modes,
)
}
pub(crate) fn apply_to(
&self,
context: &mut TraverserContext<'_, S>,
parent: &Traverser<S>,
) -> TraverserResult<Vec<Traverser<S>>> {
context.with_persist(false, |context| self.apply_to_inner(context, parent))
}
fn apply_to_inner(
&self,
context: &mut TraverserContext<'_, S>,
parent: &Traverser<S>,
) -> TraverserResult<Vec<Traverser<S>>> {
if parent.bulk > 1 && self.effects.writes_side_effects {
let mut all = Vec::new();
for _ in 0..parent.bulk {
context.tick()?;
let unit = Traverser::with_parts(parent.value.clone(), parent.path, 1, parent.sack);
all.extend(self.to_list(context, vec![unit])?);
}
return Ok(all);
}
let unit = Traverser::with_parts(parent.value.clone(), parent.path, 1, parent.sack);
let mut produced = self.to_list(context, vec![unit])?;
if parent.bulk != 1 {
for item in &mut produced {
item.bulk =
crate::traversal::bulk::mul_bulk(item.bulk, parent.bulk, "child traversal")?;
}
}
Ok(produced)
}
pub(crate) fn first(
&self,
context: &mut TraverserContext<'_, S>,
source: &Traverser<S>,
) -> TraverserResult<Option<Traverser<S>>> {
context.with_persist(false, |context| self.first_inner(context, source))
}
fn first_inner(
&self,
context: &mut TraverserContext<'_, S>,
source: &Traverser<S>,
) -> TraverserResult<Option<Traverser<S>>> {
if source.bulk > 1 && self.effects.writes_side_effects {
let mut result = None;
for run in 0..source.bulk {
context.tick()?;
let unit = Traverser::with_parts(source.value.clone(), source.path, 1, source.sack);
let found = self.first(context, &unit)?;
if run == 0 {
result = found;
}
}
return Ok(result);
}
let unit;
let source = if source.bulk == 1 {
source
} else {
unit = Traverser::with_parts(source.value.clone(), source.path, 1, source.sack);
&unit
};
GraphTraversalSource::<'g, S>::execute_optimized_steps_first(
&self.steps,
source,
context,
self.child_index,
self.level.as_deref(),
&self.path_modes,
)
}
pub(crate) fn exists(
&self,
context: &mut TraverserContext<'_, S>,
source: &Traverser<S>,
) -> TraverserResult<bool> {
context.with_persist(false, |context| self.exists_inner(context, source))
}
fn exists_inner(
&self,
context: &mut TraverserContext<'_, S>,
source: &Traverser<S>,
) -> TraverserResult<bool> {
if source.bulk > 1 && self.effects.writes_side_effects {
let mut found = false;
for _ in 0..source.bulk {
context.tick()?;
let unit = Traverser::with_parts(source.value.clone(), source.path, 1, source.sack);
found |= self.exists(context, &unit)?;
}
return Ok(found);
}
let unit;
let source = if source.bulk == 1 {
source
} else {
unit = Traverser::with_parts(source.value.clone(), source.path, 1, source.sack);
&unit
};
GraphTraversalSource::<'g, S>::execute_optimized_steps_exists(
&self.steps,
source,
context,
self.child_index,
self.level.as_deref(),
&self.path_modes,
)
}
}
impl<'g, S: GraphStorage + 'g> From<GraphTraversalSource<'g, S>> for AnonymousTraversal<'g, S> {
fn from(gts: GraphTraversalSource<'g, S>) -> Self {
Self {
steps: gts.steps,
level: None,
optimized: false,
path_modes: Vec::new(),
effects: Default::default(),
child_index: 0,
}
}
}
impl<'g, S: GraphStorage + 'g> From<Vec<GraphStep<'g, S>>> for AnonymousTraversal<'g, S> {
fn from(steps: Vec<GraphStep<'g, S>>) -> Self {
Self {
steps,
level: None,
optimized: false,
path_modes: Vec::new(),
effects: Default::default(),
child_index: 0,
}
}
}
impl<'g, S: GraphStorage + 'g> std::fmt::Debug for AnonymousTraversal<'g, S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AnonymousTraversal")
.field("steps", &self.steps)
.finish()
}
}
impl<'g, S: GraphStorage + 'g> std::fmt::Display for AnonymousTraversal<'g, S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("__")?;
let mut any = false;
for step in &self.steps {
if matches!(step, GraphStep::Modulator(_, _)) {
continue;
}
any = true;
write!(f, ".{}", step.get_name())?;
}
if !any {
f.write_str(".identity()")?;
}
Ok(())
}
}
#[non_exhaustive]
pub enum By<'g, S: GraphStorage + 'g> {
Property(CompactString),
Traversal(AnonymousTraversal<'g, S>),
Order(Order),
Label,
Id,
Value,
Identity,
Key,
Count,
JsonPath(crate::value::JsonPath),
Labels(Vec<CompactString>),
Column(crate::value::Column),
}
impl<'g, S: GraphStorage + 'g> std::fmt::Debug for By<'g, S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
By::Property(prop) => f.debug_tuple("Property").field(prop).finish(),
By::Traversal(t) => f.debug_tuple("Traversal").field(t).finish(),
By::Order(o) => f.debug_tuple("Order").field(o).finish(),
By::Label => write!(f, "Label"),
By::Id => write!(f, "Id"),
By::Value => write!(f, "Value"),
By::Identity => write!(f, "Identity"),
By::Key => write!(f, "Key"),
By::Count => write!(f, "Count"),
By::JsonPath(path) => write!(f, "jpath(\"{path}\")"),
By::Labels(labels) => f.debug_tuple("Labels").field(labels).finish(),
By::Column(column) => write!(f, "Column.{column}"),
}
}
}
impl<'g, S: GraphStorage + 'g> std::fmt::Display for By<'g, S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use crate::value::literal::{rhai_string, rhai_string_array};
match self {
By::Property(prop) => f.write_str(&rhai_string(prop)),
By::Traversal(t) => write!(f, "{t}"),
By::Order(o) => write!(f, "{o}"),
By::Label => f.write_str("T.label"),
By::Id => f.write_str("T.id"),
By::Value => f.write_str("T.value"),
By::Identity => f.write_str("__.identity()"),
By::Key => f.write_str("T.key"),
By::Count => f.write_str("By.Count"),
By::JsonPath(path) => write!(f, "jpath({})", rhai_string(&path.to_string())),
By::Labels(labels) => f.write_str(&rhai_string_array(labels)),
By::Column(column) => write!(f, "Column.{column}"),
}
}
}
pub(crate) fn render_traversals<'g, S: GraphStorage + 'g>(
traversals: &[AnonymousTraversal<'g, S>],
) -> String {
traversals
.iter()
.map(|traversal| traversal.to_string())
.collect::<Vec<_>>()
.join(", ")
}
pub(crate) fn render_by<'g, S: GraphStorage + 'g>(by: &By<'g, S>) -> String {
format!(".by({by})")
}
impl<'g, S: GraphStorage + 'g> Clone for By<'g, S> {
fn clone(&self) -> Self {
match self {
By::Property(prop) => By::Property(prop.clone()),
By::Traversal(t) => By::Traversal(t.clone()),
By::Order(o) => By::Order(o.clone()),
By::Label => By::Label,
By::Id => By::Id,
By::Value => By::Value,
By::Identity => By::Identity,
By::Key => By::Key,
By::Count => By::Count,
By::JsonPath(path) => By::JsonPath(path.clone()),
By::Labels(labels) => By::Labels(labels.clone()),
By::Column(column) => By::Column(*column),
}
}
}
impl<'g, S: GraphStorage + 'g> By<'g, S> {
pub(crate) fn traversal_mut(&mut self) -> Option<&mut AnonymousTraversal<'g, S>> {
match self {
By::Traversal(traversal) => Some(traversal),
_ => None,
}
}
pub(crate) fn analyze_paths(
&mut self,
analysis: &crate::traversal::optimizer::path_analysis::PathAnalysis,
) -> crate::traversal::optimizer::path_analysis::PathRequirement {
match self {
By::Traversal(traversal) => traversal.analyze_paths(
analysis,
crate::traversal::optimizer::path_analysis::PathRequirement::None,
),
_ => crate::traversal::optimizer::path_analysis::PathRequirement::None,
}
}
pub(crate) fn analyze_all_paths(
list: &mut [By<'g, S>],
analysis: &crate::traversal::optimizer::path_analysis::PathAnalysis,
) -> crate::traversal::optimizer::path_analysis::PathRequirement {
list.iter_mut().fold(
crate::traversal::optimizer::path_analysis::PathRequirement::None,
|need, by| need.union(by.analyze_paths(analysis)),
)
}
}
#[must_use]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ByModulation {
Accepted,
Unsupported,
}
#[must_use]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ArgsModulation {
Accepted,
Unsupported,
}
pub(crate) fn push_step_into<'g, S: GraphStorage + 'g>(
steps: &mut Vec<GraphStep<'g, S>>,
mut step: GraphStep<'g, S>,
source: &'static str,
) {
if let GraphStep::Modulator(modulator_name, mod_step) = &mut step {
let args = mod_step.get_args();
let by = mod_step.get_by();
let child = mod_step.get_traversal();
let with_option = mod_step.get_with_option();
let target = if args.is_some() || child.is_some() {
from_to_target(steps)
} else {
steps
.iter()
.rposition(|s| !matches!(s, GraphStep::Modulator(_, _) | GraphStep::As(_, _)))
};
let rejected = match target {
Some(index) => {
let position = steps[index + 1..]
.iter()
.filter(
|s| matches!(s, GraphStep::Modulator(name, _) if *name == *modulator_name),
)
.count()
+ 1;
let target = &mut steps[index];
let args_rejected =
args.and_then(
|args| match target.push_modulator_args(args, modulator_name) {
ArgsModulation::Accepted => None,
ArgsModulation::Unsupported => {
let rendered = target.get_name();
let name = user_step_name(&rendered);
Some(StepModulatorRejected {
step: name.clone(),
position,
modulator: args_reason::MODULATOR,
reason: format!("{name}(){}", args_reason::NOT_ACCEPTED),
})
}
},
);
let by_rejected =
by.and_then(|by| match target.push_modulator_by(by, modulator_name) {
ByModulation::Accepted => None,
ByModulation::Unsupported => {
let rendered = target.get_name();
let name = user_step_name(&rendered);
let shown = if name == "select" {
rendered
} else {
format!("{name}()")
};
Some(StepModulatorRejected {
step: name,
position,
modulator: by_reason::MODULATOR,
reason: format!("{shown}{}", by_reason::NOT_ACCEPTED),
})
}
});
let child_rejected = child.and_then(|child| {
match target.push_modulator_traversal(child, modulator_name) {
ArgsModulation::Accepted => None,
ArgsModulation::Unsupported => {
let rendered = target.get_name();
let name = user_step_name(&rendered);
Some(StepModulatorRejected {
step: name.clone(),
position,
modulator: args_reason::MODULATOR,
reason: format!("{name}(){}", args_reason::NOT_ACCEPTED),
})
}
}
});
let with_rejected = with_option.and_then(|(key, value)| {
target.push_with_option(key, value).map(|reason| {
let name = user_step_name(&target.get_name());
StepModulatorRejected {
step: name.clone(),
position,
modulator: with_reason::MODULATOR,
reason: format!("{name}(): {reason}"),
}
})
});
args_rejected
.or(child_rejected)
.or(by_rejected)
.or(with_rejected)
}
None => {
if with_option.is_some() {
Some(StepModulatorRejected {
step: source.to_string(),
position: 1,
modulator: with_reason::MODULATOR,
reason: with_reason::NO_STEP.to_string(),
})
} else if args.is_some() || child.is_some() {
Some(StepModulatorRejected {
step: source.to_string(),
position: 1,
modulator: args_reason::MODULATOR,
reason: args_reason::NO_STEP.to_string(),
})
} else {
by.map(|_| StepModulatorRejected {
step: source.to_string(),
position: 1,
modulator: by_reason::MODULATOR,
reason: by_reason::NO_STEP.to_string(),
})
}
}
};
if let Some(rejected) = rejected {
step = GraphStep::Modulator(modulator_name, Box::new(rejected));
}
}
steps.push(step);
}
impl<'g, S: GraphStorage + 'g> GraphTraversalSource<'g, S> {
#[cfg_attr(not(feature = "script"), allow(dead_code))]
pub(crate) fn adopt_lowered(mut self, body: AnonymousTraversal<'g, S>) -> Self {
self.steps = body.steps;
for step in &mut self.steps {
if let GraphStep::Modulator(_, modulator) = step {
modulator.rename_root("g");
}
}
self
}
}
pub(crate) fn from_to_target<'g, S: GraphStorage + 'g>(
steps: &[GraphStep<'g, S>],
) -> Option<usize> {
let mut index = steps.len();
while index > 0 {
match &steps[index - 1] {
GraphStep::Modulator(_, _)
| GraphStep::As(_, _)
| GraphStep::SideEffectMut("property", _) => {
index -= 1;
}
GraphStep::SideEffectMut("add_e", _) => return Some(index - 1),
_ => break,
}
}
steps
.iter()
.rposition(|s| !matches!(s, GraphStep::Modulator(_, _) | GraphStep::As(_, _)))
}
fn user_step_name(rendered: &str) -> String {
let base = rendered.split('(').next().unwrap_or(rendered);
match base {
"has_p" => "has".to_string(),
"where_p" | "where_t" => "where".to_string(),
other => other.to_string(),
}
}
pub(crate) mod by_reason {
pub(crate) const MODULATOR: &str = "by()";
pub(crate) const NOT_ACCEPTED: &str = " does not take by() modulators";
pub(crate) const NO_STEP: &str = "by() modulates the step before it, and there is none";
pub(crate) const COLUMN: &str = "a Column (keys/values) is a sort key only order() takes";
}
pub(crate) mod args_reason {
pub(crate) const MODULATOR: &str = "from()/to()";
pub(crate) const NOT_ACCEPTED: &str = " does not take from()/to() modulators";
pub(crate) const NO_STEP: &str = "from()/to() modulates the step before it, and there is none";
pub(crate) const ONE_LABEL: &str = "from()/to() of a path step takes exactly one step label";
}
pub(crate) mod with_reason {
pub(crate) const MODULATOR: &str = "with()";
pub(crate) const NOT_ACCEPTED: &str = "this step does not take with(WithOptions..) options";
pub(crate) const NO_STEP: &str = "with() configures the step before it, and there is none";
}
#[derive(Debug, Clone)]
pub(crate) struct StepModulatorRejected {
step: String,
position: usize,
modulator: &'static str,
reason: String,
}
impl std::fmt::Display for StepModulatorRejected {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.modulator)
}
}
impl<'g, S: GraphStorage + 'g> GraphStepModulator<'g, S> for StepModulatorRejected {
fn rename_root(&mut self, root: &'static str) {
if self.reason == by_reason::NO_STEP
|| self.reason == args_reason::NO_STEP
|| self.reason == with_reason::NO_STEP
{
self.step = root.to_string();
}
}
fn check(&self) -> crate::error::TraverserResult<()> {
Err(Box::new(crate::error::TraverserError::InvalidModulator {
step: self.step.clone(),
position: self.position,
modulator: self.modulator,
reason: self.reason.clone(),
}))
}
fn clone_box(&self) -> Box<dyn GraphStepModulator<'g, S> + 'g> {
Box::new(self.clone())
}
}
pub(crate) mod ring_reason {
pub(crate) const KIND: &str =
"an order or Count is not a projection; this step applies each by() to a value";
}
#[derive(Debug, Clone, Default)]
pub(crate) struct ByModulatorCheck {
calls: usize,
rejected: Option<(usize, &'static str)>,
}
impl ByModulatorCheck {
pub(crate) fn observe<'g, S: GraphStorage + 'g>(&mut self, by: &By<'g, S>) {
self.observe_inner(by, false);
}
pub(crate) fn observe_sort_key<'g, S: GraphStorage + 'g>(&mut self, by: &By<'g, S>) {
self.observe_inner(by, true);
}
fn observe_inner<'g, S: GraphStorage + 'g>(&mut self, by: &By<'g, S>, accepts_column: bool) {
self.calls += 1;
if self.rejected.is_some() {
return;
}
match by {
By::Labels(_) => {
self.rejected = Some((
self.calls,
"a vertex-label list is not a projection this step can use",
));
}
By::Column(_) => {
if !accepts_column {
self.rejected = Some((self.calls, by_reason::COLUMN));
}
}
By::Property(_)
| By::Traversal(_)
| By::Order(_)
| By::Label
| By::Id
| By::Value
| By::Identity
| By::Key
| By::Count
| By::JsonPath(_) => {}
}
}
pub(crate) fn observe_ring<'g, S: GraphStorage + 'g>(&mut self, by: &By<'g, S>) {
self.observe(by);
match by {
By::Order(_) | By::Count => self.reject_last(ring_reason::KIND),
By::Property(_)
| By::Traversal(_)
| By::Label
| By::Id
| By::Value
| By::Identity
| By::Key
| By::JsonPath(_)
| By::Labels(_)
| By::Column(_) => {}
}
}
pub(crate) fn reject_last(&mut self, reason: &'static str) {
if self.rejected.is_none() {
self.rejected = Some((self.calls, reason));
}
}
pub(crate) fn has_rejected(&self) -> bool {
self.rejected.is_some()
}
#[inline]
pub(crate) fn check(&self, step: &str) -> crate::error::TraverserResult<()> {
match self.rejected {
Some((position, reason)) => {
Err(Box::new(crate::error::TraverserError::InvalidModulator {
step: step.to_string(),
position,
modulator: by_reason::MODULATOR,
reason: reason.to_string(),
}))
}
None => Ok(()),
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum Order {
Asc,
Desc,
Shuffle,
}
impl std::fmt::Display for Order {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Order::Asc => "Order.asc",
Order::Desc => "Order.desc",
Order::Shuffle => "Order.shuffle",
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_send_sync<T: Send + Sync>() {}
#[test]
fn test_anonymous_traversal_send_sync() {
assert_send_sync::<AnonymousTraversal<crate::graph::TraversalGraph>>();
}
#[test]
fn test_anonymous_traversal_step_chain() {
let anon: AnonymousTraversal<crate::graph::TraversalGraph> =
__::out("knows").has("name", "Marko");
let display = format!("{}", anon);
assert_eq!(display, r#"__.out("knows").has("name", "Marko")"#);
assert!(display.contains("has"));
}
}