Skip to content

Commit a395d46

Browse files
committed
Add transaction savepoint API. Fixes #775
1 parent aae6b72 commit a395d46

File tree

3 files changed

+182
-38
lines changed

3 files changed

+182
-38
lines changed

docs/content/api/mysql-transaction.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,25 @@ Async version of Commit
2929
`public Task RollbackAsync(CancellationToken cancellationToken)`
3030

3131
Async version of Rollback
32-
***
32+
***
33+
`public Task Save(string savepointName)`
34+
35+
`public Task SaveAsync(string savepointName, CancellationToken cancellationToken = default)`
36+
37+
Sets a named transaction savepoint with the specified `savepointName`. If the current transaction already has
38+
a savepoint with the same name, the old savepoint is deleted and a new one is set.
39+
***
40+
`public Task Release(string savepointName)`
41+
42+
`public Task ReleaseAsync(string savepointName, CancellationToken cancellationToken = default)`
43+
44+
Removes the named transaction savepoint with the specified `savepointName`. No commit or rollback occurs.
45+
***
46+
***
47+
`public Task Rollback(string savepointName)`
48+
49+
`public Task RollbackAsync(string savepointName, CancellationToken cancellationToken = default)`
50+
51+
Rolls back the current transaction to the savepoint with the specified `savepointName` without aborting the transaction.
52+
The name must have been created with `Save`, but not released by calling `Release`.
53+
***

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

Lines changed: 41 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -19,25 +19,12 @@ public sealed class MySqlTransaction : DbTransaction
1919

2020
private async Task CommitAsync(IOBehavior ioBehavior, CancellationToken cancellationToken)
2121
{
22-
VerifyNotDisposed();
23-
if (Connection is null)
24-
throw new InvalidOperationException("Already committed or rolled back.");
22+
VerifyValid();
2523

26-
if (Connection.CurrentTransaction == this)
27-
{
28-
using (var cmd = new MySqlCommand("commit", Connection, this))
29-
await cmd.ExecuteNonQueryAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
30-
Connection.CurrentTransaction = null;
31-
Connection = null;
32-
}
33-
else if (Connection.CurrentTransaction is object)
34-
{
35-
throw new InvalidOperationException("This is not the active transaction.");
36-
}
37-
else if (Connection.CurrentTransaction is null)
38-
{
39-
throw new InvalidOperationException("There is no active transaction.");
40-
}
24+
using (var cmd = new MySqlCommand("commit", Connection, this))
25+
await cmd.ExecuteNonQueryAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
26+
Connection!.CurrentTransaction = null;
27+
Connection = null;
4128
}
4229

4330
public override void Rollback() => RollbackAsync(IOBehavior.Synchronous, default).GetAwaiter().GetResult();
@@ -49,25 +36,34 @@ private async Task CommitAsync(IOBehavior ioBehavior, CancellationToken cancella
4936

5037
private async Task RollbackAsync(IOBehavior ioBehavior, CancellationToken cancellationToken)
5138
{
52-
VerifyNotDisposed();
53-
if (Connection is null)
54-
throw new InvalidOperationException("Already committed or rolled back.");
39+
VerifyValid();
5540

56-
if (Connection.CurrentTransaction == this)
57-
{
58-
using (var cmd = new MySqlCommand("rollback", Connection, this))
59-
await cmd.ExecuteNonQueryAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
60-
Connection.CurrentTransaction = null;
61-
Connection = null;
62-
}
63-
else if (Connection.CurrentTransaction is object)
64-
{
65-
throw new InvalidOperationException("This is not the active transaction.");
66-
}
67-
else if (Connection.CurrentTransaction is null)
68-
{
69-
throw new InvalidOperationException("There is no active transaction.");
70-
}
41+
using (var cmd = new MySqlCommand("rollback", Connection, this))
42+
await cmd.ExecuteNonQueryAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
43+
Connection!.CurrentTransaction = null;
44+
Connection = null;
45+
}
46+
47+
public void Release(string savepointName) => ExecuteSavepointAsync("release ", savepointName, IOBehavior.Synchronous, default).GetAwaiter().GetResult();
48+
public Task ReleaseAsync(string savepointName, CancellationToken cancellationToken = default) => ExecuteSavepointAsync("release ", savepointName, Connection?.AsyncIOBehavior ?? IOBehavior.Asynchronous, cancellationToken);
49+
50+
public void Rollback(string savepointName) => ExecuteSavepointAsync("rollback to ", savepointName, IOBehavior.Synchronous, default).GetAwaiter().GetResult();
51+
public Task RollbackAsync(string savepointName, CancellationToken cancellationToken = default) => ExecuteSavepointAsync("rollback to ", savepointName, Connection?.AsyncIOBehavior ?? IOBehavior.Asynchronous, cancellationToken);
52+
53+
public void Save(string savepointName) => ExecuteSavepointAsync("", savepointName, IOBehavior.Synchronous, default).GetAwaiter().GetResult();
54+
public Task SaveAsync(string savepointName, CancellationToken cancellationToken = default) => ExecuteSavepointAsync("", savepointName, Connection?.AsyncIOBehavior ?? IOBehavior.Asynchronous, cancellationToken);
55+
56+
private async Task ExecuteSavepointAsync(string command, string savepointName, IOBehavior ioBehavior, CancellationToken cancellationToken)
57+
{
58+
VerifyValid();
59+
60+
if (savepointName is null)
61+
throw new ArgumentNullException(nameof(savepointName));
62+
if (savepointName.Length == 0)
63+
throw new ArgumentException("savepointName must not be empty", nameof(savepointName));
64+
65+
using var cmd = new MySqlCommand(command + "savepoint " + QuoteIdentifier(savepointName), Connection, this);
66+
await cmd.ExecuteNonQueryAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
7167
}
7268

7369
public new MySqlConnection? Connection { get; private set; }
@@ -130,12 +126,20 @@ internal MySqlTransaction(MySqlConnection connection, IsolationLevel isolationLe
130126
IsolationLevel = isolationLevel;
131127
}
132128

133-
private void VerifyNotDisposed()
129+
private void VerifyValid()
134130
{
135131
if (m_isDisposed)
136132
throw new ObjectDisposedException(nameof(MySqlTransaction));
133+
if (Connection is null)
134+
throw new InvalidOperationException("Already committed or rolled back.");
135+
if (Connection.CurrentTransaction is null)
136+
throw new InvalidOperationException("There is no active transaction.");
137+
if (!object.ReferenceEquals(Connection.CurrentTransaction, this))
138+
throw new InvalidOperationException("This is not the active transaction.");
137139
}
138140

141+
private static string QuoteIdentifier(string identifier) => "`" + identifier.Replace("`", "``") + "`";
142+
139143
bool m_isDisposed;
140144
}
141145
}

