You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tinkerpop.apache.org by mi...@apache.org on 2023/01/02 16:35:19 UTC

[tinkerpop] 01/01: Initial commit

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

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

commit a17b40547ee163d5615c6f93ff194a7d16139f25
Author: Mike Personick <mi...@supersonick.io>
AuthorDate: Mon Jan 2 09:33:42 2023 -0700

    Initial commit
---
 .../language/grammar/GenericLiteralVisitor.java    |   9 +
 .../tinkerpop/gremlin/process/traversal/Merge.java |   4 +-
 .../traversal/dsl/graph/GraphTraversal.java        |   2 -
 .../traversal/dsl/graph/GraphTraversalSource.java  |   2 -
 .../process/traversal/step/map/MergeEdgeStep.java  | 625 +++++++++------------
 .../process/traversal/step/map/MergeStep.java      | 299 ++++++++++
 .../traversal/step/map/MergeVertexStep.java        | 383 ++++---------
 .../gremlin/util/iterator/IteratorUtils.java       | 163 ++++--
 .../Gherkin/CommonSteps.cs                         |   6 +
 .../main/javascript/gremlin-javascript/index.js    |   1 +
 .../test/cucumber/feature-steps.js                 |   8 +-
 gremlin-language/src/main/antlr4/Gremlin.g4        |   3 +
 .../src/main/python/radish/feature_steps.py        |   4 +-
 .../tinkerpop/gremlin/features/StepDefinition.java |  28 +-
 .../process/ProcessLimitedStandardSuite.java       |  53 +-
 .../process/traversal/step/map/MergeEdgeTest.java  |  54 ++
 .../gremlin/test/features/map/MergeEdge.feature    | 274 +++++----
 .../gremlin/test/features/map/MergeVertex.feature  | 101 +++-
 .../traversal/step/map/TinkerMergeEdgeStep.java    | 115 ----
 .../traversal/step/map/TinkerMergeVertexStep.java  |  90 ---
 .../optimization/TinkerMergeEVStepStrategy.java    |  61 --
 .../gremlin/tinkergraph/structure/TinkerGraph.java |   4 +-
 .../tinkergraph/TinkerGraphFeatureTest.java        |   4 +-
 23 files changed, 1205 insertions(+), 1088 deletions(-)

diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/GenericLiteralVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/GenericLiteralVisitor.java
index b3ae657bab..78d144616f 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/GenericLiteralVisitor.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/GenericLiteralVisitor.java
@@ -21,6 +21,7 @@ package org.apache.tinkerpop.gremlin.language.grammar;
 import org.antlr.v4.runtime.tree.ParseTree;
 import org.antlr.v4.runtime.tree.TerminalNode;
 import org.apache.commons.text.StringEscapeUtils;
+import org.apache.tinkerpop.gremlin.process.traversal.Merge;
 import org.apache.tinkerpop.gremlin.process.traversal.Pick;
 import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
 import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategy;
