blob: 2dd001469d8a665bc8d6d3f690e046d12b7c0258 [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/hudi_reader.h"
#include <arrow/api.h>
#include <arrow/io/api.h>
#include <gtest/gtest.h>
#include <parquet/api/reader.h>
#include <parquet/arrow/writer.h>
#include <filesystem>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#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/field.h"
#include "format_v2/column_data.h"
#include "gen_cpp/ExternalTableSchema_types.h"
#include "gen_cpp/PlanNodes_types.h"
#include "io/io_common.h"
#include "runtime/runtime_profile.h"
#include "runtime/runtime_state.h"
namespace doris::format {
namespace {
schema::external::TFieldPtr external_schema_field(std::string name, int32_t id,
std::vector<std::string> aliases = {}) {
auto field = std::make_shared<schema::external::TField>();
field->__set_name(std::move(name));
field->__set_id(id);
if (!aliases.empty()) {
field->__set_name_mapping(std::move(aliases));
}
schema::external::TFieldPtr field_ptr;
field_ptr.field_ptr = std::move(field);
field_ptr.__isset.field_ptr = true;
return field_ptr;
}
schema::external::TSchema external_schema(int64_t schema_id,
std::vector<schema::external::TFieldPtr> fields) {
schema::external::TStructField root_field;
root_field.__set_fields(std::move(fields));
schema::external::TSchema schema;
schema.__set_schema_id(schema_id);
schema.__set_root_field(std::move(root_field));
return schema;
}
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 = type;
return field;
}
ColumnDefinition make_table_column(int32_t id, const std::string& name, const DataTypePtr& type) {
ColumnDefinition column;
column.identifier = Field::create_field<TYPE_INT>(id);
column.name = name;
column.type = make_nullable(type);
return column;
}
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 write_int_parquet_file(const std::string& file_path, const std::vector<int32_t>& values) {
arrow::Int32Builder value_builder;
for (const auto value : values) {
ASSERT_TRUE(value_builder.Append(value).ok());
}
std::shared_ptr<arrow::Array> value_array;
ASSERT_TRUE(value_builder.Finish(&value_array).ok());
const auto table = arrow::Table::Make(
arrow::schema({arrow::field("id", arrow::int32(), false)}), {value_array});
auto file_result = arrow::io::FileOutputStream::Open(file_path);
ASSERT_TRUE(file_result.ok()) << file_result.status();
std::shared_ptr<arrow::io::FileOutputStream> output = *file_result;
PARQUET_THROW_NOT_OK(::parquet::arrow::WriteTable(*table, arrow::default_memory_pool(), output,
static_cast<int64_t>(values.size())));
}
TTableFormatFileDesc hudi_table_format_desc(std::optional<int64_t> schema_id) {
TTableFormatFileDesc table_format_params;
table_format_params.__set_table_format_type("hudi");
THudiFileDesc hudi_params;
if (schema_id.has_value()) {
hudi_params.__set_schema_id(*schema_id);
}
table_format_params.__set_hudi_params(hudi_params);
return table_format_params;
}
// Scenario: FileScannerV2 Hudi native reader uses the split schema id to annotate the physical
// file schema before TableColumnMapper runs. This keeps schema-evolved Hudi files on field-id
// mapping, including renamed nested children.
TEST(HudiReaderTest, AnnotatesFileSchemaFromSplitHistorySchema) {
TFileScanRangeParams scan_params;
scan_params.__set_current_schema_id(200);
auto profile_field = external_schema_field("profile", 20);
schema::external::TStructField profile_struct;
profile_struct.__set_fields({external_schema_field("old_age", 21, {"age"})});
profile_field.field_ptr->nestedField.__set_struct_field(std::move(profile_struct));
profile_field.field_ptr->__isset.nestedField = true;
scan_params.__set_history_schema_info({
external_schema(100, {external_schema_field("old_name", 10, {"name"}), profile_field}),
external_schema(
200, {external_schema_field("name", 10), external_schema_field("profile", 20)}),
});
hudi::HudiReader reader;
reader.TEST_set_scan_params(&scan_params);
SplitReadOptions split_options;
split_options.current_range.__set_table_format_params(hudi_table_format_desc(100));
ASSERT_TRUE(reader.prepare_split(split_options).ok());
EXPECT_EQ(reader.TEST_mapping_mode(), TableColumnMappingMode::BY_FIELD_ID);
auto string_type = std::make_shared<DataTypeString>();
auto int_type = std::make_shared<DataTypeInt32>();
auto profile_type = std::make_shared<DataTypeStruct>(DataTypes {int_type}, Strings {"old_age"});
auto profile_column = make_file_column(1, "profile", profile_type);
profile_column.children = {make_file_column(0, "old_age", int_type)};
std::vector<ColumnDefinition> file_schema {
make_file_column(0, "old_name", string_type),
profile_column,
};
ASSERT_TRUE(reader.TEST_annotate_file_schema(&file_schema).ok());
ASSERT_EQ(file_schema.size(), 2);
EXPECT_EQ(file_schema[0].get_identifier_field_id(), 10);
EXPECT_EQ(file_schema[0].name_mapping, std::vector<std::string>({"name"}));
EXPECT_EQ(file_schema[1].get_identifier_field_id(), 20);
ASSERT_EQ(file_schema[1].children.size(), 1);
EXPECT_EQ(file_schema[1].children[0].get_identifier_field_id(), 21);
EXPECT_EQ(file_schema[1].children[0].name_mapping, std::vector<std::string>({"age"}));
}
// Scenario: a Hudi split can only use field-id mapping when its schema id resolves to a historical
// schema sent by FE. Unknown or missing split schema ids must fall back to BY_NAME and leave the
// physical file schema untouched.
TEST(HudiReaderTest, FallsBackToByNameWhenSplitHistorySchemaIsMissing) {
TFileScanRangeParams scan_params;
scan_params.__set_current_schema_id(200);
scan_params.__set_history_schema_info({
external_schema(200, {external_schema_field("name", 10)}),
});
hudi::HudiReader reader;
reader.TEST_set_scan_params(&scan_params);
SplitReadOptions split_options;
split_options.current_range.__set_table_format_params(hudi_table_format_desc(100));
ASSERT_TRUE(reader.prepare_split(split_options).ok());
EXPECT_EQ(reader.TEST_mapping_mode(), TableColumnMappingMode::BY_NAME);
std::vector<ColumnDefinition> file_schema {
make_file_column(0, "old_name", std::make_shared<DataTypeString>()),
};
ASSERT_TRUE(reader.TEST_annotate_file_schema(&file_schema).ok());
EXPECT_EQ(file_schema[0].get_identifier_field_id(), 0);
EXPECT_TRUE(file_schema[0].name_mapping.empty());
}
// Scenario: HudiReader must reset the previous split schema id before each split. Otherwise a
// BY_FIELD_ID split could leak its schema id into the next split that carries no schema id.
TEST(HudiReaderTest, ResetsSplitSchemaIdBeforePreparingNextSplit) {
TFileScanRangeParams scan_params;
scan_params.__set_current_schema_id(200);
scan_params.__set_history_schema_info({
external_schema(100, {external_schema_field("old_name", 10, {"name"})}),
external_schema(200, {external_schema_field("name", 10)}),
});
hudi::HudiReader reader;
reader.TEST_set_scan_params(&scan_params);
SplitReadOptions split_with_schema_id;
split_with_schema_id.current_range.__set_table_format_params(hudi_table_format_desc(100));
ASSERT_TRUE(reader.prepare_split(split_with_schema_id).ok());
EXPECT_EQ(reader.TEST_mapping_mode(), TableColumnMappingMode::BY_FIELD_ID);
SplitReadOptions split_without_schema_id;
split_without_schema_id.current_range.__set_table_format_params(
hudi_table_format_desc(std::nullopt));
ASSERT_TRUE(reader.prepare_split(split_without_schema_id).ok());
EXPECT_EQ(reader.TEST_mapping_mode(), TableColumnMappingMode::BY_NAME);
}
TEST(HudiReaderTest, NativeBaseFilesAreMarkedImmutableForPageCache) {
hudi::HudiReader reader;
for (const auto format : {FileFormat::PARQUET, FileFormat::ORC}) {
SplitReadOptions split_options;
split_options.current_split_format = format;
split_options.current_range.__set_path("hudi-base-file");
split_options.current_range.__set_table_format_params(hudi_table_format_desc(100));
ASSERT_TRUE(reader.prepare_split(split_options).ok());
EXPECT_TRUE(reader.TEST_current_data_file_is_immutable());
}
}
TEST(HudiHybridReaderTest, AdaptiveBatchSizeReachesBothChildReaders) {
hudi::HudiHybridReader reader;
reader.TEST_install_batch_size_children();
reader.set_batch_size(123);
const auto child_batch_sizes = reader.TEST_child_batch_sizes();
EXPECT_EQ(child_batch_sizes.first, 123);
EXPECT_EQ(child_batch_sizes.second, 123);
}
TEST(HudiHybridReaderTest, NativeCountStarReportsMetadataRowsThroughHybridReader) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_hudi_hybrid_count_star_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "base-file.parquet").string();
write_int_parquet_file(file_path, {1, 2, 3});
const std::vector<ColumnDefinition> projected_columns {
make_table_column(0, "id", std::make_shared<DataTypeInt32>()),
};
RuntimeProfile profile("test_profile");
RuntimeState state {TQueryOptions(), TQueryGlobals()};
TFileScanRangeParams scan_params;
scan_params.__set_file_type(TFileType::FILE_LOCAL);
scan_params.__set_format_type(TFileFormatType::FORMAT_PARQUET);
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;
ShardedKVCache cache(1);
hudi::HudiHybridReader 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,
.push_down_count_columns = std::vector<GlobalIndex> {},
})
.ok());
SplitReadOptions split_options;
split_options.cache = &cache;
split_options.current_split_format = FileFormat::PARQUET;
split_options.current_range.__set_path(file_path);
split_options.current_range.__set_file_size(
static_cast<int64_t>(std::filesystem::file_size(file_path)));
split_options.current_range.__set_format_type(TFileFormatType::FORMAT_PARQUET);
split_options.current_range.__set_table_format_params(hudi_table_format_desc(std::nullopt));
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());
EXPECT_EQ(block.rows(), 3);
EXPECT_TRUE(reader.current_split_uses_metadata_count());
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
} // namespace
} // namespace doris::format