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());
+        }
+    }
 }


Reply via email to