Coverage Report

Created: 2026-07-24 01:26

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