Skip to content

Commit 18ef235

Browse files
authored
Merge pull request #534 from bgrainger/prepared-commands
Implement first phase of prepared commands.
2 parents 9996ec1 + cee7719 commit 18ef235

30 files changed

+1662
-452
lines changed

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,8 @@ The following bugs in Connector/NET are fixed by switching to MySqlConnector.
128128
* [#89335](https://bugs.mysql.com/bug.php?id=89335): `MySqlCommandBuilder.DeriveParameters` fails for `JSON` type
129129
* [#91123](https://bugs.mysql.com/bug.php?id=91123): Database names are case-sensitive when calling a stored procedure
130130
* [#91199](https://bugs.mysql.com/bug.php?id=91199): Can't insert `MySqlDateTime` values
131+
* [#91751](https://bugs.mysql.com/bug.php?id=91751): `YEAR` column retrieved incorrectly with prepared command
132+
* [#91752](https://bugs.mysql.com/bug.php?id=91752): `00:00:00` is converted to `NULL` with prepared command
133+
* [#91753](https://bugs.mysql.com/bug.php?id=91753): Unnamed parameter not supported by `MySqlCommand.Prepare`
134+
* [#91754](https://bugs.mysql.com/bug.php?id=91754): Inserting 16MiB `BLOB` shifts it by four bytes when prepared
135+
* [#91770](https://bugs.mysql.com/bug.php?id=91770): `TIME(n)` column loses microseconds with prepared command

src/MySqlConnector/Core/BinaryRow.cs

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
using System;
2+
using System.Buffers.Text;
3+
using System.Runtime.InteropServices;
4+
using System.Text;
5+
using MySql.Data.MySqlClient;
6+
using MySql.Data.Types;
7+
using MySqlConnector.Protocol;
8+
using MySqlConnector.Protocol.Payloads;
9+
using MySqlConnector.Protocol.Serialization;
10+
using MySqlConnector.Utilities;
11+
12+
namespace MySqlConnector.Core
13+
{
14+
internal sealed class BinaryRow : Row
15+
{
16+
public BinaryRow(ResultSet resultSet)
17+
: base(resultSet)
18+
{
19+
}
20+
21+
protected override Row CloneCore() => new BinaryRow(ResultSet);
22+
23+
protected override void GetDataOffsets(ReadOnlySpan<byte> data, int[] dataOffsets, int[] dataLengths)
24+
{
25+
Array.Clear(dataOffsets, 0, dataOffsets.Length);
26+
for (var column = 0; column < dataOffsets.Length; column++)
27+
{
28+
if ((data[(column + 2) / 8 + 1] & (1 << ((column + 2) % 8))) != 0)
29+
{
30+
// column is NULL
31+
dataOffsets[column] = -1;
32+
}
33+
}
34+
35+
var reader = new ByteArrayReader(data);
36+
37+
// skip packet header (1 byte) and NULL bitmap (formula for length at https://dev.mysql.com/doc/internals/en/null-bitmap.html)
38+
reader.Offset += 1 + (dataOffsets.Length + 7 + 2) / 8;
39+
for (var column = 0; column < dataOffsets.Length; column++)
40+
{
41+
if (dataOffsets[column] == -1)
42+
{
43+
dataLengths[column] = 0;
44+
}
45+
else
46+
{
47+
var columnDefinition = ResultSet.ColumnDefinitions[column];
48+
int length;
49+
if (columnDefinition.ColumnType == ColumnType.Longlong || columnDefinition.ColumnType == ColumnType.Double)
50+
length = 8;
51+
else if (columnDefinition.ColumnType == ColumnType.Long || columnDefinition.ColumnType == ColumnType.Int24 || columnDefinition.ColumnType == ColumnType.Float)
52+
length = 4;
53+
else if (columnDefinition.ColumnType == ColumnType.Short || columnDefinition.ColumnType == ColumnType.Year)
54+
length = 2;
55+
else if (columnDefinition.ColumnType == ColumnType.Tiny)
56+
length = 1;
57+
else if (columnDefinition.ColumnType == ColumnType.Date || columnDefinition.ColumnType == ColumnType.DateTime || columnDefinition.ColumnType == ColumnType.Timestamp || columnDefinition.ColumnType == ColumnType.Time)
58+
length = reader.ReadByte();
59+
else if (columnDefinition.ColumnType == ColumnType.DateTime2 || columnDefinition.ColumnType == ColumnType.NewDate || columnDefinition.ColumnType == ColumnType.Timestamp2)
60+
throw new NotSupportedException("ColumnType {0} is not supported".FormatInvariant(columnDefinition.ColumnType));
61+
else
62+
length = checked((int) reader.ReadLengthEncodedInteger());
63+
64+
dataLengths[column] = length;
65+
dataOffsets[column] = reader.Offset;
66+
}
67+
68+
reader.Offset += dataLengths[column];
69+
}
70+
}
71+
72+
protected override object GetValueCore(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition)
73+
{
74+
var isUnsigned = (columnDefinition.ColumnFlags & ColumnFlags.Unsigned) != 0;
75+
switch (columnDefinition.ColumnType)
76+
{
77+
case ColumnType.Tiny:
78+
if (Connection.TreatTinyAsBoolean && columnDefinition.ColumnLength == 1 && !isUnsigned)
79+
return data[0] != 0;
80+
return isUnsigned ? (object) data[0] : (sbyte) data[0];
81+
82+
case ColumnType.Int24:
83+
case ColumnType.Long:
84+
return isUnsigned ? (object) MemoryMarshal.Read<uint>(data) : MemoryMarshal.Read<int>(data);
85+
86+
case ColumnType.Longlong:
87+
return isUnsigned ? (object) MemoryMarshal.Read<ulong>(data) : MemoryMarshal.Read<long>(data);
88+
89+
case ColumnType.Bit:
90+
// BIT column is transmitted as MSB byte array
91+
ulong bitValue = 0;
92+
for (int i = 0; i < data.Length; i++)
93+
bitValue = bitValue * 256 + data[i];
94+
return bitValue;
95+
96+
case ColumnType.String:
97+
if (Connection.GuidFormat == MySqlGuidFormat.Char36 && columnDefinition.ColumnLength / ProtocolUtility.GetBytesPerCharacter(columnDefinition.CharacterSet) == 36)
98+
return Utf8Parser.TryParse(data, out Guid guid, out int guid36BytesConsumed, 'D') && guid36BytesConsumed == 36 ? guid : throw new FormatException();
99+
if (Connection.GuidFormat == MySqlGuidFormat.Char32 && columnDefinition.ColumnLength / ProtocolUtility.GetBytesPerCharacter(columnDefinition.CharacterSet) == 32)
100+
return Utf8Parser.TryParse(data, out Guid guid, out int guid32BytesConsumed, 'N') && guid32BytesConsumed == 32 ? guid : throw new FormatException();
101+
goto case ColumnType.VarString;
102+
103+
case ColumnType.VarString:
104+
case ColumnType.VarChar:
105+
case ColumnType.TinyBlob:
106+
case ColumnType.Blob:
107+
case ColumnType.MediumBlob:
108+
case ColumnType.LongBlob:
109+
if (columnDefinition.CharacterSet == CharacterSet.Binary)
110+
{
111+
var guidFormat = Connection.GuidFormat;
112+
if ((guidFormat == MySqlGuidFormat.Binary16 || guidFormat == MySqlGuidFormat.TimeSwapBinary16 || guidFormat == MySqlGuidFormat.LittleEndianBinary16) && columnDefinition.ColumnLength == 16)
113+
return CreateGuidFromBytes(guidFormat, data);
114+
115+
return data.ToArray();
116+
}
117+
return Encoding.UTF8.GetString(data);
118+
119+
case ColumnType.Json:
120+
return Encoding.UTF8.GetString(data);
121+
122+
case ColumnType.Short:
123+
return isUnsigned ? (object) MemoryMarshal.Read<ushort>(data) : MemoryMarshal.Read<short>(data);
124+
125+
case ColumnType.Date:
126+
case ColumnType.DateTime:
127+
case ColumnType.Timestamp:
128+
return ParseDateTime(data);
129+
130+
case ColumnType.Time:
131+
return ParseTime(data);
132+
133+
case ColumnType.Year:
134+
return (int) MemoryMarshal.Read<short>(data);
135+
136+
case ColumnType.Float:
137+
return MemoryMarshal.Read<float>(data);
138+
139+
case ColumnType.Double:
140+
return MemoryMarshal.Read<double>(data);
141+
142+
case ColumnType.Decimal:
143+
case ColumnType.NewDecimal:
144+
return Utf8Parser.TryParse(data, out decimal decimalValue, out int bytesConsumed) && bytesConsumed == data.Length ? decimalValue : throw new FormatException();
145+
146+
case ColumnType.Geometry:
147+
return data.ToArray();
148+
149+
default:
150+
throw new NotImplementedException("Reading {0} not implemented".FormatInvariant(columnDefinition.ColumnType));
151+
}
152+
}
153+
154+
private object ParseDateTime(ReadOnlySpan<byte> value)
155+
{
156+
if (value.Length == 0)
157+
{
158+
if (Connection.ConvertZeroDateTime)
159+
return DateTime.MinValue;
160+
if (Connection.AllowZeroDateTime)
161+
return new MySqlDateTime();
162+
throw new InvalidCastException("Unable to convert MySQL date/time to System.DateTime.");
163+
}
164+
165+
int year = value[0] + value[1] * 256;
166+
int month = value[2];
167+
int day = value[3];
168+
169+
int hour, minute, second;
170+
if (value.Length <= 4)
171+
{
172+
hour = 0;
173+
minute = 0;
174+
second = 0;
175+
}
176+
else
177+
{
178+
hour = value[4];
179+
minute = value[5];
180+
second = value[6];
181+
}
182+
183+
var microseconds = value.Length <= 7 ? 0 : MemoryMarshal.Read<int>(value.Slice(7));
184+
185+
try
186+
{
187+
return Connection.AllowZeroDateTime ? (object) new MySqlDateTime(year, month, day, hour, minute, second, microseconds) :
188+
new DateTime(year, month, day, hour, minute, second, microseconds / 1000, Connection.DateTimeKind).AddTicks(microseconds % 1000 * 10);
189+
}
190+
catch (Exception ex)
191+
{
192+
throw new FormatException("Couldn't interpret value as a valid DateTime".FormatInvariant(Encoding.UTF8.GetString(value)), ex);
193+
}
194+
}
195+
196+
private object ParseTime(ReadOnlySpan<byte> value)
197+
{
198+
if (value.Length == 0)
199+
return TimeSpan.Zero;
200+
201+
var isNegative = value[0];
202+
var days = MemoryMarshal.Read<int>(value.Slice(1));
203+
var hours = (int) value[5];
204+
var minutes = (int) value[6];
205+
var seconds = (int) value[7];
206+
var microseconds = value.Length == 8 ? 0 : MemoryMarshal.Read<int>(value.Slice(8));
207+
208+
if (isNegative != 0)
209+
{
210+
days = -days;
211+
hours = -hours;
212+
minutes = -minutes;
213+
seconds = -seconds;
214+
microseconds = -microseconds;
215+
}
216+
217+
return new TimeSpan(days, hours, minutes, seconds) + TimeSpan.FromTicks(microseconds * 10);
218+
}
219+
}
220+
}

src/MySqlConnector/Core/ConnectionSettings.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ public ConnectionSettings(MySqlConnectionStringBuilder csb)
7070
DefaultCommandTimeout = (int) csb.DefaultCommandTimeout;
7171
ForceSynchronous = csb.ForceSynchronous;
7272
IgnoreCommandTransaction = csb.IgnoreCommandTransaction;
73+
IgnorePrepare = csb.IgnorePrepare;
7374
InteractiveSession = csb.InteractiveSession;
7475
GuidFormat = GetEffectiveGuidFormat(csb.GuidFormat, csb.OldGuids);
7576
Keepalive = csb.Keepalive;
@@ -145,6 +146,7 @@ private static MySqlGuidFormat GetEffectiveGuidFormat(MySqlGuidFormat guidFormat
145146
public bool ForceSynchronous { get; }
146147
public MySqlGuidFormat GuidFormat { get; }
147148
public bool IgnoreCommandTransaction { get; }
149+
public bool IgnorePrepare { get; }
148150
public bool InteractiveSession { get; }
149151
public uint Keepalive { get; }
150152
public bool PersistSecurityInfo { get; }

src/MySqlConnector/Core/ICommandExecutor.cs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,6 @@ namespace MySqlConnector.Core
99
{
1010
internal interface ICommandExecutor
1111
{
12-
Task<int> ExecuteNonQueryAsync(string commandText, MySqlParameterCollection parameterCollection, IOBehavior ioBehavior, CancellationToken cancellationToken);
13-
14-
Task<object> ExecuteScalarAsync(string commandText, MySqlParameterCollection parameterCollection, IOBehavior ioBehavior, CancellationToken cancellationToken);
15-
1612
Task<DbDataReader> ExecuteReaderAsync(string commandText, MySqlParameterCollection parameterCollection, CommandBehavior behavior, IOBehavior ioBehavior, CancellationToken cancellationToken);
1713
}
1814
}
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+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using MySqlConnector.Protocol.Payloads;
2+
3+
namespace MySqlConnector.Core
4+
{
5+
/// <summary>
6+
/// <see cref="PreparedStatement"/> is a statement that has been prepared on the MySQL Server.
7+
/// </summary>
8+
internal sealed class PreparedStatement
9+
{
10+
public PreparedStatement(int statementId, ParsedStatement statement, ColumnDefinitionPayload[] columns, ColumnDefinitionPayload[] parameters)
11+
{
12+
StatementId = statementId;
13+
Statement = statement;
14+
Columns = columns;
15+
Parameters = parameters;
16+
}
17+
18+
public int StatementId { get; }
19+
public ParsedStatement Statement { get; }
20+
public ColumnDefinitionPayload[] Columns { get; }
21+
public ColumnDefinitionPayload[] Parameters { get; }
22+
}
23+
}

0 commit comments

Comments
 (0)