Skip to content

Commit 64496ac

Browse files
committed
Support DateTimeKind connection option.
Fixes #477.
1 parent 7863506 commit 64496ac

File tree

12 files changed

+118
-17
lines changed

12 files changed

+118
-17
lines changed

docs/content/connection-options.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,13 @@ These are the other options that MySqlConnector supports. They are set to sensib
189189
<td>false</td>
190190
<td>True to have MySqlDataReader.GetValue() and MySqlDataReader.GetDateTime() return DateTime.MinValue for date or datetime columns that have disallowed values.</td>
191191
</tr>
192+
<tr>
193+
<td>DateTimeKind</td>
194+
<td>Unspecified</td>
195+
<td>The <code>DateTimeKind</code> used when <code>MySqlDataReader</code> returns a <code>DateTime</code>. If set to <code>Utc</code> or <code>Local</code>,
196+
a <code>MySqlException</code> will be thrown if a <code>DateTime</code> command parameter has a <code>Kind</code> of <code>Local</code> or <code>Utc</code>,
197+
respectively.</td>
198+
</tr>
192199
<tr>
193200
<td>Default Command Timeout, Command Timeout, DefaultCommandTimeout</td>
194201
<td>30</td>

src/MySqlConnector/Core/ConnectionSettings.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ public ConnectionSettings(MySqlConnectionStringBuilder csb)
5555
AutoEnlist = csb.AutoEnlist;
5656
ConnectionTimeout = (int) csb.ConnectionTimeout;
5757
ConvertZeroDateTime = csb.ConvertZeroDateTime;
58+
DateTimeKind = (DateTimeKind) csb.DateTimeKind;
5859
DefaultCommandTimeout = (int) csb.DefaultCommandTimeout;
5960
ForceSynchronous = csb.ForceSynchronous;
6061
IgnoreCommandTransaction = csb.IgnoreCommandTransaction;
@@ -105,6 +106,7 @@ public ConnectionSettings(MySqlConnectionStringBuilder csb)
105106
public bool AutoEnlist { get; }
106107
public int ConnectionTimeout { get; }
107108
public bool ConvertZeroDateTime { get; }
109+
public DateTimeKind DateTimeKind { get; }
108110
public int DefaultCommandTimeout { get; }
109111
public bool ForceSynchronous { get; }
110112
public bool IgnoreCommandTransaction { get; }

src/MySqlConnector/Core/Row.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -456,7 +456,7 @@ private DateTime ParseDateTime(ArraySegment<byte> value)
456456
return new DateTime(year, month, day, hour, minute, second);
457457

458458
var microseconds = int.Parse(parts[6] + new string('0', 6 - parts[6].Length), CultureInfo.InvariantCulture);
459-
return new DateTime(year, month, day, hour, minute, second, microseconds / 1000).AddTicks(microseconds % 1000 * 10);
459+
return new DateTime(year, month, day, hour, minute, second, microseconds / 1000, Connection.DateTimeKind).AddTicks(microseconds % 1000 * 10);
460460
}
461461

462462
private static TimeSpan ParseTimeSpan(ArraySegment<byte> value)

src/MySqlConnector/Core/StatementPreparer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ private void DoAppendParameter(int parameterIndex, int textIndex, int textLength
7373
var parameter = m_preparer.m_parameters[parameterIndex];
7474
if (parameter.Direction != ParameterDirection.Input && (m_preparer.m_options & StatementPreparerOptions.AllowOutputParameters) == 0)
7575
throw new MySqlException("Only ParameterDirection.Input is supported when CommandType is Text (parameter name: {0})".FormatInvariant(parameter.ParameterName));
76-
m_preparer.m_parameters[parameterIndex].AppendSqlString(m_writer, m_preparer.m_options);
76+
m_preparer.m_parameters[parameterIndex].AppendSqlString(m_writer, m_preparer.m_options, parameter.ParameterName);
7777
m_lastIndex = textIndex + textLength;
7878
}
7979

src/MySqlConnector/Core/StatementPreparerOptions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,7 @@ internal enum StatementPreparerOptions
99
AllowUserVariables = 1,
1010
OldGuids = 2,
1111
AllowOutputParameters = 4,
12+
DateTimeUtc = 8,
13+
DateTimeLocal = 16,
1214
}
1315
}

