diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs index 22aa0360ec..b4a7eb54df 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs @@ -1396,9 +1396,6 @@ internal static ArgumentOutOfRangeException InvalidParameterDirection(ParameterD internal static ArgumentException InvalidSizeValue(int value) => Argument(StringsHelper.GetString(Strings.ADP_InvalidSizeValue, value.ToString(CultureInfo.InvariantCulture))); - internal static ArgumentException ParameterValueOutOfRange(decimal value) - => ADP.Argument(StringsHelper.GetString(Strings.ADP_ParameterValueOutOfRange, value.ToString((IFormatProvider)null))); - internal static ArgumentException ParameterValueOutOfRange(SqlDecimal value) => ADP.Argument(StringsHelper.GetString(Strings.ADP_ParameterValueOutOfRange, value.ToString())); internal static ArgumentException ParameterValueOutOfRange(string value) => ADP.Argument(StringsHelper.GetString(Strings.ADP_ParameterValueOutOfRange, value)); diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs index 1a42894779..8a4186e299 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -8260,7 +8260,7 @@ internal static SqlDecimal AdjustSqlDecimalScale(SqlDecimal d, int newScale) return d; } - internal static decimal AdjustDecimalScale(decimal value, int newScale) + internal static SqlDecimal AdjustDecimalScale(decimal value, int newScale) { #if NET Span decimalBits = stackalloc int[4]; @@ -8269,16 +8269,15 @@ internal static decimal AdjustDecimalScale(decimal value, int newScale) int[] decimalBits = decimal.GetBits(value); #endif int oldScale = (decimalBits[3] & 0x00ff0000) >> 0x10; + SqlDecimal num = new SqlDecimal(value); if (newScale != oldScale) { bool round = !LocalAppContextSwitches.TruncateScaledDecimal; - SqlDecimal num = new SqlDecimal(value); num = SqlDecimal.AdjustScale(num, newScale - oldScale, round); - return num.Value; } - return value; + return num; } internal byte[] SerializeSqlDecimal(SqlDecimal d, TdsParserStateObject stateObj) @@ -10411,34 +10410,31 @@ private Task TDSExecuteRPCAddParameter(TdsParserStateObject stateObj, SqlParamet // bug 49512, make sure the value matches the scale the user enters if (!isNull) { + SqlDecimal adjustedValue; + if (isSqlVal) { - value = AdjustSqlDecimalScale((SqlDecimal)value, scale); - - // If Precision is specified, verify value precision vs param precision - if (precision != 0) - { - if (precision < ((SqlDecimal)value).Precision) - { - throw ADP.ParameterValueOutOfRange((SqlDecimal)value); - } - } + adjustedValue = AdjustSqlDecimalScale((SqlDecimal)value, scale); } else { - value = AdjustDecimalScale((Decimal)value, scale); - - SqlDecimal sqlValue = new SqlDecimal((Decimal)value); + // If we encounter a System.Decimal at this point, we always convert it to + // a SqlDecimal. It's necessary in order to adjust the scale and to be able + // to transport the full range of numeric(38,X) values without overflowing. + adjustedValue = AdjustDecimalScale((decimal)value, scale); + isSqlVal = true; + } - // If Precision is specified, verify value precision vs param precision - if (precision != 0) + // If Precision is specified, verify value precision vs param precision + if (precision != 0) + { + if (precision < adjustedValue.Precision) { - if (precision < sqlValue.Precision) - { - throw ADP.ParameterValueOutOfRange((Decimal)value); - } + throw ADP.ParameterValueOutOfRange(adjustedValue); } } + + value = adjustedValue; } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs index 6f564250ce..c098d313aa 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. @@ -175,7 +175,7 @@ public void SqlParameterProperties(string connection) string inputProcedureName = DataTestUtility.GetShortName("InputProc").ToString(); string outputProcedureName = DataTestUtility.GetShortName("OutputProc").ToString(); const int charColumnSize = 100; - const int decimalColumnPrecision = 10; + const int decimalColumnPrecision = 38; const int decimalColumnScale = 4; const int timeColumnScale = 5; 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 d2cafd3c76..5e01d59265 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/ParametersTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/ParametersTest.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. @@ -532,6 +532,36 @@ public static void TestScaledDecimalParameter_CommandInsert(string connectionStr Assert.True(ValidateInsertedValues(connection, decimalTable.Name, truncateScaledDecimal), $"Invalid test happened with connection string [{connection.ConnectionString}]"); } + [Fact] + public static void TestOutOfRangeDecimalParameter_CommandInsert() + { + using SqlConnection connection = new(DataTestUtility.TCPConnectionString); + connection.Open(); + + using SqlCommand cmd = new("SELECT @Value", connection); + // A System.Decimal value has a maximum precision of 29 digits. We specify a Precision of 38 and a Scale of 2 in order + // to prove that the client can change the scale and precision of the decimal value in ways which System.Decimal doesn't + // intrinsically support. + SqlParameter p = new("@Value", decimal.MaxValue) + { + Precision = 38, + Scale = 2 + }; + + cmd.Parameters.Add(p); + + using SqlDataReader reader = cmd.ExecuteReader(); + + reader.Read(); + // Read the original value back as a SqlDecimal, with matching scale and precision. + SqlDecimal roundtrippedDecimal = reader.GetSqlDecimal(0); + + Assert.Equal(38, roundtrippedDecimal.Precision); + Assert.Equal(2, roundtrippedDecimal.Scale); + Assert.Throws(() => roundtrippedDecimal.Value); + Assert.Equal($"{decimal.MaxValue}.00", roundtrippedDecimal.ToString()); + } + // Enumeration is disabled to prevent generating empty test set when connection strings are not setup. [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] [MemberData(nameof(TestScaledDecimalParameter_Data), DisableDiscoveryEnumeration = true)]