Skip to content

Commit aba7c0a

Browse files
committed
Merge prepared-commands into master.
This caches prepared commands client-side for the lifetime of the prepared command on the server.
2 parents d851b9b + a04dac0 commit aba7c0a

File tree

6 files changed

+164
-36
lines changed

6 files changed

+164
-36
lines changed

src/MySqlConnector/Core/PreparedStatementCommandExecutor.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,18 @@ namespace MySqlConnector.Core
1515
{
1616
internal sealed class PreparedStatementCommandExecutor : ICommandExecutor
1717
{
18-
public PreparedStatementCommandExecutor(MySqlCommand command)
18+
public PreparedStatementCommandExecutor(MySqlCommand command, PreparedStatements preparedStatements)
1919
{
2020
m_command = command;
21+
m_preparedStatements = preparedStatements;
2122
}
2223

2324
public async Task<DbDataReader> ExecuteReaderAsync(string commandText, MySqlParameterCollection parameterCollection, CommandBehavior behavior, IOBehavior ioBehavior, CancellationToken cancellationToken)
2425
{
2526
cancellationToken.ThrowIfCancellationRequested();
2627
if (Log.IsDebugEnabled())
2728
Log.Debug("Session{0} ExecuteBehavior {1} CommandText: {2}", m_command.Connection.Session.Id, ioBehavior, commandText);
28-
using (var payload = CreateQueryPayload(m_command.PreparedStatements[0], parameterCollection, m_command.Connection.GuidFormat))
29+
using (var payload = CreateQueryPayload(m_preparedStatements.Statements[0], parameterCollection, m_command.Connection.GuidFormat))
2930
using (m_command.RegisterCancel(cancellationToken))
3031
{
3132
m_command.Connection.Session.StartQuerying(m_command);
@@ -105,5 +106,6 @@ private PayloadData CreateQueryPayload(PreparedStatement preparedStatement, MySq
105106
static IMySqlConnectorLogger Log { get; } = MySqlConnectorLogManager.CreateLogger(nameof(PreparedStatementCommandExecutor));
106107

107108
readonly MySqlCommand m_command;
109+
readonly PreparedStatements m_preparedStatements;
108110
}
109111
}

src/MySqlConnector/Core/ServerSession.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Buffers.Text;
3+
using System.Collections.Generic;
34
using System.Data;
45
using System.Diagnostics;
56
using System.Globalization;
@@ -126,6 +127,23 @@ public void AbortCancel(MySqlCommand command)
126127
}
127128
}
128129

130+
public void AddPreparedStatement(string commandText, PreparedStatements preparedStatements)
131+
{
132+
if (m_preparedStatements == null)
133+
m_preparedStatements = new Dictionary<string, PreparedStatements>();
134+
m_preparedStatements.Add(commandText, preparedStatements);
135+
}
136+
137+
public PreparedStatements TryGetPreparedStatement(string commandText)
138+
{
139+
if (m_preparedStatements != null)
140+
{
141+
if (m_preparedStatements.TryGetValue(commandText, out var statement))
142+
return statement;
143+
}
144+
return null;
145+
}
146+
129147
public void StartQuerying(MySqlCommand command)
130148
{
131149
lock (m_lock)
@@ -218,6 +236,8 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
218236
}
219237
}
220238

