Skip to content

Allow Steps to Accept Traversal#3458

Open
xiazcy wants to merge 40 commits into
masterfrom
steps-taking-traversal
Open

Allow Steps to Accept Traversal#3458
xiazcy wants to merge 40 commits into
masterfrom
steps-taking-traversal

Conversation

@xiazcy

@xiazcy xiazcy commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Allow child traversals as arguments to has(), is(), V(), E(), property(), where(P), and all P/TextP predicates. The child traversal is evaluated per-traverser at runtime, enabling dynamic value resolution.

JIRA:

  • TINKERPOP-2586: Traversal objects inside within() silently return empty instead of executing or erroring - fully addressed (traversals are now executed)
  • TINKERPOP-2777: V() cannot accept traversals like select() for data-driven lookups - fully addressed
  • TINKERPOP-3005: property() should accept a traversal producing a Map - fully addressed
  • TINKERPOP-1463: has(key, traversal) doesn't work as expected - partially addressed (dynamic filtering works; runtime folding into index lookups is deferred as a future optimization)

What it enables

// with Modern graph
// Data-driven vertex lookup (TINKERPOP-2777)
gremlin> g.inject(['1','2','3']).as('a').V(__.select('a')).values('name')
==>marko
==>vadas
==>lop

// Dynamic filtering - find people older than marko
gremlin> g.V().has('age', P.gt(__.V(1).values('age'))).values('name')
==>josh
==>peter

// Computed property mutation (TINKERPOP-3005) - set multiple properties from a Map
gremlin> g.V(4).property(__.V(1).project('friendCount','createdSoftware').by(__.out('knows').count()).by(__.out('created').values('name')))
==>v[4]
gremlin> g.V(4).valueMap('friendCount','createdSoftware')
==>[friendCount:[2],createdSoftware:[lop]]

// with AirRoutes graph
// Find airports where the city name ends with the airport code (lowercased)
gremlin> g.V().has("city", TextP.endingWith(__.values("code").toLower())).limit(10).values("code","city").fold()
==>[CUN, Cancun, ORK, Cork, OME, Nome, IEV, Kiev, AJU, Aracaju, IRA, Kirakira, OHE, Mohe, RUA, Arua, VAS, Sivas, AZD, Yazd]

Review Guide

This is a large change (68 files, ~7k lines) but structured in layers. Review bottom-up:

# Commit Layer What to look for
1 5f37d11 Predicate resolution Core: P.resolve(), empty-result semantics, AndP short-circuit, ConnectiveP/NotP delegation
2 6a547d2 Step implementations How steps use P.resolve(). HasStep single path, GraphStep idTraversal, AddPropertyStep mapForm, AcceptsChildPredicateTraversal marker
3 e80423a Grammar Gremlin.g4 nestedTraversal alternatives. Visitors delegate to API (not addStep())
4 1b21d15 GLVs within()/without() serialization. .NET typed overloads
5 793e7a8 Tests + docs Cucumber .feature files define behavioral contract. Reference doc updates
6 400f5ae Cleanup Dead code removal from HasContainer unification. Test quality fixes

Architecture (single resolution path)

                    has("age", __.V(1).values("age"))
                              │
                    DSL wraps: has("age", P.eq(traversal))
                              │
                              ▼
                    HasContainer(key, P.eq(traversal))
                              │
              ┌───────────────┴───────────────┐
              │ HasStep.resolvePredicate()     │
              │   P.resolve(traverser)         │
              │   hc.test(element)             │
              └────────────────────────────────┘

