From 4fe477ae8e12d8201db9e0c5f5fbc55d4a151b74 Mon Sep 17 00:00:00 2001 From: Johannes Misch Date: Tue, 21 Apr 2026 13:02:05 +0200 Subject: [PATCH 1/2] Add clangd cache directory to .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 8def1549..10fcc112 100644 --- a/.gitignore +++ b/.gitignore @@ -278,3 +278,6 @@ BUCKAROO_DEPS # Vim *.swp *.swo + +# clangd cache +/.cache/clangd From 2858cbcd9b57f4c0d7d8e63819a4ee0a2d331e3f Mon Sep 17 00:00:00 2001 From: Johannes Misch Date: Tue, 21 Apr 2026 13:04:26 +0200 Subject: [PATCH 2/2] Add Tuple field names to `TupleType` This extends the type parser to support an element name alongside the element type and wires this through to make the field names in a tuple available in the C++ API. --- clickhouse/columns/factory.cpp | 10 ++++++ clickhouse/columns/tuple.cpp | 19 ++++++++-- clickhouse/columns/tuple.h | 2 ++ clickhouse/types/type_parser.cpp | 8 +++++ clickhouse/types/type_parser.h | 3 ++ clickhouse/types/types.cpp | 31 +++++++++++++++-- clickhouse/types/types.h | 10 ++++++ ut/type_parser_ut.cpp | 34 +++++++++++++++++- ut/types_ut.cpp | 60 ++++++++++++++++++++++++++++++++ 9 files changed, 172 insertions(+), 5 deletions(-) diff --git a/clickhouse/columns/factory.cpp b/clickhouse/columns/factory.cpp index 460d66fa..47e3a06e 100644 --- a/clickhouse/columns/factory.cpp +++ b/clickhouse/columns/factory.cpp @@ -162,16 +162,26 @@ static ColumnRef CreateColumnFromAst(const TypeAst& ast, CreateColumnByTypeSetti case TypeAst::Tuple: { std::vector columns; + std::vector names; columns.reserve(ast.elements.size()); + names.reserve(ast.elements.size()); + bool any_named = false; for (const auto& elem : ast.elements) { if (auto col = CreateColumnFromAst(elem, settings)) { columns.push_back(col); + names.push_back(elem.element_name); + if (!elem.element_name.empty()) { + any_named = true; + } } else { return nullptr; } } + if (any_named) { + return std::make_shared(columns, std::move(names)); + } return std::make_shared(columns); } diff --git a/clickhouse/columns/tuple.cpp b/clickhouse/columns/tuple.cpp index 56858590..8e3bfa7f 100644 --- a/clickhouse/columns/tuple.cpp +++ b/clickhouse/columns/tuple.cpp @@ -16,6 +16,13 @@ ColumnTuple::ColumnTuple(const std::vector& columns) { } +ColumnTuple::ColumnTuple(const std::vector& columns, + std::vector names) + : Column(Type::CreateTuple(CollectTypes(columns), std::move(names))) + , columns_(columns) +{ +} + size_t ColumnTuple::TupleSize() const { return columns_.size(); } @@ -48,7 +55,11 @@ ColumnRef ColumnTuple::Slice(size_t begin, size_t len) const { sliced_columns.push_back(column->Slice(begin, len)); } - return std::make_shared(sliced_columns); + const auto& names = this->Type()->As()->GetItemNames(); + if (names.empty()) { + return std::make_shared(sliced_columns); + } + return std::make_shared(sliced_columns, names); } ColumnRef ColumnTuple::CloneEmpty() const { @@ -59,7 +70,11 @@ ColumnRef ColumnTuple::CloneEmpty() const { result_columns.push_back(column->CloneEmpty()); } - return std::make_shared(result_columns); + const auto& names = this->Type()->As()->GetItemNames(); + if (names.empty()) { + return std::make_shared(result_columns); + } + return std::make_shared(result_columns, names); } bool ColumnTuple::LoadPrefix(InputStream* input, size_t rows) { diff --git a/clickhouse/columns/tuple.h b/clickhouse/columns/tuple.h index ebc1b895..5bf3b0d6 100644 --- a/clickhouse/columns/tuple.h +++ b/clickhouse/columns/tuple.h @@ -13,6 +13,8 @@ namespace clickhouse { class ColumnTuple : public Column { public: ColumnTuple(const std::vector& columns); + ColumnTuple(const std::vector& columns, + std::vector names); /// Returns count of columns in the tuple. size_t TupleSize() const; diff --git a/clickhouse/types/type_parser.cpp b/clickhouse/types/type_parser.cpp index d488a079..385f4e90 100644 --- a/clickhouse/types/type_parser.cpp +++ b/clickhouse/types/type_parser.cpp @@ -22,7 +22,9 @@ bool TypeAst::operator==(const TypeAst & other) const { return meta == other.meta && code == other.code && name == other.name + && element_name == other.element_name && value == other.value + && value_string == other.value_string && std::equal(elements.begin(), elements.end(), other.elements.begin(), other.elements.end()); } @@ -167,6 +169,12 @@ bool TypeParser::Parse(TypeAst* type) { break; } case Token::Name: + if (!type_->name.empty()) { + // A second Name token on the same element means the + // previous one was a field name in a named-tuple element + // (e.g. "a" in "Tuple(a Int32, …)"). + type_->element_name = std::move(type_->name); + } type_->meta = GetTypeMeta(token.value); type_->name = token.value.to_string(); type_->code = GetTypeCode(type_->name); diff --git a/clickhouse/types/type_parser.h b/clickhouse/types/type_parser.h index 2f8f2f6f..9cc29512 100644 --- a/clickhouse/types/type_parser.h +++ b/clickhouse/types/type_parser.h @@ -31,6 +31,9 @@ struct TypeAst { /// Type's name. /// Need to cache TypeAst, so can't use StringView for name. std::string name; + /// Name of this element inside its parent (e.g. field name inside a named + /// Tuple). Empty for unnamed elements. + std::string element_name; /// Value associated with the node, /// used for fixed-width types and enum values. int64_t value = 0; diff --git a/clickhouse/types/types.cpp b/clickhouse/types/types.cpp index a5588c68..ab4219e7 100644 --- a/clickhouse/types/types.cpp +++ b/clickhouse/types/types.cpp @@ -243,6 +243,11 @@ TypeRef Type::CreateTuple(const std::vector& item_types) { return TypeRef(new TupleType(item_types)); } +TypeRef Type::CreateTuple(const std::vector& item_types, + std::vector item_names) { + return TypeRef(new TupleType(item_types, std::move(item_names))); +} + TypeRef Type::CreateEnum8(const std::vector& enum_items) { return TypeRef(new EnumType(Type::Enum8, enum_items)); } @@ -447,6 +452,11 @@ NullableType::NullableType(TypeRef nested_type) : Type(Nullable), nested_type_(n TupleType::TupleType(const std::vector& item_types) : Type(Tuple), item_types_(item_types) { } +TupleType::TupleType(const std::vector& item_types, + std::vector item_names) + : Type(Tuple), item_types_(item_types), item_names_(std::move(item_names)) { +} + /// class LowCardinalityType LowCardinalityType::LowCardinalityType(TypeRef nested_type) : Type(LowCardinality), nested_type_(nested_type) { } @@ -456,13 +466,30 @@ LowCardinalityType::~LowCardinalityType() { std::string TupleType::GetName() const { std::string result("Tuple("); + bool has_complete_names = item_names_.size() == item_types_.size(); + if (has_complete_names) { + for (const auto& item_name : item_names_) { + if (item_name.empty()) { + has_complete_names = false; + break; + } + } + } if (!item_types_.empty()) { - result += item_types_[0]->GetName(); + if (has_complete_names) { + result += item_names_[0] + " " + item_types_[0]->GetName(); + } else { + result += item_types_[0]->GetName(); + } } for (size_t i = 1; i < item_types_.size(); ++i) { - result += ", " + item_types_[i]->GetName(); + if (has_complete_names) { + result += ", " + item_names_[i] + " " + item_types_[i]->GetName(); + } else { + result += ", " + item_types_[i]->GetName(); + } } result += ")"; diff --git a/clickhouse/types/types.h b/clickhouse/types/types.h index 2275cfba..95d764cb 100644 --- a/clickhouse/types/types.h +++ b/clickhouse/types/types.h @@ -126,6 +126,9 @@ class Type { static TypeRef CreateTuple(const std::vector& item_types); + static TypeRef CreateTuple(const std::vector& item_types, + std::vector item_names); + static TypeRef CreateEnum8(const std::vector& enum_items); static TypeRef CreateEnum16(const std::vector& enum_items); @@ -293,14 +296,21 @@ class NullableType : public Type { class TupleType : public Type { public: explicit TupleType(const std::vector& item_types); + TupleType(const std::vector& item_types, + std::vector item_names); std::string GetName() const; /// Type of nested Tuple element type. std::vector GetTupleType() const { return item_types_; } + /// Field names for named tuples. Same length as GetTupleType() when + /// populated, or empty when the tuple has no field names. + const std::vector& GetItemNames() const { return item_names_; } + private: std::vector item_types_; + std::vector item_names_; }; class LowCardinalityType : public Type { diff --git a/ut/type_parser_ut.cpp b/ut/type_parser_ut.cpp index 4cff5237..d4012f82 100644 --- a/ut/type_parser_ut.cpp +++ b/ut/type_parser_ut.cpp @@ -89,10 +89,28 @@ TEST(TypeParserCase, ParseTuple) { auto element = ast.elements.begin(); for (size_t i = 0; i < 2; ++i) { ASSERT_EQ(element->name, names[i]); + ASSERT_TRUE(element->element_name.empty()); ++element; } } +TEST(TypeParserCase, ParseNamedTuple) { + TypeAst ast; + TypeParser("Tuple(a UInt8, b String)").Parse(&ast); + ASSERT_EQ(ast.meta, TypeAst::Tuple); + ASSERT_EQ(ast.name, "Tuple"); + ASSERT_EQ(ast.code, Type::Tuple); + ASSERT_EQ(ast.elements.size(), 2u); + + ASSERT_EQ(ast.elements[0].element_name, "a"); + ASSERT_EQ(ast.elements[0].name, "UInt8"); + ASSERT_EQ(ast.elements[0].code, Type::UInt8); + + ASSERT_EQ(ast.elements[1].element_name, "b"); + ASSERT_EQ(ast.elements[1].name, "String"); + ASSERT_EQ(ast.elements[1].code, Type::String); +} + TEST(TypeParserCase, ParseDecimal) { TypeAst ast; TypeParser("Decimal(12, 5)").Parse(&ast); @@ -167,6 +185,20 @@ TEST(TypeParserCase, ParseDateTime_MINSK_TIMEZONE) { ASSERT_EQ(ast.elements[0].meta, TypeAst::Terminal); } +TEST(TypeParserCase, EqualityIncludesValueString) { + TypeAst utc; + TypeAst minsk; + ASSERT_TRUE(TypeParser("DateTime('UTC')").Parse(&utc)); + ASSERT_TRUE(TypeParser("DateTime('Europe/Minsk')").Parse(&minsk)); + ASSERT_NE(utc, minsk); + + TypeAst enum_one; + TypeAst enum_two; + ASSERT_TRUE(TypeParser("Enum8('ONE' = 1)").Parse(&enum_one)); + ASSERT_TRUE(TypeParser("Enum8('TWO' = 1)").Parse(&enum_two)); + ASSERT_NE(enum_one, enum_two); +} + TEST(TypeParserCase, LowCardinality_String) { TypeAst ast; ASSERT_TRUE(TypeParser("LowCardinality(String)").Parse(&ast)); @@ -194,7 +226,7 @@ TEST(TypeParserCase, LowCardinality_FixedString) { ASSERT_EQ(ast.elements[0].name, "FixedString"); ASSERT_EQ(ast.elements[0].value, 0); ASSERT_EQ(ast.elements[0].elements.size(), 1u); - auto param = TypeAst{TypeAst::Number, Type::Void, "", 10, {}, {}}; + auto param = TypeAst{TypeAst::Number, Type::Void, "", "", 10, {}, {}}; ASSERT_EQ(ast.elements[0].elements[0], param); } diff --git a/ut/types_ut.cpp b/ut/types_ut.cpp index 7af343b5..f561e89a 100644 --- a/ut/types_ut.cpp +++ b/ut/types_ut.cpp @@ -41,6 +41,66 @@ TEST(TypesCase, NullableType) { ASSERT_EQ(Type::CreateNullable(nested)->As()->GetNestedType(), nested); } +TEST(TypesCase, TupleTypeItemNames) { + auto unnamed = Type::CreateTuple({ + Type::CreateSimple(), + Type::CreateString()}); + ASSERT_TRUE(unnamed->As()->GetItemNames().empty()); + + auto named = Type::CreateTuple( + {Type::CreateSimple(), Type::CreateString()}, + {"a", "b"}); + const auto& names = named->As()->GetItemNames(); + ASSERT_EQ(names.size(), 2u); + ASSERT_EQ(names[0], "a"); + ASSERT_EQ(names[1], "b"); +} + +TEST(TypesCase, TupleTypeNameIncludesFieldNames) { + auto named = Type::CreateTuple( + {Type::CreateSimple(), Type::CreateString()}, + {"a", "b"}); + ASSERT_EQ(named->GetName(), "Tuple(a UInt8, b String)"); + + auto partially_named = Type::CreateTuple( + {Type::CreateSimple(), Type::CreateString()}, + {"a", ""}); + ASSERT_EQ(partially_named->GetName(), "Tuple(UInt8, String)"); + + auto mismatched_names = Type::CreateTuple( + {Type::CreateSimple(), Type::CreateString()}, + {"a"}); + ASSERT_EQ(mismatched_names->GetName(), "Tuple(UInt8, String)"); +} + +TEST(TypesCase, TupleTypeNamesFromFactory) { + auto col = CreateColumnByType("Tuple(a UInt8, b String)"); + ASSERT_NE(col, nullptr); + const auto& names = col->Type()->As()->GetItemNames(); + ASSERT_EQ(names.size(), 2u); + ASSERT_EQ(names[0], "a"); + ASSERT_EQ(names[1], "b"); + + auto col_unnamed = CreateColumnByType("Tuple(UInt8, String)"); + ASSERT_NE(col_unnamed, nullptr); + ASSERT_TRUE(col_unnamed->Type()->As()->GetItemNames().empty()); +} + +TEST(TypesCase, TupleTypeEqualityIncludesFieldNames) { + auto unnamed = Type::CreateTuple( + {Type::CreateSimple(), Type::CreateString()}); + auto named_ab = Type::CreateTuple( + {Type::CreateSimple(), Type::CreateString()}, + {"a", "b"}); + auto named_xy = Type::CreateTuple( + {Type::CreateSimple(), Type::CreateString()}, + {"x", "y"}); + + ASSERT_TRUE(named_ab->IsEqual(named_ab)); + ASSERT_FALSE(named_ab->IsEqual(unnamed)); + ASSERT_FALSE(named_ab->IsEqual(named_xy)); +} + TEST(TypesCase, EnumTypes) { auto enum8 = Type::CreateEnum8({{"One", 1}, {"Two", 2}}); ASSERT_EQ(enum8->GetName(), "Enum8('One' = 1, 'Two' = 2)");