Skip to content

Add UUID2 data type with correct sorting - #110084

Open
alexey-milovidov wants to merge 148 commits into
masterfrom
uuid2-proper-sorting
Open

Add UUID2 data type with correct sorting#110084
alexey-milovidov wants to merge 148 commits into
masterfrom
uuid2-proper-sorting

Conversation

@alexey-milovidov

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

Copy link
Copy Markdown
Member

Closes: #110066

Introduces UUID2, a variant of the UUID data type with correct (lexicographic) sorting.

Motivation

For historical reasons, the UUID data type sorts by the second half of the value. This is unexpected and, in particular, hurts the performance of primary indexes built on UUIDv7 columns, whose most significant bits are a timestamp: with sorting by the second half, primary-key analysis cannot prune granules by the timestamp.

What this does

UUID2 stores the 128-bit value as a plain big-endian integer of the 16 canonical bytes, so that natural integer comparison of the underlying value matches the textual (lexicographic) order and the canonical byte order used by most other systems. It reuses the UUID column and Field representation (like DateTime reuses UInt32), so sorting is correct with no extra comparison code, and its binary/interchange serialization is the canonical big-endian byte order.

  • UUID1 is an alias of the current UUID type.
  • A new setting uuid_type_version (default 1) controls whether the bare name UUID resolves to UUID (1) or UUID2 (2) at CREATE/ALTER time. The resolved concrete type is materialized into the stored table definition (including nested types such as Array(UUID)), so reads never depend on the session setting and existing tables are never rewritten. The default will be flipped to 2 in a later, separate change.
  • Conversions to/from String, UInt128, FixedString(16) and UUID, plus toUUID2 / toUUID2OrZero / toUUID2OrNull.
  • Parity across functions (hex/bin, reinterpretAs*, UUIDv7ToDateTime, UUIDToNum, empty/notEmpty, min/max, hashing, uniq), formats (RowBinary, Native, JSON, CSV, TSV, Arrow, Parquet, Avro, BSON, MsgPack, Protobuf, CapnProto, JSONExtract) and storage (generateRandom, bloom_filter skip index).

The UUID type is unchanged (verified: still sorts by second half, identical conversions, default uuid_type_version = 1).

Changelog category (leave one):

  • New Feature

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

Added a new data type UUID2, a variant of UUID that sorts by its textual (lexicographic) representation instead of by the second half of the value. The setting uuid_type_version (default 1) selects whether the type name UUID resolves to UUID or UUID2.

Documentation entry for user-facing changes

  • Documentation is written (mandatory for new features)

Workflow [PR]
Sync PR [sync-upstream/pr/110084]

alexey-milovidov and others added 3 commits July 11, 2026 01:12
The `UUID` data type sorts by the second half of the value for historical
reasons, which is unexpected and, in particular, hurts the performance of
primary indexes built on `UUIDv7` columns whose timestamp is in the first
half (see the note in the `UUID` documentation).

This introduces `UUID2`, a variant of `UUID` that stores the value as a plain
big-endian integer of the 16 canonical bytes, so that natural integer
comparison of the underlying value matches the textual (lexicographic) order
and the canonical byte order used by other systems. `UUID2` reuses the `UUID`
column and `Field` representation (like `DateTime` reuses `UInt32`), so sorting
is correct with no extra comparison code, and its binary serialization is the
canonical big-endian byte order.

Also:
- `UUID1` is added as an alias of the current `UUID` type.
- A new setting `uuid_type_version` (default 1) controls whether the bare type
  name `UUID` resolves to `UUID` (1) or `UUID2` (2) at `CREATE`/`ALTER` time.
  The resolved concrete type is materialized into the stored table definition
  (including nested types such as `Array(UUID)`), so reads never depend on the
  session setting, and existing tables are never rewritten. The default will be
  flipped to 2 in a later, separate change.
- Conversions to/from `String`, `UInt128`, `FixedString(16)` and `UUID`, plus
  `toUUID2` / `toUUID2OrZero` / `toUUID2OrNull` and `min`/`max`/`any`.

Implements the first part of #110066

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

