diff options
author | dgolear <dgolear@yandex-team.com> | 2024-03-12 01:00:49 +0300 |
---|---|---|
committer | dgolear <dgolear@yandex-team.com> | 2024-03-12 01:10:51 +0300 |
commit | 397a915fb597f46120a368c922143962f305f17a (patch) | |
tree | 1d1bbde0f434a77f4a13cccfa6f850f11c5c8d7a | |
parent | 767dfbd8f2e59e28ba40727a8c858600ce3d83ea (diff) | |
download | ydb-397a915fb597f46120a368c922143962f305f17a.tar.gz |
YTORM-275: Fix TInstant deserialization from microsecond values
47845f7aede6898d43680bd7c6f1d48c05607e34
-rw-r--r-- | library/cpp/yt/misc/cast-inl.h | 14 | ||||
-rw-r--r-- | yt/yt/client/table_client/unittests/serialization_ut.cpp | 22 | ||||
-rw-r--r-- | yt/yt/core/ytree/serialize.cpp | 40 |
3 files changed, 65 insertions, 11 deletions
diff --git a/library/cpp/yt/misc/cast-inl.h b/library/cpp/yt/misc/cast-inl.h index ceacda91bd..59be41ac1c 100644 --- a/library/cpp/yt/misc/cast-inl.h +++ b/library/cpp/yt/misc/cast-inl.h @@ -9,7 +9,6 @@ #include <util/string/cast.h> #include <util/string/printf.h> -#include <concepts> #include <type_traits> namespace NYT { @@ -78,6 +77,8 @@ inline TString FormatInvalidCastValue(char8_t value) } // namespace NDetail +//////////////////////////////////////////////////////////////////////////////// + template <class T, class S> bool TryIntegralCast(S value, T* result) { @@ -93,8 +94,12 @@ T CheckedIntegralCast(S value) { T result; if (!TryIntegralCast<T>(value, &result)) { - throw TSimpleException(Sprintf("Argument value %s is out of expected range", - NYT::NDetail::FormatInvalidCastValue(value).c_str())); + throw TSimpleException(Sprintf("Error casting %s value \"%s\" to %s: value is out of expected range [%s; %s]", + TypeName<S>().c_str(), + NYT::NDetail::FormatInvalidCastValue(value).c_str(), + TypeName<T>().c_str(), + ::ToString(std::numeric_limits<T>::min()).c_str(), + ::ToString(std::numeric_limits<T>::max()).c_str())); } return result; } @@ -119,7 +124,8 @@ T CheckedEnumCast(S value) { T result; if (!TryEnumCast<T>(value, &result)) { - throw TSimpleException(Sprintf("Invalid value %d of enum type %s", + throw TSimpleException(Sprintf("Error casting %s value \"%d\" to enum %s", + TypeName<S>().c_str(), static_cast<int>(value), TEnumTraits<T>::GetTypeName().data())); } diff --git a/yt/yt/client/table_client/unittests/serialization_ut.cpp b/yt/yt/client/table_client/unittests/serialization_ut.cpp index 9280dd1f76..e48de97ab8 100644 --- a/yt/yt/client/table_client/unittests/serialization_ut.cpp +++ b/yt/yt/client/table_client/unittests/serialization_ut.cpp @@ -1,3 +1,4 @@ +#include <yt/yt/client/table_client/helpers.h> #include <yt/yt/client/table_client/schema.h> #include <yt/yt/library/formats/format.h> @@ -66,6 +67,27 @@ TEST(TSchemaSerialization, Deleted) "Stable name should be set for a deleted column"); } +TEST(TInstantSerialization, YsonCompatibility) +{ + auto convert = [] (auto value) { + TUnversionedValue unversioned; + ToUnversionedValue(&unversioned, value, /*rowBuffer*/ nullptr); + auto node = NYTree::ConvertToNode(unversioned); + return NYTree::ConvertTo<TInstant>(node); + }; + + TInstant now = TInstant::Now(); + TInstant lower = TInstant::TInstant::ParseIso8601("1970-03-01"); + TInstant upper = TInstant::ParseIso8601("2100-01-01"); + + EXPECT_EQ(now, convert(now)); + EXPECT_EQ(TInstant::MilliSeconds(now.MilliSeconds()), convert(now.MilliSeconds())); + EXPECT_EQ(lower, convert(lower)); + EXPECT_EQ(TInstant::MilliSeconds(lower.MilliSeconds()), convert(lower.MilliSeconds())); + EXPECT_EQ(upper, convert(upper)); + EXPECT_EQ(TInstant::MilliSeconds(upper.MilliSeconds()), convert(upper.MilliSeconds())); +} + //////////////////////////////////////////////////////////////////////////////// } // namespace diff --git a/yt/yt/core/ytree/serialize.cpp b/yt/yt/core/ytree/serialize.cpp index 553ab3b03f..897439d31c 100644 --- a/yt/yt/core/ytree/serialize.cpp +++ b/yt/yt/core/ytree/serialize.cpp @@ -16,6 +16,33 @@ using namespace google::protobuf::io; //////////////////////////////////////////////////////////////////////////////// +// Unversioned values use microsecond precision for TInstant values, +// while YSON deserializes values with millisecond precision. +// It can lead to unexpected results when converting TInstant -> TUnversionedValue -> INodePtr -> TInstant. +// These boundaries allow to correctly deserialize microsecond values back to TInstant. +// +// log2(timeEpoch("2100-01-01") * 10**3) < 42. +// log2(timeEpoch("1970-03-01") * 10**6) > 42. +// log2(timeEpoch("2100-01-01") * 10**6) < 52. +static constexpr ui64 MicrosecondLowerWidthBoundary = 42; +static constexpr ui64 MicrosecondUpperWidthBoundary = 52; +static constexpr ui64 UnixTimeMicrosecondLowerBoundary = 1ull << MicrosecondLowerWidthBoundary; +static constexpr ui64 UnixTimeMicrosecondUpperBoundary = 1ull << MicrosecondUpperWidthBoundary; + +TInstant ConvertRawValueToUnixTime(ui64 value) +{ + if (value < UnixTimeMicrosecondLowerBoundary) { + return TInstant::MilliSeconds(value); + } else if (value < UnixTimeMicrosecondUpperBoundary) { + return TInstant::MicroSeconds(value); + } else { + THROW_ERROR_EXCEPTION("Value %Qv does not represent valid UNIX time", + value); + } +} + +//////////////////////////////////////////////////////////////////////////////// + EYsonType GetYsonType(const TYsonString& yson) { return yson.GetType(); @@ -272,17 +299,16 @@ void Deserialize(TInstant& value, INodePtr node) { switch (node->GetType()) { case ENodeType::Int64: { - auto ms = node->AsInt64()->GetValue(); - if (ms < 0) { - THROW_ERROR_EXCEPTION("Instant cannot be negative"); - } - value = TInstant::MilliSeconds(ms); + auto ms = CheckedIntegralCast<ui64>(node->AsInt64()->GetValue()); + value = ConvertRawValueToUnixTime(ms); break; } - case ENodeType::Uint64: - value = TInstant::MilliSeconds(node->AsUint64()->GetValue()); + case ENodeType::Uint64: { + auto ms = node->AsUint64()->GetValue(); + value = ConvertRawValueToUnixTime(ms); break; + } case ENodeType::Double: { auto ms = node->AsDouble()->GetValue(); |