diff --git a/src/System.Data.SqlClient/src/System/Data/SqlClient/SqlCommand.cs b/src/System.Data.SqlClient/src/System/Data/SqlClient/SqlCommand.cs index 53abc5286adb..491b0e5c2843 100644 --- a/src/System.Data.SqlClient/src/System/Data/SqlClient/SqlCommand.cs +++ b/src/System.Data.SqlClient/src/System/Data/SqlClient/SqlCommand.cs @@ -52,8 +52,9 @@ private enum EXECTYPE // // _prepareHandle - the handle of a prepared command. Apparently there can be multiple prepared commands at a time - a feature that we do not support yet. + private static readonly object s_cachedInvalidPrepareHandle = (object)-1; private bool _inPrepare = false; - private int _prepareHandle = -1; + private object _prepareHandle = s_cachedInvalidPrepareHandle; // this is an int which is used in the object typed SqlParameter.Value field, avoid repeated boxing by storing in a box private bool _hiddenPrepare = false; private int _preparedConnectionCloseCount = -1; private int _preparedConnectionReconnectCount = -1; @@ -83,7 +84,7 @@ internal bool InPrepare } // Cached info for async executions - private class CachedAsyncState + private sealed class CachedAsyncState { private int _cachedAsyncCloseCount = -1; // value of the connection's CloseCount property when the asyncResult was set; tracks when connections are closed after an async operation private TaskCompletionSource _cachedAsyncResult = null; @@ -261,7 +262,7 @@ private SqlCommand(SqlCommand from) : this() // Don't allow the connection to be changed while in an async operation. if (_activeConnection != value && _activeConnection != null) { // If new value... - if (cachedAsyncState.PendingAsyncOperation) + if (_cachedAsyncState!=null && _cachedAsyncState.PendingAsyncOperation) { // If in pending async state, throw. throw SQL.CannotModifyPropertyAsyncOperationInProgress(); } @@ -292,7 +293,7 @@ private SqlCommand(SqlCommand from) : this() finally { // clean prepare status (even successful Unprepare does not do that) - _prepareHandle = -1; + _prepareHandle = s_cachedInvalidPrepareHandle; _execType = EXECTYPE.UNPREPARED; } } @@ -672,7 +673,7 @@ internal void Unprepare() if ((_activeConnection.CloseCount != _preparedConnectionCloseCount) || (_activeConnection.ReconnectCount != _preparedConnectionReconnectCount)) { // reset our handle - _prepareHandle = -1; + _prepareHandle = s_cachedInvalidPrepareHandle; } _cachedMetaData = null; @@ -918,7 +919,7 @@ public IAsyncResult BeginExecuteNonQuery(AsyncCallback callback, object stateObj cachedAsyncState.SetActiveConnectionAndResult(completion, nameof(EndExecuteNonQuery), _activeConnection); if (execNQ != null) { - AsyncHelper.ContinueTask(execNQ, completion, () => BeginExecuteNonQueryInternalReadStage(completion)); + BeginExecuteNonQuerySetupCompletionContinuation(completion, execNQ); } else { @@ -952,6 +953,12 @@ public IAsyncResult BeginExecuteNonQuery(AsyncCallback callback, object stateObj } } + // This is in its own method to avoid always allocating the lambda in BeginExecuteNonQuery + private void BeginExecuteNonQuerySetupCompletionContinuation(TaskCompletionSource completion, Task execNQ) + { + AsyncHelper.ContinueTask(execNQ, completion, () => BeginExecuteNonQueryInternalReadStage(completion)); + } + private void BeginExecuteNonQueryInternalReadStage(TaskCompletionSource completion) { // Read SNI does not have catches for async exceptions, handle here. @@ -1170,7 +1177,7 @@ private Task InternalExecuteNonQuery(TaskCompletionSource completion, bo { if (task != null) { - task = AsyncHelper.CreateContinuationTask(task, () => reader.Close()); + task = InternalExecuteNonQuerySetupCloseContinuation(task, reader); } else { @@ -1182,6 +1189,11 @@ private Task InternalExecuteNonQuery(TaskCompletionSource completion, bo return task; } + private static Task InternalExecuteNonQuerySetupCloseContinuation(Task task, SqlDataReader reader) + { + return AsyncHelper.CreateContinuationTask(task, () => reader.Close()); + } + public XmlReader ExecuteXmlReader() { // Reset _pendingCancel upon entry into any Execute - used to synchronize state @@ -2328,27 +2340,7 @@ private Task RunExecuteNonQueryTds(string methodName, bool async, int timeout, b TaskCompletionSource completion = new TaskCompletionSource(); _activeConnection.RegisterWaitingForReconnect(completion.Task); _reconnectionCompletionSource = completion; - CancellationTokenSource timeoutCTS = new CancellationTokenSource(); - AsyncHelper.SetTimeoutException(completion, timeout, SQL.CR_ReconnectTimeout, timeoutCTS.Token); - AsyncHelper.ContinueTask(reconnectTask, completion, - () => - { - if (completion.Task.IsCompleted) - { - return; - } - Interlocked.CompareExchange(ref _reconnectionCompletionSource, null, completion); - timeoutCTS.Cancel(); - Task subTask = RunExecuteNonQueryTds(methodName, async, TdsParserStaticMethods.GetRemainingTimeout(timeout, reconnectionStart), asyncWrite); - if (subTask == null) - { - completion.SetResult(null); - } - else - { - AsyncHelper.ContinueTask(subTask, completion, () => completion.SetResult(null)); - } - }, connectionToAbort: _activeConnection); + RunExecuteNonQueryTdsSetupReconnnectContinuation(methodName, async, timeout, asyncWrite, reconnectTask, reconnectionStart, completion); return completion.Task; } else @@ -2401,6 +2393,31 @@ private Task RunExecuteNonQueryTds(string methodName, bool async, int timeout, b return null; } + // This is in its own method to avoid always allocating the lambda in RunExecuteNonQueryTds + private void RunExecuteNonQueryTdsSetupReconnnectContinuation(string methodName, bool async, int timeout, bool asyncWrite, Task reconnectTask, long reconnectionStart, TaskCompletionSource completion) + { + CancellationTokenSource timeoutCTS = new CancellationTokenSource(); + AsyncHelper.SetTimeoutException(completion, timeout, SQL.CR_ReconnectTimeout, timeoutCTS.Token); + AsyncHelper.ContinueTask(reconnectTask, completion, + () => + { + if (completion.Task.IsCompleted) + { + return; + } + Interlocked.CompareExchange(ref _reconnectionCompletionSource, null, completion); + timeoutCTS.Cancel(); + Task subTask = RunExecuteNonQueryTds(methodName, async, TdsParserStaticMethods.GetRemainingTimeout(timeout, reconnectionStart), asyncWrite); + if (subTask == null) + { + completion.SetResult(null); + } + else + { + AsyncHelper.ContinueTask(subTask, completion, () => completion.SetResult(null)); + } + }, connectionToAbort: _activeConnection); + } internal SqlDataReader RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, bool returnStream, [CallerMemberName] string method = "") { @@ -2470,28 +2487,7 @@ private SqlDataReader RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavi TaskCompletionSource completion = new TaskCompletionSource(); _activeConnection.RegisterWaitingForReconnect(completion.Task); _reconnectionCompletionSource = completion; - CancellationTokenSource timeoutCTS = new CancellationTokenSource(); - AsyncHelper.SetTimeoutException(completion, timeout, SQL.CR_ReconnectTimeout, timeoutCTS.Token); - AsyncHelper.ContinueTask(reconnectTask, completion, - () => - { - if (completion.Task.IsCompleted) - { - return; - } - Interlocked.CompareExchange(ref _reconnectionCompletionSource, null, completion); - timeoutCTS.Cancel(); - Task subTask; - RunExecuteReaderTds(cmdBehavior, runBehavior, returnStream, async, TdsParserStaticMethods.GetRemainingTimeout(timeout, reconnectionStart), out subTask, asyncWrite, ds); - if (subTask == null) - { - completion.SetResult(null); - } - else - { - AsyncHelper.ContinueTask(subTask, completion, () => completion.SetResult(null)); - } - }, connectionToAbort: _activeConnection); + RunExecuteReaderTdsSetupReconnectContinuation(cmdBehavior, runBehavior, returnStream, async, timeout, asyncWrite, ds, reconnectTask, reconnectionStart, completion); task = completion.Task; return ds; } @@ -2568,7 +2564,7 @@ private SqlDataReader RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavi if (_execType == EXECTYPE.PREPARED) { - Debug.Assert(this.IsPrepared && (_prepareHandle != -1), "invalid attempt to call sp_execute without a handle!"); + Debug.Assert(this.IsPrepared && ((int)_prepareHandle != -1), "invalid attempt to call sp_execute without a handle!"); rpc = BuildExecute(inSchema); } else if (_execType == EXECTYPE.PREPAREPENDING) @@ -2627,15 +2623,7 @@ private SqlDataReader RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavi decrementAsyncCountOnFailure = false; if (writeTask != null) { - task = AsyncHelper.CreateContinuationTask(writeTask, () => - { - _activeConnection.GetOpenTdsConnection(); // it will throw if connection is closed - cachedAsyncState.SetAsyncReaderState(ds, runBehavior, optionSettings); - }, - onFailure: (exc) => - { - _activeConnection.GetOpenTdsConnection().DecrementAsyncCount(); - }); + task = RunExecuteReaderTdsSetupContinuation(runBehavior, ds, optionSettings, writeTask); } else { @@ -2674,6 +2662,47 @@ private SqlDataReader RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavi return ds; } + // This is in its own method to avoid always allocating the lambda in RunExecuteReaderTds + private Task RunExecuteReaderTdsSetupContinuation(RunBehavior runBehavior, SqlDataReader ds, string optionSettings, Task writeTask) + { + Task task = AsyncHelper.CreateContinuationTask(writeTask, () => + { + _activeConnection.GetOpenTdsConnection(); // it will throw if connection is closed + cachedAsyncState.SetAsyncReaderState(ds, runBehavior, optionSettings); + }, + onFailure: (exc) => + { + _activeConnection.GetOpenTdsConnection().DecrementAsyncCount(); + }); + return task; + } + + // This is in its own method to avoid always allocating the lambda in RunExecuteReaderTds + private void RunExecuteReaderTdsSetupReconnectContinuation(CommandBehavior cmdBehavior, RunBehavior runBehavior, bool returnStream, bool async, int timeout, bool asyncWrite, SqlDataReader ds, Task reconnectTask, long reconnectionStart, TaskCompletionSource completion) + { + CancellationTokenSource timeoutCTS = new CancellationTokenSource(); + AsyncHelper.SetTimeoutException(completion, timeout, SQL.CR_ReconnectTimeout, timeoutCTS.Token); + AsyncHelper.ContinueTask(reconnectTask, completion, + () => + { + if (completion.Task.IsCompleted) + { + return; + } + Interlocked.CompareExchange(ref _reconnectionCompletionSource, null, completion); + timeoutCTS.Cancel(); + Task subTask; + RunExecuteReaderTds(cmdBehavior, runBehavior, returnStream, async, TdsParserStaticMethods.GetRemainingTimeout(timeout, reconnectionStart), out subTask, asyncWrite, ds); + if (subTask == null) + { + completion.SetResult(null); + } + else + { + AsyncHelper.ContinueTask(subTask, completion, () => completion.SetResult(null)); + } + }, connectionToAbort: _activeConnection); + } private SqlDataReader CompleteAsyncExecuteReader() { @@ -2863,16 +2892,16 @@ private void ValidateCommand(bool async, [CallerMemberName] string method = "") private void ValidateAsyncCommand() { - if (cachedAsyncState.PendingAsyncOperation) + if (_cachedAsyncState!=null && _cachedAsyncState.PendingAsyncOperation) { // Enforce only one pending async execute at a time. - if (cachedAsyncState.IsActiveConnectionValid(_activeConnection)) + if (_cachedAsyncState.IsActiveConnectionValid(_activeConnection)) { throw SQL.PendingBeginXXXExists(); } else { _stateObj = null; // Session was re-claimed by session pool upon connection close. - cachedAsyncState.ResetAsyncState(); + _cachedAsyncState.ResetAsyncState(); } } } @@ -3175,10 +3204,9 @@ private SqlParameter GetParameterForOutputValueExtraction(SqlParameterCollection return null; } - private void GetRPCObject(int paramCount, ref _SqlRPC rpc) + private void GetRPCObject(int systemParamCount, int userParamCount, ref _SqlRPC rpc) { // Designed to minimize necessary allocations - int ii; if (rpc == null) { if (_rpcArrayOf1 == null) @@ -3189,42 +3217,39 @@ private void GetRPCObject(int paramCount, ref _SqlRPC rpc) rpc = _rpcArrayOf1[0]; } - rpc.ProcID = 0; - rpc.rpcName = null; rpc.options = 0; + rpc.systemParamCount = systemParamCount; - + 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) { - 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 @@ -3235,12 +3260,12 @@ 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; + options = TdsEnums.RPC_PARAM_BYREF; // set default value bit if (parameter.Direction != ParameterDirection.Output) @@ -3252,50 +3277,58 @@ 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; + const int systemParameterCount = 3; - int count = CountSendableParameters(_parameters); + 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.Size = paramList.Length; sqlParam.Value = paramList; - rpc.parameters[1] = sqlParam; //@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, systemParameterCount, false, _parameters); + return rpc; } @@ -3350,9 +3383,10 @@ 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); @@ -3371,25 +3405,24 @@ private void BuildRPC(bool inSchema, SqlParameterCollection parameters, ref _Sql private _SqlRPC BuildExecute(bool inSchema) { - Debug.Assert(_prepareHandle != -1, "Invalid call to sp_execute without a valid handle!"); - int j = 1; + Debug.Assert((int)_prepareHandle != -1, "Invalid call to sp_execute without a valid handle!"); + 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, systemParameterCount, inSchema, _parameters); return rpc; } @@ -3401,22 +3434,23 @@ private _SqlRPC BuildExecute(bool inSchema) private void BuildExecuteSql(CommandBehavior behavior, string commandText, SqlParameterCollection parameters, ref _SqlRPC rpc) { - Debug.Assert(_prepareHandle == -1, "This command has an existing handle, use sp_execute!"); + 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; @@ -3425,19 +3459,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, systemParamCount, inSchema, parameters); } } @@ -3446,21 +3483,23 @@ internal string BuildParamList(TdsParser parser, SqlParameterCollection paramete { StringBuilder paramList = new StringBuilder(); bool fAddSeperator = false; + int count = parameters.Count; - int count = 0; - - count = parameters.Count; for (int i = 0; i < count; i++) { SqlParameter sqlParam = parameters[i]; sqlParam.Validate(i, CommandType.StoredProcedure == CommandType); // skip ReturnValue parameters; we never send them to the server if (!ShouldSendParameter(sqlParam)) + { continue; + } // add our separator for the ith parameter if (fAddSeperator) + { paramList.Append(','); + } paramList.Append(sqlParam.ParameterNameFixed); @@ -3471,7 +3510,7 @@ internal string BuildParamList(TdsParser parser, SqlParameterCollection paramete // paragraph above doesn't seem to be correct. Server won't find the type // if we don't provide a fully qualified name - paramList.Append(" "); + paramList.Append(' '); if (mt.SqlDbType == SqlDbType.Udt) { string fullTypeName = sqlParam.UdtTypeName; @@ -3581,7 +3620,10 @@ internal string BuildParamList(TdsParser parser, SqlParameterCollection paramete // set the output bit for Output or InputOutput parameters if (sqlParam.Direction != ParameterDirection.Input) - paramList.Append(" " + TdsEnums.PARAM_OUTPUT); + { + paramList.Append(' '); + paramList.Append(TdsEnums.PARAM_OUTPUT); + } } return paramList.ToString(); diff --git a/src/System.Data.SqlClient/src/System/Data/SqlClient/SqlConnection.cs b/src/System.Data.SqlClient/src/System/Data/SqlClient/SqlConnection.cs index 28676a079b91..6eea597683ce 100644 --- a/src/System.Data.SqlClient/src/System/Data/SqlClient/SqlConnection.cs +++ b/src/System.Data.SqlClient/src/System/Data/SqlClient/SqlConnection.cs @@ -15,6 +15,7 @@ using System.IO; using System.Globalization; using System.Security; +using System.Runtime.CompilerServices; namespace System.Data.SqlClient { @@ -911,7 +912,7 @@ internal Task ValidateAndReconnect(Action beforeDisconnect, int timeout) catch (SqlException) { } - runningReconnect = Task.Run(() => ReconnectAsync(timeout)); + runningReconnect = ValidateAndReconnectSetupReconnectContinuation(timeout); // if current reconnect is not null, somebody already started reconnection task - some kind of race condition Debug.Assert(_currentReconnectionTask == null, "Duplicate reconnection tasks detected"); _currentReconnectionTask = runningReconnect; @@ -949,6 +950,13 @@ internal Task ValidateAndReconnect(Action beforeDisconnect, int timeout) return runningReconnect; } + // This is in its own method to avoid always allocating the lambda in ValidateAndReconnect + [MethodImpl(MethodImplOptions.NoInlining)] + private Task ValidateAndReconnectSetupReconnectContinuation(int timeout) + { + return Task.Run(() => ReconnectAsync(timeout)); + } + // this is straightforward, but expensive method to do connection resiliency - it take locks and all preparations as for TDS request partial void RepairInnerConnection() { diff --git a/src/System.Data.SqlClient/src/System/Data/SqlClient/TdsParser.cs b/src/System.Data.SqlClient/src/System/Data/SqlClient/TdsParser.cs index e7e35d351c0e..e981537da5cb 100644 --- a/src/System.Data.SqlClient/src/System/Data/SqlClient/TdsParser.cs +++ b/src/System.Data.SqlClient/src/System/Data/SqlClient/TdsParser.cs @@ -9,6 +9,7 @@ using System.Diagnostics; using System.Globalization; using System.IO; +using System.Runtime.CompilerServices; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -1419,9 +1420,14 @@ internal void WriteInt(int v, TdsParserStateObject stateObj) // internal void WriteFloat(float v, TdsParserStateObject stateObj) { - byte[] bytes = BitConverter.GetBytes(v); - - stateObj.WriteByteArray(bytes, bytes.Length, 0); +#if netcoreapp // BitConverter.TryGetBytes is only availble in netcoreapp 2.1> at this time, review support when changing + Span bytes = stackalloc byte[sizeof(float)]; + BitConverter.TryWriteBytes(bytes, v); + stateObj.WriteByteSpan(bytes); +#else + byte[] bytes = BitConverter.GetBytes(v); + stateObj.WriteByteArray(bytes, bytes.Length, 0); +#endif } // @@ -1493,9 +1499,14 @@ internal void WriteUnsignedLong(ulong uv, TdsParserStateObject stateObj) // internal void WriteDouble(double v, TdsParserStateObject stateObj) { +#if netcoreapp // BitConverter.TryGetBytes is only availble in netcoreapp 2.1> at this time, review support when changing + Span bytes = stackalloc byte[sizeof(double)]; + BitConverter.TryWriteBytes(bytes, v); + stateObj.WriteByteSpan(bytes); +#else byte[] bytes = BitConverter.GetBytes(v); - stateObj.WriteByteArray(bytes, bytes.Length, 0); +#endif } internal void PrepareResetConnection(bool preserveTransaction) @@ -2135,7 +2146,7 @@ internal bool TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataRead { // Dev11 #344723: SqlClient stress hang System_Data!Tcp::ReadSync via a call to SqlDataReader::Close // Spin until SendAttention has cleared _attentionSending, this prevents a race condition between receiving the attention ACK and setting _attentionSent - SpinWait.SpinUntil(() => !stateObj._attentionSending); + TryRunSetupSpinWaitContinuation(stateObj); Debug.Assert(stateObj._attentionSent, "Attention ACK has been received without attention sent"); if (stateObj._attentionSent) @@ -2159,6 +2170,12 @@ internal bool TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataRead return true; } + // This is in its own method to avoid always allocating the lambda in TryRun + private static void TryRunSetupSpinWaitContinuation(TdsParserStateObject stateObj) + { + SpinWait.SpinUntil(() => !stateObj._attentionSending); + } + private bool TryProcessEnvChange(int tokenLength, TdsParserStateObject stateObj, out SqlEnvChange[] sqlEnvChange) { // There could be multiple environment change messages following this token. @@ -6972,29 +6989,7 @@ internal Task TdsExecuteSQLBatch(string text, int timeout, SqlNotificationReques // Need to wait for flush - continuation will unlock the connection bool taskReleaseConnectionLock = releaseConnectionLock; releaseConnectionLock = false; - return executeTask.ContinueWith(t => - { - Debug.Assert(!t.IsCanceled, "Task should not be canceled"); - try - { - if (t.IsFaulted) - { - FailureCleanup(stateObj, t.Exception.InnerException); - throw t.Exception.InnerException; - } - else - { - stateObj.SniContext = SniContext.Snix_Read; - } - } - finally - { - if (taskReleaseConnectionLock) - { - _connHandler._parserLock.Release(); - } - } - }, TaskScheduler.Default); + return TDSExecuteSqlBatchSetupReleaseContinuation(stateObj, executeTask, taskReleaseConnectionLock); } // Finished sync @@ -7020,6 +7015,34 @@ internal Task TdsExecuteSQLBatch(string text, int timeout, SqlNotificationReques } } + // This is in its own method to avoid always allocating the lambda in TDSExecuteSqlBatch + private Task TDSExecuteSqlBatchSetupReleaseContinuation(TdsParserStateObject stateObj, Task executeTask, bool taskReleaseConnectionLock) + { + return executeTask.ContinueWith(t => + { + Debug.Assert(!t.IsCanceled, "Task should not be canceled"); + try + { + if (t.IsFaulted) + { + FailureCleanup(stateObj, t.Exception.InnerException); + throw t.Exception.InnerException; + } + else + { + stateObj.SniContext = SniContext.Snix_Read; + } + } + finally + { + if (taskReleaseConnectionLock) + { + _connHandler._parserLock.Release(); + } + } + }, TaskScheduler.Default); + } + internal Task TdsExecuteRPC(_SqlRPC[] rpcArray, int timeout, bool inSchema, SqlNotificationRequest notificationRequest, TdsParserStateObject stateObj, bool isCommandProc, bool sync = true, TaskCompletionSource completion = null, int startRpc = 0, int startParam = 0) { @@ -7105,15 +7128,19 @@ internal Task TdsExecuteRPC(_SqlRPC[] rpcArray, int timeout, bool inSchema, SqlN } // Stream out parameters - SqlParameter[] parameters = rpcext.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 + } // Validate parameters are not variable length without size and with null value. param.Validate(i, isCommandProc); @@ -7123,7 +7150,7 @@ internal Task TdsExecuteRPC(_SqlRPC[] rpcArray, int timeout, bool inSchema, SqlN 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; } @@ -7132,351 +7159,8 @@ internal Task TdsExecuteRPC(_SqlRPC[] rpcArray, int timeout, bool inSchema, SqlN { 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); - } - - // 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); - } - } - } - } - } - - // 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) - { - 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, param.GetActualScale(), actualSize, codePageByteSize, param.Offset, stateObj, param.Size, isDataFeed); - } - } + Task writeParamTask = TDSExecuteRPCAddParameter(stateObj, param, mt, options); if (!sync) { @@ -7494,11 +7178,7 @@ internal Task TdsExecuteRPC(_SqlRPC[] rpcArray, int timeout, bool inSchema, SqlN task = completion.Task; } - AsyncHelper.ContinueTask(writeParamTask, completion, - () => TdsExecuteRPC(rpcArray, timeout, inSchema, notificationRequest, stateObj, isCommandProc, sync, completion, - startRpc: ii, startParam: i + 1), - connectionToDoom: _connHandler, - onFailure: exc => TdsExecuteRPC_OnFailure(exc, stateObj)); + TDSExecuteRPCParameterSetupWriteCompletion(rpcArray, timeout, inSchema, notificationRequest, stateObj, isCommandProc, sync, completion, ii, i, writeParamTask); // Take care of releasing the locks if (releaseConnectionLock) @@ -7547,8 +7227,7 @@ internal Task TdsExecuteRPC(_SqlRPC[] rpcArray, int timeout, bool inSchema, SqlN task = completion.Task; } - bool taskReleaseConnectionLock = releaseConnectionLock; - execFlushTask.ContinueWith(tsk => ExecuteFlushTaskCallback(tsk, stateObj, completion, taskReleaseConnectionLock), TaskScheduler.Default); + TDSExecuteRPCParameterSetupFlushCompletion(stateObj, completion, execFlushTask, releaseConnectionLock); // ExecuteFlushTaskCallback will take care of the locks for us releaseConnectionLock = false; @@ -7597,6 +7276,387 @@ internal Task TdsExecuteRPC(_SqlRPC[] rpcArray, int timeout, bool inSchema, SqlN } } + // This is in its own method to avoid always allocating the lambda in TDSExecuteRPCParameter + private void TDSExecuteRPCParameterSetupFlushCompletion(TdsParserStateObject stateObj, TaskCompletionSource completion, Task execFlushTask, bool taskReleaseConnectionLock) + { + execFlushTask.ContinueWith(tsk => ExecuteFlushTaskCallback(tsk, stateObj, completion, taskReleaseConnectionLock), TaskScheduler.Default); + } + + // This is in its own method to avoid always allocating the lambda in TDSExecuteRPCParameter + private void TDSExecuteRPCParameterSetupWriteCompletion(_SqlRPC[] rpcArray, int timeout, bool inSchema, SqlNotificationRequest notificationRequest, TdsParserStateObject stateObj, bool isCommandProc, bool sync, TaskCompletionSource completion, int ii, int i, Task writeParamTask) + { + AsyncHelper.ContinueTask( + writeParamTask, + completion, + () => TdsExecuteRPC( + rpcArray, + timeout, + inSchema, + notificationRequest, + stateObj, + isCommandProc, + sync, + completion, + startRpc: ii, + startParam: i + 1 + ), + connectionToDoom: _connHandler, + onFailure: exc => TdsExecuteRPC_OnFailure(exc, stateObj) + ); + } + + 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); + } + + // 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); + } + } + } + } + } + + // 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) + { + 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 (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;//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, param.GetActualScale(), actualSize, codePageByteSize, param.Offset, stateObj, param.Size, isDataFeed); + } + } + return writeParamTask; + } + private void FinalizeExecuteRPC(TdsParserStateObject stateObj) { stateObj.SniContext = SniContext.Snix_Read; diff --git a/src/System.Data.SqlClient/src/System/Data/SqlClient/TdsParserHelperClasses.cs b/src/System.Data.SqlClient/src/System/Data/SqlClient/TdsParserHelperClasses.cs index 01923136a34d..5b62ff8a5144 100644 --- a/src/System.Data.SqlClient/src/System/Data/SqlClient/TdsParserHelperClasses.cs +++ b/src/System.Data.SqlClient/src/System/Data/SqlClient/TdsParserHelperClasses.cs @@ -559,8 +559,14 @@ sealed internal 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; @@ -573,17 +579,23 @@ sealed internal class _SqlRPC internal int warningsIndexEnd; internal SqlErrorCollection warnings; - internal string GetCommandTextOrRpcName() + public SqlParameter GetParameterByIndex(int index, out byte options) { - if (TdsEnums.RPC_PROCID_EXECUTESQL == ProcID) + options = 0; + SqlParameter retval = null; + if (index < systemParamCount) { - // Param 0 is the actual sql executing - return (string)parameters[0].Value; + retval = systemParams[index]; + options = systemParamOptions[index]; } else { - return rpcName; + long data = userParamMap[index - systemParamCount]; + int paramIndex = (int)(data & int.MaxValue); + options = (byte)((data >> 32) & 0xFF); + retval = userParams[paramIndex]; } + return retval; } } diff --git a/src/System.Data.SqlClient/src/System/Data/SqlClient/TdsParserStateObject.cs b/src/System.Data.SqlClient/src/System/Data/SqlClient/TdsParserStateObject.cs index dcc7eae1744e..26faa7316b12 100644 --- a/src/System.Data.SqlClient/src/System/Data/SqlClient/TdsParserStateObject.cs +++ b/src/System.Data.SqlClient/src/System/Data/SqlClient/TdsParserStateObject.cs @@ -2417,38 +2417,23 @@ internal bool IsConnectionAlive(bool throwOnException) } else { - uint error; + SniContext = SniContext.Snix_Connect; + uint error = CheckConnection(); - object readPacket = EmptyReadPacket; - - try + if ((error != TdsEnums.SNI_SUCCESS) && (error != TdsEnums.SNI_WAIT_TIMEOUT)) { - SniContext = SniContext.Snix_Connect; - - error = CheckConnection(); - - if ((error != TdsEnums.SNI_SUCCESS) && (error != TdsEnums.SNI_WAIT_TIMEOUT)) - { - // Connection is dead - isAlive = false; - if (throwOnException) - { - // Get the error from SNI so that we can throw the correct exception - AddError(_parser.ProcessSNIError(this)); - ThrowExceptionAndWarning(); - } - } - else + // Connection is dead + isAlive = false; + if (throwOnException) { - _lastSuccessfulIOTimer._value = DateTime.UtcNow.Ticks; + // Get the error from SNI so that we can throw the correct exception + AddError(_parser.ProcessSNIError(this)); + ThrowExceptionAndWarning(); } } - finally + else { - if (!IsPacketEmpty(readPacket)) - { - ReleasePacket(readPacket); - } + _lastSuccessfulIOTimer._value = DateTime.UtcNow.Ticks; } } } @@ -2998,11 +2983,28 @@ internal void WriteByte(byte b) _outBuff[_outBytesUsed++] = b; } - // - // Takes a byte array and writes it to the buffer. - // + internal Task WriteByteSpan(ReadOnlySpan span, bool canAccumulate = true, TaskCompletionSource completion = null) + { + return WriteBytes(span, span.Length, 0, canAccumulate, completion); + } + internal Task WriteByteArray(byte[] b, int len, int offsetBuffer, bool canAccumulate = true, TaskCompletionSource completion = null) { + return WriteBytes(ReadOnlySpan.Empty, len, offsetBuffer, canAccumulate, completion, b); + } + + // + // Takes a span or a byte array and writes it to the buffer + // If you pass in a span and a null array then the span wil be used. + // If you pass in a non-null array then the array will be used and the span is ignored. + // if the span cannot be written into the current packet then the remaining contents of the span are copied to a + // new heap allocated array that will used to callback into the method to continue the write operation. + private Task WriteBytes(ReadOnlySpan b, int len, int offsetBuffer, bool canAccumulate = true, TaskCompletionSource completion = null, byte[] array = null) + { + if (array != null) + { + b = new ReadOnlySpan(array, offsetBuffer, len); + } try { bool async = _parser._asyncWrite; // NOTE: We are capturing this now for the assert after the Task is returned, since WritePacket will turn off async if there is an exception @@ -3017,26 +3019,31 @@ internal Task WriteByteArray(byte[] b, int len, int offsetBuffer, bool canAccumu int offset = offsetBuffer; - Debug.Assert(b.Length >= len, "Invalid length sent to WriteByteArray()!"); + Debug.Assert(b.Length >= len, "Invalid length sent to WriteBytes()!"); // loop through and write the entire array do { if ((_outBytesUsed + len) > _outBuff.Length) { - // If the remainder of the string won't fit into the buffer, then we have to put + // If the remainder of the data won't fit into the buffer, then we have to put // whatever we can into the buffer, and flush that so we can then put more into // the buffer on the next loop of the while. int remainder = _outBuff.Length - _outBytesUsed; // write the remainder - Buffer.BlockCopy(b, offset, _outBuff, _outBytesUsed, remainder); + Span copyTo = _outBuff.AsSpan(_outBytesUsed, remainder); + ReadOnlySpan copyFrom = b.Slice(0, remainder); + + Debug.Assert(copyTo.Length == copyFrom.Length, $"copyTo.Length:{copyTo.Length} and copyFrom.Length{copyFrom.Length:D} should be the same"); + + copyFrom.CopyTo(copyTo); - // handle counters offset += remainder; _outBytesUsed += remainder; len -= remainder; + b = b.Slice(remainder); Task packetTask = WritePacket(TdsEnums.SOFTFLUSH, canAccumulate); @@ -3049,16 +3056,36 @@ internal Task WriteByteArray(byte[] b, int len, int offsetBuffer, bool canAccumu completion = new TaskCompletionSource(); task = completion.Task; // we only care about return from topmost call, so do not access Task property in other cases } - WriteByteArraySetupContinuation(b, len, completion, offset, packetTask); + + if (array == null) + { + byte[] tempArray = new byte[len]; + Span copyTempTo = tempArray.AsSpan(); + ReadOnlySpan copyTempFrom = b.Slice(remainder, len); + + Debug.Assert(copyTempTo.Length == copyTempFrom.Length, $"copyTempTo.Length:{copyTempTo.Length} and copyTempFrom.Length:{copyTempFrom.Length:D} should be the same"); + + copyTempFrom.CopyTo(copyTempTo); + array = tempArray; + offset = 0; + } + + WriteBytesSetupContinuation(array, len, completion, offset, packetTask); return task; } } else - { //((stateObj._outBytesUsed + len) <= stateObj._outBuff.Length ) + { + //((stateObj._outBytesUsed + len) <= stateObj._outBuff.Length ) // Else the remainder of the string will fit into the buffer, so copy it into the // buffer and then break out of the loop. - Buffer.BlockCopy(b, offset, _outBuff, _outBytesUsed, len); + Span copyTo = _outBuff.AsSpan(_outBytesUsed, len); + ReadOnlySpan copyFrom = b.Slice(0, len); + + Debug.Assert(copyTo.Length == copyFrom.Length, $"copyTo.Length:{copyTo.Length} and copyFrom.Length:{copyFrom.Length:D} should be the same"); + + copyFrom.CopyTo(copyTo); // handle out buffer bytes used counter _outBytesUsed += len; @@ -3086,12 +3113,13 @@ internal Task WriteByteArray(byte[] b, int len, int offsetBuffer, bool canAccumu } } - // This is in its own method to avoid always allocating the lambda in WriteByteArray - private void WriteByteArraySetupContinuation(byte[] b, int len, TaskCompletionSource completion, int offset, Task packetTask) + // This is in its own method to avoid always allocating the lambda in WriteBytes + private void WriteBytesSetupContinuation(byte[] array, int len, TaskCompletionSource completion, int offset, Task packetTask) { AsyncHelper.ContinueTask(packetTask, completion, - () => WriteByteArray(b, len: len, offsetBuffer: offset, canAccumulate: false, completion: completion), - connectionToDoom: _parser.Connection); + () => WriteBytes(ReadOnlySpan.Empty, len: len, offsetBuffer: offset, canAccumulate: false, completion: completion, array), + connectionToDoom: _parser.Connection + ); } // Dumps contents of buffer to SNI for network write. diff --git a/src/System.Data.SqlClient/src/System/Data/SqlClient/TdsParserStateObjectNative.cs b/src/System.Data.SqlClient/src/System/Data/SqlClient/TdsParserStateObjectNative.cs index c1fa34bd9c70..13b42fe6f40f 100644 --- a/src/System.Data.SqlClient/src/System/Data/SqlClient/TdsParserStateObjectNative.cs +++ b/src/System.Data.SqlClient/src/System/Data/SqlClient/TdsParserStateObjectNative.cs @@ -18,6 +18,8 @@ internal class TdsParserStateObjectNative : TdsParserStateObject internal SNIPacket _sniAsyncAttnPacket = null; // Packet to use to send Attn private readonly WritePacketCache _writePacketCache = new WritePacketCache(); // Store write packets that are ready to be re-used + private static readonly object s_cachedEmptyReadPacketObjectPointer = (object)IntPtr.Zero; + public TdsParserStateObjectNative(TdsParser parser) : base(parser) { } private GCHandle _gcHandle; // keeps this object alive until we're closed. @@ -35,7 +37,7 @@ internal TdsParserStateObjectNative(TdsParser parser, TdsParserStateObject physi internal override object SessionHandle => _sessionHandle; - protected override object EmptyReadPacket => IntPtr.Zero; + protected override object EmptyReadPacket => s_cachedEmptyReadPacketObjectPointer; protected override void CreateSessionHandle(TdsParserStateObject physicalConnection, bool async) {