diff --git a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs index eb35acf1e42..44fa277dc61 100644 --- a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs +++ b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections; +using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; @@ -28,6 +29,9 @@ public class SqlNullabilityProcessor : ExpressionVisitor /// private readonly Dictionary> _collectionParameterExpansionMap; + private static readonly bool UseOldBehavior37216 = + AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue37216", out var enabled) && enabled; + /// /// Creates a new instance of the class. /// @@ -185,6 +189,33 @@ protected override Expression VisitExtension(Expression node) throw new UnreachableException(); } + // We've inlined the user-provided collection from the values parameter into the SQL VALUES expression: (VALUES (1), (2)...). + // However, if the collection happens to be empty, this doesn't work as VALUES does not support empty sets. We convert it + // to a SELECT ... WHERE false to produce an empty result set instead. + if (!UseOldBehavior37216 && processedValues.Count == 0) + { + var select = new SelectExpression( + valuesExpression.Alias, + tables: [], + predicate: new SqlConstantExpression(false, Dependencies.TypeMappingSource.FindMapping(typeof(bool))), + groupBy: [], + having: null, + projections: valuesExpression.ColumnNames + .Select(n => new ProjectionExpression( + new SqlConstantExpression(value: null, elementTypeMapping.ClrType, elementTypeMapping), n)) + .ToList(), + distinct: false, + orderings: [], + offset: null, + limit: null, + tags: ReadOnlySet.Empty, + annotations: null, + sqlAliasManager: null!, + isMutable: false); + + return select; + } + return valuesExpression.Update(processedValues); } diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs index 057e4638a6b..43aba8ed1a0 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs @@ -777,6 +777,28 @@ WHERE ARRAY_CONTAINS(@ints, c["Int"]) """); } + public override async Task Parameter_collection_empty_Contains() + { + await base.Parameter_collection_empty_Contains(); + + AssertSql( + """ +@ints='[]' + +SELECT VALUE c +FROM root c +WHERE ARRAY_CONTAINS(@ints, c["Int"]) +"""); + } + + public override async Task Parameter_collection_empty_Join() + { + // Cosmos join support. Issue #16920. + await AssertTranslationFailed(base.Parameter_collection_empty_Join); + + AssertSql(); + } + public override async Task Parameter_collection_Contains_with_EF_Constant() { // #34327 diff --git a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs index c78fa4ff783..0fbe6d7e60e 100644 --- a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs @@ -458,6 +458,26 @@ await AssertQuery( assertEmpty: true); } + [ConditionalFact] + public virtual async Task Parameter_collection_empty_Contains() + { + int[] ints = []; + + await AssertQuery( + ss => ss.Set().Where(c => ints.Contains(c.Int)), + assertEmpty: true); + } + + [ConditionalFact] // #37216 + public virtual async Task Parameter_collection_empty_Join() + { + int[] ints = []; + + await AssertQuery( + ss => ss.Set().Join(ints, e => e.Id, i => i, (e, i) => e), + assertEmpty: true); + } + [ConditionalFact] public virtual Task Parameter_collection_Contains_with_EF_Constant() { diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs index 6f3d948549f..ccf4f021119 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs @@ -768,6 +768,33 @@ FROM [PrimitiveCollectionsEntity] AS [p] """); } + public override async Task Parameter_collection_empty_Contains() + { + await base.Parameter_collection_empty_Contains(); + + 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 0 = 1 +"""); + } + + public override async Task Parameter_collection_empty_Join() + { + await base.Parameter_collection_empty_Join(); + + 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] +INNER JOIN ( + SELECT NULL AS [Value] + WHERE 0 = 1 +) AS [p0] ON [p].[Id] = [p0].[Value] +"""); + } + public override async Task Parameter_collection_Contains_with_EF_Constant() { await base.Parameter_collection_Contains_with_EF_Constant(); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServer160Test.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServer160Test.cs index 1c7d9eead0d..0e4ef3caed9 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServer160Test.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServer160Test.cs @@ -762,6 +762,33 @@ FROM [PrimitiveCollectionsEntity] AS [p] """); } + public override async Task Parameter_collection_empty_Contains() + { + await base.Parameter_collection_empty_Contains(); + + 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 0 = 1 +"""); + } + + public override async Task Parameter_collection_empty_Join() + { + await base.Parameter_collection_empty_Join(); + + 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] +INNER JOIN ( + SELECT NULL AS [Value] + WHERE 0 = 1 +) AS [p0] ON [p].[Id] = [p0].[Value] +"""); + } + public override async Task Parameter_collection_Contains_with_EF_Constant() { await base.Parameter_collection_Contains_with_EF_Constant(); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs index d279073a0ad..2dac2675206 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs @@ -966,6 +966,33 @@ FROM [PrimitiveCollectionsEntity] AS [p] """); } + public override async Task Parameter_collection_empty_Contains() + { + await base.Parameter_collection_empty_Contains(); + + 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 0 = 1 +"""); + } + + public override async Task Parameter_collection_empty_Join() + { + await base.Parameter_collection_empty_Join(); + + 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] +INNER JOIN ( + SELECT NULL AS [Value] + WHERE 0 = 1 +) AS [p0] ON [p].[Id] = [p0].[Value] +"""); + } + public override async Task Column_collection_of_ints_Contains() { await base.Column_collection_of_ints_Contains(); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs index 7d68c865f60..5773571ae99 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs @@ -785,6 +785,33 @@ FROM [PrimitiveCollectionsEntity] AS [p] """); } + public override async Task Parameter_collection_empty_Contains() + { + await base.Parameter_collection_empty_Contains(); + + 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 0 = 1 +"""); + } + + public override async Task Parameter_collection_empty_Join() + { + await base.Parameter_collection_empty_Join(); + + 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] +INNER JOIN ( + SELECT NULL AS [Value] + WHERE 0 = 1 +) AS [p0] ON [p].[Id] = [p0].[Value] +"""); + } + public override async Task Parameter_collection_Contains_with_EF_Constant() { await base.Parameter_collection_Contains_with_EF_Constant(); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs index 90ab6e1d1d0..e20926ebe4f 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs @@ -773,6 +773,33 @@ WHERE 0 """); } + public override async Task Parameter_collection_empty_Contains() + { + await base.Parameter_collection_empty_Contains(); + + 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 0 +"""); + } + + public override async Task Parameter_collection_empty_Join() + { + await base.Parameter_collection_empty_Join(); + + 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" +INNER JOIN ( + SELECT NULL AS "Value" + WHERE 0 +) AS "p0" ON "p"."Id" = "p0"."Value" +"""); + } + public override async Task Parameter_collection_Contains_with_EF_Constant() { await base.Parameter_collection_Contains_with_EF_Constant();