Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -316,37 +316,41 @@ public class NpgsqlDateTimeMethodTranslator(
MethodInfo method,
IReadOnlyList<SqlExpression> arguments)
{
if (method.DeclaringType == typeof(TimeZoneInfo) && method.Name == nameof(TimeZoneInfo.ConvertTimeBySystemTimeZoneId)
&& arguments is [var convertDateTime, var timeZoneId]
&& convertDateTime.Type == typeof(DateTime))
if (method.DeclaringType != typeof(TimeZoneInfo))
{
var typeMapping = convertDateTime.TypeMapping;
if (typeMapping is null
|| (typeMapping.StoreType != "timestamp with time zone" && typeMapping.StoreType != "timestamptz"))
{
throw new InvalidOperationException(
"TimeZoneInfo.ConvertTimeBySystemTimeZoneId is only supported on columns with type 'timestamp with time zone'");
}

return _sqlExpressionFactory.AtTimeZone(convertDateTime, timeZoneId, typeof(DateTime), _timestampMapping);
return null;
}

if (method.DeclaringType == typeof(TimeZoneInfo) && method.Name == nameof(TimeZoneInfo.ConvertTimeToUtc)
&& arguments is [var utcDateTime]
&& utcDateTime.Type == typeof(DateTime))
return (method.Name, arguments) switch
{
var typeMapping = utcDateTime.TypeMapping;
if (typeMapping is null
|| (typeMapping.StoreType != "timestamp without time zone" && typeMapping.StoreType != "timestamp"))
{
throw new InvalidOperationException(
"TimeZoneInfo.ConvertTimeToUtc) is only supported on columns with type 'timestamp without time zone'");
}
(nameof(TimeZoneInfo.ConvertTimeBySystemTimeZoneId), [var convertDateTime, var timeZoneId])
when convertDateTime.Type == typeof(DateTime)
=> convertDateTime.TypeMapping?.StoreType is "timestamp with time zone" or "timestamptz"
? _sqlExpressionFactory.AtTimeZone(convertDateTime, timeZoneId, typeof(DateTime), _timestampMapping)
: throw new InvalidOperationException(
"TimeZoneInfo.ConvertTimeBySystemTimeZoneId is only supported on columns with type 'timestamp with time zone'"),

// As with the two-parameter overload, return a timestamp even when the destination is UTC.
(nameof(TimeZoneInfo.ConvertTimeBySystemTimeZoneId), [var sourceDateTime, var sourceTimeZoneId, var destinationTimeZoneId])
when sourceDateTime.Type == typeof(DateTime)
=> sourceDateTime.TypeMapping?.StoreTypeNameBase is "timestamp without time zone" or "timestamp"
? _sqlExpressionFactory.AtTimeZone(
_sqlExpressionFactory.AtTimeZone(sourceDateTime, sourceTimeZoneId, typeof(DateTime), _timestampTzMapping),
destinationTimeZoneId,
typeof(DateTime),
_timestampMapping)
: throw new InvalidOperationException(
"TimeZoneInfo.ConvertTimeBySystemTimeZoneId is only supported on columns with type 'timestamp without time zone'"),

(nameof(TimeZoneInfo.ConvertTimeToUtc), [var utcDateTime])
when utcDateTime.Type == typeof(DateTime)
=> utcDateTime.TypeMapping?.StoreType is "timestamp without time zone" or "timestamp"
? _sqlExpressionFactory.Convert(utcDateTime, utcDateTime.Type, _timestampTzMapping)
: throw new InvalidOperationException(
"TimeZoneInfo.ConvertTimeToUtc is only supported on columns with type 'timestamp without time zone'"),

return _sqlExpressionFactory.Convert(utcDateTime, utcDateTime.Type, _timestampTzMapping);
}

return null;
_ => null
};
}

