Skip to content

Commit 0f95533

Browse files
committed
Implement MySqlBatch.Prepare. Fixes #656
1 parent 3befefe commit 0f95533

File tree

6 files changed

+164
-74
lines changed

6 files changed

+164
-74
lines changed

src/MySqlConnector/Core/ConcatenatedCommandPayloadCreator.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,6 @@ public bool WriteQueryCommand(ref CommandListPosition commandListPosition, IDict
2323
do
2424
{
2525
var command = commandListPosition.Commands[commandListPosition.CommandIndex];
26-
if (command.TryGetPreparedStatements() is object)
27-
throw new InvalidOperationException("Can't send prepared statements as part of a concatenated batch.");
28-
2926
if (Log.IsDebugEnabled())
3027
Log.Debug("Session{0} Preparing command payload; CommandText: {1}", command.Connection.Session.Id, command.CommandText);
3128

src/MySqlConnector/Core/IMySqlCommand.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
using System;
22
using System.Data;
3-
using System.Threading;
43
using MySql.Data.MySqlClient;
5-
using MySqlConnector.Utilities;
64

75
namespace MySqlConnector.Core
86
{

src/MySqlConnector/Core/ServerSession.cs

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,11 +133,65 @@ public void AbortCancel(ICancellableCommand command)
133133

134134
public bool IsCancelingQuery => m_state == State.CancelingQuery;
135135

136-
public void AddPreparedStatement(string commandText, PreparedStatements preparedStatements)
136+
public async Task PrepareAsync(IMySqlCommand command, IOBehavior ioBehavior, CancellationToken cancellationToken)
137137
{
138+
var statementPreparer = new StatementPreparer(command.CommandText, command.RawParameters, command.CreateStatementPreparerOptions());
139+
var parsedStatements = statementPreparer.SplitStatements();
140+
141+
var columnsAndParameters = new ResizableArray<byte>();
142+
var columnsAndParametersSize = 0;
143+
144+
var preparedStatements = new List<PreparedStatement>(parsedStatements.Statements.Count);
145+
foreach (var statement in parsedStatements.Statements)
146+
{
147+
await SendAsync(new PayloadData(statement.StatementBytes), ioBehavior, cancellationToken).ConfigureAwait(false);
148+
var payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
149+
var response = StatementPrepareResponsePayload.Create(payload.AsSpan());
150+
151+
ColumnDefinitionPayload[] parameters = null;
152+
if (response.ParameterCount > 0)
153+
{
154+
parameters = new ColumnDefinitionPayload[response.ParameterCount];
155+
for (var i = 0; i < response.ParameterCount; i++)
156+
{
157+
payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
158+
Utility.Resize(ref columnsAndParameters, columnsAndParametersSize + payload.ArraySegment.Count);
159+
Buffer.BlockCopy(payload.ArraySegment.Array, payload.ArraySegment.Offset, columnsAndParameters.Array, columnsAndParametersSize, payload.ArraySegment.Count);
160+
parameters[i] = ColumnDefinitionPayload.Create(new ResizableArraySegment<byte>(columnsAndParameters, columnsAndParametersSize, payload.ArraySegment.Count));
161+
columnsAndParametersSize += payload.ArraySegment.Count;
162+
}
163+
if (!SupportsDeprecateEof)
164+
{
165+
payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
166+
EofPayload.Create(payload.AsSpan());
167+
}
168+
}
169+
170+
ColumnDefinitionPayload[] columns = null;
171+
if (response.ColumnCount > 0)
172+
{
173+
columns = new ColumnDefinitionPayload[response.ColumnCount];
174+
for (var i = 0; i < response.ColumnCount; i++)
175+
{
176+
payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
177+
Utility.Resize(ref columnsAndParameters, columnsAndParametersSize + payload.ArraySegment.Count);
178+
Buffer.BlockCopy(payload.ArraySegment.Array, payload.ArraySegment.Offset, columnsAndParameters.Array, columnsAndParametersSize, payload.ArraySegment.Count);
179+
columns[i] = ColumnDefinitionPayload.Create(new ResizableArraySegment<byte>(columnsAndParameters, columnsAndParametersSize, payload.ArraySegment.Count));
180+
columnsAndParametersSize += payload.ArraySegment.Count;
181+
}
182+
if (!SupportsDeprecateEof)
183+
{
184+
payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
185+
EofPayload.Create(payload.AsSpan());
186+
}
187+
}
188+
189+
preparedStatements.Add(new PreparedStatement(response.StatementId, statement, columns, parameters));
190+
}
191+
138192
if (m_preparedStatements is null)
139193
m_preparedStatements = new Dictionary<string, PreparedStatements>();
140-
m_preparedStatements.Add(commandText, preparedStatements);
194+
m_preparedStatements.Add(command.CommandText, new PreparedStatements(preparedStatements, parsedStatements));
141195
}
142196

143197
public PreparedStatements TryGetPreparedStatement(string commandText)

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

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ private Task<DbDataReader> ExecuteReaderAsync(IOBehavior ioBehavior, Cancellatio
123123
batchCommand.Batch = this;
124124

125125
var payloadCreator = Connection.Session.SupportsComMulti ? BatchedCommandPayloadCreator.Instance :
126-
// TODO: IsPrepared ? SingleCommandPayloadCreator.Instance :
126+
IsPrepared ? SingleCommandPayloadCreator.Instance :
127127
ConcatenatedCommandPayloadCreator.Instance;
128128
return CommandExecutor.ExecuteReaderAsync(BatchCommands, payloadCreator, CommandBehavior.Default, ioBehavior, cancellationToken);
129129
}
@@ -138,9 +138,19 @@ private Task<DbDataReader> ExecuteReaderAsync(IOBehavior ioBehavior, Cancellatio
138138

139139
public override int Timeout { get; set; }
140140

141-
public override void Prepare() => throw new NotImplementedException();
141+
public override void Prepare()
142+
{
143+
if (!NeedsPrepare(out var exception))
144+
{
145+
if (exception is object)
146+
throw exception;
147+
return;
148+
}
149+
150+
DoPrepareAsync(IOBehavior.Synchronous, default).GetAwaiter().GetResult();
151+
}
142152

143-
public override Task PrepareAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException();
153+
public override Task PrepareAsync(CancellationToken cancellationToken = default) => PrepareAsync(AsyncIOBehavior, cancellationToken);
144154

145155
public override void Cancel() => Connection?.Cancel(this);
146156

@@ -235,6 +245,56 @@ private bool IsValid(out Exception exception)
235245
return exception is null;
236246
}
237247

248+
private bool NeedsPrepare(out Exception exception)
249+
{
250+
exception = null;
251+
if (Connection is null)
252+
exception = new InvalidOperationException("Connection property must be non-null.");
253+
else if (Connection.State != ConnectionState.Open)
254+
exception = new InvalidOperationException("Connection must be Open; current state is {0}".FormatInvariant(Connection.State));
255+
else if (BatchCommands.Count == 0)
256+
exception = new InvalidOperationException("BatchCommands must contain a command");
257+
else if (Connection?.HasActiveReader ?? false)
258+
exception = new InvalidOperationException("Cannot call Prepare when there is an open DataReader for this command; it must be closed first.");
259+
260+
return exception is null && !Connection.IgnorePrepare;
261+
}
262+
263+
private Task PrepareAsync(IOBehavior ioBehavior, CancellationToken cancellationToken)
264+
{
265+
if (!NeedsPrepare(out var exception))
266+
return exception is null ? Utility.CompletedTask : Utility.TaskFromException(exception);
267+
268+
return DoPrepareAsync(ioBehavior, cancellationToken);
269+
}
270+
271+
private async Task DoPrepareAsync(IOBehavior ioBehavior, CancellationToken cancellationToken)
272+
{
273+
foreach (IMySqlCommand batchCommand in BatchCommands)
274+
{
275+
if (batchCommand.CommandType != CommandType.Text)
276+
throw new NotSupportedException("Only CommandType.Text is currently supported by MySqlBatch.Prepare");
277+
((MySqlBatchCommand) batchCommand).Batch = this;
278+
279+
// don't prepare the same SQL twice
280+
if (Connection.Session.TryGetPreparedStatement(batchCommand.CommandText) is null)
281+
await Connection.Session.PrepareAsync(batchCommand, ioBehavior, cancellationToken).ConfigureAwait(false);
282+
}
283+
}
284+
285+
private bool IsPrepared
286+
{
287+
get
288+
{
289+
foreach (var command in BatchCommands)
290+
{
291+
if (Connection.Session.TryGetPreparedStatement(command.CommandText) is null)
292+
return false;
293+
}
294+
return true;
295+
}
296+
}
297+
238298
private IOBehavior AsyncIOBehavior => Connection?.AsyncIOBehavior ?? IOBehavior.Asynchronous;
239299

240300
readonly int m_commandId;

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

Lines changed: 2 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
using System;
2-
using System.Collections.Generic;
32
using System.Data;
43
using System.Data.Common;
54
using System.Threading;
65
using System.Threading.Tasks;
76
using MySqlConnector.Core;
8-
using MySqlConnector.Protocol;
9-
using MySqlConnector.Protocol.Payloads;
107
using MySqlConnector.Protocol.Serialization;
118
using MySqlConnector.Utilities;
129

@@ -91,7 +88,7 @@ public override void Prepare()
9188
return;
9289
}
9390

94-
DoPrepareAsync(IOBehavior.Synchronous, default).GetAwaiter().GetResult();
91+
Connection.Session.PrepareAsync(this, IOBehavior.Synchronous, default).GetAwaiter().GetResult();
9592
}
9693

9794
#if !NETSTANDARD2_1 && !NETCOREAPP3_0
@@ -115,7 +112,7 @@ private Task PrepareAsync(IOBehavior ioBehavior, CancellationToken cancellationT
115112
if (!NeedsPrepare(out var exception))
116113
return exception is null ? Utility.CompletedTask : Utility.TaskFromException(exception);
117114

118-
return DoPrepareAsync(ioBehavior, cancellationToken);
115+
return Connection.Session.PrepareAsync(this, ioBehavior, cancellationToken);
119116
}
120117

121118
private bool NeedsPrepare(out Exception exception)
@@ -143,65 +140,6 @@ private bool NeedsPrepare(out Exception exception)
143140
return Connection.Session.TryGetPreparedStatement(CommandText) is null;
144141
}
145142

