Current section
Files
Jump to
Current section
Files
native/clickhouse-cpp/ut/Column_ut.cpp
#include <clickhouse/columns/array.h>
#include <clickhouse/columns/tuple.h>
#include <clickhouse/columns/date.h>
#include <clickhouse/columns/enum.h>
#include <clickhouse/columns/lowcardinality.h>
#include <clickhouse/columns/nullable.h>
#include <clickhouse/columns/numeric.h>
#include <clickhouse/columns/string.h>
#include <clickhouse/columns/uuid.h>
#include <clickhouse/columns/ip4.h>
#include <clickhouse/columns/ip6.h>
#include <clickhouse/base/input.h>
#include <clickhouse/base/output.h>
#include <clickhouse/base/socket.h> // for ipv4-ipv6 platform-specific stuff
#include <clickhouse/client.h>
#include <gtest/gtest.h>
#include <algorithm>
#include <initializer_list>
#include <memory>
#include <type_traits>
#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<int>(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 <typename ColumnTypeT,
typename std::shared_ptr<ColumnTypeT> (*CreatorFunction)(),
typename GeneratorValueType,
typename std::vector<GeneratorValueType> (*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 <typename T>
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 <typename Values>
static void AppendValues(std::shared_ptr<ColumnType> 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<std::string> CheckIfShouldSkipTest(clickhouse::Client& client) {
if constexpr (std::is_same_v<ColumnType, ColumnDate32>) {
// 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<ColumnType, ColumnInt128>) {
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 <typename ColumnType>
static void TestColumnRoundtrip(const std::shared_ptr<ColumnType> & 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<ColumnType>();
EXPECT_TRUE(CompareRecursive(*column, *result_typed));
}
template <typename ColumnType, typename CompressionMethods>
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 <typename ColumnType, size_t ...ConstructorParams>
auto makeColumn()
{
return std::make_shared<ColumnType>(ConstructorParams...);
}
template <typename ColumnTypeT>
struct NumberColumnTestCase : public GenericColumnTestCase<ColumnTypeT, &makeColumn<ColumnTypeT>, typename ColumnTypeT::ValueType, &MakeNumbers<typename ColumnTypeT::ValueType>>
{
using Base = GenericColumnTestCase<ColumnTypeT, &makeColumn<ColumnTypeT>, typename ColumnTypeT::ValueType, &MakeNumbers<typename ColumnTypeT::ValueType>>;
using ColumnType = typename Base::ColumnType;
using Base::createColumn;
using Base::generateValues;
};
template <typename ColumnTypeT, size_t precision, size_t scale>
struct DecimalColumnTestCase : public GenericColumnTestCase<ColumnDecimal, &makeColumn<ColumnDecimal, precision, scale>, clickhouse::Int128, &MakeDecimals<precision, scale>>
{
using Base = GenericColumnTestCase<ColumnDecimal, &makeColumn<ColumnDecimal, precision, scale>, clickhouse::Int128, &MakeDecimals<precision, scale>>;
using ColumnType = typename Base::ColumnType;
using Base::createColumn;
using Base::generateValues;
};
using TestCases = ::testing::Types<
NumberColumnTestCase<ColumnUInt8>,
NumberColumnTestCase<ColumnUInt16>,
NumberColumnTestCase<ColumnUInt32>,
NumberColumnTestCase<ColumnUInt64>,
NumberColumnTestCase<ColumnInt8>,
NumberColumnTestCase<ColumnInt16>,
NumberColumnTestCase<ColumnInt32>,
NumberColumnTestCase<ColumnInt64>,
NumberColumnTestCase<ColumnFloat32>,
NumberColumnTestCase<ColumnFloat64>,
GenericColumnTestCase<ColumnString, &makeColumn<ColumnString>, std::string, &MakeStrings>,
GenericColumnTestCase<ColumnFixedString, &makeColumn<ColumnFixedString, 12>, std::string, &MakeFixedStrings<12>>,
GenericColumnTestCase<ColumnDate, &makeColumn<ColumnDate>, time_t, &MakeDates<time_t>>,
GenericColumnTestCase<ColumnDate32, &makeColumn<ColumnDate32>, time_t, &MakeDates<time_t>>,
GenericColumnTestCase<ColumnDateTime, &makeColumn<ColumnDateTime>, clickhouse::Int64, &MakeDateTimes>,
GenericColumnTestCase<ColumnDateTime64, &makeColumn<ColumnDateTime64, 0>, clickhouse::Int64, &MakeDateTime64s<0>>,
GenericColumnTestCase<ColumnDateTime64, &makeColumn<ColumnDateTime64, 3>, clickhouse::Int64, &MakeDateTime64s<3>>,
GenericColumnTestCase<ColumnDateTime64, &makeColumn<ColumnDateTime64, 6>, clickhouse::Int64, &MakeDateTime64s<6>>,
GenericColumnTestCase<ColumnDateTime64, &makeColumn<ColumnDateTime64, 9>, clickhouse::Int64, &MakeDateTime64s<9>>,
GenericColumnTestCase<ColumnIPv4, &makeColumn<ColumnIPv4>, in_addr, &MakeIPv4s>,
GenericColumnTestCase<ColumnIPv6, &makeColumn<ColumnIPv6>, in6_addr, &MakeIPv6s>,
GenericColumnTestCase<ColumnInt128, &makeColumn<ColumnInt128>, clickhouse::Int128, &MakeInt128s>,
GenericColumnTestCase<ColumnUInt128, &makeColumn<ColumnUInt128>, clickhouse::UInt128, &MakeUInt128s>,
GenericColumnTestCase<ColumnUUID, &makeColumn<ColumnUUID>, clickhouse::UUID, &MakeUUIDs>,
DecimalColumnTestCase<ColumnDecimal, 18, 0>,
DecimalColumnTestCase<ColumnDecimal, 18, 6>,
DecimalColumnTestCase<ColumnDecimal, 18, 12>,
// there is an arithmetical overflow on some values in the test value generator harness
// DecimalColumnTestCase<ColumnDecimal, 18, 15>,
DecimalColumnTestCase<ColumnDecimal, 12, 0>,
DecimalColumnTestCase<ColumnDecimal, 12, 6>,
DecimalColumnTestCase<ColumnDecimal, 12, 9>,
DecimalColumnTestCase<ColumnDecimal, 6, 0>,
DecimalColumnTestCase<ColumnDecimal, 6, 3>,
GenericColumnTestCase<ColumnLowCardinalityT<ColumnString>, &makeColumn<ColumnLowCardinalityT<ColumnString>>, std::string, &MakeStrings>
// Array(String)
// GenericColumnTestCase<ColumnArrayT<ColumnString>, &makeColumn<ColumnArrayT<ColumnString>>, std::vector<std::string>, &MakeArrays<std::string, &MakeStrings>>
// // Array(Array(String))
// GenericColumnTestCase<ColumnArrayT<ColumnArrayT<ColumnString>>, &makeColumn<ColumnArrayT<ColumnArrayT<ColumnString>>>,
// std::vector<std::vector<std::string>>,
// &MakeArrays<std::vector<std::string>, &MakeArrays<std::string, &MakeStrings>>>
>;
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<typename TestFixture::ColumnType>());
ASSERT_EQ(0u, slice->Size());
}
{
auto clone = column->CloneEmpty();
ASSERT_NO_THROW(clone->template AsStrict<typename TestFixture::ColumnType>());
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 <typename ColumnType, typename ValueType>
inline auto convertValueForGetItem(const ColumnType& col, ValueType&& t) {
using T = std::remove_cv_t<std::decay_t<ValueType>>;
if constexpr (std::is_same_v<ColumnType, ColumnDecimal>) {
// 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<const char*>(&t), item.data.size());
} else if constexpr (std::is_same_v<T, clickhouse::UInt128> || std::is_same_v<T, clickhouse::UUID>
|| std::is_same_v<T, clickhouse::Int128>) {
return std::string_view{reinterpret_cast<const char*>(&t), sizeof(T)};
} else if constexpr (std::is_same_v<T, in_addr>) {
return htonl(t.s_addr);
} else if constexpr (std::is_same_v<T, in6_addr>) {
return std::string_view(reinterpret_cast<const char*>(t.s6_addr), 16);
} else if constexpr (std::is_same_v<ColumnType, ColumnDate>) {
return static_cast<uint16_t>(t / std::time_t(86400));
} else if constexpr (std::is_same_v<ColumnType, ColumnDate32>) {
return static_cast<uint32_t>(t / std::time_t(86400));
} else if constexpr (std::is_same_v<ColumnType, ColumnDateTime>) {
return static_cast<uint32_t>(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>{
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<decltype(v)>()))
<< " 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<typename TestFixture::ColumnType>();
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<typename TestFixture::ColumnType>();
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<column_type> && has_method_Capacity_v<column_type>) {
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<typename TestFixture::ColumnType>) {
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<typename TestFixture::ColumnType,
ColumnDate,
ColumnDate32>) {
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<char[]> buffer = std::make_unique<char[]>(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<size_t>("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<typename TestFixture::ColumnType>;
auto non_nullable_column = this->MakeColumn();
if (non_nullable_column->GetType().GetCode() == Type::Code::LowCardinality)
// TODO (vnemkov): wrap as ColumnLowCardinalityT<ColumnNullableT<NestedColumn>> instead of ColumnNullableT<ColumnLowCardinalityT<NestedColumn>>
GTEST_SKIP() << "Can't have " << non_nullable_column->GetType().GetName() << " in Nullable";
auto column = std::make_shared<NullableType>(std::move(non_nullable_column));
auto values = this->GenerateValues(10'000);
FromVectorGenerator<bool> 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<typename TestFixture::ColumnType>;
auto [nested_column, values] = this->MakeColumnWithValues(100);
auto column = std::make_shared<ColumnArrayType>(nested_column->CloneEmpty()->template As<typename TestFixture::ColumnType>());
for (size_t i = 0; i < values.size(); ++i)
{
const std::vector<std::decay_t<decltype(values[0])>> 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);
}