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 2023/05/18 18:39:16 UTC

[tinkerpop] 01/01: TINKERPOP-2873 Added union() as a start step

This is an automated email from the ASF dual-hosted git repository.

spmallette pushed a commit to branch TINKERPOP-2873
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git

commit 143d187b5a5a700465071f4dfdd4f82620b17496
Author: Stephen Mallette <st...@amazon.com>
AuthorDate: Thu May 18 14:38:40 2023 -0400

    TINKERPOP-2873 Added union() as a start step
---
 CHANGELOG.asciidoc                                 |  1 +
 docs/src/reference/the-traversal.asciidoc          |  2 +
 docs/src/upgrade/release-3.7.x.asciidoc            | 44 +++++++++++---
 .../grammar/DefaultGremlinBaseVisitor.java         |  5 +-
 .../language/grammar/TraversalRootVisitor.java     |  2 +-
 .../grammar/TraversalSourceSpawnMethodVisitor.java | 13 +++-
 .../traversal/dsl/graph/GraphTraversalSource.java  | 17 ++++++
 .../process/traversal/step/branch/UnionStep.java   | 33 +++++++++-
 gremlin-go/driver/cucumber/gremlin.go              |  5 ++
 gremlin-go/driver/graphTraversalSource.go          |  7 +++
 .../lib/process/graph-traversal.js                 | 10 +++
 .../gremlin-javascript/test/cucumber/gremlin.js    |  5 ++
 gremlin-language/src/main/antlr4/Gremlin.g4        |  5 ++
 .../gremlin_python/process/graph_traversal.py      |  5 ++
 .../gremlin/test/features/branch/Union.feature     | 71 ++++++++++++++++++++++
 15 files changed, 211 insertions(+), 14 deletions(-)

diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc
index 3399827f9f..2778c756ad 100644
--- a/CHANGELOG.asciidoc
+++ b/CHANGELOG.asciidoc
@@ -28,6 +28,7 @@ This release also includes changes from <<release-3-6-XXX, 3.6.XXX>>.
 * Removed `connectOnStartup` configuration option from gremlin-javascript.
 * Changed `Gremlin.version()` to read from the more specificly named `tinkerpop-version` attribute.
 * Added warning on vertex property cardinality mismatch when reading GraphML.
+* Added a `union()` start step.
 * Bumped to `ws` 8.x for `gremlin-javascript`.
 * Added support for mid-traversal `E()`-steps to Gremlin core and GLV's.
 * Added nullable annotations to Gremlin.NET.