239+
ClearPreparedStatements();
240+
221241
ShutdownSocket();
222242
lock (m_lock)
223243
m_state = State.Closed;
@@ -367,6 +387,9 @@ public async Task<bool> TryResetConnectionAsync(ConnectionSettings cs, IOBehavio
367387

368388
try
369389
{
390+
// clear all prepared statements; resetting the connection will clear them on the server
391+
ClearPreparedStatements();
392+
370393
if (DatabaseOverride == null && ServerVersion.Version.CompareTo(ServerVersions.SupportsResetConnection) >= 0)
371394
{
372395
m_logArguments[1] = ServerVersion.OriginalString;
@@ -1322,6 +1345,16 @@ private Exception CreateExceptionForErrorPayload(PayloadData payload)
13221345
return exception;
13231346
}
13241347

1348+
private void ClearPreparedStatements()
1349+
{
1350+
if (m_preparedStatements != null)
1351+
{
1352+
foreach (var pair in m_preparedStatements)
1353+
pair.Value.Dispose();
1354+
m_preparedStatements.Clear();
1355+
}
1356+
}
1357+
13251358
private enum State
13261359
{
13271360
// The session has been created; no connection has been made.
@@ -1372,5 +1405,6 @@ private enum State
13721405
bool m_isSecureConnection;
13731406
bool m_supportsConnectionAttributes;
13741407
bool m_supportsDeprecateEof;
1408+
Dictionary<string, PreparedStatements> m_preparedStatements;
13751409
}
13761410
}

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

Lines changed: 49 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -65,26 +65,56 @@ public MySqlCommand(string commandText, MySqlConnection connection, MySqlTransac
6565

6666
public new MySqlDataReader ExecuteReader(CommandBehavior commandBehavior) => (MySqlDataReader) base.ExecuteReader(commandBehavior);
6767

68-
public override void Prepare() => PrepareAsync(IOBehavior.Synchronous, default).GetAwaiter().GetResult();
68+
public override void Prepare()
69+
{
70+
if (!NeedsPrepare(out var exception))
71+
{
72+
if (exception != null)
73+
throw exception;
74+
return;
75+
}
76+
77+
DoPrepareAsync(IOBehavior.Synchronous, default).GetAwaiter().GetResult();
78+
}
79+
6980
public Task PrepareAsync() => PrepareAsync(AsyncIOBehavior, default);
7081
public Task PrepareAsync(CancellationToken cancellationToken) => PrepareAsync(AsyncIOBehavior, cancellationToken);
71-
72-
private async Task PrepareAsync(IOBehavior ioBehavior, CancellationToken cancellationToken)
82+
83+
private Task PrepareAsync(IOBehavior ioBehavior, CancellationToken cancellationToken)
7384
{
85+
if (!NeedsPrepare(out var exception))
86+
return exception != null ? Utility.TaskFromException(exception) : Utility.CompletedTask;
87+
88+
return DoPrepareAsync(ioBehavior, cancellationToken);
89+
}
90+
91+
private bool NeedsPrepare(out Exception exception)
92+
{
93+
exception = null;
7494
if (Connection == null)
75-
throw new InvalidOperationException("Connection property must be non-null.");
76-
if (Connection.State != ConnectionState.Open)
77-
throw new InvalidOperationException("Connection must be Open; current state is {0}".FormatInvariant(Connection.State));
78-
if (string.IsNullOrWhiteSpace(CommandText))
79-
throw new InvalidOperationException("CommandText must be specified");
80-
if (m_connection?.HasActiveReader ?? false)
81-
throw new InvalidOperationException("Cannot call Prepare when there is an open DataReader for this command; it must be closed first.");
82-
if (Connection.IgnorePrepare)
83-
return;
95+
exception = new InvalidOperationException("Connection property must be non-null.");
96+
else if (Connection.State != ConnectionState.Open)
97+
exception = new InvalidOperationException("Connection must be Open; current state is {0}".FormatInvariant(Connection.State));
98+
else if (string.IsNullOrWhiteSpace(CommandText))
99+
exception = new InvalidOperationException("CommandText must be specified");
100+
else if (Connection?.HasActiveReader ?? false)
101+
exception = new InvalidOperationException("Cannot call Prepare when there is an open DataReader for this command; it must be closed first.");
102+
103+
if (exception != null || Connection.IgnorePrepare)
104+
return false;
84105

85106
if (CommandType != CommandType.Text)
86-
throw new NotSupportedException("Only CommandType.Text is currently supported by MySqlCommand.Prepare");
107+
{
108+
exception = new NotSupportedException("Only CommandType.Text is currently supported by MySqlCommand.Prepare");
109+
return false;
110+
}
111+
112+
// don't prepare the same SQL twice
113+
return Connection.Session.TryGetPreparedStatement(CommandText) == null;
114+
}
87115

116+
private async Task DoPrepareAsync(IOBehavior ioBehavior, CancellationToken cancellationToken)
117+
{
88118
var statementPreparer = new StatementPreparer(CommandText, Parameters, CreateStatementPreparerOptions());
89119
var parsedStatements = statementPreparer.SplitStatements();
90120

@@ -142,8 +172,7 @@ private async Task PrepareAsync(IOBehavior ioBehavior, CancellationToken cancell
142172
preparedStatements.Add(new PreparedStatement(response.StatementId, statement, columns, parameters));
143173
}
144174

145-
m_parsedStatements = parsedStatements;
146-
m_statements = preparedStatements;
175+
Connection.Session.AddPreparedStatement(CommandText, new PreparedStatements(preparedStatements, parsedStatements));
147176
}
148177

149178
public override string CommandText
@@ -154,11 +183,10 @@ public override string CommandText
154183
if (m_connection?.HasActiveReader ?? false)
155184
throw new InvalidOperationException("Cannot set MySqlCommand.CommandText when there is an open DataReader for this command; it must be closed first.");
156185
m_commandText = value;
157-
ClearPreparedStatements();
158186
}
159187
}
160188

