use crate::graph::GraphStorage;
use crate::traversal::step::local_scope::{LocalSlice, slice_local};
use crate::value::Scope;
use crate::*;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum RangeMode {
Limit(i64),
Skip(i64),
Tail(i64),
Range { low: i64, high: i64 },
}
impl RangeMode {
fn name(self) -> &'static str {
match self {
RangeMode::Limit(_) => "limit",
RangeMode::Skip(_) => "skip",
RangeMode::Tail(_) => "tail",
RangeMode::Range { .. } => "range",
}
}
fn validate(self) -> TraverserResult<()> {
let invalid = |low: i64, high: i64, reason: String| {
Err(Box::new(TraverserError::InvalidRange { low, high, reason }))
};
match self {
RangeMode::Limit(n) if n < 0 => invalid(
0,
n,
format!("limit(local, {n}) needs a count of zero or more"),
),
RangeMode::Skip(n) if n < 0 => invalid(
n,
-1,
format!("skip(local, {n}) needs a count of zero or more"),
),
RangeMode::Tail(n) if n < 0 => invalid(
n,
-1,
format!("tail(local, {n}) needs a count of zero or more"),
),
RangeMode::Range { low, high } if low < 0 => invalid(
low,
high,
"low must be zero or a positive integer".to_string(),
),
RangeMode::Range { low, high } if high < -1 => invalid(
low,
high,
"high must be -1 (unbounded) or zero or a positive integer".to_string(),
),
RangeMode::Range { low, high } if high != -1 && high < low => {
invalid(low, high, "high must be at least low".to_string())
}
RangeMode::Limit(_)
| RangeMode::Skip(_)
| RangeMode::Tail(_)
| RangeMode::Range { .. } => Ok(()),
}
}
fn bounds(self, len: usize) -> (usize, usize) {
let clamp = |n: i64| usize::try_from(n).map_or(0, |n| n.min(len));
match self {
RangeMode::Limit(n) => (0, clamp(n)),
RangeMode::Skip(n) => (clamp(n), len),
RangeMode::Tail(n) => (len - clamp(n), len),
RangeMode::Range { low, high } => {
let start = clamp(low);
let end = if high == -1 { len } else { clamp(high) };
(start, end.max(start))
}
}
}
}
#[derive(Clone)]
struct StepRangeLocal {
mode: RangeMode,
}
impl StepRangeLocal {
fn step<'g, S: GraphStorage + 'g>(mode: RangeMode) -> GraphStep<'g, S> {
GraphStep::Map(mode.name(), Box::new(StepRangeLocal { mode }))
}
}
impl fmt::Display for StepRangeLocal {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.mode {
RangeMode::Limit(n) => write!(f, "limit(Scope.local, {n})"),
RangeMode::Skip(n) => write!(f, "skip(Scope.local, {n})"),
RangeMode::Tail(n) => write!(f, "tail(Scope.local, {n})"),
RangeMode::Range { low, high } => write!(f, "range(Scope.local, {low}, {high})"),
}
}
}
impl<'g, S: GraphStorage + 'g> GraphTraversalSource<'g, S> {
pub fn limit_scoped(self, scope: Scope, n: i64) -> Self {
match scope {
Scope::Global => match usize::try_from(n) {
Ok(n) => self.limit(n),
Err(_) => self.range(0, n),
},
Scope::Local => self.push_step(StepRangeLocal::step(RangeMode::Limit(n))),
}
}
pub fn skip_scoped(self, scope: Scope, n: i64) -> Self {
match scope {
Scope::Global => match usize::try_from(n) {
Ok(n) => self.skip(n),
Err(_) => self.range(n, -1),
},
Scope::Local => self.push_step(StepRangeLocal::step(RangeMode::Skip(n))),
}
}
pub fn tail_scoped(self, scope: Scope, n: i64) -> Self {
match scope {
Scope::Global => match usize::try_from(n) {
Ok(n) => self.tail(n),
Err(_) => self.range(n, -1),
},
Scope::Local => self.push_step(StepRangeLocal::step(RangeMode::Tail(n))),
}
}
pub fn range_scoped(self, scope: Scope, low: i64, high: i64) -> Self {
match scope {
Scope::Global => self.range(low, high),
Scope::Local => self.push_step(StepRangeLocal::step(RangeMode::Range { low, high })),
}
}
}
impl __ {
pub fn limit_scoped<'g, S: GraphStorage + 'g>(
scope: Scope,
n: i64,
) -> AnonymousTraversal<'g, S> {
AnonymousTraversal::new().limit_scoped(scope, n)
}
pub fn skip_scoped<'g, S: GraphStorage + 'g>(
scope: Scope,
n: i64,
) -> AnonymousTraversal<'g, S> {
AnonymousTraversal::new().skip_scoped(scope, n)
}
pub fn tail_scoped<'g, S: GraphStorage + 'g>(
scope: Scope,
n: i64,
) -> AnonymousTraversal<'g, S> {
AnonymousTraversal::new().tail_scoped(scope, n)
}
pub fn range_scoped<'g, S: GraphStorage + 'g>(
scope: Scope,
low: i64,
high: i64,
) -> AnonymousTraversal<'g, S> {
AnonymousTraversal::new().range_scoped(scope, low, high)
}
}
impl<'g, S: GraphStorage + 'g> AnonymousTraversal<'g, S> {
pub fn limit_scoped(self, scope: Scope, n: i64) -> Self {
match scope {
Scope::Global => match usize::try_from(n) {
Ok(n) => self.limit(n),
Err(_) => self.range(0, n),
},
Scope::Local => self.push_step(StepRangeLocal::step(RangeMode::Limit(n))),
}
}
pub fn skip_scoped(self, scope: Scope, n: i64) -> Self {
match scope {
Scope::Global => match usize::try_from(n) {
Ok(n) => self.skip(n),
Err(_) => self.range(n, -1),
},
Scope::Local => self.push_step(StepRangeLocal::step(RangeMode::Skip(n))),
}
}
pub fn tail_scoped(self, scope: Scope, n: i64) -> Self {
match scope {
Scope::Global => match usize::try_from(n) {
Ok(n) => self.tail(n),
Err(_) => self.range(n, -1),
},
Scope::Local => self.push_step(StepRangeLocal::step(RangeMode::Tail(n))),
}
}
pub fn range_scoped(self, scope: Scope, low: i64, high: i64) -> Self {
match scope {
Scope::Global => self.range(low, high),
Scope::Local => self.push_step(StepRangeLocal::step(RangeMode::Range { low, high })),
}
}
}
impl<'g, S: GraphStorage + 'g> GraphStepMap<'g, S> for StepRangeLocal {
#[inline]
fn process(
&self,
context: &mut TraverserContext<'_, S>,
source: Traverser<S>,
) -> TraverserResult<Traverser<S>> {
self.mode.validate()?;
let mode = self.mode;
match slice_local(context, source.value, |len| mode.bounds(len)) {
LocalSlice::Value(value) => Ok(Traverser::with_parts(
value,
source.path,
source.bulk,
source.sack,
)),
LocalSlice::Path => Err(Box::new(TraverserError::not_implemented(match mode {
RangeMode::Limit(_) => "limit(local) on a path",
RangeMode::Skip(_) => "skip(local) on a path",
RangeMode::Tail(_) => "tail(local) on a path",
RangeMode::Range { .. } => "range(local) on a path",
}))),
}
}
fn materializes(&self) -> bool {
true
}
fn clone_box(&self) -> Box<dyn GraphStepMap<'g, S> + 'g> {
Box::new(self.clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn names(values: &[&str]) -> ElementProperty {
ElementProperty::Array(values.iter().map(|v| ElementProperty::from(*v)).collect())
}
#[test]
fn limit_list_stays_list() {
test_dual!(
graph: GraphSource::tinkerpop_modern(),
rust: |g| g.v(None).values("name").fold().limit_scoped(Scope::Local, 1).to_list(),
rhai: [
r#"g.V().values("name").fold().limit(Scope.local, 1)"#,
r#"g.v().values("name").fold().limit(Scope::local, 1)"#,
],
assert: |res| assert_eq!(res, vec![names(&["marko"])]),
);
}
#[test]
fn skip_list() {
test_dual!(
graph: GraphSource::tinkerpop_modern(),
rust: |g| g.v(None).values("name").fold().skip_scoped(Scope::Local, 4).to_list(),
rhai: [
r#"g.V().values("name").fold().skip(Scope.local, 4)"#,
r#"g.v().values("name").fold().skip(Scope::Local, 4)"#,
],
assert: |res| assert_eq!(res, vec![names(&["ripple", "peter"])]),
);
}
#[test]
fn tail_list() {
test_dual!(
graph: GraphSource::tinkerpop_modern(),
rust: |g| g.v(None).values("name").fold().tail_scoped(Scope::Local, 2).to_list(),
rhai: [
r#"g.V().values("name").fold().tail(Scope.local, 2)"#,
r#"g.v().values("name").fold().tail(Scope::local, 2)"#,
],
assert: |res| assert_eq!(res, vec![names(&["ripple", "peter"])]),
);
}
#[test]
fn range_list_out_of_bounds_is_empty() {
test_dual!(
graph: GraphSource::tinkerpop_modern(),
rust: |g| g.v(None).fold().range_scoped(Scope::Local, 6, 7).to_list(),
rhai: [
r#"g.V().fold().range(Scope.local, 6, 7)"#,
r#"g.v().fold().range(Scope::Local, 6, 7)"#,
],
assert: |res| assert_eq!(res, vec![ElementProperty::Array(vec![])]),
);
}
#[test]
fn range_scalar_passes_through() {
test_dual!(
graph: GraphSource::tinkerpop_modern(),
rust: |g| g.v("1").values("age").range_scoped(Scope::Local, 20, 30).to_list(),
rhai: [
r#"g.V("1").values("age").range(Scope.local, 20, 30)"#,
r#"g.v("1").values("age").range(Scope::local, 20, 30)"#,
],
assert: |res| assert_eq!(res, vec![ElementProperty::from(29i64)]),
);
}
#[test]
fn bounds_of_every_mode() {
assert_eq!(RangeMode::Limit(2).bounds(5), (0, 2));
assert_eq!(RangeMode::Limit(9).bounds(5), (0, 5));
assert_eq!(RangeMode::Skip(2).bounds(5), (2, 5));
assert_eq!(RangeMode::Skip(9).bounds(5), (5, 5));
assert_eq!(RangeMode::Tail(2).bounds(5), (3, 5));
assert_eq!(RangeMode::Tail(9).bounds(5), (0, 5));
assert_eq!(RangeMode::Range { low: 1, high: 3 }.bounds(5), (1, 3));
assert_eq!(RangeMode::Range { low: 1, high: -1 }.bounds(5), (1, 5));
assert_eq!(RangeMode::Range { low: 6, high: 7 }.bounds(5), (5, 5));
assert!(RangeMode::Limit(-1).validate().is_err());
assert!(RangeMode::Skip(-1).validate().is_err());
assert!(RangeMode::Tail(-1).validate().is_err());
assert!(RangeMode::Range { low: 3, high: 1 }.validate().is_err());
assert!(RangeMode::Range { low: 0, high: -2 }.validate().is_err());
assert!(RangeMode::Range { low: -1, high: 2 }.validate().is_err());
assert!(RangeMode::Range { low: 0, high: -1 }.validate().is_ok());
}
#[test]
fn profile_name() {
let graph = GraphSource::tinkerpop_modern();
let g_lock = graph.read();
let g = g_lock.traversal();
let result = g
.v(None)
.fold()
.limit_scoped(Scope::Local, 2)
.profile()
.unwrap();
assert!(result.to_string().contains("limit(Scope.local, 2)"));
}
#[test]
fn global_scope_with_a_negative_count() {
test_dual!(
graph: GraphSource::tinkerpop_modern(),
rust: |g| g.v(None).limit_scoped(Scope::Global, -1).count().to_list(),
rhai: [
r#"g.V().limit(Scope.global, -1).count()"#,
r#"g.v().limit(Scope::global, -1).count()"#,
],
assert: |res| assert_eq!(res[0].as_i64(), Some(6)),
);
test_dual_err!(
graph: GraphSource::tinkerpop_modern(),
rust: |g| g.v(None).limit_scoped(Scope::Global, -2).to_list(),
rhai: [r#"g.V().limit(Scope.global, -2)"#],
assert: |err| assert!(err.contains("Invalid range [0, -2)"), "{err}"),
);
test_dual_err!(
graph: GraphSource::tinkerpop_modern(),
rust: |g| g.v(None).skip_scoped(Scope::Global, -1).to_list(),
rhai: [r#"g.V().skip(Scope.global, -1)"#],
assert: |err| assert!(err.contains("low must be zero or a positive integer"), "{err}"),
);
test_dual_err!(
graph: GraphSource::tinkerpop_modern(),
rust: |g| g.v(None).tail_scoped(Scope::Global, -1).to_list(),
rhai: [r#"g.V().tail(Scope.global, -1)"#],
assert: |err| assert!(err.contains("low must be zero or a positive integer"), "{err}"),
);
}
#[test]
fn anonymous_scoped_forms_in_child_traversals() {
test_dual!(
graph: GraphSource::tinkerpop_modern(),
rust: |g| g
.v(None)
.local(__::out(None).limit_scoped(Scope::Global, 1))
.count()
.to_list(),
rhai: [r#"g.V().local(__.out().limit(Scope.global, 1)).count()"#],
assert: |res| assert_eq!(res[0].as_i64(), Some(3)),
);
test_dual!(
graph: GraphSource::tinkerpop_modern(),
rust: |g| g
.v(None)
.values("name")
.fold()
.local(__::tail_scoped(Scope::Local, 2))
.to_list(),
rhai: [r#"g.V().values("name").fold().local(__.tail(Scope.local, 2))"#],
assert: |res| assert_eq!(res, &[names(&["ripple", "peter"])]),
);
test_dual!(
graph: GraphSource::tinkerpop_modern(),
rust: |g| g
.v(None)
.values("name")
.fold()
.local(__::skip_scoped(Scope::Local, 4))
.to_list(),
rhai: [r#"g.V().values("name").fold().local(__.skip(Scope.local, 4))"#],
assert: |res| assert_eq!(res, &[names(&["ripple", "peter"])]),
);
test_dual!(
graph: GraphSource::tinkerpop_modern(),
rust: |g| g
.v(None)
.values("name")
.fold()
.local(__::range_scoped(Scope::Local, 1, 3))
.to_list(),
rhai: [r#"g.V().values("name").fold().local(__.range(Scope.local, 1, 3))"#],
assert: |res| assert_eq!(res, &[names(&["vadas", "lop"])]),
);
test_dual!(
graph: GraphSource::tinkerpop_modern(),
rust: |g| g
.v(None)
.local(__::out(None).skip_scoped(Scope::Global, 2))
.count()
.to_list(),
rhai: [r#"g.V().local(__.out().skip(Scope.global, 2)).count()"#],
assert: |res| assert_eq!(res[0].as_i64(), Some(1)),
);
test_dual!(
graph: GraphSource::tinkerpop_modern(),
rust: |g| g
.v(None)
.local(__::out(None).tail_scoped(Scope::Global, 1))
.count()
.to_list(),
rhai: [r#"g.V().local(__.out().tail(Scope.global, 1)).count()"#],
assert: |res| assert_eq!(res[0].as_i64(), Some(3)),
);
test_dual!(
graph: GraphSource::tinkerpop_modern(),
rust: |g| g
.v(None)
.local(__::out(None).range_scoped(Scope::Global, 0, 1))
.count()
.to_list(),
rhai: [r#"g.V().local(__.out().range(Scope.global, 0, 1)).count()"#],
assert: |res| assert_eq!(res[0].as_i64(), Some(3)),
);
}
#[test]
fn a_path_is_not_sliced_yet() {
for (mode, rhai) in [
("limit", r#"g.V("1").out().path().limit(Scope.local, 1)"#),
("skip", r#"g.V("1").out().path().skip(Scope.local, 1)"#),
("tail", r#"g.V("1").out().path().tail(Scope.local, 1)"#),
("range", r#"g.V("1").out().path().range(Scope.local, 0, 1)"#),
] {
let rust = move |g: GraphTraversalSource<'_, &TraversalGraph>| {
let t = g.v("1").out(None).path();
match mode {
"limit" => t.limit_scoped(Scope::Local, 1),
"skip" => t.skip_scoped(Scope::Local, 1),
"tail" => t.tail_scoped(Scope::Local, 1),
_ => t.range_scoped(Scope::Local, 0, 1),
}
.to_list()
};
test_dual_err!(
graph: GraphSource::tinkerpop_modern(),
rust: rust,
rhai: [rhai],
assert: |err| assert!(err.contains(&format!("{mode}(local) on a path")), "{err}"),
);
}
}
}