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

xiazcy pushed a commit to branch multi-label-experiment
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git

commit ebae85332201eec72e3e6ec42b6c96a22b23311d
Author: Yang Xia <[email protected]>
AuthorDate: Fri Jun 5 14:45:39 2026 -0700

    implement edge multi-label
---
 AGENTS.md                                          |  31 ++++++
 .../grammar/DefaultGremlinBaseVisitor.java         |   8 ++
 .../language/grammar/TraversalMethodVisitor.java   |  28 +++++
 .../grammar/TraversalSourceSpawnMethodVisitor.java |  27 +++--
 .../traversal/dsl/graph/GraphTraversal.java        |  35 +++++--
 .../traversal/dsl/graph/GraphTraversalSource.java  |  52 ++++++++--
 .../gremlin/process/traversal/dsl/graph/__.java    |   7 ++
 .../step/map/AbstractAddEdgeStepPlaceholder.java   |   5 +
 .../map/AbstractAddElementStepPlaceholder.java     |   7 +-
 .../step/map/AbstractAddVertexStepPlaceholder.java |   6 ++
 .../traversal/step/map/AddEdgeStartStep.java       |  32 +++++-
 .../step/map/AddEdgeStartStepPlaceholder.java      |  10 +-
 .../process/traversal/step/map/AddEdgeStep.java    |  32 +++++-
 .../traversal/step/map/AddEdgeStepPlaceholder.java |  10 +-
 .../traversal/step/map/AddVertexStartStep.java     |  11 ++
 .../step/map/AddVertexStartStepPlaceholder.java    |  10 +-
 .../process/traversal/step/map/AddVertexStep.java  |  11 ++
 .../step/map/AddVertexStepPlaceholder.java         |  10 +-
 .../process/traversal/step/map/ElementMapStep.java |   6 +-
 .../process/traversal/step/map/MergeEdgeStep.java  | 113 ++++++++++++++++++---
 .../traversal/step/map/PropertyMapStep.java        |   6 +-
 .../traversal/step/sideEffect/AddLabelStep.java    |  18 ++--
 .../traversal/step/sideEffect/DropLabelsStep.java  |  18 ++--
 .../process/traversal/step/util/WithOptions.java   |  18 ++++
 .../process/traversal/step/util/event/Event.java   |  26 +++++
 .../traversal/step/util/event/EventUtil.java       |  20 ++++
 .../structure/io/binary/types/EdgeSerializer.java  |  63 +++++++++---
 .../io/graphson/GraphSONSerializersV4.java         |  12 ++-
 .../structure/util/detached/DetachedEdge.java      |  34 +++++++
 .../structure/util/reference/ReferenceEdge.java    |  31 ++++++
 .../gremlin/structure/util/star/StarGraph.java     |  63 +++++++++++-
 gremlin-language/src/main/antlr4/Gremlin.g4        |   8 +-
 .../tinkergraph/structure/AbstractTinkerGraph.java |  20 ++++
 .../gremlin/tinkergraph/structure/TinkerEdge.java  |  89 +++++++++++++++-
 .../tinkergraph/structure/TinkerElement.java       |   2 +-
 .../gremlin/tinkergraph/structure/TinkerGraph.java |  30 ++++++
 .../structure/TinkerTransactionGraph.java          |  34 +++++++
 .../tinkergraph/structure/TinkerVertex.java        |  43 ++------
 .../step/sideEffect/LabelMutationStepTest.java     |  16 +--
 .../structure/TinkerVertexMultiLabelTest.java      |  12 ++-
 40 files changed, 877 insertions(+), 137 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md
index 9695bfbf86..f1826478d2 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -71,3 +71,34 @@ These rules apply to any AI/IDE assistant operating on this 
repository.
 
 1. Prefer no change over an unsafe or speculative change.
 2. Ask for clarification.
+
+## Beads Issue Tracker
+
+This project uses **bd (beads)** for issue tracking. Run `bd prime` to see 
full workflow context and commands.
+
+### Quick Reference
+
+```bash
+bd ready              # Find available work
+bd show <id>          # View issue details
+bd update <id> --claim  # Claim work
+bd close <id>         # Complete work
+```
+
+### Rules
+
+- Use `bd` for ALL task tracking — do NOT use TodoWrite, TaskCreate, or 
markdown TODO lists
+- Run `bd prime` for detailed command reference and session close protocol
+- Use `bd remember` for persistent knowledge — do NOT use MEMORY.md files
+
+**Architecture in one line:** issues live in a local Dolt DB; sync uses 
`refs/dolt/data` on your git remote; `.beads/issues.jsonl` is a passive export. 
See https://github.com/gastownhall/beads/blob/main/docs/SYNC_CONCEPTS.md for 
details and anti-patterns.
+
+## Session Completion
+
+**When ending a work session**, you MUST complete ALL steps below.
+
+**MANDATORY WORKFLOW:**
+
+1. **File issues for remaining work** - Create issues for anything that needs 
follow-up
+2. **Run quality gates** (if code changed) - Tests, linters, builds
+3. **Update issue status** - Close finished work, update in-progress items
\ No newline at end of file
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/DefaultGremlinBaseVisitor.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/DefaultGremlinBaseVisitor.java
index 6a8cb92d8d..5257be977a 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/DefaultGremlinBaseVisitor.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/DefaultGremlinBaseVisitor.java
@@ -167,6 +167,14 @@ public class DefaultGremlinBaseVisitor<T> extends 
AbstractParseTreeVisitor<T> im
         * {@inheritDoc}
         */
        @Override public T visitTraversalMethod_addE_String(final 
GremlinParser.TraversalMethod_addE_StringContext ctx) { notImplemented(ctx); 
return null; }
+       /**
+        * {@inheritDoc}
+        */
+       @Override public T visitTraversalMethod_addE_StringVarargs(final 
GremlinParser.TraversalMethod_addE_StringVarargsContext ctx) { 
notImplemented(ctx); return null; }
+       /**
+        * {@inheritDoc}
+        */
+       @Override public T visitTraversalMethod_addE_Empty(final 
GremlinParser.TraversalMethod_addE_EmptyContext ctx) { notImplemented(ctx); 
return null; }
        /**
         * {@inheritDoc}
         */
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalMethodVisitor.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalMethodVisitor.java
index b12a20111d..cc65794ed6 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalMethodVisitor.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalMethodVisitor.java
@@ -30,6 +30,7 @@ import 
org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal;
 import org.apache.tinkerpop.gremlin.process.traversal.step.GValue;
 import org.apache.tinkerpop.gremlin.structure.Column;
 import org.apache.tinkerpop.gremlin.structure.Direction;
+import org.apache.tinkerpop.gremlin.structure.Edge;
 import org.apache.tinkerpop.gremlin.structure.T;
 import org.apache.tinkerpop.gremlin.structure.VertexProperty.Cardinality;
 
