diff --git a/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerTimeOnlyMethodTranslator.cs b/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerTimeOnlyMethodTranslator.cs index 9e45428e27f..08f3a172360 100644 --- a/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerTimeOnlyMethodTranslator.cs +++ b/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerTimeOnlyMethodTranslator.cs @@ -76,13 +76,29 @@ public class SqlServerTimeOnlyMethodTranslator(ISqlExpressionFactory sqlExpressi var typeMapping = ExpressionExtensions.InferTypeMapping(instance, arguments[0], arguments[1]); instance = sqlExpressionFactory.ApplyTypeMapping(instance, typeMapping); - return sqlExpressionFactory.And( - sqlExpressionFactory.GreaterThanOrEqual( - instance, - sqlExpressionFactory.ApplyTypeMapping(arguments[0], typeMapping)), - sqlExpressionFactory.LessThan( - instance, - sqlExpressionFactory.ApplyTypeMapping(arguments[1], typeMapping))); + var start = sqlExpressionFactory.ApplyTypeMapping(arguments[0], typeMapping); + var end = sqlExpressionFactory.ApplyTypeMapping(arguments[1], typeMapping); + + var isAfterStart = sqlExpressionFactory.GreaterThanOrEqual(instance, start); + var isBeforeEnd = sqlExpressionFactory.LessThan(instance, end); + + // A range whose start is after its end wraps around midnight, and matches the times that are either after + // the start or before the end. + if (arguments is [SqlConstantExpression { Value: TimeOnly startValue }, SqlConstantExpression { Value: TimeOnly endValue }]) + { + return startValue > endValue + ? sqlExpressionFactory.OrElse(isAfterStart, isBeforeEnd) + : sqlExpressionFactory.AndAlso(isAfterStart, isBeforeEnd); + } + + // The bounds aren't known when translating, so both cases have to be handled in the SQL. + return sqlExpressionFactory.OrElse( + sqlExpressionFactory.AndAlso( + sqlExpressionFactory.LessThanOrEqual(start, end), + sqlExpressionFactory.AndAlso(isAfterStart, isBeforeEnd)), + sqlExpressionFactory.AndAlso( + sqlExpressionFactory.GreaterThan(start, end), + sqlExpressionFactory.OrElse(isAfterStart, isBeforeEnd))); } return null; diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/Translations/Temporal/TimeOnlyTranslationsSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/Translations/Temporal/TimeOnlyTranslationsSqlServerTest.cs index d34bf24a762..9d2b5359568 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/Translations/Temporal/TimeOnlyTranslationsSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/Translations/Temporal/TimeOnlyTranslationsSqlServerTest.cs @@ -125,13 +125,59 @@ public override async Task IsBetween() """ SELECT [b].[Id], [b].[Bool], [b].[Byte], [b].[ByteArray], [b].[DateOnly], [b].[DateTime], [b].[DateTimeOffset], [b].[Decimal], [b].[Double], [b].[Enum], [b].[FlagsEnum], [b].[Float], [b].[Guid], [b].[Int], [b].[Long], [b].[Short], [b].[String], [b].[TimeOnly], [b].[TimeSpan] FROM [BasicTypesEntities] AS [b] -WHERE CASE - WHEN [b].[TimeOnly] >= '14:00:00' THEN CAST(1 AS bit) - ELSE CAST(0 AS bit) -END & CASE - WHEN [b].[TimeOnly] < '16:00:00' THEN CAST(1 AS bit) - ELSE CAST(0 AS bit) -END = CAST(1 AS bit) +WHERE [b].[TimeOnly] >= '14:00:00' AND [b].[TimeOnly] < '16:00:00' +"""); + } + + [Fact] + public virtual async Task IsBetween_spanning_midnight_with_parameters() + { + var start = new TimeOnly(16, 0, 0); + var end = new TimeOnly(12, 0, 0); + + await AssertQuery(ss => ss.Set().Where(b => b.TimeOnly.IsBetween(start, end))); + + AssertSql( + """ +@start='16:00' (DbType = Time) +@end='12:00' (DbType = Time) + +SELECT [b].[Id], [b].[Bool], [b].[Byte], [b].[ByteArray], [b].[DateOnly], [b].[DateTime], [b].[DateTimeOffset], [b].[Decimal], [b].[Double], [b].[Enum], [b].[FlagsEnum], [b].[Float], [b].[Guid], [b].[Int], [b].[Long], [b].[Short], [b].[String], [b].[TimeOnly], [b].[TimeSpan] +FROM [BasicTypesEntities] AS [b] +WHERE (@start <= @end AND [b].[TimeOnly] >= @start AND [b].[TimeOnly] < @end) OR (@start > @end AND ([b].[TimeOnly] >= @start OR [b].[TimeOnly] < @end)) +"""); + } + + [Fact] + public virtual async Task IsBetween_with_parameters() + { + var start = new TimeOnly(14, 0, 0); + var end = new TimeOnly(16, 0, 0); + + await AssertQuery(ss => ss.Set().Where(b => b.TimeOnly.IsBetween(start, end))); + + AssertSql( + """ +@start='14:00' (DbType = Time) +@end='16:00' (DbType = Time) + +SELECT [b].[Id], [b].[Bool], [b].[Byte], [b].[ByteArray], [b].[DateOnly], [b].[DateTime], [b].[DateTimeOffset], [b].[Decimal], [b].[Double], [b].[Enum], [b].[FlagsEnum], [b].[Float], [b].[Guid], [b].[Int], [b].[Long], [b].[Short], [b].[String], [b].[TimeOnly], [b].[TimeSpan] +FROM [BasicTypesEntities] AS [b] +WHERE (@start <= @end AND [b].[TimeOnly] >= @start AND [b].[TimeOnly] < @end) OR (@start > @end AND ([b].[TimeOnly] >= @start OR [b].[TimeOnly] < @end)) +"""); + } + + [Fact] + public virtual async Task IsBetween_spanning_midnight() + { + await AssertQuery( + ss => ss.Set().Where(b => b.TimeOnly.IsBetween(new TimeOnly(16, 0, 0), new TimeOnly(12, 0, 0)))); + + AssertSql( + """ +SELECT [b].[Id], [b].[Bool], [b].[Byte], [b].[ByteArray], [b].[DateOnly], [b].[DateTime], [b].[DateTimeOffset], [b].[Decimal], [b].[Double], [b].[Enum], [b].[FlagsEnum], [b].[Float], [b].[Guid], [b].[Int], [b].[Long], [b].[Short], [b].[String], [b].[TimeOnly], [b].[TimeSpan] +FROM [BasicTypesEntities] AS [b] +WHERE [b].[TimeOnly] >= '16:00:00' OR [b].[TimeOnly] < '12:00:00' """); }