diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index dd58b12d..5cac74fd 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -112,6 +112,7 @@ jobs: -D CMAKE_C_COMPILER=${{matrix.C_COMPILER}} \ -D CMAKE_CXX_COMPILER=${{matrix.CXX_COMPILER}} \ -D CMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} \ + -D CH_MAP_BOOL_TO_UINT8=OFF \ -D BUILD_TESTS=ON \ ${{matrix.SSL_CMAKE_OPTION}} \ ${{matrix.DEPENDENCIES_CMAKE_OPTIONS}} \ diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index e70d75c0..4abc6a38 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -55,6 +55,7 @@ jobs: run: | cmake \ -D CMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} \ + -D CH_MAP_BOOL_TO_UINT8=OFF \ -D BUILD_TESTS=ON \ ${{matrix.SSL_CMAKE_OPTION}} \ -S ${{github.workspace}} \ diff --git a/.github/workflows/windows_mingw.yml b/.github/workflows/windows_mingw.yml index c5a50961..4b620ec2 100644 --- a/.github/workflows/windows_mingw.yml +++ b/.github/workflows/windows_mingw.yml @@ -73,7 +73,7 @@ jobs: tar - name: Configure CMake - run: cmake -B build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DBUILD_TESTS=ON -DCHECK_VERSION=OFF + run: cmake -B build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCH_MAP_BOOL_TO_UINT8=OFF -DBUILD_TESTS=ON -DCHECK_VERSION=OFF # -DWITH_OPENSSL=ON was not able to make it work (some strange issues with CA paths, need debug) # -DCHECK_VERSION=OFF since it requires git, which can't be found from within cmake for some reason. diff --git a/.github/workflows/windows_msvc.yml b/.github/workflows/windows_msvc.yml index 31992921..6c8d4207 100644 --- a/.github/workflows/windows_msvc.yml +++ b/.github/workflows/windows_msvc.yml @@ -47,7 +47,7 @@ jobs: - uses: ilammy/msvc-dev-cmd@v1 - name: Configure CMake - run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DBUILD_TESTS=ON + run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCH_MAP_BOOL_TO_UINT8=OFF -DBUILD_TESTS=ON - name: Build run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} diff --git a/CMakeLists.txt b/CMakeLists.txt index 2eab3e7f..dfa7ccba 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,6 +21,7 @@ OPTION (WITH_SYSTEM_ZSTD "Use system ZSTD" OFF) OPTION (DEBUG_DEPENDENCIES "Print debug info about dependencies duting build" ON) OPTION (CHECK_VERSION "Check that version number corresponds to git tag, usefull in CI/CD to validate that new version published on GitHub has same version in sources" OFF) OPTION (DISABLE_CLANG_LIBC_WORKAROUND "Disable linking compiler-rt & gcc_s if using clang & libstdc++" OFF) +OPTION (CH_MAP_BOOL_TO_UINT8 "Map ClickHouse Bool type to UInt8 instead of exposing a distinct Bool API." ON) PROJECT (CLICKHOUSE-CLIENT VERSION "${CLICKHOUSE_CPP_VERSION}" diff --git a/README.md b/README.md index 43282e54..b83453d5 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ C++ client for [ClickHouse](https://clickhouse.com/). ## Supported data types * Array(T) +* Bool (mapped to UInt8 by default; use `-DCH_MAP_BOOL_TO_UINT8=OFF` for a distinct `clickhouse::Bool`/`ColumnBool` API) * Date * DateTime, DateTime64 * DateTime([timezone]), DateTime64(N, [timezone]) @@ -26,6 +27,10 @@ C++ client for [ClickHouse](https://clickhouse.com/). * JSON - experimental support; requires output_format_native_write_json_as_string=1; data is passed as strings +The distinct `Bool` type will become the default in some future version. The +current mapping to `UInt8` is provided only for compatibility and controlled via +`DCH_MAP_BOOL_TO_UINT8`. + ## Dependencies In the most basic case one needs only: - a C++-17-complaint compiler, @@ -258,5 +263,3 @@ client.Insert("default.test", block); ```sql 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 ``` - - diff --git a/clickhouse/CMakeLists.txt b/clickhouse/CMakeLists.txt index c759dfcb..7b68fb6e 100644 --- a/clickhouse/CMakeLists.txt +++ b/clickhouse/CMakeLists.txt @@ -8,6 +8,7 @@ SET ( clickhouse-cpp-lib-src base/endpoints_iterator.cpp columns/array.cpp + columns/bool.cpp columns/column.cpp columns/date.cpp columns/decimal.cpp @@ -53,6 +54,7 @@ SET ( clickhouse-cpp-lib-src base/wire_format.h columns/array.h + columns/bool.h columns/column.h columns/date.h columns/decimal.h @@ -122,6 +124,11 @@ TARGET_LINK_LIBRARIES (clickhouse-cpp-lib TARGET_INCLUDE_DIRECTORIES (clickhouse-cpp-lib PUBLIC ${PROJECT_SOURCE_DIR} ) +IF (CH_MAP_BOOL_TO_UINT8) + TARGET_COMPILE_DEFINITIONS (clickhouse-cpp-lib PUBLIC CH_MAP_BOOL_TO_UINT8=1) +ELSE () + TARGET_COMPILE_DEFINITIONS (clickhouse-cpp-lib PUBLIC CH_MAP_BOOL_TO_UINT8=0) +ENDIF () IF (NOT BUILD_SHARED_LIBS) ADD_LIBRARY (clickhouse-cpp-lib-static ALIAS clickhouse-cpp-lib) diff --git a/clickhouse/columns/bool.cpp b/clickhouse/columns/bool.cpp new file mode 100644 index 00000000..a524fd99 --- /dev/null +++ b/clickhouse/columns/bool.cpp @@ -0,0 +1,79 @@ +#include "bool.h" + +#include "../types/types.h" + +#if !CH_MAP_BOOL_TO_UINT8 + +namespace clickhouse { + +ColumnBool::ColumnBool() + : Column(Type::CreateSimple()) + , data_() +{ +} + +ColumnBool::ColumnBool(std::vector data) + : Column(Type::CreateSimple()) + , data_(std::move(data)) +{ +} + +void ColumnBool::Reserve(size_t new_cap) { + data_.Reserve(new_cap); +} + +void ColumnBool::Append(bool value) { + data_.Append(static_cast(value)); +} + +bool ColumnBool::At(size_t n) const { + return static_cast(data_.At(n)); +} + +void ColumnBool::Append(ColumnRef column) { + if (auto col = column->As()) { + auto& src = col->data_.GetWritableData(); + data_.GetWritableData().insert(data_.GetWritableData().end(), src.begin(), src.end()); + } else if (auto col = column->As()) { + auto& src = col->GetWritableData(); + data_.GetWritableData().insert(data_.GetWritableData().end(), src.begin(), src.end()); + } +} + +bool ColumnBool::LoadBody(InputStream* input, size_t rows) { + return data_.LoadBody(input, rows); +} + +void ColumnBool::SaveBody(OutputStream* output) { + data_.SaveBody(output); +} + +void ColumnBool::Clear() { + data_.Clear(); +} + +size_t ColumnBool::Size() const { + return data_.Size(); +} + +ColumnRef ColumnBool::Slice(size_t begin, size_t len) const { + auto sliced = std::static_pointer_cast(data_.Slice(begin, len)); + return std::make_shared(std::move(sliced->GetWritableData())); +} + +ColumnRef ColumnBool::CloneEmpty() const { + return std::make_shared(); +} + +void ColumnBool::Swap(Column& other) { + auto& col = dynamic_cast(other); + data_.Swap(col.data_); +} + +ItemView ColumnBool::GetItem(size_t index) const { + return ItemView{Type::Bool, data_.At(index)}; +} + +} // namespace clickhouse + +#endif // !CH_MAP_BOOL_TO_UINT8 diff --git a/clickhouse/columns/bool.h b/clickhouse/columns/bool.h new file mode 100644 index 00000000..fcb0e63a --- /dev/null +++ b/clickhouse/columns/bool.h @@ -0,0 +1,63 @@ +#pragma once + +#include "column.h" +#include "numeric.h" + +#include "../types/types.h" + +#if !CH_MAP_BOOL_TO_UINT8 + +#include + +namespace clickhouse { + +class ColumnBool : public Column { +public: + using ValueType = bool; + + ColumnBool(); + explicit ColumnBool(std::vector data); + + /// Increase the capacity of the column for large block insertion. + void Reserve(size_t new_cap) override; + + /// Appends one element to the end of column. + void Append(bool value); + + /// Returns element at given row number. + bool At(size_t n) const; + + /// Returns element at given row number. + bool operator[](size_t n) const { return At(n); } + +public: + /// Appends content of given column to the end of current one. + /// Accepts ColumnBool or ColumnUInt8. + void Append(ColumnRef column) override; + + /// Loads column data from input stream. + bool LoadBody(InputStream* input, size_t rows) override; + + /// Saves column data to output stream. + void SaveBody(OutputStream* output) override; + + /// Clear column data. + void Clear() override; + + /// Returns count of rows in the column. + size_t Size() const override; + + /// Makes slice of the current column. + ColumnRef Slice(size_t begin, size_t len) const override; + ColumnRef CloneEmpty() const override; + void Swap(Column& other) override; + + ItemView GetItem(size_t index) const override; + +private: + ColumnUInt8 data_; +}; + +} // namespace clickhouse + +#endif // !CH_MAP_BOOL_TO_UINT8 diff --git a/clickhouse/columns/factory.cpp b/clickhouse/columns/factory.cpp index 399551ee..364eacfe 100644 --- a/clickhouse/columns/factory.cpp +++ b/clickhouse/columns/factory.cpp @@ -13,6 +13,7 @@ #include "map.h" #include "nothing.h" #include "nullable.h" +#include "bool.h" #include "numeric.h" #include "string.h" #include "./time.h" // `./` avoids possible conflicts with standard C time.h @@ -50,6 +51,10 @@ static ColumnRef CreateTerminalColumn(const TypeAst& ast) { case Type::Void: return std::make_shared(); +#if !CH_MAP_BOOL_TO_UINT8 + case Type::Bool: + return std::make_shared(); +#endif case Type::UInt8: return std::make_shared(); case Type::UInt16: diff --git a/clickhouse/columns/itemview.cpp b/clickhouse/columns/itemview.cpp index 610db030..1ea86ba5 100644 --- a/clickhouse/columns/itemview.cpp +++ b/clickhouse/columns/itemview.cpp @@ -44,6 +44,9 @@ void ItemView::ValidateData(Type::Code type, DataType data) { case Type::Code::Int8: case Type::Code::UInt8: case Type::Code::Enum8: +#if !CH_MAP_BOOL_TO_UINT8 + case Type::Code::Bool: +#endif return AssertSize({1}); case Type::Code::Int16: diff --git a/clickhouse/columns/itemview.h b/clickhouse/columns/itemview.h index 199994b6..0fd7428d 100644 --- a/clickhouse/columns/itemview.h +++ b/clickhouse/columns/itemview.h @@ -28,7 +28,9 @@ struct ItemView { inline auto ConvertToStorageValue(const T& t) { if constexpr (std::is_same_v || std::is_same_v) { return std::string_view{t}; - } else if constexpr (std::is_fundamental_v || std::is_same_v> || std::is_same_v>) { + } else if constexpr (std::is_fundamental_v + || std::is_same_v> + || std::is_same_v>) { return std::string_view{reinterpret_cast(&t), sizeof(T)}; } else { static_assert(!std::is_same_v, "Unknown type, which can't be stored in ItemView"); @@ -65,7 +67,9 @@ struct ItemView { using ValueType = std::remove_cv_t>; if constexpr (std::is_same_v || std::is_same_v) { return data; - } else if constexpr (std::is_fundamental_v || std::is_same_v || std::is_same_v) { + } else if constexpr (std::is_fundamental_v + || std::is_same_v + || std::is_same_v) { if (sizeof(ValueType) == data.size()) { return *reinterpret_cast(data.data()); } else { diff --git a/clickhouse/types/type_parser.cpp b/clickhouse/types/type_parser.cpp index 3c60c03e..82492412 100644 --- a/clickhouse/types/type_parser.cpp +++ b/clickhouse/types/type_parser.cpp @@ -34,7 +34,11 @@ static const std::unordered_map kTypeCode = { { "Int16", Type::Int16 }, { "Int32", Type::Int32 }, { "Int64", Type::Int64 }, +#if CH_MAP_BOOL_TO_UINT8 { "Bool", Type::UInt8 }, +#else + { "Bool", Type::Bool }, +#endif { "UInt8", Type::UInt8 }, { "UInt16", Type::UInt16 }, { "UInt32", Type::UInt32 }, diff --git a/clickhouse/types/types.cpp b/clickhouse/types/types.cpp index 4435133e..da62cd25 100644 --- a/clickhouse/types/types.cpp +++ b/clickhouse/types/types.cpp @@ -55,6 +55,9 @@ const char* Type::TypeName(Type::Code code) { case Type::Code::Time: return "Time"; case Type::Code::Time64: return "Time64"; case Type::Code::JSON: return "JSON"; +#if !CH_MAP_BOOL_TO_UINT8 + case Type::Code::Bool: return "Bool"; +#endif } return "Unknown type"; @@ -87,6 +90,9 @@ std::string Type::GetName() const { case Polygon: case MultiPolygon: case JSON: +#if !CH_MAP_BOOL_TO_UINT8 + case Bool: +#endif return TypeName(code_); case Time64: return As()->GetName(); @@ -149,6 +155,9 @@ uint64_t Type::GetTypeUniqueId() const { case Ring: case Polygon: case MultiPolygon: +#if !CH_MAP_BOOL_TO_UINT8 + case Bool: +#endif // For simple types, unique ID is the same as Type::Code return code_; diff --git a/clickhouse/types/types.h b/clickhouse/types/types.h index 9cdaa99d..84aac919 100644 --- a/clickhouse/types/types.h +++ b/clickhouse/types/types.h @@ -9,12 +9,17 @@ #include #include +#ifndef CH_MAP_BOOL_TO_UINT8 +#define CH_MAP_BOOL_TO_UINT8 1 +#endif + namespace clickhouse { using Int128 = absl::int128; using UInt128 = absl::uint128; using Int64 = int64_t; + using TypeRef = std::shared_ptr; class Type { @@ -60,6 +65,9 @@ class Type { Time, Time64, JSON, +#if !CH_MAP_BOOL_TO_UINT8 + Bool, +#endif }; using EnumItem = std::pair; @@ -394,6 +402,13 @@ inline TypeRef Type::CreateSimple() { return TypeRef(new Type(UInt64)); } +#if !CH_MAP_BOOL_TO_UINT8 +template <> +inline TypeRef Type::CreateSimple() { + return TypeRef(new Type(Bool)); +} +#endif + template <> inline TypeRef Type::CreateSimple() { return TypeRef(new Type(Float32)); diff --git a/ut/CreateColumnByType_ut.cpp b/ut/CreateColumnByType_ut.cpp index c00e4bad..e312c116 100644 --- a/ut/CreateColumnByType_ut.cpp +++ b/ut/CreateColumnByType_ut.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -63,7 +64,15 @@ class CreateColumnByTypeWithName : public ::testing::TestWithParamGetType().GetName(), "UInt8"); + EXPECT_EQ(col->GetType().GetCode(), Type::UInt8); + EXPECT_NE(nullptr, col->As()); +#else + EXPECT_EQ(col->GetType().GetName(), "Bool"); + EXPECT_EQ(col->GetType().GetCode(), Type::Bool); + EXPECT_NE(nullptr, col->As()); +#endif } TEST_P(CreateColumnByTypeWithName, CreateColumnByType) @@ -79,6 +88,9 @@ INSTANTIATE_TEST_SUITE_P(Basic, CreateColumnByTypeWithName, ::testing::Values( "String", "Date", "DateTime", "UUID", "Int128", "UInt128" )); +#if !CH_MAP_BOOL_TO_UINT8 +INSTANTIATE_TEST_SUITE_P(BasicBool, CreateColumnByTypeWithName, ::testing::Values("Bool")); +#endif INSTANTIATE_TEST_SUITE_P(Parametrized, CreateColumnByTypeWithName, ::testing::Values( "FixedString(0)", "FixedString(10000)", diff --git a/ut/client_ut.cpp b/ut/client_ut.cpp index 29b0d47b..7a75c518 100644 --- a/ut/client_ut.cpp +++ b/ut/client_ut.cpp @@ -1,4 +1,5 @@ #include +#include #include "clickhouse/base/socket.h" #include "clickhouse/version.h" @@ -22,6 +23,22 @@ using namespace clickhouse; +#if CH_MAP_BOOL_TO_UINT8 +using ClientBoolColumn = ColumnUInt8; +using ClientBoolValue = uint8_t; +#else +using ClientBoolColumn = ColumnBool; +using ClientBoolValue = bool; +#endif + +ClientBoolValue MakeClientBoolValue(bool value) { +#if CH_MAP_BOOL_TO_UINT8 + return static_cast(value); +#else + return value; +#endif +} + template std::shared_ptr createTableWithOneColumn(Client & client, const std::string & table_name, const std::string & column_name) { @@ -400,11 +417,11 @@ TEST_P(ClientCase, Generic) { auto id = std::make_shared(); auto name = std::make_shared(); - auto f = std::make_shared (); + auto f = std::make_shared(); for (auto const& td : TEST_DATA) { id->Append(td.id); name->Append(td.name); - f->Append(td.f); + f->Append(MakeClientBoolValue(td.f)); } block.AppendColumn("id" , id); @@ -426,7 +443,7 @@ TEST_P(ClientCase, Generic) { for (size_t c = 0; c < block.GetRowCount(); ++c, ++row) { EXPECT_EQ(TEST_DATA[row].id, (*block[0]->As())[c]); EXPECT_EQ(TEST_DATA[row].name, (*block[1]->As())[c]); - EXPECT_EQ(TEST_DATA[row].f, (*block[2]->As())[c]); + EXPECT_EQ(MakeClientBoolValue(TEST_DATA[row].f), (*block[2]->As())[c]); } } ); @@ -468,13 +485,13 @@ TEST_P(ClientCase, InsertData) { // Fetch the derived columns. auto id = block[0]->As(); auto name = block[1]->As(); - auto f = block[2]->As(); + auto f = block[2]->As(); // Insert some values. for (auto const& td : TEST_DATA) { id->Append(td.id); name->Append(td.name); - f->Append(td.f); + f->Append(MakeClientBoolValue(td.f)); } block.RefreshRowCount(); client_->SendInsertBlock(block); @@ -484,7 +501,7 @@ TEST_P(ClientCase, InsertData) { for (auto const& td : TEST_DATA2) { id->Append(td.id); name->Append(td.name); - f->Append(td.f); + f->Append(MakeClientBoolValue(td.f)); } block.RefreshRowCount(); client_->SendInsertBlock(block); @@ -509,13 +526,13 @@ TEST_P(ClientCase, InsertData) { for (size_t c = 0; c < block.GetRowCount(); ++c, ++row) { EXPECT_EQ(TEST_DATA[row].id, (*block[0]->As())[c]); EXPECT_EQ(TEST_DATA[row].name, (*block[1]->As())[c]); - EXPECT_EQ(TEST_DATA[row].f, (*block[2]->As())[c]); + EXPECT_EQ(MakeClientBoolValue(TEST_DATA[row].f), (*block[2]->As())[c]); } } else { for (size_t c = 0; c < block.GetRowCount(); ++c, ++row) { EXPECT_EQ(TEST_DATA2[row-block_two_row_num].id, (*block[0]->As())[c]); EXPECT_EQ(TEST_DATA2[row-block_two_row_num].name, (*block[1]->As())[c]); - EXPECT_EQ(TEST_DATA2[row-block_two_row_num].f, (*block[2]->As())[c]); + EXPECT_EQ(MakeClientBoolValue(TEST_DATA2[row-block_two_row_num].f), (*block[2]->As())[c]); } } } diff --git a/ut/types_ut.cpp b/ut/types_ut.cpp index b0029cd7..09deddfe 100644 --- a/ut/types_ut.cpp +++ b/ut/types_ut.cpp @@ -1,5 +1,7 @@ #include +#include #include +#include #include #include @@ -34,7 +36,27 @@ TEST(TypesCase, TypeName) { ); ASSERT_EQ(Type::CreateMap(Type::CreateSimple(), Type::CreateString())->GetName(), "Map(Int32, String)"); + +#if !CH_MAP_BOOL_TO_UINT8 + ASSERT_EQ(Type::CreateSimple()->GetName(), "Bool"); +#endif +} + +#if !CH_MAP_BOOL_TO_UINT8 +TEST(TypesCase, ColumnBool) { + auto col = std::make_shared(); + col->Append(true); + col->Append(false); + col->Append(true); + + ASSERT_EQ(col->Size(), 3u); + ASSERT_EQ(col->At(0), true); + ASSERT_EQ(col->At(1), false); + ASSERT_EQ(col->At(2), true); + ASSERT_EQ(col->GetType().GetName(), "Bool"); + ASSERT_EQ(col->GetType().GetCode(), Type::Bool); } +#endif TEST(TypesCase, NullableType) { TypeRef nested = Type::CreateSimple(); diff --git a/ut/utils.cpp b/ut/utils.cpp index 989f7a5c..d776797d 100644 --- a/ut/utils.cpp +++ b/ut/utils.cpp @@ -362,6 +362,11 @@ std::ostream& operator<<(std::ostream& ostr, const ItemView& item_view) { case Type::UInt8: ostr << static_cast(item_view.get()); break; +#if !CH_MAP_BOOL_TO_UINT8 + case Type::Bool: + ostr << (item_view.get() ? "true" : "false"); + break; +#endif case Type::UInt16: ostr << static_cast(item_view.get()); break;