Skip to content

Support reinterpreting an Array as a String in reinterpret#109383

Merged
alexey-milovidov merged 12 commits into
masterfrom
fix-reinterpret-array-to-string
Jul 18, 2026
Merged

Support reinterpreting an Array as a String in reinterpret#109383
alexey-milovidov merged 12 commits into
masterfrom
fix-reinterpret-array-to-string

Conversation

@alexey-milovidov

@alexey-milovidov alexey-milovidov commented Jul 4, 2026

Copy link
Copy Markdown
Member

reinterpret(<string>, 'Array(T)') was already supported, but the inverse reinterpret(<array of fixed size elements>, 'String') threw ILLEGAL_TYPE_OF_ARGUMENT ("Cannot reinterpret Array(...) as String") — even though getReturnTypeImpl already permitted it, since an Array whose element type is fixed size is unambiguously represented in a contiguous memory region.

The mismatch was in executeImpl, which dispatches source/destination type pairs through callOnTwoTypeIndexes. That helper has no case for the Array type index, so an Array source fell through to the two-argument error path — while Array destinations were already handled by a dedicated fallback block. ColumnArray::getDataAt already exposes each row's elements as a single contiguous byte range, so this case is now handled explicitly by feeding it to the existing executeToString, making reinterpret between Array and String symmetric.

Note: no LowCardinality-nested guard is needed for the source (unlike the Array destination), because source columns pass through recursiveRemoveLowCardinality before reinterpret runs, so Array(LowCardinality(Int32)) arrives as Array(Int32).

Closes: #109379

Changelog category (leave one):

  • Improvement

Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):

Support reinterpret of an Array of fixed-size elements as a String, the inverse of the existing reinterpret of a String/FixedString as an Array.

Documentation entry for user-facing changes

  • Documentation is written (mandatory for new features)

Version info

  • Merged into: 26.7.1.1143 (included in 26.7 and later)

`reinterpret(<string>, 'Array(T)')` was already supported, but the inverse
`reinterpret(<array of fixed size elements>, 'String')` threw
`ILLEGAL_TYPE_OF_ARGUMENT` ("Cannot reinterpret Array(...) as String"),
even though `getReturnTypeImpl` already allowed it: an `Array` whose element
type is fixed size is unambiguously represented in a contiguous memory region.

The mismatch was in `executeImpl`, which dispatches source/destination type
pairs through `callOnTwoTypeIndexes`. That helper has no case for the `Array`
type index, so an `Array` source fell through to the two-argument error path.
`ColumnArray::getDataAt` already exposes each row's elements as a single
contiguous byte range, so handle this case explicitly by feeding it to the
existing `executeToString`.

Closes: #109379

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@clickhouse-gh

clickhouse-gh Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Workflow [PR], commit [f89350c]

Summary:


AI Review

Summary

This PR extends reinterpret and reinterpretAsString to accept source Array values whose element type is fixed-size and contiguous in memory, by adding an explicit Array -> String execution path that copies each row's bytes verbatim. I re-checked the current head against every prior review thread and the current diff; the earlier trailing-zero, big-endian, LowCardinality, documentation, and regression-test gaps are all addressed, and I did not find a remaining correctness or contract issue in the current code.

Final Verdict
  • Status: ✅ Approve

@clickhouse-gh clickhouse-gh Bot added the pr-improvement Pull request with some product improvements label Jul 4, 2026
Comment thread tests/queries/0_stateless/04503_reinterpret_array_to_string.sql
alexey-milovidov and others added 2 commits July 4, 2026 03:42
`reinterpret(<empty array>, 'String')` triggered `UndefinedBehaviorSanitizer`
because `ColumnArray::getDataAt` returns an empty range with a null data
pointer for an empty array, and `executeToString` then called
`memcpy(dst, nullptr, 0)`. Passing a null pointer to `memcpy` is undefined
behavior even when the size is zero (its arguments are declared `nonnull`),
so the `arm_asan_ubsan` and `amd_asan_ubsan` builds aborted the server with
`undefined-behavior src/Functions/reinterpretAs.cpp`, which cascaded into
sibling reinterpret tests failing with `Connection reset by peer`.

Skip the copy when the source range is empty.

CI report: https://s3.amazonaws.com/clickhouse-test-reports/json.html?PR=109383&sha=ed61b8ad23efe55fc81a3575b05b31c63e871676&name_0=PR&name_1=Stateless%20tests%20%28arm_asan_ubsan%2C%20targeted%29

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `Array -> String` branch reused the scalar `executeToString`, which trims
trailing zero bytes. That trimming is correct for scalar `reinterpretAsString`
(the `String -> scalar` path pads the value back with zeros), but wrong for
arrays: the `String -> Array` path requires the byte length to be an exact
multiple of the element size and does not pad, so trimming broke the round-trip
that this PR advertises as the inverse of `reinterpret(<string>, 'Array(T)')`.

