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.
+    }
 }

Reply via email to