From af19b405f32a3021bef45e79a6d1b9ae0df23073 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jiri=20Cincura=20=E2=86=B9?= Date: Mon, 2 Dec 2024 15:18:45 +0100 Subject: [PATCH] Fix Contains on ImmutableArray (#35247) --- ...yableMethodNormalizingExpressionVisitor.cs | 6 +++- src/EFCore/Query/QueryRootProcessor.cs | 13 ++++++++- .../PrimitiveCollectionsQueryCosmosTest.cs | 24 ++++++++++++++++ .../PrimitiveCollectionsQueryTestBase.cs | 16 +++++++++++ ...imitiveCollectionsQueryOldSqlServerTest.cs | 18 ++++++++++++ ...imitiveCollectionsQuerySqlServer160Test.cs | 28 +++++++++++++++++++ ...veCollectionsQuerySqlServerJsonTypeTest.cs | 28 +++++++++++++++++++ .../PrimitiveCollectionsQuerySqlServerTest.cs | 28 +++++++++++++++++++ .../PrimitiveCollectionsQuerySqliteTest.cs | 28 +++++++++++++++++++ 9 files changed, 187 insertions(+), 2 deletions(-) diff --git a/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs b/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs index 4e272125cda..145f28f0927 100644 --- a/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs @@ -492,12 +492,16 @@ private Expression TryConvertCollectionContainsToQueryableContains(MethodCallExp var sourceType = methodCallExpression.Method.DeclaringType!.GetGenericArguments()[0]; + var objectExpression = methodCallExpression.Object!.Type.IsValueType + ? Expression.Convert(methodCallExpression.Object!, typeof(IEnumerable<>).MakeGenericType(sourceType)) + : methodCallExpression.Object!; + return VisitMethodCall( Expression.Call( QueryableMethods.Contains.MakeGenericMethod(sourceType), Expression.Call( QueryableMethods.AsQueryable.MakeGenericMethod(sourceType), - methodCallExpression.Object!), + objectExpression), methodCallExpression.Arguments[0])); } diff --git a/src/EFCore/Query/QueryRootProcessor.cs b/src/EFCore/Query/QueryRootProcessor.cs index 34a645d35ca..8c18c4c64ed 100644 --- a/src/EFCore/Query/QueryRootProcessor.cs +++ b/src/EFCore/Query/QueryRootProcessor.cs @@ -85,7 +85,18 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp private Expression VisitQueryRootCandidate(Expression expression, Type elementClrType) { - switch (expression) + var candidateExpression = expression; + + // In case the collection was value type, in order to call methods like AsQueryable, + // we need to convert it to IEnumerable which requires boxing. + // We do that with Convert expression which we need to unwrap here. + if (expression is UnaryExpression { NodeType: ExpressionType.Convert } convertExpression + && convertExpression.Type.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + { + candidateExpression = convertExpression.Operand; + } + + switch (candidateExpression) { // An array containing only constants is represented as a ConstantExpression with the array as the value. // Convert that into a NewArrayExpression for use with InlineQueryRootExpression diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs index aae293479f5..cfbed155d22 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs @@ -613,6 +613,30 @@ WHERE ARRAY_CONTAINS(@ints, c["Int"]) """ @ints='[10,999]' +SELECT VALUE c +FROM root c +WHERE NOT(ARRAY_CONTAINS(@ints, c["Int"])) +"""); + }); + + public override Task Parameter_collection_ImmutableArray_of_ints_Contains_int(bool async) + => CosmosTestHelpers.Instance.NoSyncTest( + async, async a => + { + await base.Parameter_collection_ImmutableArray_of_ints_Contains_int(a); + + AssertSql( + """ +@ints='[10,999]' + +SELECT VALUE c +FROM root c +WHERE ARRAY_CONTAINS(@ints, c["Int"]) +""", + // + """ +@ints='[10,999]' + SELECT VALUE c FROM root c WHERE NOT(ARRAY_CONTAINS(@ints, c["Int"])) diff --git a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs index afecb186dbc..89db4ccf6e2 100644 --- a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Immutable; + namespace Microsoft.EntityFrameworkCore.Query; public abstract class PrimitiveCollectionsQueryTestBase(TFixture fixture) : QueryTestBase(fixture) @@ -363,6 +365,20 @@ await AssertQuery( ss => ss.Set().Where(c => !ints.Contains(c.Int))); } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Parameter_collection_ImmutableArray_of_ints_Contains_int(bool async) + { + var ints = ImmutableArray.Create([10, 999]); + + await AssertQuery( + async, + ss => ss.Set().Where(c => ints.Contains(c.Int))); + await AssertQuery( + async, + ss => ss.Set().Where(c => !ints.Contains(c.Int))); + } + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual async Task Parameter_collection_of_ints_Contains_nullable_int(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs index f52341e6cfe..7d620195409 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs @@ -498,6 +498,24 @@ WHERE [p].[Int] NOT IN (10, 999) """); } + public override async Task Parameter_collection_ImmutableArray_of_ints_Contains_int(bool async) + { + await base.Parameter_collection_ImmutableArray_of_ints_Contains_int(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].[NullableString], [p].[NullableStrings], [p].[NullableWrappedId], [p].[NullableWrappedIdWithNullableComparer], [p].[String], [p].[Strings], [p].[WrappedId] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE [p].[Int] IN (10, 999) +""", + // + """ +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[NullableWrappedId], [p].[NullableWrappedIdWithNullableComparer], [p].[String], [p].[Strings], [p].[WrappedId] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE [p].[Int] NOT IN (10, 999) +"""); + } + public override async Task Parameter_collection_of_ints_Contains_nullable_int(bool async) { await base.Parameter_collection_of_ints_Contains_nullable_int(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServer160Test.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServer160Test.cs index 75e95008861..1bc4bec3050 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServer160Test.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServer160Test.cs @@ -516,6 +516,34 @@ FROM OPENJSON(@ints) WITH ([value] int '$') AS [i] """ @ints='[10,999]' (Size = 4000) +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[NullableWrappedId], [p].[NullableWrappedIdWithNullableComparer], [p].[String], [p].[Strings], [p].[WrappedId] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE [p].[Int] NOT IN ( + SELECT [i].[value] + FROM OPENJSON(@ints) WITH ([value] int '$') AS [i] +) +"""); + } + + public override async Task Parameter_collection_ImmutableArray_of_ints_Contains_int(bool async) + { + await base.Parameter_collection_ImmutableArray_of_ints_Contains_int(async); + + AssertSql( + """ +@ints='[10,999]' (Nullable = false) (Size = 4000) + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[NullableWrappedId], [p].[NullableWrappedIdWithNullableComparer], [p].[String], [p].[Strings], [p].[WrappedId] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE [p].[Int] IN ( + SELECT [i].[value] + FROM OPENJSON(@ints) WITH ([value] int '$') AS [i] +) +""", + // + """ +@ints='[10,999]' (Nullable = false) (Size = 4000) + SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[NullableWrappedId], [p].[NullableWrappedIdWithNullableComparer], [p].[String], [p].[Strings], [p].[WrappedId] FROM [PrimitiveCollectionsEntity] AS [p] WHERE [p].[Int] NOT IN ( diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs index aa99a9bae29..d37a0797f4f 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs @@ -532,6 +532,34 @@ FROM OPENJSON(@ints) WITH ([value] int '$') AS [i] """ @ints='[10,999]' (Size = 4000) +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE [p].[Int] NOT IN ( + SELECT [i].[value] + FROM OPENJSON(@ints) WITH ([value] int '$') AS [i] +) +"""); + } + + public override async Task Parameter_collection_ImmutableArray_of_ints_Contains_int(bool async) + { + await base.Parameter_collection_ImmutableArray_of_ints_Contains_int(async); + + AssertSql( + """ +@ints='[10,999]' (Nullable = false) (Size = 4000) + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE [p].[Int] IN ( + SELECT [i].[value] + FROM OPENJSON(@ints) WITH ([value] int '$') AS [i] +) +""", + // + """ +@ints='[10,999]' (Nullable = false) (Size = 4000) + SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings] FROM [PrimitiveCollectionsEntity] AS [p] WHERE [p].[Int] NOT IN ( diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs index 85e51bfaef6..3e9a1f7ac00 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs @@ -539,6 +539,34 @@ FROM OPENJSON(@ints) WITH ([value] int '$') AS [i] """ @ints='[10,999]' (Size = 4000) +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[NullableWrappedId], [p].[NullableWrappedIdWithNullableComparer], [p].[String], [p].[Strings], [p].[WrappedId] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE [p].[Int] NOT IN ( + SELECT [i].[value] + FROM OPENJSON(@ints) WITH ([value] int '$') AS [i] +) +"""); + } + + public override async Task Parameter_collection_ImmutableArray_of_ints_Contains_int(bool async) + { + await base.Parameter_collection_ImmutableArray_of_ints_Contains_int(async); + + AssertSql( + """ +@ints='[10,999]' (Nullable = false) (Size = 4000) + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[NullableWrappedId], [p].[NullableWrappedIdWithNullableComparer], [p].[String], [p].[Strings], [p].[WrappedId] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE [p].[Int] IN ( + SELECT [i].[value] + FROM OPENJSON(@ints) WITH ([value] int '$') AS [i] +) +""", + // + """ +@ints='[10,999]' (Nullable = false) (Size = 4000) + SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[NullableWrappedId], [p].[NullableWrappedIdWithNullableComparer], [p].[String], [p].[Strings], [p].[WrappedId] FROM [PrimitiveCollectionsEntity] AS [p] WHERE [p].[Int] NOT IN ( diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs index 01ddc2f4973..36707983506 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs @@ -529,6 +529,34 @@ FROM json_each(@ints) AS "i" """ @ints='[10,999]' (Size = 8) +SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."NullableString", "p"."NullableStrings", "p"."NullableWrappedId", "p"."NullableWrappedIdWithNullableComparer", "p"."String", "p"."Strings", "p"."WrappedId" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE "p"."Int" NOT IN ( + SELECT "i"."value" + FROM json_each(@ints) AS "i" +) +"""); + } + + public override async Task Parameter_collection_ImmutableArray_of_ints_Contains_int(bool async) + { + await base.Parameter_collection_ImmutableArray_of_ints_Contains_int(async); + + AssertSql( + """ +@ints='[10,999]' (Nullable = false) (Size = 8) + +SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."NullableString", "p"."NullableStrings", "p"."NullableWrappedId", "p"."NullableWrappedIdWithNullableComparer", "p"."String", "p"."Strings", "p"."WrappedId" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE "p"."Int" IN ( + SELECT "i"."value" + FROM json_each(@ints) AS "i" +) +""", + // + """ +@ints='[10,999]' (Nullable = false) (Size = 8) + SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."NullableString", "p"."NullableStrings", "p"."NullableWrappedId", "p"."NullableWrappedIdWithNullableComparer", "p"."String", "p"."Strings", "p"."WrappedId" FROM "PrimitiveCollectionsEntity" AS "p" WHERE "p"."Int" NOT IN (