diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteMemberTranslatorProvider.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteMemberTranslatorProvider.cs
index 25adce48e5f..3c1825dabfc 100644
--- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteMemberTranslatorProvider.cs
+++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteMemberTranslatorProvider.cs
@@ -26,7 +26,8 @@ public SqliteMemberTranslatorProvider(RelationalMemberTranslatorProviderDependen
[
new SqliteDateTimeMemberTranslator(sqlExpressionFactory),
new SqliteStringLengthTranslator(sqlExpressionFactory),
- new SqliteDateOnlyMemberTranslator(sqlExpressionFactory)
+ new SqliteDateOnlyMemberTranslator(sqlExpressionFactory),
+ new SqliteTimeSpanMemberTranslator(sqlExpressionFactory)
]);
}
}
diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteMethodCallTranslatorProvider.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteMethodCallTranslatorProvider.cs
index 970873af563..1f07a8cc4a0 100644
--- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteMethodCallTranslatorProvider.cs
+++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteMethodCallTranslatorProvider.cs
@@ -35,7 +35,8 @@ public SqliteMethodCallTranslatorProvider(RelationalMethodCallTranslatorProvider
new SqliteRandomTranslator(sqlExpressionFactory),
new SqliteRegexMethodTranslator(sqlExpressionFactory),
new SqliteStringMethodTranslator(sqlExpressionFactory),
- new SqliteSubstrMethodTranslator(sqlExpressionFactory)
+ new SqliteSubstrMethodTranslator(sqlExpressionFactory),
+ new SqliteTimeSpanMethodTranslator(sqlExpressionFactory)
]);
}
}
diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlExpressionFactory.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlExpressionFactory.cs
index 91ddd2b8793..271d7c0e622 100644
--- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlExpressionFactory.cs
+++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlExpressionFactory.cs
@@ -105,6 +105,35 @@ public virtual SqlExpression Date(
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 SqlExpression EfDays(SqlExpression timeSpanExpression)
+ => Function(
+ "ef_days",
+ [timeSpanExpression],
+ nullable: true,
+ argumentsPropagateNullability: [true],
+ typeof(double));
+
+ ///
+ /// 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 SqlExpression EfTimespan(SqlExpression daysExpression)
+ => Function(
+ "ef_timespan",
+ [daysExpression],
+ nullable: true,
+ argumentsPropagateNullability: [true],
+ typeof(TimeSpan),
+ Dependencies.TypeMappingSource.FindMapping(typeof(TimeSpan), Dependencies.Model));
+
///
/// 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
diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlTranslatingExpressionVisitor.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlTranslatingExpressionVisitor.cs
index c0d7e051604..e2dc9a07c75 100644
--- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlTranslatingExpressionVisitor.cs
+++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlTranslatingExpressionVisitor.cs
@@ -49,44 +49,37 @@ private static readonly IReadOnlyDictionary
{
typeof(TimeOnly),
- typeof(TimeSpan),
typeof(ulong)
},
[ExpressionType.GreaterThan] = new HashSet
{
typeof(DateTimeOffset),
- typeof(TimeSpan),
typeof(ulong)
},
[ExpressionType.GreaterThanOrEqual] = new HashSet
{
typeof(DateTimeOffset),
- typeof(TimeSpan),
typeof(ulong)
},
[ExpressionType.LessThan] = new HashSet
{
typeof(DateTimeOffset),
- typeof(TimeSpan),
typeof(ulong)
},
[ExpressionType.LessThanOrEqual] = new HashSet
{
typeof(DateTimeOffset),
- typeof(TimeSpan),
typeof(ulong)
},
[ExpressionType.Modulo] = new HashSet { typeof(ulong) },
[ExpressionType.Multiply] = new HashSet
{
typeof(TimeOnly),
- typeof(TimeSpan),
typeof(ulong)
},
[ExpressionType.Subtract] = new HashSet
@@ -94,8 +87,7 @@ private static readonly IReadOnlyDictionary QueryCompilationContext.NotTranslatedExpression,
+ var t when t == typeof(TimeSpan)
+ && _sqlExpressionFactory is SqliteSqlExpressionFactory sqliteFactory
+ => sqliteFactory.EfTimespan(Dependencies.SqlExpressionFactory.Negate(sqliteFactory.EfDays(sqlUnary.Operand))),
+
_ => translation
};
}
@@ -191,6 +187,11 @@ when ModuloFunctions.TryGetValue(GetProviderType(sqlBinary.Left), out var functi
case { } when AttemptDecimalArithmetic(sqlBinary):
return DoDecimalArithmetics(translation, sqlBinary.OperatorType, sqlBinary.Left, sqlBinary.Right);
+ case { } when _sqlExpressionFactory is SqliteSqlExpressionFactory sqliteFactory
+ && TryTranslateTimeSpanDateTimeBinary(sqlBinary, sqliteFactory, out var timeSpanDateTimeResult)
+ && timeSpanDateTimeResult != null:
+ return timeSpanDateTimeResult;
+
case { }
when RestrictedBinaryExpressions.TryGetValue(sqlBinary.OperatorType, out var restrictedTypes)
&& (restrictedTypes.Contains(GetProviderType(sqlBinary.Left))
@@ -204,6 +205,145 @@ when RestrictedBinaryExpressions.TryGetValue(sqlBinary.OperatorType, out var res
return translation;
}
+ private static bool TryTranslateTimeSpanDateTimeBinary(
+ SqlBinaryExpression sqlBinary,
+ SqliteSqlExpressionFactory sqliteFactory,
+ out SqlExpression? result)
+ {
+ result = null;
+ var leftType = GetProviderType(sqlBinary.Left);
+ var rightType = GetProviderType(sqlBinary.Right);
+ var operatorType = sqlBinary.OperatorType;
+
+ if (leftType == typeof(TimeSpan) && rightType == typeof(TimeSpan))
+ {
+ var leftDays = sqliteFactory.EfDays(sqlBinary.Left);
+ var rightDays = sqliteFactory.EfDays(sqlBinary.Right);
+ switch (operatorType)
+ {
+ case ExpressionType.Add:
+ result = sqliteFactory.EfTimespan(
+ sqliteFactory.Add(leftDays, rightDays));
+ return true;
+ case ExpressionType.Subtract:
+ result = sqliteFactory.EfTimespan(
+ sqliteFactory.Subtract(leftDays, rightDays));
+ return true;
+ case ExpressionType.Divide:
+ result = sqliteFactory.Divide(leftDays, rightDays);
+ return true;
+ case ExpressionType.GreaterThan:
+ result = sqliteFactory.GreaterThan(leftDays, rightDays);
+ return true;
+ case ExpressionType.GreaterThanOrEqual:
+ result = sqliteFactory.GreaterThanOrEqual(leftDays, rightDays);
+ return true;
+ case ExpressionType.LessThan:
+ result = sqliteFactory.LessThan(leftDays, rightDays);
+ return true;
+ case ExpressionType.LessThanOrEqual:
+ result = sqliteFactory.LessThanOrEqual(leftDays, rightDays);
+ return true;
+ }
+ }
+
+ if (operatorType == ExpressionType.Divide && leftType == typeof(TimeSpan) && (rightType == typeof(double) || rightType == typeof(float)))
+ {
+ result = sqliteFactory.EfTimespan(
+ sqliteFactory.Divide(sqliteFactory.EfDays(sqlBinary.Left), sqlBinary.Right));
+ return true;
+ }
+
+ if (operatorType == ExpressionType.Multiply)
+ {
+ if ((leftType == typeof(double) || leftType == typeof(float)) && rightType == typeof(TimeSpan))
+ {
+ result = sqliteFactory.EfTimespan(
+ sqliteFactory.Multiply(sqlBinary.Left, sqliteFactory.EfDays(sqlBinary.Right)));
+ return true;
+ }
+ if (leftType == typeof(TimeSpan) && (rightType == typeof(double) || rightType == typeof(float)))
+ {
+ result = sqliteFactory.EfTimespan(
+ sqliteFactory.Multiply(sqliteFactory.EfDays(sqlBinary.Left), sqlBinary.Right));
+ return true;
+ }
+ }
+
+ if (leftType == typeof(DateTime) && rightType == typeof(TimeSpan))
+ {
+ if (operatorType == ExpressionType.Add)
+ {
+ var juliandayLeft = sqliteFactory.Function(
+ "julianday",
+ [sqlBinary.Left],
+ nullable: true,
+ argumentsPropagateNullability: Statics.TrueArrays[1],
+ typeof(double));
+ var sumDays = sqliteFactory.Add(juliandayLeft, sqliteFactory.EfDays(sqlBinary.Right));
+ result = MakeDateTimeFromJulianDay(sqliteFactory, sumDays);
+ return true;
+ }
+ if (operatorType == ExpressionType.Subtract)
+ {
+ var juliandayLeft = sqliteFactory.Function(
+ "julianday",
+ [sqlBinary.Left],
+ nullable: true,
+ argumentsPropagateNullability: Statics.TrueArrays[1],
+ typeof(double));
+ var diffDays = sqliteFactory.Subtract(juliandayLeft, sqliteFactory.EfDays(sqlBinary.Right));
+ result = MakeDateTimeFromJulianDay(sqliteFactory, diffDays);
+ return true;
+ }
+ }
+
+ if (leftType == typeof(DateTime) && rightType == typeof(DateTime) && operatorType == ExpressionType.Subtract)
+ {
+ var juliandayLeft = sqliteFactory.Function(
+ "julianday",
+ [sqlBinary.Left],
+ nullable: true,
+ argumentsPropagateNullability: Statics.TrueArrays[1],
+ typeof(double));
+ var juliandayRight = sqliteFactory.Function(
+ "julianday",
+ [sqlBinary.Right],
+ nullable: true,
+ argumentsPropagateNullability: Statics.TrueArrays[1],
+ typeof(double));
+ result = sqliteFactory.EfTimespan(sqliteFactory.Subtract(juliandayLeft, juliandayRight));
+ return true;
+ }
+
+ return false;
+ }
+
+ private static SqlExpression MakeDateTimeFromJulianDay(SqliteSqlExpressionFactory sqliteFactory, SqlExpression julianDayExpression)
+ {
+ var strftimeResult = sqliteFactory.Strftime(
+ typeof(DateTime),
+ "%Y-%m-%d %H:%M:%f",
+ julianDayExpression);
+ return sqliteFactory.Function(
+ "rtrim",
+ [
+ sqliteFactory.Function(
+ "rtrim",
+ [
+ strftimeResult,
+ sqliteFactory.Constant("0")
+ ],
+ nullable: true,
+ argumentsPropagateNullability: Statics.TrueFalse,
+ typeof(DateTime)),
+ sqliteFactory.Constant(".")
+ ],
+ nullable: true,
+ argumentsPropagateNullability: Statics.TrueFalse,
+ typeof(DateTime));
+ }
+
///
protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
{
diff --git a/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteQueryableAggregateMethodTranslator.cs b/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteQueryableAggregateMethodTranslator.cs
index af1e816e87c..6e901dc906e 100644
--- a/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteQueryableAggregateMethodTranslator.cs
+++ b/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteQueryableAggregateMethodTranslator.cs
@@ -59,13 +59,26 @@ public class SqliteQueryableAggregateMethodTranslator(ISqlExpressionFactory sqlE
&& source.Selector is SqlExpression maxSqlExpression:
var maxArgumentType = GetProviderType(maxSqlExpression);
if (maxArgumentType == typeof(DateTimeOffset)
- || maxArgumentType == typeof(TimeSpan)
|| maxArgumentType == typeof(ulong))
{
throw new NotSupportedException(
SqliteStrings.AggregateOperationNotSupported(nameof(Queryable.Max), maxArgumentType.ShortDisplayName()));
}
+ if (maxArgumentType == typeof(TimeSpan)
+ && sqlExpressionFactory is SqliteSqlExpressionFactory maxSqliteFactory)
+ {
+ maxSqlExpression = CombineTerms(source, maxSqlExpression);
+ var maxDaysExpression = maxSqliteFactory.EfDays(maxSqlExpression);
+ var maxAggregate = sqlExpressionFactory.Function(
+ "max",
+ [maxDaysExpression],
+ nullable: true,
+ argumentsPropagateNullability: Statics.FalseArrays[1],
+ typeof(double));
+ return maxSqliteFactory.EfTimespan(maxAggregate);
+ }
+
if (maxArgumentType == typeof(decimal))
{
maxSqlExpression = CombineTerms(source, maxSqlExpression);
@@ -86,13 +99,26 @@ public class SqliteQueryableAggregateMethodTranslator(ISqlExpressionFactory sqlE
&& source.Selector is SqlExpression minSqlExpression:
var minArgumentType = GetProviderType(minSqlExpression);
if (minArgumentType == typeof(DateTimeOffset)
- || minArgumentType == typeof(TimeSpan)
|| minArgumentType == typeof(ulong))
{
throw new NotSupportedException(
SqliteStrings.AggregateOperationNotSupported(nameof(Queryable.Min), minArgumentType.ShortDisplayName()));
}
+ if (minArgumentType == typeof(TimeSpan)
+ && sqlExpressionFactory is SqliteSqlExpressionFactory minSqliteFactory)
+ {
+ minSqlExpression = CombineTerms(source, minSqlExpression);
+ var minDaysExpression = minSqliteFactory.EfDays(minSqlExpression);
+ var minAggregate = sqlExpressionFactory.Function(
+ "min",
+ [minDaysExpression],
+ nullable: true,
+ argumentsPropagateNullability: Statics.FalseArrays[1],
+ typeof(double));
+ return minSqliteFactory.EfTimespan(minAggregate);
+ }
+
if (minArgumentType == typeof(decimal))
{
minSqlExpression = CombineTerms(source, minSqlExpression);
diff --git a/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteTimeSpanMemberTranslator.cs b/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteTimeSpanMemberTranslator.cs
new file mode 100644
index 00000000000..0ac60e7b904
--- /dev/null
+++ b/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteTimeSpanMemberTranslator.cs
@@ -0,0 +1,95 @@
+// 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;
+
+// ReSharper disable once CheckNamespace
+namespace Microsoft.EntityFrameworkCore.Sqlite.Query.Internal;
+
+///
+/// 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 class SqliteTimeSpanMemberTranslator(SqliteSqlExpressionFactory sqlExpressionFactory) : IMemberTranslator
+{
+ ///
+ /// 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 SqlExpression? Translate(
+ SqlExpression? instance,
+ MemberInfo member,
+ Type returnType,
+ IDiagnosticsLogger logger)
+ {
+ if (member.DeclaringType != typeof(TimeSpan) || instance is null)
+ {
+ return null;
+ }
+
+ var daysExpression = sqlExpressionFactory.EfDays(instance);
+
+ return member.Name switch
+ {
+ nameof(TimeSpan.Days)
+ => sqlExpressionFactory.Convert(daysExpression, typeof(int)),
+ nameof(TimeSpan.Hours)
+ => sqlExpressionFactory.Convert(
+ sqlExpressionFactory.Modulo(
+ sqlExpressionFactory.Multiply(daysExpression, sqlExpressionFactory.Constant(24.0)),
+ sqlExpressionFactory.Constant(24.0)),
+ returnType),
+ nameof(TimeSpan.Minutes)
+ => sqlExpressionFactory.Convert(
+ sqlExpressionFactory.Modulo(
+ sqlExpressionFactory.Multiply(daysExpression, sqlExpressionFactory.Constant(1440.0)),
+ sqlExpressionFactory.Constant(60.0)),
+ returnType),
+ nameof(TimeSpan.Seconds)
+ => sqlExpressionFactory.Convert(
+ sqlExpressionFactory.Modulo(
+ sqlExpressionFactory.Multiply(daysExpression, sqlExpressionFactory.Constant(86400.0)),
+ sqlExpressionFactory.Constant(60.0)),
+ returnType),
+ nameof(TimeSpan.Milliseconds)
+ => sqlExpressionFactory.Convert(
+ sqlExpressionFactory.Modulo(
+ sqlExpressionFactory.Multiply(daysExpression, sqlExpressionFactory.Constant(86400000.0)),
+ sqlExpressionFactory.Constant(1000.0)),
+ returnType),
+ nameof(TimeSpan.Microseconds)
+ => sqlExpressionFactory.Convert(
+ sqlExpressionFactory.Modulo(
+ sqlExpressionFactory.Multiply(daysExpression, sqlExpressionFactory.Constant(86400000000.0)),
+ sqlExpressionFactory.Constant(1000.0)),
+ returnType),
+ nameof(TimeSpan.Nanoseconds)
+ => sqlExpressionFactory.Convert(
+ sqlExpressionFactory.Modulo(
+ sqlExpressionFactory.Multiply(daysExpression, sqlExpressionFactory.Constant(86400000000000.0)),
+ sqlExpressionFactory.Constant(1000.0)),
+ returnType),
+ nameof(TimeSpan.Ticks)
+ => sqlExpressionFactory.Convert(
+ sqlExpressionFactory.Multiply(
+ daysExpression,
+ sqlExpressionFactory.Constant((double)TimeSpan.TicksPerDay)),
+ typeof(long)),
+ nameof(TimeSpan.TotalDays)
+ => daysExpression,
+ nameof(TimeSpan.TotalHours)
+ => sqlExpressionFactory.Multiply(daysExpression, sqlExpressionFactory.Constant(24.0)),
+ nameof(TimeSpan.TotalMinutes)
+ => sqlExpressionFactory.Multiply(daysExpression, sqlExpressionFactory.Constant(1440.0)),
+ nameof(TimeSpan.TotalSeconds)
+ => sqlExpressionFactory.Multiply(daysExpression, sqlExpressionFactory.Constant(86400.0)),
+ nameof(TimeSpan.TotalMilliseconds)
+ => sqlExpressionFactory.Multiply(daysExpression, sqlExpressionFactory.Constant(86400000.0)),
+ _ => null
+ };
+ }
+}
diff --git a/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteTimeSpanMethodTranslator.cs b/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteTimeSpanMethodTranslator.cs
new file mode 100644
index 00000000000..17b086325f2
--- /dev/null
+++ b/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteTimeSpanMethodTranslator.cs
@@ -0,0 +1,111 @@
+// 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;
+
+// ReSharper disable once CheckNamespace
+namespace Microsoft.EntityFrameworkCore.Sqlite.Query.Internal;
+
+///
+/// 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 class SqliteTimeSpanMethodTranslator(SqliteSqlExpressionFactory sqlExpressionFactory) : IMethodCallTranslator
+{
+ private static readonly MethodInfo DurationMethod
+ = typeof(TimeSpan).GetRuntimeMethod(nameof(TimeSpan.Duration), Type.EmptyTypes)!;
+
+ private static readonly MethodInfo FromDaysMethod
+ = typeof(TimeSpan).GetRuntimeMethod(nameof(TimeSpan.FromDays), [typeof(double)])!;
+
+ private static readonly MethodInfo FromHoursMethod
+ = typeof(TimeSpan).GetRuntimeMethod(nameof(TimeSpan.FromHours), [typeof(double)])!;
+
+ private static readonly MethodInfo FromMinutesMethod
+ = typeof(TimeSpan).GetRuntimeMethod(nameof(TimeSpan.FromMinutes), [typeof(double)])!;
+
+ private static readonly MethodInfo FromSecondsMethod
+ = typeof(TimeSpan).GetRuntimeMethod(nameof(TimeSpan.FromSeconds), [typeof(double)])!;
+
+ private static readonly MethodInfo FromMillisecondsMethod
+ = typeof(TimeSpan).GetRuntimeMethod(nameof(TimeSpan.FromMilliseconds), [typeof(double)])!;
+
+ private static readonly MethodInfo FromTicksMethod
+ = typeof(TimeSpan).GetRuntimeMethod(nameof(TimeSpan.FromTicks), [typeof(long)])!;
+
+ ///
+ /// 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 SqlExpression? Translate(
+ SqlExpression? instance,
+ MethodInfo method,
+ IReadOnlyList arguments,
+ IDiagnosticsLogger logger)
+ {
+ if (method.DeclaringType != typeof(TimeSpan))
+ {
+ return null;
+ }
+
+ if (DurationMethod.Equals(method) && instance != null)
+ {
+ var daysExpression = sqlExpressionFactory.EfDays(instance);
+ var absExpression = sqlExpressionFactory.Function(
+ "abs",
+ [daysExpression],
+ nullable: true,
+ argumentsPropagateNullability: Statics.TrueArrays[1],
+ typeof(double));
+ return sqlExpressionFactory.EfTimespan(absExpression);
+ }
+
+ if (instance != null)
+ {
+ return null;
+ }
+
+ if (FromDaysMethod.Equals(method) && arguments.Count == 1)
+ {
+ return sqlExpressionFactory.EfTimespan(arguments[0]);
+ }
+
+ if (FromHoursMethod.Equals(method) && arguments.Count == 1)
+ {
+ return sqlExpressionFactory.EfTimespan(
+ sqlExpressionFactory.Divide(arguments[0], sqlExpressionFactory.Constant(24.0)));
+ }
+
+ if (FromMinutesMethod.Equals(method) && arguments.Count == 1)
+ {
+ return sqlExpressionFactory.EfTimespan(
+ sqlExpressionFactory.Divide(arguments[0], sqlExpressionFactory.Constant(1440.0)));
+ }
+
+ if (FromSecondsMethod.Equals(method) && arguments.Count == 1)
+ {
+ return sqlExpressionFactory.EfTimespan(
+ sqlExpressionFactory.Divide(arguments[0], sqlExpressionFactory.Constant(86400.0)));
+ }
+
+ if (FromMillisecondsMethod.Equals(method) && arguments.Count == 1)
+ {
+ return sqlExpressionFactory.EfTimespan(
+ sqlExpressionFactory.Divide(arguments[0], sqlExpressionFactory.Constant(86400000.0)));
+ }
+
+ if (FromTicksMethod.Equals(method) && arguments.Count == 1)
+ {
+ return sqlExpressionFactory.EfTimespan(
+ sqlExpressionFactory.Divide(
+ sqlExpressionFactory.Convert(arguments[0], typeof(double)),
+ sqlExpressionFactory.Constant(864000000000.0)));
+ }
+
+ return null;
+ }
+}
diff --git a/src/EFCore.Sqlite.Core/Storage/Internal/SqliteRelationalConnection.cs b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteRelationalConnection.cs
index 942c1eda177..2c33a4f7421 100644
--- a/src/EFCore.Sqlite.Core/Storage/Internal/SqliteRelationalConnection.cs
+++ b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteRelationalConnection.cs
@@ -190,6 +190,31 @@ private void InitializeDbConnection(DbConnection connection)
(x, y) => decimal.Compare(
decimal.Parse(x, NumberStyles.Number, CultureInfo.InvariantCulture),
decimal.Parse(y, NumberStyles.Number, CultureInfo.InvariantCulture)));
+
+ sqliteConnection.CreateFunction(
+ "ef_days",
+ value => value == null ? null : value.Value.TotalDays,
+ isDeterministic: true);
+
+ sqliteConnection.CreateFunction(
+ "ef_days",
+ value => value == null ? null : TimeSpan.Parse(value).TotalDays,
+ isDeterministic: true);
+
+ sqliteConnection.CreateFunction(
+ "ef_timespan",
+ value =>
+ {
+ if (value == null)
+ {
+ return null;
+ }
+
+ var totalDays = value.Value;
+ var ticks = (long)Math.Round(totalDays * TimeSpan.TicksPerDay);
+ return new TimeSpan(ticks);
+ },
+ isDeterministic: true);
}
else
{
diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/Translations/Temporal/DateTimeTranslationsCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/Translations/Temporal/DateTimeTranslationsCosmosTest.cs
index f5fa9c5a1cd..e18d055bd78 100644
--- a/test/EFCore.Cosmos.FunctionalTests/Query/Translations/Temporal/DateTimeTranslationsCosmosTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/Query/Translations/Temporal/DateTimeTranslationsCosmosTest.cs
@@ -162,10 +162,10 @@ public override async Task TimeOfDay()
AssertSql();
}
- public override async Task subtract_and_TotalDays()
+ public override async Task Subtract_and_TotalDays()
{
// Cosmos client evaluation. Issue #17246.
- await AssertTranslationFailed(() => base.subtract_and_TotalDays());
+ await AssertTranslationFailed(() => base.Subtract_and_TotalDays());
AssertSql();
}
diff --git a/test/EFCore.Specification.Tests/Query/Translations/Temporal/DateTimeTranslationsTestBase.cs b/test/EFCore.Specification.Tests/Query/Translations/Temporal/DateTimeTranslationsTestBase.cs
index f5b7d32b375..39f05804fcb 100644
--- a/test/EFCore.Specification.Tests/Query/Translations/Temporal/DateTimeTranslationsTestBase.cs
+++ b/test/EFCore.Specification.Tests/Query/Translations/Temporal/DateTimeTranslationsTestBase.cs
@@ -79,7 +79,7 @@ public virtual Task TimeOfDay()
=> AssertQuery(ss => ss.Set().Where(o => o.DateTime.TimeOfDay == TimeSpan.Zero));
[ConditionalFact]
- public virtual Task subtract_and_TotalDays()
+ public virtual Task Subtract_and_TotalDays()
{
var date = new DateTime(1997, 1, 1);
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/Translations/Temporal/DateTimeTranslationsSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/Translations/Temporal/DateTimeTranslationsSqlServerTest.cs
index d8d1a3e4433..b89e29ebba1 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/Translations/Temporal/DateTimeTranslationsSqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/Translations/Temporal/DateTimeTranslationsSqlServerTest.cs
@@ -188,8 +188,8 @@ WHERE CONVERT(time, [b].[DateTime]) = '00:00:00'
""");
}
- public override Task subtract_and_TotalDays()
- => AssertTranslationFailed(() => base.subtract_and_TotalDays());
+ public override Task Subtract_and_TotalDays()
+ => AssertTranslationFailed(() => base.Subtract_and_TotalDays());
public override async Task Parse_with_constant()
{
diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/Translations/Temporal/DateTimeTranslationsSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/Translations/Temporal/DateTimeTranslationsSqliteTest.cs
index a7970c2d4f2..5c9f10858cf 100644
--- a/test/EFCore.Sqlite.FunctionalTests/Query/Translations/Temporal/DateTimeTranslationsSqliteTest.cs
+++ b/test/EFCore.Sqlite.FunctionalTests/Query/Translations/Temporal/DateTimeTranslationsSqliteTest.cs
@@ -186,8 +186,19 @@ WHERE rtrim(rtrim(strftime('%H:%M:%f', "b"."DateTime"), '0'), '.') = '00:00:00'
""");
}
- public override Task subtract_and_TotalDays()
- => AssertTranslationFailed(() => base.subtract_and_TotalDays());
+ public override async Task Subtract_and_TotalDays()
+ {
+ await base.Subtract_and_TotalDays();
+
+ AssertSql(
+ """
+@date='1997-01-01T00:00:00.0000000' (DbType = DateTime)
+
+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 ef_days(ef_timespan(julianday("b"."DateTime") - julianday(@date))) > 365.0
+""");
+ }
public override async Task Parse_with_constant()
{
diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/Translations/Temporal/TimeSpanTranslationsSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/Translations/Temporal/TimeSpanTranslationsSqliteTest.cs
index 919280880c3..4f83f6b346e 100644
--- a/test/EFCore.Sqlite.FunctionalTests/Query/Translations/Temporal/TimeSpanTranslationsSqliteTest.cs
+++ b/test/EFCore.Sqlite.FunctionalTests/Query/Translations/Temporal/TimeSpanTranslationsSqliteTest.cs
@@ -12,51 +12,76 @@ public TimeSpanTranslationsSqliteTest(BasicTypesQuerySqliteFixture fixture, ITes
Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper);
}
- // Translate TimeSpan members, #18844
public override async Task Hours()
{
- await AssertTranslationFailed(() => base.Hours());
-
- AssertSql();
+ await base.Hours();
+
+ 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 CAST((ef_days("b"."TimeSpan") * 24.0) % 24.0 AS INTEGER) = 3
+""");
}
- // Translate TimeSpan members, #18844
public override async Task Minutes()
{
- await AssertTranslationFailed(() => base.Minutes());
-
- AssertSql();
+ await base.Minutes();
+
+ 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 CAST((ef_days("b"."TimeSpan") * 1440.0) % 60.0 AS INTEGER) = 4
+""");
}
public override async Task Seconds()
{
- await AssertTranslationFailed(() => base.Seconds());
-
- AssertSql();
+ await base.Seconds();
+
+ 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 CAST((ef_days("b"."TimeSpan") * 86400.0) % 60.0 AS INTEGER) = 5
+""");
}
- // Translate TimeSpan members, #18844
public override async Task Milliseconds()
{
- await AssertTranslationFailed(() => base.Milliseconds());
-
- AssertSql();
+ await base.Milliseconds();
+
+ 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 CAST((ef_days("b"."TimeSpan") * 86400000.0) % 1000.0 AS INTEGER) = 678
+""");
}
- // Translate TimeSpan members, #18844
public override async Task Microseconds()
{
- await AssertTranslationFailed(() => base.Microseconds());
-
- AssertSql();
+ await base.Microseconds();
+
+ 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 CAST((ef_days("b"."TimeSpan") * 86400000000.0) % 1000.0 AS INTEGER) = 912
+""");
}
- // Translate TimeSpan members, #18844
public override async Task Nanoseconds()
{
- await AssertTranslationFailed(() => base.Nanoseconds());
-
- AssertSql();
+ await base.Nanoseconds();
+
+ 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 CAST((ef_days("b"."TimeSpan") * 86400000000000.0) % 1000.0 AS INTEGER) = 400
+""");
}
[ConditionalFact]