Skip to content

Commit 334a636

Browse files
Port #3399 to release/5.2
1 parent 0b1f00b commit 334a636

File tree

5 files changed

+218
-8
lines changed

5 files changed

+218
-8
lines changed

src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ internal static void Assert(string message)
5252
}
5353
}
5454

55+
private static readonly Encoding s_utf8EncodingWithoutBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
5556
private static int _objectTypeCount; // EventSource counter
5657
private readonly SqlClientLogger _logger = new SqlClientLogger();
5758

@@ -2791,7 +2792,7 @@ private bool TryProcessEnvChange(int tokenLength, TdsParserStateObject stateObj,
27912792
// UTF8 collation
27922793
if (env._newCollation.IsUTF8)
27932794
{
2794-
_defaultEncoding = Encoding.UTF8;
2795+
_defaultEncoding = s_utf8EncodingWithoutBom;
27952796
}
27962797
else
27972798
{
@@ -4199,7 +4200,7 @@ internal bool TryProcessReturnValue(int length, TdsParserStateObject stateObj, o
41994200
// UTF8 collation
42004201
if (rec.collation.IsUTF8)
42014202
{
4202-
rec.encoding = Encoding.UTF8;
4203+
rec.encoding = s_utf8EncodingWithoutBom;
42034204
}
42044205
else
42054206
{
@@ -4986,7 +4987,7 @@ private bool TryProcessTypeInfo(TdsParserStateObject stateObj, SqlMetaDataPriv c
49864987
// UTF8 collation
49874988
if (col.collation.IsUTF8)
49884989
{
4989-
col.encoding = Encoding.UTF8;
4990+
col.encoding = s_utf8EncodingWithoutBom;
49904991
}
49914992
else
49924993
{
@@ -10801,7 +10802,7 @@ internal Task WriteBulkCopyValue(object value, SqlMetaDataPriv metadata, TdsPars
1080110802
// Replace encoding if it is UTF8
1080210803
if (metadata.collation.IsUTF8)
1080310804
{
10804-
_defaultEncoding = Encoding.UTF8;
10805+
_defaultEncoding = s_utf8EncodingWithoutBom;
1080510806
}
1080610807

1080710808
_defaultCollation = metadata.collation;

src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ namespace Microsoft.Data.SqlClient
3333
// and surfacing objects to the user.
3434
sealed internal class TdsParser
3535
{
36+
private static readonly Encoding s_utf8EncodingWithoutBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
3637
private static int _objectTypeCount; // EventSource Counter
3738
private readonly SqlClientLogger _logger = new SqlClientLogger();
3839

@@ -3235,7 +3236,7 @@ private bool TryProcessEnvChange(int tokenLength, TdsParserStateObject stateObj,
32353236
// UTF8 collation
32363237
if (env._newCollation.IsUTF8)
32373238
{
3238-
_defaultEncoding = Encoding.UTF8;
3239+
_defaultEncoding = s_utf8EncodingWithoutBom;
32393240
}
32403241
else
32413242
{
@@ -4757,7 +4758,7 @@ internal bool TryProcessReturnValue(int length,
47574758

47584759
if (rec.collation.IsUTF8)
47594760
{ // UTF8 collation
4760-
rec.encoding = Encoding.UTF8;
4761+
rec.encoding = s_utf8EncodingWithoutBom;
47614762
}
47624763
else
47634764
{
@@ -5657,7 +5658,7 @@ private bool TryProcessTypeInfo(TdsParserStateObject stateObj, SqlMetaDataPriv c
56575658

56585659
if (col.collation.IsUTF8)
56595660
{ // UTF8 collation
5660-
col.encoding = Encoding.UTF8;
5661+
col.encoding = s_utf8EncodingWithoutBom;
56615662
}
56625663
else
56635664
{
@@ -11740,7 +11741,7 @@ internal Task WriteBulkCopyValue(object value, SqlMetaDataPriv metadata, TdsPars
1174011741
// Replace encoding if it is UTF8
1174111742
if (metadata.collation.IsUTF8)
1174211743
{
11743-
_defaultEncoding = Encoding.UTF8;
11744+
_defaultEncoding = s_utf8EncodingWithoutBom;
1174411745
}
1174511746

1174611747
_defaultCollation = metadata.collation;

src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,17 @@ public static string GetUniqueNameForSqlServer(string prefix, bool withBracket =
590590
return name;
591591
}
592592

593+
public static void CreateTable(SqlConnection sqlConnection, string tableName, string createBody)
594+
{
595+
DropTable(sqlConnection, tableName);
596+
string tableCreate = "CREATE TABLE " + tableName + createBody;
597+
using (SqlCommand command = sqlConnection.CreateCommand())
598+
{
599+
command.CommandText = tableCreate;
600+
command.ExecuteNonQuery();
601+
}
602+
}
603+
593604
public static void DropTable(SqlConnection sqlConnection, string tableName)
594605
{
595606
ResurrectConnection(sqlConnection);

src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,7 @@
285285
<Compile Include="SQL\Common\SystemDataInternals\TdsParserHelper.cs" />
286286
<Compile Include="SQL\Common\SystemDataInternals\TdsParserStateObjectHelper.cs" />
287287
<Compile Include="SQL\ConnectionTestWithSSLCert\CertificateTest.cs" />
288+
<Compile Include="SQL\SqlBulkCopyTest\TestBulkCopyWithUTF8.cs" />
288289
<Compile Include="SQL\SqlCommand\SqlCommandStoredProcTest.cs" />
289290
<Compile Include="TracingTests\TestTdsServer.cs" />
290291
<Compile Include="XUnitAssemblyAttributes.cs" />
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
using System;
2+
using System.Data;
3+
using System.Threading.Tasks;
4+
using Xunit;
5+
6+
namespace Microsoft.Data.SqlClient.ManualTesting.Tests.SQL.SqlBulkCopyTest
7+
{
8+
/// <summary>
9+
/// Validates SqlBulkCopy functionality when working with UTF-8 encoded data.
10+
/// Ensures that data copied from a UTF-8 source table to a destination table retains its encoding and content integrity.
11+
/// </summary>
12+
public sealed class TestBulkCopyWithUtf8 : IDisposable
13+
{
14+
private static string s_sourceTable = DataTestUtility.GetUniqueName("SourceTableForUTF8Data");
15+
private static string s_destinationTable = DataTestUtility.GetUniqueName("DestinationTableForUTF8Data");
16+
private static string s_testValue = "test";
17+
private static byte[] s_testValueInUtf8Bytes = new byte[] { 0x74, 0x65, 0x73, 0x74 };
18+
private static readonly string s_insertQuery = $"INSERT INTO {s_sourceTable} VALUES('{s_testValue}')";
19+
20+
/// <summary>
21+
/// Constructor: Initializes and populates source and destination tables required for the tests.
22+
/// </summary>
23+
public TestBulkCopyWithUtf8()
24+
{
25+
using SqlConnection sourceConnection = new SqlConnection(GetConnectionString(true));
26+
sourceConnection.Open();
27+
SetupTables(sourceConnection, s_sourceTable, s_destinationTable, s_insertQuery);
28+
}
29+
30+
/// <summary>
31+
/// Cleanup method to drop tables after test completion.
32+
/// </summary>
33+
public void Dispose()
34+
{
35+
using SqlConnection connection = new SqlConnection(GetConnectionString(true));
36+
connection.Open();
37+
DataTestUtility.DropTable(connection, s_sourceTable);
38+
DataTestUtility.DropTable(connection, s_destinationTable);
39+
connection.Close();
40+
}
41+
42+
/// <summary>
43+
/// Builds a connection string with or without Multiple Active Result Sets (MARS) property.
44+
/// </summary>
45+
private string GetConnectionString(bool enableMars)
46+
{
47+
return new SqlConnectionStringBuilder(DataTestUtility.TCPConnectionString)
48+
{
49+
MultipleActiveResultSets = enableMars
50+
}.ConnectionString;
51+
}
52+
53+
/// <summary>
54+
/// Creates source and destination tables with a varchar(max) column with a collation setting
55+
/// that stores the data in UTF8 encoding and inserts the data in the source table.
56+
/// </summary>
57+
private void SetupTables(SqlConnection connection, string sourceTable, string destinationTable, string insertQuery)
58+
{
59+
string columnDefinition = "(str_col varchar(max) COLLATE Latin1_General_100_CS_AS_KS_WS_SC_UTF8)";
60+
DataTestUtility.CreateTable(connection, sourceTable, columnDefinition);
61+
DataTestUtility.CreateTable(connection, destinationTable, columnDefinition);
62+
using SqlCommand insertCommand = connection.CreateCommand();
63+
insertCommand.CommandText = insertQuery;
64+
Helpers.TryExecute(insertCommand, insertQuery);
65+
}
66+
67+
/// <summary>
68+
/// Synchronous test case: Validates that data copied using SqlBulkCopy matches UTF-8 byte sequence for test value.
69+
/// Tested with MARS enabled and disabled, and with streaming enabled and disabled.
70+
/// </summary>
71+
[ConditionalTheory(typeof(DataTestUtility),
72+
nameof(DataTestUtility.AreConnStringsSetup),
73+
nameof(DataTestUtility.IsNotAzureServer),
74+
nameof(DataTestUtility.IsNotAzureSynapse))]
75+
[InlineData(true, true)]
76+
[InlineData(false, true)]
77+
[InlineData(true, false)]
78+
[InlineData(false, false)]
79+
public void BulkCopy_Utf8Data_ShouldMatchSource(bool isMarsEnabled, bool enableStreaming)
80+
{
81+
// Setup connections for source and destination tables
82+
string connectionString = GetConnectionString(isMarsEnabled);
83+
using SqlConnection sourceConnection = new SqlConnection(connectionString);
84+
sourceConnection.Open();
85+
using SqlConnection destinationConnection = new SqlConnection(connectionString);
86+
destinationConnection.Open();
87+
88+
// Read data from source table
89+
using SqlCommand sourceDataCommand = new SqlCommand($"SELECT str_col FROM {s_sourceTable}", sourceConnection);
90+
using SqlDataReader reader = sourceDataCommand.ExecuteReader(CommandBehavior.SequentialAccess);
91+
92+
// Verify that the destination table is empty before bulk copy
93+
using SqlCommand countCommand = new SqlCommand($"SELECT COUNT(*) FROM {s_destinationTable}", destinationConnection);
94+
Assert.Equal(0, Convert.ToInt16(countCommand.ExecuteScalar()));
95+
96+
// Initialize bulk copy configuration
97+
using SqlBulkCopy bulkCopy = new SqlBulkCopy(destinationConnection)
98+
{
99+
EnableStreaming = enableStreaming,
100+
DestinationTableName = s_destinationTable
101+
};
102+
103+
try
104+
{
105+
// Perform bulk copy from source to destination table
106+
bulkCopy.WriteToServer(reader);
107+
}
108+
catch (Exception ex)
109+
{
110+
// If bulk copy fails, fail the test with the exception message
111+
Assert.Fail($"Bulk copy failed: {ex.Message}");
112+
}
113+
114+
// Verify that the 1 row from the source table has been copied into our destination table.
115+
Assert.Equal(1, Convert.ToInt16(countCommand.ExecuteScalar()));
116+
117+
// Read the data from destination table as varbinary to verify the UTF-8 byte sequence
118+
using SqlCommand verifyCommand = new SqlCommand($"SELECT cast(str_col as varbinary) FROM {s_destinationTable}", destinationConnection);
119+
using SqlDataReader verifyReader = verifyCommand.ExecuteReader(CommandBehavior.SequentialAccess);
120+
121+
// Verify that we have data in the destination table
122+
Assert.True(verifyReader.Read(), "No data found in destination table after bulk copy.");
123+
124+
// Read the value of the column as SqlBinary.
125+
byte[] actualBytes = verifyReader.GetSqlBinary(0).Value;
126+
127+
// Verify that the byte array matches the expected UTF-8 byte sequence
128+
Assert.Equal(s_testValueInUtf8Bytes.Length, actualBytes.Length);
129+
Assert.Equal(s_testValueInUtf8Bytes, actualBytes);
130+
}
131+
132+
/// <summary>
133+
/// Asynchronous version of the testcase BulkCopy_Utf8Data_ShouldMatchSource
134+
/// </summary>
135+
[ConditionalTheory(typeof(DataTestUtility),
136+
nameof(DataTestUtility.AreConnStringsSetup),
137+
nameof(DataTestUtility.IsNotAzureServer),
138+
nameof(DataTestUtility.IsNotAzureSynapse))]
139+
[InlineData(true, true)]
140+
[InlineData(false, true)]
141+
[InlineData(true, false)]
142+
[InlineData(false, false)]
143+
public async Task BulkCopy_Utf8Data_ShouldMatchSource_Async(bool isMarsEnabled, bool enableStreaming)
144+
{
145+
// Setup connections for source and destination tables
146+
string connectionString = GetConnectionString(isMarsEnabled);
147+
using SqlConnection sourceConnection = new SqlConnection(connectionString);
148+
await sourceConnection.OpenAsync();
149+
using SqlConnection destinationConnection = new SqlConnection(connectionString);
150+
await destinationConnection.OpenAsync();
151+
152+
// Read data from source table
153+
using SqlCommand sourceDataCommand = new SqlCommand($"SELECT str_col FROM {s_sourceTable}", sourceConnection);
154+
using SqlDataReader reader = await sourceDataCommand.ExecuteReaderAsync(CommandBehavior.SequentialAccess);
155+
156+
// Verify that the destination table is empty before bulk copy
157+
using SqlCommand countCommand = new SqlCommand($"SELECT COUNT(*) FROM {s_destinationTable}", destinationConnection);
158+
Assert.Equal(0, Convert.ToInt16(await countCommand.ExecuteScalarAsync()));
159+
160+
// Initialize bulk copy configuration
161+
using SqlBulkCopy bulkCopy = new SqlBulkCopy(destinationConnection)
162+
{
163+
EnableStreaming = enableStreaming,
164+
DestinationTableName = s_destinationTable
165+
};
166+
167+
try
168+
{
169+
// Perform bulk copy from source to destination table
170+
await bulkCopy.WriteToServerAsync(reader);
171+
}
172+
catch (Exception ex)
173+
{
174+
// If bulk copy fails, fail the test with the exception message
175+
Assert.Fail($"Bulk copy failed: {ex.Message}");
176+
}
177+
178+
// Verify that the 1 row from the source table has been copied into our destination table.
179+
Assert.Equal(1, Convert.ToInt16(await countCommand.ExecuteScalarAsync()));
180+
181+
// Read the data from destination table as varbinary to verify the UTF-8 byte sequence
182+
using SqlCommand verifyCommand = new SqlCommand($"SELECT cast(str_col as varbinary) FROM {s_destinationTable}", destinationConnection);
183+
using SqlDataReader verifyReader = await verifyCommand.ExecuteReaderAsync(CommandBehavior.SequentialAccess);
184+
185+
// Verify that we have data in the destination table
186+
Assert.True(await verifyReader.ReadAsync(), "No data found in destination table after bulk copy.");
187+
188+
// Read the value of the column as SqlBinary.
189+
byte[] actualBytes = verifyReader.GetSqlBinary(0).Value;
190+
191+
// Verify that the byte array matches the expected UTF-8 byte sequence
192+
Assert.Equal(s_testValueInUtf8Bytes.Length, actualBytes.Length);
193+
Assert.Equal(s_testValueInUtf8Bytes, actualBytes);
194+
}
195+
}
196+
}

0 commit comments

Comments
 (0)