use std::fmt;
use std::sync::Arc;
use compact_str::CompactString;
use crate::error::{TraverserError, TraverserResult, ValueError};
use crate::graph::GraphStorage;
use crate::traversal::step::GraphStep;
const MAP_PRODUCING_KINDS: &[&str] = &[
"values",
"properties",
"value_map",
"element_map",
"project",
"constant",
"unfold",
"group",
"property_json",
"json_path",
"inject",
];
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct SmallLabelSet(Arc<[CompactString]>);
impl SmallLabelSet {
pub(crate) fn new<I, T>(labels: I) -> Self
where
I: IntoIterator<Item = T>,
T: Into<CompactString>,
{
let mut labels: Vec<CompactString> = labels.into_iter().map(Into::into).collect();
labels.sort_unstable();
labels.dedup();
Self(Arc::from(labels))
}
pub(crate) fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub(crate) fn as_arc(&self) -> &Arc<[CompactString]> {
&self.0
}
pub(crate) fn contains(&self, label: &str) -> bool {
self.0.binary_search_by(|l| l.as_str().cmp(label)).is_ok()
}
fn union(&self, other: &SmallLabelSet) -> SmallLabelSet {
if other.0.iter().all(|l| self.contains(l)) {
return self.clone();
}
SmallLabelSet::new(self.0.iter().chain(other.0.iter()).cloned())
}
fn without(&self, labels: &[CompactString]) -> SmallLabelSet {
if !labels.iter().any(|l| self.contains(l)) {
return self.clone();
}
SmallLabelSet::new(self.0.iter().filter(|l| !labels.contains(l)).cloned())
}
pub(crate) fn intersects(&self, labels: &[CompactString]) -> bool {
labels.iter().any(|l| self.contains(l))
}
}
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub(crate) enum PathRequirement {
#[default]
None,
Labels(SmallLabelSet),
Full,
}
pub(crate) type PathMode = PathRequirement;
impl PathRequirement {
pub(crate) fn labels<I, T>(labels: I) -> Self
where
I: IntoIterator<Item = T>,
T: Into<CompactString>,
{
let set = SmallLabelSet::new(labels);
if set.is_empty() {
PathRequirement::None
} else {
PathRequirement::Labels(set)
}
}
pub(crate) fn is_none(&self) -> bool {
matches!(self, PathRequirement::None)
}
pub(crate) fn is_full(&self) -> bool {
matches!(self, PathRequirement::Full)
}
pub(crate) fn union(self, other: PathRequirement) -> PathRequirement {
match (self, other) {
(PathRequirement::Full, _) | (_, PathRequirement::Full) => PathRequirement::Full,
(PathRequirement::None, x) | (x, PathRequirement::None) => x,
(PathRequirement::Labels(a), PathRequirement::Labels(b)) => {
PathRequirement::Labels(a.union(&b))
}
}
}
fn retract(self, labels: &[CompactString]) -> PathRequirement {
match self {
PathRequirement::Labels(set) => {
let rest = set.without(labels);
if rest.is_empty() {
PathRequirement::None
} else {
PathRequirement::Labels(rest)
}
}
other => other,
}
}
}
impl fmt::Display for PathRequirement {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PathRequirement::None => f.write_str("none"),
PathRequirement::Full => f.write_str("full"),
PathRequirement::Labels(set) => {
f.write_str("labels(")?;
for (i, label) in set.as_arc().iter().enumerate() {
if i > 0 {
f.write_str(",")?;
}
f.write_str(label)?;
}
f.write_str(")")
}
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) struct PlanEffects {
pub(crate) mutates: bool,
pub(crate) writes_side_effects: bool,
}
impl PlanEffects {
pub(crate) fn or(self, other: PlanEffects) -> PlanEffects {
PlanEffects {
mutates: self.mutates || other.mutates,
writes_side_effects: self.writes_side_effects || other.writes_side_effects,
}
}
}
#[derive(Debug)]
pub(crate) struct PathAnalysis {
effects: std::cell::Cell<PlanEffects>,
force_full: bool,
produced: std::cell::RefCell<Vec<CompactString>>,
known: Option<SmallLabelSet>,
saw_labels: std::cell::Cell<bool>,
required: std::cell::RefCell<Vec<(CompactString, &'static str)>>,
map_producing_seen: std::cell::Cell<bool>,
rejected_modulator: std::cell::RefCell<Option<crate::error::BoxedTraverserError>>,
}
impl PathAnalysis {
pub(crate) fn new(enabled: bool) -> Self {
Self {
force_full: !enabled,
effects: std::cell::Cell::new(PlanEffects::default()),
produced: std::cell::RefCell::new(Vec::new()),
known: None,
saw_labels: std::cell::Cell::new(false),
required: std::cell::RefCell::new(Vec::new()),
map_producing_seen: std::cell::Cell::new(false),
rejected_modulator: std::cell::RefCell::new(None),
}
}
pub(crate) fn analyze_query<'g, S: GraphStorage + 'g>(
enabled: bool,
steps: &mut [GraphStep<'g, S>],
predeclared_side_effects: &std::collections::HashMap<
CompactString,
crate::traversal::side_effect::SideEffectValue<S>,
>,
) -> TraverserResult<(Vec<PathMode>, PlanEffects)> {
let collect = PathAnalysis::new(enabled);
collect
.produced
.borrow_mut()
.extend(predeclared_side_effects.keys().cloned());
let (modes, _) = collect.analyze_level(steps, PathRequirement::None);
collect.check_rejected_modulator()?;
collect.check_undeclared_labels()?;
if collect.force_full || !collect.saw_labels.get() {
return Ok((modes, collect.effects.get()));
}
let effects = collect.effects.get();
let known = SmallLabelSet::new(collect.produced.into_inner());
let analysis = PathAnalysis {
force_full: false,
effects: std::cell::Cell::new(PlanEffects::default()),
produced: std::cell::RefCell::new(Vec::new()),
known: Some(known),
saw_labels: std::cell::Cell::new(false),
required: std::cell::RefCell::new(Vec::new()),
map_producing_seen: std::cell::Cell::new(false),
rejected_modulator: std::cell::RefCell::new(None),
};
Ok((
analysis.analyze_level(steps, PathRequirement::None).0,
effects,
))
}
fn check_rejected_modulator(&self) -> TraverserResult<()> {
match self.rejected_modulator.borrow_mut().take() {
Some(err) => Err(err),
None => Ok(()),
}
}
fn check_undeclared_labels(&self) -> TraverserResult<()> {
if self.map_producing_seen.get() {
return Ok(());
}
let produced = self.produced.borrow();
let required = self.required.borrow();
for (label, _step_name) in required.iter() {
if !produced.iter().any(|p| p == label) {
return Err(Box::new(TraverserError::Value(
ValueError::UndeclaredLabel {
label: label.to_string(),
},
)));
}
}
Ok(())
}
fn restrict(&self, requirement: PathRequirement) -> PathRequirement {
if matches!(requirement, PathRequirement::Labels(_)) {
self.saw_labels.set(true);
}
match (requirement, &self.known) {
(PathRequirement::Labels(set), Some(known)) => {
PathRequirement::labels(set.as_arc().iter().filter(|l| known.contains(l)).cloned())
}
(requirement, _) => requirement,
}
}
fn note_effects<'g, S: GraphStorage + 'g>(&self, step: &GraphStep<'g, S>) {
self.effects.set(self.effects.get().or(step.own_effects()));
}
pub(crate) fn take_effects(&self) -> PlanEffects {
self.effects.replace(PlanEffects::default())
}
pub(crate) fn restore_effects(&self, saved: PlanEffects, own: PlanEffects) {
self.effects.set(saved.or(own));
}
pub(crate) fn analyze_level<'g, S: GraphStorage + 'g>(
&self,
steps: &mut [GraphStep<'g, S>],
downstream: PathRequirement,
) -> (Vec<PathMode>, PathRequirement) {
let mut modes = vec![PathMode::None; steps.len()];
let mut need = if self.force_full {
PathRequirement::Full
} else {
downstream
};
let mut next_is_count = false;
let mut own_rejected_modulator: Option<(usize, crate::error::BoxedTraverserError)> = None;
for index in (0..steps.len()).rev() {
let step = &mut steps[index];
if matches!(step, GraphStep::Modulator(_, _)) {
if let Err(err) = step.modulator_check() {
let replace = match &own_rejected_modulator {
None => true,
Some((existing_index, _)) => index < *existing_index,
};
if replace {
own_rejected_modulator = Some((index, err));
}
}
continue;
}
if let Err(err) = step.modulator_check() {
let replace = match &own_rejected_modulator {
None => true,
Some((existing_index, _)) => index < *existing_index,
};
if replace {
own_rejected_modulator = Some((index, err));
}
}
self.note_effects(step);
let children = step.analyze_nested_paths(self, &need);
let is_count = matches!(step, GraphStep::Count(_, _));
if let GraphStep::As(_, as_step) = step {
let labels: &[CompactString] = as_step.step_labels().map_or(&[], |l| &l[..]);
self.produced.borrow_mut().extend(labels.iter().cloned());
}
if let Some((labels, step_name)) = step.required_labels() {
let mut required = self.required.borrow_mut();
for label in labels {
required.push((label, step_name));
}
}
if let Some(key) = step.declares_side_effect_key() {
self.produced.borrow_mut().push(CompactString::from(key));
}
if MAP_PRODUCING_KINDS.contains(&step.kind_name()) {
self.map_producing_seen.set(true);
}
if self.force_full {
modes[index] = PathMode::Full;
} else if let GraphStep::As(_, as_step) = step {
let labels: &[CompactString] = as_step.step_labels().map_or(&[], |l| &l[..]);
modes[index] = need.clone();
need = need.retract(labels).union(children);
} else {
modes[index] = need.clone();
let skipped = next_is_count && step.skipped_before_count();
let own = if skipped {
PathRequirement::None
} else {
self.restrict(step.path_requirement()).union(children)
};
need = if step.resets_path() {
own
} else {
need.union(own)
};
}
next_is_count = is_count;
}
if let Some((_, err)) = own_rejected_modulator {
let mut cell = self.rejected_modulator.borrow_mut();
if cell.is_none() {
*cell = Some(err);
}
}
(modes, need)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn label_sets_are_sorted_and_deduplicated() {
let set = SmallLabelSet::new(["b", "a", "b"]);
assert_eq!(
&set.as_arc()[..],
&[CompactString::new("a"), CompactString::new("b")]
);
assert!(set.contains("a"));
assert!(!set.contains("c"));
}
#[test]
fn union_and_retraction() {
let a = PathRequirement::labels(["a"]);
let b = PathRequirement::labels(["b"]);
let ab = a.clone().union(b);
assert_eq!(ab, PathRequirement::labels(["a", "b"]));
assert_eq!(
ab.clone().union(PathRequirement::Full),
PathRequirement::Full
);
assert_eq!(PathRequirement::None.union(a.clone()), a);
assert_eq!(ab.retract(&[CompactString::new("b")]), a);
assert_eq!(a.retract(&[CompactString::new("a")]), PathRequirement::None);
assert_eq!(
PathRequirement::Full.retract(&[CompactString::new("a")]),
PathRequirement::Full
);
assert_eq!(
PathRequirement::labels(Vec::<String>::new()),
PathRequirement::None
);
}
#[test]
fn display() {
assert_eq!(PathRequirement::Full.to_string(), "full");
assert_eq!(
PathRequirement::labels(["b", "a"]).to_string(),
"labels(a,b)"
);
assert_eq!(PathRequirement::None.to_string(), "none");
}
fn effects_of(
build: impl for<'a> FnOnce(
crate::GraphTraversalSource<'a, &'a crate::TraversalGraph>,
)
-> crate::GraphTraversalSource<'a, &'a crate::TraversalGraph>,
) -> PlanEffects {
let graph = crate::TraversalGraph::tinkerpop_modern();
let g = crate::GraphTraversalSource::new(&graph);
let mut steps = build(g).steps;
PathAnalysis::analyze_query(true, &mut steps, &std::collections::HashMap::new())
.unwrap()
.1
}
#[test]
fn plan_effects_are_collected_from_every_level() {
let none = PlanEffects::default();
assert_eq!(effects_of(|g| g.v(None).out(None).count()), none);
let writes = PlanEffects {
mutates: false,
writes_side_effects: true,
};
assert_eq!(effects_of(|g| g.v(None).aggregate("x")), writes);
assert_eq!(effects_of(|g| g.v(None).store("x")), writes);
assert_eq!(effects_of(|g| g.v(None).subgraph("s")), writes);
assert_eq!(
effects_of(|g| g.v(None).side_effect(Box::new(|_, _| {}))),
writes
);
let mutates = PlanEffects {
mutates: true,
writes_side_effects: false,
};
assert_eq!(effects_of(|g| g.add_v("x")), mutates);
assert_eq!(effects_of(|g| g.v(None).drop()), mutates);
assert_eq!(
effects_of(|g| g.v(None).union(vec![crate::__::add_v("y")])),
mutates
);
assert_eq!(
effects_of(|g| g.v(None).where_t(crate::__::out(None).aggregate("x"))),
writes
);
}
}