Key decisions to scrutinize

  1. Single resolution path - At the DSL level, has(key, traversal) wraps the traversal in P.eq(traversal) before creating the HasContainer. This means HasStep only has one code path for traversal resolution: call P.resolve(traverser) on the predicate, then hc.test(element). The GremlinLang serialization still records the original has(key, traversal) form (verified by round-trip tests). This is a new 4.0.0 feature, so no existing provider code is affected. Providers implementing custom strategies must check HasContainer.hasTraversal() before folding into index lookups.

  2. Empty-result semantics - When a child traversal produces zero results, behavior depends on predicate type. Collection predicates (within/without) resolve to an empty collection and delegate to Contains.test() which applies correct set-theoretic logic: within([]) is always false, without([]) is always true. Scalar predicates (eq/gt/lt/etc.) set resolvedEmpty=true and steps short-circuit to false since there is no meaningful comparison value. A null result (traversal produces null) is distinct from an empty result: it resolves normally and null is used as the comparison value, consistent with existing P.eq(null) semantics.

  3. AndP short-circuits, OrP doesn't - AndP.resolve() overrides ConnectiveP.resolve() to stop resolving children at the first scalar predicate that resolves empty. This is sound because and(unsatisfiable, X) is always false regardless of X. OrP does NOT short-circuit: a non-empty resolution does not guarantee the predicate will pass test(), so later children must already be resolved when their turn comes. Note: short-circuiting means side-effecting child traversals (e.g., ones using aggregate) in later AndP operands may not execute. This matches standard boolean short-circuit semantics.

  4. P.resolve() mutates in place - resolve() overwrites literals, isCollection, and resolvedEmpty fields on the P instance. In OLTP this is safe since resolve and test happen sequentially per traverser. In OLAP, HasStep.clone() deep-clones predicates per worker, providing thread isolation. The mutation pattern is a known limitation documented for future improvement (return an immutable resolved value instead). Predicate trees of arbitrary depth (AndP containing OrP containing NotP, etc.) are handled by recursive resolution and recursive cloning.

  5. Mutation guard - ChildTraversalValidator rejects any child traversal containing a step that implements the Mutating interface (addV, addE, drop, property). This runs at construction time (DSL methods and P factory methods) and again at strategy time via ChildTraversalVerificationStrategy. Aggregating side-effects (aggregate, store, group) are NOT blocked because they don't modify graph state. Whether they should be restricted is a question for reviewers.

  6. Provider contract - HasContainer.hasTraversal() is the single check point providers must use before folding. Providers that fold HasContainers into index-backed steps without this check will attempt to use an unresolved P value (returns null before resolve() is called) as an index key, producing wrong results. TinkerGraphStepStrategy demonstrates the pattern: skip traversal-bearing HasSteps with continue so that subsequent literal HasSteps can still be folded. Providers with custom GraphStep replacements must also copy the idTraversal field during strategy replacement, otherwise V(traversal) falls through to returning all vertices.

  7. choose() restriction - choose(P) and choose().option(P, ...) reject traversal-bearing predicates at construction time. This is a deliberate scope decision. PredicateTraversal (which evaluates option-key predicates in BranchStep) has access to the traverser and could technically call P.resolve() before P.test(), but extending the branching infrastructure is out of scope for this feature. Users needing dynamic branching can use the choose(Traversal, ...) form where the branch predicate is a full traversal (e.g., choose(__.is(P.gt(traversal)), trueChoice, falseChoice)). This works because IsStep handles resolution normally. Restricting is() when used inside choose()'s branch traversal is not feasible since choose() receives it as an opaque Traversal.Admin and does not inspect internal steps.

What's NOT in this PR

  • P.resolve() thread-safety refactoring (future)
  • instanceof DefaultGraphTraversal cleanup (deferred)
  • Runtime index folding for deterministic child traversals
  • FilterRankingStrategy cost awareness

Compatibility

  • Additive overloads only. No existing step or predicate signature changed.
  • Serialized GremlinLang form is unchanged: has(key, traversal) still serializes as has(key, traversal), verified by GremlinLangTraversalRoundTripTest and GremlinQueryParserTraversalTest.

Testing

All gremlin-core unit tests pass (9132, 0 failures) and the full TinkerGraph Gherkin suite passes (2172 scenarios, 0 failures) against the current master.

Category Coverage
Behavioral contract HasTraversal.feature, IsTraversal.feature, WhereTraversal.feature, VETraversal.feature, PropertyTraversal.feature, ChildTraversalVerification.feature
Internal mechanics PTraversalTest, CloneIndependenceTraversalTest, HasContainerTest, ChildTraversalValidatorTest, ChildTraversalVerificationStrategyTest
Grammar + serialization GremlinQueryParserTraversalTest, GremlinLangTraversalRoundTripTest
Strategy correctness TinkerGraphStepStrategyTraversalTest

Checklist

  • CHANGELOG entry added
  • Reference documentation updated (the-traversal.asciidoc)
  • Upgrade documentation added (release-4.x.x.asciidoc)
  • All GLVs updated (Python, .NET, Go, JavaScript)
  • Full mvn clean install passes
  • No new external dependencies introduced

VOTE +1

xiazcy added 6 commits June 12, 2026 14:07
Extend P, TextP, NotP, and the connective predicates (AndP/OrP) to carry a
child traversal whose result is resolved per-traverser at runtime instead of a
literal value. P.resolve() splits the traverser, seeds and runs the child
traversal, and installs the result as the comparison value for the current test
cycle.

Semantics:
- Scalar predicates (eq/neq/gt/lt/gte/lte) take the first result; an empty
  result cannot be satisfied and is flagged resolved-empty so steps short-circuit.
- Collection predicates (within/without) resolve to a collection; an empty
  result resolves to an empty set so within(empty) is false and without(empty)
  is true, matching literal P.within([])/P.without([]) semantics.
- Multi-traversal within/without combine the first result of each child.
- AndP short-circuits resolution at the first child that resolves empty.
- NotP exposes getWrapped() so traversal collection does not rely on negate().

GremlinLang serializes traversal-bearing predicates as op(traversal).
…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.
Extend Gremlin.g4 and the traversal/predicate/spawn visitors to accept a
nestedTraversal where a literal or predicate is expected in has(), hasLabel(),
is(), V(), E(), property(), and the P/TextP predicate productions. Start-step
V(traversal)/E(traversal) parsing is supported; mutating child traversals are
rejected downstream by the verification strategy.
Mirror the Java overloads in gremlin-python, gremlin-go, gremlin-dotnet, and
gremlin-javascript so has(), hasLabel(), is(), V(), E(), property(), and the
P/TextP predicates accept child traversals. Update the .NET translator (Java
and TS) and the translation fixtures, and add the generated cucumber step
bindings for the new feature scenarios.
…ng steps

