diff --git a/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs b/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs index cd46015679b..d520b5fc6e3 100644 --- a/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs +++ b/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs @@ -253,12 +253,6 @@ public static string OneOfTwoValuesMustBeSet(object? param1, object? param2) GetString("OneOfTwoValuesMustBeSet", nameof(param1), nameof(param2)), param1, param2); - /// - /// Only constants or parameters are currently allowed in Contains. - /// - public static string OnlyConstantsAndParametersAllowedInContains - => GetString("OnlyConstantsAndParametersAllowedInContains"); - /// /// The entity of type '{entityType}' is mapped as a part of the document mapped to '{missingEntityType}', but there is no tracked entity of this type with the corresponding key value. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the key values. /// diff --git a/src/EFCore.Cosmos/Properties/CosmosStrings.resx b/src/EFCore.Cosmos/Properties/CosmosStrings.resx index 2417a7c1a77..9aa9ddd4412 100644 --- a/src/EFCore.Cosmos/Properties/CosmosStrings.resx +++ b/src/EFCore.Cosmos/Properties/CosmosStrings.resx @@ -246,9 +246,6 @@ Exactly one of '{param1}' or '{param2}' must be set. - - Only constants or parameters are currently allowed in Contains. - The entity of type '{entityType}' is mapped as a part of the document mapped to '{missingEntityType}', but there is no tracked entity of this type with the corresponding key value. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the key values. diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.InExpressionValuesExpandingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.InExpressionValuesExpandingExpressionVisitor.cs index 01abb10bb82..8e584675de3 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.InExpressionValuesExpandingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.InExpressionValuesExpandingExpressionVisitor.cs @@ -10,64 +10,34 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; public partial class CosmosShapedQueryCompilingExpressionVisitor { - private sealed class InExpressionValuesExpandingExpressionVisitor : ExpressionVisitor + private sealed class InExpressionValuesExpandingExpressionVisitor( + ISqlExpressionFactory sqlExpressionFactory, + IReadOnlyDictionary parametersValues) + : ExpressionVisitor { - private readonly ISqlExpressionFactory _sqlExpressionFactory; - private readonly IReadOnlyDictionary _parametersValues; - - public InExpressionValuesExpandingExpressionVisitor( - ISqlExpressionFactory sqlExpressionFactory, - IReadOnlyDictionary parametersValues) - { - _sqlExpressionFactory = sqlExpressionFactory; - _parametersValues = parametersValues; - } - - public override Expression Visit(Expression expression) + protected override Expression VisitExtension(Expression expression) { if (expression is InExpression inExpression) { - var inValues = new List(); - var hasNullValue = false; + IReadOnlyList values; switch (inExpression) { - case { ValuesParameter: SqlParameterExpression valuesParameter }: - { - var typeMapping = valuesParameter.TypeMapping; - - foreach (var value in (IEnumerable)_parametersValues[valuesParameter.Name]) - { - if (value is null) - { - hasNullValue = true; - continue; - } - - inValues.Add(_sqlExpressionFactory.Constant(value, typeMapping)); - } - + case { Values: IReadOnlyList values2 }: + values = values2; break; - } - case { Values: IReadOnlyList values }: + // TODO: IN with subquery (return immediately, nothing to do here) + + case { ValuesParameter: SqlParameterExpression valuesParameter }: { - foreach (var value in values) + var typeMapping = valuesParameter.TypeMapping; + var mutableValues = new List(); + foreach (var value in (IEnumerable)parametersValues[valuesParameter.Name]) { - if (value is not (SqlConstantExpression or SqlParameterExpression)) - { - throw new InvalidOperationException(CosmosStrings.OnlyConstantsAndParametersAllowedInContains); - } - - if (IsNull(value)) - { - hasNullValue = true; - continue; - } - - inValues.Add(value); + mutableValues.Add(sqlExpressionFactory.Constant(value, typeMapping)); } - + values = mutableValues; break; } @@ -75,34 +45,12 @@ public override Expression Visit(Expression expression) throw new UnreachableException(); } - var updatedInExpression = inValues.Count > 0 - ? _sqlExpressionFactory.In((SqlExpression)Visit(inExpression.Item), inValues) - : null; - - var nullCheckExpression = hasNullValue - ? _sqlExpressionFactory.IsNull(inExpression.Item) - : null; - - if (updatedInExpression != null - && nullCheckExpression != null) - { - return _sqlExpressionFactory.OrElse(updatedInExpression, nullCheckExpression); - } - - if (updatedInExpression == null - && nullCheckExpression == null) - { - return _sqlExpressionFactory.Equal(_sqlExpressionFactory.Constant(true), _sqlExpressionFactory.Constant(false)); - } - - return (SqlExpression)updatedInExpression ?? nullCheckExpression; + return values.Count == 0 + ? sqlExpressionFactory.ApplyDefaultTypeMapping(sqlExpressionFactory.Constant(false)) + : sqlExpressionFactory.In((SqlExpression)Visit(inExpression.Item), values); } - return base.Visit(expression); + return base.VisitExtension(expression); } - - private bool IsNull(SqlExpression expression) - => expression is SqlConstantExpression { Value: null } - || expression is SqlParameterExpression { Name: string parameterName } && _parametersValues[parameterName] is null; } } diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindAggregateOperatorsQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindAggregateOperatorsQueryCosmosTest.cs index eca2f8a19fd..d08ce825887 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindAggregateOperatorsQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindAggregateOperatorsQueryCosmosTest.cs @@ -1806,9 +1806,8 @@ public override Task Contains_with_local_ordered_read_only_collection_all_null(b """ SELECT c FROM root c -WHERE ((c["Discriminator"] = "Customer") AND (c["CustomerID"] = null)) -""" - ); +WHERE ((c["Discriminator"] = "Customer") AND c["CustomerID"] IN (null, null)) +"""); }); public override Task Contains_with_local_read_only_collection_inline(bool async) @@ -1969,7 +1968,7 @@ public override Task Contains_with_local_collection_empty_inline(bool async) """ SELECT c FROM root c -WHERE ((c["Discriminator"] = "Customer") AND NOT((true = false))) +WHERE ((c["Discriminator"] = "Customer") AND NOT(false)) """); }); diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs index 389a10b238b..f57b1a24b00 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs @@ -4158,7 +4158,7 @@ public override Task Entity_equality_contains_with_list_of_null(bool async) """ SELECT c FROM root c -WHERE ((c["Discriminator"] = "Customer") AND (c["CustomerID"] IN ("ALFKI") OR (c["CustomerID"] = null))) +WHERE ((c["Discriminator"] = "Customer") AND c["CustomerID"] IN (null, "ALFKI")) """); }); diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs index 152837b212d..2c40a6e83b5 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs @@ -55,7 +55,7 @@ public override Task Inline_collection_of_nullable_ints_Contains_null(bool async """ SELECT c FROM root c -WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND (c["NullableInt"] IN (999) OR (c["NullableInt"] = null))) +WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND c["NullableInt"] IN (null, 999)) """); }); @@ -137,7 +137,7 @@ public override Task Inline_collection_Contains_with_zero_values(bool async) """ SELECT c FROM root c -WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND (true = false)) +WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND false) """); }); @@ -230,13 +230,37 @@ FROM root c """); }); - // TODO: Remove incorrect null semantics compensation for Cosmos: #31063 public override Task Inline_collection_Contains_with_mixed_value_types(bool async) - => Assert.ThrowsAsync(() => base.Inline_collection_Contains_with_mixed_value_types(async)); + => CosmosTestHelpers.Instance.NoSyncTest( + async, async a => + { + await base.Inline_collection_Contains_with_mixed_value_types(a); + + AssertSql( + """ +@__i_0='11' + +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND c["Int"] IN (999, @__i_0, c["Id"], (c["Id"] + c["Int"]))) +"""); + }); - // TODO: Remove incorrect null semantics compensation for Cosmos: #31063 public override Task Inline_collection_List_Contains_with_mixed_value_types(bool async) - => Assert.ThrowsAsync(() => base.Inline_collection_List_Contains_with_mixed_value_types(async)); + => CosmosTestHelpers.Instance.NoSyncTest( + async, async a => + { + await base.Inline_collection_List_Contains_with_mixed_value_types(a); + + AssertSql( + """ +@__i_0='11' + +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND c["Int"] IN (999, @__i_0, c["Id"], (c["Id"] + c["Int"]))) +"""); + }); public override Task Inline_collection_Contains_as_Any_with_predicate(bool async) => CosmosTestHelpers.Instance.NoSyncTest( diff --git a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs index 2017dd60587..69fa6aeae14 100644 --- a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs @@ -147,7 +147,7 @@ public virtual async Task Inline_collection_List_Contains_with_mixed_value_types await AssertQuery( async, - ss => ss.Set().Where(c => new List() { 999, i, c.Id, c.Id + c.Int }.Contains(c.Int))); + ss => ss.Set().Where(c => new List { 999, i, c.Id, c.Id + c.Int }.Contains(c.Int))); } [ConditionalTheory]