Skip to content

Commit 378c4b8

Browse files
committed
Send batch commands as concatenated SQL.
1 parent abe8bf9 commit 378c4b8

File tree

8 files changed

+221
-32
lines changed

8 files changed

+221
-32
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
using MySqlConnector.Logging;
7+
using MySqlConnector.Protocol;
8+
using MySqlConnector.Protocol.Serialization;
9+
10+
namespace MySqlConnector.Core
11+
{
12+
internal sealed class ConcatenatedCommandPayloadCreator : ICommandPayloadCreator
13+
{
14+
public static ICommandPayloadCreator Instance { get; } = new ConcatenatedCommandPayloadCreator();
15+
16+
public bool WriteQueryCommand(ref CommandListPosition commandListPosition, IDictionary<string, CachedProcedure> cachedProcedures, ByteBufferWriter writer)
17+
{
18+
if (commandListPosition.CommandIndex == commandListPosition.Commands.Count)
19+
return false;
20+
21+
writer.Write((byte) CommandKind.Query);
22+
bool isComplete;
23+
do
24+
{
25+
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+
29+
if (Log.IsDebugEnabled())
30+
Log.Debug("Session{0} Preparing command payload; CommandText: {1}", command.Connection.Session.Id, command.CommandText);
31+
32+
isComplete = SingleCommandPayloadCreator.WriteQueryPayload(command, cachedProcedures, writer);
33+
commandListPosition.CommandIndex++;
34+
}
35+
while (commandListPosition.CommandIndex < commandListPosition.Commands.Count && isComplete);
36+
37+
return true;
38+
}
39+
40+
static readonly IMySqlConnectorLogger Log = MySqlConnectorLogManager.CreateLogger(nameof(ConcatenatedCommandPayloadCreator));
41+
}
42+
}

src/MySqlConnector/Core/ResultSet.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public void Reset()
3636
m_row = null;
3737
m_rowBuffered = null;
3838
m_hasRows = false;
39+
ReadResultSetHeaderException = null;
3940
}
4041

4142
public async Task ReadResultSetHeaderAsync(IOBehavior ioBehavior)

src/MySqlConnector/Core/SingleCommandPayloadCreator.cs

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,15 @@ public bool WriteQueryCommand(ref CommandListPosition commandListPosition, IDict
4949
return true;
5050
}
5151

52-
public static void WriteQueryPayload(IMySqlCommand command, IDictionary<string, CachedProcedure> cachedProcedures, ByteBufferWriter writer)
53-
{
54-
if (command.CommandType == CommandType.StoredProcedure)
55-
WriteStoredProcedure(command, cachedProcedures, writer);
56-
else
57-
WriteCommand(command, writer);
58-
}
52+
/// <summary>
53+
/// Writes the text of <paramref name="command"/> to <paramref name="writer"/>, encoded in UTF-8.
54+
/// </summary>
55+
/// <param name="command">The command.</param>
56+
/// <param name="cachedProcedures">The cached procedures.</param>
57+
/// <param name="writer">The output writer.</param>
58+
/// <returns><c>true</c> if a complete command was written; otherwise, <c>false</c>.</returns>
59+
public static bool WriteQueryPayload(IMySqlCommand command, IDictionary<string, CachedProcedure> cachedProcedures, ByteBufferWriter writer) =>
60+
(command.CommandType == CommandType.StoredProcedure) ? WriteStoredProcedure(command, cachedProcedures, writer) : WriteCommand(command, writer);
5961

6062
private static void WritePreparedStatement(IMySqlCommand command, PreparedStatement preparedStatement, ByteBufferWriter writer)
6163
{
@@ -124,7 +126,7 @@ private static void WritePreparedStatement(IMySqlCommand command, PreparedStatem
124126
}
125127
}
126128

