Skip to content

Commit e44e9d4

Browse files
authored
Merge pull request #530 from IyeOnline/topic/expanded-low-cardinality-support
Support writing generic LowCardinality(T)
2 parents dc54c49 + 338cc06 commit e44e9d4

10 files changed

Lines changed: 460 additions & 14 deletions

clickhouse/columns/factory.cpp

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,8 +258,16 @@ static ColumnRef CreateColumnFromAst(const TypeAst& ast, CreateColumnByTypeSetti
258258
std::make_shared<ColumnUInt8>()
259259
)
260260
);
261-
default:
262-
throw UnimplementedError("LowCardinality(" + nested.name + ") is not supported");
261+
default: {
262+
// Generic LowCardinality(T): build the inner column and
263+
// wrap it. Works for any fixed-size dictionary type that
264+
// AppendToDictionary supports.
265+
auto inner = CreateColumnFromAst(nested, settings);
266+
if (!inner) {
267+
throw UnimplementedError("LowCardinality(" + nested.name + ") is not supported");
268+
}
269+
return std::make_shared<ColumnLowCardinality>(std::move(inner));
270+
}
263271
}
264272
}
265273
}

clickhouse/columns/lowcardinality.cpp

Lines changed: 115 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
#include "string.h"
44
#include "nullable.h"
5+
#include "numeric.h"
6+
#include "enum.h"
7+
#include "date.h"
8+
#include "ip4.h"
9+
#include "ip6.h"
10+
#include "uuid.h"
11+
#include "../base/socket.h" // for htonl/ntohl and in_addr/in6_addr
512
#include "../base/wire_format.h"
613

714
#include <city.h>
@@ -10,6 +17,7 @@
1017
#include <string_view>
1118
#include <type_traits>
1219
#include <cmath>
20+
#include <cstring>
1321

1422
#include <cassert>
1523

@@ -78,30 +86,60 @@ inline ResultColumnType & column_down_cast(ColumnType & c) {
7886
return dynamic_cast<ResultColumnType &>(c);
7987
}
8088

