Skip to content

Fix named Tuple/Nested columns whose element name requires backtick quoting - #504

Merged
alex-clickhouse merged 10 commits into
mainfrom
polyglot/cs-quoted-tuple-element-names
Aug 5, 2026
Merged

Fix named Tuple/Nested columns whose element name requires backtick quoting#504
alex-clickhouse merged 10 commits into
mainfrom
polyglot/cs-quoted-tuple-element-names

Conversation

@polyglotAI-bot

@polyglotAI-bot polyglotAI-bot commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Description

Stacked on #503 (base is polyglot/json-quoted-typed-paths, which makes the type tokenizer backtick-aware and adds the identifier helpers — including the shared StringExtensions.IndexOfNameTypeSeparator this PR consumes). Review/merge #503 first; GitHub will retarget this PR to main automatically when #503 merges.

ClickHouse reports a named Tuple / Nested element name back-quoted whenever it needs quoting, so the type string on the wire can be Tuple(`p q` Int64, r String) (verified on 26.5). TypeConverter.ExtractTypeName strips the element name by splitting the declaration on its first space, which cuts such a name in half: `p q` Int64 becomes `p / q` Int64, the element type resolves to q` Int64, and the whole query fails with ArgumentException: Unknown type. Any SELECT of a column with such a type is unreadable, and an insert into one fails the same way (the insert path resolves the destination column types through the same parser).

The fix locates the name/type separator past the quoted identifier instead of at the first space, by reusing the single shared scan StringExtensions.IndexOfNameTypeSeparator. That scan lives in #503 (it is the same scan the JSON typed-path fix needs, issue #502), so there is exactly one implementation and this PR only adds a call site.

Unquoted and unnamed elements keep resolving exactly as before, and the failure mode for a malformed declaration stays the same ArgumentException.

Changes

  • ClickHouse.Driver/Types/TypeConverter.cs: ExtractTypeName uses IndexOfNameTypeSeparator() instead of Split(" ", 2); the now-unused Separator field is removed.
  • CHANGELOG.md / RELEASENOTES.md: entry under Unreleased → Bug Fixes.

(The shared helper itself, and JsonType using it, are part of #503 — see that PR's Move the path/type separator scan into a shared string helper commit.)

Test

ClickHouse.Driver.Tests/Types/TupleTypeTests.cs:

  • Parse-level cases for quoted element names in Tuple and Nested: names containing a space, .+space, comma, parentheses, \``, ', \n, \, doubled backticks; scalar, Decimal(10, 2), Map(String, Array(Int32))andNullable(String)element types; quoted names wrapped inArray/Map/an outer Tuple; a single-element tuple; and a quoted name next to single-quoted element arguments (Enum8('x y' = 1, …), DateTime64(3, 'Europe/Amsterdam')`) so both quote kinds are honoured in one declaration.
  • Contrast cases pinning that unnamed and unquoted-named elements (Tuple(String, Int32), Tuple(name String, age Int32), Nested(Id Nullable(String), Comment Nullable(String)), …) resolve to exactly the same element types as before.
  • Malformed quoting (unterminated backtick, name with no type) still throws ArgumentException, for both Tuple and Nested.
  • Live-server round trips through the real entry points: reading Tuple(`p q` Int64, r String) and Nested(`a b` Decimal(10, 2), c String), and InsertBinaryAsync into a table column of type Tuple(`p q` Int64, r String) with a read-back.

15 of these fail without the ExtractTypeName change (ArgumentException: Unknown type: p q Int64) and all pass with it. Full ClickHouse.Driver.Tests suite on net10.0 after merging the current base: 9950 passed, 0 failed, 142 skipped — no existing test changed.

