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 <stepm...@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