Skip to content

Commit 6df6490

Browse files
committed
Add isReadOnly parameter to BeginTransaction. Fixes #817
1 parent d8627e8 commit 6df6490

File tree

3 files changed

+136
-10
lines changed

3 files changed

+136
-10
lines changed

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

Lines changed: 86 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,22 +31,92 @@ public MySqlConnection(string? connectionString)
3131
m_connectionString = connectionString ?? "";
3232
}
3333

34-
public new MySqlTransaction BeginTransaction() => (MySqlTransaction) base.BeginTransaction();
35-
public new MySqlTransaction BeginTransaction(IsolationLevel isolationLevel) => (MySqlTransaction) base.BeginTransaction(isolationLevel);
36-
protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) => BeginDbTransactionAsync(isolationLevel, IOBehavior.Synchronous, CancellationToken.None).GetAwaiter().GetResult();
34+
/// <summary>
35+
/// Begins a database transaction.
36+
/// </summary>
37+
/// <returns>A <see cref="MySqlTransaction"/> representing the new database transaction.</returns>
38+
/// <remarks>Transactions may not be nested.</remarks>
39+
public new MySqlTransaction BeginTransaction() => BeginTransactionAsync(IsolationLevel.Unspecified, default, IOBehavior.Synchronous, default).GetAwaiter().GetResult();
40+
41+
/// <summary>
42+
/// Begins a database transaction.
43+
/// </summary>
44+
/// <param name="isolationLevel">The <see cref="IsolationLevel"/> for the transaction.</param>
45+
/// <returns>A <see cref="MySqlTransaction"/> representing the new database transaction.</returns>
46+
/// <remarks>Transactions may not be nested.</remarks>
47+
public new MySqlTransaction BeginTransaction(IsolationLevel isolationLevel) => BeginTransactionAsync(isolationLevel, default, IOBehavior.Synchronous, default).GetAwaiter().GetResult();
48+
49+
/// <summary>
50+
/// Begins a database transaction.
51+
/// </summary>
52+
/// <param name="isolationLevel">The <see cref="IsolationLevel"/> for the transaction.</param>
53+
/// <param name="isReadOnly">If <c>true</c>, changes to tables used in the transaction are prohibited; otherwise, they are permitted.</param>
54+
/// <returns>A <see cref="MySqlTransaction"/> representing the new database transaction.</returns>
55+
/// <remarks>Transactions may not be nested.</remarks>
56+
public MySqlTransaction BeginTransaction(IsolationLevel isolationLevel, bool isReadOnly) => BeginTransactionAsync(isolationLevel, isReadOnly, IOBehavior.Synchronous, default).GetAwaiter().GetResult();
57+
58+
protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) => BeginTransactionAsync(isolationLevel, default, IOBehavior.Synchronous, default).GetAwaiter().GetResult();
3759