146-
private async Task DoPrepareAsync(IOBehavior ioBehavior, CancellationToken cancellationToken)
147-
{
148-
var statementPreparer = new StatementPreparer(CommandText, m_parameterCollection, ((IMySqlCommand) this).CreateStatementPreparerOptions());
149-
var parsedStatements = statementPreparer.SplitStatements();
150-
151-
var columnsAndParameters = new ResizableArray<byte>();
152-
var columnsAndParametersSize = 0;
153-
154-
var preparedStatements = new List<PreparedStatement>(parsedStatements.Statements.Count);
155-
foreach (var statement in parsedStatements.Statements)
156-
{
157-
await Connection.Session.SendAsync(new PayloadData(statement.StatementBytes), ioBehavior, cancellationToken).ConfigureAwait(false);
158-
var payload = await Connection.Session.ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
159-
var response = StatementPrepareResponsePayload.Create(payload.AsSpan());
160-
161-
ColumnDefinitionPayload[] parameters = null;
162-
if (response.ParameterCount > 0)
163-
{
164-
parameters = new ColumnDefinitionPayload[response.ParameterCount];
165-
for (var i = 0; i < response.ParameterCount; i++)
166-
{
167-
payload = await Connection.Session.ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
168-
Utility.Resize(ref columnsAndParameters, columnsAndParametersSize + payload.ArraySegment.Count);
169-
Buffer.BlockCopy(payload.ArraySegment.Array, payload.ArraySegment.Offset, columnsAndParameters.Array, columnsAndParametersSize, payload.ArraySegment.Count);
170-
parameters[i] = ColumnDefinitionPayload.Create(new ResizableArraySegment<byte>(columnsAndParameters, columnsAndParametersSize, payload.ArraySegment.Count));
171-
columnsAndParametersSize += payload.ArraySegment.Count;
172-
}
173-
if (!Connection.Session.SupportsDeprecateEof)
174-
{
175-
payload = await Connection.Session.ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
176-
EofPayload.Create(payload.AsSpan());
177-
}
178-
}
179-
180-
ColumnDefinitionPayload[] columns = null;
181-
if (response.ColumnCount > 0)
182-
{
183-
columns = new ColumnDefinitionPayload[response.ColumnCount];
184-
for (var i = 0; i < response.ColumnCount; i++)
185-
{
186-
payload = await Connection.Session.ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
187-
Utility.Resize(ref columnsAndParameters, columnsAndParametersSize + payload.ArraySegment.Count);
188-
Buffer.BlockCopy(payload.ArraySegment.Array, payload.ArraySegment.Offset, columnsAndParameters.Array, columnsAndParametersSize, payload.ArraySegment.Count);
189-
columns[i] = ColumnDefinitionPayload.Create(new ResizableArraySegment<byte>(columnsAndParameters, columnsAndParametersSize, payload.ArraySegment.Count));
190-
columnsAndParametersSize += payload.ArraySegment.Count;
191-
}
192-
if (!Connection.Session.SupportsDeprecateEof)
193-
{
194-
payload = await Connection.Session.ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
195-
EofPayload.Create(payload.AsSpan());
196-
}
197-
}
198-
199-
preparedStatements.Add(new PreparedStatement(response.StatementId, statement, columns, parameters));
200-
}
201-
202-
Connection.Session.AddPreparedStatement(CommandText, new PreparedStatements(preparedStatements, parsedStatements));
203-
}
204-
205143
public override string CommandText
206144
{
207145
get => m_commandText;

tests/SideBySide/BatchTests.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,49 @@ public void ExecuteInvalidSqlBatch()
250250
}
251251
}
252252
}
253+
[Fact]
254+
public void PrepareNeedsConnection()
255+
{
256+
using (var batch = new MySqlBatch
257+
{
258+
BatchCommands =
259+
{
260+
new MySqlBatchCommand("SELECT 1;"),
261+
},
262+
})
263+
{
264+
Assert.Throws<InvalidOperationException>(() => batch.Prepare());
265+
}
266+
}
267+
268+
[Fact]
269+
public void PrepareNeedsOpenConnection()
270+
{
271+
using (var connection = new MySqlConnection(AppConfig.ConnectionString))
272+
using (var batch = new MySqlBatch(connection)
273+
{
274+
BatchCommands =
275+
{
276+
new MySqlBatchCommand("SELECT 1;"),
277+
},
278+
})
279+
{
280+
Assert.Throws<InvalidOperationException>(() => batch.Prepare());
281+
}
282+
}
283+
284+
[Fact]
285+
public void PrepareNeedsCommands()
286+
{
287+
using (var connection = new MySqlConnection(AppConfig.ConnectionString))
288+
{
289+
connection.Open();
290+
using (var batch = new MySqlBatch(connection))
291+
{
292+
Assert.Throws<InvalidOperationException>(() => batch.Prepare());
293+
}
294+
}
295+
}
253296

254297
private static string GetIgnoreCommandTransactionConnectionString() =>
255298
new MySqlConnectionStringBuilder(AppConfig.ConnectionString)

0 commit comments

Comments
 (0)