diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerMemberTranslatorProvider.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerMemberTranslatorProvider.cs index 96298d97f55..4004877da20 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerMemberTranslatorProvider.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerMemberTranslatorProvider.cs @@ -17,7 +17,8 @@ public SqlServerMemberTranslatorProvider([NotNull] RelationalMemberTranslatorPro new IMemberTranslator[] { new SqlServerDateTimeMemberTranslator(sqlExpressionFactory), - new SqlServerStringMemberTranslator(sqlExpressionFactory) + new SqlServerStringMemberTranslator(sqlExpressionFactory), + new SqlServerTimeSpanMemberTranslator(sqlExpressionFactory) }); } } diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerTimeSpanMemberTranslator.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerTimeSpanMemberTranslator.cs new file mode 100644 index 00000000000..ff47dd1e20f --- /dev/null +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerTimeSpanMemberTranslator.cs @@ -0,0 +1,48 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Reflection; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal +{ + public class SqlServerTimeSpanMemberTranslator : IMemberTranslator + { + private static readonly Dictionary _datePartMappings = new Dictionary + { + { nameof(TimeSpan.Hours), "hour" }, + { nameof(TimeSpan.Minutes), "minute" }, + { nameof(TimeSpan.Seconds), "second" }, + { nameof(TimeSpan.Milliseconds), "millisecond" } + }; + + private readonly ISqlExpressionFactory _sqlExpressionFactory; + + public SqlServerTimeSpanMemberTranslator([NotNull] ISqlExpressionFactory sqlExpressionFactory) + { + _sqlExpressionFactory = sqlExpressionFactory; + } + + public virtual SqlExpression Translate(SqlExpression instance, MemberInfo member, Type returnType) + { + Check.NotNull(member, nameof(member)); + Check.NotNull(returnType, nameof(returnType)); + + if (member.DeclaringType == typeof(TimeSpan) && _datePartMappings.TryGetValue(member.Name, out string value)) + { + return _sqlExpressionFactory.Function("DATEPART", new [] + { + _sqlExpressionFactory.Fragment(value), + instance + }, returnType); + } + + return null; + } + } +} diff --git a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs index b82f86a7232..9e893edb35c 100644 --- a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs @@ -7533,6 +7533,86 @@ public virtual Task Checked_context_throws_on_client_evaluation(bool isAsync) private int GetThreatLevel() => 256; + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task TimeSpan_Hours(bool async) + { + return AssertQueryScalar( + async, + ss => ss.Set() + .Select(m => m.Duration.Hours)); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task TimeSpan_Minutes(bool async) + { + return AssertQueryScalar( + async, + ss => ss.Set() + .Select(m => m.Duration.Minutes)); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task TimeSpan_Seconds(bool async) + { + return AssertQueryScalar( + async, + ss => ss.Set() + .Select(m => m.Duration.Seconds)); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task TimeSpan_Milliseconds(bool async) + { + return AssertQueryScalar( + async, + ss => ss.Set() + .Select(m => m.Duration.Milliseconds)); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Where_TimeSpan_Hours(bool async) + { + return AssertQuery( + async, + ss => ss.Set() + .Where(m => m.Duration.Hours == 1)); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Where_TimeSpan_Minutes(bool async) + { + return AssertQuery( + async, + ss => ss.Set() + .Where(m => m.Duration.Minutes == 1)); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Where_TimeSpan_Seconds(bool async) + { + return AssertQuery( + async, + ss => ss.Set() + .Where(m => m.Duration.Seconds == 1)); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Where_TimeSpan_Milliseconds(bool async) + { + return AssertQuery( + async, + ss => ss.Set() + .Where(m => m.Duration.Milliseconds == 1)); + } + protected GearsOfWarContext CreateContext() => Fixture.CreateContext(); protected virtual void ClearLog() diff --git a/test/EFCore.Specification.Tests/TestModels/GearsOfWarModel/GearsOfWarData.cs b/test/EFCore.Specification.Tests/TestModels/GearsOfWarModel/GearsOfWarData.cs index 5ccaa8dbc6a..9cb8363e760 100644 --- a/test/EFCore.Specification.Tests/TestModels/GearsOfWarModel/GearsOfWarData.cs +++ b/test/EFCore.Specification.Tests/TestModels/GearsOfWarModel/GearsOfWarData.cs @@ -119,21 +119,24 @@ public static IReadOnlyList CreateMissions() Id = 1, CodeName = "Lightmass Offensive", Rating = 2.1, - Timeline = new DateTimeOffset(599898024001234567, new TimeSpan(1, 30, 0)) + Timeline = new DateTimeOffset(599898024001234567, new TimeSpan(1, 30, 0)), + Duration = new TimeSpan(1, 2, 3) }, new Mission { Id = 2, CodeName = "Hollow Storm", Rating = 4.2, - Timeline = new DateTimeOffset(2, 3, 1, 8, 0, 0, new TimeSpan(-5, 0, 0)) + Timeline = new DateTimeOffset(2, 3, 1, 8, 0, 0, new TimeSpan(-5, 0, 0)), + Duration = new TimeSpan(0, 1, 2, 3, 456) }, new Mission { Id = 3, CodeName = "Halvo Bay defense", Rating = null, - Timeline = new DateTimeOffset(10, 5, 3, 12, 0, 0, new TimeSpan()) + Timeline = new DateTimeOffset(10, 5, 3, 12, 0, 0, new TimeSpan()), + Duration = new TimeSpan(0, 1, 0, 15, 456) } }; diff --git a/test/EFCore.Specification.Tests/TestModels/GearsOfWarModel/Mission.cs b/test/EFCore.Specification.Tests/TestModels/GearsOfWarModel/Mission.cs index a2240b0a812..24cd8a53ed2 100644 --- a/test/EFCore.Specification.Tests/TestModels/GearsOfWarModel/Mission.cs +++ b/test/EFCore.Specification.Tests/TestModels/GearsOfWarModel/Mission.cs @@ -13,6 +13,7 @@ public class Mission public string CodeName { get; set; } public double? Rating { get; set; } public DateTimeOffset Timeline { get; set; } + public TimeSpan Duration { get; set; } public virtual ICollection ParticipatingSquads { get; set; } } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs index 4248a15e1b1..2b21720fcbf 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs @@ -2626,7 +2626,7 @@ public override async Task Where_datetimeoffset_now(bool async) await base.Where_datetimeoffset_now(async); AssertSql( - @"SELECT [m].[Id], [m].[CodeName], [m].[Rating], [m].[Timeline] + @"SELECT [m].[Id], [m].[CodeName], [m].[Duration], [m].[Rating], [m].[Timeline] FROM [Missions] AS [m] WHERE [m].[Timeline] <> SYSDATETIMEOFFSET()"); } @@ -2636,7 +2636,7 @@ public override async Task Where_datetimeoffset_utcnow(bool async) await base.Where_datetimeoffset_utcnow(async); AssertSql( - @"SELECT [m].[Id], [m].[CodeName], [m].[Rating], [m].[Timeline] + @"SELECT [m].[Id], [m].[CodeName], [m].[Duration], [m].[Rating], [m].[Timeline] FROM [Missions] AS [m] WHERE [m].[Timeline] <> CAST(SYSUTCDATETIME() AS datetimeoffset)"); } @@ -2657,7 +2657,7 @@ public override async Task Where_datetimeoffset_year_component(bool async) await base.Where_datetimeoffset_year_component(async); AssertSql( - @"SELECT [m].[Id], [m].[CodeName], [m].[Rating], [m].[Timeline] + @"SELECT [m].[Id], [m].[CodeName], [m].[Duration], [m].[Rating], [m].[Timeline] FROM [Missions] AS [m] WHERE DATEPART(year, [m].[Timeline]) = 2"); } @@ -2667,7 +2667,7 @@ public override async Task Where_datetimeoffset_month_component(bool async) await base.Where_datetimeoffset_month_component(async); AssertSql( - @"SELECT [m].[Id], [m].[CodeName], [m].[Rating], [m].[Timeline] + @"SELECT [m].[Id], [m].[CodeName], [m].[Duration], [m].[Rating], [m].[Timeline] FROM [Missions] AS [m] WHERE DATEPART(month, [m].[Timeline]) = 1"); } @@ -2677,7 +2677,7 @@ public override async Task Where_datetimeoffset_dayofyear_component(bool async) await base.Where_datetimeoffset_dayofyear_component(async); AssertSql( - @"SELECT [m].[Id], [m].[CodeName], [m].[Rating], [m].[Timeline] + @"SELECT [m].[Id], [m].[CodeName], [m].[Duration], [m].[Rating], [m].[Timeline] FROM [Missions] AS [m] WHERE DATEPART(dayofyear, [m].[Timeline]) = 2"); } @@ -2687,7 +2687,7 @@ public override async Task Where_datetimeoffset_day_component(bool async) await base.Where_datetimeoffset_day_component(async); AssertSql( - @"SELECT [m].[Id], [m].[CodeName], [m].[Rating], [m].[Timeline] + @"SELECT [m].[Id], [m].[CodeName], [m].[Duration], [m].[Rating], [m].[Timeline] FROM [Missions] AS [m] WHERE DATEPART(day, [m].[Timeline]) = 2"); } @@ -2697,7 +2697,7 @@ public override async Task Where_datetimeoffset_hour_component(bool async) await base.Where_datetimeoffset_hour_component(async); AssertSql( - @"SELECT [m].[Id], [m].[CodeName], [m].[Rating], [m].[Timeline] + @"SELECT [m].[Id], [m].[CodeName], [m].[Duration], [m].[Rating], [m].[Timeline] FROM [Missions] AS [m] WHERE DATEPART(hour, [m].[Timeline]) = 10"); } @@ -2707,7 +2707,7 @@ public override async Task Where_datetimeoffset_minute_component(bool async) await base.Where_datetimeoffset_minute_component(async); AssertSql( - @"SELECT [m].[Id], [m].[CodeName], [m].[Rating], [m].[Timeline] + @"SELECT [m].[Id], [m].[CodeName], [m].[Duration], [m].[Rating], [m].[Timeline] FROM [Missions] AS [m] WHERE DATEPART(minute, [m].[Timeline]) = 0"); } @@ -2717,7 +2717,7 @@ public override async Task Where_datetimeoffset_second_component(bool async) await base.Where_datetimeoffset_second_component(async); AssertSql( - @"SELECT [m].[Id], [m].[CodeName], [m].[Rating], [m].[Timeline] + @"SELECT [m].[Id], [m].[CodeName], [m].[Duration], [m].[Rating], [m].[Timeline] FROM [Missions] AS [m] WHERE DATEPART(second, [m].[Timeline]) = 0"); } @@ -2727,7 +2727,7 @@ public override async Task Where_datetimeoffset_millisecond_component(bool async await base.Where_datetimeoffset_millisecond_component(async); AssertSql( - @"SELECT [m].[Id], [m].[CodeName], [m].[Rating], [m].[Timeline] + @"SELECT [m].[Id], [m].[CodeName], [m].[Duration], [m].[Rating], [m].[Timeline] FROM [Missions] AS [m] WHERE DATEPART(millisecond, [m].[Timeline]) = 0"); } @@ -6548,7 +6548,7 @@ public override async Task DateTimeOffset_Contains_Less_than_Greater_than(bool a @"@__start_0='1902-01-01T10:00:00.1234567+01:30' @__end_1='1902-01-03T10:00:00.1234567+01:30' -SELECT [m].[Id], [m].[CodeName], [m].[Rating], [m].[Timeline] +SELECT [m].[Id], [m].[CodeName], [m].[Duration], [m].[Rating], [m].[Timeline] FROM [Missions] AS [m] WHERE ((@__start_0 <= CAST(CONVERT(date, [m].[Timeline]) AS datetimeoffset)) AND ([m].[Timeline] < @__end_1)) AND [m].[Timeline] IN ('1902-01-02T10:00:00.1234567+01:30')"); } @@ -7400,7 +7400,7 @@ public override async Task DateTimeOffset_Date_returns_datetime(bool async) AssertSql( @"@__dateTimeOffset_Date_0='0002-03-01T00:00:00' -SELECT [m].[Id], [m].[CodeName], [m].[Rating], [m].[Timeline] +SELECT [m].[Id], [m].[CodeName], [m].[Duration], [m].[Rating], [m].[Timeline] FROM [Missions] AS [m] WHERE CONVERT(date, [m].[Timeline]) >= @__dateTimeOffset_Date_0"); } @@ -7495,6 +7495,82 @@ FROM [LocustLeaders] AS [l] WHERE [l].[Discriminator] IN (N'LocustLeader', N'LocustCommander') AND (CAST([l].[ThreatLevel] AS bigint) >= (CAST(5 AS bigint) + CAST([l].[ThreatLevel] AS bigint)))"); } + public override async Task TimeSpan_Hours(bool async) + { + await base.TimeSpan_Hours(async); + + AssertSql( + @"SELECT DATEPART(hour, [m].[Duration]) +FROM [Missions] AS [m]"); + } + + public override async Task TimeSpan_Minutes(bool async) + { + await base.TimeSpan_Minutes(async); + + AssertSql( + @"SELECT DATEPART(minute, [m].[Duration]) +FROM [Missions] AS [m]"); + } + + public override async Task TimeSpan_Seconds(bool async) + { + await base.TimeSpan_Seconds(async); + + AssertSql( + @"SELECT DATEPART(second, [m].[Duration]) +FROM [Missions] AS [m]"); + } + + public override async Task TimeSpan_Milliseconds(bool async) + { + await base.TimeSpan_Milliseconds(async); + + AssertSql( + @"SELECT DATEPART(millisecond, [m].[Duration]) +FROM [Missions] AS [m]"); + } + + public override async Task Where_TimeSpan_Hours(bool async) + { + await base.Where_TimeSpan_Hours(async); + + AssertSql( + @"SELECT [m].[Id], [m].[CodeName], [m].[Duration], [m].[Rating], [m].[Timeline] +FROM [Missions] AS [m] +WHERE DATEPART(hour, [m].[Duration]) = 1"); + } + + public override async Task Where_TimeSpan_Minutes(bool async) + { + await base.Where_TimeSpan_Minutes(async); + + AssertSql( + @"SELECT [m].[Id], [m].[CodeName], [m].[Duration], [m].[Rating], [m].[Timeline] +FROM [Missions] AS [m] +WHERE DATEPART(minute, [m].[Duration]) = 1"); + } + + public override async Task Where_TimeSpan_Seconds(bool async) + { + await base.Where_TimeSpan_Seconds(async); + + AssertSql( + @"SELECT [m].[Id], [m].[CodeName], [m].[Duration], [m].[Rating], [m].[Timeline] +FROM [Missions] AS [m] +WHERE DATEPART(second, [m].[Duration]) = 1"); + } + + public override async Task Where_TimeSpan_Milliseconds(bool async) + { + await base.Where_TimeSpan_Milliseconds(async); + + AssertSql( + @"SELECT [m].[Id], [m].[CodeName], [m].[Duration], [m].[Rating], [m].[Timeline] +FROM [Missions] AS [m] +WHERE DATEPART(millisecond, [m].[Duration]) = 1"); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); } diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs index 5625d5bcf95..1cf70165549 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs @@ -148,6 +148,30 @@ public override async Task Byte_array_filter_by_SequenceEqual(bool async) WHERE ""s"".""Banner5"" = @__byteArrayParam_0"); } + [ConditionalTheory(Skip = "PR #19774")] + public override Task TimeSpan_Hours(bool async) => base.TimeSpan_Hours(async); + + [ConditionalTheory(Skip = "PR #19774")] + public override Task TimeSpan_Minutes(bool async) => base.TimeSpan_Minutes(async); + + [ConditionalTheory(Skip = "PR #19774")] + public override Task TimeSpan_Seconds(bool async) => base.TimeSpan_Seconds(async); + + [ConditionalTheory(Skip = "PR #19774")] + public override Task TimeSpan_Milliseconds(bool async) => base.TimeSpan_Milliseconds(async); + + [ConditionalTheory(Skip = "PR #19774")] + public override Task Where_TimeSpan_Hours(bool async) => base.Where_TimeSpan_Hours(async); + + [ConditionalTheory(Skip = "PR #19774")] + public override Task Where_TimeSpan_Minutes(bool async) => base.Where_TimeSpan_Minutes(async); + + [ConditionalTheory(Skip = "PR #19774")] + public override Task Where_TimeSpan_Seconds(bool async) => base.Where_TimeSpan_Seconds(async); + + [ConditionalTheory(Skip = "PR #19774")] + public override Task Where_TimeSpan_Milliseconds(bool async) => base.Where_TimeSpan_Milliseconds(async); + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); }