Skip to content

Commit 13010eb

Browse files
Clarify non-nullable Enum null error and add integration + JDBC tests
Reword the serializeEnumData null-guard message to begin with "Cannot write NULL into non-nullable Enum column", and add coverage requested in review of #2931: - client-v2 RowBinary round-trip of zero-like Enum values (empty-string name, underlying 0, signed negatives, by name and by int), proving correct read + write. - client-v2 parametrized test that a null Enum element in a non-nullable Array/Tuple/Map/nested-Array container now fails with the clear message instead of an opaque NullPointerException (the top-level scalar case is already guarded earlier by writeValuePreamble). - jdbc-v2 zero-like Enum round-trip through PreparedStatement/ResultSet.
1 parent c20b637 commit 13010eb

4 files changed

Lines changed: 146 additions & 2 deletions

File tree

client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/SerializerUtils.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -707,7 +707,7 @@ private static void serializeTime64(OutputStream stream, Object value) throws IO
707707

708708
public static void serializeEnumData(OutputStream stream, ClickHouseColumn column, Object value) throws IOException {
709709
if (value == null) {
710-
throw new IllegalArgumentException("Cannot insert null into non-nullable column " + column.getColumnName()
710+
throw new IllegalArgumentException("Cannot write NULL into non-nullable Enum column " + column.getColumnName()
711711
+ " of type " + column.getOriginalTypeName());
712712
}
713713
int enumValue = -1;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ public void testNullIntoNonNullableEnumThrowsIllegalArgument(String typeName) {
150150
IllegalArgumentException ex = Assert.expectThrows(IllegalArgumentException.class,
151151
() -> SerializerUtils.serializeData(new ByteArrayOutputStream(), null, column));
152152
String message = ex.getMessage();
153-
Assert.assertTrue(message.contains("Cannot insert null into non-nullable column"),
153+
Assert.assertTrue(message.contains("Cannot write NULL into non-nullable Enum column"),
154154
"Unexpected message: " + message);
155155
Assert.assertTrue(message.contains("bs_flag"),
156156
"Message should name the offending column: " + message);

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

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
import com.clickhouse.data.ClickHouseFormat;
1818
import com.clickhouse.data.ClickHouseVersion;
1919
import org.apache.commons.lang3.RandomStringUtils;
20+
import org.testng.Assert;
2021
import org.testng.annotations.BeforeMethod;
22+
import org.testng.annotations.DataProvider;
2123
import org.testng.annotations.Test;
2224

2325
import java.io.IOException;
@@ -254,6 +256,86 @@ public void writeMissingFieldsTest() throws Exception {
254256
}
255257

256258

259+
@Test (groups = { "integration" })
260+
public void writeEnumZeroLikeValuesTest() throws Exception {
261+
String tableName = "rowBinaryFormatWriterTest_enumZeroLike_" + UUID.randomUUID().toString().replace('-', '_');
262+
String tableCreate = "CREATE TABLE \"" + tableName + "\" " +
263+
" (id Int32, " +
264+
" e8 Enum8('' = 0, 'a' = 1, 'neg' = -5), " +
265+
" e16 Enum16('zero' = 0, 'big' = 30000, 'nb' = -20000), " +
266+
" tail Float64" +
267+
" ) Engine = MergeTree ORDER BY id";
268+
269+
Field[][] rows = new Field[][] {
270+
// Zero-like written by enum name: an empty-string name and a named zero both map to 0.
271+
{new Field("id", 1), new Field("e8", ""), new Field("e16", "zero"), new Field("tail", 1.5)},
272+
// The same zero-like members written by their underlying int 0.
273+
{new Field("id", 2), new Field("e8", 0).set(""), new Field("e16", 0).set("zero"), new Field("tail", 2.5)},
274+
// Negative members (Enum8/Enum16 are signed) written by name.
275+
{new Field("id", 3), new Field("e8", "neg"), new Field("e16", "nb"), new Field("tail", 3.5)},
276+
// The same negative members written by their underlying int.
277+
{new Field("id", 4), new Field("e8", -5).set("neg"), new Field("e16", -20000).set("nb"), new Field("tail", 4.5)},
278+
// Ordinary positive members.
279+
{new Field("id", 5), new Field("e8", "a"), new Field("e16", "big"), new Field("tail", 5.5)},
280+
};
281+
282+
writeTest(tableName, tableCreate, rows);
283+
}
284+
285+
286+
// A null element in a non-nullable Enum sub-column reaches serializeEnumData directly (no
287+
// per-element null preamble is written for non-nullable nested columns), which is the path
288+
// that used to throw an opaque NullPointerException. Covers every container seam that routes
289+
// an element through serializeEnumData: Array (level 1), Tuple, Map value, and a nested Array.
290+
@DataProvider(name = "nullEnumContainers")
291+
private Object[][] nullEnumContainers() {
292+
return new Object[][] {
293+
{"Array(Enum8('a' = 1, 'b' = 2))", Arrays.asList("a", null)},
294+
{"Tuple(Enum8('a' = 1, 'b' = 2), Int32)", Arrays.asList(null, 7)},
295+
{"Map(String, Enum16('a' = 1, 'b' = 2))", singleEntryMap("k", null)},
296+
{"Array(Array(Enum8('a' = 1, 'b' = 2)))", Arrays.asList(Arrays.asList("a", null))},
297+
};
298+
}
299+
300+
@Test (groups = { "integration" }, dataProvider = "nullEnumContainers")
301+
public void writeNullEnumInContainerThrowsTest(String columnType, Object valueWithNullEnum) throws Exception {
302+
String tableName = "rowBinaryFormatWriterTest_enumContainerNull_" + UUID.randomUUID().toString().replace('-', '_');
303+
initTable(tableName,
304+
"CREATE TABLE \"" + tableName + "\" (id Int32, c " + columnType + ") Engine = MergeTree ORDER BY id",
305+
new CommandSettings());
306+
TableSchema schema = client.getTableSchema(tableName);
307+
ClickHouseFormat format = ClickHouseFormat.RowBinaryWithDefaults;
308+
309+
Exception thrown = null;
310+
try (InsertResponse response = client.insert(tableName, out -> {
311+
RowBinaryFormatWriter w = new RowBinaryFormatWriter(out, schema, format);
312+
w.setValue(schema.nameToColumnIndex("id"), 1);
313+
w.setValue(schema.nameToColumnIndex("c"), valueWithNullEnum);
314+
w.commitRow();
315+
}, format, settings).get(EXECUTE_CMD_TIMEOUT, TimeUnit.SECONDS)) {
316+
// The insert must not succeed: a null Enum element in a non-nullable container is invalid.
317+
} catch (Exception e) {
318+
thrown = e;
319+
}
320+
321+
Assert.assertNotNull(thrown, "Expected the insert to fail for a null Enum element in " + columnType);
322+
boolean clearMessage = false;
323+
for (Throwable t = thrown; t != null; t = t.getCause()) {
324+
if (t.getMessage() != null && t.getMessage().contains("Cannot write NULL into non-nullable Enum column")) {
325+
clearMessage = true;
326+
break;
327+
}
328+
}
329+
Assert.assertTrue(clearMessage, "Expected a clear non-nullable Enum error for " + columnType + ", but got: " + thrown);
330+
}
331+
332+
private static Map<String, Object> singleEntryMap(String key, Object value) {
333+
Map<String, Object> map = new HashMap<>();
334+
map.put(key, value);
335+
return map;
336+
}
337+
338+
257339
@Test (groups = { "integration" })
258340
public void writeNumbersTest() throws Exception {
259341
String tableName = "rowBinaryFormatWriterTest_writeNumbersTest_" + UUID.randomUUID().toString().replace('-', '_');

jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcDataTypeTests.java

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1178,6 +1178,68 @@ public void testStringTypes() throws SQLException {
11781178
}
11791179
}
11801180

1181+
@Test(groups = { "integration" })
1182+
public void testEnumZeroLikeValues() throws SQLException {
1183+
runQuery("DROP TABLE IF EXISTS test_enum_zero_like");
1184+
runQuery("CREATE TABLE test_enum_zero_like (order Int8, "
1185+
+ "e8 Enum8('' = 0, 'a' = 1, 'neg' = -5), e16 Enum16('zero' = 0, 'big' = 30000, 'nb' = -20000)"
1186+
+ ") ENGINE = MergeTree ORDER BY ()");
1187+
1188+
try (Connection conn = getJdbcConnection()) {
1189+
try (PreparedStatement stmt = conn.prepareStatement("INSERT INTO test_enum_zero_like VALUES ( ?, ?, ? )")) {
1190+
// Zero-like written by name: an empty-string name and a named zero both map to 0.
1191+
stmt.setInt(1, 1);
1192+
stmt.setString(2, "");
1193+
stmt.setString(3, "zero");
1194+
stmt.addBatch();
1195+
// The same zero-like members written by their underlying int 0.
1196+
stmt.setInt(1, 2);
1197+
stmt.setInt(2, 0);
1198+
stmt.setInt(3, 0);
1199+
stmt.addBatch();
1200+
// Negative members (Enum8/Enum16 are signed) written by name.
1201+
stmt.setInt(1, 3);
1202+
stmt.setString(2, "neg");
1203+
stmt.setString(3, "nb");
1204+
stmt.addBatch();
1205+
// The same negative members written by their underlying int.
1206+
stmt.setInt(1, 4);
1207+
stmt.setInt(2, -5);
1208+
stmt.setInt(3, -20000);
1209+
stmt.addBatch();
1210+
stmt.executeBatch();
1211+
}
1212+
}
1213+
1214+
try (Connection conn = getJdbcConnection()) {
1215+
try (Statement stmt = conn.createStatement()) {
1216+
try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_enum_zero_like ORDER BY order")) {
1217+
assertTrue(rs.next());
1218+
assertEquals(rs.getString("e8"), "");
1219+
assertEquals(rs.getInt("e8"), 0);
1220+
assertEquals(rs.getString("e16"), "zero");
1221+
assertEquals(rs.getInt("e16"), 0);
1222+
assertTrue(rs.next());
1223+
assertEquals(rs.getString("e8"), "");
1224+
assertEquals(rs.getInt("e8"), 0);
1225+
assertEquals(rs.getString("e16"), "zero");
1226+
assertEquals(rs.getInt("e16"), 0);
1227+
assertTrue(rs.next());
1228+
assertEquals(rs.getString("e8"), "neg");
1229+
assertEquals(rs.getInt("e8"), -5);
1230+
assertEquals(rs.getString("e16"), "nb");
1231+
assertEquals(rs.getInt("e16"), -20000);
1232+
assertTrue(rs.next());
1233+
assertEquals(rs.getString("e8"), "neg");
1234+
assertEquals(rs.getInt("e8"), -5);
1235+
assertEquals(rs.getString("e16"), "nb");
1236+
assertEquals(rs.getInt("e16"), -20000);
1237+
assertFalse(rs.next());
1238+
}
1239+
}
1240+
}
1241+
}
1242+
11811243
@Test(groups = { "integration" })
11821244
public void testIpAddressTypes() throws SQLException, UnknownHostException {
11831245
runQuery("CREATE TABLE test_ips (order Int8, "

0 commit comments

Comments
 (0)