diff --git a/src/EFCore.MySql/Extensions/MySqlDbFunctionsExtensions.cs b/src/EFCore.MySql/Extensions/MySqlDbFunctionsExtensions.cs
index db8f01368..11126ffdd 100644
--- a/src/EFCore.MySql/Extensions/MySqlDbFunctionsExtensions.cs
+++ b/src/EFCore.MySql/Extensions/MySqlDbFunctionsExtensions.cs
@@ -14,6 +14,158 @@ namespace Microsoft.EntityFrameworkCore
///
public static class MySqlDbFunctionsExtensions
{
+ #region ConvertTimeZone
+
+ ///
+ /// Converts the `DateTime` value from the time zone given by to the time zone given by and returns the resulting value.
+ /// Corresponds to `CONVERT_TZ(dateTime, fromTimeZone, toTimeZone)`.
+ ///
+ /// The DbFunctions instance.
+ /// The `DateTime` value to convert.
+ /// The time zone to convert from.
+ /// The time zone to convert to.
+ /// The converted value.
+ public static DateTime? ConvertTimeZone(
+ [CanBeNull] this DbFunctions _,
+ DateTime dateTime,
+ string fromTimeZone,
+ string toTimeZone)
+ => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ConvertTimeZone)));
+
+ ///
+ /// Converts the `DateOnly` value from the time zone given by to the time zone given by and returns the resulting value.
+ /// Corresponds to `CONVERT_TZ(dateTime, fromTimeZone, toTimeZone)`..
+ ///
+ /// The DbFunctions instance.
+ /// The `DateOnly` value to convert.
+ /// The time zone to convert from.
+ /// The time zone to convert to.
+ /// The converted value.
+ public static DateOnly? ConvertTimeZone(
+ [CanBeNull] this DbFunctions _,
+ DateOnly dateOnly,
+ string fromTimeZone,
+ string toTimeZone)
+ => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ConvertTimeZone)));
+
+ ///
+ /// Converts the `DateTime?` value from the time zone given by to the time zone given by and returns the resulting value.
+ /// Corresponds to `CONVERT_TZ(dateTime, fromTimeZone, toTimeZone)`.
+ ///
+ /// The DbFunctions instance.
+ /// The `DateTime?` value to convert.
+ /// The time zone to convert from.
+ /// The time zone to convert to.
+ /// The converted value.
+ public static DateTime? ConvertTimeZone(
+ [CanBeNull] this DbFunctions _,
+ DateTime? dateTime,
+ string fromTimeZone,
+ string toTimeZone)
+ => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ConvertTimeZone)));
+
+ ///
+ /// Converts the `DateOnly?` value from the time zone given by to the time zone given by and returns the resulting value.
+ /// Corresponds to `CONVERT_TZ(dateTime, fromTimeZone, toTimeZone)`..
+ ///
+ /// The DbFunctions instance.
+ /// The `DateOnly?` value to convert.
+ /// The time zone to convert from.
+ /// The time zone to convert to.
+ /// The converted value.
+ public static DateOnly? ConvertTimeZone(
+ [CanBeNull] this DbFunctions _,
+ DateOnly? dateOnly,
+ string fromTimeZone,
+ string toTimeZone)
+ => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ConvertTimeZone)));
+
+ ///
+ /// Converts the `DateTime` value from `@@session.time_zone` to the time zone given by and returns the resulting value.
+ /// Corresponds to `CONVERT_TZ(dateTime, @@session.time_zone, toTimeZone)`.
+ ///
+ /// The DbFunctions instance.
+ /// The `DateTime` value to convert.
+ /// The time zone to convert to.
+ /// The converted value.
+ public static DateTime? ConvertTimeZone(
+ [CanBeNull] this DbFunctions _,
+ DateTime dateTime,
+ string toTimeZone)
+ => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ConvertTimeZone)));
+
+ ///
+ /// Converts the `DateTimeOffset` value from `+00:00`/UTC to the time zone given by and returns the resulting value as a `DateTime`.
+ /// Corresponds to `CONVERT_TZ(dateTime, '+00:00', toTimeZone)`.
+ ///
+ /// The DbFunctions instance.
+ /// The `DateTimeOffset` value to convert.
+ /// The time zone to convert to.
+ /// The converted `DateTime?` value.
+ public static DateTime? ConvertTimeZone(
+ [CanBeNull] this DbFunctions _,
+ DateTimeOffset dateTimeOffset,
+ string toTimeZone)
+ => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ConvertTimeZone)));
+
+ ///
+ /// Converts the `DateOnly` value from `@@session.time_zone` to the time zone given by and returns the resulting value.
+ /// Corresponds to `CONVERT_TZ(dateTime, @@session.time_zone, toTimeZone)`.
+ ///
+ /// The DbFunctions instance.
+ /// The `DateOnly` value to convert.
+ /// The time zone to convert to.
+ /// The converted value.
+ public static DateOnly? ConvertTimeZone(
+ [CanBeNull] this DbFunctions _,
+ DateOnly dateOnly,
+ string toTimeZone)
+ => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ConvertTimeZone)));
+
+ ///
+ /// Converts the `DateTime?` value from `@@session.time_zone` to the time zone given by and returns the resulting value.
+ /// Corresponds to `CONVERT_TZ(dateTime, @@session.time_zone, toTimeZone)`.
+ ///
+ /// The DbFunctions instance.
+ /// The `DateTime?` value to convert.
+ /// The time zone to convert to.
+ /// The converted value.
+ public static DateTime? ConvertTimeZone(
+ [CanBeNull] this DbFunctions _,
+ DateTime? dateTime,
+ string toTimeZone)
+ => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ConvertTimeZone)));
+
+ ///
+ /// Converts the `DateTimeOffset?` value from `+00:00`/UTC to the time zone given by and returns the resulting value as a `DateTime`.
+ /// Corresponds to `CONVERT_TZ(dateTime, '+00:00', toTimeZone)`.
+ ///
+ /// The DbFunctions instance.
+ /// The `DateTimeOffset?` value to convert.
+ /// The time zone to convert to.
+ /// The converted `DateTime?` value.
+ public static DateTime? ConvertTimeZone(
+ [CanBeNull] this DbFunctions _,
+ DateTimeOffset? dateTimeOffset,
+ string toTimeZone)
+ => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ConvertTimeZone)));
+
+ ///
+ /// Converts the `DateOnly?` value from `@@session.time_zone` to the time zone given by and returns the resulting value.
+ /// Corresponds to `CONVERT_TZ(dateTime, @@session.time_zone, toTimeZone)`.
+ ///
+ /// The DbFunctions instance.
+ /// The `DateOnly?` value to convert.
+ /// The time zone to convert to.
+ /// The converted value.
+ public static DateOnly? ConvertTimeZone(
+ [CanBeNull] this DbFunctions _,
+ DateOnly? dateOnly,
+ string toTimeZone)
+ => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ConvertTimeZone)));
+
+ #endregion ConvertTimeZone
+
///
/// Counts the number of year boundaries crossed between the startDate and endDate.
/// Corresponds to TIMESTAMPDIFF(YEAR,startDate,endDate).
diff --git a/src/EFCore.MySql/Query/ExpressionTranslators/Internal/MySqlDbFunctionsExtensionsMethodTranslator.cs b/src/EFCore.MySql/Query/ExpressionTranslators/Internal/MySqlDbFunctionsExtensionsMethodTranslator.cs
index d50ac820b..bf1d9eb83 100644
--- a/src/EFCore.MySql/Query/ExpressionTranslators/Internal/MySqlDbFunctionsExtensionsMethodTranslator.cs
+++ b/src/EFCore.MySql/Query/ExpressionTranslators/Internal/MySqlDbFunctionsExtensionsMethodTranslator.cs
@@ -11,6 +11,7 @@
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using Microsoft.EntityFrameworkCore.Storage;
using Pomelo.EntityFrameworkCore.MySql.Query.Internal;
+using Pomelo.EntityFrameworkCore.MySql.Utilities;
namespace Pomelo.EntityFrameworkCore.MySql.Query.ExpressionTranslators.Internal
{
@@ -22,6 +23,40 @@ public class MySqlDbFunctionsExtensionsMethodTranslator : IMethodCallTranslator
{
private readonly MySqlSqlExpressionFactory _sqlExpressionFactory;
+ private static readonly HashSet _convertTimeZoneMethodInfos =
+ [
+ typeof(MySqlDbFunctionsExtensions).GetRuntimeMethod(
+ nameof(MySqlDbFunctionsExtensions.ConvertTimeZone),
+ new[] { typeof(DbFunctions), typeof(DateTime), typeof(string), typeof(string) }),
+ typeof(MySqlDbFunctionsExtensions).GetRuntimeMethod(
+ nameof(MySqlDbFunctionsExtensions.ConvertTimeZone),
+ new[] { typeof(DbFunctions), typeof(DateOnly), typeof(string), typeof(string) }),
+ typeof(MySqlDbFunctionsExtensions).GetRuntimeMethod(
+ nameof(MySqlDbFunctionsExtensions.ConvertTimeZone),
+ new[] { typeof(DbFunctions), typeof(DateTime?), typeof(string), typeof(string) }),
+ typeof(MySqlDbFunctionsExtensions).GetRuntimeMethod(
+ nameof(MySqlDbFunctionsExtensions.ConvertTimeZone),
+ new[] { typeof(DbFunctions), typeof(DateOnly?), typeof(string), typeof(string) }),
+ typeof(MySqlDbFunctionsExtensions).GetRuntimeMethod(
+ nameof(MySqlDbFunctionsExtensions.ConvertTimeZone),
+ new[] { typeof(DbFunctions), typeof(DateTime), typeof(string) }),
+ typeof(MySqlDbFunctionsExtensions).GetRuntimeMethod(
+ nameof(MySqlDbFunctionsExtensions.ConvertTimeZone),
+ new[] { typeof(DbFunctions), typeof(DateTimeOffset), typeof(string) }),
+ typeof(MySqlDbFunctionsExtensions).GetRuntimeMethod(
+ nameof(MySqlDbFunctionsExtensions.ConvertTimeZone),
+ new[] { typeof(DbFunctions), typeof(DateOnly), typeof(string) }),
+ typeof(MySqlDbFunctionsExtensions).GetRuntimeMethod(
+ nameof(MySqlDbFunctionsExtensions.ConvertTimeZone),
+ new[] { typeof(DbFunctions), typeof(DateTime?), typeof(string) }),
+ typeof(MySqlDbFunctionsExtensions).GetRuntimeMethod(
+ nameof(MySqlDbFunctionsExtensions.ConvertTimeZone),
+ new[] { typeof(DbFunctions), typeof(DateTimeOffset?), typeof(string) }),
+ typeof(MySqlDbFunctionsExtensions).GetRuntimeMethod(
+ nameof(MySqlDbFunctionsExtensions.ConvertTimeZone),
+ new[] { typeof(DbFunctions), typeof(DateOnly?), typeof(string) }),
+ ];
+
private static readonly Type[] _supportedLikeTypes = {
typeof(int),
typeof(long),
@@ -148,6 +183,29 @@ public virtual SqlExpression Translate(
IReadOnlyList arguments,
IDiagnosticsLogger logger)
{
+ if (_convertTimeZoneMethodInfos.TryGetValue(method, out _))
+ {
+ // Will not just return `NULL` if any of its parameters is `NULL`, but also if `fromTimeZone` or `toTimeZone` is incorrect.
+ // Will do no conversion at all if `dateTime` is outside the supported range.
+ return _sqlExpressionFactory.NullableFunction(
+ "CONVERT_TZ",
+ arguments.Count == 3
+ ?
+ [
+ arguments[1],
+ // The implicit fromTimeZone is UTC for DateTimeOffset values and the current session time zone otherwise.
+ method.GetParameters()[1].ParameterType.UnwrapNullableType() == typeof(DateTimeOffset)
+ ? _sqlExpressionFactory.Constant("+00:00")
+ : _sqlExpressionFactory.Fragment("@@session.time_zone"),
+ arguments[2]
+ ]
+ : new[] { arguments[1], arguments[2], arguments[3] },
+ method.ReturnType.UnwrapNullableType(),
+ null,
+ false,
+ Statics.GetTrueValues(arguments.Count));
+ }
+
if (_likeMethodInfos.Any(m => Equals(method, m)))
{
var match = _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[1]);
diff --git a/test/EFCore.MySql.Tests/Query/MySqlTimeZoneTest.cs b/test/EFCore.MySql.Tests/Query/MySqlTimeZoneTest.cs
new file mode 100644
index 000000000..13407882d
--- /dev/null
+++ b/test/EFCore.MySql.Tests/Query/MySqlTimeZoneTest.cs
@@ -0,0 +1,112 @@
+using System;
+using System.Linq;
+using Microsoft.EntityFrameworkCore;
+using Xunit;
+
+namespace Pomelo.EntityFrameworkCore.MySql.Query
+{
+ public sealed class MySqlTimeZoneTest : TestWithFixture
+ {
+ public MySqlTimeZoneTest(MySqlTimeZoneFixture fixture)
+ : base(fixture)
+ {
+ }
+
+ [ConditionalFact]
+ public void ConvertTimeZone()
+ {
+ using var context = Fixture.CreateContext();
+ SetSessionTimeZone(context);
+ Fixture.ClearSql();
+
+ var metalContainer = context.Set()
+ .Where(c => c.DeliveredDateTimeOffset == c.DeliveredDateTimeUtc &&
+ EF.Functions.ConvertTimeZone(c.DeliveredDateTimeOffset, c.DeliveredTimeZone) == c.DeliveredDateTimeLocal)
+ .Select(
+ c => new
+ {
+ c.DeliveredDateTimeUtc,
+ c.DeliveredDateTimeLocal,
+ c.DeliveredDateTimeOffset,
+ c.DeliveredTimeZone,
+ DeliveredWithAppliedTimeZone = EF.Functions.ConvertTimeZone(c.DeliveredDateTimeOffset, c.DeliveredTimeZone),
+ DeliveredConvertedToDifferent = EF.Functions.ConvertTimeZone(c.DeliveredDateTimeLocal, c.DeliveredTimeZone, "+06:00"),
+ })
+ .Single();
+
+ Assert.Equal(MySqlTimeZoneFixture.OriginalDateTimeUtc, metalContainer.DeliveredDateTimeUtc);
+ Assert.Equal(MySqlTimeZoneFixture.OriginalDateTime, metalContainer.DeliveredDateTimeLocal);
+ Assert.Equal(MySqlTimeZoneFixture.OriginalDateTimeOffset, metalContainer.DeliveredDateTimeOffset);
+ Assert.Equal(MySqlTimeZoneFixture.OriginalDateTimeOffset.UtcDateTime, metalContainer.DeliveredDateTimeOffset.DateTime);
+ Assert.Equal(TimeSpan.Zero, metalContainer.DeliveredDateTimeOffset.Offset);
+ Assert.Equal(MySqlTimeZoneFixture.OriginalDateTime, metalContainer.DeliveredWithAppliedTimeZone);
+ Assert.Equal(MySqlTimeZoneFixture.OriginalDateTimeUtc.AddHours(6), metalContainer.DeliveredConvertedToDifferent);
+
+ Assert.Equal(
+ """
+SELECT `c`.`DeliveredDateTimeUtc`, `c`.`DeliveredDateTimeLocal`, `c`.`DeliveredDateTimeOffset`, `c`.`DeliveredTimeZone`, CONVERT_TZ(`c`.`DeliveredDateTimeOffset`, '+00:00', `c`.`DeliveredTimeZone`) AS `DeliveredWithAppliedTimeZone`, CONVERT_TZ(`c`.`DeliveredDateTimeLocal`, `c`.`DeliveredTimeZone`, '+06:00') AS `DeliveredConvertedToDifferent`
+FROM `Container` AS `c`
+WHERE (`c`.`DeliveredDateTimeOffset` = `c`.`DeliveredDateTimeUtc`) AND (CONVERT_TZ(`c`.`DeliveredDateTimeOffset`, '+00:00', `c`.`DeliveredTimeZone`) = `c`.`DeliveredDateTimeLocal`)
+LIMIT 2
+""",
+ Fixture.Sql);
+ }
+
+ private static void SetSessionTimeZone(MySqlTimeZoneFixture.MySqlTimeZoneContext context)
+ {
+ context.Database.OpenConnection();
+ var connection = context.Database.GetDbConnection();
+ using var command = connection.CreateCommand();
+ command.CommandText = "SET @@session.time_zone = '-08:00';";
+ command.ExecuteNonQuery();
+ }
+
+ public class MySqlTimeZoneFixture : MySqlTestFixtureBase
+ {
+ public const int OriginalOffset = 2; // UTC+2
+ public static readonly DateTime OriginalDateTimeUtc = new DateTime(2023, 12, 31, 23, 0, 0);
+ public static readonly DateTime OriginalDateTime = OriginalDateTimeUtc.AddHours(OriginalOffset);
+ public static readonly DateTimeOffset OriginalDateTimeOffset = new DateTimeOffset(OriginalDateTime, TimeSpan.FromHours(OriginalOffset));
+
+ public void ClearSql()
+ => base.SqlCommands.Clear();
+
+ public new string Sql
+ => base.Sql;
+
+ public class MySqlTimeZoneContext : ContextBase
+ {
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity(
+ entity =>
+ {
+ entity.HasData(
+ new Model.Container
+ {
+ Id = 1,
+ Name = "Heavymetal",
+ DeliveredDateTimeUtc = OriginalDateTimeUtc,
+ DeliveredDateTimeLocal = OriginalDateTime,
+ DeliveredDateTimeOffset = OriginalDateTimeOffset,
+ DeliveredTimeZone = "+02:00",
+ });
+ });
+ }
+ }
+ }
+
+ private static class Model
+ {
+ public class Container
+ {
+ public int Id { get ; set; }
+ public string Name { get ; set; }
+ public DateTime DeliveredDateTimeUtc { get; set; }
+ public DateTime DeliveredDateTimeLocal { get; set; }
+ public DateTimeOffset DeliveredDateTimeOffset { get; set; }
+ public string DeliveredTimeZone { get; set; }
+ }
+ }
+ }
+}