From 4a24cab6defc0a0a7954bb783f9b96cf0b291c25 Mon Sep 17 00:00:00 2001 From: Smit Patel Date: Wed, 18 May 2022 11:41:40 -0700 Subject: [PATCH] SqlServer: Block arithmetic on date/time expressions when type mapping is not known Resolves #27028 --- ...qlServerSqlTranslatingExpressionVisitor.cs | 48 +++++++++++++++---- .../Query/GearsOfWarQueryTestBase.cs | 11 +++++ .../Query/GearsOfWarQuerySqlServerTest.cs | 3 ++ .../Query/TPCGearsOfWarQuerySqlServerTest.cs | 3 ++ .../Query/TPTGearsOfWarQuerySqlServerTest.cs | 3 ++ .../TemporalGearsOfWarQuerySqlServerTest.cs | 3 ++ .../Query/GearsOfWarQuerySqliteTest.cs | 3 ++ .../Query/TPCGearsOfWarQuerySqliteTest.cs | 3 ++ .../Query/TPTGearsOfWarQuerySqliteTest.cs | 3 ++ 9 files changed, 70 insertions(+), 10 deletions(-) diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs index 86d970b09fb..5aca4d68afe 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs @@ -13,7 +13,7 @@ namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal; /// public class SqlServerSqlTranslatingExpressionVisitor : RelationalSqlTranslatingExpressionVisitor { - private static readonly HashSet DateTimeDataTypes + private static readonly HashSet DateTimeDataTypes = new() { "time", @@ -23,6 +23,16 @@ private static readonly HashSet DateTimeDataTypes "datetimeoffset" }; + private static readonly HashSet DateTimeClrTypes + = new() + { + typeof(TimeOnly), + typeof(DateOnly), + typeof(TimeSpan), + typeof(DateTime), + typeof(DateTimeOffset) + }; + private static readonly HashSet ArithmeticOperatorTypes = new() { @@ -82,14 +92,32 @@ protected override Expression VisitBinary(BinaryExpression binaryExpression) } } - return !(base.VisitBinary(binaryExpression) is SqlExpression visitedExpression) - ? QueryCompilationContext.NotTranslatedExpression - : visitedExpression is SqlBinaryExpression sqlBinary - && ArithmeticOperatorTypes.Contains(sqlBinary.OperatorType) - && (DateTimeDataTypes.Contains(GetProviderType(sqlBinary.Left)) - || DateTimeDataTypes.Contains(GetProviderType(sqlBinary.Right))) - ? QueryCompilationContext.NotTranslatedExpression - : visitedExpression; + var visitedExpression = base.VisitBinary(binaryExpression); + + if (visitedExpression is SqlBinaryExpression sqlBinaryExpression + && ArithmeticOperatorTypes.Contains(sqlBinaryExpression.OperatorType)) + { + var inferredProviderType = GetProviderType(sqlBinaryExpression.Left) ?? GetProviderType(sqlBinaryExpression.Right); + if (inferredProviderType != null) + { + if (DateTimeDataTypes.Contains(inferredProviderType)) + { + return QueryCompilationContext.NotTranslatedExpression; + } + } + else + { + var leftType = sqlBinaryExpression.Left.Type; + var rightType = sqlBinaryExpression.Right.Type; + if (DateTimeClrTypes.Contains(leftType) + || DateTimeClrTypes.Contains(rightType)) + { + return QueryCompilationContext.NotTranslatedExpression; + } + } + } + + return visitedExpression; } /// @@ -117,7 +145,7 @@ protected override Expression VisitUnary(UnaryExpression unaryExpression) isBinaryMaxDataType ? typeof(long) : typeof(int)); return isBinaryMaxDataType - ? (Expression)Dependencies.SqlExpressionFactory.Convert(dataLengthSqlFunction, typeof(int)) + ? Dependencies.SqlExpressionFactory.Convert(dataLengthSqlFunction, typeof(int)) : dataLengthSqlFunction; } diff --git a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs index 6785b2ab707..1118d6e2d7e 100644 --- a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs @@ -5362,6 +5362,17 @@ public virtual Task DateTimeOffset_Contains_Less_than_Greater_than(bool async) m => start <= m.Timeline.Date && m.Timeline < end && dates.Contains(m.Timeline))); } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task DateTimeOffsetNow_minus_timespan(bool async) + { + var timeSpan = new TimeSpan(1000); + + return AssertQuery( + async, + ss => ss.Set().Where(e => e.Timeline > DateTimeOffset.Now - timeSpan)); + } + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Navigation_inside_interpolated_string_expanded(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs index b1e2f0a163b..a3ebf9f0952 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs @@ -5738,6 +5738,9 @@ FROM [Missions] AS [m] WHERE @__start_0 <= CAST(CONVERT(date, [m].[Timeline]) AS datetimeoffset) AND [m].[Timeline] < @__end_1 AND [m].[Timeline] = '1902-01-02T10:00:00.1234567+01:30'"); } + public override Task DateTimeOffsetNow_minus_timespan(bool async) + => AssertTranslationFailed(() => base.DateTimeOffsetNow_minus_timespan(async)); + public override async Task Navigation_inside_interpolated_string_expanded(bool async) { await base.Navigation_inside_interpolated_string_expanded(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs index 5f5ebfbd1bd..c6242e94d21 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs @@ -8165,6 +8165,9 @@ FROM [Missions] AS [m] WHERE @__start_0 <= CAST(CONVERT(date, [m].[Timeline]) AS datetimeoffset) AND [m].[Timeline] < @__end_1 AND [m].[Timeline] = '1902-01-02T10:00:00.1234567+01:30'"); } + public override Task DateTimeOffsetNow_minus_timespan(bool async) + => AssertTranslationFailed(() => base.DateTimeOffsetNow_minus_timespan(async)); + public override async Task Navigation_inside_interpolated_string_expanded(bool async) { await base.Navigation_inside_interpolated_string_expanded(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs index da2da65afc9..2ca24164a55 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs @@ -6796,6 +6796,9 @@ FROM [Missions] AS [m] WHERE @__start_0 <= CAST(CONVERT(date, [m].[Timeline]) AS datetimeoffset) AND [m].[Timeline] < @__end_1 AND [m].[Timeline] = '1902-01-02T10:00:00.1234567+01:30'"); } + public override Task DateTimeOffsetNow_minus_timespan(bool async) + => AssertTranslationFailed(() => base.DateTimeOffsetNow_minus_timespan(async)); + public override async Task Navigation_inside_interpolated_string_expanded(bool async) { await base.Navigation_inside_interpolated_string_expanded(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs index 069b992c86b..6b74d3ac8f0 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs @@ -5434,6 +5434,9 @@ public override async Task DateTimeOffset_Contains_Less_than_Greater_than(bool a WHERE @__start_0 <= CAST(CONVERT(date, [m].[Timeline]) AS datetimeoffset) AND [m].[Timeline] < @__end_1 AND [m].[Timeline] = '1902-01-02T10:00:00.1234567+01:30'"); } + public override Task DateTimeOffsetNow_minus_timespan(bool async) + => AssertTranslationFailed(() => base.DateTimeOffsetNow_minus_timespan(async)); + public override async Task Conditional_with_conditions_evaluating_to_false_gets_optimized(bool async) { await base.Conditional_with_conditions_evaluating_to_false_gets_optimized(async); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs index f7b6840cab4..8b5a38260b9 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs @@ -102,6 +102,9 @@ public override async Task DateTimeOffset_Contains_Less_than_Greater_than(bool a AssertSql(); } + public override Task DateTimeOffsetNow_minus_timespan(bool async) + => AssertTranslationFailed(() => base.DateTimeOffsetNow_minus_timespan(async)); + public override async Task DateTimeOffset_Date_returns_datetime(bool async) { await AssertTranslationFailed(() => base.DateTimeOffset_Date_returns_datetime(async)); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/TPCGearsOfWarQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/TPCGearsOfWarQuerySqliteTest.cs index cd73a8bec08..c0d8638ae73 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/TPCGearsOfWarQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/TPCGearsOfWarQuerySqliteTest.cs @@ -50,6 +50,9 @@ public override Task Where_datetimeoffset_year_component(bool async) public override Task DateTimeOffset_Contains_Less_than_Greater_than(bool async) => AssertTranslationFailed(() => base.DateTimeOffset_Contains_Less_than_Greater_than(async)); + public override Task DateTimeOffsetNow_minus_timespan(bool async) + => AssertTranslationFailed(() => base.DateTimeOffsetNow_minus_timespan(async)); + public override Task DateTimeOffset_Date_returns_datetime(bool async) => AssertTranslationFailed(() => base.DateTimeOffset_Date_returns_datetime(async)); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/TPTGearsOfWarQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/TPTGearsOfWarQuerySqliteTest.cs index 1c0b05766e1..4ace1f21dbd 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/TPTGearsOfWarQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/TPTGearsOfWarQuerySqliteTest.cs @@ -50,6 +50,9 @@ public override Task Where_datetimeoffset_year_component(bool async) public override Task DateTimeOffset_Contains_Less_than_Greater_than(bool async) => AssertTranslationFailed(() => base.DateTimeOffset_Contains_Less_than_Greater_than(async)); + public override Task DateTimeOffsetNow_minus_timespan(bool async) + => AssertTranslationFailed(() => base.DateTimeOffsetNow_minus_timespan(async)); + public override Task DateTimeOffset_Date_returns_datetime(bool async) => AssertTranslationFailed(() => base.DateTimeOffset_Date_returns_datetime(async));