From 09f4c853d87c207c794323cf1d06c1138a9470ff Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Mon, 22 May 2023 14:00:52 +0200 Subject: [PATCH] Normalize Any to Contains instead of vice versa E.g. for easier pattern-matching of Contains --- ...yableMethodTranslatingExpressionVisitor.cs | 218 +++++++----------- ...lationalSqlTranslatingExpressionVisitor.cs | 12 +- .../Query/SqlExpressions/ExistsExpression.cs | 7 + .../QueryOptimizingExpressionVisitor.cs | 59 +++-- .../PrimitiveCollectionsQueryTestBase.cs | 16 ++ .../ComplexNavigationsQuerySqlServerTest.cs | 2 +- ...NavigationsSharedTypeQuerySqlServerTest.cs | 2 +- ...indAggregateOperatorsQuerySqlServerTest.cs | 8 +- ...imitiveCollectionsQueryOldSqlServerTest.cs | 24 ++ .../PrimitiveCollectionsQuerySqlServerTest.cs | 24 ++ .../PrimitiveCollectionsQuerySqliteTest.cs | 24 ++ 11 files changed, 218 insertions(+), 178 deletions(-) diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs index 900f54abbc1..46dedb39eb0 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs @@ -419,11 +419,6 @@ private static ShapedQueryExpression CreateShapedQueryExpression(IEntityType ent ? nestedOperand : _sqlExpressionFactory.Not(translation)); - if (TrySimplifyValuesToInExpression(source, isNegated: true, out var simplifiedQuery)) - { - return simplifiedQuery; - } - selectExpression.ReplaceProjection(new List()); selectExpression.ApplyProjection(); if (selectExpression.Limit == null @@ -452,24 +447,19 @@ private static ShapedQueryExpression CreateShapedQueryExpression(IEntityType ent } source = translatedSource; - - if (TrySimplifyValuesToInExpression(source, isNegated: false, out var simplifiedQuery)) - { - return simplifiedQuery; - } } - var selectExpression = (SelectExpression)source.QueryExpression; - selectExpression.ReplaceProjection(new List()); - selectExpression.ApplyProjection(); - if (selectExpression.Limit == null - && selectExpression.Offset == null) + var subquery = (SelectExpression)source.QueryExpression; + subquery.ReplaceProjection(new List()); + subquery.ApplyProjection(); + if (subquery.Limit == null + && subquery.Offset == null) { - selectExpression.ClearOrdering(); + subquery.ClearOrdering(); } - var translation = _sqlExpressionFactory.Exists(selectExpression, false); - selectExpression = _sqlExpressionFactory.Select(translation); + var translation = _sqlExpressionFactory.Exists(subquery, false); + var selectExpression = _sqlExpressionFactory.Select(translation); return source.Update( selectExpression, @@ -501,47 +491,65 @@ private static ShapedQueryExpression CreateShapedQueryExpression(IEntityType ent /// protected override ShapedQueryExpression? TranslateContains(ShapedQueryExpression source, Expression item) { - var selectExpression = (SelectExpression)source.QueryExpression; - var translation = TranslateExpression(item); - if (translation == null) - { - return null; - } - - if (selectExpression.Limit == null - && selectExpression.Offset == null) - { - selectExpression.ClearOrdering(); - } - - var shaperExpression = source.ShaperExpression; - // No need to check ConvertChecked since this is convert node which we may have added during projection - if (shaperExpression is UnaryExpression { NodeType: ExpressionType.Convert } unaryExpression - && unaryExpression.Operand.Type.IsNullableType() - && unaryExpression.Operand.Type.UnwrapNullableType() == unaryExpression.Type) - { - shaperExpression = unaryExpression.Operand; - } - - if (shaperExpression is ProjectionBindingExpression projectionBindingExpression) + // Pattern-match Contains over ValuesExpression, translating to simplified 'item IN (1, 2, 3)' with constant elements + if (source.QueryExpression is SelectExpression + { + Tables: + [ + ValuesExpression + { + RowValues: [{ Values.Count: 2 }, ..], + ColumnNames: [ValuesOrderingColumnName, ValuesValueColumnName] + } valuesExpression + ], + Predicate: null, + GroupBy: [], + Having: null, + IsDistinct: false, + Limit: null, + Offset: null, + // Note that in the context of Contains we don't care about orderings + } + // Make sure that the source projects the column from the ValuesExpression directly, i.e. no projection out with some expression + && TryGetProjection(source, out var projection) + && projection is ColumnExpression projectedColumn + && projectedColumn.Table == valuesExpression) { - var projection = selectExpression.GetProjection(projectionBindingExpression); - if (projection is SqlExpression sqlExpression) + if (TranslateExpression(item) is not SqlExpression translatedItem) { - selectExpression.ReplaceProjection(new List { sqlExpression }); - selectExpression.ApplyProjection(); + return null; + } - translation = _sqlExpressionFactory.In(translation, selectExpression, false); - selectExpression = _sqlExpressionFactory.Select(translation); + var values = new object?[valuesExpression.RowValues.Count]; + for (var i = 0; i < values.Length; i++) + { + // Skip the first value (_ord), which is irrelevant for Contains + if (valuesExpression.RowValues[i].Values[1] is SqlConstantExpression { Value: var constantValue }) + { + values[i] = constantValue; + } + else + { + // We only support constants for now + values = null; + break; + } + } - return source.Update( - selectExpression, - Expression.Convert( - new ProjectionBindingExpression(selectExpression, new ProjectionMember(), typeof(bool?)), typeof(bool))); + if (values is not null) + { + var inExpression = _sqlExpressionFactory.In(translatedItem, _sqlExpressionFactory.Constant(values), negated: false); + return source.Update(_sqlExpressionFactory.Select(inExpression), source.ShaperExpression); } } - return null; + // TODO: This generates an EXISTS subquery. Translate to IN instead: #30955 + var anyLambdaParameter = Expression.Parameter(item.Type, "p"); + var anyLambda = Expression.Lambda( + Infrastructure.ExpressionExtensions.CreateEqualsExpression(anyLambdaParameter, item), + anyLambdaParameter); + + return TranslateAny(source, anyLambda); } /// @@ -1812,89 +1820,6 @@ protected virtual Expression ApplyInferredTypeMappings( protected virtual bool IsOrdered(SelectExpression selectExpression) => selectExpression.Orderings.Count > 0; - /// - /// Attempts to pattern-match for Contains over , which corresponds to - /// Where(b => new[] { 1, 2, 3 }.Contains(b.Id)). Simplifies this to the tighter [b].[Id] IN (1, 2, 3) instead of the - /// full subquery with VALUES. - /// - private bool TrySimplifyValuesToInExpression( - ShapedQueryExpression source, - bool isNegated, - [NotNullWhen(true)] out ShapedQueryExpression? simplifiedQuery) - { - if (source.QueryExpression is SelectExpression - { - Tables: [ValuesExpression - { - RowValues: [{ Values.Count: 2 }, ..], - ColumnNames: [ ValuesOrderingColumnName, ValuesValueColumnName ] - } valuesExpression], - GroupBy: [], - Having: null, - IsDistinct: false, - Limit: null, - Offset: null, - // Note that we don't care about orderings, they get elided anyway by Any/All - Predicate: SqlBinaryExpression { OperatorType: ExpressionType.Equal, Left: var left, Right: var right }, - } selectExpression) - { - // The table is a ValuesExpression, and the predicate is an equality - this is a possible simplifiable Contains. - // Get the projection column pointing to the ValuesExpression, and check that it's compared to on one side of the predicate - // equality. - var shaperExpression = source.ShaperExpression; - if (shaperExpression is UnaryExpression { NodeType: ExpressionType.Convert } unaryExpression - && unaryExpression.Operand.Type.IsNullableType() - && unaryExpression.Operand.Type.UnwrapNullableType() == unaryExpression.Type) - { - shaperExpression = unaryExpression.Operand; - } - - if (shaperExpression is ProjectionBindingExpression projectionBindingExpression - && selectExpression.GetProjection(projectionBindingExpression) is ColumnExpression projectionColumn) - { - SqlExpression item; - - if (left is ColumnExpression leftColumn - && (leftColumn.Table, leftColumn.Name) == (projectionColumn.Table, projectionColumn.Name)) - { - item = right; - } - else if (right is ColumnExpression rightColumn - && (rightColumn.Table, rightColumn.Name) == (projectionColumn.Table, projectionColumn.Name)) - { - item = left; - } - else - { - simplifiedQuery = null; - return false; - } - - var values = new object?[valuesExpression.RowValues.Count]; - for (var i = 0; i < values.Length; i++) - { - // Skip the first value (_ord), which is irrelevant for Contains - if (valuesExpression.RowValues[i].Values[1] is SqlConstantExpression { Value: var constantValue }) - { - values[i] = constantValue; - } - else - { - simplifiedQuery = null; - return false; - } - } - - var inExpression = _sqlExpressionFactory.In(item, _sqlExpressionFactory.Constant(values), isNegated); - simplifiedQuery = source.Update(_sqlExpressionFactory.Select(inExpression), source.ShaperExpression); - return true; - } - } - - simplifiedQuery = null; - return false; - } - private Expression RemapLambdaBody(ShapedQueryExpression shapedQueryExpression, LambdaExpression lambdaExpression) { var lambdaBody = ReplacingExpressionVisitor.Replace( @@ -2569,6 +2494,29 @@ private static Expression MatchShaperNullabilityForSetOperation(Expression shape return source.UpdateShaperExpression(shaper); } + private bool TryGetProjection(ShapedQueryExpression shapedQueryExpression, [NotNullWhen(true)] out SqlExpression? projection) + { + var shaperExpression = shapedQueryExpression.ShaperExpression; + // No need to check ConvertChecked since this is convert node which we may have added during projection + if (shaperExpression is UnaryExpression { NodeType: ExpressionType.Convert } unaryExpression + && unaryExpression.Operand.Type.IsNullableType() + && unaryExpression.Operand.Type.UnwrapNullableType() == unaryExpression.Type) + { + shaperExpression = unaryExpression.Operand; + } + + if (shapedQueryExpression.QueryExpression is SelectExpression selectExpression + && shaperExpression is ProjectionBindingExpression projectionBindingExpression + && selectExpression.GetProjection(projectionBindingExpression) is SqlExpression sqlExpression) + { + projection = sqlExpression; + return true; + } + + projection = null; + return false; + } + /// /// A visitor which scans an expression tree and attempts to find columns for which we were missing type mappings (projected out /// of queryable constant/parameter), and those type mappings have been inferred. diff --git a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs index 38569b6d852..f0f490fc73f 100644 --- a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs @@ -1133,9 +1133,7 @@ protected override Expression VisitUnary(UnaryExpression unaryExpression) var operand = Visit(unaryExpression.Operand); if (operand is EntityReferenceExpression entityReferenceExpression - && (unaryExpression.NodeType == ExpressionType.Convert - || unaryExpression.NodeType == ExpressionType.ConvertChecked - || unaryExpression.NodeType == ExpressionType.TypeAs)) + && unaryExpression.NodeType is ExpressionType.Convert or ExpressionType.ConvertChecked or ExpressionType.TypeAs) { return entityReferenceExpression.Convert(unaryExpression.Type); } @@ -1148,7 +1146,13 @@ protected override Expression VisitUnary(UnaryExpression unaryExpression) switch (unaryExpression.NodeType) { case ExpressionType.Not: - return _sqlExpressionFactory.Not(sqlOperand!); + return sqlOperand switch + { + ExistsExpression e => e.Negate(), + InExpression e => e.Negate(), + + _ => _sqlExpressionFactory.Not(sqlOperand!) + }; case ExpressionType.Negate: case ExpressionType.NegateChecked: diff --git a/src/EFCore.Relational/Query/SqlExpressions/ExistsExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/ExistsExpression.cs index 3009d9149c5..21fd656abe0 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/ExistsExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/ExistsExpression.cs @@ -50,6 +50,13 @@ public ExistsExpression( protected override Expression VisitChildren(ExpressionVisitor visitor) => Update((SelectExpression)visitor.Visit(Subquery)); + /// + /// Negates this expression by changing presence/absence state indicated by . + /// + /// An expression which is negated form of this expression. + public virtual ExistsExpression Negate() + => new(Subquery, !IsNegated, TypeMapping); + /// /// 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. diff --git a/src/EFCore/Query/Internal/QueryOptimizingExpressionVisitor.cs b/src/EFCore/Query/Internal/QueryOptimizingExpressionVisitor.cs index cc5c4b9f069..965acfd5f37 100644 --- a/src/EFCore/Query/Internal/QueryOptimizingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/QueryOptimizingExpressionVisitor.cs @@ -210,44 +210,38 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp result); } + // Normalize x.Any(i => i == foo) to x.Contains(foo) + // And x.All(i => i != foo) to !x.Contains(foo) if (methodCallExpression.Method.IsGenericMethod && methodCallExpression.Method.GetGenericMethodDefinition() is MethodInfo methodInfo - && (methodInfo.Equals(EnumerableMethods.AnyWithPredicate) || methodInfo.Equals(EnumerableMethods.All)) - && methodCallExpression.Arguments[0].NodeType is ExpressionType nodeType - && (nodeType == ExpressionType.Parameter || nodeType == ExpressionType.Constant) - && methodCallExpression.Arguments[1] is LambdaExpression lambda - && TryExtractEqualityOperands(lambda.Body, out var left, out var right, out var negated) - && (left is ParameterExpression || right is ParameterExpression)) + && (methodInfo == EnumerableMethods.AnyWithPredicate || methodInfo == EnumerableMethods.All || methodInfo == QueryableMethods.AnyWithPredicate || methodInfo == QueryableMethods.All) + && methodCallExpression.Arguments[1].UnwrapLambdaFromQuote() is var lambda + && TryExtractEqualityOperands(lambda.Body, out var left, out var right, out var negated)) { - var nonParameterExpression = left is ParameterExpression ? right : left; + var itemExpression = left == lambda.Parameters[0] + ? right + : right == lambda.Parameters[0] + ? left + : null; - if (methodInfo.Equals(EnumerableMethods.AnyWithPredicate) - && !negated) + if (itemExpression is not null) { - var containsMethod = EnumerableMethods.Contains.MakeGenericMethod(methodCallExpression.Method.GetGenericArguments()[0]); - return Expression.Call(null, containsMethod, methodCallExpression.Arguments[0], nonParameterExpression); - } + var containsMethodDefinition = methodInfo.DeclaringType == typeof(Enumerable) + ? EnumerableMethods.Contains + : QueryableMethods.Contains; - if (methodInfo.Equals(EnumerableMethods.All) && negated) - { - var containsMethod = EnumerableMethods.Contains.MakeGenericMethod(methodCallExpression.Method.GetGenericArguments()[0]); - return Expression.Not(Expression.Call(null, containsMethod, methodCallExpression.Arguments[0], nonParameterExpression)); - } - } - - if (methodCallExpression.Method.IsGenericMethod - && methodCallExpression.Method.GetGenericMethodDefinition() is MethodInfo containsMethodInfo - && containsMethodInfo.Equals(QueryableMethods.Contains)) - { - var typeArgument = methodCallExpression.Method.GetGenericArguments()[0]; - var anyMethod = QueryableMethods.AnyWithPredicate.MakeGenericMethod(typeArgument); - - var anyLambdaParameter = Expression.Parameter(typeArgument, "p"); - var anyLambda = Expression.Lambda( - ExpressionExtensions.CreateEqualsExpression(anyLambdaParameter, methodCallExpression.Arguments[1]), - anyLambdaParameter); + if ((methodInfo == EnumerableMethods.AnyWithPredicate || methodInfo == QueryableMethods.AnyWithPredicate) && !negated) + { + var containsMethod = containsMethodDefinition.MakeGenericMethod(methodCallExpression.Method.GetGenericArguments()[0]); + return Expression.Call(null, containsMethod, methodCallExpression.Arguments[0], itemExpression); + } - return Expression.Call(null, anyMethod, new[] { Visit(methodCallExpression.Arguments[0]), anyLambda }); + if ((methodInfo == EnumerableMethods.All || methodInfo == QueryableMethods.All) && negated) + { + var containsMethod = containsMethodDefinition.MakeGenericMethod(methodCallExpression.Method.GetGenericArguments()[0]); + return Expression.Not(Expression.Call(null, containsMethod, methodCallExpression.Arguments[0], itemExpression)); + } + } } var @object = default(Expression); @@ -409,8 +403,7 @@ private static bool TryExtractEqualityOperands( (left, right) = (binaryExpression.Left, binaryExpression.Right); return true; - case MethodCallExpression methodCallExpression - when methodCallExpression.Method.Name == nameof(object.Equals): + case MethodCallExpression { Method.Name: nameof(object.Equals) } methodCallExpression: { negated = false; if (methodCallExpression.Arguments.Count == 1 diff --git a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs index 1ae89b886bd..5b941c53520 100644 --- a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs @@ -126,6 +126,22 @@ await AssertTranslationFailed( entryCount: 1)); } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Inline_collection_Contains_as_Any_with_predicate(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(c => new[] { 2, 999 }.Any(i => i == c.Id)), + entryCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Inline_collection_negated_Contains_as_All(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(c => new[] { 2, 999 }.All(i => i != c.Id)), + entryCount: 2); + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Parameter_collection_Count(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs index 0592e64383f..3b776e520e0 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs @@ -3084,7 +3084,7 @@ FROM [LevelOne] AS [l] WHERE NOT EXISTS ( SELECT 1 FROM OPENJSON(@__names_0) AS [n] - WHERE ([l0].[Name] = [n].[value] AND [l0].[Name] IS NOT NULL AND [n].[value] IS NOT NULL) OR ([l0].[Name] IS NULL AND [n].[value] IS NULL)) + WHERE [n].[value] = [l0].[Name] OR ([n].[value] IS NULL AND [l0].[Name] IS NULL)) """); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs index 31d674f760a..9d01362a8e4 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs @@ -5415,7 +5415,7 @@ WHERE [l0].[OneToOne_Required_PK_Date] IS NOT NULL AND [l0].[Level1_Required_Id] WHERE NOT EXISTS ( SELECT 1 FROM OPENJSON(@__names_0) AS [n] - WHERE ([t].[Level2_Name] = [n].[value] AND [t].[Level2_Name] IS NOT NULL AND [n].[value] IS NOT NULL) OR ([t].[Level2_Name] IS NULL AND [n].[value] IS NULL)) + WHERE [n].[value] = [t].[Level2_Name] OR ([n].[value] IS NULL AND [t].[Level2_Name] IS NULL)) """); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqlServerTest.cs index d43f5f6136d..222bd5572c9 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqlServerTest.cs @@ -2413,7 +2413,7 @@ FROM [Customers] AS [c] WHERE [c].[City] = N'México D.F.' AND EXISTS ( SELECT 1 FROM OPENJSON(@__ids_0) AS [i] - WHERE [c].[CustomerID] = CAST([i].[value] AS nchar(5))) + WHERE CAST([i].[value] AS nchar(5)) = [c].[CustomerID]) """); } @@ -2430,7 +2430,7 @@ FROM [Customers] AS [c] WHERE NOT EXISTS ( SELECT 1 FROM OPENJSON(@__ids_0) AS [i] - WHERE CAST([i].[value] AS nchar(5)) = [c].[CustomerID] AND [i].[value] IS NOT NULL) + WHERE CAST([i].[value] AS nchar(5)) = [c].[CustomerID]) """); } @@ -2476,7 +2476,7 @@ FROM [Customers] AS [c] WHERE [c].[City] = N'México D.F.' AND NOT EXISTS ( SELECT 1 FROM OPENJSON(@__ids_0) AS [i] - WHERE CAST([i].[value] AS nchar(5)) = [c].[CustomerID] AND [i].[value] IS NOT NULL) + WHERE CAST([i].[value] AS nchar(5)) = [c].[CustomerID]) """, // """ @@ -2487,7 +2487,7 @@ FROM [Customers] AS [c] WHERE [c].[City] = N'México D.F.' AND NOT EXISTS ( SELECT 1 FROM OPENJSON(@__ids_0) AS [i] - WHERE [c].[CustomerID] = CAST([i].[value] AS nchar(5)) AND [i].[value] IS NOT NULL) + WHERE CAST([i].[value] AS nchar(5)) = [c].[CustomerID]) """); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs index ed3a176b359..0748763df67 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs @@ -164,6 +164,30 @@ public override async Task Inline_collection_Contains_with_parameter_and_column_ AssertSql(); } + public override async Task Inline_collection_Contains_as_Any_with_predicate(bool async) + { + await base.Inline_collection_Contains_as_Any_with_predicate(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE [p].[Id] IN (2, 999) +"""); + } + + public override async Task Inline_collection_negated_Contains_as_All(bool async) + { + await base.Inline_collection_negated_Contains_as_All(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE [p].[Id] NOT IN (2, 999) +"""); + } + public override Task Parameter_collection_Count(bool async) => AssertTranslationFailed(() => base.Parameter_collection_Count(async)); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs index 7b6051fc93c..6dff82ec517 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs @@ -166,6 +166,30 @@ public override async Task Inline_collection_Contains_with_parameter_and_column_ AssertSql(); } + public override async Task Inline_collection_Contains_as_Any_with_predicate(bool async) + { + await base.Inline_collection_Contains_as_Any_with_predicate(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE [p].[Id] IN (2, 999) +"""); + } + + public override async Task Inline_collection_negated_Contains_as_All(bool async) + { + await base.Inline_collection_negated_Contains_as_All(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE [p].[Id] NOT IN (2, 999) +"""); + } + public override async Task Parameter_collection_Count(bool async) { await base.Parameter_collection_Count(async); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs index 8fc5012b386..4d632724f6d 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs @@ -168,6 +168,30 @@ public override async Task Inline_collection_Contains_with_parameter_and_column_ AssertSql(); } + public override async Task Inline_collection_Contains_as_Any_with_predicate(bool async) + { + await base.Inline_collection_Contains_as_Any_with_predicate(async); + + AssertSql( +""" +SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."String", "p"."Strings" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE "p"."Id" IN (2, 999) +"""); + } + + public override async Task Inline_collection_negated_Contains_as_All(bool async) + { + await base.Inline_collection_negated_Contains_as_All(async); + + AssertSql( +""" +SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."String", "p"."Strings" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE "p"."Id" NOT IN (2, 999) +"""); + } + public override async Task Parameter_collection_Count(bool async) { await base.Parameter_collection_Count(async);