@@ -518,6 +519,14 @@ public class GenericLiteralVisitor extends DefaultGremlinBaseVisitor<Object> {
         return TraversalEnumParser.parseTraversalDirectionFromContext(ctx);
     }
 
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public Object visitTraversalMerge(final GremlinParser.TraversalMergeContext ctx) {
+        return TraversalEnumParser.parseTraversalEnumFromContext(Merge.class, ctx);
+    }
+
     /**
      * {@inheritDoc}
      */
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/Merge.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/Merge.java
index 068dadd5cb..629217d74f 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/Merge.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/Merge.java
@@ -42,5 +42,7 @@ public enum Merge {
      *
      * @since 3.6.0
      */
-    onMatch
+    onMatch,
+
+    outV, inV
 }
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.java
index 6956b21bda..455709715c 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.java
@@ -1117,7 +1117,6 @@ public interface GraphTraversal<S, E> extends Traversal<S, E> {
      * @since 3.6.0
      */
     public default GraphTraversal<S, Vertex> mergeV(final Map<Object, Object> searchCreate) {
-        MergeVertexStep.validateMapInput(searchCreate, false);
         this.asAdmin().getBytecode().addStep(Symbols.mergeV, searchCreate);
         final MergeVertexStep<S> step = new MergeVertexStep<>(this.asAdmin(), false, searchCreate);
         return this.asAdmin().addStep(step);
@@ -1161,7 +1160,6 @@ public interface GraphTraversal<S, E> extends Traversal<S, E> {
      */
     public default GraphTraversal<S, Edge> mergeE(final Map<Object, Object> searchCreate) {
         // get a construction time exception if the Map is bad
-        MergeEdgeStep.validateMapInput(searchCreate, false);
         this.asAdmin().getBytecode().addStep(Symbols.mergeE, searchCreate);
         final MergeEdgeStep<S> step = new MergeEdgeStep(this.asAdmin(), false, searchCreate);
         return this.asAdmin().addStep(step);
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 696cd0b796..c12bf41e46 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
@@ -376,7 +376,6 @@ public class GraphTraversalSource implements TraversalSource {
      * @since 3.6.0
      */
     public GraphTraversal<Vertex, Vertex> mergeV(final Map<Object, Object> searchCreate) {
-        MergeVertexStep.validateMapInput(searchCreate, false);
         final GraphTraversalSource clone = this.clone();
         clone.bytecode.addStep(GraphTraversal.Symbols.mergeV, searchCreate);
         final GraphTraversal.Admin<Vertex, Vertex> traversal = new DefaultGraphTraversal<>(clone);
@@ -412,7 +411,6 @@ public class GraphTraversalSource implements TraversalSource {
      * @since 3.6.0
      */
     public GraphTraversal<Edge, Edge> mergeE(final Map<?, Object> searchCreate) {
-        MergeEdgeStep.validateMapInput(searchCreate, false);
         final GraphTraversalSource clone = this.clone();
         clone.bytecode.addStep(GraphTraversal.Symbols.mergeE, searchCreate);
         final GraphTraversal.Admin<Edge, Edge> traversal = new DefaultGraphTraversal<>(clone);
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 9e364b0a38..eadc4f5285 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
@@ -18,11 +18,26 @@
  */
 package org.apache.tinkerpop.gremlin.process.traversal.step.map;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+import org.apache.commons.collections.CollectionUtils;
 import org.apache.tinkerpop.gremlin.process.traversal.Merge;
 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.dsl.graph.GraphTraversal;
+import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__;
 import org.apache.tinkerpop.gremlin.process.traversal.lambda.ConstantTraversal;
 import org.apache.tinkerpop.gremlin.process.traversal.lambda.IdentityTraversal;
 import org.apache.tinkerpop.gremlin.process.traversal.step.Mutating;
@@ -33,6 +48,7 @@ import org.apache.tinkerpop.gremlin.process.traversal.step.util.event.Event;
 import org.apache.tinkerpop.gremlin.process.traversal.step.util.event.ListCallbackRegistry;
 import org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.EventStrategy;
 import org.apache.tinkerpop.gremlin.process.traversal.traverser.TraverserRequirement;
+import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalHelper;
 import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalUtil;
 import org.apache.tinkerpop.gremlin.structure.Direction;
 import org.apache.tinkerpop.gremlin.structure.Edge;
@@ -43,138 +59,109 @@ import org.apache.tinkerpop.gremlin.structure.Vertex;
 import org.apache.tinkerpop.gremlin.structure.util.Attachable;
 import org.apache.tinkerpop.gremlin.structure.util.CloseableIterator;
 import org.apache.tinkerpop.gremlin.structure.util.StringFactory;
-import org.apache.tinkerpop.gremlin.structure.util.empty.EmptyGraph;
-import org.apache.tinkerpop.gremlin.structure.util.reference.ReferenceVertex;
 import org.apache.tinkerpop.gremlin.util.iterator.IteratorUtils;
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.stream.Stream;
-
 /**
  * Implementation for the {@code mergeE()} step covering both the start step version and the one used mid-traversal.
  */
-public class MergeEdgeStep<S> extends FlatMapStep<S, Edge> implements Mutating<Event>,
-        TraversalOptionParent<Merge, S, Edge> {
-
-    public static final Vertex PLACEHOLDER_VERTEX = new ReferenceVertex(Graph.Hidden.hide(MergeEdgeStep.class.getName()));
-    private final boolean isStart;
-    private boolean first = true;
-    private Traversal.Admin<S,Map<Object, Object>> searchCreateTraversal;
-    private Traversal.Admin<S, Map<Object, Object>> onCreateTraversal = null;
-    private Traversal.Admin<S, Map<String, Object>> onMatchTraversal = null;
+public class MergeEdgeStep<S> extends MergeStep<S, Edge, Object> {
 
-    protected CallbackRegistry<Event> callbackRegistry;
+    private static final Set allowedTokens = new LinkedHashSet(Arrays.asList(T.id, T.label, Direction.IN, Direction.OUT));
 
-    public MergeEdgeStep(final Traversal.Admin traversal, final boolean isStart) {
-        this(traversal, isStart, new IdentityTraversal<>());
+    public static void validateMapInput(final Map map, final boolean ignoreTokens) {
+        MergeStep.validate(map, ignoreTokens, allowedTokens);
     }
 
-    public MergeEdgeStep(final Traversal.Admin traversal, final boolean isStart, final Map<Object, Object> searchCreate) {
-        this(traversal, isStart, new ConstantTraversal<>(searchCreate));
+    private Traversal.Admin<S, Object> outVTraversal = null;
+    private Traversal.Admin<S, Object> inVTraversal = null;
+
+    public MergeEdgeStep(final Traversal.Admin traversal, final boolean isStart) {
+        super(traversal, isStart);
     }
 
-    public MergeEdgeStep(final Traversal.Admin traversal, final boolean isStart, final Traversal.Admin<?,Map<Object, Object>> searchCreateTraversal) {
-        super(traversal);
-        this.isStart = isStart;
-        this.searchCreateTraversal = integrateChild(searchCreateTraversal);
+    public MergeEdgeStep(final Traversal.Admin traversal, final boolean isStart, final Map merge) {
+        super(traversal, isStart, merge);
     }
 
-    /**
-     * Gets the traversal that will be used to provide the {@code Map} that will be used to search for edges.
-     * This {@code Map} also will be used as the default data set to be used to create a edge if the search is not
-     * successful.
-     */
-    public Traversal.Admin<S, Map<Object, Object>> getSearchCreateTraversal() {
-        return searchCreateTraversal;
+    public MergeEdgeStep(final Traversal.Admin traversal, final boolean isStart, final Traversal.Admin<S,Map> mergeTraversal) {
+        super(traversal, isStart, mergeTraversal);
     }
 
     /**
-     * Gets the traversal that will be used to provide the {@code Map} that will be the override to the one provided
-     * by the {@link #getSearchCreateTraversal()} for edge creation events.
+     * Gets the traversal that will be used to provide the {@code Map} that will be used to identify the Direction.OUT
+     * vertex during merge.
      */
-    public Traversal.Admin<S, Map<Object, Object>> getOnCreateTraversal() {
-        return onCreateTraversal;
+    public Traversal.Admin<S, Object> getOutVTraversal() {
+        return outVTraversal;
     }
 
     /**
-     * Gets the traversal that will be used to provide the {@code Map} that will be used to modify edges that
-     * match the search criteria of {@link #getSearchCreateTraversal()}.
+     * Gets the traversal that will be used to provide the {@code Map} that will be used to identify the Direction.IN
+     * vertex during merge.
      */
-    public Traversal.Admin<S, Map<String, Object>> getOnMatchTraversal() {
-        return onMatchTraversal;
+    public Traversal.Admin<S, Object> getInVTraversal() {
+        return inVTraversal;
     }
 
-    /**
-     * Determines if this is a start step.
-     */
-    public boolean isStart() {
-        return isStart;
+    @Override
+    public void addChildOption(final Merge token, final Traversal.Admin<S, Object> traversalOption) {
+        if (token == Merge.outV) {
+            this.outVTraversal = this.integrateChild(traversalOption);
+        } else if (token == Merge.inV) {
+            this.inVTraversal = this.integrateChild(traversalOption);
+        } else {
+            super.addChildOption(token, traversalOption);
+        }
     }
 
-    /**
-     * Determine if this is the first pass through {@link #processNextStart()}.
-     */
-    public boolean isFirst() {
-        return first;
+    @Override
+    public <S, C> List<Traversal.Admin<S, C>> getLocalChildren() {
+        final List<Traversal.Admin<S, C>> children = super.getLocalChildren();
+        if (outVTraversal != null) children.add((Traversal.Admin<S, C>) outVTraversal);
+        if (inVTraversal != null) children.add((Traversal.Admin<S, C>) inVTraversal);
+        return children;
     }
 
-    public CallbackRegistry<Event> getCallbackRegistry() {
-        return callbackRegistry;
+    @Override
+    public int hashCode() {
+        int result = super.hashCode();
+        if (outVTraversal != null)
+            result ^= outVTraversal.hashCode();
+        if (inVTraversal != null)
+            result ^= inVTraversal.hashCode();
+        return result;
     }
 
     @Override
-    public void addChildOption(final Merge token, final Traversal.Admin<S, Edge> traversalOption) {
-        if (token == Merge.onCreate) {
-            this.onCreateTraversal = this.integrateChild(traversalOption);
-        } else if (token == Merge.onMatch) {
-            this.onMatchTraversal = this.integrateChild(traversalOption);
-        } else {
-            throw new UnsupportedOperationException(String.format("Option %s for Merge is not supported", token.name()));
-        }
+    public void reset() {
+        super.reset();
+        if (outVTraversal != null) outVTraversal.reset();
+        if (inVTraversal != null) inVTraversal.reset();
     }
 
     @Override
-    public <S, E> List<Traversal.Admin<S, E>> getLocalChildren() {
-        final List<Traversal.Admin<S, E>> children = new ArrayList<>();
-        if (searchCreateTraversal != null) children.add((Traversal.Admin<S, E>) searchCreateTraversal);
-        if (onMatchTraversal != null) children.add((Traversal.Admin<S, E>) onMatchTraversal);
-        if (onCreateTraversal != null) children.add((Traversal.Admin<S, E>) onCreateTraversal);
-        return children;
+    public String toString() {
+        return StringFactory.stepString(this, mergeTraversal, onCreateTraversal, onMatchTraversal, outVTraversal, inVTraversal);
     }
 
     @Override
-    public void configure(final Object... keyValues) {
-        // this is a Mutating step but property() should not be folded into this step.  The main issue here is that
-        // this method won't know what step called it - property() or with() or something else so it can't make the
-        // choice easily to throw an exception, write the keys/values to parameters, etc. It really is up to the
-        // caller to make sure it is handled properly at this point. this may best be left as a do-nothing method for
-        // now.
+    public void setTraversal(final Traversal.Admin<?, ?> parentTraversal) {
+        super.setTraversal(parentTraversal);
+        this.integrateChild(outVTraversal);
+        this.integrateChild(inVTraversal);
     }
 
     @Override
-    public Parameters getParameters() {
-        // merge doesn't take fold ups of property() calls. those need to get treated as regular old PropertyStep
-        // instances. not sure if this should support with() though.....none of the other Mutating steps do.
-        return null;
+    public MergeEdgeStep<S> clone() {
+        final MergeEdgeStep<S> clone = (MergeEdgeStep<S>) super.clone();
+        clone.outVTraversal = outVTraversal != null ? outVTraversal.clone() : null;
+        clone.inVTraversal = inVTraversal != null ? inVTraversal.clone() : null;
+        return clone;
     }
 
     @Override
-    protected Traverser.Admin<Edge> 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(PLACEHOLDER_VERTEX, (Step) this, 1L));
-        }
-        return super.processNextStart();
+    protected Set getAllowedTokens() {
+        return allowedTokens;
     }
 
     /**
@@ -183,202 +170,240 @@ public class MergeEdgeStep<S> extends FlatMapStep<S, Edge> implements Mutating<E
      * matching the list of edges. This implementation is only optimized for the {@link T#id} so any other usage
      * will simply be in-memory filtering which could be slow.
      */
-    protected Stream<Edge> createSearchStream(final Map<Object,Object> search) {
-        final Graph graph = this.getTraversal().getGraph().get();
-
-        final Optional<Direction> directionUsedInLookup;
-        Stream<Edge> stream;
-        // prioritize lookup by id, then use vertices as starting point if possible
-        if (null == search) {
-            return Stream.empty();
-        } else if (search.containsKey(T.id)) {
-            stream = IteratorUtils.stream(graph.edges(search.get(T.id)));
-            directionUsedInLookup = Optional.empty();
-        } else if (search.containsKey(Direction.BOTH)) {
-            // filter self-edges with distinct()
-            stream = IteratorUtils.stream(graph.vertices(search.get(Direction.BOTH))).flatMap(v -> IteratorUtils.stream(v.edges(Direction.BOTH))).distinct();
-            directionUsedInLookup = Optional.of(Direction.BOTH);
-        } else if (search.containsKey(Direction.OUT)) {
-            stream = IteratorUtils.stream(graph.vertices(search.get(Direction.OUT))).flatMap(v -> IteratorUtils.stream(v.edges(Direction.OUT)));
-            directionUsedInLookup = Optional.of(Direction.OUT);
-        } else if (search.containsKey(Direction.IN)) {
-            stream = IteratorUtils.stream(graph.vertices(search.get(Direction.IN))).flatMap(v -> IteratorUtils.stream(v.edges(Direction.IN)));
-            directionUsedInLookup = Optional.of(Direction.IN);
+    protected Iterator<Edge> searchEdges(final Map<?,?> search) {
+        final Graph graph = getGraph();
+
+        final Object edgeId = search.get(T.id);
+        final String edgeLabel = (String) search.get(T.label);
+        final Object fromId = search.get(Direction.OUT);
+        final Object toId = search.get(Direction.IN);
+
+        GraphTraversal t;
+        if (edgeId != null) {
+            // if eid:
+            //   g.E(edgeId).hasLabel(edgeLabel).where(__.outV().hasId(fromId)).where(__.inV().hasId(toId));
+            t = graph.traversal().E(edgeId);
+            if (edgeLabel != null)
+                t = t.hasLabel(edgeLabel);
+            if (fromId != null)
+                t = t.where(__.outV().hasId(fromId));
+            if (toId != null)
+                t = t.where(__.inV().hasId(toId));
+        } else if (fromId != null) {
+            // else if fromId:
+            //   g.V(fromId).outE(edgeLabel).where(__.inV().hasId(toId));
+            t = graph.traversal().V(fromId);
+            if (edgeLabel != null)
+                t = t.outE(edgeLabel);
+            else
+                t = t.outE();
+            if (toId != null)
+                t = t.where(__.inV().hasId(toId));
+        } else if (toId != null) {
+            // else if toId:
+            //   g.V(toId).inE(edgeLabel);
+            t = graph.traversal().V(toId);
+            if (edgeLabel != null)
+                t = t.inE(edgeLabel);
+            else
+                t = t.inE();
         } else {
-            stream = IteratorUtils.stream(graph.edges());
-            directionUsedInLookup = Optional.empty();
+            // else:
+            t = graph.traversal().E();
+        }
+
+        // add property constraints
+        for (final Map.Entry<?,?> e : search.entrySet()) {
+            final Object k = e.getKey();
+            if (!(k instanceof String)) continue;
+            t = t.has((String) k, e.getValue());
+        }
+
+        // this should auto-close the underlying traversal
+        return t;
+    }
+
+    protected Map<?,?> resolveVertices(final Map map, final Traverser.Admin<S> traverser) {
+        resolveVertex(Merge.outV, Direction.OUT, map, traverser, outVTraversal);
+        resolveVertex(Merge.inV, Direction.IN, map, traverser, inVTraversal);
+        return map;
+    }
+
+    protected void resolveVertex(final Merge token, final Direction direction, final Map map,
+            final Traverser.Admin<S> traverser, final Traversal.Admin<S, Object> traversal) {
+        // no Direction specified in the map, nothing to resolve
+        if (!map.containsKey(direction))
+            return;
+
+        final Object value = map.get(direction);
+        if (Objects.equals(token, value)) {
+            if (traversal == null) {
+                throw new IllegalArgumentException(String.format(
+                        "option(%s) must be specified if it is used for %s", token, direction));
+            }
+            final Vertex vertex = resolveVertex(traverser, traversal);
+            if (vertex == null)
+                throw new IllegalArgumentException(String.format(
+                        "Could not resolve vertex for option(%s)", token));
+            map.put(direction, vertex.id());
+        } else if (value instanceof Vertex) {
+            // flatten Vertex down to its id
+            map.put(direction, ((Vertex) value).id());
         }
+    }
 
-        // in-memory filter is not going to be nice here. it will be up to graphs to optimize this step as they do
-        // for other Mutation steps
-        stream = stream.filter(e -> {
-            // try to match on all search criteria skipping T.id as it was handled above
-            return search.entrySet().stream().filter(
-                    kv -> kv.getKey() != T.id && !(directionUsedInLookup.isPresent() && kv.getKey() == directionUsedInLookup.get())).
-                    allMatch(kv -> {
-                        if (kv.getKey() == T.label) {
-                            return e.label().equals(kv.getValue());
-                        } else if (kv.getKey() instanceof Direction) {
-                            final Direction direction = (Direction) kv.getKey();
-
-                            // try to take advantage of string id conversions of the graph by doing a lookup rather
-                            // than direct compare on id
-                            final Iterator<Vertex> found = graph.vertices(kv.getValue());
-                            final Iterator<Vertex> dfound = e.vertices(direction);
-                            final boolean matched = found.hasNext() && dfound.next().equals(found.next());
-                            CloseableIterator.closeIterator(found);
-                            CloseableIterator.closeIterator(dfound);
-                            return matched;
-                        } else {
-                            final Property<Object> vp = e.property(kv.getKey().toString());
-                            return vp.isPresent() && kv.getValue().equals(vp.value());
-                        }
-                    });
-        });
-
-        return stream;
+    /*
+     * outV/inV traversal can either provide a Map (which we then use to search for a vertex) or it can provide a
+     * Vertex directly.
+     */
+    protected Vertex resolveVertex(final Traverser.Admin<S> traverser, final Traversal.Admin<S, Object> traversal) {
+        final Object o = TraversalUtil.apply(traverser, traversal);
+        if (o instanceof Vertex)
+            return (Vertex) o;
+        else if (o instanceof Map) {
+            final Map search = (Map) o;
+            final Vertex v = IteratorUtils.findFirst(MergeVertexStep.searchVertices(getGraph(), search)).get();
+            return tryAttachVertex(v);
+        }
+        return null;
     }
 
     @Override
     protected Iterator<Edge> flatMap(final Traverser.Admin<S> traverser) {
-        final Map<Object,Object> searchCreate = TraversalUtil.apply(traverser, searchCreateTraversal);
-
-        validateMapInput(searchCreate, false);
+        final Map unresolvedMergeMap = materializeMap(traverser, mergeTraversal);
+        validateMapInput(unresolvedMergeMap, false);
 
-        final Vertex outV = resolveVertex(traverser, searchCreate, Direction.OUT);
-        final Vertex inV = resolveVertex(traverser, searchCreate, Direction.IN);
+        /*
+         * Create a copy of the unresolved map and attempt to resolve any Vertex references.
+         */
+        final Map mergeMap = resolveVertices(new LinkedHashMap<>(unresolvedMergeMap), traverser);
 
-        // need to copy searchCreate so that each traverser gets fresh search criteria if we use the traverser value
-        final Map<Object,Object> searchCreateCopy = null == searchCreate ? null : new HashMap<>();
-        if (searchCreateCopy != null) {
-            searchCreateCopy.putAll(searchCreate);
+        Iterator<Edge> edges = searchEdges(mergeMap);
 
-            // out/in not specified in searchCreate so try to use the traverser. BOTH is not an accepted user input
-            // but helps with the search stream as it allows in/out to both be in the search stream. in other words,
-            // g.V().mergeE([label:'knows']) will end up traversing BOTH "knows" edges for each vertex
-            if (!searchCreateCopy.containsKey(Direction.OUT) && !searchCreateCopy.containsKey(Direction.IN) &&
-                    outV == inV && inV != PLACEHOLDER_VERTEX) {
-                searchCreateCopy.put(Direction.BOTH, outV);
-            }
-        }
+        if (onMatchTraversal != null) {
 
-        Stream<Edge> stream = createSearchStream(searchCreateCopy);
-        stream = stream.map(e -> {
-            // if no onMatch is defined then there is no update - return the edge unchanged
-            if (null == onMatchTraversal) return e;
+            edges = IteratorUtils.peek(edges, e -> {
 
-            // if this was a start step the traverser is initialized with placeholder edge, so override that with
-            // the matched Edge so that the option() traversal can operate on it properly
-            if (isStart) traverser.set((S) e);
+                // if this was a start step the traverser is initialized with placeholder edge, so override that with
+                // the matched Edge so that the option() traversal can operate on it properly
+                if (isStart) traverser.set((S) e);
 
-            // assume good input from GraphTraversal - folks might drop in a T here even though it is immutable
-            final Map<String, Object> onMatchMap = TraversalUtil.apply(traverser, onMatchTraversal);
-            validateMapInput(onMatchMap, true);
+                // assume good input from GraphTraversal - folks might drop in a T here even though it is immutable
+                final Map<String, ?> onMatchMap = materializeMap(traverser, onMatchTraversal);
+                validateMapInput(onMatchMap, true);
 
-            if (onMatchMap != null) {
                 onMatchMap.forEach((key, value) -> {
                     // trigger callbacks for eventing - in this case, it's a EdgePropertyChangedEvent. 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 EventStrategy eventStrategy =
+                                getTraversal().getStrategies().getStrategy(EventStrategy.class).get();
                         final Property<?> p = e.property(key);
-                        final Property<Object> oldValue = p.isPresent() ? eventStrategy.detach(e.property(key)) : null;
+                        final Property<Object> oldValue =
+                                p.isPresent() ? eventStrategy.detach(e.property(key)) : null;
                         final Event.EdgePropertyChangedEvent vpce = new Event.EdgePropertyChangedEvent(eventStrategy.detach(e), oldValue, value);
                         this.callbackRegistry.getCallbacks().forEach(c -> c.accept(vpce));
                     }
                     e.property(key, value);
                 });
-            }
 
-            return e;
-        });
+            });
 
-        // if the stream has something then there is a match (possibly updated) and is returned, otherwise a new
-        // edge is created
-        final Iterator<Edge> edges = stream.iterator();
+        }
+
+        /*
+         * Search produced results, and onMatch action will be triggered.
+         */
         if (edges.hasNext()) {
             return edges;
-        } else {
-            final Edge edge;
-
-            // if there is an onCreateTraversal then the search criteria is ignored for the creation as it is provided
-            // by way of the traversal which will return the Map
-            final boolean useOnCreate = onCreateTraversal != null;
-            final Map<Object,Object> onCreateMap = useOnCreate ? TraversalUtil.apply(traverser, onCreateTraversal) : searchCreateCopy;
-
-            // searchCreate should have already been validated so only do it if it is overridden
-            if (useOnCreate) validateMapInput(onCreateMap, false);
-
-            if (onCreateMap != null) {
-                // check if from/to were already determined by traverser/searchMatch, and if not, then at least ensure that
-                // the from/to is set in onCreateMap
-                if (outV == PLACEHOLDER_VERTEX && !onCreateMap.containsKey(Direction.OUT))
-                    throw new IllegalArgumentException("Out Vertex not specified - edge cannot be created");
-                if (inV == PLACEHOLDER_VERTEX && !onCreateMap.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;
-
-                // assume the to/from vertices from traverser/searchMatch are what we want, but then
-                // consider the override dropping in from onCreate
-                Vertex fromV = outV;
-                Vertex toV = inV;
-
-                for (Map.Entry<Object, Object> entry : onCreateMap.entrySet()) {
-                    if (entry.getKey() instanceof Direction) {
-                        // only override if onCreate was specified otherwise stick to however traverser/searchMatch
-                        // was resolved
-                        if (useOnCreate && entry.getKey().equals(Direction.IN)) {
-                            final Object o = searchCreateCopy.getOrDefault(Direction.IN, entry.getValue());
-                            toV = tryAttachVertex(o instanceof Vertex ? (Vertex) o : new ReferenceVertex(o));
-                        } else if (useOnCreate && entry.getKey().equals(Direction.OUT)) {
-                            final Object o = searchCreateCopy.getOrDefault(Direction.OUT, entry.getValue());
-                            fromV = tryAttachVertex(o instanceof Vertex ? (Vertex) o : new ReferenceVertex(o));
-                        }
-                    } else if (entry.getKey().equals(T.label)) {
-                        label = (String) entry.getValue();
-                    } else {
-                        keyValues.add(entry.getKey());
-                        keyValues.add(entry.getValue());
-                    }
-                }
+        }
 
-                edge = fromV.addEdge(label, toV, keyValues.toArray(new Object[keyValues.size()]));
+        // make sure we close the search traversal
+        CloseableIterator.closeIterator(edges);
 
-                // 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.EdgeAddedEvent vae = new Event.EdgeAddedEvent(eventStrategy.detach(edge));
-                    this.callbackRegistry.getCallbacks().forEach(c -> c.accept(vae));
-                }
+        final Map onCreateMap = onCreateMap(traverser, unresolvedMergeMap, mergeMap);
 
-                return IteratorUtils.of(edge);
-            } else {
-                return Collections.emptyIterator();
-            }
+        // check for from/to vertices, which must be specified for the create action
+        if (!onCreateMap.containsKey(Direction.OUT))
+            throw new IllegalArgumentException("Out Vertex not specified - edge cannot be created");
+        if (!onCreateMap.containsKey(Direction.IN))
+            throw new IllegalArgumentException("In Vertex not specified - edge cannot be created");
+
+        final Vertex fromV = resolveVertex(onCreateMap.get(Direction.OUT));
+        final Vertex toV = resolveVertex(onCreateMap.get(Direction.IN));
+        final String label = (String) onCreateMap.getOrDefault(T.label, Edge.DEFAULT_LABEL);
+
+        final List<Object> properties = new ArrayList<>();
+
+        // add property constraints
+        for (final Map.Entry e : ((Map<?,?>) onCreateMap).entrySet()) {
+            final Object k = e.getKey();
+            if (k.equals(Direction.OUT) || k.equals(Direction.IN) || k.equals(T.label)) continue;
+            properties.add(k);
+            properties.add(e.getValue());
+        }
+
+        final Edge edge = fromV.addEdge(label, toV, properties.toArray());
+
+        // 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.EdgeAddedEvent vae = new Event.EdgeAddedEvent(eventStrategy.detach(edge));
+            this.callbackRegistry.getCallbacks().forEach(c -> c.accept(vae));
         }
+
+        return IteratorUtils.of(edge);
     }
 
-    /**
-     * Little helper method that will resolve {@link Direction} map keys to a {@link Vertex} which is the currency
-     * of this step. Since this step can accept a {@link Vertex} as the traverser it uses that as a default value
-     * in the case where {@link Direction} is not specified in the {@code Map}. As a result the {@code Map} value
-     * overrides the traverser. Note that if this is a start step then the traverser will contain a
-     * {@link #PLACEHOLDER_VERTEX} which is basically just a dummy to use as a marker where it will be assumed a
-     * {@code Map} argument to the step will have the necessary {@link Vertex} to allow the step to do its work. If
-     * the {@link Direction} contains something other than a {@link Vertex} it will become the {@link T#id} to a
-     * fresh {@link ReferenceVertex}.
+    protected Map onCreateMap(final Traverser.Admin<S> traverser, final Map unresolvedMergeMap, final Map mergeMap) {
+        // no onCreateTraversal - use main mergeMap argument
+        if (onCreateTraversal == null)
+            return mergeMap;
+
+        final Map onCreateMap = materializeMap(traverser, onCreateTraversal);
+        validateMapInput(onCreateMap, false);
+
+        /*
+         * Now we need to merge the two maps - onCreate should inherit traits from mergeMap, and it is not allowed to
+         * override values for any keys.
+         */
+
+        /*
+         * We use the unresolved version here in case onCreateMap uses Merge tokens or Vertex objects for its values.
+         */
+        validateNoOverrides(unresolvedMergeMap, onCreateMap);
+
+        /*
+         * Use the resolved version here so that onCreateMap picks up fully resolved vertex arguments from the main
+         * merge argument and so we don't re-resolve them below.
+         */
+        onCreateMap.putAll(mergeMap);
+
+        /*
+         * Do any final vertex resolution, for example if Merge tokens were used in option(onCreate) but not in the main
+         * merge argument.
+         */
+        resolveVertices(onCreateMap, traverser);
+
+        return onCreateMap;
+    }
+
+    /*
+     * Resolve the argument for Direction.IN/OUT into a proper Vertex.
      */
-    protected Vertex resolveVertex(final Traverser.Admin<S> traverser, final Map<Object, Object> searchCreate,
-                                   final Direction direction) {
-        final Vertex traverserVertex = traverser.get() instanceof Vertex ? (Vertex) traverser.get() : PLACEHOLDER_VERTEX;
-        final Object o = searchCreate != null ? searchCreate.getOrDefault(direction, traverserVertex) : traverserVertex;
-        final Vertex v = o instanceof Vertex ? (Vertex) o : new ReferenceVertex(o);
-        if (v != PLACEHOLDER_VERTEX && v instanceof Attachable) {
-            return tryAttachVertex(v);
-        } else {
-            return v;
+    protected Vertex resolveVertex(final Object arg) {
+        if (arg instanceof Vertex)
+            return tryAttachVertex((Vertex) arg);
+
+        final Iterator<Vertex> it = getGraph().vertices(arg);
+        try {
+            // otherwise use the arg as a vertex id
+            if (!it.hasNext())
+                throw new IllegalArgumentException(
+                    String.format("Vertex id %s could not be found and edge could not be created", arg));
+            return it.next();
+        } finally {
+            CloseableIterator.closeIterator(it);
         }
     }
 
@@ -386,104 +411,16 @@ public class MergeEdgeStep<S> extends FlatMapStep<S, Edge> implements Mutating<E
      * 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 Vertex maybeAttachable) {
-        if (maybeAttachable instanceof Attachable) {
+    protected Vertex tryAttachVertex(final Vertex v) {
+        if (v instanceof Attachable) {
             try {
-                return ((Attachable<Vertex>) maybeAttachable).attach(Attachable.Method.get(this.getTraversal().getGraph().orElse(EmptyGraph.instance())));
+                return ((Attachable<Vertex>) v).attach(Attachable.Method.get(getGraph()));
             } catch (IllegalStateException ise) {
-                throw new IllegalArgumentException(String.format("%s could not be found and edge could not be created", maybeAttachable));
+                throw new IllegalArgumentException(String.format("%s could not be found and edge could not be created", v));
             }
         } else {
-            return maybeAttachable;
-        }
-    }
-
-    /**
-     * 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.
-     */
-    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();
-                return !(k instanceof String);
-            }).findFirst().map(e -> {
-                throw new IllegalArgumentException(String.format(
-                        "option(onMatch) expects keys in Map to be of String - check: %s",
-                        e.getKey()));
-            });
-        } else {
-            m.entrySet().stream().filter(e -> {
-                final Object k = e.getKey();
-                return k != T.id && k != T.label && k != Direction.OUT && k != Direction.IN && !(k instanceof String);
-            }).findFirst().map(e -> {
-                throw new IllegalArgumentException(String.format(
-                        "mergeE() and option(onCreate) expects keys in Map to be of String, T.id, T.label, or any Direction except BOTH - check: %s",
-                        e.getKey()));
-            });
-        }
-
-        if (!ignoreTokens) {
-            m.entrySet().stream().filter(e -> e.getKey() == T.label && !(e.getValue() instanceof String)).findFirst().map(e -> {
-                throw new IllegalArgumentException(String.format(
-                        "mergeE() expects T.label value to be of String - found: %s",
-                        e.getValue().getClass().getSimpleName()));
-            });
+            return v;
         }
     }
 
-    @Override
-    public CallbackRegistry<Event> getMutatingCallbackRegistry() {
-        if (null == callbackRegistry) callbackRegistry = new ListCallbackRegistry<>();
-        return callbackRegistry;
-    }
-
-    @Override
-    public int hashCode() {
-        int result = super.hashCode();
-        if (searchCreateTraversal != null)
-            result ^= searchCreateTraversal.hashCode();
-        if (onCreateTraversal != null)
-            result ^= onCreateTraversal.hashCode();
-        if (onMatchTraversal != null)
-            result ^= onMatchTraversal.hashCode();
-        return result;
-    }
-
-    @Override
-    public void reset() {
-        super.reset();
-        first = true;
-        searchCreateTraversal.reset();
-        if (onCreateTraversal != null) onCreateTraversal.reset();
-        if (onMatchTraversal != null) onMatchTraversal.reset();
-    }
-
-    @Override
-    public Set<TraverserRequirement> getRequirements() {
-        return this.getSelfAndChildRequirements();
-    }
-
-    @Override
-    public String toString() {
-        return StringFactory.stepString(this, searchCreateTraversal, onCreateTraversal, onMatchTraversal);
-    }
-
-    @Override
-    public void setTraversal(final Traversal.Admin<?, ?> parentTraversal) {
-        super.setTraversal(parentTraversal);
-        this.integrateChild(searchCreateTraversal);
-        this.integrateChild(onCreateTraversal);
-        this.integrateChild(onMatchTraversal);
-    }
-
-    @Override
-    public MergeEdgeStep<S> clone() {
-        final MergeEdgeStep<S> clone = (MergeEdgeStep<S>) super.clone();
-        clone.searchCreateTraversal = searchCreateTraversal.clone();
-        clone.onCreateTraversal = onCreateTraversal != null ? onCreateTraversal.clone() : null;
-        clone.onMatchTraversal = onMatchTraversal != null ? onMatchTraversal.clone() : null;
-        return clone;
-    }
 }
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeStep.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeStep.java
new file mode 100644
index 0000000000..c3323023ad
--- /dev/null
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeStep.java
@@ -0,0 +1,299 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.tinkerpop.gremlin.process.traversal.step.map;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+import org.apache.tinkerpop.gremlin.process.traversal.Merge;
+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.process.traversal.lambda.IdentityTraversal;
+import org.apache.tinkerpop.gremlin.process.traversal.step.Mutating;
+import org.apache.tinkerpop.gremlin.process.traversal.step.TraversalOptionParent;
+import org.apache.tinkerpop.gremlin.process.traversal.step.util.Parameters;
+import org.apache.tinkerpop.gremlin.process.traversal.step.util.event.CallbackRegistry;
+import org.apache.tinkerpop.gremlin.process.traversal.step.util.event.Event;
+import org.apache.tinkerpop.gremlin.process.traversal.step.util.event.ListCallbackRegistry;
+import org.apache.tinkerpop.gremlin.process.traversal.traverser.TraverserRequirement;
+import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalUtil;
+import org.apache.tinkerpop.gremlin.structure.Direction;
+import org.apache.tinkerpop.gremlin.structure.Edge;
+import org.apache.tinkerpop.gremlin.structure.Graph;
+import org.apache.tinkerpop.gremlin.structure.T;
+import org.apache.tinkerpop.gremlin.structure.util.StringFactory;
+
+/**
+ * Abstract base class for for the {@code mergeV/E()} implementations.
+ */
+public abstract class MergeStep<S, E, C> extends FlatMapStep<S, E>
+                                   implements Mutating<Event>, TraversalOptionParent<Merge, S, C> {
+
+
+    protected final boolean isStart;
+    protected boolean first = true;
+    protected Traversal.Admin<S, Map> mergeTraversal;
+    protected Traversal.Admin<S, Map> onCreateTraversal = null;
+    protected Traversal.Admin<S, Map<String, ?>> onMatchTraversal = null;
+
+    protected CallbackRegistry<Event> callbackRegistry;
+
+    public MergeStep(final Traversal.Admin traversal, final boolean isStart) {
+        this(traversal, isStart, new IdentityTraversal<>());
+    }
+
+    public MergeStep(final Traversal.Admin traversal, final boolean isStart, final Map mergeMap) {
+        this(traversal, isStart, new ConstantTraversal<>(mergeMap));
+        validate(mergeMap, false);
+    }
+
+    public MergeStep(final Traversal.Admin traversal, final boolean isStart, 
+                     final Traversal.Admin mergeTraversal) {
+        super(traversal);
+        this.isStart = isStart;
+        this.mergeTraversal = integrateChild(mergeTraversal);
+    }
+
+    /**
+     * Gets the traversal that will be used to provide the {@code Map} that will be used to search for elements.
+     * This {@code Map} also will be used as the default data set to be used to create the element if the search is not
+     * successful.
+     */
+    public Traversal.Admin<S, Map> getMergeTraversal() {
+        return mergeTraversal;
+    }
+
+    /**
+     * Gets the traversal that will be used to provide the {@code Map} that will be used to create elements that
+     * do not match the search criteria of {@link #getMergeTraversal()}.
+     */
+    public Traversal.Admin<S, Map> getOnCreateTraversal() {
+        return onCreateTraversal;
+    }
+
+    /**
+     * Gets the traversal that will be used to provide the {@code Map} that will be used to modify elements that
+     * match the search criteria of {@link #getMergeTraversal()}.
+     */
+    public Traversal.Admin<S, Map<String, ?>> getOnMatchTraversal() {
+        return onMatchTraversal;
+    }
+
+    /**
+     * Determines if this is a start step.
+     */
+    public boolean isStart() {
+        return isStart;
+    }
+
+    /**
+     * Determine if this is the first pass through {@link #processNextStart()}.
+     */
+    public boolean isFirst() {
+        return first;
+    }
+
+    public CallbackRegistry<Event> getCallbackRegistry() {
+        return callbackRegistry;
+    }
+
+    @Override
+    public void addChildOption(final Merge token, final Traversal.Admin<S, C> traversalOption) {
+        if (token == Merge.onCreate) {
+            this.onCreateTraversal = this.integrateChild(traversalOption);
+        } else if (token == Merge.onMatch) {
+            this.onMatchTraversal = this.integrateChild(traversalOption);
+        } else {
+            throw new UnsupportedOperationException(String.format("Option %s for Merge is not supported", token.name()));
+        }
+    }
+
+    @Override
+    public <S, C> List<Traversal.Admin<S, C>> getLocalChildren() {
+        final List<Traversal.Admin<S, C>> children = new ArrayList<>();
+        if (mergeTraversal != null) children.add((Traversal.Admin<S, C>) mergeTraversal);
+        if (onMatchTraversal != null) children.add((Traversal.Admin<S, C>) onMatchTraversal);
+        if (onCreateTraversal != null) children.add((Traversal.Admin<S, C>) onCreateTraversal);
+        return children;
+    }
+
+    @Override
+    public void configure(final Object... keyValues) {
+        // this is a Mutating step but property() should not be folded into this step.  The main issue here is that
+        // this method won't know what step called it - property() or with() or something else so it can't make the
+        // choice easily to throw an exception, write the keys/values to parameters, etc. It really is up to the
+        // caller to make sure it is handled properly at this point. this may best be left as a do-nothing method for
+        // now.
+    }
+
+    @Override
+    public Parameters getParameters() {
+        // merge doesn't take fold ups of property() calls. those need to get treated as regular old PropertyStep
+        // instances. not sure if this should support with() though.....none of the other Mutating steps do.
+        return null;
+    }
+
+    @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;
+            generateTraverser(false);
+        }
+        return super.processNextStart();
+    }
+
+    private void generateTraverser(final Object o) {
+        final TraverserGenerator generator = this.getTraversal().getTraverserGenerator();
+        this.addStart(generator.generate(o, (Step) this, 1L));
+    }
+
+    protected Graph getGraph() {
+        return this.getTraversal().getGraph().get();
+    }
+
+    @Override
+    public CallbackRegistry<Event> getMutatingCallbackRegistry() {
+        if (null == callbackRegistry) callbackRegistry = new ListCallbackRegistry<>();
+        return callbackRegistry;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = super.hashCode();
+        if (mergeTraversal != null)
+            result ^= mergeTraversal.hashCode();
+        if (onCreateTraversal != null)
+            result ^= onCreateTraversal.hashCode();
+        if (onMatchTraversal != null)
+            result ^= onMatchTraversal.hashCode();
+        return result;
+    }
+
+    @Override
+    public void reset() {
+        super.reset();
+        first = true;
+        mergeTraversal.reset();
+        if (onCreateTraversal != null) onCreateTraversal.reset();
+        if (onMatchTraversal != null) onMatchTraversal.reset();
+    }
+
+    @Override
+    public Set<TraverserRequirement> getRequirements() {
+        return this.getSelfAndChildRequirements();
+    }
+
+    @Override
+    public String toString() {
+        return StringFactory.stepString(this, mergeTraversal, onCreateTraversal, onMatchTraversal);
+    }
+
+    @Override
+    public void setTraversal(final Traversal.Admin<?, ?> parentTraversal) {
+        super.setTraversal(parentTraversal);
+        this.integrateChild(mergeTraversal);
+        this.integrateChild(onCreateTraversal);
+        this.integrateChild(onMatchTraversal);
+    }
+
+    @Override
+    public MergeStep<S, E, C> clone() {
+        final MergeStep<S, E, C> clone = (MergeStep<S, E, C>) super.clone();
+        clone.mergeTraversal = mergeTraversal.clone();
+        clone.onCreateTraversal = onCreateTraversal != null ? onCreateTraversal.clone() : null;
+        clone.onMatchTraversal = onMatchTraversal != null ? onMatchTraversal.clone() : null;
+        return clone;
+    }
+
+    protected void validate(final Map map, final boolean ignoreTokens) {
+        final Set allowedTokens = getAllowedTokens();
+        validate(map, ignoreTokens, allowedTokens);
+    }
+
+    protected static void validate(final Map map, final boolean ignoreTokens, final Set allowedTokens) {
+        if (null == map) return;
+
+        ((Map<?,?>) map).entrySet().forEach(e -> {
+            final Object k = e.getKey();
+            final Object v = e.getValue();
+
+            if (v == null) {
+                throw new IllegalArgumentException(String.format("merge() does not allow null Map values - check: %s", k));
+            }
+
+            if (ignoreTokens) {
+                if (!(k instanceof String)) {
+                    throw new IllegalArgumentException(String.format("option(onMatch) expects keys in Map to be of String - check: %s", k));
+                }
+            } else {
+                if (!(k instanceof String) && !allowedTokens.contains(k)) {
+                    throw new IllegalArgumentException(String.format(
+                            "merge() and option(onCreate) expects keys in Map to be either String or %s - check: %s",
+                            allowedTokens, k));
+                }
+                if (k == T.label && !(v instanceof String)) {
+                    throw new IllegalArgumentException(String.format("merge() expects T.label value to be of String - found: %s",
+                            v.getClass().getSimpleName()));
+
+                }
+                if (k == Direction.OUT && v instanceof Merge && v != Merge.outV) {
+                    throw new IllegalArgumentException(String.format("Only Merge.outV token may be used for Direction.OUT, found: %s", v));
+                }
+                if (k == Direction.IN && v instanceof Merge && v != Merge.inV) {
+                    throw new IllegalArgumentException(String.format("Only Merge.inV token may be used for Direction.IN, found: %s", v));
+                }
+            }
+        });
+    }
+
+    protected void validateNoOverrides(final Map<?,?> mergeMap, final Map<?,?> onCreateMap) {
+        for (final Map.Entry e : onCreateMap.entrySet()) {
+            final Object k = e.getKey();
+            final Object v = e.getValue();
+            if (mergeMap.containsKey(k) && !Objects.equals(v, mergeMap.get(k))) {
+                throw new IllegalArgumentException(String.format(
+                        "option(onCreate) cannot override values from merge() argument: (%s, %s)", k, v));
+            }
+        }
+    }
+
+    /**
+     * null Map == empty Map
+     */
+    protected Map materializeMap(final Traverser.Admin<S> traverser, Traversal.Admin<S, ?> mapTraversal) {
+        final Map map = (Map) TraversalUtil.apply(traverser, mapTraversal);
+        return map == null ? new LinkedHashMap() : map;
+    }
+
+    @Override
+    protected abstract Iterator<E> flatMap(final Traverser.Admin<S> traverser);
+
+    protected abstract Set getAllowedTokens();
+
+}
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 b7759590d6..bc4e98919a 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
@@ -18,160 +18,86 @@
  */
 package org.apache.tinkerpop.gremlin.process.traversal.step.map;
 
-import org.apache.tinkerpop.gremlin.process.traversal.Merge;
-import org.apache.tinkerpop.gremlin.process.traversal.Step;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
 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.process.traversal.lambda.IdentityTraversal;
-import org.apache.tinkerpop.gremlin.process.traversal.step.Mutating;
-import org.apache.tinkerpop.gremlin.process.traversal.step.TraversalOptionParent;
-import org.apache.tinkerpop.gremlin.process.traversal.step.util.Parameters;
-import org.apache.tinkerpop.gremlin.process.traversal.step.util.event.CallbackRegistry;
+import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal;
 import org.apache.tinkerpop.gremlin.process.traversal.step.util.event.Event;
-import org.apache.tinkerpop.gremlin.process.traversal.step.util.event.ListCallbackRegistry;
 import org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.EventStrategy;
-import org.apache.tinkerpop.gremlin.process.traversal.traverser.TraverserRequirement;
 import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalUtil;
-import org.apache.tinkerpop.gremlin.structure.Direction;
-import org.apache.tinkerpop.gremlin.structure.Edge;
 import org.apache.tinkerpop.gremlin.structure.Graph;
 import org.apache.tinkerpop.gremlin.structure.Property;
 import org.apache.tinkerpop.gremlin.structure.T;
 import org.apache.tinkerpop.gremlin.structure.Vertex;
-import org.apache.tinkerpop.gremlin.structure.VertexProperty;
-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;
-import java.util.Set;
-import java.util.stream.Stream;
-
 /**
  * Implementation for the {@code mergeV()} step covering both the start step version and the one used mid-traversal.
  */
-public class MergeVertexStep<S> extends FlatMapStep<S, Vertex> implements Mutating<Event>,
-        TraversalOptionParent<Merge, S, Vertex> {
-
-    private final boolean isStart;
-    private boolean first = true;
-    private Traversal.Admin<S,Map<Object, Object>> searchCreateTraversal;
-    private Traversal.Admin<S, Map<Object, Object>> onCreateTraversal = null;
-    private Traversal.Admin<S, Map<String, Object>> onMatchTraversal = null;
+public class MergeVertexStep<S> extends MergeStep<S, Vertex, Map> {
 
-    protected CallbackRegistry<Event> callbackRegistry;
+    private static final Set allowedTokens = new LinkedHashSet(Arrays.asList(T.id, T.label));
 
-    public MergeVertexStep(final Traversal.Admin traversal, final boolean isStart) {
-        this(traversal, isStart, new IdentityTraversal());
+    public static void validateMapInput(final Map map, final boolean ignoreTokens) {
+        MergeStep.validate(map, ignoreTokens, allowedTokens);
     }
 
-    public MergeVertexStep(final Traversal.Admin traversal, final boolean isStart, final Map<Object, Object> searchCreate) {
-        this(traversal, isStart, new ConstantTraversal<>(searchCreate));
-    }
 
-    public MergeVertexStep(final Traversal.Admin traversal, final boolean isStart, final Traversal.Admin<S,Map<Object, Object>> searchCreateTraversal) {
-        super(traversal);
-        this.isStart = isStart;
-        this.searchCreateTraversal = integrateChild(searchCreateTraversal);
-    }
-
-    /**
-     * Gets the traversal that will be used to provide the {@code Map} that will be used to search for vertices.
-     * This {@code Map} also will be used as the default data set to be used to create a vertex if the search is not
-     * successful.
-     */
-    public Traversal.Admin<S, Map<Object, Object>> getSearchCreateTraversal() {
-        return searchCreateTraversal;
-    }
-
-    /**
-     * Gets the traversal that will be used to provide the {@code Map} that will be the override to the one provided
-     * by the {@link #getSearchCreateTraversal()} for vertex creation events.
-     */
-    public Traversal.Admin<S, Map<Object, Object>> getOnCreateTraversal() {
-        return onCreateTraversal;
-    }
-
-    /**
-     * Gets the traversal that will be used to provide the {@code Map} that will be used to modify vertices that
-     * match the search criteria of {@link #getSearchCreateTraversal()}.
-     */
-    public Traversal.Admin<S, Map<String, Object>> getOnMatchTraversal() {
-        return onMatchTraversal;
-    }
-
-    /**
-     * Determines if this is a start step.
-     */
-    public boolean isStart() {
-        return isStart;
+    public MergeVertexStep(final Traversal.Admin traversal, final boolean isStart) {
+        super(traversal, isStart);
     }
 
-    /**
-     * Determine if this is the first pass through {@link #processNextStart()}.
-     */
-    public boolean isFirst() {
-        return first;
+    public MergeVertexStep(final Traversal.Admin traversal, final boolean isStart, final Map merge) {
+        super(traversal, isStart, merge);
     }
 
-    public CallbackRegistry<Event> getCallbackRegistry() {
-        return callbackRegistry;
+    public MergeVertexStep(final Traversal.Admin traversal, final boolean isStart, final Traversal.Admin<S,Map> mergeTraversal) {
+        super(traversal, isStart, mergeTraversal);
     }
 
     @Override
-    public void addChildOption(final Merge token, final Traversal.Admin<S, Vertex> traversalOption) {
-        if (token == Merge.onCreate) {
-            this.onCreateTraversal = this.integrateChild(traversalOption);
-        } else if (token == Merge.onMatch) {
-            this.onMatchTraversal = this.integrateChild(traversalOption);
-        } else {
-            throw new UnsupportedOperationException(String.format("Option %s for Merge is not supported", token.name()));
-        }
+    public MergeVertexStep<S> clone() {
+        return (MergeVertexStep<S>) super.clone();
     }
 
     @Override
-    public <S, E> List<Traversal.Admin<S, E>> getLocalChildren() {
-        final List<Traversal.Admin<S, E>> children = new ArrayList<>();
-        if (searchCreateTraversal != null) children.add((Traversal.Admin<S, E>) searchCreateTraversal);
-        if (onMatchTraversal != null) children.add((Traversal.Admin<S, E>) onMatchTraversal);
-        if (onCreateTraversal != null) children.add((Traversal.Admin<S, E>) onCreateTraversal);
-        return children;
+    protected Set getAllowedTokens() {
+        return allowedTokens;
     }
 
-    @Override
-    public void configure(final Object... keyValues) {
-        // this is a Mutating step but property() should not be folded into this step.  The main issue here is that
-        // this method won't know what step called it - property() or with() or something else so it can't make the
-        // choice easily to throw an exception, write the keys/values to parameters, etc. It really is up to the
-        // caller to make sure it is handled properly at this point. this may best be left as a do-nothing method for
-        // now.
-    }
+    public static Iterator<Vertex> searchVertices(final Graph graph, final Map search) {
+        if (search == null)
+            return Collections.emptyIterator();
 
-    @Override
-    public Parameters getParameters() {
-        // merge doesn't take fold ups of property() calls. those need to get treated as regular old PropertyStep
-        // instances. not sure if this should support with() though.....none of the other Mutating steps do.
-        return null;
-    }
+        final Object id = search.get(T.id);
+        final String label = (String) search.get(T.label);
 
-    @Override
-    protected Traverser.Admin<Vertex> processNextStart() {
-        // when it's a start step a traverser needs to be created to kick off the traversal.
-        if (isStart && first) {
-            first = false;
-            generateTraverser(false);
+        GraphTraversal t;
+        if (id != null)
+            t = graph.traversal().V(id);
+        else
+            t = graph.traversal().V();
+        if (label != null)
+            t = t.hasLabel(label);
+
+        // add property constraints
+        for (final Map.Entry e : ((Map<?,?>) search).entrySet()) {
+            final Object k = e.getKey();
+            if (!(k instanceof String)) continue;
+            t = t.has((String) k, e.getValue());
         }
-        return super.processNextStart();
-    }
 
-    private void generateTraverser(final Object o) {
-        final TraverserGenerator generator = this.getTraversal().getTraverserGenerator();
-        this.addStart(generator.generate(o, (Step) this, 1L));
+        // this should auto-close the underlying traversal
+        return t;
     }
 
     /**
@@ -180,54 +106,31 @@ public class MergeVertexStep<S> extends FlatMapStep<S, Vertex> implements Mutati
      * matching the list of vertices. This implementation is only optimized for the {@link T#id} so any other usage
      * will simply be in-memory filtering which could be slow.
      */
-    protected Stream<Vertex> createSearchStream(final Map<Object,Object> search) {
-        final Graph graph = this.getTraversal().getGraph().get();
-
-        Stream<Vertex> stream;
-        // prioritize lookup by 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());
-
-        // in-memory filter is not going to be nice here. it will be up to graphs to optimize this step as they do
-        // for other Mutation steps
-        stream = stream.filter(v -> {
-            // try to match on all search criteria skipping T.id as it was handled above
-            return search.entrySet().stream().filter(kv -> kv.getKey() != T.id).allMatch(kv -> {
-                if (kv.getKey() == T.label) {
-                    return v.label().equals(kv.getValue());
-                } else {
-                    final VertexProperty<Object> vp = v.property(kv.getKey().toString());
-                    return vp.isPresent() && kv.getValue().equals(vp.value());
-                }
-            });
-        });
-
-        return stream;
+    protected Iterator<Vertex> searchVertices(final Map search) {
+        return searchVertices(getGraph(), search);
     }
 
     @Override
     protected Iterator<Vertex> flatMap(final Traverser.Admin<S> traverser) {
-        final Map<Object,Object> searchCreate = TraversalUtil.apply(traverser, searchCreateTraversal);
-        validateMapInput(searchCreate, false);
+        final Graph graph = getGraph();
 
-        Stream<Vertex> stream = createSearchStream(searchCreate);
-        stream = stream.map(v -> {
-            // if no onMatch is defined then there is no update - return the vertex unchanged
-            if (null == onMatchTraversal) return v;
+        final Map mergeMap = materializeMap(traverser, mergeTraversal);
+        validateMapInput(mergeMap, false);
 
-            // if this was a start step the traverser is initialized with Boolean/false, so override that with
-            // the matched Vertex so that the option() traversal can operate on it properly
-            if (isStart) traverser.set((S) v);
+        Iterator<Vertex> vertices = searchVertices(mergeMap);
 
-            // assume good input from GraphTraversal - folks might drop in a T here even though it is immutable
-            final Map<String, Object> onMatchMap = TraversalUtil.apply(traverser, onMatchTraversal);
-            validateMapInput(onMatchMap, true);
+        if (onMatchTraversal != null) {
+
+            vertices = IteratorUtils.peek(vertices, v -> {
+
+                // if this was a start step the traverser is initialized with Boolean/false, so override that with
+                // the matched Vertex so that the option() traversal can operate on it properly
+                if (isStart) traverser.set((S) v);
+
+                // assume good input from GraphTraversal - folks might drop in a T here even though it is immutable
+                final Map<String, Object> onMatchMap = materializeMap(traverser, onMatchTraversal);
+                validateMapInput(onMatchMap, true);
 
-            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
@@ -240,152 +143,56 @@ public class MergeVertexStep<S> extends FlatMapStep<S, Vertex> implements Mutati
                     }
 
                     // 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;
-        });
+        }
 
-        // if the stream has something then there is a match (possibly updated) and is returned, otherwise a new
-        // vertex is created
-        final Iterator<Vertex> vertices = stream.iterator();
+        /*
+         * Search produced results, and onMatch action will be triggered.
+         */
         if (vertices.hasNext()) {
             return vertices;
-        } else {
-            final Vertex vertex;
-
-            // if there is an onCreateTraversal then the search criteria is ignored for the creation as it is provided
-            // by way of the traversal which will return the Map
-            final boolean useOnCreate = onCreateTraversal != null;
-            final Map<Object,Object> onCreateMap = useOnCreate ? TraversalUtil.apply(traverser, onCreateTraversal) : searchCreate;
-
-            // searchCreate should have already been validated so only do it if it is overridden
-            if (useOnCreate) validateMapInput(onCreateMap, false);
-
-            // if onCreate is null then it's a do nothing
-            final List<Object> keyValues = new ArrayList<>();
-            if (onCreateMap != null) {
-                for (Map.Entry<Object, Object> entry : onCreateMap.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));
-                }
-
-                return IteratorUtils.of(vertex);
-            } else {
-                return Collections.emptyIterator();
-            }
         }
-    }
 
-    /**
-     * 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.
-     */
-    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();
-                return !(k instanceof String);
-            }).findFirst().map(e -> {
-                throw new IllegalArgumentException(String.format(
-                        "option(onMatch) expects keys in Map to be of String - check: %s",
-                        e.getKey()));
-            });
-        } else {
-            m.entrySet().stream().filter(e -> {
-                final Object k = e.getKey();
-                return k != T.id && k != T.label && !(k instanceof String);
-            }).findFirst().map(e -> {
-                throw new IllegalArgumentException(String.format(
-                        "mergeV() and option(onCreate) expects keys in Map to be of String, T.id, T.label - check: %s",
-                        e.getKey()));
-            });
-        }
+        final Map<?,?> onCreateMap = onCreateMap(traverser, mergeMap);
+
+        final Object[] flatArgs = onCreateMap.entrySet().stream()
+                .flatMap(e -> Stream.of(e.getKey(), e.getValue())).collect(Collectors.toList()).toArray();
 
-        if (!ignoreTokens) {
-            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()));
-                }
-            }
+        final Vertex vertex = graph.addVertex(flatArgs);
+
+        // 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));
         }
-    }
 
-    @Override
-    public CallbackRegistry<Event> getMutatingCallbackRegistry() {
-        if (null == callbackRegistry) callbackRegistry = new ListCallbackRegistry<>();
-        return callbackRegistry;
+        return IteratorUtils.of(vertex);
     }
 
-    @Override
-    public int hashCode() {
-        int result = super.hashCode();
-        if (searchCreateTraversal != null)
-            result ^= searchCreateTraversal.hashCode();
-        if (onCreateTraversal != null)
-            result ^= onCreateTraversal.hashCode();
-        if (onMatchTraversal != null)
-            result ^= onMatchTraversal.hashCode();
-        return result;
-    }
+    protected Map onCreateMap(final Traverser.Admin<S> traverser, final Map mergeMap) {
+        // no onCreateTraversal - use main mergeMap argument
+        if (onCreateTraversal == null)
+            return mergeMap;
 
-    @Override
-    public void reset() {
-        super.reset();
-        first = true;
-        searchCreateTraversal.reset();
-        if (onCreateTraversal != null) onCreateTraversal.reset();
-        if (onMatchTraversal != null) onMatchTraversal.reset();
-    }
+        final Map onCreateMap = materializeMap(traverser, onCreateTraversal);
+        // null result from onCreateTraversal - use main mergeMap argument
+        if (onCreateMap == null)
+            return mergeMap;
 
-    @Override
-    public Set<TraverserRequirement> getRequirements() {
-        return this.getSelfAndChildRequirements();
-    }
+        validateMapInput(onCreateMap, false);
 
-    @Override
-    public String toString() {
-        return StringFactory.stepString(this, searchCreateTraversal, onCreateTraversal, onMatchTraversal);
-    }
+        /*
+         * Now we need to merge the two maps - onCreate should inherit traits from mergeMap, and it is not allowed to
+         * override values for any keys.
+         */
+        validateNoOverrides(mergeMap, onCreateMap);
+        onCreateMap.putAll(mergeMap);
 
-    @Override
-    public void setTraversal(final Traversal.Admin<?, ?> parentTraversal) {
-        super.setTraversal(parentTraversal);
-        this.integrateChild(searchCreateTraversal);
-        this.integrateChild(onCreateTraversal);
-        this.integrateChild(onMatchTraversal);
+        return onCreateMap;
     }
 
-    @Override
-    public MergeVertexStep<S> clone() {
-        final MergeVertexStep<S> clone = (MergeVertexStep<S>) super.clone();
-        clone.searchCreateTraversal = searchCreateTraversal.clone();
-        clone.onCreateTraversal = onCreateTraversal != null ? onCreateTraversal.clone() : null;
-        clone.onMatchTraversal = onMatchTraversal != null ? onMatchTraversal.clone() : null;
-        return clone;
-    }
 }
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/util/iterator/IteratorUtils.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/util/iterator/IteratorUtils.java
index 6e4a2939d4..11dfa83950 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/util/iterator/IteratorUtils.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/util/iterator/IteratorUtils.java
@@ -30,6 +30,7 @@ import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.Spliterator;
 import java.util.Spliterators;
@@ -49,20 +50,21 @@ public final class IteratorUtils {
     private IteratorUtils() {
     }
 
-    public static final <S> Iterator<S> of(final S a) {
+    public static <S> Iterator<S> of(final S a) {
         return new SingleIterator<>(a);
     }
 
-    public static final <S> Iterator<S> of(final S a, S b) {
+    public static <S> Iterator<S> of(final S a, S b) {
         return new DoubleIterator<>(a, b);
     }
 
     ///////////////
 
-    public static final <S extends Collection<T>, T> S fill(final Iterator<T> iterator, final S collection) {
+    public static <S extends Collection<T>, T> S fill(final Iterator<T> iterator, final S collection) {
         while (iterator.hasNext()) {
             collection.add(iterator.next());
         }
+        CloseableIterator.closeIterator(iterator);
         return collection;
     }
 
@@ -70,11 +72,13 @@ public final class IteratorUtils {
         while (iterator.hasNext()) {
             iterator.next();
         }
+        CloseableIterator.closeIterator(iterator);
     }
 
-    public static final long count(final Iterator iterator) {
+    public static long count(final Iterator iterator) {
         long ix = 0;
         for (; iterator.hasNext(); ++ix) iterator.next();
+        CloseableIterator.closeIterator(iterator);
         return ix;
     }
 
@@ -97,7 +101,7 @@ public final class IteratorUtils {
     }
 
     public static <S> Iterator<S> limit(final Iterator<S> iterator, final int limit) {
-        return new Iterator<S>() {
+        return new CloseableIterator<S>() {
             private int count = 0;
 
             @Override
@@ -116,39 +120,64 @@ public final class IteratorUtils {
                     throw FastNoSuchElementException.instance();
                 return iterator.next();
             }
+
+            @Override
+            public void close() {
+                CloseableIterator.closeIterator(iterator);
+            }
         };
     }
 
     ///////////////////
 
     public static <T> boolean allMatch(final Iterator<T> iterator, final Predicate<T> predicate) {
-        while (iterator.hasNext()) {
-            if (!predicate.test(iterator.next())) {
-                return false;
+        try {
+            while (iterator.hasNext()) {
+                if (!predicate.test(iterator.next())) {
+                    return false;
+                }
             }
+            return true;
+        } finally {
+            CloseableIterator.closeIterator(iterator);
         }
-
-        return true;
     }
 
     public static <T> boolean anyMatch(final Iterator<T> iterator, final Predicate<T> predicate) {
-        while (iterator.hasNext()) {
-            if (predicate.test(iterator.next())) {
-                return true;
+        try {
+            while (iterator.hasNext()) {
+                if (predicate.test(iterator.next())) {
+                    return true;
+                }
             }
+            return false;
+        } finally {
+            CloseableIterator.closeIterator(iterator);
         }
-
-        return false;
     }
 
     public static <T> boolean noneMatch(final Iterator<T> iterator, final Predicate<T> predicate) {
-        while (iterator.hasNext()) {
-            if (predicate.test(iterator.next())) {
-                return false;
+        try {
+            while (iterator.hasNext()) {
+                if (predicate.test(iterator.next())) {
+                    return false;
+                }
             }
+            return true;
+        } finally {
+            CloseableIterator.closeIterator(iterator);
         }
+    }
 
-        return true;
+    public static <T> Optional<T> findFirst(final Iterator<T> iterator) {
+        try {
+            if (iterator.hasNext()) {
+                return Optional.ofNullable(iterator.next());
+            }
+            return Optional.empty();
+        } finally {
+            CloseableIterator.closeIterator(iterator);
+        }
     }
 
     ///////////////////
@@ -163,6 +192,7 @@ public final class IteratorUtils {
             final S obj = iterator.next();
             map.put(key.apply(obj), value.apply(obj));
         }
+        CloseableIterator.closeIterator(iterator);
         return map;
     }
 
@@ -172,6 +202,7 @@ public final class IteratorUtils {
             final S obj = iterator.next();
             map.computeIfAbsent(groupBy.apply(obj), k -> new ArrayList<>()).add(obj);
         }
+        CloseableIterator.closeIterator(iterator);
         return map;
     }
 
@@ -180,7 +211,7 @@ public final class IteratorUtils {
         while (iterator.hasNext()) {
             result = accumulator.apply(result, iterator.next());
         }
-
+        CloseableIterator.closeIterator(iterator);
         return result;
     }
 
@@ -193,7 +224,7 @@ public final class IteratorUtils {
         while (iterator.hasNext()) {
             result = accumulator.apply(result, iterator.next());
         }
-
+        CloseableIterator.closeIterator(iterator);
         return result;
     }
 
@@ -203,8 +234,8 @@ public final class IteratorUtils {
 
     ///////////////
 
-    public static final <S> Iterator<S> consume(final Iterator<S> iterator, final Consumer<S> consumer) {
-        return new Iterator<S>() {
+    public static <S> Iterator<S> consume(final Iterator<S> iterator, final Consumer<S> consumer) {
+        return new CloseableIterator<S>() {
             @Override
             public boolean hasNext() {
                 return iterator.hasNext();
@@ -221,18 +252,23 @@ public final class IteratorUtils {
                 consumer.accept(s);
                 return s;
             }
+
+            @Override
+            public void close() {
+                CloseableIterator.closeIterator(iterator);
+            }
         };
     }
 
-    public static final <S> Iterable<S> consume(final Iterable<S> iterable, final Consumer<S> consumer) {
+    public static <S> Iterable<S> consume(final Iterable<S> iterable, final Consumer<S> consumer) {
         return () -> IteratorUtils.consume(iterable.iterator(), consumer);
     }
 
 
     ///////////////
 
-    public static final <S, E> Iterator<E> map(final Iterator<S> iterator, final Function<S, E> function) {
-        return new Iterator<E>() {
+    public static <S, E> Iterator<E> map(final Iterator<S> iterator, final Function<S, E> function) {
+        return new CloseableIterator<E>() {
             @Override
             public boolean hasNext() {
                 return iterator.hasNext();
@@ -247,10 +283,15 @@ public final class IteratorUtils {
             public E next() {
                 return function.apply(iterator.next());
             }
+
+            @Override
+            public void close() {
+                CloseableIterator.closeIterator(iterator);
+            }
         };
     }
 
-    public static final <S, E> Iterable<E> map(final Iterable<S> iterable, final Function<S, E> function) {
+    public static <S, E> Iterable<E> map(final Iterable<S> iterable, final Function<S, E> function) {
         return () -> IteratorUtils.map(iterable.iterator(), function);
     }
 
@@ -260,10 +301,40 @@ public final class IteratorUtils {
 
     ///////////////
 
-    public static final <S> Iterator<S> filter(final Iterator<S> iterator, final Predicate<S> predicate) {
+    public static <S> Iterator<S> peek(final Iterator<S> iterator, final Consumer<S> function) {
+        return new CloseableIterator<S>() {
+            @Override
+            public boolean hasNext() {
+                return iterator.hasNext();
+            }
+
+            @Override
+            public void remove() {
+                iterator.remove();
+            }
 
+            @Override
+            public S next() {
+                final S next = iterator.next();
+                function.accept(next);
+                return next;
+            }
+
+            @Override
+            public void close() {
+                CloseableIterator.closeIterator(iterator);
+            }
+        };
+    }
+
+    public static <S> Iterable<S> peek(final Iterable<S> iterable, final Consumer<S> function) {
+        return () -> IteratorUtils.peek(iterable.iterator(), function);
+    }
 
-        return new Iterator<S>() {
+    ///////////////
+
+    public static <S> Iterator<S> filter(final Iterator<S> iterator, final Predicate<S> predicate) {
+        return new CloseableIterator<S>() {
             S nextResult = null;
 
             @Override
@@ -298,7 +369,12 @@ public final class IteratorUtils {
                 }
             }
 
-            private final void advance() {
+            @Override
+            public void close() {
+                CloseableIterator.closeIterator(iterator);
+            }
+
+            private void advance() {
                 this.nextResult = null;
                 while (iterator.hasNext()) {
                     final S s = iterator.next();
@@ -311,14 +387,14 @@ public final class IteratorUtils {
         };
     }
 
-    public static final <S> Iterable<S> filter(final Iterable<S> iterable, final Predicate<S> predicate) {
+    public static <S> Iterable<S> filter(final Iterable<S> iterable, final Predicate<S> predicate) {
         return () -> IteratorUtils.filter(iterable.iterator(), predicate);
     }
 
     ///////////////////
 
-    public static final <S, E> Iterator<E> flatMap(final Iterator<S> iterator, final Function<S, Iterator<E>> function) {
-        return new Iterator<E>() {
+    public static <S, E> Iterator<E> flatMap(final Iterator<S> iterator, final Function<S, Iterator<E>> function) {
+        return new CloseableIterator<E>() {
 
             private Iterator<E> currentIterator = Collections.emptyIterator();
 
@@ -348,13 +424,18 @@ public final class IteratorUtils {
                 else
                     throw FastNoSuchElementException.instance();
             }
+
+            @Override
+            public void close() {
+                CloseableIterator.closeIterator(iterator);
+            }
         };
     }
 
 
     ///////////////////
 
-    public static final <S> Iterator<S> concat(final Iterator<S>... iterators) {
+    public static <S> Iterator<S> concat(final Iterator<S>... iterators) {
         final MultiIterator<S> iterator = new MultiIterator<>();
         for (final Iterator<S> itty : iterators) {
             iterator.addIterator(itty);
@@ -400,7 +481,7 @@ public final class IteratorUtils {
     }
 
     public static <T> Iterator<T> noRemove(final Iterator<T> iterator) {
-        return new Iterator<T>() {
+        return new CloseableIterator<T>() {
             @Override
             public boolean hasNext() {
                 return iterator.hasNext();
@@ -415,11 +496,16 @@ public final class IteratorUtils {
             public T next() {
                 return iterator.next();
             }
+
+            @Override
+            public void close() {
+                CloseableIterator.closeIterator(iterator);
+            }
         };
     }
 
     public static <T> Iterator<T> removeOnNext(final Iterator<T> iterator) {
-        return new Iterator<T>() {
+        return new CloseableIterator<T>() {
             @Override
             public boolean hasNext() {
                 return iterator.hasNext();
@@ -436,6 +522,11 @@ public final class IteratorUtils {
                 iterator.remove();
                 return object;
             }
+
+            @Override
+            public void close() {
+                CloseableIterator.closeIterator(iterator);
+            }
         };
     }
 }
diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/CommonSteps.cs b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/CommonSteps.cs
index a63b8d9a2f..60c2d03ad5 100644
--- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/CommonSteps.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/CommonSteps.cs
@@ -60,6 +60,7 @@ namespace Gremlin.Net.IntegrationTest.Gherkin
                 {@"vp\[(.+)\]", ToVertexProperty},
                 {@"d\[(.*)\]\.([bsilfdmn])", ToNumber},
                 {@"D\[(.+)\]", ToDirection},
+                {@"M\[(.+)\]", ToMerge},
                 {@"v\[(.+)\]", ToVertex},
                 {@"v\[(.+)\]\.id", (x, graphName) => ToVertex(x, graphName).Id},
                 {@"v\[(.+)\]\.sid", (x, graphName) => ToVertex(x, graphName).Id!.ToString()},
@@ -368,6 +369,11 @@ namespace Gremlin.Net.IntegrationTest.Gherkin
             return Direction.GetByValue(enumName);
         }
 
+        private static object ToMerge(string enumName, string graphName)
+        {
+            return Merge.GetByValue(enumName);
+        }
+
         private static object ToNumber(string stringNumber, string graphName)
         {
             return NumericParsers[stringNumber[stringNumber.Length - 1]](
diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/index.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/index.js
index 71e60dfc4c..524bfe16fb 100644
--- a/gremlin-javascript/src/main/javascript/gremlin-javascript/index.js
+++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/index.js
@@ -67,6 +67,7 @@ module.exports = {
     cardinality: t.cardinality,
     column: t.column,
     direction: t.direction,
+    merge: t.merge,
     operator: t.operator,
     order: t.order,
     pick: t.pick,
diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/feature-steps.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/feature-steps.js
index 9dfae43f79..dbfa660ec4 100644
--- a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/feature-steps.js
+++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/feature-steps.js
@@ -40,6 +40,7 @@ const __ = graphTraversalModule.statics;
 const t = traversalModule.t;
 const P = traversalModule.P;
 const direction = traversalModule.direction;
+const merge = traversalModule.merge;
 
 // Determines whether the feature maps (m[]), are deserialized as objects (true) or maps (false).
 // Use false for GraphSON3.
@@ -61,7 +62,8 @@ const parsers = [
   [ 'm\\[(.+)\\]', toMap ],
   [ 'c\\[(.+)\\]', toLambda ],
   [ 't\\[(.+)\\]', toT ],
-  [ 'D\\[(.+)\\]', toDirection ]
+  [ 'D\\[(.+)\\]', toDirection ],
+  [ 'M\\[(.+)\\]', toMerge ]
 ].map(x => [ new RegExp('^' + x[0] + '$'), x[1] ]);
 
 const ignoreReason = {
@@ -357,6 +359,10 @@ function toDirection(value) {
     return direction[value.toLowerCase()];
 }
 
+function toDirection(value) {
+  return merge[value.toLowerCase()];
+}
+
 function toArray(stringList) {
   if (stringList === '') {
     return new Array(0);
diff --git a/gremlin-language/src/main/antlr4/Gremlin.g4 b/gremlin-language/src/main/antlr4/Gremlin.g4
index 7d9893e014..deff18b36c 100644
--- a/gremlin-language/src/main/antlr4/Gremlin.g4
+++ b/gremlin-language/src/main/antlr4/Gremlin.g4
@@ -896,6 +896,8 @@ traversalToken
 traversalMerge
     : 'onCreate' | 'Merge.onCreate'
     | 'onMatch' | 'Merge.onMatch'
+    | 'outV' | 'Merge.outV'
+    | 'inV' | 'Merge.inV'
     ;
 
 traversalOrder
@@ -1381,6 +1383,7 @@ genericLiteral
 	| traversalToken
 	| traversalCardinality
 	| traversalDirection
+    | traversalMerge
 	| traversalPick
 	| structureVertex
 	| genericLiteralCollection
diff --git a/gremlin-python/src/main/python/radish/feature_steps.py b/gremlin-python/src/main/python/radish/feature_steps.py
index 4227f56366..9ad453bac7 100644
--- a/gremlin-python/src/main/python/radish/feature_steps.py
+++ b/gremlin-python/src/main/python/radish/feature_steps.py
@@ -24,7 +24,7 @@ from gremlin_python.structure.graph import Path, Vertex
 from gremlin_python.process.anonymous_traversal import traversal
 from gremlin_python.process.graph_traversal import __
 from gremlin_python.process.traversal import Barrier, Cardinality, P, TextP, Pop, Scope, Column, Order, Direction, T, \
-    Pick, Operator, IO, WithOptions
+    Pick, Operator, IO, WithOptions, Merge
 from radish import given, when, then, world
 from hamcrest import *
 
@@ -238,6 +238,8 @@ def _convert(val, ctx):
         return T[val[2:-1]]
     elif isinstance(val, str) and re.match(r"^D\[.*\]$", val):  # parse instance of Direction enum
         return direction[__alias_direction(val[2:-1])]
+    elif isinstance(val, str) and re.match(r"^M\[.*\]$", val):  # parse instance of Merge enum
+        return Merge[val[2:-1]]
     elif isinstance(val, str) and re.match(r"^null$", val):  # parse null to None
         return None
     elif isinstance(val, str) and re.match(r"^true$", val):  # parse to boolean
diff --git a/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/features/StepDefinition.java b/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/features/StepDefinition.java
index 24635eeaa6..d7ad76ad70 100644
--- a/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/features/StepDefinition.java
+++ b/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/features/StepDefinition.java
@@ -32,6 +32,7 @@ import org.antlr.v4.runtime.CommonTokenStream;
 import org.apache.tinkerpop.gremlin.language.grammar.GremlinAntlrToJava;
 import org.apache.tinkerpop.gremlin.language.grammar.GremlinLexer;
 import org.apache.tinkerpop.gremlin.language.grammar.GremlinParser;
+import org.apache.tinkerpop.gremlin.process.traversal.Merge;
 import org.apache.tinkerpop.gremlin.process.traversal.Translator;
 import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
 import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal;
@@ -140,6 +141,7 @@ public final class StepDefinition {
         add(Pair.with(Pattern.compile("e\\[(.+)\\]\\.sid"), s -> world.convertIdToScript(getEdgeId(g, s), Edge.class)));
         add(Pair.with(Pattern.compile("t\\[(.*)\\]"), s -> String.format("T.%s", s)));
         add(Pair.with(Pattern.compile("D\\[(.*)\\]"), s -> String.format("Direction.%s", s)));
+        add(Pair.with(Pattern.compile("M\\[(.*)\\]"), s -> String.format("Merge.%s", s)));
 
         // the following force ignore conditions as they cannot be parsed by the grammar at this time. the grammar will
         // need to be modified to handle them or perhaps these tests stay relegated to the JVM in some way for certain
@@ -216,6 +218,7 @@ public final class StepDefinition {
 
         add(Pair.with(Pattern.compile("t\\[(.*)\\]"), T::valueOf));
         add(Pair.with(Pattern.compile("D\\[(.*)\\]"), Direction::valueOf));
+        add(Pair.with(Pattern.compile("M\\[(.*)\\]"), Merge::valueOf));
 
         add(Pair.with(Pattern.compile("c\\[(.*)\\]"), s -> {
             throw new AssumptionViolatedException("This test uses a lambda as a parameter which is not supported by gremlin-language");
@@ -277,8 +280,13 @@ public final class StepDefinition {
 
     @Given("the traversal of")
     public void theTraversalOf(final String docString) {
-        final String gremlin = tryUpdateDataFilePath(docString);
-        traversal = parseGremlin(applyParameters(gremlin));
+        try {
+            final String gremlin = tryUpdateDataFilePath(docString);
+            traversal = parseGremlin(applyParameters(gremlin));
+        } catch (Exception ex) {
+            ex.printStackTrace();
+            error = ex;
+        }
     }
 
     @When("iterated to list")
@@ -290,6 +298,15 @@ public final class StepDefinition {
         }
     }
 
+    @When("debug iterated to list")
+    public void debugIteratedToList() {
+        try {
+            result = traversal.toList();
+        } catch (Exception ex) {
+            error = ex;
+        }
+    }
+
     @When("iterated next")
     public void iteratedNext() {
         try {
@@ -357,6 +374,13 @@ public final class StepDefinition {
         assertEquals(count.longValue(), ((GraphTraversal) parseGremlin(applyParameters(gremlin))).count().next());
     }
 
+    @Then("debug the graph should return {int} for count of {string}")
+    public void debugTheGraphShouldReturnForCountOf(final Integer count, final String gremlin) {
+        assertThatNoErrorWasThrown();
+
+        assertEquals(count.longValue(), ((GraphTraversal) parseGremlin(applyParameters(gremlin))).count().next());
+    }
+
     @Then("the result should be empty")
     public void theResultShouldBeEmpty() {
         assertThatNoErrorWasThrown();
diff --git a/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/process/ProcessLimitedStandardSuite.java b/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/process/ProcessLimitedStandardSuite.java
index c24f69bf8c..2b32695ff4 100644
--- a/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/process/ProcessLimitedStandardSuite.java
+++ b/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/process/ProcessLimitedStandardSuite.java
@@ -26,6 +26,8 @@ import org.apache.tinkerpop.gremlin.process.traversal.step.ComplexTest;
 import org.apache.tinkerpop.gremlin.process.traversal.step.OrderabilityTest;
 import org.apache.tinkerpop.gremlin.process.traversal.step.TernaryBooleanLogicsTest;
 import org.apache.tinkerpop.gremlin.process.traversal.step.map.MatchTest;
+import org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest;
+import org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeVertexTest;
 import org.apache.tinkerpop.gremlin.process.traversal.step.map.ProfileTest;
 import org.apache.tinkerpop.gremlin.process.traversal.step.map.WriteTest;
 import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.ExplainTest;
@@ -59,31 +61,34 @@ public class ProcessLimitedStandardSuite extends AbstractGremlinSuite {
      */
     private static final Class<?>[] allTests = new Class<?>[]{
 
-            MatchTest.CountMatchTraversals.class,
-            MatchTest.GreedyMatchTraversals.class,
-            ProfileTest.Traversals.class,
-            WriteTest.Traversals.class,
-            ExplainTest.Traversals.class,
-            SideEffectTest.Traversals.class,
-            SubgraphTest.Traversals.class,
-            TreeTest.Traversals.class,
+            // MergeVertexTest.Traversals.class,
+            MergeEdgeTest.Traversals.class
 
-            // compliance
-            ComplexTest.Traversals.class,
-            CoreTraversalTest.class,
-            TraversalInterruptionTest.class,
-
-            // creations
-            TranslationStrategyProcessTest.class,
-
-            // decorations
-            ElementIdStrategyProcessTest.class,
-            EventStrategyProcessTest.class,
-            PartitionStrategyProcessTest.class,
-
-            // optimizations
-            IncidentToAdjacentStrategyProcessTest.class,
-            EarlyLimitStrategyProcessTest.class,
+            // MatchTest.CountMatchTraversals.class,
+            // MatchTest.GreedyMatchTraversals.class,
+            // ProfileTest.Traversals.class,
+            // WriteTest.Traversals.class,
+            // ExplainTest.Traversals.class,
+            // SideEffectTest.Traversals.class,
+            // SubgraphTest.Traversals.class,
+            // TreeTest.Traversals.class,
+            //
+            // // compliance
+            // ComplexTest.Traversals.class,
+            // CoreTraversalTest.class,
+            // TraversalInterruptionTest.class,
+            //
+            // // creations
+            // TranslationStrategyProcessTest.class,
+            //
+            // // decorations
+            // ElementIdStrategyProcessTest.class,
+            // EventStrategyProcessTest.class,
+            // PartitionStrategyProcessTest.class,
+            //
+            // // optimizations
+            // IncidentToAdjacentStrategyProcessTest.class,
+            // EarlyLimitStrategyProcessTest.class,
     };
 
     /**
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 15dffee512..aa8a58276f 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
@@ -62,6 +62,10 @@ public abstract class MergeEdgeTest extends AbstractGremlinTest {
 
     public abstract Traversal<Map<Object,Object>, Edge> get_g_injectXlabel_knows_out_marko_in_vadasX_mergeE();
 
+    public abstract Traversal<Edge, Edge> get_g_mergeE_ifXxX_returnXxX_else_createXyX();
+
+    public abstract Traversal<Edge, Edge> get_g_mergeE_with_outV_inV_options();
+/*
     @Test
     @FeatureRequirementSet(FeatureRequirementSet.Package.SIMPLE)
     public void g_V_mergeEXlabel_self_weight_05X() {
@@ -192,6 +196,43 @@ public abstract class MergeEdgeTest extends AbstractGremlinTest {
         assertEquals(1, IteratorUtils.count(g.V(100).out("knows").hasId(101)));
     }
 
+    @Test
+    @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_USER_SUPPLIED_IDS)
+    @FeatureRequirement(featureClass = Graph.Features.EdgeFeatures.class, feature = Graph.Features.EdgeFeatures.FEATURE_USER_SUPPLIED_IDS)
+    @FeatureRequirementSet(FeatureRequirementSet.Package.SIMPLE)
+    public void g_mergeE_ifXxX_returnXxX_else_createXyX() {
+        g.addV("person").property(T.id, 1)
+         .addV("person").property(T.id, 2)
+         .addV("person").property(T.id, 3)
+         .addV("person").property(T.id, 4).iterate();
+
+        final Traversal<Edge, Edge> traversal = get_g_mergeE_ifXxX_returnXxX_else_createXyX();
+        printTraversalForm(traversal);
+
+        assertEquals(1, IteratorUtils.count(traversal));
+        assertEquals(4, IteratorUtils.count(g.V()));
+        assertEquals(1, IteratorUtils.count(g.E()));
+        assertEquals(0, IteratorUtils.count(g.E(101)));
+        assertEquals(1, IteratorUtils.count(g.E(201)));
+        assertEquals(0, IteratorUtils.count(g.V(1).out("knows").hasId(2)));
+        assertEquals(1, IteratorUtils.count(g.V(3).out("likes").hasId(4)));
+    }
+*/
+    @Test
+    @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_USER_SUPPLIED_IDS)
+    @FeatureRequirementSet(FeatureRequirementSet.Package.SIMPLE)
+    public void g_mergeE_with_outV_inV_options() {
+        g.addV("person").property(T.id, 1)
+         .addV("person").property(T.id, 2)
+         .iterate();
+
+        final Traversal<Edge, Edge> traversal = get_g_mergeE_with_outV_inV_options();
+        printTraversalForm(traversal);
+
+        assertEquals(1, IteratorUtils.count(traversal));
+        assertEquals(1, IteratorUtils.count(g.V(1).out("knows").hasId(2)));
+    }
+
     public static class Traversals extends MergeEdgeTest {
 
         @Override
@@ -242,5 +283,18 @@ public abstract class MergeEdgeTest extends AbstractGremlinTest {
         public Traversal<Map<Object,Object>, Edge> get_g_injectXlabel_knows_out_marko_in_vadasX_mergeE() {
             return g.inject((Map<Object,Object>) asMap(T.label, "knows", Direction.IN, new ReferenceVertex(101), Direction.OUT, new ReferenceVertex(100))).mergeE();
         }
+
+        @Override
+        public Traversal<Edge, Edge> get_g_mergeE_ifXxX_returnXxX_else_createXyX() {
+            return g.mergeE(asMap(T.id, 101, T.label, "knows", Direction.OUT, 1, Direction.IN, 2))
+                    .option(Merge.onCreate, asMap(T.id, 201, T.label, "likes", Direction.OUT, 3, Direction.IN, 4));
+        }
+
+        @Override
+        public Traversal<Edge,Edge> get_g_mergeE_with_outV_inV_options() {
+            return g.mergeE(asMap(T.label, "knows", Direction.OUT, Merge.outV, Direction.IN, Merge.inV))
+                    .option(Merge.outV, asMap(T.id, 1))
+                    .option(Merge.inV, asMap(T.id, 2));
+        }
     }
 }
\ No newline at end of file
diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/MergeEdge.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/MergeEdge.feature
index 821066ee75..35e7f5a4c1 100644
--- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/MergeEdge.feature
+++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/MergeEdge.feature
@@ -23,10 +23,6 @@ Feature: Step - mergeE()
   # When the test name places a "1" following a "name" it just means that the test is using the id rather
   # than vertex reference.
   #
-  # g_V_mergeEXlabel_self_weight_05X
-  #   - mergeE(Map) using the vertex of current traverser as reference
-  #   - vertex already exists
-  #   - results in a self edge
   # g_mergeEXlabel_knows_out_marko_in_vadasX
   # g_mergeEXlabel_knows_out_marko1_in_vadas1X
   # g_mergeEXlabel_knows_out_marko_in_vadasX_aliased_direction
@@ -85,10 +81,6 @@ Feature: Step - mergeE()
   #   - mergeE(Map) specifying out/in for vertices in the match/create with option(Map)
   #   - vertices and edge already exist
   #   - results in the existing edge getting a new property
-  # g_V_mapXmergeEXlabel_self_weight_05XX
-  #   - 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
@@ -97,7 +89,7 @@ Feature: Step - mergeE()
   # g_V_mergeEXnullX
   #   - mergeE(null) and no option
   #   - vertices already exists
-  #   - results in no matched edges and no new edge
+  #   - Directions not specified - error
   # g_mergeEXemptyX
   #   - mergeE(empty) and no option
   #   - vertex present and no edges
@@ -118,18 +110,10 @@ Feature: Step - mergeE()
   #   - mergeE(empty) and no option
   #   - two vertices exist
   #   - results in two new self edges, one for each vertex
-  # g_V_mergeEXlabel_knowsX_optionXonCreate_created_YX_optionXonMatch_created_NX_exists_updated
-  #   - mergeE(Map) with onCreate(Map) and onMatch(Map)
-  #   - two vertices, two "knows" edges and one "friends" edge
-  #   - results in two returned "knows" edges where one has a property updated and the other a property added
   # g_V_mergeEXemptyX_optionXonCreate_nullX
   #   - mergeE(empty) with onCreate(null)
   #   - one existing vertex
   #   - results in no new edges because onCreate() was null
-  # g_V_mergeEXlabel_selfX_optionXonCreate_emptyX
-  #   - mergeE(Map) with onCreate(empty)
-  #   - one existing vertex
-  #   - results in one new edge with defaults because searchCreate is overridden
   # g_V_mergeEXlabel_selfX_optionXonMatch_nullX
   #   - mergeE(Map) with onMatch(null)
   #   - one existing vertex and one edge
@@ -179,63 +163,21 @@ Feature: Step - mergeE()
     And the graph should return 0 for count of "g.E().properties()"
     And the graph should return 1 for count of "g.V()"
 
-  Scenario: g_V_mergeEXlabel_selfX_optionXonCreate_emptyX
+  Scenario: g_V_mergeEXemptyX_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]\": \"self\"}]"
+    And using the parameter xx1 defined as "m[{\"t[label]\": \"self\", \"D[OUT]\":\"M[outV]\", \"D[IN]\":\"M[inV]\"}]"
     And the traversal of
       """
-      g.V().mergeE(xx1).option(Merge.onCreate,[:])
+      g.V().as("v").mergeE(xx1).option(Merge.onCreate,null).option(Merge.outV,select("v")).option(Merge.inV,select("v"))
       """
     When iterated to list
     Then the result should have a count of 1
     And the graph should return 1 for count of "g.E()"
     And the graph should return 1 for count of "g.V()"
-    And the graph should return 0 for count of "g.E().hasLabel(\"self\")"
-
-  Scenario: g_V_mergeEXemptyX_optionXonCreate_nullX
-    Given the empty graph
-    And the graph initializer of
-      """
-      g.addV("person").property("name", "marko").property("age", 29)
-      """
-    And the traversal of
-      """
-      g.V().mergeE([:]).option(Merge.onCreate,null)
-      """
-    When iterated to list
-    Then the result should be empty
-    And the graph should return 0 for count of "g.E()"
-    And the graph should return 1 for count of "g.V()"
-
-  @UserSuppliedVertexIds
-  Scenario: g_V_mergeEXlabel_knowsX_optionXonCreate_created_YX_optionXonMatch_created_NX_exists_updated
-    Given the empty graph
-    And the graph initializer of
-      """
-      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("b").to("a").
-        addE("friends").from("b").to("a")
-      """
-    And using the parameter xx1 defined as "m[{\"t[label]\": \"knows\"}]"
-    And using the parameter xx2 defined as "m[{\"t[label]\": \"knows\", \"D[OUT]\":\"v[100]\", \"D[IN]\":\"v[101]\",\"created\":\"Y\"}]"
-    And using the parameter xx3 defined as "m[{\"created\":\"N\"}]"
-    And the traversal of
-      """
-      g.V().mergeE(xx1).option(Merge.onCreate,xx2).option(Merge.onMatch,xx3)
-      """
-    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 3 for count of "g.E()"
-    And the graph should return 0 for count of "g.E().hasLabel(\"knows\").has(\"created\",\"Y\")"
-    And the graph should return 2 for count of "g.E().hasLabel(\"knows\").has(\"created\",\"N\")"
-    And the graph should return 1 for count of "g.E().hasLabel(\"friends\")"
 
   Scenario: g_mergeEXemptyX_exists
     Given the empty graph
@@ -273,15 +215,17 @@ Feature: Step - mergeE()
       g.addV("person").property("name", "marko").property("age", 29).
         addV("person").property("name", "vadas").property("age", 27)
       """
+    And using the parameter xx1 defined as "m[{\"t[label]\": \"self\", \"D[OUT]\":\"M[outV]\", \"D[IN]\":\"M[inV]\"}]"
     And the traversal of
       """
-      g.V().mergeE([:])
+      g.V().as("v").mergeE(xx1).option(Merge.outV,select("v")).option(Merge.inV,select("v"))
       """
     When iterated to list
     Then the result should have a count of 2
     And the graph should return 2 for count of "g.E()"
     And the graph should return 2 for count of "g.V()"
 
+  # null same as empty
   Scenario: g_mergeEXnullX
     Given the empty graph
     And the graph initializer of
@@ -293,10 +237,9 @@ Feature: Step - mergeE()
       g.mergeE(null)
       """
     When iterated to list
-    Then the result should be empty
-    And the graph should return 0 for count of "g.E()"
-    And the graph should return 1 for count of "g.V()"
+    Then the traversal will raise an error
 
+  # Directions not specified
   Scenario: g_V_mergeEXnullX
     Given the empty graph
     And the graph initializer of
@@ -308,26 +251,7 @@ Feature: Step - mergeE()
       g.V().mergeE(null)
       """
     When iterated to list
-    Then the result should be empty
-    And the graph should return 0 for count of "g.E()"
-    And the graph should return 1 for count of "g.V()"
-
-  Scenario: g_V_mergeEXlabel_self_weight_05X
-    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]\": \"self\", \"weight\":\"d[0.5].d\"}]"
-    And the traversal of
-      """
-      g.V().mergeE(xx1)
-      """
-    When iterated to list
-    Then the result should have a count of 1
-    And the graph should return 1 for count of "g.E()"
-    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\").as(\"a\").outE(\"self\").has(\"weight\",0.5).inV().where(eq(\"a\"))"
+    Then the traversal will raise an error
 
   @UserSuppliedVertexIds
   Scenario: g_mergeEXlabel_knows_out_marko_in_vadasX
@@ -668,23 +592,6 @@ Feature: Step - mergeE()
     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\")"
 
-  Scenario: g_V_mapXmergeEXlabel_self_weight_05XX
-    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]\": \"self\", \"weight\":\"d[0.5].d\"}]"
-    And the traversal of
-      """
-      g.V().map(__.mergeE(xx1))
-      """
-    When iterated to list
-    Then the result should have a count of 1
-    And the graph should return 1 for count of "g.E()"
-    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\").as(\"a\").outE(\"self\").has(\"weight\",0.5).inV().where(eq(\"a\"))"
-
   @UserSuppliedVertexIds
   Scenario: g_mergeEXlabel_knows_out_marko_in_vadasX_aliased_direction
     Given the empty graph
@@ -747,4 +654,163 @@ Feature: Step - mergeE()
     And the graph should return 2 for count of "g.V()"
     And the graph should return 0 for count of "g.E().hasLabel(\"knows\").has(\"created\",\"Y\")"
     And the graph should return 1 for count of "g.E().hasLabel(\"knows\").has(\"created\",\"N\")"
-    And the graph should return 0 for count of "g.E().hasLabel(\"knows\").has(\"weight\")"
\ No newline at end of file
+    And the graph should return 0 for count of "g.E().hasLabel(\"knows\").has(\"weight\")"
+
+
+  @UserSuppliedVertexIds
+  Scenario: g_mergeE_with_outVinV_options_map
+    Given the empty graph
+    And the graph initializer of
+      """
+      g.addV("person").property(T.id, 1).property("name", "a")
+       .addV("person").property(T.id, 2).property("name", "b")
+      """
+    And using the parameter xx1 defined as "m[{ \"D[OUT]\": \"M[outV]\", \"D[IN]\": \"M[inV]\", \"t[label]\": \"knows\"}]"
+    And using the parameter xx2 defined as "m[{\"t[id]\": 1}]"
+    And using the parameter xx3 defined as "m[{\"t[id]\": 2}]"
+    And the traversal of
+      """
+      g.mergeE(xx1).option(outV, xx2).option(inV, 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.V(1).out(\"knows\").hasId(2)"
+
+  @UserSuppliedVertexIds
+  Scenario: g_mergeE_with_outVinV_options_select
+    Given the empty graph
+    And the graph initializer of
+      """
+      g.addV("person").property(T.id, 1).property("name", "a")
+       .addV("person").property(T.id, 2).property("name", "b")
+      """
+    And using the parameter xx1 defined as "m[{ \"D[OUT]\": \"M[outV]\", \"D[IN]\": \"M[inV]\", \"t[label]\": \"knows\"}]"
+    And the traversal of
+      """
+      g.V(1).as("x").V(2).as("y").mergeE(xx1).option(outV, select("x")).option(inV, select("y"))
+      """
+    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.V(1).out(\"knows\").hasId(2)"
+
+  # onCreate inherits from merge and can specify an eid
+  @UserSuppliedVertexIds
+  @UserSuppliedEdgeIds
+  Scenario: g_mergeE_with_eid_specified_and_inheritance
+    Given the empty graph
+    And the graph initializer of
+      """
+      g.addV("person").property(T.id, 1).property("name", "a")
+       .addV("person").property(T.id, 2).property("name", "b")
+      """
+    And using the parameter xx1 defined as "m[{\"D[OUT]\": \"v[1]\", \"D[IN]\": \"v[2]\", \"t[label]\": \"knows\"}]"
+    And using the parameter xx2 defined as "m[{\"t[id]\": 201}]"
+    And the traversal of
+      """
+      g.mergeE(xx1).option(onCreate, xx2)
+      """
+    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()"
+    And the graph should return 1 for count of "g.E(201)"
+    And the graph should return 1 for count of "g.V(1).out(\"knows\").hasId(2)"
+
+  # onCreate inherits from merge and can specify an eid
+  @UserSuppliedVertexIds
+  @UserSuppliedEdgeIds
+  Scenario: g_mergeE_with_eid_specified_and_inheritance
+    Given the empty graph
+    And the graph initializer of
+      """
+      g.addV("person").property(T.id, 1).property("name", "a")
+       .addV("person").property(T.id, 2).property("name", "b")
+      """
+    And using the parameter xx1 defined as "m[{\"t[id]\": 201}]"
+    And using the parameter xx2 defined as "m[{\"D[OUT]\": \"v[1]\", \"D[IN]\": \"v[2]\", \"t[label]\": \"knows\"}]"
+    And the traversal of
+      """
+      g.mergeE(xx1).option(onCreate, xx2)
+      """
+    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()"
+    And the graph should return 1 for count of "g.E(201)"
+    And the graph should return 1 for count of "g.V(1).out(\"knows\").hasId(2)"
+
+  # cannot override Direction.OUT in onCreate
+  @UserSuppliedVertexIds
+  Scenario: g_mergeE_outV_override_prohibited
+    Given the empty graph
+    And the graph initializer of
+      """
+      g.addV("person").property(T.id, 1).property("name", "a")
+       .addV("person").property(T.id, 2).property("name", "b")
+      """
+    And using the parameter xx1 defined as "m[{\"D[OUT]\" : \"v[1]\", \"D[IN]\" : \"v[2]\", \"t[label]\": \"knows\"}]"
+    And using the parameter xx2 defined as "m[{\"D[OUT]\" : \"v[2]\"}]"
+    And the traversal of
+      """
+      g.mergeE(xx1).option(onCreate, xx2)
+      """
+    When iterated to list
+    Then the traversal will raise an error
+
+  # cannot override Direction.IN in onCreate
+  @UserSuppliedVertexIds
+  Scenario: g_mergeE_inV_override_prohibited
+    Given the empty graph
+    And the graph initializer of
+      """
+      g.addV("person").property(T.id, 1).property("name", "a")
+       .addV("person").property(T.id, 2).property("name", "b")
+      """
+    And using the parameter xx1 defined as "m[{\"D[OUT]\" : \"v[1]\", \"D[IN]\" : \"v[2]\", \"t[label]\": \"knows\"}]"
+    And using the parameter xx2 defined as "m[{\"D[IN]\" : \"v[1]\"}]"
+    And the traversal of
+      """
+      g.mergeE(xx1).option(onCreate, xx2)
+      """
+    When iterated to list
+    Then the traversal will raise an error
+
+  # cannot override T.label in onCreate
+  @UserSuppliedVertexIds
+  Scenario: g_mergeE_label_override_prohibited
+    Given the empty graph
+    And the graph initializer of
+      """
+      g.addV("person").property(T.id, 1).property("name", "a")
+       .addV("person").property(T.id, 2).property("name", "b")
+      """
+    And using the parameter xx1 defined as "m[{\"D[OUT]\" : \"v[1]\", \"D[IN]\" : \"v[2]\", \"t[label]\": \"knows\"}]"
+    And using the parameter xx2 defined as "m[{\"t[label]\": \"likes\"}]"
+    And the traversal of
+      """
+      g.mergeE(xx1).option(onCreate, xx2)
+      """
+    When iterated to list
+    Then the traversal will raise an error
+
+  # cannot override T.id in onCreate
+  @UserSuppliedVertexIds
+  @UserSuppliedEdgeIds
+  Scenario: g_mergeE_id_override_prohibited
+    Given the empty graph
+    And the graph initializer of
+      """
+      g.addV("person").property(T.id, 1).property("name", "a")
+       .addV("person").property(T.id, 2).property("name", "b")
+      """
+    And using the parameter xx1 defined as "m[{\"D[OUT]\" : \"v[1]\", \"D[IN]\" : \"v[2]\", \"t[label]\": \"knows\", \"t[id]\": \"101\"}]"
+    And using the parameter xx2 defined as "m[{\"t[id]\": \"201\"}]"
+    And the traversal of
+      """
+      g.mergeE(xx1).option(onCreate, xx2)
+      """
+    When iterated to list
+    Then the traversal will raise an error
+
diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/MergeVertex.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/MergeVertex.feature
index ee1bf1d017..c0283f8d3a 100644
--- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/MergeVertex.feature
+++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/MergeVertex.feature
@@ -139,9 +139,8 @@ Feature: Step - mergeV()
     And using the parameter xx1 defined as "m[{\"t[label]\": null, \"name\":\"marko\"}]"
     And the traversal of
       """
-      g.mergeV(null).option(Merge.onCreate,xx1)
+      g.mergeV(xx1)
       """
-    When iterated to list
     Then the traversal will raise an error
 
   Scenario: g_V_mergeVXnullX_optionXonCreate_label_null_name_markoX
@@ -153,9 +152,8 @@ Feature: Step - mergeV()
     And using the parameter xx1 defined as "m[{\"t[label]\": null, \"name\":\"marko\"}]"
     And the traversal of
       """
-      g.V().mergeV(null).option(Merge.onCreate,xx1)
+      g.V().mergeV(xx1)
       """
-    When iterated to list
     Then the traversal will raise an error
 
   Scenario: g_mergeVXlabel_person_name_stephenX_optionXonCreate_nullX
@@ -170,9 +168,10 @@ Feature: Step - mergeV()
       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()"
+    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.V().has(\"person\",\"name\",\"marko\")"
+    And the graph should return 1 for count of "g.V().has(\"person\",\"name\",\"stephen\")"
 
   Scenario: g_V_mergeVXlabel_person_name_stephenX_optionXonCreate_nullX
     Given the empty graph
@@ -186,9 +185,10 @@ Feature: Step - mergeV()
       g.V().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()"
+    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.V().has(\"person\",\"name\",\"marko\")"
+    And the graph should return 1 for count of "g.V().has(\"person\",\"name\",\"stephen\")"
 
   Scenario: g_mergeVXnullX_optionXonCreate_emptyX
     Given the empty graph
@@ -202,7 +202,7 @@ Feature: Step - mergeV()
       """
     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.V()"
 
   Scenario: g_V_mergeVXnullX_optionXonCreate_emptyX
     Given the empty graph
@@ -216,7 +216,7 @@ Feature: Step - mergeV()
       """
     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.V()"
 
   Scenario: g_mergeVXemptyX_no_existing
     Given the empty graph
@@ -280,7 +280,7 @@ Feature: Step - mergeV()
       g.mergeV(null)
       """
     When iterated to list
-    Then the result should have a count of 0
+    Then the result should have a count of 1
     And the graph should return 1 for count of "g.V()"
 
   Scenario: g_V_mergeVXnullX
@@ -294,7 +294,7 @@ Feature: Step - mergeV()
       g.V().mergeV(null)
       """
     When iterated to list
-    Then the result should have a count of 0
+    Then the result should have a count of 1
     And the graph should return 1 for count of "g.V()"
 
   Scenario: g_mergeVXlabel_person_name_stephenX
@@ -654,4 +654,79 @@ Feature: Step - 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\", 19)"
-    And the graph should return 1 for count of "g.V().has(\"person\",\"name\",\"marko\").has(\"age\")"
\ No newline at end of file
+    And the graph should return 1 for count of "g.V().has(\"person\",\"name\",\"marko\").has(\"age\")"
+
+
+
+  # onCreate inheritance from merge
+  Scenario: g_mergeV_onCreate_inheritance_existing
+    Given the empty graph
+    And the graph initializer of
+      """
+      g.addV("person").property("name", "mike").property(T.id, 1)
+      """
+    And using the parameter xx1 defined as "m[{\"t[id]\": 1}]"
+    And using the parameter xx2 defined as "m[{\"t[label]\": \"person\", \"name\":\"mike\"}]"
+    And the traversal of
+      """
+      g.mergeV(xx1).option(Merge.onCreate, xx2)
+      """
+    When iterated to list
+    Then the result should have a count of 1
+    And the graph should return 1 for count of "g.V()"
+    And the graph should return 1 for count of "g.V(1).has(\"person\",\"name\",\"mike\")"
+
+  # onCreate inheritance from merge
+  Scenario: g_mergeV_onCreate_inheritance_new
+    Given the empty graph
+    And using the parameter xx1 defined as "m[{\"t[id]\": 1}]"
+    And using the parameter xx2 defined as "m[{\"t[label]\": \"person\", \"name\":\"mike\"}]"
+    And the traversal of
+      """
+      g.mergeV(xx1).option(Merge.onCreate, xx2)
+      """
+    When iterated to list
+    Then the result should have a count of 1
+    And the graph should return 1 for count of "g.V()"
+    And the graph should return 1 for count of "g.V(1).has(\"person\",\"name\",\"mike\")"
+
+  # onCreate inheritance from merge
+  Scenario: g_mergeV_onCreate_inheritance_new
+    Given the empty graph
+    And using the parameter xx1 defined as "m[{\"t[label]\": \"person\", \"name\":\"mike\"}]"
+    And using the parameter xx2 defined as "m[{\"t[id]\": 1}]"
+    And the traversal of
+      """
+      g.mergeV(xx1).option(Merge.onCreate, xx2)
+      """
+    When iterated to list
+    Then the result should have a count of 1
+    And the graph should return 1 for count of "g.V()"
+    And the graph should return 1 for count of "g.V(1).has(\"person\",\"name\",\"mike\")"
+
+  # cannot override T.label in onCreate
+  @UserSuppliedVertexIds
+  Scenario: g_mergeE_label_override_prohibited
+    Given the empty graph
+    And using the parameter xx1 defined as "m[{\"t[label]\": \"a\"}]"
+    And using the parameter xx2 defined as "m[{\"t[label]\": \"b\"}]"
+    And the traversal of
+      """
+      g.mergeV(xx1).option(onCreate, xx2)
+      """
+    When iterated to list
+    Then the traversal will raise an error
+
+  # cannot override T.id in onCreate
+  @UserSuppliedVertexIds
+  Scenario: g_mergeE_id_override_prohibited
+    Given the empty graph
+    And using the parameter xx1 defined as "m[{\"t[id]\": 1}]"
+    And using the parameter xx2 defined as "m[{\"t[id]\": 2}]"
+    And the traversal of
+      """
+      g.mergeV(xx1).option(onCreate, xx2)
+      """
+    When iterated to list
+    Then the traversal will raise an error
+
diff --git a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/map/TinkerMergeEdgeStep.java b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/map/TinkerMergeEdgeStep.java
deleted file mode 100644
index 482fea1214..0000000000
--- a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/map/TinkerMergeEdgeStep.java
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.tinkerpop.gremlin.tinkergraph.process.traversal.step.map;
-
-import org.apache.tinkerpop.gremlin.process.traversal.Merge;
-import org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeStep;
-import org.apache.tinkerpop.gremlin.structure.Direction;
-import org.apache.tinkerpop.gremlin.structure.Edge;
-import org.apache.tinkerpop.gremlin.structure.Property;
-import org.apache.tinkerpop.gremlin.structure.T;
-import org.apache.tinkerpop.gremlin.structure.Vertex;
-import org.apache.tinkerpop.gremlin.structure.util.CloseableIterator;
-import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerGraph;
-import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerHelper;
-import org.apache.tinkerpop.gremlin.util.iterator.IteratorUtils;
-
-import java.util.Iterator;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.stream.Stream;
-
-/**
- * Optimizes {@code mergeE()} searches by attempting to use an index where possible.
- */
-public class TinkerMergeEdgeStep<S> extends MergeEdgeStep<S> {
-
-    public TinkerMergeEdgeStep(final MergeEdgeStep step) {
-        super(step.getTraversal(), step.isStart(), step.getSearchCreateTraversal());
-        if (step.getOnMatchTraversal() != null) this.addChildOption(Merge.onMatch, step.getOnMatchTraversal());
-        if (step.getOnCreateTraversal() != null) this.addChildOption(Merge.onCreate, step.getOnCreateTraversal());
-        if (step.getCallbackRegistry() != null) this.callbackRegistry = step.getCallbackRegistry();
-    }
-
-    @Override
-    protected Stream<Edge> createSearchStream(final Map<Object, Object> search) {
-        final TinkerGraph graph = (TinkerGraph) this.getTraversal().getGraph().get();
-        Optional<String> firstIndex = Optional.empty();
-
-        Stream<Edge> stream;
-        // prioritize lookup by id but otherwise attempt an index lookup
-        if (null == search) {
-            return Stream.empty();
-        } else if (search.containsKey(T.id)) {
-            stream = IteratorUtils.stream(graph.edges(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
-            final Set<String> indexedKeys = graph.getIndexedKeys(Edge.class);
-            firstIndex = search.keySet().stream().
-                    filter(k -> k instanceof String).
-                    map(k -> (String) k).
-                    filter(indexedKeys::contains).findFirst();
-
-            // use the index if possible otherwise just in memory filter
-            stream = firstIndex.map(s -> TinkerHelper.queryEdgeIndex(graph, s, search.get(s)).stream().map(e -> (Edge) e)).
-                    orElseGet(() -> {
-                        if (search.containsKey(Direction.BOTH)) {
-                            // filter self-edges with distinct()
-                            return IteratorUtils.stream(graph.vertices(search.get(Direction.BOTH))).flatMap(v -> IteratorUtils.stream(v.edges(Direction.BOTH))).distinct();
-                        } else if (search.containsKey(Direction.OUT)) {
-                            return IteratorUtils.stream(graph.vertices(search.get(Direction.OUT))).flatMap(v -> IteratorUtils.stream(v.edges(Direction.OUT)));
-                        } else if (search.containsKey(Direction.IN)) {
-                            return IteratorUtils.stream(graph.vertices(search.get(Direction.IN))).flatMap(v -> IteratorUtils.stream(v.edges(Direction.IN)));
-                        } else {
-                            return IteratorUtils.stream(graph.edges());
-                        }
-                    });
-        }
-
-        final Optional<String> indexUsed = firstIndex;
-        stream = stream.filter(e -> {
-            // try to match on all search criteria skipping T.id as it was handled above
-            return search.entrySet().stream().filter(kv -> {
-                final Object k = kv.getKey();
-                return k != T.id && !(indexUsed.isPresent() && indexUsed.get().equals(k));
-            }).allMatch(kv -> {
-                if (kv.getKey() == T.label) {
-                    return e.label().equals(kv.getValue());
-                } else if (kv.getKey() instanceof Direction) {
-                    final Direction direction = (Direction) kv.getKey();
-
-                    // try to take advantage of string id conversions of the graph by doing a lookup rather
-                    // than direct compare on id
-                    final Iterator<Vertex> found = graph.vertices(kv.getValue());
-                    final Iterator<Vertex> dfound = e.vertices(direction);
-                    final boolean matched = found.hasNext() && dfound.next().equals(found.next());
-                    CloseableIterator.closeIterator(found);
-                    CloseableIterator.closeIterator(dfound);
-                    return matched;
-                } else {
-                    final Property<Object> vp = e.property(kv.getKey().toString());
-                    return vp.isPresent() && kv.getValue().equals(vp.value());
-                }
-            });
-        });
-
-        return stream;
-    }
-}
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
deleted file mode 100644
index 9c3cc03c3a..0000000000
--- a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/map/TinkerMergeVertexStep.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.tinkerpop.gremlin.tinkergraph.process.traversal.step.map;
-
-import org.apache.tinkerpop.gremlin.process.traversal.Merge;
-import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
-import org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeVertexStep;
-import org.apache.tinkerpop.gremlin.structure.Graph;
-import org.apache.tinkerpop.gremlin.structure.T;
-import org.apache.tinkerpop.gremlin.structure.Vertex;
-import org.apache.tinkerpop.gremlin.structure.VertexProperty;
-import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerGraph;
-import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerHelper;
-import org.apache.tinkerpop.gremlin.util.iterator.IteratorUtils;
-
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.stream.Stream;
-
-/**
- * Optimizes {@code mergeV()} searches by attempting to use an index where possible.
- */
-public class TinkerMergeVertexStep<S> extends MergeVertexStep<S> {
-    public TinkerMergeVertexStep(final MergeVertexStep step) {
-        super(step.getTraversal(), step.isStart(), step.getSearchCreateTraversal());
-        if (step.getOnMatchTraversal() != null) this.addChildOption(Merge.onMatch, step.getOnMatchTraversal());
-        if (step.getOnCreateTraversal() != null) this.addChildOption(Merge.onCreate, step.getOnCreateTraversal());
-        if (step.getCallbackRegistry() != null) this.callbackRegistry = step.getCallbackRegistry();
-    }
-
-    @Override
-    protected Stream<Vertex> createSearchStream(final Map<Object, Object> search) {
-        final TinkerGraph graph = (TinkerGraph) this.getTraversal().getGraph().get();
-        Optional<String> firstIndex = Optional.empty();
-
-        Stream<Vertex> stream;
-        // prioritize lookup by id but otherwise attempt an index lookup
-        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
-            final Set<String> indexedKeys = graph.getIndexedKeys(Vertex.class);
-            firstIndex = search.keySet().stream().
-                    filter(k -> k instanceof String).
-                    map(k -> (String) k).
-                    filter(indexedKeys::contains).findFirst();
-
-            // use the index if possible otherwise just in memory filter
-            stream = firstIndex.map(s -> TinkerHelper.queryVertexIndex(graph, s, search.get(s)).stream().map(v -> (Vertex) v)).
-                    orElseGet(() -> IteratorUtils.stream(graph.vertices()));
-        }
-
-        final Optional<String> indexUsed = firstIndex;
-        stream = stream.filter(v -> {
-            // try to match on all search criteria skipping T.id as it was handled above
-            return search.entrySet().stream().filter(kv -> {
-                final Object k = kv.getKey();
-                return k != T.id && !(indexUsed.isPresent() && indexUsed.get().equals(k));
-            }).allMatch(kv -> {
-                if (kv.getKey() == T.label) {
-                    return v.label().equals(kv.getValue());
-                } else {
-                    final VertexProperty<Object> vp = v.property(kv.getKey().toString());
-                    return vp.isPresent() && kv.getValue().equals(vp.value());
-                }
-            });
-        });
-
-        return stream;
-    }
-}
diff --git a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/strategy/optimization/TinkerMergeEVStepStrategy.java b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/strategy/optimization/TinkerMergeEVStepStrategy.java
deleted file mode 100644
index c4b1ea583d..0000000000
--- a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/strategy/optimization/TinkerMergeEVStepStrategy.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.tinkerpop.gremlin.tinkergraph.process.traversal.strategy.optimization;
-
-import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
-import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategy;
-import org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeStep;
-import org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeVertexStep;
-import org.apache.tinkerpop.gremlin.process.traversal.strategy.AbstractTraversalStrategy;
-import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalHelper;
-import org.apache.tinkerpop.gremlin.tinkergraph.process.traversal.step.map.TinkerMergeEdgeStep;
-import org.apache.tinkerpop.gremlin.tinkergraph.process.traversal.step.map.TinkerMergeVertexStep;
-
-/**
- * Optimizes {@code mergeV()} and {@code mergeE()} search lookups by using {@link TinkerMergeVertexStep} and
- * {@link TinkerMergeEdgeStep} respectively.
- */
-public final class TinkerMergeEVStepStrategy extends AbstractTraversalStrategy<TraversalStrategy.ProviderOptimizationStrategy>
-        implements TraversalStrategy.ProviderOptimizationStrategy {
-
-    private static final TinkerMergeEVStepStrategy INSTANCE = new TinkerMergeEVStepStrategy();
-
-    private TinkerMergeEVStepStrategy() {
-    }
-
-    @Override
-    public void apply(final Traversal.Admin<?, ?> traversal) {
-        if (TraversalHelper.onGraphComputer(traversal))
-            return;
-
-        for (final MergeVertexStep originalMergeVertexStep : TraversalHelper.getStepsOfClass(MergeVertexStep.class, traversal)) {
-            final TinkerMergeVertexStep tinkerMergeVertexStep = new TinkerMergeVertexStep(originalMergeVertexStep);
-            TraversalHelper.replaceStep(originalMergeVertexStep, tinkerMergeVertexStep, traversal);
-        }
-
-        for (final MergeEdgeStep originalMergeEdgeStep : TraversalHelper.getStepsOfClass(MergeEdgeStep.class, traversal)) {
-            final TinkerMergeEdgeStep tinkerMergeEdgeStep = new TinkerMergeEdgeStep(originalMergeEdgeStep);
-            TraversalHelper.replaceStep(originalMergeEdgeStep, tinkerMergeEdgeStep, traversal);
-        }
-    }
-
-    public static TinkerMergeEVStepStrategy instance() {
-        return INSTANCE;
-    }
-}
diff --git a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerGraph.java b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerGraph.java
index 465b9d7bb2..3a4651e721 100644
--- a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerGraph.java
+++ b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerGraph.java
@@ -39,7 +39,6 @@ import org.apache.tinkerpop.gremlin.tinkergraph.process.computer.TinkerGraphComp
 import org.apache.tinkerpop.gremlin.tinkergraph.process.computer.TinkerGraphComputerView;
 import org.apache.tinkerpop.gremlin.tinkergraph.process.traversal.strategy.optimization.TinkerGraphCountStrategy;
 import org.apache.tinkerpop.gremlin.tinkergraph.process.traversal.strategy.optimization.TinkerGraphStepStrategy;
-import org.apache.tinkerpop.gremlin.tinkergraph.process.traversal.strategy.optimization.TinkerMergeEVStepStrategy;
 import org.apache.tinkerpop.gremlin.tinkergraph.services.TinkerServiceRegistry;
 import org.apache.tinkerpop.gremlin.util.iterator.IteratorUtils;
 
@@ -77,8 +76,7 @@ public final class TinkerGraph implements Graph {
     static {
         TraversalStrategies.GlobalCache.registerStrategies(TinkerGraph.class, TraversalStrategies.GlobalCache.getStrategies(Graph.class).clone().addStrategies(
                 TinkerGraphStepStrategy.instance(),
-                TinkerGraphCountStrategy.instance(),
-                TinkerMergeEVStepStrategy.instance()));
+                TinkerGraphCountStrategy.instance()));
     }
 
     private static final Configuration EMPTY_CONFIGURATION = new BaseConfiguration() {{
diff --git a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/TinkerGraphFeatureTest.java b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/TinkerGraphFeatureTest.java
index da655d5437..73a7e1ebca 100644
--- a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/TinkerGraphFeatureTest.java
+++ b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/TinkerGraphFeatureTest.java
@@ -33,7 +33,9 @@ import org.junit.runner.RunWith;
         tags = "not @RemoteOnly and not @GraphComputerOnly and not @AllowNullPropertyValues",
         glue = { "org.apache.tinkerpop.gremlin.features" },
         objectFactory = TinkerGraphFeatureTest.TinkerGraphGuiceFactory.class,
-        features = { "classpath:/org/apache/tinkerpop/gremlin/test/features" },
+        features = { "classpath:/org/apache/tinkerpop/gremlin/test/features/map/MergeEdge.feature"
+                     //"classpath:/org/apache/tinkerpop/gremlin/test/features/map/MergeVertex.feature"
+        },
         plugin = {"progress", "junit:target/cucumber.xml"})
 public class TinkerGraphFeatureTest {