Skip to content

Merge | SqlCommand Prepare/Unprepare #3514

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 15 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,9 @@
<Compile Include="$(CommonSourceRoot)Microsoft\Data\SqlClient\SqlColumnEncryptionKeyStoreProvider.cs">
<Link>Microsoft\Data\SqlClient\SqlColumnEncryptionKeyStoreProvider.cs</Link>
</Compile>
<Compile Include="$(CommonSourceRoot)Microsoft\Data\SqlClient\SqlCommand.cs">
<Link>Microsoft\Data\SqlClient\SqlCommand.cs</Link>
</Compile>
<Compile Include="$(CommonSourceRoot)Microsoft\Data\SqlClient\SqlCommandSet.cs">
<Link>Microsoft\Data\SqlClient\SqlCommandSet.cs</Link>
</Compile>
Expand Down Expand Up @@ -799,7 +802,7 @@
<Link>System\Diagnostics\CodeAnalysis.cs</Link>
</Compile>

<Compile Include="Microsoft\Data\SqlClient\SqlCommand.cs" />
<Compile Include="Microsoft\Data\SqlClient\SqlCommand.netcore.cs" />
<Compile Include="Microsoft\Data\SqlClient\SqlConnection.cs" />
<Compile Include="Microsoft\Data\SqlClient\SqlConnectionHelper.cs" />
<Compile Include="Microsoft\Data\SqlClient\SqlInternalConnectionTds.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,10 @@
// New attributes that are designed to work with Microsoft.Data.SqlClient and are publicly documented should be included in future.
namespace Microsoft.Data.SqlClient
{
/// <include file='../../../../../../../doc/snippets/Microsoft.Data.SqlClient/SqlCommand.xml' path='docs/members[@name="SqlCommand"]/SqlCommand/*'/>
[DefaultEvent("RecordsAffected")]
[ToolboxItem(true)]
[DesignerCategory("")]
// TODO: Add designer attribute when Microsoft.VSDesigner.Data.VS.SqlCommandDesigner uses Microsoft.Data.SqlClient
public sealed partial class SqlCommand : DbCommand, ICloneable
{
private static int _objectTypeCount; // EventSource Counter
private const int MaxRPCNameLength = 1046;
internal readonly int ObjectID = Interlocked.Increment(ref _objectTypeCount);

internal sealed class ExecuteReaderAsyncCallContext : AAsyncCallContext<SqlCommand, SqlDataReader, CancellationTokenRegistration>
{
Expand Down Expand Up @@ -113,8 +107,6 @@ protected override void AfterCleared(SqlCommand owner)
}
}

private string _commandText;
private CommandType _commandType;
private int? _commandTimeout;
private UpdateRowSource _updatedRowSource = UpdateRowSource.Both;
private bool _designTimeInvisible;
Expand Down Expand Up @@ -164,39 +156,18 @@ protected override void AfterCleared(SqlCommand owner)
private bool _parentOperationStarted = false;

internal static readonly Action<object> s_cancelIgnoreFailure = CancelIgnoreFailureCallback;

// Prepare
// Against 7.0 Serve a prepare/unprepare requires an extra roundtrip to the server.
//
// From 8.0 and above, the preparation can be done as part of the command execution.

private enum EXECTYPE
{
UNPREPARED, // execute unprepared commands, all server versions (results in sp_execsql call)
PREPAREPENDING, // prepare and execute command, 8.0 and above only (results in sp_prepexec call)
PREPARED, // execute prepared commands, all server versions (results in sp_exec call)
}

// _hiddenPrepare
// On 8.0 and above the Prepared state cannot be left. Once a command is prepared it will always be prepared.
// A change in parameters, commandtext etc (IsDirty) automatically causes a hidden prepare
//
// _inPrepare will be set immediately before the actual prepare is done.
// The OnReturnValue function will test this flag to determine whether the returned value is a _prepareHandle or something else.



//
// _prepareHandle - the handle of a prepared command. Apparently there can be multiple prepared commands at a time - a feature that we do not support yet.

private static readonly object s_cachedInvalidPrepareHandle = (object)-1;
private bool _inPrepare = false;
private object _prepareHandle = s_cachedInvalidPrepareHandle; // this is an int which is used in the object typed SqlParameter.Value field, avoid repeated boxing by storing in a box
private bool _hiddenPrepare = false;
private int _preparedConnectionCloseCount = -1;
private int _preparedConnectionReconnectCount = -1;

private SqlParameterCollection _parameters;
private SqlConnection _activeConnection;
private bool _dirty = false; // true if the user changes the commandtext or number of parameters after the command is already prepared
private EXECTYPE _execType = EXECTYPE.UNPREPARED; // by default, assume the user is not sharing a connection so the command has not been prepared
private _SqlRPC[] _rpcArrayOf1 = null; // Used for RPC executes
private _SqlRPC _rpcForEncryption = null; // Used for sp_describe_parameter_encryption RPC executes

Expand Down Expand Up @@ -227,13 +198,6 @@ private bool ShouldCacheEncryptionMetadata
#if DEBUG
internal static int DebugForceAsyncWriteDelay { get; set; }
#endif
internal bool InPrepare
{
get
{
return _inPrepare;
}
}

/// <summary>
/// Return if column encryption setting is enabled.
Expand Down Expand Up @@ -382,8 +346,6 @@ private AsyncState CachedAsyncState

private StatementCompletedEventHandler _statementCompletedEventHandler;

private TdsParserStateObject _stateObj; // this is the TDS session we're using.

// 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 synchronization
// we require in the command is only from entering an Execute* API to obtaining a
Expand Down Expand Up @@ -880,131 +842,6 @@ private void PropertyChanging()
this.IsDirty = true;
}

