Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima
* Modified HTTP API to expect gremlin-lang strings for parameters and update all GLVs to send requests in new format.
* Added string parameter parsing to `GremlinServer` to prevent traversal injection and excessive nesting depths.
* Modified all GLVs to detect unsupported types in `GremlinLang` and throw consistent error for that case.
* Added GraphBinary 4.0 `Graph` (`0x10`) serializer/deserializer to `gremlin-javascript`, `gremlin-dotnet`, and `gremlin-go` so that `subgraph()` results are returned as a detached `Graph` data container.

[[release-4-0-0-beta-2]]
=== TinkerPop 4.0.0-beta.2 (April 1, 2026)
Expand Down
29 changes: 20 additions & 9 deletions docs/src/reference/gremlin-variants.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,12 @@ that can be used to fulfill the `gremlingo.Set` interface if desired.
* Go does not support ordered maps natively as the built-in `map` type does not guarantee iteration order. Traversal
results which contain maps may not preserve original ordering when deserialized into Go's native map types.

* The `subgraph()`-step returns a detached `*Graph` data container exposing
`Vertices map[interface{}]*Vertex` and `Edges map[interface{}]*Edge`. The result is not a live `Graph` instance:
mutating the maps has no effect on the source graph, and it cannot be passed to `traversal().with(...)`. To
re-query subgraph elements against the original graph, extract their `Id` and use `g.V(id)` / `g.E(id)` on the
original `GraphTraversalSource`.

[[gremlin-go-examples]]
=== Application Examples

Expand Down Expand Up @@ -2035,9 +2041,11 @@ exact type sent to the server — see <<gremlin-javascript-numeric-types>>.
signed range are unsuffixed (Int), integers beyond that up to `Number.MAX_SAFE_INTEGER` use the `L` suffix (Long),
non-integer numbers and integers beyond the safe range use the `D` suffix (Double), and `BigInt` values use the `N`
suffix (BigInteger).
* The `subgraph()`-step is not supported by any variant that is not running on the Java Virtual Machine as there is
no `Graph` instance to deserialize a result into on the client-side. A workaround is to replace the step with
`aggregate(local)` and then convert those results to something the client can use locally.
* The `subgraph()`-step returns a detached `Graph` data container exposing
`vertices: Map<any, Vertex>` and `edges: Map<any, Edge>`. The result is not a live `Graph` instance: mutating the
collections has no effect on the source graph, and it cannot be passed to `traversal().with(...)`. To re-query
subgraph elements against the original graph, extract their `id` and use `g.V(id)` / `g.E(id)` on the original
`GraphTraversalSource`.

[[gremlin-javascript-examples]]
=== Application Examples
Expand Down Expand Up @@ -2451,9 +2459,11 @@ anchor:gremlin-net-limitations[]
[[gremlin-dotnet-limitations]]
=== Limitations

