Coverage Report

Created: 2026-07-13 13:25

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/format_v2/column_mapper.cpp
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
#include "format_v2/column_mapper.h"
19
20
#include <algorithm>
21
#include <cstddef>
22
#include <memory>
23
#include <optional>
24
#include <sstream>
25
#include <string_view>
26
#include <utility>
27
#include <vector>
28
29
#include "common/consts.h"
30
#include "common/exception.h"
31
#include "common/status.h"
32
#include "core/data_type/convert_field_to_type.h"
33
#include "core/data_type/data_type_array.h"
34
#include "core/data_type/data_type_map.h"
35
#include "core/data_type/data_type_nullable.h"
36
#include "core/data_type/data_type_string.h"
37
#include "core/data_type/data_type_struct.h"
38
#include "core/data_type/primitive_type.h"
39
#include "exprs/runtime_filter_expr.h"
40
#include "exprs/short_circuit_evaluation_expr.h"
41
#include "exprs/vcase_expr.h"
42
#include "exprs/vcast_expr.h"
43
#include "exprs/vcondition_expr.h"
44
#include "exprs/vectorized_fn_call.h"
45
#include "exprs/vexpr_context.h"
46
#include "exprs/vin_predicate.h"
47
#include "exprs/vliteral.h"
48
#include "format_v2/column_mapper_nested.h"
49
#include "format_v2/expr/cast.h"
50
#include "format_v2/file_reader.h"
51
#include "format_v2/schema_projection.h"
52
#include "format_v2/table_reader.h"
53
#include "gen_cpp/Exprs_types.h"
54
55
namespace doris::format {
56
57
namespace {
58
59
16
std::string mapping_mode_to_string(TableColumnMappingMode mode) {
60
16
    switch (mode) {
61
1
    case TableColumnMappingMode::BY_FIELD_ID:
62
1
        return "BY_FIELD_ID";
63
14
    case TableColumnMappingMode::BY_NAME:
64
14
        return "BY_NAME";
65
1
    case TableColumnMappingMode::BY_INDEX:
66
1
        return "BY_INDEX";
67
16
    }
68
0
    return "UNKNOWN";
69
16
}
70
71
13.4M
bool column_has_name(const ColumnDefinition& column, const std::string& name) {
72
13.4M
    if (to_lower(column.name) == to_lower(name)) {
73
312k
        return true;
74
312k
    }
75
13.2M
    if (column.has_identifier_name() && to_lower(column.get_identifier_name()) == to_lower(name)) {
76
0
        return true;
77
0
    }
78
13.1M
    return std::ranges::any_of(column.name_mapping, [&](const std::string& alias) {
79
146
        return to_lower(alias) == to_lower(name);
80
146
    });
81
13.1M
}
82
83
7.06M
bool column_names_match(const ColumnDefinition& lhs, const ColumnDefinition& rhs) {
84
7.06M
    if (column_has_name(rhs, lhs.name)) {
85
312k
        return true;
86
312k
    }
87
6.74M
    if (lhs.has_identifier_name() && column_has_name(rhs, lhs.get_identifier_name())) {
88
1
        return true;
89
1
    }
90
6.74M
    return std::ranges::any_of(lhs.name_mapping, [&](const std::string& alias) {
91
18
        return column_has_name(rhs, alias);
92
18
    });
93
6.74M
}
94
95
class ColumnMatcher {
96
public:
97
3
    virtual ~ColumnMatcher() = default;
98
    virtual const ColumnDefinition* find(
99
            const ColumnDefinition& table_column,
100
            const std::vector<ColumnDefinition>& file_schema) const = 0;
101
};
102
103
class FieldIdMatcher final : public ColumnMatcher {
104
public:
105
    const ColumnDefinition* find(const ColumnDefinition& table_column,
106
112k
                                 const std::vector<ColumnDefinition>& file_schema) const override {
107
112k
        if (!table_column.has_identifier_field_id()) {
108
7
            return nullptr;
109
7
        }
110
112k
        const auto field_id = table_column.get_identifier_field_id();
111
774k
        const auto field_it = std::ranges::find_if(file_schema, [&](const ColumnDefinition& field) {
112
774k
            return field.has_identifier_field_id() && field.get_identifier_field_id() == field_id;
113
774k
        });
114
112k
        return field_it == file_schema.end() ? nullptr : &*field_it;
115
112k
    }
116
};
117
118
class NameMatcher final : public ColumnMatcher {
119
public:
120
    const ColumnDefinition* find(const ColumnDefinition& table_column,
121
313k
                                 const std::vector<ColumnDefinition>& file_schema) const override {
122
7.05M
        const auto field_it = std::ranges::find_if(file_schema, [&](const ColumnDefinition& field) {
123
7.05M
            return column_names_match(table_column, field);
124
7.05M
        });
125
313k
        return field_it == file_schema.end() ? nullptr : &*field_it;
126
313k
    }
127
};
128
129
class PositionMatcher final : public ColumnMatcher {
130
public:
131
    const ColumnDefinition* find(const ColumnDefinition& table_column,
132
2
                                 const std::vector<ColumnDefinition>& file_schema) const override {
133
2
        if (!table_column.has_identifier_field_id()) {
134
2
            return nullptr;
135
2
        }
136
0
        const auto position = table_column.get_identifier_position();
137
0
        if (position < 0 || static_cast<size_t>(position) >= file_schema.size()) {
138
0
            return nullptr;
139
0
        }
140
0
        return &file_schema[static_cast<size_t>(position)];
141
0
    }
142
};
143
144
426k
const ColumnMatcher& matcher_for_mode(TableColumnMappingMode mode) {
145
426k
    static const FieldIdMatcher field_id_matcher;
146
426k
    static const NameMatcher name_matcher;
147
426k
    static const PositionMatcher position_matcher;
148
426k
    switch (mode) {
149
112k
    case TableColumnMappingMode::BY_FIELD_ID:
150
112k
        return field_id_matcher;
151
313k
    case TableColumnMappingMode::BY_NAME:
152
313k
        return name_matcher;
153
2
    case TableColumnMappingMode::BY_INDEX:
154
2
        return position_matcher;
155
426k
    }
156
0
    return field_id_matcher;
157
426k
}
158
159
12
std::string virtual_column_type_to_string(TableVirtualColumnType type) {
160
12
    switch (type) {
161
9
    case TableVirtualColumnType::INVALID:
162
9
        return "INVALID";
163
1
    case TableVirtualColumnType::ROW_ID:
164
1
        return "ROW_ID";
165
1
    case TableVirtualColumnType::LAST_UPDATED_SEQUENCE_NUMBER:
166
1
        return "LAST_UPDATED_SEQUENCE_NUMBER";
167
1
    case TableVirtualColumnType::ICEBERG_ROWID:
168
1
        return "ICEBERG_ROWID";
169
12
    }
170
0
    return "UNKNOWN";
171
12
}
172
173
12
std::string filter_conversion_type_to_string(FilterConversionType type) {
174
12
    switch (type) {
175
3
    case FilterConversionType::COPY_DIRECTLY:
176
3
        return "COPY_DIRECTLY";
177
1
    case FilterConversionType::CAST_FILTER:
178
1
        return "CAST_FILTER";
179
1
    case FilterConversionType::READER_EXPRESSION:
180
1
        return "READER_EXPRESSION";
181
6
    case FilterConversionType::FINALIZE_ONLY:
182
6
        return "FINALIZE_ONLY";
183
1
    case FilterConversionType::CONSTANT:
184
1
        return "CONSTANT";
185
12
    }
186
0
    return "UNKNOWN";
187
12
}
188
189
47
std::string data_type_debug_string(const DataTypePtr& type) {
190
47
    return type == nullptr ? "null" : type->get_name();
191
47
}
192
193
11
std::string field_debug_string(const Field& field) {
194
11
    std::ostringstream out;
195
11
    out << "Field{type=" << type_to_string(field.get_type()) << ", value=";
196
11
    switch (field.get_type()) {
197
0
    case TYPE_NULL:
198
0
        out << "null";
199
0
        break;
200
9
    case TYPE_INT:
201
9
        out << field.get<TYPE_INT>();
202
9
        break;
203
0
    case TYPE_BIGINT:
204
0
        out << field.get<TYPE_BIGINT>();
205
0
        break;
206
2
    case TYPE_STRING:
207
2
        out << field.get<TYPE_STRING>();
208
2
        break;
209
0
    default:
210
0
        out << field.to_debug_string(0);
211
0
        break;
212
11
    }
213
11
    out << "}";
214
11
    return out.str();
215
11
}
216
217
template <typename T, typename Formatter>
218
50
std::string join_debug_strings(const std::vector<T>& values, Formatter formatter) {
219
50
    std::ostringstream out;
220
50
    out << "[";
221
72
    for (size_t i = 0; i < values.size(); ++i) {
222
22
        if (i > 0) {
223
1
            out << ", ";
224
1
        }
225
22
        out << formatter(values[i]);
226
22
    }
227
50
    out << "]";
228
50
    return out.str();
229
50
}
column_mapper.cpp:_ZN5doris6format12_GLOBAL__N_118join_debug_stringsINSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEZNKS0_16ColumnDefinition12debug_stringEvE3$_0EES8_RKSt6vectorIT_SaISC_EET0_
Line
Count
Source
218
11
std::string join_debug_strings(const std::vector<T>& values, Formatter formatter) {
219
11
    std::ostringstream out;
220
11
    out << "[";
221
19
    for (size_t i = 0; i < values.size(); ++i) {
222
8
        if (i > 0) {
223
0
            out << ", ";
224
0
        }
225
8
        out << formatter(values[i]);
226
8
    }
227
11
    out << "]";
228
11
    return out.str();
229
11
}
column_mapper.cpp:_ZN5doris6format12_GLOBAL__N_118join_debug_stringsINS0_16ColumnDefinitionEZNKS3_12debug_stringB5cxx11EvE3$_1EENSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEERKSt6vectorIT_SaISC_EET0_
Line
Count
Source
218
11
std::string join_debug_strings(const std::vector<T>& values, Formatter formatter) {
219
11
    std::ostringstream out;
220
11
    out << "[";
221
12
    for (size_t i = 0; i < values.size(); ++i) {
222
1
        if (i > 0) {
223
0
            out << ", ";
224
0
        }
225
1
        out << formatter(values[i]);
226
1
    }
227
11
    out << "]";
228
11
    return out.str();
229
11
}
column_mapper.cpp:_ZN5doris6format12_GLOBAL__N_118join_debug_stringsINS0_16LocalColumnIndexEZNKS3_12debug_stringB5cxx11EvE3$_0EENSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEERKSt6vectorIT_SaISC_EET0_
Line
Count
Source
218
2
std::string join_debug_strings(const std::vector<T>& values, Formatter formatter) {
219
2
    std::ostringstream out;
220
2
    out << "[";
221
3
    for (size_t i = 0; i < values.size(); ++i) {
222
1
        if (i > 0) {
223
0
            out << ", ";
224
0
        }
225
1
        out << formatter(values[i]);
226
1
    }
227
2
    out << "]";
228
2
    return out.str();
229
2
}
column_mapper.cpp:_ZN5doris6format12_GLOBAL__N_118join_debug_stringsINS0_16ColumnDefinitionEZNKS0_13ColumnMapping12debug_stringB5cxx11EvE3$_0EENSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEERKSt6vectorIT_SaISD_EET0_
Line
Count
Source
218
12
std::string join_debug_strings(const std::vector<T>& values, Formatter formatter) {
219
12
    std::ostringstream out;
220
12
    out << "[";
221
17
    for (size_t i = 0; i < values.size(); ++i) {
222
5
        if (i > 0) {
223
0
            out << ", ";
224
0
        }
225
5
        out << formatter(values[i]);
226
5
    }
227
12
    out << "]";
228
12
    return out.str();
229
12
}
column_mapper.cpp:_ZN5doris6format12_GLOBAL__N_118join_debug_stringsINS0_13ColumnMappingEZNKS3_12debug_stringB5cxx11EvE3$_1EENSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEERKSt6vectorIT_SaISC_EET0_
Line
Count
Source
218
12
std::string join_debug_strings(const std::vector<T>& values, Formatter formatter) {
219
12
    std::ostringstream out;
220
12
    out << "[";
221
17
    for (size_t i = 0; i < values.size(); ++i) {
222
5
        if (i > 0) {
223
0
            out << ", ";
224
0
        }
225
5
        out << formatter(values[i]);
226
5
    }
227
12
    out << "]";
228
12
    return out.str();
229
12
}
column_mapper.cpp:_ZN5doris6format12_GLOBAL__N_118join_debug_stringsINS0_13ColumnMappingEZNKS0_17TableColumnMapper12debug_stringB5cxx11EvE3$_0EENSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEERKSt6vectorIT_SaISD_EET0_
Line
Count
Source
218
1
std::string join_debug_strings(const std::vector<T>& values, Formatter formatter) {
219
1
    std::ostringstream out;
220
1
    out << "[";
221
3
    for (size_t i = 0; i < values.size(); ++i) {
222
2
        if (i > 0) {
223
1
            out << ", ";
224
1
        }
225
2
        out << formatter(values[i]);
226
2
    }
227
1
    out << "]";
228
1
    return out.str();
229
1
}
column_mapper.cpp:_ZN5doris6format12_GLOBAL__N_118join_debug_stringsINS0_13ColumnMappingEZNKS0_17TableColumnMapper12debug_stringB5cxx11EvE3$_1EENSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEERKSt6vectorIT_SaISD_EET0_
Line
Count
Source
218
1
std::string join_debug_strings(const std::vector<T>& values, Formatter formatter) {
219
1
    std::ostringstream out;
220
1
    out << "[";
221
1
    for (size_t i = 0; i < values.size(); ++i) {
222
0
        if (i > 0) {
223
0
            out << ", ";
224
0
        }
225
0
        out << formatter(values[i]);
226
0
    }
227
1
    out << "]";
228
1
    return out.str();
229
1
}
230
231
} // namespace
232
233
const Field* find_partition_value(const ColumnDefinition& table_column,
234
409k
                                  const std::map<std::string, Field>& partition_values) {
235
679k
    const auto find_by_name = [&](const std::string& name) -> const Field* {
236
679k
        const auto value_it = partition_values.find(name);
237
679k
        return value_it == partition_values.end() ? nullptr : &value_it->second;
238
679k
    };
239
409k
    if (const auto* value = find_by_name(table_column.name); value != nullptr) {
240
18.3k
        return value;
241
18.3k
    }
242
390k
    if (table_column.has_identifier_name()) {
243
270k
        if (const auto* value = find_by_name(table_column.get_identifier_name());
244
270k
            value != nullptr) {
245
0
            return value;
246
0
        }
247
270k
    }
248
390k
    for (const auto& alias : table_column.name_mapping) {
249
21
        if (const auto* value = find_by_name(alias); value != nullptr) {
250
1
            return value;
251
1
        }
252
21
    }
253
390k
    return nullptr;
254
390k
}
255
256
struct FileSlotRewriteInfo {
257
    size_t block_position = 0;
258
    DataTypePtr file_type;
259
    DataTypePtr table_type;
260
    std::string file_column_name;
261
};
262
263
struct RewriteContext {
264
    RuntimeState* runtime_state = nullptr;
265
    std::vector<VExprSPtr> created_exprs {};
266
267
50.1k
    void add_created_expr(VExprSPtr expr) { created_exprs.push_back(std::move(expr)); }
268
269
32.8k
    Status prepare_created_exprs(VExprContext* context) const {
270
32.8k
        DORIS_CHECK(context != nullptr);
271
32.8k
        RowDescriptor row_desc;
272
49.5k
        for (const auto& expr : created_exprs) {
273
49.5k
            if (dynamic_cast<const Cast*>(expr.get()) != nullptr && runtime_state == nullptr) {
274
0
                return Status::InvalidArgument(
275
0
                        "RuntimeState is required to prepare rewritten cast expression {}",
276
0
                        expr->expr_name());
277
0
            }
278
49.5k
            RETURN_IF_ERROR(expr->prepare(runtime_state, row_desc, context));
279
49.5k
        }
280
32.8k
        return Status::OK();
281
32.8k
    }
282
};
283
284
static VExprSPtr create_file_slot_ref(const VSlotRef& slot_ref,
285
                                      const FileSlotRewriteInfo& rewrite_info,
286
34.2k
                                      RewriteContext* rewrite_context) {
287
34.2k
    auto ref =
288
34.2k
            VSlotRef::create_shared(slot_ref.slot_id(), cast_set<int>(rewrite_info.block_position),
289
34.2k
                                    -1, rewrite_info.file_type, rewrite_info.file_column_name);
290
34.2k
    rewrite_context->add_created_expr(ref);
291
34.2k
    return ref;
292
34.2k
}
293
294
93.7k
static bool is_cast_expr(const VExprSPtr& expr) {
295
93.7k
    return dynamic_cast<const Cast*>(expr.get()) != nullptr;
296
93.7k
}
297
298
79.3k
static bool is_binary_comparison_predicate(const VExprSPtr& expr) {
299
79.3k
    if (expr == nullptr || expr->get_num_children() != 2 ||
300
79.3k
        (expr->node_type() != TExprNodeType::BINARY_PRED &&
301
62.1k
         expr->node_type() != TExprNodeType::NULL_AWARE_BINARY_PRED)) {
302
62.1k
        return false;
303
62.1k
    }
304
17.1k
    switch (expr->op()) {
305
7.91k
    case TExprOpcode::EQ:
306
7.91k
    case TExprOpcode::EQ_FOR_NULL:
307
9.22k
    case TExprOpcode::NE:
308
11.2k
    case TExprOpcode::GE:
309
14.2k
    case TExprOpcode::GT:
310
16.0k
    case TExprOpcode::LE:
311
17.1k
    case TExprOpcode::LT:
312
17.1k
        return true;
313
0
    default:
314
0
        return false;
315
17.1k
    }
316
17.1k
}
317
318
16
std::string TableColumnMapperOptions::debug_string() const {
319
16
    std::ostringstream out;
320
16
    out << "TableColumnMapperOptions{mode=" << mapping_mode_to_string(mode) << "}";
321
16
    return out.str();
322
16
}
323
324
11
std::string ColumnDefinition::debug_string() const {
325
11
    std::ostringstream out;
326
11
    out << "ColumnDefinition{name=" << name << ", identifier=" << field_debug_string(identifier)
327
11
        << ", name_mapping="
328
11
        << join_debug_strings(name_mapping, [](const std::string& name) { return name; })
329
11
        << ", local_id=" << local_id << ", type=" << data_type_debug_string(type) << ", children="
330
11
        << join_debug_strings(children,
331
11
                              [](const ColumnDefinition& child) { return child.debug_string(); })
332
11
        << ", has_default_expr=" << (default_expr != nullptr)
333
11
        << ", is_partition_key=" << is_partition_key << "}";
334
11
    return out.str();
335
11
}
336
337
2
std::string LocalColumnIndex::debug_string() const {
338
2
    std::ostringstream out;
339
2
    out << "LocalColumnIndex{index=" << index << ", project_all_children=" << project_all_children
340
2
        << ", children="
341
2
        << join_debug_strings(children,
342
2
                              [](const LocalColumnIndex& child) { return child.debug_string(); })
343
2
        << "}";
344
2
    return out.str();
345
2
}
346
347
12
std::string ColumnMapping::debug_string() const {
348
12
    std::ostringstream out;
349
12
    out << "ColumnMapping{global_index=" << global_index
350
12
        << ", table_column_name=" << table_column_name << ", file_local_id=";
351
12
    if (file_local_id.has_value()) {
352
7
        out << *file_local_id;
353
7
    } else {
354
5
        out << "null";
355
5
    }
356
12
    out << ", constant_index=";
357
12
    if (constant_index.has_value()) {
358
5
        out << *constant_index;
359
7
    } else {
360
7
        out << "null";
361
7
    }
362
12
    out << ", file_column_name=" << file_column_name
363
12
        << ", original_file_type=" << data_type_debug_string(original_file_type)
364
12
        << ", original_file_children="
365
12
        << join_debug_strings(original_file_children,
366
12
                              [](const ColumnDefinition& child) { return child.debug_string(); })
367
12
        << ", file_type=" << data_type_debug_string(file_type)
368
12
        << ", table_type=" << data_type_debug_string(table_type)
369
12
        << ", has_projection=" << (projection != nullptr) << ", child_mappings="
370
12
        << join_debug_strings(child_mappings,
371
12
                              [](const ColumnMapping& child) { return child.debug_string(); })
372
12
        << ", is_trivial=" << is_trivial << ", is_constant=" << constant_index.has_value()
373
12
        << ", filter_conversion=" << filter_conversion_type_to_string(filter_conversion)
374
12
        << ", virtual_column_type=" << virtual_column_type_to_string(virtual_column_type)
375
12
        << ", has_default_expr=" << (default_expr != nullptr) << "}";
376
12
    return out.str();
377
12
}
378
379
1
std::string TableColumnMapper::debug_string() const {
380
1
    std::ostringstream out;
381
1
    out << "TableColumnMapper{options=" << _options.debug_string() << ", mappings="
382
1
        << join_debug_strings(_mappings,
383
2
                              [](const ColumnMapping& mapping) { return mapping.debug_string(); })
384
1
        << ", hidden_mappings="
385
1
        << join_debug_strings(_hidden_mappings,
386
1
                              [](const ColumnMapping& mapping) { return mapping.debug_string(); })
387
1
        << ", constant_count=" << _constant_map.size() << "}";
388
1
    return out.str();
389
1
}
390
391
static const FileSlotRewriteInfo* find_slot_rewrite_info(
392
        const VExprSPtr& expr,
393
        const std::map<GlobalIndex, FileSlotRewriteInfo>& global_to_file_slot,
394
25.1k
        const VSlotRef** slot_ref) {
395
25.1k
    if (expr == nullptr) {
396
0
        return nullptr;
397
0
    }
398
25.1k
    VExprSPtr slot_expr = expr;
399
25.1k
    const bool input_is_cast = is_cast_expr(expr) && expr->get_num_children() == 1;
400
25.1k
    if (is_cast_expr(expr) && expr->get_num_children() == 1) {
401
462
        slot_expr = expr->children()[0];
402
462
    }
403
25.1k
    if (!slot_expr->is_slot_ref()) {
404
12.0k
        return nullptr;
405
12.0k
    }
406
13.0k
    const auto* candidate_slot_ref = assert_cast<const VSlotRef*>(slot_expr.get());
407
13.0k
    const auto rewrite_it = global_to_file_slot.find(slot_ref_global_index(*candidate_slot_ref));
408
13.0k
    if (rewrite_it == global_to_file_slot.end()) {
409
0
        return nullptr;
410
0
    }
411
13.0k
    if (input_is_cast && !expr->data_type()->equals(*rewrite_it->second.table_type)) {
412
426
        return nullptr;
413
426
    }
414
12.6k
    if (slot_ref != nullptr) {
415
12.6k
        *slot_ref = candidate_slot_ref;
416
12.6k
    }
417
12.6k
    return &rewrite_it->second;
418
13.0k
}
419
420
449k
static bool filter_conversion_has_local_source(FilterConversionType conversion) {
421
449k
    switch (conversion) {
422
415k
    case FilterConversionType::COPY_DIRECTLY:
423
434k
    case FilterConversionType::CAST_FILTER:
424
448k
    case FilterConversionType::READER_EXPRESSION:
425
448k
        return true;
426
1.02k
    case FilterConversionType::FINALIZE_ONLY:
427
1.02k
    case FilterConversionType::CONSTANT:
428
1.02k
        return false;
429
449k
    }
430
0
    return false;
431
449k
}
432
433
static bool table_filter_has_only_local_entries(
434
40.0k
        const TableFilter& table_filter, const std::map<GlobalIndex, FilterEntry>& filter_entries) {
435
40.8k
    for (const auto global_index : table_filter.global_indices) {
436
40.8k
        const auto entry_it = filter_entries.find(global_index);
437
40.8k
        if (entry_it == filter_entries.end() || !entry_it->second.is_local()) {
438
4.88k
            return false;
439
4.88k
        }
440
40.8k
    }
441
35.1k
    return true;
442
40.0k
}
443
444
static VExprSPtr unwrap_literal_for_file_cast(const VExprSPtr& expr,
445
15.2k
                                              const DataTypePtr& table_type) {
446
15.2k
    if (expr == nullptr) {
447
0
        return nullptr;
448
0
    }
449
15.2k
    if (expr->is_literal()) {
450
15.2k
        return expr;
451
15.2k
    }
452
14
    if (is_cast_expr(expr) && expr->get_num_children() == 1 && expr->children()[0]->is_literal() &&
453
14
        expr->children()[0]->data_type()->equals(*table_type)) {
454
0
        return expr->children()[0];
455
0
    }
456
14
    return nullptr;
457
14
}
458
459
0
static Field literal_field_from_expr(const VExpr& literal_expr) {
460
0
    DORIS_CHECK(literal_expr.is_literal());
461
0
    const auto* literal = dynamic_cast<const VLiteral*>(&literal_expr);
462
0
    DORIS_CHECK(literal != nullptr);
463
0
    Field field;
464
0
    literal->get_column_ptr()->get(0, field);
465
0
    return field;
466
0
}
467
468
// Table filter localization clones an already-prepared table expr and then rewrites it to file
469
// slots. Only split-local literals and BE cast nodes need table-reader-specific clone behavior;
470
// plain slot refs and literals use their own VExpr::clone_node().
471
545k
static Status clone_table_expr_node(const VExpr& expr, VExprSPtr* cloned_expr) {
472
545k
    DORIS_CHECK(cloned_expr != nullptr);
473
545k
    if (const auto* split_literal = dynamic_cast<const SplitLocalFileLiteral*>(&expr)) {
474
0
        *cloned_expr = std::make_shared<SplitLocalFileLiteral>(
475
0
                split_literal->data_type(), literal_field_from_expr(expr),
476
0
                split_literal->original_type(), split_literal->original_field());
477
545k
    } else if (const auto* vcast_expr = dynamic_cast<const VCastExpr*>(&expr);
478
545k
               vcast_expr != nullptr && vcast_expr->node_type() == TExprNodeType::CAST_EXPR) {
479
6.80k
        *cloned_expr = Cast::create_shared(vcast_expr->data_type());
480
6.80k
    }
481
545k
    return Status::OK();
482
545k
}
483
484
181k
Status clone_table_expr_tree(const VExprSPtr& expr, VExprSPtr* cloned_expr) {
485
181k
    DORIS_CHECK(cloned_expr != nullptr);
486
181k
    if (expr == nullptr) {
487
0
        *cloned_expr = nullptr;
488
0
        return Status::OK();
489
0
    }
490
181k
    return expr->deep_clone(cloned_expr, clone_table_expr_node);
491
181k
}
492
493
static VExprSPtr original_table_literal(const VExprSPtr& literal_expr,
494
16.3k
                                        RewriteContext* rewrite_context = nullptr) {
495
16.3k
    DORIS_CHECK(literal_expr != nullptr);
496
16.3k
    DORIS_CHECK(literal_expr->is_literal());
497
16.3k
    const auto* rewritten_literal = dynamic_cast<const SplitLocalFileLiteral*>(literal_expr.get());
498
16.3k
    if (rewritten_literal == nullptr) {
499
16.3k
        return literal_expr;
500
16.3k
    }
501
0
    auto literal = VLiteral::create_shared(rewritten_literal->original_type(),
502
0
                                           rewritten_literal->original_field());
503
0
    if (rewrite_context != nullptr) {
504
0
        rewrite_context->add_created_expr(literal);
505
0
    }
506
0
    return literal;
507
16.3k
}
508
509
41.2k
static ColumnDefinition hidden_column_from_slot_ref(const VSlotRef& slot_ref) {
510
41.2k
    ColumnDefinition column;
511
41.2k
    column.name = slot_ref.column_name();
512
41.2k
    column.identifier = Field::create_field<TYPE_STRING>(column.name);
513
41.2k
    column.type = slot_ref.data_type();
514
41.2k
    return column;
515
41.2k
}
516
517
static void collect_top_level_slot_columns(const VExprSPtr& expr,
518
130k
                                           std::map<GlobalIndex, ColumnDefinition>* columns) {
519
130k
    DORIS_CHECK(columns != nullptr);
520
130k
    if (expr == nullptr) {
521
0
        return;
522
0
    }
523
130k
    if (expr->is_slot_ref()) {
524
41.2k
        const auto* slot_ref = assert_cast<const VSlotRef*>(expr.get());
525
41.2k
        columns->try_emplace(slot_ref_global_index(*slot_ref),
526
41.2k
                             hidden_column_from_slot_ref(*slot_ref));
527
41.2k
        return;
528
41.2k
    }
529
90.0k
    for (const auto& child : expr->children()) {
530
90.0k
        collect_top_level_slot_columns(child, columns);
531
90.0k
    }
532
88.7k
}
533
534
230
static std::optional<uint8_t> signed_integer_width(PrimitiveType type) {
535
230
    switch (type) {
536
0
    case TYPE_TINYINT:
537
0
        return 8;
538
4
    case TYPE_SMALLINT:
539
4
        return 16;
540
60
    case TYPE_INT:
541
60
        return 32;
542
39
    case TYPE_BIGINT:
543
39
        return 64;
544
0
    case TYPE_LARGEINT:
545
0
        return 128;
546
127
    default:
547
127
        return std::nullopt;
548
230
    }
549
230
}
550
551
111
static std::optional<uint8_t> floating_width(PrimitiveType type) {
552
111
    switch (type) {
553
0
    case TYPE_FLOAT:
554
0
        return 32;
555
1
    case TYPE_DOUBLE:
556
1
        return 64;
557
110
    default:
558
110
        return std::nullopt;
559
111
    }
560
111
}
561
562
17
static std::optional<uint8_t> floating_exact_integer_width(PrimitiveType type) {
563
17
    switch (type) {
564
0
    case TYPE_FLOAT:
565
0
        return 24;
566
3
    case TYPE_DOUBLE:
567
3
        return 53;
568
14
    default:
569
14
        return std::nullopt;
570
17
    }
571
17
}
572
573
static bool is_lossless_file_to_table_numeric_cast(const DataTypePtr& file_type,
574
12.3k
                                                   const DataTypePtr& table_type) {
575
12.3k
    const auto file_nested_type = remove_nullable(file_type);
576
12.3k
    const auto table_nested_type = remove_nullable(table_type);
577
12.3k
    if (file_nested_type->equals(*table_nested_type)) {
578
12.2k
        return true;
579
12.2k
    }
580
581
170
    const auto file_primitive_type = file_nested_type->get_primitive_type();
582
170
    const auto table_primitive_type = table_nested_type->get_primitive_type();
583
170
    if (const auto file_width = signed_integer_width(file_primitive_type)) {
584
60
        if (const auto table_width = signed_integer_width(table_primitive_type)) {
585
43
            return *table_width >= *file_width;
586
43
        }
587
17
        if (const auto table_width = floating_exact_integer_width(table_primitive_type)) {
588
3
            return *table_width >= *file_width;
589
3
        }
590
14
        return false;
591
17
    }
592
110
    if (const auto file_width = floating_width(file_primitive_type)) {
593
1
        const auto table_width = floating_width(table_primitive_type);
594
1
        return table_width.has_value() && *table_width >= *file_width;
595
1
    }
596
109
    return false;
597
110
}
598
599
static VExprSPtr rewrite_literal_to_file_type(const VExprSPtr& literal_expr,
600
                                              const FileSlotRewriteInfo& rewrite_info,
601
15.1k
                                              RewriteContext* rewrite_context) {
602
15.1k
    DORIS_CHECK(literal_expr != nullptr);
603
15.1k
    DORIS_CHECK(literal_expr->is_literal());
604
15.1k
    const auto original_literal = original_table_literal(literal_expr, rewrite_context);
605
15.1k
    const Field original_field = literal_field(original_literal);
606
15.1k
    if (rewrite_info.file_type->equals(*original_literal->data_type())) {
607
2.74k
        return original_literal;
608
2.74k
    }
609
    // A literal round trip alone cannot prove that file-local evaluation is safe: the file slot
610
    // itself may lose information when materialized as the table type. For example, DOUBLE 1.5
611
    // becomes BIGINT 1, so table predicate `value = 1` is true while file predicate
612
    // `value = 1.0` is false. Complex Field equality also does not compare nested contents.
613
    // Restrict localization to scalar numeric casts that preserve every file value; unsupported
614
    // and complex casts keep the table predicate and evaluate after materialization.
615
12.3k
    if (!is_lossless_file_to_table_numeric_cast(rewrite_info.file_type,
616
12.3k
                                                original_literal->data_type())) {
617
148
        return nullptr;
618
148
    }
619
12.2k
    Field file_field;
620
12.2k
    try {
621
12.2k
        convert_field_to_type(original_field, *rewrite_info.file_type, &file_field,
622
12.2k
                              original_literal->data_type().get());
623
12.2k
    } catch (const Exception&) {
624
0
        return nullptr;
625
0
    }
626
12.2k
    if (file_field.is_null()) {
627
0
        return nullptr;
628
0
    }
629
12.2k
    if (file_field.get_type() != remove_nullable(rewrite_info.file_type)->get_primitive_type()) {
630
38
        return nullptr;
631
38
    }
632
12.1k
    Field round_trip_field;
633
12.1k
    try {
634
12.1k
        convert_field_to_type(file_field, *original_literal->data_type(), &round_trip_field,
635
12.1k
                              rewrite_info.file_type.get());
636
12.1k
    } catch (const Exception&) {
637
968
        return nullptr;
638
968
    }
639
    // The file-to-table type check protects every possible file value. This round trip separately
640
    // proves that the specific predicate boundary is exactly representable in the file type.
641
11.2k
    if (round_trip_field != original_field) {
642
2
        return nullptr;
643
2
    }
644
11.2k
    auto literal = std::make_shared<SplitLocalFileLiteral>(
645
11.2k
            rewrite_info.file_type, file_field, original_literal->data_type(), original_field);
646
11.2k
    rewrite_context->add_created_expr(literal);
647
11.2k
    return literal;
648
11.2k
}
649
650
static bool rewrite_binary_slot_literal_predicate(
651
        const VExprSPtr& expr,
652
        const std::map<GlobalIndex, FileSlotRewriteInfo>& global_to_file_slot,
653
79.3k
        RewriteContext* rewrite_context) {
654
79.3k
    if (!is_binary_comparison_predicate(expr)) {
655
62.1k
        return false;
656
62.1k
    }
657
17.1k
    auto children = expr->children();
658
17.1k
    const VSlotRef* slot_ref = nullptr;
659
17.1k
    const FileSlotRewriteInfo* rewrite_info =
660
17.1k
            find_slot_rewrite_info(children[0], global_to_file_slot, &slot_ref);
661
17.1k
    int slot_child_idx = 0;
662
17.1k
    int literal_child_idx = 1;
663
17.1k
    if (rewrite_info == nullptr) {
664
6.20k
        rewrite_info = find_slot_rewrite_info(children[1], global_to_file_slot, &slot_ref);
665
6.20k
        slot_child_idx = 1;
666
6.20k
        literal_child_idx = 0;
667
6.20k
    }
668
17.1k
    if (rewrite_info == nullptr || slot_ref == nullptr) {
669
6.20k
        return false;
670
6.20k
    }
671
10.9k
    auto literal_expr =
672
10.9k
            unwrap_literal_for_file_cast(children[literal_child_idx], rewrite_info->table_type);
673
10.9k
    if (literal_expr == nullptr) {
674
12
        return false;
675
12
    }
676
677
10.9k
    auto rewritten_literal =
678
10.9k
            rewrite_literal_to_file_type(literal_expr, *rewrite_info, rewrite_context);
679
10.9k
    if (rewritten_literal == nullptr) {
680
1.10k
        children[literal_child_idx] = original_table_literal(literal_expr, rewrite_context);
681
1.10k
        expr->set_children(std::move(children));
682
1.10k
        return false;
683
1.10k
    }
684
685
9.84k
    children[slot_child_idx] = create_file_slot_ref(*slot_ref, *rewrite_info, rewrite_context);
686
9.84k
    children[literal_child_idx] = std::move(rewritten_literal);
687
9.84k
    expr->set_children(std::move(children));
688
9.84k
    return true;
689
10.9k
}
690
691
static bool rewrite_in_slot_literal_predicate(
692
        const VExprSPtr& expr,
693
        const std::map<GlobalIndex, FileSlotRewriteInfo>& global_to_file_slot,
694
69.4k
        RewriteContext* rewrite_context) {
695
69.4k
    if (expr->node_type() != TExprNodeType::IN_PRED || expr->get_num_children() < 2) {
696
67.7k
        return false;
697
67.7k
    }
698
1.76k
    auto children = expr->children();
699
1.76k
    const VSlotRef* slot_ref = nullptr;
700
1.76k
    const FileSlotRewriteInfo* rewrite_info =
701
1.76k
            find_slot_rewrite_info(children[0], global_to_file_slot, &slot_ref);
702
1.76k
    if (rewrite_info == nullptr || slot_ref == nullptr) {
703
74
        return false;
704
74
    }
705
706
1.69k
    VExprSPtrs rewritten_literals;
707
1.69k
    rewritten_literals.reserve(children.size() - 1);
708
5.81k
    for (size_t child_idx = 1; child_idx < children.size(); ++child_idx) {
709
4.17k
        auto literal_expr =
710
4.17k
                unwrap_literal_for_file_cast(children[child_idx], rewrite_info->table_type);
711
4.17k
        if (literal_expr == nullptr) {
712
0
            return false;
713
0
        }
714
4.17k
        auto rewritten_literal =
715
4.17k
                rewrite_literal_to_file_type(literal_expr, *rewrite_info, rewrite_context);
716
4.17k
        if (rewritten_literal == nullptr) {
717
150
            for (size_t restore_idx = 1; restore_idx < children.size(); ++restore_idx) {
718
100
                auto restore_literal = unwrap_literal_for_file_cast(children[restore_idx],
719
100
                                                                    rewrite_info->table_type);
720
100
                if (restore_literal != nullptr) {
721
100
                    children[restore_idx] =
722
100
                            original_table_literal(restore_literal, rewrite_context);
723
100
                }
724
100
            }
725
50
            expr->set_children(std::move(children));
726
50
            return false;
727
50
        }
728
4.12k
        rewritten_literals.push_back(std::move(rewritten_literal));
729
4.12k
    }
730
731
1.64k
    children[0] = create_file_slot_ref(*slot_ref, *rewrite_info, rewrite_context);
732
5.76k
    for (size_t literal_idx = 0; literal_idx < rewritten_literals.size(); ++literal_idx) {
733
4.12k
        children[literal_idx + 1] = std::move(rewritten_literals[literal_idx]);
734
4.12k
    }
735
1.64k
    expr->set_children(std::move(children));
736
1.64k
    return true;
737
1.69k
}
738
739
static VExprSPtr create_file_struct_child_name_literal(const std::string& file_child_name,
740
799
                                                       RewriteContext* rewrite_context) {
741
799
    auto literal = VLiteral::create_shared(std::make_shared<DataTypeString>(),
742
799
                                           Field::create_field<TYPE_STRING>(file_child_name));
743
799
    rewrite_context->add_created_expr(literal);
744
799
    return literal;
745
799
}
746
747
static bool needs_complex_file_slot_cast(const DataTypePtr& file_type,
748
4.46k
                                         const DataTypePtr& table_type) {
749
4.46k
    if (file_type == nullptr || table_type == nullptr || file_type->equals(*table_type)) {
750
0
        return false;
751
0
    }
752
4.46k
    const auto file_nested_type = remove_nullable(file_type);
753
4.46k
    const auto table_nested_type = remove_nullable(table_type);
754
4.46k
    if (file_nested_type->equals(*table_nested_type)) {
755
0
        return false;
756
0
    }
757
4.46k
    return is_complex_type(file_nested_type->get_primitive_type()) ||
758
4.46k
           is_complex_type(table_nested_type->get_primitive_type());
759
4.46k
}
760
761
799
static bool collect_struct_element_chain(const VExprSPtr& expr, std::vector<VExprSPtr>* chain) {
762
799
    DORIS_CHECK(chain != nullptr);
763
799
    if (!is_struct_element_expr(expr)) {
764
0
        return false;
765
0
    }
766
799
    const auto& parent = expr->children()[0];
767
799
    if (is_struct_element_expr(parent)) {
768
101
        if (!collect_struct_element_chain(parent, chain)) {
769
0
            return false;
770
0
        }
771
698
    } else if (!parent->is_slot_ref()) {
772
        // Only support file-local rewrite for struct child chains rooted directly at a top-level
773
        // slot, for example `element_at(s, 'a')` or `element_at(element_at(s, 'a'), 'b')`.
774
        //
775
        // Do not localize computed complex parents such as
776
        // `element_at(element_at(map_values(m), 1), 'full_name')`. The intermediate map/array
777
        // result has already been reshaped by scan projection and may have a different child order
778
        // from the table expression. Partially rewriting that expression against the file block can
779
        // silently evaluate the wrong struct child and filter out valid rows. Those predicates must
780
        // remain as table-level conjuncts and be evaluated after TableReader materialization.
781
0
        return false;
782
0
    }
783
799
    chain->push_back(expr);
784
799
    return true;
785
799
}
786
787
static bool rewrite_struct_element_path_to_file_expr(
788
        const VExprSPtr& expr, const std::vector<ColumnMapping>& mappings,
789
        const std::map<GlobalIndex, FileSlotRewriteInfo>& global_to_file_slot,
790
2.39k
        RewriteContext* rewrite_context) {
791
2.39k
    ResolvedNestedStructPath resolved;
792
2.39k
    if (!resolve_nested_struct_expr_for_file(expr, mappings, &resolved)) {
793
1.69k
        return false;
794
1.69k
    }
795
796
698
    std::vector<VExprSPtr> struct_element_chain;
797
698
    if (!collect_struct_element_chain(expr, &struct_element_chain) ||
798
698
        struct_element_chain.size() != resolved.file_child_names.size() ||
799
698
        struct_element_chain.size() != resolved.file_child_types.size()) {
800
0
        return false;
801
0
    }
802
803
698
    auto root_children = struct_element_chain.front()->children();
804
698
    if (!root_children[0]->is_slot_ref()) {
805
0
        return false;
806
0
    }
807
698
    const auto* slot_ref = assert_cast<const VSlotRef*>(root_children[0].get());
808
698
    const auto rewrite_it = global_to_file_slot.find(slot_ref_global_index(*slot_ref));
809
698
    if (rewrite_it == global_to_file_slot.end()) {
810
0
        return false;
811
0
    }
812
813
    // File-local conjuncts are prepared against the file-reader Block, so both the root slot and
814
    // every struct selector must be expressed in file schema terms. For a renamed Iceberg field,
815
    // keeping the table selector would prepare `element_at(file_struct<rename>, 'renamed')` and
816
    // fail before any rows are read. Rewrite the whole chain while ColumnMapping still preserves
817
    // the table-to-file relationship. Example:
818
    //   table filter: element_at(element_at(s, 'renamed_parent'), 'renamed_leaf')
819
    //   old file:     s<parent<leaf>>
820
    //   file filter:  element_at(element_at(s, 'parent'), 'leaf')
821
698
    root_children[0] = create_file_slot_ref(*slot_ref, rewrite_it->second, rewrite_context);
822
698
    struct_element_chain.front()->set_children(std::move(root_children));
823
1.49k
    for (size_t idx = 0; idx < struct_element_chain.size(); ++idx) {
824
799
        auto children = struct_element_chain[idx]->children();
825
799
        children[1] = create_file_struct_child_name_literal(resolved.file_child_names[idx],
826
799
                                                            rewrite_context);
827
799
        struct_element_chain[idx]->set_children(std::move(children));
828
        // The selector name and the expression return type must be moved to file schema together.
829
        // Example:
830
        //   table filter: element_at(element_at(s, 'new_a'), 'new_aa') = 50
831
        //   old file:     s.new_a STRUCT<aa, bb>
832
        //   file filter:  element_at(element_at(s, 'new_a'), 'aa') = 50
833
        //
834
        // If the inner element_at keeps the table return type STRUCT<new_aa, bb>, preparing the
835
        // outer element_at(..., 'aa') fails before scanning because `aa` is not a table field.
836
799
        struct_element_chain[idx]->data_type() = resolved.file_child_types[idx];
837
799
    }
838
698
    return true;
839
698
}
840
841
static VExprSPtr rewrite_table_expr_to_file_expr(
842
        const VExprSPtr& expr,
843
        const std::map<GlobalIndex, FileSlotRewriteInfo>& global_to_file_slot,
844
        const std::vector<ColumnMapping>& filter_mappings, RewriteContext* rewrite_context,
845
83.4k
        bool* can_localize) {
846
83.4k
    if (expr == nullptr) {
847
0
        return nullptr;
848
0
    }
849
83.4k
    DORIS_CHECK(rewrite_context != nullptr);
850
83.4k
    DORIS_CHECK(can_localize != nullptr);
851
83.4k
    if (auto* runtime_filter = dynamic_cast<RuntimeFilterExpr*>(expr.get());
852
83.4k
        runtime_filter != nullptr) {
853
4.09k
        auto impl = runtime_filter->get_impl();
854
4.09k
        if (impl == nullptr) {
855
0
            *can_localize = false;
856
0
            return expr;
857
0
        }
858
4.09k
        auto localized_impl = rewrite_table_expr_to_file_expr(
859
4.09k
                impl, global_to_file_slot, filter_mappings, rewrite_context, can_localize);
860
4.09k
        if (!*can_localize) {
861
0
            return expr;
862
0
        }
863
4.09k
        runtime_filter->set_impl(std::move(localized_impl));
864
4.09k
        return expr;
865
4.09k
    }
866
79.3k
    if (rewrite_binary_slot_literal_predicate(expr, global_to_file_slot, rewrite_context)) {
867
9.83k
        return expr;
868
9.83k
    }
869
69.4k
    if (rewrite_in_slot_literal_predicate(expr, global_to_file_slot, rewrite_context)) {
870
1.65k
        return expr;
871
1.65k
    }
872
67.8k
    if (is_struct_element_expr(expr)) {
873
2.39k
        if (!rewrite_struct_element_path_to_file_expr(expr, filter_mappings, global_to_file_slot,
874
2.39k
                                                      rewrite_context)) {
875
            // The scanner still evaluates the original table-level conjunct after TableReader
876
            // finalizes the output block. Skipping an unlocalizable file conjunct is therefore
877
            // safer than preparing a partially rewritten expression against the wrong struct
878
            // layout. In particular, do not generate file-local conjuncts for computed complex
879
            // parents such as `element_at(element_at(map_values(m), 1), 'field')`; only direct
880
            // slot-rooted struct chains are supported here.
881
1.69k
            *can_localize = false;
882
1.69k
        }
883
2.39k
        return expr;
884
2.39k
    }
885
65.4k
    if (expr->is_slot_ref()) {
886
22.0k
        const auto* slot_ref = assert_cast<const VSlotRef*>(expr.get());
887
22.0k
        const auto rewrite_it = global_to_file_slot.find(slot_ref_global_index(*slot_ref));
888
22.0k
        if (rewrite_it != global_to_file_slot.end()) {
889
22.0k
            const auto& rewrite_info = rewrite_it->second;
890
22.0k
            auto file_slot = create_file_slot_ref(*slot_ref, rewrite_info, rewrite_context);
891
22.0k
            if (rewrite_info.file_type->equals(*rewrite_info.table_type)) {
892
17.5k
                return file_slot;
893
17.5k
            }
894
4.48k
            if (needs_complex_file_slot_cast(rewrite_info.file_type, rewrite_info.table_type)) {
895
                // Generic file-local expressions cannot safely cast an evolved complex file slot
896
                // back to the table type. Example:
897
                //
898
                //   table filter: ARRAY_CONTAINS(MAP_KEYS(m), 'person5')
899
                //   old file:     m MAP<STRING, STRUCT<name, age>>
900
                //   table:        m MAP<STRING, STRUCT<age, full_name, gender>>
901
                //
902
                // Although MAP_KEYS only reads the key column, wrapping the file slot as
903
                // `CAST(file_m AS table_m)` forces the value struct cast first and fails because
904
                // the old and new value structs have different fields. Keep such filters at the
905
                // table level, where TableReader materializes the evolved complex value before
906
                // Scanner evaluates the original conjunct. Direct slot-rooted struct child paths
907
                // are handled by rewrite_struct_element_path_to_file_expr() above.
908
566
                *can_localize = false;
909
566
                return expr;
910
566
            }
911
3.91k
            auto cast_expr = Cast::create_shared(rewrite_info.table_type);
912
3.91k
            cast_expr->add_child(std::move(file_slot));
913
3.91k
            rewrite_context->add_created_expr(cast_expr);
914
3.91k
            return cast_expr;
915
4.48k
        }
916
18.4E
        return expr;
917
22.0k
    }
918
    // The input is a split-local cloned tree. A previous split-local clone may already have
919
    // inserted Cast(slot). Keep that rewrite idempotent: rewrite the cast child from table slot to
920
    // the current split's file slot, and drop the cast when the current split no longer needs it.
921
43.4k
    if (is_cast_expr(expr) && expr->get_num_children() == 1) {
922
1.66k
        const auto& child = expr->children()[0];
923
1.66k
        if (child->is_slot_ref()) {
924
1.60k
            const auto* slot_ref = assert_cast<const VSlotRef*>(child.get());
925
1.60k
            const auto rewrite_it = global_to_file_slot.find(slot_ref_global_index(*slot_ref));
926
1.60k
            if (rewrite_it != global_to_file_slot.end() &&
927
1.60k
                expr->data_type()->equals(*rewrite_it->second.table_type)) {
928
1
                auto rewritten_child =
929
1
                        create_file_slot_ref(*slot_ref, rewrite_it->second, rewrite_context);
930
1
                if (rewrite_it->second.file_type->equals(*rewrite_it->second.table_type)) {
931
0
                    return rewritten_child;
932
0
                }
933
1
                if (needs_complex_file_slot_cast(rewrite_it->second.file_type,
934
1
                                                 rewrite_it->second.table_type)) {
935
0
                    *can_localize = false;
936
0
                    return expr;
937
0
                }
938
1
                expr->set_children({std::move(rewritten_child)});
939
1
                return expr;
940
1
            }
941
1.60k
        }
942
1.66k
    }
943
944
43.4k
    VExprSPtrs rewritten_children;
945
43.4k
    rewritten_children.reserve(expr->children().size());
946
44.1k
    for (const auto& child : expr->children()) {
947
44.1k
        rewritten_children.push_back(rewrite_table_expr_to_file_expr(
948
44.1k
                child, global_to_file_slot, filter_mappings, rewrite_context, can_localize));
949
44.1k
    }
950
43.4k
    expr->set_children(std::move(rewritten_children));
951
43.4k
    return expr;
952
43.4k
}
953
954
static constexpr const char* ROW_LINEAGE_ROW_ID = "_row_id";
955
static constexpr const char* ROW_LINEAGE_LAST_UPDATED_SEQ_NUMBER = "_last_updated_sequence_number";
956
static constexpr int32_t ROW_LINEAGE_ROW_ID_FIELD_ID = 2147483540;
957
static constexpr int32_t ROW_LINEAGE_LAST_UPDATED_SEQ_NUMBER_FIELD_ID = 2147483539;
958
959
315k
static TableVirtualColumnType row_lineage_virtual_column_type(const std::string& column_name) {
960
315k
    if (column_name == ROW_LINEAGE_ROW_ID) {
961
2
        return TableVirtualColumnType::ROW_ID;
962
2
    }
963
315k
    if (column_name == ROW_LINEAGE_LAST_UPDATED_SEQ_NUMBER) {
964
2
        return TableVirtualColumnType::LAST_UPDATED_SEQUENCE_NUMBER;
965
2
    }
966
315k
    return TableVirtualColumnType::INVALID;
967
315k
}
968
969
static TableVirtualColumnType row_lineage_virtual_column_type_by_field_id(
970
87.8k
        const ColumnDefinition& column) {
971
87.8k
    if (!column.has_identifier_field_id()) {
972
545
        return TableVirtualColumnType::INVALID;
973
545
    }
974
87.2k
    switch (column.get_identifier_field_id()) {
975
480
    case ROW_LINEAGE_ROW_ID_FIELD_ID:
976
480
        return TableVirtualColumnType::ROW_ID;
977
395
    case ROW_LINEAGE_LAST_UPDATED_SEQ_NUMBER_FIELD_ID:
978
395
        return TableVirtualColumnType::LAST_UPDATED_SEQUENCE_NUMBER;
979
86.2k
    default:
980
86.2k
        return TableVirtualColumnType::INVALID;
981
87.2k
    }
982
87.2k
}
983
984
static TableVirtualColumnType row_lineage_virtual_column_type(const ColumnDefinition& column,
985
403k
                                                              TableColumnMappingMode mode) {
986
403k
    switch (mode) {
987
87.8k
    case TableColumnMappingMode::BY_FIELD_ID:
988
87.8k
        return row_lineage_virtual_column_type_by_field_id(column);
989
270k
    case TableColumnMappingMode::BY_NAME:
990
315k
    case TableColumnMappingMode::BY_INDEX:
991
315k
        return row_lineage_virtual_column_type(column.name);
992
403k
    }
993
0
    return TableVirtualColumnType::INVALID;
994
403k
}
995
996
// Returns true when the current file type is not the exact nested type the scan should expose.
997
// This is about building the projected file-side type/projection, not about whether TableReader
998
// later needs to rematerialize the complex value back to table layout.
999
511k
static bool needs_projected_file_type_rebuild(const ColumnMapping& mapping) {
1000
511k
    if (!is_complex_type(mapping.file_type->get_primitive_type())) {
1001
286k
        return false;
1002
286k
    }
1003
224k
    if (mapping.child_mappings.empty()) {
1004
0
        return false;
1005
0
    }
1006
224k
    DORIS_CHECK(mapping.file_type != nullptr);
1007
224k
    DORIS_CHECK(mapping.table_type != nullptr);
1008
224k
    if (remove_nullable(mapping.file_type)->get_primitive_type() !=
1009
224k
        remove_nullable(mapping.table_type)->get_primitive_type()) {
1010
0
        return true;
1011
0
    }
1012
224k
    if (!mapping.table_type->equals(*mapping.file_type)) {
1013
16.6k
        return true;
1014
16.6k
    }
1015
315k
    for (const auto& child_mapping : mapping.child_mappings) {
1016
        // Rename-only child mappings do not change the file-side projected shape. If field-id
1017
        // matching maps table child `renamed_b` to file child `b`, the file reader can still expose
1018
        // the original file type as long as child count/order/types are unchanged.
1019
315k
        if (!child_mapping.file_local_id.has_value() ||
1020
315k
            needs_projected_file_type_rebuild(child_mapping)) {
1021
661
            return true;
1022
661
        }
1023
315k
    }
1024
207k
    return false;
1025
208k
}
1026
1027
static std::optional<size_t> file_child_ordinal_in_scan_type(const ColumnMapping& mapping,
1028
287k
                                                             const ColumnMapping& child_mapping) {
1029
287k
    if (!child_mapping.file_local_id.has_value()) {
1030
241
        return std::nullopt;
1031
241
    }
1032
287k
    const auto& file_children = !mapping.projected_file_children.empty()
1033
287k
                                        ? mapping.projected_file_children
1034
18.4E
                                        : mapping.original_file_children;
1035
402k
    const auto child_it = std::ranges::find_if(file_children, [&](const ColumnDefinition& child) {
1036
402k
        return child.file_local_id() == *child_mapping.file_local_id;
1037
402k
    });
1038
287k
    if (child_it == file_children.end()) {
1039
0
        return std::nullopt;
1040
0
    }
1041
287k
    return static_cast<size_t>(std::distance(file_children.begin(), child_it));
1042
287k
}
1043
1044
1.11M
static bool needs_complex_rematerialize(const ColumnMapping& mapping) {
1045
1.11M
    if (mapping.child_mappings.empty()) {
1046
919k
        return false;
1047
919k
    }
1048
200k
    if (mapping.table_type == nullptr || mapping.file_type == nullptr ||
1049
200k
        !mapping.table_type->equals(*mapping.file_type)) {
1050
10.1k
        return true;
1051
10.1k
    }
1052
475k
    for (size_t table_child_idx = 0; table_child_idx < mapping.child_mappings.size();
1053
287k
         ++table_child_idx) {
1054
287k
        const auto& child_mapping = mapping.child_mappings[table_child_idx];
1055
287k
        const auto file_child_idx = file_child_ordinal_in_scan_type(mapping, child_mapping);
1056
287k
        if (!file_child_idx.has_value() || *file_child_idx != table_child_idx ||
1057
287k
            needs_complex_rematerialize(child_mapping) ||
1058
287k
            (child_mapping.table_type != nullptr && child_mapping.file_type != nullptr &&
1059
285k
             !child_mapping.table_type->equals(*child_mapping.file_type))) {
1060
1.96k
            return true;
1061
1.96k
        }
1062
287k
    }
1063
188k
    return false;
1064
190k
}
1065
1066
870k
static bool mapping_can_use_file_column_directly(const ColumnMapping& mapping) {
1067
870k
    if (mapping.table_type == nullptr || mapping.file_type == nullptr) {
1068
0
        return false;
1069
0
    }
1070
870k
    const auto table_type = remove_nullable(mapping.table_type);
1071
870k
    const auto file_type = remove_nullable(mapping.file_type);
1072
870k
    const bool same_timestamptz_with_different_scale =
1073
870k
            table_type->get_primitive_type() == TYPE_TIMESTAMPTZ &&
1074
870k
            file_type->get_primitive_type() == TYPE_TIMESTAMPTZ;
1075
870k
    if (!mapping.table_type->equals(*mapping.file_type) && !same_timestamptz_with_different_scale) {
1076
61.7k
        return false;
1077
61.7k
    }
1078
808k
    return !needs_complex_rematerialize(mapping);
1079
870k
}
1080
1081
1.08M
static bool type_contains_varbinary(const DataTypePtr& type) {
1082
1.08M
    DORIS_CHECK(type != nullptr);
1083
1.08M
    const auto nested_type = remove_nullable(type);
1084
1.08M
    switch (nested_type->get_primitive_type()) {
1085
617
    case TYPE_VARBINARY:
1086
617
        return true;
1087
103k
    case TYPE_ARRAY:
1088
103k
        return type_contains_varbinary(
1089
103k
                assert_cast<const DataTypeArray&>(*nested_type).get_nested_type());
1090
74.8k
    case TYPE_MAP: {
1091
74.8k
        const auto& map_type = assert_cast<const DataTypeMap&>(*nested_type);
1092
74.8k
        return type_contains_varbinary(map_type.get_key_type()) ||
1093
74.8k
               type_contains_varbinary(map_type.get_value_type());
1094
0
    }
1095
72.8k
    case TYPE_STRUCT:
1096
72.8k
        return std::ranges::any_of(
1097
72.8k
                assert_cast<const DataTypeStruct&>(*nested_type).get_elements(),
1098
138k
                [](const DataTypePtr& child_type) { return type_contains_varbinary(child_type); });
1099
840k
    default:
1100
840k
        return false;
1101
1.08M
    }
1102
1.08M
}
1103
1104
699k
static FilterConversionType direct_filter_conversion(const ColumnMapping& mapping) {
1105
699k
    DORIS_CHECK(mapping.table_type != nullptr);
1106
699k
    DORIS_CHECK(mapping.file_type != nullptr);
1107
    // FileScanOperator deliberately keeps VARBINARY predicates above external readers. Their
1108
    // physical binary representations are not uniformly supported by reader-side expression and
1109
    // metadata filtering, so localizing a late runtime filter here can incorrectly reject rows.
1110
    // Apply the same rule to a complex root because generic array/map/struct expressions rewrite
1111
    // the root slot and can otherwise expose a nested VARBINARY child to the reader.
1112
699k
    if (type_contains_varbinary(mapping.table_type)) {
1113
617
        return FilterConversionType::FINALIZE_ONLY;
1114
617
    }
1115
699k
    const auto table_type = remove_nullable(mapping.table_type);
1116
699k
    const auto file_type = remove_nullable(mapping.file_type);
1117
    // TIMESTAMPTZ scale mismatch is intentionally materialized as pass-through: a SQL cast rounds
1118
    // fractional seconds. A file-local cast would therefore filter different instants from the
1119
    // scanner-level predicate evaluated on the pass-through value.
1120
699k
    if (table_type->get_primitive_type() == TYPE_TIMESTAMPTZ &&
1121
699k
        file_type->get_primitive_type() == TYPE_TIMESTAMPTZ &&
1122
699k
        !mapping.table_type->equals(*mapping.file_type)) {
1123
1
        return FilterConversionType::FINALIZE_ONLY;
1124
1
    }
1125
699k
    return mapping.is_trivial ? FilterConversionType::COPY_DIRECTLY
1126
699k
                              : FilterConversionType::CAST_FILTER;
1127
699k
}
1128
1129
16.9k
static FilterConversionType projected_filter_conversion(const ColumnMapping& mapping) {
1130
16.9k
    const auto conversion = direct_filter_conversion(mapping);
1131
16.9k
    return !mapping.is_trivial && conversion != FilterConversionType::FINALIZE_ONLY
1132
16.9k
                   ? FilterConversionType::READER_EXPRESSION
1133
16.9k
                   : conversion;
1134
16.9k
}
1135
1136
static const ColumnDefinition* find_file_child_for_mapping(const ColumnDefinition& table_child,
1137
                                                           const ColumnDefinition& file_parent,
1138
                                                           TableColumnMappingMode mode,
1139
                                                           size_t table_child_idx,
1140
304k
                                                           bool allow_ordinal_fallback) {
1141
304k
    const auto file_parent_type = remove_nullable(file_parent.type)->get_primitive_type();
1142
304k
    switch (file_parent_type) {
1143
87.0k
    case TYPE_ARRAY:
1144
87.0k
        DORIS_CHECK(file_parent.children.size() == 1);
1145
87.0k
        return &file_parent.children[0];
1146
137k
    case TYPE_MAP:
1147
137k
        DORIS_CHECK(file_parent.children.size() == 2);
1148
137k
        if (table_child.name == "key") {
1149
68.7k
            return &file_parent.children[0];
1150
68.7k
        }
1151
68.7k
        if (table_child.name == "value") {
1152
68.7k
            return &file_parent.children[1];
1153
68.7k
        }
1154
4
        if (table_child.local_id == 0 || table_child.local_id == 1) {
1155
0
            return &file_parent.children[table_child.local_id];
1156
0
        }
1157
4
        return nullptr;
1158
80.1k
    default:
1159
        // Hive BY_INDEX is a top-level column matching rule. Once a complex root is selected by
1160
        // file position, nested struct children follow Hive reader's historical name matching
1161
        // semantics; their integer identifiers can be field ids, not file positions.
1162
80.1k
        const auto nested_mode =
1163
80.1k
                mode == TableColumnMappingMode::BY_INDEX ? TableColumnMappingMode::BY_NAME : mode;
1164
80.1k
        if (const auto* file_child =
1165
80.1k
                    matcher_for_mode(nested_mode).find(table_child, file_parent.children);
1166
80.1k
            file_child != nullptr) {
1167
75.6k
            return file_child;
1168
75.6k
        }
1169
4.48k
        if (allow_ordinal_fallback && mode == TableColumnMappingMode::BY_FIELD_ID &&
1170
4.48k
            !table_child.has_identifier_field_id()) {
1171
            // Synthetic children are derived from the table DataType when nested ColumnDefinition
1172
            // metadata has been pruned away. They do not carry Iceberg field ids, so try a name
1173
            // match before falling back to ordinal order. Example:
1174
            //   table value type: Struct(age, full_name, gender)
1175
            //   old file value:   Struct(name, age)
1176
            // Name matching keeps `age -> age`; the later unused-child fallback can then map the
1177
            // renamed `full_name -> name` instead of consuming `age` twice.
1178
3
            if (const auto* file_child = NameMatcher().find(table_child, file_parent.children);
1179
3
                file_child != nullptr) {
1180
1
                return file_child;
1181
1
            }
1182
3
        }
1183
        // Some callers only carry the full complex DataType for a projected table column, without
1184
        // expanded nested ColumnDefinitions. In that case we can still preserve full materialization
1185
        // by walking table/file struct fields by ordinal. This is a fallback only: explicit
1186
        // ColumnDefinition children keep using the requested table-format matching rule, which is
1187
        // required for precise schema evolution.
1188
4.48k
        if (allow_ordinal_fallback && table_child_idx < file_parent.children.size()) {
1189
3
            return &file_parent.children[table_child_idx];
1190
3
        }
1191
4.48k
        return nullptr;
1192
304k
    }
1193
304k
}
1194
1195
static ColumnDefinition synthetic_child_definition(const std::string& name, DataTypePtr type,
1196
13
                                                   int32_t local_id) {
1197
13
    ColumnDefinition child;
1198
13
    child.identifier = Field::create_field<TYPE_STRING>(name);
1199
13
    child.local_id = local_id;
1200
13
    child.name = name;
1201
13
    child.type = std::move(type);
1202
13
    return child;
1203
13
}
1204
1205
static std::vector<ColumnDefinition> synthesize_complex_children_from_type(
1206
4
        const DataTypePtr& type) {
1207
4
    std::vector<ColumnDefinition> children;
1208
4
    if (type == nullptr) {
1209
0
        return children;
1210
0
    }
1211
4
    const auto nested_type = remove_nullable(type);
1212
4
    switch (nested_type->get_primitive_type()) {
1213
0
    case TYPE_ARRAY: {
1214
0
        const auto* array_type = assert_cast<const DataTypeArray*>(nested_type.get());
1215
0
        children.push_back(synthetic_child_definition("element", array_type->get_nested_type(), 0));
1216
0
        break;
1217
0
    }
1218
1
    case TYPE_MAP: {
1219
1
        const auto* map_type = assert_cast<const DataTypeMap*>(nested_type.get());
1220
1
        children.push_back(synthetic_child_definition("key", map_type->get_key_type(), 0));
1221
1
        children.push_back(synthetic_child_definition("value", map_type->get_value_type(), 1));
1222
1
        break;
1223
0
    }
1224
3
    case TYPE_STRUCT: {
1225
3
        const auto* struct_type = assert_cast<const DataTypeStruct*>(nested_type.get());
1226
3
        children.reserve(struct_type->get_elements().size());
1227
12
        for (size_t idx = 0; idx < struct_type->get_elements().size(); ++idx) {
1228
9
            children.push_back(synthetic_child_definition(struct_type->get_element_name(idx),
1229
9
                                                          struct_type->get_element(idx),
1230
9
                                                          cast_set<int32_t>(idx)));
1231
9
        }
1232
3
        break;
1233
0
    }
1234
0
    default:
1235
0
        break;
1236
4
    }
1237
4
    return children;
1238
4
}
1239
1240
static bool has_table_child_named(const std::vector<ColumnDefinition>& children,
1241
8.97k
                                  std::string_view name) {
1242
13.4k
    return std::ranges::any_of(children, [&](const ColumnDefinition& child) {
1243
13.4k
        return std::string_view(child.name) == name;
1244
13.4k
    });
1245
8.97k
}
1246
1247
static void complete_required_complex_children_from_type(const DataTypePtr& type,
1248
16.3k
                                                         std::vector<ColumnDefinition>* children) {
1249
16.3k
    DORIS_CHECK(children != nullptr);
1250
16.3k
    if (type == nullptr) {
1251
0
        return;
1252
0
    }
1253
16.3k
    const auto nested_type = remove_nullable(type);
1254
16.3k
    switch (nested_type->get_primitive_type()) {
1255
4.49k
    case TYPE_MAP: {
1256
4.49k
        const auto* map_type = assert_cast<const DataTypeMap*>(nested_type.get());
1257
        // MAP key/value are structural children, not independently materializable table fields.
1258
        // A key-only projection can still be attached to a whole-map output root, for example:
1259
        //   SELECT * FROM t WHERE ARRAY_CONTAINS(MAP_KEYS(new_map_column), 'person5')
1260
        //
1261
        // In that shape the scanner keeps the value stream readable, but the table projection can
1262
        // carry only the key child. Add the missing value child so recursive mapping can evolve the
1263
        // value type instead of letting TableReader cast old/new value structs directly.
1264
4.49k
        if (has_table_child_named(*children, "key") && !has_table_child_named(*children, "value")) {
1265
2
            children->push_back(synthetic_child_definition("value", map_type->get_value_type(), 1));
1266
2
        }
1267
4.49k
        break;
1268
0
    }
1269
4.99k
    case TYPE_ARRAY:
1270
        // ARRAY has only one required structural child (`element`), so a non-empty projection is
1271
        // already rooted at the element path.
1272
4.99k
        break;
1273
6.89k
    case TYPE_STRUCT:
1274
        // STRUCT children are real fields and must remain prunable. Completing missing struct
1275
        // fields here would turn `SELECT s.a` into a full-struct read and undo nested projection.
1276
6.89k
        break;
1277
0
    default:
1278
0
        break;
1279
16.3k
    }
1280
16.3k
}
1281
1282
197k
static Status validate_file_schema_children(const ColumnDefinition& file_field) {
1283
197k
    if (file_field.type == nullptr) {
1284
0
        return Status::InternalError("File column '{}' has null type", file_field.name);
1285
0
    }
1286
197k
    const auto nested_type = remove_nullable(file_field.type);
1287
197k
    size_t expected_children = 0;
1288
197k
    bool complex_with_fixed_children = true;
1289
197k
    switch (nested_type->get_primitive_type()) {
1290
87.0k
    case TYPE_ARRAY:
1291
87.0k
        expected_children = 1;
1292
87.0k
        break;
1293
68.7k
    case TYPE_MAP:
1294
68.7k
        expected_children = 2;
1295
68.7k
        break;
1296
41.4k
    case TYPE_STRUCT:
1297
41.4k
        expected_children =
1298
41.4k
                assert_cast<const DataTypeStruct*>(nested_type.get())->get_elements().size();
1299
41.4k
        break;
1300
0
    default:
1301
0
        complex_with_fixed_children = false;
1302
0
        break;
1303
197k
    }
1304
197k
    if (!complex_with_fixed_children || file_field.children.size() == expected_children) {
1305
197k
        return Status::OK();
1306
197k
    }
1307
21
    return Status::InternalError(
1308
21
            "Malformed complex file schema for column '{}': type={}, expected_children={}, "
1309
21
            "actual_children={}",
1310
21
            file_field.name, file_field.type->get_name(), expected_children,
1311
21
            file_field.children.size());
1312
197k
}
1313
1314
290k
static bool has_projected_file_children(const ColumnMapping& mapping) {
1315
290k
    if (mapping.original_file_children.empty() || mapping.projected_file_children.empty()) {
1316
225k
        return false;
1317
225k
    }
1318
65.8k
    if (mapping.original_file_children.size() != mapping.projected_file_children.size()) {
1319
4.79k
        return true;
1320
4.79k
    }
1321
160k
    for (size_t idx = 0; idx < mapping.original_file_children.size(); ++idx) {
1322
99.1k
        if (mapping.original_file_children[idx].file_local_id() !=
1323
99.1k
            mapping.projected_file_children[idx].file_local_id()) {
1324
0
            return true;
1325
0
        }
1326
99.1k
    }
1327
61.0k
    return false;
1328
61.0k
}
1329
1330
290k
static bool needs_nested_file_projection(const ColumnMapping& mapping) {
1331
290k
    if (has_projected_file_children(mapping)) {
1332
        // Return True if the projected child column is missing / re-ordered
1333
4.79k
        return true;
1334
4.79k
    }
1335
286k
    return std::ranges::any_of(mapping.child_mappings, [](const ColumnMapping& child_mapping) {
1336
100k
        return needs_nested_file_projection(child_mapping);
1337
100k
    });
1338
290k
}
1339
1340
static Status build_complex_projection(const ColumnMapping& mapping, LocalColumnIndex* projection);
1341
1342
// Build the projected file children/type according to the pruned complex projection. For example,
1343
// if we have a struct column `s` with children `id` and `name`, and the projection only keeps
1344
// `s.name`, then the file reader should expose `STRUCT<name ...>`.
1345
static Status rebuild_projected_file_children_and_type(
1346
        const DataTypePtr& file_type, const std::vector<ColumnDefinition>& original_file_children,
1347
        const std::vector<ColumnMapping>& child_mappings,
1348
16.9k
        std::vector<ColumnDefinition>* projected_file_children, DataTypePtr* projected_type) {
1349
16.9k
    DORIS_CHECK(file_type != nullptr);
1350
16.9k
    DORIS_CHECK(projected_file_children != nullptr);
1351
16.9k
    DORIS_CHECK(projected_type != nullptr);
1352
16.9k
    ColumnDefinition field;
1353
16.9k
    field.type = file_type;
1354
16.9k
    field.children = original_file_children;
1355
16.9k
    LocalColumnIndex projection = LocalColumnIndex::partial_local(-1);
1356
16.9k
    projection.children.reserve(child_mappings.size());
1357
24.5k
    for (const auto* child_mapping : present_child_mappings_in_file_order(child_mappings)) {
1358
24.5k
        DORIS_CHECK(child_mapping->file_local_id.has_value());
1359
24.5k
        LocalColumnIndex child_projection;
1360
24.5k
        RETURN_IF_ERROR(build_complex_projection(*child_mapping, &child_projection));
1361
24.5k
        projection.children.push_back(std::move(child_projection));
1362
24.5k
    }
1363
1364
16.9k
    ColumnDefinition projected_field;
1365
16.9k
    RETURN_IF_ERROR(project_column_definition(field, projection, &projected_field));
1366
16.9k
    *projected_file_children = std::move(projected_field.children);
1367
16.9k
    *projected_type = std::move(projected_field.type);
1368
16.9k
    return Status::OK();
1369
16.9k
}
1370
1371
// Build the complex column projection according to the ColumnMapping which is re-ordered by the
1372
// file-schema's order.
1373
//
1374
// For MAP, a partial projection represents value-subtree pruning only. The key child is not a
1375
// projected output shape; file readers still read full keys to construct ColumnMap offsets and keep
1376
// key semantics unchanged. If a caller tries to project only/prune the key child, the common schema
1377
// projection helper rejects it.
1378
45.4k
static Status build_complex_projection(const ColumnMapping& mapping, LocalColumnIndex* projection) {
1379
45.4k
    if (projection == nullptr) {
1380
0
        return Status::InvalidArgument("projection is null");
1381
0
    }
1382
45.4k
    DORIS_CHECK(mapping.file_local_id.has_value());
1383
45.4k
    *projection = LocalColumnIndex::local(*mapping.file_local_id);
1384
45.4k
    projection->project_all_children = mapping.child_mappings.empty();
1385
45.4k
    projection->children.clear();
1386
45.4k
    const auto present_children = present_child_mappings_in_file_order(mapping.child_mappings);
1387
45.4k
    if (!projection->project_all_children && present_children.empty()) {
1388
        // All requested table children under this complex node are missing/default-only. The file
1389
        // reader cannot expose an empty complex projection, but TableReader can still rematerialize
1390
        // the table shape from a full file subtree and fill the missing children with defaults.
1391
236
        projection->project_all_children = true;
1392
236
        return Status::OK();
1393
236
    }
1394
45.2k
    for (const auto* child_mapping : present_children) {
1395
16.0k
        LocalColumnIndex child_projection;
1396
16.0k
        RETURN_IF_ERROR(build_complex_projection(*child_mapping, &child_projection));
1397
16.0k
        projection->children.push_back(std::move(child_projection));
1398
16.0k
    }
1399
45.2k
    if (!projection->project_all_children && projection->children.empty()) {
1400
0
        return Status::NotSupported("Projection for complex column {} contains no file children",
1401
0
                                    mapping.file_column_name);
1402
0
    }
1403
45.2k
    return Status::OK();
1404
45.2k
}
1405
1406
using FilterProjectionMap = std::map<LocalColumnId, LocalColumnIndex>;
1407
1408
// Update the mapping's file type according to the projection, and determine whether the projection
1409
// is trivial (i.e. the projected file type is the same as the table type, so no need to
1410
// rematerialize the complex value back to table layout after reading from file).
1411
static Status apply_projection_to_mapping_file_type(const LocalColumnIndex& projection,
1412
383k
                                                    ColumnMapping* mapping) {
1413
383k
    DORIS_CHECK(mapping != nullptr);
1414
383k
    if (mapping->original_file_type == nullptr) {
1415
0
        mapping->original_file_type = mapping->file_type;
1416
0
    }
1417
383k
    if (mapping->original_file_type == nullptr ||
1418
383k
        !is_complex_type(remove_nullable(mapping->original_file_type)->get_primitive_type())) {
1419
213k
        return Status::OK();
1420
213k
    }
1421
170k
    ColumnDefinition field;
1422
170k
    field.type = mapping->original_file_type;
1423
170k
    field.children = mapping->original_file_children;
1424
170k
    ColumnDefinition projected_field;
1425
170k
    RETURN_IF_ERROR(project_column_definition(field, projection, &projected_field));
1426
170k
    mapping->file_type = std::move(projected_field.type);
1427
170k
    mapping->projected_file_children = std::move(projected_field.children);
1428
170k
    mapping->is_trivial = mapping_can_use_file_column_directly(*mapping);
1429
170k
    return Status::OK();
1430
170k
}
1431
1432
static Status merge_filter_projection(const FilterProjectionMap* filter_projections,
1433
35.6k
                                      LocalColumnIndex* projection) {
1434
35.6k
    DORIS_CHECK(projection != nullptr);
1435
35.6k
    if (filter_projections == nullptr) {
1436
0
        return Status::OK();
1437
0
    }
1438
35.6k
    const auto filter_projection_it = filter_projections->find(projection->column_id());
1439
35.6k
    if (filter_projection_it == filter_projections->end()) {
1440
34.9k
        return Status::OK();
1441
34.9k
    }
1442
    // Merge predicate-only nested paths into the root projection that is about to be scanned.
1443
    // Example: `SELECT s.a WHERE s.b > 1` first builds the output projection `s -> a` from
1444
    // ColumnMapping, while build_nested_struct_filter_projection_map() records `s -> b`. This merge
1445
    // produces one file scan projection `s -> a,b`.
1446
711
    RETURN_IF_ERROR(merge_local_column_index(projection, filter_projection_it->second));
1447
711
    return Status::OK();
1448
711
}
1449
1450
86
static bool table_root_is_map(const ColumnMapping& mapping) {
1451
86
    if (mapping.table_type == nullptr) {
1452
0
        return false;
1453
0
    }
1454
86
    return remove_nullable(mapping.table_type)->get_primitive_type() == TYPE_MAP;
1455
86
}
1456
1457
static Status add_scan_column(FileScanRequest* file_request, ColumnMapping* mapping,
1458
                              bool is_predicate_column, bool force_full_complex_scan_projection,
1459
391k
                              const FilterProjectionMap* filter_projections = nullptr) {
1460
391k
    const auto file_column_id = LocalColumnId(mapping->file_local_id.value());
1461
391k
    LocalColumnIndex projection = LocalColumnIndex::top_level(file_column_id);
1462
    // Columnar readers can turn a complex mapping into a nested file projection, but
1463
    // row-oriented readers must scan the full top-level complex field because all children are
1464
    // encoded in the same text cell.
1465
391k
    if (!force_full_complex_scan_projection && needs_nested_file_projection(*mapping)) {
1466
4.79k
        RETURN_IF_ERROR(build_complex_projection(*mapping, &projection));
1467
4.79k
    }
1468
391k
    if (is_predicate_column && !force_full_complex_scan_projection) {
1469
35.7k
        DCHECK(filter_projections != nullptr);
1470
        // If a projected complex root is also used by a predicate, rebuild the predicate scan
1471
        // projection from the output mapping before merging predicate-only children. For
1472
        // `SELECT s.a WHERE s.b > 1`, build_complex_projection() produces `s -> a` and
1473
        // merge_filter_projection() adds `s -> b`, so the predicate column reads both children.
1474
35.7k
        RETURN_IF_ERROR(merge_filter_projection(filter_projections, &projection));
1475
35.7k
    }
1476
391k
    FileScanRequestBuilder builder(file_request);
1477
391k
    if (is_predicate_column) {
1478
35.6k
        return builder.add_predicate_column(std::move(projection));
1479
35.6k
    }
1480
355k
    return builder.add_non_predicate_column(std::move(projection));
1481
391k
}
1482
1483
static const LocalColumnIndex* find_scan_projection(
1484
738k
        const std::vector<LocalColumnIndex>& scan_columns, LocalColumnId file_column_id) {
1485
738k
    const auto projection_it =
1486
8.07M
            std::ranges::find_if(scan_columns, [&](const LocalColumnIndex& projection) {
1487
8.07M
                return projection.column_id() == file_column_id;
1488
8.07M
            });
1489
738k
    return projection_it == scan_columns.end() ? nullptr : &*projection_it;
1490
738k
}
1491
1492
// Apply the final scan projection of one root file column back to its ColumnMapping. This updates
1493
// mapping.file_type/projected_file_children from the original file schema to the exact shape that
1494
// FileReader will return.
1495
//
1496
// Example: for `SELECT s.a WHERE s.b > 1`, add_scan_column() keeps only one predicate scan
1497
// projection `s -> a,b`. Applying that projection changes the mapping's file type from the full
1498
// file struct `s<a,b,c>` to the projected file struct `s<a,b>`, so later filter rewrite and
1499
// TableReader final materialization use the same column shape as the file-local block.
1500
static Status apply_scan_projection_to_mapping_file_type(const FileScanRequest& file_request,
1501
383k
                                                         ColumnMapping* mapping) {
1502
383k
    DORIS_CHECK(mapping != nullptr);
1503
383k
    DORIS_CHECK(mapping->file_local_id.has_value());
1504
383k
    const auto file_column_id = LocalColumnId(*mapping->file_local_id);
1505
    // Predicate columns are the actual scan projection when a column is used by row-level filters:
1506
    // add_scan_column() removes the duplicate non-predicate projection in that case.
1507
383k
    const auto* projection = find_scan_projection(file_request.predicate_columns, file_column_id);
1508
383k
    if (projection == nullptr) {
1509
354k
        projection = find_scan_projection(file_request.non_predicate_columns, file_column_id);
1510
354k
    }
1511
383k
    DORIS_CHECK(projection != nullptr);
1512
383k
    return apply_projection_to_mapping_file_type(*projection, mapping);
1513
383k
}
1514
1515
// Build extra scan projections required only by row-level filters on nested struct children.
1516
//
1517
// Example: for `SELECT s.a FROM t WHERE s.b.c > 1`, the output projection may only contain `s.a`,
1518
// but the file reader must also read `s.b.c` to evaluate the predicate. This function collects the
1519
// table-side filter path, resolves it through ColumnMapping first, and records the corresponding
1520
// file-side projection in filter_projections. This keeps renamed fields consistent between the scan
1521
// projection and row-level conjunct rewrite. Example:
1522
//   table filter path: s -> renamed_b -> c
1523
//   old file path:     s -> b -> c
1524
//   recorded path:     s -> b -> c
1525
// When add_scan_column() adds the same root as a predicate column, it rebuilds that root from the
1526
// output mapping, merges this filter-only projection into it, and removes the duplicate
1527
// non-predicate root entry.
1528
static Status build_nested_struct_filter_projection_map(
1529
        const std::vector<TableFilter>& table_filters, const std::vector<ColumnMapping>& mappings,
1530
60.3k
        FilterProjectionMap* filter_projections) {
1531
60.3k
    DORIS_CHECK(filter_projections != nullptr);
1532
60.3k
    filter_projections->clear();
1533
60.3k
    for (const auto& table_filter : table_filters) {
1534
40.0k
        if (table_filter.conjunct == nullptr) {
1535
2
            continue;
1536
2
        }
1537
        // Collect all nested struct paths in the table filter. For example, for
1538
        // `s.id > 5 AND element_at(s, 'renamed_name') = 'abc'`, collect the table paths
1539
        // `s -> id` and `s -> renamed_name`, then resolve each one to its file-side projection.
1540
40.0k
        std::vector<NestedStructPath> paths;
1541
40.0k
        collect_nested_struct_paths(table_filter.conjunct->root(), &paths);
1542
40.0k
        for (const auto& path : paths) {
1543
3.48k
            auto mapping_it = std::ranges::find_if(mappings, [&](const ColumnMapping& mapping) {
1544
3.48k
                return mapping.global_index == path.root_global_index;
1545
3.48k
            });
1546
1.16k
            if (mapping_it == mappings.end() || !mapping_it->file_local_id.has_value() ||
1547
1.16k
                path.selectors.empty()) {
1548
384
                continue;
1549
384
            }
1550
1551
784
            ResolvedNestedStructPath resolved;
1552
784
            LocalColumnIndex root_projection;
1553
784
            if (!resolve_nested_struct_path_for_file(path, mappings, &resolved)) {
1554
86
                if (!table_root_is_map(*mapping_it)) {
1555
85
                    continue;
1556
85
                }
1557
                // Direct map value filters such as `m.value.a > 1` need the value leaf for row
1558
                // evaluation even when the query only projects another value child. This is only a
1559
                // scan projection fallback; complex map/array expressions are still not rewritten
1560
                // into file-local conjuncts.
1561
1
                LocalColumnIndex child_projection;
1562
1
                RETURN_IF_ERROR(build_file_child_projection_from_schema(
1563
1
                        mapping_it->original_file_children, path.selectors, &child_projection));
1564
1
                if (child_projection.local_id() < 0) {
1565
0
                    continue;
1566
0
                }
1567
1
                root_projection = LocalColumnIndex::partial_local(*mapping_it->file_local_id);
1568
1
                root_projection.children.push_back(std::move(child_projection));
1569
698
            } else {
1570
698
                root_projection = std::move(resolved.file_projection);
1571
698
            }
1572
699
            auto filter_projection_it = filter_projections->find(root_projection.column_id());
1573
699
            if (filter_projection_it == filter_projections->end()) {
1574
631
                filter_projections->emplace(root_projection.column_id(),
1575
631
                                            std::move(root_projection));
1576
631
                continue;
1577
631
            }
1578
68
            RETURN_IF_ERROR(
1579
68
                    merge_local_column_index(&filter_projection_it->second, root_projection));
1580
68
        }
1581
40.0k
    }
1582
60.3k
    return Status::OK();
1583
60.3k
}
1584
1585
383k
static void rebuild_projection(ColumnMapping* mapping, LocalIndex block_position) {
1586
383k
    DORIS_CHECK(mapping->file_local_id.has_value());
1587
383k
    if (mapping->is_trivial || needs_complex_rematerialize(*mapping)) {
1588
370k
        mapping->projection = VExprContext::create_shared(VSlotRef::create_shared(
1589
370k
                cast_set<int>(block_position.value()), cast_set<int>(block_position.value()), -1,
1590
370k
                mapping->file_type, mapping->file_column_name));
1591
370k
        return;
1592
370k
    }
1593
1594
13.4k
    auto expr = Cast::create_shared(mapping->table_type);
1595
13.4k
    expr->add_child(VSlotRef::create_shared(cast_set<int>(block_position.value()),
1596
13.4k
                                            cast_set<int>(block_position.value()), -1,
1597
13.4k
                                            mapping->file_type, mapping->file_column_name));
1598
13.4k
    mapping->projection = VExprContext::create_shared(expr);
1599
13.4k
}
1600
1601
// Build file slot rewrite info from the localized filter targets. Only local targets can enter
1602
// file-reader expressions; constant and unset targets stay above the file reader.
1603
static std::map<GlobalIndex, FileSlotRewriteInfo> build_file_slot_rewrite_map(
1604
        const std::vector<ColumnMapping>& mappings,
1605
60.2k
        const std::map<GlobalIndex, FilterEntry>& filter_entries) {
1606
60.2k
    std::map<GlobalIndex, FileSlotRewriteInfo> global_to_file_slot;
1607
403k
    for (const auto& mapping : mappings) {
1608
403k
        const auto entry_it = filter_entries.find(mapping.global_index);
1609
403k
        if (entry_it == filter_entries.end() || !entry_it->second.is_local()) {
1610
20.3k
            continue;
1611
20.3k
        }
1612
383k
        DORIS_CHECK(mapping.file_local_id.has_value());
1613
383k
        global_to_file_slot.emplace(
1614
383k
                mapping.global_index,
1615
383k
                FileSlotRewriteInfo {.block_position = entry_it->second.local_index().value(),
1616
383k
                                     .file_type = mapping.file_type,
1617
383k
                                     .table_type = mapping.table_type,
1618
383k
                                     .file_column_name = mapping.file_column_name});
1619
383k
    }
1620
60.2k
    return global_to_file_slot;
1621
60.2k
}
1622
1623
Status TableColumnMapper::_create_by_index_mapping(const ColumnDefinition& table_column,
1624
                                                   const std::vector<ColumnDefinition>& file_schema,
1625
37.1k
                                                   ColumnMapping* mapping) {
1626
37.1k
    DORIS_CHECK(mapping != nullptr);
1627
37.1k
    DORIS_CHECK(!table_column.is_partition_key);
1628
1629
    // Key contract: in BY_INDEX mode, `ColumnDefinition::identifier` TYPE_INT is interpreted as the
1630
    // 0-based position of this column inside `file_schema`. FE writes the physical file position
1631
    // of each non-partition projected column into that identifier. This interpretation allows:
1632
    //   - sparse projection: read only a subset of file columns (for example only `_col2`
1633
    //     and `_col4`);
1634
    //   - column reordering: table column order differs from file column order;
1635
    //   - no many-to-one mapping: FE must guarantee that each file position is referenced by at
1636
    //     most one table column.
1637
37.1k
    const auto file_index = table_column.get_identifier_position();
1638
1639
    // Case A: file_index is in range, so build a direct positional mapping.
1640
    // The file column name (for example `_col0`) is intentionally ignored here.
1641
37.1k
    if (file_index >= 0 && static_cast<size_t>(file_index) < file_schema.size()) {
1642
36.5k
        return _create_direct_mapping(table_column, file_schema[static_cast<size_t>(file_index)],
1643
36.5k
                                      mapping);
1644
36.5k
    }
1645
1646
    // Case B: file_index is out of range, which means the file does not contain this column.
1647
    // Route it through the missing-column path used by schema evolution.
1648
563
    if (table_column.default_expr != nullptr) {
1649
527
        _set_constant_mapping(mapping, table_column.default_expr);
1650
527
        return Status::OK();
1651
527
    }
1652
    // Keep the mapping empty (`file_local_id` remains `nullopt`) and let the upper finalize
1653
    // stage fill NULL/default values.
1654
36
    return Status::OK();
1655
563
}
1656
1657
18.7k
void TableColumnMapper::_set_constant_mapping(ColumnMapping* mapping, VExprContextSPtr expr) {
1658
18.7k
    DORIS_CHECK(mapping != nullptr);
1659
18.7k
    DORIS_CHECK(expr != nullptr);
1660
18.7k
    mapping->default_expr = std::move(expr);
1661
18.7k
    mapping->constant_index = _constant_map.add(ConstantEntry {
1662
18.7k
            .global_index = mapping->global_index,
1663
18.7k
            .expr = mapping->default_expr,
1664
18.7k
            .type = mapping->table_type,
1665
18.7k
    });
1666
18.7k
    mapping->filter_conversion = FilterConversionType::CONSTANT;
1667
18.7k
}
1668
1669
Status TableColumnMapper::_create_mapping_for_column(const ColumnDefinition& table_column,
1670
                                                     GlobalIndex global_index,
1671
403k
                                                     ColumnMapping* mapping) {
1672
403k
    DORIS_CHECK(mapping != nullptr);
1673
403k
    *mapping = ColumnMapping {};
1674
403k
    mapping->global_index = global_index;
1675
403k
    mapping->table_column_name = table_column.name;
1676
403k
    mapping->table_type = table_column.type;
1677
403k
    const auto row_lineage_type = row_lineage_virtual_column_type(table_column, _options.mode);
1678
403k
    if (const auto* partition_value = find_partition_value(table_column, _partition_values);
1679
403k
        table_column.is_partition_key && partition_value != nullptr) {
1680
        // Partition values are split constants and must take precedence over defaults.
1681
12.7k
        _set_constant_mapping(mapping, VExprContext::create_shared(VLiteral::create_shared(
1682
12.7k
                                               mapping->table_type, *partition_value)));
1683
390k
    } else if (_options.mode == TableColumnMappingMode::BY_INDEX &&
1684
390k
               !table_column.is_partition_key && table_column.has_identifier_field_id()) {
1685
        // BY_INDEX interprets ColumnDefinition::identifier as physical file position.
1686
37.1k
        RETURN_IF_ERROR(_create_by_index_mapping(table_column, _file_schema, mapping));
1687
353k
    } else if (const auto* file_field = _find_file_field(table_column, _file_schema)) {
1688
        // Normal physical file column mapping.
1689
347k
        RETURN_IF_ERROR(_create_direct_mapping(table_column, *file_field, mapping));
1690
347k
        if (row_lineage_type != TableVirtualColumnType::INVALID) {
1691
            // Iceberg v3 rewritten files may physically contain row lineage metadata fields.
1692
            // File non-null values must be preserved, while file NULLs still inherit from data file
1693
            // metadata in IcebergTableReader. Therefore the mapping has a real file source plus a
1694
            // virtual post-materialization step, and filters must wait for finalize output.
1695
134
            mapping->virtual_column_type = row_lineage_type;
1696
134
            mapping->filter_conversion = FilterConversionType::FINALIZE_ONLY;
1697
134
        }
1698
347k
    } else if (row_lineage_type != TableVirtualColumnType::INVALID) {
1699
        // Iceberg row lineage metadata fields are optional in data files. Missing fields are exposed
1700
        // as all-NULL table columns first; IcebergTableReader fills inherited values only when the
1701
        // split carries first_row_id / last_updated_sequence_number metadata.
1702
        // FE may attach a default_expr to these hidden metadata columns, but the Iceberg v3
1703
        // inheritance rule must take precedence over the generic missing-column default path.
1704
749
        mapping->virtual_column_type = row_lineage_type;
1705
5.31k
    } else if (table_column.name == BeConsts::ICEBERG_ROWID_COL) {
1706
        // Doris internal Iceberg row locator is never a physical Iceberg data column. It is built
1707
        // from file path, row position and partition metadata for delete/update/merge.
1708
192
        mapping->virtual_column_type = TableVirtualColumnType::ICEBERG_ROWID;
1709
5.44k
    } else if (table_column.default_expr != nullptr) {
1710
        // Missing schema-evolution column with an explicit default expression.
1711
5.44k
        _set_constant_mapping(mapping, table_column.default_expr);
1712
18.4E
    } else {
1713
18.4E
        if (table_column.is_partition_key) {
1714
0
            return Status::InvalidArgument(
1715
0
                    "Table column '{}' (global_index={}) does not have a matching partition value",
1716
0
                    table_column.name, mapping->global_index.value());
1717
0
        }
1718
18.4E
    }
1719
403k
    return Status::OK();
1720
403k
}
1721
1722
Status TableColumnMapper::_create_hidden_filter_mapping(const ColumnDefinition& table_column,
1723
                                                        GlobalIndex global_index,
1724
2
                                                        ColumnMapping* mapping) {
1725
2
    auto status = _create_mapping_for_column(table_column, global_index, mapping);
1726
2
    if (mapping->file_local_id.has_value() || mapping->constant_index.has_value() ||
1727
2
        mapping->virtual_column_type != TableVirtualColumnType::INVALID) {
1728
0
        return Status::OK();
1729
0
    }
1730
2
    if (_options.mode == TableColumnMappingMode::BY_NAME) {
1731
0
        return status;
1732
0
    }
1733
1734
    // Predicate-only slot refs carry the table name/type but do not carry the table-format field
1735
    // id used by BY_FIELD_ID or the file position used by BY_INDEX. Use a name fallback only for
1736
    // hidden filter localization; projected columns still obey the requested mapping mode.
1737
2
    const auto* file_field =
1738
2
            matcher_for_mode(TableColumnMappingMode::BY_NAME).find(table_column, _file_schema);
1739
2
    if (file_field == nullptr) {
1740
0
        return status;
1741
0
    }
1742
2
    ColumnMapping fallback_mapping;
1743
2
    fallback_mapping.global_index = global_index;
1744
2
    fallback_mapping.table_column_name = table_column.name;
1745
2
    fallback_mapping.table_type = table_column.type;
1746
2
    RETURN_IF_ERROR(_create_direct_mapping(table_column, *file_field, &fallback_mapping));
1747
2
    *mapping = std::move(fallback_mapping);
1748
2
    return Status::OK();
1749
2
}
1750
1751
Status TableColumnMapper::_build_hidden_filter_mappings(
1752
60.2k
        const std::vector<TableFilter>& table_filters) {
1753
60.2k
    _hidden_mappings.clear();
1754
1755
60.2k
    std::map<GlobalIndex, ColumnDefinition> filter_columns;
1756
60.2k
    for (const auto& table_filter : table_filters) {
1757
40.0k
        if (table_filter.conjunct != nullptr) {
1758
40.0k
            collect_top_level_slot_columns(table_filter.conjunct->root(), &filter_columns);
1759
40.0k
        }
1760
40.0k
    }
1761
1762
60.2k
    for (const auto& [global_index, table_column] : filter_columns) {
1763
33.3k
        if (_find_mapping(global_index) != nullptr) {
1764
            // Ignore columns that are already mapped by the projected columns
1765
33.3k
            continue;
1766
33.3k
        }
1767
18.4E
        ColumnMapping mapping;
1768
18.4E
        RETURN_IF_ERROR(_create_hidden_filter_mapping(table_column, global_index, &mapping));
1769
18.4E
        if (mapping.file_local_id.has_value() || mapping.constant_index.has_value() ||
1770
18.4E
            mapping.virtual_column_type != TableVirtualColumnType::INVALID) {
1771
2
            _hidden_mappings.push_back(std::move(mapping));
1772
2
        }
1773
18.4E
    }
1774
60.2k
    return Status::OK();
1775
60.2k
}
1776
1777
Status TableColumnMapper::create_mapping(const std::vector<ColumnDefinition>& projected_columns,
1778
                                         const std::map<std::string, Field>& partition_values,
1779
60.3k
                                         const std::vector<ColumnDefinition>& file_schema) {
1780
60.3k
    clear();
1781
60.3k
    _partition_values = partition_values;
1782
60.3k
    _file_schema = file_schema;
1783
464k
    for (size_t column_idx = 0; column_idx < projected_columns.size(); ++column_idx) {
1784
403k
        ColumnMapping mapping;
1785
403k
        RETURN_IF_ERROR(_create_mapping_for_column(projected_columns[column_idx],
1786
403k
                                                   GlobalIndex(column_idx), &mapping));
1787
403k
        _mappings.push_back(std::move(mapping));
1788
403k
    }
1789
60.3k
    return Status::OK();
1790
60.3k
}
1791
1792
180k
std::vector<ColumnMapping> TableColumnMapper::_filter_visible_mappings() const {
1793
180k
    std::vector<ColumnMapping> mappings;
1794
180k
    mappings.reserve(_mappings.size() + _hidden_mappings.size());
1795
180k
    mappings.insert(mappings.end(), _mappings.begin(), _mappings.end());
1796
180k
    mappings.insert(mappings.end(), _hidden_mappings.begin(), _hidden_mappings.end());
1797
180k
    return mappings;
1798
180k
}
1799
1800
60.2k
Status TableColumnMapper::_build_filter_entries(const FileScanRequest& file_request) {
1801
60.2k
    _filter_entries.clear();
1802
60.2k
    const auto mappings = _filter_visible_mappings();
1803
403k
    for (const auto& mapping : mappings) {
1804
403k
        FilterEntry entry;
1805
403k
        if (mapping.constant_index.has_value()) {
1806
18.7k
            entry = FilterEntry::constant(*mapping.constant_index);
1807
384k
        } else if (mapping.file_local_id.has_value() &&
1808
384k
                   filter_conversion_has_local_source(mapping.filter_conversion)) {
1809
383k
            const auto local_position_it =
1810
383k
                    file_request.local_positions.find(LocalColumnId(*mapping.file_local_id));
1811
383k
            if (local_position_it != file_request.local_positions.end()) {
1812
383k
                entry = FilterEntry::local(local_position_it->second);
1813
383k
            }
1814
383k
        }
1815
403k
        _filter_entries.emplace(mapping.global_index, entry);
1816
403k
    }
1817
60.2k
    return Status::OK();
1818
60.2k
}
1819
1820
Status TableColumnMapper::create_scan_request(
1821
        const std::vector<TableFilter>& table_filters,
1822
        const std::vector<ColumnDefinition>& projected_columns, FileScanRequest* file_request,
1823
60.2k
        RuntimeState* runtime_state) {
1824
    // FileReader evaluates expressions against a file-local block. This mapper owns the
1825
    // table-column to file-column conversion, so it also owns the file-local block positions.
1826
60.2k
    file_request->predicate_columns.clear();
1827
60.2k
    file_request->non_predicate_columns.clear();
1828
60.2k
    file_request->local_positions.clear();
1829
60.2k
    file_request->conjuncts.clear();
1830
60.2k
    file_request->delete_conjuncts.clear();
1831
60.2k
    _filter_entries.clear();
1832
    // 1. Build referenced non-predicate columns
1833
463k
    for (size_t column_idx = 0; column_idx < projected_columns.size(); ++column_idx) {
1834
403k
        const auto global_index = GlobalIndex(column_idx);
1835
403k
        auto* mapping = _find_mapping(global_index);
1836
403k
        if (mapping != nullptr && mapping->file_local_id.has_value()) {
1837
            // A file column can be read lazily as a non-predicate column only when it is not used
1838
            // by row-level expression filters.
1839
383k
            bool used_by_filter = false;
1840
383k
            for (const auto& table_filter : table_filters) {
1841
90.2k
                const auto& global_indices = table_filter.global_indices;
1842
90.2k
                if (std::find(global_indices.begin(), global_indices.end(), global_index) !=
1843
90.2k
                            global_indices.end() &&
1844
90.2k
                    filter_conversion_has_local_source(mapping->filter_conversion)) {
1845
28.8k
                    used_by_filter = true;
1846
28.8k
                    break;
1847
28.8k
                }
1848
90.2k
            }
1849
383k
            if (!used_by_filter || !enable_lazy_materialization()) {
1850
354k
                RETURN_IF_ERROR(add_scan_column(file_request, mapping, false,
1851
354k
                                                force_full_complex_scan_projection()));
1852
354k
            }
1853
383k
        }
1854
403k
    }
1855
    // 2. Build referenced predicate columns
1856
    // Hidden filter mappings must be built before localizing filters, so that they can be localized together with visible mappings and referenced by localized filter expressions.
1857
60.2k
    RETURN_IF_ERROR(_build_hidden_filter_mappings(table_filters));
1858
60.2k
    RETURN_IF_ERROR(localize_filters(table_filters, file_request, runtime_state));
1859
    // 3. Rebuild output projection expressions for projected columns. localize_filters() has
1860
    // already applied the final scan projection to mapping.file_type/projected_file_children before
1861
    // rewriting filter expressions.
1862
403k
    for (auto& mapping : _mappings) {
1863
403k
        if (!mapping.file_local_id.has_value()) {
1864
19.6k
            continue;
1865
19.6k
        }
1866
383k
        auto position_it =
1867
383k
                file_request->local_positions.find(LocalColumnId(*mapping.file_local_id));
1868
383k
        DORIS_CHECK(position_it != file_request->local_positions.end())
1869
33
                << file_request->local_positions.size() << " " << *mapping.file_local_id << " "
1870
33
                << mapping.file_column_name;
1871
383k
        rebuild_projection(&mapping, position_it->second);
1872
383k
    }
1873
60.2k
    return Status::OK();
1874
60.2k
}
1875
1876
477k
ColumnMapping* TableColumnMapper::_find_mapping(GlobalIndex global_index) {
1877
8.33M
    for (auto& mapping : _mappings) {
1878
8.33M
        if (mapping.global_index == global_index) {
1879
477k
            return &mapping;
1880
477k
        }
1881
8.33M
    }
1882
40
    return nullptr;
1883
477k
}
1884
1885
41.1k
ColumnMapping* TableColumnMapper::_find_filter_mapping(GlobalIndex global_index) {
1886
41.1k
    if (auto* mapping = _find_mapping(global_index); mapping != nullptr) {
1887
41.1k
        return mapping;
1888
41.1k
    }
1889
12
    for (auto& mapping : _hidden_mappings) {
1890
2
        if (mapping.global_index == global_index) {
1891
2
            return &mapping;
1892
2
        }
1893
2
    }
1894
10
    return nullptr;
1895
12
}
1896
1897
Status TableColumnMapper::localize_filters(const std::vector<TableFilter>& table_filters,
1898
                                           FileScanRequest* file_request,
1899
60.2k
                                           RuntimeState* runtime_state) {
1900
60.2k
    FilterProjectionMap filter_projections;
1901
60.2k
    auto filter_mappings = _filter_visible_mappings();
1902
60.2k
    RETURN_IF_ERROR(build_nested_struct_filter_projection_map(table_filters, filter_mappings,
1903
60.2k
                                                              &filter_projections));
1904
60.2k
    for (const auto& table_filter : table_filters) {
1905
41.1k
        for (const auto& global_index : table_filter.global_indices) {
1906
41.1k
            auto* mapping = _find_filter_mapping(global_index);
1907
41.1k
            if (mapping == nullptr || !mapping->file_local_id.has_value() ||
1908
41.1k
                !filter_conversion_has_local_source(mapping->filter_conversion)) {
1909
5.13k
                continue;
1910
5.13k
            }
1911
35.9k
            RETURN_IF_ERROR(add_scan_column(file_request, mapping, enable_lazy_materialization(),
1912
35.9k
                                            force_full_complex_scan_projection(),
1913
35.9k
                                            &filter_projections));
1914
35.9k
        }
1915
40.0k
    }
1916
    // Rebuild the file type for every scan-local mapping before expression rewrite. Predicate-only
1917
    // hidden mappings must see the same projected file type as the file reader will produce.
1918
403k
    for (auto& mapping : _mappings) {
1919
403k
        if (mapping.file_local_id.has_value() &&
1920
403k
            file_request->local_positions.contains(LocalColumnId(*mapping.file_local_id))) {
1921
383k
            RETURN_IF_ERROR(apply_scan_projection_to_mapping_file_type(*file_request, &mapping));
1922
383k
        }
1923
403k
    }
1924
60.2k
    for (auto& mapping : _hidden_mappings) {
1925
2
        if (mapping.file_local_id.has_value() &&
1926
2
            file_request->local_positions.contains(LocalColumnId(*mapping.file_local_id))) {
1927
2
            RETURN_IF_ERROR(apply_scan_projection_to_mapping_file_type(*file_request, &mapping));
1928
2
        }
1929
2
    }
1930
60.2k
    RETURN_IF_ERROR(_build_filter_entries(*file_request));
1931
1932
    // Build the complete table-slot rewrite map after all predicate columns have been assigned.
1933
    // This keeps expression localization independent from filter iteration order.
1934
60.2k
    filter_mappings = _filter_visible_mappings();
1935
60.2k
    const auto global_to_file_slot = build_file_slot_rewrite_map(filter_mappings, _filter_entries);
1936
60.2k
    for (const auto& table_filter : table_filters) {
1937
40.0k
        if (table_filter.conjunct != nullptr &&
1938
40.0k
            table_filter_has_only_local_entries(table_filter, _filter_entries)) {
1939
35.1k
            RewriteContext rewrite_context {.runtime_state = runtime_state};
1940
35.1k
            VExprSPtr rewrite_root;
1941
35.1k
            Status clone_status;
1942
35.1k
            try {
1943
35.1k
                clone_status = clone_table_expr_tree(table_filter.conjunct->root(), &rewrite_root);
1944
35.1k
            } catch ([[maybe_unused]] const Exception& e) {
1945
                // Some table filters contain complex intermediate values, for example
1946
                // `element_at(MAP_VALUES(m)[1], 'age') > 30`. The current file-local rewrite only
1947
                // understands top-level slots and struct-element paths rooted at top-level slots;
1948
                // cloning such expressions can hit the generic TExpr complex-type limitation.
1949
                // Leave them above TableReader, where Scanner evaluates the original table-level
1950
                // conjunct after final materialization.
1951
0
#ifndef NDEBUG
1952
0
                return Status::InternalError(
1953
0
                        "Failed to clone table filter for file-local rewrite: {}, expr={}",
1954
0
                        e.to_string(), table_filter.conjunct->root()->debug_string());
1955
#else
1956
                continue;
1957
#endif
1958
0
            } catch ([[maybe_unused]] const std::exception& e) {
1959
0
#ifndef NDEBUG
1960
0
                return Status::InternalError(
1961
0
                        "Failed to clone table filter for file-local rewrite: {}, expr={}",
1962
0
                        e.what(), table_filter.conjunct->root()->debug_string());
1963
#else
1964
                continue;
1965
#endif
1966
0
            }
1967
35.1k
            if (!clone_status.ok()) {
1968
0
#ifndef NDEBUG
1969
0
                return Status::InternalError(
1970
0
                        "Failed to clone table filter for file-local rewrite: {}, expr={}",
1971
0
                        clone_status.to_string(), table_filter.conjunct->root()->debug_string());
1972
#else
1973
                continue;
1974
#endif
1975
0
            }
1976
35.1k
            bool can_localize = true;
1977
35.1k
            auto localized_root = rewrite_table_expr_to_file_expr(rewrite_root, global_to_file_slot,
1978
35.1k
                                                                  filter_mappings, &rewrite_context,
1979
35.1k
                                                                  &can_localize);
1980
35.1k
            if (!can_localize) {
1981
2.26k
                continue;
1982
2.26k
            }
1983
32.9k
            auto localized_conjunct = VExprContext::create_shared(std::move(localized_root));
1984
32.9k
            RETURN_IF_ERROR(rewrite_context.prepare_created_exprs(localized_conjunct.get()));
1985
32.9k
            file_request->conjuncts.push_back(std::move(localized_conjunct));
1986
32.9k
        }
1987
40.0k
    }
1988
60.2k
    return Status::OK();
1989
60.2k
}
1990
1991
const ColumnDefinition* TableColumnMapper::_find_file_field(
1992
        const ColumnDefinition& table_column,
1993
353k
        const std::vector<ColumnDefinition>& file_schema) const {
1994
353k
    if (table_column.name.starts_with(BeConsts::GLOBAL_ROWID_COL)) {
1995
1.04M
        const auto field_it = std::ranges::find_if(file_schema, [](const ColumnDefinition& field) {
1996
1.04M
            return field.column_type == ColumnType::GLOBAL_ROWID;
1997
1.04M
        });
1998
7.51k
        return field_it == file_schema.end() ? nullptr : &*field_it;
1999
7.51k
    }
2000
346k
    return matcher_for_mode(_options.mode).find(table_column, file_schema);
2001
353k
}
2002
2003
Status TableColumnMapper::_create_direct_mapping(const ColumnDefinition& table_column,
2004
                                                 const ColumnDefinition& file_field,
2005
683k
                                                 ColumnMapping* mapping) const {
2006
683k
    DORIS_CHECK(mapping != nullptr);
2007
683k
    DORIS_CHECK(file_field.local_id >= 0 || file_field.local_id == GLOBAL_ROWID_COLUMN_ID);
2008
683k
    mapping->file_local_id = file_field.local_id;
2009
683k
    mapping->table_column_name = table_column.name;
2010
683k
    mapping->file_column_name = file_field.name;
2011
683k
    mapping->original_file_type = file_field.type;
2012
683k
    mapping->original_file_children = file_field.children;
2013
683k
    mapping->projected_file_children = file_field.children;
2014
683k
    mapping->file_type = file_field.type;
2015
683k
    mapping->is_trivial = mapping_can_use_file_column_directly(*mapping);
2016
683k
    mapping->filter_conversion = direct_filter_conversion(*mapping);
2017
683k
    mapping->child_mappings.clear();
2018
2019
683k
    auto table_children = table_column.children;
2020
683k
    const auto nested_table_type = remove_nullable(mapping->table_type);
2021
    // Some scan paths, especially SELECT *, only carry the complete complex DataType for a table
2022
    // column and leave ColumnDefinition::children empty. If the file type is an older complex
2023
    // schema, treating this as a leaf mapping would make TableReader fall back to a plain CAST.
2024
    // That is invalid for evolved structs with different field counts.
2025
    //
2026
    // Example:
2027
    //   table column type: Map(String, Struct(age, full_name, gender))
2028
    //   old file type:    Map(String, Struct(age, name))
2029
    //   table children:   empty
2030
    //
2031
    // Synthesize key/value/struct-field children from the table type so the normal recursive
2032
    // mapping path can rematerialize `name -> full_name` and fill missing `gender` with defaults,
2033
    // instead of trying to CAST Struct(age, name) to Struct(age, full_name, gender).
2034
683k
    const bool synthesized_table_children =
2035
683k
            table_children.empty() && is_complex_type(nested_table_type->get_primitive_type()) &&
2036
683k
            !mapping->table_type->equals(*mapping->file_type);
2037
683k
    if (synthesized_table_children) {
2038
4
        table_children = synthesize_complex_children_from_type(mapping->table_type);
2039
683k
    } else if (!table_children.empty() && !mapping->table_type->equals(*mapping->file_type)) {
2040
16.3k
        complete_required_complex_children_from_type(mapping->table_type, &table_children);
2041
16.3k
    }
2042
2043
683k
    if (!table_children.empty()) {
2044
197k
        if (!is_complex_type(remove_nullable(mapping->file_type)->get_primitive_type())) {
2045
0
            return Status::NotSupported(
2046
0
                    "Cannot map complex table column '{}' to scalar parquet column '{}', table "
2047
0
                    "type={}, file type={}",
2048
0
                    table_column.name, file_field.name, mapping->table_type->get_name(),
2049
0
                    mapping->file_type->get_name());
2050
0
        }
2051
197k
        RETURN_IF_ERROR(validate_file_schema_children(file_field));
2052
197k
        std::vector<int32_t> synthesized_used_file_child_ids;
2053
501k
        for (size_t table_child_idx = 0; table_child_idx < table_children.size();
2054
304k
             ++table_child_idx) {
2055
304k
            const auto& table_child = table_children[table_child_idx];
2056
304k
            const auto* file_child =
2057
304k
                    find_file_child_for_mapping(table_child, file_field, _options.mode,
2058
304k
                                                table_child_idx, synthesized_table_children);
2059
304k
            if (synthesized_table_children && file_child != nullptr) {
2060
8
                const auto file_child_id = file_child->file_local_id();
2061
8
                if (std::ranges::find(synthesized_used_file_child_ids, file_child_id) !=
2062
8
                    synthesized_used_file_child_ids.end()) {
2063
2
                    file_child = nullptr;
2064
2
                    for (const auto& candidate : file_field.children) {
2065
2
                        const auto candidate_id = candidate.file_local_id();
2066
2
                        if (std::ranges::find(synthesized_used_file_child_ids, candidate_id) ==
2067
2
                            synthesized_used_file_child_ids.end()) {
2068
2
                            file_child = &candidate;
2069
2
                            break;
2070
2
                        }
2071
2
                    }
2072
2
                }
2073
8
                if (file_child != nullptr) {
2074
8
                    synthesized_used_file_child_ids.push_back(file_child->file_local_id());
2075
8
                }
2076
8
            }
2077
304k
            if (file_child == nullptr) {
2078
4.46k
                ColumnMapping child_mapping;
2079
4.46k
                child_mapping.table_column_name = table_child.name;
2080
4.46k
                child_mapping.file_column_name = table_child.name;
2081
4.46k
                child_mapping.table_type = table_child.type;
2082
4.46k
                child_mapping.file_type = table_child.type;
2083
4.46k
                child_mapping.filter_conversion = FilterConversionType::FINALIZE_ONLY;
2084
4.46k
                mapping->child_mappings.push_back(std::move(child_mapping));
2085
4.46k
                continue;
2086
4.46k
            }
2087
300k
            ColumnMapping child_mapping;
2088
300k
            child_mapping.table_column_name = table_child.name;
2089
300k
            child_mapping.table_type = table_child.type;
2090
300k
            RETURN_IF_ERROR(_create_direct_mapping(table_child, *file_child, &child_mapping));
2091
300k
            mapping->child_mappings.push_back(std::move(child_mapping));
2092
300k
        }
2093
197k
        if (needs_projected_file_type_rebuild(*mapping)) {
2094
            // If complex projection prunes some children, we have to rebuild the projected file type to make sure the reader expression can find the correct child types by name.
2095
16.9k
            RETURN_IF_ERROR(rebuild_projected_file_children_and_type(
2096
16.9k
                    mapping->file_type, mapping->original_file_children, mapping->child_mappings,
2097
16.9k
                    &mapping->projected_file_children, &mapping->file_type));
2098
16.9k
            DCHECK(mapping->table_type != nullptr);
2099
16.9k
            mapping->is_trivial = mapping_can_use_file_column_directly(*mapping);
2100
16.9k
            mapping->filter_conversion = projected_filter_conversion(*mapping);
2101
16.9k
        }
2102
197k
    }
2103
683k
    return Status::OK();
2104
683k
}
2105
2106
} // namespace doris::format