@@ -204,6 +205,33 @@ public class TraversalMethodVisitor extends 
TraversalRootVisitor<GraphTraversal>
         }
     }
 
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public GraphTraversal visitTraversalMethod_addE_StringVarargs(final 
GremlinParser.TraversalMethod_addE_StringVarargsContext ctx) {
+        final List<GremlinParser.StringArgumentContext> args = 
ctx.stringArgument();
+        final Object firstLiteralOrVar = 
antlr.argumentVisitor.visitStringArgument(args.get(0));
+        final String firstLabel = firstLiteralOrVar instanceof String ? 
(String) firstLiteralOrVar : ((GValue<String>) firstLiteralOrVar).get();
+        final Object secondLiteralOrVar = 
antlr.argumentVisitor.visitStringArgument(args.get(1));
+        final String secondLabel = secondLiteralOrVar instanceof String ? 
(String) secondLiteralOrVar : ((GValue<String>) secondLiteralOrVar).get();
+
+        final String[] moreLabels = new String[args.size() - 2];
+        for (int i = 2; i < args.size(); i++) {
+            final Object literalOrVar = 
antlr.argumentVisitor.visitStringArgument(args.get(i));
+            moreLabels[i - 2] = literalOrVar instanceof String ? (String) 
literalOrVar : ((GValue<String>) literalOrVar).get();
+        }
+        return this.graphTraversal.addE(firstLabel, secondLabel, moreLabels);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public GraphTraversal visitTraversalMethod_addE_Empty(final 
GremlinParser.TraversalMethod_addE_EmptyContext ctx) {
+        return this.graphTraversal.addE(Edge.DEFAULT_LABEL);
+    }
+
     /**
      * {@inheritDoc}
      */
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalSourceSpawnMethodVisitor.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalSourceSpawnMethodVisitor.java
index c2ca344c99..5ce9b41c09 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalSourceSpawnMethodVisitor.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalSourceSpawnMethodVisitor.java
@@ -57,17 +57,32 @@ public class TraversalSourceSpawnMethodVisitor extends 
DefaultGremlinBaseVisitor
      */
     @Override
     public GraphTraversal visitTraversalSourceSpawnMethod_addE(final 
GremlinParser.TraversalSourceSpawnMethod_addEContext ctx) {
-        if (ctx.stringArgument() != null) {
-            final Object literalOrVar = 
antlr.argumentVisitor.visitStringArgument(ctx.stringArgument());
-            if (GValue.valueInstanceOf(literalOrVar, String.class)) {
-                return this.traversalSource.addE((GValue<String>) 
literalOrVar);
+        final List<GremlinParser.StringArgumentContext> stringArgs = 
ctx.stringArgument();
+        if (stringArgs != null && !stringArgs.isEmpty()) {
+            if (stringArgs.size() == 1) {
+                final Object literalOrVar = 
antlr.argumentVisitor.visitStringArgument(stringArgs.get(0));
+                if (GValue.valueInstanceOf(literalOrVar, String.class)) {
+                    return this.traversalSource.addE((GValue<String>) 
literalOrVar);
+                } else {
+                    return this.traversalSource.addE((String) literalOrVar);
+                }
             } else {
-                return this.traversalSource.addE((String) literalOrVar);
+                // Multi-label: addE("a", "b", ...)
+                final Object firstLiteralOrVar = 
antlr.argumentVisitor.visitStringArgument(stringArgs.get(0));
+                final String firstLabel = firstLiteralOrVar instanceof String 
? (String) firstLiteralOrVar : ((GValue<String>) firstLiteralOrVar).get();
+                final Object secondLiteralOrVar = 
antlr.argumentVisitor.visitStringArgument(stringArgs.get(1));
+                final String secondLabel = secondLiteralOrVar instanceof 
String ? (String) secondLiteralOrVar : ((GValue<String>) 
secondLiteralOrVar).get();
+                final String[] moreLabels = new String[stringArgs.size() - 2];
+                for (int i = 2; i < stringArgs.size(); i++) {
+                    final Object literalOrVar = 
antlr.argumentVisitor.visitStringArgument(stringArgs.get(i));
+                    moreLabels[i - 2] = literalOrVar instanceof String ? 
(String) literalOrVar : ((GValue<String>) literalOrVar).get();
+                }
+                return this.traversalSource.addE(firstLabel, secondLabel, 
moreLabels);
             }
         } else if (ctx.nestedTraversal() != null) {
             return 
this.traversalSource.addE(anonymousVisitor.visitNestedTraversal(ctx.nestedTraversal()));
         } else {
-            throw new IllegalArgumentException("addE with empty arguments is 
not valid.");
+            return this.traversalSource.addE();
         }
     }
 
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 31c9874af5..126c48ce73 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
@@ -234,6 +234,7 @@ import java.util.Comparator;
 import java.util.Date;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
@@ -1464,12 +1465,11 @@ public interface GraphTraversal<S, E> extends 
Traversal<S, E> {
             if (null == l) throw new IllegalArgumentException("vertexLabel 
cannot be null");
         }
         this.asAdmin().getGremlinLang().addStep(Symbols.addV, label1, label2, 
moreLabels);
-        this.asAdmin().addStep(new AddVertexStepPlaceholder<>(this.asAdmin(), 
label1));
-        // Add the AddLabelStep directly to avoid double-recording in 
GremlinLang.
-        // The addV step above already recorded all labels; calling 
t.addLabel() would
-        // record an additional addLabel() step in GremlinLang, producing 
incorrect output
-        // like g.addV("a","b").addLabel("b") instead of g.addV("a","b").
-        return this.asAdmin().addStep(new AddLabelStep<>(this.asAdmin(), 
label2, moreLabels));
+        final Set<String> allLabels = new LinkedHashSet<>();
+        allLabels.add(label1);
+        allLabels.add(label2);
+        Collections.addAll(allLabels, moreLabels);
+        return this.asAdmin().addStep(new 
AddVertexStepPlaceholder<>(this.asAdmin(), allLabels));
     }
 
     /**
@@ -1629,6 +1629,29 @@ public interface GraphTraversal<S, E> extends 
Traversal<S, E> {
         return this.asAdmin().addStep(step);
     }
 
+    /**
+     * Multi-label edge creation.
+     *
+     * @param label1     the first label
+     * @param label2     the second label
+     * @param moreLabels additional labels
+     * @return the traversal with the {@link AddEdgeStepContract} added
+     * @since 4.0.0
+     */
+    public default GraphTraversal<S, Edge> addE(final String label1, final 
String label2, final String... moreLabels) {
+        if (null == label1) throw new IllegalArgumentException("edgeLabel 
cannot be null");
+        if (null == label2) throw new IllegalArgumentException("edgeLabel 
cannot be null");
+        for (final String l : moreLabels) {
+            if (null == l) throw new IllegalArgumentException("edgeLabel 
cannot be null");
+        }
+        this.asAdmin().getGremlinLang().addStep(Symbols.addE, label1, label2, 
moreLabels);
+        final Set<String> allLabels = new LinkedHashSet<>();
+        allLabels.add(label1);
+        allLabels.add(label2);
+        Collections.addAll(allLabels, moreLabels);
+        return this.asAdmin().addStep(new 
AddEdgeStepPlaceholder<>(this.asAdmin(), allLabels));
+    }
+
     /**
      * Provide {@code from()}-modulation to respective steps.
      *
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 8777de2d14..288841e8ee 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
@@ -31,7 +31,6 @@ import 
org.apache.tinkerpop.gremlin.process.traversal.step.GValue;
 import org.apache.tinkerpop.gremlin.process.traversal.step.branch.UnionStep;
 import 
org.apache.tinkerpop.gremlin.process.traversal.step.map.AddEdgeStartStepPlaceholder;
 import 
org.apache.tinkerpop.gremlin.process.traversal.step.map.AddVertexStartStepPlaceholder;
-import 
org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.AddLabelStep;
 import org.apache.tinkerpop.gremlin.process.traversal.step.map.CallStep;
 import 
org.apache.tinkerpop.gremlin.process.traversal.step.map.CallStepPlaceholder;
 import org.apache.tinkerpop.gremlin.process.traversal.step.map.GraphStep;
@@ -53,9 +52,12 @@ import 
org.apache.tinkerpop.gremlin.structure.util.StringFactory;
 import org.apache.tinkerpop.gremlin.structure.util.empty.EmptyGraph;
 
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
 import java.util.Map;
 import java.util.Optional;
+import java.util.Set;
 import java.util.function.BinaryOperator;
 import java.util.function.Supplier;
 import java.util.function.UnaryOperator;
@@ -383,12 +385,11 @@ public class GraphTraversalSource implements 
TraversalSource {
         final GraphTraversalSource clone = this.clone();
         clone.gremlinLang.addStep(GraphTraversal.Symbols.addV, label1, label2, 
moreLabels);
         final GraphTraversal.Admin<Vertex, Vertex> traversal = new 
DefaultGraphTraversal<>(clone);
-        traversal.addStep(new AddVertexStartStepPlaceholder(traversal, 
label1));
-        // Add the AddLabelStep directly to avoid double-recording in 
GremlinLang.
-        // The addV step above already recorded all labels; calling 
t.addLabel() would
-        // record an additional addLabel() step in GremlinLang, producing 
incorrect output
-        // like g.addV("a","b").addLabel("b") instead of g.addV("a","b").
-        return traversal.addStep(new AddLabelStep<>(traversal, label2, 
moreLabels));
+        final Set<String> allLabels = new LinkedHashSet<>();
+        allLabels.add(label1);
+        allLabels.add(label2);
+        Collections.addAll(allLabels, moreLabels);
+        return traversal.addStep(new AddVertexStartStepPlaceholder(traversal, 
allLabels));
     }
 
     /**
@@ -427,6 +428,43 @@ public class GraphTraversalSource implements 
TraversalSource {
         return traversal.addStep(new AddEdgeStartStepPlaceholder(traversal, 
label));
     }
 
+    /**
+     * Spawns a {@link GraphTraversal} by adding an edge with multiple labels.
+     *
+     * @param label1     the first label
+     * @param label2     the second label
+     * @param moreLabels additional labels
+     * @return the traversal with the edge added
+     * @since 4.0.0
+     */
+    public GraphTraversal<Edge, Edge> addE(final String label1, final String 
label2, final String... moreLabels) {
+        if (null == label1) throw new IllegalArgumentException("edgeLabel 
cannot be null");
+        if (null == label2) throw new IllegalArgumentException("edgeLabel 
cannot be null");
+        for (final String l : moreLabels) {
+            if (null == l) throw new IllegalArgumentException("edgeLabel 
cannot be null");
+        }
+        final GraphTraversalSource clone = this.clone();
+        clone.gremlinLang.addStep(GraphTraversal.Symbols.addE, label1, label2, 
moreLabels);
+        final GraphTraversal.Admin<Edge, Edge> traversal = new 
DefaultGraphTraversal<>(clone);
+        final Set<String> allLabels = new LinkedHashSet<>();
+        allLabels.add(label1);
+        allLabels.add(label2);
+        Collections.addAll(allLabels, moreLabels);
+        return traversal.addStep(new AddEdgeStartStepPlaceholder(traversal, 
allLabels));
+    }
+
+    /**
+     * Spawns a {@link GraphTraversal} by adding an edge with the provider's 
default label.
+     *
+     * @since 4.0.0
+     */
+    public GraphTraversal<Edge, Edge> addE() {
+        final GraphTraversalSource clone = this.clone();
+        clone.gremlinLang.addStep(GraphTraversal.Symbols.addE);
+        final GraphTraversal.Admin<Edge, Edge> traversal = new 
DefaultGraphTraversal<>(clone);
+        return traversal.addStep(new AddEdgeStartStepPlaceholder(traversal, 
(String) null));
+    }
+
     /**
      * Spawns a {@link GraphTraversal} by doing a merge (i.e. upsert) style 
operation for an {@link Vertex} using a
      * {@code Map} as an argument. The {@code Map} represents search criteria 
and will match each of the supplied
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/__.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/__.java
index a9ad25b924..8324ca6095 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/__.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/__.java
@@ -738,6 +738,13 @@ public class __ {
         return __.<A>start().addE(edgeLabel);
     }
 
+    /**
+     * @see GraphTraversal#addE(String, String, String...)
+     */
+    public static <A> GraphTraversal<A, Edge> addE(final String label1, final 
String label2, final String... moreLabels) {
+        return __.<A>start().addE(label1, label2, moreLabels);
+    }
+
     /**
      * @see GraphTraversal#addE(GValue)
      */
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AbstractAddEdgeStepPlaceholder.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AbstractAddEdgeStepPlaceholder.java
index fb1ce7e8c9..95d377c317 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AbstractAddEdgeStepPlaceholder.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AbstractAddEdgeStepPlaceholder.java
@@ -29,6 +29,7 @@ import 
org.apache.tinkerpop.gremlin.structure.util.reference.ReferenceVertex;
 import java.util.Collection;
 import java.util.List;
 import java.util.Objects;
+import java.util.Set;
 
 public abstract class AbstractAddEdgeStepPlaceholder<S> extends 
AbstractAddElementStepPlaceholder<S, Edge, Event.EdgeAddedEvent> implements 
AddEdgeStepContract<S> {
     protected Traversal.Admin<?, ?> from;
@@ -42,6 +43,10 @@ public abstract class AbstractAddEdgeStepPlaceholder<S> 
extends AbstractAddEleme
         super(traversal, label);
     }
 
+    public AbstractAddEdgeStepPlaceholder(Traversal.Admin traversal, 
Set<String> labels) {
+        super(traversal, labels);
+    }
+
     public AbstractAddEdgeStepPlaceholder(Traversal.Admin traversal, 
Traversal.Admin<S, String> labelTraversal) {
         super(traversal, labelTraversal);
     }
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AbstractAddElementStepPlaceholder.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AbstractAddElementStepPlaceholder.java
index 830fef2c80..a2015b4613 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AbstractAddElementStepPlaceholder.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AbstractAddElementStepPlaceholder.java
@@ -61,7 +61,7 @@ public abstract class AbstractAddElementStepPlaceholder<S, E 
extends Element, X
     protected AbstractAddElementStepPlaceholder(final Traversal.Admin 
traversal, final GValue<String> label) {
         super(traversal);
         this.label = label == null ? this.getDefaultLabel() : label;
-        if (label.isVariable()) {
+        if (label != null && label.isVariable()) {
             traversal.getGValueManager().register(label);
         }
     }
@@ -75,6 +75,11 @@ public abstract class AbstractAddElementStepPlaceholder<S, E 
extends Element, X
         addTraversal(labelTraversal);
     }
 
+    protected AbstractAddElementStepPlaceholder(final Traversal.Admin 
traversal, final Set<String> labels) {
+        super(traversal);
+        this.label = (labels == null || labels.isEmpty()) ? 
this.getDefaultLabel() : labels;
+    }
+
     protected abstract String getDefaultLabel();
 
     @Override
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AbstractAddVertexStepPlaceholder.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AbstractAddVertexStepPlaceholder.java
index 66aa3df116..3e0f0c1c0f 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AbstractAddVertexStepPlaceholder.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AbstractAddVertexStepPlaceholder.java
@@ -25,6 +25,7 @@ import 
org.apache.tinkerpop.gremlin.process.traversal.step.util.event.Event;
 import org.apache.tinkerpop.gremlin.structure.Vertex;
 
 import java.util.Objects;
+import java.util.Set;
 
 public abstract class AbstractAddVertexStepPlaceholder<S> extends 
AbstractAddElementStepPlaceholder<S, Vertex, Event.VertexAddedEvent>
         implements AddVertexStepContract<S>, GValueHolder<S, Vertex> {
@@ -46,6 +47,11 @@ public abstract class AbstractAddVertexStepPlaceholder<S> 
extends AbstractAddEle
         userProvidedLabel = vertexLabelTraversal != null;
     }
 
+    protected AbstractAddVertexStepPlaceholder(final Traversal.Admin 
traversal, final Set<String> labels) {
+        super(traversal, labels);
+        userProvidedLabel = labels != null && !labels.isEmpty();
+    }
+
     @Override
     protected String getDefaultLabel() {
         return Vertex.DEFAULT_LABEL;
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddEdgeStartStep.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddEdgeStartStep.java
index 628587a509..99ccd7ce9e 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddEdgeStartStep.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddEdgeStartStep.java
@@ -19,7 +19,10 @@
 
 package org.apache.tinkerpop.gremlin.process.traversal.step.map;
 
+import java.util.Arrays;
 import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -67,6 +70,15 @@ public class AddEdgeStartStep extends AbstractStep<Edge, 
Edge> implements AddEdg
         this.internalParameters.set(this, T.label, edgeLabelTraversal);
     }
 
+    public AddEdgeStartStep(final Traversal.Admin traversal, final Set<String> 
labels) {
+        super(traversal);
+        if (labels == null || labels.isEmpty()) {
+            this.internalParameters.set(this, T.label, Edge.DEFAULT_LABEL);
+        } else {
+            this.internalParameters.set(this, T.label, labels);
+        }
+    }
+
     @Override
     public <S, E> List<Traversal.Admin<S, E>> getLocalChildren() {
         return this.internalParameters.getTraversals();
@@ -121,7 +133,21 @@ public class AddEdgeStartStep extends AbstractStep<Edge, 
Edge> implements AddEdg
             // a dead traverser to trigger the traversal
             final Traverser.Admin traverser = generator.generate(1, (Step) 
this, 1);
 
-            final String edgeLabel = (String) 
this.internalParameters.get(traverser, T.label, () -> 
Edge.DEFAULT_LABEL).get(0);
+            final Object labelParam = this.internalParameters.get(traverser, 
T.label, () -> Edge.DEFAULT_LABEL).get(0);
+            final String edgeLabel;
+            final Set<String> additionalLabels;
+            if (labelParam instanceof Set) {
+                final Set<String> labelSet = (Set<String>) labelParam;
+                final Iterator<String> iter = labelSet.iterator();
+                edgeLabel = iter.next();
+                additionalLabels = new LinkedHashSet<>();
+                while (iter.hasNext()) {
+                    additionalLabels.add(iter.next());
+                }
+            } else {
+                edgeLabel = (String) labelParam;
+                additionalLabels = Collections.emptySet();
+            }
 
             // FROM/TO must be set and must be vertices
             Object theTo = this.internalParameters.get(traverser, TO, () -> 
null).get(0);
@@ -165,6 +191,10 @@ public class AddEdgeStartStep extends AbstractStep<Edge, 
Edge> implements AddEdg
             }
 
             final Edge edge = fromVertex.addEdge(edgeLabel, toVertex, 
this.internalParameters.getKeyValues(traverser, TO, FROM, T.label));
+            if (!additionalLabels.isEmpty()) {
+                final String[] extra = additionalLabels.toArray(new String[0]);
+                edge.addLabel(extra[0], Arrays.copyOfRange(extra, 1, 
extra.length));
+            }
             EventUtil.registerEdgeCreation(callbackRegistry, getTraversal(), 
edge);
             return generator.generate(edge, this, 1L);
         } else
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddEdgeStartStepPlaceholder.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddEdgeStartStepPlaceholder.java
index 6042823e17..bbf4d429f3 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddEdgeStartStepPlaceholder.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddEdgeStartStepPlaceholder.java
@@ -24,6 +24,8 @@ import 
org.apache.tinkerpop.gremlin.process.traversal.lambda.GValueConstantTrave
 import org.apache.tinkerpop.gremlin.process.traversal.step.GValue;
 import org.apache.tinkerpop.gremlin.structure.Edge;
 
+import java.util.Set;
+
 public class AddEdgeStartStepPlaceholder extends 
AbstractAddEdgeStepPlaceholder<Edge> {
 
     public AddEdgeStartStepPlaceholder(final Traversal.Admin traversal, final 
String edgeLabel) {
@@ -40,10 +42,16 @@ public class AddEdgeStartStepPlaceholder extends 
AbstractAddEdgeStepPlaceholder<
                 (Traversal.Admin<Edge, String>) edgeLabelTraversal);
     }
 
+    public AddEdgeStartStepPlaceholder(final Traversal.Admin traversal, final 
Set<String> labels) {
+        super(traversal, labels);
+    }
+
     @Override
     public AddEdgeStartStep asConcreteStep() {
         AddEdgeStartStep step;
-        if (label instanceof Traversal) {
+        if (label instanceof Set) {
+            step = new AddEdgeStartStep(traversal, (Set<String>) label);
+        } else if (label instanceof Traversal) {
             step = new AddEdgeStartStep(traversal, ((Traversal<?, String>) 
label).asAdmin());
         } else if (label instanceof GValue) {
             step = new AddEdgeStartStep(traversal, ((GValue<String>) 
label).get());
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddEdgeStep.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddEdgeStep.java
index 753d9ae993..7073a7e80f 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddEdgeStep.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddEdgeStep.java
@@ -18,7 +18,10 @@
  */
 package org.apache.tinkerpop.gremlin.process.traversal.step.map;
 
+import java.util.Arrays;
 import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -62,6 +65,15 @@ public class AddEdgeStep<S> extends ScalarMapStep<S, Edge> 
implements AddEdgeSte
         this.internalParameters.set(this, T.label, edgeLabelTraversal);
     }
 
+    public AddEdgeStep(final Traversal.Admin traversal, final Set<String> 
labels) {
+        super(traversal);
+        if (labels == null || labels.isEmpty()) {
+            this.internalParameters.set(this, T.label, Edge.DEFAULT_LABEL);
+        } else {
+            this.internalParameters.set(this, T.label, labels);
+        }
+    }
+
     @Override
     public <S, E> List<Traversal.Admin<S, E>> getLocalChildren() {
         return this.internalParameters.getTraversals();
@@ -109,7 +121,21 @@ public class AddEdgeStep<S> extends ScalarMapStep<S, Edge> 
implements AddEdgeSte
 
     @Override
     protected Edge map(final Traverser.Admin<S> traverser) {
-        final String edgeLabel = this.internalParameters.get(traverser, 
T.label, () -> Edge.DEFAULT_LABEL).get(0);
+        final Object labelParam = this.internalParameters.get(traverser, 
T.label, () -> Edge.DEFAULT_LABEL).get(0);
+        final String edgeLabel;
+        final Set<String> additionalLabels;
+        if (labelParam instanceof Set) {
+            final Set<String> labelSet = (Set<String>) labelParam;
+            final Iterator<String> iter = labelSet.iterator();
+            edgeLabel = iter.next();
+            additionalLabels = new LinkedHashSet<>();
+            while (iter.hasNext()) {
+                additionalLabels.add(iter.next());
+            }
+        } else {
+            edgeLabel = (String) labelParam;
+            additionalLabels = Collections.emptySet();
+        }
 
         Vertex toVertex = getAdjacentVertex(this.internalParameters, TO, 
traverser, edgeLabel);
         Vertex fromVertex = getAdjacentVertex(this.internalParameters, FROM, 
traverser, edgeLabel);
@@ -135,6 +161,10 @@ public class AddEdgeStep<S> extends ScalarMapStep<S, Edge> 
implements AddEdgeSte
         }
 
         final Edge edge = fromVertex.addEdge(edgeLabel, toVertex, 
this.internalParameters.getKeyValues(traverser, TO, FROM, T.label));
+        if (!additionalLabels.isEmpty()) {
+            final String[] extra = additionalLabels.toArray(new String[0]);
+            edge.addLabel(extra[0], Arrays.copyOfRange(extra, 1, 
extra.length));
+        }
         EventUtil.registerEdgeCreation(callbackRegistry, getTraversal(), edge);
         return edge;
     }
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddEdgeStepPlaceholder.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddEdgeStepPlaceholder.java
index a6a5a4466a..a992a1314a 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddEdgeStepPlaceholder.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddEdgeStepPlaceholder.java
@@ -24,6 +24,8 @@ import 
org.apache.tinkerpop.gremlin.process.traversal.lambda.GValueConstantTrave
 import org.apache.tinkerpop.gremlin.process.traversal.step.GValue;
 import org.apache.tinkerpop.gremlin.structure.Edge;
 
+import java.util.Set;
+
 public class AddEdgeStepPlaceholder<S> extends 
AbstractAddEdgeStepPlaceholder<S> {
 
     public AddEdgeStepPlaceholder(final Traversal.Admin traversal, final 
String edgeLabel) {
@@ -39,10 +41,16 @@ public class AddEdgeStepPlaceholder<S> extends 
AbstractAddEdgeStepPlaceholder<S>
                 new ConstantTraversal<>(Edge.DEFAULT_LABEL) : 
edgeLabelTraversal);
     }
 
+    public AddEdgeStepPlaceholder(final Traversal.Admin traversal, final 
Set<String> labels) {
+        super(traversal, labels);
+    }
+
     @Override
     public AddEdgeStep<S> asConcreteStep() {
         AddEdgeStep<S> step;
-        if (label instanceof Traversal) {
+        if (label instanceof Set) {
+            step = new AddEdgeStep<>(traversal, (Set<String>) label);
+        } else if (label instanceof Traversal) {
             step = new AddEdgeStep<>(traversal, ((Traversal<S, String>) 
label).asAdmin());
         } else if (label instanceof GValue) {
             step = new AddEdgeStep<>(traversal, ((GValue<String>) 
label).get());
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStartStep.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStartStep.java
index 7718cc2883..5f109ae426 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStartStep.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStartStep.java
@@ -63,6 +63,17 @@ public class AddVertexStartStep extends AbstractStep<Vertex, 
Vertex> implements
         userProvidedLabel = vertexLabelTraversal != null;
     }
 
+    public AddVertexStartStep(final Traversal.Admin traversal, final 
Set<String> labels) {
+        super(traversal);
+        if (labels == null || labels.isEmpty()) {
+            this.internalParameters.set(this, T.label, Vertex.DEFAULT_LABEL);
+            userProvidedLabel = false;
+        } else {
+            this.internalParameters.set(this, T.label, labels);
+            userProvidedLabel = true;
+        }
+    }
+
     @Override
     public boolean hasUserProvidedLabel() {
         return userProvidedLabel;
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStartStepPlaceholder.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStartStepPlaceholder.java
index e84f4a7f1c..a1b5fa2449 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStartStepPlaceholder.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStartStepPlaceholder.java
@@ -24,6 +24,8 @@ import 
org.apache.tinkerpop.gremlin.process.traversal.step.GValue;
 import org.apache.tinkerpop.gremlin.process.traversal.step.GValueHolder;
 import org.apache.tinkerpop.gremlin.structure.Vertex;
 
+import java.util.Set;
+
 public class AddVertexStartStepPlaceholder extends 
AbstractAddVertexStepPlaceholder<Vertex>
         implements AddVertexStepContract<Vertex>, GValueHolder<Vertex, Vertex> 
{
 
@@ -40,10 +42,16 @@ public class AddVertexStartStepPlaceholder extends 
AbstractAddVertexStepPlacehol
                 new ConstantTraversal<>(Vertex.DEFAULT_LABEL) : 
(Traversal.Admin<Vertex,String>) vertexLabelTraversal);
     }
 
+    public AddVertexStartStepPlaceholder(final Traversal.Admin traversal, 
final Set<String> labels) {
+        super(traversal, labels);
+    }
+
     @Override
     public AddVertexStartStep asConcreteStep() {
         AddVertexStartStep step;
-        if (label instanceof Traversal) {
+        if (label instanceof Set) {
+            step = new AddVertexStartStep(traversal, (Set<String>) label);
+        } else if (label instanceof Traversal) {
             step = new AddVertexStartStep(traversal, ((Traversal<?, String>) 
label).asAdmin());
         } else if (label instanceof GValue) {
             step = new AddVertexStartStep(traversal, ((GValue<String>) 
label).get());
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStep.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStep.java
index b015920e17..4c2b505b53 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStep.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStep.java
@@ -59,6 +59,17 @@ public class AddVertexStep<S> extends ScalarMapStep<S, 
Vertex> implements AddVer
         userProvidedLabel = vertexLabelTraversal != null;
     }
 
+    public AddVertexStep(final Traversal.Admin traversal, final Set<String> 
labels) {
+        super(traversal);
+        if (labels == null || labels.isEmpty()) {
+            this.internalParameters.set(this, T.label, Vertex.DEFAULT_LABEL);
+            userProvidedLabel = false;
+        } else {
+            this.internalParameters.set(this, T.label, labels);
+            userProvidedLabel = true;
+        }
+    }
+
     @Override
     public boolean hasUserProvidedLabel() {
         return userProvidedLabel;
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStepPlaceholder.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStepPlaceholder.java
index 505bc47c1f..b749760051 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStepPlaceholder.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStepPlaceholder.java
@@ -22,6 +22,8 @@ import 
org.apache.tinkerpop.gremlin.process.traversal.Traversal;
 import 
org.apache.tinkerpop.gremlin.process.traversal.lambda.GValueConstantTraversal;
 import org.apache.tinkerpop.gremlin.process.traversal.step.GValue;
 
+import java.util.Set;
+
 public class AddVertexStepPlaceholder<S> extends 
AbstractAddVertexStepPlaceholder<S> {
 
     public AddVertexStepPlaceholder(Traversal.Admin traversal, String label) {
@@ -36,10 +38,16 @@ public class AddVertexStepPlaceholder<S> extends 
AbstractAddVertexStepPlaceholde
         super(traversal, vertexLabelTraversal);
     }
 
+    public AddVertexStepPlaceholder(Traversal.Admin traversal, Set<String> 
labels) {
+        super(traversal, labels);
+    }
+
     @Override
     public AddVertexStep<S> asConcreteStep() {
         AddVertexStep<S> step;
-        if (label instanceof Traversal) {
+        if (label instanceof Set) {
+            step = new AddVertexStep<>(traversal, (Set<String>) label);
+        } else if (label instanceof Traversal) {
             step = new AddVertexStep<>(traversal, ((Traversal<S, String>) 
label).asAdmin());
         } else if (label instanceof GValue) {
             step = new AddVertexStep<>(traversal, ((GValue<String>) 
label).get());
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/ElementMapStep.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/ElementMapStep.java
index 370b0cd059..b05e620849 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/ElementMapStep.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/ElementMapStep.java
@@ -25,7 +25,6 @@ import 
org.apache.tinkerpop.gremlin.process.traversal.step.GraphComputing;
 import org.apache.tinkerpop.gremlin.process.traversal.step.TraversalParent;
 import org.apache.tinkerpop.gremlin.process.traversal.step.util.Parameters;
 import org.apache.tinkerpop.gremlin.process.traversal.step.util.WithOptions;
-import 
org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.OptionsStrategy;
 import 
org.apache.tinkerpop.gremlin.process.traversal.traverser.TraverserRequirement;
 import org.apache.tinkerpop.gremlin.structure.Direction;
 import org.apache.tinkerpop.gremlin.structure.Edge;
@@ -131,10 +130,7 @@ public class ElementMapStep<K,E> extends 
ScalarMapStep<Element, Map<K, E>> imple
      * or source-level {@code g.with("multilabel")}.
      */
     private boolean isMultilabelEnabled() {
-        if (this.multilabel) return true;
-        return 
getTraversal().getStrategies().getStrategy(OptionsStrategy.class)
-                .map(os -> os.getOptions().containsKey("multilabel") || 
os.getOptions().containsKey(WithOptions.multilabel))
-                .orElse(false);
+        return WithOptions.isMultilabelEnabled(this.multilabel, 
getTraversal());
     }
 
     public String[] getPropertyKeys() {
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 3535143192..261ab78eb8 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
@@ -20,6 +20,7 @@ package 
org.apache.tinkerpop.gremlin.process.traversal.step.map;
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
@@ -166,17 +167,21 @@ public class MergeEdgeStep<S> extends MergeElementStep<S, 
Edge, Map<Object, Obje
         final Graph graph = getGraph();
 
         final Object edgeId = search.get(T.id);
-        final String edgeLabel = (String) search.get(T.label);
+        final Object labelValue = search.get(T.label);
         final Object fromId = search.get(Direction.OUT);
         final Object toId = search.get(Direction.IN);
 
+        // Extract a single label string for use in outE(label)/inE(label) 
calls.
+        // For multi-label (collection), we use hasLabel() chaining instead.
+        final String singleLabel = labelValue instanceof String ? (String) 
labelValue : null;
+        final boolean hasLabelConstraint = labelValue != null;
+
         GraphTraversal t;
         if (edgeId != null) {
 
             // 
g.E(eid).hasLabel(label).where(outV().hasId(fromId)).where(inV().hasId(toId));
             t = graph.traversal().E(edgeId);
-            if (edgeLabel != null)
-                t = t.hasLabel(edgeLabel);
+            t = applyLabelConstraint(t, labelValue);
             if (fromId != null)
                 t = t.where(outV().hasId(fromId));
             if (toId != null)
@@ -186,10 +191,14 @@ public class MergeEdgeStep<S> extends MergeElementStep<S, 
Edge, Map<Object, Obje
 
             // g.V(fromId).outE(label).where(inV().hasId(toId));
             t = graph.traversal().V(fromId);
-            if (edgeLabel != null)
-                t = t.outE(edgeLabel);
-            else
+            if (singleLabel != null)
+                t = t.outE(singleLabel);
+            else {
                 t = t.outE();
+                // apply multi-label constraint after outE() if it's a 
collection
+                if (hasLabelConstraint && singleLabel == null)
+                    t = applyLabelConstraint(t, labelValue);
+            }
             if (toId != null)
                 t = t.where(inV().hasId(toId));
 
@@ -197,17 +206,20 @@ public class MergeEdgeStep<S> extends MergeElementStep<S, 
Edge, Map<Object, Obje
 
             // g.V(toId).inE(edgeLabel);
             t = graph.traversal().V(toId);
-            if (edgeLabel != null)
-                t = t.inE(edgeLabel);
-            else
+            if (singleLabel != null)
+                t = t.inE(singleLabel);
+            else {
                 t = t.inE();
+                // apply multi-label constraint after inE() if it's a 
collection
+                if (hasLabelConstraint && singleLabel == null)
+                    t = applyLabelConstraint(t, labelValue);
+            }
 
         } else {
 
             // g.E().hasLabel(label)
             t = graph.traversal().E();
-            if (edgeLabel != null)
-                t = t.hasLabel(edgeLabel);
+            t = applyLabelConstraint(t, labelValue);
 
         }
 
@@ -222,6 +234,26 @@ public class MergeEdgeStep<S> extends MergeElementStep<S, 
Edge, Map<Object, Obje
         return CloseableIterator.of(t);
     }
 
+    /**
+     * Apply label constraint(s) to the traversal. Supports both single String 
labels and
+     * Collection labels with AND semantics (edge must have ALL specified 
labels).
+     */
+    protected GraphTraversal applyLabelConstraint(GraphTraversal t, final 
Object labelValue) {
+        if (labelValue == null) {
+            return t;
+        }
+        if (labelValue instanceof String) {
+            return t.hasLabel((String) labelValue);
+        } else if (labelValue instanceof java.util.Collection) {
+            // Multi-label: AND semantics - must have ALL specified labels
+            for (final Object label : (java.util.Collection<?>) labelValue) {
+                t = t.hasLabel((String) label);
+            }
+            return t;
+        }
+        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);
@@ -279,14 +311,34 @@ public class MergeEdgeStep<S> extends MergeElementStep<S, 
Edge, Map<Object, Obje
                 traverser.set((S) e);
 
                 // assume good input from GraphTraversal - folks might drop in 
a T here even though it is immutable
-                final Map<String, ?> onMatchMap = materializeMap(traverser, 
onMatchTraversal);
+                final Map onMatchMap = materializeMap(traverser, 
onMatchTraversal);
                 validateMapInput(onMatchMap, true);
 
                 onMatchMap.forEach((key, value) -> {
+                    // Handle T.label replacement for multi-label support
+                    if (T.label.equals(key) || 
T.label.getAccessor().equals(key)) {
+                        // Drop all existing labels and replace with new ones
+                        e.dropLabels();
+                        if (value instanceof String) {
+                            e.addLabel((String) value);
+                        } else if (value instanceof java.util.Collection) {
+                            final java.util.Collection<?> labels = 
(java.util.Collection<?>) value;
+                            if (!labels.isEmpty()) {
+                                final String[] labelArray = labels.stream()
+                                        .map(l -> (String) l)
+                                        .toArray(String[]::new);
+                                e.addLabel(labelArray[0],
+                                        Arrays.copyOfRange(labelArray, 1, 
labelArray.length));
+                            }
+                            // Empty collection = use default label behavior 
(already handled by dropLabels())
+                        }
+                        return;
+                    }
+
                     // trigger callbacks for eventing - in this case, it's a 
EdgePropertyChangedEvent. if there's no
                     // registry/callbacks then just set the property
-                    EventUtil.registerEdgePropertyChange(callbackRegistry, 
getTraversal(), e, key, value);
-                    e.property(key, value);
+                    EventUtil.registerEdgePropertyChange(callbackRegistry, 
getTraversal(), e, (String) key, value);
+                    e.property((String) key, value);
                 });
 
             });
@@ -317,7 +369,30 @@ public class MergeEdgeStep<S> extends MergeElementStep<S, 
Edge, Map<Object, Obje
 
         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 Object labelValue = onCreateMap.getOrDefault(T.label, 
Edge.DEFAULT_LABEL);
+        final String primaryLabel;
+        final java.util.Collection<String> additionalLabels;
+
+        if (labelValue instanceof String) {
+            primaryLabel = (String) labelValue;
+            additionalLabels = Collections.emptyList();
+        } else if (labelValue instanceof java.util.Collection) {
+            final java.util.Collection<?> labelCollection = 
(java.util.Collection<?>) labelValue;
+            if (labelCollection.isEmpty()) {
+                primaryLabel = Edge.DEFAULT_LABEL;
+                additionalLabels = Collections.emptyList();
+            } else {
+                final Iterator<?> iter = labelCollection.iterator();
+                primaryLabel = (String) iter.next();
+                additionalLabels = new ArrayList<>();
+                while (iter.hasNext()) {
+                    additionalLabels.add((String) iter.next());
+                }
+            }
+        } else {
+            primaryLabel = Edge.DEFAULT_LABEL;
+            additionalLabels = Collections.emptyList();
+        }
 
         final List<Object> properties = new ArrayList<>();
 
@@ -329,7 +404,13 @@ public class MergeEdgeStep<S> extends MergeElementStep<S, 
Edge, Map<Object, Obje
             properties.add(e.getValue());
         }
 
-        final Edge edge = fromV.addEdge(label, toV, properties.toArray());
+        final Edge edge = fromV.addEdge(primaryLabel, toV, 
properties.toArray());
+
+        // add any additional labels from a multi-label collection
+        if (!additionalLabels.isEmpty()) {
+            final String[] extraLabels = additionalLabels.toArray(new 
String[0]);
+            edge.addLabel(extraLabels[0], Arrays.copyOfRange(extraLabels, 1, 
extraLabels.length));
+        }
 
         // trigger callbacks for eventing - in this case, it's a 
VertexAddedEvent
         
EventUtil.registerEdgeCreationWithGenericEventRegistry(callbackRegistry, 
getTraversal(), edge);
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/PropertyMapStep.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/PropertyMapStep.java
index 25bc143bc3..665ea10e4b 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/PropertyMapStep.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/PropertyMapStep.java
@@ -25,7 +25,6 @@ import 
org.apache.tinkerpop.gremlin.process.traversal.step.Configuring;
 import org.apache.tinkerpop.gremlin.process.traversal.step.TraversalParent;
 import org.apache.tinkerpop.gremlin.process.traversal.step.util.Parameters;
 import org.apache.tinkerpop.gremlin.process.traversal.step.util.WithOptions;
-import 
org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.OptionsStrategy;
 import 
org.apache.tinkerpop.gremlin.process.traversal.traverser.TraverserRequirement;
 import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalProduct;
 import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalUtil;
@@ -220,10 +219,7 @@ public class PropertyMapStep<K,E> extends 
ScalarMapStep<Element, Map<K, E>>
      * or source-level {@code g.with("multilabel")}.
      */
     private boolean isMultilabelEnabled() {
-        if (this.multilabel) return true;
-        return 
getTraversal().getStrategies().getStrategy(OptionsStrategy.class)
-                .map(os -> os.getOptions().containsKey("multilabel") || 
os.getOptions().containsKey(WithOptions.multilabel))
-                .orElse(false);
+        return WithOptions.isMultilabelEnabled(this.multilabel, 
getTraversal());
     }
 
     protected Object getElementId(Element element){
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/AddLabelStep.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/AddLabelStep.java
index c239623601..1cb2d01108 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/AddLabelStep.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/AddLabelStep.java
@@ -26,7 +26,6 @@ import 
org.apache.tinkerpop.gremlin.process.traversal.step.util.event.CallbackRe
 import org.apache.tinkerpop.gremlin.process.traversal.step.util.event.Event;
 import 
org.apache.tinkerpop.gremlin.process.traversal.step.util.event.EventUtil;
 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.Element;
@@ -36,9 +35,9 @@ import 
org.apache.tinkerpop.gremlin.structure.util.StringFactory;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Objects;
-import java.util.Optional;
 import java.util.Set;
 
 /**
@@ -48,11 +47,11 @@ import java.util.Set;
  * @since 4.0.0
  */
 public class AddLabelStep<S extends Element> extends SideEffectStep<S>
-        implements Mutating<Event.ElementPropertyChangedEvent>, 
TraversalParent {
+        implements Mutating<Event.ElementLabelChangedEvent>, TraversalParent {
 
     private final String[] labels;
     private Traversal.Admin<S, String> labelTraversal;
-    private CallbackRegistry<Event.ElementPropertyChangedEvent> 
callbackRegistry;
+    private CallbackRegistry<Event.ElementLabelChangedEvent> callbackRegistry;
 
     public AddLabelStep(final Traversal.Admin traversal, final String label, 
final String... moreLabels) {
         super(traversal);
@@ -76,6 +75,7 @@ public class AddLabelStep<S extends Element> extends 
SideEffectStep<S>
     @Override
     protected void sideEffect(final Traverser.Admin<S> traverser) {
         final Element element = traverser.get();
+        final Set<String> oldLabels = new LinkedHashSet<>(element.labels());
 
         if (this.labelTraversal != null) {
             final List<String> collectedLabels = new ArrayList<>();
@@ -93,16 +93,14 @@ public class AddLabelStep<S extends Element> extends 
SideEffectStep<S>
                     Arrays.copyOfRange(this.labels, 1, this.labels.length));
         }
 
-        // trigger event callbacks
-        final Optional<EventStrategy> optEventStrategy = 
getTraversal().getStrategies().getStrategy(EventStrategy.class);
-        if (EventUtil.hasAnyCallbacks(callbackRegistry) && 
optEventStrategy.isPresent()) {
-            final EventStrategy es = optEventStrategy.get();
-            EventUtil.registerPropertyChange(callbackRegistry, es, element, 
null, null, new Object[0]);
+        // trigger event callbacks only if labels actually changed
+        if (!oldLabels.equals(element.labels())) {
+            EventUtil.registerLabelChange(callbackRegistry, getTraversal(), 
element, oldLabels, element.labels());
         }
     }
 
     @Override
-    public CallbackRegistry<Event.ElementPropertyChangedEvent> 
getMutatingCallbackRegistry() {
+    public CallbackRegistry<Event.ElementLabelChangedEvent> 
getMutatingCallbackRegistry() {
         if (null == callbackRegistry) callbackRegistry = new 
ListCallbackRegistry<>();
         return callbackRegistry;
     }
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/DropLabelsStep.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/DropLabelsStep.java
index 14e5d0634f..9812b0b786 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/DropLabelsStep.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/DropLabelsStep.java
@@ -26,7 +26,6 @@ import 
org.apache.tinkerpop.gremlin.process.traversal.step.util.event.CallbackRe
 import org.apache.tinkerpop.gremlin.process.traversal.step.util.event.Event;
 import 
org.apache.tinkerpop.gremlin.process.traversal.step.util.event.EventUtil;
 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.Element;
@@ -35,8 +34,8 @@ import 
org.apache.tinkerpop.gremlin.structure.util.StringFactory;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.LinkedHashSet;
 import java.util.List;
-import java.util.Optional;
 import java.util.Set;
 
 /**
@@ -46,12 +45,12 @@ import java.util.Set;
  * @since 4.0.0
  */
 public class DropLabelsStep<S extends Element> extends SideEffectStep<S>
-        implements Mutating<Event.ElementPropertyChangedEvent>, 
TraversalParent {
+        implements Mutating<Event.ElementLabelChangedEvent>, TraversalParent {
 
     private final boolean dropAll;
     private final String[] labels;
     private Traversal.Admin<S, String> labelTraversal;
-    private CallbackRegistry<Event.ElementPropertyChangedEvent> 
callbackRegistry;
+    private CallbackRegistry<Event.ElementLabelChangedEvent> callbackRegistry;
 
     /**
      * Constructor for dropLabels() - removes all labels.
@@ -89,6 +88,7 @@ public class DropLabelsStep<S extends Element> extends 
SideEffectStep<S>
     @Override
     protected void sideEffect(final Traverser.Admin<S> traverser) {
         final Element element = traverser.get();
+        final Set<String> oldLabels = new LinkedHashSet<>(element.labels());
 
         if (this.labelTraversal != null) {
             final String label = TraversalUtil.apply(traverser, 
this.labelTraversal);
@@ -102,16 +102,14 @@ public class DropLabelsStep<S extends Element> extends 
SideEffectStep<S>
                     Arrays.copyOfRange(this.labels, 1, this.labels.length));
         }
 
-        // trigger event callbacks
-        final Optional<EventStrategy> optEventStrategy = 
getTraversal().getStrategies().getStrategy(EventStrategy.class);
-        if (EventUtil.hasAnyCallbacks(callbackRegistry) && 
optEventStrategy.isPresent()) {
-            final EventStrategy es = optEventStrategy.get();
-            EventUtil.registerPropertyChange(callbackRegistry, es, element, 
null, null, new Object[0]);
+        // trigger event callbacks only if labels actually changed
+        if (!oldLabels.equals(element.labels())) {
+            EventUtil.registerLabelChange(callbackRegistry, getTraversal(), 
element, oldLabels, element.labels());
         }
     }
 
     @Override
-    public CallbackRegistry<Event.ElementPropertyChangedEvent> 
getMutatingCallbackRegistry() {
+    public CallbackRegistry<Event.ElementLabelChangedEvent> 
getMutatingCallbackRegistry() {
         if (null == callbackRegistry) callbackRegistry = new 
ListCallbackRegistry<>();
         return callbackRegistry;
     }
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/WithOptions.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/WithOptions.java
index 9be8e4f939..8b7df79f02 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/WithOptions.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/WithOptions.java
@@ -18,8 +18,10 @@
  */
 package org.apache.tinkerpop.gremlin.process.traversal.step.util;
 
+import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
 import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal;
 import org.apache.tinkerpop.gremlin.process.traversal.step.map.IndexStep;
+import 
org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.OptionsStrategy;
 import org.apache.tinkerpop.gremlin.structure.Edge;
 import org.apache.tinkerpop.gremlin.structure.Element;
 import org.apache.tinkerpop.gremlin.structure.Graph;
@@ -102,4 +104,20 @@ public class WithOptions {
      * @since 4.0.0
      */
     public static final String multilabel = 
Graph.Hidden.hide("tinkerpop.multilabel");
+
+    /**
+     * The user-facing key for multilabel configuration used with {@code 
g.with("multilabel")}.
+     */
+    public static final String MULTILABEL_KEY = "multilabel";
+
+    /**
+     * Checks whether multi-label output is enabled for the given traversal, 
either at the step level
+     * (via step-level configuration) or at the source level (via {@code 
g.with("multilabel")}).
+     */
+    public static boolean isMultilabelEnabled(final boolean stepLevelEnabled, 
final Traversal.Admin<?, ?> traversal) {
+        if (stepLevelEnabled) return true;
+        return traversal.getStrategies().getStrategy(OptionsStrategy.class)
+                .map(os -> os.getOptions().containsKey(MULTILABEL_KEY) || 
os.getOptions().containsKey(multilabel))
+                .orElse(false);
+    }
 }
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/event/Event.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/event/Event.java
index 4a9d9c06e6..f02e6bc63f 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/event/Event.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/event/Event.java
@@ -27,6 +27,7 @@ import org.apache.tinkerpop.gremlin.structure.Vertex;
 import org.apache.tinkerpop.gremlin.structure.VertexProperty;
 
 import java.util.Iterator;
+import java.util.Set;
 
 /**
  * A representation of some action that occurs on a {@link Graph} for a {@link 
Traversal}.
@@ -213,6 +214,31 @@ public interface Event {
         }
     }
 
+    /**
+     * An event that represents a label change on an element (vertex or edge).
+     * Fired when labels are added or removed via 
addLabel()/dropLabel()/dropLabels() steps.
+     */
+    class ElementLabelChangedEvent implements Event {
+        private final Element element;
+        private final Set<String> oldLabels;
+        private final Set<String> newLabels;
+
+        public ElementLabelChangedEvent(final Element element, final 
Set<String> oldLabels, final Set<String> newLabels) {
+            this.element = element;
+            this.oldLabels = oldLabels;
+            this.newLabels = newLabels;
+        }
+
+        public Element getElement() { return element; }
+        public Set<String> getOldLabels() { return oldLabels; }
+        public Set<String> getNewLabels() { return newLabels; }
+
+        @Override
+        public void fireEvent(final Iterator<MutationListener> eventListeners) 
{
+            // Label change events are dispatched directly via 
CallbackRegistry, not MutationListener.
+        }
+    }
+
     /**
      * A base class for {@link Property} mutation events.
      */
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/event/EventUtil.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/event/EventUtil.java
index 2fce97e150..85d8072f89 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/event/EventUtil.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/event/EventUtil.java
@@ -27,6 +27,8 @@ import org.apache.tinkerpop.gremlin.structure.Property;
 import org.apache.tinkerpop.gremlin.structure.Vertex;
 import org.apache.tinkerpop.gremlin.structure.VertexProperty;
 
+import java.util.Set;
+
 /**
  * Logic for registering events with the {@link EventStrategy} and the 
callback registry. Extracting this logic allows
  * providers to reuse these utilities more readily.
@@ -202,6 +204,24 @@ public final class EventUtil {
         }
     }
 
+    /**
+     * Register a label change event with the callback registry.
+     */
+    @SuppressWarnings("unchecked")
+    public static void registerLabelChange(final 
CallbackRegistry<Event.ElementLabelChangedEvent> callbackRegistry,
+                                           final Traversal.Admin<?, ?> 
traversal, final Element element,
+                                           final Set<String> oldLabels, final 
Set<String> newLabels) {
+        if (hasAnyCallbacks(callbackRegistry)) {
+            final EventStrategy eventStrategy = ((Traversal.Admin<Object, 
Object>) traversal)
+                    
.getStrategies().getStrategy(EventStrategy.class).orElse(null);
+            final Element detachedElement = eventStrategy != null ? 
eventStrategy.detach(element) : element;
+            final Event.ElementLabelChangedEvent event = new 
Event.ElementLabelChangedEvent(detachedElement, oldLabels, newLabels);
+            for (EventCallback<Event.ElementLabelChangedEvent> c : 
callbackRegistry.getCallbacks()) {
+                c.accept(event);
+            }
+        }
+    }
+
     /**
      * Register a vertex property addition event with the callback registry.
      */
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/EdgeSerializer.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/EdgeSerializer.java
index 6d614d5126..fe48d6f6ae 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/EdgeSerializer.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/EdgeSerializer.java
@@ -30,8 +30,11 @@ import 
org.apache.tinkerpop.gremlin.structure.util.detached.DetachedVertex;
 import org.apache.tinkerpop.gremlin.structure.util.reference.ReferenceEdge;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Collections;
+import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Set;
 
 /**
  * @author Stephen Mallette (http://stephen.genoprime.com)
@@ -44,25 +47,43 @@ public class EdgeSerializer extends 
SimpleTypeSerializer<Edge> {
     @Override
     protected Edge readValue(final Buffer buffer, final GraphBinaryReader 
context) throws IOException {
         final Object id = context.read(buffer);
-        // reading single string value for now according to GraphBinaryV4
-        final String label = (String) context.readValue(buffer, List.class, 
false).get(0);
+        // Read all labels as List<String> for multi-label support
+        final List<String> labelList = context.readValue(buffer, List.class, 
false);
 
         final Object inVId = context.read(buffer);
-        // reading single string value for now according to GraphBinaryV4
-        final String inVLabel = (String) context.readValue(buffer, List.class, 
false).get(0);
+        // Read all inVertex labels as List<String> for multi-label support
+        final List<String> inVLabelList = context.readValue(buffer, 
List.class, false);
         final Object outVId = context.read(buffer);
-        // reading single string value for now according to GraphBinaryV4
-        final String outVLabel = (String) context.readValue(buffer, 
List.class, false).get(0);
+        // Read all outVertex labels as List<String> for multi-label support
+        final List<String> outVLabelList = context.readValue(buffer, 
List.class, false);
 
         // discard the parent vertex
         context.read(buffer);
 
         final List<Property> properties = context.read(buffer);
 
-        final DetachedVertex inV = 
DetachedVertex.build().setId(inVId).setLabel(inVLabel).create();
-        final DetachedVertex outV = 
DetachedVertex.build().setId(outVId).setLabel(outVLabel).create();
+        final DetachedVertex.Builder inVBuilder = 
DetachedVertex.build().setId(inVId);
+        if (inVLabelList.size() == 1) {
+            inVBuilder.setLabel(inVLabelList.get(0));
+        } else if (!inVLabelList.isEmpty()) {
+            inVBuilder.setLabels(new LinkedHashSet<>(inVLabelList));
+        }
+        final DetachedVertex inV = inVBuilder.create();
+
+        final DetachedVertex.Builder outVBuilder = 
DetachedVertex.build().setId(outVId);
+        if (outVLabelList.size() == 1) {
+            outVBuilder.setLabel(outVLabelList.get(0));
+        } else if (!outVLabelList.isEmpty()) {
+            outVBuilder.setLabels(new LinkedHashSet<>(outVLabelList));
+        }
+        final DetachedVertex outV = outVBuilder.create();
 
-        final DetachedEdge.Builder builder = 
DetachedEdge.build().setId(id).setLabel(label).setInV(inV).setOutV(outV);
+        final DetachedEdge.Builder builder = 
DetachedEdge.build().setId(id).setInV(inV).setOutV(outV);
+        if (labelList.size() == 1) {
+            builder.setLabel(labelList.get(0));
+        } else if (!labelList.isEmpty()) {
+            builder.setLabels(new LinkedHashSet<>(labelList));
+        }
 
         if (properties != null) {
             for (final Property p : properties) {
@@ -77,16 +98,28 @@ public class EdgeSerializer extends 
SimpleTypeSerializer<Edge> {
     protected void writeValue(final Edge value, final Buffer buffer, final 
GraphBinaryWriter context) throws IOException {
 
         context.write(value.id(), buffer);
-        // wrapping label into list here for now according to GraphBinaryV4, 
but we aren't allowing null label yet
-        if (value.label() == null) {
-            throw new IOException("Unexpected null value when nullable is 
false");
+        // Write all edge labels as List<String> for multi-label support
+        final Set<String> edgeLabels = value.labels();
+        if (edgeLabels == null || edgeLabels.isEmpty()) {
+            throw new IOException("Unexpected null or empty labels when 
nullable is false");
         }
-        context.writeValue(Collections.singletonList(value.label()), buffer, 
false);
+        context.writeValue(new ArrayList<>(edgeLabels), buffer, false);
 
         context.write(value.inVertex().id(), buffer);
-        
context.writeValue(Collections.singletonList(value.inVertex().label()), buffer, 
false);
+        // Write all inVertex labels as List<String> for multi-label support
+        final Set<String> inVLabels = value.inVertex().labels();
+        if (inVLabels == null || inVLabels.isEmpty()) {
+            throw new IOException("Unexpected null or empty labels when 
nullable is false");
+        }
+        context.writeValue(new ArrayList<>(inVLabels), buffer, false);
+
         context.write(value.outVertex().id(), buffer);
-        
context.writeValue(Collections.singletonList(value.outVertex().label()), 
buffer, false);
+        // Write all outVertex labels as List<String> for multi-label support
+        final Set<String> outVLabels = value.outVertex().labels();
+        if (outVLabels == null || outVLabels.isEmpty()) {
+            throw new IOException("Unexpected null or empty labels when 
nullable is false");
+        }
+        context.writeValue(new ArrayList<>(outVLabels), buffer, false);
 
         // we don't serialize the parent Vertex for edges.
         context.write(null, buffer);
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONSerializersV4.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONSerializersV4.java
index bc1962f8db..afd27256fc 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONSerializersV4.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONSerializersV4.java
@@ -146,7 +146,7 @@ class GraphSONSerializersV4 {
             jsonGenerator.writeStartObject();
 
             jsonGenerator.writeObjectField(GraphSONTokens.ID, edge.id());
-            writeLabel(jsonGenerator, GraphSONTokens.LABEL, edge.label());
+            writeLabels(jsonGenerator, GraphSONTokens.LABEL, edge.labels());
             writeTypeForGraphObjectIfUntyped(jsonGenerator, typeInfo, 
GraphSONTokens.EDGE);
             writeVertex(GraphSONTokens.IN, edge.inVertex(), jsonGenerator);
             writeVertex(GraphSONTokens.OUT, edge.outVertex(), jsonGenerator);
@@ -160,7 +160,7 @@ class GraphSONSerializersV4 {
             jsonGenerator.writeFieldName(vertexDirection);
             jsonGenerator.writeStartObject();
             jsonGenerator.writeObjectField(GraphSONTokens.ID, v.id());
-            writeLabel(jsonGenerator, GraphSONTokens.LABEL, v.label());
+            writeLabels(jsonGenerator, GraphSONTokens.LABEL, v.labels());
             jsonGenerator.writeEndObject();
         }
 
@@ -439,8 +439,14 @@ class GraphSONSerializersV4 {
                     e.setId(deserializationContext.readValue(jsonParser, 
Object.class));
                 } else if 
(jsonParser.getCurrentName().equals(GraphSONTokens.LABEL)) {
                     jsonParser.nextToken();
+                    final java.util.Set<String> labels = new 
java.util.LinkedHashSet<>();
                     while (jsonParser.nextToken() != JsonToken.END_ARRAY) {
-                        e.setLabel(jsonParser.getText());
+                        labels.add(jsonParser.getText());
+                    }
+                    if (labels.size() == 1) {
+                        e.setLabel(labels.iterator().next());
+                    } else if (!labels.isEmpty()) {
+                        e.setLabels(labels);
                     }
                 } else if 
(jsonParser.getCurrentName().equals(GraphSONTokens.OUT)) {
                     jsonParser.nextToken();
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/detached/DetachedEdge.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/detached/DetachedEdge.java
index 2c91afb615..77f53876d4 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/detached/DetachedEdge.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/detached/DetachedEdge.java
@@ -30,8 +30,10 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Iterator;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * Represents an {@link Edge} that is disconnected from a {@link Graph}.  
"Disconnection" can mean detachment from
@@ -51,6 +53,7 @@ public class DetachedEdge extends DetachedElement<Edge> 
implements Edge {
 
     private DetachedVertex outVertex;
     private DetachedVertex inVertex;
+    private Set<String> edgeLabels;
 
     private DetachedEdge() {}
 
@@ -59,6 +62,12 @@ public class DetachedEdge extends DetachedElement<Edge> 
implements Edge {
         this.outVertex = DetachedFactory.detach(edge.outVertex(), false);
         this.inVertex = DetachedFactory.detach(edge.inVertex(), false);
 
+        // Capture all labels from the source edge for multi-label support.
+        final Set<String> srcLabels = edge.labels();
+        if (srcLabels.size() > 1) {
+            this.edgeLabels = new LinkedHashSet<>(srcLabels);
+        }
+
         // only serialize properties if requested, the graph supports it and 
there are meta properties present.
         // this prevents unnecessary object creation of a new HashMap of a new 
HashMap which will just be empty.
         // it will use Collections.emptyMap() by default
@@ -113,6 +122,23 @@ public class DetachedEdge extends DetachedElement<Edge> 
implements Edge {
         return StringFactory.edgeString(this);
     }
 
+    @Override
+    public Set<String> labels() {
+        if (this.edgeLabels != null) {
+            return Collections.unmodifiableSet(this.edgeLabels);
+        }
+        // Fall back to single label from parent
+        return this.label != null ? Collections.singleton(this.label) : 
Collections.emptySet();
+    }
+
+    @Override
+    public String label() {
+        if (this.edgeLabels != null && !this.edgeLabels.isEmpty()) {
+            return this.edgeLabels.iterator().next();
+        }
+        return this.label != null ? this.label : Edge.DEFAULT_LABEL;
+    }
+
     @Override
     public Vertex inVertex() {
         return this.inVertex;
@@ -180,6 +206,14 @@ public class DetachedEdge extends DetachedElement<Edge> 
implements Edge {
             return this;
         }
 
+        public Builder setLabels(final Set<String> labels) {
+            e.edgeLabels = labels != null ? new LinkedHashSet<>(labels) : null;
+            if (labels != null && !labels.isEmpty()) {
+                e.label = labels.iterator().next();
+            }
+            return this;
+        }
+
         public Builder setOutV(final DetachedVertex v) {
             e.outVertex = v;
             return this;
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/reference/ReferenceEdge.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/reference/ReferenceEdge.java
index 6f716022ef..8f797b96b3 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/reference/ReferenceEdge.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/reference/ReferenceEdge.java
@@ -28,6 +28,8 @@ import 
org.apache.tinkerpop.gremlin.util.iterator.IteratorUtils;
 
 import java.util.Collections;
 import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.Set;
 
 /**
  * @author Marko A. Rodriguez (http://markorodriguez.com)
@@ -36,6 +38,7 @@ public class ReferenceEdge extends ReferenceElement<Edge> 
implements Edge {
 
     private ReferenceVertex inVertex;
     private ReferenceVertex outVertex;
+    private Set<String> edgeLabels;
 
     private ReferenceEdge() {
 
@@ -45,6 +48,11 @@ public class ReferenceEdge extends ReferenceElement<Edge> 
implements Edge {
         super(edge);
         this.inVertex = new ReferenceVertex(edge.inVertex());
         this.outVertex = new ReferenceVertex(edge.outVertex());
+        // Capture all labels from the source edge for multi-label support.
+        final Set<String> srcLabels = edge.labels();
+        if (srcLabels.size() > 1) {
+            this.edgeLabels = new LinkedHashSet<>(srcLabels);
+        }
     }
 
     public ReferenceEdge(final Object id, final String label, final 
ReferenceVertex inVertex, final ReferenceVertex outVertex) {
@@ -53,6 +61,29 @@ public class ReferenceEdge extends ReferenceElement<Edge> 
implements Edge {
         this.outVertex = outVertex;
     }
 
+    public ReferenceEdge(final Object id, final Set<String> labels, final 
ReferenceVertex inVertex, final ReferenceVertex outVertex) {
+        super(id, labels != null && !labels.isEmpty() ? 
labels.iterator().next() : Edge.DEFAULT_LABEL);
+        this.edgeLabels = labels != null ? new LinkedHashSet<>(labels) : null;
+        this.inVertex = inVertex;
+        this.outVertex = outVertex;
+    }
+
+    @Override
+    public Set<String> labels() {
+        if (this.edgeLabels != null) {
+            return Collections.unmodifiableSet(this.edgeLabels);
+        }
+        return this.label != null ? Collections.singleton(this.label) : 
Collections.emptySet();
+    }
+
+    @Override
+    public String label() {
+        if (this.edgeLabels != null && !this.edgeLabels.isEmpty()) {
+            return this.edgeLabels.iterator().next();
+        }
+        return this.label != null ? this.label : Edge.DEFAULT_LABEL;
+    }
+
     @Override
     public <V> Property<V> property(final String key, final V value) {
         throw Element.Exceptions.propertyAdditionNotSupported();
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/star/StarGraph.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/star/StarGraph.java
index 8daaf64aef..1981a3257b 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/star/StarGraph.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/star/StarGraph.java
@@ -43,6 +43,7 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.NoSuchElementException;
@@ -206,6 +207,12 @@ public final class StarGraph implements Graph, 
Serializable {
         final StarGraph starGraph = new StarGraph();
         final StarVertex starVertex = (StarVertex) starGraph.addVertex(T.id, 
vertex.id(), T.label, vertex.label());
 
+        // Copy multi-labels from source vertex
+        final Set<String> srcLabels = vertex.labels();
+        if (srcLabels.size() > 1) {
+            starVertex.setLabels(srcLabels);
+        }
+
         final boolean supportsMetaProperties = 
vertex.graph().features().vertex().supportsMetaProperties();
 
         vertex.properties().forEachRemaining(vp -> {
@@ -216,11 +223,19 @@ public final class StarGraph implements Graph, 
Serializable {
         vertex.edges(Direction.IN).forEachRemaining(edge -> {
             final Edge starEdge = starVertex.addInEdge(edge.label(), 
starGraph.addVertex(T.id, edge.outVertex().id()), T.id, edge.id());
             edge.properties().forEachRemaining(p -> starEdge.property(p.key(), 
p.value()));
+            final Set<String> edgeSrcLabels = edge.labels();
+            if (edgeSrcLabels.size() > 1) {
+                ((StarGraph.StarEdge) starEdge).setLabels(edgeSrcLabels);
+            }
         });
 
         vertex.edges(Direction.OUT).forEachRemaining(edge -> {
             final Edge starEdge = starVertex.addOutEdge(edge.label(), 
starGraph.addVertex(T.id, edge.inVertex().id()), T.id, edge.id());
             edge.properties().forEachRemaining(p -> starEdge.property(p.key(), 
p.value()));
+            final Set<String> edgeSrcLabels = edge.labels();
+            if (edgeSrcLabels.size() > 1) {
+                ((StarGraph.StarEdge) starEdge).setLabels(edgeSrcLabels);
+            }
         });
         return starGraph;
     }
@@ -308,7 +323,7 @@ public final class StarGraph implements Graph, Serializable 
{
     public abstract class StarElement<E extends Element> implements Element, 
Attachable<E> {
 
         protected final Object id;
-        protected final String label;
+        protected String label;
 
         protected StarElement(final Object id, final String label) {
             this.id = id;
@@ -352,12 +367,35 @@ public final class StarGraph implements Graph, 
Serializable {
 
     public final class StarVertex extends StarElement<Vertex> implements 
Vertex {
 
+        private Set<String> vertexLabels;
         protected Map<String, List<Edge>> outEdges = null;
         protected Map<String, List<Edge>> inEdges = null;
         protected Map<String, List<VertexProperty>> vertexProperties = null;
 
         public StarVertex(final Object id, final String label) {
             super(id, label);
+            this.vertexLabels = new LinkedHashSet<>();
+            this.vertexLabels.add(internStrings ? label.intern() : label);
+        }
+
+        @Override
+        public Set<String> labels() {
+            return Collections.unmodifiableSet(this.vertexLabels);
+        }
+
+        @Override
+        public String label() {
+            return this.vertexLabels.isEmpty() ? this.label : 
this.vertexLabels.iterator().next();
+        }
+
+        /**
+         * Sets the vertex labels from a source set. Used when copying labels 
from a multi-label vertex.
+         */
+        public void setLabels(final Set<String> labels) {
+            if (labels != null && !labels.isEmpty()) {
+                this.vertexLabels = new LinkedHashSet<>(labels);
+                this.label = this.vertexLabels.iterator().next();
+            }
         }
 
         private void dropEdgeProperty(Object id) {
@@ -774,11 +812,34 @@ public final class StarGraph implements Graph, 
Serializable {
 
     public abstract class StarEdge extends StarElement<Edge> implements Edge {
 
+        private Set<String> edgeLabels;
         protected final Object otherId;
 
         private StarEdge(final Object id, final String label, final Object 
otherId) {
             super(id, label);
             this.otherId = otherId;
+            this.edgeLabels = new LinkedHashSet<>();
+            this.edgeLabels.add(internStrings ? label.intern() : label);
+        }
+
+        @Override
+        public Set<String> labels() {
+            return Collections.unmodifiableSet(this.edgeLabels);
+        }
+
+        @Override
+        public String label() {
+            return this.edgeLabels.isEmpty() ? this.label : 
this.edgeLabels.iterator().next();
+        }
+
+        /**
+         * Sets the edge labels from a source set. Used when copying labels 
from a multi-label edge.
+         */
+        public void setLabels(final Set<String> labels) {
+            if (labels != null && !labels.isEmpty()) {
+                this.edgeLabels = new LinkedHashSet<>(labels);
+                this.label = this.edgeLabels.iterator().next();
+            }
         }
 
         @Override
diff --git a/gremlin-language/src/main/antlr4/Gremlin.g4 
b/gremlin-language/src/main/antlr4/Gremlin.g4
index af76bc0593..2d7fa316a6 100644
--- a/gremlin-language/src/main/antlr4/Gremlin.g4
+++ b/gremlin-language/src/main/antlr4/Gremlin.g4
@@ -111,7 +111,9 @@ traversalSourceSpawnMethod
     ;
 
 traversalSourceSpawnMethod_addE
-    : K_ADDE LPAREN stringArgument RPAREN
+    : K_ADDE LPAREN RPAREN
+    | K_ADDE LPAREN stringArgument RPAREN
+    | K_ADDE LPAREN stringArgument COMMA stringArgument (COMMA 
stringArgument)* RPAREN
     | K_ADDE LPAREN nestedTraversal RPAREN
     ;
 
@@ -325,7 +327,9 @@ traversalMethod_E
     ;
 
 traversalMethod_addE
-    : K_ADDE LPAREN stringArgument RPAREN #traversalMethod_addE_String
+    : K_ADDE LPAREN RPAREN #traversalMethod_addE_Empty
+    | K_ADDE LPAREN stringArgument RPAREN #traversalMethod_addE_String
+    | K_ADDE LPAREN stringArgument (COMMA stringArgument)+ RPAREN 
#traversalMethod_addE_StringVarargs
     | K_ADDE LPAREN nestedTraversal RPAREN #traversalMethod_addE_Traversal
     ;
 
diff --git 
a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/AbstractTinkerGraph.java
 
b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/AbstractTinkerGraph.java
index cf9f72bd14..b2b5b99281 100644
--- 
a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/AbstractTinkerGraph.java
+++ 
b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/AbstractTinkerGraph.java
@@ -292,6 +292,26 @@ public abstract class AbstractTinkerGraph implements Graph 
{
 
     protected abstract void addInEdge(final TinkerVertex vertex, final String 
label, final Edge edge);
 
+    /**
+     * Add an edge to the per-vertex adjacency maps under the given label.
+     * Used when labels are added to an existing edge.
+     *
+     * @param edge  the edge to register
+     * @param label the label under which to register the edge
+     * @since 4.0.0
+     */
+    public abstract void addEdgeToAdjacency(final TinkerEdge edge, final 
String label);
+
+    /**
+     * Remove an edge from the per-vertex adjacency maps for the given label.
+     * Used when labels are removed from an existing edge.
+     *
+     * @param edge  the edge to unregister
+     * @param label the label from which to unregister the edge
+     * @since 4.0.0
+     */
+    public abstract void removeEdgeFromAdjacency(final TinkerEdge edge, final 
String label);
+
     /**
      * Called when a vertex's labels are modified to allow the graph to update 
any internal label indices.
      * The default implementation is a no-op since TinkerGraph does not 
maintain a separate label index.
diff --git 
a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerEdge.java
 
b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerEdge.java
index 3ced83454e..b37913a0aa 100644
--- 
a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerEdge.java
+++ 
b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerEdge.java
@@ -30,6 +30,7 @@ import 
org.apache.tinkerpop.gremlin.util.iterator.IteratorUtils;
 
 import java.util.Collections;
 import java.util.Iterator;
+import java.util.LinkedHashSet;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
@@ -40,6 +41,7 @@ import java.util.stream.Collectors;
  */
 public class TinkerEdge extends TinkerElement implements Edge {
 
+    protected final Set<String> edgeLabels;
     protected Map<String, Property> properties;
 
     protected Vertex inVertex = null;
@@ -62,8 +64,30 @@ public class TinkerEdge extends TinkerElement implements 
Edge {
         }
     }
 
+    protected TinkerEdge(final Object id, final Vertex outVertex, final 
Set<String> labels, final Vertex inVertex) {
+        this(id, outVertex, labels, inVertex, 0);
+    }
+
+    protected TinkerEdge(final Object id, final Vertex outVertex, final 
Set<String> labels, final Vertex inVertex, final long currentVersion) {
+        this(id, (AbstractTinkerGraph) outVertex.graph(), outVertex.id(),
+             (labels == null || labels.isEmpty()) ? Edge.DEFAULT_LABEL : 
labels.iterator().next(),
+             inVertex.id(), currentVersion, false);
+        this.edgeLabels.clear();
+        if (labels != null && !labels.isEmpty()) {
+            this.edgeLabels.addAll(labels);
+        } else {
+            this.edgeLabels.add(Edge.DEFAULT_LABEL);
+        }
+        if (!isTxMode) {
+            this.inVertex = inVertex;
+            this.outVertex = outVertex;
+        }
+    }
+
     private TinkerEdge(final Object id, AbstractTinkerGraph graph, final 
Object outVertexId, final String label, final Object inVertexId, final long 
currentVersion, final Boolean skipIndexUpdate) {
         super(id, label, currentVersion);
+        this.edgeLabels = new LinkedHashSet<>();
+        this.edgeLabels.add(label);
         isTxMode = graph instanceof TinkerTransactionGraph;
         this.graph = graph;
         if (isTxMode) {
@@ -107,7 +131,66 @@ public class TinkerEdge extends TinkerElement implements 
Edge {
 
     @Override
     public Set<String> labels() {
-        return Collections.singleton(this.label);
+        return Collections.unmodifiableSet(this.edgeLabels);
+    }
+
+    @Override
+    @Deprecated
+    public String label() {
+        return this.edgeLabels.iterator().next();
+    }
+
+    @Override
+    public void addLabel(final String label, final String... labels) {
+        graph.touch(this);
+        ElementHelper.validateLabel(label);
+        for (final String l : labels) {
+            ElementHelper.validateLabel(l);
+        }
+        // Remove default label if it was the only label
+        if (this.edgeLabels.size() == 1 && 
this.edgeLabels.contains(Edge.DEFAULT_LABEL)) {
+            this.graph.removeEdgeFromAdjacency(this, Edge.DEFAULT_LABEL);
+            this.edgeLabels.remove(Edge.DEFAULT_LABEL);
+        }
+        this.edgeLabels.add(label);
+        this.graph.addEdgeToAdjacency(this, label);
+        for (final String l : labels) {
+            this.edgeLabels.add(l);
+            this.graph.addEdgeToAdjacency(this, l);
+        }
+        this.label = this.edgeLabels.iterator().next();
+    }
+
+    @Override
+    public void dropLabels() {
+        graph.touch(this);
+        for (final String l : new LinkedHashSet<>(this.edgeLabels)) {
+            this.graph.removeEdgeFromAdjacency(this, l);
+        }
+        this.edgeLabels.clear();
+        this.edgeLabels.add(Edge.DEFAULT_LABEL);
+        this.graph.addEdgeToAdjacency(this, Edge.DEFAULT_LABEL);
+        this.label = this.edgeLabels.iterator().next();
+    }
+
+    @Override
+    public void dropLabel(final String label, final String... labels) {
+        graph.touch(this);
+        if (this.edgeLabels.contains(label)) {
+            this.graph.removeEdgeFromAdjacency(this, label);
+            this.edgeLabels.remove(label);
+        }
+        for (final String l : labels) {
+            if (this.edgeLabels.contains(l)) {
+                this.graph.removeEdgeFromAdjacency(this, l);
+                this.edgeLabels.remove(l);
+            }
+        }
+        if (this.edgeLabels.isEmpty()) {
+            this.edgeLabels.add(Edge.DEFAULT_LABEL);
+            this.graph.addEdgeToAdjacency(this, Edge.DEFAULT_LABEL);
+        }
+        this.label = this.edgeLabels.iterator().next();
     }
 
     @Override
@@ -129,11 +212,15 @@ public class TinkerEdge extends TinkerElement implements 
Edge {
         if (!isTxMode) {
             // shallow copy for non-tx mode
             final TinkerEdge edge = new TinkerEdge(id, outVertex, label, 
inVertex, currentVersion);
+            edge.edgeLabels.clear();
+            edge.edgeLabels.addAll(this.edgeLabels);
             edge.properties = properties;
             return edge;
         }
 
         final TinkerEdge edge = new TinkerEdge(id, graph, outVertexId, label, 
inVertexId, currentVersion, true);
+        edge.edgeLabels.clear();
+        edge.edgeLabels.addAll(this.edgeLabels);
 
         if (properties != null) {
             final Map<String, Property> cloned = new 
ConcurrentHashMap<>(properties.size());
diff --git 
a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerElement.java
 
b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerElement.java
index 8aa2d1db03..d8587b71fa 100644
--- 
a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerElement.java
+++ 
b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerElement.java
@@ -27,7 +27,7 @@ import 
org.apache.tinkerpop.gremlin.structure.util.ElementHelper;
 public abstract class TinkerElement implements Element {
 
     protected final Object id;
-    protected final String label;
+    protected String label;
     protected boolean removed = false;
     protected long currentVersion;
 
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 f9b3bc4811..b780b71281 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
@@ -303,6 +303,36 @@ public class TinkerGraph extends AbstractTinkerGraph {
         edges.add(edge);
     }
 
+    @Override
+    public void addEdgeToAdjacency(final TinkerEdge edge, final String label) {
+        final TinkerVertex outV = (TinkerVertex) edge.outVertex();
+        final TinkerVertex inV = (TinkerVertex) edge.inVertex();
+        if (null == outV.outEdges) outV.outEdges = new HashMap<>();
+        outV.outEdges.computeIfAbsent(label, k -> new HashSet<>()).add(edge);
+        if (null == inV.inEdges) inV.inEdges = new HashMap<>();
+        inV.inEdges.computeIfAbsent(label, k -> new HashSet<>()).add(edge);
+    }
+
+    @Override
+    public void removeEdgeFromAdjacency(final TinkerEdge edge, final String 
label) {
+        final TinkerVertex outV = (TinkerVertex) edge.outVertex();
+        final TinkerVertex inV = (TinkerVertex) edge.inVertex();
+        if (outV.outEdges != null) {
+            final Set<Edge> edges = outV.outEdges.get(label);
+            if (edges != null) {
+                edges.remove(edge);
+                if (edges.isEmpty()) outV.outEdges.remove(label);
+            }
+        }
+        if (inV.inEdges != null) {
+            final Set<Edge> edges = inV.inEdges.get(label);
+            if (edges != null) {
+                edges.remove(edge);
+                if (edges.isEmpty()) inV.inEdges.remove(label);
+            }
+        }
+    }
+
     /**
      * Return TinkerGraph feature set.
      * <p/>
diff --git 
a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerTransactionGraph.java
 
b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerTransactionGraph.java
index e6673e4be6..ad1c59a511 100644
--- 
a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerTransactionGraph.java
+++ 
b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerTransactionGraph.java
@@ -393,6 +393,40 @@ public final class TinkerTransactionGraph extends 
AbstractTinkerGraph {
         edges.add(edge.id());
     }
 
+    @Override
+    public void addEdgeToAdjacency(final TinkerEdge edge, final String label) {
+        final TinkerVertex outV = (TinkerVertex) edge.outVertex();
+        final TinkerVertex inV = (TinkerVertex) edge.inVertex();
+        touch(outV);
+        touch(inV);
+        if (null == outV.outEdgesId) outV.outEdgesId = new 
ConcurrentHashMap<>();
+        outV.outEdgesId.computeIfAbsent(label, k -> 
ConcurrentHashMap.newKeySet()).add(edge.id());
+        if (null == inV.inEdgesId) inV.inEdgesId = new ConcurrentHashMap<>();
+        inV.inEdgesId.computeIfAbsent(label, k -> 
ConcurrentHashMap.newKeySet()).add(edge.id());
+    }
+
+    @Override
+    public void removeEdgeFromAdjacency(final TinkerEdge edge, final String 
label) {
+        final TinkerVertex outV = (TinkerVertex) edge.outVertex();
+        final TinkerVertex inV = (TinkerVertex) edge.inVertex();
+        touch(outV);
+        touch(inV);
+        if (outV.outEdgesId != null) {
+            final Set<Object> edges = outV.outEdgesId.get(label);
+            if (edges != null) {
+                edges.remove(edge.id());
+                if (edges.isEmpty()) outV.outEdgesId.remove(label);
+            }
+        }
+        if (inV.inEdgesId != null) {
+            final Set<Object> edges = inV.inEdgesId.get(label);
+            if (edges != null) {
+                edges.remove(edge.id());
+                if (edges.isEmpty()) inV.inEdgesId.remove(label);
+            }
+        }
+    }
+
     /**
      * Return TinkerGraph feature set.
      * <p/>
diff --git 
a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertex.java
 
b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertex.java
index 67c5ad4692..f3a97beb13 100644
--- 
a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertex.java
+++ 
b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertex.java
@@ -61,53 +61,31 @@ public class TinkerVertex extends TinkerElement implements 
Vertex {
     protected final Set<String> vertexLabels;
 
     protected TinkerVertex(final Object id, final String label, final 
AbstractTinkerGraph graph) {
-        super(id, label);
-        this.graph = graph;
-        this.isTxMode = graph instanceof TinkerTransactionGraph;
-        this.allowNullPropertyValues = 
graph.features().vertex().supportsNullPropertyValues();
-        this.vertexLabels = new LinkedHashSet<>();
-        this.vertexLabels.add(null == label ? Vertex.DEFAULT_LABEL : label);
+        this(id, Collections.singleton(label != null ? label : 
Vertex.DEFAULT_LABEL), graph, -1);
     }
 
     protected TinkerVertex(final Object id, final String label, final 
AbstractTinkerGraph graph, final long currentVersion) {
-        super(id, label, currentVersion);
-        this.graph = graph;
-        this.isTxMode = graph instanceof TinkerTransactionGraph;
-        this.allowNullPropertyValues = 
graph.features().vertex().supportsNullPropertyValues();
-        this.vertexLabels = new LinkedHashSet<>();
-        this.vertexLabels.add(null == label ? Vertex.DEFAULT_LABEL : label);
+        this(id, Collections.singleton(label != null ? label : 
Vertex.DEFAULT_LABEL), graph, currentVersion);
     }
 
     /**
      * Constructs a TinkerVertex with multiple labels.
      */
     protected TinkerVertex(final Object id, final Set<String> labels, final 
AbstractTinkerGraph graph) {
-        super(id, (labels == null || labels.isEmpty()) ? Vertex.DEFAULT_LABEL 
: labels.iterator().next());
-        this.graph = graph;
-        this.isTxMode = graph instanceof TinkerTransactionGraph;
-        this.allowNullPropertyValues = 
graph.features().vertex().supportsNullPropertyValues();
-        if (labels == null || labels.isEmpty()) {
-            this.vertexLabels = new LinkedHashSet<>();
-            this.vertexLabels.add(Vertex.DEFAULT_LABEL);
-        } else {
-            this.vertexLabels = new LinkedHashSet<>(labels);
-        }
+        this(id, labels, graph, -1);
     }
 
     /**
-     * Constructs a TinkerVertex with multiple labels and a specific version 
(for transactional graphs).
+     * Canonical constructor. Constructs a TinkerVertex with multiple labels 
and a specific version (for transactional graphs).
      */
     protected TinkerVertex(final Object id, final Set<String> labels, final 
AbstractTinkerGraph graph, final long currentVersion) {
         super(id, (labels == null || labels.isEmpty()) ? Vertex.DEFAULT_LABEL 
: labels.iterator().next(), currentVersion);
         this.graph = graph;
         this.isTxMode = graph instanceof TinkerTransactionGraph;
         this.allowNullPropertyValues = 
graph.features().vertex().supportsNullPropertyValues();
-        if (labels == null || labels.isEmpty()) {
-            this.vertexLabels = new LinkedHashSet<>();
-            this.vertexLabels.add(Vertex.DEFAULT_LABEL);
-        } else {
-            this.vertexLabels = new LinkedHashSet<>(labels);
-        }
+        this.vertexLabels = (labels == null || labels.isEmpty())
+                ? new 
LinkedHashSet<>(Collections.singleton(Vertex.DEFAULT_LABEL))
+                : new LinkedHashSet<>(labels);
     }
 
     @Override
@@ -135,6 +113,7 @@ public class TinkerVertex extends TinkerElement implements 
Vertex {
 
         this.vertexLabels.add(label);
         Collections.addAll(this.vertexLabels, labels);
+        this.label = this.vertexLabels.iterator().next();
         this.graph.updateVertexLabelIndex(this);
     }
 
@@ -142,6 +121,7 @@ public class TinkerVertex extends TinkerElement implements 
Vertex {
     public void dropLabels() {
         this.vertexLabels.clear();
         this.vertexLabels.add(Vertex.DEFAULT_LABEL);
+        this.label = this.vertexLabels.iterator().next();
         this.graph.updateVertexLabelIndex(this);
     }
 
@@ -155,6 +135,7 @@ public class TinkerVertex extends TinkerElement implements 
Vertex {
         if (this.vertexLabels.isEmpty()) {
             this.vertexLabels.add(Vertex.DEFAULT_LABEL);
         }
+        this.label = this.vertexLabels.iterator().next();
         this.graph.updateVertexLabelIndex(this);
     }
 
@@ -168,9 +149,7 @@ public class TinkerVertex extends TinkerElement implements 
Vertex {
             return vertex;
         }
 
-        final TinkerVertex vertex = new TinkerVertex(id, label, graph, 
currentVersion);
-        vertex.vertexLabels.clear();
-        vertex.vertexLabels.addAll(this.vertexLabels);
+        final TinkerVertex vertex = new TinkerVertex(id, new 
LinkedHashSet<>(vertexLabels), graph, currentVersion);
         if (inEdgesId != null)
             vertex.inEdgesId = CollectionUtil.clone((ConcurrentHashMap<String, 
Set<Object>>) inEdgesId);
 
diff --git 
a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/sideEffect/LabelMutationStepTest.java
 
b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/sideEffect/LabelMutationStepTest.java
index 58ff4fbce1..7ac5c88015 100644
--- 
a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/sideEffect/LabelMutationStepTest.java
+++ 
b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/sideEffect/LabelMutationStepTest.java
@@ -78,12 +78,14 @@ public class LabelMutationStepTest {
         assertThat(v.labels(), containsInAnyOrder("person", "manager"));
     }
 
-    @Test(expected = UnsupportedOperationException.class)
-    public void shouldThrowWhenAddingLabelToEdgeViaTraversal() {
+    @Test
+    public void shouldAddLabelToEdgeViaTraversal() {
         final Vertex v1 = g.addV("person").next();
         final Vertex v2 = g.addV("person").next();
-        v1.addEdge("knows", v2);
+        final Edge e = v1.addEdge("knows", v2);
         g.E().addLabel("friend").iterate();
+        assertThat(e.labels(), hasSize(2));
+        assertThat(e.labels(), containsInAnyOrder("knows", "friend"));
     }
 
     // --- dropLabel step tests ---
@@ -112,12 +114,14 @@ public class LabelMutationStepTest {
         assertThat(v.labels(), containsInAnyOrder("employee"));
     }
 
-    @Test(expected = UnsupportedOperationException.class)
-    public void shouldThrowWhenDroppingLabelsOnEdgeViaTraversal() {
+    @Test
+    public void shouldDropLabelsOnEdgeViaTraversal() {
         final Vertex v1 = g.addV("person").next();
         final Vertex v2 = g.addV("person").next();
-        v1.addEdge("knows", v2);
+        final Edge e = v1.addEdge("knows", v2);
         g.E().dropLabels().iterate();
+        assertThat(e.labels(), hasSize(1));
+        assertThat(e.labels(), containsInAnyOrder(Edge.DEFAULT_LABEL));
     }
 
     // --- addV multi-label tests ---
diff --git 
a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertexMultiLabelTest.java
 
b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertexMultiLabelTest.java
index 5fc84f8de2..27387d60e9 100644
--- 
a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertexMultiLabelTest.java
+++ 
b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertexMultiLabelTest.java
@@ -147,20 +147,24 @@ public class TinkerVertexMultiLabelTest {
         assertThat(v.labels(), containsInAnyOrder(Vertex.DEFAULT_LABEL));
     }
 
-    @Test(expected = UnsupportedOperationException.class)
-    public void shouldThrowWhenAddingLabelToEdge() {
+    @Test
+    public void shouldAddLabelToEdge() {
         final Vertex v1 = g.addV("person").next();
         final Vertex v2 = g.addV("person").next();
         final Edge e = v1.addEdge("knows", v2);
         e.addLabel("friend");
+        assertThat(e.labels(), hasSize(2));
+        assertThat(e.labels(), containsInAnyOrder("knows", "friend"));
     }
 
-    @Test(expected = UnsupportedOperationException.class)
-    public void shouldThrowWhenDroppingLabelsOnEdge() {
+    @Test
+    public void shouldDropLabelsOnEdge() {
         final Vertex v1 = g.addV("person").next();
         final Vertex v2 = g.addV("person").next();
         final Edge e = v1.addEdge("knows", v2);
         e.dropLabels();
+        assertThat(e.labels(), hasSize(1));
+        assertThat(e.labels(), containsInAnyOrder(Edge.DEFAULT_LABEL));
     }
 
     @Test

Reply via email to