Skip to content

Commit cfeb20b

Browse files
committed
Support DateOnly and TimeOnly. Fixes #963
1 parent b176fac commit cfeb20b

File tree

6 files changed

+137
-8
lines changed

6 files changed

+137
-8
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,3 +282,4 @@ The following bugs in Connector/NET are fixed by switching to MySqlConnector. (~
282282
* ~~[#101714](https://bugs.mysql.com/bug.php?id=101714): Extremely slow performance reading result sets~~
283283
* [#102593](https://bugs.mysql.com/bug.php?id=102593): Can't use `MemoryStream` as `MySqlParameter.Value`
284284
* [#103390](https://bugs.mysql.com/bug.php?id=103390): Can't query `CHAR(36)` column if `MySqlCommand` is prepared
285+
* [#103801](https://bugs.mysql.com/bug.php?id=103801): `TimeSpan` parameters lose microseconds with prepared statement

src/MySqlConnector/Core/TypeMapper.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,15 @@ private TypeMapper()
9292
AddColumnTypeMetadata(new("GEOMCOLLECTION", typeBinary, MySqlDbType.Geometry, binary: true));
9393

9494
// date/time
95+
#if NET6_0_OR_GREATER
96+
AddDbTypeMapping(new(typeof(DateOnly), new[] { DbType.Date }));
97+
#endif
9598
var typeDate = AddDbTypeMapping(new(typeof(DateTime), new[] { DbType.Date }));
9699
var typeDateTime = AddDbTypeMapping(new(typeof(DateTime), new[] { DbType.DateTime, DbType.DateTime2, DbType.DateTimeOffset }));
97100
AddDbTypeMapping(new(typeof(DateTimeOffset), new[] { DbType.DateTimeOffset }));
101+
#if NET6_0_OR_GREATER
102+
AddDbTypeMapping(new(typeof(TimeOnly), new[] { DbType.Time }));
103+
#endif
98104
var typeTime = AddDbTypeMapping(new(typeof(TimeSpan), new[] { DbType.Time }, convert: static o => o is string s ? Utility.ParseTimeSpan(Encoding.UTF8.GetBytes(s)) : Convert.ChangeType(o, typeof(TimeSpan))));
99105
AddColumnTypeMetadata(new("DATETIME", typeDateTime, MySqlDbType.DateTime));
100106
AddColumnTypeMetadata(new("DATE", typeDate, MySqlDbType.Date));

src/MySqlConnector/MySqlDataReader.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,11 @@ public override long GetChars(int ordinal, long dataOffset, char[]? buffer, int
258258

259259
protected override DbDataReader GetDbDataReader(int ordinal) => throw new NotSupportedException();
260260

261+
#if NET6_0_OR_GREATER
262+
public DateOnly GetDateOnly(int ordinal) => DateOnly.FromDateTime(GetDateTime(ordinal));
263+
public DateOnly GetDateOnly(string name) => GetDateOnly(GetOrdinal(name));
264+
#endif
265+
261266
public override DateTime GetDateTime(int ordinal) => GetResultSet().GetCurrentRow().GetDateTime(ordinal);
262267
public DateTime GetDateTime(string name) => GetDateTime(GetOrdinal(name));
263268

@@ -270,6 +275,11 @@ public override long GetChars(int ordinal, long dataOffset, char[]? buffer, int
270275
public MySqlGeometry GetMySqlGeometry(int ordinal) => GetResultSet().GetCurrentRow().GetMySqlGeometry(ordinal);
271276
public MySqlGeometry GetMySqlGeometry(string name) => GetMySqlGeometry(GetOrdinal(name));
272277

278+
#if NET6_0_OR_GREATER
279+
public TimeOnly GetTimeOnly(int ordinal) => TimeOnly.FromTimeSpan(GetTimeSpan(ordinal));
280+
public TimeOnly GetTimeOnly(string name) => GetTimeOnly(GetOrdinal(name));
281+
#endif
282+
273283
public TimeSpan GetTimeSpan(int ordinal) => (TimeSpan) GetValue(ordinal);
274284
public TimeSpan GetTimeSpan(string name) => GetTimeSpan(GetOrdinal(name));
275285

@@ -404,6 +414,12 @@ public override T GetFieldValue<T>(int ordinal)
404414
return (T) (object) GetTextReader(ordinal);
405415
if (typeof(T) == typeof(TimeSpan))
406416
return (T) (object) GetTimeSpan(ordinal);
417+
#if NET6_0_OR_GREATER
418+
if (typeof(T) == typeof(DateOnly))
419+
return (T) (object) GetDateOnly(ordinal);
420+
if (typeof(T) == typeof(TimeOnly))
421+
return (T) (object) GetTimeOnly(ordinal);
422+
#endif
407423

408424
return base.GetFieldValue<T>(ordinal);
409425
}

src/MySqlConnector/MySqlParameter.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,12 @@ internal void AppendSqlString(ByteBufferWriter writer, StatementPreparerOptions
340340
else
341341
writer.Write("timestamp('0000-00-00')");
342342
}
343+
#if NET6_0_OR_GREATER
344+
else if (Value is DateOnly dateOnlyValue)
345+
{
346+
writer.Write("timestamp('{0:yyyy'-'MM'-'dd}')".FormatInvariant(dateOnlyValue));
347+
}
348+
#endif
343349
else if (Value is DateTime dateTimeValue)
344350
{
345351
if ((options & StatementPreparerOptions.DateTimeUtc) != 0 && dateTimeValue.Kind == DateTimeKind.Local)
@@ -354,6 +360,13 @@ internal void AppendSqlString(ByteBufferWriter writer, StatementPreparerOptions
354360
// store as UTC as it will be read as such when deserialized from a timespan column
355361
writer.Write("timestamp('{0:yyyy'-'MM'-'dd' 'HH':'mm':'ss'.'ffffff}')".FormatInvariant(dateTimeOffsetValue.UtcDateTime));
356362
}
363+
#if NET6_0_OR_GREATER
364+
else if (Value is TimeOnly timeOnlyValue)
365+
{
366+
writer.Write("time '");
367+
writer.Write("{0:HH':'mm':'ss'.'ffffff}'".FormatInvariant(timeOnlyValue));
368+
}
369+
#endif
357370
else if (Value is TimeSpan ts)
358371
{
359372
writer.Write("time '");
@@ -605,6 +618,12 @@ internal void AppendBinary(ByteBufferWriter writer, StatementPreparerOptions opt
605618
else
606619
writer.Write((byte) 0);
607620
}
621+
#if NET6_0_OR_GREATER
622+
else if (Value is DateOnly dateOnlyValue)
623+
{
624+
WriteDateOnly(writer, dateOnlyValue);
625+
}
626+
#endif
608627
else if (Value is DateTime dateTimeValue)
609628
{
610629
if ((options & StatementPreparerOptions.DateTimeUtc) != 0 && dateTimeValue.Kind == DateTimeKind.Local)
@@ -619,6 +638,12 @@ internal void AppendBinary(ByteBufferWriter writer, StatementPreparerOptions opt
619638
// store as UTC as it will be read as such when deserialized from a timespan column
620639
WriteDateTime(writer, dateTimeOffsetValue.UtcDateTime);
621640
}
641+
#if NET6_0_OR_GREATER
642+
else if (Value is TimeOnly timeOnlyValue)
643+
{
644+
WriteTime(writer, timeOnlyValue.ToTimeSpan());
645+
}
646+
#endif
622647
else if (Value is TimeSpan ts)
623648
{
624649
WriteTime(writer, ts);
@@ -725,6 +750,16 @@ internal static string NormalizeParameterName(string name)
725750
return name.StartsWith("@", StringComparison.Ordinal) || name.StartsWith("?", StringComparison.Ordinal) ? name.Substring(1) : name;
726751
}
727752

753+
#if NET6_0_OR_GREATER
754+
private static void WriteDateOnly(ByteBufferWriter writer, DateOnly dateOnly)
755+
{
756+
writer.Write((byte) 4);
757+
writer.Write((ushort) dateOnly.Year);
758+
writer.Write((byte) dateOnly.Month);
759+
writer.Write((byte) dateOnly.Day);
760+
}
761+
#endif
762+
728763
private static void WriteDateTime(ByteBufferWriter writer, DateTime dateTime)
729764
{
730765
byte length;

tests/SideBySide/DataTypes.cs

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -660,6 +660,16 @@ public void QueryDate(string column, string dataTypeName, object[] expected)
660660
#endif
661661
}
662662

663+
#if NET6_0_OR_GREATER && !BASELINE
664+
[Theory]
665+
[InlineData("`Date`", "DATE", new object[] { null, "1000 01 01", "9999 12 31", null, "2016 04 05" })]
666+
public void QueryDateOnly(string column, string dataTypeName, object[] expected)
667+
{
668+
DoQuery("times", column, dataTypeName, ConvertToDateOnly(expected), reader => reader.GetDateOnly(column.Replace("`", "")),
669+
matchesDefaultType: false, assertEqual: (x, y) => Assert.Equal((DateOnly) x, y is DateTime dt ? DateOnly.FromDateTime(dt) : (DateOnly) y));
670+
}
671+
#endif
672+
663673
[SkippableTheory(ServerFeatures.ZeroDateTime)]
664674
[InlineData(false)]
665675
[InlineData(true)]
@@ -772,9 +782,23 @@ insert into date_time_kind(d, dt0, dt1, dt2, dt3, dt4, dt5, dt6) values(?, ?, ?,
772782
[InlineData("`Time`", "TIME", new object[] { null, "-838 -59 -59", "838 59 59", "0 0 0", "0 14 3 4 567890" })]
773783
public void QueryTime(string column, string dataTypeName, object[] expected)
774784
{
775-
DoQuery("times", column, dataTypeName, ConvertToTimeSpan(expected), reader => reader.GetTimeSpan(0));
785+
DoQuery("times", column, dataTypeName, ConvertToTimeSpan(expected), reader => reader.GetTimeSpan(0)
786+
#if BASELINE // https://bugs.mysql.com/bug.php?id=103801
787+
, omitWherePrepareTest: true
788+
#endif
789+
);
776790
}
777791

792+
#if NET6_0_OR_GREATER && !BASELINE
793+
[Theory]
794+
[InlineData("TimeOnly", "TIME", new object[] { null, "0 0 0", "0 23 59 59 999999", "0 0 0", "0 14 3 4 567890" })]
795+
public void QueryTimeOnly(string column, string dataTypeName, object[] expected)
796+
{
797+
DoQuery("times", column, dataTypeName, ConvertToTimeOnly(expected), reader => reader.GetTimeOnly(0),
798+
matchesDefaultType: false, assertEqual: (x, y) => Assert.Equal((TimeOnly) x, y is TimeSpan ts ? TimeOnly.FromTimeSpan(ts) : (TimeOnly) y));
799+
}
800+
#endif
801+
778802
[Theory]
779803
[InlineData("`Year`", "YEAR", new object[] { null, 1901, 2155, 0, 2016 })]
780804
public void QueryYear(string column, string dataTypeName, object[] expected)
@@ -1543,13 +1567,14 @@ private void DoQuery(
15431567
Func<MySqlDataReader, object> getValue,
15441568
object baselineCoercedNullValue = null,
15451569
bool omitWhereTest = false,
1570+
bool omitWherePrepareTest = false,
15461571
bool matchesDefaultType = true,
15471572
MySqlConnection connection = null,
15481573
Action<object, object> assertEqual = null,
15491574
Type getFieldValueType = null,
15501575
bool omitGetFieldValueTest = false)
15511576
{
1552-
DoQuery<GetValueWhenNullException>(table, column, dataTypeName, expected, getValue, baselineCoercedNullValue, omitWhereTest, matchesDefaultType, connection, assertEqual, getFieldValueType, omitGetFieldValueTest);
1577+
DoQuery<GetValueWhenNullException>(table, column, dataTypeName, expected, getValue, baselineCoercedNullValue, omitWhereTest, omitWherePrepareTest, matchesDefaultType, connection, assertEqual, getFieldValueType, omitGetFieldValueTest);
15531578
}
15541579

15551580
// NOTE: baselineCoercedNullValue is to work around inconsistencies in mysql-connector-net; DBNull.Value will
@@ -1562,6 +1587,7 @@ private void DoQuery<TException>(
15621587
Func<MySqlDataReader, object> getValue,
15631588
object baselineCoercedNullValue = null,
15641589
bool omitWhereTest = false,
1590+
bool omitWherePrepareTest = false,
15651591
bool matchesDefaultType = true,
15661592
MySqlConnection connection = null,
15671593
Action<object, object> assertEqual = null,
@@ -1639,8 +1665,31 @@ private void DoQuery<TException>(
16391665
cmd.Parameters.Add(p);
16401666
var result = cmd.ExecuteScalar();
16411667
Assert.Equal(Array.IndexOf(expected, p.Value) + 1, result);
1668+
1669+
if (!omitWherePrepareTest)
1670+
{
1671+
cmd.Prepare();
1672+
result = cmd.ExecuteScalar();
1673+
Assert.Equal(Array.IndexOf(expected, p.Value) + 1, result);
1674+
}
1675+
}
1676+
}
1677+
1678+
#if NET6_0_OR_GREATER
1679+
private static object[] ConvertToDateOnly(object[] input)
1680+
{
1681+
var output = new object[input.Length];
1682+
for (int i = 0; i < input.Length; i++)
1683+
{
1684+
var value = SplitAndParse(input[i]);
1685+
if (value?.Length == 3)
1686+
output[i] = new DateOnly(value[0], value[1], value[2]);
1687+
else if (value is not null)
1688+
throw new NotSupportedException("Can't convert to DateOnly");
16421689
}
1690+
return output;
16431691
}
1692+
#endif
16441693

16451694
private static object[] ConvertToDateTime(object[] input, DateTimeKind kind)
16461695
{
@@ -1670,6 +1719,27 @@ private static object[] ConvertToDateTimeOffset(object[] input)
16701719
return output;
16711720
}
16721721

1722+
#if NET6_0_OR_GREATER
1723+
private static object[] ConvertToTimeOnly(object[] input)
1724+
{
1725+
var output = new object[input.Length];
1726+
for (int i = 0; i < input.Length; i++)
1727+
{
1728+
var value = SplitAndParse(input[i]);
1729+
if (value?.Length == 3)
1730+
{
1731+
output[i] = new TimeOnly(value[0], value[1], value[2]);
1732+
}
1733+
else if (value?.Length == 5)
1734+
{
1735+
Assert.Equal(0, value[0]);
1736+
output[i] = new TimeOnly(value[1], value[2], value[3], value[4] / 1000).Add(TimeSpan.FromTicks(value[4] % 1000 * 10));
1737+
}
1738+
}
1739+
return output;
1740+
}
1741+
#endif
1742+
16731743
private static object[] ConvertToTimeSpan(object[] input)
16741744
{
16751745
var output = new object[input.Length];

tests/SideBySide/DataTypesFixture.cs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -171,15 +171,16 @@ create table datatypes_times(
171171
`DateTime` datetime(6) null,
172172
`Timestamp` timestamp(6) null,
173173
`Time` time(6) null,
174+
TimeOnly time(6) null,
174175
`Year` year null);
175176
176-
insert into datatypes_times(`Date`, `DateTime`, `Timestamp`, `Time`, `Year`)
177+
insert into datatypes_times(`Date`, `DateTime`, `Timestamp`, `Time`, `TimeOnly`, `Year`)
177178
values
178-
(null, null, null, null, null),
179-
(date '1000-01-01', timestamp '1000-01-01 00:00:00', timestamp '1970-01-01 00:00:01', time '-838:59:59' , 1901),
180-
(date '9999-12-31', timestamp '9999-12-31 23:59:59.999999', '2038-01-18 03:14:07.999999', time '838:59:59.000', 2155), -- not actually maximum Timestamp value, due to TZ conversion
181-
(null, null, null, time '00:00:00', 0),
182-
(date '2016-04-05', timestamp '2016-04-05 14:03:04.56789', timestamp '2016-04-05 14:03:04.56789', time '14:03:04.56789', 2016);
179+
(null, null, null, null, null, null),
180+
(date '1000-01-01', timestamp '1000-01-01 00:00:00', timestamp '1970-01-01 00:00:01', time '-838:59:59' , time '00:00:00', 1901),
181+
(date '9999-12-31', timestamp '9999-12-31 23:59:59.999999', '2038-01-18 03:14:07.999999', time '838:59:59.000', time '23:59:59.999999', 2155), -- not actually maximum Timestamp value, due to TZ conversion
182+
(null, null, null, time '00:00:00', time '00:00:00', 0),
183+
(date '2016-04-05', timestamp '2016-04-05 14:03:04.56789', timestamp '2016-04-05 14:03:04.56789', time '14:03:04.56789', time '14:03:04.56789', 2016);
183184
184185
drop table if exists datatypes_guids;
185186
create table datatypes_guids (

0 commit comments

Comments
 (0)