blob: 407d5924e9f64a434326acc14f93ec87b6dd0773 [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/table/iceberg_reader.h"
#include <algorithm>
#include <memory>
#include <sstream>
#include <utility>
#include "common/cast_set.h"
#include "common/consts.h"
#include "core/assert_cast.h"
#include "core/block/block.h"
#include "core/column/column_const.h"
#include "core/column/column_nullable.h"
#include "core/column/column_string.h"
#include "core/column/column_struct.h"
#include "core/column/column_vector.h"
#include "core/data_type/data_type_number.h"
#include "core/data_type/define_primitive_type.h"
#include "core/field.h"
#include "exprs/vliteral.h"
#include "exprs/vslot_ref.h"
#include "format/table/deletion_vector_reader.h"
#include "format_v2/expr/cast.h"
#include "format_v2/expr/equality_delete_predicate.h"
#include "format_v2/orc/orc_reader.h"
#include "format_v2/parquet/parquet_reader.h"
#include "format_v2/parquet/reader/column_reader.h"
#include "format_v2/table_reader.h"
#include "io/file_factory.h"
#include "util/debug_points.h"
#include "util/url_coding.h"
namespace doris::format::iceberg {
static constexpr const char* ROW_LINEAGE_ROW_ID = "_row_id";
static constexpr int32_t ROW_LINEAGE_ROW_ID_FIELD_ID = 2147483540;
template <typename T>
static std::string join_values_for_debug(const std::vector<T>& values) {
std::ostringstream out;
out << "[";
for (size_t idx = 0; idx < values.size(); ++idx) {
if (idx > 0) {
out << ", ";
}
out << values[idx];
}
out << "]";
return out.str();
}
static bool is_projected_row_lineage_row_id(const format::ColumnDefinition& column) {
// Iceberg row lineage columns can be bound by field id when a mapper has already been built,
// but customize_file_scan_request() is also exercised directly by scan-request tests before the
// mapper exists. In that path, inspect the projected table schema so row-position dependencies
// are still added for `_row_id`.
return column.name == ROW_LINEAGE_ROW_ID ||
(column.has_identifier_field_id() &&
column.get_identifier_field_id() == ROW_LINEAGE_ROW_ID_FIELD_ID);
}
static bool is_projected_iceberg_rowid(const format::ColumnDefinition& column) {
return column.name == BeConsts::ICEBERG_ROWID_COL;
}
static Status build_missing_equality_delete_key_expr(const format::ColumnDefinition& table_field,
const DataTypePtr& delete_key_type,
VExprSPtr* key_expr) {
DORIS_CHECK(delete_key_type != nullptr);
DORIS_CHECK(key_expr != nullptr);
if (!table_field.initial_default_value.has_value()) {
// A newly added optional field without an initial default is logically NULL in older
// files. EqualityDeletePredicate treats NULL == NULL as a match.
*key_expr = VLiteral::create_shared(make_nullable(delete_key_type), Field());
return Status::OK();
}
VExprSPtr literal;
if (table_field.initial_default_value_is_base64 ||
table_field.type->get_primitive_type() == TYPE_VARBINARY) {
// New FE versions mark every Iceberg UUID/BINARY/FIXED default as Base64 regardless of its
// Doris mapping. Keep the VARBINARY fallback for scan descriptors produced before that
// marker existed. Decode before parsing so STRING/CHAR and VARBINARY all compare against
// the raw bytes stored in equality-delete files.
std::string decoded_default;
if (!base64_decode(*table_field.initial_default_value, &decoded_default)) {
return Status::InvalidArgument("Invalid Base64 Iceberg initial default for field {}",
table_field.name);
}
if (table_field.type->get_primitive_type() == TYPE_VARBINARY) {
const auto initial_default =
Field::create_field<TYPE_VARBINARY>(StringView(decoded_default));
// VLiteral must copy the borrowed StringView while decoded_default is alive; UUID and
// long FIXED defaults otherwise retain a pointer into freed decode storage.
literal = VLiteral::create_shared(table_field.type, initial_default);
} else {
DORIS_CHECK(is_string_type(table_field.type->get_primitive_type()));
literal = VLiteral::create_shared(table_field.type,
Field::create_field<TYPE_STRING>(decoded_default));
}
} else {
// An added field's initial default is its logical value in every older data file that lacks
// the physical column. FE normalizes the string for the current Doris table type.
Field initial_default;
RETURN_IF_ERROR(table_field.type->get_serde()->from_fe_string(
*table_field.initial_default_value, initial_default));
literal = VLiteral::create_shared(table_field.type, initial_default);
}
DORIS_CHECK(literal != nullptr);
if (table_field.type->equals(*delete_key_type)) {
*key_expr = std::move(literal);
return Status::OK();
}
auto cast_expr = Cast::create_shared(delete_key_type);
cast_expr->add_child(std::move(literal));
*key_expr = std::move(cast_expr);
return Status::OK();
}
static std::string iceberg_delete_file_debug_string(const TIcebergDeleteFileDesc& delete_file) {
std::ostringstream out;
out << "TIcebergDeleteFileDesc{path=" << (delete_file.__isset.path ? delete_file.path : "null")
<< ", content=" << (delete_file.__isset.content ? delete_file.content : -1)
<< ", file_format="
<< (delete_file.__isset.file_format ? static_cast<int>(delete_file.file_format) : -1)
<< ", position_lower_bound="
<< (delete_file.__isset.position_lower_bound ? delete_file.position_lower_bound : -1)
<< ", position_upper_bound="
<< (delete_file.__isset.position_upper_bound ? delete_file.position_upper_bound : -1)
<< ", field_ids="
<< (delete_file.__isset.field_ids ? join_values_for_debug(delete_file.field_ids) : "[]")
<< ", content_offset="
<< (delete_file.__isset.content_offset ? delete_file.content_offset : -1)
<< ", content_size_in_bytes="
<< (delete_file.__isset.content_size_in_bytes ? delete_file.content_size_in_bytes : -1)
<< "}";
return out.str();
}
static std::string iceberg_delete_files_debug_string(
const std::vector<TIcebergDeleteFileDesc>& delete_files) {
std::ostringstream out;
out << "[";
for (size_t idx = 0; idx < delete_files.size(); ++idx) {
if (idx > 0) {
out << ", ";
}
out << iceberg_delete_file_debug_string(delete_files[idx]);
}
out << "]";
return out.str();
}
static std::string iceberg_params_debug_string(const std::optional<TIcebergFileDesc>& params) {
if (!params.has_value()) {
return "null";
}
const auto& iceberg_params = *params;
std::ostringstream out;
out << "TIcebergFileDesc{format_version="
<< (iceberg_params.__isset.format_version ? iceberg_params.format_version : -1)
<< ", content=" << (iceberg_params.__isset.content ? iceberg_params.content : -1)
<< ", original_file_path="
<< (iceberg_params.__isset.original_file_path ? iceberg_params.original_file_path : "null")
<< ", row_count=" << (iceberg_params.__isset.row_count ? iceberg_params.row_count : -1)
<< ", partition_spec_id="
<< (iceberg_params.__isset.partition_spec_id ? iceberg_params.partition_spec_id : 0)
<< ", has_partition_data_json=" << iceberg_params.__isset.partition_data_json
<< ", first_row_id="
<< (iceberg_params.__isset.first_row_id ? iceberg_params.first_row_id : -1)
<< ", last_updated_sequence_number="
<< (iceberg_params.__isset.last_updated_sequence_number
? iceberg_params.last_updated_sequence_number
: -1)
<< ", delete_file_count="
<< (iceberg_params.__isset.delete_files ? iceberg_params.delete_files.size() : 0)
<< ", delete_files="
<< (iceberg_params.__isset.delete_files
? iceberg_delete_files_debug_string(iceberg_params.delete_files)
: "[]")
<< ", has_serialized_split=" << iceberg_params.__isset.serialized_split << "}";
return out.str();
}
IcebergTableReader::PositionDeleteRowsCollector::PositionDeleteRowsCollector(
PositionDeleteFile* rows_by_data_file)
: _rows_by_data_file(rows_by_data_file) {
DORIS_CHECK(_rows_by_data_file != nullptr);
}
Status IcebergTableReader::PositionDeleteRowsCollector::collect(const Block& block,
size_t read_rows) {
if (read_rows == 0) {
return Status::OK();
}
const auto& file_path_column_ptr =
block.get_by_position(ICEBERG_FILE_PATH_BLOCK_POSITION).column;
const auto& pos_column_ptr = block.get_by_position(ICEBERG_ROW_POS_BLOCK_POSITION).column;
if (const auto* nullable_column = check_and_get_column<ColumnNullable>(*file_path_column_ptr);
nullable_column != nullptr && nullable_column->has_null(0, read_rows)) {
return Status::Corruption("Iceberg position delete column file_path contains null values");
}
if (const auto* nullable_column = check_and_get_column<ColumnNullable>(*pos_column_ptr);
nullable_column != nullptr && nullable_column->has_null(0, read_rows)) {
return Status::Corruption("Iceberg position delete column pos contains null values");
}
const auto& file_path_column =
assert_cast<const ColumnString&>(*remove_nullable(file_path_column_ptr));
const auto& pos_column = assert_cast<const ColumnInt64&>(*remove_nullable(pos_column_ptr));
for (size_t row = 0; row < read_rows; ++row) {
const auto file_path = file_path_column.get_data_at(row).to_string();
(*_rows_by_data_file)[file_path].push_back(pos_column.get_element(row));
}
return Status::OK();
}
Status IcebergTableReader::prepare_split(const format::SplitReadOptions& options) {
{
SCOPED_TIMER(_profile.total_timer);
SCOPED_TIMER(_profile.prepare_split_timer);
_row_lineage_columns = {};
_iceberg_params.reset();
_delete_predicates_initialized = false;
_position_delete_rows_storage.clear();
_equality_delete_filters.clear();
_split_cache = options.cache;
if (options.current_range.__isset.table_format_params &&
options.current_range.table_format_params.__isset.iceberg_params) {
const auto& iceberg_params = options.current_range.table_format_params.iceberg_params;
_iceberg_params = iceberg_params;
if (iceberg_params.__isset.first_row_id) {
_row_lineage_columns.first_row_id = iceberg_params.first_row_id;
}
if (iceberg_params.__isset.last_updated_sequence_number) {
_row_lineage_columns.last_updated_sequence_number =
iceberg_params.last_updated_sequence_number;
}
}
}
RETURN_IF_ERROR(TableReader::prepare_split(options));
SCOPED_TIMER(_profile.total_timer);
SCOPED_TIMER(_profile.prepare_split_timer);
if (current_split_pruned()) {
return Status::OK();
}
// Iceberg data files are immutable once referenced by a snapshot; updates create new data files
// at new paths instead of overwriting existing files. This lets the Parquet V2 reader use page
// cache when the scan range does not carry an mtime, without extending V1's path::0 behavior to
// mutable Hive/local files.
mark_current_data_file_immutable();
if (_is_table_level_count_active()) {
return Status::OK();
}
DBUG_EXECUTE_IF("IcebergTableReader.prepare_split.before_delete_file_scan",
DBUG_RUN_CALLBACK());
RETURN_IF_ERROR(_init_delete_predicates(options.current_range.table_format_params));
return Status::OK();
}
std::string IcebergTableReader::debug_string() const {
size_t position_delete_file_count = 0;
size_t equality_delete_file_count = 0;
size_t deletion_vector_file_count = 0;
if (_iceberg_params.has_value() && _iceberg_params->__isset.delete_files) {
for (const auto& delete_file : _iceberg_params->delete_files) {
if (!delete_file.__isset.content) {
continue;
}
if (delete_file.content == POSITION_DELETE) {
++position_delete_file_count;
} else if (delete_file.content == EQUALITY_DELETE) {
++equality_delete_file_count;
} else if (delete_file.content == DELETION_VECTOR) {
++deletion_vector_file_count;
}
}
}
std::ostringstream equality_filters;
equality_filters << "[";
for (size_t idx = 0; idx < _equality_delete_filters.size(); ++idx) {
if (idx > 0) {
equality_filters << ", ";
}
const auto& filter = _equality_delete_filters[idx];
equality_filters << "EqualityDeleteFilter{field_ids="
<< join_values_for_debug(filter.field_ids) << ", key_types=[";
for (size_t type_idx = 0; type_idx < filter.key_types.size(); ++type_idx) {
if (type_idx > 0) {
equality_filters << ", ";
}
equality_filters << (filter.key_types[type_idx] == nullptr
? "null"
: filter.key_types[type_idx]->get_name());
}
equality_filters << "], delete_block_rows=" << filter.delete_block.rows()
<< ", delete_block_columns=" << filter.delete_block.columns() << "}";
}
equality_filters << "]";
std::ostringstream out;
out << "IcebergTableReader{base=" << format::TableReader::debug_string()
<< ", iceberg_params=" << iceberg_params_debug_string(_iceberg_params)
<< ", row_lineage_first_row_id=" << _row_lineage_columns.first_row_id
<< ", row_lineage_last_updated_sequence_number="
<< _row_lineage_columns.last_updated_sequence_number
<< ", need_row_lineage_row_id=" << _need_row_lineage_row_id()
<< ", need_iceberg_rowid=" << _need_iceberg_rowid()
<< ", row_position_block_position=" << _row_position_block_position
<< ", delete_predicates_initialized=" << _delete_predicates_initialized
<< ", position_delete_file_count=" << position_delete_file_count
<< ", equality_delete_file_count=" << equality_delete_file_count
<< ", deletion_vector_file_count=" << deletion_vector_file_count
<< ", position_delete_rows_storage_count=" << _position_delete_rows_storage.size()
<< ", equality_delete_filter_count=" << _equality_delete_filters.size()
<< ", equality_delete_filters=" << equality_filters.str() << "}";
return out.str();
}
Status IcebergTableReader::materialize_virtual_columns(Block* table_block) {
for (size_t column_idx = 0; column_idx < _data_reader.column_mapper->mappings().size();
++column_idx) {
const auto& mapping = _data_reader.column_mapper->mappings()[column_idx];
switch (mapping.virtual_column_type) {
case format::TableVirtualColumnType::ROW_ID:
RETURN_IF_ERROR(_materialize_row_lineage_row_id(table_block, column_idx));
break;
case format::TableVirtualColumnType::LAST_UPDATED_SEQUENCE_NUMBER:
RETURN_IF_ERROR(
_materialize_row_lineage_last_updated_sequence_number(table_block, column_idx));
break;
case format::TableVirtualColumnType::ICEBERG_ROWID:
RETURN_IF_ERROR(_materialize_iceberg_rowid(table_block, column_idx));
break;
case format::TableVirtualColumnType::INVALID:
break;
}
}
return Status::OK();
}
Status IcebergTableReader::customize_file_scan_request(format::FileScanRequest* file_request) {
RETURN_IF_ERROR(TableReader::customize_file_scan_request(file_request));
if ((_row_lineage_columns.first_row_id >= 0 && _need_row_lineage_row_id()) ||
_need_iceberg_rowid()) {
RETURN_IF_ERROR(_append_row_position_output_column(file_request));
}
RETURN_IF_ERROR(_append_equality_delete_predicates(file_request));
return Status::OK();
}
bool IcebergTableReader::_supports_aggregate_pushdown(TPushAggOp::type agg_type) const {
if (!TableReader::_supports_aggregate_pushdown(agg_type)) {
return false;
}
return _equality_delete_filters.empty();
}
Status IcebergTableReader::_parse_deletion_vector_file(const TTableFormatFileDesc& t_desc,
DeleteFileDesc* desc,
bool* has_delete_file) {
DORIS_CHECK(desc != nullptr);
DORIS_CHECK(has_delete_file != nullptr);
*has_delete_file = false;
if (!t_desc.__isset.iceberg_params) {
return Status::OK();
}
const auto& iceberg_params = t_desc.iceberg_params;
if (!iceberg_params.__isset.format_version ||
iceberg_params.format_version < MIN_SUPPORT_DELETE_FILES_VERSION ||
!iceberg_params.__isset.delete_files || iceberg_params.delete_files.empty()) {
return Status::OK();
}
const TIcebergDeleteFileDesc* deletion_vector = nullptr;
for (const auto& delete_file : iceberg_params.delete_files) {
if (!delete_file.__isset.content || delete_file.content != DELETION_VECTOR) {
continue;
}
if (deletion_vector != nullptr) {
return Status::DataQualityError("This iceberg data file has multiple DVs.");
}
deletion_vector = &delete_file;
}
if (deletion_vector == nullptr) {
return Status::OK();
}
size_t bytes_read = 0;
RETURN_IF_ERROR(validate_iceberg_deletion_vector_descriptor(*deletion_vector, bytes_read));
const std::string data_file_path = iceberg_params.__isset.original_file_path
? iceberg_params.original_file_path
: _data_file_path();
desc->key = build_iceberg_deletion_vector_cache_key(data_file_path, *deletion_vector);
desc->path = deletion_vector->path;
desc->start_offset = deletion_vector->content_offset;
desc->size = static_cast<int64_t>(bytes_read);
desc->file_size = -1;
desc->format = DeleteFileDesc::Format::ICEBERG;
*has_delete_file = true;
return Status::OK();
}
Status IcebergTableReader::_init_delete_predicates(const TTableFormatFileDesc& t_desc) {
if (!t_desc.__isset.iceberg_params || _delete_predicates_initialized) {
_delete_predicates_initialized = true;
return Status::OK();
}
const auto& iceberg_params = t_desc.iceberg_params;
if (!iceberg_params.__isset.format_version ||
iceberg_params.format_version < MIN_SUPPORT_DELETE_FILES_VERSION ||
!iceberg_params.__isset.delete_files || iceberg_params.delete_files.empty()) {
_delete_predicates_initialized = true;
return Status::OK();
}
std::vector<TIcebergDeleteFileDesc> position_delete_files;
std::vector<TIcebergDeleteFileDesc> equality_delete_files;
for (const auto& delete_file : iceberg_params.delete_files) {
if (!delete_file.__isset.content) {
continue;
}
if (delete_file.content == POSITION_DELETE) {
position_delete_files.push_back(delete_file);
} else if (delete_file.content == EQUALITY_DELETE) {
equality_delete_files.push_back(delete_file);
}
}
// Per Iceberg scan planning, position delete files apply only when there is no deletion vector
// for the data file. DVs and position deletes now intentionally use different in-memory
// representations, so use the Roaring pointer as the DV sentinel.
if (_deletion_vector != nullptr) {
position_delete_files.clear();
}
// Initialize position and equality delete predicates. Position delete files contain row
// positions of deleted rows, which can be directly added to `_delete_rows`. Equality delete
// files contain values of deleted rows, which require reading the files and building
// predicates for later filtering.
if (!position_delete_files.empty()) {
RETURN_IF_ERROR(_init_position_delete_rows(position_delete_files));
}
if (!equality_delete_files.empty()) {
RETURN_IF_ERROR(_init_equality_delete_predicates(equality_delete_files));
}
_delete_predicates_initialized = true;
return Status::OK();
}
std::shared_ptr<io::FileSystemProperties> IcebergTableReader::_delete_file_system_properties(
const TFileScanRangeParams& scan_params) {
auto system_properties = std::make_shared<io::FileSystemProperties>();
system_properties->system_type =
scan_params.__isset.file_type ? scan_params.file_type : TFileType::FILE_LOCAL;
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::unique_ptr<io::FileDescription> IcebergTableReader::_delete_file_description(
const TFileRangeDesc& range) {
auto file_description = std::make_unique<io::FileDescription>();
file_description->path = range.path;
file_description->file_size = range.__isset.file_size ? range.file_size : -1;
file_description->range_start_offset = range.__isset.start_offset ? range.start_offset : 0;
file_description->range_size = range.__isset.size ? range.size : -1;
// Iceberg delete files follow the same immutable-file contract as data files: a snapshot
// references a fixed object and later changes publish a new file rather than replacing it.
file_description->is_immutable = true;
if (range.__isset.fs_name) {
file_description->fs_name = range.fs_name;
}
return file_description;
}
std::string IcebergTableReader::_data_file_path() const {
if (_iceberg_params.has_value() && _iceberg_params->__isset.original_file_path) {
return _iceberg_params->original_file_path;
}
DORIS_CHECK(_current_task != nullptr);
DORIS_CHECK(_current_task->data_file != nullptr);
return _current_task->data_file->path;
}
Status IcebergTableReader::_append_row_position_output_column(format::FileScanRequest* request) {
const auto row_position_column_id = format::LocalColumnId(format::ROW_POSITION_COLUMN_ID);
_append_file_scan_column(request, row_position_column_id, &request->non_predicate_columns);
_row_position_block_position = request->local_positions.at(row_position_column_id).value();
return Status::OK();
}
const format::ColumnDefinition* IcebergTableReader::_find_equality_delete_data_field(
const EqualityDeleteFilter& filter, size_t key_idx) const {
DORIS_CHECK(key_idx < filter.field_ids.size());
DORIS_CHECK(key_idx < filter.field_names.size());
if (mapping_mode() != format::TableColumnMappingMode::BY_NAME) {
const int field_id = filter.field_ids[key_idx];
const auto field_it = std::ranges::find_if(
_data_reader.file_schema, [field_id](const format::ColumnDefinition& field) {
return field.has_identifier_field_id() &&
field.get_identifier_field_id() == field_id;
});
return field_it == _data_reader.file_schema.end() ? nullptr : &*field_it;
}
// Equality keys are hidden scan dependencies and need not appear in the query projection.
// Resolve their current name and aliases from the full table schema supplied by FE, falling
// back to the delete-file name when history metadata is unavailable. Reuse ColumnMapper's
// exact BY_NAME rules so case, string identifiers, and aliases on either side stay consistent.
auto table_field = _find_equality_delete_table_field(filter, key_idx);
return format::find_column_by_name(*table_field, _data_reader.file_schema);
}
std::optional<format::ColumnDefinition> IcebergTableReader::_find_equality_delete_table_field(
const EqualityDeleteFilter& filter, size_t key_idx) const {
DORIS_CHECK(key_idx < filter.field_ids.size());
DORIS_CHECK(key_idx < filter.field_names.size());
const int field_id = filter.field_ids[key_idx];
auto table_field = _find_current_table_column_by_field_id(field_id, filter.key_types[key_idx]);
if (!table_field.has_value()) {
const auto projected_field = std::ranges::find_if(
_projected_columns, [field_id](const format::ColumnDefinition& field) {
return field.has_identifier_field_id() &&
field.get_identifier_field_id() == field_id;
});
if (projected_field != _projected_columns.end()) {
// Older scan descriptors and focused unit tests may omit history_schema_info. Keep the
// projected metadata as a compatibility fallback, but never require projection when
// the complete current schema is available.
table_field = *projected_field;
}
}
if (!table_field.has_value()) {
table_field = format::ColumnDefinition {
.identifier = {},
.name = filter.field_names[key_idx],
.type = filter.key_types[key_idx],
};
}
return table_field;
}
std::string IcebergTableReader::_delete_file_cache_key(const char* prefix,
const std::string& path) const {
DORIS_CHECK(prefix != nullptr);
std::string fs_name;
if (_current_task != nullptr && _current_task->data_file != nullptr) {
fs_name = _current_task->data_file->fs_name;
}
// Delete descriptors can reuse the same path text in different filesystem namespaces. Encode
// both variable-length strings so neither an fs/path boundary nor equality field-id suffixes
// can be reinterpreted as path content; scan-level credentials/properties are shared here.
std::ostringstream key;
key << prefix << fs_name.size() << ':' << fs_name << ':' << path.size() << ':' << path;
return key.str();
}
void IcebergTableReader::_append_equality_delete_row_count_carrier(
format::FileScanRequest* request) {
DORIS_CHECK(request != nullptr);
// Columnar readers establish a filter batch's row count from predicate columns. If all
// equality keys are missing, the predicate consists only of NULL literals and the filter block
// would otherwise have zero rows. Use the virtual row-position column as the carrier instead
// of an arbitrary physical column. For example, a data file may start with an unsupported
// TIME_MILLIS leaf while the query projects only a supported `id`; selecting that TIME leaf as
// a hidden carrier would make Parquet reject a column the query never requested. Row position
// has one value per input row in both Parquet and ORC, is already used by delete predicates,
// and is explicitly excluded from physical logical-type validation.
_append_file_scan_column(request, format::LocalColumnId(format::ROW_POSITION_COLUMN_ID),
&request->predicate_columns);
}
Status IcebergTableReader::_append_equality_delete_predicates(format::FileScanRequest* request) {
DORIS_CHECK(request != nullptr);
for (const auto& filter : _equality_delete_filters) {
auto delete_predicate =
std::make_shared<EqualityDeletePredicate>(filter.delete_block, filter.field_ids);
DCHECK_EQ(filter.field_ids.size(), filter.key_types.size());
bool has_missing_key = false;
for (size_t idx = 0; idx < filter.field_ids.size(); ++idx) {
const auto* field = _find_equality_delete_data_field(filter, idx);
if (field == nullptr) {
auto table_field = _find_equality_delete_table_field(filter, idx);
DORIS_CHECK(table_field.has_value());
VExprSPtr key_expr;
RETURN_IF_ERROR(build_missing_equality_delete_key_expr(
*table_field, filter.key_types[idx], &key_expr));
delete_predicate->add_child(key_expr);
has_missing_key = true;
continue;
}
const auto field_column_id = format::LocalColumnId(field->file_local_id());
_append_file_scan_column(request, field_column_id, &request->predicate_columns);
const auto block_position = request->local_positions.at(field_column_id).value();
auto slot = VSlotRef::create_shared(cast_set<int>(block_position),
cast_set<int>(block_position), -1, field->type,
field->name);
if (field->type->equals(*filter.key_types[idx])) {
delete_predicate->add_child(std::move(slot));
} else {
auto cast_expr = Cast::create_shared(filter.key_types[idx]);
cast_expr->add_child(std::move(slot));
delete_predicate->add_child(std::move(cast_expr));
}
}
if (has_missing_key && request->predicate_columns.empty()) {
_append_equality_delete_row_count_carrier(request);
}
request->delete_conjuncts.push_back(
VExprContext::create_shared(std::move(delete_predicate)));
}
return Status::OK();
}
Status IcebergTableReader::_create_delete_file_reader(const TIcebergDeleteFileDesc& delete_file,
const TFileScanRangeParams& scan_params,
IcebergDeleteFileIOContext* delete_io_ctx,
std::unique_ptr<format::FileReader>* reader) {
DORIS_CHECK(delete_io_ctx != nullptr);
DORIS_CHECK(reader != nullptr);
if (!delete_file.__isset.file_format) {
return Status::InternalError("Iceberg delete file is missing file format");
}
if (delete_file.file_format != TFileFormatType::FORMAT_PARQUET &&
delete_file.file_format != TFileFormatType::FORMAT_ORC) {
return Status::NotSupported("Unsupported Iceberg delete file format {}",
delete_file.file_format);
}
auto delete_range = build_iceberg_delete_file_range(delete_file.path);
if (_current_task != nullptr && _current_task->data_file != nullptr &&
!_current_task->data_file->fs_name.empty()) {
delete_range.__set_fs_name(_current_task->data_file->fs_name);
}
auto system_properties = _delete_file_system_properties(scan_params);
auto file_description = _delete_file_description(delete_range);
std::shared_ptr<io::IOContext> io_ctx(&delete_io_ctx->io_ctx, [](io::IOContext*) {});
const bool enable_mapping_timestamp_tz = scan_params.__isset.enable_mapping_timestamp_tz &&
scan_params.enable_mapping_timestamp_tz;
const bool enable_mapping_varbinary =
scan_params.__isset.enable_mapping_varbinary && scan_params.enable_mapping_varbinary;
if (delete_file.file_format == TFileFormatType::FORMAT_PARQUET) {
// Delete and data files must parse raw binary fields with the same scan-level mapping.
*reader = std::make_unique<format::parquet::ParquetReader>(
system_properties, file_description, io_ctx, _scanner_profile, std::nullopt,
enable_mapping_timestamp_tz, enable_mapping_varbinary);
} else {
*reader = std::make_unique<format::orc::OrcReader>(system_properties, file_description,
io_ctx, _scanner_profile, std::nullopt,
enable_mapping_timestamp_tz);
}
RETURN_IF_ERROR((*reader)->init(_runtime_state));
return Status::OK();
}
Status IcebergTableReader::_read_position_delete_file(const TIcebergDeleteFileDesc& delete_file,
const TFileScanRangeParams& scan_params,
IcebergDeleteFileIOContext* delete_io_ctx,
PositionDeleteRowsCollector* collector) {
DORIS_CHECK(collector != nullptr);
std::unique_ptr<format::FileReader> reader;
RETURN_IF_ERROR(_create_delete_file_reader(delete_file, scan_params, delete_io_ctx, &reader));
DORIS_CHECK(reader != nullptr);
std::vector<format::ColumnDefinition> schema;
RETURN_IF_ERROR(reader->get_schema(&schema));
format::ColumnDefinition* file_path_field = nullptr;
format::ColumnDefinition* pos_field = nullptr;
for (auto& field : schema) {
if (field.name == ICEBERG_FILE_PATH) {
file_path_field = &field;
} else if (field.name == ICEBERG_ROW_POS) {
pos_field = &field;
}
}
if (file_path_field == nullptr || pos_field == nullptr) {
return Status::InternalError("Position delete file is missing required columns");
}
auto request = std::make_shared<format::FileScanRequest>();
request->non_predicate_columns = {
format::LocalColumnIndex::top_level(
format::LocalColumnId(file_path_field->file_local_id())),
format::LocalColumnIndex::top_level(format::LocalColumnId(pos_field->file_local_id()))};
request->local_positions = {
{format::LocalColumnId(file_path_field->file_local_id()),
format::LocalIndex(ICEBERG_FILE_PATH_BLOCK_POSITION)},
{format::LocalColumnId(pos_field->file_local_id()),
format::LocalIndex(ICEBERG_ROW_POS_BLOCK_POSITION)},
};
RETURN_IF_ERROR(reader->open(request));
bool eof = false;
auto build_position_delete_block = [](const format::ColumnDefinition& file_path_field,
const format::ColumnDefinition& pos_field) -> Block {
Block block;
block.insert(
{file_path_field.type->create_column(), file_path_field.type, ICEBERG_FILE_PATH});
block.insert({pos_field.type->create_column(), pos_field.type, ICEBERG_ROW_POS});
return block;
};
while (!eof) {
Block block = build_position_delete_block(*file_path_field, *pos_field);
size_t read_rows = 0;
RETURN_IF_ERROR(reader->get_block(&block, &read_rows, &eof));
RETURN_IF_ERROR(collector->collect(block, read_rows));
}
return reader->close();
}
Status IcebergTableReader::_init_position_delete_rows(
const std::vector<TIcebergDeleteFileDesc>& delete_files) {
DORIS_CHECK(_split_cache != nullptr);
TFileScanRangeParams delete_scan_params =
_scan_params == nullptr ? TFileScanRangeParams() : *_scan_params;
format::DeleteRows position_delete_rows;
IcebergDeleteFileIOContext delete_io_ctx(_runtime_state);
for (const auto& delete_file : delete_files) {
Status read_status = Status::OK();
// A position delete file normally references many data files. Cache the complete
// path-to-position map once; caching only the current data file would still rescan the
// shared delete file for every subsequent split.
auto* rows_by_data_file =
_split_cache->get<PositionDeleteRowsCollector::PositionDeleteFile>(
_delete_file_cache_key("iceberg_v2_position_delete_", delete_file.path),
[&]() -> PositionDeleteRowsCollector::PositionDeleteFile* {
auto result = std::make_unique<
PositionDeleteRowsCollector::PositionDeleteFile>();
PositionDeleteRowsCollector collector(result.get());
read_status = _read_position_delete_file(
delete_file, delete_scan_params, &delete_io_ctx, &collector);
if (!read_status.ok()) {
return nullptr;
}
for (auto& [_, rows] : *result) {
std::ranges::sort(rows);
}
return result.release();
});
RETURN_IF_ERROR(read_status);
DORIS_CHECK(rows_by_data_file != nullptr);
const auto rows_it = rows_by_data_file->find(_data_file_path());
if (rows_it == rows_by_data_file->end()) {
continue;
}
auto first = rows_it->second.begin();
auto last = rows_it->second.end();
// Bounds are inclusive Iceberg position statistics supplied by FE. Apply them after the
// cached per-data-file vector is sorted so irrelevant positions are sliced without a scan.
if (delete_file.__isset.position_lower_bound) {
first = std::lower_bound(first, last, delete_file.position_lower_bound);
}
if (delete_file.__isset.position_upper_bound) {
last = std::upper_bound(first, last, delete_file.position_upper_bound);
}
position_delete_rows.insert(position_delete_rows.end(), first, last);
}
if (position_delete_rows.empty()) {
return Status::OK();
}
// Position delete files and deletion vectors both become row-position deletes for the
// common TableReader DeletePredicate path. Keep the merged rows in a member vector because
// DeletePredicate stores a reference to the vector used by _delete_rows.
_position_delete_rows_storage.insert(_position_delete_rows_storage.end(),
position_delete_rows.begin(), position_delete_rows.end());
std::sort(_position_delete_rows_storage.begin(), _position_delete_rows_storage.end());
_position_delete_rows_storage.erase(
std::unique(_position_delete_rows_storage.begin(), _position_delete_rows_storage.end()),
_position_delete_rows_storage.end());
_delete_rows = &_position_delete_rows_storage;
return Status::OK();
}
Status IcebergTableReader::_init_equality_delete_predicates(
const std::vector<TIcebergDeleteFileDesc>& delete_files) {
DORIS_CHECK(_split_cache != nullptr);
TFileScanRangeParams delete_scan_params =
_scan_params == nullptr ? TFileScanRangeParams() : *_scan_params;
IcebergDeleteFileIOContext delete_io_ctx(_runtime_state);
for (const auto& delete_file : delete_files) {
RETURN_IF_ERROR(
_read_equality_delete_file(delete_file, delete_scan_params, &delete_io_ctx));
}
return Status::OK();
}
Status IcebergTableReader::_resolve_equality_delete_fields(
const TIcebergDeleteFileDesc& delete_file,
const std::vector<format::ColumnDefinition>& schema,
std::vector<format::ColumnDefinition>* delete_fields, EqualityDeleteFilter* result) const {
DORIS_CHECK(delete_fields != nullptr);
DORIS_CHECK(result != nullptr);
for (const auto field_id : delete_file.field_ids) {
const auto field_it =
std::ranges::find_if(schema, [field_id](const format::ColumnDefinition& field) {
return field.has_identifier_field_id() &&
field_id == field.get_identifier_field_id();
});
if (field_it == schema.end()) {
return Status::InternalError("Can not find field id {} in equality delete file {}",
field_id, delete_file.path);
}
if (!field_it->children.empty()) {
return Status::NotSupported(
"Iceberg equality delete does not support complex column {}", field_it->name);
}
delete_fields->push_back(*field_it);
result->field_ids.push_back(field_id);
result->field_names.push_back(field_it->name);
result->key_types.push_back(field_it->type);
}
return Status::OK();
}
Status IcebergTableReader::_load_equality_delete_file(const TIcebergDeleteFileDesc& delete_file,
const TFileScanRangeParams& scan_params,
IcebergDeleteFileIOContext* delete_io_ctx,
EqualityDeleteFilter* result) {
DORIS_CHECK(result != nullptr);
std::unique_ptr<format::FileReader> reader;
RETURN_IF_ERROR(_create_delete_file_reader(delete_file, scan_params, delete_io_ctx, &reader));
DORIS_CHECK(reader != nullptr);
std::vector<format::ColumnDefinition> schema;
RETURN_IF_ERROR(reader->get_schema(&schema));
std::vector<format::ColumnDefinition> delete_fields;
RETURN_IF_ERROR(_resolve_equality_delete_fields(delete_file, schema, &delete_fields, result));
auto request = std::make_shared<format::FileScanRequest>();
Block delete_block_template;
for (size_t idx = 0; idx < delete_fields.size(); ++idx) {
const auto& delete_field = delete_fields[idx];
const auto local_column_id = format::LocalColumnId(delete_field.file_local_id());
request->non_predicate_columns.push_back(
format::LocalColumnIndex::top_level(local_column_id));
request->local_positions.emplace(local_column_id, format::LocalIndex(idx));
delete_block_template.insert(
{delete_field.type->create_column(), delete_field.type, delete_field.name});
}
RETURN_IF_ERROR(reader->open(request));
MutableBlock mutable_delete_block(delete_block_template.clone_empty());
bool eof = false;
while (!eof) {
Block block = delete_block_template.clone_empty();
size_t read_rows = 0;
RETURN_IF_ERROR(reader->get_block(&block, &read_rows, &eof));
if (read_rows > 0) {
RETURN_IF_ERROR(mutable_delete_block.merge(block));
}
}
RETURN_IF_ERROR(reader->close());
result->delete_block = mutable_delete_block.to_block();
return Status::OK();
}
Status IcebergTableReader::_read_equality_delete_file(const TIcebergDeleteFileDesc& delete_file,
const TFileScanRangeParams& scan_params,
IcebergDeleteFileIOContext* delete_io_ctx) {
if (!delete_file.__isset.field_ids || delete_file.field_ids.empty()) {
return Status::InternalError("Iceberg equality delete file is missing field ids");
}
std::ostringstream cache_key;
cache_key << _delete_file_cache_key("iceberg_v2_equality_delete_", delete_file.path);
cache_key << ':' << delete_file.field_ids.size();
for (const auto field_id : delete_file.field_ids) {
cache_key << ':' << field_id;
}
Status read_status = Status::OK();
// Include the ordered equality ids in the key because the same physical delete file can be
// projected with different key layouts. The cached block and its key metadata are immutable
// after construction and therefore safe to copy into each split-local predicate.
auto* cached_filter = _split_cache->get<EqualityDeleteFilter>(
cache_key.str(), [&]() -> EqualityDeleteFilter* {
auto result = std::make_unique<EqualityDeleteFilter>();
read_status = _load_equality_delete_file(delete_file, scan_params, delete_io_ctx,
result.get());
if (!read_status.ok()) {
return nullptr;
}
return result.release();
});
RETURN_IF_ERROR(read_status);
DORIS_CHECK(cached_filter != nullptr);
_equality_delete_filters.push_back(*cached_filter);
return Status::OK();
}
Status IcebergTableReader::_materialize_row_lineage_row_id(Block* table_block, size_t column_idx) {
if (_row_lineage_columns.first_row_id < 0) {
return Status::OK();
}
DORIS_CHECK(_row_position_block_position < _data_reader.block_template.columns());
const auto& row_position_column = assert_cast<const ColumnInt64&>(
*_data_reader.block_template.get_by_position(_row_position_block_position).column);
DORIS_CHECK(row_position_column.size() == table_block->rows());
auto column = IColumn::mutate(
table_block->get_by_position(column_idx).column->convert_to_full_column_if_const());
auto* nullable_column = assert_cast<ColumnNullable*>(column.get());
auto& null_map = nullable_column->get_null_map_data();
auto& data = assert_cast<ColumnInt64&>(*nullable_column->get_nested_column_ptr()).get_data();
DORIS_CHECK(null_map.size() == row_position_column.size());
DORIS_CHECK(data.size() == row_position_column.size());
for (size_t row = 0; row < row_position_column.size(); ++row) {
if (null_map[row]) {
null_map[row] = 0;
data[row] = _row_lineage_columns.first_row_id + row_position_column.get_element(row);
}
}
table_block->replace_by_position(column_idx, std::move(column));
return Status::OK();
}
Status IcebergTableReader::_materialize_iceberg_rowid(Block* table_block, size_t column_idx) {
DORIS_CHECK(_row_position_block_position < _data_reader.block_template.columns());
const auto& row_position_column = assert_cast<const ColumnInt64&>(
*_data_reader.block_template.get_by_position(_row_position_block_position).column);
DORIS_CHECK(row_position_column.size() == table_block->rows());
const auto& type = table_block->get_by_position(column_idx).type;
auto column = type->create_column();
auto* nullable_column = check_and_get_column<ColumnNullable>(column.get());
auto* struct_column = nullable_column != nullptr
? check_and_get_column<ColumnStruct>(
nullable_column->get_nested_column_ptr().get())
: check_and_get_column<ColumnStruct>(column.get());
DORIS_CHECK(struct_column != nullptr);
DORIS_CHECK(struct_column->tuple_size() >= 4);
const auto rows = row_position_column.size();
const auto file_path = _data_file_path();
const int32_t partition_spec_id =
_iceberg_params.has_value() && _iceberg_params->__isset.partition_spec_id
? _iceberg_params->partition_spec_id
: 0;
const std::string partition_data_json =
_iceberg_params.has_value() && _iceberg_params->__isset.partition_data_json
? _iceberg_params->partition_data_json
: "";
auto& file_path_column = struct_column->get_column(0);
auto& row_pos_column = struct_column->get_column(1);
auto& spec_id_column = struct_column->get_column(2);
auto& partition_data_column = struct_column->get_column(3);
file_path_column.reserve(rows);
row_pos_column.reserve(rows);
spec_id_column.reserve(rows);
partition_data_column.reserve(rows);
for (size_t row = 0; row < rows; ++row) {
file_path_column.insert_data(file_path.data(), file_path.size());
const int64_t row_pos = row_position_column.get_element(row);
row_pos_column.insert_data(reinterpret_cast<const char*>(&row_pos), sizeof(row_pos));
spec_id_column.insert_data(reinterpret_cast<const char*>(&partition_spec_id),
sizeof(partition_spec_id));
partition_data_column.insert_data(partition_data_json.data(), partition_data_json.size());
}
if (nullable_column != nullptr) {
nullable_column->get_null_map_data().resize_fill(rows, 0);
}
table_block->replace_by_position(column_idx, std::move(column));
return Status::OK();
}
Status IcebergTableReader::_materialize_row_lineage_last_updated_sequence_number(
Block* table_block, size_t column_idx) {
if (_row_lineage_columns.last_updated_sequence_number < 0) {
return Status::OK();
}
auto column = IColumn::mutate(
table_block->get_by_position(column_idx).column->convert_to_full_column_if_const());
auto* nullable_column = assert_cast<ColumnNullable*>(column.get());
auto& null_map = nullable_column->get_null_map_data();
auto& data = assert_cast<ColumnInt64&>(*nullable_column->get_nested_column_ptr()).get_data();
DORIS_CHECK(null_map.size() == table_block->rows());
DORIS_CHECK(data.size() == table_block->rows());
for (size_t row = 0; row < table_block->rows(); ++row) {
if (null_map[row]) {
null_map[row] = 0;
data[row] = _row_lineage_columns.last_updated_sequence_number;
}
}
table_block->replace_by_position(column_idx, std::move(column));
return Status::OK();
}
bool IcebergTableReader::_need_row_lineage_row_id() const {
if (_data_reader.column_mapper != nullptr) {
for (const auto& mapping : _data_reader.column_mapper->mappings()) {
if (mapping.virtual_column_type == format::TableVirtualColumnType::ROW_ID) {
return true;
}
}
}
return std::ranges::any_of(_projected_columns, is_projected_row_lineage_row_id);
}
bool IcebergTableReader::_need_iceberg_rowid() const {
if (_data_reader.column_mapper != nullptr) {
for (const auto& mapping : _data_reader.column_mapper->mappings()) {
if (mapping.virtual_column_type == format::TableVirtualColumnType::ICEBERG_ROWID) {
return true;
}
}
}
return std::ranges::any_of(_projected_columns, is_projected_iceberg_rowid);
}
} // namespace doris::format::iceberg