src/MySqlConnector/Core/TextCommandExecutor.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ private PayloadData CreateQueryPayload(string commandText, MySqlParameterCollect
9494
statementPreparerOptions |= StatementPreparerOptions.AllowUserVariables;
9595
if (m_command.Connection.OldGuids)
9696
statementPreparerOptions |= StatementPreparerOptions.OldGuids;
97+
if (m_command.Connection.DateTimeKind == DateTimeKind.Utc)
98+
statementPreparerOptions |= StatementPreparerOptions.DateTimeUtc;
99+
else if (m_command.Connection.DateTimeKind == DateTimeKind.Local)
100+
statementPreparerOptions |= StatementPreparerOptions.DateTimeLocal;
97101
if (m_command.CommandType == CommandType.StoredProcedure)
98102
statementPreparerOptions |= StatementPreparerOptions.AllowOutputParameters;
99103
var preparer = new StatementPreparer(commandText, parameterCollection, statementPreparerOptions);

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,7 @@ internal async Task<CachedProcedure> GetCachedProcedure(IOBehavior ioBehavior, s
348348
internal MySqlTransaction CurrentTransaction { get; set; }
349349
internal bool AllowUserVariables => m_connectionSettings.AllowUserVariables;
350350
internal bool ConvertZeroDateTime => m_connectionSettings.ConvertZeroDateTime;
351+
internal DateTimeKind DateTimeKind => m_connectionSettings.DateTimeKind;
351352
internal int DefaultCommandTimeout => GetConnectionSettings().DefaultCommandTimeout;
352353
internal bool IgnoreCommandTransaction => m_connectionSettings.IgnoreCommandTransaction;
353354
internal bool OldGuids => m_connectionSettings.OldGuids;

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

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,12 @@ public bool ConvertZeroDateTime
159159
set => MySqlConnectionStringOption.ConvertZeroDateTime.SetValue(this, value);
160160
}
161161

162+
public MySqlDateTimeKind DateTimeKind
163+
{
164+
get => MySqlConnectionStringOption.DateTimeKind.GetValue(this);
165+
set => MySqlConnectionStringOption.DateTimeKind.SetValue(this, value);
166+
}
167+
162168
public uint DefaultCommandTimeout
163169
{
164170
get => MySqlConnectionStringOption.DefaultCommandTimeout.GetValue(this);
@@ -294,6 +300,7 @@ internal abstract class MySqlConnectionStringOption
294300
public static readonly MySqlConnectionStringOption<string> CharacterSet;
295301
public static readonly MySqlConnectionStringOption<uint> ConnectionTimeout;
296302
public static readonly MySqlConnectionStringOption<bool> ConvertZeroDateTime;
303+
public static readonly MySqlConnectionStringOption<MySqlDateTimeKind> DateTimeKind;
297304
public static readonly MySqlConnectionStringOption<uint> DefaultCommandTimeout;
298305
public static readonly MySqlConnectionStringOption<bool> ForceSynchronous;
299306
public static readonly MySqlConnectionStringOption<bool> IgnoreCommandTransaction;
@@ -427,6 +434,10 @@ static MySqlConnectionStringOption()
427434
keys: new[] { "Convert Zero Datetime", "ConvertZeroDateTime" },
428435
defaultValue: false));
429436

437+
AddOption(DateTimeKind = new MySqlConnectionStringOption<MySqlDateTimeKind>(
438+
keys: new[] { "DateTimeKind" },
439+
defaultValue: MySqlDateTimeKind.Unspecified));
440+
430441
AddOption(DefaultCommandTimeout = new MySqlConnectionStringOption<uint>(
431442
keys: new[] { "Default Command Timeout", "DefaultCommandTimeout", "Command Timeout" },
432443
defaultValue: 30u));
@@ -505,21 +516,11 @@ private static T ChangeType(object objectValue)
505516
return (T) (object) false;
506517
}
507518

