|
4 | 4 | import com.clickhouse.data.ClickHouseDataType; |
5 | 5 | import com.clickhouse.data.value.ClickHouseGeoPolygonValue; |
6 | 6 | import org.testng.Assert; |
| 7 | +import org.testng.annotations.DataProvider; |
7 | 8 | import org.testng.annotations.Test; |
8 | 9 |
|
9 | 10 | import java.io.ByteArrayInputStream; |
10 | 11 | import java.io.ByteArrayOutputStream; |
| 12 | +import java.math.BigDecimal; |
| 13 | +import java.math.BigInteger; |
| 14 | +import java.net.InetAddress; |
| 15 | +import java.time.LocalDate; |
| 16 | +import java.util.ArrayList; |
11 | 17 | import java.util.Arrays; |
| 18 | +import java.util.LinkedHashMap; |
12 | 19 | import java.util.List; |
| 20 | +import java.util.Map; |
13 | 21 | import java.util.TimeZone; |
| 22 | +import java.util.UUID; |
14 | 23 |
|
15 | 24 | @Test(groups = {"unit"}) |
16 | 25 | public class SerializerUtilsTest { |
@@ -134,6 +143,115 @@ public void testGeometrySerializationRejectsMalformedList() { |
134 | 143 | ClickHouseColumn.of("v", "Geometry"))); |
135 | 144 | } |
136 | 145 |
|
| 146 | + @Test(dataProvider = "nestedNullableData") |
| 147 | + public void testNestedNullableRoundTrip(String typeName, Object value) throws Exception { |
| 148 | + ClickHouseColumn column = ClickHouseColumn.of("v", typeName); |
| 149 | + |
| 150 | + ByteArrayOutputStream out = new ByteArrayOutputStream(); |
| 151 | + SerializerUtils.serializeData(out, value, column); |
| 152 | + |
| 153 | + Object actual = newReader(out.toByteArray()).readValue(column); |
| 154 | + Assert.assertEquals(normalize(actual), normalize(value)); |
| 155 | + } |
| 156 | + |
| 157 | + @DataProvider(name = "nestedNullableData") |
| 158 | + private Object[][] nestedNullableData() throws Exception { |
| 159 | + UUID uuid = UUID.fromString("61f0c404-5cb3-11e7-907b-a6006ad3dba0"); |
| 160 | + InetAddress ipv4 = InetAddress.getByName("1.2.3.4"); |
| 161 | + return new Object[][] { |
| 162 | + // Each present Nullable element sits in the MIDDLE of the schema: a non-nullable |
| 163 | + // leading column, the Nullable, then a trailing non-nullable Float64. If the |
| 164 | + // present-marker byte is dropped, every following byte shifts and the trailing |
| 165 | + // Float64 reads a wrong value, so a faulty serialization is detected positionally |
| 166 | + // rather than only by running out of bytes. The assertion compares the whole row. |
| 167 | + {"Tuple(Int32, Nullable(String), Float64)", Arrays.asList(7, "opt", 9.5d)}, |
| 168 | + {"Tuple(Int32, Nullable(FixedString(3)), Float64)", Arrays.asList(7, "abc", 9.5d)}, |
| 169 | + {"Tuple(Int32, Nullable(Int8), Float64)", Arrays.asList(7, (byte) -5, 9.5d)}, |
| 170 | + {"Tuple(Int32, Nullable(UInt8), Float64)", Arrays.asList(7, (short) 200, 9.5d)}, |
| 171 | + {"Tuple(Int32, Nullable(Int16), Float64)", Arrays.asList(7, (short) -1600, 9.5d)}, |
| 172 | + {"Tuple(Int32, Nullable(UInt16), Float64)", Arrays.asList(7, 40000, 9.5d)}, |
| 173 | + {"Tuple(Int32, Nullable(Int32), Float64)", Arrays.asList(7, 42, 9.5d)}, |
| 174 | + {"Tuple(Int32, Nullable(UInt32), Float64)", Arrays.asList(7, 4_000_000_000L, 9.5d)}, |
| 175 | + {"Tuple(Int32, Nullable(Int64), Float64)", Arrays.asList(7, -64L, 9.5d)}, |
| 176 | + {"Tuple(Int32, Nullable(UInt64), Float64)", Arrays.asList(7, BigInteger.valueOf(64), 9.5d)}, |
| 177 | + {"Tuple(Int32, Nullable(Float32), Float64)", Arrays.asList(7, 1.5f, 9.5d)}, |
| 178 | + {"Tuple(Int32, Nullable(Float64), Float64)", Arrays.asList(7, 2.5d, 9.5d)}, |
| 179 | + {"Tuple(Int32, Nullable(Bool), Float64)", Arrays.asList(7, true, 9.5d)}, |
| 180 | + {"Tuple(Int32, Nullable(UUID), Float64)", Arrays.asList(7, uuid, 9.5d)}, |
| 181 | + {"Tuple(Int32, Nullable(Date), Float64)", Arrays.asList(7, LocalDate.of(2021, 2, 3), 9.5d)}, |
| 182 | + {"Tuple(Int32, Nullable(Decimal64(4)), Float64)", Arrays.asList(7, new BigDecimal("1.2345"), 9.5d)}, |
| 183 | + {"Tuple(Int32, Nullable(IPv4), Float64)", Arrays.asList(7, ipv4, 9.5d)}, |
| 184 | + |
| 185 | + // A Tuple value given as a Java array (not a List) takes the other branch of |
| 186 | + // serializeTupleData, which is routed through the same nested-marker path, for |
| 187 | + // both a present value and a null. |
| 188 | + {"Tuple(Int32, Nullable(String), Float64)", new Object[] {7, "opt", 9.5d}}, |
| 189 | + {"Tuple(Int32, Nullable(String), Float64)", new Object[] {7, null, 9.5d}}, |
| 190 | + |
| 191 | + // The Map value path: the Nullable map value sits between the key and a trailing |
| 192 | + // Float64, so a dropped value-marker misaligns the float. |
| 193 | + {"Tuple(Int32, Map(String, Nullable(String)), Float64)", Arrays.asList(7, newMap("k", "v"), 9.5d)}, |
| 194 | + {"Tuple(Int32, Map(String, Nullable(Int32)), Float64)", Arrays.asList(7, newMap("k", 32), 9.5d)}, |
| 195 | + {"Tuple(Int32, Map(String, Nullable(Float64)), Float64)", Arrays.asList(7, newMap("k", 2.5d), 9.5d)}, |
| 196 | + {"Tuple(Int32, Map(String, Nullable(UUID)), Float64)", Arrays.asList(7, newMap("k", uuid), 9.5d)}, |
| 197 | + |
| 198 | + // Null elements/values still serialize a single null-marker byte; the trailing |
| 199 | + // Float64 confirms the following data stays aligned. |
| 200 | + {"Tuple(Int32, Nullable(String), Float64)", Arrays.asList(7, null, 9.5d)}, |
| 201 | + {"Tuple(Int32, Nullable(Int32), Nullable(String), Float64)", Arrays.asList(7, null, null, 9.5d)}, |
| 202 | + {"Tuple(Int32, Map(String, Nullable(String)), Float64)", Arrays.asList(7, newMap("k", null), 9.5d)}, |
| 203 | + |
| 204 | + // Containers compose: marker handling threads through nested Tuple/Map/Array, |
| 205 | + // including Array(Tuple(Nullable)) which is how Nested columns are encoded. A |
| 206 | + // trailing Float64 after each nested container detects misalignment. |
| 207 | + {"Array(Tuple(Int32, Nullable(String), Float64))", |
| 208 | + Arrays.asList(Arrays.asList(7, "a", 9.5d), Arrays.asList(7, null, 8.5d))}, |
| 209 | + {"Tuple(String, Map(String, Nullable(Int32)), Float64)", |
| 210 | + Arrays.asList("id", newMap("k1", 7, "k2", null), 9.5d)}, |
| 211 | + {"Tuple(Array(Nullable(Int32)), Float64)", Arrays.asList(Arrays.asList(1, null, 3), 9.5d)}, |
| 212 | + |
| 213 | + // Contrast: non-nullable nested elements must keep serializing without a marker, |
| 214 | + // so these rows round-trip identically with or without the fix. |
| 215 | + {"Tuple(Int32, String, Float64)", Arrays.asList(7, "tail", 9.5d)}, |
| 216 | + {"Tuple(Int32, Map(String, String), Float64)", Arrays.asList(7, newMap("k", "v"), 9.5d)}, |
| 217 | + }; |
| 218 | + } |
| 219 | + |
| 220 | + // Normalizes Tuple (Object[]) and Array (ArrayValue / List) results to nested Lists so |
| 221 | + // round-tripped values compare structurally regardless of the container representation the |
| 222 | + // reader returns. |
| 223 | + @SuppressWarnings("unchecked") |
| 224 | + private static Object normalize(Object value) { |
| 225 | + if (value instanceof BinaryStreamReader.ArrayValue) { |
| 226 | + return normalizeList(((BinaryStreamReader.ArrayValue) value).asList()); |
| 227 | + } else if (value instanceof Object[]) { |
| 228 | + return normalizeList(Arrays.asList((Object[]) value)); |
| 229 | + } else if (value instanceof List) { |
| 230 | + return normalizeList((List<Object>) value); |
| 231 | + } else if (value instanceof Map) { |
| 232 | + Map<Object, Object> result = new LinkedHashMap<>(); |
| 233 | + ((Map<Object, Object>) value).forEach((k, v) -> result.put(k, normalize(v))); |
| 234 | + return result; |
| 235 | + } |
| 236 | + return value; |
| 237 | + } |
| 238 | + |
| 239 | + private static List<Object> normalizeList(List<Object> values) { |
| 240 | + List<Object> result = new ArrayList<>(values.size()); |
| 241 | + for (Object v : values) { |
| 242 | + result.add(normalize(v)); |
| 243 | + } |
| 244 | + return result; |
| 245 | + } |
| 246 | + |
| 247 | + private static Map<Object, Object> newMap(Object... kv) { |
| 248 | + Map<Object, Object> map = new LinkedHashMap<>(); |
| 249 | + for (int i = 0; i < kv.length; i += 2) { |
| 250 | + map.put(kv[i], kv[i + 1]); |
| 251 | + } |
| 252 | + return map; |
| 253 | + } |
| 254 | + |
137 | 255 | private void assertCustomGeoTypeTag(String typeName) throws Exception { |
138 | 256 | ByteArrayOutputStream out = new ByteArrayOutputStream(); |
139 | 257 | SerializerUtils.writeDynamicTypeTag(out, ClickHouseColumn.of("v", typeName)); |
|
0 commit comments