From a591622f4ddd91edc886a86d80657db0ab0a01ae Mon Sep 17 00:00:00 2001 From: "David E. Wheeler" Date: Wed, 19 Nov 2025 14:28:10 -0500 Subject: [PATCH] Add JSON support Implemented the same as String, which seems to work fine except for round-tripping. I assume it's not actually a string in the protocol so it will need a different format. But hopefully the rest of the infrastructure for it is helpful. --- README.md | 1 + clickhouse/columns/factory.cpp | 6 + clickhouse/columns/itemview.cpp | 1 + clickhouse/columns/itemview.h | 2 +- clickhouse/columns/string.cpp | 211 +++++++++++++++++++++++++++++++ clickhouse/columns/string.h | 72 +++++++++++ clickhouse/types/type_parser.cpp | 5 +- clickhouse/types/types.cpp | 7 + clickhouse/types/types.h | 5 +- ut/Column_ut.cpp | 1 + ut/CreateColumnByType_ut.cpp | 3 + ut/column_array_ut.cpp | 15 +++ ut/columns_ut.cpp | 23 ++++ ut/itemview_ut.cpp | 3 + ut/type_parser_ut.cpp | 9 ++ ut/types_ut.cpp | 1 + ut/utils.cpp | 3 + ut/utils_ut.cpp | 1 + ut/value_generators.cpp | 34 +++++ ut/value_generators.h | 1 + 20 files changed, 400 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index bd485753..ae3a894c 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ C++ client for [ClickHouse](https://clickhouse.com/). * UUID * Map * Point, Ring, Polygon, MultiPolygon +* JSON ## Dependencies In the most basic case one needs only: diff --git a/clickhouse/columns/factory.cpp b/clickhouse/columns/factory.cpp index 73f2cde1..244ad5a1 100644 --- a/clickhouse/columns/factory.cpp +++ b/clickhouse/columns/factory.cpp @@ -86,6 +86,8 @@ static ColumnRef CreateTerminalColumn(const TypeAst& ast) { case Type::String: return std::make_shared(); + case Type::JSON: + return std::make_shared(); case Type::FixedString: return std::make_shared(GetASTChildElement(ast, 0).value); @@ -201,6 +203,8 @@ static ColumnRef CreateColumnFromAst(const TypeAst& ast, CreateColumnByTypeSetti // TODO (nemkov): update this to maximize code reuse. case Type::String: return std::make_shared>(); + case Type::JSON: + return std::make_shared>(); case Type::FixedString: return std::make_shared>(GetASTChildElement(nested, 0).value); case Type::Nullable: @@ -214,6 +218,8 @@ static ColumnRef CreateColumnFromAst(const TypeAst& ast, CreateColumnByTypeSetti // TODO (nemkov): update this to maximize code reuse. case Type::String: return std::make_shared>(); + case Type::JSON: + return std::make_shared>(); case Type::FixedString: return std::make_shared>(GetASTChildElement(nested, 0).value); case Type::Nullable: diff --git a/clickhouse/columns/itemview.cpp b/clickhouse/columns/itemview.cpp index c92627d2..42eed60e 100644 --- a/clickhouse/columns/itemview.cpp +++ b/clickhouse/columns/itemview.cpp @@ -70,6 +70,7 @@ void ItemView::ValidateData(Type::Code type, DataType data) { case Type::Code::String: case Type::Code::FixedString: + case Type::Code::JSON: // value can be of any size return; diff --git a/clickhouse/columns/itemview.h b/clickhouse/columns/itemview.h index 199994b6..66474cdd 100644 --- a/clickhouse/columns/itemview.h +++ b/clickhouse/columns/itemview.h @@ -69,7 +69,7 @@ struct ItemView { if (sizeof(ValueType) == data.size()) { return *reinterpret_cast(data.data()); } else { - throw AssertionError("Incompatitable value type and size. Requested size: " + throw AssertionError("Incompatible value type and size. Requested size: " + std::to_string(sizeof(ValueType)) + " stored size: " + std::to_string(data.size())); } } diff --git a/clickhouse/columns/string.cpp b/clickhouse/columns/string.cpp index dff45bac..93ceb609 100644 --- a/clickhouse/columns/string.cpp +++ b/clickhouse/columns/string.cpp @@ -330,4 +330,215 @@ ItemView ColumnString::GetItem(size_t index) const { return ItemView{Type::String, this->At(index)}; } +struct ColumnJSON::Block +{ + using CharT = typename std::string::value_type; + + explicit Block(size_t starting_capacity) + : size(0), + capacity(starting_capacity), + data_(new CharT[capacity]) + {} + + inline auto GetAvailable() const { + return capacity - size; + } + + std::string_view AppendUnsafe(std::string_view str) { + const auto pos = &data_[size]; + + memcpy(pos, str.data(), str.size()); + size += str.size(); + + return std::string_view(pos, str.size()); + } + + auto GetCurrentWritePos() { + return &data_[size]; + } + + std::string_view ConsumeTailAsJSONViewUnsafe(size_t len) { + const auto start = &data_[size]; + size += len; + return std::string_view(start, len); + } + + size_t size; + const size_t capacity; + std::unique_ptr data_; +}; + +ColumnJSON::ColumnJSON() + : Column(Type::CreateJSON()) +{ +} + +ColumnJSON::ColumnJSON(size_t element_count) + : Column(Type::CreateJSON()) +{ + items_.reserve(element_count); + // 16 is arbitrary number, assumption that string values are about ~256 bytes long. + blocks_.reserve(std::max(1, element_count / 16)); +} + +ColumnJSON::ColumnJSON(const std::vector& data) + : ColumnJSON() +{ + items_.reserve(data.size()); + blocks_.emplace_back(ComputeTotalSize(data)); + + for (const auto & s : data) { + AppendUnsafe(s); + } +} + +ColumnJSON::ColumnJSON(std::vector&& data) + : ColumnJSON() +{ + items_.reserve(data.size()); + + for (auto&& d : data) { + append_data_.emplace_back(std::move(d)); + auto& last_data = append_data_.back(); + items_.emplace_back(std::string_view{ last_data.data(),last_data.length() }); + } +} + +ColumnJSON::~ColumnJSON() +{} + +void ColumnJSON::Reserve(size_t new_cap) { + items_.reserve(new_cap); + // 16 is arbitrary number, assumption that string values are about ~256 bytes long. + blocks_.reserve(std::max(1, new_cap / 16)); +} + +void ColumnJSON::Append(std::string_view str) { + if (blocks_.size() == 0 || blocks_.back().GetAvailable() < str.length()) { + blocks_.emplace_back(std::max(DEFAULT_BLOCK_SIZE, str.size())); + } + + items_.emplace_back(blocks_.back().AppendUnsafe(str)); +} + +void ColumnJSON::Append(const char* str) { + Append(std::string_view(str, strlen(str))); +} + +void ColumnJSON::Append(std::string&& steal_value) { + append_data_.emplace_back(std::move(steal_value)); + auto& last_data = append_data_.back(); + items_.emplace_back(std::string_view{ last_data.data(),last_data.length() }); +} + +void ColumnJSON::AppendNoManagedLifetime(std::string_view str) { + items_.emplace_back(str); +} + +void ColumnJSON::AppendUnsafe(std::string_view str) { + items_.emplace_back(blocks_.back().AppendUnsafe(str)); +} + +void ColumnJSON::Clear() { + items_.clear(); + blocks_.clear(); + append_data_.clear(); +} + +std::string_view ColumnJSON::At(size_t n) const { + return items_.at(n); +} + +void ColumnJSON::Append(ColumnRef column) { + if (auto col = column->As()) { + const auto total_size = ComputeTotalSize(col->items_); + + // TODO: fill up existing block with some items and then add a new one for the rest of items + if (blocks_.size() == 0 || blocks_.back().GetAvailable() < total_size) + blocks_.emplace_back(std::max(DEFAULT_BLOCK_SIZE, total_size)); + + // Intentionally not doing items_.reserve() since that cripples performance. + for (size_t i = 0; i < column->Size(); ++i) { + this->AppendUnsafe((*col)[i]); + } + } +} + +bool ColumnJSON::LoadBody(InputStream* input, size_t rows) { + if (rows == 0) { + items_.clear(); + blocks_.clear(); + + return true; + } + + decltype(items_) new_items; + decltype(blocks_) new_blocks; + + new_items.reserve(rows); + + // Suboptimzal if the first row string is >DEFAULT_BLOCK_SIZE, but that must be a very rare case. + Block * block = &new_blocks.emplace_back(DEFAULT_BLOCK_SIZE); + + for (size_t i = 0; i < rows; ++i) { + uint64_t len; + if (!WireFormat::ReadUInt64(*input, &len)) + return false; + + if (len > block->GetAvailable()) + block = &new_blocks.emplace_back(std::max(DEFAULT_BLOCK_SIZE, len)); + + if (!WireFormat::ReadBytes(*input, block->GetCurrentWritePos(), len)) + return false; + + new_items.emplace_back(block->ConsumeTailAsJSONViewUnsafe(len)); + } + + items_.swap(new_items); + blocks_.swap(new_blocks); + + return true; +} + +void ColumnJSON::SaveBody(OutputStream* output) { + for (const auto & item : items_) { + WireFormat::WriteString(*output, item); + } +} + +size_t ColumnJSON::Size() const { + return items_.size(); +} + +ColumnRef ColumnJSON::Slice(size_t begin, size_t len) const { + auto result = std::make_shared(); + + if (begin < items_.size()) { + len = std::min(len, items_.size() - begin); + result->items_.reserve(len); + + result->blocks_.emplace_back(ComputeTotalSize(items_, begin, len)); + for (size_t i = begin; i < begin + len; ++i) { + result->Append(items_[i]); + } + } + + return result; +} + +ColumnRef ColumnJSON::CloneEmpty() const { + return std::make_shared(); +} + +void ColumnJSON::Swap(Column& other) { + auto & col = dynamic_cast(other); + items_.swap(col.items_); + blocks_.swap(col.blocks_); + append_data_.swap(col.append_data_); +} + +ItemView ColumnJSON::GetItem(size_t index) const { + return ItemView{Type::JSON, this->At(index)}; +} + } diff --git a/clickhouse/columns/string.h b/clickhouse/columns/string.h index d6006556..689d4ea5 100644 --- a/clickhouse/columns/string.h +++ b/clickhouse/columns/string.h @@ -142,4 +142,76 @@ class ColumnString : public Column { std::deque append_data_; }; +/** + * Represents column of variable-length strings. + */ +class ColumnJSON : public Column { +public: + // Type this column takes as argument of Append and returns with At() and operator[] + using ValueType = std::string_view; + + ColumnJSON(); + ~ColumnJSON(); + + explicit ColumnJSON(size_t element_count); + explicit ColumnJSON(const std::vector & data); + explicit ColumnJSON(std::vector&& data); + ColumnJSON& operator=(const ColumnJSON&) = delete; + ColumnJSON(const ColumnJSON&) = delete; + + /// Increase the capacity of the column for large block insertion. + void Reserve(size_t new_cap) override; + + /// Appends one element to the column. + void Append(std::string_view str); + + /// Appends one element to the column. + void Append(const char* str); + + /// Appends one element to the column. + void Append(std::string&& steal_value); + + /// Appends one element to the column. + /// If str lifetime is managed elsewhere and guaranteed to outlive the Block sent to the server + void AppendNoManagedLifetime(std::string_view str); + + /// Returns element at given row number. + std::string_view At(size_t n) const; + + /// Returns element at given row number. + inline std::string_view operator [] (size_t n) const { return At(n); } + +public: + /// 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; + + /// Saves column data to output stream. + void SaveBody(OutputStream* output) override; + + /// Clear column data . + void Clear() 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) const override; + +private: + void AppendUnsafe(std::string_view); + +private: + struct Block; + + std::vector items_; + std::vector blocks_; + std::deque append_data_; +}; + } diff --git a/clickhouse/types/type_parser.cpp b/clickhouse/types/type_parser.cpp index 52058e80..70c4e963 100644 --- a/clickhouse/types/type_parser.cpp +++ b/clickhouse/types/type_parser.cpp @@ -40,6 +40,7 @@ static const std::unordered_map kTypeCode = { { "Float32", Type::Float32 }, { "Float64", Type::Float64 }, { "String", Type::String }, + { "JSON", Type::JSON }, { "FixedString", Type::FixedString }, { "DateTime", Type::DateTime }, { "DateTime64", Type::DateTime64 }, @@ -68,7 +69,7 @@ static const std::unordered_map kTypeCode = { }; template -inline int CompateStringsCaseInsensitive(const L& left, const R& right) { +inline int CompareStringsCaseInsensitive(const L& left, const R& right) { int64_t size_diff = left.size() - right.size(); if (size_diff != 0) return size_diff > 0 ? 1 : -1; @@ -129,7 +130,7 @@ bool ValidateAST(const TypeAst& ast) { // Void terminal that is not actually "void" produced when unknown type is encountered. if (ast.meta == TypeAst::Terminal && ast.code == Type::Void - && CompateStringsCaseInsensitive(ast.name, std::string_view("void")) != 0) + && CompareStringsCaseInsensitive(ast.name, std::string_view("void")) != 0) //throw UnimplementedError("Unsupported type: " + ast.name); return false; diff --git a/clickhouse/types/types.cpp b/clickhouse/types/types.cpp index c14c86c5..50df5f74 100644 --- a/clickhouse/types/types.cpp +++ b/clickhouse/types/types.cpp @@ -52,6 +52,7 @@ 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::JSON: return "JSON"; } return "Unknown type"; @@ -82,6 +83,7 @@ std::string Type::GetName() const { case Ring: case Polygon: case MultiPolygon: + case JSON: return TypeName(code_); case FixedString: return As()->GetName(); @@ -141,6 +143,7 @@ uint64_t Type::GetTypeUniqueId() const { case Ring: case Polygon: case MultiPolygon: + case JSON: // For simple types, unique ID is the same as Type::Code return code_; @@ -224,6 +227,10 @@ TypeRef Type::CreateString(size_t n) { return TypeRef(new FixedStringType(n)); } +TypeRef Type::CreateJSON() { + return TypeRef(new Type(Type::JSON)); +} + TypeRef Type::CreateTuple(const std::vector& item_types) { return TypeRef(new TupleType(item_types)); } diff --git a/clickhouse/types/types.h b/clickhouse/types/types.h index 71613323..f51f69ef 100644 --- a/clickhouse/types/types.h +++ b/clickhouse/types/types.h @@ -33,6 +33,7 @@ class Type { Float64, String, FixedString, + JSON, DateTime, Date, Array, @@ -56,7 +57,7 @@ class Type { Point, Ring, Polygon, - MultiPolygon + MultiPolygon, }; using EnumItem = std::pair; @@ -122,6 +123,8 @@ class Type { static TypeRef CreateString(size_t n); + static TypeRef CreateJSON(); + static TypeRef CreateTuple(const std::vector& item_types); static TypeRef CreateEnum8(const std::vector& enum_items); diff --git a/ut/Column_ut.cpp b/ut/Column_ut.cpp index 04cb5d78..2064a598 100644 --- a/ut/Column_ut.cpp +++ b/ut/Column_ut.cpp @@ -194,6 +194,7 @@ using TestCases = ::testing::Types< GenericColumnTestCase, std::string, &MakeStrings>, GenericColumnTestCase, std::string, &MakeFixedStrings<12>>, + GenericColumnTestCase, std::string, &MakeJSONStrings>, GenericColumnTestCase, time_t, &MakeDates>, GenericColumnTestCase, time_t, &MakeDates>, diff --git a/ut/CreateColumnByType_ut.cpp b/ut/CreateColumnByType_ut.cpp index 556dfc36..6bda7822 100644 --- a/ut/CreateColumnByType_ut.cpp +++ b/ut/CreateColumnByType_ut.cpp @@ -40,6 +40,9 @@ TEST(CreateColumnByType, LowCardinalityAsWrappedColumn) { ASSERT_EQ(Type::FixedString, CreateColumnByType("LowCardinality(FixedString(10000))", create_column_settings)->GetType().GetCode()); ASSERT_EQ(Type::FixedString, CreateColumnByType("LowCardinality(FixedString(10000))", create_column_settings)->As()->GetType().GetCode()); + + ASSERT_EQ(Type::JSON, CreateColumnByType("LowCardinality(JSON)", create_column_settings)->GetType().GetCode()); + ASSERT_EQ(Type::JSON, CreateColumnByType("LowCardinality(JSON)", create_column_settings)->As()->GetType().GetCode()); } TEST(CreateColumnByType, DateTime) { diff --git a/ut/column_array_ut.cpp b/ut/column_array_ut.cpp index 6fe0bd19..27b39b33 100644 --- a/ut/column_array_ut.cpp +++ b/ut/column_array_ut.cpp @@ -227,6 +227,21 @@ TEST(ColumnArrayT, SimpleFixedString) { EXPECT_EQ("world\0"sv, (*array)[0][1]); } +TEST(ColumnArrayT, JSON) { + using namespace std::literals; + auto array = std::make_shared>(6); + array->Append({"[\"hello\"]", "{\"place\": \"world\"}"}); + + EXPECT_EQ("[\"hello\"]"sv, array->At(0).At(0)); + + auto row = array->At(0); + EXPECT_EQ("[\"hello\"]"sv, row.At(0)); + EXPECT_EQ(9u, row[0].length()); + EXPECT_EQ("[\"hel", row[0].substr(0, 5)); + + EXPECT_EQ("{\"place\": \"world\"}"sv, (*array)[0][1]); +} + TEST(ColumnArrayT, SimpleUInt64_2D) { // Nested 2D-arrays are supported too: auto array = std::make_shared>>(); diff --git a/ut/columns_ut.cpp b/ut/columns_ut.cpp index 314cabdf..5adcd66e 100644 --- a/ut/columns_ut.cpp +++ b/ut/columns_ut.cpp @@ -137,6 +137,29 @@ TEST(ColumnsCase, StringAppend) { ASSERT_EQ(col->At(2), "11"); } +TEST(ColumnsCase, JSONInit) { + auto values = MakeJSONStrings(); + auto col = std::make_shared(values); + + ASSERT_EQ(col->Size(), values.size()); + ASSERT_EQ(col->At(1), "98.6"); + ASSERT_EQ(col->At(3), "false"); +} + +TEST(ColumnsCase, JSONAppend) { + auto col = std::make_shared(); + const char* expected = "\"ufiudhf3493fyiudferyer3yrifhdflkdjfeuroe\""; + std::string data(expected); + col->Append(data); + col->Append(std::move(data)); + col->Append("11"); + + ASSERT_EQ(col->Size(), 3u); + ASSERT_EQ(col->At(0), expected); + ASSERT_EQ(col->At(1), expected); + ASSERT_EQ(col->At(2), "11"); +} + TEST(ColumnsCase, TupleAppend){ auto tuple1 = std::make_shared(std::vector({ std::make_shared(), diff --git a/ut/itemview_ut.cpp b/ut/itemview_ut.cpp index 6413e190..744cf884 100644 --- a/ut/itemview_ut.cpp +++ b/ut/itemview_ut.cpp @@ -68,6 +68,9 @@ TEST(ItemView, StorableTypes) { TEST_ITEMVIEW_TYPE_VALUE(Type::Code::FixedString, std::string_view, ""); TEST_ITEMVIEW_TYPE_VALUE(Type::Code::FixedString, std::string_view, "here is a string"); + + TEST_ITEMVIEW_TYPE_VALUE(Type::Code::JSON, std::string_view, "{}"); + TEST_ITEMVIEW_TYPE_VALUE(Type::Code::JSON, std::string_view, "{\"x\": \"some JSON\"}"); } #define EXPECT_ITEMVIEW_ERROR(TypeCode, NativeType) \ diff --git a/ut/type_parser_ut.cpp b/ut/type_parser_ut.cpp index b0193ded..2ee7eb08 100644 --- a/ut/type_parser_ut.cpp +++ b/ut/type_parser_ut.cpp @@ -24,6 +24,15 @@ TEST(TypeParserCase, ParseFixedString) { ASSERT_EQ(ast.elements.front().value, 24U); } +TEST(TypeParserCase, ParseJSON) { + TypeAst ast; + TypeParser("JSON").Parse(&ast); + + ASSERT_EQ(ast.meta, TypeAst::Terminal); + ASSERT_EQ(ast.name, "JSON"); + ASSERT_EQ(ast.code, Type::JSON); +} + TEST(TypeParserCase, ParseArray) { TypeAst ast; TypeParser("Array(Int32)").Parse(&ast); diff --git a/ut/types_ut.cpp b/ut/types_ut.cpp index 7af343b5..171f9bd6 100644 --- a/ut/types_ut.cpp +++ b/ut/types_ut.cpp @@ -82,6 +82,7 @@ TEST(TypesCase, IsEqual) { "Int8", // "UInt128", "String", + "JSON", "FixedString(0)", "FixedString(10000)", "DateTime('UTC')", diff --git a/ut/utils.cpp b/ut/utils.cpp index e2219dcd..1a4c198e 100644 --- a/ut/utils.cpp +++ b/ut/utils.cpp @@ -60,6 +60,7 @@ bool doPrintValue(const ColumnRef & c, const size_t row, std::ostream & ostr) { if (const auto & casted_c = c->As()) { if constexpr (is_container_v> && !std::is_same_v + && !std::is_same_v && !std::is_same_v) { ostr << PrintContainer{static_cast(casted_c->At(row))}; } else { @@ -166,6 +167,7 @@ std::ostream & printColumnValue(const ColumnRef& c, const size_t row, std::ostre const auto r = false || doPrintValue(c, row, ostr) || doPrintValue(c, row, ostr) + || doPrintValue(c, row, ostr) || doPrintValue(c, row, ostr) || doPrintValue(c, row, ostr) || doPrintValue(c, row, ostr) @@ -377,6 +379,7 @@ std::ostream& operator<<(std::ostream& ostr, const ItemView& item_view) { ostr << static_cast(item_view.get()); break; case Type::String: + case Type::JSON: case Type::FixedString: ostr << "\"" << item_view.data << "\" (" << item_view.data.size() << " bytes)"; break; diff --git a/ut/utils_ut.cpp b/ut/utils_ut.cpp index 31ae566f..7d35f08b 100644 --- a/ut/utils_ut.cpp +++ b/ut/utils_ut.cpp @@ -235,6 +235,7 @@ TEST(ItemView, OutputToOstream_VALID) { // Positive cases: output should be generated EXPECTED_SERIALIZATION("String : \"string\" (6 bytes)", ColumnString(), "string"); EXPECTED_SERIALIZATION("FixedString : \"string\" (6 bytes)", ColumnFixedString(6), "string"); + EXPECTED_SERIALIZATION("JSON : \"98.6\" (4 bytes)", ColumnJSON(), "98.6"); EXPECTED_SERIALIZATION("Int8 : -123", ColumnInt8(), -123); EXPECTED_SERIALIZATION("Int16 : -1234", ColumnInt16(), -1234); diff --git a/ut/value_generators.cpp b/ut/value_generators.cpp index 5504fd1d..1aecdedd 100644 --- a/ut/value_generators.cpp +++ b/ut/value_generators.cpp @@ -51,6 +51,40 @@ std::vector MakeStrings() { }; } +std::vector MakeJSONStrings() { + return { + "1", "98.6", "true", "false", "null", "\"hello\"", + "[1, 98.6, true, false, null, \"hello\"]", + "{\"num\": 1, \"got\": true}", + "{\"widget\": { \ + \"debug\": \"on\", \ + \"window\": { \ + \"title\": \"Sample Konfabulator Widget\", \ + \"name\": \"main_window\", \ + \"width\": 500, \ + \"height\": 500 \ + }, \ + \"image\": { \ + \"src\": \"Images/Sun.png\", \ + \"name\": \"sun1\", \ + \"hOffset\": 250, \ + \"vOffset\": 250, \ + \"alignment\": \"center\" \ + }, \ + \"text\": { \ + \"data\": \"Click Here\", \ + \"size\": 36, \ + \"style\": \"bold\", \ + \"name\": \"text1\", \ + \"hOffset\": 250, \ + \"vOffset\": 100, \ + \"alignment\": \"center\", \ + \"onMouseUp\": \"sun1.opacity = (sun1.opacity / 100) * 90;\" \ + } \ + }}" + }; +} + std::vector MakeUUIDs() { return { UUID(0llu, 0llu), diff --git a/ut/value_generators.h b/ut/value_generators.h index f0bae267..829ca3ec 100644 --- a/ut/value_generators.h +++ b/ut/value_generators.h @@ -32,6 +32,7 @@ std::vector MakeNumbers(); std::vector MakeBools(); std::vector MakeFixedStrings(size_t string_size); std::vector MakeStrings(); +std::vector MakeJSONStrings(); std::vector MakeDateTime64s(size_t scale, size_t values_size = 200); std::vector MakeDates32(); std::vector MakeDateTimes();