use crate::graph::GraphStorage;
use crate::*;
#[derive(Clone, Debug)]
pub(crate) enum OptionToken {
Value(ElementProperty),
Predicate(P<PValue>),
#[cfg_attr(not(feature = "script"), allow(dead_code))]
Any,
#[cfg_attr(not(feature = "script"), allow(dead_code))]
None,
#[cfg_attr(not(feature = "script"), allow(dead_code))]
Unproductive,
}
impl fmt::Display for OptionToken {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
OptionToken::Value(value) => f.write_str(&crate::value::literal::rhai_literal(value)),
OptionToken::Predicate(predicate) => {
f.write_str(&crate::value::predicate::render_predicate(predicate))
}
OptionToken::Any => f.write_str("Pick.any"),
OptionToken::None => f.write_str("Pick.none"),
OptionToken::Unproductive => f.write_str("Pick.unproductive"),
}
}
}
pub(crate) enum OptionKey<'g, S: GraphStorage + 'g> {
Token(OptionToken),
Traversal(AnonymousTraversal<'g, S>),
}
impl<'g, S: GraphStorage + 'g> Clone for OptionKey<'g, S> {
fn clone(&self) -> Self {
match self {
OptionKey::Token(token) => OptionKey::Token(token.clone()),
OptionKey::Traversal(traversal) => OptionKey::Traversal(traversal.clone()),
}
}
}
impl<'g, S: GraphStorage + 'g> fmt::Display for OptionKey<'g, S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
OptionKey::Token(token) => write!(f, "{token}"),
OptionKey::Traversal(traversal) => write!(f, "{traversal}"),
}
}
}
enum Selection<S: GraphStorage> {
Unproductive,
Value(Traverser<S>),
}
struct Picked {
indices: Vec<usize>,
pass_through: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum BranchKind {
Choose,
Branch,
}
pub(crate) struct StepBranch<'g, S: GraphStorage + 'g> {
kind: BranchKind,
predicate_mode: bool,
selector: AnonymousTraversal<'g, S>,
options: Vec<(OptionKey<'g, S>, AnonymousTraversal<'g, S>)>,
locked: bool,
}
impl<'g, S: GraphStorage + 'g> Clone for StepBranch<'g, S> {
fn clone(&self) -> Self {
Self {
kind: self.kind,
predicate_mode: self.predicate_mode,
selector: self.selector.clone(),
options: self.options.clone(),
locked: self.locked,
}
}
}
impl<'g, S: GraphStorage + 'g> fmt::Display for StepBranch<'g, S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let name = match self.kind {
BranchKind::Choose => "choose",
BranchKind::Branch => "branch",
};
if self.predicate_mode {
let on_true = self.options.first().map(|(_, t)| t);
let on_false = self.options.get(1).map(|(_, t)| t);
write!(f, "{name}({}", self.selector)?;
for branch in [on_true, on_false].into_iter().flatten() {
write!(f, ", {branch}")?;
}
return f.write_str(")");
}
write!(f, "{name}({})", self.selector)?;
for (key, branch) in &self.options {
write!(f, ".option({key}, {branch})")?;
}
Ok(())
}
}
impl<'g, S: GraphStorage + 'g> StepBranch<'g, S> {
pub(crate) fn new_switch(kind: BranchKind, selector: AnonymousTraversal<'g, S>) -> Self {
Self {
kind,
predicate_mode: false,
selector,
options: Vec::new(),
locked: false,
}
}
pub(crate) fn new_if_then_else(
predicate: AnonymousTraversal<'g, S>,
on_true: AnonymousTraversal<'g, S>,
on_false: AnonymousTraversal<'g, S>,
) -> Self {
Self {
kind: BranchKind::Choose,
predicate_mode: true,
selector: predicate,
options: vec![
(
OptionKey::Token(OptionToken::Value(ElementProperty::Boolean(true))),
on_true,
),
(
OptionKey::Token(OptionToken::Value(ElementProperty::Boolean(false))),
on_false,
),
],
locked: true,
}
}
pub(crate) fn push_option(
&mut self,
key: OptionKey<'g, S>,
branch: AnonymousTraversal<'g, S>,
) -> bool {
if self.locked {
return false;
}
self.options.push((key, branch));
true
}
fn select(
&self,
context: &mut TraverserContext<'_, S>,
source: &Traverser<S>,
) -> TraverserResult<Selection<S>> {
if self.predicate_mode {
let matched = self.selector.exists(context, source)?;
return Ok(Selection::Value(Traverser::with_parts(
TraverserValue::Computed(ElementProperty::Boolean(matched), None),
source.path,
1,
source.sack,
)));
}
let mut results = self.selector.apply_to(context, source)?;
if results.is_empty() {
return Ok(Selection::Unproductive);
}
Ok(Selection::Value(results.swap_remove(0)))
}
fn run_branch(
context: &mut TraverserContext<'_, S>,
source: &Traverser<S>,
branch: &AnonymousTraversal<'g, S>,
output: &mut Vec<Traverser<S>>,
) -> TraverserResult<()> {
let results = branch.apply_to(context, source)?;
output.extend(results);
Ok(())
}
}
fn literal_matches<S: GraphStorage>(
context: &mut TraverserContext<'_, S>,
value: &TraverserValue<S>,
literal: &ElementProperty,
) -> TraverserResult<bool> {
let operand = match literal {
ElementProperty::Null => PValue::Null,
ElementProperty::String(text) => PValue::String(text.clone()),
ElementProperty::Compressed(text) => PValue::String(text.text()),
ElementProperty::Number(number) => PValue::Number(number.clone()),
ElementProperty::Boolean(flag) => PValue::Boolean(*flag),
ElementProperty::Uuid(bits) => PValue::Uuid(*bits),
ElementProperty::Object(_)
| ElementProperty::Map(_)
| ElementProperty::Array(_)
| ElementProperty::Vertex(_)
| ElementProperty::Edge(_) => {
return Ok(&value.into_node_property(context.storage)? == literal);
}
};
P::Eq(operand).test_literal(context, value)
}
impl<'g, S: GraphStorage + 'g> StepBranch<'g, S> {
fn check_options(&self) -> TraverserResult<()> {
if !matches!(self.kind, BranchKind::Choose) {
return Ok(());
}
for (key, _) in &self.options {
match key {
OptionKey::Token(OptionToken::Any) => {
return Err(Box::new(TraverserError::InvalidOption {
key: "choose().option".to_string(),
reason: "choose() picks at most one branch and does not accept Pick.any, \
branch()'s always-additional catch-all; use branch() instead, or Pick.none \
for a default branch."
.to_string(),
}));
}
OptionKey::Traversal(_) => {
return Err(Box::new(TraverserError::InvalidOption {
key: "choose().option".to_string(),
reason: "a traversal is not allowed as a choose() option key; use a \
predicate (P.eq(..), P.between(..)), a literal, or branch(), which \
accepts a traversal key."
.to_string(),
}));
}
OptionKey::Token(_) => {}
}
}
Ok(())
}
fn key_matches(
&self,
context: &mut TraverserContext<'_, S>,
source: &Traverser<S>,
selected: &Traverser<S>,
key: &OptionKey<'g, S>,
) -> TraverserResult<bool> {
match key {
OptionKey::Token(OptionToken::Value(literal)) => {
literal_matches(context, &selected.value, literal)
}
OptionKey::Token(OptionToken::Predicate(predicate)) => {
predicate.test_literal(context, &selected.value)
}
OptionKey::Token(OptionToken::Any | OptionToken::None | OptionToken::Unproductive) => {
Ok(false)
}
OptionKey::Traversal(traversal) => {
let start =
Traverser::with_parts(selected.value.clone(), source.path, 1, source.sack);
traversal.exists(context, &start)
}
}
}
fn pick(
&self,
context: &mut TraverserContext<'_, S>,
source: &Traverser<S>,
) -> TraverserResult<Picked> {
let is_choose = matches!(self.kind, BranchKind::Choose);
let mut indices = Vec::new();
let selection = self.select(context, source)?;
match &selection {
Selection::Value(selected) => {
for (idx, (key, _)) in self.options.iter().enumerate() {
if self.key_matches(context, source, selected, key)? {
indices.push(idx);
if is_choose {
break;
}
}
}
if indices.is_empty() {
self.push_fallback(&mut indices, |t| matches!(t, OptionToken::None), is_choose);
}
}
Selection::Unproductive => {
self.push_fallback(
&mut indices,
|t| matches!(t, OptionToken::Unproductive),
is_choose,
);
}
}
let pass_through = is_choose && indices.is_empty();
if !is_choose {
for (idx, (key, _)) in self.options.iter().enumerate() {
if matches!(key, OptionKey::Token(OptionToken::Any)) {
indices.push(idx);
}
}
}
Ok(Picked {
indices,
pass_through,
})
}
fn push_fallback(
&self,
indices: &mut Vec<usize>,
is_fallback: impl Fn(&OptionToken) -> bool,
is_choose: bool,
) {
for (idx, (key, _)) in self.options.iter().enumerate() {
if matches!(key, OptionKey::Token(token) if is_fallback(token)) {
indices.push(idx);
if is_choose {
return;
}
}
}
}
}
impl<'g, S: GraphStorage + 'g> GraphStepFlatMap<'g, S> for StepBranch<'g, S> {
#[inline]
fn process(
&self,
context: &mut TraverserContext<'_, S>,
source: Traverser<S>,
output: &mut Vec<Traverser<S>>,
) -> TraverserResult<()> {
self.check_options()?;
let picked = self.pick(context, &source)?;
if picked.pass_through {
output.push(source);
return Ok(());
}
for idx in picked.indices {
Self::run_branch(context, &source, &self.options[idx].1, output)?;
}
Ok(())
}
fn is_batch(&self) -> bool {
self.options.iter().any(|(_, branch)| branch.has_barrier())
}
fn process_batch(
&self,
context: &mut TraverserContext<'_, S>,
input: Vec<Traverser<S>>,
output: &mut Vec<Traverser<S>>,
) -> TraverserResult<()> {
self.check_options()?;
if input.is_empty() {
return Ok(());
}
let mut buckets: Vec<Vec<Traverser<S>>> = vec![Vec::new(); self.options.len()];
let mut passed = Vec::new();
for source in input {
let picked = self.pick(context, &source)?;
if picked.pass_through {
passed.push(source);
continue;
}
for idx in picked.indices {
buckets[idx].push(source.clone());
}
}
for (idx, bucket) in buckets.into_iter().enumerate() {
if bucket.is_empty() {
continue;
}
output.extend(self.options[idx].1.to_list(context, bucket)?);
}
output.extend(passed);
Ok(())
}
fn child_traversals_mut(&mut self) -> Vec<&mut AnonymousTraversal<'g, S>> {
let mut children = vec![&mut self.selector];
for (key, branch) in &mut self.options {
if let OptionKey::Traversal(key) = key {
children.push(key);
}
children.push(branch);
}
children
}
fn extends_path(&self) -> bool {
false
}
fn analyze_nested_paths(
&mut self,
analysis: &crate::traversal::optimizer::path_analysis::PathAnalysis,
downstream: &crate::traversal::optimizer::path_analysis::PathRequirement,
) -> crate::traversal::optimizer::path_analysis::PathRequirement {
let mut need = self.selector.analyze_paths(
analysis,
crate::traversal::optimizer::path_analysis::PathRequirement::None,
);
for (key, branch) in &mut self.options {
if let OptionKey::Traversal(key) = key {
need = need.union(key.analyze_paths(
analysis,
crate::traversal::optimizer::path_analysis::PathRequirement::None,
));
}
need = need.union(branch.analyze_paths(analysis, downstream.clone()));
}
need
}
fn as_branch_mut(&mut self) -> Option<&mut StepBranch<'g, S>> {
Some(self)
}
fn clone_box(&self) -> Box<dyn GraphStepFlatMap<'g, S> + 'g> {
Box::new(self.clone())
}
}
struct StepOptionWithoutBranch {
rendered: String,
}
impl fmt::Display for StepOptionWithoutBranch {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.rendered)
}
}
impl<'g, S: GraphStorage + 'g> GraphStepFlatMap<'g, S> for StepOptionWithoutBranch {
fn process(
&self,
_context: &mut TraverserContext<'_, S>,
_source: Traverser<S>,
_output: &mut Vec<Traverser<S>>,
) -> TraverserResult<()> {
Err(Box::new(TraverserError::OptionWithoutBranch {
modulator: self.rendered.clone(),
}))
}
fn clone_box(&self) -> Box<dyn GraphStepFlatMap<'g, S> + 'g> {
Box::new(Self {
rendered: self.rendered.clone(),
})
}
}
pub(crate) fn option_without_branch<'g, S: GraphStorage + 'g>(
rendered: String,
) -> GraphStep<'g, S> {
GraphStep::FlatMap("option", Box::new(StepOptionWithoutBranch { rendered }))
}
fn last_branch<'a, 'g, S: GraphStorage + 'g>(
steps: &'a mut [GraphStep<'g, S>],
) -> Option<&'a mut StepBranch<'g, S>> {
match steps
.iter_mut()
.rev()
.find(|s| !matches!(s, GraphStep::Modulator(_, _)))
{
Some(GraphStep::FlatMap(_, step)) => step.as_branch_mut(),
_ => None,
}
}
pub(crate) fn attach_option<'g, S: GraphStorage + 'g>(
steps: &mut Vec<GraphStep<'g, S>>,
key: OptionKey<'g, S>,
branch: AnonymousTraversal<'g, S>,
) {
let rendered = format!("option({key}, {branch})");
if let Some(step) = last_branch(steps)
&& step.push_option(key, branch)
{
return;
}
steps.push(GraphStep::FlatMap(
"option",
Box::new(StepOptionWithoutBranch { rendered }),
));
}
impl __ {
pub fn choose<'g, S: GraphStorage + 'g>(
predicate: AnonymousTraversal<'g, S>,
on_true: AnonymousTraversal<'g, S>,
on_false: AnonymousTraversal<'g, S>,
) -> AnonymousTraversal<'g, S> {
AnonymousTraversal::new().choose(predicate, on_true, on_false)
}
pub fn choose_switch<'g, S: GraphStorage + 'g>(
selector: AnonymousTraversal<'g, S>,
) -> AnonymousTraversal<'g, S> {
AnonymousTraversal::new().choose_switch(selector)
}
}
impl<'g, S: GraphStorage + 'g> GraphTraversalSource<'g, S> {
pub fn choose(
self,
predicate: AnonymousTraversal<'g, S>,
on_true: AnonymousTraversal<'g, S>,
on_false: AnonymousTraversal<'g, S>,
) -> Self {
self.push_step(GraphStep::FlatMap(
"choose",
Box::new(StepBranch::new_if_then_else(predicate, on_true, on_false)),
))
}
pub fn choose_switch(self, selector: AnonymousTraversal<'g, S>) -> Self {
self.push_step(GraphStep::FlatMap(
"choose",
Box::new(StepBranch::new_switch(BranchKind::Choose, selector)),
))
}
pub fn option(self, pick: ElementProperty, branch: AnonymousTraversal<'g, S>) -> Self {
self.option_token(OptionToken::Value(pick), branch)
}
pub(crate) fn option_token(
mut self,
token: OptionToken,
branch: AnonymousTraversal<'g, S>,
) -> Self {
attach_option(&mut self.steps, OptionKey::Token(token), branch);
self
}
pub fn option_p(self, predicate: P<PValue>, branch: AnonymousTraversal<'g, S>) -> Self {
self.option_token(OptionToken::Predicate(predicate), branch)
}
pub fn option_traversal(
mut self,
key: AnonymousTraversal<'g, S>,
branch: AnonymousTraversal<'g, S>,
) -> Self {
attach_option(&mut self.steps, OptionKey::Traversal(key), branch);
self
}
}
impl<'g, S: GraphStorage + 'g> AnonymousTraversal<'g, S> {
pub fn choose(
self,
predicate: AnonymousTraversal<'g, S>,
on_true: AnonymousTraversal<'g, S>,
on_false: AnonymousTraversal<'g, S>,
) -> Self {
let step = GraphStep::FlatMap(
"choose",
Box::new(StepBranch::new_if_then_else(predicate, on_true, on_false)),
);
self.push_step(step)
}
pub fn choose_switch(self, selector: AnonymousTraversal<'g, S>) -> Self {
let step = GraphStep::FlatMap(
"choose",
Box::new(StepBranch::new_switch(BranchKind::Choose, selector)),
);
self.push_step(step)
}
pub fn option(self, pick: ElementProperty, branch: AnonymousTraversal<'g, S>) -> Self {
self.option_token(OptionToken::Value(pick), branch)
}
pub(crate) fn option_token(
mut self,
token: OptionToken,
branch: AnonymousTraversal<'g, S>,
) -> Self {
attach_option(&mut self.steps, OptionKey::Token(token), branch);
self
}
pub fn option_p(self, predicate: P<PValue>, branch: AnonymousTraversal<'g, S>) -> Self {
self.option_token(OptionToken::Predicate(predicate), branch)
}
pub fn option_traversal(
mut self,
key: AnonymousTraversal<'g, S>,
branch: AnonymousTraversal<'g, S>,
) -> Self {
attach_option(&mut self.steps, OptionKey::Traversal(key), branch);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn choose_if_then_else() {
test_dual!(
graph: GraphSource::tinkerpop_modern(),
rust: |g| g
.v(None)
.choose(__::has_label("person"), __::out("knows"), __::out("created"))
.values("name")
.to_list(),
rhai: [
r#"g.V().choose(__.hasLabel("person"), __.out("knows"), __.out("created")).values("name")"#,
r#"g.v().choose(__.has_label("person"), __.out("knows"), __.out("created")).values("name")"#,
],
assert: |res| {
let mut names: Vec<String> = res.iter().filter_map(|v| v.as_string().map(String::from)).collect();
names.sort();
assert_eq!(names, vec!["josh", "vadas"]);
},
);
}
#[test]
fn choose_switch_with_default() {
test_dual!(
graph: GraphSource::tinkerpop_modern(),
rust: |g| g
.v(None)
.choose_switch(__::label())
.option(ElementProperty::from("person"), __::constant(ElementProperty::from("human")))
.option(ElementProperty::from("software"), __::constant(ElementProperty::from("code")))
.to_list(),
rhai: [
r#"g.V().choose(__.label()).option("person", __.constant("human")).option("software", __.constant("code"))"#,
r#"g.v().choose(__.label()).option("person", __.constant("human")).option("software", __.constant("code"))"#,
],
assert: |res| {
assert_eq!(res.len(), 6);
let humans = res.iter().filter(|v| v.as_string() == Some("human")).count();
let code = res.iter().filter(|v| v.as_string() == Some("code")).count();
assert_eq!(humans, 4);
assert_eq!(code, 2);
},
);
}
#[test]
fn choose_switch_no_match_passes_traverser_through() {
test_dual!(
graph: GraphSource::tinkerpop_modern(),
rust: |g| g
.v(None)
.choose_switch(__::label())
.option(ElementProperty::from("person"), __::constant(ElementProperty::from("human")))
.to_list(),
rhai: [
r#"g.V().choose(__.label()).option("person", __.constant("human"))"#,
r#"g.v().choose(__.label()).option("person", __.constant("human"))"#,
],
assert: |res| {
assert_eq!(res.len(), 6); },
);
}
#[test]
fn choose_first_match_wins() {
test_dual!(
graph: GraphSource::tinkerpop_modern(),
rust: |g| g
.v("1")
.choose_switch(__::label())
.option(ElementProperty::from("person"), __::out("knows"))
.option(ElementProperty::from("person"), __::out("created"))
.values("name")
.to_list(),
rhai: [
r#"g.V("1").choose(__.label()).option("person", __.out("knows")).option("person", __.out("created")).values("name")"#,
r#"g.v("1").choose(__.label()).option("person", __.out("knows")).option("person", __.out("created")).values("name")"#,
],
assert: |res| {
assert_eq!(res.len(), 2);
},
);
}
#[test]
fn profile() {
let graph = GraphSource::tinkerpop_modern();
let g_lock = graph.read();
let g = g_lock.traversal();
let result = g
.v(None)
.choose(
__::has_label("person"),
__::out("knows"),
__::out("created"),
)
.profile()
.unwrap();
assert!(result.to_string().contains("choose("));
}
#[test]
fn option_without_branch_fails_at_execution() {
let graph = GraphSource::tinkerpop_modern();
let g_lock = graph.read();
let g = g_lock.traversal();
let err = g
.v(None)
.option(ElementProperty::from("person"), __::identity())
.to_list()
.unwrap_err();
assert!(err.to_string().contains("option"));
}
}