Skip to content

[backfill: ClickHouse/clickhouse-java] null query parameter values sent as literal "null", rejected as Nullable(T) #2977

Description

@alex-clickhouse

Target client repo

ClickHouse/clickhouse-java

Severity

sev:2 — Visible error (the server returns BAD_QUERY_PARAMETER and the query fails outright, no silent corruption). The affected pattern is Nullable(T) query parameters, which is a common type. There is no flag-flip workaround inside client-v2: the user has to either rewrite the SQL to avoid passing the null, switch to JDBC v2's PreparedStatement (which inlines SQL), or pass the magic sentinel string "\\N" instead of a Java null. None of those are a normal SDK usage path.

Description

The client-v2 Client.query(String sqlQuery, Map<String, Object> queryParams, QuerySettings settings) API forwards parameters to the ClickHouse HTTP endpoint as param_<name>=<value> URL or multipart form fields. The conversion of each value is unconditionally String.valueOf(v) — see clickhouse-java/client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java around line 754:

private void addStatementParams(Map<String, Object> requestConfig, BiConsumer<String, String> consumer) {
    if (requestConfig.containsKey(KEY_STATEMENT_PARAMS)) {
        Map<?, ?> params = (Map<?, ?>) requestConfig.get(KEY_STATEMENT_PARAMS);
        params.forEach((k, v) -> consumer.accept("param_" + k, String.valueOf(v)));
    }
}

When v is a Java null, String.valueOf((Object)null) returns the literal four-character string "null". That string is sent on the wire as param_x=null. The ClickHouse server then tries to parse "null" as the declared Nullable(T) type and fails with BAD_QUERY_PARAMETER.

The correct wire encoding for a NULL query parameter value is \N. Confirmed against the local server (26.4.2.10):

$ curl -s 'http://localhost:8123/?param_x=null&query=SELECT%20%7Bx%3ANullable(Decimal128(8))%7D'
Code: 457. DB::Exception: Value null cannot be parsed as Nullable(Decimal128(8)) ...

$ curl -s 'http://localhost:8123/?param_x=NULL&query=SELECT%20%7Bx%3ANullable(Decimal128(8))%7D'
Code: 457. DB::Exception: Value NULL cannot be parsed as Nullable(Decimal128(8)) ...

$ curl -s 'http://localhost:8123/?param_x=%5CN&query=SELECT%20%7Bx%3ANullable(Decimal128(8))%7D'
\N

This is the same bug class reported against clickhouse-go (#1760): passing nil / null in the parameter map produces a wire value the server cannot parse against any Nullable(T) type, regardless of T.

Note: this affects client-v2's Client.query(sql, queryParams, settings) API. The legacy jdbc-v2 PreparedStatementImpl.encodeObject path inlines null into the SQL string as the literal NULL and is not affected — it does not use server-side query parameters at all.

ClickHouse server version

Verified against 26.4.2.10 running locally at http://localhost:8123. Code analysis of clickhouse-java main HEAD was also performed; the buggy line is unchanged.

Reproduction

Minimal client-v2 snippet — any null-valued entry in the queryParams map triggers it. The query type is irrelevant; Nullable(Decimal128(8)), Nullable(UUID), Nullable(Bool), etc. all produce equivalent Value null cannot be parsed as Nullable(...) errors.

import com.clickhouse.client.api.Client;
import com.clickhouse.client.api.query.QueryResponse;
import java.util.HashMap;
import java.util.Map;

public class NullParamRepro {
    public static void main(String[] args) throws Exception {
        try (Client client = new Client.Builder()
                .addEndpoint("http://localhost:8123")
                .setUsername("default")
                .setPassword("")
                .build()) {

            Map<String, Object> params = new HashMap<>();
            params.put("x", null);

            // Expected: query returns NULL (or treats {x} as IS NULL in a WHERE clause).
            // Actual:   ServerException — "Value null cannot be parsed as Nullable(Decimal128(8))"
            QueryResponse resp = client
                    .query("SELECT {x:Nullable(Decimal128(8))}", params, null)
                    .get();
        }
    }
}

Expected: the result row contains a SQL NULL.
Actual: BAD_QUERY_PARAMETER error propagated to the caller — Value null cannot be parsed as Nullable(Decimal128(8)) for query parameter 'x' ....

Suggested fix

In clickhouse-java/client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java addStatementParams (around line 754–758), special-case null values to send the ClickHouse NULL sentinel rather than the string "null":

params.forEach((k, v) -> consumer.accept("param_" + k, v == null ? "\\N" : String.valueOf(v)));

This matches the server's expected wire encoding for null parameters and resolves the failure for every Nullable(T).

Source bug

ClickHouse/clickhouse-go#1760

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions