From 3bd7a06c6e9f765766eddf8a497c946a977e1cb8 Mon Sep 17 00:00:00 2001 From: Polyglot AI <293096396+polyglotAI-bot@users.noreply.github.com> Date: Wed, 29 Jul 2026 16:13:58 +0000 Subject: [PATCH] [client-v2] Send null query parameter as \N sentinel A top-level scalar null query-parameter value was formatted with String.valueOf, producing the literal "null", which the server rejects with BAD_QUERY_PARAMETER for any Nullable(T) placeholder. DataTypeConverter# convertParameterToString now returns the \N NULL sentinel for a top-level null so it binds SQL NULL; nested nulls inside Array/Map values continue to use the SQL NULL keyword. Fixes: https://github.com/ClickHouse/clickhouse-java/issues/2977 --- CHANGELOG.md | 7 ++++++ .../api/internal/DataTypeConverter.java | 18 +++++++++++--- .../client/ParameterizedQueryTest.java | 24 +++++++++++++++++++ .../api/internal/DataTypeConverterTest.java | 4 +++- docs/features.md | 1 + 5 files changed, 50 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31243a46d..a7afd45b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,13 @@ which inlines parameters as SQL literals and already escaped the backslash and single quote, is unchanged and covered by a new regression test. (https://github.com/ClickHouse/clickhouse-java/issues/2781) +- **[client-v2]** Fixed a `null` query-parameter value being sent as the literal string `"null"`, so + `Client.query(sql, params, ...)` binding a Java `null` to a scalar placeholder such as + `{x:Nullable(Decimal128(8))}` was rejected by the server with `BAD_QUERY_PARAMETER` + (`Value null cannot be parsed as Nullable(...)`). A top-level scalar `null` is now sent as the ClickHouse + `\N` NULL sentinel so it binds SQL `NULL`; a `null` nested inside an `Array`/`Map` parameter value + continues to render as the SQL `NULL` keyword. (https://github.com/ClickHouse/clickhouse-java/issues/2977) + - **[client-v2]** Fixed binary array decoding for nullable element types so `Array(Nullable(Float64))` and similar columns now return boxed arrays such as `Double[]` instead of `Object[]`. This keeps null-supporting arrays aligned with their element type while preserving the existing `Object[]` fallback for Variant/Dynamic/Geometry arrays. (https://github.com/ClickHouse/clickhouse-java/issues/2846) - **[client-v2]** Fixed `Float32`/`Float64` columns throwing `ClassCastException` when a value of a diff --git a/client-v2/src/main/java/com/clickhouse/client/api/internal/DataTypeConverter.java b/client-v2/src/main/java/com/clickhouse/client/api/internal/DataTypeConverter.java index aea3578ad..c19df528f 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/internal/DataTypeConverter.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/internal/DataTypeConverter.java @@ -37,6 +37,10 @@ public class DataTypeConverter { private static final String NULL = "NULL"; + // NULL sentinel for a top-level scalar param_ value: the server accepts \N (but not the + // literal "null") for a scalar Nullable(T) placeholder; a nested null uses the SQL NULL keyword. + private static final String NULL_SCALAR_PARAM = "\\N"; + public static final DataTypeConverter INSTANCE = new DataTypeConverter(); private final ListAsStringWriter listAsStringWriter = new ListAsStringWriter(); @@ -237,7 +241,9 @@ public String arrayToString(Object value, String columnDef) { * *

A top-level scalar is returned in its bare, unquoted text form, which is what the server * expects for a scalar {@code {name:Type}} placeholder (e.g. a {@code Date} is sent as - * {@code 2026-05-13}, not {@code '2026-05-13'}). A container is rendered as a ClickHouse + * {@code 2026-05-13}, not {@code '2026-05-13'}). A top-level {@code null} becomes the + * {@code \N} NULL sentinel, which the server accepts for a scalar {@code Nullable(T)} + * placeholder (the literal {@code "null"} would be rejected). A container is rendered as a ClickHouse * {@code Array} ({@code [..]}) or {@code Map} ({@code {..}}) text literal in which * {@code String}/temporal leaves are single-quoted (and escaped) while numeric/boolean leaves * are left unquoted, as required by the server's array/map text parser.