diff --git a/docs/src/reference/the-traversal.asciidoc b/docs/src/reference/the-traversal.asciidoc
index 99cbf29bda..5ab49721ac 100644
--- a/docs/src/reference/the-traversal.asciidoc
+++ b/docs/src/reference/the-traversal.asciidoc
@@ -4277,6 +4277,8 @@ g.V(4).union(
 g.V(4).union(
          __.in().values('age'),
          out().values('lang')).path()
+g.union(V().has('person','name','vadas'),
+        V().has('software','name','lop').in('created'))
 ----
 
 *Additional References*
diff --git a/docs/src/upgrade/release-3.7.x.asciidoc b/docs/src/upgrade/release-3.7.x.asciidoc
index cf4c80b128..9450706119 100644
--- a/docs/src/upgrade/release-3.7.x.asciidoc
+++ b/docs/src/upgrade/release-3.7.x.asciidoc
@@ -29,6 +29,33 @@ Please see the link:https://github.com/apache/tinkerpop/blob/3.7.0/CHANGELOG.asc
 
 === Upgrading for Users
 
+==== union() Start Step
+
+The `union()`-step could only be used mid-traversal after a start step. The typical workaround for this issue was to
+use `inject()` with a dummy value to start the traversal and then utilize `union()`:
+
+[source,text]
+----
+gremlin> g.inject(0).union(V().has('name','vadas'),
+......1>                   V().has('software','name','lop').in('created')).
+......2>   values('name')
+==>vadas
+==>marko
+==>josh
+==>peter
+----
+
+As of this version, `union()` can be used more directly to avoid the workaround:
+
+[source,text]
+----
+g.union(V().has('name','vadas'),
+        V().has('software','name','lop').in('created')).
+  values('name')
+----
+
+See: link:https://issues.apache.org/jira/browse/TINKERPOP-2873[TINKERPOP-2873]
+
 ==== Properties on Elements
 
 ===== Introduction
@@ -43,7 +70,7 @@ If you need to get a property, then this can be explicitly configured with `Halt
 
 [source,java]
 ----
-  g.withComputer().withStrategies(HaltedTraverserFactoryStrategy.detached())
+g.withComputer().withStrategies(HaltedTraverserFactoryStrategy.detached())
 ----
 
 ===== Output comparison for Gremlin Server 3.5/3.6 and 3.7
@@ -52,24 +79,24 @@ Let's take a closer look at a Javascript GLV code example in 3.6 and 3.7:
 
 [source,javascript]
 ----
-  const client = new Client('ws://localhost:8182/gremlin',{traversalSource: 'gmodern'});
-  await client.open();
-  const result = await client.submit('g.V(1)');
-  console.log(JSON.stringify(result.first()));
-  await client.close();
+const client = new Client('ws://localhost:8182/gremlin',{traversalSource: 'gmodern'});
+await client.open();
+const result = await client.submit('g.V(1)');
+console.log(JSON.stringify(result.first()));
+await client.close();
 ----
 
 The result will be different depending on the version of Gremlin Server.
 For 3.5/3.6:
 [source,json]
 ----
-  {"id":1,"label":"person"}
+{"id":1,"label":"person"}
 ----
 
 For 3.7:
 [source,json]
 ----
-  {"id":1,"label":"person","properties":{"name":[{"id":0,"label":"name","value":"marko","key":"name"}],"age":[{"id":1,"label":"age","value":29,"key":"age"}]}}
+{"id":1,"label":"person","properties":{"name":[{"id":0,"label":"name","value":"marko","key":"name"}],"age":[{"id":1,"label":"age","value":29,"key":"age"}]}}
 ---- 
 
 ===== Enabling the previous behavior
@@ -96,7 +123,6 @@ ReferenceElement-type objects are no longer returned - you get a DetachedElement
 
 See: link:https://issues.apache.org/jira/browse/TINKERPOP-2824[TINKERPOP-2824]
 
-
 ==== Gremlin.NET: Nullable Annotations
 
 Gremlin.NET now uses link:https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references#nullable-variable-annotations[nullable annotations]
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/DefaultGremlinBaseVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/DefaultGremlinBaseVisitor.java
index 4531861838..798578dfac 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/DefaultGremlinBaseVisitor.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/DefaultGremlinBaseVisitor.java
@@ -1432,8 +1432,11 @@ public class DefaultGremlinBaseVisitor<T> extends AbstractParseTreeVisitor<T> im
 	 * {@inheritDoc}
 	 */
 	@Override public T visitTraversalMethod_mergeV_empty(final GremlinParser.TraversalMethod_mergeV_emptyContext ctx) { notImplemented(ctx); return null; }
+
+	@Override public T visitTraversalMethod_mergeE_empty(final GremlinParser.TraversalMethod_mergeE_emptyContext ctx) { notImplemented(ctx); return null; }
 	/**
 	 * {@inheritDoc}
 	 */
-	@Override public T visitTraversalMethod_mergeE_empty(final GremlinParser.TraversalMethod_mergeE_emptyContext ctx) { notImplemented(ctx); return null; }
+	@Override
+	public T visitTraversalSourceSpawnMethod_union(final GremlinParser.TraversalSourceSpawnMethod_unionContext ctx) { notImplemented(ctx); return null; }
 }
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalRootVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalRootVisitor.java
index 3d3ca4f695..0e52b606d5 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalRootVisitor.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalRootVisitor.java
@@ -75,7 +75,7 @@ public class TraversalRootVisitor<G extends Traversal> extends DefaultGremlinBas
                 (GremlinParser.TraversalSourceContext) ctx.getChild(childIndexOfTraversalSource));
         // call traversal source spawn method
         final int childIndexOfTraversalSourceSpawnMethod = 2;
-        final GraphTraversal traversal = new TraversalSourceSpawnMethodVisitor(source, this).visitTraversalSourceSpawnMethod(
+        final GraphTraversal traversal = new TraversalSourceSpawnMethodVisitor(source, this, antlr).visitTraversalSourceSpawnMethod(
                 (GremlinParser.TraversalSourceSpawnMethodContext) ctx.getChild(childIndexOfTraversalSourceSpawnMethod));
 
         if (ctx.getChildCount() == 5) {
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalSourceSpawnMethodVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalSourceSpawnMethodVisitor.java
index 5d1ebaa8c9..de890a0c5f 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalSourceSpawnMethodVisitor.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalSourceSpawnMethodVisitor.java
@@ -33,10 +33,14 @@ public class TraversalSourceSpawnMethodVisitor extends DefaultGremlinBaseVisitor
     protected GraphTraversal graphTraversal;
     protected final DefaultGremlinBaseVisitor<Traversal> anonymousVisitor;
 
+    protected final GremlinAntlrToJava antlr;
+
     public TraversalSourceSpawnMethodVisitor(final GraphTraversalSource traversalSource,
-                                             final DefaultGremlinBaseVisitor<Traversal> anonymousVisitor) {
+                                             final DefaultGremlinBaseVisitor<Traversal> anonymousVisitor,
+                                             final GremlinAntlrToJava antlr) {
         this.traversalSource = traversalSource;
         this.anonymousVisitor = anonymousVisitor;
+        this.antlr = antlr;
     }
 
     /**
@@ -200,4 +204,11 @@ public class TraversalSourceSpawnMethodVisitor extends DefaultGremlinBaseVisitor
                 anonymousVisitor.visitNestedTraversal(ctx.nestedTraversal()));
     }
 
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public GraphTraversal visitTraversalSourceSpawnMethod_union(final GremlinParser.TraversalSourceSpawnMethod_unionContext ctx) {
+        return this.traversalSource.union(antlr.tListVisitor.visitNestedTraversalList(ctx.nestedTraversalList()));
+    }
 }
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversalSource.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversalSource.java
index c12bf41e46..9dfd7499cc 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversalSource.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversalSource.java
@@ -27,6 +27,7 @@ import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
 import org.apache.tinkerpop.gremlin.process.traversal.TraversalSource;
 import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategies;
 import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategy;
+import org.apache.tinkerpop.gremlin.process.traversal.step.branch.UnionStep;
 import org.apache.tinkerpop.gremlin.process.traversal.step.map.AddEdgeStartStep;
 import org.apache.tinkerpop.gremlin.process.traversal.step.map.AddVertexStartStep;
 import org.apache.tinkerpop.gremlin.process.traversal.step.map.CallStep;
@@ -46,6 +47,7 @@ import org.apache.tinkerpop.gremlin.structure.Vertex;
 import org.apache.tinkerpop.gremlin.structure.util.StringFactory;
 import org.apache.tinkerpop.gremlin.structure.util.empty.EmptyGraph;
 
+import java.util.Arrays;
 import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.Optional;
@@ -554,6 +556,21 @@ public class GraphTraversalSource implements TraversalSource {
         return traversal.addStep(step);
     }
 
+    /**
+     * Merges the results of an arbitrary number of traversals.
+     *
+     * @param unionTraversals the traversals to merge
+     * @see <a href="http://tinkerpop.apache.org/docs/${project.version}/reference/#union-step" target="_blank">Reference Documentation - Union Step</a>
+     * @since 3.7.0
+     */
+    public <S> GraphTraversal<S, S> union(final Traversal<?, S>... unionTraversals) {
+        final GraphTraversalSource clone = this.clone();
+        clone.bytecode.addStep(GraphTraversal.Symbols.union, unionTraversals);
+        final GraphTraversal.Admin traversal = new DefaultGraphTraversal(clone);
+        final UnionStep<?, S> step = new UnionStep<>(traversal, true, Arrays.copyOf(unionTraversals, unionTraversals.length, Traversal.Admin[].class));
+        return traversal.addStep(step);
+    }
+
     /**
      * Performs a read or write based operation on the {@link Graph} backing this {@code GraphTraversalSource}. This
      * step can be accompanied by the {@link GraphTraversal#with(String, Object)} modulator for further configuration
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/branch/UnionStep.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/branch/UnionStep.java
index 1ce613733e..96060821c8 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/branch/UnionStep.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/branch/UnionStep.java
@@ -19,25 +19,48 @@
 package org.apache.tinkerpop.gremlin.process.traversal.step.branch;
 
 import org.apache.tinkerpop.gremlin.process.traversal.Pick;
+import org.apache.tinkerpop.gremlin.process.traversal.Step;
 import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
+import org.apache.tinkerpop.gremlin.process.traversal.Traverser;
+import org.apache.tinkerpop.gremlin.process.traversal.TraverserGenerator;
 import org.apache.tinkerpop.gremlin.process.traversal.lambda.ConstantTraversal;
 import org.apache.tinkerpop.gremlin.structure.util.StringFactory;
 
 import java.util.Collections;
+import java.util.Iterator;
 
 /**
  * @author Marko A. Rodriguez (http://markorodriguez.com)
  */
-public final class UnionStep<S, E> extends BranchStep<S, E, Pick> {
+public class UnionStep<S, E> extends BranchStep<S, E, Pick> {
 
-    public UnionStep(final Traversal.Admin traversal, final Traversal.Admin<?, E>... unionTraversals) {
+    private final boolean isStart;
+    protected boolean first = true;
+
+    public UnionStep(final Traversal.Admin traversal, final boolean isStart, final Traversal.Admin<?, E>... unionTraversals) {
         super(traversal);
+        this.isStart = isStart;
         this.setBranchTraversal(new ConstantTraversal<>(Pick.any));
         for (final Traversal.Admin<?, E> union : unionTraversals) {
             this.addChildOption(Pick.any, (Traversal.Admin) union);
         }
     }
 
+    public UnionStep(final Traversal.Admin traversal, final Traversal.Admin<?, E>... unionTraversals) {
+        this(traversal, false, unionTraversals);
+    }
+
+    @Override
+    protected Traverser.Admin<E> processNextStart() {
+        // when it's a start step a traverser needs to be created to kick off the traversal.
+        if (isStart && first) {
+            first = false;
+            final TraverserGenerator generator = this.getTraversal().getTraverserGenerator();
+            this.addStart(generator.generate(false, (Step) this, 1L));
+        }
+        return super.processNextStart();
+    }
+
     @Override
     public void addChildOption(final Pick pickToken, final Traversal.Admin<S, E> traversalOption) {
         if (Pick.any != pickToken)
@@ -45,6 +68,12 @@ public final class UnionStep<S, E> extends BranchStep<S, E, Pick> {
         super.addChildOption(pickToken, traversalOption);
     }
 
+    @Override
+    public void reset() {
+        super.reset();
+        first = true;
+    }
+
     @Override
     public String toString() {
         return StringFactory.stepString(this, this.traversalPickOptions.getOrDefault(Pick.any, Collections.emptyList()));
diff --git a/gremlin-go/driver/cucumber/gremlin.go b/gremlin-go/driver/cucumber/gremlin.go
index bfa60c2bb5..1133945263 100644
--- a/gremlin-go/driver/cucumber/gremlin.go
+++ b/gremlin-go/driver/cucumber/gremlin.go
@@ -86,6 +86,11 @@ var translationMap = map[string][]func(g *gremlingo.GraphTraversalSource, p map[
     "g_VX1X_repeatXrepeatXunionXout_uses_out_traversesXX_whereXloops_isX0X_timesX1X_timeX2X_name": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V(p["vid1"]).Repeat(gremlingo.T__.Repeat(gremlingo.T__.Union(gremlingo.T__.Out("uses"), gremlingo.T__.Out("traverses")).Where(gremlingo.T__.Loops().Is(0))).Times(1)).Times(2).Values("name")}}, 
     "g_V_repeatXa_outXknows_repeatXb_outXcreatedX_filterXloops_isX0XX_emit_lang": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Repeat("a", gremlingo.T__.Out("knows").Repeat("b", gremlingo.T__.Out("created").Filter(gremlingo.T__.Loops("a").Is(0))).Emit()).Emit().Values("lang")}}, 
     "g_VX6X_repeatXa_bothXcreatedX_simplePathX_emitXrepeatXb_bothXknowsXX_untilXloopsXbX_asXb_whereXloopsXaX_asXbX_hasXname_vadasXX_dedup_name": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V(p["vid6"]).Repeat("a", gremlingo.T__.Both("created").SimplePath()).Emit(gremlingo.T__.Repeat("b", gremlingo.T__.Both("knows")).Until(gremlingo.T__.Loops("b").As("b").Where(gremlingo.T__.Loops("a").As("b"))).Has("name", "vadas")).Dedup().Value [...]
+    "g_unionXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Union()}}, 
+    "g_unionXconstantX1X_constantX2X_constantX3XX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Union(gremlingo.T__.Constant(p["xx1"]), gremlingo.T__.Constant(p["xx2"]), gremlingo.T__.Constant(p["xx3"]))}}, 
+    "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")}}, 
+    "g_unionXV_hasLabelXsoftwareX_V_hasLabelXpersonXX_name": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Union(gremlingo.T__.V().HasLabel("software"), gremlingo.T__.V().HasLabel("person")).Values("name")}}, 
     "g_V_unionXout__inX_name": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Union(gremlingo.T__.Out(), gremlingo.T__.In()).Values("name")}}, 
     "g_VX1X_unionXrepeatXoutX_timesX2X__outX_name": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V(p["vid1"]).Union(gremlingo.T__.Repeat(gremlingo.T__.Out()).Times(2), gremlingo.T__.Out()).Values("name")}}, 
     "g_V_chooseXlabel_is_person__unionX__out_lang__out_nameX__in_labelX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Choose(gremlingo.T__.Label().Is("person"), gremlingo.T__.Union(gremlingo.T__.Out().Values("lang"), gremlingo.T__.Out().Values("name")), gremlingo.T__.In().Label())}}, 
diff --git a/gremlin-go/driver/graphTraversalSource.go b/gremlin-go/driver/graphTraversalSource.go
index b8f866048d..31ee2d4be7 100644
--- a/gremlin-go/driver/graphTraversalSource.go
+++ b/gremlin-go/driver/graphTraversalSource.go
@@ -215,6 +215,13 @@ func (gts *GraphTraversalSource) MergeV(args ...interface{}) *GraphTraversal {
 	return traversal
 }
 
+// Union allows for a multi-branched start to a traversal.
+func (gts *GraphTraversalSource) Union(args ...interface{}) *GraphTraversal {
+	traversal := gts.GetGraphTraversal()
+	traversal.Bytecode.AddStep("union", args...)
+	return traversal
+}
+
 func (gts *GraphTraversalSource) Tx() *Transaction {
 	return &Transaction{g: gts, remoteConnection: gts.remoteConnection}
 }
diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/process/graph-traversal.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/process/graph-traversal.js
index 60b03ccf80..328c7f3ed9 100644
--- a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/process/graph-traversal.js
+++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/process/graph-traversal.js
@@ -337,6 +337,16 @@ class GraphTraversalSource {
     const b = new Bytecode(this.bytecode).addStep('call', args);
     return new this.graphTraversalClass(this.graph, new TraversalStrategies(this.traversalStrategies), b);
   }
+
+  /**
+   * union GraphTraversalSource method.
+   * @param {...Object} args
+   * @returns {GraphTraversal}
+   */
+  union(...args) {
+    const b = new Bytecode(this.bytecode).addStep('union', args);
+    return new this.graphTraversalClass(this.graph, new TraversalStrategies(this.traversalStrategies), b);
+  }
 }
 
 /**
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 0f052598d8..1e63756572 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
@@ -105,6 +105,11 @@ const gremlins = {
     g_VX1X_repeatXrepeatXunionXout_uses_out_traversesXX_whereXloops_isX0X_timesX1X_timeX2X_name: [function({g, vid1}) { return g.V(vid1).repeat(__.repeat(__.union(__.out("uses"),__.out("traverses")).where(__.loops().is(0))).times(1)).times(2).values("name") }], 
     g_V_repeatXa_outXknows_repeatXb_outXcreatedX_filterXloops_isX0XX_emit_lang: [function({g}) { return g.V().repeat("a",__.out("knows").repeat("b",__.out("created").filter(__.loops("a").is(0))).emit()).emit().values("lang") }], 
     g_VX6X_repeatXa_bothXcreatedX_simplePathX_emitXrepeatXb_bothXknowsXX_untilXloopsXbX_asXb_whereXloopsXaX_asXbX_hasXname_vadasXX_dedup_name: [function({g, vid6}) { return g.V(vid6).repeat("a",__.both("created").simplePath()).emit(__.repeat("b",__.both("knows")).until(__.loops("b").as("b").where(__.loops("a").as("b"))).has("name","vadas")).dedup().values("name") }], 
+    g_unionXX: [function({g}) { return g.union() }], 
+    g_unionXconstantX1X_constantX2X_constantX3XX: [function({g, xx1, xx3, xx2}) { return g.union(__.constant(xx1),__.constant(xx2),__.constant(xx3)) }], 
+    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") }], 
+    g_unionXV_hasLabelXsoftwareX_V_hasLabelXpersonXX_name: [function({g}) { return g.union(__.V().hasLabel("software"),__.V().hasLabel("person")).values("name") }], 
     g_V_unionXout__inX_name: [function({g}) { return g.V().union(__.out(),__.in_()).values("name") }], 
     g_VX1X_unionXrepeatXoutX_timesX2X__outX_name: [function({g, vid1}) { return g.V(vid1).union(__.repeat(__.out()).times(2),__.out()).values("name") }], 
     g_V_chooseXlabel_is_person__unionX__out_lang__out_nameX__in_labelX: [function({g}) { return g.V().choose(__.label().is("person"),__.union(__.out().values("lang"),__.out().values("name")),__.in_().label()) }], 
diff --git a/gremlin-language/src/main/antlr4/Gremlin.g4 b/gremlin-language/src/main/antlr4/Gremlin.g4
index bc41b628dd..6f2f9414dc 100644
--- a/gremlin-language/src/main/antlr4/Gremlin.g4
+++ b/gremlin-language/src/main/antlr4/Gremlin.g4
@@ -102,6 +102,7 @@ traversalSourceSpawnMethod
 	| traversalSourceSpawnMethod_inject
     | traversalSourceSpawnMethod_io
     | traversalSourceSpawnMethod_call
+    | traversalSourceSpawnMethod_union
     ;
 
 traversalSourceSpawnMethod_addE
@@ -149,6 +150,10 @@ traversalSourceSpawnMethod_call
     | 'call' LPAREN stringBasedLiteral COMMA genericLiteralMap COMMA nestedTraversal RPAREN #traversalSourceSpawnMethod_call_string_map_traversal
     ;
 
+traversalSourceSpawnMethod_union
+    : 'union' LPAREN nestedTraversalList RPAREN
+    ;
+
 chainedTraversal
     : traversalMethod
     | chainedTraversal DOT traversalMethod
diff --git a/gremlin-python/src/main/python/gremlin_python/process/graph_traversal.py b/gremlin-python/src/main/python/gremlin_python/process/graph_traversal.py
index 3decfb2f73..ed4e0ca4c2 100644
--- a/gremlin-python/src/main/python/gremlin_python/process/graph_traversal.py
+++ b/gremlin-python/src/main/python/gremlin_python/process/graph_traversal.py
@@ -243,6 +243,11 @@ class GraphTraversalSource(object):
         traversal.bytecode.add_step("call", *args)
         return traversal
 
+    def union(self, *args):
+        traversal = self.get_graph_traversal()
+        traversal.bytecode.add_step("union", *args)
+        return traversal
+
 
 class GraphTraversal(Traversal):
     def __init__(self, graph, traversal_strategies, bytecode):
diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/branch/Union.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/branch/Union.feature
index 0f60240371..5d08599568 100644
--- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/branch/Union.feature
+++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/branch/Union.feature
@@ -18,6 +18,77 @@
 @StepClassBranch @StepUnion
 Feature: Step - union()
 
+  Scenario: g_unionXX
+    Given the modern graph
+    And the traversal of
+       """
+       g.union()
+       """
+    When iterated to list
+    Then the result should be empty
+
+  Scenario: g_unionXconstantX1X_constantX2X_constantX3XX
+    Given the modern graph
+    And using the parameter xx1 defined as "d[1].i"
+    And using the parameter xx2 defined as "d[2].i"
+    And using the parameter xx3 defined as "d[3].i"
+    And the traversal of
+       """
+       g.union(constant(xx1), constant(xx2), constant(xx3))
+       """
+    When iterated to list
+    Then the result should be unordered
+       | result |
+       | d[1].i |
+       | d[2].i |
+       | d[3].i |
+
+  Scenario: g_unionXV_name
+    Given the modern graph
+    And the traversal of
+       """
+       g.union(__.V().values("name"))
+       """
+    When iterated to list
+    Then the result should be unordered
+      | result |
+      | marko  |
+      | vadas  |
+      | lop    |
+      | josh   |
+      | ripple |
+      | peter  |
+
+  Scenario: g_unionXVXv1X_VX4XX_name
+    Given the modern graph
+    And using the parameter v1 defined as "v[vadas]"
+    And using the parameter v4 defined as "v[josh]"
+    And the traversal of
+       """
+       g.union(__.V(v1), __.V(v4)).values("name")
+       """
+    When iterated to list
+    Then the result should be unordered
+      | result |
+      | vadas  |
+      | josh   |
+
+  Scenario: g_unionXV_hasLabelXsoftwareX_V_hasLabelXpersonXX_name
+    Given the modern graph
+    And the traversal of
+       """
+       g.union(__.V().hasLabel("software"), __.V().hasLabel("person")).values("name")
+       """
+    When iterated to list
+    Then the result should be unordered
+      | result |
+      | marko  |
+      | vadas  |
+      | lop    |
+      | josh   |
+      | ripple |
+      | peter  |
+
   Scenario: g_V_unionXout__inX_name
     Given the modern graph
     And the traversal of