Skip to content

Commit 1e9832a

Browse files
committed
Throw better exception if DELIMITER is used. Fixes #1010
1 parent c169f35 commit 1e9832a

File tree

6 files changed

+135
-2
lines changed

6 files changed

+135
-2
lines changed

docs/content/troubleshooting/datetime-storage.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
date: 2020-02-08
33
title: DateTime Storage
4-
weight: 20
4+
weight: 24
55
menu:
66
main:
77
parent: troubleshooting
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
date: 2021-07-27
3+
title: Delimiter
4+
customtitle: "Fix: Using DELIMITER in SQL"
5+
weight: 28
6+
menu:
7+
main:
8+
parent: troubleshooting
9+
---
10+
11+
# Fix: Using DELIMITER in SQL
12+
13+
In MySQL Workbench, it's common to use `DELIMITER $$` (or similar) when defining stored procedures or using other compound statements that include an embedded semicolon (`;`).
14+
15+
This is not required by MySQL Server, but is a workaround for [limitations in the mysql client](https://dev.mysql.com/doc/refman/8.0/en/stored-programs-defining.html):
16+
17+
> By default, mysql itself recognizes the semicolon as a statement delimiter, so you must redefine the delimiter temporarily to cause mysql to pass the entire stored program definition to the server.
18+
19+
This limitation does not exist in MySqlConnector, so using `DELIMITER` is unnecessary and it must be removed (to avoid sending invalid SQL to the server).
20+
21+
## Incorrect Code
22+
23+
```csharp
24+
using var command = connection.CreateCommand();
25+
command.CommandText = @"
26+
DELIMITER $$
27+
CREATE FUNCTION echo(
28+
name VARCHAR(63)
29+
) RETURNS VARCHAR(63)
30+
BEGIN
31+
RETURN name;
32+
END
33+
$$";
34+
command.ExecuteNonQuery();
35+
```
36+
37+
## Fixed Code
38+
39+
To fix the problem, remove the `DELIMITER` declaration and any trailing instances of the delimiter:
40+
41+
```csharp
42+
using var command = connection.CreateCommand();
43+
command.CommandText = @"
44+
CREATE FUNCTION echo(
45+
name VARCHAR(63)
46+
) RETURNS VARCHAR(63)
47+
BEGIN
48+
RETURN name;
49+
END;";
50+
command.ExecuteNonQuery();
51+
```

src/MySqlConnector/Core/ServerSession.cs

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,17 @@ public async Task PrepareAsync(IMySqlCommand command, IOBehavior ioBehavior, Can
216216
foreach (var statement in parsedStatements.Statements)
217217
{
218218
await SendAsync(new PayloadData(statement.StatementBytes), ioBehavior, cancellationToken).ConfigureAwait(false);
219-
var payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
219+
PayloadData payload;
220+
try
221+
{
222+
payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
223+
}
224+
catch (MySqlException exception)
225+
{
226+
ThrowIfStatementContainsDelimiter(exception, command);
227+
throw;
228+
}
229+
220230
var response = StatementPrepareResponsePayload.Create(payload.Span);
221231

222232
ColumnDefinitionPayload[]? parameters = null;
@@ -918,6 +928,18 @@ private async ValueTask<int> SendReplyAsyncAwaited(ValueTask<int> task)
918928
}
919929
}
920930

931+
public static void ThrowIfStatementContainsDelimiter(MySqlException exception, IMySqlCommand command)
932+
{
933+
// check if the command used "DELIMITER"
934+
if (exception.ErrorCode == MySqlErrorCode.ParseError && command.CommandText?.IndexOf("delimiter", StringComparison.OrdinalIgnoreCase) >= 0)
935+
{
936+
var parser = new DelimiterSqlParser(command);
937+
parser.Parse(command.CommandText);
938+
if (parser.HasDelimiter)
939+
throw new MySqlException(MySqlErrorCode.DelimiterNotSupported, "'DELIMITER' should not be used with MySqlConnector. See https://fl.vu/mysql-delimiter", exception);
940+
}
941+
}
942+
921943
internal void HandleTimeout()
922944
{
923945
if (OwningConnection is not null && OwningConnection.TryGetTarget(out var connection))
@@ -1738,6 +1760,25 @@ private enum State
17381760
Failed,
17391761
}
17401762

1763+
private sealed class DelimiterSqlParser : SqlParser
1764+
{
1765+
public DelimiterSqlParser(IMySqlCommand command)
1766+
: base(new StatementPreparer(command.CommandText!, null, command.CreateStatementPreparerOptions()))
1767+
{
1768+
m_sql = command.CommandText!;
1769+
}
1770+
1771+
public bool HasDelimiter { get; private set; }
1772+
1773+
protected override void OnStatementBegin(int index)
1774+
{
1775+
if (index + 10 < m_sql.Length && string.Equals("delimiter ", m_sql.Substring(index, 10), StringComparison.OrdinalIgnoreCase))
1776+
HasDelimiter = true;
1777+
}
1778+
1779+
readonly string m_sql;
1780+
}
1781+
17411782
static ReadOnlySpan<byte> BeginCertificateBytes => new byte[] { 45, 45, 45, 45, 45, 66, 69, 71, 73, 78, 32, 67, 69, 82, 84, 73, 70, 73, 67, 65, 84, 69, 45, 45, 45, 45, 45 }; // -----BEGIN CERTIFICATE-----
17421783
static int s_lastId;
17431784
static readonly IMySqlConnectorLogger Log = MySqlConnectorLogManager.CreateLogger(nameof(ServerSession));

src/MySqlConnector/MySqlDataReader.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,11 @@ private void ActivateResultSet(CancellationToken cancellationToken)
132132
throw MySqlException.CreateForTimeout(mySqlException);
133133

134134
if (mySqlException is not null)
135+
{
136+
ServerSession.ThrowIfStatementContainsDelimiter(mySqlException, Command!);
137+
135138
m_resultSet.ReadResultSetHeaderException.Throw();
139+
}
136140

137141
throw new MySqlException("Failed to read the result set.", m_resultSet.ReadResultSetHeaderException.SourceException);
138142
}

