|
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; |
12 | 18 | import java.util.LinkedHashMap; |
13 | 19 | import java.util.List; |
14 | 20 | import java.util.Map; |
15 | 21 | import java.util.TimeZone; |
| 22 | +import java.util.UUID; |
16 | 23 |
|
17 | 24 | @Test(groups = {"unit"}) |
18 | 25 | public class SerializerUtilsTest { |
@@ -136,72 +143,104 @@ public void testGeometrySerializationRejectsMalformedList() { |
136 | 143 | ClickHouseColumn.of("v", "Geometry"))); |
137 | 144 | } |
138 | 145 |
|
139 | | - @Test |
140 | | - public void testTupleWithNullableElementsRoundTrip() throws Exception { |
141 | | - // Regression for #2721: a Nullable element nested inside a Tuple must be prefixed with its |
142 | | - // null-marker byte (0x00 present, 0x01 null). Before the fix the present-value marker was |
143 | | - // missing, so the reader mis-parsed the bytes that followed. The non-nullable sibling element |
144 | | - // must keep being written without a marker. |
145 | | - ClickHouseColumn column = ClickHouseColumn.of("v", "Tuple(Nullable(String), String)"); |
146 | | - |
147 | | - ByteArrayOutputStream present = new ByteArrayOutputStream(); |
148 | | - SerializerUtils.serializeData(present, Arrays.asList("optional-value", "value-2"), column); |
149 | | - Assert.assertEquals((Object[]) newReader(present.toByteArray()).readValue(column), |
150 | | - new Object[] {"optional-value", "value-2"}); |
151 | | - |
152 | | - ByteArrayOutputStream absent = new ByteArrayOutputStream(); |
153 | | - SerializerUtils.serializeData(absent, Arrays.asList(null, "value-2"), column); |
154 | | - Assert.assertEquals((Object[]) newReader(absent.toByteArray()).readValue(column), |
155 | | - new Object[] {null, "value-2"}); |
156 | | - } |
157 | | - |
158 | | - @Test |
159 | | - public void testMapWithNullableValuesRoundTrip() throws Exception { |
160 | | - // Regression for #2721: a Nullable Map value must be prefixed with its null-marker byte. |
161 | | - ClickHouseColumn column = ClickHouseColumn.of("v", "Map(String, Nullable(String))"); |
162 | | - Map<String, String> value = new LinkedHashMap<>(); |
163 | | - value.put("k1", "v1"); |
164 | | - value.put("k2", null); |
| 146 | + @Test(dataProvider = "nestedNullableData") |
| 147 | + public void testNestedNullableRoundTrip(String typeName, Object value) throws Exception { |
| 148 | + ClickHouseColumn column = ClickHouseColumn.of("v", typeName); |
165 | 149 |
|
166 | 150 | ByteArrayOutputStream out = new ByteArrayOutputStream(); |
167 | 151 | SerializerUtils.serializeData(out, value, column); |
168 | 152 |
|
169 | | - Map<?, ?> result = newReader(out.toByteArray()).readValue(column); |
170 | | - Assert.assertEquals(result, value); |
171 | | - } |
172 | | - |
173 | | - @Test |
174 | | - public void testTupleWithNullableFixedWidthElementsRoundTrip() throws Exception { |
175 | | - // #2721 is about the marker byte, not the element type: a fixed-width Nullable element |
176 | | - // (here Int32) must also get its marker so the following value bytes are not misaligned. |
177 | | - // Also covers an all-null tuple. |
178 | | - ClickHouseColumn column = ClickHouseColumn.of("v", "Tuple(Nullable(Int32), Nullable(String))"); |
179 | | - |
180 | | - ByteArrayOutputStream present = new ByteArrayOutputStream(); |
181 | | - SerializerUtils.serializeData(present, Arrays.asList(42, "x"), column); |
182 | | - Assert.assertEquals((Object[]) newReader(present.toByteArray()).readValue(column), |
183 | | - new Object[] {42, "x"}); |
184 | | - |
185 | | - ByteArrayOutputStream allNull = new ByteArrayOutputStream(); |
186 | | - SerializerUtils.serializeData(allNull, Arrays.asList(null, null), column); |
187 | | - Assert.assertEquals((Object[]) newReader(allNull.toByteArray()).readValue(column), |
188 | | - new Object[] {null, null}); |
189 | | - } |
190 | | - |
191 | | - @Test |
192 | | - public void testNestedContainerWithNullableRoundTrip() throws Exception { |
193 | | - // #2721: the marker handling must compose through nested containers — here a Map carrying a |
194 | | - // Nullable value sits inside a Tuple, exercising serializeTupleData -> serializeMapData. |
195 | | - ClickHouseColumn column = ClickHouseColumn.of("v", "Tuple(String, Map(String, Nullable(String)))"); |
196 | | - Map<String, String> inner = new LinkedHashMap<>(); |
197 | | - inner.put("k1", "v1"); |
198 | | - inner.put("k2", null); |
199 | | - |
200 | | - ByteArrayOutputStream out = new ByteArrayOutputStream(); |
201 | | - SerializerUtils.serializeData(out, Arrays.asList("id", inner), column); |
202 | | - |
203 | | - Assert.assertEquals((Object[]) newReader(out.toByteArray()).readValue(column), |
204 | | - new Object[] {"id", inner}); |
| 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 | + // A present Nullable element of each datatype nested in a Tuple, with a trailing |
| 163 | + // non-nullable sibling whose bytes misalign if the present-marker is dropped. |
| 164 | + {"Tuple(Nullable(String), String)", Arrays.asList("opt", "tail")}, |
| 165 | + {"Tuple(Nullable(FixedString(3)), String)", Arrays.asList("abc", "tail")}, |
| 166 | + {"Tuple(Nullable(Int8), String)", Arrays.asList((byte) -5, "tail")}, |
| 167 | + {"Tuple(Nullable(UInt8), String)", Arrays.asList((short) 200, "tail")}, |
| 168 | + {"Tuple(Nullable(Int16), String)", Arrays.asList((short) -1600, "tail")}, |
| 169 | + {"Tuple(Nullable(UInt16), String)", Arrays.asList(40000, "tail")}, |
| 170 | + {"Tuple(Nullable(Int32), String)", Arrays.asList(42, "tail")}, |
| 171 | + {"Tuple(Nullable(UInt32), String)", Arrays.asList(4_000_000_000L, "tail")}, |
| 172 | + {"Tuple(Nullable(Int64), String)", Arrays.asList(-64L, "tail")}, |
| 173 | + {"Tuple(Nullable(UInt64), String)", Arrays.asList(BigInteger.valueOf(64), "tail")}, |
| 174 | + {"Tuple(Nullable(Float32), String)", Arrays.asList(1.5f, "tail")}, |
| 175 | + {"Tuple(Nullable(Float64), String)", Arrays.asList(2.5d, "tail")}, |
| 176 | + {"Tuple(Nullable(Bool), String)", Arrays.asList(true, "tail")}, |
| 177 | + {"Tuple(Nullable(UUID), String)", Arrays.asList(uuid, "tail")}, |
| 178 | + {"Tuple(Nullable(Date), String)", Arrays.asList(LocalDate.of(2021, 2, 3), "tail")}, |
| 179 | + {"Tuple(Nullable(Decimal64(4)), String)", Arrays.asList(new BigDecimal("1.2345"), "tail")}, |
| 180 | + {"Tuple(Nullable(IPv4), String)", Arrays.asList(ipv4, "tail")}, |
| 181 | + |
| 182 | + // A Tuple value given as a Java array (not a List) takes the other branch of |
| 183 | + // serializeTupleData, which is routed through the same nested-marker path. |
| 184 | + {"Tuple(Nullable(String), String)", new Object[] {"opt", "tail"}}, |
| 185 | + |
| 186 | + // The same marker handling on the Map value path, across a range of widths. |
| 187 | + {"Map(String, Nullable(String))", newMap("k", "v")}, |
| 188 | + {"Map(String, Nullable(Int32))", newMap("k", 32)}, |
| 189 | + {"Map(String, Nullable(Float64))", newMap("k", 2.5d)}, |
| 190 | + {"Map(String, Nullable(UUID))", newMap("k", uuid)}, |
| 191 | + |
| 192 | + // Null elements/values still serialize a single null-marker byte. |
| 193 | + {"Tuple(Nullable(String), String)", Arrays.asList(null, "tail")}, |
| 194 | + {"Tuple(Nullable(Int32), String)", Arrays.asList(null, "tail")}, |
| 195 | + {"Tuple(Nullable(Int32), Nullable(String))", Arrays.asList(null, null)}, |
| 196 | + {"Map(String, Nullable(String))", newMap("k", null)}, |
| 197 | + |
| 198 | + // Containers compose: marker handling threads through nested Tuple/Map/Array, |
| 199 | + // including Array(Tuple(Nullable)) which is how Nested columns are encoded. |
| 200 | + {"Tuple(String, Map(String, Nullable(String)))", Arrays.asList("id", newMap("k1", "v1", "k2", null))}, |
| 201 | + {"Tuple(Nullable(String), Map(String, Nullable(Int32)))", Arrays.asList("opt", newMap("k", 7))}, |
| 202 | + {"Array(Tuple(Nullable(String), String))", Arrays.asList(Arrays.asList("a", "b"), Arrays.asList(null, "c"))}, |
| 203 | + {"Tuple(Array(Nullable(Int32)), String)", Arrays.asList(Arrays.asList(1, null, 3), "tail")}, |
| 204 | + |
| 205 | + // Contrast: non-nullable nested elements must keep serializing without a marker. |
| 206 | + {"Tuple(Int32, String)", Arrays.asList(7, "tail")}, |
| 207 | + {"Map(String, String)", newMap("k", "v")}, |
| 208 | + }; |
| 209 | + } |
| 210 | + |
| 211 | + // Normalizes Tuple (Object[]) and Array (ArrayValue / List) results to nested Lists so |
| 212 | + // round-tripped values compare structurally regardless of the container representation the |
| 213 | + // reader returns. |
| 214 | + @SuppressWarnings("unchecked") |
| 215 | + private static Object normalize(Object value) { |
| 216 | + if (value instanceof BinaryStreamReader.ArrayValue) { |
| 217 | + return normalizeList(((BinaryStreamReader.ArrayValue) value).asList()); |
| 218 | + } else if (value instanceof Object[]) { |
| 219 | + return normalizeList(Arrays.asList((Object[]) value)); |
| 220 | + } else if (value instanceof List) { |
| 221 | + return normalizeList((List<Object>) value); |
| 222 | + } else if (value instanceof Map) { |
| 223 | + Map<Object, Object> result = new LinkedHashMap<>(); |
| 224 | + ((Map<Object, Object>) value).forEach((k, v) -> result.put(k, normalize(v))); |
| 225 | + return result; |
| 226 | + } |
| 227 | + return value; |
| 228 | + } |
| 229 | + |
| 230 | + private static List<Object> normalizeList(List<Object> values) { |
| 231 | + List<Object> result = new ArrayList<>(values.size()); |
| 232 | + for (Object v : values) { |
| 233 | + result.add(normalize(v)); |
| 234 | + } |
| 235 | + return result; |
| 236 | + } |
| 237 | + |
| 238 | + private static Map<Object, Object> newMap(Object... kv) { |
| 239 | + Map<Object, Object> map = new LinkedHashMap<>(); |
| 240 | + for (int i = 0; i < kv.length; i += 2) { |
| 241 | + map.put(kv[i], kv[i + 1]); |
| 242 | + } |
| 243 | + return map; |
205 | 244 | } |
206 | 245 |
|
207 | 246 | private void assertCustomGeoTypeTag(String typeName) throws Exception { |
|
0 commit comments