diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs
index ed2c4aedf7..bc2a302464 100644
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs
@@ -19,12 +19,15 @@ private enum Tristate : byte
internal const string LegacyRowVersionNullString = @"Switch.Microsoft.Data.SqlClient.LegacyRowVersionNullBehavior";
internal const string SuppressInsecureTLSWarningString = @"Switch.Microsoft.Data.SqlClient.SuppressInsecureTLSWarning";
internal const string UseMinimumLoginTimeoutString = @"Switch.Microsoft.Data.SqlClient.UseOneSecFloorInTimeoutCalculationDuringLogin";
+ internal const string LegacyVarTimeZeroScaleBehaviourString = @"Switch.Microsoft.Data.SqlClient.LegacyVarTimeZeroScaleBehaviour";
// this field is accessed through reflection in tests and should not be renamed or have the type changed without refactoring NullRow related tests
private static Tristate s_legacyRowVersionNullBehavior;
private static Tristate s_suppressInsecureTLSWarning;
private static Tristate s_makeReadAsyncBlocking;
private static Tristate s_useMinimumLoginTimeout;
+ // 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
+ private static Tristate s_legacyVarTimeZeroScaleBehaviour;
#if !NETFRAMEWORK
static LocalAppContextSwitches()
@@ -176,5 +179,32 @@ public static bool UseMinimumLoginTimeout
return s_useMinimumLoginTimeout == Tristate.True;
}
}
+
+
+ ///
+ /// When set to 'true' this will output a scale value of 7 (DEFAULT_VARTIME_SCALE) when the scale
+ /// is explicitly set to zero for VarTime data types ('datetime2', 'datetimeoffset' and 'time')
+ /// If no scale is set explicitly it will continue to output scale of 7 (DEFAULT_VARTIME_SCALE)
+ /// regardsless of switch value.
+ /// This app context switch defaults to 'true'.
+ ///
+ public static bool LegacyVarTimeZeroScaleBehaviour
+ {
+ get
+ {
+ if (s_legacyVarTimeZeroScaleBehaviour == Tristate.NotInitialized)
+ {
+ if (!AppContext.TryGetSwitch(LegacyVarTimeZeroScaleBehaviourString, out bool returnedValue))
+ {
+ s_legacyVarTimeZeroScaleBehaviour = Tristate.True;
+ }
+ else
+ {
+ s_legacyVarTimeZeroScaleBehaviour = returnedValue ? Tristate.True : Tristate.False;
+ }
+ }
+ return s_legacyVarTimeZeroScaleBehaviour == Tristate.True;
+ }
+ }
}
}
diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlParameter.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlParameter.cs
index ee3a09c17a..8f0e54258d 100644
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlParameter.cs
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlParameter.cs
@@ -583,7 +583,17 @@ internal byte ScaleInternal
}
}
- private bool ShouldSerializeScale() => _scale != 0; // V1.0 compat, ignore _hasScale
+ private bool ShouldSerializeScale_Legacy() => _scale != 0; // V1.0 compat, ignore _hasScale
+
+ private bool ShouldSerializeScale()
+ {
+ if (LocalAppContextSwitches.LegacyVarTimeZeroScaleBehaviour)
+ {
+ return ShouldSerializeScale_Legacy();
+ }
+ return _scale != 0 || (GetMetaTypeOnly().IsVarTime && HasFlag(SqlParameterFlags.HasScale));
+ }
+
///
[
@@ -1509,7 +1519,6 @@ internal byte GetActualScale()
return ScaleInternal;
}
- // issue: how could a user specify 0 as the actual scale?
if (GetMetaTypeOnly().IsVarTime)
{
return TdsEnums.DEFAULT_VARTIME_SCALE;
diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlParameterTest.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlParameterTest.cs
index a5172b04bf..5b1a2d7687 100644
--- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlParameterTest.cs
+++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlParameterTest.cs
@@ -6,6 +6,7 @@
using System.Data;
using System.Data.Common;
using System.Data.SqlTypes;
+using System.Reflection;
using Xunit;
namespace Microsoft.Data.SqlClient.Tests
@@ -1851,5 +1852,112 @@ private enum Int64Enum : long
A = long.MinValue,
B = long.MaxValue
}
+
+
+ private static readonly object _parameterLegacyScaleLock = new();
+
+ [Theory]
+ [InlineData(null, 7, true)]
+ [InlineData(0, 7, true)]
+ [InlineData(1, 1, true)]
+ [InlineData(2, 2, true)]
+ [InlineData(3, 3, true)]
+ [InlineData(4, 4, true)]
+ [InlineData(5, 5, true)]
+ [InlineData(6, 6, true)]
+ [InlineData(7, 7, true)]
+ [InlineData(null, 7, false)]
+ [InlineData(0, 0, false)]
+ [InlineData(1, 1, false)]
+ [InlineData(2, 2, false)]
+ [InlineData(3, 3, false)]
+ [InlineData(4, 4, false)]
+ [InlineData(5, 5, false)]
+ [InlineData(6, 6, false)]
+ [InlineData(7, 7, false)]
+ [InlineData(null, 7, null)]
+ [InlineData(0, 7, null)]
+ [InlineData(1, 1, null)]
+ [InlineData(2, 2, null)]
+ [InlineData(3, 3, null)]
+ [InlineData(4, 4, null)]
+ [InlineData(5, 5, null)]
+ [InlineData(6, 6, null)]
+ [InlineData(7, 7, null)]
+ public void SqlDatetime2Scale_Legacy(int? setScale, byte outputScale, bool? legacyVarTimeZeroScaleSwitchValue)
+ {
+ lock (_parameterLegacyScaleLock)
+ {
+ var originalLegacyVarTimeZeroScaleSwitchValue = SetLegacyVarTimeZeroScaleBehaviour(legacyVarTimeZeroScaleSwitchValue);
+ try
+ {
+ var parameter = new SqlParameter
+ {
+ DbType = DbType.DateTime2
+ };
+ if (setScale.HasValue)
+ {
+ parameter.Scale = (byte)setScale.Value;
+ }
+
+ var actualScale = (byte)typeof(SqlParameter).GetMethod("GetActualScale", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(parameter, null);
+
+ Assert.Equal(outputScale, actualScale);
+ }
+
+ finally
+ {
+ SetLegacyVarTimeZeroScaleBehaviour(originalLegacyVarTimeZeroScaleSwitchValue);
+ }
+ }
+ }
+
+ [Fact]
+ public void SetLegacyVarTimeZeroScaleBehaviour_Defaults_to_True()
+ {
+ var legacyVarTimeZeroScaleBehaviour = (bool)LocalAppContextSwitchesType.GetProperty("LegacyVarTimeZeroScaleBehaviour", BindingFlags.Public | BindingFlags.Static).GetValue(null);
+
+ Assert.True(legacyVarTimeZeroScaleBehaviour);
+ }
+
+ private static Type LocalAppContextSwitchesType => typeof(SqlCommand).Assembly.GetType("Microsoft.Data.SqlClient.LocalAppContextSwitches");
+
+ private static bool? SetLegacyVarTimeZeroScaleBehaviour(bool? value)
+ {
+ const string LegacyVarTimeZeroScaleBehaviourSwitchname = @"Switch.Microsoft.Data.SqlClient.LegacyVarTimeZeroScaleBehaviour";
+
+ //reset internal state to "NotInitialized" so we pick up the value via AppContext
+ FieldInfo switchField = LocalAppContextSwitchesType.GetField("s_legacyVarTimeZeroScaleBehaviour", BindingFlags.NonPublic | BindingFlags.Static);
+ switchField.SetValue(null, (byte)0);
+
+ bool? returnValue = null;
+ if (AppContext.TryGetSwitch(LegacyVarTimeZeroScaleBehaviourSwitchname, out var originalValue))
+ {
+ returnValue = originalValue;
+ }
+
+ if (value.HasValue)
+ {
+ AppContext.SetSwitch(LegacyVarTimeZeroScaleBehaviourSwitchname, value.Value);
+ }
+ else
+ {
+ //need to remove the switch value via reflection as AppContext does not expose a means to do that.
+#if NET5_0_OR_GREATER
+ var switches = typeof(AppContext).GetField("s_switches", BindingFlags.NonPublic | BindingFlags.Static).GetValue(null);
+ if (switches is not null) //may be null if not initialised yet
+ {
+ MethodInfo removeMethod = switches.GetType().GetMethod("Remove", BindingFlags.Public | BindingFlags.Instance, new Type[] { typeof(string) });
+ removeMethod.Invoke(switches, new[] { LegacyVarTimeZeroScaleBehaviourSwitchname });
+ }
+#else
+ var switches = typeof(AppContext).GetField("s_switchMap", BindingFlags.NonPublic | BindingFlags.Static).GetValue(null);
+ MethodInfo removeMethod = switches.GetType().GetMethod("Remove", BindingFlags.Public | BindingFlags.Instance);
+ removeMethod.Invoke(switches, new[] { LegacyVarTimeZeroScaleBehaviourSwitchname });
+#endif
+ }
+
+ return returnValue;
+ }
}
}