Files
cpp/ut/Column_ut.cpp
zeekay 286f257aae De-brand the client: clickhouse-cpp -> datastore-cpp
Pure rename applied on top of upstream tag v2.6.2 (8d2415d6), not HEAD.
The fork was cut at 737145d = v2.6.2-57, which still declared version 2.6.2
in version.h; renaming there would have shipped 57 unreleased commits wearing
a 2.6.2 label. Reset to the tag first so this commit changes names only.

  clickhouse/            -> datastore/            (public include root)
  namespace clickhouse   -> namespace datastore
  CLICKHOUSE_CPP_VERSION -> DATASTORE_CPP_VERSION
  clickhouse-cpp-lib/-ut -> datastore-cpp-lib/-ut (CMake, Bazel)
  CH_MAP_BOOL_TO_UINT8   -> DS_MAP_BOOL_TO_UINT8
  CLICKHOUSE_*           -> DATASTORE_*           (test env vars)
  doc links              -> docs.hanzo.ai/datastore

This client speaks the NATIVE binary protocol only - there is no HTTP path
and no X-ClickHouse-* header anywhere in it, so the header rename that the
HTTP clients required does not apply and nothing here was broken.

The one string that reaches the wire is CLIENT_NAME, sent by both SendHello
and SendQuery's ClientInfo from a single macro, and it lands in
system.query_log.client_name. Both sites still read the one macro, so they
stay consistent and the server's Hello/Query consistency check is satisfied.
Old rows keep the old name; that is a reporting discontinuity, not a
migration, since nothing keys on it.

CI is repointed at our own server: ghcr.io/hanzoai/datastore:26.2.3.2, the
image production runs. Its entrypoint honours DATASTORE_SKIP_USER_SETUP, not
the CLICKHOUSE_ spelling the compose file used, and its config lives in
/etc/hanzo-datastore-server. ci/docker-compose/config.xml is now layered in
as a config.d drop-in so the image keeps its own white-labelling. That file
and users.xml deliberately keep a <clickhouse> root: it must match the roots
of the drop-ins the image ships, and a mismatch fails silently - the server
logs "Merging bypassed" and starts with every drop-in discarded.

The remote-TLS suites used to default to the third-party host
play.clickhouse.com. They now read DATASTORE_SECURE*_HOST and skip when it is
unset rather than point at a host we do not run.

Dropped as upstream infrastructure with no meaning here: AI_POLICY.md
(upstream's policy about their codebase), .travis.yml (dead CI referencing a
third-party apt repo), cross-repo-bug-relay.yml (relays our issues into an
upstream triage repo). LICENSE keeps its copyright notice, as Apache-2.0
requires.

Verified: cmake configure + 115-target build clean; the full suite is green
against ghcr.io/hanzoai/datastore:26.2.3.2 (894 passed, 0 failed); the
pristine v2.6.2 build run against that same server passes the identical set,
and both logged the same 3914 queries under their respective client names.
2026-07-26 11:44:39 -07:00

519 lines
20 KiB
C++

#include <datastore/columns/array.h>
#include <datastore/columns/tuple.h>
#include <datastore/columns/date.h>
#include <datastore/columns/time.h>
#include <datastore/columns/enum.h>
#include <datastore/columns/lowcardinality.h>
#include <datastore/columns/nullable.h>
#include <datastore/columns/numeric.h>
#include <datastore/columns/string.h>
#include <datastore/columns/uuid.h>
#include <datastore/columns/ip4.h>
#include <datastore/columns/ip6.h>
#include <datastore/base/input.h>
#include <datastore/base/output.h>
#include <datastore/base/socket.h> // for ipv4-ipv6 platform-specific stuff
#include <datastore/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 datastore;
}
namespace datastore{
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(datastore::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);
datastore::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>, datastore::Int128, &MakeDecimals<precision, scale>>
{
using Base = GenericColumnTestCase<ColumnDecimal, &makeColumn<ColumnDecimal, precision, scale>, datastore::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>,
#if !DS_MAP_BOOL_TO_UINT8
GenericColumnTestCase<ColumnBool, &makeColumn<ColumnBool>, uint8_t, &MakeBools>,
#endif
GenericColumnTestCase<ColumnString, &makeColumn<ColumnString>, std::string, &MakeStrings>,
GenericColumnTestCase<ColumnFixedString, &makeColumn<ColumnFixedString, 12>, std::string, &MakeFixedStrings<12>>,
GenericColumnTestCase<ColumnJSON, &makeColumn<ColumnJSON>, std::string, &MakeJSONs>,
GenericColumnTestCase<ColumnDate, &makeColumn<ColumnDate>, time_t, &MakeDates<time_t>>,
GenericColumnTestCase<ColumnDate32, &makeColumn<ColumnDate32>, time_t, &MakeDates<time_t>>,
GenericColumnTestCase<ColumnDateTime, &makeColumn<ColumnDateTime>, datastore::Int64, &MakeDateTimes>,
GenericColumnTestCase<ColumnDateTime64, &makeColumn<ColumnDateTime64, 0>, datastore::Int64, &MakeDateTime64s<0>>,
GenericColumnTestCase<ColumnDateTime64, &makeColumn<ColumnDateTime64, 3>, datastore::Int64, &MakeDateTime64s<3>>,
GenericColumnTestCase<ColumnDateTime64, &makeColumn<ColumnDateTime64, 6>, datastore::Int64, &MakeDateTime64s<6>>,
GenericColumnTestCase<ColumnDateTime64, &makeColumn<ColumnDateTime64, 9>, datastore::Int64, &MakeDateTime64s<9>>,
GenericColumnTestCase<ColumnTime, &makeColumn<ColumnTime>, int32_t, &MakeTime>,
GenericColumnTestCase<ColumnTime64, &makeColumn<ColumnTime64, 0>, int64_t, &MakeTime64<0>>,
GenericColumnTestCase<ColumnTime64, &makeColumn<ColumnTime64, 3>, int64_t, &MakeTime64<3>>,
GenericColumnTestCase<ColumnTime64, &makeColumn<ColumnTime64, 6>, int64_t, &MakeTime64<6>>,
GenericColumnTestCase<ColumnTime64, &makeColumn<ColumnTime64, 9>, int64_t, &MakeTime64<9>>,
GenericColumnTestCase<ColumnIPv4, &makeColumn<ColumnIPv4>, in_addr, &MakeIPv4s>,
GenericColumnTestCase<ColumnIPv6, &makeColumn<ColumnIPv6>, in6_addr, &MakeIPv6s>,
GenericColumnTestCase<ColumnInt128, &makeColumn<ColumnInt128>, datastore::Int128, &MakeInt128s>,
GenericColumnTestCase<ColumnUInt128, &makeColumn<ColumnUInt128>, datastore::UInt128, &MakeUInt128s>,
GenericColumnTestCase<ColumnUUID, &makeColumn<ColumnUUID>, datastore::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, datastore::UInt128> || std::is_same_v<T, datastore::UUID>
|| std::is_same_v<T, datastore::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("DATASTORE_HOST", "localhost"))
.SetPort( getEnvOrDefault<size_t>("DATASTORE_PORT", "9000"))
.SetUser( getEnvOrDefault("DATASTORE_USER", "default"))
.SetPassword( getEnvOrDefault("DATASTORE_PASSWORD", ""))
.SetDefaultDatabase(getEnvOrDefault("DATASTORE_DB", "default"));
const auto AllCompressionMethods = {
datastore::CompressionMethod::None,
datastore::CompressionMethod::LZ4,
datastore::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);
}