Accept `UUID2` arguments in `hex`/`bin`, `reinterpretAs*`, `UUIDv7ToDateTime`
and `UUIDToNum`. For the layout-dependent functions (`hex`/`bin`,
`UUIDv7ToDateTime`, `UUIDToNum`) the big-endian `UUID2` value is converted to
the logical `UUID` layout with `UUIDHelpers::swapHalves` before reusing the
existing UUID code path, so results match `UUID` for the same textual value.

Hashing (`sipHash*`, `cityHash*`, `uniq*`), `GROUP BY`, `min`/`max`/`any` and
the `String`/`CSV`/`TSV`/`JSON`/`RowBinary`/`Native`/`Arrow` formats already
work for `UUID2`. Bitwise aggregates (`groupBitOr` etc.) are unsupported for
`UUID2` as they are for `UUID`.

Related: #110066

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add UUID2 support to the data formats and storage features that special-case
UUID. The uniform rule: UUID2 shares ColumnUUID with UUID but stores the value
with the two 64-bit halves swapped, so at the column boundary the value is run
through UUIDHelpers::swapHalves and the existing UUID code path is reused;
format-to-ClickHouse type inference keeps returning UUID.

- Formats: Parquet (native writer), Arrow (arrow.uuid extension), Avro, BSON,
  MsgPack, Protobuf, CapnProto (input and output), and JSONExtract.
- Storage/functions: generateRandom, bloom_filter skip index, empty/notEmpty.

The native Parquet writer switch keys on the column's data type (which is UUID
for both UUID and UUID2), so UUID2 is detected via the concrete type in
ColumnChunkWriteState::type; the Parquet reader reconciles the inferred UUID to
a UUID2 target through the existing UUID->UUID2 cast.

Extends the 04402_uuid2_data_type test with function, format-adjacent and
storage cases.

Related: #110066

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

mintlify Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
ClickHouse-docs 🟢 Ready View Preview Jul 11, 2026, 11:29 AM

Conflict in src/DataTypes/DataTypesBinaryEncoding.h: master took binary type
index 0x37 for QBitWithStride, so UUID2 moved from 0x37 to 0x38 and
BINARY_TYPE_INDEX_SIZE was bumped to 0x39. Also added the missing UUID2 rows
to the binary type encoding tables in the header comment and in
docs/en/sql-reference/data-types/data-types-binary-encoding.md.
…ent the `UUID2` binary encoding

After merging master, the first release that will contain the new setting is
26.7, so its entry in `SettingsChangesHistory.cpp` belongs to the "26.7"
block instead of the already-released "26.6".

Also document the `UUID2` type in the Native format specification (fixed-width
table, the `UUID` wire-encoding section, and the type diagram): the wire
encoding is the 16 canonical big-endian bytes with no half-swapping, and the
binary type encoding tag is `0x38` (`0x37` was taken by `QBitWithStride`
in master).
@clickhouse-gh

clickhouse-gh Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Workflow [PR], commit [90657e0]

Summary:

job_name test_name status info comment
Stress test (arm_tsan) FAIL
Logical error: Not-ready Set is passed as the second argument for function 'A (STID: 0250-41a5) FAIL cidb, issue
Integration tests (amd_asan_ubsan, db disk, old analyzer, 6/6) ERROR
Container memory budget exceeded (/docker) ERROR cidb
Parser memory check ERROR
Resolve master binary ERROR

AI Review

Summary

This PR adds the new UUID2 type, broadens format and function support around it, and materializes bare UUID to UUID2 under uuid_type_version = 2. Most of the earlier parity gaps are closed in the current head, but the rollout still leaves one user-visible Kusto path behind.

Findings

⚠️ Majors

  • [src/Functions/Kusto/FunctionKQLParameterCast.cpp:166-169] Typed Kusto guid parameters still reject UUID2. targetOf accepts guid only when the argument type is UUID, so a KQL function like let F = (g:guid) { g }; rejects a column created as g UUID under uuid_type_version = 2, or any explicit UUID2 argument, even though the rollout is supposed to keep the same logical UUID surfaces working. Suggested fix: accept UUID2 there as the same logical guid type and add a focused regression to 05047_kql_typed_parameter_enforcement.
Final Verdict

Needs changes: the uuid_type_version rollout is still incomplete for typed Kusto guid parameters.

LLVM Coverage Report

⚠️ No coverage measurement for commit 90657e0: incomplete coverage measurement: 1 of 21 shard profiles are missing: LLVM_COVERAGE_FILE_it_7.profdata.

