diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosReadItemAndPartitionKeysExtractor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosReadItemAndPartitionKeysExtractor.cs index 411e06a526e..c0221b87e81 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosReadItemAndPartitionKeysExtractor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosReadItemAndPartitionKeysExtractor.cs @@ -217,7 +217,7 @@ protected override Expression VisitExtension(Expression node) return node; } - case SqlBinaryExpression { OperatorType: ExpressionType.Equal, Left: var left, Right: var right } binary: + case SqlBinaryExpression { OperatorType: ExpressionType.Equal, Left: SqlExpression left, Right: SqlExpression right } binary: { // TODO: Handle property accesses into complex types/owned entity types, #25548 var (scalarAccess, propertyValue) = diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs index 5e6694e3045..20992155bf3 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs @@ -4,6 +4,7 @@ using System.Collections; using System.Diagnostics.CodeAnalysis; using Microsoft.EntityFrameworkCore.Cosmos.Internal; +using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; using Microsoft.EntityFrameworkCore.Internal; using static Microsoft.EntityFrameworkCore.Infrastructure.ExpressionExtensions; @@ -815,6 +816,8 @@ protected override Expression VisitUnary(UnaryExpression unaryExpression) return unaryExpression.NodeType switch { + ExpressionType.Not when operand is SqlConstantExpression { Value: bool boolValue } + => sqlExpressionFactory.Constant(!boolValue), ExpressionType.Not => sqlExpressionFactory.Not(sqlOperand!), @@ -1079,27 +1082,22 @@ private bool TryRewriteEntityEquality( || right is SqlConstantExpression { Value: null }) { var nonNullEntityReference = (left is SqlConstantExpression { Value: null } ? rightEntityReference : leftEntityReference)!; - var entityType1 = nonNullEntityReference.EntityType; - var primaryKeyProperties1 = entityType1.FindPrimaryKey()?.Properties; - if (primaryKeyProperties1 == null) + var shaper = nonNullEntityReference.Parameter + ?? (StructuralTypeShaperExpression)nonNullEntityReference.Subquery!.ShaperExpression; + + if (!shaper.IsNullable) { - throw new InvalidOperationException( - CoreStrings.EntityEqualityOnKeylessEntityNotSupported( - nodeType == ExpressionType.Equal - ? equalsMethod ? nameof(object.Equals) : "==" - : equalsMethod - ? "!" + nameof(object.Equals) - : "!=", - entityType1.DisplayName())); + result = Visit(Expression.Constant(nodeType != ExpressionType.Equal)); + return true; } - result = Visit( - primaryKeyProperties1.Select(p => - Expression.MakeBinary( - nodeType, CreatePropertyAccessExpression(nonNullEntityReference, p), - Expression.Constant(null, p.ClrType.MakeNullable()))) - .Aggregate((l, r) => nodeType == ExpressionType.Equal ? Expression.OrElse(l, r) : Expression.AndAlso(l, r))); - + var access = Visit(shaper.ValueBufferExpression); + result = new SqlBinaryExpression( + nodeType, + access, + sqlExpressionFactory.Constant(null, typeof(object), CosmosTypeMapping.Default)!, + typeof(bool), + typeMappingSource.FindMapping(typeof(bool)))!; return true; } diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosValueConverterCompensatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosValueConverterCompensatingExpressionVisitor.cs index 5ed43e4dacb..a90d957170d 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosValueConverterCompensatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosValueConverterCompensatingExpressionVisitor.cs @@ -91,10 +91,15 @@ ScalarAccessExpression keyAccessExpression SqlUnaryExpression sqlUnaryExpression => sqlUnaryExpression.Update(TryCompensateForBoolWithValueConverter(sqlUnaryExpression.Operand)), - SqlBinaryExpression { OperatorType: ExpressionType.AndAlso or ExpressionType.OrElse } sqlBinaryExpression + SqlBinaryExpression + { + OperatorType: ExpressionType.AndAlso or ExpressionType.OrElse, + Left: SqlExpression left, + Right: SqlExpression right + } sqlBinaryExpression => sqlBinaryExpression.Update( - TryCompensateForBoolWithValueConverter(sqlBinaryExpression.Left), - TryCompensateForBoolWithValueConverter(sqlBinaryExpression.Right)), + TryCompensateForBoolWithValueConverter(left), + TryCompensateForBoolWithValueConverter(right)), _ => sqlExpression }; diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/SqlBinaryExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/SqlBinaryExpression.cs index a0e3dc07408..17f0ca9c086 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/SqlBinaryExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/SqlBinaryExpression.cs @@ -22,8 +22,8 @@ public class SqlBinaryExpression : SqlExpression /// public SqlBinaryExpression( ExpressionType operatorType, - SqlExpression left, - SqlExpression right, + Expression left, + Expression right, Type type, CoreTypeMapping? typeMapping) : base(type, typeMapping) @@ -54,7 +54,7 @@ public SqlBinaryExpression( /// 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 Left { get; } + public virtual Expression Left { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -62,7 +62,7 @@ public SqlBinaryExpression( /// 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 Right { get; } + public virtual Expression Right { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -72,8 +72,8 @@ public SqlBinaryExpression( /// protected override Expression VisitChildren(ExpressionVisitor visitor) { - var left = (SqlExpression)visitor.Visit(Left); - var right = (SqlExpression)visitor.Visit(Right); + var left = visitor.Visit(Left); + var right = visitor.Visit(Right); return Update(left, right); } @@ -84,7 +84,7 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) /// 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 SqlBinaryExpression Update(SqlExpression left, SqlExpression right) + public virtual SqlBinaryExpression Update(Expression left, Expression right) => left != Left || right != Right ? new SqlBinaryExpression(OperatorType, left, right, Type, TypeMapping) : this; @@ -166,7 +166,7 @@ protected override void Print(ExpressionPrinter expressionPrinter) expressionPrinter.Append(")"); } - static bool RequiresBrackets(SqlExpression expression) + static bool RequiresBrackets(Expression expression) => expression is SqlBinaryExpression; } diff --git a/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs b/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs index 951ab06cf82..abd26d5e46e 100644 --- a/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs +++ b/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs @@ -109,8 +109,8 @@ private SqlExpression ApplyTypeMappingOnSqlBinary( SqlBinaryExpression sqlBinaryExpression, CoreTypeMapping? typeMapping) { - var left = sqlBinaryExpression.Left; - var right = sqlBinaryExpression.Right; + var left = sqlBinaryExpression.Left as SqlExpression ?? throw new UnreachableException(); + var right = sqlBinaryExpression.Right as SqlExpression ?? throw new UnreachableException(); Type resultType; CoreTypeMapping? resultTypeMapping; diff --git a/test/EFCore.Cosmos.FunctionalTests/CosmosTransactionalBatchTest.cs b/test/EFCore.Cosmos.FunctionalTests/CosmosTransactionalBatchTest.cs index 439bb87ee7f..be8b2e23294 100644 --- a/test/EFCore.Cosmos.FunctionalTests/CosmosTransactionalBatchTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/CosmosTransactionalBatchTest.cs @@ -13,6 +13,7 @@ public class CosmosTransactionalBatchTest(CosmosTransactionalBatchTest.CosmosFix private const string DatabaseName = nameof(CosmosTransactionalBatchTest); protected CosmosFixture Fixture { get; } = fixture; + [ConditionalFact] public virtual async Task SaveChanges_fails_for_duplicate_key_in_same_partition_prevents_other_inserts_in_same_partition_even_if_staged_before_add() { diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsCollectionCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsCollectionCosmosTest.cs index 674facf26dd..294657d03a3 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsCollectionCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsCollectionCosmosTest.cs @@ -26,6 +26,19 @@ FROM root c """); } + [ConditionalFact] + public async Task Where_first_inline_not_null() + { + await AssertQuery(ss => ss.Set().Where(e => e.AssociateCollection.FirstOrDefault() != null)); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE ((c["AssociateCollection"][0] ?? null) != null) +"""); + } + public override async Task Where() { await base.Where(); diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualityCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualityCosmosTest.cs index b19b56da69b..7db2fb379e3 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualityCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualityCosmosTest.cs @@ -48,14 +48,32 @@ WHERE false """); } - public override Task Associate_with_inline_null() - => Assert.ThrowsAsync(() => base.Associate_with_inline_null()); + public override async Task Associate_with_inline_null() + { + await base.Associate_with_inline_null(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (c["OptionalAssociate"] = null) +"""); + } public override Task Associate_with_parameter_null() => Assert.ThrowsAsync(() => base.Associate_with_parameter_null()); - public override Task Nested_associate_with_inline_null() - => Assert.ThrowsAsync(() => base.Nested_associate_with_inline_null()); + public override async Task Nested_associate_with_inline_null() + { + await base.Nested_associate_with_inline_null(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (c["RequiredAssociate"]["OptionalNestedAssociate"] = null) +"""); + } public override async Task Nested_associate_with_inline() { @@ -99,6 +117,23 @@ public override async Task Nested_collection_with_parameter() #region Contains + [ConditionalFact] + public async Task Contains_with_inline_null() + { + await AssertQuery(ss => ss.Set().Where(e => + e.RequiredAssociate.NestedCollection.Contains(null!)), assertEmpty: true); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE EXISTS ( + SELECT 1 + FROM n IN c["RequiredAssociate"]["NestedCollection"] + WHERE false) +"""); + } + public override async Task Contains_with_inline() { // No backing field could be found for property 'RootEntity.RequiredRelated#RelatedType.NestedCollection#NestedType.RelatedTypeRootEntityId' and the property does not have a getter. diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs index 66192a6c080..1b33b8007bc 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs @@ -167,7 +167,7 @@ public override Task Entity_equality_null(bool async) """ SELECT VALUE c["id"] FROM root c -WHERE (c["id"] = null) +WHERE false """); }); @@ -181,7 +181,6 @@ public override Task Entity_equality_not_null(bool async) """ SELECT VALUE c["id"] FROM root c -WHERE (c["id"] != null) """); }); @@ -2895,7 +2894,7 @@ public override Task Comparing_entity_to_null_using_Equals(bool async) """ SELECT VALUE c["id"] FROM root c -WHERE (STARTSWITH(c["id"], "A") AND NOT((c["id"] = null))) +WHERE STARTSWITH(c["id"], "A") ORDER BY c["id"] """); }); @@ -2941,7 +2940,7 @@ public override Task Comparing_collection_navigation_to_null(bool async) """ SELECT VALUE c["id"] FROM root c -WHERE (c["id"] = null) +WHERE false """); }); @@ -4016,7 +4015,7 @@ public override Task Entity_equality_through_include(bool async) """ SELECT VALUE c["id"] FROM root c -WHERE (c["id"] = null) +WHERE false """); }); @@ -4125,7 +4124,7 @@ public override Task Entity_equality_not_null_composite_key(bool async) """ SELECT VALUE c FROM root c -WHERE ((c["$type"] = "OrderDetail") AND ((c["OrderID"] != null) AND (c["ProductID"] != null))) +WHERE (c["$type"] = "OrderDetail") """); }); @@ -4195,7 +4194,12 @@ public override Task Null_parameter_name_works(bool async) { await base.Null_parameter_name_works(a); - AssertSql("ReadItem(None, null)"); + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE false +"""); }); public override Task Where_Property_shadow_closure(bool async) @@ -4288,7 +4292,7 @@ public override Task Entity_equality_null_composite_key(bool async) """ SELECT VALUE c FROM root c -WHERE ((c["$type"] = "OrderDetail") AND ((c["OrderID"] = null) OR (c["ProductID"] = null))) +WHERE ((c["$type"] = "OrderDetail") AND false) """); });