@@ -246,6 +252,12 @@ public String arrayToString(Object value, String columnDef) { * @return the formatted {@code param_} value */ public String convertParameterToString(Object value) { + if (value == null) { + // A top-level scalar null must be sent as \N; the server rejects the literal "null" + // (String.valueOf(null)). Nested nulls inside containers use the SQL NULL keyword and + // are handled by convertParameterContainer. + return NULL_SCALAR_PARAM; + } if (isParameterContainer(value)) { return convertParameterContainer(value); } @@ -256,8 +268,8 @@ public String convertParameterToString(Object value) { // the value). Escape those three characters so any String value round-trips. return escapeStringParameter((CharSequence) value); } - // Other scalars (and null) have no escapable characters and are read verbatim by the server, - // so they are passed through unquoted (e.g. Date, numbers, Identifier). + // Other scalars have no escapable characters and are read verbatim by the server, so they + // are passed through unquoted (e.g. Date, numbers, Identifier). return String.valueOf(value); } diff --git a/client-v2/src/test/java/com/clickhouse/client/ParameterizedQueryTest.java b/client-v2/src/test/java/com/clickhouse/client/ParameterizedQueryTest.java index 916c4a8cd..8b1d32035 100644 --- a/client-v2/src/test/java/com/clickhouse/client/ParameterizedQueryTest.java +++ b/client-v2/src/test/java/com/clickhouse/client/ParameterizedQueryTest.java @@ -355,4 +355,28 @@ private static Object[][] createStringParameterValues() { }; } + @Test(groups = {"integration"}, dataProvider = "nullableParamTypes") + void testNullParam(String paramType) throws Exception { + Map params = new HashMap<>(); + params.put("x", null); + try (QueryResponse response = client.query( + "SELECT isNull({x:" + paramType + "})", params).get(); + ClickHouseBinaryFormatReader reader = client.newBinaryFormatReader(response)) + { + reader.next(); + Assert.assertTrue(reader.getBoolean(1), + "null query parameter should bind SQL NULL for " + paramType); + } + } + + @DataProvider(name = "nullableParamTypes") + private static Object[][] nullableParamTypes() { + return new Object[][] { + { "Nullable(Int32)" }, + { "Nullable(String)" }, + { "Nullable(Decimal128(8))" }, + { "Nullable(Date)" }, + }; + } + } diff --git a/client-v2/src/test/java/com/clickhouse/client/api/internal/DataTypeConverterTest.java b/client-v2/src/test/java/com/clickhouse/client/api/internal/DataTypeConverterTest.java index e8d7a1370..06d472ce5 100644 --- a/client-v2/src/test/java/com/clickhouse/client/api/internal/DataTypeConverterTest.java +++ b/client-v2/src/test/java/com/clickhouse/client/api/internal/DataTypeConverterTest.java @@ -176,7 +176,9 @@ public static Object[][] queryParameters() { {"hello", "hello"}, {42, "42"}, {new BigDecimal("1.50"), "1.50"}, - {null, "null"}, + // A scalar null is sent as the \N sentinel, which the server accepts for a scalar + // Nullable(T) placeholder; the literal "null" is rejected with BAD_QUERY_PARAMETER. + {null, "\\N"}, // --- Scalar String special characters: the server parses a {name:String} value with // deserializeTextEscaped, so the client escapes the three characters that reader treats diff --git a/docs/features.md b/docs/features.md index e69a54460..7e809048b 100644 --- a/docs/features.md +++ b/docs/features.md @@ -40,6 +40,7 @@ Compatibility-sensitive traits: - Named parameter typing is part of the contract: placeholders are written as `{name:Type}` and the supplied value must match the expected ClickHouse textual representation for that type. - String query parameters are expected to round-trip correctly for ordinary text, Unicode, slashes, dashes, leading or trailing spaces, and characters the server's escaped-text parameter parser treats as structural — a backslash, tab, or newline — which the client escapes before sending. +- A `null` query-parameter value is sent as the ClickHouse `\N` NULL sentinel, so a top-level `{name:Nullable(T)}` placeholder binds SQL `NULL`; a `null` nested inside an `Array`/`Map` parameter value renders as the SQL `NULL` keyword. - Runtime authentication changes are compatibility-sensitive: after `updateUserAndPassword()`, `updateAccessToken()`, or `updateBearerToken()`, subsequent requests from the same `Client` are expected to use the updated credentials. The authentication method itself is fixed at construction time; calling a runtime updater that does not match the configured method throws `ClientMisconfigurationException`. - String escaping behavior in `SQLUtils` is compatibility-sensitive: `enquoteLiteral()` uses SQL-style doubled single quotes, while `escapeSingleQuotes()` escapes both backslashes and single quotes with backslashes. - Identifier quoting behavior is stable API for helper callers: identifiers are double-quoted, embedded double quotes are doubled, and optional quoting keeps simple identifiers unchanged.