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 6e521a75731848be3a37c78bd292b5127345a477 Author: Yang Xia <[email protected]> AuthorDate: Wed Feb 25 18:25:32 2026 -0800 multilabel prototype --- .../grammar/DefaultGremlinBaseVisitor.java | 28 +++ .../language/grammar/TraversalMethodVisitor.java | 94 ++++++++ .../grammar/TraversalSourceSpawnMethodVisitor.java | 28 ++- .../process/computer/util/ComputerGraph.java | 5 + .../traversal/dsl/graph/GraphTraversal.java | 112 ++++++++++ .../traversal/dsl/graph/GraphTraversalSource.java | 28 +++ .../gremlin/process/traversal/dsl/graph/__.java | 49 +++++ .../process/traversal/step/map/ElementMapStep.java | 39 +++- .../process/traversal/step/map/LabelsStep.java | 53 +++++ .../traversal/step/map/MergeElementStep.java | 59 ++++- .../traversal/step/map/MergeVertexStep.java | 28 ++- .../traversal/step/map/PropertyMapStep.java | 23 +- .../traversal/step/sideEffect/AddLabelStep.java | 147 +++++++++++++ .../traversal/step/sideEffect/DropLabelsStep.java | 158 +++++++++++++ .../process/traversal/step/util/HasContainer.java | 9 +- .../process/traversal/step/util/WithOptions.java | 12 + .../tinkerpop/gremlin/structure/Element.java | 56 +++++ .../gremlin/structure/VertexProperty.java | 14 ++ .../io/binary/types/VertexSerializer.java | 25 ++- .../io/graphson/GraphSONSerializersV4.java | 22 +- .../gremlin/structure/util/ElementHelper.java | 37 ++++ .../structure/util/detached/DetachedVertex.java | 40 ++++ .../structure/util/reference/ReferenceVertex.java | 35 +++ .../gremlin/structure/util/star/StarGraph.java | 5 + gremlin-language/src/main/antlr4/Gremlin.g4 | 30 +++ .../ser/binary/TypeSerializerFailureTests.java | 2 +- .../tinkergraph/structure/AbstractTinkerGraph.java | 24 ++ .../gremlin/tinkergraph/structure/TinkerEdge.java | 5 + .../gremlin/tinkergraph/structure/TinkerGraph.java | 5 +- .../tinkergraph/structure/TinkerVertex.java | 78 ++++++- .../process/traversal/step/map/LabelsStepTest.java | 106 +++++++++ .../traversal/step/map/MergeVMultiLabelTest.java | 154 +++++++++++++ .../step/sideEffect/LabelMutationPropertyTest.java | 205 +++++++++++++++++ .../step/sideEffect/LabelMutationStepTest.java | 245 +++++++++++++++++++++ .../TinkerVertexMultiLabelGremlinLangTest.java | 121 ++++++++++ .../structure/TinkerVertexMultiLabelTest.java | 191 ++++++++++++++++ 36 files changed, 2236 insertions(+), 36 deletions(-) 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 a133cd0928..f50e47ae45 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 @@ -183,6 +183,10 @@ public class DefaultGremlinBaseVisitor<T> extends AbstractParseTreeVisitor<T> im * {@inheritDoc} */ @Override public T visitTraversalMethod_addV_Traversal(final GremlinParser.TraversalMethod_addV_TraversalContext ctx) { notImplemented(ctx); return null; } + /** + * {@inheritDoc} + */ + @Override public T visitTraversalMethod_addV_StringVarargs(final GremlinParser.TraversalMethod_addV_StringVarargsContext ctx) { notImplemented(ctx); return null; } /** * {@inheritDoc} */ @@ -539,6 +543,30 @@ public class DefaultGremlinBaseVisitor<T> extends AbstractParseTreeVisitor<T> im * {@inheritDoc} */ @Override public T visitTraversalMethod_label(final GremlinParser.TraversalMethod_labelContext ctx) { notImplemented(ctx); return null; } + /** + * {@inheritDoc} + */ + @Override public T visitTraversalMethod_labels(final GremlinParser.TraversalMethod_labelsContext ctx) { notImplemented(ctx); return null; } + /** + * {@inheritDoc} + */ + @Override public T visitTraversalMethod_addLabel_String(final GremlinParser.TraversalMethod_addLabel_StringContext ctx) { notImplemented(ctx); return null; } + /** + * {@inheritDoc} + */ + @Override public T visitTraversalMethod_addLabel_Traversal(final GremlinParser.TraversalMethod_addLabel_TraversalContext ctx) { notImplemented(ctx); return null; } + /** + * {@inheritDoc} + */ + @Override public T visitTraversalMethod_dropLabels_Empty(final GremlinParser.TraversalMethod_dropLabels_EmptyContext ctx) { notImplemented(ctx); return null; } + /** + * {@inheritDoc} + */ + @Override public T visitTraversalMethod_dropLabel_String(final GremlinParser.TraversalMethod_dropLabel_StringContext ctx) { notImplemented(ctx); return null; } + /** + * {@inheritDoc} + */ + @Override public T visitTraversalMethod_dropLabel_Traversal(final GremlinParser.TraversalMethod_dropLabel_TraversalContext 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 f6967033e2..c46470092e 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 @@ -34,6 +34,7 @@ import org.apache.tinkerpop.gremlin.structure.T; import org.apache.tinkerpop.gremlin.structure.VertexProperty.Cardinality; import java.util.Arrays; +import java.util.List; import java.util.Map; import java.util.function.BiFunction; @@ -97,6 +98,25 @@ public class TraversalMethodVisitor extends TraversalRootVisitor<GraphTraversal> } } + /** + * {@inheritDoc} + */ + @Override + public GraphTraversal visitTraversalMethod_addV_StringVarargs(final GremlinParser.TraversalMethod_addV_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.addV(firstLabel, secondLabel, moreLabels); + } + /** * {@inheritDoc} */ @@ -978,6 +998,80 @@ public class TraversalMethodVisitor extends TraversalRootVisitor<GraphTraversal> return graphTraversal.label(); } + /** + * {@inheritDoc} + */ + @Override + public GraphTraversal visitTraversalMethod_labels(final GremlinParser.TraversalMethod_labelsContext ctx) { + return graphTraversal.labels(); + } + + /** + * {@inheritDoc} + */ + @Override + public GraphTraversal visitTraversalMethod_addLabel_String(final GremlinParser.TraversalMethod_addLabel_StringContext 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(); + + if (args.size() == 1) { + return this.graphTraversal.addLabel(firstLabel); + } else { + final String[] moreLabels = new String[args.size() - 1]; + for (int i = 1; i < args.size(); i++) { + final Object literalOrVar = antlr.argumentVisitor.visitStringArgument(args.get(i)); + moreLabels[i - 1] = literalOrVar instanceof String ? (String) literalOrVar : ((GValue<String>) literalOrVar).get(); + } + return this.graphTraversal.addLabel(firstLabel, moreLabels); + } + } + + /** + * {@inheritDoc} + */ + @Override + public GraphTraversal visitTraversalMethod_addLabel_Traversal(final GremlinParser.TraversalMethod_addLabel_TraversalContext ctx) { + return this.graphTraversal.addLabel(antlr.tvisitor.visitNestedTraversal(ctx.nestedTraversal())); + } + + /** + * {@inheritDoc} + */ + @Override + public GraphTraversal visitTraversalMethod_dropLabels_Empty(final GremlinParser.TraversalMethod_dropLabels_EmptyContext ctx) { + return this.graphTraversal.dropLabels(); + } + + /** + * {@inheritDoc} + */ + @Override + public GraphTraversal visitTraversalMethod_dropLabel_String(final GremlinParser.TraversalMethod_dropLabel_StringContext 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(); + + if (args.size() == 1) { + return this.graphTraversal.dropLabel(firstLabel); + } else { + final String[] moreLabels = new String[args.size() - 1]; + for (int i = 1; i < args.size(); i++) { + final Object literalOrVar = antlr.argumentVisitor.visitStringArgument(args.get(i)); + moreLabels[i - 1] = literalOrVar instanceof String ? (String) literalOrVar : ((GValue<String>) literalOrVar).get(); + } + return this.graphTraversal.dropLabel(firstLabel, moreLabels); + } + } + + /** + * {@inheritDoc} + */ + @Override + public GraphTraversal visitTraversalMethod_dropLabel_Traversal(final GremlinParser.TraversalMethod_dropLabel_TraversalContext ctx) { + return this.graphTraversal.dropLabel(antlr.tvisitor.visitNestedTraversal(ctx.nestedTraversal())); + } + /** * {@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 7fccb00070..25b75641e5 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 @@ -22,6 +22,7 @@ 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.dsl.graph.GraphTraversalSource; import org.apache.tinkerpop.gremlin.process.traversal.step.GValue; +import java.util.List; import java.util.Map; /** @@ -75,12 +76,29 @@ public class TraversalSourceSpawnMethodVisitor extends DefaultGremlinBaseVisitor */ @Override public GraphTraversal visitTraversalSourceSpawnMethod_addV(final GremlinParser.TraversalSourceSpawnMethod_addVContext ctx) { - if (ctx.stringArgument() != null) { - final Object literalOrVar = antlr.argumentVisitor.visitStringArgument(ctx.stringArgument()); - if (GValue.valueInstanceOf(literalOrVar, String.class)) { - return this.traversalSource.addV((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.addV((GValue<String>) literalOrVar); + } else { + return this.traversalSource.addV((String) literalOrVar); + } } else { - return this.traversalSource.addV((String) literalOrVar); + // Multi-label: addV("a", "b", ...) + final Object firstLiteralOrVar = antlr.argumentVisitor.visitStringArgument(stringArgs.get(0)); + final String firstLabel = firstLiteralOrVar instanceof String ? (String) firstLiteralOrVar : ((GValue<String>) firstLiteralOrVar).get(); + // Create vertex with first label, then add remaining labels + GraphTraversal t = this.traversalSource.addV(firstLabel); + 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 t.addLabel(secondLabel, moreLabels); } } else if (ctx.nestedTraversal() != null) { return this.traversalSource.addV(anonymousVisitor.visitNestedTraversal(ctx.nestedTraversal())); diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/computer/util/ComputerGraph.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/computer/util/ComputerGraph.java index 26b3e595e8..638d76dbce 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/computer/util/ComputerGraph.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/computer/util/ComputerGraph.java @@ -456,6 +456,11 @@ public final class ComputerGraph implements Graph { throw GraphComputer.Exceptions.adjacentVertexLabelsCanNotBeRead(); } + @Override + public Set<String> labels() { + throw GraphComputer.Exceptions.adjacentVertexLabelsCanNotBeRead(); + } + @Override public Graph graph() { return ComputerGraph.this; 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 286cd68b55..c0e6c221b3 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 @@ -65,6 +65,8 @@ import org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeVertexStepPl import org.apache.tinkerpop.gremlin.process.traversal.step.PropertiesHolder; import org.apache.tinkerpop.gremlin.process.traversal.step.map.VertexStepPlaceholder; import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.AddPropertyStepPlaceholder; +import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.AddLabelStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.DropLabelsStep; import org.apache.tinkerpop.gremlin.process.traversal.step.map.AddEdgeStepContract; import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.AddPropertyStepContract; import org.apache.tinkerpop.gremlin.process.traversal.step.FromToModulating; @@ -132,6 +134,7 @@ import org.apache.tinkerpop.gremlin.process.traversal.step.map.IntersectStep; import org.apache.tinkerpop.gremlin.process.traversal.step.map.LTrimGlobalStep; import org.apache.tinkerpop.gremlin.process.traversal.step.map.LTrimLocalStep; import org.apache.tinkerpop.gremlin.process.traversal.step.map.LabelStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.LabelsStep; import org.apache.tinkerpop.gremlin.process.traversal.step.map.LambdaCollectingBarrierStep; import org.apache.tinkerpop.gremlin.process.traversal.step.map.LambdaFlatMapStep; import org.apache.tinkerpop.gremlin.process.traversal.step.map.LambdaMapStep; @@ -350,12 +353,26 @@ public interface GraphTraversal<S, E> extends Traversal<S, E> { * @return the traversal with an appended {@link LabelStep}. * @see <a href="http://tinkerpop.apache.org/docs/${project.version}/reference/#label-step" target="_blank">Reference Documentation - Label Step</a> * @since 3.0.0-incubating + * @deprecated As of release 4.0.0, replaced by {@link #labels()}. */ + @Deprecated public default GraphTraversal<S, String> label() { this.asAdmin().getGremlinLang().addStep(Symbols.label); return this.asAdmin().addStep(new LabelStep<>(this.asAdmin())); } + /** + * Map the {@link Element} to its labels, emitting each label as a separate traverser. + * For vertices with multiple labels, each label is emitted individually. + * + * @return the traversal with an appended {@link LabelsStep}. + * @since 4.0.0 + */ + public default GraphTraversal<S, String> labels() { + this.asAdmin().getGremlinLang().addStep(Symbols.labels); + return this.asAdmin().addStep(new LabelsStep<>(this.asAdmin())); + } + /** * Map the <code>E</code> object to itself. In other words, a "no op." * @@ -1430,6 +1447,31 @@ public interface GraphTraversal<S, E> extends Traversal<S, E> { return this.asAdmin().addStep(new AddVertexStepPlaceholder<>(this.asAdmin(), (String) null)); } + /** + * Adds a {@link Vertex} with multiple labels. Use this method to create multi-labeled vertices. + * Creates the vertex with the first label, then adds the remaining labels. + * + * @param label1 the first label + * @param label2 the second label + * @param moreLabels additional labels + * @return the traversal with the {@link AddVertexStepContract} added + * @since 4.0.0 + */ + public default GraphTraversal<S, Vertex> addV(final String label1, final String label2, final String... moreLabels) { + if (null == label1) throw new IllegalArgumentException("vertexLabel cannot be null"); + if (null == label2) throw new IllegalArgumentException("vertexLabel cannot be null"); + for (final String l : moreLabels) { + 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)); + } + /** * Performs a merge (i.e. upsert) style operation for an {@link Vertex} using the incoming {@code Map} traverser as * an argument. The {@code Map} represents search criteria and will match each of the supplied key/value pairs where @@ -3427,6 +3469,72 @@ public interface GraphTraversal<S, E> extends Traversal<S, E> { return this.asAdmin().addStep(new DropStep<>(this.asAdmin())); } + /** + * Adds one or more labels to the current element. This is a side-effect step that passes the + * element through unchanged. + * + * @param label the first label to add + * @param moreLabels additional labels to add + * @return the traversal with an appended {@link AddLabelStep} + * @since 4.0.0 + */ + public default GraphTraversal<S, E> addLabel(final String label, final String... moreLabels) { + this.asAdmin().getGremlinLang().addStep(Symbols.addLabel, label, moreLabels); + return this.asAdmin().addStep((AddLabelStep) new AddLabelStep<>(this.asAdmin(), label, moreLabels)); + } + + /** + * Adds dynamically computed labels to the current element. This is a side-effect step that passes the + * element through unchanged. + * + * @param labelTraversal the traversal that produces labels to add + * @return the traversal with an appended {@link AddLabelStep} + * @since 4.0.0 + */ + public default GraphTraversal<S, E> addLabel(final Traversal<?, String> labelTraversal) { + this.asAdmin().getGremlinLang().addStep(Symbols.addLabel, labelTraversal); + return this.asAdmin().addStep((AddLabelStep) new AddLabelStep(this.asAdmin(), labelTraversal.asAdmin())); + } + + /** + * Removes all labels from the current element, triggering the provider's default label behavior. + * This is a side-effect step that passes the element through unchanged. + * + * @return the traversal with an appended {@link DropLabelsStep} + * @since 4.0.0 + */ + public default GraphTraversal<S, E> dropLabels() { + this.asAdmin().getGremlinLang().addStep(Symbols.dropLabels); + return this.asAdmin().addStep((DropLabelsStep) new DropLabelsStep<>(this.asAdmin())); + } + + /** + * Removes specific labels from the current element. This is a side-effect step that passes the + * element through unchanged. + * + * @param label the first label to remove + * @param moreLabels additional labels to remove + * @return the traversal with an appended {@link DropLabelsStep} + * @since 4.0.0 + */ + public default GraphTraversal<S, E> dropLabel(final String label, final String... moreLabels) { + this.asAdmin().getGremlinLang().addStep(Symbols.dropLabel, label, moreLabels); + return this.asAdmin().addStep((DropLabelsStep) new DropLabelsStep<>(this.asAdmin(), label, moreLabels)); + } + + /** + * Removes a dynamically computed label from the current element. This is a side-effect step that passes the + * element through unchanged. + * + * @param labelTraversal the traversal that produces the label to remove + * @return the traversal with an appended {@link DropLabelsStep} + * @since 4.0.0 + */ + public default GraphTraversal<S, E> dropLabel(final Traversal<?, String> labelTraversal) { + this.asAdmin().getGremlinLang().addStep(Symbols.dropLabel, labelTraversal); + return this.asAdmin().addStep((DropLabelsStep) new DropLabelsStep(this.asAdmin(), labelTraversal.asAdmin())); + } + /** * Filters <code>E</code> lists given the provided {@code predicate}. * @@ -4724,6 +4832,7 @@ public interface GraphTraversal<S, E> extends Traversal<S, E> { public static final String flatMap = "flatMap"; public static final String id = "id"; public static final String label = "label"; + public static final String labels = "labels"; public static final String identity = "identity"; public static final String constant = "constant"; public static final String V = "V"; @@ -4830,6 +4939,9 @@ public interface GraphTraversal<S, E> extends Traversal<S, E> { public static final String sample = "sample"; public static final String drop = "drop"; + public static final String addLabel = "addLabel"; + public static final String dropLabels = "dropLabels"; + public static final String dropLabel = "dropLabel"; public static final String sideEffect = "sideEffect"; public static final String cap = "cap"; 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 9e1d350503..4c13a27384 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,6 +31,7 @@ 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; @@ -363,6 +364,33 @@ public class GraphTraversalSource implements TraversalSource { return traversal.addStep(new AddVertexStartStepPlaceholder(traversal, vertexLabel)); } + /** + * Spawns a {@link GraphTraversal} by adding a vertex with multiple labels. + * Creates the vertex with the first label, then adds the remaining labels. + * + * @param label1 the first label + * @param label2 the second label + * @param moreLabels additional labels + * @return the traversal with the vertex added + * @since 4.0.0 + */ + public GraphTraversal<Vertex, Vertex> addV(final String label1, final String label2, final String... moreLabels) { + if (null == label1) throw new IllegalArgumentException("vertexLabel cannot be null"); + if (null == label2) throw new IllegalArgumentException("vertexLabel cannot be null"); + for (final String l : moreLabels) { + if (null == l) throw new IllegalArgumentException("vertexLabel cannot be null"); + } + 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)); + } + /** * Spawns a {@link GraphTraversal} by adding an edge with the specified label. * 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 65007d9631..37ed57f580 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 @@ -126,6 +126,48 @@ public class __ { return __.<A>start().label(); } + /** + * @see GraphTraversal#labels() + */ + public static <A extends Element> GraphTraversal<A, String> labels() { + return __.<A>start().labels(); + } + + /** + * @see GraphTraversal#addLabel(String, String...) + */ + public static <A extends Element> GraphTraversal<A, A> addLabel(final String label, final String... moreLabels) { + return __.<A>start().addLabel(label, moreLabels); + } + + /** + * @see GraphTraversal#addLabel(Traversal) + */ + public static <A extends Element> GraphTraversal<A, A> addLabel(final Traversal<?, String> labelTraversal) { + return __.<A>start().addLabel(labelTraversal); + } + + /** + * @see GraphTraversal#dropLabels() + */ + public static <A extends Element> GraphTraversal<A, A> dropLabels() { + return __.<A>start().dropLabels(); + } + + /** + * @see GraphTraversal#dropLabel(String, String...) + */ + public static <A extends Element> GraphTraversal<A, A> dropLabel(final String label, final String... moreLabels) { + return __.<A>start().dropLabel(label, moreLabels); + } + + /** + * @see GraphTraversal#dropLabel(Traversal) + */ + public static <A extends Element> GraphTraversal<A, A> dropLabel(final Traversal<?, String> labelTraversal) { + return __.<A>start().dropLabel(labelTraversal); + } + /** * @see GraphTraversal#id() */ @@ -654,6 +696,13 @@ public class __ { return __.<A>start().addV(); } + /** + * @see GraphTraversal#addV(String, String, String...) + */ + public static <A> GraphTraversal<A, Vertex> addV(final String label1, final String label2, final String... moreLabels) { + return __.<A>start().addV(label1, label2, moreLabels); + } + /** * @see GraphTraversal#mergeV() */ 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 71d919ca18..152301e0d7 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 @@ -20,8 +20,12 @@ package org.apache.tinkerpop.gremlin.process.traversal.step.map; import org.apache.tinkerpop.gremlin.process.traversal.Traversal; import org.apache.tinkerpop.gremlin.process.traversal.Traverser; +import org.apache.tinkerpop.gremlin.process.traversal.step.Configuring; 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; @@ -46,16 +50,32 @@ import java.util.Set; * @author Daniel Kuppitz (http://gremlin.guru) * @author Stephen Mallette (http://stephen.genoprime.com) */ -public class ElementMapStep<K,E> extends ScalarMapStep<Element, Map<K, E>> implements TraversalParent, GraphComputing { +public class ElementMapStep<K,E> extends ScalarMapStep<Element, Map<K, E>> implements TraversalParent, GraphComputing, Configuring { protected final String[] propertyKeys; private boolean onGraphComputer = false; + private boolean multilabel = false; + private final Parameters parameters = new Parameters(); public ElementMapStep(final Traversal.Admin traversal, final String... propertyKeys) { super(traversal); this.propertyKeys = propertyKeys; } + @Override + public void configure(final Object... keyValues) { + if (keyValues[0].equals(WithOptions.multilabel)) { + this.multilabel = true; + } else { + this.parameters.set(this, keyValues); + } + } + + @Override + public Parameters getParameters() { + return this.parameters; + } + @Override protected Map<K, E> map(final Traverser.Admin<Element> traverser) { final Map<Object, Object> map = new LinkedHashMap<>(); @@ -65,7 +85,11 @@ public class ElementMapStep<K,E> extends ScalarMapStep<Element, Map<K, E>> imple map.put(T.key, ((VertexProperty<?>) element).key()); map.put(T.value, ((VertexProperty<?>) element).value()); } else { - map.put(T.label, element.label()); + if (isMultilabelEnabled()) { + map.put(T.label, element.labels()); + } else { + map.put(T.label, element.label()); + } } if (element instanceof Edge) { @@ -102,6 +126,17 @@ public class ElementMapStep<K,E> extends ScalarMapStep<Element, Map<K, E>> imple return onGraphComputer; } + /** + * Checks if multilabel mode is enabled either via step-level {@code .with(WithOptions.multilabel)} + * 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); + } + public String[] getPropertyKeys() { return propertyKeys; } diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/LabelsStep.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/LabelsStep.java new file mode 100644 index 0000000000..a5ba807176 --- /dev/null +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/LabelsStep.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.tinkerpop.gremlin.process.traversal.step.map; + +import org.apache.tinkerpop.gremlin.process.traversal.Traversal; +import org.apache.tinkerpop.gremlin.process.traversal.Traverser; +import org.apache.tinkerpop.gremlin.process.traversal.traverser.TraverserRequirement; +import org.apache.tinkerpop.gremlin.structure.Element; +import org.apache.tinkerpop.gremlin.structure.util.StringFactory; + +import java.util.Collections; +import java.util.Iterator; +import java.util.Set; + +/** + * Maps an {@link Element} to its labels, emitting each label as a separate traverser. + * For vertices with multiple labels, each label is emitted individually. + * For edges, the single label is emitted. + * + * @since 4.0.0 + */ +public class LabelsStep<S extends Element> extends FlatMapStep<S, String> { + + public LabelsStep(final Traversal.Admin traversal) { + super(traversal); + } + + @Override + protected Iterator<String> flatMap(final Traverser.Admin<S> traverser) { + return traverser.get().labels().iterator(); + } + + @Override + public Set<TraverserRequirement> getRequirements() { + return Collections.singleton(TraverserRequirement.OBJECT); + } +} diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeElementStep.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeElementStep.java index daae4816fd..68f4380227 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeElementStep.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeElementStep.java @@ -252,7 +252,25 @@ public abstract class MergeElementStep<S, E, C> extends FlatMapStep<S, E> final Object v = e.getValue(); if (ignoreTokens) { - if (!(k instanceof String)) { + // Allow T.label in onMatch for multi-label replacement support + if (k == T.label) { + if (v instanceof String) { + ElementHelper.validateLabel((String) v); + } else if (v instanceof java.util.Collection) { + for (final Object label : (java.util.Collection<?>) v) { + if (!(label instanceof String)) { + throw new IllegalArgumentException(String.format( + "option(onMatch) expects T.label collection to contain only Strings - found: %s", + label.getClass().getSimpleName())); + } + ElementHelper.validateLabel((String) label); + } + } else { + throw new IllegalArgumentException(String.format( + "option(onMatch) expects T.label value to be String or Collection<String> - found: %s", + v.getClass().getSimpleName())); + } + } else if (!(k instanceof String)) { throw new IllegalArgumentException(String.format("option(onMatch) expects keys in Map to be of String - check: %s", k)); } else { ElementHelper.validateProperty((String) k, v); @@ -269,12 +287,21 @@ public abstract class MergeElementStep<S, E, C> extends FlatMapStep<S, E> op, allowedTokens, k)); } if (k == T.label) { - if (!(v instanceof String)) { - throw new IllegalArgumentException(String.format( - "%s() and option(onCreate) args expect T.label value to be of String - found: %s", op, - v.getClass().getSimpleName())); - } else { + if (v instanceof String) { ElementHelper.validateLabel((String) v); + } else if (v instanceof java.util.Collection) { + for (final Object label : (java.util.Collection<?>) v) { + if (!(label instanceof String)) { + throw new IllegalArgumentException(String.format( + "%s() expects T.label collection to contain only Strings - found: %s", + op, label.getClass().getSimpleName())); + } + ElementHelper.validateLabel((String) label); + } + } else { + throw new IllegalArgumentException(String.format( + "%s() and option(onCreate) args expect T.label value to be String or Collection<String> - found: %s", + op, v.getClass().getSimpleName())); } } if (k == Direction.OUT && v instanceof Merge && v != Merge.outV) { @@ -335,10 +362,10 @@ public abstract class MergeElementStep<S, E, C> extends FlatMapStep<S, E> final Graph graph = getGraph(); final Object id = search.get(T.id); - final String label = (String) search.get(T.label); + final Object labelValue = search.get(T.label); GraphTraversal t = searchVerticesTraversal(graph, id); - t = searchVerticesLabelConstraint(t, label); + t = searchVerticesLabelConstraint(t, labelValue); t = searchVerticesPropertyConstraints(t, search); // this should auto-close the underlying traversal @@ -349,8 +376,20 @@ public abstract class MergeElementStep<S, E, C> extends FlatMapStep<S, E> return id != null ? graph.traversal().V(id) : graph.traversal().V(); } - protected GraphTraversal searchVerticesLabelConstraint(GraphTraversal t, final String label) { - return label != null ? t.hasLabel(label) : t; + protected GraphTraversal searchVerticesLabelConstraint(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 GraphTraversal searchVerticesPropertyConstraints(GraphTraversal t, final Map search) { diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeVertexStep.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeVertexStep.java index 279af6519d..a2c59516b4 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeVertexStep.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeVertexStep.java @@ -99,12 +99,32 @@ public class MergeVertexStep<S> extends MergeElementStep<S, Vertex, Map<Object, traverser.set((S) v); // assume good input from GraphTraversal - folks might drop in a T here even though it is immutable - final Map<String, Object> onMatchMap = materializeMap(traverser, onMatchTraversal); + 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 + v.dropLabels(); + if (value instanceof String) { + v.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); + v.addLabel(labelArray[0], + java.util.Arrays.copyOfRange(labelArray, 1, labelArray.length)); + } + // Empty collection = use default label behavior (already handled by dropLabels()) + } + return; + } + Object val = value; - VertexProperty.Cardinality card = graph.features().vertex().getCardinality(key); + VertexProperty.Cardinality card = graph.features().vertex().getCardinality((String) key); // a value can be a traversal in the case where the user specifies the cardinality for the value. if (value instanceof CardinalityValueTraversal) { @@ -115,10 +135,10 @@ public class MergeVertexStep<S> extends MergeElementStep<S, Vertex, Map<Object, // trigger callbacks for eventing - in this case, it's a VertexPropertyChangedEvent. if there's no // registry/callbacks then just set the property - EventUtil.registerVertexPropertyChange(callbackRegistry, getTraversal(), v, key, val); + EventUtil.registerVertexPropertyChange(callbackRegistry, getTraversal(), v, (String) key, val); // try to detect proper cardinality for the key according to the graph - v.property(card, key, val); + v.property(card, (String) key, val); }); }); } 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 3534b7e313..9ce9f13331 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,6 +25,7 @@ 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; @@ -58,6 +59,7 @@ public class PropertyMapStep<K,E> extends ScalarMapStep<Element, Map<K, E>> protected int tokens; protected Traversal.Admin<Element, ? extends Property> propertyTraversal; + protected boolean multilabel = false; protected Parameters parameters = new Parameters(); protected Traversal.Admin<K, E> valueTraversal; @@ -97,6 +99,8 @@ public class PropertyMapStep<K,E> extends ScalarMapStep<Element, Map<K, E>> this.tokens |= (int) keyValues[i]; } } + } else if (keyValues[0].equals(WithOptions.multilabel)) { + this.multilabel = true; } else { this.parameters.set(this, keyValues); } @@ -200,11 +204,28 @@ public class PropertyMapStep<K,E> extends ScalarMapStep<Element, Map<K, E>> if (includeToken(WithOptions.keys)) map.put(T.key, getVertexPropertyKey((VertexProperty<?>) element)); if (includeToken(WithOptions.values)) map.put(T.value, getVertexPropertyValue((VertexProperty<?>) element)); } else { - if (includeToken(WithOptions.labels)) map.put(T.label, getElementLabel(element)); + if (includeToken(WithOptions.labels)) { + if (isMultilabelEnabled()) { + map.put(T.label, element.labels()); + } else { + map.put(T.label, getElementLabel(element)); + } + } } } } + /** + * Checks if multilabel mode is enabled either via step-level {@code .with(WithOptions.multilabel)} + * 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); + } + protected Object getElementId(Element element){ return element.id(); } 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 new file mode 100644 index 0000000000..c239623601 --- /dev/null +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/AddLabelStep.java @@ -0,0 +1,147 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect; + +import org.apache.tinkerpop.gremlin.process.traversal.Traversal; +import org.apache.tinkerpop.gremlin.process.traversal.Traverser; +import org.apache.tinkerpop.gremlin.process.traversal.step.Mutating; +import org.apache.tinkerpop.gremlin.process.traversal.step.TraversalParent; +import org.apache.tinkerpop.gremlin.process.traversal.step.util.event.CallbackRegistry; +import org.apache.tinkerpop.gremlin.process.traversal.step.util.event.Event; +import org.apache.tinkerpop.gremlin.process.traversal.step.util.event.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; +import org.apache.tinkerpop.gremlin.structure.util.ElementHelper; +import org.apache.tinkerpop.gremlin.structure.util.StringFactory; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +/** + * Side-effect step that adds labels to the current element by calling {@link Element#addLabel(String, String...)}. + * Providers that support label mutation must override {@code addLabel()} in their Element implementations. + * + * @since 4.0.0 + */ +public class AddLabelStep<S extends Element> extends SideEffectStep<S> + implements Mutating<Event.ElementPropertyChangedEvent>, TraversalParent { + + private final String[] labels; + private Traversal.Admin<S, String> labelTraversal; + private CallbackRegistry<Event.ElementPropertyChangedEvent> callbackRegistry; + + public AddLabelStep(final Traversal.Admin traversal, final String label, final String... moreLabels) { + super(traversal); + ElementHelper.validateLabel(label); + for (final String l : moreLabels) { + ElementHelper.validateLabel(l); + } + final List<String> allLabels = new ArrayList<>(); + allLabels.add(label); + allLabels.addAll(Arrays.asList(moreLabels)); + this.labels = allLabels.toArray(new String[0]); + this.labelTraversal = null; + } + + public AddLabelStep(final Traversal.Admin traversal, final Traversal.Admin<S, String> labelTraversal) { + super(traversal); + this.labels = null; + this.labelTraversal = this.integrateChild(labelTraversal); + } + + @Override + protected void sideEffect(final Traverser.Admin<S> traverser) { + final Element element = traverser.get(); + + if (this.labelTraversal != null) { + final List<String> collectedLabels = new ArrayList<>(); + TraversalUtil.applyAll(traverser, this.labelTraversal) + .forEachRemaining(label -> { + ElementHelper.validateLabel(label); + collectedLabels.add(label); + }); + if (!collectedLabels.isEmpty()) { + element.addLabel(collectedLabels.get(0), + collectedLabels.subList(1, collectedLabels.size()).toArray(new String[0])); + } + } else { + element.addLabel(this.labels[0], + 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]); + } + } + + @Override + public CallbackRegistry<Event.ElementPropertyChangedEvent> getMutatingCallbackRegistry() { + if (null == callbackRegistry) callbackRegistry = new ListCallbackRegistry<>(); + return callbackRegistry; + } + + @Override + public Set<TraverserRequirement> getRequirements() { + return Collections.singleton(TraverserRequirement.OBJECT); + } + + @Override + public String toString() { + return StringFactory.stepString(this, this.labels != null ? Arrays.asList(this.labels) : this.labelTraversal); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + if (this.labels != null) result ^= Arrays.hashCode(this.labels); + if (this.labelTraversal != null) result ^= this.labelTraversal.hashCode(); + return result; + } + + @Override + public List<Traversal.Admin<S, String>> getLocalChildren() { + return this.labelTraversal != null ? Collections.singletonList(this.labelTraversal) : Collections.emptyList(); + } + + @Override + public AddLabelStep<S> clone() { + final AddLabelStep<S> clone = (AddLabelStep<S>) super.clone(); + if (this.labelTraversal != null) + clone.labelTraversal = this.labelTraversal.clone(); + return clone; + } + + @Override + public void setTraversal(final Traversal.Admin<?, ?> parentTraversal) { + super.setTraversal(parentTraversal); + if (this.labelTraversal != null) + this.integrateChild(this.labelTraversal); + } +} 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 new file mode 100644 index 0000000000..14e5d0634f --- /dev/null +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/DropLabelsStep.java @@ -0,0 +1,158 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect; + +import org.apache.tinkerpop.gremlin.process.traversal.Traversal; +import org.apache.tinkerpop.gremlin.process.traversal.Traverser; +import org.apache.tinkerpop.gremlin.process.traversal.step.Mutating; +import org.apache.tinkerpop.gremlin.process.traversal.step.TraversalParent; +import org.apache.tinkerpop.gremlin.process.traversal.step.util.event.CallbackRegistry; +import org.apache.tinkerpop.gremlin.process.traversal.step.util.event.Event; +import org.apache.tinkerpop.gremlin.process.traversal.step.util.event.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; +import org.apache.tinkerpop.gremlin.structure.util.StringFactory; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +/** + * Side-effect step that removes labels from the current element by calling + * {@link Element#dropLabels()} or {@link Element#dropLabel(String, String...)}. + * + * @since 4.0.0 + */ +public class DropLabelsStep<S extends Element> extends SideEffectStep<S> + implements Mutating<Event.ElementPropertyChangedEvent>, TraversalParent { + + private final boolean dropAll; + private final String[] labels; + private Traversal.Admin<S, String> labelTraversal; + private CallbackRegistry<Event.ElementPropertyChangedEvent> callbackRegistry; + + /** + * Constructor for dropLabels() - removes all labels. + */ + public DropLabelsStep(final Traversal.Admin traversal) { + super(traversal); + this.dropAll = true; + this.labels = null; + this.labelTraversal = null; + } + + /** + * Constructor for dropLabel(String, String...) - removes specific labels. + */ + public DropLabelsStep(final Traversal.Admin traversal, final String label, final String... moreLabels) { + super(traversal); + this.dropAll = false; + final List<String> allLabels = new ArrayList<>(); + allLabels.add(label); + allLabels.addAll(Arrays.asList(moreLabels)); + this.labels = allLabels.toArray(new String[0]); + this.labelTraversal = null; + } + + /** + * Constructor for dropLabel(Traversal) - removes dynamically computed label. + */ + public DropLabelsStep(final Traversal.Admin traversal, final Traversal.Admin<S, String> labelTraversal) { + super(traversal); + this.dropAll = false; + this.labels = null; + this.labelTraversal = this.integrateChild(labelTraversal); + } + + @Override + protected void sideEffect(final Traverser.Admin<S> traverser) { + final Element element = traverser.get(); + + if (this.labelTraversal != null) { + final String label = TraversalUtil.apply(traverser, this.labelTraversal); + if (label != null) { + element.dropLabel(label); + } + } else if (this.dropAll) { + element.dropLabels(); + } else { + element.dropLabel(this.labels[0], + 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]); + } + } + + @Override + public CallbackRegistry<Event.ElementPropertyChangedEvent> getMutatingCallbackRegistry() { + if (null == callbackRegistry) callbackRegistry = new ListCallbackRegistry<>(); + return callbackRegistry; + } + + @Override + public Set<TraverserRequirement> getRequirements() { + return Collections.singleton(TraverserRequirement.OBJECT); + } + + @Override + public String toString() { + if (this.dropAll) return StringFactory.stepString(this); + return StringFactory.stepString(this, this.labels != null ? Arrays.asList(this.labels) : this.labelTraversal); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result ^= Boolean.hashCode(this.dropAll); + if (this.labels != null) result ^= Arrays.hashCode(this.labels); + if (this.labelTraversal != null) result ^= this.labelTraversal.hashCode(); + return result; + } + + @Override + public List<Traversal.Admin<S, String>> getLocalChildren() { + return this.labelTraversal != null ? Collections.singletonList(this.labelTraversal) : Collections.emptyList(); + } + + @Override + public DropLabelsStep<S> clone() { + final DropLabelsStep<S> clone = (DropLabelsStep<S>) super.clone(); + if (this.labelTraversal != null) + clone.labelTraversal = this.labelTraversal.clone(); + return clone; + } + + @Override + public void setTraversal(final Traversal.Admin<?, ?> parentTraversal) { + super.setTraversal(parentTraversal); + if (this.labelTraversal != null) + this.integrateChild(this.labelTraversal); + } +} diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/HasContainer.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/HasContainer.java index c20c68fde0..7f719bfbed 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/HasContainer.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/HasContainer.java @@ -92,7 +92,14 @@ public class HasContainer implements Serializable, Cloneable, Predicate<Element> } protected boolean testLabel(final Element element) { - return this.predicate.test(element.label()); + // Test against all labels for multi-label support. + // For single-label elements this is equivalent to testing element.label(). + for (final String label : element.labels()) { + if (this.predicate.test(label)) { + return true; + } + } + return false; } protected boolean testValue(final Property property) { 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 43671c824b..68f89cc615 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 @@ -90,4 +90,16 @@ public class WithOptions { * Index items using a {@code LinkedHashMap}. */ public static int map = 1; + + // + // Multi-label configuration + // + + /** + * Configures multi-label behavior for valueMap and elementMap steps. + * When enabled, labels are returned as {@code Set<String>} instead of {@code String}. + * + * @since 4.0.0 + */ + public static final String multilabel = Graph.Hidden.hide("tinkerpop.multilabel"); } diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/Element.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/Element.java index 0b796a1f25..354d945535 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/Element.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/Element.java @@ -44,11 +44,28 @@ public abstract interface Element { */ public Object id(); + /** + * Gets all labels for this element. + * <p> + * For {@link Vertex}: may return zero or more labels (multi-label support). + * For {@link Edge}: returns a singleton set (single label only in TinkerGraph). + * For {@link VertexProperty}: returns a singleton set containing the property key. + * + * @return An unmodifiable {@link Set} of labels; may be empty for vertices with no labels + * @since 4.0.0 + */ + public default Set<String> labels() { + return Collections.singleton(label()); + } + /** * Gets the label for the graph {@code Element} which helps categorize it. * * @return The label of the element + * @deprecated As of release 4.0.0, replaced by {@link #labels()}. This method returns an arbitrary label + * when multiple labels exist. */ + @Deprecated public String label(); /** @@ -99,6 +116,41 @@ public abstract interface Element { */ public void remove(); + /** + * Adds one or more labels to this element. + * + * @param label the first label to add + * @param labels additional labels to add + * @throws UnsupportedOperationException if the element does not support label mutation + * @since 4.0.0 + */ + public default void addLabel(final String label, final String... labels) { + throw Element.Exceptions.labelMutationNotSupported(); + } + + /** + * Removes all labels from this element, triggering the provider's default label behavior. + * + * @throws UnsupportedOperationException if the element does not support label mutation + * @since 4.0.0 + */ + public default void dropLabels() { + throw Element.Exceptions.labelMutationNotSupported(); + } + + /** + * Removes specific labels from this element. + * If this action removes all labels, triggers the provider's default label behavior. + * + * @param label the first label to remove + * @param labels additional labels to remove + * @throws UnsupportedOperationException if the element does not support label mutation + * @since 4.0.0 + */ + public default void dropLabel(final String label, final String... labels) { + throw Element.Exceptions.labelMutationNotSupported(); + } + /** * Get the values of properties as an {@link Iterator}. @@ -145,5 +197,9 @@ public abstract interface Element { public static IllegalArgumentException labelCanNotBeAHiddenKey(final String label) { return new IllegalArgumentException("Label can not be a hidden key: " + label); } + + public static UnsupportedOperationException labelMutationNotSupported() { + return new UnsupportedOperationException("Label mutation is not supported on this element"); + } } } diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/VertexProperty.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/VertexProperty.java index 962140adb5..d9ab208937 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/VertexProperty.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/VertexProperty.java @@ -21,7 +21,9 @@ package org.apache.tinkerpop.gremlin.structure; import org.apache.tinkerpop.gremlin.process.traversal.lambda.CardinalityValueTraversal; import org.apache.tinkerpop.gremlin.structure.util.empty.EmptyVertexProperty; +import java.util.Collections; import java.util.Iterator; +import java.util.Set; /** * A {@code VertexProperty} is similar to a {@link Property} in that it denotes a key/value pair associated with an @@ -78,6 +80,18 @@ public interface VertexProperty<V> extends Property<V>, Element { return this.key(); } + /** + * Returns a singleton set containing the property key, consistent with the single-label + * semantics of {@link VertexProperty}. + * + * @return a singleton {@link Set} containing this property's key + * @since 4.0.0 + */ + @Override + public default Set<String> labels() { + return Collections.singleton(this.key()); + } + /** * Constructs an empty {@code VertexProperty}. */ diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/VertexSerializer.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/VertexSerializer.java index 8c16ce2b80..cc932f8057 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/VertexSerializer.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/VertexSerializer.java @@ -29,7 +29,9 @@ import org.apache.tinkerpop.gremlin.structure.util.detached.DetachedVertexProper import org.apache.tinkerpop.gremlin.structure.util.reference.ReferenceVertex; import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; +import java.util.LinkedHashSet; import java.util.List; /** @@ -43,11 +45,19 @@ public class VertexSerializer extends SimpleTypeSerializer<Vertex> { @Override protected Vertex 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 labels as List<String> for multi-label support + final List<String> labelList = context.readValue(buffer, List.class, false); final List<DetachedVertexProperty> properties = context.read(buffer); - final DetachedVertex.Builder builder = DetachedVertex.build().setId(id).setLabel(label); + final DetachedVertex.Builder builder = DetachedVertex.build().setId(id); + + if (labelList != null && !labelList.isEmpty()) { + if (labelList.size() == 1) { + builder.setLabel(labelList.get(0)); + } else { + builder.setLabels(new LinkedHashSet<>(labelList)); + } + } if (properties != null) { for (final DetachedVertexProperty vp : properties) { @@ -61,11 +71,12 @@ public class VertexSerializer extends SimpleTypeSerializer<Vertex> { @Override protected void writeValue(final Vertex 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 labels as List<String> for multi-label support. + final java.util.Set<String> labels = value.labels(); + if (labels == null || labels.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<>(labels), buffer, false); if (value instanceof ReferenceVertex) { 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 7bfff073bc..43c24d7270 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 @@ -86,7 +86,7 @@ class GraphSONSerializersV4 { jsonGenerator.writeStartObject(); jsonGenerator.writeObjectField(GraphSONTokens.ID, vertex.id()); - writeLabel(jsonGenerator, GraphSONTokens.LABEL, vertex.label()); + writeLabels(jsonGenerator, GraphSONTokens.LABEL, vertex.labels()); writeTypeForGraphObjectIfUntyped(jsonGenerator, typeInfo, GraphSONTokens.VERTEX); writeProperties(vertex, jsonGenerator, serializerProvider); @@ -383,8 +383,14 @@ class GraphSONSerializersV4 { v.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) { - v.setLabel(jsonParser.getText()); + labels.add(jsonParser.getText()); + } + if (labels.size() == 1) { + v.setLabel(labels.iterator().next()); + } else if (!labels.isEmpty()) { + v.setLabels(labels); } } else if (jsonParser.getCurrentName().equals(GraphSONTokens.PROPERTIES)) { jsonParser.nextToken(); @@ -647,4 +653,16 @@ class GraphSONSerializersV4 { jsonGenerator.writeString(labelValue); jsonGenerator.writeEndArray(); } + + /** + * Helper method for writing multiple labels as an array. Used for multi-label vertex support. + */ + private static void writeLabels(final JsonGenerator jsonGenerator, final String labelName, final java.util.Set<String> labels) throws IOException { + jsonGenerator.writeFieldName(labelName); + jsonGenerator.writeStartArray(); + for (final String label : labels) { + jsonGenerator.writeString(label); + } + jsonGenerator.writeEndArray(); + } } \ No newline at end of file diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/ElementHelper.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/ElementHelper.java index 40dc7014c3..68a56905e3 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/ElementHelper.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/ElementHelper.java @@ -30,9 +30,11 @@ import org.javatuples.Pair; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; 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.Objects; @@ -245,6 +247,41 @@ public final class ElementHelper { return Optional.empty(); } + /** + * Extracts the value of the {@link T#label} key from the list of arguments as a {@link Set} of labels. + * Supports both single {@link String} values and {@link java.util.Collection} values for multi-label vertices. + * + * @param keyValues a list of key/value pairs + * @return the labels associated with {@link T#label}, or empty if not present + * @since 4.0.0 + */ + public static Optional<Set<String>> getLabelsValue(final Object... keyValues) { + for (int i = 0; i < keyValues.length; i = i + 2) { + if (keyValues[i].equals(T.label)) { + final Object labelValue = keyValues[i + 1]; + if (labelValue instanceof String) { + ElementHelper.validateLabel((String) labelValue); + final Set<String> labels = new LinkedHashSet<>(); + labels.add((String) labelValue); + return Optional.of(labels); + } else if (labelValue instanceof Collection) { + final Set<String> labels = new LinkedHashSet<>(); + for (final Object l : (Collection<?>) labelValue) { + if (!(l instanceof String)) { + throw new IllegalArgumentException("T.label collection must contain only Strings"); + } + ElementHelper.validateLabel((String) l); + labels.add((String) l); + } + return Optional.of(labels); + } else { + throw new IllegalArgumentException("T.label value must be String or Collection<String>"); + } + } + } + return Optional.empty(); + } + /** * Assign key/value pairs as properties to an {@link Element}. If the value of {@link T#id} or * {@link T#label} is in the set of pairs, then they are ignored. diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/detached/DetachedVertex.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/detached/DetachedVertex.java index f331d8da50..57f87059d7 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/detached/DetachedVertex.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/detached/DetachedVertex.java @@ -32,8 +32,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 a {@link Vertex} that is disconnected from a {@link Graph}. "Disconnection" can mean detachment from @@ -53,11 +55,24 @@ public class DetachedVertex extends DetachedElement<Vertex> implements Vertex { private static final String VALUE = "value"; private static final String PROPERTIES = "properties"; + private Set<String> vertexLabels; + private DetachedVertex() {} protected DetachedVertex(final Vertex vertex, final boolean withProperties) { super(vertex); + // Capture all labels from the source vertex for multi-label support. + // super(vertex) only stores element.label() (single label) in DetachedElement. + try { + final Set<String> srcLabels = vertex.labels(); + if (srcLabels.size() > 1) { + this.vertexLabels = new LinkedHashSet<>(srcLabels); + } + } catch (UnsupportedOperationException e) { + // Adjacent vertices in graph computer context may not support labels() + } + // only serialize properties if requested, 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 @@ -99,6 +114,23 @@ public class DetachedVertex extends DetachedElement<Vertex> implements Vertex { } } + @Override + public Set<String> labels() { + if (this.vertexLabels != null) { + return Collections.unmodifiableSet(this.vertexLabels); + } + // Fall back to single label from parent + return this.label != null ? Collections.singleton(this.label) : Collections.emptySet(); + } + + @Override + public String label() { + if (this.vertexLabels != null && !this.vertexLabels.isEmpty()) { + return this.vertexLabels.iterator().next(); + } + return this.label != null ? this.label : Vertex.DEFAULT_LABEL; + } + @Override public <V> VertexProperty<V> property(final String key, final V value) { throw Element.Exceptions.propertyAdditionNotSupported(); @@ -196,6 +228,14 @@ public class DetachedVertex extends DetachedElement<Vertex> implements Vertex { return this; } + public Builder setLabels(final Set<String> labels) { + v.vertexLabels = labels != null ? new LinkedHashSet<>(labels) : null; + if (labels != null && !labels.isEmpty()) { + v.label = labels.iterator().next(); + } + return this; + } + public DetachedVertex create() { return v; } diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/reference/ReferenceVertex.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/reference/ReferenceVertex.java index c6e646a563..006f2fd49f 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/reference/ReferenceVertex.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/reference/ReferenceVertex.java @@ -28,12 +28,16 @@ import org.apache.tinkerpop.gremlin.structure.util.StringFactory; import java.util.Collections; import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Set; /** * @author Marko A. Rodriguez (http://markorodriguez.com) */ public class ReferenceVertex extends ReferenceElement<Vertex> implements Vertex { + private Set<String> vertexLabels; + private ReferenceVertex() { } @@ -46,8 +50,39 @@ public class ReferenceVertex extends ReferenceElement<Vertex> implements Vertex super(id, label); } + public ReferenceVertex(final Object id, final Set<String> labels) { + super(id, labels != null && !labels.isEmpty() ? labels.iterator().next() : Vertex.DEFAULT_LABEL); + this.vertexLabels = labels != null ? new LinkedHashSet<>(labels) : null; + } + public ReferenceVertex(final Vertex vertex) { super(vertex); + // Capture all labels from the source vertex for multi-label support. + // super(vertex) only stores element.label() (single label) in ReferenceElement. + try { + final Set<String> srcLabels = vertex.labels(); + if (srcLabels.size() > 1) { + this.vertexLabels = new LinkedHashSet<>(srcLabels); + } + } catch (UnsupportedOperationException e) { + // Adjacent vertices in graph computer context may not support labels() + } + } + + @Override + public Set<String> labels() { + if (this.vertexLabels != null) { + return Collections.unmodifiableSet(this.vertexLabels); + } + return this.label != null ? Collections.singleton(this.label) : Collections.emptySet(); + } + + @Override + public String label() { + if (this.vertexLabels != null && !this.vertexLabels.isEmpty()) { + return this.vertexLabels.iterator().next(); + } + return this.label != null ? this.label : Vertex.DEFAULT_LABEL; } @Override 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 9b3de0bd34..a8e1dc593d 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 @@ -732,6 +732,11 @@ public final class StarGraph implements Graph, Serializable { throw GraphComputer.Exceptions.adjacentVertexLabelsCanNotBeRead(); } + @Override + public Set<String> labels() { + throw GraphComputer.Exceptions.adjacentVertexLabelsCanNotBeRead(); + } + @Override public Graph graph() { return StarGraph.this; diff --git a/gremlin-language/src/main/antlr4/Gremlin.g4 b/gremlin-language/src/main/antlr4/Gremlin.g4 index ef19a6206c..54bbff520d 100644 --- a/gremlin-language/src/main/antlr4/Gremlin.g4 +++ b/gremlin-language/src/main/antlr4/Gremlin.g4 @@ -118,6 +118,7 @@ traversalSourceSpawnMethod_addE traversalSourceSpawnMethod_addV : K_ADDV LPAREN RPAREN | K_ADDV LPAREN stringArgument RPAREN + | K_ADDV LPAREN stringArgument COMMA stringArgument (COMMA stringArgument)* RPAREN | K_ADDV LPAREN nestedTraversal RPAREN ; @@ -309,6 +310,10 @@ traversalMethod | traversalMethod_dateAdd | traversalMethod_dateDiff | traversalMethod_asNumber + | traversalMethod_labels + | traversalMethod_addLabel + | traversalMethod_dropLabels + | traversalMethod_dropLabel ; traversalMethod_V @@ -327,6 +332,7 @@ traversalMethod_addE traversalMethod_addV : K_ADDV LPAREN RPAREN #traversalMethod_addV_Empty | K_ADDV LPAREN stringArgument RPAREN #traversalMethod_addV_String + | K_ADDV LPAREN stringArgument COMMA stringArgument (COMMA stringArgument)* RPAREN #traversalMethod_addV_StringVarargs | K_ADDV LPAREN nestedTraversal RPAREN #traversalMethod_addV_Traversal ; @@ -622,6 +628,24 @@ traversalMethod_label : K_LABEL LPAREN RPAREN ; +traversalMethod_labels + : K_LABELS LPAREN RPAREN + ; + +traversalMethod_addLabel + : K_ADDLABEL LPAREN stringArgument (COMMA stringArgument)* RPAREN #traversalMethod_addLabel_String + | K_ADDLABEL LPAREN nestedTraversal RPAREN #traversalMethod_addLabel_Traversal + ; + +traversalMethod_dropLabels + : K_DROPLABELS LPAREN RPAREN #traversalMethod_dropLabels_Empty + ; + +traversalMethod_dropLabel + : K_DROPLABEL LPAREN stringArgument (COMMA stringArgument)* RPAREN #traversalMethod_dropLabel_String + | K_DROPLABEL LPAREN nestedTraversal RPAREN #traversalMethod_dropLabel_Traversal + ; + traversalMethod_length : K_LENGTH LPAREN RPAREN #traversalMethod_length_Empty | K_LENGTH LPAREN traversalScope RPAREN #traversalMethod_length_Scope @@ -1737,6 +1761,7 @@ keyword : TRAVERSAL_ROOT // g - __ is not an allowable key in this context | K_ADDALL | K_ADDE + | K_ADDLABEL | K_ADDV | K_AGGREGATE | K_ALL @@ -1807,6 +1832,8 @@ keyword | K_DOUBLE | K_DOUBLEU | K_DROP + | K_DROPLABEL + | K_DROPLABELS | K_DT | K_DURATION | K_DURATIONU @@ -2047,6 +2074,7 @@ keyword K_ADDALL: 'addAll'; K_ADDE: 'addE'; +K_ADDLABEL: 'addLabel'; K_ADDV: 'addV'; K_AGGREGATE: 'aggregate'; K_ALL: 'all'; @@ -2117,6 +2145,8 @@ K_DIV: 'div'; K_DOUBLE: 'double'; K_DOUBLEU: 'DOUBLE'; K_DROP: 'drop'; +K_DROPLABEL: 'dropLabel'; +K_DROPLABELS: 'dropLabels'; K_DT: 'DT'; K_DURATION: 'duration'; K_DURATIONU: 'DURATION'; diff --git a/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/util/ser/binary/TypeSerializerFailureTests.java b/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/util/ser/binary/TypeSerializerFailureTests.java index e4afbbe308..96254a57da 100644 --- a/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/util/ser/binary/TypeSerializerFailureTests.java +++ b/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/util/ser/binary/TypeSerializerFailureTests.java @@ -57,7 +57,7 @@ public class TypeSerializerFailureTests { @Parameterized.Parameters(name = "Value={0}") public static Collection input() { - final ReferenceVertex vertex = new ReferenceVertex("a vertex", null); + final ReferenceVertex vertex = new ReferenceVertex("a vertex", (String) null); final BulkSet<Object> bulkSet = new BulkSet<>(); bulkSet.add(vertex, 1L); 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 9a361793e6..197b43e987 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 @@ -283,6 +283,17 @@ public abstract class AbstractTinkerGraph implements Graph { protected abstract void addInEdge(final TinkerVertex vertex, final String label, final Edge edge); + /** + * 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. + * + * @param vertex the vertex whose labels have changed + * @since 4.0.0 + */ + public void updateVertexLabelIndex(final TinkerVertex vertex) { + // no-op by default - TinkerGraph does not maintain a separate label index + } + protected TinkerVertex createTinkerVertex(final Object id, final String label, final AbstractTinkerGraph graph) { return new TinkerVertex(id, label, graph); } @@ -291,6 +302,19 @@ public abstract class AbstractTinkerGraph implements Graph { return new TinkerVertex(id, label, graph, currentVersion); } + /** + * Creates a TinkerVertex with multiple labels. + * + * @param id the vertex id + * @param labels the set of labels for the vertex + * @param graph the graph instance + * @return a new TinkerVertex + * @since 4.0.0 + */ + protected TinkerVertex createTinkerVertex(final Object id, final Set<String> labels, final AbstractTinkerGraph graph) { + return new TinkerVertex(id, labels, graph); + } + protected TinkerEdge createTinkerEdge(final Object id, final Vertex outVertex, final String label, final Vertex inVertex) { return new TinkerEdge(id, outVertex, label, inVertex); } 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 43fcc29d13..dbe16e80b0 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 @@ -105,6 +105,11 @@ public class TinkerEdge extends TinkerElement implements Edge { return null == this.properties ? Collections.emptySet() : this.properties.keySet(); } + @Override + public Set<String> labels() { + return Collections.singleton(this.label); + } + @Override public void remove() { graph.touch(this); 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 b5bcfd9f9a..ef1f0f846e 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 @@ -135,7 +135,8 @@ public class TinkerGraph extends AbstractTinkerGraph { public Vertex addVertex(final Object... keyValues) { ElementHelper.legalPropertyKeyValueArray(keyValues); Object idValue = vertexIdManager.convert(ElementHelper.getIdValue(keyValues).orElse(null)); - final String label = ElementHelper.getLabelValue(keyValues).orElse(Vertex.DEFAULT_LABEL); + final Set<String> labels = ElementHelper.getLabelsValue(keyValues).orElse( + Collections.singleton(Vertex.DEFAULT_LABEL)); if (null != idValue) { if (this.vertices.containsKey(idValue)) @@ -144,7 +145,7 @@ public class TinkerGraph extends AbstractTinkerGraph { idValue = vertexIdManager.getNextId(this); } - final Vertex vertex = createTinkerVertex(idValue, label, this); + final Vertex vertex = createTinkerVertex(idValue, labels, this); ElementHelper.attachProperties(vertex, VertexProperty.Cardinality.list, keyValues); this.vertices.put(vertex.id(), vertex); 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 80a0f80642..ecbac567bd 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 @@ -31,6 +31,7 @@ import org.apache.tinkerpop.gremlin.util.iterator.IteratorUtils; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Optional; @@ -54,11 +55,18 @@ public class TinkerVertex extends TinkerElement implements Vertex { private boolean allowNullPropertyValues; private final boolean isTxMode; + /** + * Multi-label storage for this vertex. Shadows the single-label field in TinkerElement. + */ + 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); } protected TinkerVertex(final Object id, final String label, final AbstractTinkerGraph graph, final long currentVersion) { @@ -66,12 +74,78 @@ public class TinkerVertex extends TinkerElement implements Vertex { 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); + } + + /** + * 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); + } + } + + @Override + public Set<String> labels() { + return Collections.unmodifiableSet(this.vertexLabels); + } + + @Override + @Deprecated + public String label() { + return this.vertexLabels.iterator().next(); + } + + @Override + public void addLabel(final String label, final String... labels) { + ElementHelper.validateLabel(label); + for (final String l : labels) { + ElementHelper.validateLabel(l); + } + + // Remove default label if it was the only label and we're adding real labels + if (this.vertexLabels.size() == 1 && this.vertexLabels.contains(Vertex.DEFAULT_LABEL)) { + this.vertexLabels.remove(Vertex.DEFAULT_LABEL); + } + + this.vertexLabels.add(label); + Collections.addAll(this.vertexLabels, labels); + this.graph.updateVertexLabelIndex(this); + } + + @Override + public void dropLabels() { + this.vertexLabels.clear(); + this.vertexLabels.add(Vertex.DEFAULT_LABEL); + this.graph.updateVertexLabelIndex(this); + } + + @Override + public void dropLabel(final String label, final String... labels) { + this.vertexLabels.remove(label); + for (final String l : labels) { + this.vertexLabels.remove(l); + } + + if (this.vertexLabels.isEmpty()) { + this.vertexLabels.add(Vertex.DEFAULT_LABEL); + } + this.graph.updateVertexLabelIndex(this); } @Override public Object clone() { if (!isTxMode) { - final TinkerVertex vertex = new TinkerVertex(id, label, graph, currentVersion); + final TinkerVertex vertex = new TinkerVertex(id, new LinkedHashSet<>(vertexLabels), graph); vertex.inEdgesId = inEdgesId; vertex.outEdgesId = outEdgesId; vertex.properties = properties; @@ -79,6 +153,8 @@ public class TinkerVertex extends TinkerElement implements Vertex { } final TinkerVertex vertex = new TinkerVertex(id, label, graph, currentVersion); + vertex.vertexLabels.clear(); + vertex.vertexLabels.addAll(this.vertexLabels); 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/map/LabelsStepTest.java b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/map/LabelsStepTest.java new file mode 100644 index 0000000000..9c816cf9c8 --- /dev/null +++ b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/map/LabelsStepTest.java @@ -0,0 +1,106 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.tinkerpop.gremlin.tinkergraph.process.traversal.step.map; + +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; +import org.apache.tinkerpop.gremlin.structure.Edge; +import org.apache.tinkerpop.gremlin.structure.Graph; +import org.apache.tinkerpop.gremlin.structure.Vertex; +import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerGraph; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; + +/** + * Tests for the labels() traversal step. + */ +public class LabelsStepTest { + + private Graph graph; + private GraphTraversalSource g; + + @Before + public void setup() { + graph = TinkerGraph.open(); + g = graph.traversal(); + } + + @After + public void tearDown() throws Exception { + graph.close(); + } + + @Test + public void shouldStreamAllLabelsFromVertex() { + final Vertex v = g.addV("person").addLabel("employee").next(); + final List<String> labels = g.V(v).labels().toList(); + assertThat(labels, hasSize(2)); + assertThat(labels, containsInAnyOrder("person", "employee")); + } + + @Test + public void shouldCountLabelsCorrectly() { + final Vertex v = g.addV("person").addLabel("employee").next(); + final long count = g.V(v).labels().count().next(); + assertThat(count, is(2L)); + } + + @Test + public void shouldFoldAllLabels() { + final Vertex v = g.addV("person").addLabel("employee").next(); + final List<String> folded = g.V(v).labels().fold().next(); + assertThat(folded, hasSize(2)); + assertThat(folded, containsInAnyOrder("person", "employee")); + } + + @Test + public void shouldReturnSingletonForEdge() { + final Vertex v1 = g.addV("person").next(); + final Vertex v2 = g.addV("person").next(); + final Edge e = v1.addEdge("knows", v2); + final List<String> labels = g.E(e).labels().toList(); + assertThat(labels, hasSize(1)); + assertThat(labels, containsInAnyOrder("knows")); + } + + @Test + public void shouldStreamSingleLabelFromSingleLabelVertex() { + final Vertex v = g.addV("person").next(); + final List<String> labels = g.V(v).labels().toList(); + assertThat(labels, hasSize(1)); + assertThat(labels, containsInAnyOrder("person")); + } + + @Test + public void shouldStreamDefaultLabelFromDefaultVertex() { + final Vertex v = g.addV().next(); + final List<String> labels = g.V(v).labels().toList(); + assertThat(labels, hasSize(1)); + assertThat(labels, containsInAnyOrder(Vertex.DEFAULT_LABEL)); + } +} diff --git a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/map/MergeVMultiLabelTest.java b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/map/MergeVMultiLabelTest.java new file mode 100644 index 0000000000..2257a15b60 --- /dev/null +++ b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/map/MergeVMultiLabelTest.java @@ -0,0 +1,154 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.tinkerpop.gremlin.tinkergraph.process.traversal.step.map; + +import org.apache.tinkerpop.gremlin.process.traversal.Merge; +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; +import org.apache.tinkerpop.gremlin.structure.Graph; +import org.apache.tinkerpop.gremlin.structure.T; +import org.apache.tinkerpop.gremlin.structure.Vertex; +import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerGraph; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; + +/** + * Tests for mergeV() multi-label support. + */ +public class MergeVMultiLabelTest { + + private Graph graph; + private GraphTraversalSource g; + + @Before + public void setup() { + graph = TinkerGraph.open(); + g = graph.traversal(); + } + + @After + public void tearDown() throws Exception { + graph.close(); + } + + // --- mergeV create with multi-label --- + + @Test + public void shouldCreateVertexWithMultiLabelViaMergeV() { + final Set<String> labels = new LinkedHashSet<>(Arrays.asList("person", "employee")); + final Vertex v = g.mergeV(Map.of(T.label, labels, "name", "marko")).next(); + assertThat(v.labels(), hasSize(2)); + assertThat(v.labels(), containsInAnyOrder("person", "employee")); + assertThat(v.value("name"), is("marko")); + } + + @Test + public void shouldCreateVertexWithSingleLabelViaMergeV() { + final Vertex v = g.mergeV(Map.of(T.label, "person", "name", "marko")).next(); + assertThat(v.labels(), hasSize(1)); + assertThat(v.labels(), containsInAnyOrder("person")); + } + + @Test + public void shouldCreateVertexWithListLabelViaMergeV() { + final List<String> labels = Arrays.asList("a", "b", "c"); + final Vertex v = g.mergeV(Map.of(T.label, labels, "name", "test")).next(); + assertThat(v.labels(), hasSize(3)); + assertThat(v.labels(), containsInAnyOrder("a", "b", "c")); + } + + // --- mergeV match with multi-label --- + + @Test + public void shouldMatchVertexWithMultiLabelViaMergeV() { + // create a vertex with two labels + final Vertex existing = g.addV("person").addLabel("employee").property("name", "marko").next(); + + // mergeV should match it using AND semantics + final Set<String> labels = new LinkedHashSet<>(Arrays.asList("person", "employee")); + final Vertex matched = g.mergeV(Map.of(T.label, labels, "name", "marko")).next(); + + assertThat(matched.id(), is(existing.id())); + // should not create a new vertex + assertThat(g.V().count().next(), is(1L)); + } + + @Test + public void shouldNotMatchWhenVertexMissingOneLabel() { + // create a vertex with only one label + g.addV("person").property("name", "marko").next(); + + // mergeV with two labels should NOT match (AND semantics) + final Set<String> labels = new LinkedHashSet<>(Arrays.asList("person", "employee")); + g.mergeV(Map.of(T.label, labels, "name", "marko")).next(); + + // should have created a new vertex + assertThat(g.V().count().next(), is(2L)); + } + + // --- mergeV onMatch label replacement --- + + @Test + public void shouldReplaceLabelsOnMatchWithSingleLabel() { + final Vertex v = g.addV("person").addLabel("employee").property("name", "marko").next(); + + g.mergeV(Map.of(T.label, "person", "name", "marko")) + .option(Merge.onMatch, Map.of(T.label, "manager")).next(); + + // labels should be wholly replaced + assertThat(v.labels(), hasSize(1)); + assertThat(v.labels(), containsInAnyOrder("manager")); + } + + @Test + public void shouldReplaceLabelsOnMatchWithMultiLabel() { + final Vertex v = g.addV("person").property("name", "marko").next(); + + final Set<String> newLabels = new LinkedHashSet<>(Arrays.asList("manager", "director")); + g.mergeV(Map.of(T.label, "person", "name", "marko")) + .option(Merge.onMatch, Map.of(T.label, newLabels)).next(); + + assertThat(v.labels(), hasSize(2)); + assertThat(v.labels(), containsInAnyOrder("manager", "director")); + } + + @Test + public void shouldApplyDefaultLabelOnMatchWithEmptyCollection() { + final Vertex v = g.addV("person").property("name", "marko").next(); + + g.mergeV(Map.of(T.label, "person", "name", "marko")) + .option(Merge.onMatch, Map.of(T.label, Collections.emptySet())).next(); + + // empty collection triggers default label behavior + assertThat(v.labels(), hasSize(1)); + assertThat(v.labels(), containsInAnyOrder(Vertex.DEFAULT_LABEL)); + } +} diff --git a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/sideEffect/LabelMutationPropertyTest.java b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/sideEffect/LabelMutationPropertyTest.java new file mode 100644 index 0000000000..5aeea9daa8 --- /dev/null +++ b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/sideEffect/LabelMutationPropertyTest.java @@ -0,0 +1,205 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.tinkerpop.gremlin.tinkergraph.process.traversal.step.sideEffect; + +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; +import org.apache.tinkerpop.gremlin.process.traversal.step.util.WithOptions; +import org.apache.tinkerpop.gremlin.structure.Graph; +import org.apache.tinkerpop.gremlin.structure.T; +import org.apache.tinkerpop.gremlin.structure.Vertex; +import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerGraph; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; + +/** + * Property-based tests for addLabel() and dropLabel() traversal steps. + * Uses randomized inputs to validate universal correctness properties. + */ +public class LabelMutationPropertyTest { + + private static final Logger logger = LoggerFactory.getLogger(LabelMutationPropertyTest.class); + private static final int ITERATIONS = 100; + private static final String LABEL_CHARS = "abcdefghijklmnopqrstuvwxyz"; + + private Graph graph; + private GraphTraversalSource g; + private Random random; + + @Before + public void setup() { + graph = TinkerGraph.open(); + g = graph.traversal(); + random = new Random(42); // deterministic seed for reproducibility + } + + @After + public void tearDown() throws Exception { + graph.close(); + } + + private String randomLabel() { + final int len = 1 + random.nextInt(8); + final StringBuilder sb = new StringBuilder(len); + for (int i = 0; i < len; i++) { + sb.append(LABEL_CHARS.charAt(random.nextInt(LABEL_CHARS.length()))); + } + return sb.toString(); + } + + private Set<String> randomLabelSet(final int minSize, final int maxSize) { + final int size = minSize + random.nextInt(maxSize - minSize + 1); + final Set<String> labels = new HashSet<>(); + while (labels.size() < size) { + labels.add(randomLabel()); + } + return labels; + } + + /** + * Property 4: AddLabel idempotence. + * For any vertex and any label L, if L is already in the vertex's label set, + * calling addLabel(L) shall result in the label set being unchanged. + */ + @Test + public void shouldBeIdempotentWhenAddingExistingLabel() { + for (int i = 0; i < ITERATIONS; i++) { + final Set<String> initialLabels = randomLabelSet(1, 5); + final Vertex v = g.addV(initialLabels.iterator().next()).next(); + // add remaining labels + for (final String l : initialLabels) { + g.V(v).addLabel(l).iterate(); + } + + // pick a label already present + final List<String> labelList = new ArrayList<>(v.labels()); + final String existingLabel = labelList.get(random.nextInt(labelList.size())); + final Set<String> beforeAdd = new HashSet<>(v.labels()); + + // add it again - should be idempotent + g.V(v).addLabel(existingLabel).iterate(); + final Set<String> afterAdd = new HashSet<>(v.labels()); + + assertThat("Iteration " + i + ": addLabel should be idempotent for label '" + existingLabel + "'", + afterAdd, is(beforeAdd)); + } + } + + /** + * Property 6: DropLabels removes all labels and applies default. + * For any TinkerVertex with any number of labels, calling dropLabels() + * shall result in the vertex having only the default label "vertex". + */ + @Test + public void shouldApplyDefaultLabelAfterDroppingAllLabels() { + for (int i = 0; i < ITERATIONS; i++) { + final Set<String> initialLabels = randomLabelSet(1, 5); + final Vertex v = g.addV(initialLabels.iterator().next()).next(); + for (final String l : initialLabels) { + g.V(v).addLabel(l).iterate(); + } + + // drop all labels + g.V(v).dropLabels().iterate(); + + assertThat("Iteration " + i + ": after dropLabels(), vertex should have exactly one label", + v.labels(), hasSize(1)); + assertThat("Iteration " + i + ": after dropLabels(), vertex should have default label", + v.labels(), containsInAnyOrder(Vertex.DEFAULT_LABEL)); + } + } + + /** + * Property 8: DropLabel non-existent labels no-op. + * For any vertex and any label L not present in the vertex's label set, + * calling dropLabel(L) shall leave the label set unchanged. + */ + @Test + public void shouldBeNoOpWhenDroppingNonExistentLabel() { + for (int i = 0; i < ITERATIONS; i++) { + final Set<String> initialLabels = randomLabelSet(1, 5); + final Vertex v = g.addV(initialLabels.iterator().next()).next(); + for (final String l : initialLabels) { + g.V(v).addLabel(l).iterate(); + } + + // generate a label guaranteed not to be in the set + String nonExistent = randomLabel(); + while (v.labels().contains(nonExistent)) { + nonExistent = randomLabel(); + } + + final Set<String> beforeDrop = new HashSet<>(v.labels()); + g.V(v).dropLabel(nonExistent).iterate(); + final Set<String> afterDrop = new HashSet<>(v.labels()); + + assertThat("Iteration " + i + ": dropLabel of non-existent label '" + nonExistent + "' should be no-op", + afterDrop, is(beforeDrop)); + } + } + + /** + * Property 12: ValueMap/ElementMap multilabel configuration. + * For any element with labels L, valueMap(true).with(WithOptions.multilabel) shall return + * labels as Set<String> equal to L, and without the config shall return a single String from L. + */ + @SuppressWarnings("unchecked") + @Test + public void shouldReturnCorrectLabelTypeBasedOnMultilabelConfig() { + for (int i = 0; i < ITERATIONS; i++) { + final Set<String> initialLabels = randomLabelSet(1, 4); + final Vertex v = g.addV(initialLabels.iterator().next()).next(); + for (final String l : initialLabels) { + g.V(v).addLabel(l).iterate(); + } + + // with multilabel config: should return Set<String> + final Map<Object, Object> mapWithConfig = g.V(v).valueMap(true) + .with(WithOptions.multilabel).next(); + final Object labelWithConfig = mapWithConfig.get(T.label); + assertThat("Iteration " + i + ": with multilabel config, label should be a Set", + labelWithConfig, instanceOf(Set.class)); + assertThat("Iteration " + i + ": with multilabel config, labels should match", + (Set<String>) labelWithConfig, is(v.labels())); + + // without multilabel config: should return single String + final Map<Object, Object> mapWithoutConfig = g.V(v).valueMap(true).next(); + final Object labelWithoutConfig = mapWithoutConfig.get(T.label); + assertThat("Iteration " + i + ": without multilabel config, label should be a String", + labelWithoutConfig, instanceOf(String.class)); + assertThat("Iteration " + i + ": without multilabel config, label should be in vertex labels", + v.labels().contains((String) labelWithoutConfig), is(true)); + } + } +} 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 new file mode 100644 index 0000000000..58ff4fbce1 --- /dev/null +++ b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/sideEffect/LabelMutationStepTest.java @@ -0,0 +1,245 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.tinkerpop.gremlin.tinkergraph.process.traversal.step.sideEffect; + +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; +import org.apache.tinkerpop.gremlin.process.traversal.step.util.WithOptions; +import org.apache.tinkerpop.gremlin.structure.Edge; +import org.apache.tinkerpop.gremlin.structure.Graph; +import org.apache.tinkerpop.gremlin.structure.T; +import org.apache.tinkerpop.gremlin.structure.Vertex; +import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerGraph; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__.constant; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; + +/** + * Tests for addLabel() and dropLabel() traversal steps, addV multi-label, + * and valueMap/elementMap multilabel configuration. + */ +public class LabelMutationStepTest { + + private Graph graph; + private GraphTraversalSource g; + + @Before + public void setup() { + graph = TinkerGraph.open(); + g = graph.traversal(); + } + + @After + public void tearDown() throws Exception { + graph.close(); + } + + // --- addLabel step tests --- + + @Test + public void shouldAddLabelViaTraversal() { + final Vertex v = g.addV("person").next(); + g.V(v).addLabel("employee").iterate(); + assertThat(v.labels(), hasSize(2)); + assertThat(v.labels(), containsInAnyOrder("person", "employee")); + } + + @Test + public void shouldAddLabelWithConstantTraversal() { + final Vertex v = g.addV("person").next(); + g.V(v).addLabel(constant("manager")).iterate(); + assertThat(v.labels(), hasSize(2)); + assertThat(v.labels(), containsInAnyOrder("person", "manager")); + } + + @Test(expected = UnsupportedOperationException.class) + public void shouldThrowWhenAddingLabelToEdgeViaTraversal() { + final Vertex v1 = g.addV("person").next(); + final Vertex v2 = g.addV("person").next(); + v1.addEdge("knows", v2); + g.E().addLabel("friend").iterate(); + } + + // --- dropLabel step tests --- + + @Test + public void shouldDropSpecificLabelViaTraversal() { + final Vertex v = g.addV("person").addLabel("employee").next(); + g.V(v).dropLabel("person").iterate(); + assertThat(v.labels(), hasSize(1)); + assertThat(v.labels(), containsInAnyOrder("employee")); + } + + @Test + public void shouldDropAllLabelsViaTraversal() { + final Vertex v = g.addV("person").addLabel("employee").next(); + g.V(v).dropLabels().iterate(); + assertThat(v.labels(), hasSize(1)); + assertThat(v.labels(), containsInAnyOrder(Vertex.DEFAULT_LABEL)); + } + + @Test + public void shouldDropLabelWithConstantTraversal() { + final Vertex v = g.addV("person").addLabel("employee").next(); + g.V(v).dropLabel(constant("person")).iterate(); + assertThat(v.labels(), hasSize(1)); + assertThat(v.labels(), containsInAnyOrder("employee")); + } + + @Test(expected = UnsupportedOperationException.class) + public void shouldThrowWhenDroppingLabelsOnEdgeViaTraversal() { + final Vertex v1 = g.addV("person").next(); + final Vertex v2 = g.addV("person").next(); + v1.addEdge("knows", v2); + g.E().dropLabels().iterate(); + } + + // --- addV multi-label tests --- + + @Test + public void shouldCreateVertexWithMultipleLabelsViaAddV() { + final Vertex v = g.addV("a").addLabel("b").next(); + assertThat(v.labels(), hasSize(2)); + assertThat(v.labels(), containsInAnyOrder("a", "b")); + } + + @Test + public void shouldCreateVertexWithDefaultLabelViaAddV() { + final Vertex v = g.addV().next(); + assertThat(v.labels(), hasSize(1)); + assertThat(v.labels(), containsInAnyOrder(Vertex.DEFAULT_LABEL)); + } + + // --- hasLabel index consistency tests --- + + @Test + public void shouldFindVertexByLabelAfterAddLabel() { + final Vertex v = g.addV("person").next(); + g.V(v).addLabel("employee").iterate(); + final List<Vertex> found = g.V().hasLabel("employee").toList(); + assertThat(found, hasSize(1)); + assertThat(found.get(0).id(), is(v.id())); + } + + @Test + public void shouldNotFindVertexByLabelAfterDropLabel() { + final Vertex v = g.addV("person").addLabel("employee").next(); + g.V(v).dropLabel("employee").iterate(); + final List<Vertex> found = g.V().hasLabel("employee").toList(); + assertThat(found, hasSize(0)); + } + + @Test + public void shouldFindVertexByBothLabelsWithChainedHasLabel() { + final Vertex v = g.addV("person").addLabel("employee").next(); + final List<Vertex> found = g.V().hasLabel("person").hasLabel("employee").toList(); + assertThat(found, hasSize(1)); + assertThat(found.get(0).id(), is(v.id())); + } + + @Test + public void shouldNotFindVertexMissingOneOfChainedHasLabels() { + final Vertex v = g.addV("person").next(); + final List<Vertex> found = g.V().hasLabel("person").hasLabel("employee").toList(); + assertThat(found, hasSize(0)); + } + + // --- valueMap/elementMap multilabel config tests --- + + @SuppressWarnings("unchecked") + @Test + public void shouldReturnLabelsAsSetWithMultilabelConfig() { + final Vertex v = g.addV("person").addLabel("employee").next(); + final Map<Object, Object> map = g.V(v).valueMap(true) + .with(WithOptions.multilabel).next(); + final Object labelValue = map.get(T.label); + assertThat(labelValue, instanceOf(Set.class)); + final Set<String> labels = (Set<String>) labelValue; + assertThat(labels, containsInAnyOrder("person", "employee")); + } + + @SuppressWarnings("deprecation") + @Test + public void shouldReturnLabelAsSingleStringWithoutMultilabelConfig() { + final Vertex v = g.addV("person").next(); + final Map<Object, Object> map = g.V(v).valueMap(true).next(); + final Object labelValue = map.get(T.label); + assertThat(labelValue, instanceOf(String.class)); + assertThat((String) labelValue, is("person")); + } + + // --- addLabel pass-through (chaining) test --- + + @Test + public void shouldReturnSameVertexAfterAddLabel() { + final Vertex v = g.addV("person").next(); + final Vertex result = g.V(v).addLabel("employee").next(); + assertThat(result.id(), is(v.id())); + assertThat(result.labels(), containsInAnyOrder("person", "employee")); + } + + // --- dropLabel pass-through (chaining) test --- + + @Test + public void shouldReturnSameVertexAfterDropLabel() { + final Vertex v = g.addV("person").addLabel("employee").next(); + final Vertex result = g.V(v).dropLabel("employee").next(); + assertThat(result.id(), is(v.id())); + } + + // --- elementMap multilabel config test --- + + @SuppressWarnings("unchecked") + @Test + public void shouldReturnLabelsAsSetWithElementMapMultilabelConfig() { + final Vertex v = g.addV("person").addLabel("employee").next(); + final Map<Object, Object> map = g.V(v).elementMap() + .with(WithOptions.multilabel).next(); + final Object labelValue = map.get(T.label); + assertThat(labelValue, instanceOf(Set.class)); + final Set<String> labels = (Set<String>) labelValue; + assertThat(labels, containsInAnyOrder("person", "employee")); + } + + // --- GraphTraversalSource multi-label addV test --- + + @Test + public void shouldCreateVertexWithMultipleLabelsFromSource() { + final Vertex v = g.addV("person", "employee").next(); + assertThat(v.labels(), hasSize(2)); + assertThat(v.labels(), containsInAnyOrder("person", "employee")); + } + + @Test + public void shouldCreateVertexWithThreeLabelsFromSource() { + final Vertex v = g.addV("person", "employee", "manager").next(); + assertThat(v.labels(), hasSize(3)); + assertThat(v.labels(), containsInAnyOrder("person", "employee", "manager")); + } +} diff --git a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertexMultiLabelGremlinLangTest.java b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertexMultiLabelGremlinLangTest.java new file mode 100644 index 0000000000..99d373bcdd --- /dev/null +++ b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertexMultiLabelGremlinLangTest.java @@ -0,0 +1,121 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.tinkerpop.gremlin.tinkergraph.structure; + +import org.apache.tinkerpop.gremlin.process.remote.EmbeddedRemoteConnection; +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal; +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; +import org.apache.tinkerpop.gremlin.structure.Graph; +import org.apache.tinkerpop.gremlin.structure.Vertex; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.util.List; + +import static org.apache.tinkerpop.gremlin.process.traversal.AnonymousTraversalSource.traversal; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.containsString; + +/** + * Tests that verify GremlinLang output for multi-label addV does not double-record + * addLabel steps, and that the EmbeddedRemoteConnection path works correctly. + */ +public class TinkerVertexMultiLabelGremlinLangTest { + + private Graph graph; + private GraphTraversalSource g; + + @Before + public void setup() { + graph = TinkerGraph.open(); + g = graph.traversal(); + } + + @After + public void tearDown() throws Exception { + graph.close(); + } + + @Test + public void shouldNotDoubleRecordAddLabelInGremlinLangFromSource() { + final GraphTraversal<Vertex, Vertex> t = g.addV("a", "b"); + final String gremlinLang = t.asAdmin().getGremlinLang().getGremlin(); + // Should contain addV("a","b") but NOT a trailing addLabel + assertThat(gremlinLang, not(containsString("addLabel"))); + } + + @Test + public void shouldNotDoubleRecordAddLabelInGremlinLangFromTraversal() { + final GraphTraversal<Vertex, Vertex> t = g.V().addV("a", "b"); + final String gremlinLang = t.asAdmin().getGremlinLang().getGremlin(); + // Should contain addV("a","b") but NOT a trailing addLabel + assertThat(gremlinLang, not(containsString("addLabel"))); + } + + @Test + public void shouldNotDoubleRecordAddLabelWithThreeLabelsFromSource() { + final GraphTraversal<Vertex, Vertex> t = g.addV("a", "b", "c"); + final String gremlinLang = t.asAdmin().getGremlinLang().getGremlin(); + assertThat(gremlinLang, not(containsString("addLabel"))); + } + + @Test + public void shouldCreateExactlyOneVertexViaEmbeddedRemoteConnection() throws Exception { + final GraphTraversalSource remoteG = traversal().with(new EmbeddedRemoteConnection(g)); + try { + final List<Vertex> vertices = remoteG.addV("a", "b").toList(); + assertThat(vertices, hasSize(1)); + assertThat(vertices.get(0).labels(), hasSize(2)); + assertThat(vertices.get(0).labels(), containsInAnyOrder("a", "b")); + } finally { + remoteG.close(); + } + } + + @Test + public void shouldCreateExactlyOneVertexWithThreeLabelsViaEmbeddedRemote() throws Exception { + final GraphTraversalSource remoteG = traversal().with(new EmbeddedRemoteConnection(g)); + try { + final List<Vertex> vertices = remoteG.addV("x", "y", "z").toList(); + assertThat(vertices, hasSize(1)); + assertThat(vertices.get(0).labels(), hasSize(3)); + assertThat(vertices.get(0).labels(), containsInAnyOrder("x", "y", "z")); + } finally { + remoteG.close(); + } + } + + @Test + public void shouldNotDuplicateVerticesOnRepeatedEmbeddedRemoteCalls() throws Exception { + final GraphTraversalSource remoteG = traversal().with(new EmbeddedRemoteConnection(g)); + try { + remoteG.addV("a", "b").iterate(); + remoteG.addV("a", "b").iterate(); + final long count = g.V().count().next(); + assertThat(count, is(2L)); + } finally { + remoteG.close(); + } + } +} 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 new file mode 100644 index 0000000000..5fc84f8de2 --- /dev/null +++ b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertexMultiLabelTest.java @@ -0,0 +1,191 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.tinkerpop.gremlin.tinkergraph.structure; + +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; +import org.apache.tinkerpop.gremlin.structure.Edge; +import org.apache.tinkerpop.gremlin.structure.Graph; +import org.apache.tinkerpop.gremlin.structure.Vertex; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.util.Set; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; + +/** + * Tests for multi-label support on TinkerVertex. + */ +public class TinkerVertexMultiLabelTest { + + private Graph graph; + private GraphTraversalSource g; + + @Before + public void setup() { + graph = TinkerGraph.open(); + g = graph.traversal(); + } + + @After + public void tearDown() throws Exception { + graph.close(); + } + + @Test + public void shouldCreateVertexWithSingleLabel() { + final Vertex v = g.addV("person").next(); + assertThat(v.labels(), hasSize(1)); + assertThat(v.labels(), containsInAnyOrder("person")); + } + + @Test + public void shouldCreateVertexWithMultipleLabels() { + final Vertex v = g.addV("person").addLabel("employee").next(); + assertThat(v.labels(), hasSize(2)); + assertThat(v.labels(), containsInAnyOrder("person", "employee")); + } + + @Test + public void shouldCreateVertexWithDefaultLabelWhenNoneSpecified() { + final Vertex v = g.addV().next(); + assertThat(v.labels(), hasSize(1)); + assertThat(v.labels(), containsInAnyOrder(Vertex.DEFAULT_LABEL)); + } + + @SuppressWarnings("deprecation") + @Test + public void shouldReturnFirstLabelFromDeprecatedLabelMethod() { + final Vertex v = g.addV("person").next(); + assertThat(v.label(), is("person")); + assertThat(v.labels().contains(v.label()), is(true)); + } + + @Test(expected = UnsupportedOperationException.class) + public void shouldReturnUnmodifiableSetFromLabels() { + final Vertex v = g.addV("person").next(); + v.labels().add("hacker"); + } + + @Test + public void shouldAddLabelToExistingVertex() { + final Vertex v = g.addV("person").next(); + v.addLabel("employee"); + assertThat(v.labels(), hasSize(2)); + assertThat(v.labels(), containsInAnyOrder("person", "employee")); + } + + @Test + public void shouldBeIdempotentWhenAddingExistingLabel() { + final Vertex v = g.addV("person").next(); + v.addLabel("person"); + assertThat(v.labels(), hasSize(1)); + assertThat(v.labels(), containsInAnyOrder("person")); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowWhenAddingNullLabel() { + final Vertex v = g.addV("person").next(); + v.addLabel(null); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowWhenAddingEmptyLabel() { + final Vertex v = g.addV("person").next(); + v.addLabel(""); + } + + @Test + public void shouldDropAllLabelsAndAssignDefault() { + final Vertex v = g.addV("person").addLabel("employee").next(); + v.dropLabels(); + assertThat(v.labels(), hasSize(1)); + assertThat(v.labels(), containsInAnyOrder(Vertex.DEFAULT_LABEL)); + } + + @Test + public void shouldDropSpecificLabel() { + final Vertex v = g.addV("person").addLabel("employee").next(); + v.dropLabel("person"); + assertThat(v.labels(), hasSize(1)); + assertThat(v.labels(), containsInAnyOrder("employee")); + } + + @Test + public void shouldBeNoOpWhenDroppingNonExistentLabel() { + final Vertex v = g.addV("person").next(); + final Set<String> before = Set.copyOf(v.labels()); + v.dropLabel("nonexistent"); + assertThat(v.labels(), is(before)); + } + + @Test + public void shouldAssignDefaultWhenDroppingLastSpecificLabel() { + final Vertex v = g.addV("person").next(); + v.dropLabel("person"); + assertThat(v.labels(), hasSize(1)); + assertThat(v.labels(), containsInAnyOrder(Vertex.DEFAULT_LABEL)); + } + + @Test(expected = UnsupportedOperationException.class) + public void shouldThrowWhenAddingLabelToEdge() { + final Vertex v1 = g.addV("person").next(); + final Vertex v2 = g.addV("person").next(); + final Edge e = v1.addEdge("knows", v2); + e.addLabel("friend"); + } + + @Test(expected = UnsupportedOperationException.class) + public void shouldThrowWhenDroppingLabelsOnEdge() { + final Vertex v1 = g.addV("person").next(); + final Vertex v2 = g.addV("person").next(); + final Edge e = v1.addEdge("knows", v2); + e.dropLabels(); + } + + @Test + public void shouldReturnSingletonSetForEdgeLabels() { + final Vertex v1 = g.addV("person").next(); + final Vertex v2 = g.addV("person").next(); + final Edge e = v1.addEdge("knows", v2); + assertThat(e.labels(), hasSize(1)); + assertThat(e.labels(), containsInAnyOrder("knows")); + } + + @Test + public void shouldRemoveDefaultLabelWhenAddingFirstRealLabel() { + final Vertex v = g.addV().next(); + assertThat(v.labels(), containsInAnyOrder(Vertex.DEFAULT_LABEL)); + v.addLabel("person"); + assertThat(v.labels(), hasSize(1)); + assertThat(v.labels(), containsInAnyOrder("person")); + } + + @Test + public void shouldDeduplicateLabelsOnAddV() { + final Vertex v = g.addV("person").addLabel("person").next(); + // addLabel("person") on a vertex already labeled "person" is idempotent + assertThat(v.labels(), hasSize(1)); + assertThat(v.labels(), containsInAnyOrder("person")); + } +}
