Allow Steps to Accept Traversal#3458
Conversation
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
|
|
||
| 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. |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
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') |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
Need a catchier title here. "Expanding Dynamic Arguments"??
| 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 |
There was a problem hiding this comment.
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). Forwithin()/without(),
usefold()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.
|
|
||
| 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. |
There was a problem hiding this comment.
Aren't there some JIRAs for this to reference?
|
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 | |||
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 | |||
There was a problem hiding this comment.
same general comments as for has() - https://github.com/apache/tinkerpop/pull/3458/changes#r3443937188
| @@ -0,0 +1,168 @@ | |||
| # Licensed to the Apache Software Foundation (ASF) under one | |||
There was a problem hiding this comment.
same general comments as for has() - https://github.com/apache/tinkerpop/pull/3458/changes#r3443937188
| | v[ripple] | | ||
| | v[lop] | | ||
|
|
||
| # Multi-traversal without() - exclusion from multiple relationship sources |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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())) |
There was a problem hiding this comment.
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()))) |
There was a problem hiding this comment.
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
|
@spmallette Regarding your comment here:
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
additional reference link for V(Traversal)?
|
|
||
| @Override | ||
| public void apply(final Traversal.Admin<?, ?> traversal) { | ||
| for (final Step<?, ?> step : traversal.getSteps()) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| for (final Traversal.Admin<?, ?> child : ((ReadOnlyTraversalParent) step).getLocalChildren()) { | ||
| try { | ||
| ReadOnlyChildValidator.validate(child); | ||
| } catch (final IllegalArgumentException e) { | ||
| throw new VerificationException(e.getMessage(), traversal); | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| 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
Summary
Allow child traversals as arguments to
has(),is(),V(),E(),property(),where(P), and allP/TextPpredicates. The child traversal is evaluated per-traverser at runtime, enabling dynamic value resolution.JIRA:
within()silently return empty instead of executing or erroring - fully addressed (traversals are now executed)V()cannot accept traversals likeselect()for data-driven lookups - fully addressedproperty()should accept a traversal producing a Map - fully addressedhas(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
Review Guide
This is a large change (68 files, ~7k lines) but structured in layers. Review bottom-up:
5f37d11P.resolve(), empty-result semantics,AndPshort-circuit,ConnectiveP/NotPdelegation6a547d2P.resolve().HasStepsingle path,GraphStepidTraversal,AddPropertyStepmapForm,AcceptsChildPredicateTraversalmarkere80423aGremlin.g4nestedTraversalalternatives. Visitors delegate to API (notaddStep())1b21d15within()/without()serialization. .NET typed overloads793e7a8.featurefiles define behavioral contract. Reference doc updates400f5aeArchitecture (single resolution path)
Key decisions to scrutinize
Single resolution path - At the DSL level,
has(key, traversal)wraps the traversal inP.eq(traversal)before creating theHasContainer. This means HasStep only has one code path for traversal resolution: callP.resolve(traverser)on the predicate, thenhc.test(element). The GremlinLang serialization still records the originalhas(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 checkHasContainer.hasTraversal()before folding into index lookups.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 toContains.test()which applies correct set-theoretic logic:within([])is always false,without([])is always true. Scalar predicates (eq/gt/lt/etc.) setresolvedEmpty=trueand steps short-circuit to false since there is no meaningful comparison value. A null result (traversal producesnull) is distinct from an empty result: it resolves normally andnullis used as the comparison value, consistent with existingP.eq(null)semantics.AndP short-circuits, OrP doesn't -
AndP.resolve()overridesConnectiveP.resolve()to stop resolving children at the first scalar predicate that resolves empty. This is sound becauseand(unsatisfiable, X)is always false regardless of X.OrPdoes NOT short-circuit: a non-empty resolution does not guarantee the predicate will passtest(), so later children must already be resolved when their turn comes. Note: short-circuiting means side-effecting child traversals (e.g., ones usingaggregate) in later AndP operands may not execute. This matches standard boolean short-circuit semantics.P.resolve()mutates in place -resolve()overwritesliterals,isCollection, andresolvedEmptyfields on thePinstance. 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.Mutation guard -
ChildTraversalValidatorrejects any child traversal containing a step that implements theMutatinginterface (addV, addE, drop, property). This runs at construction time (DSL methods and P factory methods) and again at strategy time viaChildTraversalVerificationStrategy. 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.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 unresolvedPvalue (returnsnullbeforeresolve()is called) as an index key, producing wrong results.TinkerGraphStepStrategydemonstrates the pattern: skip traversal-bearing HasSteps withcontinueso that subsequent literal HasSteps can still be folded. Providers with customGraphStepreplacements must also copy theidTraversalfield during strategy replacement, otherwiseV(traversal)falls through to returning all vertices.choose()restriction -choose(P)andchoose().option(P, ...)reject traversal-bearing predicates at construction time. This is a deliberate scope decision.PredicateTraversal(which evaluates option-key predicates inBranchStep) has access to the traverser and could technically callP.resolve()beforeP.test(), but extending the branching infrastructure is out of scope for this feature. Users needing dynamic branching can use thechoose(Traversal, ...)form where the branch predicate is a full traversal (e.g.,choose(__.is(P.gt(traversal)), trueChoice, falseChoice)). This works becauseIsStephandles resolution normally. Restrictingis()when used insidechoose()'s branch traversal is not feasible sincechoose()receives it as an opaqueTraversal.Adminand does not inspect internal steps.What's NOT in this PR
P.resolve()thread-safety refactoring (future)instanceof DefaultGraphTraversalcleanup (deferred)FilterRankingStrategycost awarenessCompatibility
has(key, traversal)still serializes ashas(key, traversal), verified byGremlinLangTraversalRoundTripTestandGremlinQueryParserTraversalTest.Testing
All
gremlin-coreunit tests pass (9132, 0 failures) and the full TinkerGraph Gherkin suite passes (2172 scenarios, 0 failures) against the currentmaster.HasTraversal.feature,IsTraversal.feature,WhereTraversal.feature,VETraversal.feature,PropertyTraversal.feature,ChildTraversalVerification.featurePTraversalTest,CloneIndependenceTraversalTest,HasContainerTest,ChildTraversalValidatorTest,ChildTraversalVerificationStrategyTestGremlinQueryParserTraversalTest,GremlinLangTraversalRoundTripTestTinkerGraphStepStrategyTraversalTestChecklist
the-traversal.asciidoc)release-4.x.x.asciidoc)mvn clean installpassesVOTE +1