Add cross-language Gherkin scenarios covering has/is/where/property/V/E with
child-traversal arguments and the child-traversal mutation verification, plus
CHANGELOG, upgrade, and reference documentation describing the feature, its
first-result/empty-set semantics, and the provider-side HasContainer folding
guard.
…te leftover null guards and dead code from HasContainer unification

Assisted-by: Kiro:claude-opus-4-6
Comment thread gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/P.java Outdated
Comment thread gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/P.java Outdated
Comment thread gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/P.java Outdated
Comment thread gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/P.java Outdated
Comment thread gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/P.java Outdated
Comment thread gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/P.java Outdated
Comment thread gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/P.java Outdated
Comment thread gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/P.java Outdated

The traversal-accepting forms of `has()` allow for dynamic property comparisons. Rather than providing a literal value,
a child traversal is supplied and its first result is used as the comparison value. This follows the same first-result
semantics as `by(traversal)`. The child traversal must be read-only - mutating steps are rejected.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The child traversal must be read-only - mutating steps are rejected.

Don't really like dashes in our docs this way, how about: "The child traversal cannot include mutating steps like addV() or mergeE(). If these steps are present, they will be rejected at runtime with a XYZException."

<2> Find projects having two or more contributors.
<3> Find projects whose contributors average age is between 30 and 35.

The `is()` step also supports predicates that contain traversal arguments for dynamic threshold comparison.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any reason to not just fold this back into the previous examples? i'm not sure it needs to stand out as a separate thing.

<7> The label value can be specified as a property only at the time a vertex is added and if one is not specified in the addV()
<8> If you pass a `null` value for the Map this will be treated as a no-op and the input will be returned

The `property()` step also accepts a traversal that produces a `Map` of key-value pairs to set as properties. The

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again, don't like the dash this way in our docs:

The child traversal must be read-only - mutating steps are rejected.

you could use the other verbiage i had or some variation of it.

If the traversal does not produce a Map, the result is rejected.

rejected when and in with what exception?


[gremlin-groovy,modern]
----
g.V(4).property(__.V(1).project('friendCount','createdSoftware').by(__.out('knows').count()).by(__.out('created').values('name'))) <1>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please format the query to make this easier to read.

[gremlin-groovy,modern]
----
g.V(4).property(__.V(1).project('friendCount','createdSoftware').by(__.out('knows').count()).by(__.out('created').values('name'))) <1>
g.V(4).valueMap('friendCount','createdSoftware')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe better to show the whole vertex with all properties to demonstrate we augmented an existing vertex that had properteis?

g.V(4).valueMap('friendCount','createdSoftware')
----

<1> Set two properties on vertex 4 (josh) from a Map produced by a child traversal. The `project()` step builds a Map

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you get a better multi-line formatting as i mentioned above, then this description can be broken into smaller bits for folks to understand more easily

<1> Normally the `V()`-step will iterate over all vertices. However, graph strategies can fold ``HasContainer``'s into a `GraphStep` to allow index lookups.
<2> Whether the graph system provider supports mid-traversal `V()` index lookups or not can easily be determined by inspecting the `toString()` output of the iterated traversal. If `has` conditions were folded into the `V()`-step, an index - if one exists - will be used.

The `V()` step also accepts a traversal argument. The child traversal is evaluated and its results are used as the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again, i feel like this doesn't need to be an "also" in how this step works. just integrate it into the other examples and content more seamlessly.

<8> Marko is younger than josh, but josh knows someone equal in age to marko (which is marko).
<9> The "age" property is not <<by-step,productive>> for all vertices and therefore those values are filtered.

The `where()` step also supports predicates that contain traversal arguments. When a predicate contains a child

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again, as with other comments, let's integrate this more seamlessly into the content. Also, perhaps where is a case where we could come up with a more complex example since it's a more advanced step.

Comment thread docs/src/upgrade/release-4.x.x.asciidoc Outdated
partial work is discarded if the user forgets to call `commit()`. In Java (both remote and embedded mode), the behavior
can still be overridden via `tx.onClose(Transaction.CLOSE_BEHAVIOR.COMMIT)`. The non-Java GLVs do not support
configuring close behavior and always rollback.
==== Traversal-Accepting Steps

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need a catchier title here. "Expanding Dynamic Arguments"??

Comment thread docs/src/upgrade/release-4.x.x.asciidoc Outdated
configuring close behavior and always rollback.
==== Traversal-Accepting Steps

Steps and predicates that previously only accepted literal values now accept child traversals resolved per-traverser

