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)