blob: 3ab3c1e9119d1185031d3bed555ea335383cd93f [file]
// 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 <cctype>
#include <cstddef>
#include <map>
#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 "exprs/vslot_ref.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"
namespace doris::format {
namespace {
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";
case TableVirtualColumnType::ICEBERG_FILE_PATH:
return "ICEBERG_FILE_PATH";
case TableVirtualColumnType::ICEBERG_ROW_POSITION:
return "ICEBERG_ROW_POSITION";
case TableVirtualColumnType::PAIMON_FILE_PATH:
return "PAIMON_FILE_PATH";
case TableVirtualColumnType::PAIMON_ROW_POSITION:
return "PAIMON_ROW_POSITION";
}
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();
}
void remap_localized_slot_positions(const VExprSPtr& expr,
const std::map<size_t, size_t>& position_remap,
std::set<const VExpr*>* visited) {
if (expr == nullptr || !visited->insert(expr.get()).second) {
return;
}
if (auto* slot = dynamic_cast<VSlotRef*>(expr.get());
slot != nullptr && slot->column_id() >= 0) {
const auto remap = position_remap.find(static_cast<size_t>(slot->column_id()));
DORIS_CHECK(remap != position_remap.end()) << slot->column_id();
slot->set_column_id(static_cast<int>(remap->second));
}
for (const auto& child : expr->children()) {
remap_localized_slot_positions(child, position_remap, visited);
}
remap_localized_slot_positions(expr->get_impl(), position_remap, visited);
}
void compact_file_block_positions(FileScanRequest* request) {
std::set<size_t> occupied_positions;
for (const auto& [_, position] : request->local_positions) {
occupied_positions.insert(position.value());
}
for (const auto& [_, position] : request->non_predicate_positions) {
occupied_positions.insert(position.value());
}
std::map<size_t, size_t> position_remap;
size_t dense_position = 0;
for (size_t old_position : occupied_positions) {
position_remap.emplace(old_position, dense_position++);
}
for (auto& [_, position] : request->local_positions) {
position = LocalIndex(position_remap.at(position.value()));
}
for (auto& [_, position] : request->non_predicate_positions) {
position = LocalIndex(position_remap.at(position.value()));
}
std::set<const VExpr*> visited;
for (const auto& conjunct : request->conjuncts) {
remap_localized_slot_positions(conjunct->root(), position_remap, &visited);
}
for (const auto& conjunct : request->delete_conjuncts) {
remap_localized_slot_positions(conjunct->root(), position_remap, &visited);
}
}
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 ColumnDefinition* find_column_by_field_id(const ColumnDefinition& table_column,
const std::vector<ColumnDefinition>& file_schema,
bool allow_idless_complex_wrapper_projection) {
const auto* matched =
matcher_for_mode(TableColumnMappingMode::BY_FIELD_ID).find(table_column, file_schema);
if (matched != nullptr || !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;
}
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;
const ColumnMapping* root_mapping = nullptr;
LocalColumnIndex scan_projection;
};
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_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)
<< ", reject_missing_required_field=" << reject_missing_required_field
<< ", allow_idless_complex_wrapper_projection=" << allow_idless_complex_wrapper_projection
<< ", enable_row_lineage_virtual_columns=" << enable_row_lineage_virtual_columns
<< ", enable_iceberg_metadata_virtual_columns=" << enable_iceberg_metadata_virtual_columns
<< ", enable_paimon_metadata_virtual_columns=" << enable_paimon_metadata_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
<< ", timestamp_is_adjusted_to_utc="
<< (timestamp_is_adjusted_to_utc.has_value()
? (*timestamp_is_adjusted_to_utc ? "true" : "false")
: "unset")
<< ", 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)
<< ", has_initial_default=" << initial_default_value.has_value() << ", is_optional=";
if (is_optional.has_value()) {
out << *is_optional;
} else {
out << "unknown";
}
out << ", is_partition_key=" << is_partition_key << ", is_synthesized=" << is_synthesized
<< "}";
return out.str();
}
std::string LocalColumnIndex::debug_string() const {
std::ostringstream out;
out << "LocalColumnIndex{index=" << index << ", project_all_children=" << project_all_children
<< ", timestamp_is_adjusted_to_utc="
<< (timestamp_is_adjusted_to_utc.has_value()
? (*timestamp_is_adjusted_to_utc ? "true" : "false")
: "unset")
<< ", 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 << ", timestamp_is_adjusted_to_utc="
<< (timestamp_is_adjusted_to_utc.has_value()
? (*timestamp_is_adjusted_to_utc ? "true" : "false")
: "unset")
<< ", 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 bool table_filter_has_only_constant_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_constant()) {
return false;
}
}
return !table_filter.global_indices.empty();
}
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);
const auto is_supported_element = [](const VExprSPtr& candidate) {
if (is_struct_element_expr(candidate)) {
return true;
}
return candidate != nullptr && candidate->get_num_children() == 2 &&
candidate->fn().name.function_name == "element_at" &&
candidate->children()[0]->data_type() != nullptr &&
remove_nullable(candidate->children()[0]->data_type())->get_primitive_type() ==
TYPE_ARRAY;
};
if (!is_supported_element(expr)) {
return false;
}
const auto& parent = expr->children()[0];
if (is_supported_element(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 const ColumnMapping* find_projected_child_mapping(const ColumnMapping& mapping,
int32_t file_local_id) {
const auto child_it = std::ranges::find_if(
mapping.child_mappings, [file_local_id](const ColumnMapping& child) {
return child.file_local_id.has_value() && *child.file_local_id == file_local_id;
});
return child_it == mapping.child_mappings.end() ? nullptr : &*child_it;
}
static bool projected_mapping_allows_file_filtering(const ColumnMapping& mapping,
const LocalColumnIndex* projection) {
if (!can_filter_before_table_nullability_alignment(mapping.file_type, mapping.table_type)) {
return false;
}
const auto file_type = remove_nullable(mapping.file_type);
const auto table_type = remove_nullable(mapping.table_type);
if (table_type->get_primitive_type() == TYPE_VARIANT) {
// Shredded Variant children describe physical encoding, not table-schema nullability
// contracts. The Variant root is therefore the only mapped level that can be validated.
return true;
}
const auto file_primitive_type = file_type->get_primitive_type();
const auto table_primitive_type = table_type->get_primitive_type();
if (is_complex_type(file_primitive_type) || is_complex_type(table_primitive_type)) {
if (file_primitive_type != table_primitive_type) {
return false;
}
if (mapping.child_mappings.empty()) {
return file_type->equals(*table_type);
}
} else if (!file_type->equals(*table_type) &&
!is_lossless_file_to_table_numeric_cast(mapping.file_type, mapping.table_type)) {
// A file-local filter can discard a row before TableReader casts a projected sibling.
// Require every projected scalar cast to preserve all source values so filtering cannot
// hide overflow or other materialization errors in that sibling.
return false;
}
if (is_full_projection(projection)) {
for (const auto& child : mapping.child_mappings) {
if (child.file_local_id.has_value() &&
!projected_mapping_allows_file_filtering(child, nullptr)) {
return false;
}
}
return true;
}
for (const auto& child_projection : projection->children) {
const auto* child = find_projected_child_mapping(mapping, child_projection.local_id());
if (child == nullptr ||
!projected_mapping_allows_file_filtering(*child, &child_projection)) {
return false;
}
}
return true;
}
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() ||
struct_element_chain.size() != resolved.table_child_types.size() ||
struct_element_chain.size() != resolved.file_array_elements.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;
}
DORIS_CHECK(rewrite_it->second.root_mapping != nullptr);
// File-local filtering cannot discard rows before every physically projected mapped child has
// reached TableReader's schema validation and casts. ARRAY access uses a full element
// projection on this branch, so validating only the selected STRUCT chain can miss an invalid
// required or narrowing sibling.
if (!projected_mapping_allows_file_filtering(*rewrite_it->second.root_mapping,
&rewrite_it->second.scan_projection)) {
return false;
}
for (size_t idx = 0; idx < struct_element_chain.size(); ++idx) {
// Accessor results become nullable for missing ARRAY indices and NULL parents. Compare the
// file child with the declared table child instead, or that execution-only wrapper can
// hide a nullable-file-to-required-table contract violation before alignment reports it.
if (!can_filter_before_table_nullability_alignment(resolved.file_child_types[idx],
resolved.table_child_types[idx])) {
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();
if (!resolved.file_array_elements[idx]) {
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 = {},
.root_mapping = nullptr,
.scan_projection = {},
};
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 = {},
.root_mapping = nullptr,
.scan_projection = {},
};
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 (remove_nullable(mapping.file_type)->get_primitive_type() == TYPE_STRUCT &&
mapping.child_mappings.size() != mapping.original_file_children.size()) {
// A predicate access path keeps the parent Struct type but intentionally carries only the
// referenced child descriptors; type equality alone must not restore the pruned siblings.
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,
bool include_variant_access_paths = false) {
if (has_projected_file_children(mapping)) {
// Return True if the projected child column is missing / re-ordered
return true;
}
if (include_variant_access_paths && !mapping.variant_access_paths.empty()) {
return true;
}
return std::ranges::any_of(
mapping.child_mappings, [include_variant_access_paths](const ColumnMapping& child) {
return needs_nested_file_projection(child, include_variant_access_paths);
});
}
static bool build_variant_projection(const ColumnMapping& mapping, LocalColumnIndex* projection);
static Status build_complex_projection(const ColumnMapping& mapping, LocalColumnIndex* projection,
bool enable_variant_leaf_projection = false);
// 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,
bool enable_variant_leaf_projection) {
if (projection == nullptr) {
return Status::InvalidArgument("projection is null");
}
DORIS_CHECK(mapping.file_local_id.has_value());
if (enable_variant_leaf_projection && build_variant_projection(mapping, projection)) {
return Status::OK();
}
*projection = LocalColumnIndex::local(*mapping.file_local_id);
projection->timestamp_is_adjusted_to_utc = mapping.timestamp_is_adjusted_to_utc;
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,
enable_variant_leaf_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();
}
static bool has_timestamp_semantics(const ColumnMapping& mapping) {
return mapping.timestamp_is_adjusted_to_utc.has_value() ||
std::ranges::any_of(mapping.child_mappings, has_timestamp_semantics);
}
static void attach_timestamp_semantics(const ColumnMapping& mapping, LocalColumnIndex* projection) {
DORIS_CHECK(projection != nullptr);
projection->timestamp_is_adjusted_to_utc = mapping.timestamp_is_adjusted_to_utc;
for (const auto& child_mapping : mapping.child_mappings) {
// A full projection represents ordinary children implicitly; materialize only paths that
// carry an override so existing readers still observe an empty children list.
if (!child_mapping.file_local_id.has_value() || !has_timestamp_semantics(child_mapping)) {
continue;
}
auto child_it =
std::ranges::find_if(projection->children, [&](const LocalColumnIndex& child) {
return child.local_id() == *child_mapping.file_local_id;
});
if (child_it == projection->children.end()) {
projection->children.push_back(LocalColumnIndex::local(*child_mapping.file_local_id));
child_it = std::prev(projection->children.end());
}
attach_timestamp_semantics(child_mapping, &*child_it);
}
}
static Status apply_projected_file_definition_to_mapping(const ColumnDefinition& projected_field,
ColumnMapping* mapping) {
DORIS_CHECK(mapping != nullptr);
mapping->file_type = projected_field.type;
mapping->projected_file_children = projected_field.children;
for (auto& child_mapping : mapping->child_mappings) {
if (!child_mapping.file_local_id.has_value()) {
continue;
}
const auto child_it =
std::ranges::find_if(projected_field.children, [&](const ColumnDefinition& child) {
return child.file_local_id() == *child_mapping.file_local_id;
});
if (child_it == projected_field.children.end()) {
return Status::InternalError(
"Projected file type for '{}' is missing mapped child id {}",
mapping->file_column_name, *child_mapping.file_local_id);
}
// A full root projection changes every descendant's runtime shape too. Keep the recursive
// mapping in sync so a formerly pruned child cannot be mistaken for a trivial direct column.
RETURN_IF_ERROR(apply_projected_file_definition_to_mapping(*child_it, &child_mapping));
}
mapping->is_trivial = mapping_can_use_file_column_directly(*mapping);
return Status::OK();
}
// 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));
return apply_projected_file_definition_to_mapping(projected_field, mapping);
}
static const ColumnDefinition* find_file_child_by_name(
const std::vector<ColumnDefinition>& children, std::string_view name) {
const auto child_it = std::ranges::find_if(
children, [name](const ColumnDefinition& child) { return child.name == name; });
return child_it == children.end() ? nullptr : &*child_it;
}
static bool variant_leaf_type_preserves_physical_identity(const ColumnDefinition& leaf) {
if (!leaf.children.empty() || leaf.type == nullptr) {
return false;
}
// ColumnDefinition does not transport Parquet's raw-binary/UUID and timestamp-unit tags.
// Limit direct leaves to identities fully described by the Doris scalar type; every ambiguous
// identity must retain the complete wrapper so reconstruction can inspect its physical schema.
switch (remove_nullable(leaf.type)->get_primitive_type()) {
case TYPE_BOOLEAN:
case TYPE_TINYINT:
case TYPE_SMALLINT:
case TYPE_INT:
case TYPE_BIGINT:
case TYPE_FLOAT:
case TYPE_DOUBLE:
case TYPE_DECIMAL128I:
case TYPE_DATEV2:
return true;
default:
return false;
}
}
static bool build_variant_leaf_path_projection(const ColumnMapping& mapping,
const std::vector<std::string>& path,
LocalColumnIndex* root_projection) {
DORIS_CHECK(root_projection != nullptr);
const auto is_numeric_selector = [](std::string_view value) {
if (value.empty()) {
return false;
}
const size_t digits_begin = value.front() == '+' || value.front() == '-' ? 1 : 0;
return digits_begin < value.size() &&
std::ranges::all_of(value.substr(digits_begin),
[](unsigned char c) { return std::isdigit(c); });
};
if (path.size() != 1 || path[0].empty() || path[0] == "NULL" ||
path[0].find('.') != std::string::npos || is_numeric_selector(path[0]) ||
!mapping.file_local_id.has_value()) {
// Thrift currently carries access paths as strings without segment-kind or escaping
// metadata. Signed numeric tokens are therefore also ambiguous between an array selector
// and an object key, so only a single unambiguous key can be mapped losslessly to a leaf.
return false;
}
*root_projection = LocalColumnIndex::partial_local(*mapping.file_local_id);
const auto* root_typed = find_file_child_by_name(mapping.original_file_children, "typed_value");
if (root_typed == nullptr || root_typed->children.empty() || root_typed->type == nullptr ||
remove_nullable(root_typed->type)->get_primitive_type() != TYPE_STRUCT) {
return false;
}
root_projection->children.push_back(
LocalColumnIndex::partial_local(root_typed->file_local_id()));
auto* current_projection = &root_projection->children.back();
const auto* typed_children = &root_typed->children;
for (size_t position = 0; position < path.size(); ++position) {
const auto* wrapper = find_file_child_by_name(*typed_children, path[position]);
if (wrapper == nullptr) {
return false;
}
current_projection->children.push_back(
LocalColumnIndex::partial_local(wrapper->file_local_id()));
current_projection = &current_projection->children.back();
const auto* typed = find_file_child_by_name(wrapper->children, "typed_value");
if (typed == nullptr) {
return false;
}
auto typed_projection = LocalColumnIndex::partial_local(typed->file_local_id());
const bool leaf = position + 1 == path.size();
if (leaf) {
// Only primitive typed values can be returned as a direct vector. Complex shredded
// values still need their wrapper shape and therefore keep the full Variant fallback.
if (!variant_leaf_type_preserves_physical_identity(*typed)) {
return false;
}
typed_projection.project_all_children = true;
}
current_projection->children.push_back(std::move(typed_projection));
current_projection = &current_projection->children.back();
typed_children = &typed->children;
}
return true;
}
static bool build_variant_projection(const ColumnMapping& mapping, LocalColumnIndex* projection) {
DORIS_CHECK(projection != nullptr);
if (mapping.table_type == nullptr || mapping.variant_access_paths.empty() ||
remove_nullable(mapping.table_type)->get_primitive_type() != TYPE_VARIANT) {
return false;
}
std::optional<LocalColumnIndex> merged;
for (const auto& path : mapping.variant_access_paths) {
LocalColumnIndex path_projection;
if (!build_variant_leaf_path_projection(mapping, path, &path_projection)) {
return false;
}
if (!merged.has_value()) {
merged = std::move(path_projection);
} else if (!merge_local_column_index(&*merged, path_projection).ok()) {
return false;
}
}
if (!merged.has_value()) {
return false;
}
*projection = std::move(*merged);
return true;
}
static Status build_scan_projection(ColumnMapping* mapping, bool force_full_complex_scan_projection,
bool enable_variant_leaf_projection,
LocalColumnIndex* projection) {
DORIS_CHECK(projection != nullptr);
const auto file_column_id = LocalColumnId(mapping->file_local_id.value());
*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 (enable_variant_leaf_projection && !force_full_complex_scan_projection &&
build_variant_projection(*mapping, projection)) {
// The per-file Parquet reader will validate residual-value statistics before honoring this
// physical leaf projection; unsafe files atomically fall back to the complete Variant.
} else if (!force_full_complex_scan_projection &&
needs_nested_file_projection(*mapping, enable_variant_leaf_projection)) {
RETURN_IF_ERROR(
build_complex_projection(*mapping, projection, enable_variant_leaf_projection));
}
return Status::OK();
}
static Status add_scan_column(FileScanRequest* file_request, ColumnMapping* mapping,
bool is_predicate_column, bool force_full_complex_scan_projection,
bool enable_variant_leaf_projection) {
LocalColumnIndex projection;
RETURN_IF_ERROR(build_scan_projection(mapping, force_full_complex_scan_projection,
enable_variant_leaf_projection, &projection));
attach_timestamp_semantics(*mapping, &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;
}
static bool same_projected_file_shape(const std::vector<ColumnDefinition>& lhs,
const std::vector<ColumnDefinition>& rhs) {
if (lhs.size() != rhs.size()) {
return false;
}
for (size_t index = 0; index < lhs.size(); ++index) {
if (lhs[index].local_id != rhs[index].local_id ||
!same_projected_file_shape(lhs[index].children, rhs[index].children)) {
return false;
}
}
return true;
}
// 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.
//
// Applying the selected projection changes a mapping's file type to the exact nested shape exposed
// by FileReader, so later filter rewrite and TableReader materialization agree with the file block.
static Status apply_scan_projection_to_mapping_file_type(const FileScanRequest& file_request,
ColumnMapping* mapping,
bool predicate_mapping = false) {
DORIS_CHECK(mapping != nullptr);
DORIS_CHECK(mapping->file_local_id.has_value());
const auto file_column_id = LocalColumnId(*mapping->file_local_id);
const LocalColumnIndex* projection = nullptr;
if (!predicate_mapping && file_request.has_deferred_non_predicate_column(file_column_id)) {
projection = find_scan_projection(file_request.non_predicate_columns, file_column_id);
}
if (projection == nullptr) {
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);
}
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::vector<ColumnMapping>& output_mappings,
const std::map<GlobalIndex, FilterEntry>& filter_entries,
const FileScanRequest& file_request) {
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());
const auto file_column_id = LocalColumnId(*mapping.file_local_id);
const auto* scan_projection =
find_scan_projection(file_request.predicate_columns, file_column_id);
if (scan_projection == nullptr) {
scan_projection =
find_scan_projection(file_request.non_predicate_columns, file_column_id);
}
DORIS_CHECK(scan_projection != nullptr);
const auto output_mapping_it =
std::ranges::find_if(output_mappings, [&](const ColumnMapping& output_mapping) {
return output_mapping.global_index == mapping.global_index;
});
const auto* physical_mapping =
output_mapping_it == output_mappings.end() ? &mapping : &*output_mapping_it;
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,
.root_mapping = physical_mapping,
.scan_projection = *scan_projection});
}
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;
mapping->variant_access_paths = table_column.variant_access_paths;
const auto iceberg_metadata_type = [&] {
if (!_options.enable_iceberg_metadata_virtual_columns || !table_column.is_synthesized) {
return TableVirtualColumnType::INVALID;
}
if (iequal(table_column.name, BeConsts::ICEBERG_FILE_PATH_COL)) {
return TableVirtualColumnType::ICEBERG_FILE_PATH;
}
if (iequal(table_column.name, BeConsts::ICEBERG_ROW_POSITION_COL)) {
return TableVirtualColumnType::ICEBERG_ROW_POSITION;
}
return TableVirtualColumnType::INVALID;
}();
const auto paimon_metadata_type = [&] {
if (!_options.enable_paimon_metadata_virtual_columns || !table_column.is_synthesized) {
return TableVirtualColumnType::INVALID;
}
if (iequal(table_column.name, BeConsts::PAIMON_FILE_PATH_COL)) {
return TableVirtualColumnType::PAIMON_FILE_PATH;
}
if (iequal(table_column.name, BeConsts::PAIMON_ROW_POSITION_COL)) {
return TableVirtualColumnType::PAIMON_ROW_POSITION;
}
return TableVirtualColumnType::INVALID;
}();
// 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 (iceberg_metadata_type != TableVirtualColumnType::INVALID) {
// Iceberg `_file` and `_pos` are metadata contracts only when the current FE explicitly
// classifies the slot as synthesized. Old FE plans can still read physical fields with the
// same spelling during a rolling upgrade.
mapping->virtual_column_type = iceberg_metadata_type;
} else if (paimon_metadata_type != TableVirtualColumnType::INVALID) {
// Paimon metadata is carried by RawFile. The explicit synthesized marker prevents a
// physical same-name field from being reinterpreted during a rolling upgrade.
mapping->virtual_column_type = paimon_metadata_type;
} else 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.default_expr != nullptr) {
// Table-format readers build typed default expressions before mapping. Keep that typed
// expression authoritative over the raw transport metadata, which cannot represent complex
// defaults safely in this table-format-neutral layer.
_set_constant_mapping(mapping, table_column.default_expr);
} else if (table_column.initial_default_value.has_value()) {
return Status::InvalidArgument(
"Missing typed initial-default expression for table field '{}'", table_column.name);
} else if (_options.reject_missing_required_field && table_column.is_optional.has_value() &&
!*table_column.is_optional) {
return Status::InvalidArgument("Missing required field: {}", table_column.name);
} 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));
if (enable_independent_predicate_projection() &&
projected_columns[column_idx].has_predicate_access_paths) {
auto predicate_column = projected_columns[column_idx];
predicate_column.children = predicate_column.predicate_children;
predicate_column.variant_access_paths = predicate_column.predicate_variant_access_paths;
predicate_column.has_predicate_access_paths = false;
predicate_column.predicate_children.clear();
predicate_column.predicate_variant_access_paths.clear();
ColumnMapping predicate_mapping;
RETURN_IF_ERROR(_create_mapping_for_column(predicate_column, GlobalIndex(column_idx),
&predicate_mapping));
_predicate_mappings.push_back(std::move(predicate_mapping));
}
}
return Status::OK();
}
std::vector<ColumnMapping> TableColumnMapper::_filter_visible_mappings() const {
std::vector<ColumnMapping> mappings;
mappings.reserve(_mappings.size() + _hidden_mappings.size());
for (const auto& mapping : _mappings) {
const auto predicate_it = std::ranges::find_if(
_predicate_mappings, [&](const ColumnMapping& predicate_mapping) {
return predicate_mapping.global_index == mapping.global_index;
});
mappings.push_back(predicate_it == _predicate_mappings.end() ? mapping : *predicate_it);
}
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,
const std::map<LocalColumnId, LocalIndex>* fixed_local_positions,
const std::map<LocalColumnId, LocalIndex>* fixed_non_predicate_positions) {
// 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();
if (fixed_local_positions != nullptr) {
// A refreshed predicate may promote a lazy column, but the active split's block slots are
// immutable. Seed their positions before rebuilding expressions so every rewritten SlotRef
// continues to address the same physical column.
file_request->local_positions = *fixed_local_positions;
}
file_request->non_predicate_positions.clear();
if (fixed_non_predicate_positions != nullptr) {
// Deferred output slots are part of the active reader's immutable block layout, just like
// eager slots; retaining only local_positions can shift a later complex root out of bounds.
file_request->non_predicate_positions = *fixed_non_predicate_positions;
}
file_request->conjuncts.clear();
file_request->metadata_pruning_safe_conjunct_count = 0;
file_request->constant_pruning_safe_table_filter_count = 0;
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(),
enable_variant_leaf_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;
}
const auto local_id = LocalColumnId(*mapping.file_local_id);
const auto position_it = file_request->local_positions.find(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, file_request->non_predicate_position(local_id));
}
return Status::OK();
}
Status TableColumnMapper::reconcile_scan_request_after_customization(
FileScanRequest* file_request) {
DORIS_CHECK(file_request != nullptr);
bool output_shape_changed = false;
for (auto& mapping : _mappings) {
if (!mapping.file_local_id.has_value() ||
!file_request->local_positions.contains(LocalColumnId(*mapping.file_local_id))) {
continue;
}
const auto previous_file_type = mapping.file_type;
const auto previous_file_children = mapping.projected_file_children;
RETURN_IF_ERROR(apply_scan_projection_to_mapping_file_type(*file_request, &mapping));
output_shape_changed |=
previous_file_type == nullptr || mapping.file_type == nullptr ||
!previous_file_type->equals(*mapping.file_type) ||
!same_projected_file_shape(previous_file_children, mapping.projected_file_children);
rebuild_projection(&mapping, file_request->non_predicate_position(
LocalColumnId(*mapping.file_local_id)));
}
if (output_shape_changed) {
// Localized conjuncts embed nested child ordinals from the pre-hook projection. Scanner
// still evaluates the original table conjuncts, so discard stale file-local copies rather
// than allowing a late equality-delete dependency to reinterpret another child.
file_request->conjuncts.clear();
file_request->metadata_pruning_safe_conjunct_count = 0;
}
RETURN_IF_ERROR(_build_filter_entries(*file_request));
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_predicate_mapping(GlobalIndex global_index) {
for (auto& mapping : _predicate_mappings) {
if (mapping.global_index == global_index) {
return &mapping;
}
}
return nullptr;
}
ColumnMapping* TableColumnMapper::_find_filter_mapping(GlobalIndex global_index) {
if (auto* mapping = _find_predicate_mapping(global_index); mapping != nullptr) {
return mapping;
}
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;
auto filter_mappings = _filter_visible_mappings();
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;
}
// Nested eager projection is an FE contract. Without predicate_access_paths the
// all-access-path mapping is read as one unit instead of inferring another subtree
// from VExpr and risking a shape that disagrees with final materialization.
RETURN_IF_ERROR(add_scan_column(file_request, mapping, enable_lazy_materialization(),
force_full_complex_scan_projection(),
enable_variant_leaf_projection()));
auto* output_mapping = _find_mapping(global_index);
if (!enable_independent_predicate_projection() || output_mapping == nullptr ||
mapping == output_mapping || !output_mapping->file_local_id.has_value()) {
continue;
}
LocalColumnIndex output_projection;
RETURN_IF_ERROR(
build_scan_projection(output_mapping, force_full_complex_scan_projection(),
enable_variant_leaf_projection(), &output_projection));
const auto* predicate_projection = find_scan_projection(file_request->predicate_columns,
output_projection.column_id());
DORIS_CHECK(predicate_projection != nullptr);
if (!same_local_column_index(*predicate_projection, output_projection)) {
FileScanRequestBuilder builder(file_request);
RETURN_IF_ERROR(
builder.add_deferred_non_predicate_column(std::move(output_projection)));
}
}
}
// 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 : _predicate_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,
/*predicate_mapping=*/true));
}
}
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, _mappings, _filter_entries, *file_request);
std::vector<bool> localized_table_filters(table_filters.size(), false);
for (size_t table_filter_idx = 0; table_filter_idx < table_filters.size(); ++table_filter_idx) {
const auto& table_filter = table_filters[table_filter_idx];
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));
localized_table_filters[table_filter_idx] = true;
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);
}
}
}
}
bool in_metadata_pruning_safe_prefix = true;
bool in_constant_pruning_safe_prefix = true;
for (size_t table_filter_idx = 0; table_filter_idx < table_filters.size(); ++table_filter_idx) {
const auto& table_filter = table_filters[table_filter_idx];
const bool constant_filter =
table_filter_has_only_constant_entries(table_filter, _filter_entries);
if (!table_filter.metadata_pruning_safe) {
in_metadata_pruning_safe_prefix = false;
}
if (constant_filter) {
// Safe constant filters are evaluated before opening the file and do not occupy a
// file-local conjunct position.
} else if (!localized_table_filters[table_filter_idx]) {
in_metadata_pruning_safe_prefix = false;
} else if (in_metadata_pruning_safe_prefix) {
++file_request->metadata_pruning_safe_conjunct_count;
}
if (in_constant_pruning_safe_prefix &&
(constant_filter || localized_table_filters[table_filter_idx])) {
++file_request->constant_pruning_safe_table_filter_count;
} else {
// A rejected localization must preserve all later filters for post-materialization
// evaluation, even if a later constant would otherwise prune the complete split.
in_constant_pruning_safe_prefix = false;
}
}
// 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, merge any independent output/filter subtrees and move the result 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;
}
LocalColumnIndex demoted_projection = std::move(*predicate_it);
file_request->predicate_columns.erase(predicate_it);
const auto output_it = std::ranges::find_if(file_request->non_predicate_columns,
[local_id](const LocalColumnIndex& projection) {
return projection.column_id() == local_id;
});
if (output_it != file_request->non_predicate_columns.end()) {
// A rejected complex predicate still needs its filter-only subtree on Scanner's
// table-level path. Merge it with the deferred output before collapsing the two block
// positions, or branch-4.1 can silently drop the child used by the residual filter.
RETURN_IF_ERROR(merge_local_column_index(&demoted_projection, *output_it));
file_request->non_predicate_columns.erase(output_it);
file_request->non_predicate_positions.erase(local_id);
std::erase(file_request->predicate_only_columns, local_id);
}
FileScanRequestBuilder builder(file_request);
RETURN_IF_ERROR(builder.add_non_predicate_column(std::move(demoted_projection)));
}
// Predicate demotion can widen a nested projection after mappings were localized. Reapply the
// final shape so TableReader interprets the same child ordinals that FileReader returns.
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));
}
}
// Final readers allocate a dense file block, so every retained slot must follow the same compaction.
compact_file_block_positions(file_request);
RETURN_IF_ERROR(_build_filter_entries(*file_request));
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;
}
if (_options.mode == TableColumnMappingMode::BY_FIELD_ID) {
return find_column_by_field_id(table_column, file_schema,
_options.allow_idless_complex_wrapper_projection);
}
return matcher_for_mode(_options.mode).find(table_column, file_schema);
}
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->timestamp_is_adjusted_to_utc = file_field.timestamp_is_adjusted_to_utc;
mapping->file_type = file_field.type;
// Access paths are relative to the Variant terminal, so recursive complex mappings must carry
// them instead of leaving them only on the top-level table column.
mapping->variant_access_paths = table_column.variant_access_paths;
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) {
if (table_child.default_expr == nullptr &&
table_child.initial_default_value.has_value()) {
return Status::InvalidArgument(
"Missing typed initial-default expression for table field '{}'",
table_child.name);
}
if (_options.reject_missing_required_field && table_child.is_optional.has_value() &&
!*table_child.is_optional && table_child.default_expr == nullptr) {
return Status::InvalidArgument("Missing required field: {}", table_child.name);
}
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.default_expr = table_child.default_expr;
child_mapping.variant_access_paths = table_child.variant_access_paths;
child_mapping.filter_conversion = FilterConversionType::FINALIZE_ONLY;
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