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 2022/02/07 12:30:11 UTC

[tinkerpop] branch TINKERPOP-2681 updated (ce3924c -> 8939d5e)

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

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


    from ce3924c  Added from/to aliases for Direction OUT/IN
     new 2337b5a  Prevent creation of vertices if they do not exist for mergeE()
     new 8939d5e  TINKERPOP-2681 Finalize mergeV semantics around null/empty args

The 2 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/dev/provider/gremlin-semantics.asciidoc   |  66 ++++++++++++
 .../language/grammar/TraversalMethodVisitor.java   |  10 ++
 .../grammar/TraversalSourceSpawnMethodVisitor.java |   6 ++
 .../process/traversal/step/map/MergeEdgeStep.java  |  28 +++--
 .../traversal/step/map/MergeVertexStep.java        |  90 ++++++++++------
 .../Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs |   5 +-
 .../gremlin-javascript/test/cucumber/gremlin.js    |   5 +-
 gremlin-language/src/main/antlr4/Gremlin.g4        |  11 +-
 gremlin-python/src/main/python/radish/gremlin.py   |   5 +-
 gremlin-test/features/map/MergeEdge.feature        |  40 +++++--
 gremlin-test/features/map/MergeVertex.feature      | 116 +++++++++++++++++++++
 .../process/traversal/step/map/MergeEdgeTest.java  |  17 +--
 .../traversal/step/map/TinkerMergeVertexStep.java  |   4 +-
 13 files changed, 334 insertions(+), 69 deletions(-)

[tinkerpop] 01/02: Prevent creation of vertices if they do not exist for mergeE()

Posted by sp...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 2337b5a96b18600b3b684da09c2fe22983d31975
Author: Stephen Mallette <st...@amazon.com>
AuthorDate: Sat Feb 5 09:52:48 2022 -0500

    Prevent creation of vertices if they do not exist for mergeE()
---
 .../process/traversal/step/map/MergeEdgeStep.java  | 28 +++++++++++----
 .../Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs |  5 +--
 .../gremlin-javascript/test/cucumber/gremlin.js    |  5 +--
 gremlin-python/src/main/python/radish/gremlin.py   |  5 +--
 gremlin-test/features/map/MergeEdge.feature        | 40 ++++++++++++++++------
 .../process/traversal/step/map/MergeEdgeTest.java  | 17 +++++----
 6 files changed, 71 insertions(+), 29 deletions(-)

diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeEdgeStep.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeEdgeStep.java
index 94d4273..7734f95 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeEdgeStep.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeEdgeStep.java
@@ -279,6 +279,13 @@ public class MergeEdgeStep<S> extends FlatMapStep<S, Edge> implements Mutating<E
             // searchCreate should have alredy been validated so only do it if it is overridden
             if (useOnCreate) validateMapInput(m, false);
 
+            // check if from/to were already determined by traverser/searchMatch, and if not, then at least ensure that
+            // the from/to is set in m
+            if (outV == PLACEHOLDER_VERTEX && !m.containsKey(Direction.OUT))
+                throw new IllegalArgumentException("Out Vertex not specified - edge cannot be created");
+            if (inV == PLACEHOLDER_VERTEX && !m.containsKey(Direction.IN))
+                throw new IllegalArgumentException("In Vertex not specified - edge cannot be created");
+
             final List<Object> keyValues = new ArrayList<>();
             String label = Edge.DEFAULT_LABEL;
 
@@ -292,11 +299,9 @@ public class MergeEdgeStep<S> extends FlatMapStep<S, Edge> implements Mutating<E
                     // only override if onCreate was specified otherwise stick to however traverser/searchMatch
                     // was resolved
                     if (useOnCreate && entry.getKey().equals(Direction.IN)) {
-                        toV = ((Attachable<Vertex>) entry.getValue())
-                                .attach(Attachable.Method.getOrCreate(this.getTraversal().getGraph().orElse(EmptyGraph.instance())));
+                        toV = tryAttachVertex((Attachable<Vertex>) entry.getValue());
                     } else if (useOnCreate && entry.getKey().equals(Direction.OUT)) {
-                        fromV = ((Attachable<Vertex>) entry.getValue())
-                                .attach(Attachable.Method.getOrCreate(this.getTraversal().getGraph().orElse(EmptyGraph.instance())));
+                        fromV = tryAttachVertex((Attachable<Vertex>) entry.getValue());
                     }
                 } else if (entry.getKey().equals(T.label)) {
                     label = (String) entry.getValue();
@@ -335,14 +340,25 @@ public class MergeEdgeStep<S> extends FlatMapStep<S, Edge> implements Mutating<E
         final Object o = searchCreate.getOrDefault(direction, possibleVertex);
         final Vertex v = o instanceof Vertex ? (Vertex) o : new ReferenceVertex(o);
         if (v != PLACEHOLDER_VERTEX && v instanceof Attachable) {
-            return ((Attachable<Vertex>) v)
-                    .attach(Attachable.Method.getOrCreate(this.getTraversal().getGraph().orElse(EmptyGraph.instance())));
+            return tryAttachVertex((Attachable<Vertex>) v);
         } else {
             return v;
         }
     }
 
     /**
+     * Tries to attach a {@link Vertex} to its host {@link Graph} of the traversal. If the {@link Vertex} cannot be
+     * found then an {@code IllegalArgumentException} is expected.
+     */
+    protected Vertex tryAttachVertex(final Attachable<Vertex> attachable) {
+        try {
+            return attachable.attach(Attachable.Method.get(this.getTraversal().getGraph().orElse(EmptyGraph.instance())));
+        } catch (IllegalStateException ise) {
+            throw new IllegalArgumentException(String.format("%s could not be found and edge could not be created", attachable));
+        }
+    }
+
+    /**
      * Validates input to any {@code Map} arguments to this step. For {@link Merge#onMatch} updates cannot be applied
      * to immutable parts of an {@link Edge} (id, label, incident vertices) so those can be ignored in the validation.
      */
diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs
index d600ff9..95c4069 100644
--- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs
@@ -505,8 +505,8 @@ namespace Gremlin.Net.IntegrationTest.Gherkin
                {"g_withSideEffectXa_label_knows_out_marko_in_vadasX_mergeEXselectXaXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV("person").Property(T.Id,100).Property("name","marko").AddV("person").Property(T.Id,101).Property("name","vadas"), (g,p) =>g.WithSideEffect("a",p["xx1"]).MergeE((IDictionary<object,object>) __.Select<object>("a")), (g,p) =>g.V().Has("person","name","marko").Out("knows").Has("person","name","vadas")}}, 
                {"g_mergeEXlabel_knows_out_marko1_in_vadas1X", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV("person").Property(T.Id,100).Property("name","marko").AddV("person").Property(T.Id,101).Property("name","vadas"), (g,p) =>g.MergeE((IDictionary<object,object>) p["xx1"]), (g,p) =>g.V().Has("person","name","marko").Out("knows").Has("person","name","vadas")}}, 
                {"g_mergeEXlabel_knows_out_marko_in_vadas_weight_05X_exists", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV("person").Property(T.Id,100).Property("name","marko").As("a").AddV("person").Property(T.Id,101).Property("name","vadas").As("b").AddE("knows").From("a").To("b"), (g,p) =>g.MergeE((IDictionary<object,object>) p["xx1"]), (g,p) =>g.V().Has("person","name","marko").OutE("knows").Has("weight",0.5).InV().Has("person","name"," [...]
-               {"g_mergeEXlabel_knows_out_marko_in_vadas_weight_05X", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.MergeE((IDictionary<object,object>) p["xx1"]), (g,p) =>g.V(), (g,p) =>g.E().HasLabel("knows").Has("weight",0.5)}}, 
-               {"g_mergeEXlabel_knows_out_marko_in_vadasX_optionXonCreate_created_YX_optionXonMatch_created_NX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.MergeE((IDictionary<object,object>) p["xx1"]).Option(Merge.OnCreate, (IDictionary<object,object>) p["xx2"]).Option(Merge.OnMatch, (IDictionary<object,object>) p["xx3"]), (g,p) =>g.V(), (g,p) =>g.E().HasLabel("knows").Has("created","Y"), (g,p) =>g.E().HasLabel("knows").Has("created","N")}}, 
+               {"g_mergeEXlabel_knows_out_marko_in_vadas_weight_05X", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.MergeE((IDictionary<object,object>) p["xx1"])}}, 
+               {"g_mergeEXlabel_knows_out_marko_in_vadasX_optionXonCreate_created_YX_optionXonMatch_created_NX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.MergeE((IDictionary<object,object>) p["xx1"]).Option(Merge.OnCreate, (IDictionary<object,object>) p["xx2"]).Option(Merge.OnMatch, (IDictionary<object,object>) p["xx3"])}}, 
                {"g_mergeEXlabel_knows_out_marko_in_vadasX_optionXonCreate_created_YX_optionXonMatch_created_NX_exists", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV("person").Property(T.Id,100).Property("name","marko").As("a").AddV("person").Property(T.Id,101).Property("name","vadas").As("b").AddE("knows").From("a").To("b"), (g,p) =>g.MergeE((IDictionary<object,object>) p["xx1"]).Option(Merge.OnCreate, (IDictionary<object,object>) p["xx2"] [...]
                {"g_mergeEXlabel_knows_out_marko_in_vadasX_optionXonCreate_created_YX_optionXonMatch_created_NX_exists_updated", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV("person").Property(T.Id,100).Property("name","marko").As("a").AddV("person").Property(T.Id,101).Property("name","vadas").As("b").AddE("knows").From("a").To("b").Property("created","Y"), (g,p) =>g.MergeE((IDictionary<object,object>) p["xx1"]).Option(Merge.OnCreate, (IDic [...]
                {"g_V_hasXperson_name_marko_X_mergeEXlabel_knowsX_optionXonCreate_created_YX_optionXonMatch_created_NX_exists_updated", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV("person").Property(T.Id,100).Property("name","marko").As("a").AddV("person").Property(T.Id,101).Property("name","vadas").As("b").AddE("knows").From("a").To("b").Property("created","Y").AddE("knows").From("a").To("b"), (g,p) =>g.V().Has("person","name","marko").Me [...]
@@ -519,6 +519,7 @@ namespace Gremlin.Net.IntegrationTest.Gherkin
                {"g_withSideEffectXc_created_YX_withSideEffectXm_matchedX_mergeEXlabel_knows_out_marko_in_vadasX_optionXonCreate_selectXcXX_optionXonMatch_selectXmXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV("person").Property(T.Id,100).Property("name","marko").As("a").AddV("person").Property(T.Id,101).Property("name","vadas").As("b"), (g,p) =>g.WithSideEffect("c",p["xx2"]).WithSideEffect("m",p["xx3"]).MergeE((IDictionary<object,object [...]
                {"g_V_mapXmergeEXlabel_self_weight_05XX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV("person").Property("name","marko").Property("age",29), (g,p) =>g.V().Map<object>(__.MergeE((IDictionary<object,object>) p["xx1"])), (g,p) =>g.E(), (g,p) =>g.V(), (g,p) =>g.V().Has("person","name","marko").As("a").OutE("self").Has("weight",0.5).InV().Where(P.Eq("a"))}}, 
                {"g_mergeEXlabel_knows_out_marko_in_vadasX_aliased_direction", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV("person").Property(T.Id,100).Property("name","marko").AddV("person").Property(T.Id,101).Property("name","vadas"), (g,p) =>g.MergeE((IDictionary<object,object>) p["xx1"]), (g,p) =>g.V().Has("person","name","marko").Out("knows").Has("person","name","vadas")}}, 
+               {"g_injectXlabel_knows_out_marko_in_vadas_label_self_out_vadas_in_vadasX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV("person").Property(T.Id,100).Property("name","marko").AddV("person").Property(T.Id,101).Property("name","vadas"), (g,p) =>g.Inject(p["xx1"],p["xx2"]).MergeE(), (g,p) =>g.V(), (g,p) =>g.E(), (g,p) =>g.V().Has("person","name","marko").Out("knows").Has("person","name","vadas"), (g,p) =>g.V().Has("person","name [...]
                {"g_mergeVXlabel_person_name_stephenX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV("person").Property("name","marko").Property("age",29), (g,p) =>g.MergeV((IDictionary<object,object>) p["xx1"]), (g,p) =>g.V().Has("person","name","stephen")}}, 
                {"g_mergeVXlabel_person_name_markoX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV("person").Property("name","marko").Property("age",29), (g,p) =>g.MergeV((IDictionary<object,object>) p["xx1"]), (g,p) =>g.V().Has("person","name","marko")}}, 
                {"g_mergeVXlabel_person_name_stephenX_optionXonCreate_label_person_name_stephen_age_19X_option", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV("person").Property("name","marko").Property("age",29), (g,p) =>g.MergeV((IDictionary<object,object>) p["xx1"]).Option(Merge.OnCreate, (IDictionary<object,object>) p["xx2"]), (g,p) =>g.V().Has("person","name","stephen").Has("age",19)}}, 
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 cfc914c..58b4316 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
@@ -496,8 +496,8 @@ const gremlins = {
     g_withSideEffectXa_label_knows_out_marko_in_vadasX_mergeEXselectXaXX: [function({g, xx1}) { return g.addV("person").property(T.id,100).property("name","marko").addV("person").property(T.id,101).property("name","vadas") }, function({g, xx1}) { return g.withSideEffect("a",xx1).mergeE(__.select("a")) }, function({g, xx1}) { return g.V().has("person","name","marko").out("knows").has("person","name","vadas") }], 
     g_mergeEXlabel_knows_out_marko1_in_vadas1X: [function({g, xx1}) { return g.addV("person").property(T.id,100).property("name","marko").addV("person").property(T.id,101).property("name","vadas") }, function({g, xx1}) { return g.mergeE(xx1) }, function({g, xx1}) { return g.V().has("person","name","marko").out("knows").has("person","name","vadas") }], 
     g_mergeEXlabel_knows_out_marko_in_vadas_weight_05X_exists: [function({g, xx1}) { return g.addV("person").property(T.id,100).property("name","marko").as("a").addV("person").property(T.id,101).property("name","vadas").as("b").addE("knows").from_("a").to("b") }, function({g, xx1}) { return g.mergeE(xx1) }, function({g, xx1}) { return g.V().has("person","name","marko").outE("knows").has("weight",0.5).inV().has("person","name","vadas") }, function({g, xx1}) { return g.V().has("person","na [...]
-    g_mergeEXlabel_knows_out_marko_in_vadas_weight_05X: [function({g, xx1}) { return g.mergeE(xx1) }, function({g, xx1}) { return g.V() }, function({g, xx1}) { return g.E().hasLabel("knows").has("weight",0.5) }], 
-    g_mergeEXlabel_knows_out_marko_in_vadasX_optionXonCreate_created_YX_optionXonMatch_created_NX: [function({g, xx1, xx3, xx2}) { return g.mergeE(xx1).option(Merge.onCreate,xx2).option(Merge.onMatch,xx3) }, function({g, xx1, xx3, xx2}) { return g.V() }, function({g, xx1, xx3, xx2}) { return g.E().hasLabel("knows").has("created","Y") }, function({g, xx1, xx3, xx2}) { return g.E().hasLabel("knows").has("created","N") }], 
+    g_mergeEXlabel_knows_out_marko_in_vadas_weight_05X: [function({g, xx1}) { return g.mergeE(xx1) }], 
+    g_mergeEXlabel_knows_out_marko_in_vadasX_optionXonCreate_created_YX_optionXonMatch_created_NX: [function({g, xx1, xx3, xx2}) { return g.mergeE(xx1).option(Merge.onCreate,xx2).option(Merge.onMatch,xx3) }], 
     g_mergeEXlabel_knows_out_marko_in_vadasX_optionXonCreate_created_YX_optionXonMatch_created_NX_exists: [function({g, xx1, xx3, xx2}) { return g.addV("person").property(T.id,100).property("name","marko").as("a").addV("person").property(T.id,101).property("name","vadas").as("b").addE("knows").from_("a").to("b") }, function({g, xx1, xx3, xx2}) { return g.mergeE(xx1).option(Merge.onCreate,xx2).option(Merge.onMatch,xx3) }, function({g, xx1, xx3, xx2}) { return g.V() }, function({g, xx1, xx [...]
     g_mergeEXlabel_knows_out_marko_in_vadasX_optionXonCreate_created_YX_optionXonMatch_created_NX_exists_updated: [function({g, xx1, xx3, xx2}) { return g.addV("person").property(T.id,100).property("name","marko").as("a").addV("person").property(T.id,101).property("name","vadas").as("b").addE("knows").from_("a").to("b").property("created","Y") }, function({g, xx1, xx3, xx2}) { return g.mergeE(xx1).option(Merge.onCreate,xx2).option(Merge.onMatch,xx3) }, function({g, xx1, xx3, xx2}) { retu [...]
     g_V_hasXperson_name_marko_X_mergeEXlabel_knowsX_optionXonCreate_created_YX_optionXonMatch_created_NX_exists_updated: [function({g, xx1, xx3, xx2}) { return g.addV("person").property(T.id,100).property("name","marko").as("a").addV("person").property(T.id,101).property("name","vadas").as("b").addE("knows").from_("a").to("b").property("created","Y").addE("knows").from_("a").to("b") }, function({g, xx1, xx3, xx2}) { return g.V().has("person","name","marko").mergeE(xx1).option(Merge.onCre [...]
@@ -510,6 +510,7 @@ const gremlins = {
     g_withSideEffectXc_created_YX_withSideEffectXm_matchedX_mergeEXlabel_knows_out_marko_in_vadasX_optionXonCreate_selectXcXX_optionXonMatch_selectXmXX: [function({g, xx1, xx3, xx2}) { return g.addV("person").property(T.id,100).property("name","marko").as("a").addV("person").property(T.id,101).property("name","vadas").as("b") }, function({g, xx1, xx3, xx2}) { return g.withSideEffect("c",xx2).withSideEffect("m",xx3).mergeE(xx1).option(Merge.onCreate,__.select("c")).option(Merge.onMatch,__ [...]
     g_V_mapXmergeEXlabel_self_weight_05XX: [function({g, xx1}) { return g.addV("person").property("name","marko").property("age",29) }, function({g, xx1}) { return g.V().map(__.mergeE(xx1)) }, function({g, xx1}) { return g.E() }, function({g, xx1}) { return g.V() }, function({g, xx1}) { return g.V().has("person","name","marko").as("a").outE("self").has("weight",0.5).inV().where(P.eq("a")) }], 
     g_mergeEXlabel_knows_out_marko_in_vadasX_aliased_direction: [function({g, xx1}) { return g.addV("person").property(T.id,100).property("name","marko").addV("person").property(T.id,101).property("name","vadas") }, function({g, xx1}) { return g.mergeE(xx1) }, function({g, xx1}) { return g.V().has("person","name","marko").out("knows").has("person","name","vadas") }], 
+    g_injectXlabel_knows_out_marko_in_vadas_label_self_out_vadas_in_vadasX: [function({g, xx1, xx2}) { return g.addV("person").property(T.id,100).property("name","marko").addV("person").property(T.id,101).property("name","vadas") }, function({g, xx1, xx2}) { return g.inject(xx1,xx2).mergeE() }, function({g, xx1, xx2}) { return g.V() }, function({g, xx1, xx2}) { return g.E() }, function({g, xx1, xx2}) { return g.V().has("person","name","marko").out("knows").has("person","name","vadas") }, [...]
     g_mergeVXlabel_person_name_stephenX: [function({g, xx1}) { return g.addV("person").property("name","marko").property("age",29) }, function({g, xx1}) { return g.mergeV(xx1) }, function({g, xx1}) { return g.V().has("person","name","stephen") }], 
     g_mergeVXlabel_person_name_markoX: [function({g, xx1}) { return g.addV("person").property("name","marko").property("age",29) }, function({g, xx1}) { return g.mergeV(xx1) }, function({g, xx1}) { return g.V().has("person","name","marko") }], 
     g_mergeVXlabel_person_name_stephenX_optionXonCreate_label_person_name_stephen_age_19X_option: [function({g, xx1, xx2}) { return g.addV("person").property("name","marko").property("age",29) }, function({g, xx1, xx2}) { return g.mergeV(xx1).option(Merge.onCreate,xx2) }, function({g, xx1, xx2}) { return g.V().has("person","name","stephen").has("age",19) }], 
diff --git a/gremlin-python/src/main/python/radish/gremlin.py b/gremlin-python/src/main/python/radish/gremlin.py
index 761ad00..0199247 100644
--- a/gremlin-python/src/main/python/radish/gremlin.py
+++ b/gremlin-python/src/main/python/radish/gremlin.py
@@ -478,8 +478,8 @@ world.gremlins = {
     'g_withSideEffectXa_label_knows_out_marko_in_vadasX_mergeEXselectXaXX': [(lambda g, xx1=None:g.addV('person').property(T.id_,100).property('name','marko').addV('person').property(T.id_,101).property('name','vadas')), (lambda g, xx1=None:g.withSideEffect('a',xx1).mergeE(__.select('a'))), (lambda g, xx1=None:g.V().has('person','name','marko').out('knows').has('person','name','vadas'))], 
     'g_mergeEXlabel_knows_out_marko1_in_vadas1X': [(lambda g, xx1=None:g.addV('person').property(T.id_,100).property('name','marko').addV('person').property(T.id_,101).property('name','vadas')), (lambda g, xx1=None:g.mergeE(xx1)), (lambda g, xx1=None:g.V().has('person','name','marko').out('knows').has('person','name','vadas'))], 
     'g_mergeEXlabel_knows_out_marko_in_vadas_weight_05X_exists': [(lambda g, xx1=None:g.addV('person').property(T.id_,100).property('name','marko').as_('a').addV('person').property(T.id_,101).property('name','vadas').as_('b').addE('knows').from_('a').to('b')), (lambda g, xx1=None:g.mergeE(xx1)), (lambda g, xx1=None:g.V().has('person','name','marko').outE('knows').has('weight',float(0.5)).inV().has('person','name','vadas')), (lambda g, xx1=None:g.V().has('person','name','marko').out('know [...]
-    'g_mergeEXlabel_knows_out_marko_in_vadas_weight_05X': [(lambda g, xx1=None:g.mergeE(xx1)), (lambda g, xx1=None:g.V()), (lambda g, xx1=None:g.E().hasLabel('knows').has('weight',float(0.5)))], 
-    'g_mergeEXlabel_knows_out_marko_in_vadasX_optionXonCreate_created_YX_optionXonMatch_created_NX': [(lambda g, xx1=None,xx3=None,xx2=None:g.mergeE(xx1).option(Merge.onCreate,xx2).option(Merge.onMatch,xx3)), (lambda g, xx1=None,xx3=None,xx2=None:g.V()), (lambda g, xx1=None,xx3=None,xx2=None:g.E().hasLabel('knows').has('created','Y')), (lambda g, xx1=None,xx3=None,xx2=None:g.E().hasLabel('knows').has('created','N'))], 
+    'g_mergeEXlabel_knows_out_marko_in_vadas_weight_05X': [(lambda g, xx1=None:g.mergeE(xx1))], 
+    'g_mergeEXlabel_knows_out_marko_in_vadasX_optionXonCreate_created_YX_optionXonMatch_created_NX': [(lambda g, xx1=None,xx3=None,xx2=None:g.mergeE(xx1).option(Merge.onCreate,xx2).option(Merge.onMatch,xx3))], 
     'g_mergeEXlabel_knows_out_marko_in_vadasX_optionXonCreate_created_YX_optionXonMatch_created_NX_exists': [(lambda g, xx1=None,xx3=None,xx2=None:g.addV('person').property(T.id_,100).property('name','marko').as_('a').addV('person').property(T.id_,101).property('name','vadas').as_('b').addE('knows').from_('a').to('b')), (lambda g, xx1=None,xx3=None,xx2=None:g.mergeE(xx1).option(Merge.onCreate,xx2).option(Merge.onMatch,xx3)), (lambda g, xx1=None,xx3=None,xx2=None:g.V()), (lambda g, xx1=No [...]
     'g_mergeEXlabel_knows_out_marko_in_vadasX_optionXonCreate_created_YX_optionXonMatch_created_NX_exists_updated': [(lambda g, xx1=None,xx3=None,xx2=None:g.addV('person').property(T.id_,100).property('name','marko').as_('a').addV('person').property(T.id_,101).property('name','vadas').as_('b').addE('knows').from_('a').to('b').property('created','Y')), (lambda g, xx1=None,xx3=None,xx2=None:g.mergeE(xx1).option(Merge.onCreate,xx2).option(Merge.onMatch,xx3)), (lambda g, xx1=None,xx3=None,xx [...]
     'g_V_hasXperson_name_marko_X_mergeEXlabel_knowsX_optionXonCreate_created_YX_optionXonMatch_created_NX_exists_updated': [(lambda g, xx1=None,xx3=None,xx2=None:g.addV('person').property(T.id_,100).property('name','marko').as_('a').addV('person').property(T.id_,101).property('name','vadas').as_('b').addE('knows').from_('a').to('b').property('created','Y').addE('knows').from_('a').to('b')), (lambda g, xx1=None,xx3=None,xx2=None:g.V().has('person','name','marko').mergeE(xx1).option(Merge. [...]
@@ -492,6 +492,7 @@ world.gremlins = {
     'g_withSideEffectXc_created_YX_withSideEffectXm_matchedX_mergeEXlabel_knows_out_marko_in_vadasX_optionXonCreate_selectXcXX_optionXonMatch_selectXmXX': [(lambda g, xx1=None,xx3=None,xx2=None:g.addV('person').property(T.id_,100).property('name','marko').as_('a').addV('person').property(T.id_,101).property('name','vadas').as_('b')), (lambda g, xx1=None,xx3=None,xx2=None:g.withSideEffect('c',xx2).withSideEffect('m',xx3).mergeE(xx1).option(Merge.onCreate,__.select('c')).option(Merge.onMat [...]
     'g_V_mapXmergeEXlabel_self_weight_05XX': [(lambda g, xx1=None:g.addV('person').property('name','marko').property('age',29)), (lambda g, xx1=None:g.V().map(__.mergeE(xx1))), (lambda g, xx1=None:g.E()), (lambda g, xx1=None:g.V()), (lambda g, xx1=None:g.V().has('person','name','marko').as_('a').outE('self').has('weight',float(0.5)).inV().where(P.eq('a')))], 
     'g_mergeEXlabel_knows_out_marko_in_vadasX_aliased_direction': [(lambda g, xx1=None:g.addV('person').property(T.id_,100).property('name','marko').addV('person').property(T.id_,101).property('name','vadas')), (lambda g, xx1=None:g.mergeE(xx1)), (lambda g, xx1=None:g.V().has('person','name','marko').out('knows').has('person','name','vadas'))], 
+    'g_injectXlabel_knows_out_marko_in_vadas_label_self_out_vadas_in_vadasX': [(lambda g, xx1=None,xx2=None:g.addV('person').property(T.id_,100).property('name','marko').addV('person').property(T.id_,101).property('name','vadas')), (lambda g, xx1=None,xx2=None:g.inject(xx1,xx2).mergeE()), (lambda g, xx1=None,xx2=None:g.V()), (lambda g, xx1=None,xx2=None:g.E()), (lambda g, xx1=None,xx2=None:g.V().has('person','name','marko').out('knows').has('person','name','vadas')), (lambda g, xx1=None, [...]
     'g_mergeVXlabel_person_name_stephenX': [(lambda g, xx1=None:g.addV('person').property('name','marko').property('age',29)), (lambda g, xx1=None:g.mergeV(xx1)), (lambda g, xx1=None:g.V().has('person','name','stephen'))], 
     'g_mergeVXlabel_person_name_markoX': [(lambda g, xx1=None:g.addV('person').property('name','marko').property('age',29)), (lambda g, xx1=None:g.mergeV(xx1)), (lambda g, xx1=None:g.V().has('person','name','marko'))], 
     'g_mergeVXlabel_person_name_stephenX_optionXonCreate_label_person_name_stephen_age_19X_option': [(lambda g, xx1=None,xx2=None:g.addV('person').property('name','marko').property('age',29)), (lambda g, xx1=None,xx2=None:g.mergeV(xx1).option(Merge.onCreate,xx2)), (lambda g, xx1=None,xx2=None:g.V().has('person','name','stephen').has('age',19))], 
diff --git a/gremlin-test/features/map/MergeEdge.feature b/gremlin-test/features/map/MergeEdge.feature
index 4d9304a..b696e05 100644
--- a/gremlin-test/features/map/MergeEdge.feature
+++ b/gremlin-test/features/map/MergeEdge.feature
@@ -44,11 +44,11 @@ Feature: Step - mergeE()
   # g_mergeEXlabel_knows_out_marko_in_vadas_weight_05X
   #   - mergeE(Map) specifying out/in for vertices in the match/create with no option()
   #   - no vertices/edges in graph
-  #   - results in two new vertices and one new edge
+  #   - results in error as vertices don't exist
   # g_mergeEXlabel_knows_out_marko_in_vadasX_optionXonCreate_created_YX_optionXonMatch_created_NX
   #   - mergeE(Map) specifying out/in for vertices in the match/create with option(Map)
   #   - no vertices/edges in graph
-  #   - results in two new vertices and one new edge
+  #   - results in error as vertices don't exist
   # g_mergeEXlabel_knows_out_marko_in_vadasX_optionXonCreate_created_YX_optionXonMatch_created_NX_exists
   #   - mergeE(Map) specifying out/in for vertices in the match/create with option(Map)
   #   - vertices and edge already exist
@@ -89,6 +89,10 @@ Feature: Step - mergeE()
   #   - mergeE(Map) using the vertex of current traverser as reference - testing child traversal
   #   - vertex already exists
   #   - results in a self edge
+  # g_injectXlabel_knows_out_marko_in_vadas_label_self_out_vadas_in_vadasX
+  #   - mergeE() using the map of current traverser as reference
+  #   - vertices already exists
+  #   - results in two new edges
 
   Scenario: g_V_mergeEXlabel_self_weight_05X
     Given the empty graph
@@ -186,9 +190,7 @@ Feature: Step - mergeE()
       g.mergeE(xx1)
       """
     When iterated to list
-    Then the result should have a count of 1
-    And the graph should return 2 for count of "g.V()"
-    And the graph should return 1 for count of "g.E().hasLabel(\"knows\").has(\"weight\",0.5)"
+    Then the traversal will raise an error
 
   @UserSuppliedVertexIds
   Scenario: g_mergeEXlabel_knows_out_marko_in_vadasX_optionXonCreate_created_YX_optionXonMatch_created_NX
@@ -201,10 +203,7 @@ Feature: Step - mergeE()
       g.mergeE(xx1).option(Merge.onCreate,xx2).option(Merge.onMatch,xx3)
       """
     When iterated to list
-    Then the result should have a count of 1
-    And the graph should return 2 for count of "g.V()"
-    And the graph should return 1 for count of "g.E().hasLabel(\"knows\").has(\"created\",\"Y\")"
-    And the graph should return 0 for count of "g.E().hasLabel(\"knows\").has(\"created\",\"N\")"
+    Then the traversal will raise an error
 
   @UserSuppliedVertexIds
   Scenario: g_mergeEXlabel_knows_out_marko_in_vadasX_optionXonCreate_created_YX_optionXonMatch_created_NX_exists
@@ -460,4 +459,25 @@ Feature: Step - mergeE()
       """
     When iterated to list
     Then the result should have a count of 1
-    And the graph should return 1 for count of "g.V().has(\"person\",\"name\",\"marko\").out(\"knows\").has(\"person\",\"name\",\"vadas\")"
\ No newline at end of file
+    And the graph should return 1 for count of "g.V().has(\"person\",\"name\",\"marko\").out(\"knows\").has(\"person\",\"name\",\"vadas\")"
+
+  @UserSuppliedVertexIds
+  Scenario: g_injectXlabel_knows_out_marko_in_vadas_label_self_out_vadas_in_vadasX
+    Given the empty graph
+    And the graph initializer of
+      """
+      g.addV("person").property(T.id, 100).property("name", "marko").
+        addV("person").property(T.id, 101).property("name", "vadas")
+      """
+    And using the parameter xx1 defined as "m[{\"t[label]\": \"knows\", \"D[OUT]\":\"v[100]\", \"D[IN]\":\"v[101]\"}]"
+    And using the parameter xx2 defined as "m[{\"t[label]\": \"self\", \"D[OUT]\":\"v[101]\", \"D[IN]\":\"v[101]\"}]"
+    And the traversal of
+      """
+      g.inject(xx1, xx2).mergeE()
+      """
+    When iterated to list
+    Then the result should have a count of 2
+    And the graph should return 2 for count of "g.V()"
+    And the graph should return 2 for count of "g.E()"
+    And the graph should return 1 for count of "g.V().has(\"person\",\"name\",\"marko\").out(\"knows\").has(\"person\",\"name\",\"vadas\")"
+    And the graph should return 1 for count of "g.V().has(\"person\",\"name\",\"vadas\").out(\"self\").has(\"person\",\"name\",\"vadas\")"
\ No newline at end of file
diff --git a/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeEdgeTest.java b/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeEdgeTest.java
index c30201c..15dffee 100644
--- a/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeEdgeTest.java
+++ b/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeEdgeTest.java
@@ -37,8 +37,11 @@ import org.junit.runner.RunWith;
 import java.util.Map;
 
 import static org.apache.tinkerpop.gremlin.util.tools.CollectionFactory.asMap;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.StringEndsWith.endsWith;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.fail;
 
 @RunWith(GremlinProcessRunner.class)
 public abstract class MergeEdgeTest extends AbstractGremlinTest {
@@ -112,13 +115,13 @@ public abstract class MergeEdgeTest extends AbstractGremlinTest {
     public void g_mergeEXlabel_knows_out_marko_in_vadasX_optionXonCreate_created_YX_optionXonMatch_created_NX() {
         final Traversal<Edge, Edge> traversal = get_g_mergeEXlabel_knows_out_marko_in_vadasX_optionXonCreate_created_YX_optionXonMatch_created_NX();
         printTraversalForm(traversal);
-        final Edge edge = traversal.next();
-        assertEquals("knows", edge.label());
-        assertEquals(100, edge.outVertex().id());
-        assertEquals(101, edge.inVertex().id());
-        assertEquals("Y", edge.<String>value("created"));
-        assertFalse(traversal.hasNext());
-        assertEquals(1, IteratorUtils.count(g.E()));
+        try {
+            traversal.next();
+            fail("Should have failed as vertices are not created");
+        } catch (Exception ex) {
+            assertThat(ex.getMessage(), endsWith("could not be found and edge could not be created"));
+        }
+        assertEquals(0, IteratorUtils.count(g.E()));
     }
 
     @Test

[tinkerpop] 02/02: TINKERPOP-2681 Finalize mergeV semantics around null/empty args

Posted by sp...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 8939d5ea4a1ad7dcf27c5b9c807ae17fa93078ad
Author: Stephen Mallette <st...@amazon.com>
AuthorDate: Mon Feb 7 07:27:45 2022 -0500

    TINKERPOP-2681 Finalize mergeV semantics around null/empty args
---
 docs/src/dev/provider/gremlin-semantics.asciidoc   |  66 ++++++++++++
 .../language/grammar/TraversalMethodVisitor.java   |  10 ++
 .../grammar/TraversalSourceSpawnMethodVisitor.java |   6 ++
 .../traversal/step/map/MergeVertexStep.java        |  90 ++++++++++------
 gremlin-language/src/main/antlr4/Gremlin.g4        |  11 +-
 gremlin-test/features/map/MergeVertex.feature      | 116 +++++++++++++++++++++
 .../traversal/step/map/TinkerMergeVertexStep.java  |   4 +-
 7 files changed, 263 insertions(+), 40 deletions(-)

diff --git a/docs/src/dev/provider/gremlin-semantics.asciidoc b/docs/src/dev/provider/gremlin-semantics.asciidoc
index 3bb1244..c6a0223 100644
--- a/docs/src/dev/provider/gremlin-semantics.asciidoc
+++ b/docs/src/dev/provider/gremlin-semantics.asciidoc
@@ -590,3 +590,69 @@ Same as Comparability, except orderability semantics apply for element compariso
 * If the unknown arguments are of different types or do not define a natural order, order first by Classname, then
 by `Object.toString()`.
 
+== Steps
+
+While TinkerPop has a full test suite for validating functionality of Gremlin, tests alone aren't always exhaustive or
+fully demonstrative of Gremlin step semantics. It is also hard to simply read the tests to understand exactly how a
+step is meant to behave. This section discusses the semantics for individual steps to help users and providers
+understand implementation expectations.
+
+=== mergeE()
+
+
+
+=== mergeV()
+
+*Description:* Provides upsert-like functionality for vertices.
+*Syntax:* `mergeV()` | `mergeV(Map)` | `mergeV(Traversal)`
+
+[width="100%",options="header"]
+|=========================================================
+|Start Step |Mid Step |Modulated |Domain |Range
+|Y |Y |`option()` |`Map` |`Vertex`
+
+*Arguments:*
+
+* `searchCreate` - A `Map` used to match a `Vertex` and if not found be the default set of data to create the new one.
+* `onCreate` - A `Map` that is the override to the default of `searchCreate`
+* `onMatch` - A `Map` used to update the `Vertex` that is found using the `searchCreate` criteria.
+
+The `searchCreate` and `onCreate` `Map` instances must consists of any combination of `T.id`, `T.label`, or arbitrary
+`String` keys (which are assumed to be vertex properties). The `onMatch` `Map` instance only allows for `String` keys
+as the `id` and `label` of a `Vertex` are immutable. Values for these valid keys that are `null` will be treated
+according to the semantics of the `addV()` step.
+
+The `Map` that is used as the argument for `searchCreate` may be assigned from the incoming `Traverser` for the no-arg
+`mergeV()`. If `mergeV(Map)` is used, then it will override the incoming `Traverser`. If `mergeV(Traversal)` is used,
+the `Traversal` argument must resolve to a `Map` and it would also override the incoming Traverser. The `onCreate` and
+`onMatch` arguments are assigned via modulation as described below.
+
+[width="100%",options="header"]
+|=========================================================
+|Event |Null `Map` |Empty `Map`
+|Search |Filters all vertices |Matches all vertices
+|Create |No new vertex |New vertex with defaults
+|Update |No update to matched vertex |No update to matched vertex
+|=========================================================
+
+If `T.id` is used for `searchCreate` or `onCreate`, it may be ignored for vertex creation if the `Graph` does not
+support user supplied identifiers.
+
+*Modulation:*
+
+* `option(Merge, Map)` - Sets the `onCreate` or `onMatch` arguments directly.
+* `option(Merge, Traversal)` - Sets the `onCreate` or `onMatch` arguments dynamically where the `Traversal` must
+resolve to a `Map`.
+
+*Exceptions*
+
+* `Map` arguments are validated for their keys resulting in exception if they do not meet requirements defined above.
+* Use of `T.label` should always have a value that is a `String`.
+
+*Considerations:*
+
+* `mergeV()` Can only be used mid-traversal. It is not a start step.
+* As is common to Gremlin, it is expected that `Traversal` arguments may utilize `sideEffect()` steps.
+
+See: link:https://github.com/apache/tinkerpop/tree/master/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeVertexStep.java[source],
+link:https://tinkerpop.apache.org/docs/current/reference/#mergev-step[reference]
\ No newline at end of file
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalMethodVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalMethodVisitor.java
index ed1ae19..ed630cc 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalMethodVisitor.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalMethodVisitor.java
@@ -94,6 +94,9 @@ public class TraversalMethodVisitor extends TraversalRootVisitor<GraphTraversal>
      */
     @Override
     public GraphTraversal visitTraversalMethod_mergeV_Map(final GremlinParser.TraversalMethod_mergeV_MapContext ctx) {
+        if (ctx.nullLiteral() != null) {
+            return this.graphTraversal.mergeV((Map) null);
+        }
         return this.graphTraversal.mergeV(GenericLiteralVisitor.getMapLiteral(ctx.genericLiteralMap()));
     }
 
@@ -126,6 +129,9 @@ public class TraversalMethodVisitor extends TraversalRootVisitor<GraphTraversal>
      */
     @Override
     public GraphTraversal visitTraversalMethod_mergeE_Map(final GremlinParser.TraversalMethod_mergeE_MapContext ctx) {
+        if (ctx.nullLiteral() != null) {
+            return this.graphTraversal.mergeE((Map) null);
+        }
         return this.graphTraversal.mergeE(GenericLiteralVisitor.getMapLiteral(ctx.genericLiteralMap()));
     }
 
@@ -1035,6 +1041,10 @@ public class TraversalMethodVisitor extends TraversalRootVisitor<GraphTraversal>
      */
     @Override
     public GraphTraversal visitTraversalMethod_option_Merge_Map(final GremlinParser.TraversalMethod_option_Merge_MapContext ctx) {
+        if (ctx.nullLiteral() != null) {
+            return this.graphTraversal.option(TraversalEnumParser.parseTraversalEnumFromContext(Merge.class, ctx.traversalMerge()), (Map) null);
+        }
+
         return graphTraversal.option(TraversalEnumParser.parseTraversalEnumFromContext(Merge.class, ctx.traversalMerge()),
                 (Map) new GenericLiteralVisitor(antlr).visitGenericLiteralMap(ctx.genericLiteralMap()));
     }
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 f7d04f7..6fdda82 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
@@ -119,6 +119,9 @@ public class TraversalSourceSpawnMethodVisitor extends GremlinBaseVisitor<GraphT
 
     @Override
     public GraphTraversal visitTraversalSourceSpawnMethod_mergeV_Map(final GremlinParser.TraversalSourceSpawnMethod_mergeV_MapContext ctx) {
+        if (ctx.nullLiteral() != null) {
+            return this.traversalSource.mergeV((Map) null);
+        }
         return this.traversalSource.mergeV(GenericLiteralVisitor.getMapLiteral(ctx.genericLiteralMap()));
     }
 
@@ -134,6 +137,9 @@ public class TraversalSourceSpawnMethodVisitor extends GremlinBaseVisitor<GraphT
 
     @Override
     public GraphTraversal visitTraversalSourceSpawnMethod_mergeE_Map(final GremlinParser.TraversalSourceSpawnMethod_mergeE_MapContext ctx) {
+        if (ctx.nullLiteral() != null) {
+            return this.traversalSource.mergeE((Map) null);
+        }
         return this.traversalSource.mergeE(GenericLiteralVisitor.getMapLiteral(ctx.genericLiteralMap()));
     }
 }
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeVertexStep.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeVertexStep.java
index b67621b..e92c11d 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeVertexStep.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeVertexStep.java
@@ -45,6 +45,7 @@ import org.apache.tinkerpop.gremlin.structure.util.StringFactory;
 import org.apache.tinkerpop.gremlin.util.iterator.IteratorUtils;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
@@ -180,7 +181,9 @@ public class MergeVertexStep<S> extends FlatMapStep<S, Vertex> implements Mutati
 
         Stream<Vertex> stream;
         // prioritize lookup by id
-        if (search.containsKey(T.id))
+        if (null == search) {
+            return Stream.empty();
+        } else if (search.containsKey(T.id))
             stream = IteratorUtils.stream(graph.vertices(search.get(T.id)));
         else
             stream = IteratorUtils.stream(graph.vertices());
@@ -216,21 +219,23 @@ public class MergeVertexStep<S> extends FlatMapStep<S, Vertex> implements Mutati
             final Map<String, Object> onMatchMap = TraversalUtil.apply(traverser, onMatchTraversal);
             validateMapInput(onMatchMap, true);
 
-            onMatchMap.forEach((key, value) -> {
-                // trigger callbacks for eventing - in this case, it's a VertexPropertyChangedEvent. if there's no
-                // registry/callbacks then just set the property
-                if (this.callbackRegistry != null && !callbackRegistry.getCallbacks().isEmpty()) {
-                    final EventStrategy eventStrategy = getTraversal().getStrategies().getStrategy(EventStrategy.class).get();
-                    final Property<?> p = v.property(key);
-                    final Property<Object> oldValue = p.isPresent() ? eventStrategy.detach(v.property(key)) : null;
-                    final Event.VertexPropertyChangedEvent vpce = new Event.VertexPropertyChangedEvent(eventStrategy.detach(v), oldValue, value);
-                    this.callbackRegistry.getCallbacks().forEach(c -> c.accept(vpce));
-                }
-
-                // try to detect proper cardinality for the key according to the graph
-                final Graph graph = this.getTraversal().getGraph().get();
-                v.property(graph.features().vertex().getCardinality(key), key, value);
-            });
+            if (onMatchMap != null) {
+                onMatchMap.forEach((key, value) -> {
+                    // trigger callbacks for eventing - in this case, it's a VertexPropertyChangedEvent. if there's no
+                    // registry/callbacks then just set the property
+                    if (this.callbackRegistry != null && !callbackRegistry.getCallbacks().isEmpty()) {
+                        final EventStrategy eventStrategy = getTraversal().getStrategies().getStrategy(EventStrategy.class).get();
+                        final Property<?> p = v.property(key);
+                        final Property<Object> oldValue = p.isPresent() ? eventStrategy.detach(v.property(key)) : null;
+                        final Event.VertexPropertyChangedEvent vpce = new Event.VertexPropertyChangedEvent(eventStrategy.detach(v), oldValue, value);
+                        this.callbackRegistry.getCallbacks().forEach(c -> c.accept(vpce));
+                    }
+
+                    // try to detect proper cardinality for the key according to the graph
+                    final Graph graph = this.getTraversal().getGraph().get();
+                    v.property(graph.features().vertex().getCardinality(key), key, value);
+                });
+            }
 
             return v;
         });
@@ -251,21 +256,26 @@ public class MergeVertexStep<S> extends FlatMapStep<S, Vertex> implements Mutati
             // searchCreate should have already been validated so only do it if it is overridden
             if (useOnCreate) validateMapInput(m, false);
 
+            // if onCreate is null then it's a do nothing
             final List<Object> keyValues = new ArrayList<>();
-            for (Map.Entry<Object, Object> entry : m.entrySet()) {
-                keyValues.add(entry.getKey());
-                keyValues.add(entry.getValue());
-            }
-            vertex = this.getTraversal().getGraph().get().addVertex(keyValues.toArray(new Object[keyValues.size()]));
+            if (m != null) {
+                for (Map.Entry<Object, Object> entry : m.entrySet()) {
+                    keyValues.add(entry.getKey());
+                    keyValues.add(entry.getValue());
+                }
+                vertex = this.getTraversal().getGraph().get().addVertex(keyValues.toArray(new Object[keyValues.size()]));
 
-            // trigger callbacks for eventing - in this case, it's a VertexAddedEvent
-            if (this.callbackRegistry != null && !callbackRegistry.getCallbacks().isEmpty()) {
-                final EventStrategy eventStrategy = getTraversal().getStrategies().getStrategy(EventStrategy.class).get();
-                final Event.VertexAddedEvent vae = new Event.VertexAddedEvent(eventStrategy.detach(vertex));
-                this.callbackRegistry.getCallbacks().forEach(c -> c.accept(vae));
-            }
+                // trigger callbacks for eventing - in this case, it's a VertexAddedEvent
+                if (this.callbackRegistry != null && !callbackRegistry.getCallbacks().isEmpty()) {
+                    final EventStrategy eventStrategy = getTraversal().getStrategies().getStrategy(EventStrategy.class).get();
+                    final Event.VertexAddedEvent vae = new Event.VertexAddedEvent(eventStrategy.detach(vertex));
+                    this.callbackRegistry.getCallbacks().forEach(c -> c.accept(vae));
+                }
 
-            return IteratorUtils.of(vertex);
+                return IteratorUtils.of(vertex);
+            } else {
+                return Collections.emptyIterator();
+            }
         }
     }
 
@@ -274,6 +284,7 @@ public class MergeVertexStep<S> extends FlatMapStep<S, Vertex> implements Mutati
      * to immutable parts of an {@link Edge} (id, label, incident vertices) so those can be ignored in the validation.
      */
     public static void validateMapInput(final Map<?,Object> m, final boolean ignoreTokens) {
+        if (null == m) return;
         if (ignoreTokens) {
             m.entrySet().stream().filter(e -> {
                 final Object k = e.getKey();
@@ -295,11 +306,24 @@ public class MergeVertexStep<S> extends FlatMapStep<S, Vertex> implements Mutati
         }
 
         if (!ignoreTokens) {
-            m.entrySet().stream().filter(e -> e.getKey() == T.label && !(e.getValue() instanceof String)).findFirst().map(e -> {
-                throw new IllegalArgumentException(String.format(
-                        "mergeV() expects T.label value to be of String - found: %s",
-                        e.getValue().getClass().getSimpleName()));
-            });
+            if (m.containsKey(T.id)) {
+                if (null == m.get(T.id)) {
+                    throw new IllegalArgumentException("Vertex id cannot be null");
+                }
+            }
+
+            if (m.containsKey(T.label)) {
+                final Object l = m.get(T.label);
+                if (null == l) {
+                    throw new IllegalArgumentException("Vertex label cannot be null");
+                }
+
+                if (!(l instanceof String)) {
+                    throw new IllegalArgumentException(String.format(
+                            "mergeV() expects T.label value to be of String - found: %s",
+                            l.getClass().getSimpleName()));
+                }
+            }
         }
     }
 
diff --git a/gremlin-language/src/main/antlr4/Gremlin.g4 b/gremlin-language/src/main/antlr4/Gremlin.g4
index f359dd8..298cc3e 100644
--- a/gremlin-language/src/main/antlr4/Gremlin.g4
+++ b/gremlin-language/src/main/antlr4/Gremlin.g4
@@ -131,12 +131,12 @@ traversalSourceSpawnMethod_io
     ;
 
 traversalSourceSpawnMethod_mergeV
-    : 'mergeV' LPAREN genericLiteralMap RPAREN #traversalSourceSpawnMethod_mergeV_Map
+    : 'mergeV' LPAREN (genericLiteralMap | nullLiteral) RPAREN #traversalSourceSpawnMethod_mergeV_Map
     | 'mergeV' LPAREN nestedTraversal RPAREN #traversalSourceSpawnMethod_mergeV_Traversal
     ;
 
 traversalSourceSpawnMethod_mergeE
-    : 'mergeE' LPAREN genericLiteralMap RPAREN #traversalSourceSpawnMethod_mergeE_Map
+    : 'mergeE' LPAREN (genericLiteralMap | nullLiteral) RPAREN #traversalSourceSpawnMethod_mergeE_Map
     | 'mergeE' LPAREN nestedTraversal RPAREN #traversalSourceSpawnMethod_mergeE_Traversal
     ;
 
@@ -288,17 +288,16 @@ traversalMethod_addV
 
 traversalMethod_mergeV
     : 'mergeV' LPAREN RPAREN #traversalMethod_mergeV_empty
-    | 'mergeV' LPAREN genericLiteralMap RPAREN #traversalMethod_mergeV_Map
+    | 'mergeV' LPAREN (genericLiteralMap | nullLiteral) RPAREN #traversalMethod_mergeV_Map
     | 'mergeV' LPAREN nestedTraversal RPAREN #traversalMethod_mergeV_Traversal
     ;
 
 traversalMethod_mergeE
     : 'mergeE' LPAREN RPAREN #traversalMethod_mergeE_empty
-    | 'mergeE' LPAREN genericLiteralMap RPAREN #traversalMethod_mergeE_Map
+    | 'mergeE' LPAREN (genericLiteralMap | nullLiteral) RPAREN #traversalMethod_mergeE_Map
     | 'mergeE' LPAREN nestedTraversal RPAREN #traversalMethod_mergeE_Traversal
     ;
 
-
 traversalMethod_aggregate
 	: 'aggregate' LPAREN traversalScope COMMA stringLiteral RPAREN #traversalMethod_aggregate_Scope_String
 	| 'aggregate' LPAREN stringLiteral RPAREN #traversalMethod_aggregate_String
@@ -558,7 +557,7 @@ traversalMethod_not
 
 traversalMethod_option
 	: 'option' LPAREN traversalPredicate COMMA nestedTraversal RPAREN #traversalMethod_option_Predicate_Traversal
-	| 'option' LPAREN traversalMerge COMMA genericLiteralMap RPAREN #traversalMethod_option_Merge_Map
+	| 'option' LPAREN traversalMerge COMMA (genericLiteralMap | nullLiteral) RPAREN #traversalMethod_option_Merge_Map
 	| 'option' LPAREN traversalMerge COMMA nestedTraversal RPAREN #traversalMethod_option_Merge_Traversal
 	| 'option' LPAREN genericLiteral COMMA nestedTraversal RPAREN #traversalMethod_option_Object_Traversal
 	| 'option' LPAREN nestedTraversal RPAREN #traversalMethod_option_Traversal
diff --git a/gremlin-test/features/map/MergeVertex.feature b/gremlin-test/features/map/MergeVertex.feature
index 4707555..7201433 100644
--- a/gremlin-test/features/map/MergeVertex.feature
+++ b/gremlin-test/features/map/MergeVertex.feature
@@ -64,7 +64,123 @@ Feature: Step - mergeV()
   # g_V_mapXmergeXlabel_person_name_joshXX
   #   - mergeV(Map) with no option() - testing child traversal usage
   #   - results in one new vertex and one existing vertex that was just created
+  # g_mergeVXnullX
+  #   - mergeV(null) with no option()
+  #   - results in no new vertex and nothing returned
+  # g_mergeVXemptyX
+  #   - mergeV(empty) with no option()
+  #   - results in matched vertex with no updates
+  # g_mergeVXemptyX_no_existing
+  #   - mergeV(empty) with no option()
+  #   - results in not matched updates and a creation of a vertex with default values
+  # g_mergeVXnullX_optionXonCreate_emptyX
+  #   - mergeV(null) with onCreate(empty)
+  #   - results in no matches and creates a default vertex
+  # g_mergeVXlabel_person_name_stephenX_optionXonCreate_nullX
+  #   - mergeV(Map) with onCreate(null)
+  #   - results in no match and no vertex creation
+  # g_mergeVXnullX_optionXonCreate_label_null_name_markoX
+  #   - mergeV(null) with onCreate(Map) where Map has a null label
+  #   - results in error
+  # g_mergeVXemptyX_optionXonMatch_nullX
+  #   - mergeV(empty) with onMatch(null)
+  #   - results in a match and no vertex update
 
+  Scenario: g_mergeVXemptyX_optionXonMatch_nullX
+    Given the empty graph
+    And the graph initializer of
+      """
+      g.addV("person").property("name", "marko").property("age", 29)
+      """
+    And the traversal of
+      """
+      g.mergeV([:]).option(Merge.onMatch, null)
+      """
+    When iterated to list
+    Then the result should have a count of 1
+    And the graph should return 1 for count of "g.V().has(\"person\",\"name\",\"marko\").has(\"age\",29)"
+
+  Scenario: g_mergeVXnullX_optionXonCreate_label_null_name_markoX
+    Given the empty graph
+    And the graph initializer of
+      """
+      g.addV("person").property("name", "marko").property("age", 29)
+      """
+    And using the parameter xx1 defined as "m[{\"t[label]\": null, \"name\":\"marko\"}]"
+    And the traversal of
+      """
+      g.mergeV(null).option(Merge.onCreate,xx1)
+      """
+    When iterated to list
+    Then the traversal will raise an error
+
+  Scenario: g_mergeVXlabel_person_name_stephenX_optionXonCreate_nullX
+    Given the empty graph
+    And the graph initializer of
+      """
+      g.addV("person").property("name", "marko").property("age", 29)
+      """
+    And using the parameter xx1 defined as "m[{\"t[label]\": \"person\", \"name\":\"stephen\"}]"
+    And the traversal of
+      """
+      g.mergeV(xx1).option(Merge.onCreate, null)
+      """
+    When iterated to list
+    Then the result should have a count of 0
+    And the graph should return 1 for count of "g.V()"
+    And the graph should return 1 for count of "g.V().has(\"person\",\"name\",\"marko\")"
+
+  Scenario: g_mergeVXnullX_optionXonCreate_emptyX
+    Given the empty graph
+    And the graph initializer of
+      """
+      g.addV("person").property("name", "marko").property("age", 29)
+      """
+    And the traversal of
+      """
+      g.mergeV(null).option(Merge.onCreate,[:])
+      """
+    When iterated to list
+    Then the result should have a count of 1
+    And the graph should return 2 for count of "g.V()"
+
+  Scenario: g_mergeVXemptyX_no_existing
+    Given the empty graph
+    And the traversal of
+      """
+      g.mergeV([:])
+      """
+    When iterated to list
+    Then the result should have a count of 1
+    And the graph should return 1 for count of "g.V()"
+
+  Scenario: g_mergeVXemptyX
+    Given the empty graph
+    And the graph initializer of
+      """
+      g.addV("person").property("name", "marko").property("age", 29)
+      """
+    And the traversal of
+      """
+      g.mergeV([:])
+      """
+    When iterated to list
+    Then the result should have a count of 1
+    And the graph should return 1 for count of "g.V().has(\"person\",\"name\",\"marko\").has(\"age\",29)"
+
+  Scenario: g_mergeVXnullX
+    Given the empty graph
+    And the graph initializer of
+      """
+      g.addV("person").property("name", "marko").property("age", 29)
+      """
+    And the traversal of
+      """
+      g.mergeV(null)
+      """
+    When iterated to list
+    Then the result should have a count of 0
+    And the graph should return 1 for count of "g.V()"
 
   Scenario: g_mergeVXlabel_person_name_stephenX
     Given the empty graph
diff --git a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/map/TinkerMergeVertexStep.java b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/map/TinkerMergeVertexStep.java
index 6771ebd..9c3cc03 100644
--- a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/map/TinkerMergeVertexStep.java
+++ b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/map/TinkerMergeVertexStep.java
@@ -52,7 +52,9 @@ public class TinkerMergeVertexStep<S> extends MergeVertexStep<S> {
 
         Stream<Vertex> stream;
         // prioritize lookup by id but otherwise attempt an index lookup
-        if (search.containsKey(T.id)) {
+        if (null == search) {
+            return Stream.empty();
+        } else if (search.containsKey(T.id)) {
             stream = IteratorUtils.stream(graph.vertices(search.get(T.id)));
         } else {
             // look for the first index we can find - that's the lucky winner. may or may not be the most selective