Skip to content
Closed
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
162 changes: 162 additions & 0 deletions ydb/include/userver/ydb/io/decimal64.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
#pragma once

/// @file userver/ydb/io/decimal64.hpp
/// @brief YDB serialization support for `userver::decimal64::Decimal`
///
/// `decimal64::Decimal<Prec, RoundPolicy>` is mapped to the YDB type
/// `Decimal(22, Prec)`. The precision is fixed at `22`, which matches the
/// most common "money-like" YDB schema `Decimal(22, 9)` and is sufficient to
/// hold any value representable by `decimal64::Decimal` (its mantissa fits in
/// `int64_t`, i.e. up to 19 significant digits).
///
/// Schemas that use a different precision (e.g. `Decimal(35, 18)`) should use
/// `ydb::Decimal` instead, which carries `precision` and `scale` at runtime.
///
/// On read, `decimal64::Decimal<Prec>::FromStringPermissive` is used, so
/// values stored with a YDB scale larger than `Prec` are rounded according
/// to `RoundPolicy` instead of being rejected.

#include <optional>

#include <ydb-cpp-sdk/client/params/params.h>
#include <ydb-cpp-sdk/client/value/value.h>

#include <userver/compiler/demangle.hpp>
#include <userver/decimal64/decimal64.hpp>
#include <userver/logging/log.hpp>

#include <userver/ydb/impl/cast.hpp>
#include <userver/ydb/io/traits.hpp>
#include <userver/ydb/types.hpp>

USERVER_NAMESPACE_BEGIN

namespace ydb {

namespace impl {

inline bool IsOptionalDecimal64(const NYdb::TValueParser& parser) {
return parser.GetKind() == NYdb::TTypeParser::ETypeKind::Optional;
}

template <int Prec, typename RoundPolicy>
decimal64::Decimal<Prec, RoundPolicy> ParseDecimal64Value(const NYdb::TValueParser& parser) {
// YDB stores decimals with a fixed scale, which may differ from `Prec`.
// `FromStringPermissive` tolerates trailing zeros and rounds extra
// fractional digits according to `RoundPolicy`, which is what we want.
return decimal64::Decimal<Prec, RoundPolicy>::FromStringPermissive(parser.GetDecimal().ToString());
}

template <int Prec, typename RoundPolicy, typename Builder>
void WriteDecimal64Value(
NYdb::TValueBuilderBase<Builder>& builder,
const decimal64::Decimal<Prec, RoundPolicy>& value
) {
builder.Decimal(NYdb::TDecimalValue(
impl::ToString(decimal64::ToString(value)),
Decimal::kDefaultPrecision,
static_cast<std::uint8_t>(Prec)
));
}

inline NYdb::TType MakeDecimal64Type(std::uint8_t scale) {
NYdb::TTypeBuilder builder;
builder.Decimal(NYdb::TDecimalType{Decimal::kDefaultPrecision, scale});
return builder.Build();
}

inline NYdb::TType MakeOptionalDecimal64Type(std::uint8_t scale) {
NYdb::TTypeBuilder builder;
builder.BeginOptional();
builder.Decimal(NYdb::TDecimalType{Decimal::kDefaultPrecision, scale});
builder.EndOptional();
return builder.Build();
}

} // namespace impl

template <int Prec, typename RoundPolicy>
struct ValueTraits<decimal64::Decimal<Prec, RoundPolicy>> {
static_assert(
Prec >= 0 && Prec <= Decimal::kDefaultPrecision,
"decimal64::Decimal<Prec> must have 0 <= Prec <= ydb::Decimal::kDefaultPrecision (22) "
"to map to YDB Decimal(22, Prec); use ydb::Decimal for non-money-like schemas"
);

using Type = decimal64::Decimal<Prec, RoundPolicy>;

static Type Parse(NYdb::TValueParser& parser, const ParseContext& /*context*/) {
const bool is_optional = impl::IsOptionalDecimal64(parser);

if (is_optional) {
parser.OpenOptional();
}

// Will throw exception if value is null.
auto value = impl::ParseDecimal64Value<Prec, RoundPolicy>(parser);

if (is_optional) {
parser.CloseOptional();
}

return value;
}

template <typename Builder>
static void Write(NYdb::TValueBuilderBase<Builder>& builder, const Type& value) {
impl::WriteDecimal64Value(builder, value);
}

static NYdb::TType MakeType() { return impl::MakeDecimal64Type(static_cast<std::uint8_t>(Prec)); }
};

template <int Prec, typename RoundPolicy>
struct ValueTraits<std::optional<decimal64::Decimal<Prec, RoundPolicy>>> {
static_assert(
Prec >= 0 && Prec <= Decimal::kDefaultPrecision,
"decimal64::Decimal<Prec> must have 0 <= Prec <= ydb::Decimal::kDefaultPrecision (22) "
"to map to YDB Decimal(22, Prec); use ydb::Decimal for non-money-like schemas"
);

using Type = decimal64::Decimal<Prec, RoundPolicy>;

static std::optional<Type> Parse(NYdb::TValueParser& parser, const ParseContext& context) {
const bool is_optional = impl::IsOptionalDecimal64(parser);
if (is_optional) {
parser.OpenOptional();

if (parser.IsNull()) {
parser.CloseOptional();
return {};
}
} else {
LOG_WARNING()
<< "Trying to parse " << context.column_name << " as "
<< compiler::GetTypeName<std::optional<Type>>() << " while actual type is not Optional";
}

auto value = impl::ParseDecimal64Value<Prec, RoundPolicy>(parser);
if (is_optional) {
parser.CloseOptional();
}

return value;
}

template <typename Builder>
static void Write(NYdb::TValueBuilderBase<Builder>& builder, const std::optional<Type>& value) {
if (value) {
builder.BeginOptional();
impl::WriteDecimal64Value(builder, *value);
builder.EndOptional();
} else {
builder.EmptyOptional(impl::MakeDecimal64Type(static_cast<std::uint8_t>(Prec)));
}
}

static NYdb::TType MakeType() { return impl::MakeOptionalDecimal64Type(static_cast<std::uint8_t>(Prec)); }
};

} // namespace ydb

