github-actions[bot] commented on code in PR #65561:
URL: https://github.com/apache/doris/pull/65561#discussion_r3664657993


##########
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/ExplicitlyCastableSignature.java:
##########
@@ -42,6 +44,9 @@ static boolean isExplicitlyCastable(DataType signatureType, 
DataType realType) {
 
     /** isExplicitlyCastable */
     static boolean isPrimitiveExplicitlyCastable(DataType signatureType, 
DataType realType) {
+        if (signatureType instanceof JsonType && realType instanceof 
VariantType) {

Review Comment:
   [P1] Preserve the legacy Variant-to-JSON binding while V2 is disabled. This 
unconditional check also rejects storage-backed V1 arguments, although the 
legacy BE Variant-to-JSON cast still exists. The patch has to rewrite existing 
`json_extract(v, ...)` and `json_object_flatten(v)` regressions with explicit 
casts as a result, so merely merging this default-disabled experiment breaks 
established queries in default sessions. Please require an explicit JSON cast 
only for `VariantType.isComputeV2()` here and in 
`TypeCoercionUtils.implicitCast()` until V1 is removed.



##########
be/src/exprs/table_function/vexplode_v2.cpp:
##########
@@ -49,8 +54,30 @@ VExplodeV2TableFunction::VExplodeV2TableFunction() {
 Status VExplodeV2TableFunction::_process_init_variant(Block* block, int 
value_column_idx,
                                                       int children_column_idx) 
{
     // explode variant array
-    auto column_without_nullable = 
remove_nullable(block->get_by_position(value_column_idx).column);
-    auto column = column_without_nullable->convert_to_full_column_if_const();
+    auto materialized =
+            
block->get_by_position(value_column_idx).column->convert_to_full_column_if_const();
+    std::span<const uint8_t> outer_nulls;
+    const IColumn* nested = materialized.get();
+    if (const auto* nullable = check_and_get_column<ColumnNullable>(nested)) {
+        outer_nulls = nullable->get_null_map_data();
+        nested = &nullable->get_nested_column();
+    }
+    if (const auto* variant_v2 = 
check_and_get_column<ColumnVariantV2>(nested)) {
+        auto variant_type = std::make_shared<DataTypeVariantV2>();
+        auto target_type = std::make_shared<DataTypeArray>(variant_type);
+        ColumnPtr array_column;
+        
RETURN_IF_ERROR(CastWrapper::variant_v2_internal::cast_variant_to_array(

Review Comment:
   [P1] Fix the exhausted-input boundary before sending V2 multi-explode 
through this path. For inputs of lengths 1 and 2, `_cur_size` is 2. If this 
generator precedes another lateral view, `get_same_many_values()` is used; at 
offset 1 its shorter input has `element_size == _cur_offset`, but that method 
checks `<` rather than `<=` and then dereferences `nested_nullmap_data[pos]` / 
inserts from `pos == nested_column.size()`. This can read the following row or 
fail instead of emitting SQL NULL. Please use the same strict `element_size > 
_cur_offset` boundary as `get_value()` and add an uneven V2 multi-explode 
followed by a second generator.



##########
be/src/exprs/function/function_variant_element.cpp:
##########
@@ -97,10 +100,72 @@ class FunctionVariantElement : public IFunction {
         return make_nullable(col);
     }
 
+    // Keep legacy/V2 physical-column dispatch in one entry point so nullable 
handling stays shared.
+    // NOLINTNEXTLINE(readability-function-size)
     Status execute_impl(FunctionContext* context, Block& block, const 
ColumnNumbers& arguments,
                         uint32_t result, size_t input_rows_count) const 
override {
-        const auto* variant_col = check_and_get_column<ColumnVariant>(
-                
remove_nullable(block.get_by_position(arguments[0]).column).get());
+        const ColumnPtr materialized =
+                
block.get_by_position(arguments[0]).column->convert_to_full_column_if_const();
+        const IColumn* physical = materialized.get();
+        std::span<const uint8_t> outer_nulls;
+        if (const auto* nullable = 
check_and_get_column<ColumnNullable>(physical)) {
+            outer_nulls = nullable->get_null_map_data();
+            physical = &nullable->get_nested_column();
+        }
+        if (const auto* variant_v2 = 
check_and_get_column<ColumnVariantV2>(physical)) {
+            if (block.empty()) {
+                block.replace_by_position(result, 
ColumnNullable::create(ColumnVariantV2::create(),
+                                                                         
ColumnUInt8::create()));
+                return Status::OK();
+            }
+
+            auto replace_with_all_null_result = [&]() {
+                auto null_values = ColumnVariantV2::create();
+                null_values->insert_many_defaults(variant_v2->size());
+                block.replace_by_position(
+                        result, ColumnNullable::create(std::move(null_values),
+                                                       
ColumnUInt8::create(variant_v2->size(), 1)));
+            };
+            const auto& index_argument = block.get_by_position(arguments[1]);
+            const ColumnPtr materialized_index =
+                    index_argument.column->convert_to_full_column_if_const();
+            const IColumn* index_column = materialized_index.get();
+            if (index_column->is_null_at(0)) {
+                replace_with_all_null_result();
+                return Status::OK();
+            }
+            if (const auto* nullable = 
check_and_get_column<ColumnNullable>(*index_column)) {
+                index_column = &nullable->get_nested_column();
+            }
+
+            std::optional<VariantElementV2PathSegment> segment;
+            const PrimitiveType index_type =
+                    remove_nullable(index_argument.type)->get_primitive_type();
+            if (is_string_type(index_type)) {
+                segment = 
VariantElementV2PathSegment::object_key(index_column->get_data_at(0));

Review Comment:
   [P1] Align FE with the constant-selector contract for Variant. Nereids only 
requires a literal second argument for structs, so 
`element_at(parse_to_variant('[10,20]'), number + 1)` resolves the new 
Variant/BigInt signature. BE still declares argument 1 always constant and 
rejects the query before this code with `Argument at index 1 ... must be 
constant`. This branch also reads selector row 0 only, so simply dropping that 
BE guard would be incorrect. Either reject nonconstant Variant selectors during 
analysis or resolve/extract per row, and add a multi-row selector regression.



##########
be/src/core/value/variant/variant_canonical.cpp:
##########
@@ -0,0 +1,1168 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include "core/value/variant/variant_canonical.h"
+
+#include <crc32c/crc32c.h>
+
+#include <algorithm>
+#include <array>
+#include <bit>
+#include <cmath>
+#include <cstdint>
+#include <cstring>
+#include <limits>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "common/exception.h"
+#include "core/value/variant/variant_field.h"
+#include "core/value/variant/variant_parquet_encoding.h"
+#include "exec/common/sip_hash.h"
+#include "util/hash_util.hpp"
+#include "util/utf8_check.h"
+
+namespace doris {
+namespace {
+
+constexpr uint64_t CANONICAL_NAN_BITS = 0x7FF8000000000000ULL;
+
+uint32_t read_bounded_unsigned(StringRef bytes, size_t offset, uint8_t width, 
const char* field) {
+    if (offset > bytes.size || width > bytes.size - offset) {
+        throw Exception(ErrorCode::CORRUPTION, "Truncated Variant canonical 
cell while reading {}",
+                        field);
+    }
+    uint32_t result = 0;
+    for (uint8_t byte = 0; byte < width; ++byte) {
+        result |= static_cast<uint32_t>(static_cast<uint8_t>(bytes.data[offset 
+ byte]))
+                  << (byte * 8);
+    }
+    return result;
+}
+
+enum class CanonicalKind : uint8_t {
+    NULL_VALUE = 0,
+    BOOL = 1,
+    EXACT_INTEGER = 2,
+    DECIMAL = 3,
+    FLOATING = 4,
+    STRING = 5,
+    BINARY = 6,
+    DATE = 7,
+    TIMESTAMP_TZ = 8,
+    TIMESTAMP_NTZ = 9,
+    TIME = 10,
+    UUID = 11,
+    OBJECT = 12,
+    ARRAY = 13,
+};
+
+struct NormalizedValue {
+    __int128 integer = 0;
+    uint64_t floating_bits = 0;
+    StringRef bytes;
+    CanonicalKind kind = CanonicalKind::NULL_VALUE;
+    uint8_t scale = 0;
+    bool boolean = false;
+    std::array<uint8_t, 16> uuid {};
+};
+
+struct ObjectEntry {
+    StringRef key;
+    VariantRef value;
+};
+
+NormalizedValue normalized_kind(CanonicalKind kind) {
+    NormalizedValue result;
+    result.kind = kind;
+    return result;
+}
+
+NormalizedValue normalized_integer(CanonicalKind kind, __int128 value) {
+    NormalizedValue result;
+    result.kind = kind;
+    result.integer = value;
+    return result;
+}
+
+NormalizedValue normalized_floating(uint64_t bits) {
+    NormalizedValue result;
+    result.kind = CanonicalKind::FLOATING;
+    result.floating_bits = bits;
+    return result;
+}
+
+NormalizedValue normalized_bytes(CanonicalKind kind, StringRef bytes) {
+    NormalizedValue result;
+    result.kind = kind;
+    result.bytes = bytes;
+    return result;
+}
+
+void require_depth(uint32_t depth) {
+    if (depth > VARIANT_MAX_NESTING_DEPTH) {
+        throw Exception(ErrorCode::INVALID_ARGUMENT,
+                        "Variant canonical traversal exceeds maximum depth {}",
+                        VARIANT_MAX_NESTING_DEPTH);
+    }
+}
+
+void require_exact_value(VariantRef value) {
+    const size_t encoded_size = value.value_size();
+    if (encoded_size != value.value.size) {
+        throw Exception(ErrorCode::CORRUPTION,
+                        "Variant value has {} trailing bytes after its {} byte 
root",
+                        value.value.size - encoded_size, encoded_size);
+    }
+}
+
+void require_valid_utf8(StringRef value, const char* description) {
+    if (value.size != 0 && !validate_utf8(value.data, value.size)) {
+        throw Exception(ErrorCode::CORRUPTION, "Variant {} is not valid 
UTF-8", description);
+    }
+}
+
+NormalizedValue normalize_floating(double value) {
+    constexpr double INT128_UPPER_EXCLUSIVE = 0x1p127;
+    if (std::isfinite(value) && std::trunc(value) == value && value >= 
-INT128_UPPER_EXCLUSIVE &&
+        value < INT128_UPPER_EXCLUSIVE) {
+        const auto integer = static_cast<__int128>(value);
+        if (variant_unsigned_magnitude(integer) <= VARIANT_DECIMAL16_MAX) {
+            return normalized_integer(CanonicalKind::EXACT_INTEGER, integer);
+        }
+    }
+    if (std::isnan(value)) {
+        return normalized_floating(CANONICAL_NAN_BITS);
+    }
+    auto bits = std::bit_cast<uint64_t>(value);
+    if (value == 0) {
+        bits = 0;
+    }
+    return normalized_floating(bits);
+}
+
+NormalizedValue normalize_primitive(VariantRef value) {
+    switch (value.primitive_id()) {
+    case VariantPrimitiveId::NULL_VALUE:
+        return normalized_kind(CanonicalKind::NULL_VALUE);
+    case VariantPrimitiveId::TRUE_VALUE: {
+        NormalizedValue result = normalized_kind(CanonicalKind::BOOL);
+        result.boolean = true;
+        return result;
+    }
+    case VariantPrimitiveId::FALSE_VALUE:
+        return normalized_kind(CanonicalKind::BOOL);
+    case VariantPrimitiveId::INT8:
+    case VariantPrimitiveId::INT16:
+    case VariantPrimitiveId::INT32:
+    case VariantPrimitiveId::INT64:
+        return normalized_integer(CanonicalKind::EXACT_INTEGER, 
value.get_int());
+    case VariantPrimitiveId::DOUBLE:
+        return normalize_floating(value.get_double());
+    case VariantPrimitiveId::FLOAT:
+        return normalize_floating(static_cast<double>(value.get_float()));
+    case VariantPrimitiveId::DECIMAL4:
+    case VariantPrimitiveId::DECIMAL8:
+    case VariantPrimitiveId::DECIMAL16: {
+        VariantDecimal decimal = value.get_decimal();
+        if (variant_unsigned_magnitude(decimal.unscaled) > 
VARIANT_DECIMAL16_MAX) {
+            throw Exception(ErrorCode::CORRUPTION,
+                            "Variant decimal unscaled value exceeds precision 
38");
+        }
+        while (decimal.scale != 0 && decimal.unscaled % 10 == 0) {
+            decimal.unscaled /= 10;
+            --decimal.scale;
+        }
+        if (decimal.scale == 0) {
+            return normalized_integer(CanonicalKind::EXACT_INTEGER, 
decimal.unscaled);
+        }
+        NormalizedValue result = normalized_integer(CanonicalKind::DECIMAL, 
decimal.unscaled);
+        result.scale = decimal.scale;
+        return result;
+    }
+    case VariantPrimitiveId::DATE:
+        return normalized_integer(CanonicalKind::DATE, value.get_date());
+    case VariantPrimitiveId::TIMESTAMP_MICROS:
+        return normalized_integer(CanonicalKind::TIMESTAMP_TZ,
+                                  
static_cast<__int128>(value.get_timestamp_micros()) * 1000);
+    case VariantPrimitiveId::TIMESTAMP_NTZ_MICROS:
+        return normalized_integer(CanonicalKind::TIMESTAMP_NTZ,
+                                  
static_cast<__int128>(value.get_timestamp_ntz_micros()) * 1000);
+    case VariantPrimitiveId::TIMESTAMP_NANOS:
+        return normalized_integer(CanonicalKind::TIMESTAMP_TZ, 
value.get_timestamp_nanos());
+    case VariantPrimitiveId::TIMESTAMP_NTZ_NANOS:
+        return normalized_integer(CanonicalKind::TIMESTAMP_NTZ, 
value.get_timestamp_ntz_nanos());
+    case VariantPrimitiveId::BINARY:
+        return normalized_bytes(CanonicalKind::BINARY, value.get_binary());
+    case VariantPrimitiveId::STRING: {
+        const StringRef string = value.get_string();
+        require_valid_utf8(string, "string");
+        return normalized_bytes(CanonicalKind::STRING, string);
+    }
+    case VariantPrimitiveId::TIME_NTZ_MICROS:
+        return normalized_integer(CanonicalKind::TIME, 
value.get_time_ntz_micros());
+    case VariantPrimitiveId::UUID: {
+        NormalizedValue result = normalized_kind(CanonicalKind::UUID);
+        result.uuid = value.get_uuid();
+        return result;
+    }
+    }
+    throw Exception(ErrorCode::INVALID_ARGUMENT, "Unknown Variant primitive id 
{}",
+                    static_cast<uint8_t>(value.primitive_id()));
+}
+
+NormalizedValue normalize_value(VariantRef value) {
+    switch (value.basic_type()) {
+    case VariantBasicType::SHORT_STRING: {
+        const StringRef string = value.get_string();
+        require_valid_utf8(string, "string");
+        return normalized_bytes(CanonicalKind::STRING, string);
+    }
+    case VariantBasicType::OBJECT:
+        return normalized_kind(CanonicalKind::OBJECT);
+    case VariantBasicType::ARRAY:
+        return normalized_kind(CanonicalKind::ARRAY);
+    case VariantBasicType::PRIMITIVE:
+        return normalize_primitive(value);
+    }
+    throw Exception(ErrorCode::INVALID_ARGUMENT, "Unknown Variant basic type");
+}
+
+ObjectEntry object_entry_at(VariantRef object, uint32_t index, StringRef 
previous_key,
+                            bool has_previous) {
+    uint32_t field_id = 0;
+    VariantRef child = object.object_value_at(index, &field_id);
+    const StringRef key = object.metadata.key_at(field_id);
+    require_valid_utf8(key, "object key");
+    if (has_previous && previous_key.compare(key) >= 0) {
+        throw Exception(ErrorCode::CORRUPTION,
+                        "Variant object keys are not strictly byte-sorted at 
field {}", index);
+    }
+    return {.key = key, .value = child};
+}
+
+bool scalar_equals(const NormalizedValue& left, const NormalizedValue& right) {
+    switch (left.kind) {
+    case CanonicalKind::NULL_VALUE:
+        return true;
+    case CanonicalKind::BOOL:
+        return left.boolean == right.boolean;
+    case CanonicalKind::EXACT_INTEGER:
+    case CanonicalKind::DATE:
+    case CanonicalKind::TIMESTAMP_TZ:
+    case CanonicalKind::TIMESTAMP_NTZ:
+    case CanonicalKind::TIME:
+        return left.integer == right.integer;
+    case CanonicalKind::DECIMAL:
+        return left.integer == right.integer && left.scale == right.scale;
+    case CanonicalKind::FLOATING:
+        return left.floating_bits == right.floating_bits;
+    case CanonicalKind::STRING:
+    case CanonicalKind::BINARY:
+        return left.bytes == right.bytes;
+    case CanonicalKind::UUID:
+        return left.uuid == right.uuid;
+    case CanonicalKind::OBJECT:
+    case CanonicalKind::ARRAY:
+        break;
+    }
+    DCHECK(false) << "Container reached scalar equality";
+    return false;
+}
+
+bool equals_node(VariantRef left, VariantRef right, uint32_t depth) {
+    require_depth(depth);
+    require_exact_value(left);
+    require_exact_value(right);
+    const NormalizedValue normalized_left = normalize_value(left);
+    const NormalizedValue normalized_right = normalize_value(right);
+    if (normalized_left.kind != normalized_right.kind) {
+        return false;
+    }
+    if (normalized_left.kind == CanonicalKind::ARRAY) {
+        const uint32_t count = left.num_elements();
+        if (count != right.num_elements()) {
+            return false;
+        }
+        for (uint32_t index = 0; index < count; ++index) {
+            if (!equals_node(left.array_at(index), right.array_at(index), 
depth + 1)) {
+                return false;
+            }
+        }
+        return true;
+    }
+    if (normalized_left.kind == CanonicalKind::OBJECT) {
+        const uint32_t count = left.num_elements();
+        if (count != right.num_elements()) {
+            return false;
+        }
+        StringRef previous_left;
+        StringRef previous_right;
+        for (uint32_t index = 0; index < count; ++index) {
+            const ObjectEntry left_entry = object_entry_at(left, index, 
previous_left, index != 0);
+            const ObjectEntry right_entry =
+                    object_entry_at(right, index, previous_right, index != 0);
+            if (left_entry.key != right_entry.key ||
+                !equals_node(left_entry.value, right_entry.value, depth + 1)) {
+                return false;
+            }
+            previous_left = left_entry.key;
+            previous_right = right_entry.key;
+        }
+        return true;
+    }
+    return scalar_equals(normalized_left, normalized_right);
+}
+
+template <typename Sink>
+void update_unsigned(Sink& sink, unsigned __int128 value, uint8_t width) {
+    std::array<char, 16> bytes {};
+    for (uint8_t index = 0; index < width; ++index) {
+        bytes[index] = static_cast<char>(value >> (index * 8));
+    }
+    sink.update(bytes.data(), width);
+}
+
+template <typename Sink>
+void update_signed(Sink& sink, __int128 value, uint8_t width) {
+    update_unsigned(sink, static_cast<unsigned __int128>(value), width);
+}
+
+template <typename Sink>
+void update_bytes(Sink& sink, StringRef bytes) {
+    if (bytes.size > std::numeric_limits<uint32_t>::max()) {
+        throw Exception(ErrorCode::CORRUPTION, "Variant byte sequence exceeds 
uint32 length");
+    }
+    update_unsigned(sink, bytes.size, sizeof(uint32_t));
+    if (bytes.size != 0) {
+        sink.update(bytes.data, bytes.size);
+    }
+}
+
+template <typename Sink>
+void hash_normalized_scalar(const NormalizedValue& normalized, Sink& sink) {
+    switch (normalized.kind) {
+    case CanonicalKind::NULL_VALUE:
+        return;
+    case CanonicalKind::BOOL: {
+        const char boolean = normalized.boolean ? 1 : 0;
+        sink.update(&boolean, 1);
+        return;
+    }
+    case CanonicalKind::EXACT_INTEGER:
+        update_signed(sink, normalized.integer, 16);
+        return;
+    case CanonicalKind::DECIMAL:
+        update_signed(sink, normalized.integer, 16);
+        sink.update(reinterpret_cast<const char*>(&normalized.scale), 1);
+        return;
+    case CanonicalKind::FLOATING:
+        update_unsigned(sink, normalized.floating_bits, sizeof(uint64_t));
+        return;
+    case CanonicalKind::STRING:
+    case CanonicalKind::BINARY:
+        update_bytes(sink, normalized.bytes);
+        return;
+    case CanonicalKind::DATE:
+        update_signed(sink, normalized.integer, sizeof(int32_t));
+        return;
+    case CanonicalKind::TIMESTAMP_TZ:
+    case CanonicalKind::TIMESTAMP_NTZ:
+        update_signed(sink, normalized.integer, 16);
+        return;
+    case CanonicalKind::TIME:
+        update_signed(sink, normalized.integer, sizeof(int64_t));
+        return;
+    case CanonicalKind::UUID:
+        sink.update(reinterpret_cast<const char*>(normalized.uuid.data()), 
normalized.uuid.size());
+        return;
+    case CanonicalKind::OBJECT:
+    case CanonicalKind::ARRAY:
+        break;
+    }
+    DCHECK(false) << "Container reached scalar hash";
+}
+
+template <typename Sink>
+void hash_node(VariantRef value, Sink& sink, uint32_t depth) {
+    require_depth(depth);
+    require_exact_value(value);
+    const NormalizedValue normalized = normalize_value(value);
+    const char tag = static_cast<char>(normalized.kind);
+    sink.update(&tag, 1);
+
+    switch (normalized.kind) {
+    case CanonicalKind::OBJECT: {
+        const uint32_t count = value.num_elements();
+        update_unsigned(sink, count, sizeof(uint32_t));
+        StringRef previous;
+        for (uint32_t index = 0; index < count; ++index) {
+            const ObjectEntry entry = object_entry_at(value, index, previous, 
index != 0);
+            update_bytes(sink, entry.key);
+            hash_node(entry.value, sink, depth + 1);
+            previous = entry.key;
+        }
+        return;
+    }
+    case CanonicalKind::ARRAY: {
+        const uint32_t count = value.num_elements();
+        update_unsigned(sink, count, sizeof(uint32_t));
+        for (uint32_t index = 0; index < count; ++index) {
+            hash_node(value.array_at(index), sink, depth + 1);
+        }
+        return;
+    }
+    default:
+        hash_normalized_scalar(normalized, sink);
+        return;
+    }
+}
+
+uint8_t minimum_integer_width(__int128 value) {
+    if (value >= std::numeric_limits<int8_t>::min() &&
+        value <= std::numeric_limits<int8_t>::max()) {
+        return 1;
+    }
+    if (value >= std::numeric_limits<int16_t>::min() &&
+        value <= std::numeric_limits<int16_t>::max()) {
+        return 2;
+    }
+    if (value >= std::numeric_limits<int32_t>::min() &&
+        value <= std::numeric_limits<int32_t>::max()) {
+        return 4;
+    }
+    return 8;
+}
+
+uint8_t minimum_decimal_width(__int128 value) {
+    const unsigned __int128 absolute = variant_unsigned_magnitude(value);
+    if (absolute <= VARIANT_DECIMAL4_MAX) {
+        return 4;
+    }
+    if (absolute <= VARIANT_DECIMAL8_MAX) {
+        return 8;
+    }
+    return 16;
+}
+
+bool fits_int64(__int128 value) {
+    return value >= std::numeric_limits<int64_t>::min() &&
+           value <= std::numeric_limits<int64_t>::max();
+}
+
+size_t scalar_encoded_size(const NormalizedValue& value) {
+    switch (value.kind) {
+    case CanonicalKind::NULL_VALUE:
+    case CanonicalKind::BOOL:
+        return 1;
+    case CanonicalKind::EXACT_INTEGER:
+        return fits_int64(value.integer) ? 1 + 
minimum_integer_width(value.integer) : 18;
+    case CanonicalKind::DECIMAL:
+        return 2 + minimum_decimal_width(value.integer);
+    case CanonicalKind::FLOATING:
+        return 1 + sizeof(uint64_t);
+    case CanonicalKind::STRING:
+        return value.bytes.size <= VARIANT_MAX_SHORT_STRING_SIZE
+                       ? 1 + value.bytes.size
+                       : 1 + sizeof(uint32_t) + value.bytes.size;
+    case CanonicalKind::BINARY:
+        return 1 + sizeof(uint32_t) + value.bytes.size;
+    case CanonicalKind::DATE:
+        return 1 + sizeof(int32_t);
+    case CanonicalKind::TIMESTAMP_TZ:
+    case CanonicalKind::TIMESTAMP_NTZ:
+    case CanonicalKind::TIME:
+        return 1 + sizeof(int64_t);
+    case CanonicalKind::UUID:
+        return 1 + 16;
+    case CanonicalKind::OBJECT:
+    case CanonicalKind::ARRAY:
+        break;
+    }
+    DCHECK(false) << "Container reached scalar size";
+    return 0;
+}
+
+struct PlanNode {
+    NormalizedValue normalized;
+    size_t encoded_size = 0;
+    uint32_t values_size = 0;
+    size_t children_begin = 0;
+    uint32_t child_count = 0;
+    uint8_t count_width = 0;
+    uint8_t offset_width = 0;
+    uint8_t id_width = 0;
+};
+
+struct PlanChild {
+    uint32_t node_index = 0;
+    uint32_t field_id = 0;
+    StringRef key;
+};
+
+struct SerializePlan {
+    std::vector<PlanNode> nodes;

Review Comment:
   [P2] Avoid rebuilding heap-backed canonical plans twice for every encoded 
Variant key. The serialized GROUP BY/DISTINCT setup first calls 
`get_max_row_byte_size()`, which invokes this planner for every row, and then 
`serialize()` invokes it again for every row. Each call creates a fresh `Impl` 
plus three `std::vector`s; even scalar encoded keys allocate, while wide 
objects rebuild O(nodes + children + keys) scratch on both passes. Please reuse 
a planner workspace or combine sizing/writing (with a scalar fast path where 
possible), and add a representative encoded-key benchmark.



##########
be/src/exprs/function/cast/variant_v2/cast_variant_to_scalar.cpp:
##########
@@ -0,0 +1,520 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include <cctz/time_zone.h>
+
+#include <algorithm>
+#include <array>
+#include <cstdint>
+#include <memory>
+#include <utility>
+
+#include "common/exception.h"
+#include "core/assert_cast.h"
+#include "core/block/block.h"
+#include "core/column/column_decimal.h"
+#include "core/column/column_string.h"
+#include "core/column/column_vector.h"
+#include "core/column/variant_v2/column_variant_v2.h"
+#include "core/custom_allocator.h"
+#include "core/data_type/data_type_date_or_datetime_v2.h"
+#include "core/data_type/data_type_decimal.h"
+#include "core/data_type/data_type_nullable.h"
+#include "core/data_type/data_type_number.h"
+#include "core/data_type/data_type_string.h"
+#include "core/data_type/data_type_timestamptz.h"
+#include "core/value/timestamptz_value.h"
+#include "core/value/vdatetime_value.h"
+#include "exprs/function/cast/cast_base.h"
+#include "exprs/function/cast/variant_v2/cast_variant_v2_internal.h"
+#include "exprs/function_context.h"
+
+namespace doris::CastWrapper::variant_v2_internal {
+namespace {
+
+constexpr size_t DECIMAL_SCALE_COUNT = DataTypeDecimal128::max_precision() + 1;
+
+enum GroupIndex : size_t {
+    INVALID_GROUP = 0,
+    BOOL_GROUP = INVALID_GROUP + 1,
+    INT_GROUP = BOOL_GROUP + 1,
+    FLOAT_GROUP = INT_GROUP + 1,
+    DOUBLE_GROUP = FLOAT_GROUP + 1,
+    DECIMAL_GROUP_BEGIN = DOUBLE_GROUP + 1,
+    DATE_GROUP = DECIMAL_GROUP_BEGIN + DECIMAL_SCALE_COUNT,
+    TIMESTAMP_NTZ_GROUP = DATE_GROUP + 1,
+    TIMESTAMP_TZ_GROUP = TIMESTAMP_NTZ_GROUP + 1,
+    STRING_GROUP = TIMESTAMP_TZ_GROUP + 1,
+    GROUP_COUNT = STRING_GROUP + 1,
+};
+
+struct ScalarGroup {
+    DataTypePtr type;
+    MutableColumnPtr values;
+    DorisVector<size_t> source_rows;
+};
+
+using ScalarGroups = std::array<ScalarGroup, GROUP_COUNT>;
+
+template <typename TypeFactory>
+ScalarGroup& initialize_group(ScalarGroups& groups, size_t index, 
TypeFactory&& make_type) {
+    ScalarGroup& group = groups[index];
+    if (!group.values) {
+        group.type = std::forward<TypeFactory>(make_type)();
+        group.values = group.type->create_column();
+    }
+    return group;
+}
+
+void append_invalid(ScalarGroups& groups, size_t row) {
+    groups[INVALID_GROUP].source_rows.push_back(row);
+}
+
+std::pair<int64_t, uint32_t> split_epoch_micros(int64_t micros) {
+    constexpr int64_t MICROS_PER_SECOND = 1'000'000;
+    int64_t seconds = micros / MICROS_PER_SECOND;
+    int64_t fraction = micros % MICROS_PER_SECOND;
+    if (fraction < 0) {
+        --seconds;
+        fraction += MICROS_PER_SECOND;
+    }
+    return {seconds, static_cast<uint32_t>(fraction)};
+}
+
+int64_t nanos_to_micros(int64_t nanos) {
+    int64_t micros = nanos / 1000;
+    if (nanos % 1000 < 0) {
+        --micros;
+    }
+    return micros;
+}
+
+bool epoch_micros_to_civil(int64_t micros, cctz::civil_second* civil, 
uint32_t* fraction) {
+    const auto [seconds, micros_fraction] = split_epoch_micros(micros);
+    const auto lookup =
+            
cctz::utc_time_zone().lookup(cctz::time_point<cctz::seconds>(cctz::seconds(seconds)));
+    if (lookup.cs.year() < 1 || lookup.cs.year() > 9999) {
+        return false;
+    }
+    *civil = lookup.cs;
+    *fraction = micros_fraction;
+    return true;
+}
+
+void append_bool(ScalarGroups& groups, size_t row, bool value) {
+    auto& group =
+            initialize_group(groups, BOOL_GROUP, [] { return 
std::make_shared<DataTypeBool>(); });
+    assert_cast<ColumnUInt8&>(*group.values).insert_value(value);
+    group.source_rows.push_back(row);
+}
+
+void append_int(ScalarGroups& groups, size_t row, int64_t value) {
+    auto& group =
+            initialize_group(groups, INT_GROUP, [] { return 
std::make_shared<DataTypeInt64>(); });
+    assert_cast<ColumnInt64&>(*group.values).insert_value(value);
+    group.source_rows.push_back(row);
+}
+
+void append_float(ScalarGroups& groups, size_t row, float value) {
+    auto& group = initialize_group(groups, FLOAT_GROUP,
+                                   [] { return 
std::make_shared<DataTypeFloat32>(); });
+    assert_cast<ColumnFloat32&>(*group.values).insert_value(value);
+    group.source_rows.push_back(row);
+}
+
+void append_double(ScalarGroups& groups, size_t row, double value) {
+    auto& group = initialize_group(groups, DOUBLE_GROUP,
+                                   [] { return 
std::make_shared<DataTypeFloat64>(); });
+    assert_cast<ColumnFloat64&>(*group.values).insert_value(value);
+    group.source_rows.push_back(row);
+}
+
+void append_decimal(ScalarGroups& groups, size_t row, VariantDecimal value) {
+    if (value.scale >= DECIMAL_SCALE_COUNT) {
+        append_invalid(groups, row);
+        return;
+    }
+    const size_t index = DECIMAL_GROUP_BEGIN + value.scale;
+    auto& group = initialize_group(groups, index, [scale = value.scale] {
+        return 
std::make_shared<DataTypeDecimal128>(DataTypeDecimal128::max_precision(), 
scale);
+    });
+    assert_cast<ColumnDecimal128V3&>(*group.values).insert_value(Decimal128V3 
{value.unscaled});
+    group.source_rows.push_back(row);
+}
+
+void append_date(ScalarGroups& groups, size_t row, int32_t days_since_epoch) {
+    const cctz::civil_day civil = cctz::civil_day(1970, 1, 1) + 
days_since_epoch;
+    if (civil.year() < 1 || civil.year() > 9999) {
+        append_invalid(groups, row);
+        return;
+    }
+    DateV2Value<DateV2ValueType> value;
+    value.unchecked_set_time(static_cast<uint16_t>(civil.year()),
+                             static_cast<uint8_t>(civil.month()), 
static_cast<uint8_t>(civil.day()),
+                             0, 0, 0);
+    auto& group =
+            initialize_group(groups, DATE_GROUP, [] { return 
std::make_shared<DataTypeDateV2>(); });
+    assert_cast<ColumnDateV2&>(*group.values).insert_value(value);
+    group.source_rows.push_back(row);
+}
+
+void append_timestamp(ScalarGroups& groups, size_t row, int64_t micros, bool 
utc_adjusted) {
+    cctz::civil_second civil;
+    uint32_t fraction = 0;
+    if (!epoch_micros_to_civil(micros, &civil, &fraction)) {
+        append_invalid(groups, row);
+        return;
+    }
+    if (utc_adjusted) {
+        TimestampTzValue value;
+        value.unchecked_set_time(
+                static_cast<uint16_t>(civil.year()), 
static_cast<uint8_t>(civil.month()),
+                static_cast<uint8_t>(civil.day()), 
static_cast<uint8_t>(civil.hour()),
+                static_cast<uint8_t>(civil.minute()), 
static_cast<uint8_t>(civil.second()),
+                fraction);
+        auto& group = initialize_group(groups, TIMESTAMP_TZ_GROUP,
+                                       [] { return 
std::make_shared<DataTypeTimeStampTz>(6); });
+        assert_cast<ColumnTimeStampTz&>(*group.values).insert_value(value);
+        group.source_rows.push_back(row);
+        return;
+    }
+    DateV2Value<DateTimeV2ValueType> value;
+    value.unchecked_set_time(
+            static_cast<uint16_t>(civil.year()), 
static_cast<uint8_t>(civil.month()),
+            static_cast<uint8_t>(civil.day()), 
static_cast<uint8_t>(civil.hour()),
+            static_cast<uint8_t>(civil.minute()), 
static_cast<uint8_t>(civil.second()), fraction);
+    auto& group = initialize_group(groups, TIMESTAMP_NTZ_GROUP,
+                                   [] { return 
std::make_shared<DataTypeDateTimeV2>(6); });
+    assert_cast<ColumnDateTimeV2&>(*group.values).insert_value(value);
+    group.source_rows.push_back(row);
+}
+
+void append_string(ScalarGroups& groups, size_t row, StringRef value) {
+    auto& group = initialize_group(groups, STRING_GROUP,
+                                   [] { return 
std::make_shared<DataTypeString>(); });
+    assert_cast<ColumnString&>(*group.values).insert_data(value.data, 
value.size);
+    group.source_rows.push_back(row);
+}
+
+void classify_value(ScalarGroups& groups, size_t row, VariantRef value, bool 
forced_null) {
+    if (forced_null) {
+        append_invalid(groups, row);
+        return;
+    }
+    switch (value.basic_type()) {
+    case VariantBasicType::SHORT_STRING:
+        append_string(groups, row, value.get_string());
+        return;
+    case VariantBasicType::OBJECT:
+    case VariantBasicType::ARRAY:
+        append_invalid(groups, row);
+        return;
+    case VariantBasicType::PRIMITIVE:
+        break;
+    }
+
+    switch (value.primitive_id()) {
+    case VariantPrimitiveId::NULL_VALUE:
+    case VariantPrimitiveId::BINARY:
+    case VariantPrimitiveId::TIME_NTZ_MICROS:
+    case VariantPrimitiveId::UUID:
+        append_invalid(groups, row);
+        return;
+    case VariantPrimitiveId::TRUE_VALUE:
+        append_bool(groups, row, true);
+        return;
+    case VariantPrimitiveId::FALSE_VALUE:
+        append_bool(groups, row, false);
+        return;
+    case VariantPrimitiveId::INT8:
+    case VariantPrimitiveId::INT16:
+    case VariantPrimitiveId::INT32:
+    case VariantPrimitiveId::INT64:
+        append_int(groups, row, value.get_int());
+        return;
+    case VariantPrimitiveId::FLOAT:
+        append_float(groups, row, value.get_float());
+        return;
+    case VariantPrimitiveId::DOUBLE:
+        append_double(groups, row, value.get_double());
+        return;
+    case VariantPrimitiveId::DECIMAL4:
+    case VariantPrimitiveId::DECIMAL8:
+    case VariantPrimitiveId::DECIMAL16:
+        append_decimal(groups, row, value.get_decimal());
+        return;
+    case VariantPrimitiveId::DATE:
+        append_date(groups, row, value.get_date());
+        return;
+    case VariantPrimitiveId::TIMESTAMP_MICROS:
+        append_timestamp(groups, row, value.get_timestamp_micros(), true);
+        return;
+    case VariantPrimitiveId::TIMESTAMP_NTZ_MICROS:
+        append_timestamp(groups, row, value.get_timestamp_ntz_micros(), false);
+        return;
+    case VariantPrimitiveId::TIMESTAMP_NANOS:
+        append_timestamp(groups, row, 
nanos_to_micros(value.get_timestamp_nanos()), true);
+        return;
+    case VariantPrimitiveId::TIMESTAMP_NTZ_NANOS:
+        append_timestamp(groups, row, 
nanos_to_micros(value.get_timestamp_ntz_nanos()), false);
+        return;
+    case VariantPrimitiveId::STRING:
+        append_string(groups, row, value.get_string());
+        return;
+    }
+    throw Exception(ErrorCode::CORRUPTION, "Unknown Variant primitive id");
+}
+
+Status execute_non_strict_scalar_cast(FunctionContext* context, const 
ColumnPtr& source,
+                                      const DataTypePtr& source_type,
+                                      const DataTypePtr& target_type, const 
char* source_name,
+                                      size_t rows, ColumnPtr* output) {
+    if (context == nullptr) {
+        return Status::InvalidArgument("Variant V2 scalar CAST requires a 
FunctionContext");
+    }
+    auto cast_context = context->clone();
+    cast_context->set_enable_strict_mode(false);
+    const DataTypePtr nullable_target = make_nullable(target_type);
+    Block temporary {{source, source_type, source_name},
+                     {nullable_target->create_column(), nullable_target, 
"variant-v2-result"}};
+    WrapperType wrapper =
+            prepare_unpack_dictionaries(cast_context.get(), source_type, 
nullable_target);
+    Status status = wrapper(cast_context.get(), temporary, {0}, 1, rows, 
nullptr);
+    if (!status.ok()) {
+        if (status.is<ErrorCode::INVALID_ARGUMENT>()) {
+            *output = make_all_null_column(target_type, rows);
+            return Status::OK();
+        }
+        return status;
+    }
+    *output = make_nullable(temporary.get_by_position(1).column);
+    return Status::OK();
+}
+
+Status execute_concrete_cast(FunctionContext* context, const ScalarGroup& 
group,
+                             const DataTypePtr& target_type, ColumnPtr* 
output) {
+    return execute_non_strict_scalar_cast(context, group.values->get_ptr(), 
group.type, target_type,
+                                          "variant-v2-group", 
group.source_rows.size(), output);
+}
+
+Status assemble_groups(FunctionContext* context, const ScalarGroups& groups,
+                       const DataTypePtr& target_type, size_t rows, ColumnPtr* 
output) {
+    const ScalarGroup* only_group = nullptr;
+    size_t only_group_index = 0;
+    for (size_t index = 0; index < groups.size(); ++index) {
+        if (groups[index].source_rows.empty()) {
+            continue;
+        }
+        if (only_group != nullptr) {
+            only_group = nullptr;
+            break;
+        }
+        only_group = &groups[index];
+        only_group_index = index;
+    }
+    if (only_group != nullptr && only_group->source_rows.size() == rows) {
+        if (only_group_index == INVALID_GROUP) {
+            *output = make_all_null_column(target_type, rows);
+            return Status::OK();
+        }
+        return execute_concrete_cast(context, *only_group, target_type, 
output);
+    }
+
+    MutableColumnPtr nested = target_type->create_column();
+    auto nulls = ColumnUInt8::create();
+    nested->reserve(rows);
+    nulls->reserve(rows);
+    IColumn::Permutation permutation(rows);
+    size_t concatenated_row = 0;
+
+    for (size_t index = 0; index < groups.size(); ++index) {
+        const ScalarGroup& group = groups[index];
+        if (group.source_rows.empty()) {
+            continue;
+        }
+        ColumnPtr cast_result;
+        if (index == INVALID_GROUP) {
+            cast_result = make_all_null_column(target_type, 
group.source_rows.size());
+        } else {
+            RETURN_IF_ERROR(execute_concrete_cast(context, group, target_type, 
&cast_result));
+        }
+        const auto& nullable = assert_cast<const 
ColumnNullable&>(*cast_result);
+        nested->insert_range_from(nullable.get_nested_column(), 0, 
group.source_rows.size());
+        nulls->insert_range_from(nullable.get_null_map_column(), 0, 
group.source_rows.size());
+        for (size_t source_row : group.source_rows) {
+            permutation[source_row] = concatenated_row++;
+        }
+    }
+    if (concatenated_row != rows) {
+        return Status::InternalError("Variant V2 scalar grouping produced {} 
rows, expected {}",
+                                     concatenated_row, rows);
+    }
+    ColumnPtr concatenated = ColumnNullable::create(std::move(nested), 
std::move(nulls));
+    *output = concatenated->permute(permutation, rows);
+    return Status::OK();
+}
+
+Status execute_typed_cast(FunctionContext* context, const ColumnPtr& source,
+                          const DataTypePtr& source_type, const DataTypePtr& 
target_type,
+                          size_t rows, ColumnPtr* output) {
+    const DataTypePtr nullable_source = make_nullable(source_type);
+    return execute_non_strict_scalar_cast(context, source, nullable_source, 
target_type,
+                                          "variant-v2-typed", rows, output);
+}
+
+} // namespace
+
+bool is_supported_scalar_source(const DataTypePtr& type) {
+    // Every scalar admitted by the typed state has a Parquet Variant mapping 
in
+    // with_variant_typed_scalar(), so CAST and the physical typed-state 
whitelist must stay
+    // identical.
+    return 
is_supported_variant_typed_identity(remove_nullable(type)->get_primitive_type());
+}
+
+bool is_supported_scalar_target(const DataTypePtr& type) {

Review Comment:
   [P1] Make FE enforce this narrower V2 cast matrix. Nereids currently accepts 
Variant to every primitive/array and also accepts sources such as DECIMAL256, 
TIMEV2, MAP, and STRUCT for `CAST(... AS VARIANT)`. These V2 kernels omit 
TIME/IP/VARBINARY targets, omit DECIMAL256/TIME sources, and have no MAP/STRUCT 
source branch, so those expressions analyze and then return `Conversion ... is 
not supported` from BE (the new DECIMAL256 test even expects that runtime 
error). Please define the supported matrix once on both sides—either implement 
the missing directions or reject them during analysis—and add paired 
FE/execution boundary tests.



##########
be/src/core/column/variant_v2/column_variant_v2_typed_column.h:
##########
@@ -0,0 +1,156 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#pragma once
+
+#include <array>
+#include <cstdint>
+#include <limits>
+#include <string_view>
+#include <utility>
+
+#include "common/check.h"
+#include "common/exception.h"
+#include "core/assert_cast.h"
+#include "core/call_on_type_index.h"
+#include "core/column/column_decimal.h"
+#include "core/column/column_nullable.h"
+#include "core/column/column_string.h"
+#include "core/column/column_vector.h"
+#include "core/data_type/data_type.h"
+#include "core/value/large_int_value.h"
+#include "core/value/variant/variant_parquet_encoding.h"
+#include "core/value/variant/variant_scalar.h"
+#include "exec/common/format_ip.h"
+
+namespace doris {
+
+bool is_supported_variant_typed_identity(PrimitiveType type);
+
+template <typename DateValue>
+int32_t variant_days_since_epoch(const DateValue& value, size_t row, 
std::string_view description) {
+    if (!value.is_valid_date()) {
+        throw Exception(ErrorCode::INVALID_ARGUMENT,
+                        "Cannot encode invalid {} value at row {} as Variant", 
description, row);
+    }
+    const int64_t days = value.daynr() - static_cast<int64_t>(calc_daynr(1970, 
1, 1));
+    DCHECK_GE(days, std::numeric_limits<int32_t>::min());
+    DCHECK_LE(days, std::numeric_limits<int32_t>::max());
+    return static_cast<int32_t>(days);
+}
+
+template <typename DateTimeValue>
+int64_t variant_timestamp_micros(const DateTimeValue& value, size_t row,
+                                 std::string_view description) {
+    constexpr int64_t MICROS_PER_SECOND = 1'000'000;
+    constexpr int64_t SECONDS_PER_DAY = 86'400;
+    const int64_t days = variant_days_since_epoch(value, row, description);
+    const int64_t seconds =
+            days * SECONDS_PER_DAY + value.hour() * 3600 + value.minute() * 60 
+ value.second();
+    return seconds * MICROS_PER_SECOND + value.microsecond();
+}
+
+template <PrimitiveType Type, typename Column, typename Callback>
+// NOLINTNEXTLINE(readability-function-size) -- centralized compile-time 
scalar mapping matrix.
+void with_variant_typed_scalar(const Column& column, size_t row, uint8_t scale,
+                               Callback&& callback) {
+    // The callback must consume the scalar synchronously: string refs for 
LARGEINT/IP textual
+    // adapters borrow stack storage owned by this function.
+    if constexpr (Type == TYPE_BOOLEAN) {
+        const bool value = column.get_data()[row] != 0;
+        callback(VariantScalarRef::boolean(value));
+    } else if constexpr (Type == TYPE_TINYINT || Type == TYPE_SMALLINT || Type 
== TYPE_INT ||
+                         Type == TYPE_BIGINT) {
+        const auto value = column.get_data()[row];
+        callback(VariantScalarRef::integer(value));
+    } else if constexpr (Type == TYPE_LARGEINT) {
+        const __int128 value = column.get_data()[row];
+        if (variant_unsigned_magnitude(value) <= VARIANT_DECIMAL16_MAX) {

Review Comment:
   [P1] Do not encode an out-of-range numeric LARGEINT as a Variant STRING. At 
`10^38`, this branch makes `CAST(CAST('100000000000000000000000000000000000000' 
AS LARGEINT) AS VARIANT)` canonically identical to 
`CAST(CAST('100000000000000000000000000000000000000' AS STRING) AS VARIANT)`, 
because both hashing and arena serialization consume the resulting 
`VariantScalarRef::string`. GROUP BY, DISTINCT, and multi-distinct can 
therefore collapse a numeric value with text, while the value at `10^38-1` 
remains correctly distinct. Please reject the unsupported numeric magnitude or 
preserve a numeric representation, and add E/T equality plus GROUP BY/DISTINCT 
coverage against the same digit string.



##########
be/src/exprs/function/cast/variant_v2/cast_variant_to_scalar.cpp:
##########
@@ -0,0 +1,520 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include <cctz/time_zone.h>
+
+#include <algorithm>
+#include <array>
+#include <cstdint>
+#include <memory>
+#include <utility>
+
+#include "common/exception.h"
+#include "core/assert_cast.h"
+#include "core/block/block.h"
+#include "core/column/column_decimal.h"
+#include "core/column/column_string.h"
+#include "core/column/column_vector.h"
+#include "core/column/variant_v2/column_variant_v2.h"
+#include "core/custom_allocator.h"
+#include "core/data_type/data_type_date_or_datetime_v2.h"
+#include "core/data_type/data_type_decimal.h"
+#include "core/data_type/data_type_nullable.h"
+#include "core/data_type/data_type_number.h"
+#include "core/data_type/data_type_string.h"
+#include "core/data_type/data_type_timestamptz.h"
+#include "core/value/timestamptz_value.h"
+#include "core/value/vdatetime_value.h"
+#include "exprs/function/cast/cast_base.h"
+#include "exprs/function/cast/variant_v2/cast_variant_v2_internal.h"
+#include "exprs/function_context.h"
+
+namespace doris::CastWrapper::variant_v2_internal {
+namespace {
+
+constexpr size_t DECIMAL_SCALE_COUNT = DataTypeDecimal128::max_precision() + 1;
+
+enum GroupIndex : size_t {
+    INVALID_GROUP = 0,
+    BOOL_GROUP = INVALID_GROUP + 1,
+    INT_GROUP = BOOL_GROUP + 1,
+    FLOAT_GROUP = INT_GROUP + 1,
+    DOUBLE_GROUP = FLOAT_GROUP + 1,
+    DECIMAL_GROUP_BEGIN = DOUBLE_GROUP + 1,
+    DATE_GROUP = DECIMAL_GROUP_BEGIN + DECIMAL_SCALE_COUNT,
+    TIMESTAMP_NTZ_GROUP = DATE_GROUP + 1,
+    TIMESTAMP_TZ_GROUP = TIMESTAMP_NTZ_GROUP + 1,
+    STRING_GROUP = TIMESTAMP_TZ_GROUP + 1,
+    GROUP_COUNT = STRING_GROUP + 1,
+};
+
+struct ScalarGroup {
+    DataTypePtr type;
+    MutableColumnPtr values;
+    DorisVector<size_t> source_rows;
+};
+
+using ScalarGroups = std::array<ScalarGroup, GROUP_COUNT>;
+
+template <typename TypeFactory>
+ScalarGroup& initialize_group(ScalarGroups& groups, size_t index, 
TypeFactory&& make_type) {
+    ScalarGroup& group = groups[index];
+    if (!group.values) {
+        group.type = std::forward<TypeFactory>(make_type)();
+        group.values = group.type->create_column();
+    }
+    return group;
+}
+
+void append_invalid(ScalarGroups& groups, size_t row) {
+    groups[INVALID_GROUP].source_rows.push_back(row);
+}
+
+std::pair<int64_t, uint32_t> split_epoch_micros(int64_t micros) {
+    constexpr int64_t MICROS_PER_SECOND = 1'000'000;
+    int64_t seconds = micros / MICROS_PER_SECOND;
+    int64_t fraction = micros % MICROS_PER_SECOND;
+    if (fraction < 0) {
+        --seconds;
+        fraction += MICROS_PER_SECOND;
+    }
+    return {seconds, static_cast<uint32_t>(fraction)};
+}
+
+int64_t nanos_to_micros(int64_t nanos) {
+    int64_t micros = nanos / 1000;
+    if (nanos % 1000 < 0) {
+        --micros;
+    }
+    return micros;
+}
+
+bool epoch_micros_to_civil(int64_t micros, cctz::civil_second* civil, 
uint32_t* fraction) {
+    const auto [seconds, micros_fraction] = split_epoch_micros(micros);
+    const auto lookup =
+            
cctz::utc_time_zone().lookup(cctz::time_point<cctz::seconds>(cctz::seconds(seconds)));
+    if (lookup.cs.year() < 1 || lookup.cs.year() > 9999) {
+        return false;
+    }
+    *civil = lookup.cs;
+    *fraction = micros_fraction;
+    return true;
+}
+
+void append_bool(ScalarGroups& groups, size_t row, bool value) {
+    auto& group =
+            initialize_group(groups, BOOL_GROUP, [] { return 
std::make_shared<DataTypeBool>(); });
+    assert_cast<ColumnUInt8&>(*group.values).insert_value(value);
+    group.source_rows.push_back(row);
+}
+
+void append_int(ScalarGroups& groups, size_t row, int64_t value) {
+    auto& group =
+            initialize_group(groups, INT_GROUP, [] { return 
std::make_shared<DataTypeInt64>(); });
+    assert_cast<ColumnInt64&>(*group.values).insert_value(value);
+    group.source_rows.push_back(row);
+}
+
+void append_float(ScalarGroups& groups, size_t row, float value) {
+    auto& group = initialize_group(groups, FLOAT_GROUP,
+                                   [] { return 
std::make_shared<DataTypeFloat32>(); });
+    assert_cast<ColumnFloat32&>(*group.values).insert_value(value);
+    group.source_rows.push_back(row);
+}
+
+void append_double(ScalarGroups& groups, size_t row, double value) {
+    auto& group = initialize_group(groups, DOUBLE_GROUP,
+                                   [] { return 
std::make_shared<DataTypeFloat64>(); });
+    assert_cast<ColumnFloat64&>(*group.values).insert_value(value);
+    group.source_rows.push_back(row);
+}
+
+void append_decimal(ScalarGroups& groups, size_t row, VariantDecimal value) {
+    if (value.scale >= DECIMAL_SCALE_COUNT) {
+        append_invalid(groups, row);
+        return;
+    }
+    const size_t index = DECIMAL_GROUP_BEGIN + value.scale;
+    auto& group = initialize_group(groups, index, [scale = value.scale] {
+        return 
std::make_shared<DataTypeDecimal128>(DataTypeDecimal128::max_precision(), 
scale);
+    });
+    assert_cast<ColumnDecimal128V3&>(*group.values).insert_value(Decimal128V3 
{value.unscaled});
+    group.source_rows.push_back(row);
+}
+
+void append_date(ScalarGroups& groups, size_t row, int32_t days_since_epoch) {
+    const cctz::civil_day civil = cctz::civil_day(1970, 1, 1) + 
days_since_epoch;
+    if (civil.year() < 1 || civil.year() > 9999) {
+        append_invalid(groups, row);
+        return;
+    }
+    DateV2Value<DateV2ValueType> value;
+    value.unchecked_set_time(static_cast<uint16_t>(civil.year()),
+                             static_cast<uint8_t>(civil.month()), 
static_cast<uint8_t>(civil.day()),
+                             0, 0, 0);
+    auto& group =
+            initialize_group(groups, DATE_GROUP, [] { return 
std::make_shared<DataTypeDateV2>(); });
+    assert_cast<ColumnDateV2&>(*group.values).insert_value(value);
+    group.source_rows.push_back(row);
+}
+
+void append_timestamp(ScalarGroups& groups, size_t row, int64_t micros, bool 
utc_adjusted) {
+    cctz::civil_second civil;
+    uint32_t fraction = 0;
+    if (!epoch_micros_to_civil(micros, &civil, &fraction)) {
+        append_invalid(groups, row);
+        return;
+    }
+    if (utc_adjusted) {
+        TimestampTzValue value;
+        value.unchecked_set_time(
+                static_cast<uint16_t>(civil.year()), 
static_cast<uint8_t>(civil.month()),
+                static_cast<uint8_t>(civil.day()), 
static_cast<uint8_t>(civil.hour()),
+                static_cast<uint8_t>(civil.minute()), 
static_cast<uint8_t>(civil.second()),
+                fraction);
+        auto& group = initialize_group(groups, TIMESTAMP_TZ_GROUP,
+                                       [] { return 
std::make_shared<DataTypeTimeStampTz>(6); });
+        assert_cast<ColumnTimeStampTz&>(*group.values).insert_value(value);
+        group.source_rows.push_back(row);
+        return;
+    }
+    DateV2Value<DateTimeV2ValueType> value;
+    value.unchecked_set_time(
+            static_cast<uint16_t>(civil.year()), 
static_cast<uint8_t>(civil.month()),
+            static_cast<uint8_t>(civil.day()), 
static_cast<uint8_t>(civil.hour()),
+            static_cast<uint8_t>(civil.minute()), 
static_cast<uint8_t>(civil.second()), fraction);
+    auto& group = initialize_group(groups, TIMESTAMP_NTZ_GROUP,
+                                   [] { return 
std::make_shared<DataTypeDateTimeV2>(6); });
+    assert_cast<ColumnDateTimeV2&>(*group.values).insert_value(value);
+    group.source_rows.push_back(row);
+}
+
+void append_string(ScalarGroups& groups, size_t row, StringRef value) {
+    auto& group = initialize_group(groups, STRING_GROUP,
+                                   [] { return 
std::make_shared<DataTypeString>(); });
+    assert_cast<ColumnString&>(*group.values).insert_data(value.data, 
value.size);
+    group.source_rows.push_back(row);
+}
+
+void classify_value(ScalarGroups& groups, size_t row, VariantRef value, bool 
forced_null) {
+    if (forced_null) {
+        append_invalid(groups, row);
+        return;
+    }
+    switch (value.basic_type()) {
+    case VariantBasicType::SHORT_STRING:
+        append_string(groups, row, value.get_string());
+        return;
+    case VariantBasicType::OBJECT:
+    case VariantBasicType::ARRAY:
+        append_invalid(groups, row);
+        return;
+    case VariantBasicType::PRIMITIVE:
+        break;
+    }
+
+    switch (value.primitive_id()) {
+    case VariantPrimitiveId::NULL_VALUE:
+    case VariantPrimitiveId::BINARY:
+    case VariantPrimitiveId::TIME_NTZ_MICROS:
+    case VariantPrimitiveId::UUID:
+        append_invalid(groups, row);
+        return;
+    case VariantPrimitiveId::TRUE_VALUE:
+        append_bool(groups, row, true);
+        return;
+    case VariantPrimitiveId::FALSE_VALUE:
+        append_bool(groups, row, false);
+        return;
+    case VariantPrimitiveId::INT8:
+    case VariantPrimitiveId::INT16:
+    case VariantPrimitiveId::INT32:
+    case VariantPrimitiveId::INT64:
+        append_int(groups, row, value.get_int());
+        return;
+    case VariantPrimitiveId::FLOAT:
+        append_float(groups, row, value.get_float());
+        return;
+    case VariantPrimitiveId::DOUBLE:
+        append_double(groups, row, value.get_double());
+        return;
+    case VariantPrimitiveId::DECIMAL4:
+    case VariantPrimitiveId::DECIMAL8:
+    case VariantPrimitiveId::DECIMAL16:
+        append_decimal(groups, row, value.get_decimal());
+        return;
+    case VariantPrimitiveId::DATE:
+        append_date(groups, row, value.get_date());
+        return;
+    case VariantPrimitiveId::TIMESTAMP_MICROS:
+        append_timestamp(groups, row, value.get_timestamp_micros(), true);
+        return;
+    case VariantPrimitiveId::TIMESTAMP_NTZ_MICROS:
+        append_timestamp(groups, row, value.get_timestamp_ntz_micros(), false);
+        return;
+    case VariantPrimitiveId::TIMESTAMP_NANOS:
+        append_timestamp(groups, row, 
nanos_to_micros(value.get_timestamp_nanos()), true);
+        return;
+    case VariantPrimitiveId::TIMESTAMP_NTZ_NANOS:
+        append_timestamp(groups, row, 
nanos_to_micros(value.get_timestamp_ntz_nanos()), false);
+        return;
+    case VariantPrimitiveId::STRING:
+        append_string(groups, row, value.get_string());
+        return;
+    }
+    throw Exception(ErrorCode::CORRUPTION, "Unknown Variant primitive id");
+}
+
+Status execute_non_strict_scalar_cast(FunctionContext* context, const 
ColumnPtr& source,
+                                      const DataTypePtr& source_type,
+                                      const DataTypePtr& target_type, const 
char* source_name,
+                                      size_t rows, ColumnPtr* output) {
+    if (context == nullptr) {
+        return Status::InvalidArgument("Variant V2 scalar CAST requires a 
FunctionContext");
+    }
+    auto cast_context = context->clone();
+    cast_context->set_enable_strict_mode(false);
+    const DataTypePtr nullable_target = make_nullable(target_type);
+    Block temporary {{source, source_type, source_name},
+                     {nullable_target->create_column(), nullable_target, 
"variant-v2-result"}};
+    WrapperType wrapper =
+            prepare_unpack_dictionaries(cast_context.get(), source_type, 
nullable_target);
+    Status status = wrapper(cast_context.get(), temporary, {0}, 1, rows, 
nullptr);
+    if (!status.ok()) {
+        if (status.is<ErrorCode::INVALID_ARGUMENT>()) {
+            *output = make_all_null_column(target_type, rows);
+            return Status::OK();
+        }
+        return status;
+    }
+    *output = make_nullable(temporary.get_by_position(1).column);
+    return Status::OK();
+}
+
+Status execute_concrete_cast(FunctionContext* context, const ScalarGroup& 
group,
+                             const DataTypePtr& target_type, ColumnPtr* 
output) {
+    return execute_non_strict_scalar_cast(context, group.values->get_ptr(), 
group.type, target_type,
+                                          "variant-v2-group", 
group.source_rows.size(), output);
+}
+
+Status assemble_groups(FunctionContext* context, const ScalarGroups& groups,
+                       const DataTypePtr& target_type, size_t rows, ColumnPtr* 
output) {
+    const ScalarGroup* only_group = nullptr;
+    size_t only_group_index = 0;
+    for (size_t index = 0; index < groups.size(); ++index) {
+        if (groups[index].source_rows.empty()) {
+            continue;
+        }
+        if (only_group != nullptr) {
+            only_group = nullptr;
+            break;
+        }
+        only_group = &groups[index];
+        only_group_index = index;
+    }
+    if (only_group != nullptr && only_group->source_rows.size() == rows) {
+        if (only_group_index == INVALID_GROUP) {
+            *output = make_all_null_column(target_type, rows);
+            return Status::OK();
+        }
+        return execute_concrete_cast(context, *only_group, target_type, 
output);
+    }
+
+    MutableColumnPtr nested = target_type->create_column();
+    auto nulls = ColumnUInt8::create();
+    nested->reserve(rows);
+    nulls->reserve(rows);
+    IColumn::Permutation permutation(rows);
+    size_t concatenated_row = 0;
+
+    for (size_t index = 0; index < groups.size(); ++index) {
+        const ScalarGroup& group = groups[index];
+        if (group.source_rows.empty()) {
+            continue;
+        }
+        ColumnPtr cast_result;
+        if (index == INVALID_GROUP) {
+            cast_result = make_all_null_column(target_type, 
group.source_rows.size());
+        } else {
+            RETURN_IF_ERROR(execute_concrete_cast(context, group, target_type, 
&cast_result));
+        }
+        const auto& nullable = assert_cast<const 
ColumnNullable&>(*cast_result);
+        nested->insert_range_from(nullable.get_nested_column(), 0, 
group.source_rows.size());
+        nulls->insert_range_from(nullable.get_null_map_column(), 0, 
group.source_rows.size());
+        for (size_t source_row : group.source_rows) {
+            permutation[source_row] = concatenated_row++;
+        }
+    }
+    if (concatenated_row != rows) {
+        return Status::InternalError("Variant V2 scalar grouping produced {} 
rows, expected {}",
+                                     concatenated_row, rows);
+    }
+    ColumnPtr concatenated = ColumnNullable::create(std::move(nested), 
std::move(nulls));
+    *output = concatenated->permute(permutation, rows);
+    return Status::OK();
+}
+
+Status execute_typed_cast(FunctionContext* context, const ColumnPtr& source,
+                          const DataTypePtr& source_type, const DataTypePtr& 
target_type,
+                          size_t rows, ColumnPtr* output) {
+    const DataTypePtr nullable_source = make_nullable(source_type);
+    return execute_non_strict_scalar_cast(context, source, nullable_source, 
target_type,
+                                          "variant-v2-typed", rows, output);
+}
+
+} // namespace
+
+bool is_supported_scalar_source(const DataTypePtr& type) {
+    // Every scalar admitted by the typed state has a Parquet Variant mapping 
in
+    // with_variant_typed_scalar(), so CAST and the physical typed-state 
whitelist must stay
+    // identical.
+    return 
is_supported_variant_typed_identity(remove_nullable(type)->get_primitive_type());
+}
+
+bool is_supported_scalar_target(const DataTypePtr& type) {
+    switch (remove_nullable(type)->get_primitive_type()) {
+    case TYPE_BOOLEAN:
+    case TYPE_TINYINT:
+    case TYPE_SMALLINT:
+    case TYPE_INT:
+    case TYPE_BIGINT:
+    case TYPE_LARGEINT:
+    case TYPE_FLOAT:
+    case TYPE_DOUBLE:
+    case TYPE_DECIMALV2:
+    case TYPE_DECIMAL32:
+    case TYPE_DECIMAL64:
+    case TYPE_DECIMAL128I:
+    case TYPE_DECIMAL256:
+    case TYPE_DATE:
+    case TYPE_DATEV2:
+    case TYPE_DATETIME:
+    case TYPE_DATETIMEV2:
+    case TYPE_TIMESTAMPTZ:
+        return true;
+    default:
+        return false;
+    }
+}
+
+Status cast_scalar_to_variant(const ColumnPtr& source, const DataTypePtr& 
source_type, size_t rows,
+                              ForcedNulls forced_nulls, ColumnPtr* output) {
+    if (!source || source->size() != rows ||
+        (!forced_nulls.empty() && forced_nulls.size() != rows)) {
+        return Status::InvalidArgument("Invalid scalar input shape for Variant 
V2 CAST");
+    }
+    auto nulls = ColumnUInt8::create(rows, 0);
+    if (!forced_nulls.empty()) {
+        std::ranges::copy(forced_nulls, nulls->get_data().begin());
+    }
+    ColumnPtr null_map = std::move(nulls);
+    ColumnPtr nullable = ColumnNullable::create(source, null_map);
+    *output = ColumnVariantV2::create_typed(std::move(nullable), source_type);

Review Comment:
   [P1] Validate STRING input before publishing a typed Variant. Doris STRING 
can contain arbitrary bytes, but this path only wraps the source column; 
`CAST(CAST(unhex('FF') AS STRING) AS VARIANT)` can therefore cast straight back 
to STRING successfully, while grouping, serialization, exchange, or Variant 
output later constructs `VariantScalarRef::string` and rejects the same row as 
invalid UTF-8. That makes cast success depend on which operator first 
materializes the Variant. Please validate visible non-null STRING rows at this 
boundary (or require an explicit binary representation) and cover direct 
output, round-trip, hash/group, and exchange behavior for invalid UTF-8.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to