This is an automated email from the ASF dual-hosted git repository.
lxy-9602 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-cpp.git
The following commit(s) were added to refs/heads/main by this push:
new 3366987b fix(parquet): decode DECIMAL page statistics by physical type
(#346)
3366987b is described below
commit 3366987bc90ebb6f03254ac91a2ccd6a9a668b96
Author: Yunpei Zhou <[email protected]>
AuthorDate: Tue Sep 15 17:14:20 2026 +0800
fix(parquet): decode DECIMAL page statistics by physical type (#346)
---
src/paimon/format/parquet/column_index_filter.cpp | 188 +++++++++++++--------
src/paimon/format/parquet/column_index_filter.h | 51 ++++--
.../format/parquet/column_index_filter_test.cpp | 165 +++++++++++++++---
src/paimon/format/parquet/file_reader_wrapper.cpp | 6 +-
4 files changed, 298 insertions(+), 112 deletions(-)
diff --git a/src/paimon/format/parquet/column_index_filter.cpp
b/src/paimon/format/parquet/column_index_filter.cpp
index 1e0a8f59..74dc37fe 100644
--- a/src/paimon/format/parquet/column_index_filter.cpp
+++ b/src/paimon/format/parquet/column_index_filter.cpp
@@ -39,27 +39,29 @@ namespace paimon::parquet {
Result<RowRanges> ColumnIndexFilter::CalculateRowRanges(
const std::shared_ptr<Predicate>& predicate,
const std::shared_ptr<::parquet::RowGroupPageIndexReader>&
rg_page_index_reader,
- const std::map<std::string, int32_t>& column_name_to_index, int64_t
row_group_row_count) {
+ const std::map<std::string, int32_t>& column_name_to_index,
+ const ::parquet::SchemaDescriptor* schema, int64_t row_group_row_count) {
if (!predicate || !rg_page_index_reader) {
return RowRanges::CreateSingle(row_group_row_count);
}
- return VisitPredicate(predicate, column_name_to_index, row_group_row_count,
+ return VisitPredicate(predicate, column_name_to_index, schema,
row_group_row_count,
rg_page_index_reader.get());
}
Result<RowRanges> ColumnIndexFilter::VisitPredicate(
const std::shared_ptr<Predicate>& predicate,
- const std::map<std::string, int32_t>& column_name_to_index, int64_t
row_group_row_count,
+ const std::map<std::string, int32_t>& column_name_to_index,
+ const ::parquet::SchemaDescriptor* schema, int64_t row_group_row_count,
::parquet::RowGroupPageIndexReader* rg_page_index_reader) {
if (auto leaf_predicate =
std::dynamic_pointer_cast<LeafPredicate>(predicate)) {
- return VisitLeafPredicate(leaf_predicate, column_name_to_index,
row_group_row_count,
+ return VisitLeafPredicate(leaf_predicate, column_name_to_index,
schema, row_group_row_count,
rg_page_index_reader);
}
if (auto compound_predicate =
std::dynamic_pointer_cast<CompoundPredicate>(predicate)) {
- return VisitCompoundPredicate(compound_predicate,
column_name_to_index, row_group_row_count,
- rg_page_index_reader);
+ return VisitCompoundPredicate(compound_predicate,
column_name_to_index, schema,
+ row_group_row_count,
rg_page_index_reader);
}
return Status::Invalid("Unknown predicate type");
@@ -67,7 +69,8 @@ Result<RowRanges> ColumnIndexFilter::VisitPredicate(
Result<RowRanges> ColumnIndexFilter::VisitLeafPredicate(
const std::shared_ptr<LeafPredicate>& leaf_predicate,
- const std::map<std::string, int32_t>& column_name_to_index, int64_t
row_group_row_count,
+ const std::map<std::string, int32_t>& column_name_to_index,
+ const ::parquet::SchemaDescriptor* schema, int64_t row_group_row_count,
::parquet::RowGroupPageIndexReader* rg_page_index_reader) {
const std::string& field_name = leaf_predicate->FieldName();
auto it = column_name_to_index.find(field_name);
@@ -92,6 +95,14 @@ Result<RowRanges> ColumnIndexFilter::VisitLeafPredicate(
const auto& literals = leaf_predicate->Literals();
FieldType field_type = leaf_predicate->GetFieldType();
+ // The physical type decides how the encoded page statistics are laid out.
It cannot
+ // be inferred from the length of the encoded bounds: a DECIMAL stored as
a 4-byte
+ // FIXED_LEN_BYTE_ARRAY (precision 7-9) is indistinguishable from an INT32
one by
+ // length alone, yet is big-endian rather than little-endian.
+ ::parquet::Type::type physical_type = ::parquet::Type::UNDEFINED;
+ if (schema && column_index < schema->num_columns()) {
+ physical_type = schema->Column(column_index)->physical_type();
+ }
if (function_type != Function::Type::IS_NULL && function_type !=
Function::Type::IS_NOT_NULL &&
literals.empty()) {
@@ -108,25 +119,31 @@ Result<RowRanges> ColumnIndexFilter::VisitLeafPredicate(
matching_pages = FilterPagesByIsNotNull(column_index_ptr);
break;
case Function::Type::EQUAL:
- matching_pages = FilterPagesByEqual(column_index_ptr, literals[0],
field_type);
+ matching_pages =
+ FilterPagesByEqual(column_index_ptr, literals[0], field_type,
physical_type);
break;
case Function::Type::NOT_EQUAL:
- matching_pages = FilterPagesByNotEqual(column_index_ptr,
literals[0], field_type);
+ matching_pages =
+ FilterPagesByNotEqual(column_index_ptr, literals[0],
field_type, physical_type);
break;
case Function::Type::LESS_THAN:
- matching_pages = FilterPagesByLessThan(column_index_ptr,
literals[0], field_type);
+ matching_pages =
+ FilterPagesByLessThan(column_index_ptr, literals[0],
field_type, physical_type);
break;
case Function::Type::LESS_OR_EQUAL:
- matching_pages = FilterPagesByLessOrEqual(column_index_ptr,
literals[0], field_type);
+ matching_pages =
+ FilterPagesByLessOrEqual(column_index_ptr, literals[0],
field_type, physical_type);
break;
case Function::Type::GREATER_THAN:
- matching_pages = FilterPagesByGreaterThan(column_index_ptr,
literals[0], field_type);
+ matching_pages =
+ FilterPagesByGreaterThan(column_index_ptr, literals[0],
field_type, physical_type);
break;
case Function::Type::GREATER_OR_EQUAL:
- matching_pages = FilterPagesByGreaterOrEqual(column_index_ptr,
literals[0], field_type);
+ matching_pages = FilterPagesByGreaterOrEqual(column_index_ptr,
literals[0], field_type,
+ physical_type);
break;
case Function::Type::IN:
- matching_pages = FilterPagesByIn(column_index_ptr, literals,
field_type);
+ matching_pages = FilterPagesByIn(column_index_ptr, literals,
field_type, physical_type);
break;
case Function::Type::NOT_IN:
matching_pages = FilterPagesByNotIn(column_index_ptr, literals);
@@ -141,7 +158,8 @@ Result<RowRanges> ColumnIndexFilter::VisitLeafPredicate(
Result<RowRanges> ColumnIndexFilter::VisitCompoundPredicate(
const std::shared_ptr<CompoundPredicate>& compound_predicate,
- const std::map<std::string, int32_t>& column_name_to_index, int64_t
row_group_row_count,
+ const std::map<std::string, int32_t>& column_name_to_index,
+ const ::parquet::SchemaDescriptor* schema, int64_t row_group_row_count,
::parquet::RowGroupPageIndexReader* rg_page_index_reader) {
const auto& children = compound_predicate->Children();
const auto& function = compound_predicate->GetFunction();
@@ -153,8 +171,8 @@ Result<RowRanges> ColumnIndexFilter::VisitCompoundPredicate(
// Calculate row ranges for first child
PAIMON_ASSIGN_OR_RAISE(RowRanges result,
- VisitPredicate(children[0], column_name_to_index,
row_group_row_count,
- rg_page_index_reader));
+ VisitPredicate(children[0], column_name_to_index,
schema,
+ row_group_row_count,
rg_page_index_reader));
if (function_type == Function::Type::AND) {
// Short-circuit: if result is empty, no need to continue
@@ -164,7 +182,7 @@ Result<RowRanges> ColumnIndexFilter::VisitCompoundPredicate(
for (size_t i = 1; i < children.size(); ++i) {
PAIMON_ASSIGN_OR_RAISE(RowRanges child_ranges,
- VisitPredicate(children[i],
column_name_to_index,
+ VisitPredicate(children[i],
column_name_to_index, schema,
row_group_row_count,
rg_page_index_reader));
result = RowRanges::Intersection(result, child_ranges);
@@ -182,7 +200,7 @@ Result<RowRanges> ColumnIndexFilter::VisitCompoundPredicate(
for (size_t i = 1; i < children.size(); ++i) {
PAIMON_ASSIGN_OR_RAISE(RowRanges child_ranges,
- VisitPredicate(children[i],
column_name_to_index,
+ VisitPredicate(children[i],
column_name_to_index, schema,
row_group_row_count,
rg_page_index_reader));
result = RowRanges::Union(result, child_ranges);
@@ -201,7 +219,7 @@ Result<RowRanges> ColumnIndexFilter::VisitCompoundPredicate(
std::vector<int32_t> ColumnIndexFilter::FilterPagesByEqual(
const std::shared_ptr<::parquet::ColumnIndex>& column_index, const
Literal& literal,
- FieldType field_type) {
+ FieldType field_type, ::parquet::Type::type physical_type) {
std::vector<int32_t> matching_pages;
if (literal.IsNull()) {
@@ -219,7 +237,8 @@ std::vector<int32_t> ColumnIndexFilter::FilterPagesByEqual(
continue;
}
- if (PageMightContainEqual(min_values[i], max_values[i], literal,
field_type)) {
+ if (PageMightContainEqual(min_values[i], max_values[i], literal,
field_type,
+ physical_type)) {
matching_pages.push_back(i);
}
}
@@ -229,7 +248,7 @@ std::vector<int32_t> ColumnIndexFilter::FilterPagesByEqual(
std::vector<int32_t> ColumnIndexFilter::FilterPagesByNotEqual(
const std::shared_ptr<::parquet::ColumnIndex>& column_index, const
Literal& literal,
- FieldType field_type) {
+ FieldType field_type, ::parquet::Type::type physical_type) {
std::vector<int32_t> matching_pages;
if (literal.IsNull()) {
@@ -251,8 +270,8 @@ std::vector<int32_t>
ColumnIndexFilter::FilterPagesByNotEqual(
// Try to exclude pages where min == max == literal (all non-null
values equal literal).
// NULL != literal is NULL (UNKNOWN) in SQL, so nulls don't produce
true either.
- auto cmp_min = CompareEncodedWithLiteral(min_values[i], literal,
field_type);
- auto cmp_max = CompareEncodedWithLiteral(max_values[i], literal,
field_type);
+ auto cmp_min = CompareEncodedWithLiteral(min_values[i], literal,
field_type, physical_type);
+ auto cmp_max = CompareEncodedWithLiteral(max_values[i], literal,
field_type, physical_type);
if (cmp_min.has_value() && cmp_max.has_value() && *cmp_min == 0 &&
*cmp_max == 0) {
// min == max == literal: all non-null values equal literal, and
nulls
// don't satisfy != either. Skip this page entirely.
@@ -267,7 +286,7 @@ std::vector<int32_t>
ColumnIndexFilter::FilterPagesByNotEqual(
std::vector<int32_t> ColumnIndexFilter::FilterPagesByLessThan(
const std::shared_ptr<::parquet::ColumnIndex>& column_index, const
Literal& literal,
- FieldType field_type) {
+ FieldType field_type, ::parquet::Type::type physical_type) {
std::vector<int32_t> matching_pages;
const auto& null_pages = column_index->null_pages();
const auto& min_values = column_index->encoded_min_values();
@@ -278,7 +297,7 @@ std::vector<int32_t>
ColumnIndexFilter::FilterPagesByLessThan(
continue;
}
- if (PageMightContainLessThan(min_values[i], literal, field_type)) {
+ if (PageMightContainLessThan(min_values[i], literal, field_type,
physical_type)) {
matching_pages.push_back(i);
}
}
@@ -288,7 +307,7 @@ std::vector<int32_t>
ColumnIndexFilter::FilterPagesByLessThan(
std::vector<int32_t> ColumnIndexFilter::FilterPagesByLessOrEqual(
const std::shared_ptr<::parquet::ColumnIndex>& column_index, const
Literal& literal,
- FieldType field_type) {
+ FieldType field_type, ::parquet::Type::type physical_type) {
std::vector<int32_t> matching_pages;
const auto& null_pages = column_index->null_pages();
const auto& min_values = column_index->encoded_min_values();
@@ -299,7 +318,7 @@ std::vector<int32_t>
ColumnIndexFilter::FilterPagesByLessOrEqual(
continue;
}
- if (PageMightContainLessOrEqual(min_values[i], literal, field_type)) {
+ if (PageMightContainLessOrEqual(min_values[i], literal, field_type,
physical_type)) {
matching_pages.push_back(i);
}
}
@@ -309,7 +328,7 @@ std::vector<int32_t>
ColumnIndexFilter::FilterPagesByLessOrEqual(
std::vector<int32_t> ColumnIndexFilter::FilterPagesByGreaterThan(
const std::shared_ptr<::parquet::ColumnIndex>& column_index, const
Literal& literal,
- FieldType field_type) {
+ FieldType field_type, ::parquet::Type::type physical_type) {
std::vector<int32_t> matching_pages;
const auto& null_pages = column_index->null_pages();
const auto& max_values = column_index->encoded_max_values();
@@ -320,7 +339,7 @@ std::vector<int32_t>
ColumnIndexFilter::FilterPagesByGreaterThan(
continue;
}
- if (PageMightContainGreaterThan(max_values[i], literal, field_type)) {
+ if (PageMightContainGreaterThan(max_values[i], literal, field_type,
physical_type)) {
matching_pages.push_back(i);
}
}
@@ -330,7 +349,7 @@ std::vector<int32_t>
ColumnIndexFilter::FilterPagesByGreaterThan(
std::vector<int32_t> ColumnIndexFilter::FilterPagesByGreaterOrEqual(
const std::shared_ptr<::parquet::ColumnIndex>& column_index, const
Literal& literal,
- FieldType field_type) {
+ FieldType field_type, ::parquet::Type::type physical_type) {
std::vector<int32_t> matching_pages;
const auto& null_pages = column_index->null_pages();
const auto& max_values = column_index->encoded_max_values();
@@ -341,7 +360,7 @@ std::vector<int32_t>
ColumnIndexFilter::FilterPagesByGreaterOrEqual(
continue;
}
- if (PageMightContainGreaterOrEqual(max_values[i], literal,
field_type)) {
+ if (PageMightContainGreaterOrEqual(max_values[i], literal, field_type,
physical_type)) {
matching_pages.push_back(i);
}
}
@@ -390,7 +409,8 @@ std::vector<int32_t>
ColumnIndexFilter::FilterPagesByIsNotNull(
std::vector<int32_t> ColumnIndexFilter::FilterPagesByIn(
const std::shared_ptr<::parquet::ColumnIndex>& column_index,
- const std::vector<Literal>& literals, FieldType field_type) {
+ const std::vector<Literal>& literals, FieldType field_type,
+ ::parquet::Type::type physical_type) {
std::vector<int32_t> matching_pages;
const auto& null_pages = column_index->null_pages();
const auto& min_values = column_index->encoded_min_values();
@@ -426,7 +446,8 @@ std::vector<int32_t> ColumnIndexFilter::FilterPagesByIn(
if (literal.IsNull()) {
continue;
}
- if (PageMightContainEqual(min_values[i], max_values[i], literal,
field_type)) {
+ if (PageMightContainEqual(min_values[i], max_values[i], literal,
field_type,
+ physical_type)) {
matching_pages.push_back(i);
break; // Page matched, no need to check more literals
}
@@ -501,9 +522,9 @@ RowRanges ColumnIndexFilter::BuildRowRangesFromPageIndices(
return ranges;
}
-std::optional<int32_t> ColumnIndexFilter::CompareEncodedWithLiteral(const
std::string& encoded,
- const
Literal& literal,
- FieldType
field_type) {
+std::optional<int32_t> ColumnIndexFilter::CompareEncodedWithLiteral(
+ const std::string& encoded, const Literal& literal, FieldType field_type,
+ ::parquet::Type::type physical_type) {
if (literal.IsNull()) {
return std::nullopt;
}
@@ -570,33 +591,54 @@ std::optional<int32_t>
ColumnIndexFilter::CompareEncodedWithLiteral(const std::s
return (cmp < 0) ? -1 : (cmp > 0) ? 1 : 0;
}
case FieldType::DECIMAL: {
- // Parquet stores DECIMAL as INT32, INT64, or FIXED_LEN_BYTE_ARRAY
depending
- // on precision. All are stored as unscaled integer values.
+ // Parquet stores DECIMAL as INT32, INT64, FIXED_LEN_BYTE_ARRAY or
BYTE_ARRAY.
+ // All carry the unscaled integer value, but INT32/INT64 are
little-endian
+ // plain values while the byte arrays are big-endian two's
complement, so the
+ // physical type from the file schema must decide. The encoded
length cannot:
+ // FIXED_LEN_BYTE_ARRAY is legal for any precision with length
+ // ceil((precision * log2(10) + 1) / 8), which is 4 bytes for
precision 7-9
+ // and 8 bytes for precision 17-18.
auto lit_decimal = literal.GetValue<Decimal>();
Decimal::int128_t lit_val = lit_decimal.Value();
Decimal::int128_t enc_val;
- if (encoded.size() == sizeof(int32_t)) {
- // INT32 physical type (precision <= 9)
- int32_t raw;
- std::memcpy(&raw, encoded.data(), sizeof(int32_t));
- enc_val = static_cast<Decimal::int128_t>(raw);
- } else if (encoded.size() == sizeof(int64_t)) {
- // INT64 physical type (precision <= 18)
- int64_t raw;
- std::memcpy(&raw, encoded.data(), sizeof(int64_t));
- enc_val = static_cast<Decimal::int128_t>(raw);
- } else {
- // FIXED_LEN_BYTE_ARRAY / BYTE_ARRAY: big-endian two's
complement.
- // Defer to Decimal::FromUnscaledBytes so endianness, padding,
and
- // sign extension stay consistent with parquet_stats_extractor.
- if (encoded.empty()) {
- return std::nullopt;
+ switch (physical_type) {
+ case ::parquet::Type::INT32: {
+ if (encoded.size() < sizeof(int32_t)) {
+ return std::nullopt;
+ }
+ int32_t raw;
+ std::memcpy(&raw, encoded.data(), sizeof(int32_t));
+ enc_val = static_cast<Decimal::int128_t>(raw);
+ break;
}
- Bytes bytes(encoded, GetDefaultPool().get());
- enc_val =
- Decimal::FromUnscaledBytes(lit_decimal.Precision(),
lit_decimal.Scale(), &bytes)
- .Value();
+ case ::parquet::Type::INT64: {
+ if (encoded.size() < sizeof(int64_t)) {
+ return std::nullopt;
+ }
+ int64_t raw;
+ std::memcpy(&raw, encoded.data(), sizeof(int64_t));
+ enc_val = static_cast<Decimal::int128_t>(raw);
+ break;
+ }
+ case ::parquet::Type::FIXED_LEN_BYTE_ARRAY:
+ case ::parquet::Type::BYTE_ARRAY: {
+ // Big-endian two's complement. Defer to
Decimal::FromUnscaledBytes so
+ // endianness, padding, and sign extension stay consistent
with
+ // parquet_stats_extractor.
+ if (encoded.empty()) {
+ return std::nullopt;
+ }
+ Bytes bytes(encoded, GetDefaultPool().get());
+ enc_val =
Decimal::FromUnscaledBytes(lit_decimal.Precision(),
+ lit_decimal.Scale(),
&bytes)
+ .Value();
+ break;
+ }
+ default:
+ // Physical type unknown (no schema available) or not a
valid decimal
+ // encoding: fall back to safe behavior (include page).
+ return std::nullopt;
}
return (enc_val < lit_val) ? -1 : (enc_val > lit_val) ? 1 : 0;
@@ -611,13 +653,14 @@ std::optional<int32_t>
ColumnIndexFilter::CompareEncodedWithLiteral(const std::s
bool ColumnIndexFilter::PageMightContainEqual(const std::string& encoded_min,
const std::string& encoded_max,
- const Literal& literal,
FieldType field_type) {
+ const Literal& literal,
FieldType field_type,
+ ::parquet::Type::type
physical_type) {
if (literal.IsNull()) {
return false; // Null is handled separately via null_pages
}
// Page might contain equal if min <= literal <= max
- auto cmp_min = CompareEncodedWithLiteral(encoded_min, literal, field_type);
+ auto cmp_min = CompareEncodedWithLiteral(encoded_min, literal, field_type,
physical_type);
if (!cmp_min.has_value()) {
return true; // Can't compare, assume match
}
@@ -625,7 +668,7 @@ bool ColumnIndexFilter::PageMightContainEqual(const
std::string& encoded_min,
return false; // min > literal
}
- auto cmp_max = CompareEncodedWithLiteral(encoded_max, literal, field_type);
+ auto cmp_max = CompareEncodedWithLiteral(encoded_max, literal, field_type,
physical_type);
if (!cmp_max.has_value()) {
return true;
}
@@ -637,13 +680,14 @@ bool ColumnIndexFilter::PageMightContainEqual(const
std::string& encoded_min,
}
bool ColumnIndexFilter::PageMightContainLessThan(const std::string&
encoded_min,
- const Literal& literal,
FieldType field_type) {
+ const Literal& literal,
FieldType field_type,
+ ::parquet::Type::type
physical_type) {
if (literal.IsNull()) {
return false;
}
// Page might contain values < literal if min < literal
- auto cmp_min = CompareEncodedWithLiteral(encoded_min, literal, field_type);
+ auto cmp_min = CompareEncodedWithLiteral(encoded_min, literal, field_type,
physical_type);
if (!cmp_min.has_value()) {
return true;
}
@@ -651,13 +695,14 @@ bool ColumnIndexFilter::PageMightContainLessThan(const
std::string& encoded_min,
}
bool ColumnIndexFilter::PageMightContainLessOrEqual(const std::string&
encoded_min,
- const Literal& literal,
FieldType field_type) {
+ const Literal& literal,
FieldType field_type,
+ ::parquet::Type::type
physical_type) {
if (literal.IsNull()) {
return false;
}
// Page might contain values <= literal if min <= literal
- auto cmp_min = CompareEncodedWithLiteral(encoded_min, literal, field_type);
+ auto cmp_min = CompareEncodedWithLiteral(encoded_min, literal, field_type,
physical_type);
if (!cmp_min.has_value()) {
return true;
}
@@ -665,13 +710,14 @@ bool ColumnIndexFilter::PageMightContainLessOrEqual(const
std::string& encoded_m
}
bool ColumnIndexFilter::PageMightContainGreaterThan(const std::string&
encoded_max,
- const Literal& literal,
FieldType field_type) {
+ const Literal& literal,
FieldType field_type,
+ ::parquet::Type::type
physical_type) {
if (literal.IsNull()) {
return false;
}
// Page might contain values > literal if max > literal
- auto cmp_max = CompareEncodedWithLiteral(encoded_max, literal, field_type);
+ auto cmp_max = CompareEncodedWithLiteral(encoded_max, literal, field_type,
physical_type);
if (!cmp_max.has_value()) {
return true;
}
@@ -679,14 +725,14 @@ bool ColumnIndexFilter::PageMightContainGreaterThan(const
std::string& encoded_m
}
bool ColumnIndexFilter::PageMightContainGreaterOrEqual(const std::string&
encoded_max,
- const Literal& literal,
- FieldType field_type) {
+ const Literal& literal,
FieldType field_type,
+ ::parquet::Type::type
physical_type) {
if (literal.IsNull()) {
return false;
}
// Page might contain values >= literal if max >= literal
- auto cmp_max = CompareEncodedWithLiteral(encoded_max, literal, field_type);
+ auto cmp_max = CompareEncodedWithLiteral(encoded_max, literal, field_type,
physical_type);
if (!cmp_max.has_value()) {
return true;
}
diff --git a/src/paimon/format/parquet/column_index_filter.h
b/src/paimon/format/parquet/column_index_filter.h
index a3016241..a0b7eecf 100644
--- a/src/paimon/format/parquet/column_index_filter.h
+++ b/src/paimon/format/parquet/column_index_filter.h
@@ -32,6 +32,7 @@
#include "paimon/predicate/predicate.h"
#include "paimon/result.h"
#include "parquet/page_index.h"
+#include "parquet/schema.h"
namespace paimon {
class CompoundPredicate;
@@ -59,61 +60,69 @@ class ColumnIndexFilter {
/// @param predicate The predicate to evaluate.
/// @param rg_page_index_reader The page index reader of target row group
for the file.
/// @param column_name_to_index Map from column name to column index.
+ /// @param schema The schema descriptor of the file, used to learn the
physical type
+ /// each column is encoded with. May be null; page statistics whose
decoding
+ /// depends on the physical type are then left uncompared so that
no page is
+ /// pruned on them.
/// @param row_group_row_count The number of rows in the row group.
/// @return RowRanges that may contain matching rows.
static Result<RowRanges> CalculateRowRanges(
const std::shared_ptr<Predicate>& predicate,
const std::shared_ptr<::parquet::RowGroupPageIndexReader>&
rg_page_index_reader,
- const std::map<std::string, int32_t>& column_name_to_index, int64_t
row_group_row_count);
+ const std::map<std::string, int32_t>& column_name_to_index,
+ const ::parquet::SchemaDescriptor* schema, int64_t
row_group_row_count);
private:
/// Visit a predicate and calculate row ranges.
static Result<RowRanges> VisitPredicate(
const std::shared_ptr<Predicate>& predicate,
- const std::map<std::string, int32_t>& column_name_to_index, int64_t
row_group_row_count,
+ const std::map<std::string, int32_t>& column_name_to_index,
+ const ::parquet::SchemaDescriptor* schema, int64_t row_group_row_count,
::parquet::RowGroupPageIndexReader* rg_page_index_reader);
/// Visit a leaf predicate and calculate row ranges.
static Result<RowRanges> VisitLeafPredicate(
const std::shared_ptr<LeafPredicate>& leaf_predicate,
- const std::map<std::string, int32_t>& column_name_to_index, int64_t
row_group_row_count,
+ const std::map<std::string, int32_t>& column_name_to_index,
+ const ::parquet::SchemaDescriptor* schema, int64_t row_group_row_count,
::parquet::RowGroupPageIndexReader* rg_page_index_reader);
/// Visit a compound predicate (AND/OR) and calculate row ranges.
static Result<RowRanges> VisitCompoundPredicate(
const std::shared_ptr<CompoundPredicate>& compound_predicate,
- const std::map<std::string, int32_t>& column_name_to_index, int64_t
row_group_row_count,
+ const std::map<std::string, int32_t>& column_name_to_index,
+ const ::parquet::SchemaDescriptor* schema, int64_t row_group_row_count,
::parquet::RowGroupPageIndexReader* rg_page_index_reader);
/// Filter pages based on column index statistics for EQUAL predicate.
static std::vector<int32_t> FilterPagesByEqual(
const std::shared_ptr<::parquet::ColumnIndex>& column_index, const
Literal& literal,
- FieldType field_type);
+ FieldType field_type, ::parquet::Type::type physical_type);
/// Filter pages based on column index statistics for NOT_EQUAL predicate.
static std::vector<int32_t> FilterPagesByNotEqual(
const std::shared_ptr<::parquet::ColumnIndex>& column_index, const
Literal& literal,
- FieldType field_type);
+ FieldType field_type, ::parquet::Type::type physical_type);
/// Filter pages based on column index statistics for LESS_THAN predicate.
static std::vector<int32_t> FilterPagesByLessThan(
const std::shared_ptr<::parquet::ColumnIndex>& column_index, const
Literal& literal,
- FieldType field_type);
+ FieldType field_type, ::parquet::Type::type physical_type);
/// Filter pages based on column index statistics for LESS_OR_EQUAL
predicate.
static std::vector<int32_t> FilterPagesByLessOrEqual(
const std::shared_ptr<::parquet::ColumnIndex>& column_index, const
Literal& literal,
- FieldType field_type);
+ FieldType field_type, ::parquet::Type::type physical_type);
/// Filter pages based on column index statistics for GREATER_THAN
predicate.
static std::vector<int32_t> FilterPagesByGreaterThan(
const std::shared_ptr<::parquet::ColumnIndex>& column_index, const
Literal& literal,
- FieldType field_type);
+ FieldType field_type, ::parquet::Type::type physical_type);
/// Filter pages based on column index statistics for GREATER_OR_EQUAL
predicate.
static std::vector<int32_t> FilterPagesByGreaterOrEqual(
const std::shared_ptr<::parquet::ColumnIndex>& column_index, const
Literal& literal,
- FieldType field_type);
+ FieldType field_type, ::parquet::Type::type physical_type);
/// Filter pages based on column index statistics for IS_NULL predicate.
static std::vector<int32_t> FilterPagesByIsNull(
@@ -126,7 +135,8 @@ class ColumnIndexFilter {
/// Filter pages based on column index statistics for IN predicate.
static std::vector<int32_t> FilterPagesByIn(
const std::shared_ptr<::parquet::ColumnIndex>& column_index,
- const std::vector<Literal>& literals, FieldType field_type);
+ const std::vector<Literal>& literals, FieldType field_type,
+ ::parquet::Type::type physical_type);
/// Filter pages based on column index statistics for NOT_IN predicate.
static std::vector<int32_t> FilterPagesByNotIn(
@@ -139,37 +149,44 @@ class ColumnIndexFilter {
const std::shared_ptr<::parquet::OffsetIndex>& offset_index, int64_t
row_group_row_count);
/// Compare a parquet encoded value with a Literal.
+ /// @param physical_type The parquet physical type the column is encoded
with, which
+ /// decides how the encoded statistics are decoded for types that
Parquet can
+ /// store in more than one physical representation.
/// @return -1 if encoded < literal, 0 if equal, 1 if encoded > literal.
/// nullopt if comparison cannot be performed (unsupported type,
etc.).
static std::optional<int32_t> CompareEncodedWithLiteral(const std::string&
encoded,
const Literal&
literal,
- FieldType
field_type);
+ FieldType
field_type,
+
::parquet::Type::type physical_type);
/// Check if a page might contain a value equal to the literal.
/// Condition: min <= literal <= max
static bool PageMightContainEqual(const std::string& encoded_min,
const std::string& encoded_max, const
Literal& literal,
- FieldType field_type);
+ FieldType field_type,
::parquet::Type::type physical_type);
/// Check if a page might contain values less than the literal.
/// Condition: min < literal
static bool PageMightContainLessThan(const std::string& encoded_min, const
Literal& literal,
- FieldType field_type);
+ FieldType field_type,
::parquet::Type::type physical_type);
/// Check if a page might contain values less than or equal to the literal.
/// Condition: min <= literal
static bool PageMightContainLessOrEqual(const std::string& encoded_min,
const Literal& literal,
- FieldType field_type);
+ FieldType field_type,
+ ::parquet::Type::type
physical_type);
/// Check if a page might contain values greater than the literal.
/// Condition: max > literal
static bool PageMightContainGreaterThan(const std::string& encoded_max,
const Literal& literal,
- FieldType field_type);
+ FieldType field_type,
+ ::parquet::Type::type
physical_type);
/// Check if a page might contain values greater than or equal to the
literal.
/// Condition: max >= literal
static bool PageMightContainGreaterOrEqual(const std::string& encoded_max,
- const Literal& literal,
FieldType field_type);
+ const Literal& literal,
FieldType field_type,
+ ::parquet::Type::type
physical_type);
};
} // namespace paimon::parquet
diff --git a/src/paimon/format/parquet/column_index_filter_test.cpp
b/src/paimon/format/parquet/column_index_filter_test.cpp
index 94c65ef5..f1b916bb 100644
--- a/src/paimon/format/parquet/column_index_filter_test.cpp
+++ b/src/paimon/format/parquet/column_index_filter_test.cpp
@@ -36,6 +36,7 @@
#include "paimon/common/predicate/leaf_predicate_impl.h"
#include "paimon/common/utils/arrow/arrow_input_stream_adapter.h"
#include "paimon/common/utils/arrow/mem_utils.h"
+#include "paimon/data/decimal.h"
#include "paimon/defs.h"
#include "paimon/format/parquet/parquet_format_defs.h"
#include "paimon/format/parquet/parquet_format_writer.h"
@@ -305,8 +306,9 @@ class ColumnIndexFilterTest : public ::testing::Test {
}
Result<RowRanges> Filter(const std::shared_ptr<Predicate>& predicate) {
- return ColumnIndexFilter::CalculateRowRanges(predicate,
page_index_reader_->RowGroup(0),
- column_name_to_index_,
row_group_row_count_);
+ return ColumnIndexFilter::CalculateRowRanges(
+ predicate, page_index_reader_->RowGroup(0), column_name_to_index_,
+ parquet_reader_->metadata()->schema(), row_group_row_count_);
}
std::shared_ptr<arrow::MemoryPool> arrow_pool_;
@@ -552,19 +554,19 @@ TEST_F(ColumnIndexFilterTest,
SignedZeroUsesJavaOrderForFloatingPointPages) {
auto less_negative_zero = PredicateBuilder::LessThan(
/*field_index=*/0, /*field_name=*/"value", field_type,
field_type == FieldType::FLOAT ? Literal(-0.0f) : Literal(-0.0));
- ASSERT_OK_AND_ASSIGN(auto ranges,
- ColumnIndexFilter::CalculateRowRanges(
- less_negative_zero,
page_index_reader->RowGroup(0), {{"value", 0}},
- reader->metadata()->RowGroup(0)->num_rows()));
+ ASSERT_OK_AND_ASSIGN(auto ranges,
ColumnIndexFilter::CalculateRowRanges(
+ less_negative_zero,
page_index_reader->RowGroup(0),
+ {{"value", 0}},
reader->metadata()->schema(),
+
reader->metadata()->RowGroup(0)->num_rows()));
ASSERT_TRUE(ranges.IsEmpty()) << "field type: " <<
static_cast<int32_t>(field_type);
auto less_positive_zero = PredicateBuilder::LessThan(
/*field_index=*/0, /*field_name=*/"value", field_type,
field_type == FieldType::FLOAT ? Literal(0.0f) : Literal(0.0));
- ASSERT_OK_AND_ASSIGN(ranges,
- ColumnIndexFilter::CalculateRowRanges(
- less_positive_zero,
page_index_reader->RowGroup(0), {{"value", 0}},
- reader->metadata()->RowGroup(0)->num_rows()));
+ ASSERT_OK_AND_ASSIGN(
+ ranges, ColumnIndexFilter::CalculateRowRanges(
+ less_positive_zero, page_index_reader->RowGroup(0),
{{"value", 0}},
+ reader->metadata()->schema(),
reader->metadata()->RowGroup(0)->num_rows()));
ASSERT_EQ(20, ranges.RowCount());
ASSERT_EQ(1, ranges.GetRanges().size());
ASSERT_EQ(0, ranges.GetRanges()[0].from);
@@ -576,7 +578,7 @@ TEST_F(ColumnIndexFilterTest,
SignedZeroUsesJavaOrderForFloatingPointPages) {
ASSERT_OK_AND_ASSIGN(
ranges, ColumnIndexFilter::CalculateRowRanges(
greater_negative_zero, page_index_reader->RowGroup(0),
{{"value", 0}},
- reader->metadata()->RowGroup(0)->num_rows()));
+ reader->metadata()->schema(),
reader->metadata()->RowGroup(0)->num_rows()));
ASSERT_EQ(30, ranges.RowCount());
auto not_equal_negative_zero = PredicateBuilder::NotEqual(
@@ -585,16 +587,16 @@ TEST_F(ColumnIndexFilterTest,
SignedZeroUsesJavaOrderForFloatingPointPages) {
ASSERT_OK_AND_ASSIGN(
ranges, ColumnIndexFilter::CalculateRowRanges(
not_equal_negative_zero,
page_index_reader->RowGroup(0), {{"value", 0}},
- reader->metadata()->RowGroup(0)->num_rows()));
+ reader->metadata()->schema(),
reader->metadata()->RowGroup(0)->num_rows()));
ASSERT_EQ(30, ranges.RowCount());
auto greater_finite = PredicateBuilder::GreaterThan(
/*field_index=*/0, /*field_name=*/"value", field_type,
field_type == FieldType::FLOAT ? Literal(2.0f) : Literal(2.0));
- ASSERT_OK_AND_ASSIGN(ranges,
- ColumnIndexFilter::CalculateRowRanges(
- greater_finite,
page_index_reader->RowGroup(0), {{"value", 0}},
- reader->metadata()->RowGroup(0)->num_rows()));
+ ASSERT_OK_AND_ASSIGN(
+ ranges, ColumnIndexFilter::CalculateRowRanges(
+ greater_finite, page_index_reader->RowGroup(0),
{{"value", 0}},
+ reader->metadata()->schema(),
reader->metadata()->RowGroup(0)->num_rows()));
ASSERT_TRUE(ranges.IsEmpty());
auto greater_between_pages = PredicateBuilder::GreaterThan(
@@ -603,7 +605,7 @@ TEST_F(ColumnIndexFilterTest,
SignedZeroUsesJavaOrderForFloatingPointPages) {
ASSERT_OK_AND_ASSIGN(
ranges, ColumnIndexFilter::CalculateRowRanges(
greater_between_pages, page_index_reader->RowGroup(0),
{{"value", 0}},
- reader->metadata()->RowGroup(0)->num_rows()));
+ reader->metadata()->schema(),
reader->metadata()->RowGroup(0)->num_rows()));
ASSERT_EQ(10, ranges.RowCount());
ASSERT_EQ(1, ranges.GetRanges().size());
ASSERT_EQ(20, ranges.GetRanges()[0].from);
@@ -612,14 +614,135 @@ TEST_F(ColumnIndexFilterTest,
SignedZeroUsesJavaOrderForFloatingPointPages) {
auto equal_finite = PredicateBuilder::Equal(
/*field_index=*/0, /*field_name=*/"value", field_type,
field_type == FieldType::FLOAT ? Literal(2.0f) : Literal(2.0));
- ASSERT_OK_AND_ASSIGN(ranges,
- ColumnIndexFilter::CalculateRowRanges(
- equal_finite, page_index_reader->RowGroup(0),
{{"value", 0}},
- reader->metadata()->RowGroup(0)->num_rows()));
+ ASSERT_OK_AND_ASSIGN(
+ ranges, ColumnIndexFilter::CalculateRowRanges(
+ equal_finite, page_index_reader->RowGroup(0),
{{"value", 0}},
+ reader->metadata()->schema(),
reader->metadata()->RowGroup(0)->num_rows()));
ASSERT_TRUE(ranges.IsEmpty());
}
}
+/// Arrow writes DECIMAL as FIXED_LEN_BYTE_ARRAY unless
store_decimal_as_integer is set,
+/// and those page bounds are big-endian two's complement. Precision 7-9
encodes into
+/// exactly 4 bytes and precision 17-18 into exactly 8 bytes, so the encoded
length alone
+/// cannot tell them apart from INT32/INT64 decimals; decoding them
little-endian turns
+/// min 1.00 into 1,677,721,600 and prunes away pages that do match.
+TEST_F(ColumnIndexFilterTest, DecimalFixedLenByteArrayPages) {
+ // Page 0: 1.00, page 1: 10.00, page 2: 100.00 (10 rows each).
+ constexpr int32_t kScale = 2;
+ const std::vector<int64_t> page_unscaled_values = {100, 1000, 10000};
+
+ for (int32_t precision : {9, 18}) {
+ auto decimal_type = arrow::decimal128(precision, kScale);
+ arrow::Decimal128Builder builder(decimal_type);
+ for (int64_t unscaled : page_unscaled_values) {
+ for (int32_t i = 0; i < 10; ++i) {
+ ASSERT_TRUE(builder.Append(arrow::Decimal128(unscaled)).ok());
+ }
+ }
+ std::shared_ptr<arrow::Array> values = builder.Finish().ValueOrDie();
+ auto field = arrow::field("value", decimal_type);
+ auto data = arrow::StructArray::Make({values}, {field}).ValueOrDie();
+
+ std::string file_name =
+ dir_->Str() + "/decimal_flba_p" + std::to_string(precision) +
".parquet";
+ WriteTestFile(file_name, data, /*write_batch_size=*/10,
/*max_row_group_length=*/30);
+
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<InputStream> in,
fs_->Open(file_name));
+ ASSERT_OK_AND_ASSIGN(int64_t length, in->Length());
+ auto in_stream = std::make_shared<ArrowInputStreamAdapter>(in, length,
arrow_pool_);
+ auto reader = ::parquet::ParquetFileReader::Open(in_stream);
+ ASSERT_TRUE(reader);
+ const auto* schema = reader->metadata()->schema();
+ // The fix is only exercised if the writer really chose the byte-array
encoding
+ // with a length that collides with INT32 (precision 9) or INT64
(precision 18).
+ ASSERT_EQ(::parquet::Type::FIXED_LEN_BYTE_ARRAY,
schema->Column(0)->physical_type());
+ ASSERT_EQ(precision == 9 ? 4 : 8, schema->Column(0)->type_length());
+
+ auto page_index_reader = reader->GetPageIndexReader();
+ ASSERT_TRUE(page_index_reader);
+ auto row_group_page_index = page_index_reader->RowGroup(0);
+ ASSERT_TRUE(row_group_page_index);
+ auto column_index = row_group_page_index->GetColumnIndex(0);
+ ASSERT_TRUE(column_index);
+ ASSERT_EQ(3, column_index->encoded_min_values().size());
+
+ int64_t row_count = reader->metadata()->RowGroup(0)->num_rows();
+ auto filter = [&](const std::shared_ptr<Predicate>& predicate) {
+ return ColumnIndexFilter::CalculateRowRanges(predicate,
page_index_reader->RowGroup(0),
+ {{"value", 0}},
schema, row_count);
+ };
+
+ // value < 5.00 → only page 0 (rows [0, 9]).
+ auto less_than = PredicateBuilder::LessThan(
+ /*field_index=*/0, /*field_name=*/"value", FieldType::DECIMAL,
+ Literal(Decimal(precision, kScale, 500)));
+ ASSERT_OK_AND_ASSIGN(auto ranges, filter(less_than));
+ ASSERT_EQ(1, ranges.GetRanges().size()) << "precision: " << precision;
+ EXPECT_EQ(0, ranges.GetRanges()[0].from);
+ EXPECT_EQ(9, ranges.GetRanges()[0].to);
+
+ // value > 50.00 → only page 2 (rows [20, 29]).
+ auto greater_than = PredicateBuilder::GreaterThan(
+ /*field_index=*/0, /*field_name=*/"value", FieldType::DECIMAL,
+ Literal(Decimal(precision, kScale, 5000)));
+ ASSERT_OK_AND_ASSIGN(ranges, filter(greater_than));
+ ASSERT_EQ(1, ranges.GetRanges().size()) << "precision: " << precision;
+ EXPECT_EQ(20, ranges.GetRanges()[0].from);
+ EXPECT_EQ(29, ranges.GetRanges()[0].to);
+
+ // value = 10.00 → only page 1 (rows [10, 19]).
+ auto equal = PredicateBuilder::Equal(
+ /*field_index=*/0, /*field_name=*/"value", FieldType::DECIMAL,
+ Literal(Decimal(precision, kScale, 1000)));
+ ASSERT_OK_AND_ASSIGN(ranges, filter(equal));
+ ASSERT_EQ(1, ranges.GetRanges().size()) << "precision: " << precision;
+ EXPECT_EQ(10, ranges.GetRanges()[0].from);
+ EXPECT_EQ(19, ranges.GetRanges()[0].to);
+
+ // value > 1000.00 → no page matches.
+ auto greater_than_all = PredicateBuilder::GreaterThan(
+ /*field_index=*/0, /*field_name=*/"value", FieldType::DECIMAL,
+ Literal(Decimal(precision, kScale, 100000)));
+ ASSERT_OK_AND_ASSIGN(ranges, filter(greater_than_all));
+ EXPECT_TRUE(ranges.IsEmpty()) << "precision: " << precision;
+ }
+}
+
+/// Without a schema descriptor the physical type of a DECIMAL column is
unknown, so no
+/// page may be pruned on its statistics.
+TEST_F(ColumnIndexFilterTest, DecimalWithoutSchemaKeepsAllPages) {
+ auto decimal_type = arrow::decimal128(9, 2);
+ arrow::Decimal128Builder builder(decimal_type);
+ for (int64_t unscaled : {100, 1000, 10000}) {
+ for (int32_t i = 0; i < 10; ++i) {
+ ASSERT_TRUE(builder.Append(arrow::Decimal128(unscaled)).ok());
+ }
+ }
+ std::shared_ptr<arrow::Array> values = builder.Finish().ValueOrDie();
+ auto field = arrow::field("value", decimal_type);
+ auto data = arrow::StructArray::Make({values}, {field}).ValueOrDie();
+
+ std::string file_name = dir_->Str() + "/decimal_no_schema.parquet";
+ WriteTestFile(file_name, data, /*write_batch_size=*/10,
/*max_row_group_length=*/30);
+
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<InputStream> in,
fs_->Open(file_name));
+ ASSERT_OK_AND_ASSIGN(int64_t length, in->Length());
+ auto in_stream = std::make_shared<ArrowInputStreamAdapter>(in, length,
arrow_pool_);
+ auto reader = ::parquet::ParquetFileReader::Open(in_stream);
+ ASSERT_TRUE(reader);
+ auto page_index_reader = reader->GetPageIndexReader();
+ ASSERT_TRUE(page_index_reader);
+
+ int64_t row_count = reader->metadata()->RowGroup(0)->num_rows();
+ auto less_than = PredicateBuilder::LessThan(
+ /*field_index=*/0, /*field_name=*/"value", FieldType::DECIMAL,
Literal(Decimal(9, 2, 500)));
+ ASSERT_OK_AND_ASSIGN(auto ranges, ColumnIndexFilter::CalculateRowRanges(
+ less_than,
page_index_reader->RowGroup(0), {{"value", 0}},
+ /*schema=*/nullptr, row_count));
+ EXPECT_EQ(row_count, ranges.RowCount());
+}
+
/// Predicates referencing fields absent from the data file are stripped
upstream
/// by FieldMappingBuilder, so reaching ColumnIndexFilter with such a
predicate is
/// a contract violation and surfaces as an error.
diff --git a/src/paimon/format/parquet/file_reader_wrapper.cpp
b/src/paimon/format/parquet/file_reader_wrapper.cpp
index f69b89d4..19c35310 100644
--- a/src/paimon/format/parquet/file_reader_wrapper.cpp
+++ b/src/paimon/format/parquet/file_reader_wrapper.cpp
@@ -631,9 +631,9 @@ Result<RowRanges>
FileReaderWrapper::CalculateFilteredRowRanges(
{/*column_index=*/true,
/*offset_index=*/true});
// Keep this restricted reader separate: projected payload columns
still need
// their offset indexes when the data reader is initialized later.
- return ColumnIndexFilter::CalculateRowRanges(predicate,
-
page_index_reader->RowGroup(row_group_index),
- column_name_to_index,
row_count);
+ return ColumnIndexFilter::CalculateRowRanges(
+ predicate, page_index_reader->RowGroup(row_group_index),
column_name_to_index,
+ meta_data->schema(), row_count);
}
PAIMON_PARQUET_CATCH_AND_RETURN_STATUS("FileReaderWrapper::CalculateFilteredRowRanges")
}