Skip to content

Commit 876514b

Browse files
committed
Allow GetDateTime to read a string as a DateTime. Fixes #980
1 parent d465650 commit 876514b

File tree

3 files changed

+156
-78
lines changed

3 files changed

+156
-78
lines changed

src/MySqlConnector/Core/Row.cs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Buffers.Text;
33
using System.IO;
44
using System.Runtime.InteropServices;
5+
using System.Text;
56
using MySqlConnector.Protocol;
67
using MySqlConnector.Protocol.Payloads;
78
using MySqlConnector.Utilities;
@@ -353,6 +354,16 @@ public ulong GetUInt64(int ordinal)
353354
public DateTime GetDateTime(int ordinal)
354355
{
355356
var value = GetValue(ordinal);
357+
358+
if (value is string dateString)
359+
{
360+
// slightly inefficient to roundtrip the bytes through a string, but this is assumed to be an infrequent code path; this could be optimised to reprocess the original bytes
361+
if (dateString.Length is >= 10 and <= 26)
362+
value = ParseDateTime(Encoding.UTF8.GetBytes(dateString));
363+
else
364+
throw new FormatException("Couldn't interpret '{0}' as a valid DateTime".FormatInvariant(value));
365+
}
366+
356367
if (value is MySqlDateTime mySqlDateTime)
357368
return mySqlDateTime.GetDateTime();
358369
return (DateTime) value;
@@ -462,6 +473,82 @@ protected Row(ResultSet resultSet)
462473
protected ResultSet ResultSet { get; }
463474
protected MySqlConnection Connection => ResultSet.Connection;
464475

476+
protected object ParseDateTime(ReadOnlySpan<byte> value)
477+
{
478+
Exception? exception = null;
479+
if (!Utf8Parser.TryParse(value, out int year, out var bytesConsumed) || bytesConsumed != 4)
480+
goto InvalidDateTime;
481+
if (value.Length < 5 || value[4] != 45)
482+
goto InvalidDateTime;
483+
if (!Utf8Parser.TryParse(value.Slice(5), out int month, out bytesConsumed) || bytesConsumed != 2)
484+
goto InvalidDateTime;
485+
if (value.Length < 8 || value[7] != 45)
486+
goto InvalidDateTime;
487+
if (!Utf8Parser.TryParse(value.Slice(8), out int day, out bytesConsumed) || bytesConsumed != 2)
488+
goto InvalidDateTime;
489+
490+
if (year == 0 && month == 0 && day == 0)
491+
{
492+
if (Connection.ConvertZeroDateTime)
493+
return DateTime.MinValue;
494+
if (Connection.AllowZeroDateTime)
495+
return new MySqlDateTime();
496+
throw new InvalidCastException("Unable to convert MySQL date/time to System.DateTime, set AllowZeroDateTime=True or ConvertZeroDateTime=True in the connection string. See https://mysqlconnector.net/connection-options/");
497+
}
498+
499+
int hour, minute, second, microseconds;
500+
if (value.Length == 10)
501+
{
502+
hour = 0;
503+
minute = 0;
504+
second = 0;
505+
microseconds = 0;
506+
}
507+
else
508+
{
509+
if (value[10] != 32)
510+
goto InvalidDateTime;
511+
if (!Utf8Parser.TryParse(value.Slice(11), out hour, out bytesConsumed) || bytesConsumed != 2)
512+
goto InvalidDateTime;
513+
if (value.Length < 14 || value[13] != 58)
514+
goto InvalidDateTime;
515+
if (!Utf8Parser.TryParse(value.Slice(14), out minute, out bytesConsumed) || bytesConsumed != 2)
516+
goto InvalidDateTime;
517+
if (value.Length < 17 || value[16] != 58)
518+
goto InvalidDateTime;
519+
if (!Utf8Parser.TryParse(value.Slice(17), out second, out bytesConsumed) || bytesConsumed != 2)
520+
goto InvalidDateTime;
521+
522+
if (value.Length == 19)
523+
{
524+
microseconds = 0;
525+
}
526+
else
527+
{
528+
if (value[19] != 46)
529+
goto InvalidDateTime;
530+
531+
if (!Utf8Parser.TryParse(value.Slice(20), out microseconds, out bytesConsumed) || bytesConsumed != value.Length - 20)
532+
goto InvalidDateTime;
533+
for (; bytesConsumed < 6; bytesConsumed++)
534+
microseconds *= 10;
535+
}
536+
}
537+
538+
try
539+
{
540+
return Connection.AllowZeroDateTime ? (object) new MySqlDateTime(year, month, day, hour, minute, second, microseconds) :
541+
new DateTime(year, month, day, hour, minute, second, microseconds / 1000, Connection.DateTimeKind).AddTicks(microseconds % 1000 * 10);
542+
}
543+
catch (Exception ex)
544+
{
545+
exception = ex;
546+
}
547+
548+
InvalidDateTime:
549+
throw new FormatException("Couldn't interpret '{0}' as a valid DateTime".FormatInvariant(Encoding.UTF8.GetString(value)), exception);
550+
}
551+
465552
protected unsafe static Guid CreateGuidFromBytes(MySqlGuidFormat guidFormat, ReadOnlySpan<byte> bytes) =>
466553
guidFormat switch
467554
{

src/MySqlConnector/Core/TextRow.cs

Lines changed: 0 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -128,81 +128,5 @@ private static uint ParseUInt32(ReadOnlySpan<byte> data) =>
128128

129129
private static long ParseInt64(ReadOnlySpan<byte> data) =>
130130
!Utf8Parser.TryParse(data, out long value, out var bytesConsumed) || bytesConsumed != data.Length ? throw new FormatException() : value;
131-
132-
private object ParseDateTime(ReadOnlySpan<byte> value)
133-
{
134-
Exception? exception = null;
135-
if (!Utf8Parser.TryParse(value, out int year, out var bytesConsumed) || bytesConsumed != 4)
136-
goto InvalidDateTime;
137-
if (value.Length < 5 || value[4] != 45)
138-
goto InvalidDateTime;
139-
if (!Utf8Parser.TryParse(value.Slice(5), out int month, out bytesConsumed) || bytesConsumed != 2)
140-
goto InvalidDateTime;
141-
if (value.Length < 8 || value[7] != 45)
142-
goto InvalidDateTime;
143-
if (!Utf8Parser.TryParse(value.Slice(8), out int day, out bytesConsumed) || bytesConsumed != 2)
144-
goto InvalidDateTime;
145-
146-
if (year == 0 && month == 0 && day == 0)
147-
{
148-
if (Connection.ConvertZeroDateTime)
149-
return DateTime.MinValue;
150-
if (Connection.AllowZeroDateTime)
151-
return new MySqlDateTime();
152-
throw new InvalidCastException("Unable to convert MySQL date/time to System.DateTime, set AllowZeroDateTime=True or ConvertZeroDateTime=True in the connection string. See https://mysqlconnector.net/connection-options/");
153-
}
154-
155-
int hour, minute, second, microseconds;
156-
if (value.Length == 10)
157-
{
158-
hour = 0;
159-
minute = 0;
160-
second = 0;
161-
microseconds = 0;
162-
}
163-
else
164-
{
165-
if (value[10] != 32)
166-
goto InvalidDateTime;
167-
if (!Utf8Parser.TryParse(value.Slice(11), out hour, out bytesConsumed) || bytesConsumed != 2)
168-
goto InvalidDateTime;
169-
if (value.Length < 14 || value[13] != 58)
170-
goto InvalidDateTime;
171-
if (!Utf8Parser.TryParse(value.Slice(14), out minute, out bytesConsumed) || bytesConsumed != 2)
172-
goto InvalidDateTime;
173-
if (value.Length < 17 || value[16] != 58)
174-
goto InvalidDateTime;
175-
if (!Utf8Parser.TryParse(value.Slice(17), out second, out bytesConsumed) || bytesConsumed != 2)
176-
goto InvalidDateTime;
177-
178-
if (value.Length == 19)
179-
{
180-
microseconds = 0;
181-
}
182-
else
183-
{
184-
if (value[19] != 46)
185-
goto InvalidDateTime;
186-
187-
if (!Utf8Parser.TryParse(value.Slice(20), out microseconds, out bytesConsumed) || bytesConsumed != value.Length - 20)
188-
goto InvalidDateTime;
189-
for (; bytesConsumed < 6; bytesConsumed++)
190-
microseconds *= 10;
191-
}
192-
}
193-
194-
try
195-
{
196-
return Connection.AllowZeroDateTime ? (object) new MySqlDateTime(year, month, day, hour, minute, second, microseconds) :
197-
new DateTime(year, month, day, hour, minute, second, microseconds / 1000, Connection.DateTimeKind).AddTicks(microseconds % 1000 * 10);
198-
}
199-
catch (Exception ex)
200-
{
201-
exception = ex;
202-
}
203-
204-
InvalidDateTime:
205-
throw new FormatException("Couldn't interpret '{0}' as a valid DateTime".FormatInvariant(Encoding.UTF8.GetString(value)), exception);
206-
}
207131
}
208132
}

