Skip to content

Port #3399 to release/5.1 #3409

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

Open
wants to merge 1 commit into
base: release/5.1
Choose a base branch
from
Open
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 @@ -39,6 +39,7 @@ internal struct SNIErrorDetails
// and surfacing objects to the user.
internal sealed partial class TdsParser
{
private static readonly Encoding s_utf8EncodingWithoutBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
private static int _objectTypeCount; // EventSource counter
private readonly SqlClientLogger _logger = new SqlClientLogger();

Expand Down Expand Up @@ -2767,7 +2768,7 @@ private bool TryProcessEnvChange(int tokenLength, TdsParserStateObject stateObj,
// UTF8 collation
if (env._newCollation.IsUTF8)
{
_defaultEncoding = Encoding.UTF8;
_defaultEncoding = s_utf8EncodingWithoutBom;
}
else
{
Expand Down Expand Up @@ -4171,7 +4172,7 @@ internal bool TryProcessReturnValue(int length, TdsParserStateObject stateObj, o
// UTF8 collation
if (rec.collation.IsUTF8)
{
rec.encoding = Encoding.UTF8;
rec.encoding = s_utf8EncodingWithoutBom;
}
else
{
Expand Down Expand Up @@ -4955,7 +4956,7 @@ private bool TryProcessTypeInfo(TdsParserStateObject stateObj, SqlMetaDataPriv c
// UTF8 collation
if (col.collation.IsUTF8)
{
col.encoding = Encoding.UTF8;
col.encoding = s_utf8EncodingWithoutBom;
}
else
{
Expand Down Expand Up @@ -10681,7 +10682,7 @@ internal Task WriteBulkCopyValue(object value, SqlMetaDataPriv metadata, TdsPars
// Replace encoding if it is UTF8
if (metadata.collation.IsUTF8)
{
_defaultEncoding = Encoding.UTF8;
_defaultEncoding = s_utf8EncodingWithoutBom;
}

_defaultCollation = metadata.collation;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ namespace Microsoft.Data.SqlClient
// and surfacing objects to the user.
sealed internal class TdsParser
{
private static readonly Encoding s_utf8EncodingWithoutBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
private static int _objectTypeCount; // EventSource Counter
private readonly SqlClientLogger _logger = new SqlClientLogger();

Expand Down Expand Up @@ -3221,7 +3222,7 @@ private bool TryProcessEnvChange(int tokenLength, TdsParserStateObject stateObj,
// UTF8 collation
if (env._newCollation.IsUTF8)
{
_defaultEncoding = Encoding.UTF8;
_defaultEncoding = s_utf8EncodingWithoutBom;
}
else
{
Expand Down Expand Up @@ -4739,7 +4740,7 @@ internal bool TryProcessReturnValue(int length,

if (rec.collation.IsUTF8)
{ // UTF8 collation
rec.encoding = Encoding.UTF8;
rec.encoding = s_utf8EncodingWithoutBom;
}
else
{
Expand Down Expand Up @@ -5636,7 +5637,7 @@ private bool TryProcessTypeInfo(TdsParserStateObject stateObj, SqlMetaDataPriv c

if (col.collation.IsUTF8)
{ // UTF8 collation
col.encoding = Encoding.UTF8;
col.encoding = s_utf8EncodingWithoutBom;
}
else
{
Expand Down Expand Up @@ -11670,7 +11671,7 @@ internal Task WriteBulkCopyValue(object value, SqlMetaDataPriv metadata, TdsPars
// Replace encoding if it is UTF8
if (metadata.collation.IsUTF8)
{
_defaultEncoding = Encoding.UTF8;
_defaultEncoding = s_utf8EncodingWithoutBom;
}

_defaultCollation = metadata.collation;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,17 @@ public static bool IsSupportingDistributedTransactions()
#endif
}

public static void CreateTable(SqlConnection sqlConnection, string tableName, string createBody)
{
DropTable(sqlConnection, tableName);
string tableCreate = "CREATE TABLE " + tableName + createBody;
Copy link
Preview

Copilot AI Jun 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] It may be clearer to interpolate or explicitly include a space before the column definition, e.g.: var tableCreate = $"CREATE TABLE {tableName} {createBody}".

Suggested change
string tableCreate = "CREATE TABLE " + tableName + createBody;
string tableCreate = $"CREATE TABLE {tableName} {createBody}";

Copilot uses AI. Check for mistakes.

using (SqlCommand command = sqlConnection.CreateCommand())
{
command.CommandText = tableCreate;
command.ExecuteNonQuery();
}
}

public static void DropTable(SqlConnection sqlConnection, string tableName)
{
ResurrectConnection(sqlConnection);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@
<Compile Include="SQL\Common\SystemDataInternals\DataReaderHelper.cs" />
<Compile Include="SQL\Common\SystemDataInternals\TdsParserHelper.cs" />
<Compile Include="SQL\Common\SystemDataInternals\TdsParserStateObjectHelper.cs" />
<Compile Include="SQL\SqlBulkCopyTest\TestBulkCopyWithUTF8.cs" />
<Compile Include="SQL\SqlCommand\SqlCommandStoredProcTest.cs" />
<Compile Include="TracingTests\TestTdsServer.cs" />
<Compile Include="XUnitAssemblyAttributes.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
using System;
using System.Data;
using System.Threading.Tasks;
using Xunit;

namespace Microsoft.Data.SqlClient.ManualTesting.Tests.SQL.SqlBulkCopyTest
{
/// <summary>
/// Validates SqlBulkCopy functionality when working with UTF-8 encoded data.
/// Ensures that data copied from a UTF-8 source table to a destination table retains its encoding and content integrity.
/// </summary>
public sealed class TestBulkCopyWithUtf8 : IDisposable
{
private static string s_sourceTable = DataTestUtility.GetUniqueName("SourceTableForUTF8Data");
private static string s_destinationTable = DataTestUtility.GetUniqueName("DestinationTableForUTF8Data");
private static string s_testValue = "test";
private static byte[] s_testValueInUtf8Bytes = new byte[] { 0x74, 0x65, 0x73, 0x74 };
private static readonly string s_insertQuery = $"INSERT INTO {s_sourceTable} VALUES('{s_testValue}')";

/// <summary>
/// Constructor: Initializes and populates source and destination tables required for the tests.
/// </summary>
public TestBulkCopyWithUtf8()
{
using SqlConnection sourceConnection = new SqlConnection(GetConnectionString(true));
sourceConnection.Open();
SetupTables(sourceConnection, s_sourceTable, s_destinationTable, s_insertQuery);
}

/// <summary>
/// Cleanup method to drop tables after test completion.
/// </summary>
public void Dispose()
{
using SqlConnection connection = new SqlConnection(GetConnectionString(true));
connection.Open();
DataTestUtility.DropTable(connection, s_sourceTable);
DataTestUtility.DropTable(connection, s_destinationTable);
connection.Close();
}

/// <summary>
/// Builds a connection string with or without Multiple Active Result Sets (MARS) property.
/// </summary>
private string GetConnectionString(bool enableMars)
{
return new SqlConnectionStringBuilder(DataTestUtility.TCPConnectionString)
{
MultipleActiveResultSets = enableMars
}.ConnectionString;
}

/// <summary>
/// Creates source and destination tables with a varchar(max) column with a collation setting
/// that stores the data in UTF8 encoding and inserts the data in the source table.
/// </summary>
private void SetupTables(SqlConnection connection, string sourceTable, string destinationTable, string insertQuery)
{
string columnDefinition = "(str_col varchar(max) COLLATE Latin1_General_100_CS_AS_KS_WS_SC_UTF8)";
DataTestUtility.CreateTable(connection, sourceTable, columnDefinition);
DataTestUtility.CreateTable(connection, destinationTable, columnDefinition);
using SqlCommand insertCommand = connection.CreateCommand();
insertCommand.CommandText = insertQuery;
Helpers.TryExecute(insertCommand, insertQuery);
}

/// <summary>
/// Synchronous test case: Validates that data copied using SqlBulkCopy matches UTF-8 byte sequence for test value.
/// Tested with MARS enabled and disabled, and with streaming enabled and disabled.
/// </summary>
[ConditionalTheory(typeof(DataTestUtility),
nameof(DataTestUtility.AreConnStringsSetup),
nameof(DataTestUtility.IsNotAzureServer),
nameof(DataTestUtility.IsNotAzureSynapse))]
[InlineData(true, true)]
[InlineData(false, true)]
[InlineData(true, false)]
[InlineData(false, false)]
public void BulkCopy_Utf8Data_ShouldMatchSource(bool isMarsEnabled, bool enableStreaming)
{
// Setup connections for source and destination tables
string connectionString = GetConnectionString(isMarsEnabled);
using SqlConnection sourceConnection = new SqlConnection(connectionString);
sourceConnection.Open();
using SqlConnection destinationConnection = new SqlConnection(connectionString);
destinationConnection.Open();

// Read data from source table
using SqlCommand sourceDataCommand = new SqlCommand($"SELECT str_col FROM {s_sourceTable}", sourceConnection);
using SqlDataReader reader = sourceDataCommand.ExecuteReader(CommandBehavior.SequentialAccess);

// Verify that the destination table is empty before bulk copy
using SqlCommand countCommand = new SqlCommand($"SELECT COUNT(*) FROM {s_destinationTable}", destinationConnection);
Assert.Equal(0, Convert.ToInt16(countCommand.ExecuteScalar()));

// Initialize bulk copy configuration
using SqlBulkCopy bulkCopy = new SqlBulkCopy(destinationConnection)
{
EnableStreaming = enableStreaming,
DestinationTableName = s_destinationTable
};

try
{
// Perform bulk copy from source to destination table
bulkCopy.WriteToServer(reader);
}
catch (Exception ex)
{
// If bulk copy fails, fail the test with the exception message
Assert.Fail($"Bulk copy failed: {ex.Message}");
Copy link
Preview

Copilot AI Jun 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Xunit does not provide Assert.Fail. Replace with Assert.True(false, ...) or another Xunit-friendly assertion to fail the test with the exception message.

Suggested change
Assert.Fail($"Bulk copy failed: {ex.Message}");
Assert.True(false, $"Bulk copy failed: {ex.Message}");

Copilot uses AI. Check for mistakes.

}

// Verify that the 1 row from the source table has been copied into our destination table.
Assert.Equal(1, Convert.ToInt16(countCommand.ExecuteScalar()));

// Read the data from destination table as varbinary to verify the UTF-8 byte sequence
using SqlCommand verifyCommand = new SqlCommand($"SELECT cast(str_col as varbinary) FROM {s_destinationTable}", destinationConnection);
using SqlDataReader verifyReader = verifyCommand.ExecuteReader(CommandBehavior.SequentialAccess);

// Verify that we have data in the destination table
Assert.True(verifyReader.Read(), "No data found in destination table after bulk copy.");

// Read the value of the column as SqlBinary.
byte[] actualBytes = verifyReader.GetSqlBinary(0).Value;

// Verify that the byte array matches the expected UTF-8 byte sequence
Assert.Equal(s_testValueInUtf8Bytes.Length, actualBytes.Length);
Assert.Equal(s_testValueInUtf8Bytes, actualBytes);
}

/// <summary>
/// Asynchronous version of the testcase BulkCopy_Utf8Data_ShouldMatchSource
/// </summary>
[ConditionalTheory(typeof(DataTestUtility),
nameof(DataTestUtility.AreConnStringsSetup),
nameof(DataTestUtility.IsNotAzureServer),
nameof(DataTestUtility.IsNotAzureSynapse))]
[InlineData(true, true)]
[InlineData(false, true)]
[InlineData(true, false)]
[InlineData(false, false)]
public async Task BulkCopy_Utf8Data_ShouldMatchSource_Async(bool isMarsEnabled, bool enableStreaming)
{
// Setup connections for source and destination tables
string connectionString = GetConnectionString(isMarsEnabled);
using SqlConnection sourceConnection = new SqlConnection(connectionString);
await sourceConnection.OpenAsync();
using SqlConnection destinationConnection = new SqlConnection(connectionString);
await destinationConnection.OpenAsync();

// Read data from source table
using SqlCommand sourceDataCommand = new SqlCommand($"SELECT str_col FROM {s_sourceTable}", sourceConnection);
using SqlDataReader reader = await sourceDataCommand.ExecuteReaderAsync(CommandBehavior.SequentialAccess);

// Verify that the destination table is empty before bulk copy
using SqlCommand countCommand = new SqlCommand($"SELECT COUNT(*) FROM {s_destinationTable}", destinationConnection);
Assert.Equal(0, Convert.ToInt16(await countCommand.ExecuteScalarAsync()));

// Initialize bulk copy configuration
using SqlBulkCopy bulkCopy = new SqlBulkCopy(destinationConnection)
{
EnableStreaming = enableStreaming,
DestinationTableName = s_destinationTable
};

try
{
// Perform bulk copy from source to destination table
await bulkCopy.WriteToServerAsync(reader);
}
catch (Exception ex)
{
// If bulk copy fails, fail the test with the exception message
Assert.Fail($"Bulk copy failed: {ex.Message}");
Copy link
Preview

Copilot AI Jun 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Xunit does not provide Assert.Fail. Replace with Assert.True(false, ...) or another Xunit-friendly assertion to fail the test with the exception message.

Suggested change
Assert.Fail($"Bulk copy failed: {ex.Message}");
Assert.True(false, $"Bulk copy failed: {ex.Message}");

Copilot uses AI. Check for mistakes.

}

// Verify that the 1 row from the source table has been copied into our destination table.
Assert.Equal(1, Convert.ToInt16(await countCommand.ExecuteScalarAsync()));

// Read the data from destination table as varbinary to verify the UTF-8 byte sequence
using SqlCommand verifyCommand = new SqlCommand($"SELECT cast(str_col as varbinary) FROM {s_destinationTable}", destinationConnection);
using SqlDataReader verifyReader = await verifyCommand.ExecuteReaderAsync(CommandBehavior.SequentialAccess);

// Verify that we have data in the destination table
Assert.True(await verifyReader.ReadAsync(), "No data found in destination table after bulk copy.");

// Read the value of the column as SqlBinary.
byte[] actualBytes = verifyReader.GetSqlBinary(0).Value;

// Verify that the byte array matches the expected UTF-8 byte sequence
Assert.Equal(s_testValueInUtf8Bytes.Length, actualBytes.Length);
Assert.Equal(s_testValueInUtf8Bytes, actualBytes);
}
}
}
Loading