From 2d8207e680c2debc18f451dfc8bbd25b5b54dbe8 Mon Sep 17 00:00:00 2001
From: DavoudEshtehari <61173489+DavoudEshtehari@users.noreply.github.com>
Date: Thu, 16 Sep 2021 10:36:26 -0700
Subject: [PATCH] Fix | Improve decimal conversion from SqlDecimal to .Net
Decimal (#1179)
---
.../src/Microsoft/Data/SqlClient/SqlBuffer.cs | 117 ++++++++++++++++++
.../src/Microsoft/Data/SqlClient/SqlBuffer.cs | 117 ++++++++++++++++++
.../SQL/ParameterTest/ParametersTest.cs | 61 +++++++++
3 files changed, 295 insertions(+)
diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlBuffer.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlBuffer.cs
index 1099c974ba..725a59fb8d 100644
--- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlBuffer.cs
+++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlBuffer.cs
@@ -205,6 +205,7 @@ internal DateTime DateTime
}
}
+ #region Decimal
internal decimal Decimal
{
get
@@ -215,6 +216,43 @@ internal decimal Decimal
{
if (_value._numericInfo._data4 != 0 || _value._numericInfo._scale > 28)
{
+ // Only removing trailing zeros from a decimal part won't hit its value!
+ if (_value._numericInfo._scale > 0)
+ {
+ int zeroCnt = FindTrailingZerosAndPrec((uint)_value._numericInfo._data1, (uint)_value._numericInfo._data2,
+ (uint)_value._numericInfo._data3, (uint)_value._numericInfo._data4,
+ _value._numericInfo._scale, out int precision);
+
+ int minScale = _value._numericInfo._scale - zeroCnt; // minimum possible sacle after removing the trailing zeros.
+
+ if (zeroCnt > 0 && minScale <= 28 && precision <= 29)
+ {
+ SqlDecimal sqlValue = new(_value._numericInfo._precision, _value._numericInfo._scale, _value._numericInfo._positive,
+ _value._numericInfo._data1, _value._numericInfo._data2,
+ _value._numericInfo._data3, _value._numericInfo._data4);
+
+ int integral = precision - minScale;
+ int newPrec = 29;
+
+ if (integral != 1 && precision != 29)
+ {
+ newPrec = 28;
+ }
+
+ try
+ {
+ // Precision could be 28 or 29
+ // ex: (precision == 29 && scale == 28)
+ // valid: (+/-)7.1234567890123456789012345678
+ // invalid: (+/-)8.1234567890123456789012345678
+ return SqlDecimal.ConvertToPrecScale(sqlValue, newPrec, newPrec - integral).Value;
+ }
+ catch (OverflowException)
+ {
+ throw new OverflowException(SQLResource.ConversionOverflowMessage);
+ }
+ }
+ }
throw new OverflowException(SQLResource.ConversionOverflowMessage);
}
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
}
}
+ ///
+ /// Returns number of trailing zeros using the supplied parameters.
+ ///
+ /// An 32-bit unsigned integer which will be combined with data2, data3, and data4
+ /// An 32-bit unsigned integer which will be combined with data1, data3, and data4
+ /// An 32-bit unsigned integer which will be combined with data1, data2, and data4
+ /// An 32-bit unsigned integer which will be combined with data1, data2, and data3
+ /// The number of decimal places
+ /// OUT |The number of digits without trailing zeros
+ /// Number of trailing zeros
+ private static int FindTrailingZerosAndPrec(uint data1, uint data2, uint data3, uint data4, byte scale, out int valuablePrecision)
+ {
+ // Make local copy of data to avoid modifying input.
+ Span rgulNumeric = stackalloc uint[4] { data1, data2, data3, data4 };
+ int zeroCnt = 0; //Number of trailing zero digits
+ int precCnt = 0; //Valuable precision
+ uint uiRem = 0; //Remainder of a division by 10
+ int len = 4; // Max possible items
+
+ //Retrieve each digit from the lowest significant digit
+ while (len > 1 || rgulNumeric[0] != 0)
+ {
+ SqlDecimalDivBy(rgulNumeric, ref len, 10, out uiRem);
+ if (uiRem == 0 && precCnt == 0)
+ {
+ zeroCnt++;
+ }
+ else
+ {
+ precCnt++;
+ }
+ }
+
+ if (uiRem == 0)
+ {
+ zeroCnt = scale;
+ }
+
+ // if scale of the number has not been reached, pad remaining number with zeros.
+ if (zeroCnt + precCnt <= scale)
+ {
+ precCnt = scale - zeroCnt + 1;
+ }
+ valuablePrecision = precCnt;
+ return zeroCnt;
+ }
+
+ ///
+ /// Multi-precision one super-digit divide in place.
+ /// U = U / D,
+ /// R = U % D
+ /// (Length of U can decrease)
+ ///
+ /// InOut | U
+ /// InOut | Number of items with non-zero value in U between 1 to 4
+ /// In | D
+ /// Out | R
+ private static void SqlDecimalDivBy(Span data, ref int len, uint divisor, out uint remainder)
+ {
+ uint uiCarry = 0;
+ ulong ulAccum;
+ ulong ulDivisor = (ulong)divisor;
+ int iLen = len;
+
+ while (iLen > 0)
+ {
+ iLen--;
+ ulAccum = (((ulong)uiCarry) << 32) + (ulong)(data[iLen]);
+ data[iLen] = (uint)(ulAccum / ulDivisor);
+ uiCarry = (uint)(ulAccum - (ulong)data[iLen] * ulDivisor); // (ULONG) (ulAccum % divisor)
+ }
+ remainder = uiCarry;
+
+ // Normalize multi-precision number - remove leading zeroes
+ while (len > 1 && data[len - 1] == 0)
+ { len--; }
+ }
+ #endregion
+
internal double Double
{
get
diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlBuffer.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlBuffer.cs
index 66071812dc..36c0df16d3 100644
--- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlBuffer.cs
+++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlBuffer.cs
@@ -202,6 +202,7 @@ internal DateTime DateTime
}
}
+ #region Decimal
internal decimal Decimal
{
get
@@ -212,6 +213,43 @@ internal decimal Decimal
{
if (_value._numericInfo._data4 != 0 || _value._numericInfo._scale > 28)
{
+ // Only removing trailing zeros from a decimal part won't hit its value!
+ if (_value._numericInfo._scale > 0)
+ {
+ int zeroCnt = FindTrailingZerosAndPrec((uint)_value._numericInfo._data1, (uint)_value._numericInfo._data2,
+ (uint)_value._numericInfo._data3, (uint)_value._numericInfo._data4,
+ _value._numericInfo._scale, out int precision);
+
+ int minScale = _value._numericInfo._scale - zeroCnt; // minimum possible sacle after removing the trailing zeros.
+
+ if (zeroCnt > 0 && minScale <= 28 && precision <= 29)
+ {
+ SqlDecimal sqlValue = new(_value._numericInfo._precision, _value._numericInfo._scale, _value._numericInfo._positive,
+ _value._numericInfo._data1, _value._numericInfo._data2,
+ _value._numericInfo._data3, _value._numericInfo._data4);
+
+ int integral = precision - minScale;
+ int newPrec = 29;
+
+ if (integral != 1 && precision != 29)
+ {
+ newPrec = 28;
+ }
+
+ try
+ {
+ // Precision could be 28 or 29
+ // ex: (precision == 29 && scale == 28)
+ // valid: (+/-)7.1234567890123456789012345678
+ // invalid: (+/-)8.1234567890123456789012345678
+ return SqlDecimal.ConvertToPrecScale(sqlValue, newPrec, newPrec - integral).Value;
+ }
+ catch (OverflowException)
+ {
+ throw new OverflowException(SQLResource.ConversionOverflowMessage);
+ }
+ }
+ }
throw new OverflowException(SQLResource.ConversionOverflowMessage);
}
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
}
}
+ ///
+ /// Returns number of trailing zeros using the supplied parameters.
+ ///
+ /// An 32-bit unsigned integer which will be combined with data2, data3, and data4
+ /// An 32-bit unsigned integer which will be combined with data1, data3, and data4
+ /// An 32-bit unsigned integer which will be combined with data1, data2, and data4
+ /// An 32-bit unsigned integer which will be combined with data1, data2, and data3
+ /// The number of decimal places
+ /// OUT |The number of digits without trailing zeros
+ /// Number of trailing zeros
+ private static int FindTrailingZerosAndPrec(uint data1, uint data2, uint data3, uint data4, byte scale, out int valuablePrecision)
+ {
+ // Make local copy of data to avoid modifying input.
+ Span rgulNumeric = stackalloc uint[4] { data1, data2, data3, data4 };
+ int zeroCnt = 0; //Number of trailing zero digits
+ int precCnt = 0; //Valuable precision
+ uint uiRem = 0; //Remainder of a division by 10
+ int len = 4; // Max possible items
+
+ //Retrieve each digit from the lowest significant digit
+ while (len > 1 || rgulNumeric[0] != 0)
+ {
+ SqlDecimalDivBy(rgulNumeric, ref len, 10, out uiRem);
+ if (uiRem == 0 && precCnt == 0)
+ {
+ zeroCnt++;
+ }
+ else
+ {
+ precCnt++;
+ }
+ }
+
+ if (uiRem == 0)
+ {
+ zeroCnt = scale;
+ }
+
+ // if scale of the number has not been reached, pad remaining number with zeros.
+ if (zeroCnt + precCnt <= scale)
+ {
+ precCnt = scale - zeroCnt + 1;
+ }
+ valuablePrecision = precCnt;
+ return zeroCnt;
+ }
+
+ ///
+ /// Multi-precision one super-digit divide in place.
+ /// U = U / D,
+ /// R = U % D
+ /// (Length of U can decrease)
+ ///
+ /// InOut | U
+ /// InOut | Number of items with non-zero value in U between 1 to 4
+ /// In | D
+ /// Out | R
+ private static void SqlDecimalDivBy(Span data, ref int len, uint divisor, out uint remainder)
+ {
+ uint uiCarry = 0;
+ ulong ulAccum;
+ ulong ulDivisor = (ulong)divisor;
+ int iLen = len;
+
+ while (iLen > 0)
+ {
+ iLen--;
+ ulAccum = (((ulong)uiCarry) << 32) + (ulong)(data[iLen]);
+ data[iLen] = (uint)(ulAccum / ulDivisor);
+ uiCarry = (uint)(ulAccum - (ulong)data[iLen] * ulDivisor); // (ULONG) (ulAccum % divisor)
+ }
+ remainder = uiCarry;
+
+ // Normalize multi-precision number - remove leading zeroes
+ while (len > 1 && data[len - 1] == 0)
+ { len--; }
+ }
+ #endregion
+
internal double Double
{
get
diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/ParametersTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/ParametersTest.cs
index 3c55b079e5..3b176ef921 100644
--- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/ParametersTest.cs
+++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/ParametersTest.cs
@@ -319,6 +319,67 @@ public static void TestParametersWithDatatablesTVPInsert()
}
#region Scaled Decimal Parameter & TVP Test
+ [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))]
+ [InlineData("CAST(1.0 as decimal(38, 37))", "1.0000000000000000000000000000")]
+ [InlineData("CAST(7.1234567890123456789012345678 as decimal(38, 35))", "7.1234567890123456789012345678")]
+ [InlineData("CAST(-7.1234567890123456789012345678 as decimal(38, 35))", "-7.1234567890123456789012345678")]
+ [InlineData("CAST(-0.1234567890123456789012345678 as decimal(38, 35))", "-0.1234567890123456789012345678")]
+ [InlineData("CAST(4210862852.86 as decimal(38, 20))", "4210862852.860000000000000000")]
+ [InlineData("CAST(0 as decimal(38, 36))", "0.0000000000000000000000000000")]
+ [InlineData("CAST(79228162514264337593543950335 as decimal(38, 9))", "79228162514264337593543950335")]
+ [InlineData("CAST(-79228162514264337593543950335 as decimal(38, 9))", "-79228162514264337593543950335")]
+ [InlineData("CAST(0.4210862852 as decimal(38, 38))", "0.4210862852000000000000000000")]
+ [InlineData("CAST(0.1234567890123456789012345678 as decimal(38, 38))", "0.1234567890123456789012345678")]
+ [InlineData("CAST(249454727.14678312032280248320 as decimal(38, 20))", "249454727.14678312032280248320")]
+ [InlineData("CAST(3961408124790879675.7769715711 as decimal(38, 10))", "3961408124790879675.7769715711")]
+ [InlineData("CAST(3961408124790879675776971571.1 as decimal(38, 1))", "3961408124790879675776971571.1")]
+ [InlineData("CAST(79228162514264337593543950335 as decimal(38, 0))", "79228162514264337593543950335")]
+ [InlineData("CAST(-79228162514264337593543950335 as decimal(38, 0))", "-79228162514264337593543950335")]
+ [InlineData("CAST(0.0000000000000000000000000001 as decimal(38, 38))", "0.0000000000000000000000000001")]
+ [InlineData("CAST(-0.0000000000000000000000000001 as decimal(38, 38))", "-0.0000000000000000000000000001")]
+ public static void SqlDecimalConvertToDecimal_TestInRange(string sqlDecimalValue, string expectedDecimalValue)
+ {
+ using(SqlConnection cnn = new(s_connString))
+ {
+ cnn.Open();
+ using(SqlCommand cmd = new($"SELECT {sqlDecimalValue} val"))
+ {
+ cmd.Connection = cnn;
+ using (SqlDataReader rdr = cmd.ExecuteReader())
+ {
+ Assert.True(rdr.Read(), "SqlDataReader must have a value");
+ decimal retrunValue = rdr.GetDecimal(0);
+ Assert.Equal(expectedDecimalValue, retrunValue.ToString());
+ }
+ }
+ }
+ }
+
+ [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))]
+ [InlineData("CAST(7.9999999999999999999999999999 as decimal(38, 35))")]
+ [InlineData("CAST(8.1234567890123456789012345678 as decimal(38, 35))")]
+ [InlineData("CAST(-8.1234567890123456789012345678 as decimal(38, 35))")]
+ [InlineData("CAST(123456789012345678901234567890 as decimal(38, 0))")]
+ [InlineData("CAST(7922816251426433759354395.9999 as decimal(38, 8))")]
+ [InlineData("CAST(-7922816251426433759354395.9999 as decimal(38, 8))")]
+ [InlineData("CAST(0.123456789012345678901234567890 as decimal(38, 36))")]
+ public static void SqlDecimalConvertToDecimal_TestOutOfRange(string sqlDecimalValue)
+ {
+ using (SqlConnection cnn = new(s_connString))
+ {
+ cnn.Open();
+ using (SqlCommand cmd = new($"SELECT {sqlDecimalValue} val"))
+ {
+ cmd.Connection = cnn;
+ using (SqlDataReader rdr = cmd.ExecuteReader())
+ {
+ Assert.True(rdr.Read(), "SqlDataReader must have a value");
+ Assert.Throws(() => rdr.GetDecimal(0));
+ }
+ }
+ }
+ }
+
[Theory]
[ClassData(typeof(ConnectionStringsProvider))]
public static void TestScaledDecimalParameter_CommandInsert(string connectionString, bool truncateScaledDecimal)