81-
// std::visit-ish function to avoid including <variant> header, which is not present in older version of XCode.
82-
template <typename Vizitor, typename ColumnType>
83-
inline auto VisitIndexColumn(Vizitor && vizitor, ColumnType && col) {
84-
switch (col.Type()->GetCode()) {
89+
// Number of bytes an ItemView holds for a fixed-size dictionary type, or 0 for
90+
// variable-size (String/FixedString) or unsupported types. Used to build a
91+
// correctly-sized zero value for the default/null dictionary item.
92+
inline size_t FixedSizeForDictionaryType(Type::Code code) {
93+
switch (code) {
94+
case Type::Int8:
8595
case Type::UInt8:
86-
return vizitor(column_down_cast<ColumnUInt8>(col));
96+
return 1;
97+
case Type::Int16:
8798
case Type::UInt16:
88-
return vizitor(column_down_cast<ColumnUInt16>(col));
99+
case Type::Date:
100+
return 2;
101+
case Type::Int32:
89102
case Type::UInt32:
90-
return vizitor(column_down_cast<ColumnUInt32>(col));
103+
case Type::Float32:
104+
case Type::DateTime:
105+
case Type::Date32:
106+
case Type::IPv4:
107+
return 4;
108+
case Type::Int64:
91109
case Type::UInt64:
92-
return vizitor(column_down_cast<ColumnUInt64>(col));
110+
case Type::Float64:
111+
return 8;
112+
case Type::Int128:
113+
case Type::UInt128:
114+
case Type::IPv6:
115+
case Type::UUID:
116+
return 16;
93117
default:
94-
throw ValidationError("Invalid index column type " + col.GetType().GetName());
118+
return 0;
95119
}
96120
}
97121

122+
// A zero-filled, correctly-sized ItemView for a fixed-size dictionary type. The
123+
// backing buffer is static so the non-owning view stays valid.
124+
inline ItemView ZeroItemForDictionary(Type::Code code) {
125+
if (const auto size = FixedSizeForDictionaryType(code)) {
126+
static const char zeros[16] = {};
127+
if (size > sizeof(zeros)) {
128+
throw AssertionError("The size of item view for ColumnLowCardinality exceeds the buffer size");
129+
}
130+
return ItemView{code, std::string_view{zeros, size}};
131+
}
132+
// Variable-size types (String/FixedString) accept an empty value.
133+
return ItemView{code, std::string_view{}};
134+
}
135+
98136
// A special NULL-item, which is expected at pos(0) in dictionary,
99137
// note that we distinguish empty string from NULL-value.
100138
inline auto GetNullItemForDictionary(const ColumnRef dictionary) {
101139
if (auto n = dictionary->As<ColumnNullable>()) {
102140
return ItemView {};
103141
} else {
104-
return ItemView{dictionary->Type()->GetCode(), std::string_view{}};
142+
return ZeroItemForDictionary(dictionary->Type()->GetCode());
105143
}
106144
}
107145

@@ -111,7 +149,7 @@ inline ItemView GetDefaultItemForDictionary(const ColumnRef dictionary) {
111149
if (auto n = dictionary->As<ColumnNullable>()) {
112150
return GetDefaultItemForDictionary(n->Nested());
113151
} else {
114-
return ItemView{dictionary->Type()->GetCode(), std::string_view{}};
152+
return ZeroItemForDictionary(dictionary->Type()->GetCode());
115153
}
116154
}
117155

@@ -147,6 +185,72 @@ inline void AppendToDictionary(Column& dictionary, const ItemView & item) {
147185
case Type::Nullable:
148186
AppendNullableToDictionary(column_down_cast<ColumnNullable>(dictionary), item);
149187
return;
188+
// Fixed-size dictionary types. The ItemView holds the raw stored bytes
189+
// (see the matching ColumnXxx::GetItem), so we re-append the raw value.
190+
case Type::Int8:
191+
column_down_cast<ColumnInt8>(dictionary).Append(item.get<int8_t>());
192+
return;
193+
case Type::Int16:
194+
column_down_cast<ColumnInt16>(dictionary).Append(item.get<int16_t>());
195+
return;
196+
case Type::Int32:
197+
column_down_cast<ColumnInt32>(dictionary).Append(item.get<int32_t>());
198+
return;
199+
case Type::Int64:
200+
column_down_cast<ColumnInt64>(dictionary).Append(item.get<int64_t>());
201+
return;
202+
case Type::UInt8:
203+
column_down_cast<ColumnUInt8>(dictionary).Append(item.get<uint8_t>());
204+
return;
205+
case Type::UInt16:
206+
column_down_cast<ColumnUInt16>(dictionary).Append(item.get<uint16_t>());
207+
return;
208+
case Type::UInt32:
209+
column_down_cast<ColumnUInt32>(dictionary).Append(item.get<uint32_t>());
210+
return;
211+
case Type::UInt64:
212+
column_down_cast<ColumnUInt64>(dictionary).Append(item.get<uint64_t>());
213+
return;
214+
case Type::Int128:
215+
column_down_cast<ColumnInt128>(dictionary).Append(item.get<Int128>());
216+
return;
217+
case Type::UInt128:
218+
column_down_cast<ColumnUInt128>(dictionary).Append(item.get<UInt128>());
219+
return;
220+
case Type::Float32:
221+
column_down_cast<ColumnFloat32>(dictionary).Append(item.get<float>());
222+
return;
223+
case Type::Float64:
224+
column_down_cast<ColumnFloat64>(dictionary).Append(item.get<double>());
225+
return;
226+
case Type::Date:
227+
column_down_cast<ColumnDate>(dictionary).AppendRaw(item.get<uint16_t>());
228+
return;
229+
case Type::Date32:
230+
column_down_cast<ColumnDate32>(dictionary).AppendRaw(item.get<int32_t>());
231+
return;
232+
case Type::DateTime:
233+
column_down_cast<ColumnDateTime>(dictionary).AppendRaw(item.get<uint32_t>());
234+
return;
235+
case Type::IPv4:
236+
// ColumnIPv4::Append applies htonl, and GetItem returns the stored
237+
// (already byte-swapped) value, so undo the swap to re-store as-is.
238+
column_down_cast<ColumnIPv4>(dictionary).Append(ntohl(item.get<uint32_t>()));
239+
return;
240+
case Type::IPv6: {
241+
in6_addr addr;
242+
std::memcpy(&addr, item.data.data(), sizeof(addr));
243+
column_down_cast<ColumnIPv6>(dictionary).Append(addr);
244+
return;
245+
}
246+
case Type::UUID: {
247+
UUID value;
248+
std::memcpy(&value.first, item.data.data(), sizeof(value.first));
249+
std::memcpy(&value.second, item.data.data() + sizeof(value.first),
250+
sizeof(value.second));
251+
column_down_cast<ColumnUUID>(dictionary).Append(value);
252+
return;
253+
}
150254
default:
151255
throw ValidationError("Unexpected dictionary column type: " + dictionary.GetType().GetName());
152256
}

clickhouse/columns/uuid.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ namespace clickhouse {
1212
*/
1313
class ColumnUUID : public Column {
1414
public:
15+
using ValueType = UUID;
16+
1517
ColumnUUID();
1618

1719
explicit ColumnUUID(ColumnRef data);

ut/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ cc_test(
105105
"connection_failed_client_test.cpp",
106106
"connection_failed_client_test.h",
107107
"low_cardinality_nullable_tests.cpp",
108+
"low_cardinality_types_ut.cpp",
108109
"performance_tests.cpp",
109110
"readonly_client_test.cpp",
110111
"readonly_client_test.h",

ut/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ SET ( clickhouse-cpp-ut-src
1010
column_as_ut.cpp
1111
column_array_ut.cpp
1212
itemview_ut.cpp
13+
low_cardinality_types_ut.cpp
1314
socket_ut.cpp
1415
stream_ut.cpp
1516
type_parser_ut.cpp

ut/CreateColumnByType_ut.cpp

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,28 @@ TEST(CreateColumnByType, LowCardinalityAsWrappedColumn) {
7575
ASSERT_EQ(Type::FixedString, CreateColumnByType("LowCardinality(FixedString(10000))", create_column_settings)->As<ColumnFixedString>()->GetType().GetCode());
7676
}
7777

78+
TEST(CreateColumnByType, LowCardinalityGeneralInnerTypes) {
79+
// LowCardinality used to be supported only over String/FixedString. The
80+
// factory now builds a generic ColumnLowCardinality for any fixed-size inner
81+
// type.
82+
for (const auto* type_name : {
83+
"LowCardinality(Int8)",
84+
"LowCardinality(Int64)",
85+
"LowCardinality(UInt64)",
86+
"LowCardinality(Float64)",
87+
"LowCardinality(Date)",
88+
"LowCardinality(DateTime)",
89+
"LowCardinality(Nullable(Int64))",
90+
"LowCardinality(Nullable(Float64))",
91+
}) {
92+
auto col = CreateColumnByType(type_name);
93+
ASSERT_NE(nullptr, col) << type_name;
94+
ASSERT_EQ(Type::LowCardinality, col->GetType().GetCode()) << type_name;
95+
ASSERT_NE(nullptr, col->As<ColumnLowCardinality>()) << type_name;
96+
EXPECT_EQ(std::string{type_name}, col->GetType().GetName()) << type_name;
97+
}
98+
}
99+
78100
TEST(CreateColumnByType, LowCardinality) {
79101
// In the default (non-wrapped) mode, LowCardinality(String)/LowCardinality(FixedString) map to
80102
// the base ColumnLowCardinality (like Array/Nullable/Tuple/Map do), and the strongly-typed

ut/columns_ut.cpp

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1993,6 +1993,76 @@ TEST(ColumnsCase, ColumnMapT_Wrap) {
19931993
EXPECT_EQ("abc", map_view.At(2));
19941994
}
19951995

1996+
// Regression tests for general LowCardinality support over non-String inner
1997+
// types (previously only String/FixedString were supported).
1998+
TEST(ColumnLowCardinality, AppendAndReadNumeric) {
1999+
auto col = std::make_shared<ColumnLowCardinalityT<ColumnInt64>>();
2000+
col->Append(7);
2001+
col->Append(7);
2002+
col->Append(9);
2003+
col->Append(7);
2004+
2005+
ASSERT_EQ(4u, col->Size());
2006+
EXPECT_EQ(7, col->At(0));
2007+
EXPECT_EQ(7, col->At(1));
2008+
EXPECT_EQ(9, col->At(2));
2009+
EXPECT_EQ(7, col->At(3));
2010+
// Dictionary holds the default item plus the two distinct values {7, 9}.
2011+
EXPECT_EQ(3u, col->GetDictionarySize());
2012+
2013+
// GetItem returns the raw value with the correct type code.
2014+
const auto item = col->GetItem(2);
2015+
EXPECT_EQ(Type::Int64, item.type);
2016+
EXPECT_EQ(9, item.get<int64_t>());
2017+
}
2018+
2019+
TEST(ColumnLowCardinality, AppendAndReadNullableNumeric) {
2020+
auto col
2021+
= std::make_shared<ColumnLowCardinalityT<ColumnNullableT<ColumnInt64>>>();
2022+
col->Append(7);
2023+
col->Append(std::nullopt);
2024+
col->Append(7);
2025+
col->Append(9);
2026+
col->Append(std::nullopt);
2027+
2028+
ASSERT_EQ(5u, col->Size());
2029+
EXPECT_EQ(std::optional<int64_t>{7}, col->At(0));
2030+
EXPECT_EQ(std::nullopt, col->At(1));
2031+
EXPECT_EQ(std::optional<int64_t>{7}, col->At(2));
2032+
EXPECT_EQ(std::optional<int64_t>{9}, col->At(3));
2033+
EXPECT_EQ(std::nullopt, col->At(4));
2034+
2035+
// Null rows are represented by a Void ItemView.
2036+
EXPECT_EQ(Type::Void, col->GetItem(1).type);
2037+
EXPECT_EQ(Type::Int64, col->GetItem(0).type);
2038+
}
2039+
2040+
TEST(ColumnLowCardinality, NumericLoadAndSave) {
2041+
auto column_A = std::make_shared<ColumnLowCardinalityT<ColumnUInt64>>();
2042+
for (auto v : {1u, 2u, 1u, 3u, 2u, 1u}) {
2043+
column_A->Append(v);
2044+
}
2045+
2046+
const auto BufferSize = 64 * 1024;
2047+
std::unique_ptr<char[]> buffer = std::make_unique<char[]>(BufferSize);
2048+
memset(buffer.get(), 0, BufferSize);
2049+
{
2050+
ArrayOutput output(buffer.get(), BufferSize);
2051+
ASSERT_NO_THROW(column_A->Save(&output));
2052+
}
2053+
2054+
auto column_B = std::make_shared<ColumnLowCardinalityT<ColumnUInt64>>();
2055+
{
2056+
ArrayInput input(buffer.get(), BufferSize);
2057+
ASSERT_TRUE(column_B->Load(&input, column_A->Size()));
2058+
}
2059+
2060+
ASSERT_EQ(column_A->Size(), column_B->Size());
2061+
for (size_t i = 0; i < column_A->Size(); ++i) {
2062+
EXPECT_EQ(column_A->At(i), column_B->At(i)) << "row " << i;
2063+
}
2064+
}
2065+
19962066
TEST(ColumnsCase, ColumnMapT_Wrap_AcceptsLvalue) {
19972067
auto tupls = std::make_shared<ColumnTuple>(std::vector<ColumnRef>{
19982068
std::make_shared<ColumnUInt64>(),

0 commit comments

Comments
 (0)