Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Ydb.Sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
- Dev: Parameterized Decimal overflow check (precision/scale).
- Feat: Implement `YdbRetryPolicy` with AWS-inspired Exponential Backoff and Jitter.
- Dev: LogLevel `Warning` -> `Debug` on DeleteSession has been `RpcException`.

Expand Down
43 changes: 25 additions & 18 deletions src/Ydb.Sdk/src/Ado/YdbType/YdbTypedValueExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Globalization;
using System.Numerics;
using Google.Protobuf;
using Google.Protobuf.WellKnownTypes;

Expand Down Expand Up @@ -73,28 +75,33 @@ internal static TypedValue Double(this double value) =>

internal static TypedValue Decimal(this decimal value, byte precision, byte scale)
{
value *= 1.00000000000000000000000000000m; // 29 zeros, max supported by c# decimal
value = Math.Round(value, scale);

var bits = decimal.GetBits(value);
var low = ((ulong)bits[1] << 32) + (uint)bits[0];
var high = (ulong)bits[2];
var scale0 = (bits[3] >> 16) & 0xFF;
var isNegative = (bits[3] & unchecked((int)0x80000000)) != 0;

unchecked
{
if (value < 0)
{
low = ~low;
high = ~high;
if (scale0 > scale)
throw new OverflowException(
$"Decimal scale overflow: fractional digits {scale0} exceed allowed {scale} for DECIMAL({precision},{scale}). Value={value}");

if (low == (ulong)-1L)
{
high += 1;
}
var lo = (uint)bits[0];
var mid = (uint)bits[1];
var hi = (uint)bits[2];
var mantissa = ((BigInteger)hi << 64) | ((BigInteger)mid << 32) | lo;

low += 1;
}
}
var delta = scale - scale0;
if (delta > 0)
mantissa *= BigInteger.Pow(10, delta);

var totalDigits = mantissa.IsZero ? 1 : mantissa.ToString(CultureInfo.InvariantCulture).Length;
if (totalDigits > precision)
throw new OverflowException(
$"Decimal precision overflow: total digits {totalDigits} exceed allowed {precision} for DECIMAL({precision},{scale}). Value={value}");

if (isNegative) mantissa = -mantissa;

var mod128 = (mantissa % (BigInteger.One << 128) + (BigInteger.One << 128)) % (BigInteger.One << 128);
var low = (ulong)(mod128 & ((BigInteger.One << 64) - 1));
var high = (ulong)(mod128 >> 64);

return new TypedValue
{
Expand Down
83 changes: 82 additions & 1 deletion src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbParameterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ public async Task Date_WhenSetDateOnly_ReturnDateTime()
[Theory]
[InlineData("12345", "12345.0000000000", 22, 9)]
[InlineData("54321", "54321", 5, 0)]
[InlineData("493235.4", "493235.40", 7, 2)]
[InlineData("493235.4", "493235.40", 8, 2)]
[InlineData("123.46", "123.46", 5, 2)]
[InlineData("-184467434073.70911616", "-184467434073.7091161600", 35, 10)]
[InlineData("-18446744074", "-18446744074", 12, 0)]
Expand Down Expand Up @@ -227,6 +227,87 @@ PRIMARY KEY (DecimalField)
await new YdbCommand(ydbConnection) { CommandText = $"DROP TABLE {decimalTableName};" }.ExecuteNonQueryAsync();
}

[Fact]
public async Task Decimal_WhenFractionalDigitsExceedScale_Throws()
{
await using var ydb = await CreateOpenConnectionAsync();
var t = $"T_{Random.Shared.Next()}";
await new YdbCommand(ydb) { CommandText = $"CREATE TABLE {t}(d Decimal(5,2), PRIMARY KEY(d))" }
.ExecuteNonQueryAsync();

var cmd = new YdbCommand(ydb)
{
CommandText = $"INSERT INTO {t}(d) VALUES (@d);",
Parameters = { new YdbParameter("d", DbType.Decimal, 123.456m) { Precision = 5, Scale = 2 } }
};

await Assert.ThrowsAsync<OverflowException>(() => cmd.ExecuteNonQueryAsync());
}

[Fact]
public async Task Decimal_WhenIntegerDigitsExceedPrecisionMinusScale_Throws()
{
await using var ydb = await CreateOpenConnectionAsync();
var t = $"T_{Random.Shared.Next()}";
await new YdbCommand(ydb) { CommandText = $"CREATE TABLE {t}(d Decimal(5,0), PRIMARY KEY(d))" }
.ExecuteNonQueryAsync();

var cmd = new YdbCommand(ydb)
{
CommandText = $"INSERT INTO {t}(d) VALUES (@d);",
Parameters = { new YdbParameter("d", DbType.Decimal, 100000m) { Precision = 5, Scale = 0 } }
};

await Assert.ThrowsAsync<OverflowException>(() => cmd.ExecuteNonQueryAsync());
}

[Fact]
public async Task Decimal_WhenScaleGreaterThanPrecision_ThrowsByMathNotByIf()
{
await using var ydb = await CreateOpenConnectionAsync();
var t = $"T_{Random.Shared.Next()}";
await new YdbCommand(ydb) { CommandText = $"CREATE TABLE {t}(d Decimal(5,4), PRIMARY KEY(d))" }
.ExecuteNonQueryAsync();

var cmd = new YdbCommand(ydb)
{
CommandText = $"INSERT INTO {t}(d) VALUES (@d);",
Parameters = { new YdbParameter("d", DbType.Decimal, 0.0m) { Precision = 1, Scale = 2 } }
};

await Assert.ThrowsAnyAsync<Exception>(() => cmd.ExecuteNonQueryAsync());
}

[Fact]
public async Task Decimal_WhenYdbReturnsDecimal35_0_OverflowsDotNetDecimal()
{
await using var ydb = await CreateOpenConnectionAsync();
var t = $"T_{Random.Shared.Next()}";
await new YdbCommand(ydb) { CommandText = $"CREATE TABLE {t}(d Decimal(35,0), PRIMARY KEY(d))" }
.ExecuteNonQueryAsync();

await new YdbCommand(ydb)
{
CommandText = $@"INSERT INTO {t}(d) VALUES (CAST('10000000000000000000000000000000000' AS Decimal(35,0)));"
}.ExecuteNonQueryAsync();

var select = new YdbCommand(ydb) { CommandText = $"SELECT d FROM {t};" };

await Assert.ThrowsAsync<OverflowException>(() => select.ExecuteScalarAsync());
}

[Fact]
public void Decimal_WhenEncodingP35_10_With25IntegerDigits_DoesNotOverflow()
{
var val = decimal.Parse("1234567890123456789012345", CultureInfo.InvariantCulture);
var param = new YdbParameter("d", DbType.Decimal, val) { Precision = 35, Scale = 10 };

var tv = param.TypedValue;

Assert.Equal((byte)35, tv.Type.DecimalType.Precision);
Assert.Equal((byte)10, tv.Type.DecimalType.Scale);
}

[Fact]
public async Task YdbParameter_WhenYdbDbTypeSetAndValueIsNull_ReturnsNullValue()
{
Expand Down
Loading