127-
private static void WriteStoredProcedure(IMySqlCommand command, IDictionary<string, CachedProcedure> cachedProcedures, ByteBufferWriter writer)
129+
private static bool WriteStoredProcedure(IMySqlCommand command, IDictionary<string, CachedProcedure> cachedProcedures, ByteBufferWriter writer)
128130
{
129131
var parameterCollection = command.RawParameters;
130132
var cachedProcedure = cachedProcedures[command.CommandText];
@@ -184,13 +186,13 @@ private static void WriteStoredProcedure(IMySqlCommand command, IDictionary<stri
184186
command.ReturnParameter = returnParameter;
185187

186188
var preparer = new StatementPreparer(commandText, inParameters, command.CreateStatementPreparerOptions());
187-
preparer.ParseAndBindParameters(writer);
189+
return preparer.ParseAndBindParameters(writer);
188190
}
189191

190-
private static void WriteCommand(IMySqlCommand command, ByteBufferWriter writer)
192+
private static bool WriteCommand(IMySqlCommand command, ByteBufferWriter writer)
191193
{
192194
var preparer = new StatementPreparer(command.CommandText, command.RawParameters, command.CreateStatementPreparerOptions());
193-
preparer.ParseAndBindParameters(writer);
195+
return preparer.ParseAndBindParameters(writer);
194196
}
195197

196198
static readonly IMySqlConnectorLogger Log = MySqlConnectorLogManager.CreateLogger(nameof(SingleCommandPayloadCreator));

src/MySqlConnector/Core/SqlParser.cs

Lines changed: 81 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ public void Parse(string sql)
1313

1414
var state = State.Beginning;
1515
var beforeCommentState = State.Beginning;
16-
bool isNamedParameter = false;
17-
for (int index = 0; index <= sql.Length; index++)
16+
var isNamedParameter = false;
17+
for (var index = 0; index < sql.Length; index++)
1818
{
19-
char ch = index == sql.Length ? ';' : sql[index];
19+
var ch = sql[index];
2020
if (state == State.EndOfLineComment)
2121
{
2222
if (ch == '\n')
@@ -68,7 +68,15 @@ public void Parse(string sql)
6868
{
6969
if (isNamedParameter)
7070
OnNamedParameter(parameterStartIndex, index - parameterStartIndex);
71-
state = State.Statement;
71+
if (ch == ';')
72+
{
73+
OnStatementEnd(index);
74+
state = State.Beginning;
75+
}
76+
else
77+
{
78+
state = State.Statement;
79+
}
7280
}
7381
}
7482
else if (state == State.DoubleQuotedStringDoubleQuote)
@@ -81,7 +89,15 @@ public void Parse(string sql)
8189
{
8290
if (isNamedParameter)
8391
OnNamedParameter(parameterStartIndex, index - parameterStartIndex);
84-
state = State.Statement;
92+
if (ch == ';')
93+
{
94+
OnStatementEnd(index);
95+
state = State.Beginning;
96+
}
97+
else
98+
{
99+
state = State.Statement;
100+
}
85101
}
86102
}
87103
else if (state == State.BacktickQuotedStringBacktick)
@@ -94,7 +110,15 @@ public void Parse(string sql)
94110
{
95111
if (isNamedParameter)
96112
OnNamedParameter(parameterStartIndex, index - parameterStartIndex);
97-
state = State.Statement;
113+
if (ch == ';')
114+
{
115+
OnStatementEnd(index);
116+
state = State.Beginning;
117+
}
118+
else
119+
{
120+
state = State.Statement;
121+
}
98122
}
99123
}
100124
else if (state == State.SecondHyphen)
@@ -225,7 +249,35 @@ public void Parse(string sql)
225249
}
226250
}
227251

228-
OnParsed();
252+
var states = FinalParseStates.None;
253+
if (state == State.NamedParameter)
254+
{
255+
OnNamedParameter(parameterStartIndex, sql.Length - parameterStartIndex);
256+
state = State.Statement;
257+
}
258+
else if (state == State.QuestionMark)
259+
{
260+
OnPositionalParameter(parameterStartIndex);
261+
state = State.Statement;
262+
}
263+
else if (state == State.EndOfLineComment)
264+
{
265+
states |= FinalParseStates.NeedsNewline;
266+
state = beforeCommentState;
267+
}
268+
269+
if (state == State.Statement)
270+
{
271+
OnStatementEnd(sql.Length);
272+
states |= FinalParseStates.NeedsSemicolon;
273+
state = State.Beginning;
274+
}
275+
if (state == State.Beginning)
276+
{
277+
states |= FinalParseStates.Complete;
278+
}
279+
280+
OnParsed(states);
229281
}
230282

