Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 33 additions & 11 deletions clickhouse/types/type_parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <cmath>
#include <map>
#include <mutex>
#include <array>
#include <unordered_map>

#if defined _win_
Expand Down Expand Up @@ -76,6 +77,20 @@ static const std::unordered_map<std::string, Type::Code> kTypeCode = {
{ "JSON", Type::JSON },
};

static constexpr auto kUnescapeMap = [](){
std::array<char, 256> m{};
// std::fill or m.fill are not yet constant expressions in C++17
for (auto& x : m) x = (char)-1;
m['0'] = '\0';
m['\\'] = '\\';
m['b'] = '\b';
m['f'] = '\f';
m['n'] = '\n';
m['r'] = '\r';
m['t'] = '\t';
return m;
}();

template <typename L, typename R>
inline int CompateStringsCaseInsensitive(const L& left, const R& right) {
int64_t size_diff = left.size() - right.size();
Expand Down Expand Up @@ -247,19 +262,26 @@ TypeParser::Token TypeParser::NextToken() {
return Token{Token::Comma, StringView(cur_++, 1)};
case '\'':
{
const auto end_quote_length = 1;
const StringView end_quote{cur_, end_quote_length};
// Fast forward to the closing quote.
const auto start = cur_++;
for (; cur_ < end_ - end_quote_length; ++cur_) {
// TODO (nemkov): handle escaping ?
if (end_quote == StringView{cur_, end_quote_length}) {
cur_ += end_quote_length;

return Token{Token::QuotedString, StringView{start, cur_}};
scratch_.clear();
scratch_ += *(cur_++); // quotes must be included
for (; end_ - cur_ > 1; ++cur_) {
if (*cur_ == '\\' && end_ - cur_ > 1 && kUnescapeMap[(uint8_t)*(cur_ + 1)] != (char)-1) {
scratch_ += kUnescapeMap[(uint8_t)*(cur_ + 1)];
Comment on lines +265 to +269

@slabko slabko Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is, unfortunately, true and can result in errors different from what they really are if the type name is incorrect. However this fix require a separate PR.

++cur_;
}
else if (*cur_ == '\\' && end_ - cur_ > 1 && *(cur_ + 1) == '\'') {
scratch_ += '\'';
++cur_;
}
else if ('\'' == *cur_) {
scratch_ += *(cur_++);
return Token{Token::QuotedString, StringView(scratch_)};
}
else {
scratch_ += *cur_;
}
}
return Token{Token::QuotedString, StringView(cur_++, 1)};
return Token{Token::QuotedString, StringView(scratch_)};
}
case '"':
case '`':
Expand Down
10 changes: 6 additions & 4 deletions clickhouse/types/type_parser.h
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ class TypeParser {
bool Parse(TypeAst* type);

private:
// WARNING: The StringView in this token is only valid until the next NextToken() call.
Token NextToken();

private:
Expand All @@ -85,10 +86,11 @@ class TypeParser {

TypeAst* type_;
std::stack<TypeAst*> open_elements_;
// Backing storage for unescaped QuotedIdentifier token values. When a
// quoted identifier contains escape sequences the unescaped content is
// written here and the returned StringView points into this string.
// Valid only until the next NextToken() call.

// Backing storage for identifiers with processed escape sequences. When an
// identifier contains escape sequences, the cleaned content is written here and
// the returned StringView points into this scratch.
// WARNING: Valid only until the next NextToken() call.
std::string scratch_;
};

Expand Down
38 changes: 35 additions & 3 deletions clickhouse/types/types.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,25 @@
#include <city.h>

#include <cassert>
#include <array>

namespace clickhouse {

static constexpr auto kEscapeMap = []{
std::array<char, 256> m{};
// std::fill or m.fill are not yet constant expressions in C++17
for (auto& x : m) x = (char)-1;
m['\''] = '\'';
m['\0'] = '0';
m['\\'] = '\\';
m['\b'] = 'b';
m['\f'] = 'f';
m['\n'] = 'n';
m['\r'] = 'r';
m['\t'] = 't';
return m;
}();

Type::Type(const Code code)
: code_(code)
, type_unique_id_(0)
Expand Down Expand Up @@ -304,6 +320,22 @@ DecimalType::DecimalType(size_t precision, size_t scale)
// TODO: assert(precision <= 38 && precision > 0);
}

static std::string EscapeStringLiteral(std::string_view in)
{
std::string ret{};
ret.reserve(in.size());
for (char c : in) {
if (kEscapeMap[(uint8_t)c] != (char)-1) {
ret.push_back('\\');
ret.push_back(kEscapeMap[(uint8_t)c]);
}
else {
ret.push_back(c);
}
}
return ret;
}

std::string DecimalType::GetName() const {
switch (GetCode()) {
case Decimal:
Expand Down Expand Up @@ -340,7 +372,7 @@ std::string EnumType::GetName() const {

for (auto ei = value_to_name_.begin(); ei != value_to_name_.end();) {
result += "'";
result += ei->second;
result += EscapeStringLiteral(ei->second);
result += "' = ";
result += std::to_string(ei->first);

Expand Down Expand Up @@ -413,7 +445,7 @@ std::string DateTimeType::GetName() const {
std::string datetime_representation = "DateTime";
const auto & timezone = Timezone();
if (!timezone.empty())
datetime_representation += "('" + timezone + "')";
datetime_representation += "('" + EscapeStringLiteral(timezone) + "')";

return datetime_representation;
}
Expand All @@ -436,7 +468,7 @@ std::string DateTime64Type::GetName() const {

const auto & timezone = Timezone();
if (!timezone.empty()) {
datetime64_representation += ", '" + timezone + "'";
datetime64_representation += ", '" + EscapeStringLiteral(timezone) + "'";
}

datetime64_representation += ")";
Expand Down
59 changes: 59 additions & 0 deletions ut/CreateColumnByType_ut.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <clickhouse/columns/date.h>
#include <clickhouse/columns/numeric.h>
#include <clickhouse/columns/string.h>
#include <clickhouse/columns/tuple.h>
#include <clickhouse/columns/json.h>

#include <gtest/gtest.h>
Expand Down Expand Up @@ -50,6 +51,64 @@ TEST(CreateColumnByType, DateTime) {

ASSERT_EQ(CreateColumnByType("DateTime('UTC')")->As<ColumnDateTime>()->Timezone(), "UTC");
ASSERT_EQ(CreateColumnByType("DateTime64(3, 'UTC')")->As<ColumnDateTime64>()->Timezone(), "UTC");
ASSERT_EQ(CreateColumnByType("DateTime('Etc/Can\\'t')")->As<ColumnDateTime>()->Timezone(), "Etc/Can't");
ASSERT_EQ(CreateColumnByType("DateTime64(3, 'A\\\\B')")->As<ColumnDateTime64>()->Timezone(), "A\\B");
}

TEST(CreateColumnByType, EnumEscapedNames) {
const std::vector<Type::EnumItem> enum_items = {{"can't", 1}, {"a\\b", 2}, {"a,b=(c)", 3}, {"", 4}};
auto col = CreateColumnByType("Enum8('can\\'t' = 1, 'a\\\\b' = 2, 'a,b=(c)' = 3, '' = 4)");
ASSERT_NE(nullptr, col);
ASSERT_TRUE(col->Type()->IsEqual(Type::CreateEnum8(enum_items)));
}

// Round-trip: build a Type from raw values, render it via GetName() (escape),
// re-parse the rendered name via CreateColumnByType (unescape) and verify the
// raw values survive the render->parse cycle. Uses only escape symbols that are
// currently implemented.

TEST(CreateColumnByType, RoundTrip_Enum) {
const std::vector<Type::EnumItem> enum_items = {{"can't", 1}, {"a\\b", 2}, {"tab\there", 3}, {"line\nbreak", 4}};
auto type = Type::CreateEnum8(enum_items);

auto col = CreateColumnByType(type->GetName());
ASSERT_NE(nullptr, col);
EXPECT_TRUE(col->Type()->IsEqual(type));

const auto* enum_type = col->Type()->As<EnumType>();
ASSERT_NE(nullptr, enum_type);
EXPECT_EQ(enum_type->GetEnumName(1), "can't");
EXPECT_EQ(enum_type->GetEnumValue("a\\b"), 2);
EXPECT_EQ(enum_type->GetEnumName(3), "tab\there");
EXPECT_EQ(enum_type->GetEnumValue("line\nbreak"), 4);
}

TEST(CreateColumnByType, RoundTrip_DateTime) {
auto type = Type::CreateDateTime("Etc/Can't");

auto col = CreateColumnByType(type->GetName());
ASSERT_NE(nullptr, col);
EXPECT_EQ(col->As<ColumnDateTime>()->Timezone(), "Etc/Can't");
}

TEST(CreateColumnByType, RoundTrip_DateTime64) {
auto type = Type::CreateDateTime64(3, "A\\B");

auto col = CreateColumnByType(type->GetName());
ASSERT_NE(nullptr, col);
EXPECT_EQ(col->As<ColumnDateTime64>()->Timezone(), "A\\B");
}

TEST(CreateColumnByType, RoundTrip_Tuple) {
const std::vector<std::string> item_names = {"a`b", "c.d"};
auto type = Type::CreateTuple({Type::CreateSimple<uint8_t>(), Type::CreateString()}, item_names);

auto col = CreateColumnByType(type->GetName());
ASSERT_NE(nullptr, col);

const auto* tuple_type = col->Type()->As<TupleType>();
ASSERT_NE(nullptr, tuple_type);
EXPECT_EQ(tuple_type->GetItemNames(), item_names);
}

TEST(CreateColumnByType, AggregateFunction) {
Expand Down
54 changes: 54 additions & 0 deletions ut/client_ut.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,60 @@ TEST_P(ClientCase, Enum) {
EXPECT_EQ(sizeof(TEST_DATA)/sizeof(TEST_DATA[0]), row);
}

// Live-server round-trip of escape sequences in an Enum type: hand-write a
// CREATE TABLE whose Enum labels contain escape sequences, insert matching
// values via the streaming BeginInsert/SendInsertBlock/EndInsert API (so the
// inserted block's columns are derived from the server-reported schema), select
// them back and verify the client decodes the labels unchanged.
// NOTE: Includes backspace ('\b') and other escape sequences to verify the client unescapes Enum labels correctly.
// NOTE: DateTime/DateTime64 are intentionally not covered here because the
// server validates timezone names, so escaped/unknown timezones can't be
// stored (those cases stay covered by the client-side unit tests).
TEST_P(ClientCase, EscapeRoundtrip_Enum) {
const std::vector<Type::EnumItem> enum_items = {
{"q'q", 1}, {"bs\\bs", 2}, {"tab\ttab", 3}, {"nl\nnl", 4}, {"cr\rcr", 5}, {"bsp\bbsp", 6},
};

client_->Execute("DROP TEMPORARY TABLE IF EXISTS test_clickhouse_cpp_escape_enum;");
client_->Execute(
"CREATE TEMPORARY TABLE IF NOT EXISTS test_clickhouse_cpp_escape_enum "
"(id UInt64, e Enum8('q\\'q' = 1, 'bs\\\\bs' = 2, 'tab\\ttab' = 3, 'nl\\nnl' = 4, 'cr\\rcr' = 5, 'bsp\\bbsp' = 6))");

{
auto block = client_->BeginInsert("INSERT INTO test_clickhouse_cpp_escape_enum VALUES");
ASSERT_EQ(size_t(2), block.GetColumnCount());
auto id = block[0]->As<ColumnUInt64>();
auto e = block[1]->As<ColumnEnum8>();
for (const auto& [name, value] : enum_items) {
id->Append(static_cast<uint64_t>(value));
e->Append(name);
}
block.RefreshRowCount();
client_->SendInsertBlock(block);
client_->EndInsert();
}

size_t row = 0;
client_->Select("SELECT id, e FROM test_clickhouse_cpp_escape_enum ORDER BY id",
[&](const Block& block) {
if (block.GetRowCount() == 0) {
return;
}
const auto* enum_type = block[1]->Type()->As<EnumType>();
ASSERT_NE(nullptr, enum_type);
for (size_t i = 0; i < block.GetRowCount(); ++i, ++row) {
ASSERT_LT(row, enum_items.size());
const auto& [name, value] = enum_items[row];
EXPECT_EQ(static_cast<uint64_t>(value), (*block[0]->As<ColumnUInt64>())[i]);
EXPECT_EQ(value, block[1]->As<ColumnEnum8>()->At(i));
EXPECT_EQ(name, block[1]->As<ColumnEnum8>()->NameAt(i));
EXPECT_EQ(name, enum_type->GetEnumName(value));
EXPECT_EQ(value, enum_type->GetEnumValue(name));
}
});
EXPECT_EQ(enum_items.size(), row);
}

TEST_P(ClientCase, Decimal) {
client_->Execute(
"CREATE TEMPORARY TABLE IF NOT EXISTS "
Expand Down
Loading
Loading