Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement prepared commands #534

Merged
merged 11 commits into from Jul 24, 2018
5 changes: 5 additions & 0 deletions docs/content/tutorials/migrating-from-connector-net.md
Expand Up @@ -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
220 changes: 220 additions & 0 deletions src/MySqlConnector/Core/BinaryRow.cs
@@ -0,0 +1,220 @@
using System;
using System.Buffers.Text;
using System.Runtime.InteropServices;
using System.Text;
using MySql.Data.MySqlClient;
using MySql.Data.Types;
using MySqlConnector.Protocol;
using MySqlConnector.Protocol.Payloads;
using MySqlConnector.Protocol.Serialization;
using MySqlConnector.Utilities;

namespace MySqlConnector.Core
{
internal sealed class BinaryRow : Row
{
public BinaryRow(ResultSet resultSet)
: base(resultSet)
{
}

protected override Row CloneCore() => new BinaryRow(ResultSet);

protected override void GetDataOffsets(ReadOnlySpan<byte> data, int[] dataOffsets, int[] dataLengths)
{
Array.Clear(dataOffsets, 0, dataOffsets.Length);
for (var column = 0; column < dataOffsets.Length; column++)
{
if ((data[(column + 2) / 8 + 1] & (1 << ((column + 2) % 8))) != 0)
{
// column is NULL
dataOffsets[column] = -1;
}
}

var reader = new ByteArrayReader(data);

// skip packet header (1 byte) and NULL bitmap (formula for length at https://dev.mysql.com/doc/internals/en/null-bitmap.html)
reader.Offset += 1 + (dataOffsets.Length + 7 + 2) / 8;
for (var column = 0; column < dataOffsets.Length; column++)
{
if (dataOffsets[column] == -1)
{
dataLengths[column] = 0;
}
else
{
var columnDefinition = ResultSet.ColumnDefinitions[column];
int length;
if (columnDefinition.ColumnType == ColumnType.Longlong || columnDefinition.ColumnType == ColumnType.Double)
length = 8;
else if (columnDefinition.ColumnType == ColumnType.Long || columnDefinition.ColumnType == ColumnType.Int24 || columnDefinition.ColumnType == ColumnType.Float)
length = 4;
else if (columnDefinition.ColumnType == ColumnType.Short || columnDefinition.ColumnType == ColumnType.Year)
length = 2;
else if (columnDefinition.ColumnType == ColumnType.Tiny)
length = 1;
else if (columnDefinition.ColumnType == ColumnType.Date || columnDefinition.ColumnType == ColumnType.DateTime || columnDefinition.ColumnType == ColumnType.Timestamp || columnDefinition.ColumnType == ColumnType.Time)
length = reader.ReadByte();
else if (columnDefinition.ColumnType == ColumnType.DateTime2 || columnDefinition.ColumnType == ColumnType.NewDate || columnDefinition.ColumnType == ColumnType.Timestamp2)
throw new NotSupportedException("ColumnType {0} is not supported".FormatInvariant(columnDefinition.ColumnType));
else
length = checked((int) reader.ReadLengthEncodedInteger());

dataLengths[column] = length;
dataOffsets[column] = reader.Offset;
}

reader.Offset += dataLengths[column];
}
}

protected override object GetValueCore(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition)
{
var isUnsigned = (columnDefinition.ColumnFlags & ColumnFlags.Unsigned) != 0;
switch (columnDefinition.ColumnType)
{
case ColumnType.Tiny:
if (Connection.TreatTinyAsBoolean && columnDefinition.ColumnLength == 1 && !isUnsigned)
return data[0] != 0;
return isUnsigned ? (object) data[0] : (sbyte) data[0];

case ColumnType.Int24:
case ColumnType.Long:
return isUnsigned ? (object) MemoryMarshal.Read<uint>(data) : MemoryMarshal.Read<int>(data);

case ColumnType.Longlong:
return isUnsigned ? (object) MemoryMarshal.Read<ulong>(data) : MemoryMarshal.Read<long>(data);

case ColumnType.Bit:
// BIT column is transmitted as MSB byte array
ulong bitValue = 0;
for (int i = 0; i < data.Length; i++)
bitValue = bitValue * 256 + data[i];
return bitValue;

case ColumnType.String:
if (Connection.GuidFormat == MySqlGuidFormat.Char36 && columnDefinition.ColumnLength / ProtocolUtility.GetBytesPerCharacter(columnDefinition.CharacterSet) == 36)
return Utf8Parser.TryParse(data, out Guid guid, out int guid36BytesConsumed, 'D') && guid36BytesConsumed == 36 ? guid : throw new FormatException();
if (Connection.GuidFormat == MySqlGuidFormat.Char32 && columnDefinition.ColumnLength / ProtocolUtility.GetBytesPerCharacter(columnDefinition.CharacterSet) == 32)
return Utf8Parser.TryParse(data, out Guid guid, out int guid32BytesConsumed, 'N') && guid32BytesConsumed == 32 ? guid : throw new FormatException();
goto case ColumnType.VarString;

case ColumnType.VarString:
case ColumnType.VarChar:
case ColumnType.TinyBlob:
case ColumnType.Blob:
case ColumnType.MediumBlob:
case ColumnType.LongBlob:
if (columnDefinition.CharacterSet == CharacterSet.Binary)
{
var guidFormat = Connection.GuidFormat;
if ((guidFormat == MySqlGuidFormat.Binary16 || guidFormat == MySqlGuidFormat.TimeSwapBinary16 || guidFormat == MySqlGuidFormat.LittleEndianBinary16) && columnDefinition.ColumnLength == 16)
return CreateGuidFromBytes(guidFormat, data);

return data.ToArray();
}
return Encoding.UTF8.GetString(data);

case ColumnType.Json:
return Encoding.UTF8.GetString(data);

case ColumnType.Short:
return isUnsigned ? (object) MemoryMarshal.Read<ushort>(data) : MemoryMarshal.Read<short>(data);

case ColumnType.Date:
case ColumnType.DateTime:
case ColumnType.Timestamp:
return ParseDateTime(data);

case ColumnType.Time:
return ParseTime(data);

case ColumnType.Year:
return (int) MemoryMarshal.Read<short>(data);

case ColumnType.Float:
return MemoryMarshal.Read<float>(data);

case ColumnType.Double:
return MemoryMarshal.Read<double>(data);

case ColumnType.Decimal:
case ColumnType.NewDecimal:
return Utf8Parser.TryParse(data, out decimal decimalValue, out int bytesConsumed) && bytesConsumed == data.Length ? decimalValue : throw new FormatException();

case ColumnType.Geometry:
return data.ToArray();

default:
throw new NotImplementedException("Reading {0} not implemented".FormatInvariant(columnDefinition.ColumnType));
}
}

private object ParseDateTime(ReadOnlySpan<byte> value)
{
if (value.Length == 0)
{
if (Connection.ConvertZeroDateTime)
return DateTime.MinValue;
if (Connection.AllowZeroDateTime)
return new MySqlDateTime();
throw new InvalidCastException("Unable to convert MySQL date/time to System.DateTime.");
}

int year = value[0] + value[1] * 256;
int month = value[2];
int day = value[3];

int hour, minute, second;
if (value.Length <= 4)
{
hour = 0;
minute = 0;
second = 0;
}
else
{
hour = value[4];
minute = value[5];
second = value[6];
}

var microseconds = value.Length <= 7 ? 0 : MemoryMarshal.Read<int>(value.Slice(7));

try
{
return Connection.AllowZeroDateTime ? (object) new MySqlDateTime(year, month, day, hour, minute, second, microseconds) :
new DateTime(year, month, day, hour, minute, second, microseconds / 1000, Connection.DateTimeKind).AddTicks(microseconds % 1000 * 10);
}
catch (Exception ex)
{
throw new FormatException("Couldn't interpret value as a valid DateTime".FormatInvariant(Encoding.UTF8.GetString(value)), ex);
}
}

private object ParseTime(ReadOnlySpan<byte> value)
{
if (value.Length == 0)
return TimeSpan.Zero;

var isNegative = value[0];
var days = MemoryMarshal.Read<int>(value.Slice(1));
var hours = (int) value[5];
var minutes = (int) value[6];
var seconds = (int) value[7];
var microseconds = value.Length == 8 ? 0 : MemoryMarshal.Read<int>(value.Slice(8));

if (isNegative != 0)
{
days = -days;
hours = -hours;
minutes = -minutes;
seconds = -seconds;
microseconds = -microseconds;
}

return new TimeSpan(days, hours, minutes, seconds) + TimeSpan.FromTicks(microseconds * 10);
}
}
}
2 changes: 2 additions & 0 deletions src/MySqlConnector/Core/ConnectionSettings.cs
Expand Up @@ -70,6 +70,7 @@ public ConnectionSettings(MySqlConnectionStringBuilder csb)
DefaultCommandTimeout = (int) csb.DefaultCommandTimeout;
ForceSynchronous = csb.ForceSynchronous;
IgnoreCommandTransaction = csb.IgnoreCommandTransaction;
IgnorePrepare = csb.IgnorePrepare;
InteractiveSession = csb.InteractiveSession;
GuidFormat = GetEffectiveGuidFormat(csb.GuidFormat, csb.OldGuids);
Keepalive = csb.Keepalive;
Expand Down Expand Up @@ -145,6 +146,7 @@ private static MySqlGuidFormat GetEffectiveGuidFormat(MySqlGuidFormat guidFormat
public bool ForceSynchronous { get; }
public MySqlGuidFormat GuidFormat { get; }
public bool IgnoreCommandTransaction { get; }
public bool IgnorePrepare { get; }
public bool InteractiveSession { get; }
public uint Keepalive { get; }
public bool PersistSecurityInfo { get; }
Expand Down
4 changes: 0 additions & 4 deletions src/MySqlConnector/Core/ICommandExecutor.cs
Expand Up @@ -9,10 +9,6 @@ namespace MySqlConnector.Core
{
internal interface ICommandExecutor
{
Task<int> ExecuteNonQueryAsync(string commandText, MySqlParameterCollection parameterCollection, IOBehavior ioBehavior, CancellationToken cancellationToken);

Task<object> ExecuteScalarAsync(string commandText, MySqlParameterCollection parameterCollection, IOBehavior ioBehavior, CancellationToken cancellationToken);

Task<DbDataReader> ExecuteReaderAsync(string commandText, MySqlParameterCollection parameterCollection, CommandBehavior behavior, IOBehavior ioBehavior, CancellationToken cancellationToken);
}
}
29 changes: 29 additions & 0 deletions src/MySqlConnector/Core/ParsedStatement.cs
@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;

namespace MySqlConnector.Core
{
/// <summary>
/// <see cref="ParsedStatement"/> represents an individual SQL statement that's been parsed
/// from a string possibly containing multiple semicolon-delimited SQL statements.
/// </summary>
internal sealed class ParsedStatement
{
/// <summary>
/// The bytes for this statement that will be written on the wire.
/// </summary>
public ArraySegment<byte> StatementBytes { get; set; }

/// <summary>
/// The names of the parameters (if known) of the parameters in the prepared statement. There
/// is one entry in this list for each parameter, which will be <c>null</c> if the name is unknown.
/// </summary>
public List<string> ParameterNames { get; } = new List<string>();

/// <summary>
/// The indexes of the parameters in the prepared statement. There is one entry in this list for
/// each parameter; it will be <c>-1</c> if the parameter is named.
/// </summary>
public List<int> ParameterIndexes { get; }= new List<int>();
}
}
31 changes: 31 additions & 0 deletions src/MySqlConnector/Core/ParsedStatements.cs
@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using MySqlConnector.Protocol;

namespace MySqlConnector.Core
{
/// <summary>
/// <see cref="ParsedStatements"/> wraps a collection of <see cref="ParsedStatement"/> objects.
/// It implements <see cref="IDisposable"/> to return the memory backing the statements to a shared pool.
/// </summary>
internal sealed class ParsedStatements : IDisposable
{
public IReadOnlyList<ParsedStatement> Statements => m_statements;

public void Dispose()
{
m_statements.Clear();
m_payloadData.Dispose();
m_payloadData = default;
}

internal ParsedStatements(List<ParsedStatement> statements, PayloadData payloadData)
{
m_statements = statements;
m_payloadData = payloadData;
}

readonly List<ParsedStatement> m_statements;
PayloadData m_payloadData;
}
}
23 changes: 23 additions & 0 deletions src/MySqlConnector/Core/PreparedStatement.cs
@@ -0,0 +1,23 @@
using MySqlConnector.Protocol.Payloads;

namespace MySqlConnector.Core
{
/// <summary>
/// <see cref="PreparedStatement"/> is a statement that has been prepared on the MySQL Server.
/// </summary>
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; }
}
}