Skip to content

Commit 2d8207e

Browse files
Fix | Improve decimal conversion from SqlDecimal to .Net Decimal (#1179)
1 parent aea4bbe commit 2d8207e

File tree

3 files changed

+295
-0
lines changed

3 files changed

+295
-0
lines changed

src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlBuffer.cs

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ internal DateTime DateTime
205205
}
206206
}
207207

208+
#region Decimal
208209
internal decimal Decimal
209210
{
210211
get
@@ -215,6 +216,43 @@ internal decimal Decimal
215216
{
216217
if (_value._numericInfo._data4 != 0 || _value._numericInfo._scale > 28)
217218
{
219+
// Only removing trailing zeros from a decimal part won't hit its value!
220+
if (_value._numericInfo._scale > 0)
221+
{
222+
int zeroCnt = FindTrailingZerosAndPrec((uint)_value._numericInfo._data1, (uint)_value._numericInfo._data2,
223+
(uint)_value._numericInfo._data3, (uint)_value._numericInfo._data4,
224+
_value._numericInfo._scale, out int precision);
225+
226+
int minScale = _value._numericInfo._scale - zeroCnt; // minimum possible sacle after removing the trailing zeros.
227+
228+
if (zeroCnt > 0 && minScale <= 28 && precision <= 29)
229+
{
230+
SqlDecimal sqlValue = new(_value._numericInfo._precision, _value._numericInfo._scale, _value._numericInfo._positive,
231+
_value._numericInfo._data1, _value._numericInfo._data2,
232+
_value._numericInfo._data3, _value._numericInfo._data4);
233+
234+
int integral = precision - minScale;
235+
int newPrec = 29;
236+
237+
if (integral != 1 && precision != 29)
238+
{
239+
newPrec = 28;
240+
}
241+
242+
try
243+
{
244+
// Precision could be 28 or 29
245+
// ex: (precision == 29 && scale == 28)
246+
// valid: (+/-)7.1234567890123456789012345678
247+
// invalid: (+/-)8.1234567890123456789012345678
248+
return SqlDecimal.ConvertToPrecScale(sqlValue, newPrec, newPrec - integral).Value;
249+
}
250+
catch (OverflowException)
251+
{
252+
throw new OverflowException(SQLResource.ConversionOverflowMessage);
253+
}
254+
}
255+
}
218256
throw new OverflowException(SQLResource.ConversionOverflowMessage);
219257
}
220258
return new decimal(_value._numericInfo._data1, _value._numericInfo._data2, _value._numericInfo._data3, !_value._numericInfo._positive, _value._numericInfo._scale);
@@ -234,6 +272,85 @@ internal decimal Decimal
234272
}
235273
}
236274

