This is an automated email from the ASF dual-hosted git repository.
Gabriel39 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new d8fd23f7f38 [fix](iceberg) Allow optional position delete fields
without nulls (#65401)
d8fd23f7f38 is described below
commit d8fd23f7f3868a2031f0c38aa8231e7dddcb132d
Author: Gabriel <[email protected]>
AuthorDate: Thu Jul 9 22:35:42 2026 +0800
[fix](iceberg) Allow optional position delete fields without nulls (#65401)
Relax Iceberg position delete Parquet reads so Doris can consume delete
files where `file_path` or `pos` are declared optional in Parquet
metadata but contain no actual null values.
The reader now materializes position delete columns as nullable,
validates that each batch has no nulls, then strips the nullable wrapper
before building delete ranges. Actual null values in `file_path` or
`pos` still fail with a corruption error.
This covers the legacy Iceberg Parquet reader, the shared delete-file
helper used by Iceberg writes, and the format_v2 position delete
collector.
## Problem
Some writers, including DuckLake/DuckDB-produced data seen in the field,
can produce Iceberg position delete files whose required Iceberg fields
are encoded as optional Parquet fields. Doris previously rejected these
files before checking whether the data actually contained nulls.
---
.../table/iceberg_delete_file_reader_helper.cpp | 40 +++--
be/src/format/table/iceberg_reader.cpp | 25 +--
be/src/format/table/iceberg_reader_mixin.h | 23 ++-
be/src/format_v2/table/iceberg_reader.cpp | 18 ++-
.../iceberg_delete_file_reader_helper_test.cpp | 121 ++++++++++++++
be/test/format_v2/table/iceberg_reader_test.cpp | 179 +++++++++++++++++++++
6 files changed, 378 insertions(+), 28 deletions(-)
diff --git a/be/src/format/table/iceberg_delete_file_reader_helper.cpp
b/be/src/format/table/iceberg_delete_file_reader_helper.cpp
index 21bf9865868..50738587d8b 100644
--- a/be/src/format/table/iceberg_delete_file_reader_helper.cpp
+++ b/be/src/format/table/iceberg_delete_file_reader_helper.cpp
@@ -30,8 +30,10 @@
#include "core/block/block.h"
#include "core/block/column_with_type_and_name.h"
#include "core/column/column_dictionary.h"
+#include "core/column/column_nullable.h"
#include "core/column/column_string.h"
#include "core/column/column_vector.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 "exec/common/endian.h"
@@ -90,9 +92,26 @@ Status visit_position_delete_block(const Block& block,
size_t read_rows,
return Status::InternalError("Position delete block is missing
required columns");
}
- const auto* pos_column =
- assert_cast<const
ColumnInt64*>(block.get_by_position(pos_it->second).column.get());
- const auto* path_column =
block.get_by_position(path_it->second).column.get();
+ ColumnPtr path_column_ptr = block.get_by_position(path_it->second).column;
+ ColumnPtr pos_column_ptr = block.get_by_position(pos_it->second).column;
+ if (const auto* nullable_col =
check_and_get_column<ColumnNullable>(*path_column_ptr);
+ nullable_col != nullptr) {
+ if (nullable_col->has_null(0, read_rows)) {
+ return Status::Corruption(
+ "Iceberg position delete column file_path contains null
values");
+ }
+ path_column_ptr = remove_nullable(path_column_ptr);
+ }
+ if (const auto* nullable_col =
check_and_get_column<ColumnNullable>(*pos_column_ptr);
+ nullable_col != nullptr) {
+ if (nullable_col->has_null(0, read_rows)) {
+ return Status::Corruption("Iceberg position delete column pos
contains null values");
+ }
+ pos_column_ptr = remove_nullable(pos_column_ptr);
+ }
+
+ const auto* pos_column = assert_cast<const
ColumnInt64*>(pos_column_ptr.get());
+ const auto* path_column = path_column_ptr.get();
if (const auto* string_column =
check_and_get_column<ColumnString>(path_column);
string_column != nullptr) {
@@ -252,16 +271,15 @@ Status read_iceberg_position_delete_file(const
TIcebergDeleteFileDesc& delete_fi
while (!eof) {
Block block;
if (dictionary_coded) {
- block.insert(ColumnWithTypeAndName(ColumnDictI32::create(),
-
std::make_shared<DataTypeString>(),
- ICEBERG_FILE_PATH));
+ block.insert(ColumnWithTypeAndName(
+ ColumnNullable::create(ColumnDictI32::create(),
ColumnUInt8::create()),
+ make_nullable(std::make_shared<DataTypeString>()),
ICEBERG_FILE_PATH));
} else {
- block.insert(ColumnWithTypeAndName(ColumnString::create(),
-
std::make_shared<DataTypeString>(),
- ICEBERG_FILE_PATH));
+ block.insert(ColumnWithTypeAndName(
+ make_nullable(std::make_shared<DataTypeString>()),
ICEBERG_FILE_PATH));
}
- block.insert(ColumnWithTypeAndName(ColumnInt64::create(),
-
std::make_shared<DataTypeInt64>(), ICEBERG_ROW_POS));
+
block.insert(ColumnWithTypeAndName(make_nullable(std::make_shared<DataTypeInt64>()),
+ ICEBERG_ROW_POS));
size_t read_rows = 0;
RETURN_IF_ERROR(reader.get_next_block(&block, &read_rows, &eof));
RETURN_IF_ERROR(visit_position_delete_block(block, read_rows,
visitor));
diff --git a/be/src/format/table/iceberg_reader.cpp
b/be/src/format/table/iceberg_reader.cpp
index 6f6d0b9e9d5..110a10f74c0 100644
--- a/be/src/format/table/iceberg_reader.cpp
+++ b/be/src/format/table/iceberg_reader.cpp
@@ -37,9 +37,11 @@
#include "core/block/block.h"
#include "core/block/column_with_type_and_name.h"
#include "core/column/column.h"
+#include "core/column/column_nullable.h"
#include "core/column/column_string.h"
#include "core/column/column_vector.h"
#include "core/data_type/data_type_factory.hpp"
+#include "core/data_type/data_type_nullable.h"
#include "core/data_type/define_primitive_type.h"
#include "core/data_type/primitive_type.h"
#include "core/string_ref.h"
@@ -386,23 +388,26 @@ Status
IcebergParquetReader::_read_position_delete_file(const TFileRangeDesc* de
break;
}
}
- DataTypePtr data_type_file_path {new DataTypeString};
- DataTypePtr data_type_pos {new DataTypeInt64};
+ DataTypePtr data_type_file_path =
make_nullable(std::make_shared<DataTypeString>());
+ DataTypePtr data_type_pos =
make_nullable(std::make_shared<DataTypeInt64>());
bool eof = false;
while (!eof) {
- Block block = {dictionary_coded
- ? ColumnWithTypeAndName
{ColumnDictI32::create(),
- data_type_file_path,
ICEBERG_FILE_PATH}
- : ColumnWithTypeAndName {data_type_file_path,
ICEBERG_FILE_PATH},
-
- {data_type_pos, ICEBERG_ROW_POS}};
+ Block block = {
+ dictionary_coded
+ ? ColumnWithTypeAndName
{ColumnNullable::create(ColumnDictI32::create(),
+
ColumnUInt8::create()),
+ data_type_file_path,
ICEBERG_FILE_PATH}
+ : ColumnWithTypeAndName {data_type_file_path,
ICEBERG_FILE_PATH},
+
+ {data_type_pos, ICEBERG_ROW_POS}};
size_t read_rows = 0;
RETURN_IF_ERROR(parquet_delete_reader.get_next_block(&block,
&read_rows, &eof));
if (read_rows <= 0) {
break;
}
- _gen_position_delete_file_range(block, position_delete, read_rows,
dictionary_coded);
+ RETURN_IF_ERROR(_gen_position_delete_file_range(block,
position_delete, read_rows,
+ dictionary_coded));
}
return Status::OK();
};
@@ -650,7 +655,7 @@ Status IcebergOrcReader::_read_position_delete_file(const
TFileRangeDesc* delete
size_t read_rows = 0;
RETURN_IF_ERROR(orc_delete_reader.get_next_block(&block, &read_rows,
&eof));
- _gen_position_delete_file_range(block, position_delete, read_rows,
false);
+ RETURN_IF_ERROR(_gen_position_delete_file_range(block,
position_delete, read_rows, false));
}
return Status::OK();
}
diff --git a/be/src/format/table/iceberg_reader_mixin.h
b/be/src/format/table/iceberg_reader_mixin.h
index 2e9bd06b6fd..c0b2969678a 100644
--- a/be/src/format/table/iceberg_reader_mixin.h
+++ b/be/src/format/table/iceberg_reader_mixin.h
@@ -247,8 +247,9 @@ protected:
PositionDeleteRange _get_range(const ColumnString& file_path_column);
static void _sort_delete_rows(const std::vector<std::vector<int64_t>*>&
delete_rows_array,
int64_t num_delete_rows,
std::vector<int64_t>& result);
- void _gen_position_delete_file_range(Block& block, DeleteFile*
position_delete,
- size_t read_rows, bool
file_path_column_dictionary_coded);
+ Status _gen_position_delete_file_range(Block& block, DeleteFile*
position_delete,
+ size_t read_rows,
+ bool
file_path_column_dictionary_coded);
void _generate_equality_delete_block(Block* block,
const std::vector<std::string>&
equality_delete_col_names,
const std::vector<DataTypePtr>&
equality_delete_col_types);
@@ -776,7 +777,7 @@ void IcebergReaderMixin<BaseReader>::_sort_delete_rows(
}
template <typename BaseReader>
-void IcebergReaderMixin<BaseReader>::_gen_position_delete_file_range(
+Status IcebergReaderMixin<BaseReader>::_gen_position_delete_file_range(
Block& block, DeleteFile* position_delete, size_t read_rows,
bool file_path_column_dictionary_coded) {
SCOPED_TIMER(_iceberg_profile.parse_delete_file_time);
@@ -784,6 +785,21 @@ void
IcebergReaderMixin<BaseReader>::_gen_position_delete_file_range(
ColumnPtr path_column =
block.get_by_position(name_to_pos_map[ICEBERG_FILE_PATH]).column;
DCHECK_EQ(path_column->size(), read_rows);
ColumnPtr pos_column =
block.get_by_position(name_to_pos_map[ICEBERG_ROW_POS]).column;
+ if (const auto* nullable_col =
check_and_get_column<ColumnNullable>(*path_column);
+ nullable_col != nullptr) {
+ if (nullable_col->has_null(0, read_rows)) {
+ return Status::Corruption(
+ "Iceberg position delete column file_path contains null
values");
+ }
+ path_column = remove_nullable(path_column);
+ }
+ if (const auto* nullable_col =
check_and_get_column<ColumnNullable>(*pos_column);
+ nullable_col != nullptr) {
+ if (nullable_col->has_null(0, read_rows)) {
+ return Status::Corruption("Iceberg position delete column pos
contains null values");
+ }
+ pos_column = remove_nullable(pos_column);
+ }
using ColumnType = typename PrimitiveTypeTraits<TYPE_BIGINT>::ColumnType;
const int64_t* src_data = assert_cast<const
ColumnType&>(*pos_column).get_data().data();
PositionDeleteRange range;
@@ -810,6 +826,7 @@ void
IcebergReaderMixin<BaseReader>::_gen_position_delete_file_range(
int64_t* dest_position = &(*delete_rows)[origin_size];
memcpy(dest_position, cpy_start, cpy_count * sizeof(int64_t));
}
+ return Status::OK();
}
template <typename BaseReader>
diff --git a/be/src/format_v2/table/iceberg_reader.cpp
b/be/src/format_v2/table/iceberg_reader.cpp
index eb1172ed358..7e81b860536 100644
--- a/be/src/format_v2/table/iceberg_reader.cpp
+++ b/be/src/format_v2/table/iceberg_reader.cpp
@@ -150,10 +150,20 @@ Status
IcebergTableReader::PositionDeleteRowsCollector::collect(const Block& blo
if (read_rows == 0) {
return Status::OK();
}
- const auto& file_path_column = assert_cast<const ColumnString&>(
-
*remove_nullable((block.get_by_position(ICEBERG_FILE_PATH_BLOCK_POSITION).column)));
- const auto& pos_column = assert_cast<const ColumnInt64&>(
-
*remove_nullable(block.get_by_position(ICEBERG_ROW_POS_BLOCK_POSITION).column));
+ const auto& file_path_column_ptr =
+ block.get_by_position(ICEBERG_FILE_PATH_BLOCK_POSITION).column;
+ const auto& pos_column_ptr =
block.get_by_position(ICEBERG_ROW_POS_BLOCK_POSITION).column;
+ if (const auto* nullable_column =
check_and_get_column<ColumnNullable>(*file_path_column_ptr);
+ nullable_column != nullptr && nullable_column->has_null(0, read_rows))
{
+ return Status::Corruption("Iceberg position delete column file_path
contains null values");
+ }
+ if (const auto* nullable_column =
check_and_get_column<ColumnNullable>(*pos_column_ptr);
+ nullable_column != nullptr && nullable_column->has_null(0, read_rows))
{
+ return Status::Corruption("Iceberg position delete column pos contains
null values");
+ }
+ const auto& file_path_column =
+ assert_cast<const
ColumnString&>(*remove_nullable(file_path_column_ptr));
+ const auto& pos_column = assert_cast<const
ColumnInt64&>(*remove_nullable(pos_column_ptr));
for (size_t row = 0; row < read_rows; ++row) {
const auto file_path = file_path_column.get_data_at(row).to_string();
if (file_path == _data_file_path) {
diff --git
a/be/test/format/table/iceberg/iceberg_delete_file_reader_helper_test.cpp
b/be/test/format/table/iceberg/iceberg_delete_file_reader_helper_test.cpp
index 34d2e60f712..cb23fe9f036 100644
--- a/be/test/format/table/iceberg/iceberg_delete_file_reader_helper_test.cpp
+++ b/be/test/format/table/iceberg/iceberg_delete_file_reader_helper_test.cpp
@@ -17,13 +17,17 @@
#include "format/table/iceberg_delete_file_reader_helper.h"
+#include <arrow/api.h>
+#include <arrow/io/api.h>
#include <gen_cpp/PlanNodes_types.h>
#include <gen_cpp/Types_types.h>
#include <gtest/gtest.h>
+#include <parquet/arrow/writer.h>
#include <cstring>
#include <filesystem>
#include <fstream>
+#include <optional>
#include <string>
#include <thread>
#include <unordered_map>
@@ -99,6 +103,45 @@ int64_t write_iceberg_deletion_vector_file(const
std::string& file_path,
return static_cast<int64_t>(blob.size());
}
+std::string temp_parquet_path(const std::string& filename) {
+ return (std::filesystem::temp_directory_path() / filename).string();
+}
+
+void write_position_delete_parquet(const std::string& path,
+ const
std::vector<std::optional<std::string>>& file_paths,
+ const std::vector<std::optional<int64_t>>&
positions) {
+ ASSERT_EQ(file_paths.size(), positions.size());
+
+ arrow::StringBuilder path_builder;
+ arrow::Int64Builder pos_builder;
+ for (size_t i = 0; i < file_paths.size(); ++i) {
+ if (file_paths[i].has_value()) {
+ ASSERT_TRUE(path_builder.Append(*file_paths[i]).ok());
+ } else {
+ ASSERT_TRUE(path_builder.AppendNull().ok());
+ }
+ if (positions[i].has_value()) {
+ ASSERT_TRUE(pos_builder.Append(*positions[i]).ok());
+ } else {
+ ASSERT_TRUE(pos_builder.AppendNull().ok());
+ }
+ }
+
+ std::shared_ptr<arrow::Array> path_array;
+ std::shared_ptr<arrow::Array> pos_array;
+ ASSERT_TRUE(path_builder.Finish(&path_array).ok());
+ ASSERT_TRUE(pos_builder.Finish(&pos_array).ok());
+
+ auto schema = arrow::schema({arrow::field("file_path", arrow::utf8(),
true),
+ arrow::field("pos", arrow::int64(), true)});
+ auto table = arrow::Table::Make(schema, {path_array, pos_array});
+
+ std::shared_ptr<arrow::io::FileOutputStream> output;
+ ASSERT_TRUE(arrow::io::FileOutputStream::Open(path).Value(&output).ok());
+ PARQUET_THROW_NOT_OK(
+ parquet::arrow::WriteTable(*table, arrow::default_memory_pool(),
output, 1024));
+}
+
IcebergDeleteFileReaderOptions make_delete_file_reader_options(
RuntimeState* state, RuntimeProfile* profile, const
TFileScanRangeParams* scan_params,
io::IOContext* io_ctx) {
@@ -110,6 +153,21 @@ IcebergDeleteFileReaderOptions
make_delete_file_reader_options(
};
}
+IcebergDeleteFileReaderOptions delete_reader_options(RuntimeState*
runtime_state,
+ RuntimeProfile* profile,
+ TFileScanRangeParams*
scan_params,
+
IcebergDeleteFileIOContext* io_context,
+ FileMetaCache*
meta_cache) {
+ IcebergDeleteFileReaderOptions options;
+ options.state = runtime_state;
+ options.profile = profile;
+ options.scan_params = scan_params;
+ options.io_ctx = &io_context->io_ctx;
+ options.meta_cache = meta_cache;
+ options.batch_size = 1024;
+ return options;
+}
+
} // namespace
TEST(IcebergDeleteFileReaderHelperTest, BuildDeleteFileRange) {
@@ -397,4 +455,67 @@ TEST(IcebergDeleteFileReaderHelperTest,
ReadMixedEncodingParquetPositionDeleteFi
EXPECT_EQ(it->second, expected_positions);
}
+TEST(IcebergDeleteFileReaderHelperTest,
ReadOptionalParquetPositionDeleteColumnsWithoutNulls) {
+ const auto delete_file_path =
+
temp_parquet_path("iceberg_optional_position_delete_without_nulls.parquet");
+ write_position_delete_parquet(delete_file_path,
+ {std::string(kTargetDataFilePath),
+ std::string(kTargetDataFilePath),
"s3://other/file.parquet"},
+ {3, 9, 11});
+
+ RuntimeProfile profile("test_profile");
+ RuntimeState runtime_state((TQueryOptions()), TQueryGlobals());
+ FileMetaCache meta_cache(1024);
+ IcebergDeleteFileIOContext io_context(&runtime_state);
+
+ TFileScanRangeParams scan_params;
+ scan_params.file_type = TFileType::FILE_LOCAL;
+ scan_params.format_type = TFileFormatType::FORMAT_PARQUET;
+
+ TIcebergDeleteFileDesc delete_file;
+ delete_file.path = delete_file_path;
+ delete_file.file_format = TFileFormatType::FORMAT_PARQUET;
+ delete_file.__isset.file_format = true;
+
+ CollectPositionDeleteVisitor visitor;
+ auto options =
+ delete_reader_options(&runtime_state, &profile, &scan_params,
&io_context, &meta_cache);
+ auto st = read_iceberg_position_delete_file(delete_file, options,
&visitor);
+
+ std::filesystem::remove(delete_file_path);
+ ASSERT_TRUE(st.ok()) << st;
+ ASSERT_EQ(visitor.total_rows, 3);
+ EXPECT_EQ(visitor.delete_rows[kTargetDataFilePath],
std::vector<int64_t>({3, 9}));
+}
+
+TEST(IcebergDeleteFileReaderHelperTest,
RejectParquetPositionDeleteColumnsWithActualNulls) {
+ const auto delete_file_path =
temp_parquet_path("iceberg_position_delete_with_nulls.parquet");
+ write_position_delete_parquet(delete_file_path,
+ {std::string(kTargetDataFilePath),
std::nullopt}, {3, 9});
+
+ RuntimeProfile profile("test_profile");
+ RuntimeState runtime_state((TQueryOptions()), TQueryGlobals());
+ FileMetaCache meta_cache(1024);
+ IcebergDeleteFileIOContext io_context(&runtime_state);
+
+ TFileScanRangeParams scan_params;
+ scan_params.file_type = TFileType::FILE_LOCAL;
+ scan_params.format_type = TFileFormatType::FORMAT_PARQUET;
+
+ TIcebergDeleteFileDesc delete_file;
+ delete_file.path = delete_file_path;
+ delete_file.file_format = TFileFormatType::FORMAT_PARQUET;
+ delete_file.__isset.file_format = true;
+
+ CollectPositionDeleteVisitor visitor;
+ auto options =
+ delete_reader_options(&runtime_state, &profile, &scan_params,
&io_context, &meta_cache);
+ auto st = read_iceberg_position_delete_file(delete_file, options,
&visitor);
+
+ std::filesystem::remove(delete_file_path);
+ ASSERT_FALSE(st.ok());
+ EXPECT_NE(st.to_string().find("file_path contains null values"),
std::string::npos)
+ << st.to_string();
+}
+
} // namespace doris
diff --git a/be/test/format_v2/table/iceberg_reader_test.cpp
b/be/test/format_v2/table/iceberg_reader_test.cpp
index 2d3f74963d9..8a10fc06a41 100644
--- a/be/test/format_v2/table/iceberg_reader_test.cpp
+++ b/be/test/format_v2/table/iceberg_reader_test.cpp
@@ -260,6 +260,19 @@ std::shared_ptr<arrow::Array> build_nullable_int64_array(
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) {
@@ -405,6 +418,31 @@ void write_position_delete_parquet_file(const std::string&
file_path,
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;
@@ -1980,6 +2018,147 @@ TEST(IcebergV2ReaderTest,
IcebergTableReaderAppliesPositionDeleteFile) {
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";
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]