This is an automated email from the ASF dual-hosted git repository.
Cole-Greer pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
The following commit(s) were added to refs/heads/master by this push:
new 5271e1a764 [gremlin-dotnet] Handle non-bulked results in remote
traversals (#3525)
5271e1a764 is described below
commit 5271e1a764660136d811296638b24e859f2ab1b7
Author: Yang Xia <[email protected]>
AuthorDate: Mon Jul 20 09:21:40 2026 -0700
[gremlin-dotnet] Handle non-bulked results in remote traversals (#3525)
The `gremlin-dotnet` remote traversal path (`DriverRemoteConnection` /
`TransactionRemoteConnection`) assumed every server result was a `Traverser`.
It submitted as `SubmitAsync<Traverser>`, so `ResultSet<Traverser>` performed
an unconditional `(Traverser)item` cast during iteration.
When a compatible HTTP endpoint returns non-bulked results, e.g. a raw
`Int64` for `g.V().count()`, or any traversal run with `bulkResults=false`, the
values arrive as raw scalars rather than `Traverser` instances, and the cast
throws `InvalidCastException`. In effect, `bulkResults=false` was unusable on
the remote traversal path.
Submit as `SubmitAsync<object>` and adapt the stream at the iteration
boundary: pass `Traverser` instances through unchanged, and wrap raw values in
`new Traverser(item, 1)`. This mirrors the other GLVs. The generic
`ResultSet<T>` and the plain `client.SubmitAsync<T>` path are unchanged.
Assisted-by: Kiro:claude-opus-4-8
---
.../Driver/Remote/DriverRemoteConnection.cs | 2 +-
.../Driver/Remote/DriverRemoteTraversal.cs | 21 ++++++-
.../Driver/Remote/TransactionRemoteConnection.cs | 2 +-
.../Driver/GremlinClientTests.cs | 39 ++++++++++++
.../DriverRemoteConnection/GraphTraversalTests.cs | 28 +++++++++
.../Driver/DriverRemoteConnectionTests.cs | 72 +++++++++++++++++++++-
6 files changed, 157 insertions(+), 7 deletions(-)
diff --git
a/gremlin-dotnet/src/Gremlin.Net/Driver/Remote/DriverRemoteConnection.cs
b/gremlin-dotnet/src/Gremlin.Net/Driver/Remote/DriverRemoteConnection.cs
index 3f3ef7462b..57cefda32a 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Driver/Remote/DriverRemoteConnection.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Driver/Remote/DriverRemoteConnection.cs
@@ -142,7 +142,7 @@ namespace Gremlin.Net.Driver.Remote
requestMsg.AddField(Tokens.ArgsBulkResults, true);
}
- var resultSet = await
_client.SubmitAsync<Traverser>(requestMsg.Create(), cancellationToken)
+ var resultSet = await
_client.SubmitAsync<object>(requestMsg.Create(), cancellationToken)
.ConfigureAwait(false);
return new DriverRemoteTraversal<TStart, TEnd>(resultSet);
}
diff --git
a/gremlin-dotnet/src/Gremlin.Net/Driver/Remote/DriverRemoteTraversal.cs
b/gremlin-dotnet/src/Gremlin.Net/Driver/Remote/DriverRemoteTraversal.cs
index e4ff99563a..fa19257584 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Driver/Remote/DriverRemoteTraversal.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Driver/Remote/DriverRemoteTraversal.cs
@@ -22,22 +22,37 @@
#endregion
using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
using Gremlin.Net.Process.Traversal;
namespace Gremlin.Net.Driver.Remote
{
/// <summary>
/// A traversal returned from a remote Gremlin Server submission,
wrapping a
- /// <see cref="ResultSet{T}"/> of <see cref="Traverser"/> instances.
+ /// <see cref="ResultSet{T}"/> of result objects.
/// </summary>
internal class DriverRemoteTraversal<TStart, TEnd> :
DefaultTraversal<TStart, TEnd>
{
- public DriverRemoteTraversal(ResultSet<Traverser> resultSet)
+ public DriverRemoteTraversal(ResultSet<object> resultSet)
{
- Traversers = resultSet;
+ Traversers = AdaptResults(resultSet);
}
/// <inheritdoc />
public override GremlinLang GremlinLang => throw new
NotSupportedException("Remote traversals do not have GremlinLang");
+
+ // Bulked responses arrive as Traversers; non-bulked responses arrive
as raw values
+ // (e.g. a long for g.V().count()) and are wrapped with a bulk of 1,
as the other GLVs do.
+ private static async IAsyncEnumerable<Traverser>
AdaptResults(ResultSet<object> resultSet,
+ [EnumeratorCancellation] CancellationToken cancellationToken =
default)
+ {
+ await foreach (var item in
resultSet.WithCancellation(cancellationToken).ConfigureAwait(false))
+ {
+ yield return item as Traverser ?? new Traverser(item, 1);
+ }
+ }
}
}
\ No newline at end of file
diff --git
a/gremlin-dotnet/src/Gremlin.Net/Driver/Remote/TransactionRemoteConnection.cs
b/gremlin-dotnet/src/Gremlin.Net/Driver/Remote/TransactionRemoteConnection.cs
index ac60914b5a..c80d5ab80a 100644
---
a/gremlin-dotnet/src/Gremlin.Net/Driver/Remote/TransactionRemoteConnection.cs
+++
b/gremlin-dotnet/src/Gremlin.Net/Driver/Remote/TransactionRemoteConnection.cs
@@ -87,7 +87,7 @@ namespace Gremlin.Net.Driver.Remote
requestMsg.AddField(Tokens.ArgsTransactionId, _transactionId);
// Route through Transaction's serialized submission to guarantee
ordering
- var resultSet = await
_transaction.SubmitAsync<Traverser>(requestMsg.Create(), cancellationToken)
+ var resultSet = await
_transaction.SubmitAsync<object>(requestMsg.Create(), cancellationToken)
.ConfigureAwait(false);
return new DriverRemoteTraversal<TStart, TEnd>(resultSet);
}
diff --git
a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/GremlinClientTests.cs
b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/GremlinClientTests.cs
index a35f026522..0f14c88b84 100644
---
a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/GremlinClientTests.cs
+++
b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/GremlinClientTests.cs
@@ -23,11 +23,13 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using System.Threading.Tasks;
using Gremlin.Net.Driver;
using Gremlin.Net.Driver.Exceptions;
using Gremlin.Net.Driver.Messages;
using Gremlin.Net.IntegrationTest.Util;
+using Gremlin.Net.Process.Traversal;
using Gremlin.Net.Structure;
using Microsoft.Extensions.Logging;
using NSubstitute;
@@ -135,6 +137,43 @@ namespace Gremlin.Net.IntegrationTest.Driver
}
}
+ [Fact]
+ public async Task ShouldReturnRawValuesWithBulkResultsFalse()
+ {
+ var gremlinServer = new GremlinServer(TestHost, TestPort);
+ using (var gremlinClient = new GremlinClient(gremlinServer))
+ {
+ // bulkResults=false: the ResultSet contains raw values (no
Traverser wrapping).
+ var requestMessage =
RequestMessage.Build("g.inject(1,2,3,2,1)")
+ .AddBulkResults(false).Create();
+
+ var response = await
gremlinClient.SubmitAsync<object>(requestMessage);
+ var results = await response.ToListAsync();
+
+ Assert.Equal(5, results.Count);
+ Assert.All(results, r => Assert.IsNotType<Traverser>(r));
+ }
+ }
+
+ [Fact]
+ public async Task ShouldReturnTraversersWithBulkResultsTrue()
+ {
+ var gremlinServer = new GremlinServer(TestHost, TestPort);
+ using (var gremlinClient = new GremlinClient(gremlinServer))
+ {
+ // bulkResults=true: values are bulked into Traversers, read
as Traverser.
+ var requestMessage =
RequestMessage.Build("g.inject(1,2,3,2,1)")
+ .AddBulkResults(true).Create();
+
+ var response = await
gremlinClient.SubmitAsync<Traverser>(requestMessage);
+ var results = await response.ToListAsync();
+
+ // 3 unique values with bulk counts summing to the 5 injected.
+ Assert.Equal(3, results.Count);
+ Assert.Equal(5L, results.Sum(t => t.Bulk));
+ }
+ }
+
[Fact]
public async Task ShouldThrowOnExecutionOfSimpleInvalidScript()
{
diff --git
a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Process/Traversal/DriverRemoteConnection/GraphTraversalTests.cs
b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Process/Traversal/DriverRemoteConnection/GraphTraversalTests.cs
index 489994b4c0..0c9104ba8c 100644
---
a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Process/Traversal/DriverRemoteConnection/GraphTraversalTests.cs
+++
b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Process/Traversal/DriverRemoteConnection/GraphTraversalTests.cs
@@ -61,6 +61,34 @@ namespace
Gremlin.Net.IntegrationTest.Process.Traversal.DriverRemoteConnection
Assert.Equal(2, count);
}
+ [Fact]
+ public void g_V_Count_WithBulkResultsFalse()
+ {
+ // bulkResults=false makes the server return a raw scalar instead
of a bulked Traverser.
+ var connection = _connectionFactory.CreateRemoteConnection();
+ var g = AnonymousTraversalSource.Traversal().With(connection)
+ .With(Tokens.ArgsBulkResults, false);
+
+ var count = g.V().Count().Next();
+
+ Assert.Equal(6, count);
+ }
+
+ [Fact]
+ public void g_V_Values_WithBulkResultsFalse()
+ {
+ // Non-bulked multi-result response: each raw value must be
yielded.
+ var connection = _connectionFactory.CreateRemoteConnection();
+ var g = AnonymousTraversalSource.Traversal().With(connection)
+ .With(Tokens.ArgsBulkResults, false);
+
+ var names = g.V().Values<string>("name").ToList();
+
+ Assert.Equal(6, names.Count);
+ Assert.Contains("marko", names);
+ Assert.Contains("lop", names);
+ }
+
[Fact]
public void g_V_Count_Clone()
{
diff --git
a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Driver/DriverRemoteConnectionTests.cs
b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Driver/DriverRemoteConnectionTests.cs
index e4c1b1fb05..4d21579498 100644
---
a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Driver/DriverRemoteConnectionTests.cs
+++
b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Driver/DriverRemoteConnectionTests.cs
@@ -218,20 +218,88 @@ namespace Gremlin.Net.UnitTest.Driver
Assert.Equal(false,
capturedRequest!.Fields[Tokens.ArgsBulkResults]);
}
+ [Fact]
+ public async Task ShouldYieldRawScalarFromNonBulkedResponse()
+ {
+ // A raw (non-bulked) scalar must be yielded directly, not
cast-failed as a Traverser.
+ var client = CreateResultClient(10L);
+ var connection = new DriverRemoteConnection(client, "g");
+ var gl = new GremlinLang();
+ gl.AddStep("V", Array.Empty<object>());
+
+ var traversal = await connection.SubmitAsync<object, long>(gl);
+ var results = traversal.ToList();
+
+ Assert.Equal(new long[] { 10L }, results);
+ }
+
+ [Fact]
+ public async Task ShouldExpandBulkedTraverserFromResponse()
+ {
+ // A bulked Traverser must be expanded by its bulk count.
+ var client = CreateResultClient(new Traverser("a", 3));
+ var connection = new DriverRemoteConnection(client, "g");
+ var gl = new GremlinLang();
+ gl.AddStep("V", Array.Empty<object>());
+
+ var traversal = await connection.SubmitAsync<object, string>(gl);
+ var results = traversal.ToList();
+
+ Assert.Equal(new[] { "a", "a", "a" }, results);
+ }
+
+ [Fact]
+ public async Task ShouldHandleMixOfRawValuesAndTraversers()
+ {
+ // Raw values and bulked Traversers may interleave in one response.
+ var client = CreateResultClient(1L, new Traverser(2L, 2), 3L);
+ var connection = new DriverRemoteConnection(client, "g");
+ var gl = new GremlinLang();
+ gl.AddStep("V", Array.Empty<object>());
+
+ var traversal = await connection.SubmitAsync<object, long>(gl);
+ var results = traversal.ToList();
+
+ Assert.Equal(new long[] { 1L, 2L, 2L, 3L }, results);
+ }
+
+ /// <summary>
+ /// Creates a mock IGremlinClient whose submission returns a
ResultSet streaming the
+ /// provided items (raw values and/or Traverser instances).
+ /// </summary>
+ private static IGremlinClient CreateResultClient(params object[] items)
+ {
+ var client = Substitute.For<IGremlinClient>();
+ client.SubmitAsync<object>(Arg.Any<RequestMessage>(),
Arg.Any<CancellationToken>())
+ .Returns(_ =>
+ {
+ var channel =
System.Threading.Channels.Channel.CreateUnbounded<object>();
+ foreach (var item in items)
+ {
+ channel.Writer.TryWrite(item);
+ }
+ channel.Writer.Complete();
+ var cts = new CancellationTokenSource();
+ var resultSet = new ResultSet<object>(channel.Reader, cts,
Task.CompletedTask);
+ return Task.FromResult(resultSet);
+ });
+ return client;
+ }
+
/// <summary>
/// Creates a mock IGremlinClient that captures the submitted
RequestMessage.
/// </summary>
private static IGremlinClient
CreateCapturingClient(Action<RequestMessage> capture)
{
var client = Substitute.For<IGremlinClient>();
- client.SubmitAsync<Traverser>(Arg.Any<RequestMessage>(),
Arg.Any<CancellationToken>())
+ client.SubmitAsync<object>(Arg.Any<RequestMessage>(),
Arg.Any<CancellationToken>())
.Returns(callInfo =>
{
capture(callInfo.Arg<RequestMessage>());
var channel =
System.Threading.Channels.Channel.CreateUnbounded<object>();
channel.Writer.Complete();
var cts = new CancellationTokenSource();
- var emptyResult = new ResultSet<Traverser>(
+ var emptyResult = new ResultSet<object>(
channel.Reader, cts, Task.CompletedTask);
return Task.FromResult(emptyResult);
});