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 6ae23e11e6..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 @@ -3374,7 +3374,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; @@ -3419,20 +3419,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); @@ -3482,20 +3473,23 @@ 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 - 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. 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; - sqlParam = GetSqlParameterWithQueryText(text); + text = (string)originalRpcRequest.systemParams[0].Value; + //@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 { @@ -3504,42 +3498,57 @@ 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); + 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.parameters[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,20 +3577,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.parameters[1] = sqlParam; + //@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.parameters[2] = attestationParametersParam; + SqlParameter attestationParametersParam = describeParameterEncryptionRequest.systemParams[2]; + attestationParametersParam.Direction = ParameterDirection.Input; + attestationParametersParam.Size = attestationParameters.Length; + attestationParametersParam.Value = attestationParameters; } } @@ -3731,8 +3739,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) @@ -3756,13 +3763,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 ?? 0; + int recievedMetadataCount = 0; if (!enclaveMetadataExists || ds.NextResult()) { + // Iterate over the parameter names to read the encryption type info while (ds.Read()) { @@ -3774,16 +3779,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) @@ -3811,7 +3817,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; @@ -3822,15 +3830,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()); + } } } @@ -4225,7 +4238,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!"); @@ -4715,86 +4728,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. @@ -5068,10 +5049,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) @@ -5098,40 +5078,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 @@ -5142,20 +5124,20 @@ 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) { @@ -5166,54 +5148,60 @@ 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; } - // // returns true if the parameter is not a return value // and it's value is not DBNull (for a nullable parameter) @@ -5246,7 +5234,9 @@ private int CountSendableParameters(SqlParameterCollection parameters) for (int i = 0; i < count; i++) { if (ShouldSendParameter(parameters[i])) + { cParams++; + } } } return cParams; @@ -5255,7 +5245,7 @@ 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; } // @@ -5264,20 +5254,15 @@ private int GetParameterCount(SqlParameterCollection 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); } - // - // 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: @@ -5286,24 +5271,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; } @@ -5317,20 +5301,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; @@ -5339,19 +5323,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); } } @@ -5363,7 +5350,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"); @@ -5396,27 +5383,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"); } @@ -5424,18 +5411,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"); } @@ -5597,7 +5586,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 c331e19a95..ccb91ccef5 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,226 +8747,353 @@ 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); + Task writeParamTask = TDSExecuteRPCAddParameter(stateObj, param, mt, options); - // 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) + if (!sync) { - precision = param.GetActualPrecision(); - scale = param.GetActualScale(); - - if (precision > TdsEnums.MAX_NUMERIC_PRECISION) + if (writeParamTask == null) { - throw SQL.PrecisionValueOutOfRange(precision); + writeParamTask = stateObj.WaitForAccumulatedWrites(); } - // bug 49512, make sure the value matches the scale the user enters - if (!isNull) + if (writeParamTask != null) { - if (isSqlVal) + Task task = null; + if (completion == null) { - 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); - } - } + completion = new TaskCompletionSource(); + task = completion.Task; } - else - { - value = AdjustDecimalScale((Decimal)value, scale); - SqlDecimal sqlValue = new SqlDecimal((Decimal)value); + TDSExecuteRPCParameterSetupWriteCompletion( + cmd, + rpcArray, + timeout, + inSchema, + notificationRequest, + stateObj, + isCommandProc, + sync, + completion, + ii, + i + 1, + writeParamTask + ); - // If Precision is specified, verify value precision vs param precision - if (precision != 0) - { - if (precision < sqlValue.Precision) - { - throw ADP.ParameterValueOutOfRange((Decimal)value); - } - } + // Take care of releasing the locks + if (releaseConnectionLock) + { + task.ContinueWith( + (_, state) => ((SqlInternalConnectionTds)state)._parserLock.Release(), + state: _connHandler, + TaskScheduler.Default + ); + releaseConnectionLock = false; } + + return task; } } +#if DEBUG + else + { + Debug.Assert(writeParamTask == null, "Should not have a task when executing sync"); + } +#endif + } // parameter for loop - bool isParameterEncrypted = 0 != (rpcext.paramoptions[i] & TdsEnums.RPC_PARAM_ENCRYPTED); + // If this is not the last RPC we are sending, add the batch flag + if (ii < (rpcArray.Length - 1)) + { + stateObj.WriteByte(TdsEnums.YUKON_RPCBATCHFLAG); + } + } // rpc for loop - // Additional information we need to send over wire to the server when writing encrypted parameters. - SqlColumnEncryptionInputParameterInfo encryptedParameterInfoToWrite = null; + Task execFlushTask = stateObj.ExecuteFlush(); + Debug.Assert(!sync || execFlushTask == null, "Should not get a task when executing sync"); + if (execFlushTask != null) + { + Task task = 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"); + if (completion == null) + { + completion = new TaskCompletionSource(); + task = completion.Task; + } - byte[] serializedValue = null; - byte[] encryptedValue = null; + TDSExecuteRPCParameterSetupFlushCompletion(stateObj, completion, execFlushTask, releaseConnectionLock); - 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); - } + // TDSExecuteRPCParameterSetupFlushCompletion calling ExecuteFlushTaskCallback will take care of the locks for us + releaseConnectionLock = false; - 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); - } + return task; + } + } + catch (Exception e) + { + if (!ADP.IsCatchableExceptionType(e)) + { + throw; + } - Debug.Assert(encryptedValue != null && encryptedValue.Length > 0, - "encryptedValue should not be null or empty in TdsExecuteRPC."); - } - else - { - encryptedValue = null; - } + FailureCleanup(stateObj, e); + + throw; + } + FinalizeExecuteRPC(stateObj); + if (completion != null) + { + completion.SetResult(null); + } + return null; + } + catch (Exception e) + { + FinalizeExecuteRPC(stateObj); + if (completion != null) + { + completion.SetException(e); + return null; + } + else + { + throw; + } + } + finally + { + Debug.Assert(firstCall || !releaseConnectionLock, "Shouldn't be releasing locks synchronously after the first call"); + if (releaseConnectionLock) + { + _connHandler._parserLock.Release(); + } + } + } - // 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; + 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; + } + } - encryptedParameterInfoToWrite = new SqlColumnEncryptionInputParameterInfo(param.GetMetadataForTypeInfo(), - param.CipherMetadata); + WriteParameterName(param.ParameterNameFixed, stateObj); - // Set the value to the encrypted value and mark isSqlVal as false for VARBINARY encrypted value. - value = encryptedValue; - isSqlVal = false; - } + // Write parameter status + stateObj.WriteByte(options); - Debug.Assert(isParameterEncrypted == (encryptedParameterInfoToWrite != null), - "encryptedParameterInfoToWrite can be not null if and only if isParameterEncrypted is true."); + // 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(); - Debug.Assert(!isSqlVal || !isParameterEncrypted, "isParameterEncrypted can be true only if isSqlVal is false."); + //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); + } - // 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); + // bug 49512, make sure the value matches the scale the user enters + if (!isNull) + { + if (isSqlVal) + { + value = AdjustSqlDecimalScale((SqlDecimal)value, scale); - // handle variants here: the SQLVariant writing routine will write the maxlen and actual len columns - if (mt.TDSType == TdsEnums.SQLVARIANT) + // If Precision is specified, verify value precision vs param precision + if (precision != 0) + { + if (precision < ((SqlDecimal)value).Precision) { - // 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; + throw ADP.ParameterValueOutOfRange((SqlDecimal)value); } + } + } + else + { + value = AdjustDecimalScale((Decimal)value, scale); - int codePageByteSize = 0; - int maxsize = 0; + SqlDecimal sqlValue = new SqlDecimal((Decimal)value); - if (mt.IsAnsiType) + // If Precision is specified, verify value precision vs param precision + if (precision != 0) + { + if (precision < sqlValue.Precision) { - // Avoid the following code block if ANSI but unfilled LazyMat blob - if ((!isNull) && (!isDataFeed)) - { - string s; + throw ADP.ParameterValueOutOfRange((Decimal)value); + } + } + } + } + } - 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; - } + bool isParameterEncrypted = 0 != (options & TdsEnums.RPC_PARAM_ENCRYPTED); - codePageByteSize = GetEncodingCharLength(s, actualSize, param.Offset, _defaultEncoding); - } + // Additional information we need to send over wire to the server when writing encrypted parameters. + SqlColumnEncryptionInputParameterInfo encryptedParameterInfoToWrite = null; - 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; - } + // 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); } @@ -9027,8 +9154,8 @@ internal Task TdsExecuteRPC(SqlCommand cmd, _SqlRPC[] rpcArray, int timeout, boo 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; + Debug.Assert(null != udtVal, "GetBytes returned null instance. Make sure that it always returns non-null value"); + size = udtVal.Length; if (size >= maxSupportedSize && maxsize != -1) { @@ -9052,258 +9179,150 @@ internal Task TdsExecuteRPC(SqlCommand cmd, _SqlRPC[] rpcArray, int timeout, boo throw ADP.ArgumentOutOfRange(nameof(names)); } - WriteUDTMetaData(value, names[0], names[1], names[2], stateObj); + 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); - } - - if (!sync) - { - if (writeParamTask == null) - { - writeParamTask = stateObj.WaitForAccumulatedWrites(); - } + 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; + } - if (writeParamTask != null) - { - Task task = null; - if (completion == null) - { - completion = new TaskCompletionSource(); - task = completion.Task; - } + WriteParameterVarLen(mt, maxsize, false /*IsNull*/, stateObj); + } + } - 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)); + // 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); + } - // Take care of releasing the locks - if (releaseConnectionLock) - { - task.ContinueWith( - (_, state) => ((SqlInternalConnectionTds)state)._parserLock.Release(), - state: _connHandler, - TaskScheduler.Default - ); - releaseConnectionLock = false; - } + stateObj.WriteByte(scale); + } + else if (mt.IsVarTime) + { + stateObj.WriteByte(param.GetActualScale()); + } - return task; - } - } -#if DEBUG - else - { - Debug.Assert(writeParamTask == null, "Should not have a task when executing sync"); - } -#endif - } // parameter for loop + // write out collation or xml metadata - // If this is not the last RPC we are sending, add the batch flag - if (ii < (rpcArray.Length - 1)) - { - stateObj.WriteByte(TdsEnums.YUKON_RPCBATCHFLAG); - } - } // rpc for loop + 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 - Task execFlushTask = stateObj.ExecuteFlush(); - Debug.Assert(!sync || execFlushTask == null, "Should not get a task when executing sync"); - if (execFlushTask != null) + if ((param.XmlSchemaCollectionDatabase != null) && (param.XmlSchemaCollectionDatabase != ADP.StrEmpty)) { - Task task = null; - - if (completion == null) - { - completion = new TaskCompletionSource(); - task = completion.Task; - } - - TDSExecuteRPCParameterSetupFlushCompletion(stateObj, completion, execFlushTask, releaseConnectionLock); - - // TDSExecuteRPCParameterSetupFlushCompletion calling ExecuteFlushTaskCallback will take care of the locks for us - releaseConnectionLock = false; - - return task; + tempLen = (param.XmlSchemaCollectionDatabase).Length; + stateObj.WriteByte((byte)(tempLen)); + WriteString(param.XmlSchemaCollectionDatabase, tempLen, 0, stateObj); } - } - catch (Exception e) - { - if (!ADP.IsCatchableExceptionType(e)) + else { - throw; + stateObj.WriteByte(0); // No dbname } - FailureCleanup(stateObj, e); + 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 + } - throw; + 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 + } } - FinalizeExecuteRPC(stateObj); - if (completion != null) + else { - completion.SetResult(null); + stateObj.WriteByte(0); // No schema } - return null; } - catch (Exception e) + else if (mt.IsCharType) { - FinalizeExecuteRPC(stateObj); - if (completion != null) + // 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) { - completion.SetException(e); - return null; + writeParamTask = WriteSqlValue(value, mt, actualSize, codePageByteSize, param.Offset, stateObj); } else { - throw; + // 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); } } - finally + + // Send encryption metadata for encrypted parameters. + if (isParameterEncrypted) { - Debug.Assert(firstCall || !releaseConnectionLock, "Shouldn't be releasing locks synchronously after the first call"); - if (releaseConnectionLock) - { - _connHandler._parserLock.Release(); - } + writeParamTask = WriteEncryptionMetadata(writeParamTask, encryptedParameterInfoToWrite, stateObj); } + + return writeParamTask; } // This is in its own method to avoid always allocating the lambda in TDSExecuteRPCParameter 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