Skip to content

Commit d6288a2

Browse files
authored
Merge pull request #512 from bgrainger/mysqldatetime
Add MySqlDateTime and AllowZeroDateTime. Fixes #507
2 parents dce98de + 42b6ac6 commit d6288a2

16 files changed

+346
-30
lines changed

docs/content/connection-options.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,13 @@ These are the other options that MySqlConnector supports. They are set to sensib
188188
<td>Allows user-defined variables (prefixed with <code>@</code>) to be used in SQL statements. The default value (<code>false</code>)
189189
only allows <code>@</code>-prefixed names to refer to command parameters.</td>
190190
</tr>
191+
<tr>
192+
<td>AllowZeroDateTime, Allow Zero DateTime</td>
193+
<td>false</td>
194+
<td>If set to <c>true</c> all `DATE`, `DATETIME` and `TIMESTAMP` columns are returned as `MySqlDateTime` objects instead of `DateTime`.
195+
This allows the special “zero” date value `0000-00-00` to be retrieved from the database. If <code>false</code> (the default)
196+
date columns are returned as `DateTime` values, and an exception is thrown for unrepresentable dates.</td>
197+
</tr>
191198
<tr>
192199
<td>Compress, Use Compression, UseCompression</td>
193200
<td>false</td>

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,4 @@ The following bugs in Connector/NET are fixed by switching to MySqlConnector.
127127
* [#89159](https://bugs.mysql.com/bug.php?id=89159): `MySqlDataReader` cannot outlive `MySqlCommand`
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
130+
* [#91199](https://bugs.mysql.com/bug.php?id=91199): Can't insert `MySqlDateTime` values

src/MySqlConnector/Core/ConnectionSettings.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ public ConnectionSettings(MySqlConnectionStringBuilder csb)
6262
// Other Options
6363
AllowPublicKeyRetrieval = csb.AllowPublicKeyRetrieval;
6464
AllowUserVariables = csb.AllowUserVariables;
65+
AllowZeroDateTime = csb.AllowZeroDateTime;
6566
AutoEnlist = csb.AutoEnlist;
6667
ConnectionTimeout = (int) csb.ConnectionTimeout;
6768
ConvertZeroDateTime = csb.ConvertZeroDateTime;
@@ -135,6 +136,7 @@ private static MySqlGuidFormat GetEffectiveGuidFormat(MySqlGuidFormat guidFormat
135136
// Other Options
136137
public bool AllowPublicKeyRetrieval { get; }
137138
public bool AllowUserVariables { get; }
139+
public bool AllowZeroDateTime { get; }
138140
public bool AutoEnlist { get; }
139141
public int ConnectionTimeout { get; }
140142
public bool ConvertZeroDateTime { get; }

src/MySqlConnector/Core/ResultSet.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Threading;
66
using System.Threading.Tasks;
77
using MySql.Data.MySqlClient;
8+
using MySql.Data.Types;
89
using MySqlConnector.Protocol;
910
using MySqlConnector.Protocol.Payloads;
1011
using MySqlConnector.Protocol.Serialization;
@@ -297,7 +298,10 @@ public Type GetFieldType(int ordinal)
297298
if (ordinal < 0 || ordinal > ColumnDefinitions.Length)
298299
throw new ArgumentOutOfRangeException(nameof(ordinal), "value must be between 0 and {0}.".FormatInvariant(ColumnDefinitions.Length));
299300

300-
return TypeMapper.Instance.GetColumnTypeMetadata(ColumnTypes[ordinal]).DbTypeMapping.ClrType;
301+
var type = TypeMapper.Instance.GetColumnTypeMetadata(ColumnTypes[ordinal]).DbTypeMapping.ClrType;
302+
if (Connection.AllowZeroDateTime && type == typeof(DateTime))
303+
type = typeof(MySqlDateTime);
304+
return type;
301305
}
302306

303307
public int FieldCount => ColumnDefinitions?.Length ?? 0;

src/MySqlConnector/Core/Row.cs

Lines changed: 50 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Globalization;
44
using System.Text;
55
using MySql.Data.MySqlClient;
6+
using MySql.Data.Types;
67
using MySqlConnector.Protocol;
78
using MySqlConnector.Protocol.Serialization;
89
using MySqlConnector.Utilities;
@@ -315,6 +316,14 @@ public double GetDouble(int ordinal)
315316

316317
public float GetFloat(int ordinal) => (float) GetValue(ordinal);
317318

319+
public MySqlDateTime GetMySqlDateTime(int ordinal)
320+
{
321+
var value = GetValue(ordinal);
322+
if (value is DateTime dateTime)
323+
return new MySqlDateTime(dateTime);
324+
return (MySqlDateTime) value;
325+
}
326+
318327
public int GetValues(object[] values)
319328
{
320329
int count = Math.Min(values.Length, ResultSet.ColumnDefinitions.Length);
@@ -451,7 +460,7 @@ private static void CheckBufferArguments<T>(long dataOffset, T[] buffer, int buf
451460
throw new ArgumentException("bufferOffset + length cannot exceed buffer.Length", nameof(length));
452461
}
453462

454-
private DateTime ParseDateTime(ReadOnlySpan<byte> value)
463+
private object ParseDateTime(ReadOnlySpan<byte> value)
455464
{
456465
if (!Utf8Parser.TryParse(value, out int year, out var bytesConsumed) || bytesConsumed != 4)
457466
goto InvalidDateTime;
@@ -468,36 +477,52 @@ private DateTime ParseDateTime(ReadOnlySpan<byte> value)
468477
{
469478
if (Connection.ConvertZeroDateTime)
470479
return DateTime.MinValue;
480+
if (Connection.AllowZeroDateTime)
481+
return new MySqlDateTime();
471482
throw new InvalidCastException("Unable to convert MySQL date/time to System.DateTime.");
472483
}
473484

485+
int hour, minute, second, microseconds;
474486
if (value.Length == 10)
475-
return new DateTime(year, month, day, 0, 0, 0, Connection.DateTimeKind);
476-
477-
if (value[10] != 32)
478-
goto InvalidDateTime;
479-
if (!Utf8Parser.TryParse(value.Slice(11), out int hour, out bytesConsumed) || bytesConsumed != 2)
480-
goto InvalidDateTime;
481-
if (value.Length < 14 || value[13] != 58)
482-
goto InvalidDateTime;
483-
if (!Utf8Parser.TryParse(value.Slice(14), out int minute, out bytesConsumed) || bytesConsumed != 2)
484-
goto InvalidDateTime;
485-
if (value.Length < 17 || value[16] != 58)
486-
goto InvalidDateTime;
487-
if (!Utf8Parser.TryParse(value.Slice(17), out int second, out bytesConsumed) || bytesConsumed != 2)
488-
goto InvalidDateTime;
489-
490-
if (value.Length == 19)
491-
return new DateTime(year, month, day, hour, minute, second, Connection.DateTimeKind);
492-
if (value[19] != 46)
493-
goto InvalidDateTime;
487+
{
488+
hour = 0;
489+
minute = 0;
490+
second = 0;
491+
microseconds = 0;
492+
}
493+
else
494+
{
495+
if (value[10] != 32)
496+
goto InvalidDateTime;
497+
if (!Utf8Parser.TryParse(value.Slice(11), out hour, out bytesConsumed) || bytesConsumed != 2)
498+
goto InvalidDateTime;
499+
if (value.Length < 14 || value[13] != 58)
500+
goto InvalidDateTime;
501+
if (!Utf8Parser.TryParse(value.Slice(14), out minute, out bytesConsumed) || bytesConsumed != 2)
502+
goto InvalidDateTime;
503+
if (value.Length < 17 || value[16] != 58)
504+
goto InvalidDateTime;
505+
if (!Utf8Parser.TryParse(value.Slice(17), out second, out bytesConsumed) || bytesConsumed != 2)
506+
goto InvalidDateTime;
507+
508+
if (value.Length == 19)
509+
{
510+
microseconds = 0;
511+
}
512+
else
513+
{
514+
if (value[19] != 46)
515+
goto InvalidDateTime;
494516

495-
if (!Utf8Parser.TryParse(value.Slice(20), out int microseconds, out bytesConsumed) || bytesConsumed != value.Length - 20)
496-
goto InvalidDateTime;
497-
for (; bytesConsumed < 6; bytesConsumed++)
498-
microseconds *= 10;
517+
if (!Utf8Parser.TryParse(value.Slice(20), out microseconds, out bytesConsumed) || bytesConsumed != value.Length - 20)
518+
goto InvalidDateTime;
519+
for (; bytesConsumed < 6; bytesConsumed++)
520+
microseconds *= 10;
521+
}
522+
}
499523

500-
return new DateTime(year, month, day, hour, minute, second, microseconds / 1000, Connection.DateTimeKind).AddTicks(microseconds % 1000 * 10);
524+
var dt = new DateTime(year, month, day, hour, minute, second, microseconds / 1000, Connection.DateTimeKind).AddTicks(microseconds % 1000 * 10);
525+
return Connection.AllowZeroDateTime ? (object) new MySqlDateTime(dt) : dt;
501526

502527
InvalidDateTime:
503528
throw new FormatException("Couldn't interpret '{0}' as a valid DateTime".FormatInvariant(Encoding.UTF8.GetString(value)));

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,7 @@ internal async Task<CachedProcedure> GetCachedProcedure(IOBehavior ioBehavior, s
346346

347347
internal MySqlTransaction CurrentTransaction { get; set; }
348348
internal bool AllowUserVariables => m_connectionSettings.AllowUserVariables;
349+
internal bool AllowZeroDateTime => m_connectionSettings.AllowZeroDateTime;
349350
internal bool ConvertZeroDateTime => m_connectionSettings.ConvertZeroDateTime;
350351
internal DateTimeKind DateTimeKind => m_connectionSettings.DateTimeKind;
351352
internal int DefaultCommandTimeout => GetConnectionSettings().DefaultCommandTimeout;

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,12 @@ public bool AllowUserVariables
147147
set => MySqlConnectionStringOption.AllowUserVariables.SetValue(this, value);
148148
}
149149

150+
public bool AllowZeroDateTime
151+
{
152+
get => MySqlConnectionStringOption.AllowZeroDateTime.GetValue(this);
153+
set => MySqlConnectionStringOption.AllowZeroDateTime.SetValue(this, value);
154+
}
155+
150156
public bool AutoEnlist
151157
{
152158
get => MySqlConnectionStringOption.AutoEnlist.GetValue(this);
@@ -322,6 +328,7 @@ internal abstract class MySqlConnectionStringOption
322328
// Other Options
323329
public static readonly MySqlConnectionStringOption<bool> AllowPublicKeyRetrieval;
324330
public static readonly MySqlConnectionStringOption<bool> AllowUserVariables;
331+
public static readonly MySqlConnectionStringOption<bool> AllowZeroDateTime;
325332
public static readonly MySqlConnectionStringOption<bool> AutoEnlist;
326333
public static readonly MySqlConnectionStringOption<string> CharacterSet;
327334
public static readonly MySqlConnectionStringOption<uint> ConnectionTimeout;
@@ -454,6 +461,10 @@ static MySqlConnectionStringOption()
454461
keys: new[] { "AllowUserVariables", "Allow User Variables" },
455462
defaultValue: false));
456463

464+
AddOption(AllowZeroDateTime = new MySqlConnectionStringOption<bool>(
465+
keys: new[] { "AllowZeroDateTime", "Allow Zero DateTime" },
466+
defaultValue: false));
467+
457468
AddOption(AutoEnlist = new MySqlConnectionStringOption<bool>(
458469
keys: new[] { "AutoEnlist", "Auto Enlist" },
459470
defaultValue: true));

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Linq;
99
using System.Threading;
1010
using System.Threading.Tasks;
11+
using MySql.Data.Types;
1112
using MySqlConnector.Core;
1213
using MySqlConnector.Protocol.Serialization;
1314
using MySqlConnector.Utilities;
@@ -224,6 +225,9 @@ public override long GetChars(int ordinal, long dataOffset, char[] buffer, int b
224225
public DateTimeOffset GetDateTimeOffset(int ordinal) => GetResultSet().GetCurrentRow().GetDateTimeOffset(ordinal);
225226
public DateTimeOffset GetDateTimeOffset(string name) => GetDateTimeOffset(GetOrdinal(name));
226227

228+
public MySqlDateTime GetMySqlDateTime(int ordinal) => GetResultSet().GetCurrentRow().GetMySqlDateTime(ordinal);
229+
public MySqlDateTime GetMySqlDateTime(string name) => GetMySqlDateTime(GetOrdinal(name));
230+
227231
public TimeSpan GetTimeSpan(int ordinal) => (TimeSpan) GetValue(ordinal);
228232
public TimeSpan GetTimeSpan(string name) => GetTimeSpan(GetOrdinal(name));
229233

@@ -270,7 +274,7 @@ public override void Close()
270274
public ReadOnlyCollection<DbColumn> GetColumnSchema()
271275
{
272276
return GetResultSet().ColumnDefinitions
273-
.Select((c, n) => (DbColumn) new MySqlDbColumn(n, c, GetResultSet().ColumnTypes[n]))
277+
.Select((c, n) => (DbColumn) new MySqlDbColumn(n, c, Connection.AllowZeroDateTime, GetResultSet().ColumnTypes[n]))
274278
.ToList().AsReadOnly();
275279
}
276280

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Globalization;
3+
using MySql.Data.Types;
34
using MySqlConnector.Core;
45
using MySqlConnector.Protocol;
56
using MySqlConnector.Protocol.Payloads;
@@ -42,7 +43,7 @@ namespace MySql.Data.MySqlClient
4243
{
4344
public sealed class MySqlDbColumn : System.Data.Common.DbColumn
4445
{
45-
internal MySqlDbColumn(int ordinal, ColumnDefinitionPayload column, MySqlDbType mySqlDbType)
46+
internal MySqlDbColumn(int ordinal, ColumnDefinitionPayload column, bool allowZeroDateTime, MySqlDbType mySqlDbType)
4647
{
4748
var columnTypeMetadata = TypeMapper.Instance.GetColumnTypeMetadata(mySqlDbType);
4849

@@ -59,7 +60,7 @@ internal MySqlDbColumn(int ordinal, ColumnDefinitionPayload column, MySqlDbType
5960
ColumnName = column.Name;
6061
ColumnOrdinal = ordinal;
6162
ColumnSize = columnSize > int.MaxValue ? int.MaxValue : unchecked((int) columnSize);
62-
DataType = type;
63+
DataType = (allowZeroDateTime && type == typeof(DateTime)) ? typeof(MySqlDateTime) : type;
6364
DataTypeName = columnTypeMetadata.SimpleDataTypeName;
6465
if (mySqlDbType == MySqlDbType.String)
6566
DataTypeName += string.Format(CultureInfo.InvariantCulture, "({0})", columnSize);

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Data;
44
using System.Data.Common;
55
using System.Diagnostics;
6+
using MySql.Data.Types;
67
using MySqlConnector.Core;
78
using MySqlConnector.Protocol.Serialization;
89
using MySqlConnector.Utilities;
@@ -266,6 +267,13 @@ internal void AppendSqlString(ByteBufferWriter writer, StatementPreparerOptions
266267
// NOTE: Utf8Formatter doesn't support "R"
267268
writer.Write("{0:R}".FormatInvariant(Value));
268269
}
270+
else if (Value is MySqlDateTime mySqlDateTimeValue)
271+
{
272+
if (mySqlDateTimeValue.IsValidDateTime)
273+
writer.Write("timestamp('{0:yyyy'-'MM'-'dd' 'HH':'mm':'ss'.'ffffff}')".FormatInvariant(mySqlDateTimeValue.GetDateTime()));
274+
else
275+
writer.Write("timestamp('0000-00-00')");
276+
}
269277
else if (Value is DateTime dateTimeValue)
270278
{
271279
if ((options & StatementPreparerOptions.DateTimeUtc) != 0 && dateTimeValue.Kind == DateTimeKind.Local)

0 commit comments

Comments
 (0)