This is an automated email from the ASF dual-hosted git repository.
morningman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new ab963392b1e [fix](be) Convert an encoded Variant UUID to STRING like a
typed one (#68938)
ab963392b1e is described below
commit ab963392b1ef3c5cff506a24bb4064848e2ece97
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Tue Oct 13 15:09:48 2026 +0800
[fix](be) Convert an encoded Variant UUID to STRING like a typed one
(#68938)
### What problem does this PR solve?
Issue Number: None
Related PR: #65561, #67627, #68935
Problem Summary:
**In short.** A Variant holds a UUID either as a typed value or as an
encoded Parquet Variant value, and `CAST(... AS STRING)` gives different
text for the two: the typed one becomes
`00112233-4455-6677-8899-aabbccddeeff`, the encoded one becomes the JSON
string `"00112233-4455-6677-8899-aabbccddeeff"`, quotes included. This
PR makes the encoded form convert like the typed one. It is not a
branch-4.x port (branch-4.x has no UUID type); it came up while testing
#68935.
**Background.** `ColumnVariantV2` keeps a column either typed (one Doris
scalar type) or encoded (Parquet Variant bytes), and the two states are
meant to give the same results; #67627 already checks that a typed and
an encoded UUID hash the same. For a STRING target,
`cast_variant_to_string.cpp` sends each encoded scalar either through
the concrete cast of its Doris type or to JSON text. #65561 wrote that
split before Doris had a UUID type, so `VariantPrimitiveId::UUID` went
to the JSON side together with BINARY and TIME_NTZ, which still have no
Doris type. #67627 then added the UUID type and the encoded-UUID group
in `cast_variant_to_scalar.cpp` (so `CAST(... AS UUID)` of an encoded
value works), but left the STRING split unchanged.
**The problem, and what it cost.** An encoded UUID shows up as soon as
the value goes through an encoded Variant: an element of a Variant
array, or a Paimon VARIANT column read back.
| Expression | master | with this PR |
|---|---|---|
| `CAST(CAST(CAST(u AS UUID) AS VARIANT) AS STRING)` (typed) |
`00112233-…` | `00112233-…` |
| `CAST(CAST(ARRAY(CAST(u AS UUID)) AS VARIANT)[1] AS STRING)` (encoded)
| `"00112233-…"` | `00112233-…` |
| `CAST(CAST(ARRAY(CAST(u AS UUID)) AS VARIANT) AS ARRAY<STRING>)` |
`[""00112233-…""]`: the element itself carries the quotes |
`["00112233-…"]` |
| `CAST(v AS STRING)` on a Paimon VARIANT column holding a UUID |
`"6ba7b810-…"` | `6ba7b810-…` |
| `CAST(CAST(ARRAY(CAST(u AS UUID)) AS VARIANT)[1] AS JSON)` |
`"00112233-…"` | unchanged (a JSON string) |
So the same value printed differently depending on how it was stored,
and a comparison such as `CAST(v AS STRING) = '00112233-…'` failed for
the encoded form. DATE, DECIMAL and TIMESTAMP values were not affected:
they already used their concrete cast.
**How this PR fixes it.** `uses_concrete_string_cast` now sends
`VariantPrimitiveId::UUID` to the concrete cast, which decodes the value
into the UUID group and casts it with the UUID-to-STRING cast, the same
path a typed UUID takes. BINARY and TIME_NTZ stay JSON text. `CAST(...
AS ARRAY<STRING>)` uses the same rule for its elements, so it changes
too; JSON output is a separate path and keeps the quoted string.
**Results.** The encoded and typed forms now give the same STRING, in a
whole-column cast, in a column that mixes UUIDs with JSON-only values,
and as `ARRAY<STRING>` elements.
---
.../cast/variant_v2/cast_variant_to_string.cpp | 2 +-
.../function/cast/cast_variant_v2_from_test.cpp | 91 +++++++++++++++++++---
.../uuid/test_uuid_variant_identity.out | 3 +
.../uuid/test_uuid_variant_identity.groovy | 10 +++
4 files changed, 94 insertions(+), 12 deletions(-)
diff --git a/be/src/exprs/function/cast/variant_v2/cast_variant_to_string.cpp
b/be/src/exprs/function/cast/variant_v2/cast_variant_to_string.cpp
index 069e58fc2c1..e218cc4e438 100644
--- a/be/src/exprs/function/cast/variant_v2/cast_variant_to_string.cpp
+++ b/be/src/exprs/function/cast/variant_v2/cast_variant_to_string.cpp
@@ -55,11 +55,11 @@ bool uses_concrete_string_cast(VariantRef value) {
case VariantPrimitiveId::STRING:
case VariantPrimitiveId::TIMESTAMP_NANOS:
case VariantPrimitiveId::TIMESTAMP_NTZ_NANOS:
+ case VariantPrimitiveId::UUID:
return true;
case VariantPrimitiveId::NULL_VALUE:
case VariantPrimitiveId::BINARY:
case VariantPrimitiveId::TIME_NTZ_MICROS:
- case VariantPrimitiveId::UUID:
return false;
}
throw Exception(ErrorCode::CORRUPTION, "Unknown Variant primitive id");
diff --git a/be/test/exprs/function/cast/cast_variant_v2_from_test.cpp
b/be/test/exprs/function/cast/cast_variant_v2_from_test.cpp
index 8ddbfc88bf4..69089d94058 100644
--- a/be/test/exprs/function/cast/cast_variant_v2_from_test.cpp
+++ b/be/test/exprs/function/cast/cast_variant_v2_from_test.cpp
@@ -41,6 +41,7 @@
#include "core/data_type/data_type_uuid.h"
#include "core/data_type/data_type_variant_v2.h"
#include "core/field.h"
+#include "core/value/uuid_value.h"
#include "core/value/variant/variant_batch_builder.h"
#include "core/value/vdatetime_value.h"
#include "exprs/function/cast/variant_v2/cast_variant_v2.h"
@@ -416,11 +417,7 @@ TEST(CastVariantV2FromTest,
TypedStringValidatesOnlyVisibleDateRows) {
TEST(CastVariantV2FromTest, EOnlyScalarStringRulesStayJsonQuoted) {
const std::array<char, 2> binary {0, 1};
- std::array<uint8_t, 16> uuid {};
- for (size_t index = 0; index < uuid.size(); ++index) {
- uuid[index] = static_cast<uint8_t>(index);
- }
- VariantBatchBuilder builder(VariantBatchBuilder::ReserveHint {.rows = 3});
+ VariantBatchBuilder builder(VariantBatchBuilder::ReserveHint {.rows = 2});
{
auto row = builder.begin_row();
row.add_binary(StringRef(binary.data(), binary.size()));
@@ -431,18 +428,12 @@ TEST(CastVariantV2FromTest,
EOnlyScalarStringRulesStayJsonQuoted) {
row.add_time_ntz_micros(1);
row.finish();
}
- {
- auto row = builder.begin_row();
- row.add_uuid(uuid);
- row.finish();
- }
CastResult cast = execute_from_variant(finish(&builder),
std::make_shared<DataTypeString>());
ASSERT_TRUE(cast.status.ok()) << cast.status;
const auto& nullable = nullable_result(cast.column);
const auto& strings = assert_cast<const
ColumnString&>(nullable.get_nested_column());
EXPECT_EQ(strings.get_data_at(0), StringRef(R"("AAE=")"));
EXPECT_EQ(strings.get_data_at(1), StringRef(R"("00:00:00.000001")"));
- EXPECT_EQ(strings.get_data_at(2),
StringRef(R"("00010203-0405-0607-0809-0a0b0c0d0e0f")"));
EXPECT_FALSE(nullable.has_null());
}
@@ -878,4 +869,82 @@ TEST(CastVariantV2FromTest,
EncodedUuidPreservesAllBitsAndNulls) {
EXPECT_TRUE(nullable_result(cast.column).is_null_at(0));
}
+TEST(CastVariantV2FromTest, EncodedUuidStringMatchesTypedUuidString) {
+ // UUID has a Doris type, so an encoded UUID converts to STRING through
the concrete UUID cast,
+ // as a typed UUID does, instead of keeping the JSON text of scalars that
have no Doris type.
+ const std::array<uint8_t, 16> bytes {0x00, 0x11, 0x22, 0x33, 0x44, 0x55,
0x66, 0x77,
+ 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd,
0xee, 0xff};
+ const StringRef expected("00112233-4455-6677-8899-aabbccddeeff");
+ const auto string_type = std::make_shared<DataTypeString>();
+
+ auto values = ColumnUUID::create();
+ values->insert_value(UUIDValue::from_big_endian(bytes.data()));
+ ColumnPtr typed = ColumnVariantV2::create_typed(
+ ColumnNullable::create(std::move(values), ColumnUInt8::create(1,
0)),
+ std::make_shared<DataTypeUUID>());
+ CastResult cast = execute_from_variant(typed, string_type);
+ ASSERT_TRUE(cast.status.ok()) << cast.status;
+ EXPECT_EQ(nullable_result(cast.column).get_nested_column().get_data_at(0),
expected);
+
+ // An all-UUID column takes the whole-column concrete cast.
+ VariantBatchBuilder uuid_only;
+ {
+ auto row = uuid_only.begin_row();
+ row.add_uuid(bytes);
+ row.finish();
+ }
+ cast = execute_from_variant(finish(&uuid_only), string_type);
+ ASSERT_TRUE(cast.status.ok()) << cast.status;
+ EXPECT_EQ(nullable_result(cast.column).get_nested_column().get_data_at(0),
expected);
+
+ // A mixed column puts the concrete UUID rows back in order with the JSON
fallback rows.
+ VariantBatchBuilder mixed(VariantBatchBuilder::ReserveHint {.rows = 3});
+ {
+ auto row = mixed.begin_row();
+ auto object = row.start_object();
+ object.add_key(StringRef("a"));
+ row.add_int(1);
+ object.finish();
+ row.finish();
+ }
+ {
+ auto row = mixed.begin_row();
+ row.add_uuid(bytes);
+ row.finish();
+ }
+ {
+ auto row = mixed.begin_row();
+ row.add_null();
+ row.finish();
+ }
+ cast = execute_from_variant(finish(&mixed), string_type);
+ ASSERT_TRUE(cast.status.ok()) << cast.status;
+ const auto& nullable = nullable_result(cast.column);
+ const auto& strings = assert_cast<const
ColumnString&>(nullable.get_nested_column());
+ ASSERT_EQ(strings.size(), 3);
+ EXPECT_EQ(strings.get_data_at(0), StringRef(R"({"a":1})"));
+ EXPECT_EQ(strings.get_data_at(1), expected);
+ EXPECT_EQ(strings.get_data_at(2), StringRef("null"));
+ EXPECT_FALSE(nullable.has_null());
+
+ // ARRAY<STRING> converts its elements with the same rule.
+ VariantBatchBuilder array_of_uuid;
+ {
+ auto row = array_of_uuid.begin_row();
+ auto array = row.start_array();
+ row.add_uuid(bytes);
+ array.finish();
+ row.finish();
+ }
+ cast = execute_from_variant(finish(&array_of_uuid),
+ std::make_shared<DataTypeArray>(string_type));
+ ASSERT_TRUE(cast.status.ok()) << cast.status;
+ const auto& arrays =
+ assert_cast<const
ColumnArray&>(nullable_result(cast.column).get_nested_column());
+ ASSERT_EQ(arrays.size_at(0), 1);
+ const auto& elements = assert_cast<const
ColumnNullable&>(arrays.get_data());
+ EXPECT_EQ(assert_cast<const
ColumnString&>(elements.get_nested_column()).get_data_at(0),
+ expected);
+}
+
} // namespace doris::CastWrapper
diff --git
a/regression-test/data/datatype_p0/uuid/test_uuid_variant_identity.out
b/regression-test/data/datatype_p0/uuid/test_uuid_variant_identity.out
index eb2e5a2ae30..8b862300b91 100644
--- a/regression-test/data/datatype_p0/uuid/test_uuid_variant_identity.out
+++ b/regression-test/data/datatype_p0/uuid/test_uuid_variant_identity.out
@@ -2,6 +2,9 @@
-- !native_uuid_variant_type --
uuid 00112233-4455-6677-8899-aabbccddeeff \N
+-- !uuid_variant_to_string --
+00112233-4455-6677-8899-aabbccddeeff 00112233-4455-6677-8899-aabbccddeeff
["00112233-4455-6677-8899-aabbccddeeff"]
"00112233-4455-6677-8899-aabbccddeeff" ["00112233-4455-6677-8899-aabbccddeeff"]
+
-- !persisted_uuid_variant_type --
1 uuid 00112233-4455-6677-8899-aabbccddeeff
2 \N \N
diff --git
a/regression-test/suites/datatype_p0/uuid/test_uuid_variant_identity.groovy
b/regression-test/suites/datatype_p0/uuid/test_uuid_variant_identity.groovy
index b246e9a9fc6..78167b65dec 100644
--- a/regression-test/suites/datatype_p0/uuid/test_uuid_variant_identity.groovy
+++ b/regression-test/suites/datatype_p0/uuid/test_uuid_variant_identity.groovy
@@ -21,6 +21,16 @@ suite("test_uuid_variant_identity") {
CAST(CAST(CAST('00112233-4455-6677-8899-aabbccddeeff' AS UUID)
AS VARIANT) AS UUID),
variant_type(CAST(CAST(NULL AS UUID) AS VARIANT))
"""
+ // An array element of a Variant is encoded, while a cast scalar is typed.
Both convert a UUID
+ // to the same STRING, without JSON quotes, also as an ARRAY<STRING>
element; JSON output
+ // keeps the quoted string.
+ qt_uuid_variant_to_string """
+ SELECT CAST(CAST(CAST('00112233-4455-6677-8899-aabbccddeeff' AS UUID)
AS VARIANT) AS STRING),
+ CAST(CAST(ARRAY(CAST('00112233-4455-6677-8899-aabbccddeeff' AS
UUID)) AS VARIANT)[1] AS STRING),
+ CAST(CAST(ARRAY(CAST('00112233-4455-6677-8899-aabbccddeeff' AS
UUID)) AS VARIANT) AS ARRAY<STRING>),
+ CAST(CAST(ARRAY(CAST('00112233-4455-6677-8899-aabbccddeeff' AS
UUID)) AS VARIANT)[1] AS JSON),
+ CAST(CAST(ARRAY(CAST('00112233-4455-6677-8899-aabbccddeeff' AS
UUID)) AS VARIANT) AS STRING)
+ """
sql "DROP TABLE IF EXISTS uuid_variant_identity"
sql """
CREATE TABLE uuid_variant_identity (
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]