Pre-PR validation gate

  • Deterministic repro confirmed (select cast(tuple(toInt64(1), 'a') as Tuple(p q Int64, r String)) throws on the base branch)
  • Root cause documented above
  • Fix targets the root cause (separator location, not a symptom guard)
  • Tests fail without the fix, pass with it
  • No existing tests broken or weakened
  • Convention compliance verified per AGENTS.md (integration tests preferred, TestCase parametrization, three-part test names, CHANGELOG + RELEASENOTES updated, no public API change — all touched members are internal)

A JSON typed path may contain characters that need quoting, such as a space
or a comma; the server accepts, stores and round-trips those columns, and
reports the type as e.g. JSON(`a b` Int64). The driver could not parse that
type name, so the whole query failed with
"SerializationException: Unsupported path in JSON hint" — any table with
such a column was unreadable, with no way to opt out.

Two independent causes:

* Tokenizer only treated ' as a quote character, so a comma or parenthesis
  inside a backtick-quoted identifier split the token before it reached
  JsonType.Parse.
* JsonType.Parse split each hint on every space and required exactly two
  parts, and only trimmed backticks off the path instead of unescaping it.
  The path names in HintedTypes must match the raw path names on the wire,
  otherwise the hinted type is not applied.

Fixes: #502
…uoting

TypeConverter.ExtractTypeName stripped a named element's name by splitting
the declaration on its first space, which cuts a backtick-quoted name in
half: `p q` Int64 became "`p" / "q` Int64", so the element type resolved to
"q` Int64" and the whole query failed with ArgumentException: Unknown type.

The separator scan that the JSON typed-path fix introduced is factored out
of JsonType into StringExtensions.IndexOfNameTypeSeparator and reused here,
so the quoted identifier is skipped as a whole before the name/type
separator is located.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 7581fc8. Configure here.

Comment thread ClickHouse.Driver/Utility/StringExtensions.cs
…d JSON path

Adds the JSON(`a\` b` Int64) shape (path name "a` b") to
ParseShouldUnquotePathWhenPathIsBacktickQuoted. The server emits exactly this
form for such a path, and it is the one shape where the escape branch of
IndexOfPathTypeSeparator interacts with the separator search.
…typed-paths

# Conflicts:
#	CHANGELOG.md
#	ClickHouse.Driver.Tests/Types/JsonTypeTests.cs
#	RELEASENOTES.md
@alex-clickhouse

Copy link
Copy Markdown
Collaborator

There's some duplication between the two PRs with the IndexOfPathTypeSeparator, let's DRY please.

…typed-paths

# Conflicts:
#	CHANGELOG.md
#	RELEASENOTES.md
…' into polyglot/cs-quoted-tuple-element-names
The scan that skips a backtick-quoted identifier before locating the space
separating a name from its type is not specific to JSON typed paths: named
Tuple and Nested elements are spelled the same way and need the same scan.
Keep a single implementation by exposing it as
StringExtensions.IndexOfNameTypeSeparator instead of a private JsonType
method, so the other call sites can reuse it rather than copy it.

No behavior change for JSON hints. The shared helper additionally tolerates
a hand-written identifier that doubles a backtick the way SQL does (``);
the server always renders the backslash form.
@polyglotAI-bot

Copy link
Copy Markdown
Collaborator Author

Done — the scan now exists in exactly one place, and it is introduced once, in the base PR.

Concretely: IndexOfPathTypeSeparator is gone. #503 now adds StringExtensions.IndexOfNameTypeSeparator (commit Move the path/type separator scan into a shared string helper) and JsonType calls it; this PR's diff is down to the TypeConverter.ExtractTypeName call site plus its tests and the changelog entry — no helper code, no JsonType change. Previously #503 added the private copy and this PR immediately replaced it, which is the churn you were seeing across the stack.

Both branches have been pushed and rebased onto the current base. Full ClickHouse.Driver.Tests on net10.0: 9950 passed, 0 failed, 142 skipped.

Replace the parser-level type-string matrices with tests that create the
column (or cast/parameterize) on a live server and read the value back, so
every covered element name is one the server itself accepts and reports.
@polyglotAI-bot

Copy link
Copy Markdown
Collaborator Author

