Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ public class DataTypeConverter {

private static final String NULL = "NULL";

// NULL sentinel for a top-level scalar param_<name> 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();
Expand Down Expand Up @@ -251,7 +255,9 @@ public String arrayToString(Object value, String columnDef) {
*
* <p>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.</p>
Expand All @@ -260,6 +266,12 @@ public String arrayToString(Object value, String columnDef) {
* @return the formatted {@code param_<name>} 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);
}
Expand All @@ -270,8 +282,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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -355,4 +355,28 @@ private static Object[][] createStringParameterValues() {
};
}

@Test(groups = {"integration"}, dataProvider = "nullableParamTypes")
void testNullParam(String paramType) throws Exception {
Map<String, Object> 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)" },
};
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,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
Expand Down
1 change: 1 addition & 0 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,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.
Expand Down
Loading