161-
public bool IsPrepared => m_statements != null;
189+
public bool IsPrepared => TryGetPreparedStatement() != null;
162190

163191
public new MySqlTransaction Transaction { get; set; }
164192

@@ -170,7 +198,6 @@ public override string CommandText
170198
if (m_connection?.HasActiveReader ?? false)
171199
throw new InvalidOperationException("Cannot set MySqlCommand.Connection when there is an open DataReader for this command; it must be closed first.");
172200
m_connection = value;
173-
ClearPreparedStatements();
174201
}
175202
}
176203

@@ -188,7 +215,6 @@ public override CommandType CommandType
188215
if (value != CommandType.Text && value != CommandType.StoredProcedure)
189216
throw new ArgumentException("CommandType must be Text or StoredProcedure.", nameof(value));
190217
m_commandType = value;
191-
ClearPreparedStatements();
192218
}
193219
}
194220

@@ -277,8 +303,9 @@ internal Task<DbDataReader> ExecuteReaderAsync(CommandBehavior behavior, IOBehav
277303
if (!IsValid(out var exception))
278304
return Utility.TaskFromException<DbDataReader>(exception);
279305

280-
if (m_statements != null)
281-
m_commandExecutor = new PreparedStatementCommandExecutor(this);
306+
var preparedStatements = TryGetPreparedStatement();
307+
if (preparedStatements != null)
308+
m_commandExecutor = new PreparedStatementCommandExecutor(this, preparedStatements);
282309
else if (m_commandType == CommandType.Text)
283310
m_commandExecutor = new TextCommandExecutor(this);
284311
else if (m_commandType == CommandType.StoredProcedure)
@@ -292,10 +319,7 @@ protected override void Dispose(bool disposing)
292319
try
293320
{
294321
if (disposing)
295-
{
296322
m_parameterCollection = null;
297-
ClearPreparedStatements();
298-
}
299323
}
300324
finally
301325
{
@@ -324,8 +348,6 @@ internal IDisposable RegisterCancel(CancellationToken token)
324348

325349
internal int CancelAttemptCount { get; set; }
326350

327-
internal IReadOnlyList<PreparedStatement> PreparedStatements => m_statements;
328-
329351
/// <summary>
330352
/// Causes the effective command timeout to be reset back to the value specified by <see cref="CommandTimeout"/>.
331353
/// </summary>
@@ -400,12 +422,7 @@ private bool IsValid(out Exception exception)
400422
return exception == null;
401423
}
402424

403-
private void ClearPreparedStatements()
404-
{
405-
m_parsedStatements?.Dispose();
406-
m_parsedStatements = null;
407-
m_statements = null;
408-
}
425+
private PreparedStatements TryGetPreparedStatement() => CommandType == CommandType.Text && !string.IsNullOrWhiteSpace(CommandText) ? m_connection.Session.TryGetPreparedStatement(CommandText) : null;
409426

410427
internal void ReaderClosed() => (m_commandExecutor as StoredProcedureCommandExecutor)?.SetParams();
411428

@@ -414,8 +431,6 @@ private void ClearPreparedStatements()
414431
MySqlConnection m_connection;
415432
string m_commandText;
416433
MySqlParameterCollection m_parameterCollection;
417-
ParsedStatements m_parsedStatements;
418-
IReadOnlyList<PreparedStatement> m_statements;
419434
int? m_commandTimeout;
420435
CommandType m_commandType;
421436
ICommandExecutor m_commandExecutor;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
namespace MySqlConnector.Core
5+
{
6+
internal sealed class PreparedStatements : IDisposable
7+
{
8+
public IReadOnlyList<PreparedStatement> Statements { get; }
9+
10+
public PreparedStatements(IReadOnlyList<PreparedStatement> preparedStatements, ParsedStatements parsedStatements)
11+
{
12+
Statements = preparedStatements;
13+
m_parsedStatements = parsedStatements;
14+
}
15+
16+
public void Dispose()
17+
{
18+
m_parsedStatements?.Dispose();
19+
m_parsedStatements = null;
20+
}
21+
22+
ParsedStatements m_parsedStatements;
23+
}
24+
}

src/MySqlConnector/Utilities/Utility.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,13 +301,31 @@ public static TimeSpan ParseTimeSpan(ReadOnlySpan<byte> value)
301301
}
302302