@spmallette spmallette Jun 16, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Folks have been waiting on this feature for a long time. It needs a strong introduction with some history about the challenges folks had with the old way to do some of this stuff. Show some comparisons demonstrating the old way versus the ease of the new way.

Stuff like the following is instructional details and doesn't really belong here:

Child traversals take only the first result (consistent with by(traversal) semantics). For within()/without(),
use fold() to collect multiple values into a list.

i think we should also be telling users in this section what patterns this feature allows them to replace to make their code perform better, read better, etc.

Comment thread docs/src/upgrade/release-4.x.x.asciidoc Outdated

All child traversals must be read-only. Mutating steps (`addV`, `addE`, `drop`, `property`) inside child traversals
are rejected at construction time with `IllegalArgumentException`. A `ChildTraversalVerificationStrategy` provides
additional safety at strategy time.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aren't there some JIRAs for this to reference?

@spmallette

Copy link
Copy Markdown
Contributor

There are some docs here that cover the feature, but what I don't see are updates to existing docs that use old patterns that are no longer relevant and that we should no longer promote. Has that kind of search been done?

Consolidate traversal detection and validation:
- setValue() is the single entry point for all value assignment. Traversal
  detection (GraphTraversal instanceof check) and ChildTraversalValidator
  calls happen centrally rather than in each static factory method.
- Extract applyValue() private helper from setValue() so resolve() can
  reuse value-processing logic without wiping child traversal state.
- Broaden instanceof checks from DefaultGraphTraversal to GraphTraversal.

Unify value storage:
- Replace traversalValue/traversalValues fields with single
  childTraversals list, using isCollection to distinguish scalar vs
  collection semantics (mirroring how literals already works).
- within(Traversal)/without(Traversal) factories box the single traversal
  into a singleton list, consistent with how within(V...) boxes varargs.

Simplify resolve():
- Evaluate all child traversals, then delegate to applyValue() for the
  resolved result. Collection results are flattened; scalar results are
  passed directly.
- Branch on isCollection flag rather than instanceof Contains.
- Internalize resolvedEmpty handling in test() so callers (including
  OrP/AndP bipredicates) do not need to check isResolvedEmpty before
  testing. Fixes a bug where OrP.test() would incorrectly test
  resolved-empty children against null.

Remove redundant code:
- Remove P.integrateTraversals(); steps now use collectTraversals() +
  integrateChild() matching HasStep/WherePredicateStep pattern.
- Remove redundant validate() calls from P and TextP static factories.
- Simplify ChildTraversalValidator to use TraversalHelper
  .getStepsOfAssignableClassRecursively().
- Inline HasStep.resolvePredicate() helper.
- Remove isResolvedEmpty checks from all step filter methods.

Assisted-by: Kiro:claude-opus-4.6
Short-circuits on the first mutating step found rather than collecting
all matches into a list.

Assisted-by: Kiro:claude-opus-4.6
Replace the (first, second, more...) pattern with (first, more...) for
multi-traversal within/without. The forced second parameter was
unnecessary - (first, more...) handles 2+ args through varargs.

Keep the single-arg within(Traversal)/without(Traversal) overload to
disambiguate from the GValue<V>... varargs overload.

Remove the now-unused containsTraversals helper.

Assisted-by: Kiro:claude-opus-4.6
Move eq/neq/lt/lte/gt/gte/within/without Traversal overloads next to
their matching V and GValue overloads instead of grouping them in a
separate block at the end of the class.

Assisted-by: Kiro:claude-opus-4.6
setValue() already handles GraphTraversal detection in collections
(single traversal, all-traversals, and mixed-throws cases), making this
varargs-specific detection redundant.

Assisted-by: Kiro:claude-opus-4.6
Add test cases for steps that are newly TraversalParent or have new
types of child traversals from this PR: IsStep, HasStep, AllStep,
AnyStep, NoneStep, GraphStep, and WherePredicateStep.

Each predicate-bearing step includes both a scalar predicate case
(P.eq) and a collection predicate case (P.within).

Also fix WherePredicateStep.clone() and setTraversal() to properly
re-collect and re-parent predicate child traversals after cloning.

Assisted-by: Kiro:claude-opus-4.6
Add traversal sugar overloads for hasKey() and hasValue() in
GraphTraversal and __, mirroring the existing hasLabel(Traversal)
pattern. Add grammar rules and visitor methods.

Add feature tests for hasLabel(Traversal), hasId(P.eq(Traversal)),
hasKey(Traversal), hasKey(P.eq(Traversal)), hasValue(Traversal), and
hasValue(P.eq(Traversal)).

Also reset resolvedEmpty in P.setValue() to prevent stale state, and
remove unnecessary @SuppressWarnings annotations from filter steps.

Assisted-by: Kiro:claude-opus-4.6
…rentTest

FormatStep.clone() was sharing the traversalRing instance instead of
cloning it. AbstractAddElementStepPlaceholder and
AddPropertyStepPlaceholder were sharing traversal-typed property keys
across original and clone.

TraversalParentTest now asserts that after clone, child traversal
instances are independent (not shared references) in addition to
verifying correct parent assignment.

