Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[5.2.1] | Fix DateTimeOffset size in TdsValueSetter.cs class file. (#2453) #2506

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,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
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
}
Loading