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 <[email protected]> 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