Assisted-by: Kiro:claude-opus-4.6
@@ -0,0 +1,433 @@
# Licensed to the Apache Software Foundation (ASF) under one

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of these *Traversal.feature tests should be integrated to their parent step files.

For this file, the fact that every single test has @GraphComputerVerificationMidVNotSupported tells me something is wrong because it either means we've mis-labeled the test (as happened elsewhere) or we don't have all the right testing.

Where are the tests for use cases with select() for example? I know V() is popular but select() will equally come into play. That approach and others seem necessary. What about filtering on a sack() value? that might be interesting.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've migrated all of these *Traversal.feature to their appropriate parent step file. For has() specifically, I've removed excessive @GraphComputerVerificationMidVNotSupported tags and added a new case for select() and sack()

@@ -0,0 +1,273 @@
# Licensed to the Apache Software Foundation (ASF) under one

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@ -0,0 +1,168 @@
# Licensed to the Apache Software Foundation (ASF) under one

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

| v[ripple] |
| v[lop] |

# Multi-traversal without() - exclusion from multiple relationship sources

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe the multi-traversal semantics are correct. In the reference docs we describe it as:

  • The multi-traversal form within(trav1, trav2, ...) takes the first result from each traversal and combines them into a collection for membership testing.

but within doesn't really work like that:

gremlin> g.V().has('name',within(['josh','lop'], 'vadas'))
==>v[2]

It only expands the list if the first and singular argument is a collection:

gremlin> g.V().has('name',within(['vadas','josh','lop']))
==>v[2]
==>v[3]
==>v[4]

Otherwise, as shown in the first example, it will treat it like comparing on a List and a String. V and E have similar semantics, but oddly:

gremlin> g.V().hasId(2, [3, 1])
==>v[2]
==>v[3]
==>v[1]
gremlin> g.V(2, [3, 1])
Expected an id that is convertible to class java.lang.Integer but received class java.util.ArrayList - [[3, 1]]
Type ':help' or ':h' for help.
Display stack trace? [yN]

This goes back to https://issues.apache.org/jira/browse/TINKERPOP-2863 where I think there is a bug in the implementation which tried to get hasId to be like V and E but allowed a little too much to happen. I think all of this needs to be rectified.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the "auto-unfolding" semantics to properly align with the literal-form of within()

gremlin> g.inject(1).is(P.within(constant([1,2]), constant(3)))
gremlin> g.inject(1).is(P.within(constant([1,2,3])))
==>1

And using the parameter vid3 defined as "v[josh].id"
And the traversal of
"""
g.V().has("name", P.within(__.V(vid1).out("knows").values("name").fold(), __.V(vid3).out("created").values("name").fold()))

@spmallette spmallette Jun 19, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given my comment here; https://github.com/apache/tinkerpop/pull/3458/changes#r3443997744 i think this should be written as:

g.V().has("name", P.within(__.union(__.V(vid1).out("knows").values("name"), 
                                    __.V(vid3).out("created").values("name")).fold()))

And using the parameter vid1 defined as "v[marko].id"
And the traversal of
"""
g.V(vid1).outE("knows").filter(__.inV().has("name", P.within(__.V().has("name","lop").in("created").values("name").fold(), __.V().has("name","ripple").in("created").values("name").fold())))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Gremlin in our tests should be formatted to match our best practices. These tests need to continue to be a solid dependable source for human curated Gremlin.

- InlineFilterStrategy: update class javadoc, add inline comments for
  traversal-bearing short-circuits, add test cases proving exclusion
- choose(P): add javadoc noting traversal-bearing predicates throw
  IllegalArgumentException (GraphTraversal and option())
- P.java: fix wording 'all values' -> 'each value'
- ReadOnlyChildValidator: include mutating step name and parent
  traversal in error message for easier debugging

Assisted-by: Kiro:claude-opus-4.6
- HasTraversal.feature (29 scenarios) → Has.feature
- IsTraversal.feature (20 scenarios) → Is.feature
- WhereTraversal.feature (10 scenarios) → Where.feature
- VETraversal.feature → Vertex.feature (6) + Edge.feature (4)
- PropertyTraversal.feature → renamed to Property.feature (canonical)
- ReadOnlyChildVerification.feature → moved to integrated/ directory
  with @StepClassIntegrated tag (strategy test, not step test)
- Remove incorrect @GraphComputerVerificationMidVNotSupported from
  g_V_hasXname_notXidentityXX (no mid-V in that traversal)

Assisted-by: Kiro:claude-opus-4.6
- GremlinLangTraversalRoundTripTest: convert to parameterized test with
  45 cases covering all traversal argument forms (has, is, where, V, E,
  property, hasId, hasLabel, hasKey, hasValue, ConnectiveP, within/without)
- PTraversalTest: add ComplexConnectiveTest with deeply nested and/or
  combinations including empty-resolution edge cases
