Skip to content

Commit 8960986

Browse files
committed
fix: migration of checks to extension, correct scale, new overflow tests
1 parent 2f85444 commit 8960986

File tree

3 files changed

+75
-52
lines changed

3 files changed

+75
-52
lines changed

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

Lines changed: 2 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
using System.Data;
33
using System.Data.Common;
44
using System.Diagnostics.CodeAnalysis;
5-
using System.Globalization;
6-
using System.Numerics;
75
using Ydb.Sdk.Ado.YdbType;
86
using Ydb.Sdk.Value;
97

@@ -274,54 +272,12 @@ internal TypedValue TypedValue
274272

275273
private TypedValue Decimal(decimal value)
276274
{
277-
var p = (Precision == 0 && Scale == 0) ? 22 : Precision;
278-
var s = (Precision == 0 && Scale == 0) ? 9 : Scale;
279-
280-
EnsureDecimalFits(value, p, s);
275+
var p = Precision == 0 && Scale == 0 ? 22 : Precision;
276+
var s = Precision == 0 && Scale == 0 ? 9 : Scale;
281277

282278
return value.Decimal((byte)p, (byte)s);
283279
}
284280

285-
private static void EnsureDecimalFits(decimal value, int precision, int scale)
286-
{
287-
if (precision <= 0 || scale < 0 || scale > precision)
288-
throw new ArgumentOutOfRangeException(
289-
$"Invalid DECIMAL({precision},{scale}) — must have precision>0 and 0<=scale<=precision.");
290-
291-
GetTrimmedMantissaAndScale(value, out BigInteger mantissa, out int fracDigits /* = scale' */);
292-
293-
var totalDigits = mantissa.IsZero ? 1 : mantissa.ToString(CultureInfo.InvariantCulture).Length;
294-
295-
var intDigits = Math.Max(1, totalDigits - fracDigits);
296-
297-
if (fracDigits > scale)
298-
throw new OverflowException(
299-
$"Decimal scale overflow: fractional digits {fracDigits} exceed allowed {scale} for DECIMAL({precision},{scale}). Value={value}");
300-
301-
if (intDigits > (precision - scale))
302-
throw new OverflowException(
303-
$"Decimal precision overflow: integer digits {intDigits} exceed allowed {precision - scale} for DECIMAL({precision},{scale}). Value={value}");
304-
}
305-
306-
private static void GetTrimmedMantissaAndScale(decimal value, out BigInteger mantissa, out int scale)
307-
{
308-
var bits = decimal.GetBits(value);
309-
scale = (bits[3] >> 16) & 0xFF;
310-
311-
var lo = (uint)bits[0];
312-
var mid = (uint)bits[1];
313-
var hi = (uint)bits[2];
314-
315-
mantissa = ((BigInteger)hi << 64) | ((BigInteger)mid << 32) | lo;
316-
mantissa = BigInteger.Abs(mantissa);
317-
318-
while (scale > 0 && !mantissa.IsZero && mantissa % 10 == 0)
319-
{
320-
mantissa /= 10;
321-
scale--;
322-
}
323-
}
324-
325281
private TypedValue NullTypedValue()
326282
{
327283
if (YdbNullByDbType.TryGetValue(YdbDbType, out var value))

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

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Globalization;
12
using Google.Protobuf;
23
using Google.Protobuf.WellKnownTypes;
34

@@ -73,16 +74,49 @@ internal static TypedValue Double(this double value) =>
7374

7475
internal static TypedValue Decimal(this decimal value, byte precision, byte scale)
7576
{
76-
value *= 1.00000000000000000000000000000m; // 29 zeros, max supported by c# decimal
77-
value = Math.Round(value, scale);
77+
var bits0 = decimal.GetBits(value);
78+
var fracDigits0 = (bits0[3] >> 16) & 0xFF;
7879

79-
var bits = decimal.GetBits(value);
80-
var low = ((ulong)bits[1] << 32) + (uint)bits[0];
81-
var high = (ulong)bits[2];
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)
90+
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);
94+
95+
var rb = decimal.GetBits(rounded);
96+
var roundedScale = (rb[3] >> 16) & 0xFF;
97+
var negative = (rb[3] & unchecked((int)0x80000000)) != 0;
98+
99+
var unscaled = new decimal(rb[0], rb[1], rb[2], false, 0);
100+
101+
int delta = scale - roundedScale;
102+
if (delta > 0)
103+
{
104+
for (int i = 0; i < delta; i++)
105+
unscaled *= 10m;
106+
}
107+
else if (delta < 0)
108+
{
109+
for (int 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];
82116

83117
unchecked
84118
{
85-
if (value < 0)
119+
if (negative)
86120
{
87121
low = ~low;
88122
high = ~high;

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,39 @@ public async Task Decimal_WhenIntegerDigitsExceedPrecisionMinusScale_Throws()
258258

259259
await Assert.ThrowsAsync<OverflowException>(() => cmd.ExecuteNonQueryAsync());
260260
}
261+
262+
[Fact]
263+
public async Task Decimal_WhenScaleGreaterThanPrecision_ThrowsByMathNotByIf()
264+
{
265+
await using var ydb = await CreateOpenConnectionAsync();
266+
var t = $"T_{Random.Shared.Next()}";
267+
await new YdbCommand(ydb){ CommandText = $"CREATE TABLE {t}(d Decimal(5,4), PRIMARY KEY(d))" }.ExecuteNonQueryAsync();
268+
269+
var cmd = new YdbCommand(ydb)
270+
{
271+
CommandText = $"INSERT INTO {t}(d) VALUES (@d);",
272+
Parameters = { new YdbParameter("d", DbType.Decimal, 0.0m){ Precision = 1, Scale = 2 } }
273+
};
274+
275+
await Assert.ThrowsAnyAsync<Exception>(() => cmd.ExecuteNonQueryAsync());
276+
}
277+
278+
[Fact]
279+
public async Task Decimal_WhenYdbReturnsDecimal35_0_OverflowsDotNetDecimal()
280+
{
281+
await using var ydb = await CreateOpenConnectionAsync();
282+
var t = $"T_{Random.Shared.Next()}";
283+
await new YdbCommand(ydb){ CommandText = $"CREATE TABLE {t}(d Decimal(35,0), PRIMARY KEY(d))" }.ExecuteNonQueryAsync();
284+
285+
await new YdbCommand(ydb)
286+
{
287+
CommandText = $@"INSERT INTO {t}(d) VALUES (CAST('10000000000000000000000000000000000' AS Decimal(35,0)));"
288+
}.ExecuteNonQueryAsync();
289+
290+
var select = new YdbCommand(ydb){ CommandText = $"SELECT d FROM {t};" };
291+
292+
await Assert.ThrowsAsync<OverflowException>(() => select.ExecuteScalarAsync());
293+
}
261294

262295
[Fact]
263296
public async Task YdbParameter_WhenYdbDbTypeSetAndValueIsNull_ReturnsNullValue()

0 commit comments

Comments
 (0)