| // Licensed to the Apache Software Foundation (ASF) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The ASF licenses this file |
| // to you under the Apache License, Version 2.0 (the |
| // "License"); you may not use this file except in compliance |
| // with the License. You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, |
| // software distributed under the License is distributed on an |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| // KIND, either express or implied. See the License for the |
| // specific language governing permissions and limitations |
| // under the License. |
| |
| #include "format_v2/column_mapper.h" |
| |
| #include <algorithm> |
| #include <cstddef> |
| #include <memory> |
| #include <optional> |
| #include <set> |
| #include <sstream> |
| #include <string_view> |
| #include <utility> |
| #include <vector> |
| |
| #include "common/consts.h" |
| #include "common/exception.h" |
| #include "common/status.h" |
| #include "core/data_type/convert_field_to_type.h" |
| #include "core/data_type/data_type_array.h" |
| #include "core/data_type/data_type_map.h" |
| #include "core/data_type/data_type_nullable.h" |
| #include "core/data_type/data_type_string.h" |
| #include "core/data_type/data_type_struct.h" |
| #include "core/data_type/primitive_type.h" |
| #include "exprs/runtime_filter_expr.h" |
| #include "exprs/short_circuit_evaluation_expr.h" |
| #include "exprs/vcase_expr.h" |
| #include "exprs/vcast_expr.h" |
| #include "exprs/vcondition_expr.h" |
| #include "exprs/vectorized_fn_call.h" |
| #include "exprs/vexpr_context.h" |
| #include "exprs/vin_predicate.h" |
| #include "exprs/vliteral.h" |
| #include "format_v2/column_mapper_nested.h" |
| #include "format_v2/expr/cast.h" |
| #include "format_v2/file_reader.h" |
| #include "format_v2/schema_projection.h" |
| #include "format_v2/table_reader.h" |
| #include "gen_cpp/Exprs_types.h" |
| #include "util/url_coding.h" |
| |
| namespace doris::format { |
| |
| namespace { |
| |
| Status build_initial_default_column(const ColumnDefinition& column, ColumnPtr* value) { |
| DORIS_CHECK(value != nullptr); |
| *value = nullptr; |
| if (!column.initial_default_value.has_value()) { |
| return Status::OK(); |
| } |
| const auto nested_type = remove_nullable(column.type); |
| Field parsed; |
| if (column.initial_default_value_is_base64 || |
| nested_type->get_primitive_type() == TYPE_VARBINARY) { |
| std::string decoded; |
| if (!base64_decode(*column.initial_default_value, &decoded)) { |
| return Status::InvalidArgument("Invalid Base64 Iceberg initial default for field {}", |
| column.name); |
| } |
| parsed = nested_type->get_primitive_type() == TYPE_VARBINARY |
| ? Field::create_field<TYPE_VARBINARY>(StringView(decoded)) |
| : Field::create_field<TYPE_STRING>(decoded); |
| // Variable-width Fields borrow their input. Materialize while decoded is alive so the |
| // resulting column owns the payload before it crosses a mapping/literal boundary. |
| *value = column.type->create_column_const(1, parsed); |
| return Status::OK(); |
| } else { |
| RETURN_IF_ERROR( |
| nested_type->get_serde()->from_fe_string(*column.initial_default_value, parsed)); |
| } |
| *value = column.type->create_column_const(1, parsed); |
| return Status::OK(); |
| } |
| |
| Status build_initial_default_literal(const ColumnDefinition& column, VExprContextSPtr* literal) { |
| DORIS_CHECK(literal != nullptr); |
| ColumnPtr owned_value; |
| RETURN_IF_ERROR(build_initial_default_column(column, &owned_value)); |
| DORIS_CHECK(static_cast<bool>(owned_value)); |
| Field value; |
| owned_value->get(0, value); |
| // VLiteral copies the borrowed Field into its own column while owned_value is still alive. |
| *literal = VExprContext::create_shared(VLiteral::create_shared(column.type, value)); |
| return Status::OK(); |
| } |
| |
| bool has_shared_descendant_field_id(const ColumnDefinition& table, const ColumnDefinition& file) { |
| const auto& table_children = |
| table.identity_children.empty() ? table.children : table.identity_children; |
| for (const auto& table_child : table_children) { |
| if (!table_child.has_identifier_field_id()) { |
| continue; |
| } |
| const auto file_child = |
| std::ranges::find_if(file.children, [&](const ColumnDefinition& candidate) { |
| return candidate.has_identifier_field_id() && |
| candidate.get_identifier_field_id() == |
| table_child.get_identifier_field_id(); |
| }); |
| if (file_child != file.children.end() || |
| std::ranges::any_of(file.children, [&](const ColumnDefinition& candidate) { |
| return has_shared_descendant_field_id(table_child, candidate); |
| })) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| std::string mapping_mode_to_string(TableColumnMappingMode mode) { |
| switch (mode) { |
| case TableColumnMappingMode::BY_FIELD_ID: |
| return "BY_FIELD_ID"; |
| case TableColumnMappingMode::BY_NAME: |
| return "BY_NAME"; |
| case TableColumnMappingMode::BY_INDEX: |
| return "BY_INDEX"; |
| } |
| return "UNKNOWN"; |
| } |
| |
| bool column_has_name(const ColumnDefinition& column, const std::string& name) { |
| if (to_lower(column.name) == to_lower(name)) { |
| return true; |
| } |
| if (column.has_identifier_name() && to_lower(column.get_identifier_name()) == to_lower(name)) { |
| return true; |
| } |
| return std::ranges::any_of(column.name_mapping, [&](const std::string& alias) { |
| return to_lower(alias) == to_lower(name); |
| }); |
| } |
| |
| bool column_names_match(const ColumnDefinition& lhs, const ColumnDefinition& rhs) { |
| if (!lhs.has_name_mapping) { |
| if (column_has_name(rhs, lhs.name)) { |
| return true; |
| } |
| if (lhs.has_identifier_name() && column_has_name(rhs, lhs.get_identifier_name())) { |
| return true; |
| } |
| } |
| // Explicit Iceberg name mapping is authoritative: an empty alias list represents a field that |
| // did not exist in the imported file, so only transported aliases may match. |
| return std::ranges::any_of(lhs.name_mapping, [&](const std::string& alias) { |
| return column_has_name(rhs, alias); |
| }); |
| } |
| |
| class ColumnMatcher { |
| public: |
| virtual ~ColumnMatcher() = default; |
| virtual const ColumnDefinition* find( |
| const ColumnDefinition& table_column, |
| const std::vector<ColumnDefinition>& file_schema) const = 0; |
| }; |
| |
| class FieldIdMatcher final : public ColumnMatcher { |
| public: |
| const ColumnDefinition* find(const ColumnDefinition& table_column, |
| const std::vector<ColumnDefinition>& file_schema) const override { |
| if (!table_column.has_identifier_field_id()) { |
| return nullptr; |
| } |
| const auto field_id = table_column.get_identifier_field_id(); |
| const auto field_it = std::ranges::find_if(file_schema, [&](const ColumnDefinition& field) { |
| return field.has_identifier_field_id() && field.get_identifier_field_id() == field_id; |
| }); |
| return field_it == file_schema.end() ? nullptr : &*field_it; |
| } |
| }; |
| |
| class NameMatcher final : public ColumnMatcher { |
| public: |
| const ColumnDefinition* find(const ColumnDefinition& table_column, |
| const std::vector<ColumnDefinition>& file_schema) const override { |
| const auto field_it = std::ranges::find_if(file_schema, [&](const ColumnDefinition& field) { |
| return column_names_match(table_column, field); |
| }); |
| return field_it == file_schema.end() ? nullptr : &*field_it; |
| } |
| }; |
| |
| class PositionMatcher final : public ColumnMatcher { |
| public: |
| const ColumnDefinition* find(const ColumnDefinition& table_column, |
| const std::vector<ColumnDefinition>& file_schema) const override { |
| if (!table_column.has_identifier_field_id()) { |
| return nullptr; |
| } |
| const auto position = table_column.get_identifier_position(); |
| if (position < 0 || static_cast<size_t>(position) >= file_schema.size()) { |
| return nullptr; |
| } |
| return &file_schema[static_cast<size_t>(position)]; |
| } |
| }; |
| |
| const ColumnMatcher& matcher_for_mode(TableColumnMappingMode mode) { |
| static const FieldIdMatcher field_id_matcher; |
| static const NameMatcher name_matcher; |
| static const PositionMatcher position_matcher; |
| switch (mode) { |
| case TableColumnMappingMode::BY_FIELD_ID: |
| return field_id_matcher; |
| case TableColumnMappingMode::BY_NAME: |
| return name_matcher; |
| case TableColumnMappingMode::BY_INDEX: |
| return position_matcher; |
| } |
| return field_id_matcher; |
| } |
| |
| std::string virtual_column_type_to_string(TableVirtualColumnType type) { |
| switch (type) { |
| case TableVirtualColumnType::INVALID: |
| return "INVALID"; |
| case TableVirtualColumnType::ROW_ID: |
| return "ROW_ID"; |
| case TableVirtualColumnType::LAST_UPDATED_SEQUENCE_NUMBER: |
| return "LAST_UPDATED_SEQUENCE_NUMBER"; |
| case TableVirtualColumnType::ICEBERG_ROWID: |
| return "ICEBERG_ROWID"; |
| } |
| return "UNKNOWN"; |
| } |
| |
| std::string filter_conversion_type_to_string(FilterConversionType type) { |
| switch (type) { |
| case FilterConversionType::COPY_DIRECTLY: |
| return "COPY_DIRECTLY"; |
| case FilterConversionType::CAST_FILTER: |
| return "CAST_FILTER"; |
| case FilterConversionType::READER_EXPRESSION: |
| return "READER_EXPRESSION"; |
| case FilterConversionType::FINALIZE_ONLY: |
| return "FINALIZE_ONLY"; |
| case FilterConversionType::CONSTANT: |
| return "CONSTANT"; |
| } |
| return "UNKNOWN"; |
| } |
| |
| std::string data_type_debug_string(const DataTypePtr& type) { |
| return type == nullptr ? "null" : type->get_name(); |
| } |
| |
| std::string field_debug_string(const Field& field) { |
| std::ostringstream out; |
| out << "Field{type=" << type_to_string(field.get_type()) << ", value="; |
| switch (field.get_type()) { |
| case TYPE_NULL: |
| out << "null"; |
| break; |
| case TYPE_INT: |
| out << field.get<TYPE_INT>(); |
| break; |
| case TYPE_BIGINT: |
| out << field.get<TYPE_BIGINT>(); |
| break; |
| case TYPE_STRING: |
| out << field.get<TYPE_STRING>(); |
| break; |
| default: |
| out << field.to_debug_string(0); |
| break; |
| } |
| out << "}"; |
| return out.str(); |
| } |
| |
| template <typename T, typename Formatter> |
| std::string join_debug_strings(const std::vector<T>& values, Formatter formatter) { |
| std::ostringstream out; |
| out << "["; |
| for (size_t i = 0; i < values.size(); ++i) { |
| if (i > 0) { |
| out << ", "; |
| } |
| out << formatter(values[i]); |
| } |
| out << "]"; |
| return out.str(); |
| } |
| |
| } // namespace |
| |
| const ColumnDefinition* find_column_by_name(const ColumnDefinition& table_column, |
| const std::vector<ColumnDefinition>& file_schema) { |
| return matcher_for_mode(TableColumnMappingMode::BY_NAME).find(table_column, file_schema); |
| } |
| |
| const Field* find_partition_value(const ColumnDefinition& table_column, |
| const std::map<std::string, Field>& partition_values) { |
| const auto find_by_name = [&](const std::string& name) -> const Field* { |
| const auto value_it = partition_values.find(name); |
| return value_it == partition_values.end() ? nullptr : &value_it->second; |
| }; |
| if (const auto* value = find_by_name(table_column.name); value != nullptr) { |
| return value; |
| } |
| if (table_column.has_identifier_name()) { |
| if (const auto* value = find_by_name(table_column.get_identifier_name()); |
| value != nullptr) { |
| return value; |
| } |
| } |
| for (const auto& alias : table_column.name_mapping) { |
| if (const auto* value = find_by_name(alias); value != nullptr) { |
| return value; |
| } |
| } |
| return nullptr; |
| } |
| |
| struct FileSlotRewriteInfo { |
| size_t block_position = 0; |
| DataTypePtr file_type; |
| DataTypePtr table_type; |
| std::string file_column_name; |
| }; |
| |
| struct RewriteContext { |
| RuntimeState* runtime_state = nullptr; |
| std::vector<VExprSPtr> created_exprs {}; |
| |
| void add_created_expr(VExprSPtr expr) { created_exprs.push_back(std::move(expr)); } |
| |
| Status prepare_created_exprs(VExprContext* context) const { |
| DORIS_CHECK(context != nullptr); |
| RowDescriptor row_desc; |
| for (const auto& expr : created_exprs) { |
| if (dynamic_cast<const Cast*>(expr.get()) != nullptr && runtime_state == nullptr) { |
| return Status::InvalidArgument( |
| "RuntimeState is required to prepare rewritten cast expression {}", |
| expr->expr_name()); |
| } |
| RETURN_IF_ERROR(expr->prepare(runtime_state, row_desc, context)); |
| } |
| return Status::OK(); |
| } |
| }; |
| |
| static VExprSPtr create_file_slot_ref(const VSlotRef& slot_ref, |
| const FileSlotRewriteInfo& rewrite_info, |
| RewriteContext* rewrite_context) { |
| auto ref = |
| VSlotRef::create_shared(slot_ref.slot_id(), cast_set<int>(rewrite_info.block_position), |
| -1, rewrite_info.file_type, rewrite_info.file_column_name); |
| rewrite_context->add_created_expr(ref); |
| return ref; |
| } |
| |
| static bool is_cast_expr(const VExprSPtr& expr) { |
| return dynamic_cast<const Cast*>(expr.get()) != nullptr; |
| } |
| |
| static bool is_binary_comparison_predicate(const VExprSPtr& expr) { |
| if (expr == nullptr || expr->get_num_children() != 2 || |
| (expr->node_type() != TExprNodeType::BINARY_PRED && |
| expr->node_type() != TExprNodeType::NULL_AWARE_BINARY_PRED)) { |
| return false; |
| } |
| switch (expr->op()) { |
| case TExprOpcode::EQ: |
| case TExprOpcode::EQ_FOR_NULL: |
| case TExprOpcode::NE: |
| case TExprOpcode::GE: |
| case TExprOpcode::GT: |
| case TExprOpcode::LE: |
| case TExprOpcode::LT: |
| return true; |
| default: |
| return false; |
| } |
| } |
| |
| std::string TableColumnMapperOptions::debug_string() const { |
| std::ostringstream out; |
| out << "TableColumnMapperOptions{mode=" << mapping_mode_to_string(mode) |
| << ", allow_idless_complex_wrapper_projection=" << allow_idless_complex_wrapper_projection |
| << ", enable_row_lineage_virtual_columns=" << enable_row_lineage_virtual_columns << "}"; |
| return out.str(); |
| } |
| |
| std::string ColumnDefinition::debug_string() const { |
| std::ostringstream out; |
| out << "ColumnDefinition{name=" << name << ", identifier=" << field_debug_string(identifier) |
| << ", name_mapping=" |
| << join_debug_strings(name_mapping, [](const std::string& name) { return name; }) |
| << ", has_name_mapping=" << has_name_mapping << ", local_id=" << local_id |
| << ", type=" << data_type_debug_string(type) << ", children=" |
| << join_debug_strings(children, |
| [](const ColumnDefinition& child) { return child.debug_string(); }) |
| << ", identity_children=" |
| << join_debug_strings(identity_children, |
| [](const ColumnDefinition& child) { return child.debug_string(); }) |
| << ", has_default_expr=" << (default_expr != nullptr) |
| << ", is_partition_key=" << is_partition_key << "}"; |
| return out.str(); |
| } |
| |
| std::string LocalColumnIndex::debug_string() const { |
| std::ostringstream out; |
| out << "LocalColumnIndex{index=" << index << ", project_all_children=" << project_all_children |
| << ", children=" |
| << join_debug_strings(children, |
| [](const LocalColumnIndex& child) { return child.debug_string(); }) |
| << "}"; |
| return out.str(); |
| } |
| |
| std::string ColumnMapping::debug_string() const { |
| std::ostringstream out; |
| out << "ColumnMapping{global_index=" << global_index |
| << ", table_column_name=" << table_column_name << ", file_local_id="; |
| if (file_local_id.has_value()) { |
| out << *file_local_id; |
| } else { |
| out << "null"; |
| } |
| out << ", constant_index="; |
| if (constant_index.has_value()) { |
| out << *constant_index; |
| } else { |
| out << "null"; |
| } |
| out << ", file_column_name=" << file_column_name |
| << ", original_file_type=" << data_type_debug_string(original_file_type) |
| << ", original_file_children=" |
| << join_debug_strings(original_file_children, |
| [](const ColumnDefinition& child) { return child.debug_string(); }) |
| << ", file_type=" << data_type_debug_string(file_type) |
| << ", table_type=" << data_type_debug_string(table_type) |
| << ", has_projection=" << (projection != nullptr) << ", child_mappings=" |
| << join_debug_strings(child_mappings, |
| [](const ColumnMapping& child) { return child.debug_string(); }) |
| << ", is_trivial=" << is_trivial << ", is_constant=" << constant_index.has_value() |
| << ", filter_conversion=" << filter_conversion_type_to_string(filter_conversion) |
| << ", virtual_column_type=" << virtual_column_type_to_string(virtual_column_type) |
| << ", has_default_expr=" << (default_expr != nullptr) << "}"; |
| return out.str(); |
| } |
| |
| std::string TableColumnMapper::debug_string() const { |
| std::ostringstream out; |
| out << "TableColumnMapper{options=" << _options.debug_string() << ", mappings=" |
| << join_debug_strings(_mappings, |
| [](const ColumnMapping& mapping) { return mapping.debug_string(); }) |
| << ", hidden_mappings=" |
| << join_debug_strings(_hidden_mappings, |
| [](const ColumnMapping& mapping) { return mapping.debug_string(); }) |
| << ", constant_count=" << _constant_map.size() << "}"; |
| return out.str(); |
| } |
| |
| static const FileSlotRewriteInfo* find_slot_rewrite_info( |
| const VExprSPtr& expr, |
| const std::map<GlobalIndex, FileSlotRewriteInfo>& global_to_file_slot, |
| const VSlotRef** slot_ref) { |
| if (expr == nullptr) { |
| return nullptr; |
| } |
| VExprSPtr slot_expr = expr; |
| const bool input_is_cast = is_cast_expr(expr) && expr->get_num_children() == 1; |
| if (is_cast_expr(expr) && expr->get_num_children() == 1) { |
| slot_expr = expr->children()[0]; |
| } |
| if (!slot_expr->is_slot_ref()) { |
| return nullptr; |
| } |
| const auto* candidate_slot_ref = assert_cast<const VSlotRef*>(slot_expr.get()); |
| const auto rewrite_it = global_to_file_slot.find(slot_ref_global_index(*candidate_slot_ref)); |
| if (rewrite_it == global_to_file_slot.end()) { |
| return nullptr; |
| } |
| if (input_is_cast && !expr->data_type()->equals(*rewrite_it->second.table_type)) { |
| return nullptr; |
| } |
| if (slot_ref != nullptr) { |
| *slot_ref = candidate_slot_ref; |
| } |
| return &rewrite_it->second; |
| } |
| |
| static bool filter_conversion_has_local_source(FilterConversionType conversion) { |
| switch (conversion) { |
| case FilterConversionType::COPY_DIRECTLY: |
| case FilterConversionType::CAST_FILTER: |
| case FilterConversionType::READER_EXPRESSION: |
| return true; |
| case FilterConversionType::FINALIZE_ONLY: |
| case FilterConversionType::CONSTANT: |
| return false; |
| } |
| return false; |
| } |
| |
| static bool table_filter_has_only_local_entries( |
| const TableFilter& table_filter, const std::map<GlobalIndex, FilterEntry>& filter_entries) { |
| for (const auto global_index : table_filter.global_indices) { |
| const auto entry_it = filter_entries.find(global_index); |
| if (entry_it == filter_entries.end() || !entry_it->second.is_local()) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| static VExprSPtr unwrap_literal_for_file_cast(const VExprSPtr& expr, |
| const DataTypePtr& table_type) { |
| if (expr == nullptr) { |
| return nullptr; |
| } |
| if (expr->is_literal()) { |
| return expr; |
| } |
| if (is_cast_expr(expr) && expr->get_num_children() == 1 && expr->children()[0]->is_literal() && |
| expr->children()[0]->data_type()->equals(*table_type)) { |
| return expr->children()[0]; |
| } |
| return nullptr; |
| } |
| |
| static Field literal_field_from_expr(const VExpr& literal_expr) { |
| DORIS_CHECK(literal_expr.is_literal()); |
| const auto* literal = dynamic_cast<const VLiteral*>(&literal_expr); |
| DORIS_CHECK(literal != nullptr); |
| Field field; |
| literal->get_column_ptr()->get(0, field); |
| return field; |
| } |
| |
| // Table filter localization clones an already-prepared table expr and then rewrites it to file |
| // slots. Only split-local literals and BE cast nodes need table-reader-specific clone behavior; |
| // plain slot refs and literals use their own VExpr::clone_node(). |
| static Status clone_table_expr_node(const VExpr& expr, VExprSPtr* cloned_expr) { |
| DORIS_CHECK(cloned_expr != nullptr); |
| if (const auto* split_literal = dynamic_cast<const SplitLocalFileLiteral*>(&expr)) { |
| *cloned_expr = std::make_shared<SplitLocalFileLiteral>( |
| split_literal->data_type(), literal_field_from_expr(expr), |
| split_literal->original_type(), split_literal->original_field()); |
| } else if (const auto* vcast_expr = dynamic_cast<const VCastExpr*>(&expr); |
| vcast_expr != nullptr && vcast_expr->node_type() == TExprNodeType::CAST_EXPR) { |
| *cloned_expr = Cast::create_shared(vcast_expr->data_type()); |
| } |
| return Status::OK(); |
| } |
| |
| Status clone_table_expr_tree(const VExprSPtr& expr, VExprSPtr* cloned_expr) { |
| DORIS_CHECK(cloned_expr != nullptr); |
| if (expr == nullptr) { |
| *cloned_expr = nullptr; |
| return Status::OK(); |
| } |
| return expr->deep_clone(cloned_expr, clone_table_expr_node); |
| } |
| |
| static VExprSPtr original_table_literal(const VExprSPtr& literal_expr, |
| RewriteContext* rewrite_context = nullptr) { |
| DORIS_CHECK(literal_expr != nullptr); |
| DORIS_CHECK(literal_expr->is_literal()); |
| const auto* rewritten_literal = dynamic_cast<const SplitLocalFileLiteral*>(literal_expr.get()); |
| if (rewritten_literal == nullptr) { |
| return literal_expr; |
| } |
| auto literal = VLiteral::create_shared(rewritten_literal->original_type(), |
| rewritten_literal->original_field()); |
| if (rewrite_context != nullptr) { |
| rewrite_context->add_created_expr(literal); |
| } |
| return literal; |
| } |
| |
| static ColumnDefinition hidden_column_from_slot_ref(const VSlotRef& slot_ref) { |
| ColumnDefinition column; |
| column.name = slot_ref.column_name(); |
| column.identifier = Field::create_field<TYPE_STRING>(column.name); |
| column.type = slot_ref.data_type(); |
| return column; |
| } |
| |
| static void collect_top_level_slot_columns(const VExprSPtr& expr, |
| std::map<GlobalIndex, ColumnDefinition>* columns) { |
| DORIS_CHECK(columns != nullptr); |
| if (expr == nullptr) { |
| return; |
| } |
| if (expr->is_slot_ref()) { |
| const auto* slot_ref = assert_cast<const VSlotRef*>(expr.get()); |
| columns->try_emplace(slot_ref_global_index(*slot_ref), |
| hidden_column_from_slot_ref(*slot_ref)); |
| return; |
| } |
| for (const auto& child : expr->children()) { |
| collect_top_level_slot_columns(child, columns); |
| } |
| } |
| |
| static std::optional<uint8_t> signed_integer_width(PrimitiveType type) { |
| switch (type) { |
| case TYPE_TINYINT: |
| return 8; |
| case TYPE_SMALLINT: |
| return 16; |
| case TYPE_INT: |
| return 32; |
| case TYPE_BIGINT: |
| return 64; |
| case TYPE_LARGEINT: |
| return 128; |
| default: |
| return std::nullopt; |
| } |
| } |
| |
| static std::optional<uint8_t> floating_width(PrimitiveType type) { |
| switch (type) { |
| case TYPE_FLOAT: |
| return 32; |
| case TYPE_DOUBLE: |
| return 64; |
| default: |
| return std::nullopt; |
| } |
| } |
| |
| static std::optional<uint8_t> floating_exact_integer_width(PrimitiveType type) { |
| switch (type) { |
| case TYPE_FLOAT: |
| return 24; |
| case TYPE_DOUBLE: |
| return 53; |
| default: |
| return std::nullopt; |
| } |
| } |
| |
| static bool is_lossless_file_to_table_numeric_cast(const DataTypePtr& file_type, |
| const DataTypePtr& table_type) { |
| const auto file_nested_type = remove_nullable(file_type); |
| const auto table_nested_type = remove_nullable(table_type); |
| if (file_nested_type->equals(*table_nested_type)) { |
| return true; |
| } |
| |
| const auto file_primitive_type = file_nested_type->get_primitive_type(); |
| const auto table_primitive_type = table_nested_type->get_primitive_type(); |
| if (const auto file_width = signed_integer_width(file_primitive_type)) { |
| if (const auto table_width = signed_integer_width(table_primitive_type)) { |
| return *table_width >= *file_width; |
| } |
| if (const auto table_width = floating_exact_integer_width(table_primitive_type)) { |
| return *table_width >= *file_width; |
| } |
| return false; |
| } |
| if (const auto file_width = floating_width(file_primitive_type)) { |
| const auto table_width = floating_width(table_primitive_type); |
| return table_width.has_value() && *table_width >= *file_width; |
| } |
| return false; |
| } |
| |
| static VExprSPtr rewrite_literal_to_file_type(const VExprSPtr& literal_expr, |
| const FileSlotRewriteInfo& rewrite_info, |
| RewriteContext* rewrite_context) { |
| DORIS_CHECK(literal_expr != nullptr); |
| DORIS_CHECK(literal_expr->is_literal()); |
| const auto original_literal = original_table_literal(literal_expr, rewrite_context); |
| const Field original_field = literal_field(original_literal); |
| if (rewrite_info.file_type->equals(*original_literal->data_type())) { |
| return original_literal; |
| } |
| // A literal round trip alone cannot prove that file-local evaluation is safe: the file slot |
| // itself may lose information when materialized as the table type. For example, DOUBLE 1.5 |
| // becomes BIGINT 1, so table predicate `value = 1` is true while file predicate |
| // `value = 1.0` is false. Complex Field equality also does not compare nested contents. |
| // Restrict localization to scalar numeric casts that preserve every file value; unsupported |
| // and complex casts keep the table predicate and evaluate after materialization. |
| if (!is_lossless_file_to_table_numeric_cast(rewrite_info.file_type, |
| original_literal->data_type())) { |
| return nullptr; |
| } |
| Field file_field; |
| try { |
| convert_field_to_type(original_field, *rewrite_info.file_type, &file_field, |
| original_literal->data_type().get()); |
| } catch (const Exception&) { |
| return nullptr; |
| } |
| if (file_field.is_null()) { |
| return nullptr; |
| } |
| if (file_field.get_type() != remove_nullable(rewrite_info.file_type)->get_primitive_type()) { |
| return nullptr; |
| } |
| Field round_trip_field; |
| try { |
| convert_field_to_type(file_field, *original_literal->data_type(), &round_trip_field, |
| rewrite_info.file_type.get()); |
| } catch (const Exception&) { |
| return nullptr; |
| } |
| // The file-to-table type check protects every possible file value. This round trip separately |
| // proves that the specific predicate boundary is exactly representable in the file type. |
| if (round_trip_field != original_field) { |
| return nullptr; |
| } |
| auto literal = std::make_shared<SplitLocalFileLiteral>( |
| rewrite_info.file_type, file_field, original_literal->data_type(), original_field); |
| rewrite_context->add_created_expr(literal); |
| return literal; |
| } |
| |
| static bool rewrite_binary_slot_literal_predicate( |
| const VExprSPtr& expr, |
| const std::map<GlobalIndex, FileSlotRewriteInfo>& global_to_file_slot, |
| RewriteContext* rewrite_context) { |
| if (!is_binary_comparison_predicate(expr)) { |
| return false; |
| } |
| auto children = expr->children(); |
| const VSlotRef* slot_ref = nullptr; |
| const FileSlotRewriteInfo* rewrite_info = |
| find_slot_rewrite_info(children[0], global_to_file_slot, &slot_ref); |
| int slot_child_idx = 0; |
| int literal_child_idx = 1; |
| if (rewrite_info == nullptr) { |
| rewrite_info = find_slot_rewrite_info(children[1], global_to_file_slot, &slot_ref); |
| slot_child_idx = 1; |
| literal_child_idx = 0; |
| } |
| if (rewrite_info == nullptr || slot_ref == nullptr) { |
| return false; |
| } |
| auto literal_expr = |
| unwrap_literal_for_file_cast(children[literal_child_idx], rewrite_info->table_type); |
| if (literal_expr == nullptr) { |
| return false; |
| } |
| |
| auto rewritten_literal = |
| rewrite_literal_to_file_type(literal_expr, *rewrite_info, rewrite_context); |
| if (rewritten_literal == nullptr) { |
| children[literal_child_idx] = original_table_literal(literal_expr, rewrite_context); |
| expr->set_children(std::move(children)); |
| return false; |
| } |
| |
| children[slot_child_idx] = create_file_slot_ref(*slot_ref, *rewrite_info, rewrite_context); |
| children[literal_child_idx] = std::move(rewritten_literal); |
| expr->set_children(std::move(children)); |
| return true; |
| } |
| |
| static bool rewrite_in_slot_literal_predicate( |
| const VExprSPtr& expr, |
| const std::map<GlobalIndex, FileSlotRewriteInfo>& global_to_file_slot, |
| RewriteContext* rewrite_context) { |
| if (expr->node_type() != TExprNodeType::IN_PRED || expr->get_num_children() < 2) { |
| return false; |
| } |
| auto children = expr->children(); |
| const VSlotRef* slot_ref = nullptr; |
| const FileSlotRewriteInfo* rewrite_info = |
| find_slot_rewrite_info(children[0], global_to_file_slot, &slot_ref); |
| if (rewrite_info == nullptr || slot_ref == nullptr) { |
| return false; |
| } |
| |
| VExprSPtrs rewritten_literals; |
| rewritten_literals.reserve(children.size() - 1); |
| for (size_t child_idx = 1; child_idx < children.size(); ++child_idx) { |
| auto literal_expr = |
| unwrap_literal_for_file_cast(children[child_idx], rewrite_info->table_type); |
| if (literal_expr == nullptr) { |
| return false; |
| } |
| auto rewritten_literal = |
| rewrite_literal_to_file_type(literal_expr, *rewrite_info, rewrite_context); |
| if (rewritten_literal == nullptr) { |
| for (size_t restore_idx = 1; restore_idx < children.size(); ++restore_idx) { |
| auto restore_literal = unwrap_literal_for_file_cast(children[restore_idx], |
| rewrite_info->table_type); |
| if (restore_literal != nullptr) { |
| children[restore_idx] = |
| original_table_literal(restore_literal, rewrite_context); |
| } |
| } |
| expr->set_children(std::move(children)); |
| return false; |
| } |
| rewritten_literals.push_back(std::move(rewritten_literal)); |
| } |
| |
| children[0] = create_file_slot_ref(*slot_ref, *rewrite_info, rewrite_context); |
| for (size_t literal_idx = 0; literal_idx < rewritten_literals.size(); ++literal_idx) { |
| children[literal_idx + 1] = std::move(rewritten_literals[literal_idx]); |
| } |
| expr->set_children(std::move(children)); |
| return true; |
| } |
| |
| static VExprSPtr create_file_struct_child_name_literal(const std::string& file_child_name, |
| RewriteContext* rewrite_context) { |
| auto literal = VLiteral::create_shared(std::make_shared<DataTypeString>(), |
| Field::create_field<TYPE_STRING>(file_child_name)); |
| rewrite_context->add_created_expr(literal); |
| return literal; |
| } |
| |
| static bool needs_complex_file_slot_cast(const DataTypePtr& file_type, |
| const DataTypePtr& table_type) { |
| if (file_type == nullptr || table_type == nullptr || file_type->equals(*table_type)) { |
| return false; |
| } |
| const auto file_nested_type = remove_nullable(file_type); |
| const auto table_nested_type = remove_nullable(table_type); |
| if (file_nested_type->equals(*table_nested_type)) { |
| return false; |
| } |
| return is_complex_type(file_nested_type->get_primitive_type()) || |
| is_complex_type(table_nested_type->get_primitive_type()); |
| } |
| |
| static bool collect_struct_element_chain(const VExprSPtr& expr, std::vector<VExprSPtr>* chain) { |
| DORIS_CHECK(chain != nullptr); |
| if (!is_struct_element_expr(expr)) { |
| return false; |
| } |
| const auto& parent = expr->children()[0]; |
| if (is_struct_element_expr(parent)) { |
| if (!collect_struct_element_chain(parent, chain)) { |
| return false; |
| } |
| } else if (!parent->is_slot_ref()) { |
| // Only support file-local rewrite for struct child chains rooted directly at a top-level |
| // slot, for example `element_at(s, 'a')` or `element_at(element_at(s, 'a'), 'b')`. |
| // |
| // Do not localize computed complex parents such as |
| // `element_at(element_at(map_values(m), 1), 'full_name')`. The intermediate map/array |
| // result has already been reshaped by scan projection and may have a different child order |
| // from the table expression. Partially rewriting that expression against the file block can |
| // silently evaluate the wrong struct child and filter out valid rows. Those predicates must |
| // remain as table-level conjuncts and be evaluated after TableReader materialization. |
| return false; |
| } |
| chain->push_back(expr); |
| return true; |
| } |
| |
| static bool can_filter_before_table_nullability_alignment(const DataTypePtr& file_type, |
| const DataTypePtr& table_type) { |
| DORIS_CHECK(file_type != nullptr); |
| DORIS_CHECK(table_type != nullptr); |
| // File-local conjuncts run before TableReader validates the materialized table schema. A |
| // nullable file value mapped to a required table value must therefore reach |
| // _align_column_nullability(). For example, with file STRUCT<a: Nullable(INT)>, table |
| // STRUCT<a: BIGINT>, rows [NULL, 20], and `s.a > 10`, filtering in the file domain would drop |
| // NULL first and hide the table-contract violation. The reverse direction is safe: a required |
| // file value can always be wrapped as a nullable table value after filtering. |
| return !file_type->is_nullable() || table_type->is_nullable(); |
| } |
| |
| static bool rewrite_struct_element_path_to_file_expr( |
| const VExprSPtr& expr, const std::vector<ColumnMapping>& mappings, |
| const std::map<GlobalIndex, FileSlotRewriteInfo>& global_to_file_slot, |
| RewriteContext* rewrite_context) { |
| ResolvedNestedStructPath resolved; |
| if (!resolve_nested_struct_expr_for_file(expr, mappings, &resolved)) { |
| return false; |
| } |
| |
| std::vector<VExprSPtr> struct_element_chain; |
| if (!collect_struct_element_chain(expr, &struct_element_chain) || |
| struct_element_chain.size() != resolved.file_child_names.size() || |
| struct_element_chain.size() != resolved.file_child_types.size()) { |
| return false; |
| } |
| |
| auto root_children = struct_element_chain.front()->children(); |
| if (!root_children[0]->is_slot_ref()) { |
| return false; |
| } |
| const auto* slot_ref = assert_cast<const VSlotRef*>(root_children[0].get()); |
| const auto rewrite_it = global_to_file_slot.find(slot_ref_global_index(*slot_ref)); |
| if (rewrite_it == global_to_file_slot.end()) { |
| return false; |
| } |
| |
| // Check every value-producing level, including the root struct. A nullable parent also makes |
| // a child access nullable even when the child type itself is required, so checking only the |
| // final leaf is insufficient. If any file level is more nullable than its table counterpart, |
| // keep the complete predicate above TableReader so schema validation observes all NULLs before |
| // row filtering. |
| if (!can_filter_before_table_nullability_alignment(rewrite_it->second.file_type, |
| rewrite_it->second.table_type)) { |
| return false; |
| } |
| for (size_t idx = 0; idx < struct_element_chain.size(); ++idx) { |
| if (!can_filter_before_table_nullability_alignment( |
| resolved.file_child_types[idx], struct_element_chain[idx]->data_type())) { |
| return false; |
| } |
| } |
| |
| // File-local conjuncts are prepared against the file-reader Block, so both the root slot and |
| // every struct selector must be expressed in file schema terms. For a renamed Iceberg field, |
| // keeping the table selector would prepare `element_at(file_struct<rename>, 'renamed')` and |
| // fail before any rows are read. Rewrite the whole chain while ColumnMapping still preserves |
| // the table-to-file relationship. Example: |
| // table filter: element_at(element_at(s, 'renamed_parent'), 'renamed_leaf') |
| // old file: s<parent<leaf>> |
| // file filter: element_at(element_at(s, 'parent'), 'leaf') |
| root_children[0] = create_file_slot_ref(*slot_ref, rewrite_it->second, rewrite_context); |
| struct_element_chain.front()->set_children(std::move(root_children)); |
| for (size_t idx = 0; idx < struct_element_chain.size(); ++idx) { |
| auto children = struct_element_chain[idx]->children(); |
| children[1] = create_file_struct_child_name_literal(resolved.file_child_names[idx], |
| rewrite_context); |
| struct_element_chain[idx]->set_children(std::move(children)); |
| // The selector name and the expression return type must be moved to file schema together. |
| // Example: |
| // table filter: element_at(element_at(s, 'new_a'), 'new_aa') = 50 |
| // old file: s.new_a STRUCT<aa, bb> |
| // file filter: element_at(element_at(s, 'new_a'), 'aa') = 50 |
| // |
| // If the inner element_at keeps the table return type STRUCT<new_aa, bb>, preparing the |
| // outer element_at(..., 'aa') fails before scanning because `aa` is not a table field. |
| struct_element_chain[idx]->data_type() = resolved.file_child_types[idx]; |
| } |
| return true; |
| } |
| |
| static VExprSPtr cast_file_expr_to_table_type(const VExprSPtr& file_expr, |
| const DataTypePtr& table_type, |
| RewriteContext* rewrite_context) { |
| DORIS_CHECK(file_expr != nullptr); |
| DORIS_CHECK(table_type != nullptr); |
| DORIS_CHECK(rewrite_context != nullptr); |
| auto cast_expr = Cast::create_shared(table_type); |
| cast_expr->add_child(file_expr); |
| rewrite_context->add_created_expr(cast_expr); |
| return cast_expr; |
| } |
| |
| // Prefer comparing in the physical file leaf type when a table predicate uses a promoted struct |
| // child. For example, with table STRUCT<a: BIGINT>, old-file STRUCT<a: INT>, and `s.a = 10`, the |
| // localized predicate should be `file_s.a::INT = 10::INT`, not |
| // `CAST(file_s.a::INT AS BIGINT) = 10::BIGINT`. Converting one literal avoids a cast for every row. |
| // |
| // This rewrite is valid only when every possible file value survives file-to-table conversion and |
| // the particular literal survives a table-to-file-to-table round trip. A value such as BIGINT |
| // 2147483648 cannot be represented by an INT file leaf, so that case deliberately falls back to |
| // `CAST(file_s.a AS BIGINT) = 2147483648`, which preserves the original table-level semantics. |
| static bool rewrite_binary_struct_literal_predicate( |
| const VExprSPtr& expr, const std::vector<ColumnMapping>& filter_mappings, |
| const std::map<GlobalIndex, FileSlotRewriteInfo>& global_to_file_slot, |
| RewriteContext* rewrite_context, bool* can_localize) { |
| DORIS_CHECK(can_localize != nullptr); |
| if (!is_binary_comparison_predicate(expr)) { |
| return false; |
| } |
| auto children = expr->children(); |
| int struct_child_idx = -1; |
| int literal_child_idx = -1; |
| if (is_struct_element_expr(children[0])) { |
| struct_child_idx = 0; |
| literal_child_idx = 1; |
| } else if (is_struct_element_expr(children[1])) { |
| struct_child_idx = 1; |
| literal_child_idx = 0; |
| } else { |
| return false; |
| } |
| |
| const auto table_leaf_type = children[struct_child_idx]->data_type(); |
| DORIS_CHECK(table_leaf_type != nullptr); |
| auto table_literal = unwrap_literal_for_file_cast(children[literal_child_idx], table_leaf_type); |
| if (table_literal == nullptr || |
| !rewrite_struct_element_path_to_file_expr(children[struct_child_idx], filter_mappings, |
| global_to_file_slot, rewrite_context)) { |
| return false; |
| } |
| |
| const auto file_leaf_type = children[struct_child_idx]->data_type(); |
| DORIS_CHECK(file_leaf_type != nullptr); |
| const FileSlotRewriteInfo leaf_rewrite_info { |
| .block_position = 0, |
| .file_type = file_leaf_type, |
| .table_type = table_leaf_type, |
| .file_column_name = {}, |
| }; |
| auto file_literal = |
| rewrite_literal_to_file_type(table_literal, leaf_rewrite_info, rewrite_context); |
| if (file_literal != nullptr) { |
| children[literal_child_idx] = std::move(file_literal); |
| } else { |
| if (!is_lossless_file_to_table_numeric_cast(file_leaf_type, table_leaf_type)) { |
| // A narrowing or otherwise lossy cast can fail or produce NULL while TableReader |
| // materializes the table schema. Evaluating it here could filter the offending row |
| // before that validation, so keep the complete predicate above TableReader. |
| *can_localize = false; |
| return true; |
| } |
| children[struct_child_idx] = cast_file_expr_to_table_type(children[struct_child_idx], |
| table_leaf_type, rewrite_context); |
| children[literal_child_idx] = original_table_literal(table_literal, rewrite_context); |
| } |
| expr->set_children(std::move(children)); |
| return true; |
| } |
| |
| // IN must use one comparison type for its probe and every candidate. Rewrite the complete literal |
| // set only when all values are exactly representable in the file leaf type; one unsafe value makes |
| // the whole predicate fall back to a table-type cast. For example, an INT file leaf can evaluate |
| // `BIGINT IN (10, 20)` as `INT IN (10, 20)`, but `BIGINT IN (10, 2147483648)` must stay BIGINT. |
| static bool rewrite_in_struct_literal_predicate( |
| const VExprSPtr& expr, const std::vector<ColumnMapping>& filter_mappings, |
| const std::map<GlobalIndex, FileSlotRewriteInfo>& global_to_file_slot, |
| RewriteContext* rewrite_context, bool* can_localize) { |
| DORIS_CHECK(can_localize != nullptr); |
| if (expr->node_type() != TExprNodeType::IN_PRED || expr->get_num_children() < 2 || |
| !is_struct_element_expr(expr->children()[0])) { |
| return false; |
| } |
| auto children = expr->children(); |
| const auto table_leaf_type = children[0]->data_type(); |
| DORIS_CHECK(table_leaf_type != nullptr); |
| VExprSPtrs table_literals; |
| table_literals.reserve(children.size() - 1); |
| for (size_t child_idx = 1; child_idx < children.size(); ++child_idx) { |
| auto table_literal = unwrap_literal_for_file_cast(children[child_idx], table_leaf_type); |
| if (table_literal == nullptr) { |
| return false; |
| } |
| table_literals.push_back(std::move(table_literal)); |
| } |
| if (!rewrite_struct_element_path_to_file_expr(children[0], filter_mappings, global_to_file_slot, |
| rewrite_context)) { |
| return false; |
| } |
| |
| const auto file_leaf_type = children[0]->data_type(); |
| DORIS_CHECK(file_leaf_type != nullptr); |
| const FileSlotRewriteInfo leaf_rewrite_info { |
| .block_position = 0, |
| .file_type = file_leaf_type, |
| .table_type = table_leaf_type, |
| .file_column_name = {}, |
| }; |
| VExprSPtrs file_literals; |
| file_literals.reserve(table_literals.size()); |
| for (const auto& table_literal : table_literals) { |
| auto file_literal = |
| rewrite_literal_to_file_type(table_literal, leaf_rewrite_info, rewrite_context); |
| if (file_literal == nullptr) { |
| if (!is_lossless_file_to_table_numeric_cast(file_leaf_type, table_leaf_type)) { |
| *can_localize = false; |
| return true; |
| } |
| children[0] = |
| cast_file_expr_to_table_type(children[0], table_leaf_type, rewrite_context); |
| for (size_t literal_idx = 0; literal_idx < table_literals.size(); ++literal_idx) { |
| children[literal_idx + 1] = |
| original_table_literal(table_literals[literal_idx], rewrite_context); |
| } |
| expr->set_children(std::move(children)); |
| return true; |
| } |
| file_literals.push_back(std::move(file_literal)); |
| } |
| |
| for (size_t literal_idx = 0; literal_idx < file_literals.size(); ++literal_idx) { |
| children[literal_idx + 1] = std::move(file_literals[literal_idx]); |
| } |
| expr->set_children(std::move(children)); |
| return true; |
| } |
| |
| static VExprSPtr rewrite_struct_or_slot_expr_to_file_expr( |
| const VExprSPtr& expr, |
| const std::map<GlobalIndex, FileSlotRewriteInfo>& global_to_file_slot, |
| const std::vector<ColumnMapping>& filter_mappings, RewriteContext* rewrite_context, |
| bool* can_localize) { |
| if (is_struct_element_expr(expr)) { |
| const auto table_leaf_type = expr->data_type(); |
| if (!rewrite_struct_element_path_to_file_expr(expr, filter_mappings, global_to_file_slot, |
| rewrite_context)) { |
| // The scanner still evaluates the original table-level conjunct after TableReader |
| // finalizes the output block. Skipping an unlocalizable file conjunct is therefore |
| // safer than preparing a partially rewritten expression against the wrong struct |
| // layout. In particular, do not generate file-local conjuncts for computed complex |
| // parents such as `element_at(element_at(map_values(m), 1), 'field')`; only direct |
| // slot-rooted struct chains are supported here. |
| *can_localize = false; |
| return expr; |
| } |
| DORIS_CHECK(table_leaf_type != nullptr); |
| DORIS_CHECK(expr->data_type() != nullptr); |
| if (!expr->data_type()->equals(*table_leaf_type)) { |
| if (!is_lossless_file_to_table_numeric_cast(expr->data_type(), table_leaf_type)) { |
| *can_localize = false; |
| return expr; |
| } |
| // Path localization changes the leaf to the physical file type. For example, after an |
| // Iceberg evolution from STRUCT<a: INT> to STRUCT<a: BIGINT>, the localized old-file |
| // predicate is initially `element_at(file_col, 'a')::INT = 10::BIGINT`. Cast only the |
| // leaf back to BIGINT so the comparison has matching operands without forcing a cast |
| // of the entire evolved struct (whose children may also have been added or reordered). |
| return cast_file_expr_to_table_type(expr, table_leaf_type, rewrite_context); |
| } |
| return expr; |
| } |
| |
| DORIS_CHECK(expr->is_slot_ref()); |
| const auto* slot_ref = assert_cast<const VSlotRef*>(expr.get()); |
| const auto rewrite_it = global_to_file_slot.find(slot_ref_global_index(*slot_ref)); |
| if (rewrite_it == global_to_file_slot.end()) { |
| return expr; |
| } |
| const auto& rewrite_info = rewrite_it->second; |
| auto file_slot = create_file_slot_ref(*slot_ref, rewrite_info, rewrite_context); |
| if (rewrite_info.file_type->equals(*rewrite_info.table_type)) { |
| return file_slot; |
| } |
| if (needs_complex_file_slot_cast(rewrite_info.file_type, rewrite_info.table_type)) { |
| // Generic file-local expressions cannot safely cast an evolved complex file slot back to |
| // the table type. For example, ARRAY_CONTAINS(MAP_KEYS(m), 'person5') only reads map keys, |
| // but CAST(file_m AS table_m) first forces an incompatible old value struct into the new |
| // layout. Keep such predicates at table level, after TableReader materializes evolution. |
| *can_localize = false; |
| return expr; |
| } |
| return cast_file_expr_to_table_type(file_slot, rewrite_info.table_type, rewrite_context); |
| } |
| |
| static VExprSPtr rewrite_table_expr_to_file_expr( |
| const VExprSPtr& expr, |
| const std::map<GlobalIndex, FileSlotRewriteInfo>& global_to_file_slot, |
| const std::vector<ColumnMapping>& filter_mappings, RewriteContext* rewrite_context, |
| bool* can_localize) { |
| if (expr == nullptr) { |
| return nullptr; |
| } |
| DORIS_CHECK(rewrite_context != nullptr); |
| DORIS_CHECK(can_localize != nullptr); |
| if (auto* runtime_filter = dynamic_cast<RuntimeFilterExpr*>(expr.get()); |
| runtime_filter != nullptr) { |
| auto impl = runtime_filter->get_impl(); |
| if (impl == nullptr) { |
| *can_localize = false; |
| return expr; |
| } |
| auto localized_impl = rewrite_table_expr_to_file_expr( |
| impl, global_to_file_slot, filter_mappings, rewrite_context, can_localize); |
| if (!*can_localize) { |
| return expr; |
| } |
| runtime_filter->set_impl(std::move(localized_impl)); |
| return expr; |
| } |
| if (rewrite_binary_slot_literal_predicate(expr, global_to_file_slot, rewrite_context)) { |
| return expr; |
| } |
| if (rewrite_in_slot_literal_predicate(expr, global_to_file_slot, rewrite_context)) { |
| return expr; |
| } |
| if (rewrite_binary_struct_literal_predicate(expr, filter_mappings, global_to_file_slot, |
| rewrite_context, can_localize)) { |
| return expr; |
| } |
| if (rewrite_in_struct_literal_predicate(expr, filter_mappings, global_to_file_slot, |
| rewrite_context, can_localize)) { |
| return expr; |
| } |
| if (is_struct_element_expr(expr) || expr->is_slot_ref()) { |
| return rewrite_struct_or_slot_expr_to_file_expr(expr, global_to_file_slot, filter_mappings, |
| rewrite_context, can_localize); |
| } |
| // The input is a split-local cloned tree. A previous split-local clone may already have |
| // inserted Cast(slot). Keep that rewrite idempotent: rewrite the cast child from table slot to |
| // the current split's file slot, and drop the cast when the current split no longer needs it. |
| if (is_cast_expr(expr) && expr->get_num_children() == 1) { |
| const auto& child = expr->children()[0]; |
| if (child->is_slot_ref()) { |
| const auto* slot_ref = assert_cast<const VSlotRef*>(child.get()); |
| const auto rewrite_it = global_to_file_slot.find(slot_ref_global_index(*slot_ref)); |
| if (rewrite_it != global_to_file_slot.end() && |
| expr->data_type()->equals(*rewrite_it->second.table_type)) { |
| auto rewritten_child = |
| create_file_slot_ref(*slot_ref, rewrite_it->second, rewrite_context); |
| if (rewrite_it->second.file_type->equals(*rewrite_it->second.table_type)) { |
| return rewritten_child; |
| } |
| if (needs_complex_file_slot_cast(rewrite_it->second.file_type, |
| rewrite_it->second.table_type)) { |
| *can_localize = false; |
| return expr; |
| } |
| expr->set_children({std::move(rewritten_child)}); |
| return expr; |
| } |
| } |
| } |
| |
| VExprSPtrs rewritten_children; |
| rewritten_children.reserve(expr->children().size()); |
| for (const auto& child : expr->children()) { |
| rewritten_children.push_back(rewrite_table_expr_to_file_expr( |
| child, global_to_file_slot, filter_mappings, rewrite_context, can_localize)); |
| } |
| expr->set_children(std::move(rewritten_children)); |
| return expr; |
| } |
| |
| static constexpr const char* ROW_LINEAGE_ROW_ID = "_row_id"; |
| static constexpr const char* ROW_LINEAGE_LAST_UPDATED_SEQ_NUMBER = "_last_updated_sequence_number"; |
| static constexpr int32_t ROW_LINEAGE_ROW_ID_FIELD_ID = 2147483540; |
| static constexpr int32_t ROW_LINEAGE_LAST_UPDATED_SEQ_NUMBER_FIELD_ID = 2147483539; |
| |
| static TableVirtualColumnType row_lineage_virtual_column_type(const std::string& column_name) { |
| if (column_name == ROW_LINEAGE_ROW_ID) { |
| return TableVirtualColumnType::ROW_ID; |
| } |
| if (column_name == ROW_LINEAGE_LAST_UPDATED_SEQ_NUMBER) { |
| return TableVirtualColumnType::LAST_UPDATED_SEQUENCE_NUMBER; |
| } |
| return TableVirtualColumnType::INVALID; |
| } |
| |
| static TableVirtualColumnType row_lineage_virtual_column_type_by_field_id( |
| const ColumnDefinition& column) { |
| if (!column.has_identifier_field_id()) { |
| return TableVirtualColumnType::INVALID; |
| } |
| switch (column.get_identifier_field_id()) { |
| case ROW_LINEAGE_ROW_ID_FIELD_ID: |
| return TableVirtualColumnType::ROW_ID; |
| case ROW_LINEAGE_LAST_UPDATED_SEQ_NUMBER_FIELD_ID: |
| return TableVirtualColumnType::LAST_UPDATED_SEQUENCE_NUMBER; |
| default: |
| return TableVirtualColumnType::INVALID; |
| } |
| } |
| |
| static TableVirtualColumnType row_lineage_virtual_column_type(const ColumnDefinition& column, |
| TableColumnMappingMode mode) { |
| switch (mode) { |
| case TableColumnMappingMode::BY_FIELD_ID: |
| return row_lineage_virtual_column_type_by_field_id(column); |
| case TableColumnMappingMode::BY_NAME: |
| case TableColumnMappingMode::BY_INDEX: |
| return row_lineage_virtual_column_type(column.name); |
| } |
| return TableVirtualColumnType::INVALID; |
| } |
| |
| // Returns true when the current file type is not the exact nested type the scan should expose. |
| // This is about building the projected file-side type/projection, not about whether TableReader |
| // later needs to rematerialize the complex value back to table layout. |
| static bool needs_projected_file_type_rebuild(const ColumnMapping& mapping) { |
| if (!is_complex_type(mapping.file_type->get_primitive_type())) { |
| return false; |
| } |
| if (mapping.child_mappings.empty()) { |
| return false; |
| } |
| DORIS_CHECK(mapping.file_type != nullptr); |
| DORIS_CHECK(mapping.table_type != nullptr); |
| if (remove_nullable(mapping.file_type)->get_primitive_type() != |
| remove_nullable(mapping.table_type)->get_primitive_type()) { |
| return true; |
| } |
| if (!mapping.table_type->equals(*mapping.file_type)) { |
| return true; |
| } |
| for (const auto& child_mapping : mapping.child_mappings) { |
| // Rename-only child mappings do not change the file-side projected shape. If field-id |
| // matching maps table child `renamed_b` to file child `b`, the file reader can still expose |
| // the original file type as long as child count/order/types are unchanged. |
| if (!child_mapping.file_local_id.has_value() || |
| needs_projected_file_type_rebuild(child_mapping)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| static std::optional<size_t> file_child_ordinal_in_scan_type(const ColumnMapping& mapping, |
| const ColumnMapping& child_mapping) { |
| if (!child_mapping.file_local_id.has_value()) { |
| return std::nullopt; |
| } |
| const auto& file_children = !mapping.projected_file_children.empty() |
| ? mapping.projected_file_children |
| : mapping.original_file_children; |
| const auto child_it = std::ranges::find_if(file_children, [&](const ColumnDefinition& child) { |
| return child.file_local_id() == *child_mapping.file_local_id; |
| }); |
| if (child_it == file_children.end()) { |
| return std::nullopt; |
| } |
| return static_cast<size_t>(std::distance(file_children.begin(), child_it)); |
| } |
| |
| static bool needs_complex_rematerialize(const ColumnMapping& mapping) { |
| if (mapping.child_mappings.empty()) { |
| return false; |
| } |
| if (mapping.table_type == nullptr || mapping.file_type == nullptr || |
| !mapping.table_type->equals(*mapping.file_type)) { |
| return true; |
| } |
| for (size_t table_child_idx = 0; table_child_idx < mapping.child_mappings.size(); |
| ++table_child_idx) { |
| const auto& child_mapping = mapping.child_mappings[table_child_idx]; |
| const auto file_child_idx = file_child_ordinal_in_scan_type(mapping, child_mapping); |
| if (!file_child_idx.has_value() || *file_child_idx != table_child_idx || |
| needs_complex_rematerialize(child_mapping) || |
| (child_mapping.table_type != nullptr && child_mapping.file_type != nullptr && |
| !child_mapping.table_type->equals(*child_mapping.file_type))) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| static bool mapping_can_use_file_column_directly(const ColumnMapping& mapping) { |
| if (mapping.table_type == nullptr || mapping.file_type == nullptr) { |
| return false; |
| } |
| const auto table_type = remove_nullable(mapping.table_type); |
| const auto file_type = remove_nullable(mapping.file_type); |
| const bool same_timestamptz_with_different_scale = |
| table_type->get_primitive_type() == TYPE_TIMESTAMPTZ && |
| file_type->get_primitive_type() == TYPE_TIMESTAMPTZ; |
| if (!mapping.table_type->equals(*mapping.file_type) && !same_timestamptz_with_different_scale) { |
| return false; |
| } |
| return !needs_complex_rematerialize(mapping); |
| } |
| |
| static bool type_contains_varbinary(const DataTypePtr& type) { |
| DORIS_CHECK(type != nullptr); |
| const auto nested_type = remove_nullable(type); |
| switch (nested_type->get_primitive_type()) { |
| case TYPE_VARBINARY: |
| return true; |
| case TYPE_ARRAY: |
| return type_contains_varbinary( |
| assert_cast<const DataTypeArray&>(*nested_type).get_nested_type()); |
| case TYPE_MAP: { |
| const auto& map_type = assert_cast<const DataTypeMap&>(*nested_type); |
| return type_contains_varbinary(map_type.get_key_type()) || |
| type_contains_varbinary(map_type.get_value_type()); |
| } |
| case TYPE_STRUCT: |
| return std::ranges::any_of( |
| assert_cast<const DataTypeStruct&>(*nested_type).get_elements(), |
| [](const DataTypePtr& child_type) { return type_contains_varbinary(child_type); }); |
| default: |
| return false; |
| } |
| } |
| |
| static FilterConversionType direct_filter_conversion(const ColumnMapping& mapping) { |
| DORIS_CHECK(mapping.table_type != nullptr); |
| DORIS_CHECK(mapping.file_type != nullptr); |
| // FileScanOperator deliberately keeps VARBINARY predicates above external readers. Their |
| // physical binary representations are not uniformly supported by reader-side expression and |
| // metadata filtering, so localizing a late runtime filter here can incorrectly reject rows. |
| // Apply the same rule to a complex root because generic array/map/struct expressions rewrite |
| // the root slot and can otherwise expose a nested VARBINARY child to the reader. |
| if (type_contains_varbinary(mapping.table_type)) { |
| return FilterConversionType::FINALIZE_ONLY; |
| } |
| const auto table_type = remove_nullable(mapping.table_type); |
| const auto file_type = remove_nullable(mapping.file_type); |
| // TIMESTAMPTZ scale mismatch is intentionally materialized as pass-through: a SQL cast rounds |
| // fractional seconds. A file-local cast would therefore filter different instants from the |
| // scanner-level predicate evaluated on the pass-through value. |
| if (table_type->get_primitive_type() == TYPE_TIMESTAMPTZ && |
| file_type->get_primitive_type() == TYPE_TIMESTAMPTZ && |
| !mapping.table_type->equals(*mapping.file_type)) { |
| return FilterConversionType::FINALIZE_ONLY; |
| } |
| return mapping.is_trivial ? FilterConversionType::COPY_DIRECTLY |
| : FilterConversionType::CAST_FILTER; |
| } |
| |
| static FilterConversionType projected_filter_conversion(const ColumnMapping& mapping) { |
| const auto conversion = direct_filter_conversion(mapping); |
| return !mapping.is_trivial && conversion != FilterConversionType::FINALIZE_ONLY |
| ? FilterConversionType::READER_EXPRESSION |
| : conversion; |
| } |
| |
| static const ColumnDefinition* find_file_child_for_mapping(const ColumnDefinition& table_child, |
| const ColumnDefinition& file_parent, |
| TableColumnMappingMode mode, |
| size_t table_child_idx, |
| bool allow_ordinal_fallback) { |
| const auto file_parent_type = remove_nullable(file_parent.type)->get_primitive_type(); |
| switch (file_parent_type) { |
| case TYPE_ARRAY: |
| DORIS_CHECK(file_parent.children.size() == 1); |
| return &file_parent.children[0]; |
| case TYPE_MAP: |
| DORIS_CHECK(file_parent.children.size() == 2); |
| if (table_child.name == "key") { |
| return &file_parent.children[0]; |
| } |
| if (table_child.name == "value") { |
| return &file_parent.children[1]; |
| } |
| if (table_child.local_id == 0 || table_child.local_id == 1) { |
| return &file_parent.children[table_child.local_id]; |
| } |
| return nullptr; |
| default: |
| // Hive BY_INDEX is a top-level column matching rule. Once a complex root is selected by |
| // file position, nested struct children follow Hive reader's historical name matching |
| // semantics; their integer identifiers can be field ids, not file positions. |
| const auto nested_mode = |
| mode == TableColumnMappingMode::BY_INDEX ? TableColumnMappingMode::BY_NAME : mode; |
| if (const auto* file_child = |
| matcher_for_mode(nested_mode).find(table_child, file_parent.children); |
| file_child != nullptr) { |
| return file_child; |
| } |
| if (allow_ordinal_fallback && mode == TableColumnMappingMode::BY_FIELD_ID && |
| !table_child.has_identifier_field_id()) { |
| // Synthetic children are derived from the table DataType when nested ColumnDefinition |
| // metadata has been pruned away. They do not carry Iceberg field ids, so try a name |
| // match before falling back to ordinal order. Example: |
| // table value type: Struct(age, full_name, gender) |
| // old file value: Struct(name, age) |
| // Name matching keeps `age -> age`; the later unused-child fallback can then map the |
| // renamed `full_name -> name` instead of consuming `age` twice. |
| if (const auto* file_child = NameMatcher().find(table_child, file_parent.children); |
| file_child != nullptr) { |
| return file_child; |
| } |
| } |
| // Some callers only carry the full complex DataType for a projected table column, without |
| // expanded nested ColumnDefinitions. In that case we can still preserve full materialization |
| // by walking table/file struct fields by ordinal. This is a fallback only: explicit |
| // ColumnDefinition children keep using the requested table-format matching rule, which is |
| // required for precise schema evolution. |
| if (allow_ordinal_fallback && table_child_idx < file_parent.children.size()) { |
| return &file_parent.children[table_child_idx]; |
| } |
| return nullptr; |
| } |
| } |
| |
| static ColumnDefinition synthetic_child_definition(const std::string& name, DataTypePtr type, |
| int32_t local_id) { |
| ColumnDefinition child; |
| child.identifier = Field::create_field<TYPE_STRING>(name); |
| child.local_id = local_id; |
| child.name = name; |
| child.type = std::move(type); |
| return child; |
| } |
| |
| static std::vector<ColumnDefinition> synthesize_complex_children_from_type( |
| const DataTypePtr& type) { |
| std::vector<ColumnDefinition> children; |
| if (type == nullptr) { |
| return children; |
| } |
| const auto nested_type = remove_nullable(type); |
| switch (nested_type->get_primitive_type()) { |
| case TYPE_ARRAY: { |
| const auto* array_type = assert_cast<const DataTypeArray*>(nested_type.get()); |
| children.push_back(synthetic_child_definition("element", array_type->get_nested_type(), 0)); |
| break; |
| } |
| case TYPE_MAP: { |
| const auto* map_type = assert_cast<const DataTypeMap*>(nested_type.get()); |
| children.push_back(synthetic_child_definition("key", map_type->get_key_type(), 0)); |
| children.push_back(synthetic_child_definition("value", map_type->get_value_type(), 1)); |
| break; |
| } |
| case TYPE_STRUCT: { |
| const auto* struct_type = assert_cast<const DataTypeStruct*>(nested_type.get()); |
| children.reserve(struct_type->get_elements().size()); |
| for (size_t idx = 0; idx < struct_type->get_elements().size(); ++idx) { |
| children.push_back(synthetic_child_definition(struct_type->get_element_name(idx), |
| struct_type->get_element(idx), |
| cast_set<int32_t>(idx))); |
| } |
| break; |
| } |
| default: |
| break; |
| } |
| return children; |
| } |
| |
| static void align_struct_child_types_with_parent(const DataTypePtr& parent_type, |
| std::vector<ColumnDefinition>& children) { |
| const auto nested_parent_type = remove_nullable(parent_type); |
| DORIS_CHECK(nested_parent_type->get_primitive_type() == TYPE_STRUCT); |
| const auto type_children = synthesize_complex_children_from_type(parent_type); |
| for (auto& child : children) { |
| const auto type_child = std::ranges::find_if( |
| type_children, [&](const auto& candidate) { return candidate.name == child.name; }); |
| DORIS_CHECK(type_child != type_children.end()) |
| << "Complex child '" << child.name |
| << "' is absent from its parent table type: " << parent_type->get_name(); |
| // The parent DataType is the authoritative output contract. Nested schema descriptors can |
| // omit child nullability even though the parent struct still declares Nullable(String). |
| // For example, the Iceberg full-schema-change case maps nullable `location` to `city`, but |
| // its child descriptor carries String. Keeping String here makes rematerialization strip |
| // the child's null map and creates Struct(String) under a Struct(Nullable(String)) type. |
| child.type = type_child->type; |
| } |
| } |
| |
| static bool has_table_child_named(const std::vector<ColumnDefinition>& children, |
| std::string_view name) { |
| return std::ranges::any_of(children, [&](const ColumnDefinition& child) { |
| return std::string_view(child.name) == name; |
| }); |
| } |
| |
| static void complete_required_complex_children_from_type(const DataTypePtr& type, |
| std::vector<ColumnDefinition>& children) { |
| if (type == nullptr) { |
| return; |
| } |
| const auto nested_type = remove_nullable(type); |
| switch (nested_type->get_primitive_type()) { |
| case TYPE_MAP: { |
| const auto* map_type = assert_cast<const DataTypeMap*>(nested_type.get()); |
| // MAP key/value are structural children, not independently materializable table fields. |
| // A key-only projection can still be attached to a whole-map output root, for example: |
| // SELECT * FROM t WHERE ARRAY_CONTAINS(MAP_KEYS(new_map_column), 'person5') |
| // |
| // In that shape the scanner keeps the value stream readable, but the table projection can |
| // carry only the key child. Add the missing value child so recursive mapping can evolve the |
| // value type instead of letting TableReader cast old/new value structs directly. |
| if (has_table_child_named(children, "key") && !has_table_child_named(children, "value")) { |
| children.push_back(synthetic_child_definition("value", map_type->get_value_type(), 1)); |
| } |
| break; |
| } |
| case TYPE_ARRAY: |
| // ARRAY has only one required structural child (`element`), so a non-empty projection is |
| // already rooted at the element path. |
| break; |
| case TYPE_STRUCT: |
| // STRUCT children are real fields and must remain prunable. Completing missing struct |
| // fields here would turn `SELECT s.a` into a full-struct read and undo nested projection. |
| break; |
| default: |
| break; |
| } |
| } |
| |
| struct PreparedTableChildren { |
| std::vector<ColumnDefinition> children; |
| bool synthesized_from_type = false; |
| }; |
| |
| static PreparedTableChildren prepare_table_children_for_mapping( |
| const ColumnDefinition& table_column, const DataTypePtr& file_type) { |
| PreparedTableChildren prepared {.children = table_column.children}; |
| const auto nested_table_type = remove_nullable(table_column.type); |
| |
| // Some scan paths, especially SELECT *, only carry the complete complex DataType for a table |
| // column and leave ColumnDefinition::children empty. Synthesize the hierarchy so recursive |
| // mapping can evolve nested fields instead of falling back to an invalid whole-column cast. |
| prepared.synthesized_from_type = prepared.children.empty() && |
| is_complex_type(nested_table_type->get_primitive_type()) && |
| !table_column.type->equals(*file_type); |
| if (prepared.synthesized_from_type) { |
| prepared.children = synthesize_complex_children_from_type(table_column.type); |
| } else if (!prepared.children.empty() && !table_column.type->equals(*file_type)) { |
| complete_required_complex_children_from_type(table_column.type, prepared.children); |
| } |
| |
| if (!prepared.children.empty() && nested_table_type->get_primitive_type() == TYPE_STRUCT) { |
| // Struct children are table fields, so the parent Struct type is authoritative for their |
| // nullability. ARRAY and MAP children are format-level structural wrappers and keep the |
| // descriptor types used by their recursive mappings. |
| align_struct_child_types_with_parent(table_column.type, prepared.children); |
| } |
| return prepared; |
| } |
| |
| static Status validate_file_schema_children(const ColumnDefinition& file_field) { |
| if (file_field.type == nullptr) { |
| return Status::InternalError("File column '{}' has null type", file_field.name); |
| } |
| const auto nested_type = remove_nullable(file_field.type); |
| size_t expected_children = 0; |
| bool complex_with_fixed_children = true; |
| switch (nested_type->get_primitive_type()) { |
| case TYPE_ARRAY: |
| expected_children = 1; |
| break; |
| case TYPE_MAP: |
| expected_children = 2; |
| break; |
| case TYPE_STRUCT: |
| expected_children = |
| assert_cast<const DataTypeStruct*>(nested_type.get())->get_elements().size(); |
| break; |
| default: |
| complex_with_fixed_children = false; |
| break; |
| } |
| if (!complex_with_fixed_children || file_field.children.size() == expected_children) { |
| return Status::OK(); |
| } |
| return Status::InternalError( |
| "Malformed complex file schema for column '{}': type={}, expected_children={}, " |
| "actual_children={}", |
| file_field.name, file_field.type->get_name(), expected_children, |
| file_field.children.size()); |
| } |
| |
| static bool has_projected_file_children(const ColumnMapping& mapping) { |
| if (mapping.original_file_children.empty() || mapping.projected_file_children.empty()) { |
| return false; |
| } |
| if (mapping.original_file_children.size() != mapping.projected_file_children.size()) { |
| return true; |
| } |
| for (size_t idx = 0; idx < mapping.original_file_children.size(); ++idx) { |
| if (mapping.original_file_children[idx].file_local_id() != |
| mapping.projected_file_children[idx].file_local_id()) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| static bool needs_nested_file_projection(const ColumnMapping& mapping) { |
| if (has_projected_file_children(mapping)) { |
| // Return True if the projected child column is missing / re-ordered |
| return true; |
| } |
| return std::ranges::any_of(mapping.child_mappings, [](const ColumnMapping& child_mapping) { |
| return needs_nested_file_projection(child_mapping); |
| }); |
| } |
| |
| static Status build_complex_projection(const ColumnMapping& mapping, LocalColumnIndex* projection); |
| |
| // Build the projected file children/type according to the pruned complex projection. For example, |
| // if we have a struct column `s` with children `id` and `name`, and the projection only keeps |
| // `s.name`, then the file reader should expose `STRUCT<name ...>`. |
| static Status rebuild_projected_file_children_and_type( |
| const DataTypePtr& file_type, const std::vector<ColumnDefinition>& original_file_children, |
| const std::vector<ColumnMapping>& child_mappings, |
| std::vector<ColumnDefinition>* projected_file_children, DataTypePtr* projected_type) { |
| DORIS_CHECK(file_type != nullptr); |
| DORIS_CHECK(projected_file_children != nullptr); |
| DORIS_CHECK(projected_type != nullptr); |
| ColumnDefinition field; |
| field.type = file_type; |
| field.children = original_file_children; |
| LocalColumnIndex projection = LocalColumnIndex::partial_local(-1); |
| projection.children.reserve(child_mappings.size()); |
| for (const auto* child_mapping : present_child_mappings_in_file_order(child_mappings)) { |
| DORIS_CHECK(child_mapping->file_local_id.has_value()); |
| LocalColumnIndex child_projection; |
| RETURN_IF_ERROR(build_complex_projection(*child_mapping, &child_projection)); |
| projection.children.push_back(std::move(child_projection)); |
| } |
| |
| ColumnDefinition projected_field; |
| RETURN_IF_ERROR(project_column_definition(field, projection, &projected_field)); |
| *projected_file_children = std::move(projected_field.children); |
| *projected_type = std::move(projected_field.type); |
| return Status::OK(); |
| } |
| |
| // Build the complex column projection according to the ColumnMapping which is re-ordered by the |
| // file-schema's order. |
| // |
| // For MAP, a partial projection represents value-subtree pruning only. The key child is not a |
| // projected output shape; file readers still read full keys to construct ColumnMap offsets and keep |
| // key semantics unchanged. If a caller tries to project only/prune the key child, the common schema |
| // projection helper rejects it. |
| static Status build_complex_projection(const ColumnMapping& mapping, LocalColumnIndex* projection) { |
| if (projection == nullptr) { |
| return Status::InvalidArgument("projection is null"); |
| } |
| DORIS_CHECK(mapping.file_local_id.has_value()); |
| *projection = LocalColumnIndex::local(*mapping.file_local_id); |
| projection->project_all_children = mapping.child_mappings.empty(); |
| projection->children.clear(); |
| const auto present_children = present_child_mappings_in_file_order(mapping.child_mappings); |
| if (!projection->project_all_children && present_children.empty()) { |
| // All requested table children under this complex node are missing/default-only. The file |
| // reader cannot expose an empty complex projection, but TableReader can still rematerialize |
| // the table shape from a full file subtree and fill the missing children with defaults. |
| projection->project_all_children = true; |
| return Status::OK(); |
| } |
| for (const auto* child_mapping : present_children) { |
| LocalColumnIndex child_projection; |
| RETURN_IF_ERROR(build_complex_projection(*child_mapping, &child_projection)); |
| projection->children.push_back(std::move(child_projection)); |
| } |
| if (!projection->project_all_children && projection->children.empty()) { |
| return Status::NotSupported("Projection for complex column {} contains no file children", |
| mapping.file_column_name); |
| } |
| return Status::OK(); |
| } |
| |
| using FilterProjectionMap = std::map<LocalColumnId, LocalColumnIndex>; |
| |
| // Update the mapping's file type according to the projection, and determine whether the projection |
| // is trivial (i.e. the projected file type is the same as the table type, so no need to |
| // rematerialize the complex value back to table layout after reading from file). |
| static Status apply_projection_to_mapping_file_type(const LocalColumnIndex& projection, |
| ColumnMapping* mapping) { |
| DORIS_CHECK(mapping != nullptr); |
| if (mapping->original_file_type == nullptr) { |
| mapping->original_file_type = mapping->file_type; |
| } |
| if (mapping->original_file_type == nullptr || |
| !is_complex_type(remove_nullable(mapping->original_file_type)->get_primitive_type())) { |
| return Status::OK(); |
| } |
| ColumnDefinition field; |
| field.type = mapping->original_file_type; |
| field.children = mapping->original_file_children; |
| ColumnDefinition projected_field; |
| RETURN_IF_ERROR(project_column_definition(field, projection, &projected_field)); |
| mapping->file_type = std::move(projected_field.type); |
| mapping->projected_file_children = std::move(projected_field.children); |
| mapping->is_trivial = mapping_can_use_file_column_directly(*mapping); |
| return Status::OK(); |
| } |
| |
| static Status merge_filter_projection(const FilterProjectionMap* filter_projections, |
| LocalColumnIndex* projection) { |
| DORIS_CHECK(projection != nullptr); |
| if (filter_projections == nullptr) { |
| return Status::OK(); |
| } |
| const auto filter_projection_it = filter_projections->find(projection->column_id()); |
| if (filter_projection_it == filter_projections->end()) { |
| return Status::OK(); |
| } |
| // Merge predicate-only nested paths into the root projection that is about to be scanned. |
| // Example: `SELECT s.a WHERE s.b > 1` first builds the output projection `s -> a` from |
| // ColumnMapping, while build_nested_struct_filter_projection_map() records `s -> b`. This merge |
| // produces one file scan projection `s -> a,b`. |
| RETURN_IF_ERROR(merge_local_column_index(projection, filter_projection_it->second)); |
| return Status::OK(); |
| } |
| |
| static bool table_root_is_map(const ColumnMapping& mapping) { |
| if (mapping.table_type == nullptr) { |
| return false; |
| } |
| return remove_nullable(mapping.table_type)->get_primitive_type() == TYPE_MAP; |
| } |
| |
| static Status add_scan_column(FileScanRequest* file_request, ColumnMapping* mapping, |
| bool is_predicate_column, bool force_full_complex_scan_projection, |
| const FilterProjectionMap* filter_projections = nullptr) { |
| const auto file_column_id = LocalColumnId(mapping->file_local_id.value()); |
| LocalColumnIndex projection = LocalColumnIndex::top_level(file_column_id); |
| // Columnar readers can turn a complex mapping into a nested file projection, but |
| // row-oriented readers must scan the full top-level complex field because all children are |
| // encoded in the same text cell. |
| if (!force_full_complex_scan_projection && needs_nested_file_projection(*mapping)) { |
| RETURN_IF_ERROR(build_complex_projection(*mapping, &projection)); |
| } |
| if (is_predicate_column && !force_full_complex_scan_projection) { |
| DCHECK(filter_projections != nullptr); |
| // If a projected complex root is also used by a predicate, rebuild the predicate scan |
| // projection from the output mapping before merging predicate-only children. For |
| // `SELECT s.a WHERE s.b > 1`, build_complex_projection() produces `s -> a` and |
| // merge_filter_projection() adds `s -> b`, so the predicate column reads both children. |
| RETURN_IF_ERROR(merge_filter_projection(filter_projections, &projection)); |
| } |
| FileScanRequestBuilder builder(file_request); |
| if (is_predicate_column) { |
| return builder.add_predicate_column(std::move(projection)); |
| } |
| return builder.add_non_predicate_column(std::move(projection)); |
| } |
| |
| static const LocalColumnIndex* find_scan_projection( |
| const std::vector<LocalColumnIndex>& scan_columns, LocalColumnId file_column_id) { |
| const auto projection_it = |
| std::ranges::find_if(scan_columns, [&](const LocalColumnIndex& projection) { |
| return projection.column_id() == file_column_id; |
| }); |
| return projection_it == scan_columns.end() ? nullptr : &*projection_it; |
| } |
| |
| // Apply the final scan projection of one root file column back to its ColumnMapping. This updates |
| // mapping.file_type/projected_file_children from the original file schema to the exact shape that |
| // FileReader will return. |
| // |
| // Example: for `SELECT s.a WHERE s.b > 1`, add_scan_column() keeps only one predicate scan |
| // projection `s -> a,b`. Applying that projection changes the mapping's file type from the full |
| // file struct `s<a,b,c>` to the projected file struct `s<a,b>`, so later filter rewrite and |
| // TableReader final materialization use the same column shape as the file-local block. |
| static Status apply_scan_projection_to_mapping_file_type(const FileScanRequest& file_request, |
| ColumnMapping* mapping) { |
| DORIS_CHECK(mapping != nullptr); |
| DORIS_CHECK(mapping->file_local_id.has_value()); |
| const auto file_column_id = LocalColumnId(*mapping->file_local_id); |
| // Predicate columns are the actual scan projection when a column is used by row-level filters: |
| // add_scan_column() removes the duplicate non-predicate projection in that case. |
| const auto* projection = find_scan_projection(file_request.predicate_columns, file_column_id); |
| if (projection == nullptr) { |
| projection = find_scan_projection(file_request.non_predicate_columns, file_column_id); |
| } |
| DORIS_CHECK(projection != nullptr); |
| return apply_projection_to_mapping_file_type(*projection, mapping); |
| } |
| |
| // Build extra scan projections required only by row-level filters on nested struct children. |
| // |
| // Example: for `SELECT s.a FROM t WHERE s.b.c > 1`, the output projection may only contain `s.a`, |
| // but the file reader must also read `s.b.c` to evaluate the predicate. This function collects the |
| // table-side filter path, resolves it through ColumnMapping first, and records the corresponding |
| // file-side projection in filter_projections. This keeps renamed fields consistent between the scan |
| // projection and row-level conjunct rewrite. Example: |
| // table filter path: s -> renamed_b -> c |
| // old file path: s -> b -> c |
| // recorded path: s -> b -> c |
| // When add_scan_column() adds the same root as a predicate column, it rebuilds that root from the |
| // output mapping, merges this filter-only projection into it, and removes the duplicate |
| // non-predicate root entry. |
| static Status build_nested_struct_filter_projection_map( |
| const std::vector<TableFilter>& table_filters, const std::vector<ColumnMapping>& mappings, |
| FilterProjectionMap* filter_projections) { |
| DORIS_CHECK(filter_projections != nullptr); |
| filter_projections->clear(); |
| for (const auto& table_filter : table_filters) { |
| if (table_filter.conjunct == nullptr) { |
| continue; |
| } |
| // Collect all nested struct paths in the table filter. For example, for |
| // `s.id > 5 AND element_at(s, 'renamed_name') = 'abc'`, collect the table paths |
| // `s -> id` and `s -> renamed_name`, then resolve each one to its file-side projection. |
| std::vector<NestedStructPath> paths; |
| collect_nested_struct_paths(table_filter.conjunct->root(), &paths); |
| for (const auto& path : paths) { |
| auto mapping_it = std::ranges::find_if(mappings, [&](const ColumnMapping& mapping) { |
| return mapping.global_index == path.root_global_index; |
| }); |
| if (mapping_it == mappings.end() || !mapping_it->file_local_id.has_value() || |
| path.selectors.empty()) { |
| continue; |
| } |
| |
| ResolvedNestedStructPath resolved; |
| LocalColumnIndex root_projection; |
| if (!resolve_nested_struct_path_for_file(path, mappings, &resolved)) { |
| if (!table_root_is_map(*mapping_it)) { |
| continue; |
| } |
| // Direct map value filters such as `m.value.a > 1` need the value leaf for row |
| // evaluation even when the query only projects another value child. This is only a |
| // scan projection fallback; complex map/array expressions are still not rewritten |
| // into file-local conjuncts. |
| LocalColumnIndex child_projection; |
| RETURN_IF_ERROR(build_file_child_projection_from_schema( |
| mapping_it->original_file_children, path.selectors, &child_projection)); |
| if (child_projection.local_id() < 0) { |
| continue; |
| } |
| root_projection = LocalColumnIndex::partial_local(*mapping_it->file_local_id); |
| root_projection.children.push_back(std::move(child_projection)); |
| } else { |
| root_projection = std::move(resolved.file_projection); |
| } |
| auto filter_projection_it = filter_projections->find(root_projection.column_id()); |
| if (filter_projection_it == filter_projections->end()) { |
| filter_projections->emplace(root_projection.column_id(), |
| std::move(root_projection)); |
| continue; |
| } |
| RETURN_IF_ERROR( |
| merge_local_column_index(&filter_projection_it->second, root_projection)); |
| } |
| } |
| return Status::OK(); |
| } |
| |
| static void rebuild_projection(ColumnMapping* mapping, LocalIndex block_position) { |
| DORIS_CHECK(mapping->file_local_id.has_value()); |
| if (mapping->is_trivial || needs_complex_rematerialize(*mapping)) { |
| mapping->projection = VExprContext::create_shared(VSlotRef::create_shared( |
| cast_set<int>(block_position.value()), cast_set<int>(block_position.value()), -1, |
| mapping->file_type, mapping->file_column_name)); |
| return; |
| } |
| |
| auto expr = Cast::create_shared(mapping->table_type); |
| expr->add_child(VSlotRef::create_shared(cast_set<int>(block_position.value()), |
| cast_set<int>(block_position.value()), -1, |
| mapping->file_type, mapping->file_column_name)); |
| mapping->projection = VExprContext::create_shared(expr); |
| } |
| |
| // Build file slot rewrite info from the localized filter targets. Only local targets can enter |
| // file-reader expressions; constant and unset targets stay above the file reader. |
| static std::map<GlobalIndex, FileSlotRewriteInfo> build_file_slot_rewrite_map( |
| const std::vector<ColumnMapping>& mappings, |
| const std::map<GlobalIndex, FilterEntry>& filter_entries) { |
| std::map<GlobalIndex, FileSlotRewriteInfo> global_to_file_slot; |
| for (const auto& mapping : mappings) { |
| const auto entry_it = filter_entries.find(mapping.global_index); |
| if (entry_it == filter_entries.end() || !entry_it->second.is_local()) { |
| continue; |
| } |
| DORIS_CHECK(mapping.file_local_id.has_value()); |
| global_to_file_slot.emplace( |
| mapping.global_index, |
| FileSlotRewriteInfo {.block_position = entry_it->second.local_index().value(), |
| .file_type = mapping.file_type, |
| .table_type = mapping.table_type, |
| .file_column_name = mapping.file_column_name}); |
| } |
| return global_to_file_slot; |
| } |
| |
| Status TableColumnMapper::_create_by_index_mapping(const ColumnDefinition& table_column, |
| const std::vector<ColumnDefinition>& file_schema, |
| ColumnMapping* mapping) { |
| DORIS_CHECK(mapping != nullptr); |
| DORIS_CHECK(!table_column.is_partition_key); |
| |
| // Key contract: in BY_INDEX mode, `ColumnDefinition::identifier` TYPE_INT is interpreted as the |
| // 0-based position of this column inside `file_schema`. FE writes the physical file position |
| // of each non-partition projected column into that identifier. This interpretation allows: |
| // - sparse projection: read only a subset of file columns (for example only `_col2` |
| // and `_col4`); |
| // - column reordering: table column order differs from file column order; |
| // - no many-to-one mapping: FE must guarantee that each file position is referenced by at |
| // most one table column. |
| const auto file_index = table_column.get_identifier_position(); |
| |
| // Case A: file_index is in range, so build a direct positional mapping. |
| // The file column name (for example `_col0`) is intentionally ignored here. |
| if (file_index >= 0 && static_cast<size_t>(file_index) < file_schema.size()) { |
| return _create_direct_mapping(table_column, file_schema[static_cast<size_t>(file_index)], |
| mapping); |
| } |
| |
| // Case B: file_index is out of range, which means the file does not contain this column. |
| // Route it through the missing-column path used by schema evolution. |
| if (table_column.default_expr != nullptr) { |
| _set_constant_mapping(mapping, table_column.default_expr); |
| return Status::OK(); |
| } |
| // Keep the mapping empty (`file_local_id` remains `nullopt`) and let the upper finalize |
| // stage fill NULL/default values. |
| return Status::OK(); |
| } |
| |
| void TableColumnMapper::_set_constant_mapping(ColumnMapping* mapping, VExprContextSPtr expr) { |
| DORIS_CHECK(mapping != nullptr); |
| DORIS_CHECK(expr != nullptr); |
| mapping->default_expr = std::move(expr); |
| mapping->constant_index = _constant_map.add(ConstantEntry { |
| .global_index = mapping->global_index, |
| .expr = mapping->default_expr, |
| .type = mapping->table_type, |
| }); |
| mapping->filter_conversion = FilterConversionType::CONSTANT; |
| } |
| |
| Status TableColumnMapper::_create_mapping_for_column(const ColumnDefinition& table_column, |
| GlobalIndex global_index, |
| ColumnMapping* mapping) { |
| DORIS_CHECK(mapping != nullptr); |
| *mapping = ColumnMapping {}; |
| mapping->global_index = global_index; |
| mapping->table_column_name = table_column.name; |
| mapping->table_type = table_column.type; |
| // Row-lineage names are Iceberg metadata contracts, not reserved names in generic Hive, |
| // Hudi, or Paimon schemas. Only the Iceberg reader may opt into virtual synthesis. |
| const auto row_lineage_type = |
| _options.enable_row_lineage_virtual_columns |
| ? row_lineage_virtual_column_type(table_column, _options.mode) |
| : TableVirtualColumnType::INVALID; |
| if (const auto* partition_value = find_partition_value(table_column, _partition_values); |
| table_column.is_partition_key && partition_value != nullptr) { |
| // Partition values are split constants and must take precedence over defaults. |
| _set_constant_mapping(mapping, VExprContext::create_shared(VLiteral::create_shared( |
| mapping->table_type, *partition_value))); |
| } else if (_options.mode == TableColumnMappingMode::BY_INDEX && |
| !table_column.is_partition_key && table_column.has_identifier_field_id()) { |
| // BY_INDEX interprets ColumnDefinition::identifier as physical file position. |
| RETURN_IF_ERROR(_create_by_index_mapping(table_column, _file_schema, mapping)); |
| } else if (const auto* file_field = _find_file_field(table_column, _file_schema)) { |
| // Normal physical file column mapping. |
| RETURN_IF_ERROR(_create_direct_mapping(table_column, *file_field, mapping)); |
| if (row_lineage_type != TableVirtualColumnType::INVALID) { |
| // Iceberg v3 rewritten files may physically contain row lineage metadata fields. |
| // File non-null values must be preserved, while file NULLs still inherit from data file |
| // metadata in IcebergTableReader. Therefore the mapping has a real file source plus a |
| // virtual post-materialization step, and filters must wait for finalize output. |
| mapping->virtual_column_type = row_lineage_type; |
| mapping->filter_conversion = FilterConversionType::FINALIZE_ONLY; |
| } |
| } else if (row_lineage_type != TableVirtualColumnType::INVALID) { |
| // Iceberg row lineage metadata fields are optional in data files. Missing fields are exposed |
| // as all-NULL table columns first; IcebergTableReader fills inherited values only when the |
| // split carries first_row_id / last_updated_sequence_number metadata. |
| // FE may attach a default_expr to these hidden metadata columns, but the Iceberg v3 |
| // inheritance rule must take precedence over the generic missing-column default path. |
| mapping->virtual_column_type = row_lineage_type; |
| } else if (table_column.name == BeConsts::ICEBERG_ROWID_COL) { |
| // Doris internal Iceberg row locator is never a physical Iceberg data column. It is built |
| // from file path, row position and partition metadata for delete/update/merge. |
| mapping->virtual_column_type = TableVirtualColumnType::ICEBERG_ROWID; |
| } else if (table_column.initial_default_value.has_value()) { |
| VExprContextSPtr initial_default; |
| RETURN_IF_ERROR(build_initial_default_literal(table_column, &initial_default)); |
| // Iceberg metadata is the authoritative logical value for files written before the field |
| // existed; the generic FE expression may still contain its Base64 transport text. |
| _set_constant_mapping(mapping, std::move(initial_default)); |
| } else if (table_column.default_expr != nullptr) { |
| // Missing schema-evolution column with an explicit default expression. |
| _set_constant_mapping(mapping, table_column.default_expr); |
| } else { |
| if (table_column.is_partition_key) { |
| return Status::InvalidArgument( |
| "Table column '{}' (global_index={}) does not have a matching partition value", |
| table_column.name, mapping->global_index.value()); |
| } |
| } |
| return Status::OK(); |
| } |
| |
| Status TableColumnMapper::_create_hidden_filter_mapping(const ColumnDefinition& table_column, |
| GlobalIndex global_index, |
| ColumnMapping* mapping) { |
| auto status = _create_mapping_for_column(table_column, global_index, mapping); |
| if (mapping->file_local_id.has_value() || mapping->constant_index.has_value() || |
| mapping->virtual_column_type != TableVirtualColumnType::INVALID) { |
| return Status::OK(); |
| } |
| if (_options.mode == TableColumnMappingMode::BY_NAME) { |
| return status; |
| } |
| |
| // Predicate-only slot refs carry the table name/type but do not carry the table-format field |
| // id used by BY_FIELD_ID or the file position used by BY_INDEX. Use a name fallback only for |
| // hidden filter localization; projected columns still obey the requested mapping mode. |
| const auto* file_field = |
| matcher_for_mode(TableColumnMappingMode::BY_NAME).find(table_column, _file_schema); |
| if (file_field == nullptr) { |
| return status; |
| } |
| ColumnMapping fallback_mapping; |
| fallback_mapping.global_index = global_index; |
| fallback_mapping.table_column_name = table_column.name; |
| fallback_mapping.table_type = table_column.type; |
| RETURN_IF_ERROR(_create_direct_mapping(table_column, *file_field, &fallback_mapping)); |
| *mapping = std::move(fallback_mapping); |
| return Status::OK(); |
| } |
| |
| Status TableColumnMapper::_build_hidden_filter_mappings( |
| const std::vector<TableFilter>& table_filters) { |
| _hidden_mappings.clear(); |
| |
| std::map<GlobalIndex, ColumnDefinition> filter_columns; |
| for (const auto& table_filter : table_filters) { |
| if (table_filter.conjunct != nullptr) { |
| collect_top_level_slot_columns(table_filter.conjunct->root(), &filter_columns); |
| } |
| } |
| |
| for (const auto& [global_index, table_column] : filter_columns) { |
| if (_find_mapping(global_index) != nullptr) { |
| // Ignore columns that are already mapped by the projected columns |
| continue; |
| } |
| ColumnMapping mapping; |
| RETURN_IF_ERROR(_create_hidden_filter_mapping(table_column, global_index, &mapping)); |
| if (mapping.file_local_id.has_value() || mapping.constant_index.has_value() || |
| mapping.virtual_column_type != TableVirtualColumnType::INVALID) { |
| _hidden_mappings.push_back(std::move(mapping)); |
| } |
| } |
| return Status::OK(); |
| } |
| |
| Status TableColumnMapper::create_mapping(const std::vector<ColumnDefinition>& projected_columns, |
| const std::map<std::string, Field>& partition_values, |
| const std::vector<ColumnDefinition>& file_schema) { |
| clear(); |
| _partition_values = partition_values; |
| _file_schema = file_schema; |
| for (size_t column_idx = 0; column_idx < projected_columns.size(); ++column_idx) { |
| ColumnMapping mapping; |
| RETURN_IF_ERROR(_create_mapping_for_column(projected_columns[column_idx], |
| GlobalIndex(column_idx), &mapping)); |
| _mappings.push_back(std::move(mapping)); |
| } |
| return Status::OK(); |
| } |
| |
| std::vector<ColumnMapping> TableColumnMapper::_filter_visible_mappings() const { |
| std::vector<ColumnMapping> mappings; |
| mappings.reserve(_mappings.size() + _hidden_mappings.size()); |
| mappings.insert(mappings.end(), _mappings.begin(), _mappings.end()); |
| mappings.insert(mappings.end(), _hidden_mappings.begin(), _hidden_mappings.end()); |
| return mappings; |
| } |
| |
| Status TableColumnMapper::_build_filter_entries(const FileScanRequest& file_request) { |
| _filter_entries.clear(); |
| const auto mappings = _filter_visible_mappings(); |
| for (const auto& mapping : mappings) { |
| FilterEntry entry; |
| if (mapping.constant_index.has_value()) { |
| entry = FilterEntry::constant(*mapping.constant_index); |
| } else if (mapping.file_local_id.has_value() && |
| filter_conversion_has_local_source(mapping.filter_conversion)) { |
| const auto local_position_it = |
| file_request.local_positions.find(LocalColumnId(*mapping.file_local_id)); |
| if (local_position_it != file_request.local_positions.end()) { |
| entry = FilterEntry::local(local_position_it->second); |
| } |
| } |
| _filter_entries.emplace(mapping.global_index, entry); |
| } |
| return Status::OK(); |
| } |
| |
| Status TableColumnMapper::create_scan_request( |
| const std::vector<TableFilter>& table_filters, |
| const std::vector<ColumnDefinition>& projected_columns, FileScanRequest* file_request, |
| RuntimeState* runtime_state) { |
| // FileReader evaluates expressions against a file-local block. This mapper owns the |
| // table-column to file-column conversion, so it also owns the file-local block positions. |
| file_request->predicate_columns.clear(); |
| file_request->non_predicate_columns.clear(); |
| file_request->predicate_only_columns.clear(); |
| file_request->local_positions.clear(); |
| file_request->conjuncts.clear(); |
| file_request->delete_conjuncts.clear(); |
| _filter_entries.clear(); |
| // 1. Build referenced non-predicate columns |
| for (size_t column_idx = 0; column_idx < projected_columns.size(); ++column_idx) { |
| const auto global_index = GlobalIndex(column_idx); |
| auto* mapping = _find_mapping(global_index); |
| if (mapping != nullptr && mapping->file_local_id.has_value()) { |
| // A file column can be read lazily as a non-predicate column only when it is not used |
| // by row-level expression filters. |
| bool used_by_filter = false; |
| for (const auto& table_filter : table_filters) { |
| const auto& global_indices = table_filter.global_indices; |
| if (std::find(global_indices.begin(), global_indices.end(), global_index) != |
| global_indices.end() && |
| filter_conversion_has_local_source(mapping->filter_conversion)) { |
| used_by_filter = true; |
| break; |
| } |
| } |
| if (!used_by_filter || !enable_lazy_materialization()) { |
| RETURN_IF_ERROR(add_scan_column(file_request, mapping, false, |
| force_full_complex_scan_projection())); |
| } |
| } |
| } |
| // 2. Build referenced predicate columns |
| // 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. |
| RETURN_IF_ERROR(_build_hidden_filter_mappings(table_filters)); |
| RETURN_IF_ERROR(localize_filters(table_filters, file_request, runtime_state)); |
| for (const auto& mapping : _hidden_mappings) { |
| if (!mapping.file_local_id.has_value()) { |
| continue; |
| } |
| const auto local_id = LocalColumnId(*mapping.file_local_id); |
| const bool is_visible_output = |
| std::ranges::any_of(_mappings, [local_id](const ColumnMapping& visible_mapping) { |
| return visible_mapping.file_local_id.has_value() && |
| LocalColumnId(*visible_mapping.file_local_id) == local_id; |
| }); |
| if (is_visible_output) { |
| continue; |
| } |
| // File-local filtering is an optimization; Scanner still evaluates the original |
| // table-level conjunct after TableReader returns. Only truly hidden mappings are absent |
| // from that scanner-visible block and may safely discard their payload here. |
| if (std::ranges::any_of(file_request->predicate_columns, |
| [local_id](const LocalColumnIndex& projection) { |
| return projection.column_id() == local_id; |
| }) && |
| !file_request->is_predicate_only(local_id)) { |
| file_request->predicate_only_columns.push_back(local_id); |
| } |
| } |
| // 3. Rebuild output projection expressions for projected columns. localize_filters() has |
| // already applied the final scan projection to mapping.file_type/projected_file_children before |
| // rewriting filter expressions. |
| for (auto& mapping : _mappings) { |
| if (!mapping.file_local_id.has_value()) { |
| continue; |
| } |
| auto position_it = |
| file_request->local_positions.find(LocalColumnId(*mapping.file_local_id)); |
| DORIS_CHECK(position_it != file_request->local_positions.end()) |
| << file_request->local_positions.size() << " " << *mapping.file_local_id << " " |
| << mapping.file_column_name; |
| rebuild_projection(&mapping, position_it->second); |
| } |
| return Status::OK(); |
| } |
| |
| ColumnMapping* TableColumnMapper::_find_mapping(GlobalIndex global_index) { |
| for (auto& mapping : _mappings) { |
| if (mapping.global_index == global_index) { |
| return &mapping; |
| } |
| } |
| return nullptr; |
| } |
| |
| ColumnMapping* TableColumnMapper::_find_filter_mapping(GlobalIndex global_index) { |
| if (auto* mapping = _find_mapping(global_index); mapping != nullptr) { |
| return mapping; |
| } |
| for (auto& mapping : _hidden_mappings) { |
| if (mapping.global_index == global_index) { |
| return &mapping; |
| } |
| } |
| return nullptr; |
| } |
| |
| Status TableColumnMapper::localize_filters(const std::vector<TableFilter>& table_filters, |
| FileScanRequest* file_request, |
| RuntimeState* runtime_state) { |
| std::set<LocalColumnId> localized_predicate_columns; |
| FilterProjectionMap filter_projections; |
| auto filter_mappings = _filter_visible_mappings(); |
| RETURN_IF_ERROR(build_nested_struct_filter_projection_map(table_filters, filter_mappings, |
| &filter_projections)); |
| for (const auto& table_filter : table_filters) { |
| for (const auto& global_index : table_filter.global_indices) { |
| auto* mapping = _find_filter_mapping(global_index); |
| if (mapping == nullptr || !mapping->file_local_id.has_value() || |
| !filter_conversion_has_local_source(mapping->filter_conversion)) { |
| continue; |
| } |
| RETURN_IF_ERROR(add_scan_column(file_request, mapping, enable_lazy_materialization(), |
| force_full_complex_scan_projection(), |
| &filter_projections)); |
| } |
| } |
| // Rebuild the file type for every scan-local mapping before expression rewrite. Predicate-only |
| // hidden mappings must see the same projected file type as the file reader will produce. |
| for (auto& mapping : _mappings) { |
| if (mapping.file_local_id.has_value() && |
| file_request->local_positions.contains(LocalColumnId(*mapping.file_local_id))) { |
| RETURN_IF_ERROR(apply_scan_projection_to_mapping_file_type(*file_request, &mapping)); |
| } |
| } |
| for (auto& mapping : _hidden_mappings) { |
| if (mapping.file_local_id.has_value() && |
| file_request->local_positions.contains(LocalColumnId(*mapping.file_local_id))) { |
| RETURN_IF_ERROR(apply_scan_projection_to_mapping_file_type(*file_request, &mapping)); |
| } |
| } |
| RETURN_IF_ERROR(_build_filter_entries(*file_request)); |
| |
| // Build the complete table-slot rewrite map after all predicate columns have been assigned. |
| // This keeps expression localization independent from filter iteration order. |
| filter_mappings = _filter_visible_mappings(); |
| const auto global_to_file_slot = build_file_slot_rewrite_map(filter_mappings, _filter_entries); |
| for (const auto& table_filter : table_filters) { |
| if (table_filter.conjunct != nullptr && table_filter.conjunct->root() != nullptr) { |
| const auto root = table_filter.conjunct->root(); |
| const auto impl = root->get_impl(); |
| const auto predicate = impl != nullptr ? impl : root; |
| if (!predicate->is_deterministic() || |
| !table_filter_has_only_local_entries(table_filter, _filter_entries)) { |
| continue; |
| } |
| // Scanner evaluates the original conjunct after final materialization. Only predicates |
| // whose result is stable across repeated execution may also run as a file-local copy. |
| RewriteContext rewrite_context {.runtime_state = runtime_state}; |
| VExprSPtr rewrite_root; |
| Status clone_status; |
| try { |
| clone_status = clone_table_expr_tree(table_filter.conjunct->root(), &rewrite_root); |
| } catch ([[maybe_unused]] const Exception& e) { |
| // Some table filters contain complex intermediate values, for example |
| // `element_at(MAP_VALUES(m)[1], 'age') > 30`. The current file-local rewrite only |
| // understands top-level slots and struct-element paths rooted at top-level slots; |
| // cloning such expressions can hit the generic TExpr complex-type limitation. |
| // Leave them above TableReader, where Scanner evaluates the original table-level |
| // conjunct after final materialization. |
| #ifndef NDEBUG |
| return Status::InternalError( |
| "Failed to clone table filter for file-local rewrite: {}, expr={}", |
| e.to_string(), table_filter.conjunct->root()->debug_string()); |
| #else |
| continue; |
| #endif |
| } catch ([[maybe_unused]] const std::exception& e) { |
| #ifndef NDEBUG |
| return Status::InternalError( |
| "Failed to clone table filter for file-local rewrite: {}, expr={}", |
| e.what(), table_filter.conjunct->root()->debug_string()); |
| #else |
| continue; |
| #endif |
| } |
| if (!clone_status.ok()) { |
| #ifndef NDEBUG |
| return Status::InternalError( |
| "Failed to clone table filter for file-local rewrite: {}, expr={}", |
| clone_status.to_string(), table_filter.conjunct->root()->debug_string()); |
| #else |
| continue; |
| #endif |
| } |
| bool can_localize = true; |
| auto localized_root = rewrite_table_expr_to_file_expr(rewrite_root, global_to_file_slot, |
| filter_mappings, &rewrite_context, |
| &can_localize); |
| if (!can_localize) { |
| continue; |
| } |
| auto localized_conjunct = VExprContext::create_shared(std::move(localized_root)); |
| RETURN_IF_ERROR(rewrite_context.prepare_created_exprs(localized_conjunct.get())); |
| file_request->conjuncts.push_back(std::move(localized_conjunct)); |
| for (const auto global_index : table_filter.global_indices) { |
| const auto* mapping = _find_filter_mapping(global_index); |
| if (mapping != nullptr && mapping->file_local_id.has_value() && |
| filter_conversion_has_local_source(mapping->filter_conversion)) { |
| localized_predicate_columns.emplace(*mapping->file_local_id); |
| } |
| } |
| } |
| } |
| |
| // Candidate columns are added before expression rewriting because their file-block positions |
| // are needed to localize slot refs. If rewriting rejects every filter that references a visible |
| // column, move its already-merged output/filter projection to the lazy non-predicate set |
| // instead of forcing it through the eager predicate path. |
| for (auto& mapping : _mappings) { |
| if (!mapping.file_local_id.has_value()) { |
| continue; |
| } |
| const auto local_id = LocalColumnId(*mapping.file_local_id); |
| if (localized_predicate_columns.contains(local_id)) { |
| continue; |
| } |
| const auto predicate_it = std::ranges::find_if( |
| file_request->predicate_columns, [local_id](const LocalColumnIndex& projection) { |
| return projection.column_id() == local_id; |
| }); |
| if (predicate_it == file_request->predicate_columns.end()) { |
| continue; |
| } |
| file_request->non_predicate_columns.push_back(std::move(*predicate_it)); |
| file_request->predicate_columns.erase(predicate_it); |
| } |
| return Status::OK(); |
| } |
| |
| const ColumnDefinition* TableColumnMapper::_find_file_field( |
| const ColumnDefinition& table_column, |
| const std::vector<ColumnDefinition>& file_schema) const { |
| if (table_column.name.starts_with(BeConsts::GLOBAL_ROWID_COL)) { |
| const auto field_it = std::ranges::find_if(file_schema, [](const ColumnDefinition& field) { |
| return field.column_type == ColumnType::GLOBAL_ROWID; |
| }); |
| return field_it == file_schema.end() ? nullptr : &*field_it; |
| } |
| const auto* matched = matcher_for_mode(_options.mode).find(table_column, file_schema); |
| if (matched != nullptr || _options.mode != TableColumnMappingMode::BY_FIELD_ID || |
| !_options.allow_idless_complex_wrapper_projection || table_column.children.empty()) { |
| return matched; |
| } |
| const ColumnDefinition* wrapper = nullptr; |
| for (const auto& candidate : file_schema) { |
| if (candidate.has_identifier_field_id() || candidate.children.empty() || |
| !has_shared_descendant_field_id(table_column, candidate)) { |
| continue; |
| } |
| if (wrapper != nullptr) { |
| return nullptr; |
| } |
| wrapper = &candidate; |
| } |
| // Iceberg Parquet's PruneColumns retains an ID-less complex wrapper when a nested field ID is |
| // selected. Descendant IDs, not aliases, identify that wrapper; ambiguity remains unmapped. |
| return wrapper; |
| } |
| |
| Status TableColumnMapper::_create_direct_mapping(const ColumnDefinition& table_column, |
| const ColumnDefinition& file_field, |
| ColumnMapping* mapping) const { |
| DORIS_CHECK(mapping != nullptr); |
| DORIS_CHECK(file_field.local_id >= 0 || file_field.local_id == GLOBAL_ROWID_COLUMN_ID); |
| mapping->file_local_id = file_field.local_id; |
| mapping->table_column_name = table_column.name; |
| mapping->file_column_name = file_field.name; |
| mapping->original_file_type = file_field.type; |
| mapping->original_file_children = file_field.children; |
| mapping->projected_file_children = file_field.children; |
| mapping->file_type = file_field.type; |
| mapping->is_trivial = mapping_can_use_file_column_directly(*mapping); |
| mapping->filter_conversion = direct_filter_conversion(*mapping); |
| mapping->child_mappings.clear(); |
| |
| auto [table_children, synthesized_table_children] = |
| prepare_table_children_for_mapping(table_column, mapping->file_type); |
| |
| if (!table_children.empty()) { |
| if (!is_complex_type(remove_nullable(mapping->file_type)->get_primitive_type())) { |
| return Status::NotSupported( |
| "Cannot map complex table column '{}' to scalar parquet column '{}', table " |
| "type={}, file type={}", |
| table_column.name, file_field.name, mapping->table_type->get_name(), |
| mapping->file_type->get_name()); |
| } |
| RETURN_IF_ERROR(validate_file_schema_children(file_field)); |
| std::vector<int32_t> synthesized_used_file_child_ids; |
| for (size_t table_child_idx = 0; table_child_idx < table_children.size(); |
| ++table_child_idx) { |
| const auto& table_child = table_children[table_child_idx]; |
| const auto* file_child = |
| find_file_child_for_mapping(table_child, file_field, _options.mode, |
| table_child_idx, synthesized_table_children); |
| if (file_child == nullptr && !synthesized_table_children && |
| _options.mode == TableColumnMappingMode::BY_FIELD_ID && |
| _options.allow_idless_complex_wrapper_projection) { |
| // Parquet can retain an ID-less wrapper at any depth when a selected descendant |
| // has an ID; apply the same opt-in fallback used for root lookup recursively. |
| file_child = _find_file_field(table_child, file_field.children); |
| } |
| if (synthesized_table_children && file_child != nullptr) { |
| const auto file_child_id = file_child->file_local_id(); |
| if (std::ranges::find(synthesized_used_file_child_ids, file_child_id) != |
| synthesized_used_file_child_ids.end()) { |
| file_child = nullptr; |
| for (const auto& candidate : file_field.children) { |
| const auto candidate_id = candidate.file_local_id(); |
| if (std::ranges::find(synthesized_used_file_child_ids, candidate_id) == |
| synthesized_used_file_child_ids.end()) { |
| file_child = &candidate; |
| break; |
| } |
| } |
| } |
| if (file_child != nullptr) { |
| synthesized_used_file_child_ids.push_back(file_child->file_local_id()); |
| } |
| } |
| if (file_child == nullptr) { |
| ColumnMapping child_mapping; |
| child_mapping.table_column_name = table_child.name; |
| child_mapping.file_column_name = table_child.name; |
| child_mapping.table_type = table_child.type; |
| child_mapping.file_type = table_child.type; |
| child_mapping.filter_conversion = FilterConversionType::FINALIZE_ONLY; |
| // A missing nested field still has its Iceberg initial-default value in every row |
| // written before the field was added; carry it into recursive materialization. |
| RETURN_IF_ERROR(build_initial_default_column( |
| table_child, &child_mapping.initial_default_column)); |
| mapping->child_mappings.push_back(std::move(child_mapping)); |
| continue; |
| } |
| ColumnMapping child_mapping; |
| child_mapping.table_column_name = table_child.name; |
| child_mapping.table_type = table_child.type; |
| RETURN_IF_ERROR(_create_direct_mapping(table_child, *file_child, &child_mapping)); |
| mapping->child_mappings.push_back(std::move(child_mapping)); |
| } |
| if (needs_projected_file_type_rebuild(*mapping)) { |
| // 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. |
| RETURN_IF_ERROR(rebuild_projected_file_children_and_type( |
| mapping->file_type, mapping->original_file_children, mapping->child_mappings, |
| &mapping->projected_file_children, &mapping->file_type)); |
| DCHECK(mapping->table_type != nullptr); |
| mapping->is_trivial = mapping_can_use_file_column_directly(*mapping); |
| mapping->filter_conversion = projected_filter_conversion(*mapping); |
| } |
| } |
| return Status::OK(); |
| } |
| |
| } // namespace doris::format |