Skip to content

Commit 8f84081

Browse files
committed
Add StatementPreparer.SplitStatements.
This splits a single MySqlCommand.CommandText string into multiple individual SQL statements that can be prepared individually; MySQL Server doesn't support prepared commands that comprise multiple SQL statements. Signed-off-by: Bradley Grainger <[email protected]>
1 parent cdfc6c7 commit 8f84081

File tree

7 files changed

+245
-23
lines changed

7 files changed

+245
-23
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
namespace MySqlConnector.Core
5+
{
6+
/// <summary>
7+
/// <see cref="ParsedStatement"/> represents an individual SQL statement that's been parsed
8+
/// from a string possibly containing multiple semicolon-delimited SQL statements.
9+
/// </summary>
10+
internal sealed class ParsedStatement
11+
{
12+
/// <summary>
13+
/// The bytes for this statement that will be written on the wire.
14+
/// </summary>
15+
public ArraySegment<byte> StatementBytes { get; set; }
16+
17+
/// <summary>
18+
/// The names of the parameters (if known) of the parameters in the prepared statement. There
19+
/// is one entry in this list for each parameter, which will be <c>null</c> if the name is unknown.
20+
/// </summary>
21+
public List<string> ParameterNames { get; } = new List<string>();
22+
23+
/// <summary>
24+
/// The indexes of the parameters in the prepared statement. There is one entry in this list for
25+
/// each parameter; it will be <c>-1</c> if the parameter is named.
26+
/// </summary>
27+
public List<int> ParameterIndexes { get; }= new List<int>();
28+
}
29+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using MySqlConnector.Protocol;
4+
5+
namespace MySqlConnector.Core
6+
{
7+
/// <summary>
8+
/// <see cref="ParsedStatements"/> wraps a collection of <see cref="ParsedStatement"/> objects.
9+
/// It implements <see cref="IDisposable"/> to return the memory backing the statements to a shared pool.
10+
/// </summary>
11+
internal sealed class ParsedStatements : IDisposable
12+
{
13+
public IReadOnlyList<ParsedStatement> Statements => m_statements;
14+
15+
public void Dispose()
16+
{
17+
m_statements.Clear();
18+
m_payloadData.Dispose();
19+
m_payloadData = default;
20+
}
21+
22+
internal ParsedStatements(List<ParsedStatement> statements, PayloadData payloadData)
23+
{
24+
m_statements = statements;
25+
m_payloadData = payloadData;
26+
}
27+
28+
readonly List<ParsedStatement> m_statements;
29+
PayloadData m_payloadData;
30+
}
31+
}

src/MySqlConnector/Core/SqlParser.cs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,15 @@ public void Parse(string sql)
9797
else
9898
{
9999
OnPositionalParameter(parameterStartIndex);
100-
state = State.Statement;
100+
if (ch == ';')
101+
{
102+
OnStatementEnd(index);
103+
state = State.Beginning;
104+
}
105+
else
106+
{
107+
state = State.Statement;
108+
}
101109
}
102110
}
103111
else if (state == State.AtSign)
@@ -109,7 +117,15 @@ public void Parse(string sql)
109117
if (!IsVariableName(ch))
110118
{
111119
OnNamedParameter(parameterStartIndex, index - parameterStartIndex);
112-
state = State.Statement;
120+
if (ch == ';')
121+
{
122+
OnStatementEnd(index);
123+
state = State.Beginning;
124+
}
125+
else
126+
{
127+
state = State.Statement;
128+
}
113129
}
114130
}
115131
else
@@ -147,11 +163,14 @@ public void Parse(string sql)
147163
}
148164
else if (ch == ';')
149165
{
166+
if (state != State.Beginning)
167+
OnStatementEnd(index);
150168
state = State.Beginning;
151169
}
152170
else if (!IsWhitespace(ch) && state == State.Beginning)
153171
{
154172
state = State.Statement;
173+
OnStatementBegin(index);
155174
}
156175
}
157176
}
@@ -163,6 +182,10 @@ protected virtual void OnBeforeParse(string sql)
163182
{
164183
}
165184

185+
protected virtual void OnStatementBegin(int index)
186+
{
187+
}
188+
166189
protected virtual void OnPositionalParameter(int index)
167190
{
168191
}
@@ -171,10 +194,14 @@ protected virtual void OnNamedParameter(int index, int length)
171194
{
172195
}
173196

197+
protected virtual void OnStatementEnd(int index)
198+
{
199+
}
200+
174201
protected virtual void OnParsed()
175202
{
176203
}
177-
204+
178205
private static bool IsWhitespace(char ch) => ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n';
179206

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

src/MySqlConnector/Core/StatementPreparer.cs

Lines changed: 91 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.Data;
34
using MySql.Data.MySqlClient;
45
using MySqlConnector.Protocol;
@@ -16,6 +17,18 @@ public StatementPreparer(string commandText, MySqlParameterCollection parameters
1617
m_options = options;
1718
}
1819

