#include #include #include #include #include #include #include #include #include #include #include #include #include #include // for ipv4-ipv6 platform-specific stuff #include #include #include #include #include #include #include "gtest/internal/gtest-internal.h" #include "ut/utils_comparison.h" #include "ut/utils_meta.h" #include "utils.h" #include "roundtrip_column.h" #include "value_generators.h" namespace { using namespace clickhouse; } namespace clickhouse{ std::ostream& operator<<(std::ostream& ostr, const Type::Code& type_code) { return ostr << Type::TypeName(type_code) << " (" << static_cast(type_code) << ")"; } } // Generic tests for a Column subclass against basic API: // 1. Constructor: Create, ensure that it is empty // 2. Append: Create, add some data one by one via Append, make sure that values inserted match extracted with At() and operator[] // 3. Slice: Create, add some data via Append, do Slice() // 4. CloneEmpty Create, invoke CloneEmplty, ensure that clone is Empty // 5. Clear: Create, add some data, invoke Clear(), make sure column is empty // 6. Swap: create two instances, populate one with data, swap with second, make sure has data was transferred // 7. Load/Save: create, append some data, save to buffer, load from same buffer into new column, make sure columns match. template (*CreatorFunction)(), typename GeneratorValueType, typename std::vector (*GeneratorFunction)()> struct GenericColumnTestCase { using ColumnType = ColumnTypeT; static constexpr auto Creator = CreatorFunction; static constexpr auto Generator = GeneratorFunction; static auto createColumn() { return CreatorFunction(); } static auto generateValues() { return GeneratorFunction(); } }; template class GenericColumnTest : public testing::Test { public: using ColumnType = typename T::ColumnType; static auto MakeColumn() { return T::createColumn(); } static auto GenerateValues(size_t values_size) { return GenerateVector(values_size, FromVectorGenerator{T::generateValues()}); } template static void AppendValues(std::shared_ptr column, const Values& values) { for (const auto & v : values) { column->Append(v); } } static auto MakeColumnWithValues(size_t values_size) { auto column = MakeColumn(); auto values = GenerateValues(values_size); AppendValues(column, values); return std::tuple{column, values}; } static std::optional CheckIfShouldSkipTest(clickhouse::Client& client) { if constexpr (std::is_same_v) { // Date32 first appeared in v21.9.2.17-stable const auto server_info = client.GetServerInfo(); if (versionNumber(server_info) < versionNumber(21, 9)) { std::stringstream buffer; buffer << "Date32 is available since v21.9.2.17-stable and can't be tested against server: " << server_info; return buffer.str(); } } if constexpr (std::is_same_v) { const auto server_info = client.GetServerInfo(); if (versionNumber(server_info) < versionNumber(21, 7)) { std::stringstream buffer; buffer << "ColumnInt128 is available since v21.7.2.7-stable and can't be tested against server: " << server_info; return buffer.str(); } } return std::nullopt; } template static void TestColumnRoundtrip(const std::shared_ptr & column, const ClientOptions & client_options) { SCOPED_TRACE(::testing::Message("Column type: ") << column->GetType().GetName()); SCOPED_TRACE(::testing::Message("Client options: ") << client_options); clickhouse::Client client(client_options); if (auto message = CheckIfShouldSkipTest(client)) { GTEST_SKIP() << *message; } auto result_typed = RoundtripColumnValues(client, column)->template AsStrict(); EXPECT_TRUE(CompareRecursive(*column, *result_typed)); } template static void TestColumnRoundtrip(const ColumnType & column, const ClientOptions & client_options, CompressionMethods && compression_methods) { for (auto compressionMethod : compression_methods) { ClientOptions new_options = ClientOptions(client_options).SetCompressionMethod(compressionMethod); TestColumnRoundtrip(column, new_options); } } }; // Luckily all (non-data copying/moving) constructors have size_t params. template auto makeColumn() { return std::make_shared(ConstructorParams...); } template struct NumberColumnTestCase : public GenericColumnTestCase, typename ColumnTypeT::ValueType, &MakeNumbers> { using Base = GenericColumnTestCase, typename ColumnTypeT::ValueType, &MakeNumbers>; using ColumnType = typename Base::ColumnType; using Base::createColumn; using Base::generateValues; }; template struct DecimalColumnTestCase : public GenericColumnTestCase, clickhouse::Int128, &MakeDecimals> { using Base = GenericColumnTestCase, clickhouse::Int128, &MakeDecimals>; using ColumnType = typename Base::ColumnType; using Base::createColumn; using Base::generateValues; }; using TestCases = ::testing::Types< NumberColumnTestCase, NumberColumnTestCase, NumberColumnTestCase, NumberColumnTestCase, NumberColumnTestCase, NumberColumnTestCase, NumberColumnTestCase, NumberColumnTestCase, NumberColumnTestCase, NumberColumnTestCase, GenericColumnTestCase, std::string, &MakeStrings>, GenericColumnTestCase, std::string, &MakeFixedStrings<12>>, GenericColumnTestCase, time_t, &MakeDates>, GenericColumnTestCase, time_t, &MakeDates>, GenericColumnTestCase, clickhouse::Int64, &MakeDateTimes>, GenericColumnTestCase, clickhouse::Int64, &MakeDateTime64s<0>>, GenericColumnTestCase, clickhouse::Int64, &MakeDateTime64s<3>>, GenericColumnTestCase, clickhouse::Int64, &MakeDateTime64s<6>>, GenericColumnTestCase, clickhouse::Int64, &MakeDateTime64s<9>>, GenericColumnTestCase, in_addr, &MakeIPv4s>, GenericColumnTestCase, in6_addr, &MakeIPv6s>, GenericColumnTestCase, clickhouse::Int128, &MakeInt128s>, GenericColumnTestCase, clickhouse::UInt128, &MakeUInt128s>, GenericColumnTestCase, clickhouse::UUID, &MakeUUIDs>, DecimalColumnTestCase, DecimalColumnTestCase, DecimalColumnTestCase, // there is an arithmetical overflow on some values in the test value generator harness // DecimalColumnTestCase, DecimalColumnTestCase, DecimalColumnTestCase, DecimalColumnTestCase, DecimalColumnTestCase, DecimalColumnTestCase, GenericColumnTestCase, &makeColumn>, std::string, &MakeStrings> // Array(String) // GenericColumnTestCase, &makeColumn>, std::vector, &MakeArrays> // // Array(Array(String)) // GenericColumnTestCase>, &makeColumn>>, // std::vector>, // &MakeArrays, &MakeArrays>> >; TYPED_TEST_SUITE(GenericColumnTest, TestCases); TYPED_TEST(GenericColumnTest, Construct) { auto column = this->MakeColumn(); ASSERT_EQ(0u, column->Size()); } TYPED_TEST(GenericColumnTest, EmptyColumn) { auto column = this->MakeColumn(); ASSERT_EQ(0u, column->Size()); // verify that Column methods work as expected on empty column: // some throw exceptions, some return poper values (like CloneEmpty) // Shouldn't be able to get items on empty column. ASSERT_ANY_THROW(column->At(0)); { auto slice = column->Slice(0, 0); ASSERT_NO_THROW(slice->template AsStrict()); ASSERT_EQ(0u, slice->Size()); } { auto clone = column->CloneEmpty(); ASSERT_NO_THROW(clone->template AsStrict()); ASSERT_EQ(0u, clone->Size()); } ASSERT_NO_THROW(column->Clear()); ASSERT_NO_THROW(column->Swap(*this->MakeColumn())); } TYPED_TEST(GenericColumnTest, Append) { auto column = this->MakeColumn(); const auto values = this->GenerateValues(10'000); for (const auto & v : values) { EXPECT_NO_THROW(column->Append(v)); } EXPECT_TRUE(CompareRecursive(values, *column)); } // To make some value types compatitable with Column::GetItem() template inline auto convertValueForGetItem(const ColumnType& col, ValueType&& t) { using T = std::remove_cv_t>; if constexpr (std::is_same_v) { // Since ColumnDecimal can hold 32, 64, 128-bit wide data and there is no way telling at run-time. const ItemView item = col.GetItem(0); return std::string_view(reinterpret_cast(&t), item.data.size()); } else if constexpr (std::is_same_v || std::is_same_v || std::is_same_v) { return std::string_view{reinterpret_cast(&t), sizeof(T)}; } else if constexpr (std::is_same_v) { return htonl(t.s_addr); } else if constexpr (std::is_same_v) { return std::string_view(reinterpret_cast(t.s6_addr), 16); } else if constexpr (std::is_same_v) { return static_cast(t / std::time_t(86400)); } else if constexpr (std::is_same_v) { return static_cast(t / std::time_t(86400)); } else if constexpr (std::is_same_v) { return static_cast(t); } else { return t; } } TYPED_TEST(GenericColumnTest, GetItem) { auto [column, values] = this->MakeColumnWithValues(10'000); ASSERT_EQ(values.size(), column->Size()); const auto wrapping_types = std::set{ Type::Code::LowCardinality, Type::Code::Array, Type::Code::Nullable }; // For wrapping types, type of ItemView can be different from type of column if (wrapping_types.find(column->GetType().GetCode()) == wrapping_types.end() ) { EXPECT_EQ(column->GetItem(0).type, column->GetType().GetCode()); } for (size_t i = 0; i < values.size(); ++i) { const auto v = convertValueForGetItem(*column, values[i]); const ItemView item = column->GetItem(i); ASSERT_TRUE(CompareRecursive(v, item.get())) << " On item " << i << " of " << PrintContainer{values}; } } TYPED_TEST(GenericColumnTest, Slice) { auto [column, values] = this->MakeColumnWithValues(10'000); auto untyped_slice = column->Slice(0, column->Size()); auto slice = untyped_slice->template AsStrict(); EXPECT_EQ(column->GetType(), slice->GetType()); EXPECT_TRUE(CompareRecursive(values, *slice)); // TODO: slices of different sizes } TYPED_TEST(GenericColumnTest, CloneEmpty) { auto [column, values] = this->MakeColumnWithValues(10'000); EXPECT_EQ(values.size(), column->Size()); auto clone_untyped = column->CloneEmpty(); // Check that type matches auto clone = clone_untyped->template AsStrict(); EXPECT_EQ(0u, clone->Size()); EXPECT_EQ(column->GetType(), clone->GetType()); } TYPED_TEST(GenericColumnTest, Clear) { auto [column, values] = this->MakeColumnWithValues(10'000); EXPECT_EQ(values.size(), column->Size()); column->Clear(); EXPECT_EQ(0u, column->Size()); } TYPED_TEST(GenericColumnTest, Swap) { auto [column_A, values] = this->MakeColumnWithValues(10'000); auto column_B = this->MakeColumn(); column_A->Swap(*column_B); EXPECT_EQ(0u, column_A->Size()); EXPECT_TRUE(CompareRecursive(values, *column_B)); } // GTEST_SKIP for debug builds to draw attention of developer #if !defined(NDEBUG) #define COLUMN_DOESNT_IMPLEMENT(comment) GTEST_SKIP() << this->MakeColumn()->GetType().GetName() << " doesn't implement " << comment; #else #define COLUMN_DOESNT_IMPLEMENT(comment) GTEST_SUCCEED() << this->MakeColumn()->GetType().GetName() << " doesn't implement " << comment; #endif TYPED_TEST(GenericColumnTest, ReserveAndCapacity) { using column_type = typename TestFixture::ColumnType; auto [column0, values] = this->MakeColumnWithValues(2); auto values_copy = values; EXPECT_NO_THROW(column0->Reserve(0u)); EXPECT_EQ(2u, column0->Size()); EXPECT_TRUE(CompareRecursive(values, values_copy)); auto column1 = this->MakeColumn(); column1->Reserve(10u); EXPECT_EQ(0u, column1->Size()); if constexpr (has_method_Reserve_v && has_method_Capacity_v) { auto column = this->MakeColumn(); EXPECT_EQ(0u, column->Capacity()); EXPECT_NO_THROW(column->Reserve(100u)); EXPECT_EQ(100u, column->Capacity()); EXPECT_EQ(0u, column->Size()); } else { COLUMN_DOESNT_IMPLEMENT("method Reserve() and Capacity()"); } } TYPED_TEST(GenericColumnTest, GetWritableData) { if constexpr (has_method_GetWritableData_v) { auto [column, values] = this->MakeColumnWithValues(111); // Do conversion from time_t to internal representation, similar to what ColumnDate and ColumnDate32 do if constexpr (is_one_of_v) { std::for_each(values.begin(), values.end(), [](auto & value) { value /= 86400; }); } EXPECT_TRUE(CompareRecursive(values, column->GetWritableData())); } else { COLUMN_DOESNT_IMPLEMENT("method GetWritableData()"); } } TYPED_TEST(GenericColumnTest, LoadAndSave) { auto [column_A, values] = this->MakeColumnWithValues(100); // large buffer since we have pretty big values for String column auto const BufferSize = 10*1024*1024; std::unique_ptr buffer = std::make_unique(BufferSize); memset(buffer.get(), 0, BufferSize); { ArrayOutput output(buffer.get(), BufferSize); // Save ASSERT_NO_THROW(column_A->Save(&output)); } auto column_B = this->MakeColumn(); { ArrayInput input(buffer.get(), BufferSize); // Load ASSERT_TRUE(column_B->Load(&input, values.size())); } EXPECT_TRUE(CompareRecursive(*column_A, *column_B)); } const auto LocalHostEndpoint = ClientOptions() .SetHost( getEnvOrDefault("CLICKHOUSE_HOST", "localhost")) .SetPort( getEnvOrDefault("CLICKHOUSE_PORT", "9000")) .SetUser( getEnvOrDefault("CLICKHOUSE_USER", "default")) .SetPassword( getEnvOrDefault("CLICKHOUSE_PASSWORD", "")) .SetDefaultDatabase(getEnvOrDefault("CLICKHOUSE_DB", "default")); const auto AllCompressionMethods = { clickhouse::CompressionMethod::None, clickhouse::CompressionMethod::LZ4, clickhouse::CompressionMethod::ZSTD }; TYPED_TEST(GenericColumnTest, RoundTrip) { auto [column, values] = this->MakeColumnWithValues(10'000); EXPECT_EQ(values.size(), column->Size()); this->TestColumnRoundtrip(column, LocalHostEndpoint, AllCompressionMethods); } TYPED_TEST(GenericColumnTest, NullableT_RoundTrip) { using NullableType = ColumnNullableT; auto non_nullable_column = this->MakeColumn(); if (non_nullable_column->GetType().GetCode() == Type::Code::LowCardinality) // TODO (vnemkov): wrap as ColumnLowCardinalityT> instead of ColumnNullableT> GTEST_SKIP() << "Can't have " << non_nullable_column->GetType().GetName() << " in Nullable"; auto column = std::make_shared(std::move(non_nullable_column)); auto values = this->GenerateValues(10'000); FromVectorGenerator is_null({true, false}); for (size_t i = 0; i < values.size(); ++i) { if (is_null(i)) { column->Append(std::nullopt); } else { column->Append(values[i]); } } this->TestColumnRoundtrip(column, LocalHostEndpoint, AllCompressionMethods); } TYPED_TEST(GenericColumnTest, ArrayT_RoundTrip) { using ColumnArrayType = ColumnArrayT; auto [nested_column, values] = this->MakeColumnWithValues(100); auto column = std::make_shared(nested_column->CloneEmpty()->template As()); for (size_t i = 0; i < values.size(); ++i) { const std::vector> row{values.begin(), values.begin() + i}; column->Append(values.begin(), values.begin() + i); EXPECT_TRUE(CompareRecursive(row, (*column)[column->Size() - 1])); } EXPECT_EQ(values.size(), column->Size()); this->TestColumnRoundtrip(column, LocalHostEndpoint, AllCompressionMethods); }