275+
/// <summary>
276+
/// Returns number of trailing zeros using the supplied parameters.
277+
/// </summary>
278+
/// <param name="data1">An 32-bit unsigned integer which will be combined with data2, data3, and data4</param>
279+
/// <param name="data2">An 32-bit unsigned integer which will be combined with data1, data3, and data4</param>
280+
/// <param name="data3">An 32-bit unsigned integer which will be combined with data1, data2, and data4</param>
281+
/// <param name="data4">An 32-bit unsigned integer which will be combined with data1, data2, and data3</param>
282+
/// <param name="scale">The number of decimal places</param>
283+
/// <param name="valuablePrecision">OUT |The number of digits without trailing zeros</param>
284+
/// <returns>Number of trailing zeros</returns>
285+
private static int FindTrailingZerosAndPrec(uint data1, uint data2, uint data3, uint data4, byte scale, out int valuablePrecision)
286+
{
287+
// Make local copy of data to avoid modifying input.
288+
Span<uint> rgulNumeric = stackalloc uint[4] { data1, data2, data3, data4 };
289+
int zeroCnt = 0; //Number of trailing zero digits
290+
int precCnt = 0; //Valuable precision
291+
uint uiRem = 0; //Remainder of a division by 10
292+
int len = 4; // Max possible items
293+
294+
//Retrieve each digit from the lowest significant digit
295+
while (len > 1 || rgulNumeric[0] != 0)
296+
{
297+
SqlDecimalDivBy(rgulNumeric, ref len, 10, out uiRem);
298+
if (uiRem == 0 && precCnt == 0)
299+
{
300+
zeroCnt++;
301+
}
302+
else
303+
{
304+
precCnt++;
305+
}
306+
}
307+
308+
if (uiRem == 0)
309+
{
310+
zeroCnt = scale;
311+
}
312+
313+
// if scale of the number has not been reached, pad remaining number with zeros.
314+
if (zeroCnt + precCnt <= scale)
315+
{
316+
precCnt = scale - zeroCnt + 1;
317+
}
318+
valuablePrecision = precCnt;
319+
return zeroCnt;
320+
}
321+
322+
/// <summary>
323+
/// Multi-precision one super-digit divide in place.
324+
/// U = U / D,
325+
/// R = U % D
326+
/// (Length of U can decrease)
327+
/// </summary>
328+
/// <param name="data">InOut | U</param>
329+
/// <param name="len">InOut | Number of items with non-zero value in U between 1 to 4</param>
330+
/// <param name="divisor">In | D</param>
331+
/// <param name="remainder">Out | R</param>
332+
private static void SqlDecimalDivBy(Span<uint> data, ref int len, uint divisor, out uint remainder)
333+
{
334+
uint uiCarry = 0;
335+
ulong ulAccum;
336+
ulong ulDivisor = (ulong)divisor;
337+
int iLen = len;
338+
339+
while (iLen > 0)
340+
{
341+
iLen--;
342+
ulAccum = (((ulong)uiCarry) << 32) + (ulong)(data[iLen]);
343+
data[iLen] = (uint)(ulAccum / ulDivisor);
344+
uiCarry = (uint)(ulAccum - (ulong)data[iLen] * ulDivisor); // (ULONG) (ulAccum % divisor)
345+
}
346+
remainder = uiCarry;
347+
348+
// Normalize multi-precision number - remove leading zeroes
349+
while (len > 1 && data[len - 1] == 0)
350+
{ len--; }
351+
}
352+
#endregion
353+
237354
internal double Double
238355
{
239356
get

src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlBuffer.cs

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ internal DateTime DateTime
202202
}
203203
}
204204

205+
#region Decimal
205206
internal decimal Decimal
206207
{
207208
get
@@ -212,6 +213,43 @@ internal decimal Decimal
212213
{
213214
if (_value._numericInfo._data4 != 0 || _value._numericInfo._scale > 28)
214215
{
216+
// Only removing trailing zeros from a decimal part won't hit its value!
217+
if (_value._numericInfo._scale > 0)
218+
{
219+
int zeroCnt = FindTrailingZerosAndPrec((uint)_value._numericInfo._data1, (uint)_value._numericInfo._data2,
220+
(uint)_value._numericInfo._data3, (uint)_value._numericInfo._data4,
221+
_value._numericInfo._scale, out int precision);
222+
223+
int minScale = _value._numericInfo._scale - zeroCnt; // minimum possible sacle after removing the trailing zeros.
224+
225+
if (zeroCnt > 0 && minScale <= 28 && precision <= 29)
226+
{
227+
SqlDecimal sqlValue = new(_value._numericInfo._precision, _value._numericInfo._scale, _value._numericInfo._positive,
228+
_value._numericInfo._data1, _value._numericInfo._data2,
229+
_value._numericInfo._data3, _value._numericInfo._data4);
230+
231+
int integral = precision - minScale;
232+
int newPrec = 29;
233+
234+
if (integral != 1 && precision != 29)
235+
{
236+
newPrec = 28;
237+
}
238+
239+
try
240+
{
241+
// Precision could be 28 or 29
242+
// ex: (precision == 29 && scale == 28)
243+
// valid: (+/-)7.1234567890123456789012345678
244+
// invalid: (+/-)8.1234567890123456789012345678
245+
return SqlDecimal.ConvertToPrecScale(sqlValue, newPrec, newPrec - integral).Value;
246+
}
247+
catch (OverflowException)
248+
{
249+
throw new OverflowException(SQLResource.ConversionOverflowMessage);
250+
}
251+
}
252+
}
215253
throw new OverflowException(SQLResource.ConversionOverflowMessage);
216254
}
217255
return new decimal(_value._numericInfo._data1, _value._numericInfo._data2, _value._numericInfo._data3, !_value._numericInfo._positive, _value._numericInfo._scale);
@@ -231,6 +269,85 @@ internal decimal Decimal
231269
}
232270
}
233271

