From 1e3a14193f3f16bb53e6b849f36a23f504601d33 Mon Sep 17 00:00:00 2001 From: Wraith2 Date: Sun, 22 Sep 2019 15:49:41 +0100 Subject: [PATCH 1/6] port rpc changes from corefx --- .../Microsoft/Data/SqlClient/SqlCommand.cs | 291 +++--- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 927 +++++++++--------- .../Data/SqlClient/TdsParserHelperClasses.cs | 32 +- 3 files changed, 663 insertions(+), 587 deletions(-) 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 29ce7ec2e5..c73b1e6380 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 @@ -3372,7 +3372,7 @@ private SqlDataReader TryFetchInputParameterEncryptionInfo(int timeout, { // In BatchRPCMode, the actual T-SQL query is in the first parameter and not present as the rpcName, as is the case with non-BatchRPCMode. // So input parameters start at parameters[1]. parameters[0] is the actual T-SQL Statement. rpcName is sp_executesql. - if (_SqlRPCBatchArray[i].parameters.Length > 1) + if (_SqlRPCBatchArray[i].systemParams.Length > 1) { _SqlRPCBatchArray[i].needsFetchParameterEncryptionMetadata = true; @@ -3417,20 +3417,11 @@ private SqlDataReader TryFetchInputParameterEncryptionInfo(int timeout, _sqlRPCParameterEncryptionReqArray = new _SqlRPC[1]; _SqlRPC rpc = null; - GetRPCObject(GetParameterCount(_parameters), ref rpc); + GetRPCObject(0, GetParameterCount(_parameters), ref rpc); Debug.Assert(rpc != null, "GetRPCObject should not return rpc as null."); rpc.rpcName = CommandText; - - int i = 0; - - if (_parameters != null) - { - foreach (SqlParameter sqlParam in _parameters) - { - rpc.parameters[i++] = sqlParam; - } - } + rpc.userParams = _parameters; // Prepare the RPC request for describe parameter encryption procedure. PrepareDescribeParameterEncryptionRequest(rpc, ref _sqlRPCParameterEncryptionReqArray[0], serializedAttestatationParameters); @@ -3480,7 +3471,7 @@ private void PrepareDescribeParameterEncryptionRequest(_SqlRPC originalRpcReques // Construct the RPC request for sp_describe_parameter_encryption // sp_describe_parameter_encryption always has 2 parameters (stmt, paramlist). // sp_describe_parameter_encryption can have an optional 3rd parameter (attestationParametes), used to identify and execute attestation protocol - GetRPCObject(attestationParameters == null ? 2 : 3, ref describeParameterEncryptionRequest, forSpDescribeParameterEncryption: true); + GetRPCObject(attestationParameters == null ? 2 : 3, 0, ref describeParameterEncryptionRequest, forSpDescribeParameterEncryption: true); describeParameterEncryptionRequest.rpcName = "sp_describe_parameter_encryption"; // Prepare @tsql parameter @@ -3490,9 +3481,9 @@ private void PrepareDescribeParameterEncryptionRequest(_SqlRPC originalRpcReques // In BatchRPCMode, The actual T-SQL query is in the first parameter and not present as the rpcName, as is the case with non-BatchRPCMode. if (BatchRPCMode) { - Debug.Assert(originalRpcRequest.parameters != null && originalRpcRequest.parameters.Length > 0, + Debug.Assert(originalRpcRequest.systemParamCount > 0, "originalRpcRequest didn't have at-least 1 parameter in BatchRPCMode, in PrepareDescribeParameterEncryptionRequest."); - text = (string)originalRpcRequest.parameters[0].Value; + text = (string)originalRpcRequest.systemParams[0].Value; sqlParam = GetSqlParameterWithQueryText(text); } else @@ -3502,7 +3493,7 @@ private void PrepareDescribeParameterEncryptionRequest(_SqlRPC originalRpcReques { // For stored procedures, we need to prepare @tsql in the following format // N'EXEC sp_name @param1=@param1, @param1=@param2, ..., @paramN=@paramN' - sqlParam = BuildStoredProcedureStatementForColumnEncryption(text, originalRpcRequest.parameters); + sqlParam = BuildStoredProcedureStatementForColumnEncryption(text, originalRpcRequest.userParams); } else { @@ -3512,32 +3503,45 @@ private void PrepareDescribeParameterEncryptionRequest(_SqlRPC originalRpcReques Debug.Assert(text != null, "@tsql parameter is null in PrepareDescribeParameterEncryptionRequest."); - describeParameterEncryptionRequest.parameters[0] = sqlParam; + describeParameterEncryptionRequest.systemParams[0] = sqlParam; string parameterList = null; // In BatchRPCMode, the input parameters start at parameters[1]. parameters[0] is the T-SQL statement. rpcName is sp_executesql. // And it is already in the format expected out of BuildParamList, which is not the case with Non-BatchRPCMode. if (BatchRPCMode) { - if (originalRpcRequest.parameters.Length > 1) + // systemParamCount == 2 when user parameters are supplied to BuildExcucuteSql + if (originalRpcRequest.systemParamCount > 1) { - parameterList = (string)originalRpcRequest.parameters[1].Value; + parameterList = (string)originalRpcRequest.systemParams[1].Value; } } else { // Prepare @params parameter // Need to create new parameters as we cannot have the same parameter being part of two SqlCommand objects - SqlParameter paramCopy; SqlParameterCollection tempCollection = new SqlParameterCollection(); - if (_parameters != null) - { - for (int i = 0; i < _parameters.Count; i++) - { - SqlParameter param = originalRpcRequest.parameters[i]; - paramCopy = new SqlParameter(param.ParameterName, param.SqlDbType, param.Size, param.Direction, param.Precision, param.Scale, param.SourceColumn, param.SourceVersion, - param.SourceColumnNullMapping, param.Value, param.XmlSchemaCollectionDatabase, param.XmlSchemaCollectionOwningSchema, param.XmlSchemaCollectionName); + if (originalRpcRequest.userParams != null) + { + for (int i = 0; i < originalRpcRequest.userParams.Count; i++) + { + SqlParameter param = originalRpcRequest.userParams[i]; + SqlParameter paramCopy = new SqlParameter( + param.ParameterName, + param.SqlDbType, + param.Size, + param.Direction, + param.Precision, + param.Scale, + param.SourceColumn, + param.SourceVersion, + param.SourceColumnNullMapping, + param.Value, + param.XmlSchemaCollectionDatabase, + param.XmlSchemaCollectionOwningSchema, + param.XmlSchemaCollectionName + ); paramCopy.CompareInfo = param.CompareInfo; paramCopy.TypeName = param.TypeName; paramCopy.UdtTypeName = param.UdtTypeName; @@ -3568,7 +3572,8 @@ private void PrepareDescribeParameterEncryptionRequest(_SqlRPC originalRpcReques sqlParam = new SqlParameter(null, ((parameterList.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT) ? SqlDbType.NVarChar : SqlDbType.NText, parameterList.Length); sqlParam.Value = parameterList; - describeParameterEncryptionRequest.parameters[1] = sqlParam; + describeParameterEncryptionRequest.systemParams[1] = sqlParam; + describeParameterEncryptionRequest.systemParamCount = 2; if (attestationParameters != null) { @@ -3579,7 +3584,8 @@ private void PrepareDescribeParameterEncryptionRequest(_SqlRPC originalRpcReques Value = attestationParameters }; - describeParameterEncryptionRequest.parameters[2] = attestationParametersParam; + describeParameterEncryptionRequest.systemParams[2] = attestationParametersParam; + describeParameterEncryptionRequest.systemParamCount = 3; } } @@ -3729,8 +3735,7 @@ private void ReadDescribeEncryptionParameterResults(SqlDataReader ds, ReadOnlyDi throw SQL.UnexpectedDescribeParamFormatParameterMetadata(); } - int paramIdx = 0; - int parameterStartIndex = 0; + // Find the RPC command that generated this tce request if (BatchRPCMode) @@ -3754,13 +3759,11 @@ private void ReadDescribeEncryptionParameterResults(SqlDataReader ds, ReadOnlyDi Debug.Assert(rpc != null, "rpc should not be null here."); - // This is the index in the parameters array where the actual parameters start. - // In BatchRPCMode, parameters[0] has the t-sql, parameters[1] has the param list - // and actual parameters of the query start at parameters[2]. - parameterStartIndex = (BatchRPCMode ? 2 : 0); - + int userParamCount = rpc.userParams.Count; + int recievedMetadataCount = 0; if (!enclaveMetadataExists || ds.NextResult()) { + // Iterate over the parameter names to read the encryption type info while (ds.Read()) { @@ -3772,16 +3775,17 @@ private void ReadDescribeEncryptionParameterResults(SqlDataReader ds, ReadOnlyDi // When the RPC object gets reused, the parameter array has more parameters that the valid params for the command. // Null is used to indicate the end of the valid part of the array. Refer to GetRPCObject(). - for (paramIdx = parameterStartIndex; paramIdx < rpc.parameters.Length && rpc.parameters[paramIdx] != null; paramIdx++) + + for (int index = 0; index < userParamCount; index++) { - SqlParameter sqlParameter = rpc.parameters[paramIdx]; + SqlParameter sqlParameter = rpc.userParams[index]; Debug.Assert(sqlParameter != null, "sqlParameter should not be null."); if (sqlParameter.ParameterNameFixed.Equals(parameterName, StringComparison.Ordinal)) { Debug.Assert(sqlParameter.CipherMetadata == null, "param.CipherMetadata should be null."); sqlParameter.HasReceivedMetadata = true; - + recievedMetadataCount += 1; // Found the param, setup the encryption info. byte columnEncryptionType = ds.GetByte((int)DescribeParameterEncryptionResultSet2.ColumnEncrytionType); if ((byte)SqlClientEncryptionType.PlainText != columnEncryptionType) @@ -3809,7 +3813,9 @@ private void ReadDescribeEncryptionParameterResults(SqlDataReader ds, ReadOnlyDi // This is effective only for BatchRPCMode even though we set it for non-BatchRPCMode also, // since for non-BatchRPCMode mode, paramoptions gets thrown away and reconstructed in BuildExecuteSql. - rpc.paramoptions[paramIdx] |= TdsEnums.RPC_PARAM_ENCRYPTED; + int options = (int)(rpc.userParamMap[index] >> 32); + options |= TdsEnums.RPC_PARAM_ENCRYPTED; + rpc.userParamMap[index] = ((((long)options) << 32) | (long)index); } break; @@ -3820,15 +3826,20 @@ private void ReadDescribeEncryptionParameterResults(SqlDataReader ds, ReadOnlyDi // When the RPC object gets reused, the parameter array has more parameters that the valid params for the command. // Null is used to indicate the end of the valid part of the array. Refer to GetRPCObject(). - for (paramIdx = parameterStartIndex; paramIdx < rpc.parameters.Length && rpc.parameters[paramIdx] != null; paramIdx++) + + if (recievedMetadataCount != userParamCount) { - if (!rpc.parameters[paramIdx].HasReceivedMetadata && rpc.parameters[paramIdx].Direction != ParameterDirection.ReturnValue) + for (int index = 0; index < userParamCount; index++) { - // Encryption MD wasn't sent by the server - we expect the metadata to be sent for all the parameters - // that were sent in the original sp_describe_parameter_encryption but not necessarily for return values, - // since there might be multiple return values but server will only send for one of them. - // For parameters that don't need encryption, the encryption type is set to plaintext. - throw SQL.ParamEncryptionMetadataMissing(rpc.parameters[paramIdx].ParameterName, rpc.GetCommandTextOrRpcName()); + SqlParameter sqlParameter = rpc.userParams[index]; + if (!sqlParameter.HasReceivedMetadata && sqlParameter.Direction != ParameterDirection.ReturnValue) + { + // Encryption MD wasn't sent by the server - we expect the metadata to be sent for all the parameters + // that were sent in the original sp_describe_parameter_encryption but not necessarily for return values, + // since there might be multiple return values but server will only send for one of them. + // For parameters that don't need encryption, the encryption type is set to plaintext. + throw SQL.ParamEncryptionMetadataMissing(sqlParameter.ParameterName, rpc.GetCommandTextOrRpcName()); + } } } @@ -4223,7 +4234,7 @@ private SqlDataReader RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavi Debug.Assert(_SqlRPCBatchArray != null, "RunExecuteReader rpc array not provided"); writeTask = _stateObj.Parser.TdsExecuteRPC(this, _SqlRPCBatchArray, timeout, inSchema, this.Notification, _stateObj, CommandType.StoredProcedure == CommandType, sync: !asyncWrite); } - else if ((System.Data.CommandType.Text == this.CommandType) && (0 == GetParameterCount(_parameters))) + else if ((CommandType.Text == this.CommandType) && (0 == GetParameterCount(_parameters))) { // Send over SQL Batch command if we are not a stored proc and have no parameters Debug.Assert(!IsUserPrepared, "CommandType.Text with no params should not be prepared!"); @@ -5066,10 +5077,9 @@ private SqlParameter GetParameterForOutputValueExtraction(SqlParameterCollection return null; } - private void GetRPCObject(int paramCount, ref _SqlRPC rpc, bool forSpDescribeParameterEncryption = false) + private void GetRPCObject(int systemParamCount, int userParamCount, ref _SqlRPC rpc, bool forSpDescribeParameterEncryption = false) { // Designed to minimize necessary allocations - int ii; if (rpc == null) { if (!forSpDescribeParameterEncryption) @@ -5096,40 +5106,42 @@ private void GetRPCObject(int paramCount, ref _SqlRPC rpc, bool forSpDescribePar rpc.ProcID = 0; rpc.rpcName = null; rpc.options = 0; + rpc.systemParamCount = systemParamCount; rpc.needsFetchParameterEncryptionMetadata = false; - + int currentCount = rpc.systemParams?.Length ?? 0; // Make sure there is enough space in the parameters and paramoptions arrays - if (rpc.parameters == null || rpc.parameters.Length < paramCount) - { - rpc.parameters = new SqlParameter[paramCount]; - } - else if (rpc.parameters.Length > paramCount) + + if (currentCount < systemParamCount) { - rpc.parameters[paramCount] = null; // Terminator + Array.Resize(ref rpc.systemParams, systemParamCount); + Array.Resize(ref rpc.systemParamOptions, systemParamCount); + for (int index = currentCount; index < systemParamCount; index++) + { + rpc.systemParams[index] = new SqlParameter(); + } } - if (rpc.paramoptions == null || (rpc.paramoptions.Length < paramCount)) + + for (int ii = 0; ii < systemParamCount; ii++) { - rpc.paramoptions = new byte[paramCount]; + rpc.systemParamOptions[ii] = 0; } - else + + if ((rpc.userParamMap?.Length ?? 0) < userParamCount) { - for (ii = 0; ii < paramCount; ii++) - rpc.paramoptions[ii] = 0; + Array.Resize(ref rpc.userParamMap, userParamCount); } } - private void SetUpRPCParameters(_SqlRPC rpc, int startCount, bool inSchema, SqlParameterCollection parameters) + private void SetUpRPCParameters(_SqlRPC rpc, bool inSchema, SqlParameterCollection parameters) { - int ii; int paramCount = GetParameterCount(parameters); - int j = startCount; - TdsParser parser = _activeConnection.Parser; + int userParamCount = 0; - for (ii = 0; ii < paramCount; ii++) + for (int index = 0; index < paramCount; index++) { - SqlParameter parameter = parameters[ii]; - parameter.Validate(ii, CommandType.StoredProcedure == CommandType); + SqlParameter parameter = parameters[index]; + parameter.Validate(index, CommandType.StoredProcedure == CommandType); // func will change type to that with a 4 byte length if the type has a two // byte length and a parameter length > than that expressible in 2 bytes @@ -5140,20 +5152,23 @@ private void SetUpRPCParameters(_SqlRPC rpc, int startCount, bool inSchema, SqlP if (ShouldSendParameter(parameter)) { - rpc.parameters[j] = parameter; + byte options = 0; // set output bit - if (parameter.Direction == ParameterDirection.InputOutput || - parameter.Direction == ParameterDirection.Output) - rpc.paramoptions[j] = TdsEnums.RPC_PARAM_BYREF; + if ( + parameter.Direction == ParameterDirection.InputOutput || + parameter.Direction == ParameterDirection.Output + ) + { + options = TdsEnums.RPC_PARAM_BYREF; + } // Set the encryped bit, if the parameter is to be encrypted. if (parameter.CipherMetadata != null) { - rpc.paramoptions[j] |= TdsEnums.RPC_PARAM_ENCRYPTED; + options |= TdsEnums.RPC_PARAM_ENCRYPTED; } - // set default value bit if (parameter.Direction != ParameterDirection.Output) { @@ -5164,50 +5179,57 @@ private void SetUpRPCParameters(_SqlRPC rpc, int startCount, bool inSchema, SqlP // TVPs use DEFAULT and do not allow NULL, even for schema only. if (null == parameter.Value && (!inSchema || SqlDbType.Structured == parameter.SqlDbType)) { - rpc.paramoptions[j] |= TdsEnums.RPC_PARAM_DEFAULT; + options |= TdsEnums.RPC_PARAM_DEFAULT; } } + rpc.userParamMap[userParamCount] = ((((long)options) << 32) | (long)index); + userParamCount += 1; // Must set parameter option bit for LOB_COOKIE if unfilled LazyMat blob - j++; } } + + rpc.userParamCount = userParamCount; + rpc.userParams = parameters; } private _SqlRPC BuildPrepExec(CommandBehavior behavior) { Debug.Assert(System.Data.CommandType.Text == this.CommandType, "invalid use of sp_prepexec for stored proc invocation!"); SqlParameter sqlParam; - int j = 3; - int count = CountSendableParameters(_parameters); + const int systemParameterCount = 3; + int userParameterCount = CountSendableParameters(_parameters); _SqlRPC rpc = null; - GetRPCObject(count + j, ref rpc); + GetRPCObject(systemParameterCount, userParameterCount, ref rpc); rpc.ProcID = TdsEnums.RPC_PROCID_PREPEXEC; rpc.rpcName = TdsEnums.SP_PREPEXEC; //@handle - sqlParam = new SqlParameter(null, SqlDbType.Int); - sqlParam.Direction = ParameterDirection.InputOutput; + sqlParam = rpc.systemParams[0]; + sqlParam.SqlDbType = SqlDbType.Int; sqlParam.Value = _prepareHandle; - rpc.parameters[0] = sqlParam; - rpc.paramoptions[0] = TdsEnums.RPC_PARAM_BYREF; + sqlParam.Size = 4; + sqlParam.Direction = ParameterDirection.InputOutput; + rpc.systemParamOptions[0] = TdsEnums.RPC_PARAM_BYREF; //@batch_params string paramList = BuildParamList(_stateObj.Parser, _parameters); - sqlParam = new SqlParameter(null, ((paramList.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT) ? SqlDbType.NVarChar : SqlDbType.NText, paramList.Length); + sqlParam = rpc.systemParams[1]; + sqlParam.SqlDbType = ((paramList.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT) ? SqlDbType.NVarChar : SqlDbType.NText; sqlParam.Value = paramList; - rpc.parameters[1] = sqlParam; + sqlParam.Size = paramList.Length; //@batch_text string text = GetCommandText(behavior); - sqlParam = new SqlParameter(null, ((text.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT) ? SqlDbType.NVarChar : SqlDbType.NText, text.Length); + sqlParam = rpc.systemParams[2]; + sqlParam.SqlDbType = ((text.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT) ? SqlDbType.NVarChar : SqlDbType.NText; + sqlParam.Size = text.Length; sqlParam.Value = text; - rpc.parameters[2] = sqlParam; - SetUpRPCParameters(rpc, j, false, _parameters); + SetUpRPCParameters(rpc, false, _parameters); return rpc; } @@ -5244,7 +5266,9 @@ private int CountSendableParameters(SqlParameterCollection parameters) for (int i = 0; i < count; i++) { if (ShouldSendParameter(parameters[i])) + { cParams++; + } } } return cParams; @@ -5253,21 +5277,24 @@ private int CountSendableParameters(SqlParameterCollection parameters) // Returns total number of parameters private int GetParameterCount(SqlParameterCollection parameters) { - return ((null != parameters) ? parameters.Count : 0); + return (null != parameters) ? parameters.Count : 0; } + + // // build the RPC record header for this stored proc and add parameters // private void BuildRPC(bool inSchema, SqlParameterCollection parameters, ref _SqlRPC rpc) { Debug.Assert(this.CommandType == System.Data.CommandType.StoredProcedure, "Command must be a stored proc to execute an RPC"); - int count = CountSendableParameters(parameters); - GetRPCObject(count, ref rpc); + int userParameterCount = CountSendableParameters(parameters); + GetRPCObject(0, userParameterCount, ref rpc); + rpc.ProcID = 0; rpc.rpcName = this.CommandText; // just get the raw command text - SetUpRPCParameters(rpc, 0, inSchema, parameters); + SetUpRPCParameters(rpc, inSchema, parameters); } // @@ -5284,24 +5311,23 @@ private void BuildRPC(bool inSchema, SqlParameterCollection parameters, ref _Sql private _SqlRPC BuildExecute(bool inSchema) { Debug.Assert((int)_prepareHandle != -1, "Invalid call to sp_execute without a valid handle!"); - int j = 1; + const int systemParameterCount = 1; - int count = CountSendableParameters(_parameters); + int userParameterCount = CountSendableParameters(_parameters); _SqlRPC rpc = null; - GetRPCObject(count + j, ref rpc); - - SqlParameter sqlParam; + GetRPCObject(systemParameterCount, userParameterCount, ref rpc); rpc.ProcID = TdsEnums.RPC_PROCID_EXECUTE; rpc.rpcName = TdsEnums.SP_EXECUTE; //@handle - sqlParam = new SqlParameter(null, SqlDbType.Int); + SqlParameter sqlParam = rpc.systemParams[0]; + sqlParam.SqlDbType = SqlDbType.Int; sqlParam.Value = _prepareHandle; - rpc.parameters[0] = sqlParam; + sqlParam.Direction = ParameterDirection.Input; - SetUpRPCParameters(rpc, j, inSchema, _parameters); + SetUpRPCParameters(rpc, inSchema, _parameters); return rpc; } @@ -5315,20 +5341,20 @@ private void BuildExecuteSql(CommandBehavior behavior, string commandText, SqlPa Debug.Assert((int)_prepareHandle == -1, "This command has an existing handle, use sp_execute!"); Debug.Assert(CommandType.Text == this.CommandType, "invalid use of sp_executesql for stored proc invocation!"); - int j; + int systemParamCount; SqlParameter sqlParam; - int cParams = CountSendableParameters(parameters); - if (cParams > 0) + int userParamCount = CountSendableParameters(parameters); + if (userParamCount > 0) { - j = 2; + systemParamCount = 2; } else { - j = 1; + systemParamCount = 1; } - GetRPCObject(cParams + j, ref rpc); + GetRPCObject(systemParamCount, userParamCount, ref rpc); rpc.ProcID = TdsEnums.RPC_PROCID_EXECUTESQL; rpc.rpcName = TdsEnums.SP_EXECUTESQL; @@ -5337,19 +5363,22 @@ private void BuildExecuteSql(CommandBehavior behavior, string commandText, SqlPa { commandText = GetCommandText(behavior); } - sqlParam = new SqlParameter(null, ((commandText.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT) ? SqlDbType.NVarChar : SqlDbType.NText, commandText.Length); + sqlParam = rpc.systemParams[0]; + sqlParam.SqlDbType = ((commandText.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT) ? SqlDbType.NVarChar : SqlDbType.NText; + sqlParam.Size = commandText.Length; sqlParam.Value = commandText; - rpc.parameters[0] = sqlParam; + sqlParam.Direction = ParameterDirection.Input; - if (cParams > 0) + if (userParamCount > 0) { string paramList = BuildParamList(_stateObj.Parser, BatchRPCMode ? parameters : _parameters); - sqlParam = new SqlParameter(null, ((paramList.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT) ? SqlDbType.NVarChar : SqlDbType.NText, paramList.Length); + sqlParam = rpc.systemParams[1]; + sqlParam.SqlDbType = ((paramList.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT) ? SqlDbType.NVarChar : SqlDbType.NText; + sqlParam.Size = paramList.Length; sqlParam.Value = paramList; - rpc.parameters[1] = sqlParam; bool inSchema = (0 != (behavior & CommandBehavior.SchemaOnly)); - SetUpRPCParameters(rpc, j, inSchema, parameters); + SetUpRPCParameters(rpc, inSchema, parameters); } } @@ -5361,7 +5390,7 @@ private void BuildExecuteSql(CommandBehavior behavior, string commandText, SqlPa /// Stored procedure name /// SqlParameter list /// A string SqlParameter containing the constructed sql statement value - private SqlParameter BuildStoredProcedureStatementForColumnEncryption(string storedProcedureName, SqlParameter[] parameters) + private SqlParameter BuildStoredProcedureStatementForColumnEncryption(string storedProcedureName, SqlParameterCollection parameters) { Debug.Assert(CommandType == CommandType.StoredProcedure, "BuildStoredProcedureStatementForColumnEncryption() should only be called for stored procedures"); Debug.Assert(!string.IsNullOrWhiteSpace(storedProcedureName), "storedProcedureName cannot be null or empty in BuildStoredProcedureStatementForColumnEncryption"); @@ -5394,27 +5423,27 @@ private SqlParameter BuildStoredProcedureStatementForColumnEncryption(string sto // @param1=@param1, @param1=@param2, ..., @paramn=@paramn // Append the first parameter - int i = 0; - - if (parameters.Count() > 0) + int index = 0; + int count = parameters.Count; + if (count > 0) { // Skip the return value parameters. - while (i < parameters.Count() && parameters[i].Direction == ParameterDirection.ReturnValue) + while (index < parameters.Count && parameters[index].Direction == ParameterDirection.ReturnValue) { - i++; + index++; } - if (i < parameters.Count()) + if (index < count) { // Possibility of a SQL Injection issue through parameter names and how to construct valid identifier for parameters. // Since the parameters comes from application itself, there should not be a security vulnerability. // Also since the query is not executed, but only analyzed there is no possibility for elevation of priviledge, but only for // incorrect results which would only affect the user that attempts the injection. - execStatement.AppendFormat(@" {0}={0}", parameters[i].ParameterNameFixed); + execStatement.AppendFormat(@" {0}={0}", parameters[index].ParameterNameFixed); // InputOutput and Output parameters need to be marked as such. - if (parameters[i].Direction == ParameterDirection.Output || - parameters[i].Direction == ParameterDirection.InputOutput) + if (parameters[index].Direction == ParameterDirection.Output || + parameters[index].Direction == ParameterDirection.InputOutput) { execStatement.AppendFormat(@" OUTPUT"); } @@ -5422,18 +5451,20 @@ private SqlParameter BuildStoredProcedureStatementForColumnEncryption(string sto } // Move to the next parameter. - i++; + index++; // Append the rest of parameters - for (; i < parameters.Count(); i++) + for (; index < count; index++) { - if (parameters[i].Direction != ParameterDirection.ReturnValue) + if (parameters[index].Direction != ParameterDirection.ReturnValue) { - execStatement.AppendFormat(@", {0}={0}", parameters[i].ParameterNameFixed); + execStatement.AppendFormat(@", {0}={0}", parameters[index].ParameterNameFixed); // InputOutput and Output parameters need to be marked as such. - if (parameters[i].Direction == ParameterDirection.Output || - parameters[i].Direction == ParameterDirection.InputOutput) + if ( + parameters[index].Direction == ParameterDirection.Output || + parameters[index].Direction == ParameterDirection.InputOutput + ) { execStatement.AppendFormat(@" OUTPUT"); } @@ -5595,7 +5626,7 @@ internal string BuildParamList(TdsParser parser, SqlParameterCollection paramete // Adds quotes to each part of a SQL identifier that may be multi-part, while leaving // the result as a single composite name. - private string ParseAndQuoteIdentifier(string identifier, bool isUdtTypeName) + private static string ParseAndQuoteIdentifier(string identifier, bool isUdtTypeName) { string[] strings = SqlParameter.ParseTypeName(identifier, isUdtTypeName); return ADP.BuildMultiPartName(strings); diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs index e530dacc6a..1db47e4eb1 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -8704,12 +8704,12 @@ internal Task TdsExecuteRPC(SqlCommand cmd, _SqlRPC[] rpcArray, int timeout, boo } // Stream out parameters - SqlParameter[] parameters = rpcext.parameters; + int parametersLength = rpcext.userParamCount + rpcext.systemParamCount; - for (int i = (ii == startRpc) ? startParam : 0; i < parameters.Length; i++) + for (int i = (ii == startRpc) ? startParam : 0; i < parametersLength; i++) { - // parameters can be unnamed - SqlParameter param = parameters[i]; + byte options = 0; + SqlParameter param = rpcext.GetParameterByIndex(i, out options); // Since we are reusing the parameters array, we cannot rely on length to indicate no of parameters. if (param == null) break; // End of parameters for this execute @@ -8738,7 +8738,7 @@ internal Task TdsExecuteRPC(SqlCommand cmd, _SqlRPC[] rpcArray, int timeout, boo if (mt.IsNewKatmaiType) { - WriteSmiParameter(param, i, 0 != (rpcext.paramoptions[i] & TdsEnums.RPC_PARAM_DEFAULT), stateObj); + WriteSmiParameter(param, i, 0 != (options & TdsEnums.RPC_PARAM_DEFAULT), stateObj); continue; } @@ -8747,452 +8747,8 @@ internal Task TdsExecuteRPC(SqlCommand cmd, _SqlRPC[] rpcArray, int timeout, boo { throw ADP.VersionDoesNotSupportDataType(mt.TypeName); } - object value = null; - bool isNull = true; - bool isSqlVal = false; - bool isDataFeed = false; - // if we have an output param, set the value to null so we do not send it across to the server - if (param.Direction == ParameterDirection.Output) - { - isSqlVal = param.ParameterIsSqlType; // We have to forward the TYPE info, we need to know what type we are returning. Once we null the parameter we will no longer be able to distinguish what type were seeing. - param.Value = null; - param.ParameterIsSqlType = isSqlVal; - } - else - { - value = param.GetCoercedValue(); - isNull = param.IsNull; - if (!isNull) - { - isSqlVal = param.CoercedValueIsSqlType; - isDataFeed = param.CoercedValueIsDataFeed; - } - } - - WriteParameterName(param.ParameterNameFixed, stateObj); - - // Write parameter status - stateObj.WriteByte(rpcext.paramoptions[i]); - - // MaxLen field is only written out for non-fixed length data types - // use the greater of the two sizes for maxLen - int actualSize; - int size = mt.IsSizeInCharacters ? param.GetParameterSize() * 2 : param.GetParameterSize(); - - //for UDTs, we calculate the length later when we get the bytes. This is a really expensive operation - if (mt.TDSType != TdsEnums.SQLUDT) - // getting the actualSize is expensive, cache here and use below - actualSize = param.GetActualSize(); - else - actualSize = 0; //get this later - - byte precision = 0; - byte scale = 0; - - // scale and precision are only relevant for numeric and decimal types - // adjust the actual value scale and precision to match the user specified - if (mt.SqlDbType == SqlDbType.Decimal) - { - precision = param.GetActualPrecision(); - scale = param.GetActualScale(); - - if (precision > TdsEnums.MAX_NUMERIC_PRECISION) - { - throw SQL.PrecisionValueOutOfRange(precision); - } - - // bug 49512, make sure the value matches the scale the user enters - if (!isNull) - { - if (isSqlVal) - { - value = AdjustSqlDecimalScale((SqlDecimal)value, scale); - - // If Precision is specified, verify value precision vs param precision - if (precision != 0) - { - if (precision < ((SqlDecimal)value).Precision) - { - throw ADP.ParameterValueOutOfRange((SqlDecimal)value); - } - } - } - else - { - value = AdjustDecimalScale((Decimal)value, scale); - - SqlDecimal sqlValue = new SqlDecimal((Decimal)value); - - // If Precision is specified, verify value precision vs param precision - if (precision != 0) - { - if (precision < sqlValue.Precision) - { - throw ADP.ParameterValueOutOfRange((Decimal)value); - } - } - } - } - } - - bool isParameterEncrypted = 0 != (rpcext.paramoptions[i] & TdsEnums.RPC_PARAM_ENCRYPTED); - - // Additional information we need to send over wire to the server when writing encrypted parameters. - SqlColumnEncryptionInputParameterInfo encryptedParameterInfoToWrite = null; - - // If the parameter is encrypted, we need to encrypt the value. - if (isParameterEncrypted) - { - Debug.Assert(mt.TDSType != TdsEnums.SQLVARIANT && - mt.TDSType != TdsEnums.SQLUDT && - mt.TDSType != TdsEnums.SQLXMLTYPE && - mt.TDSType != TdsEnums.SQLIMAGE && - mt.TDSType != TdsEnums.SQLTEXT && - mt.TDSType != TdsEnums.SQLNTEXT, "Type unsupported for encryption"); - - byte[] serializedValue = null; - byte[] encryptedValue = null; - - if (!isNull) - { - try - { - if (isSqlVal) - { - serializedValue = SerializeUnencryptedSqlValue(value, mt, actualSize, param.Offset, param.NormalizationRuleVersion, stateObj); - } - else - { - // for codePageEncoded types, WriteValue simply expects the number of characters - // For plp types, we also need the encoded byte size - serializedValue = SerializeUnencryptedValue(value, mt, param.GetActualScale(), actualSize, param.Offset, isDataFeed, param.NormalizationRuleVersion, stateObj); - } - - Debug.Assert(serializedValue != null, "serializedValue should not be null in TdsExecuteRPC."); - encryptedValue = SqlSecurityUtility.EncryptWithKey(serializedValue, param.CipherMetadata, _connHandler.ConnectionOptions.DataSource); - } - catch (Exception e) - { - throw SQL.ParamEncryptionFailed(param.ParameterName, null, e); - } - - Debug.Assert(encryptedValue != null && encryptedValue.Length > 0, - "encryptedValue should not be null or empty in TdsExecuteRPC."); - } - else - { - encryptedValue = null; - } - - // Change the datatype to varbinary(max). - // Since we don't know the size of the encrypted parameter on the server side, always set to (max). - // - mt = MetaType.MetaMaxVarBinary; - size = -1; - actualSize = (encryptedValue == null) ? 0 : encryptedValue.Length; - - encryptedParameterInfoToWrite = new SqlColumnEncryptionInputParameterInfo(param.GetMetadataForTypeInfo(), - param.CipherMetadata); - - // Set the value to the encrypted value and mark isSqlVal as false for VARBINARY encrypted value. - value = encryptedValue; - isSqlVal = false; - } - - Debug.Assert(isParameterEncrypted == (encryptedParameterInfoToWrite != null), - "encryptedParameterInfoToWrite can be not null if and only if isParameterEncrypted is true."); - - Debug.Assert(!isSqlVal || !isParameterEncrypted, "isParameterEncrypted can be true only if isSqlVal is false."); - - // fixup the types by using the NullableType property of the MetaType class - // - // following rules should be followed based on feedback from the M-SQL team - // 1) always use the BIG* types (ex: instead of SQLCHAR use SQLBIGCHAR) - // 2) always use nullable types (ex: instead of SQLINT use SQLINTN) - // 3) DECIMALN should always be sent as NUMERICN - // - stateObj.WriteByte(mt.NullableType); - - // handle variants here: the SQLVariant writing routine will write the maxlen and actual len columns - if (mt.TDSType == TdsEnums.SQLVARIANT) - { - // devnote: Do we ever hit this codepath? Yes, when a null value is being written out via a sql variant - // param.GetActualSize is not used - WriteSqlVariantValue(isSqlVal ? MetaType.GetComValueFromSqlVariant(value) : value, param.GetActualSize(), param.Offset, stateObj); - continue; - } - - int codePageByteSize = 0; - int maxsize = 0; - - if (mt.IsAnsiType) - { - // Avoid the following code block if ANSI but unfilled LazyMat blob - if ((!isNull) && (!isDataFeed)) - { - string s; - - if (isSqlVal) - { - if (value is SqlString) - { - s = ((SqlString)value).Value; - } - else - { - Debug.Assert(value is SqlChars, "Unknown value for Ansi datatype"); - s = new string(((SqlChars)value).Value); - } - } - else - { - s = (string)value; - } - - codePageByteSize = GetEncodingCharLength(s, actualSize, param.Offset, _defaultEncoding); - } - - if (mt.IsPlp) - { - WriteShort(TdsEnums.SQL_USHORTVARMAXLEN, stateObj); - } - else - { - maxsize = (size > codePageByteSize) ? size : codePageByteSize; - if (maxsize == 0) - { - // Yukon doesn't like 0 as MaxSize. Change it to 2 for unicode types - if (mt.IsNCharType) - maxsize = 2; - else - maxsize = 1; - } - - WriteParameterVarLen(mt, maxsize, false /*IsNull*/, stateObj); - } - } - else - { - // If type timestamp - treat as fixed type and always send over timestamp length, which is 8. - // For fixed types, we either send null or fixed length for type length. We want to match that - // behavior for timestamps. However, in the case of null, we still must send 8 because if we - // send null we will not receive a output val. You can send null for fixed types and still - // receive a output value, but not for variable types. So, always send 8 for timestamp because - // while the user sees it as a fixed type, we are actually representing it as a bigbinary which - // is variable. - if (mt.SqlDbType == SqlDbType.Timestamp) - { - WriteParameterVarLen(mt, TdsEnums.TEXT_TIME_STAMP_LEN, false, stateObj); - } - else if (mt.SqlDbType == SqlDbType.Udt) - { - byte[] udtVal = null; - Format format = Format.Native; - - Debug.Assert(_isYukon, "Invalid DataType UDT for non-Yukon or later server!"); - if (!isNull) - { - // When writing UDT parameter values to the TDS stream, allow sending byte[] or SqlBytes - // directly to the server and not rejected as invalid. This allows users to handle - // serialization and deserialization logic without having to have SqlClient be aware of - // the types and without using inefficient text representations. - if (value is byte[] rawBytes) - { - udtVal = rawBytes; - } - else if (value is SqlBytes sqlBytes) - { - switch (sqlBytes.Storage) - { - case StorageState.Buffer: - // use the buffer directly, the only way to create it is with the correctly sized byte array - udtVal = sqlBytes.Buffer; - break; - case StorageState.Stream: - case StorageState.UnmanagedBuffer: - // allocate a new byte array to store the data - udtVal = sqlBytes.Value; - break; - } - } - else - { - udtVal = _connHandler.Connection.GetBytes(value, out format, out maxsize); - } - - Debug.Assert(null != udtVal, "GetBytes returned null instance. Make sure that it always returns non-null value"); - size = udtVal.Length; - - //it may be legitimate, but we dont support it yet - if (size < 0 || (size >= ushort.MaxValue && maxsize != -1)) - throw new IndexOutOfRangeException(); - } - - //if this is NULL value, write special null value - byte[] lenBytes = BitConverter.GetBytes((long)size); - - if (string.IsNullOrEmpty(param.UdtTypeName)) - throw SQL.MustSetUdtTypeNameForUdtParams(); - - // Split the input name. TypeName is returned as single 3 part name during DeriveParameters. - // NOTE: ParseUdtTypeName throws if format is incorrect - string[] names = SqlParameter.ParseTypeName(param.UdtTypeName, true /* is UdtTypeName */); - if (!string.IsNullOrEmpty(names[0]) && TdsEnums.MAX_SERVERNAME < names[0].Length) - { - throw ADP.ArgumentOutOfRange(nameof(names)); - } - if (!string.IsNullOrEmpty(names[1]) && TdsEnums.MAX_SERVERNAME < names[names.Length - 2].Length) - { - throw ADP.ArgumentOutOfRange(nameof(names)); - } - if (TdsEnums.MAX_SERVERNAME < names[2].Length) - { - throw ADP.ArgumentOutOfRange(nameof(names)); - } - - WriteUDTMetaData(value, names[0], names[1], names[2], stateObj); - - if (!isNull) - { - WriteUnsignedLong((ulong)udtVal.Length, stateObj); // PLP length - if (udtVal.Length > 0) - { // Only write chunk length if its value is greater than 0 - WriteInt(udtVal.Length, stateObj); // Chunk length - stateObj.WriteByteArray(udtVal, udtVal.Length, 0); // Value - } - WriteInt(0, stateObj); // Terminator - } - else - { - WriteUnsignedLong(TdsEnums.SQL_PLP_NULL, stateObj); // PLP Null. - } - continue; // End of UDT - continue to next parameter. - } - else if (mt.IsPlp) - { - if (mt.SqlDbType != SqlDbType.Xml) - WriteShort(TdsEnums.SQL_USHORTVARMAXLEN, stateObj); - } - else if ((!mt.IsVarTime) && (mt.SqlDbType != SqlDbType.Date)) - { // Time, Date, DateTime2, DateTimeoffset do not have the size written out - maxsize = (size > actualSize) ? size : actualSize; - if (maxsize == 0 && _isYukon) - { - // Yukon doesn't like 0 as MaxSize. Change it to 2 for unicode types (SQL9 - 682322) - if (mt.IsNCharType) - maxsize = 2; - else - maxsize = 1; - } - - WriteParameterVarLen(mt, maxsize, false /*IsNull*/, stateObj); - } - } - - // scale and precision are only relevant for numeric and decimal types - if (mt.SqlDbType == SqlDbType.Decimal) - { - if (0 == precision) - { - stateObj.WriteByte(TdsEnums.DEFAULT_NUMERIC_PRECISION); - } - else - { - stateObj.WriteByte(precision); - } - - stateObj.WriteByte(scale); - } - else if (mt.IsVarTime) - { - stateObj.WriteByte(param.GetActualScale()); - } - - // write out collation or xml metadata - - if (_isYukon && (mt.SqlDbType == SqlDbType.Xml)) - { - if (((param.XmlSchemaCollectionDatabase != null) && (param.XmlSchemaCollectionDatabase != ADP.StrEmpty)) || - ((param.XmlSchemaCollectionOwningSchema != null) && (param.XmlSchemaCollectionOwningSchema != ADP.StrEmpty)) || - ((param.XmlSchemaCollectionName != null) && (param.XmlSchemaCollectionName != ADP.StrEmpty))) - { - stateObj.WriteByte(1); //Schema present flag - - if ((param.XmlSchemaCollectionDatabase != null) && (param.XmlSchemaCollectionDatabase != ADP.StrEmpty)) - { - tempLen = (param.XmlSchemaCollectionDatabase).Length; - stateObj.WriteByte((byte)(tempLen)); - WriteString(param.XmlSchemaCollectionDatabase, tempLen, 0, stateObj); - } - else - { - stateObj.WriteByte(0); // No dbname - } - - if ((param.XmlSchemaCollectionOwningSchema != null) && (param.XmlSchemaCollectionOwningSchema != ADP.StrEmpty)) - { - tempLen = (param.XmlSchemaCollectionOwningSchema).Length; - stateObj.WriteByte((byte)(tempLen)); - WriteString(param.XmlSchemaCollectionOwningSchema, tempLen, 0, stateObj); - } - else - { - stateObj.WriteByte(0); // no xml schema name - } - - if ((param.XmlSchemaCollectionName != null) && (param.XmlSchemaCollectionName != ADP.StrEmpty)) - { - tempLen = (param.XmlSchemaCollectionName).Length; - WriteShort((short)(tempLen), stateObj); - WriteString(param.XmlSchemaCollectionName, tempLen, 0, stateObj); - } - else - { - WriteShort(0, stateObj); // No xml schema collection name - } - } - else - { - stateObj.WriteByte(0); // No schema - } - } - else if (mt.IsCharType) - { - // if it is not supplied, simply write out our default collation, otherwise, write out the one attached to the parameter - SqlCollation outCollation = (param.Collation != null) ? param.Collation : _defaultCollation; - Debug.Assert(_defaultCollation != null, "_defaultCollation is null!"); - - WriteUnsignedInt(outCollation.info, stateObj); - stateObj.WriteByte(outCollation.sortId); - } - - if (0 == codePageByteSize) - WriteParameterVarLen(mt, actualSize, isNull, stateObj, isDataFeed); - else - WriteParameterVarLen(mt, codePageByteSize, isNull, stateObj, isDataFeed); - - Task writeParamTask = null; - // write the value now - if (!isNull) - { - if (isSqlVal) - { - writeParamTask = WriteSqlValue(value, mt, actualSize, codePageByteSize, param.Offset, stateObj); - } - else - { - // for codePageEncoded types, WriteValue simply expects the number of characters - // For plp types, we also need the encoded byte size - writeParamTask = WriteValue(value, mt, isParameterEncrypted ? (byte)0 : param.GetActualScale(), actualSize, codePageByteSize, isParameterEncrypted ? 0 : param.Offset, stateObj, isParameterEncrypted ? 0 : param.Size, isDataFeed); - } - } - - // Send encryption metadata for encrypted parameters. - if (isParameterEncrypted) - { - writeParamTask = WriteEncryptionMetadata(writeParamTask, encryptedParameterInfoToWrite, stateObj); - } + Task writeParamTask = TDSExecuteRPCAddParameter(stateObj, param, mt, options); if (!sync) { @@ -9210,10 +8766,20 @@ internal Task TdsExecuteRPC(SqlCommand cmd, _SqlRPC[] rpcArray, int timeout, boo task = completion.Task; } - AsyncHelper.ContinueTask(writeParamTask, completion, - () => TdsExecuteRPC(cmd, rpcArray, timeout, inSchema, notificationRequest, stateObj, isCommandProc, sync, completion, - startRpc: ii, startParam: i + 1), - onFailure: exc => TdsExecuteRPC_OnFailure(exc, stateObj)); + TDSExecuteRPCParameterSetupWriteCompletion( + cmd, + rpcArray, + timeout, + inSchema, + notificationRequest, + stateObj, + isCommandProc, + sync, + completion, + ii, + i + 1, + writeParamTask + ); // Take care of releasing the locks if (releaseConnectionLock) @@ -9305,6 +8871,459 @@ internal Task TdsExecuteRPC(SqlCommand cmd, _SqlRPC[] rpcArray, int timeout, boo } } + private Task TDSExecuteRPCAddParameter(TdsParserStateObject stateObj, SqlParameter param, MetaType mt, byte options) + { + int tempLen; + object value = null; + bool isNull = true; + bool isSqlVal = false; + bool isDataFeed = false; + // if we have an output param, set the value to null so we do not send it across to the server + if (param.Direction == ParameterDirection.Output) + { + isSqlVal = param.ParameterIsSqlType; // We have to forward the TYPE info, we need to know what type we are returning. Once we null the parameter we will no longer be able to distinguish what type were seeing. + param.Value = null; + param.ParameterIsSqlType = isSqlVal; + } + else + { + value = param.GetCoercedValue(); + isNull = param.IsNull; + if (!isNull) + { + isSqlVal = param.CoercedValueIsSqlType; + isDataFeed = param.CoercedValueIsDataFeed; + } + } + + WriteParameterName(param.ParameterNameFixed, stateObj); + + // Write parameter status + stateObj.WriteByte(options); + + // MaxLen field is only written out for non-fixed length data types + // use the greater of the two sizes for maxLen + int actualSize; + int size = mt.IsSizeInCharacters ? param.GetParameterSize() * 2 : param.GetParameterSize(); + + //for UDTs, we calculate the length later when we get the bytes. This is a really expensive operation + if (mt.TDSType != TdsEnums.SQLUDT) + // getting the actualSize is expensive, cache here and use below + actualSize = param.GetActualSize(); + else + actualSize = 0; //get this later + + byte precision = 0; + byte scale = 0; + + // scale and precision are only relevant for numeric and decimal types + // adjust the actual value scale and precision to match the user specified + if (mt.SqlDbType == SqlDbType.Decimal) + { + precision = param.GetActualPrecision(); + scale = param.GetActualScale(); + + if (precision > TdsEnums.MAX_NUMERIC_PRECISION) + { + throw SQL.PrecisionValueOutOfRange(precision); + } + + // bug 49512, make sure the value matches the scale the user enters + if (!isNull) + { + if (isSqlVal) + { + value = AdjustSqlDecimalScale((SqlDecimal)value, scale); + + // If Precision is specified, verify value precision vs param precision + if (precision != 0) + { + if (precision < ((SqlDecimal)value).Precision) + { + throw ADP.ParameterValueOutOfRange((SqlDecimal)value); + } + } + } + else + { + value = AdjustDecimalScale((Decimal)value, scale); + + SqlDecimal sqlValue = new SqlDecimal((Decimal)value); + + // If Precision is specified, verify value precision vs param precision + if (precision != 0) + { + if (precision < sqlValue.Precision) + { + throw ADP.ParameterValueOutOfRange((Decimal)value); + } + } + } + } + } + + bool isParameterEncrypted = 0 != (options & TdsEnums.RPC_PARAM_ENCRYPTED); + + // Additional information we need to send over wire to the server when writing encrypted parameters. + SqlColumnEncryptionInputParameterInfo encryptedParameterInfoToWrite = null; + + // If the parameter is encrypted, we need to encrypt the value. + if (isParameterEncrypted) + { + Debug.Assert(mt.TDSType != TdsEnums.SQLVARIANT && + mt.TDSType != TdsEnums.SQLUDT && + mt.TDSType != TdsEnums.SQLXMLTYPE && + mt.TDSType != TdsEnums.SQLIMAGE && + mt.TDSType != TdsEnums.SQLTEXT && + mt.TDSType != TdsEnums.SQLNTEXT, "Type unsupported for encryption"); + + byte[] serializedValue = null; + byte[] encryptedValue = null; + + if (!isNull) + { + try + { + if (isSqlVal) + { + serializedValue = SerializeUnencryptedSqlValue(value, mt, actualSize, param.Offset, param.NormalizationRuleVersion, stateObj); + } + else + { + // for codePageEncoded types, WriteValue simply expects the number of characters + // For plp types, we also need the encoded byte size + serializedValue = SerializeUnencryptedValue(value, mt, param.GetActualScale(), actualSize, param.Offset, isDataFeed, param.NormalizationRuleVersion, stateObj); + } + + Debug.Assert(serializedValue != null, "serializedValue should not be null in TdsExecuteRPC."); + encryptedValue = SqlSecurityUtility.EncryptWithKey(serializedValue, param.CipherMetadata, _connHandler.ConnectionOptions.DataSource); + } + catch (Exception e) + { + throw SQL.ParamEncryptionFailed(param.ParameterName, null, e); + } + + Debug.Assert(encryptedValue != null && encryptedValue.Length > 0, + "encryptedValue should not be null or empty in TdsExecuteRPC."); + } + else + { + encryptedValue = null; + } + + // Change the datatype to varbinary(max). + // Since we don't know the size of the encrypted parameter on the server side, always set to (max). + // + mt = MetaType.MetaMaxVarBinary; + size = -1; + actualSize = (encryptedValue == null) ? 0 : encryptedValue.Length; + + encryptedParameterInfoToWrite = new SqlColumnEncryptionInputParameterInfo(param.GetMetadataForTypeInfo(), + param.CipherMetadata); + + // Set the value to the encrypted value and mark isSqlVal as false for VARBINARY encrypted value. + value = encryptedValue; + isSqlVal = false; + } + + Debug.Assert(isParameterEncrypted == (encryptedParameterInfoToWrite != null), + "encryptedParameterInfoToWrite can be not null if and only if isParameterEncrypted is true."); + + Debug.Assert(!isSqlVal || !isParameterEncrypted, "isParameterEncrypted can be true only if isSqlVal is false."); + + // fixup the types by using the NullableType property of the MetaType class + // + // following rules should be followed based on feedback from the M-SQL team + // 1) always use the BIG* types (ex: instead of SQLCHAR use SQLBIGCHAR) + // 2) always use nullable types (ex: instead of SQLINT use SQLINTN) + // 3) DECIMALN should always be sent as NUMERICN + // + stateObj.WriteByte(mt.NullableType); + + // handle variants here: the SQLVariant writing routine will write the maxlen and actual len columns + if (mt.TDSType == TdsEnums.SQLVARIANT) + { + // devnote: Do we ever hit this codepath? Yes, when a null value is being written out via a sql variant + // param.GetActualSize is not used + WriteSqlVariantValue(isSqlVal ? MetaType.GetComValueFromSqlVariant(value) : value, param.GetActualSize(), param.Offset, stateObj); + return null; + } + + int codePageByteSize = 0; + int maxsize = 0; + + if (mt.IsAnsiType) + { + // Avoid the following code block if ANSI but unfilled LazyMat blob + if ((!isNull) && (!isDataFeed)) + { + string s; + + if (isSqlVal) + { + if (value is SqlString) + { + s = ((SqlString)value).Value; + } + else + { + Debug.Assert(value is SqlChars, "Unknown value for Ansi datatype"); + s = new string(((SqlChars)value).Value); + } + } + else + { + s = (string)value; + } + + codePageByteSize = GetEncodingCharLength(s, actualSize, param.Offset, _defaultEncoding); + } + + if (mt.IsPlp) + { + WriteShort(TdsEnums.SQL_USHORTVARMAXLEN, stateObj); + } + else + { + maxsize = (size > codePageByteSize) ? size : codePageByteSize; + if (maxsize == 0) + { + // Yukon doesn't like 0 as MaxSize. Change it to 2 for unicode types + if (mt.IsNCharType) + maxsize = 2; + else + maxsize = 1; + } + + WriteParameterVarLen(mt, maxsize, false /*IsNull*/, stateObj); + } + } + else + { + // If type timestamp - treat as fixed type and always send over timestamp length, which is 8. + // For fixed types, we either send null or fixed length for type length. We want to match that + // behavior for timestamps. However, in the case of null, we still must send 8 because if we + // send null we will not receive a output val. You can send null for fixed types and still + // receive a output value, but not for variable types. So, always send 8 for timestamp because + // while the user sees it as a fixed type, we are actually representing it as a bigbinary which + // is variable. + if (mt.SqlDbType == SqlDbType.Timestamp) + { + WriteParameterVarLen(mt, TdsEnums.TEXT_TIME_STAMP_LEN, false, stateObj); + } + else if (mt.SqlDbType == SqlDbType.Udt) + { + byte[] udtVal = null; + Format format = Format.Native; + + Debug.Assert(_isYukon, "Invalid DataType UDT for non-Yukon or later server!"); + + if (!isNull) + { + // When writing UDT parameter values to the TDS stream, allow sending byte[] or SqlBytes + // directly to the server and not rejected as invalid. This allows users to handle + // serialization and deserialization logic without having to have SqlClient be aware of + // the types and without using inefficient text representations. + if (value is byte[] rawBytes) + { + udtVal = rawBytes; + } + else if (value is SqlBytes sqlBytes) + { + switch (sqlBytes.Storage) + { + case StorageState.Buffer: + // use the buffer directly, the only way to create it is with the correctly sized byte array + udtVal = sqlBytes.Buffer; + break; + case StorageState.Stream: + case StorageState.UnmanagedBuffer: + // allocate a new byte array to store the data + udtVal = sqlBytes.Value; + break; + } + } + else + { + udtVal = _connHandler.Connection.GetBytes(value, out format, out maxsize); + } + + Debug.Assert(null != udtVal, "GetBytes returned null instance. Make sure that it always returns non-null value"); + size = udtVal.Length; + + //it may be legitimate, but we dont support it yet + if (size < 0 || (size >= ushort.MaxValue && maxsize != -1)) + throw new IndexOutOfRangeException(); + } + + //if this is NULL value, write special null value + byte[] lenBytes = BitConverter.GetBytes((long)size); + + if (string.IsNullOrEmpty(param.UdtTypeName)) + throw SQL.MustSetUdtTypeNameForUdtParams(); + + // Split the input name. TypeName is returned as single 3 part name during DeriveParameters. + // NOTE: ParseUdtTypeName throws if format is incorrect + string[] names = SqlParameter.ParseTypeName(param.UdtTypeName, true /* is UdtTypeName */); + if (!string.IsNullOrEmpty(names[0]) && TdsEnums.MAX_SERVERNAME < names[0].Length) + { + throw ADP.ArgumentOutOfRange(nameof(names)); + } + if (!string.IsNullOrEmpty(names[1]) && TdsEnums.MAX_SERVERNAME < names[names.Length - 2].Length) + { + throw ADP.ArgumentOutOfRange(nameof(names)); + } + if (TdsEnums.MAX_SERVERNAME < names[2].Length) + { + throw ADP.ArgumentOutOfRange(nameof(names)); + } + + WriteUDTMetaData(value, names[0], names[1], names[2], stateObj); + + if (!isNull) + { + WriteUnsignedLong((ulong)udtVal.Length, stateObj); // PLP length + if (udtVal.Length > 0) + { // Only write chunk length if its value is greater than 0 + WriteInt(udtVal.Length, stateObj); // Chunk length + stateObj.WriteByteArray(udtVal, udtVal.Length, 0); // Value + } + WriteInt(0, stateObj); // Terminator + } + else + { + WriteUnsignedLong(TdsEnums.SQL_PLP_NULL, stateObj); // PLP Null. + } + return null; // End of UDT - continue to next parameter. + } + else if (mt.IsPlp) + { + if (mt.SqlDbType != SqlDbType.Xml) + WriteShort(TdsEnums.SQL_USHORTVARMAXLEN, stateObj); + } + else if ((!mt.IsVarTime) && (mt.SqlDbType != SqlDbType.Date)) + { // Time, Date, DateTime2, DateTimeoffset do not have the size written out + maxsize = (size > actualSize) ? size : actualSize; + if (maxsize == 0 && _isYukon) + { + // Yukon doesn't like 0 as MaxSize. Change it to 2 for unicode types (SQL9 - 682322) + if (mt.IsNCharType) + maxsize = 2; + else + maxsize = 1; + } + + WriteParameterVarLen(mt, maxsize, false /*IsNull*/, stateObj); + } + } + + // scale and precision are only relevant for numeric and decimal types + if (mt.SqlDbType == SqlDbType.Decimal) + { + if (0 == precision) + { + stateObj.WriteByte(TdsEnums.DEFAULT_NUMERIC_PRECISION); + } + else + { + stateObj.WriteByte(precision); + } + + stateObj.WriteByte(scale); + } + else if (mt.IsVarTime) + { + stateObj.WriteByte(param.GetActualScale()); + } + + // write out collation or xml metadata + + if (_isYukon && (mt.SqlDbType == SqlDbType.Xml)) + { + if (((param.XmlSchemaCollectionDatabase != null) && (param.XmlSchemaCollectionDatabase != ADP.StrEmpty)) || + ((param.XmlSchemaCollectionOwningSchema != null) && (param.XmlSchemaCollectionOwningSchema != ADP.StrEmpty)) || + ((param.XmlSchemaCollectionName != null) && (param.XmlSchemaCollectionName != ADP.StrEmpty))) + { + stateObj.WriteByte(1); //Schema present flag + + if ((param.XmlSchemaCollectionDatabase != null) && (param.XmlSchemaCollectionDatabase != ADP.StrEmpty)) + { + tempLen = (param.XmlSchemaCollectionDatabase).Length; + stateObj.WriteByte((byte)(tempLen)); + WriteString(param.XmlSchemaCollectionDatabase, tempLen, 0, stateObj); + } + else + { + stateObj.WriteByte(0); // No dbname + } + + if ((param.XmlSchemaCollectionOwningSchema != null) && (param.XmlSchemaCollectionOwningSchema != ADP.StrEmpty)) + { + tempLen = (param.XmlSchemaCollectionOwningSchema).Length; + stateObj.WriteByte((byte)(tempLen)); + WriteString(param.XmlSchemaCollectionOwningSchema, tempLen, 0, stateObj); + } + else + { + stateObj.WriteByte(0); // no xml schema name + } + + if ((param.XmlSchemaCollectionName != null) && (param.XmlSchemaCollectionName != ADP.StrEmpty)) + { + tempLen = (param.XmlSchemaCollectionName).Length; + WriteShort((short)(tempLen), stateObj); + WriteString(param.XmlSchemaCollectionName, tempLen, 0, stateObj); + } + else + { + WriteShort(0, stateObj); // No xml schema collection name + } + } + else + { + stateObj.WriteByte(0); // No schema + } + } + else if (mt.IsCharType) + { + // if it is not supplied, simply write out our default collation, otherwise, write out the one attached to the parameter + SqlCollation outCollation = (param.Collation != null) ? param.Collation : _defaultCollation; + Debug.Assert(_defaultCollation != null, "_defaultCollation is null!"); + + WriteUnsignedInt(outCollation.info, stateObj); + stateObj.WriteByte(outCollation.sortId); + } + + if (0 == codePageByteSize) + WriteParameterVarLen(mt, actualSize, isNull, stateObj, isDataFeed); + else + WriteParameterVarLen(mt, codePageByteSize, isNull, stateObj, isDataFeed); + + Task writeParamTask = null; + // write the value now + if (!isNull) + { + if (isSqlVal) + { + writeParamTask = WriteSqlValue(value, mt, actualSize, codePageByteSize, param.Offset, stateObj); + } + else + { + // for codePageEncoded types, WriteValue simply expects the number of characters + // For plp types, we also need the encoded byte size + writeParamTask = WriteValue(value, mt, isParameterEncrypted ? (byte)0 : param.GetActualScale(), actualSize, codePageByteSize, isParameterEncrypted ? 0 : param.Offset, stateObj, isParameterEncrypted ? 0 : param.Size, isDataFeed); + } + } + + // Send encryption metadata for encrypted parameters. + if (isParameterEncrypted) + { + writeParamTask = WriteEncryptionMetadata(writeParamTask, encryptedParameterInfoToWrite, stateObj); + } + + return writeParamTask; + } + // This is in its own method to avoid always allocating the lambda in TDSExecuteRPCParameter private void TDSExecuteRPCParameterSetupWriteCompletion(SqlCommand cmd, _SqlRPC[] rpcArray, int timeout, bool inSchema, SqlNotificationRequest notificationRequest, TdsParserStateObject stateObj, bool isCommandProc, bool sync, TaskCompletionSource completion, int startRpc, int startParam, Task writeParamTask) { diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserHelperClasses.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserHelperClasses.cs index 159f821082..b87e83137e 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserHelperClasses.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserHelperClasses.cs @@ -768,8 +768,14 @@ internal sealed class _SqlRPC internal string rpcName; internal ushort ProcID; // Used instead of name internal ushort options; - internal SqlParameter[] parameters; - internal byte[] paramoptions; + + internal SqlParameter[] systemParams; + internal byte[] systemParamOptions; + internal int systemParamCount; + + internal SqlParameterCollection userParams; + internal long[] userParamMap; + internal int userParamCount; internal int? recordsAffected; internal int cumulativeRecordsAffected; @@ -789,13 +795,33 @@ internal string GetCommandTextOrRpcName() if (TdsEnums.RPC_PROCID_EXECUTESQL == ProcID) { // Param 0 is the actual sql executing - return (string)parameters[0].Value; + return (string)systemParams[0].Value; } else { return rpcName; } } + + internal SqlParameter GetParameterByIndex(int index, out byte options) + { + options = 0; + SqlParameter retval = null; + if (index < systemParamCount) + { + retval = systemParams[index]; + options = systemParamOptions[index]; + } + else + { + long data = userParamMap[index - systemParamCount]; + int paramIndex = (int)(data & int.MaxValue); + options = (byte)((data >> 32) & 0xFF); + retval = userParams[paramIndex]; + } + return retval; + } + } internal sealed class SqlReturnValue : SqlMetaDataPriv From 7a15a0b107bd418202ec6b8f8a9e6430456e56bb Mon Sep 17 00:00:00 2001 From: Wraith2 Date: Wed, 27 Nov 2019 19:21:26 +0000 Subject: [PATCH 2/6] fixup encryption rpc --- .../netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c73b1e6380..9802ddbe22 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 @@ -3759,7 +3759,7 @@ private void ReadDescribeEncryptionParameterResults(SqlDataReader ds, ReadOnlyDi Debug.Assert(rpc != null, "rpc should not be null here."); - int userParamCount = rpc.userParams.Count; + int userParamCount = rpc.userParamCount; int recievedMetadataCount = 0; if (!enclaveMetadataExists || ds.NextResult()) { From bfd7b7e086386d733df2b1eefcd4e52cfe6465e6 Mon Sep 17 00:00:00 2001 From: Wraith2 Date: Wed, 27 Nov 2019 23:38:15 +0000 Subject: [PATCH 3/6] reuse parameter variables --- .../Microsoft/Data/SqlClient/SqlCommand.cs | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) 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 9802ddbe22..8bf97a9b80 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 @@ -3475,7 +3475,6 @@ private void PrepareDescribeParameterEncryptionRequest(_SqlRPC originalRpcReques describeParameterEncryptionRequest.rpcName = "sp_describe_parameter_encryption"; // Prepare @tsql parameter - SqlParameter sqlParam; string text; // In BatchRPCMode, The actual T-SQL query is in the first parameter and not present as the rpcName, as is the case with non-BatchRPCMode. @@ -3484,7 +3483,11 @@ private void PrepareDescribeParameterEncryptionRequest(_SqlRPC originalRpcReques Debug.Assert(originalRpcRequest.systemParamCount > 0, "originalRpcRequest didn't have at-least 1 parameter in BatchRPCMode, in PrepareDescribeParameterEncryptionRequest."); text = (string)originalRpcRequest.systemParams[0].Value; - sqlParam = GetSqlParameterWithQueryText(text); + //@tsql + SqlParameter tsqlParam = describeParameterEncryptionRequest.systemParams[0]; + tsqlParam.SqlDbType = ((text.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT) ? SqlDbType.NVarChar : SqlDbType.NText; + tsqlParam.Value = text; + tsqlParam.Size = text.Length; } else { @@ -3493,17 +3496,19 @@ private void PrepareDescribeParameterEncryptionRequest(_SqlRPC originalRpcReques { // For stored procedures, we need to prepare @tsql in the following format // N'EXEC sp_name @param1=@param1, @param1=@param2, ..., @paramN=@paramN' - sqlParam = BuildStoredProcedureStatementForColumnEncryption(text, originalRpcRequest.userParams); + describeParameterEncryptionRequest.systemParams[0] = BuildStoredProcedureStatementForColumnEncryption(text, originalRpcRequest.userParams); } else { - sqlParam = GetSqlParameterWithQueryText(text); + //@tsql + SqlParameter tsqlParam = describeParameterEncryptionRequest.systemParams[0]; + tsqlParam.SqlDbType = ((text.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT) ? SqlDbType.NVarChar : SqlDbType.NText; + tsqlParam.Value = text; + tsqlParam.Size = text.Length; } } Debug.Assert(text != null, "@tsql parameter is null in PrepareDescribeParameterEncryptionRequest."); - - describeParameterEncryptionRequest.systemParams[0] = sqlParam; string parameterList = null; // In BatchRPCMode, the input parameters start at parameters[1]. parameters[0] is the T-SQL statement. rpcName is sp_executesql. @@ -3570,22 +3575,19 @@ private void PrepareDescribeParameterEncryptionRequest(_SqlRPC originalRpcReques parameterList = BuildParamList(tdsParser, tempCollection, includeReturnValue: true); } - sqlParam = new SqlParameter(null, ((parameterList.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT) ? SqlDbType.NVarChar : SqlDbType.NText, parameterList.Length); - sqlParam.Value = parameterList; - describeParameterEncryptionRequest.systemParams[1] = sqlParam; - describeParameterEncryptionRequest.systemParamCount = 2; + //@parameters + + SqlParameter paramsParam = describeParameterEncryptionRequest.systemParams[1]; + paramsParam.SqlDbType = ((parameterList.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT) ? SqlDbType.NVarChar : SqlDbType.NText; + paramsParam.Size = parameterList.Length; + paramsParam.Value = parameterList; if (attestationParameters != null) { - var attestationParametersParam = new SqlParameter(null, SqlDbType.VarBinary) - { - Direction = ParameterDirection.Input, - Size = attestationParameters.Length, - Value = attestationParameters - }; - - describeParameterEncryptionRequest.systemParams[2] = attestationParametersParam; - describeParameterEncryptionRequest.systemParamCount = 3; + SqlParameter attestationParametersParam = describeParameterEncryptionRequest.systemParams[2]; + attestationParametersParam.Direction = ParameterDirection.Input; + attestationParametersParam.Size = attestationParameters.Length; + attestationParametersParam.Value = attestationParameters; } } From b312bd5b7436e0aa6da324c186fa16c146bb61d8 Mon Sep 17 00:00:00 2001 From: Wraith2 Date: Fri, 29 Nov 2019 10:16:38 +0000 Subject: [PATCH 4/6] fixup parameter access to use collection --- .../netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 8bf97a9b80..a945d91a02 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 @@ -3761,7 +3761,7 @@ private void ReadDescribeEncryptionParameterResults(SqlDataReader ds, ReadOnlyDi Debug.Assert(rpc != null, "rpc should not be null here."); - int userParamCount = rpc.userParamCount; + int userParamCount = rpc.userParams?.Count ?? 0; int recievedMetadataCount = 0; if (!enclaveMetadataExists || ds.NextResult()) { From 280ea94be3a793fa56d55768dbc9ef5a121f2aeb Mon Sep 17 00:00:00 2001 From: Wraith2 Date: Sun, 1 Dec 2019 19:31:59 +0000 Subject: [PATCH 5/6] merge DoneProc implementations --- .../Microsoft/Data/SqlClient/SqlCommand.cs | 94 ++++++------------- 1 file changed, 31 insertions(+), 63 deletions(-) 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 a945d91a02..0a0571d1e4 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 @@ -4726,86 +4726,54 @@ private void PutStateObject() } } - /// - /// IMPORTANT NOTE: This is created as a copy of OnDoneProc below for Transparent Column Encryption improvement - /// as there is not much time, to address regressions. Will revisit removing the duplication, when we have time again. - /// internal void OnDoneDescribeParameterEncryptionProc(TdsParserStateObject stateObj) { // called per rpc batch complete if (BatchRPCMode) { - // track the records affected for the just completed rpc batch - // _rowsAffected is cumulative for ExecuteNonQuery across all rpc batches - _sqlRPCParameterEncryptionReqArray[_currentlyExecutingDescribeParameterEncryptionRPC].cumulativeRecordsAffected = _rowsAffected; - - _sqlRPCParameterEncryptionReqArray[_currentlyExecutingDescribeParameterEncryptionRPC].recordsAffected = - (((0 < _currentlyExecutingDescribeParameterEncryptionRPC) && (0 <= _rowsAffected)) - ? (_rowsAffected - Math.Max(_sqlRPCParameterEncryptionReqArray[_currentlyExecutingDescribeParameterEncryptionRPC - 1].cumulativeRecordsAffected, 0)) - : _rowsAffected); - - // track the error collection (not available from TdsParser after ExecuteNonQuery) - // and the which errors are associated with the just completed rpc batch - _sqlRPCParameterEncryptionReqArray[_currentlyExecutingDescribeParameterEncryptionRPC].errorsIndexStart = - ((0 < _currentlyExecutingDescribeParameterEncryptionRPC) - ? _sqlRPCParameterEncryptionReqArray[_currentlyExecutingDescribeParameterEncryptionRPC - 1].errorsIndexEnd - : 0); - _sqlRPCParameterEncryptionReqArray[_currentlyExecutingDescribeParameterEncryptionRPC].errorsIndexEnd = stateObj.ErrorCount; - _sqlRPCParameterEncryptionReqArray[_currentlyExecutingDescribeParameterEncryptionRPC].errors = stateObj._errors; - - // track the warning collection (not available from TdsParser after ExecuteNonQuery) - // and the which warnings are associated with the just completed rpc batch - _sqlRPCParameterEncryptionReqArray[_currentlyExecutingDescribeParameterEncryptionRPC].warningsIndexStart = - ((0 < _currentlyExecutingDescribeParameterEncryptionRPC) - ? _sqlRPCParameterEncryptionReqArray[_currentlyExecutingDescribeParameterEncryptionRPC - 1].warningsIndexEnd - : 0); - _sqlRPCParameterEncryptionReqArray[_currentlyExecutingDescribeParameterEncryptionRPC].warningsIndexEnd = stateObj.WarningCount; - _sqlRPCParameterEncryptionReqArray[_currentlyExecutingDescribeParameterEncryptionRPC].warnings = stateObj._warnings; - + OnDone(stateObj, _currentlyExecutingDescribeParameterEncryptionRPC, _sqlRPCParameterEncryptionReqArray, _rowsAffected); _currentlyExecutingDescribeParameterEncryptionRPC++; } } - /// - /// IMPORTANT NOTE: There is a copy of this function above in OnDoneDescribeParameterEncryptionProc. - /// Please consider the changes being done in this function for the above function as well. - /// internal void OnDoneProc() - { // called per rpc batch complete + { + // called per rpc batch complete if (BatchRPCMode) { - // track the records affected for the just completed rpc batch - // _rowsAffected is cumulative for ExecuteNonQuery across all rpc batches - _SqlRPCBatchArray[_currentlyExecutingBatch].cumulativeRecordsAffected = _rowsAffected; - - _SqlRPCBatchArray[_currentlyExecutingBatch].recordsAffected = - (((0 < _currentlyExecutingBatch) && (0 <= _rowsAffected)) - ? (_rowsAffected - Math.Max(_SqlRPCBatchArray[_currentlyExecutingBatch - 1].cumulativeRecordsAffected, 0)) - : _rowsAffected); - - // track the error collection (not available from TdsParser after ExecuteNonQuery) - // and the which errors are associated with the just completed rpc batch - _SqlRPCBatchArray[_currentlyExecutingBatch].errorsIndexStart = - ((0 < _currentlyExecutingBatch) - ? _SqlRPCBatchArray[_currentlyExecutingBatch - 1].errorsIndexEnd - : 0); - _SqlRPCBatchArray[_currentlyExecutingBatch].errorsIndexEnd = _stateObj.ErrorCount; - _SqlRPCBatchArray[_currentlyExecutingBatch].errors = _stateObj._errors; - - // track the warning collection (not available from TdsParser after ExecuteNonQuery) - // and the which warnings are associated with the just completed rpc batch - _SqlRPCBatchArray[_currentlyExecutingBatch].warningsIndexStart = - ((0 < _currentlyExecutingBatch) - ? _SqlRPCBatchArray[_currentlyExecutingBatch - 1].warningsIndexEnd - : 0); - _SqlRPCBatchArray[_currentlyExecutingBatch].warningsIndexEnd = _stateObj.WarningCount; - _SqlRPCBatchArray[_currentlyExecutingBatch].warnings = _stateObj._warnings; - + OnDone(_stateObj, _currentlyExecutingBatch, _SqlRPCBatchArray, _rowsAffected); _currentlyExecutingBatch++; Debug.Assert(_parameterCollectionList.Count >= _currentlyExecutingBatch, "OnDoneProc: Too many DONEPROC events"); } } + private static void OnDone(TdsParserStateObject stateObj, int index, _SqlRPC[] array, int rowsAffected) + { + _SqlRPC current = array[index]; + _SqlRPC previous = (index > 0) ? array[index - 1] : null; + + // track the records affected for the just completed rpc batch + // _rowsAffected is cumulative for ExecuteNonQuery across all rpc batches + current.cumulativeRecordsAffected = rowsAffected; + + current.recordsAffected = + (((previous != null) && (0 <= rowsAffected)) + ? (rowsAffected - Math.Max(previous.cumulativeRecordsAffected, 0)) + : rowsAffected); + + // track the error collection (not available from TdsParser after ExecuteNonQuery) + // and the which errors are associated with the just completed rpc batch + current.errorsIndexStart = previous?.errorsIndexEnd ?? 0; + current.errorsIndexEnd = stateObj.ErrorCount; + current.errors = stateObj._errors; + + // track the warning collection (not available from TdsParser after ExecuteNonQuery) + // and the which warnings are associated with the just completed rpc batch + current.warningsIndexStart = previous?.warningsIndexEnd ?? 0; + current.warningsIndexEnd = stateObj.WarningCount; + current.warnings = stateObj._warnings; + } + internal void OnReturnStatus(int status) { // Don't set the return status if this is the status for sp_describe_parameter_encryption. From 14063d21858f3d00d6cbfa75dfb1b5f9146865c5 Mon Sep 17 00:00:00 2001 From: Wraith2 Date: Fri, 6 Dec 2019 13:53:25 +0000 Subject: [PATCH 6/6] address feedback --- .../src/Microsoft/Data/SqlClient/SqlCommand.cs | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) 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 2cf77eb7ac..0283736785 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 @@ -3819,7 +3819,7 @@ private void ReadDescribeEncryptionParameterResults(SqlDataReader ds, ReadOnlyDi // since for non-BatchRPCMode mode, paramoptions gets thrown away and reconstructed in BuildExecuteSql. int options = (int)(rpc.userParamMap[index] >> 32); options |= TdsEnums.RPC_PARAM_ENCRYPTED; - rpc.userParamMap[index] = ((((long)options) << 32) | (long)index); + rpc.userParamMap[index] = ((((long)options) << 32) | (long)index); } break; @@ -5127,10 +5127,7 @@ private void SetUpRPCParameters(_SqlRPC rpc, bool inSchema, SqlParameterCollecti byte options = 0; // set output bit - if ( - parameter.Direction == ParameterDirection.InputOutput || - parameter.Direction == ParameterDirection.Output - ) + if (parameter.Direction == ParameterDirection.InputOutput || parameter.Direction == ParameterDirection.Output) { options = TdsEnums.RPC_PARAM_BYREF; } @@ -5205,7 +5202,6 @@ private _SqlRPC BuildPrepExec(CommandBehavior behavior) return rpc; } - // // returns true if the parameter is not a return value // and it's value is not DBNull (for a nullable parameter) @@ -5252,8 +5248,6 @@ private int GetParameterCount(SqlParameterCollection parameters) return (null != parameters) ? parameters.Count : 0; } - - // // build the RPC record header for this stored proc and add parameters // @@ -5269,12 +5263,6 @@ private void BuildRPC(bool inSchema, SqlParameterCollection parameters, ref _Sql SetUpRPCParameters(rpc, inSchema, parameters); } - // - // build the RPC record header for sp_unprepare - // - // prototype for sp_unprepare is: - // sp_unprepare(@handle) - // build the RPC record header for sp_execute // // prototype for sp_execute is: