Coverage Report

Created: 2026-09-04 00:30

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/exprs/function/cast/cast_to_variant.h
Line
Count
Source
1
// Licensed to the Apache Software Foundation (ASF) under one
2
// or more contributor license agreements.  See the NOTICE file
3
// distributed with this work for additional information
4
// regarding copyright ownership.  The ASF licenses this file
5
// to you under the Apache License, Version 2.0 (the
6
// "License"); you may not use this file except in compliance
7
// with the License.  You may obtain a copy of the License at
8
//
9
//   http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing,
12
// software distributed under the License is distributed on an
13
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
// KIND, either express or implied.  See the License for the
15
// specific language governing permissions and limitations
16
// under the License.
17
18
#pragma once
19
20
#include <algorithm>
21
22
#include "core/column/column_nullable.h"
23
#include "core/data_type/data_type_variant.h"
24
#include "exprs/function/cast/cast_base.h"
25
#include "exprs/function/cast/cast_to_string.h"
26
27
namespace doris::CastWrapper {
28
29
// shared implementation for casting from variant to arbitrary non-nullable target type
30
inline Status cast_from_variant_impl(FunctionContext* context, Block& block,
31
                                     const ColumnNumbers& arguments, uint32_t result,
32
                                     size_t input_rows_count, const NullMap::value_type* null_map,
33
18
                                     const DataTypePtr& data_type_to) {
34
18
    auto& col_with_type_and_name = block.get_by_position(arguments[0]);
35
18
    auto& col_from = col_with_type_and_name.column;
36
18
    const IColumn* variant_column = col_from.get();
37
18
    const auto* nullable = check_and_get_column<ColumnNullable>(*variant_column);
38
18
    if (nullable != nullptr) {
39
0
        variant_column = &nullable->get_nested_column();
40
0
    }
41
18
    const auto* variant = assert_cast<const ColumnVariant*>(variant_column);
42
18
    ColumnPtr col_to = data_type_to->create_column();
43
44
18
    ColumnPtr finalized_input_column;
45
18
    if (!variant->is_finalized()) {
46
        // Local exchange can share the same input block across multiple downstream tasks.
47
        // Finalize a private copy so variant casts never mutate shared input columns.
48
2
        auto finalized_variant = variant->clone_finalized();
49
2
        variant = assert_cast<const ColumnVariant*>(finalized_variant.get());
50
2
        if (nullable != nullptr) {
51
0
            finalized_input_column = ColumnNullable::create(std::move(finalized_variant),
52
0
                                                            nullable->get_null_map_column_ptr());
53
2
        } else {
54
2
            finalized_input_column = std::move(finalized_variant);
55
2
        }
56
2
    }
57
18
    auto execute_on_finalized_input = [&](auto&& executor) -> Status {
58
4
        if (!finalized_input_column) {
59
3
            return executor(block);
60
3
        }
61
1
        Block finalized_block = block;
62
1
        finalized_block.replace_by_position(arguments[0], finalized_input_column);
63
1
        RETURN_IF_ERROR(executor(finalized_block));
64
1
        block.replace_by_position(result, finalized_block.get_by_position(result).column);
65
1
        return Status::OK();
66
1
    };
_ZZN5doris11CastWrapper22cast_from_variant_implEPNS_15FunctionContextERNS_5BlockERKSt6vectorIjSaIjEEjmPKhRKSt10shared_ptrIKNS_9IDataTypeEEENKUlOT_E_clIZNS0_22cast_from_variant_implES2_S4_S9_jmSB_SH_EUlS4_E_EENS_6StatusESJ_
Line
Count
Source
57
4
    auto execute_on_finalized_input = [&](auto&& executor) -> Status {
58
4
        if (!finalized_input_column) {
59
3
            return executor(block);
60
3
        }
61
1
        Block finalized_block = block;
62
1
        finalized_block.replace_by_position(arguments[0], finalized_input_column);
63
1
        RETURN_IF_ERROR(executor(finalized_block));
64
1
        block.replace_by_position(result, finalized_block.get_by_position(result).column);
65
1
        return Status::OK();
66
1
    };
Unexecuted instantiation: _ZZN5doris11CastWrapper22cast_from_variant_implEPNS_15FunctionContextERNS_5BlockERKSt6vectorIjSaIjEEjmPKhRKSt10shared_ptrIKNS_9IDataTypeEEENKUlOT_E_clIZNS0_22cast_from_variant_implES2_S4_S9_jmSB_SH_EUlS4_E0_EENS_6StatusESJ_
67
68
    // It's important to convert as many elements as possible in this context. For instance,
69
    // if the root of this variant column is a number column, converting it to a number column
70
    // is acceptable. However, if the destination type is a string and root is none scalar root, then
71
    // we should convert the entire tree to a string.
72
    // A scalar variant is only worth converting through its root when the root actually carries a
73
    // value. A column whose rows are all empty JSON objects is still "scalar" - it simply has no
74
    // path, so its root never got a type - and routing it through the root conversion leaves
75
    // nothing to convert and turns every row into NULL, instead of the `{}` that serializing the
76
    // whole tree produces for the very same value in a column that does hold paths. See #67367.
77
18
    const bool root_carries_value = !variant->is_null_root() &&
78
18
                                    variant->get_root_type()->get_primitive_type() != INVALID_TYPE;
79
18
    const bool to_string_or_jsonb = is_string_type(data_type_to->get_primitive_type()) ||
80
18
                                    data_type_to->get_primitive_type() == TYPE_JSONB;
81
18
    bool is_root_valuable =
82
18
            root_carries_value && (variant->is_scalar_variant() || !to_string_or_jsonb);
83
18
    if (is_root_valuable && to_string_or_jsonb) {
84
        // Once the root is typed, a row that holds an empty JSON object shows up as a NULL root
85
        // inside a variant row that is not itself NULL. Converting such a root to STRING/JSONB
86
        // yields NULL, while serializing the tree renders it as `{}` - which is what `SELECT
87
        // <variant>` returns, and what the same value returns from a column that also holds
88
        // paths. When no row's root holds a value there is nothing for the root conversion to
89
        // convert, so serialize the tree instead. Rows whose root does hold a value keep the root
90
        // conversion, which is what unwraps a JSON string into its text. See #67367.
91
        // Only the rows this call actually converts get a say. prepare_remove_nullable hands the
92
        // outer null map over as a separate argument and leaves the masked rows' payloads in
93
        // place, so a masked row whose root does carry a value would otherwise keep every visible
94
        // row on the root conversion.
95
1
        const auto* nullable_root = check_and_get_column<ColumnNullable>(*variant->get_root());
96
1
        if (nullable_root != nullptr) {
97
0
            const auto& root_null_map = nullable_root->get_null_map_data();
98
0
            const NullMap::value_type* outer_null_map = null_map;
99
0
            if (outer_null_map == nullptr && nullable != nullptr) {
100
0
                outer_null_map = nullable->get_null_map_data().data();
101
0
            }
102
0
            const size_t rows_to_scan = std::min(input_rows_count, root_null_map.size());
103
0
            is_root_valuable = false;
104
0
            for (size_t row = 0; row < rows_to_scan; ++row) {
105
0
                if (outer_null_map != nullptr && outer_null_map[row] != 0) {
106
0
                    continue;
107
0
                }
108
0
                if (root_null_map[row] == 0) {
109
0
                    is_root_valuable = true;
110
0
                    break;
111
0
                }
112
0
            }
113
0
        }
114
1
    }
115
116
18
    if (is_root_valuable) {
117
11
        ColumnPtr nested = variant->get_root();
118
11
        auto nested_from_type = variant->get_root_type();
119
        // DCHECK(nested_from_type->is_nullable());
120
11
        DCHECK(!data_type_to->is_nullable());
121
11
        auto new_context = context == nullptr ? nullptr : context->clone();
122
11
        if (new_context != nullptr) {
123
11
            new_context->set_jsonb_string_as_string(true);
124
            // Disable strict mode for the inner JSONB→target conversion.
125
            // The variant root column may contain null/empty JSONB entries for rows
126
            // where the subcolumn doesn't exist (e.g., mixed-schema variant data).
127
            // In strict mode (INSERT context), these null entries cause the ENTIRE
128
            // cast to fail and return all NULLs. Since this is an internal type
129
            // conversion within variant, not user-provided INSERT data validation,
130
            // strict mode should not apply here.
131
11
            new_context->set_enable_strict_mode(false);
132
11
        }
133
        // dst type nullable has been removed, so we should remove the inner nullable of root column
134
11
        auto wrapper =
135
11
                prepare_impl(new_context.get(), remove_nullable(nested_from_type), data_type_to);
136
11
        Block tmp_block {{remove_nullable(nested), remove_nullable(nested_from_type), ""}};
137
11
        tmp_block.insert({nullptr, data_type_to, ""});
138
        /// Perform the requested conversion.
139
11
        Status st = wrapper(new_context.get(), tmp_block, {0}, 1, input_rows_count, nullptr);
140
11
        if (!st.ok()) {
141
            // Fill with default values, which is null
142
0
            col_to->assert_mutable()->insert_many_defaults(input_rows_count);
143
0
            col_to = make_nullable(col_to, true);
144
11
        } else {
145
11
            col_to = tmp_block.get_by_position(1).column;
146
11
            col_to = wrap_in_nullable(col_to,
147
11
                                      Block({{nested, nested_from_type, ""},
148
11
                                             {col_from, col_with_type_and_name.type, ""},
149
11
                                             {col_to, data_type_to, ""}}),
150
11
                                      {0, 1}, input_rows_count);
151
11
        }
152
11
    } else {
153
        // A variant that only holds default values still has a JSON rendering - every such row is
154
        // the empty object - so a STRING/JSONB target has to serialize the tree rather than fall
155
        // into the all-NULL branch below, which stays the right answer for scalar targets.
156
7
        if (is_string_type(data_type_to->get_primitive_type())) {
157
            // serialize to string
158
4
            return execute_on_finalized_input([&](Block& finalized_block) {
159
4
                return CastToStringFunction::execute_impl(context, finalized_block, arguments,
160
4
                                                          result, input_rows_count);
161
4
            });
162
4
        } else if (data_type_to->get_primitive_type() == TYPE_JSONB) {
163
            // serialize to json by parsing
164
0
            return execute_on_finalized_input([&](Block& finalized_block) {
165
0
                return cast_from_generic_to_jsonb(context, finalized_block, arguments, result,
166
0
                                                  input_rows_count);
167
0
            });
168
3
        } else if (variant->only_have_default_values()) {
169
0
            col_to->assert_mutable()->insert_many_defaults(input_rows_count);
170
0
            col_to = make_nullable(col_to, true);
171
3
        } else if (!data_type_to->is_nullable() &&
172
3
                   !is_string_type(data_type_to->get_primitive_type())) {
173
            // other types
174
3
            col_to->assert_mutable()->insert_many_defaults(input_rows_count);
175
3
            col_to = make_nullable(col_to, true);
176
3
        } else {
177
0
            assert_cast<ColumnNullable&>(*col_to->assert_mutable())
178
0
                    .insert_many_defaults(input_rows_count);
179
0
        }
180
7
    }
181
182
14
    if (null_map == nullptr) {
183
9
        if (const auto* nullable_result = check_and_get_column<ColumnNullable>(*col_to);
184
9
            nullable_result != nullptr && !nullable_result->has_null()) {
185
3
            col_to = nullable_result->get_nested_column_ptr();
186
3
        }
187
9
    }
188
189
14
    if (col_to->size() != input_rows_count) {
190
0
        return Status::InternalError("Unmatched row count {}, expected {}", col_to->size(),
191
0
                                     input_rows_count);
192
0
    }
193
194
14
    block.replace_by_position(result, std::move(col_to));
195
14
    return Status::OK();
196
14
}
197
198
struct CastFromVariant {
199
    static Status execute(FunctionContext* context, Block& block, const ColumnNumbers& arguments,
200
                          uint32_t result, size_t input_rows_count,
201
0
                          const NullMap::value_type* null_map = nullptr) {
202
0
        auto& data_type_to = block.get_by_position(result).type;
203
0
        return cast_from_variant_impl(context, block, arguments, result, input_rows_count, null_map,
204
0
                                      data_type_to);
205
0
    }
206
};
207
208
struct CastToVariant {
209
    static Status execute(FunctionContext* context, Block& block, const ColumnNumbers& arguments,
210
                          uint32_t result, size_t input_rows_count,
211
3
                          const NullMap::value_type* null_map = nullptr) {
212
        // auto& data_type_to = block.get_by_position(result).type;
213
3
        const auto& col_with_type_and_name = block.get_by_position(arguments[0]);
214
3
        const auto& from_type = col_with_type_and_name.type;
215
3
        const auto& col_from = col_with_type_and_name.column;
216
        // set variant root column/type to from column/type
217
3
        const auto& data_type_to = block.get_by_position(result).type;
218
3
        const auto* variant_type =
219
3
                typeid_cast<const DataTypeVariant*>(remove_nullable(data_type_to).get());
220
3
        auto variant = ColumnVariant::create(
221
3
                variant_type ? variant_type->variant_max_subcolumns_count() : 0,
222
3
                variant_type ? variant_type->enable_doc_mode() : false);
223
3
        variant->create_root(from_type, IColumn::mutate(col_from));
224
3
        block.replace_by_position(result, std::move(variant));
225
3
        return Status::OK();
226
3
    }
227
};
228
229
// create corresponding variant value to wrap from_type
230
WrapperType create_cast_to_variant_wrapper(const DataTypePtr& from_type,
231
3
                                           const DataTypeVariant& to_type) {
232
3
    if (from_type->get_primitive_type() == TYPE_VARIANT) {
233
        // variant_max_subcolumns_count is not equal
234
0
        return create_unsupport_wrapper(from_type->get_name(), to_type.get_name());
235
0
    }
236
3
    return &CastToVariant::execute;
237
3
}
238
239
// create corresponding type convert from variant
240
WrapperType create_cast_from_variant_wrapper(const DataTypeVariant& from_type,
241
18
                                             const DataTypePtr& to_type) {
242
18
    if (to_type->get_primitive_type() == TYPE_VARIANT) {
243
        // variant_max_subcolumns_count is not equal
244
0
        return create_unsupport_wrapper(from_type.get_name(), to_type->get_name());
245
0
    }
246
    // Capture explicit target type to make the cast independent from Block[result].type.
247
18
    DataTypePtr captured_to_type = to_type;
248
18
    return [captured_to_type](FunctionContext* context, Block& block,
249
18
                              const ColumnNumbers& arguments, uint32_t result,
250
18
                              size_t input_rows_count,
251
18
                              const NullMap::value_type* null_map) -> Status {
252
18
        return cast_from_variant_impl(context, block, arguments, result, input_rows_count, null_map,
253
18
                                      captured_to_type);
254
18
    };
255
18
}
256
257
} // namespace doris::CastWrapper