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 0a8db81dc2..4350dde326 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.cs + + Microsoft\Data\SqlClient\SqlCommand.Batch.cs + Microsoft\Data\SqlClient\SqlCommand.Encryption.cs @@ -840,8 +843,7 @@ System\Diagnostics\CodeAnalysis.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 deleted file mode 100644 index 65d8ac6318..0000000000 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs +++ /dev/null @@ -1,1780 +0,0 @@ -// 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.Collections.Concurrent; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Data; -using System.Data.Common; -using System.Data.SqlTypes; -using System.Diagnostics; -using System.IO; -using System.Runtime.CompilerServices; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Data.Common; - -// NOTE: The current Microsoft.VSDesigner editor attributes are implemented for System.Data.SqlClient, and are not publicly available. -// New attributes that are designed to work with Microsoft.Data.SqlClient and are publicly documented should be included in future. -namespace Microsoft.Data.SqlClient -{ - // TODO: Add designer attribute when Microsoft.VSDesigner.Data.VS.SqlCommandDesigner uses Microsoft.Data.SqlClient - public sealed partial class SqlCommand : DbCommand, ICloneable - { - private const int MaxRPCNameLength = 1046; - - internal static readonly Action s_cancelIgnoreFailure = CancelIgnoreFailureCallback; - - private _SqlRPC[] _rpcArrayOf1 = null; // Used for RPC executes - - // cut down on object creation and cache all these - // cached metadata - private _SqlMetaDataSet _cachedMetaData; - - // Last TaskCompletionSource for reconnect task - use for cancellation only - private TaskCompletionSource _reconnectionCompletionSource = null; - -#if DEBUG - internal static int DebugForceAsyncWriteDelay { get; set; } -#endif - - // Cached info for async executions - private sealed class AsyncState - { - // @TODO: Autoproperties - private int _cachedAsyncCloseCount = -1; // value of the connection's CloseCount property when the asyncResult was set; tracks when connections are closed after an async operation - private TaskCompletionSource _cachedAsyncResult = null; - private SqlConnection _cachedAsyncConnection = null; // Used to validate that the connection hasn't changed when end the connection; - private SqlDataReader _cachedAsyncReader = null; - private RunBehavior _cachedRunBehavior = RunBehavior.ReturnImmediately; - private string _cachedSetOptions = null; - private string _cachedEndMethod = null; - - internal AsyncState() - { - } - - internal SqlDataReader CachedAsyncReader - { - get { return _cachedAsyncReader; } - } - internal RunBehavior CachedRunBehavior - { - get { return _cachedRunBehavior; } - } - internal string CachedSetOptions - { - get { return _cachedSetOptions; } - } - internal bool PendingAsyncOperation - { - get { return _cachedAsyncResult != null; } - } - internal string EndMethodName - { - get { return _cachedEndMethod; } - } - - internal bool IsActiveConnectionValid(SqlConnection activeConnection) - { - return (_cachedAsyncConnection == activeConnection && _cachedAsyncCloseCount == activeConnection.CloseCount); - } - - internal void ResetAsyncState() - { - SqlClientEventSource.Log.TryTraceEvent("CachedAsyncState.ResetAsyncState | API | ObjectId {0}, Client Connection Id {1}, AsyncCommandInProgress={2}", - _cachedAsyncConnection?.ObjectID, _cachedAsyncConnection?.ClientConnectionId, _cachedAsyncConnection?.AsyncCommandInProgress); - _cachedAsyncCloseCount = -1; - _cachedAsyncResult = null; - if (_cachedAsyncConnection != null) - { - _cachedAsyncConnection.AsyncCommandInProgress = false; - _cachedAsyncConnection = null; - } - _cachedAsyncReader = null; - _cachedRunBehavior = RunBehavior.ReturnImmediately; - _cachedSetOptions = null; - _cachedEndMethod = null; - } - - internal void SetActiveConnectionAndResult(TaskCompletionSource completion, string endMethod, SqlConnection activeConnection) - { - Debug.Assert(activeConnection != null, "Unexpected null connection argument on SetActiveConnectionAndResult!"); - TdsParser parser = activeConnection?.Parser; - SqlClientEventSource.Log.TryTraceEvent("SqlCommand.SetActiveConnectionAndResult | API | ObjectId {0}, Client Connection Id {1}, MARS={2}", activeConnection?.ObjectID, activeConnection?.ClientConnectionId, parser?.MARSOn); - if ((parser == null) || (parser.State == TdsParserState.Closed) || (parser.State == TdsParserState.Broken)) - { - throw ADP.ClosedConnectionError(); - } - - _cachedAsyncCloseCount = activeConnection.CloseCount; - _cachedAsyncResult = completion; - if (!parser.MARSOn) - { - if (activeConnection.AsyncCommandInProgress) - { - throw SQL.MARSUnsupportedOnConnection(); - } - } - _cachedAsyncConnection = activeConnection; - - // Should only be needed for non-MARS, but set anyways. - _cachedAsyncConnection.AsyncCommandInProgress = true; - _cachedEndMethod = endMethod; - } - - internal void SetAsyncReaderState(SqlDataReader ds, RunBehavior runBehavior, string optionSettings) - { - _cachedAsyncReader = ds; - _cachedRunBehavior = runBehavior; - _cachedSetOptions = optionSettings; - } - } - - private AsyncState _cachedAsyncState = null; - - // @TODO: This is never null, so we can remove the null checks from usages of it. - private AsyncState CachedAsyncState - { - get - { - _cachedAsyncState ??= new AsyncState(); - return _cachedAsyncState; - } - } - - private List<_SqlRPC> _RPCList; - private int _currentlyExecutingBatch; - - /// - /// A flag to indicate if EndExecute was already initiated by the Begin call. - /// - private volatile bool _internalEndExecuteInitiated; - - /// - /// A flag to indicate whether we postponed caching the query metadata for this command. - /// - internal bool CachingQueryMetadataPostponed { get; set; } - - private bool IsProviderRetriable => SqlConfigurableRetryFactory.IsRetriable(RetryLogicProvider); - - internal void OnStatementCompleted(int recordCount) - { - if (0 <= recordCount) - { - StatementCompletedEventHandler handler = _statementCompletedEventHandler; - if (handler != null) - { - try - { - SqlClientEventSource.Log.TryTraceEvent("SqlCommand.OnStatementCompleted | Info | ObjectId {0}, Record Count {1}, Client Connection Id {2}", ObjectID, recordCount, Connection?.ClientConnectionId); - handler(this, new StatementCompletedEventArgs(recordCount)); - } - catch (Exception e) - { - if (!ADP.IsCatchableOrSecurityExceptionType(e)) - { - throw; - } - } - } - } - } - - private void VerifyEndExecuteState(Task completionTask, string endMethod, bool fullCheckForColumnEncryption = false) - { - Debug.Assert(completionTask != null); - SqlClientEventSource.Log.TryTraceEvent("SqlCommand.VerifyEndExecuteState | API | ObjectId {0}, Client Connection Id {1}, MARS={2}, AsyncCommandInProgress={3}", - _activeConnection?.ObjectID, _activeConnection?.ClientConnectionId, - _activeConnection?.Parser?.MARSOn, _activeConnection?.AsyncCommandInProgress); - - if (completionTask.IsCanceled) - { - if (_stateObj != null) - { - _stateObj.Parser.State = TdsParserState.Broken; // We failed to respond to attention, we have to quit! - _stateObj.Parser.Connection.BreakConnection(); - _stateObj.Parser.ThrowExceptionAndWarning(_stateObj, this); - } - else - { - Debug.Assert(_reconnectionCompletionSource == null || _reconnectionCompletionSource.Task.IsCanceled, "ReconnectCompletionSource should be null or cancelled"); - throw SQL.CR_ReconnectionCancelled(); - } - } - else if (completionTask.IsFaulted) - { - throw completionTask.Exception.InnerException; - } - - // If transparent parameter encryption was attempted, then we need to skip other checks like those on EndMethodName - // since we want to wait for async results before checking those fields. - if (IsColumnEncryptionEnabled && !fullCheckForColumnEncryption) - { - if (_activeConnection.State != ConnectionState.Open) - { - // If the connection is not 'valid' then it was closed while we were executing - throw ADP.ClosedConnectionError(); - } - - return; - } - - if (CachedAsyncState.EndMethodName == null) - { - throw ADP.MethodCalledTwice(endMethod); - } - if (endMethod != CachedAsyncState.EndMethodName) - { - throw ADP.MismatchedAsyncResult(CachedAsyncState.EndMethodName, endMethod); - } - if ((_activeConnection.State != ConnectionState.Open) || (!CachedAsyncState.IsActiveConnectionValid(_activeConnection))) - { - // If the connection is not 'valid' then it was closed while we were executing - throw ADP.ClosedConnectionError(); - } - } - - private void WaitForAsyncResults(IAsyncResult asyncResult, bool isInternal) - { - Task completionTask = (Task)asyncResult; - if (!asyncResult.IsCompleted) - { - asyncResult.AsyncWaitHandle.WaitOne(); - } - - if (_stateObj != null) - { - _stateObj._networkPacketTaskSource = null; - } - - // If this is an internal command we will decrement the count when the End method is actually called by the user. - // If we are using Column Encryption and the previous task failed, the async count should have already been fixed up. - // There is a generic issue in how we handle the async count because: - // a) BeginExecute might or might not clean it up on failure. - // b) In EndExecute, we check the task state before waiting and throw if it's failed, whereas if we wait we will always adjust the count. - if (!isInternal && (!IsColumnEncryptionEnabled || !completionTask.IsFaulted)) - { - _activeConnection.GetOpenTdsConnection().DecrementAsyncCount(); - } - } - - private void ThrowIfReconnectionHasBeenCanceled() - { - if (_stateObj == null) - { - var reconnectionCompletionSource = _reconnectionCompletionSource; - if (reconnectionCompletionSource != null && reconnectionCompletionSource.Task != null && reconnectionCompletionSource.Task.IsCanceled) - { - throw SQL.CR_ReconnectionCancelled(); - } - } - } - - private bool TriggerInternalEndAndRetryIfNecessary( - CommandBehavior behavior, - object stateObject, - int timeout, - bool usedCache, - bool isRetry, - bool asyncWrite, - TaskCompletionSource globalCompletion, - TaskCompletionSource localCompletion, - Func endFunc, - Func retryFunc, - string endMethod) - { - // We shouldn't be using the cache if we are in retry. - Debug.Assert(!usedCache || !isRetry); - - // If column encryption is enabled and we used the cache, we want to catch any potential exceptions that were caused by the query cache and retry if the error indicates that we should. - // So, try to read the result of the query before completing the overall task and trigger a retry if appropriate. - if ((IsColumnEncryptionEnabled && !isRetry && (usedCache || ShouldUseEnclaveBasedWorkflow)) -#if DEBUG - || _forceInternalEndQuery -#endif - ) - { - long firstAttemptStart = ADP.TimerCurrent(); - - CreateLocalCompletionTask( - behavior, - stateObject, - timeout, - usedCache, - asyncWrite, - globalCompletion, - localCompletion, - endFunc, - retryFunc, - endMethod, - firstAttemptStart); - - return true; - } - else - { - return false; - } - } - - private void CreateLocalCompletionTask( - CommandBehavior behavior, - object stateObject, - int timeout, - bool usedCache, - bool asyncWrite, - TaskCompletionSource globalCompletion, - TaskCompletionSource localCompletion, - Func endFunc, - Func retryFunc, - string endMethod, - long firstAttemptStart - ) - { - localCompletion.Task.ContinueWith(tsk => - { - if (tsk.IsFaulted) - { - globalCompletion.TrySetException(tsk.Exception.InnerException); - } - else if (tsk.IsCanceled) - { - globalCompletion.TrySetCanceled(); - } - else - { - try - { - // Mark that we initiated the internal EndExecute. This should always be false until we set it here. - Debug.Assert(!_internalEndExecuteInitiated); - _internalEndExecuteInitiated = true; - - // lock on _stateObj prevents races with close/cancel. - lock (_stateObj) - { - endFunc(this, tsk, /*isInternal:*/ true, endMethod); - } - - globalCompletion.TrySetResult(tsk.Result); - } - catch (Exception e) - { - // Put the state object back to the cache. - // Do not reset the async state, since this is managed by the user Begin/End and not internally. - if (ADP.IsCatchableExceptionType(e)) - { - ReliablePutStateObject(); - } - - bool shouldRetry = e is EnclaveDelegate.RetryableEnclaveQueryExecutionException; - - // Check if we have an error indicating that we can retry. - if (e is SqlException) - { - SqlException sqlEx = e as SqlException; - - for (int i = 0; i < sqlEx.Errors.Count; i++) - { - if ((usedCache && (sqlEx.Errors[i].Number == TdsEnums.TCE_CONVERSION_ERROR_CLIENT_RETRY)) || - (ShouldUseEnclaveBasedWorkflow && - (sqlEx.Errors[i].Number == TdsEnums.TCE_ENCLAVE_INVALID_SESSION_HANDLE))) - { - shouldRetry = true; - break; - } - } - } - - if (!shouldRetry) - { - // If we cannot retry, Reset the async state to make sure we leave a clean state. - if (CachedAsyncState != null) - { - CachedAsyncState.ResetAsyncState(); - } - - try - { - _activeConnection.GetOpenTdsConnection().DecrementAsyncCount(); - - globalCompletion.TrySetException(e); - } - catch (Exception e2) - { - globalCompletion.TrySetException(e2); - } - } - else - { - // Remove the entry from the cache since it was inconsistent. - SqlQueryMetadataCache.GetInstance().InvalidateCacheEntry(this); - - InvalidateEnclaveSession(); - - try - { - // Kick off the retry. - _internalEndExecuteInitiated = false; - Task retryTask = (Task)retryFunc( - this, - behavior, - null, - stateObject, - TdsParserStaticMethods.GetRemainingTimeout(timeout, firstAttemptStart), - /*isRetry:*/ true, - asyncWrite); - - retryTask.ContinueWith( - static (Task retryTask, object state) => - { - TaskCompletionSource completion = (TaskCompletionSource)state; - if (retryTask.IsFaulted) - { - completion.TrySetException(retryTask.Exception.InnerException); - } - else if (retryTask.IsCanceled) - { - completion.TrySetCanceled(); - } - else - { - completion.TrySetResult(retryTask.Result); - } - }, - state: globalCompletion, - TaskScheduler.Default - ); - } - catch (Exception e2) - { - globalCompletion.TrySetException(e2); - } - } - } - } - }, TaskScheduler.Default); - } - - // If the user part is quoted, remove first and last brackets and then unquote any right square - // brackets in the procedure. This is a very simple parser that performs no validation. As - // with the function below, ideally we should have support from the server for this. - private static string UnquoteProcedurePart(string part) - { - if (part != null && (2 <= part.Length)) - { - if ('[' == part[0] && ']' == part[part.Length - 1]) - { - part = part.Substring(1, part.Length - 2); // strip outer '[' & ']' - part = part.Replace("]]", "]"); // undo quoted "]" from "]]" to "]" - } - } - return part; - } - - // User value in this format: [server].[database].[schema].[sp_foo];1 - // This function should only be passed "[sp_foo];1". - // This function uses a pretty simple parser that doesn't do any validation. - // Ideally, we would have support from the server rather than us having to do this. - private static string UnquoteProcedureName(string name, out object groupNumber) - { - groupNumber = null; // Out param - initialize value to no value. - string sproc = name; - - if (sproc != null) - { - if (char.IsDigit(sproc[sproc.Length - 1])) - { - // If last char is a digit, parse. - int semicolon = sproc.LastIndexOf(';'); - if (semicolon != -1) - { - // If we found a semicolon, obtain the integer. - string part = sproc.Substring(semicolon + 1); - int number = 0; - if (int.TryParse(part, out number)) - { - // No checking, just fail if this doesn't work. - groupNumber = number; - sproc = sproc.Substring(0, semicolon); - } - } - } - sproc = UnquoteProcedurePart(sproc); - } - return sproc; - } - - // Index into indirection arrays for columns of interest to DeriveParameters - private enum ProcParamsColIndex - { - ParameterName = 0, - ParameterType, - DataType, // obsolete in 2008, use ManagedDataType instead - ManagedDataType, // new in 2008 - CharacterMaximumLength, - NumericPrecision, - NumericScale, - TypeCatalogName, - TypeSchemaName, - TypeName, - XmlSchemaCollectionCatalogName, - XmlSchemaCollectionSchemaName, - XmlSchemaCollectionName, - UdtTypeName, // obsolete in 2008. Holds the actual typename if UDT, since TypeName didn't back then. - DateTimeScale // new in 2008 - }; - - // 2005- column ordinals (this array indexed by ProcParamsColIndex - internal static readonly string[] PreSql2008ProcParamsNames = new string[] { - "PARAMETER_NAME", // ParameterName, - "PARAMETER_TYPE", // ParameterType, - "DATA_TYPE", // DataType - null, // ManagedDataType, introduced in 2008 - "CHARACTER_MAXIMUM_LENGTH", // CharacterMaximumLength, - "NUMERIC_PRECISION", // NumericPrecision, - "NUMERIC_SCALE", // NumericScale, - "UDT_CATALOG", // TypeCatalogName, - "UDT_SCHEMA", // TypeSchemaName, - "TYPE_NAME", // TypeName, - "XML_CATALOGNAME", // XmlSchemaCollectionCatalogName, - "XML_SCHEMANAME", // XmlSchemaCollectionSchemaName, - "XML_SCHEMACOLLECTIONNAME", // XmlSchemaCollectionName - "UDT_NAME", // UdtTypeName - null, // Scale for datetime types with scale, introduced in 2008 - }; - - // 2008+ column ordinals (this array indexed by ProcParamsColIndex - internal static readonly string[] Sql2008ProcParamsNames = new string[] { - "PARAMETER_NAME", // ParameterName, - "PARAMETER_TYPE", // ParameterType, - null, // DataType, removed from 2008+ - "MANAGED_DATA_TYPE", // ManagedDataType, - "CHARACTER_MAXIMUM_LENGTH", // CharacterMaximumLength, - "NUMERIC_PRECISION", // NumericPrecision, - "NUMERIC_SCALE", // NumericScale, - "TYPE_CATALOG_NAME", // TypeCatalogName, - "TYPE_SCHEMA_NAME", // TypeSchemaName, - "TYPE_NAME", // TypeName, - "XML_CATALOGNAME", // XmlSchemaCollectionCatalogName, - "XML_SCHEMANAME", // XmlSchemaCollectionSchemaName, - "XML_SCHEMACOLLECTIONNAME", // XmlSchemaCollectionName - null, // UdtTypeName, removed from 2008+ - "SS_DATETIME_PRECISION", // Scale for datetime types with scale - }; - - internal void DeriveParameters() - { - switch (CommandType) - { - case CommandType.Text: - throw ADP.DeriveParametersNotSupported(this); - case CommandType.StoredProcedure: - break; - case CommandType.TableDirect: - // CommandType.TableDirect - do nothing, parameters are not supported - throw ADP.DeriveParametersNotSupported(this); - default: - throw ADP.InvalidCommandType(CommandType); - } - - // validate that we have a valid connection - ValidateCommand(isAsync: false); - - // Use common parser for SqlClient and OleDb - parse into 4 parts - Server, Catalog, Schema, ProcedureName - string[] parsedSProc = MultipartIdentifier.ParseMultipartIdentifier(CommandText, "[\"", "]\"", Strings.SQL_SqlCommandCommandText, false); - if (string.IsNullOrEmpty(parsedSProc[3])) - { - throw ADP.NoStoredProcedureExists(CommandText); - } - - Debug.Assert(parsedSProc.Length == 4, "Invalid array length result from SqlCommandBuilder.ParseProcedureName"); - - SqlCommand paramsCmd = null; - StringBuilder cmdText = new StringBuilder(); - - // Build call for sp_procedure_params_rowset built of unquoted values from user: - // [user server, if provided].[user catalog, else current database].[sys if 2005, else blank].[sp_procedure_params_rowset] - - // Server - pass only if user provided. - if (!string.IsNullOrEmpty(parsedSProc[0])) - { - SqlCommandSet.BuildStoredProcedureName(cmdText, parsedSProc[0]); - cmdText.Append("."); - } - - // Catalog - pass user provided, otherwise use current database. - if (string.IsNullOrEmpty(parsedSProc[1])) - { - parsedSProc[1] = Connection.Database; - } - SqlCommandSet.BuildStoredProcedureName(cmdText, parsedSProc[1]); - cmdText.Append("."); - - // Schema - only if 2005, and then only pass sys. Also - pass managed version of sproc - // for 2005, else older sproc. - string[] colNames; - bool useManagedDataType; - if (Connection.Is2008OrNewer) - { - // Procedure - [sp_procedure_params_managed] - cmdText.Append("[sys].[").Append(TdsEnums.SP_PARAMS_MGD10).Append("]"); - - colNames = Sql2008ProcParamsNames; - useManagedDataType = true; - } - else - { - // Procedure - [sp_procedure_params_managed] - cmdText.Append("[sys].[").Append(TdsEnums.SP_PARAMS_MANAGED).Append("]"); - - colNames = PreSql2008ProcParamsNames; - useManagedDataType = false; - } - - - paramsCmd = new SqlCommand(cmdText.ToString(), Connection, Transaction) - { - CommandType = CommandType.StoredProcedure - }; - - object groupNumber; - - // Prepare parameters for sp_procedure_params_rowset: - // 1) procedure name - unquote user value - // 2) group number - parsed at the time we unquoted procedure name - // 3) procedure schema - unquote user value - - paramsCmd.Parameters.Add(new SqlParameter("@procedure_name", SqlDbType.NVarChar, 255)); - paramsCmd.Parameters[0].Value = UnquoteProcedureName(parsedSProc[3], out groupNumber); // ProcedureName is 4rd element in parsed array - - if (groupNumber != null) - { - SqlParameter param = paramsCmd.Parameters.Add(new SqlParameter("@group_number", SqlDbType.Int)); - param.Value = groupNumber; - } - - if (!string.IsNullOrEmpty(parsedSProc[2])) - { - // SchemaName is 3rd element in parsed array - SqlParameter param = paramsCmd.Parameters.Add(new SqlParameter("@procedure_schema", SqlDbType.NVarChar, 255)); - param.Value = UnquoteProcedurePart(parsedSProc[2]); - } - - SqlDataReader r = null; - - List parameters = new List(); - bool processFinallyBlock = true; - - try - { - r = paramsCmd.ExecuteReader(); - - SqlParameter p = null; - - while (r.Read()) - { - // each row corresponds to a parameter of the stored proc. Fill in all the info - p = new SqlParameter() - { - ParameterName = (string)r[colNames[(int)ProcParamsColIndex.ParameterName]] - }; - - // type - if (useManagedDataType) - { - p.SqlDbType = (SqlDbType)(short)r[colNames[(int)ProcParamsColIndex.ManagedDataType]]; - - // 2005 didn't have as accurate of information as we're getting for 2008, so re-map a couple of - // types for backward compatability. - switch (p.SqlDbType) - { - case SqlDbType.Image: - case SqlDbType.Timestamp: - p.SqlDbType = SqlDbType.VarBinary; - break; - - case SqlDbType.NText: - p.SqlDbType = SqlDbType.NVarChar; - break; - - case SqlDbType.Text: - p.SqlDbType = SqlDbType.VarChar; - break; - - default: - break; - } - } - else - { - p.SqlDbType = MetaType.GetSqlDbTypeFromOleDbType((short)r[colNames[(int)ProcParamsColIndex.DataType]], - ADP.IsNull(r[colNames[(int)ProcParamsColIndex.TypeName]]) ? "" : - (string)r[colNames[(int)ProcParamsColIndex.TypeName]]); - } - - // size - object a = r[colNames[(int)ProcParamsColIndex.CharacterMaximumLength]]; - if (a is int) - { - int size = (int)a; - - // Map MAX sizes correctly. The 2008 server-side proc sends 0 for these instead of -1. - // Should be fixed on the 2008 side, but would likely hold up the RI, and is safer to fix here. - // If we can get the server-side fixed before shipping 2008, we can remove this mapping. - if (0 == size && - (p.SqlDbType == SqlDbType.NVarChar || - p.SqlDbType == SqlDbType.VarBinary || - p.SqlDbType == SqlDbType.VarChar)) - { - size = -1; - } - p.Size = size; - } - - // direction - p.Direction = ParameterDirectionFromOleDbDirection((short)r[colNames[(int)ProcParamsColIndex.ParameterType]]); - - if (p.SqlDbType == SqlDbType.Decimal) - { - p.ScaleInternal = (byte)((short)r[colNames[(int)ProcParamsColIndex.NumericScale]] & 0xff); - p.PrecisionInternal = (byte)((short)r[colNames[(int)ProcParamsColIndex.NumericPrecision]] & 0xff); - } - - // type name for Udt - if (SqlDbType.Udt == p.SqlDbType) - { - string udtTypeName; - if (useManagedDataType) - { - udtTypeName = (string)r[colNames[(int)ProcParamsColIndex.TypeName]]; - } - else - { - udtTypeName = (string)r[colNames[(int)ProcParamsColIndex.UdtTypeName]]; - } - - //read the type name - p.UdtTypeName = r[colNames[(int)ProcParamsColIndex.TypeCatalogName]] + "." + - r[colNames[(int)ProcParamsColIndex.TypeSchemaName]] + "." + - udtTypeName; - } - - // type name for Structured types (same as for Udt's except assign p.TypeName instead of p.UdtTypeName - if (SqlDbType.Structured == p.SqlDbType) - { - Debug.Assert(_activeConnection.Is2008OrNewer, "Invalid datatype token received from pre-2008 server"); - - //read the type name - p.TypeName = r[colNames[(int)ProcParamsColIndex.TypeCatalogName]] + "." + - r[colNames[(int)ProcParamsColIndex.TypeSchemaName]] + "." + - r[colNames[(int)ProcParamsColIndex.TypeName]]; - - // the constructed type name above is incorrectly formatted, it should be a 2 part name not 3 - // for compatibility we can't change this because the bug has existed for a long time and been - // worked around by users, so identify that it is present and catch it later in the execution - // process once users can no longer interact with with the parameter type name - p.IsDerivedParameterTypeName = true; - } - - // XmlSchema name for Xml types - if (SqlDbType.Xml == p.SqlDbType) - { - object value; - - value = r[colNames[(int)ProcParamsColIndex.XmlSchemaCollectionCatalogName]]; - p.XmlSchemaCollectionDatabase = ADP.IsNull(value) ? string.Empty : (string)value; - - value = r[colNames[(int)ProcParamsColIndex.XmlSchemaCollectionSchemaName]]; - p.XmlSchemaCollectionOwningSchema = ADP.IsNull(value) ? string.Empty : (string)value; - - value = r[colNames[(int)ProcParamsColIndex.XmlSchemaCollectionName]]; - p.XmlSchemaCollectionName = ADP.IsNull(value) ? string.Empty : (string)value; - } - - if (MetaType._IsVarTime(p.SqlDbType)) - { - object value = r[colNames[(int)ProcParamsColIndex.DateTimeScale]]; - if (value is int) - { - p.ScaleInternal = (byte)(((int)value) & 0xff); - } - } - - parameters.Add(p); - } - } - catch (Exception e) - { - processFinallyBlock = ADP.IsCatchableExceptionType(e); - throw; - } - finally - { - if (processFinallyBlock) - { - r?.Close(); - - // always unhook the user's connection - paramsCmd.Connection = null; - } - } - - if (parameters.Count == 0) - { - throw ADP.NoStoredProcedureExists(this.CommandText); - } - - Parameters.Clear(); - - foreach (SqlParameter temp in parameters) - { - _parameters.Add(temp); - } - } - - private ParameterDirection ParameterDirectionFromOleDbDirection(short oledbDirection) - { - Debug.Assert(oledbDirection >= 1 && oledbDirection <= 4, "invalid parameter direction from params_rowset!"); - - switch (oledbDirection) - { - case 2: - return ParameterDirection.InputOutput; - case 3: - return ParameterDirection.Output; - case 4: - return ParameterDirection.ReturnValue; - default: - return ParameterDirection.Input; - } - - } - - // get cached metadata - internal _SqlMetaDataSet MetaData - { - get - { - return _cachedMetaData; - } - } - - // Check to see if notifications auto enlistment is turned on. Enlist if so. - private void CheckNotificationStateAndAutoEnlist() - { - // Auto-enlist not supported in Core - - // If we have a notification with a dependency, setup the notification options at this time. - - // If user passes options, then we will always have option data at the time the SqlDependency - // ctor is called. But, if we are using default queue, then we do not have this data until - // Start(). Due to this, we always delay setting options until execute. - - // There is a variance in order between Start(), SqlDependency(), and Execute. This is the - // best way to solve that problem. - if (Notification != null) - { - if (_sqlDep != null) - { - if (_sqlDep.Options == null) - { - // If null, SqlDependency was not created with options, so we need to obtain default options now. - // GetDefaultOptions can and will throw under certain conditions. - - // In order to match to the appropriate start - we need 3 pieces of info: - // 1) server 2) user identity (SQL Auth or Int Sec) 3) database - - SqlDependency.IdentityUserNamePair identityUserName = null; - - // Obtain identity from connection. - SqlInternalConnectionTds internalConnection = _activeConnection.InnerConnection as SqlInternalConnectionTds; - if (internalConnection.Identity != null) - { - identityUserName = new SqlDependency.IdentityUserNamePair(internalConnection.Identity, null); - } - else - { - identityUserName = new SqlDependency.IdentityUserNamePair(null, internalConnection.ConnectionOptions.UserID); - } - - Notification.Options = SqlDependency.GetDefaultComposedOptions(_activeConnection.DataSource, - InternalTdsConnection.ServerProvidedFailoverPartner, - identityUserName, _activeConnection.Database); - } - - // Set UserData on notifications, as well as adding to the appdomain dispatcher. The value is - // computed by an algorithm on the dependency - fixed and will always produce the same value - // given identical commandtext + parameter values. - Notification.UserData = _sqlDep.ComputeHashAndAddToDispatcher(this); - // Maintain server list for SqlDependency. - _sqlDep.AddToServerList(_activeConnection.DataSource); - } - } - } - - private Task RegisterForConnectionCloseNotification(Task outerTask) - { - SqlConnection connection = _activeConnection; - if (connection == null) - { - // No connection - throw ADP.ClosedConnectionError(); - } - - return connection.RegisterForConnectionCloseNotification(outerTask, this, SqlReferenceCollection.CommandTag); - } - - // validates that a command has commandText and a non-busy open connection - // throws exception for error case, returns false if the commandText is empty - private void ValidateCommand(bool isAsync, [CallerMemberName] string method = "") - { - if (_activeConnection == null) - { - throw ADP.ConnectionRequired(method); - } - - // Ensure that the connection is open and that the Parser is in the correct state - SqlInternalConnectionTds tdsConnection = _activeConnection.InnerConnection as SqlInternalConnectionTds; - - // Ensure that if column encryption override was used then server supports its - if (((SqlCommandColumnEncryptionSetting.UseConnectionSetting == ColumnEncryptionSetting && _activeConnection.IsColumnEncryptionSettingEnabled) - || (ColumnEncryptionSetting == SqlCommandColumnEncryptionSetting.Enabled || ColumnEncryptionSetting == SqlCommandColumnEncryptionSetting.ResultSetOnly)) - && tdsConnection != null - && tdsConnection.Parser != null - && !tdsConnection.Parser.IsColumnEncryptionSupported) - { - throw SQL.TceNotSupported(); - } - - if (tdsConnection != null) - { - var parser = tdsConnection.Parser; - if ((parser == null) || (parser.State == TdsParserState.Closed)) - { - throw ADP.OpenConnectionRequired(method, ConnectionState.Closed); - } - else if (parser.State != TdsParserState.OpenLoggedIn) - { - throw ADP.OpenConnectionRequired(method, ConnectionState.Broken); - } - } - else if (_activeConnection.State == ConnectionState.Closed) - { - throw ADP.OpenConnectionRequired(method, ConnectionState.Closed); - } - else if (_activeConnection.State == ConnectionState.Broken) - { - throw ADP.OpenConnectionRequired(method, ConnectionState.Broken); - } - - ValidateAsyncCommand(); - - // close any non MARS dead readers, if applicable, and then throw if still busy. - // Throw if we have a live reader on this command - _activeConnection.ValidateConnectionForExecute(method, this); - // Check to see if the currently set transaction has completed. If so, - // null out our local reference. - if (_transaction != null && _transaction.Connection == null) - { - _transaction = null; - } - - // throw if the connection is in a transaction but there is no - // locally assigned transaction object - if (_activeConnection.HasLocalTransactionFromAPI && _transaction == null) - { - throw ADP.TransactionRequired(method); - } - - // if we have a transaction, check to ensure that the active - // connection property matches the connection associated with - // the transaction - if (_transaction != null && _activeConnection != _transaction.Connection) - { - throw ADP.TransactionConnectionMismatch(); - } - - if (string.IsNullOrEmpty(this.CommandText)) - { - throw ADP.CommandTextRequired(method); - } - } - - private void ValidateAsyncCommand() - { - if (CachedAsyncState != null && CachedAsyncState.PendingAsyncOperation) - { - // Enforce only one pending async execute at a time. - if (CachedAsyncState.IsActiveConnectionValid(_activeConnection)) - { - throw SQL.PendingBeginXXXExists(); - } - else - { - _stateObj = null; // Session was re-claimed by session pool upon connection close. - CachedAsyncState.ResetAsyncState(); - } - } - } - - private void GetStateObject(TdsParser parser = null) - { - Debug.Assert(_stateObj == null, "StateObject not null on GetStateObject"); - Debug.Assert(_activeConnection != null, "no active connection?"); - - if (_pendingCancel) - { - _pendingCancel = false; // Not really needed, but we'll reset anyways. - - // If a pendingCancel exists on the object, we must have had a Cancel() call - // between the point that we entered an Execute* API and the point in Execute* that - // we proceeded to call this function and obtain a stateObject. In that case, - // we now throw a cancelled error. - throw SQL.OperationCancelled(); - } - - if (parser == null) - { - parser = _activeConnection.Parser; - if ((parser == null) || (parser.State == TdsParserState.Broken) || (parser.State == TdsParserState.Closed)) - { - // Connection's parser is null as well, therefore we must be closed - throw ADP.ClosedConnectionError(); - } - } - - TdsParserStateObject stateObj = parser.GetSession(this); - stateObj.StartSession(this); - - _stateObj = stateObj; - - if (_pendingCancel) - { - _pendingCancel = false; // Not really needed, but we'll reset anyways. - - // If a pendingCancel exists on the object, we must have had a Cancel() call - // between the point that we entered this function and the point where we obtained - // and actually assigned the stateObject to the local member. It is possible - // that the flag is set as well as a call to stateObj.Cancel - though that would - // be a no-op. So - throw. - throw SQL.OperationCancelled(); - } - } - - private void ReliablePutStateObject() - { - PutStateObject(); - } - - private void PutStateObject() - { - TdsParserStateObject stateObj = _stateObj; - _stateObj = null; - - if (stateObj != null) - { - stateObj.CloseSession(); - } - } - - private SqlParameterCollection GetCurrentParameterCollection() - { - if (_batchRPCMode) - { - if (_RPCList.Count > _currentlyExecutingBatch) - { - return _RPCList[_currentlyExecutingBatch].userParams; - } - else - { - Debug.Fail("OnReturnValue: SqlCommand got too many DONEPROC events"); - return null; - } - } - else - { - return _parameters; - } - } - - private SqlParameter GetParameterForOutputValueExtraction(SqlParameterCollection parameters, - string paramName, int paramCount) - { - SqlParameter thisParam = null; - bool foundParam = false; - - if (paramName == null) - { - // rec.parameter should only be null for a return value from a function - for (int i = 0; i < paramCount; i++) - { - thisParam = parameters[i]; - // searching for ReturnValue - if (thisParam.Direction == ParameterDirection.ReturnValue) - { - foundParam = true; - break; // found it - } - } - } - else - { - for (int i = 0; i < paramCount; i++) - { - thisParam = parameters[i]; - // searching for Output or InputOutput or ReturnValue with matching name - if ( - thisParam.Direction != ParameterDirection.Input && - thisParam.Direction != ParameterDirection.ReturnValue && - SqlParameter.ParameterNamesEqual(paramName, thisParam.ParameterName, StringComparison.Ordinal) - ) - { - foundParam = true; - break; // found it - } - } - } - - if (foundParam) - { - return thisParam; - } - else - { - return null; - } - } - - // @TODO: Why not *return* it? - private void GetRPCObject(int systemParamCount, int userParamCount, ref _SqlRPC rpc, bool forSpDescribeParameterEncryption = false) - { - // Designed to minimize necessary allocations - if (rpc == null) - { - if (!forSpDescribeParameterEncryption) - { - if (_rpcArrayOf1 == null) - { - _rpcArrayOf1 = new _SqlRPC[1]; - _rpcArrayOf1[0] = new _SqlRPC(); - } - - rpc = _rpcArrayOf1[0]; - } - else - { - if (_rpcForEncryption == null) - { - _rpcForEncryption = new _SqlRPC(); - } - - rpc = _rpcForEncryption; - } - } - - rpc.ProcID = 0; - rpc.rpcName = null; - rpc.options = 0; - rpc.systemParamCount = systemParamCount; - rpc.needsFetchParameterEncryptionMetadata = false; - - int currentCount = rpc.systemParams?.Length ?? 0; - - // Make sure there is enough space in the parameters and paramoptions arrays - if (currentCount < systemParamCount) - { - Array.Resize(ref rpc.systemParams, systemParamCount); - Array.Resize(ref rpc.systemParamOptions, systemParamCount); - for (int index = currentCount; index < systemParamCount; index++) - { - rpc.systemParams[index] = new SqlParameter(); - } - } - - for (int ii = 0; ii < systemParamCount; ii++) - { - rpc.systemParamOptions[ii] = 0; - } - - if ((rpc.userParamMap?.Length ?? 0) < userParamCount) - { - Array.Resize(ref rpc.userParamMap, userParamCount); - } - } - - private void SetUpRPCParameters(_SqlRPC rpc, bool inSchema, SqlParameterCollection parameters) - { - int paramCount = GetParameterCount(parameters); - int userParamCount = 0; - - for (int index = 0; index < paramCount; index++) - { - SqlParameter parameter = parameters[index]; - parameter.Validate(index, CommandType.StoredProcedure == CommandType); - - // func will change type to that with a 4 byte length if the type has a two - // byte length and a parameter length > than that expressible in 2 bytes - if ((!parameter.ValidateTypeLengths().IsPlp) && (parameter.Direction != ParameterDirection.Output)) - { - parameter.FixStreamDataForNonPLP(); - } - - if (ShouldSendParameter(parameter)) - { - byte options = 0; - - // set output bit - if (parameter.Direction == ParameterDirection.InputOutput || parameter.Direction == ParameterDirection.Output) - { - options = TdsEnums.RPC_PARAM_BYREF; - } - - // Set the encryped bit, if the parameter is to be encrypted. - if (parameter.CipherMetadata != null) - { - options |= TdsEnums.RPC_PARAM_ENCRYPTED; - } - - // set default value bit - if (parameter.Direction != ParameterDirection.Output) - { - // remember that Convert.IsEmpty is null, DBNull.Value is a database null! - - // Don't assume a default value exists for parameters in the case when - // the user is simply requesting schema. - // TVPs use DEFAULT and do not allow NULL, even for schema only. - if (parameter.Value == null && (!inSchema || SqlDbType.Structured == parameter.SqlDbType)) - { - options |= TdsEnums.RPC_PARAM_DEFAULT; - } - - // detect incorrectly derived type names unchanged by the caller and fix them - if (parameter.IsDerivedParameterTypeName) - { - string[] parts = MultipartIdentifier.ParseMultipartIdentifier(parameter.TypeName, "[\"", "]\"", Strings.SQL_TDSParserTableName, false); - if (parts != null && parts.Length == 4) // will always return int[4] right justified - { - if ( - parts[3] != null && // name must not be null - parts[2] != null && // schema must not be null - parts[1] != null // server should not be null or we don't need to remove it - ) - { - parameter.TypeName = QuoteIdentifier(parts.AsSpan(2, 2)); - } - } - } - } - - rpc.userParamMap[userParamCount] = ((((long)options) << 32) | (long)index); - userParamCount += 1; - - // Must set parameter option bit for LOB_COOKIE if unfilled LazyMat blob - } - } - - rpc.userParamCount = userParamCount; - rpc.userParams = parameters; - } - - // - // returns true if the parameter is not a return value - // and it's value is not DBNull (for a nullable parameter) - // - private static bool ShouldSendParameter(SqlParameter p, bool includeReturnValue = false) - { - switch (p.Direction) - { - case ParameterDirection.ReturnValue: - // return value parameters are not sent, except for the parameter list of sp_describe_parameter_encryption - return includeReturnValue; - case ParameterDirection.Output: - case ParameterDirection.InputOutput: - case ParameterDirection.Input: - // InputOutput/Output parameters are aways sent - return true; - default: - Debug.Fail("Invalid ParameterDirection!"); - return false; - } - } - - private static int CountSendableParameters(SqlParameterCollection parameters) - { - int cParams = 0; - - if (parameters != null) - { - int count = parameters.Count; - for (int i = 0; i < count; i++) - { - if (ShouldSendParameter(parameters[i])) - { - cParams++; - } - } - } - return cParams; - } - - // Returns total number of parameters - private static int GetParameterCount(SqlParameterCollection parameters) - { - return parameters != null ? parameters.Count : 0; - } - - // paramList parameter for sp_executesql, sp_prepare, and sp_prepexec - internal string BuildParamList(TdsParser parser, SqlParameterCollection parameters, bool includeReturnValue = false) - { - StringBuilder paramList = new StringBuilder(); - bool fAddSeparator = false; - - int count = parameters.Count; - for (int i = 0; i < count; i++) - { - SqlParameter sqlParam = parameters[i]; - sqlParam.Validate(i, CommandType.StoredProcedure == CommandType); - // skip ReturnValue parameters; we never send them to the server - if (!ShouldSendParameter(sqlParam, includeReturnValue)) - { - continue; - } - - // add our separator for the ith parameter - if (fAddSeparator) - { - paramList.Append(','); - } - - SqlParameter.AppendPrefixedParameterName(paramList, sqlParam.ParameterName); - - MetaType mt = sqlParam.InternalMetaType; - - //for UDTs, get the actual type name. Get only the typename, omit catalog and schema names. - //in TSQL you should only specify the unqualified type name - - // paragraph above doesn't seem to be correct. Server won't find the type - // if we don't provide a fully qualified name - paramList.Append(" "); - if (mt.SqlDbType == SqlDbType.Udt) - { - string fullTypeName = sqlParam.UdtTypeName; - if (string.IsNullOrEmpty(fullTypeName)) - { - throw SQL.MustSetUdtTypeNameForUdtParams(); - } - - paramList.Append(ParseAndQuoteIdentifier(fullTypeName, true /* is UdtTypeName */)); - } - else if (mt.SqlDbType == SqlDbType.Structured) - { - string typeName = sqlParam.TypeName; - if (string.IsNullOrEmpty(typeName)) - { - throw SQL.MustSetTypeNameForParam(mt.TypeName, sqlParam.GetPrefixedParameterName()); - } - paramList.Append(ParseAndQuoteIdentifier(typeName, false /* is not UdtTypeName*/)); - - // TVPs currently are the only Structured type and must be read only, so add that keyword - paramList.Append(" READONLY"); - } - else - { - // func will change type to that with a 4 byte length if the type has a two - // byte length and a parameter length > than that expressible in 2 bytes - mt = sqlParam.ValidateTypeLengths(); - if ((!mt.IsPlp) && (sqlParam.Direction != ParameterDirection.Output)) - { - sqlParam.FixStreamDataForNonPLP(); - } - paramList.Append(mt.TypeName); - } - - fAddSeparator = true; - - if (mt.SqlDbType == SqlDbType.Decimal) - { - byte precision = sqlParam.GetActualPrecision(); - byte scale = sqlParam.GetActualScale(); - - paramList.Append('('); - - if (0 == precision) - { - precision = TdsEnums.DEFAULT_NUMERIC_PRECISION; - } - - paramList.Append(precision); - paramList.Append(','); - paramList.Append(scale); - paramList.Append(')'); - } - else if (mt.IsVarTime) - { - byte scale = sqlParam.GetActualScale(); - - paramList.Append('('); - paramList.Append(scale); - paramList.Append(')'); - } - else if (mt.SqlDbType == SqlDbTypeExtensions.Vector) - { - // The validate function for SqlParameters would - // have already thrown InvalidCastException if an incompatible - // value is specified for SqlDbType Vector. - var sqlVectorProps = (ISqlVector)sqlParam.Value; - paramList.Append('('); - paramList.Append(sqlVectorProps.Length); - paramList.Append(')'); - } - else if (!mt.IsFixed && !mt.IsLong && mt.SqlDbType != SqlDbType.Timestamp && mt.SqlDbType != SqlDbType.Udt && SqlDbType.Structured != mt.SqlDbType) - { - int size = sqlParam.Size; - - paramList.Append('('); - - // if using non unicode types, obtain the actual byte length from the parser, with it's associated code page - if (mt.IsAnsiType) - { - object val = sqlParam.GetCoercedValue(); - string s = null; - - // deal with the sql types - if (val != null && (DBNull.Value != val)) - { - s = (val as string); - if (s == null) - { - SqlString sval = val is SqlString ? (SqlString)val : SqlString.Null; - if (!sval.IsNull) - { - s = sval.Value; - } - } - } - - if (s != null) - { - int actualBytes = parser.GetEncodingCharLength(s, sqlParam.GetActualSize(), sqlParam.Offset, null); - // if actual number of bytes is greater than the user given number of chars, use actual bytes - if (actualBytes > size) - { - size = actualBytes; - } - } - } - - // If the user specifies a 0-sized parameter for a variable len field - // pass over max size (8000 bytes or 4000 characters for wide types) - if (0 == size) - { - size = mt.IsSizeInCharacters ? (TdsEnums.MAXSIZE >> 1) : TdsEnums.MAXSIZE; - } - - paramList.Append(size); - paramList.Append(')'); - } - else if (mt.IsPlp && (mt.SqlDbType != SqlDbType.Xml) && (mt.SqlDbType != SqlDbType.Udt) && (mt.SqlDbType != SqlDbTypeExtensions.Json)) - { - paramList.Append("(max) "); - } - - // set the output bit for Output or InputOutput parameters - if (sqlParam.Direction != ParameterDirection.Input) - paramList.Append(" " + TdsEnums.PARAM_OUTPUT); - } - - return paramList.ToString(); - } - - // Adds quotes to each part of a SQL identifier that may be multi-part, while leaving - // the result as a single composite name. - private static string ParseAndQuoteIdentifier(string identifier, bool isUdtTypeName) - { - string[] strings = SqlParameter.ParseTypeName(identifier, isUdtTypeName); - return QuoteIdentifier(strings); - } - - private static string QuoteIdentifier(ReadOnlySpan strings) - { - StringBuilder bld = new StringBuilder(); - - // Stitching back together is a little tricky. Assume we want to build a full multi-part name - // with all parts except trimming separators for leading empty names (null or empty strings, - // but not whitespace). Separators in the middle should be added, even if the name part is - // null/empty, to maintain proper location of the parts. - for (int i = 0; i < strings.Length; i++) - { - if (0 < bld.Length) - { - bld.Append('.'); - } - if (strings[i] != null && 0 != strings[i].Length) - { - ADP.AppendQuotedString(bld, "[", "]", strings[i]); - } - } - - return bld.ToString(); - } - - // returns set option text to turn on format only and key info on and off - // When we are executing as a text command, then we never need - // to turn off the options since they command text is executed in the scope of sp_executesql. - // For a stored proc command, however, we must send over batch sql and then turn off - // the set options after we read the data. See the code in Command.Execute() - private string GetSetOptionsString(CommandBehavior behavior) - { - string s = null; - - if ((System.Data.CommandBehavior.SchemaOnly == (behavior & CommandBehavior.SchemaOnly)) || - (System.Data.CommandBehavior.KeyInfo == (behavior & CommandBehavior.KeyInfo))) - { - // SET FMTONLY ON will cause the server to ignore other SET OPTIONS, so turn - // it off before we ask for browse mode metadata - s = TdsEnums.FMTONLY_OFF; - - if (System.Data.CommandBehavior.KeyInfo == (behavior & CommandBehavior.KeyInfo)) - { - s = s + TdsEnums.BROWSE_ON; - } - - if (System.Data.CommandBehavior.SchemaOnly == (behavior & CommandBehavior.SchemaOnly)) - { - s = s + TdsEnums.FMTONLY_ON; - } - } - - return s; - } - - private string GetResetOptionsString(CommandBehavior behavior) - { - string s = null; - - // SET FMTONLY ON OFF - if (System.Data.CommandBehavior.SchemaOnly == (behavior & CommandBehavior.SchemaOnly)) - { - s = s + TdsEnums.FMTONLY_OFF; - } - - // SET NO_BROWSETABLE OFF - if (System.Data.CommandBehavior.KeyInfo == (behavior & CommandBehavior.KeyInfo)) - { - s = s + TdsEnums.BROWSE_OFF; - } - - return s; - } - - private string GetCommandText(CommandBehavior behavior) - { - // build the batch string we send over, since we execute within a stored proc (sp_executesql), the SET options never need to be - // turned off since they are scoped to the sproc - Debug.Assert(System.Data.CommandType.Text == this.CommandType, "invalid call to GetCommandText for stored proc!"); - return GetSetOptionsString(behavior) + this.CommandText; - } - - internal void CheckThrowSNIException() - { - var stateObj = _stateObj; - if (stateObj != null) - { - stateObj.CheckThrowSNIException(); - } - } - - // We're being notified that the underlying connection has closed - internal void OnConnectionClosed() - { - var stateObj = _stateObj; - if (stateObj != null) - { - stateObj.OnConnectionClosed(); - } - } - - internal void ClearBatchCommand() - { - _RPCList?.Clear(); - _currentlyExecutingBatch = 0; - } - - internal void SetBatchRPCMode(bool value, int commandCount = 1) - { - _batchRPCMode = value; - ClearBatchCommand(); - if (_batchRPCMode) - { - if (_RPCList == null) - { - _RPCList = new List<_SqlRPC>(commandCount); - } - else - { - _RPCList.Capacity = commandCount; - } - } - } - - internal void SetBatchRPCModeReadyToExecute() - { - Debug.Assert(_batchRPCMode, "Command is not in batch RPC Mode"); - Debug.Assert(_RPCList != null, "No batch commands specified"); - - _currentlyExecutingBatch = 0; - } - - internal void AddBatchCommand(SqlBatchCommand batchCommand) - { - Debug.Assert(_batchRPCMode, "Command is not in batch RPC Mode"); - Debug.Assert(_RPCList != null); - - _SqlRPC rpc = new _SqlRPC - { - batchCommand = batchCommand - }; - string commandText = batchCommand.CommandText; - CommandType cmdType = batchCommand.CommandType; - - CommandText = commandText; - CommandType = cmdType; - - // Set the column encryption setting. - SetColumnEncryptionSetting(batchCommand.ColumnEncryptionSetting); - - GetStateObject(); - if (cmdType == CommandType.StoredProcedure) - { - BuildRPC(false, batchCommand.Parameters, ref rpc); - } - else - { - // All batch sql statements must be executed inside sp_executesql, including those without parameters - BuildExecuteSql(CommandBehavior.Default, commandText, batchCommand.Parameters, ref rpc); - } - - _RPCList.Add(rpc); - - ReliablePutStateObject(); - } - - internal int? GetRecordsAffected(int commandIndex) - { - Debug.Assert(_batchRPCMode, "Command is not in batch RPC Mode"); - Debug.Assert(_RPCList != null, "batch command have been cleared"); - return _RPCList[commandIndex].recordsAffected; - } - - internal SqlBatchCommand GetCurrentBatchCommand() - { - if (_batchRPCMode) - { - return _RPCList[_currentlyExecutingBatch].batchCommand; - } - else - { - return _rpcArrayOf1?[0].batchCommand; - } - } - - internal SqlBatchCommand GetBatchCommand(int index) - { - return _RPCList[index].batchCommand; - } - - internal int GetCurrentBatchIndex() - { - return _batchRPCMode ? _currentlyExecutingBatch : -1; - } - - internal SqlException GetErrors(int commandIndex) - { - SqlException result = null; - _SqlRPC rpc = _RPCList[commandIndex]; - int length = (rpc.errorsIndexEnd - rpc.errorsIndexStart); - if (0 < length) - { - SqlErrorCollection errors = new SqlErrorCollection(); - for (int i = rpc.errorsIndexStart; i < rpc.errorsIndexEnd; ++i) - { - errors.Add(rpc.errors[i]); - } - for (int i = rpc.warningsIndexStart; i < rpc.warningsIndexEnd; ++i) - { - errors.Add(rpc.warnings[i]); - } - result = SqlException.CreateException(errors, Connection.ServerVersion, Connection.ClientConnectionId, innerException: null, batchCommand: null); - } - return result; - } - - private static void CancelIgnoreFailureCallback(object state) - { - SqlCommand command = (SqlCommand)state; - command.CancelIgnoreFailure(); - } - - private void CancelIgnoreFailure() - { - // This method is used to route CancellationTokens to the Cancel method. - // Cancellation is a suggestion, and exceptions should be ignored - // rather than allowed to be unhandled, as there is no way to route - // them to the caller. It would be expected that the error will be - // observed anyway from the regular method. An example is cancelling - // an operation on a closed connection. - try - { - Cancel(); - } - catch (Exception) - { - } - } - - private void NotifyDependency() - { - if (_sqlDep != null) - { - _sqlDep.StartTimer(Notification); - } - } - - private void WriteBeginExecuteEvent() - { - SqlClientEventSource.Log.TryBeginExecuteEvent(ObjectID, Connection?.DataSource, Connection?.Database, CommandText, Connection?.ClientConnectionId); - } - - /// - /// Writes and end execute event in Event Source. - /// - /// True if SQL command finished successfully, otherwise false. - /// Gets a number that identifies the type of error. - /// True if SQL command was executed synchronously, otherwise false. - private void WriteEndExecuteEvent(bool success, int? sqlExceptionNumber, bool synchronous) - { - if (SqlClientEventSource.Log.IsExecutionTraceEnabled()) - { - // SqlEventSource.WriteEvent(int, int, int, int) is faster than provided overload SqlEventSource.WriteEvent(int, object[]). - // that's why trying to fit several booleans in one integer value - - // success state is stored the first bit in compositeState 0x01 - int successFlag = success ? 1 : 0; - - // isSqlException is stored in the 2nd bit in compositeState 0x100 - int isSqlExceptionFlag = sqlExceptionNumber.HasValue ? 2 : 0; - - // synchronous state is stored in the second bit in compositeState 0x10 - int synchronousFlag = synchronous ? 4 : 0; - - int compositeState = successFlag | isSqlExceptionFlag | synchronousFlag; - - SqlClientEventSource.Log.TryEndExecuteEvent(ObjectID, compositeState, sqlExceptionNumber.GetValueOrDefault(), Connection?.ClientConnectionId); - } - } - } -} 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 5be11f71a7..bc258f4ac1 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj @@ -756,6 +756,9 @@ Microsoft\Data\SqlClient\SqlCommand.cs + + Microsoft\Data\SqlClient\SqlCommand.Batch.cs + Microsoft\Data\SqlClient\SqlCommand.Encryption.cs @@ -1014,8 +1017,7 @@ System\Runtime\CompilerServices\IsExternalInit.netfx.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 deleted file mode 100644 index 0763c1b3f2..0000000000 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs +++ /dev/null @@ -1,1791 +0,0 @@ -// 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.Collections.Generic; -using System.Collections.ObjectModel; -using System.Data; -using System.Data.Common; -using System.Data.SqlTypes; -using System.Diagnostics; -using System.IO; -using System.Runtime.CompilerServices; -using System.Runtime.ConstrainedExecution; -using System.Security.Permissions; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Data.Common; -using System.Collections.Concurrent; - -// NOTE: The current Microsoft.VSDesigner editor attributes are implemented for System.Data.SqlClient, and are not publicly available. -// New attributes that are designed to work with Microsoft.Data.SqlClient and are publicly documented should be included in future. -namespace Microsoft.Data.SqlClient -{ - // TODO: Add designer attribute when Microsoft.VSDesigner.Data.VS.SqlCommandDesigner uses Microsoft.Data.SqlClient - public sealed partial class SqlCommand : DbCommand, ICloneable - { - private const int MaxRPCNameLength = 1046; - - internal static readonly Action s_cancelIgnoreFailure = CancelIgnoreFailureCallback; - - private _SqlRPC[] _rpcArrayOf1 = null; // Used for RPC executes - - // cut down on object creation and cache all these - // cached metadata - private _SqlMetaDataSet _cachedMetaData; - - // Last TaskCompletionSource for reconnect task - use for cancellation only - private TaskCompletionSource _reconnectionCompletionSource = null; - -#if DEBUG - internal static int DebugForceAsyncWriteDelay { get; set; } -#endif - - // Cached info for async executions - private sealed class AsyncState - { - // @TODO: Autoproperties - private int _cachedAsyncCloseCount = -1; // value of the connection's CloseCount property when the asyncResult was set; tracks when connections are closed after an async operation - private TaskCompletionSource _cachedAsyncResult = null; - private SqlConnection _cachedAsyncConnection = null; // Used to validate that the connection hasn't changed when end the connection; - private SqlDataReader _cachedAsyncReader = null; - private RunBehavior _cachedRunBehavior = RunBehavior.ReturnImmediately; - private string _cachedSetOptions = null; - private string _cachedEndMethod = null; - - internal AsyncState() - { - } - - internal SqlDataReader CachedAsyncReader - { - get { return _cachedAsyncReader; } - } - internal RunBehavior CachedRunBehavior - { - get { return _cachedRunBehavior; } - } - internal string CachedSetOptions - { - get { return _cachedSetOptions; } - } - internal bool PendingAsyncOperation - { - get { return _cachedAsyncResult != null; } - } - internal string EndMethodName - { - get { return _cachedEndMethod; } - } - - internal bool IsActiveConnectionValid(SqlConnection activeConnection) - { - return (_cachedAsyncConnection == activeConnection && _cachedAsyncCloseCount == activeConnection.CloseCount); - } - - internal void ResetAsyncState() - { - SqlClientEventSource.Log.TryTraceEvent("CachedAsyncState.ResetAsyncState | API | ObjectId {0}, Client Connection Id {1}, AsyncCommandInProgress={2}", - _cachedAsyncConnection?.ObjectID, _cachedAsyncConnection?.ClientConnectionId, _cachedAsyncConnection?.AsyncCommandInProgress); - _cachedAsyncCloseCount = -1; - _cachedAsyncResult = null; - if (_cachedAsyncConnection != null) - { - _cachedAsyncConnection.AsyncCommandInProgress = false; - _cachedAsyncConnection = null; - } - _cachedAsyncReader = null; - _cachedRunBehavior = RunBehavior.ReturnImmediately; - _cachedSetOptions = null; - _cachedEndMethod = null; - } - - internal void SetActiveConnectionAndResult(TaskCompletionSource completion, string endMethod, SqlConnection activeConnection) - { - Debug.Assert(activeConnection != null, "Unexpected null connection argument on SetActiveConnectionAndResult!"); - TdsParser parser = activeConnection.Parser; - SqlClientEventSource.Log.TryTraceEvent("SqlCommand.SetActiveConnectionAndResult | API | ObjectId {0}, Client Connection Id {1}, MARS={2}", activeConnection?.ObjectID, activeConnection?.ClientConnectionId, parser?.MARSOn); - if ((parser == null) || (parser.State == TdsParserState.Closed) || (parser.State == TdsParserState.Broken)) - { - throw ADP.ClosedConnectionError(); - } - - _cachedAsyncCloseCount = activeConnection.CloseCount; - _cachedAsyncResult = completion; - if (activeConnection != null && !parser.MARSOn) - { - if (activeConnection.AsyncCommandInProgress) - { - throw SQL.MARSUnsupportedOnConnection(); - } - } - _cachedAsyncConnection = activeConnection; - - // Should only be needed for non-MARS, but set anyways. - _cachedAsyncConnection.AsyncCommandInProgress = true; - _cachedEndMethod = endMethod; - } - - internal void SetAsyncReaderState(SqlDataReader ds, RunBehavior runBehavior, string optionSettings) - { - _cachedAsyncReader = ds; - _cachedRunBehavior = runBehavior; - _cachedSetOptions = optionSettings; - } - } - - private AsyncState _cachedAsyncState = null; - - // @TODO: This is never null, so we can remove the null checks from usages of it. - private AsyncState CachedAsyncState - { - get - { - _cachedAsyncState ??= new AsyncState(); - return _cachedAsyncState; - } - } - - private List<_SqlRPC> _RPCList; - private int _currentlyExecutingBatch; - - /// - /// A flag to indicate if EndExecute was already initiated by the Begin call. - /// - private volatile bool _internalEndExecuteInitiated; - - /// - /// A flag to indicate whether we postponed caching the query metadata for this command. - /// - internal bool CachingQueryMetadataPostponed { get; set; } - - private bool IsProviderRetriable => SqlConfigurableRetryFactory.IsRetriable(RetryLogicProvider); - - internal void OnStatementCompleted(int recordCount) - { - if (0 <= recordCount) - { - StatementCompletedEventHandler handler = _statementCompletedEventHandler; - if (handler != null) - { - try - { - SqlClientEventSource.Log.TryTraceEvent("SqlCommand.OnStatementCompleted | Info | ObjectId {0}, Record Count {1}, Client Connection Id {2}", ObjectID, recordCount, Connection?.ClientConnectionId); - handler(this, new StatementCompletedEventArgs(recordCount)); - } - catch (Exception e) - { - if (!ADP.IsCatchableOrSecurityExceptionType(e)) - { - throw; - } - - ADP.TraceExceptionWithoutRethrow(e); - } - } - } - } - - private void VerifyEndExecuteState(Task completionTask, string endMethod, bool fullCheckForColumnEncryption = false) - { - Debug.Assert(completionTask != null); - SqlClientEventSource.Log.TryTraceEvent("SqlCommand.VerifyEndExecuteState | API | ObjectId {0}, Client Connection Id {1}, MARS={2}, AsyncCommandInProgress={3}", - _activeConnection?.ObjectID, _activeConnection?.ClientConnectionId, - _activeConnection?.Parser?.MARSOn, _activeConnection?.AsyncCommandInProgress); - - if (completionTask.IsCanceled) - { - if (_stateObj != null) - { - _stateObj.Parser.State = TdsParserState.Broken; // We failed to respond to attention, we have to quit! - _stateObj.Parser.Connection.BreakConnection(); - _stateObj.Parser.ThrowExceptionAndWarning(_stateObj, this); - } - else - { - Debug.Assert(_reconnectionCompletionSource == null || _reconnectionCompletionSource.Task.IsCanceled, "ReconnectCompletionSource should be null or cancelled"); - throw SQL.CR_ReconnectionCancelled(); - } - } - else if (completionTask.IsFaulted) - { - throw completionTask.Exception.InnerException; - } - - // If transparent parameter encryption was attempted, then we need to skip other checks like those on EndMethodName - // since we want to wait for async results before checking those fields. - if (IsColumnEncryptionEnabled && !fullCheckForColumnEncryption) - { - if (_activeConnection.State != ConnectionState.Open) - { - // If the connection is not 'valid' then it was closed while we were executing - throw ADP.ClosedConnectionError(); - } - - return; - } - - if (CachedAsyncState.EndMethodName == null) - { - throw ADP.MethodCalledTwice(endMethod); - } - if (endMethod != CachedAsyncState.EndMethodName) - { - throw ADP.MismatchedAsyncResult(CachedAsyncState.EndMethodName, endMethod); - } - if ((_activeConnection.State != ConnectionState.Open) || (!CachedAsyncState.IsActiveConnectionValid(_activeConnection))) - { - // If the connection is not 'valid' then it was closed while we were executing - throw ADP.ClosedConnectionError(); - } - } - - private void WaitForAsyncResults(IAsyncResult asyncResult, bool isInternal) - { - Task completionTask = (Task)asyncResult; - if (!asyncResult.IsCompleted) - { - asyncResult.AsyncWaitHandle.WaitOne(); - } - - if (_stateObj != null) - { - _stateObj._networkPacketTaskSource = null; - } - - // If this is an internal command we will decrement the count when the End method is actually called by the user. - // If we are using Column Encryption and the previous task failed, the async count should have already been fixed up. - // There is a generic issue in how we handle the async count because: - // a) BeginExecute might or might not clean it up on failure. - // b) In EndExecute, we check the task state before waiting and throw if it's failed, whereas if we wait we will always adjust the count. - if (!isInternal && (!IsColumnEncryptionEnabled || !completionTask.IsFaulted)) - { - _activeConnection.GetOpenTdsConnection().DecrementAsyncCount(); - } - } - - private void ThrowIfReconnectionHasBeenCanceled() - { - if (_stateObj == null) - { - var reconnectionCompletionSource = _reconnectionCompletionSource; - if (reconnectionCompletionSource != null && reconnectionCompletionSource.Task.IsCanceled) - { - throw SQL.CR_ReconnectionCancelled(); - } - } - } - - private bool TriggerInternalEndAndRetryIfNecessary( - CommandBehavior behavior, - object stateObject, - int timeout, - bool usedCache, - bool isRetry, - bool asyncWrite, - TaskCompletionSource globalCompletion, - TaskCompletionSource localCompletion, - Func endFunc, - Func retryFunc, - string endMethod) - { - // We shouldn't be using the cache if we are in retry. - Debug.Assert(!usedCache || !isRetry); - - // If column encryption is enabled and we used the cache, we want to catch any potential exceptions that were caused by the query cache and retry if the error indicates that we should. - // So, try to read the result of the query before completing the overall task and trigger a retry if appropriate. - if ((IsColumnEncryptionEnabled && !isRetry && (usedCache || ShouldUseEnclaveBasedWorkflow)) -#if DEBUG - || _forceInternalEndQuery -#endif - ) - { - long firstAttemptStart = ADP.TimerCurrent(); - - localCompletion.Task.ContinueWith(tsk => - { - if (tsk.IsFaulted) - { - globalCompletion.TrySetException(tsk.Exception.InnerException); - } - else if (tsk.IsCanceled) - { - globalCompletion.TrySetCanceled(); - } - else - { - try - { - // Mark that we initiated the internal EndExecute. This should always be false until we set it here. - Debug.Assert(!_internalEndExecuteInitiated); - _internalEndExecuteInitiated = true; - - // lock on _stateObj prevents races with close/cancel. - lock (_stateObj) - { - endFunc(this, tsk, /*isInternal:*/ true, endMethod); - } - globalCompletion.TrySetResult(tsk.Result); - } - catch (Exception e) - { - // Put the state object back to the cache. - // Do not reset the async state, since this is managed by the user Begin/End and not internally. - if (ADP.IsCatchableExceptionType(e)) - { - ReliablePutStateObject(); - } - - bool shouldRetry = e is EnclaveDelegate.RetryableEnclaveQueryExecutionException; - - // Check if we have an error indicating that we can retry. - if (e is SqlException) - { - SqlException sqlEx = e as SqlException; - - for (int i = 0; i < sqlEx.Errors.Count; i++) - { - if ((usedCache && (sqlEx.Errors[i].Number == TdsEnums.TCE_CONVERSION_ERROR_CLIENT_RETRY)) || - (ShouldUseEnclaveBasedWorkflow && (sqlEx.Errors[i].Number == TdsEnums.TCE_ENCLAVE_INVALID_SESSION_HANDLE))) - { - shouldRetry = true; - break; - } - } - } - - if (!shouldRetry) - { - // If we cannot retry, Reset the async state to make sure we leave a clean state. - if (CachedAsyncState != null) - { - CachedAsyncState.ResetAsyncState(); - } - try - { - _activeConnection.GetOpenTdsConnection().DecrementAsyncCount(); - - globalCompletion.TrySetException(e); - } - catch (Exception e2) - { - globalCompletion.TrySetException(e2); - } - } - else - { - // Remove the entry from the cache since it was inconsistent. - SqlQueryMetadataCache.GetInstance().InvalidateCacheEntry(this); - - InvalidateEnclaveSession(); - - try - { - // Kick off the retry. - _internalEndExecuteInitiated = false; - Task retryTask = (Task)retryFunc( - this, - behavior, - null, - stateObject, - TdsParserStaticMethods.GetRemainingTimeout(timeout, firstAttemptStart), - /*isRetry:*/ true, - asyncWrite); - - retryTask.ContinueWith( - static (Task retryTask, object state) => - { - TaskCompletionSource completion = (TaskCompletionSource)state; - if (retryTask.IsFaulted) - { - completion.TrySetException(retryTask.Exception.InnerException); - } - else if (retryTask.IsCanceled) - { - completion.TrySetCanceled(); - } - else - { - completion.TrySetResult(retryTask.Result); - } - }, - state: globalCompletion, - TaskScheduler.Default - ); - } - catch (Exception e2) - { - globalCompletion.TrySetException(e2); - } - } - } - } - }, TaskScheduler.Default); - - return true; - } - else - { - return false; - } - } - - // If the user part is quoted, remove first and last brackets and then unquote any right square - // brackets in the procedure. This is a very simple parser that performs no validation. As - // with the function below, ideally we should have support from the server for this. - private static string UnquoteProcedurePart(string part) - { - if (part != null && (2 <= part.Length)) - { - if ('[' == part[0] && ']' == part[part.Length - 1]) - { - part = part.Substring(1, part.Length - 2); // strip outer '[' & ']' - part = part.Replace("]]", "]"); // undo quoted "]" from "]]" to "]" - } - } - return part; - } - - // User value in this format: [server].[database].[schema].[sp_foo];1 - // This function should only be passed "[sp_foo];1". - // This function uses a pretty simple parser that doesn't do any validation. - // Ideally, we would have support from the server rather than us having to do this. - private static string UnquoteProcedureName(string name, out object groupNumber) - { - groupNumber = null; // Out param - initialize value to no value. - string sproc = name; - - if (sproc != null) - { - if (char.IsDigit(sproc[sproc.Length - 1])) - { - // If last char is a digit, parse. - int semicolon = sproc.LastIndexOf(';'); - if (semicolon != -1) - { - // If we found a semicolon, obtain the integer. - string part = sproc.Substring(semicolon + 1); - int number = 0; - if (int.TryParse(part, out number)) - { - // No checking, just fail if this doesn't work. - groupNumber = number; - sproc = sproc.Substring(0, semicolon); - } - } - } - sproc = UnquoteProcedurePart(sproc); - } - return sproc; - } - - // Index into indirection arrays for columns of interest to DeriveParameters - private enum ProcParamsColIndex - { - ParameterName = 0, - ParameterType, - DataType, // obsolete in 2008, use ManagedDataType instead - ManagedDataType, // new in 2008 - CharacterMaximumLength, - NumericPrecision, - NumericScale, - TypeCatalogName, - TypeSchemaName, - TypeName, - XmlSchemaCollectionCatalogName, - XmlSchemaCollectionSchemaName, - XmlSchemaCollectionName, - UdtTypeName, // obsolete in 2008. Holds the actual typename if UDT, since TypeName didn't back then. - DateTimeScale // new in 2008 - }; - - // 2005- column ordinals (this array indexed by ProcParamsColIndex - internal static readonly string[] PreSql2008ProcParamsNames = new string[] { - "PARAMETER_NAME", // ParameterName, - "PARAMETER_TYPE", // ParameterType, - "DATA_TYPE", // DataType - null, // ManagedDataType, introduced in 2008 - "CHARACTER_MAXIMUM_LENGTH", // CharacterMaximumLength, - "NUMERIC_PRECISION", // NumericPrecision, - "NUMERIC_SCALE", // NumericScale, - "UDT_CATALOG", // TypeCatalogName, - "UDT_SCHEMA", // TypeSchemaName, - "TYPE_NAME", // TypeName, - "XML_CATALOGNAME", // XmlSchemaCollectionCatalogName, - "XML_SCHEMANAME", // XmlSchemaCollectionSchemaName, - "XML_SCHEMACOLLECTIONNAME", // XmlSchemaCollectionName - "UDT_NAME", // UdtTypeName - null, // Scale for datetime types with scale, introduced in 2008 - }; - - // 2008+ column ordinals (this array indexed by ProcParamsColIndex - internal static readonly string[] Sql2008ProcParamsNames = new string[] { - "PARAMETER_NAME", // ParameterName, - "PARAMETER_TYPE", // ParameterType, - null, // DataType, removed from 2008+ - "MANAGED_DATA_TYPE", // ManagedDataType, - "CHARACTER_MAXIMUM_LENGTH", // CharacterMaximumLength, - "NUMERIC_PRECISION", // NumericPrecision, - "NUMERIC_SCALE", // NumericScale, - "TYPE_CATALOG_NAME", // TypeCatalogName, - "TYPE_SCHEMA_NAME", // TypeSchemaName, - "TYPE_NAME", // TypeName, - "XML_CATALOGNAME", // XmlSchemaCollectionCatalogName, - "XML_SCHEMANAME", // XmlSchemaCollectionSchemaName, - "XML_SCHEMACOLLECTIONNAME", // XmlSchemaCollectionName - null, // UdtTypeName, removed from 2008+ - "SS_DATETIME_PRECISION", // Scale for datetime types with scale - }; - - internal void DeriveParameters() - { - switch (CommandType) - { - case CommandType.Text: - throw ADP.DeriveParametersNotSupported(this); - case CommandType.StoredProcedure: - break; - case CommandType.TableDirect: - // CommandType.TableDirect - do nothing, parameters are not supported - throw ADP.DeriveParametersNotSupported(this); - default: - throw ADP.InvalidCommandType(CommandType); - } - - // validate that we have a valid connection - ValidateCommand(isAsync: false); - - // Use common parser for SqlClient and OleDb - parse into 4 parts - Server, Catalog, Schema, ProcedureName - string[] parsedSProc = MultipartIdentifier.ParseMultipartIdentifier(CommandText, "[\"", "]\"", Strings.SQL_SqlCommandCommandText, false); - if (string.IsNullOrEmpty(parsedSProc[3])) - { - throw ADP.NoStoredProcedureExists(CommandText); - } - - Debug.Assert(parsedSProc.Length == 4, "Invalid array length result from SqlCommandBuilder.ParseProcedureName"); - - SqlCommand paramsCmd = null; - StringBuilder cmdText = new StringBuilder(); - - // Build call for sp_procedure_params_rowset built of unquoted values from user: - // [user server, if provided].[user catalog, else current database].[sys if 2005, else blank].[sp_procedure_params_rowset] - - // Server - pass only if user provided. - if (!string.IsNullOrEmpty(parsedSProc[0])) - { - SqlCommandSet.BuildStoredProcedureName(cmdText, parsedSProc[0]); - cmdText.Append("."); - } - - // Catalog - pass user provided, otherwise use current database. - if (string.IsNullOrEmpty(parsedSProc[1])) - { - parsedSProc[1] = Connection.Database; - } - SqlCommandSet.BuildStoredProcedureName(cmdText, parsedSProc[1]); - cmdText.Append("."); - - // Schema - only if 2005, and then only pass sys. Also - pass managed version of sproc - // for 2005, else older sproc. - string[] colNames; - bool useManagedDataType; - if (Connection.Is2008OrNewer) - { - // Procedure - [sp_procedure_params_managed] - cmdText.Append("[sys].[").Append(TdsEnums.SP_PARAMS_MGD10).Append("]"); - - colNames = Sql2008ProcParamsNames; - useManagedDataType = true; - } - else - { - // Procedure - [sp_procedure_params_managed] - cmdText.Append("[sys].[").Append(TdsEnums.SP_PARAMS_MANAGED).Append("]"); - - colNames = PreSql2008ProcParamsNames; - useManagedDataType = false; - } - - - paramsCmd = new SqlCommand(cmdText.ToString(), Connection, Transaction) - { - CommandType = CommandType.StoredProcedure - }; - - object groupNumber; - - // Prepare parameters for sp_procedure_params_rowset: - // 1) procedure name - unquote user value - // 2) group number - parsed at the time we unquoted procedure name - // 3) procedure schema - unquote user value - - paramsCmd.Parameters.Add(new SqlParameter("@procedure_name", SqlDbType.NVarChar, 255)); - paramsCmd.Parameters[0].Value = UnquoteProcedureName(parsedSProc[3], out groupNumber); // ProcedureName is 4rd element in parsed array - - if (groupNumber != null) - { - SqlParameter param = paramsCmd.Parameters.Add(new SqlParameter("@group_number", SqlDbType.Int)); - param.Value = groupNumber; - } - - if (!string.IsNullOrEmpty(parsedSProc[2])) - { - // SchemaName is 3rd element in parsed array - SqlParameter param = paramsCmd.Parameters.Add(new SqlParameter("@procedure_schema", SqlDbType.NVarChar, 255)); - param.Value = UnquoteProcedurePart(parsedSProc[2]); - } - - SqlDataReader r = null; - - List parameters = new List(); - bool processFinallyBlock = true; - - try - { - r = paramsCmd.ExecuteReader(); - - SqlParameter p = null; - - while (r.Read()) - { - // each row corresponds to a parameter of the stored proc. Fill in all the info - p = new SqlParameter() - { - ParameterName = (string)r[colNames[(int)ProcParamsColIndex.ParameterName]] - }; - - // type - if (useManagedDataType) - { - p.SqlDbType = (SqlDbType)(short)r[colNames[(int)ProcParamsColIndex.ManagedDataType]]; - - // 2005 didn't have as accurate of information as we're getting for 2008, so re-map a couple of - // types for backward compatability. - switch (p.SqlDbType) - { - case SqlDbType.Image: - case SqlDbType.Timestamp: - p.SqlDbType = SqlDbType.VarBinary; - break; - - case SqlDbType.NText: - p.SqlDbType = SqlDbType.NVarChar; - break; - - case SqlDbType.Text: - p.SqlDbType = SqlDbType.VarChar; - break; - - default: - break; - } - } - else - { - p.SqlDbType = MetaType.GetSqlDbTypeFromOleDbType((short)r[colNames[(int)ProcParamsColIndex.DataType]], - ADP.IsNull(r[colNames[(int)ProcParamsColIndex.TypeName]]) ? "" : - (string)r[colNames[(int)ProcParamsColIndex.TypeName]]); - } - - // size - object a = r[colNames[(int)ProcParamsColIndex.CharacterMaximumLength]]; - if (a is int) - { - int size = (int)a; - - // Map MAX sizes correctly. The 2008 server-side proc sends 0 for these instead of -1. - // Should be fixed on the 2008 side, but would likely hold up the RI, and is safer to fix here. - // If we can get the server-side fixed before shipping 2008, we can remove this mapping. - if (0 == size && - (p.SqlDbType == SqlDbType.NVarChar || - p.SqlDbType == SqlDbType.VarBinary || - p.SqlDbType == SqlDbType.VarChar)) - { - size = -1; - } - p.Size = size; - } - - // direction - p.Direction = ParameterDirectionFromOleDbDirection((short)r[colNames[(int)ProcParamsColIndex.ParameterType]]); - - if (p.SqlDbType == SqlDbType.Decimal) - { - p.ScaleInternal = (byte)((short)r[colNames[(int)ProcParamsColIndex.NumericScale]] & 0xff); - p.PrecisionInternal = (byte)((short)r[colNames[(int)ProcParamsColIndex.NumericPrecision]] & 0xff); - } - - // type name for Udt - if (SqlDbType.Udt == p.SqlDbType) - { - string udtTypeName; - if (useManagedDataType) - { - udtTypeName = (string)r[colNames[(int)ProcParamsColIndex.TypeName]]; - } - else - { - udtTypeName = (string)r[colNames[(int)ProcParamsColIndex.UdtTypeName]]; - } - - //read the type name - p.UdtTypeName = r[colNames[(int)ProcParamsColIndex.TypeCatalogName]] + "." + - r[colNames[(int)ProcParamsColIndex.TypeSchemaName]] + "." + - udtTypeName; - } - - // type name for Structured types (same as for Udt's except assign p.TypeName instead of p.UdtTypeName - if (SqlDbType.Structured == p.SqlDbType) - { - Debug.Assert(_activeConnection.Is2008OrNewer, "Invalid datatype token received from pre-2008 server"); - - //read the type name - p.TypeName = r[colNames[(int)ProcParamsColIndex.TypeCatalogName]] + "." + - r[colNames[(int)ProcParamsColIndex.TypeSchemaName]] + "." + - r[colNames[(int)ProcParamsColIndex.TypeName]]; - - // the constructed type name above is incorrectly formatted, it should be a 2 part name not 3 - // for compatibility we can't change this because the bug has existed for a long time and been - // worked around by users, so identify that it is present and catch it later in the execution - // process once users can no longer interact with with the parameter type name - p.IsDerivedParameterTypeName = true; - } - - // XmlSchema name for Xml types - if (SqlDbType.Xml == p.SqlDbType) - { - object value; - - value = r[colNames[(int)ProcParamsColIndex.XmlSchemaCollectionCatalogName]]; - p.XmlSchemaCollectionDatabase = ADP.IsNull(value) ? string.Empty : (string)value; - - value = r[colNames[(int)ProcParamsColIndex.XmlSchemaCollectionSchemaName]]; - p.XmlSchemaCollectionOwningSchema = ADP.IsNull(value) ? string.Empty : (string)value; - - value = r[colNames[(int)ProcParamsColIndex.XmlSchemaCollectionName]]; - p.XmlSchemaCollectionName = ADP.IsNull(value) ? string.Empty : (string)value; - } - - if (MetaType._IsVarTime(p.SqlDbType)) - { - object value = r[colNames[(int)ProcParamsColIndex.DateTimeScale]]; - if (value is int) - { - p.ScaleInternal = (byte)(((int)value) & 0xff); - } - } - - parameters.Add(p); - } - } - catch (Exception e) - { - processFinallyBlock = ADP.IsCatchableExceptionType(e); - throw; - } - finally - { - if (processFinallyBlock) - { - r?.Close(); - - // always unhook the user's connection - paramsCmd.Connection = null; - } - } - - if (parameters.Count == 0) - { - throw ADP.NoStoredProcedureExists(this.CommandText); - } - - Parameters.Clear(); - - foreach (SqlParameter temp in parameters) - { - _parameters.Add(temp); - } - } - - private ParameterDirection ParameterDirectionFromOleDbDirection(short oledbDirection) - { - Debug.Assert(oledbDirection >= 1 && oledbDirection <= 4, "invalid parameter direction from params_rowset!"); - - switch (oledbDirection) - { - case 2: - return ParameterDirection.InputOutput; - case 3: - return ParameterDirection.Output; - case 4: - return ParameterDirection.ReturnValue; - default: - return ParameterDirection.Input; - } - - } - - // get cached metadata - internal _SqlMetaDataSet MetaData - { - get - { - return _cachedMetaData; - } - } - - // Check to see if notifications auto enlistment is turned on. Enlist if so. - private void CheckNotificationStateAndAutoEnlist() - { - // First, if auto-enlist is on, check server version and then obtain context if - // present. If so, auto enlist to the dependency ID given in the context data. - if (NotificationAutoEnlist) - { - string notifyContext = SqlNotificationContext(); - if (!string.IsNullOrEmpty(notifyContext)) - { - // Map to dependency by ID set in context data. - SqlDependency dependency = SqlDependencyPerAppDomainDispatcher.SingletonInstance.LookupDependencyEntry(notifyContext); - - if (dependency != null) - { - // Add this command to the dependency. - dependency.AddCommandDependency(this); - } - } - } - - // If we have a notification with a dependency, setup the notification options at this time. - - // If user passes options, then we will always have option data at the time the SqlDependency - // ctor is called. But, if we are using default queue, then we do not have this data until - // Start(). Due to this, we always delay setting options until execute. - - // There is a variance in order between Start(), SqlDependency(), and Execute. This is the - // best way to solve that problem. - if (Notification != null) - { - if (_sqlDep != null) - { - if (_sqlDep.Options == null) - { - // If null, SqlDependency was not created with options, so we need to obtain default options now. - // GetDefaultOptions can and will throw under certain conditions. - - // In order to match to the appropriate start - we need 3 pieces of info: - // 1) server 2) user identity (SQL Auth or Int Sec) 3) database - - SqlDependency.IdentityUserNamePair identityUserName = null; - - // Obtain identity from connection. - SqlInternalConnectionTds internalConnection = _activeConnection.InnerConnection as SqlInternalConnectionTds; - if (internalConnection.Identity != null) - { - identityUserName = new SqlDependency.IdentityUserNamePair(internalConnection.Identity, null); - } - else - { - identityUserName = new SqlDependency.IdentityUserNamePair(null, internalConnection.ConnectionOptions.UserID); - } - - Notification.Options = SqlDependency.GetDefaultComposedOptions(_activeConnection.DataSource, - InternalTdsConnection.ServerProvidedFailoverPartner, - identityUserName, _activeConnection.Database); - } - - // Set UserData on notifications, as well as adding to the appdomain dispatcher. The value is - // computed by an algorithm on the dependency - fixed and will always produce the same value - // given identical commandtext + parameter values. - Notification.UserData = _sqlDep.ComputeHashAndAddToDispatcher(this); - // Maintain server list for SqlDependency. - _sqlDep.AddToServerList(_activeConnection.DataSource); - } - } - } - - [System.Security.Permissions.SecurityPermission(SecurityAction.Assert, Infrastructure = true)] - static internal string SqlNotificationContext() - { - SqlConnection.VerifyExecutePermission(); - - // since this information is protected, follow it so that it is not exposed to the user. - // SQLBU 329633, SQLBU 329637 - return (System.Runtime.Remoting.Messaging.CallContext.GetData("MS.SqlDependencyCookie") as string); - } - - private Task RegisterForConnectionCloseNotification(Task outterTask) - { - SqlConnection connection = _activeConnection; - if (connection == null) - { - // No connection - throw ADP.ClosedConnectionError(); - } - - return connection.RegisterForConnectionCloseNotification(outterTask, this, SqlReferenceCollection.CommandTag); - } - - // validates that a command has commandText and a non-busy open connection - // throws exception for error case, returns false if the commandText is empty - private void ValidateCommand(bool isAsync, [CallerMemberName] string method = "") - { - if (_activeConnection == null) - { - throw ADP.ConnectionRequired(method); - } - - // Ensure that the connection is open and that the Parser is in the correct state - SqlInternalConnectionTds tdsConnection = _activeConnection.InnerConnection as SqlInternalConnectionTds; - - // Ensure that if column encryption override was used then server supports its - if (((SqlCommandColumnEncryptionSetting.UseConnectionSetting == ColumnEncryptionSetting && _activeConnection.IsColumnEncryptionSettingEnabled) - || (ColumnEncryptionSetting == SqlCommandColumnEncryptionSetting.Enabled || ColumnEncryptionSetting == SqlCommandColumnEncryptionSetting.ResultSetOnly)) - && tdsConnection != null - && tdsConnection.Parser != null - && !tdsConnection.Parser.IsColumnEncryptionSupported) - { - throw SQL.TceNotSupported(); - } - - if (tdsConnection != null) - { - var parser = tdsConnection.Parser; - if ((parser == null) || (parser.State == TdsParserState.Closed)) - { - throw ADP.OpenConnectionRequired(method, ConnectionState.Closed); - } - else if (parser.State != TdsParserState.OpenLoggedIn) - { - throw ADP.OpenConnectionRequired(method, ConnectionState.Broken); - } - } - else if (_activeConnection.State == ConnectionState.Closed) - { - throw ADP.OpenConnectionRequired(method, ConnectionState.Closed); - } - else if (_activeConnection.State == ConnectionState.Broken) - { - throw ADP.OpenConnectionRequired(method, ConnectionState.Broken); - } - - ValidateAsyncCommand(); - - // close any non MARS dead readers, if applicable, and then throw if still busy. - // Throw if we have a live reader on this command - _activeConnection.ValidateConnectionForExecute(method, this); - // @TODO: CER Exception Handling was removed here (see GH#3581) - - // Check to see if the currently set transaction has completed. If so, - // null out our local reference. - if (_transaction != null && _transaction.Connection == null) - { - _transaction = null; - } - - // throw if the connection is in a transaction but there is no - // locally assigned transaction object - if (_activeConnection.HasLocalTransactionFromAPI && _transaction == null) - { - throw ADP.TransactionRequired(method); - } - - // if we have a transaction, check to ensure that the active - // connection property matches the connection associated with - // the transaction - if (_transaction != null && _activeConnection != _transaction.Connection) - { - throw ADP.TransactionConnectionMismatch(); - } - - if (string.IsNullOrEmpty(this.CommandText)) - { - throw ADP.CommandTextRequired(method); - } - } - - private void ValidateAsyncCommand() - { - if (CachedAsyncState.PendingAsyncOperation) - { - // Enforce only one pending async execute at a time. - if (CachedAsyncState.IsActiveConnectionValid(_activeConnection)) - { - throw SQL.PendingBeginXXXExists(); - } - else - { - _stateObj = null; // Session was re-claimed by session pool upon connection close. - CachedAsyncState.ResetAsyncState(); - } - } - } - - private void GetStateObject(TdsParser parser = null) - { - Debug.Assert(_stateObj == null, "StateObject not null on GetStateObject"); - Debug.Assert(_activeConnection != null, "no active connection?"); - - if (_pendingCancel) - { - _pendingCancel = false; // Not really needed, but we'll reset anyways. - - // If a pendingCancel exists on the object, we must have had a Cancel() call - // between the point that we entered an Execute* API and the point in Execute* that - // we proceeded to call this function and obtain a stateObject. In that case, - // we now throw a cancelled error. - throw SQL.OperationCancelled(); - } - - if (parser == null) - { - parser = _activeConnection.Parser; - if ((parser == null) || (parser.State == TdsParserState.Broken) || (parser.State == TdsParserState.Closed)) - { - // Connection's parser is null as well, therefore we must be closed - throw ADP.ClosedConnectionError(); - } - } - - TdsParserStateObject stateObj = parser.GetSession(this); - stateObj.StartSession(this); - - _stateObj = stateObj; - - if (_pendingCancel) - { - _pendingCancel = false; // Not really needed, but we'll reset anyways. - - // If a pendingCancel exists on the object, we must have had a Cancel() call - // between the point that we entered this function and the point where we obtained - // and actually assigned the stateObject to the local member. It is possible - // that the flag is set as well as a call to stateObj.Cancel - though that would - // be a no-op. So - throw. - throw SQL.OperationCancelled(); - } - } - - private void ReliablePutStateObject() - { - PutStateObject(); - // @TODO: CER Exception Handling was removed here (see GH#3581) - } - - private void PutStateObject() - { - TdsParserStateObject stateObj = _stateObj; - _stateObj = null; - - if (stateObj != null) - { - stateObj.CloseSession(); - } - } - - private SqlParameterCollection GetCurrentParameterCollection() - { - if (_batchRPCMode) - { - if (_RPCList.Count > _currentlyExecutingBatch) - { - return _RPCList[_currentlyExecutingBatch].userParams; - } - else - { - Debug.Fail("OnReturnValue: SqlCommand got too many DONEPROC events"); - return null; - } - } - else - { - return _parameters; - } - } - - private SqlParameter GetParameterForOutputValueExtraction(SqlParameterCollection parameters, - string paramName, int paramCount) - { - SqlParameter thisParam = null; - bool foundParam = false; - - if (paramName == null) - { - // rec.parameter should only be null for a return value from a function - for (int i = 0; i < paramCount; i++) - { - thisParam = parameters[i]; - // searching for ReturnValue - if (thisParam.Direction == ParameterDirection.ReturnValue) - { - foundParam = true; - break; // found it - } - } - } - else - { - for (int i = 0; i < paramCount; i++) - { - thisParam = parameters[i]; - // searching for Output or InputOutput or ReturnValue with matching name - if ( - thisParam.Direction != ParameterDirection.Input && - thisParam.Direction != ParameterDirection.ReturnValue && - SqlParameter.ParameterNamesEqual(paramName, thisParam.ParameterName, StringComparison.Ordinal) - ) - { - foundParam = true; - break; // found it - } - } - } - - if (foundParam) - { - return thisParam; - } - else - { - return null; - } - } - - private void GetRPCObject(int systemParamCount, int userParamCount, ref _SqlRPC rpc, bool forSpDescribeParameterEncryption = false) - { - // Designed to minimize necessary allocations - if (rpc == null) - { - if (!forSpDescribeParameterEncryption) - { - if (_rpcArrayOf1 == null) - { - _rpcArrayOf1 = new _SqlRPC[1]; - _rpcArrayOf1[0] = new _SqlRPC(); - } - - rpc = _rpcArrayOf1[0]; - } - else - { - if (_rpcForEncryption == null) - { - _rpcForEncryption = new _SqlRPC(); - } - - rpc = _rpcForEncryption; - } - } - - rpc.ProcID = 0; - rpc.rpcName = null; - rpc.options = 0; - rpc.systemParamCount = systemParamCount; - - rpc.recordsAffected = default(int?); - rpc.cumulativeRecordsAffected = -1; - - rpc.errorsIndexStart = 0; - rpc.errorsIndexEnd = 0; - rpc.errors = null; - - rpc.warningsIndexStart = 0; - rpc.warningsIndexEnd = 0; - rpc.warnings = null; - rpc.needsFetchParameterEncryptionMetadata = false; - - int currentCount = rpc.systemParams?.Length ?? 0; - - // Make sure there is enough space in the parameters and paramoptions arrays - if (currentCount < systemParamCount) - { - Array.Resize(ref rpc.systemParams, systemParamCount); - Array.Resize(ref rpc.systemParamOptions, systemParamCount); - for (int index = currentCount; index < systemParamCount; index++) - { - rpc.systemParams[index] = new SqlParameter(); - } - } - - for (int ii = 0; ii < systemParamCount; ii++) - { - rpc.systemParamOptions[ii] = 0; - } - - if ((rpc.userParamMap?.Length ?? 0) < userParamCount) - { - Array.Resize(ref rpc.userParamMap, userParamCount); - } - } - - private void SetUpRPCParameters(_SqlRPC rpc, bool inSchema, SqlParameterCollection parameters) - { - int paramCount = GetParameterCount(parameters); - int userParamCount = 0; - - for (int index = 0; index < paramCount; index++) - { - SqlParameter parameter = parameters[index]; - parameter.Validate(index, CommandType.StoredProcedure == CommandType); - - // func will change type to that with a 4 byte length if the type has a two - // byte length and a parameter length > than that expressible in 2 bytes - if ((!parameter.ValidateTypeLengths().IsPlp) && (parameter.Direction != ParameterDirection.Output)) - { - parameter.FixStreamDataForNonPLP(); - } - - if (ShouldSendParameter(parameter)) - { - byte options = 0; - - // set output bit - if (parameter.Direction == ParameterDirection.InputOutput || parameter.Direction == ParameterDirection.Output) - { - options = TdsEnums.RPC_PARAM_BYREF; - } - - // Set the encryped bit, if the parameter is to be encrypted. - if (parameter.CipherMetadata != null) - { - options |= TdsEnums.RPC_PARAM_ENCRYPTED; - } - - // set default value bit - if (parameter.Direction != ParameterDirection.Output) - { - // remember that Convert.IsEmpty is null, DBNull.Value is a database null! - - // Don't assume a default value exists for parameters in the case when - // the user is simply requesting schema. - // TVPs use DEFAULT and do not allow NULL, even for schema only. - if (parameter.Value == null && (!inSchema || SqlDbType.Structured == parameter.SqlDbType)) - { - options |= TdsEnums.RPC_PARAM_DEFAULT; - } - - // detect incorrectly derived type names unchanged by the caller and fix them - if (parameter.IsDerivedParameterTypeName) - { - string[] parts = MultipartIdentifier.ParseMultipartIdentifier(parameter.TypeName, "[\"", "]\"", Strings.SQL_TDSParserTableName, false); - if (parts != null && parts.Length == 4) // will always return int[4] right justified - { - if ( - parts[3] != null && // name must not be null - parts[2] != null && // schema must not be null - parts[1] != null // server should not be null or we don't need to remove it - ) - { - parameter.TypeName = QuoteIdentifier(parts, 2, 2); - } - } - } - } - - rpc.userParamMap[userParamCount] = ((((long)options) << 32) | (long)index); - userParamCount += 1; - - // Must set parameter option bit for LOB_COOKIE if unfilled LazyMat blob - } - } - - rpc.userParamCount = userParamCount; - rpc.userParams = parameters; - } - - // - // returns true if the parameter is not a return value - // and it's value is not DBNull (for a nullable parameter) - // - private static bool ShouldSendParameter(SqlParameter p, bool includeReturnValue = false) - { - switch (p.Direction) - { - case ParameterDirection.ReturnValue: - // return value parameters are not sent, except for the parameter list of sp_describe_parameter_encryption - return includeReturnValue; - case ParameterDirection.Output: - case ParameterDirection.InputOutput: - case ParameterDirection.Input: - // InputOutput/Output parameters are aways sent - return true; - default: - Debug.Fail("Invalid ParameterDirection!"); - return false; - } - } - - private static int CountSendableParameters(SqlParameterCollection parameters) - { - int cParams = 0; - - if (parameters != null) - { - int count = parameters.Count; - for (int i = 0; i < count; i++) - { - if (ShouldSendParameter(parameters[i])) - { - cParams++; - } - } - } - return cParams; - } - - // Returns total number of parameters - private static int GetParameterCount(SqlParameterCollection parameters) - { - return parameters != null ? parameters.Count : 0; - } - - // paramList parameter for sp_executesql, sp_prepare, and sp_prepexec - internal string BuildParamList(TdsParser parser, SqlParameterCollection parameters, bool includeReturnValue = false) - { - StringBuilder paramList = new StringBuilder(); - bool fAddSeparator = false; - - int count = parameters.Count; - for (int i = 0; i < count; i++) - { - SqlParameter sqlParam = parameters[i]; - sqlParam.Validate(i, CommandType.StoredProcedure == CommandType); - // skip ReturnValue parameters; we never send them to the server - if (!ShouldSendParameter(sqlParam, includeReturnValue)) - { - continue; - } - - // add our separator for the ith parameter - if (fAddSeparator) - { - paramList.Append(','); - } - - SqlParameter.AppendPrefixedParameterName(paramList, sqlParam.ParameterName); - - MetaType mt = sqlParam.InternalMetaType; - - //for UDTs, get the actual type name. Get only the typename, omit catalog and schema names. - //in TSQL you should only specify the unqualified type name - - // paragraph above doesn't seem to be correct. Server won't find the type - // if we don't provide a fully qualified name - paramList.Append(" "); - if (mt.SqlDbType == SqlDbType.Udt) - { - string fullTypeName = sqlParam.UdtTypeName; - if (string.IsNullOrEmpty(fullTypeName)) - { - throw SQL.MustSetUdtTypeNameForUdtParams(); - } - - paramList.Append(ParseAndQuoteIdentifier(fullTypeName, true /* is UdtTypeName */)); - } - else if (mt.SqlDbType == SqlDbType.Structured) - { - string typeName = sqlParam.TypeName; - if (string.IsNullOrEmpty(typeName)) - { - throw SQL.MustSetTypeNameForParam(mt.TypeName, sqlParam.GetPrefixedParameterName()); - } - paramList.Append(ParseAndQuoteIdentifier(typeName, false /* is not UdtTypeName*/)); - - // TVPs currently are the only Structured type and must be read only, so add that keyword - paramList.Append(" READONLY"); - } - else - { - // func will change type to that with a 4 byte length if the type has a two - // byte length and a parameter length > than that expressible in 2 bytes - mt = sqlParam.ValidateTypeLengths(); - if ((!mt.IsPlp) && (sqlParam.Direction != ParameterDirection.Output)) - { - sqlParam.FixStreamDataForNonPLP(); - } - paramList.Append(mt.TypeName); - } - - fAddSeparator = true; - - if (mt.SqlDbType == SqlDbType.Decimal) - { - byte precision = sqlParam.GetActualPrecision(); - byte scale = sqlParam.GetActualScale(); - - paramList.Append('('); - - if (0 == precision) - { - precision = TdsEnums.DEFAULT_NUMERIC_PRECISION; - } - - paramList.Append(precision); - paramList.Append(','); - paramList.Append(scale); - paramList.Append(')'); - } - else if (mt.IsVarTime) - { - byte scale = sqlParam.GetActualScale(); - - paramList.Append('('); - paramList.Append(scale); - paramList.Append(')'); - } - else if (mt.SqlDbType == SqlDbTypeExtensions.Vector) - { - // The validate function for SqlParameters would - // have already thrown InvalidCastException if an incompatible - // value is specified for SqlDbType Vector. - var sqlVectorProps = (ISqlVector)sqlParam.Value; - paramList.Append('('); - paramList.Append(sqlVectorProps.Length); - paramList.Append(')'); - } - else if (!mt.IsFixed && !mt.IsLong && mt.SqlDbType != SqlDbType.Timestamp && mt.SqlDbType != SqlDbType.Udt && SqlDbType.Structured != mt.SqlDbType) - { - int size = sqlParam.Size; - - paramList.Append('('); - - // if using non unicode types, obtain the actual byte length from the parser, with it's associated code page - if (mt.IsAnsiType) - { - object val = sqlParam.GetCoercedValue(); - string s = null; - - // deal with the sql types - if (val != null && (DBNull.Value != val)) - { - s = (val as string); - if (s == null) - { - SqlString sval = val is SqlString ? (SqlString)val : SqlString.Null; - if (!sval.IsNull) - { - s = sval.Value; - } - } - } - - if (s != null) - { - int actualBytes = parser.GetEncodingCharLength(s, sqlParam.GetActualSize(), sqlParam.Offset, null); - // if actual number of bytes is greater than the user given number of chars, use actual bytes - if (actualBytes > size) - { - size = actualBytes; - } - } - } - - // If the user specifies a 0-sized parameter for a variable len field - // pass over max size (8000 bytes or 4000 characters for wide types) - if (0 == size) - size = mt.IsSizeInCharacters ? (TdsEnums.MAXSIZE >> 1) : TdsEnums.MAXSIZE; - - paramList.Append(size); - paramList.Append(')'); - } - else if (mt.IsPlp && (mt.SqlDbType != SqlDbType.Xml) && (mt.SqlDbType != SqlDbType.Udt) && (mt.SqlDbType != SqlDbTypeExtensions.Json)) - { - paramList.Append("(max) "); - } - - // set the output bit for Output or InputOutput parameters - if (sqlParam.Direction != ParameterDirection.Input) - { - paramList.Append(" " + TdsEnums.PARAM_OUTPUT); - } - } - - return paramList.ToString(); - } - - // Adds quotes to each part of a SQL identifier that may be multi-part, while leaving - // the result as a single composite name. - private static string ParseAndQuoteIdentifier(string identifier, bool isUdtTypeName) - { - string[] strings = SqlParameter.ParseTypeName(identifier, isUdtTypeName); - return ADP.BuildMultiPartName(strings); - } - - private static string QuoteIdentifier(string[] strings, int offset, int length) - { - StringBuilder bld = new StringBuilder(); - - // Stitching back together is a little tricky. Assume we want to build a full multi-part name - // with all parts except trimming separators for leading empty names (null or empty strings, - // but not whitespace). Separators in the middle should be added, even if the name part is - // null/empty, to maintain proper location of the parts. - for (int i = offset; i < (offset + length); i++) - { - if (0 < bld.Length) - { - bld.Append('.'); - } - if (strings[i] != null && 0 != strings[i].Length) - { - ADP.AppendQuotedString(bld, "[", "]", strings[i]); - } - } - - return bld.ToString(); - } - - // returns set option text to turn on format only and key info on and off - // When we are executing as a text command, then we never need - // to turn off the options since they command text is executed in the scope of sp_executesql. - // For a stored proc command, however, we must send over batch sql and then turn off - // the set options after we read the data. See the code in Command.Execute() - private string GetSetOptionsString(CommandBehavior behavior) - { - string s = null; - - if ((System.Data.CommandBehavior.SchemaOnly == (behavior & CommandBehavior.SchemaOnly)) || - (System.Data.CommandBehavior.KeyInfo == (behavior & CommandBehavior.KeyInfo))) - { - // SET FMTONLY ON will cause the server to ignore other SET OPTIONS, so turn - // it off before we ask for browse mode metadata - s = TdsEnums.FMTONLY_OFF; - - if (System.Data.CommandBehavior.KeyInfo == (behavior & CommandBehavior.KeyInfo)) - { - s = s + TdsEnums.BROWSE_ON; - } - - if (System.Data.CommandBehavior.SchemaOnly == (behavior & CommandBehavior.SchemaOnly)) - { - s = s + TdsEnums.FMTONLY_ON; - } - } - - return s; - } - - private string GetResetOptionsString(CommandBehavior behavior) - { - string s = null; - - // SET FMTONLY ON OFF - if (System.Data.CommandBehavior.SchemaOnly == (behavior & CommandBehavior.SchemaOnly)) - { - s = s + TdsEnums.FMTONLY_OFF; - } - - // SET NO_BROWSETABLE OFF - if (System.Data.CommandBehavior.KeyInfo == (behavior & CommandBehavior.KeyInfo)) - { - s = s + TdsEnums.BROWSE_OFF; - } - - return s; - } - - private string GetCommandText(CommandBehavior behavior) - { - // build the batch string we send over, since we execute within a stored proc (sp_executesql), the SET options never need to be - // turned off since they are scoped to the sproc - Debug.Assert(System.Data.CommandType.Text == this.CommandType, "invalid call to GetCommandText for stored proc!"); - return GetSetOptionsString(behavior) + this.CommandText; - } - - internal void CheckThrowSNIException() - { - var stateObj = _stateObj; - if (stateObj != null) - { - stateObj.CheckThrowSNIException(); - } - } - - // We're being notified that the underlying connection has closed - internal void OnConnectionClosed() - { - var stateObj = _stateObj; - if (stateObj != null) - { - stateObj.OnConnectionClosed(); - } - } - - internal void ClearBatchCommand() - { - _RPCList?.Clear(); - _currentlyExecutingBatch = 0; - } - - internal void SetBatchRPCMode(bool value, int commandCount = 1) - { - _batchRPCMode = value; - ClearBatchCommand(); - if (_batchRPCMode) - { - if (_RPCList == null) - { - _RPCList = new List<_SqlRPC>(commandCount); - } - else - { - _RPCList.Capacity = commandCount; - } - } - } - - internal void SetBatchRPCModeReadyToExecute() - { - Debug.Assert(_batchRPCMode, "Command is not in batch RPC Mode"); - Debug.Assert(_RPCList != null, "No batch commands specified"); - - _currentlyExecutingBatch = 0; - } - - internal void AddBatchCommand(SqlBatchCommand batchCommand) - { - Debug.Assert(_batchRPCMode, "Command is not in batch RPC Mode"); - Debug.Assert(_RPCList != null); - - _SqlRPC rpc = new _SqlRPC - { - batchCommand = batchCommand - }; - string commandText = batchCommand.CommandText; - CommandType cmdType = batchCommand.CommandType; - - CommandText = commandText; - CommandType = cmdType; - - // Set the column encryption setting. - SetColumnEncryptionSetting(batchCommand.ColumnEncryptionSetting); - - GetStateObject(); - if (cmdType == CommandType.StoredProcedure) - { - BuildRPC(false, batchCommand.Parameters, ref rpc); - } - else - { - // All batch sql statements must be executed inside sp_executesql, including those without parameters - BuildExecuteSql(CommandBehavior.Default, commandText, batchCommand.Parameters, ref rpc); - } - - _RPCList.Add(rpc); - - ReliablePutStateObject(); - } - - internal int? GetRecordsAffected(int commandIndex) - { - Debug.Assert(_batchRPCMode, "Command is not in batch RPC Mode"); - Debug.Assert(_RPCList != null, "batch command have been cleared"); - return _RPCList[commandIndex].recordsAffected; - } - - internal SqlBatchCommand GetCurrentBatchCommand() - { - if (_batchRPCMode) - { - return _RPCList[_currentlyExecutingBatch].batchCommand; - } - else - { - return _rpcArrayOf1?[0].batchCommand; - } - } - - internal SqlBatchCommand GetBatchCommand(int index) - { - return _RPCList[index].batchCommand; - } - - internal int GetCurrentBatchIndex() - { - return _batchRPCMode ? _currentlyExecutingBatch : -1; - } - - internal SqlException GetErrors(int commandIndex) - { - SqlException result = null; - int length = (_RPCList[commandIndex].errorsIndexEnd - _RPCList[commandIndex].errorsIndexStart); - if (0 < length) - { - SqlErrorCollection errors = new SqlErrorCollection(); - for (int i = _RPCList[commandIndex].errorsIndexStart; i < _RPCList[commandIndex].errorsIndexEnd; ++i) - { - errors.Add(_RPCList[commandIndex].errors[i]); - } - for (int i = _RPCList[commandIndex].warningsIndexStart; i < _RPCList[commandIndex].warningsIndexEnd; ++i) - { - errors.Add(_RPCList[commandIndex].warnings[i]); - } - result = SqlException.CreateException(errors, Connection.ServerVersion, Connection.ClientConnectionId, innerException: null, batchCommand: null); - } - return result; - } - - private static void CancelIgnoreFailureCallback(object state) - { - SqlCommand command = (SqlCommand)state; - command.CancelIgnoreFailure(); - } - - private void CancelIgnoreFailure() - { - // This method is used to route CancellationTokens to the Cancel method. - // Cancellation is a suggestion, and exceptions should be ignored - // rather than allowed to be unhandled, as there is no way to route - // them to the caller. It would be expected that the error will be - // observed anyway from the regular method. An example is cancelling - // an operation on a closed connection. - try - { - Cancel(); - } - catch (Exception) - { - } - } - - private void NotifyDependency() - { - if (_sqlDep != null) - { - _sqlDep.StartTimer(Notification); - } - } - - private void WriteBeginExecuteEvent() - { - SqlClientEventSource.Log.TryBeginExecuteEvent(ObjectID, Connection?.DataSource, Connection?.Database, CommandText, Connection?.ClientConnectionId); - } - - /// - /// Writes and end execute event in Event Source. - /// - /// True if SQL command finished successfully, otherwise false. - /// Gets a number that identifies the type of error. - /// True if SQL command was executed synchronously, otherwise false. - private void WriteEndExecuteEvent(bool success, int? sqlExceptionNumber, bool synchronous) - { - if (SqlClientEventSource.Log.IsExecutionTraceEnabled()) - { - // SqlEventSource.WriteEvent(int, int, int, int) is faster than provided overload SqlEventSource.WriteEvent(int, object[]). - // that's why trying to fit several booleans in one integer value - - // success state is stored the first bit in compositeState 0x01 - int successFlag = success ? 1 : 0; - - // isSqlException is stored in the 2nd bit in compositeState 0x100 - int isSqlExceptionFlag = sqlExceptionNumber.HasValue ? 2 : 0; - - // synchronous state is stored in the second bit in compositeState 0x10 - int synchronousFlag = synchronous ? 4 : 0; - - int compositeState = successFlag | isSqlExceptionFlag | synchronousFlag; - - SqlClientEventSource.Log.TryEndExecuteEvent(ObjectID, compositeState, sqlExceptionNumber.GetValueOrDefault(), Connection?.ClientConnectionId); - } - } - } -} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Batch.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Batch.cs new file mode 100644 index 0000000000..7b6d16a420 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Batch.cs @@ -0,0 +1,141 @@ +// 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.Collections.Generic; +using System.Data; +using System.Diagnostics; + +namespace Microsoft.Data.SqlClient +{ + // @TODO: There's a good question here - should this be a separate type of SqlCommand? + public sealed partial class SqlCommand + { + #region Internal Methods + + internal void AddBatchCommand(SqlBatchCommand batchCommand) + { + Debug.Assert(_batchRPCMode, "Command is not in batch RPC Mode"); + Debug.Assert(_RPCList is not null); + + _SqlRPC rpc = new _SqlRPC { batchCommand = batchCommand }; + string commandText = batchCommand.CommandText; + CommandType cmdType = batchCommand.CommandType; + + CommandText = commandText; + CommandType = cmdType; + + SetColumnEncryptionSetting(batchCommand.ColumnEncryptionSetting); + + // @TODO: Hmm, maybe we could have get/put become a IDisposable thing + GetStateObject(); + if (cmdType is CommandType.StoredProcedure) + { + BuildRPC(inSchema: false, batchCommand.Parameters, ref rpc); + } + else + { + // All batch sql statements must be executed inside sp_executesql, including those + // without parameters + BuildExecuteSql(CommandBehavior.Default, commandText, batchCommand.Parameters, ref rpc); + } + + _RPCList.Add(rpc); + ReliablePutStateObject(); + } + + internal SqlBatchCommand GetBatchCommand(int index) => + _RPCList[index].batchCommand; + + // @TODO: This should be a property. + internal SqlBatchCommand GetCurrentBatchCommand() + { + return _batchRPCMode + ? _RPCList[_currentlyExecutingBatch].batchCommand + : _rpcArrayOf1?[0].batchCommand; + } + + // @TODO: 1) This should be a property + // @TODO: 2) This could be a `int?` + internal int GetCurrentBatchIndex() => + _batchRPCMode ? _currentlyExecutingBatch : -1; + + // @TODO: Indicate this is for batch RPC usage + internal SqlException GetErrors(int commandIndex) + { + SqlException result = null; + + _SqlRPC rpc = _RPCList[commandIndex]; + int length = rpc.errorsIndexEnd - rpc.errorsIndexStart; + if (length > 0) + { + SqlErrorCollection errors = new SqlErrorCollection(); + for (int i = rpc.errorsIndexStart; i < rpc.errorsIndexEnd; i++) + { + errors.Add(rpc.errors[i]); + } + for (int i = rpc.warningsIndexStart; i < rpc.warningsIndexEnd; i++) + { + errors.Add(rpc.warnings[i]); + } + + result = SqlException.CreateException( + errors, + _activeConnection.ServerVersion, + _activeConnection.ClientConnectionId, + innerException: null, + batchCommand: null); + } + + return result; + } + + // @TODO: Should be renamed to indicate only applies to batch RPC mode + internal int? GetRecordsAffected(int commandIndex) + { + Debug.Assert(_batchRPCMode, "Command is not in batch RPC mode"); + Debug.Assert(_RPCList is not null, "Batch commands have been cleared"); + return _RPCList[commandIndex].recordsAffected; + } + + // @TODO: Rename to match naming conventions + internal void SetBatchRPCMode(bool value, int commandCount = 1) + { + _batchRPCMode = value; + ClearBatchCommand(); + if (_batchRPCMode) + { + if (_RPCList is null) + { + // @TODO: Could this be done with an array? + _RPCList = new List<_SqlRPC>(commandCount); + } + else + { + _RPCList.Capacity = commandCount; + } + } + } + + // @TODO: Rename to match naming conventions + internal void SetBatchRPCModeReadyToExecute() + { + Debug.Assert(_batchRPCMode, "Command is not in batch RPC Mode"); + Debug.Assert(_RPCList is not null, "No batch commands specified"); + + _currentlyExecutingBatch = 0; + } + + #endregion + + #region Private Methods + + private void ClearBatchCommand() + { + _RPCList?.Clear(); + _currentlyExecutingBatch = 0; + } + + #endregion + } +} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs index cec571ca1e..cf90d29632 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs @@ -1351,7 +1351,11 @@ private SqlDataReader TryFetchInputParameterEncryptionInfo( _sqlRPCParameterEncryptionReqArray = new _SqlRPC[1]; _SqlRPC rpc = null; - GetRPCObject(systemParamCount: 0, GetParameterCount(_parameters), ref rpc); + GetRPCObject( + systemParamCount: 0, + GetParameterCount(_parameters), + ref rpc, + forSpDescribeParameterEncryption: false); Debug.Assert(rpc is not null, "GetRPCObject should not return rpc as null."); rpc.rpcName = CommandText; 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 df5c372890..28bd1cf2ef 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 @@ -138,7 +138,7 @@ public override int ExecuteNonQuery() finally { SqlStatistics.StopTimer(statistics); - WriteEndExecuteEvent(success, sqlExceptionNumber, synchronous: true); + WriteEndExecuteEvent(success, sqlExceptionNumber, isSynchronous: true); } } @@ -414,7 +414,7 @@ private int EndExecuteNonQueryInternal(IAsyncResult asyncResult) finally { SqlStatistics.StopTimer(statistics); - WriteEndExecuteEvent(success, sqlExceptionNumber, synchronous: false); + WriteEndExecuteEvent(success, sqlExceptionNumber, isSynchronous: false); } } 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 index 91900a13d2..9d63e41ee6 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Reader.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Reader.cs @@ -146,7 +146,7 @@ public SqlDataReader EndExecuteReader(IAsyncResult asyncResult) finally { SqlStatistics.StopTimer(statistics); - WriteEndExecuteEvent(success, sqlExceptionNumber, synchronous: true); + WriteEndExecuteEvent(success, sqlExceptionNumber, isSynchronous: true); if (e is not null) { @@ -409,7 +409,11 @@ private _SqlRPC BuildExecute(bool inSchema) int userParameterCount = CountSendableParameters(_parameters); _SqlRPC rpc = null; - GetRPCObject(systemParameterCount, userParameterCount, ref rpc); + GetRPCObject( + systemParameterCount, + userParameterCount, + ref rpc, + forSpDescribeParameterEncryption: false); rpc.ProcID = TdsEnums.RPC_PROCID_EXECUTE; rpc.rpcName = TdsEnums.SP_EXECUTE; @@ -445,7 +449,11 @@ private void BuildExecuteSql( int userParamCount = CountSendableParameters(parameters); int systemParamCount = userParamCount > 0 ? 2 : 1; - GetRPCObject(systemParamCount, userParamCount, ref rpc); + GetRPCObject( + systemParamCount, + userParamCount, + ref rpc, + forSpDescribeParameterEncryption: false); rpc.ProcID = TdsEnums.RPC_PROCID_EXECUTESQL; rpc.rpcName = TdsEnums.SP_EXECUTESQL; @@ -465,7 +473,10 @@ private void BuildExecuteSql( if (userParamCount > 0) { // @TODO: Why does batch RPC mode use different parameters? - string paramList = BuildParamList(_stateObj.Parser, _batchRPCMode ? parameters : _parameters); + string paramList = BuildParamList( + _stateObj.Parser, + _batchRPCMode ? parameters : _parameters, + includeReturnValue: false); sqlParam = rpc.systemParams[1]; sqlParam.SqlDbType = (paramList.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT ? SqlDbType.NVarChar @@ -488,7 +499,11 @@ private _SqlRPC BuildPrepExec(CommandBehavior behavior) int userParameterCount = CountSendableParameters(_parameters); _SqlRPC rpc = null; - GetRPCObject(systemParameterCount, userParameterCount, ref rpc); + GetRPCObject( + systemParameterCount, + userParameterCount, + ref rpc, + forSpDescribeParameterEncryption: false); rpc.ProcID = TdsEnums.RPC_PROCID_PREPEXEC; rpc.rpcName = TdsEnums.SP_PREPEXEC; @@ -503,7 +518,10 @@ private _SqlRPC BuildPrepExec(CommandBehavior behavior) rpc.systemParamOptions[0] = TdsEnums.RPC_PARAM_BYREF; // @batch_params - string paramList = BuildParamList(_stateObj.Parser, _parameters); + string paramList = BuildParamList( + _stateObj.Parser, + _parameters, + includeReturnValue: false); 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 @@ -519,8 +537,8 @@ private _SqlRPC BuildPrepExec(CommandBehavior behavior) sqlParam.SqlDbType = (text.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT ? SqlDbType.NVarChar : SqlDbType.NText; - sqlParam.Size = text.Length; sqlParam.Value = text; + sqlParam.Size = text.Length; sqlParam.Direction = ParameterDirection.Input; SetUpRPCParameters(rpc, inSchema: false, _parameters); @@ -538,7 +556,11 @@ private void BuildRPC(bool inSchema, SqlParameterCollection parameters, ref _Sql 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); + GetRPCObject( + systemParamCount: 0, + userParameterCount, + ref rpc, + forSpDescribeParameterEncryption: false); rpc.ProcID = 0; // TDS Protocol allows rpc name with maximum length of 1046 bytes for ProcName @@ -710,7 +732,7 @@ private SqlDataReader EndExecuteReaderInternal(IAsyncResult asyncResult) finally { SqlStatistics.StopTimer(statistics); - WriteEndExecuteEvent(success, sqlExceptionNumber, synchronous: false); + WriteEndExecuteEvent(success, sqlExceptionNumber, isSynchronous: false); } } @@ -1359,7 +1381,7 @@ private SqlDataReader RunExecuteReaderTds( $"Command Text '{CommandText}'"); } - string text = GetCommandText(cmdBehavior) + GetResetOptionsString(cmdBehavior); + string text = GetCommandText(cmdBehavior) + GetOptionsResetString(cmdBehavior); // If the query requires enclave computations, pass the enclave package in the // SqlBatch TDS stream @@ -1467,7 +1489,7 @@ private SqlDataReader RunExecuteReaderTds( // 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); + optionSettings = GetOptionsSetString(cmdBehavior); if (returnStream) { @@ -1506,7 +1528,7 @@ private SqlDataReader RunExecuteReaderTds( } // And turn OFF when the ds exhausts the stream on Close() - optionSettings = GetResetOptionsString(cmdBehavior); + optionSettings = GetOptionsResetString(cmdBehavior); } // Execute sproc diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Scalar.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Scalar.cs index f06f85c0a5..f44e4476d5 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Scalar.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Scalar.cs @@ -70,7 +70,7 @@ public override object ExecuteScalar() finally { SqlStatistics.StopTimer(statistics); - WriteEndExecuteEvent(success, sqlExceptionNumber, synchronous: true); + WriteEndExecuteEvent(success, sqlExceptionNumber, isSynchronous: true); } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Xml.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Xml.cs index 1a9a778cd0..6a18b6b1b6 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Xml.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Xml.cs @@ -126,7 +126,7 @@ public XmlReader ExecuteXmlReader() finally { SqlStatistics.StopTimer(statistics); - WriteEndExecuteEvent(success, sqlExceptionNumber, synchronous: true); + WriteEndExecuteEvent(success, sqlExceptionNumber, isSynchronous: true); } } @@ -448,7 +448,7 @@ private XmlReader EndExecuteXmlReaderInternal(IAsyncResult asyncResult) } finally { - WriteEndExecuteEvent(success, sqlExceptionNumber, synchronous: false); + WriteEndExecuteEvent(success, sqlExceptionNumber, isSynchronous: false); } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs index 45646e5539..836d3d4392 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs @@ -11,11 +11,18 @@ using System.Data.SqlTypes; using System.Diagnostics; using System.IO; +using System.Runtime.CompilerServices; +using System.Text; using System.Threading; +using System.Threading.Tasks; using Microsoft.Data.Common; using Microsoft.Data.Sql; using Microsoft.Data.SqlClient.Diagnostics; +#if NETFRAMEWORK +using System.Security.Permissions; +#endif + namespace Microsoft.Data.SqlClient { /// @@ -33,11 +40,64 @@ public sealed partial class SqlCommand : DbCommand, ICloneable { #region Constants + // @TODO: Rename to match naming conventions + private const int MaxRPCNameLength = 1046; + + // @TODO: Make property (externally visible fields are bad) + internal static readonly Action s_cancelIgnoreFailure = CancelIgnoreFailureCallback; + /// /// Pre-boxed invalid prepare handle - used to optimize boxing behavior. /// private static readonly object s_cachedInvalidPrepareHandle = (object)-1; + /// + /// 2005- column ordinals (this array indexed by ProcParamsColIndex + /// + // @TODO: There's gotta be a better way to define these than with 3x static structures + // @TODO: Rename to ProcParamNamesPreSql2008 + private static readonly string[] PreSql2008ProcParamsNames = new string[] + { + "PARAMETER_NAME", // ParameterName, + "PARAMETER_TYPE", // ParameterType, + "DATA_TYPE", // DataType + null, // ManagedDataType, introduced in 2008 + "CHARACTER_MAXIMUM_LENGTH", // CharacterMaximumLength, + "NUMERIC_PRECISION", // NumericPrecision, + "NUMERIC_SCALE", // NumericScale, + "UDT_CATALOG", // TypeCatalogName, + "UDT_SCHEMA", // TypeSchemaName, + "TYPE_NAME", // TypeName, + "XML_CATALOGNAME", // XmlSchemaCollectionCatalogName, + "XML_SCHEMANAME", // XmlSchemaCollectionSchemaName, + "XML_SCHEMACOLLECTIONNAME", // XmlSchemaCollectionName + "UDT_NAME", // UdtTypeName + null, // Scale for datetime types with scale, introduced in 2008 + }; + + /// + /// 2008+ column ordinals (this array indexed by ProcParamsColIndex. + /// + // @TODO: There's gotta be a better way to define these than with 3x static structures + // @TODO: Rename to ProcParamNamesPostSql2008 + internal static readonly string[] Sql2008ProcParamsNames = new[] { + "PARAMETER_NAME", // ParameterName, + "PARAMETER_TYPE", // ParameterType, + null, // DataType, removed from 2008+ + "MANAGED_DATA_TYPE", // ManagedDataType, + "CHARACTER_MAXIMUM_LENGTH", // CharacterMaximumLength, + "NUMERIC_PRECISION", // NumericPrecision, + "NUMERIC_SCALE", // NumericScale, + "TYPE_CATALOG_NAME", // TypeCatalogName, + "TYPE_SCHEMA_NAME", // TypeSchemaName, + "TYPE_NAME", // TypeName, + "XML_CATALOGNAME", // XmlSchemaCollectionCatalogName, + "XML_SCHEMANAME", // XmlSchemaCollectionSchemaName, + "XML_SCHEMACOLLECTIONNAME", // XmlSchemaCollectionName + null, // UdtTypeName, removed from 2008+ + "SS_DATETIME_PRECISION", // Scale for datetime types with scale + }; + #endregion #region Fields @@ -85,7 +145,14 @@ public sealed partial class SqlCommand : DbCommand, ICloneable // @TODO: Rename _batchRpcMode to follow pattern private bool _batchRPCMode; - + + /// + /// Cached information for asynchronous execution. + /// + private AsyncState _cachedAsyncState = null; + + private int _currentlyExecutingBatch; + /// /// Number of instances of SqlCommand that have been created. Used to generate ObjectId /// @@ -98,15 +165,14 @@ public sealed partial class SqlCommand : DbCommand, ICloneable private static readonly SqlDiagnosticListener s_diagnosticListener = new(); /// - /// Prevents the completion events for ExecuteReader from being fired if ExecuteReader is being - /// called as part of a parent operation (e.g. ExecuteScalar, or SqlBatch.ExecuteScalar.) + /// Connection that will be used to process the current instance. /// - private bool _parentOperationStarted = false; + private SqlConnection _activeConnection; /// - /// Connection that will be used to process the current instance. + /// Cut down on object creation and cache all the cached metadata /// - private SqlConnection _activeConnection; + private _SqlMetaDataSet _cachedMetaData; /// /// Column Encryption Override. Defaults to SqlConnectionSetting, in which case it will be @@ -191,7 +257,12 @@ public sealed partial class SqlCommand : DbCommand, ICloneable /// // @TODO: Make auto-property private bool _inPrepare = false; - + + /// + /// A flag to indicate if EndExecute was already initiated by the Begin call. + /// + private volatile bool _internalEndExecuteInitiated; + private SqlNotificationRequest _notification; #if NETFRAMEWORK @@ -204,6 +275,12 @@ public sealed partial class SqlCommand : DbCommand, ICloneable /// private SqlParameterCollection _parameters; + /// + /// Prevents the completion events for ExecuteReader from being fired if ExecuteReader is being + /// called as part of a parent operation (e.g. ExecuteScalar, or SqlBatch.ExecuteScalar.) + /// + private bool _parentOperationStarted = false; + /// /// Volatile bool used to synchronize with cancel thread the state change of an executing /// command going from pre-processing to obtaining a stateObject. The cancel @@ -232,6 +309,12 @@ public sealed partial class SqlCommand : DbCommand, ICloneable /// private object _prepareHandle = s_cachedInvalidPrepareHandle; + /// + /// Last TaskCompletionSource for reconnect task - use for cancellation only. + /// + // @TODO: Ideally we should have this as a Task. + private TaskCompletionSource _reconnectionCompletionSource = null; + /// /// Retry logic provider to use for execution of the current instance. /// @@ -252,11 +335,21 @@ public sealed partial class SqlCommand : DbCommand, ICloneable // @TODO: Rename to drop Sp private int _rowsAffectedBySpDescribeParameterEncryption = -1; + /// + /// Used for RPC executes. + /// + // @TODO: This is very unclear why it needs to be an array + private _SqlRPC[] _rpcArrayOf1 = null; + /// /// RPC for tracking execution of sp_describe_parameter_encryption. /// private _SqlRPC _rpcForEncryption = null; + // @TODO: Rename to match naming conventions + // @TODO: This could probably be an array + private List<_SqlRPC> _RPCList; + // @TODO: Rename to match naming convention private _SqlRPC[] _sqlRPCParameterEncryptionReqArray; @@ -399,7 +492,27 @@ private enum EXECTYPE /// PREPARED, } - + + // Index into indirection arrays for columns of interest to DeriveParameters + private enum ProcParamsColIndex + { + ParameterName = 0, + ParameterType, + DataType, // Obsolete in 2008, use ManagedDataType instead + ManagedDataType, // New in 2008 + CharacterMaximumLength, + NumericPrecision, + NumericScale, + TypeCatalogName, + TypeSchemaName, + TypeName, + XmlSchemaCollectionCatalogName, + XmlSchemaCollectionSchemaName, + XmlSchemaCollectionName, + UdtTypeName, // Obsolete in 2008. Holds the actual typename if UDT, since TypeName didn't back then. + DateTimeScale // New in 2008 + } + #endregion #region Public Properties @@ -709,6 +822,16 @@ public override UpdateRowSource UpdatedRowSource #region Internal/Protected/Private Properties + #if DEBUG + // @TODO: 1) This is never set, 2) This is only used in TdsParserStateObject + internal static int DebugForceAsyncWriteDelay { get; set; } + #endif + + /// + /// A flag to indicate whether we postponed caching the query metadata for this command. + /// + internal bool CachingQueryMetadataPostponed { get; set; } + internal bool HasColumnEncryptionKeyStoreProvidersRegistered { get => _customColumnEncryptionKeyStoreProviders?.Count > 0; @@ -717,6 +840,7 @@ internal bool HasColumnEncryptionKeyStoreProvidersRegistered internal bool InPrepare => _inPrepare; // @TODO: Rename RowsAffectedInternal or + // @TODO: Make `int?` internal int InternalRecordsAffected { get => _rowsAffected; @@ -738,8 +862,13 @@ internal int InternalRecordsAffected /// Reset to false when completed. /// // @TODO: Rename to match naming conventions + // @TODO: Can be made private? internal bool IsDescribeParameterEncryptionRPCCurrentlyInProgress { get; private set; } + // @TODO: Autoproperty + // @TODO: MetaData or Metadata? + internal _SqlMetaDataSet MetaData => _cachedMetaData; + // @TODO: Rename to match conventions. internal int ObjectID { get; } = Interlocked.Increment(ref _objectTypeCount); @@ -811,6 +940,17 @@ protected override DbTransaction DbTransaction } } + // @TODO: This is never null, so we can remove the null checks from usages of it. + // @TODO: Since we never want it to be null, we could make this a lazy and get rid of the property. + private AsyncState CachedAsyncState + { + get + { + _cachedAsyncState ??= new AsyncState(); + return _cachedAsyncState; + } + } + private int DefaultCommandTimeout { // @TODO: Should use connection? Should DefaultCommandTimeout be defined *in the command object*? @@ -876,15 +1016,17 @@ private bool IsDirty } private bool IsPrepared => _execType is not EXECTYPE.UNPREPARED; - - // @TODO: IsPrepared is part of IsDirty - this is confusing. - private bool IsUserPrepared => IsPrepared && !_hiddenPrepare && !IsDirty; + + private bool IsProviderRetriable => SqlConfigurableRetryFactory.IsRetriable(RetryLogicProvider); private bool IsStoredProcedure => CommandType is CommandType.StoredProcedure; private bool IsSimpleTextQuery => CommandType is CommandType.Text && (_parameters is null || _parameters.Count == 0); + // @TODO: IsPrepared is part of IsDirty - this is confusing. + private bool IsUserPrepared => IsPrepared && !_hiddenPrepare && !IsDirty; + private bool ShouldCacheEncryptionMetadata { // @TODO: Should we check for null on _activeConnection? @@ -901,7 +1043,7 @@ private bool ShouldUseEnclaveBasedWorkflow #endregion - #region Public/Internal Methods + #region Public Methods object ICloneable.Clone() => Clone(); @@ -1126,6 +1268,282 @@ public void ResetCommandTimeout() #region Internal Methods + // @TODO: This is only called by SqlCommandBuilder, it should live there. EXCEPT for the one call to ValidateCommand and setting _parameters at the end. Is that really necessary? + // @TODO: This also an crazy long method. + internal void DeriveParameters() + { + switch (CommandType) + { + case CommandType.StoredProcedure: + // This is the only case we can handle with this method. + break; + case System.Data.CommandType.TableDirect: + case System.Data.CommandType.Text: + throw ADP.DeriveParametersNotSupported(this); + default: + throw ADP.InvalidCommandType(CommandType); + } + + // Validate that we have a valid connection + ValidateCommand(isAsync: false); + + // Use common parser for SqlClient and OleDb - parse into 4 parts - Server, Catalog, + // Schema, ProcedureName + string[] parsedSProc = MultipartIdentifier.ParseMultipartIdentifier( + name: CommandText, + leftQuote: "[\"", + rightQuote: "]\"", + property: Strings.SQL_SqlCommandCommandText, + ThrowOnEmptyMultipartName: false); + + if (string.IsNullOrEmpty(parsedSProc[3])) + { + throw ADP.NoStoredProcedureExists(CommandText); + } + + Debug.Assert(parsedSProc.Length == 4, + "Invalid array length result from SqlCommandBuilder.ParseProcedureName"); + + // Build call for sp_procedure_params_rowset built of unquoted values from user: + // [user server, if provided].[user catalog, else current database]. + // [sys if 2005, else blank].[sp_procedure_params_rowset] + StringBuilder cmdText = new StringBuilder(); + + // Server - pass only if user provided. + if (!string.IsNullOrEmpty(parsedSProc[0])) + { + SqlCommandSet.BuildStoredProcedureName(cmdText, parsedSProc[0]); + cmdText.Append("."); + } + + // Catalog - pass user provided, otherwise use current database. + if (string.IsNullOrEmpty(parsedSProc[1])) + { + parsedSProc[1] = Connection.Database; + } + SqlCommandSet.BuildStoredProcedureName(cmdText, parsedSProc[1]); + cmdText.Append("."); + + // Schema - only if 2005, and then only pass sys. Also - pass managed version of sproc + // for 2005, else older sproc. + string[] colNames; + bool useManagedDataType; + if (Connection.Is2008OrNewer) + { + // Procedure - [sp_procedure_params_managed] + cmdText.Append("[sys].[").Append(TdsEnums.SP_PARAMS_MGD10).Append("]"); + + colNames = Sql2008ProcParamsNames; + useManagedDataType = true; + } + else + { + // Procedure - [sp_procedure_params_managed] + cmdText.Append("[sys].[").Append(TdsEnums.SP_PARAMS_MANAGED).Append("]"); + + colNames = PreSql2008ProcParamsNames; + useManagedDataType = false; + } + + SqlCommand paramsCmd = new SqlCommand(cmdText.ToString(), Connection, Transaction) + { + CommandType = CommandType.StoredProcedure + }; + + // Prepare parameters for sp_procedure_params_rowset: + // 1) procedure name - unquote user value + // 2) group number - parsed at the time we unquoted procedure name + // 3) procedure schema - unquote user value + + // ProcedureName is 4th element in parsed array + paramsCmd.Parameters.Add(new SqlParameter("@procedure_name", SqlDbType.NVarChar, 255)); + paramsCmd.Parameters[0].Value = UnquoteProcedureName(parsedSProc[3], out object groupNumber); + + if (groupNumber is not null) + { + SqlParameter param = paramsCmd.Parameters.Add(new SqlParameter("@group_number", SqlDbType.Int)); + param.Value = groupNumber; + } + + if (!string.IsNullOrEmpty(parsedSProc[2])) + { + // SchemaName is 3rd element in parsed array + SqlParameter param = paramsCmd.Parameters.Add(new SqlParameter("@procedure_schema", SqlDbType.NVarChar, 255)); + param.Value = UnquoteProcedurePart(parsedSProc[2]); + } + + SqlDataReader r = null; + + List parameters = new List(); + bool processFinallyBlock = true; + + try + { + // @TODO: Use using block + r = paramsCmd.ExecuteReader(); + while (r.Read()) + { + // each row corresponds to a parameter of the stored proc. Fill in all the info + SqlParameter p = new SqlParameter + { + ParameterName = (string)r[colNames[(int)ProcParamsColIndex.ParameterName]] + }; + + // type + if (useManagedDataType) + { + p.SqlDbType = (SqlDbType)(short)r[colNames[(int)ProcParamsColIndex.ManagedDataType]]; + + // 2005 didn't have as accurate of information as we're getting for 2008, so re-map a couple of + // types for backward compatability. + switch (p.SqlDbType) + { + case SqlDbType.Image: + case SqlDbType.Timestamp: + p.SqlDbType = SqlDbType.VarBinary; + break; + + case SqlDbType.NText: + p.SqlDbType = SqlDbType.NVarChar; + break; + + case SqlDbType.Text: + p.SqlDbType = SqlDbType.VarChar; + break; + + default: + break; + } + } + else + { + short dbType = (short)r[colNames[(int)ProcParamsColIndex.DataType]]; + string typeName = ADP.IsNull(r[colNames[(int)ProcParamsColIndex.TypeName]]) + ? string.Empty + : (string)r[colNames[(int)ProcParamsColIndex.TypeName]]; + p.SqlDbType = MetaType.GetSqlDbTypeFromOleDbType(dbType, typeName); + } + + // size + object a = r[colNames[(int)ProcParamsColIndex.CharacterMaximumLength]]; + if (a is int size) + { + // Map MAX sizes correctly. The 2008 server-side proc sends 0 for these + // instead of -1. Should be fixed on the 2008 side, but would likely hold + // up the RI, and is safer to fix here. If we can get the server-side fixed + // before shipping 2008, we can remove this mapping. + if (size == 0 && p.SqlDbType is SqlDbType.NVarChar or SqlDbType.VarBinary or SqlDbType.VarChar) + { + size = -1; + } + p.Size = size; + } + + // direction + p.Direction = GetParameterDirectionFromOleDbDirection( + (short)r[colNames[(int)ProcParamsColIndex.ParameterType]]); + + if (p.SqlDbType is SqlDbType.Decimal) + { + p.ScaleInternal = (byte)((short)r[colNames[(int)ProcParamsColIndex.NumericScale]] & 0xff); + p.PrecisionInternal = (byte)((short)r[colNames[(int)ProcParamsColIndex.NumericPrecision]] & 0xff); + } + + // Type name for Udt + if (p.SqlDbType is SqlDbType.Udt) + { + string udtTypeName = useManagedDataType + ? (string)r[colNames[(int)ProcParamsColIndex.TypeName]] + : (string)r[colNames[(int)ProcParamsColIndex.UdtTypeName]]; + + // Read the type name + p.UdtTypeName = r[colNames[(int)ProcParamsColIndex.TypeCatalogName]] + "." + + r[colNames[(int)ProcParamsColIndex.TypeSchemaName]] + "." + + udtTypeName; + } + + // type name for Structured types (same as for Udt's except assign p.TypeName + // instead of p.UdtTypeName) + if (p.SqlDbType is SqlDbType.Structured) + { + Debug.Assert(_activeConnection.Is2008OrNewer, + "Invalid datatype token received from pre-2008 server"); + + //read the type name + p.TypeName = r[colNames[(int)ProcParamsColIndex.TypeCatalogName]] + "." + + r[colNames[(int)ProcParamsColIndex.TypeSchemaName]] + "." + + r[colNames[(int)ProcParamsColIndex.TypeName]]; + + // the constructed type name above is incorrectly formatted, it should be a + // 2 part name not 3 for compatibility we can't change this because the bug + // has existed for a long time and been worked around by users, so identify + // that it is present and catch it later in the execution process once + // users can no longer interact with the parameter type name. + p.IsDerivedParameterTypeName = true; + } + + // XmlSchema name for Xml types + if (p.SqlDbType is SqlDbType.Xml) + { + object value; + + value = r[colNames[(int)ProcParamsColIndex.XmlSchemaCollectionCatalogName]]; + p.XmlSchemaCollectionDatabase = ADP.IsNull(value) ? string.Empty : (string)value; + + value = r[colNames[(int)ProcParamsColIndex.XmlSchemaCollectionSchemaName]]; + p.XmlSchemaCollectionOwningSchema = ADP.IsNull(value) ? string.Empty : (string)value; + + value = r[colNames[(int)ProcParamsColIndex.XmlSchemaCollectionName]]; + p.XmlSchemaCollectionName = ADP.IsNull(value) ? string.Empty : (string)value; + } + + if (MetaType._IsVarTime(p.SqlDbType)) + { + object value = r[colNames[(int)ProcParamsColIndex.DateTimeScale]]; + if (value is int intValue) + { + p.ScaleInternal = (byte)(intValue & 0xff); + } + } + + parameters.Add(p); + } + } + catch (Exception e) + { + processFinallyBlock = ADP.IsCatchableExceptionType(e); + throw; + } + finally + { + if (processFinallyBlock) + { + r?.Close(); + + // always unhook the user's connection + paramsCmd.Connection = null; + } + } + + if (parameters.Count == 0) + { + throw ADP.NoStoredProcedureExists(CommandText); + } + + Parameters.Clear(); + + foreach (SqlParameter temp in parameters) + { + _parameters.Add(temp); + } + } + + /// + /// We're being notified that the underlying connection was closed. + /// + internal void OnConnectionClosed() => + _stateObj?.OnConnectionClosed(); + internal void OnDoneDescribeParameterEncryptionProc(TdsParserStateObject stateObj) { // @TODO: Is this not the same stateObj as the currently stored one? @@ -1389,6 +1807,34 @@ thisParam.CipherMetadata is not null && } } + internal void OnStatementCompleted(int recordCount) + { + if (recordCount >= 0) + { + StatementCompletedEventHandler handler = _statementCompletedEventHandler; + if (handler is not null) + { + try + { + SqlClientEventSource.Log.TryTraceEvent( + $"SqlCommand.OnStatementCompleted | Info | " + + $"Object Id {ObjectID}, " + + $"Record Count {recordCount}, " + + $"Client Connection Id {_activeConnection?.ClientConnectionId}"); + + handler(this, new StatementCompletedEventArgs(recordCount)); + } + catch (Exception e) + { + if (!ADP.IsCatchableOrSecurityExceptionType(e)) + { + throw; + } + } + } + } + } + #endregion #region Protected Methods @@ -1417,6 +1863,150 @@ protected override void Dispose(bool disposing) #region Private Methods + private static void CancelIgnoreFailureCallback(object state) => + ((SqlCommand)state).CancelIgnoreFailure(); + + // @TODO: Assess if a parameterized version of this method is necessary or if a property on SqlParameterCollection can suffice. + private static int CountSendableParameters(SqlParameterCollection parameters) + { + if (parameters is null) + { + return 0; + } + + int sendableParameters = 0; + int count = parameters.Count; + for (int i = 0; i < count; i++) + { + if (ShouldSendParameter(parameters[i])) + { + sendableParameters++; + } + } + + return sendableParameters; + } + + /// + /// Returns SET option text to turn off format only and key info on and off. When we are + /// executing as a text command, then we never need to turn off the options since the + /// command text is executed in the scope of sp_executesql. For a sproc command, however, + /// we must send over batch sql and then turn off the SET options after we read the data. + /// + private static string GetOptionsSetString(CommandBehavior behavior) + { + string s = null; + + bool hasKeyInfo = (behavior & CommandBehavior.KeyInfo) == CommandBehavior.KeyInfo; + bool hasSchemaOnly = (behavior & CommandBehavior.SchemaOnly) == CommandBehavior.SchemaOnly; + if (hasKeyInfo || hasSchemaOnly) + { + // SET FMTONLY ON will cause the server to ignore other SET OPTIONS, so turn if off + // before we ask for browse mode metadata + s = TdsEnums.FMTONLY_OFF; + + if (hasKeyInfo) + { + s += TdsEnums.BROWSE_ON; + } + + if (hasSchemaOnly) + { + s += TdsEnums.FMTONLY_ON; + } + } + + return s; + } + + private static string GetOptionsResetString(CommandBehavior behavior) + { + string s = null; + + if ((behavior & CommandBehavior.SchemaOnly) == CommandBehavior.SchemaOnly) + { + s += TdsEnums.FMTONLY_OFF; + } + + if ((behavior & CommandBehavior.KeyInfo) == CommandBehavior.KeyInfo) + { + s += TdsEnums.BROWSE_OFF; + } + + return s; + } + + // @TODO: Assess if a parameterized version of this method is necessary or if a property can suffice. + private static int GetParameterCount(SqlParameterCollection parameters) => + parameters?.Count ?? 0; + + // @TODO: This is only used in DeriveParameters, and as such should live in SqlCommandBuilder + private static ParameterDirection GetParameterDirectionFromOleDbDirection(short oleDbDirection) + { + Debug.Assert(oleDbDirection >= 1 && oleDbDirection <= 4, + "invalid parameter direction from params_rowset!"); + + return oleDbDirection switch + { + 2 => ParameterDirection.InputOutput, + 3 => ParameterDirection.Output, + 4 => ParameterDirection.ReturnValue, + _ => ParameterDirection.Input + }; + } + + private static SqlParameter GetParameterForOutputValueExtraction( + SqlParameterCollection parameters, // @TODO: Is this ever not Parameters? + string paramName, + int paramCount) // @TODO: Is this ever not Parameters.Count? + { + SqlParameter thisParam; + if (paramName is null) + { + // rec.parameter should only be null for a return value from a function + for (int i = 0; i < paramCount; i++) + { + thisParam = parameters[i]; + if (thisParam.Direction is ParameterDirection.ReturnValue) + { + return thisParam; + } + } + } + else + { + for (int i = 0; i < paramCount; i++) + { + thisParam = parameters[i]; + if (thisParam.Direction is not (ParameterDirection.Input or ParameterDirection.ReturnValue) && + SqlParameter.ParameterNamesEqual(paramName, thisParam.ParameterName, StringComparison.Ordinal)) + { + return thisParam; + } + } + } + + return null; + } + + private static bool ShouldSendParameter(SqlParameter p, bool includeReturnValue = false) + { + switch (p.Direction) + { + case ParameterDirection.ReturnValue: + // Return value parameters are not sent, except for the parameter list of + // sp_describe_parameter_encryption + return includeReturnValue; + case ParameterDirection.Input: + case ParameterDirection.Output: + case ParameterDirection.InputOutput: + return true; + default: + Debug.Fail("Invalid ParameterDirection!"); + return false; + } + } + private static void OnDone(TdsParserStateObject stateObj, int index, IList<_SqlRPC> rpcList, int rowsAffected) { // @TODO: Is the state object not the same as the currently stored one? @@ -1446,12 +2036,669 @@ private static void OnDone(TdsParserStateObject stateObj, int index, IList<_SqlR current.warnings = stateObj._warnings; } - // @TODO: Rename PrepareInternal - private void InternalPrepare() + /// + /// Adds quotes to each part of a SQL identifier that may be multi-part, while leaving the + /// result as a single composite name. + /// + // @TODO: This little utility is either likely duplicated in other places, and likely belongs in some other class. + private static string ParseAndQuoteIdentifier(string identifier, bool isUdtTypeName) => + QuoteIdentifier(SqlParameter.ParseTypeName(identifier, isUdtTypeName)); + + // @TODO: This little utility is either likely duplicated in other places, and likely belongs in some other class. + private static string QuoteIdentifier(ReadOnlySpan strings) { - if (IsDirty) + // Stitching back together is a little tricky. Assume we want to build a full + // multipart name with all parts except trimming separators for leading empty names + // (null or empty strings, but not whitespace). Separators in the middle should be + // added, even if the name part is null/empty, to maintain proper location of the parts + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < strings.Length; i++) { - Debug.Assert(_cachedMetaData is null || !_dirty, "dirty query should not have cached metadata!"); + if (builder.Length > 0) + { + builder.Append('.'); + } + + string str = strings[i]; + if (!string.IsNullOrEmpty(str)) + { + ADP.AppendQuotedString(builder, "[", "]", str); + } + } + + return builder.ToString(); + } + + #if NETFRAMEWORK + [SecurityPermission(SecurityAction.Assert, Infrastructure = true)] + private static string SqlNotificationContext() + { + SqlConnection.VerifyExecutePermission(); + + // Since this information is protected, follow it so that it is not exposed to the user. + return System.Runtime.Remoting.Messaging.CallContext.GetData("MS.SqlDependencyCookie") as string; + } + #endif + + /// + /// User value in this format: [server].[database].[schema].[sp_foo];1 This function should + /// only be passed "[sp_foo];1". This function uses a pretty simple parser that doesn't do + /// any validation. Ideally, we would have support from the server rather than us having to + /// do this. + /// + // @TODO: Verify that this method needs to exist, and needs to exist in this class. + private static string UnquoteProcedureName(string name, out object groupNumber) + { + groupNumber = null; // Out param - initialize value to no value. + string sproc = name; + + if (sproc != null) + { + if (char.IsDigit(sproc[sproc.Length - 1])) + { + // If last char is a digit, parse. + int semicolon = sproc.LastIndexOf(';'); + if (semicolon != -1) + { + // If we found a semicolon, obtain the integer. + string part = sproc.Substring(semicolon + 1); + int number = 0; + if (int.TryParse(part, out number)) + { + // No checking, just fail if this doesn't work. + groupNumber = number; + sproc = sproc.Substring(0, semicolon); + } + } + } + sproc = UnquoteProcedurePart(sproc); + } + + return sproc; + } + + // + /// + /// If the user part is quoted, remove first and last brackets and then unquote any right + /// square brackets in the procedure. This is a very simple parser that performs no + /// validation. As with the function below, ideally we should have support from the server + /// for this. + /// + // @TODO: Verify that this method needs to exist, and needs to exist in this class. + private static string UnquoteProcedurePart(string part) + { + // @TODO: Combine if statements if this method should exist + if (part is not null && part.Length >= 2) + { + if (part[0] == '[' && part[part.Length - 1] == ']') + { + // strip outer '[' & ']' + part = part.Substring(1, part.Length - 2); + + // undo quoted "]" from "]]" to "]" + part = part.Replace("]]", "]"); + } + } + + return part; + } + + /// + /// Generates a parameter list string for use with sp_executesql, sp_prepare, and sp_prepexec. + /// + // @TODO: How does this compare with BuildStoredProcedureStatementForColumnEncryption + private string BuildParamList(TdsParser parser, SqlParameterCollection parameters, bool includeReturnValue) + { + // @TODO: Rather than manually add separators, is this something that could be done with a string.Join? + StringBuilder paramList = new StringBuilder(); + bool fAddSeparator = false; // @TODO: Drop f prefix + + int count = parameters.Count; + for (int i = 0; i < count; i++) + { + SqlParameter sqlParam = parameters[i]; + sqlParam.Validate(i, isCommandProc: CommandType is CommandType.StoredProcedure); + + // Skip return value parameters, we never send them to the server + // @TODO: Is that true if "includeReturnValue" is true? + if (!ShouldSendParameter(sqlParam, includeReturnValue)) + { + continue; + } + + // Add separator for the ith parameter + if (fAddSeparator) + { + paramList.Append(','); + } + + // @TODO: This section could do with a bit of cleanup. --vvv + + SqlParameter.AppendPrefixedParameterName(paramList, sqlParam.ParameterName); + + MetaType mt = sqlParam.InternalMetaType; + + //for UDTs, get the actual type name. Get only the typename, omit catalog and schema names. + //in TSQL you should only specify the unqualified type name + + // paragraph above doesn't seem to be correct. Server won't find the type + // if we don't provide a fully qualified name + // @TODO: So ... what's correct? ---^^^ + paramList.Append(" "); + if (mt.SqlDbType is SqlDbType.Udt) // @TODO: Switch statement :) + { + string fullTypeName = sqlParam.UdtTypeName; + if (string.IsNullOrEmpty(fullTypeName)) + { + throw SQL.MustSetUdtTypeNameForUdtParams(); + } + + paramList.Append(ParseAndQuoteIdentifier(fullTypeName, isUdtTypeName: true)); + } + else if (mt.SqlDbType is SqlDbType.Structured) + { + string typeName = sqlParam.TypeName; + if (string.IsNullOrEmpty(typeName)) + { + throw SQL.MustSetTypeNameForParam(mt.TypeName, sqlParam.GetPrefixedParameterName()); + } + + // TVPs currently are the only Structured type and must be read only, so add that keyword + paramList.Append(ParseAndQuoteIdentifier(typeName, isUdtTypeName: false)); + paramList.Append(" READONLY"); + } + else + { + // Func will change type to that with a 4 byte length if the type has a two + // byte length and a parameter length > that expressible in 2 bytes. + // @TODO: what func? + mt = sqlParam.ValidateTypeLengths(); + if (!mt.IsPlp && sqlParam.Direction is not ParameterDirection.Output) + { + sqlParam.FixStreamDataForNonPLP(); + } + + paramList.Append(mt.TypeName); + } + + fAddSeparator = true; + + // @TODO: These seem to be a total hodge-podge of conditions. Can we make a list of categories we're checking and expected behaviors + if (mt.SqlDbType is SqlDbType.Decimal) + { + byte precision = sqlParam.GetActualPrecision(); + if (precision == 0) + { + precision = TdsEnums.DEFAULT_NUMERIC_PRECISION; + } + + byte scale = sqlParam.GetActualScale(); + + paramList.AppendFormat("({0},{1})", precision, scale); + } + else if (mt.IsVarTime) + { + byte scale = sqlParam.GetActualScale(); + paramList.AppendFormat("({0})", scale); + } + else if (mt.SqlDbType is SqlDbTypeExtensions.Vector) + { + // The validate function for SqlParameters would have already thrown + // InvalidCastException if an incompatible value is specified for vector type. + ISqlVector vectorProps = (ISqlVector)sqlParam.Value; + paramList.AppendFormat("({0})", vectorProps.Length); + } + else if (!mt.IsFixed && !mt.IsLong && mt.SqlDbType is not SqlDbType.Timestamp + and not SqlDbType.Udt + and not SqlDbType.Structured) + { + int size = sqlParam.Size; + + // If using non-unicode types, obtain the actual length in bytes from the + // parser, with it's associated code page. + if (mt.IsAnsiType) + { + object val = sqlParam.GetCoercedValue(); + string s = null; + + // Deal with the sql types + if (val is not null && val != DBNull.Value) + { + // @TODO: I swear this can be one line in the if statement... + s = val as string; + if (s is null) + { + SqlString sval = val is SqlString ? (SqlString)val : SqlString.Null; + if (!sval.IsNull) + { + s = sval.Value; + } + } + } + + if (s is not null) + { + int actualBytes = parser.GetEncodingCharLength( + value: s, + numChars: sqlParam.GetActualSize(), + charOffset: sqlParam.Offset, + encoding: null); + if (actualBytes > size) + { + size = actualBytes; + } + } + } + + // If the user specified a 0-sized parameter for a variable length field, pass + // in the maximum size (8000 bytes or 4000 characters for wide types) + if (size == 0) + { + size = mt.IsSizeInCharacters + ? TdsEnums.MAXSIZE >> 1 + : TdsEnums.MAXSIZE; + } + + paramList.AppendFormat("({0})", size); + } + else if (mt.IsPlp && mt.SqlDbType is not SqlDbType.Xml + and not SqlDbType.Udt + and not SqlDbTypeExtensions.Json) + { + paramList.Append("(max) "); // @TODO: All caps? + } + + // Set the output bit for Output/InputOutput parameters + if (sqlParam.Direction is not ParameterDirection.Input) + { + paramList.Append(" " + TdsEnums.PARAM_OUTPUT); + } + } + + return paramList.ToString(); + } + + /// + /// This method is used to route CancellationTokens to the Cancel method. Cancellation is a + /// suggestion, and exceptions should be ignored rather than allowed to be unhandled, as + /// there is no way to route them to the caller. It would be expected that the error will + /// be observed anyway from the regular method. An example is cancelling an operation on a + /// closed connection. + /// + // @TODO: If this is only used in above callback, just combine them together. + private void CancelIgnoreFailure() + { + try + { + Cancel(); + } + catch + { + // We are ignoring failure here. + } + } + + private void CheckNotificationStateAndAutoEnlist() + { + #if NETFRAMEWORK + // First, if auto-enlist is on, check server version and then obtain context if + // present. If so, auto-enlist to the dependency ID given in the context data. + if (NotificationAutoEnlist) + { + string notifyContext = SqlNotificationContext(); + if (!string.IsNullOrEmpty(notifyContext)) + { + // Map to dependency by ID set in context data. + SqlDependency dependency = SqlDependencyPerAppDomainDispatcher.SingletonInstance + .LookupDependencyEntry(notifyContext); + dependency?.AddCommandDependency(this); + } + } + #endif + + // If we have a notification with a dependency, set up the notification at this time. + // If the user passes options, then we will always have option data at the time the + // SqlDependency ctor is called. BUt if we are using default queue, then we do not have + // this data until Start(). Due to this, we always delay setting options until execute. + + // There is a variance between Start(), SqlDependency(), and Execute. This is the best + // way to solve that problem. + // @TODO: Combine these two if statements + if (Notification is not null) + { + if (_sqlDep is not null) + { + if (_sqlDep.Options is null) + { + // If null, SqlDependency was not created with options, so we need to + // obtain default options now. GetDefaultOptions can and will throw under + // certain conditions. @TODO: Like what? + + // In order to match the appropriate start, we need 3 pieces of info: + // 1) server, + // 2) user identify (SQL auth or integrated security) + // 3) database + + // Obtain identity from connection. + // @TODO: Remove cast when possible. + SqlInternalConnectionTds internalConnection = + (SqlInternalConnectionTds)_activeConnection.InnerConnection; + + SqlDependency.IdentityUserNamePair identityUserName = internalConnection.Identity is not null + ? new SqlDependency.IdentityUserNamePair( + internalConnection.Identity, + userName: null) + : new SqlDependency.IdentityUserNamePair( + identity: null, + internalConnection.ConnectionOptions.UserID); + + Notification.Options = SqlDependency.GetDefaultComposedOptions( + _activeConnection.DataSource, + InternalTdsConnection.ServerProvidedFailoverPartner, + identityUserName, + _activeConnection.Database); + } + + // Set UserData on notifications, as well as adding to the appdomain + // dispatcher. The value is computed by an algorithm on the dependency, fixed + // and will always produce the same value given identical command text and + // parameter values. + Notification.UserData = _sqlDep.ComputeHashAndAddToDispatcher(this); + + // Maintain server list for SqlDependency + _sqlDep.AddToServerList(_activeConnection.DataSource); + } + } + } + + // @TODO: Rename to match naming convention + private void CheckThrowSNIException() => + _stateObj?.CheckThrowSNIException(); + + // @TODO: This method *needs* to be decomposed. + private void CreateLocalCompletionTask( + CommandBehavior behavior, + object stateObject, + int timeout, + bool usedCache, + bool asyncWrite, + TaskCompletionSource globalCompletion, + TaskCompletionSource localCompletion, + Func endFunc, + Func retryFunc, + string endMethod, + long firstAttemptStart) + { + localCompletion.Task.ContinueWith( + task => + { + if (task.IsFaulted) + { + globalCompletion.TrySetException(task.Exception.InnerException); + return; + } + + if (task.IsCanceled) + { + globalCompletion.TrySetCanceled(); + return; + } + + try + { + // Mark that we initiated the internal EndExecute. This should always be false + // until we set it here. + Debug.Assert(!_internalEndExecuteInitiated); + _internalEndExecuteInitiated = true; + + // Lock on _stateObj prevents races with close/cancel + lock (_stateObj) + { + endFunc(this, task, /*isInternal:*/ true, endMethod); + } + + globalCompletion.TrySetResult(task.Result); + } + catch (Exception e) + { + // Put the state object back in the cache. + // Do not reset the async state, since this is managed by the user Begin/End, + // not internally. + if (ADP.IsCatchableExceptionType(e)) + { + ReliablePutStateObject(); + } + + // Check if we have an error we can retry + bool shouldRetry = e is EnclaveDelegate.RetryableEnclaveQueryExecutionException; + if (e is SqlException sqlException) + { + for (int i = 0; i < sqlException.Errors.Count; i++) + { + bool isConversionError = + sqlException.Errors[i].Number == TdsEnums.TCE_CONVERSION_ERROR_CLIENT_RETRY; + bool isEnclaveSessionError = + sqlException.Errors[i].Number == TdsEnums.TCE_ENCLAVE_INVALID_SESSION_HANDLE; + + if ((usedCache && isConversionError) || + (ShouldUseEnclaveBasedWorkflow && isEnclaveSessionError)) + { + shouldRetry = true; + break; + } + } + } + + if (!shouldRetry) + { + // If we cannot retry, reset the async state to make sure we leave a + // clean state + CachedAsyncState?.ResetAsyncState(); + + try + { + _activeConnection.GetOpenTdsConnection().DecrementAsyncCount(); + globalCompletion.TrySetException(e); + } + catch (Exception e2) + { + globalCompletion.TrySetException(e2); + } + + return; + } + + // Remove the entry from the cache since it was inconsistent + SqlQueryMetadataCache.GetInstance().InvalidateCacheEntry(this); + InvalidateEnclaveSession(); + + // Kick off the retry + try + { + _internalEndExecuteInitiated = false; + Task retryTask = (Task)retryFunc( + this, + behavior, + /*asyncCallback:*/ null, // @TODO: Is this ever not null? + stateObject, + TdsParserStaticMethods.GetRemainingTimeout(timeout, firstAttemptStart), + /*isRetry:*/ true, + asyncWrite); + + retryTask.ContinueWith( + static (retryTask, state) => + { + TaskCompletionSource completionSource = (TaskCompletionSource)state; + if (retryTask.IsFaulted) + { + completionSource.TrySetException(retryTask.Exception.InnerException); + } + else if (retryTask.IsCanceled) + { + completionSource.TrySetCanceled(); + } + else + { + completionSource.TrySetResult(retryTask.Result); + } + }, + state: globalCompletion, + TaskScheduler.Default); + } + catch (Exception e2) + { + globalCompletion.TrySetException(e2); + } + } + }, + TaskScheduler.Default); + } + + // @TODO: The naming of this is a bit sketchy, it implies we're just returning CommandText, but we're adding the options string to it. I'd suggest either removing the method entirely or renaming to indicate the + private string GetCommandText(CommandBehavior behavior) + { + // Build the batch string we send over, since we execute within a stored proc + // (sp_executesql), the SET options never need to be turned off since they are scoped + // to the sproc. + Debug.Assert(CommandType is CommandType.Text, + "invalid call to GetCommandText for stored proc!"); + return GetOptionsSetString(behavior) + CommandText; + } + + private SqlParameterCollection GetCurrentParameterCollection() + { + if (!_batchRPCMode) + { + return _parameters; + } + + if (_RPCList.Count > _currentlyExecutingBatch) + { + return _RPCList[_currentlyExecutingBatch].userParams; + } + + // @TODO: Is there any point to us failing here? + Debug.Fail("OnReturnValue: SqlCommand got too many DONEPROC events"); + return null; + } + + // @TODO: Why not *return* it? (update: ok, this is because the intention is to reuse existing RPC objects, but the way it is doing it is confusing) + // @TODO: Rename to match naming conventions GetRpcObject + // @TODO: This method would be less confusing if the initialized rpc is always provided (ie, the caller knows which member to grab an existing rpc from), and this method just initializes it. + private void GetRPCObject( + int systemParamCount, + int userParamCount, + ref _SqlRPC rpc, // @TODO: When is this not null? + bool forSpDescribeParameterEncryption) + { + // @TODO: This method seems like its overoptimizing for no good reason. It basically exists just to allow reuse of an _SqlRPC object. + + // Designed to minimize necessary allocations + if (rpc is null) + { + if (!forSpDescribeParameterEncryption) + { + // @TODO: When the arrayOf1 is used vs the list of RPCs is used is confusing to say the least. + if (_rpcArrayOf1 is null) + { + _rpcArrayOf1 = new _SqlRPC[1]; + _rpcArrayOf1[0] = new _SqlRPC(); + } + + rpc = _rpcArrayOf1[0]; + } + else + { + _rpcForEncryption ??= new _SqlRPC(); + rpc = _rpcForEncryption; + } + } + + // @TODO: This should be a "clear" or "reset" method on the _SqlRPC object. But reuse of an object like this is dangerous. + rpc.ProcID = 0; + rpc.rpcName = null; + rpc.options = 0; + rpc.systemParamCount = systemParamCount; + rpc.needsFetchParameterEncryptionMetadata = false; + + // Make sure there is enough space in the parameters and param options arrays + int currentSystemParamCount = rpc.systemParams?.Length ?? 0; + if (currentSystemParamCount < systemParamCount) + { + // @TODO: It's especially dangerous because we'll be leaking parameters and data. + Array.Resize(ref rpc.systemParams, systemParamCount); + Array.Resize(ref rpc.systemParamOptions, systemParamCount); + + // Initialize new elements in the array + for (int index = currentSystemParamCount; index < systemParamCount; index++) + { + rpc.systemParams[index] = new SqlParameter(); + } + } + + for (int i = 0; i < systemParamCount; i++) + { + rpc.systemParamOptions[i] = 0; + } + + int currentUserParamCount = rpc.userParamMap?.Length ?? 0; + if (currentUserParamCount < userParamCount) + { + Array.Resize(ref rpc.userParamMap, userParamCount); + } + } + + // @TODO: This method is smelly - it gets the parser state object from the parser and stores it, but also handles the cancelled exception throwing and connection closed exception throwing. + private void GetStateObject(TdsParser parser = null) // @TODO: Is this ever not null? + { + Debug.Assert(_stateObj is null, "StateObject not null on GetStateObject"); + Debug.Assert(_activeConnection is not null, "no active connection?"); + + if (_pendingCancel) + { + _pendingCancel = false; // Not really needed, but we'll reset anyway. + + // If a pendingCancel exists on the object, we must have had a Cancel() call + // between the point that we entered an Execute* API and the point in Execute* that + // we proceeded to call this function and obtain a stateObject. In that case, we + // now throw a cancelled error. + throw SQL.OperationCancelled(); + } + + if (parser == null) + { + parser = _activeConnection.Parser; + if (parser == null || parser.State is TdsParserState.Broken or TdsParserState.Closed) + { + // Connection's parser is null as well, therefore we must be closed + throw ADP.ClosedConnectionError(); + } + } + + TdsParserStateObject stateObj = parser.GetSession(this); + stateObj.StartSession(this); + + _stateObj = stateObj; + + if (_pendingCancel) + { + _pendingCancel = false; // Not really needed, but we'll reset anyway. + + // If a pendingCancel exists on the object, we must have had a Cancel() call + // between the point that we entered this function and the point where we obtained + // and actually assigned the stateObject to the local member. It is possible that + // the flag is set as well as a call to stateObj.Cancel - though that would be a + // no-op. So - throw. + throw SQL.OperationCancelled(); + } + } + + // @TODO: Rename PrepareInternal + private void InternalPrepare() + { + if (IsDirty) + { + Debug.Assert(_cachedMetaData is null || !_dirty, "dirty query should not have cached metadata!"); // Someone changed the command text or the parameter schema so we must unprepare the command Unprepare(); @@ -1476,11 +2723,180 @@ private void InternalPrepare() Statistics?.SafeIncrement(ref Statistics._prepares); } + private void NotifyDependency() => + _sqlDep?.StartTimer(Notification); + private void PropertyChanging() { IsDirty = true; } + private void PutStateObject() + { + TdsParserStateObject stateObject = _stateObj; + _stateObj = null; + + stateObject?.CloseSession(); + } + + private Task RegisterForConnectionCloseNotification(Task outerTask) + { + SqlConnection connection = _activeConnection; + if (connection is null) + { + throw ADP.ClosedConnectionError(); + } + + return connection.RegisterForConnectionCloseNotification( + outerTask, + this, + SqlReferenceCollection.CommandTag); + } + + // @TODO: THERE IS NOTHING RELIABLE ABOUT THIS!!! REMOVE!!! + private void ReliablePutStateObject() => + PutStateObject(); + + // @TODO Rename to match naming conventions + private void SetUpRPCParameters(_SqlRPC rpc, bool inSchema, SqlParameterCollection parameters) + { + int paramCount = GetParameterCount(parameters); + int userParamCount = 0; + + for (int index = 0; index < paramCount; index++) + { + SqlParameter parameter = parameters[index]; + parameter.Validate(index, isCommandProc: CommandType is CommandType.StoredProcedure); + + // Func will change type to that with a 4 byte length if the type has a 2 byte + // length and a parameter length > than that expressible in 2 bytes. + if (!parameter.ValidateTypeLengths().IsPlp && parameter.Direction is not ParameterDirection.Output) + { + parameter.FixStreamDataForNonPLP(); + } + + if (ShouldSendParameter(parameter)) + { + byte options = 0; + + // Set output bit + if (parameter.Direction is ParameterDirection.InputOutput or ParameterDirection.Output) + { + options |= TdsEnums.RPC_PARAM_BYREF; + } + + // Set the encrypted bit if the parameter is to be encrypted + if (parameter.CipherMetadata is not null) + { + options |= TdsEnums.RPC_PARAM_ENCRYPTED; + } + + // Set default value bit + if (parameter.Direction is not ParameterDirection.Output) + { + // Remember that Convert.IsEmpty is null, DBNull.Value is a database null! + + // Don't assume a default value exists for parameters in the case when the + // user is simply requesting schema. TVPs use DEFAULT and do not allow + // NULL, even for schema only. + if (parameter.Value is null && (!inSchema || parameter.SqlDbType is SqlDbType.Structured)) + { + options |= TdsEnums.RPC_PARAM_DEFAULT; + } + + // Detect incorrectly derived type names unchanged by the caller and fix + if (parameter.IsDerivedParameterTypeName) + { + string[] parts = MultipartIdentifier.ParseMultipartIdentifier( + parameter.TypeName, + leftQuote: "[\"", + rightQuote: "]\"", + property: Strings.SQL_TDSParserTableName, + ThrowOnEmptyMultipartName: false); + // @TODO: Combine this and inner if statement + if (parts?.Length == 4) + { + if (parts[3] is not null && // Name must not be null + parts[2] is not null && // Schema must not be null + parts[1] is not null) // Server should not be null or we don't need to remove it + { + parameter.TypeName = QuoteIdentifier(parts.AsSpan(2, 2)); + } + } + } + } + + rpc.userParamMap[userParamCount] = ((long)options << 32) | (long)index; + userParamCount++; + + // Must set parameter option bit for LOB_COOKIE if unfilled LazyMat blob + } + } + + rpc.userParamCount = userParamCount; + rpc.userParams = parameters; + } + + private void ThrowIfReconnectionHasBeenCanceled() + { + if (_stateObj is not null) + { + return; + } + + if (_reconnectionCompletionSource?.Task.IsCanceled ?? false) + { + throw SQL.CR_ReconnectionCancelled(); + } + } + + private bool TriggerInternalEndAndRetryIfNecessary( + CommandBehavior behavior, + object stateObject, + int timeout, + bool usedCache, + bool isRetry, + bool asyncWrite, + TaskCompletionSource globalCompletion, + TaskCompletionSource localCompletion, + Func endFunc, + Func retryFunc, + string endMethod) + { + // We shouldn't be using the cache if we are in retry. + Debug.Assert(!usedCache || !isRetry); + + // If column encryption is enabled, and we used the cache, we want to catch any + // potential exceptions that were caused by the query cache and retry if the error + // indicates that we should. So, try to read the result of the query before completing + // the overall task and trigger a retry if appropriate. + if ((IsColumnEncryptionEnabled && !isRetry && (usedCache || ShouldUseEnclaveBasedWorkflow)) + #if DEBUG + || _forceInternalEndQuery + #endif + ) + { + long firstAttemptStart = ADP.TimerCurrent(); + + CreateLocalCompletionTask( + behavior, + stateObject, + timeout, + usedCache, + asyncWrite, + globalCompletion, + localCompletion, + endFunc, + retryFunc, + endMethod, + firstAttemptStart); + + return true; + } + + return false; + } + private void Unprepare() { Debug.Assert(IsPrepared, "Invalid attempt to Unprepare a non-prepared command!"); @@ -1510,6 +2926,341 @@ private void Unprepare() $"Object Id {ObjectID}, Command unprepared."); } + private void ValidateAsyncCommand() + { + if (CachedAsyncState is not null && CachedAsyncState.PendingAsyncOperation) + { + // Enforce only one pending async execute at a time. + if (CachedAsyncState.IsActiveConnectionValid(_activeConnection)) + { + throw SQL.PendingBeginXXXExists(); + } + + // @TODO: This is smelly - why are we mucking with parser state obj here? + _stateObj = null; // Session was re-claimed by session pool upon connection close. + CachedAsyncState.ResetAsyncState(); + } + } + + // @TODO: isAsync isn't used. + private void ValidateCommand(bool isAsync, [CallerMemberName] string method = "") + { + if (_activeConnection is null) + { + throw ADP.ConnectionRequired(method); + } + + // Ensure that the connection is open and that the parser is in the correct state + // @TODO: Remove cast when possible. + SqlInternalConnectionTds tdsConnection = _activeConnection.InnerConnection as SqlInternalConnectionTds; + + // Ensure that if column encryption override was used then server supports it + // @TODO: This is kinda clunky + if (((ColumnEncryptionSetting == SqlCommandColumnEncryptionSetting.UseConnectionSetting && _activeConnection.IsColumnEncryptionSettingEnabled) || + (ColumnEncryptionSetting == SqlCommandColumnEncryptionSetting.Enabled || ColumnEncryptionSetting == SqlCommandColumnEncryptionSetting.ResultSetOnly)) + && tdsConnection != null + && tdsConnection.Parser != null + && !tdsConnection.Parser.IsColumnEncryptionSupported) + { + throw SQL.TceNotSupported(); + } + + if (tdsConnection is not null) // @TODO: Why would it ever be null?? + { + // @TODO: We check state here and in GetStateObject ... one or the other seems fishy. + TdsParser parser = tdsConnection.Parser; + if (parser is null || parser.State is TdsParserState.Closed) + { + throw ADP.OpenConnectionRequired(method, ConnectionState.Closed); + } + + if (parser.State is not TdsParserState.OpenLoggedIn) + { + throw ADP.OpenConnectionRequired(method, ConnectionState.Broken); + } + } + else if (_activeConnection.State is ConnectionState.Closed) + { + throw ADP.OpenConnectionRequired(method, ConnectionState.Closed); + } + else if (_activeConnection.State is ConnectionState.Broken) + { + throw ADP.OpenConnectionRequired(method, ConnectionState.Broken); + } + + // @TODO: Ok, so ... why do we have other places calling this method? + ValidateAsyncCommand(); + + // Close any dead, non-MARS readers, if applicable, and throw if still busy. Throw if + // we have a live reader for this command. + _activeConnection.ValidateConnectionForExecute(method, this); + + // Check to see if the currently set transaction has completed. If so, null out our + // local reference. + if (_transaction is not null && _transaction.Connection is null) + { + _transaction = null; + } + + // Throw if the connection is in a transaction but there is no locally assigned + // transaction object. + if (_activeConnection.HasLocalTransactionFromAPI && _transaction is null) + { + throw ADP.TransactionRequired(method); + } + + // If we have a transaction, check to ensure that the active connection associated with + // the transaction. + if (_transaction is not null && _activeConnection != _transaction.Connection) + { + throw ADP.TransactionConnectionMismatch(); + } + + if (string.IsNullOrEmpty(CommandText)) + { + throw ADP.CommandTextRequired(method); + } + } + + private void VerifyEndExecuteState( + Task completionTask, + string endMethod, + bool fullCheckForColumnEncryption = false) + { + Debug.Assert(completionTask is not null); + + SqlClientEventSource.Log.TryTraceEvent( + $"SqlCommand.VerifyEndExecuteState | API | " + + $"Object Id {ObjectID}, " + + $"Client Connection Id {_activeConnection?.ClientConnectionId}, " + + $"MARS={_activeConnection?.Parser?.MARSOn}, " + + $"AsyncCommandInProgress={_activeConnection?.AsyncCommandInProgress}"); + + if (completionTask.IsCanceled) + { + if (_stateObj is not null) + { + // We failed to respond to attention, we have to quit! + _stateObj.Parser.State = TdsParserState.Broken; + _stateObj.Parser.Connection.BreakConnection(); + _stateObj.Parser.ThrowExceptionAndWarning(_stateObj, this); + } + else + { + Debug.Assert(_reconnectionCompletionSource is null || _reconnectionCompletionSource.Task.IsCanceled, + "ReconnectCompletionSource should be null or cancelled"); + throw SQL.CR_ReconnectionCancelled(); + } + } + else if (completionTask.IsFaulted) + { + throw completionTask.Exception.InnerException; + } + + // If transparent parameter encryption was attempted, then we need to skip other checks + // like those on EndMethodName since we want to wait for async results before checking + // those fields. + if (IsColumnEncryptionEnabled && !fullCheckForColumnEncryption) + { + if (_activeConnection?.State is not ConnectionState.Open) + { + // If the connection is not "valid" then it was closed while we were executing + throw ADP.ClosedConnectionError(); + } + + return; + } + + if (CachedAsyncState.EndMethodName is null) + { + throw ADP.MethodCalledTwice(endMethod); + } + + if (CachedAsyncState.EndMethodName != endMethod) + { + throw ADP.MismatchedAsyncResult(CachedAsyncState.EndMethodName, endMethod); + } + + if (_activeConnection?.State is not ConnectionState.Open || + !CachedAsyncState.IsActiveConnectionValid(_activeConnection)) + { + // @TODO: Why do we have multiple checks for an "invalid" connection? + // If the connection is not 'valid' then it was closed while we were executing + throw ADP.ClosedConnectionError(); + } + } + + private void WaitForAsyncResults(IAsyncResult asyncResult, bool isInternal) + { + Task completionTask = (Task)asyncResult; + if (!asyncResult.IsCompleted) + { + asyncResult.AsyncWaitHandle.WaitOne(); + } + + if (_stateObj is not null) + { + _stateObj._networkPacketTaskSource = null; + } + + // If this is an internal command we will decrement the count when the End method is + // actually called by the user. If we are using Column Encryption and the previous task + // failed, the async count should have already been fixed up. There is a generic issue + // in how we handle the async count because: + // 1) BeginExecute might or might not clean it up on failure. + // 2) In EndExecute, we check the task state before waiting and throw if it's failed, whereas if we wait we will always adjust the count. + if (!isInternal && (!IsColumnEncryptionEnabled || !completionTask.IsFaulted)) + { + _activeConnection.GetOpenTdsConnection().DecrementAsyncCount(); + } + } + + private void WriteBeginExecuteEvent() + { + SqlClientEventSource.Log.TryBeginExecuteEvent( + ObjectID, + _activeConnection?.DataSource, + _activeConnection?.Database, + CommandText, + _activeConnection?.ClientConnectionId); + } + + /// + /// Writes and end execute event in Event Source. + /// + /// True if SQL command finished successfully, otherwise false. + /// Number that identifies the type of error. + /// + /// True if SQL command was executed synchronously, otherwise false. + /// + private void WriteEndExecuteEvent(bool success, int? sqlExceptionNumber, bool isSynchronous) + { + if (!SqlClientEventSource.Log.IsExecutionTraceEnabled()) + { + return; + } + + // SqlEventSource.WriteEvent(int, int, int, int) is faster than provided overload + // SqlEventSource.WriteEvent(int, object[]). That's why we're trying to fit several + // booleans in one integer value. + + // Success state is stored the first bit in compositeState 0x01 + int successFlag = success ? 1 : 0; + + // isSqlException is stored in the 2nd bit in compositeState 0x100 + int isSqlExceptionFlag = sqlExceptionNumber.HasValue ? 2 : 0; + + // Synchronous state is stored in the second bit in compositeState 0x10 + int synchronousFlag = isSynchronous ? 4 : 0; + + int compositeState = successFlag | isSqlExceptionFlag | synchronousFlag; + + SqlClientEventSource.Log.TryEndExecuteEvent( + ObjectID, + compositeState, + sqlExceptionNumber.GetValueOrDefault(), + _activeConnection?.ClientConnectionId); + } + #endregion + + private sealed class AsyncState + { + // @TODO: Autoproperties + private int _cachedAsyncCloseCount = -1; // value of the connection's CloseCount property when the asyncResult was set; tracks when connections are closed after an async operation + private TaskCompletionSource _cachedAsyncResult = null; + private SqlConnection _cachedAsyncConnection = null; // Used to validate that the connection hasn't changed when end the connection; + private SqlDataReader _cachedAsyncReader = null; + private RunBehavior _cachedRunBehavior = RunBehavior.ReturnImmediately; + private string _cachedSetOptions = null; + private string _cachedEndMethod = null; + + internal AsyncState() + { + } + + internal SqlDataReader CachedAsyncReader + { + get { return _cachedAsyncReader; } + } + internal RunBehavior CachedRunBehavior + { + get { return _cachedRunBehavior; } + } + internal string CachedSetOptions + { + get { return _cachedSetOptions; } + } + internal bool PendingAsyncOperation + { + get { return _cachedAsyncResult != null; } + } + internal string EndMethodName + { + get { return _cachedEndMethod; } + } + + internal bool IsActiveConnectionValid(SqlConnection activeConnection) + { + return (_cachedAsyncConnection == activeConnection && _cachedAsyncCloseCount == activeConnection.CloseCount); + } + + internal void ResetAsyncState() + { + SqlClientEventSource.Log.TryTraceEvent("CachedAsyncState.ResetAsyncState | API | ObjectId {0}, Client Connection Id {1}, AsyncCommandInProgress={2}", + _cachedAsyncConnection?.ObjectID, _cachedAsyncConnection?.ClientConnectionId, _cachedAsyncConnection?.AsyncCommandInProgress); + _cachedAsyncCloseCount = -1; + _cachedAsyncResult = null; + if (_cachedAsyncConnection != null) + { + _cachedAsyncConnection.AsyncCommandInProgress = false; + _cachedAsyncConnection = null; + } + _cachedAsyncReader = null; + _cachedRunBehavior = RunBehavior.ReturnImmediately; + _cachedSetOptions = null; + _cachedEndMethod = null; + } + + internal void SetActiveConnectionAndResult(TaskCompletionSource completion, string endMethod, SqlConnection activeConnection) + { + Debug.Assert(activeConnection != null, + "Unexpected null connection argument on SetActiveConnectionAndResult!"); + + TdsParser parser = activeConnection?.Parser; + + SqlClientEventSource.Log.TryTraceEvent( + $"SqlCommand.SetActiveConnectionAndResult | API | " + + $"Object ID {activeConnection.ObjectID}, " + + $"Client Connection ID {activeConnection.ClientConnectionId}, " + + $"MARS={parser?.MARSOn}"); + + if (parser == null || parser.State == TdsParserState.Closed || parser.State == TdsParserState.Broken) + { + throw ADP.ClosedConnectionError(); + } + + _cachedAsyncCloseCount = activeConnection.CloseCount; + _cachedAsyncResult = completion; + + if (!parser.MARSOn && activeConnection.AsyncCommandInProgress) + { + throw SQL.MARSUnsupportedOnConnection(); + } + + _cachedAsyncConnection = activeConnection; + + // Should only be needed for non-MARS, but set anyway. + _cachedAsyncConnection.AsyncCommandInProgress = true; + _cachedEndMethod = endMethod; + } + + internal void SetAsyncReaderState(SqlDataReader ds, RunBehavior runBehavior, string optionSettings) + { + _cachedAsyncReader = ds; + _cachedRunBehavior = runBehavior; + _cachedSetOptions = optionSettings; + } + } } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs index e13a77bd73..fd814370b5 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs @@ -635,6 +635,7 @@ internal static Exception OperationCancelled() return exception; } + // @TODO: Rename.... internal static Exception PendingBeginXXXExists() { return ADP.InvalidOperation(StringsHelper.GetString(Strings.SQL_PendingBeginXXXExists));