From 15d2f8457a2cebb23feaa55af403123dc2bcd79b Mon Sep 17 00:00:00 2001 From: bjornbouetsmith Date: Thu, 14 Jan 2021 12:40:57 +0100 Subject: [PATCH 1/2] Implementation of DateTime2 + accompanying GetSqlDateTime2 method (#846) --- .../SqlDataReader.xml | 19 +++ .../Microsoft.Data.SqlTypes/SqlDateTime2.xml | 72 ++++++++++ src/Microsoft.Data.SqlClient.sln | 1 + .../src/Microsoft/Data/Common/AdapterUtil.cs | 5 + .../src/Microsoft.Data.SqlClient.csproj | 1 + .../src/Microsoft/Data/SqlClient/SqlBuffer.cs | 34 +++++ .../Microsoft/Data/SqlClient/SqlDataReader.cs | 7 + .../netfx/src/Microsoft.Data.SqlClient.csproj | 25 +++- .../src/Microsoft/Data/SqlClient/SqlBuffer.cs | 34 +++++ .../Microsoft/Data/SqlClient/SqlDataReader.cs | 7 + .../Microsoft/Data/SqlTypes/SqlDateTime2.cs | 134 ++++++++++++++++++ .../Microsoft.Data.SqlClient.Tests.csproj | 1 + .../FunctionalTests/SerializeSqlTypesTest.cs | 19 ++- .../tests/FunctionalTests/SqlDateTime2Test.cs | 79 +++++++++++ ....Data.SqlClient.ManualTesting.Tests.csproj | 1 + .../SQL/DateTime2Test/DateTime2Test.cs | 94 ++++++++++++ 16 files changed, 526 insertions(+), 7 deletions(-) create mode 100644 doc/snippets/Microsoft.Data.SqlTypes/SqlDateTime2.xml create mode 100644 src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlDateTime2.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlDateTime2Test.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DateTime2Test/DateTime2Test.cs diff --git a/doc/snippets/Microsoft.Data.SqlClient/SqlDataReader.xml b/doc/snippets/Microsoft.Data.SqlClient/SqlDataReader.xml index f4459bdda9..fe8afbdfad 100644 --- a/doc/snippets/Microsoft.Data.SqlClient/SqlDataReader.xml +++ b/doc/snippets/Microsoft.Data.SqlClient/SqlDataReader.xml @@ -634,6 +634,25 @@ ]]> + + The zero-based column ordinal. + + Gets the value of the specified column as a . + + + The value of the column expressed as a . + + + + + + + The zero-based column ordinal. Gets the value of the specified column as a . diff --git a/doc/snippets/Microsoft.Data.SqlTypes/SqlDateTime2.xml b/doc/snippets/Microsoft.Data.SqlTypes/SqlDateTime2.xml new file mode 100644 index 0000000000..d5721cd9e6 --- /dev/null +++ b/doc/snippets/Microsoft.Data.SqlTypes/SqlDateTime2.xml @@ -0,0 +1,72 @@ + + + + Exposes SQL Server data that is stored as a DATETIME2. + + + class is used to work with `DATETIME2` data. +`DATETIME2`has a wider range than DATETIME and thus needs its own type. + +]]> + + + + + Whether or not this instance should be considered null - true means null, false not null + + Initializes a new instance of the struct. + Will initialize the datetime value to 0. + + + + Number of ticks to initialize this instance with - in the range of & + + + Initializes a new instance of the class. + + + is not in the range of & + + + + + Gets the representation of this instance. + + + If this instance is null + + + + + Gets an instance representing the value NULL from the database. + + + + + Converts a DateTime into a SqlDateTime2 + + + + + Converts a DBNull instance into a Null SqlDateTime2 + + + + + Converts a SqlDateTime2 into DateTime + + + If the SqlDateTime2 instance has a null value + + + + + returns a for serialization purposes + + unused parameter + + + diff --git a/src/Microsoft.Data.SqlClient.sln b/src/Microsoft.Data.SqlClient.sln index 855baeedaa..3de749056c 100644 --- a/src/Microsoft.Data.SqlClient.sln +++ b/src/Microsoft.Data.SqlClient.sln @@ -159,6 +159,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Microsoft.Data.SqlClient.Se EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Microsoft.Data.SqlTypes", "Microsoft.Data.SqlTypes", "{5A7600BD-AED8-44AB-8F2A-7CB33A8D9C02}" ProjectSection(SolutionItems) = preProject + ..\doc\snippets\Microsoft.Data.SqlTypes\SqlDateTime2.xml = ..\doc\snippets\Microsoft.Data.SqlTypes\SqlDateTime2.xml ..\doc\snippets\Microsoft.Data.SqlTypes\SqlFileStream.xml = ..\doc\snippets\Microsoft.Data.SqlTypes\SqlFileStream.xml EndProjectSection EndProject diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Common/src/Microsoft/Data/Common/AdapterUtil.cs b/src/Microsoft.Data.SqlClient/netcore/src/Common/src/Microsoft/Data/Common/AdapterUtil.cs index 396d43caa6..6cbea1e8c1 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Common/src/Microsoft/Data/Common/AdapterUtil.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Common/src/Microsoft/Data/Common/AdapterUtil.cs @@ -527,5 +527,10 @@ internal static void SetCurrentTransaction(Transaction transaction) { Transaction.Current = transaction; } + + static internal Exception WrongType(Type got, Type expected) + { + return Argument(StringsHelper.GetString(Strings.SQL_WrongType, got.ToString(), expected.ToString())); + } } } diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj index 3e7db7d945..6b05bb4ab4 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj @@ -464,6 +464,7 @@ + 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..98ac4f1d89 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 @@ -615,6 +615,40 @@ internal SqlDateTime SqlDateTime } } + internal SqlDateTime2 SqlDateTime2 + { + get + { + if (IsNull) + { + return SqlDateTime2.Null; + } + + if (StorageType.DateTime2 == _type) + { + return new SqlDateTime2(GetTicksFromDateTime2Info(_value._dateTime2Info)); + } + + if (StorageType.DateTime == _type) + { + // also handle DATETIME without boxing to object first + var dateTime = SqlTypeWorkarounds.SqlDateTimeToDateTime(_value._dateTimeInfo._daypart, _value._dateTimeInfo._timepart); + + return new SqlDateTime2(dateTime.Ticks); + } + + if (StorageType.Date == _type) + { + return (SqlDateTime2)DateTime.MinValue.AddDays(_value._int32); + } + + // cannot use SqlValue, since that causes invalid cast exception since object cannot be dynamic cast to SqlDateTime2 - only explicit cast + // (SqlDateTime2)(object)dateTimeValue; + // So assume its called on some kind of DATE type, so DateTime property can handle it + return (SqlDateTime2)DateTime; + } + } + internal SqlDecimal SqlDecimal { get diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlDataReader.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlDataReader.cs index 83fa69a74f..ed89108565 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlDataReader.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlDataReader.cs @@ -2397,6 +2397,13 @@ virtual public SqlDateTime GetSqlDateTime(int i) return _data[i].SqlDateTime; } + /// + virtual public SqlDateTime2 GetSqlDateTime2(int i) + { + ReadColumn(i); + return _data[i].SqlDateTime2; + } + /// virtual public SqlDecimal GetSqlDecimal(int i) { diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj index cf8c458f2e..04c999129f 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj @@ -103,6 +103,9 @@ Microsoft\Data\Common\ActivityCorrelator.cs + + Microsoft\Data\SqlTypes\SqlDateTime2.cs + Microsoft\Data\Common\DbConnectionPoolKey.cs @@ -368,10 +371,16 @@ - - + + Component + + + Component + - + + Component + @@ -379,7 +388,9 @@ - + + Component + @@ -443,7 +454,9 @@ - + + Component + @@ -545,4 +558,4 @@ - + \ No newline at end of file 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..53198b88d7 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 @@ -597,6 +597,40 @@ internal SqlDateTime SqlDateTime } } + internal SqlDateTime2 SqlDateTime2 + { + get + { + if (IsNull) + { + return SqlDateTime2.Null; + } + + if (StorageType.DateTime2 == _type) + { + return new SqlDateTime2(GetTicksFromDateTime2Info(_value._dateTime2Info)); + } + + if (StorageType.DateTime == _type) + { + // also handle DATETIME without boxing to object first + var dateTime = SqlTypeWorkarounds.SqlDateTimeToDateTime(_value._dateTimeInfo._daypart, _value._dateTimeInfo._timepart); + + return new SqlDateTime2(dateTime.Ticks); + } + + if (StorageType.Date == _type) + { + return (SqlDateTime2)DateTime.MinValue.AddDays(_value._int32); + } + + // cannot use SqlValue, since that causes invalid cast exception since object cannot be dynamic cast to SqlDateTime2 - only explicit cast + // (SqlDateTime2)(object)dateTimeValue; + // So assume its called on some kind of DATE type, so DateTime property can handle it + return (SqlDateTime2)DateTime; + } + } + internal SqlDecimal SqlDecimal { get diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlDataReader.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlDataReader.cs index 176b3ddf3c..445a7020cd 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlDataReader.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlDataReader.cs @@ -2777,6 +2777,13 @@ virtual public SqlDateTime GetSqlDateTime(int i) return _data[i].SqlDateTime; } + /// + virtual public SqlDateTime2 GetSqlDateTime2(int i) + { + ReadColumn(i); + return _data[i].SqlDateTime2; + } + /// virtual public SqlDecimal GetSqlDecimal(int i) { diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlDateTime2.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlDateTime2.cs new file mode 100644 index 0000000000..6704d3a3c3 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlDateTime2.cs @@ -0,0 +1,134 @@ +using System; +using System.Data.SqlTypes; +using System.Runtime.InteropServices; +using System.Xml; +using System.Xml.Schema; +using System.Xml.Serialization; +using Microsoft.Data.Common; + +namespace Microsoft.Data.SqlTypes +{ + /// + [XmlSchemaProvider("GetXsdType")] + [Serializable] + [StructLayout(LayoutKind.Sequential)] + public struct SqlDateTime2 : INullable, IComparable, IEquatable, IComparable, IXmlSerializable + { + private const long _minTicks = 0; // DateTime.MinValue.Ticks + private const long _maxTicks = 3155378975999999999; // DateTime.MaxValue.Ticks + + private bool _notNull; // false if null - has to be _notNull and not _isNull - otherwise default(SqlDateTime2) will return a SqlDateTime2 with _isNull=false and that would make XmlSerialization fail + private long _ticks; // Ticks representation similar to DateTime.Tics + + /// + private SqlDateTime2(bool isNull) + { + _notNull = !isNull; + _ticks = 0; + } + + /// + public SqlDateTime2(long ticks) + { + if (ticks < _minTicks || ticks > _maxTicks) + { + throw ADP.ArgumentOutOfRange(nameof(ticks)); + } + _notNull = true; + _ticks = ticks; + } + + /// + public bool IsNull => !_notNull; + + /// + public DateTime Value => _notNull ? new DateTime(_ticks) : throw new SqlNullValueException(); + + /// + public static readonly SqlDateTime2 Null = new SqlDateTime2(true); + + /// + public bool Equals(SqlDateTime2 other) + { + return _notNull == other._notNull && _ticks == other._ticks; + } + + /// + public int CompareTo(SqlDateTime2 other) + { + var result = _notNull.CompareTo(other._notNull); + + if (result != 0) + { + return result; + } + + return _ticks.CompareTo(other._ticks); + } + + /// + public int CompareTo(object obj) + { + if (obj is SqlDateTime2 sqlDateTime2) + { + return CompareTo(sqlDateTime2); + } + + throw ADP.WrongType(obj.GetType(), typeof(SqlDateTime2)); + } + + /// + public static explicit operator SqlDateTime2(DateTime dateTime) + { + return new SqlDateTime2(dateTime.Ticks); + } + + /// + public static explicit operator SqlDateTime2(DBNull _) + { + return Null; + } + /// + public static explicit operator DateTime(SqlDateTime2 sqlDateTime2) + { + return sqlDateTime2.Value; + } + + /// + public static XmlQualifiedName GetXsdType(XmlSchemaSet schemaSet) + { + return new XmlQualifiedName("dateTime2", "http://www.w3.org/2001/XMLSchema"); + } + + XmlSchema IXmlSerializable.GetSchema() + { + return null; + } + + void IXmlSerializable.ReadXml(XmlReader reader) + { + string attribute = reader.GetAttribute("nil", "http://www.w3.org/2001/XMLSchema-instance"); + if (attribute != null && XmlConvert.ToBoolean(attribute)) + { + reader.ReadElementString(); + _notNull = false; + } + else + { + DateTime dateTime = XmlConvert.ToDateTime(reader.ReadElementString(), XmlDateTimeSerializationMode.RoundtripKind); + if (dateTime.Kind != DateTimeKind.Unspecified) + throw new SqlTypeException(SQLResource.TimeZoneSpecifiedMessage); + _ticks = dateTime.Ticks; + _notNull = true; + } + } + + void IXmlSerializable.WriteXml(XmlWriter writer) + { + if (IsNull) + writer.WriteAttributeString("xsi", "nil", "http://www.w3.org/2001/XMLSchema-instance", "true"); + else + writer.WriteString(XmlConvert.ToString(Value, "yyyy-MM-ddTHH:mm:ss.fffffff")); + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.Tests.csproj b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.Tests.csproj index c41a99cc4d..c6cb94b7fb 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.Tests.csproj +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.Tests.csproj @@ -42,6 +42,7 @@ + diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SerializeSqlTypesTest.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SerializeSqlTypesTest.cs index eee79bdc63..f57c05033a 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SerializeSqlTypesTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SerializeSqlTypesTest.cs @@ -9,6 +9,7 @@ using Xunit; using System.IO; using System.Runtime.Serialization; +using Microsoft.Data.SqlTypes; namespace Microsoft.Data.SqlClient.Tests { @@ -27,7 +28,14 @@ private T SerializeAndDeserialize(T value, string expectedSerializedValue) Assert.Equal(expectedSerializedValue, serializedValue); memoryStream.Position = 0; - T deserializedValue = (T)serializer.ReadObject(memoryStream); + var obj = serializer.ReadObject(memoryStream); + if (obj == null) + { + // Handle structs that serialize to xsi:nil, which returns null from ReadObject + return default; + } + + T deserializedValue = (T)obj; return deserializedValue; } } @@ -80,6 +88,15 @@ public void SerializeAndDeserializeSqlTypes() SqlString sqlStringDeserialized = SerializeAndDeserialize(sqlString, "abcdefghijklmnopqrstuvwxyz"); // Cannot use StrictEqual because information such as LCID is lost when the SqlString is serialized Assert.Equal(sqlString.Value, sqlStringDeserialized.Value); + + SqlDateTime2 nullDateTime = SqlDateTime2.Null; + Assert.StrictEqual(nullDateTime, SerializeAndDeserialize(nullDateTime, @"")); + + SqlDateTime2 minSqlDateTime = new SqlDateTime2(0); + Assert.StrictEqual(minSqlDateTime, SerializeAndDeserialize(minSqlDateTime, @"0001-01-01T00:00:00.0000000")); + + SqlDateTime2 maxSqlDateTime = new SqlDateTime2(3155378975999999999); + Assert.StrictEqual(maxSqlDateTime, SerializeAndDeserialize(maxSqlDateTime, @"9999-12-31T23:59:59.9999999")); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlDateTime2Test.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlDateTime2Test.cs new file mode 100644 index 0000000000..7d2f5cbfe4 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlDateTime2Test.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Data.SqlTypes; +using Xunit; + +namespace Microsoft.Data.SqlClient.Tests +{ + public class SqlDateTime2Test + { + [Fact] + public void ConvertFromDBNull() + { + var value = DBNull.Value; + + SqlDateTime2 sqlDateTime2 = (SqlDateTime2)value; + Assert.Equal(SqlDateTime2.Null, sqlDateTime2); + } + + [Fact] + public void ConvertFromDateTime() + { + var value = new DateTime(2020, 12, 17, 14, 06, 10, 123, DateTimeKind.Utc); + + SqlDateTime2 expected = new SqlDateTime2(value.Ticks); + SqlDateTime2 sqlDateTime2 = (SqlDateTime2)value; + + Assert.Equal(expected, sqlDateTime2); + } + + [Fact] + public void ConvertToDateTime() + { + var value = new DateTime(2020, 12, 17, 14, 06, 10, 123, DateTimeKind.Utc); + + SqlDateTime2 expected = new SqlDateTime2(value.Ticks); + DateTime converted = (DateTime)expected; + + Assert.Equal(value, converted); + } + + [Fact] + public void TwoSameShouldBeEquals() + { + Assert.Equal(SqlDateTime2.Null, SqlDateTime2.Null); + } + + [Fact] + public void NullShouldBeLessThanSomething() + { + var result = SqlDateTime2.Null.CompareTo(new SqlDateTime2(1)); + Assert.Equal(-1, result); + } + + [Fact] + public void ZeroShouldBeLessThanOne() + { + var result = new SqlDateTime2(0).CompareTo(new SqlDateTime2(1)); + Assert.Equal(-1, result); + } + + [Fact] + public void OutOfLowRangeTicksTest() + { + Assert.Throws(() => new SqlDateTime2(-1)); + } + + [Fact] + public void OutOfHighRangeTicksTest() + { + Assert.Throws(() => new SqlDateTime2(long.MaxValue)); + } + [Fact] + public void OutOfHighRangeTicksTest2() + { + Assert.Throws(() => new SqlDateTime2(DateTime.MaxValue.Ticks+1)); + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj index 2a5bf658c5..881c1b3117 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj @@ -57,6 +57,7 @@ Common\System\Collections\DictionaryExtensions.cs + diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DateTime2Test/DateTime2Test.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DateTime2Test/DateTime2Test.cs new file mode 100644 index 0000000000..c3f85ee216 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DateTime2Test/DateTime2Test.cs @@ -0,0 +1,94 @@ +// 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. + +using System; +using Xunit; + +namespace Microsoft.Data.SqlClient.ManualTesting.Tests.SQL.DateTime2Test +{ + public class DateTime2Test + { + private static readonly DateTime TestDate = new DateTime(2020, 12, 17, 18, 33, 12, 123).AddTicks(4567); + + private const string SqlDateTimeFormat = "yyyy'-'MM'-'dd HH':'mm':'ss'.'fff"; + private const string SqlDateTime2Format = "yyyy'-'MM'-'dd HH':'mm':'ss'.'fffffff"; + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public void NullDefinedAsDateTime2() + { + var value = GetValueFromReader(@"SELECT CAST(NULL AS DATETIME2)", reader => reader.GetSqlDateTime2(0)); + Assert.True(value.IsNull); + } + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public void DateTimeAsNullDateTime2() + { + var value = GetValueFromReader(@"SELECT CAST(NULL AS DATETIME)", reader => reader.GetSqlDateTime2(0)); + Assert.True(value.IsNull); + } + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public void DateAsNullDateTime2() + { + var value = GetValueFromReader(@"SELECT CAST(NULL AS DATE)", reader => reader.GetSqlDateTime2(0)); + Assert.True(value.IsNull); + } + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public void DateTimeAsDateTime2() + { + var value = GetValueFromReader($"SELECT CAST(CAST('{TestDate.ToString(SqlDateTime2Format)}' AS DATETIME2) as DATETIME)", reader => reader.GetSqlDateTime2(0)); + Assert.False(value.IsNull); + //DATETIME has reduced precision, so we only get the first 3 digits of time + var expected = new DateTime(TestDate.Year, TestDate.Month, TestDate.Day, TestDate.Hour, TestDate.Minute, TestDate.Second, 123); + Assert.Equal(expected, value.Value); + } + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public void DateAsDateTime2() + { + var value = GetValueFromReader($"SELECT CAST(CAST('{TestDate.ToString(SqlDateTimeFormat)}' AS DATETIME) as DATE)", reader => reader.GetSqlDateTime2(0)); + Assert.False(value.IsNull); + Assert.Equal(TestDate.Date, value.Value.Date); + } + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public void DateTime2AsDateTime2DecreasedPrecision() + { + var value = GetValueFromReader($"SELECT CAST('{TestDate.ToString(SqlDateTime2Format)}' AS DATETIME2(3))", reader => reader.GetSqlDateTime2(0)); + Assert.False(value.IsNull); + var expected = new DateTime(TestDate.Year, TestDate.Month, TestDate.Day, TestDate.Hour, TestDate.Minute, TestDate.Second, 123); + Assert.Equal(expected, value.Value); + } + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public void DateTime2AsDateTime2FullPrecision() + { + var value = GetValueFromReader($"SELECT CAST('{TestDate.ToString(SqlDateTime2Format)}' AS DATETIME2(7))", reader => reader.GetSqlDateTime2(0)); + Assert.False(value.IsNull); + + Assert.Equal(TestDate, value.Value); + } + + private static T GetValueFromReader(string sql, Func extractor) + { + using (SqlConnection connection = new SqlConnection(DataTestUtility.TCPConnectionString)) + { + connection.Open(); + + using (SqlCommand command = connection.CreateCommand()) + { + command.CommandText = sql; + command.CommandType = System.Data.CommandType.Text; + + using (var reader = command.ExecuteReader()) + { + reader.Read(); + return extractor(reader); + } + } + } + } + } +} From 346b225c0aa2c9d19958b1567a8254240a0caafc Mon Sep 17 00:00:00 2001 From: bjornbouetsmith Date: Fri, 5 Mar 2021 09:40:52 +0100 Subject: [PATCH 2/2] Fix typo Co-authored-by: Stuart Lang --- .../src/Microsoft/Data/SqlTypes/SqlDateTime2.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlDateTime2.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlDateTime2.cs index 6704d3a3c3..1a2dc0b859 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlDateTime2.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlDateTime2.cs @@ -18,7 +18,7 @@ public struct SqlDateTime2 : INullable, IComparable, IEquatable private SqlDateTime2(bool isNull)