ClickHouse / ClickHouse/clickhouse-cs
Types: named Tuple/Map element of type JSON is unreadable — "Unknown type: x JSON"
- Dominant language
- C#
- Stars
- 94
- Forks
- 22
- Avg merge
- 11h 26m
- Merged PRs (30d)
- 22
Description
### Describe the bug
A column whose type contains a **named Tuple element of type `JSON`** cannot be read. Any
`SELECT` of such a column throws `ArgumentException: Unknown type: x JSON` before the first
row is returned.
The server always reports this element type with the spelling `JSON`. `JSON` is an *alias*
in the driver's type registry (`Aliases["JSON"] = "Json"`), and `TypeConverter.ExtractTypeName`
resolves the alias **before** it strips the element name — so the alias lookup is attempted on
`"x JSON"`, misses, and the name that survives (`"JSON"`) is never mapped to the registered
type name (`"Json"`).
There is **no server-side workaround**: declaring the column as `Tuple(x Json(...))`
(canonical spelling) is normalized by the server back to `Tuple(x JSON(...))`, so it fails
identically.
`JSON` appears to be the only alias reachable this way. Every other entry in `Aliases` is
normalized to its canonical name by the server before it reaches the client
(`Tuple(x INT)` -> `Tuple(x Int32)`, `Tuple(x TIMESTAMP)` -> `Tuple(x DateTime)`, etc.), so
those spellings never appear in a server-declared type.
### Steps to reproduce
1. Create a table with a named Tuple element of type `JSON`.
2. Insert one row.
3. `SELECT` the column with `ClickHouseClient.ExecuteReaderAsync`.
### Expected behaviour
The column is read successfully, the same way an unnamed `JSON` column
(`JSON(a Int64)`) and a named element of a non-aliased type (`Tuple(x Int32)`) already are.
The server considers the type valid and reports it as such:
```
$ curl -s --data-binary "DESCRIBE TABLE probe_tup" http://localhost:8123/
c Tuple(\n x JSON(a Int64))
```
### Code example
```csharp
using var client = new ClickHouseClient("Host=localhost;Port=8123");
await client.ExecuteNonQueryAsync("DROP TABLE IF EXISTS probe_tup");
await client.ExecuteNonQueryAsync(
"CREATE TABLE probe_tup (c Tuple(x JSON(a Int64))) ENGINE=Memory " +
"SETTINGS allow_experimental_json_type=1");
await client.ExecuteNonQueryAsync("INSERT INTO probe_tup VALUES (tuple('{\"a\":1}'))");
using var reader = await client.ExecuteReaderAsync("SELECT c FROM probe_tup");
while (await reader.ReadAsync())
Console.WriteLine(reader.GetValue(0)); // throws
```
### Error log
```
System.ArgumentException : Unknown type: x JSON(a Int64)
at ClickHouse.Driver.Types.TypeConverter.ParseClickHouseType(SyntaxTreeNode, TypeSettings)
TypeConverter.cs:302
at ClickHouse.Driver.Types.TupleType.Parse(...) TupleType.cs:116
```
Observed matrix (each row = create table, insert one row, `SELECT` through
`ExecuteReaderAsync`; server version 26.7.3.19):
| Declared type | Server-reported type | Result |
| --- | --- | --- |
| `Tuple(x JSON(a Int64))` | `Tuple(x JSON(a Int64))` | ❌ `Unknown type: x JSON(a Int64)` |
| `Tuple(x Json(a Int64))` | `Tuple(x JSON(a Int64))` | ❌ `Unknown type: x JSON(a Int64)` |
| `Tuple(x JSON)` | `Tuple(x JSON)` | ❌ `Unknown type: x JSON` |
| `Array(Tuple(x JSON))` | `Array(Tuple(x JSON))` | ❌ `Unknown type: x JSON` |
| `Map(String, Tuple(x JSON))` | `Map(String, Tuple(x JSON))` | ❌ `Unknown type: x JSON` |
| `JSON(a Int64)` (unnamed) | `JSON(a Int64)` | ✅ |
| `Nested(x JSON)` | `Nested(x JSON)` | ✅ |
| `Tuple(x Int32)` | `Tuple(x Int32)` | ✅ |
| `Tuple(x INT)` | `Tuple(x Int32)` | ✅ (server normalizes) |
| `Tuple(x DECIMAL(10, 2))` | `Tuple(x Decimal(10, 2))` | ✅ (server normalizes) |
| `Tuple(x TIMESTAMP)` | `Tuple(x DateTime)` | ✅ (server normalizes) |
| `Tuple(x BOOL)` | `Tuple(x Bool)` | ✅ (server normalizes) |
| `Tuple(x TEXT)` | `Tuple(x String)` | ✅ (server normalizes) |
### Root cause
`ClickHouse.Driver/Types/TypeConverter.cs:246-270` — `ExtractTypeName` resolves the alias
before stripping the element name:
```csharp
var typeName = node.Value.Trim().Trim('\''); // "x JSON(a Int64)" -> "x JSON"
if (Aliases.TryGetValue(typeName.ToUpperInvariant(), out var alias))
typeName = alias; // "X JSON" -> no match
if (typeName.Contains(' '))
{
var separator = typeName.IndexOfNameTypeSeparator();
if (separator > 0)
typeName = typeName.Substring(separator + 1).Trim(); // -> "JSON"
...
}
return typeName; // "JSON", never alias-resolved
```
`Aliases["JSON"] = "Json"` (TypeConverter.cs:96), but `SimpleTypes` and
`ParameterizedTypes` are keyed by the registered `Name` (`"Json"`), so the returned
`"JSON"` misses both lookups and `ParseClickHouseType` falls through to
`throw new ArgumentException("Unknown type: " + ...)` at TypeConverter.cs:302.
This is a different mechanism from #504 / PR #504, which fixed *where* the name/type
separator is located (`IndexOfNameTypeSeparator`). Here the separator is found correctly;
the defect is the ordering of alias resolution relative to name stripping.
### Suggested fix
Strip the element name **first**, then resolve the alias — a single resolution point:
```csharp
var typeName = node.Value.Trim().Trim('\'');
if (typeName.Contains(' ') && !Aliases.ContainsKey(typeName.ToUpperInvariant()))
{
// strip the element name
}
if (Aliases.TryGetValue(typeName.ToUpperInvariant(), out var alias))
typeName = alias;
```
Note the contrast case that must keep working: several aliases **contain a space**
(`BIGINT UNSIGNED`, `DOUBLE PRECISION`, `NATIONAL CHARACTER VARYING`, …). A naive
"strip before the first space" reordering would break those, so the alias table has to be
consulted for the whole string before deciding the string is a named element — or the
alias resolved on both the full string and the stripped remainder.
Suggested regression coverage (integration tests preferred per AGENTS.md): read a
`Tuple(x JSON(a Int64))` column, plus `Array(Tuple(x JSON))` and
`Map(String, Tuple(x JSON))`, and keep an assertion that a space-containing alias such as
`DOUBLE PRECISION` still resolves.
### Configuration
#### Environment
* Client version: `main` @ dd51bfe
* .NET version: 10.0
* OS: Ubuntu 24.04 (linux-x64)
#### ClickHouse server
* ClickHouse Server version: 26.7.3.19
* ClickHouse Server non-default settings: `allow_experimental_json_type=1`
* `CREATE TABLE` statements:
```sql
CREATE TABLE probe_tup (c Tuple(x JSON(a Int64))) ENGINE=Memory
SETTINGS allow_experimental_json_type=1;
```
* Sample data: `INSERT INTO probe_tup VALUES (tuple('{"a":1}'));`
Contributor guide
Research direction
Start with ClickHouse.Driver/Types/TypeConverter.cs, especially ExtractTypeName and the alias table, then read AGENTS.md for the integration-test conventions. Reproduce the named JSON tuple failure and add coverage for Tuple, Array(Tuple), and Map(String, Tuple), while retaining a space-containing alias case; done means these types read successfully without regressing existing aliases.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- databases
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100