Skip to content

Commit 4858a6f

Browse files
Restructure nested-Nullable tests into one data-provider test (review feedback)
Address review on #2886: squash the four nested-Nullable round-trip tests into a single TestNG @DataProvider-driven test, drop the issue-referencing/verbose test comments, and broaden coverage to all nested datatypes. The provider now exercises a present Nullable element of every byte-width class (String, FixedString, Int/UInt 8/16/32/64, Float32/64, Bool, UUID, Date, Decimal64, IPv4) nested in a Tuple, the same on the Map value path, both serializeTupleData branches (List and array input), null markers, container compositions including Array(Tuple(Nullable)) (how Nested is encoded), and non-nullable contrast cases that must keep serializing without a marker. A small normalize() helper compares round-tripped values structurally across the Tuple/Array/Map container representations the reader returns.
1 parent 0da71d8 commit 4858a6f

1 file changed

Lines changed: 101 additions & 62 deletions

File tree

client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/SerializerUtilsTest.java

Lines changed: 101 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,22 @@
44
import com.clickhouse.data.ClickHouseDataType;
55
import com.clickhouse.data.value.ClickHouseGeoPolygonValue;
66
import org.testng.Assert;
7+
import org.testng.annotations.DataProvider;
78
import org.testng.annotations.Test;
89

910
import java.io.ByteArrayInputStream;
1011
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;
1117
import java.util.Arrays;
1218
import java.util.LinkedHashMap;
1319
import java.util.List;
1420
import java.util.Map;
1521
import java.util.TimeZone;
22+
import java.util.UUID;
1623

1724
@Test(groups = {"unit"})
1825
public class SerializerUtilsTest {
@@ -136,72 +143,104 @@ public void testGeometrySerializationRejectsMalformedList() {
136143
ClickHouseColumn.of("v", "Geometry")));
137144
}
138145

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);
165149

166150
ByteArrayOutputStream out = new ByteArrayOutputStream();
167151
SerializerUtils.serializeData(out, value, column);
168152

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;
205244
}
206245

207246
private void assertCustomGeoTypeTag(String typeName) throws Exception {

0 commit comments

Comments
 (0)