From 4576eb65ad72d67f3f4ed1ece516e84dd92bcd68 Mon Sep 17 00:00:00 2001
From: Francois Jehl <16541584+francoisjehl@users.noreply.github.com>
Date: Wed, 14 Oct 2026 14:30:28 +0200
Subject: [PATCH 1/2] fix: clamp RecordsAffected to int.MaxValue instead of
throwing OverflowException
---
src/SingleStoreConnector/SingleStoreDataReader.cs | 5 +++--
tests/SingleStoreConnector.Tests/ConnectionTests.cs | 9 +++++++++
.../FakeSingleStoreServerConnection.cs | 12 ++++++++++++
3 files changed, 24 insertions(+), 2 deletions(-)
diff --git a/src/SingleStoreConnector/SingleStoreDataReader.cs b/src/SingleStoreConnector/SingleStoreDataReader.cs
index 92ed68a5a..69bdae93e 100644
--- a/src/SingleStoreConnector/SingleStoreDataReader.cs
+++ b/src/SingleStoreConnector/SingleStoreDataReader.cs
@@ -208,8 +208,9 @@ public override bool HasRows
///
/// For UPDATE, INSERT, and DELETE statements, the return value is the number of rows affected by the command.
/// For stored procedures, the return value is the number of rows affected by the last statement in the stored procedure,
- /// or zero if the last statement is a SELECT. For all other types of statements, the return value is -1.
- public override int RecordsAffected => RealRecordsAffected is ulong recordsAffected ? checked((int) recordsAffected) : -1;
+ /// or zero if the last statement is a SELECT. For all other types of statements, the return value is -1.
+ /// If the number of affected rows exceeds , the return value is .
+ public override int RecordsAffected => RealRecordsAffected is ulong recordsAffected ? (int) Math.Min(recordsAffected, int.MaxValue) : -1;
public override int GetOrdinal(string name) => GetResultSet().GetOrdinal(name);
public override bool GetBoolean(int ordinal) => GetResultSet().GetCurrentRow().GetBoolean(ordinal);
diff --git a/tests/SingleStoreConnector.Tests/ConnectionTests.cs b/tests/SingleStoreConnector.Tests/ConnectionTests.cs
index 6f453f1a8..087b54368 100644
--- a/tests/SingleStoreConnector.Tests/ConnectionTests.cs
+++ b/tests/SingleStoreConnector.Tests/ConnectionTests.cs
@@ -19,6 +19,15 @@ public void Dispose()
m_server.Stop();
}
+ [Fact]
+ public void RecordsAffectedAboveInt32MaxValueIsClamped()
+ {
+ using var connection = new SingleStoreConnection(m_csb.ConnectionString);
+ connection.Open();
+ using var command = new SingleStoreCommand("ALTER TABLE big_table ADD COLUMN c INT", connection);
+ Assert.Equal(int.MaxValue, command.ExecuteNonQuery());
+ }
+
[Fact]
public void PooledConnectionIsReturnedToPool()
{
diff --git a/tests/SingleStoreConnector.Tests/FakeSingleStoreServerConnection.cs b/tests/SingleStoreConnector.Tests/FakeSingleStoreServerConnection.cs
index 3b3eabedd..22453f687 100644
--- a/tests/SingleStoreConnector.Tests/FakeSingleStoreServerConnection.cs
+++ b/tests/SingleStoreConnector.Tests/FakeSingleStoreServerConnection.cs
@@ -167,6 +167,18 @@ public async Task RunAsync(TcpClient client, CancellationToken token)
var wasSet = CancelQueryEvent.Wait(0, token);
await SendAsync(stream, 1, WriteOk);
}
+ else if (query == "ALTER TABLE big_table ADD COLUMN c INT")
+ {
+ await SendAsync(stream, 1, x =>
+ {
+ x.Write((byte) 0); // signature
+ x.Write((byte) 0xFE); // 8-byte length-encoded integer
+ x.Write(4_000_000_000UL); // rows affected
+ x.Write((byte) 0); // last insert ID
+ x.Write((ushort) 0); // server status
+ x.Write((ushort) 0); // warning count
+ });
+ }
else if (query == "select infinity")
{
var packets = new[]
From 2e76d90af609bf58596742320b0d39863f103e9e Mon Sep 17 00:00:00 2001
From: Francois Jehl <16541584+francoisjehl@users.noreply.github.com>
Date: Fri, 16 Oct 2026 14:56:43 +0200
Subject: [PATCH 2/2] fix: expose RealRecordsAffected and reuse WriteOk in fake
server
---
.../SingleStoreCommand.cs | 8 +++++--
.../SingleStoreDataReader.cs | 9 +++++--
.../ConnectionTests.cs | 3 +++
.../FakeSingleStoreServerConnection.cs | 24 ++++++++++---------
4 files changed, 29 insertions(+), 15 deletions(-)
diff --git a/src/SingleStoreConnector/SingleStoreCommand.cs b/src/SingleStoreConnector/SingleStoreCommand.cs
index 5901ba438..00a71c695 100644
--- a/src/SingleStoreConnector/SingleStoreCommand.cs
+++ b/src/SingleStoreConnector/SingleStoreCommand.cs
@@ -104,7 +104,9 @@ private SingleStoreCommand(SingleStoreCommand other)
/// The number of rows affected.
/// For UPDATE, INSERT, and DELETE statements, the return value is the number of rows affected by the command.
/// For stored procedures, the return value is the number of rows affected by the last statement in the stored procedure,
- /// or zero if the last statement is a SELECT. For all other types of statements, the return value is -1.
+ /// or zero if the last statement is a SELECT. For all other types of statements, the return value is -1.
+ /// If the number of affected rows exceeds , the return value is ;
+ /// use to get the exact count.
public override int ExecuteNonQuery() => ExecuteNonQueryAsync(IOBehavior.Synchronous, CancellationToken.None).Result;
public override object? ExecuteScalar() => ExecuteScalarAsync(IOBehavior.Synchronous, CancellationToken.None).Result;
@@ -292,7 +294,9 @@ protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) =>
/// A task representing the asynchronous operation.
/// For UPDATE, INSERT, and DELETE statements, the return value is the number of rows affected by the command.
/// For stored procedures, the return value is the number of rows affected by the last statement in the stored procedure,
- /// or zero if the last statement is a SELECT. For all other types of statements, the return value is -1.
+ /// or zero if the last statement is a SELECT. For all other types of statements, the return value is -1.
+ /// If the number of affected rows exceeds , the return value is ;
+ /// use to get the exact count.
public override Task ExecuteNonQueryAsync(CancellationToken cancellationToken) =>
ExecuteNonQueryAsync(AsyncIOBehavior, cancellationToken).AsTask();
diff --git a/src/SingleStoreConnector/SingleStoreDataReader.cs b/src/SingleStoreConnector/SingleStoreDataReader.cs
index 69bdae93e..acadde98e 100644
--- a/src/SingleStoreConnector/SingleStoreDataReader.cs
+++ b/src/SingleStoreConnector/SingleStoreDataReader.cs
@@ -209,7 +209,8 @@ public override bool HasRows
/// For UPDATE, INSERT, and DELETE statements, the return value is the number of rows affected by the command.
/// For stored procedures, the return value is the number of rows affected by the last statement in the stored procedure,
/// or zero if the last statement is a SELECT. For all other types of statements, the return value is -1.
- /// If the number of affected rows exceeds , the return value is .
+ /// If the number of affected rows exceeds , the return value is ;
+ /// use to get the exact count.
public override int RecordsAffected => RealRecordsAffected is ulong recordsAffected ? (int) Math.Min(recordsAffected, int.MaxValue) : -1;
public override int GetOrdinal(string name) => GetResultSet().GetOrdinal(name);
@@ -464,7 +465,11 @@ protected override void Dispose(bool disposing)
internal Activity? Activity { get; private set; }
internal ISingleStoreCommand? Command { get; private set; }
internal SingleStoreConnection? Connection => Command?.Connection;
- internal ulong? RealRecordsAffected { get; set; }
+ ///
+ /// Gets the number of rows changed, inserted, or deleted by execution of the SQL statement, without truncation to .
+ ///
+ /// Returns null when would return -1.
+ public ulong? RealRecordsAffected { get; internal set; }
internal ServerSession? Session => Command?.Connection!.Session;
internal async Task InitAsync(CommandListPosition commandListPosition, ICommandPayloadCreator payloadCreator, IDictionary? cachedProcedures, ISingleStoreCommand command, CommandBehavior behavior, Activity? activity, IOBehavior ioBehavior, CancellationToken cancellationToken)
diff --git a/tests/SingleStoreConnector.Tests/ConnectionTests.cs b/tests/SingleStoreConnector.Tests/ConnectionTests.cs
index 087b54368..1889422a3 100644
--- a/tests/SingleStoreConnector.Tests/ConnectionTests.cs
+++ b/tests/SingleStoreConnector.Tests/ConnectionTests.cs
@@ -26,6 +26,9 @@ public void RecordsAffectedAboveInt32MaxValueIsClamped()
connection.Open();
using var command = new SingleStoreCommand("ALTER TABLE big_table ADD COLUMN c INT", connection);
Assert.Equal(int.MaxValue, command.ExecuteNonQuery());
+ using var reader = command.ExecuteReader();
+ Assert.Equal(int.MaxValue, reader.RecordsAffected);
+ Assert.Equal(4_000_000_000UL, reader.RealRecordsAffected);
}
[Fact]
diff --git a/tests/SingleStoreConnector.Tests/FakeSingleStoreServerConnection.cs b/tests/SingleStoreConnector.Tests/FakeSingleStoreServerConnection.cs
index 22453f687..9733b67a1 100644
--- a/tests/SingleStoreConnector.Tests/FakeSingleStoreServerConnection.cs
+++ b/tests/SingleStoreConnector.Tests/FakeSingleStoreServerConnection.cs
@@ -169,15 +169,7 @@ public async Task RunAsync(TcpClient client, CancellationToken token)
}
else if (query == "ALTER TABLE big_table ADD COLUMN c INT")
{
- await SendAsync(stream, 1, x =>
- {
- x.Write((byte) 0); // signature
- x.Write((byte) 0xFE); // 8-byte length-encoded integer
- x.Write(4_000_000_000UL); // rows affected
- x.Write((byte) 0); // last insert ID
- x.Write((ushort) 0); // server status
- x.Write((ushort) 0); // warning count
- });
+ await SendAsync(stream, 1, x => WriteOk(x, 4_000_000_000UL));
}
else if (query == "select infinity")
{
@@ -307,10 +299,20 @@ private void WriteInitialHandshake(BinaryWriter writer)
writer.Write((byte) 0);
}
- private static void WriteOk(BinaryWriter writer)
+ private static void WriteOk(BinaryWriter writer) => WriteOk(writer, 0);
+
+ private static void WriteOk(BinaryWriter writer, ulong affectedRows)
{
writer.Write((byte) 0); // signature
- writer.Write((byte) 0); // 0 rows affected
+ if (affectedRows < 251)
+ {
+ writer.Write((byte) affectedRows); // rows affected
+ }
+ else
+ {
+ writer.Write((byte) 0xFE); // 8-byte length-encoded integer
+ writer.Write(affectedRows); // rows affected
+ }
writer.Write((byte) 0); // last insert ID
writer.Write((ushort) 0); // server status
writer.Write((ushort) 0); // warning count