tests/SideBySide/Transaction.cs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,125 @@ public void NoCommit()
181181
}
182182

183183
#if !BASELINE
184+
[Fact]
185+
public void SavepointNullName()
186+
{
187+
using var transaction = m_connection.BeginTransaction();
188+
Assert.Throws<ArgumentNullException>("savepointName", () => transaction.Save(null));
189+
Assert.Throws<ArgumentNullException>("savepointName", () => transaction.Release(null));
190+
Assert.Throws<ArgumentNullException>("savepointName", () => transaction.Rollback(null));
191+
}
192+
193+
[Fact]
194+
public void SavepointEmptyName()
195+
{
196+
using var transaction = m_connection.BeginTransaction();
197+
Assert.Throws<ArgumentException>("savepointName", () => transaction.Save(""));
198+
Assert.Throws<ArgumentException>("savepointName", () => transaction.Release(""));
199+
Assert.Throws<ArgumentException>("savepointName", () => transaction.Rollback(""));
200+
}
201+
202+
[Fact]
203+
public void SavepointRollbackUnknownName()
204+
{
205+
using var transaction = m_connection.BeginTransaction();
206+
try
207+
{
208+
transaction.Rollback("a");
209+
Assert.True(false);
210+
}
211+
catch (MySqlException ex)
212+
{
213+
Assert.Equal(MySqlErrorCode.StoredProcedureDoesNotExist, (MySqlErrorCode) ex.Number);
214+
}
215+
}
216+
217+
[Fact]
218+
public void SavepointReleaseUnknownName()
219+
{
220+
using var transaction = m_connection.BeginTransaction();
221+
try
222+
{
223+
transaction.Release("a");
224+
Assert.True(false);
225+
}
226+
catch (MySqlException ex)
227+
{
228+
Assert.Equal(MySqlErrorCode.StoredProcedureDoesNotExist, (MySqlErrorCode) ex.Number);
229+
}
230+
}
231+
232+
[Fact]
233+
public void SavepointRollbackReleasedName()
234+
{
235+
using var transaction = m_connection.BeginTransaction();
236+
transaction.Save("a");
237+
transaction.Release("a");
238+
try
239+
{
240+
transaction.Rollback("a");
241+
Assert.True(false);
242+
}
243+
catch (MySqlException ex)
244+
{
245+
Assert.Equal(MySqlErrorCode.StoredProcedureDoesNotExist, (MySqlErrorCode) ex.Number);
246+
}
247+
}
248+
249+
[Fact]
250+
public void RollbackSavepoint()
251+
{
252+
using (var trans = m_connection.BeginTransaction())
253+
{
254+
m_connection.Execute("insert into transactions_test values(1), (2)", transaction: trans);
255+
trans.Save("a");
256+
m_connection.Execute("insert into transactions_test values(3), (4)", transaction: trans);
257+
trans.Save("b");
258+
m_connection.Execute("insert into transactions_test values(5), (6)", transaction: trans);
259+
trans.Rollback("a");
260+
trans.Commit();
261+
}
262+
var results = m_connection.Query<int>(@"select value from transactions_test order by value;");
263+
Assert.Equal(new int[] { 1, 2 }, results);
264+
}
265+
266+
[Fact]
267+
public async Task RollbackSavepointAsync()
268+
{
269+
using (var trans = await m_connection.BeginTransactionAsync())
270+
{
271+
await m_connection.ExecuteAsync("insert into transactions_test values(1), (2)", transaction: trans);
272+
await trans.SaveAsync("a");
273+
await m_connection.ExecuteAsync("insert into transactions_test values(3), (4)", transaction: trans);
274+
await trans.SaveAsync("b");
275+
await m_connection.ExecuteAsync("insert into transactions_test values(5), (6)", transaction: trans);
276+
await trans.RollbackAsync("a");
277+
await trans.CommitAsync();
278+
}
279+
var results = await m_connection.QueryAsync<int>(@"select value from transactions_test order by value;");
280+
Assert.Equal(new int[] { 1, 2 }, results);
281+
}
282+
283+
[Fact]
284+
public void SavepointRolledBackTransaction()
285+
{
286+
using (var transaction = m_connection.BeginTransaction())
287+
{
288+
transaction.Rollback();
289+
Assert.Throws<InvalidOperationException>(() => transaction.Save("a"));
290+
}
291+
}
292+
293+
[Fact]
294+
public void SavepointCommittedTransaction()
295+
{
296+
using (var transaction = m_connection.BeginTransaction())
297+
{
298+
transaction.Commit();
299+
Assert.Throws<InvalidOperationException>(() => transaction.Save("a"));
300+
}
301+
}
302+
184303
[Fact]
185304
public async Task DisposeAsync()
186305
{

0 commit comments

Comments
 (0)