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 48bbe6e0629 IGNITE-28012 Client protocol: Fix
ErrorExtensions.SQL_UPDATE_COUNTERS handling (#7836)
48bbe6e0629 is described below
commit 48bbe6e0629df0960fbb5e9cb48de6f7b845f8ee
Author: Pavel Tupitsyn <[email protected]>
AuthorDate: Tue Mar 24 13:06:52 2026 +0100
IGNITE-28012 Client protocol: Fix ErrorExtensions.SQL_UPDATE_COUNTERS
handling (#7836)
---
.../org/apache/ignite/sql/SqlBatchException.java | 2 +-
.../internal/client/proto/ClientMessagePacker.java | 21 ++++++++++
.../client/proto/ClientMessageUnpacker.java | 27 +++++++++++++
.../internal/client/proto/ErrorExtensions.java | 5 +++
.../client/proto/ProtocolBitmaskFeature.java | 7 +++-
.../client/proto/ClientMessagePackerTest.java | 40 ++++++++++++++++++
.../proto/ClientMessagePackerUnpackerTest.java | 47 ++++++++++++++++++++++
.../ignite/client/handler/ItClientHandlerTest.java | 1 +
.../ignite/client/handler/ClientHandlerModule.java | 3 +-
.../handler/ClientInboundMessageHandler.java | 37 ++++++++++-------
.../ignite/internal/client/TcpClientChannel.java | 7 +++-
.../internal/client/ClientCompatibilityTests.java | 44 ++++++++++++++++++++
...ldClientWithCurrentServerCompatibilityTest.java | 9 +++++
.../cpp/ignite/protocol/bitmask_feature.h | 6 ++-
modules/platforms/cpp/ignite/protocol/reader.h | 40 ++++++++++++++++++
modules/platforms/cpp/ignite/protocol/utils.cpp | 6 +++
modules/platforms/cpp/ignite/protocol/utils.h | 1 +
.../dotnet/Apache.Ignite.Tests/Sql/SqlTests.cs | 22 ++++++++++
.../dotnet/Apache.Ignite/Internal/ClientSocket.cs | 5 ++-
.../Internal/Proto/ProtocolBitmaskFeature.cs | 7 +++-
20 files changed, 315 insertions(+), 22 deletions(-)
diff --git
a/modules/api/src/main/java/org/apache/ignite/sql/SqlBatchException.java
b/modules/api/src/main/java/org/apache/ignite/sql/SqlBatchException.java
index 5a7fa853da6..13613b5c262 100644
--- a/modules/api/src/main/java/org/apache/ignite/sql/SqlBatchException.java
+++ b/modules/api/src/main/java/org/apache/ignite/sql/SqlBatchException.java
@@ -45,7 +45,7 @@ public class SqlBatchException extends SqlException {
* @param message Detailed message.
* @param cause Optional cause.
*/
- public SqlBatchException(UUID traceId, int code, long[] updCntrs, String
message, @Nullable Throwable cause) {
+ public SqlBatchException(UUID traceId, int code, long @Nullable []
updCntrs, String message, @Nullable Throwable cause) {
super(traceId, code, message, cause);
this.updCntrs = updCntrs != null ? updCntrs : LONG_EMPTY_ARRAY;
diff --git
a/modules/client-common/src/main/java/org/apache/ignite/internal/client/proto/ClientMessagePacker.java
b/modules/client-common/src/main/java/org/apache/ignite/internal/client/proto/ClientMessagePacker.java
index 63f6da5b368..52ef39b0cbf 100644
---
a/modules/client-common/src/main/java/org/apache/ignite/internal/client/proto/ClientMessagePacker.java
+++
b/modules/client-common/src/main/java/org/apache/ignite/internal/client/proto/ClientMessagePacker.java
@@ -630,6 +630,27 @@ public class ClientMessagePacker implements AutoCloseable {
}
}
+ /**
+ * Writes a long array as a single binary value.
+ *
+ * @param arr Long array value.
+ */
+ public void packLongArrayAsBinary(long @Nullable [] arr) {
+ assert !closed : "Packer is closed";
+
+ if (arr == null) {
+ packNil();
+
+ return;
+ }
+
+ packBinaryHeader(arr.length * 8);
+
+ for (long value : arr) {
+ buf.writeLong(value);
+ }
+ }
+
/**
* Packs an array of objects in BinaryTuple format.
*
diff --git
a/modules/client-common/src/main/java/org/apache/ignite/internal/client/proto/ClientMessageUnpacker.java
b/modules/client-common/src/main/java/org/apache/ignite/internal/client/proto/ClientMessageUnpacker.java
index 4b010fd0fdb..8ac5be8e9b1 100644
---
a/modules/client-common/src/main/java/org/apache/ignite/internal/client/proto/ClientMessageUnpacker.java
+++
b/modules/client-common/src/main/java/org/apache/ignite/internal/client/proto/ClientMessageUnpacker.java
@@ -791,6 +791,33 @@ public class ClientMessageUnpacker implements
AutoCloseable {
return res;
}
+ /**
+ * Reads a long array from a single binary value.
+ *
+ * @return Array of longs.
+ */
+ public long @Nullable [] unpackLongArrayAsBinary() {
+ assert refCnt > 0 : "Unpacker is closed";
+
+ if (tryUnpackNil()) {
+ return null;
+ }
+
+ int binSize = unpackBinaryHeader();
+
+ if (binSize % 8 != 0) {
+ throw new MessageFormatException("Binary size should be a multiple
of 8, but was " + binSize);
+ }
+
+ long[] res = new long[binSize / 8];
+
+ for (int i = 0; i < res.length; i++) {
+ res[i] = buf.readLong();
+ }
+
+ return res;
+ }
+
/**
* Unpacks batch of arguments from binary tuples.
*
diff --git
a/modules/client-common/src/main/java/org/apache/ignite/internal/client/proto/ErrorExtensions.java
b/modules/client-common/src/main/java/org/apache/ignite/internal/client/proto/ErrorExtensions.java
index de99dda7b21..c55f121e982 100644
---
a/modules/client-common/src/main/java/org/apache/ignite/internal/client/proto/ErrorExtensions.java
+++
b/modules/client-common/src/main/java/org/apache/ignite/internal/client/proto/ErrorExtensions.java
@@ -23,8 +23,13 @@ package org.apache.ignite.internal.client.proto;
public class ErrorExtensions {
public static final String EXPECTED_SCHEMA_VERSION = "expected-schema-ver";
+ // Problematic array format that can't be skipped by clients.
+ @Deprecated
public static final String SQL_UPDATE_COUNTERS = "sql-update-counters";
+ // New format with single binary value for counters.
+ public static final String SQL_UPDATE_COUNTERS_2 = "sql-update-counters-2";
+
public static final String DELAYED_ACK = "delayed-ack";
public static final String TX_KILL = "tx-kill";
diff --git
a/modules/client-common/src/main/java/org/apache/ignite/internal/client/proto/ProtocolBitmaskFeature.java
b/modules/client-common/src/main/java/org/apache/ignite/internal/client/proto/ProtocolBitmaskFeature.java
index a908678aebc..de7ddd28298 100644
---
a/modules/client-common/src/main/java/org/apache/ignite/internal/client/proto/ProtocolBitmaskFeature.java
+++
b/modules/client-common/src/main/java/org/apache/ignite/internal/client/proto/ProtocolBitmaskFeature.java
@@ -112,7 +112,12 @@ public enum ProtocolBitmaskFeature {
/**
* Send discard requests to directly mapped partitions.
*/
- TX_DIRECT_MAPPING_SEND_DISCARD(17);
+ TX_DIRECT_MAPPING_SEND_DISCARD(17),
+
+ /**
+ * Client supports SQL_UPDATE_COUNTERS_2 error extension (single binary
value instead of array).
+ */
+ SQL_UPDATE_COUNTERS_2(18);
private static final EnumSet<ProtocolBitmaskFeature>
ALL_FEATURES_AS_ENUM_SET =
EnumSet.allOf(ProtocolBitmaskFeature.class);
diff --git
a/modules/client-common/src/test/java/org/apache/ignite/internal/client/proto/ClientMessagePackerTest.java
b/modules/client-common/src/test/java/org/apache/ignite/internal/client/proto/ClientMessagePackerTest.java
index 6f338dadd2d..95fc8193576 100644
---
a/modules/client-common/src/test/java/org/apache/ignite/internal/client/proto/ClientMessagePackerTest.java
+++
b/modules/client-common/src/test/java/org/apache/ignite/internal/client/proto/ClientMessagePackerTest.java
@@ -178,6 +178,46 @@ public class ClientMessagePackerTest {
testPacker(p -> p.writePayload(b, 1, 1), p -> p.writePayload(b, 1, 1));
}
+ @Test
+ public void testPackLongArrayAsBinaryFormat() throws IOException {
+ long[] arr = {1L, 2L, Long.MAX_VALUE, Long.MIN_VALUE};
+ byte[] packed = packIgnite(p -> p.packLongArrayAsBinary(arr));
+
+ try (var unpacker = MessagePack.newDefaultUnpacker(packed)) {
+ int binarySize = unpacker.unpackBinaryHeader();
+ assertEquals(arr.length * 8, binarySize, "Binary size should be
array length * 8 bytes");
+
+ byte[] bytes = unpacker.readPayload(binarySize);
+ ByteBuffer buffer = ByteBuffer.wrap(bytes);
+
+ for (long expected : arr) {
+ long actual = buffer.getLong();
+ assertEquals(expected, actual, "Long value should match");
+ }
+ }
+ }
+
+ @Test
+ public void testPackLongArrayAsBinaryNull() throws IOException {
+ byte[] packed = packIgnite(p -> p.packLongArrayAsBinary(null));
+
+ try (var unpacker = MessagePack.newDefaultUnpacker(packed)) {
+ // Should be nil
+ unpacker.unpackNil();
+ }
+ }
+
+ @Test
+ public void testPackLongArrayAsBinaryEmpty() throws IOException {
+ byte[] packed = packIgnite(p -> p.packLongArrayAsBinary(new long[0]));
+
+ try (var unpacker = MessagePack.newDefaultUnpacker(packed)) {
+ // Should be binary with size 0
+ int binarySize = unpacker.unpackBinaryHeader();
+ assertEquals(0, binarySize);
+ }
+ }
+
private interface MessagePackerConsumer {
void accept(MessagePacker p) throws IOException;
}
diff --git
a/modules/client-common/src/test/java/org/apache/ignite/internal/client/proto/ClientMessagePackerUnpackerTest.java
b/modules/client-common/src/test/java/org/apache/ignite/internal/client/proto/ClientMessagePackerUnpackerTest.java
index 9e486eb29d4..3f8278ab80d 100644
---
a/modules/client-common/src/test/java/org/apache/ignite/internal/client/proto/ClientMessagePackerUnpackerTest.java
+++
b/modules/client-common/src/test/java/org/apache/ignite/internal/client/proto/ClientMessagePackerUnpackerTest.java
@@ -36,6 +36,7 @@ import java.util.BitSet;
import java.util.Random;
import java.util.UUID;
import org.apache.ignite.table.QualifiedName;
+import org.jetbrains.annotations.Nullable;
import org.junit.jupiter.api.Test;
/**
@@ -246,4 +247,50 @@ public class ClientMessagePackerUnpackerTest {
}
}
}
+
+ @Test
+ public void testLongArrayAsBinary() {
+ long[] arr = {1L, 2L, 3L, 100L, 1000L, 10000L};
+ assertArrayEquals(arr, packUnpackLongArrayAsBinary(arr));
+ }
+
+ @Test
+ public void testLongArrayAsBinaryEmpty() {
+ long[] arr = new long[0];
+ long[] res = packUnpackLongArrayAsBinary(arr);
+
+ assertArrayEquals(arr, res);
+ }
+
+ @Test
+ public void testLongArrayAsBinaryNull() {
+ assertNull(packUnpackLongArrayAsBinary(null));
+ }
+
+ @Test
+ public void testLongArrayAsBinarySingleElement() {
+ long[] arr = {42L};
+ assertArrayEquals(arr, packUnpackLongArrayAsBinary(arr));
+ }
+
+ @Test
+ public void testLongArrayAsBinaryMinMaxValues() {
+ long[] arr = {Long.MIN_VALUE, Long.MAX_VALUE, 0L, -1L, 1L};
+ assertArrayEquals(arr, packUnpackLongArrayAsBinary(arr));
+ }
+
+ private static long @Nullable [] packUnpackLongArrayAsBinary(long
@Nullable [] arr) {
+ try (var packer = new
ClientMessagePacker(PooledByteBufAllocator.DEFAULT.directBuffer())) {
+ packer.packLongArrayAsBinary(arr);
+
+ var buf = packer.getBuffer();
+ byte[] data = new byte[buf.readableBytes()];
+ buf.readBytes(data);
+
+ try (var unpacker = new
ClientMessageUnpacker(Unpooled.wrappedBuffer(data))) {
+ unpacker.skipValues(4);
+ return unpacker.unpackLongArrayAsBinary();
+ }
+ }
+ }
}
diff --git
a/modules/client-handler/src/integrationTest/java/org/apache/ignite/client/handler/ItClientHandlerTest.java
b/modules/client-handler/src/integrationTest/java/org/apache/ignite/client/handler/ItClientHandlerTest.java
index 88ecf25bfa8..b87b84c8ef1 100644
---
a/modules/client-handler/src/integrationTest/java/org/apache/ignite/client/handler/ItClientHandlerTest.java
+++
b/modules/client-handler/src/integrationTest/java/org/apache/ignite/client/handler/ItClientHandlerTest.java
@@ -564,6 +564,7 @@ public class ItClientHandlerTest extends
BaseIgniteAbstractTest {
expected.set(15);
expected.set(16);
expected.set(17);
+ expected.set(18);
assertEquals(expected, supportedFeatures);
diff --git
a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientHandlerModule.java
b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientHandlerModule.java
index 802aa726f6d..b7e6a357fc6 100644
---
a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientHandlerModule.java
+++
b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientHandlerModule.java
@@ -102,7 +102,8 @@ public class ClientHandlerModule implements
IgniteComponent, PlatformComputeTran
ProtocolBitmaskFeature.COMPUTE_OBSERVABLE_TS,
ProtocolBitmaskFeature.TX_DIRECT_MAPPING_SEND_REMOTE_WRITES,
ProtocolBitmaskFeature.SQL_PARTITION_AWARENESS_TABLE_NAME,
- ProtocolBitmaskFeature.TX_DIRECT_MAPPING_SEND_DISCARD
+ ProtocolBitmaskFeature.TX_DIRECT_MAPPING_SEND_DISCARD,
+ ProtocolBitmaskFeature.SQL_UPDATE_COUNTERS_2
));
/** Connection id generator.
diff --git
a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientInboundMessageHandler.java
b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientInboundMessageHandler.java
index 16b3a90fbc0..36348e5c90f 100644
---
a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientInboundMessageHandler.java
+++
b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientInboundMessageHandler.java
@@ -21,6 +21,7 @@ import static
org.apache.ignite.internal.client.proto.ProtocolBitmaskFeature.SQL
import static
org.apache.ignite.internal.client.proto.ProtocolBitmaskFeature.SQL_MULTISTATEMENT_SUPPORT;
import static
org.apache.ignite.internal.client.proto.ProtocolBitmaskFeature.SQL_PARTITION_AWARENESS;
import static
org.apache.ignite.internal.client.proto.ProtocolBitmaskFeature.SQL_PARTITION_AWARENESS_TABLE_NAME;
+import static
org.apache.ignite.internal.client.proto.ProtocolBitmaskFeature.SQL_UPDATE_COUNTERS_2;
import static
org.apache.ignite.internal.client.proto.ProtocolBitmaskFeature.STREAMER_RECEIVER_EXECUTION_OPTIONS;
import static
org.apache.ignite.internal.client.proto.ProtocolBitmaskFeature.TX_ALLOW_NOOP_ENLIST;
import static
org.apache.ignite.internal.client.proto.ProtocolBitmaskFeature.TX_CLIENT_GETALL_SUPPORTS_TX_OPTIONS;
@@ -730,23 +731,11 @@ public class ClientInboundMessageHandler
}
private void writeErrorCore(Throwable err, ClientMessagePacker packer) {
- int extCnt = 0;
- boolean retriable = false;
-
SchemaVersionMismatchException schemaVersionMismatchException =
findException(err, SchemaVersionMismatchException.class);
SqlBatchException sqlBatchException = findException(err,
SqlBatchException.class);
DelayedAckException delayedAckException = findException(err,
DelayedAckException.class);
TransactionKilledException killedException = findException(err,
TransactionKilledException.class);
- if (schemaVersionMismatchException != null || sqlBatchException !=
null || delayedAckException != null || killedException != null) {
- extCnt = 1;
- } else {
- retriable = findException(err,
RetriableTransactionException.class) != null;
- if (retriable) {
- extCnt++;
- }
- }
-
err = firstNotNull(
schemaVersionMismatchException,
sqlBatchException,
@@ -781,7 +770,18 @@ public class ClientInboundMessageHandler
}
// Extensions.
+ int extCnt = 0;
+ if (schemaVersionMismatchException != null || sqlBatchException !=
null || delayedAckException != null || killedException != null) {
+ extCnt++;
+ }
+
+ var retriable = findException(err,
RetriableTransactionException.class) != null;
+ if (retriable) {
+ extCnt++;
+ }
+
if (extCnt > 0) {
+ // IMPORTANT: every extension must be a single msgpack value, so
that the client can skip unknown values.
packer.packInt(extCnt);
if (retriable) {
@@ -793,9 +793,16 @@ public class ClientInboundMessageHandler
packer.packString(ErrorExtensions.EXPECTED_SCHEMA_VERSION);
packer.packInt(schemaVersionMismatchException.expectedVersion());
} else if (sqlBatchException != null) {
- // TODO IGNITE-28012 SQL_UPDATE_COUNTERS is an array and must
come last
- packer.packString(ErrorExtensions.SQL_UPDATE_COUNTERS);
- packer.packLongArray(sqlBatchException.updateCounters());
+ if (clientContext.hasFeature(SQL_UPDATE_COUNTERS_2)) {
+ // New format: single binary value (protocol-compliant).
+ packer.packString(ErrorExtensions.SQL_UPDATE_COUNTERS_2);
+
packer.packLongArrayAsBinary(sqlBatchException.updateCounters());
+ } else {
+ // Old format: array of longs (for backward compatibility
with older clients).
+ // IMPORTANT: This part must come last in the payload, it
can't be skipped correctly.
+ packer.packString(ErrorExtensions.SQL_UPDATE_COUNTERS);
+ packer.packLongArray(sqlBatchException.updateCounters());
+ }
} else if (delayedAckException != null) {
packer.packString(ErrorExtensions.DELAYED_ACK);
packer.packUuid(delayedAckException.txId());
diff --git
a/modules/client/src/main/java/org/apache/ignite/internal/client/TcpClientChannel.java
b/modules/client/src/main/java/org/apache/ignite/internal/client/TcpClientChannel.java
index bbc257ff6d8..5c198f1da98 100644
---
a/modules/client/src/main/java/org/apache/ignite/internal/client/TcpClientChannel.java
+++
b/modules/client/src/main/java/org/apache/ignite/internal/client/TcpClientChannel.java
@@ -105,7 +105,8 @@ class TcpClientChannel implements ClientChannel,
ClientMessageHandler, ClientCon
ProtocolBitmaskFeature.SQL_MULTISTATEMENT_SUPPORT,
ProtocolBitmaskFeature.COMPUTE_OBSERVABLE_TS,
ProtocolBitmaskFeature.TX_DIRECT_MAPPING_SEND_REMOTE_WRITES,
- ProtocolBitmaskFeature.TX_DIRECT_MAPPING_SEND_DISCARD
+ ProtocolBitmaskFeature.TX_DIRECT_MAPPING_SEND_DISCARD,
+ ProtocolBitmaskFeature.SQL_UPDATE_COUNTERS_2
));
/** Minimum supported heartbeat interval. */
@@ -659,8 +660,12 @@ class TcpClientChannel implements ClientChannel,
ClientMessageHandler, ClientCon
if (key.equals(ErrorExtensions.EXPECTED_SCHEMA_VERSION)) {
expectedSchemaVersion = unpacker.unpackInt();
} else if (key.equals(ErrorExtensions.SQL_UPDATE_COUNTERS)) {
+ // Deprecated format, keep for compat with older servers.
return new SqlBatchException(traceId, code,
unpacker.unpackLongArray(),
errMsg != null ? errMsg : "SQL batch execution error",
causeWithStackTrace);
+ } else if (key.equals(ErrorExtensions.SQL_UPDATE_COUNTERS_2)) {
+ return new SqlBatchException(traceId, code,
unpacker.unpackLongArrayAsBinary(),
+ errMsg != null ? errMsg : "SQL batch execution error",
causeWithStackTrace);
} else if (key.equals(ErrorExtensions.DELAYED_ACK)) {
return new ClientDelayedAckException(traceId, code, errMsg,
unpacker.unpackUuid(), causeWithStackTrace);
} else if (key.equals(ErrorExtensions.TX_KILL)) {
diff --git
a/modules/compatibility-tests/src/integrationTest/java/org/apache/ignite/internal/client/ClientCompatibilityTests.java
b/modules/compatibility-tests/src/integrationTest/java/org/apache/ignite/internal/client/ClientCompatibilityTests.java
index be63b944d76..7f60fd20070 100644
---
a/modules/compatibility-tests/src/integrationTest/java/org/apache/ignite/internal/client/ClientCompatibilityTests.java
+++
b/modules/compatibility-tests/src/integrationTest/java/org/apache/ignite/internal/client/ClientCompatibilityTests.java
@@ -60,7 +60,9 @@ import org.apache.ignite.internal.jobs.DeploymentUtils;
import org.apache.ignite.network.ClusterNode;
import org.apache.ignite.sql.BatchedArguments;
import org.apache.ignite.sql.ColumnMetadata;
+import org.apache.ignite.sql.IgniteSql;
import org.apache.ignite.sql.ResultSetMetadata;
+import org.apache.ignite.sql.SqlBatchException;
import org.apache.ignite.sql.SqlRow;
import org.apache.ignite.sql.Statement;
import org.apache.ignite.table.DataStreamerItem;
@@ -236,6 +238,48 @@ public interface ClientCompatibilityTests {
assertEquals(2, rows.size());
}
+ @Test
+ default void testSqlBatchException() {
+ IgniteSql sql = client().sql();
+
+ // Insert initial row to trigger constraint violation
+ int duplicateId = idGen().incrementAndGet();
+ sql.execute((Transaction) null, "INSERT INTO " + TABLE_NAME_TEST + "
(id, name) VALUES (?, ?)", duplicateId, "initial");
+
+ // Create batch where some rows will succeed and one will fail with
duplicate key
+ int id1 = idGen().incrementAndGet();
+ int id2 = idGen().incrementAndGet();
+ int id3 = idGen().incrementAndGet();
+ int id4 = idGen().incrementAndGet();
+
+ BatchedArguments args = BatchedArguments.create()
+ .add(id1, "test1")
+ .add(id2, "test2")
+ .add(id3, "test3")
+ .add(duplicateId, "duplicate") // This will fail - duplicate
primary key
+ .add(id4, "test4"); // This won't be executed due to error
+
+ var ex = assertThrows(
+ SqlBatchException.class,
+ () -> sql.executeBatch(null, "INSERT INTO " + TABLE_NAME_TEST
+ " (id, name) VALUES (?, ?)", args));
+
+ // Verify error extensions: update counters should reflect 3
successful inserts before the error
+ assertEquals(3, ex.updateCounters().length, "Expected 3 successful
updates before error");
+
+ // Verify all successful inserts have counter = 1
+ for (long counter : ex.updateCounters()) {
+ assertEquals(1, counter, "Each successful insert should have
update count = 1");
+ }
+
+ // Verify the successful rows were actually inserted
+ List<SqlRow> rows = sql("SELECT * FROM " + TABLE_NAME_TEST + " WHERE
id IN (?, ?, ?)", id1, id2, id3);
+ assertEquals(3, rows.size(), "The 3 rows before the error should have
been inserted");
+
+ // Verify the row after the error was not inserted
+ List<SqlRow> notInserted = sql("SELECT * FROM " + TABLE_NAME_TEST + "
WHERE id = ?", id4);
+ assertEquals(0, notInserted.size(), "Row after the error should not
have been inserted");
+ }
+
@Test
default void testRecordViewOperations() {
int id = idGen().incrementAndGet();
diff --git
a/modules/compatibility-tests/src/integrationTest/java/org/apache/ignite/internal/client/OldClientWithCurrentServerCompatibilityTest.java
b/modules/compatibility-tests/src/integrationTest/java/org/apache/ignite/internal/client/OldClientWithCurrentServerCompatibilityTest.java
index 0e546f9a8a4..8f1afb5086d 100644
---
a/modules/compatibility-tests/src/integrationTest/java/org/apache/ignite/internal/client/OldClientWithCurrentServerCompatibilityTest.java
+++
b/modules/compatibility-tests/src/integrationTest/java/org/apache/ignite/internal/client/OldClientWithCurrentServerCompatibilityTest.java
@@ -167,6 +167,15 @@ public class OldClientWithCurrentServerCompatibilityTest
extends BaseIgniteAbstr
delegate.testSqlBatch();
}
+ @Test
+ @Override
+ public void testSqlBatchException() {
+ // Old clients don't support SQL_UPDATE_COUNTERS_2 extension,
+ // but they should still be able to receive and process
SqlBatchException.
+ // The updateCounters() will return null for old clients.
+ delegate.testSqlBatchException();
+ }
+
@Test
@Override
public void testRecordViewOperations() {
diff --git a/modules/platforms/cpp/ignite/protocol/bitmask_feature.h
b/modules/platforms/cpp/ignite/protocol/bitmask_feature.h
index 41bdaea0a7f..6edac29641a 100644
--- a/modules/platforms/cpp/ignite/protocol/bitmask_feature.h
+++ b/modules/platforms/cpp/ignite/protocol/bitmask_feature.h
@@ -30,6 +30,9 @@ namespace ignite::protocol {
enum class bitmask_feature {
/** Qualified name table requests. */
TABLE_REQS_USE_QUALIFIED_NAME = 2,
+
+ /** SQL_UPDATE_COUNTERS_2 error extension (single binary value instead of
array). */
+ SQL_UPDATE_COUNTERS_2 = 18,
};
/**
@@ -38,10 +41,11 @@ enum class bitmask_feature {
* @return Return all supported bitmask features in binary form.
*/
inline std::vector<std::byte> all_supported_bitmask_features() {
- std::vector<std::byte> res(1, std::byte{0});
+ std::vector<std::byte> res(3, std::byte{0});
bitset_span span(res.data(), res.size());
span.set(static_cast<std::size_t>(bitmask_feature::TABLE_REQS_USE_QUALIFIED_NAME));
+ span.set(static_cast<std::size_t>(bitmask_feature::SQL_UPDATE_COUNTERS_2));
return res;
}
diff --git a/modules/platforms/cpp/ignite/protocol/reader.h
b/modules/platforms/cpp/ignite/protocol/reader.h
index 9c57eefc96c..3aceb99b170 100644
--- a/modules/platforms/cpp/ignite/protocol/reader.h
+++ b/modules/platforms/cpp/ignite/protocol/reader.h
@@ -18,6 +18,7 @@
#pragma once
#include <ignite/common/bytes_view.h>
+#include <ignite/common/detail/bytes.h>
#include <ignite/common/ignite_error.h>
#include <ignite/common/uuid.h>
#include <ignite/protocol/utils.h>
@@ -235,6 +236,45 @@ public:
return read_int64_array();
}
+ /**
+ * Read array of int64 from binary data.
+ * Compatible with Java's ClientMessageUnpacker.unpackLongArrayAsBinary.
+ *
+ * @return Vector of int64 values.
+ */
+ [[nodiscard]] std::vector<std::int64_t> read_int64_array_from_binary() {
+ auto binary_data = read_binary();
+
+ if (binary_data.size() % 8 != 0) {
+ throw ignite_error("Binary data size must be a multiple of 8, but
was " + std::to_string(binary_data.size()));
+ }
+
+ std::size_t count = binary_data.size() / 8;
+ std::vector<std::int64_t> result;
+ result.reserve(count);
+
+ for (std::size_t i = 0; i < count; ++i) {
+ std::int64_t value = detail::bytes::load<detail::endian::BIG,
std::int64_t>(
+ binary_data.data() + i * 8);
+ result.push_back(value);
+ }
+
+ return result;
+ }
+
+ /**
+ * Read array of int64 from binary data, or nullopt if nil.
+ *
+ * @return Vector of int64 values or nullopt.
+ */
+ [[nodiscard]] std::optional<std::vector<std::int64_t>>
read_int64_array_from_binary_nullable() {
+ if (try_read_nil()) {
+ return std::nullopt;
+ }
+
+ return read_int64_array_from_binary();
+ }
+
/**
* Read int32 or nullopt.
*
diff --git a/modules/platforms/cpp/ignite/protocol/utils.cpp
b/modules/platforms/cpp/ignite/protocol/utils.cpp
index 65e17337ad8..2663bab7ae6 100644
--- a/modules/platforms/cpp/ignite/protocol/utils.cpp
+++ b/modules/platforms/cpp/ignite/protocol/utils.cpp
@@ -223,8 +223,14 @@ ignite_error read_error(reader &reader) {
auto ver = reader.read_int32();
res.add_extra<std::int32_t>(std::move(key), ver);
} else if (key == error_extensions::SQL_UPDATE_COUNTERS) {
+ // Deprecated format: array of int64 values. Keep for
compatibility with older servers.
auto affected_rows = reader.read_int64_array();
res.add_extra<std::vector<std::int64_t>>(std::move(key),
std::move(affected_rows));
+ } else if (key == error_extensions::SQL_UPDATE_COUNTERS_2) {
+ // New format: single binary value containing int64 array.
+ auto affected_rows = reader.read_int64_array_from_binary();
+ // Store under the old key for backward compatibility with
existing code.
+
res.add_extra<std::vector<std::int64_t>>(error_extensions::SQL_UPDATE_COUNTERS,
std::move(affected_rows));
} else {
reader.skip();
}
diff --git a/modules/platforms/cpp/ignite/protocol/utils.h
b/modules/platforms/cpp/ignite/protocol/utils.h
index e505896e484..25b9a753367 100644
--- a/modules/platforms/cpp/ignite/protocol/utils.h
+++ b/modules/platforms/cpp/ignite/protocol/utils.h
@@ -44,6 +44,7 @@ class reader;
namespace error_extensions {
constexpr const char* EXPECTED_SCHEMA_VERSION = "expected-schema-ver";
constexpr const char* SQL_UPDATE_COUNTERS = "sql-update-counters";
+constexpr const char* SQL_UPDATE_COUNTERS_2 = "sql-update-counters-2";
};
/** Magic bytes. */
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/SqlTests.cs
b/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/SqlTests.cs
index b5bd59599da..09a1956da44 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/SqlTests.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/SqlTests.cs
@@ -764,6 +764,28 @@ namespace Apache.Ignite.Tests.Sql
Assert.AreEqual("Statement of type \"Query\" is not allowed in
current context [allowedTypes=[DML]].", ex.Message);
}
+ [Test]
+ public async Task TestExecuteBatchWithDuplicateKeyException()
+ {
+ int duplicateId = 1000;
+ var sql = "INSERT INTO TEST (ID, VAL) VALUES (?, ?)";
+ await Client.Sql.ExecuteAsync(null, sql, duplicateId, "initial");
+
+ object?[][] args =
+ [
+ [1001, "test1"],
+ [1002, "test2"],
+ [1003, "test3"],
+ [duplicateId, "duplicate"],
+ [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);
+ }
+
[Test]
public async Task TestCancelQueryCursor([Values(true, false)] bool
beforeIter)
{
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/ClientSocket.cs
b/modules/platforms/dotnet/Apache.Ignite/Internal/ClientSocket.cs
index 06ceed22d0d..8d41b343537 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/ClientSocket.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/ClientSocket.cs
@@ -54,7 +54,8 @@ namespace Apache.Ignite.Internal
ProtocolBitmaskFeature.StreamerReceiverExecutionOptions |
ProtocolBitmaskFeature.SqlPartitionAwareness |
ProtocolBitmaskFeature.SqlPartitionAwarenessTableName |
- ProtocolBitmaskFeature.ComputeObservableTs;
+ ProtocolBitmaskFeature.ComputeObservableTs |
+ ProtocolBitmaskFeature.SqlUpdateCounters2;
/** Features as a byte array */
private static readonly byte[] FeatureBytes = Features.ToBytes();
@@ -482,6 +483,8 @@ namespace Apache.Ignite.Internal
}
}
+ Debug.Assert(reader.End, "All error response bytes should be
consumed.");
+
return ex;
}
diff --git
a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/ProtocolBitmaskFeature.cs
b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/ProtocolBitmaskFeature.cs
index e516ec318c5..88114696baf 100644
---
a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/ProtocolBitmaskFeature.cs
+++
b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/ProtocolBitmaskFeature.cs
@@ -68,5 +68,10 @@ internal enum ProtocolBitmaskFeature
/// <summary>
/// Partition awareness for SQL requests with table name in metadata.
/// </summary>
- SqlPartitionAwarenessTableName = 1 << 16
+ SqlPartitionAwarenessTableName = 1 << 16,
+
+ /// <summary>
+ /// SQL_UPDATE_COUNTERS_2 error extension (single binary value instead of
array).
+ /// </summary>
+ SqlUpdateCounters2 = 1 << 18
}