Skip to content

Commit a20f386

Browse files
committed
Tests for repeat().times(0) and semantics clarity CTR
1 parent 3ddf400 commit a20f386

File tree

7 files changed

+134
-2
lines changed

7 files changed

+134
-2
lines changed

docs/src/dev/provider/gremlin-semantics.asciidoc

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1380,6 +1380,79 @@ applies to list types which means that non-iterable types (including null) will
13801380
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],
13811381
link:https://tinkerpop.apache.org/docs/x.y.z/reference/#product-step[reference]
13821382
1383+
[[repeat-step]]
1384+
=== repeat()
1385+
1386+
*Description:* Iteratively applies a traversal (the "loop body") to each incoming traverser until a stopping
1387+
condition is met. Optionally, it can emit traversers on each iteration according to an emit predicate. The
1388+
repeat step supports loop naming and a loop counter via `loops()`.
1389+
1390+
*Syntax:* `repeat(Traversal repeatTraversal)` | `repeat(String loopName, Traversal repeatTraversal)`
1391+
1392+
[width="100%",options="header"]
1393+
|=========================================================
1394+
|Start Step |Mid Step |Modulated |Domain |Range
1395+
|N |Y |`emit()`, `until()`, `times()` |`any` |`any`
1396+
|=========================================================
1397+
1398+
*Arguments:*
1399+
1400+
* `repeatTraversal` - The traversal that represents the loop body to apply on each iteration.
1401+
* `loopName` - Optional name used to identify the loop for nested loops and to access a specific counter via
1402+
`loops(loopName)`.
1403+
1404+
*Modulation:*
1405+
1406+
* `emit()` | `emit(Traversal<?, ?> emitTraversal)` | `emit(Predicate<Traverser<?>> emitPredicate)` - Controls if/when a
1407+
traverser is emitted to the downstream of repeat() in addition to being looped again. If supplied before `repeat(...)`
1408+
the predicate is evaluated prior to the first iteration (pre-emit). If supplied after `repeat(...)`, the predicate is
1409+
evaluated after each completed iteration (post-emit). Calling `emit()` without arguments is equivalent to a predicate
1410+
that always evaluates to true at the given check position.
1411+
* `until(Traversal<?, ?> untilTraversal)` | `until(Predicate<Traverser<?>> untilPredicate)` - Controls when repetition
1412+
stops. If supplied before `repeat(...)` the predicate is evaluated prior to the first iteration (pre-check). If the
1413+
predicate is true, the traverser will pass downstream without any loop iteration. If supplied after `repeat(...)`, the
1414+
predicate is evaluated after each completed iteration (post-check). When the predicate is true, the traverser stops
1415+
repeating and passes downstream.
1416+
* `times(int n)` - Convenience for a loop bound. Equivalent to `until(loops().is(n))` when placed after `repeat(...)`
1417+
(post-check), and equivalent to `until(loops().is(n))` placed before `repeat(...)` (pre-check) when specified before.
1418+
See Considerations for details and examples.
1419+
1420+
*Considerations:*
1421+
1422+
- Evaluation order matters. The placement of `emit()` and `until()` relative to `repeat()` controls whether their
1423+
predicates are evaluated before the first iteration (pre) or after each iteration (post) allowing for `while/do` or
1424+
`do/while` semantics respectively:
1425+
- Pre-check / pre-emit: when the modulator appears before `repeat(...)`.
1426+
- Post-check / post-emit: when the modulator appears after `repeat(...)`.
1427+
- Loop counter semantics:
1428+
- The loop counter for a given named or unnamed repeat is incremented once per completion of the loop body (i.e.,
1429+
after the body finishes), not before. Therefore, `loops()` reflects the number of completed iterations.
1430+
- `loops()` without arguments returns the counter for the closest (innermost) `repeat()`. `loops("name")` returns the
1431+
counter for the named loop.
1432+
- Re-queuing for the next iteration:
1433+
- After each iteration, if `until` is not satisfied at the post-check, the traverser is sent back into the loop body
1434+
for another iteration. If it is satisfied, the traverser exits the loop and proceeds downstream.
1435+
- Interaction of `times(n)`:
1436+
- `g.V().repeat(x).times(2)` applies `x` exactly twice; no values are emitted unless `emit()` is specified.
1437+
- `g.V().emit().repeat(x).times(2)` emits the original input (pre-emit) and then the results of each iteration.
1438+
- Placing `times(0)` before `repeat(...)` yields no iterations and passes the input downstream unchanged.
1439+
- Placing `times(0)` after `repeat(...)` yields the same as `times(1)` because of `do/while` semantics.
1440+
- Errors when `repeatTraversal` is missing:
1441+
- Using `emit()`, `until()`, or `times()` without an associated `repeat()` will raise an error at iteration time with a
1442+
message containing: `The repeat()-traversal was not defined`.
1443+
- Nested repeats and loop names:
1444+
- Nested `repeat()` steps maintain separate loop counters. Use `repeat("a", ...)` and `loops("a")` to reference a
1445+
specific counter inside nested loops.
1446+
1447+
*Exceptions*
1448+
1449+
* Using `emit()`, `until()`, or `times()` without a matching `repeat()` will raise an `IllegalStateException` at runtime
1450+
when the step is initialized during iteration with the message containing: `The repeat()-traversal was not defined`.
1451+
1452+
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],
1453+
link:https://tinkerpop.apache.org/docs/x.y.z/reference/#repeat-step[reference],
1454+
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]
1455+
13831456
[[replace-step]]
13841457
=== replace()
13851458

gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/service/ServiceRegistry.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
import org.apache.tinkerpop.shaded.jackson.core.JsonProcessingException;
2626
import org.apache.tinkerpop.shaded.jackson.databind.ObjectMapper;
2727

28-
import java.util.HashMap;
2928
import java.util.LinkedHashMap;
3029
import java.util.Map;
3130
import java.util.Objects;

gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,10 @@ private static IDictionary<string, List<Func<GraphTraversalSource, IDictionary<s
119119
{"g_V_emit", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Emit()}},
120120
{"g_V_untilXidentityX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Until(__.Identity())}},
121121
{"g_V_timesX5X", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Times(5)}},
122+
{"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")}},
123+
{"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")}},
124+
{"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")}},
125+
{"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")}},
122126
{"g_unionXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Union<object>()}},
123127
{"g_unionXV_name", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Union<object>(__.V().Values<object>("name"))}},
124128
{"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")}},

gremlin-go/driver/cucumber/gremlin.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ var translationMap = map[string][]func(g *gremlingo.GraphTraversalSource, p map[
9090
"g_V_emit": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Emit()}},
9191
"g_V_untilXidentityX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Until(gremlingo.T__.Identity())}},
9292
"g_V_timesX5X": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Times(5)}},
93+
"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")}},
94+
"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")}},
95+
"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")}},
96+
"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")}},
9397
"g_unionXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Union()}},
9498
"g_unionXV_name": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Union(gremlingo.T__.V().Values("name"))}},
9599
"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")}},

gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/gremlin.js

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gremlin-python/src/main/python/radish/gremlin.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@
9292
'g_V_emit': [(lambda g:g.V().emit())],
9393
'g_V_untilXidentityX': [(lambda g:g.V().until(__.identity()))],
9494
'g_V_timesX5X': [(lambda g:g.V().times(5))],
95+
'g_V_haxXperson_name_markoX_repeatXoutXcreatedXX_timesX1X_name': [(lambda g:g.V().has('person','name','marko').repeat(__.out('created')).times(1).name)],
96+
'g_V_haxXperson_name_markoX_repeatXoutXcreatedXX_timesX0X_name': [(lambda g:g.V().has('person','name','marko').repeat(__.out('created')).times(0).name)],
97+
'g_V_haxXperson_name_markoX_timesX1X_repeatXoutXcreatedXX_name': [(lambda g:g.V().has('person','name','marko').times(1).repeat(__.out('created')).name)],
98+
'g_V_haxXperson_name_markoX_timesX0X_repeatXoutXcreatedXX_name': [(lambda g:g.V().has('person','name','marko').times(0).repeat(__.out('created')).name)],
9599
'g_unionXX': [(lambda g:g.union())],
96100
'g_unionXV_name': [(lambda g:g.union(__.V().name))],
97101
'g_unionXVXv1X_VX4XX_name': [(lambda g, v4=None,v1=None:g.union(__.V(v1),__.V(v4)).name)],

gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/branch/Repeat.feature

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -397,4 +397,48 @@ Feature: Step - repeat()
397397
g.V().times(5)
398398
"""
399399
When iterated to list
400-
Then the traversal will raise an error with message containing text of "The repeat()-traversal was not defined"
400+
Then the traversal will raise an error with message containing text of "The repeat()-traversal was not defined"
401+
402+
Scenario: g_V_haxXperson_name_markoX_repeatXoutXcreatedXX_timesX1X_name
403+
Given the modern graph
404+
And the traversal of
405+
"""
406+
g.V().has("person","name","marko").repeat(__.out("created")).times(1).values("name")
407+
"""
408+
When iterated to list
409+
Then the result should be unordered
410+
| result |
411+
| lop |
412+
413+
Scenario: g_V_haxXperson_name_markoX_repeatXoutXcreatedXX_timesX0X_name
414+
Given the modern graph
415+
And the traversal of
416+
"""
417+
g.V().has("person","name","marko").repeat(out("created")).times(0).values("name")
418+
"""
419+
When iterated to list
420+
Then the result should be unordered
421+
| result |
422+
| lop |
423+
424+
Scenario: g_V_haxXperson_name_markoX_timesX1X_repeatXoutXcreatedXX_name
425+
Given the modern graph
426+
And the traversal of
427+
"""
428+
g.V().has("person","name","marko").times(1).repeat(out("created")).values("name")
429+
"""
430+
When iterated to list
431+
Then the result should be unordered
432+
| result |
433+
| lop |
434+
435+
Scenario: g_V_haxXperson_name_markoX_timesX0X_repeatXoutXcreatedXX_name
436+
Given the modern graph
437+
And the traversal of
438+
"""
439+
g.V().has("person","name","marko").times(0).repeat(out("created")).values("name")
440+
"""
441+
When iterated to list
442+
Then the result should be unordered
443+
| result |
444+
| marko |

0 commit comments

Comments
 (0)