For example `[toInt32(1)]` produced a 1-byte string that can no longer be
reinterpreted as `Array(Int32)` (it throws `BAD_ARGUMENTS`), and
`[toUInt8(1), toUInt8(0)]` silently round-tripped as `[1]`.

Add `executeContiguousToString`, which copies each row's contiguous byte range
verbatim (keeping the null-pointer guard for empty `Array` sources), and route
the `Array -> String` branch through it. Extend the test with trailing-zero
`hex` checks and round-trip regressions for `Array(UInt8)` and `Array(Int32)`.

Addresses the AI review "Request changes" verdict on
#109383

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread tests/queries/0_stateless/04503_reinterpret_array_to_string.sql
alexey-milovidov and others added 2 commits July 4, 2026 06:05
Address the AI-review nit on #109383: the structured `FunctionDocumentation`
for `reinterpretAsString` still advertised only scalar inputs and stated that
trailing zero bytes are always ignored. Since this PR added the `Array` of
fixed-size elements -> `String` path, the docs are now inaccurate.

Update the description to note that for an `Array` the element bytes are copied
verbatim (trailing zero bytes are NOT trimmed, so the result round-trips back to
the original `Array` type), add `Array` to the accepted argument types, and add
an `Array` example mirroring the existing `reinterpret` one. The example output
`02010403` matches the verified reference of the identical query in
`04503_reinterpret_array_to_string`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread src/Functions/reinterpretAs.cpp Outdated
alexey-milovidov and others added 2 commits July 13, 2026 04:29
The `Array -> String` reinterpret helper `executeContiguousToString`
reversed the whole row buffer on big-endian via `reverseMemcpy`. That
broke the round-trip with `String -> Array`: the `String -> Array` path
(`ColumnArray::insertData`) splits the string into element-sized chunks
and stores each chunk into the array element memory verbatim, with no
byte swapping on any endianness. A whole-row reverse therefore does not
invert it — for example `[toUInt16(0x0102), toUInt16(0x0304)]` has
in-memory bytes `01 02 03 04` on big-endian, a whole-row reverse yields
`04 03 02 01`, and reading that back as `Array(UInt16)` gives
`[0x0403, 0x0201]` instead of the original array.

Copy the bytes verbatim on every architecture, matching the
`String -> Array` semantics so the two directions are exact inverses.
Unlike the scalar `executeToString`, this path must not apply the
little-endian String byte-order convention, because `String -> Array`
does not apply it either.

No change on little-endian (the copy was already a plain `memcpy`
there). The existing multi-element round-trip regressions in
04503_reinterpret_array_to_string guard element order and would catch
this on a big-endian machine.

Addresses review comment:
#109383 (comment)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread src/Functions/reinterpretAs.cpp Outdated
…docs to scalar inputs

The `reinterpretAsString` `FunctionDocumentation` description said the
function reinterprets its input "assuming little endian order" as a
blanket statement, but that only holds for scalar inputs: `executeToString`
normalizes to little endian by reversing bytes on big-endian builds.
The `Array` path (`executeContiguousToString`) deliberately copies each
element's bytes verbatim in native in-memory order with no byte-order
normalization, since it must stay the exact inverse of
`reinterpret(<string>, 'Array(T)')` on every architecture. Scoped the
little-endian sentence to scalars and spelled out the `Array` exception.
Comment thread tests/queries/0_stateless/04503_reinterpret_array_to_string.sql Outdated
…dent

The AI review (`clickhouse-gh`) noted that the `hex` checks in
`04503_reinterpret_array_to_string.sql` and the two new `reinterpretAsString` /
`reinterpret` `FunctionDocumentation` examples hard-code the little-endian byte
layout. The `Array -> String` path (`executeContiguousToString`) copies each
element's bytes verbatim in native in-memory order (to match the verbatim
`String -> Array` path on every architecture), so `hex` of a multi-byte element
type differs between little- and big-endian builds. On a big-endian build (e.g.
s390x) `[toUInt16(0x0102), toUInt16(0x0304)]` yields `01020304`, not `02010403`,
and the `Int32` / `LowCardinality(Int32)` expectations change for the same
reason, so the test and the documented examples would be wrong there.

Keep byte-exact `hex` assertions only for the architecture-independent cases
(single-byte `Array(UInt8)` and `Array(FixedString(N))`, whose bytes are the same
on every architecture) and the table read. For multi-byte element types, assert
the byte count via `length` (element size times element count, independent of
byte order) and rely on the existing round-trip assertions to verify the element
values on every architecture. Replace the `LowCardinality(Int32)` `hex` check
with a round-trip. Change both documentation examples to an `Array(UInt8)` input
so the shown `hex` output (`0102FF`) is correct on every architecture.

No runtime logic changes; verified locally that the updated test matches the
reference.
Comment thread src/Functions/reinterpretAs.cpp
…einterpret

