Skip to content
2 changes: 2 additions & 0 deletions src/Ydb.Sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
- Fix bug wrap-around ADO.NET: Big parameterized Decimal — `((ulong)bits[1] << 32)` -> `((ulong)(uint)bits[1] << 32)`
- Feat ADO.NET: Parameterized Decimal overflow check: `Precision` and `Scale`.
- Feat ADO.NET: Deleted support for `DateTimeOffset` was a mistake.
- Feat ADO.NET: Added support for `Date32`, `Datetime64`, `Timestamp64` and `Interval64` types in YDB.
- Feat: Implement `YdbRetryPolicy` with AWS-inspired Exponential Backoff and Jitter.
Expand Down
42 changes: 28 additions & 14 deletions src/Ydb.Sdk/src/Ado/Internal/YdbTypedValueExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ namespace Ydb.Sdk.Ado.Internal;

internal static class YdbTypedValueExtensions
{
private const byte MaxPrecisionDecimal = 29;
private static readonly decimal[] Pow10 = CreatePow10();

private static decimal[] CreatePow10()
{
var a = new decimal[29];
a[0] = 1m;
for (var i = 1; i < a.Length; i++) a[i] = a[i - 1] * 10m; // 1..1e28
return a;
}

internal static TypedValue Null(this Type.Types.PrimitiveTypeId primitiveTypeId) => new()
{
Type = new Type { OptionalType = new OptionalType { Item = new Type { TypeId = primitiveTypeId } } },
Expand Down Expand Up @@ -73,26 +84,29 @@ 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);
if (scale > precision)
throw new ArgumentOutOfRangeException(nameof(scale), "Scale cannot exceed precision");

var origScale = (decimal.GetBits(value)[3] >> 16) & 0xFF;

if (origScale > scale || (precision < MaxPrecisionDecimal && Pow10[precision - scale] <= Math.Abs(value)))
{
throw new OverflowException($"Value {value} does not fit Decimal({precision}, {scale})");
}

value *= 1.0000000000000000000000000000m; // 28 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 low = ((ulong)(uint)bits[1] << 32) | (uint)bits[0];
var high = (ulong)(uint)bits[2];
var isNegative = bits[3] < 0;

unchecked
{
if (value < 0)
if (isNegative)
{
low = ~low;
high = ~high;

if (low == (ulong)-1L)
{
high += 1;
}

low += 1;
low = ~low + 1UL;
high = ~high + (low == 0 ? 1UL : 0UL);
}
}

Expand Down
18 changes: 7 additions & 11 deletions src/Ydb.Sdk/src/Ado/Internal/YdbValueExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,25 +77,21 @@ internal static Guid GetUuid(this Ydb.Value value)

internal static decimal GetDecimal(this Ydb.Value value, uint scale)
{
var lo = value.Low128;
var hi = value.High128;
var isNegative = (hi & 0x8000_0000_0000_0000UL) != 0;
var low = value.Low128;
var high = value.High128;
var isNegative = (high & 0x8000_0000_0000_0000UL) != 0;
unchecked
{
if (isNegative)
{
if (lo == 0)
hi--;

lo--;
lo = ~lo;
hi = ~hi;
low = ~low + 1UL;
high = ~high + (low == 0 ? 1UL : 0UL);
}
}

if (hi >> 32 != 0)
if (high >> 32 != 0)
throw new OverflowException("Value does not fit into decimal");

return new decimal((int)lo, (int)(lo >> 32), (int)hi, isNegative, (byte)scale);
return new decimal((int)low, (int)(low >> 32), (int)high, isNegative, (byte)scale);
}
}
126 changes: 110 additions & 16 deletions src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbParameterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,44 +148,138 @@ 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("0.46", "0.46", 2, 2)]
[InlineData("-184467434073.70911616", "-184467434073.7091161600", 35, 10)]
[InlineData("-18446744074", "-18446744074", 12, 0)]
[InlineData("-184467440730709551616", "-184467440730709551616", 21, 0)]
[InlineData("-218446744073.709551616", "-218446744073.7095516160", 22, 10)]
[InlineData("79228162514264337593543950335", "79228162514264337593543950335", 29, 0)]
[InlineData("79228162514264337593543950.335", "79228162514264337593543950.335", 29, 3)]
[InlineData("-79228162514264337593543950335", "-79228162514264337593543950335", 29, 0)]
[InlineData("-79228162514264337593543950.335", "-79228162514264337593543950.335", 29, 3)]
[InlineData(null, null, 22, 9)]
[InlineData(null, null, 35, 9)]
[InlineData(null, null, 35, 0)]
public async Task Decimal_WhenDecimalIsScaleAndPrecision_ReturnDecimal(string? value, string? expected,
byte precision, byte scale)
{
await using var ydbConnection = await CreateOpenConnectionAsync();
var decimalTableName = $"DecimalTable_{Random.Shared.Next()}";
var tableName = $"DecimalTable_{Random.Shared.Next()}";
var decimalValue = value == null ? (decimal?)null : decimal.Parse(value, CultureInfo.InvariantCulture);
await new YdbCommand(ydbConnection)
{
CommandText = $"""
CREATE TABLE {decimalTableName} (
DecimalField Decimal({precision}, {scale}),
PRIMARY KEY (DecimalField)
)
"""
}.ExecuteNonQueryAsync();
{ CommandText = $"CREATE TABLE {tableName} (d Decimal({precision}, {scale}), PRIMARY KEY (d))" }
.ExecuteNonQueryAsync();
await new YdbCommand(ydbConnection)
{
CommandText = $"INSERT INTO {decimalTableName}(DecimalField) VALUES (@DecimalField);",
CommandText = $"INSERT INTO {tableName}(d) VALUES (@d);",
Parameters =
{
new YdbParameter("DecimalField", DbType.Decimal, decimalValue) { Precision = precision, Scale = scale }
}
{ new YdbParameter("d", DbType.Decimal, decimalValue) { Precision = precision, Scale = scale } }
}.ExecuteNonQueryAsync();

Assert.Equal(expected == null ? DBNull.Value : decimal.Parse(expected, CultureInfo.InvariantCulture),
await new YdbCommand(ydbConnection) { CommandText = $"SELECT DecimalField FROM {decimalTableName};" }
await new YdbCommand(ydbConnection) { CommandText = $"SELECT d FROM {tableName};" }
.ExecuteScalarAsync());

await new YdbCommand(ydbConnection) { CommandText = $"DROP TABLE {decimalTableName};" }.ExecuteNonQueryAsync();
await new YdbCommand(ydbConnection) { CommandText = $"DROP TABLE {tableName};" }.ExecuteNonQueryAsync();
}

[Theory]
[InlineData("123.456", 5, 2)]
[InlineData("1.46", 2, 2)]
[InlineData("654321", 5, 0)]
[InlineData("493235.4", 7, 2)]
[InlineData("10.46", 3, 2)]
[InlineData("99.999", 5, 2)]
[InlineData("0.001", 3, 2)]
[InlineData("-12.345", 5, 2)]
[InlineData("7.001", 4, 2)]
[InlineData("1.0001", 5, 3)]
[InlineData("1000.00", 5, 2)]
[InlineData("123456.7", 6, 1)]
[InlineData("999.99", 5, 4)]
[InlineData("-100", 2, 0)]
[InlineData("-0.12", 2, 1)]
[InlineData("10.0", 2, 0)]
[InlineData("-0.1", 1, 0)]
[InlineData("10000", 4, 0)]
[InlineData("12345", 4, 0)]
[InlineData("12.3456", 6, 3)]
[InlineData("123.45", 4, 1)]
[InlineData("9999.9", 5, 0)]
[InlineData("-1234.56", 5, 1)]
[InlineData("-1000", 3, 0)]
[InlineData("0.0001", 4, 3)]
[InlineData("99999", 4, 0)]
[InlineData("9.999", 3, 2)]
[InlineData("123.4", 3, 0)]
[InlineData("1.234", 4, 2)]
[InlineData("-98.765", 5, 2)]
[InlineData("100.01", 5, 1)]
[InlineData("100000", 5, 0)]
public async Task Decimal_WhenNotRepresentableBySystemDecimal_ThrowsOverflowException(string value, byte precision,
byte scale)
{
await using var ydbConnection = await CreateOpenConnectionAsync();
var tableName = $"DecimalOverflowTable__{Random.Shared.Next()}";
var decimalValue = decimal.Parse(value, CultureInfo.InvariantCulture);
await new YdbCommand(ydbConnection)
{ CommandText = $"CREATE TABLE {tableName}(d Decimal(5,2), PRIMARY KEY(d))" }
.ExecuteNonQueryAsync();

Assert.Equal($"Value {decimalValue} does not fit Decimal({precision}, {scale})",
(await Assert.ThrowsAsync<OverflowException>(() => new YdbCommand(ydbConnection)
{
CommandText = $"INSERT INTO {tableName}(d) VALUES (@d);",
Parameters =
{
new YdbParameter("d", DbType.Decimal, 123.456m)
{ Value = decimalValue, Precision = precision, Scale = scale }
}
}.ExecuteNonQueryAsync())).Message);

Assert.Equal(0ul,
(ulong)(await new YdbCommand(ydbConnection) { CommandText = $"SELECT COUNT(*) FROM {tableName};" }
.ExecuteScalarAsync())!
);

await new YdbCommand(ydbConnection) { CommandText = $"DROP TABLE {tableName};" }.ExecuteNonQueryAsync();
}


[Fact]
public void Decimal_WhenScaleGreaterThanPrecision_ThrowsArgumentOutOfRangeException() =>
Assert.Throws<ArgumentOutOfRangeException>(() =>
new YdbParameter("d", DbType.Decimal, 0.0m) { Precision = 1, Scale = 2 }.TypedValue);

[Theory]
[InlineData("10000000000000000000000000000000000", 35, 0)]
[InlineData("1000000000000000000000000.0000000000", 35, 10)]
[InlineData("1000000000000000000000000000000000", 34, 0)]
[InlineData("100000000000000000000000.0000000000", 34, 10)]
[InlineData("100000000000000000000000000000000", 33, 0)]
[InlineData("10000000000000000000000.0000000000", 33, 10)]
[InlineData("-10000000000000000000000000000000", 32, 0)]
[InlineData("-1000000000000000000000.0000000000", 32, 10)]
[InlineData("-1000000000000000000000000000000", 31, 0)]
[InlineData("-100000000000000000000.0000000000", 31, 10)]
[InlineData("1000000000000000000000000000000", 30, 0)]
[InlineData("100000000000000000000.0000000000", 30, 10)]
[InlineData("79228162514264337593543950336", 29, 0)]
[InlineData("79228162514264337593543950.336", 29, 3)]
[InlineData("-79228162514264337593543950336", 29, 0)]
[InlineData("-79228162514264337593543950.336", 29, 3)]
[InlineData("100000", 4, 0)] // inf
public async Task Decimal_WhenYdbReturnsDecimalWithPrecisionGreaterThan28_ThrowsOverflowException(string value,
int precision, int scale)
{
await using var ydbConnection = await CreateOpenConnectionAsync();
Assert.Equal("Value does not fit into decimal", (await Assert.ThrowsAsync<OverflowException>(() =>
new YdbCommand(ydbConnection)
{ CommandText = $"SELECT (CAST('{value}' AS Decimal({precision}, {scale})));" }
.ExecuteScalarAsync())
).Message);
}

[Fact]
Expand Down
Loading