Skip to content

[client-v2] JSON typed paths containing a space or comma break when the JSON value is nested in a Dynamic column #3001

Description

@claude

Description

ClickHouse allows a JSON typed path to contain characters that require backtick quoting (a space, a comma). When such a JSON value is nested inside a Dynamic column, BinaryStreamReader.readDynamicData() reconstructs the type as a string from the binary type encoding and re-parses it with ClickHouseColumn.of(...). Path names are appended unquoted, so a path containing a space or comma produces a malformed type string and the whole query fails.

Top-level JSON columns are not affected: there the type comes from the RowBinaryWithNamesAndTypes header, where the server emits backticks, and ClickHouseUtils.readNameOrQuotedString handles them correctly. Same for JSON inside Map/Tuple/Array (header-derived). Only the binary Dynamic type-encoding path is broken.

Observed:

Query Result
SELECT '{"a b": 1}'::JSON(`a b` Int64) {a b=1}
SELECT '{"a,b": 1}'::JSON(`a,b` Int64) {a,b=1}
SELECT map('k', '{"a b": 1}'::JSON(`a b` Int64)) {k={a b=1}}
SELECT '{"a": 1}'::JSON(`a` Int64)::Dynamic {a=1}
SELECT '{"a b": 1}'::JSON(`a b` Int64)::Dynamic IllegalArgumentException: Unknown data type: b Int64
SELECT '{"a,b": 1}'::JSON(`a,b` Int64)::Dynamic IllegalArgumentException: Unknown data type: b Int64

The failure happens while decoding the column type, so the entire query fails — not just the offending path.

ClickHouse server version

26.7.1.1315 (local HTTP endpoint), verified against main (385f9b4-era working tree, client-v2 0.10.0-rc1-SNAPSHOT), Java 17.

Reproduction

client-v2/src/test/java/com/clickhouse/client/datatypes/JsonSpacePathTest.java:

package com.clickhouse.client.datatypes;

import com.clickhouse.client.api.Client;
import com.clickhouse.client.api.query.GenericRecord;
import org.testng.annotations.Test;

import java.util.List;
import java.util.Map;

import static org.testng.Assert.assertEquals;

public class JsonSpacePathTest {

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

            // works
            List<GenericRecord> ok = client.queryAll("SELECT '{\"a b\": 1}'::JSON(`a b` Int64)");
            assertEquals(((Map<?, ?>) ok.get(0).getObject(1)).get("a b"), 1L);

            // throws: IllegalArgumentException: Unknown data type: b Int64
            List<GenericRecord> dyn = client.queryAll("SELECT '{\"a b\": 1}'::JSON(`a b` Int64)::Dynamic");
            assertEquals(((Map<?, ?>) dyn.get(0).getObject(1)).get("a b"), 1L);
        }
    }
}

Expected: both selects return {a b=1}.

Actual: the second one throws

com.clickhouse.client.api.ClientException: Failed to get query response
Caused by: java.lang.IllegalArgumentException: Unknown data type: b Int64
	at com.clickhouse.data.ClickHouseColumn.readColumn(ClickHouseColumn.java:...)
	at com.clickhouse.data.ClickHouseColumn.parseJSONColumn(ClickHouseColumn.java:846)
	at com.clickhouse.client.api.data_formats.internal.BinaryStreamReader.readDynamicData(BinaryStreamReader.java:1530)

Suggested fix

client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReader.java, readDynamicData() case JSON: (~lines 1508–1530): each path string read from the wire is appended verbatim:

for (int i = 0; i < numberOfTypedPaths; i++) {
    typeDef.append(readString(input)).append(' '); // path
    ClickHouseColumn column = readDynamicData();
    typeDef.append(column.getOriginalTypeName()).append(',');
}

Paths (and likewise the skip-path and path-regexp strings that follow) should be backtick-quoted with inner backticks escaped before being appended, e.g. typeDef.append('`').append(path.replace("`", "")).append('') ``. ClickHouseUtils.readNameOrQuotedString` already understands backticks and doubled-backtick escapes, so the round-trip works once the quoting is emitted.

The Nested branch a few lines below builds its type string the same way and likely has the same weakness for names needing quoting.

Link

Analogous issue reported against the .NET client: ClickHouse/clickhouse-cs#502 (there the top-level case is broken too; in clickhouse-java only the Dynamic-nested case is).

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