You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tinkerpop.apache.org by sp...@apache.org on 2025/08/21 18:29:04 UTC
(tinkerpop) 01/03: Tests for repeat().times(0) and semantics clarity CTR
This is an automated email from the ASF dual-hosted git repository.
spmallette pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
commit a20f3861f793bc465e83a3e1245a56024e2de3fa
Author: Stephen Mallette <st...@amazon.com>
AuthorDate: Thu Aug 21 12:43:03 2025 -0400
Tests for repeat().times(0) and semantics clarity CTR
---
docs/src/dev/provider/gremlin-semantics.asciidoc | 73 ++++++++++++++++++++++
.../gremlin/structure/service/ServiceRegistry.java | 1 -
.../Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs | 4 ++
gremlin-go/driver/cucumber/gremlin.go | 4 ++
.../gremlin-javascript/test/cucumber/gremlin.js | 4 ++
gremlin-python/src/main/python/radish/gremlin.py | 4 ++
.../gremlin/test/features/branch/Repeat.feature | 46 +++++++++++++-
7 files changed, 134 insertions(+), 2 deletions(-)
diff --git a/docs/src/dev/provider/gremlin-semantics.asciidoc b/docs/src/dev/provider/gremlin-semantics.asciidoc
index 1ef5dfe65d..399ab1d5b7 100644
--- a/docs/src/dev/provider/gremlin-semantics.asciidoc
+++ b/docs/src/dev/provider/gremlin-semantics.asciidoc
@@ -1380,6 +1380,79 @@ applies to list types which means that non-iterable types (including null) will
See: link:https://github.com/apache/tinkerpop/tree/x.y.z/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/ProductStep.java[source],
link:https://tinkerpop.apache.org/docs/x.y.z/reference/#product-step[reference]
+[[repeat-step]]
+=== repeat()
+
+*Description:* Iteratively applies a traversal (the "loop body") to each incoming traverser until a stopping
+condition is met. Optionally, it can emit traversers on each iteration according to an emit predicate. The
+repeat step supports loop naming and a loop counter via `loops()`.
+
+*Syntax:* `repeat(Traversal repeatTraversal)` | `repeat(String loopName, Traversal repeatTraversal)`
+
+[width="100%",options="header"]
+|=========================================================
+|Start Step |Mid Step |Modulated |Domain |Range
+|N |Y |`emit()`, `until()`, `times()` |`any` |`any`
+|=========================================================
+
+*Arguments:*
+
+* `repeatTraversal` - The traversal that represents the loop body to apply on each iteration.
+* `loopName` - Optional name used to identify the loop for nested loops and to access a specific counter via
+`loops(loopName)`.
+
+*Modulation:*
+
+* `emit()` | `emit(Traversal<?, ?> emitTraversal)` | `emit(Predicate<Traverser<?>> emitPredicate)` - Controls if/when a
+ traverser is emitted to the downstream of repeat() in addition to being looped again. If supplied before `repeat(...)`
+ the predicate is evaluated prior to the first iteration (pre-emit). If supplied after `repeat(...)`, the predicate is
+ evaluated after each completed iteration (post-emit). Calling `emit()` without arguments is equivalent to a predicate
+ that always evaluates to true at the given check position.
+* `until(Traversal<?, ?> untilTraversal)` | `until(Predicate<Traverser<?>> untilPredicate)` - Controls when repetition
+ stops. If supplied before `repeat(...)` the predicate is evaluated prior to the first iteration (pre-check). If the
+ predicate is true, the traverser will pass downstream without any loop iteration. If supplied after `repeat(...)`, the
+ predicate is evaluated after each completed iteration (post-check). When the predicate is true, the traverser stops
+ repeating and passes downstream.
+* `times(int n)` - Convenience for a loop bound. Equivalent to `until(loops().is(n))` when placed after `repeat(...)`
+ (post-check), and equivalent to `until(loops().is(n))` placed before `repeat(...)` (pre-check) when specified before.
+ See Considerations for details and examples.
+
+*Considerations:*
+
+- Evaluation order matters. The placement of `emit()` and `until()` relative to `repeat()` controls whether their
+predicates are evaluated before the first iteration (pre) or after each iteration (post) allowing for `while/do` or
+`do/while` semantics respectively:
+ - Pre-check / pre-emit: when the modulator appears before `repeat(...)`.
+ - Post-check / post-emit: when the modulator appears after `repeat(...)`.
+- Loop counter semantics:
+ - The loop counter for a given named or unnamed repeat is incremented once per completion of the loop body (i.e.,
+after the body finishes), not before. Therefore, `loops()` reflects the number of completed iterations.
+ - `loops()` without arguments returns the counter for the closest (innermost) `repeat()`. `loops("name")` returns the
+counter for the named loop.
+- Re-queuing for the next iteration:
+ - After each iteration, if `until` is not satisfied at the post-check, the traverser is sent back into the loop body
+for another iteration. If it is satisfied, the traverser exits the loop and proceeds downstream.
+- Interaction of `times(n)`:
+ - `g.V().repeat(x).times(2)` applies `x` exactly twice; no values are emitted unless `emit()` is specified.
+ - `g.V().emit().repeat(x).times(2)` emits the original input (pre-emit) and then the results of each iteration.
+ - Placing `times(0)` before `repeat(...)` yields no iterations and passes the input downstream unchanged.
+ - Placing `times(0)` after `repeat(...)` yields the same as `times(1)` because of `do/while` semantics.
+- Errors when `repeatTraversal` is missing:
+ - Using `emit()`, `until()`, or `times()` without an associated `repeat()` will raise an error at iteration time with a
+message containing: `The repeat()-traversal was not defined`.
+- Nested repeats and loop names:
+ - Nested `repeat()` steps maintain separate loop counters. Use `repeat("a", ...)` and `loops("a")` to reference a
+ specific counter inside nested loops.
+
+*Exceptions*
+
+* Using `emit()`, `until()`, or `times()` without a matching `repeat()` will raise an `IllegalStateException` at runtime
+ when the step is initialized during iteration with the message containing: `The repeat()-traversal was not defined`.
+
+See: link:https://github.com/apache/tinkerpop/tree/x.y.z/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/branch/RepeatStep.java[source],
+link:https://tinkerpop.apache.org/docs/x.y.z/reference/#repeat-step[reference],
+link:https://github.com/apache/tinkerpop/tree/x.y.z/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/branch/Repeat.feature[tests]
+
[[replace-step]]
=== replace()
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/service/ServiceRegistry.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/service/ServiceRegistry.java
index aae9bed8bf..8aba51bd3c 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/service/ServiceRegistry.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/service/ServiceRegistry.java
@@ -25,7 +25,6 @@ import org.apache.tinkerpop.gremlin.structure.util.CloseableIterator;
import org.apache.tinkerpop.shaded.jackson.core.JsonProcessingException;
import org.apache.tinkerpop.shaded.jackson.databind.ObjectMapper;
-import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs
index 47c8064581..289228fe00 100644
--- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs
@@ -119,6 +119,10 @@ namespace Gremlin.Net.IntegrationTest.Gherkin
{"g_V_emit", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Emit()}},
{"g_V_untilXidentityX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Until(__.Identity())}},
{"g_V_timesX5X", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Times(5)}},
+ {"g_V_haxXperson_name_markoX_repeatXoutXcreatedXX_timesX1X_name", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Has("person","name","marko").Repeat(__.Out("created")).Times(1).Values<object>("name")}},
+ {"g_V_haxXperson_name_markoX_repeatXoutXcreatedXX_timesX0X_name", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Has("person","name","marko").Repeat(__.Out("created")).Times(0).Values<object>("name")}},
+ {"g_V_haxXperson_name_markoX_timesX1X_repeatXoutXcreatedXX_name", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Has("person","name","marko").Times(1).Repeat(__.Out("created")).Values<object>("name")}},
+ {"g_V_haxXperson_name_markoX_timesX0X_repeatXoutXcreatedXX_name", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Has("person","name","marko").Times(0).Repeat(__.Out("created")).Values<object>("name")}},
{"g_unionXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Union<object>()}},
{"g_unionXV_name", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Union<object>(__.V().Values<object>("name"))}},
{"g_unionXVXv1X_VX4XX_name", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Union<object>(__.V((Vertex) p["v1"]),__.V((Vertex) p["v4"])).Values<object>("name")}},
diff --git a/gremlin-go/driver/cucumber/gremlin.go b/gremlin-go/driver/cucumber/gremlin.go
index 27cfb0aa58..1ce169f7f5 100644
--- a/gremlin-go/driver/cucumber/gremlin.go
+++ b/gremlin-go/driver/cucumber/gremlin.go
@@ -90,6 +90,10 @@ var translationMap = map[string][]func(g *gremlingo.GraphTraversalSource, p map[
"g_V_emit": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Emit()}},
"g_V_untilXidentityX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Until(gremlingo.T__.Identity())}},
"g_V_timesX5X": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Times(5)}},
+ "g_V_haxXperson_name_markoX_repeatXoutXcreatedXX_timesX1X_name": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Has("person", "name", "marko").Repeat(gremlingo.T__.Out("created")).Times(1).Values("name")}},
+ "g_V_haxXperson_name_markoX_repeatXoutXcreatedXX_timesX0X_name": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Has("person", "name", "marko").Repeat(gremlingo.T__.Out("created")).Times(0).Values("name")}},
+ "g_V_haxXperson_name_markoX_timesX1X_repeatXoutXcreatedXX_name": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Has("person", "name", "marko").Times(1).Repeat(gremlingo.T__.Out("created")).Values("name")}},
+ "g_V_haxXperson_name_markoX_timesX0X_repeatXoutXcreatedXX_name": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Has("person", "name", "marko").Times(0).Repeat(gremlingo.T__.Out("created")).Values("name")}},
"g_unionXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Union()}},
"g_unionXV_name": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Union(gremlingo.T__.V().Values("name"))}},
"g_unionXVXv1X_VX4XX_name": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Union(gremlingo.T__.V(p["v1"]), gremlingo.T__.V(p["v4"])).Values("name")}},
diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/gremlin.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/gremlin.js
index 6d6847a0fc..a15710cfb1 100644
--- a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/gremlin.js
+++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/gremlin.js
@@ -110,6 +110,10 @@ const gremlins = {
g_V_emit: [function({g}) { return g.V().emit() }],
g_V_untilXidentityX: [function({g}) { return g.V().until(__.identity()) }],
g_V_timesX5X: [function({g}) { return g.V().times(5) }],
+ g_V_haxXperson_name_markoX_repeatXoutXcreatedXX_timesX1X_name: [function({g}) { return g.V().has("person","name","marko").repeat(__.out("created")).times(1).values("name") }],
+ g_V_haxXperson_name_markoX_repeatXoutXcreatedXX_timesX0X_name: [function({g}) { return g.V().has("person","name","marko").repeat(__.out("created")).times(0).values("name") }],
+ g_V_haxXperson_name_markoX_timesX1X_repeatXoutXcreatedXX_name: [function({g}) { return g.V().has("person","name","marko").times(1).repeat(__.out("created")).values("name") }],
+ g_V_haxXperson_name_markoX_timesX0X_repeatXoutXcreatedXX_name: [function({g}) { return g.V().has("person","name","marko").times(0).repeat(__.out("created")).values("name") }],
g_unionXX: [function({g}) { return g.union() }],
g_unionXV_name: [function({g}) { return g.union(__.V().values("name")) }],
g_unionXVXv1X_VX4XX_name: [function({g, v4, v1}) { return g.union(__.V(v1),__.V(v4)).values("name") }],
diff --git a/gremlin-python/src/main/python/radish/gremlin.py b/gremlin-python/src/main/python/radish/gremlin.py
index 4fd1a36a45..9601ce5ee6 100644
--- a/gremlin-python/src/main/python/radish/gremlin.py
+++ b/gremlin-python/src/main/python/radish/gremlin.py
@@ -92,6 +92,10 @@ world.gremlins = {
'g_V_emit': [(lambda g:g.V().emit())],
'g_V_untilXidentityX': [(lambda g:g.V().until(__.identity()))],
'g_V_timesX5X': [(lambda g:g.V().times(5))],
+ 'g_V_haxXperson_name_markoX_repeatXoutXcreatedXX_timesX1X_name': [(lambda g:g.V().has('person','name','marko').repeat(__.out('created')).times(1).name)],
+ 'g_V_haxXperson_name_markoX_repeatXoutXcreatedXX_timesX0X_name': [(lambda g:g.V().has('person','name','marko').repeat(__.out('created')).times(0).name)],
+ 'g_V_haxXperson_name_markoX_timesX1X_repeatXoutXcreatedXX_name': [(lambda g:g.V().has('person','name','marko').times(1).repeat(__.out('created')).name)],
+ 'g_V_haxXperson_name_markoX_timesX0X_repeatXoutXcreatedXX_name': [(lambda g:g.V().has('person','name','marko').times(0).repeat(__.out('created')).name)],
'g_unionXX': [(lambda g:g.union())],
'g_unionXV_name': [(lambda g:g.union(__.V().name))],
'g_unionXVXv1X_VX4XX_name': [(lambda g, v4=None,v1=None:g.union(__.V(v1),__.V(v4)).name)],
diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/branch/Repeat.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/branch/Repeat.feature
index 113ab6adac..ba95f1c45a 100644
--- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/branch/Repeat.feature
+++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/branch/Repeat.feature
@@ -397,4 +397,48 @@ Feature: Step - repeat()
g.V().times(5)
"""
When iterated to list
- Then the traversal will raise an error with message containing text of "The repeat()-traversal was not defined"
\ No newline at end of file
+ Then the traversal will raise an error with message containing text of "The repeat()-traversal was not defined"
+
+ Scenario: g_V_haxXperson_name_markoX_repeatXoutXcreatedXX_timesX1X_name
+ Given the modern graph
+ And the traversal of
+ """
+ g.V().has("person","name","marko").repeat(__.out("created")).times(1).values("name")
+ """
+ When iterated to list
+ Then the result should be unordered
+ | result |
+ | lop |
+
+ Scenario: g_V_haxXperson_name_markoX_repeatXoutXcreatedXX_timesX0X_name
+ Given the modern graph
+ And the traversal of
+ """
+ g.V().has("person","name","marko").repeat(out("created")).times(0).values("name")
+ """
+ When iterated to list
+ Then the result should be unordered
+ | result |
+ | lop |
+
+ Scenario: g_V_haxXperson_name_markoX_timesX1X_repeatXoutXcreatedXX_name
+ Given the modern graph
+ And the traversal of
+ """
+ g.V().has("person","name","marko").times(1).repeat(out("created")).values("name")
+ """
+ When iterated to list
+ Then the result should be unordered
+ | result |
+ | lop |
+
+ Scenario: g_V_haxXperson_name_markoX_timesX0X_repeatXoutXcreatedXX_name
+ Given the modern graph
+ And the traversal of
+ """
+ g.V().has("person","name","marko").times(0).repeat(out("created")).values("name")
+ """
+ When iterated to list
+ Then the result should be unordered
+ | result |
+ | marko |
\ No newline at end of file