231283
protected virtual void OnBeforeParse(string sql)
@@ -248,10 +300,31 @@ protected virtual void OnStatementEnd(int index)
248300
{
249301
}
250302

251-
protected virtual void OnParsed()
303+
protected virtual void OnParsed(FinalParseStates states)
252304
{
253305
}
254306

307+
[Flags]
308+
protected enum FinalParseStates
309+
{
310+
None = 0,
311+
312+
/// <summary>
313+
/// The statement is complete (apart from potentially needing a semicolon or newline).
314+
/// </summary>
315+
Complete = 1,
316+
317+
/// <summary>
318+
/// The statement needs a newline (e.g., to terminate a final comment).
319+
/// </summary>
320+
NeedsNewline = 2,
321+
322+
/// <summary>
323+
/// The statement needs a semicolon (if another statement is going to be concatenated to it).
324+
/// </summary>
325+
NeedsSemicolon = 4,
326+
}
327+
255328
private static bool IsWhitespace(char ch) => ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n';
256329

257330
private static bool IsVariableName(char ch) => (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '.' || ch == '_' || ch == '$' || (ch >= 0x0080 && ch <= 0xFFFF);

src/MySqlConnector/Core/StatementPreparer.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,15 @@ public ParsedStatements SplitStatements()
2929
return new ParsedStatements(statements, writer.ToPayloadData());
3030
}
3131

32-
public void ParseAndBindParameters(ByteBufferWriter writer)
32+
public bool ParseAndBindParameters(ByteBufferWriter writer)
3333
{
3434
if (!string.IsNullOrWhiteSpace(m_commandText))
3535
{
3636
var parser = new ParameterSqlParser(this, writer);
3737
parser.Parse(m_commandText);
38+
return parser.IsComplete;
3839
}
40+
return true;
3941
}
4042

4143
private int GetParameterIndex(string name)
@@ -64,6 +66,8 @@ public ParameterSqlParser(StatementPreparer preparer, ByteBufferWriter writer)
6466
m_writer = writer;
6567
}
6668

69+
public bool IsComplete { get; private set; }
70+
6771
protected override void OnNamedParameter(int index, int length)
6872
{
6973
var parameterIndex = m_preparer.GetParameterIndex(m_preparer.m_commandText.Substring(index, length));
@@ -85,9 +89,14 @@ private void DoAppendParameter(int parameterIndex, int textIndex, int textLength
8589
m_lastIndex = textIndex + textLength;
8690
}
8791

88-
protected override void OnParsed()
92+
protected override void OnParsed(FinalParseStates states)
8993
{
9094
m_writer.Write(m_preparer.m_commandText, m_lastIndex, m_preparer.m_commandText.Length - m_lastIndex);
95+
if ((states & FinalParseStates.NeedsNewline) == FinalParseStates.NeedsNewline)
96+
m_writer.Write((byte) '\n');
97+
if ((states & FinalParseStates.NeedsSemicolon) == FinalParseStates.NeedsSemicolon)
98+
m_writer.Write((byte) ';');
99+
IsComplete = (states & FinalParseStates.Complete) == FinalParseStates.Complete;
91100
}
92101

93102
readonly StatementPreparer m_preparer;

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,9 @@ private Task<DbDataReader> ExecuteReaderAsync(IOBehavior ioBehavior, Cancellatio
122122
foreach (MySqlBatchCommand batchCommand in BatchCommands)
123123
batchCommand.Batch = this;
124124

125-
var payloadCreator = Connection.Session.SupportsComMulti ? BatchedCommandPayloadCreator.Instance : SingleCommandPayloadCreator.Instance;
125+
var payloadCreator = Connection.Session.SupportsComMulti ? BatchedCommandPayloadCreator.Instance :
126+
// TODO: IsPrepared ? SingleCommandPayloadCreator.Instance :
127+
ConcatenatedCommandPayloadCreator.Instance;
126128
return CommandExecutor.ExecuteReaderAsync(BatchCommands, payloadCreator, CommandBehavior.Default, ioBehavior, cancellationToken);
127129
}
128130

tests/MySqlConnector.Tests/StatementPreparerTests.cs

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public void Bug429(string sql)
2121
var parameters = new MySqlParameterCollection();
2222
parameters.AddWithValue("@param", 123);
2323
var parsedSql = GetParsedSql(sql, parameters);
24-
Assert.Equal(sql.Replace("@param", "123"), parsedSql);
24+
Assert.Equal(sql.Replace("@param", "123") + ";", parsedSql);
2525
}
2626

