diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosMemberTranslatorProvider.cs b/src/EFCore.Cosmos/Query/Internal/CosmosMemberTranslatorProvider.cs index 3acf24c955a..b8c8f8c042b 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosMemberTranslatorProvider.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosMemberTranslatorProvider.cs @@ -1,6 +1,8 @@ // 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.Internal; + namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; /// @@ -27,8 +29,9 @@ public CosmosMemberTranslatorProvider( _plugins.AddRange(plugins.SelectMany(p => p.Translators)); _translators.AddRange( [ - new CosmosStringMemberTranslator(sqlExpressionFactory), - new CosmosDateTimeMemberTranslator(sqlExpressionFactory) + new CosmosDateTimeMemberTranslator(sqlExpressionFactory), + new CosmosNullableMemberTranslator(sqlExpressionFactory), + new CosmosStringMemberTranslator(sqlExpressionFactory) ]); } diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosMethodCallTranslatorProvider.cs b/src/EFCore.Cosmos/Query/Internal/CosmosMethodCallTranslatorProvider.cs index 65ea3e9d188..0d871e9230a 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosMethodCallTranslatorProvider.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosMethodCallTranslatorProvider.cs @@ -28,11 +28,12 @@ public CosmosMethodCallTranslatorProvider( _translators.AddRange( [ + new CosmosDateTimeMethodTranslator(sqlExpressionFactory), new CosmosEqualsTranslator(sqlExpressionFactory), - new CosmosStringMethodTranslator(sqlExpressionFactory), - new CosmosRandomTranslator(sqlExpressionFactory), new CosmosMathTranslator(sqlExpressionFactory), + new CosmosRandomTranslator(sqlExpressionFactory), new CosmosRegexTranslator(sqlExpressionFactory), + new CosmosStringMethodTranslator(sqlExpressionFactory), new CosmosTypeCheckingTranslator(sqlExpressionFactory) //new LikeTranslator(sqlExpressionFactory), //new EnumHasFlagTranslator(sqlExpressionFactory), diff --git a/src/EFCore.Cosmos/Query/Internal/Translators/CosmosDateTimeMemberTranslator.cs b/src/EFCore.Cosmos/Query/Internal/Translators/CosmosDateTimeMemberTranslator.cs index eaa98ab331c..8a460c8547c 100644 --- a/src/EFCore.Cosmos/Query/Internal/Translators/CosmosDateTimeMemberTranslator.cs +++ b/src/EFCore.Cosmos/Query/Internal/Translators/CosmosDateTimeMemberTranslator.cs @@ -25,16 +25,34 @@ public class CosmosDateTimeMemberTranslator(ISqlExpressionFactory sqlExpressionF IDiagnosticsLogger logger) { var declaringType = member.DeclaringType; - if ((declaringType == typeof(DateTime) - || declaringType == typeof(DateTimeOffset)) - && member.Name == nameof(DateTime.UtcNow)) + + if (declaringType != typeof(DateTime) && declaringType != typeof(DateTimeOffset)) { - return sqlExpressionFactory.Function( - "GetCurrentDateTime", - [], - returnType); + return null; } - return null; + return member.Name switch + { + nameof(DateTime.Year) => DatePart("yyyy"), + nameof(DateTime.Month) => DatePart("mm"), + nameof(DateTime.Day) => DatePart("dd"), + nameof(DateTime.Hour) => DatePart("hh"), + nameof(DateTime.Minute) => DatePart("mi"), + nameof(DateTime.Second) => DatePart("ss"), + nameof(DateTime.Millisecond) => DatePart("ms"), + nameof(DateTime.Microsecond) => DatePart("mcs"), + nameof(DateTime.Nanosecond) => DatePart("ns"), + + nameof(DateTime.UtcNow) + => sqlExpressionFactory.Function( + "GetCurrentDateTime", + [], + returnType), + + _ => null + }; + + SqlFunctionExpression DatePart(string part) + => sqlExpressionFactory.Function("DateTimePart", arguments: [sqlExpressionFactory.Constant(part), instance!], returnType); } } diff --git a/src/EFCore.Cosmos/Query/Internal/Translators/CosmosDateTimeMethodTranslator.cs b/src/EFCore.Cosmos/Query/Internal/Translators/CosmosDateTimeMethodTranslator.cs new file mode 100644 index 00000000000..bbec5002a50 --- /dev/null +++ b/src/EFCore.Cosmos/Query/Internal/Translators/CosmosDateTimeMethodTranslator.cs @@ -0,0 +1,70 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore.Cosmos.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 CosmosDateTimeMethodTranslator(ISqlExpressionFactory sqlExpressionFactory) : IMethodCallTranslator +{ + private static readonly Dictionary MethodInfoDatePartMapping = new() + { + { typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddYears), [typeof(int)])!, "yyyy" }, + { typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddMonths), [typeof(int)])!, "mm" }, + { typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddDays), [typeof(double)])!, "dd" }, + { typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddHours), [typeof(double)])!, "hh" }, + { typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddMinutes), [typeof(double)])!, "mi" }, + { typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddSeconds), [typeof(double)])!, "ss" }, + { typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddMilliseconds), [typeof(double)])!, "ms" }, + { typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddMicroseconds), [typeof(double)])!, "mcs" }, + { typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddYears), [typeof(int)])!, "yyyy" }, + { typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddMonths), [typeof(int)])!, "mm" }, + { typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddDays), [typeof(double)])!, "dd" }, + { typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddHours), [typeof(double)])!, "hh" }, + { typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddMinutes), [typeof(double)])!, "mi" }, + { typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddSeconds), [typeof(double)])!, "ss" }, + { typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddMilliseconds), [typeof(double)])!, "ms" }, + { typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddMicroseconds), [typeof(double)])!, "mcs" } + }; + + private static readonly Dictionary MethodInfoDateDiffMapping = new() + { + { typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.ToUnixTimeSeconds), Type.EmptyTypes)!, "second" }, + { typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.ToUnixTimeMilliseconds), Type.EmptyTypes)!, "millisecond" } + }; + + /// + /// 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 SqlExpression? Translate( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + if (method.DeclaringType != typeof(DateTime) && method.DeclaringType != typeof(DateTimeOffset)) + { + return null; + } + + if (MethodInfoDatePartMapping.TryGetValue(method, out var datePart) + && instance != null) + { + return sqlExpressionFactory.Function( + "DateTimeAdd", + arguments: [sqlExpressionFactory.Constant(datePart), arguments[0], instance], + instance.Type, + instance.TypeMapping); + } + + return null; + } +} diff --git a/src/EFCore.Cosmos/Query/Internal/Translators/CosmosNullableMemberTranslator.cs b/src/EFCore.Cosmos/Query/Internal/Translators/CosmosNullableMemberTranslator.cs new file mode 100644 index 00000000000..0e5294664ef --- /dev/null +++ b/src/EFCore.Cosmos/Query/Internal/Translators/CosmosNullableMemberTranslator.cs @@ -0,0 +1,42 @@ +// 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.Cosmos.Query.Internal; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore.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 CosmosNullableMemberTranslator(ISqlExpressionFactory 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?.IsNullableValueType() == true + && instance != null) + { + return member.Name switch + { + nameof(Nullable.Value) => instance, + nameof(Nullable.HasValue) => sqlExpressionFactory.IsNotNull(instance), + _ => null + }; + } + + return null; + } +} diff --git a/src/EFCore.Relational/Query/Internal/Translators/NullableMemberTranslator.cs b/src/EFCore.Relational/Query/Internal/Translators/NullableMemberTranslator.cs index 167a17ec546..618d28e347b 100644 --- a/src/EFCore.Relational/Query/Internal/Translators/NullableMemberTranslator.cs +++ b/src/EFCore.Relational/Query/Internal/Translators/NullableMemberTranslator.cs @@ -12,21 +12,8 @@ namespace Microsoft.EntityFrameworkCore.Query.Internal; /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// -public class NullableMemberTranslator : IMemberTranslator +public class NullableMemberTranslator(ISqlExpressionFactory sqlExpressionFactory) : IMemberTranslator { - 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 NullableMemberTranslator(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 @@ -42,14 +29,12 @@ public NullableMemberTranslator(ISqlExpressionFactory sqlExpressionFactory) if (member.DeclaringType?.IsNullableValueType() == true && instance != null) { - switch (member.Name) + return member.Name switch { - case nameof(Nullable.Value): - return instance; - - case nameof(Nullable.HasValue): - return _sqlExpressionFactory.IsNotNull(instance); - } + nameof(Nullable.Value) => instance, + nameof(Nullable.HasValue) => sqlExpressionFactory.IsNotNull(instance), + _ => null + }; } return null; diff --git a/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerDateTimeMemberTranslator.cs b/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerDateTimeMemberTranslator.cs index ff31e7147f4..b2abb4be5cf 100644 --- a/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerDateTimeMemberTranslator.cs +++ b/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerDateTimeMemberTranslator.cs @@ -12,38 +12,11 @@ namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal; /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// -public class SqlServerDateTimeMemberTranslator : IMemberTranslator +public class SqlServerDateTimeMemberTranslator( + ISqlExpressionFactory sqlExpressionFactory, + IRelationalTypeMappingSource typeMappingSource) + : IMemberTranslator { - private static readonly Dictionary DatePartMapping - = new() - { - { nameof(DateTime.Year), "year" }, - { nameof(DateTime.Month), "month" }, - { nameof(DateTime.DayOfYear), "dayofyear" }, - { nameof(DateTime.Day), "day" }, - { nameof(DateTime.Hour), "hour" }, - { nameof(DateTime.Minute), "minute" }, - { nameof(DateTime.Second), "second" }, - { nameof(DateTime.Millisecond), "millisecond" } - }; - - private readonly ISqlExpressionFactory _sqlExpressionFactory; - private readonly IRelationalTypeMappingSource _typeMappingSource; - - /// - /// 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 SqlServerDateTimeMemberTranslator( - ISqlExpressionFactory sqlExpressionFactory, - IRelationalTypeMappingSource typeMappingSource) - { - _sqlExpressionFactory = sqlExpressionFactory; - _typeMappingSource = typeMappingSource; - } - /// /// 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 @@ -58,81 +31,93 @@ public SqlServerDateTimeMemberTranslator( { var declaringType = member.DeclaringType; - if (declaringType == typeof(DateTime) - || declaringType == typeof(DateTimeOffset)) + if (declaringType != typeof(DateTime) && declaringType != typeof(DateTimeOffset)) + { + return null; + } + + return member.Name switch { - var memberName = member.Name; + nameof(DateTime.Year) => DatePart("year"), + nameof(DateTime.Month) => DatePart("month"), + nameof(DateTime.DayOfYear) => DatePart("dayofyear"), + nameof(DateTime.Day) => DatePart("day"), + nameof(DateTime.Hour) => DatePart("hour"), + nameof(DateTime.Minute) => DatePart("minute"), + nameof(DateTime.Second) => DatePart("second"), + nameof(DateTime.Millisecond) => DatePart("millisecond"), - if (DatePartMapping.TryGetValue(memberName, out var datePart)) - { - return _sqlExpressionFactory.Function( - "DATEPART", - new[] { _sqlExpressionFactory.Fragment(datePart), instance! }, + nameof(DateTime.Date) + => sqlExpressionFactory.Function( + "CONVERT", + new[] { sqlExpressionFactory.Fragment("date"), instance! }, nullable: true, - argumentsPropagateNullability: new[] { false, true }, - returnType); - } + argumentsPropagateNullability: [false, true], + returnType, + declaringType == typeof(DateTime) + ? instance!.TypeMapping + : typeMappingSource.FindMapping(typeof(DateTime))), - switch (memberName) - { - case nameof(DateTime.Date): - return _sqlExpressionFactory.Function( - "CONVERT", - new[] { _sqlExpressionFactory.Fragment("date"), instance! }, - nullable: true, - argumentsPropagateNullability: new[] { false, true }, - returnType, - declaringType == typeof(DateTime) - ? instance!.TypeMapping - : _typeMappingSource.FindMapping(typeof(DateTime))); + nameof(DateTime.TimeOfDay) + => sqlExpressionFactory.Function( + "CONVERT", + new[] { sqlExpressionFactory.Fragment("time"), instance! }, + nullable: true, + argumentsPropagateNullability: [false, true], + returnType), - case nameof(DateTime.TimeOfDay): - return _sqlExpressionFactory.Function( - "CONVERT", - new[] { _sqlExpressionFactory.Fragment("time"), instance! }, - nullable: true, - argumentsPropagateNullability: new[] { false, true }, - returnType); + nameof(DateTime.Now) + => sqlExpressionFactory.Function( + declaringType == typeof(DateTime) ? "GETDATE" : "SYSDATETIMEOFFSET", + arguments: [], + nullable: false, + argumentsPropagateNullability: [], + returnType), - case nameof(DateTime.Now): - return _sqlExpressionFactory.Function( - declaringType == typeof(DateTime) ? "GETDATE" : "SYSDATETIMEOFFSET", - Enumerable.Empty(), - nullable: false, - argumentsPropagateNullability: Enumerable.Empty(), - returnType); + nameof(DateTime.UtcNow) + when declaringType == typeof(DateTime) + => sqlExpressionFactory.Function( + "GETUTCDATE", + arguments: [], + nullable: false, + argumentsPropagateNullability: [], + returnType), - case nameof(DateTime.UtcNow): - var serverTranslation = _sqlExpressionFactory.Function( - declaringType == typeof(DateTime) ? "GETUTCDATE" : "SYSUTCDATETIME", - Enumerable.Empty(), - nullable: false, - argumentsPropagateNullability: Enumerable.Empty(), - returnType); + nameof(DateTime.UtcNow) + when declaringType == typeof(DateTimeOffset) + => sqlExpressionFactory.Convert(sqlExpressionFactory.Function( + "SYSUTCDATETIME", + arguments: [], + nullable: false, + argumentsPropagateNullability: [], + returnType), returnType), - return declaringType == typeof(DateTime) - ? serverTranslation - : _sqlExpressionFactory.Convert(serverTranslation, returnType); + nameof(DateTime.Today) + => sqlExpressionFactory.Function( + "CONVERT", + new SqlExpression[] + { + sqlExpressionFactory.Fragment("date"), + sqlExpressionFactory.Function( + "GETDATE", + arguments: [], + nullable: false, + argumentsPropagateNullability: [], + typeof(DateTime)) + }, + nullable: true, + argumentsPropagateNullability: [false, true], + returnType), - case nameof(DateTime.Today): - return _sqlExpressionFactory.Function( - "CONVERT", - new SqlExpression[] - { - _sqlExpressionFactory.Fragment("date"), - _sqlExpressionFactory.Function( - "GETDATE", - Enumerable.Empty(), - nullable: false, - argumentsPropagateNullability: Enumerable.Empty(), - typeof(DateTime)) - }, - nullable: true, - argumentsPropagateNullability: new[] { false, true }, - returnType); - } - } + _ => null + }; - return null; + SqlFunctionExpression DatePart(string part) + => sqlExpressionFactory.Function( + "DATEPART", + arguments: [sqlExpressionFactory.Fragment(part), instance!], + nullable: true, + argumentsPropagateNullability: new[] { false, true }, + returnType); } } diff --git a/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerDateTimeMethodTranslator.cs b/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerDateTimeMethodTranslator.cs index c5d977c8727..b38e0261479 100644 --- a/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerDateTimeMethodTranslator.cs +++ b/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerDateTimeMethodTranslator.cs @@ -12,9 +12,12 @@ namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal; /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// -public class SqlServerDateTimeMethodTranslator : IMethodCallTranslator +public class SqlServerDateTimeMethodTranslator( + ISqlExpressionFactory sqlExpressionFactory, + IRelationalTypeMappingSource typeMappingSource) + : IMethodCallTranslator { - private readonly Dictionary _methodInfoDatePartMapping = new() + private static readonly Dictionary MethodInfoDatePartMapping = new() { { typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddYears), [typeof(int)])!, "year" }, { typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddMonths), [typeof(int)])!, "month" }, @@ -32,7 +35,7 @@ public class SqlServerDateTimeMethodTranslator : IMethodCallTranslator { typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddMilliseconds), [typeof(double)])!, "millisecond" } }; - private static readonly Dictionary _methodInfoDateDiffMapping = new() + private static readonly Dictionary MethodInfoDateDiffMapping = new() { { typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.ToUnixTimeSeconds), Type.EmptyTypes)!, "second" }, { typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.ToUnixTimeMilliseconds), Type.EmptyTypes)!, "millisecond" } @@ -46,23 +49,6 @@ public class SqlServerDateTimeMethodTranslator : IMethodCallTranslator .GetRuntimeMethod( nameof(SqlServerDbFunctionsExtensions.AtTimeZone), [typeof(DbFunctions), typeof(DateTime), typeof(string)])!; - private readonly ISqlExpressionFactory _sqlExpressionFactory; - private readonly IRelationalTypeMappingSource _typeMappingSource; - - /// - /// 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 SqlServerDateTimeMethodTranslator( - ISqlExpressionFactory sqlExpressionFactory, - IRelationalTypeMappingSource typeMappingSource) - { - _sqlExpressionFactory = sqlExpressionFactory; - _typeMappingSource = typeMappingSource; - } - /// /// 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 @@ -75,7 +61,7 @@ public SqlServerDateTimeMethodTranslator( IReadOnlyList arguments, IDiagnosticsLogger logger) { - if (_methodInfoDatePartMapping.TryGetValue(method, out var datePart) + if (MethodInfoDatePartMapping.TryGetValue(method, out var datePart) && instance != null) { // Some Add methods accept a double, and SQL Server DateAdd does not accept number argument outside of int range @@ -88,14 +74,14 @@ public SqlServerDateTimeMethodTranslator( // Our default mapping for DateTime is datetime2, so we force constants to be datetime instead here. if (instance is SqlConstantExpression instanceConstant) { - instance = instanceConstant.ApplyTypeMapping(_typeMappingSource.FindMapping(typeof(DateTime), "datetime")); + instance = instanceConstant.ApplyTypeMapping(typeMappingSource.FindMapping(typeof(DateTime), "datetime")); } - return _sqlExpressionFactory.Function( + return sqlExpressionFactory.Function( "DATEADD", - new[] { _sqlExpressionFactory.Fragment(datePart), _sqlExpressionFactory.Convert(arguments[0], typeof(int)), instance }, + arguments: [sqlExpressionFactory.Fragment(datePart), sqlExpressionFactory.Convert(arguments[0], typeof(int)), instance], nullable: true, - argumentsPropagateNullability: new[] { false, true, true }, + argumentsPropagateNullability: [false, true, true], instance.Type, instance.TypeMapping); } @@ -116,7 +102,7 @@ public SqlServerDateTimeMethodTranslator( "datetimeoffset" => operandTypeMapping, "datetime" or "datetime2" or "smalldatetime" - => _typeMappingSource.FindMapping( + => typeMappingSource.FindMapping( typeof(DateTimeOffset), "datetimeoffset", precision: operandTypeMapping.Precision), _ => null }; @@ -130,28 +116,28 @@ public SqlServerDateTimeMethodTranslator( { // Our constant representation for datetime/datetimeoffset is an untyped string literal, which the AT TIME ZONE expression // does not accept. Type it explicitly. - operand = _sqlExpressionFactory.Convert(operand, operand.Type); + operand = sqlExpressionFactory.Convert(operand, operand.Type); } return new AtTimeZoneExpression( operand, - _sqlExpressionFactory.ApplyTypeMapping(timeZone, _typeMappingSource.FindMapping("varchar")), + sqlExpressionFactory.ApplyTypeMapping(timeZone, typeMappingSource.FindMapping("varchar")), typeof(DateTimeOffset), resultTypeMapping); } - if (_methodInfoDateDiffMapping.TryGetValue(method, out var timePart)) + if (MethodInfoDateDiffMapping.TryGetValue(method, out var timePart)) { - return _sqlExpressionFactory.Function( + return sqlExpressionFactory.Function( "DATEDIFF_BIG", - new[] - { - _sqlExpressionFactory.Fragment(timePart), - _sqlExpressionFactory.Constant(DateTimeOffset.UnixEpoch, instance!.TypeMapping), + arguments: + [ + sqlExpressionFactory.Fragment(timePart), + sqlExpressionFactory.Constant(DateTimeOffset.UnixEpoch, instance!.TypeMapping), instance - }, + ], nullable: true, - argumentsPropagateNullability: new[] { false, true, true }, + argumentsPropagateNullability: [false, true, true], typeof(long)); } diff --git a/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteDateTimeMemberTranslator.cs b/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteDateTimeMemberTranslator.cs index 008b30360ab..954f1f694e2 100644 --- a/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteDateTimeMemberTranslator.cs +++ b/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteDateTimeMemberTranslator.cs @@ -12,34 +12,8 @@ namespace Microsoft.EntityFrameworkCore.Sqlite.Query.Internal; /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// -public class SqliteDateTimeMemberTranslator : IMemberTranslator +public class SqliteDateTimeMemberTranslator(SqliteSqlExpressionFactory sqlExpressionFactory) : IMemberTranslator { - private static readonly Dictionary DatePartMapping - = new() - { - { nameof(DateTime.Year), "%Y" }, - { nameof(DateTime.Month), "%m" }, - { nameof(DateTime.DayOfYear), "%j" }, - { nameof(DateTime.Day), "%d" }, - { nameof(DateTime.Hour), "%H" }, - { nameof(DateTime.Minute), "%M" }, - { nameof(DateTime.Second), "%S" }, - { nameof(DateTime.DayOfWeek), "%w" } - }; - - private readonly SqliteSqlExpressionFactory _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 SqliteDateTimeMemberTranslator(SqliteSqlExpressionFactory 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 @@ -52,112 +26,123 @@ public SqliteDateTimeMemberTranslator(SqliteSqlExpressionFactory sqlExpressionFa Type returnType, IDiagnosticsLogger logger) { - if (member.DeclaringType == typeof(DateTime)) + if (member.DeclaringType != typeof(DateTime)) { - var memberName = member.Name; + return null; + } - if (DatePartMapping.TryGetValue(memberName, out var datePart)) - { - return _sqlExpressionFactory.Convert( - _sqlExpressionFactory.Strftime( - typeof(string), - datePart, - instance!), - returnType); - } - - if (memberName == nameof(DateTime.Ticks)) - { - return _sqlExpressionFactory.Convert( - _sqlExpressionFactory.Multiply( - _sqlExpressionFactory.Subtract( - _sqlExpressionFactory.Function( + var memberName = member.Name; + + switch (memberName) + { + case nameof(DateTime.Year): + return DatePart("%Y"); + case nameof(DateTime.Month): + return DatePart("%m"); + case nameof(DateTime.DayOfYear): + return DatePart("%j"); + case nameof(DateTime.Day): + return DatePart("%d"); + case nameof(DateTime.Hour): + return DatePart("%H"); + case nameof(DateTime.Minute): + return DatePart("%M"); + case nameof(DateTime.Second): + return DatePart("%S"); + case nameof(DateTime.DayOfWeek): + return DatePart("%w"); + + case nameof(DateTime.Ticks): + return sqlExpressionFactory.Convert( + sqlExpressionFactory.Multiply( + sqlExpressionFactory.Subtract( + sqlExpressionFactory.Function( "julianday", new[] { instance! }, nullable: true, argumentsPropagateNullability: new[] { true }, typeof(double)), - _sqlExpressionFactory.Constant(1721425.5)), // NB: Result of julianday('0001-01-01 00:00:00') - _sqlExpressionFactory.Constant(TimeSpan.TicksPerDay)), + sqlExpressionFactory.Constant(1721425.5)), // NB: Result of julianday('0001-01-01 00:00:00') + sqlExpressionFactory.Constant(TimeSpan.TicksPerDay)), typeof(long)); - } - if (memberName == nameof(DateTime.Millisecond)) - { - return _sqlExpressionFactory.Modulo( - _sqlExpressionFactory.Multiply( - _sqlExpressionFactory.Convert( - _sqlExpressionFactory.Strftime( + case nameof(DateTime.Millisecond): + return sqlExpressionFactory.Modulo( + sqlExpressionFactory.Multiply( + sqlExpressionFactory.Convert( + sqlExpressionFactory.Strftime( typeof(string), "%f", instance!), typeof(double)), - _sqlExpressionFactory.Constant(1000)), - _sqlExpressionFactory.Constant(1000)); - } + sqlExpressionFactory.Constant(1000)), + sqlExpressionFactory.Constant(1000)); + } - var format = "%Y-%m-%d %H:%M:%f"; - SqlExpression timestring; - var modifiers = new List(); + var format = "%Y-%m-%d %H:%M:%f"; + SqlExpression timestring; + var modifiers = new List(); - switch (memberName) - { - case nameof(DateTime.Now): - timestring = _sqlExpressionFactory.Constant("now"); - modifiers.Add(_sqlExpressionFactory.Constant("localtime")); - break; - - case nameof(DateTime.UtcNow): - timestring = _sqlExpressionFactory.Constant("now"); - break; - - case nameof(DateTime.Date): - timestring = instance!; - modifiers.Add(_sqlExpressionFactory.Constant("start of day")); - break; - - case nameof(DateTime.Today): - timestring = _sqlExpressionFactory.Constant("now"); - modifiers.Add(_sqlExpressionFactory.Constant("localtime")); - modifiers.Add(_sqlExpressionFactory.Constant("start of day")); - break; - - case nameof(DateTime.TimeOfDay): - format = "%H:%M:%f"; - timestring = instance!; - break; - - default: - return null; - } - - Check.DebugAssert(timestring != null, "timestring is null"); - - return _sqlExpressionFactory.Function( - "rtrim", - new SqlExpression[] - { - _sqlExpressionFactory.Function( - "rtrim", - new SqlExpression[] - { - _sqlExpressionFactory.Strftime( - returnType, - format, - timestring, - modifiers), - _sqlExpressionFactory.Constant("0") - }, - nullable: true, - argumentsPropagateNullability: new[] { true, false }, - returnType), - _sqlExpressionFactory.Constant(".") - }, - nullable: true, - argumentsPropagateNullability: new[] { true, false }, - returnType); + switch (memberName) + { + case nameof(DateTime.Now): + timestring = sqlExpressionFactory.Constant("now"); + modifiers.Add(sqlExpressionFactory.Constant("localtime")); + break; + + case nameof(DateTime.UtcNow): + timestring = sqlExpressionFactory.Constant("now"); + break; + + case nameof(DateTime.Date): + timestring = instance!; + modifiers.Add(sqlExpressionFactory.Constant("start of day")); + break; + + case nameof(DateTime.Today): + timestring = sqlExpressionFactory.Constant("now"); + modifiers.Add(sqlExpressionFactory.Constant("localtime")); + modifiers.Add(sqlExpressionFactory.Constant("start of day")); + break; + + case nameof(DateTime.TimeOfDay): + format = "%H:%M:%f"; + timestring = instance!; + break; + + default: + return null; } - return null; + Check.DebugAssert(timestring != null, "timestring is null"); + + return sqlExpressionFactory.Function( + "rtrim", + new SqlExpression[] + { + sqlExpressionFactory.Function( + "rtrim", + new SqlExpression[] + { + sqlExpressionFactory.Strftime( + returnType, + format, + timestring, + modifiers), + sqlExpressionFactory.Constant("0") + }, + nullable: true, + argumentsPropagateNullability: new[] { true, false }, + returnType), + sqlExpressionFactory.Constant(".") + }, + nullable: true, + argumentsPropagateNullability: new[] { true, false }, + returnType); + + SqlExpression DatePart(string part) + => sqlExpressionFactory.Convert( + sqlExpressionFactory.Strftime(typeof(string), part, instance!), + returnType); } } diff --git a/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteDateTimeMethodTranslator.cs b/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteDateTimeMethodTranslator.cs index 09b6cef56d9..a547abab5cc 100644 --- a/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteDateTimeMethodTranslator.cs +++ b/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteDateTimeMethodTranslator.cs @@ -12,7 +12,7 @@ namespace Microsoft.EntityFrameworkCore.Sqlite.Query.Internal; /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// -public class SqliteDateTimeMethodTranslator : IMethodCallTranslator +public class SqliteDateTimeMethodTranslator(SqliteSqlExpressionFactory sqlExpressionFactory) : IMethodCallTranslator { private static readonly MethodInfo AddMilliseconds = typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddMilliseconds), [typeof(double)])!; @@ -20,7 +20,7 @@ private static readonly MethodInfo AddMilliseconds private static readonly MethodInfo AddTicks = typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddTicks), [typeof(long)])!; - private readonly Dictionary _methodInfoToUnitSuffix = new() + private static readonly Dictionary MethodInfoToUnitSuffix = new() { { typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddYears), [typeof(int)])!, " years" }, { typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddMonths), [typeof(int)])!, " months" }, @@ -33,19 +33,6 @@ private static readonly MethodInfo AddTicks { typeof(DateOnly).GetRuntimeMethod(nameof(DateOnly.AddDays), [typeof(int)])!, " days" } }; - private readonly SqliteSqlExpressionFactory _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 SqliteDateTimeMethodTranslator(SqliteSqlExpressionFactory 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 @@ -71,55 +58,55 @@ public SqliteDateTimeMethodTranslator(SqliteSqlExpressionFactory sqlExpressionFa SqlExpression? modifier = null; if (AddMilliseconds.Equals(method)) { - modifier = _sqlExpressionFactory.Add( - _sqlExpressionFactory.Convert( - _sqlExpressionFactory.Divide( + modifier = sqlExpressionFactory.Add( + sqlExpressionFactory.Convert( + sqlExpressionFactory.Divide( arguments[0], - _sqlExpressionFactory.Constant(1000.0)), + sqlExpressionFactory.Constant(1000.0)), typeof(string)), - _sqlExpressionFactory.Constant(" seconds")); + sqlExpressionFactory.Constant(" seconds")); } else if (AddTicks.Equals(method)) { - modifier = _sqlExpressionFactory.Add( - _sqlExpressionFactory.Convert( - _sqlExpressionFactory.Divide( + modifier = sqlExpressionFactory.Add( + sqlExpressionFactory.Convert( + sqlExpressionFactory.Divide( arguments[0], - _sqlExpressionFactory.Constant((double)TimeSpan.TicksPerSecond)), + sqlExpressionFactory.Constant((double)TimeSpan.TicksPerSecond)), typeof(string)), - _sqlExpressionFactory.Constant(" seconds")); + sqlExpressionFactory.Constant(" seconds")); } - else if (_methodInfoToUnitSuffix.TryGetValue(method, out var unitSuffix)) + else if (MethodInfoToUnitSuffix.TryGetValue(method, out var unitSuffix)) { - modifier = _sqlExpressionFactory.Add( - _sqlExpressionFactory.Convert(arguments[0], typeof(string)), - _sqlExpressionFactory.Constant(unitSuffix)); + modifier = sqlExpressionFactory.Add( + sqlExpressionFactory.Convert(arguments[0], typeof(string)), + sqlExpressionFactory.Constant(unitSuffix)); } if (modifier != null) { - return _sqlExpressionFactory.Function( + return sqlExpressionFactory.Function( "rtrim", new SqlExpression[] { - _sqlExpressionFactory.Function( + sqlExpressionFactory.Function( "rtrim", new SqlExpression[] { - _sqlExpressionFactory.Strftime( + sqlExpressionFactory.Strftime( method.ReturnType, "%Y-%m-%d %H:%M:%f", instance!, - new[] { modifier }), - _sqlExpressionFactory.Constant("0") + modifiers: [modifier]), + sqlExpressionFactory.Constant("0") }, nullable: true, - argumentsPropagateNullability: new[] { true, false }, + argumentsPropagateNullability: [true, false], method.ReturnType), - _sqlExpressionFactory.Constant(".") + sqlExpressionFactory.Constant(".") }, nullable: true, - argumentsPropagateNullability: new[] { true, false }, + argumentsPropagateNullability: [true, false], method.ReturnType); } @@ -131,17 +118,17 @@ public SqliteDateTimeMethodTranslator(SqliteSqlExpressionFactory sqlExpressionFa MethodInfo method, IReadOnlyList arguments) { - if (instance is not null && _methodInfoToUnitSuffix.TryGetValue(method, out var unitSuffix)) + if (instance is not null && MethodInfoToUnitSuffix.TryGetValue(method, out var unitSuffix)) { - return _sqlExpressionFactory.Date( + return sqlExpressionFactory.Date( method.ReturnType, instance, - new[] - { - _sqlExpressionFactory.Add( - _sqlExpressionFactory.Convert(arguments[0], typeof(string)), - _sqlExpressionFactory.Constant(unitSuffix)) - }); + modifiers: + [ + sqlExpressionFactory.Add( + sqlExpressionFactory.Convert(arguments[0], typeof(string)), + sqlExpressionFactory.Constant(unitSuffix)) + ]); } return null; diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs index f57b1a24b00..b97f09b9848 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs @@ -2423,29 +2423,77 @@ public override async Task Handle_materialization_properly_when_more_than_two_qu AssertSql(); } - public override async Task Parameter_extraction_short_circuits_1(bool async) - { - // Optimize query SQL. Issue #13159. - await AssertTranslationFailed(() => base.Parameter_extraction_short_circuits_1(async)); + public override Task Parameter_extraction_short_circuits_1(bool async) + => Fixture.NoSyncTest( + async, async a => + { + await base.Parameter_extraction_short_circuits_1(a); - AssertSql(); - } + // Optimize query SQL. Issue #13159. + AssertSql( + """ +@__dateFilter_Value_Month_0='7' +@__dateFilter_Value_Year_1='1996' - public override async Task Parameter_extraction_short_circuits_2(bool async) - { - // Cosmos client evaluation. Issue #17246. - await AssertTranslationFailed(() => base.Parameter_extraction_short_circuits_2(async)); +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "Order") AND ((c["OrderID"] < 10400) AND (false OR (((c["OrderDate"] != null) AND (DateTimePart("mm", c["OrderDate"]) = @__dateFilter_Value_Month_0)) AND (DateTimePart("yyyy", c["OrderDate"]) = @__dateFilter_Value_Year_1))))) +""", + // + """ +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "Order") AND (c["OrderID"] < 10400)) +"""); + }); - AssertSql(); - } + public override Task Parameter_extraction_short_circuits_2(bool async) + => Fixture.NoSyncTest( + async, async a => + { + await base.Parameter_extraction_short_circuits_2(a); - public override async Task Parameter_extraction_short_circuits_3(bool async) - { - // Optimize query SQL. Issue #13159. - await AssertTranslationFailed(() => base.Parameter_extraction_short_circuits_3(async)); + // Optimize query SQL. Issue #13159. + AssertSql( + """ +@__dateFilter_Value_Month_0='7' +@__dateFilter_Value_Year_1='1996' - AssertSql(); - } +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "Order") AND ((c["OrderID"] < 10400) AND (((c["OrderDate"] != null) AND (DateTimePart("mm", c["OrderDate"]) = @__dateFilter_Value_Month_0)) AND (DateTimePart("yyyy", c["OrderDate"]) = @__dateFilter_Value_Year_1)))) +""", + // + """ +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "Order") AND false) +"""); + }); + + public override Task Parameter_extraction_short_circuits_3(bool async) + => Fixture.NoSyncTest( + async, async a => + { + await base.Parameter_extraction_short_circuits_3(a); + + // Optimize query SQL. Issue #13159. + AssertSql( + """ +@__dateFilter_Value_Month_0='7' +@__dateFilter_Value_Year_1='1996' + +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "Order") AND ((c["OrderID"] < 10400) OR (((c["OrderDate"] != null) AND (DateTimePart("mm", c["OrderDate"]) = @__dateFilter_Value_Month_0)) AND (DateTimePart("yyyy", c["OrderDate"]) = @__dateFilter_Value_Year_1)))) +""", + // + """ +SELECT c +FROM root c +WHERE (c["Discriminator"] = "Order") +"""); + }); public override async Task Subquery_member_pushdown_does_not_change_original_subquery_model(bool async) { @@ -2533,7 +2581,7 @@ public override Task Select_expression_date_add_year(bool async) AssertSql( """ -SELECT c["OrderDate"] +SELECT DateTimeAdd("yyyy", 1, c["OrderDate"]) AS c FROM root c WHERE ((c["Discriminator"] = "Order") AND (c["OrderDate"] != null)) """); @@ -2547,7 +2595,7 @@ public override Task Select_expression_datetime_add_month(bool async) AssertSql( """ -SELECT c["OrderDate"] +SELECT DateTimeAdd("mm", 1, c["OrderDate"]) AS c FROM root c WHERE ((c["Discriminator"] = "Order") AND (c["OrderDate"] != null)) """); @@ -2561,7 +2609,7 @@ public override Task Select_expression_datetime_add_hour(bool async) AssertSql( """ -SELECT c["OrderDate"] +SELECT DateTimeAdd("hh", 1.0, c["OrderDate"]) AS c FROM root c WHERE ((c["Discriminator"] = "Order") AND (c["OrderDate"] != null)) """); @@ -2575,7 +2623,7 @@ public override Task Select_expression_datetime_add_minute(bool async) AssertSql( """ -SELECT c["OrderDate"] +SELECT DateTimeAdd("mi", 1.0, c["OrderDate"]) AS c FROM root c WHERE ((c["Discriminator"] = "Order") AND (c["OrderDate"] != null)) """); @@ -2589,7 +2637,7 @@ public override Task Select_expression_datetime_add_second(bool async) AssertSql( """ -SELECT c["OrderDate"] +SELECT DateTimeAdd("ss", 1.0, c["OrderDate"]) AS c FROM root c WHERE ((c["Discriminator"] = "Order") AND (c["OrderDate"] != null)) """); @@ -2603,7 +2651,7 @@ public override Task Select_expression_date_add_milliseconds_above_the_range(boo AssertSql( """ -SELECT c["OrderDate"] +SELECT DateTimeAdd("ms", 1000000000000.0, c["OrderDate"]) AS c FROM root c WHERE ((c["Discriminator"] = "Order") AND (c["OrderDate"] != null)) """); @@ -2617,7 +2665,7 @@ public override Task Select_expression_date_add_milliseconds_below_the_range(boo AssertSql( """ -SELECT c["OrderDate"] +SELECT DateTimeAdd("ms", -1000000000000.0, c["OrderDate"]) AS c FROM root c WHERE ((c["Discriminator"] = "Order") AND (c["OrderDate"] != null)) """); @@ -2631,7 +2679,7 @@ public override Task Select_expression_date_add_milliseconds_large_number_divide AssertSql( """ -SELECT c["OrderDate"] +SELECT c["OrderDate"], DateTimePart("ms", c["OrderDate"]) AS c FROM root c WHERE ((c["Discriminator"] = "Order") AND (c["OrderDate"] != null)) """); @@ -2652,13 +2700,21 @@ ORDER BY c["OrderID"] """); }); - public override async Task Select_expression_references_are_updated_correctly_with_subquery(bool async) - { - // Cosmos client evaluation. Issue #17246. - await AssertTranslationFailed(() => base.Select_expression_references_are_updated_correctly_with_subquery(async)); + public override Task Select_expression_references_are_updated_correctly_with_subquery(bool async) + => Fixture.NoSyncTest( + async, async a => + { + await base.Select_expression_references_are_updated_correctly_with_subquery(a); - AssertSql(); - } + AssertSql( + """ +@__nextYear_0='2017' + +SELECT DISTINCT DateTimePart("yyyy", c["OrderDate"]) AS c +FROM root c +WHERE (((c["Discriminator"] = "Order") AND (c["OrderDate"] != null)) AND (DateTimePart("yyyy", c["OrderDate"]) < @__nextYear_0)) +"""); + }); public override async Task DefaultIfEmpty_without_group_join(bool async) { diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindSelectQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindSelectQueryCosmosTest.cs index 0de240b54cc..db461fc5360 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindSelectQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindSelectQueryCosmosTest.cs @@ -712,18 +712,7 @@ public override async Task Projection_in_a_subquery_should_be_liftable(bool asyn } public override Task Projection_containing_DateTime_subtraction(bool async) - => Fixture.NoSyncTest( - async, async a => - { - await base.Projection_containing_DateTime_subtraction(a); - - AssertSql( - """ -SELECT c["OrderDate"] -FROM root c -WHERE ((c["Discriminator"] = "Order") AND (c["OrderID"] < 10300)) -"""); - }); + => Assert.ThrowsAsync(() => base.Projection_containing_DateTime_subtraction(async)); public override async Task Project_single_element_from_collection_with_OrderBy_Take_and_FirstOrDefault(bool async) { @@ -824,7 +813,7 @@ public override Task Select_datetime_year_component(bool async) AssertSql( """ -SELECT c["OrderDate"] +SELECT DateTimePart("yyyy", c["OrderDate"]) AS c FROM root c WHERE (c["Discriminator"] = "Order") """); @@ -838,7 +827,7 @@ public override Task Select_datetime_month_component(bool async) AssertSql( """ -SELECT c["OrderDate"] +SELECT DateTimePart("mm", c["OrderDate"]) AS c FROM root c WHERE (c["Discriminator"] = "Order") """); @@ -848,6 +837,7 @@ public override Task Select_datetime_day_of_year_component(bool async) => Fixture.NoSyncTest( async, async a => { + // DateTime.DayOfYear not supported by Cosmos await base.Select_datetime_day_of_year_component(a); AssertSql( @@ -866,7 +856,7 @@ public override Task Select_datetime_day_component(bool async) AssertSql( """ -SELECT c["OrderDate"] +SELECT DateTimePart("dd", c["OrderDate"]) AS c FROM root c WHERE (c["Discriminator"] = "Order") """); @@ -880,7 +870,7 @@ public override Task Select_datetime_hour_component(bool async) AssertSql( """ -SELECT c["OrderDate"] +SELECT DateTimePart("hh", c["OrderDate"]) AS c FROM root c WHERE (c["Discriminator"] = "Order") """); @@ -894,7 +884,7 @@ public override Task Select_datetime_minute_component(bool async) AssertSql( """ -SELECT c["OrderDate"] +SELECT DateTimePart("mi", c["OrderDate"]) AS c FROM root c WHERE (c["Discriminator"] = "Order") """); @@ -908,7 +898,7 @@ public override Task Select_datetime_second_component(bool async) AssertSql( """ -SELECT c["OrderDate"] +SELECT DateTimePart("ss", c["OrderDate"]) AS c FROM root c WHERE (c["Discriminator"] = "Order") """); @@ -922,7 +912,7 @@ public override Task Select_datetime_millisecond_component(bool async) AssertSql( """ -SELECT c["OrderDate"] +SELECT DateTimePart("ms", c["OrderDate"]) AS c FROM root c WHERE (c["Discriminator"] = "Order") """); @@ -1543,8 +1533,10 @@ public override Task Ternary_in_client_eval_assigns_correct_types(bool async) SELECT VALUE { "CustomerID" : c["CustomerID"], + "c" : (c["OrderDate"] != null), "OrderDate" : c["OrderDate"], - "c" : (c["OrderID"] - 10000) + "c0" : (c["OrderID"] - 10000), + "c1" : ((c["OrderDate"] != null) = false) } FROM root c WHERE ((c["Discriminator"] = "Order") AND (c["OrderID"] < 10300)) diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindWhereQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindWhereQueryCosmosTest.cs index 7b339e1b598..9d7781da38b 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindWhereQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindWhereQueryCosmosTest.cs @@ -1293,77 +1293,125 @@ public override async Task Where_datetime_date_component(bool async) AssertSql(); } - public override async Task Where_date_add_year_constant_component(bool async) - { - // Cosmos client evaluation. Issue #17246. - await AssertTranslationFailed(() => base.Where_date_add_year_constant_component(async)); + public override Task Where_date_add_year_constant_component(bool async) + => Fixture.NoSyncTest( + async, async a => + { + await base.Where_date_add_year_constant_component(a); - AssertSql(); - } + AssertSql( + """ +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "Order") AND (DateTimePart("yyyy", DateTimeAdd("yyyy", -1, c["OrderDate"])) = 1997)) +"""); + }); - public override async Task Where_datetime_year_component(bool async) - { - // Cosmos client evaluation. Issue #17246. - await AssertTranslationFailed(() => base.Where_datetime_year_component(async)); + public override Task Where_datetime_year_component(bool async) + => Fixture.NoSyncTest( + async, async a => + { + await base.Where_datetime_year_component(a); - AssertSql(); - } + AssertSql( + """ +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "Order") AND (DateTimePart("yyyy", c["OrderDate"]) = 1998)) +"""); + }); - public override async Task Where_datetime_month_component(bool async) - { - // Cosmos client evaluation. Issue #17246. - await AssertTranslationFailed(() => base.Where_datetime_month_component(async)); + public override Task Where_datetime_month_component(bool async) + => Fixture.NoSyncTest( + async, async a => + { + await base.Where_datetime_month_component(a); - AssertSql(); - } + AssertSql( + """ +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "Order") AND (DateTimePart("mm", c["OrderDate"]) = 4)) +"""); + }); public override async Task Where_datetime_dayOfYear_component(bool async) { - // Cosmos client evaluation. Issue #17246. + // DateTime.DayOfYear not supported by Cosmos await AssertTranslationFailed(() => base.Where_datetime_dayOfYear_component(async)); AssertSql(); } - public override async Task Where_datetime_day_component(bool async) - { - // Cosmos client evaluation. Issue #17246. - await AssertTranslationFailed(() => base.Where_datetime_day_component(async)); + public override Task Where_datetime_day_component(bool async) + => Fixture.NoSyncTest( + async, async a => + { + await base.Where_datetime_day_component(a); - AssertSql(); - } + AssertSql( + """ +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "Order") AND (DateTimePart("dd", c["OrderDate"]) = 4)) +"""); + }); - public override async Task Where_datetime_hour_component(bool async) - { - // Cosmos client evaluation. Issue #17246. - await AssertTranslationFailed(() => base.Where_datetime_hour_component(async)); + public override Task Where_datetime_hour_component(bool async) + => Fixture.NoSyncTest( + async, async a => + { + await base.Where_datetime_hour_component(a); - AssertSql(); - } + AssertSql( + """ +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "Order") AND (DateTimePart("hh", c["OrderDate"]) = 0)) +"""); + }); - public override async Task Where_datetime_minute_component(bool async) - { - // Cosmos client evaluation. Issue #17246. - await AssertTranslationFailed(() => base.Where_datetime_minute_component(async)); + public override Task Where_datetime_minute_component(bool async) + => Fixture.NoSyncTest( + async, async a => + { + await base.Where_datetime_minute_component(a); - AssertSql(); - } + AssertSql( + """ +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "Order") AND (DateTimePart("mi", c["OrderDate"]) = 0)) +"""); + }); - public override async Task Where_datetime_second_component(bool async) - { - // Cosmos client evaluation. Issue #17246. - await AssertTranslationFailed(() => base.Where_datetime_second_component(async)); + public override Task Where_datetime_second_component(bool async) + => Fixture.NoSyncTest( + async, async a => + { + await base.Where_datetime_second_component(a); - AssertSql(); - } + AssertSql( + """ +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "Order") AND (DateTimePart("ss", c["OrderDate"]) = 0)) +"""); + }); - public override async Task Where_datetime_millisecond_component(bool async) - { - // Cosmos client evaluation. Issue #17246. - await AssertTranslationFailed(() => base.Where_datetime_millisecond_component(async)); + public override Task Where_datetime_millisecond_component(bool async) + => Fixture.NoSyncTest( + async, async a => + { + await base.Where_datetime_millisecond_component(a); - AssertSql(); - } + AssertSql( + """ +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "Order") AND (DateTimePart("ms", c["OrderDate"]) = 0)) +"""); + }); public override async Task Where_datetimeoffset_now_component(bool async) { diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs index b1f35960b9c..cf3f7aaa11c 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs @@ -1566,7 +1566,7 @@ public override async Task Project_collection_of_datetimes_filtered(bool async) // Always throws for sync. if (async) { - await Assert.ThrowsAsync(() => base.Project_collection_of_datetimes_filtered(async)); + await Assert.ThrowsAsync(() => base.Project_collection_of_datetimes_filtered(async)); } } @@ -1656,8 +1656,32 @@ public override async Task Project_multiple_collections(bool async) // Always throws for sync. if (async) { - // TODO: Project out primitive collection subquery: #33797 - await Assert.ThrowsAsync(() => base.Project_multiple_collections(async)); + var exception = await Assert.ThrowsAsync(() => base.Project_multiple_collections(async)); + + Assert.Contains("'ORDER BY' is not supported in subqueries.", exception.Message); + + AssertSql( + """ +SELECT VALUE +{ + "Ints" : c["Ints"], + "c" : ARRAY( + SELECT VALUE i + FROM i IN c["Ints"] + ORDER BY i DESC), + "c0" : ARRAY( + SELECT VALUE i + FROM i IN c["DateTimes"] + WHERE (DateTimePart("dd", i) != 1)), + "c1" : ARRAY( + SELECT VALUE i + FROM i IN c["DateTimes"] + WHERE (i > "2000-01-01T00:00:00")) +} +FROM root c +WHERE (c["Discriminator"] = "PrimitiveCollectionsEntity") +ORDER BY c["Id"] +"""); } }