tests/SideBySide/DataTypes.cs

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -984,6 +984,74 @@ public void ReadNewDate(bool prepare)
984984
}
985985
}
986986

987+
[Theory]
988+
[InlineData("Date", false, "9999 12 31")]
989+
[InlineData("Date", true, "9999 12 31")]
990+
[InlineData("DateTime", false, "9999 12 31 23 59 59 999999")]
991+
[InlineData("DateTime", true, "9999 12 31 23 59 59 999999")]
992+
[InlineData("Time", false, null)]
993+
[InlineData("Time", true, null)]
994+
public void ReadVarCharFromNestedQueryAsDate(string columnName, bool prepare, string expectedValue)
995+
{
996+
var expectedDate = (DateTime?) ConvertToDateTime(new object[] { expectedValue }, DateTimeKind.Unspecified)[0];
997+
998+
// returns VARCHAR in MySQL 5.7; DATE in MySQL 8.0
999+
using var cmd = new MySqlCommand($@"SELECT MAX(CASE WHEN 1 = t.`Key` THEN t.`{columnName}` END) AS `Max`
1000+
FROM (SELECT `{columnName}`, 1 AS `Key` FROM datatypes_times) t
1001+
GROUP BY t.`Key`
1002+
ORDER BY t.`Key`", Connection);
1003+
if (prepare)
1004+
cmd.Prepare();
1005+
1006+
using var reader = cmd.ExecuteReader();
1007+
Assert.True(reader.Read());
1008+
if (expectedDate.HasValue)
1009+
Assert.Equal(expectedDate.Value, reader.GetDateTime(0));
1010+
else
1011+
Assert.ThrowsAny<Exception>(() => reader.GetDateTime(0));
1012+
}
1013+
1014+
[Theory]
1015+
#if !BASELINE
1016+
[InlineData("1001-02", false, null)]
1017+
[InlineData("1001-02", true, null)]
1018+
[InlineData("2000-01-02 03-04-05", true, null)]
1019+
[InlineData("2000-01-02 18:19:20.9876543", false, null)]
1020+
[InlineData("2000-01-02 18:19:20.9876543", true, null)]
1021+
[InlineData("2000-01-02 03-04-05 123456", false, null)]
1022+
#endif
1023+
[InlineData("1001-02-03", false, "1001 2 3")]
1024+
[InlineData("1001-02-0A", true, null)]
1025+
[InlineData("2000-01-02 03:04:05", false, "2000 1 2 3 4 5")]
1026+
[InlineData("2000-01-02T03:04:05", false, null)]
1027+
[InlineData("2000-01-02 2003-04-05", true, null)]
1028+
[InlineData("2000-01-02 18:19:20.9", true, "2000 1 2 18 19 20 900000")]
1029+
[InlineData("2000-01-02 18:19:20.98", false, "2000 1 2 18 19 20 980000")]
1030+
[InlineData("2000-01-02 18:19:20.987", true, "2000 1 2 18 19 20 987000")]
1031+
[InlineData("2000-01-02 18:19:20.9876", false, "2000 1 2 18 19 20 987600")]
1032+
[InlineData("2000-01-02 18:19:20.98765", true, "2000 1 2 18 19 20 987650")]
1033+
[InlineData("2000-01-02 18:19:20.987654", false, "2000 1 2 18 19 20 987654")]
1034+
public void ReadVarCharAsDate(string value, bool prepare, string expectedValue)
1035+
{
1036+
var expectedDate = (DateTime?) ConvertToDateTime(new object[] { expectedValue }, DateTimeKind.Unspecified)[0];
1037+
1038+
// returns VARCHAR in MySQL 5.7; DATE in MySQL 8.0
1039+
using var cmd = new MySqlCommand($@"SELECT '{value}' AS value", Connection);
1040+
if (prepare)
1041+
cmd.Prepare();
1042+
1043+
using var reader = cmd.ExecuteReader();
1044+
Assert.True(reader.Read());
1045+
if (expectedDate.HasValue)
1046+
Assert.Equal(expectedDate.Value, reader.GetDateTime(0));
1047+
else
1048+
#if BASELINE
1049+
Assert.ThrowsAny<Exception>(() => reader.GetDateTime(0));
1050+
#else
1051+
Assert.Throws<FormatException>(() => reader.GetDateTime(0));
1052+
#endif
1053+
}
1054+
9871055
[Theory]
9881056
[InlineData("Geometry", "GEOMETRY", new byte[] { 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 240, 63, 0, 0, 0, 0, 0, 0, 240, 63 })]
9891057
[InlineData("Point", "GEOMETRY", new byte[] { 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 240, 63, 0, 0, 0, 0, 0, 0, 240, 63 })]
@@ -1717,8 +1785,7 @@ private static object[] ConvertToTimeSpan(object[] input)
17171785

17181786
private static int[] SplitAndParse(object obj)
17191787
{
1720-
var value = obj as string;
1721-
if (value is null)
1788+
if (obj is not string value)
17221789
return null;
17231790

17241791
var split = value.Split();

0 commit comments

Comments
 (0)