2727
[Theory]
@@ -60,7 +60,7 @@ public void ParametersIgnoredInComments(string sql)
6060
[InlineData(null, DummyEnum.FirstValue, "0")]
6161
public void EnumParametersAreParsedCorrectly(MySqlDbType? type, object value, string replacedValue)
6262
{
63-
const string sql = "SELECT @param";
63+
const string sql = "SELECT @param;";
6464
var parameters = new MySqlParameterCollection();
6565
var parameter = new MySqlParameter("@param", value);
6666

@@ -129,15 +129,15 @@ public void Bug589(string sql)
129129
var parameters = new MySqlParameterCollection();
130130
parameters.AddWithValue("@foo", 22);
131131
var parsedSql = GetParsedSql(sql, parameters, StatementPreparerOptions.AllowUserVariables);
132-
Assert.Equal(sql.Replace("@foo", "22"), parsedSql);
132+
Assert.Equal(sql.Replace("@foo", "22") + ";", parsedSql);
133133
}
134134

135135
[Theory]
136136
[MemberData(nameof(FormatParameterData))]
137137
public void FormatParameter(object parameterValue, string replacedValue)
138138
{
139139
var parameters = new MySqlParameterCollection { new MySqlParameter("@param", parameterValue) };
140-
const string sql = "SELECT @param";
140+
const string sql = "SELECT @param;";
141141
var parsedSql = GetParsedSql(sql, parameters);
142142
Assert.Equal(sql.Replace("@param", replacedValue), parsedSql);
143143
}
@@ -178,11 +178,30 @@ public void FormatParameter(object parameterValue, string replacedValue)
178178
public void GuidFormat(object options, string replacedValue)
179179
{
180180
var parameters = new MySqlParameterCollection { new MySqlParameter("@param", new Guid("61626364-6566-6768-696a-6b6c6d6e6f70")) };
181-
const string sql = "SELECT @param";
181+
const string sql = "SELECT @param;";
182182
var parsedSql = GetParsedSql(sql, parameters, (StatementPreparerOptions) options);
183183
Assert.Equal(sql.Replace("@param", replacedValue), parsedSql);
184184
}
185185

186+
[Theory]
187+
[InlineData("SELECT 1;", "SELECT 1;", true)]
188+
[InlineData("SELECT 1", "SELECT 1;", true)]
189+
[InlineData("SELECT 1 -- comment", "SELECT 1 -- comment\n;", true)]
190+
[InlineData("SELECT 1 # comment", "SELECT 1 # comment\n;", true)]
191+
[InlineData("SELECT '1", "SELECT '1", false)]
192+
[InlineData("SELECT '1' /* test", "SELECT '1' /* test", false)]
193+
public void CompleteStatements(string sql, string expectedSql, bool expectedComplete)
194+
{
195+
var preparer = new StatementPreparer(sql, new MySqlParameterCollection(), new StatementPreparerOptions());
196+
var writer = new ByteBufferWriter();
197+
var isComplete = preparer.ParseAndBindParameters(writer);
198+
Assert.Equal(expectedComplete, isComplete);
199+
string parsedSql;
200+
using (var payload = writer.ToPayloadData())
201+
parsedSql = Encoding.UTF8.GetString(payload.AsSpan());
202+
Assert.Equal(expectedSql, parsedSql);
203+
}
204+
186205
[Theory]
187206
[InlineData("SELECT 1", new[] { "SELECT 1" }, "")]
188207
[InlineData("SELECT 1;", new[] { "SELECT 1" }, "")]

0 commit comments

Comments
 (0)