diff --git a/README.md b/README.md index acb4232a..b35d5c55 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ C++ client for [ClickHouse](https://clickhouse.com/). * UInt8, UInt16, UInt32, UInt64, Int8, Int16, Int32, Int64 * Int128 * UUID +* Map ## Building @@ -87,7 +88,7 @@ int main() ## Retries If you wish to implement some retry logic atop of `clickhouse::Client` there are few simple rules to make you life easier: -- If previous attempt threw an exception, then make sure to call `clickhouse::Client::ResetConnection()` before the next try. +- If previous attempt threw an exception, then make sure to call `clickhouse::Client::ResetConnection()` before the next try. - For `clickhouse::Client::Insert()` you can reuse a block from previous try, no need to rebuild it from scratch. See https://github.com/ClickHouse/clickhouse-cpp/issues/184 for details. diff --git a/clickhouse/CMakeLists.txt b/clickhouse/CMakeLists.txt index db1c8692..2caeebba 100644 --- a/clickhouse/CMakeLists.txt +++ b/clickhouse/CMakeLists.txt @@ -18,6 +18,7 @@ SET ( clickhouse-cpp-lib-src columns/lowcardinalityadaptor.h columns/nullable.cpp columns/numeric.cpp + columns/map.cpp columns/string.cpp columns/tuple.cpp columns/uuid.cpp @@ -121,6 +122,7 @@ INSTALL(FILES columns/itemview.h DESTINATION include/clickhouse/columns/) INSTALL(FILES columns/lowcardinality.h DESTINATION include/clickhouse/columns/) INSTALL(FILES columns/nullable.h DESTINATION include/clickhouse/columns/) INSTALL(FILES columns/numeric.h DESTINATION include/clickhouse/columns/) +INSTALL(FILES columns/map.h DESTINATION include/clickhouse/columns/) INSTALL(FILES columns/string.h DESTINATION include/clickhouse/columns/) INSTALL(FILES columns/tuple.h DESTINATION include/clickhouse/columns/) INSTALL(FILES columns/utils.h DESTINATION include/clickhouse/columns/) diff --git a/clickhouse/client.h b/clickhouse/client.h index 452226d9..7dfc75bd 100644 --- a/clickhouse/client.h +++ b/clickhouse/client.h @@ -12,6 +12,7 @@ #include "columns/lowcardinality.h" #include "columns/nullable.h" #include "columns/numeric.h" +#include "columns/map.h" #include "columns/string.h" #include "columns/tuple.h" #include "columns/uuid.h" diff --git a/clickhouse/columns/array.h b/clickhouse/columns/array.h index 1d3eb192..f3bb23d6 100644 --- a/clickhouse/columns/array.h +++ b/clickhouse/columns/array.h @@ -2,6 +2,7 @@ #include "column.h" #include "numeric.h" +#include "utils.h" #include @@ -121,13 +122,8 @@ class ColumnArrayT : public ColumnArray { * This is a static method to make such conversion verbose. */ static auto Wrap(ColumnArray&& col) { - if constexpr (std::is_base_of_v && !std::is_same_v) { - // assuming NestedColumnType is ArrayT specialization - return std::make_shared>(NestedColumnType::Wrap(col.GetData()), col.offsets_); - } else { - auto nested_data = col.GetData()->template AsStrict(); - return std::make_shared>(nested_data, col.offsets_); - } + auto nested_data = WrapColumn(col.GetData()); + return std::make_shared>(nested_data, col.offsets_); } static auto Wrap(Column&& col) { @@ -146,7 +142,7 @@ class ColumnArrayT : public ColumnArray { const size_t size_; public: - using ValueType = typename NestedColumnType::ValueType; + using ValueType = std::decay_t().At(0))>; ArrayValueView(std::shared_ptr data, size_t offset = 0, size_t size = std::numeric_limits::max()) : typed_nested_data_(data) @@ -178,7 +174,7 @@ class ColumnArrayT : public ColumnArray { , index_(index) {} - using ValueType = typename NestedColumnType::ValueType; + using ValueType = typename ArrayValueView::ValueType; inline auto operator*() const { return typed_nested_data_->At(offset_ + index_); @@ -226,6 +222,22 @@ class ColumnArrayT : public ColumnArray { inline size_t Size() const { return size_; } + + inline bool operator==(const ArrayValueView& other) const { + if (size() != other.size()) { + return false; + } + for (size_t i = 0; i < size_; ++i) { + if ((*this)[i] != other[i]) { + return false; + } + } + return true; + } + + inline bool operator!=(const ArrayValueView& other) const { + return !(*this == other); + } }; inline auto At(size_t index) const { @@ -267,6 +279,20 @@ class ColumnArrayT : public ColumnArray { AddOffset(counter); } + ColumnRef Slice(size_t begin, size_t size) const override { + return Wrap(ColumnArray::Slice(begin, size)); + } + + ColumnRef CloneEmpty() const override { + return Wrap(ColumnArray::CloneEmpty()); + } + + void Swap(Column& other) override { + auto & col = dynamic_cast &>(other); + typed_nested_data_.swap(col.typed_nested_data_); + ColumnArray::Swap(other); + } + private: /// Helper to allow wrapping a "typeless" ColumnArray ColumnArrayT(ColumnArray&& array, std::shared_ptr nested_data) diff --git a/clickhouse/columns/factory.cpp b/clickhouse/columns/factory.cpp index 38b02e1d..2ea7b6f5 100644 --- a/clickhouse/columns/factory.cpp +++ b/clickhouse/columns/factory.cpp @@ -14,6 +14,7 @@ #include "string.h" #include "tuple.h" #include "uuid.h" +#include "map.h" #include "../types/type_parser.h" @@ -86,7 +87,7 @@ static ColumnRef CreateTerminalColumn(const TypeAst& ast) { case Type::Date: return std::make_shared(); case Type::Date32: - return std::make_shared(); + return std::make_shared(); case Type::IPv4: return std::make_shared(); @@ -198,6 +199,27 @@ static ColumnRef CreateColumnFromAst(const TypeAst& ast, CreateColumnByTypeSetti return CreateTerminalColumn(ast.elements.back()); } + case TypeAst::Map: { + if (ast.elements.size() != 2) { + throw ValidationError(ast.name + " content is not correct"); + } + + std::vector columns; + + columns.reserve(ast.elements.size()); + for (const auto& elem : ast.elements) { + if (auto col = CreateColumnFromAst(elem, settings)) { + columns.push_back(col); + } else { + return nullptr; + } + } + + return std::make_shared( + std::make_shared( + std::make_shared(columns))); + } + case TypeAst::Assign: case TypeAst::Null: case TypeAst::Number: diff --git a/clickhouse/columns/itemview.cpp b/clickhouse/columns/itemview.cpp index a2cb69c2..12c89c31 100644 --- a/clickhouse/columns/itemview.cpp +++ b/clickhouse/columns/itemview.cpp @@ -77,6 +77,7 @@ void ItemView::ValidateData(Type::Code type, DataType data) { case Type::Code::Nullable: case Type::Code::Tuple: case Type::Code::LowCardinality: + case Type::Code::Map: throw AssertionError("Unsupported type in ItemView: " + std::string(Type::TypeName(type))); case Type::Code::IPv6: diff --git a/clickhouse/columns/map.cpp b/clickhouse/columns/map.cpp new file mode 100644 index 00000000..3f5616df --- /dev/null +++ b/clickhouse/columns/map.cpp @@ -0,0 +1,83 @@ +#include "map.h" + +#include + +#include "../exceptions.h" +#include "utils.h" + +namespace { + +using namespace clickhouse; + +TypeRef GetMapType(const Type& data_type) { + auto array = data_type.As(); + if (!array) { + throw ValidationError("Wrong type " + data_type.GetName() + " of data for map"); + } + auto tuple = array->GetItemType()->As(); + if (!tuple) { + throw ValidationError("Wrong type " + data_type.GetName() + " of data for map"); + } + auto types = tuple->GetTupleType(); + if (types.size() != 2) { + throw ValidationError("Wrong type " + data_type.GetName() + " of data for map"); + } + return Type::CreateMap(types[0], types[1]); +} + +} // namespace + +namespace clickhouse { + +ColumnMap::ColumnMap(ColumnRef data) + : Column(GetMapType(data->GetType())), data_(data->As()) { +} + +void ColumnMap::Clear() { + data_->Clear(); +} + +void ColumnMap::Append(ColumnRef column) { + if (auto col = column->As()) { + data_->Append(col->data_); + } +} + +bool ColumnMap::LoadPrefix(InputStream* input, size_t rows) { + return data_->LoadPrefix(input, rows); +} + +bool ColumnMap::LoadBody(InputStream* input, size_t rows) { + return data_->LoadBody(input, rows); +} + +void ColumnMap::SavePrefix(OutputStream* output) { + data_->SavePrefix(output); +} + +void ColumnMap::SaveBody(OutputStream* output) { + data_->SaveBody(output); +} + +size_t ColumnMap::Size() const { + return data_->Size(); +} + +ColumnRef ColumnMap::Slice(size_t begin, size_t len) const { + return std::make_shared(data_->Slice(begin, len)); +} + +ColumnRef ColumnMap::CloneEmpty() const { + return std::make_shared(data_->CloneEmpty()); +} + +void ColumnMap::Swap(Column& other) { + auto& col = dynamic_cast(other); + data_.swap(col.data_); +} + +ColumnRef ColumnMap::GetAsColumn(size_t n) const { + return data_->GetAsColumn(n); +} + +} // namespace clickhouse diff --git a/clickhouse/columns/map.h b/clickhouse/columns/map.h new file mode 100644 index 00000000..298d2c80 --- /dev/null +++ b/clickhouse/columns/map.h @@ -0,0 +1,240 @@ +#pragma once + +#include "array.h" +#include "column.h" +#include "tuple.h" + +#include + +namespace clickhouse { + +template +class ColumnMapT; + +/** + * Represents column of Map(K, V). + */ +class ColumnMap : public Column { +public: + /** Create a map of given type, with actual values and offsets. + * + * Both `data` and `offsets` are used (and modified) internally bye ColumnArray. + * Users are strongly advised against modifying contents of `data` or `offsets` afterwards. + */ + explicit ColumnMap(ColumnRef data); + + /// Appends content of given column to the end of current one. + void Append(ColumnRef column) override; + + /// Loads column prefix from input stream. + bool LoadPrefix(InputStream* input, size_t rows) override; + + /// Loads column data from input stream. + bool LoadBody(InputStream* input, size_t rows) override; + + /// Saves column prefix to output stream. + void SavePrefix(OutputStream* output) 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, size_t) const override; + ColumnRef CloneEmpty() const override; + void Swap(Column&) override; + + /// Converts map at pos n to column. + /// Type of row is tuple {key, value}. + ColumnRef GetAsColumn(size_t n) const; + +protected: + template + friend class ColumnMapT; + + ColumnMap(ColumnMap&& map); + +private: + std::shared_ptr data_; +}; + +template +class ColumnMapT : public ColumnMap { +public: + using KeyColumnType = K; + using ValueColumnType = V; + using Key = std::decay_t().At(0))>; + using Value = std::decay_t().At(0))>; + using TupleColumnType = ColumnTupleT; + using ArrayColumnType = ColumnArrayT; + + ColumnMapT(ColumnRef data) + : ColumnMap(data), typed_data_(data->AsStrict>()) {} + + ColumnMapT(std::shared_ptr keys, std::shared_ptr values) + : ColumnMap(std::make_shared(std::make_shared( + std::make_tuple(std::move(keys), std::move(values))))), + typed_data_(data_->template As()) {} + + ColumnRef Slice(size_t begin, size_t len) const override { + return std::make_shared>(typed_data_->Slice(begin, len)); + } + + ColumnRef CloneEmpty() const override { + return std::make_shared>(typed_data_->CloneEmpty()); + } + + void Swap(Column& other) override { + auto& col = dynamic_cast&>(other); + col.typed_data_.swap(typed_data_); + ColumnMap::Swap(other); + } + + /// A single (row) value of the Map-column i.e. read-only map. + /// It has a linear time complexity to access items + /// Because data base type has same structure + /// "This lookup works now with a linear complexity." + /// https://clickhouse.com/docs/en/sql-reference/data-types/map + /// Convert it to a suitable container required to access more than one element + + class MapValueView { + const typename ArrayColumnType::ArrayValueView data_; + + public: + using ValueType = std::pair; + + MapValueView(typename ArrayColumnType::ArrayValueView data) : data_(std::move(data)) {} + + inline auto operator[](const Key& key) const { return (*Find(key)).second; } + + inline auto At(const Key& key) const { + auto it = Find(key); + if (it == end()) throw ValidationError("ColumnMap value key not found"); + return (*it).second; + } + + class Iterator { + typename ArrayColumnType::ArrayValueView::Iterator data_iterator_; + + public: + Iterator(typename ArrayColumnType::ArrayValueView::Iterator data_iterator) + : data_iterator_(data_iterator) {} + + using ValueType = std::pair; + using difference_type = size_t; + using value_type = ValueType; + using pointer = void; + using reference = ValueType&; + using iterator_category = std::forward_iterator_tag; + + inline auto operator*() const { + auto tuple = *data_iterator_; + return ValueType{std::get<0>(tuple), std::get<1>(tuple)}; + } + + inline Iterator& operator++() { + ++data_iterator_; + return *this; + } + + inline bool operator==(const Iterator& other) const { + return this->data_iterator_ == other.data_iterator_; + } + + inline bool operator!=(const Iterator& other) const { return !(*this == other); } + }; + + // minimalistic stl-like container interface, hence the lowercase + inline Iterator begin() const { return Iterator{data_.begin()}; } + + inline Iterator cbegin() const { return Iterator{data_.cbegin()}; } + + inline Iterator end() const { return Iterator{data_.end()}; } + + inline Iterator cend() const { return Iterator{data_.cend()}; } + + inline size_t size() const { return data_.size(); } + + // It is ugly to have both size() and Size(), but it is for compatitability with both STL + // and rest of the clickhouse-cpp. + inline size_t Size() const { return data_.Size(); } + + inline size_t Count(const Key& key) const { + size_t result = 0; + for (auto item : data_) { + if (std::get<0>(item) == key) { + ++result; + } + } + return result; + } + + inline Iterator Find(const Key& key) const { + for (auto it = data_.begin(); it != data_.end(); ++it) { + if (std::get<0>(*it) == key) { + return Iterator{it}; + } + } + return end(); + } + + inline bool operator==(const MapValueView& other) const { + if (size() != other.size()) { + return false; + } + using Vector = std::vector>; + Vector l(begin(), end()); + Vector r(other.begin(), other.end()); + auto comp = [](const auto& l, const auto& r) { return l.frist < r.first; }; + std::sort(l.begin(), l.end(), comp); + std::sort(r.begin(), r.end(), comp); + return std::equal(l.begin(), l.end(), r.begin(), r.end()); + return true; + } + + inline bool operator!=(const MapValueView& other) const { return !(*this == other); } + }; + + inline auto At(size_t index) const { return MapValueView{typed_data_->At(index)}; } + + inline auto operator[](size_t index) const { return MapValueView{typed_data_->At(index)}; } + + using ColumnMap::Append; + + inline void Append(const MapValueView& value) { typed_data_->Append(value.data_); } + + inline void Append(const std::vector>& tuples) { + typed_data_->Append(tuples.begin(), tuples.end()); + } + + template + inline void Append(const T& value) { + // TODO Refuse to copy. + std::vector> container; + container.reserve(value.size()); + for (const auto& i : value) { + container.emplace_back(i.first, i.second); + } + typed_data_->Append(container.begin(), container.end()); + } + + static auto Wrap(ColumnMap&& col) { + auto data = ArrayColumnType::Wrap(std::move(col.data_)); + return std::make_shared>(std::move(data)); + } + + static auto Wrap(Column&& col) { return Wrap(std::move(dynamic_cast(col))); } + + // Helper to simplify integration with other APIs + static auto Wrap(ColumnRef&& col) { return Wrap(std::move(*col->AsStrict())); } + +private: + std::shared_ptr typed_data_; +}; + +} // namespace clickhouse diff --git a/clickhouse/columns/tuple.h b/clickhouse/columns/tuple.h index 63cfc689..a5f3a25f 100644 --- a/clickhouse/columns/tuple.h +++ b/clickhouse/columns/tuple.h @@ -1,6 +1,7 @@ #pragma once #include "column.h" +#include "utils.h" #include @@ -51,4 +52,113 @@ class ColumnTuple : public Column { std::vector columns_; }; -} +template +class ColumnTupleT : public ColumnTuple { +public: + using TupleOfColumns = std::tuple...>; + + using ValueType = std::tuple().At(0))>...>; + + ColumnTupleT(std::tuple...> columns) + : ColumnTuple(TupleToVector(columns)), typed_columns_(std::move(columns)) {} + + ColumnTupleT(std::vector columns) + : ColumnTuple(columns), typed_columns_(VectorToTuple(std::move(columns))) {} + + ColumnTupleT(const std::initializer_list columns) + : ColumnTuple(columns), typed_columns_(VectorToTuple(std::move(columns))) {} + + inline ValueType At(size_t index) const { return GetTupleOfValues(index); } + + inline ValueType operator[](size_t index) const { return GetTupleOfValues(index); } + + template > + inline void Append([[maybe_unused]] T value) { + static_assert(index <= std::tuple_size_v); + static_assert(std::tuple_size_v == std::tuple_size_v); + if constexpr (index == 0) { + return; + } else { + std::get(typed_columns_)->Append(std::move(std::get(value))); + Append(std::move(value)); + } + } + + /** Create a ColumnTupleT from a ColumnTuple, without copying data and offsets, but by + * 'stealing' those from `col`. + * + * Ownership of column internals is transferred to returned object, original (argument) object + * MUST NOT BE USED IN ANY WAY, it is only safe to dispose it. + * + * Throws an exception if `col` is of wrong type, it is safe to use original col in this case. + * This is a static method to make such conversion verbose. + */ + static auto Wrap(ColumnTuple&& col) { + if (col.TupleSize() != std::tuple_size_v) { + throw ValidationError("Can't wrap from " + col.GetType().GetName()); + } + return std::make_shared>(VectorToTuple(std::move(col))); + } + + static auto Wrap(Column&& col) { return Wrap(std::move(dynamic_cast(col))); } + + // Helper to simplify integration with other APIs + static auto Wrap(ColumnRef&& col) { return Wrap(std::move(*col->AsStrict())); } + + ColumnRef Slice(size_t begin, size_t size) const override { + return Wrap(ColumnTuple::Slice(begin, size)); + } + + ColumnRef CloneEmpty() const override { return Wrap(ColumnTuple::CloneEmpty()); } + + void Swap(Column& other) override { + auto& col = dynamic_cast&>(other); + typed_columns_.swap(col.typed_columns_); + ColumnTuple::Swap(other); + } + +private: + template > + inline static std::vector TupleToVector([[maybe_unused]] const T& value) { + static_assert(index <= std::tuple_size_v); + if constexpr (index == 0) { + std::vector result; + result.reserve(std::tuple_size_v); + return result; + } else { + auto result = std::move(TupleToVector(value)); + result.push_back(std::get(value)); + return result; + } + } + + template > + inline static auto VectorToTuple([[maybe_unused]] T columns) { + static_assert(column_index <= std::tuple_size_v); + if constexpr (column_index == 0) { + return std::make_tuple(); + } else { + using ColumnType = + typename std::tuple_element::type::element_type; + auto column = WrapColumn(columns[column_index - 1]); + return std::tuple_cat(std::move(VectorToTuple(std::move(columns))), + std::make_tuple(std::move(column))); + } + } + + template > + inline auto GetTupleOfValues([[maybe_unused]]size_t index) const { + static_assert(column_index <= std::tuple_size_v); + if constexpr (column_index == 0) { + return std::make_tuple(); + } else { + return std::tuple_cat( + std::move(GetTupleOfValues(index)), + std::move(std::make_tuple(std::get(typed_columns_)->At(index)))); + } + } + + TupleOfColumns typed_columns_; +}; + +} // namespace clickhouse diff --git a/clickhouse/columns/utils.h b/clickhouse/columns/utils.h index bcc5b988..9fbafa16 100644 --- a/clickhouse/columns/utils.h +++ b/clickhouse/columns/utils.h @@ -17,4 +17,24 @@ std::vector SliceVector(const std::vector& vec, size_t begin, size_t len) return result; } +template +struct HasWrapMethod { +private: + static int detect(...); + template + static decltype(U::Wrap(std::move(std::declval()))) detect(const U&); + +public: + static constexpr bool value = !std::is_same()))>::value; +}; + +template +inline std::shared_ptr WrapColumn(ColumnRef&& column) { + if constexpr (HasWrapMethod::value) { + return T::Wrap(std::move(column)); + } else { + return column->template AsStrict(); + } +} + } diff --git a/clickhouse/types/type_parser.cpp b/clickhouse/types/type_parser.cpp index 37a049a0..ee05a244 100644 --- a/clickhouse/types/type_parser.cpp +++ b/clickhouse/types/type_parser.cpp @@ -46,6 +46,7 @@ static const std::unordered_map kTypeCode = { { "Decimal64", Type::Decimal64 }, { "Decimal128", Type::Decimal128 }, { "LowCardinality", Type::LowCardinality }, + { "Map", Type::Map}, }; static Type::Code GetTypeCode(const std::string& name) { @@ -85,6 +86,10 @@ static TypeAst::Meta GetTypeMeta(const StringView& name) { return TypeAst::SimpleAggregateFunction; } + if (name == "Map") { + return TypeAst::Map; + } + return TypeAst::Terminal; } diff --git a/clickhouse/types/type_parser.h b/clickhouse/types/type_parser.h index 2c81d2e5..2f8f2f6f 100644 --- a/clickhouse/types/type_parser.h +++ b/clickhouse/types/type_parser.h @@ -21,7 +21,8 @@ struct TypeAst { Tuple, Enum, LowCardinality, - SimpleAggregateFunction + SimpleAggregateFunction, + Map }; /// Type's category. diff --git a/clickhouse/types/types.cpp b/clickhouse/types/types.cpp index c34c0b4e..370bc47b 100644 --- a/clickhouse/types/types.cpp +++ b/clickhouse/types/types.cpp @@ -46,6 +46,7 @@ const char* Type::TypeName(Type::Code code) { case Type::Code::LowCardinality: return "LowCardinality"; case Type::Code::DateTime64: return "DateTime64"; case Type::Code::Date32: return "Date32"; + case Type::Code::Map: return "Map"; } return "Unknown type"; @@ -94,6 +95,8 @@ std::string Type::GetName() const { return As()->GetName(); case LowCardinality: return As()->GetName(); + case Map: + return As()->GetName(); } // XXX: NOT REACHED! @@ -138,7 +141,8 @@ uint64_t Type::GetTypeUniqueId() const { case Decimal32: case Decimal64: case Decimal128: - case LowCardinality: { + case LowCardinality: + case Map: { // For complex types, exact unique ID depends on nested types and/or parameters, // the easiest way is to lazy-compute unique ID from name once. // Here we do not care if multiple threads are computing value simultaneosly since it is both: @@ -225,6 +229,10 @@ TypeRef Type::CreateLowCardinality(TypeRef item_type) { return std::make_shared(item_type); } +TypeRef Type::CreateMap(TypeRef key_type, TypeRef value_type) { + return std::make_shared(key_type, value_type); +} + /// class ArrayType ArrayType::ArrayType(TypeRef item_type) : Type(Array), item_type_(item_type) { @@ -404,4 +412,15 @@ std::string TupleType::GetName() const { return result; } +/// class MapType +MapType::MapType(TypeRef key_type, TypeRef value_type) + : Type(Map) + , key_type_(key_type) + , value_type_(value_type) { +} + +std::string MapType::GetName() const { + return std::string("Map(") + key_type_->GetName() + ", " +value_type_->GetName() + ")"; +} + } // namespace clickhouse diff --git a/clickhouse/types/types.h b/clickhouse/types/types.h index 8a27257a..16570f34 100644 --- a/clickhouse/types/types.h +++ b/clickhouse/types/types.h @@ -50,6 +50,7 @@ class Type { LowCardinality, DateTime64, Date32, + Map }; using EnumItem = std::pair; @@ -92,7 +93,7 @@ class Type { static TypeRef CreateDate(); - static TypeRef CreateDate32(); + static TypeRef CreateDate32(); static TypeRef CreateDateTime(std::string timezone = std::string()); @@ -125,6 +126,8 @@ class Type { static TypeRef CreateLowCardinality(TypeRef item_type); + static TypeRef CreateMap(TypeRef key_type, TypeRef value_type); + private: uint64_t GetTypeUniqueId() const; @@ -280,6 +283,23 @@ class LowCardinalityType : public Type { TypeRef nested_type_; }; +class MapType : public Type { +public: + explicit MapType(TypeRef key_type, TypeRef value_type); + + std::string GetName() const; + + /// Type of keys. + TypeRef GetKeyType() const { return key_type_; } + + /// Type of values. + TypeRef GetValueType() const { return value_type_; } + +private: + TypeRef key_type_; + TypeRef value_type_; +}; + template <> inline TypeRef Type::CreateSimple() { return TypeRef(new Type(Int8)); diff --git a/ut/client_ut.cpp b/ut/client_ut.cpp index 6a0af56b..58d6304b 100644 --- a/ut/client_ut.cpp +++ b/ut/client_ut.cpp @@ -1022,6 +1022,35 @@ TEST_P(ClientCase, RoundtripArrayTString) { EXPECT_TRUE(CompareRecursive(*array, *result_typed)); } +TEST_P(ClientCase, RoundtripMapTUint64String) { + using Map = ColumnMapT; + auto map = std::make_shared(std::make_shared(), std::make_shared()); + + std::map 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(ClientCase, RoundtripMapUUID_Tuple_String_Array_Uint64) { + using Tuple = ColumnTupleT>; + using Map = ColumnMapT; + auto map = std::make_shared(std::make_shared(), std::make_shared( + std::make_tuple(std::make_shared(), std::make_shared>()))); + + + std::map>> row; + row[UUID{1, 1}] = std::make_tuple("hello", std::vector{1, 2, 3}) ; + row[UUID{2, 2}] = std::make_tuple("world", std::vector{4, 5, 6}) ; + map->Append(row); + + auto result_typed = Map::Wrap(RoundtripColumnValues(*client_, map)); + EXPECT_TRUE(CompareRecursive(*map, *result_typed)); +} + TEST_P(ClientCase, OnProgress) { Block block; createTableWithOneColumn(block); diff --git a/ut/columns_ut.cpp b/ut/columns_ut.cpp index 604c94a4..eda33292 100644 --- a/ut/columns_ut.cpp +++ b/ut/columns_ut.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -787,3 +788,103 @@ TEST(ColumnsCase, ColumnLowCardinalityString_WithEmptyString_3) { EXPECT_EQ(values[i], col.At(i)) << " at pos: " << i; } } + + +TEST(ColumnsCase, ColumnTupleT) { + using TestTuple = ColumnTupleT; + + TestTuple col( + std::make_tuple( + std::make_shared(), + std::make_shared(), + std::make_shared(3) + ) + ); + const auto val = std::make_tuple(1, "a", "bcd"); + col.Append(val); + static_assert(std::is_same_v::type>); + static_assert(std::is_same_v::type>); + static_assert(std::is_same_v::type>); + EXPECT_EQ(val, col.At(0)); +} + +TEST(ColumnsCase, ColumnTupleT_Wrap) { + ColumnTuple col ({ + std::make_shared(), + std::make_shared(), + std::make_shared(3) + } + ); + + const auto val = std::make_tuple(1, "a", "bcd"); + + col[0]->AsStrict()->Append(std::get<0>(val)); + col[1]->AsStrict()->Append(std::get<1>(val)); + col[2]->AsStrict()->Append(std::get<2>(val)); + + using TestTuple = ColumnTupleT; + auto wrapped_col = TestTuple::Wrap(std::move(col)); + + EXPECT_EQ(wrapped_col->Size(), 1u); + EXPECT_EQ(val, wrapped_col->At(0)); +} + +TEST(ColumnsCase, ColumnTupleT_Empty) { + using TestTuple = ColumnTupleT<>; + + TestTuple col(std::make_tuple()); + const auto val = std::make_tuple(); + col.Append(val); + EXPECT_EQ(col.Size(), 0u); +} + +TEST(ColumnsCase, ColumnMapT) { + ColumnMapT col( + std::make_shared(), + std::make_shared()); + + std::map val; + val[1] = "123"; + val[2] = "abc"; + col.Append(val); + + auto map_view = col.At(0); + + EXPECT_THROW(map_view.At(0), ValidationError); + EXPECT_EQ(val[1], map_view.At(1)); + EXPECT_EQ(val[2], map_view.At(2)); + + std::map map{map_view.begin(), map_view.end()}; + + EXPECT_EQ(val[1], map.at(1)); + EXPECT_EQ(val[2], map.at(2)); +} + +TEST(ColumnsCase, ColumnMapT_Wrap) { + auto tupls = std::make_shared(std::vector{ + std::make_shared(), + std::make_shared()}); + + auto data = std::make_shared(tupls); + + auto val = tupls->CloneEmpty()->As(); + + (*val)[0]->AsStrict()->Append(1); + (*val)[1]->AsStrict()->Append("123"); + + (*val)[0]->AsStrict()->Append(2); + (*val)[1]->AsStrict()->Append("abc"); + + data->AppendAsColumn(val); + + ColumnMap col{data}; + + using TestMap = ColumnMapT; + auto wrapped_col = TestMap::Wrap(std::move(col)); + + auto map_view = wrapped_col->At(0); + + EXPECT_THROW(map_view.At(0), ValidationError); + EXPECT_EQ("123", map_view.At(1)); + EXPECT_EQ("abc", map_view.At(2)); +} diff --git a/ut/itemview_ut.cpp b/ut/itemview_ut.cpp index 40da5027..6413e190 100644 --- a/ut/itemview_ut.cpp +++ b/ut/itemview_ut.cpp @@ -80,6 +80,7 @@ TEST(ItemView, ErrorTypes) { EXPECT_ITEMVIEW_ERROR(Type::Code::Nullable, int); EXPECT_ITEMVIEW_ERROR(Type::Code::Tuple, int); EXPECT_ITEMVIEW_ERROR(Type::Code::LowCardinality, int); + EXPECT_ITEMVIEW_ERROR(Type::Code::Map, int); } TEST(ItemView, TypeSizeMismatch) { diff --git a/ut/type_parser_ut.cpp b/ut/type_parser_ut.cpp index e7fe6bf0..ee1258b7 100644 --- a/ut/type_parser_ut.cpp +++ b/ut/type_parser_ut.cpp @@ -226,3 +226,16 @@ TEST(TypeParserCase, ParseDateTime64) { ASSERT_EQ(ast.elements[1].value_string, "UTC"); ASSERT_EQ(ast.elements[1].value, 0); } + +TEST(TypeParserCase, ParseMap) { + TypeAst ast; + TypeParser("Map(Int32, String)").Parse(&ast); + ASSERT_EQ(ast.meta, TypeAst::Map); + ASSERT_EQ(ast.name, "Map"); + ASSERT_EQ(ast.code, Type::Map); + ASSERT_EQ(ast.elements.size(), 2u); + ASSERT_EQ(ast.elements[0].meta, TypeAst::Terminal); + ASSERT_EQ(ast.elements[0].name, "Int32"); + ASSERT_EQ(ast.elements[1].meta, TypeAst::Terminal); + ASSERT_EQ(ast.elements[1].name, "String"); +} diff --git a/ut/types_ut.cpp b/ut/types_ut.cpp index c5922d0e..8e2f943c 100644 --- a/ut/types_ut.cpp +++ b/ut/types_ut.cpp @@ -32,6 +32,8 @@ TEST(TypesCase, TypeName) { Type::CreateEnum8({})->GetName(), "Enum8()" ); + + ASSERT_EQ(Type::CreateMap(Type::CreateSimple(), Type::CreateString())->GetName(), "Map(Int32, String)"); } TEST(TypesCase, NullableType) { @@ -103,6 +105,11 @@ TEST(TypesCase, IsEqual) { "Array(Array(Array(Nullable(Tuple(String, Int8, Date, DateTime)))))", "Array(Array(Array(Array(Nullable(Tuple(String, Int8, Date, DateTime('UTC')))))))" "Array(Array(Array(Array(Nullable(Tuple(String, Int8, Date, DateTime('UTC'), Tuple(LowCardinality(String), Enum8('READ'=1, 'WRITE'=0))))))))", + "Map(String, Int8)", + "Map(String, Tuple(String, Int8, Date, DateTime))", + "Map(UUID, Array(Tuple(String, Int8, Date, DateTime)))", + "Map(String, Array(Array(Array(Nullable(Tuple(String, Int8, Date, DateTime))))))", + "Map(LowCardinality(FixedString(10)), Array(Array(Array(Array(Nullable(Tuple(String, Int8, Date, DateTime('UTC'))))))))" }; // Check that Type::IsEqual returns true only if: @@ -133,9 +140,9 @@ TEST(TypesCase, ErrorEnumContent) { "Enum8('ONE'=1,'TWO')", "Enum16('TWO'=,'TWO')", }; - + for (const auto& type_name : type_names) { SCOPED_TRACE(type_name); EXPECT_THROW(clickhouse::CreateColumnByType(type_name)->Type(), ValidationError); } -} \ No newline at end of file +} diff --git a/ut/utils.cpp b/ut/utils.cpp index e624f45c..9f8d9cab 100644 --- a/ut/utils.cpp +++ b/ut/utils.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -125,6 +126,27 @@ bool doPrintValue(const ColumnRef & c, const size_t row, std:: return false; } +template <> +bool doPrintValue(const ColumnRef & c, const size_t row, std::ostream & ostr) { + // via temporary stream to preserve fill and alignment of the ostr + std::stringstream sstr; + if (const auto & map_col = c->As()) { + sstr << "{"; + const auto tuples = map_col->GetAsColumn(row); + for (size_t i = 0; i < tuples->Size(); ++i) { + printColumnValue(tuples, i, sstr); + + if (i < tuples->Size() - 1) + sstr << ", "; + } + + sstr << "}"; + ostr << sstr.str(); + return true; + } + return false; +} + std::ostream & printColumnValue(const ColumnRef& c, const size_t row, std::ostream & ostr) { const auto r = false @@ -150,7 +172,8 @@ std::ostream & printColumnValue(const ColumnRef& c, const size_t row, std::ostre || doPrintValue(c, row, ostr) || doPrintValue(c, row, ostr) || doPrintValue(c, row, ostr) - || doPrintValue(c, row, ostr); + || doPrintValue(c, row, ostr) + || doPrintValue(c, row, ostr); if (!r) ostr << "Unable to print value of type " << c->GetType().GetName(); diff --git a/ut/utils.h b/ut/utils.h index 621119fb..a0d136fd 100644 --- a/ut/utils.h +++ b/ut/utils.h @@ -76,6 +76,27 @@ inline const char * getPrefix() { return prefix; } +template > +inline std::ostream & printTuple(std::ostream & ostr, [[maybe_unused]] const T & t) { + static_assert(index <= std::tuple_size_v); + if constexpr (index == 0) { + return ostr << "( "; + } else { + printTuple(ostr, t); + using ElementType = std::tuple_element_t; + if constexpr (is_container_v) { + ostr << PrintContainer{std::get(t)}; + } else { + ostr << std::get<0>(t); + } + if constexpr (index == std::tuple_size_v) { + return ostr << " )"; + } else { + return ostr << ", "; + } + } +} + namespace std { template inline ostream & operator<<(ostream & ostr, const chrono::duration & d) { @@ -86,6 +107,11 @@ template inline ostream & operator<<(ostream & ostr, const pair & t) { return ostr << "{ " << t.first << ", " << t.second << " }"; } + +template +inline ostream & operator<<(ostream & ostr, const tuple & t) { + return printTuple(ostr, t); +} }