303303
#if NET45
304+
public static Task CompletedTask
305+
{
306+
get
307+
{
308+
if (s_completedTask == null)
309+
{
310+
var tcs = new TaskCompletionSource<object>();
311+
tcs.SetResult(null);
312+
s_completedTask = tcs.Task;
313+
}
314+
return s_completedTask;
315+
}
316+
}
317+
static Task s_completedTask;
318+
319+
public static Task TaskFromException(Exception exception) => TaskFromException<object>(exception);
304320
public static Task<T> TaskFromException<T>(Exception exception)
305321
{
306322
var tcs = new TaskCompletionSource<T>();
307323
tcs.SetException(exception);
308324
return tcs.Task;
309325
}
310326
#else
327+
public static Task CompletedTask => Task.CompletedTask;
328+
public static Task TaskFromException(Exception exception) => Task.FromException(exception);
311329
public static Task<T> TaskFromException<T>(Exception exception) => Task.FromException<T>(exception);
312330
#endif
313331

tests/SideBySide/PreparedCommandTests.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,41 @@ public void InsertAndQuery(bool isPrepared, string dataType, object dataValue)
131131
}
132132
}
133133

134+
[Fact]
135+
public void PrepareMultipleTimes()
136+
{
137+
using (var connection = CreatePrepareConnection())
138+
{
139+
using (var cmd = new MySqlCommand("SELECT 'test';", connection))
140+
{
141+
Assert.False(cmd.IsPrepared);
142+
cmd.Prepare();
143+
Assert.True(cmd.IsPrepared);
144+
cmd.Prepare();
145+
Assert.Equal("test", cmd.ExecuteScalar());
146+
}
147+
}
148+
}
149+
150+
[SkippableFact(Baseline = "Connector/NET doesn't cache prepared commands")]
151+
public void PreparedCommandIsCached()
152+
{
153+
using (var connection = CreatePrepareConnection())
154+
{
155+
using (var cmd = new MySqlCommand("SELECT 'test';", connection))
156+
{
157+
cmd.Prepare();
158+
Assert.Equal("test", cmd.ExecuteScalar());
159+
}
160+
161+
using (var cmd = new MySqlCommand("SELECT 'test';", connection))
162+
{
163+
Assert.True(cmd.IsPrepared);
164+
Assert.Equal("test", cmd.ExecuteScalar());
165+
}
166+
}
167+
}
168+
134169
public static IEnumerable<object[]> GetInsertAndQueryData()
135170
{
136171
foreach (var isPrepared in new[] { false, true })

0 commit comments

Comments
 (0)