Files
cpp/ut/roundtrip_tests.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

399 lines
14 KiB
C++

#include <datastore/client.h>
#include "utils.h"
#include "roundtrip_column.h"
#include <gtest/gtest.h>
#include <map>
#include <optional>
using namespace datastore;
// Use value-parameterized tests to run same tests with different client
// options.
class RoundtripCase : public testing::TestWithParam<ClientOptions> {
protected:
void SetUp() override {
client_ = std::make_unique<Client>(GetParam());
}
void TearDown() override {
client_.reset();
}
std::string GetSettingValue(const std::string& name) {
std::string result;
client_->Select("SELECT value FROM system.settings WHERE name = \'" + name + "\'",
[&result](const Block& block)
{
if (block.GetRowCount() == 0) {
return;
}
result = block[0]->AsStrict<ColumnString>()->At(0);
}
);
return result;
}
std::unique_ptr<Client> client_;
};
TEST_P(RoundtripCase, ArrayTUint64) {
auto array = std::make_shared<ColumnArrayT<ColumnUInt64>>();
array->Append({0, 1, 2});
auto result = RoundtripColumnValues(*client_, array)->AsStrict<ColumnArray>();
auto row = result->GetAsColumn(0)->As<ColumnUInt64>();
EXPECT_EQ(0u, row->At(0));
EXPECT_EQ(1u, (*row)[1]);
EXPECT_EQ(2u, (*row)[2]);
}
TEST_P(RoundtripCase, ArrayTArrayTUint64) {
const std::vector<std::vector<uint64_t>> row_values = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9, 10}
};
auto array = std::make_shared<ColumnArrayT<ColumnArrayT<ColumnUInt64>>>();
array->Append(row_values);
auto result_typed = ColumnArrayT<ColumnArrayT<ColumnUInt64>>::Wrap(RoundtripColumnValues(*client_, array));
EXPECT_TRUE(CompareRecursive(*array, *result_typed));
}
TEST_P(RoundtripCase, ArrayTArrayTArrayTUint64) {
using ColumnType = ColumnArrayT<ColumnArrayT<ColumnArrayT<ColumnUInt64>>>;
const std::vector<std::vector<std::vector<uint64_t>>> row_values = {
{{1, 2, 3}, {3, 2, 1}},
{{4, 5, 6}, {6, 5, 4}},
{{7, 8, 9, 10}, {}},
{{}, {10, 9, 8, 7}}
};
auto array = std::make_shared<ColumnType>();
array->Append(row_values);
auto result_typed = ColumnType::Wrap(RoundtripColumnValues(*client_, array));
EXPECT_TRUE(CompareRecursive(*array, *result_typed));
}
TEST_P(RoundtripCase, ArrayTFixedString) {
auto array = std::make_shared<ColumnArrayT<ColumnFixedString>>(6);
array->Append({"hello", "world"});
auto result_typed = ColumnArrayT<ColumnFixedString>::Wrap(RoundtripColumnValues(*client_, array));
EXPECT_TRUE(CompareRecursive(*array, *result_typed));
}
TEST_P(RoundtripCase, ArrayTString) {
auto array = std::make_shared<ColumnArrayT<ColumnString>>();
array->Append({"hello", "world"});
auto result_typed = ColumnArrayT<ColumnString>::Wrap(RoundtripColumnValues(*client_, array));
EXPECT_TRUE(CompareRecursive(*array, *result_typed));
}
TEST_P(RoundtripCase, MapTUint64String) {
using Map = ColumnMapT<ColumnUInt64, ColumnString>;
auto map = std::make_shared<Map>(std::make_shared<ColumnUInt64>(), std::make_shared<ColumnString>());
std::map<uint64_t, std::string> row;
row[1] = "hello";
row[2] = "world";
map->Append(row);
auto result_typed = Map::Wrap(RoundtripColumnValues(*client_, map));
EXPECT_TRUE(CompareRecursive(*map, *result_typed));
}
TEST_P(RoundtripCase, MapUUID_Tuple_String_Array_Uint64) {
using Tuple = ColumnTupleT<ColumnString, ColumnArrayT<ColumnUInt64>>;
using Map = ColumnMapT<ColumnUUID, Tuple>;
auto map = std::make_shared<Map>(std::make_shared<ColumnUUID>(), std::make_shared<Tuple>(
std::make_tuple(std::make_shared<ColumnString>(), std::make_shared<ColumnArrayT<ColumnUInt64>>())));
std::map<UUID, std::tuple<std::string, std::vector<uint64_t>>> row;
row[UUID{1, 1}] = std::make_tuple("hello", std::vector<uint64_t>{1, 2, 3}) ;
row[UUID{2, 2}] = std::make_tuple("world", std::vector<uint64_t>{4, 5, 6}) ;
map->Append(row);
auto result_typed = Map::Wrap(RoundtripColumnValues(*client_, map));
EXPECT_TRUE(CompareRecursive(*map, *result_typed));
}
TEST_P(RoundtripCase, Point) {
if (GetSettingValue("allow_experimental_geo_types") != "1") {
GTEST_SKIP() << "Test is skipped because experimental geo types are not allowed. Set setting allow_experimental_geo_types = 1 in order to allow it." << std::endl;
}
auto col = std::make_shared<ColumnPoint>();
col->Append({1.0, 2.0});
col->Append({0.1, 0.2});
auto result_typed = RoundtripColumnValues(*client_, col)->AsStrict<ColumnPoint>();
EXPECT_TRUE(CompareRecursive(*col, *result_typed));
}
TEST_P(RoundtripCase, Ring) {
if (GetSettingValue("allow_experimental_geo_types") != "1") {
GTEST_SKIP() << "Test is skipped because experimental geo types are not allowed. Set setting allow_experimental_geo_types = 1 in order to allow it." << std::endl;
}
auto col = std::make_shared<ColumnRing>();
{
std::vector<ColumnPoint::ValueType> ring{{1.0, 2.0}, {3.0, 4.0}};
col->Append(ring);
}
{
std::vector<ColumnPoint::ValueType> ring{{0.1, 0.2}, {0.3, 0.4}};
col->Append(ring);
}
auto result_typed = RoundtripColumnValues(*client_, col)->AsStrict<ColumnRing>();
EXPECT_TRUE(CompareRecursive(*col, *result_typed));
}
TEST_P(RoundtripCase, Polygon) {
if (GetSettingValue("allow_experimental_geo_types") != "1") {
GTEST_SKIP() << "Test is skipped because experimental geo types are not allowed. Set setting allow_experimental_geo_types = 1 in order to allow it." << std::endl;
}
auto col = std::make_shared<ColumnPolygon>();
{
std::vector<std::vector<ColumnPoint::ValueType>> polygon
{{{1.0, 2.0}, {3.0, 4.0}}, {{5.0, 6.0}, {7.0, 8.0}}};
col->Append(polygon);
}
{
std::vector<std::vector<ColumnPoint::ValueType>> polygon
{{{0.1, 0.2}, {0.3, 0.4}}, {{0.5, 0.6}, {0.7, 0.8}}};
col->Append(polygon);
}
auto result_typed = RoundtripColumnValues(*client_, col)->AsStrict<ColumnPolygon>();
EXPECT_TRUE(CompareRecursive(*col, *result_typed));
}
TEST_P(RoundtripCase, MultiPolygon) {
if (GetSettingValue("allow_experimental_geo_types") != "1") {
GTEST_SKIP() << "Test is skipped because experimental geo types are not allowed. Set setting allow_experimental_geo_types = 1 in order to allow it." << std::endl;
}
auto col = std::make_shared<ColumnMultiPolygon>();
{
std::vector<std::vector<std::vector<ColumnPoint::ValueType>>> multi_polygon
{{{{1.0, 2.0}, {3.0, 4.0}}, {{5.0, 6.0}, {7.0, 8.0}}},
{{{1.1, 2.2}, {3.3, 4.4}}, {{5.5, 6.6}, {7.7, 8.8}}}};
col->Append(multi_polygon);
}
{
std::vector<std::vector<std::vector<ColumnPoint::ValueType>>> multi_polygon
{{{{0.1, 0.2}, {0.3, 0.4}}, {{0.5, 0.6}, {0.7, 0.8}}},
{{{1.1, 1.2}, {1.3, 1.4}}, {{1.5, 1.6}, {1.7, 1.8}}}};
col->Append(multi_polygon);
}
auto result_typed = RoundtripColumnValues(*client_, col)->AsStrict<ColumnMultiPolygon>();
EXPECT_TRUE(CompareRecursive(*col, *result_typed));
}
TEST_P(RoundtripCase, LowCardinalityTString) {
using TestColumn = ColumnLowCardinalityT<ColumnString>;
auto col = std::make_shared<TestColumn>();
col->Append("abc");
col->Append("def");
col->Append("abc");
col->Append("abc");
auto result_typed = RoundtripColumnValues(*client_, col)->As<TestColumn>();
EXPECT_TRUE(CompareRecursive(*col, *result_typed));
}
TEST_P(RoundtripCase, LowCardinalityTNullableString) {
using TestColumn = ColumnLowCardinalityT<ColumnNullableT<ColumnString>>;
auto col = std::make_shared<TestColumn>();
col->Append("abc");
col->Append("def");
col->Append("abc");
col->Append(std::nullopt);
col->Append("abc");
col->Append(std::nullopt);
col->Append(std::nullopt);
col->Append("foobar");
auto result_typed = RoundtripColumnValues(*client_, col)->As<TestColumn>();
EXPECT_TRUE(CompareRecursive(*col, *result_typed));
}
TEST_P(RoundtripCase, ArrayTNullableString) {
using TestColumn = ColumnArrayT<ColumnNullableT<ColumnString>>;
auto col = std::make_shared<TestColumn>();
col->Append({std::nullopt, std::nullopt, std::nullopt});
col->Append(std::vector<std::optional<std::string>>{"abc", std::nullopt});
auto result_typed = RoundtripColumnValues(*client_, col)->As<TestColumn>();
EXPECT_TRUE(CompareRecursive(*col, *result_typed));
}
TEST_P(RoundtripCase, TupleTNullableString) {
using TestColumn = ColumnTupleT<ColumnNullableT<ColumnString>>;
auto col = std::make_shared<TestColumn>(std::make_tuple(std::make_shared<ColumnNullableT<ColumnString>>()));
col->Append(std::make_tuple(std::nullopt));
col->Append(std::make_tuple("abc"));
auto result_typed = RoundtripColumnValues(*client_, col)->As<TestColumn>();
EXPECT_TRUE(CompareRecursive(*col, *result_typed));
}
TEST_P(RoundtripCase, TupleWithQuotedFieldNames) {
auto col_a = std::make_shared<ColumnInt8>(std::vector<int8_t>{1});
auto col_b = std::make_shared<ColumnInt16>(std::vector<int16_t>{2});
auto col_c = std::make_shared<ColumnInt32>(std::vector<int32_t>{3});
auto col = std::make_shared<ColumnTuple>(
std::vector<ColumnRef>({col_a, col_b, col_c}),
std::vector<std::string>{"a.a", "b`b", "c``c"}
);
auto result = RoundtripColumnValues(*client_, col)->AsStrict<ColumnTuple>();
EXPECT_TRUE(CompareRecursive(*col->At(0), *result->At(0)));
EXPECT_TRUE(CompareRecursive(*col->At(1), *result->At(1)));
EXPECT_TRUE(CompareRecursive(*col->At(2), *result->At(2)));
const auto& names = result->Type()->As<TupleType>()->GetItemNames();
ASSERT_EQ(names.size(), 3u);
EXPECT_EQ(names[0], "a.a");
EXPECT_EQ(names[1], "b`b");
EXPECT_EQ(names[2], "c``c");
}
TEST_P(RoundtripCase, SelectTupleByFieldNames) {
auto col_a = std::make_shared<ColumnInt8>(std::vector<int8_t>{1});
auto col_b = std::make_shared<ColumnInt16>(std::vector<int16_t>{2});
auto col_c = std::make_shared<ColumnInt32>(std::vector<int32_t>{3});
auto col = std::make_shared<ColumnTuple>(
std::vector<ColumnRef>({col_a, col_b, col_c}),
std::vector<std::string>{"a.a", "b`b", "c``c"}
);
// skip result, we will do it manually with a separate SELECT statement
RoundtripColumnValues(*client_, col)->AsStrict<ColumnTuple>();
// NOTE: Each backtick must be escaped with either "\\" (double "\\" so the compiler
// turns it into "\") or a double backtick, "``". When we create or receive the columns,
// this escaping is done automatically by the type parser, but when we write queries
// ourselves, the escaping has to be done manually.
client_->BeginSelect(
"SELECT "
" col.`a.a`, "
" col.`b``b`, col.`b\\`b`, "
" col.`c````c`, col.`c\\`\\`c` "
"FROM temporary_roundtrip_table "
"ORDER BY id");
Block last_block;
while (auto tmp = client_->NextBlock()) {
if (tmp->GetRowCount() > 0) {
last_block = *tmp;
}
}
EXPECT_TRUE(CompareRecursive(*col->At(0), *last_block.At(0)));
EXPECT_TRUE(CompareRecursive(*col->At(1), *last_block.At(1)));
EXPECT_TRUE(CompareRecursive(*col->At(1), *last_block.At(2)));
EXPECT_TRUE(CompareRecursive(*col->At(2), *last_block.At(3)));
EXPECT_TRUE(CompareRecursive(*col->At(2), *last_block.At(4)));
}
TEST_P(RoundtripCase, Map_TString_TNullableString) {
using Key = ColumnString;
using Value = ColumnNullableT<ColumnString>;
using TestColumn = ColumnMapT<Key, Value>;
auto col = std::make_shared<TestColumn>(std::make_shared<Key>(), std::make_shared<Value>());
{
std::map<std::string, std::optional<std::string>> value;
value["1"] = "one";
value["2"] = std::nullopt;
col->Append(value);
}
{
std::map<std::string, std::optional<std::string>> value;
value["4"] = "one";
value["2"] = std::nullopt;
col->Append(value);
}
auto result_typed = RoundtripColumnValues(*client_, col)->As<TestColumn>();
EXPECT_TRUE(CompareRecursive(*col, *result_typed));
}
TEST_P(RoundtripCase, Map_LowCardinalityTString_LowCardinalityTNullableString) {
using Key = ColumnLowCardinalityT<ColumnString>;
using Value = ColumnLowCardinalityT<ColumnNullableT<ColumnString>>;
using TestColumn = ColumnMapT<Key, Value>;
auto col = std::make_shared<TestColumn>(std::make_shared<Key>(), std::make_shared<Value>());
{
std::map<std::string, std::optional<std::string>> value;
value["1"] = "one";
value["2"] = std::nullopt;
col->Append(value);
}
{
std::map<std::string, std::optional<std::string>> value;
value["4"] = "one";
value["2"] = std::nullopt;
col->Append(value);
}
auto result_typed = RoundtripColumnValues(*client_, col)->As<TestColumn>();
EXPECT_TRUE(CompareRecursive(*col, *result_typed));
}
TEST_P(RoundtripCase, RoundtripArrayLowCardinalityTString) {
using TestColumn = ColumnArrayT<ColumnLowCardinalityT<ColumnString>>;
auto array = std::make_shared<TestColumn>();
array->Append(std::vector<std::string>{});
array->Append(std::vector<std::string>{});
auto result_typed = RoundtripColumnValues(*client_, array)->As<TestColumn>();
EXPECT_TRUE(CompareRecursive(*array, *result_typed));
}
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"));
INSTANTIATE_TEST_SUITE_P(
Roundtrip, RoundtripCase,
::testing::Values(
ClientOptions(LocalHostEndpoint)
.SetPingBeforeQuery(true)
.SetBakcwardCompatibilityFeatureLowCardinalityAsWrappedColumn(false),
ClientOptions(LocalHostEndpoint)
.SetPingBeforeQuery(false)
.SetCompressionMethod(CompressionMethod::LZ4)
.SetBakcwardCompatibilityFeatureLowCardinalityAsWrappedColumn(false),
ClientOptions(LocalHostEndpoint)
.SetPingBeforeQuery(false)
.SetCompressionMethod(CompressionMethod::ZSTD)
.SetBakcwardCompatibilityFeatureLowCardinalityAsWrappedColumn(false)
));