src/MySqlConnector/MySqlErrorCode.g.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ namespace MySqlConnector
66
[System.CodeDom.Compiler.GeneratedCode("https://gist.github.com/bgrainger/791cecb647d514a9dd2f3d83b2387e49", "2")]
77
public enum MySqlErrorCode
88
{
9+
DelimiterNotSupported = -3,
10+
911
/// <summary>
1012
/// Not all rows from the source supplied to <see cref="MySqlBulkCopy"/> were copied to <see cref="MySqlBulkCopy.DestinationTableName"/>.
1113
/// </summary>

tests/SideBySide/QueryTests.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1253,6 +1253,41 @@ public void GetIntForTinyInt1(bool treatTinyAsBoolean, bool prepare)
12531253
Assert.False(reader.Read());
12541254
}
12551255

1256+
[Theory]
1257+
[InlineData(false, false)]
1258+
[InlineData(true, false)]
1259+
[InlineData(false, true)]
1260+
[InlineData(true, true)]
1261+
public async Task Delimiter(bool prepareCommand, bool isAsync)
1262+
{
1263+
using var command = m_database.Connection.CreateCommand();
1264+
command.CommandText = @"DELIMITER $$
1265+
CREATE FUNCTION echo(
1266+
name VARCHAR(63)
1267+
) RETURNS VARCHAR(63)
1268+
BEGIN
1269+
RETURN name;
1270+
END
1271+
$$";
1272+
MySqlException exception;
1273+
if (prepareCommand)
1274+
{
1275+
exception = isAsync ? (await Assert.ThrowsAsync<MySqlException>(async () => await command.PrepareAsync())) :
1276+
Assert.Throws<MySqlException>(() => command.Prepare());
1277+
}
1278+
else
1279+
{
1280+
exception = isAsync ? (await Assert.ThrowsAsync<MySqlException>(async () => await command.ExecuteNonQueryAsync())) :
1281+
Assert.Throws<MySqlException>(() => command.ExecuteNonQuery());
1282+
}
1283+
1284+
#if !BASELINE
1285+
Assert.Equal(MySqlErrorCode.DelimiterNotSupported, exception.ErrorCode);
1286+
#else
1287+
Assert.Equal((int) MySqlErrorCode.ParseError, exception.Number);
1288+
#endif
1289+
}
1290+
12561291
#if !NETCOREAPP1_1_2
12571292
[Fact]
12581293
public void QueryDateTimeLiteral()

0 commit comments

Comments
 (0)