20+
public ParsedStatements SplitStatements()
21+
{
22+
var statements = new List<ParsedStatement>();
23+
var statementStartEndIndexes = new List<int>();
24+
var writer = new ByteBufferWriter(m_commandText.Length + 1);
25+
var parser = new PreparedCommandSqlParser(this, statements, statementStartEndIndexes, writer);
26+
parser.Parse(m_commandText);
27+
for (var i = 0; i < statements.Count; i++)
28+
statements[i].StatementBytes = writer.ArraySegment.Slice(statementStartEndIndexes[i * 2], statementStartEndIndexes[i * 2 + 1] - statementStartEndIndexes[i * 2]);
29+
return new ParsedStatements(statements, writer.ToPayloadData());
30+
}
31+
1932
public ArraySegment<byte> ParseAndBindParameters()
2033
{
2134
var writer = new ByteBufferWriter(m_commandText.Length + 1);
@@ -30,6 +43,22 @@ public ArraySegment<byte> ParseAndBindParameters()
3043
return writer.ArraySegment;
3144
}
3245

46+
private int GetParameterIndex(string name)
47+
{
48+
var index = m_parameters.NormalizedIndexOf(name);
49+
if (index == -1 && (m_options & StatementPreparerOptions.AllowUserVariables) == 0)
50+
throw new MySqlException("Parameter '{0}' must be defined. To use this as a variable, set 'Allow User Variables=true' in the connection string.".FormatInvariant(name));
51+
return index;
52+
}
53+
54+
private MySqlParameter GetInputParameter(int index)
55+
{
56+
var parameter = m_parameters[index];
57+
if (parameter.Direction != ParameterDirection.Input && (m_options & StatementPreparerOptions.AllowOutputParameters) == 0)
58+
throw new MySqlException("Only ParameterDirection.Input is supported when CommandType is Text (parameter name: {0})".FormatInvariant(parameter.ParameterName));
59+
return parameter;
60+
}
61+
3362
private sealed class ParameterSqlParser : SqlParser
3463
{
3564
public ParameterSqlParser(StatementPreparer preparer, ByteBufferWriter writer)
@@ -38,18 +67,11 @@ public ParameterSqlParser(StatementPreparer preparer, ByteBufferWriter writer)
3867
m_writer = writer;
3968
}
4069

41-
protected override void OnBeforeParse(string sql)
42-
{
43-
}
44-
4570
protected override void OnNamedParameter(int index, int length)
4671
{
47-
var parameterName = m_preparer.m_commandText.Substring(index, length);
48-
var parameterIndex = m_preparer.m_parameters.NormalizedIndexOf(parameterName);
72+
var parameterIndex = m_preparer.GetParameterIndex(m_preparer.m_commandText.Substring(index, length));
4973
if (parameterIndex != -1)
5074
DoAppendParameter(parameterIndex, index, length);
51-
else if ((m_preparer.m_options & StatementPreparerOptions.AllowUserVariables) == 0)
52-
throw new MySqlException("Parameter '{0}' must be defined. To use this as a variable, set 'Allow User Variables=true' in the connection string.".FormatInvariant(parameterName));
5375
}
5476

5577
protected override void OnPositionalParameter(int index)
@@ -60,30 +82,83 @@ protected override void OnPositionalParameter(int index)
6082

6183
private void DoAppendParameter(int parameterIndex, int textIndex, int textLength)
6284
{
63-
AppendString(m_preparer.m_commandText, m_lastIndex, textIndex - m_lastIndex);
64-
var parameter = m_preparer.m_parameters[parameterIndex];
65-
if (parameter.Direction != ParameterDirection.Input && (m_preparer.m_options & StatementPreparerOptions.AllowOutputParameters) == 0)
66-
throw new MySqlException("Only ParameterDirection.Input is supported when CommandType is Text (parameter name: {0})".FormatInvariant(parameter.ParameterName));
67-
m_preparer.m_parameters[parameterIndex].AppendSqlString(m_writer, m_preparer.m_options, parameter.ParameterName);
85+
m_writer.Write(m_preparer.m_commandText, m_lastIndex, textIndex - m_lastIndex);
86+
var parameter = m_preparer.GetInputParameter(parameterIndex);
87+
parameter.AppendSqlString(m_writer, m_preparer.m_options);
6888
m_lastIndex = textIndex + textLength;
6989
}
7090

