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:40:56 UTC

[tinkerpop] branch TINKERPOP-2873 updated (143d187b5a -> 649a07e2c8)

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

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


 discard 143d187b5a TINKERPOP-2873 Added union() as a start step
     new 649a07e2c8 TINKERPOP-2873 Added union() as a start step

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (143d187b5a)
            \
             N -- N -- N   refs/heads/TINKERPOP-2873 (649a07e2c8)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 docs/src/upgrade/release-3.7.x.asciidoc | 10 +++++++---
 1 file changed, 7 insertions(+), 3 deletions(-)


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

Posted by sp...@apache.org.
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 649a07e2c803c69bb58c90c400e3019acdfd03e5
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            | 48 ++++++++++++---
 .../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, 215 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..7811b0f3b0 100644
--- a/docs/src/upgrade/release-3.7.x.asciidoc
+++ b/docs/src/upgrade/release-3.7.x.asciidoc
@@ -29,6 +29,37 @@ 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]
+----
+gremlin> g.union(V().has('name','vadas'),
+......1>         V().has('software','name','lop').in('created')).
+......2>   values('name')
+==>vadas
+==>marko
+==>josh
+==>peter
+----
+
+See: link:https://issues.apache.org/jira/browse/TINKERPOP-2873[TINKERPOP-2873]
+
 ==== Properties on Elements
 
 ===== Introduction
@@ -43,7 +74,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 +83,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 +127,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