github-actions[bot] commented on code in PR #65559:
URL: https://github.com/apache/doris/pull/65559#discussion_r3594357331


##########
fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergUtils.java:
##########
@@ -1047,10 +1114,14 @@ public static List<Column> parseSchema(Schema schema, 
boolean enableMappingVarbi
         List<Types.NestedField> columns = schema.columns();
         List<Column> resSchema = 
Lists.newArrayListWithCapacity(columns.size());
         for (Types.NestedField field : columns) {
-            Column column = new Column(field.name().toLowerCase(Locale.ROOT),
+            String initialDefault = null;

Review Comment:
   [P1] Preserve nested Iceberg initial defaults end to end
   
   This loop assigns `initialDefault` only to top-level fields; 
`updateIcebergColumnUniqueId()` recursively copies IDs but not defaults, so a 
nested `INT` default never reaches the Thrift schema. Nested binary defaults 
can arrive through the separate Base64 map, but the V2 access-path/child 
mapping then drops them and materializes NULL/type defaults. Consequently old 
files return and filter on the wrong value for newly added nested fields. 
Please serialize defaults recursively here and preserve/parse them through the 
BE child mapping, with old-file projection and predicate tests.



##########
be/src/core/data_type_serde/data_type_datev2_serde.cpp:
##########
@@ -126,6 +127,29 @@ Status 
DataTypeDateV2SerDe::read_column_from_arrow(IColumn& column, const arrow:
     return Status::OK();
 }
 
+Status DataTypeDateV2SerDe::read_column_from_decoded_values(IColumn& column,
+                                                            const 
DecodedColumnView& view) const {
+    if (view.value_kind != DecodedValueKind::INT32) {
+        return decoded_column_view_handle_conversion_failure(
+                column, view, Status::NotSupported("DATEV2 decoded reader 
expects INT32 source"));
+    }
+    if (view.values == nullptr && 
decoded_column_view_has_non_null_value(view)) {
+        return Status::Corruption("Decoded value buffer is null for {}", 
column.get_name());
+    }
+    auto& data = assert_cast<ColumnDateV2&>(column).get_data();
+    const auto* values = reinterpret_cast<const int32_t*>(view.values);
+    for (int64_t row = 0; row < view.row_count; ++row) {
+        if (decoded_column_view_row_is_null(view, row)) {
+            data.push_back(DateV2Value<DateV2ValueType>());
+            continue;
+        }
+        DateV2Value<DateV2ValueType> date_v2;
+        date_v2.get_date_from_daynr(values[row] + date_threshold);

Review Comment:
   [P1] Validate decoded Parquet DATE values before appending
   
   `get_date_from_daynr()` returns false outside Doris's date range, but this 
path ignores that result and unconditionally appends the default/invalid value. 
The `int32_t` addition can also overflow for extreme, valid physical DATE 
encodings. Consequently an out-of-range external date becomes a non-null bad 
value and can change predicates/results instead of raising a data-quality error 
or becoming NULL in nullable non-strict mode. Compute the day number in a wider 
type and route range failures through the decoded conversion-failure policy.



##########
be/src/exec/operator/file_scan_operator.cpp:
##########
@@ -91,6 +104,28 @@ ScannerScheduler* 
FileScanLocalState::scan_scheduler(RuntimeState* state) const
     return state->get_query_ctx()->get_remote_scan_scheduler();
 }
 
+#ifdef BE_TEST
+bool FileScanLocalState::TEST_should_use_file_scanner_v2(const TQueryOptions& 
query_options,
+                                                         bool is_load,
+                                                         const 
TFileScanRangeParams& scan_params) {
+    return _should_use_file_scanner_v2(query_options, is_load, scan_params);
+}
+#endif
+
+bool FileScanLocalState::_should_use_file_scanner_v2(const TQueryOptions& 
query_options,
+                                                     bool is_load,
+                                                     const 
TFileScanRangeParams& scan_params) {
+    const bool is_transactional_hive =
+            scan_params.__isset.table_format_params &&
+            scan_params.table_format_params.table_format_type == 
"transactional_hive";
+    // JNI reader selection is stored per split, but this scan-level selector 
cannot inspect the
+    // split yet. Older FEs may omit both the scan-level Paimon marker and 
split-level reader_type,
+    // so keep JNI scans on V1 until scanner selection can distinguish every 
compatibility shape.
+    return query_options.__isset.enable_file_scanner_v2 && 
query_options.enable_file_scanner_v2 &&

Review Comment:
   [P1] Keep unsupported query formats on the legacy scanner
   
   With the new default-true flag, this scan-level predicate selects V2 for 
every non-load format except WAL/JNI. Query AVRO and generic Arrow ranges 
therefore reach `FileScannerV2::is_supported()`, which rejects AVRO and 
non-remote-Doris Arrow on the first split, even though V1 has `AvroJNIReader` 
and `ArrowStreamReader` implementations for them. Gate this selector with the 
same complete capability matrix (or explicitly retain V1 for these formats) and 
cover both in the selector test.



##########
be/src/format_v2/table_reader.cpp:
##########
@@ -0,0 +1,1024 @@
+// 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/table_reader.h"
+
+#include <gen_cpp/ExternalTableSchema_types.h>
+#include <gen_cpp/PlanNodes_types.h>
+#include <gen_cpp/Types_types.h>
+
+#include <algorithm>
+#include <memory>
+#include <ranges>
+#include <set>
+#include <sstream>
+#include <utility>
+#include <vector>
+
+#include "common/cast_set.h"
+#include "common/status.h"
+#include "core/assert_cast.h"
+#include "core/data_type/data_type_array.h"
+#include "core/data_type/data_type_factory.hpp"
+#include "core/data_type/data_type_map.h"
+#include "core/data_type/data_type_struct.h"
+#include "core/data_type/primitive_type.h"
+#include "exprs/vexpr_context.h"
+#include "exprs/vslot_ref.h"
+#include "format/table/iceberg_delete_file_reader_helper.h"
+#include "format/table/paimon_reader.h"
+#include "format_v2/column_mapper.h"
+#include "format_v2/deletion_vector_reader.h"
+#include "format_v2/delimited_text/csv_reader.h"
+#include "format_v2/delimited_text/text_reader.h"
+#include "format_v2/json/json_reader.h"
+#include "format_v2/native/native_reader.h"
+#include "format_v2/orc/orc_reader.h"
+#include "format_v2/parquet/parquet_reader.h"
+#include "storage/segment/condition_cache.h"
+#include "util/debug_points.h"
+#include "util/string_util.h"
+
+namespace doris::format {
+namespace {
+
+template <typename T, typename Formatter>
+std::string join_table_reader_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();
+}
+
+std::string file_format_to_string(FileFormat format) {
+    switch (format) {
+    case FileFormat::PARQUET:
+        return "PARQUET";
+    case FileFormat::ORC:
+        return "ORC";
+    case FileFormat::CSV:
+        return "CSV";
+    case FileFormat::JSON:
+        return "JSON";
+    case FileFormat::TEXT:
+        return "TEXT";
+    case FileFormat::JNI:
+        return "JNI";
+    case FileFormat::NATIVE:
+        return "NATIVE";
+    case FileFormat::ARROW:
+        return "ARROW";
+    }
+    return "UNKNOWN";
+}
+
+std::string push_down_agg_to_string(TPushAggOp::type op) {
+    switch (op) {
+    case TPushAggOp::NONE:
+        return "NONE";
+    case TPushAggOp::COUNT:
+        return "COUNT";
+    case TPushAggOp::MINMAX:
+        return "MINMAX";
+    case TPushAggOp::MIX:
+        return "MIX";
+    case TPushAggOp::COUNT_ON_INDEX:
+        return "COUNT_ON_INDEX";
+    }
+    return "UNKNOWN";
+}
+
+std::string current_file_debug_string(const std::unique_ptr<ScanTask>& task) {
+    if (task == nullptr || task->data_file == nullptr) {
+        return "null";
+    }
+    const auto& file = *task->data_file;
+    std::ostringstream out;
+    out << "FileDescription{path=" << file.path << ", file_size=" << 
file.file_size
+        << ", range_start_offset=" << file.range_start_offset << ", 
range_size=" << file.range_size
+        << ", mtime=" << file.mtime << ", fs_name=" << file.fs_name
+        << ", file_cache_admission=" << file.file_cache_admission << "}";
+    return out.str();
+}
+
+std::string partition_values_debug_string(const std::map<std::string, Field>& 
partition_values) {
+    std::ostringstream out;
+    out << "{";
+    size_t idx = 0;
+    for (const auto& [key, _] : partition_values) {
+        if (idx++ > 0) {
+            out << ", ";
+        }
+        out << key;
+    }
+    out << "}";
+    return out.str();
+}
+
+const schema::external::TField* get_field_ptr(const 
schema::external::TFieldPtr& field_ptr) {
+    if (!field_ptr.__isset.field_ptr || field_ptr.field_ptr == nullptr) {
+        return nullptr;
+    }
+    return field_ptr.field_ptr.get();
+}
+
+bool external_field_matches_name(const schema::external::TField& field, const 
std::string& name) {
+    if (field.__isset.name && to_lower(field.name) == to_lower(name)) {
+        return true;
+    }
+    return field.__isset.name_mapping &&
+           std::ranges::any_of(field.name_mapping, [&](const std::string& 
alias) {
+               return to_lower(alias) == to_lower(name);
+           });
+}
+
+DataTypePtr find_struct_child_type_by_external_field(const DataTypeStruct& 
struct_type,
+                                                     const 
schema::external::TField& field) {
+    for (size_t field_idx = 0; field_idx < struct_type.get_elements().size(); 
++field_idx) {
+        if (external_field_matches_name(field, 
struct_type.get_element_name(field_idx))) {
+            return struct_type.get_element(field_idx);
+        }
+    }
+    return nullptr;
+}
+
+DataTypePtr restore_current_primitive_type(const schema::external::TField& 
field,
+                                           DataTypePtr fallback_type) {
+    if (!field.__isset.type) {
+        return fallback_type;
+    }
+    const auto primitive_type = thrift_to_type(field.type.type);
+    if (is_complex_type(primitive_type)) {
+        return fallback_type;
+    }
+    // The delete file can expose an older physical type, but initial defaults 
belong to the
+    // current table field. Restore that type from FE before parsing the 
default and let the table
+    // reader apply the normal promotion cast to the delete-key type.
+    return DataTypeFactory::instance().create_data_type(
+            primitive_type, false, field.type.__isset.precision ? 
field.type.precision : 0,
+            field.type.__isset.scale ? field.type.scale : 0,
+            field.type.__isset.len ? field.type.len : -1);
+}
+
+ColumnDefinition build_schema_column_from_external_field(const 
schema::external::TField& field,
+                                                         DataTypePtr type) {
+    type = restore_current_primitive_type(field, std::move(type));
+    ColumnDefinition column {
+            .identifier = field.__isset.id ? 
Field::create_field<TYPE_INT>(field.id) : Field {},
+            .name = field.__isset.name ? field.name : "",
+            .name_mapping =
+                    field.__isset.name_mapping ? field.name_mapping : 
std::vector<std::string> {},
+            .type = std::move(type),
+            .children = {},
+            .default_expr = nullptr,
+            .initial_default_value = field.__isset.initial_default_value
+                                             ? 
std::make_optional(field.initial_default_value)
+                                             : std::nullopt,
+            .initial_default_value_is_base64 = 
field.__isset.initial_default_value_is_base64 &&
+                                               
field.initial_default_value_is_base64,
+            .is_partition_key = false,
+    };
+    if (column.type == nullptr || !field.__isset.nestedField) {
+        return column;
+    }
+
+    const auto nested_type = remove_nullable(column.type);
+    switch (nested_type->get_primitive_type()) {
+    case TYPE_STRUCT: {
+        if (!field.nestedField.__isset.struct_field ||
+            !field.nestedField.struct_field.__isset.fields) {
+            return column;
+        }
+        const auto& struct_type = assert_cast<const 
DataTypeStruct&>(*nested_type);
+        for (const auto& child_ptr : field.nestedField.struct_field.fields) {
+            const auto* child_field = get_field_ptr(child_ptr);
+            if (child_field == nullptr || !child_field->__isset.name) {
+                continue;
+            }
+            auto child_type = 
find_struct_child_type_by_external_field(struct_type, *child_field);
+            if (child_type == nullptr) {
+                continue;
+            }
+            column.children.push_back(
+                    build_schema_column_from_external_field(*child_field, 
child_type));
+        }
+        break;
+    }
+    case TYPE_ARRAY: {
+        if (!field.nestedField.__isset.array_field ||
+            !field.nestedField.array_field.__isset.item_field) {
+            return column;
+        }
+        const auto* item_field = 
get_field_ptr(field.nestedField.array_field.item_field);
+        if (item_field == nullptr) {
+            return column;
+        }
+        const auto& array_type = assert_cast<const 
DataTypeArray&>(*nested_type);
+        auto child =
+                build_schema_column_from_external_field(*item_field, 
array_type.get_nested_type());
+        child.name = "element";
+        if (child.has_identifier_name()) {
+            child.identifier = Field::create_field<TYPE_STRING>(child.name);
+        }
+        column.children.push_back(std::move(child));
+        break;
+    }
+    case TYPE_MAP: {
+        if (!field.nestedField.__isset.map_field ||
+            !field.nestedField.map_field.__isset.key_field ||
+            !field.nestedField.map_field.__isset.value_field) {
+            return column;
+        }
+        const auto& map_type = assert_cast<const DataTypeMap&>(*nested_type);
+        const auto* key_field = 
get_field_ptr(field.nestedField.map_field.key_field);
+        if (key_field != nullptr) {
+            auto child =
+                    build_schema_column_from_external_field(*key_field, 
map_type.get_key_type());
+            child.name = "key";
+            if (child.has_identifier_name()) {
+                child.identifier = 
Field::create_field<TYPE_STRING>(child.name);
+            }
+            column.children.push_back(std::move(child));
+        }
+        const auto* value_field = 
get_field_ptr(field.nestedField.map_field.value_field);
+        if (value_field != nullptr) {
+            auto child = build_schema_column_from_external_field(*value_field,
+                                                                 
map_type.get_value_type());
+            child.name = "value";
+            if (child.has_identifier_name()) {
+                child.identifier = 
Field::create_field<TYPE_STRING>(child.name);
+            }
+            column.children.push_back(std::move(child));
+        }
+        break;
+    }
+    default:
+        break;
+    }
+    return column;
+}
+
+const schema::external::TField* find_external_root_field(const 
TFileScanRangeParams* params,
+                                                         const 
ColumnDefinition& column) {
+    if (params == nullptr || !params->__isset.history_schema_info ||
+        params->history_schema_info.empty()) {
+        return nullptr;
+    }
+    const auto* schema = &params->history_schema_info.front();
+    if (params->__isset.current_schema_id) {
+        for (const auto& candidate_schema : params->history_schema_info) {
+            if (candidate_schema.__isset.schema_id &&
+                candidate_schema.schema_id == params->current_schema_id) {
+                schema = &candidate_schema;
+                break;
+            }
+        }
+    }
+    if (!schema->__isset.root_field || !schema->root_field.__isset.fields) {
+        return nullptr;
+    }
+    for (const auto& field_ptr : schema->root_field.fields) {
+        const auto* field = get_field_ptr(field_ptr);
+        if (field == nullptr) {
+            continue;
+        }
+        if (external_field_matches_name(*field, column.name)) {
+            return field;
+        }
+    }
+    return nullptr;
+}
+
+std::string expr_context_debug_string(const VExprContextSPtr& context) {
+    if (context == nullptr) {
+        return "null";
+    }
+    const auto root = context->root();
+    if (root == nullptr) {
+        return "VExprContext{root=null}";
+    }
+    std::ostringstream out;
+    out << "VExprContext{root_name=" << root->expr_name() << ", root_debug=" 
<< root->debug_string()
+        << "}";
+    return out.str();
+}
+
+std::string table_filter_debug_string(const TableFilter& filter) {
+    std::ostringstream out;
+    out << "TableFilter{conjunct=" << 
expr_context_debug_string(filter.conjunct)
+        << ", global_indices="
+        << join_table_reader_debug_strings(
+                   filter.global_indices,
+                   [](GlobalIndex global_index) { return 
std::to_string(global_index.value()); })
+        << "}";
+    return out.str();
+}
+
+bool contains_runtime_filter(const VExprContextSPtrs& conjuncts) {
+    return std::ranges::any_of(conjuncts, [](const auto& conjunct) {
+        return conjunct != nullptr && conjunct->root() != nullptr &&
+               conjunct->root()->is_rf_wrapper();
+    });
+}
+
+void collect_global_indices(const VExprSPtr& expr, std::set<GlobalIndex>* 
global_indices) {
+    if (expr == nullptr) {
+        return;
+    }
+    if (expr->is_rf_wrapper()) {
+        // RuntimeFilterExpr wraps a real predicate expression but its own 
thrift node can still
+        // look like SLOT_REF. Collect indices from the wrapped predicate; do 
not cast the wrapper
+        // itself to VSlotRef.
+        collect_global_indices(expr->get_impl(), global_indices);
+        return;
+    }
+    if (expr->is_slot_ref()) {
+        const auto* slot_ref = assert_cast<const VSlotRef*>(expr.get());
+        DORIS_CHECK(slot_ref->column_id() >= 0);
+        
global_indices->insert(GlobalIndex(cast_set<size_t>(slot_ref->column_id())));
+    }
+    for (const auto& child : expr->children()) {
+        collect_global_indices(child, global_indices);
+    }
+}
+
+Status build_table_filters_from_conjunct(const VExprContextSPtr& conjunct, 
RuntimeState* state,
+                                         std::vector<TableFilter>* 
table_filters) {
+    if (conjunct == nullptr) {
+        return Status::OK();
+    }
+    std::set<GlobalIndex> global_indices;
+    collect_global_indices(conjunct->root(), &global_indices);
+    if (!global_indices.empty()) {
+        TableFilter table_filter;
+        VExprSPtr filter_root;
+        RETURN_IF_ERROR(clone_table_expr_tree(conjunct->root(), &filter_root));
+        table_filter.conjunct = 
VExprContext::create_shared(std::move(filter_root));
+        for (const auto global_index : global_indices) {
+            table_filter.global_indices.push_back(global_index);
+        }
+        table_filters->push_back(std::move(table_filter));
+    }
+    return Status::OK();
+}
+
+Status parse_deletion_vector(const char* buf, size_t buffer_size, 
DeleteFileDesc::Format format,
+                             DeletionVector* deletion_vector) {
+    DORIS_CHECK(buf != nullptr);
+    DORIS_CHECK(deletion_vector != nullptr);
+    DORIS_CHECK(format == DeleteFileDesc::Format::PAIMON ||
+                format == DeleteFileDesc::Format::ICEBERG);
+
+    if (format == DeleteFileDesc::Format::PAIMON) {
+        RETURN_IF_ERROR(decode_paimon_deletion_vector_buffer(buf, buffer_size, 
deletion_vector));
+        return Status::OK();
+    }
+
+    return decode_iceberg_deletion_vector_buffer(buf, buffer_size, 
deletion_vector);
+}
+
+} // namespace
+
+std::shared_ptr<io::FileSystemProperties> create_system_properties(
+        const TFileScanRangeParams* scan_params) {
+    auto system_properties = std::make_shared<io::FileSystemProperties>();
+    if (scan_params == nullptr || !scan_params->__isset.file_type) {
+        system_properties->system_type = TFileType::FILE_LOCAL;
+        return system_properties;
+    }
+    system_properties->system_type = scan_params->file_type;
+    system_properties->properties = scan_params->properties;
+    system_properties->hdfs_params = scan_params->hdfs_params;
+    if (scan_params->__isset.broker_addresses) {
+        
system_properties->broker_addresses.assign(scan_params->broker_addresses.begin(),
+                                                   
scan_params->broker_addresses.end());
+    }
+    return system_properties;
+}
+
+std::string TableReader::debug_string() const {
+    std::ostringstream out;
+    out << "TableReader{format=" << file_format_to_string(_format)
+        << ", push_down_agg_type=" << 
push_down_agg_to_string(_push_down_agg_type)
+        << ", aggregate_pushdown_tried=" << _aggregate_pushdown_tried
+        << ", has_current_reader=" << (_data_reader.reader != nullptr)
+        << ", has_current_task=" << (_current_task != nullptr)
+        << ", current_file=" << current_file_debug_string(_current_task)
+        << ", has_delete_rows=" << (_delete_rows != nullptr)
+        << ", delete_row_count=" << (_delete_rows == nullptr ? 0 : 
_delete_rows->size())
+        << ", has_deletion_vector=" << (_deletion_vector != nullptr)
+        << ", deletion_vector_cardinality="
+        << (_deletion_vector == nullptr ? 0 : _deletion_vector->cardinality())
+        << ", has_system_properties=" << (_system_properties != nullptr) << ", 
system_type="
+        << (_system_properties == nullptr ? 
static_cast<int>(TFileType::FILE_LOCAL)
+                                          : 
static_cast<int>(_system_properties->system_type))
+        << ", has_scan_params=" << (_scan_params != nullptr)
+        << ", has_io_ctx=" << (_io_ctx != nullptr)
+        << ", has_runtime_state=" << (_runtime_state != nullptr)
+        << ", has_scanner_profile=" << (_scanner_profile != nullptr)
+        << ", mapper_options=" << _mapper_options.debug_string() << ", 
projected_columns="
+        << join_table_reader_debug_strings(
+                   _projected_columns,
+                   [](const ColumnDefinition& column) { return 
column.debug_string(); })
+        << ", partition_values=" << 
partition_values_debug_string(_partition_values)
+        << ", table_filters="
+        << join_table_reader_debug_strings(
+                   _table_filters,
+                   [](const TableFilter& filter) { return 
table_filter_debug_string(filter); })
+        << ", conjunct_count=" << _conjuncts.size() << ", conjuncts="
+        << join_table_reader_debug_strings(_conjuncts,
+                                           [](const VExprContextSPtr& 
conjunct) {
+                                               return 
expr_context_debug_string(conjunct);
+                                           })
+        << ", file_schema="
+        << join_table_reader_debug_strings(
+                   _data_reader.file_schema,
+                   [](const ColumnDefinition& field) { return 
field.debug_string(); })
+        << ", file_block_layout="
+        << join_table_reader_debug_strings(
+                   _data_reader.file_block_layout,
+                   [](const FileBlockColumn& column) {
+                       std::ostringstream column_out;
+                       column_out << "FileBlockColumn{file_column_id=" << 
column.file_column_id
+                                  << ", name=" << column.name << ", type="
+                                  << (column.type == nullptr ? "null" : 
column.type->get_name())
+                                  << "}";
+                       return column_out.str();
+                   })
+        << ", block_template_columns=" << _data_reader.block_template.columns()
+        << ", column_mapper="
+        << (_data_reader.column_mapper == nullptr ? "null"
+                                                  : 
_data_reader.column_mapper->debug_string())
+        << "}";
+    return out.str();
+}
+
+Status TableReader::annotate_projected_column(const TFileScanSlotInfo& 
slot_info,
+                                              ProjectedColumnBuildContext* 
context,
+                                              ColumnDefinition* column) const {
+    (void)slot_info;
+    DORIS_CHECK(context != nullptr);
+    DORIS_CHECK(column != nullptr);
+    context->schema_column.reset();
+    const auto* schema_field = find_external_root_field(context->scan_params, 
*column);
+    if (schema_field == nullptr) {
+        return Status::OK();
+    }
+    context->schema_column = 
build_schema_column_from_external_field(*schema_field, column->type);
+    column->identifier = context->schema_column->identifier;
+    column->name_mapping = context->schema_column->name_mapping;
+    return Status::OK();
+}
+
+std::optional<ColumnDefinition> 
TableReader::_find_current_table_column_by_field_id(
+        int32_t field_id, DataTypePtr type) const {
+    if (_scan_params == nullptr || !_scan_params->__isset.history_schema_info 
||
+        _scan_params->history_schema_info.empty()) {
+        return std::nullopt;
+    }
+    const auto* schema = &_scan_params->history_schema_info.front();
+    if (_scan_params->__isset.current_schema_id) {
+        for (const auto& candidate_schema : _scan_params->history_schema_info) 
{
+            if (candidate_schema.__isset.schema_id &&
+                candidate_schema.schema_id == _scan_params->current_schema_id) 
{
+                schema = &candidate_schema;
+                break;
+            }
+        }
+    }
+    if (!schema->__isset.root_field || !schema->root_field.__isset.fields) {
+        return std::nullopt;
+    }
+    for (const auto& field_ptr : schema->root_field.fields) {
+        const auto* field = get_field_ptr(field_ptr);
+        if (field != nullptr && field->__isset.id && field->id == field_id) {
+            return build_schema_column_from_external_field(*field, 
std::move(type));
+        }
+    }
+    return std::nullopt;
+}
+
+Status TableReader::init(TableReadOptions&& options) {
+    _scan_params = options.scan_params;
+    _format = options.format;
+    _io_ctx = options.io_ctx;
+    _runtime_state = options.runtime_state;
+    _scanner_profile = options.scanner_profile;
+    _file_slot_descs = options.file_slot_descs;
+    _push_down_agg_type = options.push_down_agg_type;
+    _condition_cache_digest = options.condition_cache_digest;
+    _projected_columns = std::move(options.projected_columns);
+    _system_properties = create_system_properties(_scan_params);
+    _mapper_options.mode = TableColumnMappingMode::BY_NAME;
+    _conjuncts = std::move(options.conjuncts);
+
+    if (_scanner_profile != nullptr) {
+        static const char* table_profile = "TableReader";
+        ADD_TIMER_WITH_LEVEL(_scanner_profile, table_profile, 1);
+        _profile.num_delete_files = 
ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, "NumDeleteFiles",
+                                                                 TUnit::UNIT, 
table_profile, 1);
+        _profile.num_delete_rows = 
ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, "NumDeleteRows",
+                                                                TUnit::UNIT, 
table_profile, 1);
+        _profile.parse_delete_file_time = ADD_CHILD_TIMER_WITH_LEVEL(
+                _scanner_profile, "ParseDeleteFileTime", table_profile, 1);
+        _profile.decoded_dv_cache_hit_count =
+                ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, 
"DeletionVectorDecodedCacheHitCount",
+                                             TUnit::UNIT, table_profile, 1);
+        _profile.decoded_dv_cache_miss_count = ADD_CHILD_COUNTER_WITH_LEVEL(
+                _scanner_profile, "DeletionVectorDecodedCacheMissCount", 
TUnit::UNIT, table_profile,
+                1);
+        _profile.dv_file_cache_hit_count = ADD_CHILD_COUNTER_WITH_LEVEL(
+                _scanner_profile, "DeletionVectorFileCacheHitCount", 
TUnit::UNIT, table_profile, 1);
+        _profile.dv_file_cache_miss_count =
+                ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, 
"DeletionVectorFileCacheMissCount",
+                                             TUnit::UNIT, table_profile, 1);
+        _profile.dv_file_cache_peer_read_count = ADD_CHILD_COUNTER_WITH_LEVEL(
+                _scanner_profile, "DeletionVectorFileCachePeerReadCount", 
TUnit::UNIT,
+                table_profile, 1);
+        _profile.exec_timer =
+                ADD_CHILD_TIMER_WITH_LEVEL(_scanner_profile, "GetBlockTime", 
table_profile, 1);
+        _profile.prepare_split_timer =
+                ADD_CHILD_TIMER_WITH_LEVEL(_scanner_profile, 
"PrepareSplitTime", table_profile, 1);
+        _profile.finalize_timer =
+                ADD_CHILD_TIMER_WITH_LEVEL(_scanner_profile, 
"FinalizeBlockTime", table_profile, 1);
+        _profile.create_reader_timer =
+                ADD_CHILD_TIMER_WITH_LEVEL(_scanner_profile, 
"CreateReaderTime", table_profile, 1);
+        _profile.pushdown_agg_timer =
+                ADD_CHILD_TIMER_WITH_LEVEL(_scanner_profile, 
"PushDownAggTime", table_profile, 1);
+        _profile.open_reader_timer =
+                ADD_CHILD_TIMER_WITH_LEVEL(_scanner_profile, "OpenReaderTime", 
table_profile, 1);
+        _profile.runtime_filter_partition_prune_timer = ADD_TIMER_WITH_LEVEL(
+                _scanner_profile, 
"FileScannerRuntimeFilterPartitionPruningTime", 1);
+        _profile.runtime_filter_partition_pruned_range_counter = 
ADD_COUNTER_WITH_LEVEL(
+                _scanner_profile, "RuntimeFilterPartitionPrunedRangeNum", 
TUnit::UNIT, 1);
+    }
+    return Status::OK();
+}
+
+Status TableReader::_build_table_filters_from_conjuncts() {
+    _table_filters.clear();
+    _constant_pruning_safe_filter_count = 0;
+    bool in_safe_prefix = true;
+    for (const auto& conjunct : _conjuncts) {
+        DORIS_CHECK(conjunct != nullptr);
+        DORIS_CHECK(conjunct->root() != nullptr);
+        // `_table_filters` omits expressions without slot references, but 
such an expression still
+        // occupies a position in the row-level conjunct order. Record how 
many localized filters
+        // precede the first unsafe original conjunct so constant pruning 
cannot jump over a
+        // slotless non-deterministic/error-preserving barrier.
+        if (in_safe_prefix && !_is_safe_to_pre_execute(conjunct)) {
+            in_safe_prefix = false;
+        }
+        if (!in_safe_prefix) {
+            continue;
+        }
+        RETURN_IF_ERROR(
+                build_table_filters_from_conjunct(conjunct, _runtime_state, 
&_table_filters));
+        _constant_pruning_safe_filter_count = _table_filters.size();
+    }
+    return Status::OK();
+}
+
+Status TableReader::_open_local_filter_exprs(const FileScanRequest& 
file_request) {
+    RowDescriptor row_desc;
+    for (const auto& conjunct : file_request.conjuncts) {
+        RETURN_IF_ERROR(conjunct->prepare(_runtime_state, row_desc));
+        RETURN_IF_ERROR(conjunct->open(_runtime_state));
+    }
+    for (const auto& delete_conjunct : file_request.delete_conjuncts) {
+        RETURN_IF_ERROR(delete_conjunct->prepare(_runtime_state, row_desc));
+        RETURN_IF_ERROR(delete_conjunct->open(_runtime_state));
+    }
+    return Status::OK();
+}
+
+bool TableReader::_should_enable_condition_cache(const FileScanRequest& 
file_request) const {
+    if (_condition_cache_digest == 0 || _push_down_agg_type == 
TPushAggOp::type::COUNT ||
+        _current_file_description == std::nullopt || _data_reader.reader == 
nullptr) {
+        return false;
+    }
+    // Condition cache is populated by file readers after evaluating 
file-local row-level
+    // conjuncts. Metadata pruning can skip row groups/pages, but it does not 
produce a per-row
+    // survivor bitmap that can safely populate the cache.
+    if (file_request.conjuncts.empty()) {
+        return false;
+    }
+    // Delete files/deletion vectors are table-format state. They may change 
independently of the
+    // data file path/mtime/size used by the external cache key, so caching 
their result can become
+    // stale. Keep delete filtering enabled, but do not read or write 
condition cache.
+    if (_delete_rows != nullptr || _deletion_vector != nullptr ||
+        !file_request.delete_conjuncts.empty()) {
+        return false;
+    }
+    // Runtime filters can arrive late and their payload is not guaranteed to 
be represented by the
+    // scan-local digest. Without a read-only mode, a MISS could insert a 
bitmap for P AND RF under
+    // the digest for only P. This mirrors the old FileScanner guard.
+    return !contains_runtime_filter(file_request.conjuncts);
+}
+
+Status TableReader::_init_reader_condition_cache(const FileScanRequest& 
file_request) {
+    _condition_cache = nullptr;
+    _condition_cache_ctx = nullptr;
+    if (!_should_enable_condition_cache(file_request)) {
+        return Status::OK();
+    }
+
+    auto* cache = segment_v2::ConditionCache::instance();
+    if (cache == nullptr) {
+        return Status::OK();
+    }
+    const auto& file = *_current_file_description;
+    _condition_cache_key = segment_v2::ConditionCache::ExternalCacheKey(
+            file.path, file.mtime, file.file_size, _condition_cache_digest, 
file.range_start_offset,
+            file.range_size,
+            
segment_v2::ConditionCache::ExternalCacheKey::BASE_GRANULE_AWARE_VERSION);
+
+    segment_v2::ConditionCacheHandle handle;
+    const bool condition_cache_hit = cache->lookup(_condition_cache_key, 
&handle);
+    if (condition_cache_hit) {
+        _condition_cache = handle.get_filter_result();
+        ++_condition_cache_hit_count;
+    } else {
+        const int64_t total_rows = _data_reader.reader->get_total_rows();
+        if (total_rows <= 0) {
+            return Status::OK();
+        }
+        // Add one guard granule for split ranges that start in the middle of 
a granule. A guard
+        // false bit beyond the real range never overlaps real rows, but 
avoids boundary overflow
+        // when a reader marks the last partial granule.
+        const size_t num_granules = (total_rows + 
ConditionCacheContext::GRANULE_SIZE - 1) /
+                                    ConditionCacheContext::GRANULE_SIZE;
+        _condition_cache = std::make_shared<std::vector<bool>>(num_granules + 
1, false);
+    }
+
+    if (_condition_cache != nullptr) {
+        _condition_cache_ctx = std::make_shared<ConditionCacheContext>();
+        _condition_cache_ctx->is_hit = condition_cache_hit;
+        _condition_cache_ctx->filter_result = _condition_cache;
+        _condition_cache_ctx->num_granules = _condition_cache->size();
+        if (condition_cache_hit) {
+            _condition_cache_ctx->base_granule = handle.get_base_granule();
+        }
+        _data_reader.reader->set_condition_cache_context(_condition_cache_ctx);
+    }
+    return Status::OK();
+}
+
+void TableReader::_finalize_reader_condition_cache() {
+    if (_condition_cache_ctx == nullptr || _condition_cache_ctx->is_hit) {
+        _condition_cache = nullptr;
+        _condition_cache_ctx = nullptr;
+        return;
+    }
+    // LIMIT or scanner cancellation may close a reader before all selected 
row ranges are visited.
+    // Unvisited granules remain false in a MISS bitmap, so inserting a 
partial bitmap would make a
+    // later HIT skip valid rows. Only publish cache entries after the 
physical reader reaches EOF.
+    if (!_current_reader_reached_eof) {
+        _condition_cache = nullptr;
+        _condition_cache_ctx = nullptr;
+        return;
+    }
+    DORIS_CHECK(_condition_cache_ctx->num_granules <= 
_condition_cache->size());
+    _condition_cache->resize(_condition_cache_ctx->num_granules);
+    segment_v2::ConditionCache::instance()->insert(
+            _condition_cache_key, std::move(_condition_cache), 
_condition_cache_ctx->base_granule);
+    _condition_cache = nullptr;
+    _condition_cache_ctx = nullptr;
+}
+
+Status TableReader::create_next_reader(bool* eos) {
+    SCOPED_TIMER(_profile.create_reader_timer);
+    DCHECK(_data_reader.reader == nullptr);
+    if (_current_task == nullptr) {
+        *eos = true;
+        return Status::OK();
+    }
+
+    RETURN_IF_ERROR(create_file_reader(&_data_reader.reader));
+    DORIS_CHECK(_data_reader.reader != nullptr);
+    if (_batch_size > 0) {
+        _data_reader.reader->set_batch_size(_batch_size);
+    }
+    Status st = _data_reader.reader->init(_runtime_state);
+    if (!st.ok()) {
+        if (_io_ctx != nullptr && _io_ctx->should_stop && 
st.is<ErrorCode::END_OF_FILE>()) {

Review Comment:
   [P1] Treat reader EOF as an empty split
   
   A zero-byte CSV/Text file deliberately returns `END_OF_FILE` from 
`DelimitedTextReader::_open_file()`, and a valid header-only Native file 
returns the same status during schema probing. This branch only converts EOF 
when `should_stop` is set, so normal empty files escape through 
`RETURN_IF_ERROR` and fail the entire FileScannerV2 query; the legacy scanner 
skips such readers. Please close/reset the reader and finish this split on 
non-cancellation EOF as well, with an empty-then-nonempty split regression.



##########
be/src/format_v2/table_reader.cpp:
##########
@@ -0,0 +1,1024 @@
+// 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/table_reader.h"
+
+#include <gen_cpp/ExternalTableSchema_types.h>
+#include <gen_cpp/PlanNodes_types.h>
+#include <gen_cpp/Types_types.h>
+
+#include <algorithm>
+#include <memory>
+#include <ranges>
+#include <set>
+#include <sstream>
+#include <utility>
+#include <vector>
+
+#include "common/cast_set.h"
+#include "common/status.h"
+#include "core/assert_cast.h"
+#include "core/data_type/data_type_array.h"
+#include "core/data_type/data_type_factory.hpp"
+#include "core/data_type/data_type_map.h"
+#include "core/data_type/data_type_struct.h"
+#include "core/data_type/primitive_type.h"
+#include "exprs/vexpr_context.h"
+#include "exprs/vslot_ref.h"
+#include "format/table/iceberg_delete_file_reader_helper.h"
+#include "format/table/paimon_reader.h"
+#include "format_v2/column_mapper.h"
+#include "format_v2/deletion_vector_reader.h"
+#include "format_v2/delimited_text/csv_reader.h"
+#include "format_v2/delimited_text/text_reader.h"
+#include "format_v2/json/json_reader.h"
+#include "format_v2/native/native_reader.h"
+#include "format_v2/orc/orc_reader.h"
+#include "format_v2/parquet/parquet_reader.h"
+#include "storage/segment/condition_cache.h"
+#include "util/debug_points.h"
+#include "util/string_util.h"
+
+namespace doris::format {
+namespace {
+
+template <typename T, typename Formatter>
+std::string join_table_reader_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();
+}
+
+std::string file_format_to_string(FileFormat format) {
+    switch (format) {
+    case FileFormat::PARQUET:
+        return "PARQUET";
+    case FileFormat::ORC:
+        return "ORC";
+    case FileFormat::CSV:
+        return "CSV";
+    case FileFormat::JSON:
+        return "JSON";
+    case FileFormat::TEXT:
+        return "TEXT";
+    case FileFormat::JNI:
+        return "JNI";
+    case FileFormat::NATIVE:
+        return "NATIVE";
+    case FileFormat::ARROW:
+        return "ARROW";
+    }
+    return "UNKNOWN";
+}
+
+std::string push_down_agg_to_string(TPushAggOp::type op) {
+    switch (op) {
+    case TPushAggOp::NONE:
+        return "NONE";
+    case TPushAggOp::COUNT:
+        return "COUNT";
+    case TPushAggOp::MINMAX:
+        return "MINMAX";
+    case TPushAggOp::MIX:
+        return "MIX";
+    case TPushAggOp::COUNT_ON_INDEX:
+        return "COUNT_ON_INDEX";
+    }
+    return "UNKNOWN";
+}
+
+std::string current_file_debug_string(const std::unique_ptr<ScanTask>& task) {
+    if (task == nullptr || task->data_file == nullptr) {
+        return "null";
+    }
+    const auto& file = *task->data_file;
+    std::ostringstream out;
+    out << "FileDescription{path=" << file.path << ", file_size=" << 
file.file_size
+        << ", range_start_offset=" << file.range_start_offset << ", 
range_size=" << file.range_size
+        << ", mtime=" << file.mtime << ", fs_name=" << file.fs_name
+        << ", file_cache_admission=" << file.file_cache_admission << "}";
+    return out.str();
+}
+
+std::string partition_values_debug_string(const std::map<std::string, Field>& 
partition_values) {
+    std::ostringstream out;
+    out << "{";
+    size_t idx = 0;
+    for (const auto& [key, _] : partition_values) {
+        if (idx++ > 0) {
+            out << ", ";
+        }
+        out << key;
+    }
+    out << "}";
+    return out.str();
+}
+
+const schema::external::TField* get_field_ptr(const 
schema::external::TFieldPtr& field_ptr) {
+    if (!field_ptr.__isset.field_ptr || field_ptr.field_ptr == nullptr) {
+        return nullptr;
+    }
+    return field_ptr.field_ptr.get();
+}
+
+bool external_field_matches_name(const schema::external::TField& field, const 
std::string& name) {
+    if (field.__isset.name && to_lower(field.name) == to_lower(name)) {
+        return true;
+    }
+    return field.__isset.name_mapping &&
+           std::ranges::any_of(field.name_mapping, [&](const std::string& 
alias) {
+               return to_lower(alias) == to_lower(name);
+           });
+}
+
+DataTypePtr find_struct_child_type_by_external_field(const DataTypeStruct& 
struct_type,
+                                                     const 
schema::external::TField& field) {
+    for (size_t field_idx = 0; field_idx < struct_type.get_elements().size(); 
++field_idx) {
+        if (external_field_matches_name(field, 
struct_type.get_element_name(field_idx))) {
+            return struct_type.get_element(field_idx);
+        }
+    }
+    return nullptr;
+}
+
+DataTypePtr restore_current_primitive_type(const schema::external::TField& 
field,
+                                           DataTypePtr fallback_type) {
+    if (!field.__isset.type) {
+        return fallback_type;
+    }
+    const auto primitive_type = thrift_to_type(field.type.type);
+    if (is_complex_type(primitive_type)) {
+        return fallback_type;
+    }
+    // The delete file can expose an older physical type, but initial defaults 
belong to the
+    // current table field. Restore that type from FE before parsing the 
default and let the table
+    // reader apply the normal promotion cast to the delete-key type.
+    return DataTypeFactory::instance().create_data_type(
+            primitive_type, false, field.type.__isset.precision ? 
field.type.precision : 0,
+            field.type.__isset.scale ? field.type.scale : 0,
+            field.type.__isset.len ? field.type.len : -1);
+}
+
+ColumnDefinition build_schema_column_from_external_field(const 
schema::external::TField& field,
+                                                         DataTypePtr type) {
+    type = restore_current_primitive_type(field, std::move(type));
+    ColumnDefinition column {
+            .identifier = field.__isset.id ? 
Field::create_field<TYPE_INT>(field.id) : Field {},
+            .name = field.__isset.name ? field.name : "",
+            .name_mapping =
+                    field.__isset.name_mapping ? field.name_mapping : 
std::vector<std::string> {},
+            .type = std::move(type),
+            .children = {},
+            .default_expr = nullptr,
+            .initial_default_value = field.__isset.initial_default_value
+                                             ? 
std::make_optional(field.initial_default_value)
+                                             : std::nullopt,
+            .initial_default_value_is_base64 = 
field.__isset.initial_default_value_is_base64 &&
+                                               
field.initial_default_value_is_base64,
+            .is_partition_key = false,
+    };
+    if (column.type == nullptr || !field.__isset.nestedField) {
+        return column;
+    }
+
+    const auto nested_type = remove_nullable(column.type);
+    switch (nested_type->get_primitive_type()) {
+    case TYPE_STRUCT: {
+        if (!field.nestedField.__isset.struct_field ||
+            !field.nestedField.struct_field.__isset.fields) {
+            return column;
+        }
+        const auto& struct_type = assert_cast<const 
DataTypeStruct&>(*nested_type);
+        for (const auto& child_ptr : field.nestedField.struct_field.fields) {
+            const auto* child_field = get_field_ptr(child_ptr);
+            if (child_field == nullptr || !child_field->__isset.name) {
+                continue;
+            }
+            auto child_type = 
find_struct_child_type_by_external_field(struct_type, *child_field);
+            if (child_type == nullptr) {
+                continue;
+            }
+            column.children.push_back(
+                    build_schema_column_from_external_field(*child_field, 
child_type));
+        }
+        break;
+    }
+    case TYPE_ARRAY: {
+        if (!field.nestedField.__isset.array_field ||
+            !field.nestedField.array_field.__isset.item_field) {
+            return column;
+        }
+        const auto* item_field = 
get_field_ptr(field.nestedField.array_field.item_field);
+        if (item_field == nullptr) {
+            return column;
+        }
+        const auto& array_type = assert_cast<const 
DataTypeArray&>(*nested_type);
+        auto child =
+                build_schema_column_from_external_field(*item_field, 
array_type.get_nested_type());
+        child.name = "element";
+        if (child.has_identifier_name()) {
+            child.identifier = Field::create_field<TYPE_STRING>(child.name);
+        }
+        column.children.push_back(std::move(child));
+        break;
+    }
+    case TYPE_MAP: {
+        if (!field.nestedField.__isset.map_field ||
+            !field.nestedField.map_field.__isset.key_field ||
+            !field.nestedField.map_field.__isset.value_field) {
+            return column;
+        }
+        const auto& map_type = assert_cast<const DataTypeMap&>(*nested_type);
+        const auto* key_field = 
get_field_ptr(field.nestedField.map_field.key_field);
+        if (key_field != nullptr) {
+            auto child =
+                    build_schema_column_from_external_field(*key_field, 
map_type.get_key_type());
+            child.name = "key";
+            if (child.has_identifier_name()) {
+                child.identifier = 
Field::create_field<TYPE_STRING>(child.name);
+            }
+            column.children.push_back(std::move(child));
+        }
+        const auto* value_field = 
get_field_ptr(field.nestedField.map_field.value_field);
+        if (value_field != nullptr) {
+            auto child = build_schema_column_from_external_field(*value_field,
+                                                                 
map_type.get_value_type());
+            child.name = "value";
+            if (child.has_identifier_name()) {
+                child.identifier = 
Field::create_field<TYPE_STRING>(child.name);
+            }
+            column.children.push_back(std::move(child));
+        }
+        break;
+    }
+    default:
+        break;
+    }
+    return column;
+}
+
+const schema::external::TField* find_external_root_field(const 
TFileScanRangeParams* params,
+                                                         const 
ColumnDefinition& column) {
+    if (params == nullptr || !params->__isset.history_schema_info ||
+        params->history_schema_info.empty()) {
+        return nullptr;
+    }
+    const auto* schema = &params->history_schema_info.front();
+    if (params->__isset.current_schema_id) {
+        for (const auto& candidate_schema : params->history_schema_info) {
+            if (candidate_schema.__isset.schema_id &&
+                candidate_schema.schema_id == params->current_schema_id) {
+                schema = &candidate_schema;
+                break;
+            }
+        }
+    }
+    if (!schema->__isset.root_field || !schema->root_field.__isset.fields) {
+        return nullptr;
+    }
+    for (const auto& field_ptr : schema->root_field.fields) {
+        const auto* field = get_field_ptr(field_ptr);
+        if (field == nullptr) {
+            continue;
+        }
+        if (external_field_matches_name(*field, column.name)) {
+            return field;
+        }
+    }
+    return nullptr;
+}
+
+std::string expr_context_debug_string(const VExprContextSPtr& context) {
+    if (context == nullptr) {
+        return "null";
+    }
+    const auto root = context->root();
+    if (root == nullptr) {
+        return "VExprContext{root=null}";
+    }
+    std::ostringstream out;
+    out << "VExprContext{root_name=" << root->expr_name() << ", root_debug=" 
<< root->debug_string()
+        << "}";
+    return out.str();
+}
+
+std::string table_filter_debug_string(const TableFilter& filter) {
+    std::ostringstream out;
+    out << "TableFilter{conjunct=" << 
expr_context_debug_string(filter.conjunct)
+        << ", global_indices="
+        << join_table_reader_debug_strings(
+                   filter.global_indices,
+                   [](GlobalIndex global_index) { return 
std::to_string(global_index.value()); })
+        << "}";
+    return out.str();
+}
+
+bool contains_runtime_filter(const VExprContextSPtrs& conjuncts) {
+    return std::ranges::any_of(conjuncts, [](const auto& conjunct) {
+        return conjunct != nullptr && conjunct->root() != nullptr &&
+               conjunct->root()->is_rf_wrapper();
+    });
+}
+
+void collect_global_indices(const VExprSPtr& expr, std::set<GlobalIndex>* 
global_indices) {
+    if (expr == nullptr) {
+        return;
+    }
+    if (expr->is_rf_wrapper()) {
+        // RuntimeFilterExpr wraps a real predicate expression but its own 
thrift node can still
+        // look like SLOT_REF. Collect indices from the wrapped predicate; do 
not cast the wrapper
+        // itself to VSlotRef.
+        collect_global_indices(expr->get_impl(), global_indices);
+        return;
+    }
+    if (expr->is_slot_ref()) {
+        const auto* slot_ref = assert_cast<const VSlotRef*>(expr.get());
+        DORIS_CHECK(slot_ref->column_id() >= 0);
+        
global_indices->insert(GlobalIndex(cast_set<size_t>(slot_ref->column_id())));
+    }
+    for (const auto& child : expr->children()) {
+        collect_global_indices(child, global_indices);
+    }
+}
+
+Status build_table_filters_from_conjunct(const VExprContextSPtr& conjunct, 
RuntimeState* state,
+                                         std::vector<TableFilter>* 
table_filters) {
+    if (conjunct == nullptr) {
+        return Status::OK();
+    }
+    std::set<GlobalIndex> global_indices;
+    collect_global_indices(conjunct->root(), &global_indices);
+    if (!global_indices.empty()) {
+        TableFilter table_filter;
+        VExprSPtr filter_root;
+        RETURN_IF_ERROR(clone_table_expr_tree(conjunct->root(), &filter_root));
+        table_filter.conjunct = 
VExprContext::create_shared(std::move(filter_root));
+        for (const auto global_index : global_indices) {
+            table_filter.global_indices.push_back(global_index);
+        }
+        table_filters->push_back(std::move(table_filter));
+    }
+    return Status::OK();
+}
+
+Status parse_deletion_vector(const char* buf, size_t buffer_size, 
DeleteFileDesc::Format format,
+                             DeletionVector* deletion_vector) {
+    DORIS_CHECK(buf != nullptr);
+    DORIS_CHECK(deletion_vector != nullptr);
+    DORIS_CHECK(format == DeleteFileDesc::Format::PAIMON ||
+                format == DeleteFileDesc::Format::ICEBERG);
+
+    if (format == DeleteFileDesc::Format::PAIMON) {
+        RETURN_IF_ERROR(decode_paimon_deletion_vector_buffer(buf, buffer_size, 
deletion_vector));
+        return Status::OK();
+    }
+
+    return decode_iceberg_deletion_vector_buffer(buf, buffer_size, 
deletion_vector);
+}
+
+} // namespace
+
+std::shared_ptr<io::FileSystemProperties> create_system_properties(
+        const TFileScanRangeParams* scan_params) {
+    auto system_properties = std::make_shared<io::FileSystemProperties>();
+    if (scan_params == nullptr || !scan_params->__isset.file_type) {
+        system_properties->system_type = TFileType::FILE_LOCAL;
+        return system_properties;
+    }
+    system_properties->system_type = scan_params->file_type;
+    system_properties->properties = scan_params->properties;
+    system_properties->hdfs_params = scan_params->hdfs_params;
+    if (scan_params->__isset.broker_addresses) {
+        
system_properties->broker_addresses.assign(scan_params->broker_addresses.begin(),
+                                                   
scan_params->broker_addresses.end());
+    }
+    return system_properties;
+}
+
+std::string TableReader::debug_string() const {
+    std::ostringstream out;
+    out << "TableReader{format=" << file_format_to_string(_format)
+        << ", push_down_agg_type=" << 
push_down_agg_to_string(_push_down_agg_type)
+        << ", aggregate_pushdown_tried=" << _aggregate_pushdown_tried
+        << ", has_current_reader=" << (_data_reader.reader != nullptr)
+        << ", has_current_task=" << (_current_task != nullptr)
+        << ", current_file=" << current_file_debug_string(_current_task)
+        << ", has_delete_rows=" << (_delete_rows != nullptr)
+        << ", delete_row_count=" << (_delete_rows == nullptr ? 0 : 
_delete_rows->size())
+        << ", has_deletion_vector=" << (_deletion_vector != nullptr)
+        << ", deletion_vector_cardinality="
+        << (_deletion_vector == nullptr ? 0 : _deletion_vector->cardinality())
+        << ", has_system_properties=" << (_system_properties != nullptr) << ", 
system_type="
+        << (_system_properties == nullptr ? 
static_cast<int>(TFileType::FILE_LOCAL)
+                                          : 
static_cast<int>(_system_properties->system_type))
+        << ", has_scan_params=" << (_scan_params != nullptr)
+        << ", has_io_ctx=" << (_io_ctx != nullptr)
+        << ", has_runtime_state=" << (_runtime_state != nullptr)
+        << ", has_scanner_profile=" << (_scanner_profile != nullptr)
+        << ", mapper_options=" << _mapper_options.debug_string() << ", 
projected_columns="
+        << join_table_reader_debug_strings(
+                   _projected_columns,
+                   [](const ColumnDefinition& column) { return 
column.debug_string(); })
+        << ", partition_values=" << 
partition_values_debug_string(_partition_values)
+        << ", table_filters="
+        << join_table_reader_debug_strings(
+                   _table_filters,
+                   [](const TableFilter& filter) { return 
table_filter_debug_string(filter); })
+        << ", conjunct_count=" << _conjuncts.size() << ", conjuncts="
+        << join_table_reader_debug_strings(_conjuncts,
+                                           [](const VExprContextSPtr& 
conjunct) {
+                                               return 
expr_context_debug_string(conjunct);
+                                           })
+        << ", file_schema="
+        << join_table_reader_debug_strings(
+                   _data_reader.file_schema,
+                   [](const ColumnDefinition& field) { return 
field.debug_string(); })
+        << ", file_block_layout="
+        << join_table_reader_debug_strings(
+                   _data_reader.file_block_layout,
+                   [](const FileBlockColumn& column) {
+                       std::ostringstream column_out;
+                       column_out << "FileBlockColumn{file_column_id=" << 
column.file_column_id
+                                  << ", name=" << column.name << ", type="
+                                  << (column.type == nullptr ? "null" : 
column.type->get_name())
+                                  << "}";
+                       return column_out.str();
+                   })
+        << ", block_template_columns=" << _data_reader.block_template.columns()
+        << ", column_mapper="
+        << (_data_reader.column_mapper == nullptr ? "null"
+                                                  : 
_data_reader.column_mapper->debug_string())
+        << "}";
+    return out.str();
+}
+
+Status TableReader::annotate_projected_column(const TFileScanSlotInfo& 
slot_info,
+                                              ProjectedColumnBuildContext* 
context,
+                                              ColumnDefinition* column) const {
+    (void)slot_info;
+    DORIS_CHECK(context != nullptr);
+    DORIS_CHECK(column != nullptr);
+    context->schema_column.reset();
+    const auto* schema_field = find_external_root_field(context->scan_params, 
*column);
+    if (schema_field == nullptr) {
+        return Status::OK();
+    }
+    context->schema_column = 
build_schema_column_from_external_field(*schema_field, column->type);
+    column->identifier = context->schema_column->identifier;
+    column->name_mapping = context->schema_column->name_mapping;
+    return Status::OK();
+}
+
+std::optional<ColumnDefinition> 
TableReader::_find_current_table_column_by_field_id(
+        int32_t field_id, DataTypePtr type) const {
+    if (_scan_params == nullptr || !_scan_params->__isset.history_schema_info 
||
+        _scan_params->history_schema_info.empty()) {
+        return std::nullopt;
+    }
+    const auto* schema = &_scan_params->history_schema_info.front();
+    if (_scan_params->__isset.current_schema_id) {
+        for (const auto& candidate_schema : _scan_params->history_schema_info) 
{
+            if (candidate_schema.__isset.schema_id &&
+                candidate_schema.schema_id == _scan_params->current_schema_id) 
{
+                schema = &candidate_schema;
+                break;
+            }
+        }
+    }
+    if (!schema->__isset.root_field || !schema->root_field.__isset.fields) {
+        return std::nullopt;
+    }
+    for (const auto& field_ptr : schema->root_field.fields) {
+        const auto* field = get_field_ptr(field_ptr);
+        if (field != nullptr && field->__isset.id && field->id == field_id) {
+            return build_schema_column_from_external_field(*field, 
std::move(type));
+        }
+    }
+    return std::nullopt;
+}
+
+Status TableReader::init(TableReadOptions&& options) {
+    _scan_params = options.scan_params;
+    _format = options.format;
+    _io_ctx = options.io_ctx;
+    _runtime_state = options.runtime_state;
+    _scanner_profile = options.scanner_profile;
+    _file_slot_descs = options.file_slot_descs;
+    _push_down_agg_type = options.push_down_agg_type;
+    _condition_cache_digest = options.condition_cache_digest;
+    _projected_columns = std::move(options.projected_columns);
+    _system_properties = create_system_properties(_scan_params);
+    _mapper_options.mode = TableColumnMappingMode::BY_NAME;
+    _conjuncts = std::move(options.conjuncts);
+
+    if (_scanner_profile != nullptr) {
+        static const char* table_profile = "TableReader";
+        ADD_TIMER_WITH_LEVEL(_scanner_profile, table_profile, 1);
+        _profile.num_delete_files = 
ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, "NumDeleteFiles",
+                                                                 TUnit::UNIT, 
table_profile, 1);
+        _profile.num_delete_rows = 
ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, "NumDeleteRows",
+                                                                TUnit::UNIT, 
table_profile, 1);
+        _profile.parse_delete_file_time = ADD_CHILD_TIMER_WITH_LEVEL(
+                _scanner_profile, "ParseDeleteFileTime", table_profile, 1);
+        _profile.decoded_dv_cache_hit_count =
+                ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, 
"DeletionVectorDecodedCacheHitCount",
+                                             TUnit::UNIT, table_profile, 1);
+        _profile.decoded_dv_cache_miss_count = ADD_CHILD_COUNTER_WITH_LEVEL(
+                _scanner_profile, "DeletionVectorDecodedCacheMissCount", 
TUnit::UNIT, table_profile,
+                1);
+        _profile.dv_file_cache_hit_count = ADD_CHILD_COUNTER_WITH_LEVEL(
+                _scanner_profile, "DeletionVectorFileCacheHitCount", 
TUnit::UNIT, table_profile, 1);
+        _profile.dv_file_cache_miss_count =
+                ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, 
"DeletionVectorFileCacheMissCount",
+                                             TUnit::UNIT, table_profile, 1);
+        _profile.dv_file_cache_peer_read_count = ADD_CHILD_COUNTER_WITH_LEVEL(
+                _scanner_profile, "DeletionVectorFileCachePeerReadCount", 
TUnit::UNIT,
+                table_profile, 1);
+        _profile.exec_timer =
+                ADD_CHILD_TIMER_WITH_LEVEL(_scanner_profile, "GetBlockTime", 
table_profile, 1);
+        _profile.prepare_split_timer =
+                ADD_CHILD_TIMER_WITH_LEVEL(_scanner_profile, 
"PrepareSplitTime", table_profile, 1);
+        _profile.finalize_timer =
+                ADD_CHILD_TIMER_WITH_LEVEL(_scanner_profile, 
"FinalizeBlockTime", table_profile, 1);
+        _profile.create_reader_timer =
+                ADD_CHILD_TIMER_WITH_LEVEL(_scanner_profile, 
"CreateReaderTime", table_profile, 1);
+        _profile.pushdown_agg_timer =
+                ADD_CHILD_TIMER_WITH_LEVEL(_scanner_profile, 
"PushDownAggTime", table_profile, 1);
+        _profile.open_reader_timer =
+                ADD_CHILD_TIMER_WITH_LEVEL(_scanner_profile, "OpenReaderTime", 
table_profile, 1);
+        _profile.runtime_filter_partition_prune_timer = ADD_TIMER_WITH_LEVEL(
+                _scanner_profile, 
"FileScannerRuntimeFilterPartitionPruningTime", 1);
+        _profile.runtime_filter_partition_pruned_range_counter = 
ADD_COUNTER_WITH_LEVEL(
+                _scanner_profile, "RuntimeFilterPartitionPrunedRangeNum", 
TUnit::UNIT, 1);
+    }
+    return Status::OK();
+}
+
+Status TableReader::_build_table_filters_from_conjuncts() {
+    _table_filters.clear();
+    _constant_pruning_safe_filter_count = 0;
+    bool in_safe_prefix = true;
+    for (const auto& conjunct : _conjuncts) {
+        DORIS_CHECK(conjunct != nullptr);
+        DORIS_CHECK(conjunct->root() != nullptr);
+        // `_table_filters` omits expressions without slot references, but 
such an expression still
+        // occupies a position in the row-level conjunct order. Record how 
many localized filters
+        // precede the first unsafe original conjunct so constant pruning 
cannot jump over a
+        // slotless non-deterministic/error-preserving barrier.
+        if (in_safe_prefix && !_is_safe_to_pre_execute(conjunct)) {
+            in_safe_prefix = false;
+        }
+        if (!in_safe_prefix) {
+            continue;
+        }
+        RETURN_IF_ERROR(
+                build_table_filters_from_conjunct(conjunct, _runtime_state, 
&_table_filters));
+        _constant_pruning_safe_filter_count = _table_filters.size();
+    }
+    return Status::OK();
+}
+
+Status TableReader::_open_local_filter_exprs(const FileScanRequest& 
file_request) {
+    RowDescriptor row_desc;
+    for (const auto& conjunct : file_request.conjuncts) {
+        RETURN_IF_ERROR(conjunct->prepare(_runtime_state, row_desc));
+        RETURN_IF_ERROR(conjunct->open(_runtime_state));
+    }
+    for (const auto& delete_conjunct : file_request.delete_conjuncts) {
+        RETURN_IF_ERROR(delete_conjunct->prepare(_runtime_state, row_desc));
+        RETURN_IF_ERROR(delete_conjunct->open(_runtime_state));
+    }
+    return Status::OK();
+}
+
+bool TableReader::_should_enable_condition_cache(const FileScanRequest& 
file_request) const {
+    if (_condition_cache_digest == 0 || _push_down_agg_type == 
TPushAggOp::type::COUNT ||
+        _current_file_description == std::nullopt || _data_reader.reader == 
nullptr) {
+        return false;
+    }
+    // Condition cache is populated by file readers after evaluating 
file-local row-level
+    // conjuncts. Metadata pruning can skip row groups/pages, but it does not 
produce a per-row
+    // survivor bitmap that can safely populate the cache.
+    if (file_request.conjuncts.empty()) {
+        return false;
+    }
+    // Delete files/deletion vectors are table-format state. They may change 
independently of the
+    // data file path/mtime/size used by the external cache key, so caching 
their result can become
+    // stale. Keep delete filtering enabled, but do not read or write 
condition cache.
+    if (_delete_rows != nullptr || _deletion_vector != nullptr ||
+        !file_request.delete_conjuncts.empty()) {
+        return false;
+    }
+    // Runtime filters can arrive late and their payload is not guaranteed to 
be represented by the
+    // scan-local digest. Without a read-only mode, a MISS could insert a 
bitmap for P AND RF under
+    // the digest for only P. This mirrors the old FileScanner guard.
+    return !contains_runtime_filter(file_request.conjuncts);
+}
+
+Status TableReader::_init_reader_condition_cache(const FileScanRequest& 
file_request) {
+    _condition_cache = nullptr;
+    _condition_cache_ctx = nullptr;
+    if (!_should_enable_condition_cache(file_request)) {
+        return Status::OK();
+    }
+
+    auto* cache = segment_v2::ConditionCache::instance();
+    if (cache == nullptr) {
+        return Status::OK();
+    }
+    const auto& file = *_current_file_description;
+    _condition_cache_key = segment_v2::ConditionCache::ExternalCacheKey(

Review Comment:
   [P1] Include storage identity in the external condition-cache key
   
   This process-global key omits the filesystem/object-store configuration and 
is enabled with `mtime == 0`. Iceberg/Hudi send zero mtime, while different 
S3-compatible endpoints normalize to the same `s3://bucket/key`; two catalogs 
can therefore have identical path/size/predicate keys but different bytes. A 
false granule cached for one object is then skipped before residual filtering 
on the other, silently losing qualifying rows. Include a stable 
endpoint/filesystem plus object-version identity, or disable the cache when 
they are unavailable as the new Parquet page cache does.



##########
be/src/core/data_type_serde/data_type_datetimev2_serde.cpp:
##########
@@ -44,6 +45,95 @@ namespace doris {
 static const int64_t micro_to_nano_second = 1000;
 #include "common/compile_check_begin.h"
 
+namespace {
+
+#pragma pack(1)
+struct DecodedInt96Timestamp {
+    int64_t nanos_of_day;
+    int32_t julian_day;
+
+    int64_t to_timestamp_micros() const {
+        static constexpr int32_t JULIAN_EPOCH_OFFSET_DAYS = 2440588;
+        static constexpr int64_t MICROS_IN_DAY = 86400000000;
+        static constexpr int64_t NANOS_PER_MICROSECOND = 1000;
+        return (julian_day - JULIAN_EPOCH_OFFSET_DAYS) * MICROS_IN_DAY +
+               nanos_of_day / NANOS_PER_MICROSECOND;
+    }
+};
+#pragma pack()
+static_assert(sizeof(DecodedInt96Timestamp) == 12);
+
+Status append_datetimev2_from_epoch_micros(ColumnDateTimeV2::Container& data,
+                                           int64_t timestamp_micros) {
+    static constexpr int64_t MICROS_PER_SECOND = 1000000;
+    static constexpr int64_t MICROS_PER_MINUTE = MICROS_PER_SECOND * 60;
+    static constexpr int64_t MICROS_PER_HOUR = MICROS_PER_MINUTE * 60;
+    static constexpr int64_t MICROS_PER_DAY = MICROS_PER_HOUR * 24;
+    static const int64_t EPOCH_DAYNR = calc_daynr(1970, 1, 1);
+
+    int64_t days_since_epoch = timestamp_micros / MICROS_PER_DAY;
+    int64_t micros_of_day = timestamp_micros % MICROS_PER_DAY;
+    if (micros_of_day < 0) {
+        micros_of_day += MICROS_PER_DAY;
+        --days_since_epoch;
+    }
+
+    const int64_t daynr = EPOCH_DAYNR + days_since_epoch;
+    if (daynr <= 0) {
+        return Status::DataQualityError(
+                "Decoded DATETIMEV2 timestamp is out of range: micros={}, 
daynr={}",
+                timestamp_micros, daynr);
+    }
+
+    DateV2Value<DateTimeV2ValueType> datetime_value;
+    if (!datetime_value.get_date_from_daynr(static_cast<uint64_t>(daynr))) {
+        return Status::DataQualityError(
+                "Decoded DATETIMEV2 timestamp is out of range: micros={}, 
daynr={}",
+                timestamp_micros, daynr);
+    }
+
+    const auto hour = static_cast<uint8_t>(micros_of_day / MICROS_PER_HOUR);
+    micros_of_day %= MICROS_PER_HOUR;
+    const auto minute = static_cast<uint8_t>(micros_of_day / 
MICROS_PER_MINUTE);
+    micros_of_day %= MICROS_PER_MINUTE;
+    const auto second = static_cast<uint16_t>(micros_of_day / 
MICROS_PER_SECOND);
+    const auto microsecond = static_cast<uint32_t>(micros_of_day % 
MICROS_PER_SECOND);
+    datetime_value.unchecked_set_time(datetime_value.year(), 
datetime_value.month(),
+                                      datetime_value.day(), hour, minute, 
second, microsecond);
+    data.push_back(datetime_value);
+    return Status::OK();
+}
+
+void append_datetimev2_from_utc_epoch_micros(ColumnDateTimeV2::Container& data,
+                                             int64_t timestamp_micros,
+                                             const cctz::time_zone& timezone) {
+    static constexpr int64_t MICROS_PER_SECOND = 1000000;
+
+    int64_t epoch_seconds = timestamp_micros / MICROS_PER_SECOND;
+    int64_t micros_of_second = timestamp_micros % MICROS_PER_SECOND;
+    if (micros_of_second < 0) {
+        micros_of_second += MICROS_PER_SECOND;
+        --epoch_seconds;
+    }
+
+    DateV2Value<DateTimeV2ValueType> datetime_value;
+    datetime_value.from_unixtime(epoch_seconds, timezone);
+    datetime_value.set_microsecond(static_cast<uint32_t>(micros_of_second));
+    data.push_back(datetime_value);
+}
+
+int64_t decoded_timestamp_micros(const DecodedColumnView& view, int64_t value) 
{
+    if (view.time_unit == DecodedTimeUnit::MILLIS) {
+        return value * 1000;

Review Comment:
   [P1] Avoid overflow when converting Parquet milliseconds
   
   `MILLIS` is an arbitrary signed `int64_t`, so multiplying by 1000 can 
overflow before any Doris range check. For example, `INT64_MAX` commonly wraps 
to `-1000` microseconds and is returned as a plausible 1969 value; the same 
conversion also feeds TIMESTAMPTZ and statistics pruning. The legacy reader 
avoids this intermediate by splitting the original value into seconds and 
remainder. Use checked arithmetic or direct quotient/remainder conversion, then 
route out-of-range values through the strict/nullable failure policy.



##########
be/src/format_v2/table/hive_reader.cpp:
##########
@@ -0,0 +1,246 @@
+// 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/table/hive_reader.h"
+
+#include <algorithm>
+#include <cctype>
+#include <string_view>
+
+#include "common/consts.h"
+#include "format_v2/column_mapper.h"
+#include "format_v2/file_reader.h"
+#include "runtime/runtime_state.h"
+
+namespace doris::format::hive {
+namespace {
+
+TFileFormatType::type format_type_from_context(const 
format::ProjectedColumnBuildContext& context) {
+    DORIS_CHECK(context.scan_params != nullptr);
+    if (context.range != nullptr && context.range->__isset.format_type) {
+        return context.range->format_type;
+    }
+    return context.scan_params->format_type;
+}
+
+bool use_column_position_mapping(const format::ProjectedColumnBuildContext& 
context) {
+    if (context.runtime_state == nullptr || context.scan_params == nullptr) {
+        return false;
+    }
+    switch (format_type_from_context(context)) {
+    case TFileFormatType::FORMAT_PARQUET:
+        return 
!context.runtime_state->query_options().hive_parquet_use_column_names;
+    case TFileFormatType::FORMAT_ORC:
+        return 
!context.runtime_state->query_options().hive_orc_use_column_names;
+    default:
+        return false;
+    }
+}
+
+bool is_file_column_position_slot(const TFileScanSlotInfo& slot_info,
+                                  const std::string& column_name) {
+    if (column_name.starts_with(BeConsts::GLOBAL_ROWID_COL) ||
+        column_name == BeConsts::ICEBERG_ROWID_COL) {
+        return false;
+    }
+    if (slot_info.__isset.is_file_slot) {
+        return slot_info.is_file_slot;
+    }
+    return true;
+}
+
+bool is_hive1_orc_column_name(std::string_view name) {
+    if (name.size() <= 4 || name.substr(0, 4) != "_col") {
+        return false;
+    }
+    return std::ranges::all_of(name.substr(4), [](unsigned char c) { return 
std::isdigit(c); });
+}
+
+bool is_hive1_orc_file_schema(const std::vector<format::ColumnDefinition>& 
file_schema) {
+    bool has_data_column = false;
+    for (const auto& field : file_schema) {
+        if (field.column_type != format::ColumnType::DATA_COLUMN) {
+            continue;
+        }
+        has_data_column = true;
+        if (!is_hive1_orc_column_name(field.name)) {
+            return false;
+        }
+    }
+    return has_data_column;
+}
+
+bool is_file_column_for_hive1_position_mapping(const format::ColumnDefinition& 
column) {
+    if (column.column_type != format::ColumnType::DATA_COLUMN || 
column.is_partition_key) {
+        return false;
+    }
+    return !column.name.starts_with(BeConsts::GLOBAL_ROWID_COL) &&
+           column.name != BeConsts::ICEBERG_ROWID_COL;
+}
+
+void add_name_mapping(std::vector<std::string>* name_mapping, const 
std::string& alias) {
+    DORIS_CHECK(name_mapping != nullptr);
+    if (std::ranges::find(*name_mapping, alias) == name_mapping->end()) {
+        name_mapping->push_back(alias);
+    }
+}
+
+} // namespace
+
+Status HiveReader::prepare_split(const format::SplitReadOptions& options) {
+    if (options.current_split_format != _format) {
+        return Status::InternalError(
+                "Hive scan expects all splits to use the same file format, "
+                "initialized_format={}, current_split_format={}",
+                static_cast<int>(_format), 
static_cast<int>(options.current_split_format));
+    }
+    return format::TableReader::prepare_split(options);
+}
+
+format::TableColumnMappingMode HiveReader::mapping_mode() const {
+    // Hive-specific behavior: choose the column matching mode based on file 
format and the
+    // matching session variable.
+    //   - hive_orc_use_column_names / hive_parquet_use_column_names == true
+    //     => BY_NAME (modern Hive default, match by column name)
+    //   - those options == false
+    //     => BY_INDEX (mainly for Hive1 ORC `_col0` / `_col1`, match by 
top-level position;
+    //                  Parquet exposes the same switch for consistency)
+    // TableReader updates `_format` in prepare_split(), so this is evaluated 
per split.
+    DORIS_CHECK(_runtime_state != nullptr);
+    const auto& query_options = _runtime_state->query_options();
+    bool use_column_names = true;
+    if (_format == format::FileFormat::ORC) {
+        use_column_names = query_options.hive_orc_use_column_names;

Review Comment:
   [P1] Key condition-cache entries by the resolved Hive mapping
   
   The condition digest is finalized before these options and the table schema 
are resolved into file-local columns; Hive slot digests contain unique id `-1` 
plus the logical name, not the chosen file ordinal. Thus both a mode toggle and 
two positional Hive schemas that map logical `a` to different ordinals can 
reuse the same path/version/predicate entry. A false bitmap from one binding 
then skips rows before residual evaluation in the other. Include the resolved 
table-to-file mapping/schema identity in the cache key or digest; hashing only 
the two mode booleans is not sufficient.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to