Skip to content

Commit a30bb41

Browse files
committed
feat ydb: add support for Decimal type
1 parent 9a0dc8e commit a30bb41

6 files changed

Lines changed: 701 additions & 1 deletion

File tree

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
#pragma once
2+
3+
/// @file userver/ydb/io/decimal64.hpp
4+
/// @brief YDB serialization support for `userver::decimal64::Decimal`
5+
///
6+
/// `decimal64::Decimal<Prec, RoundPolicy>` is mapped to the YDB type
7+
/// `Decimal(22, Prec)`. The precision is fixed at `22`, which matches the
8+
/// most common "money-like" YDB schema `Decimal(22, 9)` and is sufficient to
9+
/// hold any value representable by `decimal64::Decimal` (its mantissa fits in
10+
/// `int64_t`, i.e. up to 19 significant digits).
11+
///
12+
/// Schemas that use a different precision (e.g. `Decimal(35, 18)`) should use
13+
/// `ydb::Decimal` instead, which carries `precision` and `scale` at runtime.
14+
///
15+
/// On read, `decimal64::Decimal<Prec>::FromStringPermissive` is used, so
16+
/// values stored with a YDB scale larger than `Prec` are rounded according
17+
/// to `RoundPolicy` instead of being rejected.
18+
19+
#include <optional>
20+
21+
#include <ydb-cpp-sdk/client/params/params.h>
22+
#include <ydb-cpp-sdk/client/value/value.h>
23+
24+
#include <userver/compiler/demangle.hpp>
25+
#include <userver/decimal64/decimal64.hpp>
26+
#include <userver/logging/log.hpp>
27+
28+
#include <userver/ydb/impl/cast.hpp>
29+
#include <userver/ydb/io/traits.hpp>
30+
#include <userver/ydb/types.hpp>
31+
32+
USERVER_NAMESPACE_BEGIN
33+
34+
namespace ydb {
35+
36+
namespace impl {
37+
38+
inline bool IsOptionalDecimal64(const NYdb::TValueParser& parser) {
39+
return parser.GetKind() == NYdb::TTypeParser::ETypeKind::Optional;
40+
}
41+
42+
template <int Prec, typename RoundPolicy>
43+
decimal64::Decimal<Prec, RoundPolicy> ParseDecimal64Value(const NYdb::TValueParser& parser) {
44+
// YDB stores decimals with a fixed scale, which may differ from `Prec`.
45+
// `FromStringPermissive` tolerates trailing zeros and rounds extra
46+
// fractional digits according to `RoundPolicy`, which is what we want.
47+
return decimal64::Decimal<Prec, RoundPolicy>::FromStringPermissive(parser.GetDecimal().ToString());
48+
}
49+
50+
template <int Prec, typename RoundPolicy, typename Builder>
51+
void WriteDecimal64Value(
52+
NYdb::TValueBuilderBase<Builder>& builder,
53+
const decimal64::Decimal<Prec, RoundPolicy>& value
54+
) {
55+
builder.Decimal(NYdb::TDecimalValue(
56+
impl::ToString(decimal64::ToString(value)),
57+
Decimal::kDefaultPrecision,
58+
static_cast<std::uint8_t>(Prec)
59+
));
60+
}
61+
62+
inline NYdb::TType MakeDecimal64Type(std::uint8_t scale) {
63+
NYdb::TTypeBuilder builder;
64+
builder.Decimal(NYdb::TDecimalType{Decimal::kDefaultPrecision, scale});
65+
return builder.Build();
66+
}
67+
68+
inline NYdb::TType MakeOptionalDecimal64Type(std::uint8_t scale) {
69+
NYdb::TTypeBuilder builder;
70+
builder.BeginOptional();
71+
builder.Decimal(NYdb::TDecimalType{Decimal::kDefaultPrecision, scale});
72+
builder.EndOptional();
73+
return builder.Build();
74+
}
75+
76+
} // namespace impl
77+
78+
template <int Prec, typename RoundPolicy>
79+
struct ValueTraits<decimal64::Decimal<Prec, RoundPolicy>> {
80+
static_assert(
81+
Prec >= 0 && Prec <= Decimal::kDefaultPrecision,
82+
"decimal64::Decimal<Prec> must have 0 <= Prec <= ydb::Decimal::kDefaultPrecision (22) "
83+
"to map to YDB Decimal(22, Prec); use ydb::Decimal for non-money-like schemas"
84+
);
85+
86+
using Type = decimal64::Decimal<Prec, RoundPolicy>;
87+
88+
static Type Parse(NYdb::TValueParser& parser, const ParseContext& /*context*/) {
89+
const bool is_optional = impl::IsOptionalDecimal64(parser);
90+
91+
if (is_optional) {
92+
parser.OpenOptional();
93+
}
94+
95+
// Will throw exception if value is null.
96+
auto value = impl::ParseDecimal64Value<Prec, RoundPolicy>(parser);
97+
98+
if (is_optional) {
99+
parser.CloseOptional();
100+
}
101+
102+
return value;
103+
}
104+
105+
template <typename Builder>
106+
static void Write(NYdb::TValueBuilderBase<Builder>& builder, const Type& value) {
107+
impl::WriteDecimal64Value(builder, value);
108+
}
109+
110+
static NYdb::TType MakeType() { return impl::MakeDecimal64Type(static_cast<std::uint8_t>(Prec)); }
111+
};
112+
113+
template <int Prec, typename RoundPolicy>
114+
struct ValueTraits<std::optional<decimal64::Decimal<Prec, RoundPolicy>>> {
115+
static_assert(
116+
Prec >= 0 && Prec <= Decimal::kDefaultPrecision,
117+
"decimal64::Decimal<Prec> must have 0 <= Prec <= ydb::Decimal::kDefaultPrecision (22) "
118+
"to map to YDB Decimal(22, Prec); use ydb::Decimal for non-money-like schemas"
119+
);
120+
121+
using Type = decimal64::Decimal<Prec, RoundPolicy>;
122+
123+
static std::optional<Type> Parse(NYdb::TValueParser& parser, const ParseContext& context) {
124+
const bool is_optional = impl::IsOptionalDecimal64(parser);
125+
if (is_optional) {
126+
parser.OpenOptional();
127+
128+
if (parser.IsNull()) {
129+
parser.CloseOptional();
130+
return {};
131+
}
132+
} else {
133+
LOG_WARNING()
134+
<< "Trying to parse " << context.column_name << " as "
135+
<< compiler::GetTypeName<std::optional<Type>>() << " while actual type is not Optional";
136+
}
137+
138+
auto value = impl::ParseDecimal64Value<Prec, RoundPolicy>(parser);
139+
if (is_optional) {
140+
parser.CloseOptional();
141+
}
142+
143+
return value;
144+
}
145+
146+
template <typename Builder>
147+
static void Write(NYdb::TValueBuilderBase<Builder>& builder, const std::optional<Type>& value) {
148+
if (value) {
149+
builder.BeginOptional();
150+
impl::WriteDecimal64Value(builder, *value);
151+
builder.EndOptional();
152+
} else {
153+
builder.EmptyOptional(impl::MakeDecimal64Type(static_cast<std::uint8_t>(Prec)));
154+
}
155+
}
156+
157+
static NYdb::TType MakeType() { return impl::MakeOptionalDecimal64Type(static_cast<std::uint8_t>(Prec)); }
158+
};
159+
160+
} // namespace ydb
161+
162+
USERVER_NAMESPACE_END

