From da4ec2ac208e08c82cf4611e3e4f171804467319 Mon Sep 17 00:00:00 2001 From: xiazcy Date: Fri, 26 Jun 2026 16:11:39 -0700 Subject: [PATCH 01/29] Add multi-label vertex support: core data model, steps, and grammar - LabelCardinality enum (ONE, ONE_OR_MORE, ZERO_OR_MORE) on Graph.Features - Element.labels() returns Set, addLabel()/dropLabels() mutation steps - LabelsStep for g.V().labels() traversal - LabelCardinalityValidator for constraint enforcement - LabelsDropVerificationStrategy prevents accidental label removal - MergeVertex supports multi-label via T.label list in merge map - elementMap()/valueMap() label output controlled by with("multilabel")/with("singlelabel") - GremlinLang propagates with("multilabel")/with("singlelabel") in gremlin text - Grammar rules for addLabel(), dropLabels(), labels(), addV(String...) - Java test infrastructure: World.getMultiLabelGraphTraversalSource(), @MultiLabel/@MultiLabelDefault/@SingleLabelDefault tags - GraphBinary V4 serialization for multi-label vertices/edges --- .../grammar/DefaultGremlinBaseVisitor.java | 31 ++++ .../grammar/TraversalMethodVisitor.java | 94 +++++++++++ .../TraversalSourceSpawnMethodVisitor.java | 33 +++- .../process/computer/util/ComputerGraph.java | 5 + .../process/traversal/GremlinLang.java | 8 + .../traversal/TraversalStrategies.java | 3 + .../traversal/dsl/graph/GraphTraversal.java | 112 +++++++++++++ .../dsl/graph/GraphTraversalSource.java | 29 ++++ .../process/traversal/dsl/graph/__.java | 49 ++++++ .../AbstractAddElementStepPlaceholder.java | 7 +- .../map/AbstractAddVertexStepPlaceholder.java | 12 ++ .../step/map/AddVertexStartStep.java | 18 +- .../map/AddVertexStartStepPlaceholder.java | 23 ++- .../traversal/step/map/AddVertexStep.java | 18 +- .../step/map/AddVertexStepPlaceholder.java | 10 +- .../traversal/step/map/ElementMapStep.java | 19 ++- .../traversal/step/map/LabelsStep.java | 53 ++++++ .../traversal/step/map/MergeElementStep.java | 59 +++++-- .../traversal/step/map/MergeVertexStep.java | 21 ++- .../traversal/step/map/PropertyMapStep.java | 20 ++- .../step/sideEffect/AddLabelStep.java | 145 ++++++++++++++++ .../step/sideEffect/DropLabelsStep.java | 156 ++++++++++++++++++ .../traversal/step/util/HasContainer.java | 9 +- .../traversal/step/util/WithOptions.java | 28 ++++ .../traversal/step/util/event/Event.java | 26 +++ .../traversal/step/util/event/EventUtil.java | 20 +++ .../LabelsDropVerificationStrategy.java | 66 ++++++++ .../tinkerpop/gremlin/structure/Element.java | 56 +++++++ .../tinkerpop/gremlin/structure/Graph.java | 33 ++++ .../gremlin/structure/LabelCardinality.java | 85 ++++++++++ .../gremlin/structure/VertexProperty.java | 14 ++ .../io/binary/types/EdgeSerializer.java | 56 +++++-- .../io/binary/types/GraphSerializer.java | 6 +- .../io/binary/types/VertexSerializer.java | 25 ++- .../io/graphson/GraphSONSerializersV4.java | 34 +++- .../gremlin/structure/util/ElementHelper.java | 59 +++++++ .../util/LabelCardinalityValidator.java | 104 ++++++++++++ .../structure/util/detached/DetachedEdge.java | 6 + .../util/detached/DetachedElement.java | 40 ++++- .../util/detached/DetachedVertex.java | 6 + .../util/reference/ReferenceEdge.java | 8 + .../util/reference/ReferenceElement.java | 39 +++++ .../util/reference/ReferenceVertex.java | 6 + .../structure/util/star/StarGraph.java | 82 ++++++++- .../LabelsDropVerificationStrategyTest.java | 85 ++++++++++ gremlin-language/src/main/antlr4/Gremlin.g4 | 30 ++++ .../gremlin/driver/remote/RemoteWorld.java | 30 ++++ .../server/util/CheckedGraphManagerTest.java | 2 +- .../server/util/DefaultGraphManagerTest.java | 10 +- .../server/gremlin-server-integration.yaml | 3 + .../scripts/tinkergraph-multilabel.properties | 21 +++ .../gremlin/features/StepDefinition.java | 18 +- .../tinkerpop/gremlin/features/World.java | 21 +++ .../structure/ExceptionCoverageTest.java | 3 + .../binary/TypeSerializerFailureTests.java | 7 +- 55 files changed, 1879 insertions(+), 84 deletions(-) create mode 100644 gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/LabelsStep.java create mode 100644 gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/AddLabelStep.java create mode 100644 gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/DropLabelsStep.java create mode 100644 gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/strategy/verification/LabelsDropVerificationStrategy.java create mode 100644 gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/LabelCardinality.java create mode 100644 gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/LabelCardinalityValidator.java create mode 100644 gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/strategy/verification/LabelsDropVerificationStrategyTest.java create mode 100644 gremlin-server/src/test/scripts/tinkergraph-multilabel.properties 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 0431a36f056..b6b38b0b438 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/DefaultGremlinBaseVisitor.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/DefaultGremlinBaseVisitor.java @@ -167,6 +167,9 @@ protected void notImplemented(final ParseTree ctx) { * {@inheritDoc} */ @Override public T visitTraversalMethod_addE_String(final GremlinParser.TraversalMethod_addE_StringContext ctx) { notImplemented(ctx); return null; } + /** + * {@inheritDoc} + */ /** * {@inheritDoc} */ @@ -183,6 +186,10 @@ protected void notImplemented(final ParseTree ctx) { * {@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 +546,30 @@ protected void notImplemented(final ParseTree ctx) { * {@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 2c4980224a8..216af370b46 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.VertexProperty.Cardinality; import java.util.Arrays; +import java.util.List; import java.util.Map; import java.util.function.BiFunction; @@ -97,6 +98,25 @@ public GraphTraversal visitTraversalMethod_addV_String(final GremlinParser.Trave } } + /** + * {@inheritDoc} + */ + @Override + public GraphTraversal visitTraversalMethod_addV_StringVarargs(final GremlinParser.TraversalMethod_addV_StringVarargsContext ctx) { + final List args = ctx.stringArgument(); + final Object firstLiteralOrVar = antlr.argumentVisitor.visitStringArgument(args.get(0)); + final String firstLabel = firstLiteralOrVar instanceof String ? (String) firstLiteralOrVar : ((GValue) firstLiteralOrVar).get(); + final Object secondLiteralOrVar = antlr.argumentVisitor.visitStringArgument(args.get(1)); + final String secondLabel = secondLiteralOrVar instanceof String ? (String) secondLiteralOrVar : ((GValue) 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) literalOrVar).get(); + } + return this.graphTraversal.addV(firstLabel, secondLabel, moreLabels); + } + /** * {@inheritDoc} */ @@ -978,6 +998,80 @@ public GraphTraversal visitTraversalMethod_label(final GremlinParser.TraversalMe 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 args = ctx.stringArgument(); + final Object firstLiteralOrVar = antlr.argumentVisitor.visitStringArgument(args.get(0)); + final String firstLabel = firstLiteralOrVar instanceof String ? (String) firstLiteralOrVar : ((GValue) 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) 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 args = ctx.stringArgument(); + final Object firstLiteralOrVar = antlr.argumentVisitor.visitStringArgument(args.get(0)); + final String firstLabel = firstLiteralOrVar instanceof String ? (String) firstLiteralOrVar : ((GValue) 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) 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 1b70a58c8d1..cd50ccc1c92 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.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; /** @@ -56,8 +57,9 @@ public GraphTraversal visitTraversalSourceSpawnMethod(final GremlinParser.Traver */ @Override public GraphTraversal visitTraversalSourceSpawnMethod_addE(final GremlinParser.TraversalSourceSpawnMethod_addEContext ctx) { - if (ctx.stringArgument() != null) { - final Object literalOrVar = antlr.argumentVisitor.visitStringArgument(ctx.stringArgument()); + final GremlinParser.StringArgumentContext stringArg = ctx.stringArgument(); + if (stringArg != null) { + final Object literalOrVar = antlr.argumentVisitor.visitStringArgument(stringArg); if (GValue.valueInstanceOf(literalOrVar, String.class)) { return this.traversalSource.addE((GValue) literalOrVar); } else { @@ -75,12 +77,29 @@ public GraphTraversal visitTraversalSourceSpawnMethod_addE(final GremlinParser.T */ @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) literalOrVar); + final List 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) 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) 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) 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) 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 3a8a29e7436..5634cbbc091 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 String label() { throw GraphComputer.Exceptions.adjacentVertexLabelsCanNotBeRead(); } + @Override + public Set 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/GremlinLang.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLang.java index 3bd1cb5287c..a93998a6d2d 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLang.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLang.java @@ -501,6 +501,14 @@ private String buildStrategyArgs(final Object[] arguments) { // special handling for OptionsStrategy if (arguments[i] instanceof OptionsStrategy) { optionsStrategies.add((OptionsStrategy) arguments[i]); + // Render multilabel/singlelabel in gremlin text (temporary until these options are removed) + final Configuration configuration = ((OptionsStrategy) arguments[i]).getConfiguration(); + if (configuration.containsKey("multilabel")) { + gremlin.append(".with(\"multilabel\")"); + } + if (configuration.containsKey("singlelabel")) { + gremlin.append(".with(\"singlelabel\")"); + } break; } diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/TraversalStrategies.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/TraversalStrategies.java index bccb983665e..509b9b7d8d0 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/TraversalStrategies.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/TraversalStrategies.java @@ -54,6 +54,7 @@ import org.apache.tinkerpop.gremlin.process.traversal.strategy.optimization.RepeatUnrollStrategy; import org.apache.tinkerpop.gremlin.process.traversal.strategy.verification.ComputerVerificationStrategy; import org.apache.tinkerpop.gremlin.process.traversal.strategy.verification.EdgeLabelVerificationStrategy; +import org.apache.tinkerpop.gremlin.process.traversal.strategy.verification.LabelsDropVerificationStrategy; import org.apache.tinkerpop.gremlin.process.traversal.strategy.verification.LambdaRestrictionStrategy; import org.apache.tinkerpop.gremlin.process.traversal.strategy.verification.ReadOnlyStrategy; import org.apache.tinkerpop.gremlin.process.traversal.strategy.verification.ReservedKeysVerificationStrategy; @@ -263,6 +264,7 @@ public static final class GlobalCache { // verification put(EdgeLabelVerificationStrategy.class.getSimpleName(), EdgeLabelVerificationStrategy.class); + put(LabelsDropVerificationStrategy.class.getSimpleName(), LabelsDropVerificationStrategy.class); put(LambdaRestrictionStrategy.class.getSimpleName(), LambdaRestrictionStrategy.class); put(ReadOnlyStrategy.class.getSimpleName(), ReadOnlyStrategy.class); put(ReservedKeysVerificationStrategy.class.getSimpleName(), ReservedKeysVerificationStrategy.class); @@ -287,6 +289,7 @@ public static final class GlobalCache { LazyBarrierStrategy.instance(), ProfileStrategy.instance(), StandardVerificationStrategy.instance(), + LabelsDropVerificationStrategy.instance(), GValueReductionStrategy.instance()); registerStrategies(Graph.class, graphStrategies); registerStrategies(EmptyGraph.class, new DefaultTraversalStrategies()); 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 df50a92462e..3e0a8471544 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 @@ -66,6 +66,8 @@ 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; @@ -133,6 +135,7 @@ 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; @@ -232,6 +235,7 @@ import java.util.Date; import java.util.Iterator; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Optional; @@ -351,12 +355,26 @@ public default GraphTraversal id() { * @return the traversal with an appended {@link LabelStep}. * @see Reference Documentation - Label Step * @since 3.0.0-incubating + * @deprecated As of release 4.0.0, replaced by {@link #labels()}. */ + @Deprecated public default GraphTraversal 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 labels() { + this.asAdmin().getGremlinLang().addStep(Symbols.labels); + return this.asAdmin().addStep(new LabelsStep<>(this.asAdmin())); + } + /** * Map the E object to itself. In other words, a "no op." * @@ -1466,6 +1484,30 @@ public default GraphTraversal addV() { 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 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); + final Set allLabels = new LinkedHashSet<>(); + allLabels.add(label1); + allLabels.add(label2); + Collections.addAll(allLabels, moreLabels); + return this.asAdmin().addStep(new AddVertexStepPlaceholder<>(this.asAdmin(), allLabels)); + } + /** * 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 @@ -3463,6 +3505,72 @@ public default GraphTraversal drop() { 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 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 addLabel(final Traversal 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 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 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 dropLabel(final Traversal labelTraversal) { + this.asAdmin().getGremlinLang().addStep(Symbols.dropLabel, labelTraversal); + return this.asAdmin().addStep((DropLabelsStep) new DropLabelsStep(this.asAdmin(), labelTraversal.asAdmin())); + } + /** * Filters E lists given the provided {@code predicate}. * @@ -4760,6 +4868,7 @@ private Symbols() { 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"; @@ -4866,6 +4975,9 @@ private Symbols() { 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 fa9be0a639e..d4f29e30567 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 @@ -55,9 +55,12 @@ import org.slf4j.LoggerFactory; import java.util.Arrays; +import java.util.Collections; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.function.BinaryOperator; import java.util.function.Consumer; import java.util.function.Function; @@ -373,6 +376,32 @@ public GraphTraversal addV(final GValue vertexLabel) { 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 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 traversal = new DefaultGraphTraversal<>(clone); + final Set allLabels = new LinkedHashSet<>(); + allLabels.add(label1); + allLabels.add(label2); + Collections.addAll(allLabels, moreLabels); + return traversal.addStep(new AddVertexStartStepPlaceholder(traversal, allLabels)); + } + /** * 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 390c4cc48fb..421a1f33ad3 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 @@ -128,6 +128,48 @@ public static GraphTraversal label() { return __.start().label(); } + /** + * @see GraphTraversal#labels() + */ + public static GraphTraversal labels() { + return __.start().labels(); + } + + /** + * @see GraphTraversal#addLabel(String, String...) + */ + public static GraphTraversal addLabel(final String label, final String... moreLabels) { + return __.start().addLabel(label, moreLabels); + } + + /** + * @see GraphTraversal#addLabel(Traversal) + */ + public static GraphTraversal addLabel(final Traversal labelTraversal) { + return __.start().addLabel(labelTraversal); + } + + /** + * @see GraphTraversal#dropLabels() + */ + public static GraphTraversal dropLabels() { + return __.start().dropLabels(); + } + + /** + * @see GraphTraversal#dropLabel(String, String...) + */ + public static GraphTraversal dropLabel(final String label, final String... moreLabels) { + return __.start().dropLabel(label, moreLabels); + } + + /** + * @see GraphTraversal#dropLabel(Traversal) + */ + public static GraphTraversal dropLabel(final Traversal labelTraversal) { + return __.start().dropLabel(labelTraversal); + } + /** * @see GraphTraversal#id() */ @@ -672,6 +714,13 @@ public static GraphTraversal addV() { return __.start().addV(); } + /** + * @see GraphTraversal#addV(String, String, String...) + */ + public static GraphTraversal addV(final String label1, final String label2, final String... moreLabels) { + return __.start().addV(label1, label2, moreLabels); + } + /** * @see GraphTraversal#mergeV() */ diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AbstractAddElementStepPlaceholder.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AbstractAddElementStepPlaceholder.java index 830fef2c80e..a2015b46130 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AbstractAddElementStepPlaceholder.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AbstractAddElementStepPlaceholder.java @@ -61,7 +61,7 @@ protected AbstractAddElementStepPlaceholder(final Traversal.Admin traversal, fin protected AbstractAddElementStepPlaceholder(final Traversal.Admin traversal, final GValue label) { super(traversal); this.label = label == null ? this.getDefaultLabel() : label; - if (label.isVariable()) { + if (label != null && label.isVariable()) { traversal.getGValueManager().register(label); } } @@ -75,6 +75,11 @@ protected AbstractAddElementStepPlaceholder(final Traversal.Admin traversal, fin addTraversal(labelTraversal); } + protected AbstractAddElementStepPlaceholder(final Traversal.Admin traversal, final Set labels) { + super(traversal); + this.label = (labels == null || labels.isEmpty()) ? this.getDefaultLabel() : labels; + } + protected abstract String getDefaultLabel(); @Override diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AbstractAddVertexStepPlaceholder.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AbstractAddVertexStepPlaceholder.java index 66aa3df1168..f14e9998d06 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AbstractAddVertexStepPlaceholder.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AbstractAddVertexStepPlaceholder.java @@ -25,6 +25,7 @@ import org.apache.tinkerpop.gremlin.structure.Vertex; import java.util.Objects; +import java.util.Set; public abstract class AbstractAddVertexStepPlaceholder extends AbstractAddElementStepPlaceholder implements AddVertexStepContract, GValueHolder { @@ -46,11 +47,22 @@ protected AbstractAddVertexStepPlaceholder(final Traversal.Admin traversal, fina userProvidedLabel = vertexLabelTraversal != null; } + protected AbstractAddVertexStepPlaceholder(final Traversal.Admin traversal, final Set labels) { + super(traversal, labels); + userProvidedLabel = labels != null && !labels.isEmpty(); + } + @Override protected String getDefaultLabel() { return Vertex.DEFAULT_LABEL; } + @Override + public void setLabel(Object label) { + super.setLabel(label); + userProvidedLabel = true; + } + @Override public boolean hasUserProvidedLabel() { return userProvidedLabel; diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStartStep.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStartStep.java index 7718cc28831..4e256fc96be 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStartStep.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStartStep.java @@ -53,16 +53,30 @@ public class AddVertexStartStep extends AbstractStep implements public AddVertexStartStep(final Traversal.Admin traversal, final String label) { super(traversal); - this.internalParameters.set(this, T.label, null == label ? Vertex.DEFAULT_LABEL : label); + if (label != null) { + this.internalParameters.set(this, T.label, label); + } userProvidedLabel = label != null; } public AddVertexStartStep(final Traversal.Admin traversal, final Traversal vertexLabelTraversal) { super(traversal); - this.internalParameters.set(this, T.label, null == vertexLabelTraversal ? Vertex.DEFAULT_LABEL : vertexLabelTraversal); + if (vertexLabelTraversal != null) { + this.internalParameters.set(this, T.label, vertexLabelTraversal); + } userProvidedLabel = vertexLabelTraversal != null; } + public AddVertexStartStep(final Traversal.Admin traversal, final Set labels) { + super(traversal); + if (labels != null && !labels.isEmpty()) { + this.internalParameters.set(this, T.label, labels); + userProvidedLabel = true; + } else { + userProvidedLabel = false; + } + } + @Override public boolean hasUserProvidedLabel() { return userProvidedLabel; diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStartStepPlaceholder.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStartStepPlaceholder.java index e84f4a7f1c4..24c25d3ad45 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStartStepPlaceholder.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStartStepPlaceholder.java @@ -19,36 +19,45 @@ package org.apache.tinkerpop.gremlin.process.traversal.step.map; import org.apache.tinkerpop.gremlin.process.traversal.Traversal; -import org.apache.tinkerpop.gremlin.process.traversal.lambda.ConstantTraversal; import org.apache.tinkerpop.gremlin.process.traversal.step.GValue; import org.apache.tinkerpop.gremlin.process.traversal.step.GValueHolder; import org.apache.tinkerpop.gremlin.structure.Vertex; +import java.util.Set; + public class AddVertexStartStepPlaceholder extends AbstractAddVertexStepPlaceholder implements AddVertexStepContract, GValueHolder { public AddVertexStartStepPlaceholder(final Traversal.Admin traversal, final String label) { - super(traversal, label == null ? Vertex.DEFAULT_LABEL : label); + super(traversal, label); } public AddVertexStartStepPlaceholder(final Traversal.Admin traversal, final GValue label) { - super(traversal, label == null ? GValue.of(Vertex.DEFAULT_LABEL) : label); + super(traversal, label); } public AddVertexStartStepPlaceholder(final Traversal.Admin traversal, final Traversal.Admin vertexLabelTraversal) { - super(traversal, vertexLabelTraversal == null ? - new ConstantTraversal<>(Vertex.DEFAULT_LABEL) : (Traversal.Admin) vertexLabelTraversal); + super(traversal, vertexLabelTraversal == null ? null : (Traversal.Admin) vertexLabelTraversal); + } + + public AddVertexStartStepPlaceholder(final Traversal.Admin traversal, final Set labels) { + super(traversal, labels); } @Override public AddVertexStartStep asConcreteStep() { AddVertexStartStep step; - if (label instanceof Traversal) { + if (label instanceof Set) { + step = new AddVertexStartStep(traversal, (Set) label); + } else if (label instanceof Traversal) { step = new AddVertexStartStep(traversal, ((Traversal) label).asAdmin()); } else if (label instanceof GValue) { step = new AddVertexStartStep(traversal, ((GValue) label).get()); } else { - step = new AddVertexStartStep(traversal, (String) label); + // When userProvidedLabel is false, label may be the default from the placeholder + // hierarchy. Pass null so AddVertexStartStep does not inject T.label. + final String labelStr = hasUserProvidedLabel() ? (String) label : null; + step = new AddVertexStartStep(traversal, labelStr); } super.configureConcreteStep(step); return step; diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStep.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStep.java index b015920e170..8a48d1728b5 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStep.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStep.java @@ -49,16 +49,30 @@ public class AddVertexStep extends ScalarMapStep implements AddVer public AddVertexStep(final Traversal.Admin traversal, final String label) { super(traversal); - this.internalParameters.set(this, T.label, null == label ? Vertex.DEFAULT_LABEL : label); + if (label != null) { + this.internalParameters.set(this, T.label, label); + } userProvidedLabel = label != null; } public AddVertexStep(final Traversal.Admin traversal, final Traversal.Admin vertexLabelTraversal) { super(traversal); - this.internalParameters.set(this, T.label, null == vertexLabelTraversal ? Vertex.DEFAULT_LABEL : vertexLabelTraversal); + if (vertexLabelTraversal != null) { + this.internalParameters.set(this, T.label, vertexLabelTraversal); + } userProvidedLabel = vertexLabelTraversal != null; } + public AddVertexStep(final Traversal.Admin traversal, final Set labels) { + super(traversal); + if (labels != null && !labels.isEmpty()) { + this.internalParameters.set(this, T.label, labels); + userProvidedLabel = true; + } else { + userProvidedLabel = false; + } + } + @Override public boolean hasUserProvidedLabel() { return userProvidedLabel; diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStepPlaceholder.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStepPlaceholder.java index 505bc47c1f3..b7497600513 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStepPlaceholder.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStepPlaceholder.java @@ -22,6 +22,8 @@ import org.apache.tinkerpop.gremlin.process.traversal.lambda.GValueConstantTraversal; import org.apache.tinkerpop.gremlin.process.traversal.step.GValue; +import java.util.Set; + public class AddVertexStepPlaceholder extends AbstractAddVertexStepPlaceholder { public AddVertexStepPlaceholder(Traversal.Admin traversal, String label) { @@ -36,10 +38,16 @@ public AddVertexStepPlaceholder(Traversal.Admin traversal, Traversal.Admin labels) { + super(traversal, labels); + } + @Override public AddVertexStep asConcreteStep() { AddVertexStep step; - if (label instanceof Traversal) { + if (label instanceof Set) { + step = new AddVertexStep<>(traversal, (Set) label); + } else if (label instanceof Traversal) { step = new AddVertexStep<>(traversal, ((Traversal) label).asAdmin()); } else if (label instanceof GValue) { step = new AddVertexStep<>(traversal, ((GValue) label).get()); diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/ElementMapStep.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/ElementMapStep.java index 972511a4411..94b59b87ec1 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 @@ -22,6 +22,7 @@ import org.apache.tinkerpop.gremlin.process.traversal.Traverser; 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.WithOptions; import org.apache.tinkerpop.gremlin.process.traversal.traverser.TraverserRequirement; import org.apache.tinkerpop.gremlin.structure.Direction; import org.apache.tinkerpop.gremlin.structure.Edge; @@ -50,6 +51,7 @@ public class ElementMapStep extends ScalarMapStep> imple protected final String[] propertyKeys; private boolean onGraphComputer = false; + private Boolean multilabelEnabled; public ElementMapStep(final Traversal.Admin traversal, final String... propertyKeys) { super(traversal); @@ -65,7 +67,11 @@ protected Map map(final Traverser.Admin traverser) { 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 +108,17 @@ public boolean isOnGraphComputer() { return onGraphComputer; } + /** + * Checks if multilabel mode is enabled via source-level {@code g.with("multilabel")}. + * Result is cached since strategies are immutable after traversal compilation. + */ + private boolean isMultilabelEnabled() { + if (multilabelEnabled == null) { + multilabelEnabled = WithOptions.isMultilabelEnabled(getTraversal()); + } + return multilabelEnabled; + } + 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 00000000000..a5ba8071769 --- /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 extends FlatMapStep { + + public LabelsStep(final Traversal.Admin traversal) { + super(traversal); + } + + @Override + protected Iterator flatMap(final Traverser.Admin traverser) { + return traverser.get().labels().iterator(); + } + + @Override + public Set 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 053213f96cc..6b7c261d487 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 @@ -254,7 +254,25 @@ protected static void validate(final Map map, final boolean ignoreTokens, final 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 - 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); @@ -271,12 +289,21 @@ protected static void validate(final Map map, final boolean ignoreTokens, final 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 - found: %s", + op, v.getClass().getSimpleName())); } } if (k == Direction.OUT && v instanceof Merge && v != Merge.outV) { @@ -337,10 +364,10 @@ protected CloseableIterator searchVertices(final Map search) { 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 @@ -351,8 +378,20 @@ protected GraphTraversal searchVerticesTraversal(final Graph graph, final Object 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 118468236f6..448ee768be0 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 @@ -36,6 +36,7 @@ import org.apache.tinkerpop.gremlin.structure.Vertex; import org.apache.tinkerpop.gremlin.structure.VertexProperty; import org.apache.tinkerpop.gremlin.structure.util.CloseableIterator; +import org.apache.tinkerpop.gremlin.structure.util.ElementHelper; import org.apache.tinkerpop.gremlin.util.iterator.IteratorUtils; import static java.util.stream.Collectors.toList; @@ -99,12 +100,24 @@ protected Iterator flatMap(final Traverser.Admin traverser) { traverser.set((S) v); // assume good input from GraphTraversal - folks might drop in a T here even though it is immutable - final Map onMatchMap = materializeMap(traverser, onMatchTraversal); + final Map onMatchMap = materializeMap(traverser, onMatchTraversal); validateMapInput(onMatchMap, true); + // Handle T.label separately: append-only addLabel semantics for multi-label support + Object labelValue = onMatchMap.get(T.label); + if (labelValue == null) { + labelValue = onMatchMap.get(T.label.getAccessor()); + } + if (labelValue != null) { + ElementHelper.applyLabelsToVertex(v, labelValue); + } + + // Remaining entries are all String property keys — skip T.label if present onMatchMap.forEach((key, value) -> { + if (T.label.equals(key) || T.label.getAccessor().equals(key)) 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 +128,10 @@ protected Iterator flatMap(final Traverser.Admin traverser) { // 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 489fea88952..3be7d728649 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 @@ -61,6 +61,7 @@ public class PropertyMapStep extends ScalarMapStep> protected Parameters parameters = new Parameters(); protected Traversal.Admin valueTraversal; + private Boolean multilabelEnabled; public PropertyMapStep(final Traversal.Admin traversal, final PropertyType propertyType, final String... propertyKeys) { super(traversal); @@ -200,11 +201,28 @@ protected void addIncludedOptions(Element element, Map map){ 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 via source-level {@code g.with("multilabel")}. + * Result is cached since strategies are immutable after traversal compilation. + */ + private boolean isMultilabelEnabled() { + if (multilabelEnabled == null) { + multilabelEnabled = WithOptions.isMultilabelEnabled(getTraversal()); + } + return multilabelEnabled; + } + 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 00000000000..1cb2d011083 --- /dev/null +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/AddLabelStep.java @@ -0,0 +1,145 @@ +/* + * 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.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.LinkedHashSet; +import java.util.List; +import java.util.Objects; +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 extends SideEffectStep + implements Mutating, TraversalParent { + + private final String[] labels; + private Traversal.Admin labelTraversal; + private CallbackRegistry 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 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 labelTraversal) { + super(traversal); + this.labels = null; + this.labelTraversal = this.integrateChild(labelTraversal); + } + + @Override + protected void sideEffect(final Traverser.Admin traverser) { + final Element element = traverser.get(); + final Set oldLabels = new LinkedHashSet<>(element.labels()); + + if (this.labelTraversal != null) { + final List 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 only if labels actually changed + if (!oldLabels.equals(element.labels())) { + EventUtil.registerLabelChange(callbackRegistry, getTraversal(), element, oldLabels, element.labels()); + } + } + + @Override + public CallbackRegistry getMutatingCallbackRegistry() { + if (null == callbackRegistry) callbackRegistry = new ListCallbackRegistry<>(); + return callbackRegistry; + } + + @Override + public Set 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> getLocalChildren() { + return this.labelTraversal != null ? Collections.singletonList(this.labelTraversal) : Collections.emptyList(); + } + + @Override + public AddLabelStep clone() { + final AddLabelStep clone = (AddLabelStep) 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 00000000000..9812b0b7868 --- /dev/null +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/DropLabelsStep.java @@ -0,0 +1,156 @@ +/* + * 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.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.LinkedHashSet; +import java.util.List; +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 extends SideEffectStep + implements Mutating, TraversalParent { + + private final boolean dropAll; + private final String[] labels; + private Traversal.Admin labelTraversal; + private CallbackRegistry 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 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 labelTraversal) { + super(traversal); + this.dropAll = false; + this.labels = null; + this.labelTraversal = this.integrateChild(labelTraversal); + } + + @Override + protected void sideEffect(final Traverser.Admin traverser) { + final Element element = traverser.get(); + final Set oldLabels = new LinkedHashSet<>(element.labels()); + + 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 only if labels actually changed + if (!oldLabels.equals(element.labels())) { + EventUtil.registerLabelChange(callbackRegistry, getTraversal(), element, oldLabels, element.labels()); + } + } + + @Override + public CallbackRegistry getMutatingCallbackRegistry() { + if (null == callbackRegistry) callbackRegistry = new ListCallbackRegistry<>(); + return callbackRegistry; + } + + @Override + public Set 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> getLocalChildren() { + return this.labelTraversal != null ? Collections.singletonList(this.labelTraversal) : Collections.emptyList(); + } + + @Override + public DropLabelsStep clone() { + final DropLabelsStep clone = (DropLabelsStep) 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 99a9c4e959d..a6f7a39d9e9 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 @@ protected boolean testIdAsString(final Element 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 2d3c9405504..b87f10d8ba9 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/WithOptions.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/WithOptions.java @@ -18,8 +18,10 @@ */ package org.apache.tinkerpop.gremlin.process.traversal.step.util; +import org.apache.tinkerpop.gremlin.process.traversal.Traversal; import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal; import org.apache.tinkerpop.gremlin.process.traversal.step.map.IndexStep; +import org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.OptionsStrategy; import org.apache.tinkerpop.gremlin.structure.Edge; import org.apache.tinkerpop.gremlin.structure.Element; import org.apache.tinkerpop.gremlin.structure.Graph; @@ -101,4 +103,30 @@ public class WithOptions { * @since 4.0.0 */ public static final String queryLanguage = "queryLanguage"; + + // Multi-label configuration + // + + /** + * The user-facing key for multilabel configuration used with {@code g.with("multilabel")}. + */ + public static final String MULTILABEL_KEY = "multilabel"; + + /** + * The user-facing key for singlelabel override used with {@code g.with("singlelabel")}. + * When present, overrides multilabel to force single-label output in valueMap/elementMap steps. + */ + public static final String SINGLELABEL_KEY = "singlelabel"; + + /** + * Checks whether multi-label output is enabled for the given traversal via source-level + * {@code g.with("multilabel")} configuration. Returns {@code false} if {@code g.with("singlelabel")} + * is also present, as singlelabel overrides multilabel. + */ + public static boolean isMultilabelEnabled(final Traversal.Admin traversal) { + return traversal.getStrategies().getStrategy(OptionsStrategy.class) + .map(os -> os.getOptions().containsKey(MULTILABEL_KEY) + && !os.getOptions().containsKey(SINGLELABEL_KEY)) + .orElse(false); + } } diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/event/Event.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/event/Event.java index 4a9d9c06e61..f02e6bc63fe 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/event/Event.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/event/Event.java @@ -27,6 +27,7 @@ import org.apache.tinkerpop.gremlin.structure.VertexProperty; import java.util.Iterator; +import java.util.Set; /** * A representation of some action that occurs on a {@link Graph} for a {@link Traversal}. @@ -213,6 +214,31 @@ public void fireEvent(final Iterator eventListeners) { } } + /** + * An event that represents a label change on an element (vertex or edge). + * Fired when labels are added or removed via addLabel()/dropLabel()/dropLabels() steps. + */ + class ElementLabelChangedEvent implements Event { + private final Element element; + private final Set oldLabels; + private final Set newLabels; + + public ElementLabelChangedEvent(final Element element, final Set oldLabels, final Set newLabels) { + this.element = element; + this.oldLabels = oldLabels; + this.newLabels = newLabels; + } + + public Element getElement() { return element; } + public Set getOldLabels() { return oldLabels; } + public Set getNewLabels() { return newLabels; } + + @Override + public void fireEvent(final Iterator eventListeners) { + // Label change events are dispatched directly via CallbackRegistry, not MutationListener. + } + } + /** * A base class for {@link Property} mutation events. */ diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/event/EventUtil.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/event/EventUtil.java index 2fce97e1502..85d8072f89e 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/event/EventUtil.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/event/EventUtil.java @@ -27,6 +27,8 @@ import org.apache.tinkerpop.gremlin.structure.Vertex; import org.apache.tinkerpop.gremlin.structure.VertexProperty; +import java.util.Set; + /** * Logic for registering events with the {@link EventStrategy} and the callback registry. Extracting this logic allows * providers to reuse these utilities more readily. @@ -202,6 +204,24 @@ public static void registerPropertyChange(final CallbackRegistry callbackRegistry, + final Traversal.Admin traversal, final Element element, + final Set oldLabels, final Set newLabels) { + if (hasAnyCallbacks(callbackRegistry)) { + final EventStrategy eventStrategy = ((Traversal.Admin) traversal) + .getStrategies().getStrategy(EventStrategy.class).orElse(null); + final Element detachedElement = eventStrategy != null ? eventStrategy.detach(element) : element; + final Event.ElementLabelChangedEvent event = new Event.ElementLabelChangedEvent(detachedElement, oldLabels, newLabels); + for (EventCallback c : callbackRegistry.getCallbacks()) { + c.accept(event); + } + } + } + /** * Register a vertex property addition event with the callback registry. */ diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/strategy/verification/LabelsDropVerificationStrategy.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/strategy/verification/LabelsDropVerificationStrategy.java new file mode 100644 index 00000000000..b13a5b15262 --- /dev/null +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/strategy/verification/LabelsDropVerificationStrategy.java @@ -0,0 +1,66 @@ +/* + * 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.strategy.verification; + +import org.apache.tinkerpop.gremlin.process.traversal.Step; +import org.apache.tinkerpop.gremlin.process.traversal.Traversal; +import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategy; +import org.apache.tinkerpop.gremlin.process.traversal.step.filter.DropStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.filter.FilterStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.LabelsStep; +import org.apache.tinkerpop.gremlin.process.traversal.strategy.AbstractTraversalStrategy; +import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalHelper; + +/** + * Prevents {@code labels().drop()} patterns in traversals. Users should use + * {@code dropLabel(label)} or {@code dropLabels()} instead. + * + * @since 4.0.0 + */ +public final class LabelsDropVerificationStrategy + extends AbstractTraversalStrategy + implements TraversalStrategy.VerificationStrategy { + + private static final LabelsDropVerificationStrategy INSTANCE = new LabelsDropVerificationStrategy(); + + private LabelsDropVerificationStrategy() { + } + + @Override + public void apply(final Traversal.Admin traversal) { + for (final DropStep dropStep : TraversalHelper.getStepsOfClass(DropStep.class, traversal)) { + Step current = dropStep.getPreviousStep(); + + // Walk backward through filter steps (IsStep, HasStep, WhereStep, NotStep, etc.) + while (current instanceof FilterStep) { + current = current.getPreviousStep(); + } + + if (current instanceof LabelsStep) { + throw new VerificationException( + "labels().drop() is not supported. Use dropLabel(label) or dropLabels() instead.", + traversal); + } + } + } + + public static LabelsDropVerificationStrategy instance() { + return INSTANCE; + } +} 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 f4412123b77..9eee6e6ba08 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. + *

+ * 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 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 default V value(final String key) throws NoSuchElementException { */ 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 static IllegalArgumentException labelCanNotBeEmpty() { 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/Graph.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/Graph.java index 4d064c1df25..6166193a6f4 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/Graph.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/Graph.java @@ -737,6 +737,28 @@ default VertexPropertyFeatures properties() { return new VertexPropertyFeatures() { }; } + + /** + * Gets the {@link LabelCardinality} for vertices in this graph. Defines how many labels + * a vertex can have and whether labels are mutable. + * + * @return the label cardinality for vertices, defaulting to {@link LabelCardinality#ONE} + * @since 4.0.0 + */ + default LabelCardinality getLabelCardinality() { + return LabelCardinality.ONE; + } + + /** + * Gets the default label returned for vertices with no explicit labels when the cardinality + * requires at least one label ({@link LabelCardinality#ONE} or {@link LabelCardinality#ONE_OR_MORE}). + * + * @return the default vertex label, typically {@link Vertex#DEFAULT_LABEL} + * @since 4.0.0 + */ + default String getDefaultLabel() { + return Vertex.DEFAULT_LABEL; + } } /** @@ -785,6 +807,17 @@ default EdgePropertyFeatures properties() { return new EdgePropertyFeatures() { }; } + + /** + * Gets the {@link LabelCardinality} for edges in this graph. Edge labels are always + * immutable (exactly one label, set at creation time). + * + * @return the label cardinality for edges, always {@link LabelCardinality#ONE} + * @since 4.0.0 + */ + default LabelCardinality getLabelCardinality() { + return LabelCardinality.ONE; + } } /** diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/LabelCardinality.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/LabelCardinality.java new file mode 100644 index 00000000000..f8a80799f3d --- /dev/null +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/LabelCardinality.java @@ -0,0 +1,85 @@ +/* + * 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.structure; + +/** + * Defines the label cardinality for graph elements. Each value declares its multiplicity constraints + * via {@link #min()} and {@link #max()}, and whether mutation is permitted via {@link #supportsMutation()}. + *

+ * Providers declare their supported cardinality via {@link Graph.Features}. + * Validation of label operations against these constraints is handled by + * {@link org.apache.tinkerpop.gremlin.structure.util.LabelCardinalityValidator}. + * + * @since 4.0.0 + */ +public enum LabelCardinality { + + /** + * Exactly one label, immutable. All mutation operations throw. + * This is the default for TinkerGraph and provides backward compatibility with TinkerPop 3.x. + */ + ONE(1, 1, false), + + /** + * One or more labels. The element must always have at least one label. + * {@code dropLabels()} always throws. {@code dropLabel(x)} succeeds only if at least one label remains. + */ + ONE_OR_MORE(1, Integer.MAX_VALUE, true), + + /** + * Zero or more labels, fully flexible. No constraints on the number of labels. + * Elements can have any number of labels including zero. + */ + ZERO_OR_MORE(0, Integer.MAX_VALUE, true); + + private final int min; + private final int max; + private final boolean mutable; + + LabelCardinality(final int min, final int max, final boolean mutable) { + this.min = min; + this.max = max; + this.mutable = mutable; + } + + /** + * The minimum number of labels an element must have under this cardinality. + */ + public int min() { return min; } + + /** + * The maximum number of labels an element may have under this cardinality. + */ + public int max() { return max; } + + /** + * Whether this cardinality allows multiple labels on an element simultaneously. + */ + public boolean supportsMultiLabel() { return max > 1; } + + /** + * Whether this cardinality allows an element to have zero labels. + */ + public boolean supportsZeroLabels() { return min == 0; } + + /** + * Whether this cardinality allows label mutation (addLabel/dropLabel). + */ + public boolean supportsMutation() { return mutable; } +} 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 1bf76b3c54b..679b13da7ac 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 @@ 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 default String label() { 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 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/EdgeSerializer.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/EdgeSerializer.java index 6d614d51262..2f63384fcf9 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/EdgeSerializer.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/EdgeSerializer.java @@ -30,8 +30,11 @@ import org.apache.tinkerpop.gremlin.structure.util.reference.ReferenceEdge; import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; /** * @author Stephen Mallette (http://stephen.genoprime.com) @@ -44,25 +47,43 @@ public EdgeSerializer() { @Override protected Edge readValue(final Buffer buffer, final GraphBinaryReader context) throws IOException { final Object id = context.read(buffer); - // reading single string value for now according to GraphBinaryV4 - final String label = (String) context.readValue(buffer, List.class, false).get(0); + // Read all labels as List for multi-label support + final List labelList = context.readValue(buffer, List.class, false); final Object inVId = context.read(buffer); - // reading single string value for now according to GraphBinaryV4 - final String inVLabel = (String) context.readValue(buffer, List.class, false).get(0); + // Read all inVertex labels as List for multi-label support + final List inVLabelList = context.readValue(buffer, List.class, false); final Object outVId = context.read(buffer); - // reading single string value for now according to GraphBinaryV4 - final String outVLabel = (String) context.readValue(buffer, List.class, false).get(0); + // Read all outVertex labels as List for multi-label support + final List outVLabelList = context.readValue(buffer, List.class, false); // discard the parent vertex context.read(buffer); final List properties = context.read(buffer); - final DetachedVertex inV = DetachedVertex.build().setId(inVId).setLabel(inVLabel).create(); - final DetachedVertex outV = DetachedVertex.build().setId(outVId).setLabel(outVLabel).create(); + final DetachedVertex.Builder inVBuilder = DetachedVertex.build().setId(inVId); + if (inVLabelList.size() == 1) { + inVBuilder.setLabel(inVLabelList.get(0)); + } else if (!inVLabelList.isEmpty()) { + inVBuilder.setLabels(new LinkedHashSet<>(inVLabelList)); + } + final DetachedVertex inV = inVBuilder.create(); + + final DetachedVertex.Builder outVBuilder = DetachedVertex.build().setId(outVId); + if (outVLabelList.size() == 1) { + outVBuilder.setLabel(outVLabelList.get(0)); + } else if (!outVLabelList.isEmpty()) { + outVBuilder.setLabels(new LinkedHashSet<>(outVLabelList)); + } + final DetachedVertex outV = outVBuilder.create(); - final DetachedEdge.Builder builder = DetachedEdge.build().setId(id).setLabel(label).setInV(inV).setOutV(outV); + final DetachedEdge.Builder builder = DetachedEdge.build().setId(id).setInV(inV).setOutV(outV); + if (labelList.size() == 1) { + builder.setLabel(labelList.get(0)); + } else if (!labelList.isEmpty()) { + builder.setLabels(new LinkedHashSet<>(labelList)); + } if (properties != null) { for (final Property p : properties) { @@ -77,16 +98,19 @@ protected Edge readValue(final Buffer buffer, final GraphBinaryReader context) t protected void writeValue(final Edge value, final Buffer buffer, final GraphBinaryWriter context) throws IOException { context.write(value.id(), buffer); - // wrapping label into list here for now according to GraphBinaryV4, but we aren't allowing null label yet - if (value.label() == null) { - throw new IOException("Unexpected null value when nullable is false"); - } - context.writeValue(Collections.singletonList(value.label()), buffer, false); + // Write all edge labels as List for multi-label support + final Set edgeLabels = value.labels(); + context.writeValue(edgeLabels == null ? new ArrayList<>() : new ArrayList<>(edgeLabels), buffer, false); context.write(value.inVertex().id(), buffer); - context.writeValue(Collections.singletonList(value.inVertex().label()), buffer, false); + // Write all inVertex labels as List for multi-label support + final Set inVLabels = value.inVertex().labels(); + context.writeValue(inVLabels == null ? new ArrayList<>() : new ArrayList<>(inVLabels), buffer, false); + context.write(value.outVertex().id(), buffer); - context.writeValue(Collections.singletonList(value.outVertex().label()), buffer, false); + // Write all outVertex labels as List for multi-label support + final Set outVLabels = value.outVertex().labels(); + context.writeValue(outVLabels == null ? new ArrayList<>() : new ArrayList<>(outVLabels), buffer, false); // we don't serialize the parent Vertex for edges. context.write(null, buffer); diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/GraphSerializer.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/GraphSerializer.java index 4138555dc19..62f5e2546c6 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/GraphSerializer.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/GraphSerializer.java @@ -135,7 +135,8 @@ private void writeVertex(Buffer buffer, GraphBinaryWriter context, Vertex vertex context.write(vertex.id(), buffer); // serializing label as list here for now according to GraphBinaryV4 - context.writeValue(Collections.singletonList(vertex.label()), buffer, false); + final String vLabel = vertex.label(); + context.writeValue(vLabel == null ? Collections.emptyList() : Collections.singletonList(vLabel), buffer, false); context.writeValue(vertexProperties.size(), buffer, false); for (VertexProperty vp : vertexProperties) { @@ -154,7 +155,8 @@ private void writeVertex(Buffer buffer, GraphBinaryWriter context, Vertex vertex private void writeEdge(Buffer buffer, GraphBinaryWriter context, Edge edge) throws IOException { context.write(edge.id(), buffer); // serializing label as list here for now according to GraphBinaryV4 - context.writeValue(Collections.singletonList(edge.label()), buffer, false); + final String eLabel = edge.label(); + context.writeValue(eLabel == null ? Collections.emptyList() : Collections.singletonList(eLabel), buffer, false); context.write(edge.inVertex().id(), buffer); 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 1ab1d82ba01..f5074cc6435 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.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 VertexSerializer() { @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 for multi-label support + final List labelList = context.readValue(buffer, List.class, false); final List 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,10 @@ protected Vertex readValue(final Buffer buffer, final GraphBinaryReader context) @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"); - } - context.writeValue(Collections.singletonList(value.label()), buffer, false); + // Write all labels as List for multi-label support. + // Empty list is valid for zero-label elements (ZERO_OR_MORE / ZERO_OR_ONE cardinality). + final java.util.Set labels = value.labels(); + context.writeValue(labels == null ? new ArrayList<>() : 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 d68458b6717..cad3bb52761 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 @@ public void serialize(final Vertex vertex, final JsonGenerator jsonGenerator, fi 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); @@ -146,7 +146,7 @@ public void serialize(final Edge edge, final JsonGenerator jsonGenerator, final jsonGenerator.writeStartObject(); jsonGenerator.writeObjectField(GraphSONTokens.ID, edge.id()); - writeLabel(jsonGenerator, GraphSONTokens.LABEL, edge.label()); + writeLabels(jsonGenerator, GraphSONTokens.LABEL, edge.labels()); writeTypeForGraphObjectIfUntyped(jsonGenerator, typeInfo, GraphSONTokens.EDGE); writeVertex(GraphSONTokens.IN, edge.inVertex(), jsonGenerator); writeVertex(GraphSONTokens.OUT, edge.outVertex(), jsonGenerator); @@ -160,7 +160,7 @@ private static void writeVertex(final String vertexDirection, final Vertex v, fi jsonGenerator.writeFieldName(vertexDirection); jsonGenerator.writeStartObject(); jsonGenerator.writeObjectField(GraphSONTokens.ID, v.id()); - writeLabel(jsonGenerator, GraphSONTokens.LABEL, v.label()); + writeLabels(jsonGenerator, GraphSONTokens.LABEL, v.labels()); jsonGenerator.writeEndObject(); } @@ -394,8 +394,14 @@ public Vertex deserialize(final JsonParser jsonParser, final DeserializationCont v.setId(deserializationContext.readValue(jsonParser, Object.class)); } else if (jsonParser.getCurrentName().equals(GraphSONTokens.LABEL)) { jsonParser.nextToken(); + final java.util.Set 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(); @@ -432,8 +438,14 @@ public Edge deserialize(final JsonParser jsonParser, final DeserializationContex e.setId(deserializationContext.readValue(jsonParser, Object.class)); } else if (jsonParser.getCurrentName().equals(GraphSONTokens.LABEL)) { jsonParser.nextToken(); + final java.util.Set labels = new java.util.LinkedHashSet<>(); while (jsonParser.nextToken() != JsonToken.END_ARRAY) { - e.setLabel(jsonParser.getText()); + labels.add(jsonParser.getText()); + } + if (labels.size() == 1) { + e.setLabel(labels.iterator().next()); + } else if (!labels.isEmpty()) { + e.setLabels(labels); } } else if (jsonParser.getCurrentName().equals(GraphSONTokens.OUT)) { jsonParser.nextToken(); @@ -692,4 +704,16 @@ private static void writeLabel(final JsonGenerator jsonGenerator, final String l 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 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 1924e6ef326..0ca8aeda132 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 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,63 @@ public static Optional getLabelValue(final Object... keyValues) { 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> 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 labels = new LinkedHashSet<>(); + labels.add((String) labelValue); + return Optional.of(labels); + } else if (labelValue instanceof Collection) { + final Set 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"); + } + } + } + return Optional.empty(); + } + + /** + * Applies label value(s) to a vertex using addLabel semantics. Handles both single String + * and Collection<String> label values as used in mergeV onMatch maps. + * + * @param vertex the vertex to add labels to + * @param labelValue the label value (String or Collection<String>) + * @since 4.0.0 + */ + public static void applyLabelsToVertex(final Vertex vertex, final Object labelValue) { + if (labelValue instanceof String) { + vertex.addLabel((String) labelValue); + } else if (labelValue instanceof Collection) { + final Collection labels = (Collection) labelValue; + if (!labels.isEmpty()) { + final String[] labelArray = labels.stream() + .map(l -> (String) l) + .toArray(String[]::new); + vertex.addLabel(labelArray[0], Arrays.copyOfRange(labelArray, 1, labelArray.length)); + } + } + } + /** * 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/LabelCardinalityValidator.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/LabelCardinalityValidator.java new file mode 100644 index 00000000000..82f1c2c8630 --- /dev/null +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/LabelCardinalityValidator.java @@ -0,0 +1,104 @@ +/* + * 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.structure.util; + +import org.apache.tinkerpop.gremlin.structure.LabelCardinality; + +import java.util.HashSet; +import java.util.Set; + +/** + * Validates label operations against a {@link LabelCardinality} constraint. All methods are stateless and static. + *

+ * This class is separated from the enum so that cardinality remains a pure data descriptor while validation + * logic can be extended, overridden by providers, or tested independently. + * + * @since 4.0.0 + */ +public final class LabelCardinalityValidator { + + private LabelCardinalityValidator() {} + + /** + * Validates that adding labels would not violate cardinality constraints. + * + * @throws IllegalStateException if mutation is not supported or max would be exceeded + */ + public static void validateAdd(final LabelCardinality cardinality, final Set currentLabels, + final String label, final String... moreLabels) { + if (!cardinality.supportsMutation()) + throw new IllegalStateException("Label mutation is not supported with cardinality " + cardinality + + ". Labels are immutable once assigned."); + if (currentLabels.size() + 1 + moreLabels.length > cardinality.max()) + throw new IllegalStateException("Cannot add label(s): would result in " + + (currentLabels.size() + 1 + moreLabels.length) + " labels but cardinality " + + cardinality + " allows at most " + cardinality.max() + "."); + } + + /** + * Validates that dropping specific labels would not violate cardinality constraints. + * Simulates the removal to check whether the minimum label count would be violated. + * + * @throws IllegalStateException if mutation is not supported or min would be violated + */ + public static void validateDrop(final LabelCardinality cardinality, final Set currentLabels, + final String label, final String... moreLabels) { + if (!cardinality.supportsMutation()) + throw new IllegalStateException("Label mutation is not supported with cardinality " + cardinality + + ". Labels are immutable once assigned."); + if (cardinality.min() > 0) { + final Set result = new HashSet<>(currentLabels); + result.remove(label); + for (final String l : moreLabels) { + result.remove(l); + } + if (result.size() < cardinality.min()) + throw new IllegalStateException("Cannot drop label(s): would leave " + result.size() + + " labels but cardinality " + cardinality + " requires at least " + cardinality.min() + "."); + } + } + + /** + * Validates that dropping all labels would not violate cardinality constraints. + * + * @throws IllegalStateException if mutation is not supported or min > 0 + */ + public static void validateDropAll(final LabelCardinality cardinality, final Set currentLabels) { + if (!cardinality.supportsMutation()) + throw new IllegalStateException("Label mutation is not supported with cardinality " + cardinality + + ". Labels are immutable once assigned."); + if (cardinality.min() > 0) + throw new IllegalStateException("Cannot drop all labels: cardinality " + cardinality + + " requires at least " + cardinality.min() + " label(s)."); + } + + /** + * Validates that a set of labels is valid for element creation under this cardinality. + * + * @throws IllegalStateException if the label count violates min or max + */ + public static void validateCreation(final LabelCardinality cardinality, final Set labels) { + if (labels.size() < cardinality.min()) + throw new IllegalStateException("Element creation requires at least " + cardinality.min() + + " label(s) with cardinality " + cardinality + ", got " + labels.size()); + if (labels.size() > cardinality.max()) + throw new IllegalStateException("Element creation allows at most " + cardinality.max() + + " label(s) with cardinality " + cardinality + ", got " + labels.size()); + } +} diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/detached/DetachedEdge.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/detached/DetachedEdge.java index 2c91afb6151..9f6103a744f 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/detached/DetachedEdge.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/detached/DetachedEdge.java @@ -32,6 +32,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Set; /** * Represents an {@link Edge} that is disconnected from a {@link Graph}. "Disconnection" can mean detachment from @@ -180,6 +181,11 @@ public Builder setLabel(final String label) { return this; } + public Builder setLabels(final Set labels) { + e.setElementLabels(labels); + return this; + } + public Builder setOutV(final DetachedVertex v) { e.outVertex = v; return this; diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/detached/DetachedElement.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/detached/DetachedElement.java index 70c0900046a..73521422587 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/detached/DetachedElement.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/detached/DetachedElement.java @@ -29,8 +29,10 @@ import java.io.Serializable; import java.util.Collections; import java.util.Iterator; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Set; /** * @author Stephen Mallette (http://stephen.genoprime.com) @@ -42,6 +44,12 @@ public abstract class DetachedElement implements Element, Serializable, Attac protected String label; protected Map> properties = null; + /** + * Multi-label storage. When non-null, takes precedence over the single {@link #label} field. + * Only populated when the source element has more than one label. + */ + protected Set elementLabels; + protected DetachedElement() { } @@ -53,6 +61,15 @@ protected DetachedElement(final Element element) { } catch (final UnsupportedOperationException e) { // ghetto. this.label = Vertex.DEFAULT_LABEL; } + // Capture all labels from the source element for multi-label support. + try { + final Set srcLabels = element.labels(); + if (srcLabels.size() > 1) { + this.elementLabels = new LinkedHashSet<>(srcLabels); + } + } catch (UnsupportedOperationException e) { + // Adjacent vertices in graph computer context may not support labels() + } } protected DetachedElement(final Object id, final String label) { @@ -72,7 +89,28 @@ public Object id() { @Override public String label() { - return this.label; + if (this.elementLabels != null && !this.elementLabels.isEmpty()) { + return this.elementLabels.iterator().next(); + } + return this.label != null ? this.label : ""; + } + + @Override + public Set labels() { + if (this.elementLabels != null) { + return Collections.unmodifiableSet(this.elementLabels); + } + return this.label != null ? Collections.singleton(this.label) : Collections.emptySet(); + } + + /** + * Sets multi-label storage directly. Used by builders and deserialization. + */ + protected void setElementLabels(final Set labels) { + if (labels != null && !labels.isEmpty()) { + this.elementLabels = new LinkedHashSet<>(labels); + this.label = labels.iterator().next(); + } } @Override 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 c9be9f71da9..8ad1d6195a3 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 @@ -34,6 +34,7 @@ import java.util.Iterator; 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 @@ -196,6 +197,11 @@ public Builder setLabel(final String label) { return this; } + public Builder setLabels(final Set labels) { + v.setElementLabels(labels); + return this; + } + public DetachedVertex create() { return v; } diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/reference/ReferenceEdge.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/reference/ReferenceEdge.java index 6f716022ef3..3d99b44f6e0 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/reference/ReferenceEdge.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/reference/ReferenceEdge.java @@ -28,6 +28,7 @@ import java.util.Collections; import java.util.Iterator; +import java.util.Set; /** * @author Marko A. Rodriguez (http://markorodriguez.com) @@ -53,6 +54,13 @@ public ReferenceEdge(final Object id, final String label, final ReferenceVertex this.outVertex = outVertex; } + public ReferenceEdge(final Object id, final Set labels, final ReferenceVertex inVertex, final ReferenceVertex outVertex) { + super(id, labels != null && !labels.isEmpty() ? labels.iterator().next() : Edge.DEFAULT_LABEL); + this.setElementLabels(labels); + this.inVertex = inVertex; + this.outVertex = outVertex; + } + @Override public Property property(final String key, final V value) { throw Element.Exceptions.propertyAdditionNotSupported(); diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/reference/ReferenceElement.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/reference/ReferenceElement.java index 9ff4db47d17..1b31f10d4e2 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/reference/ReferenceElement.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/reference/ReferenceElement.java @@ -29,6 +29,9 @@ import org.apache.tinkerpop.gremlin.structure.util.empty.EmptyGraph; import java.io.Serializable; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; /** * @author Marko A. Rodriguez (http://markorodriguez.com) @@ -38,6 +41,12 @@ public abstract class ReferenceElement implements Element, Se protected Object id; protected String label; + /** + * Multi-label storage. When non-null, takes precedence over the single {@link #label} field. + * Only populated when the source element has more than one label. + */ + protected Set elementLabels; + protected ReferenceElement() { } @@ -64,6 +73,15 @@ else if (element instanceof Edge) else this.label = VertexProperty.DEFAULT_LABEL; } + // Capture all labels from the source element for multi-label support. + try { + final Set srcLabels = element.labels(); + if (srcLabels.size() > 1) { + this.elementLabels = new LinkedHashSet<>(srcLabels); + } + } catch (UnsupportedOperationException e) { + // Adjacent vertices in graph computer context may not support labels() + } } @Override @@ -73,9 +91,30 @@ public Object id() { @Override public String label() { + if (this.elementLabels != null && !this.elementLabels.isEmpty()) { + return this.elementLabels.iterator().next(); + } return this.label; } + @Override + public Set labels() { + if (this.elementLabels != null) { + return Collections.unmodifiableSet(this.elementLabels); + } + return this.label != null ? Collections.singleton(this.label) : Collections.emptySet(); + } + + /** + * Sets multi-label storage directly. Used by constructors and deserialization. + */ + protected void setElementLabels(final Set labels) { + if (labels != null && !labels.isEmpty()) { + this.elementLabels = new LinkedHashSet<>(labels); + this.label = labels.iterator().next(); + } + } + @Override public Graph graph() { return EmptyGraph.instance(); 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 ac50c3a6a00..8a97d1649a4 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,6 +28,7 @@ import java.util.Collections; import java.util.Iterator; +import java.util.Set; /** * @author Marko A. Rodriguez (http://markorodriguez.com) @@ -46,6 +47,11 @@ public ReferenceVertex(final Object id, final String label) { super(id, label); } + public ReferenceVertex(final Object id, final Set labels) { + super(id, labels != null && !labels.isEmpty() ? labels.iterator().next() : Vertex.DEFAULT_LABEL); + this.setElementLabels(labels); + } + public ReferenceVertex(final Vertex vertex) { super(vertex); } 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 b84d1d36bb4..30c5cb9066d 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/star/StarGraph.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/star/StarGraph.java @@ -43,6 +43,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; @@ -204,7 +205,19 @@ public static StarGraph of(final Vertex vertex) { if (vertex instanceof StarVertex) return (StarGraph) vertex.graph(); // else convert to a star graph final StarGraph starGraph = new StarGraph(); - final StarVertex starVertex = (StarVertex) starGraph.addVertex(T.id, vertex.id(), T.label, vertex.label()); + final String vertexLabel = vertex.label(); + final StarVertex starVertex; + if (vertexLabel != null) { + starVertex = (StarVertex) starGraph.addVertex(T.id, vertex.id(), T.label, vertexLabel); + } else { + starVertex = (StarVertex) starGraph.addVertex(T.id, vertex.id()); + } + + // Copy multi-labels from source vertex + final Set srcLabels = vertex.labels(); + if (srcLabels.size() > 1) { + starVertex.setLabels(srcLabels); + } final boolean supportsMetaProperties = vertex.graph().features().vertex().supportsMetaProperties(); @@ -214,13 +227,23 @@ public static StarGraph of(final Vertex vertex) { vp.properties().forEachRemaining(p -> starVertexProperty.property(p.key(), p.value())); }); vertex.edges(Direction.IN).forEachRemaining(edge -> { - final Edge starEdge = starVertex.addInEdge(edge.label(), starGraph.addVertex(T.id, edge.outVertex().id()), T.id, edge.id()); + final String edgeLabel = edge.label() == null ? Edge.DEFAULT_LABEL : edge.label(); + final Edge starEdge = starVertex.addInEdge(edgeLabel, starGraph.addVertex(T.id, edge.outVertex().id()), T.id, edge.id()); edge.properties().forEachRemaining(p -> starEdge.property(p.key(), p.value())); + final Set edgeSrcLabels = edge.labels(); + if (edgeSrcLabels.size() > 1) { + ((StarGraph.StarEdge) starEdge).setLabels(edgeSrcLabels); + } }); vertex.edges(Direction.OUT).forEachRemaining(edge -> { - final Edge starEdge = starVertex.addOutEdge(edge.label(), starGraph.addVertex(T.id, edge.inVertex().id()), T.id, edge.id()); + final String edgeLabel = edge.label() == null ? Edge.DEFAULT_LABEL : edge.label(); + final Edge starEdge = starVertex.addOutEdge(edgeLabel, starGraph.addVertex(T.id, edge.inVertex().id()), T.id, edge.id()); edge.properties().forEachRemaining(p -> starEdge.property(p.key(), p.value())); + final Set edgeSrcLabels = edge.labels(); + if (edgeSrcLabels.size() > 1) { + ((StarGraph.StarEdge) starEdge).setLabels(edgeSrcLabels); + } }); return starGraph; } @@ -308,7 +331,7 @@ private boolean idExists(final Object id, final Object... providedIds) { public abstract class StarElement implements Element, Attachable { protected final Object id; - protected final String label; + protected String label; protected StarElement(final Object id, final String label) { this.id = id; @@ -352,12 +375,35 @@ public E get() { public final class StarVertex extends StarElement implements Vertex { + private Set vertexLabels; protected Map> outEdges = null; protected Map> inEdges = null; protected Map> vertexProperties = null; public StarVertex(final Object id, final String label) { super(id, label); + this.vertexLabels = new LinkedHashSet<>(); + this.vertexLabels.add(internStrings ? label.intern() : label); + } + + @Override + public Set labels() { + return Collections.unmodifiableSet(this.vertexLabels); + } + + @Override + public String label() { + return this.vertexLabels.isEmpty() ? this.label : this.vertexLabels.iterator().next(); + } + + /** + * Sets the vertex labels from a source set. Used when copying labels from a multi-label vertex. + */ + public void setLabels(final Set labels) { + if (labels != null && !labels.isEmpty()) { + this.vertexLabels = new LinkedHashSet<>(labels); + this.label = this.vertexLabels.iterator().next(); + } } private void dropEdgeProperty(Object id) { @@ -732,6 +778,11 @@ public String label() { throw GraphComputer.Exceptions.adjacentVertexLabelsCanNotBeRead(); } + @Override + public Set labels() { + throw GraphComputer.Exceptions.adjacentVertexLabelsCanNotBeRead(); + } + @Override public Graph graph() { return StarGraph.this; @@ -769,11 +820,34 @@ public String toString() { public abstract class StarEdge extends StarElement implements Edge { + private Set edgeLabels; protected final Object otherId; private StarEdge(final Object id, final String label, final Object otherId) { super(id, label); this.otherId = otherId; + this.edgeLabels = new LinkedHashSet<>(); + this.edgeLabels.add(internStrings ? label.intern() : label); + } + + @Override + public Set labels() { + return Collections.unmodifiableSet(this.edgeLabels); + } + + @Override + public String label() { + return this.edgeLabels.isEmpty() ? this.label : this.edgeLabels.iterator().next(); + } + + /** + * Sets the edge labels from a source set. Used when copying labels from a multi-label edge. + */ + public void setLabels(final Set labels) { + if (labels != null && !labels.isEmpty()) { + this.edgeLabels = new LinkedHashSet<>(labels); + this.label = this.edgeLabels.iterator().next(); + } } @Override diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/strategy/verification/LabelsDropVerificationStrategyTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/strategy/verification/LabelsDropVerificationStrategyTest.java new file mode 100644 index 00000000000..eefe785c2bf --- /dev/null +++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/strategy/verification/LabelsDropVerificationStrategyTest.java @@ -0,0 +1,85 @@ +/* + * 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.strategy.verification; + +import org.apache.tinkerpop.gremlin.process.traversal.P; +import org.apache.tinkerpop.gremlin.process.traversal.Traversal; +import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategies; +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__; +import org.apache.tinkerpop.gremlin.process.traversal.util.DefaultTraversalStrategies; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.Arrays; + +import static org.junit.Assert.fail; + +/** + * Tests for {@link LabelsDropVerificationStrategy}. + * Verifies that {@code labels().drop()} patterns are rejected while unrelated patterns are allowed. + */ +@RunWith(Parameterized.class) +public class LabelsDropVerificationStrategyTest { + + @Parameterized.Parameters(name = "{0}") + public static Iterable data() { + return Arrays.asList(new Object[][]{ + // Should REJECT: labels() followed by drop() (possibly with filter steps in between) + {"labels().drop()", __.labels().drop(), false}, + {"labels().is('x').drop()", __.labels().is("x").drop(), false}, + {"labels().where(P.eq('x')).drop()", __.labels().where(P.eq("x")).drop(), false}, + {"labels().not(__.is('x')).drop()", __.labels().not(__.is("x")).drop(), false}, + + // Should ALLOW: no LabelsStep before drop + {"V().drop()", __.V().drop(), true}, + {"properties().drop()", __.properties().drop(), true}, + + // Should ALLOW: non-filter step between labels() and drop() breaks the walk + {"labels().map(__.constant('x')).drop()", __.labels().map(__.constant("x")).drop(), true}, + {"labels().order().drop()", __.labels().order().drop(), true}, + }); + } + + @Parameterized.Parameter(value = 0) + public String name; + + @Parameterized.Parameter(value = 1) + public Traversal.Admin traversal; + + @Parameterized.Parameter(value = 2) + public boolean allow; + + @Test + public void shouldVerifyLabelsDropPattern() { + final TraversalStrategies strategies = new DefaultTraversalStrategies(); + strategies.addStrategies(LabelsDropVerificationStrategy.instance()); + traversal.asAdmin().setStrategies(strategies); + if (allow) { + traversal.asAdmin().applyStrategies(); + } else { + try { + traversal.asAdmin().applyStrategies(); + fail("The strategy should not allow labels().drop() pattern: " + name); + } catch (VerificationException ve) { + // expected + } + } + } +} diff --git a/gremlin-language/src/main/antlr4/Gremlin.g4 b/gremlin-language/src/main/antlr4/Gremlin.g4 index 31205483b3b..f10489dd1ba 100644 --- a/gremlin-language/src/main/antlr4/Gremlin.g4 +++ b/gremlin-language/src/main/antlr4/Gremlin.g4 @@ -119,6 +119,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 ; @@ -315,6 +316,10 @@ traversalMethod | traversalMethod_dateAdd | traversalMethod_dateDiff | traversalMethod_asNumber + | traversalMethod_labels + | traversalMethod_addLabel + | traversalMethod_dropLabels + | traversalMethod_dropLabel ; traversalMethod_V @@ -333,6 +338,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 ; @@ -628,6 +634,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 @@ -1761,6 +1785,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 @@ -1832,6 +1857,8 @@ keyword | K_DOUBLE | K_DOUBLEU | K_DROP + | K_DROPLABEL + | K_DROPLABELS | K_DT | K_DURATION | K_DURATIONC @@ -2074,6 +2101,7 @@ keyword K_ADDALL: 'addAll'; K_ADDE: 'addE'; +K_ADDLABEL: 'addLabel'; K_ADDV: 'addV'; K_AGGREGATE: 'aggregate'; K_ALL: 'all'; @@ -2145,6 +2173,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_DURATIONC: 'Duration'; diff --git a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/driver/remote/RemoteWorld.java b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/driver/remote/RemoteWorld.java index 54b811d6e40..8a19b4f3b74 100644 --- a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/driver/remote/RemoteWorld.java +++ b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/driver/remote/RemoteWorld.java @@ -23,6 +23,7 @@ import org.apache.tinkerpop.gremlin.TestHelper; import org.apache.tinkerpop.gremlin.driver.Client; import org.apache.tinkerpop.gremlin.driver.Cluster; +import org.apache.tinkerpop.gremlin.driver.RequestOptions; import org.apache.tinkerpop.gremlin.features.World; import org.apache.tinkerpop.gremlin.process.computer.Computer; import org.apache.tinkerpop.gremlin.process.traversal.AnonymousTraversalSource; @@ -99,6 +100,30 @@ public GraphTraversalSource getGraphTraversalSource(final LoadGraphWith.GraphDat return AnonymousTraversalSource.traversal().withRemote(DriverRemoteConnection.using(client, remoteTraversalSource)); } + @Override + public GraphTraversalSource getMultiLabelGraphTraversalSource() { + final Client client = cluster.connect(); + try { // Clear data before run because tests are allowed to modify data for the multi-label graph. + final RequestOptions options = RequestOptions.build().traversalSource("gmultilabel").create(); + client.submit("g.V().drop()", options).all().get(); + } catch (Exception e) { + throw new RuntimeException(e); + } + return AnonymousTraversalSource.traversal().withRemote(DriverRemoteConnection.using(client, "gmultilabel")); + } + + @Override + public GraphTraversalSource getMultiLabelDefaultGraphTraversalSource() { + final Client client = cluster.connect(); + try { + final RequestOptions options = RequestOptions.build().traversalSource("gmultilabel").create(); + client.submit("g.V().drop()", options).all().get(); + } catch (Exception e) { + throw new RuntimeException(e); + } + return AnonymousTraversalSource.traversal().withRemote(DriverRemoteConnection.using(client, "gmultilabel")).with("multilabel"); + } + @Override public String changePathToDataFile(final String pathToFileFromGremlin) { return ".." + File.separator + pathToFileFromGremlin; @@ -137,6 +162,11 @@ public GraphTraversalSource getGraphTraversalSource(final LoadGraphWith.GraphDat } } + @Override + public GraphTraversalSource getMultiLabelGraphTraversalSource() { + throw new AssumptionViolatedException("GraphComputer does not support mutation"); + } + @Override public void beforeEachScenario(Scenario scenario) { super.beforeEachScenario(scenario); diff --git a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/util/CheckedGraphManagerTest.java b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/util/CheckedGraphManagerTest.java index 9fed97cf988..490ead1423f 100644 --- a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/util/CheckedGraphManagerTest.java +++ b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/util/CheckedGraphManagerTest.java @@ -66,7 +66,7 @@ public void singleGraphFails() { public void justAGraphFails() { settings.graphs.put("invalid", "conf/invalidPath"); final GraphManager manager = new CheckedGraphManager(settings); - assertThat(manager.getGraphNames(), hasSize(7)); + assertThat(manager.getGraphNames(), hasSize(8)); } /** diff --git a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/util/DefaultGraphManagerTest.java b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/util/DefaultGraphManagerTest.java index 6cb6238a89a..87af675779f 100644 --- a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/util/DefaultGraphManagerTest.java +++ b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/util/DefaultGraphManagerTest.java @@ -48,7 +48,7 @@ public void shouldReturnGraphs() { final Set graphNames = graphManager.getGraphNames(); assertNotNull(graphNames); - assertEquals(7, graphNames.size()); + assertEquals(8, graphNames.size()); assertThat(graphNames.contains("graph"), is(true)); assertThat(graphNames.contains("classic"), is(true)); @@ -68,7 +68,7 @@ public void shouldGetAsBindings() { final Bindings bindings = graphManager.getAsBindings(); assertNotNull(bindings); - assertEquals(7, bindings.size()); + assertEquals(8, bindings.size()); assertThat(bindings.containsKey("graph"), is(true)); assertThat(bindings.containsKey("classic"), is(true)); assertThat(bindings.containsKey("modern"), is(true)); @@ -99,7 +99,7 @@ public void shouldGetDynamicallyAddedGraph() { final Set graphNames = graphManager.getGraphNames(); assertNotNull(graphNames); - assertEquals(8, graphNames.size()); + assertEquals(9, graphNames.size()); assertThat(graphNames.contains("newGraph"), is(true)); assertThat(graphNames.contains("graph"), is(true)); assertThat(graphNames.contains("classic"), is(true)); @@ -120,14 +120,14 @@ public void shouldNotGetRemovedGraph() throws Exception { graphManager.putGraph("newGraph", graph); final Set graphNames = graphManager.getGraphNames(); assertNotNull(graphNames); - assertEquals(8, graphNames.size()); + assertEquals(9, graphNames.size()); assertThat(graphNames.contains("newGraph"), is(true)); assertThat(graphManager.getGraph("newGraph"), instanceOf(TinkerGraph.class)); graphManager.removeGraph("newGraph"); final Set graphNames2 = graphManager.getGraphNames(); - assertEquals(7, graphNames2.size()); + assertEquals(8, graphNames2.size()); assertThat(graphNames2.contains("newGraph"), is(false)); } diff --git a/gremlin-server/src/test/resources/org/apache/tinkerpop/gremlin/server/gremlin-server-integration.yaml b/gremlin-server/src/test/resources/org/apache/tinkerpop/gremlin/server/gremlin-server-integration.yaml index be135f03c79..5e9a753eedf 100644 --- a/gremlin-server/src/test/resources/org/apache/tinkerpop/gremlin/server/gremlin-server-integration.yaml +++ b/gremlin-server/src/test/resources/org/apache/tinkerpop/gremlin/server/gremlin-server-integration.yaml @@ -35,6 +35,9 @@ graphs: { graph: { configuration: conf/tinkergraph-empty.properties, traversalSources: [{name: g}, {name: ggraph}]}, + multilabel: { + configuration: conf/tinkergraph-multilabel.properties, + traversalSources: [{name: gmultilabel}]}, classic: { configuration: conf/tinkergraph-empty.properties, traversalSources: [{name: gclassic}]}, diff --git a/gremlin-server/src/test/scripts/tinkergraph-multilabel.properties b/gremlin-server/src/test/scripts/tinkergraph-multilabel.properties new file mode 100644 index 00000000000..54391e8c5f7 --- /dev/null +++ b/gremlin-server/src/test/scripts/tinkergraph-multilabel.properties @@ -0,0 +1,21 @@ +# 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. +gremlin.graph=org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerGraph +gremlin.tinkergraph.vertexIdManager=INTEGER +gremlin.tinkergraph.edgeIdManager=INTEGER +gremlin.tinkergraph.vertexPropertyIdManager=LONG +gremlin.tinkergraph.vertexLabelCardinality=ZERO_OR_MORE diff --git a/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/features/StepDefinition.java b/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/features/StepDefinition.java index 59bc869f63d..6b7918dcf56 100644 --- a/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/features/StepDefinition.java +++ b/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/features/StepDefinition.java @@ -111,6 +111,7 @@ public final class StepDefinition { private static final ObjectMapper mapper = new ObjectMapper(); private World world; + private Scenario currentScenario; private GraphTraversalSource g; private File tempWorkingDir; private final Map stringParameters = new HashMap<>(); @@ -316,6 +317,7 @@ public StepDefinition(final World world) { @Before public void beforeEachScenario(final Scenario scenario) throws Exception { + this.currentScenario = scenario; world.beforeEachScenario(scenario); stringParameters.clear(); if (traversal != null) { @@ -355,10 +357,20 @@ private static void deleteDirectory(final File directory) { @Given("the {word} graph") public void givenTheXGraph(final String graphName) { - if (graphName.equals("empty")) - this.g = world.getGraphTraversalSource(null); - else + final boolean isMultiLabel = currentScenario != null && + currentScenario.getSourceTagNames().contains("@MultiLabel"); + final boolean isMultiLabelDefault = currentScenario != null && + currentScenario.getSourceTagNames().contains("@MultiLabelDefault"); + if (graphName.equals("empty")) { + if (isMultiLabelDefault) + this.g = world.getMultiLabelDefaultGraphTraversalSource(); + else if (isMultiLabel) + this.g = world.getMultiLabelGraphTraversalSource(); + else + this.g = world.getGraphTraversalSource(null); + } else { this.g = world.getGraphTraversalSource(GraphData.valueOf(graphName.toUpperCase())); + } } @Given("the graph initializer of") diff --git a/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/features/World.java b/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/features/World.java index b5e284250ce..c65433b0518 100644 --- a/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/features/World.java +++ b/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/features/World.java @@ -55,6 +55,27 @@ public interface World { */ public GraphTraversalSource getGraphTraversalSource(final GraphData graphData); + /** + * Gets a {@link GraphTraversalSource} configured for multi-label support (ZERO_OR_MORE vertex label cardinality). + * This source is used by {@code @MultiLabel} tagged scenarios that require label mutation operations such as + * {@code addLabel()} and {@code dropLabel()}. The default implementation delegates to + * {@link #getGraphTraversalSource(GraphData)} with {@code null} which works for embedded graphs that already + * have multi-label cardinality configured. Remote implementations should override this to connect to a + * dedicated multi-label traversal source. + */ + public default GraphTraversalSource getMultiLabelGraphTraversalSource() { + return getGraphTraversalSource(null); + } + + /** + * Gets a {@link GraphTraversalSource} that returns labels as a set by default from + * {@code elementMap()}/{@code valueMap()}. Used by {@code @MultiLabelDefault} tagged scenarios to simulate + * a provider whose default label output is a set. Applies {@code with("multilabel")} to the multi-label source. + */ + public default GraphTraversalSource getMultiLabelDefaultGraphTraversalSource() { + return getMultiLabelGraphTraversalSource().with("multilabel"); + } + /** * Called before each individual test is executed which provides an opportunity to do some setup. For example, * if there is a specific test that can't be supported it can be ignored by checking for the name with diff --git a/gremlin-test/src/test/java/org/apache/tinkerpop/gremlin/structure/ExceptionCoverageTest.java b/gremlin-test/src/test/java/org/apache/tinkerpop/gremlin/structure/ExceptionCoverageTest.java index a8f39461282..17b8b020dc7 100644 --- a/gremlin-test/src/test/java/org/apache/tinkerpop/gremlin/structure/ExceptionCoverageTest.java +++ b/gremlin-test/src/test/java/org/apache/tinkerpop/gremlin/structure/ExceptionCoverageTest.java @@ -75,6 +75,9 @@ public void shouldCoverAllExceptionsInTests() { add("org.apache.tinkerpop.gremlin.structure.Element$Exceptions#elementAlreadyRemoved"); // as of 3.1.0 add("org.apache.tinkerpop.gremlin.structure.Graph$Exceptions#traversalEngineNotSupported"); // as of 3.2.0 + // this is a general default exception for providers that don't support label mutation: + add("org.apache.tinkerpop.gremlin.structure.Element$Exceptions#labelMutationNotSupported"); + // need to write consistency tests for the following items still........... add("org.apache.tinkerpop.gremlin.process.computer.GraphComputer$Exceptions#supportsDirectObjects"); }}; 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 0ff8db09cfa..469a865d2a6 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,10 @@ public class TypeSerializerFailureTests { @Parameterized.Parameters(name = "Value={0}") public static Collection input() { - final ReferenceVertex vertex = new ReferenceVertex("a vertex", null); + // Use an unserializable id (raw Object has no registered TypeSerializer) to force + // serialization failure for vertex/edge cases. Previously null labels caused the + // failure, but now empty labels are serialized gracefully as empty lists. + final ReferenceVertex vertex = new ReferenceVertex(new Object(), "a vertex"); final BulkSet bulkSet = new BulkSet<>(); bulkSet.add(vertex, 1L); @@ -72,7 +75,7 @@ public static Collection input() { vertex, bulkSet, Collections.singletonList(vertex), - new ReferenceEdge("an edge", null, vertex, vertex), + new ReferenceEdge(new Object(), "an edge", vertex, vertex), Lambda.supplier(null), metrics, new DefaultTraversalMetrics(1L, Collections.singletonList(metrics)), From be3e714d4866a0b002e26f2ce007d43db205cce6 Mon Sep 17 00:00:00 2001 From: xiazcy Date: Fri, 26 Jun 2026 16:11:48 -0700 Subject: [PATCH 02/29] Implement multi-label in TinkerGraph reference implementation - TinkerVertex stores labels as CopyOnWriteArraySet - TinkerGraph.vertexLabelCardinality configuration property - addLabel/dropLabels wired to TinkerVertex mutation - Unit tests for label cardinality, mutation, merge, and GremlinLang --- .../structure/AbstractTinkerGraph.java | 60 ++++ .../tinkergraph/structure/TinkerEdge.java | 37 ++ .../tinkergraph/structure/TinkerElement.java | 2 +- .../tinkergraph/structure/TinkerGraph.java | 52 ++- .../structure/TinkerTransactionGraph.java | 45 ++- .../tinkergraph/structure/TinkerVertex.java | 137 +++++++- .../gremlin/tinkergraph/TinkerWorld.java | 1 + .../traversal/step/map/LabelsStepTest.java | 95 ++++++ .../step/map/MergeVMultiLabelTest.java | 159 +++++++++ .../sideEffect/LabelMutationPropertyTest.java | 208 ++++++++++++ .../sideEffect/LabelMutationStepTest.java | 295 ++++++++++++++++ .../structure/LabelCardinalityTest.java | 318 ++++++++++++++++++ .../LabelReplacePatternValidationTest.java | 264 +++++++++++++++ ...TinkerVertexMultiLabelGremlinLangTest.java | 113 +++++++ .../structure/TinkerVertexMultiLabelTest.java | 199 +++++++++++ 15 files changed, 1970 insertions(+), 15 deletions(-) create mode 100644 tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/map/LabelsStepTest.java create mode 100644 tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/map/MergeVMultiLabelTest.java create mode 100644 tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/sideEffect/LabelMutationPropertyTest.java create mode 100644 tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/sideEffect/LabelMutationStepTest.java create mode 100644 tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/LabelCardinalityTest.java create mode 100644 tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/LabelReplacePatternValidationTest.java create mode 100644 tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertexMultiLabelGremlinLangTest.java create mode 100644 tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertexMultiLabelTest.java 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 4e08872e53f..f786eda09d6 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 @@ -23,6 +23,7 @@ import org.apache.tinkerpop.gremlin.structure.Edge; import org.apache.tinkerpop.gremlin.structure.Element; import org.apache.tinkerpop.gremlin.structure.Graph; +import org.apache.tinkerpop.gremlin.structure.LabelCardinality; import org.apache.tinkerpop.gremlin.structure.Transaction; import org.apache.tinkerpop.gremlin.structure.Vertex; import org.apache.tinkerpop.gremlin.structure.VertexProperty; @@ -64,6 +65,7 @@ public abstract class AbstractTinkerGraph implements Graph { public static final String GREMLIN_TINKERGRAPH_GRAPH_FORMAT = "gremlin.tinkergraph.graphFormat"; public static final String GREMLIN_TINKERGRAPH_ALLOW_NULL_PROPERTY_VALUES = "gremlin.tinkergraph.allowNullPropertyValues"; public static final String GREMLIN_TINKERGRAPH_SERVICE = "gremlin.tinkergraph.service"; + public static final String GREMLIN_TINKERGRAPH_VERTEX_LABEL_CARDINALITY = "gremlin.tinkergraph.vertexLabelCardinality"; protected AtomicLong currentId = new AtomicLong(-1L); protected Map vertexProperties = new ConcurrentHashMap<>(); @@ -78,6 +80,10 @@ public abstract class AbstractTinkerGraph implements Graph { protected IdManager vertexPropertyIdManager; protected VertexProperty.Cardinality defaultVertexPropertyCardinality; protected boolean allowNullPropertyValues; + protected LabelCardinality vertexLabelCardinality; + protected LabelCardinality edgeLabelCardinality; + protected String defaultVertexLabel; + protected String defaultEdgeLabel; protected TinkerServiceRegistry serviceRegistry; @@ -354,6 +360,37 @@ public Configuration configuration() { protected abstract void addInEdge(final TinkerVertex vertex, final String label, final Edge edge); + /** + * Add an edge to the per-vertex adjacency maps under the given label. + * Used when labels are added to an existing edge. + * + * @param edge the edge to register + * @param label the label under which to register the edge + * @since 4.0.0 + */ + public abstract void addEdgeToAdjacency(final TinkerEdge edge, final String label); + + /** + * Remove an edge from the per-vertex adjacency maps for the given label. + * Used when labels are removed from an existing edge. + * + * @param edge the edge to unregister + * @param label the label from which to unregister the edge + * @since 4.0.0 + */ + public abstract void removeEdgeFromAdjacency(final TinkerEdge edge, final String label); + + /** + * Called when a vertex's labels are modified to allow the graph to update any internal label indices. + * The default implementation is a no-op since TinkerGraph does not maintain a separate label index. + * + * @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); } @@ -362,6 +399,19 @@ protected TinkerVertex createTinkerVertex(final Object id, final String label, f 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 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); } @@ -403,6 +453,16 @@ public boolean willAllowId(final Object id) { public VertexProperty.Cardinality getCardinality(final String key) { return defaultVertexPropertyCardinality; } + + @Override + public LabelCardinality getLabelCardinality() { + return vertexLabelCardinality; + } + + @Override + public String getDefaultLabel() { + return defaultVertexLabel; + } } public class TinkerGraphEdgeFeatures implements Features.EdgeFeatures { 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 49bfc7401e7..88f0b2cf0ce 100644 --- a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerEdge.java +++ b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerEdge.java @@ -30,6 +30,7 @@ import java.util.Collections; import java.util.Iterator; +import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -40,6 +41,7 @@ */ public class TinkerEdge extends TinkerElement implements Edge { + protected Set edgeLabels; protected Map properties; protected Vertex inVertex = null; @@ -62,8 +64,30 @@ protected TinkerEdge(final Object id, final Vertex outVertex, final String label } } + protected TinkerEdge(final Object id, final Vertex outVertex, final Set labels, final Vertex inVertex) { + this(id, outVertex, labels, inVertex, 0); + } + + protected TinkerEdge(final Object id, final Vertex outVertex, final Set labels, final Vertex inVertex, final long currentVersion) { + this(id, (AbstractTinkerGraph) outVertex.graph(), outVertex.id(), + (labels == null || labels.isEmpty()) ? Edge.DEFAULT_LABEL : labels.iterator().next(), + inVertex.id(), currentVersion, false); + // Override the singleton set from the private constructor with the provided labels. + // Edge labels remain immutable (singleton) since TinkerEdge doesn't support label mutation. + if (labels != null && !labels.isEmpty()) { + this.edgeLabels = labels.size() == 1 + ? Collections.singleton(labels.iterator().next()) + : Collections.unmodifiableSet(new LinkedHashSet<>(labels)); + } + if (!isTxMode) { + this.inVertex = inVertex; + this.outVertex = outVertex; + } + } + private TinkerEdge(final Object id, AbstractTinkerGraph graph, final Object outVertexId, final String label, final Object inVertexId, final long currentVersion, final Boolean skipIndexUpdate) { super(id, label, currentVersion); + this.edgeLabels = Collections.singleton(label); isTxMode = graph instanceof TinkerTransactionGraph; this.graph = graph; if (isTxMode) { @@ -105,6 +129,17 @@ public Set keys() { return null == this.properties ? Collections.emptySet() : this.properties.keySet(); } + @Override + public Set labels() { + return Collections.unmodifiableSet(this.edgeLabels); + } + + @Override + @Deprecated + public String label() { + return this.edgeLabels.iterator().next(); + } + @Override public void remove() { graph.touch(this); @@ -124,11 +159,13 @@ public Object clone() { if (!isTxMode) { // shallow copy for non-tx mode final TinkerEdge edge = new TinkerEdge(id, outVertex, label, inVertex, currentVersion); + edge.edgeLabels = this.edgeLabels; edge.properties = properties; return edge; } final TinkerEdge edge = new TinkerEdge(id, graph, outVertexId, label, inVertexId, currentVersion, true); + edge.edgeLabels = this.edgeLabels; if (properties != null) { final Map cloned = new ConcurrentHashMap<>(properties.size()); diff --git a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerElement.java b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerElement.java index 8aa2d1db032..d8587b71fa0 100644 --- a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerElement.java +++ b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerElement.java @@ -27,7 +27,7 @@ public abstract class TinkerElement implements Element { protected final Object id; - protected final String label; + protected String label; protected boolean removed = false; protected long currentVersion; diff --git a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerGraph.java b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerGraph.java index 4b5ceeadae9..a87dea92553 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 @@ -24,6 +24,7 @@ import org.apache.tinkerpop.gremlin.structure.Edge; import org.apache.tinkerpop.gremlin.structure.Element; import org.apache.tinkerpop.gremlin.structure.Graph; +import org.apache.tinkerpop.gremlin.structure.LabelCardinality; import org.apache.tinkerpop.gremlin.structure.Transaction; import org.apache.tinkerpop.gremlin.structure.Vertex; import org.apache.tinkerpop.gremlin.structure.VertexProperty; @@ -92,6 +93,12 @@ public class TinkerGraph extends AbstractTinkerGraph { configuration.getString(GREMLIN_TINKERGRAPH_DEFAULT_VERTEX_PROPERTY_CARDINALITY, VertexProperty.Cardinality.single.name())); allowNullPropertyValues = configuration.getBoolean(GREMLIN_TINKERGRAPH_ALLOW_NULL_PROPERTY_VALUES, false); + vertexLabelCardinality = LabelCardinality.valueOf( + configuration.getString(GREMLIN_TINKERGRAPH_VERTEX_LABEL_CARDINALITY, LabelCardinality.ONE.name())); + edgeLabelCardinality = LabelCardinality.ONE; + defaultVertexLabel = Vertex.DEFAULT_LABEL; + defaultEdgeLabel = Edge.DEFAULT_LABEL; + graphLocation = configuration.getString(GREMLIN_TINKERGRAPH_GRAPH_LOCATION, null); graphFormat = configuration.getString(GREMLIN_TINKERGRAPH_GRAPH_FORMAT, null); @@ -141,7 +148,7 @@ public static TinkerGraph open(final Configuration configuration) { 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 labels = ElementHelper.getLabelsValue(keyValues).orElse(null); if (null != idValue) { if (this.vertices.containsKey(idValue)) @@ -150,10 +157,12 @@ public Vertex addVertex(final Object... keyValues) { 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); - vertexLabelCounts.computeIfAbsent(label, l -> new AtomicInteger()).incrementAndGet(); + for (final String l : vertex.labels()) { + vertexLabelCounts.computeIfAbsent(l, k -> new AtomicInteger()).incrementAndGet(); + } return vertex; } @@ -162,8 +171,11 @@ public Vertex addVertex(final Object... keyValues) { public void removeVertex(final Object vertexId) { final Vertex removed = this.vertices.remove(vertexId); - if (removed != null) - vertexLabelCounts.computeIfPresent(removed.label(), (l, c) -> { c.decrementAndGet(); return c; }); + if (removed != null) { + for (final String l : removed.labels()) { + vertexLabelCounts.computeIfPresent(l, (k, c) -> { c.decrementAndGet(); return c; }); + } + } } @Override @@ -329,6 +341,36 @@ protected void addInEdge(final TinkerVertex vertex, final String label, final Ed edges.add(edge); } + @Override + public void addEdgeToAdjacency(final TinkerEdge edge, final String label) { + final TinkerVertex outV = (TinkerVertex) edge.outVertex(); + final TinkerVertex inV = (TinkerVertex) edge.inVertex(); + if (null == outV.outEdges) outV.outEdges = new HashMap<>(); + outV.outEdges.computeIfAbsent(label, k -> new HashSet<>()).add(edge); + if (null == inV.inEdges) inV.inEdges = new HashMap<>(); + inV.inEdges.computeIfAbsent(label, k -> new HashSet<>()).add(edge); + } + + @Override + public void removeEdgeFromAdjacency(final TinkerEdge edge, final String label) { + final TinkerVertex outV = (TinkerVertex) edge.outVertex(); + final TinkerVertex inV = (TinkerVertex) edge.inVertex(); + if (outV.outEdges != null) { + final Set edges = outV.outEdges.get(label); + if (edges != null) { + edges.remove(edge); + if (edges.isEmpty()) outV.outEdges.remove(label); + } + } + if (inV.inEdges != null) { + final Set edges = inV.inEdges.get(label); + if (edges != null) { + edges.remove(edge); + if (edges.isEmpty()) inV.inEdges.remove(label); + } + } + } + /** * Return TinkerGraph feature set. *

diff --git a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerTransactionGraph.java b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerTransactionGraph.java index d7914981d53..6a8a0284a94 100644 --- a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerTransactionGraph.java +++ b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerTransactionGraph.java @@ -24,6 +24,7 @@ import org.apache.tinkerpop.gremlin.structure.Edge; import org.apache.tinkerpop.gremlin.structure.Element; import org.apache.tinkerpop.gremlin.structure.Graph; +import org.apache.tinkerpop.gremlin.structure.LabelCardinality; import org.apache.tinkerpop.gremlin.structure.Transaction; import org.apache.tinkerpop.gremlin.structure.Vertex; import org.apache.tinkerpop.gremlin.structure.VertexProperty; @@ -87,6 +88,12 @@ private TinkerTransactionGraph(final Configuration configuration) { configuration.getString(GREMLIN_TINKERGRAPH_DEFAULT_VERTEX_PROPERTY_CARDINALITY, VertexProperty.Cardinality.single.name())); allowNullPropertyValues = configuration.getBoolean(GREMLIN_TINKERGRAPH_ALLOW_NULL_PROPERTY_VALUES, false); + vertexLabelCardinality = LabelCardinality.valueOf( + configuration.getString(GREMLIN_TINKERGRAPH_VERTEX_LABEL_CARDINALITY, LabelCardinality.ONE.name())); + edgeLabelCardinality = LabelCardinality.ONE; + defaultVertexLabel = Vertex.DEFAULT_LABEL; + defaultEdgeLabel = Edge.DEFAULT_LABEL; + graphLocation = configuration.getString(GREMLIN_TINKERGRAPH_GRAPH_LOCATION, null); graphFormat = configuration.getString(GREMLIN_TINKERGRAPH_GRAPH_FORMAT, null); @@ -139,7 +146,7 @@ public Vertex addVertex(final Object... keyValues) { Object idValue = vertexIdManager.convert(ElementHelper.getIdValue(keyValues).orElse(null)); if (null == idValue) idValue = vertexIdManager.getNextId(this); - final String label = ElementHelper.getLabelValue(keyValues).orElse(Vertex.DEFAULT_LABEL); + final Set labels = ElementHelper.getLabelsValue(keyValues).orElse(null); this.tx().readWrite(); final long txNumber = transaction.getTxNumber(); @@ -164,7 +171,7 @@ public Vertex addVertex(final Object... keyValues) { if (container == null) container = newContainer; - final TinkerVertex vertex = new TinkerVertex(idValue, label, this, version); + final TinkerVertex vertex = new TinkerVertex(idValue, labels, this, version); ElementHelper.attachProperties(vertex, VertexProperty.Cardinality.list, keyValues); container.setDraft(vertex, (TinkerTransaction) tx()); @@ -410,6 +417,40 @@ protected void addInEdge(final TinkerVertex vertex, final String label, final Ed edges.add(edge.id()); } + @Override + public void addEdgeToAdjacency(final TinkerEdge edge, final String label) { + final TinkerVertex outV = (TinkerVertex) edge.outVertex(); + final TinkerVertex inV = (TinkerVertex) edge.inVertex(); + touch(outV); + touch(inV); + if (null == outV.outEdgesId) outV.outEdgesId = new ConcurrentHashMap<>(); + outV.outEdgesId.computeIfAbsent(label, k -> ConcurrentHashMap.newKeySet()).add(edge.id()); + if (null == inV.inEdgesId) inV.inEdgesId = new ConcurrentHashMap<>(); + inV.inEdgesId.computeIfAbsent(label, k -> ConcurrentHashMap.newKeySet()).add(edge.id()); + } + + @Override + public void removeEdgeFromAdjacency(final TinkerEdge edge, final String label) { + final TinkerVertex outV = (TinkerVertex) edge.outVertex(); + final TinkerVertex inV = (TinkerVertex) edge.inVertex(); + touch(outV); + touch(inV); + if (outV.outEdgesId != null) { + final Set edges = outV.outEdgesId.get(label); + if (edges != null) { + edges.remove(edge.id()); + if (edges.isEmpty()) outV.outEdgesId.remove(label); + } + } + if (inV.inEdgesId != null) { + final Set edges = inV.inEdgesId.get(label); + if (edges != null) { + edges.remove(edge.id()); + if (edges.isEmpty()) inV.inEdgesId.remove(label); + } + } + } + /** * Return TinkerGraph feature set. *

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 c8498b94340..d3e37dd9ca7 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 @@ -24,13 +24,16 @@ import org.apache.tinkerpop.gremlin.structure.Vertex; import org.apache.tinkerpop.gremlin.structure.VertexProperty; import org.apache.tinkerpop.gremlin.structure.util.ElementHelper; +import org.apache.tinkerpop.gremlin.structure.util.LabelCardinalityValidator; import org.apache.tinkerpop.gremlin.structure.util.StringFactory; import org.apache.tinkerpop.gremlin.util.CollectionUtil; import org.apache.tinkerpop.gremlin.util.iterator.IteratorUtils; import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.Iterator; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Optional; @@ -54,31 +57,150 @@ public class TinkerVertex extends TinkerElement implements Vertex { private boolean allowNullPropertyValues; private final boolean isTxMode; + /** + * Multi-label storage for this vertex. Starts as an immutable set (lightweight) and is + * upgraded to a mutable ConcurrentHashMap-backed set on first label mutation. + */ + protected Set 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(id, Collections.singleton(label != null ? label : Vertex.DEFAULT_LABEL), graph, -1); } protected TinkerVertex(final Object id, final String label, final AbstractTinkerGraph graph, final long currentVersion) { - super(id, label, currentVersion); + this(id, Collections.singleton(label != null ? label : Vertex.DEFAULT_LABEL), graph, currentVersion); + } + + /** + * Constructs a TinkerVertex with multiple labels. + */ + protected TinkerVertex(final Object id, final Set labels, final AbstractTinkerGraph graph) { + this(id, labels, graph, -1); + } + + /** + * Canonical constructor. Constructs a TinkerVertex with multiple labels and a specific version (for transactional graphs). + * Uses a single-switch pattern to handle default label injection per the configured cardinality. + */ + protected TinkerVertex(final Object id, final Set labels, final AbstractTinkerGraph graph, final long currentVersion) { + super(id, null, currentVersion); // label field set below this.graph = graph; this.isTxMode = graph instanceof TinkerTransactionGraph; this.allowNullPropertyValues = graph.features().vertex().supportsNullPropertyValues(); + + // Build the initial label set. Use lightweight immutable sets when possible. + final Set initial = new LinkedHashSet<>(); + switch (graph.vertexLabelCardinality) { + case ONE: + // Exactly 1 label: use provided or default + if (labels != null && !labels.isEmpty()) + initial.addAll(labels); + else + initial.add(graph.defaultVertexLabel); + break; + case ONE_OR_MORE: + // Default label always present alongside any user labels + initial.add(graph.defaultVertexLabel); + if (labels != null) initial.addAll(labels); + break; + case ZERO_OR_MORE: + if (labels != null) initial.addAll(labels); + break; + } + + // Validate the resulting set conforms to cardinality + LabelCardinalityValidator.validateCreation(graph.vertexLabelCardinality, initial); + + // Store as immutable — will be upgraded on first mutation via ensureMutableLabels() + this.vertexLabels = initial.size() <= 1 + ? (initial.isEmpty() ? Collections.emptySet() : Collections.singleton(initial.iterator().next())) + : Collections.unmodifiableSet(initial); + + this.label = this.vertexLabels.isEmpty() ? "" : this.vertexLabels.iterator().next(); + } + + /** + * Upgrades the label set to a mutable ConcurrentHashMap-backed set on first mutation. + * This is a one-time cost per vertex that actually needs label mutation. + */ + private void ensureMutableLabels() { + if (!(this.vertexLabels instanceof ConcurrentHashMap.KeySetView)) { + final Set mutable = ConcurrentHashMap.newKeySet(); + mutable.addAll(this.vertexLabels); + this.vertexLabels = mutable; + } + } + + @Override + public Set labels() { + if (this.vertexLabels.isEmpty()) { + return Collections.emptySet(); + } + // If already immutable (pre-mutation), return directly. If mutable (post-mutation), wrap. + if (this.vertexLabels instanceof ConcurrentHashMap.KeySetView) { + return Collections.unmodifiableSet(this.vertexLabels); + } + return this.vertexLabels; // already immutable from construction + } + + @Override + @Deprecated + public String label() { + if (this.vertexLabels.isEmpty()) { + return ""; + } + 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); + } + LabelCardinalityValidator.validateAdd(this.graph.vertexLabelCardinality, this.vertexLabels, label, labels); + graph.touch(this); + ensureMutableLabels(); + this.vertexLabels.add(label); + Collections.addAll(this.vertexLabels, labels); + this.label = this.vertexLabels.iterator().next(); + this.graph.updateVertexLabelIndex(this); + } + + @Override + public void dropLabels() { + LabelCardinalityValidator.validateDropAll(this.graph.vertexLabelCardinality, this.vertexLabels); + graph.touch(this); + ensureMutableLabels(); + this.vertexLabels.clear(); + this.label = ""; + this.graph.updateVertexLabelIndex(this); + } + + @Override + public void dropLabel(final String label, final String... labels) { + LabelCardinalityValidator.validateDrop(this.graph.vertexLabelCardinality, this.vertexLabels, label, labels); + graph.touch(this); + ensureMutableLabels(); + this.vertexLabels.remove(label); + for (final String l : labels) { + this.vertexLabels.remove(l); + } + this.label = this.vertexLabels.isEmpty() ? "" : this.vertexLabels.iterator().next(); + 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; return vertex; } - final TinkerVertex vertex = new TinkerVertex(id, label, graph, currentVersion); + final TinkerVertex vertex = new TinkerVertex(id, new LinkedHashSet<>(vertexLabels), graph, currentVersion); if (inEdgesId != null) vertex.inEdgesId = CollectionUtil.clone((ConcurrentHashMap>) inEdgesId); @@ -196,6 +318,7 @@ public void remove() { final List edges = new ArrayList<>(); this.edges(Direction.BOTH).forEachRemaining(edge -> edges.add(edge)); edges.stream().filter(edge -> !((TinkerEdge) edge).removed).forEach(Edge::remove); + TinkerIndexHelper.removeElementIndex(this); if (null != this.properties) this.properties.values().forEach(vpList -> vpList.forEach(vp -> graph.vertexProperties.remove(vp.id()))); diff --git a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/TinkerWorld.java b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/TinkerWorld.java index 1a6ff95e020..1b5b6518b87 100644 --- a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/TinkerWorld.java +++ b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/TinkerWorld.java @@ -65,6 +65,7 @@ protected static Configuration getNumberIdManagerConfiguration() { conf.setProperty(TinkerGraph.GREMLIN_TINKERGRAPH_VERTEX_ID_MANAGER, AbstractTinkerGraph.DefaultIdManager.INTEGER.name()); conf.setProperty(TinkerGraph.GREMLIN_TINKERGRAPH_EDGE_ID_MANAGER, AbstractTinkerGraph.DefaultIdManager.INTEGER.name()); conf.setProperty(TinkerGraph.GREMLIN_TINKERGRAPH_VERTEX_PROPERTY_ID_MANAGER, AbstractTinkerGraph.DefaultIdManager.LONG.name()); + conf.setProperty(AbstractTinkerGraph.GREMLIN_TINKERGRAPH_VERTEX_LABEL_CARDINALITY, "ZERO_OR_MORE"); return conf; } 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 00000000000..e4d98aa5a41 --- /dev/null +++ b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/map/LabelsStepTest.java @@ -0,0 +1,95 @@ +/* + * 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.AbstractTinkerGraph; +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() { + final org.apache.commons.configuration2.Configuration config = new org.apache.commons.configuration2.BaseConfiguration(); + config.setProperty(Graph.GRAPH, TinkerGraph.class.getName()); + config.setProperty(AbstractTinkerGraph.GREMLIN_TINKERGRAPH_VERTEX_LABEL_CARDINALITY, "ZERO_OR_MORE"); + graph = TinkerGraph.open(config); + 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 labels = g.V(v).labels().toList(); + assertThat(labels, hasSize(2)); + assertThat(labels, 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 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 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 labels = g.V(v).labels().toList(); + // Under ZERO_OR_MORE cardinality, addV() with no label produces an empty label set + assertThat(labels, hasSize(0)); + } +} 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 00000000000..c64996ee35a --- /dev/null +++ b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/map/MergeVMultiLabelTest.java @@ -0,0 +1,159 @@ +/* + * 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.AbstractTinkerGraph; +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() { + final org.apache.commons.configuration2.Configuration config = new org.apache.commons.configuration2.BaseConfiguration(); + config.setProperty(Graph.GRAPH, TinkerGraph.class.getName()); + config.setProperty(AbstractTinkerGraph.GREMLIN_TINKERGRAPH_VERTEX_LABEL_CARDINALITY, "ZERO_OR_MORE"); + graph = TinkerGraph.open(config); + g = graph.traversal(); + } + + @After + public void tearDown() throws Exception { + graph.close(); + } + + // --- mergeV create with multi-label --- + + @Test + public void shouldCreateVertexWithMultiLabelViaMergeV() { + final Set 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 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 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 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 shouldAppendLabelsOnMatchWithSingleLabel() { + 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 appended (addLabel semantics), not replaced + assertThat(v.labels(), hasSize(3)); + assertThat(v.labels(), containsInAnyOrder("person", "employee", "manager")); + } + + @Test + public void shouldAppendLabelsOnMatchWithMultiLabel() { + final Vertex v = g.addV("person").property("name", "marko").next(); + + final Set 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(); + + // labels should be appended (addLabel semantics) + assertThat(v.labels(), hasSize(3)); + assertThat(v.labels(), containsInAnyOrder("person", "manager", "director")); + } + + @Test + public void shouldNoOpOnMatchWithEmptyCollection() { + 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 = nothing to add, labels unchanged + assertThat(v.labels(), hasSize(1)); + assertThat(v.labels(), containsInAnyOrder("person")); + } +} 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 00000000000..b9bc0e4d415 --- /dev/null +++ b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/sideEffect/LabelMutationPropertyTest.java @@ -0,0 +1,208 @@ +/* + * 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.AbstractTinkerGraph; +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() { + final org.apache.commons.configuration2.Configuration config = new org.apache.commons.configuration2.BaseConfiguration(); + config.setProperty(Graph.GRAPH, TinkerGraph.class.getName()); + config.setProperty(AbstractTinkerGraph.GREMLIN_TINKERGRAPH_VERTEX_LABEL_CARDINALITY, "ZERO_OR_MORE"); + graph = TinkerGraph.open(config); + 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 randomLabelSet(final int minSize, final int maxSize) { + final int size = minSize + random.nextInt(maxSize - minSize + 1); + final Set labels = new HashSet<>(); + while (labels.size() < size) { + labels.add(randomLabel()); + } + return labels; + } + + /** + * 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 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 labelList = new ArrayList<>(v.labels()); + final String existingLabel = labelList.get(random.nextInt(labelList.size())); + final Set beforeAdd = new HashSet<>(v.labels()); + + // add it again - should be idempotent + g.V(v).addLabel(existingLabel).iterate(); + final Set afterAdd = new HashSet<>(v.labels()); + + assertThat("Iteration " + i + ": addLabel should be idempotent for label '" + existingLabel + "'", + afterAdd, is(beforeAdd)); + } + } + + /** + * 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 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(); + + // Under ZERO_OR_MORE cardinality, dropping all labels results in empty set + assertThat("Iteration " + i + ": after dropLabels(), vertex should have no labels", + v.labels(), hasSize(0)); + } + } + + /** + * 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 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 beforeDrop = new HashSet<>(v.labels()); + g.V(v).dropLabel(nonExistent).iterate(); + final Set afterDrop = new HashSet<>(v.labels()); + + assertThat("Iteration " + i + ": dropLabel of non-existent label '" + nonExistent + "' should be no-op", + afterDrop, is(beforeDrop)); + } + } + + /** + * ValueMap/ElementMap multilabel configuration. + * For any element with labels L, g.with("multilabel").V(v).valueMap(true) 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 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 + final GraphTraversalSource gml = g.with("multilabel"); + final Map mapWithConfig = gml.V(v).valueMap(true).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) labelWithConfig, is(v.labels())); + + // without multilabel config: should return single String + final Map 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 00000000000..761d7145245 --- /dev/null +++ b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/sideEffect/LabelMutationStepTest.java @@ -0,0 +1,295 @@ +/* + * 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.AbstractTinkerGraph; +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() { + final org.apache.commons.configuration2.Configuration config = new org.apache.commons.configuration2.BaseConfiguration(); + config.setProperty(Graph.GRAPH, TinkerGraph.class.getName()); + config.setProperty(AbstractTinkerGraph.GREMLIN_TINKERGRAPH_VERTEX_LABEL_CARDINALITY, "ZERO_OR_MORE"); + graph = TinkerGraph.open(config); + 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 shouldAddLabelToEdgeViaTraversal() { + final Vertex v1 = g.addV("person").next(); + final Vertex v2 = g.addV("person").next(); + final Edge e = v1.addEdge("knows", v2); + // Under ZERO_OR_ONE cardinality for edges, adding a second label throws + 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(); + // Under ZERO_OR_MORE cardinality, dropping all labels results in empty set + assertThat(v.labels(), hasSize(0)); + } + + @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 shouldDropLabelsOnEdgeViaTraversal() { + final Vertex v1 = g.addV("person").next(); + final Vertex v2 = g.addV("person").next(); + final Edge e = v1.addEdge("knows", v2); + // Edge cardinality is always ONE - dropLabels() throws + 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(); + // Under ZERO_OR_MORE cardinality, addV() with no label produces an empty label set + assertThat(v.labels(), hasSize(0)); + } + + // --- hasLabel index consistency tests --- + + @Test + public void shouldFindVertexByLabelAfterAddLabel() { + final Vertex v = g.addV("person").next(); + g.V(v).addLabel("employee").iterate(); + final List 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 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 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 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 GraphTraversalSource gml = g.with("multilabel"); + final Map map = gml.V(v).valueMap(true).next(); + final Object labelValue = map.get(T.label); + assertThat(labelValue, instanceOf(Set.class)); + final Set labels = (Set) labelValue; + assertThat(labels, containsInAnyOrder("person", "employee")); + } + + @SuppressWarnings("deprecation") + @Test + public void shouldReturnLabelAsSingleStringWithoutMultilabelConfig() { + final Vertex v = g.addV("person").next(); + final Map 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 GraphTraversalSource gml = g.with("multilabel"); + final Map map = gml.V(v).elementMap().next(); + final Object labelValue = map.get(T.label); + assertThat(labelValue, instanceOf(Set.class)); + final Set labels = (Set) 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")); + } + + // --- null/empty label argument validation --- + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowOnAddLabelNull() { + final Vertex v = g.addV("person").next(); + v.addLabel(null); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowOnAddLabelEmpty() { + final Vertex v = g.addV("person").next(); + v.addLabel(""); + } + + // --- hasLabel OR semantics on multi-label vertex --- + + @Test + public void shouldMatchHasLabelOrSemanticsOnMultiLabelVertex() { + g.addV("person").addLabel("employee").next(); + // hasLabel("person", "nonexistent") is OR - should match because vertex has "person" + assertThat(g.V().hasLabel("person", "nonexistent").toList(), hasSize(1)); + } + + @Test + public void shouldMatchHasLabelAndSemanticsOnMultiLabelVertex() { + g.addV("person").addLabel("employee").next(); + // hasLabel("person").hasLabel("employee") is AND - vertex has both + assertThat(g.V().hasLabel("person").hasLabel("employee").toList(), hasSize(1)); + // hasLabel("person").hasLabel("nonexistent") is AND - vertex doesn't have "nonexistent" + assertThat(g.V().hasLabel("person").hasLabel("nonexistent").toList(), hasSize(0)); + } + + // --- index consistency: add → drop → re-add cycle --- + + @Test + public void shouldMaintainIndexConsistencyAfterReaddCycle() { + final Vertex v = g.addV("person").addLabel("employee").next(); + assertThat(g.V().hasLabel("employee").toList(), hasSize(1)); + g.V(v).dropLabel("employee").iterate(); + assertThat(g.V().hasLabel("employee").toList(), hasSize(0)); + g.V(v).addLabel("employee").iterate(); + assertThat(g.V().hasLabel("employee").toList(), hasSize(1)); + } +} diff --git a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/LabelCardinalityTest.java b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/LabelCardinalityTest.java new file mode 100644 index 00000000000..0e49ab9a6fa --- /dev/null +++ b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/LabelCardinalityTest.java @@ -0,0 +1,318 @@ +/* + * 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.commons.configuration2.BaseConfiguration; +import org.apache.commons.configuration2.Configuration; +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.Test; + +import java.util.Set; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasSize; + +/** + * Tests for vertex label cardinality modes: ONE, ONE_OR_MORE, and ZERO_OR_MORE. + */ +public class LabelCardinalityTest { + + // ======================================================================== + // ONE mode (TinkerGraph default) + // ======================================================================== + + @Test + public void oneMode_addVNoLabel_shouldDefaultToVertex() throws Exception { + final Graph graph = TinkerGraph.open(); + try { + final GraphTraversalSource g = graph.traversal(); + final Vertex v = g.addV().next(); + assertThat(v.labels(), hasSize(1)); + assertThat(v.labels(), containsInAnyOrder(Vertex.DEFAULT_LABEL)); + } finally { + graph.close(); + } + } + + @Test + public void oneMode_addVWithLabel_shouldHaveThatLabel() throws Exception { + final Graph graph = TinkerGraph.open(); + try { + final GraphTraversalSource g = graph.traversal(); + final Vertex v = g.addV("person").next(); + assertThat(v.labels(), hasSize(1)); + assertThat(v.labels(), containsInAnyOrder("person")); + } finally { + graph.close(); + } + } + + @Test(expected = IllegalStateException.class) + public void oneMode_addVWithMultipleLabels_shouldThrow() throws Exception { + final Graph graph = TinkerGraph.open(); + try { + final GraphTraversalSource g = graph.traversal(); + g.addV("a", "b").next(); + } finally { + graph.close(); + } + } + + @Test(expected = IllegalStateException.class) + public void oneMode_addLabel_shouldThrow() throws Exception { + final Graph graph = TinkerGraph.open(); + try { + final GraphTraversalSource g = graph.traversal(); + final Vertex v = g.addV("person").next(); + v.addLabel("x"); + } finally { + graph.close(); + } + } + + @Test(expected = IllegalStateException.class) + public void oneMode_dropLabel_shouldThrow() throws Exception { + final Graph graph = TinkerGraph.open(); + try { + final GraphTraversalSource g = graph.traversal(); + final Vertex v = g.addV("person").next(); + v.dropLabel(Vertex.DEFAULT_LABEL); + } finally { + graph.close(); + } + } + + @Test(expected = IllegalStateException.class) + public void oneMode_dropLabels_shouldThrow() throws Exception { + final Graph graph = TinkerGraph.open(); + try { + final GraphTraversalSource g = graph.traversal(); + final Vertex v = g.addV("person").next(); + v.dropLabels(); + } finally { + graph.close(); + } + } + + @Test(expected = IllegalStateException.class) + public void oneMode_addLabelViaTraversal_shouldThrow() throws Exception { + final Graph graph = TinkerGraph.open(); + try { + final GraphTraversalSource g = graph.traversal(); + g.addV("person").addLabel("x").iterate(); + } finally { + graph.close(); + } + } + + @Test(expected = IllegalStateException.class) + public void oneMode_dropLabelViaTraversal_shouldThrow() throws Exception { + final Graph graph = TinkerGraph.open(); + try { + final GraphTraversalSource g = graph.traversal(); + g.addV("person").dropLabel("person").iterate(); + } finally { + graph.close(); + } + } + + // ======================================================================== + // ONE_OR_MORE mode + // ======================================================================== + + private Graph openOneOrMore() { + final Configuration config = new BaseConfiguration(); + config.setProperty(Graph.GRAPH, TinkerGraph.class.getName()); + config.setProperty(AbstractTinkerGraph.GREMLIN_TINKERGRAPH_VERTEX_LABEL_CARDINALITY, "ONE_OR_MORE"); + return TinkerGraph.open(config); + } + + @Test + public void oneOrMoreMode_addVNoLabel_shouldContainDefault() throws Exception { + final Graph graph = openOneOrMore(); + try { + final GraphTraversalSource g = graph.traversal(); + final Vertex v = g.addV().next(); + assertThat(v.labels(), containsInAnyOrder(Vertex.DEFAULT_LABEL)); + } finally { + graph.close(); + } + } + + @Test + public void oneOrMoreMode_addVWithLabel_shouldContainDefaultAndUserLabel() throws Exception { + final Graph graph = openOneOrMore(); + try { + final GraphTraversalSource g = graph.traversal(); + final Vertex v = g.addV("person").next(); + assertThat(v.labels(), containsInAnyOrder(Vertex.DEFAULT_LABEL, "person")); + } finally { + graph.close(); + } + } + + @Test + public void oneOrMoreMode_addLabel_shouldAccumulate() throws Exception { + final Graph graph = openOneOrMore(); + try { + final GraphTraversalSource g = graph.traversal(); + final Vertex v = g.addV("person").next(); + v.addLabel("employee"); + assertThat(v.labels(), containsInAnyOrder(Vertex.DEFAULT_LABEL, "person", "employee")); + } finally { + graph.close(); + } + } + + @Test + public void oneOrMoreMode_dropLabel_shouldSucceedIfAtLeastOneRemains() throws Exception { + final Graph graph = openOneOrMore(); + try { + final GraphTraversalSource g = graph.traversal(); + final Vertex v = g.addV("person").next(); + // Has {"vertex", "person"} - dropping "person" leaves {"vertex"} + v.dropLabel("person"); + assertThat(v.labels(), containsInAnyOrder(Vertex.DEFAULT_LABEL)); + } finally { + graph.close(); + } + } + + @Test(expected = IllegalStateException.class) + public void oneOrMoreMode_dropLabel_shouldThrowIfWouldLeaveZero() throws Exception { + final Graph graph = openOneOrMore(); + try { + final GraphTraversalSource g = graph.traversal(); + final Vertex v = g.addV().next(); + // Has {"vertex"} - dropping "vertex" would leave 0 + v.dropLabel(Vertex.DEFAULT_LABEL); + } finally { + graph.close(); + } + } + + @Test(expected = IllegalStateException.class) + public void oneOrMoreMode_dropMultipleLabels_shouldThrowIfWouldLeaveZero() throws Exception { + final Graph graph = openOneOrMore(); + try { + final GraphTraversalSource g = graph.traversal(); + final Vertex v = g.addV("person").next(); + // Has {"vertex", "person"} - dropping both would leave 0 + v.dropLabel("person", Vertex.DEFAULT_LABEL); + } finally { + graph.close(); + } + } + + @Test(expected = IllegalStateException.class) + public void oneOrMoreMode_dropLabels_shouldAlwaysThrow() throws Exception { + final Graph graph = openOneOrMore(); + try { + final GraphTraversalSource g = graph.traversal(); + final Vertex v = g.addV("person").next(); + v.dropLabels(); + } finally { + graph.close(); + } + } + + @Test + public void oneOrMoreMode_addVWithMultipleLabels_shouldIncludeDefault() throws Exception { + final Graph graph = openOneOrMore(); + try { + final GraphTraversalSource g = graph.traversal(); + final Vertex v = g.addV("a", "b").next(); + assertThat(v.labels(), containsInAnyOrder(Vertex.DEFAULT_LABEL, "a", "b")); + } finally { + graph.close(); + } + } + + @Test + public void oneOrMoreMode_addDuplicateLabel_shouldBeNoOp() throws Exception { + final Graph graph = openOneOrMore(); + try { + final GraphTraversalSource g = graph.traversal(); + final Vertex v = g.addV("person").next(); + // Has {"vertex", "person"}. Adding "person" again should be no-op. + v.addLabel("person"); + assertThat(v.labels(), containsInAnyOrder(Vertex.DEFAULT_LABEL, "person")); + assertThat(v.labels(), hasSize(2)); + } finally { + graph.close(); + } + } + + @Test + public void oneOrMoreMode_dropNonExistentLabel_shouldBeNoOp() throws Exception { + final Graph graph = openOneOrMore(); + try { + final GraphTraversalSource g = graph.traversal(); + final Vertex v = g.addV("person").next(); + // Has {"vertex", "person"}. Dropping "nonexistent" should be no-op. + v.dropLabel("nonexistent"); + assertThat(v.labels(), containsInAnyOrder(Vertex.DEFAULT_LABEL, "person")); + assertThat(v.labels(), hasSize(2)); + } finally { + graph.close(); + } + } + + // ======================================================================== + // ZERO_OR_MORE mode + // ======================================================================== + + private Graph openZeroOrMore() { + final Configuration config = new BaseConfiguration(); + config.setProperty(Graph.GRAPH, TinkerGraph.class.getName()); + config.setProperty(AbstractTinkerGraph.GREMLIN_TINKERGRAPH_VERTEX_LABEL_CARDINALITY, "ZERO_OR_MORE"); + return TinkerGraph.open(config); + } + + @Test + public void zeroOrMoreMode_labelSingular_shouldReturnEmptyStringForNoLabels() throws Exception { + final Graph graph = openZeroOrMore(); + try { + final GraphTraversalSource g = graph.traversal(); + final Vertex v = g.addV().next(); + assertThat(v.labels(), is(empty())); + // label() (singular, deprecated) should return "" not null + assertThat(v.label(), isString("")); + } finally { + graph.close(); + } + } + + // helper for static import of Matchers.is and empty + private static org.hamcrest.Matcher> is(org.hamcrest.Matcher> matcher) { + return org.hamcrest.Matchers.is(matcher); + } + + private static org.hamcrest.Matcher> empty() { + return org.hamcrest.Matchers.empty(); + } + + private static org.hamcrest.Matcher isString(final String expected) { + return org.hamcrest.Matchers.is(expected); + } +} diff --git a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/LabelReplacePatternValidationTest.java b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/LabelReplacePatternValidationTest.java new file mode 100644 index 00000000000..be5e427e043 --- /dev/null +++ b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/LabelReplacePatternValidationTest.java @@ -0,0 +1,264 @@ +/* + * 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.Merge; +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__; +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.AbstractTinkerGraph; +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.LinkedHashMap; +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.empty; +import static org.hamcrest.Matchers.hasSize; +import static org.junit.Assert.assertEquals; + +/** + * Validation tests for label replace/swap/clear workaround patterns + * with multi-label support on TinkerGraph (ZERO_OR_MORE vertex cardinality). + * + * These tests validate that common user patterns for replacing labels work + * correctly in the context of the append-only onMatch T.label semantics. + */ +public class LabelReplacePatternValidationTest { + + private Graph graph; + private GraphTraversalSource g; + + @Before + public void setup() { + final org.apache.commons.configuration2.Configuration config = new org.apache.commons.configuration2.BaseConfiguration(); + config.setProperty(Graph.GRAPH, TinkerGraph.class.getName()); + config.setProperty(AbstractTinkerGraph.GREMLIN_TINKERGRAPH_VERTEX_LABEL_CARDINALITY, "ZERO_OR_MORE"); + graph = TinkerGraph.open(config); + g = graph.traversal(); + } + + @After + public void tearDown() throws Exception { + graph.close(); + } + + // ===================================================== + // Pattern 1: Replace all labels via post-merge chaining + // ===================================================== + + @Test + public void pattern1_replaceAllLabelsPostMerge() { + // Setup: vertex with labels [person, employee] + g.addV("person").property("name", "marko").addLabel("employee").iterate(); + + // Verify initial state + final Set before = g.V().has("name", "marko").next().labels(); + assertThat(before, hasSize(2)); + assertThat(before, containsInAnyOrder("person", "employee")); + + // Verify mergeV can find it + final Vertex merged = g.mergeV(new LinkedHashMap<>(Map.of("name", "marko"))).next(); + assertEquals("marko", merged.value("name")); + + // Pattern: dropLabels() then addLabel() after merge + g.V().has("name", "marko").dropLabels().addLabel("manager").iterate(); + + // Verify result + final Set after = g.V().has("name", "marko").next().labels(); + assertThat(after, hasSize(1)); + assertThat(after, containsInAnyOrder("manager")); + } + + // ===================================================== + // Pattern 2: Replace specific label + // ===================================================== + + @Test + public void pattern2_replaceSpecificLabel() { + // Setup: vertex with labels [person, employee] + g.addV("person").property("name", "josh").addLabel("employee").iterate(); + + // Verify initial state + final Set before = g.V().has("name", "josh").next().labels(); + assertThat(before, hasSize(2)); + assertThat(before, containsInAnyOrder("person", "employee")); + + // Pattern: dropLabel("employee") then addLabel("manager") + g.V().has("name", "josh").dropLabel("employee").addLabel("manager").iterate(); + + // Verify result: person remains, employee replaced with manager + final Set after = g.V().has("name", "josh").next().labels(); + assertThat(after, hasSize(2)); + assertThat(after, containsInAnyOrder("person", "manager")); + } + + // ===================================================== + // Pattern 3: Clear all labels + // ===================================================== + + @Test + public void pattern3_clearAllLabels() { + // Setup: vertex with label [person] + g.addV("person").property("name", "vadas").iterate(); + + // Verify initial state + final Set before = g.V().has("name", "vadas").next().labels(); + assertThat(before, hasSize(1)); + assertThat(before, containsInAnyOrder("person")); + + // Pattern: dropLabels() to clear all + g.V().has("name", "vadas").dropLabels().iterate(); + + // Verify result: empty label set (ZERO_OR_MORE cardinality allows this) + final Set after = g.V().has("name", "vadas").next().labels(); + assertThat(after, empty()); + } + + // ===================================================== + // Pattern 4: mergeV then replace labels on match + // ===================================================== + + @Test + public void pattern4_mergeVThenReplaceLabelsOnMatch() { + // Setup: vertex with label [person] + g.addV("person").property("name", "marko").iterate(); + + // Verify initial state + final Set before = g.V().has("name", "marko").next().labels(); + assertThat(before, hasSize(1)); + assertThat(before, containsInAnyOrder("person")); + + // Pattern: mergeV finds the vertex, then chain dropLabels().addLabel() + final Map mergeMap = new LinkedHashMap<>(); + mergeMap.put(T.label, "person"); + mergeMap.put("name", "marko"); + g.mergeV(mergeMap).dropLabels().addLabel("manager").iterate(); + + // Verify result + final Set after = g.V().has("name", "marko").next().labels(); + assertThat(after, hasSize(1)); + assertThat(after, containsInAnyOrder("manager")); + } + + // ===================================================== + // Pattern 5: onMatch with T.label is APPEND-ONLY + // ===================================================== + + @Test + public void pattern5_onMatchTLabelAppendsOnly() { + // Setup: vertex with label [person] + g.addV("person").property("name", "alex").iterate(); + + // onMatch with T.label adds to existing labels, does NOT replace + final Map mergeMap = new LinkedHashMap<>(); + mergeMap.put(T.label, "person"); + mergeMap.put("name", "alex"); + + final Map onMatchMap = new LinkedHashMap<>(); + onMatchMap.put(T.label, "manager"); + + g.mergeV(mergeMap).option(Merge.onMatch, onMatchMap).iterate(); + + // Result: labels are [person, manager] - "manager" was appended + final Set after = g.V().has("name", "alex").next().labels(); + assertThat(after, hasSize(2)); + assertThat(after, containsInAnyOrder("person", "manager")); + } + + // ===================================================== + // ===================================================== + // Pattern 8: mergeV + dropLabels + addLabel with sideEffect pattern + // (label replace using sideEffect within the traversal) + // ===================================================== + + @Test + public void pattern8_labelReplaceViaSideEffectInTraversal() { + // Setup: vertex with labels [person, employee] + g.addV("person").property("name", "dana").addLabel("employee").iterate(); + + // Pattern: use sideEffect to perform mutation inline + g.V().has("name", "dana").sideEffect(__.dropLabels()).addLabel("manager").iterate(); + + // Verify result + final Set after = g.V().has("name", "dana").next().labels(); + assertThat(after, hasSize(1)); + assertThat(after, containsInAnyOrder("manager")); + } + + // ===================================================== + // Pattern 9: Conditional label replace using choose() + // ===================================================== + + @Test + public void pattern9_conditionalLabelReplace() { + // Setup: vertex with labels [person, temp_worker] + g.addV("person").property("name", "bob").addLabel("temp_worker").iterate(); + + // Pattern: if has "temp_worker" label, swap to "permanent" + g.V().has("name", "bob"). + choose(__.hasLabel("temp_worker"), + __.dropLabel("temp_worker").addLabel("permanent")). + iterate(); + + // Verify result: person kept, temp_worker -> permanent + final Set after = g.V().has("name", "bob").next().labels(); + assertThat(after, hasSize(2)); + assertThat(after, containsInAnyOrder("person", "permanent")); + } + + // ===================================================== + // Pattern 10: onMatch with sideEffect for label drop+add + // (demonstrates that sideEffect traversal in onMatch can + // mutate labels, but the Map returned still uses append) + // ===================================================== + + @Test + public void pattern10_onMatchSideEffectForLabelMutation() { + // Setup: vertex with labels [person, employee] + g.addV("person").property("name", "eve").addLabel("employee").iterate(); + + // The onMatch option traversal can use sideEffect to mutate labels + // but the final Map returned (empty here) doesn't touch labels + final Map mergeMap = new LinkedHashMap<>(); + mergeMap.put(T.label, "person"); + mergeMap.put("name", "eve"); + + final Map emptyMatch = new LinkedHashMap<>(); + + g.withSideEffect("m", emptyMatch). + mergeV(mergeMap). + option(Merge.onMatch, __.sideEffect(__.dropLabels().addLabel("manager")).select("m")). + iterate(); + + // Verify: labels should be [manager] because sideEffect ran drop+add + final Set after = g.V().has("name", "eve").next().labels(); + assertThat(after, hasSize(1)); + assertThat(after, containsInAnyOrder("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 00000000000..8d30574624a --- /dev/null +++ b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertexMultiLabelGremlinLangTest.java @@ -0,0 +1,113 @@ +/* + * 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.apache.tinkerpop.gremlin.tinkergraph.structure.AbstractTinkerGraph; +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 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() { + final org.apache.commons.configuration2.Configuration config = new org.apache.commons.configuration2.BaseConfiguration(); + config.setProperty(Graph.GRAPH, TinkerGraph.class.getName()); + config.setProperty(AbstractTinkerGraph.GREMLIN_TINKERGRAPH_VERTEX_LABEL_CARDINALITY, "ZERO_OR_MORE"); + graph = TinkerGraph.open(config); + g = graph.traversal(); + } + + @After + public void tearDown() throws Exception { + graph.close(); + } + + @Test + public void shouldNotDoubleRecordAddLabelInGremlinLangFromSource() { + final GraphTraversal 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 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 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 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 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(); + } + } +} 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 00000000000..a06be7eff09 --- /dev/null +++ b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertexMultiLabelTest.java @@ -0,0 +1,199 @@ +/* + * 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.apache.tinkerpop.gremlin.tinkergraph.structure.AbstractTinkerGraph; +import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerGraph; +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() { + final org.apache.commons.configuration2.Configuration config = new org.apache.commons.configuration2.BaseConfiguration(); + config.setProperty(Graph.GRAPH, TinkerGraph.class.getName()); + config.setProperty(AbstractTinkerGraph.GREMLIN_TINKERGRAPH_VERTEX_LABEL_CARDINALITY, "ZERO_OR_MORE"); + graph = TinkerGraph.open(config); + 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(); + // Under ZERO_OR_MORE cardinality, addV() with no label produces an empty label set + assertThat(v.labels(), hasSize(0)); + } + + @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(); + // Under ZERO_OR_MORE cardinality, dropping all labels results in empty set (no virtual default) + assertThat(v.labels(), hasSize(0)); + } + + @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 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"); + // Under ZERO_OR_MORE cardinality, dropping last label results in empty set (no virtual default) + assertThat(v.labels(), hasSize(0)); + } + + @Test(expected = UnsupportedOperationException.class) + public void shouldAddLabelToEdge() { + final Vertex v1 = g.addV("person").next(); + final Vertex v2 = g.addV("person").next(); + final Edge e = v1.addEdge("knows", v2); + // Under ZERO_OR_ONE cardinality for edges, adding a second label throws + e.addLabel("friend"); + } + + @Test(expected = UnsupportedOperationException.class) + public void shouldDropLabelsOnEdge() { + final Vertex v1 = g.addV("person").next(); + final Vertex v2 = g.addV("person").next(); + final Edge e = v1.addEdge("knows", v2); + // Edge cardinality is always ONE - dropLabels() throws + 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 shouldAddLabelToExistingVertexWithDefaultLabel() { + // Under ZERO_OR_MORE, addV() assigns no labels. Adding "person" creates a single-label set. + final Vertex v = g.addV().next(); + assertThat(v.labels(), hasSize(0)); + 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")); + } +} From f4180a2843287ef493ac97980a95d25a2e8957b1 Mon Sep 17 00:00:00 2001 From: xiazcy Date: Fri, 26 Jun 2026 16:12:02 -0700 Subject: [PATCH 03/29] Add GLV implementations for multi-label across Go, Python, JS, .NET - GremlinLang with("multilabel")/with("singlelabel") propagation in all GLVs - Fix: render options in gremlin text both when OptionsStrategy is first created and when it already exists (from prior with() calls like with("language",...)) - Go: With() method now variadic (single-arg g.With("multilabel") works) - Feature tests: full matrix for valueMap/elementMap with @MultiLabel, @MultiLabelDefault, @SingleLabelDefault tags - Existing valueMap/elementMap tests use with("singlelabel") for provider safety - Test infrastructure: @MultiLabelDefault uses gmultilabel + with("multilabel") programmatically (no server-side YAML config needed) - Docker gremlin-server-integration.yaml: multilabel graph config - Cucumber support in all GLVs: terrain/world/steps handle multilabel graphs --- .../gremlin-server-integration.yaml | 3 + .../Process/Traversal/GraphTraversal.cs | 94 + .../Process/Traversal/GraphTraversalSource.cs | 42 +- .../Process/Traversal/GremlinLang.cs | 17 + .../src/Gremlin.Net/Process/Traversal/__.cs | 72 + .../src/Gremlin.Net/Structure/Edge.cs | 14 +- .../IO/GraphBinary4/Types/EdgeSerializer.cs | 29 +- .../IO/GraphBinary4/Types/VertexSerializer.cs | 9 +- .../src/Gremlin.Net/Structure/Vertex.cs | 13 +- .../Gherkin/CommonSteps.cs | 40 +- .../Gherkin/Gremlin.cs | 59 +- .../Gherkin/ScenarioData.cs | 20 + gremlin-go/docker-compose.yml | 1 + gremlin-go/driver/anonymousTraversal.go | 28 + .../driver/cucumber/cucumberSteps_test.go | 43 +- gremlin-go/driver/cucumber/cucumberWorld.go | 22 +- gremlin-go/driver/cucumber/gremlin.go | 59 +- gremlin-go/driver/graph.go | 6 +- gremlin-go/driver/graphBinaryDeserializer.go | 15 +- gremlin-go/driver/graphBinarySerializer.go | 32 +- gremlin-go/driver/graphTraversal.go | 24 + gremlin-go/driver/graphTraversalSource.go | 18 +- gremlin-go/driver/graph_test.go | 6 +- gremlin-go/driver/gremlinlang.go | 7 + .../lib/process/graph-traversal.ts | 48 + .../lib/process/gremlin-lang.ts | 11 + .../gremlin-javascript/lib/structure/graph.ts | 30 +- .../io/binary/internals/EdgeSerializer.js | 49 +- .../io/binary/internals/GraphSerializer.js | 24 +- .../io/binary/internals/VertexSerializer.js | 16 +- .../test/cucumber/feature-steps.js | 30 +- .../test/cucumber/gremlin.js | 59 +- .../gremlin-javascript/test/cucumber/world.js | 24 +- .../gremlin-mcp/src/translator/stepNames.ts | 8 + .../gremlin_python/process/graph_traversal.py | 123 ++ .../python/gremlin_python/structure/graph.py | 26 +- .../structure/io/graphbinaryV4.py | 55 +- .../python/tests/feature/feature_steps.py | 21 +- .../src/main/python/tests/feature/gremlin.py | 59 +- .../src/main/python/tests/feature/terrain.py | 13 + .../conf/tinkergraph-multilabel.properties | 21 + .../language/translator/translations.json | 1633 +++++++++++++++-- .../gremlin/test/features/map/AddEdge.feature | 2 +- .../test/features/map/AddVertex.feature | 25 +- .../test/features/map/ElementMap.feature | 85 +- .../gremlin/test/features/map/Labels.feature | 94 + .../test/features/map/MergeEdge.feature | 4 +- .../test/features/map/MergeVertex.feature | 141 ++ .../test/features/map/ValueMap.feature | 89 +- .../test/features/sideEffect/AddLabel.feature | 80 + .../features/sideEffect/DropLabel.feature | 124 ++ 51 files changed, 3232 insertions(+), 335 deletions(-) create mode 100644 gremlin-server/conf/tinkergraph-multilabel.properties create mode 100644 gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Labels.feature create mode 100644 gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/sideEffect/AddLabel.feature create mode 100644 gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/sideEffect/DropLabel.feature diff --git a/docker/gremlin-server/gremlin-server-integration.yaml b/docker/gremlin-server/gremlin-server-integration.yaml index 1880b34810e..a730970f851 100644 --- a/docker/gremlin-server/gremlin-server-integration.yaml +++ b/docker/gremlin-server/gremlin-server-integration.yaml @@ -23,6 +23,9 @@ graphs: { graph: { configuration: conf/tinkergraph-service.properties, traversalSources: [{name: g}, {name: ggraph}]}, + multilabel: { + configuration: conf/tinkergraph-multilabel.properties, + traversalSources: [{name: gmultilabel}]}, immutable: { configuration: conf/tinkergraph-service.properties, traversalSources: [{name: gimmutable}]}, diff --git a/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GraphTraversal.cs b/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GraphTraversal.cs index 13447b6cf60..78393e3d9e8 100644 --- a/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GraphTraversal.cs +++ b/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GraphTraversal.cs @@ -140,6 +140,21 @@ public GraphTraversal AddE (ITraversal edgeLabelTraversal) return Wrap(this); } + ///

+ /// Adds the addE step with multiple labels to this . + /// + public GraphTraversal AddE (string label1, string label2, params string[] moreLabels) + { + if (label1 == null) throw new ArgumentNullException(nameof(label1)); + if (label2 == null) throw new ArgumentNullException(nameof(label2)); + if (moreLabels == null) throw new ArgumentNullException(nameof(moreLabels)); + + var args = new List(2 + moreLabels.Length) { label1, label2 }; + args.AddRange(moreLabels); + GremlinLang.AddStep("addE", args.ToArray()); + return Wrap(this); + } + /// /// Adds the addV step to this . /// @@ -167,6 +182,21 @@ public GraphTraversal AddV (ITraversal vertexLabelTraversal) return Wrap(this); } + /// + /// Adds the addV step with multiple labels to this . + /// + public GraphTraversal AddV (string label1, string label2, params string[] moreLabels) + { + if (label1 == null) throw new ArgumentNullException(nameof(label1)); + if (label2 == null) throw new ArgumentNullException(nameof(label2)); + if (moreLabels == null) throw new ArgumentNullException(nameof(moreLabels)); + + var args = new List(2 + moreLabels.Length) { label1, label2 }; + args.AddRange(moreLabels); + GremlinLang.AddStep("addV", args.ToArray()); + return Wrap(this); + } + /// /// Adds the aggregate step to this . /// @@ -1278,6 +1308,70 @@ public GraphTraversal Label () GremlinLang.AddStep("label"); return Wrap(this); } + + /// + /// Adds the labels step to this . + /// + public GraphTraversal Labels () + { + GremlinLang.AddStep("labels"); + return Wrap(this); + } + + /// + /// Adds the addLabel step to this . + /// + public GraphTraversal AddLabel (string label, params string[] moreLabels) + { + if (label == null) throw new ArgumentNullException(nameof(label)); + if (moreLabels == null) throw new ArgumentNullException(nameof(moreLabels)); + + var args = new List(1 + moreLabels.Length) { label }; + args.AddRange(moreLabels); + GremlinLang.AddStep("addLabel", args.ToArray()); + return Wrap(this); + } + + /// + /// Adds the addLabel step to this . + /// + public GraphTraversal AddLabel (ITraversal labelTraversal) + { + GremlinLang.AddStep("addLabel", labelTraversal); + return Wrap(this); + } + + /// + /// Adds the dropLabels step to this . + /// + public GraphTraversal DropLabels () + { + GremlinLang.AddStep("dropLabels"); + return Wrap(this); + } + + /// + /// Adds the dropLabel step to this . + /// + public GraphTraversal DropLabel (string label, params string[] moreLabels) + { + if (label == null) throw new ArgumentNullException(nameof(label)); + if (moreLabels == null) throw new ArgumentNullException(nameof(moreLabels)); + + var args = new List(1 + moreLabels.Length) { label }; + args.AddRange(moreLabels); + GremlinLang.AddStep("dropLabel", args.ToArray()); + return Wrap(this); + } + + /// + /// Adds the dropLabel step to this . + /// + public GraphTraversal DropLabel (ITraversal labelTraversal) + { + GremlinLang.AddStep("dropLabel", labelTraversal); + return Wrap(this); + } /// /// Adds the length step to this . diff --git a/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GraphTraversalSource.cs b/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GraphTraversalSource.cs index 9de184eb883..0d4c3564adf 100644 --- a/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GraphTraversalSource.cs +++ b/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GraphTraversalSource.cs @@ -104,8 +104,14 @@ public GraphTraversalSource With(string key, object? value) optionsStrategy = existingOptions[existingOptions.Count - 1]; optionsStrategy.Configuration[key] = value; - return new GraphTraversalSource(new List(TraversalStrategies), + var source = new GraphTraversalSource(new List(TraversalStrategies), GremlinLang.Clone()); + // Render multilabel/singlelabel in gremlin text (temporary until these options are removed) + if (key == "multilabel" || key == "singlelabel") + { + source.GremlinLang.Append($".with(\"{key}\")"); + } + return source; } @@ -461,6 +467,23 @@ public GraphTraversal AddE(ITraversal edgeLabelTraversal) return traversal; } + /// + /// Spawns a off this graph traversal source and adds the addE step + /// with multiple labels to that traversal. + /// + public GraphTraversal AddE(string label1, string label2, params string[] moreLabels) + { + if (label1 == null) throw new ArgumentNullException(nameof(label1)); + if (label2 == null) throw new ArgumentNullException(nameof(label2)); + if (moreLabels == null) throw new ArgumentNullException(nameof(moreLabels)); + + var traversal = new GraphTraversal(TraversalStrategies, GremlinLang.Clone()); + var args = new List(2 + moreLabels.Length) { label1, label2 }; + args.AddRange(moreLabels); + traversal.GremlinLang.AddStep("addE", args.ToArray()); + return traversal; + } + /// /// Spawns a off this graph traversal source and adds the mergeE step to that /// traversal. @@ -522,6 +545,23 @@ public GraphTraversal AddV(ITraversal vertexLabelTraversal) return traversal; } + /// + /// Spawns a off this graph traversal source and adds the addV step + /// with multiple labels to that traversal. + /// + public GraphTraversal AddV(string label1, string label2, params string[] moreLabels) + { + if (label1 == null) throw new ArgumentNullException(nameof(label1)); + if (label2 == null) throw new ArgumentNullException(nameof(label2)); + if (moreLabels == null) throw new ArgumentNullException(nameof(moreLabels)); + + var traversal = new GraphTraversal(TraversalStrategies, GremlinLang.Clone()); + var args = new List(2 + moreLabels.Length) { label1, label2 }; + args.AddRange(moreLabels); + traversal.GremlinLang.AddStep("addV", args.ToArray()); + return traversal; + } + /// /// Spawns a off this graph traversal source and adds the mergeV step to that /// traversal. diff --git a/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GremlinLang.cs b/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GremlinLang.cs index 684a76c2f6f..4cf6eb9f079 100644 --- a/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GremlinLang.cs +++ b/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GremlinLang.cs @@ -99,6 +99,14 @@ public void AddG(string g) _parameters["g"] = g; } + /// + /// Appends raw text to the gremlin string. Used for temporary options rendering. + /// + internal void Append(string text) + { + _gremlin.Append(text); + } + /// /// Gets the gremlin-lang compatible string representation prefixed with "g". /// @@ -581,6 +589,15 @@ private string BuildStrategyArgs(object?[] arguments) if (arg is OptionsStrategy optionsStrategy) { _optionsStrategies.Add(optionsStrategy); + // Render multilabel/singlelabel in gremlin text (temporary until these options are removed) + if (optionsStrategy.Configuration.ContainsKey("multilabel")) + { + _gremlin.Append(".with(\"multilabel\")"); + } + if (optionsStrategy.Configuration.ContainsKey("singlelabel")) + { + _gremlin.Append(".with(\"singlelabel\")"); + } continue; } diff --git a/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/__.cs b/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/__.cs index 974521747f4..1d21df4a32c 100644 --- a/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/__.cs +++ b/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/__.cs @@ -76,6 +76,16 @@ public static GraphTraversal AddE(ITraversal edgeLabelTraversal) return new GraphTraversal().AddE(edgeLabelTraversal); } + /// + /// Spawns a and adds the addE step with multiple labels to that traversal. + /// + public static GraphTraversal AddE(string label1, string label2, params string[] moreLabels) + { + return moreLabels is { Length: 0 } + ? new GraphTraversal().AddE(label1, label2) + : new GraphTraversal().AddE(label1, label2, moreLabels); + } + /// /// Spawns a and adds the addV step to that traversal. /// @@ -100,6 +110,16 @@ public static GraphTraversal AddV(ITraversal vertexLabelTraversa return new GraphTraversal().AddV(vertexLabelTraversal); } + /// + /// Spawns a and adds the addV step with multiple labels to that traversal. + /// + public static GraphTraversal AddV(string label1, string label2, params string[] moreLabels) + { + return moreLabels is { Length: 0 } + ? new GraphTraversal().AddV(label1, label2) + : new GraphTraversal().AddV(label1, label2, moreLabels); + } + /// /// Spawns a and adds the aggregate step to that traversal. /// @@ -893,6 +913,58 @@ public static GraphTraversal Label() { return new GraphTraversal().Label(); } + + /// + /// Spawns a and adds the labels step to that traversal. + /// + public static GraphTraversal Labels() + { + return new GraphTraversal().Labels(); + } + + /// + /// Spawns a and adds the addLabel step to that traversal. + /// + public static GraphTraversal AddLabel(string label, params string[] moreLabels) + { + return moreLabels is { Length: 0 } + ? new GraphTraversal().AddLabel(label) + : new GraphTraversal().AddLabel(label, moreLabels); + } + + /// + /// Spawns a and adds the addLabel step to that traversal. + /// + public static GraphTraversal AddLabel(ITraversal labelTraversal) + { + return new GraphTraversal().AddLabel(labelTraversal); + } + + /// + /// Spawns a and adds the dropLabels step to that traversal. + /// + public static GraphTraversal DropLabels() + { + return new GraphTraversal().DropLabels(); + } + + /// + /// Spawns a and adds the dropLabel step to that traversal. + /// + public static GraphTraversal DropLabel(string label, params string[] moreLabels) + { + return moreLabels is { Length: 0 } + ? new GraphTraversal().DropLabel(label) + : new GraphTraversal().DropLabel(label, moreLabels); + } + + /// + /// Spawns a and adds the dropLabel step to that traversal. + /// + public static GraphTraversal DropLabel(ITraversal labelTraversal) + { + return new GraphTraversal().DropLabel(labelTraversal); + } /// /// Spawns a and adds the length step to that traversal. diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/Edge.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/Edge.cs index 63461fa4b23..d0e2cc01dbc 100644 --- a/gremlin-dotnet/src/Gremlin.Net/Structure/Edge.cs +++ b/gremlin-dotnet/src/Gremlin.Net/Structure/Edge.cs @@ -21,6 +21,8 @@ #endregion +using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; namespace Gremlin.Net.Structure @@ -38,13 +40,23 @@ public class Edge : Element /// The label of the edge. /// The incoming/head vertex of the edge. /// Optional properties of the edge. - public Edge(object? id, Vertex outV, string label, Vertex inV, dynamic[]? properties = null) + /// Optional set of labels for multi-label support. + public Edge(object? id, Vertex outV, string label, Vertex inV, dynamic[]? properties = null, + IEnumerable? labels = null) : base(id, label, properties) { OutV = outV; InV = inV; + Labels = labels != null + ? ImmutableHashSet.CreateRange(labels) + : ImmutableHashSet.Create(label ?? "edge"); } + /// + /// Gets all labels on this edge. + /// + public IReadOnlySet Labels { get; } + /// /// Gets the incoming/head vertex of this edge. /// diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/EdgeSerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/EdgeSerializer.cs index b3a8097de5e..483d4b3fd88 100644 --- a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/EdgeSerializer.cs +++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/EdgeSerializer.cs @@ -47,12 +47,12 @@ protected override async Task WriteValueAsync(Edge value, Stream stream, GraphBi CancellationToken cancellationToken = default) { await writer.WriteAsync(value.Id, stream, cancellationToken).ConfigureAwait(false); - await writer.WriteNonNullableValueAsync(new List { value.Label }, stream, cancellationToken).ConfigureAwait(false); + await writer.WriteNonNullableValueAsync(new List(value.Labels), stream, cancellationToken).ConfigureAwait(false); await writer.WriteAsync(value.InV.Id, stream, cancellationToken).ConfigureAwait(false); - await writer.WriteNonNullableValueAsync(new List { value.InV.Label }, stream, cancellationToken).ConfigureAwait(false); + await writer.WriteNonNullableValueAsync(new List(value.InV.Labels), stream, cancellationToken).ConfigureAwait(false); await writer.WriteAsync(value.OutV.Id, stream, cancellationToken).ConfigureAwait(false); - await writer.WriteNonNullableValueAsync(new List { value.OutV.Label }, stream, cancellationToken).ConfigureAwait(false); + await writer.WriteNonNullableValueAsync(new List(value.OutV.Labels), stream, cancellationToken).ConfigureAwait(false); // Placeholder for the parent vertex await writer.WriteAsync(null, stream, cancellationToken).ConfigureAwait(false); @@ -68,14 +68,31 @@ protected override async Task ReadValueAsync(Stream stream, GraphBinaryRea var id = await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false); var labelList = (List)await reader.ReadNonNullableValueAsync>(stream, cancellationToken).ConfigureAwait(false); var label = labelList.Count > 0 ? labelList[0] ?? "" : ""; + var labels = new List(); + foreach (var l in labelList) + { + if (l != null) labels.Add(l); + } var inVId = await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false); var inVLabelList = (List)await reader.ReadNonNullableValueAsync>(stream, cancellationToken).ConfigureAwait(false); - var inV = new Vertex(inVId, inVLabelList.Count > 0 ? inVLabelList[0] ?? "" : ""); + var inVLabel = inVLabelList.Count > 0 ? inVLabelList[0] ?? "" : ""; + var inVLabels = new List(); + foreach (var l in inVLabelList) + { + if (l != null) inVLabels.Add(l); + } + var inV = new Vertex(inVId, inVLabel, labels: inVLabels); var outVId = await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false); var outVLabelList = (List)await reader.ReadNonNullableValueAsync>(stream, cancellationToken).ConfigureAwait(false); - var outV = new Vertex(outVId, outVLabelList.Count > 0 ? outVLabelList[0] ?? "" : ""); + var outVLabel = outVLabelList.Count > 0 ? outVLabelList[0] ?? "" : ""; + var outVLabels = new List(); + foreach (var l in outVLabelList) + { + if (l != null) outVLabels.Add(l); + } + var outV = new Vertex(outVId, outVLabel, labels: outVLabels); // discard possible parent vertex await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false); @@ -83,7 +100,7 @@ protected override async Task ReadValueAsync(Stream stream, GraphBinaryRea var properties = await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false); var propertiesAsArray = null == properties ? Array.Empty() : (properties as List)?.ToArray(); - return new Edge(id, outV, label, inV, propertiesAsArray); + return new Edge(id, outV, label, inV, propertiesAsArray, labels); } } } diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/VertexSerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/VertexSerializer.cs index 9bad7be2d7a..2c3e1c2d5ed 100644 --- a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/VertexSerializer.cs +++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/VertexSerializer.cs @@ -47,7 +47,7 @@ protected override async Task WriteValueAsync(Vertex value, Stream stream, Graph CancellationToken cancellationToken = default) { await writer.WriteAsync(value.Id, stream, cancellationToken).ConfigureAwait(false); - await writer.WriteNonNullableValueAsync(new List { value.Label }, stream, cancellationToken).ConfigureAwait(false); + await writer.WriteNonNullableValueAsync(new List(value.Labels), stream, cancellationToken).ConfigureAwait(false); await writer.WriteAsync(null, stream, cancellationToken).ConfigureAwait(false); // properties } @@ -58,11 +58,16 @@ protected override async Task ReadValueAsync(Stream stream, GraphBinaryR var id = await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false); var labelList = (List)await reader.ReadNonNullableValueAsync>(stream, cancellationToken).ConfigureAwait(false); var label = labelList.Count > 0 ? labelList[0] ?? "" : ""; + var labels = new List(); + foreach (var l in labelList) + { + if (l != null) labels.Add(l); + } var properties = await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false); var propertiesAsArray = null == properties ? Array.Empty() : (properties as List)?.ToArray(); - return new Vertex(id, label, propertiesAsArray); + return new Vertex(id, label, propertiesAsArray, labels); } } } diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/Vertex.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/Vertex.cs index 6f56102ac0e..036efd46e42 100644 --- a/gremlin-dotnet/src/Gremlin.Net/Structure/Vertex.cs +++ b/gremlin-dotnet/src/Gremlin.Net/Structure/Vertex.cs @@ -22,6 +22,7 @@ #endregion using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; namespace Gremlin.Net.Structure @@ -42,11 +43,21 @@ public class Vertex : Element /// The id of the vertex. /// The label of the vertex. /// Optional properties of the vertex. - public Vertex(object? id, string label = DefaultLabel, dynamic[]? properties = null) + /// Optional set of labels for multi-label support. + public Vertex(object? id, string label = DefaultLabel, dynamic[]? properties = null, + IEnumerable? labels = null) : base(id, label, properties) { + Labels = labels != null + ? ImmutableHashSet.CreateRange(labels) + : ImmutableHashSet.Create(label ?? DefaultLabel); } + /// + /// Gets all labels on this vertex. + /// + public IReadOnlySet Labels { get; } + /// /// Get property by key /// diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/CommonSteps.cs b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/CommonSteps.cs index 2c42d1a8bed..2e1218494c5 100644 --- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/CommonSteps.cs +++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/CommonSteps.cs @@ -105,13 +105,37 @@ internal class CommonSteps : StepDefinition [Given("the (\\w+) graph")] public void ChooseModernGraph(string graphName) { - if (graphName == "empty") + var isMultiLabel = ScenarioData.CurrentScenario != null && + (ScenarioData.CurrentScenario.Tags.Any(t => t.Name == "@MultiLabel") || + (ScenarioData.CurrentFeature != null && ScenarioData.CurrentFeature.Tags.Any(t => t.Name == "@MultiLabel"))); + var isMultiLabelDefault = ScenarioData.CurrentScenario != null && + (ScenarioData.CurrentScenario.Tags.Any(t => t.Name == "@MultiLabelDefault") || + (ScenarioData.CurrentFeature != null && ScenarioData.CurrentFeature.Tags.Any(t => t.Name == "@MultiLabelDefault"))); + + if (isMultiLabelDefault && graphName == "empty") { - ScenarioData.CleanEmptyData(); + ScenarioData.CleanMultilabelData(); + var data = ScenarioData.GetByGraphName("multilabel"); + _graphName = "multilabel"; + _g = Traversal().With(data.Connection).With("multilabel"); + } + else if (isMultiLabel && graphName == "empty") + { + ScenarioData.CleanMultilabelData(); + var data = ScenarioData.GetByGraphName("multilabel"); + _graphName = "multilabel"; + _g = Traversal().With(data.Connection); + } + else + { + if (graphName == "empty") + { + ScenarioData.CleanEmptyData(); + } + var data = ScenarioData.GetByGraphName(graphName); + _graphName = graphName; + _g = Traversal().With(data.Connection); } - var data = ScenarioData.GetByGraphName(graphName); - _graphName = graphName; - _g = Traversal().With(data.Connection); } [Given("using the parameter (\\w+) defined as \"(.*)\"")] @@ -159,11 +183,15 @@ public void InitTraversal(string traversalText) Gremlin.UseTraversal(ScenarioData.CurrentScenario!.Name, _g, _parameters, _sideEffects); traversal.Iterate(); - // We may have modified the so-called `empty` graph + // We may have modified the so-called `empty` or `multilabel` graph if (_graphName == "empty") { ScenarioData.ReloadEmptyData(); } + else if (_graphName == "multilabel") + { + ScenarioData.ReloadMultilabelData(); + } } [Given("an unsupported test")] diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs index 315b5fb70a3..067bd4ffc18 100644 --- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs +++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs @@ -956,6 +956,8 @@ private static IDictionary, ITraversal>> {(g,p) =>g.AddV((string) "person").Property("name", "marko").Property("age", 29).As("marko").AddV((string) "person").Property("name", "vadas").Property("age", 27).As("vadas").AddV((string) "software").Property("name", "lop").Property("lang", "java").As("lop").AddV((string) "person").Property("name", "josh").Property("age", 32).As("josh").AddV((string) "software").Property("name", "ripple").Property("lang", "java").As("ripple").AddV((string) "person").Property("name", "peter").Property("age", 35).As("peter").AddE((string) "knows").From("marko").To("vadas").Property("weight", 0.5d).AddE((string) "knows").From("marko").To("josh").Property("weight", 1.0d).AddE((string) "created").From("marko").To("lop").Property("weight", 0.4d).AddE((string) "created").From("josh").To("ripple").Property("weight", 1.0d).AddE((string) "created").From("josh").To("lop").Property("weight", 0.4d).AddE((string) "created").From("peter").To("lop").Property("weight", 0.2d), (g,p) =>g.V().Limit(3).AddV((string) "software").Aggregate("a1").By(T.Label).Aggregate("a2").By(T.Label).Cap("a1", "a2").Select("a1", "a2").By(__.Unfold().Fold())}}, {"g_addV_propertyXname_markoX_withXkey_valueX_valuesXname_keyX", new List, ITraversal>> {(g,p) =>g.AddV().Property("name", "marko").With("key", "value").Values("name", "key")}}, {"g_addV_propertyXname_marko_since_2010X_withXkey_valueX_propertiesXnameX_valuesXsince_keyX", new List, ITraversal>> {(g,p) =>g.AddV().Property("name", "marko", "since", 2010).With("key", "value").Properties("name").Values("since", "key")}}, + {"g_addVXa_bX_labels", new List, ITraversal>> {(g,p) =>g.AddV((string) "a", (string) "b").Labels().Fold(), (g,p) =>g.V().HasLabel("a").HasLabel("b")}}, + {"g_addVXa_b_cX_labels_count", new List, ITraversal>> {(g,p) =>g.AddV((string) "a", (string) "b", (string) "c").Labels().Count()}}, {"g_injectX1X_asBool", new List, ITraversal>> {(g,p) =>g.Inject(1).AsBool()}}, {"g_injectX3_14X_asBool", new List, ITraversal>> {(g,p) =>g.Inject(3.14).AsBool()}}, {"g_injectXneg_1X_asBool", new List, ITraversal>> {(g,p) =>g.Inject(-1).AsBool()}}, @@ -1196,10 +1198,15 @@ private static IDictionary, ITraversal>> {(g,p) =>g.E().Properties().Element()}}, {"g_VXv7_properties_properties_element_element", new List, ITraversal>> {(g,p) =>g.V(p["vid7"]).Properties().Properties().Element().Element().Limit(1)}}, {"g_V_properties_properties_element_element", new List, ITraversal>> {(g,p) =>g.V(p["vid7"]).Properties().Properties().Element().Element()}}, - {"g_V_elementMap", new List, ITraversal>> {(g,p) =>g.V().ElementMap()}}, - {"g_V_elementMapXname_ageX", new List, ITraversal>> {(g,p) =>g.V().ElementMap("name", "age")}}, - {"g_EX11X_elementMap", new List, ITraversal>> {(g,p) =>g.E(p["eid11"]).ElementMap()}}, - {"g_V_elementMapXname_age_nullX", new List, ITraversal>> {(g,p) =>g.V().ElementMap("name", "age", null)}}, + {"g_V_elementMap", new List, ITraversal>> {(g,p) =>g.With("singlelabel").V().ElementMap()}}, + {"g_V_elementMapXname_ageX", new List, ITraversal>> {(g,p) =>g.With("singlelabel").V().ElementMap("name", "age")}}, + {"g_EX11X_elementMap", new List, ITraversal>> {(g,p) =>g.With("singlelabel").E(p["eid11"]).ElementMap()}}, + {"g_V_elementMapXname_age_nullX", new List, ITraversal>> {(g,p) =>g.With("singlelabel").V().ElementMap("name", "age", null)}}, + {"g_withXmultilabelX_VXmarkoX_elementMap", new List, ITraversal>> {(g,p) =>g.With("multilabel").V().Has("name", "marko").ElementMap()}}, + {"g_V_elementMap_single_label_default", new List, ITraversal>> {(g,p) =>g.AddV((string) "person").AddLabel("employee").Property("name", "marko"), (g,p) =>g.V().ElementMap()}}, + {"g_withXmultilabelX_V_elementMap_multilabel", new List, ITraversal>> {(g,p) =>g.AddV((string) "person").AddLabel("employee").Property("name", "marko"), (g,p) =>g.With("multilabel").V().ElementMap()}}, + {"g_V_elementMap_multi_label_default", new List, ITraversal>> {(g,p) =>g.AddV((string) "person").AddLabel("employee").Property("name", "marko"), (g,p) =>g.V().ElementMap()}}, + {"g_withXsinglelabelX_V_elementMap_multi_label_default", new List, ITraversal>> {(g,p) =>g.AddV((string) "person").AddLabel("employee").Property("name", "marko"), (g,p) =>g.With("singlelabel").V().ElementMap()}}, {"g_V_asXaX_flatMapXselectXaXX", new List, ITraversal>> {(g,p) =>g.V().As("a").FlatMap(__.Select("a"))}}, {"g_V_valuesXnameX_flatMapXsplitXaX_unfoldX", new List, ITraversal>> {(g,p) =>g.V().Values("name").FlatMap(__.Split("a").Unfold())}}, {"g_V_flatMapXout_outX_path", new List, ITraversal>> {(g,p) =>g.V().FlatMap(__.Out().Out()).Path()}}, @@ -1250,6 +1257,11 @@ private static IDictionary, ITraversal>> {(g,p) =>g.Inject(new List { 1, 2 }).LTrim(Scope.Local)}}, {"g_V_valuesXnameX_lTrim", new List, ITraversal>> {(g,p) =>g.AddV((string) "person").Property("name", " marko ").Property("age", 29).As("marko").AddV((string) "person").Property("name", " vadas ").Property("age", 27).As("vadas").AddV((string) "software").Property("name", " lop").Property("lang", "java").As("lop").AddV((string) "person").Property("name", "josh ").Property("age", 32).As("josh").AddV((string) "software").Property("name", " ripple ").Property("lang", "java").As("ripple").AddV((string) "person").Property("name", "peter").Property("age", 35).As("peter").AddE((string) "knows").From("marko").To("vadas").Property("weight", 0.5d).AddE((string) "knows").From("marko").To("josh").Property("weight", 1.0d).AddE((string) "created").From("marko").To("lop").Property("weight", 0.4d).AddE((string) "created").From("josh").To("ripple").Property("weight", 1.0d).AddE((string) "created").From("josh").To("lop").Property("weight", 0.4d).AddE((string) "created").From("peter").To("lop").Property("weight", 0.2d), (g,p) =>g.V().Values("name").LTrim()}}, {"g_V_valuesXnameX_order_fold_lTrimXlocalX", new List, ITraversal>> {(g,p) =>g.AddV((string) "person").Property("name", " marko ").Property("age", 29).As("marko").AddV((string) "person").Property("name", " vadas ").Property("age", 27).As("vadas").AddV((string) "software").Property("name", " lop").Property("lang", "java").As("lop").AddV((string) "person").Property("name", "josh ").Property("age", 32).As("josh").AddV((string) "software").Property("name", " ripple ").Property("lang", "java").As("ripple").AddV((string) "person").Property("name", "peter").Property("age", 35).As("peter").AddE((string) "knows").From("marko").To("vadas").Property("weight", 0.5d).AddE((string) "knows").From("marko").To("josh").Property("weight", 1.0d).AddE((string) "created").From("marko").To("lop").Property("weight", 0.4d).AddE((string) "created").From("josh").To("ripple").Property("weight", 1.0d).AddE((string) "created").From("josh").To("lop").Property("weight", 0.4d).AddE((string) "created").From("peter").To("lop").Property("weight", 0.2d), (g,p) =>g.V().Values("name").Order().Fold().LTrim(Scope.Local)}}, + {"g_V_hasLabelXpersonX_labels", new List, ITraversal>> {(g,p) =>g.V().HasLabel("person").Labels()}}, + {"g_V_labels_multilabel", new List, ITraversal>> {(g,p) =>g.AddV((string) "a", (string) "b"), (g,p) =>g.V().Labels()}}, + {"g_addVXa_bX_labels_count", new List, ITraversal>> {(g,p) =>g.AddV((string) "a", (string) "b"), (g,p) =>g.V().Labels().Count()}}, + {"g_addV_labels", new List, ITraversal>> {(g,p) =>g.AddV(), (g,p) =>g.V().Labels()}}, + {"g_E_labels", new List, ITraversal>> {(g,p) =>g.E().HasLabel("knows").Labels()}}, {"g_injectXfeature_test_nullX_length", new List, ITraversal>> {(g,p) =>g.Inject("feature", "test", null).Length()}}, {"g_injectXfeature_test_nullX_lengthXlocalX", new List, ITraversal>> {(g,p) =>g.Inject("feature", "test", null).Length(Scope.Local)}}, {"g_injectXListXa_bXX_length", new List, ITraversal>> {(g,p) =>g.Inject(new List { "a", "b" }).Length()}}, @@ -1516,6 +1528,15 @@ private static IDictionary, ITraversal>> {(g,p) =>g.MergeV((IDictionary) new Dictionary {}).Option(Merge.OnMatch, (IDictionary) p["xx1"])}}, {"g_injectXlist1_list2_list3X_fold_asXmX_mergeVXselectXmX_limitXlocal_1X_unfoldX_optionXonCreate_selectXmX_rangeXlocal_1_2X_unfoldX_optionXonMatch_selectXmX_tailXlocalX_unfoldX_to_match", new List, ITraversal>> {(g,p) =>g.AddV((string) "person").Property("name", "marko").Property("age", 29), (g,p) =>g.Inject(new Dictionary {{ T.Label, "person" }, { "name", "marko" }}, new Dictionary {{ T.Label, "person" }, { "name", "marko" }}, new Dictionary {{ "created", "N" }}).Fold().As("m").MergeV((ITraversal) __.Select("m").Limit(Scope.Local, 1).Unfold()).Option(Merge.OnCreate, (ITraversal) __.Select("m").Range(Scope.Local, 1, 2).Unfold()).Option(Merge.OnMatch, (ITraversal) __.Select("m").Tail(Scope.Local).Unfold()), (g,p) =>g.V().Has("person", "name", "marko").Has("created", "N"), (g,p) =>g.V()}}, {"g_injectXlist1_list2_list3X_fold_asXmX_mergeVXselectXmX_limitXlocal_1X_unfoldX_optionXonCreate_selectXmX_rangeXlocal_1_2X_unfoldX_optionXonMatch_selectXmX_tailXlocalX_unfoldX_to_create", new List, ITraversal>> {(g,p) =>g.AddV((string) "person").Property("name", "marko").Property("age", 29), (g,p) =>g.Inject(new Dictionary {{ T.Label, "person" }, { "name", "stephen" }}, new Dictionary {{ T.Label, "person" }, { "name", "stephen" }}, new Dictionary {{ "created", "N" }}).Fold().As("m").MergeV((ITraversal) __.Select("m").Limit(Scope.Local, 1).Unfold()).Option(Merge.OnCreate, (ITraversal) __.Select("m").Range(Scope.Local, 1, 2).Unfold()).Option(Merge.OnMatch, (ITraversal) __.Select("m").Tail(Scope.Local).Unfold()), (g,p) =>g.V().Has("person", "name", "stephen").HasNot("created"), (g,p) =>g.V()}}, + {"g_mergeVXlabel_ab_name_markoX_multilabel_create", new List, ITraversal>> {(g,p) =>g.MergeV((IDictionary) new Dictionary {{ T.Label, new List { "person", "employee" } }, { "name", "marko" }}), (g,p) =>g.V(), (g,p) =>g.V().HasLabel("person").HasLabel("employee"), (g,p) =>g.V().Has("name", "marko")}}, + {"g_mergeVXlabel_abc_name_testX_multilabel_create", new List, ITraversal>> {(g,p) =>g.MergeV((IDictionary) new Dictionary {{ T.Label, new List { "a", "b", "c" } }, { "name", "test" }}), (g,p) =>g.V(), (g,p) =>g.V().HasLabel("a").HasLabel("b").HasLabel("c")}}, + {"g_mergeVXlabel_ab_name_markoX_multilabel_match", new List, ITraversal>> {(g,p) =>g.AddV((string) "person").AddLabel("employee").Property("name", "marko"), (g,p) =>g.MergeV((IDictionary) new Dictionary {{ T.Label, new List { "person", "employee" } }, { "name", "marko" }}), (g,p) =>g.V(), (g,p) =>g.V().HasLabel("person").HasLabel("employee")}}, + {"g_mergeVXlabel_ab_name_markoX_multilabel_nomatch", new List, ITraversal>> {(g,p) =>g.AddV((string) "person").Property("name", "marko"), (g,p) =>g.MergeV((IDictionary) new Dictionary {{ T.Label, new List { "person", "employee" } }, { "name", "marko" }}), (g,p) =>g.V()}}, + {"g_mergeVXlabel_person_name_markoX_optionXonMatch_label_managerX", new List, ITraversal>> {(g,p) =>g.AddV((string) "person").AddLabel("employee").Property("name", "marko"), (g,p) =>g.MergeV((IDictionary) new Dictionary {{ T.Label, "person" }, { "name", "marko" }}).Option(Merge.OnMatch, (IDictionary) new Dictionary {{ T.Label, "manager" }}), (g,p) =>g.V(), (g,p) =>g.V().HasLabel("manager"), (g,p) =>g.V().HasLabel("person"), (g,p) =>g.V().HasLabel("employee")}}, + {"g_mergeVXlabel_person_name_markoX_optionXonMatch_label_manager_directorX", new List, ITraversal>> {(g,p) =>g.AddV((string) "person").Property("name", "marko"), (g,p) =>g.MergeV((IDictionary) new Dictionary {{ T.Label, "person" }, { "name", "marko" }}).Option(Merge.OnMatch, (IDictionary) new Dictionary {{ T.Label, new List { "manager", "director" } }}), (g,p) =>g.V(), (g,p) =>g.V().HasLabel("manager").HasLabel("director"), (g,p) =>g.V().HasLabel("person")}}, + {"g_mergeVXlabel_person_name_markoX_optionXonMatch_label_emptyX", new List, ITraversal>> {(g,p) =>g.AddV((string) "person").Property("name", "marko"), (g,p) =>g.MergeV((IDictionary) new Dictionary {{ T.Label, "person" }, { "name", "marko" }}).Option(Merge.OnMatch, (IDictionary) new Dictionary {{ T.Label, new List { } }}), (g,p) =>g.V(), (g,p) =>g.V().HasLabel("person")}}, + {"g_mergeVXname_markoX_optionXonCreate_label_ab_name_markoX", new List, ITraversal>> {(g,p) =>g.MergeV((IDictionary) new Dictionary {{ "name", "marko" }}).Option(Merge.OnCreate, (IDictionary) new Dictionary {{ T.Label, new List { "person", "employee" } }, { "name", "marko" }}), (g,p) =>g.V(), (g,p) =>g.V().HasLabel("person").HasLabel("employee"), (g,p) =>g.V().Has("name", "marko")}}, + {"g_mergeVXlabel_person_name_markoX_optionXonMatch_label_existingX", new List, ITraversal>> {(g,p) =>g.AddV((string) "person").AddLabel("employee").Property("name", "marko"), (g,p) =>g.MergeV((IDictionary) new Dictionary {{ T.Label, "person" }, { "name", "marko" }}).Option(Merge.OnMatch, (IDictionary) new Dictionary {{ T.Label, "person" }}), (g,p) =>g.V(), (g,p) =>g.V().HasLabel("person").HasLabel("employee")}}, {"g_V_age_min", new List, ITraversal>> {(g,p) =>g.V().Values("age").Min()}}, {"g_V_foo_min", new List, ITraversal>> {(g,p) =>g.V().Values("foo").Min()}}, {"g_V_name_min", new List, ITraversal>> {(g,p) =>g.V().Values("name").Min()}}, @@ -1805,19 +1826,24 @@ private static IDictionary, ITraversal>> {(g,p) =>g.V().ValueMap().Unfold().Map(__.Select(Column.Keys))}}, {"g_VX1X_repeatXboth_simplePathX_untilXhasIdX6XX_path_byXnameX_unfold", new List, ITraversal>> {(g,p) =>g.V(p["vid1"]).Repeat(__.Both().SimplePath()).Until(__.HasId(p["vid6"])).Path().By("name").Unfold()}}, {"g_V_valueMap", new List, ITraversal>> {(g,p) =>g.V().ValueMap()}}, - {"g_V_valueMapXtrueX", new List, ITraversal>> {(g,p) =>g.V().ValueMap(true)}}, - {"g_V_valueMap_withXtokensX", new List, ITraversal>> {(g,p) =>g.V().ValueMap().With(WithOptions.Tokens)}}, + {"g_V_valueMapXtrueX", new List, ITraversal>> {(g,p) =>g.With("singlelabel").V().ValueMap(true)}}, + {"g_V_valueMap_withXtokensX", new List, ITraversal>> {(g,p) =>g.With("singlelabel").V().ValueMap().With(WithOptions.Tokens)}}, {"g_V_valueMapXname_ageX", new List, ITraversal>> {(g,p) =>g.V().ValueMap("name", "age")}}, - {"g_V_valueMapXtrue_name_ageX", new List, ITraversal>> {(g,p) =>g.V().ValueMap(true, "name", "age")}}, - {"g_V_valueMapXname_ageX_withXtokensX", new List, ITraversal>> {(g,p) =>g.V().ValueMap("name", "age").With(WithOptions.Tokens)}}, - {"g_V_valueMapXname_ageX_withXtokens_labelsX_byXunfoldX", new List, ITraversal>> {(g,p) =>g.V().ValueMap("name", "age").With(WithOptions.Tokens, WithOptions.Labels).By(__.Unfold())}}, + {"g_V_valueMapXtrue_name_ageX", new List, ITraversal>> {(g,p) =>g.With("singlelabel").V().ValueMap(true, "name", "age")}}, + {"g_V_valueMapXname_ageX_withXtokensX", new List, ITraversal>> {(g,p) =>g.With("singlelabel").V().ValueMap("name", "age").With(WithOptions.Tokens)}}, + {"g_V_valueMapXname_ageX_withXtokens_labelsX_byXunfoldX", new List, ITraversal>> {(g,p) =>g.With("singlelabel").V().ValueMap("name", "age").With(WithOptions.Tokens, WithOptions.Labels).By(__.Unfold())}}, {"g_V_valueMapXname_ageX_withXtokens_idsX_byXunfoldX", new List, ITraversal>> {(g,p) =>g.V().ValueMap("name", "age").With(WithOptions.Tokens, WithOptions.Ids).By(__.Unfold())}}, {"g_VX1X_outXcreatedX_valueMap", new List, ITraversal>> {(g,p) =>g.V(p["vid1"]).Out("created").ValueMap()}}, - {"g_V_hasLabelXpersonX_filterXoutEXcreatedXX_valueMapXtrueX", new List, ITraversal>> {(g,p) =>g.V().HasLabel("person").Filter(__.OutE("created")).ValueMap(true)}}, - {"g_V_hasLabelXpersonX_filterXoutEXcreatedXX_valueMap_withXtokensX", new List, ITraversal>> {(g,p) =>g.V().HasLabel("person").Filter(__.OutE("created")).ValueMap().With(WithOptions.Tokens)}}, + {"g_V_hasLabelXpersonX_filterXoutEXcreatedXX_valueMapXtrueX", new List, ITraversal>> {(g,p) =>g.With("singlelabel").V().HasLabel("person").Filter(__.OutE("created")).ValueMap(true)}}, + {"g_V_hasLabelXpersonX_filterXoutEXcreatedXX_valueMap_withXtokensX", new List, ITraversal>> {(g,p) =>g.With("singlelabel").V().HasLabel("person").Filter(__.OutE("created")).ValueMap().With(WithOptions.Tokens)}}, {"g_VX1X_valueMapXname_locationX_byXunfoldX_by", new List, ITraversal>> {(g,p) =>g.V(p["vid1"]).ValueMap("name", "location").By(__.Unfold()).By()}}, {"g_V_valueMapXname_age_nullX", new List, ITraversal>> {(g,p) =>g.V().ValueMap("name", "age", null)}}, {"g_V_valueMapXname_ageX_byXisXxXXbyXunfoldX", new List, ITraversal>> {(g,p) =>g.V().ValueMap("name", "age").By(__.Is("x")).By(__.Unfold())}}, + {"g_withXmultilabelX_VXmarkoX_valueMap_withXtokensX", new List, ITraversal>> {(g,p) =>g.With("multilabel").V().Has("name", "marko").ValueMap().With(WithOptions.Tokens)}}, + {"g_V_valueMap_withXtokensX_single_label_default", new List, ITraversal>> {(g,p) =>g.AddV((string) "person").AddLabel("employee").Property("name", "marko"), (g,p) =>g.V().ValueMap().With(WithOptions.Tokens)}}, + {"g_withXmultilabelX_V_valueMap_withXtokensX_multilabel", new List, ITraversal>> {(g,p) =>g.AddV((string) "person").AddLabel("employee").Property("name", "marko"), (g,p) =>g.With("multilabel").V().ValueMap().With(WithOptions.Tokens)}}, + {"g_V_valueMap_withXtokensX_multi_label_default", new List, ITraversal>> {(g,p) =>g.AddV((string) "person").AddLabel("employee").Property("name", "marko"), (g,p) =>g.V().ValueMap().With(WithOptions.Tokens)}}, + {"g_withXsinglelabelX_V_valueMap_withXtokensX_multi_label_default", new List, ITraversal>> {(g,p) =>g.AddV((string) "person").AddLabel("employee").Property("name", "marko"), (g,p) =>g.With("singlelabel").V().ValueMap().With(WithOptions.Tokens)}}, {"g_VXnullX", new List, ITraversal>> {(g,p) =>g.V(null)}}, {"g_VXlistXnullXX", new List, ITraversal>> {(g,p) =>g.V(p["xx1"])}}, {"g_VX1_nullX", new List, ITraversal>> {(g,p) =>g.V(p["vid1"], null)}}, @@ -2001,6 +2027,10 @@ private static IDictionary, ITraversal>> {(g,p) =>g.V().Out().Out().Properties().As("head").Path().Order().By(Order.Desc).Select("head").Value()}}, {"g_V_out_out_values_asXheadX_path_order_byXascX_selectXheadX", new List, ITraversal>> {(g,p) =>g.V().Out().Out().Values().As("head").Path().Order().By(Order.Asc).Select("head")}}, {"g_V_out_out_values_asXheadX_path_order_byXdescX_selectXheadX", new List, ITraversal>> {(g,p) =>g.V().Out().Out().Values().As("head").Path().Order().By(Order.Desc).Select("head")}}, + {"g_V_hasLabelXpersonX_hasXname_markoX_addLabelXemployeeX_labels", new List, ITraversal>> {(g,p) =>g.AddV((string) "person").Property("name", "marko"), (g,p) =>g.V().HasLabel("person").Has("name", "marko").AddLabel("employee").Labels().Fold(), (g,p) =>g.V().HasLabel("person").HasLabel("employee")}}, + {"g_V_addLabelXa_bX_labels_count", new List, ITraversal>> {(g,p) =>g.AddV((string) "person"), (g,p) =>g.V().AddLabel("a", "b").Labels().Count()}}, + {"g_V_addLabelXexistingX_labels_count", new List, ITraversal>> {(g,p) =>g.AddV((string) "person"), (g,p) =>g.V().AddLabel("person").Labels().Count()}}, + {"g_E_addLabelXfriendX_labels_fold", new List, ITraversal>> {(g,p) =>g.AddV((string) "person").Property("name", "marko").As("a").AddV((string) "person").Property("name", "josh").As("b").AddE((string) "knows").From("a").To("b"), (g,p) =>g.E().AddLabel("friend").Labels().Fold()}}, {"g_V_valueXnameX_aggregateXxX_capXxX", new List, ITraversal>> {(g,p) =>g.V().Values("name").Aggregate("x").Cap("x")}}, {"g_V_aggregateXxX_byXnameX_capXxX", new List, ITraversal>> {(g,p) =>g.V().Aggregate("x").By("name").Cap("x")}}, {"g_V_out_aggregateXaX_path", new List, ITraversal>> {(g,p) =>g.V().Out().Aggregate("a").Path()}}, @@ -2058,6 +2088,13 @@ private static IDictionary, ITraversal>> {(g,p) =>g.V().Repeat(__.Aggregate("a")).Times(2).Cap("a").Unfold()}}, {"g_V_aggregateXaX_capXaX_unfold_both", new List, ITraversal>> {(g,p) =>g.V().Aggregate("a").Cap("a").Unfold().Both()}}, {"g_V_aggregateXaX_capXaX_unfold_barrier_both", new List, ITraversal>> {(g,p) =>g.V().Aggregate("a").Cap("a").Unfold().Barrier().Both()}}, + {"g_V_dropLabelXaX_labels", new List, ITraversal>> {(g,p) =>g.AddV((string) "a", (string) "b"), (g,p) =>g.V().DropLabel("a").Labels().Fold(), (g,p) =>g.V().HasLabel("a"), (g,p) =>g.V().HasLabel("b")}}, + {"g_V_dropLabels_labels", new List, ITraversal>> {(g,p) =>g.AddV((string) "a", (string) "b"), (g,p) =>g.V().DropLabels().Labels()}}, + {"g_V_dropLabelXa_bX_labels", new List, ITraversal>> {(g,p) =>g.AddV((string) "a", (string) "b", (string) "c"), (g,p) =>g.V().DropLabel("a", "b").Labels()}}, + {"g_V_dropLabels_defaultLabel", new List, ITraversal>> {(g,p) =>g.AddV((string) "person"), (g,p) =>g.V().DropLabels().Labels()}}, + {"g_E_dropLabelXknowsX_labels", new List, ITraversal>> {(g,p) =>g.AddV((string) "person").As("a").AddV((string) "person").As("b").AddE((string) "knows").From("a").To("b"), (g,p) =>g.E().DropLabel("knows").Labels().Fold()}}, + {"g_E_dropLabels_labels", new List, ITraversal>> {(g,p) =>g.AddV((string) "person").As("a").AddV((string) "person").As("b").AddE((string) "knows").From("a").To("b"), (g,p) =>g.E().DropLabels().Labels()}}, + {"g_V_dropLabelXnonExistentX_labels", new List, ITraversal>> {(g,p) =>g.AddV((string) "a", (string) "b"), (g,p) =>g.V().DropLabel("xyz").Labels()}}, {"g_V_fail", new List, ITraversal>> {(g,p) =>g.V().Fail()}}, {"g_V_failXmsgX", new List, ITraversal>> {(g,p) =>g.V().Fail("msg")}}, {"g_V_unionXout_failX", new List, ITraversal>> {(g,p) =>g.V().Union(__.Out(), __.Fail())}}, diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/ScenarioData.cs b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/ScenarioData.cs index 708b3db978b..b337594415b 100644 --- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/ScenarioData.cs +++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/ScenarioData.cs @@ -74,6 +74,12 @@ public void CleanEmptyData() g.V().Drop().Iterate(); } + public void CleanMultilabelData() + { + var g = Traversal().With(GetByGraphName("multilabel").Connection); + g.V().Drop().Iterate(); + } + public void ReloadEmptyData() { var graphData = _dataPerGraph["empty"]; @@ -83,6 +89,17 @@ public void ReloadEmptyData() graphData.VertexProperties = GetVertexProperties(g); } + public void ReloadMultilabelData() + { + var graphData = _dataPerGraph["multilabel"]; + var g = Traversal().With(graphData.Connection); + graphData.Vertices = GetVertices(g); + graphData.Edges = GetEdges(g); + graphData.VertexProperties = GetVertexProperties(g); + } + + + private readonly IDictionary _dataPerGraph; public ScenarioData(IMessageSerializer messageSerializer) @@ -92,6 +109,9 @@ public ScenarioData(IMessageSerializer messageSerializer) var empty = new ScenarioDataPerGraph("empty", _connectionFactory.CreateRemoteConnection("ggraph"), new Dictionary(0), new Dictionary(), new Dictionary()); _dataPerGraph.Add("empty", empty); + var multilabel = new ScenarioDataPerGraph("multilabel", _connectionFactory.CreateRemoteConnection("gmultilabel"), + new Dictionary(0), new Dictionary(), new Dictionary()); + _dataPerGraph.Add("multilabel", multilabel); } private Dictionary LoadDataPerGraph() diff --git a/gremlin-go/docker-compose.yml b/gremlin-go/docker-compose.yml index 5df15618269..d1a0d874b59 100644 --- a/gremlin-go/docker-compose.yml +++ b/gremlin-go/docker-compose.yml @@ -72,6 +72,7 @@ services: working_dir: /go_app command: > bash -c "go install github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt@latest + && go build ./... && go test -v -json ./... -race -covermode=atomic -coverprofile=\"coverage.out\" -coverpkg=./... | gotestfmt && echo 'Running examples...' && go run examples/basic_gremlin.go diff --git a/gremlin-go/driver/anonymousTraversal.go b/gremlin-go/driver/anonymousTraversal.go index 8a16eb91b88..8eb7c90db67 100644 --- a/gremlin-go/driver/anonymousTraversal.go +++ b/gremlin-go/driver/anonymousTraversal.go @@ -63,6 +63,8 @@ type AnonymousTraversal interface { V(args ...interface{}) *GraphTraversal // AddE adds the addE step to the GraphTraversal. AddE(args ...interface{}) *GraphTraversal + // AddLabel adds the addLabel step to the GraphTraversal. + AddLabel(args ...interface{}) *GraphTraversal // AddV adds the addV step to the GraphTraversal. AddV(args ...interface{}) *GraphTraversal // Aggregate adds the aggregate step to the GraphTraversal. @@ -133,6 +135,10 @@ type AnonymousTraversal interface { Disjunct(args ...interface{}) *GraphTraversal // Drop adds the drop step to the GraphTraversal. Drop(args ...interface{}) *GraphTraversal + // DropLabel adds the dropLabel step to the GraphTraversal. + DropLabel(args ...interface{}) *GraphTraversal + // DropLabels adds the dropLabels step to the GraphTraversal. + DropLabels(args ...interface{}) *GraphTraversal // Element adds the element step to the GraphTraversal. Element(args ...interface{}) *GraphTraversal // ElementMap adds the elementMap step to the GraphTraversal. @@ -189,6 +195,8 @@ type AnonymousTraversal interface { Key(args ...interface{}) *GraphTraversal // Label adds the label step to the GraphTraversal. Label(args ...interface{}) *GraphTraversal + // Labels adds the labels step to the GraphTraversal. + Labels(args ...interface{}) *GraphTraversal // Length adds the length step to the GraphTraversal. Length(args ...interface{}) *GraphTraversal // Limit adds the limit step to the GraphTraversal. @@ -361,6 +369,11 @@ func (anonymousTraversal *anonymousTraversal) AddE(args ...interface{}) *GraphTr return anonymousTraversal.graphTraversal().AddE(args...) } +// AddLabel adds the addLabel step to the GraphTraversal. +func (anonymousTraversal *anonymousTraversal) AddLabel(args ...interface{}) *GraphTraversal { + return anonymousTraversal.graphTraversal().AddLabel(args...) +} + // AddV adds the addV step to the GraphTraversal. func (anonymousTraversal *anonymousTraversal) AddV(args ...interface{}) *GraphTraversal { return anonymousTraversal.graphTraversal().AddV(args...) @@ -536,6 +549,16 @@ func (anonymousTraversal *anonymousTraversal) Drop(args ...interface{}) *GraphTr return anonymousTraversal.graphTraversal().Drop(args...) } +// DropLabel adds the dropLabel step to the GraphTraversal. +func (anonymousTraversal *anonymousTraversal) DropLabel(args ...interface{}) *GraphTraversal { + return anonymousTraversal.graphTraversal().DropLabel(args...) +} + +// DropLabels adds the dropLabels step to the GraphTraversal. +func (anonymousTraversal *anonymousTraversal) DropLabels(args ...interface{}) *GraphTraversal { + return anonymousTraversal.graphTraversal().DropLabels(args...) +} + // Element adds the element step to the GraphTraversal. func (anonymousTraversal *anonymousTraversal) Element(args ...interface{}) *GraphTraversal { return anonymousTraversal.graphTraversal().Element(args...) @@ -676,6 +699,11 @@ func (anonymousTraversal *anonymousTraversal) Label(args ...interface{}) *GraphT return anonymousTraversal.graphTraversal().Label(args...) } +// Labels adds the labels step to the GraphTraversal. +func (anonymousTraversal *anonymousTraversal) Labels(args ...interface{}) *GraphTraversal { + return anonymousTraversal.graphTraversal().Labels(args...) +} + // Length adds the length step to the GraphTraversal. func (anonymousTraversal *anonymousTraversal) Length(args ...interface{}) *GraphTraversal { return anonymousTraversal.graphTraversal().Length(args...) diff --git a/gremlin-go/driver/cucumber/cucumberSteps_test.go b/gremlin-go/driver/cucumber/cucumberSteps_test.go index 149bc9bc07a..36d7671b073 100644 --- a/gremlin-go/driver/cucumber/cucumberSteps_test.go +++ b/gremlin-go/driver/cucumber/cucumberSteps_test.go @@ -501,10 +501,31 @@ func (tg *tinkerPopGraph) nothingShouldHappenBecause(arg1 *godog.DocString) erro // Choose the graph. func (tg *tinkerPopGraph) chooseGraph(graphName string) error { - tg.graphName = graphName - data := tg.graphDataMap[graphName] + // Multi-label tests use the gmultilabel traversal source for empty graphs + isMultiLabel := false + isMultiLabelDefault := false + for _, tag := range tg.scenario.Tags { + if tag.Name == "@MultiLabel" { + isMultiLabel = true + } + if tag.Name == "@MultiLabelDefault" { + isMultiLabelDefault = true + } + } + + if isMultiLabelDefault && graphName == "empty" { + tg.graphName = "multilabel" + } else if isMultiLabel && graphName == "empty" { + tg.graphName = "multilabel" + } else { + tg.graphName = graphName + } + data := tg.graphDataMap[tg.graphName] tg.g = gremlingo.Traversal_().With(data.connection).With("language", "gremlin-lang") - if graphName == "empty" { + if isMultiLabelDefault { + tg.g = tg.g.With("multilabel", true) + } + if tg.graphName == "empty" || tg.graphName == "multilabel" { err := tg.cleanEmptyDataGraph(tg.g) if err != nil { return err @@ -528,7 +549,17 @@ func (tg *tinkerPopGraph) theGraphInitializerOf(arg1 *godog.DocString) error { return err } future := traversal.Iterate() - return <-future + err = <-future + if err != nil { + return err + } + // Reload vertex/edge data for dynamic graphs after initializer adds data + if tg.graphName == "empty" { + tg.reloadEmptyData() + } else if tg.graphName == "multilabel" { + tg.reloadMultilabelData() + } + return nil } func (tg *tinkerPopGraph) theResultShouldHaveACountOf(expectedCount int) error { @@ -994,6 +1025,8 @@ func (tg *tinkerPopGraph) theTraversalOf(arg1 *godog.DocString) error { func (tg *tinkerPopGraph) usingTheParameterDefined(name string, params string) error { if tg.graphName == "empty" { tg.reloadEmptyData() + } else if tg.graphName == "multilabel" { + tg.reloadMultilabelData() } tg.parameters[name] = parseValue(strings.Replace(params, "\\\"", "\"", -1), tg.graphName) return nil @@ -1014,6 +1047,8 @@ func (tg *tinkerPopGraph) usingTheParameterOfP(paramName, pVal, stringVal string func (tg *tinkerPopGraph) usingTheSideEffectDefined(key string, value string) error { if tg.graphName == "empty" { tg.reloadEmptyData() + } else if tg.graphName == "multilabel" { + tg.reloadMultilabelData() } tg.sideEffects[key] = parseValue(strings.Replace(value, "\\\"", "\"", -1), tg.graphName) return nil diff --git a/gremlin-go/driver/cucumber/cucumberWorld.go b/gremlin-go/driver/cucumber/cucumberWorld.go index 206094522f3..ecc50d34c5d 100644 --- a/gremlin-go/driver/cucumber/cucumberWorld.go +++ b/gremlin-go/driver/cucumber/cucumberWorld.go @@ -87,7 +87,7 @@ func NewCucumberWorld() *CucumberWorld { } } -var graphNames = []string{"modern", "classic", "crew", "grateful", "sink", "empty"} +var graphNames = []string{"modern", "classic", "crew", "grateful", "sink", "empty", "multilabel"} func (t *CucumberWorld) getDataGraphFromMap(name string) *DataGraph { if val, ok := t.graphDataMap[name]; ok { @@ -101,6 +101,8 @@ func (t *CucumberWorld) loadAllDataGraph() { for _, name := range graphNames { if name == "empty" { t.loadEmptyDataGraph() + } else if name == "multilabel" { + t.loadMultilabelDataGraph() } else { connection, err := gremlingo.NewDriverRemoteConnection(scenarioUrl(), func(settings *gremlingo.DriverRemoteConnectionSettings) { @@ -128,6 +130,13 @@ func (t *CucumberWorld) loadEmptyDataGraph() { t.graphDataMap["empty"] = &DataGraph{connection: connection} } +func (t *CucumberWorld) loadMultilabelDataGraph() { + connection, _ := gremlingo.NewDriverRemoteConnection(scenarioUrl(), func(settings *gremlingo.DriverRemoteConnectionSettings) { + settings.TraversalSource = "gmultilabel" + }) + t.graphDataMap["multilabel"] = &DataGraph{connection: connection} +} + func (t *CucumberWorld) reloadEmptyData() { graphData := t.getDataGraphFromMap("empty") g := gremlingo.Traversal_().With(graphData.connection).With("language", "gremlin-lang") @@ -135,6 +144,13 @@ func (t *CucumberWorld) reloadEmptyData() { graphData.edges = getEdges(g) } +func (t *CucumberWorld) reloadMultilabelData() { + graphData := t.getDataGraphFromMap("multilabel") + g := gremlingo.Traversal_().With(graphData.connection).With("language", "gremlin-lang") + graphData.vertices = getVertices(g) + graphData.edges = getEdges(g) +} + func (t *CucumberWorld) cleanEmptyDataGraph(g *gremlingo.GraphTraversalSource) error { future := g.V().Drop().Iterate() return <-future @@ -244,6 +260,10 @@ func (t *CucumberWorld) recreateAllDataGraphConnection() error { t.getDataGraphFromMap(name).connection, err = gremlingo.NewDriverRemoteConnection(scenarioUrl(), func(settings *gremlingo.DriverRemoteConnectionSettings) { settings.TraversalSource = "ggraph" }) + } else if name == "multilabel" { + t.getDataGraphFromMap(name).connection, err = gremlingo.NewDriverRemoteConnection(scenarioUrl(), func(settings *gremlingo.DriverRemoteConnectionSettings) { + settings.TraversalSource = "gmultilabel" + }) } else { t.getDataGraphFromMap(name).connection, err = gremlingo.NewDriverRemoteConnection(scenarioUrl(), func(settings *gremlingo.DriverRemoteConnectionSettings) { settings.TraversalSource = "g" + name diff --git a/gremlin-go/driver/cucumber/gremlin.go b/gremlin-go/driver/cucumber/gremlin.go index 343500fa13e..c3f8991766b 100644 --- a/gremlin-go/driver/cucumber/gremlin.go +++ b/gremlin-go/driver/cucumber/gremlin.go @@ -926,6 +926,8 @@ var translationMap = map[string][]func(g *gremlingo.GraphTraversalSource, p map[ "g_V_limitX3X_addVXsoftwareX_aggregateXa1X_byXlabelX_aggregateXa2X_byXlabelX_capXa1_a2X_selectXa_bX_byXunfoldX_foldX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("person").Property("name", "marko").Property("age", 29).As("marko").AddV("person").Property("name", "vadas").Property("age", 27).As("vadas").AddV("software").Property("name", "lop").Property("lang", "java").As("lop").AddV("person").Property("name", "josh").Property("age", 32).As("josh").AddV("software").Property("name", "ripple").Property("lang", "java").As("ripple").AddV("person").Property("name", "peter").Property("age", 35).As("peter").AddE("knows").From("marko").To("vadas").Property("weight", 0.5).AddE("knows").From("marko").To("josh").Property("weight", 1.0).AddE("created").From("marko").To("lop").Property("weight", 0.4).AddE("created").From("josh").To("ripple").Property("weight", 1.0).AddE("created").From("josh").To("lop").Property("weight", 0.4).AddE("created").From("peter").To("lop").Property("weight", 0.2)}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Limit(3).AddV("software").Aggregate("a1").By(gremlingo.T.Label).Aggregate("a2").By(gremlingo.T.Label).Cap("a1", "a2").Select("a1", "a2").By(gremlingo.T__.Unfold().Fold())}}, "g_addV_propertyXname_markoX_withXkey_valueX_valuesXname_keyX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV().Property("name", "marko").With("key", "value").Values("name", "key")}}, "g_addV_propertyXname_marko_since_2010X_withXkey_valueX_propertiesXnameX_valuesXsince_keyX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV().Property("name", "marko", "since", 2010).With("key", "value").Properties("name").Values("since", "key")}}, + "g_addVXa_bX_labels": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("a", "b").Labels().Fold()}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().HasLabel("a").HasLabel("b")}}, + "g_addVXa_b_cX_labels_count": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("a", "b", "c").Labels().Count()}}, "g_injectX1X_asBool": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(1).AsBool()}}, "g_injectX3_14X_asBool": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(3.14).AsBool()}}, "g_injectXneg_1X_asBool": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(-1).AsBool()}}, @@ -1166,10 +1168,15 @@ var translationMap = map[string][]func(g *gremlingo.GraphTraversalSource, p map[ "g_E_properties_element": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.E().Properties().Element()}}, "g_VXv7_properties_properties_element_element": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V(p["vid7"]).Properties().Properties().Element().Element().Limit(1)}}, "g_V_properties_properties_element_element": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V(p["vid7"]).Properties().Properties().Element().Element()}}, - "g_V_elementMap": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().ElementMap()}}, - "g_V_elementMapXname_ageX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().ElementMap("name", "age")}}, - "g_EX11X_elementMap": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.E(p["eid11"]).ElementMap()}}, - "g_V_elementMapXname_age_nullX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().ElementMap("name", "age", nil)}}, + "g_V_elementMap": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.With("singlelabel").V().ElementMap()}}, + "g_V_elementMapXname_ageX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.With("singlelabel").V().ElementMap("name", "age")}}, + "g_EX11X_elementMap": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.With("singlelabel").E(p["eid11"]).ElementMap()}}, + "g_V_elementMapXname_age_nullX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.With("singlelabel").V().ElementMap("name", "age", nil)}}, + "g_withXmultilabelX_VXmarkoX_elementMap": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.With("multilabel").V().Has("name", "marko").ElementMap()}}, + "g_V_elementMap_single_label_default": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("person").AddLabel("employee").Property("name", "marko")}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().ElementMap()}}, + "g_withXmultilabelX_V_elementMap_multilabel": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("person").AddLabel("employee").Property("name", "marko")}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.With("multilabel").V().ElementMap()}}, + "g_V_elementMap_multi_label_default": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("person").AddLabel("employee").Property("name", "marko")}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().ElementMap()}}, + "g_withXsinglelabelX_V_elementMap_multi_label_default": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("person").AddLabel("employee").Property("name", "marko")}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.With("singlelabel").V().ElementMap()}}, "g_V_asXaX_flatMapXselectXaXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().As("a").FlatMap(gremlingo.T__.Select("a"))}}, "g_V_valuesXnameX_flatMapXsplitXaX_unfoldX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Values("name").FlatMap(gremlingo.T__.Split("a").Unfold())}}, "g_V_flatMapXout_outX_path": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().FlatMap(gremlingo.T__.Out().Out()).Path()}}, @@ -1220,6 +1227,11 @@ var translationMap = map[string][]func(g *gremlingo.GraphTraversalSource, p map[ "g_injectXListX1_2XX_lTrimXlocalX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject([]interface{}{1, 2}).LTrim(gremlingo.Scope.Local)}}, "g_V_valuesXnameX_lTrim": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("person").Property("name", " marko ").Property("age", 29).As("marko").AddV("person").Property("name", " vadas ").Property("age", 27).As("vadas").AddV("software").Property("name", " lop").Property("lang", "java").As("lop").AddV("person").Property("name", "josh ").Property("age", 32).As("josh").AddV("software").Property("name", " ripple ").Property("lang", "java").As("ripple").AddV("person").Property("name", "peter").Property("age", 35).As("peter").AddE("knows").From("marko").To("vadas").Property("weight", 0.5).AddE("knows").From("marko").To("josh").Property("weight", 1.0).AddE("created").From("marko").To("lop").Property("weight", 0.4).AddE("created").From("josh").To("ripple").Property("weight", 1.0).AddE("created").From("josh").To("lop").Property("weight", 0.4).AddE("created").From("peter").To("lop").Property("weight", 0.2)}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Values("name").LTrim()}}, "g_V_valuesXnameX_order_fold_lTrimXlocalX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("person").Property("name", " marko ").Property("age", 29).As("marko").AddV("person").Property("name", " vadas ").Property("age", 27).As("vadas").AddV("software").Property("name", " lop").Property("lang", "java").As("lop").AddV("person").Property("name", "josh ").Property("age", 32).As("josh").AddV("software").Property("name", " ripple ").Property("lang", "java").As("ripple").AddV("person").Property("name", "peter").Property("age", 35).As("peter").AddE("knows").From("marko").To("vadas").Property("weight", 0.5).AddE("knows").From("marko").To("josh").Property("weight", 1.0).AddE("created").From("marko").To("lop").Property("weight", 0.4).AddE("created").From("josh").To("ripple").Property("weight", 1.0).AddE("created").From("josh").To("lop").Property("weight", 0.4).AddE("created").From("peter").To("lop").Property("weight", 0.2)}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Values("name").Order().Fold().LTrim(gremlingo.Scope.Local)}}, + "g_V_hasLabelXpersonX_labels": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().HasLabel("person").Labels()}}, + "g_V_labels_multilabel": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("a", "b")}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Labels()}}, + "g_addVXa_bX_labels_count": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("a", "b")}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Labels().Count()}}, + "g_addV_labels": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV()}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Labels()}}, + "g_E_labels": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.E().HasLabel("knows").Labels()}}, "g_injectXfeature_test_nullX_length": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject("feature", "test", nil).Length()}}, "g_injectXfeature_test_nullX_lengthXlocalX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject("feature", "test", nil).Length(gremlingo.Scope.Local)}}, "g_injectXListXa_bXX_length": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject([]interface{}{"a", "b"}).Length()}}, @@ -1486,6 +1498,15 @@ var translationMap = map[string][]func(g *gremlingo.GraphTraversalSource, p map[ "g_mergeV_hidden_label_key_onMatch_matched_prohibited": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.MergeV(map[interface{}]interface{}{ }).Option(gremlingo.Merge.OnMatch, p["xx1"])}}, "g_injectXlist1_list2_list3X_fold_asXmX_mergeVXselectXmX_limitXlocal_1X_unfoldX_optionXonCreate_selectXmX_rangeXlocal_1_2X_unfoldX_optionXonMatch_selectXmX_tailXlocalX_unfoldX_to_match": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("person").Property("name", "marko").Property("age", 29)}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(map[interface{}]interface{}{gremlingo.T.Label: "person", "name": "marko" }, map[interface{}]interface{}{gremlingo.T.Label: "person", "name": "marko" }, map[interface{}]interface{}{"created": "N" }).Fold().As("m").MergeV(gremlingo.T__.Select("m").Limit(gremlingo.Scope.Local, 1).Unfold()).Option(gremlingo.Merge.OnCreate, gremlingo.T__.Select("m").Range(gremlingo.Scope.Local, 1, 2).Unfold()).Option(gremlingo.Merge.OnMatch, gremlingo.T__.Select("m").Tail(gremlingo.Scope.Local).Unfold())}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Has("person", "name", "marko").Has("created", "N")}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V()}}, "g_injectXlist1_list2_list3X_fold_asXmX_mergeVXselectXmX_limitXlocal_1X_unfoldX_optionXonCreate_selectXmX_rangeXlocal_1_2X_unfoldX_optionXonMatch_selectXmX_tailXlocalX_unfoldX_to_create": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("person").Property("name", "marko").Property("age", 29)}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(map[interface{}]interface{}{gremlingo.T.Label: "person", "name": "stephen" }, map[interface{}]interface{}{gremlingo.T.Label: "person", "name": "stephen" }, map[interface{}]interface{}{"created": "N" }).Fold().As("m").MergeV(gremlingo.T__.Select("m").Limit(gremlingo.Scope.Local, 1).Unfold()).Option(gremlingo.Merge.OnCreate, gremlingo.T__.Select("m").Range(gremlingo.Scope.Local, 1, 2).Unfold()).Option(gremlingo.Merge.OnMatch, gremlingo.T__.Select("m").Tail(gremlingo.Scope.Local).Unfold())}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Has("person", "name", "stephen").HasNot("created")}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V()}}, + "g_mergeVXlabel_ab_name_markoX_multilabel_create": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.MergeV(map[interface{}]interface{}{gremlingo.T.Label: []interface{}{"person", "employee"}, "name": "marko" })}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V()}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().HasLabel("person").HasLabel("employee")}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Has("name", "marko")}}, + "g_mergeVXlabel_abc_name_testX_multilabel_create": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.MergeV(map[interface{}]interface{}{gremlingo.T.Label: []interface{}{"a", "b", "c"}, "name": "test" })}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V()}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().HasLabel("a").HasLabel("b").HasLabel("c")}}, + "g_mergeVXlabel_ab_name_markoX_multilabel_match": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("person").AddLabel("employee").Property("name", "marko")}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.MergeV(map[interface{}]interface{}{gremlingo.T.Label: []interface{}{"person", "employee"}, "name": "marko" })}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V()}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().HasLabel("person").HasLabel("employee")}}, + "g_mergeVXlabel_ab_name_markoX_multilabel_nomatch": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("person").Property("name", "marko")}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.MergeV(map[interface{}]interface{}{gremlingo.T.Label: []interface{}{"person", "employee"}, "name": "marko" })}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V()}}, + "g_mergeVXlabel_person_name_markoX_optionXonMatch_label_managerX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("person").AddLabel("employee").Property("name", "marko")}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.MergeV(map[interface{}]interface{}{gremlingo.T.Label: "person", "name": "marko" }).Option(gremlingo.Merge.OnMatch, map[interface{}]interface{}{gremlingo.T.Label: "manager" })}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V()}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().HasLabel("manager")}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().HasLabel("person")}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().HasLabel("employee")}}, + "g_mergeVXlabel_person_name_markoX_optionXonMatch_label_manager_directorX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("person").Property("name", "marko")}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.MergeV(map[interface{}]interface{}{gremlingo.T.Label: "person", "name": "marko" }).Option(gremlingo.Merge.OnMatch, map[interface{}]interface{}{gremlingo.T.Label: []interface{}{"manager", "director"} })}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V()}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().HasLabel("manager").HasLabel("director")}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().HasLabel("person")}}, + "g_mergeVXlabel_person_name_markoX_optionXonMatch_label_emptyX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("person").Property("name", "marko")}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.MergeV(map[interface{}]interface{}{gremlingo.T.Label: "person", "name": "marko" }).Option(gremlingo.Merge.OnMatch, map[interface{}]interface{}{gremlingo.T.Label: []interface{}{} })}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V()}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().HasLabel("person")}}, + "g_mergeVXname_markoX_optionXonCreate_label_ab_name_markoX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.MergeV(map[interface{}]interface{}{"name": "marko" }).Option(gremlingo.Merge.OnCreate, map[interface{}]interface{}{gremlingo.T.Label: []interface{}{"person", "employee"}, "name": "marko" })}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V()}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().HasLabel("person").HasLabel("employee")}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Has("name", "marko")}}, + "g_mergeVXlabel_person_name_markoX_optionXonMatch_label_existingX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("person").AddLabel("employee").Property("name", "marko")}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.MergeV(map[interface{}]interface{}{gremlingo.T.Label: "person", "name": "marko" }).Option(gremlingo.Merge.OnMatch, map[interface{}]interface{}{gremlingo.T.Label: "person" })}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V()}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().HasLabel("person").HasLabel("employee")}}, "g_V_age_min": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Values("age").Min()}}, "g_V_foo_min": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Values("foo").Min()}}, "g_V_name_min": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Values("name").Min()}}, @@ -1775,19 +1796,24 @@ var translationMap = map[string][]func(g *gremlingo.GraphTraversalSource, p map[ "g_V_valueMap_unfold_mapXselectXkeysXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().ValueMap().Unfold().Map(gremlingo.T__.Select(gremlingo.Column.Keys))}}, "g_VX1X_repeatXboth_simplePathX_untilXhasIdX6XX_path_byXnameX_unfold": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V(p["vid1"]).Repeat(gremlingo.T__.Both().SimplePath()).Until(gremlingo.T__.HasId(p["vid6"])).Path().By("name").Unfold()}}, "g_V_valueMap": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().ValueMap()}}, - "g_V_valueMapXtrueX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().ValueMap(true)}}, - "g_V_valueMap_withXtokensX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().ValueMap().With(gremlingo.WithOptions.Tokens)}}, + "g_V_valueMapXtrueX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.With("singlelabel").V().ValueMap(true)}}, + "g_V_valueMap_withXtokensX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.With("singlelabel").V().ValueMap().With(gremlingo.WithOptions.Tokens)}}, "g_V_valueMapXname_ageX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().ValueMap("name", "age")}}, - "g_V_valueMapXtrue_name_ageX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().ValueMap(true, "name", "age")}}, - "g_V_valueMapXname_ageX_withXtokensX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().ValueMap("name", "age").With(gremlingo.WithOptions.Tokens)}}, - "g_V_valueMapXname_ageX_withXtokens_labelsX_byXunfoldX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().ValueMap("name", "age").With(gremlingo.WithOptions.Tokens, gremlingo.WithOptions.Labels).By(gremlingo.T__.Unfold())}}, + "g_V_valueMapXtrue_name_ageX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.With("singlelabel").V().ValueMap(true, "name", "age")}}, + "g_V_valueMapXname_ageX_withXtokensX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.With("singlelabel").V().ValueMap("name", "age").With(gremlingo.WithOptions.Tokens)}}, + "g_V_valueMapXname_ageX_withXtokens_labelsX_byXunfoldX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.With("singlelabel").V().ValueMap("name", "age").With(gremlingo.WithOptions.Tokens, gremlingo.WithOptions.Labels).By(gremlingo.T__.Unfold())}}, "g_V_valueMapXname_ageX_withXtokens_idsX_byXunfoldX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().ValueMap("name", "age").With(gremlingo.WithOptions.Tokens, gremlingo.WithOptions.Ids).By(gremlingo.T__.Unfold())}}, "g_VX1X_outXcreatedX_valueMap": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V(p["vid1"]).Out("created").ValueMap()}}, - "g_V_hasLabelXpersonX_filterXoutEXcreatedXX_valueMapXtrueX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().HasLabel("person").Filter(gremlingo.T__.OutE("created")).ValueMap(true)}}, - "g_V_hasLabelXpersonX_filterXoutEXcreatedXX_valueMap_withXtokensX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().HasLabel("person").Filter(gremlingo.T__.OutE("created")).ValueMap().With(gremlingo.WithOptions.Tokens)}}, + "g_V_hasLabelXpersonX_filterXoutEXcreatedXX_valueMapXtrueX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.With("singlelabel").V().HasLabel("person").Filter(gremlingo.T__.OutE("created")).ValueMap(true)}}, + "g_V_hasLabelXpersonX_filterXoutEXcreatedXX_valueMap_withXtokensX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.With("singlelabel").V().HasLabel("person").Filter(gremlingo.T__.OutE("created")).ValueMap().With(gremlingo.WithOptions.Tokens)}}, "g_VX1X_valueMapXname_locationX_byXunfoldX_by": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V(p["vid1"]).ValueMap("name", "location").By(gremlingo.T__.Unfold()).By()}}, "g_V_valueMapXname_age_nullX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().ValueMap("name", "age", nil)}}, "g_V_valueMapXname_ageX_byXisXxXXbyXunfoldX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().ValueMap("name", "age").By(gremlingo.T__.Is("x")).By(gremlingo.T__.Unfold())}}, + "g_withXmultilabelX_VXmarkoX_valueMap_withXtokensX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.With("multilabel").V().Has("name", "marko").ValueMap().With(gremlingo.WithOptions.Tokens)}}, + "g_V_valueMap_withXtokensX_single_label_default": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("person").AddLabel("employee").Property("name", "marko")}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().ValueMap().With(gremlingo.WithOptions.Tokens)}}, + "g_withXmultilabelX_V_valueMap_withXtokensX_multilabel": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("person").AddLabel("employee").Property("name", "marko")}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.With("multilabel").V().ValueMap().With(gremlingo.WithOptions.Tokens)}}, + "g_V_valueMap_withXtokensX_multi_label_default": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("person").AddLabel("employee").Property("name", "marko")}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().ValueMap().With(gremlingo.WithOptions.Tokens)}}, + "g_withXsinglelabelX_V_valueMap_withXtokensX_multi_label_default": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("person").AddLabel("employee").Property("name", "marko")}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.With("singlelabel").V().ValueMap().With(gremlingo.WithOptions.Tokens)}}, "g_VXnullX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V(nil)}}, "g_VXlistXnullXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V(p["xx1"])}}, "g_VX1_nullX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V(p["vid1"], nil)}}, @@ -1971,6 +1997,10 @@ var translationMap = map[string][]func(g *gremlingo.GraphTraversalSource, p map[ "g_V_out_out_properties_asXheadX_path_order_byXdescX_selectXheadX_value": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Out().Out().Properties().As("head").Path().Order().By(gremlingo.Order.Desc).Select("head").Value()}}, "g_V_out_out_values_asXheadX_path_order_byXascX_selectXheadX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Out().Out().Values().As("head").Path().Order().By(gremlingo.Order.Asc).Select("head")}}, "g_V_out_out_values_asXheadX_path_order_byXdescX_selectXheadX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Out().Out().Values().As("head").Path().Order().By(gremlingo.Order.Desc).Select("head")}}, + "g_V_hasLabelXpersonX_hasXname_markoX_addLabelXemployeeX_labels": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("person").Property("name", "marko")}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().HasLabel("person").Has("name", "marko").AddLabel("employee").Labels().Fold()}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().HasLabel("person").HasLabel("employee")}}, + "g_V_addLabelXa_bX_labels_count": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("person")}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().AddLabel("a", "b").Labels().Count()}}, + "g_V_addLabelXexistingX_labels_count": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("person")}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().AddLabel("person").Labels().Count()}}, + "g_E_addLabelXfriendX_labels_fold": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("person").Property("name", "marko").As("a").AddV("person").Property("name", "josh").As("b").AddE("knows").From("a").To("b")}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.E().AddLabel("friend").Labels().Fold()}}, "g_V_valueXnameX_aggregateXxX_capXxX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Values("name").Aggregate("x").Cap("x")}}, "g_V_aggregateXxX_byXnameX_capXxX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Aggregate("x").By("name").Cap("x")}}, "g_V_out_aggregateXaX_path": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Out().Aggregate("a").Path()}}, @@ -2028,6 +2058,13 @@ var translationMap = map[string][]func(g *gremlingo.GraphTraversalSource, p map[ "g_V_repeatXaggregateXaXX_timesX2X_capXaX_unfold": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Repeat(gremlingo.T__.Aggregate("a")).Times(2).Cap("a").Unfold()}}, "g_V_aggregateXaX_capXaX_unfold_both": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Aggregate("a").Cap("a").Unfold().Both()}}, "g_V_aggregateXaX_capXaX_unfold_barrier_both": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Aggregate("a").Cap("a").Unfold().Barrier().Both()}}, + "g_V_dropLabelXaX_labels": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("a", "b")}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().DropLabel("a").Labels().Fold()}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().HasLabel("a")}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().HasLabel("b")}}, + "g_V_dropLabels_labels": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("a", "b")}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().DropLabels().Labels()}}, + "g_V_dropLabelXa_bX_labels": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("a", "b", "c")}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().DropLabel("a", "b").Labels()}}, + "g_V_dropLabels_defaultLabel": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("person")}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().DropLabels().Labels()}}, + "g_E_dropLabelXknowsX_labels": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("person").As("a").AddV("person").As("b").AddE("knows").From("a").To("b")}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.E().DropLabel("knows").Labels().Fold()}}, + "g_E_dropLabels_labels": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("person").As("a").AddV("person").As("b").AddE("knows").From("a").To("b")}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.E().DropLabels().Labels()}}, + "g_V_dropLabelXnonExistentX_labels": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("a", "b")}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().DropLabel("xyz").Labels()}}, "g_V_fail": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Fail()}}, "g_V_failXmsgX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Fail("msg")}}, "g_V_unionXout_failX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Union(gremlingo.T__.Out(), gremlingo.T__.Fail())}}, diff --git a/gremlin-go/driver/graph.go b/gremlin-go/driver/graph.go index 981a7fc2d30..1431b3f41bf 100644 --- a/gremlin-go/driver/graph.go +++ b/gremlin-go/driver/graph.go @@ -57,13 +57,15 @@ type Element struct { // Vertex contains a single Vertex which has a Label and an Id. type Vertex struct { Element + Labels []string // All labels; Element.Label still holds first label for compat } // Edge links two Vertex structs along with its Property Objects. An edge has both a direction and a Label. type Edge struct { Element - OutV Vertex - InV Vertex + OutV Vertex + InV Vertex + Labels []string // All labels } // VertexProperty is similar to property in that it denotes a key/value pair associated with a Vertex, but is different diff --git a/gremlin-go/driver/graphBinaryDeserializer.go b/gremlin-go/driver/graphBinaryDeserializer.go index 1d5b5bda09e..8b95e8e38d7 100644 --- a/gremlin-go/driver/graphBinaryDeserializer.go +++ b/gremlin-go/driver/graphBinaryDeserializer.go @@ -369,7 +369,13 @@ func (d *GraphBinaryDeserializer) readVertex(withProps bool) (*Vertex, error) { if !ok { return nil, newError(err0404ReadNullTypeError) } - v := &Vertex{Element: Element{Id: id, Label: label}} + allLabels := make([]string, 0, len(labelSlice)) + for _, l := range labelSlice { + if s, ok := l.(string); ok { + allLabels = append(allLabels, s) + } + } + v := &Vertex{Element: Element{Id: id, Label: label}, Labels: allLabels} if withProps { props, err := d.ReadFullyQualified() if err != nil { @@ -400,6 +406,12 @@ func (d *GraphBinaryDeserializer) readEdge() (*Edge, error) { if !ok { return nil, newError(err0404ReadNullTypeError) } + allLabels := make([]string, 0, len(labelSlice)) + for _, l := range labelSlice { + if s, ok := l.(string); ok { + allLabels = append(allLabels, s) + } + } inV, err := d.readVertex(false) if err != nil { return nil, err @@ -419,6 +431,7 @@ func (d *GraphBinaryDeserializer) readEdge() (*Edge, error) { Element: Element{Id: id, Label: label}, InV: *inV, OutV: *outV, + Labels: allLabels, } e.Properties = make([]interface{}, 0) if props != nil { diff --git a/gremlin-go/driver/graphBinarySerializer.go b/gremlin-go/driver/graphBinarySerializer.go index 5da2327d49a..c5010a89602 100644 --- a/gremlin-go/driver/graphBinarySerializer.go +++ b/gremlin-go/driver/graphBinarySerializer.go @@ -267,8 +267,12 @@ func vertexWriter(value interface{}, w io.Writer, typeSerializer *graphBinaryTyp return err } - // Not fully qualified. - if err := typeSerializer.writeValue([1]string{v.Label}, w, false); err != nil { + // Write all labels as a list. Use Labels slice if populated, otherwise fall back to single Label. + labels := v.Labels + if len(labels) == 0 { + labels = []string{v.Label} + } + if err := typeSerializer.writeValue(labels, w, false); err != nil { return err } // Note that as TinkerPop currently send "references" only, properties will always be null @@ -283,8 +287,12 @@ func edgeWriter(value interface{}, w io.Writer, typeSerializer *graphBinaryTypeS return err } - // Not fully qualified - if err := typeSerializer.writeValue([1]string{e.Label}, w, false); err != nil { + // Write all labels as a list. Use Labels slice if populated, otherwise fall back to single Label. + labels := e.Labels + if len(labels) == 0 { + labels = []string{e.Label} + } + if err := typeSerializer.writeValue(labels, w, false); err != nil { return err } @@ -293,8 +301,12 @@ func edgeWriter(value interface{}, w io.Writer, typeSerializer *graphBinaryTypeS return err } - // Not fully qualified. - if err := typeSerializer.writeValue([1]string{e.InV.Label}, w, false); err != nil { + // Write in-vertex labels + inVLabels := e.InV.Labels + if len(inVLabels) == 0 { + inVLabels = []string{e.InV.Label} + } + if err := typeSerializer.writeValue(inVLabels, w, false); err != nil { return err } // Write out-vertex @@ -302,8 +314,12 @@ func edgeWriter(value interface{}, w io.Writer, typeSerializer *graphBinaryTypeS return err } - // Not fully qualified. - if err := typeSerializer.writeValue([1]string{e.OutV.Label}, w, false); err != nil { + // Write out-vertex labels + outVLabels := e.OutV.Labels + if len(outVLabels) == 0 { + outVLabels = []string{e.OutV.Label} + } + if err := typeSerializer.writeValue(outVLabels, w, false); err != nil { return err } diff --git a/gremlin-go/driver/graphTraversal.go b/gremlin-go/driver/graphTraversal.go index 6a90dc6f7ff..22f12bcd097 100644 --- a/gremlin-go/driver/graphTraversal.go +++ b/gremlin-go/driver/graphTraversal.go @@ -77,6 +77,12 @@ func (g *GraphTraversal) AddE(args ...interface{}) *GraphTraversal { return g } +// AddLabel adds the addLabel step to the GraphTraversal. +func (g *GraphTraversal) AddLabel(args ...interface{}) *GraphTraversal { + g.GremlinLang.AddStep("addLabel", args...) + return g +} + // AddV adds the addV step to the GraphTraversal. func (g *GraphTraversal) AddV(args ...interface{}) *GraphTraversal { g.GremlinLang.AddStep("addV", args...) @@ -289,6 +295,18 @@ func (g *GraphTraversal) Drop(args ...interface{}) *GraphTraversal { return g } +// DropLabel adds the dropLabel step to the GraphTraversal. +func (g *GraphTraversal) DropLabel(args ...interface{}) *GraphTraversal { + g.GremlinLang.AddStep("dropLabel", args...) + return g +} + +// DropLabels adds the dropLabels step to the GraphTraversal. +func (g *GraphTraversal) DropLabels(args ...interface{}) *GraphTraversal { + g.GremlinLang.AddStep("dropLabels", args...) + return g +} + // Element adds the element step to the GraphTraversal. func (g *GraphTraversal) Element(args ...interface{}) *GraphTraversal { g.GremlinLang.AddStep("element", args...) @@ -464,6 +482,12 @@ func (g *GraphTraversal) Label(args ...interface{}) *GraphTraversal { return g } +// Labels adds the labels step to the GraphTraversal. +func (g *GraphTraversal) Labels(args ...interface{}) *GraphTraversal { + g.GremlinLang.AddStep("labels", args...) + return g +} + // Length adds the length step to the GraphTraversal. func (g *GraphTraversal) Length(args ...interface{}) *GraphTraversal { g.GremlinLang.AddStep("length", args...) diff --git a/gremlin-go/driver/graphTraversalSource.go b/gremlin-go/driver/graphTraversalSource.go index f75ea7c3820..4f86b2c22ed 100644 --- a/gremlin-go/driver/graphTraversalSource.go +++ b/gremlin-go/driver/graphTraversalSource.go @@ -126,9 +126,14 @@ func (gts *GraphTraversalSource) WithoutStrategies(args ...TraversalStrategy) *G } // With provides a configuration to a traversal in the form of a key value pair. -func (gts *GraphTraversalSource) With(key interface{}, value interface{}) *GraphTraversalSource { +func (gts *GraphTraversalSource) With(key interface{}, value ...interface{}) *GraphTraversalSource { source := gts.clone() + val := interface{}(true) + if len(value) > 0 { + val = value[0] + } + //TODO verify remote when connection is set-up var optionsStrategy TraversalStrategy = nil if len(gts.gremlinLang.optionsStrategies) != 0 { @@ -136,12 +141,19 @@ func (gts *GraphTraversalSource) With(key interface{}, value interface{}) *Graph } if optionsStrategy == nil { - optionsStrategy = OptionsStrategy(map[string]interface{}{key.(string): value}) + optionsStrategy = OptionsStrategy(map[string]interface{}{key.(string): val}) return source.WithStrategies(optionsStrategy) } options := optionsStrategy.(*traversalStrategy) - options.configuration[key.(string)] = value + options.configuration[key.(string)] = val + + // Render multilabel/singlelabel in gremlin text (temporary until these options are removed) + k := key.(string) + if k == "multilabel" || k == "singlelabel" { + source.gremlinLang.gremlin = append(source.gremlinLang.gremlin, ".with(\""+k+"\")") + } + return source } diff --git a/gremlin-go/driver/graph_test.go b/gremlin-go/driver/graph_test.go index 0de4a1bac97..5599857d7ff 100644 --- a/gremlin-go/driver/graph_test.go +++ b/gremlin-go/driver/graph_test.go @@ -30,7 +30,7 @@ import ( func TestGraphStructureFunctions(t *testing.T) { t.Run("Test Vertex.String()", func(t *testing.T) { uid, _ := uuid.NewUUID() - v := Vertex{Element{uid, "Vertex-Label", nil}} + v := Vertex{Element: Element{uid, "Vertex-Label", nil}} assert.Equal(t, fmt.Sprintf("v[%s]", uid.String()), v.String()) }) @@ -38,14 +38,14 @@ func TestGraphStructureFunctions(t *testing.T) { uidEdge, _ := uuid.NewUUID() uidIn, _ := uuid.NewUUID() uidOut, _ := uuid.NewUUID() - v := Edge{Element{uidEdge, "edge_label", nil}, Vertex{Element{uidOut, "vertex_out", nil}}, Vertex{Element{uidIn, "vertex_in", nil}}} + v := Edge{Element: Element{uidEdge, "edge_label", nil}, OutV: Vertex{Element: Element{uidOut, "vertex_out", nil}}, InV: Vertex{Element: Element{uidIn, "vertex_in", nil}}} assert.Equal(t, fmt.Sprintf("e[%s][%s-edge_label->%s]", uidEdge.String(), uidOut.String(), uidIn.String()), v.String()) }) t.Run("Test VertexProperty.String()", func(t *testing.T) { uidVProp, _ := uuid.NewUUID() uidV, _ := uuid.NewUUID() - v := VertexProperty{Element{uidVProp, "Vertex-prop", nil}, "Vertex", []uint32{0, 1}, Vertex{Element{uidV, "Vertex", nil}}} + v := VertexProperty{Element{uidVProp, "Vertex-prop", nil}, "Vertex", []uint32{0, 1}, Vertex{Element: Element{uidV, "Vertex", nil}}} assert.Equal(t, "vp[Vertex-prop->[0 1]]", v.String()) }) diff --git a/gremlin-go/driver/gremlinlang.go b/gremlin-go/driver/gremlinlang.go index a08c6a7d31c..a69a7d9c513 100644 --- a/gremlin-go/driver/gremlinlang.go +++ b/gremlin-go/driver/gremlinlang.go @@ -554,6 +554,13 @@ func (gl *GremlinLang) buildStrategyArgs(args ...interface{}) string { // special handling for OptionsStrategy if strategy.name == "OptionsStrategy" { gl.optionsStrategies = append(gl.optionsStrategies, strategy) + // Render multilabel/singlelabel in gremlin text (temporary until these options are removed) + if _, ok := strategy.configuration["multilabel"]; ok { + gl.gremlin = append(gl.gremlin, ".with(\"multilabel\")") + } + if _, ok := strategy.configuration["singlelabel"]; ok { + gl.gremlin = append(gl.gremlin, ".with(\"singlelabel\")") + } continue } if len(strategy.configuration) == 0 { diff --git a/gremlin-js/gremlin-javascript/lib/process/graph-traversal.ts b/gremlin-js/gremlin-javascript/lib/process/graph-traversal.ts index e4df5070861..bfa9c277d72 100644 --- a/gremlin-js/gremlin-javascript/lib/process/graph-traversal.ts +++ b/gremlin-js/gremlin-javascript/lib/process/graph-traversal.ts @@ -207,6 +207,10 @@ export class GraphTraversalSource { } glStrategies[glStrategies.length - 1].configuration[key] = val; const gl = new GremlinLang(this.gremlinLang); + // Render multilabel/singlelabel in gremlin text (temporary until these options are removed) + if (key === 'multilabel' || key === 'singlelabel') { + gl.appendGremlin(`.with("${key}")`); + } return new this.graphTraversalSourceClass( this.graph, new TraversalStrategies(this.traversalStrategies), @@ -498,6 +502,16 @@ export class GraphTraversal extends Traversal { return this; } + /** + * Graph traversal addLabel method. + * @param {...Object} args + * @returns {GraphTraversal} + */ + addLabel(...args: any[]): this { + this.gremlinLang.addStep('addLabel', args); + return this; + } + /** * Graph traversal aggregate method. * @param {...Object} args @@ -837,6 +851,26 @@ export class GraphTraversal extends Traversal { return this; } + /** + * Graph traversal dropLabel method. + * @param {...Object} args + * @returns {GraphTraversal} + */ + dropLabel(...args: any[]): this { + this.gremlinLang.addStep('dropLabel', args); + return this; + } + + /** + * Graph traversal dropLabels method. + * @param {...Object} args + * @returns {GraphTraversal} + */ + dropLabels(...args: any[]): this { + this.gremlinLang.addStep('dropLabels', args); + return this; + } + /** * Graph traversal element method. * @param {...Object} args @@ -1121,6 +1155,16 @@ export class GraphTraversal extends Traversal { return this; } + /** + * Graph traversal labels method. + * @param {...Object} args + * @returns {GraphTraversal} + */ + labels(...args: any[]): this { + this.gremlinLang.addStep('labels', args); + return this; + } + /** * Graph traversal length method. * @param {...Object} args @@ -1893,6 +1937,7 @@ export const statics = { V: (...args: any[]) => callOnEmptyTraversal('V', args), addE: (...args: any[]) => callOnEmptyTraversal('addE', args), addV: (...args: any[]) => callOnEmptyTraversal('addV', args), + addLabel: (...args: any[]) => callOnEmptyTraversal('addLabel', args), aggregate: (...args: any[]) => callOnEmptyTraversal('aggregate', args), all: (...args: any[]) => callOnEmptyTraversal('all', args), and: (...args: any[]) => callOnEmptyTraversal('and', args), @@ -1925,6 +1970,8 @@ export const statics = { discard: (...args: any[]) => callOnEmptyTraversal('discard', args), disjunct: (...args: any[]) => callOnEmptyTraversal('disjunct', args), drop: (...args: any[]) => callOnEmptyTraversal('drop', args), + dropLabel: (...args: any[]) => callOnEmptyTraversal('dropLabel', args), + dropLabels: (...args: any[]) => callOnEmptyTraversal('dropLabels', args), element: (...args: any[]) => callOnEmptyTraversal('element', args), elementMap: (...args: any[]) => callOnEmptyTraversal('elementMap', args), emit: (...args: any[]) => callOnEmptyTraversal('emit', args), @@ -1952,6 +1999,7 @@ export const statics = { is: (...args: any[]) => callOnEmptyTraversal('is', args), key: (...args: any[]) => callOnEmptyTraversal('key', args), label: (...args: any[]) => callOnEmptyTraversal('label', args), + labels: (...args: any[]) => callOnEmptyTraversal('labels', args), length: (...args: any[]) => callOnEmptyTraversal('length', args), limit: (...args: any[]) => callOnEmptyTraversal('limit', args), local: (...args: any[]) => callOnEmptyTraversal('local', args), diff --git a/gremlin-js/gremlin-javascript/lib/process/gremlin-lang.ts b/gremlin-js/gremlin-javascript/lib/process/gremlin-lang.ts index 070a88fceb6..c194c6a2afa 100644 --- a/gremlin-js/gremlin-javascript/lib/process/gremlin-lang.ts +++ b/gremlin-js/gremlin-javascript/lib/process/gremlin-lang.ts @@ -43,6 +43,10 @@ export default class GremlinLang { return this.optionsStrategies; } + appendGremlin(text: string): void { + this.gremlin += text; + } + addG(g: string): void { this.parameters.set('g', g); } @@ -206,6 +210,13 @@ export default class GremlinLang { for (const strategy of args) { if (strategy instanceof OptionsStrategy) { this.optionsStrategies.push(strategy); + // Render multilabel/singlelabel in gremlin text (temporary until these options are removed) + if (strategy.configuration['multilabel'] !== undefined) { + this.gremlin += '.with("multilabel")'; + } + if (strategy.configuration['singlelabel'] !== undefined) { + this.gremlin += '.with("singlelabel")'; + } } else { nonOptionsStrategies.push(strategy); } diff --git a/gremlin-js/gremlin-javascript/lib/structure/graph.ts b/gremlin-js/gremlin-javascript/lib/structure/graph.ts index 75baa700140..954c97cbc93 100644 --- a/gremlin-js/gremlin-javascript/lib/structure/graph.ts +++ b/gremlin-js/gremlin-javascript/lib/structure/graph.ts @@ -67,12 +67,23 @@ export class Vertex< [P in keyof TProperties]: P extends string ? VertexProperties : never; }, > extends Element { + readonly labels: Set; + constructor( id: TId, - label: TLabel, + label: TLabel | string[], properties: Property[] | null = [], + labels?: string[], ) { - super(id, label, properties ?? []); + // Determine the canonical labels array from either explicit labels param or label param + const resolvedLabels: string[] = labels + ? labels + : Array.isArray(label) ? label + : label ? [label as string] + : ['vertex']; + const primaryLabel = (resolvedLabels.length > 0 ? resolvedLabels[0] : 'vertex') as TLabel; + super(id, primaryLabel, properties ?? []); + this.labels = new Set(resolvedLabels); } toString() { @@ -87,14 +98,25 @@ export class Edge< TProperties extends Record = Record, TId = number, > extends Element { + readonly labels: Set; + constructor( id: TId, readonly outV: TOutVertex, - readonly label: TLabel, + label: TLabel | string[], readonly inV: TInVertex, properties: Property[] | null = [], + labels?: string[], ) { - super(id, label, properties ?? []); + // Determine the canonical labels array from either explicit labels param or label param + const resolvedLabels: string[] = labels + ? labels + : Array.isArray(label) ? label + : label ? [label as string] + : ['edge']; + const primaryLabel = (resolvedLabels.length > 0 ? resolvedLabels[0] : 'edge') as TLabel; + super(id, primaryLabel, properties ?? []); + this.labels = new Set(resolvedLabels); } toString() { diff --git a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/EdgeSerializer.js b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/EdgeSerializer.js index 5ec2307b018..88d1a2ee531 100644 --- a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/EdgeSerializer.js +++ b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/EdgeSerializer.js @@ -59,7 +59,14 @@ export default class EdgeSerializer { bufs.push(this.ioc.anySerializer.serialize(item.id)); // {label} - const labels = Array.isArray(item.label) ? item.label : item.label ? [item.label] : []; + const labels = + item.labels instanceof Set + ? Array.from(item.labels) + : Array.isArray(item.label) + ? item.label + : item.label + ? [item.label] + : []; bufs.push(this.ioc.listSerializer.serialize(labels, false)); // {inVId} @@ -67,8 +74,14 @@ export default class EdgeSerializer { bufs.push(this.ioc.anySerializer.serialize(inVId)); // {inVLabel} - const inVLabel = item.inV && item.inV.label; - const inVLabels = Array.isArray(inVLabel) ? inVLabel : inVLabel ? [inVLabel] : []; + const inVLabels = + item.inV && item.inV.labels instanceof Set + ? Array.from(item.inV.labels) + : item.inV && Array.isArray(item.inV.label) + ? item.inV.label + : item.inV && item.inV.label + ? [item.inV.label] + : []; bufs.push(this.ioc.listSerializer.serialize(inVLabels, false)); // {outVId} @@ -76,8 +89,14 @@ export default class EdgeSerializer { bufs.push(this.ioc.anySerializer.serialize(outVId)); // {outVLabel} - const outVLabel = item.outV && item.outV.label; - const outVLabels = Array.isArray(outVLabel) ? outVLabel : outVLabel ? [outVLabel] : []; + const outVLabels = + item.outV && item.outV.labels instanceof Set + ? Array.from(item.outV.labels) + : item.outV && Array.isArray(item.outV.label) + ? item.outV.label + : item.outV && item.outV.label + ? [item.outV.label] + : []; bufs.push(this.ioc.listSerializer.serialize(outVLabels, false)); // {parent} @@ -100,23 +119,26 @@ export default class EdgeSerializer { // {id} fully qualified const id = await this.ioc.anySerializer.deserialize(reader); - // {label} bare list, extract first element + // {label} bare list - full multi-label list const labelList = await this.ioc.listSerializer.deserializeValue(reader, 0x00, this.ioc.DataType.LIST); - const label = Array.isArray(labelList) && labelList.length > 0 ? labelList[0] : labelList; + const labels = Array.isArray(labelList) ? labelList : labelList ? [labelList] : []; + const label = labels.length > 0 ? labels[0] : 'edge'; // {inVId} fully qualified const inVId = await this.ioc.anySerializer.deserialize(reader); - // {inVLabel} bare list, extract first element + // {inVLabel} bare list - full multi-label list const inVLabelList = await this.ioc.listSerializer.deserializeValue(reader, 0x00, this.ioc.DataType.LIST); - const inVLabel = Array.isArray(inVLabelList) && inVLabelList.length > 0 ? inVLabelList[0] : inVLabelList; + const inVLabels = Array.isArray(inVLabelList) ? inVLabelList : inVLabelList ? [inVLabelList] : []; + const inVLabel = inVLabels.length > 0 ? inVLabels[0] : 'vertex'; // {outVId} fully qualified const outVId = await this.ioc.anySerializer.deserialize(reader); - // {outVLabel} bare list, extract first element + // {outVLabel} bare list - full multi-label list const outVLabelList = await this.ioc.listSerializer.deserializeValue(reader, 0x00, this.ioc.DataType.LIST); - const outVLabel = Array.isArray(outVLabelList) && outVLabelList.length > 0 ? outVLabelList[0] : outVLabelList; + const outVLabels = Array.isArray(outVLabelList) ? outVLabelList : outVLabelList ? [outVLabelList] : []; + const outVLabel = outVLabels.length > 0 ? outVLabels[0] : 'vertex'; // {parent} fully qualified (always null in current TinkerPop) await this.ioc.anySerializer.deserialize(reader); @@ -126,10 +148,11 @@ export default class EdgeSerializer { return new Edge( id, - new Vertex(outVId, outVLabel, null), + new Vertex(outVId, outVLabel, null, outVLabels), label, - new Vertex(inVId, inVLabel, null), + new Vertex(inVId, inVLabel, null, inVLabels), properties || [], + labels, ); } diff --git a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/GraphSerializer.js b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/GraphSerializer.js index abf8ac5e2e8..5c843b44452 100644 --- a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/GraphSerializer.js +++ b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/GraphSerializer.js @@ -56,8 +56,9 @@ export default class GraphSerializer { // {id} bufs.push(this.ioc.anySerializer.serialize(v.id)); - // {label} as 1-element list (value-only) - bufs.push(this.ioc.listSerializer.serialize([v.label], false)); + // {label} as list (value-only) + const vLabels = v.labels instanceof Set ? Array.from(v.labels) : [v.label]; + bufs.push(this.ioc.listSerializer.serialize(vLabels, false)); const vps = Array.isArray(v.properties) ? v.properties : []; @@ -91,8 +92,9 @@ export default class GraphSerializer { // {id} bufs.push(this.ioc.anySerializer.serialize(e.id)); - // {label} as 1-element list (value-only) - bufs.push(this.ioc.listSerializer.serialize([e.label], false)); + // {label} as list (value-only) + const eLabels = e.labels instanceof Set ? Array.from(e.labels) : [e.label]; + bufs.push(this.ioc.listSerializer.serialize(eLabels, false)); // {inV_id} bufs.push(this.ioc.anySerializer.serialize(e.inV && e.inV.id)); @@ -133,11 +135,12 @@ export default class GraphSerializer { // {id} fully qualified const vId = await this.ioc.anySerializer.deserialize(reader); - // {label} value-only list, first element + // {label} value-only list - full multi-label list const vLabelList = await this.ioc.listSerializer.deserializeValue(reader, 0x00, this.ioc.DataType.LIST); - const vLabel = Array.isArray(vLabelList) && vLabelList.length > 0 ? vLabelList[0] : vLabelList; + const vLabels = Array.isArray(vLabelList) ? vLabelList : vLabelList ? [vLabelList] : []; + const vLabel = vLabels.length > 0 ? vLabels[0] : 'vertex'; - const vertex = new Vertex(vId, vLabel, []); + const vertex = new Vertex(vId, vLabel, [], vLabels); graph.vertices.set(vId, vertex); // {vp_count} bare int @@ -170,9 +173,10 @@ export default class GraphSerializer { // {id} fully qualified const eId = await this.ioc.anySerializer.deserialize(reader); - // {label} value-only list, first element + // {label} value-only list - full multi-label list const eLabelList = await this.ioc.listSerializer.deserializeValue(reader, 0x00, this.ioc.DataType.LIST); - const eLabel = Array.isArray(eLabelList) && eLabelList.length > 0 ? eLabelList[0] : eLabelList; + const eLabels = Array.isArray(eLabelList) ? eLabelList : eLabelList ? [eLabelList] : []; + const eLabel = eLabels.length > 0 ? eLabels[0] : 'edge'; // {inV_id} fully qualified const inVId = await this.ioc.anySerializer.deserialize(reader); @@ -196,7 +200,7 @@ export default class GraphSerializer { const inV = graph.vertices.get(inVId) || new Vertex(inVId, '', []); const outV = graph.vertices.get(outVId) || new Vertex(outVId, '', []); - const edge = new Edge(eId, outV, eLabel, inV, edgeProps || []); + const edge = new Edge(eId, outV, eLabel, inV, edgeProps || [], eLabels); graph.edges.set(eId, edge); } diff --git a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/VertexSerializer.js b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/VertexSerializer.js index 49771247d0d..4c64e1df8d2 100644 --- a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/VertexSerializer.js +++ b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/VertexSerializer.js @@ -54,7 +54,14 @@ export default class VertexSerializer { bufs.push(this.ioc.anySerializer.serialize(item.id)); // {label} - const labels = Array.isArray(item.label) ? item.label : item.label ? [item.label] : []; + const labels = + item.labels instanceof Set + ? Array.from(item.labels) + : Array.isArray(item.label) + ? item.label + : item.label + ? [item.label] + : []; bufs.push(this.ioc.listSerializer.serialize(labels, false)); // {properties} @@ -74,14 +81,15 @@ export default class VertexSerializer { // {id} fully qualified const id = await this.ioc.anySerializer.deserialize(reader); - // {label} bare list, extract first element + // {label} bare list - full multi-label list const labelList = await this.ioc.listSerializer.deserializeValue(reader, 0x00, this.ioc.DataType.LIST); - const label = Array.isArray(labelList) && labelList.length > 0 ? labelList[0] : labelList; + const labels = Array.isArray(labelList) ? labelList : labelList ? [labelList] : []; + const label = labels.length > 0 ? labels[0] : 'vertex'; // {properties} fully qualified const properties = await this.ioc.anySerializer.deserialize(reader); - return new Vertex(id, label, properties || []); + return new Vertex(id, label, properties || [], labels); } /** diff --git a/gremlin-js/gremlin-javascript/test/cucumber/feature-steps.js b/gremlin-js/gremlin-javascript/test/cucumber/feature-steps.js index bb956a2538d..61a82e1f700 100644 --- a/gremlin-js/gremlin-javascript/test/cucumber/feature-steps.js +++ b/gremlin-js/gremlin-javascript/test/cucumber/feature-steps.js @@ -111,24 +111,44 @@ Given(/^the (.+) graph$/, function (graphName) { if (ignoredScenarios[this.scenario]) { return 'skipped'; } - this.graphName = graphName; + + // Multi-label tests use the gmultilabel traversal source for empty graphs + if ((this.isMultiLabelDefault || this.isMultiLabel) && graphName === 'empty') { + this.graphName = 'multilabel'; + } else { + this.graphName = graphName; + } const data = this.getData(); this.g = anon.traversal().with_(data.connection); + if (this.isMultiLabelDefault) { + this.g = this.g.with_("multilabel"); + } + if (this.isGraphComputer) { this.g = this.g.withComputer(); } - if (graphName === 'empty') { + if (this.graphName === 'empty') { return this.cleanEmptyGraph(); } + if (this.graphName === 'multilabel') { + return this.cleanMultilabelGraph(); + } }); Given('the graph initializer of', function (traversalText) { const p = Object.assign({}, this.parameters); p.g = this.g; const traversal = gremlin[this.scenario].shift()(p); - return traversal.toList(); + return traversal.toList().then(() => { + // Reload vertex/edge data for dynamic graphs after initializer adds data + if (this.graphName === 'empty') { + return this.loadEmptyGraphData(); + } else if (this.graphName === 'multilabel') { + return this.loadMultilabelGraphData(); + } + }); }); Given('an unsupported test', () => {}); @@ -151,6 +171,8 @@ Given(/^using the parameter (.+) defined as "(.+)"$/, function (paramName, strin let p = Promise.resolve(); if (this.graphName === 'empty') { p = this.loadEmptyGraphData(); + } else if (this.graphName === 'multilabel') { + p = this.loadMultilabelGraphData(); } return p.then(() => { this.parameters[paramName] = parseValue.call(this, stringValue); @@ -168,6 +190,8 @@ Given(/^using the side effect (.+) defined as "(.+)"$/, function (sideEffectKey, let p = Promise.resolve(); if (this.graphName === 'empty') { p = this.loadEmptyGraphData(); + } else if (this.graphName === 'multilabel') { + p = this.loadMultilabelGraphData(); } return p.then(() => { this.sideEffects[sideEffectKey] = parseValue.call(this, stringValue); diff --git a/gremlin-js/gremlin-javascript/test/cucumber/gremlin.js b/gremlin-js/gremlin-javascript/test/cucumber/gremlin.js index d4e6e97e758..03a9eabbb45 100644 --- a/gremlin-js/gremlin-javascript/test/cucumber/gremlin.js +++ b/gremlin-js/gremlin-javascript/test/cucumber/gremlin.js @@ -957,6 +957,8 @@ const gremlins = { g_V_limitX3X_addVXsoftwareX_aggregateXa1X_byXlabelX_aggregateXa2X_byXlabelX_capXa1_a2X_selectXa_bX_byXunfoldX_foldX: [function({g}) { return g.addV("person").property("name", "marko").property("age", 29).as("marko").addV("person").property("name", "vadas").property("age", 27).as("vadas").addV("software").property("name", "lop").property("lang", "java").as("lop").addV("person").property("name", "josh").property("age", 32).as("josh").addV("software").property("name", "ripple").property("lang", "java").as("ripple").addV("person").property("name", "peter").property("age", 35).as("peter").addE("knows").from_("marko").to("vadas").property("weight", 0.5).addE("knows").from_("marko").to("josh").property("weight", 1.0).addE("created").from_("marko").to("lop").property("weight", 0.4).addE("created").from_("josh").to("ripple").property("weight", 1.0).addE("created").from_("josh").to("lop").property("weight", 0.4).addE("created").from_("peter").to("lop").property("weight", 0.2) }, function({g}) { return g.V().limit(3).addV("software").aggregate("a1").by(T.label).aggregate("a2").by(T.label).cap("a1", "a2").select("a1", "a2").by(__.unfold().fold()) }], g_addV_propertyXname_markoX_withXkey_valueX_valuesXname_keyX: [function({g}) { return g.addV().property("name", "marko").with_("key", "value").values("name", "key") }], g_addV_propertyXname_marko_since_2010X_withXkey_valueX_propertiesXnameX_valuesXsince_keyX: [function({g}) { return g.addV().property("name", "marko", "since", 2010).with_("key", "value").properties("name").values("since", "key") }], + g_addVXa_bX_labels: [function({g}) { return g.addV("a", "b").labels().fold() }, function({g}) { return g.V().hasLabel("a").hasLabel("b") }], + g_addVXa_b_cX_labels_count: [function({g}) { return g.addV("a", "b", "c").labels().count() }], g_injectX1X_asBool: [function({g}) { return g.inject(1).asBool() }], g_injectX3_14X_asBool: [function({g}) { return g.inject(3.14).asBool() }], g_injectXneg_1X_asBool: [function({g}) { return g.inject(-1).asBool() }], @@ -1197,10 +1199,15 @@ const gremlins = { g_E_properties_element: [function({g}) { return g.E().properties().element() }], g_VXv7_properties_properties_element_element: [function({g, vid7}) { return g.V(vid7).properties().properties().element().element().limit(1) }], g_V_properties_properties_element_element: [function({g, vid7}) { return g.V(vid7).properties().properties().element().element() }], - g_V_elementMap: [function({g}) { return g.V().elementMap() }], - g_V_elementMapXname_ageX: [function({g}) { return g.V().elementMap("name", "age") }], - g_EX11X_elementMap: [function({g, eid11}) { return g.E(eid11).elementMap() }], - g_V_elementMapXname_age_nullX: [function({g}) { return g.V().elementMap("name", "age", null) }], + g_V_elementMap: [function({g}) { return g.with_("singlelabel").V().elementMap() }], + g_V_elementMapXname_ageX: [function({g}) { return g.with_("singlelabel").V().elementMap("name", "age") }], + g_EX11X_elementMap: [function({g, eid11}) { return g.with_("singlelabel").E(eid11).elementMap() }], + g_V_elementMapXname_age_nullX: [function({g}) { return g.with_("singlelabel").V().elementMap("name", "age", null) }], + g_withXmultilabelX_VXmarkoX_elementMap: [function({g}) { return g.with_("multilabel").V().has("name", "marko").elementMap() }], + g_V_elementMap_single_label_default: [function({g}) { return g.addV("person").addLabel("employee").property("name", "marko") }, function({g}) { return g.V().elementMap() }], + g_withXmultilabelX_V_elementMap_multilabel: [function({g}) { return g.addV("person").addLabel("employee").property("name", "marko") }, function({g}) { return g.with_("multilabel").V().elementMap() }], + g_V_elementMap_multi_label_default: [function({g}) { return g.addV("person").addLabel("employee").property("name", "marko") }, function({g}) { return g.V().elementMap() }], + g_withXsinglelabelX_V_elementMap_multi_label_default: [function({g}) { return g.addV("person").addLabel("employee").property("name", "marko") }, function({g}) { return g.with_("singlelabel").V().elementMap() }], g_V_asXaX_flatMapXselectXaXX: [function({g}) { return g.V().as("a").flatMap(__.select("a")) }], g_V_valuesXnameX_flatMapXsplitXaX_unfoldX: [function({g}) { return g.V().values("name").flatMap(__.split("a").unfold()) }], g_V_flatMapXout_outX_path: [function({g}) { return g.V().flatMap(__.out().out()).path() }], @@ -1251,6 +1258,11 @@ const gremlins = { g_injectXListX1_2XX_lTrimXlocalX: [function({g}) { return g.inject([1, 2]).lTrim(Scope.local) }], g_V_valuesXnameX_lTrim: [function({g}) { return g.addV("person").property("name", " marko ").property("age", 29).as("marko").addV("person").property("name", " vadas ").property("age", 27).as("vadas").addV("software").property("name", " lop").property("lang", "java").as("lop").addV("person").property("name", "josh ").property("age", 32).as("josh").addV("software").property("name", " ripple ").property("lang", "java").as("ripple").addV("person").property("name", "peter").property("age", 35).as("peter").addE("knows").from_("marko").to("vadas").property("weight", 0.5).addE("knows").from_("marko").to("josh").property("weight", 1.0).addE("created").from_("marko").to("lop").property("weight", 0.4).addE("created").from_("josh").to("ripple").property("weight", 1.0).addE("created").from_("josh").to("lop").property("weight", 0.4).addE("created").from_("peter").to("lop").property("weight", 0.2) }, function({g}) { return g.V().values("name").lTrim() }], g_V_valuesXnameX_order_fold_lTrimXlocalX: [function({g}) { return g.addV("person").property("name", " marko ").property("age", 29).as("marko").addV("person").property("name", " vadas ").property("age", 27).as("vadas").addV("software").property("name", " lop").property("lang", "java").as("lop").addV("person").property("name", "josh ").property("age", 32).as("josh").addV("software").property("name", " ripple ").property("lang", "java").as("ripple").addV("person").property("name", "peter").property("age", 35).as("peter").addE("knows").from_("marko").to("vadas").property("weight", 0.5).addE("knows").from_("marko").to("josh").property("weight", 1.0).addE("created").from_("marko").to("lop").property("weight", 0.4).addE("created").from_("josh").to("ripple").property("weight", 1.0).addE("created").from_("josh").to("lop").property("weight", 0.4).addE("created").from_("peter").to("lop").property("weight", 0.2) }, function({g}) { return g.V().values("name").order().fold().lTrim(Scope.local) }], + g_V_hasLabelXpersonX_labels: [function({g}) { return g.V().hasLabel("person").labels() }], + g_V_labels_multilabel: [function({g}) { return g.addV("a", "b") }, function({g}) { return g.V().labels() }], + g_addVXa_bX_labels_count: [function({g}) { return g.addV("a", "b") }, function({g}) { return g.V().labels().count() }], + g_addV_labels: [function({g}) { return g.addV() }, function({g}) { return g.V().labels() }], + g_E_labels: [function({g}) { return g.E().hasLabel("knows").labels() }], g_injectXfeature_test_nullX_length: [function({g}) { return g.inject("feature", "test", null).length() }], g_injectXfeature_test_nullX_lengthXlocalX: [function({g}) { return g.inject("feature", "test", null).length(Scope.local) }], g_injectXListXa_bXX_length: [function({g}) { return g.inject(["a", "b"]).length() }], @@ -1517,6 +1529,15 @@ const gremlins = { g_mergeV_hidden_label_key_onMatch_matched_prohibited: [function({g, xx1}) { return g.mergeV(new Map([])).option(Merge.onMatch, xx1) }], g_injectXlist1_list2_list3X_fold_asXmX_mergeVXselectXmX_limitXlocal_1X_unfoldX_optionXonCreate_selectXmX_rangeXlocal_1_2X_unfoldX_optionXonMatch_selectXmX_tailXlocalX_unfoldX_to_match: [function({g}) { return g.addV("person").property("name", "marko").property("age", 29) }, function({g}) { return g.inject(new Map([[T.label, "person"], ["name", "marko"]]), new Map([[T.label, "person"], ["name", "marko"]]), new Map([["created", "N"]])).fold().as("m").mergeV(__.select("m").limit(Scope.local, 1).unfold()).option(Merge.onCreate, __.select("m").range(Scope.local, 1, 2).unfold()).option(Merge.onMatch, __.select("m").tail(Scope.local).unfold()) }, function({g}) { return g.V().has("person", "name", "marko").has("created", "N") }, function({g}) { return g.V() }], g_injectXlist1_list2_list3X_fold_asXmX_mergeVXselectXmX_limitXlocal_1X_unfoldX_optionXonCreate_selectXmX_rangeXlocal_1_2X_unfoldX_optionXonMatch_selectXmX_tailXlocalX_unfoldX_to_create: [function({g}) { return g.addV("person").property("name", "marko").property("age", 29) }, function({g}) { return g.inject(new Map([[T.label, "person"], ["name", "stephen"]]), new Map([[T.label, "person"], ["name", "stephen"]]), new Map([["created", "N"]])).fold().as("m").mergeV(__.select("m").limit(Scope.local, 1).unfold()).option(Merge.onCreate, __.select("m").range(Scope.local, 1, 2).unfold()).option(Merge.onMatch, __.select("m").tail(Scope.local).unfold()) }, function({g}) { return g.V().has("person", "name", "stephen").hasNot("created") }, function({g}) { return g.V() }], + g_mergeVXlabel_ab_name_markoX_multilabel_create: [function({g}) { return g.mergeV(new Map([[T.label, ["person", "employee"]], ["name", "marko"]])) }, function({g}) { return g.V() }, function({g}) { return g.V().hasLabel("person").hasLabel("employee") }, function({g}) { return g.V().has("name", "marko") }], + g_mergeVXlabel_abc_name_testX_multilabel_create: [function({g}) { return g.mergeV(new Map([[T.label, ["a", "b", "c"]], ["name", "test"]])) }, function({g}) { return g.V() }, function({g}) { return g.V().hasLabel("a").hasLabel("b").hasLabel("c") }], + g_mergeVXlabel_ab_name_markoX_multilabel_match: [function({g}) { return g.addV("person").addLabel("employee").property("name", "marko") }, function({g}) { return g.mergeV(new Map([[T.label, ["person", "employee"]], ["name", "marko"]])) }, function({g}) { return g.V() }, function({g}) { return g.V().hasLabel("person").hasLabel("employee") }], + g_mergeVXlabel_ab_name_markoX_multilabel_nomatch: [function({g}) { return g.addV("person").property("name", "marko") }, function({g}) { return g.mergeV(new Map([[T.label, ["person", "employee"]], ["name", "marko"]])) }, function({g}) { return g.V() }], + g_mergeVXlabel_person_name_markoX_optionXonMatch_label_managerX: [function({g}) { return g.addV("person").addLabel("employee").property("name", "marko") }, function({g}) { return g.mergeV(new Map([[T.label, "person"], ["name", "marko"]])).option(Merge.onMatch, new Map([[T.label, "manager"]])) }, function({g}) { return g.V() }, function({g}) { return g.V().hasLabel("manager") }, function({g}) { return g.V().hasLabel("person") }, function({g}) { return g.V().hasLabel("employee") }], + g_mergeVXlabel_person_name_markoX_optionXonMatch_label_manager_directorX: [function({g}) { return g.addV("person").property("name", "marko") }, function({g}) { return g.mergeV(new Map([[T.label, "person"], ["name", "marko"]])).option(Merge.onMatch, new Map([[T.label, ["manager", "director"]]])) }, function({g}) { return g.V() }, function({g}) { return g.V().hasLabel("manager").hasLabel("director") }, function({g}) { return g.V().hasLabel("person") }], + g_mergeVXlabel_person_name_markoX_optionXonMatch_label_emptyX: [function({g}) { return g.addV("person").property("name", "marko") }, function({g}) { return g.mergeV(new Map([[T.label, "person"], ["name", "marko"]])).option(Merge.onMatch, new Map([[T.label, []]])) }, function({g}) { return g.V() }, function({g}) { return g.V().hasLabel("person") }], + g_mergeVXname_markoX_optionXonCreate_label_ab_name_markoX: [function({g}) { return g.mergeV(new Map([["name", "marko"]])).option(Merge.onCreate, new Map([[T.label, ["person", "employee"]], ["name", "marko"]])) }, function({g}) { return g.V() }, function({g}) { return g.V().hasLabel("person").hasLabel("employee") }, function({g}) { return g.V().has("name", "marko") }], + g_mergeVXlabel_person_name_markoX_optionXonMatch_label_existingX: [function({g}) { return g.addV("person").addLabel("employee").property("name", "marko") }, function({g}) { return g.mergeV(new Map([[T.label, "person"], ["name", "marko"]])).option(Merge.onMatch, new Map([[T.label, "person"]])) }, function({g}) { return g.V() }, function({g}) { return g.V().hasLabel("person").hasLabel("employee") }], g_V_age_min: [function({g}) { return g.V().values("age").min() }], g_V_foo_min: [function({g}) { return g.V().values("foo").min() }], g_V_name_min: [function({g}) { return g.V().values("name").min() }], @@ -1806,19 +1827,24 @@ const gremlins = { g_V_valueMap_unfold_mapXselectXkeysXX: [function({g}) { return g.V().valueMap().unfold().map(__.select(Column.keys)) }], g_VX1X_repeatXboth_simplePathX_untilXhasIdX6XX_path_byXnameX_unfold: [function({g, vid6, vid1}) { return g.V(vid1).repeat(__.both().simplePath()).until(__.hasId(vid6)).path().by("name").unfold() }], g_V_valueMap: [function({g}) { return g.V().valueMap() }], - g_V_valueMapXtrueX: [function({g}) { return g.V().valueMap(true) }], - g_V_valueMap_withXtokensX: [function({g}) { return g.V().valueMap().with_(WithOptions.tokens) }], + g_V_valueMapXtrueX: [function({g}) { return g.with_("singlelabel").V().valueMap(true) }], + g_V_valueMap_withXtokensX: [function({g}) { return g.with_("singlelabel").V().valueMap().with_(WithOptions.tokens) }], g_V_valueMapXname_ageX: [function({g}) { return g.V().valueMap("name", "age") }], - g_V_valueMapXtrue_name_ageX: [function({g}) { return g.V().valueMap(true, "name", "age") }], - g_V_valueMapXname_ageX_withXtokensX: [function({g}) { return g.V().valueMap("name", "age").with_(WithOptions.tokens) }], - g_V_valueMapXname_ageX_withXtokens_labelsX_byXunfoldX: [function({g}) { return g.V().valueMap("name", "age").with_(WithOptions.tokens, WithOptions.labels).by(__.unfold()) }], + g_V_valueMapXtrue_name_ageX: [function({g}) { return g.with_("singlelabel").V().valueMap(true, "name", "age") }], + g_V_valueMapXname_ageX_withXtokensX: [function({g}) { return g.with_("singlelabel").V().valueMap("name", "age").with_(WithOptions.tokens) }], + g_V_valueMapXname_ageX_withXtokens_labelsX_byXunfoldX: [function({g}) { return g.with_("singlelabel").V().valueMap("name", "age").with_(WithOptions.tokens, WithOptions.labels).by(__.unfold()) }], g_V_valueMapXname_ageX_withXtokens_idsX_byXunfoldX: [function({g}) { return g.V().valueMap("name", "age").with_(WithOptions.tokens, WithOptions.ids).by(__.unfold()) }], g_VX1X_outXcreatedX_valueMap: [function({g, vid1}) { return g.V(vid1).out("created").valueMap() }], - g_V_hasLabelXpersonX_filterXoutEXcreatedXX_valueMapXtrueX: [function({g}) { return g.V().hasLabel("person").filter(__.outE("created")).valueMap(true) }], - g_V_hasLabelXpersonX_filterXoutEXcreatedXX_valueMap_withXtokensX: [function({g}) { return g.V().hasLabel("person").filter(__.outE("created")).valueMap().with_(WithOptions.tokens) }], + g_V_hasLabelXpersonX_filterXoutEXcreatedXX_valueMapXtrueX: [function({g}) { return g.with_("singlelabel").V().hasLabel("person").filter(__.outE("created")).valueMap(true) }], + g_V_hasLabelXpersonX_filterXoutEXcreatedXX_valueMap_withXtokensX: [function({g}) { return g.with_("singlelabel").V().hasLabel("person").filter(__.outE("created")).valueMap().with_(WithOptions.tokens) }], g_VX1X_valueMapXname_locationX_byXunfoldX_by: [function({g, vid1}) { return g.V(vid1).valueMap("name", "location").by(__.unfold()).by() }], g_V_valueMapXname_age_nullX: [function({g}) { return g.V().valueMap("name", "age", null) }], g_V_valueMapXname_ageX_byXisXxXXbyXunfoldX: [function({g}) { return g.V().valueMap("name", "age").by(__.is("x")).by(__.unfold()) }], + g_withXmultilabelX_VXmarkoX_valueMap_withXtokensX: [function({g}) { return g.with_("multilabel").V().has("name", "marko").valueMap().with_(WithOptions.tokens) }], + g_V_valueMap_withXtokensX_single_label_default: [function({g}) { return g.addV("person").addLabel("employee").property("name", "marko") }, function({g}) { return g.V().valueMap().with_(WithOptions.tokens) }], + g_withXmultilabelX_V_valueMap_withXtokensX_multilabel: [function({g}) { return g.addV("person").addLabel("employee").property("name", "marko") }, function({g}) { return g.with_("multilabel").V().valueMap().with_(WithOptions.tokens) }], + g_V_valueMap_withXtokensX_multi_label_default: [function({g}) { return g.addV("person").addLabel("employee").property("name", "marko") }, function({g}) { return g.V().valueMap().with_(WithOptions.tokens) }], + g_withXsinglelabelX_V_valueMap_withXtokensX_multi_label_default: [function({g}) { return g.addV("person").addLabel("employee").property("name", "marko") }, function({g}) { return g.with_("singlelabel").V().valueMap().with_(WithOptions.tokens) }], g_VXnullX: [function({g}) { return g.V(null) }], g_VXlistXnullXX: [function({g, xx1}) { return g.V(xx1) }], g_VX1_nullX: [function({g, vid1}) { return g.V(vid1, null) }], @@ -2002,6 +2028,10 @@ const gremlins = { g_V_out_out_properties_asXheadX_path_order_byXdescX_selectXheadX_value: [function({g}) { return g.V().out().out().properties().as("head").path().order().by(Order.desc).select("head").value() }], g_V_out_out_values_asXheadX_path_order_byXascX_selectXheadX: [function({g}) { return g.V().out().out().values().as("head").path().order().by(Order.asc).select("head") }], g_V_out_out_values_asXheadX_path_order_byXdescX_selectXheadX: [function({g}) { return g.V().out().out().values().as("head").path().order().by(Order.desc).select("head") }], + g_V_hasLabelXpersonX_hasXname_markoX_addLabelXemployeeX_labels: [function({g}) { return g.addV("person").property("name", "marko") }, function({g}) { return g.V().hasLabel("person").has("name", "marko").addLabel("employee").labels().fold() }, function({g}) { return g.V().hasLabel("person").hasLabel("employee") }], + g_V_addLabelXa_bX_labels_count: [function({g}) { return g.addV("person") }, function({g}) { return g.V().addLabel("a", "b").labels().count() }], + g_V_addLabelXexistingX_labels_count: [function({g}) { return g.addV("person") }, function({g}) { return g.V().addLabel("person").labels().count() }], + g_E_addLabelXfriendX_labels_fold: [function({g}) { return g.addV("person").property("name", "marko").as("a").addV("person").property("name", "josh").as("b").addE("knows").from_("a").to("b") }, function({g}) { return g.E().addLabel("friend").labels().fold() }], g_V_valueXnameX_aggregateXxX_capXxX: [function({g}) { return g.V().values("name").aggregate("x").cap("x") }], g_V_aggregateXxX_byXnameX_capXxX: [function({g}) { return g.V().aggregate("x").by("name").cap("x") }], g_V_out_aggregateXaX_path: [function({g}) { return g.V().out().aggregate("a").path() }], @@ -2059,6 +2089,13 @@ const gremlins = { g_V_repeatXaggregateXaXX_timesX2X_capXaX_unfold: [function({g}) { return g.V().repeat(__.aggregate("a")).times(2).cap("a").unfold() }], g_V_aggregateXaX_capXaX_unfold_both: [function({g}) { return g.V().aggregate("a").cap("a").unfold().both() }], g_V_aggregateXaX_capXaX_unfold_barrier_both: [function({g}) { return g.V().aggregate("a").cap("a").unfold().barrier().both() }], + g_V_dropLabelXaX_labels: [function({g}) { return g.addV("a", "b") }, function({g}) { return g.V().dropLabel("a").labels().fold() }, function({g}) { return g.V().hasLabel("a") }, function({g}) { return g.V().hasLabel("b") }], + g_V_dropLabels_labels: [function({g}) { return g.addV("a", "b") }, function({g}) { return g.V().dropLabels().labels() }], + g_V_dropLabelXa_bX_labels: [function({g}) { return g.addV("a", "b", "c") }, function({g}) { return g.V().dropLabel("a", "b").labels() }], + g_V_dropLabels_defaultLabel: [function({g}) { return g.addV("person") }, function({g}) { return g.V().dropLabels().labels() }], + g_E_dropLabelXknowsX_labels: [function({g}) { return g.addV("person").as("a").addV("person").as("b").addE("knows").from_("a").to("b") }, function({g}) { return g.E().dropLabel("knows").labels().fold() }], + g_E_dropLabels_labels: [function({g}) { return g.addV("person").as("a").addV("person").as("b").addE("knows").from_("a").to("b") }, function({g}) { return g.E().dropLabels().labels() }], + g_V_dropLabelXnonExistentX_labels: [function({g}) { return g.addV("a", "b") }, function({g}) { return g.V().dropLabel("xyz").labels() }], g_V_fail: [function({g}) { return g.V().fail() }], g_V_failXmsgX: [function({g}) { return g.V().fail("msg") }], g_V_unionXout_failX: [function({g}) { return g.V().union(__.out(), __.fail()) }], diff --git a/gremlin-js/gremlin-javascript/test/cucumber/world.js b/gremlin-js/gremlin-javascript/test/cucumber/world.js index 563abb8ce81..04d11255795 100644 --- a/gremlin-js/gremlin-javascript/test/cucumber/world.js +++ b/gremlin-js/gremlin-javascript/test/cucumber/world.js @@ -54,6 +54,12 @@ TinkerPopWorld.prototype.cleanEmptyGraph = function () { return g.V().drop().toList(); }; +TinkerPopWorld.prototype.cleanMultilabelGraph = function () { + const connection = this.cache['multilabel'].connection; + const g = anon.traversal().withRemote(connection); + return g.V().drop().toList(); +}; + TinkerPopWorld.prototype.loadEmptyGraphData = function () { const cacheData = this.cache['empty']; const c = cacheData.connection; @@ -64,16 +70,30 @@ TinkerPopWorld.prototype.loadEmptyGraphData = function () { }); }; +TinkerPopWorld.prototype.loadMultilabelGraphData = function () { + const cacheData = this.cache['multilabel']; + const c = cacheData.connection; + return Promise.all([ getVertices(c), getEdges(c), getVertexProperties(c) ]).then(values => { + cacheData.vertices = values[0]; + cacheData.edges = values[1]; + cacheData.vertexProperties = values[2] + }); +}; + setWorldConstructor(TinkerPopWorld); BeforeAll(function () { // load all traversals - const promises = ['modern', 'classic', 'crew', 'grateful', 'sink', 'empty'].map(graphName => { + const promises = ['modern', 'classic', 'crew', 'grateful', 'sink', 'empty', 'multilabel'].map(graphName => { let connection = null; if (graphName === 'empty') { connection = getConnection('ggraph'); return connection.open().then(() => cache['empty'] = { connection: connection }); } + if (graphName === 'multilabel') { + connection = getConnection('gmultilabel'); + return connection.open().then(() => cache['multilabel'] = { connection: connection }); + } connection = getConnection('g' + graphName); return connection.open() .then(() => Promise.all([getVertices(connection), getEdges(connection), getVertexProperties(connection)])) @@ -96,6 +116,8 @@ AfterAll(function () { Before(function (info) { this.scenario = info.pickle.name; this.cache = cache; + this.isMultiLabel = info.pickle.tags && info.pickle.tags.some(t => t.name === '@MultiLabel'); + this.isMultiLabelDefault = info.pickle.tags && info.pickle.tags.some(t => t.name === '@MultiLabelDefault'); }); Before({tags: "@GraphComputerOnly"}, function() { diff --git a/gremlin-js/gremlin-mcp/src/translator/stepNames.ts b/gremlin-js/gremlin-mcp/src/translator/stepNames.ts index 3873293afdb..71525a298ba 100644 --- a/gremlin-js/gremlin-mcp/src/translator/stepNames.ts +++ b/gremlin-js/gremlin-mcp/src/translator/stepNames.ts @@ -28,6 +28,7 @@ export const GREMLIN_STEP_NAMES: ReadonlySet = new Set([ 'E', 'V', 'addE', + 'addLabel', 'addV', 'aggregate', 'all', @@ -62,6 +63,8 @@ export const GREMLIN_STEP_NAMES: ReadonlySet = new Set([ 'discard', 'disjunct', 'drop', + 'dropLabel', + 'dropLabels', 'element', 'elementMap', 'emit', @@ -92,6 +95,7 @@ export const GREMLIN_STEP_NAMES: ReadonlySet = new Set([ 'key', 'lTrim', 'label', + 'labels', 'length', 'limit', 'local', @@ -170,6 +174,7 @@ export const GREMLIN_STEP_NAMES_PASCAL: ReadonlySet = new Set([ 'E', 'V', 'AddE', + 'AddLabel', 'AddV', 'Aggregate', 'All', @@ -204,6 +209,8 @@ export const GREMLIN_STEP_NAMES_PASCAL: ReadonlySet = new Set([ 'Discard', 'Disjunct', 'Drop', + 'DropLabel', + 'DropLabels', 'Element', 'ElementMap', 'Emit', @@ -234,6 +241,7 @@ export const GREMLIN_STEP_NAMES_PASCAL: ReadonlySet = new Set([ 'Key', 'LTrim', 'Label', + 'Labels', 'Length', 'Limit', 'Local', diff --git a/gremlin-python/src/main/python/gremlin_python/process/graph_traversal.py b/gremlin-python/src/main/python/gremlin_python/process/graph_traversal.py index 399c44e448e..764e7e24423 100644 --- a/gremlin-python/src/main/python/gremlin_python/process/graph_traversal.py +++ b/gremlin-python/src/main/python/gremlin_python/process/graph_traversal.py @@ -143,6 +143,10 @@ def with_(self, k, v=None): else: options_strategy[1].configuration[k] = val + # Render multilabel/singlelabel in gremlin text (temporary until these options are removed) + if k == 'multilabel' or k == 'singlelabel': + source.gremlin_lang.gremlin.extend(['.', 'with', '(', f'"{k}"', ')']) + return source def tx(self): @@ -359,6 +363,17 @@ def add_v(self, *args): self.gremlin_lang.add_step("addV", *args) return self + def addLabel(self, *args): + warnings.warn( + "gremlin_python.process.GraphTraversal.addLabel will be replaced by " + "gremlin_python.process.GraphTraversal.add_label.", + DeprecationWarning) + return self.add_label(*args) + + def add_label(self, *args): + self.gremlin_lang.add_step("addLabel", *args) + return self + def aggregate(self, *args): self.gremlin_lang.add_step("aggregate", *args) return self @@ -523,6 +538,28 @@ def drop(self, *args): self.gremlin_lang.add_step("drop", *args) return self + def dropLabel(self, *args): + warnings.warn( + "gremlin_python.process.GraphTraversal.dropLabel will be replaced by " + "gremlin_python.process.GraphTraversal.drop_label.", + DeprecationWarning) + return self.drop_label(*args) + + def drop_label(self, *args): + self.gremlin_lang.add_step("dropLabel", *args) + return self + + def dropLabels(self, *args): + warnings.warn( + "gremlin_python.process.GraphTraversal.dropLabels will be replaced by " + "gremlin_python.process.GraphTraversal.drop_labels.", + DeprecationWarning) + return self.drop_labels(*args) + + def drop_labels(self, *args): + self.gremlin_lang.add_step("dropLabels", *args) + return self + def element(self, *args): self.gremlin_lang.add_step("element", *args) return self @@ -715,6 +752,10 @@ def label(self, *args): self.gremlin_lang.add_step("label", *args) return self + def labels(self, *args): + self.gremlin_lang.add_step("labels", *args) + return self + def length(self, *args): self.gremlin_lang.add_step("length", *args) return self @@ -1156,6 +1197,18 @@ def addV(cls, *args): def add_v(cls, *args): return cls.graph_traversal(None, None, GremlinLang()).add_v(*args) + @classmethod + def addLabel(cls, *args): + warnings.warn( + "gremlin_python.process.__.addLabel will be replaced by " + "gremlin_python.process.__.add_label.", + DeprecationWarning) + return cls.add_label(*args) + + @classmethod + def add_label(cls, *args): + return cls.graph_traversal(None, None, GremlinLang()).add_label(*args) + @classmethod def aggregate(cls, *args): return cls.graph_traversal(None, None, GremlinLang()).aggregate(*args) @@ -1308,6 +1361,30 @@ def disjunct(cls, *args): def drop(cls, *args): return cls.graph_traversal(None, None, GremlinLang()).drop(*args) + @classmethod + def dropLabel(cls, *args): + warnings.warn( + "gremlin_python.process.__.dropLabel will be replaced by " + "gremlin_python.process.__.drop_label.", + DeprecationWarning) + return cls.drop_label(*args) + + @classmethod + def drop_label(cls, *args): + return cls.graph_traversal(None, None, GremlinLang()).drop_label(*args) + + @classmethod + def dropLabels(cls, *args): + warnings.warn( + "gremlin_python.process.__.dropLabels will be replaced by " + "gremlin_python.process.__.drop_labels.", + DeprecationWarning) + return cls.drop_labels(*args) + + @classmethod + def drop_labels(cls, *args): + return cls.graph_traversal(None, None, GremlinLang()).drop_labels(*args) + @classmethod def element(cls, *args): return cls.graph_traversal(None, None, GremlinLang()).element(*args) @@ -1504,6 +1581,10 @@ def key(cls, *args): def label(cls, *args): return cls.graph_traversal(None, None, GremlinLang()).label(*args) + @classmethod + def labels(cls, *args): + return cls.graph_traversal(None, None, GremlinLang()).labels(*args) + @classmethod def length(cls, *args): return cls.graph_traversal(None, None, GremlinLang()).length(*args) @@ -1869,6 +1950,14 @@ def add_v(*args): return __.add_v(*args) +def addLabel(*args): + return __.add_label(*args) + + +def add_label(*args): + return __.add_label(*args) + + def aggregate(*args): return __.aggregate(*args) @@ -2009,6 +2098,22 @@ def drop(*args): return __.drop(*args) +def dropLabel(*args): + return __.drop_label(*args) + + +def drop_label(*args): + return __.drop_label(*args) + + +def dropLabels(*args): + return __.drop_labels(*args) + + +def drop_labels(*args): + return __.drop_labels(*args) + + def element(*args): return __.element(*args) @@ -2159,6 +2264,10 @@ def label(*args): return __.label(*args) +def labels(*args): + return __.labels(*args) + + def length(*args): return __.length(*args) @@ -2462,6 +2571,10 @@ def where(*args): statics.add_static('add_v', add_v) +statics.add_static('addLabel', addLabel) + +statics.add_static('add_label', add_label) + statics.add_static('aggregate', aggregate) statics.add_static('all_', all_) @@ -2532,6 +2645,14 @@ def where(*args): statics.add_static('drop', drop) +statics.add_static('dropLabel', dropLabel) + +statics.add_static('drop_label', drop_label) + +statics.add_static('dropLabels', dropLabels) + +statics.add_static('drop_labels', drop_labels) + statics.add_static('element', element) statics.add_static('elementMap', elementMap) @@ -2604,6 +2725,8 @@ def where(*args): statics.add_static('label', label) +statics.add_static('labels', labels) + statics.add_static('length', length) statics.add_static('limit', limit) diff --git a/gremlin-python/src/main/python/gremlin_python/structure/graph.py b/gremlin-python/src/main/python/gremlin_python/structure/graph.py index 619eb64899c..00c22f9dab6 100644 --- a/gremlin-python/src/main/python/gremlin_python/structure/graph.py +++ b/gremlin-python/src/main/python/gremlin_python/structure/graph.py @@ -64,19 +64,37 @@ def __hash__(self): class Vertex(Element): - def __init__(self, id, label="vertex", properties=None): - Element.__init__(self, id, label, properties) + def __init__(self, id, label="vertex", properties=None, labels=None): + if labels is not None and len(labels) > 0: + self._labels = set(labels) + Element.__init__(self, id, next(iter(labels)), properties) + else: + self._labels = {label} if label else {"vertex"} + Element.__init__(self, id, label or "vertex", properties) + + @property + def labels(self): + return frozenset(self._labels) def __repr__(self): return "v[" + str(self.id) + "]" class Edge(Element): - def __init__(self, id, outV, label, inV, properties=None): - Element.__init__(self, id, label, properties) + def __init__(self, id, outV, label, inV, properties=None, labels=None): + if labels is not None and len(labels) > 0: + self._labels = set(labels) + Element.__init__(self, id, next(iter(labels)), properties) + else: + self._labels = {label} if label else {"edge"} + Element.__init__(self, id, label or "edge", properties) self.outV = outV self.inV = inV + @property + def labels(self): + return frozenset(self._labels) + def __repr__(self): return "e[" + str(self.id) + "][" + str(self.outV.id) + "-" + self.label + "->" + str(self.inV.id) + "]" diff --git a/gremlin-python/src/main/python/gremlin_python/structure/io/graphbinaryV4.py b/gremlin-python/src/main/python/gremlin_python/structure/io/graphbinaryV4.py index 41843a33eb7..59a159fc396 100644 --- a/gremlin-python/src/main/python/gremlin_python/structure/io/graphbinaryV4.py +++ b/gremlin-python/src/main/python/gremlin_python/structure/io/graphbinaryV4.py @@ -593,12 +593,21 @@ def dictify(cls, obj, writer, to_extend, as_value=False, nullable=True): cls.prefix_bytes(cls.graphbinary_type, as_value, nullable, to_extend) writer.to_dict(obj.id, to_extend) - # serializing label as list here for now according to GraphBinaryV4 - ListIO.dictify([obj.label], writer, to_extend, True, False) + # serializing labels as list according to GraphBinaryV4 + if hasattr(obj, '_labels') and obj._labels: + ListIO.dictify(list(obj._labels), writer, to_extend, True, False) + else: + ListIO.dictify([obj.label], writer, to_extend, True, False) writer.to_dict(obj.inV.id, to_extend) - ListIO.dictify([obj.inV.label], writer, to_extend, True, False) + if hasattr(obj.inV, '_labels') and obj.inV._labels: + ListIO.dictify(list(obj.inV._labels), writer, to_extend, True, False) + else: + ListIO.dictify([obj.inV.label], writer, to_extend, True, False) writer.to_dict(obj.outV.id, to_extend) - ListIO.dictify([obj.outV.label], writer, to_extend, True, False) + if hasattr(obj.outV, '_labels') and obj.outV._labels: + ListIO.dictify(list(obj.outV._labels), writer, to_extend, True, False) + else: + ListIO.dictify([obj.outV.label], writer, to_extend, True, False) to_extend.extend(NULL_BYTES) to_extend.extend(NULL_BYTES) @@ -611,15 +620,15 @@ def objectify(cls, buff, reader, nullable=True): @classmethod def _read_edge(cls, b, r): edgeid = r.read_object(b) - # reading single string value for now according to GraphBinaryV4 - edgelbl = r.to_object(b, DataType.list, False)[0] + # reading label list according to GraphBinaryV4 + edge_labels = r.to_object(b, DataType.list, False) inv = Vertex(r.read_object(b), r.to_object(b, DataType.list, False)[0]) outv = Vertex(r.read_object(b), r.to_object(b, DataType.list, False)[0]) b.read(2) props = r.read_object(b) # null properties are returned as empty lists properties = [] if props is None else props - edge = Edge(edgeid, outv, edgelbl, inv, properties) + edge = Edge(edgeid, outv, edge_labels[0] if edge_labels else "edge", inv, properties, labels=edge_labels) return edge @@ -679,7 +688,10 @@ def dictify(cls, obj, writer, to_extend, as_value=False, nullable=True): IntIO.dictify(len(vertices), writer, to_extend, True, False) for v in vertices: writer.to_dict(v.id, to_extend) - ListIO.dictify([v.label], writer, to_extend, True, False) + if hasattr(v, '_labels') and v._labels: + ListIO.dictify(list(v._labels), writer, to_extend, True, False) + else: + ListIO.dictify([v.label], writer, to_extend, True, False) v_props = v.properties IntIO.dictify(len(v_props), writer, to_extend, True, False) for vp in v_props: @@ -692,7 +704,10 @@ def dictify(cls, obj, writer, to_extend, as_value=False, nullable=True): IntIO.dictify(len(edges), writer, to_extend, True, False) for e in edges: writer.to_dict(e.id, to_extend) - ListIO.dictify([e.label], writer, to_extend, True, False) + if hasattr(e, '_labels') and e._labels: + ListIO.dictify(list(e._labels), writer, to_extend, True, False) + else: + ListIO.dictify([e.label], writer, to_extend, True, False) writer.to_dict(e.inV.id, to_extend) writer.to_dict(None, to_extend) writer.to_dict(e.outV.id, to_extend) @@ -712,8 +727,8 @@ def _read_graph(cls, b, r): vertex_count = r.to_object(b, DataType.int, False) for _ in range(vertex_count): v_id = r.read_object(b) - v_label = r.to_object(b, DataType.list, False)[0] - vertex = Vertex(v_id, v_label) + v_labels = r.to_object(b, DataType.list, False) + vertex = Vertex(v_id, v_labels[0] if v_labels else "vertex", labels=v_labels) graph.vertices[v_id] = vertex vp_count = r.to_object(b, DataType.int, False) @@ -732,14 +747,15 @@ def _read_graph(cls, b, r): edge_count = r.to_object(b, DataType.int, False) for _ in range(edge_count): e_id = r.read_object(b) - e_label = r.to_object(b, DataType.list, False)[0] + e_labels = r.to_object(b, DataType.list, False) in_v_id = r.read_object(b) r.read_object(b) # discard in-v label out_v_id = r.read_object(b) r.read_object(b) # discard out-v label r.read_object(b) # discard parent - edge = Edge(e_id, graph.vertices[out_v_id], e_label, graph.vertices[in_v_id]) + edge = Edge(e_id, graph.vertices[out_v_id], e_labels[0] if e_labels else "edge", + graph.vertices[in_v_id], labels=e_labels) graph.edges[e_id] = edge edge_props = r.to_object(b, DataType.list, False) @@ -758,8 +774,11 @@ class VertexIO(_GraphBinaryTypeIO): def dictify(cls, obj, writer, to_extend, as_value=False, nullable=True): cls.prefix_bytes(cls.graphbinary_type, as_value, nullable, to_extend) writer.to_dict(obj.id, to_extend) - # serializing label as list here for now according to GraphBinaryV4 - ListIO.dictify([obj.label], writer, to_extend, True, False) + # serializing labels as list according to GraphBinaryV4 + if hasattr(obj, '_labels') and obj._labels: + ListIO.dictify(list(obj._labels), writer, to_extend, True, False) + else: + ListIO.dictify([obj.label], writer, to_extend, True, False) to_extend.extend(NULL_BYTES) return to_extend @@ -770,12 +789,12 @@ def objectify(cls, buff, reader, nullable=True): @classmethod def _read_vertex(cls, b, r): vertex_id = r.read_object(b) - # reading single string value for now according to GraphBinaryV4 - vertex_label = r.to_object(b, DataType.list, False)[0] + # reading label list according to GraphBinaryV4 + vertex_labels = r.to_object(b, DataType.list, False) props = r.read_object(b) # null properties are returned as empty lists properties = [] if props is None else props - vertex = Vertex(vertex_id, vertex_label, properties) + vertex = Vertex(vertex_id, vertex_labels[0] if vertex_labels else "vertex", properties, labels=vertex_labels) return vertex diff --git a/gremlin-python/src/main/python/tests/feature/feature_steps.py b/gremlin-python/src/main/python/tests/feature/feature_steps.py index f40c3fa91e2..58456a26ee7 100644 --- a/gremlin-python/src/main/python/tests/feature/feature_steps.py +++ b/gremlin-python/src/main/python/tests/feature/feature_steps.py @@ -93,8 +93,18 @@ def choose_graph(step, graph_name): if (step.context.ignore): return - step.context.graph_name = graph_name - step.context.g = traversal().with_(step.context.remote_conn[graph_name]).with_('language', 'gremlin-lang') + # Multi-label tests use the gmultilabel traversal source for empty graphs + is_multilabel = "MultiLabel" in tagset + is_multilabel_default = "MultiLabelDefault" in tagset + if is_multilabel_default and graph_name == "empty": + step.context.graph_name = "multilabel" + step.context.g = traversal().with_(step.context.remote_conn["multilabel"]).with_('language', 'gremlin-lang').with_('multilabel') + elif is_multilabel and graph_name == "empty": + step.context.graph_name = "multilabel" + step.context.g = traversal().with_(step.context.remote_conn["multilabel"]).with_('language', 'gremlin-lang') + else: + step.context.graph_name = graph_name + step.context.g = traversal().with_(step.context.remote_conn[graph_name]).with_('language', 'gremlin-lang') @given("the graph initializer of") @@ -455,6 +465,13 @@ def __find_cached_element(ctx, graph_name, identifier, element_type): cache = world.create_lookup_vp(ctx.remote_conn["empty"]) else: cache = world.create_lookup_e(ctx.remote_conn["empty"]) + elif graph_name == "multilabel": + if element_type == "v": + cache = world.create_lookup_v(ctx.remote_conn["multilabel"]) + elif element_type == "vp": + cache = world.create_lookup_vp(ctx.remote_conn["multilabel"]) + else: + cache = world.create_lookup_e(ctx.remote_conn["multilabel"]) else: if element_type == "v": cache = ctx.lookup_v[graph_name] diff --git a/gremlin-python/src/main/python/tests/feature/gremlin.py b/gremlin-python/src/main/python/tests/feature/gremlin.py index e111d52b734..bc9870371ed 100644 --- a/gremlin-python/src/main/python/tests/feature/gremlin.py +++ b/gremlin-python/src/main/python/tests/feature/gremlin.py @@ -931,6 +931,8 @@ 'g_V_limitX3X_addVXsoftwareX_aggregateXa1X_byXlabelX_aggregateXa2X_byXlabelX_capXa1_a2X_selectXa_bX_byXunfoldX_foldX': [(lambda g:g.add_v('person').property('name', 'marko').property('age', 29).as_('marko').add_v('person').property('name', 'vadas').property('age', 27).as_('vadas').add_v('software').property('name', 'lop').property('lang', 'java').as_('lop').add_v('person').property('name', 'josh').property('age', 32).as_('josh').add_v('software').property('name', 'ripple').property('lang', 'java').as_('ripple').add_v('person').property('name', 'peter').property('age', 35).as_('peter').add_e('knows').from_('marko').to('vadas').property('weight', 0.5).add_e('knows').from_('marko').to('josh').property('weight', 1.0).add_e('created').from_('marko').to('lop').property('weight', 0.4).add_e('created').from_('josh').to('ripple').property('weight', 1.0).add_e('created').from_('josh').to('lop').property('weight', 0.4).add_e('created').from_('peter').to('lop').property('weight', 0.2)), (lambda g:g.V().limit(3).add_v('software').aggregate('a1').by(T.label).aggregate('a2').by(T.label).cap('a1', 'a2').select('a1', 'a2').by(__.unfold().fold()))], 'g_addV_propertyXname_markoX_withXkey_valueX_valuesXname_keyX': [(lambda g:g.add_v().property('name', 'marko').with_('key', 'value').values('name', 'key'))], 'g_addV_propertyXname_marko_since_2010X_withXkey_valueX_propertiesXnameX_valuesXsince_keyX': [(lambda g:g.add_v().property('name', 'marko', 'since', 2010).with_('key', 'value').properties('name').values('since', 'key'))], + 'g_addVXa_bX_labels': [(lambda g:g.add_v('a', 'b').labels().fold()), (lambda g:g.V().has_label('a').has_label('b'))], + 'g_addVXa_b_cX_labels_count': [(lambda g:g.add_v('a', 'b', 'c').labels().count())], 'g_injectX1X_asBool': [(lambda g:g.inject(1).as_bool())], 'g_injectX3_14X_asBool': [(lambda g:g.inject(3.14).as_bool())], 'g_injectXneg_1X_asBool': [(lambda g:g.inject(-1).as_bool())], @@ -1171,10 +1173,15 @@ 'g_E_properties_element': [(lambda g:g.E().properties().element())], 'g_VXv7_properties_properties_element_element': [(lambda g, vid7=None:g.V(vid7).properties().properties().element().element().limit(1))], 'g_V_properties_properties_element_element': [(lambda g, vid7=None:g.V(vid7).properties().properties().element().element())], - 'g_V_elementMap': [(lambda g:g.V().element_map())], - 'g_V_elementMapXname_ageX': [(lambda g:g.V().element_map('name', 'age'))], - 'g_EX11X_elementMap': [(lambda g, eid11=None:g.E(eid11).element_map())], - 'g_V_elementMapXname_age_nullX': [(lambda g:g.V().element_map('name', 'age', None))], + 'g_V_elementMap': [(lambda g:g.with_('singlelabel').V().element_map())], + 'g_V_elementMapXname_ageX': [(lambda g:g.with_('singlelabel').V().element_map('name', 'age'))], + 'g_EX11X_elementMap': [(lambda g, eid11=None:g.with_('singlelabel').E(eid11).element_map())], + 'g_V_elementMapXname_age_nullX': [(lambda g:g.with_('singlelabel').V().element_map('name', 'age', None))], + 'g_withXmultilabelX_VXmarkoX_elementMap': [(lambda g:g.with_('multilabel').V().has('name', 'marko').element_map())], + 'g_V_elementMap_single_label_default': [(lambda g:g.add_v('person').add_label('employee').property('name', 'marko')), (lambda g:g.V().element_map())], + 'g_withXmultilabelX_V_elementMap_multilabel': [(lambda g:g.add_v('person').add_label('employee').property('name', 'marko')), (lambda g:g.with_('multilabel').V().element_map())], + 'g_V_elementMap_multi_label_default': [(lambda g:g.add_v('person').add_label('employee').property('name', 'marko')), (lambda g:g.V().element_map())], + 'g_withXsinglelabelX_V_elementMap_multi_label_default': [(lambda g:g.add_v('person').add_label('employee').property('name', 'marko')), (lambda g:g.with_('singlelabel').V().element_map())], 'g_V_asXaX_flatMapXselectXaXX': [(lambda g:g.V().as_('a').flat_map(__.select('a')))], 'g_V_valuesXnameX_flatMapXsplitXaX_unfoldX': [(lambda g:g.V().values('name').flat_map(__.split('a').unfold()))], 'g_V_flatMapXout_outX_path': [(lambda g:g.V().flat_map(__.out().out()).path())], @@ -1225,6 +1232,11 @@ 'g_injectXListX1_2XX_lTrimXlocalX': [(lambda g:g.inject([1, 2]).l_trim(Scope.local))], 'g_V_valuesXnameX_lTrim': [(lambda g:g.add_v('person').property('name', ' marko ').property('age', 29).as_('marko').add_v('person').property('name', ' vadas ').property('age', 27).as_('vadas').add_v('software').property('name', ' lop').property('lang', 'java').as_('lop').add_v('person').property('name', 'josh ').property('age', 32).as_('josh').add_v('software').property('name', ' ripple ').property('lang', 'java').as_('ripple').add_v('person').property('name', 'peter').property('age', 35).as_('peter').add_e('knows').from_('marko').to('vadas').property('weight', 0.5).add_e('knows').from_('marko').to('josh').property('weight', 1.0).add_e('created').from_('marko').to('lop').property('weight', 0.4).add_e('created').from_('josh').to('ripple').property('weight', 1.0).add_e('created').from_('josh').to('lop').property('weight', 0.4).add_e('created').from_('peter').to('lop').property('weight', 0.2)), (lambda g:g.V().values('name').l_trim())], 'g_V_valuesXnameX_order_fold_lTrimXlocalX': [(lambda g:g.add_v('person').property('name', ' marko ').property('age', 29).as_('marko').add_v('person').property('name', ' vadas ').property('age', 27).as_('vadas').add_v('software').property('name', ' lop').property('lang', 'java').as_('lop').add_v('person').property('name', 'josh ').property('age', 32).as_('josh').add_v('software').property('name', ' ripple ').property('lang', 'java').as_('ripple').add_v('person').property('name', 'peter').property('age', 35).as_('peter').add_e('knows').from_('marko').to('vadas').property('weight', 0.5).add_e('knows').from_('marko').to('josh').property('weight', 1.0).add_e('created').from_('marko').to('lop').property('weight', 0.4).add_e('created').from_('josh').to('ripple').property('weight', 1.0).add_e('created').from_('josh').to('lop').property('weight', 0.4).add_e('created').from_('peter').to('lop').property('weight', 0.2)), (lambda g:g.V().values('name').order().fold().l_trim(Scope.local))], + 'g_V_hasLabelXpersonX_labels': [(lambda g:g.V().has_label('person').labels())], + 'g_V_labels_multilabel': [(lambda g:g.add_v('a', 'b')), (lambda g:g.V().labels())], + 'g_addVXa_bX_labels_count': [(lambda g:g.add_v('a', 'b')), (lambda g:g.V().labels().count())], + 'g_addV_labels': [(lambda g:g.add_v()), (lambda g:g.V().labels())], + 'g_E_labels': [(lambda g:g.E().has_label('knows').labels())], 'g_injectXfeature_test_nullX_length': [(lambda g:g.inject('feature', 'test', None).length())], 'g_injectXfeature_test_nullX_lengthXlocalX': [(lambda g:g.inject('feature', 'test', None).length(Scope.local))], 'g_injectXListXa_bXX_length': [(lambda g:g.inject(['a', 'b']).length())], @@ -1491,6 +1503,15 @@ 'g_mergeV_hidden_label_key_onMatch_matched_prohibited': [(lambda g, xx1=None:g.merge_v({ }).option(Merge.on_match, xx1))], 'g_injectXlist1_list2_list3X_fold_asXmX_mergeVXselectXmX_limitXlocal_1X_unfoldX_optionXonCreate_selectXmX_rangeXlocal_1_2X_unfoldX_optionXonMatch_selectXmX_tailXlocalX_unfoldX_to_match': [(lambda g:g.add_v('person').property('name', 'marko').property('age', 29)), (lambda g:g.inject({ T.label: 'person', 'name': 'marko' }, { T.label: 'person', 'name': 'marko' }, { 'created': 'N' }).fold().as_('m').merge_v(__.select('m').limit(Scope.local, 1).unfold()).option(Merge.on_create, __.select('m').range_(Scope.local, 1, 2).unfold()).option(Merge.on_match, __.select('m').tail(Scope.local).unfold())), (lambda g:g.V().has('person', 'name', 'marko').has('created', 'N')), (lambda g:g.V())], 'g_injectXlist1_list2_list3X_fold_asXmX_mergeVXselectXmX_limitXlocal_1X_unfoldX_optionXonCreate_selectXmX_rangeXlocal_1_2X_unfoldX_optionXonMatch_selectXmX_tailXlocalX_unfoldX_to_create': [(lambda g:g.add_v('person').property('name', 'marko').property('age', 29)), (lambda g:g.inject({ T.label: 'person', 'name': 'stephen' }, { T.label: 'person', 'name': 'stephen' }, { 'created': 'N' }).fold().as_('m').merge_v(__.select('m').limit(Scope.local, 1).unfold()).option(Merge.on_create, __.select('m').range_(Scope.local, 1, 2).unfold()).option(Merge.on_match, __.select('m').tail(Scope.local).unfold())), (lambda g:g.V().has('person', 'name', 'stephen').has_not('created')), (lambda g:g.V())], + 'g_mergeVXlabel_ab_name_markoX_multilabel_create': [(lambda g:g.merge_v({ T.label: ['person', 'employee'], 'name': 'marko' })), (lambda g:g.V()), (lambda g:g.V().has_label('person').has_label('employee')), (lambda g:g.V().has('name', 'marko'))], + 'g_mergeVXlabel_abc_name_testX_multilabel_create': [(lambda g:g.merge_v({ T.label: ['a', 'b', 'c'], 'name': 'test' })), (lambda g:g.V()), (lambda g:g.V().has_label('a').has_label('b').has_label('c'))], + 'g_mergeVXlabel_ab_name_markoX_multilabel_match': [(lambda g:g.add_v('person').add_label('employee').property('name', 'marko')), (lambda g:g.merge_v({ T.label: ['person', 'employee'], 'name': 'marko' })), (lambda g:g.V()), (lambda g:g.V().has_label('person').has_label('employee'))], + 'g_mergeVXlabel_ab_name_markoX_multilabel_nomatch': [(lambda g:g.add_v('person').property('name', 'marko')), (lambda g:g.merge_v({ T.label: ['person', 'employee'], 'name': 'marko' })), (lambda g:g.V())], + 'g_mergeVXlabel_person_name_markoX_optionXonMatch_label_managerX': [(lambda g:g.add_v('person').add_label('employee').property('name', 'marko')), (lambda g:g.merge_v({ T.label: 'person', 'name': 'marko' }).option(Merge.on_match, { T.label: 'manager' })), (lambda g:g.V()), (lambda g:g.V().has_label('manager')), (lambda g:g.V().has_label('person')), (lambda g:g.V().has_label('employee'))], + 'g_mergeVXlabel_person_name_markoX_optionXonMatch_label_manager_directorX': [(lambda g:g.add_v('person').property('name', 'marko')), (lambda g:g.merge_v({ T.label: 'person', 'name': 'marko' }).option(Merge.on_match, { T.label: ['manager', 'director'] })), (lambda g:g.V()), (lambda g:g.V().has_label('manager').has_label('director')), (lambda g:g.V().has_label('person'))], + 'g_mergeVXlabel_person_name_markoX_optionXonMatch_label_emptyX': [(lambda g:g.add_v('person').property('name', 'marko')), (lambda g:g.merge_v({ T.label: 'person', 'name': 'marko' }).option(Merge.on_match, { T.label: [] })), (lambda g:g.V()), (lambda g:g.V().has_label('person'))], + 'g_mergeVXname_markoX_optionXonCreate_label_ab_name_markoX': [(lambda g:g.merge_v({ 'name': 'marko' }).option(Merge.on_create, { T.label: ['person', 'employee'], 'name': 'marko' })), (lambda g:g.V()), (lambda g:g.V().has_label('person').has_label('employee')), (lambda g:g.V().has('name', 'marko'))], + 'g_mergeVXlabel_person_name_markoX_optionXonMatch_label_existingX': [(lambda g:g.add_v('person').add_label('employee').property('name', 'marko')), (lambda g:g.merge_v({ T.label: 'person', 'name': 'marko' }).option(Merge.on_match, { T.label: 'person' })), (lambda g:g.V()), (lambda g:g.V().has_label('person').has_label('employee'))], 'g_V_age_min': [(lambda g:g.V().values('age').min_())], 'g_V_foo_min': [(lambda g:g.V().values('foo').min_())], 'g_V_name_min': [(lambda g:g.V().values('name').min_())], @@ -1780,19 +1801,24 @@ 'g_V_valueMap_unfold_mapXselectXkeysXX': [(lambda g:g.V().value_map().unfold().map(__.select(Column.keys)))], 'g_VX1X_repeatXboth_simplePathX_untilXhasIdX6XX_path_byXnameX_unfold': [(lambda g, vid6=None,vid1=None:g.V(vid1).repeat(__.both().simple_path()).until(__.has_id(vid6)).path().by('name').unfold())], 'g_V_valueMap': [(lambda g:g.V().value_map())], - 'g_V_valueMapXtrueX': [(lambda g:g.V().value_map(True))], - 'g_V_valueMap_withXtokensX': [(lambda g:g.V().value_map().with_(WithOptions.tokens))], + 'g_V_valueMapXtrueX': [(lambda g:g.with_('singlelabel').V().value_map(True))], + 'g_V_valueMap_withXtokensX': [(lambda g:g.with_('singlelabel').V().value_map().with_(WithOptions.tokens))], 'g_V_valueMapXname_ageX': [(lambda g:g.V().value_map('name', 'age'))], - 'g_V_valueMapXtrue_name_ageX': [(lambda g:g.V().value_map(True, 'name', 'age'))], - 'g_V_valueMapXname_ageX_withXtokensX': [(lambda g:g.V().value_map('name', 'age').with_(WithOptions.tokens))], - 'g_V_valueMapXname_ageX_withXtokens_labelsX_byXunfoldX': [(lambda g:g.V().value_map('name', 'age').with_(WithOptions.tokens, WithOptions.labels).by(__.unfold()))], + 'g_V_valueMapXtrue_name_ageX': [(lambda g:g.with_('singlelabel').V().value_map(True, 'name', 'age'))], + 'g_V_valueMapXname_ageX_withXtokensX': [(lambda g:g.with_('singlelabel').V().value_map('name', 'age').with_(WithOptions.tokens))], + 'g_V_valueMapXname_ageX_withXtokens_labelsX_byXunfoldX': [(lambda g:g.with_('singlelabel').V().value_map('name', 'age').with_(WithOptions.tokens, WithOptions.labels).by(__.unfold()))], 'g_V_valueMapXname_ageX_withXtokens_idsX_byXunfoldX': [(lambda g:g.V().value_map('name', 'age').with_(WithOptions.tokens, WithOptions.ids).by(__.unfold()))], 'g_VX1X_outXcreatedX_valueMap': [(lambda g, vid1=None:g.V(vid1).out('created').value_map())], - 'g_V_hasLabelXpersonX_filterXoutEXcreatedXX_valueMapXtrueX': [(lambda g:g.V().has_label('person').filter_(__.out_e('created')).value_map(True))], - 'g_V_hasLabelXpersonX_filterXoutEXcreatedXX_valueMap_withXtokensX': [(lambda g:g.V().has_label('person').filter_(__.out_e('created')).value_map().with_(WithOptions.tokens))], + 'g_V_hasLabelXpersonX_filterXoutEXcreatedXX_valueMapXtrueX': [(lambda g:g.with_('singlelabel').V().has_label('person').filter_(__.out_e('created')).value_map(True))], + 'g_V_hasLabelXpersonX_filterXoutEXcreatedXX_valueMap_withXtokensX': [(lambda g:g.with_('singlelabel').V().has_label('person').filter_(__.out_e('created')).value_map().with_(WithOptions.tokens))], 'g_VX1X_valueMapXname_locationX_byXunfoldX_by': [(lambda g, vid1=None:g.V(vid1).value_map('name', 'location').by(__.unfold()).by())], 'g_V_valueMapXname_age_nullX': [(lambda g:g.V().value_map('name', 'age', None))], 'g_V_valueMapXname_ageX_byXisXxXXbyXunfoldX': [(lambda g:g.V().value_map('name', 'age').by(__.is_('x')).by(__.unfold()))], + 'g_withXmultilabelX_VXmarkoX_valueMap_withXtokensX': [(lambda g:g.with_('multilabel').V().has('name', 'marko').value_map().with_(WithOptions.tokens))], + 'g_V_valueMap_withXtokensX_single_label_default': [(lambda g:g.add_v('person').add_label('employee').property('name', 'marko')), (lambda g:g.V().value_map().with_(WithOptions.tokens))], + 'g_withXmultilabelX_V_valueMap_withXtokensX_multilabel': [(lambda g:g.add_v('person').add_label('employee').property('name', 'marko')), (lambda g:g.with_('multilabel').V().value_map().with_(WithOptions.tokens))], + 'g_V_valueMap_withXtokensX_multi_label_default': [(lambda g:g.add_v('person').add_label('employee').property('name', 'marko')), (lambda g:g.V().value_map().with_(WithOptions.tokens))], + 'g_withXsinglelabelX_V_valueMap_withXtokensX_multi_label_default': [(lambda g:g.add_v('person').add_label('employee').property('name', 'marko')), (lambda g:g.with_('singlelabel').V().value_map().with_(WithOptions.tokens))], 'g_VXnullX': [(lambda g:g.V(None))], 'g_VXlistXnullXX': [(lambda g, xx1=None:g.V(xx1))], 'g_VX1_nullX': [(lambda g, vid1=None:g.V(vid1, None))], @@ -1976,6 +2002,10 @@ 'g_V_out_out_properties_asXheadX_path_order_byXdescX_selectXheadX_value': [(lambda g:g.V().out().out().properties().as_('head').path().order().by(Order.desc).select('head').value())], 'g_V_out_out_values_asXheadX_path_order_byXascX_selectXheadX': [(lambda g:g.V().out().out().values().as_('head').path().order().by(Order.asc).select('head'))], 'g_V_out_out_values_asXheadX_path_order_byXdescX_selectXheadX': [(lambda g:g.V().out().out().values().as_('head').path().order().by(Order.desc).select('head'))], + 'g_V_hasLabelXpersonX_hasXname_markoX_addLabelXemployeeX_labels': [(lambda g:g.add_v('person').property('name', 'marko')), (lambda g:g.V().has_label('person').has('name', 'marko').add_label('employee').labels().fold()), (lambda g:g.V().has_label('person').has_label('employee'))], + 'g_V_addLabelXa_bX_labels_count': [(lambda g:g.add_v('person')), (lambda g:g.V().add_label('a', 'b').labels().count())], + 'g_V_addLabelXexistingX_labels_count': [(lambda g:g.add_v('person')), (lambda g:g.V().add_label('person').labels().count())], + 'g_E_addLabelXfriendX_labels_fold': [(lambda g:g.add_v('person').property('name', 'marko').as_('a').add_v('person').property('name', 'josh').as_('b').add_e('knows').from_('a').to('b')), (lambda g:g.E().add_label('friend').labels().fold())], 'g_V_valueXnameX_aggregateXxX_capXxX': [(lambda g:g.V().values('name').aggregate('x').cap('x'))], 'g_V_aggregateXxX_byXnameX_capXxX': [(lambda g:g.V().aggregate('x').by('name').cap('x'))], 'g_V_out_aggregateXaX_path': [(lambda g:g.V().out().aggregate('a').path())], @@ -2033,6 +2063,13 @@ 'g_V_repeatXaggregateXaXX_timesX2X_capXaX_unfold': [(lambda g:g.V().repeat(__.aggregate('a')).times(2).cap('a').unfold())], 'g_V_aggregateXaX_capXaX_unfold_both': [(lambda g:g.V().aggregate('a').cap('a').unfold().both())], 'g_V_aggregateXaX_capXaX_unfold_barrier_both': [(lambda g:g.V().aggregate('a').cap('a').unfold().barrier().both())], + 'g_V_dropLabelXaX_labels': [(lambda g:g.add_v('a', 'b')), (lambda g:g.V().drop_label('a').labels().fold()), (lambda g:g.V().has_label('a')), (lambda g:g.V().has_label('b'))], + 'g_V_dropLabels_labels': [(lambda g:g.add_v('a', 'b')), (lambda g:g.V().drop_labels().labels())], + 'g_V_dropLabelXa_bX_labels': [(lambda g:g.add_v('a', 'b', 'c')), (lambda g:g.V().drop_label('a', 'b').labels())], + 'g_V_dropLabels_defaultLabel': [(lambda g:g.add_v('person')), (lambda g:g.V().drop_labels().labels())], + 'g_E_dropLabelXknowsX_labels': [(lambda g:g.add_v('person').as_('a').add_v('person').as_('b').add_e('knows').from_('a').to('b')), (lambda g:g.E().drop_label('knows').labels().fold())], + 'g_E_dropLabels_labels': [(lambda g:g.add_v('person').as_('a').add_v('person').as_('b').add_e('knows').from_('a').to('b')), (lambda g:g.E().drop_labels().labels())], + 'g_V_dropLabelXnonExistentX_labels': [(lambda g:g.add_v('a', 'b')), (lambda g:g.V().drop_label('xyz').labels())], 'g_V_fail': [(lambda g:g.V().fail())], 'g_V_failXmsgX': [(lambda g:g.V().fail('msg'))], 'g_V_unionXout_failX': [(lambda g:g.V().union(__.out(), __.fail()))], diff --git a/gremlin-python/src/main/python/tests/feature/terrain.py b/gremlin-python/src/main/python/tests/feature/terrain.py index 781216bb639..55489c7807c 100644 --- a/gremlin-python/src/main/python/tests/feature/terrain.py +++ b/gremlin-python/src/main/python/tests/feature/terrain.py @@ -83,10 +83,23 @@ def prepare_traversal_source(scenario): g = traversal().with_(remote) g.V().drop().iterate() + # create a fresh remote for multi-label tests that need ZERO_OR_MORE vertex label cardinality + tagset = [tag.name for tag in scenario.all_tags] + if "MultiLabel" in tagset: + multilabel_remote = __create_remote("gmultilabel") + scenario.context.remote_conn["multilabel"] = multilabel_remote + scenario.context.lookup_v["multilabel"] = {} + scenario.context.lookup_e["multilabel"] = {} + scenario.context.lookup_vp["multilabel"] = {} + gml = traversal().with_(multilabel_remote) + gml.V().drop().iterate() + @after.each_scenario def close_traversal_source(scenario): scenario.context.remote_conn["empty"].close() + if "multilabel" in scenario.context.remote_conn: + scenario.context.remote_conn["multilabel"].close() @after.all diff --git a/gremlin-server/conf/tinkergraph-multilabel.properties b/gremlin-server/conf/tinkergraph-multilabel.properties new file mode 100644 index 00000000000..54391e8c5f7 --- /dev/null +++ b/gremlin-server/conf/tinkergraph-multilabel.properties @@ -0,0 +1,21 @@ +# 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. +gremlin.graph=org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerGraph +gremlin.tinkergraph.vertexIdManager=INTEGER +gremlin.tinkergraph.edgeIdManager=INTEGER +gremlin.tinkergraph.vertexPropertyIdManager=LONG +gremlin.tinkergraph.vertexLabelCardinality=ZERO_OR_MORE diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/language/translator/translations.json b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/language/translator/translations.json index f152691348e..762d4cb00d1 100644 --- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/language/translator/translations.json +++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/language/translator/translations.json @@ -19290,6 +19290,52 @@ } ] }, + { + "scenario": "g_addVXa_bX_labels", + "traversals": [ + { + "original": "g.addV(\"a\", \"b\").labels().fold()", + "language": "g.addV(\"a\", \"b\").labels().fold()", + "canonical": "g.addV(\"a\", \"b\").labels().fold()", + "anonymized": "g.addV(string0, string1).labels().fold()", + "dotnet": "g.AddV((string) \"a\", (string) \"b\").Labels().Fold()", + "go": "g.AddV(\"a\", \"b\").Labels().Fold()", + "groovy": "g.addV(\"a\", \"b\").labels().fold()", + "java": "g.addV(\"a\", \"b\").labels().fold()", + "javascript": "g.addV(\"a\", \"b\").labels().fold()", + "python": "g.add_v('a', 'b').labels().fold()" + }, + { + "original": "g.V().hasLabel(\"a\").hasLabel(\"b\")", + "language": "g.V().hasLabel(\"a\").hasLabel(\"b\")", + "canonical": "g.V().hasLabel(\"a\").hasLabel(\"b\")", + "anonymized": "g.V().hasLabel(string0).hasLabel(string1)", + "dotnet": "g.V().HasLabel(\"a\").HasLabel(\"b\")", + "go": "g.V().HasLabel(\"a\").HasLabel(\"b\")", + "groovy": "g.V().hasLabel(\"a\").hasLabel(\"b\")", + "java": "g.V().hasLabel(\"a\").hasLabel(\"b\")", + "javascript": "g.V().hasLabel(\"a\").hasLabel(\"b\")", + "python": "g.V().has_label('a').has_label('b')" + } + ] + }, + { + "scenario": "g_addVXa_b_cX_labels_count", + "traversals": [ + { + "original": "g.addV(\"a\", \"b\", \"c\").labels().count()", + "language": "g.addV(\"a\", \"b\", \"c\").labels().count()", + "canonical": "g.addV(\"a\", \"b\", \"c\").labels().count()", + "anonymized": "g.addV(string0, string1, string2).labels().count()", + "dotnet": "g.AddV((string) \"a\", (string) \"b\", (string) \"c\").Labels().Count()", + "go": "g.AddV(\"a\", \"b\", \"c\").Labels().Count()", + "groovy": "g.addV(\"a\", \"b\", \"c\").labels().count()", + "java": "g.addV(\"a\", \"b\", \"c\").labels().count()", + "javascript": "g.addV(\"a\", \"b\", \"c\").labels().count()", + "python": "g.add_v('a', 'b', 'c').labels().count()" + } + ] + }, { "scenario": "g_injectX1X_asBool", "traversals": [ @@ -23457,6 +23503,103 @@ { "scenario": "g_V_elementMap", "traversals": [ + { + "original": "g.with(\"singlelabel\").V().elementMap()", + "language": "g.with(\"singlelabel\").V().elementMap()", + "canonical": "g.with(\"singlelabel\").V().elementMap()", + "anonymized": "g.with(string0).V().elementMap()", + "dotnet": "g.With(\"singlelabel\").V().ElementMap()", + "go": "g.With(\"singlelabel\").V().ElementMap()", + "groovy": "g.with(\"singlelabel\").V().elementMap()", + "java": "g.with(\"singlelabel\").V().elementMap()", + "javascript": "g.with_(\"singlelabel\").V().elementMap()", + "python": "g.with_('singlelabel').V().element_map()" + } + ] + }, + { + "scenario": "g_V_elementMapXname_ageX", + "traversals": [ + { + "original": "g.with(\"singlelabel\").V().elementMap(\"name\", \"age\")", + "language": "g.with(\"singlelabel\").V().elementMap(\"name\", \"age\")", + "canonical": "g.with(\"singlelabel\").V().elementMap(\"name\", \"age\")", + "anonymized": "g.with(string0).V().elementMap(string1, string2)", + "dotnet": "g.With(\"singlelabel\").V().ElementMap(\"name\", \"age\")", + "go": "g.With(\"singlelabel\").V().ElementMap(\"name\", \"age\")", + "groovy": "g.with(\"singlelabel\").V().elementMap(\"name\", \"age\")", + "java": "g.with(\"singlelabel\").V().elementMap(\"name\", \"age\")", + "javascript": "g.with_(\"singlelabel\").V().elementMap(\"name\", \"age\")", + "python": "g.with_('singlelabel').V().element_map('name', 'age')" + } + ] + }, + { + "scenario": "g_EX11X_elementMap", + "traversals": [ + { + "original": "g.with(\"singlelabel\").E(eid11).elementMap()", + "language": "g.with(\"singlelabel\").E(eid11).elementMap()", + "canonical": "g.with(\"singlelabel\").E(eid11).elementMap()", + "anonymized": "g.with(string0).E(eid11).elementMap()", + "dotnet": "g.With(\"singlelabel\").E(eid11).ElementMap()", + "go": "g.With(\"singlelabel\").E(eid11).ElementMap()", + "groovy": "g.with(\"singlelabel\").E(eid11).elementMap()", + "java": "g.with(\"singlelabel\").E(eid11).elementMap()", + "javascript": "g.with_(\"singlelabel\").E(eid11).elementMap()", + "python": "g.with_('singlelabel').E(eid11).element_map()" + } + ] + }, + { + "scenario": "g_V_elementMapXname_age_nullX", + "traversals": [ + { + "original": "g.with(\"singlelabel\").V().elementMap(\"name\", \"age\", null)", + "language": "g.with(\"singlelabel\").V().elementMap(\"name\", \"age\", null)", + "canonical": "g.with(\"singlelabel\").V().elementMap(\"name\", \"age\", null)", + "anonymized": "g.with(string0).V().elementMap(string1, string2, string3)", + "dotnet": "g.With(\"singlelabel\").V().ElementMap(\"name\", \"age\", null)", + "go": "g.With(\"singlelabel\").V().ElementMap(\"name\", \"age\", nil)", + "groovy": "g.with(\"singlelabel\").V().elementMap(\"name\", \"age\", null)", + "java": "g.with(\"singlelabel\").V().elementMap(\"name\", \"age\", null)", + "javascript": "g.with_(\"singlelabel\").V().elementMap(\"name\", \"age\", null)", + "python": "g.with_('singlelabel').V().element_map('name', 'age', None)" + } + ] + }, + { + "scenario": "g_withXmultilabelX_VXmarkoX_elementMap", + "traversals": [ + { + "original": "g.with(\"multilabel\").V().has(\"name\", \"marko\").elementMap()", + "language": "g.with(\"multilabel\").V().has(\"name\", \"marko\").elementMap()", + "canonical": "g.with(\"multilabel\").V().has(\"name\", \"marko\").elementMap()", + "anonymized": "g.with(string0).V().has(string1, string2).elementMap()", + "dotnet": "g.With(\"multilabel\").V().Has(\"name\", \"marko\").ElementMap()", + "go": "g.With(\"multilabel\").V().Has(\"name\", \"marko\").ElementMap()", + "groovy": "g.with(\"multilabel\").V().has(\"name\", \"marko\").elementMap()", + "java": "g.with(\"multilabel\").V().has(\"name\", \"marko\").elementMap()", + "javascript": "g.with_(\"multilabel\").V().has(\"name\", \"marko\").elementMap()", + "python": "g.with_('multilabel').V().has('name', 'marko').element_map()" + } + ] + }, + { + "scenario": "g_V_elementMap_single_label_default", + "traversals": [ + { + "original": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "language": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "canonical": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "anonymized": "g.addV(string0).addLabel(string1).property(string2, string3)", + "dotnet": "g.AddV((string) \"person\").AddLabel(\"employee\").Property(\"name\", \"marko\")", + "go": "g.AddV(\"person\").AddLabel(\"employee\").Property(\"name\", \"marko\")", + "groovy": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "java": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "javascript": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "python": "g.add_v('person').add_label('employee').property('name', 'marko')" + }, { "original": "g.V().elementMap()", "language": "g.V().elementMap()", @@ -23472,53 +23615,89 @@ ] }, { - "scenario": "g_V_elementMapXname_ageX", + "scenario": "g_withXmultilabelX_V_elementMap_multilabel", "traversals": [ { - "original": "g.V().elementMap(\"name\", \"age\")", - "language": "g.V().elementMap(\"name\", \"age\")", - "canonical": "g.V().elementMap(\"name\", \"age\")", - "anonymized": "g.V().elementMap(string0, string1)", - "dotnet": "g.V().ElementMap(\"name\", \"age\")", - "go": "g.V().ElementMap(\"name\", \"age\")", - "groovy": "g.V().elementMap(\"name\", \"age\")", - "java": "g.V().elementMap(\"name\", \"age\")", - "javascript": "g.V().elementMap(\"name\", \"age\")", - "python": "g.V().element_map('name', 'age')" + "original": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "language": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "canonical": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "anonymized": "g.addV(string0).addLabel(string1).property(string2, string3)", + "dotnet": "g.AddV((string) \"person\").AddLabel(\"employee\").Property(\"name\", \"marko\")", + "go": "g.AddV(\"person\").AddLabel(\"employee\").Property(\"name\", \"marko\")", + "groovy": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "java": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "javascript": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "python": "g.add_v('person').add_label('employee').property('name', 'marko')" + }, + { + "original": "g.with(\"multilabel\").V().elementMap()", + "language": "g.with(\"multilabel\").V().elementMap()", + "canonical": "g.with(\"multilabel\").V().elementMap()", + "anonymized": "g.with(string0).V().elementMap()", + "dotnet": "g.With(\"multilabel\").V().ElementMap()", + "go": "g.With(\"multilabel\").V().ElementMap()", + "groovy": "g.with(\"multilabel\").V().elementMap()", + "java": "g.with(\"multilabel\").V().elementMap()", + "javascript": "g.with_(\"multilabel\").V().elementMap()", + "python": "g.with_('multilabel').V().element_map()" } ] }, { - "scenario": "g_EX11X_elementMap", + "scenario": "g_V_elementMap_multi_label_default", "traversals": [ { - "original": "g.E(eid11).elementMap()", - "language": "g.E(eid11).elementMap()", - "canonical": "g.E(eid11).elementMap()", - "anonymized": "g.E(eid11).elementMap()", - "dotnet": "g.E(eid11).ElementMap()", - "go": "g.E(eid11).ElementMap()", - "groovy": "g.E(eid11).elementMap()", - "java": "g.E(eid11).elementMap()", - "javascript": "g.E(eid11).elementMap()", - "python": "g.E(eid11).element_map()" + "original": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "language": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "canonical": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "anonymized": "g.addV(string0).addLabel(string1).property(string2, string3)", + "dotnet": "g.AddV((string) \"person\").AddLabel(\"employee\").Property(\"name\", \"marko\")", + "go": "g.AddV(\"person\").AddLabel(\"employee\").Property(\"name\", \"marko\")", + "groovy": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "java": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "javascript": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "python": "g.add_v('person').add_label('employee').property('name', 'marko')" + }, + { + "original": "g.V().elementMap()", + "language": "g.V().elementMap()", + "canonical": "g.V().elementMap()", + "anonymized": "g.V().elementMap()", + "dotnet": "g.V().ElementMap()", + "go": "g.V().ElementMap()", + "groovy": "g.V().elementMap()", + "java": "g.V().elementMap()", + "javascript": "g.V().elementMap()", + "python": "g.V().element_map()" } ] }, { - "scenario": "g_V_elementMapXname_age_nullX", + "scenario": "g_withXsinglelabelX_V_elementMap_multi_label_default", "traversals": [ { - "original": "g.V().elementMap(\"name\", \"age\", null)", - "language": "g.V().elementMap(\"name\", \"age\", null)", - "canonical": "g.V().elementMap(\"name\", \"age\", null)", - "anonymized": "g.V().elementMap(string0, string1, string2)", - "dotnet": "g.V().ElementMap(\"name\", \"age\", null)", - "go": "g.V().ElementMap(\"name\", \"age\", nil)", - "groovy": "g.V().elementMap(\"name\", \"age\", null)", - "java": "g.V().elementMap(\"name\", \"age\", null)", - "javascript": "g.V().elementMap(\"name\", \"age\", null)", - "python": "g.V().element_map('name', 'age', None)" + "original": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "language": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "canonical": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "anonymized": "g.addV(string0).addLabel(string1).property(string2, string3)", + "dotnet": "g.AddV((string) \"person\").AddLabel(\"employee\").Property(\"name\", \"marko\")", + "go": "g.AddV(\"person\").AddLabel(\"employee\").Property(\"name\", \"marko\")", + "groovy": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "java": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "javascript": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "python": "g.add_v('person').add_label('employee').property('name', 'marko')" + }, + { + "original": "g.with(\"singlelabel\").V().elementMap()", + "language": "g.with(\"singlelabel\").V().elementMap()", + "canonical": "g.with(\"singlelabel\").V().elementMap()", + "anonymized": "g.with(string0).V().elementMap()", + "dotnet": "g.With(\"singlelabel\").V().ElementMap()", + "go": "g.With(\"singlelabel\").V().ElementMap()", + "groovy": "g.with(\"singlelabel\").V().elementMap()", + "java": "g.with(\"singlelabel\").V().elementMap()", + "javascript": "g.with_(\"singlelabel\").V().elementMap()", + "python": "g.with_('singlelabel').V().element_map()" } ] }, @@ -24396,6 +24575,127 @@ } ] }, + { + "scenario": "g_V_hasLabelXpersonX_labels", + "traversals": [ + { + "original": "g.V().hasLabel(\"person\").labels()", + "language": "g.V().hasLabel(\"person\").labels()", + "canonical": "g.V().hasLabel(\"person\").labels()", + "anonymized": "g.V().hasLabel(string0).labels()", + "dotnet": "g.V().HasLabel(\"person\").Labels()", + "go": "g.V().HasLabel(\"person\").Labels()", + "groovy": "g.V().hasLabel(\"person\").labels()", + "java": "g.V().hasLabel(\"person\").labels()", + "javascript": "g.V().hasLabel(\"person\").labels()", + "python": "g.V().has_label('person').labels()" + } + ] + }, + { + "scenario": "g_V_labels_multilabel", + "traversals": [ + { + "original": "g.addV(\"a\", \"b\")", + "language": "g.addV(\"a\", \"b\")", + "canonical": "g.addV(\"a\", \"b\")", + "anonymized": "g.addV(string0, string1)", + "dotnet": "g.AddV((string) \"a\", (string) \"b\")", + "go": "g.AddV(\"a\", \"b\")", + "groovy": "g.addV(\"a\", \"b\")", + "java": "g.addV(\"a\", \"b\")", + "javascript": "g.addV(\"a\", \"b\")", + "python": "g.add_v('a', 'b')" + }, + { + "original": "g.V().labels()", + "language": "g.V().labels()", + "canonical": "g.V().labels()", + "anonymized": "g.V().labels()", + "dotnet": "g.V().Labels()", + "go": "g.V().Labels()", + "groovy": "g.V().labels()", + "java": "g.V().labels()", + "javascript": "g.V().labels()", + "python": "g.V().labels()" + } + ] + }, + { + "scenario": "g_addVXa_bX_labels_count", + "traversals": [ + { + "original": "g.addV(\"a\", \"b\")", + "language": "g.addV(\"a\", \"b\")", + "canonical": "g.addV(\"a\", \"b\")", + "anonymized": "g.addV(string0, string1)", + "dotnet": "g.AddV((string) \"a\", (string) \"b\")", + "go": "g.AddV(\"a\", \"b\")", + "groovy": "g.addV(\"a\", \"b\")", + "java": "g.addV(\"a\", \"b\")", + "javascript": "g.addV(\"a\", \"b\")", + "python": "g.add_v('a', 'b')" + }, + { + "original": "g.V().labels().count()", + "language": "g.V().labels().count()", + "canonical": "g.V().labels().count()", + "anonymized": "g.V().labels().count()", + "dotnet": "g.V().Labels().Count()", + "go": "g.V().Labels().Count()", + "groovy": "g.V().labels().count()", + "java": "g.V().labels().count()", + "javascript": "g.V().labels().count()", + "python": "g.V().labels().count()" + } + ] + }, + { + "scenario": "g_addV_labels", + "traversals": [ + { + "original": "g.addV()", + "language": "g.addV()", + "canonical": "g.addV()", + "anonymized": "g.addV()", + "dotnet": "g.AddV()", + "go": "g.AddV()", + "groovy": "g.addV()", + "java": "g.addV()", + "javascript": "g.addV()", + "python": "g.add_v()" + }, + { + "original": "g.V().labels()", + "language": "g.V().labels()", + "canonical": "g.V().labels()", + "anonymized": "g.V().labels()", + "dotnet": "g.V().Labels()", + "go": "g.V().Labels()", + "groovy": "g.V().labels()", + "java": "g.V().labels()", + "javascript": "g.V().labels()", + "python": "g.V().labels()" + } + ] + }, + { + "scenario": "g_E_labels", + "traversals": [ + { + "original": "g.E().hasLabel(\"knows\").labels()", + "language": "g.E().hasLabel(\"knows\").labels()", + "canonical": "g.E().hasLabel(\"knows\").labels()", + "anonymized": "g.E().hasLabel(string0).labels()", + "dotnet": "g.E().HasLabel(\"knows\").Labels()", + "go": "g.E().HasLabel(\"knows\").Labels()", + "groovy": "g.E().hasLabel(\"knows\").labels()", + "java": "g.E().hasLabel(\"knows\").labels()", + "javascript": "g.E().hasLabel(\"knows\").labels()", + "python": "g.E().has_label('knows').labels()" + } + ] + }, { "scenario": "g_injectXfeature_test_nullX_length", "traversals": [ @@ -32675,108 +32975,597 @@ ] }, { - "scenario": "g_V_age_min", + "scenario": "g_mergeVXlabel_ab_name_markoX_multilabel_create", "traversals": [ { - "original": "g.V().values(\"age\").min()", - "language": "g.V().values(\"age\").min()", - "canonical": "g.V().values(\"age\").min()", - "anonymized": "g.V().values(string0).min()", - "dotnet": "g.V().Values(\"age\").Min()", - "go": "g.V().Values(\"age\").Min()", - "groovy": "g.V().values(\"age\").min()", - "java": "g.V().values(\"age\").min()", - "javascript": "g.V().values(\"age\").min()", - "python": "g.V().values('age').min_()" - } - ] - }, - { - "scenario": "g_V_foo_min", - "traversals": [ + "original": "g.mergeV([(T.label): [\"person\",\"employee\"], name: \"marko\"])", + "language": "g.mergeV([(T.label):[\"person\", \"employee\"], name:\"marko\"])", + "canonical": "g.mergeV([(T.label):[\"person\", \"employee\"], name:\"marko\"])", + "anonymized": "g.mergeV(map0)", + "dotnet": "g.MergeV((IDictionary) new Dictionary {{ T.Label, new List { \"person\", \"employee\" } }, { \"name\", \"marko\" }})", + "go": "g.MergeV(map[interface{}]interface{}{gremlingo.T.Label: []interface{}{\"person\", \"employee\"}, \"name\": \"marko\" })", + "groovy": "g.mergeV([(T.label):[\"person\", \"employee\"], name:\"marko\"])", + "java": "g.mergeV(new LinkedHashMap() {{ put(T.label, new ArrayList() {{ add(\"person\"); add(\"employee\"); }}); put(\"name\", \"marko\"); }})", + "javascript": "g.mergeV(new Map([[T.label, [\"person\", \"employee\"]], [\"name\", \"marko\"]]))", + "python": "g.merge_v({ T.label: ['person', 'employee'], 'name': 'marko' })" + }, { - "original": "g.V().values(\"foo\").min()", - "language": "g.V().values(\"foo\").min()", - "canonical": "g.V().values(\"foo\").min()", - "anonymized": "g.V().values(string0).min()", - "dotnet": "g.V().Values(\"foo\").Min()", - "go": "g.V().Values(\"foo\").Min()", - "groovy": "g.V().values(\"foo\").min()", - "java": "g.V().values(\"foo\").min()", - "javascript": "g.V().values(\"foo\").min()", - "python": "g.V().values('foo').min_()" + "original": "g.V()", + "language": "g.V()", + "canonical": "g.V()", + "anonymized": "g.V()", + "dotnet": "g.V()", + "go": "g.V()", + "groovy": "g.V()", + "java": "g.V()", + "javascript": "g.V()", + "python": "g.V()" + }, + { + "original": "g.V().hasLabel(\"person\").hasLabel(\"employee\")", + "language": "g.V().hasLabel(\"person\").hasLabel(\"employee\")", + "canonical": "g.V().hasLabel(\"person\").hasLabel(\"employee\")", + "anonymized": "g.V().hasLabel(string0).hasLabel(string1)", + "dotnet": "g.V().HasLabel(\"person\").HasLabel(\"employee\")", + "go": "g.V().HasLabel(\"person\").HasLabel(\"employee\")", + "groovy": "g.V().hasLabel(\"person\").hasLabel(\"employee\")", + "java": "g.V().hasLabel(\"person\").hasLabel(\"employee\")", + "javascript": "g.V().hasLabel(\"person\").hasLabel(\"employee\")", + "python": "g.V().has_label('person').has_label('employee')" + }, + { + "original": "g.V().has(\"name\",\"marko\")", + "language": "g.V().has(\"name\", \"marko\")", + "canonical": "g.V().has(\"name\", \"marko\")", + "anonymized": "g.V().has(string0, string1)", + "dotnet": "g.V().Has(\"name\", \"marko\")", + "go": "g.V().Has(\"name\", \"marko\")", + "groovy": "g.V().has(\"name\", \"marko\")", + "java": "g.V().has(\"name\", \"marko\")", + "javascript": "g.V().has(\"name\", \"marko\")", + "python": "g.V().has('name', 'marko')" } ] }, { - "scenario": "g_V_name_min", + "scenario": "g_mergeVXlabel_abc_name_testX_multilabel_create", "traversals": [ { - "original": "g.V().values(\"name\").min()", - "language": "g.V().values(\"name\").min()", - "canonical": "g.V().values(\"name\").min()", - "anonymized": "g.V().values(string0).min()", - "dotnet": "g.V().Values(\"name\").Min()", - "go": "g.V().Values(\"name\").Min()", - "groovy": "g.V().values(\"name\").min()", - "java": "g.V().values(\"name\").min()", - "javascript": "g.V().values(\"name\").min()", - "python": "g.V().values('name').min_()" + "original": "g.mergeV([(T.label): [\"a\",\"b\",\"c\"], name: \"test\"])", + "language": "g.mergeV([(T.label):[\"a\", \"b\", \"c\"], name:\"test\"])", + "canonical": "g.mergeV([(T.label):[\"a\", \"b\", \"c\"], name:\"test\"])", + "anonymized": "g.mergeV(map0)", + "dotnet": "g.MergeV((IDictionary) new Dictionary {{ T.Label, new List { \"a\", \"b\", \"c\" } }, { \"name\", \"test\" }})", + "go": "g.MergeV(map[interface{}]interface{}{gremlingo.T.Label: []interface{}{\"a\", \"b\", \"c\"}, \"name\": \"test\" })", + "groovy": "g.mergeV([(T.label):[\"a\", \"b\", \"c\"], name:\"test\"])", + "java": "g.mergeV(new LinkedHashMap() {{ put(T.label, new ArrayList() {{ add(\"a\"); add(\"b\"); add(\"c\"); }}); put(\"name\", \"test\"); }})", + "javascript": "g.mergeV(new Map([[T.label, [\"a\", \"b\", \"c\"]], [\"name\", \"test\"]]))", + "python": "g.merge_v({ T.label: ['a', 'b', 'c'], 'name': 'test' })" + }, + { + "original": "g.V()", + "language": "g.V()", + "canonical": "g.V()", + "anonymized": "g.V()", + "dotnet": "g.V()", + "go": "g.V()", + "groovy": "g.V()", + "java": "g.V()", + "javascript": "g.V()", + "python": "g.V()" + }, + { + "original": "g.V().hasLabel(\"a\").hasLabel(\"b\").hasLabel(\"c\")", + "language": "g.V().hasLabel(\"a\").hasLabel(\"b\").hasLabel(\"c\")", + "canonical": "g.V().hasLabel(\"a\").hasLabel(\"b\").hasLabel(\"c\")", + "anonymized": "g.V().hasLabel(string0).hasLabel(string1).hasLabel(string2)", + "dotnet": "g.V().HasLabel(\"a\").HasLabel(\"b\").HasLabel(\"c\")", + "go": "g.V().HasLabel(\"a\").HasLabel(\"b\").HasLabel(\"c\")", + "groovy": "g.V().hasLabel(\"a\").hasLabel(\"b\").hasLabel(\"c\")", + "java": "g.V().hasLabel(\"a\").hasLabel(\"b\").hasLabel(\"c\")", + "javascript": "g.V().hasLabel(\"a\").hasLabel(\"b\").hasLabel(\"c\")", + "python": "g.V().has_label('a').has_label('b').has_label('c')" } ] }, { - "scenario": "g_V_age_fold_minXlocalX", + "scenario": "g_mergeVXlabel_ab_name_markoX_multilabel_match", "traversals": [ { - "original": "g.V().values(\"age\").fold().min(Scope.local)", - "language": "g.V().values(\"age\").fold().min(Scope.local)", - "canonical": "g.V().values(\"age\").fold().min(Scope.local)", - "anonymized": "g.V().values(string0).fold().min(Scope.local)", - "dotnet": "g.V().Values(\"age\").Fold().Min(Scope.Local)", - "go": "g.V().Values(\"age\").Fold().Min(gremlingo.Scope.Local)", - "groovy": "g.V().values(\"age\").fold().min(Scope.local)", - "java": "g.V().values(\"age\").fold().min(Scope.local)", - "javascript": "g.V().values(\"age\").fold().min(Scope.local)", - "python": "g.V().values('age').fold().min_(Scope.local)" + "original": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "language": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "canonical": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "anonymized": "g.addV(string0).addLabel(string1).property(string2, string3)", + "dotnet": "g.AddV((string) \"person\").AddLabel(\"employee\").Property(\"name\", \"marko\")", + "go": "g.AddV(\"person\").AddLabel(\"employee\").Property(\"name\", \"marko\")", + "groovy": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "java": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "javascript": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "python": "g.add_v('person').add_label('employee').property('name', 'marko')" + }, + { + "original": "g.mergeV([(T.label): [\"person\",\"employee\"], name: \"marko\"])", + "language": "g.mergeV([(T.label):[\"person\", \"employee\"], name:\"marko\"])", + "canonical": "g.mergeV([(T.label):[\"person\", \"employee\"], name:\"marko\"])", + "anonymized": "g.mergeV(map0)", + "dotnet": "g.MergeV((IDictionary) new Dictionary {{ T.Label, new List { \"person\", \"employee\" } }, { \"name\", \"marko\" }})", + "go": "g.MergeV(map[interface{}]interface{}{gremlingo.T.Label: []interface{}{\"person\", \"employee\"}, \"name\": \"marko\" })", + "groovy": "g.mergeV([(T.label):[\"person\", \"employee\"], name:\"marko\"])", + "java": "g.mergeV(new LinkedHashMap() {{ put(T.label, new ArrayList() {{ add(\"person\"); add(\"employee\"); }}); put(\"name\", \"marko\"); }})", + "javascript": "g.mergeV(new Map([[T.label, [\"person\", \"employee\"]], [\"name\", \"marko\"]]))", + "python": "g.merge_v({ T.label: ['person', 'employee'], 'name': 'marko' })" + }, + { + "original": "g.V()", + "language": "g.V()", + "canonical": "g.V()", + "anonymized": "g.V()", + "dotnet": "g.V()", + "go": "g.V()", + "groovy": "g.V()", + "java": "g.V()", + "javascript": "g.V()", + "python": "g.V()" + }, + { + "original": "g.V().hasLabel(\"person\").hasLabel(\"employee\")", + "language": "g.V().hasLabel(\"person\").hasLabel(\"employee\")", + "canonical": "g.V().hasLabel(\"person\").hasLabel(\"employee\")", + "anonymized": "g.V().hasLabel(string0).hasLabel(string1)", + "dotnet": "g.V().HasLabel(\"person\").HasLabel(\"employee\")", + "go": "g.V().HasLabel(\"person\").HasLabel(\"employee\")", + "groovy": "g.V().hasLabel(\"person\").hasLabel(\"employee\")", + "java": "g.V().hasLabel(\"person\").hasLabel(\"employee\")", + "javascript": "g.V().hasLabel(\"person\").hasLabel(\"employee\")", + "python": "g.V().has_label('person').has_label('employee')" } ] }, { - "scenario": "g_V_aggregateXaX_byXageX_capXaX_minXlocalX", + "scenario": "g_mergeVXlabel_ab_name_markoX_multilabel_nomatch", "traversals": [ { - "original": "g.V().aggregate(\"a\").by(\"age\").cap(\"a\").min(Scope.local)", - "language": "g.V().aggregate(\"a\").by(\"age\").cap(\"a\").min(Scope.local)", - "canonical": "g.V().aggregate(\"a\").by(\"age\").cap(\"a\").min(Scope.local)", - "anonymized": "g.V().aggregate(string0).by(string1).cap(string0).min(Scope.local)", - "dotnet": "g.V().Aggregate(\"a\").By(\"age\").Cap(\"a\").Min(Scope.Local)", - "go": "g.V().Aggregate(\"a\").By(\"age\").Cap(\"a\").Min(gremlingo.Scope.Local)", - "groovy": "g.V().aggregate(\"a\").by(\"age\").cap(\"a\").min(Scope.local)", - "java": "g.V().aggregate(\"a\").by(\"age\").cap(\"a\").min(Scope.local)", - "javascript": "g.V().aggregate(\"a\").by(\"age\").cap(\"a\").min(Scope.local)", - "python": "g.V().aggregate('a').by('age').cap('a').min_(Scope.local)" + "original": "g.addV(\"person\").property(\"name\", \"marko\")", + "language": "g.addV(\"person\").property(\"name\", \"marko\")", + "canonical": "g.addV(\"person\").property(\"name\", \"marko\")", + "anonymized": "g.addV(string0).property(string1, string2)", + "dotnet": "g.AddV((string) \"person\").Property(\"name\", \"marko\")", + "go": "g.AddV(\"person\").Property(\"name\", \"marko\")", + "groovy": "g.addV(\"person\").property(\"name\", \"marko\")", + "java": "g.addV(\"person\").property(\"name\", \"marko\")", + "javascript": "g.addV(\"person\").property(\"name\", \"marko\")", + "python": "g.add_v('person').property('name', 'marko')" + }, + { + "original": "g.mergeV([(T.label): [\"person\",\"employee\"], name: \"marko\"])", + "language": "g.mergeV([(T.label):[\"person\", \"employee\"], name:\"marko\"])", + "canonical": "g.mergeV([(T.label):[\"person\", \"employee\"], name:\"marko\"])", + "anonymized": "g.mergeV(map0)", + "dotnet": "g.MergeV((IDictionary) new Dictionary {{ T.Label, new List { \"person\", \"employee\" } }, { \"name\", \"marko\" }})", + "go": "g.MergeV(map[interface{}]interface{}{gremlingo.T.Label: []interface{}{\"person\", \"employee\"}, \"name\": \"marko\" })", + "groovy": "g.mergeV([(T.label):[\"person\", \"employee\"], name:\"marko\"])", + "java": "g.mergeV(new LinkedHashMap() {{ put(T.label, new ArrayList() {{ add(\"person\"); add(\"employee\"); }}); put(\"name\", \"marko\"); }})", + "javascript": "g.mergeV(new Map([[T.label, [\"person\", \"employee\"]], [\"name\", \"marko\"]]))", + "python": "g.merge_v({ T.label: ['person', 'employee'], 'name': 'marko' })" + }, + { + "original": "g.V()", + "language": "g.V()", + "canonical": "g.V()", + "anonymized": "g.V()", + "dotnet": "g.V()", + "go": "g.V()", + "groovy": "g.V()", + "java": "g.V()", + "javascript": "g.V()", + "python": "g.V()" } ] }, { - "scenario": "g_withStrategiesXProductiveByStrategyX_V_aggregateXaX_byXageX_capXaX_minXlocalX", + "scenario": "g_mergeVXlabel_person_name_markoX_optionXonMatch_label_managerX", "traversals": [ { - "original": "g.withStrategies(ProductiveByStrategy).V().aggregate(\"a\").by(\"age\").cap(\"a\").min(Scope.local)", - "language": "g.withStrategies(ProductiveByStrategy).V().aggregate(\"a\").by(\"age\").cap(\"a\").min(Scope.local)", - "canonical": "g.withStrategies(ProductiveByStrategy).V().aggregate(\"a\").by(\"age\").cap(\"a\").min(Scope.local)", - "anonymized": "g.withStrategies(ProductiveByStrategy).V().aggregate(string0).by(string1).cap(string0).min(Scope.local)", - "dotnet": "g.WithStrategies(new ProductiveByStrategy()).V().Aggregate(\"a\").By(\"age\").Cap(\"a\").Min(Scope.Local)", - "go": "g.WithStrategies(gremlingo.ProductiveByStrategy()).V().Aggregate(\"a\").By(\"age\").Cap(\"a\").Min(gremlingo.Scope.Local)", - "groovy": "g.withStrategies(ProductiveByStrategy).V().aggregate(\"a\").by(\"age\").cap(\"a\").min(Scope.local)", - "java": "g.withStrategies(ProductiveByStrategy.instance()).V().aggregate(\"a\").by(\"age\").cap(\"a\").min(Scope.local)", - "javascript": "g.withStrategies(new ProductiveByStrategy()).V().aggregate(\"a\").by(\"age\").cap(\"a\").min(Scope.local)", - "python": "g.with_strategies(ProductiveByStrategy()).V().aggregate('a').by('age').cap('a').min_(Scope.local)" - } - ] - }, - { + "original": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "language": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "canonical": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "anonymized": "g.addV(string0).addLabel(string1).property(string2, string3)", + "dotnet": "g.AddV((string) \"person\").AddLabel(\"employee\").Property(\"name\", \"marko\")", + "go": "g.AddV(\"person\").AddLabel(\"employee\").Property(\"name\", \"marko\")", + "groovy": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "java": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "javascript": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "python": "g.add_v('person').add_label('employee').property('name', 'marko')" + }, + { + "original": "g.mergeV([(T.label): \"person\", name: \"marko\"]).option(Merge.onMatch, [(T.label): \"manager\"])", + "language": "g.mergeV([(T.label):\"person\", name:\"marko\"]).option(Merge.onMatch, [(T.label):\"manager\"])", + "canonical": "g.mergeV([(T.label):\"person\", name:\"marko\"]).option(Merge.onMatch, [(T.label):\"manager\"])", + "anonymized": "g.mergeV(map0).option(Merge.onMatch, map1)", + "dotnet": "g.MergeV((IDictionary) new Dictionary {{ T.Label, \"person\" }, { \"name\", \"marko\" }}).Option(Merge.OnMatch, (IDictionary) new Dictionary {{ T.Label, \"manager\" }})", + "go": "g.MergeV(map[interface{}]interface{}{gremlingo.T.Label: \"person\", \"name\": \"marko\" }).Option(gremlingo.Merge.OnMatch, map[interface{}]interface{}{gremlingo.T.Label: \"manager\" })", + "groovy": "g.mergeV([(T.label):\"person\", name:\"marko\"]).option(Merge.onMatch, [(T.label):\"manager\"])", + "java": "g.mergeV(new LinkedHashMap() {{ put(T.label, \"person\"); put(\"name\", \"marko\"); }}).option(Merge.onMatch, new LinkedHashMap() {{ put(T.label, \"manager\"); }})", + "javascript": "g.mergeV(new Map([[T.label, \"person\"], [\"name\", \"marko\"]])).option(Merge.onMatch, new Map([[T.label, \"manager\"]]))", + "python": "g.merge_v({ T.label: 'person', 'name': 'marko' }).option(Merge.on_match, { T.label: 'manager' })" + }, + { + "original": "g.V()", + "language": "g.V()", + "canonical": "g.V()", + "anonymized": "g.V()", + "dotnet": "g.V()", + "go": "g.V()", + "groovy": "g.V()", + "java": "g.V()", + "javascript": "g.V()", + "python": "g.V()" + }, + { + "original": "g.V().hasLabel(\"manager\")", + "language": "g.V().hasLabel(\"manager\")", + "canonical": "g.V().hasLabel(\"manager\")", + "anonymized": "g.V().hasLabel(string0)", + "dotnet": "g.V().HasLabel(\"manager\")", + "go": "g.V().HasLabel(\"manager\")", + "groovy": "g.V().hasLabel(\"manager\")", + "java": "g.V().hasLabel(\"manager\")", + "javascript": "g.V().hasLabel(\"manager\")", + "python": "g.V().has_label('manager')" + }, + { + "original": "g.V().hasLabel(\"person\")", + "language": "g.V().hasLabel(\"person\")", + "canonical": "g.V().hasLabel(\"person\")", + "anonymized": "g.V().hasLabel(string0)", + "dotnet": "g.V().HasLabel(\"person\")", + "go": "g.V().HasLabel(\"person\")", + "groovy": "g.V().hasLabel(\"person\")", + "java": "g.V().hasLabel(\"person\")", + "javascript": "g.V().hasLabel(\"person\")", + "python": "g.V().has_label('person')" + }, + { + "original": "g.V().hasLabel(\"employee\")", + "language": "g.V().hasLabel(\"employee\")", + "canonical": "g.V().hasLabel(\"employee\")", + "anonymized": "g.V().hasLabel(string0)", + "dotnet": "g.V().HasLabel(\"employee\")", + "go": "g.V().HasLabel(\"employee\")", + "groovy": "g.V().hasLabel(\"employee\")", + "java": "g.V().hasLabel(\"employee\")", + "javascript": "g.V().hasLabel(\"employee\")", + "python": "g.V().has_label('employee')" + } + ] + }, + { + "scenario": "g_mergeVXlabel_person_name_markoX_optionXonMatch_label_manager_directorX", + "traversals": [ + { + "original": "g.addV(\"person\").property(\"name\", \"marko\")", + "language": "g.addV(\"person\").property(\"name\", \"marko\")", + "canonical": "g.addV(\"person\").property(\"name\", \"marko\")", + "anonymized": "g.addV(string0).property(string1, string2)", + "dotnet": "g.AddV((string) \"person\").Property(\"name\", \"marko\")", + "go": "g.AddV(\"person\").Property(\"name\", \"marko\")", + "groovy": "g.addV(\"person\").property(\"name\", \"marko\")", + "java": "g.addV(\"person\").property(\"name\", \"marko\")", + "javascript": "g.addV(\"person\").property(\"name\", \"marko\")", + "python": "g.add_v('person').property('name', 'marko')" + }, + { + "original": "g.mergeV([(T.label): \"person\", name: \"marko\"]).option(Merge.onMatch, [(T.label): [\"manager\",\"director\"]])", + "language": "g.mergeV([(T.label):\"person\", name:\"marko\"]).option(Merge.onMatch, [(T.label):[\"manager\", \"director\"]])", + "canonical": "g.mergeV([(T.label):\"person\", name:\"marko\"]).option(Merge.onMatch, [(T.label):[\"manager\", \"director\"]])", + "anonymized": "g.mergeV(map0).option(Merge.onMatch, map1)", + "dotnet": "g.MergeV((IDictionary) new Dictionary {{ T.Label, \"person\" }, { \"name\", \"marko\" }}).Option(Merge.OnMatch, (IDictionary) new Dictionary {{ T.Label, new List { \"manager\", \"director\" } }})", + "go": "g.MergeV(map[interface{}]interface{}{gremlingo.T.Label: \"person\", \"name\": \"marko\" }).Option(gremlingo.Merge.OnMatch, map[interface{}]interface{}{gremlingo.T.Label: []interface{}{\"manager\", \"director\"} })", + "groovy": "g.mergeV([(T.label):\"person\", name:\"marko\"]).option(Merge.onMatch, [(T.label):[\"manager\", \"director\"]])", + "java": "g.mergeV(new LinkedHashMap() {{ put(T.label, \"person\"); put(\"name\", \"marko\"); }}).option(Merge.onMatch, new LinkedHashMap() {{ put(T.label, new ArrayList() {{ add(\"manager\"); add(\"director\"); }}); }})", + "javascript": "g.mergeV(new Map([[T.label, \"person\"], [\"name\", \"marko\"]])).option(Merge.onMatch, new Map([[T.label, [\"manager\", \"director\"]]]))", + "python": "g.merge_v({ T.label: 'person', 'name': 'marko' }).option(Merge.on_match, { T.label: ['manager', 'director'] })" + }, + { + "original": "g.V()", + "language": "g.V()", + "canonical": "g.V()", + "anonymized": "g.V()", + "dotnet": "g.V()", + "go": "g.V()", + "groovy": "g.V()", + "java": "g.V()", + "javascript": "g.V()", + "python": "g.V()" + }, + { + "original": "g.V().hasLabel(\"manager\").hasLabel(\"director\")", + "language": "g.V().hasLabel(\"manager\").hasLabel(\"director\")", + "canonical": "g.V().hasLabel(\"manager\").hasLabel(\"director\")", + "anonymized": "g.V().hasLabel(string0).hasLabel(string1)", + "dotnet": "g.V().HasLabel(\"manager\").HasLabel(\"director\")", + "go": "g.V().HasLabel(\"manager\").HasLabel(\"director\")", + "groovy": "g.V().hasLabel(\"manager\").hasLabel(\"director\")", + "java": "g.V().hasLabel(\"manager\").hasLabel(\"director\")", + "javascript": "g.V().hasLabel(\"manager\").hasLabel(\"director\")", + "python": "g.V().has_label('manager').has_label('director')" + }, + { + "original": "g.V().hasLabel(\"person\")", + "language": "g.V().hasLabel(\"person\")", + "canonical": "g.V().hasLabel(\"person\")", + "anonymized": "g.V().hasLabel(string0)", + "dotnet": "g.V().HasLabel(\"person\")", + "go": "g.V().HasLabel(\"person\")", + "groovy": "g.V().hasLabel(\"person\")", + "java": "g.V().hasLabel(\"person\")", + "javascript": "g.V().hasLabel(\"person\")", + "python": "g.V().has_label('person')" + } + ] + }, + { + "scenario": "g_mergeVXlabel_person_name_markoX_optionXonMatch_label_emptyX", + "traversals": [ + { + "original": "g.addV(\"person\").property(\"name\", \"marko\")", + "language": "g.addV(\"person\").property(\"name\", \"marko\")", + "canonical": "g.addV(\"person\").property(\"name\", \"marko\")", + "anonymized": "g.addV(string0).property(string1, string2)", + "dotnet": "g.AddV((string) \"person\").Property(\"name\", \"marko\")", + "go": "g.AddV(\"person\").Property(\"name\", \"marko\")", + "groovy": "g.addV(\"person\").property(\"name\", \"marko\")", + "java": "g.addV(\"person\").property(\"name\", \"marko\")", + "javascript": "g.addV(\"person\").property(\"name\", \"marko\")", + "python": "g.add_v('person').property('name', 'marko')" + }, + { + "original": "g.mergeV([(T.label): \"person\", name: \"marko\"]).option(Merge.onMatch, [(T.label): []])", + "language": "g.mergeV([(T.label):\"person\", name:\"marko\"]).option(Merge.onMatch, [(T.label):[]])", + "canonical": "g.mergeV([(T.label):\"person\", name:\"marko\"]).option(Merge.onMatch, [(T.label):[]])", + "anonymized": "g.mergeV(map0).option(Merge.onMatch, map1)", + "dotnet": "g.MergeV((IDictionary) new Dictionary {{ T.Label, \"person\" }, { \"name\", \"marko\" }}).Option(Merge.OnMatch, (IDictionary) new Dictionary {{ T.Label, new List { } }})", + "go": "g.MergeV(map[interface{}]interface{}{gremlingo.T.Label: \"person\", \"name\": \"marko\" }).Option(gremlingo.Merge.OnMatch, map[interface{}]interface{}{gremlingo.T.Label: []interface{}{} })", + "groovy": "g.mergeV([(T.label):\"person\", name:\"marko\"]).option(Merge.onMatch, [(T.label):[]])", + "java": "g.mergeV(new LinkedHashMap() {{ put(T.label, \"person\"); put(\"name\", \"marko\"); }}).option(Merge.onMatch, new LinkedHashMap() {{ put(T.label, new ArrayList() {{ }}); }})", + "javascript": "g.mergeV(new Map([[T.label, \"person\"], [\"name\", \"marko\"]])).option(Merge.onMatch, new Map([[T.label, []]]))", + "python": "g.merge_v({ T.label: 'person', 'name': 'marko' }).option(Merge.on_match, { T.label: [] })" + }, + { + "original": "g.V()", + "language": "g.V()", + "canonical": "g.V()", + "anonymized": "g.V()", + "dotnet": "g.V()", + "go": "g.V()", + "groovy": "g.V()", + "java": "g.V()", + "javascript": "g.V()", + "python": "g.V()" + }, + { + "original": "g.V().hasLabel(\"person\")", + "language": "g.V().hasLabel(\"person\")", + "canonical": "g.V().hasLabel(\"person\")", + "anonymized": "g.V().hasLabel(string0)", + "dotnet": "g.V().HasLabel(\"person\")", + "go": "g.V().HasLabel(\"person\")", + "groovy": "g.V().hasLabel(\"person\")", + "java": "g.V().hasLabel(\"person\")", + "javascript": "g.V().hasLabel(\"person\")", + "python": "g.V().has_label('person')" + } + ] + }, + { + "scenario": "g_mergeVXname_markoX_optionXonCreate_label_ab_name_markoX", + "traversals": [ + { + "original": "g.mergeV([name: \"marko\"]).option(Merge.onCreate, [(T.label): [\"person\",\"employee\"], name: \"marko\"])", + "language": "g.mergeV([name:\"marko\"]).option(Merge.onCreate, [(T.label):[\"person\", \"employee\"], name:\"marko\"])", + "canonical": "g.mergeV([name:\"marko\"]).option(Merge.onCreate, [(T.label):[\"person\", \"employee\"], name:\"marko\"])", + "anonymized": "g.mergeV(map0).option(Merge.onCreate, map1)", + "dotnet": "g.MergeV((IDictionary) new Dictionary {{ \"name\", \"marko\" }}).Option(Merge.OnCreate, (IDictionary) new Dictionary {{ T.Label, new List { \"person\", \"employee\" } }, { \"name\", \"marko\" }})", + "go": "g.MergeV(map[interface{}]interface{}{\"name\": \"marko\" }).Option(gremlingo.Merge.OnCreate, map[interface{}]interface{}{gremlingo.T.Label: []interface{}{\"person\", \"employee\"}, \"name\": \"marko\" })", + "groovy": "g.mergeV([name:\"marko\"]).option(Merge.onCreate, [(T.label):[\"person\", \"employee\"], name:\"marko\"])", + "java": "g.mergeV(new LinkedHashMap() {{ put(\"name\", \"marko\"); }}).option(Merge.onCreate, new LinkedHashMap() {{ put(T.label, new ArrayList() {{ add(\"person\"); add(\"employee\"); }}); put(\"name\", \"marko\"); }})", + "javascript": "g.mergeV(new Map([[\"name\", \"marko\"]])).option(Merge.onCreate, new Map([[T.label, [\"person\", \"employee\"]], [\"name\", \"marko\"]]))", + "python": "g.merge_v({ 'name': 'marko' }).option(Merge.on_create, { T.label: ['person', 'employee'], 'name': 'marko' })" + }, + { + "original": "g.V()", + "language": "g.V()", + "canonical": "g.V()", + "anonymized": "g.V()", + "dotnet": "g.V()", + "go": "g.V()", + "groovy": "g.V()", + "java": "g.V()", + "javascript": "g.V()", + "python": "g.V()" + }, + { + "original": "g.V().hasLabel(\"person\").hasLabel(\"employee\")", + "language": "g.V().hasLabel(\"person\").hasLabel(\"employee\")", + "canonical": "g.V().hasLabel(\"person\").hasLabel(\"employee\")", + "anonymized": "g.V().hasLabel(string0).hasLabel(string1)", + "dotnet": "g.V().HasLabel(\"person\").HasLabel(\"employee\")", + "go": "g.V().HasLabel(\"person\").HasLabel(\"employee\")", + "groovy": "g.V().hasLabel(\"person\").hasLabel(\"employee\")", + "java": "g.V().hasLabel(\"person\").hasLabel(\"employee\")", + "javascript": "g.V().hasLabel(\"person\").hasLabel(\"employee\")", + "python": "g.V().has_label('person').has_label('employee')" + }, + { + "original": "g.V().has(\"name\",\"marko\")", + "language": "g.V().has(\"name\", \"marko\")", + "canonical": "g.V().has(\"name\", \"marko\")", + "anonymized": "g.V().has(string0, string1)", + "dotnet": "g.V().Has(\"name\", \"marko\")", + "go": "g.V().Has(\"name\", \"marko\")", + "groovy": "g.V().has(\"name\", \"marko\")", + "java": "g.V().has(\"name\", \"marko\")", + "javascript": "g.V().has(\"name\", \"marko\")", + "python": "g.V().has('name', 'marko')" + } + ] + }, + { + "scenario": "g_mergeVXlabel_person_name_markoX_optionXonMatch_label_existingX", + "traversals": [ + { + "original": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "language": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "canonical": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "anonymized": "g.addV(string0).addLabel(string1).property(string2, string3)", + "dotnet": "g.AddV((string) \"person\").AddLabel(\"employee\").Property(\"name\", \"marko\")", + "go": "g.AddV(\"person\").AddLabel(\"employee\").Property(\"name\", \"marko\")", + "groovy": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "java": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "javascript": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "python": "g.add_v('person').add_label('employee').property('name', 'marko')" + }, + { + "original": "g.mergeV([(T.label): \"person\", name: \"marko\"]).option(Merge.onMatch, [(T.label): \"person\"])", + "language": "g.mergeV([(T.label):\"person\", name:\"marko\"]).option(Merge.onMatch, [(T.label):\"person\"])", + "canonical": "g.mergeV([(T.label):\"person\", name:\"marko\"]).option(Merge.onMatch, [(T.label):\"person\"])", + "anonymized": "g.mergeV(map0).option(Merge.onMatch, map1)", + "dotnet": "g.MergeV((IDictionary) new Dictionary {{ T.Label, \"person\" }, { \"name\", \"marko\" }}).Option(Merge.OnMatch, (IDictionary) new Dictionary {{ T.Label, \"person\" }})", + "go": "g.MergeV(map[interface{}]interface{}{gremlingo.T.Label: \"person\", \"name\": \"marko\" }).Option(gremlingo.Merge.OnMatch, map[interface{}]interface{}{gremlingo.T.Label: \"person\" })", + "groovy": "g.mergeV([(T.label):\"person\", name:\"marko\"]).option(Merge.onMatch, [(T.label):\"person\"])", + "java": "g.mergeV(new LinkedHashMap() {{ put(T.label, \"person\"); put(\"name\", \"marko\"); }}).option(Merge.onMatch, new LinkedHashMap() {{ put(T.label, \"person\"); }})", + "javascript": "g.mergeV(new Map([[T.label, \"person\"], [\"name\", \"marko\"]])).option(Merge.onMatch, new Map([[T.label, \"person\"]]))", + "python": "g.merge_v({ T.label: 'person', 'name': 'marko' }).option(Merge.on_match, { T.label: 'person' })" + }, + { + "original": "g.V()", + "language": "g.V()", + "canonical": "g.V()", + "anonymized": "g.V()", + "dotnet": "g.V()", + "go": "g.V()", + "groovy": "g.V()", + "java": "g.V()", + "javascript": "g.V()", + "python": "g.V()" + }, + { + "original": "g.V().hasLabel(\"person\").hasLabel(\"employee\")", + "language": "g.V().hasLabel(\"person\").hasLabel(\"employee\")", + "canonical": "g.V().hasLabel(\"person\").hasLabel(\"employee\")", + "anonymized": "g.V().hasLabel(string0).hasLabel(string1)", + "dotnet": "g.V().HasLabel(\"person\").HasLabel(\"employee\")", + "go": "g.V().HasLabel(\"person\").HasLabel(\"employee\")", + "groovy": "g.V().hasLabel(\"person\").hasLabel(\"employee\")", + "java": "g.V().hasLabel(\"person\").hasLabel(\"employee\")", + "javascript": "g.V().hasLabel(\"person\").hasLabel(\"employee\")", + "python": "g.V().has_label('person').has_label('employee')" + } + ] + }, + { + "scenario": "g_V_age_min", + "traversals": [ + { + "original": "g.V().values(\"age\").min()", + "language": "g.V().values(\"age\").min()", + "canonical": "g.V().values(\"age\").min()", + "anonymized": "g.V().values(string0).min()", + "dotnet": "g.V().Values(\"age\").Min()", + "go": "g.V().Values(\"age\").Min()", + "groovy": "g.V().values(\"age\").min()", + "java": "g.V().values(\"age\").min()", + "javascript": "g.V().values(\"age\").min()", + "python": "g.V().values('age').min_()" + } + ] + }, + { + "scenario": "g_V_foo_min", + "traversals": [ + { + "original": "g.V().values(\"foo\").min()", + "language": "g.V().values(\"foo\").min()", + "canonical": "g.V().values(\"foo\").min()", + "anonymized": "g.V().values(string0).min()", + "dotnet": "g.V().Values(\"foo\").Min()", + "go": "g.V().Values(\"foo\").Min()", + "groovy": "g.V().values(\"foo\").min()", + "java": "g.V().values(\"foo\").min()", + "javascript": "g.V().values(\"foo\").min()", + "python": "g.V().values('foo').min_()" + } + ] + }, + { + "scenario": "g_V_name_min", + "traversals": [ + { + "original": "g.V().values(\"name\").min()", + "language": "g.V().values(\"name\").min()", + "canonical": "g.V().values(\"name\").min()", + "anonymized": "g.V().values(string0).min()", + "dotnet": "g.V().Values(\"name\").Min()", + "go": "g.V().Values(\"name\").Min()", + "groovy": "g.V().values(\"name\").min()", + "java": "g.V().values(\"name\").min()", + "javascript": "g.V().values(\"name\").min()", + "python": "g.V().values('name').min_()" + } + ] + }, + { + "scenario": "g_V_age_fold_minXlocalX", + "traversals": [ + { + "original": "g.V().values(\"age\").fold().min(Scope.local)", + "language": "g.V().values(\"age\").fold().min(Scope.local)", + "canonical": "g.V().values(\"age\").fold().min(Scope.local)", + "anonymized": "g.V().values(string0).fold().min(Scope.local)", + "dotnet": "g.V().Values(\"age\").Fold().Min(Scope.Local)", + "go": "g.V().Values(\"age\").Fold().Min(gremlingo.Scope.Local)", + "groovy": "g.V().values(\"age\").fold().min(Scope.local)", + "java": "g.V().values(\"age\").fold().min(Scope.local)", + "javascript": "g.V().values(\"age\").fold().min(Scope.local)", + "python": "g.V().values('age').fold().min_(Scope.local)" + } + ] + }, + { + "scenario": "g_V_aggregateXaX_byXageX_capXaX_minXlocalX", + "traversals": [ + { + "original": "g.V().aggregate(\"a\").by(\"age\").cap(\"a\").min(Scope.local)", + "language": "g.V().aggregate(\"a\").by(\"age\").cap(\"a\").min(Scope.local)", + "canonical": "g.V().aggregate(\"a\").by(\"age\").cap(\"a\").min(Scope.local)", + "anonymized": "g.V().aggregate(string0).by(string1).cap(string0).min(Scope.local)", + "dotnet": "g.V().Aggregate(\"a\").By(\"age\").Cap(\"a\").Min(Scope.Local)", + "go": "g.V().Aggregate(\"a\").By(\"age\").Cap(\"a\").Min(gremlingo.Scope.Local)", + "groovy": "g.V().aggregate(\"a\").by(\"age\").cap(\"a\").min(Scope.local)", + "java": "g.V().aggregate(\"a\").by(\"age\").cap(\"a\").min(Scope.local)", + "javascript": "g.V().aggregate(\"a\").by(\"age\").cap(\"a\").min(Scope.local)", + "python": "g.V().aggregate('a').by('age').cap('a').min_(Scope.local)" + } + ] + }, + { + "scenario": "g_withStrategiesXProductiveByStrategyX_V_aggregateXaX_byXageX_capXaX_minXlocalX", + "traversals": [ + { + "original": "g.withStrategies(ProductiveByStrategy).V().aggregate(\"a\").by(\"age\").cap(\"a\").min(Scope.local)", + "language": "g.withStrategies(ProductiveByStrategy).V().aggregate(\"a\").by(\"age\").cap(\"a\").min(Scope.local)", + "canonical": "g.withStrategies(ProductiveByStrategy).V().aggregate(\"a\").by(\"age\").cap(\"a\").min(Scope.local)", + "anonymized": "g.withStrategies(ProductiveByStrategy).V().aggregate(string0).by(string1).cap(string0).min(Scope.local)", + "dotnet": "g.WithStrategies(new ProductiveByStrategy()).V().Aggregate(\"a\").By(\"age\").Cap(\"a\").Min(Scope.Local)", + "go": "g.WithStrategies(gremlingo.ProductiveByStrategy()).V().Aggregate(\"a\").By(\"age\").Cap(\"a\").Min(gremlingo.Scope.Local)", + "groovy": "g.withStrategies(ProductiveByStrategy).V().aggregate(\"a\").by(\"age\").cap(\"a\").min(Scope.local)", + "java": "g.withStrategies(ProductiveByStrategy.instance()).V().aggregate(\"a\").by(\"age\").cap(\"a\").min(Scope.local)", + "javascript": "g.withStrategies(new ProductiveByStrategy()).V().aggregate(\"a\").by(\"age\").cap(\"a\").min(Scope.local)", + "python": "g.with_strategies(ProductiveByStrategy()).V().aggregate('a').by('age').cap('a').min_(Scope.local)" + } + ] + }, + { "scenario": "g_V_aggregateXaX_byXageX_capXaX_unfold_min", "traversals": [ { @@ -37735,16 +38524,16 @@ "scenario": "g_V_valueMapXtrueX", "traversals": [ { - "original": "g.V().valueMap(true)", - "language": "g.V().valueMap(true)", - "canonical": "g.V().valueMap(true)", - "anonymized": "g.V().valueMap(boolean0)", - "dotnet": "g.V().ValueMap(true)", - "go": "g.V().ValueMap(true)", - "groovy": "g.V().valueMap(true)", - "java": "g.V().valueMap(true)", - "javascript": "g.V().valueMap(true)", - "python": "g.V().value_map(True)" + "original": "g.with(\"singlelabel\").V().valueMap(true)", + "language": "g.with(\"singlelabel\").V().valueMap(true)", + "canonical": "g.with(\"singlelabel\").V().valueMap(true)", + "anonymized": "g.with(string0).V().valueMap(boolean0)", + "dotnet": "g.With(\"singlelabel\").V().ValueMap(true)", + "go": "g.With(\"singlelabel\").V().ValueMap(true)", + "groovy": "g.with(\"singlelabel\").V().valueMap(true)", + "java": "g.with(\"singlelabel\").V().valueMap(true)", + "javascript": "g.with_(\"singlelabel\").V().valueMap(true)", + "python": "g.with_('singlelabel').V().value_map(True)" } ] }, @@ -37752,16 +38541,16 @@ "scenario": "g_V_valueMap_withXtokensX", "traversals": [ { - "original": "g.V().valueMap().with(WithOptions.tokens)", - "language": "g.V().valueMap().with(WithOptions.tokens)", - "canonical": "g.V().valueMap().with(WithOptions.tokens)", - "anonymized": "g.V().valueMap().with(WithOptions.tokens)", - "dotnet": "g.V().ValueMap().With(WithOptions.Tokens)", - "go": "g.V().ValueMap().With(gremlingo.WithOptions.Tokens)", - "groovy": "g.V().valueMap().with(WithOptions.tokens)", - "java": "g.V().valueMap().with(WithOptions.tokens)", - "javascript": "g.V().valueMap().with_(WithOptions.tokens)", - "python": "g.V().value_map().with_(WithOptions.tokens)" + "original": "g.with(\"singlelabel\").V().valueMap().with(WithOptions.tokens)", + "language": "g.with(\"singlelabel\").V().valueMap().with(WithOptions.tokens)", + "canonical": "g.with(\"singlelabel\").V().valueMap().with(WithOptions.tokens)", + "anonymized": "g.with(string0).V().valueMap().with(WithOptions.tokens)", + "dotnet": "g.With(\"singlelabel\").V().ValueMap().With(WithOptions.Tokens)", + "go": "g.With(\"singlelabel\").V().ValueMap().With(gremlingo.WithOptions.Tokens)", + "groovy": "g.with(\"singlelabel\").V().valueMap().with(WithOptions.tokens)", + "java": "g.with(\"singlelabel\").V().valueMap().with(WithOptions.tokens)", + "javascript": "g.with_(\"singlelabel\").V().valueMap().with_(WithOptions.tokens)", + "python": "g.with_('singlelabel').V().value_map().with_(WithOptions.tokens)" } ] }, @@ -37786,16 +38575,16 @@ "scenario": "g_V_valueMapXtrue_name_ageX", "traversals": [ { - "original": "g.V().valueMap(true, \"name\", \"age\")", - "language": "g.V().valueMap(true, \"name\", \"age\")", - "canonical": "g.V().valueMap(true, \"name\", \"age\")", - "anonymized": "g.V().valueMap(boolean0, string0, string1)", - "dotnet": "g.V().ValueMap(true, \"name\", \"age\")", - "go": "g.V().ValueMap(true, \"name\", \"age\")", - "groovy": "g.V().valueMap(true, \"name\", \"age\")", - "java": "g.V().valueMap(true, \"name\", \"age\")", - "javascript": "g.V().valueMap(true, \"name\", \"age\")", - "python": "g.V().value_map(True, 'name', 'age')" + "original": "g.with(\"singlelabel\").V().valueMap(true, \"name\", \"age\")", + "language": "g.with(\"singlelabel\").V().valueMap(true, \"name\", \"age\")", + "canonical": "g.with(\"singlelabel\").V().valueMap(true, \"name\", \"age\")", + "anonymized": "g.with(string0).V().valueMap(boolean0, string1, string2)", + "dotnet": "g.With(\"singlelabel\").V().ValueMap(true, \"name\", \"age\")", + "go": "g.With(\"singlelabel\").V().ValueMap(true, \"name\", \"age\")", + "groovy": "g.with(\"singlelabel\").V().valueMap(true, \"name\", \"age\")", + "java": "g.with(\"singlelabel\").V().valueMap(true, \"name\", \"age\")", + "javascript": "g.with_(\"singlelabel\").V().valueMap(true, \"name\", \"age\")", + "python": "g.with_('singlelabel').V().value_map(True, 'name', 'age')" } ] }, @@ -37803,16 +38592,16 @@ "scenario": "g_V_valueMapXname_ageX_withXtokensX", "traversals": [ { - "original": "g.V().valueMap(\"name\", \"age\").with(WithOptions.tokens)", - "language": "g.V().valueMap(\"name\", \"age\").with(WithOptions.tokens)", - "canonical": "g.V().valueMap(\"name\", \"age\").with(WithOptions.tokens)", - "anonymized": "g.V().valueMap(string0, string1).with(WithOptions.tokens)", - "dotnet": "g.V().ValueMap(\"name\", \"age\").With(WithOptions.Tokens)", - "go": "g.V().ValueMap(\"name\", \"age\").With(gremlingo.WithOptions.Tokens)", - "groovy": "g.V().valueMap(\"name\", \"age\").with(WithOptions.tokens)", - "java": "g.V().valueMap(\"name\", \"age\").with(WithOptions.tokens)", - "javascript": "g.V().valueMap(\"name\", \"age\").with_(WithOptions.tokens)", - "python": "g.V().value_map('name', 'age').with_(WithOptions.tokens)" + "original": "g.with(\"singlelabel\").V().valueMap(\"name\", \"age\").with(WithOptions.tokens)", + "language": "g.with(\"singlelabel\").V().valueMap(\"name\", \"age\").with(WithOptions.tokens)", + "canonical": "g.with(\"singlelabel\").V().valueMap(\"name\", \"age\").with(WithOptions.tokens)", + "anonymized": "g.with(string0).V().valueMap(string1, string2).with(WithOptions.tokens)", + "dotnet": "g.With(\"singlelabel\").V().ValueMap(\"name\", \"age\").With(WithOptions.Tokens)", + "go": "g.With(\"singlelabel\").V().ValueMap(\"name\", \"age\").With(gremlingo.WithOptions.Tokens)", + "groovy": "g.with(\"singlelabel\").V().valueMap(\"name\", \"age\").with(WithOptions.tokens)", + "java": "g.with(\"singlelabel\").V().valueMap(\"name\", \"age\").with(WithOptions.tokens)", + "javascript": "g.with_(\"singlelabel\").V().valueMap(\"name\", \"age\").with_(WithOptions.tokens)", + "python": "g.with_('singlelabel').V().value_map('name', 'age').with_(WithOptions.tokens)" } ] }, @@ -37820,16 +38609,16 @@ "scenario": "g_V_valueMapXname_ageX_withXtokens_labelsX_byXunfoldX", "traversals": [ { - "original": "g.V().valueMap(\"name\", \"age\").with(WithOptions.tokens, WithOptions.labels).by(__.unfold())", - "language": "g.V().valueMap(\"name\", \"age\").with(WithOptions.tokens, WithOptions.labels).by(__.unfold())", - "canonical": "g.V().valueMap(\"name\", \"age\").with(WithOptions.tokens, WithOptions.labels).by(__.unfold())", - "anonymized": "g.V().valueMap(string0, string1).with(WithOptions.tokens, WithOptions.labels).by(__.unfold())", - "dotnet": "g.V().ValueMap(\"name\", \"age\").With(WithOptions.Tokens, WithOptions.Labels).By(__.Unfold())", - "go": "g.V().ValueMap(\"name\", \"age\").With(gremlingo.WithOptions.Tokens, gremlingo.WithOptions.Labels).By(gremlingo.T__.Unfold())", - "groovy": "g.V().valueMap(\"name\", \"age\").with(WithOptions.tokens, WithOptions.labels).by(__.unfold())", - "java": "g.V().valueMap(\"name\", \"age\").with(WithOptions.tokens, WithOptions.labels).by(__.unfold())", - "javascript": "g.V().valueMap(\"name\", \"age\").with_(WithOptions.tokens, WithOptions.labels).by(__.unfold())", - "python": "g.V().value_map('name', 'age').with_(WithOptions.tokens, WithOptions.labels).by(__.unfold())" + "original": "g.with(\"singlelabel\").V().valueMap(\"name\", \"age\").with(WithOptions.tokens, WithOptions.labels).by(__.unfold())", + "language": "g.with(\"singlelabel\").V().valueMap(\"name\", \"age\").with(WithOptions.tokens, WithOptions.labels).by(__.unfold())", + "canonical": "g.with(\"singlelabel\").V().valueMap(\"name\", \"age\").with(WithOptions.tokens, WithOptions.labels).by(__.unfold())", + "anonymized": "g.with(string0).V().valueMap(string1, string2).with(WithOptions.tokens, WithOptions.labels).by(__.unfold())", + "dotnet": "g.With(\"singlelabel\").V().ValueMap(\"name\", \"age\").With(WithOptions.Tokens, WithOptions.Labels).By(__.Unfold())", + "go": "g.With(\"singlelabel\").V().ValueMap(\"name\", \"age\").With(gremlingo.WithOptions.Tokens, gremlingo.WithOptions.Labels).By(gremlingo.T__.Unfold())", + "groovy": "g.with(\"singlelabel\").V().valueMap(\"name\", \"age\").with(WithOptions.tokens, WithOptions.labels).by(__.unfold())", + "java": "g.with(\"singlelabel\").V().valueMap(\"name\", \"age\").with(WithOptions.tokens, WithOptions.labels).by(__.unfold())", + "javascript": "g.with_(\"singlelabel\").V().valueMap(\"name\", \"age\").with_(WithOptions.tokens, WithOptions.labels).by(__.unfold())", + "python": "g.with_('singlelabel').V().value_map('name', 'age').with_(WithOptions.tokens, WithOptions.labels).by(__.unfold())" } ] }, @@ -37871,16 +38660,16 @@ "scenario": "g_V_hasLabelXpersonX_filterXoutEXcreatedXX_valueMapXtrueX", "traversals": [ { - "original": "g.V().hasLabel(\"person\").filter(__.outE(\"created\")).valueMap(true)", - "language": "g.V().hasLabel(\"person\").filter(__.outE(\"created\")).valueMap(true)", - "canonical": "g.V().hasLabel(\"person\").filter(__.outE(\"created\")).valueMap(true)", - "anonymized": "g.V().hasLabel(string0).filter(__.outE(string1)).valueMap(boolean0)", - "dotnet": "g.V().HasLabel(\"person\").Filter(__.OutE(\"created\")).ValueMap(true)", - "go": "g.V().HasLabel(\"person\").Filter(gremlingo.T__.OutE(\"created\")).ValueMap(true)", - "groovy": "g.V().hasLabel(\"person\").filter(__.outE(\"created\")).valueMap(true)", - "java": "g.V().hasLabel(\"person\").filter(__.outE(\"created\")).valueMap(true)", - "javascript": "g.V().hasLabel(\"person\").filter(__.outE(\"created\")).valueMap(true)", - "python": "g.V().has_label('person').filter_(__.out_e('created')).value_map(True)" + "original": "g.with(\"singlelabel\").V().hasLabel(\"person\").filter(__.outE(\"created\")).valueMap(true)", + "language": "g.with(\"singlelabel\").V().hasLabel(\"person\").filter(__.outE(\"created\")).valueMap(true)", + "canonical": "g.with(\"singlelabel\").V().hasLabel(\"person\").filter(__.outE(\"created\")).valueMap(true)", + "anonymized": "g.with(string0).V().hasLabel(string1).filter(__.outE(string2)).valueMap(boolean0)", + "dotnet": "g.With(\"singlelabel\").V().HasLabel(\"person\").Filter(__.OutE(\"created\")).ValueMap(true)", + "go": "g.With(\"singlelabel\").V().HasLabel(\"person\").Filter(gremlingo.T__.OutE(\"created\")).ValueMap(true)", + "groovy": "g.with(\"singlelabel\").V().hasLabel(\"person\").filter(__.outE(\"created\")).valueMap(true)", + "java": "g.with(\"singlelabel\").V().hasLabel(\"person\").filter(__.outE(\"created\")).valueMap(true)", + "javascript": "g.with_(\"singlelabel\").V().hasLabel(\"person\").filter(__.outE(\"created\")).valueMap(true)", + "python": "g.with_('singlelabel').V().has_label('person').filter_(__.out_e('created')).value_map(True)" } ] }, @@ -37888,16 +38677,16 @@ "scenario": "g_V_hasLabelXpersonX_filterXoutEXcreatedXX_valueMap_withXtokensX", "traversals": [ { - "original": "g.V().hasLabel(\"person\").filter(__.outE(\"created\")).valueMap().with(WithOptions.tokens)", - "language": "g.V().hasLabel(\"person\").filter(__.outE(\"created\")).valueMap().with(WithOptions.tokens)", - "canonical": "g.V().hasLabel(\"person\").filter(__.outE(\"created\")).valueMap().with(WithOptions.tokens)", - "anonymized": "g.V().hasLabel(string0).filter(__.outE(string1)).valueMap().with(WithOptions.tokens)", - "dotnet": "g.V().HasLabel(\"person\").Filter(__.OutE(\"created\")).ValueMap().With(WithOptions.Tokens)", - "go": "g.V().HasLabel(\"person\").Filter(gremlingo.T__.OutE(\"created\")).ValueMap().With(gremlingo.WithOptions.Tokens)", - "groovy": "g.V().hasLabel(\"person\").filter(__.outE(\"created\")).valueMap().with(WithOptions.tokens)", - "java": "g.V().hasLabel(\"person\").filter(__.outE(\"created\")).valueMap().with(WithOptions.tokens)", - "javascript": "g.V().hasLabel(\"person\").filter(__.outE(\"created\")).valueMap().with_(WithOptions.tokens)", - "python": "g.V().has_label('person').filter_(__.out_e('created')).value_map().with_(WithOptions.tokens)" + "original": "g.with(\"singlelabel\").V().hasLabel(\"person\").filter(__.outE(\"created\")).valueMap().with(WithOptions.tokens)", + "language": "g.with(\"singlelabel\").V().hasLabel(\"person\").filter(__.outE(\"created\")).valueMap().with(WithOptions.tokens)", + "canonical": "g.with(\"singlelabel\").V().hasLabel(\"person\").filter(__.outE(\"created\")).valueMap().with(WithOptions.tokens)", + "anonymized": "g.with(string0).V().hasLabel(string1).filter(__.outE(string2)).valueMap().with(WithOptions.tokens)", + "dotnet": "g.With(\"singlelabel\").V().HasLabel(\"person\").Filter(__.OutE(\"created\")).ValueMap().With(WithOptions.Tokens)", + "go": "g.With(\"singlelabel\").V().HasLabel(\"person\").Filter(gremlingo.T__.OutE(\"created\")).ValueMap().With(gremlingo.WithOptions.Tokens)", + "groovy": "g.with(\"singlelabel\").V().hasLabel(\"person\").filter(__.outE(\"created\")).valueMap().with(WithOptions.tokens)", + "java": "g.with(\"singlelabel\").V().hasLabel(\"person\").filter(__.outE(\"created\")).valueMap().with(WithOptions.tokens)", + "javascript": "g.with_(\"singlelabel\").V().hasLabel(\"person\").filter(__.outE(\"created\")).valueMap().with_(WithOptions.tokens)", + "python": "g.with_('singlelabel').V().has_label('person').filter_(__.out_e('created')).value_map().with_(WithOptions.tokens)" } ] }, @@ -37952,6 +38741,139 @@ } ] }, + { + "scenario": "g_withXmultilabelX_VXmarkoX_valueMap_withXtokensX", + "traversals": [ + { + "original": "g.with(\"multilabel\").V().has(\"name\", \"marko\").valueMap().with(WithOptions.tokens)", + "language": "g.with(\"multilabel\").V().has(\"name\", \"marko\").valueMap().with(WithOptions.tokens)", + "canonical": "g.with(\"multilabel\").V().has(\"name\", \"marko\").valueMap().with(WithOptions.tokens)", + "anonymized": "g.with(string0).V().has(string1, string2).valueMap().with(WithOptions.tokens)", + "dotnet": "g.With(\"multilabel\").V().Has(\"name\", \"marko\").ValueMap().With(WithOptions.Tokens)", + "go": "g.With(\"multilabel\").V().Has(\"name\", \"marko\").ValueMap().With(gremlingo.WithOptions.Tokens)", + "groovy": "g.with(\"multilabel\").V().has(\"name\", \"marko\").valueMap().with(WithOptions.tokens)", + "java": "g.with(\"multilabel\").V().has(\"name\", \"marko\").valueMap().with(WithOptions.tokens)", + "javascript": "g.with_(\"multilabel\").V().has(\"name\", \"marko\").valueMap().with_(WithOptions.tokens)", + "python": "g.with_('multilabel').V().has('name', 'marko').value_map().with_(WithOptions.tokens)" + } + ] + }, + { + "scenario": "g_V_valueMap_withXtokensX_single_label_default", + "traversals": [ + { + "original": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "language": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "canonical": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "anonymized": "g.addV(string0).addLabel(string1).property(string2, string3)", + "dotnet": "g.AddV((string) \"person\").AddLabel(\"employee\").Property(\"name\", \"marko\")", + "go": "g.AddV(\"person\").AddLabel(\"employee\").Property(\"name\", \"marko\")", + "groovy": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "java": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "javascript": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "python": "g.add_v('person').add_label('employee').property('name', 'marko')" + }, + { + "original": "g.V().valueMap().with(WithOptions.tokens)", + "language": "g.V().valueMap().with(WithOptions.tokens)", + "canonical": "g.V().valueMap().with(WithOptions.tokens)", + "anonymized": "g.V().valueMap().with(WithOptions.tokens)", + "dotnet": "g.V().ValueMap().With(WithOptions.Tokens)", + "go": "g.V().ValueMap().With(gremlingo.WithOptions.Tokens)", + "groovy": "g.V().valueMap().with(WithOptions.tokens)", + "java": "g.V().valueMap().with(WithOptions.tokens)", + "javascript": "g.V().valueMap().with_(WithOptions.tokens)", + "python": "g.V().value_map().with_(WithOptions.tokens)" + } + ] + }, + { + "scenario": "g_withXmultilabelX_V_valueMap_withXtokensX_multilabel", + "traversals": [ + { + "original": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "language": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "canonical": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "anonymized": "g.addV(string0).addLabel(string1).property(string2, string3)", + "dotnet": "g.AddV((string) \"person\").AddLabel(\"employee\").Property(\"name\", \"marko\")", + "go": "g.AddV(\"person\").AddLabel(\"employee\").Property(\"name\", \"marko\")", + "groovy": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "java": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "javascript": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "python": "g.add_v('person').add_label('employee').property('name', 'marko')" + }, + { + "original": "g.with(\"multilabel\").V().valueMap().with(WithOptions.tokens)", + "language": "g.with(\"multilabel\").V().valueMap().with(WithOptions.tokens)", + "canonical": "g.with(\"multilabel\").V().valueMap().with(WithOptions.tokens)", + "anonymized": "g.with(string0).V().valueMap().with(WithOptions.tokens)", + "dotnet": "g.With(\"multilabel\").V().ValueMap().With(WithOptions.Tokens)", + "go": "g.With(\"multilabel\").V().ValueMap().With(gremlingo.WithOptions.Tokens)", + "groovy": "g.with(\"multilabel\").V().valueMap().with(WithOptions.tokens)", + "java": "g.with(\"multilabel\").V().valueMap().with(WithOptions.tokens)", + "javascript": "g.with_(\"multilabel\").V().valueMap().with_(WithOptions.tokens)", + "python": "g.with_('multilabel').V().value_map().with_(WithOptions.tokens)" + } + ] + }, + { + "scenario": "g_V_valueMap_withXtokensX_multi_label_default", + "traversals": [ + { + "original": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "language": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "canonical": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "anonymized": "g.addV(string0).addLabel(string1).property(string2, string3)", + "dotnet": "g.AddV((string) \"person\").AddLabel(\"employee\").Property(\"name\", \"marko\")", + "go": "g.AddV(\"person\").AddLabel(\"employee\").Property(\"name\", \"marko\")", + "groovy": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "java": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "javascript": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "python": "g.add_v('person').add_label('employee').property('name', 'marko')" + }, + { + "original": "g.V().valueMap().with(WithOptions.tokens)", + "language": "g.V().valueMap().with(WithOptions.tokens)", + "canonical": "g.V().valueMap().with(WithOptions.tokens)", + "anonymized": "g.V().valueMap().with(WithOptions.tokens)", + "dotnet": "g.V().ValueMap().With(WithOptions.Tokens)", + "go": "g.V().ValueMap().With(gremlingo.WithOptions.Tokens)", + "groovy": "g.V().valueMap().with(WithOptions.tokens)", + "java": "g.V().valueMap().with(WithOptions.tokens)", + "javascript": "g.V().valueMap().with_(WithOptions.tokens)", + "python": "g.V().value_map().with_(WithOptions.tokens)" + } + ] + }, + { + "scenario": "g_withXsinglelabelX_V_valueMap_withXtokensX_multi_label_default", + "traversals": [ + { + "original": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "language": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "canonical": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "anonymized": "g.addV(string0).addLabel(string1).property(string2, string3)", + "dotnet": "g.AddV((string) \"person\").AddLabel(\"employee\").Property(\"name\", \"marko\")", + "go": "g.AddV(\"person\").AddLabel(\"employee\").Property(\"name\", \"marko\")", + "groovy": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "java": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "javascript": "g.addV(\"person\").addLabel(\"employee\").property(\"name\", \"marko\")", + "python": "g.add_v('person').add_label('employee').property('name', 'marko')" + }, + { + "original": "g.with(\"singlelabel\").V().valueMap().with(WithOptions.tokens)", + "language": "g.with(\"singlelabel\").V().valueMap().with(WithOptions.tokens)", + "canonical": "g.with(\"singlelabel\").V().valueMap().with(WithOptions.tokens)", + "anonymized": "g.with(string0).V().valueMap().with(WithOptions.tokens)", + "dotnet": "g.With(\"singlelabel\").V().ValueMap().With(WithOptions.Tokens)", + "go": "g.With(\"singlelabel\").V().ValueMap().With(gremlingo.WithOptions.Tokens)", + "groovy": "g.with(\"singlelabel\").V().valueMap().with(WithOptions.tokens)", + "java": "g.with(\"singlelabel\").V().valueMap().with(WithOptions.tokens)", + "javascript": "g.with_(\"singlelabel\").V().valueMap().with_(WithOptions.tokens)", + "python": "g.with_('singlelabel').V().value_map().with_(WithOptions.tokens)" + } + ] + }, { "scenario": "g_VXnullX", "traversals": [ @@ -41231,6 +42153,134 @@ } ] }, + { + "scenario": "g_V_hasLabelXpersonX_hasXname_markoX_addLabelXemployeeX_labels", + "traversals": [ + { + "original": "g.addV(\"person\").property(\"name\", \"marko\")", + "language": "g.addV(\"person\").property(\"name\", \"marko\")", + "canonical": "g.addV(\"person\").property(\"name\", \"marko\")", + "anonymized": "g.addV(string0).property(string1, string2)", + "dotnet": "g.AddV((string) \"person\").Property(\"name\", \"marko\")", + "go": "g.AddV(\"person\").Property(\"name\", \"marko\")", + "groovy": "g.addV(\"person\").property(\"name\", \"marko\")", + "java": "g.addV(\"person\").property(\"name\", \"marko\")", + "javascript": "g.addV(\"person\").property(\"name\", \"marko\")", + "python": "g.add_v('person').property('name', 'marko')" + }, + { + "original": "g.V().hasLabel(\"person\").has(\"name\", \"marko\").addLabel(\"employee\").labels().fold()", + "language": "g.V().hasLabel(\"person\").has(\"name\", \"marko\").addLabel(\"employee\").labels().fold()", + "canonical": "g.V().hasLabel(\"person\").has(\"name\", \"marko\").addLabel(\"employee\").labels().fold()", + "anonymized": "g.V().hasLabel(string0).has(string1, string2).addLabel(string3).labels().fold()", + "dotnet": "g.V().HasLabel(\"person\").Has(\"name\", \"marko\").AddLabel(\"employee\").Labels().Fold()", + "go": "g.V().HasLabel(\"person\").Has(\"name\", \"marko\").AddLabel(\"employee\").Labels().Fold()", + "groovy": "g.V().hasLabel(\"person\").has(\"name\", \"marko\").addLabel(\"employee\").labels().fold()", + "java": "g.V().hasLabel(\"person\").has(\"name\", \"marko\").addLabel(\"employee\").labels().fold()", + "javascript": "g.V().hasLabel(\"person\").has(\"name\", \"marko\").addLabel(\"employee\").labels().fold()", + "python": "g.V().has_label('person').has('name', 'marko').add_label('employee').labels().fold()" + }, + { + "original": "g.V().hasLabel(\"person\").hasLabel(\"employee\")", + "language": "g.V().hasLabel(\"person\").hasLabel(\"employee\")", + "canonical": "g.V().hasLabel(\"person\").hasLabel(\"employee\")", + "anonymized": "g.V().hasLabel(string0).hasLabel(string1)", + "dotnet": "g.V().HasLabel(\"person\").HasLabel(\"employee\")", + "go": "g.V().HasLabel(\"person\").HasLabel(\"employee\")", + "groovy": "g.V().hasLabel(\"person\").hasLabel(\"employee\")", + "java": "g.V().hasLabel(\"person\").hasLabel(\"employee\")", + "javascript": "g.V().hasLabel(\"person\").hasLabel(\"employee\")", + "python": "g.V().has_label('person').has_label('employee')" + } + ] + }, + { + "scenario": "g_V_addLabelXa_bX_labels_count", + "traversals": [ + { + "original": "g.addV(\"person\")", + "language": "g.addV(\"person\")", + "canonical": "g.addV(\"person\")", + "anonymized": "g.addV(string0)", + "dotnet": "g.AddV((string) \"person\")", + "go": "g.AddV(\"person\")", + "groovy": "g.addV(\"person\")", + "java": "g.addV(\"person\")", + "javascript": "g.addV(\"person\")", + "python": "g.add_v('person')" + }, + { + "original": "g.V().addLabel(\"a\", \"b\").labels().count()", + "language": "g.V().addLabel(\"a\", \"b\").labels().count()", + "canonical": "g.V().addLabel(\"a\", \"b\").labels().count()", + "anonymized": "g.V().addLabel(string0, string1).labels().count()", + "dotnet": "g.V().AddLabel(\"a\", \"b\").Labels().Count()", + "go": "g.V().AddLabel(\"a\", \"b\").Labels().Count()", + "groovy": "g.V().addLabel(\"a\", \"b\").labels().count()", + "java": "g.V().addLabel(\"a\", \"b\").labels().count()", + "javascript": "g.V().addLabel(\"a\", \"b\").labels().count()", + "python": "g.V().add_label('a', 'b').labels().count()" + } + ] + }, + { + "scenario": "g_V_addLabelXexistingX_labels_count", + "traversals": [ + { + "original": "g.addV(\"person\")", + "language": "g.addV(\"person\")", + "canonical": "g.addV(\"person\")", + "anonymized": "g.addV(string0)", + "dotnet": "g.AddV((string) \"person\")", + "go": "g.AddV(\"person\")", + "groovy": "g.addV(\"person\")", + "java": "g.addV(\"person\")", + "javascript": "g.addV(\"person\")", + "python": "g.add_v('person')" + }, + { + "original": "g.V().addLabel(\"person\").labels().count()", + "language": "g.V().addLabel(\"person\").labels().count()", + "canonical": "g.V().addLabel(\"person\").labels().count()", + "anonymized": "g.V().addLabel(string0).labels().count()", + "dotnet": "g.V().AddLabel(\"person\").Labels().Count()", + "go": "g.V().AddLabel(\"person\").Labels().Count()", + "groovy": "g.V().addLabel(\"person\").labels().count()", + "java": "g.V().addLabel(\"person\").labels().count()", + "javascript": "g.V().addLabel(\"person\").labels().count()", + "python": "g.V().add_label('person').labels().count()" + } + ] + }, + { + "scenario": "g_E_addLabelXfriendX_labels_fold", + "traversals": [ + { + "original": "g.addV(\"person\").property(\"name\", \"marko\").as(\"a\").addV(\"person\").property(\"name\", \"josh\").as(\"b\").addE(\"knows\").from(\"a\").to(\"b\")", + "language": "g.addV(\"person\").property(\"name\", \"marko\").as(\"a\").addV(\"person\").property(\"name\", \"josh\").as(\"b\").addE(\"knows\").from(\"a\").to(\"b\")", + "canonical": "g.addV(\"person\").property(\"name\", \"marko\").as(\"a\").addV(\"person\").property(\"name\", \"josh\").as(\"b\").addE(\"knows\").from(\"a\").to(\"b\")", + "anonymized": "g.addV(string0).property(string1, string2).as(string3).addV(string0).property(string1, string4).as(string5).addE(string6).from(string3).to(string5)", + "dotnet": "g.AddV((string) \"person\").Property(\"name\", \"marko\").As(\"a\").AddV((string) \"person\").Property(\"name\", \"josh\").As(\"b\").AddE((string) \"knows\").From(\"a\").To(\"b\")", + "go": "g.AddV(\"person\").Property(\"name\", \"marko\").As(\"a\").AddV(\"person\").Property(\"name\", \"josh\").As(\"b\").AddE(\"knows\").From(\"a\").To(\"b\")", + "groovy": "g.addV(\"person\").property(\"name\", \"marko\").as(\"a\").addV(\"person\").property(\"name\", \"josh\").as(\"b\").addE(\"knows\").from(\"a\").to(\"b\")", + "java": "g.addV(\"person\").property(\"name\", \"marko\").as(\"a\").addV(\"person\").property(\"name\", \"josh\").as(\"b\").addE(\"knows\").from(\"a\").to(\"b\")", + "javascript": "g.addV(\"person\").property(\"name\", \"marko\").as(\"a\").addV(\"person\").property(\"name\", \"josh\").as(\"b\").addE(\"knows\").from_(\"a\").to(\"b\")", + "python": "g.add_v('person').property('name', 'marko').as_('a').add_v('person').property('name', 'josh').as_('b').add_e('knows').from_('a').to('b')" + }, + { + "original": "g.E().addLabel(\"friend\").labels().fold()", + "language": "g.E().addLabel(\"friend\").labels().fold()", + "canonical": "g.E().addLabel(\"friend\").labels().fold()", + "anonymized": "g.E().addLabel(string0).labels().fold()", + "dotnet": "g.E().AddLabel(\"friend\").Labels().Fold()", + "go": "g.E().AddLabel(\"friend\").Labels().Fold()", + "groovy": "g.E().addLabel(\"friend\").labels().fold()", + "java": "g.E().addLabel(\"friend\").labels().fold()", + "javascript": "g.E().addLabel(\"friend\").labels().fold()", + "python": "g.E().add_label('friend').labels().fold()" + } + ] + }, { "scenario": "g_V_valueXnameX_aggregateXxX_capXxX", "traversals": [ @@ -42200,6 +43250,233 @@ } ] }, + { + "scenario": "g_V_dropLabelXaX_labels", + "traversals": [ + { + "original": "g.addV(\"a\", \"b\")", + "language": "g.addV(\"a\", \"b\")", + "canonical": "g.addV(\"a\", \"b\")", + "anonymized": "g.addV(string0, string1)", + "dotnet": "g.AddV((string) \"a\", (string) \"b\")", + "go": "g.AddV(\"a\", \"b\")", + "groovy": "g.addV(\"a\", \"b\")", + "java": "g.addV(\"a\", \"b\")", + "javascript": "g.addV(\"a\", \"b\")", + "python": "g.add_v('a', 'b')" + }, + { + "original": "g.V().dropLabel(\"a\").labels().fold()", + "language": "g.V().dropLabel(\"a\").labels().fold()", + "canonical": "g.V().dropLabel(\"a\").labels().fold()", + "anonymized": "g.V().dropLabel(string0).labels().fold()", + "dotnet": "g.V().DropLabel(\"a\").Labels().Fold()", + "go": "g.V().DropLabel(\"a\").Labels().Fold()", + "groovy": "g.V().dropLabel(\"a\").labels().fold()", + "java": "g.V().dropLabel(\"a\").labels().fold()", + "javascript": "g.V().dropLabel(\"a\").labels().fold()", + "python": "g.V().drop_label('a').labels().fold()" + }, + { + "original": "g.V().hasLabel(\"a\")", + "language": "g.V().hasLabel(\"a\")", + "canonical": "g.V().hasLabel(\"a\")", + "anonymized": "g.V().hasLabel(string0)", + "dotnet": "g.V().HasLabel(\"a\")", + "go": "g.V().HasLabel(\"a\")", + "groovy": "g.V().hasLabel(\"a\")", + "java": "g.V().hasLabel(\"a\")", + "javascript": "g.V().hasLabel(\"a\")", + "python": "g.V().has_label('a')" + }, + { + "original": "g.V().hasLabel(\"b\")", + "language": "g.V().hasLabel(\"b\")", + "canonical": "g.V().hasLabel(\"b\")", + "anonymized": "g.V().hasLabel(string0)", + "dotnet": "g.V().HasLabel(\"b\")", + "go": "g.V().HasLabel(\"b\")", + "groovy": "g.V().hasLabel(\"b\")", + "java": "g.V().hasLabel(\"b\")", + "javascript": "g.V().hasLabel(\"b\")", + "python": "g.V().has_label('b')" + } + ] + }, + { + "scenario": "g_V_dropLabels_labels", + "traversals": [ + { + "original": "g.addV(\"a\", \"b\")", + "language": "g.addV(\"a\", \"b\")", + "canonical": "g.addV(\"a\", \"b\")", + "anonymized": "g.addV(string0, string1)", + "dotnet": "g.AddV((string) \"a\", (string) \"b\")", + "go": "g.AddV(\"a\", \"b\")", + "groovy": "g.addV(\"a\", \"b\")", + "java": "g.addV(\"a\", \"b\")", + "javascript": "g.addV(\"a\", \"b\")", + "python": "g.add_v('a', 'b')" + }, + { + "original": "g.V().dropLabels().labels()", + "language": "g.V().dropLabels().labels()", + "canonical": "g.V().dropLabels().labels()", + "anonymized": "g.V().dropLabels().labels()", + "dotnet": "g.V().DropLabels().Labels()", + "go": "g.V().DropLabels().Labels()", + "groovy": "g.V().dropLabels().labels()", + "java": "g.V().dropLabels().labels()", + "javascript": "g.V().dropLabels().labels()", + "python": "g.V().drop_labels().labels()" + } + ] + }, + { + "scenario": "g_V_dropLabelXa_bX_labels", + "traversals": [ + { + "original": "g.addV(\"a\", \"b\", \"c\")", + "language": "g.addV(\"a\", \"b\", \"c\")", + "canonical": "g.addV(\"a\", \"b\", \"c\")", + "anonymized": "g.addV(string0, string1, string2)", + "dotnet": "g.AddV((string) \"a\", (string) \"b\", (string) \"c\")", + "go": "g.AddV(\"a\", \"b\", \"c\")", + "groovy": "g.addV(\"a\", \"b\", \"c\")", + "java": "g.addV(\"a\", \"b\", \"c\")", + "javascript": "g.addV(\"a\", \"b\", \"c\")", + "python": "g.add_v('a', 'b', 'c')" + }, + { + "original": "g.V().dropLabel(\"a\", \"b\").labels()", + "language": "g.V().dropLabel(\"a\", \"b\").labels()", + "canonical": "g.V().dropLabel(\"a\", \"b\").labels()", + "anonymized": "g.V().dropLabel(string0, string1).labels()", + "dotnet": "g.V().DropLabel(\"a\", \"b\").Labels()", + "go": "g.V().DropLabel(\"a\", \"b\").Labels()", + "groovy": "g.V().dropLabel(\"a\", \"b\").labels()", + "java": "g.V().dropLabel(\"a\", \"b\").labels()", + "javascript": "g.V().dropLabel(\"a\", \"b\").labels()", + "python": "g.V().drop_label('a', 'b').labels()" + } + ] + }, + { + "scenario": "g_V_dropLabels_defaultLabel", + "traversals": [ + { + "original": "g.addV(\"person\")", + "language": "g.addV(\"person\")", + "canonical": "g.addV(\"person\")", + "anonymized": "g.addV(string0)", + "dotnet": "g.AddV((string) \"person\")", + "go": "g.AddV(\"person\")", + "groovy": "g.addV(\"person\")", + "java": "g.addV(\"person\")", + "javascript": "g.addV(\"person\")", + "python": "g.add_v('person')" + }, + { + "original": "g.V().dropLabels().labels()", + "language": "g.V().dropLabels().labels()", + "canonical": "g.V().dropLabels().labels()", + "anonymized": "g.V().dropLabels().labels()", + "dotnet": "g.V().DropLabels().Labels()", + "go": "g.V().DropLabels().Labels()", + "groovy": "g.V().dropLabels().labels()", + "java": "g.V().dropLabels().labels()", + "javascript": "g.V().dropLabels().labels()", + "python": "g.V().drop_labels().labels()" + } + ] + }, + { + "scenario": "g_E_dropLabelXknowsX_labels", + "traversals": [ + { + "original": "g.addV(\"person\").as(\"a\").addV(\"person\").as(\"b\").addE(\"knows\").from(\"a\").to(\"b\")", + "language": "g.addV(\"person\").as(\"a\").addV(\"person\").as(\"b\").addE(\"knows\").from(\"a\").to(\"b\")", + "canonical": "g.addV(\"person\").as(\"a\").addV(\"person\").as(\"b\").addE(\"knows\").from(\"a\").to(\"b\")", + "anonymized": "g.addV(string0).as(string1).addV(string0).as(string2).addE(string3).from(string1).to(string2)", + "dotnet": "g.AddV((string) \"person\").As(\"a\").AddV((string) \"person\").As(\"b\").AddE((string) \"knows\").From(\"a\").To(\"b\")", + "go": "g.AddV(\"person\").As(\"a\").AddV(\"person\").As(\"b\").AddE(\"knows\").From(\"a\").To(\"b\")", + "groovy": "g.addV(\"person\").as(\"a\").addV(\"person\").as(\"b\").addE(\"knows\").from(\"a\").to(\"b\")", + "java": "g.addV(\"person\").as(\"a\").addV(\"person\").as(\"b\").addE(\"knows\").from(\"a\").to(\"b\")", + "javascript": "g.addV(\"person\").as(\"a\").addV(\"person\").as(\"b\").addE(\"knows\").from_(\"a\").to(\"b\")", + "python": "g.add_v('person').as_('a').add_v('person').as_('b').add_e('knows').from_('a').to('b')" + }, + { + "original": "g.E().dropLabel(\"knows\").labels().fold()", + "language": "g.E().dropLabel(\"knows\").labels().fold()", + "canonical": "g.E().dropLabel(\"knows\").labels().fold()", + "anonymized": "g.E().dropLabel(string0).labels().fold()", + "dotnet": "g.E().DropLabel(\"knows\").Labels().Fold()", + "go": "g.E().DropLabel(\"knows\").Labels().Fold()", + "groovy": "g.E().dropLabel(\"knows\").labels().fold()", + "java": "g.E().dropLabel(\"knows\").labels().fold()", + "javascript": "g.E().dropLabel(\"knows\").labels().fold()", + "python": "g.E().drop_label('knows').labels().fold()" + } + ] + }, + { + "scenario": "g_E_dropLabels_labels", + "traversals": [ + { + "original": "g.addV(\"person\").as(\"a\").addV(\"person\").as(\"b\").addE(\"knows\").from(\"a\").to(\"b\")", + "language": "g.addV(\"person\").as(\"a\").addV(\"person\").as(\"b\").addE(\"knows\").from(\"a\").to(\"b\")", + "canonical": "g.addV(\"person\").as(\"a\").addV(\"person\").as(\"b\").addE(\"knows\").from(\"a\").to(\"b\")", + "anonymized": "g.addV(string0).as(string1).addV(string0).as(string2).addE(string3).from(string1).to(string2)", + "dotnet": "g.AddV((string) \"person\").As(\"a\").AddV((string) \"person\").As(\"b\").AddE((string) \"knows\").From(\"a\").To(\"b\")", + "go": "g.AddV(\"person\").As(\"a\").AddV(\"person\").As(\"b\").AddE(\"knows\").From(\"a\").To(\"b\")", + "groovy": "g.addV(\"person\").as(\"a\").addV(\"person\").as(\"b\").addE(\"knows\").from(\"a\").to(\"b\")", + "java": "g.addV(\"person\").as(\"a\").addV(\"person\").as(\"b\").addE(\"knows\").from(\"a\").to(\"b\")", + "javascript": "g.addV(\"person\").as(\"a\").addV(\"person\").as(\"b\").addE(\"knows\").from_(\"a\").to(\"b\")", + "python": "g.add_v('person').as_('a').add_v('person').as_('b').add_e('knows').from_('a').to('b')" + }, + { + "original": "g.E().dropLabels().labels()", + "language": "g.E().dropLabels().labels()", + "canonical": "g.E().dropLabels().labels()", + "anonymized": "g.E().dropLabels().labels()", + "dotnet": "g.E().DropLabels().Labels()", + "go": "g.E().DropLabels().Labels()", + "groovy": "g.E().dropLabels().labels()", + "java": "g.E().dropLabels().labels()", + "javascript": "g.E().dropLabels().labels()", + "python": "g.E().drop_labels().labels()" + } + ] + }, + { + "scenario": "g_V_dropLabelXnonExistentX_labels", + "traversals": [ + { + "original": "g.addV(\"a\", \"b\")", + "language": "g.addV(\"a\", \"b\")", + "canonical": "g.addV(\"a\", \"b\")", + "anonymized": "g.addV(string0, string1)", + "dotnet": "g.AddV((string) \"a\", (string) \"b\")", + "go": "g.AddV(\"a\", \"b\")", + "groovy": "g.addV(\"a\", \"b\")", + "java": "g.addV(\"a\", \"b\")", + "javascript": "g.addV(\"a\", \"b\")", + "python": "g.add_v('a', 'b')" + }, + { + "original": "g.V().dropLabel(\"xyz\").labels()", + "language": "g.V().dropLabel(\"xyz\").labels()", + "canonical": "g.V().dropLabel(\"xyz\").labels()", + "anonymized": "g.V().dropLabel(string0).labels()", + "dotnet": "g.V().DropLabel(\"xyz\").Labels()", + "go": "g.V().DropLabel(\"xyz\").Labels()", + "groovy": "g.V().dropLabel(\"xyz\").labels()", + "java": "g.V().dropLabel(\"xyz\").labels()", + "javascript": "g.V().dropLabel(\"xyz\").labels()", + "python": "g.V().drop_label('xyz').labels()" + } + ] + }, { "scenario": "g_V_fail", "traversals": [ diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/AddEdge.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/AddEdge.feature index 02c2be3d7b7..70058962bc1 100644 --- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/AddEdge.feature +++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/AddEdge.feature @@ -528,4 +528,4 @@ Feature: Step - addE() addE("knows").from(__.V().has("name","marko")) """ When iterated to list - Then the traversal will raise an error with message containing text of "must resolve to a Vertex or the ID of a Vertex present in the graph" \ No newline at end of file + Then the traversal will raise an error with message containing text of "must resolve to a Vertex or the ID of a Vertex present in the graph" diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/AddVertex.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/AddVertex.feature index 99e1492f566..0d7fdaf2c79 100644 --- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/AddVertex.feature +++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/AddVertex.feature @@ -703,4 +703,27 @@ Feature: Step - addV() When iterated to list Then the result should be unordered | result | - | d[2010].i | \ No newline at end of file + | d[2010].i | + + @MultiLabel + Scenario: g_addVXa_bX_labels + Given the empty graph + And the traversal of + """ + g.addV("a", "b").labels().fold() + """ + When iterated to list + Then the result should have a count of 1 + And the graph should return 1 for count of "g.V().hasLabel(\"a\").hasLabel(\"b\")" + + @MultiLabel + Scenario: g_addVXa_b_cX_labels_count + Given the empty graph + And the traversal of + """ + g.addV("a", "b", "c").labels().count() + """ + When iterated to list + Then the result should be unordered + | result | + | d[3].l | diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/ElementMap.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/ElementMap.feature index f119a2e9649..e5216b0ba49 100644 --- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/ElementMap.feature +++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/ElementMap.feature @@ -22,7 +22,7 @@ Feature: Step - elementMap() Given the modern graph And the traversal of """ - g.V().elementMap() + g.with("singlelabel").V().elementMap() """ When iterated to list Then the result should be unordered @@ -38,7 +38,7 @@ Feature: Step - elementMap() Given the modern graph And the traversal of """ - g.V().elementMap("name", "age") + g.with("singlelabel").V().elementMap("name", "age") """ When iterated to list Then the result should be unordered @@ -56,7 +56,7 @@ Feature: Step - elementMap() And using the parameter eid11 defined as "e[josh-created->lop].id" And the traversal of """ - g.E(eid11).elementMap() + g.with("singlelabel").E(eid11).elementMap() """ When iterated to list Then the result should be unordered @@ -67,7 +67,7 @@ Feature: Step - elementMap() Given the modern graph And the traversal of """ - g.V().elementMap("name", "age", null) + g.with("singlelabel").V().elementMap("name", "age", null) """ When iterated to list Then the result should be unordered @@ -77,4 +77,79 @@ Feature: Step - elementMap() | m[{"t[id]": "v[peter].id", "t[label]": "person", "name": "peter", "age": 35}] | | m[{"t[id]": "v[vadas].id", "t[label]": "person", "name": "vadas", "age": 27}] | | m[{"t[id]": "v[lop].id", "t[label]": "software", "name": "lop"}] | - | m[{"t[id]": "v[ripple].id", "t[label]": "software", "name": "ripple"}] | \ No newline at end of file + | m[{"t[id]": "v[ripple].id", "t[label]": "software", "name": "ripple"}] | + + Scenario: g_withXmultilabelX_VXmarkoX_elementMap + Given the modern graph + And the traversal of + """ + g.with("multilabel").V().has("name", "marko").elementMap() + """ + When iterated to list + Then the result should be unordered + | result | + | m[{"t[id]": "v[marko].id", "t[label]": "s[person]", "name": "marko", "age": 29}] | + + @MultiLabel @SingleLabelDefault + Scenario: g_V_elementMap_single_label_default + Given the empty graph + And the graph initializer of + """ + g.addV("person").addLabel("employee").property("name", "marko") + """ + And the traversal of + """ + g.V().elementMap() + """ + When iterated to list + Then the result should be unordered + | result | + | m[{"t[id]": "v[marko].id", "t[label]": "person", "name": "marko"}] | + + @MultiLabel + Scenario: g_withXmultilabelX_V_elementMap_multilabel + Given the empty graph + And the graph initializer of + """ + g.addV("person").addLabel("employee").property("name", "marko") + """ + And the traversal of + """ + g.with("multilabel").V().elementMap() + """ + When iterated to list + Then the result should be unordered + | result | + | m[{"t[id]": "v[marko].id", "t[label]": "s[person,employee]", "name": "marko"}] | + + @MultiLabel @MultiLabelDefault + Scenario: g_V_elementMap_multi_label_default + Given the empty graph + And the graph initializer of + """ + g.addV("person").addLabel("employee").property("name", "marko") + """ + And the traversal of + """ + g.V().elementMap() + """ + When iterated to list + Then the result should be unordered + | result | + | m[{"t[id]": "v[marko].id", "t[label]": "s[person,employee]", "name": "marko"}] | + + @MultiLabel @MultiLabelDefault + Scenario: g_withXsinglelabelX_V_elementMap_multi_label_default + Given the empty graph + And the graph initializer of + """ + g.addV("person").addLabel("employee").property("name", "marko") + """ + And the traversal of + """ + g.with("singlelabel").V().elementMap() + """ + When iterated to list + Then the result should be unordered + | result | + | m[{"t[id]": "v[marko].id", "t[label]": "person", "name": "marko"}] | diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Labels.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Labels.feature new file mode 100644 index 00000000000..6f668a4f6ff --- /dev/null +++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Labels.feature @@ -0,0 +1,94 @@ +# 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. + +@StepClassMap @StepLabels @MultiLabel +Feature: Step - labels() + + @MultiLabel + Scenario: g_V_hasLabelXpersonX_labels + Given the modern graph + And the traversal of + """ + g.V().hasLabel("person").labels() + """ + When iterated to list + Then the result should be unordered + | result | + | person | + | person | + | person | + | person | + + @MultiLabel + Scenario: g_V_labels_multilabel + Given the empty graph + And the graph initializer of + """ + g.addV("a", "b") + """ + And the traversal of + """ + g.V().labels() + """ + When iterated to list + Then the result should be unordered + | result | + | a | + | b | + + @MultiLabel + Scenario: g_addVXa_bX_labels_count + Given the empty graph + And the graph initializer of + """ + g.addV("a", "b") + """ + And the traversal of + """ + g.V().labels().count() + """ + When iterated to list + Then the result should be unordered + | result | + | d[2].l | + + @MultiLabel + Scenario: g_addV_labels + Given the empty graph + And the graph initializer of + """ + g.addV() + """ + And the traversal of + """ + g.V().labels() + """ + When iterated to list + Then the result should have a count of 0 + + @MultiLabel + Scenario: g_E_labels + Given the modern graph + And the traversal of + """ + g.E().hasLabel("knows").labels() + """ + When iterated to list + Then the result should be unordered + | result | + | knows | + | knows | diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/MergeEdge.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/MergeEdge.feature index 6ffb7da16d5..0513f5451db 100644 --- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/MergeEdge.feature +++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/MergeEdge.feature @@ -1108,4 +1108,6 @@ Feature: Step - mergeE() Then the result should have a count of 1 And the graph should return 2 for count of "g.V()" And the graph should return 1 for count of "g.E().hasLabel(\"knows\")" - And the graph should return 0 for count of "g.E().hasLabel(\"knows\").has(\"weight\")" \ No newline at end of file + And the graph should return 0 for count of "g.E().hasLabel(\"knows\").has(\"weight\")" + + diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/MergeVertex.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/MergeVertex.feature index 801b39db7fe..d056fce1c72 100644 --- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/MergeVertex.feature +++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/MergeVertex.feature @@ -990,3 +990,144 @@ Feature: Step - mergeV() Then the result should have a count of 1 And the graph should return 1 for count of "g.V().has(\"person\",\"name\",\"stephen\").hasNot(\"created\")" And the graph should return 2 for count of "g.V()" + + @MultiLabel + Scenario: g_mergeVXlabel_ab_name_markoX_multilabel_create + Given the empty graph + And the traversal of + """ + g.mergeV([(T.label): ["person","employee"], name: "marko"]) + """ + When iterated to list + Then the result should have a count of 1 + And the graph should return 1 for count of "g.V()" + And the graph should return 1 for count of "g.V().hasLabel(\"person\").hasLabel(\"employee\")" + And the graph should return 1 for count of "g.V().has(\"name\",\"marko\")" + + @MultiLabel + Scenario: g_mergeVXlabel_abc_name_testX_multilabel_create + Given the empty graph + And the traversal of + """ + g.mergeV([(T.label): ["a","b","c"], name: "test"]) + """ + When iterated to list + Then the result should have a count of 1 + And the graph should return 1 for count of "g.V()" + And the graph should return 1 for count of "g.V().hasLabel(\"a\").hasLabel(\"b\").hasLabel(\"c\")" + + @MultiLabel + Scenario: g_mergeVXlabel_ab_name_markoX_multilabel_match + Given the empty graph + And the graph initializer of + """ + g.addV("person").addLabel("employee").property("name", "marko") + """ + And the traversal of + """ + g.mergeV([(T.label): ["person","employee"], name: "marko"]) + """ + When iterated to list + Then the result should have a count of 1 + And the graph should return 1 for count of "g.V()" + And the graph should return 1 for count of "g.V().hasLabel(\"person\").hasLabel(\"employee\")" + + @MultiLabel + Scenario: g_mergeVXlabel_ab_name_markoX_multilabel_nomatch + Given the empty graph + And the graph initializer of + """ + g.addV("person").property("name", "marko") + """ + And the traversal of + """ + g.mergeV([(T.label): ["person","employee"], name: "marko"]) + """ + When iterated to list + Then the result should have a count of 1 + And the graph should return 2 for count of "g.V()" + + @MultiLabel + Scenario: g_mergeVXlabel_person_name_markoX_optionXonMatch_label_managerX + Given the empty graph + And the graph initializer of + """ + g.addV("person").addLabel("employee").property("name", "marko") + """ + And the traversal of + """ + g.mergeV([(T.label): "person", name: "marko"]). + option(Merge.onMatch, [(T.label): "manager"]) + """ + When iterated to list + Then the result should have a count of 1 + And the graph should return 1 for count of "g.V()" + And the graph should return 1 for count of "g.V().hasLabel(\"manager\")" + And the graph should return 1 for count of "g.V().hasLabel(\"person\")" + And the graph should return 1 for count of "g.V().hasLabel(\"employee\")" + + @MultiLabel + Scenario: g_mergeVXlabel_person_name_markoX_optionXonMatch_label_manager_directorX + Given the empty graph + And the graph initializer of + """ + g.addV("person").property("name", "marko") + """ + And the traversal of + """ + g.mergeV([(T.label): "person", name: "marko"]). + option(Merge.onMatch, [(T.label): ["manager","director"]]) + """ + When iterated to list + Then the result should have a count of 1 + And the graph should return 1 for count of "g.V()" + And the graph should return 1 for count of "g.V().hasLabel(\"manager\").hasLabel(\"director\")" + And the graph should return 1 for count of "g.V().hasLabel(\"person\")" + + @MultiLabel + Scenario: g_mergeVXlabel_person_name_markoX_optionXonMatch_label_emptyX + Given the empty graph + And the graph initializer of + """ + g.addV("person").property("name", "marko") + """ + And the traversal of + """ + g.mergeV([(T.label): "person", name: "marko"]). + option(Merge.onMatch, [(T.label): []]) + """ + When iterated to list + Then the result should have a count of 1 + And the graph should return 1 for count of "g.V()" + And the graph should return 1 for count of "g.V().hasLabel(\"person\")" + + @MultiLabel + Scenario: g_mergeVXname_markoX_optionXonCreate_label_ab_name_markoX + Given the empty graph + And the traversal of + """ + g.mergeV([name: "marko"]). + option(Merge.onCreate, [(T.label): ["person","employee"], name: "marko"]) + """ + When iterated to list + Then the result should have a count of 1 + And the graph should return 1 for count of "g.V()" + And the graph should return 1 for count of "g.V().hasLabel(\"person\").hasLabel(\"employee\")" + And the graph should return 1 for count of "g.V().has(\"name\",\"marko\")" + + @MultiLabel + Scenario: g_mergeVXlabel_person_name_markoX_optionXonMatch_label_existingX + Given the empty graph + And the graph initializer of + """ + g.addV("person").addLabel("employee").property("name", "marko") + """ + And the traversal of + """ + g.mergeV([(T.label): "person", name: "marko"]). + option(Merge.onMatch, [(T.label): "person"]) + """ + When iterated to list + Then the result should have a count of 1 + And the graph should return 1 for count of "g.V()" + And the graph should return 1 for count of "g.V().hasLabel(\"person\").hasLabel(\"employee\")" diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/ValueMap.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/ValueMap.feature index 63f52324d39..332c17b549a 100644 --- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/ValueMap.feature +++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/ValueMap.feature @@ -38,7 +38,7 @@ Feature: Step - valueMap() Given the modern graph And the traversal of """ - g.V().valueMap(true) + g.with("singlelabel").V().valueMap(true) """ When iterated to list Then the result should be unordered @@ -54,7 +54,7 @@ Feature: Step - valueMap() Given the modern graph And the traversal of """ - g.V().valueMap().with(WithOptions.tokens) + g.with("singlelabel").V().valueMap().with(WithOptions.tokens) """ When iterated to list Then the result should be unordered @@ -86,7 +86,7 @@ Feature: Step - valueMap() Given the modern graph And the traversal of """ - g.V().valueMap(true, "name", "age") + g.with("singlelabel").V().valueMap(true, "name", "age") """ When iterated to list Then the result should be unordered @@ -102,7 +102,7 @@ Feature: Step - valueMap() Given the modern graph And the traversal of """ - g.V().valueMap("name", "age").with(WithOptions.tokens) + g.with("singlelabel").V().valueMap("name", "age").with(WithOptions.tokens) """ When iterated to list Then the result should be unordered @@ -118,7 +118,7 @@ Feature: Step - valueMap() Given the modern graph And the traversal of """ - g.V().valueMap("name", "age").with(WithOptions.tokens, WithOptions.labels).by(__.unfold()) + g.with("singlelabel").V().valueMap("name", "age").with(WithOptions.tokens, WithOptions.labels).by(__.unfold()) """ When iterated to list Then the result should be unordered @@ -162,7 +162,7 @@ Feature: Step - valueMap() Given the modern graph And the traversal of """ - g.V().hasLabel("person").filter(__.outE("created")).valueMap(true) + g.with("singlelabel").V().hasLabel("person").filter(__.outE("created")).valueMap(true) """ When iterated to list Then the result should be unordered @@ -175,7 +175,7 @@ Feature: Step - valueMap() Given the modern graph And the traversal of """ - g.V().hasLabel("person").filter(__.outE("created")).valueMap().with(WithOptions.tokens) + g.with("singlelabel").V().hasLabel("person").filter(__.outE("created")).valueMap().with(WithOptions.tokens) """ When iterated to list Then the result should be unordered @@ -220,3 +220,78 @@ Feature: Step - valueMap() """ When iterated to list Then the traversal will raise an error with message containing text of "step can only have one by modulator" + + Scenario: g_withXmultilabelX_VXmarkoX_valueMap_withXtokensX + Given the modern graph + And the traversal of + """ + g.with("multilabel").V().has("name", "marko").valueMap().with(WithOptions.tokens) + """ + When iterated to list + Then the result should be unordered + | result | + | m[{"t[id]": "v[marko].id", "t[label]": "s[person]", "name": ["marko"], "age": [29]}] | + + @MultiLabel @SingleLabelDefault + Scenario: g_V_valueMap_withXtokensX_single_label_default + Given the empty graph + And the graph initializer of + """ + g.addV("person").addLabel("employee").property("name", "marko") + """ + And the traversal of + """ + g.V().valueMap().with(WithOptions.tokens) + """ + When iterated to list + Then the result should be unordered + | result | + | m[{"t[id]": "v[marko].id", "t[label]": "person", "name": ["marko"]}] | + + @MultiLabel + Scenario: g_withXmultilabelX_V_valueMap_withXtokensX_multilabel + Given the empty graph + And the graph initializer of + """ + g.addV("person").addLabel("employee").property("name", "marko") + """ + And the traversal of + """ + g.with("multilabel").V().valueMap().with(WithOptions.tokens) + """ + When iterated to list + Then the result should be unordered + | result | + | m[{"t[id]": "v[marko].id", "t[label]": "s[person,employee]", "name": ["marko"]}] | + + @MultiLabel @MultiLabelDefault + Scenario: g_V_valueMap_withXtokensX_multi_label_default + Given the empty graph + And the graph initializer of + """ + g.addV("person").addLabel("employee").property("name", "marko") + """ + And the traversal of + """ + g.V().valueMap().with(WithOptions.tokens) + """ + When iterated to list + Then the result should be unordered + | result | + | m[{"t[id]": "v[marko].id", "t[label]": "s[person,employee]", "name": ["marko"]}] | + + @MultiLabel @MultiLabelDefault + Scenario: g_withXsinglelabelX_V_valueMap_withXtokensX_multi_label_default + Given the empty graph + And the graph initializer of + """ + g.addV("person").addLabel("employee").property("name", "marko") + """ + And the traversal of + """ + g.with("singlelabel").V().valueMap().with(WithOptions.tokens) + """ + When iterated to list + Then the result should be unordered + | result | + | m[{"t[id]": "v[marko].id", "t[label]": "person", "name": ["marko"]}] | diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/sideEffect/AddLabel.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/sideEffect/AddLabel.feature new file mode 100644 index 00000000000..ec12441f1e9 --- /dev/null +++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/sideEffect/AddLabel.feature @@ -0,0 +1,80 @@ +# 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. + +@StepClassSideEffect @StepAddLabel @MultiLabel +Feature: Step - addLabel() + + @MultiLabel + Scenario: g_V_hasLabelXpersonX_hasXname_markoX_addLabelXemployeeX_labels + Given the empty graph + And the graph initializer of + """ + g.addV("person").property("name", "marko") + """ + And the traversal of + """ + g.V().hasLabel("person").has("name", "marko").addLabel("employee").labels().fold() + """ + When iterated to list + Then the result should have a count of 1 + And the graph should return 1 for count of "g.V().hasLabel(\"person\").hasLabel(\"employee\")" + + @MultiLabel + Scenario: g_V_addLabelXa_bX_labels_count + Given the empty graph + And the graph initializer of + """ + g.addV("person") + """ + And the traversal of + """ + g.V().addLabel("a", "b").labels().count() + """ + When iterated to list + Then the result should be unordered + | result | + | d[3].l | + + @MultiLabel + Scenario: g_V_addLabelXexistingX_labels_count + Given the empty graph + And the graph initializer of + """ + g.addV("person") + """ + And the traversal of + """ + g.V().addLabel("person").labels().count() + """ + When iterated to list + Then the result should be unordered + | result | + | d[1].l | + + @MultiLabel + Scenario: g_E_addLabelXfriendX_labels_fold + Given the empty graph + And the graph initializer of + """ + g.addV("person").property("name", "marko").as("a").addV("person").property("name", "josh").as("b").addE("knows").from("a").to("b") + """ + And the traversal of + """ + g.E().addLabel("friend").labels().fold() + """ + When iterated to list + Then the traversal will raise an error with message containing text of "Label mutation is not supported" diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/sideEffect/DropLabel.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/sideEffect/DropLabel.feature new file mode 100644 index 00000000000..bc28f410272 --- /dev/null +++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/sideEffect/DropLabel.feature @@ -0,0 +1,124 @@ +# 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. + +@StepClassSideEffect @StepDropLabel @MultiLabel +Feature: Step - dropLabel() / dropLabels() + + @MultiLabel + Scenario: g_V_dropLabelXaX_labels + Given the empty graph + And the graph initializer of + """ + g.addV("a", "b") + """ + And the traversal of + """ + g.V().dropLabel("a").labels().fold() + """ + When iterated to list + Then the result should have a count of 1 + And the graph should return 0 for count of "g.V().hasLabel(\"a\")" + And the graph should return 1 for count of "g.V().hasLabel(\"b\")" + + @MultiLabel + Scenario: g_V_dropLabels_labels + Given the empty graph + And the graph initializer of + """ + g.addV("a", "b") + """ + And the traversal of + """ + g.V().dropLabels().labels() + """ + When iterated to list + Then the result should have a count of 0 + + @MultiLabel + Scenario: g_V_dropLabelXa_bX_labels + Given the empty graph + And the graph initializer of + """ + g.addV("a", "b", "c") + """ + And the traversal of + """ + g.V().dropLabel("a", "b").labels() + """ + When iterated to list + Then the result should be unordered + | result | + | c | + + @MultiLabel + Scenario: g_V_dropLabels_defaultLabel + Given the empty graph + And the graph initializer of + """ + g.addV("person") + """ + And the traversal of + """ + g.V().dropLabels().labels() + """ + When iterated to list + Then the result should have a count of 0 + + @MultiLabel + Scenario: g_E_dropLabelXknowsX_labels + Given the empty graph + And the graph initializer of + """ + g.addV("person").as("a").addV("person").as("b").addE("knows").from("a").to("b") + """ + And the traversal of + """ + g.E().dropLabel("knows").labels().fold() + """ + When iterated to list + Then the traversal will raise an error with message containing text of "Label mutation is not supported" + + @MultiLabel + Scenario: g_E_dropLabels_labels + Given the empty graph + And the graph initializer of + """ + g.addV("person").as("a").addV("person").as("b").addE("knows").from("a").to("b") + """ + And the traversal of + """ + g.E().dropLabels().labels() + """ + When iterated to list + Then the traversal will raise an error with message containing text of "Label mutation is not supported" + + @MultiLabel + Scenario: g_V_dropLabelXnonExistentX_labels + Given the empty graph + And the graph initializer of + """ + g.addV("a", "b") + """ + And the traversal of + """ + g.V().dropLabel("xyz").labels() + """ + When iterated to list + Then the result should be unordered + | result | + | a | + | b | From be5072e231d3b492735fbb754825db685a9da5e2 Mon Sep 17 00:00:00 2001 From: xiazcy Date: Fri, 26 Jun 2026 16:12:11 -0700 Subject: [PATCH 04/29] Document multi-label vertex support - Upgrade docs: user guide for with("multilabel")/with("singlelabel"), provider guide for LabelCardinality and step overrides - Reference docs: elementMap/valueMap label format, TinkerGraph configuration - Provider semantics: valueMap with-options section - For-committers: @MultiLabel/@MultiLabelDefault/@SingleLabelDefault gherkin tags - CHANGELOG entry --- CHANGELOG.asciidoc | 7 + .../src/dev/developer/for-committers.asciidoc | 11 ++ .../dev/provider/gremlin-semantics.asciidoc | 92 +++++++++++ .../implementations-tinkergraph.asciidoc | 26 ++++ docs/src/reference/the-traversal.asciidoc | 147 ++++++++++++++++++ docs/src/upgrade/release-4.x.x.asciidoc | 118 ++++++++++++++ 6 files changed, 401 insertions(+) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 31085a634c3..797db7772f7 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -44,6 +44,13 @@ image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima * Changed `Tree` to no longer extend `HashMap`; it is now a final class with a tree-shaped API (`childAt`, `hasChild`, `contains`, `findSubtree`, `getOrCreateChild`, `getNodesAtDepth`, `getLeafNodes`, `nodeCount`) and is no longer a `Map`. * Changed `count(local)` on a `Tree` to return the total node count (`Tree.nodeCount()`) instead of the root-entry count. * Changed Tree class in Java to not extend from HashMap and offered a new tree-shaped API for navigation. +* Added multi-label support for vertices with configurable `LabelCardinality` (`ONE`, `ONE_OR_MORE`, `ZERO_OR_MORE`). +* Added `labels()` step to emit each vertex label as a separate traverser. +* Added `addLabel()` and `dropLabel()`/`dropLabels()` steps for vertex label mutation. +* Added `LabelCardinalityValidator` utility for constraint enforcement separated from the `LabelCardinality` enum. +* Added `with("multilabel")` source configuration for `elementMap()` and `valueMap()` to return labels as a set. +* Added `LabelsDropVerificationStrategy` to prevent accidental `labels().drop()` usage. +* Added multi-label syntax to `addV()` and `mergeV()` with append-only `onMatch` semantics. * Added typed numeric wrappers and `preciseNumbers` connection option to `gremlin-javascript` for explicit control over numeric type serialization and deserialization. * Added `NextN(n)` to `Traversal` in `gremlin-go` for batched result iteration, providing API parity with `next(n)` in the Java, Python, and .NET GLVs, and updated the Go translators in `gremlin-core` and `gremlin-javascript` to emit `NextN(n)` for the batched form. * Added Provider Defined Types (PDT) support — graph providers can define custom types via `@ProviderDefined` annotation that serialize/deserialize seamlessly across all GLVs without driver-side configuration. Replaces TP3 custom type mechanism. diff --git a/docs/src/dev/developer/for-committers.asciidoc b/docs/src/dev/developer/for-committers.asciidoc index 7340f3c12ef..25f5fa88c24 100644 --- a/docs/src/dev/developer/for-committers.asciidoc +++ b/docs/src/dev/developer/for-committers.asciidoc @@ -672,8 +672,19 @@ TinkerGQL support out of the box and runs these scenarios without any additional * `@InsertionOrderingRequired` - The scenario is reliant on the graph system predictably returning results (vertices, edges, properties) in the same order in which they were inserted into the graph. * `@MetaProperties` - The scenario makes use of meta-properties. +* `@MultiLabel` - The scenario requires a graph that supports multi-label vertices (i.e. +`ZERO_OR_MORE` vertex label cardinality). Providers that only support single-label vertices should +exclude these tests. +* `@MultiLabelDefault` - The scenario expects multi-label output as the default behavior for +`valueMap()`/`elementMap()`. The test infrastructure applies `with("multilabel")` to the traversal +source automatically (similar to how `@GraphComputerOnly` applies `withComputer()`). This simulates +a provider whose `valueMap()`/`elementMap()` returns labels as a set by default. Combined with +`@MultiLabel` to require a multi-label graph. * `@MultiProperties` - The scenario makes use of multi-properties. * `@SupportsChar` - The scenario requires that `char` is completely supported. +* `@SingleLabelDefault` - The scenario asserts that `valueMap()`/`elementMap()` returns a single +label string by default (without any `with()` configuration). Providers that override this behavior +to return labels as a set by default should exclude these tests. * `@SupportsDuration` - The scenario requires that `Duration` is completely supported. * `@SupportsBinary` - The scenario requires that `Binary` is completely supported. * `@UserSuppliedVertexIds` - The scenario relies on the vertex IDs specified in the dataset used by the scenario. diff --git a/docs/src/dev/provider/gremlin-semantics.asciidoc b/docs/src/dev/provider/gremlin-semantics.asciidoc index 1ec854de20d..2262e81a57a 100644 --- a/docs/src/dev/provider/gremlin-semantics.asciidoc +++ b/docs/src/dev/provider/gremlin-semantics.asciidoc @@ -587,6 +587,69 @@ See: link:https://github.com/apache/tinkerpop/tree/x.y.z/gremlin-core/src/main/j link:https://github.com/apache/tinkerpop/tree/x.y.z/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStartStep.java[source (start)], link:https://tinkerpop.apache.org/docs/x.y.z/reference/#addvertex-step[reference] +=== addLabel() + +*Description:* Adds one or more labels to an element. + +*Syntax:* `addLabel(String label, String... moreLabels)` | `addLabel(Traversal labelTraversal)` + +[width="100%",options="header"] +|========================================================= +|Start Step |Mid Step |Modulated |Domain |Range +|N |Y |N |`Element` |`Element` +|========================================================= + +*Arguments:* + +* `label` - The label to add. +* `moreLabels` - Additional labels to add. +* `labelTraversal` - A traversal that produces labels to add. + +*Considerations:* + +The graph must be configured with a `LabelCardinality` that supports mutation (`ONE_OR_MORE` or `ZERO_OR_MORE`). +Adding a label that already exists on the element is a no-op. The step returns the element (sideEffect semantics). + +*Exceptions* + +* If the graph's `LabelCardinality` does not support mutation, an `IllegalStateException` will be thrown. +* If any label argument is null or empty, an `IllegalArgumentException` will be thrown. + +See: link:https://github.com/apache/tinkerpop/tree/x.y.z/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/AddLabelStep.java[source], +link:https://tinkerpop.apache.org/docs/x.y.z/reference/#addlabel-step[reference] + +=== dropLabel() + +*Description:* Removes one or more specific labels from an element, or removes all labels. + +*Syntax:* `dropLabel(String label, String... moreLabels)` | `dropLabel(Traversal labelTraversal)` | `dropLabels()` + +[width="100%",options="header"] +|========================================================= +|Start Step |Mid Step |Modulated |Domain |Range +|N |Y |N |`Element` |`Element` +|========================================================= + +*Arguments:* + +* `label` - The label to remove. +* `moreLabels` - Additional labels to remove. +* `labelTraversal` - A traversal that produces labels to remove. + +*Considerations:* + +The graph must be configured with a `LabelCardinality` that supports mutation. Dropping a label that does not exist +on the element is a no-op. `dropLabels()` removes all labels (only valid with `ZERO_OR_MORE`). The step returns the +element (sideEffect semantics). + +*Exceptions* + +* If the graph's `LabelCardinality` does not support mutation, an `IllegalStateException` will be thrown. +* If `dropLabel()` or `dropLabels()` would violate the minimum label count, an `IllegalStateException` will be thrown. + +See: link:https://github.com/apache/tinkerpop/tree/x.y.z/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/DropLabelsStep.java[source], +link:https://tinkerpop.apache.org/docs/x.y.z/reference/#droplabel-step[reference] + [[all-step]] === all() @@ -1667,6 +1730,29 @@ See: link:https://github.com/apache/tinkerpop/tree/x.y.z/gremlin-core/src/main/j link:https://github.com/apache/tinkerpop/tree/x.y.z/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/LengthLocalStep.java[source (local)], link:https://tinkerpop.apache.org/docs/x.y.z/reference/#length-step[reference] +=== labels() + +*Description:* Maps an element to its labels, emitting one traverser per label. + +*Syntax:* `labels()` + +[width="100%",options="header"] +|========================================================= +|Start Step |Mid Step |Modulated |Domain |Range +|N |Y |N |`Element` |`String` +|========================================================= + +*Considerations:* + +For vertices with a single label, `labels()` emits one traverser (equivalent to `label()`). For multi-label vertices, +it emits one traverser per label. For vertices with zero labels (`ZERO_OR_MORE` mode), no traversers are emitted. + +`hasLabel("a", "b")` uses OR semantics: it matches any vertex that has label "a" or label "b". To match vertices +with all specified labels, chain calls: `hasLabel("a").hasLabel("b")`. + +See: link:https://github.com/apache/tinkerpop/tree/x.y.z/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/LabelsStep.java[source], +link:https://tinkerpop.apache.org/docs/x.y.z/reference/#labels-step[reference] + [[local-step]] === local() @@ -2564,6 +2650,12 @@ the possible values are: ** 8 for including values (VertexProperty) ** 15 for including all +The label format in the map is controlled by source-level `with("multilabel")` and `with("singlelabel")` options, not +by the graph's `LabelCardinality`. When `"multilabel"` is present and `"singlelabel"` is not, the label value is a +`Set`; otherwise it is a single `String`. When both options are present on the same source, `"singlelabel"` +always takes precedence regardless of the order in which they were applied. Providers who want label output tied to +their cardinality configuration should override `PropertyMapStep` and/or `ElementMapStep`. + See: link:https://github.com/apache/tinkerpop/tree/x.y.z/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/PropertyMapStep.java[source], link:https://tinkerpop.apache.org/docs/x.y.z/reference/#valuemap-step[reference], link:https://tinkerpop.apache.org/docs/x.y.z/reference/#propertymap-step[reference] diff --git a/docs/src/reference/implementations-tinkergraph.asciidoc b/docs/src/reference/implementations-tinkergraph.asciidoc index f9ce642b74d..9c238618936 100644 --- a/docs/src/reference/implementations-tinkergraph.asciidoc +++ b/docs/src/reference/implementations-tinkergraph.asciidoc @@ -166,6 +166,7 @@ TinkerGraph has several settings that can be provided on creation via `Configura |gremlin.tinkergraph.vertexPropertyIdManager |The `IdManager` implementation to use for vertex properties. |gremlin.tinkergraph.defaultVertexPropertyCardinality |The default `VertexProperty.Cardinality` to use when `Vertex.property(k,v)` is called. |gremlin.tinkergraph.allowNullPropertyValues |A boolean value that determines whether or not `null` property values are allowed and defaults to `false`. +|gremlin.tinkergraph.vertexLabelCardinality |The `LabelCardinality` for vertices. Options are `ONE` (default, single immutable label, backward-compatible with 3.x), `ONE_OR_MORE` (at least one label, mutable), or `ZERO_OR_MORE` (fully flexible, zero or more labels). See <>. |gremlin.tinkergraph.graphLocation |The path and file name for where TinkerGraph should persist the graph data. If a value is specified here, the `gremlin.tinkergraph.graphFormat` should also be specified. If this value is not included (default), then the graph will stay in-memory and not be loaded/persisted to disk. @@ -219,6 +220,31 @@ g.io("data/tinkerpop-crew.kryo").read().iterate() g.V().properties() ---- +[[tinkergraph-multi-label]] +=== Multi-Label Support + +TinkerGraph supports multiple labels per vertex when `gremlin.tinkergraph.vertexLabelCardinality` is set to +`ONE_OR_MORE` or `ZERO_OR_MORE`. This enables use of the `addLabel()`, `dropLabel()`, `dropLabels()`, and `labels()` +traversal steps. + +[source,groovy] +---- +conf = new BaseConfiguration() +conf.setProperty("gremlin.tinkergraph.vertexLabelCardinality", "ZERO_OR_MORE") +graph = TinkerGraph.open(conf) +g = traversal().with(graph) +g.addV("person","employee").property("name","marko").iterate() +g.V().has("name","marko").labels() // emits: person, employee +g.V().has("name","marko").addLabel("manager").labels() // emits: person, employee, manager +g.V().has("name","marko").dropLabel("employee").labels() // emits: person, manager +---- + +The `hasLabel()` step works with multi-label vertices using OR semantics: `hasLabel("a","b")` matches any vertex that +has label "a" OR label "b". To match vertices having both labels, chain multiple `hasLabel()` calls: +`hasLabel("a").hasLabel("b")`. + +IMPORTANT: Edge labels are currently fixed at cardinality `ONE` and are not configurable. + [[tinkergraph-gremlin-tx]] === Transactions diff --git a/docs/src/reference/the-traversal.asciidoc b/docs/src/reference/the-traversal.asciidoc index b085b4a0848..49fd4137d19 100644 --- a/docs/src/reference/the-traversal.asciidoc +++ b/docs/src/reference/the-traversal.asciidoc @@ -640,6 +640,33 @@ link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gre link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#addV(java.lang.String)++[`addV(String)`], link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#addV(org.apache.tinkerpop.gremlin.process.traversal.Traversal)++[`addV(Traversal)`] +[[addlabel-step]] +=== AddLabel Step + +The `addLabel()`-step (*sideEffect*) adds one or more labels to an element. The graph must be configured with a +`LabelCardinality` that supports mutation (e.g. `ZERO_OR_MORE` or `ONE_OR_MORE`). Adding a label that already exists +on the element is a no-op. + +[gremlin-groovy] +---- +conf = new BaseConfiguration() +conf.setProperty("gremlin.tinkergraph.vertexLabelCardinality", "ZERO_OR_MORE") +graph = TinkerGraph.open(conf) +g = traversal().with(graph) +g.addV('person').property('name','marko').addLabel('employee').labels() +g.V().has('name','marko').hasLabel('person') +g.V().has('name','marko').hasLabel('employee') +---- + +NOTE: When `LabelCardinality` is set to `ONE` (the default), calling `addLabel()` will throw an +`IllegalStateException` since labels are immutable in that mode. + +See: <>, <> + +*Additional References* + +link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#addLabel(java.lang.String,java.lang.String...)++[`addLabel(String, String...)`] + [[aggregate-step]] === [[store-step]]Aggregate Step @@ -1763,6 +1790,39 @@ g.V() * link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#drop()++[`drop()`] +[[droplabel-step]] +=== DropLabel Step + +The `dropLabel()`-step (*sideEffect*) removes one or more specific labels from an element. The `dropLabels()`-step +removes all labels. The graph must be configured with a `LabelCardinality` that supports mutation. Dropping a label +that does not exist on the element is a no-op. + +[gremlin-groovy] +---- +conf = new BaseConfiguration() +conf.setProperty("gremlin.tinkergraph.vertexLabelCardinality", "ZERO_OR_MORE") +graph = TinkerGraph.open(conf) +g = traversal().with(graph) +v = g.addV('person').property('name','marko').addLabel('employee').addLabel('manager').next() +g.V(v).labels() +g.V(v).dropLabel('manager').labels() +g.V(v).dropLabels().labels() +---- + +NOTE: With `LabelCardinality.ONE_OR_MORE`, `dropLabels()` always throws and `dropLabel()` throws if it would +leave the element with zero labels. + +IMPORTANT: Do not confuse `dropLabel()` with `labels().drop()`. The latter attempts to drop the string traversers +emitted by `labels()` and is blocked by the `LabelsDropVerificationStrategy`. Use `dropLabel()` or `dropLabels()` to +remove labels from vertices. + +See: <>, <> + +*Additional References* + +link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#dropLabel(java.lang.String,java.lang.String...)++[`dropLabel(String, String...)`], +link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#dropLabels()++[`dropLabels()`] + [[e-step]] === E Step @@ -1830,6 +1890,43 @@ g.V().has('name','marko').properties('location').elementMap() IMPORTANT: The `elementMap()`-step does not return the vertex labels for incident vertices when using `GraphComputer` as the `id` is the only available data to the star graph. +For multi-label graphs, `elementMap()` returns labels as a single string by default. To receive all labels as a +set, use `with("multilabel")` either per-traversal or on a persistent source: + +[gremlin-groovy] +---- +conf = new BaseConfiguration() +conf.setProperty("gremlin.tinkergraph.vertexLabelCardinality", "ZERO_OR_MORE") +graph = TinkerGraph.open(conf) +g = traversal().with(graph) +g.addV('person').property('name','marko').addLabel('employee').addLabel('manager').iterate() +g.V().has('name','marko').elementMap() <1> +g.with("multilabel").V().has('name','marko').elementMap() <2> +gml = g.with("multilabel") +gml.V().has('name','marko').elementMap() <3> +gml.V().has('name','marko').valueMap(true) <4> +---- + +<1> Without `"multilabel"`, the label entry is a single string. +<2> Per-traversal `with("multilabel")` returns all labels as a set. +<3> A persistent source avoids repeating `with("multilabel")` on each traversal. +<4> `valueMap(true)` also respects the `"multilabel"` configuration. + +When a source is configured with `with("multilabel")`, the `with("singlelabel")` option can be used per-traversal to +force single-string label output: + +[gremlin-groovy] +---- +gml = g.with("multilabel") +gml.V().has('name','marko').elementMap() +gml.with("singlelabel").V().has('name','marko').elementMap() +---- + +NOTE: The `elementMap()` step determines label format solely from the `with("multilabel")`/`with("singlelabel")` source +options, not from the graph's `LabelCardinality` setting. When both options are present on the same source, +`"singlelabel"` always takes precedence regardless of the order in which they were applied. The single-string label +path is expected to be deprecated in a future release, at which point labels will always be returned as a set. + *Additional References* link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#elementMap(java.lang.String...)++[`elementMap(String...)`] @@ -2550,10 +2647,37 @@ g.V(1).outE().label() g.V(1).properties().label() ---- +NOTE: For graphs that support multiple labels per vertex, `label()` returns only one label and the choice is +non-deterministic (implementation-dependent ordering). Use the <> to retrieve all labels +reliably. + *Additional References* link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#label()++[`label()`] +[[labels-step]] +=== Labels Step + +The `labels()`-step (*flatMap*) takes an `Element` and emits each of its labels as a separate traverser. For elements +with a single label this behaves identically to `label()`. For multi-label elements, it emits one traverser per label. + +[gremlin-groovy] +---- +conf = new BaseConfiguration() +conf.setProperty("gremlin.tinkergraph.vertexLabelCardinality", "ZERO_OR_MORE") +graph = TinkerGraph.open(conf) +g = traversal().with(graph) +g.addV('person').property('name','marko').addLabel('employee').iterate() +g.V().labels() +g.V().labels().count() +---- + +See: <>, <>, <> + +*Additional References* + +link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#labels()++[`labels()`] + [[length-step]] === Length Step @@ -5432,6 +5556,29 @@ g.V().hasLabel('person').valueMap('name').with(WithOptions.tokens, WithOptions.l g.V().hasLabel('person').properties('location').valueMap().with(WithOptions.tokens, WithOptions.values) ---- +For multi-label graphs, the label value in the map is a single string by default. Use `with("multilabel")` to +receive all labels as a set: + +[gremlin-groovy] +---- +conf = new BaseConfiguration() +conf.setProperty("gremlin.tinkergraph.vertexLabelCardinality", "ZERO_OR_MORE") +graph = TinkerGraph.open(conf) +g = traversal().with(graph) +g.addV('person').property('name','marko').addLabel('employee').iterate() +g.V().has('name','marko').valueMap(true) <1> +g.with("multilabel").V().has('name','marko').valueMap(true) <2> +---- + +<1> Without `"multilabel"`, the label entry is a single string. +<2> With `"multilabel"`, all labels are returned as a set. + +When a source is configured with `with("multilabel")`, the `with("singlelabel")` option can be used per-traversal to +force single-string label output. When both options are present on the same source, `"singlelabel"` always takes +precedence regardless of the order in which they were applied. The `valueMap()` step determines label format solely +from the source options, not from the graph's `LabelCardinality` setting. The single-string label path is expected to +be deprecated in a future release. + *Additional References* link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#valueMap(java.lang.String...)++[`valueMap(String...)`] diff --git a/docs/src/upgrade/release-4.x.x.asciidoc b/docs/src/upgrade/release-4.x.x.asciidoc index 7bbe5d64383..fe69467a643 100644 --- a/docs/src/upgrade/release-4.x.x.asciidoc +++ b/docs/src/upgrade/release-4.x.x.asciidoc @@ -186,6 +186,93 @@ For full details on the interceptor API for each language variant, refer to the each GLV's documentation in the link:https://tinkerpop.apache.org/docs/x.y.z/reference/#gremlin-drivers-variants[Gremlin Drivers and Variants] reference. +==== Multi-Label Support + +Until now, vertices in the property graph model were limited to a single, immutable label assigned at creation. This +release introduces configurable label cardinality, allowing vertices to carry multiple labels that can be added and +removed over their lifetime. + +The feature is controlled by `LabelCardinality`, a graph-level setting that defaults to `ONE`, preserving the +existing single-label behavior. To enable multi-label in TinkerGraph, set the vertex label cardinality in the graph +properties: + +``` +gremlin.tinkergraph.vertexLabelCardinality=ZERO_OR_MORE +``` + +The three modes are `ONE` (single, immutable, the 3.x default), `ONE_OR_MORE` (mutable, minimum one), and +`ZERO_OR_MORE` (fully flexible). Edge labels remain fixed at `ONE`. + +With multi-label enabled, several new traversal steps become available: + +```text +gremlin> g.addV('person','employee').property('name','marko') +==>v[0] +gremlin> g.V().has('name','marko').addLabel('manager') +==>v[0] +gremlin> g.V().has('name','marko').labels() +==>person +==>employee +==>manager +gremlin> g.V().has('name','marko').dropLabel('employee').labels() +==>person +==>manager +``` + +The `labels()` step is a flatMap that emits one traverser per label, unlike `label()` which returns a single string. +For single-label vertices the two behave identically. + +The `mergeV()` step accepts a list for `T.label` to match or create multi-label vertices. The `onMatch` option uses +append-only semantics: new labels are added, existing labels are preserved: + +```text +gremlin> g.mergeV([(T.label): ['person','employee'], name: 'marko']) +==>v[0] +gremlin> g.mergeV([(T.label): 'person', name: 'marko']).option(Merge.onMatch, [(T.label): 'director']) +==>v[0] +``` + +By default, `elementMap()` and `valueMap()` continue to return labels as a single string. To receive all labels as a +set, use `with("multilabel")` either per-traversal or as a persistent source configuration: + +```text +// assuming a vertex with labels [person, employee, manager] and name "marko" +gremlin> g.with("multilabel").V().has('name','marko').elementMap() +==>{id=0, label=[manager, person, employee], name=marko} +gremlin> g.V().has('name','marko').elementMap() +==>{id=0, label=manager, name=marko} +``` + +To avoid repeating `with("multilabel")` on every traversal, create a persistent source: + +```text +gremlin> gml = g.with("multilabel") +==>graphtraversalsource[tinkergraph[vertices:1 edges:0], standard] +gremlin> gml.V().has('name','marko').elementMap() +==>{id=0, label=[manager, person, employee], name=marko} +gremlin> gml.V().has('name','marko').valueMap(true) +==>{id=0, label=[manager, person, employee], name=[marko]} +``` + +Note the following behavioral details: + +* The deprecated `label()` step returns a single, non-deterministic label when multiple labels are present. Always + use `labels()` to retrieve the full set reliably. +* When a source is configured with `with("multilabel")`, the label output can be forced back to a single string + per-traversal using `with("singlelabel")`. This is useful for providers that enable multi-label output by default + but need an escape hatch for backward compatibility. Note that when both options are present on the same source, + `"singlelabel"` always takes precedence regardless of the order in which they were applied. +* Serialization via GraphSON V1/V2/V3 or Gryo only transmits one label per vertex (using `label()`). Multi-label data + will be silently reduced to a single label when passing through these older formats. Only GraphBinary (V4) and + GraphSON V4 preserve the full label set. +* Adding a label that already exists on the vertex is a no-op (when multi-label is enabled). Dropping a label that + does not exist is also a no-op. With cardinality `ONE`, both `addLabel()` and `dropLabel()` throw since labels are + immutable. +* `hasLabel()` tests the predicate against each label on the element and returns `true` if at least one label + satisfies it. For multi-label vertices, this means `hasLabel(P.neq("person"))` returns `true` when the vertex also + carries other labels, a natural consequence of testing a negation predicate against a set of labels, where the + non-matching labels will satisfy `neq`. To exclude vertices that carry a specific label, use the negation pattern: + `g.V().not(__.hasLabel("person"))`. For single-label vertices the behavior is unchanged. ==== Gremlator @@ -800,6 +887,37 @@ Kryo's default `Map` serializer because it extended `HashMap`, whereas in 4.x it serializer. As a result, Gryo-serialized `Tree` data written by 3.x cannot be read by 4.x and vice versa. The Gryo type registration id (`61`) is unchanged and 4.x-to-4.x Gryo round-trips correctly; this Gryo break is expected for the 4.0.0 major release. +===== Multi-Label Support + +Providers declare their supported label cardinality via `Graph.Features.getLabelCardinality()`, which defaults to +`LabelCardinality.ONE` (single label, immutable, the 3.x behavior). To support multi-label, return `ONE_OR_MORE` or +`ZERO_OR_MORE` from this method. + +The `LabelCardinality` enum exposes `min()`, `max()`, and `supportsMutation()` for programmatic introspection of +constraints. Constraint enforcement is handled by the `LabelCardinalityValidator` utility class, which providers may +use directly or replace with their own validation logic tailored to their storage backend. + +Providers implementing multi-label must: + +- Store and return a `Set` from `Element.labels()` +- Implement `Element.addLabel(String, String...)` and `Element.dropLabel(String, String...)` +- Ensure `hasLabel("a", "b")` uses OR semantics (matches vertices with label "a" or "b") +- Serialize/deserialize the label set via the V4 GraphBinary format (which sends labels as a list) + +The default `elementMap()` and `valueMap()` implementations determine label output format solely from the +`with("multilabel")` source option, not from the graph's `LabelCardinality`. This means a multi-label graph still +returns a single label string from these steps unless `with("multilabel")` is explicitly configured. The intent is +to eventually deprecate the single-string path entirely and always return labels as a set, aligning `elementMap()` +and `valueMap()` with `labels()`. Until then, `with("singlelabel")` allows users to override a source-level +`with("multilabel")` back to single-string output. + +Providers who want `elementMap()`/`valueMap()` to return the full label set by default (without requiring users to +set `with("multilabel")`) should override `PropertyMapStep` and `ElementMapStep` to tie label output to their +server-side label cardinality configuration. In that case, `with("singlelabel")` should still be respected as an +explicit user override back to single-string output. + +Older serialization formats (GraphSON V1/V2/V3, Gryo) only support a single label and will silently use the value +from `Element.label()` (the deprecated single-label accessor). ==== Graph Driver Providers From 6a3c91ed07fd687b2816bc8d75b98d0160d105f6 Mon Sep 17 00:00:00 2001 From: Cole Greer Date: Tue, 30 Jun 2026 10:24:02 -0700 Subject: [PATCH 05/29] Reconstruct source-spawned multi-label addV as a single step The grammar visitor for a source-spawned multi-label addV (e.g. g.addV('a','b')) decomposed the call into addV('a').addLabel('b'), producing a structurally different traversal than the user wrote. TraversalSourceSpawnMethodVisitor now calls the native multi-arg addV(String, String, String...) directly, matching the mid-traversal TraversalMethodVisitor and yielding a single atomic addV step. Assisted-by: Kiro:claude-opus-4.8 --- .../TraversalSourceSpawnMethodVisitor.java | 4 +--- .../grammar/TraversalMethodVisitorTest.java | 24 +++++++++++++++++++ .../process/traversal/GremlinLangTest.java | 2 ++ 3 files changed, 27 insertions(+), 3 deletions(-) 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 cd50ccc1c92..ef11c8d3d97 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 @@ -90,8 +90,6 @@ public GraphTraversal visitTraversalSourceSpawnMethod_addV(final GremlinParser.T // Multi-label: addV("a", "b", ...) final Object firstLiteralOrVar = antlr.argumentVisitor.visitStringArgument(stringArgs.get(0)); final String firstLabel = firstLiteralOrVar instanceof String ? (String) firstLiteralOrVar : ((GValue) 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) secondLiteralOrVar).get(); final String[] moreLabels = new String[stringArgs.size() - 2]; @@ -99,7 +97,7 @@ public GraphTraversal visitTraversalSourceSpawnMethod_addV(final GremlinParser.T final Object literalOrVar = antlr.argumentVisitor.visitStringArgument(stringArgs.get(i)); moreLabels[i - 2] = literalOrVar instanceof String ? (String) literalOrVar : ((GValue) literalOrVar).get(); } - return t.addLabel(secondLabel, moreLabels); + return this.traversalSource.addV(firstLabel, secondLabel, moreLabels); } } else if (ctx.nestedTraversal() != null) { return this.traversalSource.addV(anonymousVisitor.visitNestedTraversal(ctx.nestedTraversal())); diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalMethodVisitorTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalMethodVisitorTest.java index 30e3ebb0c67..c691d9884d6 100644 --- a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalMethodVisitorTest.java +++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalMethodVisitorTest.java @@ -108,6 +108,30 @@ public void shouldParseTraversalMethod_addV_Traversal() throws Exception { compare(g.addV(V().hasLabel("person").label()), eval("g.addV(V().hasLabel(\"person\").label())")); } + @Test + public void shouldParseTraversalMethod_addV_MultipleLabels() throws Exception { + // mid-traversal multi-label addV + compare(g.V().addV("dog", "pet"), eval("g.V().addV(\"dog\",\"pet\")")); + } + + @Test + public void shouldParseTraversalMethod_addV_MultipleLabelsThree() throws Exception { + // mid-traversal multi-label addV with three labels + compare(g.V().addV("dog", "pet", "animal"), eval("g.V().addV(\"dog\",\"pet\",\"animal\")")); + } + + @Test + public void shouldParseSourceSpawnMethod_addV_MultipleLabels() throws Exception { + // source-spawned multi-label addV produces same structure as mid-traversal + compare(g.addV("dog", "pet"), eval("g.addV(\"dog\",\"pet\")")); + } + + @Test + public void shouldParseSourceSpawnMethod_addV_MultipleLabelsThree() throws Exception { + // source-spawned multi-label addV with three labels + compare(g.addV("dog", "pet", "animal"), eval("g.addV(\"dog\",\"pet\",\"animal\")")); + } + @Test public void shouldParseTraversalMethod_aggregate() throws Exception { compare(g.V().aggregate("test"), eval("g.V().aggregate('test')")); diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLangTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLangTest.java index e5bf95a6f39..1109dcedf76 100644 --- a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLangTest.java +++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLangTest.java @@ -91,6 +91,8 @@ public static Iterable generateTestParameters() { {g.V().count(), "g.V().count()"}, {g.addV("test"), "g.addV(\"test\")"}, {g.addV("t\"'est"), "g.addV(\"t\\\"'est\")"}, + {g.addV("dog", "pet"), "g.addV(\"dog\",\"pet\")"}, + {g.addV("dog", "pet", "animal"), "g.addV(\"dog\",\"pet\",\"animal\")"}, {g.inject(true, (byte) 1, (short) 2, 3, 4L, 5f, 6d, BigInteger.valueOf(7L), BigDecimal.valueOf(8L)), "g.inject(true,1B,2S,3,4L,5.0F,6.0D,7N,8M)"}, {g.inject(Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY), From 99f496a86e64ec865553e5659a2185ea4ec53ae4 Mon Sep 17 00:00:00 2001 From: Cole Greer Date: Tue, 30 Jun 2026 13:59:51 -0700 Subject: [PATCH 06/29] Preserve GValue variables through multi-label addV and simplify the addV API Variable (GValue) bindings were collapsed to plain strings on the multi-label addV path, losing their variable-ness. The label is now threaded through end-to-end so variables survive parsing, the GraphTraversal API, and step storage, mirroring the single-label addV(GValue) path. The addV overloads are also simplified for 4.0.0: the single-arg addV(String)/addV(GValue) and the (first, second, more) forms are replaced by unified addV(String first, String... more) and addV(GValue first, GValue... more). A single label (empty 'more') keeps the prior scalar storage and serialization; two or more labels use the multi-label set path. Traversal-based addV remains single-arg for now. The Add*StepPlaceholder hierarchy stores multi-labels via a single Set constructor that registers any GValue variables, removing the redundant gvalueMarker constructor overload. Note: dropping the public addV(String)/addV(GValue) overloads is a binary-incompatible change appropriate for the 4.x major line. Assisted-by: Kiro:claude-opus-4.8 --- .../process/traversal/dsl/GremlinDsl.java | 2 +- .../traversal/dsl/GremlinDslProcessor.java | 8 +- .../grammar/TraversalMethodVisitor.java | 39 +++++-- .../TraversalSourceSpawnMethodVisitor.java | 38 +++++-- .../traversal/dsl/graph/GraphTraversal.java | 75 +++++++------ .../dsl/graph/GraphTraversalSource.java | 80 ++++++------- .../process/traversal/dsl/graph/__.java | 20 ++-- .../AbstractAddElementStepPlaceholder.java | 63 ++++++++++- .../map/AbstractAddVertexStepPlaceholder.java | 2 +- .../map/AddVertexStartStepPlaceholder.java | 14 ++- .../step/map/AddVertexStepPlaceholder.java | 15 ++- .../grammar/TraversalRootVisitorTest.java | 29 +++++ .../traversal/step/map/AddVertexStepTest.java | 105 +++++++++++++++++- 13 files changed, 369 insertions(+), 121 deletions(-) diff --git a/gremlin-annotations/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/GremlinDsl.java b/gremlin-annotations/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/GremlinDsl.java index b03153a9464..1d84d8d8e93 100644 --- a/gremlin-annotations/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/GremlinDsl.java +++ b/gremlin-annotations/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/GremlinDsl.java @@ -68,7 +68,7 @@ * *