Skip to content

Commit 837e103

Browse files
committed
Add deserialization of binary protocol results.
Signed-off-by: Bradley Grainger <[email protected]>
1 parent 641e44d commit 837e103

File tree

5 files changed

+235
-5
lines changed

5 files changed

+235
-5
lines changed

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/ResultSet.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ Row ScanRowAsyncRemainder(PayloadData payload, Row row_)
242242
}
243243

244244
if (row_ == null)
245-
row_ = new TextRow(this);
245+
row_ = DataReader.ResultSetProtocol == ResultSetProtocol.Binary ? (Row) new BinaryRow(this) : new TextRow(this);
246246
row_.SetData(payload.ArraySegment);
247247
m_rowBuffered = row_;
248248
m_hasRows = true;

src/MySqlConnector/Core/TextCommandExecutor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public virtual async Task<DbDataReader> ExecuteReaderAsync(string commandText, M
3434
try
3535
{
3636
await m_command.Connection.Session.SendAsync(payload, ioBehavior, CancellationToken.None).ConfigureAwait(false);
37-
return await MySqlDataReader.CreateAsync(m_command, behavior, ioBehavior).ConfigureAwait(false);
37+
return await MySqlDataReader.CreateAsync(m_command, behavior, ResultSetProtocol.Text, ioBehavior).ConfigureAwait(false);
3838
}
3939
catch (MySqlException ex) when (ex.Number == (int) MySqlErrorCode.QueryInterrupted && cancellationToken.IsCancellationRequested)
4040
{

src/MySqlConnector/MySql.Data.MySqlClient/MySqlDataReader.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -270,12 +270,13 @@ protected override void Dispose(bool disposing)
270270
}
271271

272272
internal MySqlCommand Command { get; private set; }
273+
internal ResultSetProtocol ResultSetProtocol { get; }
273274
internal MySqlConnection Connection => Command?.Connection;
274275
internal ServerSession Session => Command?.Connection.Session;
275276

276-
internal static async Task<MySqlDataReader> CreateAsync(MySqlCommand command, CommandBehavior behavior, IOBehavior ioBehavior)
277+
internal static async Task<MySqlDataReader> CreateAsync(MySqlCommand command, CommandBehavior behavior, ResultSetProtocol resultSetProtocol, IOBehavior ioBehavior)
277278
{
278-
var dataReader = new MySqlDataReader(command, behavior);
279+
var dataReader = new MySqlDataReader(command, resultSetProtocol, behavior);
279280
command.Connection.SetActiveReader(dataReader);
280281

281282
try
@@ -389,9 +390,10 @@ internal DataTable BuildSchemaTable()
389390
}
390391
#endif
391392

392-
private MySqlDataReader(MySqlCommand command, CommandBehavior behavior)
393+
private MySqlDataReader(MySqlCommand command, ResultSetProtocol resultSetProtocol, CommandBehavior behavior)
393394
{
394395
Command = command;
396+
ResultSetProtocol = resultSetProtocol;
395397
m_behavior = behavior;
396398
}
397399

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace MySqlConnector.Protocol.Serialization
2+
{
3+
internal enum ResultSetProtocol
4+
{
5+
Text,
6+
Binary,
7+
}
8+
}

0 commit comments

Comments
 (0)