You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tinkerpop.apache.org by xi...@apache.org on 2026/05/14 03:26:09 UTC
(tinkerpop) 02/02: Add multi-traversal within()/without() support, fix HasContainer folding to continue past traversal-bearing steps, and reject V(traversal)/E(traversal) at parse time for start steps
This is an automated email from the ASF dual-hosted git repository.
xiazcy pushed a commit to branch steps-taking-traversal-poc
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
commit 49d462b1c4c923010a916dbd3204326fadba28f4
Author: Yang Xia <xi...@gmail.com>
AuthorDate: Wed May 13 20:12:24 2026 -0700
Add multi-traversal within()/without() support, fix HasContainer folding to continue past traversal-bearing steps, and reject V(traversal)/E(traversal) at parse time for start steps
---
.../grammar/TraversalSourceSpawnMethodVisitor.java | 22 +--
.../gremlin/process/traversal/GremlinLang.java | 11 +-
.../tinkerpop/gremlin/process/traversal/P.java | 149 ++++++++++++++++++-
.../gremlin/process/traversal/PTraversalTest.java | 160 +++++++++++++++++++++
gremlin-language/src/main/antlr4/Gremlin.g4 | 2 -
.../gremlin/language/translator/translations.json | 153 ++++++++++++++++++++
.../test/features/filter/HasTraversal.feature | 121 ++++++++++++++++
.../test/features/filter/IsTraversal.feature | 16 +++
.../optimization/TinkerGraphStepStrategy.java | 9 +-
.../TinkerGraphStepStrategyTraversalTest.java | 55 ++++++-
10 files changed, 678 insertions(+), 20 deletions(-)
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalSourceSpawnMethodVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalSourceSpawnMethodVisitor.java
index bed196c29e..f2ca8b7299 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalSourceSpawnMethodVisitor.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalSourceSpawnMethodVisitor.java
@@ -99,12 +99,13 @@ public class TraversalSourceSpawnMethodVisitor extends DefaultGremlinBaseVisitor
*/
@Override
public GraphTraversal visitTraversalSourceSpawnMethod_E(final GremlinParser.TraversalSourceSpawnMethod_EContext ctx) {
- if (ctx.nestedTraversal() != null) {
- return this.traversalSource.E((Traversal<?, ?>) anonymousVisitor.visitNestedTraversal(ctx.nestedTraversal()));
- }
final Object[] args = antlr.argumentVisitor.parseObjectVarargs(ctx.genericArgumentVarargs());
- if (args.length == 1 && args[0] instanceof Traversal) {
- return this.traversalSource.E((Traversal<?, ?>) args[0]);
+ for (final Object arg : args) {
+ if (arg instanceof Traversal) {
+ throw new IllegalArgumentException(
+ "E(traversal) cannot be used as a start step because there is no Traverser context " +
+ "available to evaluate the child traversal. Use E(traversal) as a mid-traversal step instead.");
+ }
}
return this.traversalSource.E(args);
}
@@ -114,12 +115,13 @@ public class TraversalSourceSpawnMethodVisitor extends DefaultGremlinBaseVisitor
*/
@Override
public GraphTraversal visitTraversalSourceSpawnMethod_V(final GremlinParser.TraversalSourceSpawnMethod_VContext ctx) {
- if (ctx.nestedTraversal() != null) {
- return this.traversalSource.V((Traversal<?, ?>) anonymousVisitor.visitNestedTraversal(ctx.nestedTraversal()));
- }
final Object[] args = antlr.argumentVisitor.parseObjectVarargs(ctx.genericArgumentVarargs());
- if (args.length == 1 && args[0] instanceof Traversal) {
- return this.traversalSource.V((Traversal<?, ?>) args[0]);
+ for (final Object arg : args) {
+ if (arg instanceof Traversal) {
+ throw new IllegalArgumentException(
+ "V(traversal) cannot be used as a start step because there is no Traverser context " +
+ "available to evaluate the child traversal. Use V(traversal) as a mid-traversal step instead.");
+ }
}
return this.traversalSource.V(args);
}
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLang.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLang.java
index 5fd67b617e..f38bb01c4f 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLang.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLang.java
@@ -314,7 +314,16 @@ public class GremlinLang implements Cloneable, Serializable {
} else if (p.hasTraversal()) {
// Traversal-bearing predicate: serialize as P.op(traversalGremlinLang)
sb.append("P.").append(p.getPredicateName()).append("(");
- sb.append(argAsString(p.getTraversalValue()));
+ if (p.getTraversalValues() != null) {
+ // Multi-traversal predicate (within/without with multiple traversals)
+ final List<Traversal.Admin<?, ?>> traversals = p.getTraversalValues();
+ for (int i = 0; i < traversals.size(); i++) {
+ if (i > 0) sb.append(",");
+ sb.append(argAsString(traversals.get(i)));
+ }
+ } else {
+ sb.append(argAsString(p.getTraversalValue()));
+ }
} else {
sb.append("P.").append(p.getPredicateName()).append("(");
sb.append(argAsString(p.getValue()));
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/P.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/P.java
index 4c05bc0135..dbaa10f7da 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/P.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/P.java
@@ -56,6 +56,7 @@ public class P<V> implements Predicate<V>, Serializable, Cloneable {
private boolean isCollection = false;
private boolean resolvedEmpty = false;
private Traversal.Admin<?, ?> traversalValue;
+ private List<Traversal.Admin<?, ?>> traversalValues;
public P(final PBiPredicate<V, V> biPredicate, final V value) {
this.biPredicate = biPredicate;
@@ -108,6 +109,19 @@ public class P<V> implements Predicate<V>, Serializable, Cloneable {
this.traversalValue = traversalValue;
}
+ /**
+ * Constructs a {@code P} with multiple child traversals whose results are unioned at runtime against the
+ * current traverser. Only valid for collection predicates ({@link Contains#within}, {@link Contains#without}).
+ * The literals and variables are left at their defaults and will be populated when
+ * {@link #resolve(Traverser.Admin)} is called.
+ *
+ * @since 4.0.0
+ */
+ public P(final PBiPredicate<V, V> biPredicate, final List<Traversal.Admin<?, ?>> traversalValues) {
+ this.biPredicate = biPredicate;
+ this.traversalValues = traversalValues;
+ }
+
public PBiPredicate<V, V> getBiPredicate() {
return this.biPredicate;
}
@@ -191,6 +205,8 @@ public class P<V> implements Predicate<V>, Serializable, Cloneable {
result ^= this.literals.hashCode();
if (null != this.traversalValue)
result ^= this.traversalValue.hashCode();
+ if (null != this.traversalValues)
+ result ^= this.traversalValues.hashCode();
return result;
}
@@ -201,7 +217,8 @@ public class P<V> implements Predicate<V>, Serializable, Cloneable {
((P) other).getBiPredicate().equals(this.biPredicate) &&
((((P) other).variables == null && this.variables == null) || (((P) other).variables != null && ((P) other).variables.equals(this.variables))) &&
((((P) other).literals == null && this.literals == null) || (((P) other).literals != null && CollectionUtils.isEqualCollection(((P) other).literals, this.literals))) &&
- Objects.equals(((P) other).traversalValue, this.traversalValue);
+ Objects.equals(((P) other).traversalValue, this.traversalValue) &&
+ Objects.equals(((P) other).traversalValues, this.traversalValues);
}
@Override
@@ -234,6 +251,12 @@ public class P<V> implements Predicate<V>, Serializable, Cloneable {
if (this.traversalValue != null) {
clone.traversalValue = this.traversalValue.clone();
}
+ if (this.traversalValues != null) {
+ clone.traversalValues = new ArrayList<>(this.traversalValues.size());
+ for (final Traversal.Admin<?, ?> tv : this.traversalValues) {
+ clone.traversalValues.add(tv.clone());
+ }
+ }
return clone;
} catch (final CloneNotSupportedException e) {
throw new IllegalStateException(e.getMessage(), e);
@@ -262,7 +285,7 @@ public class P<V> implements Predicate<V>, Serializable, Cloneable {
* Determines if this predicate holds a child traversal whose result is resolved at runtime.
*/
public boolean hasTraversal() {
- return this.traversalValue != null;
+ return this.traversalValue != null || (this.traversalValues != null && !this.traversalValues.isEmpty());
}
/**
@@ -282,6 +305,16 @@ public class P<V> implements Predicate<V>, Serializable, Cloneable {
return this.traversalValue;
}
+ /**
+ * Gets the list of child traversal values for multi-traversal predicates (e.g., {@code within(trav1, trav2)}).
+ * Returns {@code null} when this predicate uses a single traversal or literal values.
+ *
+ * @since 4.0.0
+ */
+ public List<Traversal.Admin<?, ?>> getTraversalValues() {
+ return this.traversalValues;
+ }
+
/**
* Resolves the child traversal against the given traverser, replacing the traversal value with the
* resolved literal(s) for this test cycle. If no traversal is present, this method returns immediately.
@@ -289,9 +322,18 @@ public class P<V> implements Predicate<V>, Serializable, Cloneable {
* <p>For collection predicates ({@link Contains}), all results are collected into the literals
* collection. For all other predicates ({@link Compare}, {@link Text}, etc.), the traversal must
* produce exactly one result or an {@link IllegalArgumentException} is thrown.</p>
+ *
+ * <p>When multiple traversals are present (via {@link #traversalValues}), each traversal is evaluated
+ * independently and results are unioned into a single collection. This is only valid for collection
+ * predicates ({@link Contains#within}, {@link Contains#without}).</p>
*/
@SuppressWarnings("unchecked")
public void resolve(final Traverser.Admin<?> traverser) {
+ if (this.traversalValues != null && !this.traversalValues.isEmpty()) {
+ resolveMultipleTraversals(traverser);
+ return;
+ }
+
if (this.traversalValue == null) return;
// Use prepare + iteration directly to avoid ambiguous overload resolution of applyAll
@@ -329,6 +371,38 @@ public class P<V> implements Predicate<V>, Serializable, Cloneable {
}
}
+ /**
+ * Resolves multiple child traversals, unioning their results into a single collection.
+ * Only valid for collection predicates ({@link Contains}).
+ */
+ @SuppressWarnings("unchecked")
+ private void resolveMultipleTraversals(final Traverser.Admin<?> traverser) {
+ final List<Object> allResults = new ArrayList<>();
+
+ for (final Traversal.Admin<?, ?> tv : this.traversalValues) {
+ final Traversal.Admin<Object, Object> trav = (Traversal.Admin<Object, Object>) (Traversal.Admin) tv;
+ final Traverser.Admin<Object> split = (Traverser.Admin<Object>) traverser.split();
+ split.setSideEffects(trav.getSideEffects());
+ split.setBulk(1L);
+ trav.reset();
+ trav.addStart(split);
+
+ while (trav.hasNext()) {
+ allResults.add(trav.next());
+ }
+ }
+
+ this.resolvedEmpty = allResults.isEmpty();
+
+ if (allResults.isEmpty()) {
+ this.literals = Collections.emptyList();
+ this.isCollection = false;
+ } else {
+ this.literals = (Collection<V>) (Collection<?>) allResults;
+ this.isCollection = true;
+ }
+ }
+
//////////////// predicate traversal utilities
/**
@@ -344,6 +418,10 @@ public class P<V> implements Predicate<V>, Serializable, Cloneable {
integrateTraversals(((NotP<?>) p).negate(), parent);
} else if (p.getTraversalValue() != null) {
parent.integrateChild(p.getTraversalValue());
+ } else if (p.getTraversalValues() != null) {
+ for (final Traversal.Admin<?, ?> tv : p.getTraversalValues()) {
+ parent.integrateChild(tv);
+ }
}
}
@@ -360,6 +438,8 @@ public class P<V> implements Predicate<V>, Serializable, Cloneable {
collectTraversals(((NotP<?>) p).negate(), traversals);
} else if (p.getTraversalValue() != null) {
traversals.add(p.getTraversalValue());
+ } else if (p.getTraversalValues() != null) {
+ traversals.addAll(p.getTraversalValues());
}
}
@@ -539,6 +619,14 @@ public class P<V> implements Predicate<V>, Serializable, Cloneable {
if (values != null && values.length == 1 && values[0] instanceof Traversal) {
return P.within((Traversal<?, ?>) values[0]);
}
+ // If multiple Traversals are passed, redirect to the multi-traversal overload.
+ if (values != null && values.length > 1 && allTraversals(values)) {
+ final List<Traversal.Admin<?, ?>> traversals = new ArrayList<>(values.length);
+ for (final V v : values) {
+ traversals.add(((Traversal<?, ?>) v).asAdmin());
+ }
+ return new P(Contains.within, traversals);
+ }
final V[] v = null == values ? (V[]) new Object[] { null } : (V[]) values;
return P.within(Arrays.asList(v));
}
@@ -576,6 +664,14 @@ public class P<V> implements Predicate<V>, Serializable, Cloneable {
if (values != null && values.length == 1 && values[0] instanceof Traversal) {
return P.without((Traversal<?, ?>) values[0]);
}
+ // If multiple Traversals are passed, redirect to the multi-traversal overload.
+ if (values != null && values.length > 1 && allTraversals(values)) {
+ final List<Traversal.Admin<?, ?>> traversals = new ArrayList<>(values.length);
+ for (final V v : values) {
+ traversals.add(((Traversal<?, ?>) v).asAdmin());
+ }
+ return new P(Contains.without, traversals);
+ }
final V[] v = null == values ? (V[]) new Object[] { null } : values;
return P.without(Arrays.asList(v));
}
@@ -665,6 +761,24 @@ public class P<V> implements Predicate<V>, Serializable, Cloneable {
return new P(Contains.within, traversalValue.asAdmin());
}
+ /**
+ * Determines if a value is within the union of results from multiple child traversals resolved at runtime.
+ * Each traversal is evaluated independently and results are combined into a single collection.
+ *
+ * @since 4.0.0
+ */
+ public static <V> P<V> within(final Traversal<?, ?> first, final Traversal<?, ?> second, final Traversal<?, ?>... more) {
+ final List<Traversal.Admin<?, ?>> traversals = new ArrayList<>(2 + (more != null ? more.length : 0));
+ traversals.add(first.asAdmin());
+ traversals.add(second.asAdmin());
+ if (more != null) {
+ for (final Traversal<?, ?> tv : more) {
+ traversals.add(tv.asAdmin());
+ }
+ }
+ return new P(Contains.within, traversals);
+ }
+
/**
* Determines if a value is not within the results of a child traversal resolved at runtime.
*
@@ -674,6 +788,24 @@ public class P<V> implements Predicate<V>, Serializable, Cloneable {
return new P(Contains.without, traversalValue.asAdmin());
}
+ /**
+ * Determines if a value is not within the union of results from multiple child traversals resolved at runtime.
+ * Each traversal is evaluated independently and results are combined into a single collection.
+ *
+ * @since 4.0.0
+ */
+ public static <V> P<V> without(final Traversal<?, ?> first, final Traversal<?, ?> second, final Traversal<?, ?>... more) {
+ final List<Traversal.Admin<?, ?>> traversals = new ArrayList<>(2 + (more != null ? more.length : 0));
+ traversals.add(first.asAdmin());
+ traversals.add(second.asAdmin());
+ if (more != null) {
+ for (final Traversal<?, ?> tv : more) {
+ traversals.add(tv.asAdmin());
+ }
+ }
+ return new P(Contains.without, traversals);
+ }
+
/**
* Determines if a value is of a type denoted by {@code GType}.
*
@@ -718,4 +850,17 @@ public class P<V> implements Predicate<V>, Serializable, Cloneable {
public static <V> P<V> not(final P<V> predicate) {
return predicate.negate();
}
+
+ /**
+ * Checks if all elements in the array are {@link Traversal} instances (specifically
+ * {@link org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.DefaultGraphTraversal}).
+ */
+ private static <V> boolean allTraversals(final V[] values) {
+ for (final V v : values) {
+ if (!(v instanceof org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.DefaultGraphTraversal)) {
+ return false;
+ }
+ }
+ return true;
+ }
}
diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/PTraversalTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/PTraversalTest.java
index 0485f9e509..3e7af19d8b 100644
--- a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/PTraversalTest.java
+++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/PTraversalTest.java
@@ -215,4 +215,164 @@ public class PTraversalTest {
assertThat(p.test(99), is(false));
}
}
+
+ /**
+ * Tests for multi-traversal support in within() and without().
+ * <p>
+ * When multiple traversals are passed to within(trav1, trav2, ...), each traversal is evaluated
+ * independently and results are unioned into a single collection for the Contains test.
+ */
+ public static class MultiTraversalTest {
+
+ private Traverser.Admin<?> createTraverser(final Object value) {
+ return new B_O_Traverser<>(value, 1L);
+ }
+
+ // --- Detection ---
+
+ @Test
+ public void shouldDetectMultipleTraversalsInWithin() {
+ final P<Object> p = P.within(__.constant(1).asAdmin(), __.constant(2).asAdmin());
+ assertThat(p.hasTraversal(), is(true));
+ }
+
+ @Test
+ public void shouldDetectMultipleTraversalsInWithout() {
+ final P<Object> p = P.without(__.constant(1).asAdmin(), __.constant(2).asAdmin());
+ assertThat(p.hasTraversal(), is(true));
+ }
+
+ @Test
+ public void shouldReturnTraversalValuesListForMultiTraversal() {
+ final P<Object> p = P.within(__.constant(1).asAdmin(), __.constant(2).asAdmin());
+ assertThat(p.getTraversalValues() != null, is(true));
+ assertThat(p.getTraversalValues().size(), is(2));
+ }
+
+ @Test
+ public void shouldReturnNullTraversalValueForMultiTraversal() {
+ // Single traversalValue should be null when using multi-traversal form
+ final P<Object> p = P.within(__.constant(1).asAdmin(), __.constant(2).asAdmin());
+ assertThat(p.getTraversalValue() == null, is(true));
+ }
+
+ // --- Resolution and testing ---
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void shouldResolveMultipleTraversalsForWithin() {
+ // within(__.constant(1), __.constant(2)) should union results: [1, 2]
+ final P<Object> p = P.within(__.constant(1).asAdmin(), __.constant(2).asAdmin());
+ p.resolve(createTraverser("start"));
+ assertThat(p.test(1), is(true));
+ assertThat(p.test(2), is(true));
+ assertThat(p.test(3), is(false));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void shouldResolveMultipleTraversalsForWithout() {
+ // without(__.constant(1), __.constant(2)) should union results: [1, 2]
+ final P<Object> p = P.without(__.constant(1).asAdmin(), __.constant(2).asAdmin());
+ p.resolve(createTraverser("start"));
+ assertThat(p.test(1), is(false));
+ assertThat(p.test(2), is(false));
+ assertThat(p.test(3), is(true));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void shouldResolveMultipleTraversalsWithMultipleResultsEach() {
+ // within(__.inject(1,2), __.inject(3,4)) should union: [1, 2, 3, 4]
+ final P<Object> p = P.within(__.inject(1, 2).asAdmin(), __.inject(3, 4).asAdmin());
+ p.resolve(createTraverser("start"));
+ assertThat(p.test(1), is(true));
+ assertThat(p.test(2), is(true));
+ assertThat(p.test(3), is(true));
+ assertThat(p.test(4), is(true));
+ assertThat(p.test(5), is(false));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void shouldHandleEmptyResultFromOneTraversal() {
+ // within(__.inject(1,2), __.limit(0)) where second produces nothing
+ // Should still match on results from first traversal
+ final P<Object> p = P.within(__.inject(1, 2).asAdmin(), __.limit(0).asAdmin());
+ p.resolve(createTraverser("start"));
+ assertThat(p.isResolvedEmpty(), is(false));
+ assertThat(p.test(1), is(true));
+ assertThat(p.test(2), is(true));
+ assertThat(p.test(3), is(false));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void shouldHandleAllEmptyResults() {
+ // within(__.limit(0), __.limit(0)) where both produce nothing
+ final P<Object> p = P.within(__.limit(0).asAdmin(), __.limit(0).asAdmin());
+ p.resolve(createTraverser("start"));
+ assertThat(p.isResolvedEmpty(), is(true));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void shouldResolveThreeTraversals() {
+ // within(__.constant("a"), __.constant("b"), __.constant("c"))
+ final P<Object> p = P.within(__.constant("a").asAdmin(), __.constant("b").asAdmin(), __.constant("c").asAdmin());
+ p.resolve(createTraverser("start"));
+ assertThat(p.test("a"), is(true));
+ assertThat(p.test("b"), is(true));
+ assertThat(p.test("c"), is(true));
+ assertThat(p.test("d"), is(false));
+ }
+
+ // --- Clone independence ---
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void shouldCloneMultiTraversalPredicate() {
+ final P<Object> original = P.within(__.constant(1).asAdmin(), __.constant(2).asAdmin());
+ final P<Object> clone = original.clone();
+
+ // Clone should have independent traversal values
+ assertThat(clone.hasTraversal(), is(true));
+ assertThat(clone.getTraversalValues() != null, is(true));
+ assertThat(clone.getTraversalValues().size(), is(2));
+
+ // Modifying clone should not affect original
+ clone.resolve(createTraverser("start"));
+ assertThat(clone.test(1), is(true));
+ // Original should still be unresolved (literals empty)
+ assertThat(original.getTraversalValues().size(), is(2));
+ }
+
+ // --- Varargs detection ---
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void shouldDetectMultipleTraversalsInVarargs() {
+ // This tests the varargs path: P.within(trav1, trav2) going through within(V... values)
+ final Traversal<?, ?> trav1 = __.constant(10);
+ final Traversal<?, ?> trav2 = __.constant(20);
+ final P<Object> p = (P<Object>) P.within(trav1, trav2);
+ assertThat(p.hasTraversal(), is(true));
+ assertThat(p.getTraversalValues() != null, is(true));
+ assertThat(p.getTraversalValues().size(), is(2));
+ p.resolve(createTraverser("start"));
+ assertThat(p.test(10), is(true));
+ assertThat(p.test(20), is(true));
+ assertThat(p.test(30), is(false));
+ }
+
+ // --- collectTraversals and integrateTraversals ---
+
+ @Test
+ public void shouldCollectTraversalsFromMultiTraversalPredicate() {
+ final P<Object> p = P.within(__.constant(1).asAdmin(), __.constant(2).asAdmin(), __.constant(3).asAdmin());
+ final java.util.List<Traversal.Admin<?, ?>> collected = new java.util.ArrayList<>();
+ P.collectTraversals(p, collected);
+ assertThat(collected.size(), is(3));
+ }
+ }
}
diff --git a/gremlin-language/src/main/antlr4/Gremlin.g4 b/gremlin-language/src/main/antlr4/Gremlin.g4
index 822cb59481..b161a271d3 100644
--- a/gremlin-language/src/main/antlr4/Gremlin.g4
+++ b/gremlin-language/src/main/antlr4/Gremlin.g4
@@ -123,12 +123,10 @@ traversalSourceSpawnMethod_addV
traversalSourceSpawnMethod_E
: K_E LPAREN genericArgumentVarargs RPAREN
- | K_E LPAREN nestedTraversal RPAREN
;
traversalSourceSpawnMethod_V
: K_V LPAREN genericArgumentVarargs RPAREN
- | K_V LPAREN nestedTraversal RPAREN
;
traversalSourceSpawnMethod_inject
diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/language/translator/translations.json b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/language/translator/translations.json
index b1eac17c5a..40c36148be 100644
--- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/language/translator/translations.json
+++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/language/translator/translations.json
@@ -9500,6 +9500,142 @@
}
]
},
+ {
+ "scenario": "g_V_hasXname_withinXVXvid1X_outXknowsX_valuesXnameX_constantXpeterXXX",
+ "traversals": [
+ {
+ "original": "g.V().has(\"name\", P.within(__.V(vid1).out(\"knows\").values(\"name\"), __.constant(\"peter\")))",
+ "language": "g.V().has(\"name\", P.within(__.V(vid1).out(\"knows\").values(\"name\"), __.constant(\"peter\")))",
+ "canonical": "g.V().has(\"name\", P.within(__.V(vid1).out(\"knows\").values(\"name\"), __.constant(\"peter\")))",
+ "anonymized": "g.V().has(string0, P.within(__.V(vid1).out(string1).values(string0), __.constant(string2)))",
+ "dotnet": "g.V().Has(\"name\", P.Within(__.V(vid1).Out(\"knows\").Values<object>(\"name\"), __.Constant<object>(\"peter\")))",
+ "go": "g.V().Has(\"name\", gremlingo.P.Within(gremlingo.T__.V(vid1).Out(\"knows\").Values(\"name\"), gremlingo.T__.Constant(\"peter\")))",
+ "groovy": "g.V().has(\"name\", P.within(__.V(vid1).out(\"knows\").values(\"name\"), __.constant(\"peter\")))",
+ "java": "g.V().has(\"name\", P.within(__.V(vid1).out(\"knows\").values(\"name\"), __.constant(\"peter\")))",
+ "javascript": "g.V().has(\"name\", P.within(__.V(vid1).out(\"knows\").values(\"name\"), __.constant(\"peter\")))",
+ "python": "g.V().has('name', P.within(__.V(vid1).out('knows').values('name'), __.constant('peter')))"
+ }
+ ]
+ },
+ {
+ "scenario": "g_V_hasXname_withinXVXvid1X_valuesXnonexistentX_constantXmarkoXXX",
+ "traversals": [
+ {
+ "original": "g.V().has(\"name\", P.within(__.V(vid1).values(\"nonexistent\"), __.constant(\"marko\")))",
+ "language": "g.V().has(\"name\", P.within(__.V(vid1).values(\"nonexistent\"), __.constant(\"marko\")))",
+ "canonical": "g.V().has(\"name\", P.within(__.V(vid1).values(\"nonexistent\"), __.constant(\"marko\")))",
+ "anonymized": "g.V().has(string0, P.within(__.V(vid1).values(string1), __.constant(string2)))",
+ "dotnet": "g.V().Has(\"name\", P.Within(__.V(vid1).Values<object>(\"nonexistent\"), __.Constant<object>(\"marko\")))",
+ "go": "g.V().Has(\"name\", gremlingo.P.Within(gremlingo.T__.V(vid1).Values(\"nonexistent\"), gremlingo.T__.Constant(\"marko\")))",
+ "groovy": "g.V().has(\"name\", P.within(__.V(vid1).values(\"nonexistent\"), __.constant(\"marko\")))",
+ "java": "g.V().has(\"name\", P.within(__.V(vid1).values(\"nonexistent\"), __.constant(\"marko\")))",
+ "javascript": "g.V().has(\"name\", P.within(__.V(vid1).values(\"nonexistent\"), __.constant(\"marko\")))",
+ "python": "g.V().has('name', P.within(__.V(vid1).values('nonexistent'), __.constant('marko')))"
+ }
+ ]
+ },
+ {
+ "scenario": "g_V_hasXname_withinXVXvid1X_valuesXnonexistentX_VXvid1X_valuesXnonexistentXXX",
+ "traversals": [
+ {
+ "original": "g.V().has(\"name\", P.within(__.V(vid1).values(\"nonexistent\"), __.V(vid1).values(\"nonexistent\")))",
+ "language": "g.V().has(\"name\", P.within(__.V(vid1).values(\"nonexistent\"), __.V(vid1).values(\"nonexistent\")))",
+ "canonical": "g.V().has(\"name\", P.within(__.V(vid1).values(\"nonexistent\"), __.V(vid1).values(\"nonexistent\")))",
+ "anonymized": "g.V().has(string0, P.within(__.V(vid1).values(string1), __.V(vid1).values(string1)))",
+ "dotnet": "g.V().Has(\"name\", P.Within(__.V(vid1).Values<object>(\"nonexistent\"), __.V(vid1).Values<object>(\"nonexistent\")))",
+ "go": "g.V().Has(\"name\", gremlingo.P.Within(gremlingo.T__.V(vid1).Values(\"nonexistent\"), gremlingo.T__.V(vid1).Values(\"nonexistent\")))",
+ "groovy": "g.V().has(\"name\", P.within(__.V(vid1).values(\"nonexistent\"), __.V(vid1).values(\"nonexistent\")))",
+ "java": "g.V().has(\"name\", P.within(__.V(vid1).values(\"nonexistent\"), __.V(vid1).values(\"nonexistent\")))",
+ "javascript": "g.V().has(\"name\", P.within(__.V(vid1).values(\"nonexistent\"), __.V(vid1).values(\"nonexistent\")))",
+ "python": "g.V().has('name', P.within(__.V(vid1).values('nonexistent'), __.V(vid1).values('nonexistent')))"
+ }
+ ]
+ },
+ {
+ "scenario": "g_V_hasXname_withoutXVXvid1X_valuesXnameX_VXvid2X_valuesXnameX_VXvid3X_valuesXnameXXX",
+ "traversals": [
+ {
+ "original": "g.V().has(\"name\", P.without(__.V(vid1).values(\"name\"), __.V(vid2).values(\"name\"), __.V(vid3).values(\"name\")))",
+ "language": "g.V().has(\"name\", P.without(__.V(vid1).values(\"name\"), __.V(vid2).values(\"name\"), __.V(vid3).values(\"name\")))",
+ "canonical": "g.V().has(\"name\", P.without(__.V(vid1).values(\"name\"), __.V(vid2).values(\"name\"), __.V(vid3).values(\"name\")))",
+ "anonymized": "g.V().has(string0, P.without(__.V(vid1).values(string0), __.V(vid2).values(string0), __.V(vid3).values(string0)))",
+ "dotnet": "g.V().Has(\"name\", P.Without(__.V(vid1).Values<object>(\"name\"), __.V(vid2).Values<object>(\"name\"), __.V(vid3).Values<object>(\"name\")))",
+ "go": "g.V().Has(\"name\", gremlingo.P.Without(gremlingo.T__.V(vid1).Values(\"name\"), gremlingo.T__.V(vid2).Values(\"name\"), gremlingo.T__.V(vid3).Values(\"name\")))",
+ "groovy": "g.V().has(\"name\", P.without(__.V(vid1).values(\"name\"), __.V(vid2).values(\"name\"), __.V(vid3).values(\"name\")))",
+ "java": "g.V().has(\"name\", P.without(__.V(vid1).values(\"name\"), __.V(vid2).values(\"name\"), __.V(vid3).values(\"name\")))",
+ "javascript": "g.V().has(\"name\", P.without(__.V(vid1).values(\"name\"), __.V(vid2).values(\"name\"), __.V(vid3).values(\"name\")))",
+ "python": "g.V().has('name', P.without(__.V(vid1).values('name'), __.V(vid2).values('name'), __.V(vid3).values('name')))"
+ }
+ ]
+ },
+ {
+ "scenario": "g_V_hasXname_withinXVXvid1X_outXknowsX_valuesXnameX_VXvid3X_outXcreatedX_valuesXnameXXX",
+ "traversals": [
+ {
+ "original": "g.V().has(\"name\", P.within(__.V(vid1).out(\"knows\").values(\"name\"), __.V(vid3).out(\"created\").values(\"name\")))",
+ "language": "g.V().has(\"name\", P.within(__.V(vid1).out(\"knows\").values(\"name\"), __.V(vid3).out(\"created\").values(\"name\")))",
+ "canonical": "g.V().has(\"name\", P.within(__.V(vid1).out(\"knows\").values(\"name\"), __.V(vid3).out(\"created\").values(\"name\")))",
+ "anonymized": "g.V().has(string0, P.within(__.V(vid1).out(string1).values(string0), __.V(vid3).out(string2).values(string0)))",
+ "dotnet": "g.V().Has(\"name\", P.Within(__.V(vid1).Out(\"knows\").Values<object>(\"name\"), __.V(vid3).Out(\"created\").Values<object>(\"name\")))",
+ "go": "g.V().Has(\"name\", gremlingo.P.Within(gremlingo.T__.V(vid1).Out(\"knows\").Values(\"name\"), gremlingo.T__.V(vid3).Out(\"created\").Values(\"name\")))",
+ "groovy": "g.V().has(\"name\", P.within(__.V(vid1).out(\"knows\").values(\"name\"), __.V(vid3).out(\"created\").values(\"name\")))",
+ "java": "g.V().has(\"name\", P.within(__.V(vid1).out(\"knows\").values(\"name\"), __.V(vid3).out(\"created\").values(\"name\")))",
+ "javascript": "g.V().has(\"name\", P.within(__.V(vid1).out(\"knows\").values(\"name\"), __.V(vid3).out(\"created\").values(\"name\")))",
+ "python": "g.V().has('name', P.within(__.V(vid1).out('knows').values('name'), __.V(vid3).out('created').values('name')))"
+ }
+ ]
+ },
+ {
+ "scenario": "g_V_hasLabelXsoftwareX_hasXname_withoutXVXvid1X_outXcreatedX_valuesXnameX_VXvid3X_outXcreatedX_valuesXnameXXX",
+ "traversals": [
+ {
+ "original": "g.V().hasLabel(\"software\").has(\"name\", P.without(__.V(vid1).out(\"created\").values(\"name\"), __.V(vid3).out(\"created\").values(\"name\")))",
+ "language": "g.V().hasLabel(\"software\").has(\"name\", P.without(__.V(vid1).out(\"created\").values(\"name\"), __.V(vid3).out(\"created\").values(\"name\")))",
+ "canonical": "g.V().hasLabel(\"software\").has(\"name\", P.without(__.V(vid1).out(\"created\").values(\"name\"), __.V(vid3).out(\"created\").values(\"name\")))",
+ "anonymized": "g.V().hasLabel(string0).has(string1, P.without(__.V(vid1).out(string2).values(string1), __.V(vid3).out(string2).values(string1)))",
+ "dotnet": "g.V().HasLabel(\"software\").Has(\"name\", P.Without(__.V(vid1).Out(\"created\").Values<object>(\"name\"), __.V(vid3).Out(\"created\").Values<object>(\"name\")))",
+ "go": "g.V().HasLabel(\"software\").Has(\"name\", gremlingo.P.Without(gremlingo.T__.V(vid1).Out(\"created\").Values(\"name\"), gremlingo.T__.V(vid3).Out(\"created\").Values(\"name\")))",
+ "groovy": "g.V().hasLabel(\"software\").has(\"name\", P.without(__.V(vid1).out(\"created\").values(\"name\"), __.V(vid3).out(\"created\").values(\"name\")))",
+ "java": "g.V().hasLabel(\"software\").has(\"name\", P.without(__.V(vid1).out(\"created\").values(\"name\"), __.V(vid3).out(\"created\").values(\"name\")))",
+ "javascript": "g.V().hasLabel(\"software\").has(\"name\", P.without(__.V(vid1).out(\"created\").values(\"name\"), __.V(vid3).out(\"created\").values(\"name\")))",
+ "python": "g.V().has_label('software').has('name', P.without(__.V(vid1).out('created').values('name'), __.V(vid3).out('created').values('name')))"
+ }
+ ]
+ },
+ {
+ "scenario": "g_V_hasLabelXpersonX_valuesXageX_isXwithinXVXvid1X_valuesXageX_V_hasXname_lopX_inXcreatedX_valuesXageXXX",
+ "traversals": [
+ {
+ "original": "g.V().hasLabel(\"person\").values(\"age\").is(P.within(__.V(vid1).values(\"age\"), __.V().has(\"name\",\"lop\").in(\"created\").values(\"age\")))",
+ "language": "g.V().hasLabel(\"person\").values(\"age\").is(P.within(__.V(vid1).values(\"age\"), __.V().has(\"name\", \"lop\").in(\"created\").values(\"age\")))",
+ "canonical": "g.V().hasLabel(\"person\").values(\"age\").is(P.within(__.V(vid1).values(\"age\"), __.V().has(\"name\", \"lop\").in(\"created\").values(\"age\")))",
+ "anonymized": "g.V().hasLabel(string0).values(string1).is(P.within(__.V(vid1).values(string1), __.V().has(string2, string3).in(string4).values(string1)))",
+ "dotnet": "g.V().HasLabel(\"person\").Values<object>(\"age\").Is(P.Within(__.V(vid1).Values<object>(\"age\"), __.V().Has(\"name\", \"lop\").In(\"created\").Values<object>(\"age\")))",
+ "go": "g.V().HasLabel(\"person\").Values(\"age\").Is(gremlingo.P.Within(gremlingo.T__.V(vid1).Values(\"age\"), gremlingo.T__.V().Has(\"name\", \"lop\").In(\"created\").Values(\"age\")))",
+ "groovy": "g.V().hasLabel(\"person\").values(\"age\").is(P.within(__.V(vid1).values(\"age\"), __.V().has(\"name\", \"lop\").in(\"created\").values(\"age\")))",
+ "java": "g.V().hasLabel(\"person\").values(\"age\").is(P.within(__.V(vid1).values(\"age\"), __.V().has(\"name\", \"lop\").in(\"created\").values(\"age\")))",
+ "javascript": "g.V().hasLabel(\"person\").values(\"age\").is(P.within(__.V(vid1).values(\"age\"), __.V().has(\"name\", \"lop\").in_(\"created\").values(\"age\")))",
+ "python": "g.V().has_label('person').values('age').is_(P.within(__.V(vid1).values('age'), __.V().has('name', 'lop').in_('created').values('age')))"
+ }
+ ]
+ },
+ {
+ "scenario": "g_VXvid1X_outEXknowsX_filterXinV_hasXname_withinXV_hasXname_lopX_inXcreatedX_valuesXnameX_V_hasXname_rippleX_inXcreatedX_valuesXnameXXXX",
+ "traversals": [
+ {
+ "original": "g.V(vid1).outE(\"knows\").filter(__.inV().has(\"name\", P.within(__.V().has(\"name\",\"lop\").in(\"created\").values(\"name\"), __.V().has(\"name\",\"ripple\").in(\"created\").values(\"name\"))))",
+ "language": "g.V(vid1).outE(\"knows\").filter(__.inV().has(\"name\", P.within(__.V().has(\"name\", \"lop\").in(\"created\").values(\"name\"), __.V().has(\"name\", \"ripple\").in(\"created\").values(\"name\"))))",
+ "canonical": "g.V(vid1).outE(\"knows\").filter(__.inV().has(\"name\", P.within(__.V().has(\"name\", \"lop\").in(\"created\").values(\"name\"), __.V().has(\"name\", \"ripple\").in(\"created\").values(\"name\"))))",
+ "anonymized": "g.V(vid1).outE(string0).filter(__.inV().has(string1, P.within(__.V().has(string1, string2).in(string3).values(string1), __.V().has(string1, string4).in(string3).values(string1))))",
+ "dotnet": "g.V(vid1).OutE(\"knows\").Filter(__.InV().Has(\"name\", P.Within(__.V().Has(\"name\", \"lop\").In(\"created\").Values<object>(\"name\"), __.V().Has(\"name\", \"ripple\").In(\"created\").Values<object>(\"name\"))))",
+ "go": "g.V(vid1).OutE(\"knows\").Filter(gremlingo.T__.InV().Has(\"name\", gremlingo.P.Within(gremlingo.T__.V().Has(\"name\", \"lop\").In(\"created\").Values(\"name\"), gremlingo.T__.V().Has(\"name\", \"ripple\").In(\"created\").Values(\"name\"))))",
+ "groovy": "g.V(vid1).outE(\"knows\").filter(__.inV().has(\"name\", P.within(__.V().has(\"name\", \"lop\").in(\"created\").values(\"name\"), __.V().has(\"name\", \"ripple\").in(\"created\").values(\"name\"))))",
+ "java": "g.V(vid1).outE(\"knows\").filter(__.inV().has(\"name\", P.within(__.V().has(\"name\", \"lop\").in(\"created\").values(\"name\"), __.V().has(\"name\", \"ripple\").in(\"created\").values(\"name\"))))",
+ "javascript": "g.V(vid1).outE(\"knows\").filter(__.inV().has(\"name\", P.within(__.V().has(\"name\", \"lop\").in_(\"created\").values(\"name\"), __.V().has(\"name\", \"ripple\").in_(\"created\").values(\"name\"))))",
+ "python": "g.V(vid1).out_e('knows').filter_(__.in_v().has('name', P.within(__.V().has('name', 'lop').in_('created').values('name'), __.V().has('name', 'ripple').in_('created').values('name'))))"
+ }
+ ]
+ },
{
"scenario": "g_V_properties_hasValueXnullX",
"traversals": [
@@ -10010,6 +10146,23 @@
}
]
},
+ {
+ "scenario": "g_V_valuesXageX_isXwithoutXVXvid1X_valuesXageX_VXvid2X_valuesXageXXX",
+ "traversals": [
+ {
+ "original": "g.V().values(\"age\").is(P.without(__.V(vid1).values(\"age\"), __.V(vid2).values(\"age\")))",
+ "language": "g.V().values(\"age\").is(P.without(__.V(vid1).values(\"age\"), __.V(vid2).values(\"age\")))",
+ "canonical": "g.V().values(\"age\").is(P.without(__.V(vid1).values(\"age\"), __.V(vid2).values(\"age\")))",
+ "anonymized": "g.V().values(string0).is(P.without(__.V(vid1).values(string0), __.V(vid2).values(string0)))",
+ "dotnet": "g.V().Values<object>(\"age\").Is(P.Without(__.V(vid1).Values<object>(\"age\"), __.V(vid2).Values<object>(\"age\")))",
+ "go": "g.V().Values(\"age\").Is(gremlingo.P.Without(gremlingo.T__.V(vid1).Values(\"age\"), gremlingo.T__.V(vid2).Values(\"age\")))",
+ "groovy": "g.V().values(\"age\").is(P.without(__.V(vid1).values(\"age\"), __.V(vid2).values(\"age\")))",
+ "java": "g.V().values(\"age\").is(P.without(__.V(vid1).values(\"age\"), __.V(vid2).values(\"age\")))",
+ "javascript": "g.V().values(\"age\").is(P.without(__.V(vid1).values(\"age\"), __.V(vid2).values(\"age\")))",
+ "python": "g.V().values('age').is_(P.without(__.V(vid1).values('age'), __.V(vid2).values('age')))"
+ }
+ ]
+ },
{
"scenario": "g_V_valuesXageX_noneXgtX32XX",
"traversals": [
diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/filter/HasTraversal.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/filter/HasTraversal.feature
index 4d38167e4d..a0fb83ae23 100644
--- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/filter/HasTraversal.feature
+++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/filter/HasTraversal.feature
@@ -201,3 +201,124 @@ Feature: Step - has() with traversal arguments
Then the result should be unordered
| result |
| d[29].i |
+
+ # Multi-traversal within() where one traversal produces multiple results
+ @GraphComputerVerificationMidVNotSupported
+ Scenario: g_V_hasXname_withinXVXvid1X_outXknowsX_valuesXnameX_constantXpeterXXX
+ Given the modern graph
+ And using the parameter vid1 defined as "v[marko].id"
+ And the traversal of
+ """
+ g.V().has("name", P.within(__.V(vid1).out("knows").values("name"), __.constant("peter")))
+ """
+ When iterated to list
+ Then the result should be unordered
+ | result |
+ | v[vadas] |
+ | v[josh] |
+ | v[peter] |
+
+ # Multi-traversal within() where one traversal produces no results — still matches on the other
+ @GraphComputerVerificationMidVNotSupported
+ Scenario: g_V_hasXname_withinXVXvid1X_valuesXnonexistentX_constantXmarkoXXX
+ Given the modern graph
+ And using the parameter vid1 defined as "v[marko].id"
+ And the traversal of
+ """
+ g.V().has("name", P.within(__.V(vid1).values("nonexistent"), __.constant("marko")))
+ """
+ When iterated to list
+ Then the result should be unordered
+ | result |
+ | v[marko] |
+
+ # Multi-traversal within() where all traversals produce no results — filters everything
+ @GraphComputerVerificationMidVNotSupported
+ Scenario: g_V_hasXname_withinXVXvid1X_valuesXnonexistentX_VXvid1X_valuesXnonexistentXXX
+ Given the modern graph
+ And using the parameter vid1 defined as "v[marko].id"
+ And the traversal of
+ """
+ g.V().has("name", P.within(__.V(vid1).values("nonexistent"), __.V(vid1).values("nonexistent")))
+ """
+ When iterated to list
+ Then the result should be empty
+
+ # Multi-traversal without() with three traversals
+ @GraphComputerVerificationMidVNotSupported
+ Scenario: g_V_hasXname_withoutXVXvid1X_valuesXnameX_VXvid2X_valuesXnameX_VXvid3X_valuesXnameXXX
+ Given the modern graph
+ And using the parameter vid1 defined as "v[marko].id"
+ And using the parameter vid2 defined as "v[vadas].id"
+ And using the parameter vid3 defined as "v[peter].id"
+ And the traversal of
+ """
+ g.V().has("name", P.without(__.V(vid1).values("name"), __.V(vid2).values("name"), __.V(vid3).values("name")))
+ """
+ When iterated to list
+ Then the result should be unordered
+ | result |
+ | v[josh] |
+ | v[lop] |
+ | v[ripple] |
+
+ # Multi-traversal within() — union of relationship traversals from different sources
+ @GraphComputerVerificationMidVNotSupported
+ Scenario: g_V_hasXname_withinXVXvid1X_outXknowsX_valuesXnameX_VXvid3X_outXcreatedX_valuesXnameXXX
+ Given the modern graph
+ And using the parameter vid1 defined as "v[marko].id"
+ And using the parameter vid3 defined as "v[josh].id"
+ And the traversal of
+ """
+ g.V().has("name", P.within(__.V(vid1).out("knows").values("name"), __.V(vid3).out("created").values("name")))
+ """
+ When iterated to list
+ Then the result should be unordered
+ | result |
+ | v[vadas] |
+ | v[josh] |
+ | v[ripple] |
+ | v[lop] |
+
+ # Multi-traversal without() — exclusion from multiple relationship sources
+ @GraphComputerVerificationMidVNotSupported
+ Scenario: g_V_hasLabelXsoftwareX_hasXname_withoutXVXvid1X_outXcreatedX_valuesXnameX_VXvid3X_outXcreatedX_valuesXnameXXX
+ Given the modern graph
+ And using the parameter vid1 defined as "v[marko].id"
+ And using the parameter vid3 defined as "v[josh].id"
+ And the traversal of
+ """
+ g.V().hasLabel("software").has("name", P.without(__.V(vid1).out("created").values("name"), __.V(vid3).out("created").values("name")))
+ """
+ When iterated to list
+ Then the result should be empty
+
+ # Multi-traversal within() with is() — cross-label dynamic filtering
+ @GraphComputerVerificationMidVNotSupported
+ Scenario: g_V_hasLabelXpersonX_valuesXageX_isXwithinXVXvid1X_valuesXageX_V_hasXname_lopX_inXcreatedX_valuesXageXXX
+ Given the modern graph
+ And using the parameter vid1 defined as "v[marko].id"
+ And the traversal of
+ """
+ g.V().hasLabel("person").values("age").is(P.within(__.V(vid1).values("age"), __.V().has("name","lop").in("created").values("age")))
+ """
+ When iterated to list
+ Then the result should be unordered
+ | result |
+ | d[29].i |
+ | d[32].i |
+ | d[35].i |
+
+ # Multi-traversal within() — dynamic edge filtering via inV property check
+ @GraphComputerVerificationMidVNotSupported
+ Scenario: g_VXvid1X_outEXknowsX_filterXinV_hasXname_withinXV_hasXname_lopX_inXcreatedX_valuesXnameX_V_hasXname_rippleX_inXcreatedX_valuesXnameXXXX
+ Given the modern graph
+ And using the parameter vid1 defined as "v[marko].id"
+ And the traversal of
+ """
+ g.V(vid1).outE("knows").filter(__.inV().has("name", P.within(__.V().has("name","lop").in("created").values("name"), __.V().has("name","ripple").in("created").values("name"))))
+ """
+ When iterated to list
+ Then the result should be unordered
+ | result |
+ | e[marko-knows->josh] |
diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/filter/IsTraversal.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/filter/IsTraversal.feature
index 495067b6cb..42b1eb898b 100644
--- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/filter/IsTraversal.feature
+++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/filter/IsTraversal.feature
@@ -255,3 +255,19 @@ Feature: Step - is() with traversal-bearing predicates
"""
When iterated to list
Then the result should be empty
+
+ # Multi-traversal without() in is() context
+ @GraphComputerVerificationMidVNotSupported
+ Scenario: g_V_valuesXageX_isXwithoutXVXvid1X_valuesXageX_VXvid2X_valuesXageXXX
+ Given the modern graph
+ And using the parameter vid1 defined as "v[marko].id"
+ And using the parameter vid2 defined as "v[josh].id"
+ And the traversal of
+ """
+ g.V().values("age").is(P.without(__.V(vid1).values("age"), __.V(vid2).values("age")))
+ """
+ When iterated to list
+ Then the result should be unordered
+ | result |
+ | d[27].i |
+ | d[35].i |
diff --git a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/strategy/optimization/TinkerGraphStepStrategy.java b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/strategy/optimization/TinkerGraphStepStrategy.java
index 8194adc4e0..7b0774eaf7 100644
--- a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/strategy/optimization/TinkerGraphStepStrategy.java
+++ b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/strategy/optimization/TinkerGraphStepStrategy.java
@@ -56,10 +56,13 @@ public final class TinkerGraphStepStrategy extends AbstractTraversalStrategy<Tra
if (currentStep instanceof HasStep) {
final List<HasContainer> hasContainers = ((HasContainerHolder) currentStep).getHasContainers();
- // skip folding if any HasContainer holds a child traversal — its value
- // is dynamic (resolved per-traverser) and cannot be pushed into TinkerGraphStep
+ // skip folding this HasStep if any HasContainer holds a child traversal —
+ // its value is dynamic (resolved per-traverser) and cannot be pushed into
+ // TinkerGraphStep. Continue to the next step so that subsequent literal
+ // HasSteps can still be folded.
if (hasContainers.stream().anyMatch(HasContainer::hasTraversal)) {
- break;
+ currentStep = currentStep.getNextStep();
+ continue;
}
for (final HasContainer hasContainer : hasContainers) {
diff --git a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/strategy/optimization/TinkerGraphStepStrategyTraversalTest.java b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/strategy/optimization/TinkerGraphStepStrategyTraversalTest.java
index 5fdfa6fdfd..c7db04b280 100644
--- a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/strategy/optimization/TinkerGraphStepStrategyTraversalTest.java
+++ b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/strategy/optimization/TinkerGraphStepStrategyTraversalTest.java
@@ -135,15 +135,44 @@ public class TinkerGraphStepStrategyTraversalTest {
@Test
public void shouldFoldLiteralHasButNotTraversalHasInMixedTraversal() {
// g.V().has("name", "marko").has("age", __.constant(29))
- // The literal has("name", "marko") should be folded, but has("age", traversal) should not
+ // Both containers end up in the same HasStep (TraversalHelper.addHasContainer merges).
+ // The strategy skips the entire HasStep because it contains a traversal-bearing container.
final GraphTraversal<Vertex, Vertex> traversal =
g.V().has("name", "marko").has("age", __.constant(29));
final List<Step> steps = applyStrategy(traversal.asAdmin());
+ // TinkerGraphStep should have NO folded HasContainers (the merged HasStep is skipped)
+ assertThat(steps.get(0), instanceOf(TinkerGraphStep.class));
+ final TinkerGraphStep<?, ?> tinkerGraphStep = (TinkerGraphStep<?, ?>) steps.get(0);
+
+ // The HasStep with both containers should remain as a separate step
+ final boolean hasStepPresent = steps.stream().anyMatch(s -> s instanceof HasStep);
+ assertThat("HasStep with mixed containers should remain as separate step",
+ hasStepPresent, is(true));
+ }
+
+ @Test
+ public void shouldFoldLiteralHasAfterBarrierAndTraversalHas() {
+ // g.V().has("age", __.constant(29)).barrier().has("name", "marko")
+ // The barrier separates the two HasSteps. The strategy should:
+ // - Skip the traversal-bearing HasStep (has("age", traversal))
+ // - Stop at the barrier (not a HasStep or NoOpBarrierStep... actually NoOpBarrierStep IS handled)
+ // Let's use a different separator. Actually NoOpBarrierStep is handled by the while loop.
+ // The key test is: after skipping a traversal-bearing HasStep, the strategy continues
+ // and can fold subsequent literal HasSteps.
+ //
+ // With separate HasSteps (not merged), the strategy should fold the literal one.
+ // We can force separate HasSteps by inserting a NoOpBarrierStep between them.
+ final GraphTraversal<Vertex, Vertex> traversal =
+ g.V().has("age", __.constant(29)).barrier().has("name", "marko");
+ final List<Step> steps = applyStrategy(traversal.asAdmin());
+
// TinkerGraphStep should have the literal "name" HasContainer folded
assertThat(steps.get(0), instanceOf(TinkerGraphStep.class));
final TinkerGraphStep<?, ?> tinkerGraphStep = (TinkerGraphStep<?, ?>) steps.get(0);
- assertThat(tinkerGraphStep.getHasContainers(), hasSize(1));
+ assertThat("Literal HasContainer after barrier should be folded even when preceded by traversal-bearing HasStep",
+ tinkerGraphStep.getHasContainers(), hasSize(1));
+ assertThat(tinkerGraphStep.getHasContainers().get(0).getKey(), is("name"));
// The traversal-bearing HasStep for "age" should remain
final boolean hasStepPresent = steps.stream().anyMatch(s -> s instanceof HasStep);
@@ -151,4 +180,26 @@ public class TinkerGraphStepStrategyTraversalTest {
hasStepPresent, is(true));
}
+ @Test
+ public void shouldFoldMultipleLiteralHasStepsSeparatedByTraversalHas() {
+ // g.V().has("name", "marko").has("age", __.constant(29)).barrier().has("lang", "java")
+ // has("name") is a separate literal HasStep, has("age") is a separate traversal HasStep,
+ // has("lang") is a separate literal HasStep after the barrier.
+ // Strategy should fold both literal HasSteps and skip the traversal one.
+ final GraphTraversal<Vertex, Vertex> traversal =
+ g.V().has("name", "marko").has("age", __.constant(29)).barrier().has("lang", "java");
+ final List<Step> steps = applyStrategy(traversal.asAdmin());
+
+ // TinkerGraphStep should have both literal HasContainers folded ("name" and "lang")
+ assertThat(steps.get(0), instanceOf(TinkerGraphStep.class));
+ final TinkerGraphStep<?, ?> tinkerGraphStep = (TinkerGraphStep<?, ?>) steps.get(0);
+ assertThat("Both literal HasContainers should be folded",
+ tinkerGraphStep.getHasContainers(), hasSize(2));
+
+ // The traversal-bearing HasStep (age) should remain
+ final boolean hasStepPresent = steps.stream().anyMatch(s -> s instanceof HasStep);
+ assertThat("Traversal-bearing HasStep should remain as separate step",
+ hasStepPresent, is(true));
+ }
+
}