This is an automated email from the ASF dual-hosted git repository.
Cole-Greer pushed a commit to branch 3.8-dev
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
The following commit(s) were added to refs/heads/3.8-dev by this push:
new 4a45cf2cea TINKERPOP-3187 Add propertyMap() helper to Element in GLVs
(#3550)
4a45cf2cea is described below
commit 4a45cf2cea7158bcbccbe07d35480c20ffa64bbf
Author: Guian Gumpac <[email protected]>
AuthorDate: Thu Jul 23 18:30:45 2026 -0700
TINKERPOP-3187 Add propertyMap() helper to Element in GLVs (#3550)
Adds a propertyMap() convenience helper that views an element's properties
as a
map keyed by property key (key -> [property objects]), complementing the
flat
properties list. The helper is provided on all element types (Vertex, Edge,
VertexProperty) in the four GLVs and on the Element structure API in
gremlin-core. Also fixes a latent gremlin-go GraphBinary deserialization bug
where VertexProperty.Key was never populated.
Assisted-by: Kiro: Claude Opus 4.8
---
CHANGELOG.asciidoc | 2 +
docs/src/upgrade/release-3.8.2.asciidoc | 28 +++++
.../tinkerpop/gremlin/structure/Element.java | 23 ++++
gremlin-dotnet/src/Gremlin.Net/Structure/Edge.cs | 22 +++-
gremlin-dotnet/src/Gremlin.Net/Structure/Vertex.cs | 19 ++++
.../src/Gremlin.Net/Structure/VertexProperty.cs | 22 +++-
.../Gremlin.Net.UnitTest/Structure/EdgeTests.cs | 43 ++++++-
.../Structure/VertexPropertyTests.cs | 43 ++++++-
.../Gremlin.Net.UnitTest/Structure/VertexTests.cs | 41 +++++++
gremlin-go/driver/graph.go | 54 +++++++++
gremlin-go/driver/graphBinary.go | 1 +
gremlin-go/driver/graphBinary_test.go | 31 ++++++
gremlin-go/driver/graph_test.go | 123 +++++++++++++++++++++
.../gremlin-javascript/lib/structure/graph.js | 13 +++
.../test/unit/structure-types-test.js | 66 +++++++++++
.../main/python/gremlin_python/structure/graph.py | 12 ++
.../main/python/tests/unit/structure/test_graph.py | 52 +++++++++
.../tinkergraph/structure/TinkerGraphTest.java | 77 +++++++++++++
18 files changed, 668 insertions(+), 4 deletions(-)
diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc
index 6a0924aba6..d01a9fc15d 100644
--- a/CHANGELOG.asciidoc
+++ b/CHANGELOG.asciidoc
@@ -33,6 +33,8 @@ This release also includes changes from prior 3.7.x releases.
* Add missing `Configuring` interface to `GraphStepPlaceholder` and
`VertexStepPlaceholder`
* Fixed bug in `group()` value traversal where keys were retained with stale
barrier state instead of being filtered when steps following a `Barrier` in the
second `by()` produced no output (e.g. `by(values("age").fold().unfold())` or
`by(__.out().fold().count(local).is(P.gt(0)))` for vertices with no out-edges).
* Corrected numerous inaccuracies in the reference documentation, including
wrong default values (connection pool sizes, buffer sizes, ports, timeouts),
stale serializer class names, removed options documented as available, and
broken code examples across the JVM, Python, `.NET`, Go, and JavaScript drivers.
+* Added a `propertyMap()` helper to view an element's properties as a map
keyed by property key, on the `Element` structure API in `gremlin-core`
(inherited by `Vertex`, `Edge`, and `VertexProperty`) and on `Vertex`, `Edge`,
and `VertexProperty` in `gremlin-javascript`, `gremlin-python`,
`gremlin-dotnet`, and `gremlin-go`.
+* Fixed a bug in `gremlin-go` where a `VertexProperty` deserialized from
GraphBinary did not have its `Key` field populated, causing
`Vertex.PropertyMap()` to group properties under an empty key.
[[release-3-8-1]]
=== TinkerPop 3.8.1 (Release Date: April 1, 2026)
diff --git a/docs/src/upgrade/release-3.8.2.asciidoc
b/docs/src/upgrade/release-3.8.2.asciidoc
index a3126f2f9d..e602fa0335 100644
--- a/docs/src/upgrade/release-3.8.2.asciidoc
+++ b/docs/src/upgrade/release-3.8.2.asciidoc
@@ -48,3 +48,31 @@ constructor that it accesses reflectively. Java 21 support
arrived in Spark 4.0
those releases require Java 17 as a minimum and Scala 2.13, which would drop
Java 11 support for that module.
`spark-gremlin` therefore continues to target Spark 3.3.x and is built and
tested with Java 11 and Java 17 only; it is
excluded from the Java 21 and Java 25 builds.
+
+==== propertyMap() Element Helper
+
+The Gremlin Language Variants now offer a helper on all element types
(`Vertex`, `Edge`, and `VertexProperty`) that
+views an element's properties as a map keyed by property key, complementing
the flat list returned by `properties`.
+`propertyMap()` (`gremlin-javascript`), `PropertyMap()` (`gremlin-dotnet` and
`gremlin-go`), and `property_map()`
+(`gremlin-python`) each return a map of property key to the list of that key's
property objects. The default flat-list
+representation of `properties` is unchanged.
+
+[source,javascript]
+----
+// default flat list
+v.properties // [ VertexProperty{key:'name',...},
VertexProperty{key:'age',...} ]
+// grouped map view
+v.propertyMap() // { name: [ VertexProperty{...} ], age: [
VertexProperty{...} ] }
+----
+
+The JVM structure API now also exposes `Element.propertyMap()` (inherited by
`Vertex`, `Edge`, and `VertexProperty`)
+as a default method in `gremlin-core`, complementing the GLV helpers.
+
+[source,java]
+----
+// gremlin-core structure API (Vertex, Edge, VertexProperty)
+Map<String, List<Property<Object>>> propertyMap = vertex.propertyMap();
+// { name=[vp[name->marko]], age=[vp[age->29]] }
+----
+
+See: link:https://issues.apache.org/jira/browse/TINKERPOP-3187[TINKERPOP-3187]
diff --git
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/Element.java
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/Element.java
index f4412123b7..d6bc97fbc6 100644
---
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/Element.java
+++
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/Element.java
@@ -20,9 +20,13 @@ package org.apache.tinkerpop.gremlin.structure;
import org.apache.tinkerpop.gremlin.util.iterator.IteratorUtils;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
@@ -114,6 +118,25 @@ public abstract interface Element {
public <V> Iterator<? extends Property<V>> properties(final String...
propertyKeys);
+ /**
+ * Groups this element's properties by key, preserving encounter order. A
{@link Vertex} multi-property key
+ * maps to several {@link Property} objects; otherwise each key maps to a
single-element list. Empty map when
+ * there are no matching properties.
+ *
+ * @param propertyKeys the keys to filter on; if none are provided then
all properties are grouped
+ * @return a map of property key to the list of properties for that key
+ */
+ public default <V> Map<String, List<Property<V>>> propertyMap(final
String... propertyKeys) {
+ final Map<String, List<Property<V>>> propertyMap = new
LinkedHashMap<>();
+ final Iterator<? extends Property<V>> iterator =
this.properties(propertyKeys);
+ while (iterator.hasNext()) {
+ final Property<V> property = iterator.next();
+ propertyMap.computeIfAbsent(property.key(), k -> new
ArrayList<>()).add(property);
+ }
+ return propertyMap;
+ }
+
+
/**
* Common exceptions to use with an element.
*/
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/Edge.cs
b/gremlin-dotnet/src/Gremlin.Net/Structure/Edge.cs
index 63461fa4b2..396fb56681 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Structure/Edge.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/Edge.cs
@@ -21,6 +21,7 @@
#endregion
+using System.Collections.Generic;
using System.Linq;
namespace Gremlin.Net.Structure
@@ -64,10 +65,29 @@ namespace Gremlin.Net.Structure
return Properties?.Cast<Property>().FirstOrDefault(p => p.Key ==
key);
}
+ /// <summary>
+ /// Groups this edge's properties by key.
+ /// </summary>
+ /// <returns>A map of property key to the list of Property objects for
that key (empty when none).</returns>
+ public Dictionary<string, List<Property>> PropertyMap()
+ {
+ var map = new Dictionary<string, List<Property>>();
+ foreach (var p in Properties.Cast<Property>())
+ {
+ if (!map.TryGetValue(p.Key, out var list))
+ {
+ list = new List<Property>();
+ map[p.Key] = list;
+ }
+ list.Add(p);
+ }
+ return map;
+ }
+
/// <inheritdoc />
public override string ToString()
{
return $"e[{Id}][{OutV.Id}-{Label}->{InV.Id}]";
}
}
-}
\ No newline at end of file
+}
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/Vertex.cs
b/gremlin-dotnet/src/Gremlin.Net/Structure/Vertex.cs
index 6f56102ac0..2a9f423aa5 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Structure/Vertex.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/Vertex.cs
@@ -56,6 +56,25 @@ namespace Gremlin.Net.Structure
return Properties?.Cast<VertexProperty>().FirstOrDefault(p =>
p.Key == key);
}
+ /// <summary>
+ /// Groups this vertex's properties by key.
+ /// </summary>
+ /// <returns>A map of property key to the list of VertexProperty
objects for that key (empty when none).</returns>
+ public Dictionary<string, List<VertexProperty>> PropertyMap()
+ {
+ var map = new Dictionary<string, List<VertexProperty>>();
+ foreach (var p in Properties.Cast<VertexProperty>())
+ {
+ if (!map.TryGetValue(p.Key, out var list))
+ {
+ list = new List<VertexProperty>();
+ map[p.Key] = list;
+ }
+ list.Add(p);
+ }
+ return map;
+ }
+
/// <inheritdoc />
public override string ToString()
{
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/VertexProperty.cs
b/gremlin-dotnet/src/Gremlin.Net/Structure/VertexProperty.cs
index 001a3c3bc8..5911de715b 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Structure/VertexProperty.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/VertexProperty.cs
@@ -21,6 +21,7 @@
#endregion
+using System.Collections.Generic;
using System.Linq;
namespace Gremlin.Net.Structure
@@ -69,10 +70,29 @@ namespace Gremlin.Net.Structure
return Properties?.Cast<Property>().FirstOrDefault(p => p.Key ==
key);
}
+ /// <summary>
+ /// Groups this vertex property's properties by key.
+ /// </summary>
+ /// <returns>A map of property key to the list of Property objects for
that key (empty when none).</returns>
+ public Dictionary<string, List<Property>> PropertyMap()
+ {
+ var map = new Dictionary<string, List<Property>>();
+ foreach (var p in Properties.Cast<Property>())
+ {
+ if (!map.TryGetValue(p.Key, out var list))
+ {
+ list = new List<Property>();
+ map[p.Key] = list;
+ }
+ list.Add(p);
+ }
+ return map;
+ }
+
/// <inheritdoc />
public override string ToString()
{
return $"vp[{Label}->{Value}]";
}
}
-}
\ No newline at end of file
+}
diff --git a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/EdgeTests.cs
b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/EdgeTests.cs
index a6b4dbd828..5825e29f47 100644
--- a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/EdgeTests.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/EdgeTests.cs
@@ -21,6 +21,7 @@
#endregion
+using System.Collections.Generic;
using Gremlin.Net.Structure;
using Xunit;
@@ -53,5 +54,45 @@ namespace Gremlin.Net.UnitTest.Structure
Assert.Equal("e[2][1-said->hello]", edgeStr);
}
+
+ [Fact]
+ public void ShouldGroupMultiPropertiesUnderOneKeyForPropertyMap()
+ {
+ var first = new Property("name", "marko");
+ var second = new Property("name", "marko a. rodriguez");
+ var edge = new Edge(2, new Vertex(1), "said", new Vertex("hello",
"phrase"),
+ new dynamic[] {first, second});
+
+ var propertyMap = edge.PropertyMap();
+
+ Assert.Single(propertyMap);
+ Assert.True(propertyMap.ContainsKey("name"));
+ Assert.Equal(new List<Property> {first, second},
propertyMap["name"]);
+ }
+
+ [Fact]
+ public void
ShouldGroupSingleValuedPropertiesIntoSingleElementListsForPropertyMap()
+ {
+ var since = new Property("since", 2009);
+ var weight = new Property("weight", 0.5);
+ var edge = new Edge(2, new Vertex(1), "said", new Vertex("hello",
"phrase"),
+ new dynamic[] {since, weight});
+
+ var propertyMap = edge.PropertyMap();
+
+ Assert.Equal(2, propertyMap.Count);
+ Assert.Equal(new List<Property> {since}, propertyMap["since"]);
+ Assert.Equal(new List<Property> {weight}, propertyMap["weight"]);
+ }
+
+ [Fact]
+ public void ShouldReturnEmptyDictionaryForPropertyMapWhenNoProperties()
+ {
+ var edge = new Edge(2, new Vertex(1), "said", new Vertex("hello",
"phrase"));
+
+ var propertyMap = edge.PropertyMap();
+
+ Assert.Empty(propertyMap);
+ }
}
-}
\ No newline at end of file
+}
diff --git
a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/VertexPropertyTests.cs
b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/VertexPropertyTests.cs
index 26863cd4be..dc305ac361 100644
--- a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/VertexPropertyTests.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/VertexPropertyTests.cs
@@ -21,6 +21,7 @@
#endregion
+using System.Collections.Generic;
using Gremlin.Net.Structure;
using Xunit;
@@ -65,5 +66,45 @@ namespace Gremlin.Net.UnitTest.Structure
Assert.Equal("vp[name->marko]", stringRepresentation);
}
+
+ [Fact]
+ public void ShouldGroupMultiPropertiesUnderOneKeyForPropertyMap()
+ {
+ var first = new Property("startTime", 2009);
+ var second = new Property("startTime", 2010);
+ var vertexProperty = new VertexProperty((long) 24, "name",
"marko", new Vertex(1),
+ new dynamic[] {first, second});
+
+ var propertyMap = vertexProperty.PropertyMap();
+
+ Assert.Single(propertyMap);
+ Assert.True(propertyMap.ContainsKey("startTime"));
+ Assert.Equal(new List<Property> {first, second},
propertyMap["startTime"]);
+ }
+
+ [Fact]
+ public void
ShouldGroupSingleValuedPropertiesIntoSingleElementListsForPropertyMap()
+ {
+ var startTime = new Property("startTime", 2009);
+ var endTime = new Property("endTime", 2010);
+ var vertexProperty = new VertexProperty((long) 24, "name",
"marko", new Vertex(1),
+ new dynamic[] {startTime, endTime});
+
+ var propertyMap = vertexProperty.PropertyMap();
+
+ Assert.Equal(2, propertyMap.Count);
+ Assert.Equal(new List<Property> {startTime},
propertyMap["startTime"]);
+ Assert.Equal(new List<Property> {endTime}, propertyMap["endTime"]);
+ }
+
+ [Fact]
+ public void ShouldReturnEmptyDictionaryForPropertyMapWhenNoProperties()
+ {
+ var vertexProperty = new VertexProperty((long) 24, "name",
"marko", new Vertex(1));
+
+ var propertyMap = vertexProperty.PropertyMap();
+
+ Assert.Empty(propertyMap);
+ }
}
-}
\ No newline at end of file
+}
diff --git a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/VertexTests.cs
b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/VertexTests.cs
index 3c284e6782..49bc854058 100644
--- a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/VertexTests.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/VertexTests.cs
@@ -21,6 +21,7 @@
#endregion
+using System.Collections.Generic;
using Gremlin.Net.Structure;
using Xunit;
@@ -76,5 +77,45 @@ namespace Gremlin.Net.UnitTest.Structure
Assert.Equal("vertex", vertex.Label);
}
+
+ [Fact]
+ public void ShouldGroupMultiPropertiesUnderOneKeyForPropertyMap()
+ {
+ var vertex = new Vertex(1);
+ var first = new VertexProperty((long) 0, "name", "marko", vertex);
+ var second = new VertexProperty((long) 1, "name", "marko a.
rodriguez", vertex);
+ var vertexWithProperties = new Vertex(1, "person", new dynamic[]
{first, second});
+
+ var propertyMap = vertexWithProperties.PropertyMap();
+
+ Assert.Single(propertyMap);
+ Assert.True(propertyMap.ContainsKey("name"));
+ Assert.Equal(new List<VertexProperty> {first, second},
propertyMap["name"]);
+ }
+
+ [Fact]
+ public void
ShouldGroupSingleValuedPropertiesIntoSingleElementListsForPropertyMap()
+ {
+ var vertex = new Vertex(1);
+ var name = new VertexProperty((long) 0, "name", "marko", vertex);
+ var age = new VertexProperty((long) 1, "age", 29, vertex);
+ var vertexWithProperties = new Vertex(1, "person", new dynamic[]
{name, age});
+
+ var propertyMap = vertexWithProperties.PropertyMap();
+
+ Assert.Equal(2, propertyMap.Count);
+ Assert.Equal(new List<VertexProperty> {name}, propertyMap["name"]);
+ Assert.Equal(new List<VertexProperty> {age}, propertyMap["age"]);
+ }
+
+ [Fact]
+ public void ShouldReturnEmptyDictionaryForPropertyMapWhenNoProperties()
+ {
+ var vertex = new Vertex(1);
+
+ var propertyMap = vertex.PropertyMap();
+
+ Assert.Empty(propertyMap);
+ }
}
}
\ No newline at end of file
diff --git a/gremlin-go/driver/graph.go b/gremlin-go/driver/graph.go
index a59b41e9bc..3ad7aace52 100644
--- a/gremlin-go/driver/graph.go
+++ b/gremlin-go/driver/graph.go
@@ -78,16 +78,70 @@ func (v *Vertex) String() string {
return fmt.Sprintf("v[%v]", v.Id)
}
+// PropertyMap groups the properties of this Vertex by their Key. The
Properties field is expected to hold a
+// []interface{} of *VertexProperty as produced during deserialization.
Multi-valued properties are returned as
+// multiple entries in the slice associated with a Key. An empty (non-nil) map
is returned when there are no
+// properties or when Properties is not of the expected type.
+func (v *Vertex) PropertyMap() map[string][]*VertexProperty {
+ propertyMap := make(map[string][]*VertexProperty)
+ properties, ok := v.Properties.([]interface{})
+ if !ok {
+ return propertyMap
+ }
+ for _, p := range properties {
+ if vp, ok := p.(*VertexProperty); ok {
+ propertyMap[vp.Key] = append(propertyMap[vp.Key], vp)
+ }
+ }
+ return propertyMap
+}
+
// String returns the string representation of the edge.
func (e *Edge) String() string {
return fmt.Sprintf("e[%v][%v-%s->%v]", e.Id, e.OutV.Id, e.Label,
e.InV.Id)
}
+// PropertyMap groups the properties of this Edge by their Key. The Properties
field is expected to hold a
+// []interface{} of *Property as produced during deserialization. Multi-valued
properties are returned as
+// multiple entries in the slice associated with a Key. An empty (non-nil) map
is returned when there are no
+// properties or when Properties is not of the expected type.
+func (e *Edge) PropertyMap() map[string][]*Property {
+ propertyMap := make(map[string][]*Property)
+ properties, ok := e.Properties.([]interface{})
+ if !ok {
+ return propertyMap
+ }
+ for _, p := range properties {
+ if prop, ok := p.(*Property); ok {
+ propertyMap[prop.Key] = append(propertyMap[prop.Key],
prop)
+ }
+ }
+ return propertyMap
+}
+
// String returns the string representation of the vertex property.
func (vp *VertexProperty) String() string {
return fmt.Sprintf("vp[%s->%v]", vp.Label, vp.Value)
}
+// PropertyMap groups the meta-properties of this VertexProperty by their Key.
The Properties field is expected to
+// hold a []interface{} of *Property as produced during deserialization.
Multi-valued properties are returned as
+// multiple entries in the slice associated with a Key. An empty (non-nil) map
is returned when there are no
+// properties or when Properties is not of the expected type.
+func (vp *VertexProperty) PropertyMap() map[string][]*Property {
+ propertyMap := make(map[string][]*Property)
+ properties, ok := vp.Properties.([]interface{})
+ if !ok {
+ return propertyMap
+ }
+ for _, p := range properties {
+ if prop, ok := p.(*Property); ok {
+ propertyMap[prop.Key] = append(propertyMap[prop.Key],
prop)
+ }
+ }
+ return propertyMap
+}
+
// String returns the string representation of the property.
func (p *Property) String() string {
return fmt.Sprintf("p[%s->%v]", p.Key, p.Value)
diff --git a/gremlin-go/driver/graphBinary.go b/gremlin-go/driver/graphBinary.go
index b760cecf05..63d7344cc8 100644
--- a/gremlin-go/driver/graphBinary.go
+++ b/gremlin-go/driver/graphBinary.go
@@ -1278,6 +1278,7 @@ func vertexPropertyReader(data *[]byte, i *int)
(interface{}, error) {
return nil, err
}
vp.Label = label.(string)
+ vp.Key = label.(string)
vp.Value, err = readFullyQualifiedNullable(data, i, true)
if err != nil {
return nil, err
diff --git a/gremlin-go/driver/graphBinary_test.go
b/gremlin-go/driver/graphBinary_test.go
index 44da7802b1..ff414bd1d2 100644
--- a/gremlin-go/driver/graphBinary_test.go
+++ b/gremlin-go/driver/graphBinary_test.go
@@ -385,3 +385,34 @@ func TestGraphBinaryV1(t *testing.T) {
})
})
}
+
+func TestVertexPropertyReaderSetsKey(t *testing.T) {
+ serializer :=
&graphBinaryTypeSerializer{newLogHandler(&defaultLogger{}, Error,
language.English)}
+
+ source := &VertexProperty{
+ Element: Element{Id: int32(1), Label: "name"},
+ Value: "marko",
+ }
+
+ var buffer bytes.Buffer
+ buf, err := vertexPropertyWriter(source, &buffer, serializer)
+ assert.Nil(t, err)
+
+ pos := 0
+ res, err := vertexPropertyReader(&buf, &pos)
+ assert.Nil(t, err)
+
+ vp, ok := res.(*VertexProperty)
+ assert.True(t, ok)
+ assert.Equal(t, "name", vp.Label)
+ assert.Equal(t, "name", vp.Key)
+
+ // Confirm PropertyMap grouping is keyed correctly rather than
collapsing under the empty key.
+ v := &Vertex{Element{Id: int32(1), Label: "person", Properties:
[]interface{}{vp}}}
+ propertyMap := v.PropertyMap()
+ assert.Equal(t, 1, len(propertyMap))
+ assert.Equal(t, 1, len(propertyMap["name"]))
+ assert.Equal(t, vp, propertyMap["name"][0])
+ _, emptyKeyExists := propertyMap[""]
+ assert.False(t, emptyKeyExists)
+}
diff --git a/gremlin-go/driver/graph_test.go b/gremlin-go/driver/graph_test.go
index 0890e01aac..6522e579ce 100644
--- a/gremlin-go/driver/graph_test.go
+++ b/gremlin-go/driver/graph_test.go
@@ -177,3 +177,126 @@ func TestCustomStructs(t *testing.T) {
})
})
}
+
+func TestVertexPropertyMap(t *testing.T) {
+ t.Run("Test Vertex.PropertyMap() groups multi-valued properties by
key", func(t *testing.T) {
+ v := &Vertex{Element{1, "person", []interface{}{
+ &VertexProperty{Element{10, "name", nil}, "name",
"marko", Vertex{}},
+ &VertexProperty{Element{11, "name", nil}, "name",
"marko-a-polo", Vertex{}},
+ &VertexProperty{Element{12, "age", nil}, "age", 29,
Vertex{}},
+ }}}
+ propertyMap := v.PropertyMap()
+ assert.Equal(t, 2, len(propertyMap))
+ assert.Equal(t, 2, len(propertyMap["name"]))
+ assert.Equal(t, "marko", propertyMap["name"][0].Value)
+ assert.Equal(t, "marko-a-polo", propertyMap["name"][1].Value)
+ assert.Equal(t, 1, len(propertyMap["age"]))
+ assert.Equal(t, 29, propertyMap["age"][0].Value)
+ })
+
+ t.Run("Test Vertex.PropertyMap() single-valued property returns a
one-element slice", func(t *testing.T) {
+ v := &Vertex{Element{1, "person", []interface{}{
+ &VertexProperty{Element{10, "name", nil}, "name",
"marko", Vertex{}},
+ }}}
+ propertyMap := v.PropertyMap()
+ assert.Equal(t, 1, len(propertyMap))
+ assert.Equal(t, 1, len(propertyMap["name"]))
+ assert.Equal(t, "marko", propertyMap["name"][0].Value)
+ })
+
+ t.Run("Test Vertex.PropertyMap() with nil Properties returns empty
map", func(t *testing.T) {
+ v := &Vertex{Element{1, "person", nil}}
+ propertyMap := v.PropertyMap()
+ assert.NotNil(t, propertyMap)
+ assert.Equal(t, 0, len(propertyMap))
+ })
+
+ t.Run("Test Vertex.PropertyMap() with empty Properties slice returns
empty map", func(t *testing.T) {
+ v := &Vertex{Element{1, "person", []interface{}{}}}
+ propertyMap := v.PropertyMap()
+ assert.NotNil(t, propertyMap)
+ assert.Equal(t, 0, len(propertyMap))
+ })
+}
+
+func TestEdgePropertyMap(t *testing.T) {
+ t.Run("Test Edge.PropertyMap() groups multi-valued properties by key",
func(t *testing.T) {
+ e := &Edge{Element{1, "created", []interface{}{
+ &Property{"weight", 0.4, Element{}},
+ &Property{"weight", 0.6, Element{}},
+ &Property{"since", 2010, Element{}},
+ }}, Vertex{}, Vertex{}}
+ propertyMap := e.PropertyMap()
+ assert.Equal(t, 2, len(propertyMap))
+ assert.Equal(t, 2, len(propertyMap["weight"]))
+ assert.Equal(t, 0.4, propertyMap["weight"][0].Value)
+ assert.Equal(t, 0.6, propertyMap["weight"][1].Value)
+ assert.Equal(t, 1, len(propertyMap["since"]))
+ assert.Equal(t, 2010, propertyMap["since"][0].Value)
+ })
+
+ t.Run("Test Edge.PropertyMap() single-valued property returns a
one-element slice", func(t *testing.T) {
+ e := &Edge{Element{1, "created", []interface{}{
+ &Property{"weight", 0.4, Element{}},
+ }}, Vertex{}, Vertex{}}
+ propertyMap := e.PropertyMap()
+ assert.Equal(t, 1, len(propertyMap))
+ assert.Equal(t, 1, len(propertyMap["weight"]))
+ assert.Equal(t, 0.4, propertyMap["weight"][0].Value)
+ })
+
+ t.Run("Test Edge.PropertyMap() with nil Properties returns empty map",
func(t *testing.T) {
+ e := &Edge{Element{1, "created", nil}, Vertex{}, Vertex{}}
+ propertyMap := e.PropertyMap()
+ assert.NotNil(t, propertyMap)
+ assert.Equal(t, 0, len(propertyMap))
+ })
+
+ t.Run("Test Edge.PropertyMap() with empty Properties slice returns
empty map", func(t *testing.T) {
+ e := &Edge{Element{1, "created", []interface{}{}}, Vertex{},
Vertex{}}
+ propertyMap := e.PropertyMap()
+ assert.NotNil(t, propertyMap)
+ assert.Equal(t, 0, len(propertyMap))
+ })
+}
+
+func TestVertexPropertyMetaPropertyMap(t *testing.T) {
+ t.Run("Test VertexProperty.PropertyMap() groups multi-valued properties
by key", func(t *testing.T) {
+ vp := &VertexProperty{Element{1, "name", []interface{}{
+ &Property{"acl", "public", Element{}},
+ &Property{"acl", "private", Element{}},
+ &Property{"startTime", 2010, Element{}},
+ }}, "name", "marko", Vertex{}}
+ propertyMap := vp.PropertyMap()
+ assert.Equal(t, 2, len(propertyMap))
+ assert.Equal(t, 2, len(propertyMap["acl"]))
+ assert.Equal(t, "public", propertyMap["acl"][0].Value)
+ assert.Equal(t, "private", propertyMap["acl"][1].Value)
+ assert.Equal(t, 1, len(propertyMap["startTime"]))
+ assert.Equal(t, 2010, propertyMap["startTime"][0].Value)
+ })
+
+ t.Run("Test VertexProperty.PropertyMap() single-valued property returns
a one-element slice", func(t *testing.T) {
+ vp := &VertexProperty{Element{1, "name", []interface{}{
+ &Property{"acl", "public", Element{}},
+ }}, "name", "marko", Vertex{}}
+ propertyMap := vp.PropertyMap()
+ assert.Equal(t, 1, len(propertyMap))
+ assert.Equal(t, 1, len(propertyMap["acl"]))
+ assert.Equal(t, "public", propertyMap["acl"][0].Value)
+ })
+
+ t.Run("Test VertexProperty.PropertyMap() with nil Properties returns
empty map", func(t *testing.T) {
+ vp := &VertexProperty{Element{1, "name", nil}, "name", "marko",
Vertex{}}
+ propertyMap := vp.PropertyMap()
+ assert.NotNil(t, propertyMap)
+ assert.Equal(t, 0, len(propertyMap))
+ })
+
+ t.Run("Test VertexProperty.PropertyMap() with empty Properties slice
returns empty map", func(t *testing.T) {
+ vp := &VertexProperty{Element{1, "name", []interface{}{}},
"name", "marko", Vertex{}}
+ propertyMap := vp.PropertyMap()
+ assert.NotNil(t, propertyMap)
+ assert.Equal(t, 0, len(propertyMap))
+ })
+}
diff --git
a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/structure/graph.js
b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/structure/graph.js
index 46f4dd14d7..3aceac066e 100644
---
a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/structure/graph.js
+++
b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/structure/graph.js
@@ -61,6 +61,19 @@ class Element {
equals(other) {
return other instanceof Element && this.id === other.id;
}
+
+ /**
+ * Groups this element's properties by key.
+ * @returns {Object<string, Object[]>} a map of property key to the array of
+ * property objects for that key (empty object when there are no
properties).
+ */
+ propertyMap() {
+ const map = Object.create(null);
+ for (const p of this.properties) {
+ (map[p.key] = map[p.key] || []).push(p);
+ }
+ return map;
+ }
}
class Vertex extends Element {
diff --git
a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/unit/structure-types-test.js
b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/unit/structure-types-test.js
index ca972512ca..22c6730431 100644
---
a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/unit/structure-types-test.js
+++
b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/unit/structure-types-test.js
@@ -44,6 +44,25 @@ describe('Edge', () => {
assert.deepStrictEqual(edge.properties, []);
});
});
+
+ describe('#propertyMap()', () => {
+ it('should group properties by key into single-element arrays', () => {
+ const since = new Property('since', 2009);
+ const weight = new Property('weight', 0.5);
+ const edge = new Edge('123', new Vertex(1), 'knows', new Vertex(2),
[since, weight]);
+ const pm = edge.propertyMap();
+ // propertyMap() returns a null-prototype object, so compare by
keys/values
+ // instead of deep-equal against a plain {} literal.
+ assert.deepStrictEqual(Object.keys(pm), ['since', 'weight']);
+ assert.deepStrictEqual(pm.since, [since]);
+ assert.deepStrictEqual(pm.weight, [weight]);
+ });
+
+ it('should return an empty object when there are no properties', () => {
+ const edge = new Edge('123', new Vertex(1), 'knows', new Vertex(2));
+ assert.deepStrictEqual(Object.keys(edge.propertyMap()), []);
+ });
+ });
});
describe('Vertex', () => {
@@ -70,6 +89,34 @@ describe('Vertex', () => {
assert.deepStrictEqual(vertex.properties, []);
});
});
+
+ describe('#propertyMap()', () => {
+ it('should group multi-properties of the same key into an array', () => {
+ const nameA = new VertexProperty(0, 'name', 'marko');
+ const nameB = new VertexProperty(1, 'name', 'marko a. rodriguez');
+ const vertex = new Vertex(1, 'person', [nameA, nameB]);
+ const pm = vertex.propertyMap();
+ // propertyMap() returns a null-prototype object, so compare by
keys/values
+ // instead of deep-equal against a plain {} literal.
+ assert.deepStrictEqual(Object.keys(pm), ['name']);
+ assert.deepStrictEqual(pm.name, [nameA, nameB]);
+ });
+
+ it('should give single-element arrays for single-valued properties', () =>
{
+ const name = new VertexProperty(0, 'name', 'marko');
+ const age = new VertexProperty(1, 'age', 29);
+ const vertex = new Vertex(1, 'person', [name, age]);
+ const pm = vertex.propertyMap();
+ assert.deepStrictEqual(Object.keys(pm), ['name', 'age']);
+ assert.deepStrictEqual(pm.name, [name]);
+ assert.deepStrictEqual(pm.age, [age]);
+ });
+
+ it('should return an empty object when there are no properties', () => {
+ const vertex = new Vertex(1, 'person');
+ assert.deepStrictEqual(Object.keys(vertex.propertyMap()), []);
+ });
+ });
});
describe('VertexProperty', () => {
@@ -97,6 +144,25 @@ describe('VertexProperty', () => {
assert.deepStrictEqual(vp.properties, []);
});
});
+
+ describe('#propertyMap()', () => {
+ it('should group properties by key into single-element arrays', () => {
+ const startTime = new Property('startTime', 2001);
+ const endTime = new Property('endTime', 2004);
+ const vp = new VertexProperty(24, 'name', 'marko', [startTime, endTime]);
+ const pm = vp.propertyMap();
+ // propertyMap() returns a null-prototype object, so compare by
keys/values
+ // instead of deep-equal against a plain {} literal.
+ assert.deepStrictEqual(Object.keys(pm), ['startTime', 'endTime']);
+ assert.deepStrictEqual(pm.startTime, [startTime]);
+ assert.deepStrictEqual(pm.endTime, [endTime]);
+ });
+
+ it('should return an empty object when there are no properties', () => {
+ const vp = new VertexProperty(24, 'name', 'marko');
+ assert.deepStrictEqual(Object.keys(vp.propertyMap()), []);
+ });
+ });
});
describe('Property', () => {
diff --git a/gremlin-python/src/main/python/gremlin_python/structure/graph.py
b/gremlin-python/src/main/python/gremlin_python/structure/graph.py
index c093d1435c..6564f949a7 100644
--- a/gremlin-python/src/main/python/gremlin_python/structure/graph.py
+++ b/gremlin-python/src/main/python/gremlin_python/structure/graph.py
@@ -42,6 +42,18 @@ class Element(object):
def __hash__(self):
return hash(self.id)
+ def property_map(self):
+ """Groups this element's properties by key.
+
+ Returns a dict of property key -> list of property objects (an empty
+ dict when this element has no properties). ``self.properties`` is a
+ flat list where each item exposes ``.key``.
+ """
+ result = {}
+ for p in self.properties:
+ result.setdefault(p.key, []).append(p)
+ return result
+
class Vertex(Element):
def __init__(self, id, label="vertex", properties=None):
diff --git a/gremlin-python/src/main/python/tests/unit/structure/test_graph.py
b/gremlin-python/src/main/python/tests/unit/structure/test_graph.py
index f3c537828e..4d09c096b0 100644
--- a/gremlin-python/src/main/python/tests/unit/structure/test_graph.py
+++ b/gremlin-python/src/main/python/tests/unit/structure/test_graph.py
@@ -92,6 +92,58 @@ class TestGraph(object):
assert i == j
assert i.__hash__() == hash(i)
+ def test_vertex_property_map(self):
+ v = Vertex(1, "person")
+ # multi-properties: two VertexProperty entries with the same key group
together
+ name1 = VertexProperty(long(1), "name", "marko", v)
+ name2 = VertexProperty(long(2), "name", "marko a. rodriguez", v)
+ age = VertexProperty(long(3), "age", 29, v)
+ v.properties = [name1, name2, age]
+ pm = v.property_map()
+ assert set(pm.keys()) == {"name", "age"}
+ assert pm["name"] == [name1, name2]
+ assert len(pm["name"]) == 2
+ assert pm["age"] == [age]
+ # single-valued keys still map to 1-element lists
+ assert len(pm["age"]) == 1
+ #
+ # a vertex with no properties yields an empty dict
+ assert Vertex(2).property_map() == {}
+
+ def test_edge_property_map(self):
+ e = Edge(20, Vertex(10), "knows", Vertex(11))
+ # Edge holds Property objects, each exposing .key
+ weight = Property("weight", 0.5, e)
+ since = Property("since", 2006, e)
+ e.properties = [weight, since]
+ pm = e.property_map()
+ assert set(pm.keys()) == {"weight", "since"}
+ # single-valued keys map to 1-element lists
+ assert pm["weight"] == [weight]
+ assert len(pm["weight"]) == 1
+ assert pm["since"] == [since]
+ assert len(pm["since"]) == 1
+ #
+ # an edge with no properties yields an empty dict
+ assert Edge(21, Vertex(10), "knows", Vertex(11)).property_map() == {}
+
+ def test_vertex_property_property_map(self):
+ vp = VertexProperty(long(30), "name", "marko", Vertex(10))
+ # VertexProperty holds meta-properties as Property objects exposing
.key
+ since = Property("since", 2006, vp)
+ skill = Property("skill", 4, vp)
+ vp.properties = [since, skill]
+ pm = vp.property_map()
+ assert set(pm.keys()) == {"since", "skill"}
+ # single-valued keys map to 1-element lists
+ assert pm["since"] == [since]
+ assert len(pm["since"]) == 1
+ assert pm["skill"] == [skill]
+ assert len(pm["skill"]) == 1
+ #
+ # a vertex property with no meta-properties yields an empty dict
+ assert VertexProperty(long(31), "name", "marko",
Vertex(10)).property_map() == {}
+
def test_path(self):
path = Path([set(["a", "b"]), set(["c", "b"]), set([])], [1,
Vertex(1), "hello"])
assert "path[1, v[1], hello]" == str(path)
diff --git
a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerGraphTest.java
b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerGraphTest.java
index bd639ceedc..3562ff3be3 100644
---
a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerGraphTest.java
+++
b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerGraphTest.java
@@ -34,6 +34,7 @@ import
org.apache.tinkerpop.gremlin.process.traversal.util.Metrics;
import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalMetrics;
import org.apache.tinkerpop.gremlin.structure.Edge;
import org.apache.tinkerpop.gremlin.structure.Graph;
+import org.apache.tinkerpop.gremlin.structure.Property;
import org.apache.tinkerpop.gremlin.structure.T;
import org.apache.tinkerpop.gremlin.structure.Vertex;
import org.apache.tinkerpop.gremlin.structure.VertexProperty;
@@ -87,6 +88,7 @@ import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.core.StringContains.containsString;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
@@ -1066,4 +1068,79 @@ public class TinkerGraphTest {
return false;
}
}
+
+ @Test
+ public void shouldGroupMultiPropertyValuesUnderSingleKeyForPropertyMap()
throws Exception {
+ try (final TinkerGraph graph = TinkerGraph.open()) {
+ final Vertex vertex = graph.addVertex(T.label, "person");
+ vertex.property(VertexProperty.Cardinality.list, "name", "marko");
+ vertex.property(VertexProperty.Cardinality.list, "name", "marko a.
rodriguez");
+ vertex.property(VertexProperty.Cardinality.list, "name", "okram");
+
+ final Map<String, List<Property<Object>>> propertyMap =
vertex.propertyMap();
+
+ assertEquals(1, propertyMap.size());
+ assertTrue(propertyMap.containsKey("name"));
+ assertEquals(3, propertyMap.get("name").size());
+ final List<Object> values = new ArrayList<>();
+ for (final Property<Object> p : propertyMap.get("name"))
+ values.add(p.value());
+ assertTrue(values.containsAll(Arrays.asList("marko", "marko a.
rodriguez", "okram")));
+ }
+ }
+
+ @Test
+ public void
shouldGroupSingleValuedKeysIntoSingleElementListsForPropertyMap() throws
Exception {
+ try (final TinkerGraph graph = TinkerGraph.open()) {
+ final Vertex vertex = graph.addVertex(T.label, "person", "name",
"marko", "age", 29);
+
+ final Map<String, List<Property<Object>>> propertyMap =
vertex.propertyMap();
+
+ assertEquals(2, propertyMap.size());
+ assertEquals(1, propertyMap.get("name").size());
+ assertEquals(1, propertyMap.get("age").size());
+ assertEquals("marko", propertyMap.get("name").get(0).value());
+ assertEquals(Integer.valueOf(29),
propertyMap.get("age").get(0).value());
+ }
+ }
+
+ @Test
+ public void shouldReturnEmptyMapForPropertyMapWhenNoProperties() throws
Exception {
+ try (final TinkerGraph graph = TinkerGraph.open()) {
+ final Vertex vertex = graph.addVertex(T.label, "person");
+ assertTrue(vertex.propertyMap().isEmpty());
+ }
+ }
+
+ @Test
+ public void shouldGroupEdgePropertiesByKeyForPropertyMap() throws
Exception {
+ try (final TinkerGraph graph = TinkerGraph.open()) {
+ final Vertex marko = graph.addVertex(T.label, "person", "name",
"marko");
+ final Vertex vadas = graph.addVertex(T.label, "person", "name",
"vadas");
+ final Edge edge = marko.addEdge("knows", vadas, "weight", 0.5d,
"since", 2010);
+
+ final Map<String, List<Property<Object>>> propertyMap =
edge.propertyMap();
+
+ assertEquals(2, propertyMap.size());
+ assertEquals(1, propertyMap.get("weight").size());
+ assertEquals(1, propertyMap.get("since").size());
+ assertEquals(Double.valueOf(0.5d),
propertyMap.get("weight").get(0).value());
+ assertEquals(Integer.valueOf(2010),
propertyMap.get("since").get(0).value());
+ }
+ }
+
+ @Test
+ public void shouldFilterByExplicitPropertyKeysForPropertyMap() throws
Exception {
+ try (final TinkerGraph graph = TinkerGraph.open()) {
+ final Vertex vertex = graph.addVertex(T.label, "person", "name",
"marko", "age", 29, "city", "santa fe");
+
+ final Map<String, List<Property<Object>>> propertyMap =
vertex.propertyMap("name", "age");
+
+ assertEquals(2, propertyMap.size());
+ assertTrue(propertyMap.containsKey("name"));
+ assertTrue(propertyMap.containsKey("age"));
+ assertFalse(propertyMap.containsKey("city"));
+ assertEquals("marko", propertyMap.get("name").get(0).value());
+ }
+ }
}