From 2c69d298285faae7954de6854b794c8ea7e91ab0 Mon Sep 17 00:00:00 2001 From: Parminder Kaur Date: Mon, 30 Jan 2023 16:55:40 -0800 Subject: [PATCH 1/3] Backport: Fix CommandText length for stored procedures 1484 --- .../Microsoft/Data/SqlClient/SqlCommand.cs | 16 +++++++- .../Microsoft/Data/SqlClient/SqlCommand.cs | 15 ++++++- ....Data.SqlClient.ManualTesting.Tests.csproj | 1 + .../SqlCommand/SqlCommandStoredProcTest.cs | 39 +++++++++++++++++++ 4 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandStoredProcTest.cs diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs index f874d54b59..de8737cb6c 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs @@ -27,6 +27,7 @@ namespace Microsoft.Data.SqlClient public sealed partial class SqlCommand : DbCommand, ICloneable { private static int _objectTypeCount; // EventSource Counter + private const int MaxRPCNameLength = 1046; internal readonly int ObjectID = Interlocked.Increment(ref _objectTypeCount); private string _commandText; private static readonly Func s_beginExecuteReaderAsync = BeginExecuteReaderAsyncCallback; @@ -5784,7 +5785,20 @@ private void BuildRPC(bool inSchema, SqlParameterCollection parameters, ref _Sql GetRPCObject(0, userParameterCount, ref rpc); rpc.ProcID = 0; - rpc.rpcName = this.CommandText; // just get the raw command text + + // TDS Protocol allows rpc name with maximum length of 1046 bytes for ProcName + // 4-part name 1 + 128 + 1 + 1 + 1 + 128 + 1 + 1 + 1 + 128 + 1 + 1 + 1 + 128 + 1 = 523 + // each char takes 2 bytes. 523 * 2 = 1046 + int commandTextLength = ADP.CharSize * CommandText.Length; + + if (commandTextLength <= MaxRPCNameLength) + { + rpc.rpcName = CommandText; // just get the raw command text + } + else + { + throw ADP.InvalidArgumentLength(nameof(CommandText), MaxRPCNameLength); + } SetUpRPCParameters(rpc, inSchema, parameters); } diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.cs index ea2c94fb05..40c895a99f 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.cs @@ -38,6 +38,7 @@ namespace Microsoft.Data.SqlClient public sealed class SqlCommand : DbCommand, ICloneable { private static int _objectTypeCount; // EventSource Counter + private const int MaxRPCNameLength = 1046; internal readonly int ObjectID = System.Threading.Interlocked.Increment(ref _objectTypeCount); private string _commandText; @@ -6682,7 +6683,19 @@ private void BuildRPC(bool inSchema, SqlParameterCollection parameters, ref _Sql int count = CountSendableParameters(parameters); GetRPCObject(count, ref rpc); - rpc.rpcName = this.CommandText; // just get the raw command text + // TDS Protocol allows rpc name with maximum length of 1046 bytes for ProcName + // 4-part name 1 + 128 + 1 + 1 + 1 + 128 + 1 + 1 + 1 + 128 + 1 + 1 + 1 + 128 + 1 = 523 + // each char takes 2 bytes. 523 * 2 = 1046 + int commandTextLength = ADP.CharSize * CommandText.Length; + + if (commandTextLength <= MaxRPCNameLength) + { + rpc.rpcName = CommandText; // just get the raw command text + } + else + { + throw ADP.InvalidArgumentLength(nameof(CommandText), MaxRPCNameLength); + } SetUpRPCParameters(rpc, 0, inSchema, parameters); } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj index 0db90a4686..6da5a0d951 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj @@ -99,6 +99,7 @@ + diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandStoredProcTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandStoredProcTest.cs new file mode 100644 index 0000000000..1c26151d94 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandStoredProcTest.cs @@ -0,0 +1,39 @@ +// 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 Xunit; + +namespace Microsoft.Data.SqlClient.ManualTesting.Tests +{ + public class SqlCommandStoredProcTest + { + private static readonly string s_tcp_connStr = DataTestUtility.TCPConnectionString; + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse))] + public static void ShouldFailWithExceededLengthForSP() + { + string baseCommandText = "random text\u0000\u400a\u7300\u7400\u6100\u7400\u6500\u6d00\u6500\u6e00\u7400\u0000\u0006\u01ff\u0900\uf004\u0000\uffdc\u0001"; + string exceededLengthText = baseCommandText + new string(' ', 2000); + using SqlConnection conn = new(s_tcp_connStr); + conn.Open(); + using SqlCommand command = new() + { + Connection = conn, + CommandType = CommandType.StoredProcedure, + CommandText = exceededLengthText + }; + + // It should fail on the driver as the length of RPC is over 1046 + // 4-part name 1 + 128 + 1 + 1 + 1 + 128 + 1 + 1 + 1 + 128 + 1 + 1 + 1 + 128 + 1 = 523 + // each char takes 2 bytes. 523 * 2 = 1046 + Assert.Throws(() => command.ExecuteScalar()); + + command.CommandText = baseCommandText; + var ex = Assert.Throws(() => command.ExecuteScalar()); + Assert.StartsWith("Could not find stored procedure", ex.Message); + } + } +} From 6a67090a93b4248308f68959da8f1cdf70ed68a2 Mon Sep 17 00:00:00 2001 From: Parminder Kaur Date: Mon, 30 Jan 2023 17:20:59 -0800 Subject: [PATCH 2/3] Backport: Fix | Default UTF8 collation conflict (#1739) --- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 9 +-- .../SQL/Utf8SupportTest/Utf8SupportTest.cs | 62 ++++++++++++++++++- 2 files changed, 62 insertions(+), 9 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs index ccea8f1886..31a44e3221 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -3063,20 +3063,13 @@ private bool TryProcessEnvChange(int tokenLength, TdsParserStateObject stateObj, _defaultCollation = env.newCollation; _defaultLCID = env.newCollation.LCID; - int newCodePage = GetCodePage(env.newCollation, stateObj); - if ((env.newCollation.info & TdsEnums.UTF8_IN_TDSCOLLATION) == TdsEnums.UTF8_IN_TDSCOLLATION) { // UTF8 collation _defaultEncoding = Encoding.UTF8; - - if (newCodePage != _defaultCodePage) - { - _defaultCodePage = newCodePage; - } } else { - + int newCodePage = GetCodePage(env.newCollation, stateObj); if (newCodePage != _defaultCodePage) { _defaultCodePage = newCodePage; diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/Utf8SupportTest/Utf8SupportTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/Utf8SupportTest/Utf8SupportTest.cs index c6bc56d9f4..2a0cb77214 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/Utf8SupportTest/Utf8SupportTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/Utf8SupportTest/Utf8SupportTest.cs @@ -2,6 +2,8 @@ // 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.Text; using Xunit; namespace Microsoft.Data.SqlClient.ManualTesting.Tests @@ -29,6 +31,64 @@ public static void CheckSupportUtf8ConnectionProperty() } } - // TODO: Write tests using UTF8 collations + // skip creating database on Azure + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureServer), nameof(DataTestUtility.IsNotAzureSynapse))] + public static void UTF8databaseTest() + { + const string letters = @"!\#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\u007f€\u0081‚ƒ„…†‡ˆ‰Š‹Œ\u008dŽ\u008f\u0090‘’“”•–—˜™š›œ\u009džŸ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ"; + string dbName = DataTestUtility.GetUniqueNameForSqlServer("UTF8databaseTest", false); + string tblName = "Table1"; + + SqlConnectionStringBuilder builder = new(DataTestUtility.TCPConnectionString); + builder.InitialCatalog = "master"; + + using SqlConnection cn = new(builder.ConnectionString); + cn.Open(); + + try + { + PrepareDatabaseUTF8(cn, dbName, tblName, letters); + + builder.InitialCatalog = dbName; + using SqlConnection cnnTest = new(builder.ConnectionString); + // creating a databse is a time consumer action and could be retried. + SqlRetryLogicOption retryOption = new() { NumberOfTries = 3, DeltaTime = TimeSpan.FromMilliseconds(200) }; + cnnTest.RetryLogicProvider = SqlConfigurableRetryFactory.CreateIncrementalRetryProvider(retryOption); + cnnTest.Open(); + + using SqlCommand cmd = cnnTest.CreateCommand(); + cmd.CommandText = $"SELECT * FROM {tblName}"; + + using SqlDataReader reader = cmd.ExecuteReader(); + + Assert.True(reader.Read(), "The test table should have a row!"); + object[] data = new object[1]; + reader.GetSqlValues(data); + Assert.Equal(letters, data[0].ToString()); + reader.Close(); + cnnTest.Close(); + } + finally + { + DataTestUtility.DropDatabase(cn, dbName); + } + } + + private static void PrepareDatabaseUTF8(SqlConnection cnn, string dbName, string tblName, string letters) + { + StringBuilder sb = new(); + + using SqlCommand cmd = cnn.CreateCommand(); + + cmd.CommandText = $"CREATE DATABASE [{dbName}] COLLATE Latin1_General_100_CI_AS_SC_UTF8;"; + cmd.ExecuteNonQuery(); + + sb.AppendLine($"CREATE TABLE [{dbName}].dbo.[{tblName}] (col VARCHAR(7633) COLLATE Latin1_General_100_CI_AS_SC);"); + sb.AppendLine($"INSERT INTO [{dbName}].dbo.[{tblName}] VALUES (@letters);"); + + cmd.Parameters.Add(new SqlParameter("letters", letters)); + cmd.CommandText = sb.ToString(); + cmd.ExecuteNonQuery(); + } } } From b23760bf02d81a8a76baebd3d6e3af43730ea1ce Mon Sep 17 00:00:00 2001 From: Parminder Kaur Date: Mon, 30 Jan 2023 17:40:38 -0800 Subject: [PATCH 3/3] rolling back UTF8 changes (backported in seperate PR) --- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 9 ++- .../SQL/Utf8SupportTest/Utf8SupportTest.cs | 62 +------------------ 2 files changed, 9 insertions(+), 62 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs index 31a44e3221..ccea8f1886 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -3063,13 +3063,20 @@ private bool TryProcessEnvChange(int tokenLength, TdsParserStateObject stateObj, _defaultCollation = env.newCollation; _defaultLCID = env.newCollation.LCID; + int newCodePage = GetCodePage(env.newCollation, stateObj); + if ((env.newCollation.info & TdsEnums.UTF8_IN_TDSCOLLATION) == TdsEnums.UTF8_IN_TDSCOLLATION) { // UTF8 collation _defaultEncoding = Encoding.UTF8; + + if (newCodePage != _defaultCodePage) + { + _defaultCodePage = newCodePage; + } } else { - int newCodePage = GetCodePage(env.newCollation, stateObj); + if (newCodePage != _defaultCodePage) { _defaultCodePage = newCodePage; diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/Utf8SupportTest/Utf8SupportTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/Utf8SupportTest/Utf8SupportTest.cs index 2a0cb77214..c6bc56d9f4 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/Utf8SupportTest/Utf8SupportTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/Utf8SupportTest/Utf8SupportTest.cs @@ -2,8 +2,6 @@ // 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.Text; using Xunit; namespace Microsoft.Data.SqlClient.ManualTesting.Tests @@ -31,64 +29,6 @@ public static void CheckSupportUtf8ConnectionProperty() } } - // skip creating database on Azure - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureServer), nameof(DataTestUtility.IsNotAzureSynapse))] - public static void UTF8databaseTest() - { - const string letters = @"!\#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\u007f€\u0081‚ƒ„…†‡ˆ‰Š‹Œ\u008dŽ\u008f\u0090‘’“”•–—˜™š›œ\u009džŸ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ"; - string dbName = DataTestUtility.GetUniqueNameForSqlServer("UTF8databaseTest", false); - string tblName = "Table1"; - - SqlConnectionStringBuilder builder = new(DataTestUtility.TCPConnectionString); - builder.InitialCatalog = "master"; - - using SqlConnection cn = new(builder.ConnectionString); - cn.Open(); - - try - { - PrepareDatabaseUTF8(cn, dbName, tblName, letters); - - builder.InitialCatalog = dbName; - using SqlConnection cnnTest = new(builder.ConnectionString); - // creating a databse is a time consumer action and could be retried. - SqlRetryLogicOption retryOption = new() { NumberOfTries = 3, DeltaTime = TimeSpan.FromMilliseconds(200) }; - cnnTest.RetryLogicProvider = SqlConfigurableRetryFactory.CreateIncrementalRetryProvider(retryOption); - cnnTest.Open(); - - using SqlCommand cmd = cnnTest.CreateCommand(); - cmd.CommandText = $"SELECT * FROM {tblName}"; - - using SqlDataReader reader = cmd.ExecuteReader(); - - Assert.True(reader.Read(), "The test table should have a row!"); - object[] data = new object[1]; - reader.GetSqlValues(data); - Assert.Equal(letters, data[0].ToString()); - reader.Close(); - cnnTest.Close(); - } - finally - { - DataTestUtility.DropDatabase(cn, dbName); - } - } - - private static void PrepareDatabaseUTF8(SqlConnection cnn, string dbName, string tblName, string letters) - { - StringBuilder sb = new(); - - using SqlCommand cmd = cnn.CreateCommand(); - - cmd.CommandText = $"CREATE DATABASE [{dbName}] COLLATE Latin1_General_100_CI_AS_SC_UTF8;"; - cmd.ExecuteNonQuery(); - - sb.AppendLine($"CREATE TABLE [{dbName}].dbo.[{tblName}] (col VARCHAR(7633) COLLATE Latin1_General_100_CI_AS_SC);"); - sb.AppendLine($"INSERT INTO [{dbName}].dbo.[{tblName}] VALUES (@letters);"); - - cmd.Parameters.Add(new SqlParameter("letters", letters)); - cmd.CommandText = sb.ToString(); - cmd.ExecuteNonQuery(); - } + // TODO: Write tests using UTF8 collations } }