Skip to content

[client-v2] A Map/List/array bound to a Tuple query parameter is rendered as a Map/Array literal and fails with code 27 #3291

Open

Description

@claude

Description

Client.query(String, Map<String, Object>, QuerySettings) renders every parameter value through
DataTypeConverter.convertParameterToString, which is type-blind: it never sees the {name:Type}
placeholder's declared type. convertParameterContainer/pushContainer
(client-v2/src/main/java/com/clickhouse/client/api/internal/DataTypeConverter.java:340-396) therefore
renders:

  • any java.util.Map as a ClickHouse Map literal — {'protocol':6,'port':80}
  • any Collection or array as a ClickHouse Array literal — [6,80]

The server's param_<name> reader parses a Tuple(...) value with the tuple text parser, which requires
a leading (. Both renderings are rejected:

Code: 27. DB::Exception: Cannot parse input: expected '(' before: '{\'protocol\':6,\'port\':80}':
value {'protocol':6,'port':80} cannot be parsed as Tuple(protocol UInt8, port UInt16)
for query parameter 'p'. (CANNOT_PARSE_INPUT_ASSERTION_FAILED)

Net effect: there is no natural Java value that can be bound to a Tuple(...) query parameter.
The only thing that works is hand-formatting the literal yourself as a String ("(6,80)"), which
relies on the caller doing the quoting/escaping that the converter exists to do, and which is unsafe
for Tuple(String, ...) elements.

This is the Java-side equivalent of clickhouse-connect issue #1148 (there a dict bound to a named
Tuple is formatted as JSON or as a map literal and hits the same code 27).

ClickHouse server version

26.10.3.7 (official build), reached over HTTP at localhost:8123.

Reproduction

TestNG test placed in client-v2/src/test/java/com/clickhouse/client/query/:

package com.clickhouse.client.query;

import com.clickhouse.client.api.Client;
import com.clickhouse.client.api.query.QueryResponse;
import com.clickhouse.client.api.query.QuerySettings;
import com.clickhouse.data.ClickHouseFormat;
import org.testng.annotations.Test;

import java.io.InputStream;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;

public class TupleParamTest {

    @Test
    public void tupleParameterBinding() throws Exception {
        try (Client client = new Client.Builder()
                .addEndpoint("http://localhost:8123")
                .setUsername("default")
                .setPassword("")
                .build()) {

            Map<String, Object> named = new LinkedHashMap<>();
            named.put("protocol", 6);
            named.put("port", 80);

            run(client, "Map -> named Tuple", named);
            run(client, "List -> named Tuple", Arrays.asList(6, 80));
            run(client, "Object[] -> named Tuple", new Object[]{6, 80});
            run(client, "String \"(6,80)\" -> named Tuple", "(6,80)");
        }
    }

    private static void run(Client client, String label, Object value) {
        Map<String, Object> params = new LinkedHashMap<>();
        params.put("p", value);
        try (QueryResponse resp = client.query(
                "SELECT {p:Tuple(protocol UInt8, port UInt16)} AS t",
                params,
                new QuerySettings().setFormat(ClickHouseFormat.TabSeparated)).get()) {
            InputStream in = resp.getInputStream();
            byte[] buf = new byte[256];
            int n = in.read(buf);
            System.out.println("RESULT " + label + " -> OK: " + new String(buf, 0, Math.max(n, 0)).trim());
        } catch (Exception e) {
            Throwable t = e.getCause() == null ? e : e.getCause();
            String m = t.getMessage();
            System.out.println("RESULT " + label + " -> FAIL: " + (m == null ? t.toString() : m.split("\n")[0]));
        }
    }
}

mvn -pl client-v2 test -Dtest=TupleParamTest

Expected — a Map keyed by the tuple element names, and an ordered List/array, bind the same way
the hand-written (6,80) literal does:

RESULT Map -> named Tuple          -> OK: (6,80)
RESULT List -> named Tuple         -> OK: (6,80)
RESULT Object[] -> named Tuple     -> OK: (6,80)
RESULT String "(6,80)" -> named Tuple -> OK: (6,80)

Actual:

RESULT Map -> named Tuple -> FAIL: Code: 27. DB::Exception: Cannot parse input: expected '(' before: '{\'protocol\':6,\'port\':80}': value {'protocol':6,'port':80} cannot be parsed as Tuple(protocol UInt8, port UInt16) for query parameter 'p'. (CANNOT_PARSE_INPUT_ASSERTION_FAILED)
RESULT List -> named Tuple -> FAIL: Code: 27. DB::Exception: Cannot parse input: expected '(' before: '[6,80]': value [6,80] cannot be parsed as Tuple(protocol UInt8, port UInt16) for query parameter 'p'. (CANNOT_PARSE_INPUT_ASSERTION_FAILED)
RESULT Object[] -> named Tuple -> FAIL: Code: 27. DB::Exception: Cannot parse input: expected '(' before: '[6,80]': value [6,80] cannot be parsed as Tuple(protocol UInt8, port UInt16) for query parameter 'p'. (CANNOT_PARSE_INPUT_ASSERTION_FAILED)
RESULT String "(6,80)" -> named Tuple -> OK: (6,80)

Only the pre-formatted string works.

Suggested fix

In client-v2/src/main/java/com/clickhouse/client/api/internal/DataTypeConverter.java, the parameter
renderer has no notion of a tuple. Two parts:

  1. The param_<name> placeholder type is already present in the SQL text ({p:Tuple(...)}). If the
    converter were given the declared type (as arrayToString(Object, String columnDef) already is), a
    Tuple(...) placeholder could render a Collection/array as (e1,e2,...) and a Map by ordering
    its entries against the declared element names.
  2. Failing that, a dedicated marker/overload for tuple-shaped values would at least give callers a safe
    path that still applies the element quoting/escaping rules in convertParameterContainer.

Note pushContainer would also need the parenthesised form for a tuple nested inside an
Array(Tuple(...)) parameter, which has the same problem today.

Link

Upstream client report: ClickHouse/clickhouse-connect#1148

Metadata

Metadata

Assignees

No one assigned

    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