508-
if (typeof(T) == typeof(MySqlLoadBalance) && objectValue is string loadBalanceString)
509-
{
510-
foreach (var val in Enum.GetValues(typeof(T)))
511-
{
512-
if (string.Equals(loadBalanceString, val.ToString(), StringComparison.OrdinalIgnoreCase))
513-
return (T) val;
514-
}
515-
throw new InvalidOperationException("Value '{0}' not supported for option '{1}'.".FormatInvariant(objectValue, typeof(T).Name));
516-
}
517-
518-
if (typeof(T) == typeof(MySqlSslMode) && objectValue is string sslModeString)
519+
if ((typeof(T) == typeof(MySqlLoadBalance) || typeof(T) == typeof(MySqlSslMode) || typeof(T) == typeof(MySqlDateTimeKind)) && objectValue is string enumString)
519520
{
520521
foreach (var val in Enum.GetValues(typeof(T)))
521522
{
522-
if (string.Equals(sslModeString, val.ToString(), StringComparison.OrdinalIgnoreCase))
523+
if (string.Equals(enumString, val.ToString(), StringComparison.OrdinalIgnoreCase))
523524
return (T) val;
524525
}
525526
throw new InvalidOperationException("Value '{0}' not supported for option '{1}'.".FormatInvariant(objectValue, typeof(T).Name));
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using System;
2+
3+
namespace MySql.Data.MySqlClient
4+
{
5+
/// <summary>
6+
/// The <see cref="DateTimeKind" /> used when reading <see cref="DateTime" /> from the databsae.
7+
/// </summary>
8+
public enum MySqlDateTimeKind
9+
{
10+
/// <summary>
11+
/// Use <see cref="DateTimeKind.Unspecified" /> when reading; allow any <see cref="DateTimeKind" /> in command parameters.
12+
/// </summary>
13+
Unspecified = DateTimeKind.Unspecified,
14+
15+
/// <summary>
16+
/// Use <see cref="DateTimeKind.Utc" /> when reading; reject <see cref="DateTimeKind.Local" /> in command parameters.
17+
/// </summary>
18+
Utc = DateTimeKind.Utc,
19+
20+
/// <summary>
21+
/// Use <see cref="DateTimeKind.Local" /> when reading; reject <see cref="DateTimeKind.Utc" /> in command parameters.
22+
/// </summary>
23+
Local = DateTimeKind.Local,
24+
}
25+
}

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ private MySqlParameter(MySqlParameter other, string parameterName)
173173

174174
internal string NormalizedParameterName { get; private set; }
175175

176-
internal void AppendSqlString(BinaryWriter writer, StatementPreparerOptions options)
176+
internal void AppendSqlString(BinaryWriter writer, StatementPreparerOptions options, string parameterName)
177177
{
178178
if (Value == null || Value == DBNull.Value)
179179
{
@@ -236,9 +236,14 @@ internal void AppendSqlString(BinaryWriter writer, StatementPreparerOptions opti
236236
{
237237
writer.WriteUtf8("{0:R}".FormatInvariant(Value));
238238
}
239-
else if (Value is DateTime)
239+
else if (Value is DateTime dateTimeValue)
240240
{
241-
writer.WriteUtf8("timestamp('{0:yyyy'-'MM'-'dd' 'HH':'mm':'ss'.'ffffff}')".FormatInvariant(Value));
241+
if ((options & StatementPreparerOptions.DateTimeUtc) != 0 && dateTimeValue.Kind == DateTimeKind.Local)
242+
throw new MySqlException("DateTime.Kind must not be Local when DateTimeKind setting is Utc (parameter name: {0})".FormatInvariant(parameterName));
243+
else if ((options & StatementPreparerOptions.DateTimeLocal) != 0 && dateTimeValue.Kind == DateTimeKind.Utc)
244+
throw new MySqlException("DateTime.Kind must not be Utc when DateTimeKind setting is Local (parameter name: {0})".FormatInvariant(parameterName));
245+
246+
writer.WriteUtf8("timestamp('{0:yyyy'-'MM'-'dd' 'HH':'mm':'ss'.'ffffff}')".FormatInvariant(dateTimeValue));
242247
}
243248
else if (Value is DateTimeOffset dateTimeOffsetValue)
244249
{

tests/MySqlConnector.Tests/MySqlConnectionStringBuilderTests.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ public void Defaults()
2626
#endif
2727
Assert.Equal(15u, csb.ConnectionTimeout);
2828
Assert.False(csb.ConvertZeroDateTime);
29+
#if !BASELINE
30+
Assert.Equal(MySqlDateTimeKind.Unspecified, csb.DateTimeKind);
31+
#endif
2932
Assert.Equal("", csb.Database);
3033
Assert.Equal(30u, csb.DefaultCommandTimeout);
3134
#if !BASELINE
@@ -76,6 +79,9 @@ public void ParseConnectionString()
7679
"connection lifetime=15;" +
7780
"ConnectionReset=false;" +
7881
"Convert Zero Datetime=true;" +
82+
#if !BASELINE
83+
"datetimekind=utc;" +
84+
#endif
7985
"default command timeout=123;" +
8086
#if !BASELINE
8187
"connection idle ping time=60;" +
@@ -109,6 +115,9 @@ public void ParseConnectionString()
109115
Assert.False(csb.ConnectionReset);
110116
Assert.Equal(30u, csb.ConnectionTimeout);
111117
Assert.True(csb.ConvertZeroDateTime);
118+
#if !BASELINE
119+
Assert.Equal(MySqlDateTimeKind.Utc, csb.DateTimeKind);
120+
#endif
112121
Assert.Equal("schema_name", csb.Database);
113122
Assert.Equal(123u, csb.DefaultCommandTimeout);
114123
#if !BASELINE

tests/SideBySide/DataTypes.cs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,51 @@ public void QueryZeroDateTime(bool convertZeroDateTime)
549549
}
550550
}
551551

552+
#if !BASELINE
553+
[Theory]
554+
[InlineData(MySqlDateTimeKind.Unspecified, DateTimeKind.Unspecified, true)]
555+
[InlineData(MySqlDateTimeKind.Unspecified, DateTimeKind.Local, true)]
556+
[InlineData(MySqlDateTimeKind.Unspecified, DateTimeKind.Utc, true)]
557+
[InlineData(MySqlDateTimeKind.Utc, DateTimeKind.Unspecified, true)]
558+
[InlineData(MySqlDateTimeKind.Utc, DateTimeKind.Local, false)]
559+
[InlineData(MySqlDateTimeKind.Utc, DateTimeKind.Utc, true)]
560+
[InlineData(MySqlDateTimeKind.Local, DateTimeKind.Unspecified, true)]
561+
[InlineData(MySqlDateTimeKind.Local, DateTimeKind.Local, true)]
562+
[InlineData(MySqlDateTimeKind.Local, DateTimeKind.Utc, false)]
563+
public void QueryDateTimeKind(MySqlDateTimeKind kindOption, DateTimeKind kindIn, bool success)
564+
{
565+
var csb = AppConfig.CreateConnectionStringBuilder();
566+
csb.DateTimeKind = kindOption;
567+
using (var connection = new MySqlConnection(csb.ConnectionString))
568+
{
569+
connection.Open();
570+
571+
var dateTimeIn = new DateTime(2002, 3, 4, 5, 6, 7, 890, kindIn);
572+
using (var cmd = new MySqlCommand(@"drop table if exists date_time_kind;
573+
create table date_time_kind(rowid integer not null primary key auto_increment, dt datetime(3) not null);
574+
insert into date_time_kind(dt) values(?)", connection)
575+
{
576+
Parameters = { new MySqlParameter { Value = dateTimeIn } }
577+
})
578+
{
579+
if (success)
580+
{
581+
cmd.ExecuteNonQuery();
582+
long lastInsertId = cmd.LastInsertedId;
583+
cmd.CommandText = $"select dt from date_time_kind where rowid = {lastInsertId};";
584+
var dateTimeOut = (DateTime?) cmd.ExecuteScalar();
585+
Assert.Equal(dateTimeIn, dateTimeOut);
586+
Assert.Equal(kindOption, (MySqlDateTimeKind) dateTimeOut.Value.Kind);
587+
}
588+
else
589+
{
590+
Assert.Throws<MySqlException>(() => cmd.ExecuteNonQuery());
591+
}
592+
}
593+
}
594+
}
595+
#endif
596+
552597
[Theory]
553598
[InlineData("`Time`", "TIME", new object[] { null, "-838 -59 -59", "838 59 59", "0 0 0", "0 14 3 4 567890" })]
554599
public void QueryTime(string column, string dataTypeName, object[] expected)

0 commit comments

Comments
 (0)