Address review on `src/Functions/reinterpretAs.cpp`: the review suggested the
`Array -> String` path is unreachable for non-numeric fixed-size `LowCardinality`
element types such as `Array(LowCardinality(FixedString(2)))` and
`Array(LowCardinality(UUID))`, because `DataTypeLowCardinality` inherits the
default `isValueUnambiguouslyRepresentedInFixedSizeContiguousMemoryRegion` (which
is true only for numeric-like dictionaries), so `getReturnTypeImpl` would reject
them with `ILLEGAL_TYPE_OF_ARGUMENT`.

Verified empirically that these queries already work. The framework recursively
strips nested `LowCardinality` (`recursiveRemoveLowCardinality`, via
`convertLowCardinalityColumnsToFull`) in `IFunctionOverloadResolver::getReturnType`
before `getReturnTypeImpl` runs, so the type gate already sees the plain
`Array(FixedString(2))` / `Array(UUID)` and the contiguous-memory check passes;
execution strips it the same way before `executeImpl`. No runtime change is needed
and the suggested extra `recursiveRemoveLowCardinality` in `getReturnTypeImpl`
would be dead code.

Extend `04503_reinterpret_array_to_string` with non-numeric fixed-size
`LowCardinality` cases (`FixedString(2)` hex + round-trip, `UUID` round-trip,
using architecture-independent assertions) to lock the behavior in.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@alexey-milovidov

Copy link
Copy Markdown
Member Author

@groeneai, could you investigate this Stress test (arm_debug) failure and provide a fix in a separate PR (or link an existing one/tracking issue)?

It is unrelated to this PR (which only adds Array -> String to reinterpret, and does not touch the Dynamic/Variant code): the message is thrown from src/Functions/FunctionDynamicAdaptor.cpp / src/Functions/FunctionVariantAdaptor.cpp. It is a widespread flake — on play.clickhouse.com the Cannot convert nested result of function% class fired 38 times across 34 distinct PRs in the last 30 days, including on master itself (pull_request_number = 0, most recent 2026-07-17 07:49:36), across several STIDs sharing the same 4811-* source location. It looks like the Dynamic/Variant-adaptor sibling of the fuzzer-internal-path assertions tracked in #107951. There is no fix on master yet, so I have not modified this PR for it.

Comment thread src/Functions/reinterpretAs.cpp
…tring

The `Array -> String` example added `reinterpret(<array>, 'String')` to
the public `reinterpret` contract, but the generic `reinterpret`
`FunctionDocumentation` still described only the old array-*destination*
restriction. Extend `description_reinterpret` and the `x` argument
documentation to state that a source `Array` must have fixed-size,
contiguous elements when the destination is `String`, so that
`Array(String)` and `Array(Nullable(Int32))` are documented as
unsupported (they throw `ILLEGAL_TYPE_OF_ARGUMENT` during type checking).

This is a documentation-only change; no runtime behavior is affected.
Addresses the AI review finding on PR #109383.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@groeneai

Copy link
Copy Markdown
Contributor

Confirmed unrelated to this PR. STID 4811-6023 is the JSON_EXISTS / JSON_VALUE / JSON_QUERY over Dynamic multi-path-tuple family, thrown from FunctionDynamicAdaptor.cpp: a JSON path function is declared as scalar UInt8 but executed over a multi-element / tuple path, so FunctionDynamicAdaptor gets a Tuple(UInt8, ...) nested result it cannot cast back to the declared type and aborts.

Reproducer from the same runs:

SELECT JSON_EXISTS(CAST('{"a": 1, "b": 2}', 'Dynamic'), tuple('$.b'));

Spread (CIDB, 30d): the Cannot convert nested result of function JSON_* signature has 40 stress-test hits across 35 distinct PRs, including master (pull_request_number = 0), most recent master hit 2026-07-17 07:49. Widespread flake, not caused by this PR.

Owner: fix PR #109944 (open, MERGEABLE), Closes: #110345. It addresses this exact FunctionDynamicAdaptor nested-result-cast family. No separate fix needed here.

@clickhouse-gh

clickhouse-gh Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

LLVM Coverage Report

Metric Baseline Current Δ
Lines 86.00% 86.10% +0.10%
Functions 91.80% 91.80% +0.00%
Branches 78.10% 78.20% +0.10%

Changed lines: Changed C/C++ lines covered: 66/67 (98.51%) · Uncovered code

Full report · Diff report

@alexey-milovidov alexey-milovidov left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Perfect.

@alexey-milovidov alexey-milovidov self-assigned this Jul 18, 2026
@alexey-milovidov
alexey-milovidov added this pull request to the merge queue Jul 18, 2026
Merged via the queue into master with commit 5bc24b2 Jul 18, 2026
178 checks passed
@alexey-milovidov
alexey-milovidov deleted the fix-reinterpret-array-to-string branch July 18, 2026 03:00
@robot-clickhouse-ci-1 robot-clickhouse-ci-1 added the pr-synced-to-cloud The PR is synced to the cloud repo label Jul 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr-improvement Pull request with some product improvements pr-synced-to-cloud The PR is synced to the cloud repo

Projects

None yet

Development

Successfully merging this pull request may close these issues.

I can reinterpret array of numbers as a String, but not the other way around

3 participants