diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj index 5c67ef9d5b..f4e49c1ccb 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj @@ -585,6 +585,9 @@ Microsoft\Data\SqlClient\SqlCommand.NonQuery.cs + + Microsoft\Data\SqlClient\SqlCommand.Reader.cs + Microsoft\Data\SqlClient\SqlCommand.Scalar.cs diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs index ab0d672226..1de6ca10b5 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs @@ -31,33 +31,6 @@ public sealed partial class SqlCommand : DbCommand, ICloneable { private const int MaxRPCNameLength = 1046; - internal sealed class ExecuteReaderAsyncCallContext : AAsyncCallContext - { - public Guid OperationID; - public CommandBehavior CommandBehavior; - - public SqlCommand Command => _owner; - public TaskCompletionSource TaskCompletionSource => _source; - - public void Set(SqlCommand command, TaskCompletionSource source, CancellationTokenRegistration disposable, CommandBehavior behavior, Guid operationID) - { - base.Set(command, source, disposable); - CommandBehavior = behavior; - OperationID = operationID; - } - - protected override void Clear() - { - OperationID = default; - CommandBehavior = default; - } - - protected override void AfterCleared(SqlCommand owner) - { - owner?.SetCachedCommandExecuteReaderAsyncContext(this); - } - } - /// /// Indicates if the column encryption setting was set at-least once in the batch rpc mode, when using AddBatchCommand. /// @@ -481,17 +454,6 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } - private SqlDataReader RunExecuteReaderWithRetry( - CommandBehavior cmdBehavior, - RunBehavior runBehavior, - bool returnStream, - [CallerMemberName] string method = "") - { - return RetryLogicProvider.Execute( - this, - () => RunExecuteReader(cmdBehavior, runBehavior, returnStream, method)); - } - private void VerifyEndExecuteState(Task completionTask, string endMethod, bool fullCheckForColumnEncryption = false) { Debug.Assert(completionTask != null); @@ -582,322 +544,6 @@ private void ThrowIfReconnectionHasBeenCanceled() } } - /// - public IAsyncResult BeginExecuteReader() => - BeginExecuteReader(callback: null, stateObject: null, CommandBehavior.Default); - - /// - public IAsyncResult BeginExecuteReader(AsyncCallback callback, object stateObject) => - BeginExecuteReader(callback, stateObject, CommandBehavior.Default); - - /// - public IAsyncResult BeginExecuteReader(CommandBehavior behavior) => - BeginExecuteReader(callback: null, stateObject: null, behavior); - - /// - public IAsyncResult BeginExecuteReader(AsyncCallback callback, object stateObject, CommandBehavior behavior) - { - SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlCommand.BeginExecuteReader | API | Correlation | Object Id {0}, Behavior {1}, Activity Id {2}, Client Connection Id {3}, Command Text '{4}'", ObjectID, (int)behavior, ActivityCorrelator.Current, Connection?.ClientConnectionId, CommandText); - return BeginExecuteReaderInternal(behavior, callback, stateObject, 0, isRetry: false); - } - - /// - protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) - { - SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlCommand.ExecuteDbDataReader | API | Correlation | Object Id {0}, Activity Id {1}, Client Connection Id {2}, Command Text '{3}'", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId, CommandText); - return ExecuteReader(behavior); - } - - /// - new public SqlDataReader ExecuteReader() - { - SqlStatistics statistics = null; - SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlCommand.ExecuteReader | API | Correlation | ObjectID {0}, Activity Id {1}, Client Connection Id {2}, Command Text '{3}'", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId, CommandText); - try - { - statistics = SqlStatistics.StartTimer(Statistics); - return ExecuteReader(CommandBehavior.Default); - } - finally - { - SqlStatistics.StopTimer(statistics); - } - } - - /// - new public SqlDataReader ExecuteReader(CommandBehavior behavior) - { - // Reset _pendingCancel upon entry into any Execute - used to synchronize state - // between entry into Execute* API and the thread obtaining the stateObject. - _pendingCancel = false; - - SqlStatistics statistics = null; - bool success = false; - int? sqlExceptionNumber = null; - Exception e = null; - Guid operationId = s_diagnosticListener.WriteCommandBefore(this, _transaction); - - using (TryEventScope.Create("SqlCommand.ExecuteReader | API | Object Id {0}", ObjectID)) - { - try - { - WriteBeginExecuteEvent(); - statistics = SqlStatistics.StartTimer(Statistics); - return IsProviderRetriable ? - RunExecuteReaderWithRetry(behavior, RunBehavior.ReturnImmediately, returnStream: true) : - RunExecuteReader(behavior, RunBehavior.ReturnImmediately, returnStream: true); - } - catch (Exception ex) - { - if (ex is SqlException sqlException) - { - sqlExceptionNumber = sqlException.Number; - } - - e = ex; - throw; - } - finally - { - SqlStatistics.StopTimer(statistics); - WriteEndExecuteEvent(success, sqlExceptionNumber, synchronous: true); - if (e != null) - { - s_diagnosticListener.WriteCommandError(operationId, this, _transaction, e); - } - else - { - s_diagnosticListener.WriteCommandAfter(operationId, this, _transaction); - } - } - } - } - - /// - public SqlDataReader EndExecuteReader(IAsyncResult asyncResult) - { - try - { - return EndExecuteReaderInternal(asyncResult); - } - finally - { - SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlCommand.EndExecuteReader | API | Correlation | Object Id {0}, Activity Id {1}, Client Connection Id {2}, Command Text '{3}'", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId, CommandText); - } - } - - private SqlDataReader EndExecuteReaderAsync(IAsyncResult asyncResult) - { - SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlCommand.EndExecuteReaderAsync | API | Correlation | Object Id {0}, Activity Id {1}, Client Connection Id {2}, Command Text '{3}'", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId, CommandText); - Debug.Assert(!_internalEndExecuteInitiated || _stateObj == null); - - Exception asyncException = ((Task)asyncResult).Exception; - if (asyncException != null) - { - CachedAsyncState?.ResetAsyncState(); - ReliablePutStateObject(); - throw asyncException.InnerException; - } - else - { - ThrowIfReconnectionHasBeenCanceled(); - // lock on _stateObj prevents races with close/cancel. - if (!_internalEndExecuteInitiated) - { - lock (_stateObj) - { - return EndExecuteReaderInternal(asyncResult); - } - } - else - { - return EndExecuteReaderInternal(asyncResult); - } - } - } - - private SqlDataReader EndExecuteReaderInternal(IAsyncResult asyncResult) - { - SqlClientEventSource.Log.TryTraceEvent("SqlCommand.EndExecuteReaderInternal | API | ObjectId {0}, Client Connection Id {1}, MARS={2}, AsyncCommandInProgress={3}", - _activeConnection?.ObjectID, _activeConnection?.ClientConnectionId, - _activeConnection?.Parser?.MARSOn, _activeConnection?.AsyncCommandInProgress); - SqlStatistics statistics = null; - bool success = false; - int? sqlExceptionNumber = null; - try - { - success = true; - statistics = SqlStatistics.StartTimer(Statistics); - return InternalEndExecuteReader(asyncResult, false, nameof(EndExecuteReader)); - } - catch (Exception e) - { - if (e is SqlException) - { - SqlException exception = (SqlException)e; - sqlExceptionNumber = exception.Number; - } - - if (CachedAsyncState != null) - { - CachedAsyncState.ResetAsyncState(); - }; - if (ADP.IsCatchableExceptionType(e)) - { - ReliablePutStateObject(); - }; - throw; - } - finally - { - SqlStatistics.StopTimer(statistics); - WriteEndExecuteEvent(success, sqlExceptionNumber, synchronous: false); - } - } - - private void CleanupExecuteReaderAsync(Task task, TaskCompletionSource source, Guid operationId) - { - if (task.IsFaulted) - { - Exception e = task.Exception.InnerException; - if (!_parentOperationStarted) - { - s_diagnosticListener.WriteCommandError(operationId, this, _transaction, e); - } - source.SetException(e); - } - else - { - if (!_parentOperationStarted) - { - s_diagnosticListener.WriteCommandAfter(operationId, this, _transaction); - } - if (task.IsCanceled) - { - source.SetCanceled(); - } - else - { - source.SetResult(task.Result); - } - } - } - - private IAsyncResult BeginExecuteReaderInternal(CommandBehavior behavior, AsyncCallback callback, object stateObject, int timeout, bool isRetry, bool asyncWrite = false) - { - TaskCompletionSource globalCompletion = new TaskCompletionSource(stateObject); - TaskCompletionSource localCompletion = new TaskCompletionSource(stateObject); - - if (!isRetry) - { - // Reset _pendingCancel upon entry into any Execute - used to synchronize state - // between entry into Execute* API and the thread obtaining the stateObject. - _pendingCancel = false; - } - - SqlStatistics statistics = null; - try - { - if (!isRetry) - { - statistics = SqlStatistics.StartTimer(Statistics); - WriteBeginExecuteEvent(); - - ValidateAsyncCommand(); // Special case - done outside of try/catches to prevent putting a stateObj - // back into pool when we should not. - } - - bool usedCache = false; - Task writeTask = null; - try - { - // InternalExecuteNonQuery already has reliability block, but if failure will not put stateObj back into pool. - RunExecuteReader( - behavior, - RunBehavior.ReturnImmediately, - returnStream: true, - localCompletion, - timeout, - out writeTask, - out usedCache, - asyncWrite, - isRetry, - nameof(BeginExecuteReader)); - } - catch (Exception e) - { - if (!ADP.IsCatchableOrSecurityExceptionType(e)) - { - // If not catchable - the connection has already been caught and doomed in RunExecuteReader. - throw; - } - - // For async, RunExecuteReader will never put the stateObj back into the pool, so do so now. - ReliablePutStateObject(); - if (isRetry || e is not EnclaveDelegate.RetryableEnclaveQueryExecutionException) - { - throw; - } - } - - if (writeTask != null) - { - AsyncHelper.ContinueTaskWithState(writeTask, localCompletion, - state: Tuple.Create(this, localCompletion), - onSuccess: state => - { - var parameters = (Tuple>)state; - parameters.Item1.BeginExecuteReaderInternalReadStage(parameters.Item2); - } - ); - } - else - { - BeginExecuteReaderInternalReadStage(localCompletion); - } - - // When we use query caching for parameter encryption we need to retry on specific errors. - // In these cases finalize the call internally and trigger a retry when needed. - if ( - !TriggerInternalEndAndRetryIfNecessary( - behavior, - stateObject, - timeout, - usedCache, - isRetry, - asyncWrite, - globalCompletion, - localCompletion, - endFunc: static (SqlCommand command, IAsyncResult asyncResult, bool isInternal, string endMethod) => - { - return command.InternalEndExecuteReader(asyncResult, isInternal, endMethod); - }, - retryFunc: static (SqlCommand command, CommandBehavior behavior, AsyncCallback callback, object stateObject, int timeout, bool isRetry, bool asyncWrite) => - { - return command.BeginExecuteReaderInternal(behavior, callback, stateObject, timeout, isRetry, asyncWrite); - }, - nameof(EndExecuteReader))) - { - globalCompletion = localCompletion; - } - - // Add callback after work is done to avoid overlapping Begin/End methods - if (callback != null) - { - globalCompletion.Task.ContinueWith( - static (Task task, object state) => ((AsyncCallback)state)(task), - state: callback - ); - } - - return globalCompletion.Task; - } - finally - { - SqlStatistics.StopTimer(statistics); - } - } - private bool TriggerInternalEndAndRetryIfNecessary( CommandBehavior behavior, object stateObject, @@ -1103,182 +749,6 @@ private EnclaveSessionParameters GetEnclaveSessionParameters() this._activeConnection.Database); } - private void BeginExecuteReaderInternalReadStage(TaskCompletionSource completion) - { - Debug.Assert(completion != null, "CompletionSource should not be null"); - SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlCommand.BeginExecuteReaderInternalReadStage | INFO | Correlation | Object Id {0}, Activity Id {1}, Client Connection Id {2}, Command Text '{3}'", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId, CommandText); - // Read SNI does not have catches for async exceptions, handle here. - try - { - // must finish caching information before ReadSni which can activate the callback before returning - CachedAsyncState.SetActiveConnectionAndResult(completion, nameof(EndExecuteReader), _activeConnection); - _stateObj.ReadSni(completion); - } - // @TODO: CER Exception Handling was removed here (see GH#3581) - catch (Exception e) - { - // Similarly, if an exception occurs put the stateObj back into the pool. - // and reset async cache information to allow a second async execute - CachedAsyncState?.ResetAsyncState(); - ReliablePutStateObject(); - completion.TrySetException(e); - } - } - - private SqlDataReader InternalEndExecuteReader(IAsyncResult asyncResult, bool isInternal, string endMethod) - { - SqlClientEventSource.Log.TryTraceEvent("SqlCommand.InternalEndExecuteReader | INFO | ObjectId {0}, Client Connection Id {1}, MARS={2}, AsyncCommandInProgress={3}", - _activeConnection?.ObjectID, _activeConnection?.ClientConnectionId, - _activeConnection?.Parser?.MARSOn, _activeConnection?.AsyncCommandInProgress); - VerifyEndExecuteState((Task)asyncResult, endMethod); - WaitForAsyncResults(asyncResult, isInternal); - - // If column encryption is enabled, also check the state after waiting for the task. - // It would be better to do this for all cases, but avoiding for compatibility reasons. - if (IsColumnEncryptionEnabled) - { - VerifyEndExecuteState((Task)asyncResult, endMethod, fullCheckForColumnEncryption: true); - } - - CheckThrowSNIException(); - - SqlDataReader reader = CompleteAsyncExecuteReader(isInternal); - Debug.Assert(_stateObj == null, "non-null state object in InternalEndExecuteReader"); - return reader; - } - - /// - protected override Task ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken) - { - return ExecuteReaderAsync(behavior, cancellationToken).ContinueWith( - static (Task result) => - { - if (result.IsFaulted) - { - throw result.Exception.InnerException; - } - return result.Result; - }, - CancellationToken.None, - TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.NotOnCanceled, - TaskScheduler.Default - ); - } - - /// - public new Task ExecuteReaderAsync() => - ExecuteReaderAsync(CommandBehavior.Default, CancellationToken.None); - - /// - public new Task ExecuteReaderAsync(CommandBehavior behavior) => - ExecuteReaderAsync(behavior, CancellationToken.None); - - /// - public new Task ExecuteReaderAsync(CancellationToken cancellationToken) => - ExecuteReaderAsync(CommandBehavior.Default, cancellationToken); - - /// - public new Task ExecuteReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken) => - IsProviderRetriable - ? InternalExecuteReaderWithRetryAsync(behavior, cancellationToken) - : InternalExecuteReaderAsync(behavior, cancellationToken); - - private Task InternalExecuteReaderWithRetryAsync(CommandBehavior behavior, CancellationToken cancellationToken) => - RetryLogicProvider.ExecuteAsync( - sender: this, - () => InternalExecuteReaderAsync(behavior, cancellationToken), - cancellationToken); - - private Task InternalExecuteReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken) - { - SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlCommand.InternalExecuteReaderAsync | API | Correlation | Object Id {0}, Behavior {1}, Activity Id {2}, Client Connection Id {3}, Command Text '{4}'", ObjectID, (int)behavior, ActivityCorrelator.Current, Connection?.ClientConnectionId, CommandText); - SqlClientEventSource.Log.TryTraceEvent("SqlCommand.InternalExecuteReaderAsync | API> {0}, Client Connection Id {1}, Command Text = '{2}'", ObjectID, Connection?.ClientConnectionId, CommandText); - Guid operationId = default(Guid); - if (!_parentOperationStarted) - { - operationId = s_diagnosticListener.WriteCommandBefore(this, _transaction); - } - - // connection can be used as state in RegisterForConnectionCloseNotification continuation - // to avoid an allocation so use it as the state value if possible but it can be changed if - // you need it for a more important piece of data that justifies the tuple allocation later - TaskCompletionSource source = new TaskCompletionSource(_activeConnection); - - CancellationTokenRegistration registration = new CancellationTokenRegistration(); - if (cancellationToken.CanBeCanceled) - { - if (cancellationToken.IsCancellationRequested) - { - source.SetCanceled(); - return source.Task; - } - registration = cancellationToken.Register(s_cancelIgnoreFailure, this); - } - - Task returnedTask = source.Task; - ExecuteReaderAsyncCallContext context = null; - try - { - returnedTask = RegisterForConnectionCloseNotification(returnedTask); - - if (_activeConnection?.InnerConnection is SqlInternalConnection sqlInternalConnection) - { - context = Interlocked.Exchange(ref sqlInternalConnection.CachedCommandExecuteReaderAsyncContext, null); - } - if (context is null) - { - context = new ExecuteReaderAsyncCallContext(); - } - context.Set(this, source, registration, behavior, operationId); - - Task.Factory.FromAsync( - beginMethod: static (AsyncCallback callback, object stateObject) => - { - ExecuteReaderAsyncCallContext args = (ExecuteReaderAsyncCallContext)stateObject; - return args.Command.BeginExecuteReaderInternal(args.CommandBehavior, callback, stateObject, args.Command.CommandTimeout, isRetry: false, asyncWrite: true); - }, - endMethod: static (IAsyncResult asyncResult) => - { - ExecuteReaderAsyncCallContext args = (ExecuteReaderAsyncCallContext)asyncResult.AsyncState; - return args.Command.EndExecuteReaderAsync(asyncResult); - }, - state: context - ).ContinueWith( - continuationAction: static (Task task) => - { - ExecuteReaderAsyncCallContext context = (ExecuteReaderAsyncCallContext)task.AsyncState; - SqlCommand command = context.Command; - Guid operationId = context.OperationID; - TaskCompletionSource source = context.TaskCompletionSource; - context.Dispose(); - - command.CleanupExecuteReaderAsync(task, source, operationId); - }, - scheduler: TaskScheduler.Default - ); - } - catch (Exception e) - { - if (!_parentOperationStarted) - { - s_diagnosticListener.WriteCommandError(operationId, this, _transaction, e); - } - - source.SetException(e); - context?.Dispose(); - } - - return returnedTask; - } - - private void SetCachedCommandExecuteReaderAsyncContext(ExecuteReaderAsyncCallContext instance) - { - if (_activeConnection?.InnerConnection is SqlInternalConnection sqlInternalConnection) - { - Interlocked.CompareExchange(ref sqlInternalConnection.CachedCommandExecuteReaderAsyncContext, instance, null); - } - } - /// public void RegisterColumnEncryptionKeyStoreProvidersOnCommand(IDictionary customProviders) { @@ -2091,7 +1561,7 @@ private SqlDataReader GetParameterEncryptionDataReader(out Task returnTask, Task } // Complete executereader. - describeParameterEncryptionDataReader = command.CompleteAsyncExecuteReader(forDescribeParameterEncryption: true); + describeParameterEncryptionDataReader = command.CompleteAsyncExecuteReader(isInternal: false, forDescribeParameterEncryption: true); Debug.Assert(command._stateObj == null, "non-null state object in PrepareForTransparentEncryption."); // Read the results of describe parameter encryption. @@ -2162,7 +1632,7 @@ private SqlDataReader GetParameterEncryptionDataReaderAsync(out Task returnTask, } // Complete executereader. - describeParameterEncryptionDataReader = CompleteAsyncExecuteReader(forDescribeParameterEncryption: true); + describeParameterEncryptionDataReader = CompleteAsyncExecuteReader(isInternal: false, forDescribeParameterEncryption: true); Debug.Assert(_stateObj == null, "non-null state object in PrepareForTransparentEncryption."); // Read the results of describe parameter encryption. @@ -2811,748 +2281,12 @@ private void ReadDescribeEncryptionParameterResults( } } - internal SqlDataReader RunExecuteReader( - CommandBehavior cmdBehavior, - RunBehavior runBehavior, - bool returnStream, - [CallerMemberName] string method = "") + /// + public SqlCommand Clone() { - Task unused; // sync execution - SqlDataReader reader = RunExecuteReader( - cmdBehavior, - runBehavior, - returnStream, - completion: null, - timeout: CommandTimeout, - task: out unused, - usedCache: out _, - method: method); - - Debug.Assert(unused == null, "returned task during synchronous execution"); - return reader; - } - - // task is created in case of pending asynchronous write, returned SqlDataReader should not be utilized until that task is complete - internal SqlDataReader RunExecuteReader( - CommandBehavior cmdBehavior, - RunBehavior runBehavior, - bool returnStream, - TaskCompletionSource completion, - int timeout, - out Task task, - out bool usedCache, - bool asyncWrite = false, - bool isRetry = false, - [CallerMemberName] string method = "") - { - bool isAsync = completion != null; - usedCache = false; - - task = null; - - _rowsAffected = -1; - _rowsAffectedBySpDescribeParameterEncryption = -1; - - if (0 != (CommandBehavior.SingleRow & cmdBehavior)) - { - // CommandBehavior.SingleRow implies CommandBehavior.SingleResult - cmdBehavior |= CommandBehavior.SingleResult; - } - - // this function may throw for an invalid connection - // returns false for empty command text - if (!isRetry) - { - ValidateCommand(isAsync, method); - } - - CheckNotificationStateAndAutoEnlist(); // Only call after validate - requires non null connection! - - SqlStatistics statistics = Statistics; - if (statistics != null) - { - if ((!this.IsDirty && this.IsPrepared && !_hiddenPrepare) - || (this.IsPrepared && _execType == EXECTYPE.PREPAREPENDING)) - { - statistics.SafeIncrement(ref statistics._preparedExecs); - } - else - { - statistics.SafeIncrement(ref statistics._unpreparedExecs); - } - } - - // Reset the encryption related state of the command and its parameters. - ResetEncryptionState(); - - if (IsColumnEncryptionEnabled) - { - Task returnTask = null; - PrepareForTransparentEncryption(isAsync, timeout, completion, out returnTask, asyncWrite && isAsync, out usedCache, isRetry); - Debug.Assert(usedCache || (isAsync == (returnTask != null)), @"if we didn't use the cache, returnTask should be null if and only if async is false."); - - long firstAttemptStart = ADP.TimerCurrent(); - - try - { - return RunExecuteReaderTdsWithTransparentParameterEncryption( - cmdBehavior, - runBehavior, - returnStream, - isAsync, - timeout, - out task, - asyncWrite && isAsync, - isRetry: isRetry, - ds: null, - describeParameterEncryptionTask: returnTask); - } - - catch (EnclaveDelegate.RetryableEnclaveQueryExecutionException) - { - if (isRetry) - { - throw; - } - - // Retry if the command failed with appropriate error. - // First invalidate the entry from the cache, so that we refresh our encryption MD. - SqlQueryMetadataCache.GetInstance().InvalidateCacheEntry(this); - - InvalidateEnclaveSession(); - - return RunExecuteReader( - cmdBehavior, - runBehavior, - returnStream, - completion, - TdsParserStaticMethods.GetRemainingTimeout(timeout, firstAttemptStart), - out task, - out usedCache, - isAsync, - isRetry: true, - method: method); - } - - catch (SqlException ex) - { - // We only want to retry once, so don't retry if we are already in retry. - // If we didn't use the cache, we don't want to retry. - if (isRetry || (!usedCache && !ShouldUseEnclaveBasedWorkflow)) - { - throw; - } - - bool shouldRetry = false; - - // Check if we have an error indicating that we can retry. - for (int i = 0; i < ex.Errors.Count; i++) - { - - if ((usedCache && (ex.Errors[i].Number == TdsEnums.TCE_CONVERSION_ERROR_CLIENT_RETRY)) || - (ShouldUseEnclaveBasedWorkflow && (ex.Errors[i].Number == TdsEnums.TCE_ENCLAVE_INVALID_SESSION_HANDLE))) - { - shouldRetry = true; - break; - } - } - - if (!shouldRetry) - { - throw; - } - else - { - // Retry if the command failed with appropriate error. - // First invalidate the entry from the cache, so that we refresh our encryption MD. - SqlQueryMetadataCache.GetInstance().InvalidateCacheEntry(this); - - InvalidateEnclaveSession(); - - return RunExecuteReader( - cmdBehavior, - runBehavior, - returnStream, - completion, - TdsParserStaticMethods.GetRemainingTimeout(timeout, firstAttemptStart), - out task, - out usedCache, - isAsync, - isRetry: true, - method: method); - } - } - } - else - { - return RunExecuteReaderTds(cmdBehavior, runBehavior, returnStream, isAsync, timeout, out task, asyncWrite && isAsync, isRetry: isRetry); - } - } - - - private SqlDataReader RunExecuteReaderTdsWithTransparentParameterEncryption( - CommandBehavior cmdBehavior, - RunBehavior runBehavior, - bool returnStream, - bool isAsync, - int timeout, - out Task task, - bool asyncWrite, - bool isRetry, - SqlDataReader ds = null, - Task describeParameterEncryptionTask = null) - { - Debug.Assert(!asyncWrite || isAsync, "AsyncWrite should be always accompanied by Async"); - - if (ds == null && returnStream) - { - ds = new SqlDataReader(this, cmdBehavior); - } - - if (describeParameterEncryptionTask != null) - { - long parameterEncryptionStart = ADP.TimerCurrent(); - TaskCompletionSource completion = new TaskCompletionSource(); - AsyncHelper.ContinueTaskWithState(describeParameterEncryptionTask, completion, this, - (object state) => - { - SqlCommand command = (SqlCommand)state; - Task subTask = null; - command.GenerateEnclavePackage(); - command.RunExecuteReaderTds(cmdBehavior, runBehavior, returnStream, isAsync, TdsParserStaticMethods.GetRemainingTimeout(timeout, parameterEncryptionStart), out subTask, asyncWrite, isRetry, ds); - if (subTask == null) - { - completion.SetResult(null); - } - else - { - AsyncHelper.ContinueTaskWithState(subTask, completion, completion, static (object state) => ((TaskCompletionSource)state).SetResult(null)); - } - }, - onFailure: static (Exception exception, object state) => - { - ((SqlCommand)state).CachedAsyncState?.ResetAsyncState(); - if (exception != null) - { - throw exception; - } - }, - onCancellation: static (object state) => - { - ((SqlCommand)state).CachedAsyncState?.ResetAsyncState(); - } - ); - task = completion.Task; - return ds; - } - else - { - // Synchronous execution. - GenerateEnclavePackage(); - return RunExecuteReaderTds(cmdBehavior, runBehavior, returnStream, isAsync, timeout, out task, asyncWrite, isRetry, ds); - } - } - - private void GenerateEnclavePackage() - { - if (keysToBeSentToEnclave == null || keysToBeSentToEnclave.Count <= 0) - { - return; - } - - if (string.IsNullOrWhiteSpace(this._activeConnection.EnclaveAttestationUrl) && - Connection.AttestationProtocol != SqlConnectionAttestationProtocol.None) - { - throw SQL.NoAttestationUrlSpecifiedForEnclaveBasedQueryGeneratingEnclavePackage(this._activeConnection.Parser.EnclaveType); - } - - string enclaveType = this._activeConnection.Parser.EnclaveType; - if (string.IsNullOrWhiteSpace(enclaveType)) - { - throw SQL.EnclaveTypeNullForEnclaveBasedQuery(); - } - - SqlConnectionAttestationProtocol attestationProtocol = this._activeConnection.AttestationProtocol; - if (attestationProtocol == SqlConnectionAttestationProtocol.NotSpecified) - { - throw SQL.AttestationProtocolNotSpecifiedForGeneratingEnclavePackage(); - } - - try - { -#if DEBUG - if (_forceRetryableEnclaveQueryExecutionExceptionDuringGenerateEnclavePackage) - { - _forceRetryableEnclaveQueryExecutionExceptionDuringGenerateEnclavePackage = false; - throw new EnclaveDelegate.RetryableEnclaveQueryExecutionException("testing", null); - } -#endif - this.enclavePackage = EnclaveDelegate.Instance.GenerateEnclavePackage(attestationProtocol, keysToBeSentToEnclave, - this.CommandText, enclaveType, GetEnclaveSessionParameters(), _activeConnection, this); - } - catch (EnclaveDelegate.RetryableEnclaveQueryExecutionException) - { - throw; - } - catch (Exception e) - { - throw SQL.ExceptionWhenGeneratingEnclavePackage(e); - } - } - - private SqlDataReader RunExecuteReaderTds( - CommandBehavior cmdBehavior, - RunBehavior runBehavior, - bool returnStream, - bool isAsync, - int timeout, - out Task task, - bool asyncWrite, - bool isRetry, - SqlDataReader ds = null, - bool describeParameterEncryptionRequest = false) - { - Debug.Assert(!asyncWrite || isAsync, "AsyncWrite should be always accompanied by Async"); - - if (ds == null && returnStream) - { - ds = new SqlDataReader(this, cmdBehavior); - } - - Task reconnectTask = _activeConnection.ValidateAndReconnect(null, timeout); - - if (reconnectTask != null) - { - long reconnectionStart = ADP.TimerCurrent(); - if (isAsync) - { - TaskCompletionSource completion = new TaskCompletionSource(); - _activeConnection.RegisterWaitingForReconnect(completion.Task); - _reconnectionCompletionSource = completion; - RunExecuteReaderTdsSetupReconnectContinuation(cmdBehavior, runBehavior, returnStream, isAsync, timeout, asyncWrite, isRetry, ds, reconnectTask, reconnectionStart, completion); - task = completion.Task; - return ds; - } - else - { - AsyncHelper.WaitForCompletion(reconnectTask, timeout, static () => throw SQL.CR_ReconnectTimeout()); - timeout = TdsParserStaticMethods.GetRemainingTimeout(timeout, reconnectionStart); - } - } - - // make sure we have good parameter information - // prepare the command - // execute - Debug.Assert(_activeConnection.Parser != null, "TdsParser class should not be null in Command.Execute!"); - - bool inSchema = (0 != (cmdBehavior & CommandBehavior.SchemaOnly)); - - // create a new RPC - _SqlRPC rpc = null; - - task = null; - - string optionSettings = null; - bool processFinallyBlock = true; - bool decrementAsyncCountOnFailure = false; - - if (isAsync) - { - _activeConnection.GetOpenTdsConnection().IncrementAsyncCount(); - decrementAsyncCountOnFailure = true; - } - - try - { - if (asyncWrite) - { - _activeConnection.AddWeakReference(this, SqlReferenceCollection.CommandTag); - } - - GetStateObject(); - Task writeTask = null; - - if (describeParameterEncryptionRequest) - { -#if DEBUG - if (_sleepDuringRunExecuteReaderTdsForSpDescribeParameterEncryption) - { - Thread.Sleep(10000); - } -#endif - - Debug.Assert(_sqlRPCParameterEncryptionReqArray != null, "RunExecuteReader rpc array not provided for describe parameter encryption request."); - writeTask = _stateObj.Parser.TdsExecuteRPC(this, _sqlRPCParameterEncryptionReqArray, timeout, inSchema, this.Notification, _stateObj, CommandType.StoredProcedure == CommandType, sync: !asyncWrite); - } - else if (_batchRPCMode) - { - Debug.Assert(inSchema == false, "Batch RPC does not support schema only command behavior"); - Debug.Assert(!IsPrepared, "Batch RPC should not be prepared!"); - Debug.Assert(!IsDirty, "Batch RPC should not be marked as dirty!"); - Debug.Assert(_RPCList != null, "RunExecuteReader rpc array not provided"); - writeTask = _stateObj.Parser.TdsExecuteRPC(this, _RPCList, timeout, inSchema, this.Notification, _stateObj, CommandType.StoredProcedure == CommandType, sync: !asyncWrite); - } - 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!"); - - if (returnStream) - { - SqlClientEventSource.Log.TryTraceEvent("SqlCommand.RunExecuteReaderTds | Info | Object Id {0}, Activity Id {1}, Client Connection Id {2}, Command executed as SQLBATCH, Command Text '{3}' ", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId, CommandText); - } - string text = GetCommandText(cmdBehavior) + GetResetOptionsString(cmdBehavior); - - //If the query requires enclave computations, pass the enclavepackage in the SQLBatch TDS stream - if (requiresEnclaveComputations) - { - - if (this.enclavePackage == null) - { - throw SQL.NullEnclavePackageForEnclaveBasedQuery(this._activeConnection.Parser.EnclaveType, this._activeConnection.EnclaveAttestationUrl); - } - - writeTask = _stateObj.Parser.TdsExecuteSQLBatch(text, timeout, this.Notification, _stateObj, - sync: !asyncWrite, enclavePackage: this.enclavePackage.EnclavePackageBytes); - } - else - { - writeTask = _stateObj.Parser.TdsExecuteSQLBatch(text, timeout, this.Notification, _stateObj, sync: !asyncWrite); - } - } - else if (System.Data.CommandType.Text == this.CommandType) - { - if (this.IsDirty) - { - Debug.Assert(_cachedMetaData == null || !_dirty, "dirty query should not have cached metadata!"); // can have cached metadata if dirty because of parameters - // - // someone changed the command text or the parameter schema so we must unprepare the command - // - // remember that IsDirty includes test for IsPrepared! - if (_execType == EXECTYPE.PREPARED) - { - _hiddenPrepare = true; - } - Unprepare(); - IsDirty = false; - } - - if (_execType == EXECTYPE.PREPARED) - { - Debug.Assert(IsPrepared && _prepareHandle != s_cachedInvalidPrepareHandle, "invalid attempt to call sp_execute without a handle!"); - rpc = BuildExecute(inSchema); - } - else if (_execType == EXECTYPE.PREPAREPENDING) - { - rpc = BuildPrepExec(cmdBehavior); - // next time through, only do an exec - _execType = EXECTYPE.PREPARED; - _preparedConnectionCloseCount = _activeConnection.CloseCount; - _preparedConnectionReconnectCount = _activeConnection.ReconnectCount; - // mark ourselves as preparing the command - _inPrepare = true; - } - else - { - Debug.Assert(_execType == EXECTYPE.UNPREPARED, "Invalid execType!"); - BuildExecuteSql(cmdBehavior, null, _parameters, ref rpc); - } - - rpc.options = TdsEnums.RPC_NOMETADATA; - if (returnStream) - { - SqlClientEventSource.Log.TryTraceEvent("SqlCommand.RunExecuteReaderTds | Info | Object Id {0}, Activity Id {1}, Client Connection Id {2}, Command executed as RPC, RPC Name '{3}' ", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId, rpc?.rpcName); - } - - Debug.Assert(_rpcArrayOf1[0] == rpc); - writeTask = _stateObj.Parser.TdsExecuteRPC(this, _rpcArrayOf1, timeout, inSchema, this.Notification, _stateObj, CommandType.StoredProcedure == CommandType, sync: !asyncWrite); - } - else - { - Debug.Assert(this.CommandType == System.Data.CommandType.StoredProcedure, "unknown command type!"); - - BuildRPC(inSchema, _parameters, ref rpc); - - // if we need to augment the command because a user has changed the command behavior (e.g. FillSchema) - // then batch sql them over. This is inefficient (3 round trips) but the only way we can get metadata only from - // a stored proc - optionSettings = GetSetOptionsString(cmdBehavior); - if (returnStream) - { - SqlClientEventSource.Log.TryTraceEvent("SqlCommand.RunExecuteReaderTds | Info | Object Id {0}, Activity Id {1}, Client Connection Id {2}, Command executed as RPC, RPC Name '{3}' ", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId, rpc?.rpcName); - } - - // turn set options ON - if (optionSettings != null) - { - Task executeTask = _stateObj.Parser.TdsExecuteSQLBatch(optionSettings, timeout, this.Notification, _stateObj, sync: true); - Debug.Assert(executeTask == null, "Shouldn't get a task when doing sync writes"); - Debug.Assert(_stateObj._syncOverAsync, "Should not attempt pends in a synchronous call"); - TdsOperationStatus result = _stateObj.Parser.TryRun(RunBehavior.UntilDone, this, null, null, _stateObj, out bool dataReady); - if (result != TdsOperationStatus.Done) - { - throw SQL.SynchronousCallMayNotPend(); - } - // and turn OFF when the ds exhausts the stream on Close() - optionSettings = GetResetOptionsString(cmdBehavior); - } - - // execute sp - Debug.Assert(_rpcArrayOf1[0] == rpc); - writeTask = _stateObj.Parser.TdsExecuteRPC(this, _rpcArrayOf1, timeout, inSchema, this.Notification, _stateObj, CommandType.StoredProcedure == CommandType, sync: !asyncWrite); - } - - Debug.Assert(writeTask == null || isAsync, "Returned task in sync mode"); - - if (isAsync) - { - decrementAsyncCountOnFailure = false; - if (writeTask != null) - { - task = RunExecuteReaderTdsSetupContinuation(runBehavior, ds, optionSettings, writeTask); - } - else - { - CachedAsyncState.SetAsyncReaderState(ds, runBehavior, optionSettings); - } - } - else - { - // Always execute - even if no reader! - FinishExecuteReader(ds, runBehavior, optionSettings, isInternal: false, forDescribeParameterEncryption: false, shouldCacheForAlwaysEncrypted: !describeParameterEncryptionRequest); - } - } - catch (Exception e) - { - processFinallyBlock = ADP.IsCatchableExceptionType(e); - if (decrementAsyncCountOnFailure) - { - SqlInternalConnectionTds innerConnectionTds = (_activeConnection.InnerConnection as SqlInternalConnectionTds); - if (innerConnectionTds != null) - { - // it may be closed - innerConnectionTds.DecrementAsyncCount(); - } - } - throw; - } - finally - { - if (processFinallyBlock && !isAsync) - { - // When executing async, we need to keep the _stateObj alive... - PutStateObject(); - } - } - - Debug.Assert(isAsync || _stateObj == null, "non-null state object in RunExecuteReader"); - return ds; - } - - private Task RunExecuteReaderTdsSetupContinuation(RunBehavior runBehavior, SqlDataReader ds, string optionSettings, Task writeTask) - { - Task task = AsyncHelper.CreateContinuationTaskWithState( - task: writeTask, - state: _activeConnection, - onSuccess: (object state) => - { - SqlConnection sqlConnection = (SqlConnection)state; - sqlConnection.GetOpenTdsConnection(); // it will throw if connection is closed - CachedAsyncState.SetAsyncReaderState(ds, runBehavior, optionSettings); - }, - onFailure: static (Exception exc, object state) => - { - ((SqlConnection)state).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 isAsync, int timeout, bool asyncWrite, bool isRetry, SqlDataReader ds, Task reconnectTask, long reconnectionStart, TaskCompletionSource completion) - { - CancellationTokenSource timeoutCTS = new CancellationTokenSource(); - AsyncHelper.SetTimeoutException(completion, timeout, static () => 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, isAsync, TdsParserStaticMethods.GetRemainingTimeout(timeout, reconnectionStart), out subTask, asyncWrite, isRetry, ds); - if (subTask == null) - { - completion.SetResult(null); - } - else - { - AsyncHelper.ContinueTaskWithState(subTask, completion, - state: completion, - onSuccess: static (object state) => ((TaskCompletionSource)state).SetResult(null) - ); - } - } - ); - } - - - private SqlDataReader CompleteAsyncExecuteReader(bool isInternal = false, bool forDescribeParameterEncryption = false) - { - SqlDataReader ds = CachedAsyncState.CachedAsyncReader; // should not be null - bool processFinallyBlock = true; - try - { - FinishExecuteReader(ds, CachedAsyncState.CachedRunBehavior, CachedAsyncState.CachedSetOptions, isInternal, forDescribeParameterEncryption, shouldCacheForAlwaysEncrypted: !forDescribeParameterEncryption); - } - catch (Exception e) - { - processFinallyBlock = ADP.IsCatchableExceptionType(e); - throw; - } - finally - { - if (processFinallyBlock) - { - // Don't reset the state for internal End. The user End will do that eventually. - if (!isInternal) - { - CachedAsyncState.ResetAsyncState(); - } - PutStateObject(); - } - } - - return ds; - } - - private void FinishExecuteReader(SqlDataReader ds, RunBehavior runBehavior, string resetOptionsString, bool isInternal, bool forDescribeParameterEncryption, bool shouldCacheForAlwaysEncrypted = true) - { - // always wrap with a try { FinishExecuteReader(...) } finally { PutStateObject(); } - - // If this is not for internal usage, notify the dependency. If we have already initiated the end internally, the reader should be ready, so just return. - if (!isInternal && !forDescribeParameterEncryption) - { - NotifyDependency(); - - if (_internalEndExecuteInitiated) - { - Debug.Assert(_stateObj == null); - return; - } - } - - if (runBehavior == RunBehavior.UntilDone) - { - try - { - Debug.Assert(_stateObj._syncOverAsync, "Should not attempt pends in a synchronous call"); - TdsOperationStatus result = _stateObj.Parser.TryRun(RunBehavior.UntilDone, this, ds, null, _stateObj, out _); - if (result != TdsOperationStatus.Done) - { - throw SQL.SynchronousCallMayNotPend(); - } - } - catch (Exception e) - { - if (ADP.IsCatchableExceptionType(e)) - { - if (_inPrepare) - { - // The flag is expected to be reset by OnReturnValue. We should receive - // the handle unless command execution failed. If fail, move back to pending - // state. - _inPrepare = false; // reset the flag - IsDirty = true; // mark command as dirty so it will be prepared next time we're coming through - _execType = EXECTYPE.PREPAREPENDING; // reset execution type to pending - } - - if (ds != null) - { - try - { - ds.Close(); - } - catch (Exception exClose) - { - Debug.WriteLine("Received this exception from SqlDataReader.Close() while in another catch block: " + exClose.ToString()); - } - } - } - throw; - } - } - - // bind the parser to the reader if we get this far - if (ds != null) - { - ds.Bind(_stateObj); - _stateObj = null; // the reader now owns this... - ds.ResetOptionsString = resetOptionsString; - - // bind this reader to this connection now - _activeConnection.AddWeakReference(ds, SqlReferenceCollection.DataReaderTag); - - // force this command to start reading data off the wire. - // this will cause an error to be reported at Execute() time instead of Read() time - // if the command is not set. - try - { - //This flag indicates if the datareader's metadata should be cached in this SqlCommand. - //Metadata associated with sp_describe_parameter_metadats's datareader should not be cached. - //Ideally, we should be using "forDescribeParameterEncryption" flag for this, but this flag's - //semantics are overloaded with async workflow and this flag is always false for sync workflow. - //Since we are very close to a release and changing the semantics for "forDescribeParameterEncryption" - //is risky, we introduced a new parameter to determine whether we should cache a datareader's metadata or not. - if (shouldCacheForAlwaysEncrypted) - { - _cachedMetaData = ds.MetaData; - } - else - { - //we need this call to ensure that the datareader is properly intialized, the getter is initializing state in SqlDataReader - _SqlMetaDataSet temp = ds.MetaData; - } - ds.IsInitialized = true; - } - catch (Exception e) - { - if (ADP.IsCatchableExceptionType(e)) - { - if (_inPrepare) - { - // The flag is expected to be reset by OnReturnValue. We should receive - // the handle unless command execution failed. If fail, move back to pending - // state. - _inPrepare = false; // reset the flag - IsDirty = true; // mark command as dirty so it will be prepared next time we're coming through - _execType = EXECTYPE.PREPAREPENDING; // reset execution type to pending - } - - try - { - ds.Close(); - } - catch (Exception exClose) - { - Debug.WriteLine("Received this exception from SqlDataReader.Close() while in another catch block: " + exClose.ToString()); - } - } - - throw; - } - } - } - - /// - public SqlCommand Clone() - { - SqlCommand clone = new SqlCommand(this); - SqlClientEventSource.Log.TryTraceEvent("SqlCommand.Clone | API | Object Id {0}, Clone Object Id {1}, Client Connection Id {2}", ObjectID, clone.ObjectID, Connection?.ClientConnectionId); - return clone; + SqlCommand clone = new SqlCommand(this); + SqlClientEventSource.Log.TryTraceEvent("SqlCommand.Clone | API | Object Id {0}, Clone Object Id {1}, Client Connection Id {2}", ObjectID, clone.ObjectID, Connection?.ClientConnectionId); + return clone; } object ICloneable.Clone() => @@ -4189,48 +2923,6 @@ private void SetUpRPCParameters(_SqlRPC rpc, bool inSchema, SqlParameterCollecti 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; - - const int systemParameterCount = 3; - int userParameterCount = CountSendableParameters(_parameters); - - _SqlRPC rpc = null; - GetRPCObject(systemParameterCount, userParameterCount, ref rpc); - - rpc.ProcID = TdsEnums.RPC_PROCID_PREPEXEC; - rpc.rpcName = TdsEnums.SP_PREPEXEC; - - //@handle - sqlParam = rpc.systemParams[0]; - sqlParam.SqlDbType = SqlDbType.Int; - sqlParam.Value = _prepareHandle; - sqlParam.Size = 4; - sqlParam.Direction = ParameterDirection.InputOutput; - rpc.systemParamOptions[0] = TdsEnums.RPC_PARAM_BYREF; - - //@batch_params - string paramList = BuildParamList(_stateObj.Parser, _parameters); - sqlParam = rpc.systemParams[1]; - sqlParam.SqlDbType = ((paramList.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT) ? SqlDbType.NVarChar : SqlDbType.NText; - sqlParam.Value = paramList; - sqlParam.Size = paramList.Length; - sqlParam.Direction = ParameterDirection.Input; - - //@batch_text - string text = GetCommandText(behavior); - sqlParam = rpc.systemParams[2]; - sqlParam.SqlDbType = ((text.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT) ? SqlDbType.NVarChar : SqlDbType.NText; - sqlParam.Size = text.Length; - sqlParam.Value = text; - sqlParam.Direction = ParameterDirection.Input; - - 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) @@ -4277,115 +2969,6 @@ private static int GetParameterCount(SqlParameterCollection parameters) return parameters != null ? parameters.Count : 0; } - // - // build the RPC record header for this stored proc and add parameters - // - private void BuildRPC(bool inSchema, SqlParameterCollection parameters, ref _SqlRPC rpc) - { - Debug.Assert(this.CommandType == System.Data.CommandType.StoredProcedure, "Command must be a stored proc to execute an RPC"); - int userParameterCount = CountSendableParameters(parameters); - GetRPCObject(0, userParameterCount, ref rpc); - - rpc.ProcID = 0; - - // TDS Protocol allows rpc name with maximum length of 1046 bytes for ProcName - // 4-part name 1 + 128 + 1 + 1 + 1 + 128 + 1 + 1 + 1 + 128 + 1 + 1 + 1 + 128 + 1 = 523 - // each char takes 2 bytes. 523 * 2 = 1046 - int commandTextLength = ADP.CharSize * CommandText.Length; - if (commandTextLength <= MaxRPCNameLength) - { - rpc.rpcName = CommandText; // just get the raw command text - } - else - { - throw ADP.InvalidArgumentLength(nameof(CommandText), MaxRPCNameLength); - } - - SetUpRPCParameters(rpc, inSchema, parameters); - } - - // - // build the RPC record header for sp_execute - // - // prototype for sp_execute is: - // sp_execute(@handle int,param1value,param2value...) - // - private _SqlRPC BuildExecute(bool inSchema) - { - Debug.Assert(_prepareHandle != s_cachedInvalidPrepareHandle, "Invalid call to sp_execute without a valid handle!"); - - const int systemParameterCount = 1; - int userParameterCount = CountSendableParameters(_parameters); - - _SqlRPC rpc = null; - GetRPCObject(systemParameterCount, userParameterCount, ref rpc); - - rpc.ProcID = TdsEnums.RPC_PROCID_EXECUTE; - rpc.rpcName = TdsEnums.SP_EXECUTE; - - //@handle - SqlParameter sqlParam = rpc.systemParams[0]; - sqlParam.SqlDbType = SqlDbType.Int; - sqlParam.Size = 4; - sqlParam.Value = _prepareHandle; - sqlParam.Direction = ParameterDirection.Input; - - SetUpRPCParameters(rpc, inSchema, _parameters); - return rpc; - } - - // - // build the RPC record header for sp_executesql and add the parameters - // - // prototype for sp_executesql is: - // sp_executesql(@batch_text nvarchar(4000),@batch_params nvarchar(4000), param1,.. paramN) - private void BuildExecuteSql(CommandBehavior behavior, string commandText, SqlParameterCollection parameters, ref _SqlRPC rpc) - { - - Debug.Assert(_prepareHandle == s_cachedInvalidPrepareHandle, "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 systemParamCount; - SqlParameter sqlParam; - - int userParamCount = CountSendableParameters(parameters); - if (userParamCount > 0) - { - systemParamCount = 2; - } - else - { - systemParamCount = 1; - } - - GetRPCObject(systemParamCount, userParamCount, ref rpc); - rpc.ProcID = TdsEnums.RPC_PROCID_EXECUTESQL; - rpc.rpcName = TdsEnums.SP_EXECUTESQL; - - // @sql - if (commandText == null) - { - commandText = GetCommandText(behavior); - } - sqlParam = rpc.systemParams[0]; - sqlParam.SqlDbType = ((commandText.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT) ? SqlDbType.NVarChar : SqlDbType.NText; - sqlParam.Size = commandText.Length; - sqlParam.Value = commandText; - sqlParam.Direction = ParameterDirection.Input; - - if (userParamCount > 0) - { - string paramList = BuildParamList(_stateObj.Parser, _batchRPCMode ? parameters : _parameters); - sqlParam = rpc.systemParams[1]; - sqlParam.SqlDbType = ((paramList.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT) ? SqlDbType.NVarChar : SqlDbType.NText; - sqlParam.Size = paramList.Length; - sqlParam.Value = paramList; - sqlParam.Direction = ParameterDirection.Input; - - bool inSchema = (0 != (behavior & CommandBehavior.SchemaOnly)); - SetUpRPCParameters(rpc, inSchema, parameters); - } - } - /// /// This function constructs a string parameter containing the exec statement in the following format /// N'EXEC sp_name @param1=@param1, @param1=@param2, ..., @paramN=@paramN' diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj index 4ab2e2fdc5..da0f8acad6 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj @@ -759,6 +759,9 @@ Microsoft\Data\SqlClient\SqlCommand.NonQuery.cs + + Microsoft\Data\SqlClient\SqlCommand.Reader.cs + Microsoft\Data\SqlClient\SqlCommand.Scalar.cs diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs index 0f057a87db..28e9b1ab60 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs @@ -34,33 +34,6 @@ public sealed partial class SqlCommand : DbCommand, ICloneable { private const int MaxRPCNameLength = 1046; - internal sealed class ExecuteReaderAsyncCallContext : AAsyncCallContext - { - public Guid OperationID; - public CommandBehavior CommandBehavior; - - public SqlCommand Command => _owner; - public TaskCompletionSource TaskCompletionSource => _source; - - public void Set(SqlCommand command, TaskCompletionSource source, CancellationTokenRegistration disposable, CommandBehavior behavior, Guid operationID) - { - base.Set(command, source, disposable); - CommandBehavior = behavior; - OperationID = operationID; - } - - protected override void Clear() - { - OperationID = default; - CommandBehavior = default; - } - - protected override void AfterCleared(SqlCommand owner) - { - owner?.SetCachedCommandExecuteReaderAsyncContext(this); - } - } - /// /// Indicates if the column encryption setting was set at-least once in the batch rpc mode, when using AddBatchCommand. /// @@ -483,17 +456,6 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } - private SqlDataReader RunExecuteReaderWithRetry( - CommandBehavior cmdBehavior, - RunBehavior runBehavior, - bool returnStream, - [CallerMemberName] string method = "") - { - return RetryLogicProvider.Execute( - this, - () => RunExecuteReader(cmdBehavior, runBehavior, returnStream, method)); - } - private void VerifyEndExecuteState(Task completionTask, string endMethod, bool fullCheckForColumnEncryption = false) { Debug.Assert(completionTask != null); @@ -584,311 +546,6 @@ private void ThrowIfReconnectionHasBeenCanceled() } } - /// - [HostProtection(ExternalThreading = true)] - public IAsyncResult BeginExecuteReader() => - BeginExecuteReader(callback: null, stateObject: null, CommandBehavior.Default); - - /// - [HostProtection(ExternalThreading = true)] - public IAsyncResult BeginExecuteReader(AsyncCallback callback, object stateObject) => - BeginExecuteReader(callback, stateObject, CommandBehavior.Default); - - /// - [HostProtection(ExternalThreading = true)] - public IAsyncResult BeginExecuteReader(CommandBehavior behavior) => - BeginExecuteReader(callback: null, stateObject: null, behavior); - - /// - [HostProtection(ExternalThreading = true)] - public IAsyncResult BeginExecuteReader(AsyncCallback callback, object stateObject, CommandBehavior behavior) - { - SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlCommand.BeginExecuteReader | API | Correlation | Object Id {0}, Behavior {1}, Activity Id {2}, Client Connection Id {3}, Command Text '{4}'", ObjectID, (int)behavior, ActivityCorrelator.Current, Connection?.ClientConnectionId, CommandText); - SqlConnection.ExecutePermission.Demand(); - return BeginExecuteReaderInternal(behavior, callback, stateObject, 0, isRetry: false); - } - - /// - protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) - { - SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlCommand.ExecuteDbDataReader | API | Correlation | Object Id {0}, Activity Id {1}, Client Connection Id {2}, Command Text '{3}'", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId, CommandText); - return ExecuteReader(behavior); - } - - /// - new public SqlDataReader ExecuteReader() - { - SqlStatistics statistics = null; - SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlCommand.ExecuteReader | API | Correlation | ObjectID {0}, Activity Id {1}, Client Connection Id {2}, Command Text '{3}'", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId, CommandText); - try - { - statistics = SqlStatistics.StartTimer(Statistics); - return ExecuteReader(CommandBehavior.Default); - } - finally - { - SqlStatistics.StopTimer(statistics); - } - } - - /// - new public SqlDataReader ExecuteReader(CommandBehavior behavior) - { - SqlConnection.ExecutePermission.Demand(); - - // Reset _pendingCancel upon entry into any Execute - used to synchronize state - // between entry into Execute* API and the thread obtaining the stateObject. - _pendingCancel = false; - - SqlStatistics statistics = null; - bool success = false; - int? sqlExceptionNumber = null; - - using (TryEventScope.Create("SqlCommand.ExecuteReader | API | Object Id {0}", ObjectID)) - { - try - { - WriteBeginExecuteEvent(); - statistics = SqlStatistics.StartTimer(Statistics); - SqlDataReader result = IsProviderRetriable ? - RunExecuteReaderWithRetry(behavior, RunBehavior.ReturnImmediately, returnStream: true) : - RunExecuteReader(behavior, RunBehavior.ReturnImmediately, true); - success = true; - return result; - } - catch (SqlException e) - { - sqlExceptionNumber = e.Number; - throw; - } - // @TODO: CER Exception Handling was removed here (see GH#3581) - finally - { - SqlStatistics.StopTimer(statistics); - WriteEndExecuteEvent(success, sqlExceptionNumber, synchronous: true); - } - } - } - - /// - public SqlDataReader EndExecuteReader(IAsyncResult asyncResult) - { - try - { - return EndExecuteReaderInternal(asyncResult); - } - finally - { - SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlCommand.EndExecuteReader | API | Correlation | Object Id {0}, Activity Id {1}, Client Connection Id {2}, Command Text '{3}'", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId, CommandText); - } - } - - private SqlDataReader EndExecuteReaderAsync(IAsyncResult asyncResult) - { - SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlCommand.EndExecuteReaderAsync | API | Correlation | Object Id {0}, Activity Id {1}, Client Connection Id {2}, Command Text '{3}'", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId, CommandText); - Debug.Assert(!_internalEndExecuteInitiated || _stateObj == null); - - Exception asyncException = ((Task)asyncResult).Exception; - if (asyncException != null) - { - CachedAsyncState?.ResetAsyncState(); - ReliablePutStateObject(); - throw asyncException.InnerException; - } - else - { - ThrowIfReconnectionHasBeenCanceled(); - // lock on _stateObj prevents races with close/cancel. - if (!_internalEndExecuteInitiated) - { - lock (_stateObj) - { - return EndExecuteReaderInternal(asyncResult); - } - } - else - { - return EndExecuteReaderInternal(asyncResult); - } - } - } - - private SqlDataReader EndExecuteReaderInternal(IAsyncResult asyncResult) - { - SqlClientEventSource.Log.TryTraceEvent("SqlCommand.EndExecuteReaderInternal | API | ObjectId {0}, Client Connection Id {1}, MARS={2}, AsyncCommandInProgress={3}", - _activeConnection?.ObjectID, _activeConnection?.ClientConnectionId, - _activeConnection?.Parser?.MARSOn, _activeConnection?.AsyncCommandInProgress); - SqlStatistics statistics = null; - bool success = false; - int? sqlExceptionNumber = null; - try - { - statistics = SqlStatistics.StartTimer(Statistics); - SqlDataReader result = InternalEndExecuteReader( - asyncResult, - isInternal: false, - nameof(EndExecuteReader)); - success = true; - return result; - } - catch (SqlException e) - { - sqlExceptionNumber = e.Number; - if (CachedAsyncState != null) - { - CachedAsyncState.ResetAsyncState(); - }; - - // SqlException is always catchable - ReliablePutStateObject(); - throw; - } - catch (Exception e) - { - if (CachedAsyncState != null) - { - CachedAsyncState.ResetAsyncState(); - }; - if (ADP.IsCatchableExceptionType(e)) - { - ReliablePutStateObject(); - }; - throw; - } - finally - { - SqlStatistics.StopTimer(statistics); - WriteEndExecuteEvent(success, sqlExceptionNumber, synchronous: false); - } - } - - private void CleanupExecuteReaderAsync(Task task, TaskCompletionSource source, Guid operationId) - { - if (task.IsFaulted) - { - Exception e = task.Exception.InnerException; - source.SetException(e); - } - else - { - if (task.IsCanceled) - { - source.SetCanceled(); - } - else - { - source.SetResult(task.Result); - } - } - } - - private IAsyncResult BeginExecuteReaderAsync(CommandBehavior behavior, AsyncCallback callback, object stateObject) - { - return BeginExecuteReaderInternal(behavior, callback, stateObject, CommandTimeout, isRetry: false, asyncWrite: true); - } - - private IAsyncResult BeginExecuteReaderInternal(CommandBehavior behavior, AsyncCallback callback, object stateObject, int timeout, bool isRetry, bool asyncWrite = false) - { - TaskCompletionSource globalCompletion = new TaskCompletionSource(stateObject); - TaskCompletionSource localCompletion = new TaskCompletionSource(stateObject); - - if (!isRetry) - { - // Reset _pendingCancel upon entry into any Execute - used to synchronize state - // between entry into Execute* API and the thread obtaining the stateObject. - _pendingCancel = false; - } - - SqlStatistics statistics = null; - try - { - if (!isRetry) - { - statistics = SqlStatistics.StartTimer(Statistics); - WriteBeginExecuteEvent(); - - ValidateAsyncCommand(); // Special case - done outside of try/catches to prevent putting a stateObj - // back into pool when we should not. - } - - bool usedCache = false; - Task writeTask = null; - try - { - // InternalExecuteNonQuery already has reliability block, but if failure will not put stateObj back into pool. - RunExecuteReader( - behavior, - RunBehavior.ReturnImmediately, - returnStream: true, - localCompletion, - timeout, - out writeTask, - out usedCache, - asyncWrite, - isRetry, - nameof(BeginExecuteReader)); - } - catch (Exception e) - { - if (!ADP.IsCatchableOrSecurityExceptionType(e)) - { - // If not catchable - the connection has already been caught and doomed in RunExecuteReader. - throw; - } - - // For async, RunExecuteReader will never put the stateObj back into the pool, so do so now. - ReliablePutStateObject(); - throw; - } - - if (writeTask != null) - { - AsyncHelper.ContinueTaskWithState(writeTask, localCompletion, this, (object state) => ((SqlCommand)state).BeginExecuteReaderInternalReadStage(localCompletion)); - } - else - { - BeginExecuteReaderInternalReadStage(localCompletion); - } - - // When we use query caching for parameter encryption we need to retry on specific errors. - // In these cases finalize the call internally and trigger a retry when needed. - if ( - !TriggerInternalEndAndRetryIfNecessary( - behavior, - stateObject, - timeout, - usedCache, - isRetry, - asyncWrite, - globalCompletion, - localCompletion, - endFunc: static (SqlCommand command, IAsyncResult asyncResult, bool isInternal, string endMethod) => - { - return command.InternalEndExecuteReader(asyncResult, isInternal, endMethod); - }, - retryFunc: static (SqlCommand command, CommandBehavior behavior, AsyncCallback callback, object stateObject, int timeout, bool isRetry, bool asyncWrite) => - { - return command.BeginExecuteReaderInternal(behavior, callback, stateObject, timeout, isRetry, asyncWrite); - }, - nameof(EndExecuteReader))) - { - globalCompletion = localCompletion; - } - - // Add callback after work is done to avoid overlapping Begin/End methods - if (callback != null) - { - globalCompletion.Task.ContinueWith((t) => callback(t), TaskScheduler.Default); - } - - return globalCompletion.Task; - } - finally - { - SqlStatistics.StopTimer(statistics); - } - } - private bool TriggerInternalEndAndRetryIfNecessary( CommandBehavior behavior, object stateObject, @@ -1063,175 +720,6 @@ private EnclaveSessionParameters GetEnclaveSessionParameters() this._activeConnection.Database); } - private void BeginExecuteReaderInternalReadStage(TaskCompletionSource completion) - { - Debug.Assert(completion != null, "CompletionSource should not be null"); - SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlCommand.BeginExecuteReaderInternalReadStage | INFO | Correlation | Object Id {0}, Activity Id {1}, Client Connection Id {2}, Command Text '{3}'", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId, CommandText); - // Read SNI does not have catches for async exceptions, handle here. - try - { - // must finish caching information before ReadSni which can activate the callback before returning - CachedAsyncState.SetActiveConnectionAndResult(completion, nameof(EndExecuteReader), _activeConnection); - _stateObj.ReadSni(completion); - } - // @TODO: CER Exception Handling was removed here (see GH#3581) - catch (Exception e) - { - // Similarly, if an exception occurs put the stateObj back into the pool. - // and reset async cache information to allow a second async execute - CachedAsyncState?.ResetAsyncState(); - ReliablePutStateObject(); - completion.TrySetException(e); - } - } - - private SqlDataReader InternalEndExecuteReader(IAsyncResult asyncResult, bool isInternal, string endMethod) - { - SqlClientEventSource.Log.TryTraceEvent("SqlCommand.InternalEndExecuteReader | INFO | ObjectId {0}, Client Connection Id {1}, MARS={2}, AsyncCommandInProgress={3}", - _activeConnection?.ObjectID, _activeConnection?.ClientConnectionId, - _activeConnection?.Parser?.MARSOn, _activeConnection?.AsyncCommandInProgress); - VerifyEndExecuteState((Task)asyncResult, endMethod); - WaitForAsyncResults(asyncResult, isInternal); - - // If column encryption is enabled, also check the state after waiting for the task. - // It would be better to do this for all cases, but avoiding for compatibility reasons. - if (IsColumnEncryptionEnabled) - { - VerifyEndExecuteState((Task)asyncResult, endMethod, fullCheckForColumnEncryption: true); - } - - CheckThrowSNIException(); - - SqlDataReader reader = CompleteAsyncExecuteReader(isInternal); - Debug.Assert(_stateObj == null, "non-null state object in InternalEndExecuteReader"); - return reader; - // @TODO: CER Exception Handling was removed here (see GH#3581) - } - - /// - protected override Task ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken) - { - return ExecuteReaderAsync(behavior, cancellationToken).ContinueWith( - static (Task result) => - { - if (result.IsFaulted) - { - throw result.Exception.InnerException; - } - return result.Result; - }, - CancellationToken.None, - TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.NotOnCanceled, - TaskScheduler.Default - ); - } - - /// - public new Task ExecuteReaderAsync() => - ExecuteReaderAsync(CommandBehavior.Default, CancellationToken.None); - - /// - public new Task ExecuteReaderAsync(CommandBehavior behavior) => - ExecuteReaderAsync(behavior, CancellationToken.None); - - /// - public new Task ExecuteReaderAsync(CancellationToken cancellationToken) => - ExecuteReaderAsync(CommandBehavior.Default, cancellationToken); - - /// - public new Task ExecuteReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken) => - IsProviderRetriable - ? InternalExecuteReaderWithRetryAsync(behavior, cancellationToken) - : InternalExecuteReaderAsync(behavior, cancellationToken); - - private Task InternalExecuteReaderWithRetryAsync(CommandBehavior behavior, CancellationToken cancellationToken) => - RetryLogicProvider.ExecuteAsync( - sender: this, - () => InternalExecuteReaderAsync(behavior, cancellationToken), - cancellationToken); - - private Task InternalExecuteReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken) - { - SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlCommand.InternalExecuteReaderAsync | API | Correlation | Object Id {0}, Behavior {1}, Activity Id {2}, Client Connection Id {3}, Command Text '{4}'", ObjectID, (int)behavior, ActivityCorrelator.Current, Connection?.ClientConnectionId, CommandText); - SqlClientEventSource.Log.TryTraceEvent("SqlCommand.InternalExecuteReaderAsync | API> {0}, Client Connection Id {1}, Command Text = '{2}'", ObjectID, Connection?.ClientConnectionId, CommandText); - SqlConnection.ExecutePermission.Demand(); - Guid operationId = default(Guid); - - // connection can be used as state in RegisterForConnectionCloseNotification continuation - // to avoid an allocation so use it as the state value if possible but it can be changed if - // you need it for a more important piece of data that justifies the tuple allocation later - TaskCompletionSource source = new TaskCompletionSource(_activeConnection); - - CancellationTokenRegistration registration = new CancellationTokenRegistration(); - if (cancellationToken.CanBeCanceled) - { - if (cancellationToken.IsCancellationRequested) - { - source.SetCanceled(); - return source.Task; - } - registration = cancellationToken.Register(s_cancelIgnoreFailure, this); - } - - Task returnedTask = source.Task; - ExecuteReaderAsyncCallContext context = null; - try - { - returnedTask = RegisterForConnectionCloseNotification(returnedTask); - - if (_activeConnection?.InnerConnection is SqlInternalConnection sqlInternalConnection) - { - context = Interlocked.Exchange(ref sqlInternalConnection.CachedCommandExecuteReaderAsyncContext, null); - } - if (context is null) - { - context = new ExecuteReaderAsyncCallContext(); - } - context.Set(this, source, registration, behavior, operationId); - - Task.Factory.FromAsync( - beginMethod: static (AsyncCallback callback, object stateObject) => - { - ExecuteReaderAsyncCallContext args = (ExecuteReaderAsyncCallContext)stateObject; - return args.Command.BeginExecuteReaderInternal(args.CommandBehavior, callback, stateObject, args.Command.CommandTimeout, isRetry: false, asyncWrite: true); - }, - endMethod: static (IAsyncResult asyncResult) => - { - ExecuteReaderAsyncCallContext args = (ExecuteReaderAsyncCallContext)asyncResult.AsyncState; - return args.Command.EndExecuteReaderAsync(asyncResult); - }, - state: context - ).ContinueWith( - continuationAction: static (Task task) => - { - ExecuteReaderAsyncCallContext context = (ExecuteReaderAsyncCallContext)task.AsyncState; - SqlCommand command = context.Command; - Guid operationId = context.OperationID; - TaskCompletionSource source = context.TaskCompletionSource; - context.Dispose(); - - command.CleanupExecuteReaderAsync(task, source, operationId); - }, - scheduler: TaskScheduler.Default - ); - } - catch (Exception e) - { - source.SetException(e); - context?.Dispose(); - } - - return returnedTask; - } - - private void SetCachedCommandExecuteReaderAsyncContext(ExecuteReaderAsyncCallContext instance) - { - if (_activeConnection?.InnerConnection is SqlInternalConnection sqlInternalConnection) - { - Interlocked.CompareExchange(ref sqlInternalConnection.CachedCommandExecuteReaderAsyncContext, instance, null); - } - } - /// public void RegisterColumnEncryptionKeyStoreProvidersOnCommand(IDictionary customProviders) { @@ -1985,7 +1473,7 @@ private void PrepareForTransparentEncryption( } // Complete executereader. - describeParameterEncryptionDataReader = CompleteAsyncExecuteReader(forDescribeParameterEncryption: true); + describeParameterEncryptionDataReader = CompleteAsyncExecuteReader(isInternal: false, forDescribeParameterEncryption: true); Debug.Assert(_stateObj == null, "non-null state object in PrepareForTransparentEncryption."); // Read the results of describe parameter encryption. @@ -2060,7 +1548,7 @@ private void PrepareForTransparentEncryption( } // Complete executereader. - describeParameterEncryptionDataReader = CompleteAsyncExecuteReader(forDescribeParameterEncryption: true); + describeParameterEncryptionDataReader = CompleteAsyncExecuteReader(isInternal: false, forDescribeParameterEncryption: true); Debug.Assert(_stateObj == null, "non-null state object in PrepareForTransparentEncryption."); // Read the results of describe parameter encryption. @@ -2757,720 +2245,16 @@ private void ReadDescribeEncryptionParameterResults( } } - internal SqlDataReader RunExecuteReader( - CommandBehavior cmdBehavior, - RunBehavior runBehavior, - bool returnStream, - [CallerMemberName] string method = "") + /// + public SqlCommand Clone() { - Task unused; // sync execution - SqlDataReader reader = RunExecuteReader( - cmdBehavior, - runBehavior, - returnStream, - completion: null, - timeout: CommandTimeout, - task: out unused, - usedCache: out _, - method: method); - - Debug.Assert(unused == null, "returned task during synchronous execution"); - return reader; + SqlCommand clone = new SqlCommand(this); + SqlClientEventSource.Log.TryTraceEvent("SqlCommand.Clone | API | Object Id {0}, Clone Object Id {1}, Client Connection Id {2}", ObjectID, clone.ObjectID, Connection?.ClientConnectionId); + return clone; } - // task is created in case of pending asynchronous write, returned SqlDataReader should not be utilized until that task is complete - internal SqlDataReader RunExecuteReader( - CommandBehavior cmdBehavior, - RunBehavior runBehavior, - bool returnStream, - TaskCompletionSource completion, - int timeout, - out Task task, - out bool usedCache, - bool asyncWrite = false, - bool isRetry = false, - [CallerMemberName] string method = "") - { - bool isAsync = completion != null; - usedCache = false; - - task = null; - - _rowsAffected = -1; - _rowsAffectedBySpDescribeParameterEncryption = -1; - - if (0 != (CommandBehavior.SingleRow & cmdBehavior)) - { - // CommandBehavior.SingleRow implies CommandBehavior.SingleResult - cmdBehavior |= CommandBehavior.SingleResult; - } - - // this function may throw for an invalid connection - // returns false for empty command text - if (!isRetry) - { - ValidateCommand(isAsync, method); - } - - CheckNotificationStateAndAutoEnlist(); // Only call after validate - requires non null connection! - - // This section needs to occur AFTER ValidateCommand - otherwise it will AV without a connection. - SqlStatistics statistics = Statistics; - if (statistics != null) - { - if ((!this.IsDirty && this.IsPrepared && !_hiddenPrepare) - || (this.IsPrepared && _execType == EXECTYPE.PREPAREPENDING)) - { - statistics.SafeIncrement(ref statistics._preparedExecs); - } - else - { - statistics.SafeIncrement(ref statistics._unpreparedExecs); - } - } - - // Reset the encryption related state of the command and its parameters. - ResetEncryptionState(); - - if (IsColumnEncryptionEnabled) - { - Task returnTask = null; - PrepareForTransparentEncryption(isAsync, timeout, completion, out returnTask, asyncWrite && isAsync, out usedCache, isRetry); - Debug.Assert(usedCache || (isAsync == (returnTask != null)), @"if we didn't use the cache, returnTask should be null if and only if async is false."); - - long firstAttemptStart = ADP.TimerCurrent(); - - try - { - return RunExecuteReaderTdsWithTransparentParameterEncryption( - cmdBehavior, - runBehavior, - returnStream, - isAsync, - timeout, - out task, - asyncWrite && isAsync, - isRetry: isRetry, - ds: null, - describeParameterEncryptionTask: returnTask); - } - - catch (EnclaveDelegate.RetryableEnclaveQueryExecutionException) - { - if (isRetry) - { - throw; - } - - // Retry if the command failed with appropriate error. - // First invalidate the entry from the cache, so that we refresh our encryption MD. - SqlQueryMetadataCache.GetInstance().InvalidateCacheEntry(this); - - InvalidateEnclaveSession(); - - return RunExecuteReader( - cmdBehavior, - runBehavior, - returnStream, - completion, - TdsParserStaticMethods.GetRemainingTimeout(timeout, firstAttemptStart), - out task, - out usedCache, - isAsync, - isRetry: true, - method: method); - } - - catch (SqlException ex) - { - // We only want to retry once, so don't retry if we are already in retry. - // If we didn't use the cache, we don't want to retry. - if (isRetry || (!usedCache && !ShouldUseEnclaveBasedWorkflow)) - { - throw; - } - - bool shouldRetry = false; - - // Check if we have an error indicating that we can retry. - for (int i = 0; i < ex.Errors.Count; i++) - { - - if ((usedCache && (ex.Errors[i].Number == TdsEnums.TCE_CONVERSION_ERROR_CLIENT_RETRY)) || - (ShouldUseEnclaveBasedWorkflow && (ex.Errors[i].Number == TdsEnums.TCE_ENCLAVE_INVALID_SESSION_HANDLE))) - { - shouldRetry = true; - break; - } - } - - if (!shouldRetry) - { - throw; - } - else - { - // Retry if the command failed with appropriate error. - // First invalidate the entry from the cache, so that we refresh our encryption MD. - SqlQueryMetadataCache.GetInstance().InvalidateCacheEntry(this); - - InvalidateEnclaveSession(); - - return RunExecuteReader( - cmdBehavior, - runBehavior, - returnStream, - completion, - TdsParserStaticMethods.GetRemainingTimeout(timeout, firstAttemptStart), - out task, - out usedCache, - isAsync, - isRetry: true, - method: method); - } - } - } - else - { - return RunExecuteReaderTds(cmdBehavior, runBehavior, returnStream, isAsync, timeout, out task, asyncWrite && isAsync, isRetry: isRetry); - } - // @TODO: CER Exception Handling was removed here (see GH#3581) - } - - - private SqlDataReader RunExecuteReaderTdsWithTransparentParameterEncryption( - CommandBehavior cmdBehavior, - RunBehavior runBehavior, - bool returnStream, - bool isAsync, - int timeout, - out Task task, - bool asyncWrite, - bool isRetry, - SqlDataReader ds = null, - Task describeParameterEncryptionTask = null) - { - Debug.Assert(!asyncWrite || isAsync, "AsyncWrite should be always accompanied by Async"); - - if (ds == null && returnStream) - { - ds = new SqlDataReader(this, cmdBehavior); - } - - if (describeParameterEncryptionTask != null) - { - long parameterEncryptionStart = ADP.TimerCurrent(); - TaskCompletionSource completion = new TaskCompletionSource(); - AsyncHelper.ContinueTaskWithState(describeParameterEncryptionTask, completion, this, - (object state) => - { - SqlCommand command = (SqlCommand)state; - Task subTask = null; - command.GenerateEnclavePackage(); - command.RunExecuteReaderTds(cmdBehavior, runBehavior, returnStream, isAsync, TdsParserStaticMethods.GetRemainingTimeout(timeout, parameterEncryptionStart), out subTask, asyncWrite, isRetry, ds); - if (subTask == null) - { - completion.SetResult(null); - } - else - { - AsyncHelper.ContinueTaskWithState(subTask, completion, completion, static (object state2) => ((TaskCompletionSource)state2).SetResult(null)); - } - }, - onFailure: static (Exception exception, object state) => - { - ((SqlCommand)state).CachedAsyncState?.ResetAsyncState(); - if (exception != null) - { - throw exception; - } - }, - onCancellation: static (object state) => ((SqlCommand)state).CachedAsyncState?.ResetAsyncState(), - connectionToDoom: null, - connectionToAbort: _activeConnection); - task = completion.Task; - return ds; - } - else - { - // Synchronous execution. - GenerateEnclavePackage(); - return RunExecuteReaderTds(cmdBehavior, runBehavior, returnStream, isAsync, timeout, out task, asyncWrite, isRetry, ds); - } - } - - private void GenerateEnclavePackage() - { - if (keysToBeSentToEnclave == null || keysToBeSentToEnclave.Count <= 0) - { - return; - } - - if (string.IsNullOrWhiteSpace(this._activeConnection.EnclaveAttestationUrl) && - Connection.AttestationProtocol != SqlConnectionAttestationProtocol.None) - { - throw SQL.NoAttestationUrlSpecifiedForEnclaveBasedQueryGeneratingEnclavePackage(this._activeConnection.Parser.EnclaveType); - } - - string enclaveType = this._activeConnection.Parser.EnclaveType; - if (string.IsNullOrWhiteSpace(enclaveType)) - { - throw SQL.EnclaveTypeNullForEnclaveBasedQuery(); - } - - SqlConnectionAttestationProtocol attestationProtocol = this._activeConnection.AttestationProtocol; - if (attestationProtocol == SqlConnectionAttestationProtocol.NotSpecified) - { - throw SQL.AttestationProtocolNotSpecifiedForGeneratingEnclavePackage(); - } - - try - { -#if DEBUG - if (_forceRetryableEnclaveQueryExecutionExceptionDuringGenerateEnclavePackage) - { - _forceRetryableEnclaveQueryExecutionExceptionDuringGenerateEnclavePackage = false; - throw new EnclaveDelegate.RetryableEnclaveQueryExecutionException("testing", null); - } -#endif - this.enclavePackage = EnclaveDelegate.Instance.GenerateEnclavePackage(attestationProtocol, keysToBeSentToEnclave, - this.CommandText, enclaveType, GetEnclaveSessionParameters(), _activeConnection, this); - } - catch (EnclaveDelegate.RetryableEnclaveQueryExecutionException) - { - throw; - } - catch (Exception e) - { - throw SQL.ExceptionWhenGeneratingEnclavePackage(e); - } - } - - private SqlDataReader RunExecuteReaderTds( - CommandBehavior cmdBehavior, - RunBehavior runBehavior, - bool returnStream, - bool isAsync, - int timeout, - out Task task, - bool asyncWrite, - bool isRetry, - SqlDataReader ds = null, - bool describeParameterEncryptionRequest = false) - { - Debug.Assert(!asyncWrite || isAsync, "AsyncWrite should be always accompanied by Async"); - - if (ds == null && returnStream) - { - ds = new SqlDataReader(this, cmdBehavior); - } - - Task reconnectTask = _activeConnection.ValidateAndReconnect(null, timeout); - - if (reconnectTask != null) - { - long reconnectionStart = ADP.TimerCurrent(); - if (isAsync) - { - TaskCompletionSource completion = new TaskCompletionSource(); - _activeConnection.RegisterWaitingForReconnect(completion.Task); - _reconnectionCompletionSource = completion; - CancellationTokenSource timeoutCTS = new CancellationTokenSource(); - AsyncHelper.SetTimeoutException(completion, timeout, static () => 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, isAsync, TdsParserStaticMethods.GetRemainingTimeout(timeout, reconnectionStart), out subTask, asyncWrite, isRetry, ds); - if (subTask == null) - { - completion.SetResult(null); - } - else - { - AsyncHelper.ContinueTaskWithState(subTask, completion, completion, static (object state) => ((TaskCompletionSource)state).SetResult(null)); - } - }, - connectionToAbort: _activeConnection - ); - task = completion.Task; - return ds; - } - else - { - AsyncHelper.WaitForCompletion(reconnectTask, timeout, static () => throw SQL.CR_ReconnectTimeout()); - timeout = TdsParserStaticMethods.GetRemainingTimeout(timeout, reconnectionStart); - } - } - - // make sure we have good parameter information - // prepare the command - // execute - Debug.Assert(_activeConnection.Parser != null, "TdsParser class should not be null in Command.Execute!"); - - bool inSchema = (0 != (cmdBehavior & CommandBehavior.SchemaOnly)); - - // create a new RPC - _SqlRPC rpc = null; - - task = null; - - string optionSettings = null; - bool processFinallyBlock = true; - bool decrementAsyncCountOnFailure = false; - - if (isAsync) - { - _activeConnection.GetOpenTdsConnection().IncrementAsyncCount(); - decrementAsyncCountOnFailure = true; - } - - try - { - if (asyncWrite) - { - _activeConnection.AddWeakReference(this, SqlReferenceCollection.CommandTag); - } - - GetStateObject(); - Task writeTask = null; - - if (describeParameterEncryptionRequest) - { -#if DEBUG - if (_sleepDuringRunExecuteReaderTdsForSpDescribeParameterEncryption) - { - Thread.Sleep(10000); - } -#endif - - Debug.Assert(_sqlRPCParameterEncryptionReqArray != null, "RunExecuteReader rpc array not provided for describe parameter encryption request."); - writeTask = _stateObj.Parser.TdsExecuteRPC(this, _sqlRPCParameterEncryptionReqArray, timeout, inSchema, this.Notification, _stateObj, CommandType.StoredProcedure == CommandType, sync: !asyncWrite); - } - else if (_batchRPCMode) - { - Debug.Assert(inSchema == false, "Batch RPC does not support schema only command behavior"); - Debug.Assert(!IsPrepared, "Batch RPC should not be prepared!"); - Debug.Assert(!IsDirty, "Batch RPC should not be marked as dirty!"); - Debug.Assert(_RPCList != null, "RunExecuteReader rpc array not provided"); - writeTask = _stateObj.Parser.TdsExecuteRPC(this, _RPCList, timeout, inSchema, this.Notification, _stateObj, CommandType.StoredProcedure == CommandType, sync: !asyncWrite); - } - 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!"); - - if (returnStream) - { - SqlClientEventSource.Log.TryTraceEvent("SqlCommand.RunExecuteReaderTds | Info | Object Id {0}, Activity Id {1}, Client Connection Id {2}, Command executed as SQLBATCH, Command Text '{3}' ", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId, CommandText); - } - string text = GetCommandText(cmdBehavior) + GetResetOptionsString(cmdBehavior); - - //If the query requires enclave computations, pass the enclavepackage in the SQLBatch TDS stream - if (requiresEnclaveComputations) - { - - if (this.enclavePackage == null) - { - throw SQL.NullEnclavePackageForEnclaveBasedQuery(this._activeConnection.Parser.EnclaveType, this._activeConnection.EnclaveAttestationUrl); - } - - writeTask = _stateObj.Parser.TdsExecuteSQLBatch(text, timeout, this.Notification, _stateObj, - sync: !asyncWrite, enclavePackage: this.enclavePackage.EnclavePackageBytes); - } - else - { - writeTask = _stateObj.Parser.TdsExecuteSQLBatch(text, timeout, this.Notification, _stateObj, sync: !asyncWrite); - } - } - else if (System.Data.CommandType.Text == this.CommandType) - { - if (this.IsDirty) - { - Debug.Assert(_cachedMetaData == null || !_dirty, "dirty query should not have cached metadata!"); // can have cached metadata if dirty because of parameters - // - // someone changed the command text or the parameter schema so we must unprepare the command - // - // remember that IsDirty includes test for IsPrepared! - if (_execType == EXECTYPE.PREPARED) - { - _hiddenPrepare = true; - } - Unprepare(); - IsDirty = false; - } - - if (_execType == EXECTYPE.PREPARED) - { - Debug.Assert(IsPrepared && _prepareHandle != s_cachedInvalidPrepareHandle, "invalid attempt to call sp_execute without a handle!"); - rpc = BuildExecute(inSchema); - } - else if (_execType == EXECTYPE.PREPAREPENDING) - { - rpc = BuildPrepExec(cmdBehavior); - // next time through, only do an exec - _execType = EXECTYPE.PREPARED; - _preparedConnectionCloseCount = _activeConnection.CloseCount; - _preparedConnectionReconnectCount = _activeConnection.ReconnectCount; - // mark ourselves as preparing the command - _inPrepare = true; - } - else - { - Debug.Assert(_execType == EXECTYPE.UNPREPARED, "Invalid execType!"); - BuildExecuteSql(cmdBehavior, null, _parameters, ref rpc); - } - - // if 2000, then set NOMETADATA_UNLESSCHANGED flag - rpc.options = TdsEnums.RPC_NOMETADATA; - if (returnStream) - { - SqlClientEventSource.Log.TryTraceEvent("SqlCommand.RunExecuteReaderTds | Info | Object Id {0}, Activity Id {1}, Client Connection Id {2}, Command executed as RPC, RPC Name '{3}' ", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId, rpc?.rpcName); - } - - // TODO: Medusa: Unprepare only happens for SQL 7.0 which may be broken anyway (it's not re-prepared). Consider removing the reset here if we're really dropping 7.0 support. - Debug.Assert(_rpcArrayOf1[0] == rpc); - writeTask = _stateObj.Parser.TdsExecuteRPC(this, _rpcArrayOf1, timeout, inSchema, this.Notification, _stateObj, CommandType.StoredProcedure == CommandType, sync: !asyncWrite); - } - else - { - Debug.Assert(this.CommandType == System.Data.CommandType.StoredProcedure, "unknown command type!"); - - BuildRPC(inSchema, _parameters, ref rpc); - - // if we need to augment the command because a user has changed the command behavior (e.g. FillSchema) - // then batch sql them over. This is inefficient (3 round trips) but the only way we can get metadata only from - // a stored proc - optionSettings = GetSetOptionsString(cmdBehavior); - if (returnStream) - { - SqlClientEventSource.Log.TryTraceEvent("SqlCommand.RunExecuteReaderTds | Info | Object Id {0}, Activity Id {1}, Client Connection Id {2}, Command executed as RPC, RPC Name '{3}' ", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId, rpc?.rpcName); - } - - // turn set options ON - if (optionSettings != null) - { - Task executeTask = _stateObj.Parser.TdsExecuteSQLBatch(optionSettings, timeout, this.Notification, _stateObj, sync: true); - Debug.Assert(executeTask == null, "Shouldn't get a task when doing sync writes"); - Debug.Assert(_stateObj._syncOverAsync, "Should not attempt pends in a synchronous call"); - TdsOperationStatus result = _stateObj.Parser.TryRun(RunBehavior.UntilDone, this, null, null, _stateObj, out _); - if (result != TdsOperationStatus.Done) - { - throw SQL.SynchronousCallMayNotPend(); - } - // and turn OFF when the ds exhausts the stream on Close() - optionSettings = GetResetOptionsString(cmdBehavior); - } - - // execute sp - Debug.Assert(_rpcArrayOf1[0] == rpc); - writeTask = _stateObj.Parser.TdsExecuteRPC(this, _rpcArrayOf1, timeout, inSchema, this.Notification, _stateObj, CommandType.StoredProcedure == CommandType, sync: !asyncWrite); - } - - Debug.Assert(writeTask == null || isAsync, "Returned task in sync mode"); - - if (isAsync) - { - 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(); - }); - } - else - { - CachedAsyncState.SetAsyncReaderState(ds, runBehavior, optionSettings); - } - } - else - { - // Always execute - even if no reader! - FinishExecuteReader(ds, runBehavior, optionSettings, isInternal: false, forDescribeParameterEncryption: false, shouldCacheForAlwaysEncrypted: !describeParameterEncryptionRequest); - } - } - catch (Exception e) - { - processFinallyBlock = ADP.IsCatchableExceptionType(e); - if (decrementAsyncCountOnFailure) - { - SqlInternalConnectionTds innerConnectionTds = (_activeConnection.InnerConnection as SqlInternalConnectionTds); - if (innerConnectionTds != null) - { - // it may be closed - innerConnectionTds.DecrementAsyncCount(); - } - } - throw; - } - finally - { - if (processFinallyBlock && !isAsync) - { - // When executing async, we need to keep the _stateObj alive... - PutStateObject(); - } - } - - Debug.Assert(isAsync || _stateObj == null, "non-null state object in RunExecuteReader"); - return ds; - } - - private SqlDataReader CompleteAsyncExecuteReader(bool isInternal = false, bool forDescribeParameterEncryption = false) - { - SqlDataReader ds = CachedAsyncState.CachedAsyncReader; // should not be null - bool processFinallyBlock = true; - try - { - FinishExecuteReader(ds, CachedAsyncState.CachedRunBehavior, CachedAsyncState.CachedSetOptions, isInternal, forDescribeParameterEncryption, shouldCacheForAlwaysEncrypted: !forDescribeParameterEncryption); - } - catch (Exception e) - { - processFinallyBlock = ADP.IsCatchableExceptionType(e); - throw; - } - finally - { - if (processFinallyBlock) - { - // Don't reset the state for internal End. The user End will do that eventually. - if (!isInternal) - { - CachedAsyncState.ResetAsyncState(); - } - PutStateObject(); - } - } - - return ds; - } - - private void FinishExecuteReader(SqlDataReader ds, RunBehavior runBehavior, string resetOptionsString, bool isInternal, bool forDescribeParameterEncryption, bool shouldCacheForAlwaysEncrypted = true) - { - // always wrap with a try { FinishExecuteReader(...) } finally { PutStateObject(); } - - // If this is not for internal usage, notify the dependency. If we have already initiated the end internally, the reader should be ready, so just return. - if (!isInternal && !forDescribeParameterEncryption) - { - NotifyDependency(); - - if (_internalEndExecuteInitiated) - { - Debug.Assert(_stateObj == null); - return; - } - } - - if (runBehavior == RunBehavior.UntilDone) - { - try - { - Debug.Assert(_stateObj._syncOverAsync, "Should not attempt pends in a synchronous call"); - TdsOperationStatus result = _stateObj.Parser.TryRun(RunBehavior.UntilDone, this, ds, null, _stateObj, out _); - if (result != TdsOperationStatus.Done) - { - throw SQL.SynchronousCallMayNotPend(); - } - } - catch (Exception e) - { - if (ADP.IsCatchableExceptionType(e)) - { - if (_inPrepare) - { - // The flag is expected to be reset by OnReturnValue. We should receive - // the handle unless command execution failed. If fail, move back to pending - // state. - _inPrepare = false; // reset the flag - IsDirty = true; // mark command as dirty so it will be prepared next time we're coming through - _execType = EXECTYPE.PREPAREPENDING; // reset execution type to pending - } - - if (ds != null) - { - ds.Close(); - } - } - throw; - } - } - - // bind the parser to the reader if we get this far - if (ds != null) - { - ds.Bind(_stateObj); - _stateObj = null; // the reader now owns this... - ds.ResetOptionsString = resetOptionsString; - - // bind this reader to this connection now - _activeConnection.AddWeakReference(ds, SqlReferenceCollection.DataReaderTag); - - // force this command to start reading data off the wire. - // this will cause an error to be reported at Execute() time instead of Read() time - // if the command is not set. - try - { - //This flag indicates if the datareader's metadata should be cached in this SqlCommand. - //Metadata associated with sp_describe_parameter_metadats's datareader should not be cached. - //Ideally, we should be using "forDescribeParameterEncryption" flag for this, but this flag's - //semantics are overloaded with async workflow and this flag is always false for sync workflow. - //Since we are very close to a release and changing the semantics for "forDescribeParameterEncryption" - //is risky, we introduced a new parameter to determine whether we should cache a datareader's metadata or not. - if (shouldCacheForAlwaysEncrypted) - { - _cachedMetaData = ds.MetaData; - } - else - { - //we need this call to ensure that the datareader is properly intialized, the getter is initializing state in SqlDataReader - _SqlMetaDataSet temp = ds.MetaData; - } - ds.IsInitialized = true; - } - catch (Exception e) - { - if (ADP.IsCatchableExceptionType(e)) - { - if (_inPrepare) - { - // The flag is expected to be reset by OnReturnValue. We should receive - // the handle unless command execution failed. If fail, move back to pending - // state. - _inPrepare = false; // reset the flag - IsDirty = true; // mark command as dirty so it will be prepared next time we're coming through - _execType = EXECTYPE.PREPAREPENDING; // reset execution type to pending - } - - ds.Close(); - } - - throw; - } - } - } - - /// - public SqlCommand Clone() - { - SqlCommand clone = new SqlCommand(this); - SqlClientEventSource.Log.TryTraceEvent("SqlCommand.Clone | API | Object Id {0}, Clone Object Id {1}, Client Connection Id {2}", ObjectID, clone.ObjectID, Connection?.ClientConnectionId); - return clone; - } - - object ICloneable.Clone() => - Clone(); + object ICloneable.Clone() => + Clone(); private Task RegisterForConnectionCloseNotification(Task outterTask) { @@ -4122,48 +2906,6 @@ private void SetUpRPCParameters(_SqlRPC rpc, bool inSchema, SqlParameterCollecti 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; - - const int systemParameterCount = 3; - int userParameterCount = CountSendableParameters(_parameters); - - _SqlRPC rpc = null; - GetRPCObject(systemParameterCount, userParameterCount, ref rpc); - - rpc.ProcID = TdsEnums.RPC_PROCID_PREPEXEC; - rpc.rpcName = TdsEnums.SP_PREPEXEC; - - //@handle - sqlParam = rpc.systemParams[0]; - sqlParam.SqlDbType = SqlDbType.Int; - sqlParam.Value = _prepareHandle; - sqlParam.Size = 4; - sqlParam.Direction = ParameterDirection.InputOutput; - rpc.systemParamOptions[0] = TdsEnums.RPC_PARAM_BYREF; - - //@batch_params - string paramList = BuildParamList(_stateObj.Parser, _parameters); - sqlParam = rpc.systemParams[1]; - sqlParam.SqlDbType = ((paramList.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT) ? SqlDbType.NVarChar : SqlDbType.NText; - sqlParam.Value = paramList; - sqlParam.Size = paramList.Length; - sqlParam.Direction = ParameterDirection.Input; - - //@batch_text - string text = GetCommandText(behavior); - sqlParam = rpc.systemParams[2]; - sqlParam.SqlDbType = ((text.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT) ? SqlDbType.NVarChar : SqlDbType.NText; - sqlParam.Size = text.Length; - sqlParam.Value = text; - sqlParam.Direction = ParameterDirection.Input; - - 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) @@ -4210,115 +2952,6 @@ private static int GetParameterCount(SqlParameterCollection parameters) return parameters != null ? parameters.Count : 0; } - // - // build the RPC record header for this stored proc and add parameters - // - private void BuildRPC(bool inSchema, SqlParameterCollection parameters, ref _SqlRPC rpc) - { - Debug.Assert(this.CommandType == System.Data.CommandType.StoredProcedure, "Command must be a stored proc to execute an RPC"); - int userParameterCount = CountSendableParameters(parameters); - GetRPCObject(0, userParameterCount, ref rpc); - - rpc.ProcID = 0; - - // TDS Protocol allows rpc name with maximum length of 1046 bytes for ProcName - // 4-part name 1 + 128 + 1 + 1 + 1 + 128 + 1 + 1 + 1 + 128 + 1 + 1 + 1 + 128 + 1 = 523 - // each char takes 2 bytes. 523 * 2 = 1046 - int commandTextLength = ADP.CharSize * CommandText.Length; - if (commandTextLength <= MaxRPCNameLength) - { - rpc.rpcName = CommandText; // just get the raw command text - } - else - { - throw ADP.InvalidArgumentLength(nameof(CommandText), MaxRPCNameLength); - } - - SetUpRPCParameters(rpc, inSchema, parameters); - } - - // - // build the RPC record header for sp_execute - // - // prototype for sp_execute is: - // sp_execute(@handle int,param1value,param2value...) - // - private _SqlRPC BuildExecute(bool inSchema) - { - Debug.Assert(_prepareHandle != s_cachedInvalidPrepareHandle, "Invalid call to sp_execute without a valid handle!"); - - const int systemParameterCount = 1; - int userParameterCount = CountSendableParameters(_parameters); - - _SqlRPC rpc = null; - GetRPCObject(systemParameterCount, userParameterCount, ref rpc); - - rpc.ProcID = TdsEnums.RPC_PROCID_EXECUTE; - rpc.rpcName = TdsEnums.SP_EXECUTE; - - //@handle - SqlParameter sqlParam = rpc.systemParams[0]; - sqlParam.SqlDbType = SqlDbType.Int; - sqlParam.Size = 4; - sqlParam.Value = _prepareHandle; - sqlParam.Direction = ParameterDirection.Input; - - SetUpRPCParameters(rpc, inSchema, _parameters); - return rpc; - } - - // - // build the RPC record header for sp_executesql and add the parameters - // - // prototype for sp_executesql is: - // sp_executesql(@batch_text nvarchar(4000),@batch_params nvarchar(4000), param1,.. paramN) - private void BuildExecuteSql(CommandBehavior behavior, string commandText, SqlParameterCollection parameters, ref _SqlRPC rpc) - { - - Debug.Assert(_prepareHandle == s_cachedInvalidPrepareHandle, "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 systemParamCount; - SqlParameter sqlParam; - - int userParamCount = CountSendableParameters(parameters); - if (userParamCount > 0) - { - systemParamCount = 2; - } - else - { - systemParamCount = 1; - } - - GetRPCObject(systemParamCount, userParamCount, ref rpc); - rpc.ProcID = TdsEnums.RPC_PROCID_EXECUTESQL; - rpc.rpcName = TdsEnums.SP_EXECUTESQL; - - // @sql - if (commandText == null) - { - commandText = GetCommandText(behavior); - } - sqlParam = rpc.systemParams[0]; - sqlParam.SqlDbType = ((commandText.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT) ? SqlDbType.NVarChar : SqlDbType.NText; - sqlParam.Size = commandText.Length; - sqlParam.Value = commandText; - sqlParam.Direction = ParameterDirection.Input; - - if (userParamCount > 0) - { - string paramList = BuildParamList(_stateObj.Parser, _batchRPCMode ? parameters : _parameters); - sqlParam = rpc.systemParams[1]; - sqlParam.SqlDbType = ((paramList.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT) ? SqlDbType.NVarChar : SqlDbType.NText; - sqlParam.Size = paramList.Length; - sqlParam.Value = paramList; - sqlParam.Direction = ParameterDirection.Input; - - bool inSchema = (0 != (behavior & CommandBehavior.SchemaOnly)); - SetUpRPCParameters(rpc, inSchema, parameters); - } - } - /// /// This function constructs a string parameter containing the exec statement in the following format /// N'EXEC sp_name @param1=@param1, @param1=@param2, ..., @paramN=@paramN' diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.NonQuery.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.NonQuery.cs index af30865850..a88fa63319 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.NonQuery.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.NonQuery.cs @@ -506,7 +506,7 @@ private object InternalEndExecuteNonQuery( else { // Otherwise, use a full-fledged execute that can handle params and stored sprocs - SqlDataReader reader = CompleteAsyncExecuteReader(isInternal); + SqlDataReader reader = CompleteAsyncExecuteReader(isInternal, forDescribeParameterEncryption: false); reader?.Close(); } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Reader.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Reader.cs new file mode 100644 index 0000000000..7c6c90a79b --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Reader.cs @@ -0,0 +1,1836 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Data; +using System.Data.Common; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Data.Common; + +#if NETFRAMEWORK +using System.Security.Permissions; +using Microsoft.Data.SqlClient.Utilities; +#endif + +namespace Microsoft.Data.SqlClient +{ + public sealed partial class SqlCommand + { + #region Public/Internal Methods + + /// + #if NETFRAMEWORK + [HostProtection(ExternalThreading = true)] + #endif + public IAsyncResult BeginExecuteReader() => + BeginExecuteReader(callback: null, stateObject: null, CommandBehavior.Default); + + /// + #if NETFRAMEWORK + [HostProtection(ExternalThreading = true)] + #endif + public IAsyncResult BeginExecuteReader(AsyncCallback callback, object stateObject) => + BeginExecuteReader(callback, stateObject, CommandBehavior.Default); + + /// + #if NETFRAMEWORK + [HostProtection(ExternalThreading = true)] + #endif + public IAsyncResult BeginExecuteReader(AsyncCallback callback, object stateObject, CommandBehavior behavior) + { + #if NETFRAMEWORK + SqlConnection.ExecutePermission.Demand(); + #endif + + SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlCommand.BeginExecuteReader | API | Correlation | Object Id {0}, Behavior {1}, Activity Id {2}, Client Connection Id {3}, Command Text '{4}'", ObjectID, (int)behavior, ActivityCorrelator.Current, Connection?.ClientConnectionId, CommandText); + return BeginExecuteReaderInternal(behavior, callback, stateObject, 0, isRetry: false); + } + + /// + #if NETFRAMEWORK + [HostProtection(ExternalThreading = true)] + #endif + public IAsyncResult BeginExecuteReader(CommandBehavior behavior) => + BeginExecuteReader(callback: null, stateObject: null, behavior); + + /// + public SqlDataReader EndExecuteReader(IAsyncResult asyncResult) + { + try + { + return EndExecuteReaderInternal(asyncResult); + } + finally + { + SqlClientEventSource.Log.TryCorrelationTraceEvent( + "SqlCommand.EndExecuteReader | API | Correlation | " + + $"Object Id {ObjectID}, " + + $"Activity Id {ActivityCorrelator.Current}, " + + $"Client Connection Id {_activeConnection?.ClientConnectionId}, " + + $"Command Text '{CommandText}'"); + } + } + + /// + public new SqlDataReader ExecuteReader() + { + SqlClientEventSource.Log.TryCorrelationTraceEvent( + "SqlCommand.ExecuteReader | API | Correlation | " + + $"Object Id {ObjectID}, " + + $"Activity Id {ActivityCorrelator.Current}, " + + $"Client Connection Id {_activeConnection?.ClientConnectionId}, " + + $"Command Text '{CommandText}'"); + + SqlStatistics statistics = null; + try + { + statistics = SqlStatistics.StartTimer(Statistics); + return ExecuteReader(CommandBehavior.Default); + } + finally + { + SqlStatistics.StopTimer(statistics); + } + } + + /// + public new SqlDataReader ExecuteReader(CommandBehavior behavior) + { + #if NETFRAMEWORK + SqlConnection.ExecutePermission.Demand(); + #endif + + // Reset _pendingCancel upon entry into any Execute - used to synchronize state + // between entry into Execute* API and the thread obtaining the stateObject. + _pendingCancel = false; + + // @TODO: Do we want to use a command scope here like nonquery and xml? or is operation id ok? + #if NET + Guid operationId = s_diagnosticListener.WriteCommandBefore(this, _transaction); + Exception e = null; + #endif + + using var eventScope = TryEventScope.Create($"SqlCommand.ExecuteReader | API | Object Id {ObjectID}"); + // @TODO: Do we want to have a correlation trace event here like nonquery and xml? + // @TODO: Basically, this doesn't follow the same pattern as nonquery, scalar, or xml. Doesn't seem right. + + SqlStatistics statistics = null; + bool success = false; + int? sqlExceptionNumber = null; + + try + { + statistics = SqlStatistics.StartTimer(Statistics); + WriteBeginExecuteEvent(); + + SqlDataReader result = IsProviderRetriable + ? RunExecuteReaderWithRetry(behavior, RunBehavior.ReturnImmediately, returnStream: true) + : RunExecuteReader(behavior, RunBehavior.ReturnImmediately, returnStream: true); + success = true; + return result; + } + // @TODO: CER Exception Handling was removed here (see GH#3581) + catch (Exception ex) + { + #if NET + e = ex; + #endif + + if (ex is SqlException sqlException) + { + sqlExceptionNumber = sqlException.Number; + } + + throw; + } + finally + { + SqlStatistics.StopTimer(statistics); + WriteEndExecuteEvent(success, sqlExceptionNumber, synchronous: true); + + #if NET + if (e is not null) + { + s_diagnosticListener.WriteCommandError(operationId, this, _transaction, e); + } + else + { + s_diagnosticListener.WriteCommandAfter(operationId, this, _transaction); + } + #endif + } + } + + /// + public new Task ExecuteReaderAsync() => + ExecuteReaderAsync(CommandBehavior.Default, CancellationToken.None); + + /// + public new Task ExecuteReaderAsync(CancellationToken cancellationToken) => + ExecuteReaderAsync(CommandBehavior.Default, cancellationToken); + + /// + public new Task ExecuteReaderAsync(CommandBehavior behavior) => + ExecuteReaderAsync(behavior, CancellationToken.None); + + /// + public new Task ExecuteReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken) => + IsProviderRetriable + ? InternalExecuteReaderWithRetryAsync(behavior, cancellationToken) + : InternalExecuteReaderAsync(behavior, cancellationToken); + + // @TODO: This is only used for synchronous execution + internal SqlDataReader RunExecuteReader( + CommandBehavior cmdBehavior, + RunBehavior runBehavior, + bool returnStream, + [CallerMemberName] string method = "") + { + SqlDataReader reader = RunExecuteReader( + cmdBehavior, + runBehavior, + returnStream, + completion: null, + timeout: CommandTimeout, + executeTask: out Task unused, + usedCache: out _, + method: method); + + // @TODO: This really isn't necessary... + Debug.Assert(unused == null, "returned task during synchronous execution"); + return reader; + } + + #endregion + + #region Private Methods + + /// + protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) + { + // @TODO: Yknow, we use this all over the place. It could be factored out. + SqlClientEventSource.Log.TryCorrelationTraceEvent( + "SqlCommand.ExecuteDbDataReader | API | Correlation | " + + $"Object Id {ObjectID}, " + + $"Activity Id {ActivityCorrelator.Current}, " + + $"Client Connection Id {_activeConnection?.ClientConnectionId}, " + + $"Command Text '{CommandText}'"); + + return ExecuteReader(behavior); + } + + /// + protected override Task ExecuteDbDataReaderAsync( + CommandBehavior behavior, + CancellationToken cancellationToken) + { + return ExecuteReaderAsync(behavior, cancellationToken) + .ContinueWith( + static result => + { + if (result.IsFaulted) + { + throw result.Exception.InnerException; + } + + return result.Result; + }, + CancellationToken.None, + TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.NotOnCanceled, + TaskScheduler.Default); + } + + private IAsyncResult BeginExecuteReaderInternal( + CommandBehavior behavior, + AsyncCallback callback, + object stateObject, + int timeout, + bool isRetry, + bool asyncWrite = false) + { + TaskCompletionSource globalCompletion = new TaskCompletionSource(stateObject); + TaskCompletionSource localCompletion = new TaskCompletionSource(stateObject); + + if (!isRetry) + { + // Reset _pendingCancel upon entry into any Execute - used to synchronize state + // between entry into Execute* API and the thread obtaining the stateObject. + _pendingCancel = false; + } + + SqlStatistics statistics = null; + try + { + if (!isRetry) + { + statistics = SqlStatistics.StartTimer(Statistics); + WriteBeginExecuteEvent(); + + // Special case - done outside of try/catches to prevent putting a stateObj + // back into pool when we should not. + ValidateAsyncCommand(); + } + + bool usedCache = false; + Task writeTask = null; + try + { + // InternalExecuteNonQuery already has reliability block, but if failure will + // not put stateObj back into pool. + RunExecuteReader( + behavior, + RunBehavior.ReturnImmediately, + returnStream: true, + localCompletion, + timeout, + out writeTask, + out usedCache, + asyncWrite, + isRetry, + nameof(BeginExecuteReader)); + } + catch (Exception e) + { + if (!ADP.IsCatchableOrSecurityExceptionType(e)) + { + // If not catchable - the connection has already been caught and doomed in + // RunExecuteReader. + throw; + } + + // For async, RunExecuteReader will never put the stateObj back into the pool, + // so, do so now. + ReliablePutStateObject(); + if (isRetry || e is not EnclaveDelegate.RetryableEnclaveQueryExecutionException) + { + throw; + } + } + + if (writeTask is not null) + { + AsyncHelper.ContinueTaskWithState( + writeTask, + localCompletion, + state: Tuple.Create(this, localCompletion), + onSuccess: static state => + { + var parameters = (Tuple>)state; + parameters.Item1.BeginExecuteReaderInternalReadStage(parameters.Item2); + }); + } + else + { + BeginExecuteReaderInternalReadStage(localCompletion); + } + + // When we use query caching for parameter encryption we need to retry on specific errors. + // In these cases finalize the call internally and trigger a retry when needed. + // @TODO: This is way too big to be done as an if statement. + if ( + !TriggerInternalEndAndRetryIfNecessary( + behavior, + stateObject, + timeout, + usedCache, + isRetry, + asyncWrite, + globalCompletion, + localCompletion, + endFunc: static (SqlCommand command, IAsyncResult asyncResult, bool isInternal, string endMethod) => + { + return command.InternalEndExecuteReader(asyncResult, isInternal, endMethod); + }, + retryFunc: static (SqlCommand command, CommandBehavior behavior, AsyncCallback callback, object stateObject, int timeout, bool isRetry, bool asyncWrite) => + { + return command.BeginExecuteReaderInternal(behavior, callback, stateObject, timeout, isRetry, asyncWrite); + }, + nameof(EndExecuteReader))) + { + globalCompletion = localCompletion; + } + + // Add callback after work is done to avoid overlapping Begin/End methods + if (callback is not null) + { + globalCompletion.Task.ContinueWith( + static (task, state) => ((AsyncCallback)state)(task), + state: callback); + } + + return globalCompletion.Task; + } + finally + { + SqlStatistics.StopTimer(statistics); + } + } + + private void BeginExecuteReaderInternalReadStage(TaskCompletionSource completion) + { + Debug.Assert(completion is not null, "CompletionSource should not be null"); + + SqlClientEventSource.Log.TryCorrelationTraceEvent( + "SqlCommand.BeginExecuteReaderInternalReadStage | INFO | Correlation | " + + $"Object Id {ObjectID}, " + + $"Activity Id {ActivityCorrelator.Current}, " + + $"Client Connection Id {_activeConnection?.ClientConnectionId}, " + + $"Command Text '{CommandText}'"); + + // Read SNI does not have catches for async exceptions, handle here. + try + { + // Must finish caching information before ReadSni which can activate the callback + // before returning + CachedAsyncState.SetActiveConnectionAndResult(completion, nameof(EndExecuteReader), _activeConnection); + _stateObj.ReadSni(completion); + } + // @TODO: CER Exception Handling was removed here (see GH#3581) + catch (Exception e) + { + // Similarly, if an exception occurs put the stateObj back into the pool. + // and reset async cache information to allow a second async execute + CachedAsyncState?.ResetAsyncState(); + ReliablePutStateObject(); + completion.TrySetException(e); + } + } + + /// + /// Build the RPC record header for sp_execute. + /// + /// + /// Prototype for sp_execute is: + /// sp_execute(@handle int, param1value, param2value...) + /// + private _SqlRPC BuildExecute(bool inSchema) + { + Debug.Assert(_prepareHandle != s_cachedInvalidPrepareHandle, "Invalid call to sp_execute without a valid handle!"); + + const int systemParameterCount = 1; + int userParameterCount = CountSendableParameters(_parameters); + + _SqlRPC rpc = null; + GetRPCObject(systemParameterCount, userParameterCount, ref rpc); + rpc.ProcID = TdsEnums.RPC_PROCID_EXECUTE; + rpc.rpcName = TdsEnums.SP_EXECUTE; + + // @handle + SqlParameter sqlParam = rpc.systemParams[0]; + sqlParam.SqlDbType = SqlDbType.Int; + sqlParam.Size = 4; + sqlParam.Value = _prepareHandle; + sqlParam.Direction = ParameterDirection.Input; + + SetUpRPCParameters(rpc, inSchema, _parameters); + return rpc; + } + + /// + /// Build the RPC record header for sp_executesql and add the parameters. + /// + /// + /// Prototype for sp_executesql is: + /// sp_executesql(@batch_text nvarchar(4000), @batch_params nvarchar(4000), param1, param2, ...) + /// + // @TODO Does parameters need to be passed in or can _parameters be used? + // @TODO: Can we return the RPC here like BuildExecute does? + private void BuildExecuteSql( + CommandBehavior behavior, + string commandText, + SqlParameterCollection parameters, + ref _SqlRPC rpc) + { + Debug.Assert(_prepareHandle == s_cachedInvalidPrepareHandle, "This command has an existing handle, use sp_execute!"); + Debug.Assert(CommandType is CommandType.Text, "invalid use of sp_executesql for stored proc invocation!"); + + int userParamCount = CountSendableParameters(parameters); + int systemParamCount = userParamCount > 0 ? 2 : 1; + + GetRPCObject(systemParamCount, userParamCount, ref rpc); + rpc.ProcID = TdsEnums.RPC_PROCID_EXECUTESQL; + rpc.rpcName = TdsEnums.SP_EXECUTESQL; + + SqlParameter sqlParam; + + // @batch_text + commandText ??= GetCommandText(behavior); + sqlParam = rpc.systemParams[0]; + sqlParam.SqlDbType = (commandText.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT + ? SqlDbType.NVarChar + : SqlDbType.NText; + sqlParam.Size = commandText.Length; + sqlParam.Value = commandText; + sqlParam.Direction = ParameterDirection.Input; + + // @batch_params + if (userParamCount > 0) + { + // @TODO: Why does batch RPC mode use different parameters? + string paramList = BuildParamList(_stateObj.Parser, _batchRPCMode ? parameters : _parameters); + sqlParam = rpc.systemParams[1]; + sqlParam.SqlDbType = (paramList.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT + ? SqlDbType.NVarChar + : SqlDbType.NText; + sqlParam.Size = paramList.Length; + sqlParam.Value = paramList; + sqlParam.Direction = ParameterDirection.Input; + + // @TODO: This is passed into BuildRPC ... should we do that or vice versa? + bool inSchema = (behavior & CommandBehavior.SchemaOnly) != 0; + SetUpRPCParameters(rpc, inSchema, parameters); + } + } + + private _SqlRPC BuildPrepExec(CommandBehavior behavior) + { + Debug.Assert(CommandType is CommandType.Text, "invalid use of sp_prepexec for stored proc invocation!"); + + const int systemParameterCount = 3; + int userParameterCount = CountSendableParameters(_parameters); + + _SqlRPC rpc = null; + GetRPCObject(systemParameterCount, userParameterCount, ref rpc); + rpc.ProcID = TdsEnums.RPC_PROCID_PREPEXEC; + rpc.rpcName = TdsEnums.SP_PREPEXEC; + + SqlParameter sqlParam; + + // @handle + sqlParam = rpc.systemParams[0]; + sqlParam.SqlDbType = SqlDbType.Int; + sqlParam.Value = _prepareHandle; + sqlParam.Size = 4; + sqlParam.Direction = ParameterDirection.InputOutput; + rpc.systemParamOptions[0] = TdsEnums.RPC_PARAM_BYREF; + + // @batch_params + string paramList = BuildParamList(_stateObj.Parser, _parameters); + sqlParam = rpc.systemParams[1]; + // @TODO: This pattern is used quite a bit - it could be factored out + sqlParam.SqlDbType = (paramList.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT + ? SqlDbType.NVarChar + : SqlDbType.NText; + sqlParam.Value = paramList; + sqlParam.Size = paramList.Length; + sqlParam.Direction = ParameterDirection.Input; + + // @batch_text + string text = GetCommandText(behavior); + sqlParam = rpc.systemParams[2]; + sqlParam.SqlDbType = (text.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT + ? SqlDbType.NVarChar + : SqlDbType.NText; + sqlParam.Size = text.Length; + sqlParam.Value = text; + sqlParam.Direction = ParameterDirection.Input; + + SetUpRPCParameters(rpc, inSchema: false, _parameters); + return rpc; + } + + /// + /// Build the RPC record header for this stored proc and add parameters. + /// + // @TODO: Rename to fit guidelines + // @TODO: Does parameters need to be passed in or can _parameters be used? + // @TODO: Can we return the RPC here like BuildExecute does? + private void BuildRPC(bool inSchema, SqlParameterCollection parameters, ref _SqlRPC rpc) + { + Debug.Assert(CommandType is CommandType.StoredProcedure, "Command must be a stored proc to execute an RPC"); + + int userParameterCount = CountSendableParameters(parameters); + GetRPCObject(systemParamCount: 0, userParameterCount, ref rpc); + rpc.ProcID = 0; + + // TDS Protocol allows rpc name with maximum length of 1046 bytes for ProcName + // 4-part name 1 + 128 + 1 + 1 + 1 + 128 + 1 + 1 + 1 + 128 + 1 + 1 + 1 + 128 + 1 = 523 + // each char takes 2 bytes. 523 * 2 = 1046 + int commandTextLength = ADP.CharSize * CommandText.Length; + if (commandTextLength <= MaxRPCNameLength) + { + // Just use the raw command text + rpc.rpcName = CommandText; + } + else + { + throw ADP.InvalidArgumentLength(nameof(CommandText), MaxRPCNameLength); + } + + SetUpRPCParameters(rpc, inSchema, parameters); + } + + private void CleanupExecuteReaderAsync( + Task task, + TaskCompletionSource source, + Guid operationId) + { + if (task.IsFaulted) + { + Exception e = task.Exception.InnerException; + + #if NET + if (!_parentOperationStarted) + { + s_diagnosticListener.WriteCommandError(operationId, this, _transaction, e); + } + #endif + + source.SetException(e); + } + else + { + #if NET + if (!_parentOperationStarted) + { + s_diagnosticListener.WriteCommandAfter(operationId, this, _transaction); + } + #endif + + if (task.IsCanceled) + { + source.SetCanceled(); + } + else + { + source.SetResult(task.Result); + } + } + } + + private SqlDataReader CompleteAsyncExecuteReader(bool isInternal, bool forDescribeParameterEncryption) + { + SqlDataReader reader = CachedAsyncState.CachedAsyncReader; + Debug.Assert(reader is not null); + + bool processFinallyBlock = true; + try + { + // @TODO: Evaluate if forDescribeParameterEncryption/shouldCacheForAlwaysEncrypted are always opposites + FinishExecuteReader( + reader, + CachedAsyncState.CachedRunBehavior, + CachedAsyncState.CachedSetOptions, + isInternal, + forDescribeParameterEncryption, + shouldCacheForAlwaysEncrypted: !forDescribeParameterEncryption); + return reader; + } + catch (Exception e) + { + processFinallyBlock = ADP.IsCatchableExceptionType(e); + throw; + } + finally + { + if (processFinallyBlock) + { + // Don't reset the state for internal End. The user End will do that eventually. + if (!isInternal) + { + CachedAsyncState.ResetAsyncState(); + } + + PutStateObject(); + } + } + } + + private SqlDataReader EndExecuteReaderAsync(IAsyncResult asyncResult) + { + Debug.Assert(!_internalEndExecuteInitiated || _stateObj is null); + + SqlClientEventSource.Log.TryCorrelationTraceEvent( + "SqlCommand.EndExecuteReaderAsync | API | Correlation | " + + $"Object Id {ObjectID}, " + + $"Activity Id {ActivityCorrelator.Current}, " + + $"Client Connection Id {_activeConnection?.ClientConnectionId}, " + + $"Command Text '{CommandText}'"); + + Exception asyncException = ((Task)asyncResult).Exception; + if (asyncException is not null) + { + CachedAsyncState?.ResetAsyncState(); + ReliablePutStateObject(); + + throw asyncException.InnerException; + } + + ThrowIfReconnectionHasBeenCanceled(); + + // Lock on _stateObj prevents race with close/cancel + if (!_internalEndExecuteInitiated) + { + lock (_stateObj) + { + return EndExecuteReaderInternal(asyncResult); + } + } + + return EndExecuteReaderInternal(asyncResult); + } + + private SqlDataReader EndExecuteReaderInternal(IAsyncResult asyncResult) + { + SqlClientEventSource.Log.TryTraceEvent( + "SqlCommand.EndExecuteReaderInternal | API | " + + $"Object Id {ObjectID}, " + + $"Client Connection Id {_activeConnection?.ClientConnectionId}, " + + $"MARS={_activeConnection?.Parser?.MARSOn}, " + + $"AsyncCommandInProgress={_activeConnection?.AsyncCommandInProgress}"); + + SqlStatistics statistics = null; + bool success = false; + int? sqlExceptionNumber = null; + try + { + statistics = SqlStatistics.StartTimer(Statistics); + + SqlDataReader result = InternalEndExecuteReader( + asyncResult, + isInternal: false, + nameof(EndExecuteReader)); + + success = true; + return result; + } + catch (Exception e) + { + if (e is SqlException sqlException) + { + sqlExceptionNumber = sqlException.Number; + } + + if (CachedAsyncState is not null) + { + CachedAsyncState.ResetAsyncState(); + } + + if (ADP.IsCatchableExceptionType(e)) + { + ReliablePutStateObject(); + } + + throw; + } + finally + { + SqlStatistics.StopTimer(statistics); + WriteEndExecuteEvent(success, sqlExceptionNumber, synchronous: false); + } + } + + private void FinishExecuteReader( + SqlDataReader ds, + RunBehavior runBehavior, + string resetOptionsString, + bool isInternal, + bool forDescribeParameterEncryption, + bool shouldCacheForAlwaysEncrypted = true) + { + // If this is not for internal usage, notify the dependency. If we have already + // initiated the end internally, the reader should be ready, so just return. + if (!isInternal && !forDescribeParameterEncryption) + { + NotifyDependency(); + + if (_internalEndExecuteInitiated) + { + Debug.Assert(_stateObj is null); + return; + } + } + + if (runBehavior is RunBehavior.UntilDone) + { + try + { + Debug.Assert(_stateObj._syncOverAsync, "Should not attempt pends in a synchronous call"); + TdsOperationStatus result = _stateObj.Parser.TryRun( + RunBehavior.UntilDone, + cmdHandler: this, + ds, + bulkCopyHandler: null, + _stateObj, + out _); + + if (result is not TdsOperationStatus.Done) + { + throw SQL.SynchronousCallMayNotPend(); + } + } + catch (Exception e) + { + if (ADP.IsCatchableExceptionType(e)) + { + if (_inPrepare) + { + // The flag is expected to be reset by OnReturnValue. We should receive + // the handle unless command execution failed. If it fails, move back + // to pending state. + _inPrepare = false; // reset the flag + IsDirty = true; // mark command as dirty so it will be + // prepared next time we're coming through + _execType = EXECTYPE.PREPAREPENDING; // reset execution type to pending + } + } + + if (ds is not null) + { + try + { + ds.Close(); + } + catch (Exception eClose) + { + Debug.WriteLine($"Received this exception from SqlDataReader.Close() while in another catch block: {eClose}"); + } + } + + throw; + } + } + + // Bind the parser to the reader if we get this far + if (ds is not null) + { + ds.Bind(_stateObj); + _stateObj = null; // The reader now owns this... + ds.ResetOptionsString = resetOptionsString; + + // Bind the reader to this connection now + _activeConnection.AddWeakReference(ds, SqlReferenceCollection.DataReaderTag); + + // Force this command to start reading data off the wire. + // This will cause an error to be reported at Execute() time instead of Read() time + // if the command is not set + try + { + // This flag indicates if the data reader's metadata should be cached in this + // SqlCommand. Metadata associated with sp_describe_parameter_metadata's data + // reader should not be cached. Ideally, we should be using + // "forDescribeParameterEncryption" flag for this, but this flag's semantics + // are overloaded with async workflow and this flag is always false for sync + // workflow. Since we are very close to a release and changing the semantics + // for "forDescribeParameterEncryption" is risky, we introduced a new parameter + // to determine whether we should cache a data reader's metadata or not. + if (shouldCacheForAlwaysEncrypted) + { + _cachedMetaData = ds.MetaData; + } + else + { + // We need this call to ensure the data reader is properly initialized, the + // getter is initializing state in SqlDataReader. + _ = ds.MetaData; + } + + // @TODO: Why does the command set whether the reader is initialized?? + ds.IsInitialized = true; + } + catch (Exception e) + { + if (ADP.IsCatchableExceptionType(e)) + { + if (_inPrepare) + { + // The flag is expected to be reset by OnReturnValue. We should receive + // the handle unless command execution failed. If it fails, move back + // to pending state. + _inPrepare = false; // reset the flag + IsDirty = true; // mark command as dirty so it will be prepared next time we're coming through + _execType = EXECTYPE.PREPAREPENDING; // reset execution type to pending + } + + try + { + ds.Close(); + } + catch (Exception eClose) + { + Debug.WriteLine($"Received this exception from SqlDataReader.Close() while in another catch block: {eClose}"); + } + } + + throw; + } + } + } + + private void GenerateEnclavePackage() + { + // Skip processing if there are no keys to send to enclave + if (keysToBeSentToEnclave is null || keysToBeSentToEnclave.IsEmpty) + { + return; + } + + // Validate attestation url is provided when necessary + if (string.IsNullOrWhiteSpace(_activeConnection.EnclaveAttestationUrl) && + _activeConnection.AttestationProtocol is not SqlConnectionAttestationProtocol.None) + { + throw SQL.NoAttestationUrlSpecifiedForEnclaveBasedQueryGeneratingEnclavePackage( + _activeConnection.Parser.EnclaveType); + } + + // Validate enclave type + string enclaveType = _activeConnection.Parser.EnclaveType; + if (string.IsNullOrWhiteSpace(enclaveType)) + { + throw SQL.EnclaveTypeNullForEnclaveBasedQuery(); + } + + // Validate protocol type + SqlConnectionAttestationProtocol attestationProtocol = _activeConnection.AttestationProtocol; + if (attestationProtocol is SqlConnectionAttestationProtocol.NotSpecified) + { + throw SQL.AttestationProtocolNotSpecifiedForGeneratingEnclavePackage(); + } + + // Generate the enclave package + try + { + #if DEBUG + // @TODO: These should be wrapped with something other than DEBUG since we don't even run tests in debug mode + // Test-only code for forcing a retryable exception to occur + if (_forceRetryableEnclaveQueryExecutionExceptionDuringGenerateEnclavePackage) + { + _forceRetryableEnclaveQueryExecutionExceptionDuringGenerateEnclavePackage = false; + throw new EnclaveDelegate.RetryableEnclaveQueryExecutionException("testing", null); + } + #endif + + enclavePackage = EnclaveDelegate.Instance.GenerateEnclavePackage( + attestationProtocol, + keysToBeSentToEnclave, + CommandText, + enclaveType, + GetEnclaveSessionParameters(), + _activeConnection, + command: this); + } + catch (EnclaveDelegate.RetryableEnclaveQueryExecutionException) + { + throw; + } + catch (Exception e) + { + throw SQL.ExceptionWhenGeneratingEnclavePackage(e); + } + } + + private Task InternalExecuteReaderAsync( + CommandBehavior commandBehavior, + CancellationToken cancellationToken) + { + SqlClientEventSource.Log.TryCorrelationTraceEvent( + "SqlCommand.InternalExecuteReaderAsync | API | Correlation | " + + $"Object Id {ObjectID}, " + + $"Behavior {(int)commandBehavior}, " + + $"Activity Id {ActivityCorrelator.Current}, " + + $"Client Connection Id {_activeConnection?.ClientConnectionId}, " + + $"Command Text '{CommandText}'"); + SqlClientEventSource.Log.TryTraceEvent( + "SqlCommand.InternalExecuteReaderAsync | INFO | " + + $"Object Id {ObjectID}, " + + $"Client Connection Id {_activeConnection?.ClientConnectionId}, " + + $"Command Text '{CommandText}'"); + + Guid operationId = Guid.Empty; + #if NET + if (!_parentOperationStarted) + { + operationId = s_diagnosticListener.WriteCommandBefore(this, _transaction); + } + #endif + + // Connection can be used as state in RegisterForConnectionCloseNotification + // continuation to avoid an allocation so use it as the state value if possible, but it + // can be changed if you need it for a more important piece of data that justifies the + // tuple allocation later. + TaskCompletionSource source = new TaskCompletionSource(_activeConnection); + + CancellationTokenRegistration registration = new CancellationTokenRegistration(); + if (cancellationToken.CanBeCanceled) + { + if (cancellationToken.IsCancellationRequested) + { + source.SetCanceled(); + return source.Task; + } + + registration = cancellationToken.Register(s_cancelIgnoreFailure, state: this); + } + + Task returnedTask = source.Task; + ExecuteReaderAsyncCallContext context = null; + try + { + returnedTask = RegisterForConnectionCloseNotification(returnedTask); + + if (_activeConnection?.InnerConnection is SqlInternalConnection sqlInternalConnection) + { + context = Interlocked.Exchange( + ref sqlInternalConnection.CachedCommandExecuteReaderAsyncContext, + null); + } + + context ??= new ExecuteReaderAsyncCallContext(); + context.Set(this, source, registration, commandBehavior, operationId); + + Task.Factory.FromAsync( + beginMethod: static (callback, state) => + { + ExecuteReaderAsyncCallContext args = (ExecuteReaderAsyncCallContext)state; + return args.Command.BeginExecuteReaderInternal( + args.CommandBehavior, + callback, + state, + args.Command.CommandTimeout, + isRetry: false, // @TODO: Wait, this *is* a retry if the we're on the retry part of the reliability helper! + asyncWrite: true); + }, + endMethod: static asyncResult => + { + ExecuteReaderAsyncCallContext args = (ExecuteReaderAsyncCallContext)asyncResult.AsyncState; + return args.Command.EndExecuteReaderAsync(asyncResult); + }, + state: context + ).ContinueWith( + static task => + { + ExecuteReaderAsyncCallContext context = (ExecuteReaderAsyncCallContext)task.AsyncState; + SqlCommand command = context.Command; + Guid operationId = context.OperationId; + TaskCompletionSource source = context.TaskCompletionSource; + + context.Dispose(); + command.CleanupExecuteReaderAsync(task, source, operationId); + }, + scheduler: TaskScheduler.Default); + } + catch (Exception e) + { + #if NET + if (!_parentOperationStarted) + { + s_diagnosticListener.WriteCommandError(operationId, this, _transaction, e); + } + #endif + + source.SetException(e); + context?.Dispose(); + } + + return returnedTask; + } + + private Task InternalExecuteReaderWithRetryAsync( + CommandBehavior commandBehavior, + CancellationToken cancellationToken) + { + return RetryLogicProvider.ExecuteAsync( + sender: this, + () => InternalExecuteReaderAsync(commandBehavior, cancellationToken), + cancellationToken); + } + + private SqlDataReader InternalEndExecuteReader(IAsyncResult asyncResult, bool isInternal, string endMethod) + { + SqlClientEventSource.Log.TryTraceEvent( + "SqlCommand.InternalEndExecuteReader | INFO | " + + $"Object Id {ObjectID}, " + + $"Client Connection Id {_activeConnection?.ClientConnectionId}, " + + $"MARS={_activeConnection?.Parser?.MARSOn}, " + + $"AsyncCommandInProgress={_activeConnection?.AsyncCommandInProgress}"); + + VerifyEndExecuteState((Task)asyncResult, endMethod); + WaitForAsyncResults(asyncResult, isInternal); + + // If column encryption is enabled, also check the state after waiting for the task. + // It would be better to do this for all cases, but avoiding for compatibility reasons. + if (IsColumnEncryptionEnabled) + { + VerifyEndExecuteState((Task)asyncResult, endMethod, fullCheckForColumnEncryption: true); + } + + CheckThrowSNIException(); + + SqlDataReader reader = CompleteAsyncExecuteReader(isInternal, forDescribeParameterEncryption: false); + Debug.Assert(_stateObj is null, "non-null state object in InternalEndExecuteReader"); + return reader; + // @TODO: CER Exception Handling was removed here (see GH#3581) + } + + // @TODO: We're passing way too many arguments around here... can we simplify this some? + // task is created in case of pending asynchronous write, returned SqlDataReader should not be utilized until that task is complete + private SqlDataReader RunExecuteReader( + CommandBehavior cmdBehavior, + RunBehavior runBehavior, + bool returnStream, + TaskCompletionSource completion, + int timeout, + out Task executeTask, + out bool usedCache, // @TODO: This can be eliminated if we do not retry via recursion + bool asyncWrite = false, + bool isRetry = false, + [CallerMemberName] string method = "") + { + bool isAsync = completion is not null; + + usedCache = false; + executeTask = null; + + _rowsAffected = -1; + _rowsAffectedBySpDescribeParameterEncryption = -1; + + if ((cmdBehavior & CommandBehavior.SingleRow) != 0) + { + // CommandBehavior.SingleRow implies CommandBehavior.SingleResult + cmdBehavior |= CommandBehavior.SingleResult; + } + + // This function may throw for an invalid connection + if (!isRetry) + { + ValidateCommand(isAsync, method); + } + + // Only call after validate - requires non-null connection! + CheckNotificationStateAndAutoEnlist(); + + SqlStatistics statistics = Statistics; + if (statistics is not null) + { + if ((!IsDirty && IsPrepared && !_hiddenPrepare) || + (IsPrepared && _execType == EXECTYPE.PREPAREPENDING)) + { + statistics.SafeIncrement(ref statistics._preparedExecs); + } + else + { + statistics.SafeIncrement(ref statistics._unpreparedExecs); + } + } + + // Reset the encryption related state of the command and its parameters. + ResetEncryptionState(); + + if (IsColumnEncryptionEnabled) + { + // @TODO: Make this a separate method + + PrepareForTransparentEncryption( + isAsync, + timeout, + completion, + out Task prepareEncryptionTask, + asyncWrite: asyncWrite && isAsync, + out usedCache, + isRetry); + + long firstAttemptStart = ADP.TimerCurrent(); + try + { + return RunExecuteReaderTdsWithTransparentParameterEncryption( + cmdBehavior, + runBehavior, + returnStream, + isAsync, + timeout, + out executeTask, + asyncWrite: asyncWrite && isAsync, + isRetry, + ds: null, + describeParameterEncryptionTask: prepareEncryptionTask); + } + catch (EnclaveDelegate.RetryableEnclaveQueryExecutionException) + { + if (isRetry) + { + // Do not retry after the second attempt. + throw; + } + + // @TODO: This same pattern is used below. Can we do this without a recursive call? + // Retry if the command failed with an appropriate error. + // First invalidate the entry from the cache, so that we refresh our encryption + // metadata. + SqlQueryMetadataCache.GetInstance().InvalidateCacheEntry(this); + InvalidateEnclaveSession(); + + return RunExecuteReader( + cmdBehavior, + runBehavior, + returnStream, + completion, + TdsParserStaticMethods.GetRemainingTimeout(timeout, firstAttemptStart), + out executeTask, + out usedCache, + isAsync, + isRetry: true, + method); + } + catch (SqlException ex) + { + // We only want to retry once, so don't retry if we are already in retry. + // If we didn't use the cache, we don't want to retry + if (isRetry || (!usedCache && !ShouldUseEnclaveBasedWorkflow)) + { + throw; + } + + // Check if we have an error indicating that we can retry. + bool shouldRetry = false; + foreach (SqlError error in ex.Errors) + { + if ((usedCache && error.Number == TdsEnums.TCE_CONVERSION_ERROR_CLIENT_RETRY) || + (ShouldUseEnclaveBasedWorkflow && error.Number == TdsEnums.TCE_ENCLAVE_INVALID_SESSION_HANDLE)) + { + shouldRetry = true; + break; + } + } + + if (!shouldRetry) + { + throw; + } + + // Retry if the command failed with an appropriate error. + // First invalidate the entry from the cache, so that we refresh our encryption + // metadata. + SqlQueryMetadataCache.GetInstance().InvalidateCacheEntry(this); + InvalidateEnclaveSession(); + + return RunExecuteReader( + cmdBehavior, + runBehavior, + returnStream, + completion, + TdsParserStaticMethods.GetRemainingTimeout(timeout, firstAttemptStart), + out executeTask, + out usedCache, + asyncWrite: isAsync, + isRetry: true, + method); + } + } + else + { + // @TODO: Reminder, this is where we execute if transparent parameter encryption is not needed. + return RunExecuteReaderTds( + cmdBehavior, + runBehavior, + returnStream, + isAsync, + timeout, + out executeTask, + asyncWrite: asyncWrite && isAsync, + isRetry); + } + // @TODO: CER Exception Handling was removed here (see GH#3581) + } + + // @TODO: This method *needs* to be broken up into separate ones for each type of execution + private SqlDataReader RunExecuteReaderTds( + CommandBehavior cmdBehavior, + RunBehavior runBehavior, + bool returnStream, + bool isAsync, + int timeout, + out Task task, + bool asyncWrite, + bool isRetry, // @TODO: This isn't used in netfx? + SqlDataReader ds = null, + bool describeParameterEncryptionRequest = false) // @TODO: This is hidden by an overload + { + Debug.Assert(!asyncWrite || isAsync, "AsyncWrite should be always accompanied by Async"); + + if (ds == null && returnStream) + { + ds = new SqlDataReader(this, cmdBehavior); + } + + Task reconnectTask = _activeConnection.ValidateAndReconnect(beforeDisconnect: null, timeout); + if (reconnectTask is not null) + { + long reconnectionStart = ADP.TimerCurrent(); + if (isAsync) + { + TaskCompletionSource completion = new TaskCompletionSource(); + _activeConnection.RegisterWaitingForReconnect(completion.Task); + _reconnectionCompletionSource = completion; + + // Sets up a recursive call + RunExecuteReaderTdsSetupReconnectContinuation( + cmdBehavior, + runBehavior, + returnStream, + isAsync, + timeout, + asyncWrite, + isRetry, + ds, + reconnectTask, + reconnectionStart, + completion); + + task = completion.Task; + return ds; + } + else + { + AsyncHelper.WaitForCompletion( + reconnectTask, + timeout, + onTimeout: static () => throw SQL.CR_ReconnectTimeout()); + timeout = TdsParserStaticMethods.GetRemainingTimeout(timeout, reconnectionStart); + } + } + + // Make sure we have good parameter information + Debug.Assert(_activeConnection.Parser is not null, "TdsParser class should not be null in Command.Execute!"); + + bool inSchema = (cmdBehavior & CommandBehavior.SchemaOnly) != 0; + + // Create a new RPC + _SqlRPC rpc = null; + + task = null; + string optionSettings = null; + bool processFinallyBlock = true; + bool decrementAsyncCountOnFailure = false; + + if (isAsync) + { + _activeConnection.GetOpenTdsConnection().IncrementAsyncCount(); + decrementAsyncCountOnFailure = true; + } + + try + { + if (asyncWrite) + { + _activeConnection.AddWeakReference(this, SqlReferenceCollection.CommandTag); + } + + GetStateObject(); + Task writeTask; + + if (describeParameterEncryptionRequest) + { + // @TODO: Execute as encrypted RPC + #if DEBUG + if (_sleepDuringRunExecuteReaderTdsForSpDescribeParameterEncryption) + { + Thread.Sleep(10000); + } + #endif + + Debug.Assert(_sqlRPCParameterEncryptionReqArray is not null, "RunExecuteReader rpc array not provided for describe parameter encryption request."); + writeTask = _stateObj.Parser.TdsExecuteRPC( + this, + _sqlRPCParameterEncryptionReqArray, + timeout, + inSchema, + Notification, + _stateObj, + CommandType is CommandType.StoredProcedure, + sync: !asyncWrite); + } + else if (_batchRPCMode) + { + // @TODO: Execute as batch RPC + Debug.Assert(inSchema == false, "Batch RPC does not support schema only command behavior"); + Debug.Assert(!IsPrepared, "Batch RPC should not be prepared!"); + Debug.Assert(!IsDirty, "Batch RPC should not be marked as dirty!"); + Debug.Assert(_RPCList != null, "RunExecuteReader rpc array not provided"); + writeTask = _stateObj.Parser.TdsExecuteRPC( + this, + _RPCList, + timeout, + inSchema, + Notification, + _stateObj, + CommandType is CommandType.StoredProcedure, + sync: !asyncWrite); + } + else if (CommandType is CommandType.Text && GetParameterCount(_parameters) == 0) + { + // @TODO: Execute as batch (or text without 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!"); + + if (returnStream) + { + SqlClientEventSource.Log.TryTraceEvent( + "SqlCommand.RunExecuteReaderTds | Info | " + + $"Object Id {ObjectID}, " + + $"Activity Id {ActivityCorrelator.Current}, " + + $"Client Connection Id {_activeConnection?.ClientConnectionId}, " + + $"Command executed as SQLBATCH, " + + $"Command Text '{CommandText}'"); + } + + string text = GetCommandText(cmdBehavior) + GetResetOptionsString(cmdBehavior); + + // If the query requires enclave computations, pass the enclave package in the + // SqlBatch TDS stream + if (requiresEnclaveComputations) + { + if (enclavePackage is null) + { + throw SQL.NullEnclavePackageForEnclaveBasedQuery( + _activeConnection?.Parser.EnclaveType, + _activeConnection?.EnclaveAttestationUrl); + } + + writeTask = _stateObj.Parser.TdsExecuteSQLBatch( + text, + timeout, + Notification, + _stateObj, + sync: !asyncWrite, + enclavePackage: enclavePackage.EnclavePackageBytes); + } + else + { + writeTask = _stateObj.Parser.TdsExecuteSQLBatch( + text, + timeout, + Notification, + _stateObj, + sync: !asyncWrite, + enclavePackage: null); + } + } + else if (CommandType is CommandType.Text) + { + // @TODO: Execute as RPC (or text with parameters) + if (IsDirty) + { + // Can have cached metadata if dirty because of parameters + Debug.Assert(_cachedMetaData == null || !_dirty, "dirty query should not have cached metadata!"); + + // Someone changed the command text or the parameter schema so we must + // unprepare the command + // remember that IsDirty includes test for IsPrepared! @TODO: Very confusing! + if (_execType is EXECTYPE.PREPARED) + { + _hiddenPrepare = true; + } + + Unprepare(); + IsDirty = false; + } + + if (_execType is EXECTYPE.PREPARED) + { + Debug.Assert(IsPrepared && _prepareHandle != s_cachedInvalidPrepareHandle, "invalid attempt to call sp_execute without a handle!"); + rpc = BuildExecute(inSchema); + } + else if (_execType is EXECTYPE.PREPAREPENDING) + { + rpc = BuildPrepExec(cmdBehavior); + + // Next time through, only do an exec + _execType = EXECTYPE.PREPARED; + _preparedConnectionCloseCount = _activeConnection.CloseCount; + _preparedConnectionReconnectCount = _activeConnection.ReconnectCount; + + // Mark ourselves as preparing the command + _inPrepare = true; + } + else + { + Debug.Assert(_execType is EXECTYPE.UNPREPARED, "Invalid execType!"); + BuildExecuteSql(cmdBehavior, commandText: null, _parameters, ref rpc); + } + + rpc.options = TdsEnums.RPC_NOMETADATA; + if (returnStream) + { + SqlClientEventSource.Log.TryTraceEvent( + "SqlCommand.RunExecuteReaderTds | Info | " + + $"Object Id {ObjectID}, " + + $"Activity Id {ActivityCorrelator.Current}, " + + $"Client Connection Id {_activeConnection?.ClientConnectionId}, " + + $"Command executed as RPC, " + + $"RPC Name '{rpc.rpcName}'"); + } + + Debug.Assert(_rpcArrayOf1[0] == rpc); + writeTask = _stateObj.Parser.TdsExecuteRPC( + this, + _rpcArrayOf1, + timeout, + inSchema, + Notification, + _stateObj, + CommandType is CommandType.StoredProcedure, // @TODO: uhhhhh this shouldn't ever be true? + sync: !asyncWrite); + } + else + { + // @TODO: Execute as sproc + Debug.Assert(CommandType is CommandType.StoredProcedure, "unknown command type!"); + + BuildRPC(inSchema, _parameters, ref rpc); + + // If we need to augment the command because a user has changed the command + // behavior (e.g. FillSchema) then batch sql them over. This is inefficient (3 + // round trips) but the only way we can get metadata only from a stored proc. + optionSettings = GetSetOptionsString(cmdBehavior); + + if (returnStream) + { + SqlClientEventSource.Log.TryTraceEvent( + "SqlCommand.RunExecuteReaderTds | Info | " + + $"Object Id {ObjectID}, " + + $"Activity Id {ActivityCorrelator.Current}, " + + $"Client Connection Id {_activeConnection?.ClientConnectionId}, " + + $"Command executed as RPC, " + + $"RPC Name '{CommandText}'"); + } + + // Turn set options ON + if (optionSettings is not null) + { + Task executeTask = _stateObj.Parser.TdsExecuteSQLBatch( + optionSettings, + timeout, + Notification, + _stateObj, + sync: true); + + Debug.Assert(executeTask is null, "Shouldn't get a task when doing sync writes"); + Debug.Assert(_stateObj._syncOverAsync, "Should not attempt pends in a synchronous call"); + + TdsOperationStatus result = _stateObj.Parser.TryRun( + RunBehavior.UntilDone, + cmdHandler: this, + dataStream: null, + bulkCopyHandler: null, + _stateObj, + out bool _); + if (result is not TdsOperationStatus.Done) + { + throw SQL.SynchronousCallMayNotPend(); + } + + // And turn OFF when the ds exhausts the stream on Close() + optionSettings = GetResetOptionsString(cmdBehavior); + } + + // Execute sproc + Debug.Assert(_rpcArrayOf1[0] == rpc); + writeTask = _stateObj.Parser.TdsExecuteRPC( + this, + _rpcArrayOf1, + timeout, + inSchema, + Notification, + _stateObj, + isCommandProc: CommandType is CommandType.StoredProcedure, // @TODO: This should always be true... + sync: !asyncWrite); + } + + Debug.Assert(writeTask is null || isAsync, "Returned task in sync mode"); + + if (isAsync) + { + decrementAsyncCountOnFailure = false; + if (writeTask is not null) + { + task = RunExecuteReaderTdsSetupContinuation(runBehavior, ds, optionSettings, writeTask); + } + else + { + CachedAsyncState.SetAsyncReaderState(ds, runBehavior, optionSettings); + } + } + else + { + // Always execute - even if no reader! + FinishExecuteReader( + ds, + runBehavior, + optionSettings, + isInternal: false, + forDescribeParameterEncryption: false, + shouldCacheForAlwaysEncrypted: !describeParameterEncryptionRequest); + } + } + catch (Exception e) + { + processFinallyBlock = ADP.IsCatchableExceptionType(e); + + if (decrementAsyncCountOnFailure) + { + if (_activeConnection.InnerConnection is SqlInternalConnectionTds innerConnectionTds) + { + // It may be closed + innerConnectionTds.DecrementAsyncCount(); + } + } + + throw; + } + finally + { + if (processFinallyBlock && !isAsync) + { + // When executing async, we need to keep the _stateObj alive... + PutStateObject(); + } + } + + Debug.Assert(isAsync || _stateObj == null, "non-null state object in RunExecuteReader"); + return ds; + } + + private Task RunExecuteReaderTdsSetupContinuation( + RunBehavior runBehavior, + SqlDataReader ds, + string optionSettings, + Task writeTask) + { + // @TODO: Why use the state version if we can't make this a static helper? + return AsyncHelper.CreateContinuationTaskWithState( + task: writeTask, + state: _activeConnection, + onSuccess: state => + { + // This will throw if the connection is closed. + // @TODO: So... can we have something that specifically does that? + ((SqlConnection)state).GetOpenTdsConnection(); + CachedAsyncState.SetAsyncReaderState(ds, runBehavior, optionSettings); + }, + onFailure: static (exception, state) => + { + ((SqlConnection)state).GetOpenTdsConnection().DecrementAsyncCount(); + }); + } + + // @TODO: This is way too many parameters being shoveled back and forth. We can do better. + private void RunExecuteReaderTdsSetupReconnectContinuation( + CommandBehavior cmdBehavior, + RunBehavior runBehavior, + bool returnStream, + bool isAsync, + int timeout, + bool asyncWrite, + bool isRetry, + SqlDataReader ds, + Task reconnectTask, + long reconnectionStart, + TaskCompletionSource completion) // @TODO: I think this can be an untyped TCS. + { + CancellationTokenSource timeoutCts = new CancellationTokenSource(); + AsyncHelper.SetTimeoutException( + completion, + timeout, + onFailure: static () => SQL.CR_ReconnectTimeout(), + timeoutCts.Token); + + // @TODO: With an object to pass around we can use the state-based version + AsyncHelper.ContinueTask( + reconnectTask, + completion, + onSuccess: () => + { + if (completion.Task.IsCompleted) + { + return; + } + + Interlocked.CompareExchange(ref _reconnectionCompletionSource, null, completion); + timeoutCts.Cancel(); + + RunExecuteReaderTds( + cmdBehavior, + runBehavior, + returnStream, + isAsync, + TdsParserStaticMethods.GetRemainingTimeout(timeout, reconnectionStart), + out Task subTask, + asyncWrite, + isRetry, + ds); + + if (subTask is null) + { + completion.SetResult(null); + } + else + { + AsyncHelper.ContinueTaskWithState( + subTask, + completion, + state: completion, + onSuccess: static state => ((TaskCompletionSource)state).SetResult(null)); + } + }); + } + + private SqlDataReader RunExecuteReaderTdsWithTransparentParameterEncryption( + CommandBehavior cmdBehavior, + RunBehavior runBehavior, + bool returnStream, + bool isAsync, + int timeout, + out Task task, + bool asyncWrite, + bool isRetry, + SqlDataReader ds = null, + Task describeParameterEncryptionTask = null) // @TODO: This task should likely come from this method otherwise this is just setting up a continuation + { + Debug.Assert(!asyncWrite || isAsync, "AsyncWrite should be always accompanied by Async"); + + if (ds is null && returnStream) + { + ds = new SqlDataReader(command: this, cmdBehavior); + } + + if (describeParameterEncryptionTask is not null) + { + // @TODO: I guess this means async execution? Using tasks as the primary means of determining async vs sync is clunky. It would be better to have separate async vs sync pathways. + long parameterEncryptionStart = ADP.TimerCurrent(); + + // @TODO: This can totally be a non-generic TCS + // @TODO: This is a prime candidate for proper async-await execution + TaskCompletionSource completion = new TaskCompletionSource(); + AsyncHelper.ContinueTaskWithState( + task: describeParameterEncryptionTask, + completion: completion, + state: this, + onSuccess: state => + { + SqlCommand command = (SqlCommand)state; + command.GenerateEnclavePackage(); + command.RunExecuteReaderTds( + cmdBehavior, + runBehavior, + returnStream, + isAsync, + TdsParserStaticMethods.GetRemainingTimeout(timeout, parameterEncryptionStart), + out Task subTask, + asyncWrite, + isRetry, + ds); + + if (subTask is null) + { + // @TODO: Why would this ever be the case? We should structure this so that it doesn't need to be checked. + completion.SetResult(null); + } + else + { + AsyncHelper.ContinueTaskWithState( + task: subTask, + completion: completion, + state: completion, + onSuccess: static state => ((TaskCompletionSource)state).SetResult(null)); + } + }, + onFailure: static (exception, state) => + { + ((SqlCommand)state).CachedAsyncState?.ResetAsyncState(); + if (exception is not null) + { + throw exception; + } + }, + onCancellation: static state => + { + ((SqlCommand)state).CachedAsyncState?.ResetAsyncState(); + } + #if NETFRAMEWORK + , connectionToAbort: _activeConnection + #endif + ); + + task = completion.Task; + return ds; + } + else + { + // Synchronous execution + GenerateEnclavePackage(); + return RunExecuteReaderTds( + cmdBehavior, + runBehavior, + returnStream, + isAsync, + timeout, + out task, + asyncWrite, + isRetry, + ds); + } + } + + private SqlDataReader RunExecuteReaderWithRetry( + CommandBehavior cmdBehavior, + RunBehavior runBehavior, + bool returnStream, + [CallerMemberName] string method = "") => + RetryLogicProvider.Execute( + this, + () => RunExecuteReader(cmdBehavior, runBehavior, returnStream, method)); + + private void SetCachedCommandExecuteReaderAsyncContext(ExecuteReaderAsyncCallContext instance) + { + if (_activeConnection?.InnerConnection is SqlInternalConnection sqlInternalConnection) + { + // @TODO: This should be part of the sql internal connection class. + Interlocked.CompareExchange( + ref sqlInternalConnection.CachedCommandExecuteReaderAsyncContext, + instance, + null); + } + } + + #endregion + + internal sealed class ExecuteReaderAsyncCallContext + : AAsyncCallContext + { + public SqlCommand Command => _owner; + + public CommandBehavior CommandBehavior { get; set; } + + public Guid OperationId { get; set; } + + public TaskCompletionSource TaskCompletionSource => _source; + + public void Set( + SqlCommand command, + TaskCompletionSource source, + CancellationTokenRegistration disposable, + CommandBehavior behavior, + Guid operationId) + { + base.Set(command, source, disposable); + CommandBehavior = behavior; + OperationId = operationId; + } + + protected override void AfterCleared(SqlCommand owner) + { + owner?.SetCachedCommandExecuteReaderAsyncContext(this); + } + + protected override void Clear() + { + OperationId = Guid.Empty; + CommandBehavior = CommandBehavior.Default; + } + } + } +}