Coverage Report

Created: 2026-07-15 13:51

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