- PTraversalTest: add CloneAndNegateTest verifying clone/negate preserve
  child traversals correctly across scalar, collection, and connective
  predicates
- Format long Gremlin in feature tests for readability

Assisted-by: Kiro:claude-opus-4.6
Merge all traversal-bearing predicate tests into PTest so that all P
behavior is covered in one place. Traversal predicates now participate
in the existing parameterized test (shouldTest), gaining automatic
clone/negate/P.not coverage. Resolution is handled by a conditional
resolve() call for traversal-bearing predicates.

Inner classes reorganized as:
- TraversalDetectionTest: hasTraversal() accuracy
- TraversalResolutionTest: resolve semantics for scalar/collection
- TraversalConnectiveTest: and/or/not with nested traversals
- TraversalCloneAndNegateTest: clone/negate preserve traversal state

Assisted-by: Kiro:claude-opus-4.6
Upgrade doc:
- Replace 'verbose workarounds' with neutral language
- Replace 'unintuitive' with factual description
- Move JIRA links into See: section
- Add summarizing migration guidance paragraph

Reference doc:
- Replace semicolon with two sentences (has() callout)

Semantics docs:
- E(): add List form to syntax and arguments
- V(): add List form to syntax and arguments
- has(): expand with T accessor forms, Map domain detail, null semantics
- hasId(): add List form to syntax and arguments

Gherkin tests:
- Add select()-based child traversal test to Has.feature

Assisted-by: Kiro:claude-opus-4.6
PTest: the migrated take-first-result tests used inject(1,2,3) whose
output order mixes with the seed traverser. Restore union(constant,
constant) for deterministic first-result ordering.

InlineFilterStrategyTest: correct expectations for traversal-bearing
has-steps. filter() unwrapping still applies, and and-decomposition
produces two separate HasSteps (the no-merge guard prevents folding
the traversal-bearing container into an adjacent HasStep).

Verified: full gremlin-core suite passes (9276 tests, 0 failures).

Assisted-by: Kiro:claude-opus-4.6
The VETraversal split (integrating V/E scenarios into Vertex/Edge.feature)
left structural artifacts from line-range slicing: dangling
@GraphComputerVerification tags, an orphan result row, and missing blank
lines between scenarios. These caused Gherkin parse failures.

Also replace the mislabeled sack() scenario in Has.feature (which actually
used select()) with a real sack()-based test:
  g.withSack(29).V().has('age', P.gt(__.sack())).values('name')

Verified: TinkerGraphParameterizedFeatureTest passes (2280 tests, 0 failures).

Assisted-by: Kiro:claude-opus-4.6
…arios

g_VXVXvid1X_idX_name and g_EXVXvid1X_outE_idX use a child traversal
containing __.V(vid1), which becomes an unsupported mid-traversal V()
on GraphComputer. These two start-step scenarios lacked the
@GraphComputerVerificationMidVNotSupported tag in the original
VETraversal.feature.

Verified: TinkerGraphComputerFeatureTest passes (0 errors) and both
scenarios still pass on the standard engine.

Assisted-by: Kiro:claude-opus-4.6
Previously resolve() took the first result from each child traversal and
flattened any collection results into one combined membership set. This
didn't match literal within/without semantics, where unfolding only
happens for a single collection argument.

New behavior (mirrors literal forms, gated on isCollection + first-result):
- within(t): first result acts like within(Collection) - a Collection
  result unfolds into the membership set, otherwise it is one value
- within(t1, t2, ...): mirrors within(v1, v2, ...) - each first result is
  one membership element, no unfolding
- scalar predicates (eq/gt/...) never unfold

Rewrote the multi-source feature scenarios to the union(...).fold()
single-traversal idiom (per review), preserving their expected results.
Added PTest cases covering single-traversal unfold vs multi-traversal
no-unfold. Updated reference docs and P factory javadocs.

Regenerated the GLV feature test data (translations.json, gremlin.py,
gremlin.js, gremlin.go, Gremlin.cs) so they stay in sync with the
updated .feature sources.

Verified: gremlin-core (9284), TinkerGraph standard + computer feature
suites all pass.

Assisted-by: Kiro:claude-opus-4.6
The predicate factory methods (P.eq/gt/within/etc.) accept Traversal, and
__.foo() is already a GraphTraversal, so the .asAdmin() suffix on child
traversal arguments is unnecessary and unrealistic for how users write
these. Stripped them from PTest, GremlinLangTraversalRoundTripTest, and one
case in TinkerGraphStepStrategyTraversalTest.

Kept the asAdmin() calls that are genuinely required: Admin-API access
(getGremlinLang, addStep) and Admin-typed variables/parameters.

Assisted-by: Kiro:claude-opus-4.6
Addresses review feedback asking to see results for the examples. Per the
discussion, the examples remain static (not executed at doc-build time) so
they stay stable across releases, but now show gremlin> prompts and ==>
output captured from the modern graph.

Also updated the multi-source within() example to the union(...).fold()
single-traversal form, consistent with the corrected within/without
semantics.

