Skip to content
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

Fix | Default SqlCommand's transaction to owning SqlConnection's local transaction #330

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
f97cf86
Removed SqlCommand and SqlBulkCopy user-assigned Transaction dependen…
Nov 23, 2019
b8aff14
Made several changes to improve special case handling.
Nov 23, 2019
75596e3
Modified a few tests (that tested Transaction assignment errors) to f…
Nov 23, 2019
d2a03b2
Made SqlInternalTransaction return it's Parent without hesitation.
Nov 23, 2019
e476ba4
Removed 2 straggler string variables from the TransactionTest left ov…
Nov 23, 2019
361c079
Changed SqlCommand.Trasnsaction.set and SqlBulkCopy.externalTransacti…
Nov 24, 2019
bed3b4a
Since setting Transaction now checks if zombied, we remove the 3rd ar…
Nov 24, 2019
75a5f25
CertificateUtility.GetConnectionString messes up IntegratedSecurity m…
Nov 24, 2019
092dbc3
Fixed a test where inserting date '0/12/31' was expected to fail but …
Nov 24, 2019
46ef7e5
Added .StartInfo.WindowStyle = ProcessWindowStyle.Hidden;' in Certifi…
Nov 24, 2019
c79224e
Changed SqlBulkCopy.WriteToServer to prevent both internal (via Begin…
Nov 24, 2019
946079e
Small wording change in a SqlBulkCopy comment.
Nov 24, 2019
a1a158c
Removed the use of 'default' and replaced with 'default(SqlTransactio…
Nov 25, 2019
455c770
Removed SqlConnection.CurrentTransaction which I added to help me in …
Nov 25, 2019
afe1233
Touched SqlBulkCopy.AnalyzeTargetAndCreateUpdateBulkCommand.
Nov 26, 2019
ab8d7cc
Added new Tests related to these changes.
Nov 26, 2019
bc89d35
Reverted the SqlBulkCopy changes making this a half-fix.
Nov 26, 2019
0d2bc21
Removed 1 test as I have zero experience in TransactionScope and it f…
Nov 26, 2019
430058e
Reverted all changes done on .sln file.
Nov 28, 2019
d1834d7
Renamed T3 set of tests to TransactionInferenceTest.
Nov 28, 2019
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 @@ -261,9 +261,6 @@ private CachedAsyncState cachedAsyncState

private SqlNotificationRequest _notification;

// transaction support
private SqlTransaction _transaction;

private StatementCompletedEventHandler _statementCompletedEventHandler;

private TdsParserStateObject _stateObj; // this is the TDS session we're using.
Expand Down Expand Up @@ -376,14 +373,6 @@ private SqlCommand(SqlCommand from) : this()
}
}

// Check to see if the currently set transaction has completed. If so,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Behavior change. I have a command lying around and the transaction was changed for the command. Now I associate the new connection with it, and it magically overwrote previous transaction.

Can we stick to the lowest common denominator on how this convenience of deriving the transaction for this command is going to work, while preserving most of the old behavior ?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@saurabh500 Behavior change. I have a command lying around and the transaction was changed for the command. Now I associate the new connection with it, and it magically overwrote previous transaction.

Since a transaction's lifetime is BeginTransaction - Commit/Rollback/throw, and a single connection has a single active transaction, how can a command be lying around with a different transaction that magically changes? If it had a different transaction reference associated, and the connection ran another transaction, then it's a complete transaction which used to be nullified internally in the old functionality too.

If you associate a new connection to it, you don't do it in the hopes of keeping the old transaction on the .Transaction property and a different connection in the .Connection. This would not have passed validation previously also.

A few lines of code explaining this behavior would be helpful.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@saurabh500

Could you provide a few lines of code to verify how these code changes would break?

// null out our local reference.
if (null != _transaction && _transaction.Connection == null)
{
_transaction = null;
}


