diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 31085a634c3..0eafd9d74b6 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -44,6 +44,13 @@ image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima * Changed `Tree` to no longer extend `HashMap`; it is now a final class with a tree-shaped API (`childAt`, `hasChild`, `contains`, `findSubtree`, `getOrCreateChild`, `getNodesAtDepth`, `getLeafNodes`, `nodeCount`) and is no longer a `Map`. * Changed `count(local)` on a `Tree` to return the total node count (`Tree.nodeCount()`) instead of the root-entry count. * Changed Tree class in Java to not extend from HashMap and offered a new tree-shaped API for navigation. +* Added multi-label support for vertices with configurable `LabelCardinality` (`ONE`, `ONE_OR_MORE`, `ZERO_OR_MORE`). +* Added `labels()` step to emit each vertex label as a separate traverser. +* Added `addLabel()` and `dropLabel()`/`dropLabels()` steps for vertex label mutation. +* Added `LabelCardinalityValidator` utility for constraint enforcement separated from the `LabelCardinality` enum. +* Added `with("multilabel")` source configuration for `elementMap()` and `valueMap()` to return labels as a set. +* Added `labels().drop()` rejection to `StandardVerificationStrategy` to prevent accidental usage. +* Added multi-label syntax to `addV()` and `mergeV()` with append-only `onMatch` semantics. * Added typed numeric wrappers and `preciseNumbers` connection option to `gremlin-javascript` for explicit control over numeric type serialization and deserialization. * Added `NextN(n)` to `Traversal` in `gremlin-go` for batched result iteration, providing API parity with `next(n)` in the Java, Python, and .NET GLVs, and updated the Go translators in `gremlin-core` and `gremlin-javascript` to emit `NextN(n)` for the batched form. * Added Provider Defined Types (PDT) support — graph providers can define custom types via `@ProviderDefined` annotation that serialize/deserialize seamlessly across all GLVs without driver-side configuration. Replaces TP3 custom type mechanism. diff --git a/docker/gremlin-server/gremlin-server-integration.yaml b/docker/gremlin-server/gremlin-server-integration.yaml index 1880b34810e..a730970f851 100644 --- a/docker/gremlin-server/gremlin-server-integration.yaml +++ b/docker/gremlin-server/gremlin-server-integration.yaml @@ -23,6 +23,9 @@ graphs: { graph: { configuration: conf/tinkergraph-service.properties, traversalSources: [{name: g}, {name: ggraph}]}, + multilabel: { + configuration: conf/tinkergraph-multilabel.properties, + traversalSources: [{name: gmultilabel}]}, immutable: { configuration: conf/tinkergraph-service.properties, traversalSources: [{name: gimmutable}]}, diff --git a/docs/src/dev/developer/for-committers.asciidoc b/docs/src/dev/developer/for-committers.asciidoc index 7340f3c12ef..c83665bd7bf 100644 --- a/docs/src/dev/developer/for-committers.asciidoc +++ b/docs/src/dev/developer/for-committers.asciidoc @@ -672,8 +672,17 @@ TinkerGQL support out of the box and runs these scenarios without any additional * `@InsertionOrderingRequired` - The scenario is reliant on the graph system predictably returning results (vertices, edges, properties) in the same order in which they were inserted into the graph. * `@MetaProperties` - The scenario makes use of meta-properties. +* `@MultiLabel` - The scenario requires a graph that supports multiple labels per vertex (i.e. +`ONE_OR_MORE` or `ZERO_OR_MORE` vertex label cardinality). Providers that only support a vertex label cardinality of +`ONE` should exclude these tests. +* `@MultiLabelDefault` - The scenario assumes a graphs unconfigured `valueMap()`/`elementMap()` steps +produce results following `with("multilabel")` semantics. Graphs which default to single-label semantics +should exclude these tests. * `@MultiProperties` - The scenario makes use of multi-properties. * `@SupportsChar` - The scenario requires that `char` is completely supported. +* `@SingleLabelDefault` - The scenario assumes a graphs unconfigured `valueMap()`/`elementMap()` steps +produce results following `with("singlelabel")` semantics. Graphs which default to multi-label semantics +should exclude these tests. * `@SupportsDuration` - The scenario requires that `Duration` is completely supported. * `@SupportsBinary` - The scenario requires that `Binary` is completely supported. * `@UserSuppliedVertexIds` - The scenario relies on the vertex IDs specified in the dataset used by the scenario. diff --git a/docs/src/dev/provider/gremlin-semantics.asciidoc b/docs/src/dev/provider/gremlin-semantics.asciidoc index 1ec854de20d..dbe7d9157d2 100644 --- a/docs/src/dev/provider/gremlin-semantics.asciidoc +++ b/docs/src/dev/provider/gremlin-semantics.asciidoc @@ -587,6 +587,84 @@ See: link:https://github.com/apache/tinkerpop/tree/x.y.z/gremlin-core/src/main/j link:https://github.com/apache/tinkerpop/tree/x.y.z/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStartStep.java[source (start)], link:https://tinkerpop.apache.org/docs/x.y.z/reference/#addvertex-step[reference] +=== addLabel() + +*Description:* Adds one or more labels to an element. + +*Syntax:* `addLabel(String label, String... moreLabels)` | `addLabel(Traversal labelTraversal, Traversal... moreLabelTraversals)` + +[width="100%",options="header"] +|========================================================= +|Start Step |Mid Step |Modulated |Domain |Range +|N |Y |N |`Element` |`Element` +|========================================================= + +*Arguments:* + +* `label` - The label to add. +* `moreLabels` - Additional labels to add. +* `labelTraversal` - The first (or only) traversal that produces a label to add. +* `moreLabelTraversals` - Additional label-producing traversals (may be empty for single-traversal behavior). + +*Considerations:* + +Label add and remove operations require a `LabelCardinality` that permits changing the set of labels on an element +(`ONE_OR_MORE` or `ZERO_OR_MORE`). When `LabelCardinality` is `ONE`, the vertex's label is fixed at creation and +cannot be added to or removed. +Adding a label that already exists on the element is a no-op. The step returns the element (sideEffect semantics). +Each traversal argument is iterated for a single result only. If exactly one traversal is provided and it produces +a `Collection`, the collection will be automatically unfolded into the set of labels added; if more than one +traversal is provided, each must resolve to a single `String` label. + +*Exceptions* + +* If the graph's `LabelCardinality` does not permit label modification, an `IllegalStateException` will be thrown. +* If any label argument is null or empty, an `IllegalArgumentException` will be thrown. +* If more than one traversal is provided and any of them produces a `Collection`, an `IllegalArgumentException` will +be thrown. + +See: link:https://github.com/apache/tinkerpop/tree/x.y.z/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/AddLabelStep.java[source], +link:https://tinkerpop.apache.org/docs/x.y.z/reference/#addlabel-step[reference] + +=== dropLabel() + +*Description:* Removes one or more specific labels from an element, or removes all labels. + +*Syntax:* `dropLabel(String label, String... moreLabels)` | `dropLabel(Traversal labelTraversal, Traversal... moreLabelTraversals)` | `dropLabels()` + +[width="100%",options="header"] +|========================================================= +|Start Step |Mid Step |Modulated |Domain |Range +|N |Y |N |`Element` |`Element` +|========================================================= + +*Arguments:* + +* `label` - The label to remove. +* `moreLabels` - Additional labels to remove. +* `labelTraversal` - The first (or only) traversal that produces a label to remove. +* `moreLabelTraversals` - Additional label-producing traversals (may be empty for single-traversal behavior). + +*Considerations:* + +Label add and remove operations require a `LabelCardinality` that permits changing the set of labels on an element +(`ONE_OR_MORE` or `ZERO_OR_MORE`). Dropping a label that does not exist +on the element is a no-op. `dropLabels()` removes all labels (only valid with `ZERO_OR_MORE`). The step returns the +element (sideEffect semantics). +Each traversal argument is iterated for a single result only. If exactly one traversal is provided and it produces +a `Collection`, the collection will be automatically unfolded into the set of labels removed; if more than +one traversal is provided, each must resolve to a single `String` label. + +*Exceptions* + +* If the graph's `LabelCardinality` does not permit label modification, an `IllegalStateException` will be thrown. +* If `dropLabel()` or `dropLabels()` would violate the minimum label count, an `IllegalStateException` will be thrown. +* If more than one traversal is provided and any of them produces a `Collection`, an `IllegalArgumentException` will +be thrown. + +See: link:https://github.com/apache/tinkerpop/tree/x.y.z/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/DropLabelsStep.java[source], +link:https://tinkerpop.apache.org/docs/x.y.z/reference/#droplabel-step[reference] + [[all-step]] === all() @@ -1525,6 +1603,41 @@ None None +[[elementmap-step]] +=== elementMap() + +*Description:* Converts elements to a `Map` representation containing the element's `id`, `label`, and properties. +For an `Edge`, the map also includes the `IN` and `OUT` vertex structures (each a nested map of `id` and `label`). + +*Syntax:* `elementMap(String... propertyKeys)` + +[width="100%",options="header"] +|========================================================= +|Start Step |Mid Step |Modulated |Domain |Range +|N |Y |N |`Element` |`Map` +|========================================================= + +*Arguments:* + +* `propertyKeys` - If `elementMap()` is provided a list of propertyKeys, then the result map will only contain +property entries specified by propertyKeys. If the list is empty, then all properties are included. + +*Considerations:* + +The label format in the map is controlled by source-level `with("multilabel")` and `with("singlelabel")` options, not +by the graph's `LabelCardinality`. When `"multilabel"` is present, the label value is a `Set`; likewise when +`"singlelabel"` is present, it is a single `String`. The two options are mutually exclusive, configuring both +`"multilabel"` and `"singlelabel"` on the same source is rejected with a `VerificationException`. Note that is a +provider choice whether an unconfigured `GraphTraversal` defaults to `multilabel` or `singlelabel` semantics. The +reference implementation defaults to `singlelabel` semantics. + +*Exceptions* + +None + +See: link:https://github.com/apache/tinkerpop/tree/x.y.z/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/ElementMapStep.java[source], +link:https://tinkerpop.apache.org/docs/x.y.z/reference/#elementmap-step[reference] + [[format-step]] === format() @@ -1637,6 +1750,49 @@ See: link:https://github.com/apache/tinkerpop/tree/x.y.z/gremlin-core/src/main/j link:https://github.com/apache/tinkerpop/tree/x.y.z/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/GroupCountSideEffectStep.java[source (sideEffect)], link:https://tinkerpop.apache.org/docs/x.y.z/reference/#groupcount-step[reference] +[[label-step]] +=== label() + +*Description:* Maps an element to its label as a single string value. + +*Syntax:* `label()` + +[width="100%",options="header"] +|========================================================= +|Start Step |Mid Step |Modulated |Domain |Range +|N |Y |N |`Element` |`String` +|========================================================= + +*Considerations:* + +The `label()` step returns exactly one label string per element. For graphs that support multiple labels per vertex, +`label()` returns only one of the vertex's labels and the choice is non-deterministic (implementation-dependent +ordering). Use `labels()` to retrieve all labels reliably. + +See: link:https://github.com/apache/tinkerpop/tree/x.y.z/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/LabelStep.java[source], +link:https://tinkerpop.apache.org/docs/x.y.z/reference/#label-step[reference] + +[[labels-step]] +=== labels() + +*Description:* Maps an element to its labels, emitting one traverser per label. + +*Syntax:* `labels()` + +[width="100%",options="header"] +|========================================================= +|Start Step |Mid Step |Modulated |Domain |Range +|N |Y |N |`Element` |`String` +|========================================================= + +*Considerations:* + +For vertices with a single label, `labels()` emits one traverser (equivalent to `label()`). For multi-label vertices, +it emits one traverser per label. For vertices with zero labels (`ZERO_OR_MORE` mode), no traversers are emitted. + +See: link:https://github.com/apache/tinkerpop/tree/x.y.z/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/LabelsStep.java[source], +link:https://tinkerpop.apache.org/docs/x.y.z/reference/#labels-step[reference] + [[length-step]] === length() @@ -2564,6 +2720,19 @@ the possible values are: ** 8 for including values (VertexProperty) ** 15 for including all +*Considerations:* + +The label format in the map is controlled by source-level `with("multilabel")` and `with("singlelabel")` options, not +by the graph's `LabelCardinality`. When `"multilabel"` is present, the label value is a `Set`; likewise when +`"singlelabel"` is present, it is a single `String`. The two options are mutually exclusive, configuring both +`"multilabel"` and `"singlelabel"` on the same source is rejected with a `VerificationException`. Note that is a +provider choice whether an unconfigured `GraphTraversal` defaults to `multilabel` or `singlelabel` semantics. The +reference implementation defaults to `singlelabel` semantics. + +*Exceptions* + +None + See: link:https://github.com/apache/tinkerpop/tree/x.y.z/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/PropertyMapStep.java[source], link:https://tinkerpop.apache.org/docs/x.y.z/reference/#valuemap-step[reference], link:https://tinkerpop.apache.org/docs/x.y.z/reference/#propertymap-step[reference] diff --git a/docs/src/recipes/anti-patterns.asciidoc b/docs/src/recipes/anti-patterns.asciidoc index 18dde78dd61..59d061d06c7 100644 --- a/docs/src/recipes/anti-patterns.asciidoc +++ b/docs/src/recipes/anti-patterns.asciidoc @@ -266,6 +266,12 @@ g.V().choose(label). <1> Note, that tokens use a `fold()` reducer by default. <2> `by("name")` doesn't use a token, but falls into the same category as the String `"name"` is translated into an optimized traversal. +NOTE: The `label` token used above is equivalent to the `label()` step, which on a vertex with multiple labels +returns only one of them, chosen non-deterministically. On graphs configured for multiple labels per vertex, prefer +`by(labels().fold())` in place of `by(label)` so that every label a vertex carries is accounted for. See the +link:https://tinkerpop.apache.org/docs/x.y.z/reference/#by-step[By Step] section of the Reference Documentation for +further details. + [has-traversal] == has() and Traversal Arguments diff --git a/docs/src/reference/implementations-tinkergraph.asciidoc b/docs/src/reference/implementations-tinkergraph.asciidoc index f9ce642b74d..1bdaa3f5311 100644 --- a/docs/src/reference/implementations-tinkergraph.asciidoc +++ b/docs/src/reference/implementations-tinkergraph.asciidoc @@ -166,6 +166,7 @@ TinkerGraph has several settings that can be provided on creation via `Configura |gremlin.tinkergraph.vertexPropertyIdManager |The `IdManager` implementation to use for vertex properties. |gremlin.tinkergraph.defaultVertexPropertyCardinality |The default `VertexProperty.Cardinality` to use when `Vertex.property(k,v)` is called. |gremlin.tinkergraph.allowNullPropertyValues |A boolean value that determines whether or not `null` property values are allowed and defaults to `false`. +|gremlin.tinkergraph.vertexLabelCardinality |The `LabelCardinality` for vertices. Options are `ONE` (default, single immutable label, backward-compatible with 3.x), `ONE_OR_MORE` (at least one label, mutable), or `ZERO_OR_MORE` (zero or more labels). See <>. |gremlin.tinkergraph.graphLocation |The path and file name for where TinkerGraph should persist the graph data. If a value is specified here, the `gremlin.tinkergraph.graphFormat` should also be specified. If this value is not included (default), then the graph will stay in-memory and not be loaded/persisted to disk. @@ -219,6 +220,36 @@ g.io("data/tinkerpop-crew.kryo").read().iterate() g.V().properties() ---- +[[tinkergraph-multi-label]] +=== Multi-Label Support + +TinkerGraph supports multiple labels per vertex when `gremlin.tinkergraph.vertexLabelCardinality` is set to +`ONE_OR_MORE` or `ZERO_OR_MORE`. This enables use of the `addLabel()`, `dropLabel()`, `dropLabels()`, and `labels()` +traversal steps. + +[gremlin-groovy] +---- +conf = new BaseConfiguration() +conf.setProperty("gremlin.tinkergraph.vertexLabelCardinality", "ZERO_OR_MORE") +graph = TinkerGraph.open(conf) +g = traversal().with(graph) +g.addV("person","employee").property("name","marko").iterate() +g.V().has("name","marko").labels() <1> +g.V().has("name","marko").addLabel("manager").labels() <2> +g.V().has("name","marko").dropLabel("employee").labels() <3> +---- + +<1> The vertex was created with two labels. +<2> A third label is added. +<3> One label is removed. + +The `hasLabel()` step works with multi-label vertices using OR semantics: `hasLabel("a","b")` matches any vertex that +has label "a" OR label "b". To match vertices having both labels, chain multiple `hasLabel()` calls: +`hasLabel("a").hasLabel("b")`. + +This multi-label support applies to vertices only; an `Edge` must have exactly one label regardless of the +graph's configured `vertexLabelCardinality`. + [[tinkergraph-gremlin-tx]] === Transactions diff --git a/docs/src/reference/intro.asciidoc b/docs/src/reference/intro.asciidoc index 52e99076982..65e31598101 100644 --- a/docs/src/reference/intro.asciidoc +++ b/docs/src/reference/intro.asciidoc @@ -138,7 +138,7 @@ tutorial. .Primary components of the TinkerPop *structure* API * `Graph`: maintains a set of vertices and edges, and access to database functions such as transactions. - * `Element`: maintains a collection of properties and a string label denoting the element type. + * `Element`: maintains a collection of properties and one or more string labels denoting the element type. ** `Vertex`: extends Element and maintains a set of incoming and outgoing edges. ** `Edge`: extends Element and maintains an incoming and outgoing vertex. * `Property`: a string key associated with a `V` value. @@ -170,6 +170,10 @@ way for users to think when building applications with TinkerPop. The structure providers. Those interested in implementing the structure API to make their graph system TinkerPop enabled can learn more about it in the link:https://tinkerpop.apache.org/docs/x.y.z/dev/provider/[Graph Provider] documentation. +Graph providers may also optionally support certain variances on this basic structure, such as a vertex having +multiple properties per key or multiple labels. See <> +for details. + [[the-graph-process]] === The Graph Process diff --git a/docs/src/reference/the-graph.asciidoc b/docs/src/reference/the-graph.asciidoc index 4ddba5e6f16..75bd02a67ce 100644 --- a/docs/src/reference/the-graph.asciidoc +++ b/docs/src/reference/the-graph.asciidoc @@ -91,20 +91,29 @@ WARNING: Features of reference graphs which are used to connect to remote graphs graph to which it connects. It reflects the features of instantiated graph itself, which will likely be quite different considering that reference graphs will typically be immutable. +[[optional-data-model-variances]] +== Optional Data Model Variances + +The simplest property graph model consists of vertices and edges, each with a single label and a set of key/value +properties. This is the lowest common denominator that every TinkerPop-enabled graph supports. Beyond that +baseline, TinkerPop allows graph providers to optionally support certain variances on the data model that extend +what a graph element can represent. Support for these variances differs from provider to provider and can be +discovered at runtime via <>. The variances covered in this section are: + +* Multiple properties (*multi-properties*): a vertex property key can have multiple values. +* Properties on properties (*meta-properties*): a vertex property can itself have properties. +* Multiple labels (*multi-label*): a vertex can carry more than one label, or none at all, rather than exactly one. +* Null property values (*nullable properties*): a property value of `null` can be stored rather than triggering +removal of the property. + [[vertex-properties]] -== Vertex Properties +=== Vertex Properties image:vertex-properties.png[width=215,float=left] TinkerPop introduces the concept of a `VertexProperty`. All the properties of a `Vertex` are a `VertexProperty`. A `VertexProperty` implements `Property` and as such, it has a key/value pair. However, `VertexProperty` also implements `Element` and thus, can have a collection of key/value pairs. Moreover, while an `Edge` can only have one property of key "name" (for example), a `Vertex` can have multiple -"name" properties. With the inclusion of vertex properties, two features are introduced which ultimately advance the -graph modelers toolkit: - -. Multiple properties (*multi-properties*): a vertex property key can have multiple values. For example, a vertex can -have multiple "name" properties. -. Properties on properties (*meta-properties*): a vertex property can have properties (i.e. a vertex property can -have key/value data associated with it). +"name" properties. This is what enables the multi-properties and meta-properties variances introduced above. Possible use cases for meta-properties: @@ -179,6 +188,33 @@ g.V().has('name','gremlin').inE('uses'). select('a','b').by('skill').by('name') // rank the users of gremlin by their skill level ---- +[[vertex-labels]] +=== Vertex Labels + +In the simplest property graph model, a vertex has exactly one immutable label assigned at creation, the same as an +edge. TinkerPop also allows graph providers to optionally support a different vertex label cardinality, in which +case a vertex may carry multiple labels, or none at all. Whether a graph supports this, and to what degree, is +governed by its configured `LabelCardinality`: + +* `ONE` fixes exactly one immutable label per vertex +* `ONE_OR_MORE` requires at least one label but permits additional labels to be added or removed +* `ZERO_OR_MORE` allows a vertex to have any number of labels including none + +It is up to each graph provider to decide which of these cardinalities it supports and, where more than one is +supported, what its default is or whether the choice is left to the user to configure. See the +link:https://tinkerpop.apache.org/docs/x.y.z/dev/provider/#_label_cardinality[provider documentation] for configuration +details and the <>, <>, <>, and +<> steps for traversal access. + +[[nullable-property-values]] +=== Nullable Property Values + +Ordinarily, setting a property to `null` is equivalent to removing it, since a property with no value does not +exist. Some graph providers optionally support storing an explicit `null` as a property value instead, so that the +property continues to exist but with no assigned value. See a provider's documentation or check +`Graph.Features.VertexFeatures#supportsNullPropertyValues()` (and the analogous `Edge`/`VertexProperty` features) to +determine whether this variance is supported. + == Graph Variables `Graph.Variables` are key/value pairs associated with the graph itself -- in essence, a `Map`. These diff --git a/docs/src/reference/the-traversal.asciidoc b/docs/src/reference/the-traversal.asciidoc index b085b4a0848..49b8d96343e 100644 --- a/docs/src/reference/the-traversal.asciidoc +++ b/docs/src/reference/the-traversal.asciidoc @@ -640,6 +640,45 @@ link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gre link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#addV(java.lang.String)++[`addV(String)`], link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#addV(org.apache.tinkerpop.gremlin.process.traversal.Traversal)++[`addV(Traversal)`] +[[addlabel-step]] +=== AddLabel Step + +The `addLabel()`-step (*sideEffect*) adds one or more labels to an element. Because it mutates the set of labels on +an element, `addLabel()` can only be used on a graph configured with a `LabelCardinality` that allows more than one +label, namely `ZERO_OR_MORE` or `ONE_OR_MORE`. + +[gremlin-groovy] +---- +conf = new BaseConfiguration() +conf.setProperty("gremlin.tinkergraph.vertexLabelCardinality", "ZERO_OR_MORE") +graph = TinkerGraph.open(conf) +g = traversal().with(graph) +g.addV('person').property('name','marko').addLabel('employee').labels() +g.V().has('name','marko').hasLabel('person') +g.V().has('name','marko').hasLabel('employee') +g.V().has('name','marko').addLabel('employee').labels() <1> +---- + +<1> marko already carries the "employee" label, so adding it again leaves the set of labels unchanged. + +By contrast, calling `addLabel()` against a vertex whose graph is configured with `LabelCardinality.ONE`, which +fixes a single, immutable label for every vertex, results in an exception since there is no set of labels to +mutate: + +[gremlin-groovy] +---- +conf = new BaseConfiguration() +conf.setProperty("gremlin.tinkergraph.vertexLabelCardinality", "ONE") +graph = TinkerGraph.open(conf) +g = traversal().with(graph) +g.addV('person').property('name','marko').addLabel('employee') +---- + +*Additional References* + +link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#addLabel(java.lang.String,java.lang.String...)++[`addLabel(String, String...)`], +link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#addLabel(org.apache.tinkerpop.gremlin.process.traversal.Traversal,org.apache.tinkerpop.gremlin.process.traversal.Traversal...)++[`addLabel(Traversal, Traversal...)`] + [[aggregate-step]] === [[store-step]]Aggregate Step @@ -1068,6 +1107,29 @@ on a step-by-step level and thus, as discussed in their respective section of th * <>: transform the result of the values in the resulting `Map` using the `by()` modulator. * <>: determine the predicate given the testing of the results of `by()`-modulation. +NOTE: `by(label)` (or the equivalent `by(T.label)`) modulates using the <> step, which for a +multi-label vertex returns only one label chosen non-deterministically. Steps like `group()`, `groupCount()`, +`dedup()`, `order()`, `path()`, `simplePath()`, and `tree()` that modulate `by(label)` will therefore behave +unpredictably on multi-label vertices. + +[gremlin-groovy] +---- +conf = new BaseConfiguration() +conf.setProperty("gremlin.tinkergraph.vertexLabelCardinality", "ZERO_OR_MORE") +graph = TinkerGraph.open(conf) +g = traversal().with(graph) +g.addV('person','employee').property('name','marko') +g.addV('person').property('name','vadas') +g.V().group().by(label).by(values('name').fold()) <1> +g.V().group().by(labels().fold()).by(values('name').fold()) <2> +---- + +<1> marko has both the "person" and "employee" 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 marko under `[person, employee]`, distinct from vadas under +`[person]`, regardless of which order the labels were assigned in. Prefer this pattern with the +<> step over `by(label)` for graphs that support multiple labels per vertex. + *Additional References* link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#by()++[`by()`], @@ -1257,6 +1319,27 @@ g.V().choose(T.label). <1> For person vertices, traverse to the software they created; for software vertices, traverse to the people who created them. +On the "modern" graph, that works reliably because each vertex has exactly one label. A vertex with multiple labels +tells a different story, since `choose(T.label)` still selects a branch based on a single label chosen +non-deterministically from the full set: + +[gremlin-groovy] +---- +conf = new BaseConfiguration() +conf.setProperty("gremlin.tinkergraph.vertexLabelCardinality", "ZERO_OR_MORE") +graph = TinkerGraph.open(conf) +g = traversal().with(graph) +g.addV('person','employee').property('name','marko') +g.V().has('name','marko').choose(T.label). + option('person', constant('routed as a person')). + option('employee', constant('routed as an employee')) <1> +---- + +<1> marko carries both the "person" and "employee" labels, so which `option()` branch is taken depends on which +label the graph happens to return from `label()` and is not guaranteed to be consistent. Filtering with +<> beforehand, or restructuring the choice around `labels()`, gives predictable results for +multi-label vertices. + The `Pick` enum was introduced in an example earlier to handle non-matching scenarios. The following `Pick` options may be used with `choose()`: @@ -1763,6 +1846,44 @@ g.V() * link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#drop()++[`drop()`] +[[droplabel-step]] +=== DropLabel Step + +The `dropLabel()`-step (*sideEffect*) removes one or more specific labels from an element and `dropLabels()` +removes all of them. Like `addLabel()`, both require a `LabelCardinality` of `ZERO_OR_MORE` or `ONE_OR_MORE`. + +[gremlin-groovy] +---- +conf = new BaseConfiguration() +conf.setProperty("gremlin.tinkergraph.vertexLabelCardinality", "ZERO_OR_MORE") +graph = TinkerGraph.open(conf) +g = traversal().with(graph) +v = g.addV('person').property('name','marko').addLabel('employee').addLabel('manager').next() +g.V(v).labels() +g.V(v).dropLabel('manager').labels() <1> +g.V(v).dropLabel('manager').labels() <2> +g.V(v).dropLabels().labels() <3> +g.V(v).labels().count() <4> +---- + +<1> Removes the "manager" label. +<2> Dropping a label that is not present has no effect. +<3> Removes all remaining labels. +<4> Confirms zero labels remain. + +NOTE: With `LabelCardinality.ONE_OR_MORE`, `dropLabels()` always throws and `dropLabel()` throws if it would +leave the element with zero labels. + +IMPORTANT: Do not confuse `dropLabel()` with `labels().drop()`. The standalone `drop()` step either drops an +`Element` from the `Graph`, or a `Property` from it's `Element`. Since a label is simply a `String` object, +the `drop()` step cannot operate on it. + +*Additional References* + +link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#dropLabel(java.lang.String,java.lang.String...)++[`dropLabel(String, String...)`], +link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#dropLabel(org.apache.tinkerpop.gremlin.process.traversal.Traversal,org.apache.tinkerpop.gremlin.process.traversal.Traversal...)++[`dropLabel(Traversal, Traversal...)`], +link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#dropLabels()++[`dropLabels()`] + [[e-step]] === E Step @@ -1830,6 +1951,44 @@ g.V().has('name','marko').properties('location').elementMap() IMPORTANT: The `elementMap()`-step does not return the vertex labels for incident vertices when using `GraphComputer` as the `id` is the only available data to the star graph. +For multi-label graphs, `elementMap()` returns labels as a single string by default. To receive all labels as a +set, use `with("multilabel")` either per-traversal or on a persistent source: + +[gremlin-groovy] +---- +conf = new BaseConfiguration() +conf.setProperty("gremlin.tinkergraph.vertexLabelCardinality", "ZERO_OR_MORE") +graph = TinkerGraph.open(conf) +g = traversal().with(graph) +g.addV('person').property('name','marko').addLabel('employee').addLabel('manager').iterate() +g.V().has('name','marko').elementMap() <1> +g.with("multilabel").V().has('name','marko').elementMap() <2> +gml = g.with("multilabel") +gml.V().has('name','marko').elementMap() <3> +gml.V().has('name','marko').valueMap(true) <4> +---- + +<1> Without `"multilabel"`, the label entry is a single string. +<2> Per-traversal `with("multilabel")` returns all labels as a set. +<3> A persistent source avoids repeating `with("multilabel")` on each traversal. +<4> `valueMap(true)` also respects the `"multilabel"` configuration. + +When a source is configured with `with("multilabel")`, the `with("singlelabel")` option can be used per-traversal to +force single-string label output: + +[gremlin-groovy] +---- +gml = g.with("multilabel") +gml.V().has('name','marko').elementMap() +gml.with("singlelabel").V().has('name','marko').elementMap() +---- + +Since `"multilabel"` and `"singlelabel"` request opposite output formats, configuring both on the same traversal +source is rejected. Attempting to combine them, for example by calling `g.with("multilabel").with("singlelabel")` +will raise a `VerificationException`. The label format is controlled entirely by these source options and is independent of the +graph's `LabelCardinality` setting, so a graph configured for `ZERO_OR_MORE` labels will still return a single +string per element unless `with("multilabel")` is applied. + *Additional References* link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#elementMap(java.lang.String...)++[`elementMap(String...)`] @@ -2134,7 +2293,6 @@ It is possible to filter vertices, edges, and vertex properties based on their p [gremlin-groovy,modern] ---- g.V().hasLabel('person') -g.V().hasLabel('person','name','marko') g.V().hasLabel('person').out().has('name',within('vadas','josh')) g.V().hasLabel('person').out().has('name',within('vadas','josh')). outE().hasLabel('created') @@ -2147,7 +2305,9 @@ g.V().properties().hasKey('age').value() <6> g.V().hasNot('age').values('name') <7> g.V().has('person','name', startingWith('m')) <8> g.V().has(null, 'vadas') <9> +g.V().has('person','name','marko') g.V().has('person', 'name', regex('r')).values('name') <10> +g.V().hasLabel('person', 'software').count() <11> ---- <1> Find all vertices whose ages are between 20 (exclusive) and 30 (exclusive). In other words, the age must be greater than 20 and less than 30. @@ -2161,6 +2321,25 @@ the key,value pairs for those vertices. <8> Find all "person" vertices that have a name property that starts with the letter "m". <9> Property key is always stored as `String` and therefore an equality check with `null` will produce no result. <10> An example of using `has()` with regular expression predicate. +<11> Find all vertices containing the label "person" or "software". + +Chained `hasLabel()` calls requires that a vertex carry all of the specified labels, which matters in cases +where a vertex can have more than one label: + +[gremlin-groovy] +---- +conf = new BaseConfiguration() +conf.setProperty("gremlin.tinkergraph.vertexLabelCardinality", "ZERO_OR_MORE") +graph = TinkerGraph.open(conf) +g = traversal().with(graph) +g.addV('person','employee').property('name','marko') +g.addV('person').property('name','vadas') +g.V().hasLabel('person','employee').values('name') <1> +g.V().hasLabel('person').hasLabel('employee').values('name') <2> +---- + +<1> OR semantics: matches both marko and vadas, since each has at least one of "person" or "employee". +<2> Chaining `hasLabel()` calls requires both labels to be present, matching only marko. *Additional References* @@ -2550,10 +2729,49 @@ g.V(1).outE().label() g.V(1).properties().label() ---- +For elements that carry exactly one label, as in the "modern" graph above, `label()` behaves as expected. A +multi-label vertex complicates matters, since `label()` still returns only a single `String`: + +[gremlin-groovy] +---- +conf = new BaseConfiguration() +conf.setProperty("gremlin.tinkergraph.vertexLabelCardinality", "ZERO_OR_MORE") +graph = TinkerGraph.open(conf) +g = traversal().with(graph) +g.addV('person','employee').property('name','marko') +g.V().has('name','marko').label() <1> +g.V().has('name','marko').labels() <2> +---- + +<1> marko carries both the "person" and "employee" labels, but `label()` returns only one of them, chosen +non-deterministically. +<2> Use the <> step instead to reliably retrieve every label a vertex carries. + *Additional References* link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#label()++[`label()`] +[[labels-step]] +=== Labels Step + +The `labels()`-step (*flatMap*) takes an `Element` and emits each of its labels as a separate traverser. For elements +with a single label this behaves identically to `label()`. For multi-label elements, it emits one traverser per label. + +[gremlin-groovy] +---- +conf = new BaseConfiguration() +conf.setProperty("gremlin.tinkergraph.vertexLabelCardinality", "ZERO_OR_MORE") +graph = TinkerGraph.open(conf) +g = traversal().with(graph) +g.addV('person').property('name','marko').addLabel('employee').iterate() +g.V().labels() +g.V().labels().count() +---- + +*Additional References* + +link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#labels()++[`labels()`] + [[length-step]] === Length Step @@ -3353,6 +3571,38 @@ g.V().hasLabel('Dog').valueMap().with(WithOptions.tokens) NOTE: The example above is written with `gremlin-groovy` and evaluated in Gremlin Console as a Groovy script thus allowing link:https://groovy-lang.org/syntax.html#_maps[Groovy syntax] for initializing a `Map`. +On graphs that support multiple labels per vertex, `T.label` in the `mergeV()` `Map` may be supplied as a single +`String` or as a `Collection` (a `List` or `Set`) to work with multiple labels at once. + +[gremlin-groovy] +---- +conf = new BaseConfiguration() +conf.setProperty("gremlin.tinkergraph.vertexLabelCardinality", "ZERO_OR_MORE") +graph = TinkerGraph.open(conf) +g = traversal().with(graph) +g.mergeV([(T.label):['person','employee'], name:'marko']) <1> +g.V().has('name','marko').labels() +---- + +<1> Create a vertex for marko with both the "person" and "employee" labels. + +IMPORTANT: When `T.label` is used to *search* for a match with a `Collection` of labels, the match uses AND +semantics, meaning a vertex must carry *all* of the specified labels to match, unlike the OR semantics of +<>. When `T.label` is used in `Merge.onCreate` or `Merge.onMatch`, the specified labels are +*added* to the vertex using <> semantics. + +[gremlin-groovy,existing] +---- +g.mergeV([(T.label):['person','employee'], name:'marko']) <1> +g.mergeV([(T.label):'person', name:'marko']). + option(Merge.onMatch,[(T.label):'manager']) <2> +g.V().has('name','marko').labels() +---- + +<1> Matches the vertex created above since it carries both labels (AND semantics). +<2> Matches on the single "person" label and adds "manager" to the vertex's existing labels rather than replacing +them. + If a vertex already exists that matches the map passed to `mergeV()`, the existing vertex will be returned, otherwise a new one will be created. In this way, `mergeV()` provides "get or create" semantics. @@ -5432,6 +5682,28 @@ g.V().hasLabel('person').valueMap('name').with(WithOptions.tokens, WithOptions.l g.V().hasLabel('person').properties('location').valueMap().with(WithOptions.tokens, WithOptions.values) ---- +For multi-label graphs, the label value in the map is a single string by default. Use `with("multilabel")` to +receive all labels as a set: + +[gremlin-groovy] +---- +conf = new BaseConfiguration() +conf.setProperty("gremlin.tinkergraph.vertexLabelCardinality", "ZERO_OR_MORE") +graph = TinkerGraph.open(conf) +g = traversal().with(graph) +g.addV('person').property('name','marko').addLabel('employee').iterate() +g.V().has('name','marko').valueMap(true) <1> +g.with("multilabel").V().has('name','marko').valueMap(true) <2> +---- + +<1> Without `"multilabel"`, the label entry is a single string. +<2> With `"multilabel"`, all labels are returned as a set. + +When a source is configured with `with("multilabel")`, the `with("singlelabel")` option can be used per-traversal to +force single-string label output. Since the two options request opposite output formats, configuring both on the +same traversal source is rejected with a `VerificationException`. As with `elementMap()`, the label format is controlled +entirely by these source options and is independent of the graph's `LabelCardinality` setting. + *Additional References* link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#valueMap(java.lang.String...)++[`valueMap(String...)`] diff --git a/docs/src/tutorials/getting-started/index.asciidoc b/docs/src/tutorials/getting-started/index.asciidoc index 574b4ce7f9e..cd74424064a 100644 --- a/docs/src/tutorials/getting-started/index.asciidoc +++ b/docs/src/tutorials/getting-started/index.asciidoc @@ -205,10 +205,16 @@ their own identifiers and ignore assigned identifiers that you attempt to assign A graph with elements that just have identifiers does not make for much of a database. To give some meaning to this basic structure, vertices and edges can each be given labels to categorize them. +NOTE: An edge always has exactly one label. In the graph used throughout this tutorial, a vertex also has exactly +one label assigned at creation, as shown here, but TinkerPop also allows graph providers to optionally support a +vertex carrying multiple labels or none at all. See the +link:https://tinkerpop.apache.org/docs/x.y.z/reference/#vertex-labels[Vertex Labels] section of the Reference +Documentation for details. + image:modern-edge-1-to-3-2.png[width=300] You can now see that vertex "1" is a "person" and vertex "3" is a "software" vertex. They are joined by a "created" -edge which allows you to see that a "person created software". The "label" and the "id" are reserved attributes of +edge which allows you to see that a "person created software". The `label(s)` and the `id` are reserved attributes of vertices and edges, but you can add your own arbitrary properties as well: image:modern-edge-1-to-3-3.png[width=325] @@ -479,6 +485,12 @@ modulator to `group()` to transform the values. g.V().group().by(label).by('name') ---- +NOTE: `by(label)` groups using the `label()` step, which returns only one label per vertex. This is fine for graphs +like "modern" where every vertex has exactly one label, but for graphs configured to allow multiple labels per +vertex, `by(label)` will only account for one of them. See the +link:https://tinkerpop.apache.org/docs/x.y.z/reference/#by-step[By Step] section of the Reference Documentation for +grouping patterns that work reliably with multi-label vertices. + In this section, you have learned a bit more about what property graphs are and how Gremlin interacts with them. You also learned how to envision Gremlin moving about a graph and how to use some of the more complex, but commonly utilized traversal steps. You are now ready to think about TinkerPop in terms of its wider applicability to diff --git a/docs/src/upgrade/release-4.x.x.asciidoc b/docs/src/upgrade/release-4.x.x.asciidoc index 7bbe5d64383..fe69467a643 100644 --- a/docs/src/upgrade/release-4.x.x.asciidoc +++ b/docs/src/upgrade/release-4.x.x.asciidoc @@ -186,6 +186,93 @@ For full details on the interceptor API for each language variant, refer to the each GLV's documentation in the link:https://tinkerpop.apache.org/docs/x.y.z/reference/#gremlin-drivers-variants[Gremlin Drivers and Variants] reference. +==== Multi-Label Support + +Until now, vertices in the property graph model were limited to a single, immutable label assigned at creation. This +release introduces configurable label cardinality, allowing vertices to carry multiple labels that can be added and +removed over their lifetime. + +The feature is controlled by `LabelCardinality`, a graph-level setting that defaults to `ONE`, preserving the +existing single-label behavior. To enable multi-label in TinkerGraph, set the vertex label cardinality in the graph +properties: + +``` +gremlin.tinkergraph.vertexLabelCardinality=ZERO_OR_MORE +``` + +The three modes are `ONE` (single, immutable, the 3.x default), `ONE_OR_MORE` (mutable, minimum one), and +`ZERO_OR_MORE` (fully flexible). Edge labels remain fixed at `ONE`. + +With multi-label enabled, several new traversal steps become available: + +```text +gremlin> g.addV('person','employee').property('name','marko') +==>v[0] +gremlin> g.V().has('name','marko').addLabel('manager') +==>v[0] +gremlin> g.V().has('name','marko').labels() +==>person +==>employee +==>manager +gremlin> g.V().has('name','marko').dropLabel('employee').labels() +==>person +==>manager +``` + +The `labels()` step is a flatMap that emits one traverser per label, unlike `label()` which returns a single string. +For single-label vertices the two behave identically. + +The `mergeV()` step accepts a list for `T.label` to match or create multi-label vertices. The `onMatch` option uses +append-only semantics: new labels are added, existing labels are preserved: + +```text +gremlin> g.mergeV([(T.label): ['person','employee'], name: 'marko']) +==>v[0] +gremlin> g.mergeV([(T.label): 'person', name: 'marko']).option(Merge.onMatch, [(T.label): 'director']) +==>v[0] +``` + +By default, `elementMap()` and `valueMap()` continue to return labels as a single string. To receive all labels as a +set, use `with("multilabel")` either per-traversal or as a persistent source configuration: + +```text +// assuming a vertex with labels [person, employee, manager] and name "marko" +gremlin> g.with("multilabel").V().has('name','marko').elementMap() +==>{id=0, label=[manager, person, employee], name=marko} +gremlin> g.V().has('name','marko').elementMap() +==>{id=0, label=manager, name=marko} +``` + +To avoid repeating `with("multilabel")` on every traversal, create a persistent source: + +```text +gremlin> gml = g.with("multilabel") +==>graphtraversalsource[tinkergraph[vertices:1 edges:0], standard] +gremlin> gml.V().has('name','marko').elementMap() +==>{id=0, label=[manager, person, employee], name=marko} +gremlin> gml.V().has('name','marko').valueMap(true) +==>{id=0, label=[manager, person, employee], name=[marko]} +``` + +Note the following behavioral details: + +* The deprecated `label()` step returns a single, non-deterministic label when multiple labels are present. Always + use `labels()` to retrieve the full set reliably. +* When a source is configured with `with("multilabel")`, the label output can be forced back to a single string + per-traversal using `with("singlelabel")`. This is useful for providers that enable multi-label output by default + but need an escape hatch for backward compatibility. Note that when both options are present on the same source, + `"singlelabel"` always takes precedence regardless of the order in which they were applied. +* Serialization via GraphSON V1/V2/V3 or Gryo only transmits one label per vertex (using `label()`). Multi-label data + will be silently reduced to a single label when passing through these older formats. Only GraphBinary (V4) and + GraphSON V4 preserve the full label set. +* Adding a label that already exists on the vertex is a no-op (when multi-label is enabled). Dropping a label that + does not exist is also a no-op. With cardinality `ONE`, both `addLabel()` and `dropLabel()` throw since labels are + immutable. +* `hasLabel()` tests the predicate against each label on the element and returns `true` if at least one label + satisfies it. For multi-label vertices, this means `hasLabel(P.neq("person"))` returns `true` when the vertex also + carries other labels, a natural consequence of testing a negation predicate against a set of labels, where the + non-matching labels will satisfy `neq`. To exclude vertices that carry a specific label, use the negation pattern: + `g.V().not(__.hasLabel("person"))`. For single-label vertices the behavior is unchanged. ==== Gremlator @@ -800,6 +887,37 @@ Kryo's default `Map` serializer because it extended `HashMap`, whereas in 4.x it serializer. As a result, Gryo-serialized `Tree` data written by 3.x cannot be read by 4.x and vice versa. The Gryo type registration id (`61`) is unchanged and 4.x-to-4.x Gryo round-trips correctly; this Gryo break is expected for the 4.0.0 major release. +===== Multi-Label Support + +Providers declare their supported label cardinality via `Graph.Features.getLabelCardinality()`, which defaults to +`LabelCardinality.ONE` (single label, immutable, the 3.x behavior). To support multi-label, return `ONE_OR_MORE` or +`ZERO_OR_MORE` from this method. + +The `LabelCardinality` enum exposes `min()`, `max()`, and `supportsMutation()` for programmatic introspection of +constraints. Constraint enforcement is handled by the `LabelCardinalityValidator` utility class, which providers may +use directly or replace with their own validation logic tailored to their storage backend. + +Providers implementing multi-label must: + +- Store and return a `Set` from `Element.labels()` +- Implement `Element.addLabel(String, String...)` and `Element.dropLabel(String, String...)` +- Ensure `hasLabel("a", "b")` uses OR semantics (matches vertices with label "a" or "b") +- Serialize/deserialize the label set via the V4 GraphBinary format (which sends labels as a list) + +The default `elementMap()` and `valueMap()` implementations determine label output format solely from the +`with("multilabel")` source option, not from the graph's `LabelCardinality`. This means a multi-label graph still +returns a single label string from these steps unless `with("multilabel")` is explicitly configured. The intent is +to eventually deprecate the single-string path entirely and always return labels as a set, aligning `elementMap()` +and `valueMap()` with `labels()`. Until then, `with("singlelabel")` allows users to override a source-level +`with("multilabel")` back to single-string output. + +Providers who want `elementMap()`/`valueMap()` to return the full label set by default (without requiring users to +set `with("multilabel")`) should override `PropertyMapStep` and `ElementMapStep` to tie label output to their +server-side label cardinality configuration. In that case, `with("singlelabel")` should still be respected as an +explicit user override back to single-string output. + +Older serialization formats (GraphSON V1/V2/V3, Gryo) only support a single label and will silently use the value +from `Element.label()` (the deprecated single-label accessor). ==== Graph Driver Providers diff --git a/gremlin-annotations/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/GremlinDsl.java b/gremlin-annotations/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/GremlinDsl.java index b03153a9464..1d84d8d8e93 100644 --- a/gremlin-annotations/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/GremlinDsl.java +++ b/gremlin-annotations/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/GremlinDsl.java @@ -68,7 +68,7 @@ * *