Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 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
11 changes: 7 additions & 4 deletions src/Ydb.Sdk/src/Ado/YdbParameter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -270,10 +270,13 @@ internal TypedValue TypedValue
$"Writing value of '{value.GetType()}' is not supported without explicit mapping to the YdbDbType")
};

private TypedValue Decimal(decimal value) =>
Precision == 0 && Scale == 0
? value.Decimal(22, 9)
: value.Decimal(Precision, Scale);
private TypedValue Decimal(decimal value)
{
var p = Precision == 0 && Scale == 0 ? 22 : Precision;
var s = Precision == 0 && Scale == 0 ? 9 : Scale;

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

private TypedValue NullTypedValue()
{
Expand Down
46 changes: 40 additions & 6 deletions src/Ydb.Sdk/src/Ado/YdbType/YdbTypedValueExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Globalization;
using Google.Protobuf;
using Google.Protobuf.WellKnownTypes;

Expand Down Expand Up @@ -73,16 +74,49 @@ 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 bits0 = decimal.GetBits(value);
var fracDigits0 = (bits0[3] >> 16) & 0xFF;

var bits = decimal.GetBits(value);
var low = ((ulong)bits[1] << 32) + (uint)bits[0];
var high = (ulong)bits[2];
var absInt0 = decimal.Truncate(Math.Abs(value));
var integerDigits0 = absInt0 == 0m
? 1
: absInt0.ToString(CultureInfo.InvariantCulture).Length;

if (fracDigits0 > scale)
throw new OverflowException(
$"Decimal scale overflow: fractional digits {fracDigits0} exceed allowed {scale} for DECIMAL({precision},{scale}). Value={value}");

if (integerDigits0 > precision - scale)
throw new OverflowException(
$"Decimal precision overflow: integer digits {integerDigits0} exceed allowed {precision - scale} for DECIMAL({precision},{scale}). Value={value}");

var rounded = Math.Round(value, scale, MidpointRounding.ToEven);

var rb = decimal.GetBits(rounded);
var roundedScale = (rb[3] >> 16) & 0xFF;
var negative = (rb[3] & unchecked((int)0x80000000)) != 0;

var unscaled = new decimal(rb[0], rb[1], rb[2], false, 0);

var delta = scale - roundedScale;
if (delta > 0)
{
for (var i = 0; i < delta; i++)
unscaled *= 10m;
}
else if (delta < 0)
{
for (var i = 0; i < -delta; i++)
unscaled /= 10m;
}

var ub = decimal.GetBits(unscaled);
var low = ((ulong)ub[1] << 32) + (uint)ub[0];
var high = (ulong)ub[2];

unchecked
{
if (value < 0)
if (negative)
{
low = ~low;
high = ~high;
Expand Down
71 changes: 70 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,75 @@ 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 async Task YdbParameter_WhenYdbDbTypeSetAndValueIsNull_ReturnsNullValue()
{
Expand Down
Loading