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(