Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
dcf5416
Cosmos db provider: Implement owned type null comparison
JoasE Dec 8, 2025
fe40eac
Cleanup
JoasE Dec 8, 2025
b3af934
Regenerate NorthwindMiscellaneousQueryCosmosTest and optimize NOT(false)
JoasE Dec 8, 2025
27fdc25
Small cleanup
JoasE Dec 8, 2025
a2dba87
Merge branch 'main' of https://github.com/dotnet/efcore into fix/#24087
JoasE Jan 29, 2026
c294e3b
Move to IS_NULL OR NOT IS_DEFINED and add test cases that would fail
JoasE Jan 29, 2026
0583a45
Revert "Move to IS_NULL OR NOT IS_DEFINED and add test cases that wou…
JoasE Jan 31, 2026
9bc1e29
Fix typo and code style
JoasE Jan 31, 2026
2bcb90f
Update styling and small changes
JoasE Jan 31, 2026
269770b
Use old code pattern
JoasE Feb 2, 2026
d58c1f2
(ab)use ScalarReferenceExpression
JoasE Feb 2, 2026
f6f33da
Improve not false detenction
JoasE Feb 2, 2026
ec6f678
Only use IsNullable
JoasE Feb 2, 2026
fd0f1aa
Apply suggestions from code review
JoasE Feb 2, 2026
069cd7c
That's what you get for trusting copilot
JoasE Feb 2, 2026
f4b9bd8
Make SqlBinaryExpression constructible over non-Sql expressions
roji Feb 2, 2026
f820acb
Merge branch 'main' of https://github.com/dotnet/efcore into fix/#24087
JoasE Feb 3, 2026
2a9d34a
Readd !false optimization
JoasE Feb 3, 2026
9bf09f3
Assign type mapping
JoasE Feb 3, 2026
a47c832
Move to pattern matching in switch and handle !true
JoasE Feb 3, 2026
0e00b4c
Move back to unaryExpression.NodeType switch
JoasE Feb 3, 2026
132e25b
Merge branch 'main' of https://github.com/dotnet/efcore into fix/#24087
JoasE Feb 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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) =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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!),

Expand Down Expand Up @@ -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));
Comment thread
JoasE marked this conversation as resolved.
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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ public class SqlBinaryExpression : SqlExpression
/// </summary>
public SqlBinaryExpression(
ExpressionType operatorType,
SqlExpression left,
SqlExpression right,
Expression left,
Expression right,
Type type,
CoreTypeMapping? typeMapping)
: base(type, typeMapping)
Expand Down Expand Up @@ -54,15 +54,15 @@ 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.
/// </summary>
public virtual SqlExpression Left { get; }
public virtual Expression Left { get; }

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// 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.
/// </summary>
public virtual SqlExpression Right { get; }
public virtual Expression Right { get; }

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand All @@ -72,8 +72,8 @@ public SqlBinaryExpression(
/// </summary>
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);
}
Expand All @@ -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.
/// </summary>
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;
Expand Down Expand Up @@ -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;
}

Expand Down
4 changes: 2 additions & 2 deletions src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,19 @@ FROM root c
""");
}

[ConditionalFact]
public async Task Where_first_inline_not_null()
{
await AssertQuery(ss => ss.Set<RootEntity>().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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,32 @@ WHERE false
""");
}

public override Task Associate_with_inline_null()
=> Assert.ThrowsAsync<InvalidOperationException>(() => 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)
""");
Comment thread
JoasE marked this conversation as resolved.
}
Comment thread
JoasE marked this conversation as resolved.

public override Task Associate_with_parameter_null()
=> Assert.ThrowsAsync<InvalidOperationException>(() => base.Associate_with_parameter_null());

public override Task Nested_associate_with_inline_null()
=> Assert.ThrowsAsync<InvalidOperationException>(() => 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)
""");
}
Comment thread
JoasE marked this conversation as resolved.

public override async Task Nested_associate_with_inline()
{
Expand Down Expand Up @@ -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<RootEntity>().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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
""");
});

Expand All @@ -181,7 +181,6 @@ public override Task Entity_equality_not_null(bool async)
"""
SELECT VALUE c["id"]
FROM root c
WHERE (c["id"] != null)
""");
});

Expand Down Expand Up @@ -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"]
""");
});
Expand Down Expand Up @@ -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
""");
});

Expand Down Expand Up @@ -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
""");
});

Expand Down Expand Up @@ -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")
""");
});

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
""");
});

Expand Down
Loading