This is an automated email from the ASF dual-hosted git repository. xiazcy pushed a commit to branch multi-label-experiment in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
commit 2f26659d8d84bca573ef4758d6500618fcde40f3 Author: Yang Xia <[email protected]> AuthorDate: Wed Jun 24 08:34:11 2026 -0700 Make TinkerGraph default to ONE for labels, add strategy to throw for unintended drop() usage on labels() --- .../gremlin-server/gremlin-server-integration.yaml | 3 + .../process/traversal/TraversalStrategies.java | 3 + .../step/map/AbstractAddVertexStepPlaceholder.java | 6 + .../traversal/step/map/AddVertexStartStep.java | 15 +- .../step/map/AddVertexStartStepPlaceholder.java | 13 +- .../LabelsDropVerificationStrategy.java | 66 ++++ .../apache/tinkerpop/gremlin/structure/Graph.java | 23 +- .../gremlin/structure/LabelCardinality.java | 113 +++--- .../structure/util/detached/DetachedVertex.java | 2 +- .../LabelsDropVerificationStrategyTest.java | 85 +++++ .../Gherkin/CommonSteps.cs | 30 +- .../Gherkin/GherkinTestRunner.cs | 6 - .../Gherkin/IgnoreException.cs | 7 +- .../Gherkin/ScenarioData.cs | 18 + gremlin-go/driver/cucumber/cucumberSteps_test.go | 25 +- gremlin-go/driver/cucumber/cucumberWorld.go | 22 +- .../test/cucumber/feature-steps.js | 17 +- .../gremlin-javascript/test/cucumber/world.js | 27 +- .../src/main/python/tests/feature/feature_steps.py | 21 +- .../src/main/python/tests/feature/terrain.py | 10 + .../conf/tinkergraph-multilabel.properties | 21 ++ .../gremlin/driver/remote/RemoteWorld.java | 18 + .../server/util/CheckedGraphManagerTest.java | 2 +- .../server/util/DefaultGraphManagerTest.java | 10 +- .../gremlin/server/gremlin-server-integration.yaml | 3 + .../test/scripts/tinkergraph-multilabel.properties | 21 ++ .../tinkerpop/gremlin/features/StepDefinition.java | 14 +- .../apache/tinkerpop/gremlin/features/World.java | 12 + .../gremlin/test/features/map/Labels.feature | 4 +- .../test/features/sideEffect/AddLabel.feature | 2 +- .../test/features/sideEffect/DropLabel.feature | 4 +- .../tinkergraph/structure/AbstractTinkerGraph.java | 11 - .../gremlin/tinkergraph/structure/TinkerEdge.java | 45 +-- .../gremlin/tinkergraph/structure/TinkerGraph.java | 8 +- .../structure/TinkerTransactionGraph.java | 8 +- .../tinkergraph/structure/TinkerVertex.java | 60 ++-- .../tinkerpop/gremlin/tinkergraph/TinkerWorld.java | 1 + .../process/traversal/step/map/LabelsStepTest.java | 10 +- .../traversal/step/map/MergeVMultiLabelTest.java | 6 +- .../step/sideEffect/LabelMutationPropertyTest.java | 6 +- .../step/sideEffect/LabelMutationStepTest.java | 15 +- .../structure/LabelCardinalityTest.java | 396 +++++++++++++++++++++ .../LabelReplacePatternValidationTest.java | 7 +- .../structure/MergeOnMatchLabelPatternsTest.java | 7 +- .../TinkerVertexMultiLabelGremlinLangTest.java | 7 +- .../structure/TinkerVertexMultiLabelTest.java | 24 +- 46 files changed, 976 insertions(+), 258 deletions(-) diff --git a/docker/gremlin-server/gremlin-server-integration.yaml b/docker/gremlin-server/gremlin-server-integration.yaml index 1880b34810..a730970f85 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-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 bccb983665..509b9b7d8d 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.Prod 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 interface TraversalStrategies extends Serializable, Cloneable, Iterable<T // 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 interface TraversalStrategies extends Serializable, Cloneable, Iterable<T 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/step/map/AbstractAddVertexStepPlaceholder.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AbstractAddVertexStepPlaceholder.java index 3e0f0c1c0f..f14e9998d0 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 @@ -57,6 +57,12 @@ public abstract class AbstractAddVertexStepPlaceholder<S> extends AbstractAddEle 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 5f109ae426..4e256fc96b 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,24 +53,27 @@ public class AddVertexStartStep extends AbstractStep<Vertex, Vertex> 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<?, String> 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<String> labels) { super(traversal); - if (labels == null || labels.isEmpty()) { - this.internalParameters.set(this, T.label, Vertex.DEFAULT_LABEL); - userProvidedLabel = false; - } else { + if (labels != null && !labels.isEmpty()) { this.internalParameters.set(this, T.label, labels); userProvidedLabel = true; + } else { + userProvidedLabel = false; } } 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 a1b5fa2449..24c25d3ad4 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,7 +19,6 @@ 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; @@ -30,16 +29,15 @@ public class AddVertexStartStepPlaceholder extends AbstractAddVertexStepPlacehol implements AddVertexStepContract<Vertex>, GValueHolder<Vertex, Vertex> { 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<String> label) { - super(traversal, label == null ? GValue.of(Vertex.DEFAULT_LABEL) : label); + super(traversal, label); } public AddVertexStartStepPlaceholder(final Traversal.Admin traversal, final Traversal.Admin<?,String> vertexLabelTraversal) { - super(traversal, vertexLabelTraversal == null ? - new ConstantTraversal<>(Vertex.DEFAULT_LABEL) : (Traversal.Admin<Vertex,String>) vertexLabelTraversal); + super(traversal, vertexLabelTraversal == null ? null : (Traversal.Admin<Vertex,String>) vertexLabelTraversal); } public AddVertexStartStepPlaceholder(final Traversal.Admin traversal, final Set<String> labels) { @@ -56,7 +54,10 @@ public class AddVertexStartStepPlaceholder extends AbstractAddVertexStepPlacehol } else if (label instanceof GValue) { step = new AddVertexStartStep(traversal, ((GValue<String>) 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/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 0000000000..b13a5b1526 --- /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<TraversalStrategy.VerificationStrategy> + 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/Graph.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/Graph.java index fe6291e616..e8ec7e9fd7 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 @@ -660,11 +660,11 @@ public interface Graph extends AutoCloseable, Host { * 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#ZERO_OR_MORE} + * @return the label cardinality for vertices, defaulting to {@link LabelCardinality#ONE} * @since 4.0.0 */ default LabelCardinality getLabelCardinality() { - return LabelCardinality.ZERO_OR_MORE; + return LabelCardinality.ONE; } /** @@ -727,25 +727,14 @@ public interface Graph extends AutoCloseable, Host { } /** - * Gets the {@link LabelCardinality} for edges in this graph. Defines how many labels - * an edge can have and whether labels are mutable. + * 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, defaulting to {@link LabelCardinality#ZERO_OR_ONE} + * @return the label cardinality for edges, always {@link LabelCardinality#ONE} * @since 4.0.0 */ default LabelCardinality getLabelCardinality() { - return LabelCardinality.ZERO_OR_ONE; - } - - /** - * Gets the default label returned for edges with no explicit labels when the cardinality - * requires at least one label ({@link LabelCardinality#ONE} or {@link LabelCardinality#ONE_OR_MORE}). - * - * @return the default edge label, typically {@link Edge#DEFAULT_LABEL} - * @since 4.0.0 - */ - default String getDefaultLabel() { - return Edge.DEFAULT_LABEL; + 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 index 690fa75cef..28605e60d9 100644 --- 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 @@ -22,17 +22,8 @@ import java.util.HashSet; import java.util.Set; /** - * Defines the label cardinality for graph elements (vertices and edges). Providers declare their - * supported cardinality via {@link Graph.Features} and use the validation methods to enforce - * label mutation constraints. - * <p> - * The four cardinality modes are: - * <ul> - * <li>{@link #ONE} — Exactly one label, immutable (classic TinkerPop 3.x behavior)</li> - * <li>{@link #ZERO_OR_ONE} — Zero or one label, mutable, droppable to zero</li> - * <li>{@link #ONE_OR_MORE} — One or more labels, must always have at least one (virtual default fills when empty)</li> - * <li>{@link #ZERO_OR_MORE} — Zero or more labels, fully flexible</li> - * </ul> + * Defines the label cardinality for graph elements. Providers declare their supported cardinality + * via {@link Graph.Features} and use the validation methods to enforce label mutation constraints. * * @since 4.0.0 */ @@ -40,20 +31,14 @@ public enum LabelCardinality { /** * Exactly one label, immutable. All mutation operations throw. - * This provides backward compatibility with TinkerPop 3.x single-label semantics. + * This is the default for TinkerGraph and provides backward compatibility with TinkerPop 3.x. */ ONE, /** - * Zero or one label, mutable. The element can have at most one label at a time. - * To change the label, the existing one must be dropped first. - */ - ZERO_OR_ONE, - - /** - * One or more labels, mutable. The element must always have at least one label. - * When all explicit labels are removed, the provider's default label (e.g., "vertex" or "edge") - * is returned at read time as a virtual floor. + * One or more labels. The element must always have at least one label. + * The provider's default label (e.g., "vertex") is always physically present. + * {@code dropLabels()} always throws. {@code dropLabel(x)} succeeds only if at least one label remains. */ ONE_OR_MORE, @@ -65,8 +50,6 @@ public enum LabelCardinality { /** * Whether this cardinality allows multiple labels on an element simultaneously. - * - * @return {@code true} for {@link #ONE_OR_MORE} and {@link #ZERO_OR_MORE} */ public boolean supportsMultiLabel() { return this == ONE_OR_MORE || this == ZERO_OR_MORE; @@ -74,80 +57,78 @@ public enum LabelCardinality { /** * Whether this cardinality allows an element to have zero labels. - * - * @return {@code true} for {@link #ZERO_OR_ONE} and {@link #ZERO_OR_MORE} */ public boolean supportsZeroLabels() { - return this == ZERO_OR_ONE || this == ZERO_OR_MORE; + return this == ZERO_OR_MORE; } /** * Whether this cardinality allows label mutation (addLabel/dropLabel). - * - * @return {@code true} for all modes except {@link #ONE} */ public boolean supportsMutation() { return this != ONE; } /** - * Validates that adding the specified labels would not violate this cardinality's constraints. - * Must be called BEFORE any mutation to ensure no invalid intermediate states. - * - * @param currentLabels the element's current label set - * @param label the first label to add - * @param moreLabels additional labels to add (varargs) - * @throws IllegalStateException if the addition would violate cardinality constraints + * Validates that adding labels would not violate cardinality constraints. + * For ONE: always throws (immutable). + * For ONE_OR_MORE and ZERO_OR_MORE: always succeeds (multi-label add is unrestricted). */ public void validateAdd(final Set<String> currentLabels, final String label, final String... moreLabels) { if (!supportsMutation()) throw new IllegalStateException("Label mutation is not supported with cardinality " + this + ". Labels are immutable once assigned."); - - if (!supportsMultiLabel()) { - // ZERO_OR_ONE: resulting set must have at most 1 element - // Fast path: single label + empty set → always valid - if (moreLabels.length == 0 && currentLabels.isEmpty()) - return; - - // Compute the resulting set size - final Set<String> resultSet = new HashSet<>(currentLabels); - resultSet.add(label); - for (final String l : moreLabels) { - resultSet.add(l); - } - if (resultSet.size() > 1) - throw new IllegalStateException("Cannot add label: would result in " + resultSet.size() + - " labels but cardinality is " + this + ". Drop the existing label first."); - } } /** - * Validates that dropping a specific label would not violate this cardinality's constraints. - * For mutable cardinalities, dropping always succeeds (floor is applied at read time). - * - * @param currentLabels the element's current label set - * @param label the label to drop - * @throws IllegalStateException if mutation is not supported (ONE mode) + * Validates that dropping specific labels would not violate cardinality constraints. + * For ONE: always throws (immutable). + * For ONE_OR_MORE: throws if the resulting set would be empty. + * For ZERO_OR_MORE: always succeeds. */ - public void validateDrop(final Set<String> currentLabels, final String label) { + public void validateDrop(final Set<String> currentLabels, final String label, final String... moreLabels) { if (!supportsMutation()) throw new IllegalStateException("Label mutation is not supported with cardinality " + this + ". Labels are immutable once assigned."); - // No floor validation needed — floor is applied at read time by the element's labels() method + if (this == ONE_OR_MORE) { + final Set<String> result = new HashSet<>(currentLabels); + result.remove(label); + for (final String l : moreLabels) { + result.remove(l); + } + if (result.isEmpty()) + throw new IllegalStateException("Cannot drop label(s): would leave 0 labels but cardinality " + + this + " requires at least 1."); + } } /** - * Validates that dropping all labels would not violate this cardinality's constraints. - * For mutable cardinalities, dropping all always succeeds (floor is applied at read time). - * - * @param currentLabels the element's current label set - * @throws IllegalStateException if mutation is not supported (ONE mode) + * Validates that dropping all labels would not violate cardinality constraints. + * For ONE: always throws (immutable). + * For ONE_OR_MORE: always throws (would violate the ≥1 requirement). + * For ZERO_OR_MORE: always succeeds. */ public void validateDropAll(final Set<String> currentLabels) { if (!supportsMutation()) throw new IllegalStateException("Label mutation is not supported with cardinality " + this + ". Labels are immutable once assigned."); - // Always succeeds for mutable cardinalities — floor applied at read time + if (this == ONE_OR_MORE) + throw new IllegalStateException("Cannot drop all labels: cardinality " + this + + " requires at least 1 label."); + } + + /** + * Validates that a set of labels is valid for vertex creation under this cardinality. + * Called during element construction to ensure the initial label set conforms. + * For ONE: requires exactly 1 label. + * For ONE_OR_MORE: requires at least 1 label. + * For ZERO_OR_MORE: any size is valid. + */ + public void validateCreation(final Set<String> labels) { + if (this == ONE && labels.size() != 1) + throw new IllegalStateException("Vertex creation requires exactly 1 label with cardinality " + + this + ", got " + labels.size()); + if (this == ONE_OR_MORE && labels.isEmpty()) + throw new IllegalStateException("Vertex creation requires at least 1 label with cardinality " + this); } } 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 977eb8ebcc..b4ae187bc2 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 @@ -128,7 +128,7 @@ public class DetachedVertex extends DetachedElement<Vertex> implements Vertex { if (this.vertexLabels != null && !this.vertexLabels.isEmpty()) { return this.vertexLabels.iterator().next(); } - return this.label != null ? this.label : Vertex.DEFAULT_LABEL; + return this.label != null ? this.label : ""; } @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 0000000000..eefe785c2b --- /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<Object[]> 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-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/CommonSteps.cs b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/CommonSteps.cs index 2c42d1a8be..9729e7f419 100644 --- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/CommonSteps.cs +++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/CommonSteps.cs @@ -105,13 +105,27 @@ namespace Gremlin.Net.IntegrationTest.Gherkin [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"))); + + if (isMultiLabel && graphName == "empty") + { + ScenarioData.CleanMultilabelData(); + var data = ScenarioData.GetByGraphName("multilabel"); + _graphName = "multilabel"; + _g = Traversal().With(data.Connection); + } + else { - ScenarioData.CleanEmptyData(); + 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 +173,15 @@ namespace Gremlin.Net.IntegrationTest.Gherkin 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/GherkinTestRunner.cs b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/GherkinTestRunner.cs index 04969b5fae..6aee902762 100644 --- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/GherkinTestRunner.cs +++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/GherkinTestRunner.cs @@ -125,12 +125,6 @@ namespace Gremlin.Net.IntegrationTest.Gherkin continue; } - if (scenario.Tags.Any(t => t.Name == "@MultiLabel")) - { - failedSteps.Add(scenario.Steps.First(), new IgnoreException(IgnoreReason.MultiLabelStepsNotSupported)); - continue; - } - StepBlock? currentStep = null; StepDefinition? stepDefinition = null; foreach (var step in scenario.Steps) diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/IgnoreException.cs b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/IgnoreException.cs index bf3918f253..7001391004 100644 --- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/IgnoreException.cs +++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/IgnoreException.cs @@ -70,11 +70,6 @@ namespace Gremlin.Net.IntegrationTest.Gherkin /// <summary> /// write() is not supported yet for testing /// </summary> - WriteStepTestingNotSupported, - - /// <summary> - /// Multi-label steps (labels(), addLabel(), dropLabel(), dropLabels()) are not yet available in the .NET GLV - /// </summary> - MultiLabelStepsNotSupported + WriteStepTestingNotSupported } } \ No newline at end of file diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/ScenarioData.cs b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/ScenarioData.cs index 708b3db978..6644934ef8 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 @@ namespace Gremlin.Net.IntegrationTest.Gherkin 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,15 @@ namespace Gremlin.Net.IntegrationTest.Gherkin 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<string, ScenarioDataPerGraph> _dataPerGraph; public ScenarioData(IMessageSerializer messageSerializer) @@ -92,6 +107,9 @@ namespace Gremlin.Net.IntegrationTest.Gherkin var empty = new ScenarioDataPerGraph("empty", _connectionFactory.CreateRemoteConnection("ggraph"), new Dictionary<string, Vertex>(0), new Dictionary<string, Edge>(), new Dictionary<string, VertexProperty>()); _dataPerGraph.Add("empty", empty); + var multilabel = new ScenarioDataPerGraph("multilabel", _connectionFactory.CreateRemoteConnection("gmultilabel"), + new Dictionary<string, Vertex>(0), new Dictionary<string, Edge>(), new Dictionary<string, VertexProperty>()); + _dataPerGraph.Add("multilabel", multilabel); } private Dictionary<string, ScenarioDataPerGraph> LoadDataPerGraph() diff --git a/gremlin-go/driver/cucumber/cucumberSteps_test.go b/gremlin-go/driver/cucumber/cucumberSteps_test.go index 21b7d95fd5..c55ba149a9 100644 --- a/gremlin-go/driver/cucumber/cucumberSteps_test.go +++ b/gremlin-go/driver/cucumber/cucumberSteps_test.go @@ -501,10 +501,23 @@ 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 + for _, tag := range tg.scenario.Tags { + if tag.Name == "@MultiLabel" { + isMultiLabel = true + break + } + } + + 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 tg.graphName == "empty" || tg.graphName == "multilabel" { err := tg.cleanEmptyDataGraph(tg.g) if err != nil { return err @@ -997,6 +1010,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 @@ -1017,6 +1032,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 @@ -1132,7 +1149,7 @@ func TestCucumberFeatures(t *testing.T) { TestSuiteInitializer: InitializeTestSuite, ScenarioInitializer: InitializeScenario, Options: &godog.Options{ - Tags: "~@GraphComputerOnly && ~@AllowNullPropertyValues && ~@StepTree && ~@StepWrite && ~@DataChar && ~@MultiLabel", + Tags: "~@GraphComputerOnly && ~@AllowNullPropertyValues && ~@StepTree && ~@StepWrite && ~@DataChar", Format: "pretty", Paths: []string{getEnvOrDefaultString("CUCUMBER_FEATURE_FOLDER", "../../../gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features")}, TestingT: t, // Testing instance that will run subtests. diff --git a/gremlin-go/driver/cucumber/cucumberWorld.go b/gremlin-go/driver/cucumber/cucumberWorld.go index 206094522f..ecc50d34c5 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-js/gremlin-javascript/test/cucumber/feature-steps.js b/gremlin-js/gremlin-javascript/test/cucumber/feature-steps.js index bb956a2538..66e002aaf6 100644 --- a/gremlin-js/gremlin-javascript/test/cucumber/feature-steps.js +++ b/gremlin-js/gremlin-javascript/test/cucumber/feature-steps.js @@ -111,7 +111,13 @@ 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.isMultiLabel && graphName === 'empty') { + this.graphName = 'multilabel'; + } else { + this.graphName = graphName; + } const data = this.getData(); this.g = anon.traversal().with_(data.connection); @@ -119,9 +125,12 @@ Given(/^the (.+) graph$/, function (graphName) { 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) { @@ -151,6 +160,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 +179,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/world.js b/gremlin-js/gremlin-javascript/test/cucumber/world.js index 8dcd7d18ba..fd8e21449f 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,7 @@ 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'); }); Before({tags: "@GraphComputerOnly"}, function() { @@ -118,10 +139,6 @@ Before({tags: "@DataDuration"}, function() { return 'skipped' }) -Before({tags: "@MultiLabel"}, function() { - return 'skipped' -}) - function getVertices(connection) { const g = anon.traversal().withRemote(connection); return g.V().group().by('name').by(__.tail()).next().then(it => { 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 56feab3a29..d3529dc46d 100644 --- a/gremlin-python/src/main/python/tests/feature/feature_steps.py +++ b/gremlin-python/src/main/python/tests/feature/feature_steps.py @@ -90,15 +90,17 @@ def choose_graph(step, graph_name): if not step.context.ignore: step.context.ignore = "WithReservedKeysVerificationStrategy" in tagset - # Multi-label steps not yet available in Python GLV - if not step.context.ignore: - step.context.ignore = "MultiLabel" in tagset - 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 + if 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") @@ -459,6 +461,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/terrain.py b/gremlin-python/src/main/python/tests/feature/terrain.py index d81db72c19..654d1e8aa5 100644 --- a/gremlin-python/src/main/python/tests/feature/terrain.py +++ b/gremlin-python/src/main/python/tests/feature/terrain.py @@ -83,10 +83,20 @@ 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 + 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 0000000000..54391e8c5f --- /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-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 42bc9b5dee..33a0442d26 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.LoadGraphWith; 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,18 @@ public abstract class RemoteWorld implements World { 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().addG("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 String changePathToDataFile(final String pathToFileFromGremlin) { return ".." + File.separator + pathToFileFromGremlin; @@ -137,6 +150,11 @@ public abstract class RemoteWorld implements World { } } + @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 9fed97cf98..490ead1423 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 class CheckedGraphManagerTest { 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 6cb6238a89..87af675779 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 class DefaultGraphManagerTest { final Set<String> 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 class DefaultGraphManagerTest { 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 class DefaultGraphManagerTest { final Set<String> 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 class DefaultGraphManagerTest { graphManager.putGraph("newGraph", graph); final Set<String> 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<String> 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 be135f03c7..5e9a753eed 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 0000000000..54391e8c5f --- /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 45f29c17b6..4de6072983 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<String, Object> stringParameters = new HashMap<>(); @@ -316,6 +317,7 @@ public final class StepDefinition { @Before public void beforeEachScenario(final Scenario scenario) throws Exception { + this.currentScenario = scenario; world.beforeEachScenario(scenario); stringParameters.clear(); if (traversal != null) { @@ -355,10 +357,16 @@ public final class StepDefinition { @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"); + if (graphName.equals("empty")) { + 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 0e41b877f0..c693346d78 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,18 @@ 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); + } + /** * 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/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 index 322d0ca42c..a84ba88376 100644 --- 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 @@ -92,9 +92,7 @@ Feature: Step - labels() g.V().labels() """ When iterated to list - Then the result should be unordered - | result | - | vertex | + Then the result should have a count of 0 @MultiLabel Scenario: g_E_labels 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 index 0837669796..ec12441f1e 100644 --- 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 @@ -77,4 +77,4 @@ Feature: Step - addLabel() g.E().addLabel("friend").labels().fold() """ When iterated to list - Then the traversal will raise an error with message containing text of "Cannot add label" + 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 index c96e375cde..9d46da0324 100644 --- 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 @@ -90,7 +90,7 @@ Feature: Step - dropLabel() / dropLabels() g.E().dropLabel("knows").labels().fold() """ When iterated to list - Then the result should have a count of 1 + Then the traversal will raise an error with message containing text of "Label mutation is not supported" @MultiLabel Scenario: g_E_dropLabels_labels @@ -104,4 +104,4 @@ Feature: Step - dropLabel() / dropLabels() g.E().dropLabels().labels() """ When iterated to list - Then the result should have a count of 0 + Then the traversal will raise an error with message containing text of "Label mutation is not supported" 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 3f1f994af9..63bc2f28c8 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 @@ -65,7 +65,6 @@ public abstract class AbstractTinkerGraph implements Graph { 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"; - public static final String GREMLIN_TINKERGRAPH_EDGE_LABEL_CARDINALITY = "gremlin.tinkergraph.edgeLabelCardinality"; protected AtomicLong currentId = new AtomicLong(-1L); protected Map<Object, VertexProperty> vertexProperties = new ConcurrentHashMap<>(); @@ -423,16 +422,6 @@ public abstract class AbstractTinkerGraph implements Graph { public boolean willAllowId(final Object id) { return edgeIdManager.allow(id); } - - @Override - public LabelCardinality getLabelCardinality() { - return edgeLabelCardinality; - } - - @Override - public String getDefaultLabel() { - return defaultEdgeLabel; - } } public class TinkerGraphVertexPropertyFeatures implements Features.VertexPropertyFeatures { 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 c55b3a30ce..c1798c998b 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 @@ -29,7 +29,6 @@ import org.apache.tinkerpop.gremlin.structure.util.StringFactory; import org.apache.tinkerpop.gremlin.util.iterator.IteratorUtils; import java.util.Collections; -import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.Map; @@ -132,21 +131,12 @@ public class TinkerEdge extends TinkerElement implements Edge { @Override public Set<String> labels() { - if (this.edgeLabels.isEmpty()) { - if (!this.graph.edgeLabelCardinality.supportsZeroLabels()) { - return Collections.singleton(this.graph.defaultEdgeLabel); - } - return Collections.emptySet(); - } return Collections.unmodifiableSet(this.edgeLabels); } @Override @Deprecated public String label() { - if (this.edgeLabels.isEmpty()) { - return this.graph.edgeLabelCardinality.supportsZeroLabels() ? null : this.graph.defaultEdgeLabel; - } return this.edgeLabels.iterator().next(); } @@ -159,48 +149,21 @@ public class TinkerEdge extends TinkerElement implements Edge { } this.graph.edgeLabelCardinality.validateAdd(this.edgeLabels, label, labels); - - this.edgeLabels.add(label); - this.graph.addEdgeToAdjacency(this, label); - for (final String l : labels) { - this.edgeLabels.add(l); - this.graph.addEdgeToAdjacency(this, l); - } - this.label = this.edgeLabels.iterator().next(); + // Never reached — validateAdd throws for ONE } @Override public void dropLabels() { graph.touch(this); this.graph.edgeLabelCardinality.validateDropAll(this.edgeLabels); - - for (final String l : new HashSet<>(this.edgeLabels)) { - this.graph.removeEdgeFromAdjacency(this, l); - } - this.edgeLabels.clear(); - this.label = this.edgeLabels.isEmpty() - ? (this.graph.edgeLabelCardinality.supportsZeroLabels() ? null : this.graph.defaultEdgeLabel) - : this.edgeLabels.iterator().next(); + // Never reached — validateDropAll throws for ONE } @Override public void dropLabel(final String label, final String... labels) { graph.touch(this); - this.graph.edgeLabelCardinality.validateDrop(this.edgeLabels, label); - - if (this.edgeLabels.contains(label)) { - this.graph.removeEdgeFromAdjacency(this, label); - this.edgeLabels.remove(label); - } - for (final String l : labels) { - if (this.edgeLabels.contains(l)) { - this.graph.removeEdgeFromAdjacency(this, l); - this.edgeLabels.remove(l); - } - } - this.label = this.edgeLabels.isEmpty() - ? (this.graph.edgeLabelCardinality.supportsZeroLabels() ? null : this.graph.defaultEdgeLabel) - : this.edgeLabels.iterator().next(); + this.graph.edgeLabelCardinality.validateDrop(this.edgeLabels, label, labels); + // Never reached — validateDrop throws for ONE } @Override 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 b47810dc75..b90cf87709 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 @@ -88,9 +88,8 @@ public class TinkerGraph extends AbstractTinkerGraph { allowNullPropertyValues = configuration.getBoolean(GREMLIN_TINKERGRAPH_ALLOW_NULL_PROPERTY_VALUES, false); vertexLabelCardinality = LabelCardinality.valueOf( - configuration.getString(GREMLIN_TINKERGRAPH_VERTEX_LABEL_CARDINALITY, LabelCardinality.ZERO_OR_MORE.name())); - edgeLabelCardinality = LabelCardinality.valueOf( - configuration.getString(GREMLIN_TINKERGRAPH_EDGE_LABEL_CARDINALITY, LabelCardinality.ZERO_OR_ONE.name())); + configuration.getString(GREMLIN_TINKERGRAPH_VERTEX_LABEL_CARDINALITY, LabelCardinality.ONE.name())); + edgeLabelCardinality = LabelCardinality.ONE; defaultVertexLabel = Vertex.DEFAULT_LABEL; defaultEdgeLabel = Edge.DEFAULT_LABEL; @@ -143,8 +142,7 @@ public class TinkerGraph extends AbstractTinkerGraph { public Vertex addVertex(final Object... keyValues) { ElementHelper.legalPropertyKeyValueArray(keyValues); Object idValue = vertexIdManager.convert(ElementHelper.getIdValue(keyValues).orElse(null)); - final Set<String> labels = ElementHelper.getLabelsValue(keyValues).orElse( - Collections.singleton(defaultVertexLabel)); + final Set<String> labels = ElementHelper.getLabelsValue(keyValues).orElse(null); if (null != idValue) { if (this.vertices.containsKey(idValue)) 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 48bfc7a3de..3f1b37358c 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 @@ -87,9 +87,8 @@ public final class TinkerTransactionGraph extends AbstractTinkerGraph { allowNullPropertyValues = configuration.getBoolean(GREMLIN_TINKERGRAPH_ALLOW_NULL_PROPERTY_VALUES, false); vertexLabelCardinality = LabelCardinality.valueOf( - configuration.getString(GREMLIN_TINKERGRAPH_VERTEX_LABEL_CARDINALITY, LabelCardinality.ZERO_OR_MORE.name())); - edgeLabelCardinality = LabelCardinality.valueOf( - configuration.getString(GREMLIN_TINKERGRAPH_EDGE_LABEL_CARDINALITY, LabelCardinality.ZERO_OR_ONE.name())); + configuration.getString(GREMLIN_TINKERGRAPH_VERTEX_LABEL_CARDINALITY, LabelCardinality.ONE.name())); + edgeLabelCardinality = LabelCardinality.ONE; defaultVertexLabel = Vertex.DEFAULT_LABEL; defaultEdgeLabel = Edge.DEFAULT_LABEL; @@ -145,8 +144,7 @@ public final class TinkerTransactionGraph extends AbstractTinkerGraph { Object idValue = vertexIdManager.convert(ElementHelper.getIdValue(keyValues).orElse(null)); if (null == idValue) idValue = vertexIdManager.getNextId(this); - final Set<String> labels = ElementHelper.getLabelsValue(keyValues).orElse( - Collections.singleton(defaultVertexLabel)); + final Set<String> labels = ElementHelper.getLabelsValue(keyValues).orElse(null); this.tx().readWrite(); final long txNumber = transaction.getTxNumber(); 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 c605841fd0..7896c3ad1a 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 @@ -78,34 +78,43 @@ public class TinkerVertex extends TinkerElement implements Vertex { /** * 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<String> 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(); - // Store only explicit labels — never store the default label physically - this.vertexLabels = (labels == null) ? new HashSet<>() : new HashSet<>(labels); - // Set the cached label field to the effective value (including virtual default) - this.label = effectiveLabel(); - } - /** - * Computes the effective single label for the deprecated label() API and the cached field. - */ - private String effectiveLabel() { - if (this.vertexLabels.isEmpty()) { - return this.graph.vertexLabelCardinality.supportsZeroLabels() ? null : this.graph.defaultVertexLabel; + this.vertexLabels = new HashSet<>(); + switch (graph.vertexLabelCardinality) { + case ONE: + // Exactly 1 label: use provided or default + if (labels != null && !labels.isEmpty()) + this.vertexLabels.addAll(labels); + else + this.vertexLabels.add(graph.defaultVertexLabel); + break; + case ONE_OR_MORE: + // Default label always present alongside any user labels + this.vertexLabels.add(graph.defaultVertexLabel); + if (labels != null) this.vertexLabels.addAll(labels); + break; + case ZERO_OR_MORE: + // Fully flexible — store whatever was provided + if (labels != null) this.vertexLabels.addAll(labels); + break; } - return this.vertexLabels.iterator().next(); + + // Validate the resulting set conforms to cardinality + graph.vertexLabelCardinality.validateCreation(this.vertexLabels); + + this.label = this.vertexLabels.isEmpty() ? "" : this.vertexLabels.iterator().next(); } @Override public Set<String> labels() { if (this.vertexLabels.isEmpty()) { - if (!this.graph.vertexLabelCardinality.supportsZeroLabels()) { - return Collections.singleton(this.graph.defaultVertexLabel); - } return Collections.emptySet(); } return Collections.unmodifiableSet(this.vertexLabels); @@ -114,7 +123,10 @@ public class TinkerVertex extends TinkerElement implements Vertex { @Override @Deprecated public String label() { - return effectiveLabel(); + if (this.vertexLabels.isEmpty()) { + return ""; + } + return this.vertexLabels.iterator().next(); } @Override @@ -123,34 +135,32 @@ public class TinkerVertex extends TinkerElement implements Vertex { for (final String l : labels) { ElementHelper.validateLabel(l); } - this.graph.vertexLabelCardinality.validateAdd(this.vertexLabels, label, labels); - + graph.touch(this); this.vertexLabels.add(label); Collections.addAll(this.vertexLabels, labels); - this.label = effectiveLabel(); + this.label = this.vertexLabels.iterator().next(); this.graph.updateVertexLabelIndex(this); } @Override public void dropLabels() { this.graph.vertexLabelCardinality.validateDropAll(this.vertexLabels); - + graph.touch(this); this.vertexLabels.clear(); - this.label = effectiveLabel(); + this.label = ""; this.graph.updateVertexLabelIndex(this); } @Override public void dropLabel(final String label, final String... labels) { - this.graph.vertexLabelCardinality.validateDrop(this.vertexLabels, label); - + this.graph.vertexLabelCardinality.validateDrop(this.vertexLabels, label, labels); + graph.touch(this); this.vertexLabels.remove(label); for (final String l : labels) { this.vertexLabels.remove(l); } - - this.label = effectiveLabel(); + this.label = this.vertexLabels.isEmpty() ? "" : this.vertexLabels.iterator().next(); this.graph.updateVertexLabelIndex(this); } 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 1a6ff95e02..1b5b6518b8 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 @@ public abstract class TinkerWorld implements World { 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 index 9c816cf9c8..c357dd6bdb 100644 --- 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 @@ -22,6 +22,7 @@ import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSo 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; @@ -46,7 +47,10 @@ public class LabelsStepTest { @Before public void setup() { - graph = TinkerGraph.open(); + 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(); } @@ -100,7 +104,7 @@ public class LabelsStepTest { public void shouldStreamDefaultLabelFromDefaultVertex() { final Vertex v = g.addV().next(); final List<String> labels = g.V(v).labels().toList(); - assertThat(labels, hasSize(1)); - assertThat(labels, containsInAnyOrder(Vertex.DEFAULT_LABEL)); + // 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 index 847d79a077..c64996ee35 100644 --- 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 @@ -23,6 +23,7 @@ import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSo 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; @@ -50,7 +51,10 @@ public class MergeVMultiLabelTest { @Before public void setup() { - graph = TinkerGraph.open(); + 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(); } 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 index 71ffc0ce94..8fd2245842 100644 --- 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 @@ -23,6 +23,7 @@ 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; @@ -59,7 +60,10 @@ public class LabelMutationPropertyTest { @Before public void setup() { - graph = TinkerGraph.open(); + 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 } diff --git a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/sideEffect/LabelMutationStepTest.java b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/sideEffect/LabelMutationStepTest.java index d9c00fbfc6..e5cadf5e42 100644 --- a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/sideEffect/LabelMutationStepTest.java +++ b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/sideEffect/LabelMutationStepTest.java @@ -24,6 +24,7 @@ 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; @@ -51,7 +52,10 @@ public class LabelMutationStepTest { @Before public void setup() { - graph = TinkerGraph.open(); + 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(); } @@ -113,14 +117,13 @@ public class LabelMutationStepTest { assertThat(v.labels(), containsInAnyOrder("employee")); } - @Test + @Test(expected = IllegalStateException.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(); - // Under ZERO_OR_ONE cardinality with supportsZeroLabels, edge labels become empty - assertThat(e.labels(), hasSize(0)); } // --- addV multi-label tests --- @@ -135,8 +138,8 @@ public class LabelMutationStepTest { @Test public void shouldCreateVertexWithDefaultLabelViaAddV() { final Vertex v = g.addV().next(); - assertThat(v.labels(), hasSize(1)); - assertThat(v.labels(), containsInAnyOrder(Vertex.DEFAULT_LABEL)); + // Under ZERO_OR_MORE cardinality, addV() with no label produces an empty label set + assertThat(v.labels(), hasSize(0)); } // --- hasLabel index consistency tests --- 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 0000000000..f3555f31de --- /dev/null +++ b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/LabelCardinalityTest.java @@ -0,0 +1,396 @@ +/* + * 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_addVNoLabel_shouldBeEmpty() throws Exception { + final Graph graph = openZeroOrMore(); + try { + final GraphTraversalSource g = graph.traversal(); + final Vertex v = g.addV().next(); + assertThat(v.labels(), is(empty())); + } finally { + graph.close(); + } + } + + @Test + public void zeroOrMoreMode_addVWithLabel_shouldHaveOnlyThatLabel() throws Exception { + final Graph graph = openZeroOrMore(); + 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 + public void zeroOrMoreMode_addLabel_shouldSucceed() throws Exception { + final Graph graph = openZeroOrMore(); + try { + final GraphTraversalSource g = graph.traversal(); + final Vertex v = g.addV("person").next(); + v.addLabel("x"); + assertThat(v.labels(), hasSize(2)); + assertThat(v.labels(), containsInAnyOrder("person", "x")); + } finally { + graph.close(); + } + } + + @Test + public void zeroOrMoreMode_dropLabel_shouldAllowEmptyResult() throws Exception { + final Graph graph = openZeroOrMore(); + try { + final GraphTraversalSource g = graph.traversal(); + final Vertex v = g.addV("person").next(); + v.dropLabel("person"); + assertThat(v.labels(), is(empty())); + } finally { + graph.close(); + } + } + + @Test + public void zeroOrMoreMode_dropLabels_shouldResultInEmpty() throws Exception { + final Graph graph = openZeroOrMore(); + try { + final GraphTraversalSource g = graph.traversal(); + final Vertex v = g.addV("person").addLabel("employee").next(); + v.dropLabels(); + assertThat(v.labels(), is(empty())); + } finally { + graph.close(); + } + } + + @Test + public void zeroOrMoreMode_addVWithMultipleLabels_shouldHaveOnlyThoseLabels() throws Exception { + final Graph graph = openZeroOrMore(); + try { + final GraphTraversalSource g = graph.traversal(); + final Vertex v = g.addV("a", "b").next(); + assertThat(v.labels(), hasSize(2)); + assertThat(v.labels(), containsInAnyOrder("a", "b")); + } finally { + graph.close(); + } + } + + @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<java.util.Collection<?>> is(org.hamcrest.Matcher<java.util.Collection<?>> matcher) { + return org.hamcrest.Matchers.is(matcher); + } + + private static org.hamcrest.Matcher<java.util.Collection<?>> empty() { + return org.hamcrest.Matchers.empty(); + } + + private static org.hamcrest.Matcher<String> 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 index 8e81da7837..ca296dd08a 100644 --- 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 @@ -24,6 +24,8 @@ 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; @@ -54,7 +56,10 @@ public class LabelReplacePatternValidationTest { @Before public void setup() { - graph = TinkerGraph.open(); + 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(); } diff --git a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/MergeOnMatchLabelPatternsTest.java b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/MergeOnMatchLabelPatternsTest.java index 680f442bcf..0a68dbdc1d 100644 --- a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/MergeOnMatchLabelPatternsTest.java +++ b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/MergeOnMatchLabelPatternsTest.java @@ -23,6 +23,8 @@ import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSo 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.tinkergraph.structure.AbstractTinkerGraph; +import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerGraph; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -47,7 +49,10 @@ public class MergeOnMatchLabelPatternsTest { @Before public void setup() { - graph = TinkerGraph.open(); + 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(); } 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 index 99d373bcdd..933be74f92 100644 --- 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 @@ -23,6 +23,8 @@ 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; @@ -48,7 +50,10 @@ public class TinkerVertexMultiLabelGremlinLangTest { @Before public void setup() { - graph = TinkerGraph.open(); + 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(); } diff --git a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertexMultiLabelTest.java b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertexMultiLabelTest.java index ffd5efb697..e9da6dc2bf 100644 --- a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertexMultiLabelTest.java +++ b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertexMultiLabelTest.java @@ -22,6 +22,8 @@ import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSo 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; @@ -43,7 +45,10 @@ public class TinkerVertexMultiLabelTest { @Before public void setup() { - graph = TinkerGraph.open(); + 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(); } @@ -69,8 +74,8 @@ public class TinkerVertexMultiLabelTest { @Test public void shouldCreateVertexWithDefaultLabelWhenNoneSpecified() { final Vertex v = g.addV().next(); - assertThat(v.labels(), hasSize(1)); - assertThat(v.labels(), containsInAnyOrder(Vertex.DEFAULT_LABEL)); + // Under ZERO_OR_MORE cardinality, addV() with no label produces an empty label set + assertThat(v.labels(), hasSize(0)); } @SuppressWarnings("deprecation") @@ -156,14 +161,13 @@ public class TinkerVertexMultiLabelTest { e.addLabel("friend"); } - @Test + @Test(expected = IllegalStateException.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(); - // Under ZERO_OR_ONE cardinality for edges with supportsZeroLabels, labels() returns empty set - assertThat(e.labels(), hasSize(0)); } @Test @@ -177,12 +181,12 @@ public class TinkerVertexMultiLabelTest { @Test public void shouldAddLabelToExistingVertexWithDefaultLabel() { - // Under ZERO_OR_MORE, addV() assigns "vertex" physically. Adding "person" simply adds to the set. + // Under ZERO_OR_MORE, addV() assigns no labels. Adding "person" creates a single-label set. final Vertex v = g.addV().next(); - assertThat(v.labels(), containsInAnyOrder(Vertex.DEFAULT_LABEL)); + assertThat(v.labels(), hasSize(0)); v.addLabel("person"); - assertThat(v.labels(), hasSize(2)); - assertThat(v.labels(), containsInAnyOrder(Vertex.DEFAULT_LABEL, "person")); + assertThat(v.labels(), hasSize(1)); + assertThat(v.labels(), containsInAnyOrder("person")); } @Test
