Skip to content
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 @@ -4,6 +4,7 @@

using System;
using System.Data;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Data.SqlClient.ManualTesting.Tests;
using Xunit;
Expand All @@ -17,11 +18,10 @@ namespace Microsoft.Data.SqlClient.ManualTests.BulkCopy
[Trait("Set", "2")]
public sealed class TestBulkCopyWithUtf8 : IDisposable
{
private static string s_sourceTable = DataTestUtility.GetShortName("SourceTableForUTF8Data");
private static string s_destinationTable = DataTestUtility.GetShortName("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}')";
private static readonly string s_sourceTable = DataTestUtility.GetShortName("SourceTableForUTF8Data");
private static readonly string s_destinationTable = DataTestUtility.GetShortName("DestinationTableForUTF8Data");
private static readonly string s_testValue = GlobalizationTestData.CreatePacketSpanningText();
private static readonly byte[] s_testValueInUtf8Bytes = Encoding.UTF8.GetBytes(s_testValue);

/// <summary>
/// Constructor: Initializes and populates source and destination tables required for the tests.
Expand All @@ -37,7 +37,7 @@ public TestBulkCopyWithUtf8()

using SqlConnection sourceConnection = new SqlConnection(GetConnectionString(true));
sourceConnection.Open();
SetupTables(sourceConnection, s_sourceTable, s_destinationTable, s_insertQuery);
SetupTables(sourceConnection, s_sourceTable, s_destinationTable);
}

/// <summary>
Expand All @@ -61,26 +61,33 @@ public void Dispose()
/// <summary>
/// Builds a connection string with or without Multiple Active Result Sets (MARS) property.
/// </summary>
/// <param name="enableMars">Whether Multiple Active Result Sets is enabled.</param>
/// <returns>A connection string configured with a small packet size for boundary coverage.</returns>
private string GetConnectionString(bool enableMars)
{
return new SqlConnectionStringBuilder(DataTestUtility.TCPConnectionString)
{
MultipleActiveResultSets = enableMars
MultipleActiveResultSets = enableMars,
PacketSize = 512
}.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)
/// <param name="connection">The open connection used to create and populate the tables.</param>
/// <param name="sourceTable">The source table name.</param>
/// <param name="destinationTable">The destination table name.</param>
private void SetupTables(SqlConnection connection, string sourceTable, string destinationTable)
{
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);
insertCommand.CommandText = $"INSERT INTO {sourceTable} VALUES(@value)";
insertCommand.Parameters.Add(new SqlParameter("@value", SqlDbType.NVarChar, -1) { Value = s_testValue });
insertCommand.ExecuteNonQuery();
}

