diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataReaderTest/DataReaderTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataReaderTest/DataReaderTest.cs index 040119616e..5cf8b1e4b4 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataReaderTest/DataReaderTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataReaderTest/DataReaderTest.cs @@ -125,49 +125,88 @@ public static void CheckSparseColumnBit() } } - // Synapse: Statement 'Drop Database' is not supported in this version of SQL Server. - [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureServer), nameof(DataTestUtility.IsNotAzureSynapse))] - [InlineData("KAZAKH_90_CI_AI")] - [InlineData("Georgian_Modern_Sort_CI_AS")] - public static void CollatedDataReaderTest(string collation) + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public static void CollatedDataReaderTest() { - string dbName = DataTestUtility.GetShortName("CollationTest", false); - - SqlConnectionStringBuilder builder = new(DataTestUtility.TCPConnectionString) + const string SampleText = "Text with an accented é varies by encoding"; + const string CollatedStringCommandText = $@"declare c cursor for + select name + from fn_helpcollations() +declare @collation nvarchar(max) + +open c +fetch next from c into @collation + +while @@FETCH_STATUS = 0 +begin + declare @sql nvarchar(max) = N'select @collation as [Collation], + convert(int, COLLATIONPROPERTY(@collation, ''LCID'')) & 0xFFFF as [LanguageId], + convert(int, COLLATIONPROPERTY(@collation, ''LCID'')) as [LCID], + convert(int, COLLATIONPROPERTY(@collation, ''CodePage'')) as [CodePage], + @Text collate ' + @collation + ' as [Text], + convert(varbinary(max), @Text collate ' + @collation + ')' + + begin try + exec sp_executesql @sql, N'@collation nvarchar(max), @Text varchar(max)', @Text='{SampleText}', @collation=@collation + end try + begin catch + -- Error 459: Collation '%.*ls' is supported on Unicode data types only and cannot be applied to char, varchar or text data types. + if error_number() != 459 + begin + throw + end + end catch + + fetch next from c into @collation +end + +close c +deallocate c"; + + using SqlConnection conn = new(DataTestUtility.TCPConnectionString); + using SqlCommand collatedStringCommand = new(CollatedStringCommandText, conn); + + conn.Open(); + using SqlDataReader reader = collatedStringCommand.ExecuteReader(); + + // We receive one result set per collation, with identical columns: + // 1. Collation name + // 2. Language ID (the LCID without any flags) + // 3. LCID (with flags) + // 4. ID of the code page used to decode the byte array + // 5. The collated string itself + // 6. The collated string, converted to a byte array within SQL Server + do { - InitialCatalog = dbName, - Pooling = false - }; + reader.Read(); - using SqlConnection con = new(DataTestUtility.TCPConnectionString); - using SqlCommand cmd = con.CreateCommand(); - try - { - con.Open(); + string collationName = reader.GetString(0); + int languageId = reader.GetInt32(1); + int lcid = reader.GetInt32(2); + int codePageId = reader.GetInt32(3); + string collatedString = reader.GetString(4); + byte[] collatedStringBytes = reader.GetSqlBinary(5).Value; - // Create collated database - cmd.CommandText = $"CREATE DATABASE [{dbName}] COLLATE {collation}"; - cmd.ExecuteNonQuery(); + // The code page's encoding must exist. We must then be able to round-trip the string + // to and from a byte array. + Encoding codePageEncoding = Encoding.GetEncoding(codePageId); - //Create connection without pooling in order to delete database later. - using (SqlConnection dbCon = new(builder.ConnectionString)) - using (SqlCommand dbCmd = dbCon.CreateCommand()) - { - string data = Guid.NewGuid().ToString(); + Assert.True(codePageEncoding is not null, + $@"Collation ""{collationName}"", LCID {lcid}, code page {codePageId} is not identifiable as a client-side encoding."); - dbCon.Open(); - dbCmd.CommandText = $"SELECT '{data}'"; - using SqlDataReader reader = dbCmd.ExecuteReader(); - reader.Read(); - Assert.Equal(data, reader.GetString(0)); - } - } - finally - { - // Let connection close safely before dropping database for slow servers. - Thread.Sleep(500); - DataTestUtility.DropDatabase(con, dbName); + string clientSideDecodedString = codePageEncoding.GetString(collatedStringBytes); + byte[] clientSideStringBytes = codePageEncoding.GetBytes(collatedString); + + Assert.True(collatedString == clientSideDecodedString, + $@"Collation ""{collationName}"", LCID {lcid}, code page {codePageId}: server-supplied string does not match client-side decoded bytes."); + + Assert.True(collatedStringBytes.AsSpan().SequenceEqual(clientSideStringBytes), + $@"Collation ""{collationName}"", LCID {lcid}, code page {codePageId}: server-supplied byte array does not match client-side encoded bytes."); + + // The character é does not exist in the Cyrillic character set, so do not compare the + // collated string with the original input text. } + while (reader.NextResult()); } private static bool IsColumnBitSet(SqlConnection con, string selectQuery, int indexOfColumnSet) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/OutputParameterTests.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/OutputParameterTests.cs index 98e51b812d..537313792e 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/OutputParameterTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/OutputParameterTests.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Data; +using System.Text; using Xunit; namespace Microsoft.Data.SqlClient.ManualTesting.Tests @@ -12,6 +13,16 @@ namespace Microsoft.Data.SqlClient.ManualTesting.Tests /// public class OutputParameterTests { + /// + /// Test data indicating which collations do not encode the character é to 0xE9. + /// + public static TheoryData OutputParameterCodePages => + // Code page 936 and 65001/UTF8 do not encode "é" to 0xE9. CP936 encodes it to [0xA8, 0xA6], UTF8 encodes it to [0xC3, 0xA9] + // Chinese_PRC_CI_AI and Albanian_100_CI_AI_SC_UTF8 are the alphabetically first collations which use these two code pages. + DataTestUtility.IsUTF8Supported() + ? new() { { "Chinese_PRC_CI_AI", 936 }, { "Albanian_100_CI_AI_SC_UTF8", 65001 } } + : new() { { "Chinese_PRC_CI_AI", 936 } }; + /// /// Tests that setting an output SqlParameter to an invalid value (e.g. a string in a decimal param) /// doesn't throw, since the value is cleared before execution starts. @@ -48,5 +59,41 @@ public void InvalidValueInOutputParameter_ShouldSucceed() // Validate - the output value should be set correctly by SQL Server Assert.Equal(new decimal(1.23), (decimal)decimalParam.Value); } + + /// + /// Tests that text with sample collations roundtrips. + /// + /// Name of a SQL Server collation which encodes text in the given code page. + /// ID of the codepage which should be used by SQL Server and the driver to encode and decode text. + [Theory] + [MemberData(nameof(OutputParameterCodePages))] + public void CollatedStringInOutputParameter_DecodesSuccessfully(string collation, int codePage) + { + const string SampleText = "Text with an accented é varies by encoding"; + + using SqlConnection sqlConnection = new(DataTestUtility.TCPConnectionString); + using SqlCommand roundtripCollationCommand = new($"SELECT @Output_Varchar = convert(varchar(max), '{SampleText}') COLLATE {collation}, " + + $"@Output_Varbinary = convert(varbinary(max), convert(varchar(max), '{SampleText}') COLLATE {collation})", sqlConnection); + SqlParameter outputVarcharParameter = new("@Output_Varchar", SqlDbType.VarChar, 8000) + { Direction = ParameterDirection.Output }; + SqlParameter outputVarbinaryParameter = new("@Output_Varbinary", SqlDbType.VarBinary, 8000) + { Direction = ParameterDirection.Output }; + Encoding codePageEncoding = Encoding.GetEncoding(codePage); + + roundtripCollationCommand.Parameters.Add(outputVarcharParameter); + roundtripCollationCommand.Parameters.Add(outputVarbinaryParameter); + + sqlConnection.Open(); + roundtripCollationCommand.ExecuteNonQuery(); + + string clientSideDecodedString = codePageEncoding.GetString((byte[])outputVarbinaryParameter.Value); + byte[] clientSideStringBytes = codePageEncoding.GetBytes(outputVarcharParameter.Value.ToString()); + + // Verify that the varchar value has been decoded correctly and matches the sample text, + // then verify that the varbinary value roundtrips properly. + Assert.Equal(SampleText, outputVarcharParameter.Value.ToString()); + Assert.Equal(outputVarcharParameter.Value.ToString(), clientSideDecodedString); + Assert.Equal((byte[])outputVarbinaryParameter.Value, clientSideStringBytes); + } } }