From f26910d81d2e1b055f1040de3ab0706524105a4c Mon Sep 17 00:00:00 2001 From: Smit Patel Date: Thu, 5 Nov 2020 23:43:38 -0800 Subject: [PATCH] SqlServer: date constant in DateAdd method is always datetime Resolves #17507 --- .../SqlServerDateTimeMethodTranslator.cs | 45 ++++++++++++------- .../SqlServerMethodCallTranslatorProvider.cs | 2 +- .../NorthwindMiscellaneousQueryCosmosTest.cs | 11 +++++ .../NorthwindMiscellaneousQueryTestBase.cs | 13 ++++++ ...orthwindMiscellaneousQuerySqlServerTest.cs | 11 +++++ .../NorthwindMiscellaneousQuerySqliteTest.cs | 11 +++++ 6 files changed, 76 insertions(+), 17 deletions(-) diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerDateTimeMethodTranslator.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerDateTimeMethodTranslator.cs index ee51c45cce6..91f7dd0009f 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerDateTimeMethodTranslator.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerDateTimeMethodTranslator.cs @@ -8,6 +8,7 @@ using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; +using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Utilities; #nullable enable @@ -41,6 +42,7 @@ public class SqlServerDateTimeMethodTranslator : IMethodCallTranslator }; private readonly ISqlExpressionFactory _sqlExpressionFactory; + private readonly IRelationalTypeMappingSource _typeMappingSource; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -48,9 +50,12 @@ public class SqlServerDateTimeMethodTranslator : IMethodCallTranslator /// 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 SqlServerDateTimeMethodTranslator([NotNull] ISqlExpressionFactory sqlExpressionFactory) + public SqlServerDateTimeMethodTranslator( + [NotNull] ISqlExpressionFactory sqlExpressionFactory, + [NotNull] IRelationalTypeMappingSource typeMappingSource) { _sqlExpressionFactory = sqlExpressionFactory; + _typeMappingSource = typeMappingSource; } /// @@ -74,24 +79,32 @@ public SqlServerDateTimeMethodTranslator([NotNull] ISqlExpressionFactory sqlExpr { // DateAdd does not accept number argument outside of int range // AddYears/AddMonths take int argument so no need to check for range - return datePart != "year" + if (datePart != "year" && datePart != "month" && arguments[0] is SqlConstantExpression sqlConstant && ((double)sqlConstant.Value >= int.MaxValue - || (double)sqlConstant.Value <= int.MinValue) - ? null - : _sqlExpressionFactory.Function( - "DATEADD", - new[] - { - _sqlExpressionFactory.Fragment(datePart), - _sqlExpressionFactory.Convert(arguments[0], typeof(int)), - instance - }, - nullable: true, - argumentsPropagateNullability: new[] { false, true, true }, - instance.Type, - instance.TypeMapping); + || (double)sqlConstant.Value <= int.MinValue)) + { + return null; + } + + if (instance is SqlConstantExpression instanceConstant) + { + instance = instanceConstant.ApplyTypeMapping(_typeMappingSource.FindMapping(typeof(DateTime), "datetime")); + } + + return _sqlExpressionFactory.Function( + "DATEADD", + new[] + { + _sqlExpressionFactory.Fragment(datePart), + _sqlExpressionFactory.Convert(arguments[0], typeof(int)), + instance + }, + nullable: true, + argumentsPropagateNullability: new[] { false, true, true }, + instance.Type, + instance.TypeMapping); } return null; diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerMethodCallTranslatorProvider.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerMethodCallTranslatorProvider.cs index f3375133dbc..0b2c194c150 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerMethodCallTranslatorProvider.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerMethodCallTranslatorProvider.cs @@ -34,7 +34,7 @@ public SqlServerMethodCallTranslatorProvider([NotNull] RelationalMethodCallTrans new SqlServerConvertTranslator(sqlExpressionFactory), new SqlServerDataLengthFunctionTranslator(sqlExpressionFactory), new SqlServerDateDiffFunctionsTranslator(sqlExpressionFactory), - new SqlServerDateTimeMethodTranslator(sqlExpressionFactory), + new SqlServerDateTimeMethodTranslator(sqlExpressionFactory, typeMappingSource), new SqlServerFromPartsFunctionTranslator(sqlExpressionFactory, typeMappingSource), new SqlServerFullTextSearchFunctionsTranslator(sqlExpressionFactory), new SqlServerIsDateFunctionTranslator(sqlExpressionFactory), diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs index df795b7a239..d029963733a 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs @@ -2580,6 +2580,17 @@ FROM root c WHERE ((c[""Discriminator""] = ""Order"") AND (c[""OrderDate""] != null))"); } + public override async Task Add_minutes_on_constant_value(bool async) + { + await base.Add_minutes_on_constant_value(async); + + AssertSql( + @"SELECT VALUE {""c"" : (c[""OrderID""] % 25)} +FROM root c +WHERE ((c[""Discriminator""] = ""Order"") AND (c[""OrderID""] < 10500)) +ORDER BY c[""OrderID""]"); + } + [ConditionalTheory(Skip = "Issue #17246")] public override async Task Select_expression_references_are_updated_correctly_with_subquery(bool async) { diff --git a/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs index b1721ee75ea..3863f77a95e 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs @@ -4194,6 +4194,19 @@ public virtual Task Select_expression_date_add_milliseconds_large_number_divided e => e.OrderDate); } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Add_minutes_on_constant_value(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Where(c => c.OrderID < 10500) + .OrderBy(o => o.OrderID) + .Select(o => new { Test = new DateTime(1900, 1, 1).AddMinutes(o.OrderID % 25) }), + assertOrder: true, + elementAsserter: (e, a) => AssertEqual(e.Test, a.Test)); + } + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Select_expression_references_are_updated_correctly_with_subquery(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs index ee921bd1f2a..bb8ab933587 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs @@ -3229,6 +3229,17 @@ FROM [Orders] AS [o] WHERE [o].[OrderDate] IS NOT NULL"); } + public override async Task Add_minutes_on_constant_value(bool async) + { + await base.Add_minutes_on_constant_value(async); + + AssertSql( + @"SELECT DATEADD(minute, CAST(CAST(([o].[OrderID] % 25) AS float) AS int), '1900-01-01T00:00:00.000') AS [Test] +FROM [Orders] AS [o] +WHERE [o].[OrderID] < 10500 +ORDER BY [o].[OrderID]"); + } + public override async Task Select_expression_references_are_updated_correctly_with_subquery(bool async) { await base.Select_expression_references_are_updated_correctly_with_subquery(async); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindMiscellaneousQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindMiscellaneousQuerySqliteTest.cs index 68caf7b37bd..3aa50e52851 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindMiscellaneousQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindMiscellaneousQuerySqliteTest.cs @@ -172,6 +172,17 @@ SELECT rtrim(rtrim(strftime('%Y-%m-%d %H:%M:%f', ""o"".""OrderDate"", COALESCE(C WHERE ""o"".""OrderDate"" IS NOT NULL"); } + public override async Task Add_minutes_on_constant_value(bool async) + { + await base.Add_minutes_on_constant_value(async); + + AssertSql( + @"SELECT rtrim(rtrim(strftime('%Y-%m-%d %H:%M:%f', '1900-01-01 00:00:00', CAST(CAST((""o"".""OrderID"" % 25) AS REAL) AS TEXT) || ' minutes'), '0'), '.') AS ""Test"" +FROM ""Orders"" AS ""o"" +WHERE ""o"".""OrderID"" < 10500 +ORDER BY ""o"".""OrderID"""); + } + public override async Task Select_distinct_long_count(bool async) { await base.Select_distinct_long_count(async);