diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 891a9b202b90..030023801a5b 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -894,6 +894,7 @@ add_library( src/structs/utilities.cu src/table/table.cpp src/table/table_device_view.cu + src/table/table_equal.cu src/table/table_view.cpp src/text/deduplicate.cu src/text/detokenize.cu diff --git a/cpp/include/cudf/table/equality.hpp b/cpp/include/cudf/table/equality.hpp new file mode 100644 index 000000000000..22af77300f77 --- /dev/null +++ b/cpp/include/cudf/table/equality.hpp @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION. + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include +#include +#include +#include + +#include +namespace CUDF_EXPORT cudf { + +/** + * @brief Check if two tables are equal. + * + * Returns true if the input tables have the same number of rows, the same number of columns, + * matching column types, and every row in `left` compares equal to the row at the same index in + * `right`. Null equality is controlled by `nulls_equal`. Floating point NaN values compare equal. + * + * @throws cudf::logic_error if the tables contain `EMPTY` types. + * + * @param left The first table to compare + * @param right The second table to compare + * @param nulls_equal Flag to denote if null elements should be considered equal + * @param stream CUDA stream used for device memory operations and kernel launches + * @return true if the tables are equal, false otherwise + */ +[[nodiscard]] bool tables_equal(table_view const& left, + table_view const& right, + null_equality nulls_equal = null_equality::EQUAL, + rmm::cuda_stream_view stream = cudf::get_default_stream()); + +} // namespace CUDF_EXPORT cudf diff --git a/cpp/src/row_operator/row_operators.cu b/cpp/src/row_operator/row_operators.cu index 9ab21e9cfcbf..4b21d9c980cb 100644 --- a/cpp/src/row_operator/row_operators.cu +++ b/cpp/src/row_operator/row_operators.cu @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION. * SPDX-License-Identifier: Apache-2.0 */ @@ -25,6 +25,7 @@ #include #include +#include namespace cudf { namespace detail { @@ -356,9 +357,10 @@ void check_eq_compatibility(table_view const& input) { column_checker_fn_t check_column = [&](column_view const& c) { if (not is_nested(c.type())) { - CUDF_EXPECTS(is_equality_comparable(c.type()), - "Cannot compare equality for a table with a column of type " + - cudf::type_to_name(c.type())); + CUDF_EXPECTS( + is_equality_comparable(c.type()), + "Cannot compare equality for a table with a column of type " + cudf::type_to_name(c.type()), + std::invalid_argument); } for (auto child = c.child_begin(); child < c.child_end(); ++child) { check_column(*child); diff --git a/cpp/src/table/table_equal.cu b/cpp/src/table/table_equal.cu new file mode 100644 index 000000000000..0cc97e0da260 --- /dev/null +++ b/cpp/src/table/table_equal.cu @@ -0,0 +1,80 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +namespace cudf { +namespace detail { +namespace { + +template +[[nodiscard]] bool tables_equal(table_view const& left, + table_view const& right, + null_equality nulls_equal, + rmm::cuda_stream_view stream) +{ + auto const comparator = detail::row::equality::two_table_comparator{left, right, stream}; + auto const rows_equal = comparator.equal_to( + nullate::DYNAMIC{has_nested_nulls(left) or has_nested_nulls(right)}, nulls_equal); + rmm::device_uvector eq_rows{ + static_cast(left.num_rows()), stream, cudf::get_current_device_resource_ref()}; + CUDF_CUDA_TRY(cub::DeviceTransform::Transform( + cuda::counting_iterator{0}, + eq_rows.begin(), + eq_rows.size(), + [rows_equal] __device__(size_type i) -> bool { + return rows_equal(detail::row::lhs_index_type{i}, detail::row::rhs_index_type{i}); + }, + stream.value())); + return cudf::detail::reduce( + eq_rows.begin(), eq_rows.end(), true, cuda::std::logical_and{}, stream); +} + +} // namespace + +[[nodiscard]] bool tables_equal(table_view const& left, + table_view const& right, + null_equality nulls_equal, + rmm::cuda_stream_view stream) +{ + if (left.num_rows() != right.num_rows() || left.num_columns() != right.num_columns() || + !have_same_types(left, right)) { + return false; + } else if (left.num_rows() == 0) { + return true; + } + + return cudf::has_nested_columns(left) || cudf::has_nested_columns(right) + ? tables_equal(left, right, nulls_equal, stream) + : tables_equal(left, right, nulls_equal, stream); +} +} // namespace detail + +bool tables_equal(table_view const& left, + table_view const& right, + null_equality nulls_equal, + rmm::cuda_stream_view stream) +{ + CUDF_FUNC_RANGE(); + return detail::tables_equal(left, right, nulls_equal, stream); +} + +} // namespace cudf diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index 3f95595ad6f6..bae781f36bda 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -793,6 +793,7 @@ ConfigureTest( STREAM_MODE testing ) +ConfigureTest(STREAM_TABLE_EQUALITY_TEST streams/table_equality_test.cpp STREAM_MODE testing) ConfigureTest( STREAM_TEXT_TEST streams/text/edit_distance_test.cpp diff --git a/cpp/tests/streams/table_equality_test.cpp b/cpp/tests/streams/table_equality_test.cpp new file mode 100644 index 000000000000..fdd85eb1b1b5 --- /dev/null +++ b/cpp/tests/streams/table_equality_test.cpp @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include + +#include +#include +#include + +class TableEqualTest : public cudf::test::BaseFixture {}; + +TEST_F(TableEqualTest, NotEqual) +{ + cudf::test::fixed_width_column_wrapper left( + {{0, 0, 0, 0, 0}, {false, false, true, true, true}}); + cudf::test::fixed_width_column_wrapper right({1, 1, 1, 1, 1}); + std::ignore = cudf::tables_equal(cudf::table_view{{left}}, + cudf::table_view{{right}}, + cudf::null_equality::EQUAL, + cudf::test::get_default_stream()); +} + +CUDF_TEST_PROGRAM_MAIN() diff --git a/cpp/tests/table/table_tests.cpp b/cpp/tests/table/table_tests.cpp index b909d9c9392f..c7d37b943b65 100644 --- a/cpp/tests/table/table_tests.cpp +++ b/cpp/tests/table/table_tests.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2019-2025, NVIDIA CORPORATION. + * SPDX-FileCopyrightText: Copyright (c) 2019-2026, NVIDIA CORPORATION. * SPDX-License-Identifier: Apache-2.0 */ @@ -10,15 +10,22 @@ #include #include +#include #include #include +#include + +#include #include +#include template using column_wrapper = cudf::test::fixed_width_column_wrapper; -using s_col_wrapper = cudf::test::strings_column_wrapper; +using s_col_wrapper = cudf::test::strings_column_wrapper; +using lists_col_wrapper = cudf::test::lists_column_wrapper; +using structs_col_wrapper = cudf::test::structs_column_wrapper; using CVector = std::vector>; using column = cudf::column; @@ -163,4 +170,130 @@ TEST_F(TableTest, AllocSizeWithNulls) EXPECT_EQ(t.alloc_size(), 152); // bitmask has padding } +TEST_F(TableTest, TablesEqual) +{ + column_wrapper left_col0{{1, 2, 3}}; + column_wrapper left_col1{{4.0, 5.0, 6.0}}; + column_wrapper right_col0{{1, 2, 3}}; + column_wrapper right_col1{{4.0, 5.0, 6.0}}; + + EXPECT_TRUE(cudf::tables_equal(cudf::table_view{{left_col0, left_col1}}, + cudf::table_view{{right_col0, right_col1}})); +} + +TEST_F(TableTest, TablesEqualValueMismatch) +{ + column_wrapper left{{1, 2, 3}}; + column_wrapper right{{1, 4, 3}}; + + EXPECT_FALSE(cudf::tables_equal(cudf::table_view{{left}}, cudf::table_view{{right}})); +} + +TEST_F(TableTest, TablesEqualShapeAndTypeMismatch) +{ + column_wrapper left{{1, 2, 3}}; + column_wrapper shorter{{1, 2}}; + column_wrapper extra{{1, 2, 3}}; + column_wrapper different_type{{1, 2, 3}}; + + EXPECT_FALSE(cudf::tables_equal(cudf::table_view{{left}}, cudf::table_view{{shorter}})); + EXPECT_FALSE(cudf::tables_equal(cudf::table_view{{left}}, cudf::table_view{{left, extra}})); + EXPECT_FALSE(cudf::tables_equal(cudf::table_view{{left}}, cudf::table_view{{different_type}})); +} + +TEST_F(TableTest, TablesEqualNullEquality) +{ + column_wrapper left{{1, 2, 3}, {1, 0, 1}}; + column_wrapper right{{1, 4, 3}, {1, 0, 1}}; + + EXPECT_TRUE(cudf::tables_equal( + cudf::table_view{{left}}, cudf::table_view{{right}}, cudf::null_equality::EQUAL)); + EXPECT_FALSE(cudf::tables_equal( + cudf::table_view{{left}}, cudf::table_view{{right}}, cudf::null_equality::UNEQUAL)); +} + +TEST_F(TableTest, TablesEqualNaNsCompareEqual) +{ + column_wrapper left{{std::numeric_limits::quiet_NaN(), 1.0}}; + column_wrapper right{{std::numeric_limits::quiet_NaN(), 1.0}}; + + EXPECT_TRUE(cudf::tables_equal(cudf::table_view{{left}}, cudf::table_view{{right}})); +} + +TEST_F(TableTest, TablesEqualStructColumns) +{ + column_wrapper left_id{{1, 2, 3}}; + column_wrapper left_inner_value{{10, 20, 30}}; + column_wrapper left_deep_leaf{{1.25, 2.5, 3.75}}; + structs_col_wrapper left_inner{{left_inner_value, left_deep_leaf}}; + structs_col_wrapper left_outer{{left_id, left_inner}}; + + column_wrapper right_id{{1, 2, 3}}; + column_wrapper right_inner_value{{10, 20, 30}}; + column_wrapper right_deep_leaf{{1.25, 2.5, 3.75}}; + structs_col_wrapper right_inner{{right_inner_value, right_deep_leaf}}; + structs_col_wrapper right_outer{{right_id, right_inner}}; + + EXPECT_TRUE(cudf::tables_equal(cudf::table_view{{left_outer}}, cudf::table_view{{right_outer}})); +} + +TEST_F(TableTest, TablesEqualStructColumnsDeepLeafMismatch) +{ + column_wrapper left_id{{1, 2, 3}}; + column_wrapper left_inner_value{{10, 20, 30}}; + column_wrapper left_deep_leaf{{1.25, 2.5, 3.75}}; + structs_col_wrapper left_inner{{left_inner_value, left_deep_leaf}}; + structs_col_wrapper left_outer{{left_id, left_inner}}; + + column_wrapper right_id{{1, 2, 3}}; + column_wrapper right_inner_value{{10, 20, 30}}; + column_wrapper right_deep_leaf{{1.25, 2.5, 99.0}}; + structs_col_wrapper right_inner{{right_inner_value, right_deep_leaf}}; + structs_col_wrapper right_outer{{right_id, right_inner}}; + + EXPECT_FALSE(cudf::tables_equal(cudf::table_view{{left_outer}}, cudf::table_view{{right_outer}})); +} + +TEST_F(TableTest, TablesEqualThrowsForNonEqualityComparableTypes) +{ + auto left = + column{cudf::data_type{cudf::type_id::EMPTY}, 3, rmm::device_buffer{}, rmm::device_buffer{}, 0}; + auto right = + column{cudf::data_type{cudf::type_id::EMPTY}, 3, rmm::device_buffer{}, rmm::device_buffer{}, 0}; + + EXPECT_THROW( + std::ignore = cudf::tables_equal(cudf::table_view{{left}}, cudf::table_view{{right}}), + cudf::logic_error); +} + +TEST_F(TableTest, TablesEqualListColumns) +{ + lists_col_wrapper left{{1, 2}, {3}, {}}; + lists_col_wrapper right{{1, 2}, {3}, {}}; + lists_col_wrapper different_values{{1, 2}, {4}, {}}; + lists_col_wrapper different_offsets{{1}, {2, 3}, {}}; + + EXPECT_TRUE(cudf::tables_equal(cudf::table_view{{left}}, cudf::table_view{{right}})); + EXPECT_FALSE(cudf::tables_equal(cudf::table_view{{left}}, cudf::table_view{{different_values}})); + EXPECT_FALSE(cudf::tables_equal(cudf::table_view{{left}}, cudf::table_view{{different_offsets}})); +} + +TEST_F(TableTest, TablesEqualStructColumnsWithLists) +{ + column_wrapper left_id{{1, 2, 3}}; + lists_col_wrapper left_list{{1, 2}, {3}, {}}; + structs_col_wrapper left{{left_id, left_list}}; + + column_wrapper right_id{{1, 2, 3}}; + lists_col_wrapper right_list{{1, 2}, {3}, {}}; + structs_col_wrapper right{{right_id, right_list}}; + + column_wrapper different_id{{1, 2, 3}}; + lists_col_wrapper different_list{{1, 2}, {4}, {}}; + structs_col_wrapper different{{different_id, different_list}}; + + EXPECT_TRUE(cudf::tables_equal(cudf::table_view{{left}}, cudf::table_view{{right}})); + EXPECT_FALSE(cudf::tables_equal(cudf::table_view{{left}}, cudf::table_view{{different}})); +} + CUDF_TEST_PROGRAM_MAIN() diff --git a/cpp/tests/types/traits_test.cpp b/cpp/tests/types/traits_test.cpp index 6dbaadf56227..6ddbe7a53692 100644 --- a/cpp/tests/types/traits_test.cpp +++ b/cpp/tests/types/traits_test.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2019-2024, NVIDIA CORPORATION. + * SPDX-FileCopyrightText: Copyright (c) 2019-2026, NVIDIA CORPORATION. * SPDX-License-Identifier: Apache-2.0 */ @@ -115,7 +115,7 @@ TYPED_TEST(TypedTraitsTest, NotEqualityComparableWithList) bool comparable = cudf::is_equality_comparable(); EXPECT_FALSE(comparable); - cudf::is_equality_comparable(); + comparable = cudf::is_equality_comparable(); EXPECT_FALSE(comparable); }