aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authordgolear <dgolear@yandex-team.com>2024-03-12 01:00:49 +0300
committerdgolear <dgolear@yandex-team.com>2024-03-12 01:10:51 +0300
commit397a915fb597f46120a368c922143962f305f17a (patch)
tree1d1bbde0f434a77f4a13cccfa6f850f11c5c8d7a
parent767dfbd8f2e59e28ba40727a8c858600ce3d83ea (diff)
downloadydb-397a915fb597f46120a368c922143962f305f17a.tar.gz
YTORM-275: Fix TInstant deserialization from microsecond values
47845f7aede6898d43680bd7c6f1d48c05607e34
-rw-r--r--library/cpp/yt/misc/cast-inl.h14
-rw-r--r--yt/yt/client/table_client/unittests/serialization_ut.cpp22
-rw-r--r--yt/yt/core/ytree/serialize.cpp40
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();