From 91d7f360082e2ae4a3945d7cf35b9d6e49af407c Mon Sep 17 00:00:00 2001 From: Vitalii Gridnev Date: Wed, 8 Oct 2025 18:57:20 +0300 Subject: [PATCH] add validation for different precisions --- ydb/core/kqp/ut/query/kqp_query_ut.cpp | 78 ++++++++++++++++++++++++++ ydb/library/mkql_proto/mkql_proto.cpp | 59 ++++++++++++++++++- 2 files changed, 136 insertions(+), 1 deletion(-) diff --git a/ydb/core/kqp/ut/query/kqp_query_ut.cpp b/ydb/core/kqp/ut/query/kqp_query_ut.cpp index d3c659f0a032..2de77e1b2faf 100644 --- a/ydb/core/kqp/ut/query/kqp_query_ut.cpp +++ b/ydb/core/kqp/ut/query/kqp_query_ut.cpp @@ -74,6 +74,84 @@ Y_UNIT_TEST_SUITE(KqpQuery) { UNIT_ASSERT_VALUES_EQUAL(counters.RecompileRequestGet()->Val(), 1); } + Y_UNIT_TEST_QUAD(DecimalOutOfPrecision, UseOltpSink, EnableParameterizedDecimal) { + TKikimrSettings serverSettings; + serverSettings.AppConfig.MutableTableServiceConfig()->SetEnableOltpSink(UseOltpSink); + serverSettings.FeatureFlags.SetEnableParameterizedDecimal(EnableParameterizedDecimal); + serverSettings.WithSampleTables = false; + + TKikimrRunner kikimr(serverSettings); + auto client = kikimr.GetQueryClient(); + + { + auto ddlResult = client.ExecuteQuery(R"( + CREATE TABLE DecTest ( + Key Int32 NOT NULL, + Value Decimal(22, 9), + ValueLarge Decimal(35, 9), + PRIMARY KEY (Key) + ); + )", NYdb::NQuery::TTxControl::NoTx()).ExtractValueSync(); + UNIT_ASSERT_C(ddlResult.IsSuccess(), ddlResult.GetIssues().ToString()); + } + + // 10000000000000 in Decimal(35, 9), invalid for Decimal(22, 9) + Ydb::Value value; + value.set_low_128(1864712049423024128); + value.set_high_128(542); + auto invalidValue = TDecimalValue(value, NYdb::TDecimalType(22, 9)); + + auto validValue = TDecimalValue(value, NYdb::TDecimalType(35, 9)); + + { + auto params = TParamsBuilder() + .AddParam("$value").Decimal(invalidValue).Build() + .Build(); + + auto writeResult = client.ExecuteQuery(R"( + UPSERT INTO DecTest (Key, Value) VALUES + (1, CAST(10 AS Decimal(22,9))), + (2, $value), + (3, $value - CAST(1 AS Decimal(22,9))); + )", NYdb::NQuery::TTxControl::BeginTx().CommitTx(), params).ExtractValueSync(); + + // TODO: Plan A, query should fail as provided value is invalid for given type. + UNIT_ASSERT_C(!writeResult.IsSuccess(), writeResult.GetIssues().ToString()); + UNIT_ASSERT_EQUAL_C(writeResult.GetStatus(), EStatus::BAD_REQUEST, writeResult.GetIssues().ToString()); + + // TODO: Plan B, value for key 2 should be inf, as provided value is out of range + // for given type. + auto session = kikimr.GetTableClient().CreateSession().GetValueSync().GetSession(); + auto tableYson = ReadTableToYson(session, "/Root/DecTest"); + CompareYson(R"([])", tableYson); + { + auto paramsValid = TParamsBuilder() + .AddParam("$value").Decimal(validValue).Build() + .Build(); + auto writeResult = client.ExecuteQuery(R"( + UPSERT INTO DecTest (Key, ValueLarge) VALUES + (2, $value); + )", NYdb::NQuery::TTxControl::BeginTx().CommitTx(), paramsValid).ExtractValueSync(); + + // TODO: Plan A, query should fail as provided value is invalid for given type. + UNIT_ASSERT_C(writeResult.IsSuccess(), writeResult.GetIssues().ToString()); + UNIT_ASSERT_EQUAL_C(writeResult.GetStatus(), EStatus::SUCCESS, writeResult.GetIssues().ToString()); + + auto tableYson = ReadTableToYson(session, "/Root/DecTest"); + CompareYson(R"([[[2];#;["10000000000000"]]])", tableYson); + } + + { + auto writeResult = client.ExecuteQuery(R"( + UPSERT INTO DecTest (Key, Value) SELECT Key, ValueLarge as Value FROM DecTest; + )", NYdb::NQuery::TTxControl::BeginTx().CommitTx()).ExtractValueSync(); + // TODO: Plan A, query should fail as provided value is invalid for given type. + UNIT_ASSERT_C(!writeResult.IsSuccess(), writeResult.GetIssues().ToString()); + UNIT_ASSERT_EQUAL_C(writeResult.GetStatus(), EStatus::GENERIC_ERROR, writeResult.GetIssues().ToString()); + } + } + } + Y_UNIT_TEST(QueryCache) { TKikimrRunner kikimr; auto db = kikimr.GetTableClient(); diff --git a/ydb/library/mkql_proto/mkql_proto.cpp b/ydb/library/mkql_proto/mkql_proto.cpp index 97b729299b0e..efc86b5b0ac3 100644 --- a/ydb/library/mkql_proto/mkql_proto.cpp +++ b/ydb/library/mkql_proto/mkql_proto.cpp @@ -20,6 +20,52 @@ namespace NKikimr::NMiniKQL { namespace { +static constexpr std::array, NYql::NDecimal::MaxPrecision + 1> DecimalBounds = { + NYql::NDecimal::GetBounds<0>(), + NYql::NDecimal::GetBounds<1>(), + NYql::NDecimal::GetBounds<2>(), + NYql::NDecimal::GetBounds<3>(), + NYql::NDecimal::GetBounds<4>(), + NYql::NDecimal::GetBounds<5>(), + NYql::NDecimal::GetBounds<6>(), + NYql::NDecimal::GetBounds<7>(), + NYql::NDecimal::GetBounds<8>(), + NYql::NDecimal::GetBounds<9>(), + NYql::NDecimal::GetBounds<10>(), + NYql::NDecimal::GetBounds<11>(), + NYql::NDecimal::GetBounds<12>(), + NYql::NDecimal::GetBounds<13>(), + NYql::NDecimal::GetBounds<14>(), + NYql::NDecimal::GetBounds<15>(), + NYql::NDecimal::GetBounds<16>(), + NYql::NDecimal::GetBounds<17>(), + NYql::NDecimal::GetBounds<18>(), + NYql::NDecimal::GetBounds<19>(), + NYql::NDecimal::GetBounds<20>(), + NYql::NDecimal::GetBounds<21>(), + NYql::NDecimal::GetBounds<22>(), + NYql::NDecimal::GetBounds<23>(), + NYql::NDecimal::GetBounds<24>(), + NYql::NDecimal::GetBounds<25>(), + NYql::NDecimal::GetBounds<26>(), + NYql::NDecimal::GetBounds<27>(), + NYql::NDecimal::GetBounds<28>(), + NYql::NDecimal::GetBounds<29>(), + NYql::NDecimal::GetBounds<30>(), + NYql::NDecimal::GetBounds<31>(), + NYql::NDecimal::GetBounds<32>(), + NYql::NDecimal::GetBounds<33>(), + NYql::NDecimal::GetBounds<34>(), + NYql::NDecimal::GetBounds<35>(), +}; + +bool IsValidDecimal(ui8 precision, NYql::NDecimal::TInt128 v) { + if (precision >= DecimalBounds.size()) + return false; + const auto& db = DecimalBounds[precision]; + return v > db.first && v < db.second; +} + void ExportTypeToProtoImpl(TType* type, NKikimrMiniKQL::TType& res, const TVector* columnOrder = nullptr); Y_FORCE_INLINE void HandleKindDataExport(const TType* type, const NUdf::TUnboxedValuePod& value, Ydb::Value& res) { @@ -1628,7 +1674,18 @@ Y_FORCE_INLINE NUdf::TUnboxedValue KindDataImport(const TType* type, const Ydb:: return MakeString(value.bytes_value()); } case NUdf::TDataType::Id: { - return NUdf::TUnboxedValuePod(NYql::NDecimal::FromHalfs(value.low_128(), value.high_128())); + auto data = NYql::NDecimal::FromHalfs(value.low_128(), value.high_128()); + auto dataType = static_cast(type); + auto schemeType = dataType->GetSchemeType(); + Y_ENSURE(schemeType == NYql::NProto::TypeIds::Decimal, "Expected decimal type, but found " << schemeType); + auto decimalType = static_cast(dataType); + auto params = decimalType->GetParams(); + ui8 precision = params.first; + Y_ENSURE(precision <= NYql::NDecimal::MaxPrecision, "Unsupported decimal precision: " << precision); + Y_ENSURE(!NYql::NDecimal::IsError(data), "Invalid Decimal value"); + Y_ENSURE(IsValidDecimal(precision, data) || NYql::NDecimal::IsNan(data) || NYql::NDecimal::IsInf(data), + "Invalid Decimal value out of the range for specified precision: " << precision); + return NUdf::TUnboxedValuePod(data); } default: { throw yexception() << "Unsupported data type: " << dataType->GetSchemeType();