Skip to content

Commit 1144249

Browse files
authored
Allows the use of snapshot isolation level on transactions (#791)
Signed-off-by: John Battye <[email protected]>
1 parent d2abfc0 commit 1144249

File tree

3 files changed

+92
-10
lines changed

3 files changed

+92
-10
lines changed

src/MySqlConnector/Core/StandardEnlistedTransaction.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,16 @@ protected override void OnStart()
2121
IsolationLevel.ReadCommitted => "read committed",
2222
IsolationLevel.ReadUncommitted => "read uncommitted",
2323
IsolationLevel.RepeatableRead => "repeatable read",
24-
IsolationLevel.Snapshot => throw new NotSupportedException("IsolationLevel.{0} is not supported.".FormatInvariant(Transaction.IsolationLevel)),
24+
IsolationLevel.Snapshot => "repeatable read",
2525
IsolationLevel.Chaos => throw new NotSupportedException("IsolationLevel.{0} is not supported.".FormatInvariant(Transaction.IsolationLevel)),
2626

2727
// "In terms of the SQL:1992 transaction isolation levels, the default InnoDB level is REPEATABLE READ." - http://dev.mysql.com/doc/refman/5.7/en/innodb-transaction-model.html
2828
IsolationLevel.Unspecified => "repeatable read",
2929
_ => "repeatable read",
3030
};
3131

32-
using var cmd = new MySqlCommand("set transaction isolation level " + isolationLevel + "; start transaction;", Connection);
32+
var consistentSnapshotText = Transaction.IsolationLevel == IsolationLevel.Snapshot ? " with consistent snapshot" : string.Empty;
33+
using var cmd = new MySqlCommand($"set transaction isolation level {isolationLevel}; start transaction{consistentSnapshotText};", Connection);
3334
cmd.ExecuteNonQuery();
3435
}
3536

src/MySqlConnector/MySql.Data.MySqlClient/MySqlConnection.cs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,17 @@ private async ValueTask<MySqlTransaction> BeginDbTransactionAsync(IsolationLevel
6363
IsolationLevel.ReadCommitted => "read committed",
6464
IsolationLevel.RepeatableRead => "repeatable read",
6565
IsolationLevel.Serializable => "serializable",
66+
IsolationLevel.Snapshot => "repeatable read",
6667

6768
// "In terms of the SQL:1992 transaction isolation levels, the default InnoDB level is REPEATABLE READ." - http://dev.mysql.com/doc/refman/5.7/en/innodb-transaction-model.html
6869
IsolationLevel.Unspecified => "repeatable read",
6970

7071
_ => throw new NotSupportedException("IsolationLevel.{0} is not supported.".FormatInvariant(isolationLevel))
7172
};
7273

73-
using (var cmd = new MySqlCommand("set transaction isolation level " + isolationLevelValue + "; start transaction;", this))
74+
var consistentSnapshotText = isolationLevel == IsolationLevel.Snapshot ? " with consistent snapshot" : string.Empty;
75+
76+
using (var cmd = new MySqlCommand($"set transaction isolation level {isolationLevelValue}; start transaction{consistentSnapshotText};", this))
7477
await cmd.ExecuteNonQueryAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
7578