* The `subgraph()`-step is not supported by any variant that is not running on the Java Virtual Machine as there is
no `Graph` instance to deserialize a result into on the client-side. A workaround is to replace the step with
`aggregate(local)` and then convert those results to something the client can use locally.
* The `subgraph()`-step returns a detached `Graph` data container exposing
`Vertices: IDictionary<object, Vertex>` and `Edges: IDictionary<object, Edge>`. The result is not a live `Graph`
instance: mutating the collections has no effect on the source graph, and it cannot be passed to
`traversal().with(...)`. To re-query subgraph elements against the original graph, extract their `Id` and use
`g.V(id)` / `g.E(id)` on the original `GraphTraversalSource`.
* `DateTimeOffset` cannot represent the extreme values of Gremlin's `OffsetDateTime` maximum and minimum,
so offset date-time values at those boundaries will fail to deserialize.
* Gremlin's `Duration` type has a much larger range than C#'s `TimeSpan`, so extreme duration values (such as
Expand Down Expand Up @@ -3016,9 +3026,10 @@ and `timedelta`.
* In Gremlin, 1 isn't equal to the boolean true value and 0 isn't equal to the boolean false value, but they are equal
in Python. This means that in `gremlin-python` if these values are in a `Set`, you will get a different behavior than
what is intended by Gremlin, since it follows Python's behavior.
* The `subgraph()`-step is not supported by any variant that is not running on the Java Virtual Machine as there is
no `Graph` instance to deserialize a result into on the client-side. A workaround is to replace the step with
`aggregate(local)` and then convert those results to something the client can use locally.
* The `subgraph()`-step returns a detached `Graph` data container exposing `vertices: dict` and `edges: dict`
keyed by element id. The result is not a live `Graph` instance: mutating the dicts has no effect on the source
graph, and it cannot be passed to `traversal().with(...)`. To re-query subgraph elements against the original
graph, extract their `id` and use `g.V(id)` / `g.E(id)` on the original `GraphTraversalSource`.
* Use of the aiohttp library in the default transport requires the use of asyncio's event loop to run the async functions.
This can be an issue in situations where the application calling Gremlin-Python is already using an event loop.
Certain types of event loops can be patched using nest-asyncio which allows Gremlin-Python to proceed without an error like
Expand Down
12 changes: 12 additions & 0 deletions docs/src/upgrade/release-4.x.x.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,18 @@ unwrap(toInt(29)); // 29
unwrap('hello'); // 'hello'
----

==== Subgraph Support in GLVs
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to mention python here too? it's essentially all the GLVs being announced for the first time. when i did python i don't think i added any docs because i knew all these other ones had to land too.

i also think this doesn't read like Upgrade Documentation. It should be doing more to explain the relevance of this feature and less about technical details and minutiae.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've changed it to mention that all GLVs support subgraphs and took out the technical details. Thanks!


All GLVs now support the `subgraph()` step. Previously, calling `subgraph()` from a GLV produced an unknown-type error
because the variant could not interpret the `Graph` payload that the server returned. Applications can now extract a
portion of a remote graph as part of a normal traversal and inspect its vertices and edges directly from the client,
without having to re-issue queries to reconstruct the result. See: <<subgraph-step>>.

In the GLVs, the result is a detached snapshot of the captured vertices and edges, not a traversable `Graph` instance.
It cannot be passed to `traversal().with(...)`, and mutating its collections has no effect on the source graph. To
re-query elements against the original graph, extract their ids and call `g.V(id)` or `g.E(id)` against the original
`GraphTraversalSource`.

=== Upgrading for Providers

==== Graph System Providers
Expand Down
17 changes: 17 additions & 0 deletions gremlin-dotnet/src/Gremlin.Net/Structure/Graph.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#endregion

using System;
using System.Collections.Generic;
using Gremlin.Net.Process.Traversal;

namespace Gremlin.Net.Structure
Expand All @@ -32,6 +33,16 @@ namespace Gremlin.Net.Structure
/// </summary>
public class Graph
{
/// <summary>
/// Gets the <see cref="Vertex" /> instances contained in this <see cref="Graph" />, keyed by their id.
/// </summary>
public IDictionary<object, Vertex> Vertices { get; } = new Dictionary<object, Vertex>();

/// <summary>
/// Gets the <see cref="Edge" /> instances contained in this <see cref="Graph" />, keyed by their id.
/// </summary>
public IDictionary<object, Edge> Edges { get; } = new Dictionary<object, Edge>();

/// <summary>
/// Generates a reusable <see cref="GraphTraversalSource" /> instance.
/// </summary>
Expand All @@ -41,5 +52,11 @@ public GraphTraversalSource Traversal()
{
return new GraphTraversalSource();
}

/// <inheritdoc />
public override string ToString()
{
return $"graph[vertices:{Vertices.Count} edges:{Edges.Count}]";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,7 @@ public class DataType : IEquatable<DataType>
public static readonly DataType Edge = new DataType(0x0D);
public static readonly DataType Path = new DataType(0x0E);
public static readonly DataType Property = new DataType(0x0F);
// Not yet implemented
// public static readonly DataType Graph = new DataType(0x10);
public static readonly DataType Graph = new DataType(0x10);
public static readonly DataType Vertex = new DataType(0x11);
public static readonly DataType VertexProperty = new DataType(0x12);
public static readonly DataType Direction = new DataType(0x18);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public class TypeSerializerRegistry
{typeof(float), SingleTypeSerializers.FloatSerializer},
{typeof(Guid), new UuidSerializer()},
{typeof(Edge), new EdgeSerializer()},
{typeof(Graph), new GraphSerializer()},
{typeof(Path), new PathSerializer()},
{typeof(Property), new PropertySerializer()},
{typeof(Vertex), new VertexSerializer()},
Expand Down Expand Up @@ -80,6 +81,7 @@ public class TypeSerializerRegistry
{DataType.Set, new SetSerializer<HashSet<object?>, object>()},
{DataType.Uuid, new UuidSerializer()},
{DataType.Edge, new EdgeSerializer()},
{DataType.Graph, new GraphSerializer()},
{DataType.Path, new PathSerializer()},
{DataType.Property, new PropertySerializer()},
{DataType.Vertex, new VertexSerializer()},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
#region License

/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

#endregion

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Gremlin.Net.Structure.IO.GraphBinary4.Types
{
/// <summary>
/// A <see cref="Graph"/> serializer for GraphBinary. The wire format is a count-prefixed
/// list of vertices (each with their vertex properties and meta-properties), followed by a
/// count-prefixed list of edges (each with their properties). Vertex/edge labels are written
/// as a single-element list, parent placeholders are written as <c>null</c>.
/// </summary>
public class GraphSerializer : SimpleTypeSerializer<Graph>
{
/// <summary>
/// Initializes a new instance of the <see cref="GraphSerializer" /> class.
/// </summary>
public GraphSerializer() : base(DataType.Graph)
{
}

/// <inheritdoc />
protected override async Task WriteValueAsync(Graph value, Stream stream, GraphBinaryWriter writer,
CancellationToken cancellationToken = default)
{
await writer.WriteNonNullableValueAsync(value.Vertices.Count, stream, cancellationToken)
.ConfigureAwait(false);
foreach (var vertex in value.Vertices.Values)
{
await WriteVertexAsync(vertex, stream, writer, cancellationToken).ConfigureAwait(false);
}

await writer.WriteNonNullableValueAsync(value.Edges.Count, stream, cancellationToken)
.ConfigureAwait(false);
foreach (var edge in value.Edges.Values)
{
await WriteEdgeAsync(edge, stream, writer, cancellationToken).ConfigureAwait(false);
}
}

private static async Task WriteVertexAsync(Vertex vertex, Stream stream, GraphBinaryWriter writer,
CancellationToken cancellationToken)
{
await writer.WriteAsync(vertex.Id, stream, cancellationToken).ConfigureAwait(false);
await writer.WriteNonNullableValueAsync(new List<string> { vertex.Label }, stream, cancellationToken)
.ConfigureAwait(false);

var vertexProperties = AsList<VertexProperty>(vertex.Properties);
await writer.WriteNonNullableValueAsync(vertexProperties.Count, stream, cancellationToken)
.ConfigureAwait(false);
foreach (var vp in vertexProperties)
{
await writer.WriteAsync(vp.Id, stream, cancellationToken).ConfigureAwait(false);
await writer.WriteNonNullableValueAsync(new List<string> { vp.Label }, stream, cancellationToken)
.ConfigureAwait(false);
await writer.WriteAsync((object?)vp.Value, stream, cancellationToken).ConfigureAwait(false);

// placeholder for the parent vertex
await writer.WriteAsync(null, stream, cancellationToken).ConfigureAwait(false);

var metaProperties = AsList<Property>(vp.Properties);
await writer.WriteNonNullableValueAsync(metaProperties, stream, cancellationToken)
.ConfigureAwait(false);
}
}

private static async Task WriteEdgeAsync(Edge edge, Stream stream, GraphBinaryWriter writer,
CancellationToken cancellationToken)
{
await writer.WriteAsync(edge.Id, stream, cancellationToken).ConfigureAwait(false);
await writer.WriteNonNullableValueAsync(new List<string> { edge.Label }, stream, cancellationToken)
.ConfigureAwait(false);

await writer.WriteAsync(edge.InV.Id, stream, cancellationToken).ConfigureAwait(false);
// placeholder for the in-vertex label (always null in this context)
await writer.WriteAsync(null, stream, cancellationToken).ConfigureAwait(false);

await writer.WriteAsync(edge.OutV.Id, stream, cancellationToken).ConfigureAwait(false);
// placeholder for the out-vertex label (always null in this context)
await writer.WriteAsync(null, stream, cancellationToken).ConfigureAwait(false);

// placeholder for the parent (never present)
await writer.WriteAsync(null, stream, cancellationToken).ConfigureAwait(false);

var edgeProperties = AsList<Property>(edge.Properties);
await writer.WriteNonNullableValueAsync(edgeProperties, stream, cancellationToken)
.ConfigureAwait(false);
}

/// <inheritdoc />
protected override async Task<Graph> ReadValueAsync(Stream stream, GraphBinaryReader reader,
CancellationToken cancellationToken = default)
{
var graph = new Graph();

var vertexCount =
(int)await reader.ReadNonNullableValueAsync<int>(stream, cancellationToken).ConfigureAwait(false);
for (var i = 0; i < vertexCount; i++)
{
var vertex = await ReadVertexAsync(stream, reader, cancellationToken).ConfigureAwait(false);
if (vertex.Id != null)
{
graph.Vertices[vertex.Id] = vertex;
}
}

var edgeCount =
(int)await reader.ReadNonNullableValueAsync<int>(stream, cancellationToken).ConfigureAwait(false);
for (var i = 0; i < edgeCount; i++)
{
var edge = await ReadEdgeAsync(graph, stream, reader, cancellationToken).ConfigureAwait(false);
if (edge.Id != null)
{
graph.Edges[edge.Id] = edge;
}
}

return graph;
}

private static async Task<Vertex> ReadVertexAsync(Stream stream, GraphBinaryReader reader,
CancellationToken cancellationToken)
{
var vId = await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
var vLabelList = (List<string?>)await reader
.ReadNonNullableValueAsync<List<string?>>(stream, cancellationToken).ConfigureAwait(false);
var vLabel = vLabelList.Count > 0 ? vLabelList[0] ?? "" : "";

var vpCount = (int)await reader.ReadNonNullableValueAsync<int>(stream, cancellationToken)
.ConfigureAwait(false);
var vertexProperties = new List<VertexProperty>(vpCount);
for (var j = 0; j < vpCount; j++)
{
var vpId = await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
var vpLabelList = (List<string?>)await reader
.ReadNonNullableValueAsync<List<string?>>(stream, cancellationToken).ConfigureAwait(false);
var vpLabel = vpLabelList.Count > 0 ? vpLabelList[0] ?? "" : "";
var vpValue = await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false);

// discard the parent vertex placeholder
await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false);

var metaProps = await reader.ReadNonNullableValueAsync<List<object?>>(stream, cancellationToken)
.ConfigureAwait(false);
var metaPropsArray = (metaProps as List<object>)?.ToArray() ?? Array.Empty<object>();

vertexProperties.Add(new VertexProperty(vpId, vpLabel, vpValue, null, metaPropsArray));
}

return new Vertex(vId, vLabel, vertexProperties.Cast<object>().ToArray());
}

private static async Task<Edge> ReadEdgeAsync(Graph graph, Stream stream, GraphBinaryReader reader,
CancellationToken cancellationToken)
{
var eId = await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
var eLabelList = (List<string?>)await reader
.ReadNonNullableValueAsync<List<string?>>(stream, cancellationToken).ConfigureAwait(false);
var eLabel = eLabelList.Count > 0 ? eLabelList[0] ?? "" : "";

var inVId = await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
// discard the in-vertex label placeholder (always null on the wire)
await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
var outVId = await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
// discard the out-vertex label placeholder (always null on the wire)
await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
// discard the parent placeholder
await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false);

var edgeProps = await reader.ReadNonNullableValueAsync<List<object?>>(stream, cancellationToken)
.ConfigureAwait(false);
var edgePropsArray = (edgeProps as List<object>)?.ToArray() ?? Array.Empty<object>();

var inVertex = ResolveVertex(graph, inVId);
var outVertex = ResolveVertex(graph, outVId);

return new Edge(eId, outVertex, eLabel, inVertex, edgePropsArray);
}

private static Vertex ResolveVertex(Graph graph, object? vertexId)
{
if (vertexId != null && graph.Vertices.TryGetValue(vertexId, out var existing))
{
return existing;
}
return new Vertex(vertexId, "");
}

private static List<T> AsList<T>(IEnumerable? source)
{
if (source == null)
{
return new List<T>();
}
return source.Cast<T>().ToList();
}
}
}
Loading
Loading