// Command is no longer prepared on new connection, cleanup prepare status
if (IsPrepared)
{
Expand Down Expand Up @@ -467,25 +456,55 @@ internal SqlStatistics Statistics
{
get
{
// if the transaction object has been zombied, just return null
if ((null != _transaction) && (null == _transaction.Connection))
{
_transaction = null;
}
return _transaction;
// there is no real reason to read this value except when creating a new command based on this command
// so we just use the Connection's CurrentTransaction since that it the only valid value anyway
SqlTransaction transaction = (_activeConnection?.InnerConnection as SqlInternalConnectionTds)?.CurrentTransaction?.Parent;
return transaction?.IsZombied == true ? default(SqlTransaction) : transaction;
}
set
{
// Don't allow the transaction to be changed while in an async operation.
if (_transaction != value && _activeConnection != null)
if (Transaction != value && _activeConnection != null)
{ // If new value...
if (cachedAsyncState.PendingAsyncOperation)
{ // If in pending async state, throw
throw SQL.CannotModifyPropertyAsyncOperationInProgress();
}
}

_transaction = value;
// on null transaction just move on
if (value is null)
{
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that a null value here wouldn't matter as the command will just use the connection's transaction. But since this setter is validating the input, shouldn't null also throw? A user assigning null to this Transaction property will think the command will execute under that transaction, which they believe to be null.


// on zombied transaction throw and warn the user
// or just fail silently?
if (value.IsZombied)
{
throw ADP.TransactionZombied(value);
}

// if connection is not already set-up, grab it from the transaction
if (null == _activeConnection)
{
Connection = value.Connection;
}

// if connection differ's from the transaction's connection, throw
if (_activeConnection != value.Connection)
{
throw ADP.TransactionConnectionMismatch();
}

// check to see if transaction
if (value != Transaction)
{
throw ADP.TransactionConnectionMismatch();
}

// we don't store the transaction in any way as the connection has its own value
// all above code are just validations for the user's input
}
}

Expand Down Expand Up @@ -4617,21 +4636,6 @@ private void ValidateCommand(bool isAsync, [CallerMemberName] string method = ""
// 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 (null != _transaction && _transaction.Connection == null)
_transaction = null;

// throw if the connection is in a transaction but there is no
// locally assigned transaction object
if (_activeConnection.HasLocalTransactionFromAPI && (null == _transaction))
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 (null != _transaction && _activeConnection != _transaction.Connection)
throw ADP.TransactionConnectionMismatch();

if (string.IsNullOrEmpty(this.CommandText))
throw ADP.CommandTextRequired(method);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,14 +190,7 @@ internal SqlTransaction Parent
{
get
{
SqlTransaction result = null;
// Should we protect against this, since this probably is an invalid state?
Debug.Assert(null != _parent, "Why are we calling Parent with no parent?");
if (null != _parent)
{
result = (SqlTransaction)_parent.Target;
}
return result;
return _parent?.Target as SqlTransaction;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -275,9 +275,6 @@ private CachedAsyncState cachedAsyncState
private SqlNotificationRequest _notification;
private bool _notificationAutoEnlist = true; // Notifications auto enlistment is turned on by default

// transaction support
private SqlTransaction _transaction;

private StatementCompletedEventHandler _statementCompletedEventHandler;

private TdsParserStateObject _stateObj; // this is the TDS session we're using.
Expand Down Expand Up @@ -512,13 +509,6 @@ private SqlCommand(SqlCommand from) : this()
}
}

// Check to see if the currently set transaction has completed. If so,
// null out our local reference.
if (null != _transaction && _transaction.Connection == null)
{
_transaction = null;
}

// If the connection has changes, then the request context may have changed as well
_smiRequestContext = null;

Expand Down Expand Up @@ -687,27 +677,58 @@ internal SqlStatistics Statistics
{
get
{
// if the transaction object has been zombied, just return null
if ((null != _transaction) && (null == _transaction.Connection))
{ // MDAC 72720
_transaction = null;
}
return _transaction;
// there is no real reason to read this value except when creating a new command based on this command
// so we just use the Connection's CurrentTransaction since that it the only valid value anyway
SqlTransaction transaction = (_activeConnection?.InnerConnection as SqlInternalConnectionTds)?.CurrentTransaction?.Parent;
return transaction?.IsZombied == true ? default(SqlTransaction) : transaction;
}
set
{
// Don't allow the transaction to be changed while in a async opperation.
if (_transaction != value && _activeConnection != null)
if (Transaction != value && _activeConnection != null)
{ // If new value...
if (cachedAsyncState.PendingAsyncOperation)
{ // If in pending async state, throw
throw SQL.CannotModifyPropertyAsyncOperationInProgress(SQL.Transaction);
}
}

// on null transaction just move on
if (value is null)
{
return;
}

// on zombied transaction throw and warn the user
// or just fail silently?
if (value.IsZombied)
{
throw ADP.TransactionZombied(value);
}

// if connection is not already set-up, grab it from the transaction
if (null == _activeConnection)
{
Connection = value.Connection;
}

// if connection differ's from the transaction's connection, throw
if (_activeConnection != value.Connection)
{
throw ADP.TransactionConnectionMismatch();
}

// check to see if transaction
if (value != Transaction)
{
throw ADP.TransactionConnectionMismatch();
}

// TODO: Add objid here
Bid.Trace("<sc.SqlCommand.set_Transaction|API> %d#\n", ObjectID);
_transaction = value;

// we don't store the transaction in any way as the connection has its own value
// all above code are just validations for the user's input
}
}

Expand Down Expand Up @@ -5776,21 +5797,6 @@ private void ValidateCommand(string method, bool async)
SqlInternalConnection.BestEffortCleanup(bestEffortCleanupTarget);
throw;
}
// Check to see if the currently set transaction has completed. If so,
// null out our local reference.
if (null != _transaction && _transaction.Connection == null)
_transaction = null;

// throw if the connection is in a transaction but there is no
// locally assigned transaction object
if (_activeConnection.HasLocalTransactionFromAPI && (null == _transaction))
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 (null != _transaction && _activeConnection != _transaction.Connection)
throw ADP.TransactionConnectionMismatch();

if (ADP.IsEmpty(this.CommandText))
throw ADP.CommandTextRequired(method);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,14 +215,7 @@ internal SqlTransaction Parent
{
get
{
SqlTransaction result = null;
// Should we protect against this, since this probably is an invalid state?
Debug.Assert(null != _parent, "Why are we calling Parent with no parent?");
if (null != _parent)
{
result = (SqlTransaction)_parent.Target;
}
return result;
return _parent?.Target as SqlTransaction;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@ public void TestSqlTransactionRollbackToSavePoint(string connection)
sqlTransaction.Rollback();

// Now even row no:1 should not be available.
VerifyRecordAbsent(sqlConnection, customer1, sqlTransaction);
// Removed transaction argument as it's zombied.
VerifyRecordAbsent(sqlConnection, customer1);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,10 @@ List<ValueErrorTuple> GenerateOutOfRangeValuesForType(SqlDbType type, int length
case SqlDbType.Date:
// These values are out of range and should fail.
list.Add(new ValueErrorTuple("10000/1/1", true));
list.Add(new ValueErrorTuple("0/12/31", true));
// Former value of "0/12/31" passed insert even if test expected failure.
// Debugging noticed a coerced value that turned into 2000/12/31.
// Changed date to a clear invalid day value.
list.Add(new ValueErrorTuple("0/12/32", true));
break;
case SqlDbType.Time:
case SqlDbType.DateTime2:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,8 @@ internal static byte[] DecryptDataUsingAED(byte[] encryptedCellBlob, byte[] key,

internal static SqlConnection GetOpenConnection(bool fTceEnabled, SqlConnectionStringBuilder sb, bool fSuppressAttestation = false)
{
SqlConnection conn = new SqlConnection(GetConnectionString(fTceEnabled, sb, fSuppressAttestation));
var cs = GetConnectionString(fTceEnabled, sb, fSuppressAttestation);
SqlConnection conn = new SqlConnection(cs);
try
{
conn.Open();
Expand All @@ -284,8 +285,17 @@ public static string GetConnectionString(bool fTceEnabled, SqlConnectionStringBu
SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder();
builder.DataSource = sb.DataSource;
builder.InitialCatalog = sb.InitialCatalog;
builder.UserID = sb.UserID;
builder.Password = sb.Password;
// if connection string has integrated security, respect that
if (!sb.IntegratedSecurity)
{
builder.UserID = sb.UserID;
builder.Password = sb.Password;
builder.IntegratedSecurity = false;
}
else
{
builder.IntegratedSecurity = true;
}
if (fTceEnabled)
{
builder.ColumnEncryptionSetting = SqlConnectionColumnEncryptionSetting.Enabled;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ internal static void CreateCertificate(string certificateName, string certificat
Process process = new Process();
process.StartInfo = processStartInfo;
process.StartInfo.UseShellExecute = true;
process.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
process.StartInfo.CreateNoWindow = true;
process.Start();
process.WaitForExit();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@
<Compile Include="SQL\RandomStressTest\RandomStressTest.cs" />
<Compile Include="SQL\SplitPacketTest\SplitPacketTest.cs" />
<Compile Include="SQL\SqlNotificationTest\SqlNotificationTest.cs" />
<Compile Include="SQL\TransactionTest\TransactionInferenceTest.cs" />
<Compile Include="SQL\TransactionTest\TransactionTest.cs" />
<Compile Include="SQL\TransactionTest\TransactionEnlistmentTest.cs" />
<Compile Include="SQL\UdtTest\SqlServerTypesTest.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,11 @@ public static void Test(string srcConstr, string dstConstr, string dstTable)

SqlCommand myCmd = dstConn.CreateCommand();
myCmd.CommandText = "select * from " + dstTable;
myCmd.Transaction = myTrans;

DataTestUtility.AssertThrowsWrapper<InvalidOperationException>(() => myCmd.ExecuteReader(), exceptionMessage: exceptionMsg);
// ///// The following 2-lines test can no longer work
// myCmd.Transaction = myTrans;
// DataTestUtility.AssertThrowsWrapper<InvalidOperationException>(() => myCmd.ExecuteReader(), exceptionMessage: exceptionMsg);
// ///// So it is replaced with the following as now validation is done on assignment
DataTestUtility.AssertThrowsWrapper<InvalidOperationException>(() => myCmd.Transaction = myTrans, exceptionMessage: exceptionMsg);
}
}
}
Expand Down
Loading