7679
var transaction = new MySqlTransaction(this, isolationLevel);
@@ -106,7 +109,7 @@ public override void EnlistTransaction(System.Transactions.Transaction? transact
106109
else
107110
{
108111
m_enlistedTransaction = GetInitializedConnectionSettings().UseXaTransactions ?
109-
(EnlistedTransactionBase) new XaEnlistedTransaction(transaction, this) :
112+
(EnlistedTransactionBase)new XaEnlistedTransaction(transaction, this) :
110113
new StandardEnlistedTransaction(transaction, this);
111114
m_enlistedTransaction.Start();
112115

@@ -253,7 +256,7 @@ private async Task ChangeDatabaseAsync(IOBehavior ioBehavior, string databaseNam
253256
m_session.DatabaseOverride = databaseName;
254257
}
255258

256-
public new MySqlCommand CreateCommand() => (MySqlCommand) base.CreateCommand();
259+
public new MySqlCommand CreateCommand() => (MySqlCommand)base.CreateCommand();
257260

258261
public bool Ping() => PingAsync(IOBehavior.Synchronous, CancellationToken.None).GetAwaiter().GetResult();
259262
public Task<bool> PingAsync(CancellationToken cancellationToken = default) => PingAsync(SimpleAsyncIOBehavior, cancellationToken).AsTask();
@@ -400,7 +403,7 @@ private SchemaProvider GetSchemaProvider()
400403
SchemaProvider? m_schemaProvider;
401404
#endif
402405

403-
/// <summary>
406+
/// <summary>
404407
/// Gets the time (in seconds) to wait while trying to establish a connection
405408
/// before terminating the attempt and generating an error. This value
406409
/// is controlled by <see cref="MySqlConnectionStringBuilder.ConnectionTimeout"/>,
@@ -662,7 +665,7 @@ private async ValueTask<ServerSession> CreateSessionAsync(ConnectionPool? pool,
662665
var messageSuffix = (pool?.IsEmpty ?? false) ? " All pooled connections are in use." : "";
663666
throw new MySqlException(MySqlErrorCode.UnableToConnectToHost, "Connect Timeout expired." + messageSuffix, ex);
664667
}
665-
catch (MySqlException ex) when ((timeoutSource?.IsCancellationRequested ?? false) || ((MySqlErrorCode) ex.Number == MySqlErrorCode.CommandTimeoutExpired))
668+
catch (MySqlException ex) when ((timeoutSource?.IsCancellationRequested ?? false) || ((MySqlErrorCode)ex.Number == MySqlErrorCode.CommandTimeoutExpired))
666669
{
667670
throw new MySqlException(MySqlErrorCode.UnableToConnectToHost, "Connect Timeout expired.", ex);
668671
}

tests/SideBySide/Transaction.cs

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using System;
2+
using System.Data;
23
using System.Data.Common;
4+
using System.Linq;
35
using System.Threading.Tasks;
46
using Dapper;
57
using MySql.Data.MySqlClient;
@@ -50,7 +52,69 @@ public void DbConnectionCommit()
5052
Assert.Equal(new[] { 1, 2 }, results);
5153
}
5254

55+
[Theory]
56+
[InlineData(IsolationLevel.ReadUncommitted, "read uncommitted")]
57+
[InlineData(IsolationLevel.ReadCommitted, "read committed")]
58+
[InlineData(IsolationLevel.RepeatableRead, "repeatable read")]
59+
[InlineData(IsolationLevel.Serializable, "serializable")]
60+
[InlineData(IsolationLevel.Unspecified, "repeatable read")]
5361
#if !BASELINE
62+
[InlineData(IsolationLevel.Snapshot, "repeatable read")]
63+
#endif
64+
public void DbConnectionIsolationLevel(IsolationLevel inputIsolationLevel, string expectedTransactionIsolationLevel)
65+
{
66+
DbConnection connection = m_connection;
67+
m_connection.Execute(@"set global log_output = 'table';");
68+
m_connection.Execute(@"set global general_log = 1;");
69+
using (var trans = connection.BeginTransaction(inputIsolationLevel))
70+
{
71+
trans.Commit();
72+
}
73+
74+
m_connection.Execute(@"set global general_log = 0;");
75+
var results = connection.Query<string>($"select convert(argument USING utf8) from mysql.general_log where thread_id = {m_connection.ServerThread} order by event_time desc limit 10;");
76+
var lastIsolationLevelQuery = results.First(x => x.ToLower().Contains("isolation"));
77+
78+
if (IsMySqlAndVersionLessThan57(m_connection.ServerVersion))
79+
{
80+
Assert.Contains("serializable", lastIsolationLevelQuery.ToLower());
81+
return;
82+
}
83+
84+
Assert.Contains(expectedTransactionIsolationLevel.ToLower(), lastIsolationLevelQuery.ToLower());
85+
}
86+
87+
#if !BASELINE
88+
[Theory]
89+
[InlineData(IsolationLevel.ReadUncommitted, "start transaction")]
90+
[InlineData(IsolationLevel.ReadCommitted, "start transaction")]
91+
[InlineData(IsolationLevel.RepeatableRead, "start transaction")]
92+
[InlineData(IsolationLevel.Serializable, "start transaction")]
93+
[InlineData(IsolationLevel.Unspecified, "start transaction")]
94+
[InlineData(IsolationLevel.Snapshot, "start transaction with consistent snapshot")]
95+
public void DbConnectionTransactionCommand(IsolationLevel inputIsolationLevel, string expectedTransactionIsolationLevel)
96+
{
97+
DbConnection connection = m_connection;
98+
m_connection.Execute(@"set global log_output = 'table';");
99+
m_connection.Execute(@"set global general_log = 1;");
100+
using (var trans = connection.BeginTransaction(inputIsolationLevel))
101+
{
102+
trans.Commit();
103+
}
104+
105+
m_connection.Execute(@"set global general_log = 0;");
106+
var results = connection.Query<string>($"select convert(argument USING utf8) from mysql.general_log where thread_id = {m_connection.ServerThread} order by event_time desc limit 10;");
107+
var lastStartTransactionQuery = results.First(x => x.ToLower().Contains("start"));
108+
109+
if (IsMySqlAndVersionLessThan57(m_connection.ServerVersion))
110+
{
111+
Assert.Contains("start transaction", lastStartTransactionQuery.ToLower());
112+
return;
113+
}
114+
115+
Assert.Contains(expectedTransactionIsolationLevel.ToLower(), lastStartTransactionQuery.ToLower());
116+
}
117+
54118
[Fact]
55119
public async Task CommitAsync()
56120
{
@@ -210,7 +274,7 @@ public void SavepointRollbackUnknownName()
210274
}
211275
catch (MySqlException ex)
212276
{
213-
Assert.Equal(MySqlErrorCode.StoredProcedureDoesNotExist, (MySqlErrorCode) ex.Number);
277+
Assert.Equal(MySqlErrorCode.StoredProcedureDoesNotExist, (MySqlErrorCode)ex.Number);
214278
}
215279
}
216280

