diff --git a/src/EFCore.Cosmos/EFCore.Cosmos.csproj b/src/EFCore.Cosmos/EFCore.Cosmos.csproj index dbd0cd838e7..c4ae8b7580f 100644 --- a/src/EFCore.Cosmos/EFCore.Cosmos.csproj +++ b/src/EFCore.Cosmos/EFCore.Cosmos.csproj @@ -9,6 +9,7 @@ true $(PackageTags);CosmosDb;SQL API true + $(NoWarn);EF9100 $(NoWarn);EF9101 $(NoWarn);EF9102 diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosMethodCallTranslatorProvider.cs b/src/EFCore.Cosmos/Query/Internal/CosmosMethodCallTranslatorProvider.cs index 166b809c8e5..01e85f91571 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosMethodCallTranslatorProvider.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosMethodCallTranslatorProvider.cs @@ -39,9 +39,9 @@ public CosmosMethodCallTranslatorProvider( new CosmosVectorSearchTranslator(sqlExpressionFactory, typeMappingSource), new CosmosFullTextSearchTranslator(sqlExpressionFactory, typeMappingSource), //new LikeTranslator(sqlExpressionFactory), - //new EnumHasFlagTranslator(sqlExpressionFactory), + new CosmosEnumMethodTranslator(sqlExpressionFactory), //new GetValueOrDefaultTranslator(sqlExpressionFactory), - //new ComparisonTranslator(sqlExpressionFactory), + new CosmosComparisonTranslator(sqlExpressionFactory), ]); } diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs index 2b664949ba8..9da77dc8b6d 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs @@ -88,6 +88,7 @@ protected override Expression VisitExtension(Expression expression) SourceExpression e => VisitSource(e), SqlBinaryExpression e => VisitSqlBinary(e), SqlConditionalExpression e => VisitSqlConditional(e), + CaseExpression e => VisitCase(e), SqlConstantExpression e => VisitSqlConstant(e), SqlFunctionExpression e => VisitSqlFunction(e), SqlParameterExpression e => VisitSqlParameter(e), @@ -749,6 +750,48 @@ protected virtual Expression VisitSqlConditional(SqlConditionalExpression sqlCon return sqlConditionalExpression; } + /// + /// Generates SQL for a CASE clause CASE/WHEN construct. + /// + /// The for which to generate SQL. + protected virtual Expression VisitCase(CaseExpression caseExpression) + { + //using (_sqlBuilder.Indent()) + { + foreach (var whenClause in caseExpression.WhenClauses) + { + _sqlBuilder.Append("IIF("); + + if (caseExpression.Operand != null) + { + Visit(caseExpression.Operand); + _sqlBuilder.Append(" = "); + } + + Visit(whenClause.Test); + + _sqlBuilder.Append(", "); + + Visit(whenClause.Result); + + _sqlBuilder.Append(", "); + } + + if (caseExpression.ElseResult != null) + { + Visit(caseExpression.ElseResult); + } + else + { + _sqlBuilder.Append("null"); + } + + _sqlBuilder.Append(new string(')', caseExpression.WhenClauses.Count)); + } + + return caseExpression; + } + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs index 8a4bbbb01b5..1c91681cdec 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs @@ -34,7 +34,7 @@ private static readonly MethodInfo StringEqualsWithStringComparisonStatic private static readonly MethodInfo GetTypeMethodInfo = typeof(object).GetTypeInfo().GetDeclaredMethod(nameof(GetType))!; private readonly IModel _model = queryCompilationContext.Model; - private readonly SqlTypeMappingVerifyingExpressionVisitor _sqlVerifyingExpressionVisitor = new(); + //private readonly SqlTypeMappingVerifyingExpressionVisitor _sqlVerifyingExpressionVisitor = new(); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -97,7 +97,7 @@ protected virtual void AddTranslationErrorDetails(string details) return null; } - _sqlVerifyingExpressionVisitor.Visit(translation); + //_sqlVerifyingExpressionVisitor.Visit(translation); } return translation; @@ -338,7 +338,23 @@ protected override Expression VisitExtension(Expression extensionExpression) return extensionExpression; case QueryParameterExpression queryParameter: - return new SqlParameterExpression(queryParameter.Name, queryParameter.Type, null); + // If we're precompiling a query, nullability information about reference type parameters has been extracted by the + // funcletizer and stored on the query compilation context; use that information when creating the SqlParameterExpression. + if (queryParameter.IsNonNullableReferenceType) + { + /*Check.DebugAssert( + _queryCompilationContext.IsPrecompiling, + "Parameters can only be known to has non-nullable reference types in query precompilation.");*/ + return new SqlParameterExpression( + name: queryParameter.Name, + queryParameter.Type, + typeMapping: null); + } + + return new SqlParameterExpression( + name: queryParameter.Name, + queryParameter.Type.UnwrapNullableType(), + typeMapping: null); case StructuralTypeShaperExpression shaper: return new StructuralTypeReferenceExpression(shaper); diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/CaseExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/CaseExpression.cs new file mode 100644 index 00000000000..a4016b96a81 --- /dev/null +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/CaseExpression.cs @@ -0,0 +1,168 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; + +/// +/// +/// An expression that represents a CASE statement in a SQL tree. +/// +/// +/// This type is typically used by database providers (and other extensions). It is generally +/// not used in application code. +/// +/// +public class CaseExpression : SqlExpression +{ + private readonly List _whenClauses = []; + + /// + /// Creates a new instance of the class which represents a simple CASE expression. + /// + /// An expression to compare with in . + /// A list of to compare or evaluate and get result from. + /// A value to return if no matches, if any. + public CaseExpression( + SqlExpression? operand, + IReadOnlyList whenClauses, + SqlExpression? elseResult = null) + : base(whenClauses[0].Result.Type, whenClauses[0].Result.TypeMapping) + { + Operand = operand; + _whenClauses.AddRange(whenClauses); + ElseResult = elseResult; + } + + /// + /// Creates a new instance of the class which represents a searched CASE expression. + /// + /// A list of to evaluate condition and get result from. + /// A value to return if no matches, if any. + public CaseExpression( + IReadOnlyList whenClauses, + SqlExpression? elseResult = null) + : this(null, whenClauses, elseResult) + { + } + + /// + /// The value to compare in . + /// + public virtual SqlExpression? Operand { get; } + + /// + /// The list of to match or evaluate condition to get result. + /// + public virtual IReadOnlyList WhenClauses + => _whenClauses; + + /// + /// The value to return if none of the matches. + /// + public virtual SqlExpression? ElseResult { get; } + + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + { + var operand = (SqlExpression?)visitor.Visit(Operand); + var changed = operand != Operand; + var whenClauses = new List(); + foreach (var whenClause in WhenClauses) + { + var test = (SqlExpression)visitor.Visit(whenClause.Test); + var result = (SqlExpression)visitor.Visit(whenClause.Result); + + if (test != whenClause.Test + || result != whenClause.Result) + { + changed = true; + whenClauses.Add(new CaseWhenClause(test, result)); + } + else + { + whenClauses.Add(whenClause); + } + } + + var elseResult = (SqlExpression?)visitor.Visit(ElseResult); + changed |= elseResult != ElseResult; + + return changed + ? new CaseExpression(operand, whenClauses, elseResult) + : this; + } + + /// + /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will + /// return this expression. + /// + /// The property of the result. + /// The property of the result. + /// The property of the result. + /// This expression if no children changed, or an expression with the updated children. + public virtual CaseExpression Update( + SqlExpression? operand, + IReadOnlyList whenClauses, + SqlExpression? elseResult) + => operand != Operand || !whenClauses.SequenceEqual(WhenClauses) || elseResult != ElseResult + ? new CaseExpression(operand, whenClauses, elseResult) + : this; + + /// + protected override void Print(ExpressionPrinter expressionPrinter) + { + expressionPrinter.Append("CASE"); + if (Operand != null) + { + expressionPrinter.Append(" "); + expressionPrinter.Visit(Operand); + } + + using (expressionPrinter.Indent()) + { + foreach (var whenClause in WhenClauses) + { + expressionPrinter.AppendLine().Append("WHEN "); + expressionPrinter.Visit(whenClause.Test); + expressionPrinter.Append(" THEN "); + expressionPrinter.Visit(whenClause.Result); + } + + if (ElseResult != null) + { + expressionPrinter.AppendLine().Append("ELSE "); + expressionPrinter.Visit(ElseResult); + } + } + + expressionPrinter.AppendLine().Append("END"); + } + + /// + public override bool Equals(object? obj) + => obj != null + && (ReferenceEquals(this, obj) + || obj is CaseExpression caseExpression + && Equals(caseExpression)); + + private bool Equals(CaseExpression caseExpression) + => base.Equals(caseExpression) + && (Operand?.Equals(caseExpression.Operand) ?? caseExpression.Operand == null) + && WhenClauses.SequenceEqual(caseExpression.WhenClauses) + && (ElseResult?.Equals(caseExpression.ElseResult) ?? caseExpression.ElseResult == null); + + /// + public override int GetHashCode() + { + var hash = new HashCode(); + hash.Add(base.GetHashCode()); + hash.Add(Operand); + for (var i = 0; i < WhenClauses.Count; i++) + { + hash.Add(WhenClauses[i]); + } + + hash.Add(ElseResult); + return hash.ToHashCode(); + } +} diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/CaseWhenClause.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/CaseWhenClause.cs new file mode 100644 index 00000000000..724f39b8162 --- /dev/null +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/CaseWhenClause.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; + +/// +/// +/// An object that represents a WHEN...THEN... construct in a SQL tree. +/// +/// +/// This type is typically used by database providers (and other extensions). It is generally +/// not used in application code. +/// +/// +public class CaseWhenClause +{ + /// + /// Creates a new instance of the class. + /// + /// A value to compare with or condition to evaluate. + /// A value to return if test succeeds. + public CaseWhenClause(SqlExpression test, SqlExpression result) + { + Test = test; + Result = result; + } + + /// + /// The value to compare with or the condition to evaluate. + /// + public virtual SqlExpression Test { get; } + + /// + /// The value to return if succeeds. + /// + public virtual SqlExpression Result { get; } + + /// + public override bool Equals(object? obj) + => obj != null + && (ReferenceEquals(this, obj) + || obj is CaseWhenClause caseWhenClause + && Equals(caseWhenClause)); + + private bool Equals(CaseWhenClause caseWhenClause) + => Test.Equals(caseWhenClause.Test) + && Result.Equals(caseWhenClause.Result); + + /// + public override int GetHashCode() + => HashCode.Combine(Test, Result); +} diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/StructuralTypeProjectionExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/StructuralTypeProjectionExpression.cs index bc1a0174599..62a08541a1a 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/StructuralTypeProjectionExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/StructuralTypeProjectionExpression.cs @@ -111,7 +111,7 @@ public virtual Expression BindProperty(IProperty property, bool clientEval) if (!_propertyExpressionsMap.TryGetValue(property, out var expression)) { expression = new ScalarAccessExpression( - Object, property.GetJsonPropertyName(), property.ClrType, property.GetTypeMapping()); + Object, property.GetJsonPropertyName(), property.ClrType.UnwrapNullableType(), property.GetTypeMapping()); _propertyExpressionsMap[property] = expression; } diff --git a/src/EFCore.Cosmos/Query/Internal/ISqlExpressionFactory.cs b/src/EFCore.Cosmos/Query/Internal/ISqlExpressionFactory.cs index 8763c2ba674..6f480f92453 100644 --- a/src/EFCore.Cosmos/Query/Internal/ISqlExpressionFactory.cs +++ b/src/EFCore.Cosmos/Query/Internal/ISqlExpressionFactory.cs @@ -294,4 +294,26 @@ SqlExpression ScoringFunction( /// doing so can result in application failures when updating to a new Entity Framework Core release. /// SqlExpression Constant(object? value, Type type, CoreTypeMapping? typeMapping = null); + + /// + /// Creates a new which represent a CASE statement in a SQL tree. + /// + /// An expression to compare with in . + /// A list of to compare or evaluate and get result from. + /// A value to return if no matches, if any. + /// An optional expression that can be re-used if it matches the new expression. + /// An expression representing a CASE statement in a SQL tree. + SqlExpression Case( + SqlExpression? operand, + IReadOnlyList whenClauses, + SqlExpression? elseResult, + SqlExpression? existingExpression = null); + + /// + /// Creates a new which represent a CASE statement in a SQL tree. + /// + /// A list of to evaluate condition and get result from. + /// A value to return if no matches, if any. + /// An expression representing a CASE statement in a SQL tree. + SqlExpression Case(IReadOnlyList whenClauses, SqlExpression? elseResult); } diff --git a/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs b/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs index c4b1ac4b1fa..0a788dd94de 100644 --- a/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs +++ b/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs @@ -43,6 +43,7 @@ public class SqlExpressionFactory(ITypeMappingSource typeMappingSource, IModel m null or { TypeMapping: not null } => sqlExpression, ScalarSubqueryExpression e => e.ApplyTypeMapping(typeMapping), + CaseExpression e => ApplyTypeMappingOnCase(e, typeMapping), SqlConditionalExpression sqlConditionalExpression => ApplyTypeMappingOnSqlConditional(sqlConditionalExpression, typeMapping), SqlBinaryExpression sqlBinaryExpression => ApplyTypeMappingOnSqlBinary(sqlBinaryExpression, typeMapping), SqlUnaryExpression sqlUnaryExpression => ApplyTypeMappingOnSqlUnary(sqlUnaryExpression, typeMapping), @@ -105,6 +106,24 @@ when sqlUnaryExpression.IsLogicalNot(): return new SqlUnaryExpression(sqlUnaryExpression.OperatorType, operand, resultType, resultTypeMapping); } + private SqlExpression ApplyTypeMappingOnCase( + CaseExpression caseExpression, + CoreTypeMapping? typeMapping) + { + var whenClauses = new List(); + foreach (var caseWhenClause in caseExpression.WhenClauses) + { + whenClauses.Add( + new CaseWhenClause( + caseWhenClause.Test, + ApplyTypeMapping(caseWhenClause.Result, typeMapping))); + } + + var elseResult = ApplyTypeMapping(caseExpression.ElseResult, typeMapping); + + return caseExpression.Update(caseExpression.Operand, whenClauses, elseResult); + } + private SqlExpression ApplyTypeMappingOnSqlBinary( SqlBinaryExpression sqlBinaryExpression, CoreTypeMapping? typeMapping) @@ -777,4 +796,150 @@ public virtual SqlExpression Constant(object value, CoreTypeMapping? typeMapping /// public virtual SqlExpression Constant(object? value, Type type, CoreTypeMapping? typeMapping = null) => new SqlConstantExpression(value, type, typeMapping); + + /// + public virtual SqlExpression Case( + SqlExpression? operand, + IReadOnlyList whenClauses, + SqlExpression? elseResult, + SqlExpression? existingExpression = null) + { + CoreTypeMapping? testTypeMapping; + if (operand == null) + { + testTypeMapping = _boolTypeMapping; + } + else + { + testTypeMapping = operand.TypeMapping + ?? whenClauses.Select(wc => wc.Test.TypeMapping).FirstOrDefault(t => t != null) + // Since we never look at type of Operand/Test after this place, + // we need to find actual typeMapping based on non-object type. + ?? new[] { operand.Type }.Concat(whenClauses.Select(wc => wc.Test.Type)) + .Where(t => t != typeof(object)).Select(t => typeMappingSource.FindMapping(t, model)) + .FirstOrDefault(); + + operand = ApplyTypeMapping(operand, testTypeMapping); + } + + var resultTypeMapping = elseResult?.TypeMapping + ?? whenClauses.Select(wc => wc.Result.TypeMapping).FirstOrDefault(t => t != null); + + elseResult = ApplyTypeMapping(elseResult, resultTypeMapping); + + var typeMappedWhenClauses = new List(); + foreach (var caseWhenClause in whenClauses) + { + var test = caseWhenClause.Test; + + if (operand == null && test is CaseExpression { Operand: null, WhenClauses: [var nestedSingleClause] } testExpr) + { + if (nestedSingleClause.Result is SqlConstantExpression { Value: true } + && testExpr.ElseResult is null or SqlConstantExpression { Value: false or null }) + { + // WHEN CASE + // WHEN x THEN TRUE + // ELSE FALSE/NULL + // END THEN y + // simplifies to + // WHEN x THEN y + test = nestedSingleClause.Test; + } + else if (nestedSingleClause.Result is SqlConstantExpression { Value: false or null } + && testExpr.ElseResult is SqlConstantExpression { Value: true }) + { + // same for the negated results + test = Not(nestedSingleClause.Test); + } + } + + typeMappedWhenClauses.Add( + new CaseWhenClause( + ApplyTypeMapping(test, testTypeMapping), + ApplyTypeMapping(caseWhenClause.Result, resultTypeMapping))); + } + + if (operand is null && elseResult is CaseExpression { Operand: null } nestedCaseExpression) + { + typeMappedWhenClauses.AddRange(nestedCaseExpression.WhenClauses); + elseResult = nestedCaseExpression.ElseResult; + } + + typeMappedWhenClauses = typeMappedWhenClauses + .Where(c => !IsSkipped(c)) + .TakeUpTo(IsMatched) + .DistinctBy(c => c.Test) + .ToList(); + + // CASE + // ... + // WHEN TRUE THEN a + // ELSE b + // END + // simplifies to + // CASE + // ... + // ELSE a + // END + if (typeMappedWhenClauses.Count > 0 && IsMatched(typeMappedWhenClauses[^1])) + { + elseResult = typeMappedWhenClauses[^1].Result; + typeMappedWhenClauses.RemoveAt(typeMappedWhenClauses.Count - 1); + } + + var nullResult = Constant(null, elseResult?.Type ?? whenClauses[0].Result.Type, resultTypeMapping); + + // if there are no whenClauses left (e.g. their tests evaluated to false): + // - if there is Else block, return it + // - if there is no Else block, return null + if (typeMappedWhenClauses.Count == 0) + { + return elseResult ?? nullResult; + } + + // omit `ELSE NULL` (this makes it easier to compare/reuse expressions) + if (elseResult is SqlConstantExpression { Value: null }) + { + elseResult = null; + } + + // CASE + // ... + // WHEN x THEN CASE + // WHEN y THEN a + // ELSE b + // END + // ELSE b + // END + // simplifies to + // CASE + // ... + // WHEN x AND y THEN a + // ELSE b + // END + if (operand == null + && typeMappedWhenClauses[^1].Result is CaseExpression { Operand: null, WhenClauses: [var lastClause] } lastCase + && Equals(elseResult, lastCase.ElseResult)) + { + typeMappedWhenClauses[^1] = new CaseWhenClause(AndAlso(typeMappedWhenClauses[^1].Test, lastClause.Test), lastClause.Result); + elseResult = lastCase.ElseResult; + } + + return existingExpression is CaseExpression expr + && operand == expr.Operand + && typeMappedWhenClauses.SequenceEqual(expr.WhenClauses) + && elseResult == expr.ElseResult + ? expr + : new CaseExpression(operand, typeMappedWhenClauses, elseResult); + + bool IsSkipped(CaseWhenClause clause) + => operand is null && clause.Test is SqlConstantExpression { Value: false or null }; + + bool IsMatched(CaseWhenClause clause) + => operand is null && clause.Test is SqlConstantExpression { Value: true }; + } + + /// + public virtual SqlExpression Case(IReadOnlyList whenClauses, SqlExpression? elseResult) + => Case(operand: null, whenClauses, elseResult); } diff --git a/src/EFCore.Cosmos/Query/Internal/Translators/CosmosComparisonTranslator.cs b/src/EFCore.Cosmos/Query/Internal/Translators/CosmosComparisonTranslator.cs new file mode 100644 index 00000000000..454976935e2 --- /dev/null +++ b/src/EFCore.Cosmos/Query/Internal/Translators/CosmosComparisonTranslator.cs @@ -0,0 +1,81 @@ +// 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 + +using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; + +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 CosmosComparisonTranslator : IMethodCallTranslator +{ + 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 CosmosComparisonTranslator(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, + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + if (method.ReturnType == typeof(int)) + { + SqlExpression? left = null; + SqlExpression? right = null; + if (method.Name == nameof(string.Compare) + && arguments.Count == 2 + && arguments[0].Type == arguments[1].Type) + { + left = arguments[0]; + right = arguments[1]; + } + else if (method.Name == nameof(string.CompareTo) + && arguments.Count == 1 + && instance != null + && instance.Type == arguments[0].Type) + { + left = instance; + right = arguments[0]; + } + + if (left != null + && right != null) + { + return _sqlExpressionFactory.Case( + new[] + { + new CaseWhenClause( + _sqlExpressionFactory.Equal(left, right), _sqlExpressionFactory.Constant(0)), + new CaseWhenClause( + _sqlExpressionFactory.GreaterThan(left, right), _sqlExpressionFactory.Constant(1)), + new CaseWhenClause( + _sqlExpressionFactory.LessThan(left, right), _sqlExpressionFactory.Constant(-1)) + }, + null); + } + } + + return null; + } +} diff --git a/src/EFCore.Cosmos/Query/Internal/Translators/CosmosEnumMethodTranslator.cs b/src/EFCore.Cosmos/Query/Internal/Translators/CosmosEnumMethodTranslator.cs new file mode 100644 index 00000000000..8ce86f82eba --- /dev/null +++ b/src/EFCore.Cosmos/Query/Internal/Translators/CosmosEnumMethodTranslator.cs @@ -0,0 +1,87 @@ +// 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.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 CosmosEnumMethodTranslator : IMethodCallTranslator +{ + private static readonly MethodInfo HasFlagMethodInfo + = typeof(Enum).GetRuntimeMethod(nameof(Enum.HasFlag), [typeof(Enum)])!; + + private static readonly MethodInfo ToStringMethodInfo + = typeof(object).GetRuntimeMethod(nameof(ToString), [])!; + + 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 CosmosEnumMethodTranslator(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, + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + if (Equals(method, HasFlagMethodInfo) + && instance != null) + { + var argument = arguments[0]; + return instance.Type != argument.Type + ? null + : _sqlExpressionFactory.Equal(_sqlExpressionFactory.And(instance, argument), argument); + } + + if (Equals(method, ToStringMethodInfo) + && instance is { Type.IsEnum: true, TypeMapping.Converter: ValueConverter converter } + && converter.GetType() is { IsGenericType: true } converterType) + { + switch (converterType) + { + case not null when converterType.GetGenericTypeDefinition() == typeof(EnumToNumberConverter<,>): + var whenClauses = Enum.GetValues(instance.Type) + .Cast() + .Select( + value => new CaseWhenClause( + _sqlExpressionFactory.Constant(value), + _sqlExpressionFactory.Constant(value.ToString(), typeof(string)))) + .ToArray(); + + var elseResult = _sqlExpressionFactory.CoalesceUndefined( + _sqlExpressionFactory.Convert(instance, typeof(string)), + _sqlExpressionFactory.Constant(string.Empty)); + + return _sqlExpressionFactory.Case(instance, whenClauses, elseResult); + + case not null when converterType.GetGenericTypeDefinition() == typeof(EnumToStringConverter<>): + // TODO: Unnecessary cast to string, #33733 + return _sqlExpressionFactory.Convert(instance, typeof(string)); + + default: + return null; + } + } + + return null; + } +} diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/Translations/EnumTranslationsCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/Translations/EnumTranslationsCosmosTest.cs index 829f6174b5b..3f5319a16d2 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/Translations/EnumTranslationsCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/Translations/EnumTranslationsCosmosTest.cs @@ -241,16 +241,137 @@ OFFSET 0 LIMIT 1 } // #35317 - public override Task HasFlag() - => AssertTranslationFailed(() => base.HasFlag()); + public override async Task HasFlag() + { + await base.HasFlag(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE ((c["FlagsEnum"] & 8) = 8) +""", + // + """ +SELECT VALUE c +FROM root c +WHERE ((c["FlagsEnum"] & 12) = 12) +""", + // + """ +SELECT VALUE c +FROM root c +WHERE ((c["FlagsEnum"] & 8) = 8) +""", + // + """ +SELECT VALUE c +FROM root c +WHERE ((c["FlagsEnum"] & 8) = 8) +""", + // + """ +SELECT VALUE c +FROM root c +WHERE ((8 & c["FlagsEnum"]) = c["FlagsEnum"]) +""", + // + """ +SELECT VALUE +{ + "hasFlagTrue" : ((c["FlagsEnum"] & 8) = 8), + "hasFlagFalse" : ((c["FlagsEnum"] & 4) = 4) +} +FROM root c +WHERE ((c["FlagsEnum"] & 8) = 8) +OFFSET 0 LIMIT 1 +"""); + } // #35317 - public override Task HasFlag_with_non_nullable_parameter() - => AssertTranslationFailed(() => base.HasFlag()); + public override async Task HasFlag_with_non_nullable_parameter() + { + await base.HasFlag_with_non_nullable_parameter(); + + AssertSql( + """ +@flagsEnum='8' + +SELECT VALUE c +FROM root c +WHERE ((c["FlagsEnum"] & @flagsEnum) = @flagsEnum) +"""); + } // #35317 - public override Task HasFlag_with_nullable_parameter() - => AssertTranslationFailed(() => base.HasFlag()); + public override async Task HasFlag_with_nullable_parameter() + { + await base.HasFlag_with_nullable_parameter(); + + AssertSql( + """ +@flagsEnum='8' + +SELECT VALUE c +FROM root c +WHERE ((c["FlagsEnum"] & @flagsEnum) = @flagsEnum) +"""); + } + + + public override Task ToString_enum_contains(bool async) + => Fixture.NoSyncTest( + async, async a => + { + await base.ToString_enum_contains(a); + + AssertSql( + """ +SELECT VALUE c["Enum"] +FROM root c +WHERE CONTAINS(IIF(c["Enum"] = 0, "One", IIF(c["Enum"] = 1, "Two", IIF(c["Enum"] = 2, "Three", (ToString(c["Enum"]) ?? "")))), "One") +"""); + }); + + public override Task ToString_nullable_enum_contains(bool async) + => Fixture.NoSyncTest( + async, async a => + { + await base.ToString_nullable_enum_contains(a); + + AssertSql( + """ + SELECT VALUE c["Enum"] + FROM root c + WHERE CONTAINS(IIF(c["Enum"] = 0, "One", IIF(c["Enum"] = 1, "Two", IIF(c["Enum"] = 2, "Three", (ToString(c["Enum"]) ?? "")))), "One") + """); + }); + + public override Task ToString_enum_property_projection(bool async) + => Fixture.NoSyncTest( + async, async a => + { + await base.ToString_enum_property_projection(a); + + AssertSql( + """ +SELECT VALUE IIF(c["Enum"] = 0, "One", IIF(c["Enum"] = 1, "Two", IIF(c["Enum"] = 2, "Three", (ToString(c["Enum"]) ?? "")))) +FROM root c +"""); + }); + + public override Task ToString_nullable_enum_property_projection(bool async) + => Fixture.NoSyncTest( + async, async a => + { + await base.ToString_nullable_enum_property_projection(a); + + AssertSql( + """ +SELECT VALUE c["Enum"] +FROM root c +"""); + }); [Fact] public virtual void Check_all_tests_overridden() diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/Translations/MiscellaneousTranslationsCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/Translations/MiscellaneousTranslationsCosmosTest.cs index edd5c9e5549..a0c905b0da8 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/Translations/MiscellaneousTranslationsCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/Translations/MiscellaneousTranslationsCosmosTest.cs @@ -142,26 +142,164 @@ public override async Task Convert_ToString() public override async Task Int_Compare_to_simple_zero() { - // Cosmos client evaluation. Issue #17246. - await AssertTranslationFailed(() => base.Int_Compare_to_simple_zero()); + await base.Int_Compare_to_simple_zero(); - AssertSql(); + AssertSql( + """ +@orderId='8' + +SELECT VALUE c +FROM root c +WHERE (IIF((c["Int"] = @orderId), 0, IIF((c["Int"] > @orderId), 1, IIF((c["Int"] < @orderId), -1, null))) = 0) +""", + // + """ +@orderId='8' + +SELECT VALUE c +FROM root c +WHERE (0 != IIF((c["Int"] = @orderId), 0, IIF((c["Int"] > @orderId), 1, IIF((c["Int"] < @orderId), -1, null)))) +""", + // + """ +@orderId='8' + +SELECT VALUE c +FROM root c +WHERE (IIF((c["Int"] = @orderId), 0, IIF((c["Int"] > @orderId), 1, IIF((c["Int"] < @orderId), -1, null))) > 0) +""", + // + """ +@orderId='8' + +SELECT VALUE c +FROM root c +WHERE (0 >= IIF((c["Int"] = @orderId), 0, IIF((c["Int"] > @orderId), 1, IIF((c["Int"] < @orderId), -1, null)))) +""", + // + """ +@orderId='8' + +SELECT VALUE c +FROM root c +WHERE (0 < IIF((c["Int"] = @orderId), 0, IIF((c["Int"] > @orderId), 1, IIF((c["Int"] < @orderId), -1, null)))) +""", + // + """ +@orderId='8' + +SELECT VALUE c +FROM root c +WHERE (IIF((c["Int"] = @orderId), 0, IIF((c["Int"] > @orderId), 1, IIF((c["Int"] < @orderId), -1, null))) <= 0) +"""); } public override async Task DateTime_Compare_to_simple_zero(bool compareTo) { - // Cosmos client evaluation. Issue #17246. - await AssertTranslationFailed(() => base.DateTime_Compare_to_simple_zero(compareTo)); + await base.DateTime_Compare_to_simple_zero(compareTo); - AssertSql(); + AssertSql( + """ +@dateTime='1998-05-04T15:30:10' + +SELECT VALUE c +FROM root c +WHERE (IIF((c["DateTime"] = @dateTime), 0, IIF((c["DateTime"] > @dateTime), 1, IIF((c["DateTime"] < @dateTime), -1, null))) = 0) +""", + // + """ +@dateTime='1998-05-04T15:30:10' + +SELECT VALUE c +FROM root c +WHERE (0 != IIF((c["DateTime"] = @dateTime), 0, IIF((c["DateTime"] > @dateTime), 1, IIF((c["DateTime"] < @dateTime), -1, null)))) +""", + // + """ +@dateTime='1998-05-04T15:30:10' + +SELECT VALUE c +FROM root c +WHERE (IIF((c["DateTime"] = @dateTime), 0, IIF((c["DateTime"] > @dateTime), 1, IIF((c["DateTime"] < @dateTime), -1, null))) > 0) +""", + // + """ +@dateTime='1998-05-04T15:30:10' + +SELECT VALUE c +FROM root c +WHERE (0 >= IIF((c["DateTime"] = @dateTime), 0, IIF((c["DateTime"] > @dateTime), 1, IIF((c["DateTime"] < @dateTime), -1, null)))) +""", + // + """ +@dateTime='1998-05-04T15:30:10' + +SELECT VALUE c +FROM root c +WHERE (0 < IIF((c["DateTime"] = @dateTime), 0, IIF((c["DateTime"] > @dateTime), 1, IIF((c["DateTime"] < @dateTime), -1, null)))) +""", + // + """ +@dateTime='1998-05-04T15:30:10' + +SELECT VALUE c +FROM root c +WHERE (IIF((c["DateTime"] = @dateTime), 0, IIF((c["DateTime"] > @dateTime), 1, IIF((c["DateTime"] < @dateTime), -1, null))) <= 0) +"""); } public override async Task TimeSpan_Compare_to_simple_zero(bool compareTo) { - // Cosmos client evaluation. Issue #17246. - await AssertTranslationFailed(() => base.TimeSpan_Compare_to_simple_zero(compareTo)); + await base.TimeSpan_Compare_to_simple_zero(compareTo); - AssertSql(); + AssertSql( + """ + @timeSpan='01:02:03' + + SELECT VALUE c + FROM root c + WHERE (IIF((c["TimeSpan"] = @timeSpan), 0, IIF((c["TimeSpan"] > @timeSpan), 1, IIF((c["TimeSpan"] < @timeSpan), -1, null))) = 0) + """, + // + """ + @timeSpan='01:02:03' + + SELECT VALUE c + FROM root c + WHERE (0 != IIF((c["TimeSpan"] = @timeSpan), 0, IIF((c["TimeSpan"] > @timeSpan), 1, IIF((c["TimeSpan"] < @timeSpan), -1, null)))) + """, + // + """ + @timeSpan='01:02:03' + + SELECT VALUE c + FROM root c + WHERE (IIF((c["TimeSpan"] = @timeSpan), 0, IIF((c["TimeSpan"] > @timeSpan), 1, IIF((c["TimeSpan"] < @timeSpan), -1, null))) > 0) + """, + // + """ + @timeSpan='01:02:03' + + SELECT VALUE c + FROM root c + WHERE (0 >= IIF((c["TimeSpan"] = @timeSpan), 0, IIF((c["TimeSpan"] > @timeSpan), 1, IIF((c["TimeSpan"] < @timeSpan), -1, null)))) + """, + // + """ + @timeSpan='01:02:03' + + SELECT VALUE c + FROM root c + WHERE (0 < IIF((c["TimeSpan"] = @timeSpan), 0, IIF((c["TimeSpan"] > @timeSpan), 1, IIF((c["TimeSpan"] < @timeSpan), -1, null)))) + """, + // + """ + @timeSpan='01:02:03' + + SELECT VALUE c + FROM root c + WHERE (IIF((c["TimeSpan"] = @timeSpan), 0, IIF((c["TimeSpan"] > @timeSpan), 1, IIF((c["TimeSpan"] < @timeSpan), -1, null))) <= 0) + """); } #endregion Compare diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/Translations/StringTranslationsCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/Translations/StringTranslationsCosmosTest.cs index 6386661d98a..faf124932e1 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/Translations/StringTranslationsCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/Translations/StringTranslationsCosmosTest.cs @@ -861,104 +861,442 @@ public override async Task Trim_with_char_array_argument_in_predicate() public override async Task Compare_simple_zero() { - // Cosmos client evaluation. Issue #17246. - await AssertTranslationFailed(() => base.Compare_simple_zero()); + await base.Compare_simple_zero(); - AssertSql(); + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (IIF((c["String"] = "Seattle"), 0, IIF((c["String"] > "Seattle"), 1, IIF((c["String"] < "Seattle"), -1, null))) = 0) +""", + // + """ +SELECT VALUE c +FROM root c +WHERE (0 != IIF((c["String"] = "Seattle"), 0, IIF((c["String"] > "Seattle"), 1, IIF((c["String"] < "Seattle"), -1, null)))) +""", + // + """ +SELECT VALUE c +FROM root c +WHERE (IIF((c["String"] = "Seattle"), 0, IIF((c["String"] > "Seattle"), 1, IIF((c["String"] < "Seattle"), -1, null))) > 0) +""", + // + """ +SELECT VALUE c +FROM root c +WHERE (0 >= IIF((c["String"] = "Seattle"), 0, IIF((c["String"] > "Seattle"), 1, IIF((c["String"] < "Seattle"), -1, null)))) +""", + // + """ +SELECT VALUE c +FROM root c +WHERE (0 < IIF((c["String"] = "Seattle"), 0, IIF((c["String"] > "Seattle"), 1, IIF((c["String"] < "Seattle"), -1, null)))) +""", + // + """ +SELECT VALUE c +FROM root c +WHERE (IIF((c["String"] = "Seattle"), 0, IIF((c["String"] > "Seattle"), 1, IIF((c["String"] < "Seattle"), -1, null))) <= 0) +"""); } public override async Task Compare_simple_one() { - // Cosmos client evaluation. Issue #17246. - await AssertTranslationFailed(() => base.Compare_simple_one()); + await base.Compare_simple_one(); - AssertSql(); + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (IIF((c["String"] = "Seattle"), 0, IIF((c["String"] > "Seattle"), 1, IIF((c["String"] < "Seattle"), -1, null))) = 1) +""", + // + """ +SELECT VALUE c +FROM root c +WHERE (-1 = IIF((c["String"] = "Seattle"), 0, IIF((c["String"] > "Seattle"), 1, IIF((c["String"] < "Seattle"), -1, null)))) +""", + // + """ +SELECT VALUE c +FROM root c +WHERE (IIF((c["String"] = "Seattle"), 0, IIF((c["String"] > "Seattle"), 1, IIF((c["String"] < "Seattle"), -1, null))) < 1) +""", + // + """ +SELECT VALUE c +FROM root c +WHERE (1 > IIF((c["String"] = "Seattle"), 0, IIF((c["String"] > "Seattle"), 1, IIF((c["String"] < "Seattle"), -1, null)))) +""", + // + """ +SELECT VALUE c +FROM root c +WHERE (IIF((c["String"] = "Seattle"), 0, IIF((c["String"] > "Seattle"), 1, IIF((c["String"] < "Seattle"), -1, null))) > -1) +""", + // + """ +SELECT VALUE c +FROM root c +WHERE (-1 < IIF((c["String"] = "Seattle"), 0, IIF((c["String"] > "Seattle"), 1, IIF((c["String"] < "Seattle"), -1, null)))) +"""); } public override async Task Compare_with_parameter() { - // Cosmos client evaluation. Issue #17246. - await AssertTranslationFailed(() => base.Compare_with_parameter()); + await base.Compare_with_parameter(); AssertSql( """ -ReadItem([1.0], 1) -"""); + ReadItem([1.0], 1) + """, + // + """ + @basicTypeEntity_String='Seattle' + + SELECT VALUE c + FROM root c + WHERE (IIF((c["String"] = @basicTypeEntity_String), 0, IIF((c["String"] > @basicTypeEntity_String), 1, IIF((c["String"] < @basicTypeEntity_String), -1, null))) = 1) + """, + // + """ + @basicTypeEntity_String='Seattle' + + SELECT VALUE c + FROM root c + WHERE (-1 = IIF((c["String"] = @basicTypeEntity_String), 0, IIF((c["String"] > @basicTypeEntity_String), 1, IIF((c["String"] < @basicTypeEntity_String), -1, null)))) + """, + // + """ + @basicTypeEntity_String='Seattle' + + SELECT VALUE c + FROM root c + WHERE (IIF((c["String"] = @basicTypeEntity_String), 0, IIF((c["String"] > @basicTypeEntity_String), 1, IIF((c["String"] < @basicTypeEntity_String), -1, null))) < 1) + """, + // + """ + @basicTypeEntity_String='Seattle' + + SELECT VALUE c + FROM root c + WHERE (1 > IIF((c["String"] = @basicTypeEntity_String), 0, IIF((c["String"] > @basicTypeEntity_String), 1, IIF((c["String"] < @basicTypeEntity_String), -1, null)))) + """, + // + """ + @basicTypeEntity_String='Seattle' + + SELECT VALUE c + FROM root c + WHERE (IIF((c["String"] = @basicTypeEntity_String), 0, IIF((c["String"] > @basicTypeEntity_String), 1, IIF((c["String"] < @basicTypeEntity_String), -1, null))) > -1) + """, + // + """ + @basicTypeEntity_String='Seattle' + + SELECT VALUE c + FROM root c + WHERE (-1 < IIF((c["String"] = @basicTypeEntity_String), 0, IIF((c["String"] > @basicTypeEntity_String), 1, IIF((c["String"] < @basicTypeEntity_String), -1, null)))) + """); } public override async Task Compare_simple_more_than_one() { - // Cosmos client evaluation. Issue #17246. - await AssertTranslationFailed(() => base.Compare_simple_more_than_one()); + await base.Compare_simple_more_than_one(); - AssertSql(); + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (IIF((c["String"] = "Seattle"), 0, IIF((c["String"] > "Seattle"), 1, IIF((c["String"] < "Seattle"), -1, null))) = 42) +""", + // + """ +SELECT VALUE c +FROM root c +WHERE (IIF((c["String"] = "Seattle"), 0, IIF((c["String"] > "Seattle"), 1, IIF((c["String"] < "Seattle"), -1, null))) > 42) +""", + // + """ +SELECT VALUE c +FROM root c +WHERE (42 > IIF((c["String"] = "Seattle"), 0, IIF((c["String"] > "Seattle"), 1, IIF((c["String"] < "Seattle"), -1, null)))) +"""); } - public override async Task Compare_nested() + public override async Task Compare_nested() { - // Cosmos client evaluation. Issue #17246. - await AssertTranslationFailed(() => base.Compare_nested()); + await base.Compare_nested(); - AssertSql(); + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (IIF((c["String"] = ("M" || c["String"])), 0, IIF((c["String"] > ("M" || c["String"])), 1, IIF((c["String"] < ("M" || c["String"])), -1, null))) = 0) +""", + // + """ +SELECT VALUE c +FROM root c +WHERE (0 != IIF((c["String"] = LEFT(c["String"], 0)), 0, IIF((c["String"] > LEFT(c["String"], 0)), 1, IIF((c["String"] < LEFT(c["String"], 0)), -1, null)))) +""", + // + """ +SELECT VALUE c +FROM root c +WHERE (IIF((c["String"] = REPLACE("Seattle", "Sea", c["String"])), 0, IIF((c["String"] > REPLACE("Seattle", "Sea", c["String"])), 1, IIF((c["String"] < REPLACE("Seattle", "Sea", c["String"])), -1, null))) > 0) +""", + // + """ +SELECT VALUE c +FROM root c +WHERE (0 >= IIF((c["String"] = ("M" || c["String"])), 0, IIF((c["String"] > ("M" || c["String"])), 1, IIF((c["String"] < ("M" || c["String"])), -1, null)))) +""", + // + """ +SELECT VALUE c +FROM root c +WHERE (1 = IIF((c["String"] = LEFT(c["String"], 0)), 0, IIF((c["String"] > LEFT(c["String"], 0)), 1, IIF((c["String"] < LEFT(c["String"], 0)), -1, null)))) +""", + // + """ +SELECT VALUE c +FROM root c +WHERE (IIF((c["String"] = REPLACE("Seattle", "Sea", c["String"])), 0, IIF((c["String"] > REPLACE("Seattle", "Sea", c["String"])), 1, IIF((c["String"] < REPLACE("Seattle", "Sea", c["String"])), -1, null))) = -1) +"""); } public override async Task Compare_multi_predicate() { - // Cosmos client evaluation. Issue #17246. - await AssertTranslationFailed(() => base.Compare_multi_predicate()); + await base.Compare_multi_predicate(); - AssertSql(); + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE ((IIF((c["String"] = "Seattle"), 0, IIF((c["String"] > "Seattle"), 1, IIF((c["String"] < "Seattle"), -1, null))) > -1) AND (IIF((c["String"] = "Toronto"), 0, IIF((c["String"] > "Toronto"), 1, IIF((c["String"] < "Toronto"), -1, null))) = -1)) +"""); } public override async Task CompareTo_simple_zero() { - // Cosmos client evaluation. Issue #17246. - await AssertTranslationFailed(() => base.CompareTo_simple_zero()); + await base.CompareTo_simple_zero(); - AssertSql(); + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (IIF((c["String"] = "Seattle"), 0, IIF((c["String"] > "Seattle"), 1, IIF((c["String"] < "Seattle"), -1, null))) = 0) +""", + // + """ +SELECT VALUE c +FROM root c +WHERE (0 != IIF((c["String"] = "Seattle"), 0, IIF((c["String"] > "Seattle"), 1, IIF((c["String"] < "Seattle"), -1, null)))) +""", + // + """ +SELECT VALUE c +FROM root c +WHERE (IIF((c["String"] = "Seattle"), 0, IIF((c["String"] > "Seattle"), 1, IIF((c["String"] < "Seattle"), -1, null))) > 0) +""", + // + """ +SELECT VALUE c +FROM root c +WHERE (0 >= IIF((c["String"] = "Seattle"), 0, IIF((c["String"] > "Seattle"), 1, IIF((c["String"] < "Seattle"), -1, null)))) +""", + // + """ +SELECT VALUE c +FROM root c +WHERE (0 < IIF((c["String"] = "Seattle"), 0, IIF((c["String"] > "Seattle"), 1, IIF((c["String"] < "Seattle"), -1, null)))) +""", + // + """ +SELECT VALUE c +FROM root c +WHERE (IIF((c["String"] = "Seattle"), 0, IIF((c["String"] > "Seattle"), 1, IIF((c["String"] < "Seattle"), -1, null))) <= 0) +"""); } public override async Task CompareTo_simple_one() { - // Cosmos client evaluation. Issue #17246. - await AssertTranslationFailed(() => base.CompareTo_simple_one()); + await base.CompareTo_simple_one(); - AssertSql(); + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (IIF((c["String"] = "Seattle"), 0, IIF((c["String"] > "Seattle"), 1, IIF((c["String"] < "Seattle"), -1, null))) = 1) +""", + // + """ +SELECT VALUE c +FROM root c +WHERE (-1 = IIF((c["String"] = "Seattle"), 0, IIF((c["String"] > "Seattle"), 1, IIF((c["String"] < "Seattle"), -1, null)))) +""", + // + """ +SELECT VALUE c +FROM root c +WHERE (IIF((c["String"] = "Seattle"), 0, IIF((c["String"] > "Seattle"), 1, IIF((c["String"] < "Seattle"), -1, null))) < 1) +""", + // + """ +SELECT VALUE c +FROM root c +WHERE (1 > IIF((c["String"] = "Seattle"), 0, IIF((c["String"] > "Seattle"), 1, IIF((c["String"] < "Seattle"), -1, null)))) +""", + // + """ +SELECT VALUE c +FROM root c +WHERE (IIF((c["String"] = "Seattle"), 0, IIF((c["String"] > "Seattle"), 1, IIF((c["String"] < "Seattle"), -1, null))) > -1) +""", + // + """ +SELECT VALUE c +FROM root c +WHERE (-1 < IIF((c["String"] = "Seattle"), 0, IIF((c["String"] > "Seattle"), 1, IIF((c["String"] < "Seattle"), -1, null)))) +"""); } public override async Task CompareTo_with_parameter() { - // Cosmos client evaluation. Issue #17246. - await AssertTranslationFailed(() => base.CompareTo_with_parameter()); + await base.CompareTo_with_parameter(); AssertSql( """ -ReadItem([1.0], 1) -"""); + ReadItem([1.0], 1) + """, + // + """ + @basicTypesEntity_String='Seattle' + + SELECT VALUE c + FROM root c + WHERE (IIF((c["String"] = @basicTypesEntity_String), 0, IIF((c["String"] > @basicTypesEntity_String), 1, IIF((c["String"] < @basicTypesEntity_String), -1, null))) = 1) + """, + // + """ + @basicTypesEntity_String='Seattle' + + SELECT VALUE c + FROM root c + WHERE (-1 = IIF((c["String"] = @basicTypesEntity_String), 0, IIF((c["String"] > @basicTypesEntity_String), 1, IIF((c["String"] < @basicTypesEntity_String), -1, null)))) + """, + // + """ + @basicTypesEntity_String='Seattle' + + SELECT VALUE c + FROM root c + WHERE (IIF((c["String"] = @basicTypesEntity_String), 0, IIF((c["String"] > @basicTypesEntity_String), 1, IIF((c["String"] < @basicTypesEntity_String), -1, null))) < 1) + """, + // + """ + @basicTypesEntity_String='Seattle' + + SELECT VALUE c + FROM root c + WHERE (1 > IIF((c["String"] = @basicTypesEntity_String), 0, IIF((c["String"] > @basicTypesEntity_String), 1, IIF((c["String"] < @basicTypesEntity_String), -1, null)))) + """, + // + """ + @basicTypesEntity_String='Seattle' + + SELECT VALUE c + FROM root c + WHERE (IIF((c["String"] = @basicTypesEntity_String), 0, IIF((c["String"] > @basicTypesEntity_String), 1, IIF((c["String"] < @basicTypesEntity_String), -1, null))) > -1) + """, + // + """ + @basicTypesEntity_String='Seattle' + + SELECT VALUE c + FROM root c + WHERE (-1 < IIF((c["String"] = @basicTypesEntity_String), 0, IIF((c["String"] > @basicTypesEntity_String), 1, IIF((c["String"] < @basicTypesEntity_String), -1, null)))) + """); } public override async Task CompareTo_simple_more_than_one() { - // Cosmos client evaluation. Issue #17246. - await AssertTranslationFailed(() => base.CompareTo_simple_more_than_one()); + await base.CompareTo_simple_more_than_one(); - AssertSql(); + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (IIF((c["String"] = "Seattle"), 0, IIF((c["String"] > "Seattle"), 1, IIF((c["String"] < "Seattle"), -1, null))) = 42) +""", + // + """ +SELECT VALUE c +FROM root c +WHERE (IIF((c["String"] = "Seattle"), 0, IIF((c["String"] > "Seattle"), 1, IIF((c["String"] < "Seattle"), -1, null))) > 42) +""", + // + """ +SELECT VALUE c +FROM root c +WHERE (42 > IIF((c["String"] = "Seattle"), 0, IIF((c["String"] > "Seattle"), 1, IIF((c["String"] < "Seattle"), -1, null)))) +"""); } public override async Task CompareTo_nested() { - // Cosmos client evaluation. Issue #17246. - await AssertTranslationFailed(() => base.CompareTo_nested()); + await base.CompareTo_nested(); - AssertSql(); + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (IIF((c["String"] = ("M" || c["String"])), 0, IIF((c["String"] > ("M" || c["String"])), 1, IIF((c["String"] < ("M" || c["String"])), -1, null))) = 0) +""", + // + """ +SELECT VALUE c +FROM root c +WHERE (0 != IIF((c["String"] = LEFT(c["String"], 0)), 0, IIF((c["String"] > LEFT(c["String"], 0)), 1, IIF((c["String"] < LEFT(c["String"], 0)), -1, null)))) +""", + // + """ +SELECT VALUE c +FROM root c +WHERE (IIF((c["String"] = REPLACE("Seattle", "Sea", c["String"])), 0, IIF((c["String"] > REPLACE("Seattle", "Sea", c["String"])), 1, IIF((c["String"] < REPLACE("Seattle", "Sea", c["String"])), -1, null))) > 0) +""", + // + """ +SELECT VALUE c +FROM root c +WHERE (0 >= IIF((c["String"] = ("M" || c["String"])), 0, IIF((c["String"] > ("M" || c["String"])), 1, IIF((c["String"] < ("M" || c["String"])), -1, null)))) +""", + // + """ +SELECT VALUE c +FROM root c +WHERE (1 = IIF((c["String"] = LEFT(c["String"], 0)), 0, IIF((c["String"] > LEFT(c["String"], 0)), 1, IIF((c["String"] < LEFT(c["String"], 0)), -1, null)))) +""", + // + """ +SELECT VALUE c +FROM root c +WHERE (IIF((c["String"] = REPLACE("Seattle", "Sea", c["String"])), 0, IIF((c["String"] > REPLACE("Seattle", "Sea", c["String"])), 1, IIF((c["String"] < REPLACE("Seattle", "Sea", c["String"])), -1, null))) = -1) +"""); } public override async Task Compare_to_multi_predicate() { - // Cosmos client evaluation. Issue #17246. - await AssertTranslationFailed(() => base.Compare_to_multi_predicate()); + await base.Compare_to_multi_predicate(); - AssertSql(); + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE ((IIF((c["String"] = "Seattle"), 0, IIF((c["String"] > "Seattle"), 1, IIF((c["String"] < "Seattle"), -1, null))) > -1) AND (IIF((c["String"] = "Toronto"), 0, IIF((c["String"] > "Toronto"), 1, IIF((c["String"] < "Toronto"), -1, null))) = -1)) +"""); } #endregion Compare diff --git a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs index e07528c3468..1b270ccc1bb 100644 --- a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs @@ -75,30 +75,6 @@ public virtual Task ToString_boolean_computed_nullable(bool async) async, ss => ss.Set().Select(lh => (lh.Eradicated | lh.CommanderName == "Unknown").ToString())); - [Theory, MemberData(nameof(IsAsyncData))] - public virtual Task ToString_enum_property_projection(bool async) - => AssertQuery( - async, - ss => ss.Set().Select(g => g.Rank.ToString())); - - [Theory, MemberData(nameof(IsAsyncData))] - public virtual Task ToString_nullable_enum_property_projection(bool async) - => AssertQuery( - async, - ss => ss.Set().Select(w => w.AmmunitionType.ToString())); - - [Theory, MemberData(nameof(IsAsyncData))] - public virtual Task ToString_enum_contains(bool async) - => AssertQuery( - async, - ss => ss.Set().Where(g => g.Difficulty.ToString().Contains("Med")).Select(g => g.CodeName)); - - [Theory, MemberData(nameof(IsAsyncData))] - public virtual Task ToString_nullable_enum_contains(bool async) - => AssertQuery( - async, - ss => ss.Set().Where(w => w.AmmunitionType.ToString().Contains("Cart")).Select(g => g.Name)); - [Theory, MemberData(nameof(IsAsyncData))] public virtual Task Include_multiple_one_to_one_and_one_to_many_self_reference(bool async) => Assert.ThrowsAsync(() => AssertQuery(async, ss => ss.Set().Include(w => w.Owner.Weapons))); diff --git a/test/EFCore.Specification.Tests/Query/Translations/EnumTranslationsTestBase.cs b/test/EFCore.Specification.Tests/Query/Translations/EnumTranslationsTestBase.cs index faa512e30f0..b7afbb8e621 100644 --- a/test/EFCore.Specification.Tests/Query/Translations/EnumTranslationsTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/Translations/EnumTranslationsTestBase.cs @@ -5,6 +5,8 @@ namespace Microsoft.EntityFrameworkCore.Query.Translations; +#nullable disable + public abstract class EnumTranslationsTestBase(TFixture fixture) : QueryTestBase(fixture) where TFixture : BasicTypesQueryFixtureBase, new() { @@ -171,6 +173,30 @@ public virtual Task HasFlag_with_nullable_parameter() return AssertQuery(ss => ss.Set().Where(b => b.FlagsEnum.HasFlag(flagsEnum))); } + [Theory, MemberData(nameof(IsAsyncData))] + public virtual Task ToString_enum_property_projection(bool async) + => AssertQuery( + async, + ss => ss.Set().Select(g => g.Enum.ToString())); + + [Theory, MemberData(nameof(IsAsyncData))] + public virtual Task ToString_nullable_enum_property_projection(bool async) + => AssertQuery( + async, + ss => ss.Set().Select(w => w.Enum.ToString())); + + [Theory, MemberData(nameof(IsAsyncData))] + public virtual Task ToString_enum_contains(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(g => g.Enum.ToString().Contains("One")).Select(g => g.Enum)); + + [Theory, MemberData(nameof(IsAsyncData))] + public virtual Task ToString_nullable_enum_contains(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(w => w.Enum.ToString().Contains("One")).Select(g => g.Enum)); + protected BasicTypesContext CreateContext() => Fixture.CreateContext(); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs index 7bbf3f1b085..dd95878eb90 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs @@ -3482,71 +3482,6 @@ FROM [Factions] AS [f] """); } - public override async Task ToString_enum_property_projection(bool async) - { - await base.ToString_enum_property_projection(async); - - AssertSql( - """ -SELECT CASE [g].[Rank] - WHEN 0 THEN N'None' - WHEN 1 THEN N'Private' - WHEN 2 THEN N'Corporal' - WHEN 4 THEN N'Sergeant' - WHEN 8 THEN N'Lieutenant' - WHEN 16 THEN N'Captain' - WHEN 32 THEN N'Major' - WHEN 64 THEN N'Colonel' - WHEN 128 THEN N'General' - ELSE CAST([g].[Rank] AS nvarchar(max)) -END -FROM [Gears] AS [g] -"""); - } - - public override async Task ToString_nullable_enum_property_projection(bool async) - { - await base.ToString_nullable_enum_property_projection(async); - - AssertSql( - """ -SELECT CASE [w].[AmmunitionType] - WHEN 1 THEN N'Cartridge' - WHEN 2 THEN N'Shell' - ELSE ISNULL(CAST([w].[AmmunitionType] AS nvarchar(max)), N'') -END -FROM [Weapons] AS [w] -"""); - } - - public override async Task ToString_enum_contains(bool async) - { - await base.ToString_enum_contains(async); - - AssertSql( - """ -SELECT [m].[CodeName] -FROM [Missions] AS [m] -WHERE [m].[Difficulty] LIKE N'%Med%' -"""); - } - - public override async Task ToString_nullable_enum_contains(bool async) - { - await base.ToString_nullable_enum_contains(async); - - AssertSql( - """ -SELECT [w].[Name] -FROM [Weapons] AS [w] -WHERE CASE [w].[AmmunitionType] - WHEN 1 THEN N'Cartridge' - WHEN 2 THEN N'Shell' - ELSE ISNULL(CAST([w].[AmmunitionType] AS nvarchar(max)), N'') -END LIKE N'%Cart%' -"""); - } - public override async Task Correlated_collections_naked_navigation_with_ToList(bool async) { await base.Correlated_collections_naked_navigation_with_ToList(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/Inheritance/TPCGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/Inheritance/TPCGearsOfWarQuerySqlServerTest.cs index 3a3c4ee4ed1..0f46ddcda46 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/Inheritance/TPCGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/Inheritance/TPCGearsOfWarQuerySqlServerTest.cs @@ -4660,77 +4660,6 @@ FROM [Officers] AS [o] """); } - public override async Task ToString_enum_property_projection(bool async) - { - await base.ToString_enum_property_projection(async); - - AssertSql( - """ -SELECT CASE [u].[Rank] - WHEN 0 THEN N'None' - WHEN 1 THEN N'Private' - WHEN 2 THEN N'Corporal' - WHEN 4 THEN N'Sergeant' - WHEN 8 THEN N'Lieutenant' - WHEN 16 THEN N'Captain' - WHEN 32 THEN N'Major' - WHEN 64 THEN N'Colonel' - WHEN 128 THEN N'General' - ELSE CAST([u].[Rank] AS nvarchar(max)) -END -FROM ( - SELECT [g].[Rank] - FROM [Gears] AS [g] - UNION ALL - SELECT [o].[Rank] - FROM [Officers] AS [o] -) AS [u] -"""); - } - - public override async Task ToString_nullable_enum_property_projection(bool async) - { - await base.ToString_nullable_enum_property_projection(async); - - AssertSql( - """ -SELECT CASE [w].[AmmunitionType] - WHEN 1 THEN N'Cartridge' - WHEN 2 THEN N'Shell' - ELSE ISNULL(CAST([w].[AmmunitionType] AS nvarchar(max)), N'') -END -FROM [Weapons] AS [w] -"""); - } - - public override async Task ToString_enum_contains(bool async) - { - await base.ToString_enum_contains(async); - - AssertSql( - """ -SELECT [m].[CodeName] -FROM [Missions] AS [m] -WHERE [m].[Difficulty] LIKE N'%Med%' -"""); - } - - public override async Task ToString_nullable_enum_contains(bool async) - { - await base.ToString_nullable_enum_contains(async); - - AssertSql( - """ -SELECT [w].[Name] -FROM [Weapons] AS [w] -WHERE CASE [w].[AmmunitionType] - WHEN 1 THEN N'Cartridge' - WHEN 2 THEN N'Shell' - ELSE ISNULL(CAST([w].[AmmunitionType] AS nvarchar(max)), N'') -END LIKE N'%Cart%' -"""); - } - public override async Task Correlated_collections_naked_navigation_with_ToList(bool async) { await base.Correlated_collections_naked_navigation_with_ToList(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/Inheritance/TPTGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/Inheritance/TPTGearsOfWarQuerySqlServerTest.cs index 565dd038fef..003ac9c98fe 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/Inheritance/TPTGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/Inheritance/TPTGearsOfWarQuerySqlServerTest.cs @@ -4075,71 +4075,6 @@ FROM [Gears] AS [g] """); } - public override async Task ToString_enum_property_projection(bool async) - { - await base.ToString_enum_property_projection(async); - - AssertSql( - """ -SELECT CASE [g].[Rank] - WHEN 0 THEN N'None' - WHEN 1 THEN N'Private' - WHEN 2 THEN N'Corporal' - WHEN 4 THEN N'Sergeant' - WHEN 8 THEN N'Lieutenant' - WHEN 16 THEN N'Captain' - WHEN 32 THEN N'Major' - WHEN 64 THEN N'Colonel' - WHEN 128 THEN N'General' - ELSE CAST([g].[Rank] AS nvarchar(max)) -END -FROM [Gears] AS [g] -"""); - } - - public override async Task ToString_nullable_enum_property_projection(bool async) - { - await base.ToString_nullable_enum_property_projection(async); - - AssertSql( - """ -SELECT CASE [w].[AmmunitionType] - WHEN 1 THEN N'Cartridge' - WHEN 2 THEN N'Shell' - ELSE ISNULL(CAST([w].[AmmunitionType] AS nvarchar(max)), N'') -END -FROM [Weapons] AS [w] -"""); - } - - public override async Task ToString_enum_contains(bool async) - { - await base.ToString_enum_contains(async); - - AssertSql( - """ -SELECT [m].[CodeName] -FROM [Missions] AS [m] -WHERE [m].[Difficulty] LIKE N'%Med%' -"""); - } - - public override async Task ToString_nullable_enum_contains(bool async) - { - await base.ToString_nullable_enum_contains(async); - - AssertSql( - """ -SELECT [w].[Name] -FROM [Weapons] AS [w] -WHERE CASE [w].[AmmunitionType] - WHEN 1 THEN N'Cartridge' - WHEN 2 THEN N'Shell' - ELSE ISNULL(CAST([w].[AmmunitionType] AS nvarchar(max)), N'') -END LIKE N'%Cart%' -"""); - } - public override async Task Correlated_collections_naked_navigation_with_ToList(bool async) { await base.Correlated_collections_naked_navigation_with_ToList(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs index a64e37aa521..5844dcc7ac4 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs @@ -4184,71 +4184,6 @@ END AS [IsEradicated] """); } - public override async Task ToString_enum_property_projection(bool async) - { - await base.ToString_enum_property_projection(async); - - AssertSql( - """ -SELECT CASE [g].[Rank] - WHEN 0 THEN N'None' - WHEN 1 THEN N'Private' - WHEN 2 THEN N'Corporal' - WHEN 4 THEN N'Sergeant' - WHEN 8 THEN N'Lieutenant' - WHEN 16 THEN N'Captain' - WHEN 32 THEN N'Major' - WHEN 64 THEN N'Colonel' - WHEN 128 THEN N'General' - ELSE CAST([g].[Rank] AS nvarchar(max)) -END -FROM [Gears] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [g] -"""); - } - - public override async Task ToString_nullable_enum_property_projection(bool async) - { - await base.ToString_nullable_enum_property_projection(async); - - AssertSql( - """ -SELECT CASE [w].[AmmunitionType] - WHEN 1 THEN N'Cartridge' - WHEN 2 THEN N'Shell' - ELSE ISNULL(CAST([w].[AmmunitionType] AS nvarchar(max)), N'') -END -FROM [Weapons] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [w] -"""); - } - - public override async Task ToString_enum_contains(bool async) - { - await base.ToString_enum_contains(async); - - AssertSql( - """ -SELECT [m].[CodeName] -FROM [Missions] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [m] -WHERE [m].[Difficulty] LIKE N'%Med%' -"""); - } - - public override async Task ToString_nullable_enum_contains(bool async) - { - await base.ToString_nullable_enum_contains(async); - - AssertSql( - """ -SELECT [w].[Name] -FROM [Weapons] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [w] -WHERE CASE [w].[AmmunitionType] - WHEN 1 THEN N'Cartridge' - WHEN 2 THEN N'Shell' - ELSE ISNULL(CAST([w].[AmmunitionType] AS nvarchar(max)), N'') -END LIKE N'%Cart%' -"""); - } - public override async Task Correlated_collections_naked_navigation_with_ToList(bool async) { await base.Correlated_collections_naked_navigation_with_ToList(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/Translations/EnumTranslationsSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/Translations/EnumTranslationsSqlServerTest.cs index 8df28f8719d..84af19eddcc 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/Translations/EnumTranslationsSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/Translations/EnumTranslationsSqlServerTest.cs @@ -300,6 +300,72 @@ FROM [BasicTypesEntities] AS [b] """); } + public override async Task ToString_enum_contains(bool async) + { + await base.ToString_enum_contains(async); + + AssertSql( + """ +SELECT [b].[Enum] +FROM [BasicTypesEntities] AS [b] +WHERE CASE [b].[Enum] + WHEN 0 THEN N'One' + WHEN 1 THEN N'Two' + WHEN 2 THEN N'Three' + ELSE CAST([b].[Enum] AS nvarchar(max)) +END LIKE N'%One%' +"""); + } + + public override async Task ToString_nullable_enum_contains(bool async) + { + await base.ToString_nullable_enum_contains(async); + + AssertSql( + """ +SELECT [n].[Enum] +FROM [NullableBasicTypesEntities] AS [n] +WHERE CASE [n].[Enum] + WHEN 0 THEN N'One' + WHEN 1 THEN N'Two' + WHEN 2 THEN N'Three' + ELSE ISNULL(CAST([n].[Enum] AS nvarchar(max)), N'') +END LIKE N'%One%' +"""); + } + + public override async Task ToString_enum_property_projection(bool async) + { + await base.ToString_enum_property_projection(async); + + AssertSql( + """ +SELECT CASE [b].[Enum] + WHEN 0 THEN N'One' + WHEN 1 THEN N'Two' + WHEN 2 THEN N'Three' + ELSE CAST([b].[Enum] AS nvarchar(max)) +END +FROM [BasicTypesEntities] AS [b] +"""); + } + + public override async Task ToString_nullable_enum_property_projection(bool async) + { + await base.ToString_nullable_enum_property_projection(async); + + AssertSql( + """ +SELECT CASE [n].[Enum] + WHEN 0 THEN N'One' + WHEN 1 THEN N'Two' + WHEN 2 THEN N'Three' + ELSE ISNULL(CAST([n].[Enum] AS nvarchar(max)), N'') +END +FROM [NullableBasicTypesEntities] AS [n] +"""); + } + [Fact] public virtual void Check_all_tests_overridden() => TestHelpers.AssertAllMethodsOverridden(GetType()); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs index c590930011d..ae57106ba9b 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs @@ -2749,71 +2749,6 @@ public override async Task Correlated_collections_project_anonymous_collection_r """); } - public override async Task ToString_enum_property_projection(bool async) - { - await base.ToString_enum_property_projection(async); - - AssertSql( - """ -SELECT CASE "g"."Rank" - WHEN 0 THEN 'None' - WHEN 1 THEN 'Private' - WHEN 2 THEN 'Corporal' - WHEN 4 THEN 'Sergeant' - WHEN 8 THEN 'Lieutenant' - WHEN 16 THEN 'Captain' - WHEN 32 THEN 'Major' - WHEN 64 THEN 'Colonel' - WHEN 128 THEN 'General' - ELSE CAST("g"."Rank" AS TEXT) -END -FROM "Gears" AS "g" -"""); - } - - public override async Task ToString_nullable_enum_property_projection(bool async) - { - await base.ToString_nullable_enum_property_projection(async); - - AssertSql( - """ -SELECT CASE "w"."AmmunitionType" - WHEN 1 THEN 'Cartridge' - WHEN 2 THEN 'Shell' - ELSE COALESCE(CAST("w"."AmmunitionType" AS TEXT), '') -END -FROM "Weapons" AS "w" -"""); - } - - public override async Task ToString_enum_contains(bool async) - { - await base.ToString_enum_contains(async); - - AssertSql( - """ -SELECT "m"."CodeName" -FROM "Missions" AS "m" -WHERE instr("m"."Difficulty", 'Med') > 0 -"""); - } - - public override async Task ToString_nullable_enum_contains(bool async) - { - await base.ToString_nullable_enum_contains(async); - - AssertSql( - """ -SELECT "w"."Name" -FROM "Weapons" AS "w" -WHERE instr(CASE "w"."AmmunitionType" - WHEN 1 THEN 'Cartridge' - WHEN 2 THEN 'Shell' - ELSE COALESCE(CAST("w"."AmmunitionType" AS TEXT), '') -END, 'Cart') > 0 -"""); - } - public override async Task Correlated_collections_naked_navigation_with_ToList(bool async) { await base.Correlated_collections_naked_navigation_with_ToList(async); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/Translations/EnumTranslationsSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/Translations/EnumTranslationsSqliteTest.cs index 917f6d1f133..581d8dade9b 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/Translations/EnumTranslationsSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/Translations/EnumTranslationsSqliteTest.cs @@ -302,6 +302,72 @@ public override async Task HasFlag_with_nullable_parameter() """); } + public override async Task ToString_enum_contains(bool async) + { + await base.ToString_enum_contains(async); + + AssertSql( + """ +SELECT "b"."Enum" +FROM "BasicTypesEntities" AS "b" +WHERE instr(CASE "b"."Enum" + WHEN 0 THEN 'One' + WHEN 1 THEN 'Two' + WHEN 2 THEN 'Three' + ELSE CAST("b"."Enum" AS TEXT) +END, 'One') > 0 +"""); + } + + public override async Task ToString_nullable_enum_contains(bool async) + { + await base.ToString_nullable_enum_contains(async); + + AssertSql( + """ +SELECT "n"."Enum" +FROM "NullableBasicTypesEntities" AS "n" +WHERE instr(CASE "n"."Enum" + WHEN 0 THEN 'One' + WHEN 1 THEN 'Two' + WHEN 2 THEN 'Three' + ELSE COALESCE(CAST("n"."Enum" AS TEXT), '') +END, 'One') > 0 +"""); + } + + public override async Task ToString_enum_property_projection(bool async) + { + await base.ToString_enum_property_projection(async); + + AssertSql( + """ +SELECT CASE "b"."Enum" + WHEN 0 THEN 'One' + WHEN 1 THEN 'Two' + WHEN 2 THEN 'Three' + ELSE CAST("b"."Enum" AS TEXT) +END +FROM "BasicTypesEntities" AS "b" +"""); + } + + public override async Task ToString_nullable_enum_property_projection(bool async) + { + await base.ToString_nullable_enum_property_projection(async); + + AssertSql( + """ +SELECT CASE "n"."Enum" + WHEN 0 THEN 'One' + WHEN 1 THEN 'Two' + WHEN 2 THEN 'Three' + ELSE COALESCE(CAST("n"."Enum" AS TEXT), '') +END +FROM "NullableBasicTypesEntities" AS "n" +"""); + } + [Fact] public virtual void Check_all_tests_overridden() => TestHelpers.AssertAllMethodsOverridden(GetType());