Skip to content

Commit

Permalink
SQLite Query: Simplify nested date() calls (#28928)
Browse files Browse the repository at this point in the history
Introducing SqliteSqlExpressionFactory as discussed in PR #23193

Fixes #25027
  • Loading branch information
bricelam committed Aug 31, 2022
1 parent e16eab2 commit 5525fce
Show file tree
Hide file tree
Showing 8 changed files with 104 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ public static IServiceCollection AddEntityFrameworkSqlite(this IServiceCollectio
? new SqliteLegacyUpdateSqlGenerator(dependencies)
: new SqliteUpdateSqlGenerator(dependencies);
})
.TryAdd<ISqlExpressionFactory, SqliteSqlExpressionFactory>()
.TryAddProviderSpecificServices(
b => b.TryAddScoped<ISqliteRelationalConnection, SqliteRelationalConnection>());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ private static readonly Dictionary<string, string> DatePartMapping
{ nameof(DateOnly.DayOfWeek), "%w" }
};

private readonly ISqlExpressionFactory _sqlExpressionFactory;
private readonly SqliteSqlExpressionFactory _sqlExpressionFactory;

/// <summary>
/// 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.
/// </summary>
public SqliteDateOnlyMemberTranslator(ISqlExpressionFactory sqlExpressionFactory)
public SqliteDateOnlyMemberTranslator(SqliteSqlExpressionFactory sqlExpressionFactory)
{
_sqlExpressionFactory = sqlExpressionFactory;
}
Expand All @@ -49,8 +49,7 @@ public SqliteDateOnlyMemberTranslator(ISqlExpressionFactory sqlExpressionFactory
IDiagnosticsLogger<DbLoggerCategory.Query> logger)
=> member.DeclaringType == typeof(DateOnly) && DatePartMapping.TryGetValue(member.Name, out var datePart)
? _sqlExpressionFactory.Convert(
SqliteExpression.Strftime(
_sqlExpressionFactory,
_sqlExpressionFactory.Strftime(
typeof(string),
datePart,
instance!),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,15 @@ private static readonly MethodInfo AddTicks
{ typeof(DateOnly).GetRuntimeMethod(nameof(DateOnly.AddDays), new[] { typeof(int) })!, " days" }
};

private readonly ISqlExpressionFactory _sqlExpressionFactory;
private readonly SqliteSqlExpressionFactory _sqlExpressionFactory;

/// <summary>
/// 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.
/// </summary>
public SqliteDateTimeAddTranslator(ISqlExpressionFactory sqlExpressionFactory)
public SqliteDateTimeAddTranslator(SqliteSqlExpressionFactory sqlExpressionFactory)
{
_sqlExpressionFactory = sqlExpressionFactory;
}
Expand Down Expand Up @@ -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!,
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,15 @@ private static readonly Dictionary<string, string> DatePartMapping
{ nameof(DateTime.DayOfWeek), "%w" }
};

private readonly ISqlExpressionFactory _sqlExpressionFactory;
private readonly SqliteSqlExpressionFactory _sqlExpressionFactory;

/// <summary>
/// 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.
/// </summary>
public SqliteDateTimeMemberTranslator(ISqlExpressionFactory sqlExpressionFactory)
public SqliteDateTimeMemberTranslator(SqliteSqlExpressionFactory sqlExpressionFactory)
{
_sqlExpressionFactory = sqlExpressionFactory;
}
Expand All @@ -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!),
Expand Down Expand Up @@ -87,8 +86,7 @@ public SqliteDateTimeMemberTranslator(ISqlExpressionFactory sqlExpressionFactory
return _sqlExpressionFactory.Modulo(
_sqlExpressionFactory.Multiply(
_sqlExpressionFactory.Convert(
SqliteExpression.Strftime(
_sqlExpressionFactory,
_sqlExpressionFactory.Strftime(
typeof(string),
"%f",
instance!),
Expand Down Expand Up @@ -142,8 +140,7 @@ public SqliteDateTimeMemberTranslator(ISqlExpressionFactory sqlExpressionFactory
"rtrim",
new SqlExpression[]
{
SqliteExpression.Strftime(
_sqlExpressionFactory,
_sqlExpressionFactory.Strftime(
returnType,
format,
timestring,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -11,16 +11,26 @@ 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.
/// </summary>
public static class SqliteExpression
public class SqliteSqlExpressionFactory : SqlExpressionFactory
{
/// <summary>
/// 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.
/// </summary>
public static SqlFunctionExpression Strftime(
ISqlExpressionFactory sqlExpressionFactory,
public SqliteSqlExpressionFactory(SqlExpressionFactoryDependencies dependencies)
: base(dependencies)
{
}

/// <summary>
/// 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.
/// </summary>
public virtual SqlFunctionExpression Strftime(
Type returnType,
string format,
SqlExpression timestring,
Expand All @@ -47,14 +57,53 @@ 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,
argumentsPropagateNullability: finalArguments.Select(_ => true),
returnType,
typeMapping);
}

/// <summary>
/// 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.
/// </summary>
public virtual SqlFunctionExpression Date(
Type returnType,
SqlExpression timestring,
IEnumerable<SqlExpression>? modifiers = null,
RelationalTypeMapping? typeMapping = null)
{
modifiers ??= Enumerable.Empty<SqlExpression>();

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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<Mission>().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<Mission>().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);
Expand Down

0 comments on commit 5525fce

Please sign in to comment.