/// <include file='../../../../../../../doc/snippets/Microsoft.Data.SqlClient/SqlCommand.xml' path='docs/members[@name="SqlCommand"]/Prepare/*'/>
public override void Prepare()
{
// Reset _pendingCancel upon entry into any Execute - used to synchronize state
// between entry into Execute* API and the thread obtaining the stateObject.
_pendingCancel = false;

SqlStatistics statistics = null;
using (TryEventScope.Create("SqlCommand.Prepare | API | Object Id {0}", ObjectID))
{
SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlCommand.Prepare | API | Correlation | Object Id {0}, ActivityID {1}, Client Connection Id {2}", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId);
try
{
statistics = SqlStatistics.StartTimer(Statistics);

// only prepare if batch with parameters
if (this.IsPrepared && !this.IsDirty
|| (this.CommandType == CommandType.StoredProcedure)
|| ((System.Data.CommandType.Text == this.CommandType)
&& (0 == GetParameterCount(_parameters))))
{
if (Statistics != null)
{
Statistics.SafeIncrement(ref Statistics._prepares);
}
_hiddenPrepare = false;
}
else
{
// Validate the command outside of the try/catch to avoid putting the _stateObj on error
ValidateCommand(isAsync: false);

bool processFinallyBlock = true;
try
{
// NOTE: The state object isn't actually needed for this, but it is still here for back-compat (since it does a bunch of checks)
GetStateObject();

// Loop through parameters ensuring that we do not have unspecified types, sizes, scales, or precisions
if (_parameters != null)
{
int count = _parameters.Count;
for (int i = 0; i < count; ++i)
{
_parameters[i].Prepare(this);
}
}

InternalPrepare();
}
catch (Exception e)
{
processFinallyBlock = ADP.IsCatchableExceptionType(e);
throw;
}
finally
{
if (processFinallyBlock)
{
_hiddenPrepare = false; // The command is now officially prepared

ReliablePutStateObject();
}
}
}
}
finally
{
SqlStatistics.StopTimer(statistics);
}
}
}

private void InternalPrepare()
{
if (this.IsDirty)
{
Debug.Assert(_cachedMetaData == null || !_dirty, "dirty query should not have cached metadata!"); // can have cached metadata if dirty because of parameters
//
// someone changed the command text or the parameter schema so we must unprepare the command
//
this.Unprepare();
this.IsDirty = false;
}
Debug.Assert(_execType != EXECTYPE.PREPARED, "Invalid attempt to Prepare already Prepared command!");
Debug.Assert(_activeConnection != null, "must have an open connection to Prepare");
Debug.Assert(_stateObj != null, "TdsParserStateObject should not be null");
Debug.Assert(_stateObj.Parser != null, "TdsParser class should not be null in Command.Execute!");
Debug.Assert(_stateObj.Parser == _activeConnection.Parser, "stateobject parser not same as connection parser");
Debug.Assert(false == _inPrepare, "Already in Prepare cycle, this.inPrepare should be false!");

// remember that the user wants to do a prepare but don't actually do an rpc
_execType = EXECTYPE.PREPAREPENDING;
// Note the current close count of the connection - this will tell us if the connection has been closed between calls to Prepare() and Execute
_preparedConnectionCloseCount = _activeConnection.CloseCount;
_preparedConnectionReconnectCount = _activeConnection.ReconnectCount;

if (Statistics != null)
{
Statistics.SafeIncrement(ref Statistics._prepares);
}
}

