From a52beb8962d5da7a06ff86e02b12755afd3371dd Mon Sep 17 00:00:00 2001 From: Bradley Grainger Date: Mon, 23 Jul 2018 22:00:06 -0700 Subject: [PATCH] Execute prepared commands comprising a single statement. Signed-off-by: Bradley Grainger --- .../tutorials/migrating-from-connector-net.md | 5 + src/MySqlConnector/Core/PreparedStatement.cs | 23 ++ .../Core/PreparedStatementCommandExecutor.cs | 109 +++++++++ .../MySql.Data.MySqlClient/MySqlCommand.cs | 112 +++++++-- .../MySql.Data.MySqlClient/MySqlParameter.cs | 217 ++++++++++++++++++ src/MySqlConnector/Protocol/CommandKind.cs | 1 + .../StatementPrepareResponsePayload.cs | 31 +++ tests/SideBySide/QueryTests.cs | 92 ++++++++ 8 files changed, 575 insertions(+), 15 deletions(-) create mode 100644 src/MySqlConnector/Core/PreparedStatement.cs create mode 100644 src/MySqlConnector/Core/PreparedStatementCommandExecutor.cs create mode 100644 src/MySqlConnector/Protocol/Payloads/StatementPrepareResponsePayload.cs diff --git a/docs/content/tutorials/migrating-from-connector-net.md b/docs/content/tutorials/migrating-from-connector-net.md index 2974933aa..1e66af2d7 100644 --- a/docs/content/tutorials/migrating-from-connector-net.md +++ b/docs/content/tutorials/migrating-from-connector-net.md @@ -128,3 +128,8 @@ The following bugs in Connector/NET are fixed by switching to MySqlConnector. * [#89335](https://bugs.mysql.com/bug.php?id=89335): `MySqlCommandBuilder.DeriveParameters` fails for `JSON` type * [#91123](https://bugs.mysql.com/bug.php?id=91123): Database names are case-sensitive when calling a stored procedure * [#91199](https://bugs.mysql.com/bug.php?id=91199): Can't insert `MySqlDateTime` values +* [#91751](https://bugs.mysql.com/bug.php?id=91751): `YEAR` column retrieved incorrectly with prepared command +* [#91752](https://bugs.mysql.com/bug.php?id=91752): `00:00:00` is converted to `NULL` with prepared command +* [#91753](https://bugs.mysql.com/bug.php?id=91753): Unnamed parameter not supported by `MySqlCommand.Prepare` +* [#91754](https://bugs.mysql.com/bug.php?id=91754): Inserting 16MiB `BLOB` shifts it by four bytes when prepared +* [#91770](https://bugs.mysql.com/bug.php?id=91770): `TIME(n)` column loses microseconds with prepared command diff --git a/src/MySqlConnector/Core/PreparedStatement.cs b/src/MySqlConnector/Core/PreparedStatement.cs new file mode 100644 index 000000000..53c80c704 --- /dev/null +++ b/src/MySqlConnector/Core/PreparedStatement.cs @@ -0,0 +1,23 @@ +using MySqlConnector.Protocol.Payloads; + +namespace MySqlConnector.Core +{ + /// + /// is a statement that has been prepared on the MySQL Server. + /// + internal sealed class PreparedStatement + { + public PreparedStatement(int statementId, ParsedStatement statement, ColumnDefinitionPayload[] columns, ColumnDefinitionPayload[] parameters) + { + StatementId = statementId; + Statement = statement; + Columns = columns; + Parameters = parameters; + } + + public int StatementId { get; } + public ParsedStatement Statement { get; } + public ColumnDefinitionPayload[] Columns { get; } + public ColumnDefinitionPayload[] Parameters { get; } + } +} diff --git a/src/MySqlConnector/Core/PreparedStatementCommandExecutor.cs b/src/MySqlConnector/Core/PreparedStatementCommandExecutor.cs new file mode 100644 index 000000000..8fd87ca68 --- /dev/null +++ b/src/MySqlConnector/Core/PreparedStatementCommandExecutor.cs @@ -0,0 +1,109 @@ +using System; +using System.Data; +using System.Data.Common; +using System.IO; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using MySql.Data.MySqlClient; +using MySqlConnector.Logging; +using MySqlConnector.Protocol; +using MySqlConnector.Protocol.Serialization; +using MySqlConnector.Utilities; + +namespace MySqlConnector.Core +{ + internal sealed class PreparedStatementCommandExecutor : ICommandExecutor + { + public PreparedStatementCommandExecutor(MySqlCommand command) + { + m_command = command; + } + + public async Task ExecuteReaderAsync(string commandText, MySqlParameterCollection parameterCollection, CommandBehavior behavior, IOBehavior ioBehavior, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + if (Log.IsDebugEnabled()) + Log.Debug("Session{0} ExecuteBehavior {1} CommandText: {2}", m_command.Connection.Session.Id, ioBehavior, commandText); + using (var payload = CreateQueryPayload(m_command.PreparedStatements[0], parameterCollection, m_command.Connection.GuidFormat)) + using (m_command.RegisterCancel(cancellationToken)) + { + m_command.Connection.Session.StartQuerying(m_command); + m_command.LastInsertedId = -1; + try + { + await m_command.Connection.Session.SendAsync(payload, ioBehavior, CancellationToken.None).ConfigureAwait(false); + return await MySqlDataReader.CreateAsync(m_command, behavior, ResultSetProtocol.Binary, ioBehavior).ConfigureAwait(false); + } + catch (MySqlException ex) when (ex.Number == (int) MySqlErrorCode.QueryInterrupted && cancellationToken.IsCancellationRequested) + { + Log.Warn("Session{0} query was interrupted", m_command.Connection.Session.Id); + throw new OperationCanceledException(cancellationToken); + } + catch (Exception ex) when (payload.ArraySegment.Count > 4_194_304 && (ex is SocketException || ex is IOException || ex is MySqlProtocolException)) + { + // the default MySQL Server value for max_allowed_packet (in MySQL 5.7) is 4MiB: https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_max_allowed_packet + // use "decimal megabytes" (to round up) when creating the exception message + int megabytes = payload.ArraySegment.Count / 1_000_000; + throw new MySqlException("Error submitting {0}MB packet; ensure 'max_allowed_packet' is greater than {0}MB.".FormatInvariant(megabytes), ex); + } + } + } + + private PayloadData CreateQueryPayload(PreparedStatement preparedStatement, MySqlParameterCollection parameterCollection, MySqlGuidFormat guidFormat) + { + var writer = new ByteBufferWriter(); + writer.Write((byte) CommandKind.StatementExecute); + writer.Write(preparedStatement.StatementId); + writer.Write((byte) 0); + writer.Write(1); + if (preparedStatement.Parameters?.Length > 0) + { + // TODO: How to handle incorrect number of parameters? + + // build subset of parameters for this statement + var parameters = new MySqlParameter[preparedStatement.Statement.ParameterNames.Count]; + for (var i = 0; i < preparedStatement.Statement.ParameterNames.Count; i++) + { + var parameterName = preparedStatement.Statement.ParameterNames[i]; + var parameterIndex = parameterName != null ? parameterCollection.NormalizedIndexOf(parameterName) : preparedStatement.Statement.ParameterIndexes[i]; + parameters[i] = parameterCollection[parameterIndex]; + } + + // write null bitmap + byte nullBitmap = 0; + for (var i = 0; i < parameters.Length; i++) + { + var parameter = parameters[i]; + if (parameter.Value == null || parameter.Value == DBNull.Value) + { + if (i > 0 && i % 8 == 0) + { + writer.Write(nullBitmap); + nullBitmap = 0; + } + + nullBitmap |= (byte) (1 << (i % 8)); + } + } + writer.Write(nullBitmap); + + // write "new parameters bound" flag + writer.Write((byte) 1); + + foreach (var parameter in parameters) + writer.Write(TypeMapper.ConvertToColumnTypeAndFlags(parameter.MySqlDbType, guidFormat)); + + var options = m_command.CreateStatementPreparerOptions(); + foreach (var parameter in parameters) + parameter.AppendBinary(writer, options); + } + + return writer.ToPayloadData(); + } + + static IMySqlConnectorLogger Log { get; } = MySqlConnectorLogManager.CreateLogger(nameof(PreparedStatementCommandExecutor)); + + readonly MySqlCommand m_command; + } +} diff --git a/src/MySqlConnector/MySql.Data.MySqlClient/MySqlCommand.cs b/src/MySqlConnector/MySql.Data.MySqlClient/MySqlCommand.cs index 94d010ab8..013f7b9f3 100644 --- a/src/MySqlConnector/MySql.Data.MySqlClient/MySqlCommand.cs +++ b/src/MySqlConnector/MySql.Data.MySqlClient/MySqlCommand.cs @@ -1,9 +1,12 @@ using System; +using System.Collections.Generic; using System.Data; using System.Data.Common; using System.Threading; using System.Threading.Tasks; using MySqlConnector.Core; +using MySqlConnector.Protocol; +using MySqlConnector.Protocol.Payloads; using MySqlConnector.Protocol.Serialization; using MySqlConnector.Utilities; @@ -62,7 +65,11 @@ public new MySqlParameterCollection Parameters public new MySqlDataReader ExecuteReader(CommandBehavior commandBehavior) => (MySqlDataReader) base.ExecuteReader(commandBehavior); - public override void Prepare() + public override void Prepare() => PrepareAsync(IOBehavior.Synchronous, default).GetAwaiter().GetResult(); + public Task PrepareAsync() => PrepareAsync(AsyncIOBehavior, default); + public Task PrepareAsync(CancellationToken cancellationToken) => PrepareAsync(AsyncIOBehavior, cancellationToken); + + private async Task PrepareAsync(IOBehavior ioBehavior, CancellationToken cancellationToken) { if (Connection == null) throw new InvalidOperationException("Connection property must be non-null."); @@ -70,6 +77,73 @@ public override void Prepare() throw new InvalidOperationException("Connection must be Open; current state is {0}".FormatInvariant(Connection.State)); if (string.IsNullOrWhiteSpace(CommandText)) throw new InvalidOperationException("CommandText must be specified"); + if (m_connection?.HasActiveReader ?? false) + throw new InvalidOperationException("Cannot call Prepare when there is an open DataReader for this command; it must be closed first."); + if (Connection.IgnorePrepare) + return; + + if (CommandType != CommandType.Text) + throw new NotSupportedException("Only CommandType.Text is currently supported by MySqlCommand.Prepare"); + + var statementPreparer = new StatementPreparer(CommandText, Parameters, CreateStatementPreparerOptions()); + var parsedStatements = statementPreparer.SplitStatements(); + + if (parsedStatements.Statements.Count > 1) + throw new NotSupportedException("Multiple semicolon-delimited SQL statements are not supported by MySqlCommand.Prepare"); + + var columnsAndParameters = new ResizableArray(); + var columnsAndParametersSize = 0; + + var preparedStatements = new List(parsedStatements.Statements.Count); + foreach (var statement in parsedStatements.Statements) + { + await Connection.Session.SendAsync(new PayloadData(statement.StatementBytes), ioBehavior, cancellationToken).ConfigureAwait(false); + var payload = await Connection.Session.ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false); + var response = StatementPrepareResponsePayload.Create(payload); + + ColumnDefinitionPayload[] parameters = null; + if (response.ParameterCount > 0) + { + parameters = new ColumnDefinitionPayload[response.ParameterCount]; + for (var i = 0; i < response.ParameterCount; i++) + { + payload = await Connection.Session.ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false); + Utility.Resize(ref columnsAndParameters, columnsAndParametersSize + payload.ArraySegment.Count); + Buffer.BlockCopy(payload.ArraySegment.Array, payload.ArraySegment.Offset, columnsAndParameters.Array, columnsAndParametersSize, payload.ArraySegment.Count); + parameters[i] = ColumnDefinitionPayload.Create(new ResizableArraySegment(columnsAndParameters, columnsAndParametersSize, payload.ArraySegment.Count)); + columnsAndParametersSize += payload.ArraySegment.Count; + } + if (!Connection.Session.SupportsDeprecateEof) + { + payload = await Connection.Session.ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false); + EofPayload.Create(payload); + } + } + + ColumnDefinitionPayload[] columns = null; + if (response.ColumnCount > 0) + { + columns = new ColumnDefinitionPayload[response.ColumnCount]; + for (var i = 0; i < response.ColumnCount; i++) + { + payload = await Connection.Session.ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false); + Utility.Resize(ref columnsAndParameters, columnsAndParametersSize + payload.ArraySegment.Count); + Buffer.BlockCopy(payload.ArraySegment.Array, payload.ArraySegment.Offset, columnsAndParameters.Array, columnsAndParametersSize, payload.ArraySegment.Count); + columns[i] = ColumnDefinitionPayload.Create(new ResizableArraySegment(columnsAndParameters, columnsAndParametersSize, payload.ArraySegment.Count)); + columnsAndParametersSize += payload.ArraySegment.Count; + } + if (!Connection.Session.SupportsDeprecateEof) + { + payload = await Connection.Session.ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false); + EofPayload.Create(payload); + } + } + + preparedStatements.Add(new PreparedStatement(response.StatementId, statement, columns, parameters)); + } + + m_parsedStatements = parsedStatements; + m_statements = preparedStatements; } public override string CommandText @@ -80,6 +154,7 @@ public override string CommandText if (m_connection?.HasActiveReader ?? false) throw new InvalidOperationException("Cannot set MySqlCommand.CommandText when there is an open DataReader for this command; it must be closed first."); m_commandText = value; + m_statements = null; } } @@ -104,22 +179,12 @@ public override int CommandTimeout public override CommandType CommandType { - get - { - return m_commandType; - } + get => m_commandType; set { if (value != CommandType.Text && value != CommandType.StoredProcedure) throw new ArgumentException("CommandType must be Text or StoredProcedure.", nameof(value)); - if (value == m_commandType) - return; - m_commandType = value; - if (value == CommandType.Text) - m_commandExecutor = new TextCommandExecutor(this); - else if (value == CommandType.StoredProcedure) - m_commandExecutor = new StoredProcedureCommandExecutor(this); } } @@ -203,9 +268,20 @@ protected override Task ExecuteDbDataReaderAsync(CommandBehavior b return ExecuteReaderAsync(behavior, AsyncIOBehavior, cancellationToken); } - internal Task ExecuteReaderAsync(CommandBehavior behavior, IOBehavior ioBehavior, CancellationToken cancellationToken) => - !IsValid(out var exception) ? Utility.TaskFromException(exception) : - m_commandExecutor.ExecuteReaderAsync(CommandText, m_parameterCollection, behavior, ioBehavior, cancellationToken); + internal Task ExecuteReaderAsync(CommandBehavior behavior, IOBehavior ioBehavior, CancellationToken cancellationToken) + { + if (!IsValid(out var exception)) + return Utility.TaskFromException(exception); + + if (m_statements != null) + m_commandExecutor = new PreparedStatementCommandExecutor(this); + else if (m_commandType == CommandType.Text) + m_commandExecutor = new TextCommandExecutor(this); + else if (m_commandType == CommandType.StoredProcedure) + m_commandExecutor = new StoredProcedureCommandExecutor(this); + + return m_commandExecutor.ExecuteReaderAsync(CommandText, m_parameterCollection, behavior, ioBehavior, cancellationToken); + } protected override void Dispose(bool disposing) { @@ -214,6 +290,8 @@ protected override void Dispose(bool disposing) if (disposing) { m_parameterCollection = null; + m_parsedStatements?.Dispose(); + m_parsedStatements = null; } } finally @@ -243,6 +321,8 @@ internal IDisposable RegisterCancel(CancellationToken token) internal int CancelAttemptCount { get; set; } + internal IReadOnlyList PreparedStatements => m_statements; + /// /// Causes the effective command timeout to be reset back to the value specified by . /// @@ -324,6 +404,8 @@ private bool IsValid(out Exception exception) MySqlConnection m_connection; string m_commandText; MySqlParameterCollection m_parameterCollection; + ParsedStatements m_parsedStatements; + IReadOnlyList m_statements; int? m_commandTimeout; CommandType m_commandType; ICommandExecutor m_commandExecutor; diff --git a/src/MySqlConnector/MySql.Data.MySqlClient/MySqlParameter.cs b/src/MySqlConnector/MySql.Data.MySqlClient/MySqlParameter.cs index 7b6ada49c..d4f2b9268 100644 --- a/src/MySqlConnector/MySql.Data.MySqlClient/MySqlParameter.cs +++ b/src/MySqlConnector/MySql.Data.MySqlClient/MySqlParameter.cs @@ -381,6 +381,174 @@ internal void AppendSqlString(ByteBufferWriter writer, StatementPreparerOptions } } + internal void AppendBinary(ByteBufferWriter writer, StatementPreparerOptions options) + { + if (Value == null || Value == DBNull.Value) + { + // stored in "null bitmap" only + } + else if (Value is string stringValue) + { + writer.WriteLengthEncodedString(stringValue); + } + else if (Value is char charValue) + { + writer.WriteLengthEncodedString(charValue.ToString()); + } + else if (Value is sbyte sbyteValue) + { + writer.Write(unchecked((byte) sbyteValue)); + } + else if (Value is byte byteValue) + { + writer.Write(byteValue); + } + else if (Value is bool boolValue) + { + writer.Write((byte) (boolValue ? 1 : 0)); + } + else if (Value is short shortValue) + { + writer.Write(unchecked((ushort) shortValue)); + } + else if (Value is ushort ushortValue) + { + writer.Write(ushortValue); + } + else if (Value is int intValue) + { + writer.Write(intValue); + } + else if (Value is uint uintValue) + { + writer.Write(uintValue); + } + else if (Value is long longValue) + { + writer.Write(unchecked((ulong) longValue)); + } + else if (Value is ulong ulongValue) + { + writer.Write(ulongValue); + } + else if (Value is byte[] byteArrayValue) + { + writer.WriteLengthEncodedInteger(unchecked((ulong) byteArrayValue.Length)); + writer.Write(byteArrayValue); + } + else if (Value is float floatValue) + { + writer.Write(BitConverter.GetBytes(floatValue)); + } + else if (Value is double doubleValue) + { + writer.Write(unchecked((ulong) BitConverter.DoubleToInt64Bits(doubleValue))); + } + else if (Value is decimal) + { + writer.WriteLengthEncodedString("{0}".FormatInvariant(Value)); + } + else if (Value is MySqlDateTime mySqlDateTimeValue) + { + if (mySqlDateTimeValue.IsValidDateTime) + WriteDateTime(writer, mySqlDateTimeValue.GetDateTime()); + else + writer.Write((byte) 0); + } + else if (Value is DateTime dateTimeValue) + { + if ((options & StatementPreparerOptions.DateTimeUtc) != 0 && dateTimeValue.Kind == DateTimeKind.Local) + throw new MySqlException("DateTime.Kind must not be Local when DateTimeKind setting is Utc (parameter name: {0})".FormatInvariant(ParameterName)); + else if ((options & StatementPreparerOptions.DateTimeLocal) != 0 && dateTimeValue.Kind == DateTimeKind.Utc) + throw new MySqlException("DateTime.Kind must not be Utc when DateTimeKind setting is Local (parameter name: {0})".FormatInvariant(ParameterName)); + + WriteDateTime(writer, dateTimeValue); + } + else if (Value is DateTimeOffset dateTimeOffsetValue) + { + // store as UTC as it will be read as such when deserialized from a timespan column + WriteDateTime(writer, dateTimeOffsetValue.UtcDateTime); + } + else if (Value is TimeSpan ts) + { + WriteTime(writer, ts); + } + else if (Value is Guid guidValue) + { + StatementPreparerOptions guidOptions = options & StatementPreparerOptions.GuidFormatMask; + if (guidOptions == StatementPreparerOptions.GuidFormatBinary16 || + guidOptions == StatementPreparerOptions.GuidFormatTimeSwapBinary16 || + guidOptions == StatementPreparerOptions.GuidFormatLittleEndianBinary16) + { + var bytes = guidValue.ToByteArray(); + if (guidOptions != StatementPreparerOptions.GuidFormatLittleEndianBinary16) + { + Utility.SwapBytes(bytes, 0, 3); + Utility.SwapBytes(bytes, 1, 2); + Utility.SwapBytes(bytes, 4, 5); + Utility.SwapBytes(bytes, 6, 7); + + if (guidOptions == StatementPreparerOptions.GuidFormatTimeSwapBinary16) + { + Utility.SwapBytes(bytes, 0, 4); + Utility.SwapBytes(bytes, 1, 5); + Utility.SwapBytes(bytes, 2, 6); + Utility.SwapBytes(bytes, 3, 7); + Utility.SwapBytes(bytes, 0, 2); + Utility.SwapBytes(bytes, 1, 3); + } + } + writer.Write((byte) 16); + writer.Write(bytes); + } + else + { + var is32Characters = guidOptions == StatementPreparerOptions.GuidFormatChar32; + var guidLength = is32Characters ? 32 : 36; + writer.Write((byte) guidLength); + var span = writer.GetSpan(guidLength); + Utf8Formatter.TryFormat(guidValue, span, out _, is32Characters ? 'N' : 'D'); + writer.Advance(guidLength); + } + } + else if (MySqlDbType == MySqlDbType.Int16) + { + writer.Write((ushort) (short) Value); + } + else if (MySqlDbType == MySqlDbType.UInt16) + { + writer.Write((ushort) Value); + } + else if (MySqlDbType == MySqlDbType.Int32) + { + writer.Write((int) Value); + } + else if (MySqlDbType == MySqlDbType.UInt32) + { + writer.Write((uint) Value); + } + else if (MySqlDbType == MySqlDbType.Int64) + { + writer.Write((ulong) (long) Value); + } + else if (MySqlDbType == MySqlDbType.UInt64) + { + writer.Write((ulong) Value); + } + else if ((MySqlDbType == MySqlDbType.String || MySqlDbType == MySqlDbType.VarChar) && HasSetDbType && Value is Enum) + { + writer.WriteLengthEncodedString("{0:G}".FormatInvariant(Value)); + } + else if (Value is Enum) + { + writer.Write(Convert.ToInt32(Value)); + } + else + { + throw new NotSupportedException("Parameter type {0} (DbType: {1}) not currently supported. Value: {2}".FormatInvariant(Value.GetType().Name, DbType, Value)); + } + } + internal static string NormalizeParameterName(string name) { name = name.Trim(); @@ -395,6 +563,55 @@ internal static string NormalizeParameterName(string name) return name.StartsWith("@", StringComparison.Ordinal) || name.StartsWith("?", StringComparison.Ordinal) ? name.Substring(1) : name; } + private static void WriteDateTime(ByteBufferWriter writer, DateTime dateTime) + { + byte length; + var microseconds = (int) (dateTime.Ticks % 10_000_000) / 10; + if (microseconds != 0) + length = 11; + else if (dateTime.Hour != 0 || dateTime.Minute != 0 || dateTime.Second != 0) + length = 7; + else + length = 4; + writer.Write(length); + writer.Write((ushort) dateTime.Year); + writer.Write((byte) dateTime.Month); + writer.Write((byte) dateTime.Day); + if (length > 4) + { + writer.Write((byte) dateTime.Hour); + writer.Write((byte) dateTime.Minute); + writer.Write((byte) dateTime.Second); + if (length > 7) + { + writer.Write(microseconds); + } + } + } + + private static void WriteTime(ByteBufferWriter writer, TimeSpan timeSpan) + { + var ticks = timeSpan.Ticks; + if (ticks == 0) + { + writer.Write((byte) 0); + } + else + { + if (ticks < 0) + timeSpan = TimeSpan.FromTicks(-ticks); + var microseconds = (int) (timeSpan.Ticks % 10_000_000) / 10; + writer.Write((byte) (microseconds == 0 ? 8 : 12)); + writer.Write((byte) (ticks < 0 ? 1 : 0)); + writer.Write(timeSpan.Days); + writer.Write((byte) timeSpan.Hours); + writer.Write((byte) timeSpan.Minutes); + writer.Write((byte) timeSpan.Seconds); + if (microseconds != 0) + writer.Write(microseconds); + } + } + static readonly byte[] s_nullBytes = { 0x4E, 0x55, 0x4C, 0x4C }; // NULL static readonly byte[] s_trueBytes = { 0x74, 0x72, 0x75, 0x65 }; // true static readonly byte[] s_falseBytes = { 0x66, 0x61, 0x6C, 0x73, 0x65 }; // false diff --git a/src/MySqlConnector/Protocol/CommandKind.cs b/src/MySqlConnector/Protocol/CommandKind.cs index 5aae9ccdb..01cc15b46 100644 --- a/src/MySqlConnector/Protocol/CommandKind.cs +++ b/src/MySqlConnector/Protocol/CommandKind.cs @@ -8,6 +8,7 @@ internal enum CommandKind Ping = 14, ChangeUser = 17, StatementPrepare = 22, + StatementExecute = 23, ResetConnection = 31, } } diff --git a/src/MySqlConnector/Protocol/Payloads/StatementPrepareResponsePayload.cs b/src/MySqlConnector/Protocol/Payloads/StatementPrepareResponsePayload.cs new file mode 100644 index 000000000..ae659842e --- /dev/null +++ b/src/MySqlConnector/Protocol/Payloads/StatementPrepareResponsePayload.cs @@ -0,0 +1,31 @@ +using MySqlConnector.Protocol.Serialization; + +namespace MySqlConnector.Protocol.Payloads +{ + internal sealed class StatementPrepareResponsePayload + { + public int StatementId { get; } + public int ColumnCount { get; } + public int ParameterCount { get; } + + public static StatementPrepareResponsePayload Create(PayloadData payload) + { + var reader = new ByteArrayReader(payload.ArraySegment); + reader.ReadByte(0); + var statementId = reader.ReadInt32(); + var columnCount = (int) reader.ReadInt16(); + var parameterCount = (int) reader.ReadInt16(); + reader.ReadByte(0); + var warningCount = (int) reader.ReadInt16(); + + return new StatementPrepareResponsePayload(statementId, columnCount, parameterCount); + } + + private StatementPrepareResponsePayload(int statementId, int columnCount, int parameterCount) + { + StatementId = statementId; + ColumnCount = columnCount; + ParameterCount = parameterCount; + } + } +} diff --git a/tests/SideBySide/QueryTests.cs b/tests/SideBySide/QueryTests.cs index b3e01e290..aa100e985 100644 --- a/tests/SideBySide/QueryTests.cs +++ b/tests/SideBySide/QueryTests.cs @@ -989,6 +989,98 @@ enum TestLongEnum : long Value = long.MaxValue, } + [Theory] + [MemberData(nameof(GetPreparedCommandsData))] + public void PreparedCommands(bool isPrepared, string dataType, object dataValue) + { + var csb = new MySqlConnectionStringBuilder(AppConfig.ConnectionString) + { + IgnorePrepare = !isPrepared, + }; + using (var connection = new MySqlConnection(csb.ConnectionString)) + { + connection.Open(); + using (var command = new MySqlCommand($@"DROP TABLE IF EXISTS prepared_command_test; +CREATE TABLE prepared_command_test(rowid INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, data {dataType});", connection)) + { + command.ExecuteNonQuery(); + } + + using (var command = new MySqlCommand("INSERT INTO prepared_command_test(data) VALUES(@null), (@data);", connection)) + { + command.Parameters.AddWithValue("@null", null); + command.Parameters.AddWithValue("@data", dataValue); + if (isPrepared) + command.Prepare(); + command.ExecuteNonQuery(); + } + + using (var command = new MySqlCommand("SELECT data FROM prepared_command_test ORDER BY rowid;", connection)) + { + if (isPrepared) + command.Prepare(); + + using (var reader = command.ExecuteReader()) + { + Assert.True(reader.Read()); + Assert.True(reader.IsDBNull(0)); + + Assert.True(reader.Read()); + Assert.False(reader.IsDBNull(0)); + Assert.Equal(dataValue, reader.GetValue(0)); + + Assert.False(reader.Read()); + Assert.False(reader.NextResult()); + } + } + } + } + + public static IEnumerable GetPreparedCommandsData() + { + foreach (var isPrepared in new[] { false, true }) + { + yield return new object[] { isPrepared, "TINYINT", (sbyte) -123 }; + yield return new object[] { isPrepared, "TINYINT UNSIGNED", (byte) 123 }; + yield return new object[] { isPrepared, "SMALLINT", (short) -12345 }; + yield return new object[] { isPrepared, "SMALLINT UNSIGNED", (ushort) 12345 }; + yield return new object[] { isPrepared, "MEDIUMINT", -1234567 }; + yield return new object[] { isPrepared, "MEDIUMINT UNSIGNED", 1234567u }; + yield return new object[] { isPrepared, "INT", -123456789 }; + yield return new object[] { isPrepared, "INT UNSIGNED", 123456789u }; + yield return new object[] { isPrepared, "BIGINT", -1234567890123456789L }; + yield return new object[] { isPrepared, "BIGINT UNSIGNED", 1234567890123456789UL }; + yield return new object[] { isPrepared, "BIT(10)", 1000UL }; + yield return new object[] { isPrepared, "BINARY(5)", new byte[] { 5, 6, 7, 8, 9 } }; + yield return new object[] { isPrepared, "VARBINARY(100)", new byte[] { 7, 8, 9, 10 } }; + yield return new object[] { isPrepared, "BLOB", new byte[] { 5, 4, 3, 2, 1 } }; + yield return new object[] { isPrepared, "CHAR(36)", new Guid("00112233-4455-6677-8899-AABBCCDDEEFF") }; + yield return new object[] { isPrepared, "FLOAT", 12.375f }; + yield return new object[] { isPrepared, "DOUBLE", 14.21875 }; + yield return new object[] { isPrepared, "DECIMAL(9,3)", 123.45m }; + yield return new object[] { isPrepared, "VARCHAR(100)", "test;@'; -- " }; + yield return new object[] { isPrepared, "TEXT", "testing testing" }; + yield return new object[] { isPrepared, "DATE", new DateTime(2018, 7, 23) }; + yield return new object[] { isPrepared, "DATETIME(3)", new DateTime(2018, 7, 23, 20, 46, 52, 123) }; + yield return new object[] { isPrepared, "ENUM('small', 'medium', 'large')", "medium" }; + yield return new object[] { isPrepared, "SET('one','two','four','eight')", "two,eight" }; + +#if !BASELINE + // https://bugs.mysql.com/bug.php?id=78917 + yield return new object[] { isPrepared, "BOOL", true }; + + // https://bugs.mysql.com/bug.php?id=91770 + yield return new object[] { isPrepared, "TIME(3)", TimeSpan.Zero.Subtract(new TimeSpan(15, 10, 34, 56, 789)) }; + + // https://bugs.mysql.com/bug.php?id=91751 + yield return new object[] { isPrepared, "YEAR", 2134 }; +#endif + + if (AppConfig.SupportsJson) + yield return new object[] { isPrepared, "JSON", "{\"test\": true}" }; + } + } + readonly DatabaseFixture m_database; } }