Done — the tests on this PR now go through the server instead of the type parser.

What changed (tests only, no library change in this push):

  • The three parser-level type-string matrices are gone. Every element name is now exercised end-to-end: CREATE TABLE … Tuple(<quoted name> Int64, r String)INSERTSELECT, so the type string under test is the one the server itself reports rather than one I wrote by hand.
  • Every name in the matrix is a form the server accepts in DDL and renders back in the column type — verified on 26.5.1 (toTypeName): `a b`, `a b c`, `a,b`, `a(b)`, `a.b c`, `a\'b`, `a\`b c`, `a\nb c`, `a\tb c`, `a\rb c`, `a\\`. The escape spellings are the server's own.
  • Coverage per code path, all against the DB: read of a table column; Nested(...) via CAST (with a parameterized element type — a bare-name element is resolved by NestedType itself and never reaches the parser); the quoted-named tuple wrapped in Array/Map/an enclosing named tuple, plus the single-element case; parameterized element types alongside single-quoted arguments containing a space (Decimal(10, 2), Map(String, Array(Int32)), Enum8('x y' = 1, 'z' = 2)); InsertBinaryAsync into such a column, over the full name matrix (the insert path resolves destination column types from the server, so it parses the same declaration); and a hand-written parameter type hint ({var:Tuple(p q Int64, r String)}), which is the one place a doubled-backtick spelling can legitimately arrive — the server accepts it and renders it back as ```.
  • One contrast case is deliberately kept green both ways: a Dynamic-wrapped named tuple, whose per-value type header is decoded structurally and so was never affected by this bug.
  • Only one parser-level test remains — malformed quoting (unterminated identifier, element with no type). That shape has no round-trip form because the server never sends it, but it still has to be rejected rather than silently mis-parsed.

Verification: with the fix reverted, 26 of the new cases fail (every name containing a space, across the read/Nested/wrapped/insert/parameter paths, plus all three parameter-hint spellings); with the fix, ClickHouse.Driver.Tests is 9960 passed / 0 failed / 142 skipped on net10.0 against a live 26.5.1 server. No existing test was touched.

Base automatically changed from polyglot/json-quoted-typed-paths to main August 5, 2026 14:06
…ple-element-names

# Conflicts:
#	CHANGELOG.md
#	ClickHouse.Driver.Tests/Types/JsonTypeTests.cs
#	RELEASENOTES.md
Copilot AI review requested due to automatic review settings August 5, 2026 14:32
@polyglotAI-bot

Copy link
Copy Markdown
Collaborator Author

Rebased onto main — PR is MERGEABLE again, head 5964a00. ⚠️ The diff shrank substantially in this merge — please re-read the note below before re-reviewing.

#503 (backtick-quoted JSON typed paths) landed on main and contains the same shared plumbing this PR carried: Utility/StringExtensions.cs and Types/Grammar/Tokenizer.cs are byte-identical between the two branches, and Types/JsonType.cs plus the JSON tests are a superset of what was here. Git therefore auto-merged those files silently, and this PR's diff now collapses to its genuinely unique part:

file delta
ClickHouse.Driver/Types/TypeConverter.cs +12 −5 — ExtractTypeName locates the name/type separator past a quoted identifier instead of splitting on the first space
ClickHouse.Driver.Tests/Types/TupleTypeTests.cs +195 — Tuple/Nested regression tests
CHANGELOG.md / RELEASENOTES.md +1 each

Nothing was dropped: the JSON-side work this branch used to carry is now on main via #503 (verified — git diff origin/main for StringExtensions.cs, Tokenizer.cs, JsonType.cs and SyntaxParsingTests.cs is empty), so there is no duplicated or dead code.

Conflicts resolved: JsonTypeTests.cs (3 hunks — kept main's superset, which includes the extra JSON(\a'b` Int64)case, the[RequiredFeature(Feature.Json)]gate and the round-trip/binary-write tests; no duplicate method names remain) andCHANGELOG.md/RELEASENOTES.md(kept both sides, main's#502` entry then this PR's).

