Coverage Report

Created: 2026-07-20 06:15

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