Skip to content

Commit ce16109

Browse files
committed
Ignore explicit MySqlDbType for prepared statements. Fixes #659
MySQL Server can't actually process many MySqlDbType column types directly as parameters to a prepared statement, so we now just ignore all explicit types and use inferred types. This fixes MySqlDbType.JSON, MySqlDbType.Year, and MySqlDbType.Int24, among others.
1 parent 926753a commit ce16109

File tree

4 files changed

+109
-36
lines changed

4 files changed

+109
-36
lines changed

docs/content/tutorials/migrating-from-connector-net.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,14 +119,14 @@ The following bugs in Connector/NET are fixed by switching to MySqlConnector. (~
119119
* [#73610](https://bugs.mysql.com/bug.php?id=73610): Invalid password exception has wrong number
120120
* [#73788](https://bugs.mysql.com/bug.php?id=73788): Can’t use `DateTimeOffset`
121121
* [#75604](https://bugs.mysql.com/bug.php?id=75604): Crash after 29.4 days of uptime
122-
* [#75917](https://bugs.mysql.com/bug.php?id=75917), [#76597](https://bugs.mysql.com/bug.php?id=76597), [#77691](https://bugs.mysql.com/bug.php?id=77691), [#78650](https://bugs.mysql.com/bug.php?id=78650), [#78919](https://bugs.mysql.com/bug.php?id=78919), [#80921](https://bugs.mysql.com/bug.php?id=80921), [#82136](https://bugs.mysql.com/bug.php?id=82136): "Reading from the stream has failed" when connecting to a server
122+
* [#75917](https://bugs.mysql.com/bug.php?id=75917), [#76597](https://bugs.mysql.com/bug.php?id=76597), [#77691](https://bugs.mysql.com/bug.php?id=77691), [#78650](https://bugs.mysql.com/bug.php?id=78650), [#78919](https://bugs.mysql.com/bug.php?id=78919), [#80921](https://bugs.mysql.com/bug.php?id=80921), [#82136](https://bugs.mysql.com/bug.php?id=82136): Reading from the stream has failed when connecting to a server
123123
* [#77421](https://bugs.mysql.com/bug.php?id=77421): Connection is not reset when pulled from the connection pool
124124
* [#78426](https://bugs.mysql.com/bug.php?id=78426): Unknown database exception has wrong number
125125
* [#78760](https://bugs.mysql.com/bug.php?id=78760): Error when using tabs and newlines in SQL statements
126126
* ~~[#78917](https://bugs.mysql.com/bug.php?id=78917), [#79196](https://bugs.mysql.com/bug.php?id=79196), [#82292](https://bugs.mysql.com/bug.php?id=82292), [#89040](https://bugs.mysql.com/bug.php?id=89040): `TINYINT(1)` values start being returned as `sbyte` after `NULL`~~
127127
* ~~[#80030](https://bugs.mysql.com/bug.php?id=80030): Slow to connect with pooling disabled~~
128128
* [#81650](https://bugs.mysql.com/bug.php?id=81650), [#88962](https://bugs.mysql.com/bug.php?id=88962): `Server` connection string option may now contain multiple, comma separated hosts that will be tried in order until a connection succeeds
129-
* [#83229](https://bugs.mysql.com/bug.php?id=83329): "Unknown command" exception inserting large blob with UseCompression=True
129+
* [#83229](https://bugs.mysql.com/bug.php?id=83329): Unknown command exception inserting large blob with UseCompression=True
130130
* [#84220](https://bugs.mysql.com/bug.php?id=84220): Cannot call a stored procedure with `.` in its name
131131
* [#84701](https://bugs.mysql.com/bug.php?id=84701): Can’t create a parameter using a 64-bit enum with a value greater than int.MaxValue
132132
* [#85185](https://bugs.mysql.com/bug.php?id=85185): `ConnectionReset=True` does not preserve connection charset
@@ -138,7 +138,7 @@ The following bugs in Connector/NET are fixed by switching to MySqlConnector. (~
138138
* ~~[#88058](https://bugs.mysql.com/bug.php?id=88058): `decimal(n, 0)` has wrong `NumericPrecision`~~
139139
* [#88124](https://bugs.mysql.com/bug.php?id=88124): CommandTimeout isn’t reset when calling Read/NextResult
140140
* ~~[#88472](https://bugs.mysql.com/bug.php?id=88472): `TINYINT(1)` is not returned as `bool` if `MySqlCommand.Prepare` is called~~
141-
* [#88611](https://bugs.mysql.com/bug.php?id=88611): `MySqlCommand` can be executed even if it has "wrong" transaction
141+
* [#88611](https://bugs.mysql.com/bug.php?id=88611): `MySqlCommand` can be executed even if it has wrong transaction
142142
* ~~[#88660](https://bugs.mysql.com/bug.php?id=88660): `MySqlClientFactory.Instance.CreateDataAdapter()` and `CreateCommandBuilder` return `null`~~
143143
* [#89085](https://bugs.mysql.com/bug.php?id=89085): `MySqlConnection.Database` not updated after `USE database;`
144144
* [#89159](https://bugs.mysql.com/bug.php?id=89159): `MySqlDataReader` cannot outlive `MySqlCommand`
@@ -152,7 +152,7 @@ The following bugs in Connector/NET are fixed by switching to MySqlConnector. (~
152152
* [#91754](https://bugs.mysql.com/bug.php?id=91754): Inserting 16MiB `BLOB` shifts it by four bytes when prepared
153153
* [#91770](https://bugs.mysql.com/bug.php?id=91770): `TIME(n)` column loses microseconds with prepared command
154154
* [#92367](https://bugs.mysql.com/bug.php?id=92367): `MySqlDataReader.GetDateTime` and `GetValue` return inconsistent values
155-
* [#92465](https://bugs.mysql.com/bug.php?id=92465): "There is already an open DataReader" `MySqlException` thrown from `TransactionScope.Dispose`
155+
* [#92465](https://bugs.mysql.com/bug.php?id=92465): There is already an open DataReader `MySqlException` thrown from `TransactionScope.Dispose`
156156
* [#92734](https://bugs.mysql.com/bug.php?id=92734): `MySqlParameter.Clone` doesn't copy all property values
157157
* [#92789](https://bugs.mysql.com/bug.php?id=92789): Illegal connection attributes written for non-ASCII values
158158
* ~~[#92912](https://bugs.mysql.com/bug.php?id=92912): `MySqlDbType.LongText` values encoded incorrectly with prepared statements~~
@@ -167,3 +167,5 @@ The following bugs in Connector/NET are fixed by switching to MySqlConnector. (~
167167
* [#94760](https://bugs.mysql.com/bug.php?id=94760): `MySqlConnection.OpenAsync(CancellationToken)` doesn’t respect cancellation token
168168
* [#95348](https://bugs.mysql.com/bug.php?id=95348): Inefficient query when executing stored procedures
169169
* [#95436](https://bugs.mysql.com/bug.php?id=95436): Client doesn't authenticate with PEM certificate
170+
* [#95984](https://bugs.mysql.com/bug.php?id=95984): “Incorrect arguments to mysqld_stmt_execute” using prepared statement with `MySqlDbType.JSON`
171+
* [#95986](https://bugs.mysql.com/bug.php?id=95986): “Incorrect integer value” using prepared statement with `MySqlDbType.Int24`

src/MySqlConnector/Core/SingleCommandPayloadCreator.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,18 @@ private void WritePreparedStatement(IMySqlCommand command, PreparedStatement pre
9898
writer.Write((byte) 1);
9999

100100
foreach (var parameter in parameters)
101-
writer.Write(TypeMapper.ConvertToColumnTypeAndFlags(parameter.MySqlDbType, command.Connection.GuidFormat));
101+
{
102+
// override explicit MySqlDbType with inferred type from the Value
103+
var mySqlDbType = parameter.MySqlDbType;
104+
var typeMapping = (parameter.Value is null || parameter.Value == DBNull.Value) ? null : TypeMapper.Instance.GetDbTypeMapping(parameter.Value.GetType());
105+
if (typeMapping is object)
106+
{
107+
var dbType = typeMapping.DbTypes[0];
108+
mySqlDbType = TypeMapper.Instance.GetMySqlDbTypeForDbType(dbType);
109+
}
110+
111+
writer.Write(TypeMapper.ConvertToColumnTypeAndFlags(mySqlDbType, command.Connection.GuidFormat));
112+
}
102113

103114
var options = command.CreateStatementPreparerOptions();
104115
foreach (var parameter in parameters)

src/MySqlConnector/Core/TypeMapper.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,10 @@ public static ushort ConvertToColumnTypeAndFlags(MySqlDbType dbType, MySqlGuidFo
419419
columnType = ColumnType.Geometry;
420420
break;
421421

422+
case MySqlDbType.Null:
423+
columnType = ColumnType.Null;
424+
break;
425+
422426
default:
423427
throw new NotImplementedException("ConvertToColumnTypeAndFlags for {0} is not implemented".FormatInvariant(dbType));
424428
}

tests/SideBySide/PreparedCommandTests.cs

Lines changed: 87 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ public void ReuseCommand()
8888

8989
[Theory]
9090
[MemberData(nameof(GetInsertAndQueryData))]
91-
public void InsertAndQuery(bool isPrepared, string dataType, object dataValue)
91+
public void InsertAndQuery(bool isPrepared, string dataType, object dataValue, MySqlDbType dbType)
9292
{
9393
var csb = new MySqlConnectionStringBuilder(AppConfig.ConnectionString)
9494
{
@@ -100,6 +100,53 @@ public void InsertAndQuery(bool isPrepared, string dataType, object dataValue)
100100
connection.Execute($@"DROP TABLE IF EXISTS prepared_command_test;
101101
CREATE TABLE prepared_command_test(rowid INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, data {dataType});");
102102

103+
using (var command = new MySqlCommand("INSERT INTO prepared_command_test(data) VALUES(@null), (@data);", connection))
104+
{
105+
command.Parameters.AddWithValue("@null", null);
106+
command.Parameters.AddWithValue("@data", dataValue).MySqlDbType = dbType;
107+
if (isPrepared)
108+
command.Prepare();
109+
Assert.Equal(isPrepared, command.IsPrepared);
110+
command.ExecuteNonQuery();
111+
}
112+
113+
using (var command = new MySqlCommand("SELECT data FROM prepared_command_test ORDER BY rowid;", connection))
114+
{
115+
if (isPrepared)
116+
command.Prepare();
117+
Assert.Equal(isPrepared, command.IsPrepared);
118+
119+
using (var reader = command.ExecuteReader())
120+
{
121+
Assert.True(reader.Read());
122+
Assert.True(reader.IsDBNull(0));
123+
124+
Assert.True(reader.Read());
125+
Assert.False(reader.IsDBNull(0));
126+
Assert.Equal(dataValue, reader.GetValue(0));
127+
128+
Assert.False(reader.Read());
129+
Assert.False(reader.NextResult());
130+
}
131+
}
132+
}
133+
}
134+
135+
[Theory]
136+
[MemberData(nameof(GetInsertAndQueryData))]
137+
public void InsertAndQueryInferrredType(bool isPrepared, string dataType, object dataValue, MySqlDbType dbType)
138+
{
139+
GC.KeepAlive(dbType); // ignore the parameter
140+
var csb = new MySqlConnectionStringBuilder(AppConfig.ConnectionString)
141+
{
142+
IgnorePrepare = !isPrepared,
143+
};
144+
using (var connection = new MySqlConnection(csb.ConnectionString))
145+
{
146+
connection.Open();
147+
connection.Execute($@"DROP TABLE IF EXISTS prepared_command_test;
148+
CREATE TABLE prepared_command_test(rowid INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, data {dataType});");
149+
103150
using (var command = new MySqlCommand("INSERT INTO prepared_command_test(data) VALUES(@null), (@data);", connection))
104151
{
105152
command.Parameters.AddWithValue("@null", null);
@@ -134,7 +181,7 @@ public void InsertAndQuery(bool isPrepared, string dataType, object dataValue)
134181

135182
[SkippableTheory(Baseline = "https://bugs.mysql.com/bug.php?id=14115")]
136183
[MemberData(nameof(GetInsertAndQueryData))]
137-
public void InsertAndQueryMultipleStatements(bool isPrepared, string dataType, object dataValue)
184+
public void InsertAndQueryMultipleStatements(bool isPrepared, string dataType, object dataValue, MySqlDbType dbType)
138185
{
139186
var csb = new MySqlConnectionStringBuilder(AppConfig.ConnectionString)
140187
{
@@ -151,7 +198,7 @@ public void InsertAndQueryMultipleStatements(bool isPrepared, string dataType, o
151198
SELECT data FROM prepared_command_test ORDER BY rowid;", connection))
152199
{
153200
command.Parameters.AddWithValue("@null", null);
154-
command.Parameters.AddWithValue("@data", dataValue);
201+
command.Parameters.AddWithValue("@data", dataValue).MySqlDbType = dbType;
155202
if (isPrepared)
156203
command.Prepare();
157204
Assert.Equal(isPrepared, command.IsPrepared);
@@ -348,42 +395,51 @@ public static IEnumerable<object[]> GetInsertAndQueryData()
348395
{
349396
foreach (var isPrepared in new[] { false, true })
350397
{
351-
yield return new object[] { isPrepared, "TINYINT", (sbyte) -123 };
352-
yield return new object[] { isPrepared, "TINYINT UNSIGNED", (byte) 123 };
353-
yield return new object[] { isPrepared, "SMALLINT", (short) -12345 };
354-
yield return new object[] { isPrepared, "SMALLINT UNSIGNED", (ushort) 12345 };
355-
yield return new object[] { isPrepared, "MEDIUMINT", -1234567 };
356-
yield return new object[] { isPrepared, "MEDIUMINT UNSIGNED", 1234567u };
357-
yield return new object[] { isPrepared, "INT", -123456789 };
358-
yield return new object[] { isPrepared, "INT UNSIGNED", 123456789u };
359-
yield return new object[] { isPrepared, "BIGINT", -1234567890123456789L };
360-
yield return new object[] { isPrepared, "BIGINT UNSIGNED", 1234567890123456789UL };
361-
yield return new object[] { isPrepared, "BIT(10)", 1000UL };
362-
yield return new object[] { isPrepared, "BINARY(5)", new byte[] { 5, 6, 7, 8, 9 } };
363-
yield return new object[] { isPrepared, "VARBINARY(100)", new byte[] { 7, 8, 9, 10 } };
364-
yield return new object[] { isPrepared, "BLOB", new byte[] { 5, 4, 3, 2, 1 } };
365-
yield return new object[] { isPrepared, "CHAR(36)", new Guid("00112233-4455-6677-8899-AABBCCDDEEFF") };
366-
yield return new object[] { isPrepared, "FLOAT", 12.375f };
367-
yield return new object[] { isPrepared, "DOUBLE", 14.21875 };
368-
yield return new object[] { isPrepared, "DECIMAL(9,3)", 123.45m };
369-
yield return new object[] { isPrepared, "VARCHAR(100)", "test;@'; -- " };
370-
yield return new object[] { isPrepared, "TEXT", "testing testing" };
371-
yield return new object[] { isPrepared, "DATE", new DateTime(2018, 7, 23) };
372-
yield return new object[] { isPrepared, "DATETIME(3)", new DateTime(2018, 7, 23, 20, 46, 52, 123) };
373-
yield return new object[] { isPrepared, "ENUM('small', 'medium', 'large')", "medium" };
374-
yield return new object[] { isPrepared, "SET('one','two','four','eight')", "two,eight" };
375-
yield return new object[] { isPrepared, "BOOL", true };
398+
yield return new object[] { isPrepared, "TINYINT", (sbyte) -123, MySqlDbType.Byte };
399+
yield return new object[] { isPrepared, "TINYINT UNSIGNED", (byte) 123, MySqlDbType.UByte };
400+
yield return new object[] { isPrepared, "SMALLINT", (short) -12345, MySqlDbType.Int16 };
401+
yield return new object[] { isPrepared, "SMALLINT UNSIGNED", (ushort) 12345, MySqlDbType.UInt16 };
402+
#if !BASELINE
403+
yield return new object[] { isPrepared, "MEDIUMINT", -1234567, MySqlDbType.Int24 };
404+
#else
405+
// https://bugs.mysql.com/bug.php?id=95986
406+
yield return new object[] { isPrepared, "MEDIUMINT", -1234567, MySqlDbType.Int32 };
407+
#endif
408+
yield return new object[] { isPrepared, "MEDIUMINT UNSIGNED", 1234567u, MySqlDbType.UInt24 };
409+
yield return new object[] { isPrepared, "INT", -123456789, MySqlDbType.Int32 };
410+
yield return new object[] { isPrepared, "INT UNSIGNED", 123456789u, MySqlDbType.UInt32 };
411+
yield return new object[] { isPrepared, "BIGINT", -1234567890123456789L, MySqlDbType.Int64 };
412+
yield return new object[] { isPrepared, "BIGINT UNSIGNED", 1234567890123456789UL, MySqlDbType.UInt64 };
413+
yield return new object[] { isPrepared, "BIT(10)", 1000UL, MySqlDbType.Bit };
414+
yield return new object[] { isPrepared, "BINARY(5)", new byte[] { 5, 6, 7, 8, 9 }, MySqlDbType.Binary };
415+
yield return new object[] { isPrepared, "VARBINARY(100)", new byte[] { 7, 8, 9, 10 }, MySqlDbType.VarBinary };
416+
yield return new object[] { isPrepared, "BLOB", new byte[] { 5, 4, 3, 2, 1 }, MySqlDbType.Blob };
417+
yield return new object[] { isPrepared, "CHAR(36)", new Guid("00112233-4455-6677-8899-AABBCCDDEEFF"), MySqlDbType.Guid };
418+
yield return new object[] { isPrepared, "FLOAT", 12.375f, MySqlDbType.Float };
419+
yield return new object[] { isPrepared, "DOUBLE", 14.21875, MySqlDbType.Double };
420+
yield return new object[] { isPrepared, "DECIMAL(9,3)", 123.45m, MySqlDbType.Decimal };
421+
yield return new object[] { isPrepared, "VARCHAR(100)", "test;@'; -- ", MySqlDbType.VarChar };
422+
yield return new object[] { isPrepared, "TEXT", "testing testing", MySqlDbType.Text };
423+
yield return new object[] { isPrepared, "DATE", new DateTime(2018, 7, 23), MySqlDbType.Date };
424+
yield return new object[] { isPrepared, "DATETIME(3)", new DateTime(2018, 7, 23, 20, 46, 52, 123), MySqlDbType.DateTime };
425+
yield return new object[] { isPrepared, "ENUM('small', 'medium', 'large')", "medium", MySqlDbType.Enum };
426+
yield return new object[] { isPrepared, "SET('one','two','four','eight')", "two,eight", MySqlDbType.Set };
427+
#if !BASELINE
428+
yield return new object[] { isPrepared, "BOOL", true, MySqlDbType.Bool };
429+
#else
430+
yield return new object[] { isPrepared, "BOOL", true, MySqlDbType.Int32 };
431+
#endif
376432

377433
#if !BASELINE
378434
// https://bugs.mysql.com/bug.php?id=91770
379-
yield return new object[] { isPrepared, "TIME(3)", TimeSpan.Zero.Subtract(new TimeSpan(15, 10, 34, 56, 789)) };
435+
yield return new object[] { isPrepared, "TIME(3)", TimeSpan.Zero.Subtract(new TimeSpan(15, 10, 34, 56, 789)), MySqlDbType.Time };
380436

381437
// https://bugs.mysql.com/bug.php?id=91751
382-
yield return new object[] { isPrepared, "YEAR", 2134 };
438+
yield return new object[] { isPrepared, "YEAR", 2134, MySqlDbType.Year };
383439
#endif
384440

385441
if (AppConfig.SupportsJson)
386-
yield return new object[] { isPrepared, "JSON", "{\"test\": true}" };
442+
yield return new object[] { isPrepared, "JSON", "{\"test\": true}", MySqlDbType.JSON };
387443
}
388444
}
389445

0 commit comments

Comments
 (0)