From 3eff31f33449289b62b0e8439c03c40480977633 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aykut=20C=C4=B1nc=C4=B1k?= Date: Mon, 1 Jun 2026 22:33:49 +0300 Subject: [PATCH] Translate TimeOnly.Hour/Minute/Second on SQLite Add SqliteTimeOnlyMemberTranslator mapping Hour, Minute, and Second to strftime('%H'/'%M'/'%S'), mirroring SqliteDateOnlyMemberTranslator. TimeOnly is stored as TEXT, so these read cleanly with no precision loss. Sub-second members (Millisecond/Microsecond/Nanosecond), AddHours/AddMinutes, and the TimeSpan-dependent members (Add/Subtract/FromTimeSpan, see #18844) are left as follow-ups. Part of #25103 --- .../SqliteMemberTranslatorProvider.cs | 3 +- .../SqliteTimeOnlyMemberTranslator.cs | 51 +++++++++++++++++++ .../TimeOnlyTranslationsSqliteTest.cs | 30 +++++++---- 3 files changed, 74 insertions(+), 10 deletions(-) create mode 100644 src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteTimeOnlyMemberTranslator.cs diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteMemberTranslatorProvider.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteMemberTranslatorProvider.cs index 25adce48e5f..e914fd1ebbb 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 SqliteTimeOnlyMemberTranslator(sqlExpressionFactory) ]); } } diff --git a/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteTimeOnlyMemberTranslator.cs b/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteTimeOnlyMemberTranslator.cs new file mode 100644 index 00000000000..6905bd78003 --- /dev/null +++ b/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteTimeOnlyMemberTranslator.cs @@ -0,0 +1,51 @@ +// 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 SqliteTimeOnlyMemberTranslator(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(TimeOnly) || instance is null) + { + return null; + } + + return member.Name switch + { + nameof(TimeOnly.Hour) => DatePart("%H"), + nameof(TimeOnly.Minute) => DatePart("%M"), + nameof(TimeOnly.Second) => DatePart("%S"), + + _ => null + }; + + SqlExpression DatePart(string datePart) + => sqlExpressionFactory.Convert( + sqlExpressionFactory.Strftime( + typeof(string), + datePart, + instance), + returnType); + } +} diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/Translations/Temporal/TimeOnlyTranslationsSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/Translations/Temporal/TimeOnlyTranslationsSqliteTest.cs index e784c3e03b2..eae438fecd7 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/Translations/Temporal/TimeOnlyTranslationsSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/Translations/Temporal/TimeOnlyTranslationsSqliteTest.cs @@ -14,26 +14,38 @@ public TimeOnlyTranslationsSqliteTest(BasicTypesQuerySqliteFixture fixture, ITes public override async Task Hour() { - // TimeSpan. Issue #18844. - await AssertTranslationFailed(() => base.Hour()); + await base.Hour(); - AssertSql(); + 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(strftime('%H', "b"."TimeOnly") AS INTEGER) = 15 +"""); } public override async Task Minute() { - // TimeSpan. Issue #18844. - await AssertTranslationFailed(() => base.Minute()); + await base.Minute(); - AssertSql(); + 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(strftime('%M', "b"."TimeOnly") AS INTEGER) = 30 +"""); } public override async Task Second() { - // TimeSpan. Issue #18844. - await AssertTranslationFailed(() => base.Second()); + await base.Second(); - AssertSql(); + 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(strftime('%S', "b"."TimeOnly") AS INTEGER) = 10 +"""); } public override async Task Millisecond()