From 5525fce2123bc18c6cfbf4d01ecf16512f0ea94c Mon Sep 17 00:00:00 2001 From: Brice Lambson Date: Wed, 31 Aug 2022 07:56:57 -0700 Subject: [PATCH] SQLite Query: Simplify nested date() calls (#28928) Introducing SqliteSqlExpressionFactory as discussed in PR #23193 Fixes #25027 --- .../SqliteServiceCollectionExtensions.cs | 1 + .../SqliteDateOnlyMemberTranslator.cs | 7 +-- .../Internal/SqliteDateTimeAddTranslator.cs | 18 +++--- .../SqliteDateTimeMemberTranslator.cs | 13 ++-- .../SqliteMemberTranslatorProvider.cs | 2 +- .../SqliteMethodCallTranslatorProvider.cs | 2 +- ...ssion.cs => SqliteSqlExpressionFactory.cs} | 61 +++++++++++++++++-- .../Query/GearsOfWarQuerySqliteTest.cs | 31 ++++++++++ 8 files changed, 104 insertions(+), 31 deletions(-) rename src/EFCore.Sqlite.Core/Query/Internal/{SqliteExpression.cs => SqliteSqlExpressionFactory.cs} (50%) diff --git a/src/EFCore.Sqlite.Core/Extensions/SqliteServiceCollectionExtensions.cs b/src/EFCore.Sqlite.Core/Extensions/SqliteServiceCollectionExtensions.cs index 15a774d33a9..183ec5be4c1 100644 --- a/src/EFCore.Sqlite.Core/Extensions/SqliteServiceCollectionExtensions.cs +++ b/src/EFCore.Sqlite.Core/Extensions/SqliteServiceCollectionExtensions.cs @@ -122,6 +122,7 @@ public static IServiceCollection AddEntityFrameworkSqlite(this IServiceCollectio ? new SqliteLegacyUpdateSqlGenerator(dependencies) : new SqliteUpdateSqlGenerator(dependencies); }) + .TryAdd() .TryAddProviderSpecificServices( b => b.TryAddScoped()); diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteDateOnlyMemberTranslator.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteDateOnlyMemberTranslator.cs index 960a9b3a60c..ba2e7cce2c8 100644 --- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteDateOnlyMemberTranslator.cs +++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteDateOnlyMemberTranslator.cs @@ -23,7 +23,7 @@ private static readonly Dictionary DatePartMapping { nameof(DateOnly.DayOfWeek), "%w" } }; - private readonly ISqlExpressionFactory _sqlExpressionFactory; + private readonly SqliteSqlExpressionFactory _sqlExpressionFactory; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -31,7 +31,7 @@ private static readonly Dictionary DatePartMapping /// 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 SqliteDateOnlyMemberTranslator(ISqlExpressionFactory sqlExpressionFactory) + public SqliteDateOnlyMemberTranslator(SqliteSqlExpressionFactory sqlExpressionFactory) { _sqlExpressionFactory = sqlExpressionFactory; } @@ -49,8 +49,7 @@ public SqliteDateOnlyMemberTranslator(ISqlExpressionFactory sqlExpressionFactory IDiagnosticsLogger logger) => member.DeclaringType == typeof(DateOnly) && DatePartMapping.TryGetValue(member.Name, out var datePart) ? _sqlExpressionFactory.Convert( - SqliteExpression.Strftime( - _sqlExpressionFactory, + _sqlExpressionFactory.Strftime( typeof(string), datePart, instance!), diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteDateTimeAddTranslator.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteDateTimeAddTranslator.cs index 019b73d10e1..9af6bd69053 100644 --- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteDateTimeAddTranslator.cs +++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteDateTimeAddTranslator.cs @@ -32,7 +32,7 @@ private static readonly MethodInfo AddTicks { typeof(DateOnly).GetRuntimeMethod(nameof(DateOnly.AddDays), new[] { typeof(int) })!, " days" } }; - private readonly ISqlExpressionFactory _sqlExpressionFactory; + private readonly SqliteSqlExpressionFactory _sqlExpressionFactory; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -40,7 +40,7 @@ private static readonly MethodInfo AddTicks /// 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 SqliteDateTimeAddTranslator(ISqlExpressionFactory sqlExpressionFactory) + public SqliteDateTimeAddTranslator(SqliteSqlExpressionFactory sqlExpressionFactory) { _sqlExpressionFactory = sqlExpressionFactory; } @@ -105,8 +105,7 @@ public SqliteDateTimeAddTranslator(ISqlExpressionFactory sqlExpressionFactory) "rtrim", new SqlExpression[] { - SqliteExpression.Strftime( - _sqlExpressionFactory, + _sqlExpressionFactory.Strftime( method.ReturnType, "%Y-%m-%d %H:%M:%f", instance!, @@ -133,18 +132,15 @@ public SqliteDateTimeAddTranslator(ISqlExpressionFactory sqlExpressionFactory) { if (instance is not null && _methodInfoToUnitSuffix.TryGetValue(method, out var unitSuffix)) { - return _sqlExpressionFactory.Function( - "date", + return _sqlExpressionFactory.Date( + method.ReturnType, + instance, new[] { - instance, _sqlExpressionFactory.Add( _sqlExpressionFactory.Convert(arguments[0], typeof(string)), _sqlExpressionFactory.Constant(unitSuffix)) - }, - argumentsPropagateNullability: new[] { true, true }, - nullable: true, - returnType: method.ReturnType); + }); } return null; diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteDateTimeMemberTranslator.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteDateTimeMemberTranslator.cs index 361da36e4bd..e9d9573f6e0 100644 --- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteDateTimeMemberTranslator.cs +++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteDateTimeMemberTranslator.cs @@ -26,7 +26,7 @@ private static readonly Dictionary DatePartMapping { nameof(DateTime.DayOfWeek), "%w" } }; - private readonly ISqlExpressionFactory _sqlExpressionFactory; + private readonly SqliteSqlExpressionFactory _sqlExpressionFactory; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -34,7 +34,7 @@ private static readonly Dictionary DatePartMapping /// 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 SqliteDateTimeMemberTranslator(ISqlExpressionFactory sqlExpressionFactory) + public SqliteDateTimeMemberTranslator(SqliteSqlExpressionFactory sqlExpressionFactory) { _sqlExpressionFactory = sqlExpressionFactory; } @@ -58,8 +58,7 @@ public SqliteDateTimeMemberTranslator(ISqlExpressionFactory sqlExpressionFactory if (DatePartMapping.TryGetValue(memberName, out var datePart)) { return _sqlExpressionFactory.Convert( - SqliteExpression.Strftime( - _sqlExpressionFactory, + _sqlExpressionFactory.Strftime( typeof(string), datePart, instance!), @@ -87,8 +86,7 @@ public SqliteDateTimeMemberTranslator(ISqlExpressionFactory sqlExpressionFactory return _sqlExpressionFactory.Modulo( _sqlExpressionFactory.Multiply( _sqlExpressionFactory.Convert( - SqliteExpression.Strftime( - _sqlExpressionFactory, + _sqlExpressionFactory.Strftime( typeof(string), "%f", instance!), @@ -142,8 +140,7 @@ public SqliteDateTimeMemberTranslator(ISqlExpressionFactory sqlExpressionFactory "rtrim", new SqlExpression[] { - SqliteExpression.Strftime( - _sqlExpressionFactory, + _sqlExpressionFactory.Strftime( returnType, format, timestring, diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteMemberTranslatorProvider.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteMemberTranslatorProvider.cs index 95c4b410ca3..5fd49b8150f 100644 --- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteMemberTranslatorProvider.cs +++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteMemberTranslatorProvider.cs @@ -20,7 +20,7 @@ public class SqliteMemberTranslatorProvider : RelationalMemberTranslatorProvider public SqliteMemberTranslatorProvider(RelationalMemberTranslatorProviderDependencies dependencies) : base(dependencies) { - var sqlExpressionFactory = dependencies.SqlExpressionFactory; + var sqlExpressionFactory = (SqliteSqlExpressionFactory)dependencies.SqlExpressionFactory; AddTranslators( new IMemberTranslator[] diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteMethodCallTranslatorProvider.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteMethodCallTranslatorProvider.cs index f8c4233c6b3..13458bd77df 100644 --- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteMethodCallTranslatorProvider.cs +++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteMethodCallTranslatorProvider.cs @@ -20,7 +20,7 @@ public class SqliteMethodCallTranslatorProvider : RelationalMethodCallTranslator public SqliteMethodCallTranslatorProvider(RelationalMethodCallTranslatorProviderDependencies dependencies) : base(dependencies) { - var sqlExpressionFactory = dependencies.SqlExpressionFactory; + var sqlExpressionFactory = (SqliteSqlExpressionFactory)dependencies.SqlExpressionFactory; AddTranslators( new IMethodCallTranslator[] diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteExpression.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlExpressionFactory.cs similarity index 50% rename from src/EFCore.Sqlite.Core/Query/Internal/SqliteExpression.cs rename to src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlExpressionFactory.cs index 9cb150812ce..0675a166cbf 100644 --- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteExpression.cs +++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlExpressionFactory.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.EntityFrameworkCore.Query.SqlExpressions; @@ -11,7 +11,7 @@ namespace Microsoft.EntityFrameworkCore.Sqlite.Query.Internal; /// 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 static class SqliteExpression +public class SqliteSqlExpressionFactory : SqlExpressionFactory { /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -19,8 +19,18 @@ public static class SqliteExpression /// 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 static SqlFunctionExpression Strftime( - ISqlExpressionFactory sqlExpressionFactory, + public SqliteSqlExpressionFactory(SqlExpressionFactoryDependencies dependencies) + : base(dependencies) + { + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// 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 virtual SqlFunctionExpression Strftime( Type returnType, string format, SqlExpression timestring, @@ -47,9 +57,16 @@ public static SqlFunctionExpression Strftime( modifiers = strftimeFunction.Arguments.Skip(2).Concat(modifiers); } - var finalArguments = new[] { sqlExpressionFactory.Constant(format), timestring }.Concat(modifiers); + if (timestring is SqlFunctionExpression dateFunction + && dateFunction.Name == "date") + { + timestring = dateFunction.Arguments![0]; + modifiers = dateFunction.Arguments.Skip(1).Concat(modifiers); + } + + var finalArguments = new[] { Constant(format), timestring }.Concat(modifiers); - return sqlExpressionFactory.Function( + return Function( "strftime", finalArguments, nullable: true, @@ -57,4 +74,36 @@ public static SqlFunctionExpression Strftime( returnType, typeMapping); } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// 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 virtual SqlFunctionExpression Date( + Type returnType, + SqlExpression timestring, + IEnumerable? modifiers = null, + RelationalTypeMapping? typeMapping = null) + { + modifiers ??= Enumerable.Empty(); + + if (timestring is SqlFunctionExpression dateFunction + && dateFunction.Name == "date") + { + timestring = dateFunction.Arguments![0]; + modifiers = dateFunction.Arguments.Skip(1).Concat(modifiers); + } + + var finalArguments = new[] { timestring }.Concat(modifiers); + + return Function( + "date", + finalArguments, + nullable: true, + argumentsPropagateNullability: finalArguments.Select(_ => true), + returnType, + typeMapping); + } } diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs index 6993119a19e..e31f8287ebb 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.EntityFrameworkCore.Sqlite.Internal; +using Microsoft.EntityFrameworkCore.TestModels.GearsOfWarModel; namespace Microsoft.EntityFrameworkCore.Query; @@ -559,6 +560,36 @@ public override async Task Where_DateOnly_AddYears(bool async) WHERE date(""m"".""Date"", CAST(3 AS TEXT) || ' years') = '1993-11-10'"); } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Where_DateOnly_AddYears_Year(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(m => m.Date.AddYears(3).Year == 1993).AsTracking(), + entryCount: 1); + + AssertSql( + @"SELECT ""m"".""Id"", ""m"".""CodeName"", ""m"".""Date"", ""m"".""Duration"", ""m"".""Rating"", ""m"".""Time"", ""m"".""Timeline"" +FROM ""Missions"" AS ""m"" +WHERE CAST(strftime('%Y', ""m"".""Date"", CAST(3 AS TEXT) || ' years') AS INTEGER) = 1993"); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Where_DateOnly_AddYears_AddMonths(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(m => m.Date.AddYears(3).AddMonths(3) == new DateOnly(1994, 2, 10)).AsTracking(), + entryCount: 1); + + AssertSql( + @"SELECT ""m"".""Id"", ""m"".""CodeName"", ""m"".""Date"", ""m"".""Duration"", ""m"".""Rating"", ""m"".""Time"", ""m"".""Timeline"" +FROM ""Missions"" AS ""m"" +WHERE date(""m"".""Date"", CAST(3 AS TEXT) || ' years', CAST(3 AS TEXT) || ' months') = '1994-02-10'"); + } + public override async Task Where_DateOnly_AddMonths(bool async) { await base.Where_DateOnly_AddMonths(async);