From 13dd7f17f81780fddd61df09122d6f2a288639a8 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Thu, 27 May 2021 14:10:44 +0200 Subject: [PATCH] Implement DateOnly/TimeOnly support for Sqlite Closes #24506 --- .../Storage/DateOnlyTypeMapping.cs | 57 ++++++ .../Storage/TimeOnlyTypeMapping.cs | 61 ++++++ .../SqliteDateOnlyMemberTranslator.cs | 72 +++++++ .../Internal/SqliteDateTimeAddTranslator.cs | 42 ++++- .../SqliteMemberTranslatorProvider.cs | 4 +- .../Internal/SqliteDateOnlyTypeMapping.cs | 60 ++++++ .../Internal/SqliteTimeOnlyTypeMapping.cs | 60 ++++++ .../Internal/SqliteTypeMappingSource.cs | 2 + .../SqliteValueBinder.cs | 51 ++++- .../SqliteValueReader.cs | 31 +++ .../Query/GearsOfWarODataContext.cs | 8 + .../Storage/RelationalTypeMappingTest.cs | 27 +++ .../Query/GearsOfWarQueryTestBase.cs | 170 +++++++++++++++++ .../GearsOfWarModel/GearsOfWarData.cs | 12 +- .../TestModels/GearsOfWarModel/Mission.cs | 2 + .../Query/GearsOfWarQuerySqlServerFixture.cs | 20 +- .../Query/GearsOfWarQuerySqlServerTest.cs | 155 ++++++++++++++- .../TPTGearsOfWarQuerySqlServerFixture.cs | 8 + .../Query/TPTGearsOfWarQuerySqlServerTest.cs | 153 +++++++++++++++ .../Query/GearsOfWarQuerySqliteTest.cs | 177 ++++++++++++++++++ .../Query/TPTGearsOfWarQuerySqliteTest.cs | 81 ++++++++ .../Storage/SqliteTypeMappingTest.cs | 27 +++ .../SqliteDataReaderTest.cs | 24 +++ .../SqliteParameterTest.cs | 20 ++ 24 files changed, 1308 insertions(+), 16 deletions(-) create mode 100644 src/EFCore.Relational/Storage/DateOnlyTypeMapping.cs create mode 100644 src/EFCore.Relational/Storage/TimeOnlyTypeMapping.cs create mode 100644 src/EFCore.Sqlite.Core/Query/Internal/SqliteDateOnlyMemberTranslator.cs create mode 100644 src/EFCore.Sqlite.Core/Storage/Internal/SqliteDateOnlyTypeMapping.cs create mode 100644 src/EFCore.Sqlite.Core/Storage/Internal/SqliteTimeOnlyTypeMapping.cs diff --git a/src/EFCore.Relational/Storage/DateOnlyTypeMapping.cs b/src/EFCore.Relational/Storage/DateOnlyTypeMapping.cs new file mode 100644 index 00000000000..85d26f19040 --- /dev/null +++ b/src/EFCore.Relational/Storage/DateOnlyTypeMapping.cs @@ -0,0 +1,57 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Data; + +namespace Microsoft.EntityFrameworkCore.Storage +{ + /// + /// + /// Represents the mapping between a .NET type and a database type. + /// + /// + /// This type is typically used by database providers (and other extensions). It is generally + /// not used in application code. + /// + /// + public class DateOnlyTypeMapping : RelationalTypeMapping + { + private const string DateOnlyFormatConst = @"{0:yyyy-MM-dd}"; + + /// + /// Initializes a new instance of the class. + /// + /// The name of the database type. + /// The to be used. + public DateOnlyTypeMapping( + string storeType, + DbType? dbType = null) + : base(storeType, typeof(DateOnly), dbType) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Parameter object for . + protected DateOnlyTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + } + + /// + /// Creates a copy of this mapping. + /// + /// The parameters for this mapping. + /// The newly created mapping. + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new DateOnlyTypeMapping(parameters); + + /// + /// Gets the string format to be used to generate SQL literals of this type. + /// + protected override string SqlLiteralFormatString + => "DATE '" + DateOnlyFormatConst + "'"; + } +} diff --git a/src/EFCore.Relational/Storage/TimeOnlyTypeMapping.cs b/src/EFCore.Relational/Storage/TimeOnlyTypeMapping.cs new file mode 100644 index 00000000000..1a0cb5f15ad --- /dev/null +++ b/src/EFCore.Relational/Storage/TimeOnlyTypeMapping.cs @@ -0,0 +1,61 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Data; +using System.Globalization; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore.Storage +{ + /// + /// + /// Represents the mapping between a .NET type and a database type. + /// + /// + /// This type is typically used by database providers (and other extensions). It is generally + /// not used in application code. + /// + /// + public class TimeOnlyTypeMapping : RelationalTypeMapping + { + /// + /// Initializes a new instance of the class. + /// + /// The name of the database type. + /// The to be used. + public TimeOnlyTypeMapping( + string storeType, + DbType? dbType = null) + : base(storeType, typeof(TimeOnly), dbType) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Parameter object for . + protected TimeOnlyTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + } + + /// + /// Creates a copy of this mapping. + /// + /// The parameters for this mapping. + /// The newly created mapping. + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new TimeOnlyTypeMapping(parameters); + + /// + protected override string GenerateNonNullSqlLiteral(object value) + { + var timeOnly = (TimeOnly)value; + + return timeOnly.Ticks % TimeSpan.TicksPerSecond == 0 + ? FormattableString.Invariant($@"TIME '{value:HH\:mm\:ss}'") + : FormattableString.Invariant($@"TIME '{value:HH\:mm\:ss\.FFFFFFF}'"); + } + } +} diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteDateOnlyMemberTranslator.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteDateOnlyMemberTranslator.cs new file mode 100644 index 00000000000..55eef434a79 --- /dev/null +++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteDateOnlyMemberTranslator.cs @@ -0,0 +1,72 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Reflection; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; +using Microsoft.EntityFrameworkCore.Utilities; + +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 SqliteDateOnlyMemberTranslator : IMemberTranslator + { + private static readonly Dictionary _datePartMapping + = new() + { + { nameof(DateOnly.Year), "%Y" }, + { nameof(DateOnly.Month), "%m" }, + { nameof(DateOnly.DayOfYear), "%j" }, + { nameof(DateOnly.Day), "%d" }, + { nameof(DateOnly.DayOfWeek), "%w" } + }; + + private readonly ISqlExpressionFactory _sqlExpressionFactory; + + /// + /// 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 SqliteDateOnlyMemberTranslator(ISqlExpressionFactory sqlExpressionFactory) + { + _sqlExpressionFactory = sqlExpressionFactory; + } + + /// + /// 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) + { + Check.NotNull(member, nameof(member)); + Check.NotNull(returnType, nameof(returnType)); + Check.NotNull(logger, nameof(logger)); + + return member.DeclaringType == typeof(DateOnly) && _datePartMapping.TryGetValue(member.Name, out var datePart) + ? _sqlExpressionFactory.Convert( + SqliteExpression.Strftime( + _sqlExpressionFactory, + typeof(string), + datePart, + instance!), + returnType) + : null; + } + } +} diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteDateTimeAddTranslator.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteDateTimeAddTranslator.cs index acdaa523112..e18be4896b3 100644 --- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteDateTimeAddTranslator.cs +++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteDateTimeAddTranslator.cs @@ -32,7 +32,11 @@ private static readonly MethodInfo _addTicks { typeof(DateTime).GetRequiredRuntimeMethod(nameof(DateTime.AddDays), new[] { typeof(double) }), " days" }, { typeof(DateTime).GetRequiredRuntimeMethod(nameof(DateTime.AddHours), new[] { typeof(double) }), " hours" }, { typeof(DateTime).GetRequiredRuntimeMethod(nameof(DateTime.AddMinutes), new[] { typeof(double) }), " minutes" }, - { typeof(DateTime).GetRequiredRuntimeMethod(nameof(DateTime.AddSeconds), new[] { typeof(double) }), " seconds" } + { typeof(DateTime).GetRequiredRuntimeMethod(nameof(DateTime.AddSeconds), new[] { typeof(double) }), " seconds" }, + + { typeof(DateOnly).GetRequiredRuntimeMethod(nameof(DateOnly.AddYears), new[] { typeof(int) }), " years" }, + { typeof(DateOnly).GetRequiredRuntimeMethod(nameof(DateOnly.AddMonths), new[] { typeof(int) }), " months" }, + { typeof(DateOnly).GetRequiredRuntimeMethod(nameof(DateOnly.AddDays), new[] { typeof(int) }), " days" }, }; private readonly ISqlExpressionFactory _sqlExpressionFactory; @@ -64,6 +68,18 @@ public SqliteDateTimeAddTranslator(ISqlExpressionFactory sqlExpressionFactory) Check.NotNull(arguments, nameof(arguments)); Check.NotNull(logger, nameof(logger)); + return method.DeclaringType == typeof(DateTime) + ? TranslateDateTime(instance, method, arguments) + : method.DeclaringType == typeof(DateOnly) + ? TranslateDateOnly(instance, method, arguments) + : null; + } + + private SqlExpression? TranslateDateTime( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments) + { SqlExpression? modifier = null; if (_addMilliseconds.Equals(method)) { @@ -122,5 +138,29 @@ public SqliteDateTimeAddTranslator(ISqlExpressionFactory sqlExpressionFactory) return null; } + + private SqlExpression? TranslateDateOnly( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments) + { + if (instance is not null && _methodInfoToUnitSuffix.TryGetValue(method, out var unitSuffix)) + { + return _sqlExpressionFactory.Function( + "date", + 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/SqliteMemberTranslatorProvider.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteMemberTranslatorProvider.cs index 0bdebdab542..1280b6a0e3c 100644 --- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteMemberTranslatorProvider.cs +++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteMemberTranslatorProvider.cs @@ -27,7 +27,9 @@ public SqliteMemberTranslatorProvider(RelationalMemberTranslatorProviderDependen AddTranslators( new IMemberTranslator[] { - new SqliteDateTimeMemberTranslator(sqlExpressionFactory), new SqliteStringLengthTranslator(sqlExpressionFactory) + new SqliteDateTimeMemberTranslator(sqlExpressionFactory), + new SqliteStringLengthTranslator(sqlExpressionFactory), + new SqliteDateOnlyMemberTranslator(sqlExpressionFactory) }); } } diff --git a/src/EFCore.Sqlite.Core/Storage/Internal/SqliteDateOnlyTypeMapping.cs b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteDateOnlyTypeMapping.cs new file mode 100644 index 00000000000..dbe0ca40abe --- /dev/null +++ b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteDateOnlyTypeMapping.cs @@ -0,0 +1,60 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Data; +using Microsoft.EntityFrameworkCore.Storage; + +namespace Microsoft.EntityFrameworkCore.Sqlite.Storage.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 SqliteDateOnlyTypeMapping : DateOnlyTypeMapping + { + private const string DateOnlyFormatConst = @"'{0:yyyy\-MM\-dd}'"; + + /// + /// 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 SqliteDateOnlyTypeMapping( + string storeType, + DbType? dbType = null) + : base(storeType, dbType) + { + } + + /// + /// 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. + /// + protected SqliteDateOnlyTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + } + + /// + /// Creates a copy of this mapping. + /// + /// The parameters for this mapping. + /// The newly created mapping. + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new SqliteDateOnlyTypeMapping(parameters); + + /// + /// 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. + /// + protected override string SqlLiteralFormatString + => DateOnlyFormatConst; + } +} diff --git a/src/EFCore.Sqlite.Core/Storage/Internal/SqliteTimeOnlyTypeMapping.cs b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteTimeOnlyTypeMapping.cs new file mode 100644 index 00000000000..aada79a4110 --- /dev/null +++ b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteTimeOnlyTypeMapping.cs @@ -0,0 +1,60 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Data; +using Microsoft.EntityFrameworkCore.Storage; + +namespace Microsoft.EntityFrameworkCore.Sqlite.Storage.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 SqliteTimeOnlyTypeMapping : TimeOnlyTypeMapping + { + /// + /// 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 SqliteTimeOnlyTypeMapping( + string storeType, + DbType? dbType = null) + : base(storeType, dbType) + { + } + + /// + /// 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. + /// + protected SqliteTimeOnlyTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + } + + /// + /// Creates a copy of this mapping. + /// + /// The parameters for this mapping. + /// The newly created mapping. + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new SqliteTimeOnlyTypeMapping(parameters); + + /// + protected override string GenerateNonNullSqlLiteral(object value) + { + var timeOnly = (TimeOnly)value; + + return timeOnly.Ticks % TimeSpan.TicksPerSecond == 0 + ? FormattableString.Invariant($@"'{value:HH\:mm\:ss}'") + : FormattableString.Invariant($@"'{value:HH\:mm\:ss\.fffffff}'"); + } + } +} diff --git a/src/EFCore.Sqlite.Core/Storage/Internal/SqliteTypeMappingSource.cs b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteTypeMappingSource.cs index e640b3fd259..efc6cc10bac 100644 --- a/src/EFCore.Sqlite.Core/Storage/Internal/SqliteTypeMappingSource.cs +++ b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteTypeMappingSource.cs @@ -88,6 +88,8 @@ private static readonly HashSet _spatialiteTypes { typeof(DateTime), new SqliteDateTimeTypeMapping(TextTypeName) }, { typeof(DateTimeOffset), new SqliteDateTimeOffsetTypeMapping(TextTypeName) }, { typeof(TimeSpan), new TimeSpanTypeMapping(TextTypeName) }, + { typeof(DateOnly), new SqliteDateOnlyTypeMapping(TextTypeName) }, + { typeof(TimeOnly), new SqliteTimeOnlyTypeMapping(TextTypeName) }, { typeof(decimal), new SqliteDecimalTypeMapping(TextTypeName) }, { typeof(double), _real }, { typeof(float), new FloatTypeMapping(RealTypeName) }, diff --git a/src/Microsoft.Data.Sqlite.Core/SqliteValueBinder.cs b/src/Microsoft.Data.Sqlite.Core/SqliteValueBinder.cs index 52201522ab7..090be6f965b 100644 --- a/src/Microsoft.Data.Sqlite.Core/SqliteValueBinder.cs +++ b/src/Microsoft.Data.Sqlite.Core/SqliteValueBinder.cs @@ -112,6 +112,38 @@ public virtual void Bind() BindText(value); } } +#if NET6_0_OR_GREATER + else if (type == typeof(DateOnly)) + { + var dateOnly = (DateOnly)_value; + if (_sqliteType == SqliteType.Real) + { + var value = ToJulianDate(dateOnly.Year, dateOnly.Month, dateOnly.Day, 0, 0, 0, 0); + BindDouble(value); + } + else + { + var value = dateOnly.ToString(@"yyyy\-MM\-dd", CultureInfo.InvariantCulture); + BindText(value); + } + } + else if (type == typeof(TimeOnly)) + { + var timeOnly = (TimeOnly)_value; + if (_sqliteType == SqliteType.Real) + { + var value = GetTotalDays(timeOnly.Hour, timeOnly.Minute, timeOnly.Second, timeOnly.Millisecond); + BindDouble(value); + } + else + { + var value = timeOnly.Ticks % 10000000 == 0 + ? timeOnly.ToString(@"HH:mm:ss", CultureInfo.InvariantCulture) + : timeOnly.ToString(@"HH:mm:ss.fffffff", CultureInfo.InvariantCulture); + BindText(value); + } + } +#endif else if (type == typeof(DBNull)) { BindNull(); @@ -247,11 +279,15 @@ internal static SqliteType GetSqliteType(object? value) } private static double ToJulianDate(DateTime dateTime) + => ToJulianDate( + dateTime.Year, dateTime.Month, dateTime.Day, dateTime.Hour, dateTime.Minute, dateTime.Second, dateTime.Millisecond); + + private static double ToJulianDate(int year, int month, int day, int hour, int minute, int second, int millisecond) { // computeJD - var Y = dateTime.Year; - var M = dateTime.Month; - var D = dateTime.Day; + var Y = year; + var M = month; + var D = day; if (M <= 2) { @@ -265,7 +301,14 @@ private static double ToJulianDate(DateTime dateTime) var X2 = 306001 * (M + 1) / 10000; var iJD = (long)((X1 + X2 + D + B - 1524.5) * 86400000); - iJD += dateTime.Hour * 3600000 + dateTime.Minute * 60000 + (long)((dateTime.Second + dateTime.Millisecond / 1000.0) * 1000); + iJD += hour * 3600000 + minute * 60000 + (long)((second + millisecond / 1000.0) * 1000); + + return iJD / 86400000.0; + } + + private static double GetTotalDays(int hour, int minute, int second, int millisecond) + { + var iJD = hour * 3600000 + minute * 60000 + (long)((second + millisecond / 1000.0) * 1000); return iJD / 86400000.0; } diff --git a/src/Microsoft.Data.Sqlite.Core/SqliteValueReader.cs b/src/Microsoft.Data.Sqlite.Core/SqliteValueReader.cs index efaacfe4b95..d53862f4690 100644 --- a/src/Microsoft.Data.Sqlite.Core/SqliteValueReader.cs +++ b/src/Microsoft.Data.Sqlite.Core/SqliteValueReader.cs @@ -68,6 +68,25 @@ public virtual DateTimeOffset GetDateTimeOffset(int ordinal) } } +#if NET6_0_OR_GREATER + public virtual DateOnly GetDateOnly(int ordinal) + { + var sqliteType = GetSqliteType(ordinal); + switch (sqliteType) + { + case SQLITE_FLOAT: + case SQLITE_INTEGER: + return DateOnly.FromDateTime(FromJulianDate(GetDouble(ordinal))); + + default: + return DateOnly.Parse(GetString(ordinal), CultureInfo.InvariantCulture); + } + } + + public virtual TimeOnly GetTimeOnly(int ordinal) + => TimeOnly.Parse(GetString(ordinal), CultureInfo.InvariantCulture); +#endif + public virtual decimal GetDecimal(int ordinal) => decimal.Parse(GetString(ordinal), NumberStyles.Number | NumberStyles.AllowExponent, CultureInfo.InvariantCulture); @@ -169,6 +188,18 @@ public virtual string GetString(int ordinal) return (T)(object)GetDateTimeOffset(ordinal); } +#if NET6_0_OR_GREATER + if (type == typeof(DateOnly)) + { + return (T)(object)GetDateOnly(ordinal); + } + + if (type == typeof(TimeOnly)) + { + return (T)(object)GetTimeOnly(ordinal); + } +#endif + if (type == typeof(DBNull)) { // NB: NULL values handled above diff --git a/test/EFCore.OData.FunctionalTests/Query/GearsOfWarODataContext.cs b/test/EFCore.OData.FunctionalTests/Query/GearsOfWarODataContext.cs index a187fd36638..34742e9fff0 100644 --- a/test/EFCore.OData.FunctionalTests/Query/GearsOfWarODataContext.cs +++ b/test/EFCore.OData.FunctionalTests/Query/GearsOfWarODataContext.cs @@ -92,6 +92,14 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity().Property(l => l.Id).ValueGeneratedNever(); modelBuilder.Entity().Property(g => g.Location).HasColumnType("varchar(100)"); + + // No support yet for DateOnly/TimeOnly (#24507) + modelBuilder.Entity( + b => + { + b.Ignore(m => m.Date); + b.Ignore(m => m.Time); + }); } } } diff --git a/test/EFCore.Relational.Tests/Storage/RelationalTypeMappingTest.cs b/test/EFCore.Relational.Tests/Storage/RelationalTypeMappingTest.cs index 9005c2ac2d6..6204e0f9fe8 100644 --- a/test/EFCore.Relational.Tests/Storage/RelationalTypeMappingTest.cs +++ b/test/EFCore.Relational.Tests/Storage/RelationalTypeMappingTest.cs @@ -406,6 +406,33 @@ public virtual void DateTime_literal_generated_correctly() "TIMESTAMP '2015-03-12 13:36:37.3710000'"); } + [ConditionalFact] + public virtual void DateOnly_literal_generated_correctly() + { + Test_GenerateSqlLiteral_helper( + new DateOnlyTypeMapping("DateOnly"), + new DateOnly(2015, 3, 12), + "DATE '2015-03-12'"); + } + + [ConditionalFact] + public virtual void TimeOnly_literal_generated_correctly() + { + Test_GenerateSqlLiteral_helper( + new TimeOnlyTypeMapping("TimeOnly"), + new TimeOnly(13, 10, 15), + "TIME '13:10:15'"); + } + + [ConditionalFact] + public virtual void TimeOnly_literal_generated_correctly_with_milliseconds() + { + Test_GenerateSqlLiteral_helper( + new TimeOnlyTypeMapping("TimeOnly"), + new TimeOnly(13, 10, 15, 500), + "TIME '13:10:15.5'"); + } + [ConditionalFact] public virtual void Decimal_literal_generated_correctly() { diff --git a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs index 2831ed4c726..ddee8f46164 100644 --- a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs @@ -8796,6 +8796,176 @@ public virtual Task Correlated_collection_after_distinct_3_levels_without_origin }); } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Where_DateOnly_Year(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Where(m => m.Date.Year == 1990).AsTracking(), + entryCount: 1); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Where_DateOnly_Month(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Where(m => m.Date.Month == 11).AsTracking(), + entryCount: 1); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Where_DateOnly_Day(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Where(m => m.Date.Day == 10).AsTracking(), + entryCount: 1); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Where_DateOnly_DayOfYear(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Where(m => m.Date.DayOfYear == 314).AsTracking(), + entryCount: 1); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Where_DateOnly_DayOfWeek(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Where(m => m.Date.DayOfWeek == DayOfWeek.Saturday).AsTracking(), + entryCount: 1); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Where_DateOnly_AddYears(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Where(m => m.Date.AddYears(3) == new DateOnly(1993, 11, 10)).AsTracking(), + entryCount: 1); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Where_DateOnly_AddMonths(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Where(m => m.Date.AddMonths(3) == new DateOnly(1991, 2, 10)).AsTracking(), + entryCount: 1); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Where_DateOnly_AddDays(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Where(m => m.Date.AddDays(3) == new DateOnly(1990, 11, 13)).AsTracking(), + entryCount: 1); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Where_TimeOnly_Hour(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Where(m => m.Time.Hour == 10).AsTracking(), + entryCount: 1); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Where_TimeOnly_Minute(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Where(m => m.Time.Minute == 15).AsTracking(), + entryCount: 1); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Where_TimeOnly_Second(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Where(m => m.Time.Second == 50).AsTracking(), + entryCount: 1); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Where_TimeOnly_Millisecond(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Where(m => m.Time.Millisecond == 500).AsTracking(), + entryCount: 1); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Where_TimeOnly_AddHours(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Where(m => m.Time.AddHours(3) == new TimeOnly(13, 15, 50, 500)).AsTracking(), + entryCount: 1); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Where_TimeOnly_AddMinutes(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Where(m => m.Time.AddMinutes(3) == new TimeOnly(10, 18, 50, 500)).AsTracking(), + entryCount: 1); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Where_TimeOnly_Add_TimeSpan(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Where(m => m.Time.Add(new TimeSpan(3, 0, 0)) == new TimeOnly(13, 15, 50, 500)).AsTracking(), + entryCount: 1); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Where_TimeOnly_IsBetween(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Where(m => m.Time.IsBetween(new TimeOnly(10, 0, 0), new TimeOnly(11, 0, 0))).AsTracking(), + entryCount: 1); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Where_TimeOnly_subtract_TimeOnly(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Where(m => m.Time - new TimeOnly(10, 0, 0) == new TimeSpan(0, 0, 15, 50, 500)).AsTracking(), + entryCount: 1); + } + protected GearsOfWarContext CreateContext() => Fixture.CreateContext(); diff --git a/test/EFCore.Specification.Tests/TestModels/GearsOfWarModel/GearsOfWarData.cs b/test/EFCore.Specification.Tests/TestModels/GearsOfWarModel/GearsOfWarData.cs index ccf9da82e3c..d5a28acc1cb 100644 --- a/test/EFCore.Specification.Tests/TestModels/GearsOfWarModel/GearsOfWarData.cs +++ b/test/EFCore.Specification.Tests/TestModels/GearsOfWarModel/GearsOfWarData.cs @@ -132,7 +132,9 @@ public static IReadOnlyList CreateMissions() CodeName = "Lightmass Offensive", Rating = 2.1, Timeline = new DateTimeOffset(599898024001234567, new TimeSpan(1, 30, 0)), - Duration = new TimeSpan(1, 2, 3) + Duration = new TimeSpan(1, 2, 3), + Date = new DateOnly(2020, 1, 1), + Time = new TimeOnly(15, 30, 10) }, new() { @@ -140,7 +142,9 @@ public static IReadOnlyList CreateMissions() CodeName = "Hollow Storm", Rating = 4.2, Timeline = new DateTimeOffset(2, 3, 1, 8, 0, 0, new TimeSpan(-5, 0, 0)), - Duration = new TimeSpan(0, 1, 2, 3, 456) + Duration = new TimeSpan(0, 1, 2, 3, 456), + Date = new DateOnly(1990, 11, 10), + Time = new TimeOnly(10, 15, 50, 500) }, new() { @@ -148,7 +152,9 @@ public static IReadOnlyList CreateMissions() CodeName = "Halvo Bay defense", Rating = null, Timeline = new DateTimeOffset(10, 5, 3, 12, 0, 0, new TimeSpan()), - Duration = new TimeSpan(0, 1, 0, 15, 456) + Duration = new TimeSpan(0, 1, 0, 15, 456), + Date = new DateOnly(1, 1, 1), + Time = new TimeOnly(0, 0, 0) } }; diff --git a/test/EFCore.Specification.Tests/TestModels/GearsOfWarModel/Mission.cs b/test/EFCore.Specification.Tests/TestModels/GearsOfWarModel/Mission.cs index 24cd8a53ed2..45a447a86be 100644 --- a/test/EFCore.Specification.Tests/TestModels/GearsOfWarModel/Mission.cs +++ b/test/EFCore.Specification.Tests/TestModels/GearsOfWarModel/Mission.cs @@ -14,6 +14,8 @@ public class Mission public double? Rating { get; set; } public DateTimeOffset Timeline { get; set; } public TimeSpan Duration { get; set; } + public DateOnly Date { get; set; } + public TimeOnly Time { get; set; } public virtual ICollection ParticipatingSquads { get; set; } } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerFixture.cs b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerFixture.cs index a66cd5abbde..52359b2ccaf 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerFixture.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerFixture.cs @@ -17,13 +17,21 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con modelBuilder.Entity().Property(g => g.Location).HasColumnType("varchar(100)"); - // Full-text binary search - modelBuilder.Entity() - .Property("BriefingDocument"); + modelBuilder.Entity( + b => + { + // Full-text binary search + b.Property("BriefingDocument"); + b.Property("BriefingDocumentFileExtension").HasColumnType("nvarchar(16)"); + }); - modelBuilder.Entity() - .Property("BriefingDocumentFileExtension") - .HasColumnType("nvarchar(16)"); + // No support yet for DateOnly/TimeOnly (#24507) + modelBuilder.Entity( + b => + { + b.Ignore(m => m.Date); + b.Ignore(m => m.Time); + }); } protected override void Seed(GearsOfWarContext context) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs index 52bbfcc19db..de2ae245517 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs @@ -7897,7 +7897,7 @@ FROM [Weapons] AS [w] ) AS [t] ORDER BY [g].[Nickname], [g].[SquadId], [t].[IsAutomatic], [t].[Name]"); } - + public override async Task Correlated_collection_via_SelectMany_with_Distinct_missing_indentifying_columns_in_projection(bool async) { await base.Correlated_collection_via_SelectMany_with_Distinct_missing_indentifying_columns_in_projection(async); @@ -7968,6 +7968,159 @@ FROM [Weapons] AS [w] ORDER BY [t].[Length], [t2].[HasSoulPatch], [t2].[CityOfBirthName], [t2].[Id]"); } + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_DateOnly_Year(bool async) + { + await base.Where_DateOnly_Year(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_DateOnly_Month(bool async) + { + await base.Where_DateOnly_Month(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_DateOnly_Day(bool async) + { + await base.Where_DateOnly_Day(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_DateOnly_DayOfYear(bool async) + { + await base.Where_DateOnly_DayOfYear(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_DateOnly_DayOfWeek(bool async) + { + await base.Where_DateOnly_DayOfWeek(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_DateOnly_AddYears(bool async) + { + await base.Where_DateOnly_AddYears(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_DateOnly_AddMonths(bool async) + { + await base.Where_DateOnly_AddMonths(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_DateOnly_AddDays(bool async) + { + await base.Where_DateOnly_AddDays(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_TimeOnly_Hour(bool async) + { + await base.Where_TimeOnly_Hour(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_TimeOnly_Minute(bool async) + { + await base.Where_TimeOnly_Minute(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_TimeOnly_Second(bool async) + { + await base.Where_TimeOnly_Second(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_TimeOnly_Millisecond(bool async) + { + await base.Where_TimeOnly_Millisecond(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_TimeOnly_AddHours(bool async) + { + await base.Where_TimeOnly_AddHours(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_TimeOnly_AddMinutes(bool async) + { + await base.Where_TimeOnly_AddMinutes(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_TimeOnly_Add_TimeSpan(bool async) + { + await base.Where_TimeOnly_Add_TimeSpan(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_TimeOnly_IsBetween(bool async) + { + await base.Where_TimeOnly_IsBetween(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_TimeOnly_subtract_TimeOnly(bool async) + { + await base.Where_TimeOnly_subtract_TimeOnly(async); + + AssertSql(""); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerFixture.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerFixture.cs index 9791db25aca..03039c5c131 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerFixture.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerFixture.cs @@ -16,6 +16,14 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con base.OnModelCreating(modelBuilder, context); modelBuilder.Entity().Property(g => g.Location).HasColumnType("varchar(100)"); + + // No support yet for DateOnly/TimeOnly (#24507) + modelBuilder.Entity( + b => + { + b.Ignore(m => m.Date); + b.Ignore(m => m.Time); + }); } } } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs index a1081c42240..355de145da9 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs @@ -8912,6 +8912,159 @@ ELSE NULL END, [t].[Note]"); } + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_DateOnly_Year(bool async) + { + await base.Where_DateOnly_Year(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_DateOnly_Month(bool async) + { + await base.Where_DateOnly_Month(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_DateOnly_Day(bool async) + { + await base.Where_DateOnly_Day(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_DateOnly_DayOfYear(bool async) + { + await base.Where_DateOnly_DayOfYear(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_DateOnly_DayOfWeek(bool async) + { + await base.Where_DateOnly_DayOfWeek(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_DateOnly_AddYears(bool async) + { + await base.Where_DateOnly_AddYears(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_DateOnly_AddMonths(bool async) + { + await base.Where_DateOnly_AddMonths(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_DateOnly_AddDays(bool async) + { + await base.Where_DateOnly_AddDays(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_TimeOnly_Hour(bool async) + { + await base.Where_TimeOnly_Hour(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_TimeOnly_Minute(bool async) + { + await base.Where_TimeOnly_Minute(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_TimeOnly_Second(bool async) + { + await base.Where_TimeOnly_Second(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_TimeOnly_Millisecond(bool async) + { + await base.Where_TimeOnly_Millisecond(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_TimeOnly_AddHours(bool async) + { + await base.Where_TimeOnly_AddHours(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_TimeOnly_AddMinutes(bool async) + { + await base.Where_TimeOnly_AddMinutes(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_TimeOnly_Add_TimeSpan(bool async) + { + await base.Where_TimeOnly_Add_TimeSpan(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_TimeOnly_IsBetween(bool async) + { + await base.Where_TimeOnly_IsBetween(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_TimeOnly_subtract_TimeOnly(bool async) + { + await base.Where_TimeOnly_subtract_TimeOnly(async); + + AssertSql(""); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); } diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs index 23b9d99d2d2..795c24b83e2 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs @@ -341,6 +341,183 @@ public override Task Array_access_on_byte_array(bool async) return base.Array_access_on_byte_array(async); } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_DateOnly_Year(bool async) + { + await base.Where_DateOnly_Year(async); + + 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"") AS INTEGER) = 1990"); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_DateOnly_Month(bool async) + { + await base.Where_DateOnly_Month(async); + + AssertSql( + @"SELECT ""m"".""Id"", ""m"".""CodeName"", ""m"".""Date"", ""m"".""Duration"", ""m"".""Rating"", ""m"".""Time"", ""m"".""Timeline"" +FROM ""Missions"" AS ""m"" +WHERE CAST(strftime('%m', ""m"".""Date"") AS INTEGER) = 11"); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_DateOnly_Day(bool async) + { + await base.Where_DateOnly_Day(async); + + AssertSql( + @"SELECT ""m"".""Id"", ""m"".""CodeName"", ""m"".""Date"", ""m"".""Duration"", ""m"".""Rating"", ""m"".""Time"", ""m"".""Timeline"" +FROM ""Missions"" AS ""m"" +WHERE CAST(strftime('%d', ""m"".""Date"") AS INTEGER) = 10"); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_DateOnly_DayOfYear(bool async) + { + await base.Where_DateOnly_DayOfYear(async); + + AssertSql( + @"SELECT ""m"".""Id"", ""m"".""CodeName"", ""m"".""Date"", ""m"".""Duration"", ""m"".""Rating"", ""m"".""Time"", ""m"".""Timeline"" +FROM ""Missions"" AS ""m"" +WHERE CAST(strftime('%j', ""m"".""Date"") AS INTEGER) = 314"); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_DateOnly_DayOfWeek(bool async) + { + await base.Where_DateOnly_DayOfWeek(async); + + AssertSql( + @"SELECT ""m"".""Id"", ""m"".""CodeName"", ""m"".""Date"", ""m"".""Duration"", ""m"".""Rating"", ""m"".""Time"", ""m"".""Timeline"" +FROM ""Missions"" AS ""m"" +WHERE CAST(strftime('%w', ""m"".""Date"") AS INTEGER) = 6"); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_DateOnly_AddYears(bool async) + { + await base.Where_DateOnly_AddYears(async); + + 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') = '1993-11-10'"); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_DateOnly_AddMonths(bool async) + { + await base.Where_DateOnly_AddMonths(async); + + 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) || ' months') = '1991-02-10'"); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_DateOnly_AddDays(bool async) + { + await base.Where_DateOnly_AddDays(async); + + 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) || ' days') = '1990-11-13'"); + } + + [ConditionalTheory(Skip = "#18844")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_TimeOnly_Hour(bool async) + { + await base.Where_TimeOnly_Hour(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#18844")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_TimeOnly_Minute(bool async) + { + await base.Where_TimeOnly_Minute(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#18844")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_TimeOnly_Second(bool async) + { + await base.Where_TimeOnly_Second(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#18844")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_TimeOnly_Millisecond(bool async) + { + await base.Where_TimeOnly_Millisecond(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#18844")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_TimeOnly_AddHours(bool async) + { + await base.Where_TimeOnly_AddHours(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#18844")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_TimeOnly_AddMinutes(bool async) + { + await base.Where_TimeOnly_AddMinutes(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#18844")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_TimeOnly_Add_TimeSpan(bool async) + { + await base.Where_TimeOnly_Add_TimeSpan(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#18844")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_TimeOnly_IsBetween(bool async) + { + await base.Where_TimeOnly_IsBetween(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#18844")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_TimeOnly_subtract_TimeOnly(bool async) + { + await base.Where_TimeOnly_subtract_TimeOnly(async); + + AssertSql(""); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); } diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/TPTGearsOfWarQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/TPTGearsOfWarQuerySqliteTest.cs index da6dd054028..d8d4b1f16ee 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/TPTGearsOfWarQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/TPTGearsOfWarQuerySqliteTest.cs @@ -342,6 +342,87 @@ public override Task Array_access_on_byte_array(bool async) return base.Array_access_on_byte_array(async); } + [ConditionalTheory(Skip = "Issue#18844")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_TimeOnly_Hour(bool async) + { + await base.Where_TimeOnly_Hour(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "Issue#18844")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_TimeOnly_Minute(bool async) + { + await base.Where_TimeOnly_Minute(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "Issue#18844")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_TimeOnly_Second(bool async) + { + await base.Where_TimeOnly_Second(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "Issue#18844")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_TimeOnly_Millisecond(bool async) + { + await base.Where_TimeOnly_Millisecond(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "Issue#18844")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_TimeOnly_AddHours(bool async) + { + await base.Where_TimeOnly_AddHours(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "Issue#18844")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_TimeOnly_AddMinutes(bool async) + { + await base.Where_TimeOnly_AddMinutes(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "Issue#18844")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_TimeOnly_Add_TimeSpan(bool async) + { + await base.Where_TimeOnly_Add_TimeSpan(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "Issue#18844")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_TimeOnly_IsBetween(bool async) + { + await base.Where_TimeOnly_IsBetween(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "Issue#18844")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_TimeOnly_subtract_TimeOnly(bool async) + { + await base.Where_TimeOnly_subtract_TimeOnly(async); + + AssertSql(""); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); } diff --git a/test/EFCore.Sqlite.Tests/Storage/SqliteTypeMappingTest.cs b/test/EFCore.Sqlite.Tests/Storage/SqliteTypeMappingTest.cs index 47f14121b2c..c850c2c94c8 100644 --- a/test/EFCore.Sqlite.Tests/Storage/SqliteTypeMappingTest.cs +++ b/test/EFCore.Sqlite.Tests/Storage/SqliteTypeMappingTest.cs @@ -133,6 +133,33 @@ public override void DateTime_literal_generated_correctly() "'2015-03-12 13:36:37.371'"); } + [ConditionalFact] + public override void DateOnly_literal_generated_correctly() + { + Test_GenerateSqlLiteral_helper( + GetMapping(typeof(DateOnly)), + new DateOnly(2015, 3, 12), + "'2015-03-12'"); + } + + [ConditionalFact] + public override void TimeOnly_literal_generated_correctly() + { + Test_GenerateSqlLiteral_helper( + GetMapping(typeof(TimeOnly)), + new TimeOnly(13, 10, 15), + "'13:10:15'"); + } + + [ConditionalFact] + public override void TimeOnly_literal_generated_correctly_with_milliseconds() + { + Test_GenerateSqlLiteral_helper( + GetMapping(typeof(TimeOnly)), + new TimeOnly(13, 10, 15, 500), + "'13:10:15.5000000'"); + } + public override void Decimal_literal_generated_correctly() { var typeMapping = new SqliteDecimalTypeMapping("TEXT"); diff --git a/test/Microsoft.Data.Sqlite.Tests/SqliteDataReaderTest.cs b/test/Microsoft.Data.Sqlite.Tests/SqliteDataReaderTest.cs index 75a0325da68..7f40265523c 100644 --- a/test/Microsoft.Data.Sqlite.Tests/SqliteDataReaderTest.cs +++ b/test/Microsoft.Data.Sqlite.Tests/SqliteDataReaderTest.cs @@ -688,6 +688,30 @@ public void GetTimeSpan_throws_when_non_query() public void GetDateTimeOffset_throws_when_null() => GetX_throws_when_null(r => ((SqliteDataReader)r).GetDateTimeOffset(0)); + [Fact] + public void GetFieldValue_of_DateOnly_works() + => GetFieldValue_works( + "SELECT '2014-04-15';", + new DateOnly(2014, 4, 15)); + + [Fact] + public void GetFieldValue_of_DateOnly_works_with_real() + => GetFieldValue_works( + "SELECT julianday('2014-04-15');", + new DateOnly(2014, 4, 15)); + + [Fact] + public void GetFieldValue_of_TimeOnly_works() + => GetFieldValue_works( + "SELECT '13:10:15';", + new TimeOnly(13, 10, 15)); + + [Fact] + public void GetFieldValue_of_TimeOnly_works_with_milliseconds() + => GetFieldValue_works( + "SELECT '13:10:15.5';", + new TimeOnly(13, 10, 15, 500)); + [Theory] [InlineData("SELECT 1;", "INTEGER")] [InlineData("SELECT 3.14;", "REAL")] diff --git a/test/Microsoft.Data.Sqlite.Tests/SqliteParameterTest.cs b/test/Microsoft.Data.Sqlite.Tests/SqliteParameterTest.cs index 6bfc38c0c39..95d48023e97 100644 --- a/test/Microsoft.Data.Sqlite.Tests/SqliteParameterTest.cs +++ b/test/Microsoft.Data.Sqlite.Tests/SqliteParameterTest.cs @@ -257,6 +257,26 @@ public void Bind_works_when_DateTimeOffset_with_SqliteType_Real() 2456761.9680439816, SqliteType.Real); + [Fact] + public void Bind_works_when_DateOnly() + => Bind_works(new DateOnly(2014, 4, 14), "2014-04-14"); + + [Fact] + public void Bind_works_when_DateOnly_with_SqliteType_Real() + => Bind_works(new DateOnly(2014, 4, 14), 2456761.5, SqliteType.Real); + + [Fact] + public void Bind_works_when_TimeOnly() + => Bind_works(new TimeOnly(13, 10, 15), "13:10:15"); + + [Fact] + public void Bind_works_when_TimeOnly_with_milliseconds() + => Bind_works(new TimeOnly(13, 10, 15, 500), "13:10:15.5000000"); + + [Fact] + public void Bind_works_when_TimeOnly_with_SqliteType_Real() + => Bind_works(new TimeOnly(13, 10, 15), 0.5487847222222222, SqliteType.Real); + [Fact] public void Bind_works_when_DBNull() => Bind_works(DBNull.Value, DBNull.Value);