From f6af5fad5c04df52b65e15d424aa9def0e8330c2 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Thu, 8 Oct 2026 16:42:16 -0700 Subject: [PATCH 1/3] Fixed transaction compatibility of V1 with latest ClickHouse version --- .github/workflows/test_head.yml | 2 +- .../client/ClickHouseTransaction.java | 60 +++++++-- .../client/ClickHouseTransactionTest.java | 121 ++++++++++++++++++ 3 files changed, 168 insertions(+), 15 deletions(-) create mode 100644 clickhouse-client/src/test/java/com/clickhouse/client/ClickHouseTransactionTest.java diff --git a/.github/workflows/test_head.yml b/.github/workflows/test_head.yml index 53a039f47..4303f6cb3 100644 --- a/.github/workflows/test_head.yml +++ b/.github/workflows/test_head.yml @@ -56,7 +56,7 @@ jobs: strategy: matrix: protocol: ["http"] - r2dbc: ["1.0.0.RELEASE", "0.9.1.RELEASE"] + r2dbc: ["1.0.0.RELEASE"] fail-fast: false timeout-minutes: 10 name: R2DBC ${{ matrix.r2dbc }} + CH HEAD (${{ matrix.protocol }}) diff --git a/clickhouse-client/src/main/java/com/clickhouse/client/ClickHouseTransaction.java b/clickhouse-client/src/main/java/com/clickhouse/client/ClickHouseTransaction.java index c16963508..c8507cfd0 100644 --- a/clickhouse-client/src/main/java/com/clickhouse/client/ClickHouseTransaction.java +++ b/clickhouse-client/src/main/java/com/clickhouse/client/ClickHouseTransaction.java @@ -5,6 +5,8 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; @@ -12,7 +14,6 @@ import com.clickhouse.data.ClickHouseFormat; import com.clickhouse.data.ClickHouseRecord; import com.clickhouse.data.ClickHouseUtils; -import com.clickhouse.data.value.UnsignedLong; import com.clickhouse.logging.Logger; import com.clickhouse.logging.LoggerFactory; @@ -26,42 +27,60 @@ public final class ClickHouseTransaction implements Serializable { /** * This class encapsulates transaction ID, which is defined as - * {@code Tuple(snapshotVersion UInt64, localTxCounter UInt64, hostId UUID)}. + * {@code Tuple(snapshotVersion UInt64, localTxCounter UInt64, hostId UUID[, sessionNodeVersion Int64])}. */ public static class XID implements Serializable { private static final long serialVersionUID = 4907177669971332404L; public static final XID EMPTY = new XID(0L, 0L, new UUID(0L, 0L).toString()); + private static long toLong(Object value) { + if (value instanceof Number) { + return ((Number) value).longValue(); + } else if (value != null) { + return Long.parseLong(value.toString()); + } + throw new IllegalArgumentException("Non-null number is required"); + } + /** * Creates transaction ID from the given tuple. * - * @param list non-null tuple with 3 elements + * @param list non-null tuple with 3 or 4 elements * @return non-null transaction ID */ public static XID of(List list) { - if (list == null || list.size() != 3) { + if (list == null || (list.size() != 3 && list.size() != 4)) { throw new IllegalArgumentException( - "Non-null tuple with 3 elements(long, long, String) is required"); + "Non-null tuple with 3 or 4 elements(long, long, String[, long]) is required"); } - long snapshotVersion = ((UnsignedLong) list.get(0)).longValue(); - long localTxCounter = ((UnsignedLong) list.get(1)).longValue(); + long snapshotVersion = toLong(list.get(0)); + long localTxCounter = toLong(list.get(1)); String hostId = String.valueOf(list.get(2)); + Long sessionNodeVersion = list.size() == 4 && list.get(3) != null + ? toLong(list.get(3)) + : null; if (EMPTY.snapshotVersion == snapshotVersion && EMPTY.localTxCounter == localTxCounter && EMPTY.hostId.equals(hostId)) { return EMPTY; } - return new XID(snapshotVersion, localTxCounter, hostId); + return new XID(snapshotVersion, localTxCounter, hostId, sessionNodeVersion); } private final long snapshotVersion; private final long localTxCounter; private final String hostId; + private final Long sessionNodeVersion; protected XID(long snapshotVersion, long localTxCounter, String hostId) { + this(snapshotVersion, localTxCounter, hostId, null); + } + + protected XID(long snapshotVersion, long localTxCounter, String hostId, Long sessionNodeVersion) { this.snapshotVersion = snapshotVersion; this.localTxCounter = localTxCounter; this.hostId = hostId; + this.sessionNodeVersion = sessionNodeVersion; } public long getSnapshotVersion() { @@ -76,9 +95,17 @@ public String getHostId() { return hostId; } + public Optional getSessionNodeVersion() { + return Optional.ofNullable(sessionNodeVersion); + } + public String asTupleString() { - return new StringBuilder().append('(').append(snapshotVersion).append(',').append(localTxCounter) - .append(",'").append(hostId).append("')").toString(); + StringBuilder sb = new StringBuilder().append('(').append(snapshotVersion).append(',').append(localTxCounter) + .append(",'").append(hostId).append('\''); + if (sessionNodeVersion != null) { + sb.append(',').append(sessionNodeVersion); + } + return sb.append(')').toString(); } @Override @@ -87,6 +114,7 @@ public int hashCode() { int result = prime + (int) (snapshotVersion ^ (snapshotVersion >>> 32)); result = prime * result + (int) (localTxCounter ^ (localTxCounter >>> 32)); result = prime * result + hostId.hashCode(); + result = prime * result + (sessionNodeVersion != null ? sessionNodeVersion.hashCode() : 0); return result; } @@ -100,14 +128,18 @@ public boolean equals(Object obj) { XID other = (XID) obj; return snapshotVersion == other.snapshotVersion && localTxCounter == other.localTxCounter - && hostId.equals(other.hostId); + && hostId.equals(other.hostId) + && Objects.equals(sessionNodeVersion, other.sessionNodeVersion); } @Override public String toString() { - return new StringBuilder().append("TransactionId [snapshotVersion=").append(snapshotVersion) - .append(", localTxCounter=").append(localTxCounter).append(", hostId=").append(hostId).append("]@") - .append(hashCode()).toString(); + StringBuilder sb = new StringBuilder().append("TransactionId [snapshotVersion=").append(snapshotVersion) + .append(", localTxCounter=").append(localTxCounter).append(", hostId=").append(hostId); + if (sessionNodeVersion != null) { + sb.append(", sessionNodeVersion=").append(sessionNodeVersion); + } + return sb.append("]@").append(hashCode()).toString(); } } diff --git a/clickhouse-client/src/test/java/com/clickhouse/client/ClickHouseTransactionTest.java b/clickhouse-client/src/test/java/com/clickhouse/client/ClickHouseTransactionTest.java new file mode 100644 index 000000000..86b0fe607 --- /dev/null +++ b/clickhouse-client/src/test/java/com/clickhouse/client/ClickHouseTransactionTest.java @@ -0,0 +1,121 @@ +package com.clickhouse.client; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import com.clickhouse.client.ClickHouseTransaction.XID; +import com.clickhouse.data.value.UnsignedLong; + +@SuppressWarnings("deprecation") +public class ClickHouseTransactionTest { + @DataProvider(name = "validTuples") + public Object[][] getValidTuples() { + return new Object[][] { + { Arrays.asList(UnsignedLong.valueOf(10L), UnsignedLong.valueOf(20L), "host-uuid"), 10L, 20L, + "host-uuid", null, "(10,20,'host-uuid')" }, + { Arrays.asList(15L, 25L, UUID.fromString("00000000-0000-0000-0000-000000000001"), 5L), 15L, 25L, + "00000000-0000-0000-0000-000000000001", 5L, + "(15,25,'00000000-0000-0000-0000-000000000001',5)" }, + { Arrays.asList(UnsignedLong.valueOf(100L), UnsignedLong.valueOf(200L), "host-4", 0L), 100L, 200L, + "host-4", 0L, "(100,200,'host-4',0)" }, + { Arrays.asList(1L, 2L, "host-null-version", null), 1L, 2L, "host-null-version", null, + "(1,2,'host-null-version')" }, + }; + } + + @Test(groups = "unit", dataProvider = "validTuples") + public void testParseValidXid(List list, long expectedSnapshot, long expectedCounter, String expectedHost, + Long expectedSessionVersion, String expectedTupleString) { + XID xid = XID.of(list); + Assert.assertEquals(xid.getSnapshotVersion(), expectedSnapshot); + Assert.assertEquals(xid.getLocalTransactionCounter(), expectedCounter); + Assert.assertEquals(xid.getHostId(), expectedHost); + if (expectedSessionVersion != null) { + Assert.assertTrue(xid.getSessionNodeVersion().isPresent()); + Assert.assertEquals(xid.getSessionNodeVersion().get(), expectedSessionVersion); + } else { + Assert.assertFalse(xid.getSessionNodeVersion().isPresent()); + } + Assert.assertEquals(xid.asTupleString(), expectedTupleString); + } + + @DataProvider(name = "emptyTuples") + public Object[][] getEmptyTuples() { + return new Object[][] { + { Arrays.asList(UnsignedLong.valueOf(0L), UnsignedLong.valueOf(0L), + "00000000-0000-0000-0000-000000000000") }, + { Arrays.asList(0L, 0L, UUID.fromString("00000000-0000-0000-0000-000000000000"), 0L) }, + { Arrays.asList(0L, 0L, "00000000-0000-0000-0000-000000000000", 1L) }, + }; + } + + @Test(groups = "unit", dataProvider = "emptyTuples") + public void testParseEmptyXid(List list) { + XID xid = XID.of(list); + Assert.assertSame(xid, XID.EMPTY); + Assert.assertEquals(xid, XID.EMPTY); + } + + @DataProvider(name = "invalidTuples") + public Object[][] getInvalidTuples() { + return new Object[][] { + { null }, + { Collections.emptyList() }, + { Collections.singletonList(1L) }, + { Arrays.asList(1L, 2L) }, + { Arrays.asList(1L, 2L, "host", 0L, "extra") }, + }; + } + + @Test(groups = "unit", dataProvider = "invalidTuples", expectedExceptions = IllegalArgumentException.class) + public void testParseInvalidXid(List list) { + XID.of(list); + } + + @Test(groups = "unit") + public void testEqualsAndHashCode() { + XID xid3a = XID.of(Arrays.asList(1L, 2L, "host1")); + XID xid3b = XID.of(Arrays.asList(1L, 2L, "host1")); + XID xid3c = XID.of(Arrays.asList(1L, 3L, "host1")); + + XID xid4a = XID.of(Arrays.asList(1L, 2L, "host1", 10L)); + XID xid4b = XID.of(Arrays.asList(1L, 2L, "host1", 10L)); + XID xid4c = XID.of(Arrays.asList(1L, 2L, "host1", 20L)); + + Assert.assertEquals(xid3a, xid3a); + Assert.assertEquals(xid3a, xid3b); + Assert.assertEquals(xid3a.hashCode(), xid3b.hashCode()); + Assert.assertNotEquals(xid3a, xid3c); + + Assert.assertEquals(xid4a, xid4a); + Assert.assertEquals(xid4a, xid4b); + Assert.assertEquals(xid4a.hashCode(), xid4b.hashCode()); + Assert.assertNotEquals(xid4a, xid4c); + + Assert.assertNotEquals(xid3a, xid4a); + Assert.assertNotEquals(xid4a, xid3a); + Assert.assertNotEquals(xid4a, null); + Assert.assertNotEquals(xid4a, "(1,2,'host1',10)"); + } + + @Test(groups = "unit") + public void testToString() { + XID xid3 = XID.of(Arrays.asList(1L, 2L, "host1")); + Assert.assertTrue(xid3.toString().contains("snapshotVersion=1")); + Assert.assertTrue(xid3.toString().contains("localTxCounter=2")); + Assert.assertTrue(xid3.toString().contains("hostId=host1")); + Assert.assertFalse(xid3.toString().contains("sessionNodeVersion=")); + + XID xid4 = XID.of(Arrays.asList(1L, 2L, "host1", 5L)); + Assert.assertTrue(xid4.toString().contains("snapshotVersion=1")); + Assert.assertTrue(xid4.toString().contains("localTxCounter=2")); + Assert.assertTrue(xid4.toString().contains("hostId=host1")); + Assert.assertTrue(xid4.toString().contains("sessionNodeVersion=5")); + } +} From 999df25fe8935e2e811699d490ee3aef3093888f Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Thu, 8 Oct 2026 16:43:46 -0700 Subject: [PATCH 2/3] Fixed test for cluster env --- .../src/test/java/com/clickhouse/jdbc/JdbcDataTypeTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcDataTypeTests.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcDataTypeTests.java index e44a16993..b169f1e59 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcDataTypeTests.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcDataTypeTests.java @@ -1906,7 +1906,7 @@ public void testArrayOfNestedType() throws SQLException { + "aan Array(Array(Nested(a Int8, b Nullable(String)))), " + "aat Array(Array(Tuple(a Int8, b Nullable(String)))), " + "tail Int32" - + ") ENGINE = MergeTree ORDER BY (order)"); + + ") ORDER BY (order)"); runQuery("INSERT INTO test_array_of_nested_jdbc VALUES " + "(1, [[(1, 'x'), (2, NULL)], [], [(3, 'y')]], [[[(1, 'x'), (2, NULL)]], [], [[], [(3, 'y')]]], " + "[[(1, 'x'), (2, NULL)], [], [(3, 'y')]], 100), " From 4d24048c7038dca4086056e4496981a032382a68 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Thu, 8 Oct 2026 16:49:17 -0700 Subject: [PATCH 3/3] made test on CH head manual --- .github/workflows/test_head.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test_head.yml b/.github/workflows/test_head.yml index 4303f6cb3..a7012ec2c 100644 --- a/.github/workflows/test_head.yml +++ b/.github/workflows/test_head.yml @@ -3,6 +3,7 @@ name: Test with CH HEAD on: schedule: - cron: "55 10 * * *" + workflow_dispatch: push: branches: - main