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

11 KiB

Datastore C++ client Linux macOS Windows MSVC Windows mingw

C++ client for Hanzo Datastore.

Supported data types

  • Array(T)
  • Bool * (by default, mapped to UInt8 when receiving data)
  • Date
  • DateTime, DateTime64
  • DateTime([timezone]), DateTime64(N, [timezone])
  • Decimal32, Decimal64, Decimal128
  • Enum8, Enum16
  • FixedString(N)
  • Float32, Float64
  • IPv4, IPv6
  • Nullable(T)
  • String
  • LowCardinality(String) or LowCardinality(FixedString(N))
  • Tuple
  • UInt8, UInt16, UInt32, UInt64, Int8, Int16, Int32, Int64
  • UInt128, Int128
  • UUID
  • Map
  • Point, Ring, Polygon, MultiPolygon
  • JSON - experimental support; requires output_format_native_write_json_as_string=1; data is passed as strings

*: There exists a distinct ColumnBool and entry in the Type enumeration that can be used. By default, data received from the server will map Bool columns to UInt8. This is a backwards compatibility feature. If you want the library to produce ColumnBool, set the CMake variable DS_MAP_BOOL_TO_UINT8=OFF. The default for this variable will switch, with it being subsequently removed, in a future release.

Dependencies

In the most basic case one needs only:

  • a C++-17-complaint compiler,
  • cmake (3.12 or newer), and
  • ninja

Optional dependencies:

  • openssl
  • liblz4
  • libabsl
  • libzstd

Building

$ mkdir build .
$ cd build
$ cmake .. [-DBUILD_TESTS=ON]
$ make

Please refer to the workflows for the reference on dependencies/build options

Example application build with datastore-cpp

There are various ways to integrate datastore-cpp with the build system of an application. Below example uses the simple approach based on submodules presented in https://www.youtube.com/watch?v=ED-WUk440qc .

  • mkdir datastore-app && cd datastore-app && git init
  • git submodule add https://github.com/hanzo-ds/cpp.git contribs/datastore-cpp
  • touch app.cpp, then copy the following C++ code into that file
#include <iostream>
#include <datastore/client.h>

using namespace datastore;

int main()
{
    /// Initialize client connection.
    Client client(ClientOptions().SetHost("localhost"));

    /// Create a table.
    client.Execute("CREATE TABLE IF NOT EXISTS default.numbers (id UInt64, name String) ENGINE = Memory");

    /// Insert some values.
    {
        Block block;

        auto id = std::make_shared<ColumnUInt64>();
        id->Append(1);
        id->Append(7);

        auto name = std::make_shared<ColumnString>();
        name->Append("one");
        name->Append("seven");

        block.AppendColumn("id"  , id);
        block.AppendColumn("name", name);

        client.Insert("default.numbers", block);
    }

    /// Select values inserted in the previous step.
    client.Select("SELECT id, name FROM default.numbers", [] (const Block& block)
        {
            for (size_t i = 0; i < block.GetRowCount(); ++i) {
                std::cout << block[0]->As<ColumnUInt64>()->At(i) << " "
                          << block[1]->As<ColumnString>()->At(i) << "\n";
            }
        }
    );

    /// Select values inserted in the previous step using external data feature
    /// See https://docs.hanzo.ai/datastore/engines/table-engines/special/external-data
    {
        Block block1, block2;
        auto id = std::make_shared<ColumnUInt64>();
        id->Append(1);
        block1.AppendColumn("id"  , id);

        auto name = std::make_shared<ColumnString>();
        name->Append("seven");
        block2.AppendColumn("name", name);

        const std::string _1 = "_1";
        const std::string _2 = "_2";

        const ExternalTables external = {{_1, block1}, {_2, block2}};
        client.SelectWithExternalData("SELECT id, name FROM default.numbers where id in (_1) or name in (_2)",
                                      external, [] (const Block& block)
            {
                for (size_t i = 0; i < block.GetRowCount(); ++i) {
                    std::cout << block[0]->As<ColumnUInt64>()->At(i) << " "
                              << block[1]->As<ColumnString>()->At(i) << "\n";
                }
            }
        );
    }

    /// Delete table.
    client.Execute("DROP TABLE default.numbers");

    return 0;
}
  • touch CMakeLists.txt, then copy the following CMake code into that file
cmake_minimum_required(VERSION 3.12)
project(application-example)

set(CMAKE_CXX_STANDARD 17)

add_subdirectory(contribs/datastore-cpp)

add_executable(${PROJECT_NAME} "app.cpp")

target_include_directories(${PROJECT_NAME} PRIVATE contribs/datastore-cpp/ contribs/datastore-cpp/contrib/absl)

target_link_libraries(${PROJECT_NAME} PRIVATE datastore-cpp-lib)
  • run rm -rf build && cmake -B build -S . && cmake --build build -j32 to remove remainders of the previous builds, run CMake and build the application. The generated binary is located in location build/application-example.

Experimental Bazel support

If you use Bazel, you can also use it to import the library into your project. However, please keep in mind that Bazel support is currently experimental and is still being refined. This means things might change as we improve and update the setup.