USERVER_NAMESPACE_END
49 changes: 49 additions & 0 deletions ydb/include/userver/ydb/io/primitives.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,23 @@ struct PrimitiveTraits {
static NYdb::TType MakeType();
};

template <typename DecimalLikeTrait>
struct DecimalTraits {
static typename DecimalLikeTrait::Type Parse(NYdb::TValueParser& parser, const ParseContext& context);

static void Write(
NYdb::TValueBuilderBase<NYdb::TValueBuilder>& builder,
const typename DecimalLikeTrait::Type& value
);

static void Write(
NYdb::TValueBuilderBase<NYdb::TParamValueBuilder>& builder,
const typename DecimalLikeTrait::Type& value
);

static NYdb::TType MakeType();
};

struct BoolTrait {
using Type = bool;
static Type Parse(const NYdb::TValueParser& value_parser);
Expand Down Expand Up @@ -162,12 +179,44 @@ struct JsonDocumentTrait {
static void Write(NYdb::TValueBuilderBase<Builder>& builder, const Type& value);
};

struct DecimalTrait {
using Type = Decimal;
static Type Parse(const NYdb::TValueParser& value_parser);
template <typename Builder>
static void Write(NYdb::TValueBuilderBase<Builder>& builder, const Type& value);
};

template <>
struct ValueTraits<std::optional<JsonDocumentTrait::Type>> : OptionalPrimitiveTraits<JsonDocumentTrait> {};

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

template <>
struct ValueTraits<DecimalTrait::Type> : DecimalTraits<DecimalTrait> {};

// `std::optional<ydb::Decimal>` is supported on read only. Writing a NULL
// `ydb::Decimal` would require knowing the YDB column's precision and scale,
// which an empty optional does not carry. For nullable Decimal columns whose
// schema is `Decimal(22, Prec)`, use `std::optional<decimal64::Decimal<Prec>>`
// instead -- its precision/scale are encoded in the C++ type.
template <>
struct ValueTraits<std::optional<DecimalTrait::Type>> {
static std::optional<DecimalTrait::Type> Parse(NYdb::TValueParser& parser, const ParseContext& context);

template <typename Builder>
static void Write(NYdb::TValueBuilderBase<Builder>& /*builder*/, const std::optional<DecimalTrait::Type>& /*value*/) {
static_assert(
sizeof(Builder) == 0,
"Writing std::optional<ydb::Decimal> is not supported: an empty optional carries no "
"precision/scale, and YDB Decimal type requires them. Use std::optional<decimal64::Decimal<Prec>> "
"or write a non-optional ydb::Decimal{value, precision, scale}."
);
}

static NYdb::TType MakeType() = delete;
};

template <>
struct ValueTraits<std::optional<JsonTrait::Type>> : OptionalPrimitiveTraits<JsonTrait> {};

Expand Down
2 changes: 2 additions & 0 deletions ydb/include/userver/ydb/io/supported_types.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@
/// * ValueType::Utf8, ydb::Utf8
/// * ValueType::Timestamp, std::chrono::system_clock::time_point
/// * ValueType::Uuid, boost::uuids::uuid
/// * ValueType::Decimal, ydb::Decimal, decimal64::Decimal<Prec, RoundPolicy>
///
/// Available composite types:
/// * Optional, std::optional for primitive types, List and Struct
/// * List, std::vector and non-map containers
/// * Struct, @ref ydb::kStructMemberNames "C++ structs"

#include <userver/ydb/io/decimal64.hpp>
#include <userver/ydb/io/insert_row.hpp>
#include <userver/ydb/io/list.hpp>
#include <userver/ydb/io/primitives.hpp>
Expand Down
45 changes: 44 additions & 1 deletion ydb/include/userver/ydb/types.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ namespace ydb {
* Uint64 | std::uint64_t
* Float | N/A
* Double | double
* Decimal | ydb::Decimal
* Date | N/A
* Datetime | N/A
* Timestamp | std::chrono::system_clock::time_point
Expand All @@ -52,6 +53,47 @@ using Utf8 = utils::StrongTypedef<Utf8Tag, std::string>;
class JsonDocumentTag {};
using JsonDocument = utils::StrongTypedef<JsonDocumentTag, formats::json::Value>;

// A YDB Decimal value as a (value, precision, scale) triple.
//
// In YDB, `precision` and `scale` are part of the column type, not of the
// value, and must be provided when writing. `ydb::Decimal` is a thin transport
// wrapper around `NYdb::TDecimalValue` that preserves them as-is; it is NOT
// an arithmetic type. For numeric operations, parsing from numbers, rounding
// etc. use `decimal64::Decimal<Prec, RoundPolicy>` (see
// `userver/ydb/io/decimal64.hpp`), whose `ValueTraits` are also provided by
// this library.
//
// `value` stores the decimal in the same string form that the YDB SDK
// produces and accepts (e.g. `"123.456789"`). YDB returns decimals in a
// canonical form with trailing zeros stripped, so a value written as
// `"7.500000000"` will be read back as `"7.5"`. Consequently `operator==`
// performs a strict string comparison and is NOT a numeric equality check.
//
// The default `precision = 22`, `scale = 9` matches the most common
// "money-like" YDB schema `Decimal(22, 9)`. They are exposed as
// `kDefaultPrecision` / `kDefaultScale` for convenience but should be
// overridden whenever the column uses a different type, e.g.
// `ydb::Decimal{"123.456789012345678", 35, 18}`.
struct Decimal {
static constexpr std::uint8_t kDefaultPrecision = 22;
static constexpr std::uint8_t kDefaultScale = 9;

std::string value;
std::uint8_t precision{kDefaultPrecision};
std::uint8_t scale{kDefaultScale};

Decimal() = default;

Decimal(std::string value, std::uint8_t precision, std::uint8_t scale)
: value(std::move(value)), precision(precision), scale(scale) {}

friend bool operator==(const Decimal& lhs, const Decimal& rhs) {
return lhs.precision == rhs.precision && lhs.scale == rhs.scale && lhs.value == rhs.value;
}

friend bool operator!=(const Decimal& lhs, const Decimal& rhs) { return !(lhs == rhs); }
};

using InsertColumnValue = std::variant<
std::string,
bool,
Expand All @@ -78,7 +120,8 @@ using InsertColumnValue = std::variant<
std::optional<std::uint64_t>,
std::optional<double>,
std::optional<Utf8>,
std::optional<Timestamp>>;
std::optional<Timestamp>,
Decimal>;

struct InsertColumn {
std::string name;
Expand Down
Loading
Loading