272+
/// <summary>
273+
/// Returns number of trailing zeros using the supplied parameters.
274+
/// </summary>
275+
/// <param name="data1">An 32-bit unsigned integer which will be combined with data2, data3, and data4</param>
276+
/// <param name="data2">An 32-bit unsigned integer which will be combined with data1, data3, and data4</param>
277+
/// <param name="data3">An 32-bit unsigned integer which will be combined with data1, data2, and data4</param>
278+
/// <param name="data4">An 32-bit unsigned integer which will be combined with data1, data2, and data3</param>
279+
/// <param name="scale">The number of decimal places</param>
280+
/// <param name="valuablePrecision">OUT |The number of digits without trailing zeros</param>
281+
/// <returns>Number of trailing zeros</returns>
282+
private static int FindTrailingZerosAndPrec(uint data1, uint data2, uint data3, uint data4, byte scale, out int valuablePrecision)
283+
{
284+
// Make local copy of data to avoid modifying input.
285+
Span<uint> rgulNumeric = stackalloc uint[4] { data1, data2, data3, data4 };
286+
int zeroCnt = 0; //Number of trailing zero digits
287+
int precCnt = 0; //Valuable precision
288+
uint uiRem = 0; //Remainder of a division by 10
289+
int len = 4; // Max possible items
290+
291+
//Retrieve each digit from the lowest significant digit
292+
while (len > 1 || rgulNumeric[0] != 0)
293+
{
294+
SqlDecimalDivBy(rgulNumeric, ref len, 10, out uiRem);
295+
if (uiRem == 0 && precCnt == 0)
296+
{
297+
zeroCnt++;
298+
}
299+
else
300+
{
301+
precCnt++;
302+
}
303+
}
304+
305+
if (uiRem == 0)
306+
{
307+
zeroCnt = scale;
308+
}
309+
310+
// if scale of the number has not been reached, pad remaining number with zeros.
311+
if (zeroCnt + precCnt <= scale)
312+
{
313+
precCnt = scale - zeroCnt + 1;
314+
}
315+
valuablePrecision = precCnt;
316+
return zeroCnt;
317+
}
318+
319+
/// <summary>
320+
/// Multi-precision one super-digit divide in place.
321+
/// U = U / D,
322+
/// R = U % D
323+
/// (Length of U can decrease)
324+
/// </summary>
325+
/// <param name="data">InOut | U</param>
326+
/// <param name="len">InOut | Number of items with non-zero value in U between 1 to 4</param>
327+
/// <param name="divisor">In | D</param>
328+
/// <param name="remainder">Out | R</param>
329+
private static void SqlDecimalDivBy(Span<uint> data, ref int len, uint divisor, out uint remainder)
330+
{
331+
uint uiCarry = 0;
332+
ulong ulAccum;
333+
ulong ulDivisor = (ulong)divisor;
334+
int iLen = len;
335+
336+
while (iLen > 0)
337+
{
338+
iLen--;
339+
ulAccum = (((ulong)uiCarry) << 32) + (ulong)(data[iLen]);
340+
data[iLen] = (uint)(ulAccum / ulDivisor);
341+
uiCarry = (uint)(ulAccum - (ulong)data[iLen] * ulDivisor); // (ULONG) (ulAccum % divisor)
342+
}
343+
remainder = uiCarry;
344+
345+
// Normalize multi-precision number - remove leading zeroes
346+
while (len > 1 && data[len - 1] == 0)
347+
{ len--; }
348+
}
349+
#endregion
350+
234351
internal double Double
235352
{
236353
get

src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/ParametersTest.cs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,67 @@ public static void TestParametersWithDatatablesTVPInsert()
319319
}
320320

