This is an automated email from the ASF dual-hosted git repository.
ptupitsyn pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git
The following commit(s) were added to refs/heads/main by this push:
new f48255cc3e9 IGNITE-22575 .NET: Propagate
SqlBatchException.UpdateCounters (#7900)
f48255cc3e9 is described below
commit f48255cc3e9d35f12e359f7a94a1255fd429f09f
Author: Pavel Tupitsyn <[email protected]>
AuthorDate: Mon Mar 30 16:18:22 2026 +0200
IGNITE-22575 .NET: Propagate SqlBatchException.UpdateCounters (#7900)
---
.../dotnet/Apache.Ignite.Tests/Sql/SqlTests.cs | 5 +-
.../dotnet/Apache.Ignite/Internal/ClientSocket.cs | 36 ++++++++++-
.../Internal/Proto/ErrorExtensions.cs | 5 ++
.../Internal/Proto/MsgPack/MsgPackReader.cs | 27 ++++++++
.../dotnet/Apache.Ignite/Sql/SqlBatchException.cs | 71 ++++++++++++++++++++++
.../ErrorExtensions.cs => Sql/SqlException.cs} | 26 ++++++--
6 files changed, 162 insertions(+), 8 deletions(-)
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/SqlTests.cs
b/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/SqlTests.cs
index 09a1956da44..17c935f04fa 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/SqlTests.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/SqlTests.cs
@@ -762,6 +762,7 @@ namespace Apache.Ignite.Tests.Sql
async () => await Client.Sql.ExecuteBatchAsync(null, "select
1", [[1]]));
Assert.AreEqual("Statement of type \"Query\" is not allowed in
current context [allowedTypes=[DML]].", ex.Message);
+ Assert.IsEmpty(ex.UpdateCounters);
}
[Test]
@@ -780,10 +781,12 @@ namespace Apache.Ignite.Tests.Sql
[1004, "test4"]
];
- // TODO IGNITE-22575 Propagate SqlBatchException.updateCounters
var ex = Assert.ThrowsAsync<SqlBatchException>(async () => await
Client.Sql.ExecuteBatchAsync(null, sql, args));
Assert.AreEqual("PK unique constraint is violated", ex.Message);
Assert.AreEqual("IGN-SQL-5", ex.CodeAsString);
+
+ // 3 rows inserted successfully before the duplicate key error.
+ Assert.AreEqual(new long[] { 1, 1, 1 }, ex.UpdateCounters);
}
[Test]
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/ClientSocket.cs
b/modules/platforms/dotnet/Apache.Ignite/Internal/ClientSocket.cs
index be050d1ae17..e8b5e58f9ec 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/ClientSocket.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/ClientSocket.cs
@@ -467,7 +467,8 @@ namespace Apache.Ignite.Internal
javaStackTrace += $"{Environment.NewLine}---- End of
server-side stack trace ----{Environment.NewLine}";
}
- var ex = ExceptionMapper.GetException(traceId, code, className,
message, javaStackTrace);
+ long[]? updateCounters = null;
+ int? expectedSchemaVersion = null;
int extensionCount = reader.TryReadNil() ? 0 : reader.ReadInt32();
for (int i = 0; i < extensionCount; i++)
@@ -475,7 +476,11 @@ namespace Apache.Ignite.Internal
var key = reader.ReadString();
if (key == ErrorExtensions.ExpectedSchemaVersion)
{
- ex.Data[key] = reader.ReadInt32();
+ expectedSchemaVersion = reader.ReadInt32();
+ }
+ else if (key == ErrorExtensions.SqlUpdateCounters2)
+ {
+ updateCounters = reader.ReadBinaryLongArray();
}
else
{
@@ -483,6 +488,33 @@ namespace Apache.Ignite.Internal
}
}
+ IgniteException ex;
+
+ if (updateCounters != null)
+ {
+ ex = new Ignite.Sql.SqlBatchException(
+ traceId, code, updateCounters, message, new
IgniteServerException(traceId, code, className, javaStackTrace));
+ }
+ else if (className == "org.apache.ignite.sql.SqlBatchException")
+ {
+ ex = new Ignite.Sql.SqlBatchException(
+ traceId, code, message, new IgniteServerException(traceId,
code, className, javaStackTrace));
+ }
+ else if (className == "org.apache.ignite.sql.SqlException")
+ {
+ ex = new Ignite.Sql.SqlException(
+ traceId, code, message, new IgniteServerException(traceId,
code, className, javaStackTrace));
+ }
+ else
+ {
+ ex = ExceptionMapper.GetException(traceId, code, className,
message, javaStackTrace);
+ }
+
+ if (expectedSchemaVersion != null)
+ {
+ ex.Data[ErrorExtensions.ExpectedSchemaVersion] =
expectedSchemaVersion.Value;
+ }
+
Debug.Assert(reader.End, "All error response bytes should be
consumed.");
return ex;
diff --git
a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/ErrorExtensions.cs
b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/ErrorExtensions.cs
index 3fb888ff006..02a22be3f80 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/ErrorExtensions.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/ErrorExtensions.cs
@@ -26,4 +26,9 @@ internal static class ErrorExtensions
/// Expected schema version for <see
cref="ErrorGroups.Table.SchemaVersionMismatch"/> error.
/// </summary>
public const string ExpectedSchemaVersion = "expected-schema-ver";
+
+ /// <summary>
+ /// SQL update counters for <see cref="Ignite.Sql.SqlBatchException"/>
(binary format - array of big-endian longs).
+ /// </summary>
+ public const string SqlUpdateCounters2 = "sql-update-counters-2";
}
diff --git
a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/MsgPack/MsgPackReader.cs
b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/MsgPack/MsgPackReader.cs
index 30357ca7bc1..56ab4148673 100644
---
a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/MsgPack/MsgPackReader.cs
+++
b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/MsgPack/MsgPackReader.cs
@@ -209,6 +209,33 @@ internal ref struct MsgPackReader
/// <returns>Span of byte.</returns>
public ReadOnlySpan<byte> ReadBinary() => GetSpan(ReadBinaryHeader());
+ /// <summary>
+ /// Reads a binary-encoded array of big-endian longs. Returns an empty
array if nil.
+ /// </summary>
+ /// <returns>Array of long.</returns>
+ public long[] ReadBinaryLongArray()
+ {
+ if (TryReadNil())
+ {
+ return [];
+ }
+
+ ReadOnlySpan<byte> bytes = ReadBinary();
+
+ if (bytes.Length % 8 != 0)
+ {
+ throw new IgniteClientException(ErrorGroups.Client.Protocol,
"Invalid binary long array size: " + bytes.Length);
+ }
+
+ var result = new long[bytes.Length / 8];
+ for (int i = 0; i < result.Length; i++)
+ {
+ result[i] = BinaryPrimitives.ReadInt64BigEndian(bytes.Slice(i * 8,
8));
+ }
+
+ return result;
+ }
+
/// <summary>
/// Reads GUID value.
/// </summary>
diff --git a/modules/platforms/dotnet/Apache.Ignite/Sql/SqlBatchException.cs
b/modules/platforms/dotnet/Apache.Ignite/Sql/SqlBatchException.cs
new file mode 100644
index 00000000000..bd7d2419aa9
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite/Sql/SqlBatchException.cs
@@ -0,0 +1,71 @@
+/*
+ * 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.
+ */
+
+namespace Apache.Ignite.Sql;
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+
+/// <summary>
+/// Subclass of <see cref="IgniteException"/> that is thrown when an error
occurs during a batch update operation.
+/// In addition to the information provided by <see cref="IgniteException"/>,
<see cref="SqlBatchException"/> provides the update
+/// counts for all commands that were executed successfully during the batch
update, that is,
+/// all commands that were executed before the error occurred. The order of
elements in the array of update counts
+/// corresponds to the order in which these commands were added to the batch.
+/// </summary>
+[Serializable]
+[SuppressMessage(
+ "Microsoft.Design",
+ "CA1032:ImplementStandardExceptionConstructors",
+ Justification = "Ignite exceptions use a special constructor.")]
+public sealed class SqlBatchException : SqlException
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SqlBatchException"/>
class.
+ /// </summary>
+ /// <param name="traceId">Trace id.</param>
+ /// <param name="code">Code.</param>
+ /// <param name="message">Message.</param>
+ /// <param name="innerException">Inner exception.</param>
+ public SqlBatchException(Guid traceId, int code, string? message,
Exception? innerException = null)
+ : base(traceId, code, message, innerException)
+ {
+ UpdateCounters = [];
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SqlBatchException"/>
class.
+ /// </summary>
+ /// <param name="traceId">Trace id.</param>
+ /// <param name="code">Code.</param>
+ /// <param name="updateCounters">Update counters.</param>
+ /// <param name="message">Message.</param>
+ /// <param name="innerException">Inner exception.</param>
+ public SqlBatchException(Guid traceId, int code, IReadOnlyList<long>
updateCounters, string? message, Exception? innerException = null)
+ : base(traceId, code, message, innerException)
+ {
+ UpdateCounters = updateCounters;
+ }
+
+ /// <summary>
+ /// Gets the update counters. The array describes the outcome of a batch
execution.
+ /// Elements correspond to the order in which commands were added to the
batch.
+ /// Contains update counts for all commands that were executed
successfully before the error occurred.
+ /// </summary>
+ public IReadOnlyList<long> UpdateCounters { get; }
+}
diff --git
a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/ErrorExtensions.cs
b/modules/platforms/dotnet/Apache.Ignite/Sql/SqlException.cs
similarity index 50%
copy from
modules/platforms/dotnet/Apache.Ignite/Internal/Proto/ErrorExtensions.cs
copy to modules/platforms/dotnet/Apache.Ignite/Sql/SqlException.cs
index 3fb888ff006..d79c8cc38e6 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/ErrorExtensions.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Sql/SqlException.cs
@@ -15,15 +15,31 @@
* limitations under the License.
*/
-namespace Apache.Ignite.Internal.Proto;
+namespace Apache.Ignite.Sql;
+
+using System;
+using System.Diagnostics.CodeAnalysis;
/// <summary>
-/// Error data extensions. When the server returns an error response, it may
contain additional data in a map. Keys are defined here.
+/// SQL exception base class.
/// </summary>
-internal static class ErrorExtensions
+[Serializable]
+[SuppressMessage(
+ "Microsoft.Design",
+ "CA1032:ImplementStandardExceptionConstructors",
+ Justification = "Ignite exceptions use a special constructor.")]
+public class SqlException : IgniteException // Non-sealed class (generated
exceptions are sealed)
{
/// <summary>
- /// Expected schema version for <see
cref="ErrorGroups.Table.SchemaVersionMismatch"/> error.
+ /// Initializes a new instance of the <see cref="SqlException"/> class.
/// </summary>
- public const string ExpectedSchemaVersion = "expected-schema-ver";
+ /// <param name="traceId">Trace id.</param>
+ /// <param name="code">Code.</param>
+ /// <param name="message">Message.</param>
+ /// <param name="innerException">Inner exception.</param>
+ public SqlException(Guid traceId, int code, string? message, Exception?
innerException = null)
+ : base(traceId, code, message, innerException)
+ {
+ // No-op.
+ }
}