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 a9b52b44c6c875e8af1d9734569105b44b8a14b6 Author: Yang Xia <[email protected]> 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 d1305a85ce..856a610026 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)); + } + }
