diff --git a/src/EFCore.PG.NodaTime/Storage/Internal/DateTimeZoneMapping.cs b/src/EFCore.PG.NodaTime/Storage/Internal/DateTimeZoneMapping.cs new file mode 100644 index 0000000000000000000000000000000000000000..9e2e333da8538d9c317b049f860b1a966527e242 --- /dev/null +++ b/src/EFCore.PG.NodaTime/Storage/Internal/DateTimeZoneMapping.cs @@ -0,0 +1,76 @@ +// ReSharper disable once CheckNamespace +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; + +/// +/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to +/// the same compatibility standards as public APIs. It may be changed or removed without notice in +/// any release. You should only use it directly in your code with extreme caution and knowing that +/// doing so can result in application failures when updating to a new Entity Framework Core release. +/// +public class DateTimeZoneMapping : RelationalTypeMapping +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public DateTimeZoneMapping(string storeType) + : base( + new RelationalTypeMappingParameters( + new(typeof(DateTimeZone), new DateTimeZoneConverter(), new DateTimeZoneComparer()), storeType)) + { + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected DateTimeZoneMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new DateTimeZoneMapping(parameters); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public override Expression GenerateCodeLiteral(object value) + => Expression.Call( + Expression.Property(null, typeof(DateTimeZoneProviders).GetProperty(nameof(DateTimeZoneProviders.Tzdb))!), + typeof(IDateTimeZoneProvider).GetMethod(nameof(IDateTimeZoneProvider.GetZoneOrNull), new[] { typeof(string) })!, + Expression.Constant(((DateTimeZone)value).Id)); + + private sealed class DateTimeZoneConverter : ValueConverter + { + public DateTimeZoneConverter() + : base( + tz => tz.Id, + id => DateTimeZoneProviders.Tzdb[id]) + { + } + } + + private sealed class DateTimeZoneComparer : ValueComparer + { + public DateTimeZoneComparer() + : base( + (tz1, tz2) => tz1 == null ? tz2 == null : tz2 != null && tz1.Id == tz2.Id, + tz => tz.GetHashCode()) + { + } + } +} diff --git a/src/EFCore.PG.NodaTime/Storage/Internal/NpgsqlNodaTimeTypeMappingSourcePlugin.cs b/src/EFCore.PG.NodaTime/Storage/Internal/NpgsqlNodaTimeTypeMappingSourcePlugin.cs index 2455f05f23658823dc2b77ebef7031ab77c05d92..e2d487814175ffb3757723d36a38f7621e3c39d3 100644 --- a/src/EFCore.PG.NodaTime/Storage/Internal/NpgsqlNodaTimeTypeMappingSourcePlugin.cs +++ b/src/EFCore.PG.NodaTime/Storage/Internal/NpgsqlNodaTimeTypeMappingSourcePlugin.cs @@ -57,6 +57,9 @@ static NpgsqlNodaTimeTypeMappingSourcePlugin() private readonly PeriodIntervalMapping _periodInterval = new(); private readonly DurationIntervalMapping _durationInterval = new(); + // PostgreSQL has no native type for representing time zones - it just uses the IANA ID as text. + private readonly DateTimeZoneMapping _timeZone = new("text"); + // Built-in ranges private readonly NpgsqlRangeTypeMapping _timestampLocalDateTimeRange; private readonly NpgsqlRangeTypeMapping _legacyTimestampInstantRange; @@ -199,6 +202,7 @@ public NpgsqlNodaTimeTypeMappingSourcePlugin(ISqlGenerationHelper sqlGenerationH { typeof(OffsetTime), _timetz }, { typeof(Period), _periodInterval }, { typeof(Duration), _durationInterval }, + // See DateTimeZone below { typeof(NpgsqlRange), LegacyTimestampBehavior ? _legacyTimestampInstantRange : _timestamptzInstantRange }, { typeof(NpgsqlRange), _timestampLocalDateTimeRange }, @@ -290,9 +294,20 @@ public NpgsqlNodaTimeTypeMappingSourcePlugin(ISqlGenerationHelper sqlGenerationH } } - return clrType is not null && ClrTypeMappings.TryGetValue(clrType, out var mapping) - ? mapping - : null; + if (clrType is not null) + { + if (ClrTypeMappings.TryGetValue(clrType, out var mapping)) + { + return mapping; + } + + if (clrType.IsAssignableTo(typeof(DateTimeZone))) + { + return _timeZone; + } + } + + return null; } /// diff --git a/src/EFCore.PG/Query/Internal/NpgsqlSqlTranslatingExpressionVisitor.cs b/src/EFCore.PG/Query/Internal/NpgsqlSqlTranslatingExpressionVisitor.cs index c5f4cecad2e9418e5f1d577433e279dd7b9f5e1e..9bc1c887db0859c80a7f02bee9c308401ab75427 100644 --- a/src/EFCore.PG/Query/Internal/NpgsqlSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.PG/Query/Internal/NpgsqlSqlTranslatingExpressionVisitor.cs @@ -30,22 +30,9 @@ public class NpgsqlSqlTranslatingExpressionVisitor : RelationalSqlTranslatingExp private static readonly ConstructorInfo DateOnlyCtor = typeof(DateOnly).GetConstructor(new[] { typeof(int), typeof(int), typeof(int) })!; - private static readonly MethodInfo Like2MethodInfo = - typeof(DbFunctionsExtensions).GetRuntimeMethod( - nameof(DbFunctionsExtensions.Like), new[] { typeof(DbFunctions), typeof(string), typeof(string) })!; - - // ReSharper disable once InconsistentNaming - private static readonly MethodInfo ILike2MethodInfo - = typeof(NpgsqlDbFunctionsExtensions).GetRuntimeMethod( - nameof(NpgsqlDbFunctionsExtensions.ILike), new[] { typeof(DbFunctions), typeof(string), typeof(string) })!; - - private static readonly MethodInfo ObjectEquals - = typeof(object).GetRuntimeMethod(nameof(object.Equals), new[] { typeof(object), typeof(object) })!; - private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; private readonly IRelationalTypeMappingSource _typeMappingSource; private readonly NpgsqlJsonPocoTranslator _jsonPocoTranslator; - private readonly NpgsqlLTreeTranslator _ltreeTranslator; private readonly RelationalTypeMapping _timestampMapping; private readonly RelationalTypeMapping _timestampTzMapping; @@ -66,7 +53,6 @@ public class NpgsqlSqlTranslatingExpressionVisitor : RelationalSqlTranslatingExp { _sqlExpressionFactory = (NpgsqlSqlExpressionFactory)dependencies.SqlExpressionFactory; _jsonPocoTranslator = ((NpgsqlMemberTranslatorProvider)Dependencies.MemberTranslatorProvider).JsonPocoTranslator; - _ltreeTranslator = ((NpgsqlMethodCallTranslatorProvider)Dependencies.MethodCallTranslatorProvider).LTreeTranslator; _typeMappingSource = dependencies.TypeMappingSource; _timestampMapping = _typeMappingSource.FindMapping("timestamp without time zone")!; _timestampTzMapping = _typeMappingSource.FindMapping("timestamp with time zone")!; diff --git a/test/EFCore.PG.NodaTime.FunctionalTests/NodaTimeQueryNpgsqlTest.cs b/test/EFCore.PG.NodaTime.FunctionalTests/NodaTimeQueryNpgsqlTest.cs index 4d56af53d11f1a3420b9c5ccd43212a50a53a358..36ccd3571ac25cee9c119763f90177de184341d5 100644 --- a/test/EFCore.PG.NodaTime.FunctionalTests/NodaTimeQueryNpgsqlTest.cs +++ b/test/EFCore.PG.NodaTime.FunctionalTests/NodaTimeQueryNpgsqlTest.cs @@ -1540,7 +1540,7 @@ public async Task Instance_InUtc(bool async) [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public async Task Instance_InZone_LocalDateTime(bool async) + public async Task Instance_InZone_constant_LocalDateTime(bool async) { await AssertQuery( async, @@ -1558,7 +1558,7 @@ public async Task Instance_InZone_LocalDateTime(bool async) [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public async Task Instance_InZone_Date(bool async) + public async Task Instance_InZone_constant_Date(bool async) { await AssertQuery( async, @@ -1574,6 +1574,28 @@ public async Task Instance_InZone_Date(bool async) """); } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Instance_InZone_parameter_LocalDateTime(bool async) + { + var timeZone = DateTimeZoneProviders.Tzdb["Europe/Berlin"]; + + await AssertQuery( + async, + ss => ss.Set().Where(t => t.Instant.InZone(timeZone).LocalDateTime + == new LocalDateTime(2018, 4, 20, 12, 31, 33, 666)), + entryCount: 1); + + AssertSql( +""" +@__timeZone_0='Europe/Berlin' + +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE n."Instant" AT TIME ZONE @__timeZone_0 = TIMESTAMP '2018-04-20T12:31:33.666' +"""); + } + [ConditionalFact] public async Task Instance_InZone_without_LocalDateTime_fails() { diff --git a/test/EFCore.PG.NodaTime.FunctionalTests/NpgsqlNodaTimeTypeMappingTest.cs b/test/EFCore.PG.NodaTime.FunctionalTests/NpgsqlNodaTimeTypeMappingTest.cs index fbbc521389fb1102d0ea85dc6e23fc12ac4e5bfc..998be06b9381dac3dd11adc19ea6260bd72807a7 100644 --- a/test/EFCore.PG.NodaTime.FunctionalTests/NpgsqlNodaTimeTypeMappingTest.cs +++ b/test/EFCore.PG.NodaTime.FunctionalTests/NpgsqlNodaTimeTypeMappingTest.cs @@ -589,6 +589,33 @@ public void GenerateCodeLiteral_returns_Duration_literal() #endregion interval + #region DateTimeZone + + [Fact] + public void DateTimeZone_is_properly_mapped() + { + var mapping = GetMapping(typeof(DateTimeZone)); + + Assert.Same(typeof(DateTimeZone), mapping.ClrType); + Assert.Equal("text", mapping.StoreType); + } + + [Fact] + public void GenerateSqlLiteral_returns_DateTimeZone_literal() + { + var mapping = GetMapping(typeof(DateTimeZone)); + + Assert.Equal("Europe/Berlin", mapping.GenerateSqlLiteral(DateTimeZoneProviders.Tzdb["Europe/Berlin"])); + } + + [Fact] + public void GenerateCodeLiteral_returns_DateTimezone_literal() + => Assert.Equal( + """NodaTime.DateTimeZoneProviders.Tzdb.GetZoneOrNull("Europe/Berlin")""", + CodeLiteral(DateTimeZoneProviders.Tzdb["Europe/Berlin"])); + + #endregion + #region Support private static readonly NpgsqlTypeMappingSource Mapper = new(