Coverage Report

Created: 2026-07-22 21:56

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