3860
#if !NETSTANDARD2_1 && !NETCOREAPP3_0
39-
public ValueTask<MySqlTransaction> BeginTransactionAsync(CancellationToken cancellationToken = default) => BeginDbTransactionAsync(IsolationLevel.Unspecified, AsyncIOBehavior, cancellationToken);
40-
public ValueTask<MySqlTransaction> BeginTransactionAsync(IsolationLevel isolationLevel, CancellationToken cancellationToken = default) => BeginDbTransactionAsync(isolationLevel, AsyncIOBehavior, cancellationToken);
61+
/// <summary>
62+
/// Begins a database transaction asynchronously.
63+
/// </summary>
64+
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
65+
/// <returns>A <see cref="Task{MySqlTransaction}"/> representing the new database transaction.</returns>
66+
/// <remarks>Transactions may not be nested.</remarks>
67+
public ValueTask<MySqlTransaction> BeginTransactionAsync(CancellationToken cancellationToken = default) => BeginTransactionAsync(IsolationLevel.Unspecified, default, AsyncIOBehavior, cancellationToken);
68+
69+
/// <summary>
70+
/// Begins a database transaction asynchronously.
71+
/// </summary>
72+
/// <param name="isolationLevel">The <see cref="IsolationLevel"/> for the transaction.</param>
73+
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
74+
/// <returns>A <see cref="Task{MySqlTransaction}"/> representing the new database transaction.</returns>
75+
/// <remarks>Transactions may not be nested.</remarks>
76+
public ValueTask<MySqlTransaction> BeginTransactionAsync(IsolationLevel isolationLevel, CancellationToken cancellationToken = default) => BeginTransactionAsync(isolationLevel, default, AsyncIOBehavior, cancellationToken);
77+
78+
/// <summary>
79+
/// Begins a database transaction asynchronously.
80+
/// </summary>
81+
/// <param name="isolationLevel">The <see cref="IsolationLevel"/> for the transaction.</param>
82+
/// <param name="isReadOnly">If <c>true</c>, changes to tables used in the transaction are prohibited; otherwise, they are permitted.</param>
83+
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
84+
/// <returns>A <see cref="Task{MySqlTransaction}"/> representing the new database transaction.</returns>
85+
/// <remarks>Transactions may not be nested.</remarks>
86+
public ValueTask<MySqlTransaction> BeginTransactionAsync(IsolationLevel isolationLevel, bool isReadOnly, CancellationToken cancellationToken = default) => BeginTransactionAsync(isolationLevel, isReadOnly, AsyncIOBehavior, cancellationToken);
4187
#else
42-
public new ValueTask<MySqlTransaction> BeginTransactionAsync(CancellationToken cancellationToken = default) => BeginDbTransactionAsync(IsolationLevel.Unspecified, AsyncIOBehavior, cancellationToken);
43-
public new ValueTask<MySqlTransaction> BeginTransactionAsync(IsolationLevel isolationLevel, CancellationToken cancellationToken = default) => BeginDbTransactionAsync(isolationLevel, AsyncIOBehavior, cancellationToken);
88+
/// <summary>
89+
/// Begins a database transaction asynchronously.
90+
/// </summary>
91+
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
92+
/// <returns>A <see cref="Task{MySqlTransaction}"/> representing the new database transaction.</returns>
93+
/// <remarks>Transactions may not be nested.</remarks>
94+
public new ValueTask<MySqlTransaction> BeginTransactionAsync(CancellationToken cancellationToken = default) => BeginTransactionAsync(IsolationLevel.Unspecified, default, AsyncIOBehavior, cancellationToken);
95+
96+
/// <summary>
97+
/// Begins a database transaction asynchronously.
98+
/// </summary>
99+
/// <param name="isolationLevel">The <see cref="IsolationLevel"/> for the transaction.</param>
100+
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
101+
/// <returns>A <see cref="Task{MySqlTransaction}"/> representing the new database transaction.</returns>
102+
/// <remarks>Transactions may not be nested.</remarks>
103+
public new ValueTask<MySqlTransaction> BeginTransactionAsync(IsolationLevel isolationLevel, CancellationToken cancellationToken = default) => BeginTransactionAsync(isolationLevel, default, AsyncIOBehavior, cancellationToken);
104+
105+
/// <summary>
106+
/// Begins a database transaction asynchronously.
107+
/// </summary>
108+
/// <param name="isolationLevel">The <see cref="IsolationLevel"/> for the transaction.</param>
109+
/// <param name="isReadOnly">If <c>true</c>, changes to tables used in the transaction are prohibited; otherwise, they are permitted.</param>
110+
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
111+
/// <returns>A <see cref="Task{MySqlTransaction}"/> representing the new database transaction.</returns>
112+
/// <remarks>Transactions may not be nested.</remarks>
113+
public ValueTask<MySqlTransaction> BeginTransactionAsync(IsolationLevel isolationLevel, bool isReadOnly, CancellationToken cancellationToken = default) => BeginTransactionAsync(isolationLevel, isReadOnly, AsyncIOBehavior, cancellationToken);
44114

45115
protected override async ValueTask<DbTransaction> BeginDbTransactionAsync(IsolationLevel isolationLevel, CancellationToken cancellationToken) =>
46-
await BeginDbTransactionAsync(isolationLevel, AsyncIOBehavior, cancellationToken).ConfigureAwait(false);
116+
await BeginTransactionAsync(isolationLevel, default, AsyncIOBehavior, cancellationToken).ConfigureAwait(false);
47117
#endif
48118