321321
#region Scaled Decimal Parameter & TVP Test
322+
[ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))]
323+
[InlineData("CAST(1.0 as decimal(38, 37))", "1.0000000000000000000000000000")]
324+
[InlineData("CAST(7.1234567890123456789012345678 as decimal(38, 35))", "7.1234567890123456789012345678")]
325+
[InlineData("CAST(-7.1234567890123456789012345678 as decimal(38, 35))", "-7.1234567890123456789012345678")]
326+
[InlineData("CAST(-0.1234567890123456789012345678 as decimal(38, 35))", "-0.1234567890123456789012345678")]
327+
[InlineData("CAST(4210862852.86 as decimal(38, 20))", "4210862852.860000000000000000")]
328+
[InlineData("CAST(0 as decimal(38, 36))", "0.0000000000000000000000000000")]
329+
[InlineData("CAST(79228162514264337593543950335 as decimal(38, 9))", "79228162514264337593543950335")]
330+
[InlineData("CAST(-79228162514264337593543950335 as decimal(38, 9))", "-79228162514264337593543950335")]
331+
[InlineData("CAST(0.4210862852 as decimal(38, 38))", "0.4210862852000000000000000000")]
332+
[InlineData("CAST(0.1234567890123456789012345678 as decimal(38, 38))", "0.1234567890123456789012345678")]
333+
[InlineData("CAST(249454727.14678312032280248320 as decimal(38, 20))", "249454727.14678312032280248320")]
334+
[InlineData("CAST(3961408124790879675.7769715711 as decimal(38, 10))", "3961408124790879675.7769715711")]
335+
[InlineData("CAST(3961408124790879675776971571.1 as decimal(38, 1))", "3961408124790879675776971571.1")]
336+
[InlineData("CAST(79228162514264337593543950335 as decimal(38, 0))", "79228162514264337593543950335")]
337+
[InlineData("CAST(-79228162514264337593543950335 as decimal(38, 0))", "-79228162514264337593543950335")]
338+
[InlineData("CAST(0.0000000000000000000000000001 as decimal(38, 38))", "0.0000000000000000000000000001")]
339+
[InlineData("CAST(-0.0000000000000000000000000001 as decimal(38, 38))", "-0.0000000000000000000000000001")]
340+
public static void SqlDecimalConvertToDecimal_TestInRange(string sqlDecimalValue, string expectedDecimalValue)
341+
{
342+
using(SqlConnection cnn = new(s_connString))
343+
{
344+
cnn.Open();
345+
using(SqlCommand cmd = new($"SELECT {sqlDecimalValue} val"))
346+
{
347+
cmd.Connection = cnn;
348+
using (SqlDataReader rdr = cmd.ExecuteReader())
349+
{
350+
Assert.True(rdr.Read(), "SqlDataReader must have a value");
351+
decimal retrunValue = rdr.GetDecimal(0);
352+
Assert.Equal(expectedDecimalValue, retrunValue.ToString());
353+
}
354+
}
355+
}
356+
}
357+
358+
[ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))]
359+
[InlineData("CAST(7.9999999999999999999999999999 as decimal(38, 35))")]
360+
[InlineData("CAST(8.1234567890123456789012345678 as decimal(38, 35))")]
361+
[InlineData("CAST(-8.1234567890123456789012345678 as decimal(38, 35))")]
362+
[InlineData("CAST(123456789012345678901234567890 as decimal(38, 0))")]
363+
[InlineData("CAST(7922816251426433759354395.9999 as decimal(38, 8))")]
364+
[InlineData("CAST(-7922816251426433759354395.9999 as decimal(38, 8))")]
365+
[InlineData("CAST(0.123456789012345678901234567890 as decimal(38, 36))")]
366+
public static void SqlDecimalConvertToDecimal_TestOutOfRange(string sqlDecimalValue)
367+
{
368+
using (SqlConnection cnn = new(s_connString))
369+
{
370+
cnn.Open();
371+
using (SqlCommand cmd = new($"SELECT {sqlDecimalValue} val"))
372+
{
373+
cmd.Connection = cnn;
374+
using (SqlDataReader rdr = cmd.ExecuteReader())
375+
{
376+
Assert.True(rdr.Read(), "SqlDataReader must have a value");
377+
Assert.Throws<OverflowException>(() => rdr.GetDecimal(0));
378+
}
379+
}
380+
}
381+
}
382+
322383
[Theory]
323384
[ClassData(typeof(ConnectionStringsProvider))]
324385
public static void TestScaledDecimalParameter_CommandInsert(string connectionString, bool truncateScaledDecimal)

0 commit comments

Comments
 (0)