You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@doris.apache.org by ga...@apache.org on 2026/05/26 09:45:51 UTC
(doris) branch refact_reader_branch updated: Complete basic parquet reader (#63659)
This is an automated email from the ASF dual-hosted git repository.
Gabriel39 pushed a commit to branch refact_reader_branch
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/refact_reader_branch by this push:
new 3dbfc4c596d Complete basic parquet reader (#63659)
3dbfc4c596d is described below
commit 3dbfc4c596dc9997db4f1211765c02872ae5252d
Author: Gabriel <li...@selectdb.com>
AuthorDate: Tue May 26 17:45:41 2026 +0800
Complete basic parquet reader (#63659)
Co-authored-by: Socrates <su...@selectdb.com>
---
.../data_type_serde/data_type_datetimev2_serde.cpp | 30 +-
.../data_type_serde/data_type_datetimev2_serde.h | 2 +
.../data_type_serde/data_type_datev2_serde.cpp | 20 +-
.../core/data_type_serde/data_type_datev2_serde.h | 2 +
.../data_type_serde/data_type_decimal_serde.cpp | 64 +++
.../core/data_type_serde/data_type_decimal_serde.h | 2 +
.../data_type_serde/data_type_nullable_serde.cpp | 20 +-
.../data_type_serde/data_type_nullable_serde.h | 2 +
.../data_type_serde/data_type_number_serde.cpp | 52 ++
.../core/data_type_serde/data_type_number_serde.h | 3 +
be/src/core/data_type_serde/data_type_serde.cpp | 6 +
be/src/core/data_type_serde/data_type_serde.h | 7 +
.../data_type_serde/data_type_string_serde.cpp | 28 +
.../core/data_type_serde/data_type_string_serde.h | 3 +
.../core/data_type_serde/data_type_time_serde.cpp | 44 ++
be/src/core/data_type_serde/data_type_time_serde.h | 2 +
be/src/core/data_type_serde/decoded_column_view.h | 62 +++
be/src/exprs/vslot_ref.h | 2 +-
be/src/format/new_parquet/column_reader.cpp | 562 +++++++++++++++++++
be/src/format/new_parquet/column_reader.h | 116 ++++
.../format/new_parquet/parquet_column_schema.cpp | 158 ++++++
be/src/format/new_parquet/parquet_column_schema.h | 67 +++
be/src/format/new_parquet/parquet_reader.cpp | 601 +++++++++++++++++++++
be/src/format/new_parquet/parquet_reader.h | 138 +++++
be/src/format/new_parquet/parquet_statistics.cpp | 254 +++++++++
be/src/format/new_parquet/parquet_statistics.h | 88 +++
be/src/format/new_parquet/parquet_type.cpp | 349 ++++++++++++
be/src/format/new_parquet/parquet_type.h | 84 +++
be/src/format/new_parquet/selection_vector.h | 116 ++++
be/src/format/parquet/parquet_reader.h | 77 ---
be/src/format/reader/column_mapper.cpp | 7 +-
be/src/format/reader/expr/delete_predicate.cpp | 4 +-
be/src/format/reader/file_reader.cpp | 42 ++
be/src/format/reader/file_reader.h | 118 ++--
be/src/format/reader/table_reader.h | 22 +-
.../data_type_serde_decoded_values_test.cpp | 278 ++++++++++
.../new_parquet/parquet_column_reader_test.cpp | 562 +++++++++++++++++++
be/test/format/new_parquet/parquet_reader_test.cpp | 341 ++++++++++++
docs/doris-arrow-parquet-reader-implementation.md | 291 ++++++++++
39 files changed, 4485 insertions(+), 141 deletions(-)
diff --git a/be/src/core/data_type_serde/data_type_datetimev2_serde.cpp b/be/src/core/data_type_serde/data_type_datetimev2_serde.cpp
index 92a5106b481..fc2c14d1829 100644
--- a/be/src/core/data_type_serde/data_type_datetimev2_serde.cpp
+++ b/be/src/core/data_type_serde/data_type_datetimev2_serde.cpp
@@ -22,9 +22,9 @@
#include <chrono> // IWYU pragma: keep
#include <cstdint>
-
#include "common/status.h"
#include "core/column/column_const.h"
+#include "core/data_type_serde/decoded_column_view.h"
#include "core/data_type/data_type_decimal.h"
#include "core/data_type/data_type_number.h"
#include "core/data_type/primitive_type.h"
@@ -451,6 +451,34 @@ Status DataTypeDateTimeV2SerDe::read_column_from_arrow(IColumn& column,
return Status::OK();
}
+Status DataTypeDateTimeV2SerDe::read_column_from_decoded_values(
+ IColumn& column, const DecodedColumnView& view) const {
+ if (view.value_kind != DecodedValueKind::INT64) {
+ return Status::NotSupported("DATETIMEV2 decoded reader expects INT64 source");
+ }
+ if (view.values == nullptr && view.row_count > 0) {
+ return Status::Corruption("Decoded value buffer is null for {}", column.get_name());
+ }
+ auto& data = assert_cast<ColumnDateTimeV2&>(column).get_data();
+ const auto* values = reinterpret_cast<const int64_t*>(view.values);
+ static const cctz::time_zone utc_time_zone = cctz::utc_time_zone();
+ const int64_t second_mask = view.time_unit == DecodedTimeUnit::MILLIS ? 1000 : 1000000;
+ for (int64_t row = 0; row < view.row_count; ++row) {
+ int64_t epoch_seconds = values[row] / second_mask;
+ int64_t sub_second = values[row] % second_mask;
+ if (sub_second < 0) {
+ sub_second += second_mask;
+ --epoch_seconds;
+ }
+ const int32_t microsecond = static_cast<int32_t>(sub_second * (1000000 / second_mask));
+ DateV2Value<DateTimeV2ValueType> datetime_value;
+ datetime_value.from_unixtime(epoch_seconds, utc_time_zone);
+ datetime_value.set_microsecond(static_cast<uint64_t>(microsecond));
+ data.push_back(datetime_value);
+ }
+ return Status::OK();
+}
+
Status DataTypeDateTimeV2SerDe::write_column_to_mysql_binary(const IColumn& column,
MysqlRowBinaryBuffer& result,
int64_t row_idx, bool col_const,
diff --git a/be/src/core/data_type_serde/data_type_datetimev2_serde.h b/be/src/core/data_type_serde/data_type_datetimev2_serde.h
index 0389432a621..34d0373eba1 100644
--- a/be/src/core/data_type_serde/data_type_datetimev2_serde.h
+++ b/be/src/core/data_type_serde/data_type_datetimev2_serde.h
@@ -88,6 +88,8 @@ public:
const cctz::time_zone& ctz) const override;
Status read_column_from_arrow(IColumn& column, const arrow::Array* arrow_array, int64_t start,
int64_t end, const cctz::time_zone& ctz) const override;
+ Status read_column_from_decoded_values(IColumn& column,
+ const DecodedColumnView& view) const override;
Status write_column_to_mysql_binary(const IColumn& column, MysqlRowBinaryBuffer& row_buffer,
int64_t row_idx, bool col_const,
diff --git a/be/src/core/data_type_serde/data_type_datev2_serde.cpp b/be/src/core/data_type_serde/data_type_datev2_serde.cpp
index d72f3664ae6..9410df86eaa 100644
--- a/be/src/core/data_type_serde/data_type_datev2_serde.cpp
+++ b/be/src/core/data_type_serde/data_type_datev2_serde.cpp
@@ -22,8 +22,8 @@
#include <fmt/core.h>
#include <cstdint>
-
#include "core/column/column_const.h"
+#include "core/data_type_serde/decoded_column_view.h"
#include "core/data_type/data_type_decimal.h"
#include "core/data_type/data_type_number.h"
#include "core/data_type/define_primitive_type.h"
@@ -125,6 +125,24 @@ Status DataTypeDateV2SerDe::read_column_from_arrow(IColumn& column, const arrow:
return Status::OK();
}
+Status DataTypeDateV2SerDe::read_column_from_decoded_values(
+ IColumn& column, const DecodedColumnView& view) const {
+ if (view.value_kind != DecodedValueKind::INT32) {
+ return Status::NotSupported("DATEV2 decoded reader expects INT32 source");
+ }
+ if (view.values == nullptr && view.row_count > 0) {
+ return Status::Corruption("Decoded value buffer is null for {}", column.get_name());
+ }
+ auto& data = assert_cast<ColumnDateV2&>(column).get_data();
+ const auto* values = reinterpret_cast<const int32_t*>(view.values);
+ for (int64_t row = 0; row < view.row_count; ++row) {
+ DateV2Value<DateV2ValueType> date_v2;
+ date_v2.get_date_from_daynr(values[row] + date_threshold);
+ data.push_back(date_v2);
+ }
+ return Status::OK();
+}
+
Status DataTypeDateV2SerDe::write_column_to_mysql_binary(const IColumn& column,
MysqlRowBinaryBuffer& result,
int64_t row_idx, bool col_const,
diff --git a/be/src/core/data_type_serde/data_type_datev2_serde.h b/be/src/core/data_type_serde/data_type_datev2_serde.h
index 0375f9be4b4..ff985d61345 100644
--- a/be/src/core/data_type_serde/data_type_datev2_serde.h
+++ b/be/src/core/data_type_serde/data_type_datev2_serde.h
@@ -86,6 +86,8 @@ public:
const cctz::time_zone& ctz) const override;
Status read_column_from_arrow(IColumn& column, const arrow::Array* arrow_array, int64_t start,
int64_t end, const cctz::time_zone& ctz) const override;
+ Status read_column_from_decoded_values(IColumn& column,
+ const DecodedColumnView& view) const override;
Status write_column_to_mysql_binary(const IColumn& column, MysqlRowBinaryBuffer& row_buffer,
int64_t row_idx, bool col_const,
const FormatOptions& options) const override;
diff --git a/be/src/core/data_type_serde/data_type_decimal_serde.cpp b/be/src/core/data_type_serde/data_type_decimal_serde.cpp
index 084f9c75df3..c744cdb8a2f 100644
--- a/be/src/core/data_type_serde/data_type_decimal_serde.cpp
+++ b/be/src/core/data_type_serde/data_type_decimal_serde.cpp
@@ -31,6 +31,7 @@
#include "core/column/column_decimal.h"
#include "core/data_type/data_type_decimal.h"
#include "core/data_type/define_primitive_type.h"
+#include "core/data_type_serde/decoded_column_view.h"
#include "core/types.h"
#include "exec/common/arithmetic_overflow.h"
#include "exprs/function/cast/cast_to_decimal.h"
@@ -43,6 +44,53 @@
#include "util/string_parser.hpp"
namespace doris {
+namespace {
+
+template <typename NativeType>
+NativeType decode_big_endian_signed_integer(const uint8_t* data, int length) {
+ using UnsignedNativeType =
+ std::conditional_t<std::is_same_v<NativeType, Int128>, unsigned __int128,
+ std::make_unsigned_t<NativeType>>;
+ UnsignedNativeType value = data != nullptr && length > 0 && (data[0] & 0x80) != 0
+ ? static_cast<UnsignedNativeType>(-1)
+ : 0;
+ for (int i = 0; i < length; ++i) {
+ value = static_cast<UnsignedNativeType>((value << 8) | data[i]);
+ }
+ return static_cast<NativeType>(value);
+}
+
+template <PrimitiveType T>
+typename PrimitiveTypeTraits<T>::CppType read_decimal_decoded_value(
+ const DecodedColumnView& view, int64_t row) {
+ using FieldType = typename PrimitiveTypeTraits<T>::CppType;
+ if (view.value_kind == DecodedValueKind::INT32) {
+ const auto* values = reinterpret_cast<const int32_t*>(view.values);
+ return FieldType {static_cast<typename FieldType::NativeType>(values[row])};
+ }
+ if (view.value_kind == DecodedValueKind::INT64) {
+ const auto* values = reinterpret_cast<const int64_t*>(view.values);
+ return FieldType {static_cast<typename FieldType::NativeType>(values[row])};
+ }
+ const auto& value = (*view.binary_values)[row];
+ const auto length = view.value_kind == DecodedValueKind::FIXED_BINARY
+ ? view.fixed_length
+ : cast_set<int, size_t, false>(value.size);
+ return FieldType {static_cast<typename FieldType::NativeType>(
+ decode_big_endian_signed_integer<Int128>(reinterpret_cast<const uint8_t*>(value.data),
+ length))};
+}
+
+template <PrimitiveType T>
+Status read_decimal_decoded_values(IColumn& column, const DecodedColumnView& view) {
+ auto& data = assert_cast<ColumnDecimal<T>&>(column).get_data();
+ for (int64_t row = 0; row < view.row_count; ++row) {
+ data.push_back(read_decimal_decoded_value<T>(view, row));
+ }
+ return Status::OK();
+}
+
+} // namespace
template <PrimitiveType T>
Status DataTypeDecimalSerDe<T>::from_string_batch(const ColumnString& str, ColumnNullable& column,
@@ -381,6 +429,22 @@ Status DataTypeDecimalSerDe<T>::read_column_from_arrow(IColumn& column,
return Status::OK();
}
+template <PrimitiveType T>
+Status DataTypeDecimalSerDe<T>::read_column_from_decoded_values(
+ IColumn& column, const DecodedColumnView& view) const {
+ if constexpr (T == TYPE_DECIMAL32 || T == TYPE_DECIMAL64 || T == TYPE_DECIMAL128I ||
+ T == TYPE_DECIMAL256) {
+ if (view.value_kind == DecodedValueKind::INT32 ||
+ view.value_kind == DecodedValueKind::INT64 ||
+ view.value_kind == DecodedValueKind::BINARY ||
+ view.value_kind == DecodedValueKind::FIXED_BINARY) {
+ return read_decimal_decoded_values<T>(column, view);
+ }
+ }
+ return Status::NotSupported("Unsupported decoded values for {} from source kind {}",
+ get_name(), static_cast<int>(view.value_kind));
+}
+
template <PrimitiveType T>
Status DataTypeDecimalSerDe<T>::write_column_to_mysql_binary(const IColumn& column,
MysqlRowBinaryBuffer& result,
diff --git a/be/src/core/data_type_serde/data_type_decimal_serde.h b/be/src/core/data_type_serde/data_type_decimal_serde.h
index 0185672e024..089835a21be 100644
--- a/be/src/core/data_type_serde/data_type_decimal_serde.h
+++ b/be/src/core/data_type_serde/data_type_decimal_serde.h
@@ -107,6 +107,8 @@ public:
const cctz::time_zone& ctz) const override;
Status read_column_from_arrow(IColumn& column, const arrow::Array* arrow_array, int64_t start,
int64_t end, const cctz::time_zone& ctz) const override;
+ Status read_column_from_decoded_values(IColumn& column,
+ const DecodedColumnView& view) const override;
Status write_column_to_mysql_binary(const IColumn& column, MysqlRowBinaryBuffer& row_buffer,
int64_t row_idx, bool col_const,
const FormatOptions& options) const override;
diff --git a/be/src/core/data_type_serde/data_type_nullable_serde.cpp b/be/src/core/data_type_serde/data_type_nullable_serde.cpp
index a93f8d6126c..6b15b29c63a 100644
--- a/be/src/core/data_type_serde/data_type_nullable_serde.cpp
+++ b/be/src/core/data_type_serde/data_type_nullable_serde.cpp
@@ -22,13 +22,14 @@
#include <algorithm>
#include <boost/iterator/iterator_facade.hpp>
-#include <memory>
+#include <vector>
#include "core/assert_cast.h"
#include "core/column/column.h"
#include "core/column/column_const.h"
#include "core/column/column_nullable.h"
#include "core/column/column_vector.h"
+#include "core/data_type_serde/decoded_column_view.h"
#include "core/data_type_serde/data_type_serde.h"
#include "core/data_type_serde/data_type_string_serde.h"
#include "exprs/function/cast/cast_base.h"
@@ -350,6 +351,23 @@ Status DataTypeNullableSerDe::read_column_from_arrow(IColumn& column,
ctz);
}
+Status DataTypeNullableSerDe::read_column_from_decoded_values(
+ IColumn& column, const DecodedColumnView& view) const {
+ auto& nullable_column = assert_cast<ColumnNullable&>(column);
+ auto& null_map = nullable_column.get_null_map_data();
+ const auto old_size = null_map.size();
+ null_map.resize(null_map.size() + view.row_count);
+ if (view.null_map != nullptr) {
+ // TODO: skip if no null in map
+ auto* dst = null_map.data() + old_size;
+ memcpy(dst, view.null_map, view.row_count);
+ }
+ DecodedColumnView nested_view = view;
+ nested_view.null_map = nullptr;
+ return nested_serde->read_column_from_decoded_values(nullable_column.get_nested_column(),
+ nested_view);
+}
+
bool DataTypeNullableSerDe::write_column_to_mysql_text(const IColumn& column, BufferWritable& bw,
int64_t row_idx,
const FormatOptions& options) const {
diff --git a/be/src/core/data_type_serde/data_type_nullable_serde.h b/be/src/core/data_type_serde/data_type_nullable_serde.h
index cfb4e1e3bca..376f3692dc1 100644
--- a/be/src/core/data_type_serde/data_type_nullable_serde.h
+++ b/be/src/core/data_type_serde/data_type_nullable_serde.h
@@ -86,6 +86,8 @@ public:
const cctz::time_zone& ctz) const override;
Status read_column_from_arrow(IColumn& column, const arrow::Array* arrow_array, int64_t start,
int64_t end, const cctz::time_zone& ctz) const override;
+ Status read_column_from_decoded_values(IColumn& column,
+ const DecodedColumnView& view) const override;
Status write_column_to_mysql_binary(const IColumn& column, MysqlRowBinaryBuffer& row_buffer,
int64_t row_idx, bool col_const,
const FormatOptions& options) const override;
diff --git a/be/src/core/data_type_serde/data_type_number_serde.cpp b/be/src/core/data_type_serde/data_type_number_serde.cpp
index 39e9c0726c4..131e6d05941 100644
--- a/be/src/core/data_type_serde/data_type_number_serde.cpp
+++ b/be/src/core/data_type_serde/data_type_number_serde.cpp
@@ -26,6 +26,7 @@
#include "core/column/column_nullable.h"
#include "core/data_type/define_primitive_type.h"
#include "core/data_type/primitive_type.h"
+#include "core/data_type_serde/decoded_column_view.h"
#include "core/data_type_serde/data_type_serde.h"
#include "core/packed_int128.h"
#include "core/types.h"
@@ -42,6 +43,29 @@
#include "util/to_string.h"
namespace doris {
+namespace {
+
+template <typename NativeType>
+const NativeType* decoded_values_as(const DecodedColumnView& view) {
+ return reinterpret_cast<const NativeType*>(view.values);
+}
+
+template <PrimitiveType DorisType, typename SourceType>
+Status read_number_decoded_values(IColumn& column, const DecodedColumnView& view) {
+ if (view.values == nullptr && view.row_count > 0) {
+ return Status::Corruption("Decoded value buffer is null for {}", column.get_name());
+ }
+ auto& data = assert_cast<typename PrimitiveTypeTraits<DorisType>::ColumnType&>(column)
+ .get_data();
+ const auto* values = decoded_values_as<SourceType>(view);
+ for (int64_t row = 0; row < view.row_count; ++row) {
+ using DorisCppType = typename PrimitiveTypeTraits<DorisType>::CppType;
+ data.push_back(static_cast<DorisCppType>(values[row]));
+ }
+ return Status::OK();
+}
+
+} // namespace
// Type map的基本结构
template <typename Key, typename Value, typename... Rest>
struct TypeMap {
@@ -156,6 +180,34 @@ Status DataTypeNumberSerDe<T>::write_column_to_arrow(const IColumn& column, cons
return Status::OK();
}
+template <PrimitiveType T>
+Status DataTypeNumberSerDe<T>::read_column_from_decoded_values(
+ IColumn& column, const DecodedColumnView& view) const {
+ if constexpr (T == TYPE_BOOLEAN) {
+ if (view.value_kind == DecodedValueKind::BOOL) {
+ return read_number_decoded_values<TYPE_BOOLEAN, bool>(column, view);
+ }
+ } else if constexpr (T == TYPE_INT) {
+ if (view.value_kind == DecodedValueKind::INT32) {
+ return read_number_decoded_values<TYPE_INT, int32_t>(column, view);
+ }
+ } else if constexpr (T == TYPE_BIGINT) {
+ if (view.value_kind == DecodedValueKind::INT64) {
+ return read_number_decoded_values<TYPE_BIGINT, int64_t>(column, view);
+ }
+ } else if constexpr (T == TYPE_FLOAT) {
+ if (view.value_kind == DecodedValueKind::FLOAT) {
+ return read_number_decoded_values<TYPE_FLOAT, float>(column, view);
+ }
+ } else if constexpr (T == TYPE_DOUBLE) {
+ if (view.value_kind == DecodedValueKind::DOUBLE) {
+ return read_number_decoded_values<TYPE_DOUBLE, double>(column, view);
+ }
+ }
+ return Status::NotSupported("Unsupported decoded values for {} from source kind {}",
+ get_name(), static_cast<int>(view.value_kind));
+}
+
template <PrimitiveType T>
Status DataTypeNumberSerDe<T>::deserialize_one_cell_from_json(IColumn& column, Slice& slice,
const FormatOptions& options) const {
diff --git a/be/src/core/data_type_serde/data_type_number_serde.h b/be/src/core/data_type_serde/data_type_number_serde.h
index b57f9f9d212..0e0a3acfc1a 100644
--- a/be/src/core/data_type_serde/data_type_number_serde.h
+++ b/be/src/core/data_type_serde/data_type_number_serde.h
@@ -117,6 +117,9 @@ public:
Status read_column_from_arrow(IColumn& column, const arrow::Array* arrow_array, int64_t start,
int64_t end, const cctz::time_zone& ctz) const override;
+ Status read_column_from_decoded_values(IColumn& column,
+ const DecodedColumnView& view) const override;
+
Status write_column_to_mysql_binary(const IColumn& column, MysqlRowBinaryBuffer& row_buffer,
int64_t row_idx, bool col_const,
const FormatOptions& options) const override;
diff --git a/be/src/core/data_type_serde/data_type_serde.cpp b/be/src/core/data_type_serde/data_type_serde.cpp
index ac688ae6c30..b6a49524887 100644
--- a/be/src/core/data_type_serde/data_type_serde.cpp
+++ b/be/src/core/data_type_serde/data_type_serde.cpp
@@ -34,6 +34,12 @@
namespace doris {
DataTypeSerDe::~DataTypeSerDe() = default;
+Status DataTypeSerDe::read_column_from_decoded_values(IColumn& column,
+ const DecodedColumnView& view) const {
+ return Status::NotSupported("read_column_from_decoded_values is not supported for {}",
+ get_name());
+}
+
DataTypeSerDeSPtrs create_data_type_serdes(const DataTypes& types) {
DataTypeSerDeSPtrs serdes;
serdes.reserve(types.size());
diff --git a/be/src/core/data_type_serde/data_type_serde.h b/be/src/core/data_type_serde/data_type_serde.h
index 7c007c6558d..07ad5d5d1b0 100644
--- a/be/src/core/data_type_serde/data_type_serde.h
+++ b/be/src/core/data_type_serde/data_type_serde.h
@@ -27,6 +27,7 @@
#include "common/cast_set.h"
#include "common/status.h"
#include "core/column/column_nullable.h"
+#include "core/data_type_serde/decoded_column_view.h"
#include "core/field.h"
#include "core/string_buffer.hpp"
#include "core/types.h"
@@ -485,6 +486,12 @@ public:
int64_t start, int64_t end,
const cctz::time_zone& ctz) const = 0;
+ // Read already decoded column values into a Doris column. The input view is format-neutral:
+ // file readers translate their decoder output into DecodedColumnView, while SerDe owns
+ // the Doris-type-specific materialization into IColumn.
+ virtual Status read_column_from_decoded_values(IColumn& column,
+ const DecodedColumnView& view) const;
+
// ORC serializer
virtual Status write_column_to_orc(const std::string& timezone, const IColumn& column,
const NullMap* null_map,
diff --git a/be/src/core/data_type_serde/data_type_string_serde.cpp b/be/src/core/data_type_serde/data_type_string_serde.cpp
index b7a59b3c07e..478cdf3b5e6 100644
--- a/be/src/core/data_type_serde/data_type_string_serde.cpp
+++ b/be/src/core/data_type_serde/data_type_string_serde.cpp
@@ -18,12 +18,29 @@
#include "core/data_type_serde/data_type_string_serde.h"
#include "core/column/column_string.h"
+#include "core/data_type_serde/decoded_column_view.h"
#include "core/data_type/define_primitive_type.h"
#include "util/jsonb_document_cast.h"
#include "util/jsonb_utils.h"
#include "util/jsonb_writer.h"
namespace doris {
+namespace {
+
+template <typename ColumnType>
+Status read_string_decoded_values(IColumn& column, const DecodedColumnView& view) {
+ if (view.binary_values == nullptr && view.row_count > 0) {
+ return Status::Corruption("Decoded binary values are null for {}", column.get_name());
+ }
+ auto& string_column = assert_cast<ColumnType&>(column);
+ for (int64_t row = 0; row < view.row_count; ++row) {
+ const auto& value = (*view.binary_values)[row];
+ string_column.insert_data(value.data, value.size);
+ }
+ return Status::OK();
+}
+
+} // namespace
template <typename ColumnType>
Status DataTypeStringSerDeBase<ColumnType>::serialize_column_to_json(const IColumn& column,
@@ -313,6 +330,17 @@ Status DataTypeStringSerDeBase<ColumnType>::read_column_from_arrow(
return Status::OK();
}
+template <typename ColumnType>
+Status DataTypeStringSerDeBase<ColumnType>::read_column_from_decoded_values(
+ IColumn& column, const DecodedColumnView& view) const {
+ if (view.value_kind != DecodedValueKind::BINARY &&
+ view.value_kind != DecodedValueKind::FIXED_BINARY) {
+ return Status::NotSupported("Unsupported decoded values for {} from source kind {}",
+ get_name(), static_cast<int>(view.value_kind));
+ }
+ return read_string_decoded_values<ColumnType>(column, view);
+}
+
template <typename ColumnType>
Status DataTypeStringSerDeBase<ColumnType>::write_column_to_orc(
const std::string& timezone, const IColumn& column, const NullMap* null_map,
diff --git a/be/src/core/data_type_serde/data_type_string_serde.h b/be/src/core/data_type_serde/data_type_string_serde.h
index 79c8450835d..81b80eab4a5 100644
--- a/be/src/core/data_type_serde/data_type_string_serde.h
+++ b/be/src/core/data_type_serde/data_type_string_serde.h
@@ -203,6 +203,9 @@ public:
Status read_column_from_arrow(IColumn& column, const arrow::Array* arrow_array, int64_t start,
int64_t end, const cctz::time_zone& ctz) const override;
+ Status read_column_from_decoded_values(IColumn& column,
+ const DecodedColumnView& view) const override;
+
Status write_column_to_mysql_binary(const IColumn& column, MysqlRowBinaryBuffer& result,
int64_t row_idx, bool col_const,
const FormatOptions& options) const override {
diff --git a/be/src/core/data_type_serde/data_type_time_serde.cpp b/be/src/core/data_type_serde/data_type_time_serde.cpp
index e57fd08a271..65e1afa577d 100644
--- a/be/src/core/data_type_serde/data_type_time_serde.cpp
+++ b/be/src/core/data_type_serde/data_type_time_serde.cpp
@@ -17,6 +17,7 @@
#include "core/data_type_serde/data_type_time_serde.h"
+#include "core/data_type_serde/decoded_column_view.h"
#include "core/data_type/data_type_decimal.h"
#include "core/data_type/data_type_number.h"
#include "core/data_type/primitive_type.h"
@@ -25,6 +26,33 @@
#include "exprs/function/cast/cast_to_time_impl.hpp"
namespace doris {
+namespace {
+
+TimeValue::TimeType read_time_decoded_value(const DecodedColumnView& view, int64_t row) {
+ int64_t micros = 0;
+ if (view.value_kind == DecodedValueKind::INT32) {
+ const auto* values = reinterpret_cast<const int32_t*>(view.values);
+ micros = static_cast<int64_t>(values[row]) * 1000;
+ } else {
+ const auto* values = reinterpret_cast<const int64_t*>(view.values);
+ micros = values[row];
+ if (view.time_unit == DecodedTimeUnit::MILLIS) {
+ micros *= 1000;
+ } else if (view.time_unit == DecodedTimeUnit::NANOS) {
+ micros /= 1000;
+ }
+ }
+ const bool negative = micros < 0;
+ const int64_t abs_micros = std::abs(micros);
+ return TimeValue::make_time(abs_micros / TimeValue::ONE_HOUR_MICROSECONDS,
+ (abs_micros % TimeValue::ONE_HOUR_MICROSECONDS) /
+ TimeValue::ONE_MINUTE_MICROSECONDS,
+ (abs_micros % TimeValue::ONE_MINUTE_MICROSECONDS) /
+ TimeValue::ONE_SECOND_MICROSECONDS,
+ abs_micros % TimeValue::ONE_SECOND_MICROSECONDS, negative);
+}
+
+} // namespace
Status DataTypeTimeV2SerDe::write_column_to_mysql_binary(const IColumn& column,
MysqlRowBinaryBuffer& result,
@@ -145,6 +173,22 @@ Status DataTypeTimeV2SerDe::from_string_strict_mode(StringRef& str, IColumn& col
return Status::OK();
}
+Status DataTypeTimeV2SerDe::read_column_from_decoded_values(
+ IColumn& column, const DecodedColumnView& view) const {
+ if (view.value_kind != DecodedValueKind::INT32 &&
+ view.value_kind != DecodedValueKind::INT64) {
+ return Status::NotSupported("TIMEV2 decoded reader expects INT32 or INT64 source");
+ }
+ if (view.values == nullptr && view.row_count > 0) {
+ return Status::Corruption("Decoded value buffer is null for {}", column.get_name());
+ }
+ auto& data = assert_cast<ColumnTimeV2&>(column).get_data();
+ for (int64_t row = 0; row < view.row_count; ++row) {
+ data.push_back(read_time_decoded_value(view, row));
+ }
+ return Status::OK();
+}
+
template <typename IntDataType>
Status DataTypeTimeV2SerDe::from_int_batch(const typename IntDataType::ColumnType& int_col,
ColumnNullable& target_col) const {
diff --git a/be/src/core/data_type_serde/data_type_time_serde.h b/be/src/core/data_type_serde/data_type_time_serde.h
index db703616b49..e3fccf379c9 100644
--- a/be/src/core/data_type_serde/data_type_time_serde.h
+++ b/be/src/core/data_type_serde/data_type_time_serde.h
@@ -67,6 +67,8 @@ public:
template <typename DecimalDataType>
Status from_decimal_strict_mode_batch(const typename DecimalDataType::ColumnType& decimal_col,
IColumn& target_col) const;
+ Status read_column_from_decoded_values(IColumn& column,
+ const DecodedColumnView& view) const override;
int get_scale() const override { return _scale; }
protected:
diff --git a/be/src/core/data_type_serde/decoded_column_view.h b/be/src/core/data_type_serde/decoded_column_view.h
new file mode 100644
index 00000000000..9b0b14b17c7
--- /dev/null
+++ b/be/src/core/data_type_serde/decoded_column_view.h
@@ -0,0 +1,62 @@
+// 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.
+
+#pragma once
+
+#include <cstddef>
+#include <cstdint>
+#include <vector>
+
+#include "common/status.h"
+#include "core/string_ref.h"
+
+namespace doris {
+
+class IColumn;
+
+// 已解码 column batch 的物理值来源类型。
+// 该枚举只描述通用内存布局,不包含 Parquet/ORC/Arrow 等格式专有类型。
+enum class DecodedValueKind {
+ BOOL,
+ INT32,
+ INT64,
+ FLOAT,
+ DOUBLE,
+ BINARY,
+ FIXED_BINARY,
+};
+
+enum class DecodedTimeUnit {
+ UNKNOWN,
+ MILLIS,
+ MICROS,
+ NANOS,
+};
+
+struct DecodedColumnView {
+ DecodedValueKind value_kind = DecodedValueKind::INT32;
+ DecodedTimeUnit time_unit = DecodedTimeUnit::UNKNOWN;
+ int64_t row_count = 0;
+ int decimal_precision = -1;
+ int decimal_scale = -1;
+ int fixed_length = -1;
+ const uint8_t* values = nullptr;
+ const uint8_t* null_map = nullptr;
+ const std::vector<StringRef>* binary_values = nullptr;
+};
+
+} // namespace doris
diff --git a/be/src/exprs/vslot_ref.h b/be/src/exprs/vslot_ref.h
index ceb702728eb..6e7197f4cf6 100644
--- a/be/src/exprs/vslot_ref.h
+++ b/be/src/exprs/vslot_ref.h
@@ -67,7 +67,7 @@ public:
column_ids.insert(_column_id);
}
- MOCK_FUNCTION const std::string& column_name() const { return *_column_name; }
+ virtual const std::string& column_name() const { return *_column_name; }
uint64_t get_digest(uint64_t seed) const override;
diff --git a/be/src/format/new_parquet/column_reader.cpp b/be/src/format/new_parquet/column_reader.cpp
new file mode 100644
index 00000000000..bb101b5c5fc
--- /dev/null
+++ b/be/src/format/new_parquet/column_reader.cpp
@@ -0,0 +1,562 @@
+// 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/new_parquet/column_reader.h"
+
+#include <arrow/array/array_binary.h>
+#include <parquet/api/reader.h>
+#include <parquet/api/schema.h>
+
+#include <algorithm>
+#include <cstddef>
+#include <cstdint>
+#include <exception>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "core/column/column.h"
+#include "core/column/column_struct.h"
+#include "core/data_type/data_type_nullable.h"
+#include "core/data_type_serde/decoded_column_view.h"
+#include "format/new_parquet/parquet_column_schema.h"
+
+namespace doris::parquet {
+namespace {
+
+class ScalarColumnReader final : public ParquetColumnReader {
+public:
+ ScalarColumnReader(int parquet_leaf_column_id, const ::parquet::ColumnDescriptor* descriptor,
+ ParquetTypeDescriptor type_descriptor, DataTypePtr type, std::string name,
+ std::shared_ptr<::parquet::internal::RecordReader> record_reader)
+ : _file_column_id(parquet_leaf_column_id),
+ _parquet_leaf_column_id(parquet_leaf_column_id),
+ _descriptor(descriptor),
+ _type_descriptor(std::move(type_descriptor)),
+ _type(std::move(type)),
+ _name(std::move(name)),
+ _record_reader(std::move(record_reader)) {}
+
+ int file_column_id() const override { return _file_column_id; }
+ int parquet_leaf_column_id() const override { return _parquet_leaf_column_id; }
+ const DataTypePtr& type() const override { return _type; }
+ const std::string& name() const override { return _name; }
+
+ Status read(int64_t rows, MutableColumnPtr& column, int64_t* rows_read) override;
+ Status skip(int64_t rows) override;
+
+ const ::parquet::ColumnDescriptor* descriptor() const { return _descriptor; }
+ const std::shared_ptr<::parquet::internal::RecordReader>& record_reader() const {
+ return _record_reader;
+ }
+
+private:
+ int _file_column_id = -1;
+ int _parquet_leaf_column_id = -1;
+ const ::parquet::ColumnDescriptor* _descriptor = nullptr;
+ ParquetTypeDescriptor _type_descriptor;
+ DataTypePtr _type;
+ std::string _name;
+ std::shared_ptr<::parquet::internal::RecordReader> _record_reader;
+};
+
+class StructColumnReader final : public ParquetColumnReader {
+public:
+ StructColumnReader(const ParquetColumnSchema& schema,
+ std::vector<std::unique_ptr<ParquetColumnReader>> children)
+ : _field_id(schema.field_id),
+ _type(schema.type),
+ _name(schema.name),
+ _children(std::move(children)) {}
+
+ int file_column_id() const override { return _field_id; }
+ int parquet_leaf_column_id() const override { return -1; }
+ const DataTypePtr& type() const override { return _type; }
+ const std::string& name() const override { return _name; }
+
+ Status read(int64_t rows, MutableColumnPtr& column, int64_t* rows_read) override;
+ Status skip(int64_t rows) override;
+
+private:
+ int _field_id = -1;
+ DataTypePtr _type;
+ std::string _name;
+ std::vector<std::unique_ptr<ParquetColumnReader>> _children;
+};
+
+Status read_records(ScalarColumnReader& column_reader, int64_t batch_rows,
+ ::parquet::internal::RecordReader** record_reader, int64_t* rows_read) {
+ auto reader = column_reader.record_reader();
+ if (reader == nullptr) {
+ return Status::InternalError("Parquet record reader is not initialized for column {}",
+ column_reader.name());
+ }
+
+ int64_t records_read = 0;
+ try {
+ reader->Reset();
+ reader->Reserve(batch_rows);
+ records_read = reader->ReadRecords(batch_rows);
+ } catch (const ::parquet::ParquetException& e) {
+ return Status::Corruption("Failed to read parquet records for column {}: {}",
+ column_reader.name(), e.what());
+ } catch (const std::exception& e) {
+ return Status::InternalError("Failed to read parquet records for column {}: {}",
+ column_reader.name(), e.what());
+ }
+ if (records_read < 0 || records_read > batch_rows) {
+ return Status::Corruption("Invalid parquet record read result for column {}: {}",
+ column_reader.name(), records_read);
+ }
+ *record_reader = reader.get();
+ *rows_read = records_read;
+ return Status::OK();
+}
+
+struct RowRange {
+ int64_t start = 0;
+ int64_t length = 0;
+};
+
+std::vector<RowRange> selection_to_ranges(const SelectionVector& selection,
+ uint16_t selected_rows) {
+ std::vector<RowRange> ranges;
+ if (selected_rows == 0) {
+ return ranges;
+ }
+
+ int64_t range_start = selection.get_index(0);
+ int64_t previous = selection.get_index(0);
+ for (uint16_t selection_idx = 1; selection_idx < selected_rows; ++selection_idx) {
+ const int64_t current = selection.get_index(selection_idx);
+ DCHECK_GT(current, previous);
+ if (current == previous + 1) {
+ previous = current;
+ continue;
+ }
+ ranges.push_back(RowRange {range_start, previous - range_start + 1});
+ range_start = current;
+ previous = current;
+ }
+ ranges.push_back(RowRange {range_start, previous - range_start + 1});
+ return ranges;
+}
+
+DecodedTimeUnit decoded_time_unit(ParquetTimeUnit time_unit) {
+ switch (time_unit) {
+ case ParquetTimeUnit::MILLIS:
+ return DecodedTimeUnit::MILLIS;
+ case ParquetTimeUnit::MICROS:
+ return DecodedTimeUnit::MICROS;
+ case ParquetTimeUnit::NANOS:
+ return DecodedTimeUnit::NANOS;
+ case ParquetTimeUnit::UNKNOWN:
+ default:
+ return DecodedTimeUnit::UNKNOWN;
+ }
+}
+
+DecodedValueKind decoded_value_kind(const ParquetTypeDescriptor& type_descriptor) {
+ switch (type_descriptor.physical_type) {
+ case ::parquet::Type::BOOLEAN:
+ return DecodedValueKind::BOOL;
+ case ::parquet::Type::INT32:
+ return DecodedValueKind::INT32;
+ case ::parquet::Type::INT64:
+ return DecodedValueKind::INT64;
+ case ::parquet::Type::FLOAT:
+ return DecodedValueKind::FLOAT;
+ case ::parquet::Type::DOUBLE:
+ return DecodedValueKind::DOUBLE;
+ case ::parquet::Type::FIXED_LEN_BYTE_ARRAY:
+ return DecodedValueKind::FIXED_BINARY;
+ case ::parquet::Type::BYTE_ARRAY:
+ default:
+ return DecodedValueKind::BINARY;
+ }
+}
+
+Status build_null_map(const ScalarColumnReader& column_reader,
+ ::parquet::internal::RecordReader& record_reader, int64_t records_read,
+ NullMap* null_map) {
+ if (column_reader.descriptor()->max_definition_level() == 0) {
+ return Status::OK();
+ }
+ if (record_reader.read_dense_for_nullable()) {
+ return Status::NotSupported(
+ "Dense nullable parquet record reader is not supported for column {}",
+ column_reader.name());
+ }
+ auto* def_levels = record_reader.def_levels();
+ if (def_levels == nullptr && records_read > 0) {
+ return Status::Corruption(
+ "Parquet record reader returned null definition levels for nullable column {}",
+ column_reader.name());
+ }
+ const int16_t max_definition_level = column_reader.descriptor()->max_definition_level();
+ null_map->resize(records_read);
+ auto* __restrict dst = null_map->data();
+ const auto* __restrict src = def_levels;
+ for (int64_t record_idx = 0; record_idx < records_read; ++record_idx) {
+ dst[record_idx] = src[record_idx] != max_definition_level;
+ }
+ return Status::OK();
+}
+
+Status get_binary_chunks(const ScalarColumnReader& column_reader,
+ ::parquet::internal::RecordReader& record_reader,
+ std::vector<std::shared_ptr<::arrow::Array>>* chunks) {
+ auto* binary_reader = dynamic_cast<::parquet::internal::BinaryRecordReader*>(&record_reader);
+ if (binary_reader == nullptr) {
+ return Status::InternalError("Parquet binary record reader is not available for column {}",
+ column_reader.name());
+ }
+ *chunks = binary_reader->GetBuilderChunks();
+ return Status::OK();
+}
+
+Status build_binary_values(const ScalarColumnReader& column_reader,
+ const std::vector<std::shared_ptr<::arrow::Array>>& chunks,
+ int64_t records_read, std::vector<StringRef>* binary_values) {
+ binary_values->reserve(records_read);
+ for (const auto& chunk : chunks) {
+ if (chunk == nullptr) {
+ return Status::Corruption(
+ "Parquet binary record reader returned null chunk for column {}",
+ column_reader.name());
+ }
+ if (auto* binary_array = dynamic_cast<::arrow::BinaryArray*>(chunk.get())) {
+ for (int64_t row_idx = 0; row_idx < binary_array->length(); ++row_idx) {
+ if (binary_array->IsNull(row_idx)) {
+ binary_values->emplace_back(static_cast<const char*>(nullptr), 0);
+ continue;
+ }
+ int32_t length = 0;
+ const uint8_t* value = binary_array->GetValue(row_idx, &length);
+ binary_values->emplace_back(reinterpret_cast<const char*>(value), length);
+ }
+ } else if (auto* fixed_array = dynamic_cast<::arrow::FixedSizeBinaryArray*>(chunk.get())) {
+ for (int64_t row_idx = 0; row_idx < fixed_array->length(); ++row_idx) {
+ if (fixed_array->IsNull(row_idx)) {
+ binary_values->emplace_back(static_cast<const char*>(nullptr), 0);
+ continue;
+ }
+ binary_values->emplace_back(
+ reinterpret_cast<const char*>(fixed_array->GetValue(row_idx)),
+ fixed_array->byte_width());
+ }
+ } else {
+ return Status::InternalError("Unexpected Arrow binary array type for column {}",
+ column_reader.name());
+ }
+ }
+ if (binary_values->size() != static_cast<size_t>(records_read)) {
+ return Status::Corruption(
+ "Invalid parquet binary record read result for column {}: rows={}, records={}",
+ column_reader.name(), binary_values->size(), records_read);
+ }
+ return Status::OK();
+}
+
+} // namespace
+
+Status ScalarColumnReader::read(int64_t rows, MutableColumnPtr& column, int64_t* rows_read) {
+ if (column.get() == nullptr || rows_read == nullptr) {
+ return Status::InvalidArgument("Invalid parquet column read result pointer for column {}",
+ _name);
+ }
+ if (_record_reader == nullptr) {
+ return Status::InternalError("Parquet record reader is not initialized for column {}",
+ _name);
+ }
+ ::parquet::internal::RecordReader* record_reader = nullptr;
+ RETURN_IF_ERROR(read_records(*this, rows, &record_reader, rows_read));
+ if (record_reader->values_written() != *rows_read) {
+ return Status::Corruption(
+ "Invalid parquet record read result for column {}: values={}, records={}", _name,
+ record_reader->values_written(), *rows_read);
+ }
+
+ NullMap null_map;
+ RETURN_IF_ERROR(build_null_map(*this, *record_reader, *rows_read, &null_map));
+
+ std::vector<StringRef> binary_values;
+ std::vector<std::shared_ptr<::arrow::Array>> binary_chunks;
+ DecodedColumnView view;
+ view.value_kind = decoded_value_kind(_type_descriptor);
+ view.time_unit = decoded_time_unit(_type_descriptor.time_unit);
+ view.row_count = *rows_read;
+ view.decimal_precision = _type_descriptor.decimal_precision;
+ view.decimal_scale = _type_descriptor.decimal_scale;
+ view.fixed_length = _type_descriptor.fixed_length;
+ view.null_map = null_map.empty() ? nullptr : null_map.data();
+ if (view.value_kind == DecodedValueKind::BINARY ||
+ view.value_kind == DecodedValueKind::FIXED_BINARY) {
+ RETURN_IF_ERROR(get_binary_chunks(*this, *record_reader, &binary_chunks));
+ RETURN_IF_ERROR(build_binary_values(*this, binary_chunks, *rows_read, &binary_values));
+ view.binary_values = &binary_values;
+ } else {
+ view.values = record_reader->values();
+ }
+
+ RETURN_IF_ERROR(_type->get_serde()->read_column_from_decoded_values(*column, view));
+ return Status::OK();
+}
+
+Status ScalarColumnReader::skip(int64_t rows) {
+ if (rows <= 0) {
+ return Status::OK();
+ }
+
+ if (_record_reader == nullptr) {
+ return Status::InternalError("Parquet record reader is not initialized for column {}",
+ _name);
+ }
+ int64_t skipped_rows = 0;
+ try {
+ _record_reader->Reset();
+ while (skipped_rows < rows) {
+ const int64_t skipped = _record_reader->SkipRecords(rows - skipped_rows);
+ if (skipped <= 0) {
+ return Status::Corruption(
+ "Failed to skip parquet records for column {}: skipped {} of {} rows",
+ _name, skipped_rows, rows);
+ }
+ skipped_rows += skipped;
+ }
+ } catch (const ::parquet::ParquetException& e) {
+ return Status::Corruption("Failed to skip parquet records for column {}: {}", _name,
+ e.what());
+ } catch (const std::exception& e) {
+ return Status::InternalError("Failed to skip parquet records for column {}: {}", _name,
+ e.what());
+ }
+ return Status::OK();
+}
+
+Status StructColumnReader::read(int64_t rows, MutableColumnPtr& column, int64_t* rows_read) {
+ if (column.get() == nullptr || rows_read == nullptr) {
+ return Status::InvalidArgument("Invalid parquet struct read result pointer for column {}",
+ _name);
+ }
+ if (_children.empty()) {
+ column->resize(static_cast<size_t>(rows));
+ *rows_read = rows;
+ return Status::OK();
+ }
+
+ int64_t expected_rows = -1;
+ size_t child_idx = 0;
+ DCHECK_EQ(assert_cast<ColumnStruct&>(*column).get_columns().size(), _children.size());
+ for (auto& child_reader : _children) {
+ int64_t child_rows = 0;
+ auto child_column =
+ assert_cast<ColumnStruct&>(*column).get_column_ptr(child_idx)->assume_mutable();
+ RETURN_IF_ERROR(child_reader->read(rows, child_column, &child_rows));
+ if (expected_rows < 0) {
+ expected_rows = child_rows;
+ } else if (child_rows != expected_rows) {
+ return Status::Corruption(
+ "Parquet struct children returned different row counts in column {}: {} vs {}",
+ _name, expected_rows, child_rows);
+ }
+ child_idx++;
+ }
+
+ *rows_read = std::max<int64_t>(expected_rows, 0);
+ return Status::OK();
+}
+
+Status StructColumnReader::skip(int64_t rows) {
+ if (rows <= 0) {
+ return Status::OK();
+ }
+ for (auto& child_reader : _children) {
+ RETURN_IF_ERROR(child_reader->skip(rows));
+ }
+ return Status::OK();
+}
+
+Status ParquetColumnReader::skip(int64_t rows) {
+ return Status::NotSupported("Parquet column skip is not implemented, rows={}", rows);
+}
+
+Status ParquetColumnReader::select(const SelectionVector& sel, uint16_t selected_rows,
+ int64_t batch_rows, MutableColumnPtr& column) {
+ if (column.get() == nullptr) {
+ return Status::InvalidArgument("Parquet selected read result is null for column {}",
+ name());
+ }
+ RETURN_IF_ERROR(sel.verify(selected_rows, batch_rows));
+
+ const auto ranges = selection_to_ranges(sel, selected_rows);
+ int64_t cursor = 0;
+ for (const auto& range : ranges) {
+ if (range.start < cursor || range.start + range.length > batch_rows) {
+ return Status::InvalidArgument("Invalid parquet selection range [{}, {}) for column {}",
+ range.start, range.start + range.length, name());
+ }
+ RETURN_IF_ERROR(skip(range.start - cursor));
+
+ int64_t range_rows_read = 0;
+ RETURN_IF_ERROR(read(range.length, column, &range_rows_read));
+ if (range_rows_read != range.length) {
+ return Status::Corruption(
+ "Parquet selected read returned {} rows, expected {} rows for column {}",
+ range_rows_read, range.length, name());
+ }
+ cursor = range.start + range.length;
+ }
+ RETURN_IF_ERROR(skip(batch_rows - cursor));
+ return Status::OK();
+}
+
+ParquetColumnReaderFactory::ParquetColumnReaderFactory(
+ std::shared_ptr<::parquet::RowGroupReader> row_group, int num_leaf_columns)
+ : _row_group(std::move(row_group)),
+ _record_readers(static_cast<size_t>(num_leaf_columns)) {}
+
+Status ParquetColumnReaderFactory::create_scalar_reader(
+ int parquet_leaf_column_id, const ParquetTypeDescriptor& type_descriptor,
+ const ::parquet::ColumnDescriptor* descriptor, DataTypePtr type, std::string name,
+ std::shared_ptr<::parquet::internal::RecordReader> record_reader,
+ std::unique_ptr<ParquetColumnReader>* reader) const {
+ if (reader == nullptr) {
+ return Status::InvalidArgument("reader is null");
+ }
+ if (descriptor == nullptr || type == nullptr || record_reader == nullptr) {
+ return Status::InvalidArgument("Invalid parquet column reader arguments for column {}",
+ name);
+ }
+ *reader = std::make_unique<ScalarColumnReader>(parquet_leaf_column_id, descriptor,
+ type_descriptor, std::move(type),
+ std::move(name), std::move(record_reader));
+ return Status::OK();
+}
+
+Status ParquetColumnReaderFactory::create_scalar_column_reader(
+ const ParquetColumnSchema& column_schema,
+ std::unique_ptr<ParquetColumnReader>* reader) const {
+ if (reader == nullptr) {
+ return Status::InvalidArgument("reader is null");
+ }
+ if (column_schema.leaf_column_id < 0 ||
+ column_schema.leaf_column_id >= static_cast<int>(_record_readers.size())) {
+ return Status::InvalidArgument("Invalid parquet leaf column id {} for column {}",
+ column_schema.leaf_column_id, column_schema.name);
+ }
+ if (!supports_record_reader(column_schema.type_descriptor)) {
+ return Status::NotSupported(
+ "Current parquet reader only supports primitive columns without repetition; "
+ "column {} is not supported",
+ column_schema.name);
+ }
+ std::shared_ptr<::parquet::internal::RecordReader> record_reader;
+ RETURN_IF_ERROR(get_record_reader(column_schema.leaf_column_id, column_schema.descriptor,
+ column_schema.name, &record_reader));
+ return create_scalar_reader(column_schema.leaf_column_id, column_schema.type_descriptor,
+ column_schema.descriptor, column_schema.type, column_schema.name,
+ std::move(record_reader), reader);
+}
+
+Status ParquetColumnReaderFactory::get_record_reader(
+ int leaf_column_id, const ::parquet::ColumnDescriptor* descriptor, const std::string& name,
+ std::shared_ptr<::parquet::internal::RecordReader>* reader) const {
+ if (reader == nullptr) {
+ return Status::InvalidArgument("reader is null");
+ }
+ if (_row_group == nullptr) {
+ return Status::InternalError("Parquet row group reader is not initialized for column {}",
+ name);
+ }
+ if (leaf_column_id < 0 || leaf_column_id >= static_cast<int>(_record_readers.size())) {
+ return Status::InvalidArgument("Invalid parquet leaf column id {} for column {}",
+ leaf_column_id, name);
+ }
+ if (descriptor == nullptr) {
+ return Status::InvalidArgument("Parquet column descriptor is null for column {}", name);
+ }
+ if (descriptor->max_repetition_level() != 0 || descriptor->max_definition_level() > 1) {
+ return Status::NotSupported(
+ "Current parquet reader only supports RecordReader-backed columns; column {} is "
+ "not supported",
+ name);
+ }
+ if (_record_readers[leaf_column_id] == nullptr) {
+ try {
+ _record_readers[leaf_column_id] =
+ _row_group->RecordReader(leaf_column_id, /*read_dictionary=*/false);
+ } catch (const ::parquet::ParquetException& e) {
+ return Status::Corruption("Failed to create parquet record reader for column {}: {}",
+ name, e.what());
+ } catch (const std::exception& e) {
+ return Status::InternalError("Failed to create parquet record reader for column {}: {}",
+ name, e.what());
+ }
+ }
+ if (_record_readers[leaf_column_id] == nullptr) {
+ return Status::Corruption("Failed to create parquet record reader for column {}", name);
+ }
+ *reader = _record_readers[leaf_column_id];
+ return Status::OK();
+}
+
+Status ParquetColumnReaderFactory::create_struct_column_reader(
+ const ParquetColumnSchema& column_schema,
+ std::unique_ptr<ParquetColumnReader>* reader) const {
+ if (reader == nullptr) {
+ return Status::InvalidArgument("reader is null");
+ }
+ if (column_schema.type != nullptr && column_schema.type->is_nullable()) {
+ return Status::NotSupported(
+ "Nullable parquet STRUCT reader is not implemented for column {}",
+ column_schema.name);
+ }
+ std::vector<std::unique_ptr<ParquetColumnReader>> child_readers;
+ child_readers.reserve(column_schema.children.size());
+ for (const auto& child_schema : column_schema.children) {
+ std::unique_ptr<ParquetColumnReader> child_reader;
+ RETURN_IF_ERROR(create(*child_schema, &child_reader));
+ child_readers.push_back(std::move(child_reader));
+ }
+ *reader = std::make_unique<StructColumnReader>(column_schema, std::move(child_readers));
+ return Status::OK();
+}
+
+Status ParquetColumnReaderFactory::create(const ParquetColumnSchema& column_schema,
+ std::unique_ptr<ParquetColumnReader>* reader) const {
+ if (reader == nullptr) {
+ return Status::InvalidArgument("reader is null");
+ }
+ switch (column_schema.kind) {
+ case ParquetColumnSchemaKind::PRIMITIVE:
+ return create_scalar_column_reader(column_schema, reader);
+ case ParquetColumnSchemaKind::STRUCT:
+ return create_struct_column_reader(column_schema, reader);
+ case ParquetColumnSchemaKind::LIST:
+ return Status::NotSupported("Parquet LIST reader is not implemented for column {}",
+ column_schema.name);
+ case ParquetColumnSchemaKind::MAP:
+ return Status::NotSupported("Parquet MAP reader is not implemented for column {}",
+ column_schema.name);
+ }
+ return Status::NotSupported("Unsupported parquet column schema kind for column {}",
+ column_schema.name);
+}
+
+} // namespace doris::parquet
diff --git a/be/src/format/new_parquet/column_reader.h b/be/src/format/new_parquet/column_reader.h
new file mode 100644
index 00000000000..cd59fc3960a
--- /dev/null
+++ b/be/src/format/new_parquet/column_reader.h
@@ -0,0 +1,116 @@
+// 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.
+
+#pragma once
+
+#include <cstdint>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "common/status.h"
+#include "core/data_type/data_type.h"
+#include "format/new_parquet/parquet_type.h"
+#include "format/new_parquet/selection_vector.h"
+
+namespace parquet {
+class ColumnDescriptor;
+class RowGroupReader;
+
+namespace internal {
+class RecordReader;
+} // namespace internal
+} // namespace parquet
+
+namespace doris {
+class IColumn;
+
+namespace parquet {
+struct ParquetColumnSchema;
+
+// Doris 的 Parquet column reader 抽象。
+// 该类包装 Arrow Parquet RecordReader,负责将 file-local Parquet leaf column 读取成
+// Doris-owned column。它不理解 Iceberg/global schema,也不处理 table-level
+// cast/default/generated/partition 语义。
+class ParquetColumnReader {
+public:
+ virtual ~ParquetColumnReader() = default;
+
+ // FileReader 暴露给上层 scan request 的 file-local column id。
+ // 对 top-level primitive 列,它通常等于 Parquet leaf column id;对 struct/list/map
+ // 这类复杂列,它表示 file schema tree 中的逻辑字段 id。
+ virtual int file_column_id() const = 0;
+
+ // Parquet 文件内部的 leaf column id,用于访问 RowGroupReader::RecordReader、
+ // ColumnChunk metadata、statistics/page index 等 Parquet 物理列结构。
+ // 只有 primitive leaf reader 有有效值;复杂列 reader 没有单一 leaf column,返回 -1。
+ virtual int parquet_leaf_column_id() const = 0;
+
+ virtual const DataTypePtr& type() const = 0;
+ virtual const std::string& name() const = 0;
+
+ // 读取一个 file-local column batch。
+ virtual Status read(int64_t rows, MutableColumnPtr& column, int64_t* rows_read) = 0;
+
+ // 跳过指定行数。这里必须使用 row-level skip,不能退回到 value-level Skip。
+ virtual Status skip(int64_t rows);
+
+ // 按 selection 读取当前 batch 中需要输出的行,并在末尾跳过 batch 内剩余行。
+ // 该方法只允许通过 skip + read 推进 reader 游标,不允许退化为整批 read + filter。
+ virtual Status select(const SelectionVector& sel, uint16_t selected_rows, int64_t batch_rows,
+ MutableColumnPtr& column);
+};
+
+// Parquet column reader 工厂。
+// 工厂绑定当前 row group,并根据 file-local schema tree 创建 Doris 自己的 column
+// reader。Arrow internal RecordReader 的创建和缓存必须封装在这里,避免泄露到
+// ParquetReader 主流程。后续 reader options、Dremel assembler、延时物化 cache/skip
+// 策略都应挂在该工厂上下文里,而不是继续扩展自由函数参数。
+class ParquetColumnReaderFactory {
+public:
+ ParquetColumnReaderFactory(std::shared_ptr<::parquet::RowGroupReader> row_group,
+ int num_leaf_columns);
+
+ // 根据 file-local schema tree 创建 column reader。复杂类型会在这里递归创建
+ // children。该入口只理解 Parquet file schema,不处理 table/global schema。
+ Status create(const ParquetColumnSchema& column_schema,
+ std::unique_ptr<ParquetColumnReader>* reader) const;
+
+private:
+ Status create_scalar_column_reader(const ParquetColumnSchema& column_schema,
+ std::unique_ptr<ParquetColumnReader>* reader) const;
+
+ Status create_struct_column_reader(const ParquetColumnSchema& column_schema,
+ std::unique_ptr<ParquetColumnReader>* reader) const;
+
+ Status get_record_reader(int leaf_column_id, const ::parquet::ColumnDescriptor* descriptor,
+ const std::string& name,
+ std::shared_ptr<::parquet::internal::RecordReader>* reader) const;
+
+ Status create_scalar_reader(int parquet_leaf_column_id,
+ const ParquetTypeDescriptor& type_descriptor,
+ const ::parquet::ColumnDescriptor* descriptor, DataTypePtr type,
+ std::string name,
+ std::shared_ptr<::parquet::internal::RecordReader> record_reader,
+ std::unique_ptr<ParquetColumnReader>* reader) const;
+
+ std::shared_ptr<::parquet::RowGroupReader> _row_group;
+ mutable std::vector<std::shared_ptr<::parquet::internal::RecordReader>> _record_readers;
+};
+
+} // namespace parquet
+} // namespace doris
diff --git a/be/src/format/new_parquet/parquet_column_schema.cpp b/be/src/format/new_parquet/parquet_column_schema.cpp
new file mode 100644
index 00000000000..131bf9f22c0
--- /dev/null
+++ b/be/src/format/new_parquet/parquet_column_schema.cpp
@@ -0,0 +1,158 @@
+// 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/new_parquet/parquet_column_schema.h"
+
+#include <parquet/api/schema.h>
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "core/data_type/data_type_array.h"
+#include "core/data_type/data_type_map.h"
+#include "core/data_type/data_type_nullable.h"
+#include "core/data_type/data_type_struct.h"
+#include "format/new_parquet/parquet_type.h"
+
+namespace doris::parquet {
+namespace {
+
+bool is_list_node(const ::parquet::schema::Node& node) {
+ const auto& logical_type = node.logical_type();
+ return node.converted_type() == ::parquet::ConvertedType::LIST ||
+ (logical_type != nullptr && logical_type->is_valid() && logical_type->is_list());
+}
+
+bool is_map_node(const ::parquet::schema::Node& node) {
+ const auto& logical_type = node.logical_type();
+ return node.converted_type() == ::parquet::ConvertedType::MAP ||
+ node.converted_type() == ::parquet::ConvertedType::MAP_KEY_VALUE ||
+ (logical_type != nullptr && logical_type->is_valid() && logical_type->is_map());
+}
+
+DataTypePtr nullable_if_needed(DataTypePtr type, const ::parquet::schema::Node& node) {
+ return node.is_optional() ? make_nullable(type) : type;
+}
+
+Status build_node_schema(const ::parquet::SchemaDescriptor& schema,
+ const ::parquet::schema::Node& node,
+ std::unique_ptr<ParquetColumnSchema>* result) {
+ if (result == nullptr) {
+ return Status::InvalidArgument("result is null");
+ }
+ auto column_schema = std::make_unique<ParquetColumnSchema>();
+ column_schema->field_id = node.field_id();
+ column_schema->name = node.name();
+ column_schema->node = &node;
+
+ if (node.is_primitive()) {
+ const int leaf_column_id = schema.ColumnIndex(node);
+ if (leaf_column_id < 0) {
+ return Status::InvalidArgument("Cannot find leaf column id for parquet column {}",
+ node.name());
+ }
+ column_schema->kind = ParquetColumnSchemaKind::PRIMITIVE;
+ column_schema->leaf_column_id = leaf_column_id;
+ column_schema->descriptor = schema.Column(leaf_column_id);
+ column_schema->type_descriptor = resolve_parquet_type(column_schema->descriptor);
+ column_schema->type = column_schema->type_descriptor.doris_type;
+ if (column_schema->type == nullptr) {
+ return Status::NotSupported("Unsupported parquet column type for column {}",
+ node.name());
+ }
+ *result = std::move(column_schema);
+ return Status::OK();
+ }
+
+ const auto& group = static_cast<const ::parquet::schema::GroupNode&>(node);
+ if (is_list_node(node)) {
+ column_schema->kind = ParquetColumnSchemaKind::LIST;
+ if (group.field_count() != 1) {
+ return Status::NotSupported("Unsupported parquet LIST encoding for column {}",
+ node.name());
+ }
+ std::unique_ptr<ParquetColumnSchema> child;
+ RETURN_IF_ERROR(build_node_schema(schema, *group.field(0), &child));
+ column_schema->type =
+ nullable_if_needed(std::make_shared<DataTypeArray>(child->type), node);
+ column_schema->children.push_back(std::move(child));
+ *result = std::move(column_schema);
+ return Status::OK();
+ }
+
+ if (is_map_node(node)) {
+ column_schema->kind = ParquetColumnSchemaKind::MAP;
+ if (group.field_count() != 1) {
+ return Status::NotSupported("Unsupported parquet MAP encoding for column {}",
+ node.name());
+ }
+ std::unique_ptr<ParquetColumnSchema> key_value;
+ RETURN_IF_ERROR(build_node_schema(schema, *group.field(0), &key_value));
+ if (key_value->children.size() != 2) {
+ return Status::NotSupported("Unsupported parquet MAP key_value layout for column {}",
+ node.name());
+ }
+ auto key_type = key_value->children[0]->type;
+ auto value_type = key_value->children[1]->type;
+ column_schema->type =
+ nullable_if_needed(std::make_shared<DataTypeMap>(key_type, value_type), node);
+ column_schema->children.push_back(std::move(key_value));
+ *result = std::move(column_schema);
+ return Status::OK();
+ }
+
+ column_schema->kind = ParquetColumnSchemaKind::STRUCT;
+ DataTypes child_types;
+ Strings child_names;
+ child_types.reserve(group.field_count());
+ child_names.reserve(group.field_count());
+ for (int child_idx = 0; child_idx < group.field_count(); ++child_idx) {
+ std::unique_ptr<ParquetColumnSchema> child;
+ RETURN_IF_ERROR(build_node_schema(schema, *group.field(child_idx), &child));
+ child_types.push_back(child->type);
+ child_names.push_back(child->name);
+ column_schema->children.push_back(std::move(child));
+ }
+ column_schema->type =
+ nullable_if_needed(std::make_shared<DataTypeStruct>(child_types, child_names), node);
+ *result = std::move(column_schema);
+ return Status::OK();
+}
+
+} // namespace
+
+Status build_parquet_column_schema(const ::parquet::SchemaDescriptor& schema,
+ std::vector<std::unique_ptr<ParquetColumnSchema>>* fields) {
+ if (fields == nullptr) {
+ return Status::InvalidArgument("fields is null");
+ }
+ fields->clear();
+ const auto* root = schema.group_node();
+ if (root == nullptr) {
+ return Status::InvalidArgument("Parquet schema root is null");
+ }
+ fields->reserve(root->field_count());
+ for (int field_idx = 0; field_idx < root->field_count(); ++field_idx) {
+ std::unique_ptr<ParquetColumnSchema> field;
+ RETURN_IF_ERROR(build_node_schema(schema, *root->field(field_idx), &field));
+ fields->push_back(std::move(field));
+ }
+ return Status::OK();
+}
+
+} // namespace doris::parquet
diff --git a/be/src/format/new_parquet/parquet_column_schema.h b/be/src/format/new_parquet/parquet_column_schema.h
new file mode 100644
index 00000000000..0d089a0f9cd
--- /dev/null
+++ b/be/src/format/new_parquet/parquet_column_schema.h
@@ -0,0 +1,67 @@
+// 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.
+
+#pragma once
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "common/status.h"
+#include "core/data_type/data_type.h"
+#include "format/new_parquet/parquet_type.h"
+
+namespace parquet {
+class ColumnDescriptor;
+class SchemaDescriptor;
+
+namespace schema {
+class Node;
+} // namespace schema
+} // namespace parquet
+
+namespace doris::parquet {
+
+enum class ParquetColumnSchemaKind {
+ PRIMITIVE,
+ STRUCT,
+ LIST,
+ MAP,
+};
+
+// 新 Parquet reader 的 file-local schema tree。
+// 它描述 Parquet 逻辑字段到 leaf column ordinal 的关系,不包含 table/global schema 语义。
+struct ParquetColumnSchema {
+ int field_id = -1;
+ // Parquet schema 中的 primitive leaf column ordinal。
+ // 该 id 用于访问 ColumnDescriptor、RowGroupReader::RecordReader、ColumnChunk
+ // metadata 和 statistics。复杂类型节点本身没有单一 leaf column,因此为 -1。
+ int leaf_column_id = -1;
+ std::string name;
+ DataTypePtr type;
+ ParquetTypeDescriptor type_descriptor;
+ ParquetColumnSchemaKind kind = ParquetColumnSchemaKind::PRIMITIVE;
+ const ::parquet::schema::Node* node = nullptr;
+ const ::parquet::ColumnDescriptor* descriptor = nullptr;
+ std::vector<std::unique_ptr<ParquetColumnSchema>> children;
+};
+
+// 从 Arrow Parquet core schema 构造 file-local schema tree。
+Status build_parquet_column_schema(const ::parquet::SchemaDescriptor& schema,
+ std::vector<std::unique_ptr<ParquetColumnSchema>>* fields);
+
+} // namespace doris::parquet
diff --git a/be/src/format/new_parquet/parquet_reader.cpp b/be/src/format/new_parquet/parquet_reader.cpp
new file mode 100644
index 00000000000..fc00484758e
--- /dev/null
+++ b/be/src/format/new_parquet/parquet_reader.cpp
@@ -0,0 +1,601 @@
+// 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/new_parquet/parquet_reader.h"
+
+#include <arrow/buffer.h>
+#include <arrow/io/interfaces.h>
+#include <arrow/result.h>
+#include <parquet/api/reader.h>
+
+#include <algorithm>
+#include <memory>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+#include "common/exception.h"
+#include "core/block/block.h"
+#include "core/data_type/data_type_nullable.h"
+#include "format/new_parquet/column_reader.h"
+#include "format/new_parquet/parquet_column_schema.h"
+#include "format/new_parquet/parquet_statistics.h"
+#include "format/new_parquet/selection_vector.h"
+#include "io/fs/file_reader.h"
+#include "storage/predicate/column_predicate.h"
+#include "util/slice.h"
+
+namespace doris::parquet {
+
+constexpr int64_t DEFAULT_PARQUET_READ_BATCH_SIZE = 4096;
+
+Status arrow_status_to_doris_status(const arrow::Status& status) {
+ if (status.ok()) {
+ return Status::OK();
+ }
+ if (status.IsIOError()) {
+ return Status::IOError(status.ToString());
+ }
+ if (status.IsInvalid()) {
+ return Status::InvalidArgument(status.ToString());
+ }
+ return Status::InternalError(status.ToString());
+}
+
+class DorisRandomAccessFile final : public arrow::io::RandomAccessFile {
+public:
+ DorisRandomAccessFile(io::FileReaderSPtr file_reader, io::IOContext* io_ctx)
+ : _file_reader(std::move(file_reader)), _io_ctx(io_ctx) {
+ set_mode(arrow::io::FileMode::READ);
+ }
+
+ arrow::Status Close() override {
+ _closed = true;
+ return arrow::Status::OK();
+ }
+
+ bool closed() const override { return _closed; }
+
+ arrow::Result<int64_t> Tell() const override { return _pos; }
+
+ arrow::Status Seek(int64_t position) override {
+ if (position < 0) {
+ return arrow::Status::Invalid("negative seek position");
+ }
+ _pos = position;
+ return arrow::Status::OK();
+ }
+
+ arrow::Result<int64_t> GetSize() override {
+ if (!_file_reader) {
+ return arrow::Status::IOError("Doris file reader is not open");
+ }
+ return static_cast<int64_t>(_file_reader->size());
+ }
+
+ arrow::Result<int64_t> Read(int64_t nbytes, void* out) override {
+ ARROW_ASSIGN_OR_RAISE(auto bytes_read, ReadAt(_pos, nbytes, out));
+ _pos += bytes_read;
+ return bytes_read;
+ }
+
+ arrow::Result<std::shared_ptr<arrow::Buffer>> Read(int64_t nbytes) override {
+ ARROW_ASSIGN_OR_RAISE(auto buffer, arrow::AllocateResizableBuffer(nbytes));
+ ARROW_ASSIGN_OR_RAISE(auto bytes_read, Read(nbytes, buffer->mutable_data()));
+ ARROW_RETURN_NOT_OK(buffer->Resize(bytes_read, false));
+ buffer->ZeroPadding();
+ return buffer;
+ }
+
+ arrow::Result<int64_t> ReadAt(int64_t position, int64_t nbytes, void* out) override {
+ if (!_file_reader) {
+ return arrow::Status::IOError("Doris file reader is not open");
+ }
+ if (position < 0 || nbytes < 0) {
+ return arrow::Status::Invalid("negative read position or length");
+ }
+ size_t bytes_read = 0;
+ Status st = _file_reader->read_at(
+ static_cast<size_t>(position),
+ Slice(static_cast<uint8_t*>(out), static_cast<size_t>(nbytes)), &bytes_read,
+ _io_ctx);
+ if (!st.ok()) {
+ return arrow::Status::IOError(st.to_string_no_stack());
+ }
+ return static_cast<int64_t>(bytes_read);
+ }
+
+ arrow::Result<std::shared_ptr<arrow::Buffer>> ReadAt(int64_t position,
+ int64_t nbytes) override {
+ ARROW_ASSIGN_OR_RAISE(auto buffer, arrow::AllocateResizableBuffer(nbytes));
+ ARROW_ASSIGN_OR_RAISE(auto bytes_read, ReadAt(position, nbytes, buffer->mutable_data()));
+ ARROW_RETURN_NOT_OK(buffer->Resize(bytes_read, false));
+ buffer->ZeroPadding();
+ return buffer;
+ }
+
+private:
+ io::FileReaderSPtr _file_reader;
+ io::IOContext* _io_ctx = nullptr;
+ int64_t _pos = 0;
+ bool _closed = false;
+};
+
+struct ParquetReaderScanState {
+ // Doris 文件句柄适配成 Arrow RandomAccessFile。该对象只处理随机读,不携带
+ // table/global schema 语义。
+ std::shared_ptr<arrow::io::RandomAccessFile> arrow_file;
+
+ // Arrow Parquet core reader 和 footer metadata。ParquetReader 只依赖 core API,
+ // 不使用 parquet::arrow reader,也不输出 Arrow Array/RecordBatch。
+ std::unique_ptr<::parquet::ParquetFileReader> file_reader;
+ std::shared_ptr<::parquet::FileMetaData> metadata;
+ const ::parquet::SchemaDescriptor* schema = nullptr;
+ std::vector<std::unique_ptr<ParquetColumnSchema>> file_schema;
+
+ // 当前 scan 的 top-level file-local projection 和 row group 列表。projected_fields
+ // 决定输出 block;具体 leaf column reader 由 ParquetColumnReaderFactory 按需创建。
+ std::vector<int> predicate_fields;
+ std::vector<int> non_predicate_fields;
+ std::vector<int> selected_row_groups;
+ size_t next_row_group_idx = 0;
+ std::shared_ptr<::parquet::RowGroupReader> current_row_group;
+ std::vector<std::unique_ptr<ParquetColumnReader>> current_predicate_columns;
+ std::vector<std::unique_ptr<ParquetColumnReader>> current_non_predicate_columns;
+ int64_t current_row_group_rows = 0;
+ int64_t current_row_group_rows_read = 0;
+};
+
+Status ParquetReader::_reset_reader_position() {
+ _state->next_row_group_idx = 0;
+ _state->current_row_group.reset();
+ _state->current_predicate_columns.clear();
+ _state->current_non_predicate_columns.clear();
+ _state->current_row_group_rows = 0;
+ _state->current_row_group_rows_read = 0;
+ return Status::OK();
+}
+
+void ParquetReader::_reset_current_row_group() {
+ _state->current_row_group.reset();
+ _state->current_predicate_columns.clear();
+ _state->current_non_predicate_columns.clear();
+ _state->current_row_group_rows = 0;
+ _state->current_row_group_rows_read = 0;
+}
+
+void ParquetReader::_fill_schema_field(const ParquetColumnSchema& column_schema,
+ reader::SchemaField* field) const {
+ field->id = column_schema.leaf_column_id >= 0 ? column_schema.leaf_column_id
+ : column_schema.field_id;
+ field->name = column_schema.name;
+ field->type = column_schema.type;
+ field->children.clear();
+ field->children.reserve(column_schema.children.size());
+ for (const auto& child : column_schema.children) {
+ reader::SchemaField child_field;
+ _fill_schema_field(*child, &child_field);
+ field->children.push_back(std::move(child_field));
+ }
+}
+
+bool ParquetReader::_has_structured_filter(const reader::FileLocalFilter& local_filter) {
+ for (const auto& predicate : local_filter.predicates) {
+ if (predicate != nullptr) {
+ return true;
+ }
+ }
+ return false;
+}
+
+Status ParquetReader::_read_filter_columns(int64_t batch_rows, Block* file_block,
+ SelectionVector* selection, uint16_t* selected_rows) {
+ selection->resize(static_cast<size_t>(batch_rows));
+ for (size_t filter_idx = 0; filter_idx < _request->predicate_columns.size(); ++filter_idx) {
+ const int file_field_id = _request->predicate_columns[filter_idx];
+ auto& column_reader = _state->current_predicate_columns[filter_idx];
+ auto column = file_block->get_by_position(column_reader->file_column_id())
+ .column->assume_mutable();
+ DCHECK_EQ(file_block->get_by_position(column_reader->file_column_id())
+ .type->get_primitive_type(),
+ column_reader->type()->get_primitive_type());
+ int64_t column_rows = 0;
+ RETURN_IF_ERROR(column_reader->read(batch_rows, column, &column_rows));
+ if (column_rows != batch_rows) {
+ return Status::Corruption("Parquet filter column {} returned {} rows, expected {} rows",
+ column_reader->name(), column_rows, batch_rows);
+ }
+
+ for (const auto& local_filter : _request->local_filters) {
+ if (local_filter.file_column_id != file_field_id ||
+ !_has_structured_filter(local_filter)) {
+ continue;
+ }
+ if (*selected_rows == 0) {
+ break;
+ }
+ for (const auto& predicate : local_filter.predicates) {
+ *selected_rows = predicate->evaluate(*column, selection->data(), *selected_rows);
+ if (*selected_rows == 0) {
+ break;
+ }
+ }
+ break;
+ }
+ file_block->replace_by_position(file_field_id, std::move(column));
+ if (*selected_rows == 0) {
+ break;
+ }
+ }
+ return Status::OK();
+}
+
+Status ParquetReader::_validate_supported_local_filters(
+ const std::vector<reader::FileLocalFilter>& local_filters) {
+ for (const auto& local_filter : local_filters) {
+ if (local_filter.conjunct != nullptr) {
+ return Status::NotSupported(
+ "Parquet expression filter fallback is not implemented for field {}",
+ local_filter.file_column_id);
+ }
+ }
+ return Status::OK();
+}
+
+IColumn::Filter ParquetReader::_selection_to_filter(const SelectionVector& selection,
+ uint16_t selected_rows, int64_t batch_rows) {
+ IColumn::Filter filter(static_cast<size_t>(batch_rows), 0);
+ for (uint16_t selection_idx = 0; selection_idx < selected_rows; ++selection_idx) {
+ filter[selection.get_index(selection_idx)] = 1;
+ }
+ return filter;
+}
+
+Status ParquetReader::_open_next_row_group(bool* has_row_group) {
+ *has_row_group = false;
+ while (_state->next_row_group_idx < _state->selected_row_groups.size()) {
+ const int row_group_idx = _state->selected_row_groups[_state->next_row_group_idx++];
+ try {
+ _state->current_row_group = _state->file_reader->RowGroup(row_group_idx);
+ } catch (const ::parquet::ParquetException& e) {
+ return Status::Corruption("Failed to open parquet row group {}: {}", row_group_idx,
+ e.what());
+ } catch (const std::exception& e) {
+ return Status::InternalError("Failed to open parquet row group {}: {}", row_group_idx,
+ e.what());
+ }
+
+ auto row_group_metadata = _state->metadata->RowGroup(row_group_idx);
+ _state->current_row_group_rows =
+ row_group_metadata == nullptr ? 0 : row_group_metadata->num_rows();
+ if (_state->current_row_group_rows < 0) {
+ return Status::Corruption("Invalid negative row count in parquet row group {}",
+ row_group_idx);
+ } else if (_state->current_row_group_rows == 0) {
+ _reset_current_row_group();
+ continue;
+ }
+ _state->current_row_group_rows_read = 0;
+ _state->current_predicate_columns.clear();
+ _state->current_non_predicate_columns.clear();
+
+ ParquetColumnReaderFactory column_reader_factory(_state->current_row_group,
+ _state->schema->num_columns());
+ for (const auto file_field_id : _request->predicate_columns) {
+ const auto& column_schema = _state->file_schema[file_field_id];
+ std::unique_ptr<ParquetColumnReader> column_reader;
+ RETURN_IF_ERROR(column_reader_factory.create(*column_schema, &column_reader));
+ _state->current_predicate_columns.push_back(std::move(column_reader));
+ }
+ for (const auto file_field_id : _request->non_predicate_columns) {
+ const auto& column_schema = _state->file_schema[file_field_id];
+ std::unique_ptr<ParquetColumnReader> column_reader;
+ RETURN_IF_ERROR(column_reader_factory.create(*column_schema, &column_reader));
+ _state->current_non_predicate_columns.push_back(std::move(column_reader));
+ }
+ *has_row_group = true;
+ break;
+ }
+ return Status::OK();
+}
+
+// `file_block` has a complete struct derived from the file's schema.
+Status ParquetReader::_read_current_row_group_batch(int64_t batch_rows, Block* file_block,
+ size_t* rows) {
+ if (_state->current_predicate_columns.empty() &&
+ _state->current_non_predicate_columns.empty()) {
+ *rows = static_cast<size_t>(batch_rows);
+ return Status::OK();
+ }
+ SelectionVector selection;
+ uint16_t selected_rows = batch_rows;
+ // 1. Read all predicate columns and evaluate selection vector.
+ RETURN_IF_ERROR(_read_filter_columns(batch_rows, file_block, &selection, &selected_rows));
+
+ // 2. Materialize all predicate columns after filtering.
+ const bool need_filter_output = selected_rows != batch_rows;
+ if (need_filter_output) {
+ IColumn::Filter output_filter = _selection_to_filter(selection, selected_rows, batch_rows);
+ for (const auto file_field_id : _request->predicate_columns) {
+ RETURN_IF_CATCH_EXCEPTION(file_block->replace_by_position(
+ file_field_id, file_block->get_by_position(file_field_id)
+ .column->filter(output_filter, selected_rows)));
+ }
+ }
+
+ // 3. Materialize all non-predicate columns with selection.
+ for (size_t output_idx = 0; output_idx < _state->current_non_predicate_columns.size();
+ ++output_idx) {
+ auto& column_reader = _state->current_non_predicate_columns[output_idx];
+ auto col = file_block->get_columns()[column_reader->file_column_id()]->assume_mutable();
+ DCHECK_EQ(file_block->get_by_position(column_reader->file_column_id())
+ .type->get_primitive_type(),
+ column_reader->type()->get_primitive_type());
+ if (need_filter_output) {
+ [[maybe_unused]] auto old_size = col->size();
+ RETURN_IF_ERROR(column_reader->select(selection, selected_rows, batch_rows, col));
+ if (col->size() != old_size + selected_rows) {
+ return Status::Corruption(
+ "Parquet selected output column {} returned {} rows, expected {} rows",
+ column_reader->name(), col->size(), old_size + selected_rows);
+ }
+ } else {
+ int64_t column_rows = 0;
+ RETURN_IF_ERROR(column_reader->read(batch_rows, col, &column_rows));
+ if (column_rows != batch_rows) {
+ return Status::Corruption(
+ "Parquet output column {} returned {} rows, expected {} rows",
+ column_reader->name(), column_rows, batch_rows);
+ }
+ }
+ }
+
+ *rows = static_cast<size_t>(selected_rows);
+ return Status::OK();
+}
+
+ParquetReader::ParquetReader(std::unique_ptr<io::FileSystemProperties>& system_properties,
+ std::unique_ptr<io::FileDescription>& file_description,
+ std::shared_ptr<io::IOContext> io_ctx, RuntimeProfile* profile)
+ : FileReader(system_properties, file_description, io_ctx, profile) {}
+
+ParquetReader::~ParquetReader() = default;
+
+Status ParquetReader::init(RuntimeState* state) {
+ RETURN_IF_ERROR(reader::FileReader::init(state));
+ _state = std::make_unique<ParquetReaderScanState>();
+ _state->arrow_file =
+ std::make_shared<DorisRandomAccessFile>(_tracing_file_reader, _io_ctx.get());
+
+ try {
+ _state->file_reader = ::parquet::ParquetFileReader::Open(
+ _state->arrow_file, ::parquet::default_reader_properties());
+ _state->metadata = _state->file_reader->metadata();
+ _state->schema = _state->metadata != nullptr ? _state->metadata->schema() : nullptr;
+ } catch (const ::parquet::ParquetException& e) {
+ return Status::Corruption("Failed to open parquet file: {}", e.what());
+ } catch (const std::exception& e) {
+ return Status::InternalError("Failed to open parquet file: {}", e.what());
+ }
+
+ if (_state->metadata == nullptr || _state->schema == nullptr) {
+ return Status::Corruption("Failed to read parquet metadata");
+ }
+ RETURN_IF_ERROR(build_parquet_column_schema(*_state->schema, &_state->file_schema));
+ return Status::OK();
+}
+
+Status ParquetReader::get_schema(std::vector<reader::SchemaField>* file_schema) const {
+ if (file_schema == nullptr) {
+ return Status::InvalidArgument("file_schema is null");
+ }
+ file_schema->clear();
+ if (_state == nullptr || _state->schema == nullptr) {
+ return Status::Uninitialized("ParquetReader is not open");
+ }
+
+ file_schema->reserve(_state->file_schema.size());
+ for (size_t column_idx = 0; column_idx < _state->file_schema.size(); ++column_idx) {
+ reader::SchemaField field;
+ _fill_schema_field(*_state->file_schema[column_idx], &field);
+ field.id = static_cast<reader::ColumnId>(column_idx);
+ file_schema->push_back(std::move(field));
+ }
+ return Status::OK();
+}
+
+Status ParquetReader::open(std::unique_ptr<reader::FileScanRequest>& request) {
+ if (_state == nullptr || _state->metadata == nullptr || _state->schema == nullptr) {
+ return Status::Uninitialized("ParquetReader is not open");
+ }
+ RETURN_IF_ERROR(reader::FileReader::open(request));
+
+ const int num_fields = static_cast<int>(_state->file_schema.size());
+ for (const auto& local_filter : _request->local_filters) {
+ if (local_filter.file_column_id < 0 || local_filter.file_column_id >= num_fields) {
+ return Status::InvalidArgument("Invalid parquet filter top-level field id {}",
+ local_filter.file_column_id);
+ }
+ }
+ RETURN_IF_ERROR(_validate_supported_local_filters(_request->local_filters));
+
+ RETURN_IF_ERROR(select_row_groups_by_statistics(*_state->metadata, _state->file_schema,
+ *_request, &_state->selected_row_groups));
+ RETURN_IF_ERROR(_reset_reader_position());
+ _eof = _state->selected_row_groups.empty();
+ return Status::OK();
+}
+
+Status ParquetReader::get_block(Block* file_block, size_t* rows, bool* eof) {
+ if (_state == nullptr || _state->file_reader == nullptr || _state->schema == nullptr) {
+ return Status::Uninitialized("ParquetReader is not open");
+ }
+ *rows = 0;
+ if (_eof) {
+ *eof = true;
+ return Status::OK();
+ }
+
+ while (true) {
+ if (_state->current_row_group == nullptr) {
+ bool has_row_group = false;
+ RETURN_IF_ERROR(_open_next_row_group(&has_row_group));
+ if (!has_row_group) {
+ _eof = true;
+ *eof = true;
+ return Status::OK();
+ }
+ }
+
+ const int64_t remaining_rows =
+ _state->current_row_group_rows - _state->current_row_group_rows_read;
+ if (remaining_rows <= 0) {
+ _reset_current_row_group();
+ continue;
+ }
+
+ const int64_t batch_rows =
+ std::min<int64_t>(DEFAULT_PARQUET_READ_BATCH_SIZE, remaining_rows);
+ const int64_t physical_rows_read = batch_rows;
+ RETURN_IF_ERROR(_read_current_row_group_batch(batch_rows, file_block, rows));
+ _state->current_row_group_rows_read += physical_rows_read;
+ if (_state->current_row_group_rows_read >= _state->current_row_group_rows) {
+ _reset_current_row_group();
+ }
+ if (*rows == 0) {
+ continue;
+ }
+ *eof = false;
+ // TODO: Compute _request->reader_expression_map to filter file_block
+ return Status::OK();
+ }
+}
+
+Status ParquetReader::close() {
+ if (_state != nullptr) {
+ if (_state->file_reader != nullptr) {
+ try {
+ _state->file_reader->Close();
+ } catch (const std::exception&) {
+ // close 需要保持幂等;这里不覆盖此前 scan 路径上的真实错误。
+ }
+ }
+ if (_state->arrow_file != nullptr) {
+ static_cast<void>(arrow_status_to_doris_status(_state->arrow_file->Close()));
+ }
+ _state = std::make_unique<ParquetReaderScanState>();
+ }
+ return FileReader::close();
+}
+
+void ParquetReader::_init_profile() {
+ if (_profile != nullptr) {
+ static const char* parquet_profile = "ParquetReader";
+ ADD_TIMER_WITH_LEVEL(_profile, parquet_profile, 1);
+
+ _parquet_profile.filtered_row_groups = ADD_CHILD_COUNTER_WITH_LEVEL(
+ _profile, "RowGroupsFiltered", TUnit::UNIT, parquet_profile, 1);
+ _parquet_profile.filtered_row_groups_by_min_max = ADD_CHILD_COUNTER_WITH_LEVEL(
+ _profile, "RowGroupsFilteredByMinMax", TUnit::UNIT, parquet_profile, 1);
+ _parquet_profile.filtered_row_groups_by_bloom_filter = ADD_CHILD_COUNTER_WITH_LEVEL(
+ _profile, "RowGroupsFilteredByBloomFilter", TUnit::UNIT, parquet_profile, 1);
+ _parquet_profile.to_read_row_groups = ADD_CHILD_COUNTER_WITH_LEVEL(
+ _profile, "RowGroupsReadNum", TUnit::UNIT, parquet_profile, 1);
+ _parquet_profile.total_row_groups = ADD_CHILD_COUNTER_WITH_LEVEL(
+ _profile, "RowGroupsTotalNum", TUnit::UNIT, parquet_profile, 1);
+ _parquet_profile.filtered_group_rows = ADD_CHILD_COUNTER_WITH_LEVEL(
+ _profile, "FilteredRowsByGroup", TUnit::UNIT, parquet_profile, 1);
+ _parquet_profile.filtered_page_rows = ADD_CHILD_COUNTER_WITH_LEVEL(
+ _profile, "FilteredRowsByPage", TUnit::UNIT, parquet_profile, 1);
+ _parquet_profile.lazy_read_filtered_rows = ADD_CHILD_COUNTER_WITH_LEVEL(
+ _profile, "FilteredRowsByLazyRead", TUnit::UNIT, parquet_profile, 1);
+ _parquet_profile.filtered_bytes = ADD_CHILD_COUNTER_WITH_LEVEL(
+ _profile, "FilteredBytes", TUnit::BYTES, parquet_profile, 1);
+ _parquet_profile.raw_rows_read = ADD_CHILD_COUNTER_WITH_LEVEL(
+ _profile, "RawRowsRead", TUnit::UNIT, parquet_profile, 1);
+ _parquet_profile.column_read_time =
+ ADD_CHILD_TIMER_WITH_LEVEL(_profile, "ColumnReadTime", parquet_profile, 1);
+ _parquet_profile.parse_meta_time =
+ ADD_CHILD_TIMER_WITH_LEVEL(_profile, "ParseMetaTime", parquet_profile, 1);
+ _parquet_profile.parse_footer_time =
+ ADD_CHILD_TIMER_WITH_LEVEL(_profile, "ParseFooterTime", parquet_profile, 1);
+ _parquet_profile.file_reader_create_time =
+ ADD_CHILD_TIMER_WITH_LEVEL(_profile, "FileReaderCreateTime", parquet_profile, 1);
+ _parquet_profile.open_file_num =
+ ADD_CHILD_COUNTER_WITH_LEVEL(_profile, "FileNum", TUnit::UNIT, parquet_profile, 1);
+ _parquet_profile.page_index_read_calls =
+ ADD_COUNTER_WITH_LEVEL(_profile, "PageIndexReadCalls", TUnit::UNIT, 1);
+ _parquet_profile.page_index_filter_time =
+ ADD_CHILD_TIMER_WITH_LEVEL(_profile, "PageIndexFilterTime", parquet_profile, 1);
+ _parquet_profile.read_page_index_time =
+ ADD_CHILD_TIMER_WITH_LEVEL(_profile, "PageIndexReadTime", parquet_profile, 1);
+ _parquet_profile.parse_page_index_time =
+ ADD_CHILD_TIMER_WITH_LEVEL(_profile, "PageIndexParseTime", parquet_profile, 1);
+ _parquet_profile.row_group_filter_time =
+ ADD_CHILD_TIMER_WITH_LEVEL(_profile, "RowGroupFilterTime", parquet_profile, 1);
+ _parquet_profile.file_footer_read_calls =
+ ADD_COUNTER_WITH_LEVEL(_profile, "FileFooterReadCalls", TUnit::UNIT, 1);
+ _parquet_profile.file_footer_hit_cache =
+ ADD_COUNTER_WITH_LEVEL(_profile, "FileFooterHitCache", TUnit::UNIT, 1);
+ _parquet_profile.decompress_time =
+ ADD_CHILD_TIMER_WITH_LEVEL(_profile, "DecompressTime", parquet_profile, 1);
+ _parquet_profile.decompress_cnt = ADD_CHILD_COUNTER_WITH_LEVEL(
+ _profile, "DecompressCount", TUnit::UNIT, parquet_profile, 1);
+ _parquet_profile.page_read_counter = ADD_CHILD_COUNTER_WITH_LEVEL(
+ _profile, "PageReadCount", TUnit::UNIT, parquet_profile, 1);
+ _parquet_profile.page_cache_write_counter = ADD_CHILD_COUNTER_WITH_LEVEL(
+ _profile, "PageCacheWriteCount", TUnit::UNIT, parquet_profile, 1);
+ _parquet_profile.page_cache_compressed_write_counter = ADD_CHILD_COUNTER_WITH_LEVEL(
+ _profile, "PageCacheCompressedWriteCount", TUnit::UNIT, parquet_profile, 1);
+ _parquet_profile.page_cache_decompressed_write_counter = ADD_CHILD_COUNTER_WITH_LEVEL(
+ _profile, "PageCacheDecompressedWriteCount", TUnit::UNIT, parquet_profile, 1);
+ _parquet_profile.page_cache_hit_counter = ADD_CHILD_COUNTER_WITH_LEVEL(
+ _profile, "PageCacheHitCount", TUnit::UNIT, parquet_profile, 1);
+ _parquet_profile.page_cache_missing_counter = ADD_CHILD_COUNTER_WITH_LEVEL(
+ _profile, "PageCacheMissingCount", TUnit::UNIT, parquet_profile, 1);
+ _parquet_profile.page_cache_compressed_hit_counter = ADD_CHILD_COUNTER_WITH_LEVEL(
+ _profile, "PageCacheCompressedHitCount", TUnit::UNIT, parquet_profile, 1);
+ _parquet_profile.page_cache_decompressed_hit_counter = ADD_CHILD_COUNTER_WITH_LEVEL(
+ _profile, "PageCacheDecompressedHitCount", TUnit::UNIT, parquet_profile, 1);
+ _parquet_profile.decode_header_time =
+ ADD_CHILD_TIMER_WITH_LEVEL(_profile, "PageHeaderDecodeTime", parquet_profile, 1);
+ _parquet_profile.read_page_header_time =
+ ADD_CHILD_TIMER_WITH_LEVEL(_profile, "PageHeaderReadTime", parquet_profile, 1);
+ _parquet_profile.decode_value_time =
+ ADD_CHILD_TIMER_WITH_LEVEL(_profile, "DecodeValueTime", parquet_profile, 1);
+ _parquet_profile.decode_dict_time =
+ ADD_CHILD_TIMER_WITH_LEVEL(_profile, "DecodeDictTime", parquet_profile, 1);
+ _parquet_profile.decode_level_time =
+ ADD_CHILD_TIMER_WITH_LEVEL(_profile, "DecodeLevelTime", parquet_profile, 1);
+ _parquet_profile.decode_null_map_time =
+ ADD_CHILD_TIMER_WITH_LEVEL(_profile, "DecodeNullMapTime", parquet_profile, 1);
+ _parquet_profile.skip_page_header_num = ADD_CHILD_COUNTER_WITH_LEVEL(
+ _profile, "SkipPageHeaderNum", TUnit::UNIT, parquet_profile, 1);
+ _parquet_profile.parse_page_header_num = ADD_CHILD_COUNTER_WITH_LEVEL(
+ _profile, "ParsePageHeaderNum", TUnit::UNIT, parquet_profile, 1);
+ _parquet_profile.predicate_filter_time =
+ ADD_CHILD_TIMER_WITH_LEVEL(_profile, "PredicateFilterTime", parquet_profile, 1);
+ _parquet_profile.dict_filter_rewrite_time =
+ ADD_CHILD_TIMER_WITH_LEVEL(_profile, "DictFilterRewriteTime", parquet_profile, 1);
+ _parquet_profile.convert_time =
+ ADD_CHILD_TIMER_WITH_LEVEL(_profile, "ConvertTime", parquet_profile, 1);
+ _parquet_profile.bloom_filter_read_time =
+ ADD_CHILD_TIMER_WITH_LEVEL(_profile, "BloomFilterReadTime", parquet_profile, 1);
+ }
+}
+
+} // namespace doris::parquet
diff --git a/be/src/format/new_parquet/parquet_reader.h b/be/src/format/new_parquet/parquet_reader.h
new file mode 100644
index 00000000000..426960a4dfd
--- /dev/null
+++ b/be/src/format/new_parquet/parquet_reader.h
@@ -0,0 +1,138 @@
+// 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.
+
+#pragma once
+
+#include <memory>
+#include <vector>
+
+#include "common/status.h"
+#include "format/reader/file_reader.h"
+#include "parquet_column_schema.h"
+#include "selection_vector.h"
+
+namespace doris {
+namespace io {
+struct IOContext;
+} // namespace io
+} // namespace doris
+
+namespace doris::parquet {
+
+struct ParquetReaderScanState;
+
+// ParquetReader 的 file-local scan 请求。
+// 当前没有新增 Parquet-only 字段,但保留独立类型,便于后续加入 row group/page index
+// 等 Parquet 专属选项。
+struct ParquetScanRequest : public reader::FileScanRequest {};
+
+// Parquet 文件物理读取层。
+// 该类只理解 Parquet file-local schema 和 ParquetScanRequest,不理解 Iceberg/global
+// schema,不处理 table-level cast/default/generated/partition 语义。
+class ParquetReader : public reader::FileReader {
+public:
+ ParquetReader(std::unique_ptr<io::FileSystemProperties>& system_properties,
+ std::unique_ptr<io::FileDescription>& file_description,
+ std::shared_ptr<io::IOContext> io_ctx, RuntimeProfile* profile);
+ ~ParquetReader() override;
+
+ // 打开 Parquet 文件并解析 footer metadata。
+ // init 成功后可以调用 get_schema() 获取 Parquet file-local schema。
+ Status init(RuntimeState* state) override;
+
+ // 解析 Parquet footer 并返回 Parquet 文件自身的 schema。
+ // 该方法只能在 open() 成功后调用,不要求 init() 已经执行。
+ // 这里不做 Iceberg schema evolution,也不把字段转换成 table/global schema。
+ Status get_schema(std::vector<reader::SchemaField>* file_schema) const override;
+
+ Status open(std::unique_ptr<reader::FileScanRequest>& request) override;
+ // 读取下一批 Parquet file-local block。
+ // 该方法只能在 init() 成功后调用。
+ // 返回列必须保持 file-local 语义,不能在这里补 default/generated/partition 列。
+ Status get_block(Block* file_block, size_t* rows, bool* eof) override;
+
+ Status close() override;
+
+protected:
+ void _init_profile() override;
+
+private:
+ struct ParquetProfile {
+ RuntimeProfile::Counter* filtered_row_groups = nullptr;
+ RuntimeProfile::Counter* filtered_row_groups_by_min_max = nullptr;
+ RuntimeProfile::Counter* filtered_row_groups_by_bloom_filter = nullptr;
+ RuntimeProfile::Counter* to_read_row_groups = nullptr;
+ RuntimeProfile::Counter* total_row_groups = nullptr;
+ RuntimeProfile::Counter* filtered_group_rows = nullptr;
+ RuntimeProfile::Counter* filtered_page_rows = nullptr;
+ RuntimeProfile::Counter* lazy_read_filtered_rows = nullptr;
+ RuntimeProfile::Counter* filtered_bytes = nullptr;
+ RuntimeProfile::Counter* raw_rows_read = nullptr;
+ RuntimeProfile::Counter* column_read_time = nullptr;
+ RuntimeProfile::Counter* parse_meta_time = nullptr;
+ RuntimeProfile::Counter* parse_footer_time = nullptr;
+ RuntimeProfile::Counter* file_reader_create_time = nullptr;
+ RuntimeProfile::Counter* open_file_num = nullptr;
+ RuntimeProfile::Counter* row_group_filter_time = nullptr;
+ RuntimeProfile::Counter* page_index_read_calls = nullptr;
+ RuntimeProfile::Counter* page_index_filter_time = nullptr;
+ RuntimeProfile::Counter* read_page_index_time = nullptr;
+ RuntimeProfile::Counter* parse_page_index_time = nullptr;
+ RuntimeProfile::Counter* file_footer_read_calls = nullptr;
+ RuntimeProfile::Counter* file_footer_hit_cache = nullptr;
+ RuntimeProfile::Counter* decompress_time = nullptr;
+ RuntimeProfile::Counter* decompress_cnt = nullptr;
+ RuntimeProfile::Counter* page_read_counter = nullptr;
+ RuntimeProfile::Counter* page_cache_write_counter = nullptr;
+ RuntimeProfile::Counter* page_cache_compressed_write_counter = nullptr;
+ RuntimeProfile::Counter* page_cache_decompressed_write_counter = nullptr;
+ RuntimeProfile::Counter* page_cache_hit_counter = nullptr;
+ RuntimeProfile::Counter* page_cache_missing_counter = nullptr;
+ RuntimeProfile::Counter* page_cache_compressed_hit_counter = nullptr;
+ RuntimeProfile::Counter* page_cache_decompressed_hit_counter = nullptr;
+ RuntimeProfile::Counter* decode_header_time = nullptr;
+ RuntimeProfile::Counter* read_page_header_time = nullptr;
+ RuntimeProfile::Counter* decode_value_time = nullptr;
+ RuntimeProfile::Counter* decode_dict_time = nullptr;
+ RuntimeProfile::Counter* decode_level_time = nullptr;
+ RuntimeProfile::Counter* decode_null_map_time = nullptr;
+ RuntimeProfile::Counter* skip_page_header_num = nullptr;
+ RuntimeProfile::Counter* parse_page_header_num = nullptr;
+ RuntimeProfile::Counter* predicate_filter_time = nullptr;
+ RuntimeProfile::Counter* dict_filter_rewrite_time = nullptr;
+ RuntimeProfile::Counter* convert_time = nullptr;
+ RuntimeProfile::Counter* bloom_filter_read_time = nullptr;
+ };
+ Status _reset_reader_position();
+ void _reset_current_row_group();
+ void _fill_schema_field(const ParquetColumnSchema& column_schema,
+ reader::SchemaField* field) const;
+ bool _has_structured_filter(const reader::FileLocalFilter& local_filter);
+ Status _read_filter_columns(int64_t batch_rows, Block* file_block, SelectionVector* selection,
+ uint16_t* selected_rows);
+ Status _validate_supported_local_filters(
+ const std::vector<reader::FileLocalFilter>& local_filters);
+ IColumn::Filter _selection_to_filter(const SelectionVector& selection, uint16_t selected_rows,
+ int64_t batch_rows);
+ Status _open_next_row_group(bool* has_row_group);
+ Status _read_current_row_group_batch(int64_t batch_rows, Block* file_block, size_t* rows);
+
+ std::unique_ptr<ParquetReaderScanState> _state;
+ ParquetProfile _parquet_profile;
+};
+
+} // namespace doris::parquet
diff --git a/be/src/format/new_parquet/parquet_statistics.cpp b/be/src/format/new_parquet/parquet_statistics.cpp
new file mode 100644
index 00000000000..aebc6d4e04d
--- /dev/null
+++ b/be/src/format/new_parquet/parquet_statistics.cpp
@@ -0,0 +1,254 @@
+// 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/new_parquet/parquet_statistics.h"
+
+#include <parquet/api/reader.h>
+#include <parquet/api/schema.h>
+#include <parquet/statistics.h>
+#include <parquet/types.h>
+
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "core/data_type/data_type.h"
+#include "core/data_type/data_type_nullable.h"
+#include "core/data_type/primitive_type.h"
+#include "core/field.h"
+#include "format/new_parquet/parquet_column_schema.h"
+#include "storage/index/zone_map/zone_map_index.h"
+#include "storage/predicate/column_predicate.h"
+
+namespace doris::parquet {
+namespace {
+
+PrimitiveType physical_filter_type(const ParquetColumnSchema& column_schema) {
+ if (column_schema.type == nullptr) {
+ return INVALID_TYPE;
+ }
+ switch (remove_nullable(column_schema.type)->get_primitive_type()) {
+ case TYPE_BOOLEAN:
+ case TYPE_INT:
+ case TYPE_BIGINT:
+ case TYPE_FLOAT:
+ case TYPE_DOUBLE:
+ case TYPE_STRING:
+ return remove_nullable(column_schema.type)->get_primitive_type();
+ default:
+ return INVALID_TYPE;
+ }
+}
+
+template <typename ParquetDType, PrimitiveType DorisType, typename ConvertFn>
+bool set_typed_min_max(const std::shared_ptr<::parquet::Statistics>& statistics, ConvertFn convert,
+ ParquetColumnStatistics* column_statistics) {
+ auto typed_statistics =
+ std::static_pointer_cast<::parquet::TypedStatistics<ParquetDType>>(statistics);
+ column_statistics->min_value = Field::create_field<DorisType>(convert(typed_statistics->min()));
+ column_statistics->max_value = Field::create_field<DorisType>(convert(typed_statistics->max()));
+ return true;
+}
+
+bool set_string_min_max(const std::shared_ptr<::parquet::Statistics>& statistics,
+ const ::parquet::ColumnDescriptor* descriptor,
+ ParquetColumnStatistics* column_statistics) {
+ switch (statistics->physical_type()) {
+ case ::parquet::Type::BYTE_ARRAY: {
+ auto typed_statistics =
+ std::static_pointer_cast<::parquet::TypedStatistics<::parquet::ByteArrayType>>(
+ statistics);
+ column_statistics->min_value = Field::create_field<TYPE_STRING>(
+ ::parquet::ByteArrayToString(typed_statistics->min()));
+ column_statistics->max_value = Field::create_field<TYPE_STRING>(
+ ::parquet::ByteArrayToString(typed_statistics->max()));
+ return true;
+ }
+ case ::parquet::Type::FIXED_LEN_BYTE_ARRAY: {
+ if (descriptor == nullptr || descriptor->type_length() <= 0) {
+ return false;
+ }
+ auto typed_statistics =
+ std::static_pointer_cast<::parquet::TypedStatistics<::parquet::FLBAType>>(
+ statistics);
+ const int type_length = descriptor->type_length();
+ column_statistics->min_value = Field::create_field<TYPE_STRING>(std::string(
+ reinterpret_cast<const char*>(typed_statistics->min().ptr), type_length));
+ column_statistics->max_value = Field::create_field<TYPE_STRING>(std::string(
+ reinterpret_cast<const char*>(typed_statistics->max().ptr), type_length));
+ return true;
+ }
+ default:
+ return false;
+ }
+}
+
+bool is_null_only_predicate(const ColumnPredicate& predicate) {
+ return predicate.type() == PredicateType::IS_NULL ||
+ predicate.type() == PredicateType::IS_NOT_NULL;
+}
+
+segment_v2::ZoneMap to_column_predicate_statistics(const ParquetColumnStatistics& statistics) {
+ segment_v2::ZoneMap predicate_statistics;
+ predicate_statistics.min_value = statistics.min_value;
+ predicate_statistics.max_value = statistics.max_value;
+ predicate_statistics.has_null = statistics.has_null;
+ predicate_statistics.has_not_null = statistics.has_not_null;
+ return predicate_statistics;
+}
+
+} // namespace
+
+ParquetColumnStatistics ParquetStatisticsUtils::TransformColumnStatistics(
+ const ParquetColumnSchema& column_schema,
+ const std::shared_ptr<::parquet::Statistics>& statistics) {
+ ParquetColumnStatistics result;
+ if (statistics == nullptr) {
+ return result;
+ }
+
+ result.has_null = statistics->HasNullCount() && statistics->null_count() > 0;
+ result.has_not_null = statistics->num_values() > 0 || statistics->HasMinMax();
+ result.has_null_count = statistics->HasNullCount();
+ if (!result.has_not_null || !statistics->HasMinMax()) {
+ return result;
+ }
+
+ switch (statistics->physical_type()) {
+ case ::parquet::Type::BOOLEAN:
+ result.has_min_max = set_typed_min_max<::parquet::BooleanType, TYPE_BOOLEAN>(
+ statistics, [](bool value) { return static_cast<UInt8>(value); }, &result);
+ return result;
+ case ::parquet::Type::INT32:
+ result.has_min_max = set_typed_min_max<::parquet::Int32Type, TYPE_INT>(
+ statistics, [](int32_t value) { return value; }, &result);
+ return result;
+ case ::parquet::Type::INT64:
+ result.has_min_max = set_typed_min_max<::parquet::Int64Type, TYPE_BIGINT>(
+ statistics, [](int64_t value) { return value; }, &result);
+ return result;
+ case ::parquet::Type::FLOAT:
+ result.has_min_max = set_typed_min_max<::parquet::FloatType, TYPE_FLOAT>(
+ statistics, [](float value) { return value; }, &result);
+ return result;
+ case ::parquet::Type::DOUBLE:
+ result.has_min_max = set_typed_min_max<::parquet::DoubleType, TYPE_DOUBLE>(
+ statistics, [](double value) { return value; }, &result);
+ return result;
+ case ::parquet::Type::BYTE_ARRAY:
+ case ::parquet::Type::FIXED_LEN_BYTE_ARRAY:
+ result.has_min_max = set_string_min_max(statistics, column_schema.descriptor, &result);
+ return result;
+ default:
+ return result;
+ }
+}
+
+bool ParquetStatisticsUtils::CheckStatistics(const reader::FileLocalFilter& local_filter,
+ const ParquetColumnStatistics& statistics) {
+ if (!statistics.has_any_statistics()) {
+ return false;
+ }
+
+ // TODO: replace local_filter.predicates by local_filter.conjuncts
+ for (const auto& column_predicate : local_filter.predicates) {
+ if (is_null_only_predicate(*column_predicate)) {
+ if (!statistics.has_null_count) {
+ continue;
+ }
+ } else if (!statistics.has_any_statistics()) {
+ continue;
+ }
+ if (!column_predicate->evaluate_and(to_column_predicate_statistics(statistics))) {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool ParquetStatisticsUtils::RowGroupExcludes(
+ const ::parquet::RowGroupMetaData& row_group,
+ const std::vector<std::unique_ptr<ParquetColumnSchema>>& schema,
+ const reader::FileLocalFilter& local_filter) {
+ DCHECK(local_filter.file_column_id >= 0 &&
+ local_filter.file_column_id < row_group.num_columns());
+ DCHECK_LT(local_filter.file_column_id, schema.size());
+ auto column_chunk = row_group.ColumnChunk(local_filter.file_column_id);
+ if (column_chunk == nullptr) {
+ return false;
+ }
+ return CheckStatistics(local_filter,
+ TransformColumnStatistics(*schema[local_filter.file_column_id],
+ column_chunk->statistics()));
+}
+
+Status ParquetStatisticsUtils::SelectRowGroups(
+ const ::parquet::FileMetaData& metadata,
+ const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
+ const reader::FileScanRequest& request, std::vector<int>* selected_row_groups) {
+ if (selected_row_groups == nullptr) {
+ return Status::InvalidArgument("selected_row_groups is null");
+ }
+ selected_row_groups->clear();
+
+ const int num_row_groups = metadata.num_row_groups();
+ selected_row_groups->reserve(num_row_groups);
+ for (int row_group_idx = 0; row_group_idx < num_row_groups; ++row_group_idx) {
+ auto row_group = metadata.RowGroup(row_group_idx);
+ if (row_group == nullptr) {
+ selected_row_groups->push_back(row_group_idx);
+ continue;
+ }
+ bool drop = false;
+ for (const auto& local_filter : request.local_filters) {
+ if (RowGroupExcludes(*row_group, file_schema, local_filter)) {
+ drop = true;
+ break;
+ }
+ }
+ if (drop) {
+ continue;
+ }
+ selected_row_groups->push_back(row_group_idx);
+ }
+ return Status::OK();
+}
+
+bool ParquetStatisticsUtils::BloomFilterSupported(const ParquetColumnSchema& column_schema) {
+ switch (physical_filter_type(column_schema)) {
+ case TYPE_BOOLEAN:
+ case TYPE_INT:
+ case TYPE_BIGINT:
+ case TYPE_FLOAT:
+ case TYPE_DOUBLE:
+ case TYPE_STRING:
+ return true;
+ default:
+ return false;
+ }
+}
+
+Status select_row_groups_by_statistics(
+ const ::parquet::FileMetaData& metadata,
+ const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
+ const reader::FileScanRequest& request, std::vector<int>* selected_row_groups) {
+ return ParquetStatisticsUtils::SelectRowGroups(metadata, file_schema, request,
+ selected_row_groups);
+}
+
+} // namespace doris::parquet
diff --git a/be/src/format/new_parquet/parquet_statistics.h b/be/src/format/new_parquet/parquet_statistics.h
new file mode 100644
index 00000000000..0def08d4b08
--- /dev/null
+++ b/be/src/format/new_parquet/parquet_statistics.h
@@ -0,0 +1,88 @@
+// 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.
+
+#pragma once
+
+#include <memory>
+#include <vector>
+
+#include "common/status.h"
+#include "core/field.h"
+#include "format/reader/file_reader.h"
+
+namespace parquet {
+class FileMetaData;
+class RowGroupMetaData;
+class Statistics;
+} // namespace parquet
+
+namespace doris {
+class ColumnPredicate;
+} // namespace doris
+
+namespace doris::parquet {
+
+struct ParquetColumnSchema;
+
+// Parquet row group column statistics 转换后的 Doris 统计视图。
+// DuckDB 会把 Parquet stats 转换成 BaseStatistics,然后让 TableFilter 自己判断;
+// Doris 新 reader 先保存 file-local min/max/null 信息,再交给 ColumnPredicate 判断。
+struct ParquetColumnStatistics {
+ Field min_value;
+ Field max_value;
+ bool has_null = false;
+ bool has_not_null = false;
+ bool has_null_count = false;
+ bool has_min_max = false;
+
+ bool has_any_statistics() const { return has_null_count || has_min_max; }
+};
+
+// Parquet file-local statistics/page index/bloom filter 工具类。
+// 结构参考 DuckDB ParquetStatisticsUtils:先把 Parquet metadata 转成统一统计对象,
+// 再由 filter/predicate 判断是否可以裁剪。这里不理解 table/global schema。
+struct ParquetStatisticsUtils {
+ static ParquetColumnStatistics TransformColumnStatistics(
+ const ParquetColumnSchema& column_schema,
+ const std::shared_ptr<::parquet::Statistics>& statistics);
+
+ // Return true if the statistics indicate that the row group can be safely skipped according to the local filter.
+ static bool CheckStatistics(const reader::FileLocalFilter& local_filter,
+ const ParquetColumnStatistics& statistics);
+
+ static bool RowGroupExcludes(const ::parquet::RowGroupMetaData& row_group,
+ const std::vector<std::unique_ptr<ParquetColumnSchema>>& schema,
+ const reader::FileLocalFilter& local_filter);
+
+ static Status SelectRowGroups(
+ const ::parquet::FileMetaData& metadata,
+ const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
+ const reader::FileScanRequest& request, std::vector<int>* selected_row_groups);
+
+ static bool BloomFilterSupported(const ParquetColumnSchema& column_schema);
+};
+
+// Parquet file-local statistics/page index/bloom filter 裁剪入口。
+// 这里只消费已经 localize 到 file schema 的 FileScanRequest,不理解 table/global schema。
+// 后续 page index、dictionary、bloom filter 等文件格式优化也应继续收敛在这一层,避免污染
+// ParquetReader 的 scan 调度代码。
+Status select_row_groups_by_statistics(
+ const ::parquet::FileMetaData& metadata,
+ const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
+ const reader::FileScanRequest& request, std::vector<int>* selected_row_groups);
+
+} // namespace doris::parquet
diff --git a/be/src/format/new_parquet/parquet_type.cpp b/be/src/format/new_parquet/parquet_type.cpp
new file mode 100644
index 00000000000..53c7b4f2ed9
--- /dev/null
+++ b/be/src/format/new_parquet/parquet_type.cpp
@@ -0,0 +1,349 @@
+// 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/new_parquet/parquet_type.h"
+
+#include <parquet/api/schema.h>
+
+#include <memory>
+#include <string>
+
+#include "core/data_type/data_type_factory.hpp"
+#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/primitive_type.h"
+
+namespace doris::parquet {
+namespace {
+
+DataTypePtr create_type(PrimitiveType type, bool nullable, int precision = 0, int scale = 0) {
+ return DataTypeFactory::instance().create_data_type(type, nullable, precision, scale);
+}
+
+PrimitiveType decimal_primitive_type(int precision) {
+ return precision > 38 ? TYPE_DECIMAL256 : TYPE_DECIMAL128I;
+}
+
+bool has_non_physical_annotation(const ::parquet::ColumnDescriptor* column) {
+ if (column == nullptr) {
+ return false;
+ }
+ const auto& logical_type = column->logical_type();
+ return column->converted_type() != ::parquet::ConvertedType::NONE ||
+ (logical_type != nullptr && logical_type->is_valid() && !logical_type->is_none());
+}
+
+void mark_decimal(const ::parquet::ColumnDescriptor* column, int precision, int scale,
+ ParquetTypeDescriptor* result) {
+ result->is_decimal = true;
+ result->decimal_precision = precision;
+ result->decimal_scale = scale;
+ switch (column->physical_type()) {
+ case ::parquet::Type::INT32:
+ result->extra_type_info = ParquetExtraTypeInfo::DECIMAL_INT32;
+ break;
+ case ::parquet::Type::INT64:
+ result->extra_type_info = ParquetExtraTypeInfo::DECIMAL_INT64;
+ break;
+ case ::parquet::Type::BYTE_ARRAY:
+ case ::parquet::Type::FIXED_LEN_BYTE_ARRAY:
+ result->extra_type_info = ParquetExtraTypeInfo::DECIMAL_BYTE_ARRAY;
+ break;
+ default:
+ result->extra_type_info = ParquetExtraTypeInfo::NONE;
+ break;
+ }
+}
+
+DataTypePtr converted_type_to_doris_type(const ::parquet::ColumnDescriptor* column,
+ ParquetTypeDescriptor* result) {
+ const bool nullable = column->max_definition_level() > 0;
+ switch (column->converted_type()) {
+ case ::parquet::ConvertedType::UTF8:
+ case ::parquet::ConvertedType::ENUM:
+ case ::parquet::ConvertedType::JSON:
+ case ::parquet::ConvertedType::BSON:
+ return create_type(TYPE_STRING, nullable);
+ case ::parquet::ConvertedType::DECIMAL:
+ mark_decimal(column, column->type_precision(), column->type_scale(), result);
+ return create_type(decimal_primitive_type(column->type_precision()), nullable,
+ column->type_precision(), column->type_scale());
+ case ::parquet::ConvertedType::DATE:
+ return create_type(TYPE_DATEV2, nullable);
+ case ::parquet::ConvertedType::TIME_MILLIS:
+ result->time_unit = ParquetTimeUnit::MILLIS;
+ result->extra_type_info = ParquetExtraTypeInfo::UNIT_MS;
+ return create_type(TYPE_TIMEV2, nullable, 0, 3);
+ case ::parquet::ConvertedType::TIME_MICROS:
+ result->time_unit = ParquetTimeUnit::MICROS;
+ result->extra_type_info = ParquetExtraTypeInfo::UNIT_MICROS;
+ return create_type(TYPE_TIMEV2, nullable, 0, 6);
+ case ::parquet::ConvertedType::TIMESTAMP_MILLIS:
+ result->is_timestamp = true;
+ result->time_unit = ParquetTimeUnit::MILLIS;
+ result->extra_type_info = ParquetExtraTypeInfo::UNIT_MS;
+ return create_type(TYPE_DATETIMEV2, nullable, 0, 3);
+ case ::parquet::ConvertedType::TIMESTAMP_MICROS:
+ result->is_timestamp = true;
+ result->time_unit = ParquetTimeUnit::MICROS;
+ result->extra_type_info = ParquetExtraTypeInfo::UNIT_MICROS;
+ return create_type(TYPE_DATETIMEV2, nullable, 0, 6);
+ case ::parquet::ConvertedType::INT_8:
+ return create_type(TYPE_TINYINT, nullable);
+ case ::parquet::ConvertedType::UINT_8:
+ case ::parquet::ConvertedType::INT_16:
+ return create_type(TYPE_SMALLINT, nullable);
+ case ::parquet::ConvertedType::UINT_16:
+ case ::parquet::ConvertedType::INT_32:
+ return create_type(TYPE_INT, nullable);
+ case ::parquet::ConvertedType::UINT_32:
+ case ::parquet::ConvertedType::INT_64:
+ return create_type(TYPE_BIGINT, nullable);
+ case ::parquet::ConvertedType::UINT_64:
+ return create_type(TYPE_LARGEINT, nullable);
+ case ::parquet::ConvertedType::NONE:
+ default:
+ return nullptr;
+ }
+}
+
+DataTypePtr logical_type_to_doris_type(const ::parquet::ColumnDescriptor* column,
+ ParquetTypeDescriptor* result) {
+ const auto& logical_type = column->logical_type();
+ if (logical_type == nullptr || !logical_type->is_valid() || logical_type->is_none()) {
+ return nullptr;
+ }
+ const bool nullable = column->max_definition_level() > 0;
+ if (logical_type->is_string() || logical_type->is_enum() || logical_type->is_JSON() ||
+ logical_type->is_BSON() || logical_type->is_UUID()) {
+ return create_type(TYPE_STRING, nullable);
+ }
+ if (logical_type->is_decimal()) {
+ const auto& decimal_type = static_cast<const ::parquet::DecimalLogicalType&>(*logical_type);
+ mark_decimal(column, decimal_type.precision(), decimal_type.scale(), result);
+ return create_type(decimal_primitive_type(decimal_type.precision()), nullable,
+ decimal_type.precision(), decimal_type.scale());
+ }
+ if (logical_type->is_date()) {
+ return create_type(TYPE_DATEV2, nullable);
+ }
+ if (logical_type->is_time()) {
+ const auto& time_type = static_cast<const ::parquet::TimeLogicalType&>(*logical_type);
+ int scale = 0;
+ if (time_type.time_unit() == ::parquet::LogicalType::TimeUnit::MILLIS) {
+ scale = 3;
+ result->time_unit = ParquetTimeUnit::MILLIS;
+ result->extra_type_info = ParquetExtraTypeInfo::UNIT_MS;
+ } else if (time_type.time_unit() == ::parquet::LogicalType::TimeUnit::MICROS) {
+ scale = 6;
+ result->time_unit = ParquetTimeUnit::MICROS;
+ result->extra_type_info = ParquetExtraTypeInfo::UNIT_MICROS;
+ } else {
+ return nullptr;
+ }
+ return create_type(TYPE_TIMEV2, nullable, 0, scale);
+ }
+ if (logical_type->is_timestamp()) {
+ const auto& timestamp_type =
+ static_cast<const ::parquet::TimestampLogicalType&>(*logical_type);
+ int scale = 0;
+ if (timestamp_type.time_unit() == ::parquet::LogicalType::TimeUnit::MILLIS) {
+ scale = 3;
+ result->time_unit = ParquetTimeUnit::MILLIS;
+ result->extra_type_info = ParquetExtraTypeInfo::UNIT_MS;
+ } else if (timestamp_type.time_unit() == ::parquet::LogicalType::TimeUnit::MICROS) {
+ scale = 6;
+ result->time_unit = ParquetTimeUnit::MICROS;
+ result->extra_type_info = ParquetExtraTypeInfo::UNIT_MICROS;
+ } else {
+ return nullptr;
+ }
+ result->is_timestamp = true;
+ return create_type(TYPE_DATETIMEV2, nullable, 0, scale);
+ }
+ if (logical_type->is_int()) {
+ const auto& int_type = static_cast<const ::parquet::IntLogicalType&>(*logical_type);
+ switch (int_type.bit_width()) {
+ case 8:
+ return create_type(int_type.is_signed() ? TYPE_TINYINT : TYPE_SMALLINT, nullable);
+ case 16:
+ return create_type(int_type.is_signed() ? TYPE_SMALLINT : TYPE_INT, nullable);
+ case 32:
+ return create_type(int_type.is_signed() ? TYPE_INT : TYPE_BIGINT, nullable);
+ case 64:
+ return create_type(int_type.is_signed() ? TYPE_BIGINT : TYPE_LARGEINT, nullable);
+ default:
+ return nullptr;
+ }
+ }
+ return nullptr;
+}
+
+DataTypePtr physical_type_to_doris_type(const ::parquet::ColumnDescriptor* column) {
+ const bool nullable = column->max_definition_level() > 0;
+ DataTypePtr type;
+ switch (column->physical_type()) {
+ case ::parquet::Type::BOOLEAN:
+ type = std::make_shared<DataTypeBool>();
+ break;
+ case ::parquet::Type::INT32:
+ type = std::make_shared<DataTypeInt32>();
+ break;
+ case ::parquet::Type::INT64:
+ type = std::make_shared<DataTypeInt64>();
+ break;
+ case ::parquet::Type::FLOAT:
+ type = std::make_shared<DataTypeFloat32>();
+ break;
+ case ::parquet::Type::DOUBLE:
+ type = std::make_shared<DataTypeFloat64>();
+ break;
+ case ::parquet::Type::BYTE_ARRAY:
+ case ::parquet::Type::FIXED_LEN_BYTE_ARRAY:
+ type = std::make_shared<DataTypeString>();
+ break;
+ case ::parquet::Type::INT96:
+ type = std::make_shared<DataTypeString>();
+ break;
+ default:
+ return nullptr;
+ }
+ return nullable ? make_nullable(type) : type;
+}
+
+DataTypePtr direct_flat_primitive_doris_type(const ::parquet::ColumnDescriptor* column) {
+ if (column == nullptr || column->max_repetition_level() != 0 ||
+ column->max_definition_level() > 1 || has_non_physical_annotation(column)) {
+ return nullptr;
+ }
+
+ const bool nullable = column->max_definition_level() > 0;
+ switch (column->physical_type()) {
+ case ::parquet::Type::BOOLEAN:
+ return create_type(TYPE_BOOLEAN, nullable);
+ case ::parquet::Type::INT32:
+ return create_type(TYPE_INT, nullable);
+ case ::parquet::Type::INT64:
+ return create_type(TYPE_BIGINT, nullable);
+ case ::parquet::Type::FLOAT:
+ return create_type(TYPE_FLOAT, nullable);
+ case ::parquet::Type::DOUBLE:
+ return create_type(TYPE_DOUBLE, nullable);
+ default:
+ return nullptr;
+ }
+}
+
+bool record_reader_physical_type_supported(::parquet::Type::type physical_type) {
+ switch (physical_type) {
+ case ::parquet::Type::BOOLEAN:
+ case ::parquet::Type::INT32:
+ case ::parquet::Type::INT64:
+ case ::parquet::Type::FLOAT:
+ case ::parquet::Type::DOUBLE:
+ case ::parquet::Type::BYTE_ARRAY:
+ case ::parquet::Type::FIXED_LEN_BYTE_ARRAY:
+ return true;
+ default:
+ return false;
+ }
+}
+
+bool record_reader_integer_annotation_supported(const ::parquet::ColumnDescriptor* column,
+ const DataTypePtr& doris_type) {
+ const auto& logical_type = column->logical_type();
+ const bool has_int_logical_type =
+ logical_type != nullptr && logical_type->is_valid() && logical_type->is_int();
+ const bool has_int_converted_type =
+ column->converted_type() == ::parquet::ConvertedType::INT_8 ||
+ column->converted_type() == ::parquet::ConvertedType::UINT_8 ||
+ column->converted_type() == ::parquet::ConvertedType::INT_16 ||
+ column->converted_type() == ::parquet::ConvertedType::UINT_16 ||
+ column->converted_type() == ::parquet::ConvertedType::INT_32 ||
+ column->converted_type() == ::parquet::ConvertedType::UINT_32 ||
+ column->converted_type() == ::parquet::ConvertedType::INT_64 ||
+ column->converted_type() == ::parquet::ConvertedType::UINT_64;
+ auto primitive_type = remove_nullable(doris_type)->get_primitive_type();
+ return (has_int_logical_type || has_int_converted_type) &&
+ (primitive_type == TYPE_TINYINT || primitive_type == TYPE_SMALLINT ||
+ primitive_type == TYPE_INT || primitive_type == TYPE_BIGINT);
+}
+
+} // namespace
+
+std::string parquet_column_name(const ::parquet::ColumnDescriptor* column) {
+ if (column == nullptr) {
+ return {};
+ }
+ auto path = column->path();
+ if (path) {
+ return path->ToDotString();
+ }
+ return column->name();
+}
+
+ParquetTypeDescriptor resolve_parquet_type(const ::parquet::ColumnDescriptor* column) {
+ ParquetTypeDescriptor result;
+ if (column == nullptr) {
+ return result;
+ }
+
+ result.physical_type = column->physical_type();
+ result.converted_type = column->converted_type();
+ result.fixed_length = column->type_length();
+
+ if (auto logical_type = logical_type_to_doris_type(column, &result); logical_type != nullptr) {
+ result.doris_type = logical_type;
+ } else if (auto converted_type = converted_type_to_doris_type(column, &result);
+ converted_type != nullptr) {
+ result.doris_type = converted_type;
+ } else {
+ result.doris_type = physical_type_to_doris_type(column);
+ if (result.physical_type == ::parquet::Type::INT96) {
+ result.extra_type_info = ParquetExtraTypeInfo::IMPALA_TIMESTAMP;
+ }
+ }
+
+ result.is_string_like =
+ !result.is_decimal && (result.physical_type == ::parquet::Type::BYTE_ARRAY ||
+ result.physical_type == ::parquet::Type::FIXED_LEN_BYTE_ARRAY);
+
+ if (column->max_repetition_level() != 0 || column->max_definition_level() > 1) {
+ result.supports_record_reader = false;
+ return result;
+ }
+ if (!record_reader_physical_type_supported(result.physical_type)) {
+ result.supports_record_reader = false;
+ return result;
+ }
+ if (direct_flat_primitive_doris_type(column) != nullptr || result.is_string_like ||
+ (result.is_decimal && result.decimal_precision <= 38) ||
+ (result.is_timestamp && result.physical_type == ::parquet::Type::INT64) ||
+ record_reader_integer_annotation_supported(column, result.doris_type) ||
+ remove_nullable(result.doris_type)->get_primitive_type() == TYPE_DATEV2 ||
+ remove_nullable(result.doris_type)->get_primitive_type() == TYPE_TIMEV2) {
+ result.supports_record_reader = true;
+ }
+ return result;
+}
+
+bool supports_record_reader(const ParquetTypeDescriptor& type_descriptor) {
+ return type_descriptor.supports_record_reader;
+}
+
+} // namespace doris::parquet
diff --git a/be/src/format/new_parquet/parquet_type.h b/be/src/format/new_parquet/parquet_type.h
new file mode 100644
index 00000000000..1404f84bc36
--- /dev/null
+++ b/be/src/format/new_parquet/parquet_type.h
@@ -0,0 +1,84 @@
+// 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.
+
+#pragma once
+
+#include <parquet/types.h>
+
+#include <string>
+
+#include "core/data_type/data_type.h"
+
+namespace parquet {
+class ColumnDescriptor;
+} // namespace parquet
+
+namespace doris::parquet {
+
+// Parquet logical/converted annotation 解析后留下的额外编码信息。
+// 这对应 DuckDB ParquetColumnSchema::type_info:Doris type 只能表达最终展示类型,
+// 读值时还需要知道 decimal/timestamp/time 在 Parquet 中的物理编码方式。
+enum class ParquetExtraTypeInfo {
+ NONE,
+ DECIMAL_INT32,
+ DECIMAL_INT64,
+ DECIMAL_BYTE_ARRAY,
+ UNIT_MS,
+ UNIT_MICROS,
+ UNIT_NS,
+ IMPALA_TIMESTAMP,
+};
+
+enum class ParquetTimeUnit {
+ UNKNOWN,
+ MILLIS,
+ MICROS,
+ NANOS,
+};
+
+// Parquet file-local column descriptor 的类型解析结果。
+// 该结构只解释 Parquet physical/logical/converted type,不包含 table/global schema
+// evolution,也不依赖 Arrow internal RecordReader API。
+struct ParquetTypeDescriptor {
+ DataTypePtr doris_type;
+ ParquetExtraTypeInfo extra_type_info = ParquetExtraTypeInfo::NONE;
+ ParquetTimeUnit time_unit = ParquetTimeUnit::UNKNOWN;
+ ::parquet::Type::type physical_type = ::parquet::Type::UNDEFINED;
+ ::parquet::ConvertedType::type converted_type = ::parquet::ConvertedType::UNDEFINED;
+ int decimal_precision = -1;
+ int decimal_scale = -1;
+ int fixed_length = -1;
+ bool is_decimal = false;
+ bool is_timestamp = false;
+ bool is_string_like = false;
+ bool supports_record_reader = false;
+};
+
+// 返回 Parquet leaf column 的 file-local 展示名。
+std::string parquet_column_name(const ::parquet::ColumnDescriptor* column);
+
+// 将 Parquet file-local column descriptor 解析成 Doris file-local 类型和读值所需的
+// 编码信息。这里不做 table schema evolution;类型提升和 default/generated/partition
+// 列由 table 层处理。
+ParquetTypeDescriptor resolve_parquet_type(const ::parquet::ColumnDescriptor* column);
+
+// 判断当前阶段是否可以通过 Arrow Parquet RecordReader 读取该列。
+// 当前支持 flat primitive/string/decimal/timestamp。复杂 nested column 仍通过 children
+// 递归组合,list/map assembler 后续补齐。
+bool supports_record_reader(const ParquetTypeDescriptor& type_descriptor);
+
+} // namespace doris::parquet
diff --git a/be/src/format/new_parquet/selection_vector.h b/be/src/format/new_parquet/selection_vector.h
new file mode 100644
index 00000000000..22a9d3507e2
--- /dev/null
+++ b/be/src/format/new_parquet/selection_vector.h
@@ -0,0 +1,116 @@
+// 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.
+
+#pragma once
+
+#include <cstddef>
+#include <cstdint>
+#include <vector>
+
+#include "common/status.h"
+
+namespace doris::parquet {
+
+// 类似 DuckDB SelectionVector 的轻量行号视图。
+// 它只表达一个 batch 内被选中的 row offset,不持有 table/global schema 语义。
+// 未绑定 data 时表示 identity selection:get_index(i) == i。
+class SelectionVector {
+public:
+ using Index = uint16_t;
+
+ SelectionVector() = default;
+
+ explicit SelectionVector(size_t count) { resize(count); }
+
+ SelectionVector(Index* data, size_t count) { initialize(data, count); }
+
+ void initialize(Index* data, size_t count) {
+ _owned.clear();
+ _data = data;
+ _size = count;
+ }
+
+ void resize(size_t count) {
+ _owned.resize(count);
+ _data = _owned.data();
+ _size = count;
+ for (size_t idx = 0; idx < count; ++idx) {
+ _data[idx] = static_cast<Index>(idx);
+ }
+ }
+
+ void clear() {
+ _owned.clear();
+ _data = nullptr;
+ _size = 0;
+ }
+
+ size_t size() const { return _size; }
+
+ bool is_set() const { return _data != nullptr; }
+
+ Index* data() { return _data; }
+
+ const Index* data() const { return _data; }
+
+ size_t get_index(size_t idx) const {
+ if (_data == nullptr) {
+ return idx;
+ }
+ return _data[idx];
+ }
+
+ void set_index(size_t idx, Index value) { _data[idx] = value; }
+
+ Status verify(size_t count, int64_t batch_rows) const {
+ if (batch_rows < 0) {
+ return Status::InvalidArgument("Negative parquet selection batch rows {}", batch_rows);
+ }
+ if (count > static_cast<size_t>(batch_rows)) {
+ return Status::InvalidArgument("Parquet selection count {} exceeds batch rows {}",
+ count, batch_rows);
+ }
+ if (_data != nullptr && count > _size) {
+ return Status::InvalidArgument("Parquet selection count {} exceeds vector size {}",
+ count, _size);
+ }
+ size_t previous = 0;
+ for (size_t idx = 0; idx < count; ++idx) {
+ const size_t current = get_index(idx);
+ if (current >= static_cast<size_t>(batch_rows)) {
+ return Status::InvalidArgument(
+ "Parquet selection index {} out of range [0, {}) at position {}", current,
+ batch_rows, idx);
+ }
+ if (idx > 0 && current <= previous) {
+ return Status::InvalidArgument(
+ "Parquet selection index {} is not strictly greater than previous {} at "
+ "position {}",
+ current, previous, idx);
+ }
+ previous = current;
+ }
+ return Status::OK();
+ }
+
+private:
+ std::vector<Index> _owned;
+ Index* _data = nullptr;
+ size_t _size = 0;
+};
+
+} // namespace doris::parquet
diff --git a/be/src/format/parquet/parquet_reader.h b/be/src/format/parquet/parquet_reader.h
deleted file mode 100644
index 65227aba04c..00000000000
--- a/be/src/format/parquet/parquet_reader.h
+++ /dev/null
@@ -1,77 +0,0 @@
-// 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.
-
-#pragma once
-
-#include <vector>
-
-#include "common/status.h"
-#include "format/reader/file_reader.h"
-
-namespace doris {
-namespace io {
-struct IOContext;
-} // namespace io
-} // namespace doris
-
-namespace doris::parquet {
-
-// ParquetReader 的 file-local scan 请求。
-// 当前没有新增 Parquet-only 字段,但保留独立类型,便于后续加入 row group/page index
-// 等 Parquet 专属选项。
-struct ParquetScanRequest : public reader::FileScanRequest {};
-
-// Parquet 文件物理读取层。
-// 该类只理解 Parquet file-local schema 和 ParquetScanRequest,不理解 Iceberg/global
-// schema,不处理 table-level cast/default/generated/partition 语义。
-class ParquetReader : public reader::FileReader {
-public:
- virtual ~ParquetReader() = default;
-
- // 解析 Parquet footer 并返回 Parquet 文件自身的 schema。
- // 这里不做 Iceberg schema evolution,也不把字段转换成 table/global schema。
- Status get_schema(std::vector<reader::SchemaField>* file_schema) const override {
- // 真实实现会从 Parquet footer / schema descriptor 展开 file-local schema。
- file_schema->clear();
- return Status::OK();
- }
-
- // 初始化 Parquet 专属 scan。
- // 后续可以在 ParquetScanRequest 中扩展 row group、page index、bloom filter 等
- // Parquet-only 选项;table-level 语义仍然必须由 TableColumnMapper 提前转换。
- Status init(const ParquetScanRequest& request) {
- // 真实实现会根据 projected_file_columns、local_filters 和 reader_expression_map
- // 初始化 row group、column chunk、page reader 以及延时物化计划。
- return reader::FileReader::init(request);
- }
-
- // 读取下一批 Parquet file-local block。
- // 返回列必须保持 file-local 语义,不能在这里补 default/generated/partition 列。
- Status next(Block* file_block, size_t* rows, bool* eof) override {
- // 真实实现会输出 file-local block。stub 默认立即 EOF。
- return reader::FileReader::next(file_block, rows, eof);
- }
-
- // 通用 FileReader 初始化入口。
- // 当上层只持有 reader::FileReader 指针时会走该接口;Parquet 专属参数通过
- // ParquetScanRequest 重载表达。
- Status init(const reader::FileScanRequest& request) override {
- return reader::FileReader::init(request);
- }
-};
-
-} // namespace doris::parquet
diff --git a/be/src/format/reader/column_mapper.cpp b/be/src/format/reader/column_mapper.cpp
index 7510413d07f..0eed9d3e566 100644
--- a/be/src/format/reader/column_mapper.cpp
+++ b/be/src/format/reader/column_mapper.cpp
@@ -95,9 +95,10 @@ Status TableColumnMapper::create_scan_request(const std::map<int32_t, TableFilte
file_request->reader_expression_map.clear();
for (const auto& table_column : projected_columns) {
const auto* mapping = _find_mapping(table_column.id);
- if (mapping != nullptr && mapping->file_column_id.has_value() &&
- table_filters.count(table_column.id) == 0) {
- file_request->non_predicate_columns.push_back(*mapping->file_column_id);
+ if (mapping != nullptr && mapping->file_column_id.has_value()) {
+ if (table_filters.count(table_column.id) == 0) {
+ file_request->non_predicate_columns.push_back(*mapping->file_column_id);
+ }
}
}
RETURN_IF_ERROR(localize_filters(table_filters, file_request));
diff --git a/be/src/format/reader/expr/delete_predicate.cpp b/be/src/format/reader/expr/delete_predicate.cpp
index d1ca03a5201..01844fa8a07 100644
--- a/be/src/format/reader/expr/delete_predicate.cpp
+++ b/be/src/format/reader/expr/delete_predicate.cpp
@@ -86,7 +86,7 @@ Status DeletePredicate::execute(VExprContext* context, Block* block, int* result
DCHECK_EQ(row_ids.size(), count);
if (_deleted_rows.empty()) {
block->insert({std::move(res_col), std::make_shared<DataTypeBool>(), expr_name()});
- *result_column_id = block->get_columns().size() - 1;
+ *result_column_id = static_cast<int>(block->get_columns().size() - 1);
return Status::OK();
}
const int64_t* delete_rows = _deleted_rows.data();
@@ -106,7 +106,7 @@ Status DeletePredicate::execute(VExprContext* context, Block* block, int* result
++start_index;
}
block->insert({std::move(res_col), std::make_shared<DataTypeBool>(), expr_name()});
- *result_column_id = block->get_columns().size() - 1;
+ *result_column_id = static_cast<int>(block->get_columns().size() - 1);
return Status::OK();
}
diff --git a/be/src/format/reader/file_reader.cpp b/be/src/format/reader/file_reader.cpp
new file mode 100644
index 00000000000..daf9e2cf4f8
--- /dev/null
+++ b/be/src/format/reader/file_reader.cpp
@@ -0,0 +1,42 @@
+// 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/reader/file_reader.h"
+
+#include "io/fs/buffered_reader.h"
+#include "io/fs/tracing_file_reader.h"
+#include "runtime/runtime_state.h"
+
+namespace doris::reader {
+
+Status FileReader::init(RuntimeState* state) {
+ _init_profile();
+ SCOPED_RAW_TIMER(&_reader_statistics.file_reader_create_time);
+ ++_reader_statistics.open_file_num;
+ io::FileReaderOptions reader_options =
+ FileFactory::get_reader_options(state->query_options(), *_file_description);
+ _file_reader = DORIS_TRY(io::DelegateReader::create_file_reader(
+ _profile, *_system_properties, *_file_description, reader_options,
+ io::DelegateReader::AccessMode::RANDOM, _io_ctx));
+ _tracing_file_reader = _io_ctx ? std::make_shared<io::TracingFileReader>(
+ _file_reader, _io_ctx->file_reader_stats)
+ : _file_reader;
+ _eof = false;
+ return Status::OK();
+}
+
+} // namespace doris::reader
diff --git a/be/src/format/reader/file_reader.h b/be/src/format/reader/file_reader.h
index 96ace67d8de..7fbdb9b576c 100644
--- a/be/src/format/reader/file_reader.h
+++ b/be/src/format/reader/file_reader.h
@@ -19,6 +19,7 @@
#include <cstddef>
#include <cstdint>
+#include <map>
#include <memory>
#include <string>
#include <utility>
@@ -27,6 +28,7 @@
#include "common/status.h"
#include "core/data_type/data_type.h"
#include "exprs/vexpr_fwd.h"
+#include "io/file_factory.h"
#include "io/fs/file_reader_writer_fwd.h"
namespace doris {
@@ -96,64 +98,75 @@ struct FileScanRequest {
std::vector<FileLocalFilter> local_filters;
// fallback path if filters cannot be localized to file-local predicates. The expression can reference projected_file_columns and partition columns.
std::vector<std::pair<ColumnId, VExprContextSPtr>> reader_expression_map;
- // partition key -> value
- std::map<std::string, Field> partition_values;
-
- // projected_columns' id is file-local column id, and they are all from file schema.
- // For example,
- // file schema: [0: id (int), 1: name (string), 2: age (int)]
- // predicate: age > 30
- // table-level projection: [name, id]
- // predicate_columns: [2]
- // non_predicate_columns: [1, 0]
- // projected_columns are columns in blocks returned to table reader: [1, 0] means only name and id are projected,
- std::vector<ColumnId> projected_columns;
};
// 文件物理读取层通用接口。
// 该接口只描述 file-local schema、file-local scan request 和 file-local block。
// TableReader/IcebergTableReader 可以通过它组合不同文件格式 reader。
+/**
+ * +-----> get_schema() -----------------+
+ * FileReader() -----> init() ----| -----> close()
+ * +-----> open() -----> get_block() ----+
+ */
class FileReader {
public:
+ struct ReaderStatistics {
+ int32_t filtered_row_groups = 0;
+ int32_t filtered_row_groups_by_min_max = 0;
+ int32_t filtered_row_groups_by_bloom_filter = 0;
+ int32_t read_row_groups = 0;
+ int64_t filtered_group_rows = 0;
+ int64_t filtered_page_rows = 0;
+ int64_t lazy_read_filtered_rows = 0;
+ int64_t read_rows = 0;
+ int64_t filtered_bytes = 0;
+ int64_t column_read_time = 0;
+ int64_t parse_meta_time = 0;
+ int64_t parse_footer_time = 0;
+ int64_t file_footer_read_calls = 0;
+ int64_t file_footer_hit_cache = 0;
+ int64_t file_reader_create_time = 0;
+ int64_t open_file_num = 0;
+ int64_t row_group_filter_time = 0;
+ int64_t page_index_filter_time = 0;
+ int64_t read_page_index_time = 0;
+ int64_t parse_page_index_time = 0;
+ int64_t predicate_filter_time = 0;
+ int64_t dict_filter_rewrite_time = 0;
+ int64_t bloom_filter_read_time = 0;
+ };
+
+ FileReader(std::unique_ptr<io::FileSystemProperties>& system_properties,
+ std::unique_ptr<io::FileDescription>& file_description,
+ std::shared_ptr<io::IOContext> io_ctx, RuntimeProfile* profile)
+ : _system_properties(std::move(system_properties)),
+ _file_description(std::move(file_description)),
+ _io_ctx(io_ctx),
+ _profile(profile) {}
virtual ~FileReader() = default;
- // 打开一个物理文件并加载文件级元数据。
- // 该方法只建立 file-local reader 状态,不接收 table schema,也不做 projection/filter
- // 规划;这些输入由 init(FileScanRequest) 提供。
- virtual Status open(io::FileReaderSPtr file, io::IOContext* io_ctx = nullptr) {
- // 真实实现会保存文件句柄、IO 上下文并读取文件元数据。
- _file = std::move(file);
- _io_ctx = io_ctx;
- _eof = false;
- return Status::OK();
- }
+ // Initialize file reader and parse file metadata.
+ virtual Status init(RuntimeState* state);
- // 返回文件自己的 schema 视图。
- // 返回结果必须是 file-local schema:列 id、类型和 children 都按文件格式展开,
- // 不在这里解释 Iceberg field id、缺失列、默认值或 generated column。
- virtual Status get_schema(std::vector<SchemaField>* file_schema) const {
- // 真实实现会展开文件格式自己的 file-local schema。
- file_schema->clear();
- return Status::OK();
- }
+ // Get file-local schema from file metadata. The file schema is determined by file format and file content, and does not contain table/global schema semantics. For example, Iceberg field id, name mapping, default/generated/partition columns are not interpreted in file reader. This method can only be called after init() successfully, but does not require open() to be called.
+ virtual Status get_schema(std::vector<SchemaField>* file_schema) const = 0;
- // 初始化一次 file-local scan。
- // request 由 TableColumnMapper 生成,只包含文件列投影、本地过滤条件和 reader
- // expression。FileReader 可以基于它初始化 row group/page/stripe 等文件格式计划。
- virtual Status init(const FileScanRequest& request) {
- // 真实实现会根据 projected columns、local filters 和 reader expressions
- // 初始化文件格式自己的物理读取计划。
- // _request.projected_file_columns = request.projected_file_columns;
- _request.local_filters = request.local_filters;
- _request.reader_expression_map = request.reader_expression_map;
+ // Open the file reader with file-local scan request. The file reader should initialize its internal state according to the request, but does not need to interpret table/global schema semantics. For example, all schema change, filter localization, default/generated/partition columns should be handled in table reader layer. This method can only be called after init() successfully.
+ virtual Status open(std::unique_ptr<FileScanRequest>& request) {
+ _request = std::move(request);
return Status::OK();
}
// 读取下一批 file-local block。
+ // 该方法只能在 init(FileScanRequest) 成功后调用。
// file_block 的列顺序和类型必须遵守 FileScanRequest,而不是 table/global schema。
- // eof 表示当前文件 reader 是否读完;多文件切换由 TableReader 负责。
- virtual Status get_block(Block* file_block, bool* eof) {
+ // rows 返回当前批次输出行数;eof 表示当前文件 reader 是否读完;多文件切换由
+ // TableReader 负责。
+ virtual Status get_block(Block* file_block, size_t* rows, bool* eof) {
// stub 默认立即 EOF。
+ if (rows != nullptr) {
+ *rows = 0;
+ }
if (eof != nullptr) {
*eof = true;
}
@@ -164,18 +177,29 @@ public:
// 关闭当前物理文件 reader 并释放文件层状态。
// 该方法不处理 table-level delete/finalize 状态,后者由 TableReader 子类管理。
virtual Status close() {
- _file.reset();
- _io_ctx = nullptr;
- _request = FileScanRequest {};
+ _file_reader.reset();
+ _tracing_file_reader.reset();
+ _io_ctx.reset();
+ _request.reset();
_eof = true;
return Status::OK();
}
protected:
- io::FileReaderSPtr _file;
- io::IOContext* _io_ctx = nullptr;
- FileScanRequest _request;
+ virtual void _init_profile() {}
+ io::FileReaderSPtr _file_reader;
+ // _tracing_file_reader wraps _file_reader.
+ // _file_reader is original file reader.
+ // _tracing_file_reader is tracing file reader with io context.
+ // If io_ctx is null, _tracing_file_reader will be the same as file_reader.
+ io::FileReaderSPtr _tracing_file_reader = nullptr;
+ std::unique_ptr<FileScanRequest> _request;
bool _eof = true;
+ ReaderStatistics _reader_statistics;
+ std::unique_ptr<io::FileSystemProperties> _system_properties;
+ std::unique_ptr<io::FileDescription> _file_description;
+ std::shared_ptr<io::IOContext> _io_ctx;
+ RuntimeProfile* _profile = nullptr;
};
} // namespace doris::reader
diff --git a/be/src/format/reader/table_reader.h b/be/src/format/reader/table_reader.h
index c3744427aa0..7572383b8ad 100644
--- a/be/src/format/reader/table_reader.h
+++ b/be/src/format/reader/table_reader.h
@@ -181,7 +181,12 @@ public:
// TODO: reuse column's memory
current_block.insert({field.type->create_column(), field.type, field.name});
}
- RETURN_IF_ERROR(_data_reader.reader->get_block(¤t_block, ¤t_eof));
+ size_t current_rows = 0;
+ RETURN_IF_ERROR(
+ _data_reader.reader->get_block(¤t_block, ¤t_rows, ¤t_eof));
+ if (current_rows == 0 && !current_eof) {
+ continue;
+ }
size_t idx = 0;
for (const auto& mapping : _data_reader.column_mapper.mappings()) {
@@ -232,15 +237,16 @@ protected:
// 打开当前具体 reader。
// 子类在这里基于当前 split/task 初始化底层 FileReader。
virtual Status open_reader() {
- _data_reader.block_schema.clear();
- RETURN_IF_ERROR(_data_reader.reader->get_schema(&_data_reader.block_schema));
- RETURN_IF_ERROR(_data_reader.column_mapper.create_mapping(
- _options.projected_columns, _partition_values, _data_reader.block_schema));
+ std::vector<SchemaField> file_schema;
+ RETURN_IF_ERROR(_data_reader.reader->get_schema(&file_schema));
+ _data_reader.block_schema = file_schema;
+ RETURN_IF_ERROR(_data_reader.column_mapper.create_mapping(_options.projected_columns,
+ _partition_values, file_schema));
- FileScanRequest file_request;
+ auto file_request = std::make_unique<FileScanRequest>();
RETURN_IF_ERROR(_data_reader.column_mapper.create_scan_request(
- _table_filters, _options.projected_columns, &file_request));
- RETURN_IF_ERROR(_data_reader.reader->init(file_request));
+ _table_filters, _options.projected_columns, file_request.get()));
+ RETURN_IF_ERROR(_data_reader.reader->open(file_request));
return Status::OK();
}
diff --git a/be/test/core/data_type_serde/data_type_serde_decoded_values_test.cpp b/be/test/core/data_type_serde/data_type_serde_decoded_values_test.cpp
new file mode 100644
index 00000000000..10f15bb28b1
--- /dev/null
+++ b/be/test/core/data_type_serde/data_type_serde_decoded_values_test.cpp
@@ -0,0 +1,278 @@
+// 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 <gtest/gtest.h>
+
+#include <cstdint>
+#include <memory>
+#include <vector>
+
+#include "core/assert_cast.h"
+#include "core/column/column_decimal.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_date_or_datetime_v2.h"
+#include "core/data_type/data_type_decimal.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_serde/decoded_column_view.h"
+#include "core/string_ref.h"
+
+namespace doris {
+
+TEST(DataTypeSerDeDecodedValuesTest, ReadInt32Values) {
+ auto type = std::make_shared<DataTypeInt32>();
+ auto column = type->create_column();
+ const int32_t values[] = {10, -20, 30};
+
+ DecodedColumnView view;
+ view.value_kind = DecodedValueKind::INT32;
+ view.row_count = 3;
+ view.values = reinterpret_cast<const uint8_t*>(values);
+
+ auto st = type->get_serde()->read_column_from_decoded_values(*column, view);
+ ASSERT_TRUE(st.ok()) << st;
+
+ const auto& int_column = assert_cast<const ColumnInt32&>(*column);
+ ASSERT_EQ(int_column.size(), 3);
+ EXPECT_EQ(int_column.get_element(0), 10);
+ EXPECT_EQ(int_column.get_element(1), -20);
+ EXPECT_EQ(int_column.get_element(2), 30);
+}
+
+TEST(DataTypeSerDeDecodedValuesTest, ReadPrimitiveNumberValues) {
+ {
+ auto type = std::make_shared<DataTypeBool>();
+ auto column = type->create_column();
+ const bool values[] = {true, false, true};
+
+ DecodedColumnView view;
+ view.value_kind = DecodedValueKind::BOOL;
+ view.row_count = 3;
+ view.values = reinterpret_cast<const uint8_t*>(values);
+
+ auto st = type->get_serde()->read_column_from_decoded_values(*column, view);
+ ASSERT_TRUE(st.ok()) << st;
+
+ const auto& bool_column = assert_cast<const ColumnBool&>(*column);
+ ASSERT_EQ(bool_column.size(), 3);
+ EXPECT_EQ(bool_column.get_element(0), 1);
+ EXPECT_EQ(bool_column.get_element(1), 0);
+ EXPECT_EQ(bool_column.get_element(2), 1);
+ }
+ {
+ auto type = std::make_shared<DataTypeInt64>();
+ auto column = type->create_column();
+ const int64_t values[] = {10000000000L, -9L, 42L};
+
+ DecodedColumnView view;
+ view.value_kind = DecodedValueKind::INT64;
+ view.row_count = 3;
+ view.values = reinterpret_cast<const uint8_t*>(values);
+
+ auto st = type->get_serde()->read_column_from_decoded_values(*column, view);
+ ASSERT_TRUE(st.ok()) << st;
+
+ const auto& int_column = assert_cast<const ColumnInt64&>(*column);
+ ASSERT_EQ(int_column.size(), 3);
+ EXPECT_EQ(int_column.get_element(0), 10000000000L);
+ EXPECT_EQ(int_column.get_element(1), -9L);
+ EXPECT_EQ(int_column.get_element(2), 42L);
+ }
+ {
+ auto type = std::make_shared<DataTypeFloat32>();
+ auto column = type->create_column();
+ const float values[] = {1.5F, -2.25F};
+
+ DecodedColumnView view;
+ view.value_kind = DecodedValueKind::FLOAT;
+ view.row_count = 2;
+ view.values = reinterpret_cast<const uint8_t*>(values);
+
+ auto st = type->get_serde()->read_column_from_decoded_values(*column, view);
+ ASSERT_TRUE(st.ok()) << st;
+
+ const auto& float_column = assert_cast<const ColumnFloat32&>(*column);
+ ASSERT_EQ(float_column.size(), 2);
+ EXPECT_FLOAT_EQ(float_column.get_element(0), 1.5F);
+ EXPECT_FLOAT_EQ(float_column.get_element(1), -2.25F);
+ }
+ {
+ auto type = std::make_shared<DataTypeFloat64>();
+ auto column = type->create_column();
+ const double values[] = {3.5, -4.75};
+
+ DecodedColumnView view;
+ view.value_kind = DecodedValueKind::DOUBLE;
+ view.row_count = 2;
+ view.values = reinterpret_cast<const uint8_t*>(values);
+
+ auto st = type->get_serde()->read_column_from_decoded_values(*column, view);
+ ASSERT_TRUE(st.ok()) << st;
+
+ const auto& double_column = assert_cast<const ColumnFloat64&>(*column);
+ ASSERT_EQ(double_column.size(), 2);
+ EXPECT_DOUBLE_EQ(double_column.get_element(0), 3.5);
+ EXPECT_DOUBLE_EQ(double_column.get_element(1), -4.75);
+ }
+}
+
+TEST(DataTypeSerDeDecodedValuesTest, ReadStringValues) {
+ auto type = std::make_shared<DataTypeString>();
+ auto column = type->create_column();
+ std::vector<StringRef> values = {
+ StringRef("alpha", 5),
+ StringRef("beta", 4),
+ StringRef("gamma", 5),
+ };
+
+ DecodedColumnView view;
+ view.value_kind = DecodedValueKind::BINARY;
+ view.row_count = values.size();
+ view.binary_values = &values;
+
+ auto st = type->get_serde()->read_column_from_decoded_values(*column, view);
+ ASSERT_TRUE(st.ok()) << st;
+
+ const auto& string_column = assert_cast<const ColumnString&>(*column);
+ ASSERT_EQ(string_column.size(), 3);
+ EXPECT_EQ(string_column.get_data_at(0).to_string(), "alpha");
+ EXPECT_EQ(string_column.get_data_at(1).to_string(), "beta");
+ EXPECT_EQ(string_column.get_data_at(2).to_string(), "gamma");
+}
+
+TEST(DataTypeSerDeDecodedValuesTest, ReadDateAndDateTimeValues) {
+ {
+ auto type = std::make_shared<DataTypeDateV2>();
+ auto column = type->create_column();
+ const int32_t values[] = {0, 1, 18628};
+
+ DecodedColumnView view;
+ view.value_kind = DecodedValueKind::INT32;
+ view.row_count = 3;
+ view.values = reinterpret_cast<const uint8_t*>(values);
+
+ auto st = type->get_serde()->read_column_from_decoded_values(*column, view);
+ ASSERT_TRUE(st.ok()) << st;
+
+ ASSERT_EQ(column->size(), 3);
+ EXPECT_EQ(type->to_string(*column, 0), "1970-01-01");
+ EXPECT_EQ(type->to_string(*column, 1), "1970-01-02");
+ EXPECT_EQ(type->to_string(*column, 2), "2021-01-01");
+ }
+ {
+ auto type = std::make_shared<DataTypeDateTimeV2>(6);
+ auto column = type->create_column();
+ const int64_t values[] = {0, 1234567, -1};
+
+ DecodedColumnView view;
+ view.value_kind = DecodedValueKind::INT64;
+ view.time_unit = DecodedTimeUnit::MICROS;
+ view.row_count = 3;
+ view.values = reinterpret_cast<const uint8_t*>(values);
+
+ auto st = type->get_serde()->read_column_from_decoded_values(*column, view);
+ ASSERT_TRUE(st.ok()) << st;
+
+ ASSERT_EQ(column->size(), 3);
+ EXPECT_EQ(type->to_string(*column, 0), "1970-01-01 00:00:00.000000");
+ EXPECT_EQ(type->to_string(*column, 1), "1970-01-01 00:00:01.234567");
+ EXPECT_EQ(type->to_string(*column, 2), "1969-12-31 23:59:59.999999");
+ }
+}
+
+TEST(DataTypeSerDeDecodedValuesTest, ReadDecimalValues) {
+ auto type = std::make_shared<DataTypeDecimal128>(18, 2);
+ auto column = type->create_column();
+ const int64_t values[] = {12345, -67, 0};
+
+ DecodedColumnView view;
+ view.value_kind = DecodedValueKind::INT64;
+ view.row_count = 3;
+ view.values = reinterpret_cast<const uint8_t*>(values);
+ view.decimal_precision = 18;
+ view.decimal_scale = 2;
+
+ auto st = type->get_serde()->read_column_from_decoded_values(*column, view);
+ ASSERT_TRUE(st.ok()) << st;
+
+ const auto& decimal_column = assert_cast<const ColumnDecimal128V3&>(*column);
+ ASSERT_EQ(decimal_column.size(), 3);
+ EXPECT_EQ(decimal_column.get_element(0), Decimal128V3(12345));
+ EXPECT_EQ(decimal_column.get_element(1), Decimal128V3(-67));
+ EXPECT_EQ(decimal_column.get_element(2), Decimal128V3(0));
+ EXPECT_EQ(type->to_string(*column, 0), "123.45");
+ EXPECT_EQ(type->to_string(*column, 1), "-0.67");
+}
+
+TEST(DataTypeSerDeDecodedValuesTest, ReadNullableInt32Values) {
+ auto type = std::make_shared<DataTypeNullable>(std::make_shared<DataTypeInt32>());
+ auto column = type->create_column();
+ const int32_t values[] = {1, 2, 3, 4};
+ const uint8_t null_map[] = {0, 1, 0, 1};
+
+ DecodedColumnView view;
+ view.value_kind = DecodedValueKind::INT32;
+ view.row_count = 4;
+ view.values = reinterpret_cast<const uint8_t*>(values);
+ view.null_map = null_map;
+
+ auto st = type->get_serde()->read_column_from_decoded_values(*column, view);
+ ASSERT_TRUE(st.ok()) << st;
+
+ const auto& nullable_column = assert_cast<const ColumnNullable&>(*column);
+ const auto& nested_column = assert_cast<const ColumnInt32&>(nullable_column.get_nested_column());
+ ASSERT_EQ(nullable_column.size(), 4);
+ EXPECT_FALSE(nullable_column.is_null_at(0));
+ EXPECT_TRUE(nullable_column.is_null_at(1));
+ EXPECT_FALSE(nullable_column.is_null_at(2));
+ EXPECT_TRUE(nullable_column.is_null_at(3));
+ EXPECT_EQ(nested_column.get_element(0), 1);
+ EXPECT_EQ(nested_column.get_element(1), 2);
+ EXPECT_EQ(nested_column.get_element(2), 3);
+ EXPECT_EQ(nested_column.get_element(3), 4);
+}
+
+TEST(DataTypeSerDeDecodedValuesTest, RejectMismatchedValueKind) {
+ auto type = std::make_shared<DataTypeInt32>();
+ auto column = type->create_column();
+ const int64_t values[] = {1};
+
+ DecodedColumnView view;
+ view.value_kind = DecodedValueKind::INT64;
+ view.row_count = 1;
+ view.values = reinterpret_cast<const uint8_t*>(values);
+
+ auto st = type->get_serde()->read_column_from_decoded_values(*column, view);
+ EXPECT_FALSE(st.ok());
+}
+
+TEST(DataTypeSerDeDecodedValuesTest, RejectMissingValueBuffer) {
+ auto type = std::make_shared<DataTypeInt32>();
+ auto column = type->create_column();
+
+ DecodedColumnView view;
+ view.value_kind = DecodedValueKind::INT32;
+ view.row_count = 1;
+
+ auto st = type->get_serde()->read_column_from_decoded_values(*column, view);
+ EXPECT_FALSE(st.ok());
+}
+
+} // namespace doris
diff --git a/be/test/format/new_parquet/parquet_column_reader_test.cpp b/be/test/format/new_parquet/parquet_column_reader_test.cpp
new file mode 100644
index 00000000000..e4a0841f5af
--- /dev/null
+++ b/be/test/format/new_parquet/parquet_column_reader_test.cpp
@@ -0,0 +1,562 @@
+// 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 <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 <functional>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "core/assert_cast.h"
+#include "core/column/column_decimal.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.h"
+#include "core/data_type/data_type_nullable.h"
+#include "core/types.h"
+#include "format/new_parquet/column_reader.h"
+#include "format/new_parquet/parquet_column_schema.h"
+#include "format/new_parquet/selection_vector.h"
+
+namespace doris::parquet {
+namespace {
+
+constexpr int64_t ROW_COUNT = 5;
+
+std::shared_ptr<arrow::Array> finish_array(arrow::ArrayBuilder* builder) {
+ std::shared_ptr<arrow::Array> array;
+ EXPECT_TRUE(builder->Finish(&array).ok());
+ return array;
+}
+
+class ParquetColumnReaderTest : public testing::Test {
+protected:
+ void SetUp() override {
+ _test_dir = std::filesystem::temp_directory_path() / "doris_parquet_column_reader_test";
+ std::filesystem::remove_all(_test_dir);
+ std::filesystem::create_directories(_test_dir);
+ _file_path = (_test_dir / "reader.parquet").string();
+ write_parquet_file();
+ _file_reader = ::parquet::ParquetFileReader::OpenFile(_file_path, false);
+ auto metadata = _file_reader->metadata();
+ ASSERT_EQ(metadata->num_row_groups(), 1);
+ _row_group = _file_reader->RowGroup(0);
+ ASSERT_NE(_row_group, nullptr);
+ auto schema_descriptor = _file_reader->metadata()->schema();
+ ASSERT_NE(schema_descriptor, nullptr);
+ auto st = build_parquet_column_schema(*schema_descriptor, &_fields);
+ ASSERT_TRUE(st.ok()) << st;
+ ASSERT_EQ(_fields.size(), _expected_by_field.size());
+ }
+
+ void TearDown() override { std::filesystem::remove_all(_test_dir); }
+
+ template <typename Builder, typename Value>
+ std::shared_ptr<arrow::Array> build_required_array(const std::vector<Value>& values) {
+ Builder builder;
+ for (const auto& value : values) {
+ EXPECT_TRUE(builder.Append(value).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);
+ }
+
+ std::shared_ptr<arrow::Array> build_binary_array(const std::vector<std::string>& values) {
+ arrow::BinaryBuilder builder;
+ for (const auto& value : values) {
+ EXPECT_TRUE(builder.Append(reinterpret_cast<const uint8_t*>(value.data()),
+ static_cast<int32_t>(value.size()))
+ .ok());
+ }
+ return finish_array(&builder);
+ }
+
+ std::shared_ptr<arrow::Array> build_fixed_binary_array(
+ const std::shared_ptr<arrow::DataType>& type,
+ const std::vector<std::string>& values) {
+ arrow::FixedSizeBinaryBuilder builder(type, arrow::default_memory_pool());
+ for (const auto& value : values) {
+ EXPECT_TRUE(builder.Append(reinterpret_cast<const uint8_t*>(value.data())).ok());
+ }
+ return finish_array(&builder);
+ }
+
+ std::shared_ptr<arrow::Array> build_nullable_int32_array() {
+ arrow::Int32Builder builder;
+ EXPECT_TRUE(builder.Append(1).ok());
+ EXPECT_TRUE(builder.AppendNull().ok());
+ EXPECT_TRUE(builder.Append(3).ok());
+ EXPECT_TRUE(builder.AppendNull().ok());
+ EXPECT_TRUE(builder.Append(5).ok());
+ return finish_array(&builder);
+ }
+
+ std::shared_ptr<arrow::Array> build_time32_array(const std::shared_ptr<arrow::DataType>& type,
+ const std::vector<int32_t>& values) {
+ arrow::Time32Builder builder(type, arrow::default_memory_pool());
+ for (const auto value : values) {
+ EXPECT_TRUE(builder.Append(value).ok());
+ }
+ return finish_array(&builder);
+ }
+
+ std::shared_ptr<arrow::Array> build_time64_array(const std::shared_ptr<arrow::DataType>& type,
+ const std::vector<int64_t>& values) {
+ arrow::Time64Builder builder(type, arrow::default_memory_pool());
+ for (const auto value : values) {
+ EXPECT_TRUE(builder.Append(value).ok());
+ }
+ return finish_array(&builder);
+ }
+
+ std::shared_ptr<arrow::Array> build_timestamp_array(
+ const std::shared_ptr<arrow::DataType>& type,
+ const std::vector<int64_t>& values) {
+ arrow::TimestampBuilder builder(type, arrow::default_memory_pool());
+ for (const auto value : values) {
+ EXPECT_TRUE(builder.Append(value).ok());
+ }
+ return finish_array(&builder);
+ }
+
+ std::shared_ptr<arrow::Array> build_decimal_array(
+ const std::shared_ptr<arrow::DataType>& type,
+ const std::vector<int64_t>& values) {
+ arrow::Decimal128Builder builder(type, arrow::default_memory_pool());
+ for (const auto value : values) {
+ EXPECT_TRUE(builder.Append(arrow::Decimal128(value)).ok());
+ }
+ return finish_array(&builder);
+ }
+
+ void add_field(const std::shared_ptr<arrow::Field>& field, std::shared_ptr<arrow::Array> array,
+ std::function<void(const ParquetColumnSchema&, const IColumn&)> validator) {
+ _arrow_fields.push_back(field);
+ _arrays.push_back(std::move(array));
+ _expected_by_field.push_back(std::move(validator));
+ }
+
+ void write_parquet_file() {
+ add_field(arrow::field("bool_col", arrow::boolean(), false),
+ build_required_array<arrow::BooleanBuilder, bool>(
+ {true, false, true, false, true}),
+ [](const ParquetColumnSchema& schema, const IColumn& column) {
+ EXPECT_EQ(schema.type_descriptor.physical_type, ::parquet::Type::BOOLEAN);
+ const auto& values = assert_cast<const ColumnBool&>(column);
+ EXPECT_EQ(values.get_element(0), 1);
+ EXPECT_EQ(values.get_element(1), 0);
+ EXPECT_EQ(values.get_element(4), 1);
+ });
+ add_field(arrow::field("int32_col", arrow::int32(), false),
+ build_required_array<arrow::Int32Builder, int32_t>({10, 20, 30, 40, 50}),
+ [](const ParquetColumnSchema& schema, const IColumn& column) {
+ EXPECT_EQ(schema.type_descriptor.physical_type, ::parquet::Type::INT32);
+ const auto& values = assert_cast<const ColumnInt32&>(column);
+ EXPECT_EQ(values.get_element(0), 10);
+ EXPECT_EQ(values.get_element(4), 50);
+ });
+ add_field(arrow::field("int64_col", arrow::int64(), false),
+ build_required_array<arrow::Int64Builder, int64_t>(
+ {10000000000L, -9L, 42L, 77L, 123L}),
+ [](const ParquetColumnSchema& schema, const IColumn& column) {
+ EXPECT_EQ(schema.type_descriptor.physical_type, ::parquet::Type::INT64);
+ const auto& values = assert_cast<const ColumnInt64&>(column);
+ EXPECT_EQ(values.get_element(0), 10000000000L);
+ EXPECT_EQ(values.get_element(1), -9L);
+ });
+ add_field(arrow::field("float_col", arrow::float32(), false),
+ build_required_array<arrow::FloatBuilder, float>(
+ {1.5F, -2.25F, 3.0F, 4.5F, 5.75F}),
+ [](const ParquetColumnSchema& schema, const IColumn& column) {
+ EXPECT_EQ(schema.type_descriptor.physical_type, ::parquet::Type::FLOAT);
+ const auto& values = assert_cast<const ColumnFloat32&>(column);
+ EXPECT_FLOAT_EQ(values.get_element(0), 1.5F);
+ EXPECT_FLOAT_EQ(values.get_element(1), -2.25F);
+ });
+ add_field(arrow::field("double_col", arrow::float64(), false),
+ build_required_array<arrow::DoubleBuilder, double>(
+ {3.5, -4.75, 6.0, 7.25, 8.5}),
+ [](const ParquetColumnSchema& schema, const IColumn& column) {
+ EXPECT_EQ(schema.type_descriptor.physical_type, ::parquet::Type::DOUBLE);
+ const auto& values = assert_cast<const ColumnFloat64&>(column);
+ EXPECT_DOUBLE_EQ(values.get_element(0), 3.5);
+ EXPECT_DOUBLE_EQ(values.get_element(1), -4.75);
+ });
+ add_field(arrow::field("binary_col", arrow::binary(), false),
+ build_binary_array({"bin_a", "bin_b", "bin_c", "bin_d", "bin_e"}),
+ [](const ParquetColumnSchema& schema, const IColumn& column) {
+ EXPECT_EQ(schema.type_descriptor.physical_type, ::parquet::Type::BYTE_ARRAY);
+ const auto& values = assert_cast<const ColumnString&>(column);
+ EXPECT_EQ(values.get_data_at(0).to_string(), "bin_a");
+ EXPECT_EQ(values.get_data_at(3).to_string(), "bin_d");
+ });
+ add_field(arrow::field("string_col", arrow::utf8(), false),
+ build_string_array({"alpha", "beta", "gamma", "delta", "epsilon"}),
+ [](const ParquetColumnSchema& schema, const IColumn& column) {
+ EXPECT_TRUE(schema.type_descriptor.is_string_like);
+ const auto& values = assert_cast<const ColumnString&>(column);
+ EXPECT_EQ(values.get_data_at(0).to_string(), "alpha");
+ EXPECT_EQ(values.get_data_at(4).to_string(), "epsilon");
+ });
+ add_field(arrow::field("fixed_binary_col", arrow::fixed_size_binary(4), false),
+ build_fixed_binary_array(arrow::fixed_size_binary(4),
+ {"aaaa", "bbbb", "cccc", "dddd", "eeee"}),
+ [](const ParquetColumnSchema& schema, const IColumn& column) {
+ EXPECT_EQ(schema.type_descriptor.physical_type,
+ ::parquet::Type::FIXED_LEN_BYTE_ARRAY);
+ EXPECT_EQ(schema.type_descriptor.fixed_length, 4);
+ const auto& values = assert_cast<const ColumnString&>(column);
+ EXPECT_EQ(values.get_data_at(0).to_string(), "aaaa");
+ EXPECT_EQ(values.get_data_at(2).to_string(), "cccc");
+ });
+ add_field(arrow::field("date_col", arrow::date32(), false),
+ build_required_array<arrow::Date32Builder, int32_t>({0, 1, 18628, 18629, 18630}),
+ [](const ParquetColumnSchema& schema, const IColumn& column) {
+ EXPECT_EQ(schema.type_descriptor.physical_type, ::parquet::Type::INT32);
+ EXPECT_EQ(remove_nullable(schema.type)->get_primitive_type(), TYPE_DATEV2);
+ EXPECT_EQ(schema.type->to_string(column, 0), "1970-01-01");
+ EXPECT_EQ(schema.type->to_string(column, 2), "2021-01-01");
+ });
+ add_field(arrow::field("time_millis_col", arrow::time32(arrow::TimeUnit::MILLI), false),
+ build_time32_array(arrow::time32(arrow::TimeUnit::MILLI),
+ {0, 1000, 3723004, 43200000, 86399000}),
+ [](const ParquetColumnSchema& schema, const IColumn& column) {
+ EXPECT_EQ(schema.type_descriptor.physical_type, ::parquet::Type::INT32);
+ EXPECT_EQ(remove_nullable(schema.type)->get_primitive_type(), TYPE_TIMEV2);
+ EXPECT_EQ(schema.type->to_string(column, 1), "00:00:01.000");
+ EXPECT_EQ(schema.type->to_string(column, 2), "01:02:03.004");
+ });
+ add_field(arrow::field("time_micros_col", arrow::time64(arrow::TimeUnit::MICRO), false),
+ build_time64_array(arrow::time64(arrow::TimeUnit::MICRO),
+ {0, 1000000, 3723004567, 43200000000, 86399000000}),
+ [](const ParquetColumnSchema& schema, const IColumn& column) {
+ EXPECT_EQ(schema.type_descriptor.physical_type, ::parquet::Type::INT64);
+ EXPECT_EQ(remove_nullable(schema.type)->get_primitive_type(), TYPE_TIMEV2);
+ EXPECT_EQ(schema.type->to_string(column, 1), "00:00:01.000000");
+ EXPECT_EQ(schema.type->to_string(column, 2), "01:02:03.004567");
+ });
+ add_field(arrow::field("timestamp_millis_col",
+ arrow::timestamp(arrow::TimeUnit::MILLI), false),
+ build_timestamp_array(arrow::timestamp(arrow::TimeUnit::MILLI),
+ {0, 1234, 1609459200000, 1609459201000, -1}),
+ [](const ParquetColumnSchema& schema, const IColumn& column) {
+ EXPECT_EQ(schema.type_descriptor.physical_type, ::parquet::Type::INT64);
+ EXPECT_EQ(remove_nullable(schema.type)->get_primitive_type(),
+ TYPE_DATETIMEV2);
+ EXPECT_EQ(schema.type->to_string(column, 1),
+ "1970-01-01 00:00:01.234");
+ EXPECT_EQ(schema.type->to_string(column, 4),
+ "1969-12-31 23:59:59.999");
+ });
+ add_field(arrow::field("timestamp_micros_col",
+ arrow::timestamp(arrow::TimeUnit::MICRO), false),
+ build_timestamp_array(arrow::timestamp(arrow::TimeUnit::MICRO),
+ {0, 1234567, 1609459200000000, 1609459201000000, -1}),
+ [](const ParquetColumnSchema& schema, const IColumn& column) {
+ EXPECT_EQ(schema.type_descriptor.physical_type, ::parquet::Type::INT64);
+ EXPECT_EQ(remove_nullable(schema.type)->get_primitive_type(),
+ TYPE_DATETIMEV2);
+ EXPECT_EQ(schema.type->to_string(column, 1),
+ "1970-01-01 00:00:01.234567");
+ EXPECT_EQ(schema.type->to_string(column, 4),
+ "1969-12-31 23:59:59.999999");
+ });
+ add_field(arrow::field("decimal_fixed_binary_9_2_col", arrow::decimal128(9, 2), false),
+ build_decimal_array(arrow::decimal128(9, 2), {12345, -67, 0, 987, 1000}),
+ [](const ParquetColumnSchema& schema, const IColumn& column) {
+ EXPECT_EQ(schema.type_descriptor.physical_type,
+ ::parquet::Type::FIXED_LEN_BYTE_ARRAY);
+ EXPECT_TRUE(schema.type_descriptor.is_decimal);
+ EXPECT_EQ(remove_nullable(schema.type)->get_primitive_type(),
+ TYPE_DECIMAL32);
+ const auto& values = assert_cast<const ColumnDecimal32&>(column);
+ EXPECT_EQ(values.get_element(0), Decimal32(12345));
+ EXPECT_EQ(schema.type->to_string(column, 0), "123.45");
+ });
+ add_field(arrow::field("decimal_fixed_binary_18_6_col", arrow::decimal128(18, 6), false),
+ build_decimal_array(arrow::decimal128(18, 6),
+ {1234567, -670000, 0, 9870000, 1000000}),
+ [](const ParquetColumnSchema& schema, const IColumn& column) {
+ EXPECT_EQ(schema.type_descriptor.physical_type,
+ ::parquet::Type::FIXED_LEN_BYTE_ARRAY);
+ EXPECT_TRUE(schema.type_descriptor.is_decimal);
+ EXPECT_EQ(remove_nullable(schema.type)->get_primitive_type(),
+ TYPE_DECIMAL64);
+ const auto& values = assert_cast<const ColumnDecimal64&>(column);
+ EXPECT_EQ(values.get_element(0), Decimal64(1234567));
+ EXPECT_EQ(schema.type->to_string(column, 0), "1.234567");
+ });
+ add_field(arrow::field("nullable_int_col", arrow::int32(), true),
+ build_nullable_int32_array(),
+ [](const ParquetColumnSchema& schema, const IColumn& column) {
+ EXPECT_TRUE(schema.type->is_nullable());
+ const auto& nullable_column = assert_cast<const ColumnNullable&>(column);
+ const auto& nested_column =
+ assert_cast<const ColumnInt32&>(nullable_column.get_nested_column());
+ ASSERT_EQ(nullable_column.size(), ROW_COUNT);
+ EXPECT_FALSE(nullable_column.is_null_at(0));
+ EXPECT_TRUE(nullable_column.is_null_at(1));
+ EXPECT_FALSE(nullable_column.is_null_at(2));
+ EXPECT_TRUE(nullable_column.is_null_at(3));
+ EXPECT_EQ(nested_column.get_element(0), 1);
+ EXPECT_EQ(nested_column.get_element(2), 3);
+ });
+
+ auto schema = arrow::schema(_arrow_fields);
+ 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, ROW_COUNT,
+ builder.build()));
+ }
+
+ std::unique_ptr<ParquetColumnReader> create_reader(size_t field_idx) const {
+ ParquetColumnReaderFactory factory(_row_group, _file_reader->metadata()->num_columns());
+ std::unique_ptr<ParquetColumnReader> reader;
+ auto st = factory.create(*_fields[field_idx], &reader);
+ EXPECT_TRUE(st.ok()) << st;
+ return reader;
+ }
+
+ void read_and_validate(size_t field_idx) const {
+ auto reader = create_reader(field_idx);
+ MutableColumnPtr column = reader->type()->create_column();
+ int64_t rows_read = 0;
+ auto st = reader->read(ROW_COUNT, column, &rows_read);
+ ASSERT_TRUE(st.ok()) << st;
+ ASSERT_EQ(rows_read, ROW_COUNT);
+ ASSERT_EQ(column->size(), ROW_COUNT);
+ _expected_by_field[field_idx](*_fields[field_idx], *column);
+ }
+
+ std::filesystem::path _test_dir;
+ std::string _file_path;
+ std::unique_ptr<::parquet::ParquetFileReader> _file_reader;
+ std::shared_ptr<::parquet::RowGroupReader> _row_group;
+ std::vector<std::unique_ptr<ParquetColumnSchema>> _fields;
+ std::vector<std::shared_ptr<arrow::Field>> _arrow_fields;
+ std::vector<std::shared_ptr<arrow::Array>> _arrays;
+ std::vector<std::function<void(const ParquetColumnSchema&, const IColumn&)>> _expected_by_field;
+};
+
+TEST_F(ParquetColumnReaderTest, ReadAllSupportedPhysicalAndLogicalTypes) {
+ for (size_t field_idx = 0; field_idx < _fields.size(); ++field_idx) {
+ SCOPED_TRACE(_fields[field_idx]->name);
+ ASSERT_TRUE(supports_record_reader(_fields[field_idx]->type_descriptor));
+ read_and_validate(field_idx);
+ }
+}
+
+TEST_F(ParquetColumnReaderTest, SkipThenRead) {
+ auto reader = create_reader(1);
+ auto st = reader->skip(2);
+ ASSERT_TRUE(st.ok()) << st;
+
+ MutableColumnPtr column = reader->type()->create_column();
+ int64_t rows_read = 0;
+ st = reader->read(2, column, &rows_read);
+ ASSERT_TRUE(st.ok()) << st;
+ ASSERT_EQ(rows_read, 2);
+
+ const auto& int_values = assert_cast<const ColumnInt32&>(*column);
+ ASSERT_EQ(int_values.size(), 2);
+ EXPECT_EQ(int_values.get_element(0), 30);
+ EXPECT_EQ(int_values.get_element(1), 40);
+}
+
+TEST_F(ParquetColumnReaderTest, SelectReadsOnlySelectedRanges) {
+ auto reader = create_reader(1);
+ SelectionVector selection(3);
+ selection.set_index(0, 0);
+ selection.set_index(1, 2);
+ selection.set_index(2, 4);
+
+ MutableColumnPtr column = reader->type()->create_column();
+ auto st = reader->select(selection, 3, ROW_COUNT, column);
+ ASSERT_TRUE(st.ok()) << st;
+
+ const auto& int_values = assert_cast<const ColumnInt32&>(*column);
+ ASSERT_EQ(int_values.size(), 3);
+ EXPECT_EQ(int_values.get_element(0), 10);
+ EXPECT_EQ(int_values.get_element(1), 30);
+ EXPECT_EQ(int_values.get_element(2), 50);
+}
+
+TEST_F(ParquetColumnReaderTest, ResolveSupportedPhysicalAndLogicalSchemas) {
+ std::vector<::parquet::schema::NodePtr> nodes = {
+ ::parquet::schema::PrimitiveNode::Make(
+ "required_bool", ::parquet::Repetition::REQUIRED, ::parquet::Type::BOOLEAN),
+ ::parquet::schema::PrimitiveNode::Make(
+ "required_int32", ::parquet::Repetition::REQUIRED, ::parquet::Type::INT32),
+ ::parquet::schema::PrimitiveNode::Make(
+ "required_int64", ::parquet::Repetition::REQUIRED, ::parquet::Type::INT64),
+ ::parquet::schema::PrimitiveNode::Make(
+ "required_float", ::parquet::Repetition::REQUIRED, ::parquet::Type::FLOAT),
+ ::parquet::schema::PrimitiveNode::Make(
+ "required_double", ::parquet::Repetition::REQUIRED, ::parquet::Type::DOUBLE),
+ ::parquet::schema::PrimitiveNode::Make(
+ "required_binary", ::parquet::Repetition::REQUIRED, ::parquet::Type::BYTE_ARRAY),
+ ::parquet::schema::PrimitiveNode::Make(
+ "required_fixed_binary", ::parquet::Repetition::REQUIRED,
+ ::parquet::Type::FIXED_LEN_BYTE_ARRAY, ::parquet::ConvertedType::NONE, 4),
+ ::parquet::schema::PrimitiveNode::Make(
+ "optional_int32", ::parquet::Repetition::OPTIONAL, ::parquet::Type::INT32),
+ ::parquet::schema::PrimitiveNode::Make(
+ "utf8_binary", ::parquet::Repetition::REQUIRED, ::parquet::Type::BYTE_ARRAY,
+ ::parquet::ConvertedType::UTF8),
+ ::parquet::schema::PrimitiveNode::Make(
+ "enum_binary", ::parquet::Repetition::REQUIRED, ::parquet::Type::BYTE_ARRAY,
+ ::parquet::ConvertedType::ENUM),
+ ::parquet::schema::PrimitiveNode::Make(
+ "json_binary", ::parquet::Repetition::REQUIRED, ::parquet::Type::BYTE_ARRAY,
+ ::parquet::ConvertedType::JSON),
+ ::parquet::schema::PrimitiveNode::Make(
+ "bson_binary", ::parquet::Repetition::REQUIRED, ::parquet::Type::BYTE_ARRAY,
+ ::parquet::ConvertedType::BSON),
+ ::parquet::schema::PrimitiveNode::Make(
+ "decimal_int32", ::parquet::Repetition::REQUIRED, ::parquet::Type::INT32,
+ ::parquet::ConvertedType::DECIMAL, -1, 9, 2),
+ ::parquet::schema::PrimitiveNode::Make(
+ "decimal_int64", ::parquet::Repetition::REQUIRED, ::parquet::Type::INT64,
+ ::parquet::ConvertedType::DECIMAL, -1, 18, 6),
+ ::parquet::schema::PrimitiveNode::Make(
+ "decimal_binary", ::parquet::Repetition::REQUIRED, ::parquet::Type::BYTE_ARRAY,
+ ::parquet::ConvertedType::DECIMAL, -1, 18, 6),
+ ::parquet::schema::PrimitiveNode::Make(
+ "decimal_fixed_binary", ::parquet::Repetition::REQUIRED,
+ ::parquet::Type::FIXED_LEN_BYTE_ARRAY, ::parquet::ConvertedType::DECIMAL, 8,
+ 18, 6),
+ ::parquet::schema::PrimitiveNode::Make(
+ "date_int32", ::parquet::Repetition::REQUIRED, ::parquet::Type::INT32,
+ ::parquet::ConvertedType::DATE),
+ ::parquet::schema::PrimitiveNode::Make(
+ "time_millis_int32", ::parquet::Repetition::REQUIRED, ::parquet::Type::INT32,
+ ::parquet::ConvertedType::TIME_MILLIS),
+ ::parquet::schema::PrimitiveNode::Make(
+ "time_micros_int64", ::parquet::Repetition::REQUIRED, ::parquet::Type::INT64,
+ ::parquet::ConvertedType::TIME_MICROS),
+ ::parquet::schema::PrimitiveNode::Make(
+ "timestamp_millis_int64", ::parquet::Repetition::REQUIRED,
+ ::parquet::Type::INT64, ::parquet::ConvertedType::TIMESTAMP_MILLIS),
+ ::parquet::schema::PrimitiveNode::Make(
+ "timestamp_micros_int64", ::parquet::Repetition::REQUIRED,
+ ::parquet::Type::INT64, ::parquet::ConvertedType::TIMESTAMP_MICROS),
+ ::parquet::schema::PrimitiveNode::Make(
+ "int8_int32", ::parquet::Repetition::REQUIRED, ::parquet::Type::INT32,
+ ::parquet::ConvertedType::INT_8),
+ ::parquet::schema::PrimitiveNode::Make(
+ "uint8_int32", ::parquet::Repetition::REQUIRED, ::parquet::Type::INT32,
+ ::parquet::ConvertedType::UINT_8),
+ ::parquet::schema::PrimitiveNode::Make(
+ "int16_int32", ::parquet::Repetition::REQUIRED, ::parquet::Type::INT32,
+ ::parquet::ConvertedType::INT_16),
+ ::parquet::schema::PrimitiveNode::Make(
+ "uint16_int32", ::parquet::Repetition::REQUIRED, ::parquet::Type::INT32,
+ ::parquet::ConvertedType::UINT_16),
+ ::parquet::schema::PrimitiveNode::Make(
+ "int32_int32", ::parquet::Repetition::REQUIRED, ::parquet::Type::INT32,
+ ::parquet::ConvertedType::INT_32),
+ ::parquet::schema::PrimitiveNode::Make(
+ "uint32_int32", ::parquet::Repetition::REQUIRED, ::parquet::Type::INT32,
+ ::parquet::ConvertedType::UINT_32),
+ ::parquet::schema::PrimitiveNode::Make(
+ "int64_int64", ::parquet::Repetition::REQUIRED, ::parquet::Type::INT64,
+ ::parquet::ConvertedType::INT_64),
+ };
+
+ auto schema =
+ ::parquet::schema::GroupNode::Make("schema", ::parquet::Repetition::REQUIRED, nodes);
+ ::parquet::SchemaDescriptor descriptor;
+ descriptor.Init(schema);
+
+ std::vector<std::unique_ptr<ParquetColumnSchema>> fields;
+ auto st = build_parquet_column_schema(descriptor, &fields);
+ ASSERT_TRUE(st.ok()) << st;
+ ASSERT_EQ(fields.size(), nodes.size());
+
+ for (const auto& field : fields) {
+ SCOPED_TRACE(field->name);
+ ASSERT_TRUE(supports_record_reader(field->type_descriptor));
+ ASSERT_NE(field->type, nullptr);
+ }
+}
+
+TEST_F(ParquetColumnReaderTest, RejectUnsupportedPhysicalAndLogicalTypes) {
+ auto schema = ::parquet::schema::GroupNode::Make(
+ "schema", ::parquet::Repetition::REQUIRED,
+ {
+ ::parquet::schema::PrimitiveNode::Make(
+ "int96_col", ::parquet::Repetition::REQUIRED, ::parquet::Type::INT96),
+ ::parquet::schema::PrimitiveNode::Make(
+ "repeated_int32_col", ::parquet::Repetition::REPEATED,
+ ::parquet::Type::INT32),
+ ::parquet::schema::PrimitiveNode::Make(
+ "decimal256_fixed_col", ::parquet::Repetition::REQUIRED,
+ ::parquet::Type::FIXED_LEN_BYTE_ARRAY, ::parquet::ConvertedType::DECIMAL,
+ 20, 39, 6),
+ ::parquet::schema::PrimitiveNode::Make(
+ "uint64_col", ::parquet::Repetition::REQUIRED, ::parquet::Type::INT64,
+ ::parquet::ConvertedType::UINT_64),
+ ::parquet::schema::PrimitiveNode::Make(
+ "time_nanos_col", ::parquet::Repetition::REQUIRED,
+ ::parquet::LogicalType::Time(false,
+ ::parquet::LogicalType::TimeUnit::NANOS),
+ ::parquet::Type::INT64),
+ ::parquet::schema::PrimitiveNode::Make(
+ "timestamp_nanos_col", ::parquet::Repetition::REQUIRED,
+ ::parquet::LogicalType::Timestamp(
+ false, ::parquet::LogicalType::TimeUnit::NANOS),
+ ::parquet::Type::INT64),
+ });
+ ::parquet::SchemaDescriptor descriptor;
+ descriptor.Init(schema);
+
+ std::vector<std::unique_ptr<ParquetColumnSchema>> fields;
+ auto st = build_parquet_column_schema(descriptor, &fields);
+ ASSERT_TRUE(st.ok()) << st;
+ ASSERT_EQ(fields.size(), 6);
+
+ for (const auto& field : fields) {
+ SCOPED_TRACE(field->name);
+ ASSERT_FALSE(supports_record_reader(field->type_descriptor));
+ }
+}
+
+} // namespace
+} // namespace doris::parquet
diff --git a/be/test/format/new_parquet/parquet_reader_test.cpp b/be/test/format/new_parquet/parquet_reader_test.cpp
new file mode 100644
index 00000000000..2086cd8cfb7
--- /dev/null
+++ b/be/test/format/new_parquet/parquet_reader_test.cpp
@@ -0,0 +1,341 @@
+// 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 <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 <string>
+#include <vector>
+
+#include "core/assert_cast.h"
+#include "core/block/block.h"
+#include "core/column/column_string.h"
+#include "core/column/column_vector.h"
+#include "core/data_type/data_type_number.h"
+#include "core/data_type/primitive_type.h"
+#include "core/field.h"
+#include "format/new_parquet/parquet_reader.h"
+#include "format/reader/file_reader.h"
+#include "gen_cpp/Types_types.h"
+#include "io/io_common.h"
+#include "runtime/runtime_state.h"
+#include "storage/predicate/predicate_creator.h"
+
+namespace doris {
+namespace {
+
+constexpr int64_t ROW_COUNT = 5;
+
+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_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);
+}
+
+void write_parquet_file(const std::string& file_path, int64_t row_group_size = ROW_COUNT) {
+ auto schema = arrow::schema({
+ arrow::field("id", arrow::int32(), false),
+ arrow::field("value", arrow::utf8(), false),
+ });
+ auto table = arrow::Table::Make(
+ schema, {build_int32_array({1, 2, 3, 4, 5}),
+ build_string_array({"one", "two", "three", "four", "five"})});
+
+ 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, row_group_size, builder.build()));
+}
+
+Block build_file_block(const std::vector<reader::SchemaField>& schema) {
+ Block block;
+ for (const auto& field : schema) {
+ block.insert({field.type->create_column(), field.type, field.name});
+ }
+ return block;
+}
+
+class TestFileReader final : public reader::FileReader {
+public:
+ TestFileReader(std::unique_ptr<io::FileSystemProperties>& system_properties,
+ std::unique_ptr<io::FileDescription>& file_description,
+ std::shared_ptr<io::IOContext> io_ctx)
+ : reader::FileReader(system_properties, file_description, std::move(io_ctx), nullptr) {}
+
+ Status get_schema(std::vector<reader::SchemaField>* file_schema) const override {
+ file_schema->clear();
+ reader::SchemaField field;
+ field.id = 0;
+ field.name = "id";
+ field.type = std::make_shared<DataTypeInt32>();
+ file_schema->push_back(std::move(field));
+ return Status::OK();
+ }
+
+ bool has_request() const { return _request != nullptr; }
+
+ bool eof() const { return _eof; }
+};
+
+TEST(FileReaderTest, OpenStoresRequestAndCloseClearsState) {
+ auto system_properties = std::make_unique<io::FileSystemProperties>();
+ system_properties->system_type = TFileType::FILE_LOCAL;
+ auto file_description = std::make_unique<io::FileDescription>();
+ auto io_ctx = std::make_shared<io::IOContext>();
+ TestFileReader reader(system_properties, file_description, io_ctx);
+
+ auto request = std::make_unique<reader::FileScanRequest>();
+ request->non_predicate_columns.push_back(0);
+ ASSERT_TRUE(reader.open(request).ok());
+ EXPECT_EQ(request, nullptr);
+ EXPECT_TRUE(reader.has_request());
+
+ ASSERT_TRUE(reader.close().ok());
+ EXPECT_FALSE(reader.has_request());
+ EXPECT_TRUE(reader.eof());
+}
+
+class NewParquetReaderTest : public testing::Test {
+protected:
+ void SetUp() override {
+ _test_dir = std::filesystem::temp_directory_path() / "doris_new_parquet_reader_test";
+ std::filesystem::remove_all(_test_dir);
+ std::filesystem::create_directories(_test_dir);
+ _file_path = (_test_dir / "reader.parquet").string();
+ write_parquet_file(_file_path);
+ }
+
+ void TearDown() override { std::filesystem::remove_all(_test_dir); }
+
+ std::unique_ptr<parquet::ParquetReader> create_reader() const {
+ auto system_properties = std::make_unique<io::FileSystemProperties>();
+ system_properties->system_type = TFileType::FILE_LOCAL;
+ auto file_description = std::make_unique<io::FileDescription>();
+ file_description->path = _file_path;
+ file_description->file_size = static_cast<int64_t>(std::filesystem::file_size(_file_path));
+ return std::make_unique<parquet::ParquetReader>(system_properties, file_description,
+ nullptr, nullptr);
+ }
+
+ std::filesystem::path _test_dir;
+ std::string _file_path;
+};
+
+TEST_F(NewParquetReaderTest, GetSchemaReturnsFileLocalColumns) {
+ auto reader = create_reader();
+ RuntimeState state {TQueryOptions(), TQueryGlobals()};
+ ASSERT_TRUE(reader->init(&state).ok());
+
+ std::vector<reader::SchemaField> schema;
+ ASSERT_TRUE(reader->get_schema(&schema).ok());
+ ASSERT_EQ(schema.size(), 2);
+ EXPECT_EQ(schema[0].id, 0);
+ EXPECT_EQ(schema[0].name, "id");
+ EXPECT_EQ(schema[0].type->get_primitive_type(), TYPE_INT);
+ EXPECT_EQ(schema[1].id, 1);
+ EXPECT_EQ(schema[1].name, "value");
+ EXPECT_EQ(schema[1].type->get_primitive_type(), TYPE_STRING);
+}
+
+TEST_F(NewParquetReaderTest, ReadSingleRowGroupThenEof) {
+ auto reader = create_reader();
+ RuntimeState state {TQueryOptions(), TQueryGlobals()};
+ ASSERT_TRUE(reader->init(&state).ok());
+
+ std::vector<reader::SchemaField> schema;
+ ASSERT_TRUE(reader->get_schema(&schema).ok());
+ Block block = build_file_block(schema);
+
+ auto request = std::make_unique<reader::FileScanRequest>();
+ request->non_predicate_columns = {0, 1};
+ ASSERT_TRUE(reader->open(request).ok());
+
+ size_t rows = 0;
+ bool eof = false;
+ ASSERT_TRUE(reader->get_block(&block, &rows, &eof).ok());
+ EXPECT_FALSE(eof);
+ ASSERT_EQ(rows, ROW_COUNT);
+
+ const auto& ids = assert_cast<const ColumnInt32&>(*block.get_by_position(0).column);
+ const auto& values = assert_cast<const ColumnString&>(*block.get_by_position(1).column);
+ ASSERT_EQ(ids.size(), ROW_COUNT);
+ ASSERT_EQ(values.size(), ROW_COUNT);
+ EXPECT_EQ(ids.get_element(0), 1);
+ EXPECT_EQ(ids.get_element(4), 5);
+ EXPECT_EQ(values.get_data_at(0).to_string(), "one");
+ EXPECT_EQ(values.get_data_at(4).to_string(), "five");
+
+ rows = 0;
+ eof = false;
+ ASSERT_TRUE(reader->get_block(&block, &rows, &eof).ok());
+ EXPECT_TRUE(eof);
+ EXPECT_EQ(rows, 0);
+}
+
+TEST_F(NewParquetReaderTest, ReadMultipleRowGroups) {
+ write_parquet_file(_file_path, 2);
+ auto parquet_file_reader = ::parquet::ParquetFileReader::OpenFile(_file_path, false);
+ ASSERT_EQ(parquet_file_reader->metadata()->num_row_groups(), 3);
+
+ auto reader = create_reader();
+ RuntimeState state {TQueryOptions(), TQueryGlobals()};
+ ASSERT_TRUE(reader->init(&state).ok());
+
+ std::vector<reader::SchemaField> schema;
+ ASSERT_TRUE(reader->get_schema(&schema).ok());
+ auto request = std::make_unique<reader::FileScanRequest>();
+ request->non_predicate_columns = {0, 1};
+ ASSERT_TRUE(reader->open(request).ok());
+
+ std::vector<int32_t> ids;
+ std::vector<std::string> values;
+ bool eof = false;
+ while (!eof) {
+ Block block = build_file_block(schema);
+ size_t rows = 0;
+ ASSERT_TRUE(reader->get_block(&block, &rows, &eof).ok());
+ if (rows == 0) {
+ continue;
+ }
+ const auto& id_column = assert_cast<const ColumnInt32&>(*block.get_by_position(0).column);
+ const auto& value_column =
+ assert_cast<const ColumnString&>(*block.get_by_position(1).column);
+ for (size_t row = 0; row < rows; ++row) {
+ ids.push_back(id_column.get_element(row));
+ values.push_back(value_column.get_data_at(row).to_string());
+ }
+ }
+
+ EXPECT_EQ(ids, std::vector<int32_t>({1, 2, 3, 4, 5}));
+ EXPECT_EQ(values, std::vector<std::string>({"one", "two", "three", "four", "five"}));
+}
+
+TEST_F(NewParquetReaderTest, ReadPredicateAndNonPredicateColumnsWithSelection) {
+ auto reader = create_reader();
+ RuntimeState state {TQueryOptions(), TQueryGlobals()};
+ ASSERT_TRUE(reader->init(&state).ok());
+
+ std::vector<reader::SchemaField> schema;
+ ASSERT_TRUE(reader->get_schema(&schema).ok());
+ Block block = build_file_block(schema);
+
+ auto request = std::make_unique<reader::FileScanRequest>();
+ request->predicate_columns = {0};
+ request->non_predicate_columns = {1};
+ reader::FileLocalFilter filter;
+ filter.file_column_id = 0;
+ filter.predicates.push_back(create_comparison_predicate<PredicateType::GT>(
+ 0, "id", schema[0].type, Field::create_field<TYPE_INT>(2), false));
+ request->local_filters.push_back(std::move(filter));
+ ASSERT_TRUE(reader->open(request).ok());
+
+ size_t rows = 0;
+ bool eof = false;
+ ASSERT_TRUE(reader->get_block(&block, &rows, &eof).ok());
+ EXPECT_FALSE(eof);
+ ASSERT_EQ(rows, 3);
+
+ const auto& ids = assert_cast<const ColumnInt32&>(*block.get_by_position(0).column);
+ const auto& values = assert_cast<const ColumnString&>(*block.get_by_position(1).column);
+ ASSERT_EQ(ids.size(), 3);
+ ASSERT_EQ(values.size(), 3);
+ EXPECT_EQ(ids.get_element(0), 3);
+ EXPECT_EQ(ids.get_element(1), 4);
+ EXPECT_EQ(ids.get_element(2), 5);
+ EXPECT_EQ(values.get_data_at(0).to_string(), "three");
+ EXPECT_EQ(values.get_data_at(1).to_string(), "four");
+ EXPECT_EQ(values.get_data_at(2).to_string(), "five");
+
+ rows = 0;
+ eof = false;
+ ASSERT_TRUE(reader->get_block(&block, &rows, &eof).ok());
+ EXPECT_TRUE(eof);
+ EXPECT_EQ(rows, 0);
+}
+
+TEST_F(NewParquetReaderTest, PredicateFiltersRowGroupsByStatistics) {
+ write_parquet_file(_file_path, 2);
+ auto parquet_file_reader = ::parquet::ParquetFileReader::OpenFile(_file_path, false);
+ ASSERT_EQ(parquet_file_reader->metadata()->num_row_groups(), 3);
+
+ auto reader = create_reader();
+ RuntimeState state {TQueryOptions(), TQueryGlobals()};
+ ASSERT_TRUE(reader->init(&state).ok());
+
+ std::vector<reader::SchemaField> schema;
+ ASSERT_TRUE(reader->get_schema(&schema).ok());
+ auto request = std::make_unique<reader::FileScanRequest>();
+ request->non_predicate_columns = {0, 1};
+ reader::FileLocalFilter filter;
+ filter.file_column_id = 0;
+ filter.predicates.push_back(create_comparison_predicate<PredicateType::GT>(
+ 0, "id", schema[0].type, Field::create_field<TYPE_INT>(2), false));
+ request->local_filters.push_back(std::move(filter));
+ ASSERT_TRUE(reader->open(request).ok());
+
+ std::vector<int32_t> ids;
+ std::vector<std::string> values;
+ bool eof = false;
+ while (!eof) {
+ Block block = build_file_block(schema);
+ size_t rows = 0;
+ ASSERT_TRUE(reader->get_block(&block, &rows, &eof).ok());
+ if (rows == 0) {
+ continue;
+ }
+ const auto& id_column = assert_cast<const ColumnInt32&>(*block.get_by_position(0).column);
+ const auto& value_column =
+ assert_cast<const ColumnString&>(*block.get_by_position(1).column);
+ for (size_t row = 0; row < rows; ++row) {
+ ids.push_back(id_column.get_element(row));
+ values.push_back(value_column.get_data_at(row).to_string());
+ }
+ }
+
+ EXPECT_EQ(ids, std::vector<int32_t>({3, 4, 5}));
+ EXPECT_EQ(values, std::vector<std::string>({"three", "four", "five"}));
+}
+
+} // namespace
+} // namespace doris
diff --git a/docs/doris-arrow-parquet-reader-implementation.md b/docs/doris-arrow-parquet-reader-implementation.md
new file mode 100644
index 00000000000..b3bfe5c69e0
--- /dev/null
+++ b/docs/doris-arrow-parquet-reader-implementation.md
@@ -0,0 +1,291 @@
+# Doris New Parquet Reader Design And Status
+
+This document describes the design and current implementation status of the new
+Parquet reader under `be/src/format/new_parquet/`.
+
+The goal of this PR is to build a file-local Parquet reader based on Arrow C++
+Parquet core APIs while keeping Doris-owned `Block` and `Column` as the scan
+output. It does not replace the old `vparquet` path yet.
+
+## Design Goals
+
+- Use Arrow C++ Parquet core APIs for Parquet file metadata, row group and column
+ decoding.
+- Keep `doris::parquet::ParquetReader` as a file-local reader.
+- Do not use `parquet::arrow::FileReader`, `arrow::RecordBatch`,
+ `arrow::Table` or `arrow::Array` as the scan output path.
+- Do not put Iceberg table schema, schema evolution, default columns, generated
+ columns or partition columns into `ParquetReader`.
+- Keep table schema mapping and filter localization in the reader/table layer,
+ especially `TableColumnMapper`.
+- Let the new implementation live in `be/src/format/new_parquet/` so it can
+ evolve independently from the old `be/src/format/parquet/` implementation.
+
+## Layering
+
+```text
+TableReader / IcebergTableReader
+ -> TableColumnMapper
+ -> reader::FileScanRequest
+ -> doris::parquet::ParquetReader
+ -> DorisRandomAccessFile
+ -> parquet::ParquetFileReader
+ -> parquet::RowGroupReader
+ -> parquet::ColumnReader / parquet::internal::RecordReader
+ -> Doris Block / Column
+```
+
+`ParquetReader` only consumes file-local information:
+
+- file-local schema fields;
+- file-local projection columns;
+- file-local predicate columns;
+- file-local `ColumnPredicate` and `VExprContext` filters.
+
+Any table-level cast, default value, generated column, partition value, Iceberg
+field id mapping or schema evolution rule must be handled before or after the
+file reader layer.
+
+## Code Layout
+
+```text
+be/src/format/new_parquet/parquet_reader.h
+be/src/format/new_parquet/parquet_reader.cpp
+be/src/format/new_parquet/column_reader.h
+be/src/format/new_parquet/column_reader.cpp
+be/src/format/new_parquet/parquet_statistics.h
+be/src/format/new_parquet/parquet_statistics.cpp
+```
+
+`parquet_reader.*` owns file open, schema export, scan state, row group
+scheduling, predicate-first reading and output block assembly.
+
+`column_reader.*` owns Doris column assembly for one projected Parquet field. It
+wraps Arrow Parquet column-level APIs and converts decoded values into Doris
+columns.
+
+`parquet_statistics.*` owns row group statistics pruning. Future page index,
+bloom filter and dictionary pruning should also live there rather than being
+mixed into the main scan loop.
+
+## Main Components
+
+### DorisRandomAccessFile
+
+`DorisRandomAccessFile` adapts Doris `io::FileReader` to
+`arrow::io::RandomAccessFile`.
+
+It only handles random IO and file size lookup. It does not parse Parquet schema,
+does not evaluate filters, and does not carry table-level semantics.
+
+### ParquetReaderScanState
+
+`ParquetReaderScanState` is an internal scan state stored in
+`parquet_reader.cpp`. It tracks:
+
+- Arrow random access file;
+- Arrow Parquet file reader and metadata;
+- Parquet schema descriptor;
+- selected row groups;
+- current row group reader;
+- current row group row offset;
+- projected file columns;
+- predicate columns and non-predicate output columns;
+- current row group column readers.
+
+This state is intentionally private to the Parquet reader implementation.
+
+### ParquetColumnReader
+
+`ParquetColumnReader` is Doris's file-local column reader abstraction. It is not
+the same as Arrow's `parquet::ColumnReader`.
+
+Current implementations:
+
+- `PrimitiveColumnReader`
+- `StructColumnReader`
+
+`PrimitiveColumnReader` supports both the existing Arrow
+`parquet::TypedColumnReader` path and the new
+`parquet::internal::RecordReader` path for selected primitive reads.
+
+`StructColumnReader` currently supports basic required struct assembly by
+recursively reading child readers. Complex nested selective materialization is
+not complete.
+
+### ParquetColumnReaderFactory
+
+`ParquetColumnReaderFactory` creates Doris column readers from the current row
+group's Arrow Parquet readers and the file-local `ParquetColumnSchema`.
+
+The factory centralizes reader construction so later work can add reader
+options, Dremel assemblers, selected-read policies and cache state without
+passing those details through free functions.
+
+### ParquetStatisticsUtils
+
+`ParquetStatisticsUtils` compiles file-local predicates into Parquet column
+predicate plans and evaluates row group metadata conservatively.
+
+It only understands Parquet file-local schema and Doris `ColumnPredicate`. It
+does not know Iceberg schema, slot descriptors or table schema mapping.
+
+## Scan Request Semantics
+
+The new reader consumes `reader::FileScanRequest`.
+
+Important fields:
+
+- `predicate_columns`: file-local columns that must be read first to evaluate
+ filters.
+- `non_predicate_columns`: file-local projection columns that are only read
+ after selection is known.
+- `projected_columns`: file-local columns that should appear in the output block.
+- `local_filters`: file-local filters produced by the table layer.
+- `reader_expression_map`: fallback expressions for filters that cannot be
+ represented as direct file-local predicates.
+
+The output block is still file-local. It is not a table/global schema block.
+
+## Predicate Pushdown
+
+Doris new reader uses two existing filter representations:
+
+- `ColumnPredicate`: structured single-column predicates, used for row group
+ statistics pruning and decoded value filtering.
+- `VExprContext`: expression filters, used for fallback expression evaluation
+ and residual filters.
+
+Current implementation status:
+
+- row group min/max pruning is wired through `parquet_statistics.*`;
+- supported stats types include boolean, int32, int64, float, double and
+ string/binary;
+- unsupported stats, missing stats or unsafe cases keep the row group;
+- `IS NULL` and `IS NOT NULL` pruning use null count when available;
+- page index, bloom filter and dictionary pruning are not implemented yet.
+
+Correctness rule: pruning must be conservative. If the reader cannot prove that
+a row group cannot match, it must keep the row group.
+
+## Lazy Materialization
+
+The scan loop follows a predicate-first model:
+
+1. Read predicate columns.
+2. Evaluate `ColumnPredicate` and build a selection vector.
+3. If a predicate column is also projected, reuse the decoded predicate column.
+4. Read non-predicate output columns using the selection.
+5. Assemble the file-local output block in projected column order.
+
+The current selected-read implementation uses Arrow Parquet
+`parquet::internal::RecordReader` for supported primitive columns.
+
+Why this is needed:
+
+- `parquet::TypedColumnReader::Skip` skips physical values, not SQL rows.
+- For nullable columns, row count and physical value count differ.
+- For repeated/nested columns, a row can contain multiple physical values.
+
+`RecordReader::SkipRecords` and `RecordReader::ReadRecords` provide row-level
+movement. Doris compresses the selection vector into row ranges and alternates
+skip/read operations.
+
+Current support:
+
+- selected read for primitive boolean, int32, int64, float and double when the
+ RecordReader path is available;
+- fallback path reads the whole batch and filters it when selected read is not
+ supported;
+- output columns are skipped when the selection is empty;
+- predicate columns are reused when they are also projected.
+
+Limitations:
+
+- `parquet::internal::RecordReader` is an Arrow internal/experimental API, so it
+ must remain hidden behind Doris `ParquetColumnReader`;
+- string, decimal and timestamp selected reads still need broader validation;
+- nested selected materialization needs a dedicated Dremel assembler.
+
+## Type Coverage
+
+Currently implemented:
+
+- flat required and nullable boolean;
+- flat required and nullable int32 / int64;
+- flat required and nullable float / double;
+- BYTE_ARRAY and FIXED_LEN_BYTE_ARRAY string/binary with Doris-owned memory;
+- decimal with precision up to 38 for int32, int64, byte array and fixed-length
+ byte array physical encodings;
+- INT64 timestamp millis and micros into Doris `DateTimeV2`;
+- basic required struct assembly.
+
+Not implemented or incomplete:
+
+- INT96 timestamp;
+- nanosecond timestamp;
+- TIMESTAMPTZ semantics;
+- DECIMAL256;
+- nullable struct;
+- list and map;
+- complex column pruning;
+- complex column lazy materialization.
+
+## Current Implementation Status
+
+Implemented in this PR:
+
+- new `new_parquet` module;
+- Arrow-backed Parquet file open and metadata read;
+- file-local schema export;
+- row group scheduling;
+- projected leaf reader creation;
+- primitive column decoding into Doris columns;
+- string, decimal and INT64 timestamp decoding;
+- basic struct reader;
+- row group statistics pruning skeleton and initial implementation;
+- predicate-first scan flow;
+- primitive RecordReader-backed selected materialization;
+- Debug BE build fixes.
+
+Validated:
+
+- `git diff --check`;
+- `BUILD_TYPE=DEBUG ./build.sh --be` on
+ `fedora:/home/socrates/code/doris`.
+
+## Future Work
+
+Near term:
+
+- add unit tests for primitive required/nullable selected reads;
+- validate selection edge cases: empty selection, full selection, sparse
+ selection and highly fragmented ranges;
+- add a selection-rate policy so dense selections can fall back to whole-batch
+ read plus filter;
+- stabilize string, decimal and timestamp selected reads;
+- keep Arrow internal API usage isolated in `column_reader.*`.
+
+Mid term:
+
+- implement page index pruning in `parquet_statistics.*`;
+- implement bloom filter pruning for equality predicates;
+- add dictionary-aware filtering where Arrow exposes enough metadata safely;
+- expand complex type assembly for nullable struct, list and map;
+- add tests for row group pruning correctness and unsupported-type fallback.
+
+Long term:
+
+- support nested column pruning;
+- support nested lazy materialization;
+- support page-level row range selection;
+- integrate the new file-local reader with table readers after the API boundary
+ is stable;
+- keep old `vparquet` compatibility until the new path is functionally complete.
+
+## Key Rule
+
+`ParquetReader` must remain a file-local reader. If a feature requires table
+schema, Iceberg schema evolution, partition values, default/generated columns or
+final table output semantics, it belongs in `TableColumnMapper` or
+`TableReader`, not in `be/src/format/new_parquet/`.
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@doris.apache.org
For additional commands, e-mail: commits-help@doris.apache.org