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/06/12 21:17:58 UTC
(tinkerpop) 02/06: Add traversal-accepting has/hasLabel/is/V/E/property/where steps and mutation guards
This is an automated email from the ASF dual-hosted git repository.
xiazcy pushed a commit to branch steps-taking-traversal
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
commit c661ea65226e9c57abbe5177e7deb5c55727cef9
Author: Yang Xia <xi...@gmail.com>
AuthorDate: Fri Jun 12 00:16:08 2026 -0700
Add traversal-accepting has/hasLabel/is/V/E/property/where steps and mutation guards
Wire the runtime-resolved predicates into the filter, lookup, and mutation
steps and their DSL entry points:
- has(key|T|label, traversal), hasLabel(traversal): normalized to a HasContainer
holding P.eq(traversal) so there is a single resolution path; the direct
traversal form on HasContainer is removed.
- is(traversal), where(P) with a traversal-bearing predicate.
- V(traversal)/E(traversal) start steps seed a synthetic traverser (consistent
with mergeV/mergeE) so the id traversal can run without an incoming traverser.
- property(traversal) producing a Map of key/value pairs, dispatched by an
internal mapForm flag rather than a sentinel key.
- All/Any/None step support for traversal-bearing predicates.
Mutation safety is enforced in two layers: ChildTraversalValidator at DSL
construction time and ChildTraversalVerificationStrategy at strategy time, the
latter validating any step marked with the new AcceptsChildPredicateTraversal
interface. GraphStep/TinkerGraphStepStrategy skip folding traversal-bearing
HasContainers into index lookups since their value is dynamic.
---
.../process/traversal/TraversalStrategies.java | 2 +
.../traversal/dsl/graph/GraphTraversal.java | 150 +++++++++++++-
.../traversal/dsl/graph/GraphTraversalSource.java | 37 ++++
.../gremlin/process/traversal/dsl/graph/__.java | 49 +++++
.../step/AcceptsChildPredicateTraversal.java | 34 +++
.../process/traversal/step/HasContainerHolder.java | 2 +-
.../process/traversal/step/filter/AllStep.java | 30 ++-
.../process/traversal/step/filter/AnyStep.java | 30 ++-
.../process/traversal/step/filter/HasStep.java | 109 +++++++++-
.../process/traversal/step/filter/IsStep.java | 28 ++-
.../process/traversal/step/filter/NoneStep.java | 32 ++-
.../traversal/step/filter/WherePredicateStep.java | 40 +++-
.../process/traversal/step/map/GraphStep.java | 124 ++++++++++-
.../traversal/step/sideEffect/AddPropertyStep.java | 129 +++++++++++-
.../step/sideEffect/AddPropertyStepContract.java | 4 +-
.../sideEffect/AddPropertyStepPlaceholder.java | 16 +-
.../process/traversal/step/util/HasContainer.java | 10 +-
.../optimization/InlineFilterStrategy.java | 9 +
.../ChildTraversalVerificationStrategy.java | 61 ++++++
.../traversal/util/ChildTraversalValidator.java | 62 ++++++
.../step/CloneIndependenceTraversalTest.java | 120 +++++++++++
.../traversal/step/util/HasContainerTest.java | 90 ++++++++
.../ChildTraversalVerificationStrategyTest.java | 69 +++++++
.../util/ChildTraversalValidatorTest.java | 229 +++++++++++++++++++++
.../traversal/step/sideEffect/TinkerGraphStep.java | 8 +
.../optimization/TinkerGraphStepStrategy.java | 14 +-
.../TinkerGraphStepStrategyTraversalTest.java | 205 ++++++++++++++++++
27 files changed, 1649 insertions(+), 44 deletions(-)
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/TraversalStrategies.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/TraversalStrategies.java
index bccb983665..e3f0d90ff6 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/TraversalStrategies.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/TraversalStrategies.java
@@ -52,6 +52,7 @@ import org.apache.tinkerpop.gremlin.process.traversal.strategy.optimization.Path
import org.apache.tinkerpop.gremlin.process.traversal.strategy.optimization.PathRetractionStrategy;
import org.apache.tinkerpop.gremlin.process.traversal.strategy.optimization.ProductiveByStrategy;
import org.apache.tinkerpop.gremlin.process.traversal.strategy.optimization.RepeatUnrollStrategy;
+import org.apache.tinkerpop.gremlin.process.traversal.strategy.verification.ChildTraversalVerificationStrategy;
import org.apache.tinkerpop.gremlin.process.traversal.strategy.verification.ComputerVerificationStrategy;
import org.apache.tinkerpop.gremlin.process.traversal.strategy.verification.EdgeLabelVerificationStrategy;
import org.apache.tinkerpop.gremlin.process.traversal.strategy.verification.LambdaRestrictionStrategy;
@@ -287,6 +288,7 @@ public interface TraversalStrategies extends Serializable, Cloneable, Iterable<T
LazyBarrierStrategy.instance(),
ProfileStrategy.instance(),
StandardVerificationStrategy.instance(),
+ ChildTraversalVerificationStrategy.instance(),
GValueReductionStrategy.instance());
registerStrategies(Graph.class, graphStrategies);
registerStrategies(EmptyGraph.class, new DefaultTraversalStrategies());
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.java
index c42859f7a5..2b8ec3eca7 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.java
@@ -209,6 +209,7 @@ import org.apache.tinkerpop.gremlin.process.traversal.step.util.WithOptions;
import org.apache.tinkerpop.gremlin.process.traversal.traverser.util.TraverserSet;
import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalHelper;
import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalMetrics;
+import org.apache.tinkerpop.gremlin.process.traversal.util.ChildTraversalValidator;
import org.apache.tinkerpop.gremlin.structure.Column;
import org.apache.tinkerpop.gremlin.structure.Direction;
import org.apache.tinkerpop.gremlin.structure.Edge;
@@ -414,6 +415,23 @@ public interface GraphTraversal<S, E> extends Traversal<S, E> {
return this.asAdmin().addStep(step);
}
+ /**
+ * A {@code V} step that accepts a child traversal whose results are used as vertex IDs for lookup.
+ * This form is only valid as a mid-traversal step; using it as a start step will throw an
+ * {@link IllegalStateException} at runtime because there is no traverser context to evaluate the child traversal.
+ *
+ * @param traversal the child traversal that produces vertex IDs
+ * @return the traversal with an appended {@link GraphStep}
+ * @see <a href="http://tinkerpop.apache.org/docs/${project.version}/reference/#graph-step" target="_blank">Reference Documentation - Graph Step</a>
+ * @since 4.0.0
+ */
+ public default GraphTraversal<S, Vertex> V(final Traversal<?, ?> traversal) {
+ if (null == traversal) return V(new Object[]{ null });
+ ChildTraversalValidator.validate(traversal.asAdmin());
+ this.asAdmin().getGremlinLang().addStep(Symbols.V, traversal);
+ return this.asAdmin().addStep(new GraphStep<>(this.asAdmin(), Vertex.class, false, traversal.asAdmin()));
+ }
+
/**
* A {@code E} step is usually used to start a traversal but it may also be used mid-traversal.
*
@@ -437,6 +455,23 @@ public interface GraphTraversal<S, E> extends Traversal<S, E> {
return this.asAdmin().addStep(step);
}
+ /**
+ * A {@code E} step that accepts a child traversal whose results are used as edge IDs for lookup.
+ * This form is only valid as a mid-traversal step; using it as a start step will throw an
+ * {@link IllegalStateException} at runtime because there is no traverser context to evaluate the child traversal.
+ *
+ * @param traversal the child traversal that produces edge IDs
+ * @return the traversal with an appended {@link GraphStep}
+ * @see <a href="http://tinkerpop.apache.org/docs/${project.version}/reference/#e-step" target="_blank">Reference Documentation - E Step</a>
+ * @since 4.0.0
+ */
+ public default GraphTraversal<S, Edge> E(final Traversal<?, ?> traversal) {
+ if (null == traversal) return E(new Object[]{ null });
+ ChildTraversalValidator.validate(traversal.asAdmin());
+ this.asAdmin().getGremlinLang().addStep(Symbols.E, traversal);
+ return this.asAdmin().addStep(new GraphStep<>(this.asAdmin(), Edge.class, false, traversal.asAdmin()));
+ }
+
/**
* Map the {@link Vertex} to its adjacent vertices given a direction.
*
@@ -2670,6 +2705,47 @@ public interface GraphTraversal<S, E> extends Traversal<S, E> {
}
}
+ /**
+ * Filters vertices, edges and vertex properties based on their properties using a child traversal
+ * whose results are resolved at runtime against the current traverser. The resolved results are used
+ * with {@code P.within()} semantics for comparison against the named property.
+ *
+ * @param propertyKey the key of the property to filter on
+ * @param traversal the child traversal whose results are used as the filter value
+ * @return the traversal with an appended {@link HasStep}
+ * @see <a href="http://tinkerpop.apache.org/docs/${project.version}/reference/#has-step" target="_blank">Reference Documentation - Has Step</a>
+ * @since 4.0.0
+ */
+ public default GraphTraversal<S, E> has(final String propertyKey, final Traversal<?, ?> traversal) {
+ if (null == traversal) return has(propertyKey, (Object) null);
+ ChildTraversalValidator.validate(traversal.asAdmin());
+ this.asAdmin().getGremlinLang().addStep(Symbols.has, propertyKey, traversal);
+ final HasContainer hasContainer = new HasContainer(propertyKey, P.eq(traversal.asAdmin()));
+ return this.asAdmin().addStep(new HasStep(this.asAdmin(), hasContainer));
+ }
+
+ /**
+ * Filters vertices, edges and vertex properties based on their properties using a child traversal
+ * whose results are resolved at runtime against the current traverser. The resolved results are used
+ * with {@code P.within()} semantics for comparison against the T-accessor value (id or label).
+ *
+ * @param accessor the {@link T} accessor of the property to filter on
+ * @param traversal the child traversal whose results are used as the filter value
+ * @return the traversal with an appended {@link HasStep}
+ * @see <a href="http://tinkerpop.apache.org/docs/${project.version}/reference/#has-step" target="_blank">Reference Documentation - Has Step</a>
+ * @since 4.0.0
+ */
+ public default GraphTraversal<S, E> has(final T accessor, final Traversal<?, ?> traversal) {
+ if (null == accessor)
+ throw new IllegalArgumentException("The T accessor value of has(T,Traversal) cannot be null");
+ if (null == traversal) return has(accessor, (Object) null);
+ ChildTraversalValidator.validate(traversal.asAdmin());
+
+ this.asAdmin().getGremlinLang().addStep(Symbols.has, accessor, traversal);
+ final HasContainer hasContainer = new HasContainer(accessor.getAccessor(), P.eq(traversal.asAdmin()));
+ return this.asAdmin().addStep(new HasStep(this.asAdmin(), hasContainer));
+ }
+
/**
* Filters vertices, edges and vertex properties based on their properties.
*
@@ -2702,6 +2778,27 @@ public interface GraphTraversal<S, E> extends Traversal<S, E> {
return TraversalHelper.addHasContainer(this.asAdmin(), new HasContainer(propertyKey, value instanceof P ? (P) value : P.eq(value)));
}
+ /**
+ * Filters vertices, edges and vertex properties based on their label and a property value resolved
+ * from a child traversal at runtime. The label is matched first, then the child traversal results
+ * are used with {@code P.within()} semantics for comparison against the named property.
+ *
+ * @param label the label of the {@link Element}
+ * @param propertyKey the key of the property to filter on
+ * @param traversal the child traversal whose results are used as the filter value
+ * @return the traversal with an appended {@link HasStep}
+ * @see <a href="http://tinkerpop.apache.org/docs/${project.version}/reference/#has-step" target="_blank">Reference Documentation - Has Step</a>
+ * @since 4.0.0
+ */
+ public default GraphTraversal<S, E> has(final String label, final String propertyKey, final Traversal<?, ?> traversal) {
+ if (null == traversal) return has(label, propertyKey, (Object) null);
+ ChildTraversalValidator.validate(traversal.asAdmin());
+ this.asAdmin().getGremlinLang().addStep(Symbols.has, label, propertyKey, traversal);
+ TraversalHelper.addHasContainer(this.asAdmin(), new HasContainer(T.label.getAccessor(), P.eq(label)));
+ final HasContainer hasContainer = new HasContainer(propertyKey, P.eq(traversal.asAdmin()));
+ return this.asAdmin().addStep(new HasStep(this.asAdmin(), hasContainer));
+ }
+
/**
* Filters vertices, edges and vertex properties based on the existence of properties.
*
@@ -2797,6 +2894,23 @@ public interface GraphTraversal<S, E> extends Traversal<S, E> {
}
}
+ /**
+ * Filters vertices, edges and vertex properties based on their label using a child traversal
+ * whose results are resolved at runtime against the current traverser.
+ *
+ * @param traversal the child traversal whose results are used as the label filter value
+ * @return the traversal with an appended {@link HasStep}
+ * @see <a href="http://tinkerpop.apache.org/docs/${project.version}/reference/#has-step" target="_blank">Reference Documentation - Has Step</a>
+ * @since 4.0.0
+ */
+ public default GraphTraversal<S, E> hasLabel(final Traversal<?, ?> traversal) {
+ if (null == traversal) return hasLabel((String) null);
+ ChildTraversalValidator.validate(traversal.asAdmin());
+ this.asAdmin().getGremlinLang().addStep(Symbols.hasLabel, traversal);
+ final HasContainer hasContainer = new HasContainer(T.label.getAccessor(), P.eq(traversal.asAdmin()));
+ return this.asAdmin().addStep(new HasStep(this.asAdmin(), hasContainer));
+ }
+
/**
* Filters vertices, edges and vertex properties based on their label.
*
@@ -3022,7 +3136,14 @@ public interface GraphTraversal<S, E> extends Traversal<S, E> {
*/
public default GraphTraversal<S, E> is(final Object value) {
this.asAdmin().getGremlinLang().addStep(Symbols.is, value);
- P<E> predicate = value instanceof P ? (P<E>) value : P.eq((E) value);
+ P<E> predicate;
+ if (value instanceof P) {
+ predicate = (P<E>) value;
+ } else if (value instanceof Traversal) {
+ predicate = (P<E>) P.eq(((Traversal<?, ?>) value).asAdmin());
+ } else {
+ predicate = P.eq((E) value);
+ }
return this.asAdmin().addStep(predicate.isParameterized() ? new IsStepPlaceholder<>(this.asAdmin(), predicate) : new IsStep<>(this.asAdmin(), predicate));
}
@@ -3825,6 +3946,25 @@ public interface GraphTraversal<S, E> extends Traversal<S, E> {
}
return this;
}
+
+ /**
+ * Sets properties on the current element using a child traversal that produces a {@code Map}.
+ * Each entry in the resulting Map becomes a property (key → value) on the element. The traversal
+ * is evaluated at execution time against the current traverser.
+ *
+ * @param mapTraversal a traversal that produces a {@code Map<String, Object>} of property key/value pairs
+ * @return the traversal with the {@link AddPropertyStepPlaceholder} added
+ * @see <a href="http://tinkerpop.apache.org/docs/${project.version}/reference/#addproperty-step" target="_blank">AddProperty Step</a>
+ * @since 4.0.0
+ */
+ public default GraphTraversal<S, E> property(final Traversal<?, ?> mapTraversal) {
+ if (null == mapTraversal) return this;
+ ChildTraversalValidator.validate(mapTraversal.asAdmin());
+ this.asAdmin().getGremlinLang().addStep(Symbols.property, mapTraversal);
+ this.asAdmin().addStep(new AddPropertyStepPlaceholder(this.asAdmin(), null, null, mapTraversal.asAdmin(), true));
+ return this;
+ }
+
///////////////////// BRANCH STEPS /////////////////////
/**
@@ -3972,6 +4112,10 @@ public interface GraphTraversal<S, E> extends Traversal<S, E> {
if (choosePredicate.isParameterized()) {
throw new IllegalArgumentException("Parameterized predicates are not supported by choose()");
}
+ if (choosePredicate.hasTraversal()) {
+ throw new IllegalArgumentException("Traversal-bearing predicates are not supported by choose(). " +
+ "Use choose(__.is(P.op(traversal)), trueChoice, falseChoice) instead.");
+ }
this.asAdmin().getGremlinLang().addStep(Symbols.choose, choosePredicate, trueChoice, falseChoice);
return this.asAdmin().addStep(new ChooseStep<E, E2, Boolean>(this.asAdmin(), (Traversal.Admin<E, ?>) __.is(choosePredicate), (Traversal.Admin<E, E2>) trueChoice, (Traversal.Admin<E, E2>) falseChoice));
}
@@ -4009,6 +4153,10 @@ public interface GraphTraversal<S, E> extends Traversal<S, E> {
if (choosePredicate.isParameterized()) {
throw new IllegalArgumentException("Parameterized predicates are not supported by choose()");
}
+ if (choosePredicate.hasTraversal()) {
+ throw new IllegalArgumentException("Traversal-bearing predicates are not supported by choose(). " +
+ "Use choose(__.is(P.op(traversal)), trueChoice) instead.");
+ }
this.asAdmin().getGremlinLang().addStep(Symbols.choose, choosePredicate, trueChoice);
return this.asAdmin().addStep(new ChooseStep<E, E2, Boolean>(this.asAdmin(), (Traversal.Admin<E, ?>) __.is(choosePredicate), (Traversal.Admin<E, E2>) trueChoice, (Traversal.Admin<E, E2>) __.identity()));
}
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversalSource.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversalSource.java
index aec1d6fadd..3bce63c401 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversalSource.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversalSource.java
@@ -42,6 +42,7 @@ import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.InjectStep
import org.apache.tinkerpop.gremlin.process.traversal.step.map.GraphStepContract;
import org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.RequirementsStrategy;
import org.apache.tinkerpop.gremlin.process.traversal.traverser.TraverserRequirement;
+import org.apache.tinkerpop.gremlin.process.traversal.util.ChildTraversalValidator;
import org.apache.tinkerpop.gremlin.structure.Direction;
import org.apache.tinkerpop.gremlin.structure.Edge;
import org.apache.tinkerpop.gremlin.structure.Graph;
@@ -540,6 +541,24 @@ public class GraphTraversalSource implements TraversalSource {
return traversal.addStep(step);
}
+ /**
+ * Spawns a {@link GraphTraversal} starting with vertices whose IDs are resolved from a child traversal.
+ * As a start step, a synthetic traverser is generated to seed the child traversal evaluation,
+ * consistent with how {@code mergeV(traversal)} handles start steps. The child traversal should
+ * be self-contained (e.g., {@code __.V(1).id()}) rather than dependent on an incoming traverser.
+ *
+ * @param traversal the child traversal that produces vertex IDs
+ * @since 4.0.0
+ */
+ public GraphTraversal<Vertex, Vertex> V(final Traversal<?, ?> traversal) {
+ if (null == traversal) return V(new Object[]{ null });
+ ChildTraversalValidator.validate(traversal.asAdmin());
+ final GraphTraversalSource clone = this.clone();
+ clone.gremlinLang.addStep(GraphTraversal.Symbols.V, traversal);
+ final GraphTraversal.Admin<Vertex, Vertex> traversalAdmin = new DefaultGraphTraversal<>(clone);
+ return traversalAdmin.addStep(new GraphStep<>(traversalAdmin, Vertex.class, true, traversal.asAdmin()));
+ }
+
/**
* Spawns a {@link GraphTraversal} starting with all edges or some subset of edges as specified by their unique
* identifier.
@@ -561,6 +580,24 @@ public class GraphTraversalSource implements TraversalSource {
return traversal.addStep(step);
}
+ /**
+ * Spawns a {@link GraphTraversal} starting with edges whose IDs are resolved from a child traversal.
+ * As a start step, a synthetic traverser is generated to seed the child traversal evaluation,
+ * consistent with how {@code mergeE(traversal)} handles start steps. The child traversal should
+ * be self-contained (e.g., {@code __.V(1).outE().id()}) rather than dependent on an incoming traverser.
+ *
+ * @param traversal the child traversal that produces edge IDs
+ * @since 4.0.0
+ */
+ public GraphTraversal<Edge, Edge> E(final Traversal<?, ?> traversal) {
+ if (null == traversal) return E(new Object[]{ null });
+ ChildTraversalValidator.validate(traversal.asAdmin());
+ final GraphTraversalSource clone = this.clone();
+ clone.gremlinLang.addStep(GraphTraversal.Symbols.E, traversal);
+ final GraphTraversal.Admin<Edge, Edge> traversalAdmin = new DefaultGraphTraversal<>(clone);
+ return traversalAdmin.addStep(new GraphStep<>(traversalAdmin, Edge.class, true, traversal.asAdmin()));
+ }
+
/**
* Spawns a {@link GraphTraversal} starting with a list of available services.
*
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/__.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/__.java
index 01986b4534..46eddc4e2f 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/__.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/__.java
@@ -140,6 +140,13 @@ public class __ {
return __.<A>start().V(vertexIdsOrElements);
}
+ /**
+ * @see GraphTraversal#V(Traversal)
+ */
+ public static <A> GraphTraversal<A, Vertex> V(final Traversal<?, ?> traversal) {
+ return __.<A>start().V(traversal);
+ }
+
/**
* @see GraphTraversal#E(Object...)
*/
@@ -147,6 +154,13 @@ public class __ {
return __.<A>start().E(edgeIdsOrElements);
}
+ /**
+ * @see GraphTraversal#E(Traversal)
+ */
+ public static <A> GraphTraversal<A, Edge> E(final Traversal<?, ?> traversal) {
+ return __.<A>start().E(traversal);
+ }
+
/**
* @see GraphTraversal#to(Direction)
*/
@@ -1092,6 +1106,13 @@ public class __ {
return __.<A>start().has(propertyKey, predicate);
}
+ /**
+ * @see GraphTraversal#has(String, Traversal)
+ */
+ public static <A> GraphTraversal<A, A> has(final String propertyKey, final Traversal<?, ?> traversal) {
+ return __.<A>start().has(propertyKey, traversal);
+ }
+
/**
* @see GraphTraversal#has(T, P)
*/
@@ -1113,6 +1134,13 @@ public class __ {
return __.<A>start().has(accessor, value);
}
+ /**
+ * @see GraphTraversal#has(T, Traversal)
+ */
+ public static <A> GraphTraversal<A, A> has(final T accessor, final Traversal<?, ?> traversal) {
+ return __.<A>start().has(accessor, traversal);
+ }
+
/**
* @see GraphTraversal#has(String, String, Object)
*/
@@ -1142,6 +1170,13 @@ public class __ {
return __.<A>start().has(label, propertyKey, predicate);
}
+ /**
+ * @see GraphTraversal#has(String, String, Traversal)
+ */
+ public static <A> GraphTraversal<A, A> has(final String label, final String propertyKey, final Traversal<?, ?> traversal) {
+ return __.<A>start().has(label, propertyKey, traversal);
+ }
+
/**
* @see GraphTraversal#has(String)
*/
@@ -1177,6 +1212,13 @@ public class __ {
return __.<A>start().hasLabel(predicate);
}
+ /**
+ * @see GraphTraversal#hasLabel(Traversal)
+ */
+ public static <A> GraphTraversal<A, A> hasLabel(final Traversal<?, ?> traversal) {
+ return __.<A>start().hasLabel(traversal);
+ }
+
/**
* @see GraphTraversal#hasId(Object, Object...)
*/
@@ -1558,6 +1600,13 @@ public class __ {
return __.<A>start().property(value);
}
+ /**
+ * @see GraphTraversal#property(Traversal)
+ */
+ public static <A> GraphTraversal<A, A> property(final Traversal<?, ?> mapTraversal) {
+ return __.<A>start().property(mapTraversal);
+ }
+
///////////////////// BRANCH STEPS /////////////////////
/**
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/AcceptsChildPredicateTraversal.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/AcceptsChildPredicateTraversal.java
new file mode 100644
index 0000000000..fef3841441
--- /dev/null
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/AcceptsChildPredicateTraversal.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.tinkerpop.gremlin.process.traversal.step;
+
+/**
+ * Marker interface for steps that accept user-supplied child traversals embedded as predicate or lookup
+ * arguments (for example {@code has(key, traversal)}, {@code is(P.gt(traversal))},
+ * {@code V(traversal)}, or {@code property(traversal)}).
+ * <p>
+ * The {@link org.apache.tinkerpop.gremlin.process.traversal.strategy.verification.ChildTraversalVerificationStrategy}
+ * validates the local children of every step implementing this interface, rejecting child traversals that
+ * contain mutating steps. New steps that accept such traversals should implement this interface so they are
+ * validated automatically rather than relying on a hardcoded step list.
+ *
+ * @since 4.0.0
+ */
+public interface AcceptsChildPredicateTraversal {
+}
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/HasContainerHolder.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/HasContainerHolder.java
index f016aa03f4..47343d3ea6 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/HasContainerHolder.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/HasContainerHolder.java
@@ -53,7 +53,7 @@ public interface HasContainerHolder<S, E> extends GValueHolder<S, E> {
}
public default Collection<P<?>> getPredicatesGValueSafe() {
- return getHasContainers().stream().map(p -> p.getPredicate()).collect(Collectors.toList());
+ return getHasContainers().stream().map(p -> p.getPredicate()).filter(p -> p != null).collect(Collectors.toList());
}
public default HasContainerHolder<S, E> asConcreteStep() {
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/filter/AllStep.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/filter/AllStep.java
index 64853f6614..f17795dc0b 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/filter/AllStep.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/filter/AllStep.java
@@ -21,15 +21,19 @@ package org.apache.tinkerpop.gremlin.process.traversal.step.filter;
import org.apache.tinkerpop.gremlin.process.traversal.P;
import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
import org.apache.tinkerpop.gremlin.process.traversal.Traverser;
+import org.apache.tinkerpop.gremlin.process.traversal.step.AcceptsChildPredicateTraversal;
+import org.apache.tinkerpop.gremlin.process.traversal.step.TraversalParent;
import org.apache.tinkerpop.gremlin.process.traversal.traverser.TraverserRequirement;
import org.apache.tinkerpop.gremlin.structure.util.StringFactory;
import org.apache.tinkerpop.gremlin.util.iterator.IteratorUtils;
-import java.util.EnumSet;
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.Iterator;
+import java.util.List;
import java.util.Set;
-public final class AllStep<S, S2> extends FilterStep<S> {
+public final class AllStep<S, S2> extends FilterStep<S> implements TraversalParent, AcceptsChildPredicateTraversal {
private P<S2> predicate;
@@ -41,10 +45,16 @@ public final class AllStep<S, S2> extends FilterStep<S> {
}
this.predicate = predicate;
+ P.integrateTraversals(this.predicate, this);
}
@Override
protected boolean filter(final Traverser.Admin<S> traverser) {
+ if (this.predicate.hasTraversal()) {
+ this.predicate.resolve(traverser);
+ if (this.predicate.isResolvedEmpty()) return false;
+ }
+
final S item = traverser.get();
if (item instanceof Iterable || item instanceof Iterator || ((item != null) && item.getClass().isArray())) {
@@ -65,6 +75,14 @@ public final class AllStep<S, S2> extends FilterStep<S> {
return StringFactory.stepString(this, this.predicate);
}
+ @SuppressWarnings("unchecked")
+ @Override
+ public <S, E> List<Traversal.Admin<S, E>> getLocalChildren() {
+ final List<Traversal.Admin<?, ?>> traversals = new ArrayList<>();
+ P.collectTraversals(this.predicate, traversals);
+ return (List) Collections.unmodifiableList(traversals);
+ }
+
@Override
public AllStep<S, S2> clone() {
final AllStep<S, S2> clone = (AllStep<S, S2>) super.clone();
@@ -74,6 +92,12 @@ public final class AllStep<S, S2> extends FilterStep<S> {
@Override
public Set<TraverserRequirement> getRequirements() {
- return EnumSet.of(TraverserRequirement.OBJECT);
+ return this.getSelfAndChildRequirements(TraverserRequirement.OBJECT);
+ }
+
+ @Override
+ public void setTraversal(final Traversal.Admin<?, ?> parentTraversal) {
+ super.setTraversal(parentTraversal);
+ P.integrateTraversals(this.predicate, this);
}
}
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/filter/AnyStep.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/filter/AnyStep.java
index 9881186d7c..ed0216934c 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/filter/AnyStep.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/filter/AnyStep.java
@@ -21,15 +21,19 @@ package org.apache.tinkerpop.gremlin.process.traversal.step.filter;
import org.apache.tinkerpop.gremlin.process.traversal.P;
import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
import org.apache.tinkerpop.gremlin.process.traversal.Traverser;
+import org.apache.tinkerpop.gremlin.process.traversal.step.AcceptsChildPredicateTraversal;
+import org.apache.tinkerpop.gremlin.process.traversal.step.TraversalParent;
import org.apache.tinkerpop.gremlin.process.traversal.traverser.TraverserRequirement;
import org.apache.tinkerpop.gremlin.structure.util.StringFactory;
import org.apache.tinkerpop.gremlin.util.iterator.IteratorUtils;
-import java.util.EnumSet;
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.Iterator;
+import java.util.List;
import java.util.Set;
-public final class AnyStep<S, S2> extends FilterStep<S> {
+public final class AnyStep<S, S2> extends FilterStep<S> implements TraversalParent, AcceptsChildPredicateTraversal {
private P<S2> predicate;
@@ -41,10 +45,16 @@ public final class AnyStep<S, S2> extends FilterStep<S> {
}
this.predicate = predicate;
+ P.integrateTraversals(this.predicate, this);
}
@Override
protected boolean filter(final Traverser.Admin<S> traverser) {
+ if (this.predicate.hasTraversal()) {
+ this.predicate.resolve(traverser);
+ if (this.predicate.isResolvedEmpty()) return false;
+ }
+
final S item = traverser.get();
if (item instanceof Iterable || item instanceof Iterator || ((item != null) && item.getClass().isArray())) {
@@ -65,6 +75,14 @@ public final class AnyStep<S, S2> extends FilterStep<S> {
return StringFactory.stepString(this, this.predicate);
}
+ @SuppressWarnings("unchecked")
+ @Override
+ public <S, E> List<Traversal.Admin<S, E>> getLocalChildren() {
+ final List<Traversal.Admin<?, ?>> traversals = new ArrayList<>();
+ P.collectTraversals(this.predicate, traversals);
+ return (List) Collections.unmodifiableList(traversals);
+ }
+
@Override
public AnyStep<S, S2> clone() {
final AnyStep<S, S2> clone = (AnyStep<S, S2>) super.clone();
@@ -74,6 +92,12 @@ public final class AnyStep<S, S2> extends FilterStep<S> {
@Override
public Set<TraverserRequirement> getRequirements() {
- return EnumSet.of(TraverserRequirement.OBJECT);
+ return this.getSelfAndChildRequirements(TraverserRequirement.OBJECT);
+ }
+
+ @Override
+ public void setTraversal(final Traversal.Admin<?, ?> parentTraversal) {
+ super.setTraversal(parentTraversal);
+ P.integrateTraversals(this.predicate, this);
}
}
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/filter/HasStep.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/filter/HasStep.java
index e7ef009565..1b56dbdfeb 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/filter/HasStep.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/filter/HasStep.java
@@ -18,10 +18,13 @@
*/
package org.apache.tinkerpop.gremlin.process.traversal.step.filter;
+import org.apache.tinkerpop.gremlin.process.traversal.P;
import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
import org.apache.tinkerpop.gremlin.process.traversal.Traverser;
+import org.apache.tinkerpop.gremlin.process.traversal.step.AcceptsChildPredicateTraversal;
import org.apache.tinkerpop.gremlin.process.traversal.step.Configuring;
import org.apache.tinkerpop.gremlin.process.traversal.step.HasContainerHolder;
+import org.apache.tinkerpop.gremlin.process.traversal.step.TraversalParent;
import org.apache.tinkerpop.gremlin.process.traversal.step.util.HasContainer;
import org.apache.tinkerpop.gremlin.process.traversal.step.util.Parameters;
import org.apache.tinkerpop.gremlin.process.traversal.traverser.TraverserRequirement;
@@ -31,22 +34,25 @@ import org.apache.tinkerpop.gremlin.structure.util.StringFactory;
import java.util.ArrayList;
import java.util.Collections;
-import java.util.EnumSet;
import java.util.List;
import java.util.Set;
/**
* @author Marko A. Rodriguez (http://markorodriguez.com)
*/
-public class HasStep<S extends Element> extends FilterStep<S> implements HasContainerHolder<S, S>, Configuring {
+public class HasStep<S extends Element> extends FilterStep<S> implements HasContainerHolder<S, S>, Configuring, TraversalParent, AcceptsChildPredicateTraversal {
private final Parameters parameters = new Parameters();
private List<HasContainer> hasContainers;
+ private List<Traversal.Admin<?, ?>> childTraversals = new ArrayList<>();
public HasStep(final Traversal.Admin traversal, final HasContainer... hasContainers) {
super(traversal);
this.hasContainers = new ArrayList<>();
- Collections.addAll(this.hasContainers, hasContainers);
+ for (final HasContainer hc : hasContainers) {
+ this.hasContainers.add(hc);
+ collectChildTraversals(hc);
+ }
}
@Override
@@ -63,14 +69,71 @@ public class HasStep<S extends Element> extends FilterStep<S> implements HasCont
protected boolean filter(final Traverser.Admin<S> traverser) {
// the generic S is defined as Element but Property can also be used with HasStep so this seems to cause
// problems with some jdk versions.
- if (traverser.get() instanceof Element)
- return HasContainer.testAll(traverser.get(), this.hasContainers);
- else if (traverser.get() instanceof Property)
- return HasContainer.testAll((Property) traverser.get(), this.hasContainers);
- else
+ // Check Property BEFORE Element because VertexProperty implements both interfaces.
+ // HasContainer.test(Property) correctly handles T.key and T.value accessors, whereas
+ // HasContainer.test(Element) would fall through to element.properties("~key") which fails.
+ if (traverser.get() instanceof Property) {
+ return testAllWithTraversals(traverser, (Property) traverser.get());
+ } else if (traverser.get() instanceof Element) {
+ return testAllWithTraversals(traverser, (Element) traverser.get());
+ } else {
throw new IllegalStateException(String.format(
"Traverser to has() must be of type Property or Element, not %s",
traverser.get().getClass().getName()));
+ }
+ }
+
+ /**
+ * Tests all HasContainers against an Element. Traversal-bearing containers carry a {@code P} predicate
+ * (e.g. {@code P.eq(traversal)}) whose child traversal is resolved against the current traverser before
+ * the standard {@link HasContainer#test(Element)} is applied.
+ */
+ private boolean testAllWithTraversals(final Traverser.Admin<S> traverser, final Element element) {
+ for (final HasContainer hc : this.hasContainers) {
+ if (!resolvePredicate(hc, traverser) || !hc.test(element)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Tests all HasContainers against a Property. See {@link #testAllWithTraversals(Traverser.Admin, Element)}.
+ */
+ private boolean testAllWithTraversals(final Traverser.Admin<S> traverser, final Property property) {
+ for (final HasContainer hc : this.hasContainers) {
+ if (!resolvePredicate(hc, traverser) || !hc.test(property)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Resolves a container's predicate traversal (if any) against the current traverser, mutating the
+ * predicate with the resolved value for this test cycle.
+ *
+ * @return {@code false} if the predicate resolved empty (no comparison value), meaning the element
+ * cannot match and the caller should short-circuit; {@code true} otherwise.
+ */
+ private boolean resolvePredicate(final HasContainer hc, final Traverser.Admin<S> traverser) {
+ if (hc.hasTraversal()) {
+ hc.getPredicate().resolve(traverser);
+ return !hc.getPredicate().isResolvedEmpty();
+ }
+ return true;
+ }
+
+ /**
+ * Collects the child traversals embedded within a HasContainer's predicate (including
+ * {@link org.apache.tinkerpop.gremlin.process.traversal.util.ConnectiveP}, which may carry traversals on
+ * child predicates) into {@link #childTraversals}. Parenting is deferred to
+ * {@link #setTraversal(Traversal.Admin)} which is the single integration point.
+ */
+ private void collectChildTraversals(final HasContainer hc) {
+ if (hc.getPredicate() != null && hc.getPredicate().hasTraversal()) {
+ P.collectTraversals(hc.getPredicate(), this.childTraversals);
+ }
}
@Override
@@ -91,23 +154,49 @@ public class HasStep<S extends Element> extends FilterStep<S> implements HasCont
@Override
public void addHasContainer(final HasContainer hasContainer) {
this.hasContainers.add(hasContainer);
+ // A container may be added after this step has already been parented (e.g. by a strategy), so collect
+ // its child traversals and integrate just those new ones immediately.
+ final int before = this.childTraversals.size();
+ collectChildTraversals(hasContainer);
+ for (int i = before; i < this.childTraversals.size(); i++) {
+ this.integrateChild(this.childTraversals.get(i));
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public <S, E> List<Traversal.Admin<S, E>> getLocalChildren() {
+ return (List) Collections.unmodifiableList(this.childTraversals);
}
@Override
public Set<TraverserRequirement> getRequirements() {
- return EnumSet.of(TraverserRequirement.OBJECT);
+ return this.getSelfAndChildRequirements(TraverserRequirement.OBJECT);
}
@Override
public HasStep<S> clone() {
final HasStep<S> clone = (HasStep<S>) super.clone();
clone.hasContainers = new ArrayList<>();
+ clone.childTraversals = new ArrayList<>();
for (final HasContainer hasContainer : this.hasContainers) {
- clone.addHasContainer(hasContainer.clone());
+ final HasContainer clonedHc = hasContainer.clone();
+ clone.hasContainers.add(clonedHc);
+ if (clonedHc.getPredicate() != null && clonedHc.getPredicate().hasTraversal()) {
+ P.collectTraversals(clonedHc.getPredicate(), clone.childTraversals);
+ }
}
return clone;
}
+ @Override
+ public void setTraversal(final Traversal.Admin<?, ?> parentTraversal) {
+ super.setTraversal(parentTraversal);
+ for (final Traversal.Admin<?, ?> childTraversal : this.childTraversals) {
+ this.integrateChild(childTraversal);
+ }
+ }
+
@Override
public int hashCode() {
int result = super.hashCode();
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/filter/IsStep.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/filter/IsStep.java
index 70ad55ff31..e49c7e3537 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/filter/IsStep.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/filter/IsStep.java
@@ -21,27 +21,36 @@ package org.apache.tinkerpop.gremlin.process.traversal.step.filter;
import org.apache.tinkerpop.gremlin.process.traversal.P;
import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
import org.apache.tinkerpop.gremlin.process.traversal.Traverser;
+import org.apache.tinkerpop.gremlin.process.traversal.step.AcceptsChildPredicateTraversal;
+import org.apache.tinkerpop.gremlin.process.traversal.step.TraversalParent;
import org.apache.tinkerpop.gremlin.process.traversal.traverser.TraverserRequirement;
import org.apache.tinkerpop.gremlin.structure.util.StringFactory;
-import java.util.EnumSet;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
import java.util.Set;
/**
* @author Daniel Kuppitz (http://gremlin.guru)
* @author Marko A. Rodriguez (http://markorodriguez.com)
*/
-public final class IsStep<S> extends FilterStep<S> implements IsStepContract<S> {
+public final class IsStep<S> extends FilterStep<S> implements IsStepContract<S>, TraversalParent, AcceptsChildPredicateTraversal {
private P<S> predicate;
public IsStep(final Traversal.Admin traversal, final P<S> predicate) {
super(traversal);
this.predicate = predicate;
+ P.integrateTraversals(this.predicate, this);
}
@Override
protected boolean filter(final Traverser.Admin<S> traverser) {
+ if (this.predicate.hasTraversal()) {
+ this.predicate.resolve(traverser);
+ if (this.predicate.isResolvedEmpty()) return false;
+ }
return this.predicate.test(traverser.get());
}
@@ -55,6 +64,14 @@ public final class IsStep<S> extends FilterStep<S> implements IsStepContract<S>
return this.predicate;
}
+ @SuppressWarnings("unchecked")
+ @Override
+ public <S, E> List<Traversal.Admin<S, E>> getLocalChildren() {
+ final List<Traversal.Admin<?, ?>> traversals = new ArrayList<>();
+ P.collectTraversals(this.predicate, traversals);
+ return (List) Collections.unmodifiableList(traversals);
+ }
+
@Override
public IsStep<S> clone() {
final IsStep<S> clone = (IsStep<S>) super.clone();
@@ -69,7 +86,12 @@ public final class IsStep<S> extends FilterStep<S> implements IsStepContract<S>
@Override
public Set<TraverserRequirement> getRequirements() {
- return EnumSet.of(TraverserRequirement.OBJECT);
+ return this.getSelfAndChildRequirements(TraverserRequirement.OBJECT);
}
+ @Override
+ public void setTraversal(final Traversal.Admin<?, ?> parentTraversal) {
+ super.setTraversal(parentTraversal);
+ P.integrateTraversals(this.predicate, this);
+ }
}
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/filter/NoneStep.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/filter/NoneStep.java
index 542d16ca5c..6344e3a2b8 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/filter/NoneStep.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/filter/NoneStep.java
@@ -21,15 +21,19 @@ package org.apache.tinkerpop.gremlin.process.traversal.step.filter;
import org.apache.tinkerpop.gremlin.process.traversal.P;
import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
import org.apache.tinkerpop.gremlin.process.traversal.Traverser;
+import org.apache.tinkerpop.gremlin.process.traversal.step.AcceptsChildPredicateTraversal;
+import org.apache.tinkerpop.gremlin.process.traversal.step.TraversalParent;
import org.apache.tinkerpop.gremlin.process.traversal.traverser.TraverserRequirement;
import org.apache.tinkerpop.gremlin.structure.util.StringFactory;
import org.apache.tinkerpop.gremlin.util.iterator.IteratorUtils;
-import java.util.EnumSet;
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.Iterator;
+import java.util.List;
import java.util.Set;
-public final class NoneStep<S, S2> extends FilterStep<S> {
+public final class NoneStep<S, S2> extends FilterStep<S> implements TraversalParent, AcceptsChildPredicateTraversal {
private P<S2> predicate;
@@ -41,10 +45,18 @@ public final class NoneStep<S, S2> extends FilterStep<S> {
}
this.predicate = predicate;
+ P.integrateTraversals(this.predicate, this);
}
@Override
protected boolean filter(final Traverser.Admin<S> traverser) {
+ if (this.predicate.hasTraversal()) {
+ this.predicate.resolve(traverser);
+ // Vacuously true: if the predicate can't be evaluated (no comparison value),
+ // then no item can match, so none() is satisfied.
+ if (this.predicate.isResolvedEmpty()) return true;
+ }
+
final S item = traverser.get();
if (item instanceof Iterable || item instanceof Iterator || ((item != null) && item.getClass().isArray())) {
@@ -65,6 +77,14 @@ public final class NoneStep<S, S2> extends FilterStep<S> {
return StringFactory.stepString(this, this.predicate);
}
+ @SuppressWarnings("unchecked")
+ @Override
+ public <S, E> List<Traversal.Admin<S, E>> getLocalChildren() {
+ final List<Traversal.Admin<?, ?>> traversals = new ArrayList<>();
+ P.collectTraversals(this.predicate, traversals);
+ return (List) Collections.unmodifiableList(traversals);
+ }
+
@Override
public NoneStep<S, S2> clone() {
final NoneStep<S, S2> clone = (NoneStep<S, S2>) super.clone();
@@ -74,6 +94,12 @@ public final class NoneStep<S, S2> extends FilterStep<S> {
@Override
public Set<TraverserRequirement> getRequirements() {
- return EnumSet.of(TraverserRequirement.OBJECT);
+ return this.getSelfAndChildRequirements(TraverserRequirement.OBJECT);
+ }
+
+ @Override
+ public void setTraversal(final Traversal.Admin<?, ?> parentTraversal) {
+ super.setTraversal(parentTraversal);
+ P.integrateTraversals(this.predicate, this);
}
}
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/filter/WherePredicateStep.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/filter/WherePredicateStep.java
index 77c3f4abc9..6a2a3a5770 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/filter/WherePredicateStep.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/filter/WherePredicateStep.java
@@ -22,6 +22,7 @@ import org.apache.tinkerpop.gremlin.process.traversal.P;
import org.apache.tinkerpop.gremlin.process.traversal.Pop;
import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
import org.apache.tinkerpop.gremlin.process.traversal.Traverser;
+import org.apache.tinkerpop.gremlin.process.traversal.step.AcceptsChildPredicateTraversal;
import org.apache.tinkerpop.gremlin.process.traversal.step.ByModulating;
import org.apache.tinkerpop.gremlin.process.traversal.step.PathProcessor;
import org.apache.tinkerpop.gremlin.process.traversal.step.Scoping;
@@ -45,7 +46,7 @@ import java.util.Set;
/**
* @author Marko A. Rodriguez (http://markorodriguez.com)
*/
-public final class WherePredicateStep<S> extends FilterStep<S> implements Scoping, PathProcessor, ByModulating, TraversalParent {
+public final class WherePredicateStep<S> extends FilterStep<S> implements Scoping, PathProcessor, ByModulating, TraversalParent, AcceptsChildPredicateTraversal {
protected String startKey;
protected List<String> selectKeys;
@@ -54,6 +55,7 @@ public final class WherePredicateStep<S> extends FilterStep<S> implements Scopin
protected Set<String> keepLabels;
protected TraversalRing<S, ?> traversalRing = new TraversalRing<>();
+ private List<Traversal.Admin<?, ?>> predicateTraversals;
public WherePredicateStep(final Traversal.Admin traversal, final Optional<String> startKey, final P<String> predicate) {
super(traversal);
@@ -63,12 +65,19 @@ public final class WherePredicateStep<S> extends FilterStep<S> implements Scopin
this.predicate = (P) predicate;
this.selectKeys = new ArrayList<>();
this.configurePredicates(this.predicate);
+ // Cache and integrate child traversals from the predicate
+ this.predicateTraversals = new ArrayList<>();
+ P.collectTraversals(this.predicate, this.predicateTraversals);
+ for (final Traversal.Admin<?, ?> t : this.predicateTraversals) {
+ this.integrateChild(t);
+ }
}
private void configurePredicates(final P<Object> predicate) {
if (predicate instanceof ConnectiveP)
((ConnectiveP<Object>) predicate).getPredicates().forEach(this::configurePredicates);
- else {
+ else if (!predicate.hasTraversal()) {
+ // Only configure scope keys for non-traversal predicates (string label references)
final String selectKey = getSelectKey(predicate);
this.selectKeys.add(selectKey);
this.scopeKeys.add(selectKey);
@@ -112,14 +121,28 @@ public final class WherePredicateStep<S> extends FilterStep<S> implements Scopin
@Override
protected boolean filter(final Traverser.Admin<S> traverser) {
+ // Resolve the left-hand value (current traverser or startKey scope value)
final TraversalProduct product = null == this.startKey ?
TraversalUtil.produce(traverser, this.traversalRing.next()) :
TraversalUtil.produce((S) this.getSafeScopeValue(Pop.last, this.startKey, traverser), this.traversalRing.next());
- final boolean predicateValuesProductive = this.setPredicateValues(this.predicate, traverser, this.selectKeys.iterator());
- this.traversalRing.reset();
+ if (!product.isProductive()) {
+ this.traversalRing.reset();
+ return false;
+ }
- return product.isProductive() && predicateValuesProductive && this.predicate.test(product.get());
+ if (this.predicate.hasTraversal()) {
+ // Traversal-bearing predicate: resolve the child traversal, then test
+ this.traversalRing.reset();
+ this.predicate.resolve(traverser);
+ if (this.predicate.isResolvedEmpty()) return false;
+ return this.predicate.test(product.get());
+ } else {
+ // Standard scope-label path: resolve predicate values from path labels
+ final boolean predicateValuesProductive = this.setPredicateValues(this.predicate, traverser, this.selectKeys.iterator());
+ this.traversalRing.reset();
+ return predicateValuesProductive && this.predicate.test(product.get());
+ }
}
@Override
@@ -166,7 +189,12 @@ public final class WherePredicateStep<S> extends FilterStep<S> implements Scopin
@Override
public List<Traversal.Admin<S, ?>> getLocalChildren() {
- return (List) this.traversalRing.getTraversals();
+ if (this.predicateTraversals.isEmpty()) {
+ return (List) this.traversalRing.getTraversals();
+ }
+ final List<Traversal.Admin<S, ?>> children = new ArrayList<>((List) this.traversalRing.getTraversals());
+ children.addAll((List) this.predicateTraversals);
+ return children;
}
@Override
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/GraphStep.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/GraphStep.java
index 7e64cd391a..c000c7c164 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/GraphStep.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/GraphStep.java
@@ -24,11 +24,14 @@ import org.apache.tinkerpop.gremlin.process.traversal.Contains;
import org.apache.tinkerpop.gremlin.process.traversal.Step;
import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
import org.apache.tinkerpop.gremlin.process.traversal.Traverser;
+import org.apache.tinkerpop.gremlin.process.traversal.step.AcceptsChildPredicateTraversal;
import org.apache.tinkerpop.gremlin.process.traversal.step.GValue;
+import org.apache.tinkerpop.gremlin.process.traversal.step.TraversalParent;
import org.apache.tinkerpop.gremlin.process.traversal.step.util.AbstractStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.util.HasContainer;
import org.apache.tinkerpop.gremlin.process.traversal.step.util.Parameters;
import org.apache.tinkerpop.gremlin.process.traversal.util.FastNoSuchElementException;
+import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalUtil;
import org.apache.tinkerpop.gremlin.structure.Edge;
import org.apache.tinkerpop.gremlin.structure.Element;
import org.apache.tinkerpop.gremlin.structure.T;
@@ -37,10 +40,12 @@ import org.apache.tinkerpop.gremlin.structure.util.CloseableIterator;
import org.apache.tinkerpop.gremlin.structure.util.StringFactory;
import org.apache.tinkerpop.gremlin.util.iterator.EmptyIterator;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
+import java.util.List;
import java.util.Objects;
import java.util.function.Supplier;
@@ -48,7 +53,7 @@ import java.util.function.Supplier;
* @author Marko A. Rodriguez (http://markorodriguez.com)
* @author Pieter Martin
*/
-public class GraphStep<S, E extends Element> extends AbstractStep<S, E> implements GraphStepContract<S, E> {
+public class GraphStep<S, E extends Element> extends AbstractStep<S, E> implements GraphStepContract<S, E>, TraversalParent, AcceptsChildPredicateTraversal {
protected Parameters parameters = new Parameters();
protected final Class<E> returnClass;
@@ -58,6 +63,7 @@ public class GraphStep<S, E extends Element> extends AbstractStep<S, E> implemen
protected boolean done = false;
private Traverser.Admin<S> head = null;
private Iterator<E> iterator = EmptyIterator.instance();
+ private Traversal.Admin<?, ?> idTraversal;
public GraphStep(final Traversal.Admin traversal, final Class<E> returnClass, final boolean isStart, final Object... ids) {
@@ -76,7 +82,36 @@ public class GraphStep<S, E extends Element> extends AbstractStep<S, E> implemen
this.getTraversal().getGraph().get().edges(this.ids));
}
+ public GraphStep(final Traversal.Admin traversal, final Class<E> returnClass, final boolean isStart, final Traversal.Admin<?, ?> idTraversal) {
+ this(traversal, returnClass, isStart);
+ this.idTraversal = idTraversal;
+ if (this.idTraversal != null) {
+ this.integrateChild(this.idTraversal);
+ }
+ }
+
+ /**
+ * Returns the child traversal used to resolve element IDs, or {@code null} if literal IDs are used.
+ */
+ public Traversal.Admin<?, ?> getIdTraversal() {
+ return this.idTraversal;
+ }
+
+ /**
+ * Sets the child traversal used to resolve element IDs. Calls {@link #integrateChild(Traversal.Admin)}
+ * on the provided traversal.
+ */
+ public void setIdTraversal(final Traversal.Admin<?, ?> idTraversal) {
+ this.idTraversal = idTraversal;
+ if (this.idTraversal != null) {
+ this.integrateChild(this.idTraversal);
+ }
+ }
+
public String toString() {
+ if (this.idTraversal != null) {
+ return StringFactory.stepString(this, this.returnClass.getSimpleName().toLowerCase(), this.idTraversal);
+ }
return StringFactory.stepString(this, this.returnClass.getSimpleName().toLowerCase(), Arrays.toString(this.ids));
}
@@ -164,16 +199,65 @@ public class GraphStep<S, E extends Element> extends AbstractStep<S, E> implemen
if (this.isStart) {
if (this.done)
throw FastNoSuchElementException.instance();
- else {
+ if (this.idTraversal != null) {
+ // Start step with idTraversal: generate a synthetic traverser to seed
+ // the child traversal, consistent with how mergeV/addV handle start steps.
+ this.done = true;
+ final Traverser.Admin<S> syntheticTraverser = (Traverser.Admin<S>)
+ this.getTraversal().getTraverserGenerator().generate(false, (Step) this, 1L);
+ final Object[] resolvedIds = resolveTraversalIds(syntheticTraverser);
+ this.iterator = lookupElements(resolvedIds);
+ } else {
this.done = true;
this.iterator = null == this.iteratorSupplier ? EmptyIterator.instance() : this.iteratorSupplier.get();
}
} else {
this.head = this.starts.next();
- this.iterator = null == this.iteratorSupplier ? EmptyIterator.instance() : this.iteratorSupplier.get();
+ if (this.idTraversal != null) {
+ final Object[] resolvedIds = resolveTraversalIds(this.head);
+ this.iterator = lookupElements(resolvedIds);
+ } else {
+ this.iterator = null == this.iteratorSupplier ? EmptyIterator.instance() : this.iteratorSupplier.get();
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Resolves element IDs by evaluating the child traversal against the current traverser.
+ * Handles Element (extract ID), Collection (unpack), and raw values.
+ */
+ @SuppressWarnings("unchecked")
+ private Object[] resolveTraversalIds(final Traverser.Admin<S> traverser) {
+ final List<Object> ids = new ArrayList<>();
+ TraversalUtil.prepare(traverser, (Traversal.Admin) this.idTraversal);
+ while (this.idTraversal.hasNext()) {
+ final Object result = this.idTraversal.next();
+ if (result instanceof Element) {
+ ids.add(((Element) result).id());
+ } else if (result instanceof Collection) {
+ for (final Object item : (Collection<?>) result) {
+ ids.add(item instanceof Element ? ((Element) item).id() : item);
}
+ } else {
+ ids.add(result);
}
}
+ return ids.toArray();
+ }
+
+ /**
+ * Looks up elements (vertices or edges) by the given IDs using the graph.
+ */
+ @SuppressWarnings("unchecked")
+ private Iterator<E> lookupElements(final Object[] elementIds) {
+ if (elementIds.length == 0) {
+ return EmptyIterator.instance();
+ }
+ return (Iterator<E>) (Vertex.class.isAssignableFrom(this.returnClass) ?
+ this.getTraversal().getGraph().get().vertices(elementIds) :
+ this.getTraversal().getGraph().get().edges(elementIds));
}
@Override
@@ -182,6 +266,9 @@ public class GraphStep<S, E extends Element> extends AbstractStep<S, E> implemen
this.head = null;
this.done = false;
this.iterator = EmptyIterator.instance();
+ if (this.idTraversal != null) {
+ this.idTraversal.reset();
+ }
}
@Override
@@ -195,6 +282,31 @@ public class GraphStep<S, E extends Element> extends AbstractStep<S, E> implemen
return result;
}
+ @SuppressWarnings("unchecked")
+ @Override
+ public <S, E> List<Traversal.Admin<S, E>> getLocalChildren() {
+ return this.idTraversal != null
+ ? Collections.singletonList((Traversal.Admin<S, E>) this.idTraversal)
+ : Collections.emptyList();
+ }
+
+ @Override
+ public GraphStep<S, E> clone() {
+ final GraphStep<S, E> clone = (GraphStep<S, E>) super.clone();
+ if (this.idTraversal != null) {
+ clone.idTraversal = this.idTraversal.clone();
+ }
+ return clone;
+ }
+
+ @Override
+ public void setTraversal(final Traversal.Admin<?, ?> parentTraversal) {
+ super.setTraversal(parentTraversal);
+ if (this.idTraversal != null) {
+ this.integrateChild(this.idTraversal);
+ }
+ }
+
/**
* Attempts to close an underlying iterator if it is of type {@link CloseableIterator}. Graph providers may choose
* to return this interface containing their vertices and edges if there are expensive resources that might need to
@@ -203,6 +315,11 @@ public class GraphStep<S, E extends Element> extends AbstractStep<S, E> implemen
@Override
public void close() {
CloseableIterator.closeIterator(iterator);
+ try {
+ TraversalParent.super.close();
+ } catch (final Exception e) {
+ throw new RuntimeException(e);
+ }
}
/**
@@ -213,6 +330,7 @@ public class GraphStep<S, E extends Element> extends AbstractStep<S, E> implemen
* @return true if the {@link HasContainer} updated ids and thus, was processed.
*/
public static boolean processHasContainerIds(final GraphStep<?, ?> graphStep, final HasContainer hasContainer) {
+ if (hasContainer.hasTraversal()) return false;
final String key = hasContainer.getKey();
if (key != null && key.equals(T.id.getAccessor()) && graphStep.ids.length == 0 &&
(hasContainer.getBiPredicate() == Compare.eq || hasContainer.getBiPredicate() == Contains.within)) {
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/AddPropertyStep.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/AddPropertyStep.java
index 4223f98bd5..7b5acbf44a 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/AddPropertyStep.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/AddPropertyStep.java
@@ -25,6 +25,7 @@ import org.apache.tinkerpop.gremlin.process.traversal.step.util.Parameters;
import org.apache.tinkerpop.gremlin.process.traversal.step.util.event.*;
import org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.EventStrategy;
import org.apache.tinkerpop.gremlin.process.traversal.traverser.TraverserRequirement;
+import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalUtil;
import org.apache.tinkerpop.gremlin.structure.Edge;
import org.apache.tinkerpop.gremlin.structure.Element;
import org.apache.tinkerpop.gremlin.structure.Property;
@@ -35,6 +36,7 @@ import org.apache.tinkerpop.gremlin.structure.util.StringFactory;
import org.apache.tinkerpop.gremlin.structure.util.keyed.KeyedProperty;
import org.apache.tinkerpop.gremlin.structure.util.keyed.KeyedVertexProperty;
+import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
@@ -47,15 +49,34 @@ import java.util.Set;
*/
public class AddPropertyStep<S extends Element> extends SideEffectStep<S> implements AddPropertyStepContract<S> {
+ /**
+ * Internal placeholder key used for the single-argument {@code property(traversal)} (Map-producing) form,
+ * where the property keys are supplied by the traversal's resulting Map rather than by a caller-provided key.
+ * This value is never used for dispatch (see {@link #mapForm}); it exists only to give the parameter a
+ * stable, non-null key within {@link org.apache.tinkerpop.gremlin.process.traversal.step.util.Parameters}.
+ */
+ static final String MAP_VALUE_TRAVERSAL_KEY = "~traversalMap";
+
private Parameters internalParameters = new Parameters();
private Parameters withConfiguration = new Parameters();
private final VertexProperty.Cardinality cardinality;
+ /**
+ * Marks the single-argument {@code property(traversal)} form whose child traversal must produce a Map of
+ * property key/value pairs. Dispatch is driven by this flag rather than by inspecting the property key, so a
+ * caller-supplied key that happens to collide with {@link #MAP_VALUE_TRAVERSAL_KEY} is handled normally.
+ */
+ private final boolean mapForm;
private CallbackRegistry<Event.ElementPropertyChangedEvent> callbackRegistry;
public AddPropertyStep(final Traversal.Admin traversal, final VertexProperty.Cardinality cardinality, final Object keyObject, final Object valueObject) {
+ this(traversal, cardinality, keyObject, valueObject, false);
+ }
+
+ public AddPropertyStep(final Traversal.Admin traversal, final VertexProperty.Cardinality cardinality, final Object keyObject, final Object valueObject, final boolean mapForm) {
super(traversal);
this.internalParameters.set(this, T.key, keyObject, T.value, valueObject);
this.cardinality = cardinality;
+ this.mapForm = mapForm;
}
@Override
@@ -89,11 +110,109 @@ public class AddPropertyStep<S extends Element> extends SideEffectStep<S> implem
throw new IllegalStateException(String.format("T.%s is immutable on existing elements", ((T) k).name()));
final String key = (String) k;
- final Object value = this.internalParameters.get(traverser, T.value, () -> {
- throw new IllegalStateException("The AddPropertyStep does not have a provided value: " + this);
- }).get(0);
+
+ // Check if the raw value is a traversal to enable multi-result handling.
+ // Exclude ConstantTraversal which is used internally by TinkerPop to wrap literal values.
+ final List<Object> rawValues = this.internalParameters.get(T.value, null);
+ final boolean valueIsTraversal = !rawValues.isEmpty()
+ && rawValues.get(0) instanceof Traversal.Admin
+ && !(rawValues.get(0) instanceof ConstantTraversal);
+
+ if (valueIsTraversal) {
+ handleTraversalValue(traverser, key);
+ } else {
+ final Object value = this.internalParameters.get(traverser, T.value, () -> {
+ throw new IllegalStateException("The AddPropertyStep does not have a provided value: " + this);
+ }).get(0);
+ final Object[] vertexPropertyKeyValues = this.internalParameters.getKeyValues(traverser, T.key, T.value);
+ applyPropertyMutation(traverser, key, value, vertexPropertyKeyValues);
+ }
+ }
+
+ /**
+ * Handles the case where the value argument is a child traversal, supporting multi-result
+ * resolution, Map-producing traversals, and cardinality-aware property setting.
+ */
+ private void handleTraversalValue(final Traverser.Admin<S> traverser, final String key) {
+ final List<Object> rawValues = this.internalParameters.get(T.value, null);
+ final Traversal.Admin<?, ?> valueTraversal = (Traversal.Admin<?, ?>) rawValues.get(0);
+
+ // Collect all results from the child traversal
+ final List<Object> results = new ArrayList<>();
+ final Traverser.Admin<S> trav = traverser;
+ final Iterator<?> itty = TraversalUtil.<S, Object>applyAll(trav, (Traversal.Admin<S, Object>) (Traversal.Admin) valueTraversal);
+ while (itty.hasNext()) {
+ results.add(itty.next());
+ }
+
+ // No-result case: skip mutation, pass element through unchanged
+ if (results.isEmpty()) {
+ return;
+ }
+
+ final Element element = traverser.get();
final Object[] vertexPropertyKeyValues = this.internalParameters.getKeyValues(traverser, T.key, T.value);
+ // Map-producing form: single-argument property(traversal) where the traversal produces a Map.
+ // Only the dedicated map form expands a Map into per-entry properties; for the key+traversal form a
+ // Map result is treated as an ordinary property value.
+ if (this.mapForm) {
+ if (results.size() == 1 && results.get(0) instanceof Map) {
+ final Map<?, ?> map = (Map<?, ?>) results.get(0);
+ for (final Map.Entry<?, ?> entry : map.entrySet()) {
+ if (!(entry.getKey() instanceof String)) {
+ throw new IllegalArgumentException(
+ "Property key must be a String but found " + entry.getKey().getClass().getSimpleName());
+ }
+ final String mapKey = (String) entry.getKey();
+ applyPropertyMutation(traverser, mapKey, entry.getValue(), vertexPropertyKeyValues);
+ }
+ return;
+ }
+
+ // The map form requires a Map result. Reject anything else rather than guessing.
+ final Object result = results.get(0);
+ throw new IllegalArgumentException(
+ "property(traversal) requires the traversal to produce a Map, but got: " +
+ (result == null ? "null" : result.getClass().getSimpleName()));
+ }
+
+ // Multi-result handling with cardinality awareness
+ if (results.size() > 1) {
+ // Determine effective cardinality
+ final VertexProperty.Cardinality effectiveCard = this.cardinality != null
+ ? this.cardinality
+ : (element instanceof Vertex ? element.graph().features().vertex().getCardinality(key) : null);
+
+ if (effectiveCard == VertexProperty.Cardinality.single) {
+ throw new IllegalArgumentException(
+ "Single-cardinality property requires exactly one value, but traversal produced " +
+ results.size() + " results");
+ }
+
+ // For list/set cardinality with multiple results, add each as a separate property value
+ if (effectiveCard == VertexProperty.Cardinality.list || effectiveCard == VertexProperty.Cardinality.set) {
+ if (!(element instanceof Vertex)) {
+ throw new IllegalStateException(String.format(
+ "Property cardinality can only be set for a Vertex but the traversal encountered %s for key: %s",
+ element.getClass().getSimpleName(), key));
+ }
+ for (final Object v : results) {
+ applyPropertyMutation(traverser, key, v, vertexPropertyKeyValues);
+ }
+ return;
+ }
+ }
+
+ // Single result or default cardinality: use the first result
+ applyPropertyMutation(traverser, key, results.get(0), vertexPropertyKeyValues);
+ }
+
+ /**
+ * Applies a single property mutation to the element, handling eventing and cardinality.
+ */
+ private void applyPropertyMutation(final Traverser.Admin<S> traverser, final String key,
+ final Object value, final Object[] vertexPropertyKeyValues) {
final Element element = traverser.get();
// can't set cardinality if the element is something other than a vertex as only vertices can have
@@ -116,7 +235,7 @@ public class AddPropertyStep<S extends Element> extends SideEffectStep<S> implem
final VertexProperty.Cardinality card = this.cardinality != null
? this.cardinality
- : element.graph().features().vertex().getCardinality(key);
+ : (element instanceof Vertex ? element.graph().features().vertex().getCardinality(key) : null);
// update property
if (element instanceof Vertex) {
@@ -183,7 +302,7 @@ public class AddPropertyStep<S extends Element> extends SideEffectStep<S> implem
@Override
public int hashCode() {
- final int hash = super.hashCode() ^ this.internalParameters.hashCode() ^ this.withConfiguration.hashCode();
+ int hash = super.hashCode() ^ this.internalParameters.hashCode() ^ this.withConfiguration.hashCode() ^ Boolean.hashCode(this.mapForm);
return (null != this.cardinality) ? (hash ^ cardinality.hashCode()) : hash;
}
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/AddPropertyStepContract.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/AddPropertyStepContract.java
index c6547ad425..70f430db4a 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/AddPropertyStepContract.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/AddPropertyStepContract.java
@@ -19,6 +19,7 @@
package org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect;
import org.apache.tinkerpop.gremlin.process.traversal.Step;
+import org.apache.tinkerpop.gremlin.process.traversal.step.AcceptsChildPredicateTraversal;
import org.apache.tinkerpop.gremlin.process.traversal.step.Configuring;
import org.apache.tinkerpop.gremlin.process.traversal.step.Deleting;
import org.apache.tinkerpop.gremlin.process.traversal.step.GValue;
@@ -33,7 +34,8 @@ import java.util.HashSet;
import java.util.List;
public interface AddPropertyStepContract<S> extends Step<S, S>, TraversalParent, Scoping, PropertiesHolder,
- Writing<Event.ElementPropertyChangedEvent>, Deleting<Event.ElementPropertyChangedEvent>, Configuring {
+ Writing<Event.ElementPropertyChangedEvent>, Deleting<Event.ElementPropertyChangedEvent>, Configuring,
+ AcceptsChildPredicateTraversal {
/**
* Concrete implementations of this contract that can be referenced as TinkerPop implementations.
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/AddPropertyStepPlaceholder.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/AddPropertyStepPlaceholder.java
index 3bb1c087cf..3dd2734cf3 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/AddPropertyStepPlaceholder.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/AddPropertyStepPlaceholder.java
@@ -62,8 +62,18 @@ public class AddPropertyStepPlaceholder<S extends Element> extends SideEffectSte
private Parameters withConfiguration = new Parameters();
+ /**
+ * Marks the single-argument {@code property(traversal)} (Map-producing) form.
+ */
+ private boolean mapForm;
+
public AddPropertyStepPlaceholder(final Traversal.Admin traversal, final VertexProperty.Cardinality cardinality, final Object keyObject, final Object valueObject) {
+ this(traversal, cardinality, keyObject, valueObject, false);
+ }
+
+ public AddPropertyStepPlaceholder(final Traversal.Admin traversal, final VertexProperty.Cardinality cardinality, final Object keyObject, final Object valueObject, final boolean mapForm) {
super(traversal);
+ this.mapForm = mapForm;
if (keyObject instanceof GValue) {
throw new IllegalArgumentException("GValue is not allowed for property keys");
}
@@ -128,13 +138,14 @@ public class AddPropertyStepPlaceholder<S extends Element> extends SideEffectSte
return Objects.equals(key, that.key) &&
Objects.equals(value, that.value) &&
cardinality == that.cardinality &&
+ mapForm == that.mapForm &&
Objects.equals(properties, that.properties) &&
Objects.equals(withConfiguration, that.withConfiguration);
}
@Override
public int hashCode() {
- return Objects.hash(super.hashCode(), key, value, cardinality, properties, withConfiguration);
+ return Objects.hash(super.hashCode(), key, value, cardinality, mapForm, properties, withConfiguration);
}
@Override
@@ -176,6 +187,7 @@ public class AddPropertyStepPlaceholder<S extends Element> extends SideEffectSte
public AddPropertyStepPlaceholder<S> clone() {
final AddPropertyStepPlaceholder<S> clone = (AddPropertyStepPlaceholder<S>) super.clone();
clone.cardinality = cardinality;
+ clone.mapForm = mapForm;
// Attempt to deep clone key for Traversal and GValue. Shallow copy is fine if key is a String or enum
if (this.key instanceof Traversal) {
@@ -218,7 +230,7 @@ public class AddPropertyStepPlaceholder<S extends Element> extends SideEffectSte
@Override
public AddPropertyStep<S> asConcreteStep() {
- AddPropertyStep<S> step = new AddPropertyStep<>(traversal, cardinality, key, value instanceof GValue ? ((GValue<?>) value).get() : value);
+ AddPropertyStep<S> step = new AddPropertyStep<>(traversal, cardinality, key, value instanceof GValue ? ((GValue<?>) value).get() : value, mapForm);
for (final Map.Entry<Object, List<Object>> entry : properties.entrySet()) {
for (Object value : entry.getValue()) {
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/HasContainer.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/HasContainer.java
index 99a9c4e959..c6d0d1fe29 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/HasContainer.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/HasContainer.java
@@ -110,7 +110,7 @@ public class HasContainer implements Serializable, Cloneable, Predicate<Element>
public HasContainer clone() {
try {
final HasContainer clone = (HasContainer) super.clone();
- clone.predicate = this.predicate.clone();
+ clone.predicate = this.predicate != null ? this.predicate.clone() : null;
return clone;
} catch (final CloneNotSupportedException e) {
throw new IllegalStateException(e.getMessage(), e);
@@ -142,6 +142,14 @@ public class HasContainer implements Serializable, Cloneable, Predicate<Element>
return this.predicate.getValue();
}
+ /**
+ * Determines if this {@code HasContainer}'s predicate holds a child traversal whose result is resolved
+ * at runtime (e.g. {@code P.eq(traversal)} or {@code P.within(traversal)}).
+ */
+ public boolean hasTraversal() {
+ return this.predicate != null && this.predicate.hasTraversal();
+ }
+
////////////
/**
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/strategy/optimization/InlineFilterStrategy.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/strategy/optimization/InlineFilterStrategy.java
index 8a6edcde28..d8c260d87f 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/strategy/optimization/InlineFilterStrategy.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/strategy/optimization/InlineFilterStrategy.java
@@ -114,6 +114,10 @@ public final class InlineFilterStrategy extends AbstractTraversalStrategy<Traver
///////////////////////////
private static boolean processHasStep(final HasStep<?> step, final Traversal.Admin<?, ?> traversal) {
+ if (step.getHasContainers().stream().anyMatch(HasContainer::hasTraversal)) return false;
+ if (step.getPreviousStep() instanceof HasStep) {
+ if (((HasStep<?>) step.getPreviousStep()).getHasContainers().stream().anyMatch(HasContainer::hasTraversal)) return false;
+ }
if (step.getPreviousStep() instanceof HasStep) {
final HasStep<?> previousStep = (HasStep<?>) step.getPreviousStep();
final List<HasContainer> hasContainers = new ArrayList<>(step.getHasContainers());
@@ -221,6 +225,11 @@ public final class InlineFilterStrategy extends AbstractTraversalStrategy<Traver
InlineFilterStrategy.instance().apply(childTraversal);
for (final Step<?, ?> childStep : childTraversal.getSteps()) {
if (childStep instanceof HasStep) {
+ // Skip optimization if any HasContainer has a traversal-bearing predicate
+ if (((HasStep<?>) childStep).getHasContainers().stream().anyMatch(HasContainer::hasTraversal)) {
+ process = false;
+ break;
+ }
P p = null;
for (final HasContainer hasContainer : ((HasStep<?>) childStep).getHasContainers()) {
if (null == key)
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/strategy/verification/ChildTraversalVerificationStrategy.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/strategy/verification/ChildTraversalVerificationStrategy.java
new file mode 100644
index 0000000000..240cc08a85
--- /dev/null
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/strategy/verification/ChildTraversalVerificationStrategy.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.tinkerpop.gremlin.process.traversal.strategy.verification;
+
+import org.apache.tinkerpop.gremlin.process.traversal.Step;
+import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
+import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategy;
+import org.apache.tinkerpop.gremlin.process.traversal.step.AcceptsChildPredicateTraversal;
+import org.apache.tinkerpop.gremlin.process.traversal.step.TraversalParent;
+import org.apache.tinkerpop.gremlin.process.traversal.strategy.AbstractTraversalStrategy;
+import org.apache.tinkerpop.gremlin.process.traversal.util.ChildTraversalValidator;
+
+/**
+ * Validates that child traversals in filter, lookup, and mutation steps do not contain mutating steps.
+ * Serves as a safety net for programmatic traversal construction that bypasses the DSL's
+ * construction-time validation.
+ */
+public final class ChildTraversalVerificationStrategy
+ extends AbstractTraversalStrategy<TraversalStrategy.VerificationStrategy>
+ implements TraversalStrategy.VerificationStrategy {
+
+ private static final ChildTraversalVerificationStrategy INSTANCE = new ChildTraversalVerificationStrategy();
+
+ private ChildTraversalVerificationStrategy() {
+ }
+
+ @Override
+ public void apply(final Traversal.Admin<?, ?> traversal) {
+ for (final Step<?, ?> step : traversal.getSteps()) {
+ if (step instanceof AcceptsChildPredicateTraversal && step instanceof TraversalParent) {
+ for (final Traversal.Admin<?, ?> child : ((TraversalParent) step).getLocalChildren()) {
+ try {
+ ChildTraversalValidator.validate(child);
+ } catch (final IllegalArgumentException e) {
+ throw new VerificationException(e.getMessage(), traversal);
+ }
+ }
+ }
+ }
+ }
+
+ public static ChildTraversalVerificationStrategy instance() {
+ return INSTANCE;
+ }
+}
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/util/ChildTraversalValidator.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/util/ChildTraversalValidator.java
new file mode 100644
index 0000000000..5e54d6d599
--- /dev/null
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/util/ChildTraversalValidator.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.tinkerpop.gremlin.process.traversal.util;
+
+import org.apache.tinkerpop.gremlin.process.traversal.Step;
+import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
+import org.apache.tinkerpop.gremlin.process.traversal.step.Mutating;
+import org.apache.tinkerpop.gremlin.process.traversal.step.TraversalParent;
+
+/**
+ * Validates that child traversals do not contain mutating steps. Child traversals used as
+ * arguments to filter predicates ({@code has()}, {@code is()}, etc.), lookup steps
+ * ({@code V(traversal)}, {@code E(traversal)}), and mutation steps ({@code property(traversal)})
+ * must be read-only - their purpose is to compute values, not produce side effects.
+ * <p>
+ * Recursion walks both {@link TraversalParent#getLocalChildren()} and
+ * {@link TraversalParent#getGlobalChildren()} to detect mutations nested inside
+ * {@code map()}, {@code union()}, {@code choose()}, {@code coalesce()}, or any other parent step.
+ */
+public final class ChildTraversalValidator {
+
+ private ChildTraversalValidator() {
+ }
+
+ /**
+ * Validates that a child traversal contains no {@link Mutating} steps at any nesting depth.
+ * Throws {@link IllegalArgumentException} if one is found.
+ */
+ public static void validate(final Traversal.Admin<?, ?> child) {
+ for (final Step<?, ?> step : child.getSteps()) {
+ if (step instanceof Mutating) {
+ throw new IllegalArgumentException(
+ "Child traversal contains mutating step " +
+ step.getClass().getSimpleName() + ". Mutating steps are not allowed in child traversals.");
+ }
+ if (step instanceof TraversalParent) {
+ for (final Traversal.Admin<?, ?> nested : ((TraversalParent) step).getLocalChildren()) {
+ validate(nested);
+ }
+ for (final Traversal.Admin<?, ?> nested : ((TraversalParent) step).getGlobalChildren()) {
+ validate(nested);
+ }
+ }
+ }
+ }
+}
diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/step/CloneIndependenceTraversalTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/step/CloneIndependenceTraversalTest.java
new file mode 100644
index 0000000000..09af6da179
--- /dev/null
+++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/step/CloneIndependenceTraversalTest.java
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.tinkerpop.gremlin.process.traversal.step;
+
+import org.apache.tinkerpop.gremlin.process.traversal.P;
+import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
+import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__;
+import org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasStep;
+import org.apache.tinkerpop.gremlin.process.traversal.step.map.GraphStep;
+import org.apache.tinkerpop.gremlin.process.traversal.step.util.HasContainer;
+import org.apache.tinkerpop.gremlin.process.traversal.util.EmptyTraversal;
+import org.apache.tinkerpop.gremlin.structure.Vertex;
+import org.junit.Test;
+
+import java.util.List;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.IsSame.sameInstance;
+
+/**
+ * Property 8: Clone independence for traversal-bearing steps.
+ * <p>
+ * For any step containing child traversal arguments, cloning the step SHALL produce a deep copy
+ * where modifying the clone's child traversal state does NOT affect the original step's child
+ * traversal state.
+ * <p>
+ * <b>Validates: Requirements 6.2</b>
+ */
+public class CloneIndependenceTraversalTest {
+
+ @Test
+ public void shouldProduceIndependentCloneForHasStepWithTraversalValue() {
+ // Create a HasStep with a traversal-bearing HasContainer (P.eq(traversal))
+ final Traversal.Admin<?, ?> childTraversal = __.constant("marko").asAdmin();
+ final HasContainer hc = new HasContainer("name", P.eq(childTraversal));
+ final HasStep<Vertex> original = new HasStep<>(EmptyTraversal.instance(), hc);
+
+ // Clone the step
+ final HasStep<Vertex> clone = original.clone();
+
+ // Verify the clone has child traversals
+ final List<Traversal.Admin<?, ?>> originalChildren = (List) original.getLocalChildren();
+ final List<Traversal.Admin<?, ?>> cloneChildren = (List) clone.getLocalChildren();
+
+ assertThat(originalChildren.size(), is(1));
+ assertThat(cloneChildren.size(), is(1));
+
+ // The child traversals should be different instances (deep copy)
+ assertThat(cloneChildren.get(0), is(not(sameInstance(originalChildren.get(0)))));
+
+ // Verify HasContainers are also independent
+ final List<HasContainer> originalContainers = original.getHasContainers();
+ final List<HasContainer> cloneContainers = clone.getHasContainers();
+ assertThat(cloneContainers.get(0), is(not(sameInstance(originalContainers.get(0)))));
+ assertThat(cloneContainers.get(0).getPredicate().getTraversalValue(),
+ is(not(sameInstance(originalContainers.get(0).getPredicate().getTraversalValue()))));
+ }
+
+ @Test
+ public void shouldProduceIndependentCloneForGraphStepWithIdTraversal() {
+ // Create a GraphStep with an idTraversal
+ final Traversal.Admin<?, ?> idTraversal = __.select("ids").asAdmin();
+ final GraphStep<Vertex, Vertex> original =
+ new GraphStep<>(EmptyTraversal.instance(), Vertex.class, false, idTraversal);
+
+ // Clone the step
+ final GraphStep<Vertex, Vertex> clone = original.clone();
+
+ // Verify the clone has a child traversal
+ assertThat(original.getIdTraversal(), is(notNullValue()));
+ assertThat(clone.getIdTraversal(), is(notNullValue()));
+
+ // The idTraversal should be a different instance (deep copy)
+ assertThat(clone.getIdTraversal(), is(not(sameInstance(original.getIdTraversal()))));
+ }
+
+ @Test
+ public void shouldProduceIndependentCloneForHasStepWithMultipleContainers() {
+ // Create a HasStep with multiple HasContainers, some with traversals
+ final HasContainer hc1 = new HasContainer("name", P.eq(__.constant("marko").asAdmin()));
+ final HasContainer hc2 = new HasContainer("age", P.eq(__.constant(29).asAdmin()));
+ final HasStep<Vertex> original = new HasStep<>(EmptyTraversal.instance(), hc1, hc2);
+
+ // Clone the step
+ final HasStep<Vertex> clone = original.clone();
+
+ // Verify all containers are independent
+ final List<HasContainer> originalContainers = original.getHasContainers();
+ final List<HasContainer> cloneContainers = clone.getHasContainers();
+
+ assertThat(originalContainers.size(), is(2));
+ assertThat(cloneContainers.size(), is(2));
+
+ for (int i = 0; i < originalContainers.size(); i++) {
+ assertThat(cloneContainers.get(i), is(not(sameInstance(originalContainers.get(i)))));
+ assertThat(cloneContainers.get(i).getPredicate().getTraversalValue(),
+ is(not(sameInstance(originalContainers.get(i).getPredicate().getTraversalValue()))));
+ }
+ }
+
+}
diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/HasContainerTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/HasContainerTest.java
new file mode 100644
index 0000000000..4049b20eb6
--- /dev/null
+++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/HasContainerTest.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.tinkerpop.gremlin.process.traversal.step.util;
+
+import org.apache.tinkerpop.gremlin.process.traversal.P;
+import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
+import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__;
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.IsNot.not;
+import static org.hamcrest.core.IsSame.sameInstance;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Tests for {@link HasContainer}, including traversal-bearing predicates and lifecycle methods.
+ * A traversal-bearing container holds a {@link P} whose value is resolved at runtime from a child traversal
+ * (e.g. {@code P.eq(traversal)}); the container exposes this via {@link HasContainer#hasTraversal()}.
+ */
+public class HasContainerTest {
+
+ @Test
+ public void shouldReturnFalseForHasTraversalWithLiteralPredicate() {
+ final HasContainer hc = new HasContainer("name", P.eq("marko"));
+ assertThat(hc.hasTraversal(), is(false));
+ }
+
+ @Test
+ public void shouldReturnTrueForHasTraversalWithTraversalPredicate() {
+ final Traversal.Admin<?, ?> traversal = __.identity().asAdmin();
+ final HasContainer hc = new HasContainer("name", P.eq(traversal));
+ assertThat(hc.hasTraversal(), is(true));
+ }
+
+ @Test
+ public void shouldSetFieldsCorrectlyWithTraversalPredicate() {
+ final Traversal.Admin<?, ?> traversal = __.identity().asAdmin();
+ final HasContainer hc = new HasContainer("age", P.eq(traversal));
+
+ assertEquals("age", hc.getKey());
+ assertThat(hc.getPredicate(), is(notNullValue()));
+ assertThat(hc.getPredicate().getTraversalValue(), is(sameInstance(traversal)));
+ }
+
+ @Test
+ public void shouldCloneProduceIndependentDeepCopyOfTraversalPredicate() {
+ final Traversal.Admin<?, ?> traversal = __.identity().asAdmin();
+ final HasContainer original = new HasContainer("name", P.eq(traversal));
+ final HasContainer clone = original.clone();
+
+ // clone's predicate should carry a traversal that is not the same instance
+ assertThat(clone.getPredicate().getTraversalValue(), is(notNullValue()));
+ assertThat(clone.getPredicate().getTraversalValue(),
+ is(not(sameInstance(original.getPredicate().getTraversalValue()))));
+
+ // key should be equal
+ assertEquals(original.getKey(), clone.getKey());
+
+ // both should still report hasTraversal
+ assertThat(clone.hasTraversal(), is(true));
+ }
+
+ @Test
+ public void shouldCloneLiteralHasContainerWithoutTraversal() {
+ final HasContainer original = new HasContainer("name", P.eq("marko"));
+ final HasContainer clone = original.clone();
+
+ assertThat(clone.hasTraversal(), is(false));
+ assertEquals("name", clone.getKey());
+ assertEquals(P.eq("marko"), clone.getPredicate());
+ }
+}
diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/strategy/verification/ChildTraversalVerificationStrategyTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/strategy/verification/ChildTraversalVerificationStrategyTest.java
new file mode 100644
index 0000000000..feb0688283
--- /dev/null
+++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/strategy/verification/ChildTraversalVerificationStrategyTest.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.tinkerpop.gremlin.process.traversal.strategy.verification;
+
+import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
+import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategies;
+import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
+import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__;
+import org.apache.tinkerpop.gremlin.structure.Graph;
+import org.apache.tinkerpop.gremlin.structure.util.empty.EmptyGraph;
+import org.junit.Test;
+
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
+
+/**
+ * Tests for the ChildTraversalVerificationStrategy (strategy-time layer).
+ */
+public class ChildTraversalVerificationStrategyTest {
+
+ private final GraphTraversalSource g = EmptyGraph.instance().traversal();
+
+ @Test
+ public void shouldBeRegisteredByDefault() {
+ // The strategy should be present in the default Graph strategy set (not EmptyGraph which has none)
+ assertTrue(TraversalStrategies.GlobalCache
+ .getStrategies(Graph.class)
+ .getStrategy(ChildTraversalVerificationStrategy.class).isPresent());
+ }
+
+ @Test
+ public void shouldBeRemovableViaWithoutStrategies() {
+ // Users should be able to remove the strategy if needed
+ final GraphTraversalSource gNoVerify = g.withoutStrategies(ChildTraversalVerificationStrategy.class);
+ assertFalse(gNoVerify.getStrategies().getStrategy(ChildTraversalVerificationStrategy.class).isPresent());
+ }
+
+ @Test
+ public void shouldNotRejectValidReadOnlyTraversal() {
+ // Valid traversal should pass strategy application without error
+ final Traversal.Admin<?, ?> traversal = g.V().has("name", __.V().values("name")).asAdmin();
+ traversal.applyStrategies();
+ // If we get here without exception, the test passes
+ }
+
+ @Test
+ public void shouldNotRejectValidLookupTraversal() {
+ // Valid V(traversal) should pass
+ final Traversal.Admin<?, ?> traversal = g.V().V(__.out("knows").id()).asAdmin();
+ traversal.applyStrategies();
+ }
+}
diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/util/ChildTraversalValidatorTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/util/ChildTraversalValidatorTest.java
new file mode 100644
index 0000000000..68b99296ec
--- /dev/null
+++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/util/ChildTraversalValidatorTest.java
@@ -0,0 +1,229 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.tinkerpop.gremlin.process.traversal.util;
+
+import org.apache.tinkerpop.gremlin.process.traversal.P;
+import org.apache.tinkerpop.gremlin.process.traversal.TextP;
+import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
+import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__;
+import org.apache.tinkerpop.gremlin.structure.util.empty.EmptyGraph;
+import org.junit.Test;
+
+import static org.junit.Assert.assertThrows;
+
+/**
+ * Tests for construction-time child traversal validation.
+ */
+public class ChildTraversalValidatorTest {
+
+ private final GraphTraversalSource g = EmptyGraph.instance().traversal();
+
+ // ===== FILTER CONTEXT: should reject mutating steps =====
+
+ @Test
+ public void shouldRejectAddVInHasTraversal() {
+ assertThrows(IllegalArgumentException.class, () ->
+ g.V().has("name", __.addV("x").values("name")));
+ }
+
+ @Test
+ public void shouldRejectDropInHasTraversal() {
+ assertThrows(IllegalArgumentException.class, () ->
+ g.V().has("name", __.V().drop().constant("x")));
+ }
+
+ @Test
+ public void shouldRejectNestedMutatingInHasTraversal() {
+ assertThrows(IllegalArgumentException.class, () ->
+ g.V().has("name", __.V().map(__.addV("x")).values("name")));
+ }
+
+ @Test
+ public void shouldRejectMutatingInHasWithTAccessor() {
+ assertThrows(IllegalArgumentException.class, () ->
+ g.V().has(org.apache.tinkerpop.gremlin.structure.T.label, __.addV("x").label()));
+ }
+
+ @Test
+ public void shouldRejectMutatingInHasWithLabel() {
+ assertThrows(IllegalArgumentException.class, () ->
+ g.V().has("person", "name", __.addV("x").values("name")));
+ }
+
+ // ===== LOOKUP CONTEXT: should reject mutating steps =====
+
+ @Test
+ public void shouldRejectAddVInMidTraversalV() {
+ assertThrows(IllegalArgumentException.class, () ->
+ g.V().V(__.addV("x").id()));
+ }
+
+ @Test
+ public void shouldRejectAddVInMidTraversalE() {
+ assertThrows(IllegalArgumentException.class, () ->
+ g.V().E(__.addV("x").id()));
+ }
+
+ @Test
+ public void shouldRejectAddVInStartStepV() {
+ assertThrows(IllegalArgumentException.class, () ->
+ g.V(__.addV("x").id()));
+ }
+
+ @Test
+ public void shouldRejectAddVInStartStepE() {
+ assertThrows(IllegalArgumentException.class, () ->
+ g.E(__.addV("x").id()));
+ }
+
+ // ===== P FACTORY METHODS: should reject mutating steps =====
+
+ @Test
+ public void shouldRejectMutatingInPEq() {
+ assertThrows(IllegalArgumentException.class, () ->
+ P.eq(__.addV("x").values("name")));
+ }
+
+ @Test
+ public void shouldRejectMutatingInPNeq() {
+ assertThrows(IllegalArgumentException.class, () ->
+ P.neq(__.addV("x").values("name")));
+ }
+
+ @Test
+ public void shouldRejectMutatingInPGt() {
+ assertThrows(IllegalArgumentException.class, () ->
+ P.gt(__.addV("x").values("age")));
+ }
+
+ @Test
+ public void shouldRejectMutatingInPLt() {
+ assertThrows(IllegalArgumentException.class, () ->
+ P.lt(__.addV("x").values("age")));
+ }
+
+ @Test
+ public void shouldRejectMutatingInPGte() {
+ assertThrows(IllegalArgumentException.class, () ->
+ P.gte(__.addV("x").values("age")));
+ }
+
+ @Test
+ public void shouldRejectMutatingInPLte() {
+ assertThrows(IllegalArgumentException.class, () ->
+ P.lte(__.addV("x").values("age")));
+ }
+
+ @Test
+ public void shouldRejectMutatingInPWithin() {
+ assertThrows(IllegalArgumentException.class, () ->
+ P.within(__.addV("x").values("name")));
+ }
+
+ @Test
+ public void shouldRejectMutatingInPWithout() {
+ assertThrows(IllegalArgumentException.class, () ->
+ P.without(__.addV("x").values("name")));
+ }
+
+ @Test
+ public void shouldRejectMutatingInMultiTraversalWithin() {
+ assertThrows(IllegalArgumentException.class, () ->
+ P.within(__.V().values("name"), __.addV("x").values("name")));
+ }
+
+ @Test
+ public void shouldRejectMutatingInMultiTraversalWithout() {
+ assertThrows(IllegalArgumentException.class, () ->
+ P.without(__.V().values("name"), __.addV("x").values("name")));
+ }
+
+ // ===== TextP FACTORY METHODS: should reject mutating steps =====
+
+ @Test
+ public void shouldRejectMutatingInTextPStartingWith() {
+ assertThrows(IllegalArgumentException.class, () ->
+ TextP.startingWith(__.addV("x").values("name")));
+ }
+
+ @Test
+ public void shouldRejectMutatingInTextPContaining() {
+ assertThrows(IllegalArgumentException.class, () ->
+ TextP.containing(__.addV("x").values("name")));
+ }
+
+ // ===== MUTATION CONTEXT: should reject DropStep but allow other mutations =====
+
+ @Test
+ public void shouldRejectDropInPropertyTraversal() {
+ assertThrows(IllegalArgumentException.class, () ->
+ g.V().property(__.V().map(__.drop()).project("x").by("name")));
+ }
+
+ @Test
+ public void shouldRejectNestedDropInPropertyTraversal() {
+ assertThrows(IllegalArgumentException.class, () ->
+ g.V().property(__.V().union(__.drop(), __.constant("x")).project("k").by()));
+ }
+
+ // ===== VALID TRAVERSALS: should pass without error =====
+
+ @Test
+ public void shouldAllowReadOnlyHasTraversal() {
+ // Should not throw
+ g.V().has("name", __.V().values("name"));
+ }
+
+ @Test
+ public void shouldAllowNavigationInHasTraversal() {
+ // Should not throw
+ g.V().has("name", __.V().out("knows").values("name"));
+ }
+
+ @Test
+ public void shouldAllowReadOnlyLookupTraversal() {
+ // Should not throw
+ g.V().V(__.out("knows").id());
+ }
+
+ @Test
+ public void shouldAllowReadOnlyStartStepV() {
+ // Should not throw
+ g.V(__.V().id());
+ }
+
+ @Test
+ public void shouldAllowReadOnlyPredicate() {
+ // Should not throw
+ P.eq(__.V().values("age"));
+ }
+
+ @Test
+ public void shouldRejectAddVInMutationContext() {
+ // addV is now blocked in property(traversal) - all mutating steps are blocked in all contexts
+ assertThrows(IllegalArgumentException.class, () ->
+ g.V().property(__.V().addV("temp").project("k").by("name")));
+ }
+
+ @Test
+ public void shouldAllowReadOnlyPropertyTraversal() {
+ // Should not throw
+ g.V().property(__.V().project("name").by("name"));
+ }
+}
diff --git a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/sideEffect/TinkerGraphStep.java b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/sideEffect/TinkerGraphStep.java
index a950daeefe..fc7c64b060 100644
--- a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/sideEffect/TinkerGraphStep.java
+++ b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/sideEffect/TinkerGraphStep.java
@@ -59,6 +59,14 @@ public final class TinkerGraphStep<S, E extends Element> extends GraphStep<S, E>
super(originalGraphStep.getTraversal(), originalGraphStep.getReturnClass(), originalGraphStep.isStartStep(), originalGraphStep.getIds());
originalGraphStep.getLabels().forEach(this::addLabel);
+ // preserve the idTraversal if the original GraphStep had one
+ if (originalGraphStep instanceof GraphStep) {
+ final GraphStep<S, E> gs = (GraphStep<S, E>) originalGraphStep;
+ if (gs.getIdTraversal() != null) {
+ this.setIdTraversal(gs.getIdTraversal());
+ }
+ }
+
// we used to only setIteratorSupplier() if there were no ids OR the first id was instanceof Element,
// but that allowed the filter in g.V(v).has('k','v') to be ignored. this created problems for
// PartitionStrategy which wants to prevent someone from passing "v" from one TraversalSource to
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 c9e19593a7..947cdcefa3 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
@@ -54,8 +54,18 @@ public final class TinkerGraphStepStrategy extends AbstractTraversalStrategy<Tra
Step<?, ?> currentStep = tinkerGraphStep.getNextStep();
while (currentStep instanceof HasStep || currentStep instanceof NoOpBarrierStep) {
if (currentStep instanceof HasStep) {
- List<HasContainer> hasContainers = ((HasContainerHolder) currentStep).getHasContainers();
- for (HasContainer hasContainer : hasContainers) {
+ final List<HasContainer> hasContainers = ((HasContainerHolder) currentStep).getHasContainers();
+
+ // 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)) {
+ currentStep = currentStep.getNextStep();
+ continue;
+ }
+
+ for (final HasContainer hasContainer : hasContainers) {
if (!GraphStep.processHasContainerIds(tinkerGraphStep, hasContainer))
tinkerGraphStep.addHasContainer(hasContainer);
}
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
new file mode 100644
index 0000000000..a91e89ea52
--- /dev/null
+++ b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/strategy/optimization/TinkerGraphStepStrategyTraversalTest.java
@@ -0,0 +1,205 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.tinkerpop.gremlin.tinkergraph.process.traversal.strategy.optimization;
+
+import org.apache.tinkerpop.gremlin.process.traversal.Step;
+import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
+import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategies;
+import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal;
+import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
+import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__;
+import org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasStep;
+import org.apache.tinkerpop.gremlin.process.traversal.step.util.HasContainer;
+import org.apache.tinkerpop.gremlin.process.traversal.util.DefaultTraversalStrategies;
+import org.apache.tinkerpop.gremlin.structure.Graph;
+import org.apache.tinkerpop.gremlin.structure.Vertex;
+import org.apache.tinkerpop.gremlin.tinkergraph.process.traversal.step.sideEffect.TinkerGraphStep;
+import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerGraph;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.List;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.greaterThanOrEqualTo;
+import static org.hamcrest.Matchers.hasSize;
+
+/**
+ * Property 9: Traversal-bearing HasContainers are not folded into GraphStep.
+ * <p>
+ * For any traversal of the form {@code g.V().has(key, traversal)} or {@code g.V().has(key, P.eq(traversal))},
+ * after applying {@link TinkerGraphStepStrategy}, the {@link HasStep} SHALL remain as a separate step in the
+ * traversal plan and SHALL NOT be folded into the {@link TinkerGraphStep}.
+ * <p>
+ * <b>Validates: Requirements 8.1, 8.2, 8.3</b>
+ */
+public class TinkerGraphStepStrategyTraversalTest {
+
+ private Graph graph;
+ private GraphTraversalSource g;
+
+ @Before
+ public void setup() {
+ graph = TinkerGraph.open();
+ g = graph.traversal();
+ }
+
+ @After
+ public void teardown() throws Exception {
+ graph.close();
+ }
+
+ /**
+ * Applies TinkerGraphStepStrategy to the given traversal and returns the step list.
+ */
+ private List<Step> applyStrategy(final Traversal.Admin<?, ?> traversal) {
+ final TraversalStrategies strategies = new DefaultTraversalStrategies();
+ strategies.addStrategies(TinkerGraphStepStrategy.instance());
+ traversal.setStrategies(strategies);
+ traversal.applyStrategies();
+ return traversal.getSteps();
+ }
+
+ @Test
+ public void shouldNotFoldTraversalBearingHasContainerIntoGraphStep() {
+ // g.V().has("name", __.constant("marko")) - traversal-bearing HasContainer
+ final GraphTraversal<Vertex, Vertex> traversal = g.V().has("name", __.constant("marko"));
+ final List<Step> steps = applyStrategy(traversal.asAdmin());
+
+ // The traversal plan should contain at least 2 steps:
+ // TinkerGraphStep (replacing GraphStep) and HasStep (not folded)
+ assertThat(steps, hasSize(greaterThanOrEqualTo(2)));
+ assertThat(steps.get(0), instanceOf(TinkerGraphStep.class));
+
+ // The HasStep should remain as a separate step
+ final boolean hasStepPresent = steps.stream().anyMatch(s -> s instanceof HasStep);
+ assertThat("HasStep with traversal-bearing HasContainer should NOT be folded into TinkerGraphStep",
+ hasStepPresent, is(true));
+
+ // The TinkerGraphStep should have no folded HasContainers
+ final TinkerGraphStep<?, ?> tinkerGraphStep = (TinkerGraphStep<?, ?>) steps.get(0);
+ assertThat("TinkerGraphStep should have no folded HasContainers for traversal-bearing has()",
+ tinkerGraphStep.getHasContainers(), hasSize(0));
+ }
+
+ @Test
+ public void shouldStillFoldLiteralHasContainerIntoGraphStep() {
+ // g.V().has("name", "marko") - literal HasContainer should still be folded
+ final GraphTraversal<Vertex, Vertex> traversal = g.V().has("name", "marko");
+ final List<Step> steps = applyStrategy(traversal.asAdmin());
+
+ // After folding, the HasStep should be absorbed into TinkerGraphStep
+ assertThat(steps, hasSize(1));
+ assertThat(steps.get(0), instanceOf(TinkerGraphStep.class));
+
+ // The TinkerGraphStep should have the folded HasContainer
+ final TinkerGraphStep<?, ?> tinkerGraphStep = (TinkerGraphStep<?, ?>) steps.get(0);
+ assertThat(tinkerGraphStep.getHasContainers(), hasSize(1));
+ }
+
+ @Test
+ public void shouldNotFoldPredicateWithTraversalIntoGraphStep() {
+ // g.V().has("name", P.eq(__.constant("marko"))) - predicate with traversal
+ final GraphTraversal<Vertex, Vertex> traversal =
+ g.V().has("name", org.apache.tinkerpop.gremlin.process.traversal.P.eq(__.constant("marko").asAdmin()));
+ final List<Step> steps = applyStrategy(traversal.asAdmin());
+
+ // The HasStep should remain as a separate step
+ assertThat(steps, hasSize(greaterThanOrEqualTo(2)));
+ assertThat(steps.get(0), instanceOf(TinkerGraphStep.class));
+
+ final boolean hasStepPresent = steps.stream().anyMatch(s -> s instanceof HasStep);
+ assertThat("HasStep with P.eq(traversal) should NOT be folded into TinkerGraphStep",
+ hasStepPresent, is(true));
+ }
+
+ @Test
+ public void shouldFoldLiteralHasButNotTraversalHasInMixedTraversal() {
+ // g.V().has("name", "marko").has("age", __.constant(29))
+ // 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("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);
+ assertThat("Traversal-bearing HasStep should remain as separate step",
+ 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));
+ }
+
+}