// SqlInternalConnectionTds needs to be able to unprepare a statement
internal void Unprepare()
{
Debug.Assert(true == IsPrepared, "Invalid attempt to Unprepare a non-prepared command!");
Debug.Assert(_activeConnection != null, "must have an open connection to UnPrepare");
Debug.Assert(false == _inPrepare, "_inPrepare should be false!");
_execType = EXECTYPE.PREPAREPENDING;

SqlClientEventSource.Log.TryTraceEvent("SqlCommand.UnPrepare | Info | Object Id {0}, Current Prepared Handle {1}", ObjectID, _prepareHandle);

// Don't zero out the handle because we'll pass it in to sp_prepexec on the next prepare
// Unless the close count isn't the same as when we last prepared
if ((_activeConnection.CloseCount != _preparedConnectionCloseCount) || (_activeConnection.ReconnectCount != _preparedConnectionReconnectCount))
{
// reset our handle
_prepareHandle = s_cachedInvalidPrepareHandle;
}

_cachedMetaData = null;
SqlClientEventSource.Log.TryTraceEvent("SqlCommand.UnPrepare | Info | Object Id {0}, Command unprepared.", ObjectID);
}

// Cancel is supposed to be multi-thread safe.
// It doesn't make sense to verify the connection exists or that it is open during cancel
// because immediately after checking the connection can be closed or removed via another thread.
Expand Down Expand Up @@ -6803,48 +6640,6 @@ internal void OnConnectionClosed()
}
}

internal TdsParserStateObject StateObject
{
get
{
return _stateObj;
}
}

private bool IsPrepared
{
get { return (_execType != EXECTYPE.UNPREPARED); }
}

private bool IsUserPrepared
{
get { return IsPrepared && !_hiddenPrepare && !IsDirty; }
}

internal bool IsDirty
{
get
{
// only dirty if prepared
var activeConnection = _activeConnection;
return (IsPrepared &&
(_dirty ||
((_parameters != null) && (_parameters.IsDirty)) ||
((activeConnection != null) && ((activeConnection.CloseCount != _preparedConnectionCloseCount) || (activeConnection.ReconnectCount != _preparedConnectionReconnectCount)))));
}
set
{
// only mark the command as dirty if it is already prepared
// but always clear the value if it we are clearing the dirty flag
_dirty = value ? IsPrepared : false;
if (_parameters != null)
{
_parameters.IsDirty = _dirty;
}
_cachedMetaData = null;
}
}

/// <summary>
/// Get or add to the number of records affected by SpDescribeParameterEncryption.
/// The below line is used only for debug asserts and not exposed publicly or impacts functionality otherwise.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,9 @@
<Compile Include="$(CommonSourceRoot)Microsoft\Data\SqlClient\SqlColumnEncryptionKeyStoreProvider.cs">
<Link>Microsoft\Data\SqlClient\SqlColumnEncryptionKeyStoreProvider.cs</Link>
</Compile>
<Compile Include="$(CommonSourceRoot)Microsoft\Data\SqlClient\SqlCommand.cs">
<Link>Microsoft\Data\SqlClient\SqlCommand.cs</Link>
</Compile>
<Compile Include="$(CommonSourceRoot)Microsoft\Data\SqlClient\SqlCommandBuilder.cs">
<Link>Microsoft\Data\SqlClient\SqlCommandBuilder.cs</Link>
</Compile>
Expand Down Expand Up @@ -870,6 +873,9 @@
<Compile Include="$(CommonSourceRoot)Microsoft\Data\SqlClient\Utilities\BufferWriterExtensions.netfx.cs">
<Link>Microsoft\Data\SqlClient\Utilities\BufferWriterExtensions.netfx.cs</Link>
</Compile>
<Compile Include="$(CommonSourceRoot)Microsoft\Data\SqlClient\Utilities\ConstrainedExecutionHelper.netfx.cs">
<Link>Microsoft\Data\SqlClient\Utilities\ConstrainedExecutionHelper.netfx.cs</Link>
</Compile>
<Compile Include="$(CommonSourceRoot)Microsoft\Data\SqlClient\Utilities\ObjectPool.cs">
<Link>Microsoft\Data\SqlClient\Utilities\ObjectPool.cs</Link>
</Compile>
Expand Down Expand Up @@ -913,7 +919,7 @@
<Link>System\Runtime\CompilerServices\IsExternalInit.netfx.cs</Link>
</Compile>

<Compile Include="Microsoft\Data\SqlClient\SqlCommand.cs" />
<Compile Include="Microsoft\Data\SqlClient\SqlCommand.netfx.cs" />
<Compile Include="Microsoft\Data\SqlClient\SqlConnection.cs" />
<Compile Include="Microsoft\Data\SqlClient\SqlConnectionHelper.cs" />
<Compile Include="Microsoft\Data\SqlClient\SqlInternalConnectionTds.cs" />
Expand Down
Loading
Loading