Most importantly, the project includes settings that were added for compatibility with older versions of the library and its API, and to preserve old behavior. These settings will not be part of the Bazel configuration. This means that even when only the minor version changes your build still might break.

Additionally, by default, the library is built with BoringSSL and not OpenSSL because it builds reliably across platforms on BCR today and matches what many Bazel workspaces, such as gRPC and Envoy, already link against.

It is still possible to build the library with OpenSSL by using the tls=openssl option. TLS support can be completely omitted with the tls=no option.

Batch Insertion

In addition to the Insert method, which inserts all the data in a block in a single call, you can use the BeginInsert / SendInsertBlock / EndInsert pattern to insert batches of data. This can be useful for managing larger data sets without inflating memory with the entire set.

To use it pass BeginInsert an INSERT statement ending in VALUES but with no actual values. Use the resulting Block to append batches of data, sending each to the sever with SendInsertBlock. Finally, call EndInsert (or let the client go out of scope) to signal the server that insertion is complete. Example:

// Start the insertion.
auto block = client->BeginInsert("INSERT INTO foo (id, name) VALUES");

// Grab the columns from the block.
auto col1 = block[0]->As<ColumnUInt64>();
auto col2 = block[1]->As<ColumnString>();

// Add a couple of records to the block.
col1.Append(1);
col1.Append(2);
col2.Append("holden");
col2.Append("naomi");

// Send those records.
block.RefreshRowCount();
client->SendInsertBlock(block);
block.Clear();

// Add another record.
col1.Append(3);
col2.Append("amos");

// Send it and finish.
block.RefreshRowCount();
client->EndInsert();

Thread-safety

⚠ Please note that Client instance is NOT thread-safe. I.e. you must create a separate Client for each thread or utilize some synchronization techniques. ⚠

Retries

If you wish to implement some retry logic atop of datastore::Client there are few simple rules to make you life easier:

  • If previous attempt threw an exception, then make sure to call datastore::Client::ResetConnection() before the next try.
  • For datastore::Client::Insert() you can reuse a block from previous try, no need to rebuild it from scratch.

See https://github.com/hanzo-ds/cpp/issues/184 for details.

Asynchronous inserts

See https://docs.hanzo.ai/datastore/best-practices/asynchronous-inserts for details.

⚠ The asynchronous setting is different according to the datastore server version. The under example with datastore server version 24.8.4.13. ⚠

Our strong recommendation is to use async_insert=1,wait_for_async_insert=1 if using asynchronous inserts. Using wait_for_async_insert=0 is very risky because your INSERT client may not be aware if there are errors, and also can cause potential overload if your client continues to write quickly in a situation where the Datastore server needs to slow down the writes and create some backpressure in order to ensure reliability of the service.

  • Only use the SDK, do not need to change the datastore server config. Asynchronous inserts only work if the data is sent as SQL text format. Here is the example.
// You can specify the asynchronous insert settings by using the SETTINGS clause of insert queries
datastore::Query query("INSERT INTO default.test SETTINGS async_insert=1,wait_for_async_insert=1,async_insert_busy_timeout_ms=5000,async_insert_use_adaptive_busy_timeout=0,async_insert_max_data_size=104857600 VALUES(10,10)");
client.Execute(query);

// Or by SetSetting
datastore::Query query("INSERT INTO default.test VALUES(10,10)");
query.SetSetting("async_insert", datastore::QuerySettingsField{ "1", 1 });
query.SetSetting("wait_for_async_insert", datastore::QuerySettingsField{ "1", 1 }); // strong recommendation
query.SetSetting("async_insert_busy_timeout_ms", datastore::QuerySettingsField{ "5000", 1 });
query.SetSetting("async_insert_max_data_size", datastore::QuerySettingsField{ "104857600", 1 });
query.SetSetting("async_insert_use_adaptive_busy_timeout", datastore::QuerySettingsField{ "0", 1 });
client.Execute(query);

// Not available case. The Insert interface actually use the native data format
datastore::Block block;
client.Insert("default.test", block);
  • Change the datastore server users.xml, enable asynchronous inserts (available for the native data format). Here is the example.
<profiles>
        <!-- Default settings. -->
        <default>
            <async_insert>1</async_insert>
            <wait_for_async_insert>1</wait_for_async_insert>
            <async_insert_use_adaptive_busy_timeout>0</async_insert_use_adaptive_busy_timeout>
            <async_insert_busy_timeout_ms>5000</async_insert_busy_timeout_ms>
            <async_insert_max_data_size>104857600</async_insert_max_data_size>
        </default>

        <!-- Profile that allows only read queries. -->
        <readonly>
            <readonly>1</readonly>
        </readonly>
    </profiles>
  • Enabling asynchronous inserts at the user level. Ensure your login account has the privileges about ALTER USER. Then you can use insert_account for asynchronous inserts.
ALTER USER insert_account SETTINGS async_insert=1,wait_for_async_insert=1,async_insert_use_adaptive_busy_timeout=0,async_insert_busy_timeout_ms=5000,async_insert_max_data_size=104857600