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

Fix | Fix DateTimeOffset size in TdsValueSetter.cs class file. #2453

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
89134e4
Fix the DateTimeOffset parameter size in the TdsValueSetter to be inc…
arellegue Apr 8, 2024
2ead820
Added unit test for DateTimeOffset in TdsValueSetter to be wrriten in…
arellegue Apr 8, 2024
3f33139
Removed unused using and sorted remaining ones.
arellegue Apr 8, 2024
2515ef4
Apply suggested fix instead.
arellegue Apr 9, 2024
8ae7ab3
Apply suggestion from code review.
arellegue Apr 16, 2024
10b0006
Move DateTimeOffsetList inside the unit test.
arellegue Apr 16, 2024
7c89a79
Add explanation to why 3 bytes time and 3 bytes are used which is alw…
arellegue Apr 26, 2024
8ce9342
Updated comment for why 3 bytes is used for time.
arellegue Apr 26, 2024
61e1ae6
Added millisecond to the unit test.
arellegue Apr 29, 2024
671a964
Add unit test that tests all scales, 0 to 7, of DateTimeOffset.
arellegue Apr 30, 2024
0aafbd3
Remove defined functions that were never used.
arellegue Apr 30, 2024
ce64aa7
Remove catch block.
arellegue Apr 30, 2024
dc69ac1
Use decreasing fractional seconds in unit test for datetimeoffset sca…
arellegue Apr 30, 2024
5949f6a
Fixed stating and ending scale to be from 0 to 7.
arellegue May 1, 2024
dfa5bdb
Test scales 0 to 2.
arellegue May 1, 2024
36eb854
Put the scale 0 to 7 back.
arellegue May 1, 2024
ce27490
Fix comment 10 - n. It's actually 1o to the power of negative n.
arellegue May 1, 2024
699e896
Add more comments for explation of expected test result.
arellegue May 1, 2024
0519f7d
Applied suggested unit test changes.
arellegue May 1, 2024
0f8914f
Removed unused variable.
arellegue May 1, 2024
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 @@ -203,6 +203,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