Still load-bearing after #503 — I re-checked rather than assuming: with TypeConverter.cs reverted to main's version, 20+ of the new Tuple/Nested cases fail (ShouldRoundTripTupleColumn_…, ShouldRoundTripNested_…, ShouldInsertBinary_IntoTupleColumnWithBacktickQuotedElementName_…); with the fix, they pass. #503 fixed the JSON path surface only — named Tuple/Nested element names still went through the first-space split.

Verification: build clean on net10.0; focused TupleType + JsonType + SyntaxParsing + TypeConverter suite 200 passed / 0 failed against a live ClickHouse server. No existing test edited or weakened.

@codecov

codecov Bot commented Aug 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes ClickHouse type parsing for named Tuple/Nested element declarations whose element name is backtick-quoted and contains spaces (e.g. Tuple(`p q` Int64, r String)), by locating the name/type separator after a quoted identifier rather than splitting on the first space. This unblocks both reading and insert type resolution for such columns.

Changes:

  • Update TypeConverter.ExtractTypeName to use StringExtensions.IndexOfNameTypeSeparator() for stripping named element prefixes safely with backtick-quoted names.
  • Add end-to-end and parse-level tests covering quoted element names across Tuple, Nested, parameter type hints, composite wrappers, and malformed quoting.
  • Add release notes and changelog entries under “Unreleased → Bug Fixes”.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
ClickHouse.Driver/Types/TypeConverter.cs Uses a quote-aware separator scan to strip named tuple/nested element names without mis-splitting backtick-quoted identifiers.
ClickHouse.Driver.Tests/Types/TupleTypeTests.cs Adds coverage for quoted tuple/nested element names (read + insert paths, nested/composite cases, and malformed declarations).
CHANGELOG.md Documents the bug fix in the Unreleased section.
RELEASENOTES.md Mirrors the Unreleased bug-fix note for release notes consumers.

@alex-clickhouse
alex-clickhouse merged commit 79143f2 into main Aug 5, 2026
19 checks passed
alex-clickhouse added a commit that referenced this pull request Aug 5, 2026
main added 22 new Unreleased entries since this branch was cut. Each is now
its own changelog.d/ fragment, extracted verbatim by line number rather than
retyped, so the assembled Unreleased section reproduces main's exactly (as a
set of lines; sorting by PR number reorders entries within their sections).

New fragments, one per (PR, category):

  #390 improvements   multidim blittable inserts
  #472 improvements   per-scalar Span<byte> reads
  #484 fixes          byte[]/TimeOnly HTTP parameters
  #485 fixes          JSON strings under ReadStringsAsByteArrays
  #490 breaking       raw results return compressed bytes
  #490 features       AcceptEncoding response compression
  #490 improvements   lz4 by default, HttpClient, errors, deflate
  #492 fixes          HTTP response disposal
  #493 fixes          Enum type declarations
  #494 fixes          raw-stream double dispose
  #497 fixes          GetSchema("Columns") restrictions
  #498 fixes          JSON paths starting with setting names
  #503 fixes          quoted JSON typed paths
  #504 fixes          quoted Tuple/Nested element names
  #509 fixes          {name:Type} scanner vs server lexer
  #511 fixes          {name:Type} hints after a non-hint brace
  #513 fixes          @name placeholders, heredocs, $ in names

#390's entry was appended to the *released* v1.3.0 section on main (v1.3.0
shipped 2026-06-29), so it would have documented an unreleased change under a
shipped version and never appeared in 1.4.0's notes. It moves to Unreleased as
a fragment; the rest of v1.3.0 is byte-identical.

RELEASENOTES.md regenerated with --sync-notes. `--check` passes, the solution
builds, and the packed .nupkg's releaseNotes open on v1.3.0 with no Unreleased
stub and no #390 bullet.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants