Skip to content

Commit 816f99c

Browse files
Add DataTypeTests integration coverage for null into non-nullable Array
Addresses review feedback on #2938: add integration tests in DataTypeTests for the null-into-non-nullable-Array RowBinary writer fix. These exercise the fix through the POJO insert path (client.insert -> POJOSerDe -> RowBinaryFormatSerializer.writeValuePreamble), a different entry point than the existing RowBinaryFormatWriterTest coverage: - null into a non-nullable Array now throws IllegalArgumentException, pinned in both the plain RowBinary branch (no defaults) and the RowBinaryWithDefaults branch (a sibling column carries a default); - valid empty/populated arrays still round-trip with the trailing column intact (no misframing); - a null into a non-nullable Array that HAS a DDL default still coerces to the default instead of throwing.
1 parent 577e765 commit 816f99c

1 file changed

Lines changed: 104 additions & 0 deletions

File tree

client-v2/src/test/java/com/clickhouse/client/datatypes/DataTypeTests.java

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,110 @@ public static String tblCreateSQL(String table) {
155155
}
156156
}
157157

158+
// A non-nullable Array column cannot represent a null. Writing a Java null into one through the POJO
159+
// insert path (POJOSerDe -> RowBinaryFormatSerializer.writeValuePreamble) must fail loudly like every
160+
// other non-nullable type instead of emitting a stray marker byte, which the server read as a phantom
161+
// extra row or a column shift. The fixed-width 'tail' column after the array would be corrupted by any
162+
// stray byte, so it doubles as a misframing sentinel.
163+
@Test(groups = {"integration"}, dataProvider = "nonNullableArrayNullColumns")
164+
public void testInsertNullIntoNonNullableArrayThrows(String columns) throws Exception {
165+
final String table = "test_non_nullable_array_null";
166+
client.execute("DROP TABLE IF EXISTS " + table).get();
167+
client.execute(tableDefinition(table, columns)).get();
168+
169+
client.register(DTOForNonNullableArrayTests.class, client.getTableSchema(table));
170+
171+
Exception thrown = null;
172+
try {
173+
client.insert(table, Collections.singletonList(new DTOForNonNullableArrayTests(1, null, 7)))
174+
.get(EXECUTE_CMD_TIMEOUT, TimeUnit.SECONDS);
175+
} catch (Exception e) {
176+
thrown = e;
177+
}
178+
179+
Assert.assertNotNull(thrown, "Expected the insert to fail for a null in a non-nullable Array column using " + columns);
180+
boolean clearMessage = false;
181+
for (Throwable t = thrown; t != null; t = t.getCause()) {
182+
if (t.getMessage() != null && t.getMessage().contains("An attempt to write null into not nullable column")) {
183+
clearMessage = true;
184+
break;
185+
}
186+
}
187+
Assert.assertTrue(clearMessage, "Expected a clear non-nullable column error using " + columns + ", but got: " + thrown);
188+
}
189+
190+
@DataProvider(name = "nonNullableArrayNullColumns")
191+
public static Object[][] nonNullableArrayNullColumns() {
192+
return new Object[][] {
193+
// No defaults anywhere: the POJO insert uses plain RowBinary (non-defaults preamble branch).
194+
{"rowId Int32, arr Array(Int32), tail Int32"},
195+
// A sibling column has a default, so the POJO insert uses RowBinaryWithDefaults; the
196+
// non-nullable arr itself has no default and so must still reject a null (defaults branch).
197+
{"rowId Int32, arr Array(Int32), tail Int32 DEFAULT 99"},
198+
};
199+
}
200+
201+
// Contrast: valid non-nullable arrays (empty and populated) still round-trip through the POJO insert
202+
// path, and the trailing 'tail' column keeps its value - proving the array length is still written as a
203+
// single leading byte and that rejecting null did not perturb valid array writes.
204+
@Test(groups = {"integration"}, dataProvider = "nonNullableArrayRoundTrip")
205+
public void testInsertNonNullableArrayRoundTrips(List<Integer> arr, int expectedLength, String expectedConcat) throws Exception {
206+
final String table = "test_non_nullable_array_round_trip";
207+
client.execute("DROP TABLE IF EXISTS " + table).get();
208+
client.execute(tableDefinition(table, "rowId Int32", "arr Array(Int32)", "tail Int32")).get();
209+
210+
client.register(DTOForNonNullableArrayTests.class, client.getTableSchema(table));
211+
client.insert(table, Collections.singletonList(new DTOForNonNullableArrayTests(1, arr, 7)))
212+
.get(EXECUTE_CMD_TIMEOUT, TimeUnit.SECONDS);
213+
214+
List<GenericRecord> records = client.queryAll(
215+
"SELECT toInt32(length(arr)) AS alen, arrayStringConcat(arr, ',') AS acat, tail FROM " + table + " ORDER BY rowId");
216+
Assert.assertEquals(records.size(), 1);
217+
GenericRecord row = records.get(0);
218+
Assert.assertEquals(row.getInteger("alen"), expectedLength);
219+
Assert.assertEquals(row.getString("acat"), expectedConcat);
220+
Assert.assertEquals(row.getInteger("tail"), 7);
221+
}
222+
223+
@DataProvider(name = "nonNullableArrayRoundTrip")
224+
public static Object[][] nonNullableArrayRoundTrip() {
225+
return new Object[][] {
226+
{new ArrayList<Integer>(), 0, ""},
227+
{Arrays.asList(1, 2, 3), 3, "1,2,3"},
228+
};
229+
}
230+
231+
// Contrast: with a DDL DEFAULT the write uses RowBinaryWithDefaults, where a null into the non-nullable
232+
// Array column falls back to the default instead of throwing - proving the fix only rejects null for a
233+
// non-nullable array that has no default.
234+
@Test(groups = {"integration"})
235+
public void testInsertNullIntoDefaultedNonNullableArrayUsesDefault() throws Exception {
236+
final String table = "test_non_nullable_array_null_default";
237+
client.execute("DROP TABLE IF EXISTS " + table).get();
238+
client.execute(tableDefinition(table, "rowId Int32", "arr Array(Int32) DEFAULT [1, 2]", "tail Int32")).get();
239+
240+
client.register(DTOForNonNullableArrayTests.class, client.getTableSchema(table));
241+
client.insert(table, Collections.singletonList(new DTOForNonNullableArrayTests(1, null, 7)))
242+
.get(EXECUTE_CMD_TIMEOUT, TimeUnit.SECONDS);
243+
244+
List<GenericRecord> records = client.queryAll(
245+
"SELECT toInt32(length(arr)) AS alen, arrayStringConcat(arr, ',') AS acat, tail FROM " + table + " ORDER BY rowId");
246+
Assert.assertEquals(records.size(), 1);
247+
GenericRecord row = records.get(0);
248+
Assert.assertEquals(row.getInteger("alen"), 2);
249+
Assert.assertEquals(row.getString("acat"), "1,2");
250+
Assert.assertEquals(row.getInteger("tail"), 7);
251+
}
252+
253+
@Data
254+
@AllArgsConstructor
255+
@NoArgsConstructor
256+
public static class DTOForNonNullableArrayTests {
257+
private int rowId;
258+
private List<Integer> arr;
259+
private int tail;
260+
}
261+
158262
@Data
159263
@AllArgsConstructor
160264
@NoArgsConstructor

0 commit comments

Comments
 (0)