@clickhouse-gh clickhouse-gh Bot added the pr-feature Pull request with new product feature label Jul 11, 2026
@clickhouse-gh

clickhouse-gh Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

@ClickHouse/integrations team, please, take a look

Reconcile the parallel master merge pushed to origin. The only content conflict
was in `DataTypesBinaryEncoding.h`: both sides keep `QBitWithStride = 0x37` and
`UUID2 = 0x38` (the enum's max, matching `BINARY_TYPE_INDEX_SIZE = 0x39`);
resolved to the compilable form with the trailing comma after `QBitWithStride`.
The `uuid_type_version` entry stays in the 26.7 settings-changes block and the
`UUID2` Native-format documentation is preserved.
The new `toUUID2`, `toUUID2OrZero` and `toUUID2OrNull` conversion
functions declare `UUID2` as their returned type in
`FunctionDocumentation`. `mapTypesToTypesWithLinks` in
`FunctionDocumentation.cpp` maps type names to documentation links and
throws `LOGICAL_ERROR` ("Unexpected data type in function ...") for any
type name it does not recognize. It knew about `UUID` but not `UUID2`,
so rendering the description of these functions threw.

Because the `description` column of `system.functions` and the
`system.documentation` table are rendered lazily, any query touching
them failed with the `LOGICAL_ERROR`, cascading into ~29 Fast test
failures (documentation web-UI tests, `system.functions`/
`system.documentation` tests, `help`/`man` command tests, and others
that read `system.functions`).

Add the missing mapping so `UUID2` links to
`/sql-reference/data-types/uuid2`. `Nullable(UUID2)` (used by
`toUUID2OrNull`) was already handled by the `Nullable` prefix branch.

CI report: https://s3.amazonaws.com/clickhouse-test-reports/json.html?PR=110084&sha=037bbe206b346508544b6adb3a803469054917cf&name_0=PR&name_1=Fast%20test

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread src/Processors/Formats/Impl/CHColumnToArrowColumn.cpp
Comment thread src/Interpreters/InterpreterAlterQuery.cpp Outdated
alexey-milovidov and others added 3 commits July 12, 2026 05:16
…o Binary

The default native Arrow / ArrowStream writer (`output_format_arrow_use_native_writer = 1`)
had no `UUID2` handling in `ArrowIPC::SchemaConverter` / `RecordBatchEncoder`. With the
default `output_format_arrow_unsupported_types_as_binary = 1`, a `UUID2` column was silently
emitted as a variable-width `Binary` column of the raw internal bytes instead of a real Arrow
`uuid` / `fixed_size_binary(16)` value, breaking schema fidelity and the default round-trip
through `FORMAT Arrow`.

Teach the native writer to treat `UUID2` like `UUID`: the schema maps it to
`fixed_size_binary(16)` flagged with the `arrow.uuid` extension, and the encoder applies
`UUIDHelpers::swapHalves` at the IPC boundary so the emitted bytes are identical to `UUID`
for the same textual value. This mirrors the Apache Arrow library writer path.

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

`ALTER TABLE ... ADD/MODIFY COLUMN x UUID` resolved the concrete type
(`UUID` vs `UUID2`, per `uuid_type_version`) on whichever node executed the command,
because `executeToTable` forwards the query to `ON CLUSTER` hosts and enqueues it into
a `Replicated` database DDL log *before* `parseAlterCommandSegments` runs. Unlike
`CREATE`, which bakes the concrete column types into the query AST on the initiator, a
bare `UUID` could therefore materialize as `UUID` on one node and `UUID2` on another when
nodes disagree on `uuid_type_version` (mixed-version rollouts, older
`distributed_ddl_entry_format_version`).

Mirror the `CREATE` flow: `materializeUUIDTypeVersion` resolves a bare `UUID` in
`ADD/MODIFY COLUMN` on the initiator before the query is forwarded, and
`parseAlterCommandSegments` keeps internal DDL (`isDDLOrOnClusterInternal`) verbatim
instead of re-resolving with the local node's setting, so every node agrees on the
initiator's decision.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Arrow library is not compiled into the fast-test build, so `FORMAT Arrow`
fails there and the query produces empty output. That made both the `UUID2` and
`UUID` branches emit nothing, so the byte-identity check passed trivially while
the round-trip lines were missing, and the test failed against its reference.

All other Arrow tests are already tagged `no-fasttest` for the same reason.

Verified locally with an Arrow-enabled build: the byte-identity check and the
round-trip both produce the exact reference output.

CI report: #110084

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread src/Interpreters/convertFieldToType.cpp
Comment thread src/Interpreters/InterpreterCreateQuery.cpp
alexey-milovidov and others added 2 commits July 12, 2026 08:14
`convertFieldToType` treated every `Field::Types::UUID` value as already
being in the destination encoding, but `UUID` and `UUID2` store the value
in layouts that differ by swapping the two 64-bit halves. When a `UUID`
constant is coerced into `UUID2` (or vice versa) - for example building an
`IN`-set for a `UUID2` key from a `toUUID(...)` constant, which reaches
`convertFieldToType` via `getSetElementsForConstantValue` with the source
type passed as `from_type_hint` - the halves were not swapped, so the
compared or stored value was wrong.

Detect the `UUID` <-> `UUID2` crossing from `from_type_hint` and apply
`UUIDHelpers::swapHalves` in that case; the same-type and no-hint cases
are left unchanged.

Addresses review feedback on #110084

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`uuid_type_version = 2` was ignored for `CREATE ... ON CLUSTER` when
`distributed_ddl_entry_format_version < NORMALIZE_CREATE_ON_INITIATOR_VERSION`.
That legacy path enqueues the query verbatim in `execute` before
`createTable` normalizes it on the initiator, and each worker then treats
the forwarded DDL as already-normalized internal DDL (forcing
`uuid_type_version = 1`), so a bare `UUID` column was created as the
historical `UUID` type instead of `UUID2`.

Materialize the setting into the column-declaration ASTs on the initiator
before the legacy enqueue, mirroring how `ALTER` and the modern `CREATE`
path bake in the concrete stored type.

Also extends `04402_uuid2_data_type` with the legacy `ON CLUSTER` case and
the Field-level `UUID`/`UUID2` coercion (`IN`-set) case.

Addresses review feedback on #110084

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread tests/queries/0_stateless/04402_uuid2_data_type.sql
alexey-milovidov and others added 2 commits July 12, 2026 09:36
`FunctionAnyHash` (the dispatcher behind `sipHash64`, `sipHash128`,
`cityHash64`, `halfMD5`, `xxHash64`, `farmHash64`, ...) had a specialized
branch only for `which.isUUID()`, hashing the value via
`executeBigIntType<UUID>`. `UUID2` fell through to `executeGeneric`, which
hashed `column->getDataAt(i)` - i.e. the raw in-memory bytes. Because `UUID`
and `UUID2` store the same logical value in layouts that differ by swapping
the two 64-bit halves, this made the hash functions return different results
for `x::UUID` and `x::UUID2` even when `x` is the same textual value, breaking
the hashing parity the PR promises (and that `hex`, `reinterpretAs*`,
`UUIDToNum`, etc. already provide for `UUID2`).

Add a `which.isUUID2()` branch that swaps the halves back into the `UUID`
layout with `UUIDHelpers::swapHalves` and then hashes through the existing
`executeBigIntType<UUID>` path, so `hash(x::UUID2)` equals `hash(x::UUID)`.
`Nullable(UUID2)` already worked via the `Nullable` branch and now inherits
the fix through the nested type.

Add a hashing-parity regression to `04402_uuid2_data_type`.

Addresses review feedback on #110084

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

The Fast test build is compiled without SSL, so `halfMD5` (which is
`#if USE_SSL`-gated) does not exist there. The hashing-parity block of
`04402_uuid2_data_type` called `halfMD5`, so the whole test failed with
`Code: 46. Function with name 'halfMD5' does not exist. (UNKNOWN_FUNCTION)`.

`halfMD5` is a `FunctionAnyHash`, exactly like `sipHash64`, `sipHash128`,
`cityHash64`, `xxHash64`, and `farmHash64`, so it exercises the same
`executeUUID2` code path as those functions. Dropping it keeps the test
running in the Fast test build while still covering `UUID2` hashing parity
via five representative functions, instead of tagging the whole test
`no-fasttest`.

CI report: https://s3.amazonaws.com/clickhouse-test-reports/json.html?PR=110084&sha=883b4ea2562dc0114f97dffc9e809f22d33d47a3&name_0=PR&name_1=Fast%20test

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread src/Interpreters/InterpreterCreateQuery.cpp
Comment thread src/Interpreters/InterpreterCreateQuery.cpp
alexey-milovidov and others added 2 commits July 12, 2026 12:28
When `uuid_type_version = 2` materializes a bare `UUID` column to `UUID2`,
a table created over an external database (`PostgreSQL`, `MySQL`, `SQLite`)
with such a column previously failed with `Unsupported type UUID2` because
`ExternalResultDescription::init` only recognized `UUID`.

Add a `vtUUID2` value type and handle it in the `PostgreSQL`, `MySQL`, and
`SQLite` readers (including the `PostgreSQL` array parser and the array
nested-column builder in `StoragePostgreSQL`). `UUID2` stores the value in
the canonical big-endian layout, while `parse<UUID>` produces the
half-swapped `UUID` layout, so the readers swap the halves back with
`UUIDHelpers::swapHalves` - matching `SerializationUUID2::deserializeText` -
so the textual value round-trips. The `PostgreSQL` write path is
serialization-driven and already correct for `UUID2`.

Add a unit test in `gtest_insertPostgreSQLValue` asserting that a
`PostgreSQL` `uuid` value read into a `UUID2` column round-trips to the
original textual value.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`UUID2` can now be used as a TimeSeries `id` type, and `uuid_type_version = 2`
consistently materializes a bare `UUID` id to `UUID2`.

Previously `SET uuid_type_version = 2` broke `TimeSeries` DDL: the inner
samples/tags columns were materialized to `UUID2` (by
`InterpreterCreateQuery::getColumnsDescription`) while the resolved id type
stayed `UUID`, so the consistency check between the inner tables failed with
`Column id in the Samples table has type UUID2, but expected UUID`.

Changes:
- `normalizeTimeSeriesDefinition` now bakes the `uuid_type_version` setting
  into the declared inner-column types before resolving them, and applies the
  same materialization to the built-in default id type. This keeps the samples
  and tags inner tables consistent and makes any later normalization pass read
  the already-materialized types. It is gated to a primary user CREATE
  (mirroring the gate in `getColumnsDescription`), so ATTACH, restore, and
  already-normalized DDL-worker queries are unaffected.
- `normalizeTimeSeriesDefinition` id-type validation and
  `checkArgumentTypeForID` (used by `timeSeriesStoreTags`, `timeSeriesIdToTags`,
  `timeSeriesIdToGroup`) now accept `UUID2`.
- `TimeSeriesIDGenerator::getDefault` generates a default id expression for a
  `UUID2` id column.

Adds test `04517_uuid2_time_series_id` and documents `UUID2` as an allowed id
type in the TimeSeries docs.

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

Copy link
Copy Markdown
Member Author

🕵 The only CI red on 40d45c45412c was Stress test (arm_tsan): logical error Equal values are not contiguous within the range assumed to be sorted raised from DistinctSortedStreamTransform over ColumnVector<Int256> — unrelated to this PR (UUID2 is 128-bit and does not use this path in the failing query). It is the known fleet issue #115455 (CI auto-matched it), and a candidate fix is already open: #110778. @groeneai, please drive that fix (or a separate one for #115455) to completion in its own PR.

Comment thread src/Core/Settings.cpp Outdated
@groeneai

Copy link
Copy Markdown
Collaborator

Thanks for routing this. One correction on which pull request owns that bucket, and then where it is stuck.

#110778 is not the fix for 2508-3319. It fixes a sibling bucket of the same assertion, 2508-348f: a stale sort property surviving applyActionsToSortDescription's hasArrayJoin() early return. Its whole diff is actionsDAGUtils.cpp plus a regression test, so it cannot reach the failing path here. In the 40d45c45412c stack the aborting column is ColumnNullable over ColumnVector<wide::integer<256>>, reached from DistinctSortedStreamTransform with no ARRAY JOIN in the shape.

The owner of 2508-3319 is #113242, the pull request you bumped on 08-18 for sibling 2508-3796. Same mechanism, different fuzzed target type: the fuzzer clones 04870_distinct_merge_over_distributed.sql's Merge column with s String retyped, and ReadFromMerge::convertAndFilterSourceStream applies that cast above the child's own sort while the initiator was told to merge pre-sorted streams. I verified it both directions on 08-19 and posted the measurements, including the release-visible wrong-results arm (9,9,9,9,9,9 where the correct maximum is 20): #113242 (comment). Same answer I gave @ Avogar on the issue: #115455 (comment).

Why #113242 has not landed. Its only functional red is Fast test / 00717_merge_and_distributed, and that is the pre-existing _table prefilter defect, reachable on master without this diff and without any cast: the prefilter evaluates the extracted _table predicate against the Merge child's own name, while the column is materialized from the resolved storage id, which at FetchColumns is the remote local table (root cause, why narrowing the gate is not an option). Making it green inside #113242 would require accepting UInt64 -> Int64 as order preserving, which is the exact case the pull request exists to refuse, so it is sequenced behind the prefilter fix #113735. #113735 has been at CHANGES_REQUESTED from @ novikd since 08-18 ("looks too complicated and very strange"); I answered his one inline question the same evening and it has been waiting on that call for six days.

So the chain is #113735, then #113242, and 2508-3319 stops. Family breadth over the last 14 days is 109 rows across 98 pull requests with 5 true master rows, 3319 being the largest single bucket at 19 rows.

Two ways forward, and I will take whichever you prefer:

  1. Unblock Fix a _table or _database filter silently returning no rows over a Merge table #113735. It needs a design call on @ novikd's review, not more code from me.
  2. Land Do not push sorting into Merge children across an order-breaking cast #113242 first with 00717_merge_and_distributed temporarily adjusted for the known prefilter row loss, restored when Fix a _table or _database filter silently returning no rows over a Merge table #113735 merges. I have not done this on my own because it weakens an existing test.

#110778 is a real fix for its own bucket and is unrelated to this. It is 12228 commits behind master but merges clean, and it has been sitting with a green matrix and no review since 08-10, so I will leave it alone unless you want it moved.

# Conflicts:
#	docs/_site/customizations/settings-legacy-routes/session-settings.js
#	docs/reference/functions/regular-functions/other-functions.mdx
#	src/Analyzer/Passes/FunctionToSubcolumnsPass.cpp
#	src/Functions/array/arrayElement.cpp
#	src/Processors/QueryPlan/ReadFromRemote.cpp
#	src/Storages/MergeTree/MergeTreeIndexBloomFilter.cpp
Follow-ups to the master merge, all mechanical:

- `SerializationUUID2::deserializeBinaryBulk` still had the `rows_offset`
  parameter that master dropped in "Remove rows_offset from the
  deserialization API", so it no longer overrode the virtual method.
- The `LowCardinality` branch of `getFieldFromColumnForASTLiteralImpl` did
  not pass the `datetime64_as_numbers` argument that master added.
- `tryBuildAdditionalFilterAST` now folds two independent fixes: master
  serializes decimal-backed constants exactly, and this branch serializes
  a `UUID2` constant as canonical text (its raw `Field` is formatted with
  `UUID` semantics and would be reparsed as a different value on the
  shard). The `UUID2` path is selected by a new file-local
  `typeMayContainUUID2`, so the raw-`Field` literal is kept for everything
  else, including `DateTime`.
The description promised that the setting covers "every expression a
`CREATE` or `ALTER` persists", but a type name carried inside a string is
only recognized when that string is a literal or is built from the string
functions `foldConstantStringExpression` mirrors without an execution
context (`concat`, `replace`, `replaceOne`, `replaceAll`, `upper`,
`ucase`, `lower`, `lcase`). `CAST(x, trim(' UUID '))` stays historical
`UUID`, as `05038_uuid2_constant_string_folding` already asserts.

Document that limitation instead of implying a broader guarantee, and
regenerate the reference documentation.
# Conflicts:
#	docs/_site/customizations/settings-legacy-routes/session-settings.js
#	docs/snippets/components/SessionSettingsExplorer/SessionSettingsExplorer.jsx
#	src/Interpreters/InterpreterAlterQuery.cpp
The entry was recorded under `26.8`, but the development version is now
`26.9`, so the `settings_changes_history` style check failed.
Conflict resolutions:
- `src/Parsers/Kusto/KustoFunctions/KQLStringFunctions.cpp`: master removed the KQL
  (Kusto) dialect implementation, so the branch's `UUID2` acceptance in
  `base64_encode_fromguid` is moot; took the deletion.
- `tests/queries/0_stateless/02366_kql_func_string.{sql,reference}`: took master's
  side for the same reason (the `base64_encode_fromguid` cases were removed there).
- The two generated docs blobs (`settings-legacy-routes/session-settings.js`,
  `SessionSettingsExplorer.jsx`): took master's generated data verbatim and
  re-inserted only the `uuid_type_version` record, bumping the group counts.
`uuid_type_version = 2` materializes the schema string of a table function in a
persisted definition. `substituteBareUUIDInPlace` used to call
`substituteBareUUIDInTableFunction` for every `ASTFunction`, recognizing the carrier
by name alone. But `format` is both the table function
`format(format_name, structure, data)` and the regular scalar string-formatting
function `format(pattern, ...)`, so a persisted
`SELECT format('{} {}', 'id UUID', 'x')` had its plain data argument rewritten to
`id UUID2`, silently changing the stored query.

The rewrite is now driven by the position of the call: the `table_function` of an
`ASTTableExpression`, the `as_table_function` of an `ASTCreateQuery`, and the whole
argument subtree of such a call (so that a table function nested in a wrapper, for
example `loop(url(...))`, is still reached).

Addresses the review finding on `src/Parsers/ASTDataType.cpp`.
A dictionary-encoded (`LowCardinality`) column written by the Apache Arrow library
writer cannot carry the `arrow.uuid` extension keys, because the registered extension
type rejects dictionary storage at read time. `CHColumnToArrowColumn` therefore marks
such a column with the ClickHouse-specific discriminator (`ClickHouse:type`) alone.
The native Arrow IPC reader still required `arrow.uuid` before honoring that key, so
`output_format_arrow_use_native_writer = 0` plus
`output_format_arrow_low_cardinality_as_dictionary = 1` plus
`input_format_arrow_use_native_reader = 1` read the column back as
`LowCardinality(FixedString(16))` instead of `LowCardinality(UUID2)` (and likewise
lost a plain `UUID`).

`isUUIDField` now treats the discriminator as authoritative on the fixed-size-binary
path, which also makes `isUUID2Field` work for that writer/reader combination.

Addresses the review finding on
`src/Processors/Formats/Impl/ArrowIPC/SchemaConverter.cpp`.
…olumn

`ReplacingConstantExpressionsMatcher` re-encodes a folded `UUID`/`UUID2` constant as
text, because an untyped `ASTLiteral` cannot tell the two apart. The text form is
itself ambiguous against a `Variant`, `Dynamic` or `JSON` column: the `DataTypeVariant`
branch of `convertFieldToType` accepts a string into the `String` alternative, so
`WHERE v = toUUID2('...')` on a `Variant(String, UUID2)` sharding key was analyzed as
a `String` and `sipHash64(v)` could select the wrong shard, pruning away the shard
that owns the matching row.

Neither representation is unambiguous there, so such tables are no longer pruned at
all: the constant is left unfolded and `evaluateExpressionOverConstantCondition`
returns no definite answer, which means every shard is queried. A plain `UUID2`
column is unambiguous and keeps being pruned.

Addresses the review finding on `src/Storages/StorageDistributed.cpp`.
The `Docs examples` check runs every documented snippet and compares its output. The
`toUUID2`, `dictGetUUID2` and `dictGetUUID2OrDefault` examples carried a hand-written
response with a truncated (`⋯`) header and, for `toUUID2`, right-aligned column
padding, neither of which the runner produces. The documented responses now match the
actual output, and the generated pages were regenerated accordingly.
pull Bot pushed a commit to Mu-L/ClickHouse that referenced this pull request Aug 31, 2026
An Enum8 or Enum16 constant compared with a String or FixedString column was
converted to the enum's underlying number instead of its name.

convertFieldToTypeImpl had a String to Enum case (via IDataTypeEnum::castToValue)
but no case for the other direction. An enum constant arrives as a numeric Field,
so the target-is-string branch handled only Field::Types::String and everything
else fell through to applyVisitor(FieldVisitorToString(), src), which stringified
the number. The converted constant was '1' rather than the name 'V0', and every
consumer that builds a point, range or hash from that Field used the wrong bytes.
EXPLAIN indexes = 1 showed Condition: (v in ['1', '1']).

Five surfaces returned wrong results, all on ordinary SELECT statements with no
forced index: primary key and PARTITION BY pruning dropped matching rows, the
bloom_filter skip index dropped them, IN and the values table function matched
and inserted the wrong value, NOT IN was inverted and returned the row it should
have excluded, and the OR-chain to IN rewrite silently dropped a disjunct. Since
CAST(enum AS String) has always produced the name, = and IN disagreed on the same
constant, which is what makes this a bug rather than a convention.

Resolve the name through IDataTypeEnum::castToName, mirroring the existing inverse
case, then re-enter convertFieldToTypeImpl so a FixedString target still zero-pads
the name to its width. The source type hint is unwrapped first because only the
target type is unwrapped by the caller, and Nullable(Enum) reaches this code with
the wrapper still attached. Fixing the primitive corrects every consumer at once,
and pruning is preserved: an enum constant now reads the same granule count as the
equivalent String literal.

An enum element inside an Array, Tuple or Map is still converted to the number,
because the container recursion passes no element type hint down. That propagation
is being added by ClickHouse#110084, so it is not duplicated here; three test cells assert
the current behaviour so the gap cannot rot silently.
# Conflicts:
#	src/Core/Settings.cpp
#	src/Storages/StorageTimeSeries.cpp
#	src/Storages/TimeSeries/TimeSeriesIDGenerator.cpp
#	src/Storages/TimeSeries/normalizeTimeSeriesDefinition.cpp
… helper

The `clickhouse_spelling` style check rejects the `clickHouse` case variant,
so rename `clickHouseUUIDTypeMetadata` to `getClickHouseUUIDTypeMetadata`.
Master now wraps the default `TimeSeries` `id` component in `LowCardinality`
and lets `TimeSeriesIDGenerator` choose an expression for a `LowCardinality`
identifier type. The `uuid_type_version = 2` default therefore becomes
`Tuple(UInt64, LowCardinality(UUID2))`; update the two references that pin
the default identifier type, and mirror the merged documentation text in the
generated `time-series.mdx`.
`04652_enum_constant_vs_string_column` documented three cells as known
limitations and named this pull request as the change that adds the element
type hint propagation through the `Array`/`Tuple`/`Map` recursion of
`convertFieldToType` and through `createColumnFromConstantArray` in the bloom
filter condition. With the hint in place an `Enum` element inside a container
is converted to the name, so replace those cells with assertions of the fixed
behavior: `x[1]` yields `7`, and `hasAny`/`hasAll` no longer over prune.
# Conflicts:
#	src/Storages/TimeSeries/normalizeTimeSeriesDefinition.cpp

For historical reasons, the `UUID` data type is sorted by the second half of the value. This is unexpected and, in particular, hurts the performance of primary indexes built on `UUIDv7` columns, whose most significant bits are a timestamp (see the note in the [UUID](/reference/data-types/uuid) documentation for details).

`UUID2` stores the value so that it is sorted by its textual (lexicographic) representation, which matches the canonical byte order used by most other systems. In every other respect it is compatible with `UUID`: it accepts the same textual representation, occupies the same 16 bytes, and supports the same set of functions.

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.

This "supports the same set of functions" contract is still not true for typed Kusto parameters. The parser wraps let F = (g:guid) { ... } arguments in kqlParameterCast, and FunctionKQLParameterCast::targetOf still accepts guid only when removeNullable(removeLowCardinality(arg_type)) is UUID (src/Functions/Kusto/FunctionKQLParameterCast.cpp:166-169). That means a column created as g UUID under uuid_type_version = 2 - or any explicit UUID2 / Nullable(UUID2) / LowCardinality(UUID2) argument - is rejected at the call boundary for the same KQL function that worked before the rollout.

I think kqlParameterCast needs the same logical-guid widening you already added for base64_encode_fromguid, plus a focused regression in 05047_kql_typed_parameter_enforcement.

@clickhouse-gh clickhouse-gh Bot added the comp-datatype-wrapper Type modifiers/wrappers (Nullable, LowCardinality, etc.). label Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp-datatype-wrapper Type modifiers/wrappers (Nullable, LowCardinality, etc.). pr-autogenerated-docs PR that regenerates docs artifacts from source; exempt from the autogenerated-region edit guard pr-feature Pull request with new product feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fix the wrong ordering of the UUID data type

3 participants