49-
private async ValueTask<MySqlTransaction> BeginDbTransactionAsync(IsolationLevel isolationLevel, IOBehavior ioBehavior, CancellationToken cancellationToken)
119+
private async ValueTask<MySqlTransaction> BeginTransactionAsync(IsolationLevel isolationLevel, bool? isReadOnly, IOBehavior ioBehavior, CancellationToken cancellationToken)
50120
{
51121
if (State != ConnectionState.Open)
52122
throw new InvalidOperationException("Connection is not open.");
@@ -76,7 +146,13 @@ private async ValueTask<MySqlTransaction> BeginDbTransactionAsync(IsolationLevel
76146
await cmd.ExecuteNonQueryAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
77147

78148
var consistentSnapshotText = isolationLevel == IsolationLevel.Snapshot ? " with consistent snapshot" : "";
79-
cmd.CommandText = $"start transaction{consistentSnapshotText};";
149+
var readOnlyText = isReadOnly switch
150+
{
151+
true => " read only",
152+
false => " read write",
153+
null => "",
154+
};
155+
cmd.CommandText = $"start transaction{consistentSnapshotText}{readOnlyText};";
80156
await cmd.ExecuteNonQueryAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
81157
}
82158

src/MySqlConnector/MySql.Data.MySqlClient/MySqlErrorCode.g.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3222,5 +3222,10 @@ public enum MySqlErrorCode
32223222
/// ER_DEBUG_SYNC_HIT_LIMIT
32233223
/// </summary>
32243224
DebugSyncHitLimit = 1640,
3225+
3226+
/// <summary>
3227+
/// ER_CANT_EXECUTE_IN_READ_ONLY_TRANSACTION
3228+
/// </summary>
3229+
CannotExecuteInReadOnlyTransaction = 1792,
32253230
}
32263231
}

tests/SideBySide/Transaction.cs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,51 @@ public async Task CommitDisposeAsync()
130130
Assert.Equal(new[] { 1, 2 }, results);
131131
}
132132

133+
[Fact]
134+
public void ReadOnlyTransaction()
135+
{
136+
using var trans = m_connection.BeginTransaction(IsolationLevel.Serializable, isReadOnly: true);
137+
try
138+
{
139+
m_connection.Execute("insert into transactions_test values(1), (2)", transaction: trans);
140+
}
141+
catch (MySqlException ex)
142+
{
143+
Assert.Equal(MySqlErrorCode.CannotExecuteInReadOnlyTransaction, (MySqlErrorCode) ex.Number);
144+
}
145+
}
146+
147+
[Fact]
148+
public void ReadWriteTransaction()
149+
{
150+
using (var trans = m_connection.BeginTransaction(IsolationLevel.Serializable, isReadOnly: false))
151+
{
152+
m_connection.Execute("insert into transactions_test values(1), (2)", transaction: trans);
153+
trans.Commit();
154+
}
155+
var results = m_connection.Query<int>(@"select value from transactions_test order by value;");
156+
Assert.Equal(new[] { 1, 2 }, results);
157+
}
158+
159+
[Fact]
160+
public async Task ReadOnlyTransactionAsync()
161+
{
162+
using var trans = await m_connection.BeginTransactionAsync(IsolationLevel.Serializable, isReadOnly: true);
163+
await Assert.ThrowsAsync<MySqlException>(async () => await m_connection.ExecuteAsync("insert into transactions_test values(1), (2)", transaction: trans));
164+
}
165+
166+
[Fact]
167+
public async Task ReadWriteTransactionAsync()
168+
{
169+
using (var trans = await m_connection.BeginTransactionAsync(IsolationLevel.Serializable, isReadOnly: false))
170+
{
171+
await m_connection.ExecuteAsync("insert into transactions_test values(1), (2)", transaction: trans);
172+
trans.Commit();
173+
}
174+
var results = m_connection.Query<int>(@"select value from transactions_test order by value;");
175+
Assert.Equal(new[] { 1, 2 }, results);
176+
}
177+
133178
#if !NET452 && !NET461 && !NET472 && !NETCOREAPP1_1_2 && !NETCOREAPP2_0 && !NETCOREAPP2_1
134179
[Fact]
135180
public async Task DbConnectionCommitAsync()

0 commit comments

Comments
 (0)