Output verified by running each example through the Gremlin Console
against TinkerFactory.createModern().

Assisted-by: Kiro:claude-opus-4.6
The has() traversal-argument examples were still in a separate code block
with their own prose, unlike is()/where()/property()/V() which integrate
their traversal examples into the main block. Folded the three examples in
as callouts <11>-<13> and condensed the explanation into the callout and a
concise NOTE (read-only constraint, P.eq wrapping, pointer to A Note on
Predicates). Removed the semicolon per earlier review feedback.

Verified the examples execute against the modern graph.

Assisted-by: Kiro:claude-opus-4.6
The traversal examples leaned on the fixed __.V(1).values(...) lookup
pattern. Added two examples that show the comparison value being computed
dynamically, which better conveys what traversal arguments enable:

- is(): filter ages above the average person's age, with the threshold
  computed by a child traversal (mean())
- has(): filter people older than a threshold carried in the traverser's
  sack(), showing the value can come from traverser state rather than the
  graph

Both verified against the modern graph (is -> [32,35], has -> [josh,peter]).

Assisted-by: Kiro:claude-opus-4.6
Correcting a misread of the review comment. The comment asked for the
reference-doc cross-links (has(), V(), property(), is(), where()) to be
folded into the See: block alongside the JIRA links, rather than left as
a separate standalone 'See ...' sentence. There is now a single unified
See: section containing both the reference links and the JIRA links.

Assisted-by: Kiro:claude-opus-4.6
ReadOnlyChildVerification.feature now carries the common strategy-test
pattern used by peer integrated features - @WithReadOnlyChildVerificationStrategy
scenarios that invoke the strategy explicitly via withStrategies() (a
pass-through, a valid read-only child, and a rejected mutating child).

completed the @GraphComputerVerificationMidVNotSupported audit.
- Removed the tag from the sack() scenario in Has.feature (start V() only,
  no mid-traversal V(); verified it runs on GraphComputer).
- Removed the tag from all ReadOnlyChildVerification rejection scenarios and
  corrected the rationale comment: the strategy rejects at strategy-application
  time with the same 'mutating step' message under both OLTP and OLAP (verified),
  so no exclusion is needed. Kept the tag only on the valid scenarios that
  actually execute a child V() lookup.

Added the missing select()/sack() cases to Is.feature (select() carries a
mid-V via as('a').V() and is tagged; sack() has no mid-V and is untagged).

Regenerated GLV feature data to stay in sync.

Verified: TinkerGraph standard (2189) and computer (3238) feature suites pass.

Assisted-by: Kiro:claude-opus-4.6
The scaffolding scenarios used withStrategies(ReadOnlyChildVerificationStrategy),
which broke the gremlin-server Groovy script path (No such property) and would
also break the GLV radish tests: the generated gremlin.py/js/go/cs emit a
ReadOnlyChildVerificationStrategy() constructor that no GLV defines. Making the
scaffolding pass everywhere would require registering this internal default
verification strategy as a user-facing, serializable strategy across CoreImports,
GraphSONModule, and all four GLVs - a public-API expansion that needs a
deliberate decision.

Reverted the three withStrategies scenarios and the CoreImports change.
Retained the in-scope, verified work: the GraphComputer tag audit (removed
over-tags from the rejection scenarios with corrected rationale) stays.

Verified: TinkerGraph standard (2186) and computer (3233) feature suites pass.

