Coverage Report

Created: 2026-07-25 13:13

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