Skip to content

Commit b556bb3

Browse files
Serialise scale for vartime SqlParameter types that have been explicitly set by the user to zero (#2411)
1 parent 55178ee commit b556bb3

File tree

3 files changed

+149
-2
lines changed

3 files changed

+149
-2
lines changed

src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,15 @@ private enum Tristate : byte
1919
internal const string LegacyRowVersionNullString = @"Switch.Microsoft.Data.SqlClient.LegacyRowVersionNullBehavior";
2020
internal const string SuppressInsecureTLSWarningString = @"Switch.Microsoft.Data.SqlClient.SuppressInsecureTLSWarning";
2121
internal const string UseMinimumLoginTimeoutString = @"Switch.Microsoft.Data.SqlClient.UseOneSecFloorInTimeoutCalculationDuringLogin";
22+
internal const string LegacyVarTimeZeroScaleBehaviourString = @"Switch.Microsoft.Data.SqlClient.LegacyVarTimeZeroScaleBehaviour";
2223

2324
// this field is accessed through reflection in tests and should not be renamed or have the type changed without refactoring NullRow related tests
2425
private static Tristate s_legacyRowVersionNullBehavior;
2526
private static Tristate s_suppressInsecureTLSWarning;
2627
private static Tristate s_makeReadAsyncBlocking;
2728
private static Tristate s_useMinimumLoginTimeout;
29+
// this field is accessed through reflection in Microsoft.Data.SqlClient.Tests.SqlParameterTests and should not be renamed or have the type changed without refactoring related tests
30+
private static Tristate s_legacyVarTimeZeroScaleBehaviour;
2831

2932
#if NET6_0_OR_GREATER
3033
static LocalAppContextSwitches()
@@ -176,5 +179,32 @@ public static bool UseMinimumLoginTimeout
176179
return s_useMinimumLoginTimeout == Tristate.True;
177180
}
178181
}
182+
183+
184+
/// <summary>
185+
/// When set to 'true' this will output a scale value of 7 (DEFAULT_VARTIME_SCALE) when the scale
186+
/// is explicitly set to zero for VarTime data types ('datetime2', 'datetimeoffset' and 'time')
187+
/// If no scale is set explicitly it will continue to output scale of 7 (DEFAULT_VARTIME_SCALE)
188+
/// regardsless of switch value.
189+
/// This app context switch defaults to 'true'.
190+
/// </summary>
191+
public static bool LegacyVarTimeZeroScaleBehaviour
192+
{
193+
get
194+
{
195+
if (s_legacyVarTimeZeroScaleBehaviour == Tristate.NotInitialized)
196+
{
197+
if (!AppContext.TryGetSwitch(LegacyVarTimeZeroScaleBehaviourString, out bool returnedValue))
198+
{
199+
s_legacyVarTimeZeroScaleBehaviour = Tristate.True;
200+
}
201+
else
202+
{
203+
s_legacyVarTimeZeroScaleBehaviour = returnedValue ? Tristate.True : Tristate.False;
204+
}
205+
}
206+
return s_legacyVarTimeZeroScaleBehaviour == Tristate.True;
207+
}
208+
}
179209
}
180210
}

src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlParameter.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -584,7 +584,17 @@ internal byte ScaleInternal
584584
}
585585
}
586586

587-
private bool ShouldSerializeScale() => _scale != 0; // V1.0 compat, ignore _hasScale
587+
private bool ShouldSerializeScale_Legacy() => _scale != 0; // V1.0 compat, ignore _hasScale
588+
589+
private bool ShouldSerializeScale()
590+
{
591+
if (LocalAppContextSwitches.LegacyVarTimeZeroScaleBehaviour)
592+
{
593+
return ShouldSerializeScale_Legacy();
594+
}
595+
return _scale != 0 || (GetMetaTypeOnly().IsVarTime && HasFlag(SqlParameterFlags.HasScale));
596+
}
597+
588598

589599
/// <include file='../../../../../../doc/snippets/Microsoft.Data.SqlClient/SqlParameter.xml' path='docs/members[@name="SqlParameter"]/SqlDbType/*' />
590600
[
@@ -1510,7 +1520,6 @@ internal byte GetActualScale()
15101520
return ScaleInternal;
15111521
}
15121522