Assisted-by: Kiro:claude-opus-4.6
Gives the strategy equivalent support to StandardVerificationStrategy so it
can be referenced via withStrategies()/withoutStrategies() consistently across
all languages, and restores the common strategy-test scaffolding (#22).

- CoreImports: import + CLASS_IMPORTS so the Groovy script engine resolves it
- GraphSONModule: register in all four strategy lists for serialization
- GLVs: define the strategy in Python (strategies.py), Go (strategies.go),
  JavaScript (traversal-strategy.ts), and .NET (new
  ReadOnlyChildVerificationStrategy.cs), mirroring StandardVerificationStrategy
- ReadOnlyChildVerification.feature: restore the withStrategies scaffolding
  scenarios (pass-through, valid read-only child, rejected mutating child)
- Regenerated GLV feature data

The Go translator lists are intentionally untouched: the config-less
withStrategies(ReadOnlyChildVerificationStrategy) form takes the simple
translation path, same as ReadOnlyStrategy.

Verified: TinkerGraph standard (2189) and computer (3238) feature suites pass;
gremlin-server Groovy, GraphSON-lang, and GraphBinary-lang feature tests pass
for the scaffolding scenarios (3/3 each).

Assisted-by: Kiro:claude-opus-4.6
Reconcile existing documentation with the new support for traversal
arguments on steps and predicates so the docs no longer promote or
describe superseded patterns.

- traversal-induced-values recipe: replace the two-traversal variable
  lookup and the as()/where().by() label-comparison with the recommended
  single-traversal predicate form (has(key, gt(traversal))), and
  modernize the "movies liked by all friends" example to use
  is(eq(traversal)) instead of as()/where(eq()) count coordination.
- anti-patterns recipe: remove the "has() and Traversal Arguments"
  section, which documented the old behavior that traversal arguments
  do not resolve to comparison values. That behavior is now reversed.
- the-traversal reference: drop the now-dangling link to the removed
  anti-pattern from the has() step's additional references.

Assisted-by: Kiro:claude-opus-4.8
@Cole-Greer

Copy link
Copy Markdown
Contributor

@spmallette Regarding your comment here:

There are some docs here that cover the feature, but what I don't see are updates to existing docs that use old patterns that are no longer relevant and that we should no longer promote. Has that kind of search been done?

I got an agent to do a scan through all of our documentation for examples in need of updates, I found a few cases in the recipes docs, notable in anti-patterns and traversal-induced-values. The anti pattern has been removed entirely, and the traversal-induced-values example has been updated to the new recommended pattern.

I haven't done any manual reads through the bulk of the docs in regards to this feature alone, but I intend to do a thorough docs review in the lead up to the 4.0.0-beta.3 release which may reveal some other examples which would benefit from refinement.

When iterated to list
Then the result should be empty No newline at end of file
Then the result should be empty
@GraphComputerVerificationMidVNotSupported

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

overall the test organization looks better since i called in out in my first review, but i'm still not seeing where we've expanded tests much to cover cases that don't use mid-V.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

additional reference link for V(Traversal)?


@Override
public void apply(final Traversal.Admin<?, ?> traversal) {
for (final Step<?, ?> step : traversal.getSteps()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

have we examined if we are over-iterating here given how strategies tend to be applied plus the recursive call in ReadOnlyChildValidator? perhaps there is a room for some added efficiency here?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, and we excessively "double up" in one case, if a ReadOnlyTraversalParent is itself a descendant of another ReadOnlyTraversalParent, the child's children will be needlessly scanned for Mutating steps a second time.

I've accepted that as a reasonable trade off for simplicity as I expect that to be a fairly rare case given the intended usage of these steps. If we were to avoid recursing into the descendants of a ReadOnlyTraversalParent and instead rely on the recursive application of the strategy to all children, we would instead need to include a test if traversal itself is a descendant of a ReadOnlyTraversalParent which we don't have a great way to do, and would likely negate any savings from double recursion.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taking another pass over this code, I think it can be simplified a bit by relying on TraversalHelper.getStepsOfAssignableClass(ReadOnlyTraversalParent.class). This wouldn't really impact the performance much but would be a decent simplification of the code.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not completely sure it's innocuous as that, but irrespective of that point I'm now wondering why this is a separate strategy. Why wouldn't we fold this functionality under the StandardVerificationStrategy? This is very standard behavior that must be executed - I can't think of a reason you would opt-out of just this verification. Folding the feature into "standard" would allow the validation to happen within the same iteration of steps as the existing verification instead of a second pass. It would also remove a bunch of GLV code and unnecessary testing.

Comment on lines +46 to +52
for (final Traversal.Admin<?, ?> child : ((ReadOnlyTraversalParent) step).getLocalChildren()) {
try {
ReadOnlyChildValidator.validate(child);
} catch (final IllegalArgumentException e) {
throw new VerificationException(e.getMessage(), traversal);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should recurse through global children as well. I don't think any of the current set of ReadOnlyTraversalParent steps actually have any global children so there's no impact right now, but it would be more future proof.

Suggested change
for (final Traversal.Admin<?, ?> child : ((ReadOnlyTraversalParent) step).getLocalChildren()) {
try {
ReadOnlyChildValidator.validate(child);
} catch (final IllegalArgumentException e) {
throw new VerificationException(e.getMessage(), traversal);
}
}
for (final Traversal.Admin<?, ?> child : ((ReadOnlyTraversalParent) step).getLocalChildren()) {
try {
ReadOnlyChildValidator.validate(child);
} catch (final IllegalArgumentException e) {
throw new VerificationException(e.getMessage(), traversal);
}
}
for (final Traversal.Admin<?, ?> child : ((ReadOnlyTraversalParent) step).getGlobalChildren()) {
try {
ReadOnlyChildValidator.validate(child);
} catch (final IllegalArgumentException e) {
throw new VerificationException(e.getMessage(), traversal);
}
}

Move read-only child traversal validation into the existing step loop in
StandardVerificationStrategy, eliminating a separate strategy pass. Also
validate global children for future-proofing.

- Remove ReadOnlyChildVerificationStrategy class and its test
- Remove GLV strategy classes (.NET, Go, JS, Python) and test translations
- Fold feature test scenarios into StandardVerificationStrategy.feature
- Rewrite feature tests to reduce @GraphComputerVerificationMidVNotSupported
  usage: use constant() for simple predicate tests, keep mid-V only where
  graph traversal is the test's purpose
- Add V(Traversal) javadoc reference link
- Expand and fix semantics doc entries for all new steps

Assisted-by: Kiro:claude-opus-4.6
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants