Skip to content

Commit 04ba60a

Browse files
update
1 parent 4e34a83 commit 04ba60a

File tree

4 files changed

+139
-122
lines changed

4 files changed

+139
-122
lines changed

src/Ydb.Sdk/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
- Feat ADO.NET: Parameterized Decimal overflow check (precision/scale).
1+
- Fix bug wrap-around ADO.NET: Big parameterized Decimal — `((ulong)bits[1] << 32)` -> `((ulong)(uint)bits[1] << 32)`
2+
- Feat ADO.NET: Parameterized Decimal overflow check: `Precision` and `Scale`.
23
- Feat ADO.NET: Deleted support for `DateTimeOffset` was a mistake.
34
- Feat ADO.NET: Added support for `Date32`, `Datetime64`, `Timestamp64` and `Interval64` types in YDB.
45
- Feat: Implement `YdbRetryPolicy` with AWS-inspired Exponential Backoff and Jitter.

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

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
1-
using System.Globalization;
2-
using System.Numerics;
31
using Google.Protobuf;
42
using Google.Protobuf.WellKnownTypes;
53

64
namespace Ydb.Sdk.Ado.Internal;
75

86
internal static class YdbTypedValueExtensions
97
{
8+
private const byte MaxPrecisionDecimal = 29;
9+
private static readonly decimal[] Pow10 = CreatePow10();
10+
11+
private static decimal[] CreatePow10()
12+
{
13+
var a = new decimal[29];
14+
a[0] = 1m;
15+
for (var i = 1; i < a.Length; i++) a[i] = a[i - 1] * 10m; // 1..1e28
16+
return a;
17+
}
18+
1019
internal static TypedValue Null(this Type.Types.PrimitiveTypeId primitiveTypeId) => new()
1120
{
1221
Type = new Type { OptionalType = new OptionalType { Item = new Type { TypeId = primitiveTypeId } } },
@@ -75,33 +84,31 @@ internal static TypedValue Double(this double value) =>
7584

7685
internal static TypedValue Decimal(this decimal value, byte precision, byte scale)
7786
{
78-
var bits = decimal.GetBits(value);
79-
var scale0 = (bits[3] >> 16) & 0xFF;
80-
var isNegative = (bits[3] & unchecked((int)0x80000000)) != 0;
81-
82-
if (scale0 > scale)
83-
throw new OverflowException(
84-
$"Decimal scale overflow: fractional digits {scale0} exceed allowed {scale} for DECIMAL({precision},{scale}). Value={value}");
85-
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;
87+
if (scale > precision)
88+
throw new ArgumentOutOfRangeException(nameof(scale), "Scale cannot exceed precision");
9089

91-
var delta = scale - scale0;
92-
if (delta > 0)
93-
mantissa *= BigInteger.Pow(10, delta);
90+
var origScale = (decimal.GetBits(value)[3] >> 16) & 0xFF;
9491

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}");
92+
if (origScale > scale || precision < MaxPrecisionDecimal && Pow10[precision - scale] <= Math.Abs(value))
93+
{
94+
throw new OverflowException($"Value {value} does not fit Decimal({precision}, {scale})");
95+
}
9996

100-
if (isNegative) mantissa = -mantissa;
97+
value *= 1.0000000000000000000000000000m; // 28 zeros, max supported by c# decimal
98+
value = Math.Round(value, scale);
99+
var bits = decimal.GetBits(value);
100+
var low = ((ulong)(uint)bits[1] << 32) | (uint)bits[0];
101+
var high = (ulong)(uint)bits[2];
102+
var isNegative = bits[3] < 0;
101103

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);
104+
unchecked
105+
{
106+
if (isNegative)
107+
{
108+
low = ~low + 1UL;
109+
high = ~high + (low == 0 ? 1UL : 0UL);
110+
}
111+
}
105112

106113
return new TypedValue
107114
{

src/Ydb.Sdk/src/Ado/Internal/YdbValueExtensions.cs

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -77,25 +77,21 @@ internal static Guid GetUuid(this Ydb.Value value)
7777

7878
internal static decimal GetDecimal(this Ydb.Value value, uint scale)
7979
{
80-
var lo = value.Low128;
81-
var hi = value.High128;
82-
var isNegative = (hi & 0x8000_0000_0000_0000UL) != 0;
80+
var low = value.Low128;
81+
var high = value.High128;
82+
var isNegative = (high & 0x8000_0000_0000_0000UL) != 0;
8383
unchecked
8484
{
8585
if (isNegative)
8686
{
87-
if (lo == 0)
88-
hi--;
89-
90-
lo--;
91-
lo = ~lo;
92-
hi = ~hi;
87+
low = ~low + 1UL;
88+
high = ~high + (low == 0 ? 1UL : 0UL);
9389
}
9490
}
9591

96-
if (hi >> 32 != 0)
92+
if (high >> 32 != 0)
9793
throw new OverflowException("Value does not fit into decimal");
9894

99-
return new decimal((int)lo, (int)(lo >> 32), (int)hi, isNegative, (byte)scale);
95+
return new decimal((int)low, (int)(low >> 32), (int)high, isNegative, (byte)scale);
10096
}
10197
}

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

Lines changed: 98 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -150,123 +150,136 @@ public async Task Date_WhenSetDateOnly_ReturnDateTime()
150150
[InlineData("54321", "54321", 5, 0)]
151151
[InlineData("493235.4", "493235.40", 8, 2)]
152152
[InlineData("123.46", "123.46", 5, 2)]
153+
[InlineData("0.46", "0.46", 2, 2)]
153154
[InlineData("-184467434073.70911616", "-184467434073.7091161600", 35, 10)]
154155
[InlineData("-18446744074", "-18446744074", 12, 0)]
155156
[InlineData("-184467440730709551616", "-184467440730709551616", 21, 0)]
156157
[InlineData("-218446744073.709551616", "-218446744073.7095516160", 22, 10)]
158+
[InlineData("79228162514264337593543950335", "79228162514264337593543950335", 29, 0)]
159+
[InlineData("79228162514264337593543950.335", "79228162514264337593543950.335", 29, 3)]
160+
[InlineData("-79228162514264337593543950335", "-79228162514264337593543950335", 29, 0)]
161+
[InlineData("-79228162514264337593543950.335", "-79228162514264337593543950.335", 29, 3)]
157162
[InlineData(null, null, 22, 9)]
158163
[InlineData(null, null, 35, 9)]
159164
[InlineData(null, null, 35, 0)]
160165
public async Task Decimal_WhenDecimalIsScaleAndPrecision_ReturnDecimal(string? value, string? expected,
161166
byte precision, byte scale)
162167
{
163168
await using var ydbConnection = await CreateOpenConnectionAsync();
164-
var decimalTableName = $"DecimalTable_{Random.Shared.Next()}";
169+
var tableName = $"DecimalTable_{Random.Shared.Next()}";
165170
var decimalValue = value == null ? (decimal?)null : decimal.Parse(value, CultureInfo.InvariantCulture);
166171
await new YdbCommand(ydbConnection)
167-
{
168-
CommandText = $"""
169-
CREATE TABLE {decimalTableName} (
170-
DecimalField Decimal({precision}, {scale}),
171-
PRIMARY KEY (DecimalField)
172-
)
173-
"""
174-
}.ExecuteNonQueryAsync();
172+
{ CommandText = $"CREATE TABLE {tableName} (d Decimal({precision}, {scale}), PRIMARY KEY (d))" }
173+
.ExecuteNonQueryAsync();
175174
await new YdbCommand(ydbConnection)
176175
{
177-
CommandText = $"INSERT INTO {decimalTableName}(DecimalField) VALUES (@DecimalField);",
176+
CommandText = $"INSERT INTO {tableName}(d) VALUES (@d);",
178177
Parameters =
179-
{
180-
new YdbParameter("DecimalField", DbType.Decimal, decimalValue) { Precision = precision, Scale = scale }
181-
}
178+
{ new YdbParameter("d", DbType.Decimal, decimalValue) { Precision = precision, Scale = scale } }
182179
}.ExecuteNonQueryAsync();
183180

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

188-
await new YdbCommand(ydbConnection) { CommandText = $"DROP TABLE {decimalTableName};" }.ExecuteNonQueryAsync();
185+
await new YdbCommand(ydbConnection) { CommandText = $"DROP TABLE {tableName};" }.ExecuteNonQueryAsync();
189186
}
190187

191-
[Fact]
192-
public async Task Decimal_WhenFractionalDigitsExceedScale_Throws()
193-
{
194-
await using var ydb = await CreateOpenConnectionAsync();
195-
var t = $"T_{Random.Shared.Next()}";
196-
await new YdbCommand(ydb) { CommandText = $"CREATE TABLE {t}(d Decimal(5,2), PRIMARY KEY(d))" }
197-
.ExecuteNonQueryAsync();
198-
199-
var cmd = new YdbCommand(ydb)
200-
{
201-
CommandText = $"INSERT INTO {t}(d) VALUES (@d);",
202-
Parameters = { new YdbParameter("d", DbType.Decimal, 123.456m) { Precision = 5, Scale = 2 } }
203-
};
204-
205-
await Assert.ThrowsAsync<OverflowException>(() => cmd.ExecuteNonQueryAsync());
206-
}
207-
208-
[Fact]
209-
public async Task Decimal_WhenIntegerDigitsExceedPrecisionMinusScale_Throws()
188+
[Theory]
189+
[InlineData("123.456", 5, 2)]
190+
[InlineData("1.46", 2, 2)]
191+
[InlineData("654321", 5, 0)]
192+
[InlineData("493235.4", 7, 2)]
193+
[InlineData("10.46", 3, 2)]
194+
[InlineData("99.999", 5, 2)]
195+
[InlineData("0.001", 3, 2)]
196+
[InlineData("-12.345", 5, 2)]
197+
[InlineData("7.001", 4, 2)]
198+
[InlineData("1.0001", 5, 3)]
199+
[InlineData("1000.00", 5, 2)]
200+
[InlineData("123456.7", 6, 1)]
201+
[InlineData("999.99", 5, 4)]
202+
[InlineData("-100", 2, 0)]
203+
[InlineData("-0.12", 2, 1)]
204+
[InlineData("10.0", 2, 0)]
205+
[InlineData("-0.1", 1, 0)]
206+
[InlineData("10000", 4, 0)]
207+
[InlineData("12345", 4, 0)]
208+
[InlineData("12.3456", 6, 3)]
209+
[InlineData("123.45", 4, 1)]
210+
[InlineData("9999.9", 5, 0)]
211+
[InlineData("-1234.56", 5, 1)]
212+
[InlineData("-1000", 3, 0)]
213+
[InlineData("0.0001", 4, 3)]
214+
[InlineData("99999", 4, 0)]
215+
[InlineData("9.999", 3, 2)]
216+
[InlineData("123.4", 3, 0)]
217+
[InlineData("1.234", 4, 2)]
218+
[InlineData("-98.765", 5, 2)]
219+
[InlineData("100.01", 5, 1)]
220+
[InlineData("100000", 5, 0)]
221+
public async Task Decimal_WhenNotRepresentableBySystemDecimal_ThrowsOverflowException(string value, byte precision,
222+
byte scale)
210223
{
211-
await using var ydb = await CreateOpenConnectionAsync();
212-
var t = $"T_{Random.Shared.Next()}";
213-
await new YdbCommand(ydb) { CommandText = $"CREATE TABLE {t}(d Decimal(5,0), PRIMARY KEY(d))" }
224+
await using var ydbConnection = await CreateOpenConnectionAsync();
225+
var tableName = $"DecimalOverflowTable__{Random.Shared.Next()}";
226+
var decimalValue = decimal.Parse(value, CultureInfo.InvariantCulture);
227+
await new YdbCommand(ydbConnection)
228+
{ CommandText = $"CREATE TABLE {tableName}(d Decimal(5,2), PRIMARY KEY(d))" }
214229
.ExecuteNonQueryAsync();
215230

216-
var cmd = new YdbCommand(ydb)
217-
{
218-
CommandText = $"INSERT INTO {t}(d) VALUES (@d);",
219-
Parameters = { new YdbParameter("d", DbType.Decimal, 100000m) { Precision = 5, Scale = 0 } }
220-
};
221-
222-
await Assert.ThrowsAsync<OverflowException>(() => cmd.ExecuteNonQueryAsync());
231+
Assert.Equal($"Value {decimalValue} does not fit Decimal({precision}, {scale})",
232+
(await Assert.ThrowsAsync<OverflowException>(() => new YdbCommand(ydbConnection)
233+
{
234+
CommandText = $"INSERT INTO {tableName}(d) VALUES (@d);",
235+
Parameters =
236+
{
237+
new YdbParameter("d", DbType.Decimal, 123.456m)
238+
{ Value = decimalValue, Precision = precision, Scale = scale }
239+
}
240+
}.ExecuteNonQueryAsync())).Message);
241+
242+
Assert.Equal(0ul,
243+
(ulong)(await new YdbCommand(ydbConnection) { CommandText = $"SELECT COUNT(*) FROM {tableName};" }
244+
.ExecuteScalarAsync())!
245+
);
246+
247+
await new YdbCommand(ydbConnection) { CommandText = $"DROP TABLE {tableName};" }.ExecuteNonQueryAsync();
223248
}
224249

225-
[Fact]
226-
public async Task Decimal_WhenScaleGreaterThanPrecision_ThrowsByMathNotByIf()
227-
{
228-
await using var ydb = await CreateOpenConnectionAsync();
229-
var t = $"T_{Random.Shared.Next()}";
230-
await new YdbCommand(ydb) { CommandText = $"CREATE TABLE {t}(d Decimal(5,4), PRIMARY KEY(d))" }
231-
.ExecuteNonQueryAsync();
232-
233-
var cmd = new YdbCommand(ydb)
234-
{
235-
CommandText = $"INSERT INTO {t}(d) VALUES (@d);",
236-
Parameters = { new YdbParameter("d", DbType.Decimal, 0.0m) { Precision = 1, Scale = 2 } }
237-
};
238-
239-
await Assert.ThrowsAnyAsync<Exception>(() => cmd.ExecuteNonQueryAsync());
240-
}
241250

242251
[Fact]
243-
public async Task Decimal_WhenYdbReturnsDecimal35_0_OverflowsDotNetDecimal()
244-
{
245-
await using var ydb = await CreateOpenConnectionAsync();
246-
var t = $"T_{Random.Shared.Next()}";
247-
await new YdbCommand(ydb) { CommandText = $"CREATE TABLE {t}(d Decimal(35,0), PRIMARY KEY(d))" }
248-
.ExecuteNonQueryAsync();
252+
public void Decimal_WhenScaleGreaterThanPrecision_ThrowsArgumentOutOfRangeException() =>
253+
Assert.Throws<ArgumentOutOfRangeException>(() =>
254+
new YdbParameter("d", DbType.Decimal, 0.0m) { Precision = 1, Scale = 2 }.TypedValue);
249255

250-
await new YdbCommand(ydb)
251-
{
252-
CommandText = $@"INSERT INTO {t}(d) VALUES (CAST('10000000000000000000000000000000000' AS Decimal(35,0)));"
253-
}.ExecuteNonQueryAsync();
254-
255-
var select = new YdbCommand(ydb) { CommandText = $"SELECT d FROM {t};" };
256-
257-
await Assert.ThrowsAsync<OverflowException>(() => select.ExecuteScalarAsync());
258-
}
259-
260-
[Fact]
261-
public void Decimal_WhenEncodingP35_10_With25IntegerDigits_DoesNotOverflow()
256+
[Theory]
257+
[InlineData("10000000000000000000000000000000000", 35, 0)]
258+
[InlineData("1000000000000000000000000.0000000000", 35, 10)]
259+
[InlineData("1000000000000000000000000000000000", 34, 0)]
260+
[InlineData("100000000000000000000000.0000000000", 34, 10)]
261+
[InlineData("100000000000000000000000000000000", 33, 0)]
262+
[InlineData("10000000000000000000000.0000000000", 33, 10)]
263+
[InlineData("-10000000000000000000000000000000", 32, 0)]
264+
[InlineData("-1000000000000000000000.0000000000", 32, 10)]
265+
[InlineData("-1000000000000000000000000000000", 31, 0)]
266+
[InlineData("-100000000000000000000.0000000000", 31, 10)]
267+
[InlineData("1000000000000000000000000000000", 30, 0)]
268+
[InlineData("100000000000000000000.0000000000", 30, 10)]
269+
[InlineData("79228162514264337593543950336", 29, 0)]
270+
[InlineData("79228162514264337593543950.336", 29, 3)]
271+
[InlineData("-79228162514264337593543950336", 29, 0)]
272+
[InlineData("-79228162514264337593543950.336", 29, 3)]
273+
[InlineData("100000", 4, 0)] // inf
274+
public async Task Decimal_WhenYdbReturnsDecimalWithPrecisionGreaterThan28_ThrowsOverflowException(string value,
275+
int precision, int scale)
262276
{
263-
var val = decimal.Parse("1234567890123456789012345", CultureInfo.InvariantCulture);
264-
var param = new YdbParameter("d", DbType.Decimal, val) { Precision = 35, Scale = 10 };
265-
266-
var tv = param.TypedValue;
267-
268-
Assert.Equal((byte)35, tv.Type.DecimalType.Precision);
269-
Assert.Equal((byte)10, tv.Type.DecimalType.Scale);
277+
await using var ydbConnection = await CreateOpenConnectionAsync();
278+
Assert.Equal("Value does not fit into decimal", (await Assert.ThrowsAsync<OverflowException>(() =>
279+
new YdbCommand(ydbConnection)
280+
{ CommandText = $"SELECT (CAST('{value}' AS Decimal({precision}, {scale})));" }
281+
.ExecuteScalarAsync())
282+
).Message);
270283
}
271284

272285
[Fact]

0 commit comments

Comments
 (0)