blob: e02ab1f082a3c0ba0a4bd96783ba561bd31d7fc1 [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 <arrow/api.h>
#include <arrow/io/api.h>
#include <gtest/gtest.h>
#include <parquet/api/reader.h>
#include <parquet/api/writer.h>
#include <parquet/arrow/writer.h>
#include <algorithm>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <memory>
#include <optional>
#include <string>
#include <typeinfo>
#include <utility>
#include <vector>
#include "common/config.h"
#include "common/consts.h"
#include "core/assert_cast.h"
#include "core/block/block.h"
#include "core/column/column_array.h"
#include "core/column/column_const.h"
#include "core/column/column_map.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_array.h"
#include "core/data_type/data_type_date_or_datetime_v2.h"
#include "core/data_type/data_type_map.h"
#include "core/data_type/data_type_nullable.h"
#include "core/data_type/data_type_number.h"
#include "core/data_type/data_type_string.h"
#include "core/data_type/data_type_struct.h"
#include "core/data_type/data_type_timestamptz.h"
#include "core/data_type/data_type_varbinary.h"
#include "exec/common/endian.h"
#include "exec/scan/access_path_parser.h"
#include "exprs/runtime_filter_expr.h"
#include "exprs/vectorized_fn_call.h"
#include "exprs/vexpr.h"
#include "exprs/vliteral.h"
#include "exprs/vslot_ref.h"
#include "format/format_common.h"
#include "format/table/deletion_vector_reader.h"
#include "format_v2/table_reader.h"
#include "gen_cpp/Exprs_types.h"
#include "gen_cpp/ExternalTableSchema_types.h"
#include "gen_cpp/PlanNodes_types.h"
#include "io/io_common.h"
#include "roaring/roaring64map.hh"
#include "runtime/runtime_profile.h"
#include "runtime/runtime_state.h"
#include "storage/segment/condition_cache.h"
#include "util/debug_points.h"
#include "util/hash_util.hpp"
namespace doris::format {
namespace {
LocalColumnIndex field_projection(int32_t column_id) {
return LocalColumnIndex {.index = column_id};
}
std::vector<int32_t> projection_ids(const std::vector<LocalColumnIndex>& projections) {
std::vector<int32_t> ids;
ids.reserve(projections.size());
for (const auto& projection : projections) {
ids.push_back(projection.index);
}
return ids;
}
VExprSPtr table_int32_slot_ref(int slot_id, int column_id, const std::string& column_name) {
const auto nullable_int_type = make_nullable(std::make_shared<DataTypeInt32>());
return VSlotRef::create_shared(slot_id, column_id, slot_id, nullable_int_type, column_name);
}
VExprSPtr table_int32_literal(int32_t value) {
return VLiteral::create_shared(std::make_shared<DataTypeInt32>(),
Field::create_field<TYPE_INT>(value));
}
VExprSPtr table_int64_literal(int64_t value) {
return VLiteral::create_shared(std::make_shared<DataTypeInt64>(),
Field::create_field<TYPE_BIGINT>(value));
}
TExprNode table_function_node(const std::string& function_name, const DataTypePtr& return_type,
const std::vector<DataTypePtr>& arg_types,
TExprNodeType::type node_type,
TExprOpcode::type opcode = TExprOpcode::INVALID_OPCODE,
bool short_circuit_evaluation = false) {
TFunctionName fn_name;
fn_name.__set_function_name(function_name);
TFunction fn;
fn.__set_name(fn_name);
fn.__set_binary_type(TFunctionBinaryType::BUILTIN);
std::vector<TTypeDesc> thrift_arg_types;
thrift_arg_types.reserve(arg_types.size());
for (const auto& arg_type : arg_types) {
thrift_arg_types.push_back(arg_type->to_thrift());
}
fn.__set_arg_types(thrift_arg_types);
fn.__set_ret_type(return_type->to_thrift());
fn.__set_has_var_args(false);
TExprNode node;
node.__set_node_type(node_type);
node.__set_opcode(opcode);
node.__set_type(return_type->to_thrift());
node.__set_fn(fn);
node.__set_num_children(static_cast<int16_t>(arg_types.size()));
node.__set_is_nullable(return_type->is_nullable());
if (short_circuit_evaluation) {
node.__set_short_circuit_evaluation(true);
}
return node;
}
VExprSPtr table_function_expr(const std::string& function_name, const DataTypePtr& return_type,
const std::vector<DataTypePtr>& arg_types,
TExprNodeType::type node_type = TExprNodeType::FUNCTION_CALL,
TExprOpcode::type opcode = TExprOpcode::INVALID_OPCODE) {
const auto node = table_function_node(function_name, return_type, arg_types, node_type, opcode);
return VectorizedFnCall::create_shared(node);
}
VExprSPtr table_int32_greater_than_expr(int slot_id, int column_id, int32_t value) {
const auto int_type = std::make_shared<DataTypeInt32>();
const auto nullable_int_type = make_nullable(int_type);
auto expr = table_function_expr("gt", make_nullable(std::make_shared<DataTypeUInt8>()),
{nullable_int_type, int_type}, TExprNodeType::BINARY_PRED,
TExprOpcode::GT);
expr->add_child(table_int32_slot_ref(slot_id, column_id, "id"));
expr->add_child(table_int32_literal(value));
return expr;
}
VExprSPtr table_nullable_int64_binary_predicate(const std::string& function_name,
TExprOpcode::type opcode, int slot_id,
int column_id, const std::string& column_name,
int64_t value) {
const auto int64_type = std::make_shared<DataTypeInt64>();
const auto nullable_int64_type = make_nullable(int64_type);
auto expr = table_function_expr(function_name, make_nullable(std::make_shared<DataTypeUInt8>()),
{nullable_int64_type, int64_type}, TExprNodeType::BINARY_PRED,
opcode);
expr->add_child(
VSlotRef::create_shared(slot_id, column_id, slot_id, nullable_int64_type, column_name));
expr->add_child(table_int64_literal(value));
return expr;
}
class IcebergTableReaderDeleteFileTestHelper final
: public doris::format::iceberg::IcebergTableReader {
public:
Status parse_deletion_vector_file(const TTableFormatFileDesc& t_desc, DeleteFileDesc* desc,
bool* has_delete_file) {
return _parse_deletion_vector_file(t_desc, desc, has_delete_file);
}
};
class IcebergTableReaderScanRequestTestHelper final
: public doris::format::iceberg::IcebergTableReader {
public:
Status init_for_scan_request_test(std::vector<ColumnDefinition> projected_columns) {
_query_options = std::make_unique<TQueryOptions>();
_query_globals = std::make_unique<TQueryGlobals>();
_state = std::make_unique<RuntimeState>(*_query_options, *_query_globals);
RETURN_IF_ERROR(init({
.projected_columns = std::move(projected_columns),
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = _state.get(),
.scanner_profile = nullptr,
}));
SplitReadOptions split_options;
split_options.current_range.__set_path("scan-request-test.parquet");
TTableFormatFileDesc table_format_params;
TIcebergFileDesc iceberg_params;
iceberg_params.__set_first_row_id(1000);
table_format_params.__set_iceberg_params(iceberg_params);
split_options.current_range.__set_table_format_params(table_format_params);
RETURN_IF_ERROR(prepare_split(split_options));
_delete_rows_storage = {1};
_delete_rows = &_delete_rows_storage;
return Status::OK();
}
Status customize_request(FileScanRequest* request) {
return customize_file_scan_request(request);
}
bool current_data_file_is_immutable() const {
DORIS_CHECK(_current_task != nullptr);
DORIS_CHECK(_current_task->data_file != nullptr);
return _current_task->data_file->is_immutable;
}
private:
std::unique_ptr<TQueryOptions> _query_options;
std::unique_ptr<TQueryGlobals> _query_globals;
std::unique_ptr<RuntimeState> _state;
DeleteRows _delete_rows_storage;
};
class IcebergTableReaderMappingModeTestHelper final
: public doris::format::iceberg::IcebergTableReader {
public:
TableColumnMappingMode mapping_mode_for_schema(std::vector<ColumnDefinition> file_schema,
TFileScanRangeParams* scan_params = nullptr) {
_scan_params = scan_params;
_data_reader.file_schema = std::move(file_schema);
return mapping_mode();
}
};
std::shared_ptr<arrow::Array> finish_array(arrow::ArrayBuilder* builder) {
std::shared_ptr<arrow::Array> array;
EXPECT_TRUE(builder->Finish(&array).ok());
return array;
}
std::shared_ptr<arrow::Array> build_int32_array(const std::vector<int32_t>& values) {
arrow::Int32Builder builder;
for (const auto value : values) {
EXPECT_TRUE(builder.Append(value).ok());
}
return finish_array(&builder);
}
std::shared_ptr<arrow::Array> build_int64_array(const std::vector<int64_t>& values) {
arrow::Int64Builder builder;
for (const auto value : values) {
EXPECT_TRUE(builder.Append(value).ok());
}
return finish_array(&builder);
}
std::shared_ptr<arrow::Array> build_nullable_int64_array(
const std::vector<std::optional<int64_t>>& values) {
arrow::Int64Builder builder;
for (const auto& value : values) {
if (value.has_value()) {
EXPECT_TRUE(builder.Append(*value).ok());
} else {
EXPECT_TRUE(builder.AppendNull().ok());
}
}
return finish_array(&builder);
}
std::shared_ptr<arrow::Array> build_nullable_string_array(
const std::vector<std::optional<std::string>>& values) {
arrow::StringBuilder builder;
for (const auto& value : values) {
if (value.has_value()) {
EXPECT_TRUE(builder.Append(*value).ok());
} else {
EXPECT_TRUE(builder.AppendNull().ok());
}
}
return finish_array(&builder);
}
std::shared_ptr<arrow::Array> build_string_array(const std::vector<std::string>& values) {
arrow::StringBuilder builder;
for (const auto& value : values) {
EXPECT_TRUE(builder.Append(value).ok());
}
return finish_array(&builder);
}
schema::external::TFieldPtr external_schema_field(
std::string name, int32_t id, std::vector<std::string> aliases = {},
std::optional<std::string> initial_default = std::nullopt,
std::optional<TColumnType> type = std::nullopt, bool initial_default_is_base64 = false) {
auto field = std::make_shared<schema::external::TField>();
field->__set_name(name);
field->__set_id(id);
if (!aliases.empty()) {
field->__set_name_mapping(aliases);
}
if (initial_default.has_value()) {
field->__set_initial_default_value(*initial_default);
if (initial_default_is_base64) {
field->__set_initial_default_value_is_base64(true);
}
}
if (type.has_value()) {
field->__set_type(*type);
}
schema::external::TFieldPtr field_ptr;
field_ptr.field_ptr = std::move(field);
field_ptr.__isset.field_ptr = true;
return field_ptr;
}
TColumnType external_primitive_type(TPrimitiveType::type type, int32_t len = -1,
int32_t scale = -1) {
TColumnType result;
result.__set_type(type);
if (len >= 0) {
result.__set_len(len);
}
if (scale >= 0) {
result.__set_scale(scale);
}
return result;
}
schema::external::TSchema external_schema(int64_t schema_id,
std::vector<schema::external::TFieldPtr> fields) {
schema::external::TStructField root_field;
root_field.__set_fields(fields);
schema::external::TSchema schema;
schema.__set_schema_id(schema_id);
schema.__set_root_field(root_field);
return schema;
}
void write_iceberg_equality_delete_parquet_file(const std::string& file_path, int32_t field_id,
int32_t value,
const std::string& field_name = "id") {
const auto metadata =
arrow::key_value_metadata({"PARQUET:field_id"}, {std::to_string(field_id)});
auto schema = arrow::schema({
arrow::field(field_name, arrow::int32(), false)->WithMetadata(metadata),
});
auto table = arrow::Table::Make(schema, {build_int32_array({value})});
auto file_result = arrow::io::FileOutputStream::Open(file_path);
ASSERT_TRUE(file_result.ok()) << file_result.status();
std::shared_ptr<arrow::io::FileOutputStream> out = *file_result;
::parquet::WriterProperties::Builder builder;
builder.version(::parquet::ParquetVersion::PARQUET_2_6);
builder.data_page_version(::parquet::ParquetDataPageVersion::V2);
builder.compression(::parquet::Compression::UNCOMPRESSED);
PARQUET_THROW_NOT_OK(::parquet::arrow::WriteTable(*table, arrow::default_memory_pool(), out, 1,
builder.build()));
}
void write_iceberg_timestamp_equality_delete_parquet_file(const std::string& file_path,
int32_t field_id, int64_t value,
const std::string& field_name,
bool adjusted_to_utc = false) {
const auto metadata =
arrow::key_value_metadata({"PARQUET:field_id"}, {std::to_string(field_id)});
const auto timestamp_type = adjusted_to_utc ? arrow::timestamp(arrow::TimeUnit::MICRO, "UTC")
: arrow::timestamp(arrow::TimeUnit::MICRO);
arrow::TimestampBuilder value_builder(timestamp_type, arrow::default_memory_pool());
ASSERT_TRUE(value_builder.Append(value).ok());
auto value_result = value_builder.Finish();
ASSERT_TRUE(value_result.ok()) << value_result.status();
auto schema = arrow::schema({
arrow::field(field_name, timestamp_type, false)->WithMetadata(metadata),
});
auto table = arrow::Table::Make(schema, {*value_result});
auto file_result = arrow::io::FileOutputStream::Open(file_path);
ASSERT_TRUE(file_result.ok()) << file_result.status();
std::shared_ptr<arrow::io::FileOutputStream> out = *file_result;
::parquet::WriterProperties::Builder builder;
builder.version(::parquet::ParquetVersion::PARQUET_2_6);
builder.data_page_version(::parquet::ParquetDataPageVersion::V2);
builder.compression(::parquet::Compression::UNCOMPRESSED);
PARQUET_THROW_NOT_OK(::parquet::arrow::WriteTable(*table, arrow::default_memory_pool(), out, 1,
builder.build()));
}
void write_iceberg_binary_equality_delete_parquet_file(const std::string& file_path,
int32_t field_id, const std::string& value,
const std::string& field_name) {
const auto metadata =
arrow::key_value_metadata({"PARQUET:field_id"}, {std::to_string(field_id)});
arrow::BinaryBuilder value_builder;
ASSERT_TRUE(value_builder.Append(value).ok());
auto value_result = value_builder.Finish();
ASSERT_TRUE(value_result.ok()) << value_result.status();
auto schema = arrow::schema({
arrow::field(field_name, arrow::binary(), false)->WithMetadata(metadata),
});
auto table = arrow::Table::Make(schema, {*value_result});
auto file_result = arrow::io::FileOutputStream::Open(file_path);
ASSERT_TRUE(file_result.ok()) << file_result.status();
std::shared_ptr<arrow::io::FileOutputStream> out = *file_result;
::parquet::WriterProperties::Builder builder;
builder.version(::parquet::ParquetVersion::PARQUET_2_6);
builder.data_page_version(::parquet::ParquetDataPageVersion::V2);
builder.compression(::parquet::Compression::UNCOMPRESSED);
PARQUET_THROW_NOT_OK(::parquet::arrow::WriteTable(*table, arrow::default_memory_pool(), out, 1,
builder.build()));
}
void write_iceberg_null_equality_delete_parquet_file(const std::string& file_path, int32_t field_id,
const std::string& field_name) {
const auto metadata =
arrow::key_value_metadata({"PARQUET:field_id"}, {std::to_string(field_id)});
auto schema = arrow::schema({
arrow::field(field_name, arrow::int32(), true)->WithMetadata(metadata),
});
arrow::Int32Builder value_builder;
ASSERT_TRUE(value_builder.AppendNull().ok());
auto value_result = value_builder.Finish();
ASSERT_TRUE(value_result.ok()) << value_result.status();
auto table = arrow::Table::Make(schema, {*value_result});
auto file_result = arrow::io::FileOutputStream::Open(file_path);
ASSERT_TRUE(file_result.ok()) << file_result.status();
std::shared_ptr<arrow::io::FileOutputStream> out = *file_result;
::parquet::WriterProperties::Builder builder;
builder.version(::parquet::ParquetVersion::PARQUET_2_6);
builder.data_page_version(::parquet::ParquetDataPageVersion::V2);
builder.compression(::parquet::Compression::UNCOMPRESSED);
PARQUET_THROW_NOT_OK(::parquet::arrow::WriteTable(*table, arrow::default_memory_pool(), out, 1,
builder.build()));
}
void write_single_int_parquet_file(const std::string& file_path, const std::string& field_name,
const std::vector<int32_t>& values,
std::optional<int32_t> field_id) {
auto field = arrow::field(field_name, arrow::int32(), false);
if (field_id.has_value()) {
field = field->WithMetadata(
arrow::key_value_metadata({"PARQUET:field_id"}, {std::to_string(*field_id)}));
}
auto table = arrow::Table::Make(arrow::schema({field}), {build_int32_array(values)});
auto file_result = arrow::io::FileOutputStream::Open(file_path);
ASSERT_TRUE(file_result.ok()) << file_result.status();
std::shared_ptr<arrow::io::FileOutputStream> out = *file_result;
::parquet::WriterProperties::Builder builder;
builder.version(::parquet::ParquetVersion::PARQUET_2_6);
builder.data_page_version(::parquet::ParquetDataPageVersion::V2);
builder.compression(::parquet::Compression::UNCOMPRESSED);
PARQUET_THROW_NOT_OK(::parquet::arrow::WriteTable(*table, arrow::default_memory_pool(), out,
static_cast<int64_t>(values.size()),
builder.build()));
}
void write_unsupported_time_first_int_parquet_file(const std::string& file_path,
const std::vector<int32_t>& ids) {
auto file_result = arrow::io::FileOutputStream::Open(file_path);
ASSERT_TRUE(file_result.ok()) << file_result.status();
std::shared_ptr<arrow::io::FileOutputStream> out = *file_result;
// Arrow writes time32 as isAdjustedToUTC=false, which Doris supports. Use Parquet's low-level
// writer so the first physical column is the deliberately unsupported adjusted TIME_MILLIS
// shape from the review report. The query will project only the following supported `id`.
const auto unsupported_time = ::parquet::schema::PrimitiveNode::Make(
"unsupported_time", ::parquet::Repetition::REQUIRED,
::parquet::LogicalType::Time(true, ::parquet::LogicalType::TimeUnit::MILLIS),
::parquet::Type::INT32);
const auto id = ::parquet::schema::PrimitiveNode::Make("id", ::parquet::Repetition::REQUIRED,
::parquet::Type::INT32);
const auto schema_node = ::parquet::schema::GroupNode::Make(
"schema", ::parquet::Repetition::REQUIRED, {unsupported_time, id});
const auto schema = std::static_pointer_cast<::parquet::schema::GroupNode>(schema_node);
auto writer = ::parquet::ParquetFileWriter::Open(out, schema);
auto* row_group = writer->AppendRowGroup();
auto* time_writer = static_cast<::parquet::Int32Writer*>(row_group->NextColumn());
std::vector<int32_t> times(ids.size(), 1000);
const auto row_count = cast_set<int64_t>(ids.size());
EXPECT_EQ(time_writer->WriteBatch(row_count, nullptr, nullptr, times.data()), row_count);
time_writer->Close();
auto* id_writer = static_cast<::parquet::Int32Writer*>(row_group->NextColumn());
EXPECT_EQ(id_writer->WriteBatch(row_count, nullptr, nullptr, ids.data()), row_count);
id_writer->Close();
row_group->Close();
writer->Close();
}
void write_two_int_parquet_file(const std::string& file_path, const std::string& first_name,
const std::vector<int32_t>& first_values,
std::optional<int32_t> first_field_id,
const std::string& second_name,
const std::vector<int32_t>& second_values,
std::optional<int32_t> second_field_id) {
ASSERT_EQ(first_values.size(), second_values.size());
auto first_field = arrow::field(first_name, arrow::int32(), false);
if (first_field_id.has_value()) {
first_field = first_field->WithMetadata(
arrow::key_value_metadata({"PARQUET:field_id"}, {std::to_string(*first_field_id)}));
}
auto second_field = arrow::field(second_name, arrow::int32(), false);
if (second_field_id.has_value()) {
second_field = second_field->WithMetadata(arrow::key_value_metadata(
{"PARQUET:field_id"}, {std::to_string(*second_field_id)}));
}
auto schema = arrow::schema({first_field, second_field});
auto table = arrow::Table::Make(
schema, {build_int32_array(first_values), build_int32_array(second_values)});
auto file_result = arrow::io::FileOutputStream::Open(file_path);
ASSERT_TRUE(file_result.ok()) << file_result.status();
std::shared_ptr<arrow::io::FileOutputStream> out = *file_result;
::parquet::WriterProperties::Builder builder;
builder.version(::parquet::ParquetVersion::PARQUET_2_6);
builder.data_page_version(::parquet::ParquetDataPageVersion::V2);
builder.compression(::parquet::Compression::UNCOMPRESSED);
PARQUET_THROW_NOT_OK(::parquet::arrow::WriteTable(*table, arrow::default_memory_pool(), out,
static_cast<int64_t>(first_values.size()),
builder.build()));
}
void write_recursive_idless_wrapper_parquet_file(const std::string& file_path, int32_t value,
bool outer_has_field_id = true) {
const auto leaf_metadata = arrow::key_value_metadata({"PARQUET:field_id"}, {"30"});
auto leaf_field = arrow::field("leaf", arrow::int32(), false)->WithMetadata(leaf_metadata);
auto inner_result = arrow::StructArray::Make({build_int32_array({value})}, {leaf_field});
ASSERT_TRUE(inner_result.ok()) << inner_result.status();
auto inner_field = arrow::field("inner", arrow::struct_({leaf_field}), false);
auto outer_result = arrow::StructArray::Make({*inner_result}, {inner_field});
ASSERT_TRUE(outer_result.ok()) << outer_result.status();
auto outer_field = arrow::field("outer", arrow::struct_({inner_field}), false);
if (outer_has_field_id) {
outer_field =
outer_field->WithMetadata(arrow::key_value_metadata({"PARQUET:field_id"}, {"10"}));
}
auto table = arrow::Table::Make(arrow::schema({outer_field}), {*outer_result});
auto file_result = arrow::io::FileOutputStream::Open(file_path);
ASSERT_TRUE(file_result.ok()) << file_result.status();
std::shared_ptr<arrow::io::FileOutputStream> out = *file_result;
::parquet::WriterProperties::Builder builder;
builder.version(::parquet::ParquetVersion::PARQUET_2_6);
builder.data_page_version(::parquet::ParquetDataPageVersion::V2);
builder.compression(::parquet::Compression::UNCOMPRESSED);
PARQUET_THROW_NOT_OK(::parquet::arrow::WriteTable(*table, arrow::default_memory_pool(), out, 1,
builder.build()));
}
void write_nullable_idless_struct_with_sibling_id_parquet_file(const std::string& file_path) {
const auto sibling_metadata = arrow::key_value_metadata({"PARQUET:field_id"}, {"2"});
auto sibling_field = arrow::field("b", arrow::int32(), false)->WithMetadata(sibling_metadata);
const auto null_bitmap = arrow::Buffer::FromString(std::string("\x02", 1));
auto struct_result =
arrow::StructArray::Make({build_int32_array({0, 42})}, {sibling_field}, null_bitmap, 1);
ASSERT_TRUE(struct_result.ok()) << struct_result.status();
auto struct_field = arrow::field("s", arrow::struct_({sibling_field}), true);
auto table = arrow::Table::Make(arrow::schema({struct_field}), {*struct_result});
auto file_result = arrow::io::FileOutputStream::Open(file_path);
ASSERT_TRUE(file_result.ok()) << file_result.status();
std::shared_ptr<arrow::io::FileOutputStream> out = *file_result;
::parquet::WriterProperties::Builder builder;
builder.version(::parquet::ParquetVersion::PARQUET_2_6);
builder.data_page_version(::parquet::ParquetDataPageVersion::V2);
builder.compression(::parquet::Compression::UNCOMPRESSED);
PARQUET_THROW_NOT_OK(::parquet::arrow::WriteTable(*table, arrow::default_memory_pool(), out, 2,
builder.build()));
}
void write_nullable_renamed_struct_child_parquet_file(const std::string& file_path) {
const auto child_metadata = arrow::key_value_metadata({"PARQUET:field_id"}, {"1"});
auto child_field = arrow::field("b", arrow::int32(), false)->WithMetadata(child_metadata);
const auto null_bitmap = arrow::Buffer::FromString(std::string("\x02", 1));
auto struct_result =
arrow::StructArray::Make({build_int32_array({0, 10})}, {child_field}, null_bitmap, 1);
ASSERT_TRUE(struct_result.ok()) << struct_result.status();
auto struct_field =
arrow::field("s", arrow::struct_({child_field}), true)
->WithMetadata(arrow::key_value_metadata({"PARQUET:field_id"}, {"10"}));
auto table = arrow::Table::Make(arrow::schema({struct_field}), {*struct_result});
auto file_result = arrow::io::FileOutputStream::Open(file_path);
ASSERT_TRUE(file_result.ok()) << file_result.status();
std::shared_ptr<arrow::io::FileOutputStream> out = *file_result;
::parquet::WriterProperties::Builder builder;
builder.version(::parquet::ParquetVersion::PARQUET_2_6);
builder.data_page_version(::parquet::ParquetDataPageVersion::V2);
builder.compression(::parquet::Compression::UNCOMPRESSED);
PARQUET_THROW_NOT_OK(::parquet::arrow::WriteTable(*table, arrow::default_memory_pool(), out, 2,
builder.build()));
}
void write_timestamp_int_parquet_file(const std::string& file_path,
const std::vector<int64_t>& timestamps,
const std::vector<int32_t>& ids) {
ASSERT_EQ(timestamps.size(), ids.size());
const auto timestamp_metadata = arrow::key_value_metadata({"PARQUET:field_id"}, {"0"});
const auto id_metadata = arrow::key_value_metadata({"PARQUET:field_id"}, {"1"});
const auto timestamp_type = arrow::timestamp(arrow::TimeUnit::MICRO, "UTC");
arrow::TimestampBuilder timestamp_builder(timestamp_type, arrow::default_memory_pool());
ASSERT_TRUE(timestamp_builder.AppendValues(timestamps).ok());
auto timestamp_result = timestamp_builder.Finish();
ASSERT_TRUE(timestamp_result.ok()) << timestamp_result.status();
auto schema = arrow::schema(
{arrow::field("event_time", timestamp_type, false)->WithMetadata(timestamp_metadata),
arrow::field("id", arrow::int32(), false)->WithMetadata(id_metadata)});
auto table = arrow::Table::Make(schema, {*timestamp_result, build_int32_array(ids)});
auto file_result = arrow::io::FileOutputStream::Open(file_path);
ASSERT_TRUE(file_result.ok()) << file_result.status();
std::shared_ptr<arrow::io::FileOutputStream> out = *file_result;
::parquet::WriterProperties::Builder builder;
builder.version(::parquet::ParquetVersion::PARQUET_2_6);
builder.data_page_version(::parquet::ParquetDataPageVersion::V2);
builder.compression(::parquet::Compression::UNCOMPRESSED);
PARQUET_THROW_NOT_OK(::parquet::arrow::WriteTable(*table, arrow::default_memory_pool(), out,
static_cast<int64_t>(timestamps.size()),
builder.build()));
}
void write_iceberg_equality_delete_bigint_parquet_file(const std::string& file_path,
int32_t field_id, int64_t value) {
const auto metadata =
arrow::key_value_metadata({"PARQUET:field_id"}, {std::to_string(field_id)});
auto schema = arrow::schema({
arrow::field("id", arrow::int64(), false)->WithMetadata(metadata),
});
auto table = arrow::Table::Make(schema, {build_int64_array({value})});
auto file_result = arrow::io::FileOutputStream::Open(file_path);
ASSERT_TRUE(file_result.ok()) << file_result.status();
std::shared_ptr<arrow::io::FileOutputStream> out = *file_result;
::parquet::WriterProperties::Builder builder;
builder.version(::parquet::ParquetVersion::PARQUET_2_6);
builder.data_page_version(::parquet::ParquetDataPageVersion::V2);
builder.compression(::parquet::Compression::UNCOMPRESSED);
PARQUET_THROW_NOT_OK(::parquet::arrow::WriteTable(*table, arrow::default_memory_pool(), out, 1,
builder.build()));
}
void write_int_pair_parquet_file(const std::string& file_path, const std::vector<int32_t>& ids,
const std::vector<int32_t>& scores,
const std::vector<std::string>& values,
int64_t row_group_size = -1) {
const auto id_metadata = arrow::key_value_metadata({"PARQUET:field_id"}, {"0"});
const auto score_metadata = arrow::key_value_metadata({"PARQUET:field_id"}, {"1"});
const auto value_metadata = arrow::key_value_metadata({"PARQUET:field_id"}, {"2"});
auto schema = arrow::schema({
arrow::field("id", arrow::int32(), false)->WithMetadata(id_metadata),
arrow::field("score", arrow::int32(), false)->WithMetadata(score_metadata),
arrow::field("value", arrow::utf8(), false)->WithMetadata(value_metadata),
});
auto table = arrow::Table::Make(schema, {build_int32_array(ids), build_int32_array(scores),
build_string_array(values)});
auto file_result = arrow::io::FileOutputStream::Open(file_path);
ASSERT_TRUE(file_result.ok()) << file_result.status();
std::shared_ptr<arrow::io::FileOutputStream> out = *file_result;
::parquet::WriterProperties::Builder builder;
builder.version(::parquet::ParquetVersion::PARQUET_2_6);
builder.data_page_version(::parquet::ParquetDataPageVersion::V2);
builder.compression(::parquet::Compression::UNCOMPRESSED);
const auto write_row_group_size =
row_group_size > 0 ? row_group_size : static_cast<int64_t>(ids.size());
PARQUET_THROW_NOT_OK(::parquet::arrow::WriteTable(*table, arrow::default_memory_pool(), out,
write_row_group_size, builder.build()));
}
void write_iceberg_row_lineage_parquet_file(
const std::string& file_path, const std::vector<int32_t>& ids,
const std::vector<std::optional<int64_t>>& row_ids,
const std::vector<std::optional<int64_t>>& last_updated_sequence_numbers = {}) {
ASSERT_EQ(ids.size(), row_ids.size());
if (!last_updated_sequence_numbers.empty()) {
ASSERT_EQ(ids.size(), last_updated_sequence_numbers.size());
}
const auto id_metadata = arrow::key_value_metadata({"PARQUET:field_id"}, {"0"});
const auto row_id_metadata = arrow::key_value_metadata({"PARQUET:field_id"}, {"2147483540"});
const auto last_updated_sequence_number_metadata =
arrow::key_value_metadata({"PARQUET:field_id"}, {"2147483539"});
auto schema = arrow::schema({
arrow::field("id", arrow::int32(), false)->WithMetadata(id_metadata),
arrow::field("_row_id", arrow::int64(), true)->WithMetadata(row_id_metadata),
});
std::vector<std::shared_ptr<arrow::Array>> arrays = {
build_int32_array(ids),
build_nullable_int64_array(row_ids),
};
if (!last_updated_sequence_numbers.empty()) {
schema =
schema->AddField(schema->num_fields(),
arrow::field("_last_updated_sequence_number", arrow::int64(), true)
->WithMetadata(last_updated_sequence_number_metadata))
.ValueOrDie();
arrays.push_back(build_nullable_int64_array(last_updated_sequence_numbers));
}
auto table = arrow::Table::Make(schema, arrays);
auto file_result = arrow::io::FileOutputStream::Open(file_path);
ASSERT_TRUE(file_result.ok()) << file_result.status();
std::shared_ptr<arrow::io::FileOutputStream> out = *file_result;
::parquet::WriterProperties::Builder builder;
builder.version(::parquet::ParquetVersion::PARQUET_2_6);
builder.data_page_version(::parquet::ParquetDataPageVersion::V2);
builder.compression(::parquet::Compression::UNCOMPRESSED);
PARQUET_THROW_NOT_OK(::parquet::arrow::WriteTable(*table, arrow::default_memory_pool(), out,
static_cast<int64_t>(ids.size()),
builder.build()));
}
void write_position_delete_parquet_file(const std::string& file_path,
const std::vector<std::string>& data_file_paths,
const std::vector<int64_t>& positions) {
auto schema = arrow::schema({
arrow::field("file_path", arrow::utf8(), false),
arrow::field("pos", arrow::int64(), false),
});
auto table = arrow::Table::Make(
schema, {build_string_array(data_file_paths), build_int64_array(positions)});
auto file_result = arrow::io::FileOutputStream::Open(file_path);
ASSERT_TRUE(file_result.ok()) << file_result.status();
std::shared_ptr<arrow::io::FileOutputStream> out = *file_result;
::parquet::WriterProperties::Builder builder;
builder.version(::parquet::ParquetVersion::PARQUET_2_6);
builder.data_page_version(::parquet::ParquetDataPageVersion::V2);
builder.compression(::parquet::Compression::UNCOMPRESSED);
PARQUET_THROW_NOT_OK(::parquet::arrow::WriteTable(*table, arrow::default_memory_pool(), out,
static_cast<int64_t>(positions.size()),
builder.build()));
}
void write_nullable_position_delete_parquet_file(
const std::string& file_path,
const std::vector<std::optional<std::string>>& data_file_paths,
const std::vector<std::optional<int64_t>>& positions) {
ASSERT_EQ(data_file_paths.size(), positions.size());
auto schema = arrow::schema({
arrow::field("file_path", arrow::utf8(), true),
arrow::field("pos", arrow::int64(), true),
});
auto table = arrow::Table::Make(schema, {build_nullable_string_array(data_file_paths),
build_nullable_int64_array(positions)});
auto file_result = arrow::io::FileOutputStream::Open(file_path);
ASSERT_TRUE(file_result.ok()) << file_result.status();
std::shared_ptr<arrow::io::FileOutputStream> out = *file_result;
::parquet::WriterProperties::Builder builder;
builder.version(::parquet::ParquetVersion::PARQUET_2_6);
builder.data_page_version(::parquet::ParquetDataPageVersion::V2);
builder.compression(::parquet::Compression::UNCOMPRESSED);
PARQUET_THROW_NOT_OK(::parquet::arrow::WriteTable(*table, arrow::default_memory_pool(), out,
static_cast<int64_t>(positions.size()),
builder.build()));
}
int64_t write_iceberg_deletion_vector_file(const std::string& file_path,
const std::vector<uint64_t>& deleted_positions) {
roaring::Roaring64Map rows;
for (const auto position : deleted_positions) {
rows.add(position);
}
const size_t bitmap_size = rows.getSizeInBytes();
std::vector<char> blob(4 + 4 + bitmap_size + 4);
rows.write(blob.data() + 8);
const uint32_t total_length = static_cast<uint32_t>(4 + bitmap_size);
BigEndian::Store32(blob.data(), total_length);
constexpr char DV_MAGIC[] = {'\xD1', '\xD3', '\x39', '\x64'};
memcpy(blob.data() + 4, DV_MAGIC, 4);
const uint32_t crc = HashUtil::zlib_crc_hash(blob.data() + 4, total_length, 0);
BigEndian::Store32(blob.data() + 8 + bitmap_size, crc);
std::ofstream output(file_path, std::ios::binary);
EXPECT_TRUE(output.is_open());
output.write(blob.data(), static_cast<std::streamsize>(blob.size()));
EXPECT_TRUE(output.good());
return static_cast<int64_t>(blob.size());
}
class ScopedDebugPoint {
public:
explicit ScopedDebugPoint(std::string name)
: _name(std::move(name)), _enable_debug_points(config::enable_debug_points) {
config::enable_debug_points = true;
DebugPoints::instance()->add(_name);
}
~ScopedDebugPoint() {
DebugPoints::instance()->remove(_name);
config::enable_debug_points = _enable_debug_points;
}
private:
std::string _name;
bool _enable_debug_points;
};
Block build_table_block(const std::vector<ColumnDefinition>& columns) {
Block block;
for (const auto& column : columns) {
block.insert({column.type->create_column(), column.type, column.name});
}
return block;
}
void expect_nullable_int64_column_values(const IColumn& column,
const std::vector<int64_t>& expected_values) {
const auto full_column = column.convert_to_full_column_if_const();
const auto& nullable_column = assert_cast<const ColumnNullable&>(*full_column);
const auto& values =
assert_cast<const ColumnInt64&>(nullable_column.get_nested_column()).get_data();
ASSERT_EQ(nullable_column.size(), expected_values.size());
for (size_t row = 0; row < expected_values.size(); ++row) {
EXPECT_EQ(nullable_column.get_null_map_data()[row], 0);
EXPECT_EQ(values[row], expected_values[row]);
}
}
void expect_nullable_int64_column_optional_values(
const IColumn& column, const std::vector<std::optional<int64_t>>& expected_values) {
const auto full_column = column.convert_to_full_column_if_const();
const auto& nullable_column = assert_cast<const ColumnNullable&>(*full_column);
const auto& values =
assert_cast<const ColumnInt64&>(nullable_column.get_nested_column()).get_data();
ASSERT_EQ(nullable_column.size(), expected_values.size());
for (size_t row = 0; row < expected_values.size(); ++row) {
if (expected_values[row].has_value()) {
EXPECT_EQ(nullable_column.get_null_map_data()[row], 0);
EXPECT_EQ(values[row], *expected_values[row]);
} else {
EXPECT_EQ(nullable_column.get_null_map_data()[row], 1);
}
}
}
const IColumn& expect_not_null_nullable_nested_column(const IColumn& column) {
if (!column.is_nullable()) {
return column;
}
const auto& nullable_column = assert_cast<const ColumnNullable&>(column);
for (const auto is_null : nullable_column.get_null_map_data()) {
EXPECT_EQ(is_null, 0);
}
return nullable_column.get_nested_column();
}
const IColumn& expect_not_null_table_column(const Block& block, size_t position) {
return expect_not_null_nullable_nested_column(*block.get_by_position(position).column);
}
ColumnDefinition make_table_column(int32_t id, const std::string& name, const DataTypePtr& type);
DataTypePtr make_iceberg_rowid_type() {
return make_nullable(std::make_shared<DataTypeStruct>(
DataTypes {std::make_shared<DataTypeString>(), std::make_shared<DataTypeInt64>(),
std::make_shared<DataTypeInt32>(), std::make_shared<DataTypeString>()},
Strings {"file_path", "row_pos", "partition_spec_id", "partition_data_json"}));
}
ColumnDefinition make_iceberg_row_lineage_row_id_column() {
return make_table_column(2147483540, "_row_id",
make_nullable(std::make_shared<DataTypeInt64>()));
}
ColumnDefinition make_iceberg_last_updated_sequence_number_column() {
return make_table_column(2147483539, "_last_updated_sequence_number",
make_nullable(std::make_shared<DataTypeInt64>()));
}
void expect_iceberg_rowid_column_values(const IColumn& column, const std::string& file_path,
const std::vector<int64_t>& row_positions,
int32_t partition_spec_id,
const std::string& partition_data_json) {
const auto full_column = column.convert_to_full_column_if_const();
const auto& nullable_column = assert_cast<const ColumnNullable&>(*full_column);
const auto& struct_column =
assert_cast<const ColumnStruct&>(nullable_column.get_nested_column());
const auto& file_path_column = assert_cast<const ColumnString&>(
expect_not_null_nullable_nested_column(struct_column.get_column(0)));
const auto& row_pos_column = assert_cast<const ColumnInt64&>(
expect_not_null_nullable_nested_column(struct_column.get_column(1)));
const auto& spec_id_column = assert_cast<const ColumnInt32&>(
expect_not_null_nullable_nested_column(struct_column.get_column(2)));
const auto& partition_data_column = assert_cast<const ColumnString&>(
expect_not_null_nullable_nested_column(struct_column.get_column(3)));
ASSERT_EQ(nullable_column.size(), row_positions.size());
for (size_t row = 0; row < row_positions.size(); ++row) {
EXPECT_EQ(nullable_column.get_null_map_data()[row], 0);
EXPECT_EQ(file_path_column.get_data_at(row).to_string(), file_path);
EXPECT_EQ(row_pos_column.get_element(row), row_positions[row]);
EXPECT_EQ(spec_id_column.get_element(row), partition_spec_id);
EXPECT_EQ(partition_data_column.get_data_at(row).to_string(), partition_data_json);
}
}
void expect_int32_column_values(const IColumn& column,
const std::vector<int32_t>& expected_values) {
const auto full_column = column.convert_to_full_column_if_const();
const auto& nested_column = expect_not_null_nullable_nested_column(*full_column);
const auto& values = assert_cast<const ColumnInt32&>(nested_column).get_data();
ASSERT_EQ(values.size(), expected_values.size());
for (size_t row = 0; row < expected_values.size(); ++row) {
EXPECT_EQ(values[row], expected_values[row]);
}
}
SplitReadOptions build_split_options(const std::string& file_path) {
SplitReadOptions options;
EXPECT_EQ(options.cache, nullptr);
options.current_range.__set_path(file_path);
options.current_range.__set_file_size(
static_cast<int64_t>(std::filesystem::file_size(file_path)));
return options;
}
void set_table_level_row_count(SplitReadOptions* split_options, int64_t row_count) {
split_options->current_range.__isset.table_format_params = true;
split_options->current_range.table_format_params.__isset.table_level_row_count = true;
split_options->current_range.table_format_params.table_level_row_count = row_count;
}
void set_iceberg_row_lineage_params(SplitReadOptions* split_options, int64_t first_row_id,
int64_t last_updated_sequence_number) {
TTableFormatFileDesc table_format_params;
TIcebergFileDesc iceberg_params;
iceberg_params.__set_first_row_id(first_row_id);
iceberg_params.__set_last_updated_sequence_number(last_updated_sequence_number);
table_format_params.__set_iceberg_params(iceberg_params);
split_options->current_range.__set_table_format_params(table_format_params);
}
void set_iceberg_rowid_params(SplitReadOptions* split_options,
const std::string& original_file_path, int32_t partition_spec_id,
const std::string& partition_data_json) {
TTableFormatFileDesc table_format_params;
TIcebergFileDesc iceberg_params;
iceberg_params.__set_original_file_path(original_file_path);
iceberg_params.__set_partition_spec_id(partition_spec_id);
iceberg_params.__set_partition_data_json(partition_data_json);
table_format_params.__set_iceberg_params(iceberg_params);
split_options->current_range.__set_table_format_params(table_format_params);
}
TIcebergDeleteFileDesc make_iceberg_deletion_vector(const std::string& path, int64_t offset,
int64_t size) {
TIcebergDeleteFileDesc delete_file;
delete_file.__set_content(3);
delete_file.__set_path(path);
delete_file.__set_content_offset(offset);
delete_file.__set_content_size_in_bytes(size);
return delete_file;
}
TIcebergDeleteFileDesc make_iceberg_position_delete_file(const std::string& path) {
TIcebergDeleteFileDesc delete_file;
delete_file.__set_content(1);
delete_file.__set_path(path);
delete_file.__set_file_format(TFileFormatType::FORMAT_PARQUET);
return delete_file;
}
TIcebergDeleteFileDesc make_iceberg_equality_delete_file(const std::string& path,
const std::vector<int32_t>& field_ids) {
TIcebergDeleteFileDesc delete_file;
delete_file.__set_content(2);
delete_file.__set_path(path);
delete_file.__set_field_ids(field_ids);
delete_file.__set_file_format(TFileFormatType::FORMAT_PARQUET);
return delete_file;
}
TFileScanRangeParams make_local_parquet_scan_params() {
TFileScanRangeParams scan_params;
scan_params.__set_file_type(TFileType::FILE_LOCAL);
scan_params.__set_format_type(TFileFormatType::FORMAT_PARQUET);
scan_params.__set_iceberg_scan_semantics_version(ICEBERG_SCAN_SEMANTICS_VERSION_1);
return scan_params;
}
TColumnAccessPath nested_data_access_path(std::vector<std::string> path) {
TColumnAccessPath access_path;
access_path.__set_type(TAccessPathType::DATA);
TDataAccessPath data_path;
data_path.__set_path(std::move(path));
access_path.__set_data_access_path(std::move(data_path));
return access_path;
}
std::shared_ptr<io::IOContext> make_io_context(io::FileReaderStats* file_reader_stats,
io::FileCacheStatistics* file_cache_stats) {
auto io_ctx = std::make_shared<io::IOContext>();
io_ctx->file_reader_stats = file_reader_stats;
io_ctx->file_cache_stats = file_cache_stats;
return io_ctx;
}
TTableFormatFileDesc make_iceberg_table_format_desc(
const std::string& data_file_path,
const std::vector<TIcebergDeleteFileDesc>& delete_files) {
TTableFormatFileDesc table_format_params;
TIcebergFileDesc iceberg_params;
iceberg_params.__set_format_version(2);
iceberg_params.__set_original_file_path(data_file_path);
iceberg_params.__set_delete_files(delete_files);
table_format_params.__set_iceberg_params(iceberg_params);
return table_format_params;
}
std::vector<int32_t> read_iceberg_ids(doris::format::iceberg::IcebergTableReader* reader,
const std::vector<ColumnDefinition>& projected_columns) {
std::vector<int32_t> ids;
bool eos = false;
while (!eos) {
Block block = build_table_block(projected_columns);
auto status = reader->get_block(&block, &eos);
if (!status.ok()) {
ADD_FAILURE() << status;
return ids;
}
if (block.rows() == 0) {
continue;
}
const auto& id_column =
assert_cast<const ColumnInt32&>(expect_not_null_table_column(block, 0));
for (size_t row = 0; row < block.rows(); ++row) {
ids.push_back(id_column.get_element(row));
}
}
return ids;
}
void init_iceberg_reader(doris::format::iceberg::IcebergTableReader* reader,
const std::vector<ColumnDefinition>& projected_columns,
TFileScanRangeParams* scan_params,
const std::shared_ptr<io::IOContext>& io_ctx, RuntimeState* state,
RuntimeProfile* profile) {
ASSERT_TRUE(reader->init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = scan_params,
.io_ctx = io_ctx,
.runtime_state = state,
.scanner_profile = profile,
})
.ok());
}
DataTypePtr make_table_test_type(const DataTypePtr& type, bool nullable_root = true) {
DORIS_CHECK(type != nullptr);
const auto nested_type = remove_nullable(type);
DataTypePtr result;
if (const auto* struct_type = typeid_cast<const DataTypeStruct*>(nested_type.get())) {
DataTypes child_types;
child_types.reserve(struct_type->get_elements().size());
for (const auto& child_type : struct_type->get_elements()) {
child_types.push_back(make_table_test_type(child_type));
}
result = std::make_shared<DataTypeStruct>(child_types, struct_type->get_element_names());
} else if (const auto* array_type = typeid_cast<const DataTypeArray*>(nested_type.get())) {
result = std::make_shared<DataTypeArray>(
make_table_test_type(array_type->get_nested_type()));
} else if (const auto* map_type = typeid_cast<const DataTypeMap*>(nested_type.get())) {
result = std::make_shared<DataTypeMap>(make_table_test_type(map_type->get_key_type()),
make_table_test_type(map_type->get_value_type()));
} else {
result = nested_type;
}
return nullable_root ? make_nullable(result) : result;
}
ColumnDefinition make_table_column(int32_t id, const std::string& name, const DataTypePtr& type) {
ColumnDefinition column;
if (id >= 0) {
column.identifier = Field::create_field<TYPE_INT>(id);
}
column.name = name;
// TableReader tests model external table scan descriptors. Those table columns are nullable
// even when the Parquet file field itself is required, so keep the test schema aligned with
// the real scan contract at the construction boundary.
column.type = make_table_test_type(type);
return column;
}
ColumnDefinition make_file_column(int32_t id, const std::string& name, const DataTypePtr& type) {
ColumnDefinition field;
field.identifier = Field::create_field<TYPE_INT>(id);
field.local_id = id;
field.name = name;
field.type = make_table_test_type(type);
return field;
}
void set_name_identifiers(std::vector<ColumnDefinition>* columns);
void set_name_identifier(ColumnDefinition* column) {
DORIS_CHECK(column != nullptr);
column->identifier = Field::create_field<TYPE_STRING>(column->name);
set_name_identifiers(&column->children);
}
void set_name_identifiers(std::vector<ColumnDefinition>* columns) {
DORIS_CHECK(columns != nullptr);
for (auto& column : *columns) {
set_name_identifier(&column);
}
}
VExprContextSPtr prepared_conjunct(RuntimeState* state, const VExprSPtr& expr) {
auto ctx = VExprContext::create_shared(expr);
auto status = ctx->prepare(state, RowDescriptor());
EXPECT_TRUE(status.ok()) << status;
status = ctx->open(state);
EXPECT_TRUE(status.ok()) << status;
return ctx;
}
void apply_final_conjuncts(Block* block, const VExprContextSPtrs& conjuncts) {
const auto status = VExprContext::filter_block(conjuncts, block, block->columns());
ASSERT_TRUE(status.ok()) << status;
}
TEST(IcebergV2ReaderTest, IcebergVirtualColumnsUseRowLineageMetadata) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_iceberg_virtual_columns_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_int_pair_parquet_file(file_path, {1, 2, 3}, {10, 20, 30}, {"one", "two", "three"});
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_iceberg_row_lineage_row_id_column());
projected_columns.push_back(make_iceberg_last_updated_sequence_number_column());
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
RuntimeState state {TQueryOptions(), TQueryGlobals()};
doris::format::iceberg::IcebergTableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {prepared_conjunct(
&state, table_int32_greater_than_expr(2, 2, 1))},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
})
.ok());
auto split_options = build_split_options(file_path);
set_iceberg_row_lineage_params(&split_options, 1000, 77);
ASSERT_TRUE(reader.prepare_split(split_options).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
const auto& id_column = assert_cast<const ColumnInt32&>(expect_not_null_table_column(block, 2));
ASSERT_EQ(block.rows(), 2);
EXPECT_EQ(id_column.get_element(0), 2);
EXPECT_EQ(id_column.get_element(1), 3);
expect_nullable_int64_column_values(*block.get_by_position(0).column, {1001, 1002});
expect_nullable_int64_column_values(*block.get_by_position(1).column, {77, 77});
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, IcebergRowLineageUsesPhysicalRowIdAndFillsNulls) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_iceberg_physical_row_id_fill_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_iceberg_row_lineage_parquet_file(file_path, {1, 2, 3}, {7000, std::nullopt, 7002},
{80, std::nullopt, 82});
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(
2147483540, "_row_id", make_nullable(std::make_shared<DataTypeInt64>())));
projected_columns.push_back(
make_table_column(2147483539, "_last_updated_sequence_number",
make_nullable(std::make_shared<DataTypeInt64>())));
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
RuntimeState state {TQueryOptions(), TQueryGlobals()};
doris::format::iceberg::IcebergTableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
})
.ok());
auto split_options = build_split_options(file_path);
set_iceberg_row_lineage_params(&split_options, 1000, 77);
ASSERT_TRUE(reader.prepare_split(split_options).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
ASSERT_EQ(block.rows(), 3);
expect_nullable_int64_column_values(*block.get_by_position(0).column, {7000, 1001, 7002});
expect_nullable_int64_column_values(*block.get_by_position(1).column, {80, 77, 82});
expect_int32_column_values(*block.get_by_position(2).column, {1, 2, 3});
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, IcebergPhysicalRowIdKeepsNullsWithoutFirstRowId) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_iceberg_physical_row_id_no_first_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_iceberg_row_lineage_parquet_file(file_path, {1, 2, 3}, {7000, std::nullopt, 7002},
{80, std::nullopt, 82});
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(
2147483540, "_row_id", make_nullable(std::make_shared<DataTypeInt64>())));
projected_columns.push_back(
make_table_column(2147483539, "_last_updated_sequence_number",
make_nullable(std::make_shared<DataTypeInt64>())));
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
RuntimeState state {TQueryOptions(), TQueryGlobals()};
doris::format::iceberg::IcebergTableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
})
.ok());
ASSERT_TRUE(reader.prepare_split(build_split_options(file_path)).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
ASSERT_EQ(block.rows(), 3);
expect_nullable_int64_column_optional_values(
*block.get_by_position(0).column,
std::vector<std::optional<int64_t>> {7000, std::nullopt, 7002});
expect_nullable_int64_column_optional_values(
*block.get_by_position(1).column,
std::vector<std::optional<int64_t>> {80, std::nullopt, 82});
expect_int32_column_values(*block.get_by_position(2).column, {1, 2, 3});
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, IcebergMissingRowIdStaysNullWithoutFirstRowId) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_iceberg_missing_row_id_no_first_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_int_pair_parquet_file(file_path, {1, 2, 3}, {10, 20, 30}, {"one", "two", "three"});
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_iceberg_row_lineage_row_id_column());
projected_columns.push_back(make_iceberg_last_updated_sequence_number_column());
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
RuntimeState state {TQueryOptions(), TQueryGlobals()};
doris::format::iceberg::IcebergTableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
})
.ok());
ASSERT_TRUE(reader.prepare_split(build_split_options(file_path)).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
ASSERT_EQ(block.rows(), 3);
expect_nullable_int64_column_optional_values(
*block.get_by_position(0).column,
std::vector<std::optional<int64_t>> {std::nullopt, std::nullopt, std::nullopt});
expect_nullable_int64_column_optional_values(
*block.get_by_position(1).column,
std::vector<std::optional<int64_t>> {std::nullopt, std::nullopt, std::nullopt});
expect_int32_column_values(*block.get_by_position(2).column, {1, 2, 3});
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, IcebergRowIdPredicateFiltersAfterRowLineageMaterialization) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_iceberg_row_id_finalize_filter_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_iceberg_row_lineage_parquet_file(file_path, {1, 2, 3}, {7000, std::nullopt, 7002},
{80, std::nullopt, 82});
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(
2147483540, "_row_id", make_nullable(std::make_shared<DataTypeInt64>())));
projected_columns.push_back(
make_table_column(2147483539, "_last_updated_sequence_number",
make_nullable(std::make_shared<DataTypeInt64>())));
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
RuntimeState state {TQueryOptions(), TQueryGlobals()};
VExprContextSPtrs conjuncts = {prepared_conjunct(
&state,
table_nullable_int64_binary_predicate("eq", TExprOpcode::EQ, 0, 0, "_row_id", 1001))};
doris::format::iceberg::IcebergTableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = conjuncts,
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
})
.ok());
auto split_options = build_split_options(file_path);
set_iceberg_row_lineage_params(&split_options, 1000, 77);
ASSERT_TRUE(reader.prepare_split(split_options).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
ASSERT_EQ(block.rows(), 3);
apply_final_conjuncts(&block, conjuncts);
ASSERT_EQ(block.rows(), 1);
expect_nullable_int64_column_values(*block.get_by_position(0).column, {1001});
expect_nullable_int64_column_values(*block.get_by_position(1).column, {77});
expect_int32_column_values(*block.get_by_position(2).column, {2});
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, IcebergLastUpdatedSequencePredicateFiltersAfterMaterialization) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_iceberg_sequence_finalize_filter_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_iceberg_row_lineage_parquet_file(file_path, {1, 2, 3}, {7000, std::nullopt, 7002},
{80, std::nullopt, 82});
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(
2147483540, "_row_id", make_nullable(std::make_shared<DataTypeInt64>())));
projected_columns.push_back(
make_table_column(2147483539, "_last_updated_sequence_number",
make_nullable(std::make_shared<DataTypeInt64>())));
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
RuntimeState state {TQueryOptions(), TQueryGlobals()};
VExprContextSPtrs conjuncts = {prepared_conjunct(
&state, table_nullable_int64_binary_predicate("eq", TExprOpcode::EQ, 1, 1,
"_last_updated_sequence_number", 77))};
doris::format::iceberg::IcebergTableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = conjuncts,
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
})
.ok());
auto split_options = build_split_options(file_path);
set_iceberg_row_lineage_params(&split_options, 1000, 77);
ASSERT_TRUE(reader.prepare_split(split_options).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
ASSERT_EQ(block.rows(), 3);
apply_final_conjuncts(&block, conjuncts);
ASSERT_EQ(block.rows(), 1);
expect_nullable_int64_column_values(*block.get_by_position(0).column, {1001});
expect_nullable_int64_column_values(*block.get_by_position(1).column, {77});
expect_int32_column_values(*block.get_by_position(2).column, {2});
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, IcebergRowidVirtualColumnUsesDataFilePosition) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_iceberg_rowid_virtual_column_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_int_pair_parquet_file(file_path, {1, 2, 3}, {10, 20, 30}, {"one", "two", "three"});
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(
make_table_column(-1, BeConsts::ICEBERG_ROWID_COL, make_iceberg_rowid_type()));
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
RuntimeState state {TQueryOptions(), TQueryGlobals()};
doris::format::iceberg::IcebergTableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {prepared_conjunct(
&state, table_int32_greater_than_expr(1, 1, 1))},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
})
.ok());
auto split_options = build_split_options(file_path);
const auto original_file_path = "s3://bucket/table/data/original.parquet";
const auto partition_data_json = R"({"part":"p1"})";
set_iceberg_rowid_params(&split_options, original_file_path, 17, partition_data_json);
ASSERT_TRUE(reader.prepare_split(split_options).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
ASSERT_EQ(block.rows(), 2);
expect_iceberg_rowid_column_values(*block.get_by_position(0).column, original_file_path, {1, 2},
17, partition_data_json);
expect_int32_column_values(*block.get_by_position(1).column, {2, 3});
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, IcebergVirtualColumnsKeepRowLineageAfterConjunctFiltering) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_iceberg_virtual_columns_conjunct_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_int_pair_parquet_file(file_path, {1, 2, 3}, {10, 20, 30}, {"one", "two", "three"});
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_iceberg_row_lineage_row_id_column());
projected_columns.push_back(make_iceberg_last_updated_sequence_number_column());
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
RuntimeState state {TQueryOptions(), TQueryGlobals()};
doris::format::iceberg::IcebergTableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {prepared_conjunct(
&state, table_int32_greater_than_expr(2, 2, 1))},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
})
.ok());
auto split_options = build_split_options(file_path);
set_iceberg_row_lineage_params(&split_options, 3000, 88);
ASSERT_TRUE(reader.prepare_split(split_options).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
const auto& id_column = assert_cast<const ColumnInt32&>(expect_not_null_table_column(block, 2));
ASSERT_EQ(block.rows(), 2);
EXPECT_EQ(id_column.get_element(0), 2);
EXPECT_EQ(id_column.get_element(1), 3);
expect_nullable_int64_column_values(*block.get_by_position(0).column, {3001, 3002});
expect_nullable_int64_column_values(*block.get_by_position(1).column, {88, 88});
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, IcebergVirtualColumnsKeepRowLineageAfterRowGroupPredicatePruning) {
const auto test_dir = std::filesystem::temp_directory_path() /
"doris_iceberg_virtual_columns_row_group_predicate_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
// Keep one row per row group so the VExpr ZoneMap path can prune the first two row groups and
// leave only the third file-local row.
write_int_pair_parquet_file(file_path, {1, 2, 3}, {10, 20, 30}, {"one", "two", "three"}, 1);
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_iceberg_row_lineage_row_id_column());
projected_columns.push_back(make_iceberg_last_updated_sequence_number_column());
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
RuntimeState state {TQueryOptions(), TQueryGlobals()};
doris::format::iceberg::IcebergTableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {prepared_conjunct(
&state, table_int32_greater_than_expr(2, 2, 2))},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
})
.ok());
auto split_options = build_split_options(file_path);
set_iceberg_row_lineage_params(&split_options, 4000, 99);
ASSERT_TRUE(reader.prepare_split(split_options).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
const auto& id_column = assert_cast<const ColumnInt32&>(expect_not_null_table_column(block, 2));
ASSERT_EQ(block.rows(), 1);
EXPECT_EQ(id_column.get_element(0), 3);
expect_nullable_int64_column_values(*block.get_by_position(0).column, {4002});
expect_nullable_int64_column_values(*block.get_by_position(1).column, {99});
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, IcebergDeletionVectorUsesTableReaderDeleteFileInterface) {
TTableFormatFileDesc table_format_desc;
TIcebergFileDesc iceberg_desc;
iceberg_desc.__set_format_version(2);
iceberg_desc.__set_original_file_path("data-a.parquet");
iceberg_desc.__set_delete_files({make_iceberg_deletion_vector("dv.bin", 8, 128)});
table_format_desc.__set_iceberg_params(iceberg_desc);
IcebergTableReaderDeleteFileTestHelper reader;
DeleteFileDesc desc;
bool has_delete_file = false;
ASSERT_TRUE(reader.parse_deletion_vector_file(table_format_desc, &desc, &has_delete_file).ok());
EXPECT_TRUE(has_delete_file);
EXPECT_EQ(desc.path, "dv.bin");
EXPECT_EQ(desc.start_offset, 8);
EXPECT_EQ(desc.size, 128);
EXPECT_EQ(desc.file_size, -1);
EXPECT_EQ(desc.format, DeleteFileDesc::Format::ICEBERG);
}
TEST(IcebergV2ReaderTest, IcebergDeletionVectorCacheKeyIncludesDataFileAndRange) {
// Scenario: Iceberg Puffin deletion vectors are scoped to the data file. The same Puffin blob
// referenced by different data files, offsets or lengths must not share a DeleteFileDesc key.
const auto shared_dv =
make_iceberg_deletion_vector("s3://bucket/table/shared-dv.puffin", 8, 128);
IcebergTableReaderDeleteFileTestHelper reader;
DeleteFileDesc first_desc;
bool has_delete_file = false;
ASSERT_TRUE(reader.parse_deletion_vector_file(
make_iceberg_table_format_desc("s3://bucket/table/data-a.parquet",
{shared_dv}),
&first_desc, &has_delete_file)
.ok());
EXPECT_TRUE(has_delete_file);
DeleteFileDesc different_data_file_desc;
ASSERT_TRUE(reader.parse_deletion_vector_file(
make_iceberg_table_format_desc("s3://bucket/table/data-b.parquet",
{shared_dv}),
&different_data_file_desc, &has_delete_file)
.ok());
DeleteFileDesc different_offset_desc;
ASSERT_TRUE(reader.parse_deletion_vector_file(
make_iceberg_table_format_desc(
"s3://bucket/table/data-a.parquet",
{make_iceberg_deletion_vector(
"s3://bucket/table/shared-dv.puffin", 16, 128)}),
&different_offset_desc, &has_delete_file)
.ok());
DeleteFileDesc different_length_desc;
ASSERT_TRUE(reader.parse_deletion_vector_file(
make_iceberg_table_format_desc(
"s3://bucket/table/data-a.parquet",
{make_iceberg_deletion_vector(
"s3://bucket/table/shared-dv.puffin", 8, 256)}),
&different_length_desc, &has_delete_file)
.ok());
EXPECT_NE(first_desc.key, different_data_file_desc.key);
EXPECT_NE(first_desc.key, different_offset_desc.key);
EXPECT_NE(first_desc.key, different_length_desc.key);
}
TEST(IcebergV2ReaderTest, IcebergDeletionVectorRejectsMultipleDeleteFiles) {
TTableFormatFileDesc table_format_desc;
TIcebergFileDesc iceberg_desc;
iceberg_desc.__set_format_version(2);
iceberg_desc.__set_delete_files({make_iceberg_deletion_vector("dv-a.bin", 8, 128),
make_iceberg_deletion_vector("dv-b.bin", 16, 256)});
table_format_desc.__set_iceberg_params(iceberg_desc);
IcebergTableReaderDeleteFileTestHelper reader;
DeleteFileDesc desc;
bool has_delete_file = false;
auto status = reader.parse_deletion_vector_file(table_format_desc, &desc, &has_delete_file);
EXPECT_FALSE(status.ok());
}
TEST(IcebergV2ReaderTest, IcebergDeletionVectorRejectsMissingRange) {
TIcebergDeleteFileDesc delete_file;
delete_file.__set_content(3);
delete_file.__set_path("dv.bin");
TTableFormatFileDesc table_format_desc;
TIcebergFileDesc iceberg_desc;
iceberg_desc.__set_format_version(2);
iceberg_desc.__set_delete_files({delete_file});
table_format_desc.__set_iceberg_params(iceberg_desc);
IcebergTableReaderDeleteFileTestHelper reader;
DeleteFileDesc desc;
bool has_delete_file = false;
auto status = reader.parse_deletion_vector_file(table_format_desc, &desc, &has_delete_file);
EXPECT_FALSE(status.ok());
EXPECT_TRUE(status.is<ErrorCode::DATA_QUALITY_ERROR>());
EXPECT_NE(status.to_string().find("descriptor misses"), std::string::npos);
EXPECT_FALSE(has_delete_file);
}
TEST(IcebergV2ReaderTest, IcebergDeletionVectorRejectsInvalidRange) {
TTableFormatFileDesc table_format_desc;
TIcebergFileDesc iceberg_desc;
iceberg_desc.__set_format_version(2);
iceberg_desc.__set_delete_files({make_iceberg_deletion_vector("dv.bin", -1, 12)});
table_format_desc.__set_iceberg_params(iceberg_desc);
IcebergTableReaderDeleteFileTestHelper reader;
DeleteFileDesc desc;
bool has_delete_file = false;
auto status = reader.parse_deletion_vector_file(table_format_desc, &desc, &has_delete_file);
EXPECT_TRUE(status.is<ErrorCode::DATA_QUALITY_ERROR>());
EXPECT_NE(status.to_string().find("offset must be non-negative"), std::string::npos);
EXPECT_FALSE(has_delete_file);
}
TEST(IcebergV2ReaderTest, IcebergTableReaderAppliesDeletionVectorFile) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_iceberg_deletion_vector_file_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
const auto dv_path = (test_dir / "delete-vector.bin").string();
write_int_pair_parquet_file(file_path, {1, 2, 3, 4, 5}, {10, 20, 30, 40, 50},
{"one", "two", "three", "four", "five"});
const auto dv_size = write_iceberg_deletion_vector_file(dv_path, {0, 4});
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
RuntimeProfile profile("test_profile");
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto scan_params = make_local_parquet_scan_params();
io::FileReaderStats file_reader_stats;
io::FileCacheStatistics file_cache_stats;
auto io_ctx = make_io_context(&file_reader_stats, &file_cache_stats);
ShardedKVCache cache(1);
doris::format::iceberg::IcebergTableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = &scan_params,
.io_ctx = io_ctx,
.runtime_state = &state,
.scanner_profile = &profile,
.push_down_agg_type = TPushAggOp::type::COUNT,
})
.ok());
auto split_options = build_split_options(file_path);
split_options.cache = &cache;
split_options.current_range.__set_table_format_params(make_iceberg_table_format_desc(
file_path, {make_iceberg_deletion_vector(dv_path, 0, dv_size)}));
ASSERT_TRUE(reader.prepare_split(split_options).ok());
EXPECT_EQ(read_iceberg_ids(&reader, projected_columns), std::vector<int32_t>({2, 3, 4}));
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, IcebergTableReaderReportsInjectedDeletionVectorReadError) {
const auto test_dir = std::filesystem::temp_directory_path() /
"doris_iceberg_v2_deletion_vector_injected_read_error_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
const auto dv_path = (test_dir / "delete-vector.bin").string();
write_int_pair_parquet_file(file_path, {1, 2, 3}, {10, 20, 30}, {"one", "two", "three"});
const auto dv_size = write_iceberg_deletion_vector_file(dv_path, {0});
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
RuntimeProfile profile("test_profile");
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto scan_params = make_local_parquet_scan_params();
io::FileReaderStats file_reader_stats;
io::FileCacheStatistics file_cache_stats;
auto io_ctx = make_io_context(&file_reader_stats, &file_cache_stats);
ShardedKVCache cache(1);
doris::format::iceberg::IcebergTableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = &scan_params,
.io_ctx = io_ctx,
.runtime_state = &state,
.scanner_profile = &profile,
})
.ok());
auto split_options = build_split_options(file_path);
split_options.cache = &cache;
split_options.current_range.__set_table_format_params(make_iceberg_table_format_desc(
file_path, {make_iceberg_deletion_vector(dv_path, 0, dv_size)}));
ScopedDebugPoint debug_point("TableReader.parse_deletion_vector.io_error");
auto status = reader.prepare_split(split_options);
EXPECT_FALSE(status.ok());
EXPECT_NE(status.to_string().find("injected format v2 deletion vector read failure"),
std::string::npos);
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, IcebergTableReaderStopsDuringDeletionVectorRead) {
const auto test_dir = std::filesystem::temp_directory_path() /
"doris_iceberg_v2_deletion_vector_should_stop_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
const auto dv_path = (test_dir / "delete-vector.bin").string();
write_int_pair_parquet_file(file_path, {1, 2, 3}, {10, 20, 30}, {"one", "two", "three"});
const auto dv_size = write_iceberg_deletion_vector_file(dv_path, {0});
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
RuntimeProfile profile("test_profile");
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto scan_params = make_local_parquet_scan_params();
io::FileReaderStats file_reader_stats;
io::FileCacheStatistics file_cache_stats;
auto io_ctx = make_io_context(&file_reader_stats, &file_cache_stats);
ShardedKVCache cache(1);
doris::format::iceberg::IcebergTableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = &scan_params,
.io_ctx = io_ctx,
.runtime_state = &state,
.scanner_profile = &profile,
})
.ok());
auto split_options = build_split_options(file_path);
split_options.cache = &cache;
split_options.current_range.__set_table_format_params(make_iceberg_table_format_desc(
file_path, {make_iceberg_deletion_vector(dv_path, 0, dv_size)}));
ScopedDebugPoint debug_point("TableReader.parse_deletion_vector.should_stop");
auto status = reader.prepare_split(split_options);
EXPECT_TRUE(status.is<ErrorCode::END_OF_FILE>());
EXPECT_NE(status.to_string().find("stop read"), std::string::npos);
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, IcebergTableReaderRejectsCorruptDeletionVectorPayload) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_iceberg_v2_corrupt_dv_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
const auto dv_path = (test_dir / "delete-vector.bin").string();
write_int_pair_parquet_file(file_path, {1, 2, 3}, {10, 20, 30}, {"one", "two", "three"});
std::vector<char> corrupted(12, 0);
BigEndian::Store32(corrupted.data(), 4);
memcpy(corrupted.data() + 4, "BAD!", 4);
{
std::ofstream output(dv_path, std::ios::binary);
ASSERT_TRUE(output.is_open());
output.write(corrupted.data(), static_cast<std::streamsize>(corrupted.size()));
ASSERT_TRUE(output.good());
}
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
RuntimeProfile profile("test_profile");
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto scan_params = make_local_parquet_scan_params();
io::FileReaderStats file_reader_stats;
io::FileCacheStatistics file_cache_stats;
auto io_ctx = make_io_context(&file_reader_stats, &file_cache_stats);
ShardedKVCache cache(1);
doris::format::iceberg::IcebergTableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = &scan_params,
.io_ctx = io_ctx,
.runtime_state = &state,
.scanner_profile = &profile,
})
.ok());
auto split_options = build_split_options(file_path);
split_options.cache = &cache;
split_options.current_range.__set_table_format_params(make_iceberg_table_format_desc(
file_path,
{make_iceberg_deletion_vector(dv_path, 0, static_cast<int64_t>(corrupted.size()))}));
auto status = reader.prepare_split(split_options);
EXPECT_TRUE(status.is<ErrorCode::DATA_QUALITY_ERROR>());
EXPECT_NE(status.to_string().find("magic number mismatch"), std::string::npos);
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, IcebergTableReaderDoesNotPushDownAggregateWithDeletes) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_iceberg_aggregate_delete_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
const auto dv_path = (test_dir / "delete-vector.bin").string();
write_int_pair_parquet_file(file_path, {1, 2, 3}, {10, 20, 30}, {"one", "two", "three"});
const auto dv_size = write_iceberg_deletion_vector_file(dv_path, {0});
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
RuntimeProfile profile("test_profile");
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto scan_params = make_local_parquet_scan_params();
io::FileReaderStats file_reader_stats;
io::FileCacheStatistics file_cache_stats;
auto io_ctx = make_io_context(&file_reader_stats, &file_cache_stats);
ShardedKVCache cache(1);
doris::format::iceberg::IcebergTableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = &scan_params,
.io_ctx = io_ctx,
.runtime_state = &state,
.scanner_profile = &profile,
.push_down_agg_type = TPushAggOp::type::COUNT,
})
.ok());
auto split_options = build_split_options(file_path);
split_options.cache = &cache;
split_options.current_range.__set_table_format_params(make_iceberg_table_format_desc(
file_path, {make_iceberg_deletion_vector(dv_path, 0, dv_size)}));
ASSERT_TRUE(reader.prepare_split(split_options).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
ASSERT_EQ(block.rows(), 2);
const auto& id_column = assert_cast<const ColumnInt32&>(expect_not_null_table_column(block, 0));
EXPECT_EQ(id_column.get_element(0), 2);
EXPECT_EQ(id_column.get_element(1), 3);
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
// Covers TopN lazy materialization on Iceberg schema-evolution tables. The first-phase scan adds a
// synthesized GLOBAL_ROWID column to the file schema. That virtual column must not make Iceberg
// fall back from field-id mapping to name mapping, otherwise renamed columns are read as defaults
// from old files.
TEST(IcebergV2ReaderTest, IcebergMappingModeIgnoresGlobalRowIdVirtualColumn) {
IcebergTableReaderMappingModeTestHelper reader;
std::vector<ColumnDefinition> file_schema {
make_file_column(1, "id", std::make_shared<DataTypeInt32>()),
make_file_column(2, "name", std::make_shared<DataTypeString>()),
global_rowid_column_definition(),
};
EXPECT_EQ(reader.mapping_mode_for_schema(std::move(file_schema)),
TableColumnMappingMode::BY_FIELD_ID);
}
// Iceberg treats a schema as ID-bearing when any physical data field has an ID. Keeping ID mode
// preserves authoritative matches even when a sibling was written without an ID.
TEST(IcebergV2ReaderTest, IcebergMappingModeUsesAnyDataColumnFieldId) {
IcebergTableReaderMappingModeTestHelper reader;
TFileScanRangeParams scan_params;
scan_params.__set_iceberg_scan_semantics_version(ICEBERG_SCAN_SEMANTICS_VERSION_1);
std::vector<ColumnDefinition> file_schema {
make_file_column(1, "id", std::make_shared<DataTypeInt32>()),
make_file_column(2, "name", std::make_shared<DataTypeString>()),
global_rowid_column_definition(),
};
file_schema[1].identifier = Field {};
EXPECT_EQ(reader.mapping_mode_for_schema(file_schema, &scan_params),
TableColumnMappingMode::BY_FIELD_ID);
file_schema[0].identifier = Field {};
file_schema[0].children.emplace_back(
make_file_column(3, "nested", std::make_shared<DataTypeInt32>()));
EXPECT_EQ(reader.mapping_mode_for_schema(std::move(file_schema), &scan_params),
TableColumnMappingMode::BY_FIELD_ID);
}
TEST(IcebergV2ReaderTest, IcebergLegacyPlanKeepsAllFieldIdsMappingRule) {
IcebergTableReaderMappingModeTestHelper reader;
TFileScanRangeParams old_fe_scan_params;
std::vector<ColumnDefinition> file_schema {
make_file_column(1, "a", std::make_shared<DataTypeInt32>()),
make_file_column(2, "b", std::make_shared<DataTypeInt32>()),
};
file_schema[1].identifier = Field {};
EXPECT_EQ(reader.mapping_mode_for_schema(std::move(file_schema), &old_fe_scan_params),
TableColumnMappingMode::BY_NAME);
auto nested = make_file_column(10, "s", std::make_shared<DataTypeInt32>());
nested.children = {
make_file_column(1, "a", std::make_shared<DataTypeInt32>()),
make_file_column(2, "b", std::make_shared<DataTypeInt32>()),
};
nested.children[1].identifier = Field {};
EXPECT_EQ(reader.mapping_mode_for_schema({std::move(nested)}, &old_fe_scan_params),
TableColumnMappingMode::BY_NAME);
}
TEST(IcebergV2ReaderTest, IcebergTableReaderDoesNotPushDownAggregateWithPositionDelete) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_iceberg_aggregate_position_delete_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
const auto delete_file_path = (test_dir / "position-delete.parquet").string();
write_int_pair_parquet_file(file_path, {1, 2, 3}, {10, 20, 30}, {"one", "two", "three"});
write_position_delete_parquet_file(delete_file_path, {file_path}, {1});
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
RuntimeProfile profile("test_profile");
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto scan_params = make_local_parquet_scan_params();
io::FileReaderStats file_reader_stats;
io::FileCacheStatistics file_cache_stats;
auto io_ctx = make_io_context(&file_reader_stats, &file_cache_stats);
ShardedKVCache cache(1);
doris::format::iceberg::IcebergTableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = &scan_params,
.io_ctx = io_ctx,
.runtime_state = &state,
.scanner_profile = &profile,
.push_down_agg_type = TPushAggOp::type::COUNT,
})
.ok());
auto split_options = build_split_options(file_path);
split_options.cache = &cache;
split_options.current_range.__set_table_format_params(make_iceberg_table_format_desc(
file_path, {make_iceberg_position_delete_file(delete_file_path)}));
ASSERT_TRUE(reader.prepare_split(split_options).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
ASSERT_EQ(block.rows(), 2);
const auto& id_column = assert_cast<const ColumnInt32&>(expect_not_null_table_column(block, 0));
EXPECT_EQ(id_column.get_element(0), 1);
EXPECT_EQ(id_column.get_element(1), 3);
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, IcebergTableLevelCountUsesAssignedRowCountWithPositionDelete) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_iceberg_table_level_count_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
const auto delete_file_path = (test_dir / "position-delete.parquet").string();
write_int_pair_parquet_file(file_path, {1, 2, 3}, {10, 20, 30}, {"one", "two", "three"});
write_position_delete_parquet_file(delete_file_path, {file_path}, {1});
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
TQueryOptions query_options;
query_options.__set_batch_size(10);
RuntimeState state {query_options, TQueryGlobals()};
doris::format::iceberg::IcebergTableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
.push_down_agg_type = TPushAggOp::type::COUNT,
// An explicit empty argument list is the new FE marker for
// COUNT(*)/COUNT(1); nullopt intentionally exercises fallback.
.push_down_count_columns = std::vector<GlobalIndex> {},
})
.ok());
auto split_options = build_split_options(file_path);
split_options.current_range.__set_table_format_params(make_iceberg_table_format_desc(
file_path, {make_iceberg_position_delete_file(delete_file_path)}));
set_table_level_row_count(&split_options, 5);
ASSERT_TRUE(reader.prepare_split(split_options).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
EXPECT_EQ(block.rows(), 5);
block = build_table_block(projected_columns);
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
EXPECT_TRUE(eos);
EXPECT_EQ(block.rows(), 0);
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, IcebergPositionDeleteFallsBackToSplitPath) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_iceberg_position_delete_path_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
const auto delete_file_path = (test_dir / "position-delete.parquet").string();
write_int_pair_parquet_file(file_path, {1, 2, 3}, {10, 20, 30}, {"one", "two", "three"});
write_position_delete_parquet_file(delete_file_path, {file_path}, {1});
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
RuntimeProfile profile("test_profile");
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto scan_params = make_local_parquet_scan_params();
io::FileReaderStats file_reader_stats;
io::FileCacheStatistics file_cache_stats;
auto io_ctx = make_io_context(&file_reader_stats, &file_cache_stats);
ShardedKVCache cache(1);
doris::format::iceberg::IcebergTableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = &scan_params,
.io_ctx = io_ctx,
.runtime_state = &state,
.scanner_profile = &profile,
})
.ok());
auto split_options = build_split_options(file_path);
split_options.cache = &cache;
TTableFormatFileDesc table_format_params;
TIcebergFileDesc iceberg_params;
iceberg_params.__set_format_version(2);
iceberg_params.__set_delete_files({make_iceberg_position_delete_file(delete_file_path)});
table_format_params.__set_iceberg_params(iceberg_params);
split_options.current_range.__set_table_format_params(table_format_params);
ASSERT_TRUE(reader.prepare_split(split_options).ok());
EXPECT_EQ(read_iceberg_ids(&reader, projected_columns), std::vector<int32_t>({1, 3}));
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, IcebergTableReaderDoesNotPushDownAggregateWithEqualityDelete) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_iceberg_aggregate_equality_delete_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
const auto delete_file_path = (test_dir / "equality-delete.parquet").string();
write_int_pair_parquet_file(file_path, {1, 2, 3}, {10, 20, 30}, {"one", "two", "three"});
write_iceberg_equality_delete_parquet_file(delete_file_path, 0, 2);
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
RuntimeProfile profile("test_profile");
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto scan_params = make_local_parquet_scan_params();
io::FileReaderStats file_reader_stats;
io::FileCacheStatistics file_cache_stats;
auto io_ctx = make_io_context(&file_reader_stats, &file_cache_stats);
ShardedKVCache cache(1);
doris::format::iceberg::IcebergTableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = &scan_params,
.io_ctx = io_ctx,
.runtime_state = &state,
.scanner_profile = &profile,
.push_down_agg_type = TPushAggOp::type::COUNT,
})
.ok());
auto split_options = build_split_options(file_path);
split_options.cache = &cache;
split_options.current_range.__set_table_format_params(make_iceberg_table_format_desc(
file_path, {make_iceberg_equality_delete_file(delete_file_path, {0})}));
ASSERT_TRUE(reader.prepare_split(split_options).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
ASSERT_EQ(block.rows(), 2);
const auto& id_column = assert_cast<const ColumnInt32&>(expect_not_null_table_column(block, 0));
EXPECT_EQ(id_column.get_element(0), 1);
EXPECT_EQ(id_column.get_element(1), 3);
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, IcebergEqualityDeleteCastsDataColumnToDeleteKeyType) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_iceberg_equality_delete_cast_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
const auto delete_file_path = (test_dir / "equality-delete.parquet").string();
write_int_pair_parquet_file(file_path, {1, 2, 3}, {10, 20, 30}, {"one", "two", "three"});
write_iceberg_equality_delete_bigint_parquet_file(delete_file_path, 0, 2);
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
RuntimeProfile profile("test_profile");
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto scan_params = make_local_parquet_scan_params();
io::FileReaderStats file_reader_stats;
io::FileCacheStatistics file_cache_stats;
auto io_ctx = make_io_context(&file_reader_stats, &file_cache_stats);
ShardedKVCache cache(1);
doris::format::iceberg::IcebergTableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = &scan_params,
.io_ctx = io_ctx,
.runtime_state = &state,
.scanner_profile = &profile,
})
.ok());
auto split_options = build_split_options(file_path);
split_options.cache = &cache;
split_options.current_range.__set_table_format_params(make_iceberg_table_format_desc(
file_path, {make_iceberg_equality_delete_file(delete_file_path, {0})}));
ASSERT_TRUE(reader.prepare_split(split_options).ok());
EXPECT_EQ(read_iceberg_ids(&reader, projected_columns), std::vector<int32_t>({1, 3}));
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, IcebergEqualityDeleteMatchesNullForMissingDataColumn) {
const auto test_dir = std::filesystem::temp_directory_path() /
"doris_iceberg_equality_delete_missing_column_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
const auto delete_file_path = (test_dir / "equality-delete.parquet").string();
write_single_int_parquet_file(file_path, "id", {1, 2, 3}, 0);
write_iceberg_null_equality_delete_parquet_file(delete_file_path, 1, "added_column");
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
RuntimeProfile profile("test_profile");
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto scan_params = make_local_parquet_scan_params();
io::FileReaderStats file_reader_stats;
io::FileCacheStatistics file_cache_stats;
auto io_ctx = make_io_context(&file_reader_stats, &file_cache_stats);
ShardedKVCache cache(1);
doris::format::iceberg::IcebergTableReader reader;
init_iceberg_reader(&reader, projected_columns, &scan_params, io_ctx, &state, &profile);
auto split_options = build_split_options(file_path);
split_options.cache = &cache;
split_options.current_range.__set_table_format_params(make_iceberg_table_format_desc(
file_path, {make_iceberg_equality_delete_file(delete_file_path, {1})}));
ASSERT_TRUE(reader.prepare_split(split_options).ok());
// Iceberg treats a field missing from an older data file as NULL. The NULL equality-delete
// key therefore matches every row in this file.
EXPECT_TRUE(read_iceberg_ids(&reader, projected_columns).empty());
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, IcebergEqualityDeleteMissingKeyDoesNotReadUnsupportedUnprojectedCarrier) {
const auto test_dir = std::filesystem::temp_directory_path() /
"doris_iceberg_equality_delete_virtual_carrier_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
const auto delete_file_path = (test_dir / "equality-delete.parquet").string();
write_unsupported_time_first_int_parquet_file(file_path, {1, 2, 3});
write_iceberg_equality_delete_parquet_file(delete_file_path, 1, 7, "added_column");
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
RuntimeProfile profile("test_profile");
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto scan_params = make_local_parquet_scan_params();
io::FileReaderStats file_reader_stats;
io::FileCacheStatistics file_cache_stats;
auto io_ctx = make_io_context(&file_reader_stats, &file_cache_stats);
ShardedKVCache cache(1);
doris::format::iceberg::IcebergTableReader reader;
init_iceberg_reader(&reader, projected_columns, &scan_params, io_ctx, &state, &profile);
auto split_options = build_split_options(file_path);
split_options.cache = &cache;
split_options.current_range.__set_table_format_params(make_iceberg_table_format_desc(
file_path, {make_iceberg_equality_delete_file(delete_file_path, {1})}));
ASSERT_TRUE(reader.prepare_split(split_options).ok());
// The missing data key is NULL, so it does not match delete key 7. More importantly, the
// hidden row-count dependency must use virtual row position instead of the first physical
// TIME_MILLIS column, which is unsupported and was not requested by this `id` projection.
EXPECT_EQ(read_iceberg_ids(&reader, projected_columns), std::vector<int32_t>({1, 2, 3}));
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, IcebergEqualityDeleteMatchesInitialDefaultForMissingDataColumn) {
const auto test_dir = std::filesystem::temp_directory_path() /
"doris_iceberg_equality_delete_missing_default_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
const auto delete_file_path = (test_dir / "equality-delete.parquet").string();
write_single_int_parquet_file(file_path, "id", {1, 2, 3}, 0);
write_iceberg_equality_delete_parquet_file(delete_file_path, 1, 7, "added_column");
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
RuntimeProfile profile("test_profile");
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto scan_params = make_local_parquet_scan_params();
scan_params.__set_current_schema_id(100);
scan_params.__set_history_schema_info(
{external_schema(100, {external_schema_field("id", 0),
external_schema_field("added_column", 1, {}, "7")})});
io::FileReaderStats file_reader_stats;
io::FileCacheStatistics file_cache_stats;
auto io_ctx = make_io_context(&file_reader_stats, &file_cache_stats);
ShardedKVCache cache(1);
doris::format::iceberg::IcebergTableReader reader;
init_iceberg_reader(&reader, projected_columns, &scan_params, io_ctx, &state, &profile);
auto split_options = build_split_options(file_path);
split_options.cache = &cache;
split_options.current_range.__set_table_format_params(make_iceberg_table_format_desc(
file_path, {make_iceberg_equality_delete_file(delete_file_path, {1})}));
ASSERT_TRUE(reader.prepare_split(split_options).ok());
// The old file has no physical added_column, but Iceberg defines every row as the initial
// default 7. The equality delete key 7 therefore removes the entire file.
EXPECT_TRUE(read_iceberg_ids(&reader, projected_columns).empty());
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, IcebergEqualityDeleteMatchesTimestampInitialDefaultForMissingColumn) {
const auto test_dir = std::filesystem::temp_directory_path() /
"doris_iceberg_equality_delete_missing_timestamp_default_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
const auto delete_file_path = (test_dir / "equality-delete.parquet").string();
write_single_int_parquet_file(file_path, "id", {1, 2, 3}, 0);
write_iceberg_timestamp_equality_delete_parquet_file(delete_file_path, 1,
1'704'067'200'123'456L, "added_timestamp");
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
auto scan_params = make_local_parquet_scan_params();
scan_params.__set_current_schema_id(100);
scan_params.__set_history_schema_info({external_schema(
100,
{external_schema_field("id", 0),
external_schema_field("added_timestamp", 1, {}, "2024-01-01 00:00:00.123456",
external_primitive_type(TPrimitiveType::DATETIMEV2, -1, 6))})});
RuntimeProfile profile("test_profile");
RuntimeState state {TQueryOptions(), TQueryGlobals()};
io::FileReaderStats file_reader_stats;
io::FileCacheStatistics file_cache_stats;
auto io_ctx = make_io_context(&file_reader_stats, &file_cache_stats);
ShardedKVCache cache(1);
doris::format::iceberg::IcebergTableReader reader;
init_iceberg_reader(&reader, projected_columns, &scan_params, io_ctx, &state, &profile);
auto split_options = build_split_options(file_path);
split_options.cache = &cache;
split_options.current_range.__set_table_format_params(make_iceberg_table_format_desc(
file_path, {make_iceberg_equality_delete_file(delete_file_path, {1})}));
ASSERT_TRUE(reader.prepare_split(split_options).ok());
EXPECT_TRUE(read_iceberg_ids(&reader, projected_columns).empty());
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, IcebergEqualityDeleteMatchesVarbinaryInitialDefaultForMissingColumn) {
const auto test_dir = std::filesystem::temp_directory_path() /
"doris_iceberg_equality_delete_missing_varbinary_default_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
const auto delete_file_path = (test_dir / "equality-delete.parquet").string();
write_single_int_parquet_file(file_path, "id", {1, 2, 3}, 0);
const std::string binary_default(
"\x12\x3e\x45\x67\xe8\x9b\x12\xd3\xa4\x56\x42\x66\x14\x17\x40\x00", 16);
write_iceberg_binary_equality_delete_parquet_file(delete_file_path, 1, binary_default,
"added_binary");
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
auto scan_params = make_local_parquet_scan_params();
scan_params.__set_current_schema_id(100);
scan_params.__set_history_schema_info({external_schema(
100, {external_schema_field("id", 0),
external_schema_field("added_binary", 1, {}, "Ej5FZ+ibEtOkVkJmFBdAAA==",
external_primitive_type(TPrimitiveType::VARBINARY, 16),
true)})});
RuntimeProfile profile("test_profile");
RuntimeState state {TQueryOptions(), TQueryGlobals()};
io::FileReaderStats file_reader_stats;
io::FileCacheStatistics file_cache_stats;
auto io_ctx = make_io_context(&file_reader_stats, &file_cache_stats);
ShardedKVCache cache(1);
doris::format::iceberg::IcebergTableReader reader;
init_iceberg_reader(&reader, projected_columns, &scan_params, io_ctx, &state, &profile);
auto split_options = build_split_options(file_path);
split_options.cache = &cache;
split_options.current_range.__set_table_format_params(make_iceberg_table_format_desc(
file_path, {make_iceberg_equality_delete_file(delete_file_path, {1})}));
ASSERT_TRUE(reader.prepare_split(split_options).ok());
EXPECT_TRUE(read_iceberg_ids(&reader, projected_columns).empty());
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, IcebergEqualityDeleteMatchesStringMappedBinaryInitialDefault) {
const auto test_dir = std::filesystem::temp_directory_path() /
"doris_iceberg_equality_delete_missing_binary_string_default_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
const auto delete_file_path = (test_dir / "equality-delete.parquet").string();
write_single_int_parquet_file(file_path, "id", {1, 2, 3}, 0);
const std::string binary_default("\x00\x01\x02\xff", 4);
write_iceberg_binary_equality_delete_parquet_file(delete_file_path, 1, binary_default,
"added_binary");
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
auto scan_params = make_local_parquet_scan_params();
scan_params.__set_current_schema_id(100);
scan_params.__set_history_schema_info({external_schema(
100, {external_schema_field("id", 0),
external_schema_field("added_binary", 1, {}, "AAEC/w==",
external_primitive_type(TPrimitiveType::STRING), true)})});
RuntimeProfile profile("test_profile");
RuntimeState state {TQueryOptions(), TQueryGlobals()};
io::FileReaderStats file_reader_stats;
io::FileCacheStatistics file_cache_stats;
auto io_ctx = make_io_context(&file_reader_stats, &file_cache_stats);
ShardedKVCache cache(1);
doris::format::iceberg::IcebergTableReader reader;
init_iceberg_reader(&reader, projected_columns, &scan_params, io_ctx, &state, &profile);
auto split_options = build_split_options(file_path);
split_options.cache = &cache;
split_options.current_range.__set_table_format_params(make_iceberg_table_format_desc(
file_path, {make_iceberg_equality_delete_file(delete_file_path, {1})}));
ASSERT_TRUE(reader.prepare_split(split_options).ok());
EXPECT_TRUE(read_iceberg_ids(&reader, projected_columns).empty());
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, IcebergEqualityDeleteDecodesBinaryDefaultMappedToString) {
const auto test_dir = std::filesystem::temp_directory_path() /
"doris_iceberg_equality_delete_missing_string_binary_default_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
const auto delete_file_path = (test_dir / "equality-delete.parquet").string();
write_single_int_parquet_file(file_path, "id", {1, 2, 3}, 0);
const std::string binary_default("\x00\x01\x02\xff", 4);
write_iceberg_binary_equality_delete_parquet_file(delete_file_path, 1, binary_default,
"added_binary");
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
auto scan_params = make_local_parquet_scan_params();
scan_params.__set_current_schema_id(100);
scan_params.__set_history_schema_info({external_schema(
100, {external_schema_field("id", 0),
external_schema_field("added_binary", 1, {}, "AAEC/w==",
external_primitive_type(TPrimitiveType::STRING), true)})});
RuntimeProfile profile("test_profile");
RuntimeState state {TQueryOptions(), TQueryGlobals()};
io::FileReaderStats file_reader_stats;
io::FileCacheStatistics file_cache_stats;
auto io_ctx = make_io_context(&file_reader_stats, &file_cache_stats);
ShardedKVCache cache(1);
doris::format::iceberg::IcebergTableReader reader;
init_iceberg_reader(&reader, projected_columns, &scan_params, io_ctx, &state, &profile);
auto split_options = build_split_options(file_path);
split_options.cache = &cache;
split_options.current_range.__set_table_format_params(make_iceberg_table_format_desc(
file_path, {make_iceberg_equality_delete_file(delete_file_path, {1})}));
ASSERT_TRUE(reader.prepare_split(split_options).ok());
EXPECT_TRUE(read_iceberg_ids(&reader, projected_columns).empty());
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, IcebergEqualityDeletePreservesTimestampTzAcrossDstFold) {
const auto test_dir = std::filesystem::temp_directory_path() /
"doris_iceberg_equality_delete_timestamp_tz_dst_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
const auto delete_file_path = (test_dir / "equality-delete.parquet").string();
// These instants are the two occurrences of 01:30 during the 2024 New York DST fall-back.
constexpr int64_t FIRST_0130_MICROS = 1'730'611'800'000'000L;
constexpr int64_t SECOND_0130_MICROS = 1'730'615'400'000'000L;
write_timestamp_int_parquet_file(file_path, {FIRST_0130_MICROS, SECOND_0130_MICROS}, {1, 2});
write_iceberg_timestamp_equality_delete_parquet_file(delete_file_path, 0, FIRST_0130_MICROS,
"event_time", true);
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(1, "id", std::make_shared<DataTypeInt32>()));
auto scan_params = make_local_parquet_scan_params();
scan_params.__set_enable_mapping_timestamp_tz(true);
scan_params.__set_current_schema_id(100);
scan_params.__set_history_schema_info({external_schema(
100,
{external_schema_field("event_time", 0, {}, std::nullopt,
external_primitive_type(TPrimitiveType::TIMESTAMPTZ, -1, 6)),
external_schema_field("id", 1)})});
RuntimeProfile profile("test_profile");
RuntimeState state {TQueryOptions(), TQueryGlobals()};
state.set_timezone("America/New_York");
io::FileReaderStats file_reader_stats;
io::FileCacheStatistics file_cache_stats;
auto io_ctx = make_io_context(&file_reader_stats, &file_cache_stats);
ShardedKVCache cache(1);
doris::format::iceberg::IcebergTableReader reader;
init_iceberg_reader(&reader, projected_columns, &scan_params, io_ctx, &state, &profile);
auto split_options = build_split_options(file_path);
split_options.cache = &cache;
split_options.current_range.__set_table_format_params(make_iceberg_table_format_desc(
file_path, {make_iceberg_equality_delete_file(delete_file_path, {0})}));
ASSERT_TRUE(reader.prepare_split(split_options).ok());
EXPECT_EQ(read_iceberg_ids(&reader, projected_columns), std::vector<int32_t>({2}));
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, IcebergEqualityDeleteUsesNameMappingWithoutFileFieldIds) {
const auto test_dir = std::filesystem::temp_directory_path() /
"doris_iceberg_equality_delete_name_mapping_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
const auto delete_file_path = (test_dir / "equality-delete.parquet").string();
write_single_int_parquet_file(file_path, "legacy_id", {1, 2, 3}, std::nullopt);
write_iceberg_equality_delete_parquet_file(delete_file_path, 0, 2, "current_id");
std::vector<ColumnDefinition> projected_columns;
auto id_column = make_table_column(0, "current_id", std::make_shared<DataTypeInt32>());
id_column.name_mapping.push_back("legacy_id");
projected_columns.push_back(std::move(id_column));
RuntimeProfile profile("test_profile");
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto scan_params = make_local_parquet_scan_params();
io::FileReaderStats file_reader_stats;
io::FileCacheStatistics file_cache_stats;
auto io_ctx = make_io_context(&file_reader_stats, &file_cache_stats);
ShardedKVCache cache(1);
doris::format::iceberg::IcebergTableReader reader;
init_iceberg_reader(&reader, projected_columns, &scan_params, io_ctx, &state, &profile);
auto split_options = build_split_options(file_path);
split_options.cache = &cache;
split_options.current_range.__set_table_format_params(make_iceberg_table_format_desc(
file_path, {make_iceberg_equality_delete_file(delete_file_path, {0})}));
ASSERT_TRUE(reader.prepare_split(split_options).ok());
EXPECT_EQ(read_iceberg_ids(&reader, projected_columns), std::vector<int32_t>({1, 3}));
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, ParquetRecursivelyRetainsIdlessWrapperForSelectedLeafId) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_iceberg_recursive_idless_wrapper_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_recursive_idless_wrapper_parquet_file(file_path, 42);
const auto int_type = std::make_shared<DataTypeInt32>();
auto leaf = make_table_column(30, "leaf", int_type);
auto inner_type = std::make_shared<DataTypeStruct>(DataTypes {int_type}, Strings {"leaf"});
auto inner = make_table_column(20, "inner", inner_type);
inner.children = {leaf};
auto outer_type = std::make_shared<DataTypeStruct>(DataTypes {inner_type}, Strings {"inner"});
auto outer = make_table_column(10, "outer", outer_type);
outer.children = {inner};
std::vector<ColumnDefinition> projected_columns = {outer};
RuntimeProfile profile("test_profile");
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto scan_params = make_local_parquet_scan_params();
io::FileReaderStats file_reader_stats;
io::FileCacheStatistics file_cache_stats;
auto io_ctx = make_io_context(&file_reader_stats, &file_cache_stats);
ShardedKVCache cache(1);
doris::format::iceberg::IcebergTableReader reader;
init_iceberg_reader(&reader, projected_columns, &scan_params, io_ctx, &state, &profile);
auto split_options = build_split_options(file_path);
split_options.cache = &cache;
split_options.current_range.__set_table_format_params(
make_iceberg_table_format_desc(file_path, {}));
ASSERT_TRUE(reader.prepare_split(split_options).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
const auto& outer_result =
assert_cast<const ColumnStruct&>(expect_not_null_table_column(block, 0));
const auto& inner_result = assert_cast<const ColumnStruct&>(
expect_not_null_nullable_nested_column(outer_result.get_column(0)));
const auto& leaf_result = assert_cast<const ColumnInt32&>(
expect_not_null_nullable_nested_column(inner_result.get_column(0)));
ASSERT_EQ(leaf_result.size(), 1);
EXPECT_EQ(leaf_result.get_element(0), 42);
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, ParquetReadsIdlessWrapperWithAuthoritativeEmptyMapping) {
const auto test_dir = std::filesystem::temp_directory_path() /
"doris_iceberg_authoritative_empty_idless_wrapper_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_recursive_idless_wrapper_parquet_file(file_path, 42, false);
const auto int_type = std::make_shared<DataTypeInt32>();
auto leaf = make_table_column(30, "leaf", int_type);
auto inner_type = std::make_shared<DataTypeStruct>(DataTypes {int_type}, Strings {"leaf"});
auto inner = make_table_column(20, "inner", inner_type);
inner.children = {leaf};
inner.has_name_mapping = true;
auto outer_type = std::make_shared<DataTypeStruct>(DataTypes {inner_type}, Strings {"inner"});
auto outer = make_table_column(10, "outer", outer_type);
outer.children = {inner};
outer.has_name_mapping = true;
std::vector<ColumnDefinition> projected_columns = {outer};
RuntimeProfile profile("test_profile");
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto scan_params = make_local_parquet_scan_params();
io::FileReaderStats file_reader_stats;
io::FileCacheStatistics file_cache_stats;
auto io_ctx = make_io_context(&file_reader_stats, &file_cache_stats);
ShardedKVCache cache(1);
doris::format::iceberg::IcebergTableReader reader;
init_iceberg_reader(&reader, projected_columns, &scan_params, io_ctx, &state, &profile);
auto split_options = build_split_options(file_path);
split_options.cache = &cache;
split_options.current_range.__set_table_format_params(
make_iceberg_table_format_desc(file_path, {}));
ASSERT_TRUE(reader.prepare_split(split_options).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
const auto& outer_result =
assert_cast<const ColumnStruct&>(expect_not_null_table_column(block, 0));
const auto& inner_result = assert_cast<const ColumnStruct&>(
expect_not_null_nullable_nested_column(outer_result.get_column(0)));
const auto& leaf_result = assert_cast<const ColumnInt32&>(
expect_not_null_nullable_nested_column(inner_result.get_column(0)));
ASSERT_EQ(leaf_result.size(), 1);
EXPECT_EQ(leaf_result.get_element(0), 42);
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, ParquetUsesUnprojectedSiblingIdToRetainNullableWrapper) {
const auto test_dir = std::filesystem::temp_directory_path() /
"doris_iceberg_unprojected_sibling_id_wrapper_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_nullable_idless_struct_with_sibling_id_parquet_file(file_path);
const auto int_type = std::make_shared<DataTypeInt32>();
auto projected_a = make_table_column(1, "a", int_type);
projected_a.initial_default_value = "7";
auto projected_struct_type =
std::make_shared<DataTypeStruct>(DataTypes {int_type}, Strings {"a"});
auto projected_s = make_table_column(10, "s", projected_struct_type);
projected_s.children = {projected_a};
std::vector<ColumnDefinition> projected_columns = {projected_s};
auto schema_a =
external_schema_field("a", 1, {}, "7", external_primitive_type(TPrimitiveType::INT));
auto schema_b = external_schema_field("b", 2, {}, std::nullopt,
external_primitive_type(TPrimitiveType::INT));
schema::external::TStructField struct_children;
struct_children.__set_fields({schema_a, schema_b});
auto schema_s = external_schema_field("s", 10, {}, std::nullopt,
external_primitive_type(TPrimitiveType::STRUCT));
schema_s.field_ptr->nestedField.__set_struct_field(std::move(struct_children));
schema_s.field_ptr->__isset.nestedField = true;
RuntimeProfile profile("test_profile");
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto scan_params = make_local_parquet_scan_params();
scan_params.__set_current_schema_id(100);
scan_params.__set_history_schema_info({external_schema(100, {schema_s})});
io::FileReaderStats file_reader_stats;
io::FileCacheStatistics file_cache_stats;
auto io_ctx = make_io_context(&file_reader_stats, &file_cache_stats);
ShardedKVCache cache(1);
doris::format::iceberg::IcebergTableReader reader;
init_iceberg_reader(&reader, projected_columns, &scan_params, io_ctx, &state, &profile);
auto split_options = build_split_options(file_path);
split_options.cache = &cache;
split_options.current_range.__set_table_format_params(
make_iceberg_table_format_desc(file_path, {}));
ASSERT_TRUE(reader.prepare_split(split_options).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
const auto result = block.get_by_position(0).column->convert_to_full_column_if_const();
const auto& nullable_s = assert_cast<const ColumnNullable&>(*result);
ASSERT_EQ(nullable_s.size(), 2);
EXPECT_TRUE(nullable_s.is_null_at(0));
EXPECT_FALSE(nullable_s.is_null_at(1));
const auto& struct_s = assert_cast<const ColumnStruct&>(nullable_s.get_nested_column());
const auto& nullable_a = assert_cast<const ColumnNullable&>(struct_s.get_column(0));
EXPECT_FALSE(nullable_a.is_null_at(1));
const auto& values = assert_cast<const ColumnInt32&>(nullable_a.get_nested_column());
EXPECT_EQ(values.get_element(1), 7);
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, ReusedRootNameReadsNewFieldInitialDefault) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_iceberg_reused_root_name_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_single_int_parquet_file(file_path, "b", {10}, 1);
auto renamed_b = external_schema_field("renamed_b", 1, {"b"}, std::nullopt,
external_primitive_type(TPrimitiveType::INT));
renamed_b.field_ptr->__set_name_mapping_is_authoritative(true);
auto current_b =
external_schema_field("b", 2, {}, "7", external_primitive_type(TPrimitiveType::INT));
current_b.field_ptr->__set_name_mapping({});
current_b.field_ptr->__set_name_mapping_is_authoritative(true);
auto scan_params = make_local_parquet_scan_params();
scan_params.__set_current_schema_id(100);
scan_params.__set_history_schema_info({external_schema(100, {renamed_b, current_b})});
auto projected_b = make_table_column(-1, "b", std::make_shared<DataTypeInt32>());
ProjectedColumnBuildContext context {.scan_params = &scan_params};
TFileScanSlotInfo slot_info;
TableReader annotation_reader;
ASSERT_TRUE(
annotation_reader.annotate_projected_column(slot_info, &context, &projected_b).ok());
std::vector<ColumnDefinition> projected_columns = {projected_b};
RuntimeProfile profile("test_profile");
RuntimeState state {TQueryOptions(), TQueryGlobals()};
io::FileReaderStats file_reader_stats;
io::FileCacheStatistics file_cache_stats;
auto io_ctx = make_io_context(&file_reader_stats, &file_cache_stats);
ShardedKVCache cache(1);
doris::format::iceberg::IcebergTableReader reader;
init_iceberg_reader(&reader, projected_columns, &scan_params, io_ctx, &state, &profile);
auto split_options = build_split_options(file_path);
split_options.cache = &cache;
split_options.current_range.__set_table_format_params(
make_iceberg_table_format_desc(file_path, {}));
ASSERT_TRUE(reader.prepare_split(split_options).ok());
EXPECT_EQ(read_iceberg_ids(&reader, projected_columns), std::vector<int32_t>({7}));
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, LegacyPlanRetainsOrderedRootNameAndAliasLookupWithAllFieldIds) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_iceberg_legacy_reused_root_name_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_two_int_parquet_file(file_path, "renamed_b", {10}, 1, "b", {20}, 2);
auto renamed_b = external_schema_field("renamed_b", 1, {"b"}, std::nullopt,
external_primitive_type(TPrimitiveType::INT));
renamed_b.field_ptr->__set_name_mapping_is_authoritative(true);
auto current_b = external_schema_field("b", 2, {}, std::nullopt,
external_primitive_type(TPrimitiveType::INT));
current_b.field_ptr->__set_name_mapping({});
current_b.field_ptr->__set_name_mapping_is_authoritative(true);
TFileScanRangeParams old_fe_scan_params;
old_fe_scan_params.__set_file_type(TFileType::FILE_LOCAL);
old_fe_scan_params.__set_format_type(TFileFormatType::FORMAT_PARQUET);
old_fe_scan_params.__set_current_schema_id(100);
old_fe_scan_params.__set_history_schema_info({external_schema(100, {renamed_b, current_b})});
auto projected_b = make_table_column(-1, "b", std::make_shared<DataTypeInt32>());
ProjectedColumnBuildContext context {.scan_params = &old_fe_scan_params};
TFileScanSlotInfo slot_info;
TableReader annotation_reader;
ASSERT_TRUE(
annotation_reader.annotate_projected_column(slot_info, &context, &projected_b).ok());
ASSERT_EQ(projected_b.get_identifier_field_id(), 1);
std::vector<ColumnDefinition> projected_columns = {projected_b};
RuntimeProfile profile("test_profile");
RuntimeState state {TQueryOptions(), TQueryGlobals()};
io::FileReaderStats file_reader_stats;
io::FileCacheStatistics file_cache_stats;
auto io_ctx = make_io_context(&file_reader_stats, &file_cache_stats);
ShardedKVCache cache(1);
doris::format::iceberg::IcebergTableReader reader;
init_iceberg_reader(&reader, projected_columns, &old_fe_scan_params, io_ctx, &state, &profile);
auto split_options = build_split_options(file_path);
split_options.cache = &cache;
split_options.current_range.__set_table_format_params(
make_iceberg_table_format_desc(file_path, {}));
ASSERT_TRUE(reader.prepare_split(split_options).ok());
EXPECT_EQ(read_iceberg_ids(&reader, projected_columns), std::vector<int32_t>({10}));
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, ReusedNestedNameReadsNewFieldInitialDefault) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_iceberg_reused_nested_name_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_nullable_renamed_struct_child_parquet_file(file_path);
auto renamed_b = external_schema_field("renamed_b", 1, {"b"}, std::nullopt,
external_primitive_type(TPrimitiveType::INT));
renamed_b.field_ptr->__set_name_mapping_is_authoritative(true);
auto current_b =
external_schema_field("b", 2, {}, "7", external_primitive_type(TPrimitiveType::INT));
current_b.field_ptr->__set_name_mapping({});
current_b.field_ptr->__set_name_mapping_is_authoritative(true);
schema::external::TStructField struct_children;
struct_children.__set_fields({renamed_b, current_b});
auto schema_s = external_schema_field("s", 10, {}, std::nullopt,
external_primitive_type(TPrimitiveType::STRUCT));
schema_s.field_ptr->nestedField.__set_struct_field(std::move(struct_children));
schema_s.field_ptr->__isset.nestedField = true;
auto scan_params = make_local_parquet_scan_params();
scan_params.__set_current_schema_id(100);
scan_params.__set_history_schema_info({external_schema(100, {schema_s})});
const auto int_type = std::make_shared<DataTypeInt32>();
auto struct_type = std::make_shared<DataTypeStruct>(DataTypes {int_type}, Strings {"b"});
auto projected_s = make_table_column(-1, "s", struct_type);
ProjectedColumnBuildContext context {.scan_params = &scan_params};
TFileScanSlotInfo slot_info;
TableReader annotation_reader;
ASSERT_TRUE(
annotation_reader.annotate_projected_column(slot_info, &context, &projected_s).ok());
ASSERT_TRUE(context.schema_column.has_value());
ASSERT_TRUE(AccessPathParser::build_nested_children(
&projected_s,
std::vector<TColumnAccessPath> {nested_data_access_path({"s", "b"})},
&*context.schema_column)
.ok());
std::vector<ColumnDefinition> projected_columns = {projected_s};
RuntimeProfile profile("test_profile");
RuntimeState state {TQueryOptions(), TQueryGlobals()};
io::FileReaderStats file_reader_stats;
io::FileCacheStatistics file_cache_stats;
auto io_ctx = make_io_context(&file_reader_stats, &file_cache_stats);
ShardedKVCache cache(1);
doris::format::iceberg::IcebergTableReader reader;
init_iceberg_reader(&reader, projected_columns, &scan_params, io_ctx, &state, &profile);
auto split_options = build_split_options(file_path);
split_options.cache = &cache;
split_options.current_range.__set_table_format_params(
make_iceberg_table_format_desc(file_path, {}));
ASSERT_TRUE(reader.prepare_split(split_options).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
const auto result = block.get_by_position(0).column->convert_to_full_column_if_const();
const auto& nullable_s = assert_cast<const ColumnNullable&>(*result);
ASSERT_EQ(nullable_s.size(), 2);
EXPECT_TRUE(nullable_s.is_null_at(0));
EXPECT_FALSE(nullable_s.is_null_at(1));
const auto& struct_s = assert_cast<const ColumnStruct&>(nullable_s.get_nested_column());
const auto& nullable_b = assert_cast<const ColumnNullable&>(struct_s.get_column(0));
EXPECT_FALSE(nullable_b.is_null_at(1));
const auto& values = assert_cast<const ColumnInt32&>(nullable_b.get_nested_column());
EXPECT_EQ(values.get_element(1), 7);
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, LegacyPlanUsesNestedNameMappingForMixedFieldIds) {
const auto test_dir = std::filesystem::temp_directory_path() /
"doris_iceberg_legacy_nested_name_mapping_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_nullable_idless_struct_with_sibling_id_parquet_file(file_path);
const auto int_type = std::make_shared<DataTypeInt32>();
auto projected_a = make_table_column(1, "a", int_type);
projected_a.name_mapping = {"b"};
projected_a.has_name_mapping = true;
auto projected_struct_type =
std::make_shared<DataTypeStruct>(DataTypes {int_type}, Strings {"a"});
auto projected_s = make_table_column(10, "s", projected_struct_type);
projected_s.children = {projected_a};
std::vector<ColumnDefinition> projected_columns = {projected_s};
RuntimeProfile profile("test_profile");
RuntimeState state {TQueryOptions(), TQueryGlobals()};
TFileScanRangeParams old_fe_scan_params;
old_fe_scan_params.__set_file_type(TFileType::FILE_LOCAL);
old_fe_scan_params.__set_format_type(TFileFormatType::FORMAT_PARQUET);
io::FileReaderStats file_reader_stats;
io::FileCacheStatistics file_cache_stats;
auto io_ctx = make_io_context(&file_reader_stats, &file_cache_stats);
ShardedKVCache cache(1);
doris::format::iceberg::IcebergTableReader reader;
init_iceberg_reader(&reader, projected_columns, &old_fe_scan_params, io_ctx, &state, &profile);
auto split_options = build_split_options(file_path);
split_options.cache = &cache;
split_options.current_range.__set_table_format_params(
make_iceberg_table_format_desc(file_path, {}));
ASSERT_TRUE(reader.prepare_split(split_options).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
const auto result = block.get_by_position(0).column->convert_to_full_column_if_const();
const auto& nullable_s = assert_cast<const ColumnNullable&>(*result);
ASSERT_EQ(nullable_s.size(), 2);
EXPECT_TRUE(nullable_s.is_null_at(0));
EXPECT_FALSE(nullable_s.is_null_at(1));
const auto& struct_s = assert_cast<const ColumnStruct&>(nullable_s.get_nested_column());
const auto& nullable_a = assert_cast<const ColumnNullable&>(struct_s.get_column(0));
EXPECT_FALSE(nullable_a.is_null_at(1));
const auto& values = assert_cast<const ColumnInt32&>(nullable_a.get_nested_column());
EXPECT_EQ(values.get_element(1), 42);
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, IcebergEqualityDeletePrefersExistingFieldIdInMixedSchema) {
const auto test_dir = std::filesystem::temp_directory_path() /
"doris_iceberg_equality_delete_stale_field_id_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
const auto delete_file_path = (test_dir / "equality-delete.parquet").string();
// Iceberg's existential hasIds contract makes the physical field carrying ID 0 authoritative;
// an ID-less alias cannot switch a mixed schema back to name projection.
write_two_int_parquet_file(file_path, "legacy_id", {1, 2, 3}, std::nullopt, "stale_key",
{100, 200, 300}, 0);
write_iceberg_equality_delete_parquet_file(delete_file_path, 0, 2, "current_id");
std::vector<ColumnDefinition> projected_columns;
auto id_column = make_table_column(0, "current_id", std::make_shared<DataTypeInt32>());
id_column.name_mapping.push_back("legacy_id");
projected_columns.push_back(std::move(id_column));
RuntimeProfile profile("test_profile");
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto scan_params = make_local_parquet_scan_params();
io::FileReaderStats file_reader_stats;
io::FileCacheStatistics file_cache_stats;
auto io_ctx = make_io_context(&file_reader_stats, &file_cache_stats);
ShardedKVCache cache(1);
doris::format::iceberg::IcebergTableReader reader;
init_iceberg_reader(&reader, projected_columns, &scan_params, io_ctx, &state, &profile);
auto split_options = build_split_options(file_path);
split_options.cache = &cache;
split_options.current_range.__set_table_format_params(make_iceberg_table_format_desc(
file_path, {make_iceberg_equality_delete_file(delete_file_path, {0})}));
ASSERT_TRUE(reader.prepare_split(split_options).ok());
EXPECT_EQ(read_iceberg_ids(&reader, projected_columns), std::vector<int32_t>({100, 200, 300}));
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, IcebergEqualityDeleteUsesUnprojectedTableSchemaNameMapping) {
const auto test_dir = std::filesystem::temp_directory_path() /
"doris_iceberg_equality_delete_hidden_name_mapping_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
const auto delete_file_path = (test_dir / "equality-delete.parquet").string();
write_two_int_parquet_file(file_path, "LEGACY_ID", {1, 2, 3}, std::nullopt, "data",
{10, 20, 30}, std::nullopt);
write_iceberg_equality_delete_parquet_file(delete_file_path, 0, 2, "current_id");
// Model SELECT data: the equality key is deliberately absent from the query projection.
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(1, "data", std::make_shared<DataTypeInt32>()));
RuntimeProfile profile("test_profile");
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto scan_params = make_local_parquet_scan_params();
scan_params.__set_current_schema_id(100);
scan_params.__set_history_schema_info(
{external_schema(100, {external_schema_field("current_id", 0, {"legacy_id"}),
external_schema_field("data", 1)})});
io::FileReaderStats file_reader_stats;
io::FileCacheStatistics file_cache_stats;
auto io_ctx = make_io_context(&file_reader_stats, &file_cache_stats);
ShardedKVCache cache(1);
doris::format::iceberg::IcebergTableReader reader;
init_iceberg_reader(&reader, projected_columns, &scan_params, io_ctx, &state, &profile);
auto split_options = build_split_options(file_path);
split_options.cache = &cache;
split_options.current_range.__set_table_format_params(make_iceberg_table_format_desc(
file_path, {make_iceberg_equality_delete_file(delete_file_path, {0})}));
ASSERT_TRUE(reader.prepare_split(split_options).ok());
EXPECT_EQ(read_iceberg_ids(&reader, projected_columns), std::vector<int32_t>({10, 30}));
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, IcebergEqualityDeleteFileIsReusedAcrossSplits) {
const auto test_dir = std::filesystem::temp_directory_path() /
"doris_iceberg_equality_delete_split_cache_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto first_file_path = (test_dir / "first.parquet").string();
const auto second_file_path = (test_dir / "second.parquet").string();
const auto delete_file_path = (test_dir / "equality-delete.parquet").string();
write_single_int_parquet_file(first_file_path, "id", {1, 2, 3}, 0);
write_single_int_parquet_file(second_file_path, "id", {1, 2, 3}, 0);
write_iceberg_equality_delete_parquet_file(delete_file_path, 0, 2);
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
RuntimeProfile profile("test_profile");
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto scan_params = make_local_parquet_scan_params();
io::FileReaderStats file_reader_stats;
io::FileCacheStatistics file_cache_stats;
auto io_ctx = make_io_context(&file_reader_stats, &file_cache_stats);
ShardedKVCache cache(1);
doris::format::iceberg::IcebergTableReader reader;
init_iceberg_reader(&reader, projected_columns, &scan_params, io_ctx, &state, &profile);
auto first_split = build_split_options(first_file_path);
first_split.cache = &cache;
first_split.current_range.__set_table_format_params(make_iceberg_table_format_desc(
first_file_path, {make_iceberg_equality_delete_file(delete_file_path, {0})}));
ASSERT_TRUE(reader.prepare_split(first_split).ok());
EXPECT_EQ(read_iceberg_ids(&reader, projected_columns), std::vector<int32_t>({1, 3}));
// Removing the source after the first split proves that the second split consumes the parsed
// delete block from SplitReadOptions.cache instead of reopening the delete file.
ASSERT_TRUE(std::filesystem::remove(delete_file_path));
auto second_split = build_split_options(second_file_path);
second_split.cache = &cache;
second_split.current_range.__set_table_format_params(make_iceberg_table_format_desc(
second_file_path, {make_iceberg_equality_delete_file(delete_file_path, {0})}));
ASSERT_TRUE(reader.prepare_split(second_split).ok());
EXPECT_EQ(read_iceberg_ids(&reader, projected_columns), std::vector<int32_t>({1, 3}));
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, IcebergEqualityDeleteCacheIsScopedByFileSystem) {
const auto test_dir = std::filesystem::temp_directory_path() /
"doris_iceberg_equality_delete_filesystem_cache_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto first_file_path = (test_dir / "first.parquet").string();
const auto second_file_path = (test_dir / "second.parquet").string();
const auto delete_file_path = (test_dir / "equality-delete.parquet").string();
write_single_int_parquet_file(first_file_path, "id", {1, 2, 3}, 0);
write_single_int_parquet_file(second_file_path, "id", {1, 2, 3}, 0);
write_iceberg_equality_delete_parquet_file(delete_file_path, 0, 2);
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
RuntimeProfile profile("test_profile");
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto scan_params = make_local_parquet_scan_params();
io::FileReaderStats file_reader_stats;
io::FileCacheStatistics file_cache_stats;
auto io_ctx = make_io_context(&file_reader_stats, &file_cache_stats);
ShardedKVCache cache(1);
doris::format::iceberg::IcebergTableReader reader;
init_iceberg_reader(&reader, projected_columns, &scan_params, io_ctx, &state, &profile);
auto first_split = build_split_options(first_file_path);
first_split.cache = &cache;
first_split.current_range.__set_fs_name("filesystem-a");
first_split.current_range.__set_table_format_params(make_iceberg_table_format_desc(
first_file_path, {make_iceberg_equality_delete_file(delete_file_path, {0})}));
ASSERT_TRUE(reader.prepare_split(first_split).ok());
EXPECT_EQ(read_iceberg_ids(&reader, projected_columns), std::vector<int32_t>({1, 3}));
// The same descriptor path resolves to different content in another filesystem namespace.
write_iceberg_equality_delete_parquet_file(delete_file_path, 0, 3);
auto second_split = build_split_options(second_file_path);
second_split.cache = &cache;
second_split.current_range.__set_fs_name("filesystem-b");
second_split.current_range.__set_table_format_params(make_iceberg_table_format_desc(
second_file_path, {make_iceberg_equality_delete_file(delete_file_path, {0})}));
ASSERT_TRUE(reader.prepare_split(second_split).ok());
EXPECT_EQ(read_iceberg_ids(&reader, projected_columns), std::vector<int32_t>({1, 2}));
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, IcebergPositionDeleteOnlyMatchesOriginalDataFilePath) {
const auto test_dir = std::filesystem::temp_directory_path() /
"doris_iceberg_position_delete_path_match_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
const auto other_file_path = (test_dir / "other.parquet").string();
const auto delete_file_path = (test_dir / "position-delete.parquet").string();
write_int_pair_parquet_file(file_path, {1, 2, 3}, {10, 20, 30}, {"one", "two", "three"});
write_position_delete_parquet_file(delete_file_path, {other_file_path, file_path}, {0, 1});
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
RuntimeProfile profile("test_profile");
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto scan_params = make_local_parquet_scan_params();
io::FileReaderStats file_reader_stats;
io::FileCacheStatistics file_cache_stats;
auto io_ctx = make_io_context(&file_reader_stats, &file_cache_stats);
ShardedKVCache cache(1);
doris::format::iceberg::IcebergTableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = &scan_params,
.io_ctx = io_ctx,
.runtime_state = &state,
.scanner_profile = &profile,
})
.ok());
auto split_options = build_split_options(file_path);
split_options.cache = &cache;
split_options.current_range.__set_table_format_params(make_iceberg_table_format_desc(
file_path, {make_iceberg_position_delete_file(delete_file_path)}));
ASSERT_TRUE(reader.prepare_split(split_options).ok());
EXPECT_EQ(read_iceberg_ids(&reader, projected_columns), std::vector<int32_t>({1, 3}));
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, IcebergRowLineageRemainsFileLocalAfterDeleteFiltering) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_iceberg_row_lineage_delete_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
const auto delete_file_path = (test_dir / "position-delete.parquet").string();
write_int_pair_parquet_file(file_path, {1, 2, 3}, {10, 20, 30}, {"one", "two", "three"});
write_position_delete_parquet_file(delete_file_path, {file_path}, {1});
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_iceberg_row_lineage_row_id_column());
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
RuntimeProfile profile("test_profile");
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto scan_params = make_local_parquet_scan_params();
io::FileReaderStats file_reader_stats;
io::FileCacheStatistics file_cache_stats;
auto io_ctx = make_io_context(&file_reader_stats, &file_cache_stats);
ShardedKVCache cache(1);
doris::format::iceberg::IcebergTableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = &scan_params,
.io_ctx = io_ctx,
.runtime_state = &state,
.scanner_profile = &profile,
})
.ok());
auto split_options = build_split_options(file_path);
split_options.cache = &cache;
TTableFormatFileDesc table_format_params = make_iceberg_table_format_desc(
file_path, {make_iceberg_position_delete_file(delete_file_path)});
table_format_params.iceberg_params.__set_first_row_id(1000);
split_options.current_range.__set_table_format_params(table_format_params);
ASSERT_TRUE(reader.prepare_split(split_options).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
ASSERT_EQ(block.rows(), 2);
expect_nullable_int64_column_values(*block.get_by_position(0).column, {1000, 1002});
const auto& id_column = assert_cast<const ColumnInt32&>(expect_not_null_table_column(block, 1));
EXPECT_EQ(id_column.get_element(0), 1);
EXPECT_EQ(id_column.get_element(1), 3);
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, IcebergTableReaderAppliesPositionDeleteFile) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_iceberg_position_delete_file_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
const auto delete_file_path = (test_dir / "position-delete.parquet").string();
write_int_pair_parquet_file(file_path, {1, 2, 3, 4, 5}, {10, 20, 30, 40, 50},
{"one", "two", "three", "four", "five"});
write_position_delete_parquet_file(delete_file_path, {file_path, file_path}, {1, 3});
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
RuntimeProfile profile("test_profile");
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto scan_params = make_local_parquet_scan_params();
io::FileReaderStats file_reader_stats;
io::FileCacheStatistics file_cache_stats;
auto io_ctx = make_io_context(&file_reader_stats, &file_cache_stats);
ShardedKVCache cache(1);
doris::format::iceberg::IcebergTableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = &scan_params,
.io_ctx = io_ctx,
.runtime_state = &state,
.scanner_profile = &profile,
})
.ok());
auto split_options = build_split_options(file_path);
split_options.cache = &cache;
split_options.current_range.__set_table_format_params(make_iceberg_table_format_desc(
file_path, {make_iceberg_position_delete_file(delete_file_path)}));
ASSERT_TRUE(reader.prepare_split(split_options).ok());
EXPECT_EQ(read_iceberg_ids(&reader, projected_columns), std::vector<int32_t>({1, 3, 5}));
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, IcebergPositionDeleteHonorsPositionBounds) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_iceberg_position_delete_bounds_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
const auto delete_file_path = (test_dir / "position-delete.parquet").string();
write_int_pair_parquet_file(file_path, {1, 2, 3, 4, 5}, {10, 20, 30, 40, 50},
{"one", "two", "three", "four", "five"});
write_position_delete_parquet_file(delete_file_path, {file_path, file_path, file_path},
{0, 2, 4});
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
RuntimeProfile profile("test_profile");
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto scan_params = make_local_parquet_scan_params();
io::FileReaderStats file_reader_stats;
io::FileCacheStatistics file_cache_stats;
auto io_ctx = make_io_context(&file_reader_stats, &file_cache_stats);
ShardedKVCache cache(1);
doris::format::iceberg::IcebergTableReader reader;
init_iceberg_reader(&reader, projected_columns, &scan_params, io_ctx, &state, &profile);
auto delete_file = make_iceberg_position_delete_file(delete_file_path);
delete_file.__set_position_lower_bound(1);
delete_file.__set_position_upper_bound(3);
auto split_options = build_split_options(file_path);
split_options.cache = &cache;
split_options.current_range.__set_table_format_params(
make_iceberg_table_format_desc(file_path, {delete_file}));
ASSERT_TRUE(reader.prepare_split(split_options).ok());
EXPECT_EQ(read_iceberg_ids(&reader, projected_columns), std::vector<int32_t>({1, 2, 4, 5}));
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, IcebergPositionDeleteFileIsReusedAcrossSplits) {
const auto test_dir = std::filesystem::temp_directory_path() /
"doris_iceberg_position_delete_split_cache_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto first_file_path = (test_dir / "first.parquet").string();
const auto second_file_path = (test_dir / "second.parquet").string();
const auto delete_file_path = (test_dir / "position-delete.parquet").string();
write_single_int_parquet_file(first_file_path, "id", {1, 2, 3}, 0);
write_single_int_parquet_file(second_file_path, "id", {1, 2, 3}, 0);
write_position_delete_parquet_file(delete_file_path, {first_file_path, second_file_path},
{1, 0});
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
RuntimeProfile profile("test_profile");
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto scan_params = make_local_parquet_scan_params();
io::FileReaderStats file_reader_stats;
io::FileCacheStatistics file_cache_stats;
auto io_ctx = make_io_context(&file_reader_stats, &file_cache_stats);
ShardedKVCache cache(1);
doris::format::iceberg::IcebergTableReader reader;
init_iceberg_reader(&reader, projected_columns, &scan_params, io_ctx, &state, &profile);
auto first_split = build_split_options(first_file_path);
first_split.cache = &cache;
first_split.current_range.__set_table_format_params(make_iceberg_table_format_desc(
first_file_path, {make_iceberg_position_delete_file(delete_file_path)}));
ASSERT_TRUE(reader.prepare_split(first_split).ok());
EXPECT_EQ(read_iceberg_ids(&reader, projected_columns), std::vector<int32_t>({1, 3}));
// The cached delete file contains entries for every referenced data file, so another split can
// select its own positions without rescanning the shared delete file.
ASSERT_TRUE(std::filesystem::remove(delete_file_path));
auto second_split = build_split_options(second_file_path);
second_split.cache = &cache;
second_split.current_range.__set_table_format_params(make_iceberg_table_format_desc(
second_file_path, {make_iceberg_position_delete_file(delete_file_path)}));
ASSERT_TRUE(reader.prepare_split(second_split).ok());
EXPECT_EQ(read_iceberg_ids(&reader, projected_columns), std::vector<int32_t>({2, 3}));
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, IcebergPositionDeleteCacheIsScopedByFileSystem) {
const auto test_dir = std::filesystem::temp_directory_path() /
"doris_iceberg_position_delete_filesystem_cache_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto first_file_path = (test_dir / "first.parquet").string();
const auto second_file_path = (test_dir / "second.parquet").string();
const auto delete_file_path = (test_dir / "position-delete.parquet").string();
write_single_int_parquet_file(first_file_path, "id", {1, 2, 3}, 0);
write_single_int_parquet_file(second_file_path, "id", {1, 2, 3}, 0);
write_position_delete_parquet_file(delete_file_path, {first_file_path}, {1});
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
RuntimeProfile profile("test_profile");
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto scan_params = make_local_parquet_scan_params();
io::FileReaderStats file_reader_stats;
io::FileCacheStatistics file_cache_stats;
auto io_ctx = make_io_context(&file_reader_stats, &file_cache_stats);
ShardedKVCache cache(1);
doris::format::iceberg::IcebergTableReader reader;
init_iceberg_reader(&reader, projected_columns, &scan_params, io_ctx, &state, &profile);
auto first_split = build_split_options(first_file_path);
first_split.cache = &cache;
first_split.current_range.__set_fs_name("filesystem-a");
first_split.current_range.__set_table_format_params(make_iceberg_table_format_desc(
first_file_path, {make_iceberg_position_delete_file(delete_file_path)}));
ASSERT_TRUE(reader.prepare_split(first_split).ok());
EXPECT_EQ(read_iceberg_ids(&reader, projected_columns), std::vector<int32_t>({1, 3}));
write_position_delete_parquet_file(delete_file_path, {second_file_path}, {0});
auto second_split = build_split_options(second_file_path);
second_split.cache = &cache;
second_split.current_range.__set_fs_name("filesystem-b");
second_split.current_range.__set_table_format_params(make_iceberg_table_format_desc(
second_file_path, {make_iceberg_position_delete_file(delete_file_path)}));
ASSERT_TRUE(reader.prepare_split(second_split).ok());
EXPECT_EQ(read_iceberg_ids(&reader, projected_columns), std::vector<int32_t>({2, 3}));
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, IcebergTableReaderAppliesNullablePositionDeleteFileWithoutNulls) {
const auto test_dir = std::filesystem::temp_directory_path() /
"doris_iceberg_nullable_position_delete_file_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
const auto delete_file_path = (test_dir / "position-delete.parquet").string();
write_int_pair_parquet_file(file_path, {1, 2, 3, 4, 5}, {10, 20, 30, 40, 50},
{"one", "two", "three", "four", "five"});
write_nullable_position_delete_parquet_file(delete_file_path, {file_path, file_path},
{int64_t {1}, int64_t {3}});
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
RuntimeProfile profile("test_profile");
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto scan_params = make_local_parquet_scan_params();
io::FileReaderStats file_reader_stats;
io::FileCacheStatistics file_cache_stats;
auto io_ctx = make_io_context(&file_reader_stats, &file_cache_stats);
ShardedKVCache cache(1);
doris::format::iceberg::IcebergTableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = &scan_params,
.io_ctx = io_ctx,
.runtime_state = &state,
.scanner_profile = &profile,
})
.ok());
auto split_options = build_split_options(file_path);
split_options.cache = &cache;
split_options.current_range.__set_table_format_params(make_iceberg_table_format_desc(
file_path, {make_iceberg_position_delete_file(delete_file_path)}));
ASSERT_TRUE(reader.prepare_split(split_options).ok());
EXPECT_EQ(read_iceberg_ids(&reader, projected_columns), std::vector<int32_t>({1, 3, 5}));
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, IcebergTableReaderRejectsNullablePositionDeleteFileWithActualNulls) {
const auto test_dir = std::filesystem::temp_directory_path() /
"doris_iceberg_nullable_position_delete_actual_null_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
const auto delete_file_path = (test_dir / "position-delete.parquet").string();
write_int_pair_parquet_file(file_path, {1, 2, 3}, {10, 20, 30}, {"one", "two", "three"});
write_nullable_position_delete_parquet_file(delete_file_path, {file_path, std::nullopt},
{int64_t {1}, int64_t {2}});
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
RuntimeProfile profile("test_profile");
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto scan_params = make_local_parquet_scan_params();
io::FileReaderStats file_reader_stats;
io::FileCacheStatistics file_cache_stats;
auto io_ctx = make_io_context(&file_reader_stats, &file_cache_stats);
ShardedKVCache cache(1);
doris::format::iceberg::IcebergTableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = &scan_params,
.io_ctx = io_ctx,
.runtime_state = &state,
.scanner_profile = &profile,
})
.ok());
auto split_options = build_split_options(file_path);
split_options.cache = &cache;
split_options.current_range.__set_table_format_params(make_iceberg_table_format_desc(
file_path, {make_iceberg_position_delete_file(delete_file_path)}));
auto status = reader.prepare_split(split_options);
ASSERT_FALSE(status.ok());
EXPECT_NE(status.to_string().find("file_path contains null values"), std::string::npos)
<< status.to_string();
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, IcebergTableReaderRejectsNullablePositionDeletePosWithActualNulls) {
const auto test_dir = std::filesystem::temp_directory_path() /
"doris_iceberg_nullable_position_delete_pos_null_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
const auto delete_file_path = (test_dir / "position-delete.parquet").string();
write_int_pair_parquet_file(file_path, {1, 2, 3}, {10, 20, 30}, {"one", "two", "three"});
write_nullable_position_delete_parquet_file(delete_file_path, {file_path, file_path},
{int64_t {1}, std::nullopt});
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
RuntimeProfile profile("test_profile");
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto scan_params = make_local_parquet_scan_params();
io::FileReaderStats file_reader_stats;
io::FileCacheStatistics file_cache_stats;
auto io_ctx = make_io_context(&file_reader_stats, &file_cache_stats);
ShardedKVCache cache(1);
doris::format::iceberg::IcebergTableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = &scan_params,
.io_ctx = io_ctx,
.runtime_state = &state,
.scanner_profile = &profile,
})
.ok());
auto split_options = build_split_options(file_path);
split_options.cache = &cache;
split_options.current_range.__set_table_format_params(make_iceberg_table_format_desc(
file_path, {make_iceberg_position_delete_file(delete_file_path)}));
auto status = reader.prepare_split(split_options);
ASSERT_FALSE(status.ok());
EXPECT_NE(status.to_string().find("pos contains null values"), std::string::npos)
<< status.to_string();
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, IcebergTableReaderIgnoresPositionDeleteFilesWhenDeletionVectorPresent) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_iceberg_delete_files_merge_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
const auto dv_path = (test_dir / "delete-vector.bin").string();
const auto position_delete_path = (test_dir / "position-delete.parquet").string();
write_int_pair_parquet_file(file_path, {1, 2, 3, 4, 5}, {10, 20, 30, 40, 50},
{"one", "two", "three", "four", "five"});
const auto dv_size = write_iceberg_deletion_vector_file(dv_path, {0});
write_position_delete_parquet_file(position_delete_path, {file_path, file_path}, {3, 3});
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
RuntimeProfile profile("test_profile");
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto scan_params = make_local_parquet_scan_params();
io::FileReaderStats file_reader_stats;
io::FileCacheStatistics file_cache_stats;
auto io_ctx = make_io_context(&file_reader_stats, &file_cache_stats);
ShardedKVCache cache(1);
doris::format::iceberg::IcebergTableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = &scan_params,
.io_ctx = io_ctx,
.runtime_state = &state,
.scanner_profile = &profile,
})
.ok());
auto split_options = build_split_options(file_path);
split_options.cache = &cache;
split_options.current_range.__set_table_format_params(make_iceberg_table_format_desc(
file_path, {make_iceberg_deletion_vector(dv_path, 0, dv_size),
make_iceberg_position_delete_file(position_delete_path)}));
ASSERT_TRUE(reader.prepare_split(split_options).ok());
EXPECT_EQ(read_iceberg_ids(&reader, projected_columns), std::vector<int32_t>({2, 3, 4, 5}));
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(IcebergV2ReaderTest, RowPositionDeletePredicateColumnIsNotRepeatedAsOutputColumn) {
const auto row_position_column_id = ROW_POSITION_COLUMN_ID;
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_iceberg_row_lineage_row_id_column());
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
IcebergTableReaderScanRequestTestHelper reader;
ASSERT_TRUE(reader.init_for_scan_request_test(projected_columns).ok());
FileScanRequest request;
request.non_predicate_columns.push_back(field_projection(0));
request.local_positions.emplace(LocalColumnId(0), LocalIndex(0));
ASSERT_TRUE(reader.customize_request(&request).ok());
EXPECT_EQ(projection_ids(request.predicate_columns),
std::vector<int32_t>({row_position_column_id}));
EXPECT_EQ(projection_ids(request.non_predicate_columns), std::vector<int32_t>({0}));
ASSERT_TRUE(request.local_positions.contains(LocalColumnId(row_position_column_id)));
EXPECT_EQ(request.local_positions.at(LocalColumnId(row_position_column_id)).value(), 1);
ASSERT_TRUE(request.conjuncts.empty());
ASSERT_EQ(request.delete_conjuncts.size(), 1);
EXPECT_NE(request.delete_conjuncts[0], nullptr);
}
TEST(IcebergV2ReaderTest, DataFileIsMarkedImmutableForPageCache) {
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
IcebergTableReaderScanRequestTestHelper reader;
ASSERT_TRUE(reader.init_for_scan_request_test(std::move(projected_columns)).ok());
EXPECT_TRUE(reader.current_data_file_is_immutable());
}
} // namespace
} // namespace doris::format