From fe037e7d903fef1d3c8f8acc7e37178da1b3f89a Mon Sep 17 00:00:00 2001 From: Andrew Slabko Date: Wed, 18 Mar 2026 18:23:05 +0100 Subject: [PATCH 1/4] Add support for Time/Time64 columns --- clickhouse/CMakeLists.txt | 3 + clickhouse/client.h | 1 + clickhouse/columns/factory.cpp | 9 +- clickhouse/columns/itemview.cpp | 2 + clickhouse/columns/time.cpp | 155 +++++++++++++++++++++++++++++++ clickhouse/columns/time.h | 118 +++++++++++++++++++++++ clickhouse/types/type_parser.cpp | 2 + clickhouse/types/types.cpp | 27 ++++++ clickhouse/types/types.h | 20 +++- ut/Column_ut.cpp | 6 ++ ut/client_ut.cpp | 71 ++++++++++++++ ut/columns_ut.cpp | 51 +++++++++- ut/itemview_ut.cpp | 12 +++ ut/type_parser_ut.cpp | 20 ++++ ut/utils.cpp | 6 ++ ut/utils_ut.cpp | 3 + ut/value_generators.cpp | 26 ++++++ ut/value_generators.h | 18 ++++ 18 files changed, 547 insertions(+), 3 deletions(-) create mode 100644 clickhouse/columns/time.cpp create mode 100644 clickhouse/columns/time.h diff --git a/clickhouse/CMakeLists.txt b/clickhouse/CMakeLists.txt index 7669420a..0ab1a487 100644 --- a/clickhouse/CMakeLists.txt +++ b/clickhouse/CMakeLists.txt @@ -22,6 +22,7 @@ SET ( clickhouse-cpp-lib-src columns/map.cpp columns/string.cpp columns/tuple.cpp + columns/time.cpp columns/uuid.cpp columns/itemview.cpp @@ -67,6 +68,7 @@ SET ( clickhouse-cpp-lib-src columns/nullable.h columns/numeric.h columns/string.h + columns/time.h columns/tuple.h columns/utils.h columns/uuid.h @@ -226,6 +228,7 @@ INSTALL(FILES columns/nullable.h DESTINATION include/clickhouse/columns/) INSTALL(FILES columns/numeric.h DESTINATION include/clickhouse/columns/) INSTALL(FILES columns/map.h DESTINATION include/clickhouse/columns/) INSTALL(FILES columns/string.h DESTINATION include/clickhouse/columns/) +INSTALL(FILES columns/time.h DESTINATION include/clickhouse/columns/) INSTALL(FILES columns/tuple.h DESTINATION include/clickhouse/columns/) INSTALL(FILES columns/utils.h DESTINATION include/clickhouse/columns/) INSTALL(FILES columns/uuid.h DESTINATION include/clickhouse/columns/) diff --git a/clickhouse/client.h b/clickhouse/client.h index 3ca9133b..0486b1c8 100644 --- a/clickhouse/client.h +++ b/clickhouse/client.h @@ -17,6 +17,7 @@ #include "columns/map.h" #include "columns/string.h" #include "columns/tuple.h" +#include "columns/time.h" #include "columns/uuid.h" #include diff --git a/clickhouse/columns/factory.cpp b/clickhouse/columns/factory.cpp index 73f2cde1..460d66fa 100644 --- a/clickhouse/columns/factory.cpp +++ b/clickhouse/columns/factory.cpp @@ -14,6 +14,7 @@ #include "nullable.h" #include "numeric.h" #include "string.h" +#include "./time.h" // `./` avoids possible conflicts with standard C time.h #include "tuple.h" #include "uuid.h" @@ -108,7 +109,6 @@ static ColumnRef CreateTerminalColumn(const TypeAst& ast) { return std::make_shared(); case Type::Date32: return std::make_shared(); - case Type::IPv4: return std::make_shared(); case Type::IPv6: @@ -129,6 +129,13 @@ static ColumnRef CreateTerminalColumn(const TypeAst& ast) { case Type::MultiPolygon: return std::make_shared(); + case Type::Time: + return std::make_shared(); + case Type::Time64: + if (ast.elements.empty()) { + return nullptr; + } + return std::make_shared(GetASTChildElement(ast, 0).value); default: return nullptr; } diff --git a/clickhouse/columns/itemview.cpp b/clickhouse/columns/itemview.cpp index c92627d2..0116070a 100644 --- a/clickhouse/columns/itemview.cpp +++ b/clickhouse/columns/itemview.cpp @@ -59,6 +59,7 @@ void ItemView::ValidateData(Type::Code type, DataType data) { case Type::Code::Date32: case Type::Code::IPv4: case Type::Code::Decimal32: + case Type::Code::Time: return AssertSize({4}); case Type::Code::Int64: @@ -66,6 +67,7 @@ void ItemView::ValidateData(Type::Code type, DataType data) { case Type::Code::Float64: case Type::Code::DateTime64: case Type::Code::Decimal64: + case Type::Code::Time64: return AssertSize({8}); case Type::Code::String: diff --git a/clickhouse/columns/time.cpp b/clickhouse/columns/time.cpp new file mode 100644 index 00000000..045efb2a --- /dev/null +++ b/clickhouse/columns/time.cpp @@ -0,0 +1,155 @@ +#include "time.h" + +namespace clickhouse { + +ColumnTime::ColumnTime() + : ColumnTime(Type::CreateTime(), std::make_shared()) {} + +ColumnTime::ColumnTime(std::vector&& data) + : ColumnTime(Type::CreateTime(), std::make_shared(std::move(data))) {} + +ColumnTime::ColumnTime(TypeRef type, std::shared_ptr data) + : Column(std::move(type)), + data_(std::move(data)) +{} + +void ColumnTime::Append(ValueType value) { + data_->Append(value); +} + +ColumnTime::ValueType ColumnTime::At(size_t n) const { + return data_->At(n); +} + +std::vector& ColumnTime::GetWritableData() { + return data_->GetWritableData(); +} + +void ColumnTime::Reserve(size_t new_cap) { + data_->Reserve(new_cap); +} + +size_t ColumnTime::Capacity() const { + return data_->Capacity(); +} + +void ColumnTime::Append(ColumnRef column) { + if (auto col = column->As()) { + data_->Append(col->data_); + } +} + +bool ColumnTime::LoadBody(InputStream* input, size_t rows) { + return data_->LoadBody(input, rows); +} + +void ColumnTime::Clear() { + data_->Clear(); +} + +void ColumnTime::SaveBody(OutputStream* output) { + data_->SaveBody(output); +} + +size_t ColumnTime::Size() const { + return data_->Size(); +} + +ColumnRef ColumnTime::Slice(size_t begin, size_t len) const { + auto sliced_data = data_->Slice(begin, len)->As(); + return ColumnRef{new ColumnTime(type_, sliced_data)}; +} + +ColumnRef ColumnTime::CloneEmpty() const { + return ColumnRef{new ColumnTime(type_, data_->CloneEmpty()->As())}; +} + +void ColumnTime::Swap(Column& other) { + auto & col = dynamic_cast(other); + data_.swap(col.data_); +} + +ItemView ColumnTime::GetItem(size_t index) const { + return ItemView{Type::Time, data_->GetItem(index)}; +} + +ColumnTime64::ColumnTime64(size_t precision) + : ColumnTime64(Type::CreateTime64(precision), std::make_shared()) +{} + +ColumnTime64::ColumnTime64(size_t precision, std::vector&& data) + : ColumnTime64(Type::CreateTime64(precision), std::make_shared(std::move(data))) +{} + +ColumnTime64::ColumnTime64(TypeRef type, std::shared_ptr data) + : Column(std::move(type)), + data_(std::move(data)), + precision_{type_->As()->GetPrecision()} +{} + +void ColumnTime64::Append(ValueType value) { + data_->Append(value); +} + +ColumnTime64::ValueType ColumnTime64::At(size_t n) const { + return data_->At(n); +} + +std::vector& ColumnTime64::GetWritableData() { + return data_->GetWritableData(); +} + +void ColumnTime64::Reserve(size_t new_cap) { + data_->Reserve(new_cap); +} + +size_t ColumnTime64::Capacity() const { + return data_->Capacity(); +} + +void ColumnTime64::Append(ColumnRef column) { + if (auto col = column->As()) { + data_->Append(col->data_); + } +} + +bool ColumnTime64::LoadBody(InputStream* input, size_t rows) { + return data_->LoadBody(input, rows); +} + +void ColumnTime64::Clear() { + data_->Clear(); +} + +void ColumnTime64::SaveBody(OutputStream* output) { + data_->SaveBody(output); +} + +size_t ColumnTime64::Size() const { + return data_->Size(); +} + +ColumnRef ColumnTime64::Slice(size_t begin, size_t len) const { + auto sliced_data = data_->Slice(begin, len)->As(); + return ColumnRef{new ColumnTime64(type_, sliced_data)}; +} + +ColumnRef ColumnTime64::CloneEmpty() const { + return ColumnRef{new ColumnTime64(type_, data_->CloneEmpty()->As())}; +} + +void ColumnTime64::Swap(Column& other) { + auto & col = dynamic_cast(other); + if (col.GetPrecision() != GetPrecision()) { + throw ValidationError("Can't swap Time64 columns when precisions are not the same: " + + std::to_string(GetPrecision()) + "(this) != " + + std::to_string(col.GetPrecision()) + "(that)"); + } + data_.swap(col.data_); +} + +ItemView ColumnTime64::GetItem(size_t index) const { + return ItemView{Type::Time64, data_->GetItem(index)}; +} + +} // namespace clickhouse diff --git a/clickhouse/columns/time.h b/clickhouse/columns/time.h new file mode 100644 index 00000000..68ddeada --- /dev/null +++ b/clickhouse/columns/time.h @@ -0,0 +1,118 @@ +#pragma once + +#include "column.h" +#include "numeric.h" + +namespace clickhouse { + +class ColumnTime : public Column { +public: + using ValueType = int32_t; + + ColumnTime(); + explicit ColumnTime(std::vector&& data); + + /// Appends one element to the end of column. + void Append(ValueType value); + + /// Returns element at given row number. + ValueType At(size_t n) const; + ValueType operator[](size_t n) const { return At(n); } + + /// Get Raw Vector Contents + std::vector& GetWritableData(); + +public: + /// Increase the capacity of the column for large block insertion. + void Reserve(size_t new_cap) override; + + /// Returns the capacity of the column + size_t Capacity() const; + + /// Appends content of given column to the end of current one. + void Append(ColumnRef column) override; + + /// Loads column data from input stream. + bool LoadBody(InputStream* input, size_t rows) override; + + /// Clear column data . + void Clear() override; + + /// Saves column data to output stream. + void SaveBody(OutputStream* output) override; + + /// Returns count of rows in the column. + size_t Size() const override; + + /// Makes slice of the current column. + ColumnRef Slice(size_t begin, size_t len) const override; + ColumnRef CloneEmpty() const override; + void Swap(Column& other) override; + + ItemView GetItem(size_t index) const override; + +private: + ColumnTime(TypeRef type, std::shared_ptr data); + +private: + std::shared_ptr data_; +}; + +class ColumnTime64 : public Column { +public: + using ValueType = int64_t; + + explicit ColumnTime64(size_t precision); + ColumnTime64(size_t precision, std::vector&& data); + + /// Appends one element to the end of column. + void Append(ValueType value); + + /// Returns element at given row number. + ValueType At(size_t n) const; + + ValueType operator[](size_t n) const { return At(n); } + + /// Get Raw Vector Contents + std::vector& GetWritableData(); + +public: + /// Increase the capacity of the column for large block insertion. + void Reserve(size_t new_cap) override; + + /// Returns the capacity of the column + size_t Capacity() const; + + /// Appends content of given column to the end of current one. + void Append(ColumnRef column) override; + + /// Loads column data from input stream. + bool LoadBody(InputStream* input, size_t rows) override; + + /// Clear column data . + void Clear() override; + + /// Saves column data to output stream. + void SaveBody(OutputStream* output) override; + + /// Returns count of rows in the column. + size_t Size() const override; + + /// Makes slice of the current column. + ColumnRef Slice(size_t begin, size_t len) const override; + ColumnRef CloneEmpty() const override; + void Swap(Column& other) override; + + ItemView GetItem(size_t index) const override; + + size_t GetPrecision() const { return precision_; }; + +private: + ColumnTime64(TypeRef type, std::shared_ptr data); + +private: + std::shared_ptr data_; + const size_t precision_; +}; + +} diff --git a/clickhouse/types/type_parser.cpp b/clickhouse/types/type_parser.cpp index 52058e80..d488a079 100644 --- a/clickhouse/types/type_parser.cpp +++ b/clickhouse/types/type_parser.cpp @@ -65,6 +65,8 @@ static const std::unordered_map kTypeCode = { { "Ring", Type::Ring }, { "Polygon", Type::Polygon }, { "MultiPolygon", Type::MultiPolygon }, + { "Time", Type::Time }, + { "Time64", Type::Time64 }, }; template diff --git a/clickhouse/types/types.cpp b/clickhouse/types/types.cpp index c14c86c5..a5588c68 100644 --- a/clickhouse/types/types.cpp +++ b/clickhouse/types/types.cpp @@ -52,6 +52,8 @@ const char* Type::TypeName(Type::Code code) { case Type::Code::Ring: return "Ring"; case Type::Code::Polygon: return "Polygon"; case Type::Code::MultiPolygon: return "MultiPolygon"; + case Type::Code::Time: return "Time"; + case Type::Code::Time64: return "Time64"; } return "Unknown type"; @@ -78,11 +80,14 @@ std::string Type::GetName() const { case IPv6: case Date: case Date32: + case Time: case Point: case Ring: case Polygon: case MultiPolygon: return TypeName(code_); + case Time64: + return As()->GetName(); case FixedString: return As()->GetName(); case DateTime: @@ -145,6 +150,8 @@ uint64_t Type::GetTypeUniqueId() const { return code_; case FixedString: + case Time: + case Time64: case DateTime: case DateTime64: case Array: @@ -188,6 +195,14 @@ TypeRef Type::CreateDate32() { return TypeRef(new Type(Type::Date32)); } +TypeRef Type::CreateTime() { + return TypeRef(new Type(Type::Time)); +} + +TypeRef Type::CreateTime64(size_t precision) { + return TypeRef(new Time64Type(precision)); +} + TypeRef Type::CreateDateTime(std::string timezone) { return TypeRef(new DateTimeType(std::move(timezone))); } @@ -366,6 +381,18 @@ const std::string & TypeWithTimeZoneMixin::Timezone() const { } } +/// class Time64 +Time64Type::Time64Type(size_t precision) + : Type(Time64), precision_{precision} { + if (precision_ > 9) { + throw ValidationError("Time64 precision is > 9"); + } +} + +std::string Time64Type::GetName() const { + return "Time64(" + std::to_string(precision_) + ")"; +} + /// class DateTimeType DateTimeType::DateTimeType(std::string timezone) : Type(DateTime), details::TypeWithTimeZoneMixin(std::move(timezone)) { diff --git a/clickhouse/types/types.h b/clickhouse/types/types.h index 71613323..2275cfba 100644 --- a/clickhouse/types/types.h +++ b/clickhouse/types/types.h @@ -56,7 +56,9 @@ class Type { Point, Ring, Polygon, - MultiPolygon + MultiPolygon, + Time, + Time64, }; using EnumItem = std::pair; @@ -142,6 +144,10 @@ class Type { static TypeRef CreateMultiPolygon(); + static TypeRef CreateTime(); + + static TypeRef CreateTime64(size_t precision); + private: uint64_t GetTypeUniqueId() const; @@ -204,6 +210,18 @@ class TypeWithTimeZoneMixin }; } +class Time64Type : public Type { +public: + explicit Time64Type(size_t precision); + + std::string GetName() const; + + inline size_t GetPrecision() const { return precision_; } + +private: + size_t precision_; +}; + class DateTimeType : public Type, public details::TypeWithTimeZoneMixin { public: explicit DateTimeType(std::string timezone); diff --git a/ut/Column_ut.cpp b/ut/Column_ut.cpp index 04cb5d78..f792caf3 100644 --- a/ut/Column_ut.cpp +++ b/ut/Column_ut.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -202,6 +203,11 @@ using TestCases = ::testing::Types< GenericColumnTestCase, clickhouse::Int64, &MakeDateTime64s<3>>, GenericColumnTestCase, clickhouse::Int64, &MakeDateTime64s<6>>, GenericColumnTestCase, clickhouse::Int64, &MakeDateTime64s<9>>, + GenericColumnTestCase, int32_t, &MakeTime>, + GenericColumnTestCase, int64_t, &MakeTime64<0>>, + GenericColumnTestCase, int64_t, &MakeTime64<3>>, + GenericColumnTestCase, int64_t, &MakeTime64<6>>, + GenericColumnTestCase, int64_t, &MakeTime64<9>>, GenericColumnTestCase, in_addr, &MakeIPv4s>, GenericColumnTestCase, in6_addr, &MakeIPv6s>, diff --git a/ut/client_ut.cpp b/ut/client_ut.cpp index e6106d25..965ba0c8 100644 --- a/ut/client_ut.cpp +++ b/ut/client_ut.cpp @@ -155,6 +155,77 @@ TEST_P(ClientCase, Array) { EXPECT_EQ(4U, row); } +TEST_P(ClientCase, Time) { + Block b; + + client_->Execute("CREATE TEMPORARY TABLE IF NOT EXISTS test_clickhouse_cpp_time (t Time)"); + auto t = std::make_shared(); + + int32_t ts = 3600 * 15 + 60 * 4 + 5; + t->Append(ts); + b.AppendColumn("t", t); + client_->Insert("test_clickhouse_cpp_time", b); + + size_t total_rows = 0; + client_->Select("SELECT t, toString(t) FROM test_clickhouse_cpp_time", [ts, &total_rows](const Block& block) + { + if (block.GetRowCount() == 0) { + return; + } + EXPECT_EQ(ts, block[0]->As()->At(0)); + EXPECT_EQ("15:04:05", block[1]->As()->At(0)); + total_rows += block.GetRowCount(); + }); + EXPECT_EQ(total_rows, 1UL); +} + +TEST_P(ClientCase, Time64) { + Block b; + + client_->Execute("CREATE TEMPORARY TABLE IF NOT EXISTS test_clickhouse_cpp_time64 " + "(t0 Time64(0), t3 Time64(3), t6 Time64(6))"); + auto t0 = std::make_shared(0); + auto t3 = std::make_shared(3); + auto t6 = std::make_shared(6); + + int64_t ts0 = 3600 * 15 + 60 * 4 + 5; + int64_t ts3 = ts0 * 1000 + 123; + int64_t ts6 = ts0 * 1000000 + 123456; + t0->Append(ts0); + t3->Append(ts3); + t6->Append(ts6); + b.AppendColumn("t0", t0); + b.AppendColumn("t3", t3); + b.AppendColumn("t6", t6); + client_->Insert("test_clickhouse_cpp_time64", b); + + size_t total_rows = 0; + client_->Select("SELECT " + "t0, toString(t0), " + "t3, toString(t3), " + "t6, toString(t6) " + "FROM test_clickhouse_cpp_time64", + [ts0, ts3, ts6, &total_rows](const Block& block) + { + if (block.GetRowCount() == 0) { + return; + } + EXPECT_EQ(ts0, block[0]->As()->At(0)); + EXPECT_EQ(0UL, block[0]->As()->GetPrecision()); + EXPECT_EQ("15:04:05", block[1]->As()->At(0)); + + EXPECT_EQ(ts3, block[2]->As()->At(0)); + EXPECT_EQ(3UL, block[2]->As()->GetPrecision()); + EXPECT_EQ("15:04:05.123", block[3]->As()->At(0)); + + EXPECT_EQ(ts6, block[4]->As()->At(0)); + EXPECT_EQ(6UL, block[4]->As()->GetPrecision()); + EXPECT_EQ("15:04:05.123456", block[5]->As()->At(0)); + total_rows += block.GetRowCount(); + }); + EXPECT_EQ(total_rows, 1UL); +} + TEST_P(ClientCase, Date) { Block b; diff --git a/ut/columns_ut.cpp b/ut/columns_ut.cpp index 314cabdf..f799cb55 100644 --- a/ut/columns_ut.cpp +++ b/ut/columns_ut.cpp @@ -170,6 +170,56 @@ TEST(ColumnsCase, TupleSlice){ ASSERT_EQ((*tuple2)[1]->As()->At(0), "3"); } +TEST(ColumnsCase, TimeAppend) { + auto col = std::make_shared(); + col->Append(1); + col->Append(60); + ASSERT_EQ(col->Size(), 2u); + ASSERT_EQ(col->At(0), 1); + ASSERT_EQ(col->At(1), 60); +} + +TEST(ColumnsCase, Time_Int32_construct_from_rvalue_data) { + auto const expected = MakeNumbers(); + + auto data = expected; + auto col = std::make_shared(std::move(data)); + + ASSERT_EQ(col->Size(), expected.size()); + for (size_t i = 0; i < expected.size(); ++i) { + ASSERT_EQ(col->At(i), expected[i]); + } +} + +TEST(ColumnsCase, Time64Append) { + auto col = std::make_shared(3); + col->Append(1); + col->Append(60); + ASSERT_EQ(col->Size(), 2u); + ASSERT_EQ(col->At(0), 1); + ASSERT_EQ(col->At(1), 60); +} + +TEST(ColumnsCase, Time64_Int64_construct_from_rvalue_data) { + auto const expected = MakeNumbers(); + + auto data = expected; + auto col1 = std::make_shared(3, std::move(data)); + + ASSERT_EQ(col1->Size(), expected.size()); + for (size_t i = 0; i < expected.size(); ++i) { + ASSERT_EQ(col1->At(i), expected[i]); + } + ASSERT_EQ(col1->GetPrecision(), 3UL); +} + +TEST(ColumnsCase, Time64SwapDifferentPrecision) { + auto col1 = std::make_shared(3); + auto col2 = std::make_shared(6); + col1->Append(1); + col1->Append(60); + EXPECT_THROW(col1->Swap(*col2), ValidationError) ; +} TEST(ColumnsCase, DateAppend) { auto col1 = std::make_shared(); @@ -183,7 +233,6 @@ TEST(ColumnsCase, DateAppend) { ASSERT_EQ(col2->At(0), (now / 86400) * 86400); } - TEST(ColumnsCase, Date_UInt16_interface) { auto col1 = std::make_shared(); diff --git a/ut/itemview_ut.cpp b/ut/itemview_ut.cpp index 6413e190..f53b54a9 100644 --- a/ut/itemview_ut.cpp +++ b/ut/itemview_ut.cpp @@ -46,6 +46,8 @@ TEST(ItemView, StorableTypes) { TEST_ITEMVIEW_TYPE_VALUES(Type::Code::Float64, double); TEST_ITEMVIEW_TYPE_VALUE(Type::Code::Float64, double, 0.5); + TEST_ITEMVIEW_TYPE_VALUES(Type::Code::Time, int32_t); + TEST_ITEMVIEW_TYPE_VALUES(Type::Code::Time64, int64_t); TEST_ITEMVIEW_TYPE_VALUES(Type::Code::Date, uint16_t); TEST_ITEMVIEW_TYPE_VALUES(Type::Code::DateTime, uint32_t); TEST_ITEMVIEW_TYPE_VALUES(Type::Code::DateTime64, int64_t); @@ -143,6 +145,16 @@ TEST(ItemView, TypeSizeMismatch) { EXPECT_ITEMVIEW_ERROR(Type::Code::Float64, Int128); EXPECT_ITEMVIEW_ERROR(Type::Code::Float64, float); + EXPECT_ITEMVIEW_ERROR(Type::Code::Time, int8_t); + EXPECT_ITEMVIEW_ERROR(Type::Code::Time, int16_t); + EXPECT_ITEMVIEW_ERROR(Type::Code::Time, int64_t); + EXPECT_ITEMVIEW_ERROR(Type::Code::Time, Int128); + + EXPECT_ITEMVIEW_ERROR(Type::Code::Time64, int8_t); + EXPECT_ITEMVIEW_ERROR(Type::Code::Time64, int16_t); + EXPECT_ITEMVIEW_ERROR(Type::Code::Time64, int32_t); + EXPECT_ITEMVIEW_ERROR(Type::Code::Time64, Int128); + EXPECT_ITEMVIEW_ERROR(Type::Code::Date, int8_t); EXPECT_ITEMVIEW_ERROR(Type::Code::Date, int32_t); EXPECT_ITEMVIEW_ERROR(Type::Code::Date, int64_t); diff --git a/ut/type_parser_ut.cpp b/ut/type_parser_ut.cpp index b0193ded..4cff5237 100644 --- a/ut/type_parser_ut.cpp +++ b/ut/type_parser_ut.cpp @@ -214,6 +214,26 @@ TEST(TypeParserCase, SimpleAggregateFunction_UInt64) { ASSERT_EQ(ast.elements[1].meta, TypeAst::Terminal); } +TEST(TypeParserCase, ParseTime) { + TypeAst ast; + TypeParser("Time").Parse(&ast); + ASSERT_EQ(ast.meta, TypeAst::Terminal); + ASSERT_EQ(ast.name, "Time"); + ASSERT_EQ(ast.code, Type::Time); + ASSERT_EQ(ast.elements.size(), 0u); +} + +TEST(TypeParserCase, ParseTime64) { + TypeAst ast; + TypeParser("Time64(3)").Parse(&ast); + ASSERT_EQ(ast.meta, TypeAst::Terminal); + ASSERT_EQ(ast.name, "Time64"); + ASSERT_EQ(ast.code, Type::Time64); + ASSERT_EQ(ast.elements.size(), 1u); + ASSERT_EQ(ast.elements[0].name, ""); + ASSERT_EQ(ast.elements[0].value, 3); +} + TEST(TypeParserCase, ParseDateTime64) { TypeAst ast; TypeParser("DateTime64(3, 'UTC')").Parse(&ast); diff --git a/ut/utils.cpp b/ut/utils.cpp index e2219dcd..5c0dec92 100644 --- a/ut/utils.cpp +++ b/ut/utils.cpp @@ -404,6 +404,12 @@ std::ostream& operator<<(std::ostream& ostr, const ItemView& item_view) { } break; } + case Type::Time: + ostr << item_view.get(); + break; + case Type::Time64: + ostr << item_view.get(); + break; case Type::Enum8: ostr << static_cast(item_view.get()); break; diff --git a/ut/utils_ut.cpp b/ut/utils_ut.cpp index 31ae566f..43d3cae8 100644 --- a/ut/utils_ut.cpp +++ b/ut/utils_ut.cpp @@ -268,6 +268,9 @@ TEST(ItemView, OutputToOstream_VALID) { EXPECTED_SERIALIZATION("Decimal : 1234567", ColumnDecimal(18, 0), 1234567); EXPECTED_SERIALIZATION("Decimal : 1234567", ColumnDecimal(18, 9), 1234567); + EXPECTED_SERIALIZATION("Time : 42", ColumnTime(), 42); + EXPECTED_SERIALIZATION("Time64 : 42", ColumnTime64(3), 42); + EXPECTED_SERIALIZATION("Date : 1970-05-04 00:00:00", ColumnDate(), time_t(123) * 86400); EXPECTED_SERIALIZATION("DateTime : 1970-01-15 06:56:07", ColumnDateTime(), 1234567); // this is completely bogus, since precision is not taken into account diff --git a/ut/value_generators.cpp b/ut/value_generators.cpp index 5504fd1d..25b38f15 100644 --- a/ut/value_generators.cpp +++ b/ut/value_generators.cpp @@ -121,6 +121,32 @@ std::vector MakeUInt128s() { }; } +std::vector MakeTime() { + std::vector values{ + 1, // 1 second + 2, + 5, + 60, + 60 * 2, + 60 * 5, + 3600, + 3600 * 2, + 3600 * 5, + 3600 * 24, + 3600 * 24 * 2, + 3600 * 24 * 5, + 3600 * 999 + 60 * 59 + 59, + }; + + auto ret = values; + ret.reserve(values.size() * 2 + 1); + ret.push_back(0); + + // append negative values as well + std::transform(values.cbegin(), values.cend(), std::back_inserter(ret), [](int32_t x){ return -x; }); + return ret; +} + std::vector MakeDecimals(size_t /*precision*/, size_t scale) { const auto scale_multiplier = static_cast(std::pow(10, scale)); const long long int rhs_value = 12345678910; diff --git a/ut/value_generators.h b/ut/value_generators.h index f0bae267..889c74e0 100644 --- a/ut/value_generators.h +++ b/ut/value_generators.h @@ -8,6 +8,9 @@ #include #include +#include +#include +#include inline in_addr MakeIPv4(uint32_t ip) { static_assert(sizeof(in_addr) == sizeof(ip)); @@ -106,6 +109,21 @@ inline std::vector MakeDecimals() { return MakeDecimals(precision, scale); } +std::vector MakeTime(); + +template +inline std::vector MakeTime64() { + std::vector ret{}; + auto time32 = MakeTime(); + int64_t exp = 1; + for (size_t i = 0; i < Precision; ++i) { + exp *= 10; + } + std::transform(time32.cbegin(), time32.cend(), std::back_inserter(ret), [exp](int32_t x) { + return x * exp; + }); + return ret; +} template inline auto MakeDates() { From 6162baec5bc19ff8808cb4665c1698683e8b973d Mon Sep 17 00:00:00 2001 From: Andrew Slabko Date: Thu, 19 Mar 2026 07:12:48 +0100 Subject: [PATCH 2/4] Add enable_time_time64_type setting to queries --- ut/client_ut.cpp | 8 ++++++-- ut/roundtrip_column.cpp | 3 ++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/ut/client_ut.cpp b/ut/client_ut.cpp index 965ba0c8..6446fd3d 100644 --- a/ut/client_ut.cpp +++ b/ut/client_ut.cpp @@ -158,7 +158,9 @@ TEST_P(ClientCase, Array) { TEST_P(ClientCase, Time) { Block b; - client_->Execute("CREATE TEMPORARY TABLE IF NOT EXISTS test_clickhouse_cpp_time (t Time)"); + client_->Execute("CREATE TEMPORARY TABLE IF NOT EXISTS test_clickhouse_cpp_time (t Time) " + "ENGINE = Memory " + "SETTINGS enable_time_time64_type = 1"); auto t = std::make_shared(); int32_t ts = 3600 * 15 + 60 * 4 + 5; @@ -183,7 +185,9 @@ TEST_P(ClientCase, Time64) { Block b; client_->Execute("CREATE TEMPORARY TABLE IF NOT EXISTS test_clickhouse_cpp_time64 " - "(t0 Time64(0), t3 Time64(3), t6 Time64(6))"); + "(t0 Time64(0), t3 Time64(3), t6 Time64(6)) " + "ENGINE = Memory " + "SETTINGS enable_time_time64_type = 1"); auto t0 = std::make_shared(0); auto t3 = std::make_shared(3); auto t6 = std::make_shared(6); diff --git a/ut/roundtrip_column.cpp b/ut/roundtrip_column.cpp index 5af9624a..b578d976 100644 --- a/ut/roundtrip_column.cpp +++ b/ut/roundtrip_column.cpp @@ -37,7 +37,8 @@ ColumnRef RoundtripColumnValues(Client& client, ColumnRef expected) { const std::string type_name = result->GetType().GetName(); client.Execute("DROP TEMPORARY TABLE IF EXISTS temporary_roundtrip_table;"); // id column is to have the same order of rows on SELECT - client.Execute("CREATE TEMPORARY TABLE IF NOT EXISTS temporary_roundtrip_table (id UInt32, col " + type_name + ");"); + client.Execute("CREATE TEMPORARY TABLE IF NOT EXISTS temporary_roundtrip_table (id UInt32, col " + type_name + ") " + "ENGINE = Memory SETTINGS enable_time_time64_type = 1"); { Block block; block.AppendColumn("col", expected); From 06d664219b6876a9d24a6c6a8959c9e8569cb48f Mon Sep 17 00:00:00 2001 From: Andrew Slabko Date: Thu, 19 Mar 2026 09:22:07 +0100 Subject: [PATCH 3/4] Switch tests to the production instance --- .github/workflows/linux.yml | 4 ++-- .github/workflows/macos.yml | 8 ++++---- .github/workflows/windows_mingw.yml | 4 ++-- .github/workflows/windows_msvc.yml | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 52ecff0c..7a161e70 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -16,9 +16,9 @@ on: env: BUILD_TYPE: Release CLICKHOUSE_SERVER_IMAGE: "clickhouse/clickhouse-server:22.3" - CLICKHOUSE_SECURE_HOST: ${{ secrets.INTEGRATIONS_TEAM_TESTS_CLOUD_HOST_SMT }} + CLICKHOUSE_SECURE_HOST: ${{ secrets.INTEGRATIONS_TEAM_TESTS_CLOUD_HOST_SMT_PROD }} CLICKHOUSE_SECURE_USER: default - CLICKHOUSE_SECURE_PASSWORD: ${{ secrets.INTEGRATIONS_TEAM_TESTS_CLOUD_PASSWORD_SMT }} + CLICKHOUSE_SECURE_PASSWORD: ${{ secrets.INTEGRATIONS_TEAM_TESTS_CLOUD_PASSWORD_SMT_PROD }} jobs: build: diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 1f6ee6d6..e70d75c0 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -15,11 +15,11 @@ on: env: BUILD_TYPE: Release CLICKHOUSE_USER: default - CLICKHOUSE_PASSWORD: ${{ secrets.INTEGRATIONS_TEAM_TESTS_CLOUD_PASSWORD_SMT }} - CLICKHOUSE_SECURE_HOST: ${{ secrets.INTEGRATIONS_TEAM_TESTS_CLOUD_HOST_SMT }} + CLICKHOUSE_PASSWORD: ${{ secrets.INTEGRATIONS_TEAM_TESTS_CLOUD_PASSWORD_SMT_PROD }} + CLICKHOUSE_SECURE_HOST: ${{ secrets.INTEGRATIONS_TEAM_TESTS_CLOUD_HOST_SMT_PROD }} CLICKHOUSE_SECURE_PORT: 9440 CLICKHOUSE_SECURE_USER: default - CLICKHOUSE_SECURE_PASSWORD: ${{ secrets.INTEGRATIONS_TEAM_TESTS_CLOUD_PASSWORD_SMT }} + CLICKHOUSE_SECURE_PASSWORD: ${{ secrets.INTEGRATIONS_TEAM_TESTS_CLOUD_PASSWORD_SMT_PROD }} CLICKHOUSE_SECURE_DB: default jobs: @@ -74,7 +74,7 @@ jobs: run: | wget https://github.com/filimonov/go-tlsoffloader/releases/download/v0.1.2/go-tlsoffloader_0.1.2_Darwin_x86_64.tar.gz tar -xvzf go-tlsoffloader_0.1.2_Darwin_x86_64.tar.gz - ./go-tlsoffloader -l localhost:9000 -b ${{ secrets.INTEGRATIONS_TEAM_TESTS_CLOUD_HOST_SMT }}:9440 & + ./go-tlsoffloader -l localhost:9000 -b ${{ secrets.INTEGRATIONS_TEAM_TESTS_CLOUD_HOST_SMT_PROD }}:9440 & - name: Test working-directory: ${{github.workspace}}/build/ut diff --git a/.github/workflows/windows_mingw.yml b/.github/workflows/windows_mingw.yml index 39c45a25..c5a50961 100644 --- a/.github/workflows/windows_mingw.yml +++ b/.github/workflows/windows_mingw.yml @@ -16,7 +16,7 @@ on: env: BUILD_TYPE: Release CLICKHOUSE_USER: default - CLICKHOUSE_PASSWORD: ${{ secrets.INTEGRATIONS_TEAM_TESTS_CLOUD_PASSWORD_SMT }} + CLICKHOUSE_PASSWORD: ${{ secrets.INTEGRATIONS_TEAM_TESTS_CLOUD_PASSWORD_SMT_PROD }} # # CLICKHOUSE_HOST: localhost @@ -87,7 +87,7 @@ jobs: run: | wget https://github.com/filimonov/go-tlsoffloader/releases/download/v0.1.2/go-tlsoffloader_0.1.2_Windows_x86_64.tar.gz tar -xvzf go-tlsoffloader_0.1.2_Windows_x86_64.tar.gz - ./go-tlsoffloader.exe -l localhost:9000 -b ${{ secrets.INTEGRATIONS_TEAM_TESTS_CLOUD_HOST_SMT }}:9440 & + ./go-tlsoffloader.exe -l localhost:9000 -b ${{ secrets.INTEGRATIONS_TEAM_TESTS_CLOUD_HOST_SMT_PROD }}:9440 & - name: Test env: diff --git a/.github/workflows/windows_msvc.yml b/.github/workflows/windows_msvc.yml index 1a6e28e4..31992921 100644 --- a/.github/workflows/windows_msvc.yml +++ b/.github/workflows/windows_msvc.yml @@ -16,7 +16,7 @@ on: env: BUILD_TYPE: Release CLICKHOUSE_USER: default - CLICKHOUSE_PASSWORD: ${{ secrets.INTEGRATIONS_TEAM_TESTS_CLOUD_PASSWORD_SMT }} + CLICKHOUSE_PASSWORD: ${{ secrets.INTEGRATIONS_TEAM_TESTS_CLOUD_PASSWORD_SMT_PROD }} # CLICKHOUSE_HOST: localhost # CLICKHOUSE_PORT: 9000 # CLICKHOUSE_USER: default @@ -61,7 +61,7 @@ jobs: choco install wget wget https://github.com/filimonov/go-tlsoffloader/releases/download/v0.1.2/go-tlsoffloader_0.1.2_Windows_x86_64.tar.gz tar -xvzf go-tlsoffloader_0.1.2_Windows_x86_64.tar.gz - ./go-tlsoffloader.exe -l localhost:9000 -b ${{ secrets.INTEGRATIONS_TEAM_TESTS_CLOUD_HOST_SMT }}:9440 & + ./go-tlsoffloader.exe -l localhost:9000 -b ${{ secrets.INTEGRATIONS_TEAM_TESTS_CLOUD_HOST_SMT_PROD }}:9440 & - name: Test env: From a125311fa9700c04e60662fbc7c22026022df6a2 Mon Sep 17 00:00:00 2001 From: Andrew Slabko Date: Thu, 19 Mar 2026 10:20:36 +0100 Subject: [PATCH 4/4] Update ClickHouse version for local tests --- .github/workflows/linux.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 7a161e70..dd58b12d 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -15,7 +15,7 @@ on: env: BUILD_TYPE: Release - CLICKHOUSE_SERVER_IMAGE: "clickhouse/clickhouse-server:22.3" + CLICKHOUSE_SERVER_IMAGE: "clickhouse/clickhouse-server:25.10" CLICKHOUSE_SECURE_HOST: ${{ secrets.INTEGRATIONS_TEAM_TESTS_CLOUD_HOST_SMT_PROD }} CLICKHOUSE_SECURE_USER: default CLICKHOUSE_SECURE_PASSWORD: ${{ secrets.INTEGRATIONS_TEAM_TESTS_CLOUD_PASSWORD_SMT_PROD }} @@ -128,7 +128,7 @@ jobs: - name: Test - Start ClickHouse server in background run: | docker pull ${CLICKHOUSE_SERVER_IMAGE} - docker run -d --name clickhouse -p 9000:9000 ${CLICKHOUSE_SERVER_IMAGE} + docker run -d --name clickhouse -p 9000:9000 -e CLICKHOUSE_SKIP_USER_SETUP=1 ${CLICKHOUSE_SERVER_IMAGE} docker ps -a docker stats -a --no-stream ## Check and wait until CH is ready to accept connections