diff --git a/cpp/src/io/parquet/experimental/page_index_filter.cu b/cpp/src/io/parquet/experimental/page_index_filter.cu index 1c63b65e24c2..51bb397a6bab 100644 --- a/cpp/src/io/parquet/experimental/page_index_filter.cu +++ b/cpp/src/io/parquet/experimental/page_index_filter.cu @@ -38,6 +38,7 @@ #include #include +#include namespace cudf::io::parquet::experimental::detail { @@ -911,7 +912,7 @@ std::unique_ptr aggregate_reader_metadata::build_row_mask_with_pag [&](auto col_idx) { auto const schema_idx = output_column_schemas[col_idx]; auto const& dtype = output_dtypes[col_idx]; - // Only participating columns and comparable types except fixed point are supported + // Only participating columns and comparable types are supported if (not stats_columns_mask[col_idx] or (cudf::is_compound(dtype) && dtype.id() != cudf::type_id::STRING)) { // Placeholder for unsupported types and non-participating columns diff --git a/cpp/src/io/parquet/predicate_pushdown.cpp b/cpp/src/io/parquet/predicate_pushdown.cpp index 03de07c5bf9a..fa6bf42bf8ac 100644 --- a/cpp/src/io/parquet/predicate_pushdown.cpp +++ b/cpp/src/io/parquet/predicate_pushdown.cpp @@ -146,7 +146,7 @@ std::optional>> aggregate_reader_metadata::ap for (size_t col_idx = 0; col_idx < output_dtypes.size(); col_idx++) { auto const schema_idx = output_column_schemas[col_idx]; auto const& dtype = output_dtypes[col_idx]; - // Only participating columns and comparable types except fixed point are supported + // Only participating columns and comparable types are supported if (not stats_columns_mask[col_idx] or (cudf::is_compound(dtype) && dtype.id() != cudf::type_id::STRING)) { // Placeholder for unsupported types and non-participating columns diff --git a/cpp/src/io/parquet/stats_filter_helpers.hpp b/cpp/src/io/parquet/stats_filter_helpers.hpp index ec46ed558111..89128de03acb 100644 --- a/cpp/src/io/parquet/stats_filter_helpers.hpp +++ b/cpp/src/io/parquet/stats_filter_helpers.hpp @@ -18,8 +18,9 @@ #include #include -#include -#include +#include +#include +#include namespace cudf::io::parquet::detail { @@ -38,8 +39,34 @@ constexpr size_t initial_chars_capacity = 1024; */ class stats_caster_base { protected: + static inline numeric::decimal128::rep decode_flba_decimal128(uint8_t const* stats_val) + { + auto constexpr endianness = std::endian::native; + static_assert(endianness == std::endian::little or endianness == std::endian::big, + "Encountered unsupported endianness while decoding decimal128 from FLBA"); + using RepType = numeric::decimal128::rep; + auto value = RepType{}; + std::memcpy(&value, stats_val, sizeof(RepType)); + auto value_rep = std::bit_cast>(value); + // byte-swap to native representation on little-endian platforms + if constexpr (endianness == std::endian::little) { std::ranges::reverse(value_rep); } + return std::bit_cast(value_rep); + } + + template + static inline T decode_fixed_width_value(uint8_t const* stats_val, size_t stats_size) + requires((cudf::is_integral() and !cudf::is_boolean()) or cudf::is_fixed_point() or + cudf::is_chrono()) + { + CUDF_EXPECTS(stats_size == sizeof(T), + "Parquet reader encountered a statistics vector larger than the type's size"); + auto value = T{}; + std::memcpy(&value, stats_val, std::min(stats_size, sizeof(T))); + return value; + } + template - static inline ToType targetType(FromType const value) + static inline ToType target_type(FromType const value) { if constexpr (cudf::is_timestamp()) { return static_cast( @@ -52,43 +79,53 @@ class stats_caster_base { } // uses storage type as T - template () or cudf::is_nested())> + template static inline T convert(uint8_t const* stats_val, size_t stats_size, Type const type) + requires(cudf::is_dictionary() or cudf::is_nested()) { CUDF_FAIL("unsupported type for stats casting"); } - template ())> + template static inline T convert(uint8_t const* stats_val, size_t stats_size, Type const type) + requires(cudf::is_boolean()) { CUDF_EXPECTS(type == Type::BOOLEAN, "Invalid type and stats combination"); - return stats_caster_base::targetType(*reinterpret_cast(stats_val)); + return stats_caster_base::target_type(*reinterpret_cast(stats_val)); } // integral but not boolean, and fixed_point, and chrono. - template () and !cudf::is_boolean()) or - cudf::is_fixed_point() or cudf::is_chrono())> + template static inline T convert(uint8_t const* stats_val, size_t stats_size, Type const type) + requires((cudf::is_integral() and !cudf::is_boolean()) or cudf::is_fixed_point() or + cudf::is_chrono()) { switch (type) { case Type::INT32: - return stats_caster_base::targetType(*reinterpret_cast(stats_val)); + return stats_caster_base::target_type( + decode_fixed_width_value(stats_val, stats_size)); case Type::INT64: - return stats_caster_base::targetType(*reinterpret_cast(stats_val)); + return stats_caster_base::target_type( + decode_fixed_width_value(stats_val, stats_size)); case Type::INT96: // Deprecated in parquet specification - return stats_caster_base::targetType( - static_cast<__int128_t>(reinterpret_cast(stats_val)[0]) << 32 | - reinterpret_cast(stats_val)[2]); + return stats_caster_base::target_type( + static_cast<__int128_t>(decode_fixed_width_value(stats_val, stats_size)) << 32 | + decode_fixed_width_value(stats_val + sizeof(int64_t), stats_size)); case Type::BYTE_ARRAY: [[fallthrough]]; case Type::FIXED_LEN_BYTE_ARRAY: if (stats_size == sizeof(T)) { - // if type size == length of stats_val. then typecast and return. if constexpr (cudf::is_chrono()) { - return stats_caster_base::targetType( - *reinterpret_cast(stats_val)); + return stats_caster_base::target_type( + decode_fixed_width_value(stats_val, stats_size)); + } else if constexpr (std::is_same_v) { + // Decimals with physical type FLBA/BYTE_ARRAY are stored as two's complement using + // big-endian. + return stats_caster_base::target_type(decode_flba_decimal128(stats_val)); } else { - return stats_caster_base::targetType(*reinterpret_cast(stats_val)); + // TODO(mh): We may need to add support for `decimal256` (two's complement using + // big-endian) and `UUID` types (big-endian) + return stats_caster_base::target_type( + decode_fixed_width_value(stats_val, stats_size)); } } // unsupported type @@ -96,20 +133,22 @@ class stats_caster_base { } } - template ())> + template static inline T convert(uint8_t const* stats_val, size_t stats_size, Type const type) + requires(cudf::is_floating_point()) { switch (type) { case Type::FLOAT: - return stats_caster_base::targetType(*reinterpret_cast(stats_val)); + return stats_caster_base::target_type(*reinterpret_cast(stats_val)); case Type::DOUBLE: - return stats_caster_base::targetType(*reinterpret_cast(stats_val)); + return stats_caster_base::target_type(*reinterpret_cast(stats_val)); default: CUDF_FAIL("Invalid type and stats combination"); } } - template )> + template static inline T convert(uint8_t const* stats_val, size_t stats_size, Type const type) + requires(std::is_same_v) { switch (type) { case Type::BYTE_ARRAY: [[fallthrough]]; diff --git a/cpp/tests/io/experimental/hybrid_scan_filters_test.cpp b/cpp/tests/io/experimental/hybrid_scan_filters_test.cpp index fa3d25b862e0..40adbf0e35ff 100644 --- a/cpp/tests/io/experimental/hybrid_scan_filters_test.cpp +++ b/cpp/tests/io/experimental/hybrid_scan_filters_test.cpp @@ -1193,9 +1193,9 @@ TEST_F(HybridScanFiltersTest, FilterRowGroupsWithDictionary) template struct RowGroupFilteringWithDictTest : public HybridScanFiltersTest {}; -// Booleans are not supported for dictionary based filtering +// Booleans and fixed-point types are not supported for dictionary based filtering using DictionaryTestTypes = - cudf::test::RemoveIf>, SupportedTestTypes>; + cudf::test::RemoveIf>, SupportedTestTypesJIT>; TYPED_TEST_SUITE(RowGroupFilteringWithDictTest, DictionaryTestTypes); diff --git a/cpp/tests/io/parquet_common.hpp b/cpp/tests/io/parquet_common.hpp index ff698b9be70d..5f1a85aea697 100644 --- a/cpp/tests/io/parquet_common.hpp +++ b/cpp/tests/io/parquet_common.hpp @@ -45,9 +45,14 @@ using ByteLikeTypes = cudf::test::Types; -// Also fixed point types unsupported, because AST does not support them yet. -using SupportedTestTypes = cudf::test::RemoveIf, - cudf::test::ComparableTypes>; + +// Support types for AST expression evaluator +using SupportedTestTypesAST = + cudf::test::RemoveIf, ComparableAndFixedTypes>; + +// JIT does not yet support fixed point types +using SupportedTestTypesJIT = + cudf::test::RemoveIf, SupportedTestTypesAST>; // removing duration_D, duration_s, and timestamp_s as they don't appear to be supported properly. // see definition of UnsupportedChronoTypes above. diff --git a/cpp/tests/io/parquet_reader_test.cpp b/cpp/tests/io/parquet_reader_test.cpp index 3968dc656bab..f4c1dad3268e 100644 --- a/cpp/tests/io/parquet_reader_test.cpp +++ b/cpp/tests/io/parquet_reader_test.cpp @@ -3111,12 +3111,6 @@ TYPED_TEST(ParquetReaderSourceTest, BufferSourceArrayTypes) ////////////////////////////// // predicate pushdown tests -// Test for Types - numeric, chrono, string. -template -struct ParquetPredicatePushdownTest : public ParquetReaderTest {}; - -TYPED_TEST_SUITE(ParquetPredicatePushdownTest, SupportedTestTypes); - template void filter_typed_test() { @@ -3170,18 +3164,17 @@ void filter_typed_test() // Filtering AST auto literal_value = []() { if constexpr (cudf::is_timestamp()) { - // table[0] < 10000 timestamp days/seconds/milliseconds/microseconds/nanoseconds - return cudf::timestamp_scalar(T(typename T::duration(10000))); // i (0-20,000) + return cudf::timestamp_scalar(T(typename T::duration(10000))); // i ∈ [0, 20,000) } else if constexpr (cudf::is_duration()) { - // table[0] < 10000 day/seconds/milliseconds/microseconds/nanoseconds - return cudf::duration_scalar(T(10000)); // i (0-20,000) + return cudf::duration_scalar(T(10000)); // i ∈ [0, 20,000) } else if constexpr (std::is_same_v) { - // table[0] < "000010000" - return cudf::string_scalar("000010000"); // i (0-20,000) + return cudf::string_scalar("000010000"); // i ∈ [0-20,000) + } else if constexpr (cudf::is_fixed_point()) { + return cudf::fixed_point_scalar(typename T::rep{0}, + numeric::scale_type{0}); // i ∈ [-10,000, 10,000) } else { - // table[0] < 0 or 100u return cudf::numeric_scalar( - (100 - 100 * std::is_signed_v)); // i/100 (-100-100/ 0-200) + (100 - 100 * std::is_signed_v)); // i/100 ∈ [-100, 100) or [0, 200) } }(); @@ -3206,6 +3199,8 @@ void filter_typed_test() return cudf::duration_scalar(T(20000)); } else if constexpr (std::is_same_v) { return cudf::string_scalar("000020000"); + } else if constexpr (cudf::is_fixed_point()) { + return cudf::fixed_point_scalar(typename T::rep{20000}, numeric::scale_type{0}); } else { return cudf::numeric_scalar(std::numeric_limits::max()); } @@ -3353,18 +3348,17 @@ void filter_unary_operation_typed_test() // Filtering AST auto literal_value = []() { if constexpr (cudf::is_timestamp()) { - // table[0] < 10000 timestamp days/seconds/milliseconds/microseconds/nanoseconds - return cudf::timestamp_scalar(T(typename T::duration(10000))); // i (0-20,000) + return cudf::timestamp_scalar(T(typename T::duration(10000))); // i ∈ [0, 20,000) } else if constexpr (cudf::is_duration()) { - // table[0] < 10000 day/seconds/milliseconds/microseconds/nanoseconds - return cudf::duration_scalar(T(10000)); // i (0-20,000) + return cudf::duration_scalar(T(10000)); // i ∈ [0, 20,000) } else if constexpr (std::is_same_v) { - // table[0] < "000010000" - return cudf::string_scalar("000010000"); // i (0-20,000) + return cudf::string_scalar("000010000"); // i ∈ [0-20,000) + } else if constexpr (cudf::is_fixed_point()) { + return cudf::fixed_point_scalar(typename T::rep{0}, + numeric::scale_type{0}); // i ∈ [-10,000, 10,000) } else { - // table[0] < 0 or 100u return cudf::numeric_scalar( - (100 - 100 * std::is_signed_v)); // i/100 (-100-100/ 0-200) + (100 - 100 * std::is_signed_v)); // i/100 ∈ [-100, 100) or [0, 200) } }(); @@ -3402,18 +3396,205 @@ void filter_unary_operation_typed_test() } } -TYPED_TEST(ParquetPredicatePushdownTest, FilterTyped) +template +void decimal_stats_filter_test() +{ + using RepType = typename DecimalType::rep; + + auto constexpr num_input_row_groups = 3; + + auto const filepath = temp_env->get_temp_filepath("DecimalStatsFilter.parquet"); + + for (auto const scale : + {numeric::scale_type{-5}, numeric::scale_type{0}, numeric::scale_type{3}}) { + { + auto const rg0 = cudf::test::fixed_point_column_wrapper( + {RepType{100}, RepType{0}, RepType{200}}, {true, false, true}, scale); + auto const rg1 = cudf::test::fixed_point_column_wrapper( + {RepType{-50}, RepType{300}, RepType{0}}, {true, true, false}, scale); + auto const rg2 = + cudf::test::fixed_point_column_wrapper({RepType{-600}, RepType{-400}}, scale); + auto const t0 = cudf::table_view{{rg0}}; + auto const t1 = cudf::table_view{{rg1}}; + auto const t2 = cudf::table_view{{rg2}}; + + auto const options = + cudf::io::chunked_parquet_writer_options::builder(cudf::io::sink_info{filepath}) + .metadata(cudf::io::table_input_metadata(t0)) + .build(); + + cudf::io::chunked_parquet_writer writer(options); + writer.write(t0); + writer.write(t1); + writer.write(t2); + writer.close(); + } + + // Verify Parquet physical type for the decimal column is as expected + { + auto const meta = cudf::io::read_parquet_metadata(cudf::io::source_info{filepath}); + auto const& root = meta.schema().root(); + ASSERT_GE(root.num_children(), 1); + auto const& col_schema = root.child(0); + if constexpr (std::is_same_v) { + EXPECT_EQ(col_schema.type(), cudf::io::parquet::Type::INT32); + } else if constexpr (std::is_same_v) { + EXPECT_EQ(col_schema.type(), cudf::io::parquet::Type::INT64); + } else if constexpr (std::is_same_v) { + EXPECT_EQ(col_schema.type(), cudf::io::parquet::Type::FIXED_LEN_BYTE_ARRAY); + } + } + + // Helper function to test predicate pushdown for decimal types + auto const test_predicate_pushdown = [&](cudf::ast::operation const& filter, + cudf::size_type expected_filtered_row_groups, + cudf::size_type expected_num_rows) { + auto const options = + cudf::io::parquet_reader_options::builder(cudf::io::source_info{filepath}) + .filter(filter) + .build(); + + auto const result = cudf::io::read_parquet(options); + + EXPECT_EQ(result.metadata.num_input_row_groups, num_input_row_groups); + EXPECT_TRUE(result.metadata.num_row_groups_after_stats_filter.has_value()); + EXPECT_EQ(result.metadata.num_row_groups_after_stats_filter.value(), + expected_filtered_row_groups); + EXPECT_EQ(result.tbl->num_rows(), expected_num_rows); + }; + + auto const col_ref = cudf::ast::column_reference(0); + + // Filter: col0 >= 100 AND col0 <= 200 + { + auto scalar_100 = cudf::fixed_point_scalar(RepType{100}, scale); + auto scalar_200 = cudf::fixed_point_scalar(RepType{200}, scale); + auto const literal_100 = cudf::ast::literal(scalar_100); + auto const literal_200 = cudf::ast::literal(scalar_200); + auto const col_ge_100 = + cudf::ast::operation(cudf::ast::ast_operator::GREATER_EQUAL, col_ref, literal_100); + auto const col_le_200 = + cudf::ast::operation(cudf::ast::ast_operator::LESS_EQUAL, col_ref, literal_200); + auto const filter = + cudf::ast::operation(cudf::ast::ast_operator::LOGICAL_AND, col_ge_100, col_le_200); + + // RGs 0, 1 pass + test_predicate_pushdown(filter, 2, 2); + } + + // Filter: col0 >= -550 AND col0 <= -450 + { + auto scalar_neg_550 = cudf::fixed_point_scalar(RepType{-550}, scale); + auto scalar_neg_450 = cudf::fixed_point_scalar(RepType{-450}, scale); + auto const literal_neg_550 = cudf::ast::literal(scalar_neg_550); + auto const literal_neg_450 = cudf::ast::literal(scalar_neg_450); + auto const col_ge_neg_550 = + cudf::ast::operation(cudf::ast::ast_operator::GREATER_EQUAL, col_ref, literal_neg_550); + auto const col_le_neg_450 = + cudf::ast::operation(cudf::ast::ast_operator::LESS_EQUAL, col_ref, literal_neg_450); + auto const filter = + cudf::ast::operation(cudf::ast::ast_operator::LOGICAL_AND, col_ge_neg_550, col_le_neg_450); + + // RG 2 passes + test_predicate_pushdown(filter, 1, 0); + } + + // Filter: col0 >= 700 AND col0 <= 900 — matches no row groups + { + auto scalar_700 = cudf::fixed_point_scalar(RepType{700}, scale); + auto scalar_900 = cudf::fixed_point_scalar(RepType{900}, scale); + auto const literal_700 = cudf::ast::literal(scalar_700); + auto const literal_900 = cudf::ast::literal(scalar_900); + auto const col_ge_700 = + cudf::ast::operation(cudf::ast::ast_operator::GREATER_EQUAL, col_ref, literal_700); + auto const col_le_900 = + cudf::ast::operation(cudf::ast::ast_operator::LESS_EQUAL, col_ref, literal_900); + auto const filter = + cudf::ast::operation(cudf::ast::ast_operator::LOGICAL_AND, col_ge_700, col_le_900); + + test_predicate_pushdown(filter, 0, 0); + } + + // Filter: col0 == -400 + { + auto scalar_neg_400 = cudf::fixed_point_scalar(RepType{-400}, scale); + auto const literal_neg_400 = cudf::ast::literal(scalar_neg_400); + auto const filter = + cudf::ast::operation(cudf::ast::ast_operator::EQUAL, col_ref, literal_neg_400); + + // RG 2 passes + test_predicate_pushdown(filter, 1, 1); + } + + // Filter: col0 >= -100 AND col0 <= 250 + { + auto scalar_neg_100 = cudf::fixed_point_scalar(RepType{-100}, scale); + auto scalar_250 = cudf::fixed_point_scalar(RepType{250}, scale); + auto const literal_neg_100 = cudf::ast::literal(scalar_neg_100); + auto const literal_250 = cudf::ast::literal(scalar_250); + auto const col_ge = + cudf::ast::operation(cudf::ast::ast_operator::GREATER_EQUAL, col_ref, literal_neg_100); + auto const col_le = + cudf::ast::operation(cudf::ast::ast_operator::LESS_EQUAL, col_ref, literal_250); + auto const filter = + cudf::ast::operation(cudf::ast::ast_operator::LOGICAL_AND, col_ge, col_le); + + // RGs 0, 1 pass + test_predicate_pushdown(filter, 2, 3); + } + + // Filter: col0 == 100 OR col0 == -50 + { + auto scalar_100 = cudf::fixed_point_scalar(RepType{100}, scale); + auto scalar_neg_50 = cudf::fixed_point_scalar(RepType{-50}, scale); + auto const literal_100 = cudf::ast::literal(scalar_100); + auto const literal_neg_50 = cudf::ast::literal(scalar_neg_50); + auto const eq_100 = + cudf::ast::operation(cudf::ast::ast_operator::EQUAL, col_ref, literal_100); + auto const eq_neg_50 = + cudf::ast::operation(cudf::ast::ast_operator::EQUAL, col_ref, literal_neg_50); + auto const filter = + cudf::ast::operation(cudf::ast::ast_operator::LOGICAL_OR, eq_100, eq_neg_50); + + // RGs 0, 1 pass + test_predicate_pushdown(filter, 2, 2); + } + + // Large value filter only for decimal128 (overflows smaller rep types) + if constexpr (std::is_same_v) { + auto const big_val = (static_cast<__int128_t>(1) << 70) + 1234; + auto scalar_val = cudf::fixed_point_scalar(big_val, scale); + auto const lit = cudf::ast::literal(scalar_val); + auto const filter = cudf::ast::operation(cudf::ast::ast_operator::GREATER, col_ref, lit); + + // No RGs pass + test_predicate_pushdown(filter, 0, 0); + } + } +} + +template +struct ParquetPredicatePushdownTestAST : public ParquetReaderTest {}; +TYPED_TEST_SUITE(ParquetPredicatePushdownTestAST, SupportedTestTypesAST); + +TYPED_TEST(ParquetPredicatePushdownTestAST, FilterTyped) { filter_typed_test(); filter_unary_operation_typed_test(); + if constexpr (cudf::is_fixed_point()) { decimal_stats_filter_test(); } } -TYPED_TEST(ParquetPredicatePushdownTest, FilterTypedJIT) +template +struct ParquetPredicatePushdownTestJIT : public ParquetReaderTest {}; +TYPED_TEST_SUITE(ParquetPredicatePushdownTestJIT, SupportedTestTypesJIT); + +TYPED_TEST(ParquetPredicatePushdownTestJIT, FilterTyped) { filter_typed_test(); - // JIT does not support nullness-dependent operators such as IS_NULL so we can't call - // `filter_unary_operation_typed_test` - // Ref: https://github.com/rapidsai/cudf/issues/20177 + // JIT does not support decimals and nullness-dependent operators (IS_NULL) so we can't test: + // `filter_unary_operation_typed_test()` and `decimal_stats_filter_test()`. + // Refs: https://github.com/rapidsai/cudf/issues/20177 and + // https://github.com/rapidsai/cudf/issues/21584 } TEST_P(ParquetDecompressionTest, RoundTripBasic)