private SqlExpression? CreateIntervalExpression(SqlExpression intervalNum, string datePart)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,140 @@ await Assert.ThrowsAsync<InvalidOperationException>(
== new DateTime(1998, 4, 12, 15, 26, 38)).ToListAsync());
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public async Task Where_ConvertTimeBySystemTimeZoneId_with_source_time_zone_on_DateTime_timestamp_column(bool async)
{
await AssertQuery(
async,
ss => ss.Set<Entity>().Where(
e => TimeZoneInfo.ConvertTimeBySystemTimeZoneId(e.TimestampDateTime, "Europe/Berlin", "UTC")
== new DateTime(1998, 4, 12, 13, 26, 38)),
expectedQuery: ss => ss.Set<Entity>().Where(
e => TimeZoneInfo.ConvertTimeBySystemTimeZoneId(
DateTime.SpecifyKind(e.TimestampDateTime, DateTimeKind.Unspecified), "Europe/Berlin", "UTC")
== new DateTime(1998, 4, 12, 13, 26, 38)));

AssertSql(
"""
SELECT e."Id", e."TimestampDateTime", e."TimestampDateTimeArray", e."TimestampDateTimeOffset", e."TimestampDateTimeOffsetArray", e."TimestampDateTimeRange", e."TimestamptzDateTime", e."TimestamptzDateTimeArray", e."TimestamptzDateTimeRange"
FROM "Entities" AS e
WHERE (e."TimestampDateTime" AT TIME ZONE 'Europe/Berlin') AT TIME ZONE 'UTC' = TIMESTAMP '1998-04-12T13:26:38'
""");
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public async Task ConvertTimeBySystemTimeZoneId_with_source_time_zone_and_SpecifyKind_to_utc(bool async)
{
await using var ctx = CreateContext();

var query = ctx.Entities.Where(e => e.Id == 1)
.Select(e => DateTime.SpecifyKind(
TimeZoneInfo.ConvertTimeBySystemTimeZoneId(e.TimestampDateTime, "Europe/Berlin", "UTC"), DateTimeKind.Utc));

var result = async ? await query.SingleAsync() : query.Single();

Assert.Equal(new DateTime(1998, 4, 12, 13, 26, 38, DateTimeKind.Utc), result);
Assert.Equal(DateTimeKind.Utc, result.Kind);

AssertSql(
"""
SELECT ((e."TimestampDateTime" AT TIME ZONE 'Europe/Berlin') AT TIME ZONE 'UTC') AT TIME ZONE 'UTC'
FROM "Entities" AS e
WHERE e."Id" = 1
LIMIT 2
""");
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public async Task ConvertTimeBySystemTimeZoneId_with_source_and_destination_time_zone_parameters(bool async)
{
await using var ctx = CreateContext();

var sourceTimeZoneId = "Europe/Berlin";
var destinationTimeZoneId = "Asia/Tokyo";
var query = ctx.Entities.Where(e => e.Id == 1)
.Select(e => TimeZoneInfo.ConvertTimeBySystemTimeZoneId(e.TimestampDateTime, sourceTimeZoneId, destinationTimeZoneId));

var result = async ? await query.SingleAsync() : query.Single();

Assert.Equal(new DateTime(1998, 4, 12, 22, 26, 38), result);
Assert.Equal(DateTimeKind.Unspecified, result.Kind);

AssertSql(
"""
@sourceTimeZoneId='Europe/Berlin'
@destinationTimeZoneId='Asia/Tokyo'

SELECT (e."TimestampDateTime" AT TIME ZONE @sourceTimeZoneId) AT TIME ZONE @destinationTimeZoneId
FROM "Entities" AS e
WHERE e."Id" = 1
LIMIT 2
""");
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public async Task ConvertTimeBySystemTimeZoneId_with_source_and_destination_time_zone_columns(bool async)
{
await using var ctx = CreateContext();

var query = ctx.Database.SqlQueryRaw<TimeZoneConversion>(
"""
SELECT * FROM (VALUES
(1, TIMESTAMP '1998-04-12T15:26:38', 'Europe/Berlin', 'UTC'),
(2, TIMESTAMP '1998-04-12T15:26:38', 'America/New_York', 'Asia/Tokyo')
) AS v("Id", "LocalTimestamp", "SourceTimeZoneId", "DestinationTimeZoneId")
""")
.OrderBy(e => e.Id)
.Select(e => TimeZoneInfo.ConvertTimeBySystemTimeZoneId(e.LocalTimestamp, e.SourceTimeZoneId, e.DestinationTimeZoneId));

var results = async ? await query.ToListAsync() : query.ToList();

Assert.Equal([new DateTime(1998, 4, 12, 13, 26, 38), new DateTime(1998, 4, 13, 4, 26, 38)], results);
Assert.All(results, result => Assert.Equal(DateTimeKind.Unspecified, result.Kind));

AssertSql(
"""
SELECT (m."LocalTimestamp" AT TIME ZONE m."SourceTimeZoneId") AT TIME ZONE m."DestinationTimeZoneId"
FROM (
SELECT * FROM (VALUES
(1, TIMESTAMP '1998-04-12T15:26:38', 'Europe/Berlin', 'UTC'),
(2, TIMESTAMP '1998-04-12T15:26:38', 'America/New_York', 'Asia/Tokyo')
) AS v("Id", "LocalTimestamp", "SourceTimeZoneId", "DestinationTimeZoneId")
) AS m
ORDER BY m."Id" NULLS FIRST
""");
}

[ConditionalFact]
public void Where_ConvertTimeBySystemTimeZoneId_with_source_time_zone_fails_on_DateTime_timestamptz_column()
{
using var ctx = CreateContext();

var exception = Assert.Throws<InvalidOperationException>(
() => ctx.Entities.Where(
e => TimeZoneInfo.ConvertTimeBySystemTimeZoneId(e.TimestamptzDateTime, "UTC", "Europe/Berlin")
== new DateTime(1998, 4, 12, 15, 26, 38)).ToQueryString());

Assert.Equal(
"TimeZoneInfo.ConvertTimeBySystemTimeZoneId is only supported on columns with type 'timestamp without time zone'",
exception.Message);
}

[ConditionalFact]
public void Where_ConvertTimeBySystemTimeZoneId_fails_on_DateTimeOffset_column()
{
using var ctx = CreateContext();

Assert.Throws<InvalidOperationException>(
() => ctx.Entities.Where(
e => TimeZoneInfo.ConvertTimeBySystemTimeZoneId(e.TimestampDateTimeOffset, "Europe/Berlin")
== new DateTimeOffset(1998, 4, 12, 15, 26, 38, TimeSpan.FromHours(2))).ToQueryString());
}

[ConditionalFact]
public virtual async Task Where_ConvertTimeToUtc_on_DateTime_timestamp_column()
{
Expand Down Expand Up @@ -988,6 +1122,18 @@ public class Entity
public NpgsqlRange<DateTime> TimestampDateTimeRange { get; set; }
}

private class TimeZoneConversion
{
public int Id { get; set; }

[Column(TypeName = "timestamp(6) without time zone")]
public DateTime LocalTimestamp { get; set; }

public string SourceTimeZoneId { get; set; } = null!;

public string DestinationTimeZoneId { get; set; } = null!;
}

public class TimestampQueryFixture : QueryFixtureBase<TimestampQueryContext>, ITestSqlLoggerFactory
{
protected override string StoreName
Expand Down
Loading