Skip to content

Commit c7df816

Browse files
committed
ADO: correct DECIMAL serialization (128-bit fixed-scale) and enforce precision/scale overflow checks for parameters
1 parent e9e0b50 commit c7df816

File tree

3 files changed

+36
-54
lines changed

3 files changed

+36
-54
lines changed

src/Ydb.Sdk/src/Ado/YdbParameter.cs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -270,13 +270,10 @@ internal TypedValue TypedValue
270270
$"Writing value of '{value.GetType()}' is not supported without explicit mapping to the YdbDbType")
271271
};
272272

273-
private TypedValue Decimal(decimal value)
274-
{
275-
var p = Precision == 0 && Scale == 0 ? 22 : Precision;
276-
var s = Precision == 0 && Scale == 0 ? 9 : Scale;
277-
278-
return value.Decimal((byte)p, (byte)s);
279-
}
273+
private TypedValue Decimal(decimal value) =>
274+
Precision == 0 && Scale == 0
275+
? value.Decimal(22, 9)
276+
: value.Decimal(Precision, Scale);
280277

281278
private TypedValue NullTypedValue()
282279
{

src/Ydb.Sdk/src/Ado/YdbType/YdbTypedValueExtensions.cs

Lines changed: 20 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Globalization;
2+
using System.Numerics;
23
using Google.Protobuf;
34
using Google.Protobuf.WellKnownTypes;
45

@@ -74,61 +75,33 @@ internal static TypedValue Double(this double value) =>
7475

7576
internal static TypedValue Decimal(this decimal value, byte precision, byte scale)
7677
{
77-
var bits0 = decimal.GetBits(value);
78-
var fracDigits0 = (bits0[3] >> 16) & 0xFF;
78+
var bits = decimal.GetBits(value);
79+
var scale0 = (bits[3] >> 16) & 0xFF;
80+
var isNegative = (bits[3] & unchecked((int)0x80000000)) != 0;
7981

80-
var absInt0 = decimal.Truncate(Math.Abs(value));
81-
var integerDigits0 = absInt0 == 0m
82-
? 1
83-
: absInt0.ToString(CultureInfo.InvariantCulture).Length;
84-
85-
if (fracDigits0 > scale)
86-
throw new OverflowException(
87-
$"Decimal scale overflow: fractional digits {fracDigits0} exceed allowed {scale} for DECIMAL({precision},{scale}). Value={value}");
88-
89-
if (integerDigits0 > precision - scale)
82+
if (scale0 > scale)
9083
throw new OverflowException(
91-
$"Decimal precision overflow: integer digits {integerDigits0} exceed allowed {precision - scale} for DECIMAL({precision},{scale}). Value={value}");
92-
93-
var rounded = Math.Round(value, scale, MidpointRounding.ToEven);
84+
$"Decimal scale overflow: fractional digits {scale0} exceed allowed {scale} for DECIMAL({precision},{scale}). Value={value}");
9485

95-
var rb = decimal.GetBits(rounded);
96-
var roundedScale = (rb[3] >> 16) & 0xFF;
97-
var negative = (rb[3] & unchecked((int)0x80000000)) != 0;
86+
var lo = (uint)bits[0];
87+
var mid = (uint)bits[1];
88+
var hi = (uint)bits[2];
89+
var mantissa = ((BigInteger)hi << 64) | ((BigInteger)mid << 32) | lo;
9890

99-
var unscaled = new decimal(rb[0], rb[1], rb[2], false, 0);
100-
101-
var delta = scale - roundedScale;
91+
var delta = scale - scale0;
10292
if (delta > 0)
103-
{
104-
for (var i = 0; i < delta; i++)
105-
unscaled *= 10m;
106-
}
107-
else if (delta < 0)
108-
{
109-
for (var i = 0; i < -delta; i++)
110-
unscaled /= 10m;
111-
}
112-
113-
var ub = decimal.GetBits(unscaled);
114-
var low = ((ulong)ub[1] << 32) + (uint)ub[0];
115-
var high = (ulong)ub[2];
93+
mantissa *= BigInteger.Pow(10, delta);
11694

117-
unchecked
118-
{
119-
if (negative)
120-
{
121-
low = ~low;
122-
high = ~high;
95+
var totalDigits = mantissa.IsZero ? 1 : mantissa.ToString(CultureInfo.InvariantCulture).Length;
96+
if (totalDigits > precision)
97+
throw new OverflowException(
98+
$"Decimal precision overflow: total digits {totalDigits} exceed allowed {precision} for DECIMAL({precision},{scale}). Value={value}");
12399

124-
if (low == (ulong)-1L)
125-
{
126-
high += 1;
127-
}
100+
if (isNegative) mantissa = -mantissa;
128101

129-
low += 1;
130-
}
131-
}
102+
var mod128 = (mantissa % (BigInteger.One << 128) + (BigInteger.One << 128)) % (BigInteger.One << 128);
103+
var low = (ulong)(mod128 & ((BigInteger.One << 64) - 1));
104+
var high = (ulong)(mod128 >> 64);
132105

133106
return new TypedValue
134107
{

src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbParameterTests.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,18 @@ public async Task Decimal_WhenYdbReturnsDecimal35_0_OverflowsDotNetDecimal()
296296
await Assert.ThrowsAsync<OverflowException>(() => select.ExecuteScalarAsync());
297297
}
298298

299+
[Fact]
300+
public void Decimal_WhenEncodingP35_10_With25IntegerDigits_DoesNotOverflow()
301+
{
302+
var val = decimal.Parse("1234567890123456789012345", CultureInfo.InvariantCulture);
303+
var param = new YdbParameter("d", DbType.Decimal, val) { Precision = 35, Scale = 10 };
304+
305+
var tv = param.TypedValue;
306+
307+
Assert.Equal((byte)35, tv.Type.DecimalType.Precision);
308+
Assert.Equal((byte)10, tv.Type.DecimalType.Scale);
309+
}
310+
299311
[Fact]
300312
public async Task YdbParameter_WhenYdbDbTypeSetAndValueIsNull_ReturnsNullValue()
301313
{

0 commit comments

Comments
 (0)