Files
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

259 lines
8.5 KiB
C++

#include "compressed.h"
#include "wire_format.h"
#include "output.h"
#include "datastore/exceptions.h"
#include <city.h>
#include <lz4.h>
#include <exception>
#include <zstd.h>
#include <stdexcept>
#include <system_error>
namespace {
constexpr size_t HEADER_SIZE = 9;
// see DB::CompressionMethodByte from src/Compression/CompressionInfo.h of Datastore project
enum class CompressionMethodByte : uint8_t {
NONE = 0x02,
LZ4 = 0x82,
ZSTD = 0x90,
};
// Documentation says that compression is faster when output buffer is larger than LZ4_compressBound/ZSTD_compressBound estimation.
constexpr size_t EXTRA_COMPRESS_BUFFER_SIZE = 4096;
constexpr size_t DBMS_MAX_COMPRESSED_SIZE = 0x40000000ULL; // 1GB
}
namespace datastore {
CompressedInput::CompressedInput(InputStream* input)
: input_(input)
{
}
CompressedInput::~CompressedInput() {
if (!mem_.Exhausted()) {
#if __cplusplus < 201703L
if (!std::uncaught_exception()) {
#else
if (!std::uncaught_exceptions()) {
#endif
throw CompressionError("some data was not read");
}
}
}
size_t CompressedInput::DoNext(const void** ptr, size_t len) {
if (mem_.Exhausted()) {
if (!Decompress()) {
return 0;
}
}
return mem_.Next(ptr, len);
}
bool CompressedInput::Decompress() {
uint128 hash;
uint32_t compressed = 0;
uint32_t original = 0;
uint8_t method = 0;
if (!WireFormat::ReadFixed(*input_, &hash)) {
return false;
}
if (!WireFormat::ReadFixed(*input_, &method)) {
return false;
}
if (method != static_cast<uint8_t>(CompressionMethodByte::LZ4) && method != static_cast<uint8_t>(CompressionMethodByte::ZSTD)) {
throw CompressionError("unsupported compression method " + std::to_string((method)));
}
if (!WireFormat::ReadFixed(*input_, &compressed)) {
return false;
}
if (!WireFormat::ReadFixed(*input_, &original)) {
return false;
}
if (compressed > DBMS_MAX_COMPRESSED_SIZE) {
throw CompressionError("compressed data too big");
}
Buffer tmp(compressed);
// Data header
{
BufferOutput out(&tmp);
out.Write(&method, sizeof(method));
out.Write(&compressed, sizeof(compressed));
out.Write(&original, sizeof(original));
out.Flush();
}
if (!WireFormat::ReadBytes(*input_, tmp.data() + HEADER_SIZE, compressed - HEADER_SIZE)) {
return false;
} else {
if (hash != CityHash128((const char*)tmp.data(), compressed)) {
throw CompressionError("data was corrupted");
}
}
data_ = Buffer(original);
switch (method) {
case static_cast<uint8_t>(CompressionMethodByte::LZ4): {
if (LZ4_decompress_safe((const char*)tmp.data() + HEADER_SIZE, (char*)data_.data(), static_cast<int>(compressed - HEADER_SIZE), original) < 0) {
throw CompressionError("can't decompress LZ4-encoded data");
} else {
mem_.Reset(data_.data(), original);
}
return true;
}
case static_cast<uint8_t>(CompressionMethodByte::ZSTD): {
size_t res = ZSTD_decompress((char*)data_.data(), original, (const char*)tmp.data() + HEADER_SIZE, static_cast<int>(compressed - HEADER_SIZE));
if (ZSTD_isError(res)) {
throw CompressionError("can't decompress ZSTD-encoded data, ZSTD error: " + std::string(ZSTD_getErrorName(res)));
} else {
mem_.Reset(data_.data(), original);
}
return true;
}
case static_cast<uint8_t>(CompressionMethodByte::NONE): {
throw CompressionError("compression method not defined" + std::to_string((method)));
}
default: {
throw CompressionError("Unknown or unsupported compression method " + std::to_string((method)));
}
}
return true;
}
CompressedOutput::CompressedOutput(OutputStream * destination, size_t max_compressed_chunk_size, CompressionMethod method)
: destination_(destination)
, max_compressed_chunk_size_(max_compressed_chunk_size)
, method_(method)
{
PreallocateCompressBuffer(max_compressed_chunk_size);
}
CompressedOutput::~CompressedOutput() { }
size_t CompressedOutput::DoWrite(const void* data, size_t len) {
const size_t original_len = len;
// what if len > max_compressed_chunk_size_ ?
const size_t max_chunk_size = max_compressed_chunk_size_ > 0 ? max_compressed_chunk_size_ : len;
if (max_chunk_size > max_compressed_chunk_size_) {
PreallocateCompressBuffer(len);
}
while (len > 0) {
auto to_compress = std::min(len, max_chunk_size);
Compress(data, to_compress);
len -= to_compress;
data = reinterpret_cast<const char*>(data) + to_compress;
}
return original_len - len;
}
void CompressedOutput::DoFlush() {
destination_->Flush();
}
void CompressedOutput::Compress(const void * data, size_t len) {
switch (method_) {
case datastore::CompressionMethod::LZ4: {
const auto compressed_size = LZ4_compress_default(
(const char*)data,
(char*)compressed_buffer_.data() + HEADER_SIZE,
static_cast<int>(len),
static_cast<int>(compressed_buffer_.size() - HEADER_SIZE));
if (compressed_size <= 0)
throw CompressionError("Failed to compress chunk of " + std::to_string(len) + " bytes, "
"LZ4 error: " + std::to_string(compressed_size));
{
auto header = compressed_buffer_.data();
WriteUnaligned(header, CompressionMethodByte::LZ4);
// Compressed data size with header
WriteUnaligned(header + 1, static_cast<uint32_t>(compressed_size + HEADER_SIZE));
// Original data size
WriteUnaligned(header + 5, static_cast<uint32_t>(len));
}
WireFormat::WriteFixed(*destination_, CityHash128((const char*)compressed_buffer_.data(), compressed_size + HEADER_SIZE));
WireFormat::WriteBytes(*destination_, compressed_buffer_.data(), compressed_size + HEADER_SIZE);
break;
}
case datastore::CompressionMethod::ZSTD: {
const size_t compressed_size = ZSTD_compress(
(char*)compressed_buffer_.data() + HEADER_SIZE,
static_cast<int>(compressed_buffer_.size() - HEADER_SIZE),
(const char*)data,
static_cast<int>(len),
ZSTD_fast);
if (ZSTD_isError(compressed_size))
throw CompressionError("Failed to compress chunk of " + std::to_string(len) + " bytes, "
"ZSTD error: " + std::string(ZSTD_getErrorName(compressed_size)));
{
auto header = compressed_buffer_.data();
WriteUnaligned(header, CompressionMethodByte::ZSTD);
// Compressed data size with header
WriteUnaligned(header + 1, static_cast<uint32_t>(compressed_size + HEADER_SIZE));
// Original data size
WriteUnaligned(header + 5, static_cast<uint32_t>(len));
}
WireFormat::WriteFixed(*destination_, CityHash128((const char*)compressed_buffer_.data(), compressed_size + HEADER_SIZE));
WireFormat::WriteBytes(*destination_, compressed_buffer_.data(), compressed_size + HEADER_SIZE);
break;
}
case datastore::CompressionMethod::None: {
throw CompressionError("no compression defined");
}
}
destination_->Flush();
}
void CompressedOutput::PreallocateCompressBuffer(size_t input_size) {
switch (method_) {
case datastore::CompressionMethod::LZ4: {
const auto estimated_compressed_buffer_size = LZ4_compressBound(static_cast<int>(input_size));
if (estimated_compressed_buffer_size <= 0)
throw CompressionError("Failed to estimate compressed buffer size, LZ4 error: " + std::to_string(estimated_compressed_buffer_size));
compressed_buffer_.resize(estimated_compressed_buffer_size + HEADER_SIZE + EXTRA_COMPRESS_BUFFER_SIZE);
break;
}
case datastore::CompressionMethod::ZSTD: {
const size_t estimated_compressed_buffer_size = ZSTD_compressBound(static_cast<int>(input_size));
if (ZSTD_isError(estimated_compressed_buffer_size))
throw CompressionError("Failed to estimate compressed buffer size, ZSTD error: " + std::string(ZSTD_getErrorName(estimated_compressed_buffer_size)));
compressed_buffer_.resize(estimated_compressed_buffer_size + HEADER_SIZE + EXTRA_COMPRESS_BUFFER_SIZE);
break;
}
case datastore::CompressionMethod::None: {
/// do nothing
break;
}
}
}
}