1513-
// issue: how could a user specify 0 as the actual scale?
15141523
if (GetMetaTypeOnly().IsVarTime)
15151524
{
15161525
return TdsEnums.DEFAULT_VARTIME_SCALE;

src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlParameterTest.cs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Data;
77
using System.Data.Common;
88
using System.Data.SqlTypes;
9+
using System.Reflection;
910
using Xunit;
1011

1112
namespace Microsoft.Data.SqlClient.Tests
@@ -1851,5 +1852,112 @@ private enum Int64Enum : long
18511852
A = long.MinValue,
18521853
B = long.MaxValue
18531854
}
1855+
1856+
1857+
private static readonly object _parameterLegacyScaleLock = new();
1858+
1859+
[Theory]
1860+
[InlineData(null, 7, true)]
1861+
[InlineData(0, 7, true)]
1862+
[InlineData(1, 1, true)]
1863+
[InlineData(2, 2, true)]
1864+
[InlineData(3, 3, true)]
1865+
[InlineData(4, 4, true)]
1866+
[InlineData(5, 5, true)]
1867+
[InlineData(6, 6, true)]
1868+
[InlineData(7, 7, true)]
1869+
[InlineData(null, 7, false)]
1870+
[InlineData(0, 0, false)]
1871+
[InlineData(1, 1, false)]
1872+
[InlineData(2, 2, false)]
1873+
[InlineData(3, 3, false)]
1874+
[InlineData(4, 4, false)]
1875+
[InlineData(5, 5, false)]
1876+
[InlineData(6, 6, false)]
1877+
[InlineData(7, 7, false)]
1878+
[InlineData(null, 7, null)]
1879+
[InlineData(0, 7, null)]
1880+
[InlineData(1, 1, null)]
1881+
[InlineData(2, 2, null)]
1882+
[InlineData(3, 3, null)]
1883+
[InlineData(4, 4, null)]
1884+
[InlineData(5, 5, null)]
1885+
[InlineData(6, 6, null)]
1886+
[InlineData(7, 7, null)]
1887+
public void SqlDatetime2Scale_Legacy(int? setScale, byte outputScale, bool? legacyVarTimeZeroScaleSwitchValue)
1888+
{
1889+
lock (_parameterLegacyScaleLock)
1890+
{
1891+
var originalLegacyVarTimeZeroScaleSwitchValue = SetLegacyVarTimeZeroScaleBehaviour(legacyVarTimeZeroScaleSwitchValue);
1892+
try
1893+
{
1894+
var parameter = new SqlParameter
1895+
{
1896+
DbType = DbType.DateTime2
1897+
};
1898+
if (setScale.HasValue)
1899+
{
1900+
parameter.Scale = (byte)setScale.Value;
1901+
}
1902+
1903+
var actualScale = (byte)typeof(SqlParameter).GetMethod("GetActualScale", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(parameter, null);
1904+
1905+
Assert.Equal(outputScale, actualScale);
1906+
}
1907+
1908+
finally
1909+
{
1910+
SetLegacyVarTimeZeroScaleBehaviour(originalLegacyVarTimeZeroScaleSwitchValue);
1911+
}
1912+
}
1913+
}
1914+
1915+
[Fact]
1916+
public void SetLegacyVarTimeZeroScaleBehaviour_Defaults_to_True()
1917+
{
1918+
var legacyVarTimeZeroScaleBehaviour = (bool)LocalAppContextSwitchesType.GetProperty("LegacyVarTimeZeroScaleBehaviour", BindingFlags.Public | BindingFlags.Static).GetValue(null);
1919+
1920+
Assert.True(legacyVarTimeZeroScaleBehaviour);
1921+
}
1922+
1923+
private static Type LocalAppContextSwitchesType => typeof(SqlCommand).Assembly.GetType("Microsoft.Data.SqlClient.LocalAppContextSwitches");
1924+
1925+
private static bool? SetLegacyVarTimeZeroScaleBehaviour(bool? value)
1926+
{
1927+
const string LegacyVarTimeZeroScaleBehaviourSwitchname = @"Switch.Microsoft.Data.SqlClient.LegacyVarTimeZeroScaleBehaviour";
1928+
1929+
//reset internal state to "NotInitialized" so we pick up the value via AppContext
1930+
FieldInfo switchField = LocalAppContextSwitchesType.GetField("s_legacyVarTimeZeroScaleBehaviour", BindingFlags.NonPublic | BindingFlags.Static);
1931+
switchField.SetValue(null, (byte)0);
1932+
1933+
bool? returnValue = null;
1934+
if (AppContext.TryGetSwitch(LegacyVarTimeZeroScaleBehaviourSwitchname, out var originalValue))
1935+
{
1936+
returnValue = originalValue;
1937+
}
1938+
1939+
if (value.HasValue)
1940+
{
1941+
AppContext.SetSwitch(LegacyVarTimeZeroScaleBehaviourSwitchname, value.Value);
1942+
}
1943+
else
1944+
{
1945+
//need to remove the switch value via reflection as AppContext does not expose a means to do that.
1946+
#if NET5_0_OR_GREATER
1947+
var switches = typeof(AppContext).GetField("s_switches", BindingFlags.NonPublic | BindingFlags.Static).GetValue(null);
1948+
if (switches is not null) //may be null if not initialised yet
1949+
{
1950+
MethodInfo removeMethod = switches.GetType().GetMethod("Remove", BindingFlags.Public | BindingFlags.Instance, new Type[] { typeof(string) });
1951+
removeMethod.Invoke(switches, new[] { LegacyVarTimeZeroScaleBehaviourSwitchname });
1952+
}
1953+
#else
1954+
var switches = typeof(AppContext).GetField("s_switchMap", BindingFlags.NonPublic | BindingFlags.Static).GetValue(null);
1955+
MethodInfo removeMethod = switches.GetType().GetMethod("Remove", BindingFlags.Public | BindingFlags.Instance);
1956+
removeMethod.Invoke(switches, new[] { LegacyVarTimeZeroScaleBehaviourSwitchname });
1957+
#endif
1958+
}
1959+
1960+
return returnValue;
1961+
}
18541962
}
18551963
}

0 commit comments

Comments
 (0)