You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tinkerpop.apache.org by xi...@apache.org on 2026/08/05 21:21:53 UTC
(tinkerpop) branch TINKERPOP-3274 created (now 99c11cfba7)
This is an automated email from the ASF dual-hosted git repository.
xiazcy pushed a change to branch TINKERPOP-3274
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
at 99c11cfba7 feat: add T.labels multi-label token
This branch includes the following new commits:
new 99c11cfba7 feat: add T.labels multi-label token
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(tinkerpop) 01/01: feat: add T.labels multi-label token
Posted by xi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
xiazcy pushed a commit to branch TINKERPOP-3274
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
commit 99c11cfba77a9aa498ad64fafd6c80aae4c18bc2
Author: Yang Xia <xi...@gmail.com>
AuthorDate: Wed Aug 5 14:21:40 2026 -0700
feat: add T.labels multi-label token
Add the T.labels token as the token-level analogue of the labels() step and Element.labels(), operating on a vertex's full label set as a Set. It is accepted for projection (by), filtering (has, any-match), as an elementMap()/valueMap() map key, in the Gremlin grammar, across all GLVs, and on creation by addV(), mergeV(), and graph.addVertex() as a synonym for T.label.
Under with("multilabel"), elementMap()/valueMap() now key the label set under T.labels rather than overloading T.label. Also fix addV() given an empty label set to create a label-less vertex on a ZERO_OR_MORE graph, consistent with graph.addVertex().
TINKERPOP-3274
Assisted-by: Kiro:claude-opus-4-8
---
CHANGELOG.asciidoc | 1 +
docs/src/dev/provider/gremlin-semantics.asciidoc | 27 +++---
docs/src/reference/the-traversal.asciidoc | 84 +++++++++++++++-
docs/src/upgrade/release-4.x.x.asciidoc | 35 ++++++-
.../step/map/AbstractAddVertexStepPlaceholder.java | 18 ++++
.../traversal/step/map/AddVertexStartStep.java | 3 +-
.../process/traversal/step/map/AddVertexStep.java | 3 +-
.../process/traversal/step/map/ElementMapStep.java | 4 +-
.../traversal/step/map/MergeElementStep.java | 40 +++++++-
.../traversal/step/map/MergeVertexStep.java | 19 ++--
.../step/map/MergeVertexStepPlaceholder.java | 2 +-
.../traversal/step/map/PropertyMapStep.java | 2 +-
.../process/traversal/step/util/HasContainer.java | 2 +-
.../org/apache/tinkerpop/gremlin/structure/T.java | 20 ++++
.../gremlin/structure/util/ElementHelper.java | 90 +++++++++++------
.../gremlin/jsr223/ImportGremlinPluginTest.java | 15 ++-
.../traversal/lambda/TokenTraversalTest.java | 20 ++++
.../traversal/step/map/AddVertexStepTest.java | 15 +++
.../traversal/step/map/MergeEdgeStepTest.java | 13 +++
.../traversal/step/map/MergeVertexStepTest.java | 30 ++++++
.../apache/tinkerpop/gremlin/structure/TTest.java | 15 +++
.../src/Gremlin.Net/Process/Traversal/T.cs | 3 +
.../Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs | 8 ++
gremlin-go/driver/cucumber/cucumberSteps_test.go | 2 +
gremlin-go/driver/cucumber/gremlin.go | 4 +
gremlin-go/driver/traversal.go | 22 +++--
.../gremlin-javascript/lib/process/traversal.ts | 2 +-
.../gremlin-javascript/test/cucumber/gremlin.js | 4 +
gremlin-language/src/main/antlr4/Gremlin.g4 | 2 +
.../python/gremlin_python/process/traversal.py | 3 +-
.../src/main/python/tests/feature/gremlin.py | 4 +
.../gremlin/language/translator/translations.json | 68 +++++++++++++
.../gremlin/test/features/map/ElementMap.feature | 28 +++---
.../gremlin/test/features/map/Labels.feature | 51 ++++++++++
.../gremlin/test/features/map/ValueMap.feature | 22 ++---
.../step/sideEffect/LabelMutationPropertyTest.java | 2 +-
.../step/sideEffect/LabelMutationStepTest.java | 108 ++++++++++++++++++++-
37 files changed, 676 insertions(+), 115 deletions(-)
diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc
index 2ca2d637af..b2e7d6303b 100644
--- a/CHANGELOG.asciidoc
+++ b/CHANGELOG.asciidoc
@@ -65,6 +65,7 @@ image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima
* Changed Tree class in Java to not extend from HashMap and offered a new tree-shaped API for navigation.
* Added multi-label support for vertices with configurable `LabelCardinality` (`ONE`, `ONE_OR_MORE`, `ZERO_OR_MORE`).
* Added `labels()` step to emit each vertex label as a separate traverser.
+* Added the `T.labels` token as the token-level analogue of `labels()` and `Element.labels()`, projecting and filtering on the full vertex label set, and accepted by `mergeV()`, `addV()`, and `graph.addVertex()` as a synonym for `T.label`.
* Added `addLabel()` and `dropLabel()`/`dropLabels()` steps for vertex label mutation.
* Added `LabelCardinalityValidator` utility for constraint enforcement separated from the `LabelCardinality` enum.
* Added `with("multilabel")` source configuration for `elementMap()` and `valueMap()` to return labels as a set.
diff --git a/docs/src/dev/provider/gremlin-semantics.asciidoc b/docs/src/dev/provider/gremlin-semantics.asciidoc
index 5ba2fca94c..fc1e079a21 100644
--- a/docs/src/dev/provider/gremlin-semantics.asciidoc
+++ b/docs/src/dev/provider/gremlin-semantics.asciidoc
@@ -1746,9 +1746,8 @@ property entries specified by propertyKeys. If the list is empty, then all prope
*Considerations:*
The label format in the map is controlled by source-level `with("multilabel")` and `with("singlelabel")` options, not
-by the number of labels the graph permits per element. When `"multilabel"` is present, the label value is a
-`SET<STRING>`. Likewise when
-`"singlelabel"` is present, it is a single `STRING`. The two options are mutually exclusive. Configuring both
+by the number of labels the graph permits per element. When `"multilabel"` is present, the element's full label set is emitted under a `labels` key as a
+`SET<STRING>`. When `"singlelabel"` is present, a single `label` key is emitted as a `STRING`. The two options are mutually exclusive. Configuring both
`"multilabel"` and `"singlelabel"` on the same source is rejected with a `State Error`. Note that it is a
provider choice whether an unconfigured `GraphTraversal` defaults to `multilabel` or `singlelabel` semantics. The
reference implementation defaults to `singlelabel` semantics.
@@ -1938,8 +1937,8 @@ link:https://tinkerpop.apache.org/docs/x.y.z/reference/#groupcount-step[referenc
*Arguments:*
* `key` - The property key to check.
-* `accessor` - A `T` accessor: `T.id` (element identifier), `T.label` (element label), `T.key` (property key), or
- `T.value` (property value).
+* `accessor` - A `T` accessor: `T.id` (element identifier), `T.label` (element label), `T.labels` (element
+ label set), `T.key` (property key), or `T.value` (property value).
* `value` - The value to compare against using equality.
* `predicate` - A `P` predicate for comparison. The predicate value may be a literal or a `Traversal`.
* `traversal` - A child traversal whose first result is used as the comparison value (implicitly wrapped in `P.eq()`).
@@ -1955,6 +1954,8 @@ None
* `has(key)` filters to elements that have the specified property (existence check).
* `has(T.label, value)` is equivalent to `hasLabel(value)`. `has(T.id, value)` is equivalent to `hasId(value)`.
+* `has(T.labels, value)` is equivalent to `has(T.label, value)` and `hasLabel(value)`, matching when any of the
+ element's labels satisfies the comparison.
* When the domain is a `Property` (e.g., from `properties()`), `T.key` and `T.value` accessors are used to filter
by property key or property value respectively.
* When the predicate value for `T.id` is a `STRING`, the element's `id().toString()` is used for comparison.
@@ -2587,9 +2588,11 @@ link:https://tinkerpop.apache.org/docs/x.y.z/reference/#mergee-step[reference]
* `onCreate` - A `MAP` used to specify additional existence criteria and/or properties not already specified in `searchCreate`.
* `onMatch` - A `MAP` used to update the `Vertex` that is found using the `searchCreate` criteria.
-The `searchCreate` and `onCreate` `MAP` instances must consist of any combination of `T.id`, `T.label`, or arbitrary
-`STRING` keys (which are assumed to be vertex properties). The `onMatch` `MAP` instance only allows for `STRING` keys
-as the `id` and `label` of a `Vertex` are immutable. `null` values for these valid keys are not allowed.
+The `searchCreate` and `onCreate` `MAP` instances must consist of any combination of `T.id`, `T.label` (or its
+multi-label synonym `T.labels`), or arbitrary `STRING` keys (which are assumed to be vertex properties). The
+`onMatch` `MAP` instance allows `STRING` property keys, and on a graph that permits multiple labels per vertex it
+also accepts `T.label` or `T.labels` to add labels to the matched vertex. The `id` of a `Vertex` is immutable.
+`null` values for these valid keys are not allowed.
The `MAP` that is used as the argument for `searchCreate` may be assigned from the incoming `Traverser` for the no-arg
`mergeV()`. If `mergeV(Map)` is used, then it will override the incoming `Traverser`. If `mergeV(Traversal)` is used,
@@ -2623,7 +2626,8 @@ resolve to a `MAP`.
*Exceptions:*
* `MAP` arguments are validated for their keys resulting in exception if they do not meet requirements defined above.
-* Use of `T.label` should always have a value that is a `STRING`.
+* Use of `T.label` or `T.labels` should have a value that is a `STRING`, or a `SET<STRING>` on a graph that permits
+ multiple labels per vertex. `T.label` and `T.labels` may not both appear in the same `MAP`.
* If `T.id` and/or `T.label` are specified in `searchCreate`, they cannot be overridden in `onCreate`.
*Considerations:*
@@ -3322,9 +3326,8 @@ keys, the resulting map entry contains a `LIST` of the values. An `Edge` and a `
value per key and the value is returned directly.
The label format in the map is controlled by source-level `with("multilabel")` and `with("singlelabel")` options, not
-by the number of labels the graph permits per element. When `"multilabel"` is present, the label value is a
-`SET<STRING>`. Likewise when
-`"singlelabel"` is present, it is a single `STRING`. The two options are mutually exclusive. Configuring both
+by the number of labels the graph permits per element. When `"multilabel"` is present, the element's full label set is emitted under a `labels` key as a
+`SET<STRING>`. When `"singlelabel"` is present, a single `label` key is emitted as a `STRING`. The two options are mutually exclusive. Configuring both
`"multilabel"` and `"singlelabel"` on the same source is rejected with a `State Error`. Note that it is a
provider choice whether an unconfigured `GraphTraversal` defaults to `multilabel` or `singlelabel` semantics. The
reference implementation defaults to `singlelabel` semantics.
diff --git a/docs/src/reference/the-traversal.asciidoc b/docs/src/reference/the-traversal.asciidoc
index 130ac5cda0..7bdfced48a 100644
--- a/docs/src/reference/the-traversal.asciidoc
+++ b/docs/src/reference/the-traversal.asciidoc
@@ -1168,8 +1168,8 @@ g.V().group().by(labels().fold()).by(values('name').fold()) <2>
<1> `atlas` carries the "animal", "reptile", "aquatic", and "endangered" labels, but is grouped under only one of
them, chosen non-deterministically, so this grouping is not reliable for multi-label vertices.
<2> Grouping by the full set of labels instead keeps each animal under its own distinct combination of labels.
-Prefer this pattern with the <<labels-step,`labels()`>> step over `by(label)` for graphs that support multiple
-labels per vertex.
+Prefer this pattern with the <<labels-step,`labels()`>> step - or the equivalent <<t-labels-token,`by(T.labels)`>> -
+over `by(label)` for graphs that support multiple labels per vertex.
*Additional References*
@@ -2912,6 +2912,86 @@ g.V().labels().count()
link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#labels()++[`labels()`]
+[llms-summary="The T.labels token is the token-level analogue of the labels() step and Element.labels(), projecting and filtering on a vertex's full label set as a Set<String>."]
+[[t-labels-token]]
+=== Labels Token
+
+The `T.labels` token (added in 4.0.0) is the token-level analogue of the <<labels-step,`labels()`>> step and
+`Element.labels()`. It operates on the full label set of an `Element`, projecting it as a `Set<String>`, and is valid
+wherever a `T` token is accepted: `by()`-modulation, `has()` filtering, as a map key, and as a creation token in
+<<addvertex-step,`addV()`>> and <<mergevertex-step,`mergeV()`>>. It complements the singular `T.label` token, which is
+backed by the deprecated <<label-step,`label()`>> step and returns only a single `String`. `T.labels` is the only `T`
+token whose value is a collection rather than a scalar, which shapes the caveats described below.
+
+NOTE: `by(T.labels)` and `has(T.labels, ...)` always operate on an element's complete label set, independent of any
+source-level `with("multilabel")` or `with("singlelabel")` configuration. That configuration governs only the map key
+emitted by <<elementmap-step,`elementMap()`>> and <<valuemap-step,`valueMap()`>>, which place the label set under a
+`labels` key under `multilabel` and a single `label` key otherwise. Code that reads a label from such a map must
+select the key that matches the active mode.
+
+Used for projection, `T.labels` returns the element's complete label set as a `Set<String>`:
+
+[gremlin-groovy,theZoo]
+----
+g.V().has('name','tux').project('labels').by(T.labels) <1>
+----
+
+<1> `tux` carries the "animal", "bird", "aquatic", and "endangered" labels, which are returned together as a
+`Set<String>`.
+
+Used for filtering, `has(T.labels, P)` matches an element when *any* label in its label set satisfies the predicate
+`P` (any-match / membership semantics), exactly as `hasLabel(...)` and the `labels()` step behave. This makes
+`has(T.labels, P)` equivalent to `has(T.label, P)`:
+
+[gremlin-groovy,theZoo]
+----
+g.V().has(T.labels, 'bird').values('name') <1>
+g.V().has(T.labels, within('bird','reptile')).values('name') <2>
+----
+
+<1> Matches every vertex that carries the "bird" label.
+<2> Matches every vertex that carries either the "bird" or "reptile" label.
+
+The predicate is tested against each label independently, so `has(T.labels, P)` cannot express label-set equality.
+`has(T.labels, eq('bird'))` retains an element that carries `bird` alongside other labels, and a negation such as
+`has(T.labels, neq('bird'))` retains an element as long as *any* label differs from `bird`. This mirrors the
+multi-label behavior described for <<has-step,`hasLabel()`>>. Wrap the filter in <<not-step,`not()`>> to exclude every
+element carrying a label, or project with `by(T.labels)` and compare the resulting `Set` to test the label set as a
+whole.
+
+Because `T.labels` projects a `Set` rather than a scalar, steps that expect a single comparable value behave
+differently than they do with the other tokens:
+
+* `order().by(T.labels)` has *undefined ordering*. A `Set` has no natural total order, so the relative order of
+elements ordered this way is not guaranteed and should not be relied upon. When a deterministic order is needed,
+order by a scalar instead (for example `by(labels().order().fold())` or by a specific label).
+* `group().by(T.labels)` groups by *set-equality*: two elements fall into the same group only when their label sets
+are equal. This is the recommended way to group multi-label vertices, since `group().by(label)` collapses each
+vertex under a single, non-deterministically chosen label. The resulting map keys are `Set` instances.
+* Reductions and arithmetic that require a scalar, such as `sum()`, `mean()`, or `math()`, raise an error when applied
+to a `by(T.labels)` value.
+* `T.labels` is defined only on an `Element`. Applying it where the traverser is a `Property`, such as inside
+`properties()`, raises an `IllegalStateException`, the same as `T.label` and `T.id`.
+
+[gremlin-groovy,theZoo]
+----
+g.V().group().by(T.labels).by(values('name').fold()) <1>
+----
+
+<1> Vertices are grouped by their exact label set. Only vertices whose label sets are equal share a group.
+
+On creation, `T.labels` is accepted as a synonym for `T.label` by <<addvertex-step,`addV()`>>,
+<<mergevertex-step,`mergeV()`>>, and `graph.addVertex()`. The two tokens may not both appear in the same argument set.
+An empty or absent label set defers to the graph's `LabelCardinality`: a `ZERO_OR_MORE` graph creates a vertex with no
+labels, while `ONE` and `ONE_OR_MORE` apply the default label. This holds regardless of which of these APIs is used.
+
+NOTE: `T.labels` requires matched 4.x peers when sent over the wire. Older servers or drivers do not recognize the
+token. See the upgrade documentation for cross-version compatibility details.
+
+*Additional References*
+
+link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/structure/T.html++[`T`]
+
[llms-summary="The length()-step (map) returns the length incoming string or list of string traverser."]
[[length-step]]
=== Length Step
diff --git a/docs/src/upgrade/release-4.x.x.asciidoc b/docs/src/upgrade/release-4.x.x.asciidoc
index 3b270392ec..155732103b 100644
--- a/docs/src/upgrade/release-4.x.x.asciidoc
+++ b/docs/src/upgrade/release-4.x.x.asciidoc
@@ -142,10 +142,10 @@ gremlin> g.V().has('name','marko').dropLabel('employee').labels()
==>manager
----
-`mergeV()` now optionally accepts a list for `T.label` to match or create multi-label vertices. By default
-`elementMap()` and `valueMap()` still return a single label string, and a new traversal-source level
-`with("multilabel")` config alters the results to wrap labels in lists. The reference documentation covers each of these
-behaviors in detail.
+`mergeV()` and `addV()` accept the vertex label set via either `T.label` or the new `T.labels` token to match or
+create multi-label vertices. By default `elementMap()` and `valueMap()` return a single label string, while the
+traversal-source level `with("multilabel")` config returns the full label set. The reference documentation covers
+each of these behaviors in detail.
See: link:https://issues.apache.org/jira/browse/TINKERPOP-3261[TINKERPOP-3261],
link:https://tinkerpop.apache.org/docs/4.0.0-beta.3/reference/#tinkergraph-multi-label[Multi-Label],
@@ -157,6 +157,33 @@ link:https://tinkerpop.apache.org/docs/4.0.0-beta.3/reference/#valuemap-step[val
link:https://tinkerpop.apache.org/docs/4.0.0-beta.3/reference/#mergevertex-step[mergeV()],
link:https://tinkerpop.apache.org/docs/4.0.0-beta.3/reference/#the-zoo-toy-graph[The Zoo]
+==== The `T.labels` Token
+
+The `T` enum gains a `labels` token as the token-level analogue of the `labels()` step and
+`Element.labels()`, completing the multi-label API. `T.labels` projects an element's full label set as a
+`Set<String>` and is accepted wherever the existing tokens are, including `by()`, `has()`, `mergeV()`,
+`addV()`, and as a map key. It complements `T.label`, which continues to produce the singular (now
+deprecated) label.
+
+[source,groovy]
+----
+gremlin> g.addV('person','employee').property('name','marko')
+==>v[0]
+gremlin> g.V().has('name','marko').project('labels').by(T.labels)
+==>[labels:[person,employee]]
+----
+
+Under `with("multilabel")`, `elementMap()` and `valueMap()` emit the label set under the `T.labels` key as a
+`Set<String>` rather than the `T.label` key. Code that reads the label set from the `T.label` key of a
+`multilabel` map must read it from `T.labels` instead. The default and `singlelabel` output is unchanged,
+emitting a single `String` under `T.label`. Because `T.labels` is a new token, traffic that carries it
+requires matched 4.x peers. An older peer that does not recognize the token raises an
+`IllegalArgumentException` on deserialization.
+
+See: link:https://issues.apache.org/jira/browse/TINKERPOP-3274[TINKERPOP-3274],
+link:https://tinkerpop.apache.org/docs/4.0.0-beta.3/reference/#elementmap-step[elementMap()],
+link:https://tinkerpop.apache.org/docs/4.0.0-beta.3/reference/#valuemap-step[valueMap()]
+
==== More Dynamic Arguments
Prior to 4.0, comparing a traverser's value against a dynamically computed reference required several coordinated
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 b48b53e9ff..69d7d04a4d 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
@@ -22,6 +22,7 @@ import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
import org.apache.tinkerpop.gremlin.process.traversal.step.GValue;
import org.apache.tinkerpop.gremlin.process.traversal.step.GValueHolder;
import org.apache.tinkerpop.gremlin.process.traversal.step.util.event.Event;
+import org.apache.tinkerpop.gremlin.structure.T;
import org.apache.tinkerpop.gremlin.structure.Vertex;
import java.util.Collection;
@@ -55,10 +56,27 @@ public abstract class AbstractAddVertexStepPlaceholder<S> extends AbstractAddEle
@Override
public void setLabel(Object label) {
+ // An explicit empty label collection means "no label provided": leave the label unset so vertex
+ // creation applies the graph's LabelCardinality default (no labels under ZERO_OR_MORE), matching the
+ // concrete step and graph.addVertex(). Flipping userProvidedLabel here would instead materialize the
+ // default label.
+ if (label instanceof Collection && ((Collection<?>) label).isEmpty()) {
+ return;
+ }
super.setLabel(label);
userProvidedLabel = true;
}
+ @Override
+ public void addProperty(final Object key, final Object value) {
+ // T.labels is a synonym for T.label on vertex creation (multi-label support)
+ if (key == T.labels) {
+ setLabel(value);
+ return;
+ }
+ super.addProperty(key, value);
+ }
+
@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 acb005f1f3..584e256881 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
@@ -128,7 +128,8 @@ public class AddVertexStartStep extends AbstractStep<Vertex, Vertex> implements
}
private void configureInternalParams(final Object... keyValues) {
- if (keyValues[0] == T.label) {
+ // T.labels is accepted as a synonym for T.label on vertex creation (multi-label support)
+ if (keyValues[0] == T.label || keyValues[0] == T.labels) {
setLabel(keyValues[1]);
return;
}
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStep.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStep.java
index b2044a6f17..d73dc27acf 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStep.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStep.java
@@ -124,7 +124,8 @@ public class AddVertexStep<S> extends ScalarMapStep<S, Vertex> implements AddVer
}
private void configureInternalParams(final Object... keyValues) {
- if (keyValues[0] == T.label) {
+ // T.labels is accepted as a synonym for T.label on vertex creation (multi-label support)
+ if (keyValues[0] == T.label || keyValues[0] == T.labels) {
setLabel(keyValues[1]);
return;
}
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/ElementMapStep.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/ElementMapStep.java
index 23befd72c9..33f50c49b0 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/ElementMapStep.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/ElementMapStep.java
@@ -68,7 +68,7 @@ public class ElementMapStep<K,E> extends ScalarMapStep<Element, Map<K, E>> imple
map.put(T.value, ((VertexProperty<?>) element).value());
} else {
if (isMultilabelEnabled()) {
- map.put(T.label, element.labels());
+ map.put(T.labels, element.labels());
} else {
final String label = element.label();
if (!label.isEmpty()) {
@@ -99,7 +99,7 @@ public class ElementMapStep<K,E> extends ScalarMapStep<Element, Map<K, E>> imple
// can't add label if doing GraphComputer stuff as there is no access to the label of the adjacent vertex
if (!onGraphComputer) {
if (isMultilabelEnabled()) {
- m.put(T.label, v.labels());
+ m.put(T.labels, v.labels());
} else {
final String label = v.label();
if (!label.isEmpty()) m.put(T.label, label);
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeElementStep.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeElementStep.java
index 6b7c261d48..19668f7167 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeElementStep.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeElementStep.java
@@ -46,7 +46,6 @@ import org.apache.tinkerpop.gremlin.process.traversal.traverser.TraverserRequire
import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalHelper;
import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalUtil;
import org.apache.tinkerpop.gremlin.structure.Direction;
-import org.apache.tinkerpop.gremlin.structure.Element;
import org.apache.tinkerpop.gremlin.structure.Graph;
import org.apache.tinkerpop.gremlin.structure.T;
import org.apache.tinkerpop.gremlin.structure.Vertex;
@@ -249,13 +248,23 @@ public abstract class MergeElementStep<S, E, C> extends FlatMapStep<S, E>
protected static void validate(final Map map, final boolean ignoreTokens, final Set allowedTokens, final String op) {
if (null == map) return;
+ // T.labels is only a valid label token where the step allows it (mergeV, not mergeE); gate on allowedTokens
+ // so edge merges continue to reject it rather than silently treating it as a label.
+ final boolean labelsAllowed = allowedTokens.contains(T.labels);
+
+ // mergeV accepts T.labels as a synonym for T.label; specifying both in the same Map is ambiguous.
+ if (labelsAllowed && ((Map<?,?>) map).containsKey(T.label) && ((Map<?,?>) map).containsKey(T.labels)) {
+ throw new IllegalArgumentException(String.format(
+ "%s() does not allow both T.label and T.labels in the same Map", op));
+ }
+
((Map<?,?>) map).entrySet().forEach(e -> {
final Object k = e.getKey();
final Object v = e.getValue();
if (ignoreTokens) {
- // Allow T.label in onMatch for multi-label replacement support
- if (k == T.label) {
+ // Allow T.label / T.labels in onMatch for multi-label replacement support
+ if (k == T.label || (labelsAllowed && k == T.labels)) {
if (v instanceof String) {
ElementHelper.validateLabel((String) v);
} else if (v instanceof java.util.Collection) {
@@ -288,7 +297,7 @@ public abstract class MergeElementStep<S, E, C> extends FlatMapStep<S, E>
"%s() and option(onCreate) args expect keys in Map to be either String or %s - check: %s",
op, allowedTokens, k));
}
- if (k == T.label) {
+ if (k == T.label || (labelsAllowed && k == T.labels)) {
if (v instanceof String) {
ElementHelper.validateLabel((String) v);
} else if (v instanceof java.util.Collection) {
@@ -319,6 +328,29 @@ public abstract class MergeElementStep<S, E, C> extends FlatMapStep<S, E>
});
}
+ /**
+ * mergeV accepts the multi-label {@link T#labels} token as a synonym for {@link T#label}, matching the
+ * {@code elementMap()}/{@code valueMap()} output produced under {@code with("multilabel")}. Because the
+ * existence search, vertex creation, and onMatch label-application paths already accept a
+ * {@code Collection<String>} under {@link T#label}, any {@code T.labels} entry is normalized onto
+ * {@link T#label} at the input boundary, leaving the rest of the step label-token agnostic. The supplied
+ * Map is never mutated; a normalized copy is returned only when a {@code T.labels} entry is present.
+ *
+ * @throws IllegalArgumentException if the Map contains both {@code T.label} and {@code T.labels}
+ * @since 4.0.0
+ */
+ protected static Map normalizeLabelsToken(final Map map) {
+ if (map == null || !map.containsKey(T.labels))
+ return map;
+ // Enforce the same "not both" rule as validate()/getLabelsValue so this holds regardless of caller ordering.
+ if (map.containsKey(T.label)) {
+ throw new IllegalArgumentException("Vertex label may be specified with either T.label or T.labels, but not both");
+ }
+ final Map normalized = new LinkedHashMap(map);
+ normalized.put(T.label, normalized.remove(T.labels));
+ return normalized;
+ }
+
/**
* Prohibit overrides to the existence criteria (id/label/from/to) in onCreate.
*/
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeVertexStep.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeVertexStep.java
index 448ee768be..c972ec30b3 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeVertexStep.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeVertexStep.java
@@ -46,7 +46,7 @@ import static java.util.stream.Collectors.toList;
*/
public class MergeVertexStep<S> extends MergeElementStep<S, Vertex, Map<Object, Object>> {
- private static final Set allowedTokens = new LinkedHashSet(Arrays.asList(T.id, T.label));
+ private static final Set allowedTokens = new LinkedHashSet(Arrays.asList(T.id, T.label, T.labels));
public static void validateMapInput(final Map map, final boolean ignoreTokens) {
MergeElementStep.validate(map, ignoreTokens, allowedTokens, "mergeV");
@@ -78,8 +78,11 @@ public class MergeVertexStep<S> extends MergeElementStep<S, Vertex, Map<Object,
protected Iterator<Vertex> flatMap(final Traverser.Admin<S> traverser) {
final Graph graph = getGraph();
- final Map mergeMap = materializeMap(traverser, mergeTraversal);
- validateMapInput(mergeMap, false);
+ final Map rawMergeMap = materializeMap(traverser, mergeTraversal);
+ validateMapInput(rawMergeMap, false);
+ // T.labels is accepted as a synonym for T.label (multi-label support); normalize onto T.label so the
+ // downstream search/create/onMatch paths remain label-token agnostic.
+ final Map mergeMap = normalizeLabelsToken(rawMergeMap);
Iterator<Vertex> vertices = searchVertices(mergeMap);
@@ -100,8 +103,10 @@ public class MergeVertexStep<S> extends MergeElementStep<S, Vertex, Map<Object,
traverser.set((S) v);
// assume good input from GraphTraversal - folks might drop in a T here even though it is immutable
- final Map<Object, Object> onMatchMap = materializeMap(traverser, onMatchTraversal);
- validateMapInput(onMatchMap, true);
+ final Map<Object, Object> rawOnMatchMap = materializeMap(traverser, onMatchTraversal);
+ validateMapInput(rawOnMatchMap, true);
+ // normalize T.labels onto T.label (multi-label synonym) before applying
+ final Map<Object, Object> onMatchMap = normalizeLabelsToken(rawOnMatchMap);
// Handle T.label separately: append-only addLabel semantics for multi-label support
Object labelValue = onMatchMap.get(T.label);
@@ -184,11 +189,13 @@ public class MergeVertexStep<S> extends MergeElementStep<S, Vertex, Map<Object,
if (onCreateTraversal == null)
return mergeMap;
- final Map onCreateMap = materializeMap(traverser, onCreateTraversal);
+ Map onCreateMap = materializeMap(traverser, onCreateTraversal);
// null result from onCreateTraversal - use main mergeMap argument
if (onCreateMap == null || onCreateMap.size() == 0)
return mergeMap;
validateMapInput(onCreateMap, false);
+ // normalize T.labels onto T.label (multi-label synonym) to match the already-normalized mergeMap
+ onCreateMap = normalizeLabelsToken(onCreateMap);
if (mergeMap == null || mergeMap.size() == 0)
return onCreateMap;
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeVertexStepPlaceholder.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeVertexStepPlaceholder.java
index fa14904514..55f9699572 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeVertexStepPlaceholder.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeVertexStepPlaceholder.java
@@ -55,7 +55,7 @@ import static org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEleme
*/
public class MergeVertexStepPlaceholder<S> extends AbstractMergeElementStepPlaceholder<S, Vertex> {
- private static final Set allowedTokens = new LinkedHashSet(Arrays.asList(T.id, T.label));
+ private static final Set allowedTokens = new LinkedHashSet(Arrays.asList(T.id, T.label, T.labels));
public static void validateMapInput(final Map map, final boolean ignoreTokens) {
MergeElementStep.validate(map, ignoreTokens, allowedTokens, "mergeV");
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/PropertyMapStep.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/PropertyMapStep.java
index a773d87b07..f3a515a80d 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/PropertyMapStep.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/PropertyMapStep.java
@@ -204,7 +204,7 @@ public class PropertyMapStep<K,E> extends ScalarMapStep<Element, Map<K, E>>
} else {
if (includeToken(WithOptions.labels)) {
if (isMultilabelEnabled()) {
- map.put(T.label, element.labels());
+ map.put(T.labels, element.labels());
} else {
final String label = getElementLabel(element);
if (!label.isEmpty()) {
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/HasContainer.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/HasContainer.java
index a94b2e3881..3b171bd522 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/HasContainer.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/HasContainer.java
@@ -55,7 +55,7 @@ public class HasContainer implements Serializable, Cloneable, Predicate<Element>
if (this.key != null) {
if (this.key.equals(T.id.getAccessor()))
return testingIdString ? testIdAsString(element) : testId(element);
- if (this.key.equals(T.label.getAccessor()))
+ if (this.key.equals(T.label.getAccessor()) || this.key.equals(T.labels.getAccessor()))
return testLabel(element);
}
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/T.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/T.java
index 66f6822f2c..9f3ed7b21f 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/T.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/T.java
@@ -18,6 +18,7 @@
*/
package org.apache.tinkerpop.gremlin.structure;
+import java.util.Set;
import java.util.function.Function;
/**
@@ -44,6 +45,22 @@ public enum T implements Function<Element, Object> {
return element.label();
}
},
+ /**
+ * Labels (representing Element.labels())
+ *
+ * @since 4.0.0
+ */
+ labels {
+ @Override
+ public String getAccessor() {
+ return LABELS;
+ }
+
+ @Override
+ public Set<String> apply(final Element element) {
+ return element.labels();
+ }
+ },
/**
* Id (representing Element.id())
*
@@ -94,6 +111,7 @@ public enum T implements Function<Element, Object> {
};
private static final String LABEL = Graph.Hidden.hide("label");
+ private static final String LABELS = Graph.Hidden.hide("labels");
private static final String ID = Graph.Hidden.hide("id");
private static final String KEY = Graph.Hidden.hide("key");
private static final String VALUE = Graph.Hidden.hide("value");
@@ -106,6 +124,8 @@ public enum T implements Function<Element, Object> {
public static T fromString(final String accessor) {
if (accessor.equals(LABEL))
return label;
+ else if (accessor.equals(LABELS))
+ return labels;
else if (accessor.equals(ID))
return id;
else if (accessor.equals(KEY))
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/ElementHelper.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/ElementHelper.java
index 0ca8aeda13..e17a4b7e83 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/ElementHelper.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/ElementHelper.java
@@ -238,48 +238,74 @@ public final class ElementHelper {
* @throws NullPointerException if the value for the {@link T#label} key is {@code null}
*/
public static Optional<String> getLabelValue(final Object... keyValues) {
- for (int i = 0; i < keyValues.length; i = i + 2) {
- if (keyValues[i].equals(T.label)) {
- ElementHelper.validateLabel((String) keyValues[i + 1]);
- return Optional.of((String) keyValues[i + 1]);
- }
- }
- return Optional.empty();
+ final Object labelValue = resolveLabelTokenValue(keyValues);
+ if (labelValue == LABEL_TOKEN_ABSENT)
+ return Optional.empty();
+ // Cast directly so a non-String value yields ClassCastException, per this method's contract.
+ ElementHelper.validateLabel((String) labelValue);
+ return Optional.of((String) labelValue);
}
/**
* Extracts the value of the {@link T#label} key from the list of arguments as a {@link Set} of labels.
* Supports both single {@link String} values and {@link java.util.Collection} values for multi-label vertices.
+ * The multi-label synonym {@link T#labels} is also accepted; specifying both tokens is rejected.
*
* @param keyValues a list of key/value pairs
- * @return the labels associated with {@link T#label}, or empty if not present
+ * @return the labels associated with {@link T#label}/{@link T#labels}, or empty if not present
* @since 4.0.0
*/
public static Optional<Set<String>> getLabelsValue(final Object... keyValues) {
- for (int i = 0; i < keyValues.length; i = i + 2) {
- if (keyValues[i].equals(T.label)) {
- final Object labelValue = keyValues[i + 1];
- if (labelValue instanceof String) {
- ElementHelper.validateLabel((String) labelValue);
- final Set<String> labels = new LinkedHashSet<>();
- labels.add((String) labelValue);
- return Optional.of(labels);
- } else if (labelValue instanceof Collection) {
- final Set<String> labels = new LinkedHashSet<>();
- for (final Object l : (Collection<?>) labelValue) {
- if (!(l instanceof String)) {
- throw new IllegalArgumentException("T.label collection must contain only Strings");
- }
- ElementHelper.validateLabel((String) l);
- labels.add((String) l);
- }
- return Optional.of(labels);
- } else {
- throw new IllegalArgumentException("T.label value must be String or Collection<String>");
+ final Object labelValue = resolveLabelTokenValue(keyValues);
+ if (labelValue == LABEL_TOKEN_ABSENT)
+ return Optional.empty();
+ if (labelValue instanceof String) {
+ ElementHelper.validateLabel((String) labelValue);
+ final Set<String> labels = new LinkedHashSet<>();
+ labels.add((String) labelValue);
+ return Optional.of(labels);
+ } else if (labelValue instanceof Collection) {
+ final Set<String> labels = new LinkedHashSet<>();
+ for (final Object l : (Collection<?>) labelValue) {
+ if (!(l instanceof String)) {
+ throw new IllegalArgumentException("T.label/T.labels collection must contain only Strings");
}
+ ElementHelper.validateLabel((String) l);
+ labels.add((String) l);
}
+ return Optional.of(labels);
+ } else {
+ throw new IllegalArgumentException("T.label/T.labels value must be String or Collection<String>");
}
- return Optional.empty();
+ }
+
+ private static final Object LABEL_TOKEN_ABSENT = new Object();
+
+ /**
+ * Resolves the label value from a key/value array, accepting either {@link T#label} or its multi-label
+ * synonym {@link T#labels} (the first occurrence of a given token wins). Returns {@link #LABEL_TOKEN_ABSENT}
+ * when neither token is present.
+ *
+ * @throws IllegalArgumentException if both {@code T.label} and {@code T.labels} are supplied
+ */
+ private static Object resolveLabelTokenValue(final Object... keyValues) {
+ boolean sawLabel = false, sawLabels = false;
+ Object labelValue = LABEL_TOKEN_ABSENT;
+ for (int i = 0; i < keyValues.length; i = i + 2) {
+ final Object k = keyValues[i];
+ if (k.equals(T.label)) {
+ if (sawLabels)
+ throw new IllegalArgumentException("Vertex label may be specified with either T.label or T.labels, but not both");
+ if (!sawLabel) labelValue = keyValues[i + 1];
+ sawLabel = true;
+ } else if (k.equals(T.labels)) {
+ if (sawLabel)
+ throw new IllegalArgumentException("Vertex label may be specified with either T.label or T.labels, but not both");
+ if (!sawLabels) labelValue = keyValues[i + 1];
+ sawLabels = true;
+ }
+ }
+ return labelValue;
}
/**
@@ -323,7 +349,7 @@ public final class ElementHelper {
element.graph().features().vertex().properties().supportsNullPropertyValues();
for (int i = 0; i < propertyKeyValues.length; i = i + 2) {
- if (!propertyKeyValues[i].equals(T.id) && !propertyKeyValues[i].equals(T.label))
+ if (!propertyKeyValues[i].equals(T.id) && !propertyKeyValues[i].equals(T.label) && !propertyKeyValues[i].equals(T.labels))
if (!allowNullPropertyValues && null == propertyKeyValues[i + 1])
element.properties(((String) propertyKeyValues[i])).forEachRemaining(Property::remove);
else
@@ -348,7 +374,7 @@ public final class ElementHelper {
final boolean allowNullPropertyValues = vertex.graph().features().vertex().supportsNullPropertyValues();
for (int i = 0; i < propertyKeyValues.length; i = i + 2) {
- if (!propertyKeyValues[i].equals(T.id) && !propertyKeyValues[i].equals(T.label))
+ if (!propertyKeyValues[i].equals(T.id) && !propertyKeyValues[i].equals(T.label) && !propertyKeyValues[i].equals(T.labels))
if (!allowNullPropertyValues && null == propertyKeyValues[i + 1])
vertex.properties(((String) propertyKeyValues[i])).forEachRemaining(VertexProperty::remove);
else
@@ -374,7 +400,7 @@ public final class ElementHelper {
final boolean allowNullPropertyValues = vertex.graph().features().vertex().supportsNullPropertyValues();
for (int i = 0; i < propertyKeyValues.length; i = i + 2) {
- if (!propertyKeyValues[i].equals(T.id) && !propertyKeyValues[i].equals(T.label))
+ if (!propertyKeyValues[i].equals(T.id) && !propertyKeyValues[i].equals(T.label) && !propertyKeyValues[i].equals(T.labels))
if (!allowNullPropertyValues && null == propertyKeyValues[i + 1])
vertex.properties(((String) propertyKeyValues[i])).forEachRemaining(VertexProperty::remove);
else
diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/jsr223/ImportGremlinPluginTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/jsr223/ImportGremlinPluginTest.java
index c628f48578..f0732e1264 100644
--- a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/jsr223/ImportGremlinPluginTest.java
+++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/jsr223/ImportGremlinPluginTest.java
@@ -18,12 +18,6 @@
*/
package org.apache.tinkerpop.gremlin.jsr223;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.hasItems;
-import static org.hamcrest.Matchers.is;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.fail;
-
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
@@ -36,6 +30,11 @@ import org.apache.tinkerpop.gremlin.structure.Graph;
import org.apache.tinkerpop.gremlin.structure.T;
import org.apache.tinkerpop.gremlin.structure.io.IoCore;
import org.apache.tinkerpop.gremlin.util.Gremlin;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.hasItems;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
import org.junit.Test;
/**
@@ -117,8 +116,8 @@ public class ImportGremlinPluginTest {
final DefaultImportCustomizer customizer = (DefaultImportCustomizer) module.getCustomizers().get()[0];
assertEquals(1, module.getCustomizers().get().length);
- assertThat(customizer.getEnumImports(), hasItems(T.id, T.key, T.label, T.value));
- assertEquals(4, customizer.getEnumImports().size());
+ assertThat(customizer.getEnumImports(), hasItems(T.id, T.key, T.label, T.labels, T.value));
+ assertEquals(5, customizer.getEnumImports().size());
}
@Test
diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/lambda/TokenTraversalTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/lambda/TokenTraversalTest.java
index 6e3588cbbe..d078cb6b85 100644
--- a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/lambda/TokenTraversalTest.java
+++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/lambda/TokenTraversalTest.java
@@ -26,6 +26,9 @@ import org.apache.tinkerpop.gremlin.structure.Vertex;
import org.apache.tinkerpop.gremlin.structure.VertexProperty;
import org.junit.Test;
+import java.util.Collections;
+import java.util.Set;
+
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -40,6 +43,23 @@ public class TokenTraversalTest {
assertEquals(100, t.next().intValue());
}
+ @Test
+ public void shouldWorkOnVertexWithLabels() {
+ final TokenTraversal<Vertex, Set<String>> t = new TokenTraversal<>(T.labels);
+ final Vertex v = mock(Vertex.class);
+ when(v.labels()).thenReturn(Collections.singleton("person"));
+ t.addStart(new B_O_Traverser<>(v, 1).asAdmin());
+ assertEquals(Collections.singleton("person"), t.next());
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void shouldThrowOnPropertyWithLabels() {
+ // T.labels is defined only on an Element; applying it to a Property is unsupported, like T.label/T.id
+ final TokenTraversal<Property<String>, Set<String>> t = new TokenTraversal<>(T.labels);
+ final Property<String> pr = mock(Property.class);
+ t.addStart(new B_O_Traverser<>(pr, 1).asAdmin());
+ }
+
@Test
public void shouldWorkOnVertexProperty() {
final TokenTraversal<VertexProperty, Integer> t = new TokenTraversal<>(T.id);
diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStepTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStepTest.java
index 583e1cb845..d176e48986 100644
--- a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStepTest.java
+++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStepTest.java
@@ -468,4 +468,19 @@ public class AddVertexStepTest extends GValueStepTest {
private GraphTraversal.Admin<Object, Vertex> getMultiLabelGValueTraversal() {
return __.addV(GValue.of("l1", "person"), GValue.of("l2", "employee")).asAdmin();
}
+
+ @Test
+ public void shouldRouteLabelsTokenPropertyWithGValueToLabel() {
+ // property(T.labels, <GValue>) must route to the label (not be stored as a property) and
+ // resolve/pin the GValue exactly as the T.label path does, confirming GValue symmetry.
+ final GraphTraversal.Admin<Object, Vertex> traversal =
+ __.addV().property(T.labels, GValue.of("l1", Set.of("person", "employee"))).asAdmin();
+ final AddVertexStepPlaceholder<?> step = (AddVertexStepPlaceholder<?>) traversal.getSteps().get(0);
+ final Object resolved = step.getLabel();
+ assertTrue(resolved instanceof Set);
+ assertEquals(Set.of("person", "employee"), resolved);
+ assertFalse(step.getProperties().containsKey(T.labels));
+ assertFalse(step.getProperties().containsKey(T.label));
+ verifyVariables(traversal, Set.of("l1"), Set.of());
+ }
}
diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeEdgeStepTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeEdgeStepTest.java
index 1f143fe8f0..f0e9946dac 100644
--- a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeEdgeStepTest.java
+++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeEdgeStepTest.java
@@ -160,6 +160,19 @@ public class MergeEdgeStepTest extends GValueStepTest {
MergeEdgeStep.validateMapInput(m, true);
}
+ @Test(expected = IllegalArgumentException.class)
+ public void shouldNotAllowLabelsTokenForMergeE() {
+ final Map<Object,Object> m = CollectionUtil.asMap(T.labels, Set.of("knows"));
+ MergeEdgeStep.validateMapInput(m, false);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void shouldNotAllowLabelsTokenForMergeEOnMatch() {
+ // edges are single-label; T.labels must be rejected on the onMatch (ignoreTokens) path too
+ final Map<Object,Object> m = CollectionUtil.asMap(T.labels, Set.of("knows"));
+ MergeEdgeStep.validateMapInput(m, true);
+ }
+
@Test(expected = IllegalArgumentException.class)
public void shouldFailToValidateWithoutTokens() {
final Map<Object,Object> m = CollectionUtil.asMap("k", "v",
diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeVertexStepTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeVertexStepTest.java
index 71a12cc815..65d519add7 100644
--- a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeVertexStepTest.java
+++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeVertexStepTest.java
@@ -127,6 +127,36 @@ public class MergeVertexStepTest extends GValueStepTest {
MergeVertexStep.validateMapInput(m, false);
}
+ @Test
+ public void shouldValidateWithLabelsToken() {
+ final Map<Object,Object> m = CollectionUtil.asMap("k", "v",
+ T.labels, Set.of("person", "employee"),
+ T.id, 10000);
+ MergeVertexStep.validateMapInput(m, false);
+ }
+
+ @Test
+ public void shouldValidateLabelsTokenOnMatch() {
+ final Map<Object,Object> m = CollectionUtil.asMap(T.labels, Set.of("person"));
+ MergeVertexStep.validateMapInput(m, true);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void shouldNotAllowBothLabelAndLabels() {
+ final Map<Object,Object> m = CollectionUtil.asMap(
+ T.label, "person",
+ T.labels, Set.of("employee"));
+ MergeVertexStep.validateMapInput(m, false);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void shouldNotAllowBothLabelAndLabelsOnMatch() {
+ final Map<Object,Object> m = CollectionUtil.asMap(
+ T.label, "person",
+ T.labels, Set.of("employee"));
+ MergeVertexStep.validateMapInput(m, true);
+ }
+
@Test(expected = IllegalArgumentException.class)
public void shouldFailToValidateWithTokensBecauseOfValue() {
final Map<Object,Object> m = CollectionUtil.asMap("k", "v",
diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/TTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/TTest.java
index 18c30172c8..443f0d01cc 100644
--- a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/TTest.java
+++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/TTest.java
@@ -20,6 +20,8 @@ package org.apache.tinkerpop.gremlin.structure;
import org.junit.Test;
+import java.util.Collections;
+
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -33,6 +35,11 @@ public class TTest {
assertEquals(T.label, T.fromString(T.label.getAccessor()));
}
+ @Test
+ public void shouldGetLabelsEnumFromString() {
+ assertEquals(T.labels, T.fromString(T.labels.getAccessor()));
+ }
+
@Test
public void shouldGetIdEnumFromString() {
assertEquals(T.id, T.fromString(T.id.getAccessor()));
@@ -61,6 +68,14 @@ public class TTest {
assertEquals("knows", T.label.apply(e));
}
+ @Test
+ public void shouldApplyLabelsOnElement() {
+ final Element e = mock(Element.class);
+ when(e.labels()).thenReturn(Collections.singleton("knows"));
+
+ assertEquals(Collections.singleton("knows"), T.labels.apply(e));
+ }
+
@Test
public void shouldApplyIdOnElement() {
final Element e = mock(Element.class);
diff --git a/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/T.cs b/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/T.cs
index deb477eeac..5148b5a76d 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/T.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/T.cs
@@ -41,6 +41,8 @@ namespace Gremlin.Net.Process.Traversal
public static T Label => new T("label");
+ public static T Labels => new T("labels");
+
public static T Value => new T("value");
private static readonly IDictionary<string, T> Properties = new Dictionary<string, T>
@@ -48,6 +50,7 @@ namespace Gremlin.Net.Process.Traversal
{ "id", Id },
{ "key", Key },
{ "label", Label },
+ { "labels", Labels },
{ "value", Value },
};
diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs
index ecb968a108..214a721f29 100644
--- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs
@@ -1401,6 +1401,10 @@ namespace Gremlin.Net.IntegrationTest.Gherkin
{"g_V_hasLabelXpersonX_labels_single_label_graph", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().HasLabel("person").Labels()}},
{"g_V_labels_single_label_graph", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Labels()}},
{"g_E_labels_single_label_graph", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.E().HasLabel("knows").Labels()}},
+ {"g_V_hasXname_markoX_projectXlabelsX_byXT_labelsX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Has("name", "marko").Project<object>("labels").By(T.Labels)}},
+ {"g_V_hasXname_tuxX_projectXlabelsX_byXT_labelsX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Has("name", "tux").Project<object>("labels").By(T.Labels)}},
+ {"g_V_hasXT_labels_personX_valuesXnameX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Has(T.Labels, "person").Values<object>("name")}},
+ {"g_withXmultilabelX_V_hasXT_labels_withinXbird_reptileXX_valuesXnameX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.With("multilabel").V().Has(T.Labels, P.Within("bird", "reptile")).Values<object>("name")}},
{"g_injectXfeature_test_nullX_length", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>("feature", "test", null).Length()}},
{"g_injectXfeature_test_nullX_lengthXlocalX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>("feature", "test", null).Length<object>(Scope.Local)}},
{"g_injectXListXa_bXX_length", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>(new List<object> { "a", "b" }).Length()}},
@@ -3759,6 +3763,10 @@ namespace Gremlin.Net.IntegrationTest.Gherkin
{"g_V_hasLabelXpersonX_labels_single_label_graph", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().HasLabel("person").Labels()}},
{"g_V_labels_single_label_graph", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Labels()}},
{"g_E_labels_single_label_graph", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.E().HasLabel("knows").Labels()}},
+ {"g_V_hasXname_markoX_projectXlabelsX_byXT_labelsX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Has("name", "marko").Project<object>("labels").By(T.Labels)}},
+ {"g_V_hasXname_tuxX_projectXlabelsX_byXT_labelsX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Has("name", "tux").Project<object>("labels").By(T.Labels)}},
+ {"g_V_hasXT_labels_personX_valuesXnameX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Has(T.Labels, "person").Values<object>("name")}},
+ {"g_withXmultilabelX_V_hasXT_labels_withinXbird_reptileXX_valuesXnameX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.With("multilabel").V().Has(T.Labels, P.Within("bird", "reptile")).Values<object>("name")}},
{"g_injectXfeature_test_nullX_length", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>("feature", "test", null).Length()}},
{"g_injectXfeature_test_nullX_lengthXlocalX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>("feature", "test", null).Length<object>(Scope.Local)}},
{"g_injectXListXa_bXX_length", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>(new List<object> { "a", "b" }).Length()}},
diff --git a/gremlin-go/driver/cucumber/cucumberSteps_test.go b/gremlin-go/driver/cucumber/cucumberSteps_test.go
index 6089a47523..99c5873793 100644
--- a/gremlin-go/driver/cucumber/cucumberSteps_test.go
+++ b/gremlin-go/driver/cucumber/cucumberSteps_test.go
@@ -401,6 +401,8 @@ func toT(name, graphName string) interface{} {
// Return as is, since T values are just strings.
if name == "label" {
return gremlingo.T.Label
+ } else if name == "labels" {
+ return gremlingo.T.Labels
} else if name == "id" {
return gremlingo.T.Id
} else if name == "key" {
diff --git a/gremlin-go/driver/cucumber/gremlin.go b/gremlin-go/driver/cucumber/gremlin.go
index e95627b537..a6fdeef710 100644
--- a/gremlin-go/driver/cucumber/gremlin.go
+++ b/gremlin-go/driver/cucumber/gremlin.go
@@ -1354,6 +1354,10 @@ var translationMap = map[string][]func(g *gremlingo.GraphTraversalSource, p map[
"g_V_hasLabelXpersonX_labels_single_label_graph": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().HasLabel("person").Labels()}},
"g_V_labels_single_label_graph": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Labels()}},
"g_E_labels_single_label_graph": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.E().HasLabel("knows").Labels()}},
+ "g_V_hasXname_markoX_projectXlabelsX_byXT_labelsX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Has("name", "marko").Project("labels").By(gremlingo.T.Labels)}},
+ "g_V_hasXname_tuxX_projectXlabelsX_byXT_labelsX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Has("name", "tux").Project("labels").By(gremlingo.T.Labels)}},
+ "g_V_hasXT_labels_personX_valuesXnameX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Has(gremlingo.T.Labels, "person").Values("name")}},
+ "g_withXmultilabelX_V_hasXT_labels_withinXbird_reptileXX_valuesXnameX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.With("multilabel").V().Has(gremlingo.T.Labels, gremlingo.P.Within("bird", "reptile")).Values("name")}},
"g_injectXfeature_test_nullX_length": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject("feature", "test", nil).Length()}},
"g_injectXfeature_test_nullX_lengthXlocalX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject("feature", "test", nil).Length(gremlingo.Scope.Local)}},
"g_injectXListXa_bXX_length": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject([]interface{}{"a", "b"}).Length()}},
diff --git a/gremlin-go/driver/traversal.go b/gremlin-go/driver/traversal.go
index a78f6ee9c2..82434f5d52 100644
--- a/gremlin-go/driver/traversal.go
+++ b/gremlin-go/driver/traversal.go
@@ -387,20 +387,22 @@ var Scope = scopes{
type t string
type ts struct {
- Id t
- Label t
- Id_ t
- Key t
- Value t
+ Id t
+ Label t
+ Labels t
+ Id_ t
+ Key t
+ Value t
}
// T is string symbols.
var T = ts{
- Id: "id",
- Label: "label",
- Id_: "id_",
- Key: "key",
- Value: "value",
+ Id: "id",
+ Label: "label",
+ Labels: "labels",
+ Id_: "id_",
+ Key: "key",
+ Value: "value",
}
type materializeProperties struct {
diff --git a/gremlin-js/gremlin-javascript/lib/process/traversal.ts b/gremlin-js/gremlin-javascript/lib/process/traversal.ts
index a8f3abfc19..188e22ea79 100644
--- a/gremlin-js/gremlin-javascript/lib/process/traversal.ts
+++ b/gremlin-js/gremlin-javascript/lib/process/traversal.ts
@@ -616,5 +616,5 @@ export const order = toEnum('Order', 'asc desc shuffle');
export const pick = toEnum('Pick', 'any none unproductive');
export const pop = toEnum('Pop', 'all first last mixed');
export const scope = toEnum('Scope', 'global local');
-export const t = toEnum('T', 'id key label value');
+export const t = toEnum('T', 'id key label labels value');
export const n = toEnum('N', 'byte_ short_ int_ long_ float_ double_ bigInt bigDecimal');
diff --git a/gremlin-js/gremlin-javascript/test/cucumber/gremlin.js b/gremlin-js/gremlin-javascript/test/cucumber/gremlin.js
index 69d1632c39..b9af73f9b7 100644
--- a/gremlin-js/gremlin-javascript/test/cucumber/gremlin.js
+++ b/gremlin-js/gremlin-javascript/test/cucumber/gremlin.js
@@ -1385,6 +1385,10 @@ const gremlins = {
g_V_hasLabelXpersonX_labels_single_label_graph: [function({g}) { return g.V().hasLabel("person").labels() }],
g_V_labels_single_label_graph: [function({g}) { return g.V().labels() }],
g_E_labels_single_label_graph: [function({g}) { return g.E().hasLabel("knows").labels() }],
+ g_V_hasXname_markoX_projectXlabelsX_byXT_labelsX: [function({g}) { return g.V().has("name", "marko").project("labels").by(T.labels) }],
+ g_V_hasXname_tuxX_projectXlabelsX_byXT_labelsX: [function({g}) { return g.V().has("name", "tux").project("labels").by(T.labels) }],
+ g_V_hasXT_labels_personX_valuesXnameX: [function({g}) { return g.V().has(T.labels, "person").values("name") }],
+ g_withXmultilabelX_V_hasXT_labels_withinXbird_reptileXX_valuesXnameX: [function({g}) { return g.with_("multilabel").V().has(T.labels, P.within("bird", "reptile")).values("name") }],
g_injectXfeature_test_nullX_length: [function({g}) { return g.inject("feature", "test", null).length() }],
g_injectXfeature_test_nullX_lengthXlocalX: [function({g}) { return g.inject("feature", "test", null).length(Scope.local) }],
g_injectXListXa_bXX_length: [function({g}) { return g.inject(["a", "b"]).length() }],
diff --git a/gremlin-language/src/main/antlr4/Gremlin.g4 b/gremlin-language/src/main/antlr4/Gremlin.g4
index d15a2982df..5c7c0547ad 100644
--- a/gremlin-language/src/main/antlr4/Gremlin.g4
+++ b/gremlin-language/src/main/antlr4/Gremlin.g4
@@ -1024,6 +1024,7 @@ traversalT
traversalTShort
: K_ID
| K_LABEL
+ | K_LABELS
| K_KEY
| K_VALUE
;
@@ -1031,6 +1032,7 @@ traversalTShort
traversalTLong
: K_T DOT K_ID
| K_T DOT K_LABEL
+ | K_T DOT K_LABELS
| K_T DOT K_KEY
| K_T DOT K_VALUE
;
diff --git a/gremlin-python/src/main/python/gremlin_python/process/traversal.py b/gremlin-python/src/main/python/gremlin_python/process/traversal.py
index a030ac616c..28772da343 100644
--- a/gremlin-python/src/main/python/gremlin_python/process/traversal.py
+++ b/gremlin-python/src/main/python/gremlin_python/process/traversal.py
@@ -244,10 +244,11 @@ Scope = Enum('Scope', ' global_ local')
statics.add_static('global_', Scope.global_)
statics.add_static('local', Scope.local)
-T = Enum('T', ' id id_ key label value')
+T = Enum('T', ' id id_ key label labels value')
statics.add_static('id', T.id)
statics.add_static('label', T.label)
+statics.add_static('labels', T.labels)
statics.add_static('id_', T.id_)
statics.add_static('key', T.key)
statics.add_static('value', T.value)
diff --git a/gremlin-python/src/main/python/tests/feature/gremlin.py b/gremlin-python/src/main/python/tests/feature/gremlin.py
index fc5b396346..5de5b49a67 100644
--- a/gremlin-python/src/main/python/tests/feature/gremlin.py
+++ b/gremlin-python/src/main/python/tests/feature/gremlin.py
@@ -1359,6 +1359,10 @@ world.gremlins = {
'g_V_hasLabelXpersonX_labels_single_label_graph': [(lambda g:g.V().has_label('person').labels())],
'g_V_labels_single_label_graph': [(lambda g:g.V().labels())],
'g_E_labels_single_label_graph': [(lambda g:g.E().has_label('knows').labels())],
+ 'g_V_hasXname_markoX_projectXlabelsX_byXT_labelsX': [(lambda g:g.V().has('name', 'marko').project('labels').by(T.labels))],
+ 'g_V_hasXname_tuxX_projectXlabelsX_byXT_labelsX': [(lambda g:g.V().has('name', 'tux').project('labels').by(T.labels))],
+ 'g_V_hasXT_labels_personX_valuesXnameX': [(lambda g:g.V().has(T.labels, 'person').values('name'))],
+ 'g_withXmultilabelX_V_hasXT_labels_withinXbird_reptileXX_valuesXnameX': [(lambda g:g.with_('multilabel').V().has(T.labels, P.within('bird', 'reptile')).values('name'))],
'g_injectXfeature_test_nullX_length': [(lambda g:g.inject('feature', 'test', None).length())],
'g_injectXfeature_test_nullX_lengthXlocalX': [(lambda g:g.inject('feature', 'test', None).length(Scope.local))],
'g_injectXListXa_bXX_length': [(lambda g:g.inject(['a', 'b']).length())],
diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/language/translator/translations.json b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/language/translator/translations.json
index efd55f89ad..ae18159e02 100644
--- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/language/translator/translations.json
+++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/language/translator/translations.json
@@ -26890,6 +26890,74 @@
}
]
},
+ {
+ "scenario": "g_V_hasXname_markoX_projectXlabelsX_byXT_labelsX",
+ "traversals": [
+ {
+ "original": "g.V().has(\"name\", \"marko\").project(\"labels\").by(T.labels)",
+ "language": "g.V().has(\"name\", \"marko\").project(\"labels\").by(T.labels)",
+ "canonical": "g.V().has(\"name\", \"marko\").project(\"labels\").by(T.labels)",
+ "anonymized": "g.V().has(string0, string1).project(string2).by(T.labels)",
+ "dotnet": "g.V().Has(\"name\", \"marko\").Project<object>(\"labels\").By(T.Labels)",
+ "go": "g.V().Has(\"name\", \"marko\").Project(\"labels\").By(gremlingo.T.Labels)",
+ "groovy": "g.V().has(\"name\", \"marko\").project(\"labels\").by(T.labels)",
+ "java": "g.V().has(\"name\", \"marko\").project(\"labels\").by(T.labels)",
+ "javascript": "g.V().has(\"name\", \"marko\").project(\"labels\").by(T.labels)",
+ "python": "g.V().has('name', 'marko').project('labels').by(T.labels)"
+ }
+ ]
+ },
+ {
+ "scenario": "g_V_hasXname_tuxX_projectXlabelsX_byXT_labelsX",
+ "traversals": [
+ {
+ "original": "g.V().has(\"name\", \"tux\").project(\"labels\").by(T.labels)",
+ "language": "g.V().has(\"name\", \"tux\").project(\"labels\").by(T.labels)",
+ "canonical": "g.V().has(\"name\", \"tux\").project(\"labels\").by(T.labels)",
+ "anonymized": "g.V().has(string0, string1).project(string2).by(T.labels)",
+ "dotnet": "g.V().Has(\"name\", \"tux\").Project<object>(\"labels\").By(T.Labels)",
+ "go": "g.V().Has(\"name\", \"tux\").Project(\"labels\").By(gremlingo.T.Labels)",
+ "groovy": "g.V().has(\"name\", \"tux\").project(\"labels\").by(T.labels)",
+ "java": "g.V().has(\"name\", \"tux\").project(\"labels\").by(T.labels)",
+ "javascript": "g.V().has(\"name\", \"tux\").project(\"labels\").by(T.labels)",
+ "python": "g.V().has('name', 'tux').project('labels').by(T.labels)"
+ }
+ ]
+ },
+ {
+ "scenario": "g_V_hasXT_labels_personX_valuesXnameX",
+ "traversals": [
+ {
+ "original": "g.V().has(T.labels, \"person\").values(\"name\")",
+ "language": "g.V().has(T.labels, \"person\").values(\"name\")",
+ "canonical": "g.V().has(T.labels, \"person\").values(\"name\")",
+ "anonymized": "g.V().has(T.labels, string0).values(string1)",
+ "dotnet": "g.V().Has(T.Labels, \"person\").Values<object>(\"name\")",
+ "go": "g.V().Has(gremlingo.T.Labels, \"person\").Values(\"name\")",
+ "groovy": "g.V().has(T.labels, \"person\").values(\"name\")",
+ "java": "g.V().has(T.labels, \"person\").values(\"name\")",
+ "javascript": "g.V().has(T.labels, \"person\").values(\"name\")",
+ "python": "g.V().has(T.labels, 'person').values('name')"
+ }
+ ]
+ },
+ {
+ "scenario": "g_withXmultilabelX_V_hasXT_labels_withinXbird_reptileXX_valuesXnameX",
+ "traversals": [
+ {
+ "original": "g.with(\"multilabel\").V().has(T.labels, P.within(\"bird\", \"reptile\")).values(\"name\")",
+ "language": "g.with(\"multilabel\").V().has(T.labels, P.within(\"bird\", \"reptile\")).values(\"name\")",
+ "canonical": "g.with(\"multilabel\").V().has(T.labels, P.within(\"bird\", \"reptile\")).values(\"name\")",
+ "anonymized": "g.with(string0).V().has(T.labels, P.within(string1, string2)).values(string3)",
+ "dotnet": "g.With(\"multilabel\").V().Has(T.Labels, P.Within(\"bird\", \"reptile\")).Values<object>(\"name\")",
+ "go": "g.With(\"multilabel\").V().Has(gremlingo.T.Labels, gremlingo.P.Within(\"bird\", \"reptile\")).Values(\"name\")",
+ "groovy": "g.with(\"multilabel\").V().has(T.labels, P.within(\"bird\", \"reptile\")).values(\"name\")",
+ "java": "g.with(\"multilabel\").V().has(T.labels, P.within(\"bird\", \"reptile\")).values(\"name\")",
+ "javascript": "g.with_(\"multilabel\").V().has(T.labels, P.within(\"bird\", \"reptile\")).values(\"name\")",
+ "python": "g.with_('multilabel').V().has(T.labels, P.within('bird', 'reptile')).values('name')"
+ }
+ ]
+ },
{
"scenario": "g_injectXfeature_test_nullX_length",
"traversals": [
diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/ElementMap.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/ElementMap.feature
index ea7e30297b..4acb2559fd 100644
--- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/ElementMap.feature
+++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/ElementMap.feature
@@ -61,12 +61,12 @@ Feature: Step - elementMap()
When iterated to list
Then the result should be unordered
| result |
- | m[{"t[id]": "v[marko].id", "t[label]": "s[person]", "name": "marko", "age": 29}] |
- | m[{"t[id]": "v[josh].id", "t[label]": "s[person]", "name": "josh", "age": 32}] |
- | m[{"t[id]": "v[peter].id", "t[label]": "s[person]", "name": "peter", "age": 35}] |
- | m[{"t[id]": "v[vadas].id", "t[label]": "s[person]", "name": "vadas", "age": 27}] |
- | m[{"t[id]": "v[lop].id", "t[label]": "s[software]", "name": "lop", "lang": "java"}] |
- | m[{"t[id]": "v[ripple].id", "t[label]": "s[software]", "name": "ripple", "lang": "java"}] |
+ | m[{"t[id]": "v[marko].id", "t[labels]": "s[person]", "name": "marko", "age": 29}] |
+ | m[{"t[id]": "v[josh].id", "t[labels]": "s[person]", "name": "josh", "age": 32}] |
+ | m[{"t[id]": "v[peter].id", "t[labels]": "s[person]", "name": "peter", "age": 35}] |
+ | m[{"t[id]": "v[vadas].id", "t[labels]": "s[person]", "name": "vadas", "age": 27}] |
+ | m[{"t[id]": "v[lop].id", "t[labels]": "s[software]", "name": "lop", "lang": "java"}] |
+ | m[{"t[id]": "v[ripple].id", "t[labels]": "s[software]", "name": "ripple", "lang": "java"}] |
Scenario: g_V_elementMapXname_ageX
Given the modern graph
@@ -122,7 +122,7 @@ Feature: Step - elementMap()
When iterated to list
Then the result should be unordered
| result |
- | m[{"t[id]": "v[marko].id", "t[label]": "s[person]", "name": "marko", "age": 29}] |
+ | m[{"t[id]": "v[marko].id", "t[labels]": "s[person]", "name": "marko", "age": 29}] |
@MultiLabel @SingleLabelDefault
Scenario: g_V_elementMap_single_label_default
@@ -152,7 +152,7 @@ Feature: Step - elementMap()
When iterated to list
Then the result should be unordered
| result |
- | m[{"t[id]": "v[tux].id", "t[label]": "s[animal,bird,aquatic,endangered]", "name": "tux", "species": "african penguin"}] |
+ | m[{"t[id]": "v[tux].id", "t[labels]": "s[animal,bird,aquatic,endangered]", "name": "tux", "species": "african penguin"}] |
@MultiLabel @MultiLabelDefault
Scenario: g_V_elementMap_multi_label_default
@@ -168,7 +168,7 @@ Feature: Step - elementMap()
When iterated to list
Then the result should be unordered
| result |
- | m[{"t[id]": "v[marko].id", "t[label]": "s[person,employee]", "name": "marko"}] |
+ | m[{"t[id]": "v[marko].id", "t[labels]": "s[person,employee]", "name": "marko"}] |
@MultiLabel @MultiLabelDefault
Scenario: g_withXsinglelabelX_V_elementMap_multi_label_default
@@ -198,7 +198,7 @@ Feature: Step - elementMap()
When iterated to list
Then the result should be unordered
| result |
- | m[{"t[id]": "v[marko].id", "t[label]": "s[person]", "name": "marko"}] |
+ | m[{"t[id]": "v[marko].id", "t[labels]": "s[person]", "name": "marko"}] |
@MultiLabel
Scenario: g_withXsinglelabelX_V_elementMap_zero_label_vertex
@@ -230,7 +230,7 @@ Feature: Step - elementMap()
When iterated to list
Then the result should be unordered
| result |
- | m[{"t[id]": "v[nobody].id", "t[label]": "s[]", "name": "nobody"}] |
+ | m[{"t[id]": "v[nobody].id", "t[labels]": "s[]", "name": "nobody"}] |
@MultiLabel @MultiLabelDefault
Scenario: g_V_elementMap_zero_label_vertex_multi_label_default
@@ -246,7 +246,7 @@ Feature: Step - elementMap()
When iterated to list
Then the result should be unordered
| result |
- | m[{"t[id]": "v[nobody].id", "t[label]": "s[]", "name": "nobody"}] |
+ | m[{"t[id]": "v[nobody].id", "t[labels]": "s[]", "name": "nobody"}] |
@MultiLabel @SingleLabelDefault
Scenario: g_V_elementMap_zero_label_vertex_single_label_default
@@ -274,7 +274,7 @@ Feature: Step - elementMap()
When iterated to list
Then the result should be unordered
| result |
- | m[{"t[id]": "e[atlas-livesIn->lagoon].id", "t[label]": "s[livesIn]", "since": 2018, "D[OUT]": "m[{\\"t[id]\\": \\"v[atlas].id\\", \\"t[label]\\": \\"s[animal,reptile,aquatic,endangered]\\"}]", "D[IN]": "m[{\\"t[id]\\": \\"v[lagoon].id\\", \\"t[label]\\": \\"s[habitat,aquatic]\\"}]"}] |
+ | m[{"t[id]": "e[atlas-livesIn->lagoon].id", "t[labels]": "s[livesIn]", "since": 2018, "D[OUT]": "m[{\\"t[id]\\": \\"v[atlas].id\\", \\"t[labels]\\": \\"s[animal,reptile,aquatic,endangered]\\"}]", "D[IN]": "m[{\\"t[id]\\": \\"v[lagoon].id\\", \\"t[labels]\\": \\"s[habitat,aquatic]\\"}]"}] |
@GraphComputerVerificationReferenceOnly @MultiLabel @MultiLabelDefault
Scenario: g_E_elementMap_multi_label_default
@@ -290,7 +290,7 @@ Feature: Step - elementMap()
When iterated to list
Then the result should be unordered
| result |
- | m[{"t[id]": "e[marko-knows->josh].id", "t[label]": "s[knows]", "weight": "d[0.5].d", "D[OUT]": "m[{\\"t[id]\\": \\"v[marko].id\\", \\"t[label]\\": \\"s[person]\\"}]", "D[IN]": "m[{\\"t[id]\\": \\"v[josh].id\\", \\"t[label]\\": \\"s[person]\\"}]"}] |
+ | m[{"t[id]": "e[marko-knows->josh].id", "t[labels]": "s[knows]", "weight": "d[0.5].d", "D[OUT]": "m[{\\"t[id]\\": \\"v[marko].id\\", \\"t[labels]\\": \\"s[person]\\"}]", "D[IN]": "m[{\\"t[id]\\": \\"v[josh].id\\", \\"t[labels]\\": \\"s[person]\\"}]"}] |
@GraphComputerVerificationReferenceOnly @MultiLabel @SingleLabelDefault
Scenario: g_E_elementMap_single_label_default
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 26fb1db50d..fc9a356449 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
@@ -132,3 +132,54 @@ Feature: Step - labels()
| result |
| knows |
| knows |
+
+ Scenario: g_V_hasXname_markoX_projectXlabelsX_byXT_labelsX
+ Given the modern graph
+ And the traversal of
+ """
+ g.V().has("name", "marko").project("labels").by(T.labels)
+ """
+ When iterated to list
+ Then the result should be unordered
+ | result |
+ | m[{"labels": "s[person]"}] |
+
+ @MultiLabel
+ Scenario: g_V_hasXname_tuxX_projectXlabelsX_byXT_labelsX
+ Given the zoo graph
+ And the traversal of
+ """
+ g.V().has("name", "tux").project("labels").by(T.labels)
+ """
+ When iterated to list
+ Then the result should be unordered
+ | result |
+ | m[{"labels": "s[animal,bird,aquatic,endangered]"}] |
+
+ Scenario: g_V_hasXT_labels_personX_valuesXnameX
+ Given the modern graph
+ And the traversal of
+ """
+ g.V().has(T.labels, "person").values("name")
+ """
+ When iterated to list
+ Then the result should be unordered
+ | result |
+ | marko |
+ | vadas |
+ | josh |
+ | peter |
+
+ @MultiLabel
+ Scenario: g_withXmultilabelX_V_hasXT_labels_withinXbird_reptileXX_valuesXnameX
+ Given the zoo graph
+ And the traversal of
+ """
+ g.with("multilabel").V().has(T.labels, P.within("bird", "reptile")).values("name")
+ """
+ When iterated to list
+ Then the result should be unordered
+ | result |
+ | tux |
+ | atlas |
+ | monty |
diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/ValueMap.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/ValueMap.feature
index 791d6333e8..a2762a94f6 100644
--- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/ValueMap.feature
+++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/ValueMap.feature
@@ -93,12 +93,12 @@ Feature: Step - valueMap()
When iterated to list
Then the result should be unordered
| result |
- | m[{"t[id]": "v[marko].id", "t[label]": "s[person]", "name": ["marko"], "age": [29]}] |
- | m[{"t[id]": "v[josh].id", "t[label]": "s[person]", "name": ["josh"], "age": [32]}] |
- | m[{"t[id]": "v[peter].id", "t[label]": "s[person]", "name": ["peter"], "age": [35]}] |
- | m[{"t[id]": "v[vadas].id", "t[label]": "s[person]", "name": ["vadas"], "age": [27]}] |
- | m[{"t[id]": "v[lop].id", "t[label]": "s[software]", "name": ["lop"], "lang": ["java"]}] |
- | m[{"t[id]": "v[ripple].id", "t[label]": "s[software]", "name": ["ripple"], "lang": ["java"]}] |
+ | m[{"t[id]": "v[marko].id", "t[labels]": "s[person]", "name": ["marko"], "age": [29]}] |
+ | m[{"t[id]": "v[josh].id", "t[labels]": "s[person]", "name": ["josh"], "age": [32]}] |
+ | m[{"t[id]": "v[peter].id", "t[labels]": "s[person]", "name": ["peter"], "age": [35]}] |
+ | m[{"t[id]": "v[vadas].id", "t[labels]": "s[person]", "name": ["vadas"], "age": [27]}] |
+ | m[{"t[id]": "v[lop].id", "t[labels]": "s[software]", "name": ["lop"], "lang": ["java"]}] |
+ | m[{"t[id]": "v[ripple].id", "t[labels]": "s[software]", "name": ["ripple"], "lang": ["java"]}] |
Scenario: g_V_valueMapXname_ageX
Given the modern graph
@@ -264,7 +264,7 @@ Feature: Step - valueMap()
When iterated to list
Then the result should be unordered
| result |
- | m[{"t[id]": "v[marko].id", "t[label]": "s[person]", "name": ["marko"], "age": [29]}] |
+ | m[{"t[id]": "v[marko].id", "t[labels]": "s[person]", "name": ["marko"], "age": [29]}] |
@MultiLabel @SingleLabelDefault
Scenario: g_V_valueMap_withXtokensX_single_label_default
@@ -294,7 +294,7 @@ Feature: Step - valueMap()
When iterated to list
Then the result should be unordered
| result |
- | m[{"t[id]": "v[tux].id", "t[label]": "s[animal,bird,aquatic,endangered]", "name": ["tux"], "species": ["african penguin"]}] |
+ | m[{"t[id]": "v[tux].id", "t[labels]": "s[animal,bird,aquatic,endangered]", "name": ["tux"], "species": ["african penguin"]}] |
@MultiLabel @MultiLabelDefault
Scenario: g_V_valueMap_withXtokensX_multi_label_default
@@ -310,7 +310,7 @@ Feature: Step - valueMap()
When iterated to list
Then the result should be unordered
| result |
- | m[{"t[id]": "v[marko].id", "t[label]": "s[person,employee]", "name": ["marko"]}] |
+ | m[{"t[id]": "v[marko].id", "t[labels]": "s[person,employee]", "name": ["marko"]}] |
@MultiLabel @MultiLabelDefault
Scenario: g_withXsinglelabelX_V_valueMap_withXtokensX_multi_label_default
@@ -356,7 +356,7 @@ Feature: Step - valueMap()
When iterated to list
Then the result should be unordered
| result |
- | m[{"t[id]": "v[nobody].id", "t[label]": "s[]", "name": ["nobody"]}] |
+ | m[{"t[id]": "v[nobody].id", "t[labels]": "s[]", "name": ["nobody"]}] |
@MultiLabel @MultiLabelDefault
Scenario: g_V_valueMapXtrueX_zero_label_vertex_multi_label_default
@@ -372,7 +372,7 @@ Feature: Step - valueMap()
When iterated to list
Then the result should be unordered
| result |
- | m[{"t[id]": "v[nobody].id", "t[label]": "s[]", "name": ["nobody"]}] |
+ | m[{"t[id]": "v[nobody].id", "t[labels]": "s[]", "name": ["nobody"]}] |
@MultiLabel @SingleLabelDefault
Scenario: g_V_valueMapXtrueX_zero_label_vertex_single_label_default
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 b9bc0e4d41..90571ba693 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
@@ -190,7 +190,7 @@ public class LabelMutationPropertyTest {
// with multilabel config: should return Set<String>
final GraphTraversalSource gml = g.with("multilabel");
final Map<Object, Object> mapWithConfig = gml.V(v).valueMap(true).next();
- final Object labelWithConfig = mapWithConfig.get(T.label);
+ final Object labelWithConfig = mapWithConfig.get(T.labels);
assertThat("Iteration " + i + ": with multilabel config, label should be a Set",
labelWithConfig, instanceOf(Set.class));
assertThat("Iteration " + i + ": with multilabel config, labels should match",
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 761d714524..a12d6c8d12 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
@@ -18,6 +18,7 @@
*/
package org.apache.tinkerpop.gremlin.tinkergraph.process.traversal.step.sideEffect;
+import org.apache.tinkerpop.gremlin.process.traversal.Merge;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
import org.apache.tinkerpop.gremlin.process.traversal.step.util.WithOptions;
import org.apache.tinkerpop.gremlin.structure.Edge;
@@ -30,6 +31,9 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Test;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -184,7 +188,7 @@ public class LabelMutationStepTest {
final Vertex v = g.addV("person").addLabel("employee").next();
final GraphTraversalSource gml = g.with("multilabel");
final Map<Object, Object> map = gml.V(v).valueMap(true).next();
- final Object labelValue = map.get(T.label);
+ final Object labelValue = map.get(T.labels);
assertThat(labelValue, instanceOf(Set.class));
final Set<String> labels = (Set<String>) labelValue;
assertThat(labels, containsInAnyOrder("person", "employee"));
@@ -227,12 +231,112 @@ public class LabelMutationStepTest {
final Vertex v = g.addV("person").addLabel("employee").next();
final GraphTraversalSource gml = g.with("multilabel");
final Map<Object, Object> map = gml.V(v).elementMap().next();
- final Object labelValue = map.get(T.label);
+ final Object labelValue = map.get(T.labels);
assertThat(labelValue, instanceOf(Set.class));
final Set<String> labels = (Set<String>) labelValue;
assertThat(labels, containsInAnyOrder("person", "employee"));
}
+ // --- mergeV T.labels symmetry tests ---
+
+ @Test
+ public void shouldCreateMultiLabelVertexWithMergeVUsingLabelsToken() {
+ final Map<Object, Object> merge = new LinkedHashMap<>();
+ merge.put(T.labels, new LinkedHashSet<>(Arrays.asList("person", "employee")));
+ final Vertex v = g.mergeV(merge).next();
+ assertThat(v.labels(), containsInAnyOrder("person", "employee"));
+ }
+
+ @Test
+ public void shouldMatchExistingVertexByLabelsToken() {
+ final Vertex v = g.addV("person").addLabel("employee").next();
+ final Map<Object, Object> merge = new LinkedHashMap<>();
+ merge.put(T.labels, new LinkedHashSet<>(Arrays.asList("person", "employee")));
+ // T.labels normalizes to T.label and drives an AND label match, resolving to the existing vertex
+ final Vertex merged = g.mergeV(merge).next();
+ assertThat(merged.id(), is(v.id()));
+ assertThat(g.V().count().next(), is(1L));
+ }
+
+ @Test
+ public void shouldMatchExistingVertexWhenMergeVFedMultilabelElementMap() {
+ final Vertex v = g.addV("person").addLabel("employee").property("name", "marko").next();
+ final GraphTraversalSource gml = g.with("multilabel");
+ // multilabel elementMap keys the label set under T.labels; feeding it straight back into
+ // mergeV must resolve to the same vertex (T.labels treated as a synonym for T.label).
+ final Map<Object, Object> elementMap = gml.V(v).elementMap().next();
+ final Vertex merged = g.mergeV(elementMap).next();
+ assertThat(merged.id(), is(v.id()));
+ assertThat(merged.labels(), containsInAnyOrder("person", "employee"));
+ }
+
+ @Test
+ public void shouldApplyLabelsTokenOnMatch() {
+ final Vertex v = g.addV("person").property("name", "marko").next();
+ final Map<Object, Object> search = new LinkedHashMap<>();
+ search.put(T.label, "person");
+ final Map<Object, Object> onMatch = new LinkedHashMap<>();
+ onMatch.put(T.labels, new LinkedHashSet<>(Arrays.asList("employee", "manager")));
+ g.mergeV(search).option(Merge.onMatch, onMatch).iterate();
+ assertThat(v.labels(), containsInAnyOrder("person", "employee", "manager"));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void shouldRejectBothLabelAndLabelsInMergeV() {
+ final Map<Object, Object> merge = new LinkedHashMap<>();
+ merge.put(T.label, "person");
+ merge.put(T.labels, new LinkedHashSet<>(Arrays.asList("employee")));
+ g.mergeV(merge).iterate();
+ }
+
+ // --- addV / addVertex T.labels create symmetry tests ---
+
+ @Test
+ public void shouldCreateVertexWithLabelsTokenViaAddV() {
+ final Vertex v = g.addV().property(T.labels, new LinkedHashSet<>(Arrays.asList("person", "employee"))).next();
+ assertThat(v.labels(), containsInAnyOrder("person", "employee"));
+ }
+
+ @Test
+ public void shouldCreateVertexWithLabelsTokenViaGraphAddVertex() {
+ final Vertex v = graph.addVertex(T.labels, new LinkedHashSet<>(Arrays.asList("person", "employee")), "name", "marko");
+ assertThat(v.labels(), containsInAnyOrder("person", "employee"));
+ // T.labels must drive the label set, not be attached as a property
+ assertThat(v.value("name"), is("marko"));
+ }
+
+ @Test
+ public void shouldCreateZeroLabelVertexWithEmptyLabelsToken() {
+ // graph is ZERO_OR_MORE, so an empty label set is permitted on create
+ final Vertex v = graph.addVertex(T.labels, new LinkedHashSet<String>());
+ assertThat(v.labels(), hasSize(0));
+ }
+
+ @Test
+ public void shouldCreateZeroLabelVertexWithEmptyLabelsTokenViaAddV() {
+ // an explicit empty T.labels set on addV() behaves like graph.addVertex(T.labels, []) under ZERO_OR_MORE
+ final Vertex v = g.addV().property(T.labels, new LinkedHashSet<String>()).next();
+ assertThat(v.labels(), hasSize(0));
+ }
+
+ @Test
+ public void shouldCreateZeroLabelVertexWithEmptyLabelTokenViaAddV() {
+ // the same holds for an explicit empty collection under the singular T.label token
+ final Vertex v = g.addV().property(T.label, new LinkedHashSet<String>()).next();
+ assertThat(v.labels(), hasSize(0));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void shouldRejectBothLabelAndLabelsOnGraphAddVertex() {
+ graph.addVertex(T.label, "person", T.labels, new LinkedHashSet<>(Arrays.asList("employee")));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void shouldRejectLabelSetTwiceViaAddVArgAndLabelsProperty() {
+ // supplying a label via the addV() argument and again via T.labels is rejected (label already set)
+ g.addV("person").property(T.labels, new LinkedHashSet<>(Arrays.asList("employee"))).iterate();
+ }
+
// --- GraphTraversalSource multi-label addV test ---
@Test