Skip to content

Commit

Permalink
Fix | Fix DateTimeOffset size in TdsValueSetter.cs class file. (#2453)
Browse files Browse the repository at this point in the history
  • Loading branch information
arellegue committed May 2, 2024
1 parent aefd723 commit 226bdb8
Show file tree
Hide file tree
Showing 3 changed files with 171 additions and 3 deletions.
Expand Up @@ -697,10 +697,34 @@ internal void SetDateTimeOffset(DateTimeOffset value)
short offset = (short)value.Offset.TotalMinutes;

#if NETCOREAPP
Span<byte> result = stackalloc byte[9];
// In TDS protocol:
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tds/786f5b8a-f87d-4980-9070-b9b7274c681d
//
// date is represented as one 3 - byte unsigned integer that represents the number of days since January 1, year 1.
//
// time(n) is represented as one unsigned integer that represents the number of 10^-n,
// (10 to the power of negative n), second increments since 12 AM within a day.
// The length, in bytes, of that integer depends on the scale n as follows:
// 3 bytes if 0 <= n < = 2.
// 4 bytes if 3 <= n < = 4.
// 5 bytes if 5 <= n < = 7.
// For example:
// DateTimeOffset dateTimeOffset = new DateTimeOffset(2024, 1, 1, 23, 59, 59, TimeSpan.Zero); // using scale of 0
// time = 23:59:59, scale is 1, is represented as 863990 in 3 bytes or { 246, 46, 13, 0, 0, 0, 0, 0 } in bytes array

Span<byte> result = stackalloc byte[8];

// https://learn.microsoft.com/en-us/dotnet/api/system.buffers.binary.binaryprimitives.writeint64bigendian?view=net-8.0
// WriteInt64LittleEndian requires 8 bytes to write the value.
BinaryPrimitives.WriteInt64LittleEndian(result, time);
BinaryPrimitives.WriteInt32LittleEndian(result.Slice(5), days);
_stateObj.WriteByteSpan(result.Slice(0, 8));
// The DateTimeOffset length is variable depending on the scale, 1 to 7, used.
// If length = 8, 8 - 5 = 3 bytes is used for time.
// If length = 10, 10 - 5 = 5 bytes is used for time.
_stateObj.WriteByteSpan(result.Slice(0, length - 5)); // this writes the time value to the state object using dynamic length based on the scale.

// Date is represented as 3 bytes. So, 3 bytes are written to the state object.
BinaryPrimitives.WriteInt32LittleEndian(result, days);
_stateObj.WriteByteSpan(result.Slice(0, 3));
#else
_stateObj.WriteByteArray(BitConverter.GetBytes(time), length - 5, 0); // time
_stateObj.WriteByteArray(BitConverter.GetBytes(days), 3, 0); // date
Expand Down
Expand Up @@ -204,6 +204,7 @@
<Compile Include="SQL\TransactionTest\TransactionEnlistmentTest.cs" />
<Compile Include="SQL\UdtTest\SqlServerTypesTest.cs" />
<Compile Include="SQL\UdtTest\UdtBulkCopyTest.cs" />
<Compile Include="SQL\UdtTest\UdtDateTimeOffsetTest.cs" />
<Compile Include="SQL\UdtTest\UdtTest.cs" />
<Compile Include="SQL\UdtTest\UdtTest2.cs" />
<Compile Include="SQL\UdtTest\UdtTestHelpers.cs" />
Expand Down
@@ -0,0 +1,143 @@
// 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 System.Data;
using Microsoft.Data.SqlClient.Server;
using Xunit;

namespace Microsoft.Data.SqlClient.ManualTesting.Tests
{
public class DateTimeOffsetList : SqlDataRecord
{
public DateTimeOffsetList(DateTimeOffset dateTimeOffset)
: base(new SqlMetaData("dateTimeOffset", SqlDbType.DateTimeOffset, 0, 1)) // this is using scale 1
{
this.SetValues(dateTimeOffset);
}
}

public class DateTimeOffsetVariableScale : SqlDataRecord
{
public DateTimeOffsetVariableScale(DateTimeOffset dateTimeOffset, int scale)
: base(new SqlMetaData("dateTimeOffset", SqlDbType.DateTimeOffset, 0, (byte)scale)) // this is using variable scale
{
this.SetValues(dateTimeOffset);
}
}

public class UdtDateTimeOffsetTest
{
private readonly string _connectionString = null;
private readonly string _udtTableType = DataTestUtility.GetUniqueNameForSqlServer("DataTimeOffsetTableType");

public UdtDateTimeOffsetTest()
{
_connectionString = DataTestUtility.TCPConnectionString;
}

// This unit test is for the reported issue #2423 using a specific scale of 1
[ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureServer), nameof(DataTestUtility.IsNotAzureSynapse))]
public void SelectFromSqlParameterShouldSucceed()
{
using SqlConnection connection = new(_connectionString);
connection.Open();
SetupUserDefinedTableType(connection, _udtTableType);

try
{
DateTimeOffset dateTimeOffset = new DateTimeOffset(2024, 1, 1, 23, 59, 59, 500, TimeSpan.Zero);
var param = new SqlParameter
{
ParameterName = "@params",
SqlDbType = SqlDbType.Structured,
TypeName = $"dbo.{_udtTableType}",
Value = new DateTimeOffsetList[] { new DateTimeOffsetList(dateTimeOffset) }
};

using (var cmd = connection.CreateCommand())
{
cmd.CommandText = "SELECT * FROM @params";
cmd.Parameters.Add(param);
var result = cmd.ExecuteScalar();
Assert.Equal(dateTimeOffset, result);
}
}
finally
{
DataTestUtility.DropUserDefinedType(connection, _udtTableType);
}
}

// This unit test is to ensure that time in DateTimeOffset with all scales are working as expected
[ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureServer), nameof(DataTestUtility.IsNotAzureSynapse))]
public void DateTimeOffsetAllScalesTestShouldSucceed()
{
string tvpTypeName = DataTestUtility.GetUniqueNameForSqlServer("tvpType");

using SqlConnection connection = new(_connectionString);
connection.Open();

try
{
// Use different scale for each test: 0 to 7
int fromScale = 0;
int toScale = 7;

for (int scale = fromScale; scale <= toScale; scale++)
{
DateTimeOffset dateTimeOffset = new DateTimeOffset(2024, 1, 1, 23, 59, 59, TimeSpan.Zero);

// Add sub-second offset corresponding to the scale being tested
TimeSpan subSeconds = TimeSpan.FromTicks((long)(TimeSpan.TicksPerSecond / Math.Pow(10, scale)));
dateTimeOffset = dateTimeOffset.Add(subSeconds);

DataTestUtility.DropUserDefinedType(connection, tvpTypeName);
SetupDateTimeOffsetTableType(connection, tvpTypeName, scale);

var param = new SqlParameter
{
ParameterName = "@params",
SqlDbType = SqlDbType.Structured,
Scale = (byte)scale,
TypeName = $"dbo.{tvpTypeName}",
Value = new DateTimeOffsetVariableScale[] { new DateTimeOffsetVariableScale(dateTimeOffset, scale) }
};

using (var cmd = connection.CreateCommand())
{
cmd.CommandText = "SELECT * FROM @params";
cmd.Parameters.Add(param);
var result = cmd.ExecuteScalar();
Assert.Equal(dateTimeOffset, result);
}
}
}
finally
{
DataTestUtility.DropUserDefinedType(connection, tvpTypeName);
}
}

private static void SetupUserDefinedTableType(SqlConnection connection, string tableTypeName)
{
using (SqlCommand cmd = connection.CreateCommand())
{
cmd.CommandType = CommandType.Text;
cmd.CommandText = $"CREATE TYPE {tableTypeName} AS TABLE ([Value] DATETIMEOFFSET(1) NOT NULL) ";
cmd.ExecuteNonQuery();
}
}

private static void SetupDateTimeOffsetTableType(SqlConnection connection, string tableTypeName, int scale)
{
using (SqlCommand cmd = connection.CreateCommand())
{
cmd.CommandType = CommandType.Text;
cmd.CommandText = $"CREATE TYPE {tableTypeName} AS TABLE ([Value] DATETIMEOFFSET({scale}) NOT NULL) ";
cmd.ExecuteNonQuery();
}
}
}
}

0 comments on commit 226bdb8

Please sign in to comment.