Coverage Report

Created: 2026-07-07 06:19

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