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)
""");
});