blob: 4b8f97f0f0d3ea602d590c79f828cb9d13b8e136 [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 "paimon/format/parquet/parquet_stats_extractor.h"
#include <cassert>
#include <optional>
#include <string_view>
#include <unordered_map>
#include "arrow/memory_pool.h"
#include "arrow/type.h"
#include "fmt/format.h"
#include "paimon/common/utils/arrow/arrow_input_stream_adapter.h"
#include "paimon/common/utils/arrow/mem_utils.h"
#include "paimon/common/utils/arrow/status_utils.h"
#include "paimon/common/utils/checked_cast.h"
#include "paimon/common/utils/date_time_utils.h"
#include "paimon/data/decimal.h"
#include "paimon/data/timestamp.h"
#include "paimon/defs.h"
#include "paimon/format/column_stats.h"
#include "paimon/format/parquet/parquet_schema_util.h"
#include "paimon/fs/file_system.h"
#include "paimon/memory/bytes.h"
#include "paimon/status.h"
#include "parquet/arrow/reader.h"
#include "parquet/file_reader.h"
#include "parquet/metadata.h"
#include "parquet/properties.h"
#include "parquet/schema.h"
#include "parquet/statistics.h"
#include "parquet/types.h"
namespace paimon {
class MemoryPool;
} // namespace paimon
namespace paimon::parquet {
namespace {
template <typename ParquetTypedStatsType, typename R = typename ParquetTypedStatsType::T>
std::pair<std::optional<R>, std::optional<R>> CollectMinMaxStats(
const std::shared_ptr<ParquetTypedStatsType>& typed_stats) {
std::optional<R> min;
std::optional<R> max;
if (typed_stats && typed_stats->HasMinMax()) {
min = typed_stats->min();
max = typed_stats->max();
}
return std::make_pair(min, max);
}
Result<std::unique_ptr<ColumnStats>> ConvertStatsToColumnStats(
const std::shared_ptr<::parquet::Statistics>& stats,
const std::shared_ptr<::parquet::schema::PrimitiveNode>& primitive_node,
const std::shared_ptr<arrow::DataType>& write_type, const std::shared_ptr<MemoryPool>& pool) {
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::DataType> data_type,
GetArrowType(*primitive_node));
auto id = data_type->id();
std::optional<int64_t> null_count;
if (stats && stats->HasNullCount()) {
null_count = stats->null_count();
}
switch (id) {
case arrow::Type::BOOL: {
auto typed_stats = checked_pointer_cast<::parquet::BoolStatistics>(stats);
auto [min, max] = CollectMinMaxStats(typed_stats);
return ColumnStats::CreateBooleanColumnStats(min, max, null_count);
}
case arrow::Type::INT8: {
auto typed_stats = checked_pointer_cast<::parquet::Int32Statistics>(stats);
std::optional<int8_t> min;
std::optional<int8_t> max;
if (typed_stats && typed_stats->HasMinMax()) {
min = static_cast<int8_t>(typed_stats->min());
max = static_cast<int8_t>(typed_stats->max());
}
return ColumnStats::CreateTinyIntColumnStats(min, max, null_count);
}
case arrow::Type::INT16: {
auto typed_stats = checked_pointer_cast<::parquet::Int32Statistics>(stats);
std::optional<int16_t> min;
std::optional<int16_t> max;
if (typed_stats && typed_stats->HasMinMax()) {
min = static_cast<int16_t>(typed_stats->min());
max = static_cast<int16_t>(typed_stats->max());
}
return ColumnStats::CreateSmallIntColumnStats(min, max, null_count);
}
case arrow::Type::INT32: {
auto typed_stats = checked_pointer_cast<::parquet::Int32Statistics>(stats);
auto [min, max] = CollectMinMaxStats(typed_stats);
return ColumnStats::CreateIntColumnStats(min, max, null_count);
}
case arrow::Type::INT64: {
auto typed_stats = checked_pointer_cast<::parquet::Int64Statistics>(stats);
auto [min, max] = CollectMinMaxStats(typed_stats);
return ColumnStats::CreateBigIntColumnStats(min, max, null_count);
}
case arrow::Type::FLOAT: {
auto typed_stats = checked_pointer_cast<::parquet::FloatStatistics>(stats);
auto [min, max] = CollectMinMaxStats(typed_stats);
return ColumnStats::CreateFloatColumnStats(min, max, null_count);
}
case arrow::Type::DOUBLE: {
auto typed_stats = checked_pointer_cast<::parquet::DoubleStatistics>(stats);
auto [min, max] = CollectMinMaxStats(typed_stats);
return ColumnStats::CreateDoubleColumnStats(min, max, null_count);
}
case arrow::Type::STRING: {
auto typed_stats = checked_pointer_cast<::parquet::ByteArrayStatistics>(stats);
std::optional<std::string> min;
std::optional<std::string> max;
if (typed_stats && typed_stats->HasMinMax()) {
min = std::string(std::string_view{typed_stats->min()});
max = std::string(std::string_view{typed_stats->max()});
}
return ColumnStats::CreateStringColumnStats(min, max, null_count);
}
case arrow::Type::BINARY: {
return ColumnStats::CreateStringColumnStats(std::nullopt, std::nullopt, null_count);
}
case arrow::Type::DATE32: {
auto typed_stats = checked_pointer_cast<::parquet::Int32Statistics>(stats);
auto [min, max] = CollectMinMaxStats<::parquet::Int32Statistics>(typed_stats);
return ColumnStats::CreateDateColumnStats(min, max, null_count);
}
case arrow::Type::TIMESTAMP: {
auto timestamp_type = checked_pointer_cast<::arrow::TimestampType>(data_type);
if (timestamp_type->unit() == arrow::TimeUnit::type::NANO) {
// int96 does not have statistics
return ColumnStats::CreateTimestampColumnStats(
std::nullopt, std::nullopt, std::nullopt, Timestamp::MAX_PRECISION);
}
auto typed_stats = checked_pointer_cast<::parquet::Int64Statistics>(stats);
auto [min, max] = CollectMinMaxStats(typed_stats);
// while write type is ts(second), data type in parquet file will be ts(milli), correct
// precision is supposed to be extracted from write type
auto write_ts_type = checked_pointer_cast<::arrow::TimestampType>(write_type);
int32_t precision = DateTimeUtils::GetPrecisionFromType(write_ts_type);
if (!min || !max) {
return ColumnStats::CreateTimestampColumnStats(std::nullopt, std::nullopt,
null_count, precision);
}
auto src_time_type = DateTimeUtils::GetTimeTypeFromArrowType(timestamp_type);
auto [milli_min, nano_min] = DateTimeUtils::TimestampConverter(
min.value(), src_time_type, DateTimeUtils::TimeType::MILLISECOND,
DateTimeUtils::TimeType::NANOSECOND);
auto [milli_max, nano_max] = DateTimeUtils::TimestampConverter(
max.value(), src_time_type, DateTimeUtils::TimeType::MILLISECOND,
DateTimeUtils::TimeType::NANOSECOND);
return ColumnStats::CreateTimestampColumnStats(Timestamp(milli_min, nano_min),
Timestamp(milli_max, nano_max),
null_count, precision);
}
case arrow::Type::DECIMAL128: {
auto decimal_type = checked_pointer_cast<::arrow::Decimal128Type>(data_type);
int32_t precision = decimal_type->precision();
int32_t scale = decimal_type->scale();
std::optional<Decimal> min_value;
std::optional<Decimal> max_value;
if (primitive_node->physical_type() == ::parquet::Type::INT32) {
auto typed_stats = checked_pointer_cast<::parquet::Int32Statistics>(stats);
if (typed_stats && typed_stats->HasMinMax()) {
min_value = Decimal(precision, scale, typed_stats->min());
max_value = Decimal(precision, scale, typed_stats->max());
}
} else if (primitive_node->physical_type() == ::parquet::Type::INT64) {
auto typed_stats = checked_pointer_cast<::parquet::Int64Statistics>(stats);
if (typed_stats && typed_stats->HasMinMax()) {
min_value = Decimal(precision, scale, typed_stats->min());
max_value = Decimal(precision, scale, typed_stats->max());
}
} else if (primitive_node->physical_type() == ::parquet::Type::FIXED_LEN_BYTE_ARRAY ||
primitive_node->physical_type() == ::parquet::Type::BYTE_ARRAY) {
if (stats && stats->HasMinMax()) {
Bytes encode_min(stats->EncodeMin(), pool.get());
min_value = Decimal::FromUnscaledBytes(precision, scale, &encode_min);
Bytes encode_max(stats->EncodeMax(), pool.get());
max_value = Decimal::FromUnscaledBytes(precision, scale, &encode_max);
}
}
return ColumnStats::CreateDecimalColumnStats(min_value, max_value, null_count,
precision, scale);
}
default:
return Status::Invalid(
fmt::format("cannot fetch statistics, invalid type {}", data_type->ToString()));
}
}
template <typename T>
void MergeTypedStats(
const std::string& column_name, const std::shared_ptr<::parquet::Statistics>& stats,
std::unordered_map<std::string, std::shared_ptr<::parquet::Statistics>>* merged_stats) {
auto& entry = (*merged_stats)[column_name];
if (!entry) {
entry = stats;
} else {
checked_pointer_cast<T>(entry)->Merge(*checked_pointer_cast<T>(stats));
}
}
Status MergeStats(
const std::string& column_name, const std::shared_ptr<::parquet::Statistics>& stats,
std::unordered_map<std::string, std::shared_ptr<::parquet::Statistics>>* merged_stats) {
switch (stats->physical_type()) {
case ::parquet::Type::BOOLEAN:
MergeTypedStats<::parquet::BoolStatistics>(column_name, stats, merged_stats);
break;
case ::parquet::Type::INT32:
MergeTypedStats<::parquet::Int32Statistics>(column_name, stats, merged_stats);
break;
case ::parquet::Type::INT64:
MergeTypedStats<::parquet::Int64Statistics>(column_name, stats, merged_stats);
break;
case ::parquet::Type::FLOAT:
MergeTypedStats<::parquet::FloatStatistics>(column_name, stats, merged_stats);
break;
case ::parquet::Type::DOUBLE:
MergeTypedStats<::parquet::DoubleStatistics>(column_name, stats, merged_stats);
break;
case ::parquet::Type::BYTE_ARRAY:
MergeTypedStats<::parquet::ByteArrayStatistics>(column_name, stats, merged_stats);
break;
case ::parquet::Type::FIXED_LEN_BYTE_ARRAY:
MergeTypedStats<::parquet::FLBAStatistics>(column_name, stats, merged_stats);
break;
default:
return Status::Invalid(fmt::format("Unsupported parquet type {} for statistics merge",
::parquet::TypeToString(stats->physical_type())));
}
return Status::OK();
}
} // namespace
Result<std::pair<ColumnStatsVector, FormatStatsExtractor::FileInfo>>
ParquetStatsExtractor::ExtractWithFileInfo(const std::shared_ptr<FileSystem>& file_system,
const std::string& path,
const std::shared_ptr<MemoryPool>& pool) {
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<InputStream> input_stream, file_system->Open(path));
assert(input_stream);
PAIMON_ASSIGN_OR_RAISE(int64_t file_length, input_stream->Length());
std::shared_ptr<arrow::MemoryPool> parquet_memory_pool = GetArrowPool(pool);
auto parquet_input_file = std::make_shared<ArrowInputStreamAdapter>(
std::move(input_stream), file_length, parquet_memory_pool);
::parquet::ReaderProperties read_properties(parquet_memory_pool.get());
read_properties.enable_buffered_stream();
::parquet::arrow::FileReaderBuilder file_reader_builder;
PAIMON_RETURN_NOT_OK_FROM_ARROW(file_reader_builder.Open(parquet_input_file, read_properties));
std::shared_ptr<::parquet::FileMetaData> file_metadata =
file_reader_builder.raw_reader()->metadata();
int32_t field_count = file_metadata->schema()->group_node()->field_count();
ColumnStatsVector result_stats;
result_stats.reserve(field_count);
std::unordered_map<std::string, std::shared_ptr<::parquet::Statistics>> merged_stats;
for (int32_t row_group_idx = 0; row_group_idx < file_metadata->num_row_groups();
++row_group_idx) {
for (int32_t col_idx = 0; col_idx < file_metadata->num_columns(); ++col_idx) {
auto column_chunk = file_metadata->RowGroup(row_group_idx)->ColumnChunk(col_idx);
if (!column_chunk->is_stats_set()) {
continue;
}
auto stats = column_chunk->statistics();
std::string column_name = column_chunk->path_in_schema()->ToDotString();
PAIMON_RETURN_NOT_OK(MergeStats(column_name, stats, &merged_stats));
}
}
for (int32_t field_idx = 0; field_idx < field_count; ++field_idx) {
auto node = file_metadata->schema()->group_node()->field(field_idx);
if (node->is_group()) {
// nested type do not have parquet stats
const auto& logical_type = node->logical_type();
FieldType nested_type = FieldType::UNKNOWN;
if (logical_type->is_list()) {
nested_type = FieldType::ARRAY;
} else if (logical_type->is_map()) {
nested_type = FieldType::MAP;
} else if (logical_type->is_none()) {
nested_type = FieldType::STRUCT;
}
result_stats.push_back(ColumnStats::CreateNestedColumnStats(nested_type, std::nullopt));
} else {
auto primitive_node = checked_pointer_cast<::parquet::schema::PrimitiveNode>(node);
auto iter = merged_stats.find(node->name());
const std::shared_ptr<::parquet::Statistics>& parquet_stats =
iter == merged_stats.end() ? nullptr : iter->second;
PAIMON_ASSIGN_OR_RAISE(
std::shared_ptr<ColumnStats> col_stats,
ConvertStatsToColumnStats(parquet_stats, primitive_node,
write_schema_->field(field_idx)->type(), pool));
result_stats.push_back(col_stats);
}
}
return std::make_pair(std::move(result_stats), FileInfo(file_metadata->num_rows()));
}
} // namespace paimon::parquet