@@ -225,7 +289,7 @@ public void SavepointReleaseUnknownName()
225289
}
226290
catch (MySqlException ex)
227291
{
228-
Assert.Equal(MySqlErrorCode.StoredProcedureDoesNotExist, (MySqlErrorCode) ex.Number);
292+
Assert.Equal(MySqlErrorCode.StoredProcedureDoesNotExist, (MySqlErrorCode)ex.Number);
229293
}
230294
}
231295

@@ -242,7 +306,7 @@ public void SavepointRollbackReleasedName()
242306
}
243307
catch (MySqlException ex)
244308
{
245-
Assert.Equal(MySqlErrorCode.StoredProcedureDoesNotExist, (MySqlErrorCode) ex.Number);
309+
Assert.Equal(MySqlErrorCode.StoredProcedureDoesNotExist, (MySqlErrorCode)ex.Number);
246310
}
247311
}
248312

@@ -318,6 +382,20 @@ public async Task DisposeAsync()
318382
}
319383
#endif
320384

385+
private bool IsMySqlAndVersionLessThan57(string currentVersionStr)
386+
{
387+
var version = new Version("5.7");
388+
Version currentVersion = null;
389+
390+
if (Version.TryParse(currentVersionStr, out currentVersion))
391+
{
392+
var result = version.CompareTo(currentVersion);
393+
return result > 0;
394+
}
395+
396+
return false;
397+
}
398+
321399
readonly TransactionFixture m_database;
322400
readonly MySqlConnection m_connection;
323401
}

0 commit comments

Comments
 (0)