/// <summary>
Expand All @@ -105,6 +112,11 @@ public void BulkCopy_Utf8Data_ShouldMatchSource(bool isMarsEnabled, bool enableS
using SqlConnection destinationConnection = new SqlConnection(connectionString);
destinationConnection.Open();

using (SqlCommand sourceVerifyCommand = new SqlCommand($"SELECT CAST(str_col AS varbinary(max)) FROM {s_sourceTable}", sourceConnection))
{
Assert.Equal(s_testValueInUtf8Bytes, sourceVerifyCommand.ExecuteScalar());
}

// Read data from source table
using SqlCommand sourceDataCommand = new SqlCommand($"SELECT str_col FROM {s_sourceTable}", sourceConnection);
using SqlDataReader reader = sourceDataCommand.ExecuteReader(CommandBehavior.SequentialAccess);
Expand Down Expand Up @@ -135,7 +147,7 @@ public void BulkCopy_Utf8Data_ShouldMatchSource(bool isMarsEnabled, bool enableS
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 SqlCommand verifyCommand = new SqlCommand($"SELECT CAST(str_col AS varbinary(max)) FROM {s_destinationTable}", destinationConnection);
using SqlDataReader verifyReader = verifyCommand.ExecuteReader(CommandBehavior.SequentialAccess);

// Verify that we have data in the destination table
Expand Down Expand Up @@ -170,6 +182,11 @@ public async Task BulkCopy_Utf8Data_ShouldMatchSource_Async(bool isMarsEnabled,
using SqlConnection destinationConnection = new SqlConnection(connectionString);
await destinationConnection.OpenAsync();

using (SqlCommand sourceVerifyCommand = new SqlCommand($"SELECT CAST(str_col AS varbinary(max)) FROM {s_sourceTable}", sourceConnection))
{
Assert.Equal(s_testValueInUtf8Bytes, await sourceVerifyCommand.ExecuteScalarAsync());
}

// 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);
Expand Down Expand Up @@ -200,7 +217,7 @@ public async Task BulkCopy_Utf8Data_ShouldMatchSource_Async(bool isMarsEnabled,
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 SqlCommand verifyCommand = new SqlCommand($"SELECT CAST(str_col AS varbinary(max)) FROM {s_destinationTable}", destinationConnection);
using SqlDataReader verifyReader = await verifyCommand.ExecuteReaderAsync(CommandBehavior.SequentialAccess);

// Verify that we have data in the destination table
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
// 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 System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects;
using Xunit;

namespace Microsoft.Data.SqlClient.ManualTesting.Tests
{
/// <summary>
/// Provides representative worldwide text for encoding validation tests.
/// </summary>
internal static class GlobalizationTestData
{
internal const string RepresentativeText =
"Latin: Caf\u00E9 / Cafe\u0301 | " +
"Arabic: \u0627\u0644\u0639\u064E\u0631\u064E\u0628\u0650\u064A\u064E\u0651\u0629 | " +
"Devanagari: \u0939\u093F\u0928\u094D\u0926\u0940 | " +
"Thai: \u0E20\u0E32\u0E29\u0E32\u0E44\u0E17\u0E22 | " +
"CJK: \u65E5\u672C\u8A9E \u4E2D\u6587 \uD55C\uAD6D\uC5B4 | " +
"Supplementary: \uD83D\uDE00 \uD834\uDD1E | " +
"Emoji sequence: \uD83D\uDC69\uD83C\uDFFD\u200D\uD83D\uDCBB";

/// <summary>
/// Repeats the representative text so reads span TDS packets and internal character buffers.
/// </summary>
internal static string CreatePacketSpanningText()
{
StringBuilder value = new();
for (int i = 0; i < 16; i++)
{
value.Append(RepresentativeText);
}

return value.ToString();
}
}

/// <summary>
/// Validates exact worldwide text preservation through parameters, SQL Server storage, and reader APIs.
/// </summary>
[Trait("Set", "3")]
public static class GlobalizationEncodingTests
{
/// <summary>
/// Verifies normal and streamed Unicode parameters round-trip exactly through buffered and sequential
/// readers on synchronous and asynchronous paths.
/// </summary>
/// <param name="useAsync">Whether command and reader operations use asynchronous APIs.</param>
/// <param name="streamInput">Whether the input parameter is supplied through a <see cref="StringReader"/>.</param>
[ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))]
[InlineData(false, false)]
[InlineData(false, true)]
[InlineData(true, false)]
[InlineData(true, true)]
public static async Task UnicodeParameterAndReaderRoundTrip_PreservesWorldwideText(bool useAsync, bool streamInput)
{
string expected = GlobalizationTestData.CreatePacketSpanningText();
SqlConnectionStringBuilder connectionString = new(DataTestUtility.TCPConnectionString)
{
PacketSize = 512
};

using SqlConnection connection = new(connectionString.ConnectionString);
if (useAsync)
{
await connection.OpenAsync();
}
else
{
connection.Open();
}

using Table table = new(connection, nameof(UnicodeParameterAndReaderRoundTrip_PreservesWorldwideText), "(Value nvarchar(max) NOT NULL)");
using StringReader inputReader = new(expected);
using (SqlCommand insert = new($"INSERT INTO {table.Name} (Value) VALUES (@value)", connection))
{
insert.Parameters.Add(new SqlParameter("@value", SqlDbType.NVarChar, -1)
{
Value = streamInput ? inputReader : expected
});

if (useAsync)
{
await insert.ExecuteNonQueryAsync();
}
else
{
insert.ExecuteNonQuery();
}
}

string query = $"SELECT Value FROM {table.Name}";
string directValue = await ReadDirectValue(connection, query, useAsync);
string streamedValue = await ReadStreamedValue(connection, query, useAsync);

Assert.Equal(expected, directValue);
Assert.Equal(expected, streamedValue);
Assert.Equal(expected.Length, directValue.Length);
Assert.Equal(expected.Length, streamedValue.Length);
}

/// <summary>
/// Verifies a UTF-8-collated varchar value returns exact worldwide text and the expected UTF-8 bytes
/// on synchronous and asynchronous paths.
/// </summary>
/// <param name="useAsync">Whether command and reader operations use asynchronous APIs.</param>
[ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsUTF8Supported))]
[InlineData(false)]
[InlineData(true)]
public static async Task Utf8VarcharRoundTrip_PreservesWorldwideTextAndBytes(bool useAsync)
{
string expected = GlobalizationTestData.CreatePacketSpanningText();
SqlConnectionStringBuilder connectionString = new(DataTestUtility.TCPConnectionString)
{
PacketSize = 512
};
using SqlConnection connection = new(connectionString.ConnectionString);
if (useAsync)
{
await connection.OpenAsync();
}
else
{
connection.Open();
}

using Table table = new(
connection,
nameof(Utf8VarcharRoundTrip_PreservesWorldwideTextAndBytes),
"(Value varchar(max) COLLATE Latin1_General_100_CS_AS_KS_WS_SC_UTF8 NOT NULL)");
using (SqlCommand insert = new($"INSERT INTO {table.Name} (Value) VALUES (@value)", connection))
{
insert.Parameters.Add(new SqlParameter("@value", SqlDbType.NVarChar, -1) { Value = expected });
if (useAsync)
{
await insert.ExecuteNonQueryAsync();
}
else
{
insert.ExecuteNonQuery();
}
}

using SqlCommand select = new($"SELECT Value, CONVERT(varbinary(max), Value) FROM {table.Name}", connection);
using SqlDataReader reader = useAsync
? await select.ExecuteReaderAsync(CommandBehavior.SequentialAccess)
: select.ExecuteReader(CommandBehavior.SequentialAccess);
bool hasRow = useAsync ? await reader.ReadAsync() : reader.Read();

Assert.True(hasRow);
Assert.Equal(expected, reader.GetString(0));
Assert.Equal(Encoding.UTF8.GetBytes(expected), reader.GetFieldValue<byte[]>(1));
Assert.False(useAsync ? await reader.ReadAsync() : reader.Read());
}

/// <summary>
/// Reads a string through the standard buffered reader path.
/// </summary>
/// <param name="connection">The open SQL connection used to execute the query.</param>
/// <param name="query">The query that returns one string value.</param>
/// <param name="useAsync">Whether command and reader operations use asynchronous APIs.</param>
/// <returns>The string returned by SQL Server.</returns>
private static async Task<string> ReadDirectValue(SqlConnection connection, string query, bool useAsync)
{
using SqlCommand command = new(query, connection);
using SqlDataReader reader = useAsync
? await command.ExecuteReaderAsync()
: command.ExecuteReader();
bool hasRow = useAsync ? await reader.ReadAsync() : reader.Read();

Assert.True(hasRow);
string result = reader.GetString(0);
Assert.False(useAsync ? await reader.ReadAsync() : reader.Read());
return result;
}

/// <summary>
/// Reads a string through sequential <see cref="TextReader"/> calls with a small buffer so character
/// sequences cross read boundaries.
/// </summary>
/// <param name="connection">The open SQL connection used to execute the query.</param>
/// <param name="query">The query that returns one string value.</param>
/// <param name="useAsync">Whether command and reader operations use asynchronous APIs.</param>
/// <returns>The string returned by SQL Server.</returns>
private static async Task<string> ReadStreamedValue(SqlConnection connection, string query, bool useAsync)
{
using SqlCommand command = new(query, connection);
using SqlDataReader reader = useAsync
? await command.ExecuteReaderAsync(CommandBehavior.SequentialAccess)
: command.ExecuteReader(CommandBehavior.SequentialAccess);
bool hasRow = useAsync ? await reader.ReadAsync() : reader.Read();

Assert.True(hasRow);
using TextReader textReader = reader.GetTextReader(0);
char[] buffer = new char[3];
StringBuilder result = new();
int charsRead;
do
{
charsRead = useAsync
? await textReader.ReadAsync(buffer, 0, buffer.Length)
: textReader.Read(buffer, 0, buffer.Length);
result.Append(buffer, 0, charsRead);
}
while (charsRead != 0);

Assert.False(useAsync ? await reader.ReadAsync() : reader.Read());
return result.ToString();
}
}
}
Loading