diff --git a/clickhouse/columns/array.cpp b/clickhouse/columns/array.cpp index 77f8f7f9..eb90e330 100644 --- a/clickhouse/columns/array.cpp +++ b/clickhouse/columns/array.cpp @@ -138,8 +138,11 @@ size_t ColumnArray::Size() const { void ColumnArray::Swap(Column& other) { auto & col = dynamic_cast(other); - data_.swap(col.data_); - offsets_.swap(col.offsets_); + // Swap sub-column CONTENTS in place (never rebind the shared_ptr slots), so the data and + // offsets objects keep their identity and any As<>/Wrap views of this column stay coherent. + // The nested Swap also type-checks the element columns and throws on a mismatch. + data_->Swap(*col.data_); + offsets_->Swap(*col.offsets_); } void ColumnArray::OffsetsIncrease(size_t n) { diff --git a/clickhouse/columns/array.h b/clickhouse/columns/array.h index f771e4af..665c22d5 100644 --- a/clickhouse/columns/array.h +++ b/clickhouse/columns/array.h @@ -2,7 +2,6 @@ #include "column.h" #include "numeric.h" -#include "utils.h" #include @@ -107,7 +106,7 @@ class ColumnArray : public Column { }; template -class ColumnArrayT : public ColumnArray { +class ColumnArrayT : public ColumnArray, public WrappableColumn, ColumnArray> { public: class ArrayValueView; using ValueType = ArrayValueView; @@ -128,28 +127,43 @@ class ColumnArrayT : public ColumnArray { : ColumnArrayT(std::make_shared(std::forward(args)...)) {} - /** Create a ColumnArrayT from a ColumnArray, without copying data and offsets, but by 'stealing' those from `col`. + /** Create a ColumnArrayT that SHARES the internals of `col` (nested data and + * offsets) via shared_ptr, WITHOUT stealing or copying them. * - * 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. + * The original `col` remains fully valid and usable. Both the original and the + * returned wrapper reference the same underlying columns, so mutations through + * one are visible through the other. * - * 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. + * The two-argument overloads are non-throwing: on a type mismatch they return + * nullptr and, if `error` is non-null, assign a description to `*error`. The + * single-argument overloads throw ValidationError on a type mismatch instead. */ - static auto Wrap(ColumnArray&& col) { - auto nested_data = WrapColumn(col.GetData()); + static std::shared_ptr> Wrap(const ColumnArray& col, ValidationError* error) { + auto nested_data = WrapColumn(col.data_, error); + if (!nested_data) { + return nullptr; + } return std::make_shared>(nested_data, col.offsets_); } - static auto Wrap(Column&& col) { - return Wrap(std::move(dynamic_cast(col))); + static std::shared_ptr> Wrap(const Column& col, ValidationError* error) { + if (auto* c = dynamic_cast(&col)) { + return Wrap(*c, error); + } + if (error) { + *error = ValidationError("Can't wrap column of type " + col.GetType().GetName() + " as Array"); + } + return nullptr; } // Helper to simplify integration with other APIs - static auto Wrap(ColumnRef&& col) { - return Wrap(std::move(*col->AsStrict())); + static std::shared_ptr> Wrap(const ColumnRef& col, ValidationError* error) { + return Wrap(*col, error); } + // Throwing single-argument overloads (concrete type / Column& / ColumnRef&). + using WrappableColumn, ColumnArray>::Wrap; + /// A single (row) value of the Array-column, i.e. readonly array of items. class ArrayValueView { const std::shared_ptr typed_nested_data_; @@ -312,8 +326,9 @@ class ColumnArrayT : public ColumnArray { void Swap(Column& other) override { auto & col = dynamic_cast &>(other); - typed_nested_data_.swap(col.typed_nested_data_); - ColumnArray::Swap(other); + // Base swaps sub-column contents in place, preserving object identity, so the cached + // typed_nested_data_ still points at the correct object and must NOT be repointed. + ColumnArray::Swap(col); } private: diff --git a/clickhouse/columns/column.h b/clickhouse/columns/column.h index 475df89a..6f6d4e18 100644 --- a/clickhouse/columns/column.h +++ b/clickhouse/columns/column.h @@ -4,8 +4,10 @@ #include "../columns/itemview.h" #include "../exceptions.h" +#include #include #include +#include namespace clickhouse { @@ -24,26 +26,32 @@ class Column : public std::enable_shared_from_this { virtual ~Column() {} /// Downcast pointer to the specific column's subtype. + /// + /// If T is a "wrappable" typed column (one exposing a static Wrap method, e.g. + /// ColumnArrayT/ColumnTupleT/ColumnMapT/ColumnNullableT/ColumnLowCardinalityT) and the + /// column is not already exactly T, this attempts to Wrap it as T (a storage-sharing + /// typed view). Returns nullptr when neither an exact cast nor a wrap is possible. + /// (Definitions are out-of-line below, after WrapColumn is declared.) template - inline std::shared_ptr As() { - return std::dynamic_pointer_cast(shared_from_this()); - } + inline std::shared_ptr As(); - /// Downcast pointer to the specific column's subtype. + /// Const overload. Behaves like the non-const As(): for a wrappable T it first tries an + /// exact downcast, then falls back to Wrap, returning a storage-sharing view typed as + /// shared_ptr (read-only surface). Wrapping never mutates the source column; it + /// does require an internal const_pointer_cast because Wrap builds a mutable wrapper. + /// Caveat: the read-only guarantee is shallow -- the wrapper still holds non-const + /// references to the shared sub-columns, so a deliberate const_pointer_cast could + /// still reach the underlying storage. Casual const use stays read-only. template - inline std::shared_ptr As() const { - return std::dynamic_pointer_cast(shared_from_this()); - } + inline std::shared_ptr As() const; - /// Downcast pointer to the specific column's subtype. + /// Like As(), but throws ValidationError instead of returning nullptr on failure. template - inline std::shared_ptr AsStrict() { - auto result = std::dynamic_pointer_cast(shared_from_this()); - if (!result) { - throw ValidationError("Can't cast from " + type_->GetName()); - } - return result; - } + inline std::shared_ptr AsStrict(); + + /// Const overload of AsStrict(); wraps like As() const and throws on failure. + template + inline std::shared_ptr AsStrict() const; /// Get type object of the column. inline TypeRef Type() const { return type_; } @@ -104,4 +112,147 @@ class Column : public std::enable_shared_from_this { TypeRef type_; }; +template +std::vector SliceVector(const std::vector& vec, size_t begin, size_t len) { + std::vector result; + + if (begin < vec.size()) { + len = std::min(len, vec.size() - begin); + result.assign(vec.begin() + begin, vec.begin() + (begin + 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; +}; + +// Non-throwing: returns nullptr and (if `error` is non-null) fills `*error` when `column` +// can't be wrapped as T. +template +inline std::shared_ptr WrapColumn(const ColumnRef& column, ValidationError* error) { + if constexpr (HasWrapMethod::value) { + return T::Wrap(column, error); + } else { + auto result = column->template As(); + if (!result && error) { + *error = ValidationError("Can't wrap column of type " + column->GetType().GetName()); + } + return result; + } +} + +// Throwing convenience wrapper. +template +inline std::shared_ptr WrapColumn(const ColumnRef& column) { + ValidationError error; + auto result = WrapColumn(column, &error); + if (!result) { + throw error; + } + return result; +} + +// Provides the throwing single-argument Wrap overloads for typed columns. +// Derived must supply the non-throwing two-argument Wrap(col, ValidationError*) overloads. +// BaseColumn is Derived's concrete base (e.g. ColumnArray) so an already-typed +// argument takes the direct path instead of the dynamic_cast'ing Column& overload. +template +struct WrappableColumn { + static auto Wrap(const BaseColumn& col) { + ValidationError error; + auto result = Derived::Wrap(col, &error); + if (!result) { + throw error; + } + return result; + } + + static auto Wrap(const Column& col) { + ValidationError error; + auto result = Derived::Wrap(col, &error); + if (!result) { + throw error; + } + return result; + } + + static auto Wrap(const ColumnRef& col) { + ValidationError error; + auto result = Derived::Wrap(col, &error); + if (!result) { + throw error; + } + return result; + } +}; + +template +inline std::shared_ptr Column::As() { + if constexpr (HasWrapMethod::value) { + if (auto exact = std::dynamic_pointer_cast(shared_from_this())) { + return exact; + } + return WrapColumn(shared_from_this(), nullptr); + } else { + return std::dynamic_pointer_cast(shared_from_this()); + } +} + +template +inline std::shared_ptr Column::As() const { + if constexpr (HasWrapMethod::value) { + if (auto exact = std::dynamic_pointer_cast(shared_from_this())) { + return exact; + } + // Wrap needs a mutable ColumnRef to build a storage-sharing view; the result is + // returned as shared_ptr so the caller keeps read-only access, and + // wrapping never mutates the source column. + return WrapColumn(std::const_pointer_cast(shared_from_this()), nullptr); + } else { + return std::dynamic_pointer_cast(shared_from_this()); + } +} + +template +inline std::shared_ptr Column::AsStrict() { + if constexpr (HasWrapMethod::value) { + if (auto exact = std::dynamic_pointer_cast(shared_from_this())) { + return exact; + } + return WrapColumn(shared_from_this()); + } else { + auto result = std::dynamic_pointer_cast(shared_from_this()); + if (!result) { + throw ValidationError("Can't cast from " + type_->GetName()); + } + return result; + } +} + +template +inline std::shared_ptr Column::AsStrict() const { + if constexpr (HasWrapMethod::value) { + if (auto exact = std::dynamic_pointer_cast(shared_from_this())) { + return exact; + } + // Throwing WrapColumn: raises ValidationError on a type mismatch. + return WrapColumn(std::const_pointer_cast(shared_from_this())); + } else { + auto result = std::dynamic_pointer_cast(shared_from_this()); + if (!result) { + throw ValidationError("Can't cast from " + type_->GetName()); + } + return result; + } +} + } // namespace clickhouse diff --git a/clickhouse/columns/enum.cpp b/clickhouse/columns/enum.cpp index 43fab893..1fa2cba5 100644 --- a/clickhouse/columns/enum.cpp +++ b/clickhouse/columns/enum.cpp @@ -1,5 +1,4 @@ #include "enum.h" -#include "utils.h" #include "../base/input.h" #include "../base/output.h" diff --git a/clickhouse/columns/factory.cpp b/clickhouse/columns/factory.cpp index a01304f8..f64109f2 100644 --- a/clickhouse/columns/factory.cpp +++ b/clickhouse/columns/factory.cpp @@ -233,13 +233,25 @@ static ColumnRef CreateColumnFromAst(const TypeAst& ast, CreateColumnByTypeSetti } } else { + // Create the base ColumnLowCardinality (like Array/Nullable/Tuple/Map create their + // base types). Callers can obtain the strongly-typed ColumnLowCardinalityT<...> view + // on demand via Column::As>(), which wraps it. switch (nested.code) { - // TODO (nemkov): update this to maximize code reuse. case Type::String: - return std::make_shared>(); case Type::FixedString: - return std::make_shared>(GetASTChildElement(nested, 0).value); + return std::make_shared(CreateColumnFromAst(nested, settings)); case Type::Nullable: + // Nullable needs its own case (it can't reuse the generic CreateColumnFromAst + // path above) for two reasons: + // 1. Constructor overload: ColumnLowCardinality has a dedicated + // ColumnLowCardinality(shared_ptr) ctor that seeds the + // special NULL item at dictionary index 0 (via AppendNullItem()). We must + // pass a statically-typed shared_ptr so that overload is + // selected; passing a ColumnRef would statically bind to the generic + // ColumnLowCardinality(ColumnRef) ctor, which only appends the default + // item and would omit the null item, producing an incorrect nullable + // dictionary. + // 2. It lets us construct the ColumnNullable with an explicit UInt8 null-map. return std::make_shared( std::make_shared( CreateColumnFromAst(GetASTChildElement(nested, 0), settings), diff --git a/clickhouse/columns/geo.cpp b/clickhouse/columns/geo.cpp index fa987732..daf70664 100644 --- a/clickhouse/columns/geo.cpp +++ b/clickhouse/columns/geo.cpp @@ -1,7 +1,5 @@ #include "geo.h" -#include "utils.h" - namespace { using namespace ::clickhouse; diff --git a/clickhouse/columns/lowcardinality.cpp b/clickhouse/columns/lowcardinality.cpp index e286c863..61efb903 100644 --- a/clickhouse/columns/lowcardinality.cpp +++ b/clickhouse/columns/lowcardinality.cpp @@ -158,8 +158,8 @@ namespace clickhouse { ColumnLowCardinality::ColumnLowCardinality(ColumnRef dictionary_column) : Column(Type::CreateLowCardinality(dictionary_column->Type())), dictionary_column_(dictionary_column->CloneEmpty()), // safe way to get an column of the same type. - index_column_(std::make_shared()), - index_type_code_(Type::UInt32) + index_(std::make_shared(IndexState{std::make_shared(), Type::UInt32})), + unique_items_map_(std::make_shared()) { Setup(dictionary_column); } @@ -167,8 +167,8 @@ ColumnLowCardinality::ColumnLowCardinality(ColumnRef dictionary_column) ColumnLowCardinality::ColumnLowCardinality(std::shared_ptr dictionary_column) : Column(Type::CreateLowCardinality(dictionary_column->Type())), dictionary_column_(dictionary_column->CloneEmpty()), // safe way to get an column of the same type. - index_column_(std::make_shared()), - index_type_code_(Type::UInt32) + index_(std::make_shared(IndexState{std::make_shared(), Type::UInt32})), + unique_items_map_(std::make_shared()) { AppendNullItem(); Setup(dictionary_column); @@ -182,14 +182,23 @@ void ColumnLowCardinality::Reserve(size_t new_cap) { // NOTE(vnemkov): Formula below (`ceil(sqrt(x))`) is a gut-feeling-good-enough estimation, // feel free to replace/adjust if you have better one suported by actual data. dictionary_column_->Reserve(static_cast(ceil(sqrt(static_cast(new_cap))))); - index_column_->Reserve(new_cap + 2); // + 1 for null item (at pos 0), + 1 for default item (at pos 1) + index_->column->Reserve(new_cap + 2); // + 1 for null item (at pos 0), + 1 for default item (at pos 1) } void ColumnLowCardinality::Setup(ColumnRef dictionary_column) { + // Cache the dictionary item type code: for a Nullable dictionary it is the code of the + // innermost non-nullable type, otherwise the dictionary's own type code. The dictionary + // type is invariant after construction, so this stays valid. + if (auto nullable = dictionary_column_->As()) { + item_type_code_ = nullable->Nested()->Type()->GetCode(); + } else { + item_type_code_ = dictionary_column_->Type()->GetCode(); + } + AppendDefaultItem(); if (dictionary_column->Size() != 0) { - // Add values, updating index_column_ and unique_items_map_. + // Add values, updating the index column and unique_items_map_. // TODO: it would be possible to eliminate copying // by adding InsertUnsafe(pos, ItemView) method to a Column @@ -202,15 +211,15 @@ void ColumnLowCardinality::Setup(ColumnRef dictionary_column) { } std::uint64_t ColumnLowCardinality::getDictionaryIndex(std::uint64_t item_index) const { - switch (index_type_code_) { + switch (index_->type_code) { case Type::UInt8: - return static_cast(*index_column_)[item_index]; + return static_cast(*index_->column)[item_index]; case Type::UInt16: - return static_cast(*index_column_)[item_index]; + return static_cast(*index_->column)[item_index]; case Type::UInt32: - return static_cast(*index_column_)[item_index]; + return static_cast(*index_->column)[item_index]; case Type::UInt64: - return static_cast(*index_column_)[item_index]; + return static_cast(*index_->column)[item_index]; default: throw ValidationError("Invalid index column type"); } @@ -218,18 +227,18 @@ std::uint64_t ColumnLowCardinality::getDictionaryIndex(std::uint64_t item_index) void ColumnLowCardinality::appendIndex(std::uint64_t item_index) { // TODO (nemkov): handle case when index should go from UInt8 to UInt16, etc. - switch (index_type_code_) { + switch (index_->type_code) { case Type::UInt8: - static_cast(*index_column_).Append(static_cast(item_index)); + static_cast(*index_->column).Append(static_cast(item_index)); break; case Type::UInt16: - static_cast(*index_column_).Append(static_cast(item_index)); + static_cast(*index_->column).Append(static_cast(item_index)); break; case Type::UInt32: - static_cast(*index_column_).Append(static_cast(item_index)); + static_cast(*index_->column).Append(static_cast(item_index)); break; case Type::UInt64: - static_cast(*index_column_).Append(static_cast(item_index)); + static_cast(*index_->column).Append(static_cast(item_index)); break; default: throw ValidationError("Invalid index column type"); @@ -237,24 +246,24 @@ void ColumnLowCardinality::appendIndex(std::uint64_t item_index) { } void ColumnLowCardinality::removeLastIndex() { - switch (index_type_code_) { + switch (index_->type_code) { case Type::UInt8: { - auto& col = static_cast(*index_column_); + auto& col = static_cast(*index_->column); col.Erase(col.Size() - 1); break; } case Type::UInt16: { - auto& col = static_cast(*index_column_); + auto& col = static_cast(*index_->column); col.Erase(col.Size() - 1); break; } case Type::UInt32: { - auto& col = static_cast(*index_column_); + auto& col = static_cast(*index_->column); col.Erase(col.Size() - 1); break; } case Type::UInt64: { - auto& col = static_cast(*index_column_); + auto& col = static_cast(*index_->column); col.Erase(col.Size() - 1); break; } @@ -380,9 +389,11 @@ bool ColumnLowCardinality::LoadBody(InputStream* input, size_t rows) { auto [new_dictionary, new_index, new_unique_items_map] = ::Load(dictionary_column_->CloneEmpty(), *input, rows); dictionary_column_->Swap(*new_dictionary); - index_column_.swap(new_index); - unique_items_map_.swap(new_unique_items_map); - index_type_code_ = index_column_->Type()->GetCode(); + // Reassign the index column inside the SHARED bundle (not a local member slot) so the + // new index and its type code are visible through every wrapped view. + index_->column = std::move(new_index); + index_->type_code = index_->column->Type()->GetCode(); + unique_items_map_->swap(new_unique_items_map); return true; } catch (...) { @@ -397,7 +408,7 @@ void ColumnLowCardinality::SavePrefix(OutputStream* output) { void ColumnLowCardinality::SaveBody(OutputStream* output) { const uint64_t index_serialization_type = - static_cast(indexTypeFromIndexColumn(*index_column_)) | IndexFlag::HasAdditionalKeysBit; + static_cast(indexTypeFromIndexColumn(*index_->column)) | IndexFlag::HasAdditionalKeysBit; WireFormat::WriteFixed(*output, index_serialization_type); const uint64_t number_of_keys = dictionary_column_->Size(); @@ -409,16 +420,16 @@ void ColumnLowCardinality::SaveBody(OutputStream* output) { dictionary_column_->SaveBody(output); } - const uint64_t number_of_rows = index_column_->Size(); + const uint64_t number_of_rows = index_->column->Size(); WireFormat::WriteFixed(*output, number_of_rows); - index_column_->SaveBody(output); + index_->column->SaveBody(output); } void ColumnLowCardinality::Clear() { - index_column_->Clear(); + index_->column->Clear(); dictionary_column_->Clear(); - unique_items_map_.clear(); + unique_items_map_->clear(); if (auto columnNullable = dictionary_column_->As()) { AppendNullItem(); @@ -427,7 +438,7 @@ void ColumnLowCardinality::Clear() { } size_t ColumnLowCardinality::Size() const { - return index_column_->Size(); + return index_->column->Size(); } ColumnRef ColumnLowCardinality::Slice(size_t begin, size_t len) const { @@ -456,9 +467,16 @@ void ColumnLowCardinality::Swap(Column& other) { // (needed for ColumnLowCardinalityT) dictionary_column_->Swap(*col.dictionary_column_); - index_column_.swap(col.index_column_); - unique_items_map_.swap(col.unique_items_map_); - std::swap(index_type_code_, col.index_type_code_); + // Swap the index bundle CONTENTS (column + type code) in place. Both sides' wrapped views + // share their respective IndexState object, so the swap is visible through every holder. + // (The index column can't be swapped content-wise like the dictionary: the two columns may + // have different numeric widths, hence the shared-bundle indirection.) + std::swap(*index_, *col.index_); + unique_items_map_->swap(*col.unique_items_map_); + + // NOTE: item_type_code_ is intentionally NOT swapped. It is derived solely from the dictionary + // type, and the guard above requires both columns to have the same dictionary type, so it is + // identical on both sides - swapping would be a no-op. } ItemView ColumnLowCardinality::GetItem(size_t index) const { @@ -478,9 +496,9 @@ ItemView ColumnLowCardinality::GetItem(size_t index) const { // No checks regarding value type or validity of value is made. void ColumnLowCardinality::AppendUnsafe(const ItemView & value) { const auto key = computeHashKey(value); - const auto initial_index_size = index_column_->Size(); + const auto initial_index_size = index_->column->Size(); // If the value is unique, then we are going to append it to a dictionary, hence new index is Size(). - auto [iterator, is_new_item] = unique_items_map_.try_emplace(key, dictionary_column_->Size()); + auto [iterator, is_new_item] = unique_items_map_->try_emplace(key, dictionary_column_->Size()); try { // Order is important, adding to dictionary last, since it is much (MUCH!!!!) harder // to remove item from dictionary column than from index column @@ -494,10 +512,10 @@ void ColumnLowCardinality::AppendUnsafe(const ItemView & value) { } } catch (...) { - if (index_column_->Size() != initial_index_size) + if (index_->column->Size() != initial_index_size) removeLastIndex(); if (is_new_item) - unique_items_map_.erase(iterator); + unique_items_map_->erase(iterator); throw; } @@ -507,13 +525,13 @@ void ColumnLowCardinality::AppendNullItem() { const auto null_item = GetNullItemForDictionary(dictionary_column_); AppendToDictionary(*dictionary_column_, null_item); - unique_items_map_.emplace(computeHashKey(null_item), 0); + unique_items_map_->emplace(computeHashKey(null_item), 0); } void ColumnLowCardinality::AppendDefaultItem() { const auto defaultItem = GetDefaultItemForDictionary(dictionary_column_); - unique_items_map_.emplace(computeHashKey(defaultItem), dictionary_column_->Size()); + unique_items_map_->emplace(computeHashKey(defaultItem), dictionary_column_->Size()); AppendToDictionary(*dictionary_column_, defaultItem); } diff --git a/clickhouse/columns/lowcardinality.h b/clickhouse/columns/lowcardinality.h index 33c339e6..cf2f6ea0 100644 --- a/clickhouse/columns/lowcardinality.h +++ b/clickhouse/columns/lowcardinality.h @@ -45,12 +45,11 @@ class ColumnLowCardinality : public Column { template friend class ColumnLowCardinalityT; -private: - // IMPLEMENTATION NOTE: ColumnLowCardinalityT takes reference to underlying dictionary column object, - // so make sure to NOT change address of the dictionary object (with reset(), swap()) or with anything else. - ColumnRef dictionary_column_; - ColumnRef index_column_; - UniqueItems unique_items_map_; +protected: + // Shallow copy: shares dictionary_column_, index_ (the index bundle) and unique_items_map_ + // (all shared_ptr) and copies the base type. Used by ColumnLowCardinalityT::Wrap to create a + // non-destructive, storage-sharing view of `col`. + ColumnLowCardinality(const ColumnLowCardinality& col) = default; public: ColumnLowCardinality(ColumnLowCardinality&& col) = default; @@ -110,19 +109,39 @@ class ColumnLowCardinality : public Column { void AppendNullItem(); void AppendDefaultItem(); - Type::Code index_type_code_; - public: static details::LowCardinalityHashKey computeHashKey(const ItemView &); + +private: + // IMPLEMENTATION NOTE: ColumnLowCardinalityT takes reference to underlying dictionary column object, + // so make sure to NOT change address of the dictionary object (with reset(), swap()) or with anything else. + ColumnRef dictionary_column_; + + // The index column and its cached type code, bundled behind one shared_ptr. A wrapped view + // (ColumnLowCardinalityT::Wrap) shares this bundle, so that LoadBody()/Swap() - which REPLACE + // the index column, since its numeric width can change - stay coherent across every holder. + // The dictionary and dedup map are only ever mutated in place (never replaced), so they don't + // need this extra level of indirection. + struct IndexState { + ColumnRef column; + Type::Code type_code; + }; + std::shared_ptr index_; + + // Shared so that a wrapped (ColumnLowCardinalityT::Wrap) column shares the same dedup map as its + // source, keeping dictionary/index/map coherent across both holders (same semantics as other columns). + std::shared_ptr unique_items_map_; + + // Dictionary item type code (for a Nullable dictionary, the code of the innermost + // non-nullable type; otherwise the dictionary's own type code). Computed once in Setup() + // and used by ColumnLowCardinalityT to build ItemView on Append. + Type::Code item_type_code_; }; /** Type-aware wrapper that provides simple convenience interface for accessing/appending individual items. */ template -class ColumnLowCardinalityT : public ColumnLowCardinality { - - DictionaryColumnType& typed_dictionary_; - const Type::Code type_; +class ColumnLowCardinalityT : public ColumnLowCardinality, public WrappableColumn, ColumnLowCardinality> { public: using WrappedColumnType = DictionaryColumnType; @@ -132,7 +151,14 @@ class ColumnLowCardinalityT : public ColumnLowCardinality { explicit ColumnLowCardinalityT(ColumnLowCardinality&& col) : ColumnLowCardinality(std::move(col)) , typed_dictionary_(dynamic_cast(*GetDictionary())) - , type_(GetTypeCode(typed_dictionary_)) + { + } + + // Shares the internals of `col` (dictionary, index and dedup map) via shared_ptr, WITHOUT + // stealing or copying them. Used by Wrap to create a non-destructive, storage-sharing view. + explicit ColumnLowCardinalityT(const ColumnLowCardinality& col) + : ColumnLowCardinality(col) + , typed_dictionary_(dynamic_cast(*GetDictionary())) { } @@ -145,9 +171,21 @@ class ColumnLowCardinalityT : public ColumnLowCardinality { explicit ColumnLowCardinalityT(std::shared_ptr dictionary_col) : ColumnLowCardinality(dictionary_col) , typed_dictionary_(dynamic_cast(*GetDictionary())) - , type_(GetTypeCode(typed_dictionary_)) {} + // Used by Wrap() when the stored dictionary is a base column (e.g. a factory-built base + // ColumnNullable for LowCardinality(Nullable(String))) that had to be wrapped into the typed + // DictionaryColumnType. Shares col's internals (index bundle and dedup map), then swaps in the + // storage-sharing typed dictionary view so typed_dictionary_ binds to exactly a + // DictionaryColumnType. `typed_dictionary` MUST be a storage-sharing view of col's dictionary + // (as produced by DictionaryColumnType::Wrap), so mutations stay coherent across both holders. + ColumnLowCardinalityT(const ColumnLowCardinality& col, std::shared_ptr typed_dictionary) + : ColumnLowCardinality(col) + , typed_dictionary_(*typed_dictionary) + { + dictionary_column_ = std::move(typed_dictionary); + } + /// Extended interface to simplify reading/adding individual items. /// Returns element at given row number. @@ -166,12 +204,12 @@ class ColumnLowCardinalityT : public ColumnLowCardinality { inline void Append(const ValueType & value) { if constexpr (IsNullable) { if (value.has_value()) { - AppendUnsafe(ItemView{type_, *value}); + AppendUnsafe(ItemView{item_type_code_, *value}); } else { AppendUnsafe(ItemView{}); } } else { - AppendUnsafe(ItemView{type_, value}); + AppendUnsafe(ItemView{item_type_code_, value}); } } @@ -182,23 +220,54 @@ class ColumnLowCardinalityT : public ColumnLowCardinality { } } - /** Create a ColumnLowCardinalityT from a ColumnLowCardinality, without copying data and offsets, but by - * 'stealing' those from `col`. + /** Create a ColumnLowCardinalityT that SHARES the internals of `col` (dictionary, index and + * dedup map) via shared_ptr, WITHOUT stealing or copying them. * - * 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. + * The original `col` remains fully valid and usable. Both the original and the returned + * wrapper reference the same underlying storage, so mutations through one are visible through + * the other and remain coherent (the dedup map is shared as well). * - * 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. + * The two-argument overloads are non-throwing: on a type mismatch they return nullptr and, + * if `error` is non-null, assign a description to `*error`. The single-argument overloads + * throw ValidationError on a type mismatch instead. */ - static auto Wrap(ColumnLowCardinality&& col) { - return std::make_shared>(std::move(col)); + static std::shared_ptr> Wrap(const ColumnLowCardinality& col, ValidationError* error) { + // Fast path: the stored dictionary is already exactly DictionaryColumnType, so share it + // directly - typed_dictionary_ can bind to it via the reference dynamic_cast unchanged. + if (std::dynamic_pointer_cast(col.dictionary_column_)) { + return std::make_shared>(col); + } + + // Fallback: the stored dictionary is a base column, not DictionaryColumnType (e.g. the + // factory builds LowCardinality(Nullable(String)) with a base ColumnNullable dictionary). + // Wrap it into the typed DictionaryColumnType - a storage-sharing view over the same + // underlying columns - and bind typed_dictionary_ to that. Binding to the base dictionary + // directly would make the reference dynamic_cast throw std::bad_cast. + auto typed_dictionary = WrapColumn(col.dictionary_column_, error); + if (!typed_dictionary) { + // error (if requested) already set by WrapColumn. + return nullptr; + } + return std::make_shared>(col, typed_dictionary); } - static auto Wrap(Column&& col) { return Wrap(std::move(dynamic_cast(col))); } + static std::shared_ptr> Wrap(const Column& col, ValidationError* error) { + if (auto* c = dynamic_cast(&col)) { + return Wrap(*c, error); + } + if (error) { + *error = ValidationError("Can't wrap column of type " + col.GetType().GetName() + " as LowCardinality"); + } + return nullptr; + } // Helper to simplify integration with other APIs - static auto Wrap(ColumnRef&& col) { return Wrap(std::move(*col->AsStrict())); } + static std::shared_ptr> Wrap(const ColumnRef& col, ValidationError* error) { + return Wrap(*col, error); + } + + // Throwing single-argument overloads (concrete type / Column& / ColumnRef&). + using WrappableColumn, ColumnLowCardinality>::Wrap; ColumnRef Slice(size_t begin, size_t size) const override { return Wrap(ColumnLowCardinality::Slice(begin, size)); @@ -208,14 +277,8 @@ class ColumnLowCardinalityT : public ColumnLowCardinality { private: - template - static auto GetTypeCode(T& column) { - if constexpr (IsNullable) { - return GetTypeCode(*column.Nested()->template AsStrict()); - } else { - return column.Type()->GetCode(); - } - } + DictionaryColumnType& typed_dictionary_; + }; } diff --git a/clickhouse/columns/map.cpp b/clickhouse/columns/map.cpp index 839b0668..d1dcfd93 100644 --- a/clickhouse/columns/map.cpp +++ b/clickhouse/columns/map.cpp @@ -3,7 +3,6 @@ #include #include "../exceptions.h" -#include "utils.h" namespace { @@ -77,7 +76,9 @@ ColumnRef ColumnMap::CloneEmpty() const { void ColumnMap::Swap(Column& other) { auto& col = dynamic_cast(other); - data_.swap(col.data_); + // Swap the backing array's CONTENTS in place (never rebind the shared_ptr slot), so the + // array/tuple/leaf objects keep their identity and any As<>/Wrap views stay coherent. + data_->Swap(*col.data_); } ColumnRef ColumnMap::GetAsColumn(size_t n) const { diff --git a/clickhouse/columns/map.h b/clickhouse/columns/map.h index 4d644802..a9b97062 100644 --- a/clickhouse/columns/map.h +++ b/clickhouse/columns/map.h @@ -69,7 +69,7 @@ class ColumnMap : public Column { }; template -class ColumnMapT : public ColumnMap { +class ColumnMapT : public ColumnMap, public WrappableColumn, ColumnMap> { public: using KeyColumnType = K; using ValueColumnType = V; @@ -96,8 +96,9 @@ class ColumnMapT : public ColumnMap { void Swap(Column& other) override { auto& col = dynamic_cast&>(other); - col.typed_data_.swap(typed_data_); - ColumnMap::Swap(other); + // Base swaps the backing array's contents in place, preserving object identity, so the + // cached typed_data_ still points at the correct object and must NOT be repointed. + ColumnMap::Swap(col); } /// A single (row) value of the Map-column i.e. read-only map. @@ -119,7 +120,9 @@ class ColumnMapT : public ColumnMap { inline auto At(const Key& key) const { auto it = Find(key); - if (it == end()) throw ValidationError("ColumnMap value key not found"); + if (it == end()) { + throw ValidationError("ColumnMap value key not found"); + } return (*it).second; } @@ -240,15 +243,42 @@ class ColumnMapT : public ColumnMap { typed_data_->Append(Iterator{value.begin(), functor}, Iterator{value.end(), functor}); } - static auto Wrap(ColumnMap&& col) { - auto data = ArrayColumnType::Wrap(std::move(col.data_)); - return std::make_shared>(std::move(data)); + /** Create a ColumnMapT that SHARES the internals of `col` (its backing array of + * key/value tuples) via shared_ptr, WITHOUT stealing or copying them. + * + * The original `col` remains fully valid and usable. Both the original and the + * returned wrapper reference the same underlying columns, so mutations through + * one are visible through the other. + * + * The two-argument overloads are non-throwing: on a type mismatch they return + * nullptr and, if `error` is non-null, assign a description to `*error`. The + * single-argument overloads throw ValidationError on a type mismatch instead. + */ + static std::shared_ptr> Wrap(const ColumnMap& col, ValidationError* error) { + auto data = ArrayColumnType::Wrap(*col.data_, error); + if (!data) { + return nullptr; + } + return std::make_shared>(data); } - static auto Wrap(Column&& col) { return Wrap(std::move(dynamic_cast(col))); } + static std::shared_ptr> Wrap(const Column& col, ValidationError* error) { + if (auto* c = dynamic_cast(&col)) { + return Wrap(*c, error); + } + if (error) { + *error = ValidationError("Can't wrap column of type " + col.GetType().GetName() + " as Map"); + } + return nullptr; + } // Helper to simplify integration with other APIs - static auto Wrap(ColumnRef&& col) { return Wrap(std::move(*col->AsStrict())); } + static std::shared_ptr> Wrap(const ColumnRef& col, ValidationError* error) { + return Wrap(*col, error); + } + + // Throwing single-argument overloads (concrete type / Column& / ColumnRef&). + using WrappableColumn, ColumnMap>::Wrap; private: std::shared_ptr typed_data_; diff --git a/clickhouse/columns/nullable.cpp b/clickhouse/columns/nullable.cpp index 23940c12..bb9f295e 100644 --- a/clickhouse/columns/nullable.cpp +++ b/clickhouse/columns/nullable.cpp @@ -95,8 +95,10 @@ void ColumnNullable::Swap(Column& other) { if (!nested_->Type()->IsEqual(col.nested_->Type())) throw ValidationError("Can't swap() Nullable columns of different types."); - nested_.swap(col.nested_); - nulls_.swap(col.nulls_); + // Swap sub-column CONTENTS in place (never rebind the shared_ptr slots), so the nested and + // nulls objects keep their identity and any As<>/Wrap views of this column stay coherent. + nested_->Swap(*col.nested_); + nulls_->Swap(*col.nulls_); } ItemView ColumnNullable::GetItem(size_t index) const { diff --git a/clickhouse/columns/nullable.h b/clickhouse/columns/nullable.h index 6b34552c..eb9997e9 100644 --- a/clickhouse/columns/nullable.h +++ b/clickhouse/columns/nullable.h @@ -64,7 +64,7 @@ class ColumnNullable : public Column { }; template -class ColumnNullableT : public ColumnNullable { +class ColumnNullableT : public ColumnNullable, public WrappableColumn, ColumnNullable> { public: using NestedColumnType = ColumnType; using ValueType = std::optional().At(0))>>; @@ -108,25 +108,49 @@ class ColumnNullableT : public ColumnNullable { } } - /** Create a ColumnNullableT from a ColumnNullable, without copying data and offsets, but by - * 'stealing' those from `col`. + /** Create a ColumnNullableT that SHARES the internals of `col` (nested data and + * null map) via shared_ptr, WITHOUT stealing or copying them. * - * 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. + * The original `col` remains fully valid and usable. Both the original and the + * returned wrapper reference the same underlying columns, so mutations through + * one are visible through the other. * - * 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. + * The two-argument overloads are non-throwing: on a type mismatch they return + * nullptr and, if `error` is non-null, assign a description to `*error`. The + * single-argument overloads throw ValidationError on a type mismatch instead. */ - static auto Wrap(ColumnNullable&& col) { - return std::make_shared>( - col.Nested()->AsStrict(), - col.Nulls()->AsStrict()) ; + static std::shared_ptr> Wrap(const ColumnNullable& col, ValidationError* error) { + auto nested = WrapColumn(col.Nested(), error); + if (!nested) { + return nullptr; + } + auto nulls = col.Nulls()->As(); + if (!nulls) { + if (error) { + *error = ValidationError("Can't wrap Nullable column: unexpected null-map type"); + } + return nullptr; + } + return std::make_shared>(nested, nulls); } - static auto Wrap(Column&& col) { return Wrap(std::move(dynamic_cast(col))); } + static std::shared_ptr> Wrap(const Column& col, ValidationError* error) { + if (auto* c = dynamic_cast(&col)) { + return Wrap(*c, error); + } + if (error) { + *error = ValidationError("Can't wrap column of type " + col.GetType().GetName() + " as Nullable"); + } + return nullptr; + } // Helper to simplify integration with other APIs - static auto Wrap(ColumnRef&& col) { return Wrap(std::move(*col->AsStrict())); } + static std::shared_ptr> Wrap(const ColumnRef& col, ValidationError* error) { + return Wrap(*col, error); + } + + // Throwing single-argument overloads (concrete type / Column& / ColumnRef&). + using WrappableColumn, ColumnNullable>::Wrap; ColumnRef Slice(size_t begin, size_t size) const override { return Wrap(ColumnNullable::Slice(begin, size)); @@ -136,8 +160,9 @@ class ColumnNullableT : public ColumnNullable { void Swap(Column& other) override { auto& col = dynamic_cast&>(other); - typed_nested_data_.swap(col.typed_nested_data_); - ColumnNullable::Swap(other); + // Base swaps sub-column contents in place, preserving object identity, so the cached + // typed_nested_data_ still points at the correct object and must NOT be repointed. + ColumnNullable::Swap(col); } private: diff --git a/clickhouse/columns/numeric.cpp b/clickhouse/columns/numeric.cpp index cc33d19d..2fdf393f 100644 --- a/clickhouse/columns/numeric.cpp +++ b/clickhouse/columns/numeric.cpp @@ -1,5 +1,4 @@ #include "numeric.h" -#include "utils.h" #include "../base/wire_format.h" diff --git a/clickhouse/columns/string.cpp b/clickhouse/columns/string.cpp index 50581eea..022aaa43 100644 --- a/clickhouse/columns/string.cpp +++ b/clickhouse/columns/string.cpp @@ -1,5 +1,4 @@ #include "string.h" -#include "utils.h" #include "../base/wire_format.h" diff --git a/clickhouse/columns/tuple.cpp b/clickhouse/columns/tuple.cpp index 72d206b6..cde48f76 100644 --- a/clickhouse/columns/tuple.cpp +++ b/clickhouse/columns/tuple.cpp @@ -138,12 +138,33 @@ void ColumnTuple::SaveBody(OutputStream* output) { } void ColumnTuple::Clear() { - columns_.clear(); + // Clear each element column in place (do NOT drop the element handles): this preserves the + // tuple structure and the element objects' identity, so any As<>/Wrap views stay coherent. + for (auto & column : columns_) { + column->Clear(); + } } void ColumnTuple::Swap(Column& other) { auto & col = dynamic_cast(other); - columns_.swap(col.columns_); + if (columns_.size() != col.columns_.size()) { + throw ValidationError("Can't swap() Tuple columns of different sizes."); + } + + for (size_t i = 0; i < columns_.size(); ++i) { + if (!columns_[i]->Type()->IsEqual(col.columns_[i]->Type())) { + throw ValidationError( + "Can't swap() Tuple elements of types " + + columns_[i]->GetType().GetName() + " and " + + col.columns_[i]->GetType().GetName() + "."); + } + } + + // Swap each element's CONTENTS in place (never rebind the columns_ vector), so element + // objects keep their identity and any As<>/Wrap views of this column stay coherent. + for (size_t i = 0; i < columns_.size(); ++i) { + columns_[i]->Swap(*col.columns_[i]); + } } } diff --git a/clickhouse/columns/tuple.h b/clickhouse/columns/tuple.h index b6b0bbc7..0e8c396f 100644 --- a/clickhouse/columns/tuple.h +++ b/clickhouse/columns/tuple.h @@ -1,8 +1,8 @@ #pragma once #include "column.h" -#include "utils.h" +#include #include namespace clickhouse { @@ -62,7 +62,7 @@ class ColumnTuple : public Column { }; template -class ColumnTupleT : public ColumnTuple { +class ColumnTupleT : public ColumnTuple, public WrappableColumn, ColumnTuple> { public: using TupleOfColumns = std::tuple...>; @@ -98,27 +98,51 @@ class ColumnTupleT : public ColumnTuple { AppendTuple(std::move(value)); } - /** Create a ColumnTupleT from a ColumnTuple, without copying data and offsets, but by - * 'stealing' those from `col`. + /** Create a ColumnTupleT that SHARES the internals of `col` (its element columns) + * via shared_ptr, WITHOUT stealing or copying them. * - * 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. + * The original `col` remains fully valid and usable. Both the original and the + * returned wrapper reference the same underlying element columns, so mutations + * through one are visible through the other. * - * 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. + * The two-argument overloads are non-throwing: on a type mismatch they return + * nullptr and, if `error` is non-null, assign a description to `*error`. The + * single-argument overloads throw ValidationError on a type mismatch instead. */ - static auto Wrap(ColumnTuple&& col) { + static std::shared_ptr> Wrap(const ColumnTuple& col, ValidationError* error) { if (col.TupleSize() != std::tuple_size_v) { - throw ValidationError("Can't wrap from " + col.GetType().GetName()); + if (error) { + *error = ValidationError("Can't wrap from " + col.GetType().GetName()); + } + return nullptr; + } + auto columns = TupleFromColumn(col, error); + const bool all_wrapped = std::apply( + [](const auto&... column) { return (... && static_cast(column)); }, columns); + if (!all_wrapped) { + return nullptr; } auto names = col.Type()->As()->GetItemNames(); - return std::make_shared>(VectorToTuple(std::move(col)), std::move(names)); + return std::make_shared>(std::move(columns), std::move(names)); } - static auto Wrap(Column&& col) { return Wrap(std::move(dynamic_cast(col))); } + static std::shared_ptr> Wrap(const Column& col, ValidationError* error) { + if (auto* c = dynamic_cast(&col)) { + return Wrap(*c, error); + } + if (error) { + *error = ValidationError("Can't wrap column of type " + col.GetType().GetName() + " as Tuple"); + } + return nullptr; + } // Helper to simplify integration with other APIs - static auto Wrap(ColumnRef&& col) { return Wrap(std::move(*col->AsStrict())); } + static std::shared_ptr> Wrap(const ColumnRef& col, ValidationError* error) { + return Wrap(*col, error); + } + + // Throwing single-argument overloads (concrete type / Column& / ColumnRef&). + using WrappableColumn, ColumnTuple>::Wrap; ColumnRef Slice(size_t begin, size_t size) const override { return Wrap(ColumnTuple::Slice(begin, size)); @@ -128,8 +152,9 @@ class ColumnTupleT : public ColumnTuple { void Swap(Column& other) override { auto& col = dynamic_cast&>(other); - typed_columns_.swap(col.typed_columns_); - ColumnTuple::Swap(other); + // Base swaps element contents in place, preserving object identity, so the cached + // typed_columns_ still point at the correct objects and must NOT be repointed. + ColumnTuple::Swap(col); } private: @@ -159,6 +184,23 @@ class ColumnTupleT : public ColumnTuple { } } + // Builds a tuple of the element columns wrapped as their typed counterparts. Any element + // that can't be wrapped is left as a null shared_ptr (and `*error` is set if provided). + template > + inline static auto TupleFromColumn([[maybe_unused]] const ColumnTuple& col, + [[maybe_unused]] ValidationError* error) { + 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(col[column_index - 1], error); + return std::tuple_cat(TupleFromColumn(col, error), + std::make_tuple(std::move(column))); + } + } + template > inline static auto VectorToTuple([[maybe_unused]] T columns) { static_assert(column_index <= std::tuple_size_v); diff --git a/clickhouse/columns/utils.h b/clickhouse/columns/utils.h index 0fb8b99b..bcc8892d 100644 --- a/clickhouse/columns/utils.h +++ b/clickhouse/columns/utils.h @@ -1,41 +1,13 @@ #pragma once -#include -#include -#include - -namespace clickhouse { - -template -std::vector SliceVector(const std::vector& vec, size_t begin, size_t len) { - std::vector result; - - if (begin < vec.size()) { - len = std::min(len, vec.size() - begin); - result.assign(vec.begin() + begin, vec.begin() + (begin + 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(); - } -} - -} +// Deprecated header. The Wrap helpers (SliceVector, HasWrapMethod, WrapColumn) +// moved into columns/column.h. Include "clickhouse/columns/column.h" instead. +// This forwarding stub is kept for backward compatibility and will be removed +// in a future release. +#if defined(__GNUC__) || defined(__clang__) || (defined(_MSC_VER) && _MSC_VER >= 1929) +# warning "clickhouse/columns/utils.h is deprecated; include \"clickhouse/columns/column.h\" instead" +#else +# pragma message("clickhouse/columns/utils.h is deprecated; include \"clickhouse/columns/column.h\" instead") +#endif + +#include "column.h" diff --git a/clickhouse/columns/uuid.cpp b/clickhouse/columns/uuid.cpp index fbaff97d..85be389f 100644 --- a/clickhouse/columns/uuid.cpp +++ b/clickhouse/columns/uuid.cpp @@ -1,5 +1,4 @@ #include "uuid.h" -#include "utils.h" #include "../exceptions.h" #include diff --git a/clickhouse/exceptions.h b/clickhouse/exceptions.h index d2cb639c..00375820 100644 --- a/clickhouse/exceptions.h +++ b/clickhouse/exceptions.h @@ -14,6 +14,11 @@ class Error : public std::runtime_error { // Caused by any user-related code, like invalid column types or arguments passed to any method. class ValidationError : public Error { using Error::Error; + +public: + // Convenience default constructor, useful for creating an empty error to pass as an + // output parameter (e.g. to Column*T::Wrap(col, &error)). + ValidationError() : Error(std::string()) {} }; // Buffers+IO errors, failure to serialize/deserialize, checksum mismatches, etc. diff --git a/clickhouse/types/types.cpp b/clickhouse/types/types.cpp index 9b52255f..58318279 100644 --- a/clickhouse/types/types.cpp +++ b/clickhouse/types/types.cpp @@ -508,12 +508,18 @@ LowCardinalityType::~LowCardinalityType() { // Checks if `name` is a valid plain identifier (must not be quoted). // The condition for this is a match against `^[a-zA-Z_][0-9a-zA-Z_]*$` static bool IsPlainIdentifier(const std::string& name) { - if (name.empty()) return false; + if (name.empty()) { + return false; + } auto is_alpha_or_under = [](char c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'; }; auto is_alnum_or_under = [&is_alpha_or_under](char c) { return is_alpha_or_under(c) || (c >= '0' && c <= '9'); }; - if (!is_alpha_or_under(name[0])) return false; + if (!is_alpha_or_under(name[0])) { + return false; + } for (size_t i = 1; i < name.size(); ++i) - if (!is_alnum_or_under(name[i])) return false; + if (!is_alnum_or_under(name[i])) { + return false; + } return true; } diff --git a/ut/CreateColumnByType_ut.cpp b/ut/CreateColumnByType_ut.cpp index 279a19cc..fa13d00f 100644 --- a/ut/CreateColumnByType_ut.cpp +++ b/ut/CreateColumnByType_ut.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -45,6 +46,66 @@ TEST(CreateColumnByType, LowCardinalityAsWrappedColumn) { ASSERT_EQ(Type::FixedString, CreateColumnByType("LowCardinality(FixedString(10000))", create_column_settings)->As()->GetType().GetCode()); } +TEST(CreateColumnByType, LowCardinality) { + // In the default (non-wrapped) mode, LowCardinality(String)/LowCardinality(FixedString) map to + // the base ColumnLowCardinality (like Array/Nullable/Tuple/Map do), and the strongly-typed + // ColumnLowCardinalityT<...> view is obtained on demand via the wrapping As<>. + { + auto col = CreateColumnByType("LowCardinality(String)"); + ASSERT_NE(nullptr, col); + EXPECT_EQ("LowCardinality(String)", col->GetType().GetName()); + // Concrete type is the base ColumnLowCardinality, not ColumnLowCardinalityT<...>. + EXPECT_NE(nullptr, col->As()); + // The wrapping As<> yields the strongly-typed view. + EXPECT_NE(nullptr, col->As>()); + } + { + auto col = CreateColumnByType("LowCardinality(FixedString(10000))"); + ASSERT_NE(nullptr, col); + EXPECT_EQ("LowCardinality(FixedString(10000))", col->GetType().GetName()); + EXPECT_NE(nullptr, col->As()); + EXPECT_NE(nullptr, col->As>()); + } +} + +TEST(CreateColumnByType, LowCardinalityNullable) { + // The factory builds LowCardinality(Nullable(String)) as a base ColumnLowCardinality whose + // dictionary is a base ColumnNullable (not a typed ColumnNullableT). The wrapping + // As<> must still produce the strongly-typed view by wrapping that base dictionary into the + // typed one, sharing its underlying storage. + using TypedLC = ColumnLowCardinalityT>; + + auto col = CreateColumnByType("LowCardinality(Nullable(String))"); + ASSERT_NE(nullptr, col); + EXPECT_EQ("LowCardinality(Nullable(String))", col->GetType().GetName()); + EXPECT_NE(nullptr, col->As()); + + auto typed = col->As(); + ASSERT_NE(nullptr, typed); + + // Appends through the typed view are visible via the base handle (shared storage), and the + // typed accessors round-trip both real values and nulls. + typed->Append(std::string("abc")); + typed->Append(std::nullopt); + typed->Append(std::string("abc")); + + EXPECT_EQ(3u, typed->Size()); + EXPECT_EQ(3u, col->Size()); + EXPECT_EQ(std::optional("abc"), typed->At(0)); + EXPECT_EQ(std::nullopt, typed->At(1)); + EXPECT_EQ(std::optional("abc"), typed->At(2)); + + // A second independent wrap of the same base column observes the same shared data. + auto typed2 = col->As(); + ASSERT_NE(nullptr, typed2); + EXPECT_EQ(3u, typed2->Size()); + EXPECT_EQ(std::optional("abc"), typed2->At(0)); + EXPECT_EQ(std::nullopt, typed2->At(1)); + + // A dictionary whose nested type does not match must still be rejected. + EXPECT_EQ(nullptr, col->As>>()); +} + TEST(CreateColumnByType, DateTime) { ASSERT_NE(nullptr, CreateColumnByType("DateTime")); ASSERT_NE(nullptr, CreateColumnByType("DateTime('Europe/Moscow')")); @@ -162,6 +223,8 @@ INSTANTIATE_TEST_SUITE_P(Parametrized, CreateColumnByTypeWithName, ::testing::Va INSTANTIATE_TEST_SUITE_P(Nested, CreateColumnByTypeWithName, ::testing::Values( "Nullable(FixedString(10000))", + "LowCardinality(String)", + "LowCardinality(FixedString(10000))", "Nullable(LowCardinality(FixedString(10000)))", "Array(Nullable(LowCardinality(FixedString(10000))))", "Array(Enum8('ONE' = 1, 'TWO' = 2))" diff --git a/ut/client_ut.cpp b/ut/client_ut.cpp index dff1400e..6dce77d1 100644 --- a/ut/client_ut.cpp +++ b/ut/client_ut.cpp @@ -1522,7 +1522,9 @@ TEST_P(ClientCase, InteractiveSelect_Basic) { std::vector values; while (auto block = client_->NextBlock()) { - if (block->GetRowCount() == 0) continue; + if (block->GetRowCount() == 0) { + continue; + } auto col = block->At(0)->AsStrict(); for (size_t i = 0; i < block->GetRowCount(); ++i) { values.push_back(col->At(i)); @@ -1561,7 +1563,9 @@ TEST_P(ClientCase, InteractiveSelect_MultipleBlocks) { size_t block_count = 0; std::vector values; while (auto block = client_->NextBlock()) { - if (block->GetRowCount() == 0) continue; + if (block->GetRowCount() == 0) { + continue; + } EXPECT_LE(block->GetRowCount(), 2u); block_count++; auto col = block->At(0)->AsStrict(); @@ -1587,7 +1591,9 @@ TEST_P(ClientCase, InteractiveSelect_Cancel) { // Consume one block of data, skipping any blocks with 0 rows. size_t rows_before_cancel = 0; while (auto b = client_->NextBlock()) { - if (b->GetRowCount() == 0) continue; + if (b->GetRowCount() == 0) { + continue; + } rows_before_cancel = b->GetRowCount(); break; } diff --git a/ut/column_array_ut.cpp b/ut/column_array_ut.cpp index 0fc28d4f..04e42ca5 100644 --- a/ut/column_array_ut.cpp +++ b/ut/column_array_ut.cpp @@ -337,6 +337,86 @@ TEST(ColumnArrayT, Wrap_UInt64_2D) { EXPECT_TRUE(CompareRecursive(values, array)); } +TEST(ColumnArrayT, Wrap_AcceptsLvalue) { + // Wrap no longer requires an rvalue: lvalues and const sources are accepted. + + const std::vector> values = { + {1u, 2u}, + {3u}, + {} + }; + + auto arr = CreateArray(values); + + // Lvalue ColumnRef, no std::move required and not consumed. + ColumnRef ref = arr; + auto w1 = ColumnArrayT::Wrap(ref); + EXPECT_TRUE(CompareRecursive(values, *w1)); + EXPECT_NE(ref, nullptr); + + // Const lvalue concrete column. + const ColumnArray& cref = *arr; + auto w2 = ColumnArrayT::Wrap(cref); + EXPECT_TRUE(CompareRecursive(values, *w2)); + + // Non-const lvalue concrete column. + auto w3 = ColumnArrayT::Wrap(*arr); + EXPECT_TRUE(CompareRecursive(values, *w3)); +} + +TEST(ColumnArrayT, Wrap_DoesNotStealSource_UInt64) { + // Wrap shares storage with the source ColumnArray and leaves its contents intact. + + const std::vector> values = { + {1u, 2u, 3u}, + {4u, 5u, 6u, 7u, 8u, 9u}, + {0u}, + {}, + {13, 14} + }; + + auto original = CreateArray(values); + // Keep an independent handle to the same underlying ColumnArray. + auto keep = original; + auto wrapped_array = ColumnArrayT::Wrap(std::move(original)); + + // Wrapper sees the same data. + EXPECT_TRUE(CompareRecursive(values, *wrapped_array)); + + // Source array contents are left intact (not stolen from). + ASSERT_NE(keep, nullptr); + EXPECT_EQ(keep->Size(), values.size()); + + // Storage is shared: appending a row through the source is visible via the wrapper. + keep->AppendAsColumn(std::make_shared(std::vector{42, 43})); + EXPECT_EQ(wrapped_array->Size(), values.size() + 1); + EXPECT_EQ(wrapped_array->At(values.size()).At(0), 42u); + EXPECT_EQ(wrapped_array->At(values.size()).At(1), 43u); +} + +TEST(ColumnArrayT, Wrap_DoesNotStealSource_UInt64_2D) { + // Wrap shares all nesting layers with the source. + + const std::vector>> values = { + {{1u, 2u}, {3u}}, + {{4u}, {5u, 6u, 7u}, {8u, 9u}, {}}, + {{0u}}, + {{}}, + {{13}, {14, 15}} + }; + + auto original = Create2DArray(values); + auto keep = original; + auto wrapped_array = ColumnArrayT>::Wrap(std::move(original)); + + EXPECT_TRUE(CompareRecursive(values, *wrapped_array)); + + // Source array contents are left intact (not stolen from). + ASSERT_NE(keep, nullptr); + EXPECT_EQ(keep->Size(), values.size()); + EXPECT_TRUE(CompareRecursive(values, *ColumnArrayT>::Wrap(std::move(keep)))); +} + TEST(ColumnArrayT, Bool) { // Check inserting\reading back data from clickhouse::ColumnArrayT diff --git a/ut/columns_ut.cpp b/ut/columns_ut.cpp index d79debbe..3c3b292b 100644 --- a/ut/columns_ut.cpp +++ b/ut/columns_ut.cpp @@ -1088,6 +1088,132 @@ TEST(ColumnsCase, ColumnLowCardinalityString_Append_and_Read) { } } +TEST(ColumnsCase, ColumnLowCardinalityT_Wrap_DoesNotStealSource) { + // Populate via the typed column (the only ergonomic per-value insert path), then Wrap it + // through an untyped ColumnRef handle, as with a column received from a query. + auto source = std::make_shared>(); + source->Append("a"); + source->Append("b"); + source->Append("a"); + + ColumnRef untyped = source; + auto wrapped = ColumnLowCardinalityT::Wrap(untyped); + + // Wrapper reads the same data. + ASSERT_EQ(wrapped->Size(), 3u); + EXPECT_EQ(wrapped->At(0), "a"); + EXPECT_EQ(wrapped->At(1), "b"); + EXPECT_EQ(wrapped->At(2), "a"); + + // Source (and the untyped handle) are left intact after Wrap (non-stealing). + EXPECT_NE(untyped, nullptr); + ASSERT_EQ(source->Size(), 3u); + EXPECT_EQ(source->At(0), "a"); + EXPECT_EQ(source->At(2), "a"); + + // Storage (dictionary + index + dedup map) is shared and stays coherent: + // a new unique value appended via the source, and a repeat appended via the wrapper. + const auto dict_before = source->GetDictionarySize(); + source->Append("c"); // new unique -> dictionary grows, visible via the wrapper + wrapped->Append("a"); // repeat -> deduped against the shared map, no dictionary growth + + EXPECT_EQ(source->Size(), 5u); + EXPECT_EQ(wrapped->Size(), 5u); + EXPECT_EQ(wrapped->At(3), "c"); + EXPECT_EQ(wrapped->At(4), "a"); + EXPECT_EQ(source->At(4), "a"); + // "c" added exactly one dictionary entry; "a" added none (shared dedup map). + EXPECT_EQ(source->GetDictionarySize(), dict_before + 1); + EXPECT_EQ(wrapped->GetDictionarySize(), dict_before + 1); +} + +TEST(ColumnsCase, ColumnLowCardinalityT_Swap_VisibleThroughAlias) { + auto a = std::make_shared>(); + a->Append("a"); + a->Append("b"); + a->Append("a"); + auto b = std::make_shared>(); + b->Append("x"); + + // Typed view of `a`, created BEFORE the swap; shares a's dictionary and index bundle. + auto alias_a = ColumnLowCardinalityT::Wrap(a); + ASSERT_NE(alias_a, nullptr); + ASSERT_EQ(alias_a->Size(), 3u); + + a->Swap(*b); + + // a now holds b's data, b holds a's data. + ASSERT_EQ(a->Size(), 1u); + EXPECT_EQ(a->At(0), "x"); + ASSERT_EQ(b->Size(), 3u); + EXPECT_EQ(b->At(0), "a"); + EXPECT_EQ(b->At(1), "b"); + + // The swap is visible through the pre-existing alias: dictionary swapped in place and the + // shared index bundle swapped, so alias_a reflects b's data (size flips 3 -> 1). + ASSERT_EQ(alias_a->Size(), 1u); + EXPECT_EQ(alias_a->At(0), "x"); +} + +TEST(ColumnsCase, ColumnLowCardinalityT_Wrap_ThenLoad_VisibleThroughAlias) { + // Serialize a source LowCardinality column. + ColumnLowCardinalityT src; + src.Append("p"); + src.Append("q"); + src.Append("p"); + + char buffer[256] = {'\0'}; + { + ArrayOutput output(buffer, sizeof(buffer)); + EXPECT_NO_THROW(src.Save(&output)); + } + + // A different target column, wrapped BEFORE the load. + auto target = std::make_shared>(); + target->Append("z"); + auto alias = ColumnLowCardinalityT::Wrap(target); + ASSERT_NE(alias, nullptr); + ASSERT_EQ(alias->Size(), 1u); + + // Load src's data into target: LoadBody swaps the dictionary in place and REPLACES the index + // column inside the shared bundle. + { + ArrayInput input(buffer, sizeof(buffer)); + EXPECT_TRUE(target->Load(&input, 3)); + } + + ASSERT_EQ(target->Size(), 3u); + // The pre-existing alias reflects the loaded data (shared dictionary + index bundle). + ASSERT_EQ(alias->Size(), 3u); + EXPECT_EQ(alias->At(0), "p"); + EXPECT_EQ(alias->At(1), "q"); + EXPECT_EQ(alias->At(2), "p"); +} + +TEST(ColumnsCase, ColumnLowCardinalityT_Wrap_AcceptsLvalue) { + auto source = std::make_shared>(); + source->Append("x"); + source->Append("y"); + + using LC = ColumnLowCardinalityT; + + // Non-const lvalue (untyped base reference), no std::move required. + ColumnLowCardinality& base = *source; + auto w1 = LC::Wrap(base); + EXPECT_EQ(w1->At(0), "x"); + + // Const lvalue. + const ColumnLowCardinality& cref = *source; + auto w2 = LC::Wrap(cref); + EXPECT_EQ(w2->At(1), "y"); + + // Lvalue ColumnRef, no std::move required and not consumed. + ColumnRef ref = source; + auto w3 = LC::Wrap(ref); + EXPECT_EQ(w3->Size(), 2u); + EXPECT_NE(ref, nullptr); +} + TEST(ColumnsCase, ColumnLowCardinalityString_Clear_and_Append) { const size_t items_count = 11; ColumnLowCardinalityT col; @@ -1254,6 +1380,61 @@ TEST(ColumnsCase, ColumnTupleT) { EXPECT_EQ(val, col.At(0)); } +TEST(ColumnsCase, ColumnNullableT_Wrap_DoesNotStealSource) { + auto nested = std::make_shared(); + auto nulls = std::make_shared(); + ColumnNullable col(nested, nulls); + + col.Append(false); + nested->Append(1); + col.Append(true); + nested->Append(0); + + using TestNullable = ColumnNullableT; + auto wrapped = TestNullable::Wrap(std::move(col)); + + // Wrapper sees the same data. + EXPECT_EQ(wrapped->Size(), 2u); + EXPECT_EQ(wrapped->At(0), std::optional(1)); + EXPECT_EQ(wrapped->At(1), std::optional{}); + + // Source column is left intact after Wrap (non-stealing). + EXPECT_EQ(col.Size(), 2u); + EXPECT_FALSE(col.IsNull(0)); + EXPECT_TRUE(col.IsNull(1)); + + // Storage is shared: appending through the original is visible via the wrapper. + col.Append(false); + nested->Append(42); + EXPECT_EQ(wrapped->Size(), 3u); + EXPECT_EQ(wrapped->At(2), std::optional(42)); +} + +TEST(ColumnsCase, ColumnNullableT_Wrap_AcceptsLvalue) { + auto nested = std::make_shared(); + auto nulls = std::make_shared(); + ColumnNullable col(nested, nulls); + col.Append(false); + nested->Append(7); + + using TestNullable = ColumnNullableT; + + // Non-const lvalue concrete column, no std::move required. + auto w1 = TestNullable::Wrap(col); + EXPECT_EQ(w1->At(0), std::optional(7)); + + // Const lvalue concrete column. + const ColumnNullable& cref = col; + auto w2 = TestNullable::Wrap(cref); + EXPECT_EQ(w2->At(0), std::optional(7)); + + // Lvalue ColumnRef, no std::move required and not consumed. + ColumnRef ref = std::make_shared(nested, nulls); + auto w3 = TestNullable::Wrap(ref); + EXPECT_EQ(w3->At(0), std::optional(7)); + EXPECT_NE(ref, nullptr); +} + TEST(ColumnsCase, ColumnTupleT_Wrap) { ColumnTuple col ({ std::make_shared(), @@ -1275,6 +1456,84 @@ TEST(ColumnsCase, ColumnTupleT_Wrap) { EXPECT_EQ(val, wrapped_col->At(0)); } +TEST(ColumnsCase, ColumnTupleT_Wrap_DoesNotStealSource) { + 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 = TestTuple::Wrap(std::move(col)); + + // Wrapper sees the same data. + EXPECT_EQ(wrapped->Size(), 1u); + EXPECT_EQ(val, wrapped->At(0)); + + // Source column is left intact after Wrap (non-stealing). + EXPECT_EQ(col.TupleSize(), 3u); + EXPECT_EQ(col.Size(), 1u); + + // Storage is shared: appending through the original element columns is visible via the wrapper. + col[0]->AsStrict()->Append(2); + col[1]->AsStrict()->Append("xy"); + col[2]->AsStrict()->Append("zzz"); + EXPECT_EQ(wrapped->Size(), 2u); + EXPECT_EQ(std::make_tuple(2, "xy", "zzz"), wrapped->At(1)); +} + +TEST(ColumnsCase, ColumnTupleT_Wrap_DoesNotStealSource_PreservesNames) { + ColumnTuple base( + {std::make_shared(), std::make_shared()}, + {"id", "name"} + ); + + using TestTuple = ColumnTupleT; + auto wrapped = TestTuple::Wrap(std::move(base)); + EXPECT_EQ(wrapped->Type()->GetName(), "Tuple(id UInt64, name String)"); + + // Source remains usable after Wrap (non-stealing). + EXPECT_EQ(base.Type()->GetName(), "Tuple(id UInt64, name String)"); + EXPECT_EQ(base.TupleSize(), 2u); +} + +TEST(ColumnsCase, ColumnTupleT_Wrap_AcceptsLvalue) { + ColumnTuple col({ + std::make_shared(), + std::make_shared() + }); + col[0]->AsStrict()->Append(1); + col[1]->AsStrict()->Append("a"); + + using TestTuple = ColumnTupleT; + + // Non-const lvalue concrete column, no std::move required. + auto w1 = TestTuple::Wrap(col); + EXPECT_EQ(w1->At(0), std::make_tuple(uint64_t(1), std::string_view("a"))); + + // Const lvalue concrete column. + const ColumnTuple& cref = col; + auto w2 = TestTuple::Wrap(cref); + EXPECT_EQ(w2->At(0), std::make_tuple(uint64_t(1), std::string_view("a"))); + + // Lvalue ColumnRef, no std::move required and not consumed. + ColumnRef ref = std::make_shared(std::vector{col[0], col[1]}); + auto w3 = TestTuple::Wrap(ref); + EXPECT_EQ(w3->At(0), std::make_tuple(uint64_t(1), std::string_view("a"))); + EXPECT_NE(ref, nullptr); + + // Source column left intact. + EXPECT_EQ(col.TupleSize(), 2u); + EXPECT_EQ(col.Size(), 1u); +} + TEST(ColumnsCase, ColumnTupleT_Empty) { using TestTuple = ColumnTupleT<>; @@ -1311,6 +1570,358 @@ TEST(ColumnsCase, ColumnTupleT_Wrap_PreservesNames) { EXPECT_EQ(wrapped->Type()->GetName(), "Tuple(id UInt64, name String)"); } +// --- Swap/Clear must stay coherent with As<>/Wrap views (contents swapped/cleared in place) --- + +TEST(ColumnsCase, ColumnArrayT_Swap_VisibleThroughAlias) { + auto a = std::make_shared>(); + a->Append(std::vector{1, 2, 3}); + auto b = std::make_shared>(); + b->Append(std::vector{7, 8}); + + // Alias sharing a's storage. + auto alias_a = ColumnArrayT::Wrap(a); + ASSERT_NE(alias_a, nullptr); + + a->Swap(*b); + + // a now holds b's row, b holds a's row. + ASSERT_EQ(a->Size(), 1u); + EXPECT_EQ(a->At(0).size(), 2u); + ASSERT_EQ(b->Size(), 1u); + EXPECT_EQ(b->At(0).size(), 3u); + + // The swap is visible through the alias (sub-object identity preserved). + ASSERT_EQ(alias_a->Size(), 1u); + EXPECT_EQ(alias_a->At(0).size(), 2u); + EXPECT_EQ(alias_a->At(0)[0], 7u); +} + +TEST(ColumnsCase, ColumnNullableT_Swap_VisibleThroughAlias) { + auto a = std::make_shared>(); + a->Append(1); + a->Append(std::nullopt); + auto b = std::make_shared>(); + b->Append(42); + + auto alias_a = ColumnNullableT::Wrap(a); + ASSERT_NE(alias_a, nullptr); + + a->Swap(*b); + + ASSERT_EQ(a->Size(), 1u); + EXPECT_EQ(a->At(0), std::optional(42)); + ASSERT_EQ(b->Size(), 2u); + + // Visible through the alias. + ASSERT_EQ(alias_a->Size(), 1u); + EXPECT_EQ(alias_a->At(0), std::optional(42)); +} + +TEST(ColumnsCase, ColumnTupleT_Swap_VisibleThroughAlias) { + ColumnTuple a({std::make_shared(), std::make_shared()}); + a[0]->AsStrict()->Append(1); + a[1]->AsStrict()->Append("a"); + + ColumnTuple b({std::make_shared(), std::make_shared()}); + b[0]->AsStrict()->Append(2); + b[1]->AsStrict()->Append("b"); + + using TestTuple = ColumnTupleT; + auto alias_a = TestTuple::Wrap(a); + ASSERT_NE(alias_a, nullptr); + + a.Swap(b); + + EXPECT_EQ(a.Size(), 1u); + EXPECT_EQ(a[0]->AsStrict()->At(0), 2u); + EXPECT_EQ(b[0]->AsStrict()->At(0), 1u); + + // Visible through the alias. + EXPECT_EQ(alias_a->At(0), std::make_tuple(uint64_t(2), std::string_view("b"))); +} + +TEST(ColumnsCase, ColumnTuple_Swap_DifferentSizeThrows) { + ColumnTuple a({std::make_shared(), std::make_shared()}); + ColumnTuple b({std::make_shared()}); + EXPECT_THROW(a.Swap(b), ValidationError); +} + +TEST(ColumnsCase, ColumnTuple_Swap_DifferentElementTypeDoesNotModify) { + auto a0 = std::make_shared(); + auto a1 = std::make_shared(); + a0->Append(1); + a1->Append("a"); + ColumnTuple a({a0, a1}); + + auto b0 = std::make_shared(); + auto b1 = std::make_shared(); + b0->Append(2); + b1->Append(3); + ColumnTuple b({b0, b1}); + + EXPECT_THROW(a.Swap(b), ValidationError); + + EXPECT_EQ(a0->At(0), 1u); + EXPECT_EQ(a1->At(0), "a"); + EXPECT_EQ(b0->At(0), 2u); + EXPECT_EQ(b1->At(0), 3u); +} + +TEST(ColumnsCase, ColumnTuple_Clear_PreservesStructure_AndAlias) { + ColumnTuple col({std::make_shared(), std::make_shared()}); + col[0]->AsStrict()->Append(1); + col[1]->AsStrict()->Append("a"); + + using TestTuple = ColumnTupleT; + auto alias = TestTuple::Wrap(col); + ASSERT_NE(alias, nullptr); + ASSERT_EQ(alias->Size(), 1u); + + col.Clear(); + + // Structure is preserved (columns not dropped) and data is cleared in place. + EXPECT_EQ(col.Size(), 0u); + EXPECT_EQ(col.TupleSize(), 2u); + // Clear propagates to the alias. + EXPECT_EQ(alias->Size(), 0u); + + // Re-appending through the original element columns is visible via the alias. + col[0]->AsStrict()->Append(2); + col[1]->AsStrict()->Append("b"); + ASSERT_EQ(alias->Size(), 1u); + EXPECT_EQ(alias->At(0), std::make_tuple(uint64_t(2), std::string_view("b"))); +} + +TEST(ColumnsCase, ColumnMapT_Swap_VisibleThroughAlias) { + using TestMap = ColumnMapT; + + auto a = std::make_shared(std::make_shared(), std::make_shared()); + a->Append(std::map{{"x", 1}}); + auto b = std::make_shared(std::make_shared(), std::make_shared()); + b->Append(std::map{{"y", 2}, {"z", 3}}); + + auto alias_a = TestMap::Wrap(a); + ASSERT_NE(alias_a, nullptr); + + a->Swap(*b); + + ASSERT_EQ(a->Size(), 1u); + EXPECT_EQ(a->At(0).size(), 2u); + ASSERT_EQ(b->Size(), 1u); + EXPECT_EQ(b->At(0).size(), 1u); + + // Visible through the alias. + ASSERT_EQ(alias_a->Size(), 1u); + EXPECT_EQ(alias_a->At(0).size(), 2u); + EXPECT_EQ(alias_a->At(0)["y"], 2u); +} + +// --- Deep-nested aliases: in-place Swap/Clear must recurse to shared leaf objects --- + +namespace { +// Builds a single-row Map(UInt64, Array(Nullable(String))): one map row with one entry +// {key -> arr}. Uses the single-offset ColumnArray ctor so the backing array has exactly one row. +std::shared_ptr MakeDeepMapRow(uint64_t key, std::vector> arr) { + auto keys = std::make_shared(); + auto vals = std::make_shared>>(); + keys->Append(key); + vals->Append(arr); + auto tuple = std::make_shared(std::vector{keys, vals}); + return std::make_shared(std::make_shared(tuple)); +} +} + +TEST(ColumnsCase, DeepMap_Swap_VisibleThroughAlias) { + using DeepMap = ColumnMapT>>; + + auto a = MakeDeepMapRow(1, {std::string("a"), std::string("b"), std::string("c")}); + auto b = MakeDeepMapRow(1, {std::string("x"), std::nullopt}); + + auto alias_a = DeepMap::Wrap(a); // deep typed view created BEFORE the swap + ASSERT_NE(alias_a, nullptr); + ASSERT_EQ(alias_a->At(0).At(1).Size(), 3u); + + a->Swap(*b); + + // The swap recurses down to the shared leaf columns, so the pre-existing alias now + // reflects B's data: the value-array flips size 3 -> 2 and the null survives. + auto arr = alias_a->At(0).At(1); + ASSERT_EQ(arr.Size(), 2u); + EXPECT_EQ(arr[0], std::optional("x")); + EXPECT_EQ(arr[1], std::optional{}); + + // b now holds a's original data. + auto arr_b = DeepMap::Wrap(b)->At(0).At(1); + EXPECT_EQ(arr_b.Size(), 3u); + EXPECT_EQ(arr_b[0], std::optional("a")); +} + +TEST(ColumnsCase, DeepMap_Clear_VisibleThroughAlias_NoStaleEntries) { + using DeepMap = ColumnMapT>>; + + auto a = MakeDeepMapRow(1, {std::string("a"), std::string("b"), std::string("c")}); + auto alias_a = DeepMap::Wrap(a); + ASSERT_NE(alias_a, nullptr); + ASSERT_EQ(alias_a->Size(), 1u); + + a->Clear(); + + // Clear recurses to the shared leaves in place, so the alias goes empty too. + EXPECT_EQ(a->Size(), 0u); + EXPECT_EQ(alias_a->Size(), 0u); + + // Re-appending a fresh row must not resurface the pre-clear ["a","b","c"] entry. + a->Append(MakeDeepMapRow(1, {std::string("z")})); + ASSERT_EQ(alias_a->Size(), 1u); + auto arr = alias_a->At(0).At(1); + ASSERT_EQ(arr.Size(), 1u); + EXPECT_EQ(arr[0], std::optional("z")); +} + +TEST(ColumnsCase, DeepNestedArray_Swap_VisibleThroughAlias) { + using DeepArray = ColumnArrayT>>; + + auto a = std::make_shared(); + a->Append(std::vector>>{ + {std::string("a")}, {std::string("b"), std::string("c")}}); + auto b = std::make_shared(); + b->Append(std::vector>>{ + {std::string("x"), std::nullopt}}); + + auto alias_a = DeepArray::Wrap(a); // created BEFORE the swap + ASSERT_NE(alias_a, nullptr); + ASSERT_EQ(alias_a->At(0).Size(), 2u); + + a->Swap(*b); + + // Alias reflects B's data all the way down to the nullable-string leaves. + auto outer = alias_a->At(0); + ASSERT_EQ(outer.Size(), 1u); + auto inner = outer.At(0); + ASSERT_EQ(inner.Size(), 2u); + EXPECT_EQ(inner[0], std::optional("x")); + EXPECT_EQ(inner[1], std::optional{}); +} + +// --- Deep nesting with LowCardinality: Map(UInt64, Array(LowCardinality(Nullable(String)))) --- + +namespace { +// Builds a single-row Map(UInt64, Array(LowCardinality(Nullable(String)))): one map row with one +// entry {key -> arr}, where the value is an array of LowCardinality(Nullable(String)) items. +std::shared_ptr MakeDeepLcMapRow(uint64_t key, std::vector> arr) { + auto keys = std::make_shared(); + auto vals = std::make_shared>>>(); + keys->Append(key); + vals->Append(arr); + auto tuple = std::make_shared(std::vector{keys, vals}); + return std::make_shared(std::make_shared(tuple)); +} +} + +TEST(ColumnsCase, DeepMapArrayLowCardinality_Swap_VisibleThroughAlias) { + using DeepLcMap = ColumnMapT>>>; + + auto a = MakeDeepLcMapRow(1, {std::string("a"), std::string("b"), std::string("c")}); + auto b = MakeDeepLcMapRow(1, {std::string("x"), std::nullopt}); + + auto alias_a = DeepLcMap::Wrap(a); // deep typed view created BEFORE the swap + ASSERT_NE(alias_a, nullptr); + ASSERT_EQ(alias_a->At(0).At(1).Size(), 3u); + + a->Swap(*b); + + // The swap recurses through Array -> LowCardinality (dictionary swapped in place, index bundle + // swapped) down to the Nullable(String) leaves, so the pre-existing alias reflects B's data: + // the value-array flips size 3 -> 2 and the null survives through LC -> Nullable. + auto arr = alias_a->At(0).At(1); + ASSERT_EQ(arr.Size(), 2u); + EXPECT_EQ(arr[0], std::optional("x")); + EXPECT_EQ(arr[1], std::optional{}); + + // b now holds a's original data. + auto arr_b = DeepLcMap::Wrap(b)->At(0).At(1); + ASSERT_EQ(arr_b.Size(), 3u); + EXPECT_EQ(arr_b[0], std::optional("a")); +} + +TEST(ColumnsCase, DeepMapArrayLowCardinality_Clear_VisibleThroughAlias) { + using DeepLcMap = ColumnMapT>>>; + + auto a = MakeDeepLcMapRow(1, {std::string("a"), std::string("b"), std::string("c")}); + auto alias_a = DeepLcMap::Wrap(a); + ASSERT_NE(alias_a, nullptr); + ASSERT_EQ(alias_a->Size(), 1u); + + a->Clear(); + + // Clear recurses to the shared leaves in place (incl. LowCardinality), so the alias empties too. + EXPECT_EQ(a->Size(), 0u); + EXPECT_EQ(alias_a->Size(), 0u); + + // Re-appending a fresh row must not resurface the pre-clear entry. + a->Append(MakeDeepLcMapRow(1, {std::string("z")})); + ASSERT_EQ(alias_a->Size(), 1u); + auto arr = alias_a->At(0).At(1); + ASSERT_EQ(arr.Size(), 1u); + EXPECT_EQ(arr[0], std::optional("z")); +} + +// --- const As()/AsStrict() wrap like their non-const counterparts --- + +TEST(ColumnsCase, Const_As_WrapsWrappableColumn) { + using TestArray = ColumnArrayT; + + auto arr = std::make_shared(); + arr->Append(std::vector{1, 2, 3}); + + // View the column only through a const handle. + std::shared_ptr c = arr; + auto view = c->As(); + ASSERT_NE(view, nullptr); + static_assert(std::is_same_v>, + "const As() must yield shared_ptr"); + + // Read access reflects the same shared storage. + ASSERT_EQ(view->Size(), 1u); + auto row = view->At(0); + ASSERT_EQ(row.Size(), 3u); + EXPECT_EQ(row[0], 1u); + EXPECT_EQ(row[2], 3u); +} + +TEST(ColumnsCase, Const_As_ExactDowncastStillWorks) { + auto leaf = std::make_shared(); + leaf->Append(42u); + + std::shared_ptr c = leaf; + auto exact = c->As(); + ASSERT_NE(exact, nullptr); + ASSERT_EQ(exact->Size(), 1u); + EXPECT_EQ(exact->At(0), 42u); + + // Non-wrappable mismatch returns nullptr (no throw). + EXPECT_EQ(c->As(), nullptr); +} + +TEST(ColumnsCase, Const_AsStrict_WrapsAndThrows) { + using TestArray = ColumnArrayT; + + auto arr = std::make_shared(); + arr->Append(std::vector{7, 8}); + + std::shared_ptr c = arr; + auto view = c->AsStrict(); + ASSERT_NE(view, nullptr); + static_assert(std::is_same_v>, + "const AsStrict() must yield shared_ptr"); + ASSERT_EQ(view->At(0).Size(), 2u); + + // Wrappable-but-incompatible element type throws. + EXPECT_THROW((void)c->AsStrict>(), ValidationError); + // Non-wrappable mismatch throws too. + EXPECT_THROW((void)c->AsStrict(), ValidationError); +} + TEST(ColumnsCase, ColumnTupleT_Slice_PreservesNames) { using TestTuple = ColumnTupleT; @@ -1381,3 +1992,179 @@ TEST(ColumnsCase, ColumnMapT_Wrap) { EXPECT_EQ("123", map_view.At(1)); EXPECT_EQ("abc", map_view.At(2)); } + +TEST(ColumnsCase, ColumnMapT_Wrap_AcceptsLvalue) { + 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"); + data->AppendAsColumn(val); + + ColumnMap col{data}; + + using TestMap = ColumnMapT; + + // Non-const lvalue concrete column, no std::move required. + auto w1 = TestMap::Wrap(col); + EXPECT_EQ("123", w1->At(0).At(1)); + + // Const lvalue concrete column. + const ColumnMap& cref = col; + auto w2 = TestMap::Wrap(cref); + EXPECT_EQ("123", w2->At(0).At(1)); + + // Lvalue ColumnRef, no std::move required and not consumed. + ColumnRef ref = std::make_shared(data); + auto w3 = TestMap::Wrap(ref); + EXPECT_EQ("123", w3->At(0).At(1)); + EXPECT_NE(ref, nullptr); + + // Source column left intact. + EXPECT_EQ(col.Size(), 1u); +} + +TEST(ColumnsCase, ColumnMapT_Wrap_DoesNotStealSource) { + 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)); + + // Wrapper sees the same data. + 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)); + + // Source column is left intact after Wrap (non-stealing). + EXPECT_EQ(col.Size(), 1u); + + // Storage is shared: appending a row through the original is visible via the wrapper. + auto val2 = tupls->CloneEmpty()->As(); + (*val2)[0]->AsStrict()->Append(7); + (*val2)[1]->AsStrict()->Append("xyz"); + data->AppendAsColumn(val2); + + EXPECT_EQ(wrapped_col->Size(), 2u); + EXPECT_EQ("xyz", wrapped_col->At(1).At(7)); +} + +// --- Wrap error-reporting overloads --------------------------------------------------------- +// The two-argument Wrap(col, ValidationError*) returns nullptr (never throws) on a type +// mismatch; the single-argument Wrap(col) throws ValidationError on the same mismatch. + +TEST(ColumnsCase, ColumnArrayT_Wrap_TypeMismatch) { + using TestArray = ColumnArrayT; + + // Right kind (Array), wrong element type (String instead of UInt64). + ColumnRef bad_element = std::make_shared(std::make_shared()); + // Wrong kind entirely. + ColumnRef not_array = std::make_shared(); + + ValidationError error; + EXPECT_NO_THROW({ + EXPECT_EQ(TestArray::Wrap(bad_element, &error), nullptr); + }); + EXPECT_FALSE(std::string_view(error.what()).empty()); + + // Passing nullptr for the error is allowed and still non-throwing. + EXPECT_NO_THROW({ + EXPECT_EQ(TestArray::Wrap(not_array, nullptr), nullptr); + }); + + // Single-argument overload throws on the same mismatches. + EXPECT_THROW(TestArray::Wrap(bad_element), ValidationError); + EXPECT_THROW(TestArray::Wrap(not_array), ValidationError); + + // Sanity: a matching column wraps fine through both overloads. + ColumnRef good = std::make_shared(std::make_shared()); + EXPECT_NE(TestArray::Wrap(good, nullptr), nullptr); + EXPECT_NE(TestArray::Wrap(good), nullptr); +} + +TEST(ColumnsCase, ColumnNullableT_Wrap_TypeMismatch) { + using TestNullable = ColumnNullableT; + + // Nullable of the wrong nested type. + ColumnRef bad_nested = std::make_shared( + std::make_shared(), std::make_shared()); + ColumnRef not_nullable = std::make_shared(); + + ValidationError error; + EXPECT_EQ(TestNullable::Wrap(bad_nested, &error), nullptr); + EXPECT_FALSE(std::string_view(error.what()).empty()); + EXPECT_EQ(TestNullable::Wrap(not_nullable, nullptr), nullptr); + + EXPECT_THROW(TestNullable::Wrap(bad_nested), ValidationError); + EXPECT_THROW(TestNullable::Wrap(not_nullable), ValidationError); +} + +TEST(ColumnsCase, ColumnTupleT_Wrap_TypeMismatch) { + using TestTuple = ColumnTupleT; + + // Correct arity, wrong element type. + ColumnRef bad_element = std::make_shared(std::vector{ + std::make_shared(), std::make_shared()}); + // Wrong arity. + ColumnRef bad_arity = std::make_shared(std::vector{ + std::make_shared()}); + // Wrong kind. + ColumnRef not_tuple = std::make_shared(); + + ValidationError error; + EXPECT_EQ(TestTuple::Wrap(bad_element, &error), nullptr); + EXPECT_FALSE(std::string_view(error.what()).empty()); + EXPECT_EQ(TestTuple::Wrap(bad_arity, nullptr), nullptr); + EXPECT_EQ(TestTuple::Wrap(not_tuple, nullptr), nullptr); + + EXPECT_THROW(TestTuple::Wrap(bad_element), ValidationError); + EXPECT_THROW(TestTuple::Wrap(bad_arity), ValidationError); + EXPECT_THROW(TestTuple::Wrap(not_tuple), ValidationError); +} + +TEST(ColumnsCase, ColumnMapT_Wrap_TypeMismatch) { + using TestMap = ColumnMapT; + ColumnRef not_map = std::make_shared(); + + ValidationError error; + EXPECT_EQ(TestMap::Wrap(not_map, &error), nullptr); + EXPECT_FALSE(std::string_view(error.what()).empty()); + EXPECT_EQ(TestMap::Wrap(not_map, nullptr), nullptr); + EXPECT_THROW(TestMap::Wrap(not_map), ValidationError); +} + +TEST(ColumnsCase, ColumnLowCardinalityT_Wrap_TypeMismatch) { + using TestLC = ColumnLowCardinalityT; + + // LowCardinality with the wrong (but valid) dictionary type. + ColumnRef bad_dict = std::make_shared(std::make_shared(4)); + ColumnRef not_lc = std::make_shared(); + + ValidationError error; + EXPECT_EQ(TestLC::Wrap(bad_dict, &error), nullptr); + EXPECT_FALSE(std::string_view(error.what()).empty()); + EXPECT_EQ(TestLC::Wrap(not_lc, nullptr), nullptr); + + EXPECT_THROW(TestLC::Wrap(bad_dict), ValidationError); + EXPECT_THROW(TestLC::Wrap(not_lc), ValidationError); +}