7191
protected override void OnParsed()
7292
{
73-
AppendString(m_preparer.m_commandText, m_lastIndex, m_preparer.m_commandText.Length - m_lastIndex);
93+
m_writer.Write(m_preparer.m_commandText, m_lastIndex, m_preparer.m_commandText.Length - m_lastIndex);
94+
}
95+
96+
readonly StatementPreparer m_preparer;
97+
readonly ByteBufferWriter m_writer;
98+
int m_currentParameterIndex;
99+
int m_lastIndex;
100+
}
101+
102+
private sealed class PreparedCommandSqlParser : SqlParser
103+
{
104+
public PreparedCommandSqlParser(StatementPreparer preparer, List<ParsedStatement> statements, List<int> statementStartEndIndexes, ByteBufferWriter writer)
105+
{
106+
m_preparer = preparer;
107+
m_statements = statements;
108+
m_statementStartEndIndexes = statementStartEndIndexes;
109+
m_writer = writer;
110+
}
111+
112+
protected override void OnStatementBegin(int index)
113+
{
114+
m_statements.Add(new ParsedStatement());
115+
m_statementStartEndIndexes.Add(m_writer.Position);
116+
m_writer.Write((byte) CommandKind.StatementPrepare);
117+
m_lastIndex = index;
74118
}
75119

76-
private void AppendString(string value, int offset, int length)
120+
protected override void OnNamedParameter(int index, int length)
77121
{
78-
m_writer.Write(value, offset, length);
122+
var parameterName = m_preparer.m_commandText.Substring(index, length);
123+
DoAppendParameter(parameterName, -1, index, length);
124+
}
125+
126+
protected override void OnPositionalParameter(int index)
127+
{
128+
DoAppendParameter(null, m_currentParameterIndex, index, 1);
129+
m_currentParameterIndex++;
130+
}
131+
132+
private void DoAppendParameter(string parameterName, int parameterIndex, int textIndex, int textLength)
133+
{
134+
// write all SQL up to the parameter
135+
m_writer.Write(m_preparer.m_commandText, m_lastIndex, textIndex - m_lastIndex);
136+
m_lastIndex = textIndex + textLength;
137+
138+
// replace the parameter with a ? placeholder
139+
m_writer.Write((byte) '?');
140+
141+
// store the parameter index
142+
m_statements[m_statements.Count - 1].ParameterNames.Add(parameterName);
143+
m_statements[m_statements.Count - 1].ParameterIndexes.Add(parameterIndex);
144+
}
145+
146+
protected override void OnStatementEnd(int index)
147+
{
148+
m_writer.Write(m_preparer.m_commandText, m_lastIndex, index - m_lastIndex);
149+
m_lastIndex = index;
150+
m_statementStartEndIndexes.Add(m_writer.Position);
79151
}
80152

81153
readonly StatementPreparer m_preparer;
154+
readonly List<ParsedStatement> m_statements;
155+
readonly List<int> m_statementStartEndIndexes;
82156
readonly ByteBufferWriter m_writer;
83157
int m_currentParameterIndex;
84158
int m_lastIndex;
85159
}
86160

161+
87162
readonly string m_commandText;
88163
readonly MySqlParameterCollection m_parameters;
89164
readonly StatementPreparerOptions m_options;

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ private MySqlParameter(MySqlParameter other, string parameterName)
178178

179179
internal string NormalizedParameterName { get; private set; }
180180

181-
internal void AppendSqlString(ByteBufferWriter writer, StatementPreparerOptions options, string parameterName)
181+
internal void AppendSqlString(ByteBufferWriter writer, StatementPreparerOptions options)
182182
{
183183
if (Value == null || Value == DBNull.Value)
184184
{
@@ -277,9 +277,9 @@ internal void AppendSqlString(ByteBufferWriter writer, StatementPreparerOptions
277277
else if (Value is DateTime dateTimeValue)
278278
{
279279
if ((options & StatementPreparerOptions.DateTimeUtc) != 0 && dateTimeValue.Kind == DateTimeKind.Local)
280-
throw new MySqlException("DateTime.Kind must not be Local when DateTimeKind setting is Utc (parameter name: {0})".FormatInvariant(parameterName));
280+
throw new MySqlException("DateTime.Kind must not be Local when DateTimeKind setting is Utc (parameter name: {0})".FormatInvariant(ParameterName));
281281
else if ((options & StatementPreparerOptions.DateTimeLocal) != 0 && dateTimeValue.Kind == DateTimeKind.Utc)
282-
throw new MySqlException("DateTime.Kind must not be Utc when DateTimeKind setting is Local (parameter name: {0})".FormatInvariant(parameterName));
282+
throw new MySqlException("DateTime.Kind must not be Utc when DateTimeKind setting is Local (parameter name: {0})".FormatInvariant(ParameterName));
283283

284284
writer.Write("timestamp('{0:yyyy'-'MM'-'dd' 'HH':'mm':'ss'.'ffffff}')".FormatInvariant(dateTimeValue));
285285
}

src/MySqlConnector/Protocol/CommandKind.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ internal enum CommandKind
77
Query = 3,
88
Ping = 14,
99
ChangeUser = 17,
10+
StatementPrepare = 22,
1011
ResetConnection = 31,
1112
}
1213
}

0 commit comments

Comments
 (0)