ydb/include/userver/ydb/io/primitives.hpp

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,23 @@ struct PrimitiveTraits {
5050
static NYdb::TType MakeType();
5151
};
5252

53+
template <typename DecimalLikeTrait>
54+
struct DecimalTraits {
55+
static typename DecimalLikeTrait::Type Parse(NYdb::TValueParser& parser, const ParseContext& context);
56+
57+
static void Write(
58+
NYdb::TValueBuilderBase<NYdb::TValueBuilder>& builder,
59+
const typename DecimalLikeTrait::Type& value
60+
);
61+
62+
static void Write(
63+
NYdb::TValueBuilderBase<NYdb::TParamValueBuilder>& builder,
64+
const typename DecimalLikeTrait::Type& value
65+
);
66+
67+
static NYdb::TType MakeType();
68+
};
69+
5370
struct BoolTrait {
5471
using Type = bool;
5572
static Type Parse(const NYdb::TValueParser& value_parser);
@@ -162,12 +179,44 @@ struct JsonDocumentTrait {
162179
static void Write(NYdb::TValueBuilderBase<Builder>& builder, const Type& value);
163180
};
164181

182+
struct DecimalTrait {
183+
using Type = Decimal;
184+
static Type Parse(const NYdb::TValueParser& value_parser);
185+
template <typename Builder>
186+
static void Write(NYdb::TValueBuilderBase<Builder>& builder, const Type& value);
187+
};
188+
165189
template <>
166190
struct ValueTraits<std::optional<JsonDocumentTrait::Type>> : OptionalPrimitiveTraits<JsonDocumentTrait> {};
167191

168192
template <>
169193
struct ValueTraits<JsonDocument> : PrimitiveTraits<JsonDocumentTrait> {};
170194

195+
template <>
196+
struct ValueTraits<DecimalTrait::Type> : DecimalTraits<DecimalTrait> {};
197+
198+
// `std::optional<ydb::Decimal>` is supported on read only. Writing a NULL
199+
// `ydb::Decimal` would require knowing the YDB column's precision and scale,
200+
// which an empty optional does not carry. For nullable Decimal columns whose
201+
// schema is `Decimal(22, Prec)`, use `std::optional<decimal64::Decimal<Prec>>`
202+
// instead -- its precision/scale are encoded in the C++ type.
203+
template <>
204+
struct ValueTraits<std::optional<DecimalTrait::Type>> {
205+
static std::optional<DecimalTrait::Type> Parse(NYdb::TValueParser& parser, const ParseContext& context);
206+
207+
template <typename Builder>
208+
static void Write(NYdb::TValueBuilderBase<Builder>& /*builder*/, const std::optional<DecimalTrait::Type>& /*value*/) {
209+
static_assert(
210+
sizeof(Builder) == 0,
211+
"Writing std::optional<ydb::Decimal> is not supported: an empty optional carries no "
212+
"precision/scale, and YDB Decimal type requires them. Use std::optional<decimal64::Decimal<Prec>> "
213+
"or write a non-optional ydb::Decimal{value, precision, scale}."
214+
);
215+
}
216+
217+
static NYdb::TType MakeType() = delete;
218+
};
219+
171220
template <>
172221
struct ValueTraits<std::optional<JsonTrait::Type>> : OptionalPrimitiveTraits<JsonTrait> {};
173222

ydb/include/userver/ydb/io/supported_types.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@
1717
/// * ValueType::Utf8, ydb::Utf8
1818
/// * ValueType::Timestamp, std::chrono::system_clock::time_point
1919
/// * ValueType::Uuid, boost::uuids::uuid
20+
/// * ValueType::Decimal, ydb::Decimal, decimal64::Decimal<Prec, RoundPolicy>
2021
///
2122
/// Available composite types:
2223
/// * Optional, std::optional for primitive types, List and Struct
2324
/// * List, std::vector and non-map containers
2425
/// * Struct, @ref ydb::kStructMemberNames "C++ structs"
2526

27+
#include <userver/ydb/io/decimal64.hpp>
2628
#include <userver/ydb/io/insert_row.hpp>
2729
#include <userver/ydb/io/list.hpp>
2830
#include <userver/ydb/io/primitives.hpp>

ydb/include/userver/ydb/types.hpp

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ namespace ydb {
2727
* Uint64 | std::uint64_t
2828
* Float | N/A
2929
* Double | double
30+
* Decimal | ydb::Decimal
3031
* Date | N/A
3132
* Datetime | N/A
3233
* Timestamp | std::chrono::system_clock::time_point
@@ -52,6 +53,47 @@ using Utf8 = utils::StrongTypedef<Utf8Tag, std::string>;
5253
class JsonDocumentTag {};
5354
using JsonDocument = utils::StrongTypedef<JsonDocumentTag, formats::json::Value>;
5455

56+
// A YDB Decimal value as a (value, precision, scale) triple.
57+
//
58+
// In YDB, `precision` and `scale` are part of the column type, not of the
59+
// value, and must be provided when writing. `ydb::Decimal` is a thin transport
60+
// wrapper around `NYdb::TDecimalValue` that preserves them as-is; it is NOT
61+
// an arithmetic type. For numeric operations, parsing from numbers, rounding
62+
// etc. use `decimal64::Decimal<Prec, RoundPolicy>` (see
63+
// `userver/ydb/io/decimal64.hpp`), whose `ValueTraits` are also provided by
64+
// this library.
65+
//
66+
// `value` stores the decimal in the same string form that the YDB SDK
67+
// produces and accepts (e.g. `"123.456789"`). YDB returns decimals in a
68+
// canonical form with trailing zeros stripped, so a value written as
69+
// `"7.500000000"` will be read back as `"7.5"`. Consequently `operator==`
70+
// performs a strict string comparison and is NOT a numeric equality check.
71+
//
72+
// The default `precision = 22`, `scale = 9` matches the most common
73+
// "money-like" YDB schema `Decimal(22, 9)`. They are exposed as
74+
// `kDefaultPrecision` / `kDefaultScale` for convenience but should be
75+
// overridden whenever the column uses a different type, e.g.
76+
// `ydb::Decimal{"123.456789012345678", 35, 18}`.
77+
struct Decimal {
78+
static constexpr std::uint8_t kDefaultPrecision = 22;
79+
static constexpr std::uint8_t kDefaultScale = 9;
80+
81+
std::string value;
82+
std::uint8_t precision{kDefaultPrecision};
83+
std::uint8_t scale{kDefaultScale};
84+
85+
Decimal() = default;
86+
87+
Decimal(std::string value, std::uint8_t precision, std::uint8_t scale)
88+
: value(std::move(value)), precision(precision), scale(scale) {}
89+
90+
friend bool operator==(const Decimal& lhs, const Decimal& rhs) {
91+
return lhs.precision == rhs.precision && lhs.scale == rhs.scale && lhs.value == rhs.value;
92+
}
93+
94+
friend bool operator!=(const Decimal& lhs, const Decimal& rhs) { return !(lhs == rhs); }
95+
};
96+
5597
using InsertColumnValue = std::variant<
5698
std::string,
5799
bool,
@@ -78,7 +120,8 @@ using InsertColumnValue = std::variant<
78120
std::optional<std::uint64_t>,
79121
std::optional<double>,
80122
std::optional<Utf8>,
81-
std::optional<Timestamp>>;
123+
std::optional<Timestamp>,
124+
Decimal>;
82125

83126
struct InsertColumn {
84127
std::string name;

0 commit comments

Comments
 (0)