From dcf5416622dc7d813ff774486747f6d1e3cdcdfd Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Mon, 8 Dec 2025 15:07:59 +0100 Subject: [PATCH 01/19] Cosmos db provider: Implement owned type null comparison Comparison used to compare by checking on primary key properties, but this doesn't work for cosmos as it will filter out any document that doesn't contain a property used in a query condition. Compare by c["Prop"] = null instead Fixes: #24087 --- .../CosmosSqlTranslatingExpressionVisitor.cs | 93 +++++++++---------- .../Expressions/SqlObjectAccessExpression.cs | 90 ++++++++++++++++++ ...NavigationsStructuralEqualityCosmosTest.cs | 26 +++++- 3 files changed, 156 insertions(+), 53 deletions(-) create mode 100644 src/EFCore.Cosmos/Query/Internal/Expressions/SqlObjectAccessExpression.cs diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs index f963e03ee06..bb3aa371a86 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs @@ -4,6 +4,9 @@ using System.Collections; using System.Diagnostics.CodeAnalysis; using Microsoft.EntityFrameworkCore.Cosmos.Internal; +using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Cosmos.Query.Internal.Expressions; +using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; using Microsoft.EntityFrameworkCore.Internal; using static Microsoft.EntityFrameworkCore.Infrastructure.ExpressionExtensions; @@ -26,6 +29,9 @@ public class CosmosSqlTranslatingExpressionVisitor( { private const string RuntimeParameterPrefix = "entity_equality_"; + private static readonly MethodInfo ParameterPropertyValueExtractorMethod = + typeof(CosmosSqlTranslatingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(ParameterPropertyValueExtractor))!; + private static readonly MethodInfo ParameterValueExtractorMethod = typeof(CosmosSqlTranslatingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(ParameterValueExtractor))!; @@ -1065,69 +1071,52 @@ private bool TryRewriteEntityEquality( bool equalsMethod, [NotNullWhen(true)] out Expression? result) { - var leftEntityReference = left as EntityReferenceExpression; - var rightEntityReference = right as EntityReferenceExpression; - - if (leftEntityReference == null - && rightEntityReference == null) + var entityReference = left as EntityReferenceExpression ?? right as EntityReferenceExpression; + if (entityReference == null) { result = null; return false; } - if (IsNullSqlConstantExpression(left) - || IsNullSqlConstantExpression(right)) + var entityType = entityReference.EntityType; + var compareReference = entityReference == left ? right : left; + + // Null equality + if (IsNullSqlConstantExpression(compareReference)) { - var nonNullEntityReference = (IsNullSqlConstantExpression(left) ? rightEntityReference : leftEntityReference)!; - var entityType1 = nonNullEntityReference.EntityType; - var primaryKeyProperties1 = entityType1.FindPrimaryKey()?.Properties; - if (primaryKeyProperties1 == null) + if (entityType.IsDocumentRoot() && entityReference.Subquery == null) { - throw new InvalidOperationException( - CoreStrings.EntityEqualityOnKeylessEntityNotSupported( - nodeType == ExpressionType.Equal - ? equalsMethod ? nameof(object.Equals) : "==" - : equalsMethod - ? "!" + nameof(object.Equals) - : "!=", - entityType1.DisplayName())); + // Document root can never be be null + 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))); - + // Treat type as object for null comparison + var access = new SqlObjectAccessExpression((Expression?)entityReference.Subquery ?? entityReference.Parameter ?? throw new UnreachableException()); + result = sqlExpressionFactory.MakeBinary(nodeType, access, sqlExpressionFactory.Constant(null, typeof(object))!, typeMappingSource.FindMapping(typeof(bool)))!; return true; } - var leftEntityType = leftEntityReference?.EntityType; - var rightEntityType = rightEntityReference?.EntityType; - var entityType = leftEntityType ?? rightEntityType; - - Check.DebugAssert(entityType != null, "At least either side should be entityReference so entityType should be non-null."); - - if (leftEntityType != null - && rightEntityType != null - && leftEntityType.GetRootType() != rightEntityType.GetRootType()) + if (entityType.FindPrimaryKey()?.Properties is not { } primaryKeyProperties) { - result = sqlExpressionFactory.Constant(false); - return true; + throw new InvalidOperationException( + CoreStrings.EntityEqualityOnKeylessEntityNotSupported( + nodeType == ExpressionType.Equal + ? equalsMethod ? nameof(object.Equals) : "==" + : equalsMethod + ? "!" + nameof(object.Equals) + : "!=", + entityType.DisplayName())); } - var primaryKeyProperties = entityType.FindPrimaryKey()?.Properties; - if (primaryKeyProperties == null) + if (compareReference is EntityReferenceExpression compareEntityReference) { - throw new InvalidOperationException( - CoreStrings.EntityEqualityOnKeylessEntityNotSupported( - nodeType == ExpressionType.Equal - ? equalsMethod ? nameof(object.Equals) : "==" - : equalsMethod - ? "!" + nameof(object.Equals) - : "!=", - entityType.DisplayName())); + // Comparing of 2 different entity types is always false. + if (entityType.GetRootType() != compareEntityReference.EntityType.GetRootType()) + { + result = Visit(Expression.Constant(false)); + return true; + } } result = Visit( @@ -1154,7 +1143,7 @@ private Expression CreatePropertyAccessExpression(Expression target, IProperty p case SqlParameterExpression sqlParameterExpression: var lambda = Expression.Lambda( Expression.Call( - ParameterValueExtractorMethod.MakeGenericMethod(property.ClrType.MakeNullable()), + ParameterPropertyValueExtractorMethod.MakeGenericMethod(property.ClrType.MakeNullable()), QueryCompilationContext.QueryContextParameter, Expression.Constant(sqlParameterExpression.Name, typeof(string)), Expression.Constant(property, typeof(IProperty))), @@ -1174,12 +1163,18 @@ when memberInitExpression.Bindings.SingleOrDefault(mb => mb.Member.Name == prope } } - private static T? ParameterValueExtractor(QueryContext context, string baseParameterName, IProperty property) + private static T? ParameterPropertyValueExtractor(QueryContext context, string baseParameterName, IProperty property) { var baseParameter = context.Parameters[baseParameterName]; return baseParameter == null ? (T?)(object?)null : (T?)property.GetGetter().GetClrValue(baseParameter); } + private static T? ParameterValueExtractor(QueryContext context, string baseParameterName) + { + var baseParameter = context.Parameters[baseParameterName]; + return (T?)baseParameter; + } + private static List? ParameterListValueExtractor( QueryContext context, string baseParameterName, diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/SqlObjectAccessExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/SqlObjectAccessExpression.cs new file mode 100644 index 00000000000..4fe2f8c54ac --- /dev/null +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/SqlObjectAccessExpression.cs @@ -0,0 +1,90 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; + +namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal.Expressions; + +/// +/// Represents an structural type object access on a CosmosJSON object +/// +/// +/// 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. +/// +[DebuggerDisplay("{Microsoft.EntityFrameworkCore.Query.ExpressionPrinter.Print(this), nq}")] +public class SqlObjectAccessExpression(Expression @object) + : SqlExpression(typeof(object), CosmosTypeMapping.Default), IAccessExpression +{ + /// + /// 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. + /// + public virtual Expression Object { get; } = @object; + + /// + /// 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. + /// + public virtual string? PropertyName => null; + + /// + /// 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. + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + => Update(visitor.Visit(Object)); + + /// + /// 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. + /// + public virtual SqlObjectAccessExpression Update(Expression @object) + => ReferenceEquals(@object, Object) + ? this + : new SqlObjectAccessExpression(@object); + + /// + /// 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. + /// + protected override void Print(ExpressionPrinter expressionPrinter) + => expressionPrinter.Visit(Object); + + /// + /// 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. + /// + public override bool Equals(object? obj) + => obj != null + && (ReferenceEquals(this, obj) + || obj is SqlObjectAccessExpression expression + && Equals(expression)); + + private bool Equals(SqlObjectAccessExpression expression) + => base.Equals(expression) + && Object.Equals(expression.Object); + + /// + /// 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. + /// + public override int GetHashCode() + => HashCode.Combine(base.GetHashCode(), Object); +} diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualityCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualityCosmosTest.cs index b19b56da69b..bed53ec3287 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() { From fe40eac96282f9910abbdc79e1957fb1bd667549 Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Mon, 8 Dec 2025 15:10:15 +0100 Subject: [PATCH 02/19] Cleanup --- .../Internal/CosmosSqlTranslatingExpressionVisitor.cs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs index bb3aa371a86..ed39801f8dd 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs @@ -6,7 +6,6 @@ using Microsoft.EntityFrameworkCore.Cosmos.Internal; using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; using Microsoft.EntityFrameworkCore.Cosmos.Query.Internal.Expressions; -using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; using Microsoft.EntityFrameworkCore.Internal; using static Microsoft.EntityFrameworkCore.Infrastructure.ExpressionExtensions; @@ -32,9 +31,6 @@ public class CosmosSqlTranslatingExpressionVisitor( private static readonly MethodInfo ParameterPropertyValueExtractorMethod = typeof(CosmosSqlTranslatingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(ParameterPropertyValueExtractor))!; - private static readonly MethodInfo ParameterValueExtractorMethod = - typeof(CosmosSqlTranslatingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(ParameterValueExtractor))!; - private static readonly MethodInfo ParameterListValueExtractorMethod = typeof(CosmosSqlTranslatingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(ParameterListValueExtractor))!; @@ -1169,12 +1165,6 @@ when memberInitExpression.Bindings.SingleOrDefault(mb => mb.Member.Name == prope return baseParameter == null ? (T?)(object?)null : (T?)property.GetGetter().GetClrValue(baseParameter); } - private static T? ParameterValueExtractor(QueryContext context, string baseParameterName) - { - var baseParameter = context.Parameters[baseParameterName]; - return (T?)baseParameter; - } - private static List? ParameterListValueExtractor( QueryContext context, string baseParameterName, From b3af934978dac848d94223d2c02e66fc9d114e61 Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Mon, 8 Dec 2025 18:15:24 +0100 Subject: [PATCH 03/19] Regenerate NorthwindMiscellaneousQueryCosmosTest and optimize NOT(false) --- ...yableMethodTranslatingExpressionVisitor.cs | 2 +- .../NorthwindMiscellaneousQueryCosmosTest.cs | 20 +++++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs index 3f826412a13..a23361e96d3 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs @@ -1521,7 +1521,7 @@ private bool TryApplyPredicate(ShapedQueryExpression source, LambdaExpression pr if (TranslateLambdaExpression(source, predicate) is { } translation) { - if (translation is not SqlConstantExpression { Value: true }) + if (translation is not SqlConstantExpression { Value: true } && translation is not SqlUnaryExpression { OperatorType: ExpressionType.Not, Operand: SqlConstantExpression { Value: false } }) { select.ApplyPredicate(translation); } diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs index 9f732656d56..07f8a382dd7 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) """); }); @@ -2883,7 +2882,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"] """); }); @@ -2929,7 +2928,7 @@ public override Task Comparing_collection_navigation_to_null(bool async) """ SELECT VALUE c["id"] FROM root c -WHERE (c["id"] = null) +WHERE false """); }); @@ -4004,7 +4003,7 @@ public override Task Entity_equality_through_include(bool async) """ SELECT VALUE c["id"] FROM root c -WHERE (c["id"] = null) +WHERE false """); }); @@ -4113,7 +4112,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") """); }); @@ -4183,7 +4182,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) @@ -4276,7 +4280,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) """); }); From 27fdc25dd55d965d69ad18961fea6206c8d3523a Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Mon, 8 Dec 2025 18:22:07 +0100 Subject: [PATCH 04/19] Small cleanup --- .../Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs index ed39801f8dd..41880efc125 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs @@ -1088,7 +1088,7 @@ private bool TryRewriteEntityEquality( } // Treat type as object for null comparison - var access = new SqlObjectAccessExpression((Expression?)entityReference.Subquery ?? entityReference.Parameter ?? throw new UnreachableException()); + var access = new SqlObjectAccessExpression(entityReference.Object); result = sqlExpressionFactory.MakeBinary(nodeType, access, sqlExpressionFactory.Constant(null, typeof(object))!, typeMappingSource.FindMapping(typeof(bool)))!; return true; } @@ -1247,6 +1247,7 @@ private EntityReferenceExpression(EntityReferenceExpression typeReference, IType EntityType = (IEntityType)structuralType; } + public Expression Object => (Expression?)Parameter ?? Subquery ?? throw new UnreachableException(); public new StructuralTypeShaperExpression? Parameter { get; } public ShapedQueryExpression? Subquery { get; } public IEntityType EntityType { get; } From c294e3bfabec061c3b789a03757e704d1e6a5e38 Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Thu, 29 Jan 2026 11:15:45 +0100 Subject: [PATCH 05/19] Move to IS_NULL OR NOT IS_DEFINED and add test cases that would fail --- .../CosmosSqlTranslatingExpressionVisitor.cs | 15 +++- .../Expressions/SqlObjectAccessExpression.cs | 90 ------------------- ...NavigationsStructuralEqualityCosmosTest.cs | 28 +++++- .../AssociationsStructuralEqualityTestBase.cs | 8 ++ ...plexJsonStructuralEqualitySqlServerTest.cs | 26 ++++++ ...plittingStructuralEqualitySqlServerTest.cs | 26 ++++++ ...igationsStructuralEqualitySqlServerTest.cs | 57 ++++++++++++ ...wnedJsonStructuralEqualitySqlServerTest.cs | 25 ++++++ ...igationsStructuralEqualitySqlServerTest.cs | 57 ++++++++++++ ...plittingStructuralEqualitySqlServerTest.cs | 45 ++++++++++ .../OwnedJsonStructuralEqualitySqliteTest.cs | 24 +++++ ...NavigationsStructuralEqualitySqliteTest.cs | 56 ++++++++++++ ...leSplittingStructuralEqualitySqliteTest.cs | 44 +++++++++ 13 files changed, 405 insertions(+), 96 deletions(-) delete mode 100644 src/EFCore.Cosmos/Query/Internal/Expressions/SqlObjectAccessExpression.cs diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs index 41880efc125..a6c23afd151 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs @@ -5,7 +5,6 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.EntityFrameworkCore.Cosmos.Internal; using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; -using Microsoft.EntityFrameworkCore.Cosmos.Query.Internal.Expressions; using Microsoft.EntityFrameworkCore.Internal; using static Microsoft.EntityFrameworkCore.Infrastructure.ExpressionExtensions; @@ -1087,9 +1086,17 @@ private bool TryRewriteEntityEquality( return true; } - // Treat type as object for null comparison - var access = new SqlObjectAccessExpression(entityReference.Object); - result = sqlExpressionFactory.MakeBinary(nodeType, access, sqlExpressionFactory.Constant(null, typeof(object))!, typeMappingSource.FindMapping(typeof(bool)))!; + var isNull = sqlExpressionFactory.Function("IS_NULL", [entityReference.Object], typeof(bool)); + var isDefined = sqlExpressionFactory.Function("IS_DEFINED", [entityReference.Object], typeof(bool)); + var notDefined = sqlExpressionFactory.Not(isDefined); + var check = sqlExpressionFactory.MakeBinary(ExpressionType.OrElse, isNull, notDefined, typeMappingSource.FindMapping(typeof(bool))) ?? throw new UnreachableException(); + + if (nodeType == ExpressionType.NotEqual) + { + check = sqlExpressionFactory.Not(check); + } + + result = check; return true; } diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/SqlObjectAccessExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/SqlObjectAccessExpression.cs deleted file mode 100644 index 4fe2f8c54ac..00000000000 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/SqlObjectAccessExpression.cs +++ /dev/null @@ -1,90 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; - -namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal.Expressions; - -/// -/// Represents an structural type object access on a CosmosJSON object -/// -/// -/// 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. -/// -[DebuggerDisplay("{Microsoft.EntityFrameworkCore.Query.ExpressionPrinter.Print(this), nq}")] -public class SqlObjectAccessExpression(Expression @object) - : SqlExpression(typeof(object), CosmosTypeMapping.Default), IAccessExpression -{ - /// - /// 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. - /// - public virtual Expression Object { get; } = @object; - - /// - /// 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. - /// - public virtual string? PropertyName => null; - - /// - /// 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. - /// - protected override Expression VisitChildren(ExpressionVisitor visitor) - => Update(visitor.Visit(Object)); - - /// - /// 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. - /// - public virtual SqlObjectAccessExpression Update(Expression @object) - => ReferenceEquals(@object, Object) - ? this - : new SqlObjectAccessExpression(@object); - - /// - /// 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. - /// - protected override void Print(ExpressionPrinter expressionPrinter) - => expressionPrinter.Visit(Object); - - /// - /// 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. - /// - public override bool Equals(object? obj) - => obj != null - && (ReferenceEquals(this, obj) - || obj is SqlObjectAccessExpression expression - && Equals(expression)); - - private bool Equals(SqlObjectAccessExpression expression) - => base.Equals(expression) - && Object.Equals(expression.Object); - - /// - /// 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. - /// - public override int GetHashCode() - => HashCode.Combine(base.GetHashCode(), Object); -} diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualityCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualityCosmosTest.cs index bed53ec3287..635f889ef1b 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualityCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualityCosmosTest.cs @@ -56,7 +56,7 @@ public override async Task Associate_with_inline_null() """ SELECT VALUE c FROM root c -WHERE (c["OptionalAssociate"] = null) +WHERE (IS_NULL(c["OptionalAssociate"]) OR NOT(IS_DEFINED(c["OptionalAssociate"]))) """); } @@ -71,7 +71,31 @@ public override async Task Nested_associate_with_inline_null() """ SELECT VALUE c FROM root c -WHERE (c["RequiredAssociate"]["OptionalNestedAssociate"] = null) +WHERE (IS_NULL(c["RequiredAssociate"]["OptionalNestedAssociate"]) OR NOT(IS_DEFINED(c["RequiredAssociate"]["OptionalNestedAssociate"]))) +"""); + } + + public override async Task Optional_associate_nested_associate_with_inline_null() + { + await base.Optional_associate_nested_associate_with_inline_null(); + + AssertSql( +""" +SELECT VALUE c +FROM root c +WHERE (IS_NULL(c["OptionalAssociate"]["OptionalNestedAssociate"]) OR NOT(IS_DEFINED(c["OptionalAssociate"]["OptionalNestedAssociate"]))) +"""); + } + + public override async Task Optional_associate_nested_associate_with_inline_not_null() + { + await base.Optional_associate_nested_associate_with_inline_not_null(); + + AssertSql( +""" +SELECT VALUE c +FROM root c +WHERE NOT((IS_NULL(c["OptionalAssociate"]["OptionalNestedAssociate"]) OR NOT(IS_DEFINED(c["OptionalAssociate"]["OptionalNestedAssociate"])))) """); } diff --git a/test/EFCore.Specification.Tests/Query/Associations/AssociationsStructuralEqualityTestBase.cs b/test/EFCore.Specification.Tests/Query/Associations/AssociationsStructuralEqualityTestBase.cs index b786d6be49b..1314043ae6c 100644 --- a/test/EFCore.Specification.Tests/Query/Associations/AssociationsStructuralEqualityTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/Associations/AssociationsStructuralEqualityTestBase.cs @@ -46,6 +46,14 @@ public virtual async Task Associate_with_parameter_null() public virtual Task Nested_associate_with_inline_null() => AssertQuery(ss => ss.Set().Where(e => e.RequiredAssociate.OptionalNestedAssociate == null)); + [ConditionalFact] + public virtual Task Optional_associate_nested_associate_with_inline_null() + => AssertQuery(ss => ss.Set().Where(e => e.OptionalAssociate!.OptionalNestedAssociate == null)); + + [ConditionalFact] + public virtual Task Optional_associate_nested_associate_with_inline_not_null() + => AssertQuery(ss => ss.Set().Where(e => e.OptionalAssociate!.OptionalNestedAssociate != null)); + [ConditionalFact] public virtual Task Nested_associate_with_inline() => AssertQuery( diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonStructuralEqualitySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonStructuralEqualitySqlServerTest.cs index af756237f77..b9f62da955d 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonStructuralEqualitySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonStructuralEqualitySqlServerTest.cs @@ -130,6 +130,32 @@ WHERE JSON_QUERY([r].[RequiredAssociate], '$.OptionalNestedAssociate') IS NULL """); } + public override async Task Optional_associate_nested_associate_with_inline_null() + { + await base.Optional_associate_nested_associate_with_inline_null(); + + + AssertSql( + """ +SELECT [r].[Id], [r].[Name], [r].[AssociateCollection], [r].[OptionalAssociate], [r].[RequiredAssociate] +FROM [RootEntity] AS [r] +WHERE JSON_QUERY([r].[OptionalAssociate], '$.OptionalNestedAssociate') IS NULL +"""); + } + + public override async Task Optional_associate_nested_associate_with_inline_not_null() + { + await base.Optional_associate_nested_associate_with_inline_not_null(); + + + AssertSql( + """ +SELECT [r].[Id], [r].[Name], [r].[AssociateCollection], [r].[OptionalAssociate], [r].[RequiredAssociate] +FROM [RootEntity] AS [r] +WHERE JSON_QUERY([r].[OptionalAssociate], '$.OptionalNestedAssociate') IS NOT NULL +"""); + } + public override async Task Nested_associate_with_inline() { await base.Nested_associate_with_inline(); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/Associations/ComplexTableSplitting/ComplexTableSplittingStructuralEqualitySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/Associations/ComplexTableSplitting/ComplexTableSplittingStructuralEqualitySqlServerTest.cs index 5a3468c7b73..32b2580d2c3 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/Associations/ComplexTableSplitting/ComplexTableSplittingStructuralEqualitySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/Associations/ComplexTableSplitting/ComplexTableSplittingStructuralEqualitySqlServerTest.cs @@ -80,6 +80,32 @@ WHERE [r].[RequiredAssociate_OptionalNestedAssociate_Id] IS NULL """); } + public override async Task Optional_associate_nested_associate_with_inline_null() + { + await base.Optional_associate_nested_associate_with_inline_null(); + + + AssertSql( + """ +SELECT [r].[Id], [r].[Name], [r].[OptionalAssociate_Id], [r].[OptionalAssociate_Int], [r].[OptionalAssociate_Ints], [r].[OptionalAssociate_Name], [r].[OptionalAssociate_String], [r].[OptionalAssociate_OptionalNestedAssociate_Id], [r].[OptionalAssociate_OptionalNestedAssociate_Int], [r].[OptionalAssociate_OptionalNestedAssociate_Ints], [r].[OptionalAssociate_OptionalNestedAssociate_Name], [r].[OptionalAssociate_OptionalNestedAssociate_String], [r].[OptionalAssociate_RequiredNestedAssociate_Id], [r].[OptionalAssociate_RequiredNestedAssociate_Int], [r].[OptionalAssociate_RequiredNestedAssociate_Ints], [r].[OptionalAssociate_RequiredNestedAssociate_Name], [r].[OptionalAssociate_RequiredNestedAssociate_String], [r].[RequiredAssociate_Id], [r].[RequiredAssociate_Int], [r].[RequiredAssociate_Ints], [r].[RequiredAssociate_Name], [r].[RequiredAssociate_String], [r].[RequiredAssociate_OptionalNestedAssociate_Id], [r].[RequiredAssociate_OptionalNestedAssociate_Int], [r].[RequiredAssociate_OptionalNestedAssociate_Ints], [r].[RequiredAssociate_OptionalNestedAssociate_Name], [r].[RequiredAssociate_OptionalNestedAssociate_String], [r].[RequiredAssociate_RequiredNestedAssociate_Id], [r].[RequiredAssociate_RequiredNestedAssociate_Int], [r].[RequiredAssociate_RequiredNestedAssociate_Ints], [r].[RequiredAssociate_RequiredNestedAssociate_Name], [r].[RequiredAssociate_RequiredNestedAssociate_String] +FROM [RootEntity] AS [r] +WHERE [r].[OptionalAssociate_OptionalNestedAssociate_Id] IS NULL +"""); + } + + public override async Task Optional_associate_nested_associate_with_inline_not_null() + { + await base.Optional_associate_nested_associate_with_inline_not_null(); + + + AssertSql( + """ +SELECT [r].[Id], [r].[Name], [r].[OptionalAssociate_Id], [r].[OptionalAssociate_Int], [r].[OptionalAssociate_Ints], [r].[OptionalAssociate_Name], [r].[OptionalAssociate_String], [r].[OptionalAssociate_OptionalNestedAssociate_Id], [r].[OptionalAssociate_OptionalNestedAssociate_Int], [r].[OptionalAssociate_OptionalNestedAssociate_Ints], [r].[OptionalAssociate_OptionalNestedAssociate_Name], [r].[OptionalAssociate_OptionalNestedAssociate_String], [r].[OptionalAssociate_RequiredNestedAssociate_Id], [r].[OptionalAssociate_RequiredNestedAssociate_Int], [r].[OptionalAssociate_RequiredNestedAssociate_Ints], [r].[OptionalAssociate_RequiredNestedAssociate_Name], [r].[OptionalAssociate_RequiredNestedAssociate_String], [r].[RequiredAssociate_Id], [r].[RequiredAssociate_Int], [r].[RequiredAssociate_Ints], [r].[RequiredAssociate_Name], [r].[RequiredAssociate_String], [r].[RequiredAssociate_OptionalNestedAssociate_Id], [r].[RequiredAssociate_OptionalNestedAssociate_Int], [r].[RequiredAssociate_OptionalNestedAssociate_Ints], [r].[RequiredAssociate_OptionalNestedAssociate_Name], [r].[RequiredAssociate_OptionalNestedAssociate_String], [r].[RequiredAssociate_RequiredNestedAssociate_Id], [r].[RequiredAssociate_RequiredNestedAssociate_Int], [r].[RequiredAssociate_RequiredNestedAssociate_Ints], [r].[RequiredAssociate_RequiredNestedAssociate_Name], [r].[RequiredAssociate_RequiredNestedAssociate_String] +FROM [RootEntity] AS [r] +WHERE [r].[OptionalAssociate_OptionalNestedAssociate_Id] IS NOT NULL +"""); + } + public override async Task Nested_associate_with_inline() { await base.Nested_associate_with_inline(); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/Associations/Navigations/NavigationsStructuralEqualitySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/Associations/Navigations/NavigationsStructuralEqualitySqlServerTest.cs index 17eff5f0b9d..0e6d27af594 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/Associations/Navigations/NavigationsStructuralEqualitySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/Associations/Navigations/NavigationsStructuralEqualitySqlServerTest.cs @@ -176,6 +176,63 @@ WHERE [n].[Id] IS NULL """); } + public override async Task Optional_associate_nested_associate_with_inline_null() + { + await base.Optional_associate_nested_associate_with_inline_null(); + + AssertSql( + """ +SELECT [r].[Id], [r].[Name], [r].[OptionalAssociateId], [r].[RequiredAssociateId], [a].[Id], [n].[Id], [n0].[Id], [a0].[Id], [n1].[Id], [n2].[Id], [s].[Id], [s].[CollectionRootId], [s].[Int], [s].[Ints], [s].[Name], [s].[OptionalNestedAssociateId], [s].[RequiredNestedAssociateId], [s].[String], [s].[Id0], [s].[Id1], [s].[Id2], [s].[CollectionAssociateId], [s].[Int0], [s].[Ints0], [s].[Name0], [s].[String0], [s].[CollectionAssociateId0], [s].[Int1], [s].[Ints1], [s].[Name1], [s].[String1], [s].[CollectionAssociateId1], [s].[Int2], [s].[Ints2], [s].[Name2], [s].[String2], [a].[CollectionRootId], [a].[Int], [a].[Ints], [a].[Name], [a].[OptionalNestedAssociateId], [a].[RequiredNestedAssociateId], [a].[String], [n6].[Id], [n6].[CollectionAssociateId], [n6].[Int], [n6].[Ints], [n6].[Name], [n6].[String], [n].[CollectionAssociateId], [n].[Int], [n].[Ints], [n].[Name], [n].[String], [n0].[CollectionAssociateId], [n0].[Int], [n0].[Ints], [n0].[Name], [n0].[String], [a0].[CollectionRootId], [a0].[Int], [a0].[Ints], [a0].[Name], [a0].[OptionalNestedAssociateId], [a0].[RequiredNestedAssociateId], [a0].[String], [n7].[Id], [n7].[CollectionAssociateId], [n7].[Int], [n7].[Ints], [n7].[Name], [n7].[String], [n1].[CollectionAssociateId], [n1].[Int], [n1].[Ints], [n1].[Name], [n1].[String], [n2].[CollectionAssociateId], [n2].[Int], [n2].[Ints], [n2].[Name], [n2].[String] +FROM [RootEntity] AS [r] +LEFT JOIN [AssociateType] AS [a] ON [r].[OptionalAssociateId] = [a].[Id] +LEFT JOIN [NestedAssociateType] AS [n] ON [a].[OptionalNestedAssociateId] = [n].[Id] +LEFT JOIN [NestedAssociateType] AS [n0] ON [a].[RequiredNestedAssociateId] = [n0].[Id] +INNER JOIN [AssociateType] AS [a0] ON [r].[RequiredAssociateId] = [a0].[Id] +LEFT JOIN [NestedAssociateType] AS [n1] ON [a0].[OptionalNestedAssociateId] = [n1].[Id] +INNER JOIN [NestedAssociateType] AS [n2] ON [a0].[RequiredNestedAssociateId] = [n2].[Id] +LEFT JOIN ( + SELECT [a1].[Id], [a1].[CollectionRootId], [a1].[Int], [a1].[Ints], [a1].[Name], [a1].[OptionalNestedAssociateId], [a1].[RequiredNestedAssociateId], [a1].[String], [n3].[Id] AS [Id0], [n4].[Id] AS [Id1], [n5].[Id] AS [Id2], [n5].[CollectionAssociateId], [n5].[Int] AS [Int0], [n5].[Ints] AS [Ints0], [n5].[Name] AS [Name0], [n5].[String] AS [String0], [n3].[CollectionAssociateId] AS [CollectionAssociateId0], [n3].[Int] AS [Int1], [n3].[Ints] AS [Ints1], [n3].[Name] AS [Name1], [n3].[String] AS [String1], [n4].[CollectionAssociateId] AS [CollectionAssociateId1], [n4].[Int] AS [Int2], [n4].[Ints] AS [Ints2], [n4].[Name] AS [Name2], [n4].[String] AS [String2] + FROM [AssociateType] AS [a1] + LEFT JOIN [NestedAssociateType] AS [n3] ON [a1].[OptionalNestedAssociateId] = [n3].[Id] + INNER JOIN [NestedAssociateType] AS [n4] ON [a1].[RequiredNestedAssociateId] = [n4].[Id] + LEFT JOIN [NestedAssociateType] AS [n5] ON [a1].[Id] = [n5].[CollectionAssociateId] +) AS [s] ON [r].[Id] = [s].[CollectionRootId] +LEFT JOIN [NestedAssociateType] AS [n6] ON [a].[Id] = [n6].[CollectionAssociateId] +LEFT JOIN [NestedAssociateType] AS [n7] ON [a0].[Id] = [n7].[CollectionAssociateId] +WHERE [n].[Id] IS NULL +ORDER BY [r].[Id], [a].[Id], [n].[Id], [n0].[Id], [a0].[Id], [n1].[Id], [n2].[Id], [s].[Id], [s].[Id0], [s].[Id1], [s].[Id2], [n6].[Id] +"""); + } + + public override async Task Optional_associate_nested_associate_with_inline_not_null() + { + await base.Optional_associate_nested_associate_with_inline_not_null(); + + + AssertSql( + """ +SELECT [r].[Id], [r].[Name], [r].[OptionalAssociateId], [r].[RequiredAssociateId], [a].[Id], [n].[Id], [n0].[Id], [a0].[Id], [n1].[Id], [n2].[Id], [s].[Id], [s].[CollectionRootId], [s].[Int], [s].[Ints], [s].[Name], [s].[OptionalNestedAssociateId], [s].[RequiredNestedAssociateId], [s].[String], [s].[Id0], [s].[Id1], [s].[Id2], [s].[CollectionAssociateId], [s].[Int0], [s].[Ints0], [s].[Name0], [s].[String0], [s].[CollectionAssociateId0], [s].[Int1], [s].[Ints1], [s].[Name1], [s].[String1], [s].[CollectionAssociateId1], [s].[Int2], [s].[Ints2], [s].[Name2], [s].[String2], [a].[CollectionRootId], [a].[Int], [a].[Ints], [a].[Name], [a].[OptionalNestedAssociateId], [a].[RequiredNestedAssociateId], [a].[String], [n6].[Id], [n6].[CollectionAssociateId], [n6].[Int], [n6].[Ints], [n6].[Name], [n6].[String], [n].[CollectionAssociateId], [n].[Int], [n].[Ints], [n].[Name], [n].[String], [n0].[CollectionAssociateId], [n0].[Int], [n0].[Ints], [n0].[Name], [n0].[String], [a0].[CollectionRootId], [a0].[Int], [a0].[Ints], [a0].[Name], [a0].[OptionalNestedAssociateId], [a0].[RequiredNestedAssociateId], [a0].[String], [n7].[Id], [n7].[CollectionAssociateId], [n7].[Int], [n7].[Ints], [n7].[Name], [n7].[String], [n1].[CollectionAssociateId], [n1].[Int], [n1].[Ints], [n1].[Name], [n1].[String], [n2].[CollectionAssociateId], [n2].[Int], [n2].[Ints], [n2].[Name], [n2].[String] +FROM [RootEntity] AS [r] +LEFT JOIN [AssociateType] AS [a] ON [r].[OptionalAssociateId] = [a].[Id] +LEFT JOIN [NestedAssociateType] AS [n] ON [a].[OptionalNestedAssociateId] = [n].[Id] +LEFT JOIN [NestedAssociateType] AS [n0] ON [a].[RequiredNestedAssociateId] = [n0].[Id] +INNER JOIN [AssociateType] AS [a0] ON [r].[RequiredAssociateId] = [a0].[Id] +LEFT JOIN [NestedAssociateType] AS [n1] ON [a0].[OptionalNestedAssociateId] = [n1].[Id] +INNER JOIN [NestedAssociateType] AS [n2] ON [a0].[RequiredNestedAssociateId] = [n2].[Id] +LEFT JOIN ( + SELECT [a1].[Id], [a1].[CollectionRootId], [a1].[Int], [a1].[Ints], [a1].[Name], [a1].[OptionalNestedAssociateId], [a1].[RequiredNestedAssociateId], [a1].[String], [n3].[Id] AS [Id0], [n4].[Id] AS [Id1], [n5].[Id] AS [Id2], [n5].[CollectionAssociateId], [n5].[Int] AS [Int0], [n5].[Ints] AS [Ints0], [n5].[Name] AS [Name0], [n5].[String] AS [String0], [n3].[CollectionAssociateId] AS [CollectionAssociateId0], [n3].[Int] AS [Int1], [n3].[Ints] AS [Ints1], [n3].[Name] AS [Name1], [n3].[String] AS [String1], [n4].[CollectionAssociateId] AS [CollectionAssociateId1], [n4].[Int] AS [Int2], [n4].[Ints] AS [Ints2], [n4].[Name] AS [Name2], [n4].[String] AS [String2] + FROM [AssociateType] AS [a1] + LEFT JOIN [NestedAssociateType] AS [n3] ON [a1].[OptionalNestedAssociateId] = [n3].[Id] + INNER JOIN [NestedAssociateType] AS [n4] ON [a1].[RequiredNestedAssociateId] = [n4].[Id] + LEFT JOIN [NestedAssociateType] AS [n5] ON [a1].[Id] = [n5].[CollectionAssociateId] +) AS [s] ON [r].[Id] = [s].[CollectionRootId] +LEFT JOIN [NestedAssociateType] AS [n6] ON [a].[Id] = [n6].[CollectionAssociateId] +LEFT JOIN [NestedAssociateType] AS [n7] ON [a0].[Id] = [n7].[CollectionAssociateId] +WHERE [n].[Id] IS NOT NULL +ORDER BY [r].[Id], [a].[Id], [n].[Id], [n0].[Id], [a0].[Id], [n1].[Id], [n2].[Id], [s].[Id], [s].[Id0], [s].[Id1], [s].[Id2], [n6].[Id] +"""); + } + public override async Task Nested_associate_with_inline() { await base.Nested_associate_with_inline(); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonStructuralEqualitySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonStructuralEqualitySqlServerTest.cs index 87797f8c30a..d6d96194443 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonStructuralEqualitySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonStructuralEqualitySqlServerTest.cs @@ -80,6 +80,31 @@ WHERE JSON_QUERY([r].[RequiredAssociate], '$.OptionalNestedAssociate') IS NULL """); } + public override async Task Optional_associate_nested_associate_with_inline_null() + { + await base.Optional_associate_nested_associate_with_inline_null(); + + AssertSql( + """ +SELECT [r].[Id], [r].[Name], [r].[AssociateCollection], [r].[OptionalAssociate], [r].[RequiredAssociate] +FROM [RootEntity] AS [r] +WHERE JSON_QUERY([r].[OptionalAssociate], '$.OptionalNestedAssociate') IS NULL +"""); + } + + public override async Task Optional_associate_nested_associate_with_inline_not_null() + { + await base.Optional_associate_nested_associate_with_inline_not_null(); + + + AssertSql( + """ +SELECT [r].[Id], [r].[Name], [r].[AssociateCollection], [r].[OptionalAssociate], [r].[RequiredAssociate] +FROM [RootEntity] AS [r] +WHERE JSON_QUERY([r].[OptionalAssociate], '$.OptionalNestedAssociate') IS NOT NULL +"""); + } + public override async Task Nested_associate_with_inline() { await base.Nested_associate_with_inline(); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualitySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualitySqlServerTest.cs index 02575f6ea0c..3a2983a99aa 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualitySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualitySqlServerTest.cs @@ -176,6 +176,63 @@ WHERE [r1].[AssociateTypeRootEntityId] IS NULL """); } + public override async Task Optional_associate_nested_associate_with_inline_null() + { + await base.Optional_associate_nested_associate_with_inline_null(); + + AssertSql( + """ +SELECT [r].[Id], [r].[Name], [o].[RootEntityId], [o0].[AssociateTypeRootEntityId], [o1].[AssociateTypeRootEntityId], [r0].[RootEntityId], [r1].[AssociateTypeRootEntityId], [r2].[AssociateTypeRootEntityId], [s].[RootEntityId], [s].[Id], [s].[Int], [s].[Ints], [s].[Name], [s].[String], [s].[AssociateTypeRootEntityId], [s].[AssociateTypeId], [s].[AssociateTypeRootEntityId0], [s].[AssociateTypeId0], [s].[AssociateTypeRootEntityId1], [s].[AssociateTypeId1], [s].[Id0], [s].[Int0], [s].[Ints0], [s].[Name0], [s].[String0], [s].[Id1], [s].[Int1], [s].[Ints1], [s].[Name1], [s].[String1], [s].[Id2], [s].[Int2], [s].[Ints2], [s].[Name2], [s].[String2], [o].[Id], [o].[Int], [o].[Ints], [o].[Name], [o].[String], [o2].[AssociateTypeRootEntityId], [o2].[Id], [o2].[Int], [o2].[Ints], [o2].[Name], [o2].[String], [o0].[Id], [o0].[Int], [o0].[Ints], [o0].[Name], [o0].[String], [o1].[Id], [o1].[Int], [o1].[Ints], [o1].[Name], [o1].[String], [r0].[Id], [r0].[Int], [r0].[Ints], [r0].[Name], [r0].[String], [r7].[AssociateTypeRootEntityId], [r7].[Id], [r7].[Int], [r7].[Ints], [r7].[Name], [r7].[String], [r1].[Id], [r1].[Int], [r1].[Ints], [r1].[Name], [r1].[String], [r2].[Id], [r2].[Int], [r2].[Ints], [r2].[Name], [r2].[String] +FROM [RootEntity] AS [r] +LEFT JOIN [OptionalRelated] AS [o] ON [r].[Id] = [o].[RootEntityId] +LEFT JOIN [OptionalRelated_OptionalNested] AS [o0] ON [o].[RootEntityId] = [o0].[AssociateTypeRootEntityId] +LEFT JOIN [OptionalRelated_RequiredNested] AS [o1] ON [o].[RootEntityId] = [o1].[AssociateTypeRootEntityId] +LEFT JOIN [RequiredRelated] AS [r0] ON [r].[Id] = [r0].[RootEntityId] +LEFT JOIN [RequiredRelated_OptionalNested] AS [r1] ON [r0].[RootEntityId] = [r1].[AssociateTypeRootEntityId] +LEFT JOIN [RequiredRelated_RequiredNested] AS [r2] ON [r0].[RootEntityId] = [r2].[AssociateTypeRootEntityId] +LEFT JOIN ( + SELECT [r3].[RootEntityId], [r3].[Id], [r3].[Int], [r3].[Ints], [r3].[Name], [r3].[String], [r4].[AssociateTypeRootEntityId], [r4].[AssociateTypeId], [r5].[AssociateTypeRootEntityId] AS [AssociateTypeRootEntityId0], [r5].[AssociateTypeId] AS [AssociateTypeId0], [r6].[AssociateTypeRootEntityId] AS [AssociateTypeRootEntityId1], [r6].[AssociateTypeId] AS [AssociateTypeId1], [r6].[Id] AS [Id0], [r6].[Int] AS [Int0], [r6].[Ints] AS [Ints0], [r6].[Name] AS [Name0], [r6].[String] AS [String0], [r4].[Id] AS [Id1], [r4].[Int] AS [Int1], [r4].[Ints] AS [Ints1], [r4].[Name] AS [Name1], [r4].[String] AS [String1], [r5].[Id] AS [Id2], [r5].[Int] AS [Int2], [r5].[Ints] AS [Ints2], [r5].[Name] AS [Name2], [r5].[String] AS [String2] + FROM [RelatedCollection] AS [r3] + LEFT JOIN [RelatedCollection_OptionalNested] AS [r4] ON [r3].[RootEntityId] = [r4].[AssociateTypeRootEntityId] AND [r3].[Id] = [r4].[AssociateTypeId] + LEFT JOIN [RelatedCollection_RequiredNested] AS [r5] ON [r3].[RootEntityId] = [r5].[AssociateTypeRootEntityId] AND [r3].[Id] = [r5].[AssociateTypeId] + LEFT JOIN [RelatedCollection_NestedCollection] AS [r6] ON [r3].[RootEntityId] = [r6].[AssociateTypeRootEntityId] AND [r3].[Id] = [r6].[AssociateTypeId] +) AS [s] ON [r].[Id] = [s].[RootEntityId] +LEFT JOIN [OptionalRelated_NestedCollection] AS [o2] ON [o].[RootEntityId] = [o2].[AssociateTypeRootEntityId] +LEFT JOIN [RequiredRelated_NestedCollection] AS [r7] ON [r0].[RootEntityId] = [r7].[AssociateTypeRootEntityId] +WHERE [o0].[AssociateTypeRootEntityId] IS NULL +ORDER BY [r].[Id], [o].[RootEntityId], [o0].[AssociateTypeRootEntityId], [o1].[AssociateTypeRootEntityId], [r0].[RootEntityId], [r1].[AssociateTypeRootEntityId], [r2].[AssociateTypeRootEntityId], [s].[RootEntityId], [s].[Id], [s].[AssociateTypeRootEntityId], [s].[AssociateTypeId], [s].[AssociateTypeRootEntityId0], [s].[AssociateTypeId0], [s].[AssociateTypeRootEntityId1], [s].[AssociateTypeId1], [s].[Id0], [o2].[AssociateTypeRootEntityId], [o2].[Id], [r7].[AssociateTypeRootEntityId] +"""); + } + + public override async Task Optional_associate_nested_associate_with_inline_not_null() + { + await base.Optional_associate_nested_associate_with_inline_not_null(); + + + AssertSql( + """ +SELECT [r].[Id], [r].[Name], [o].[RootEntityId], [o0].[AssociateTypeRootEntityId], [o1].[AssociateTypeRootEntityId], [r0].[RootEntityId], [r1].[AssociateTypeRootEntityId], [r2].[AssociateTypeRootEntityId], [s].[RootEntityId], [s].[Id], [s].[Int], [s].[Ints], [s].[Name], [s].[String], [s].[AssociateTypeRootEntityId], [s].[AssociateTypeId], [s].[AssociateTypeRootEntityId0], [s].[AssociateTypeId0], [s].[AssociateTypeRootEntityId1], [s].[AssociateTypeId1], [s].[Id0], [s].[Int0], [s].[Ints0], [s].[Name0], [s].[String0], [s].[Id1], [s].[Int1], [s].[Ints1], [s].[Name1], [s].[String1], [s].[Id2], [s].[Int2], [s].[Ints2], [s].[Name2], [s].[String2], [o].[Id], [o].[Int], [o].[Ints], [o].[Name], [o].[String], [o2].[AssociateTypeRootEntityId], [o2].[Id], [o2].[Int], [o2].[Ints], [o2].[Name], [o2].[String], [o0].[Id], [o0].[Int], [o0].[Ints], [o0].[Name], [o0].[String], [o1].[Id], [o1].[Int], [o1].[Ints], [o1].[Name], [o1].[String], [r0].[Id], [r0].[Int], [r0].[Ints], [r0].[Name], [r0].[String], [r7].[AssociateTypeRootEntityId], [r7].[Id], [r7].[Int], [r7].[Ints], [r7].[Name], [r7].[String], [r1].[Id], [r1].[Int], [r1].[Ints], [r1].[Name], [r1].[String], [r2].[Id], [r2].[Int], [r2].[Ints], [r2].[Name], [r2].[String] +FROM [RootEntity] AS [r] +LEFT JOIN [OptionalRelated] AS [o] ON [r].[Id] = [o].[RootEntityId] +LEFT JOIN [OptionalRelated_OptionalNested] AS [o0] ON [o].[RootEntityId] = [o0].[AssociateTypeRootEntityId] +LEFT JOIN [OptionalRelated_RequiredNested] AS [o1] ON [o].[RootEntityId] = [o1].[AssociateTypeRootEntityId] +LEFT JOIN [RequiredRelated] AS [r0] ON [r].[Id] = [r0].[RootEntityId] +LEFT JOIN [RequiredRelated_OptionalNested] AS [r1] ON [r0].[RootEntityId] = [r1].[AssociateTypeRootEntityId] +LEFT JOIN [RequiredRelated_RequiredNested] AS [r2] ON [r0].[RootEntityId] = [r2].[AssociateTypeRootEntityId] +LEFT JOIN ( + SELECT [r3].[RootEntityId], [r3].[Id], [r3].[Int], [r3].[Ints], [r3].[Name], [r3].[String], [r4].[AssociateTypeRootEntityId], [r4].[AssociateTypeId], [r5].[AssociateTypeRootEntityId] AS [AssociateTypeRootEntityId0], [r5].[AssociateTypeId] AS [AssociateTypeId0], [r6].[AssociateTypeRootEntityId] AS [AssociateTypeRootEntityId1], [r6].[AssociateTypeId] AS [AssociateTypeId1], [r6].[Id] AS [Id0], [r6].[Int] AS [Int0], [r6].[Ints] AS [Ints0], [r6].[Name] AS [Name0], [r6].[String] AS [String0], [r4].[Id] AS [Id1], [r4].[Int] AS [Int1], [r4].[Ints] AS [Ints1], [r4].[Name] AS [Name1], [r4].[String] AS [String1], [r5].[Id] AS [Id2], [r5].[Int] AS [Int2], [r5].[Ints] AS [Ints2], [r5].[Name] AS [Name2], [r5].[String] AS [String2] + FROM [RelatedCollection] AS [r3] + LEFT JOIN [RelatedCollection_OptionalNested] AS [r4] ON [r3].[RootEntityId] = [r4].[AssociateTypeRootEntityId] AND [r3].[Id] = [r4].[AssociateTypeId] + LEFT JOIN [RelatedCollection_RequiredNested] AS [r5] ON [r3].[RootEntityId] = [r5].[AssociateTypeRootEntityId] AND [r3].[Id] = [r5].[AssociateTypeId] + LEFT JOIN [RelatedCollection_NestedCollection] AS [r6] ON [r3].[RootEntityId] = [r6].[AssociateTypeRootEntityId] AND [r3].[Id] = [r6].[AssociateTypeId] +) AS [s] ON [r].[Id] = [s].[RootEntityId] +LEFT JOIN [OptionalRelated_NestedCollection] AS [o2] ON [o].[RootEntityId] = [o2].[AssociateTypeRootEntityId] +LEFT JOIN [RequiredRelated_NestedCollection] AS [r7] ON [r0].[RootEntityId] = [r7].[AssociateTypeRootEntityId] +WHERE [o0].[AssociateTypeRootEntityId] IS NOT NULL +ORDER BY [r].[Id], [o].[RootEntityId], [o0].[AssociateTypeRootEntityId], [o1].[AssociateTypeRootEntityId], [r0].[RootEntityId], [r1].[AssociateTypeRootEntityId], [r2].[AssociateTypeRootEntityId], [s].[RootEntityId], [s].[Id], [s].[AssociateTypeRootEntityId], [s].[AssociateTypeId], [s].[AssociateTypeRootEntityId0], [s].[AssociateTypeId0], [s].[AssociateTypeRootEntityId1], [s].[AssociateTypeId1], [s].[Id0], [o2].[AssociateTypeRootEntityId], [o2].[Id], [r7].[AssociateTypeRootEntityId] +"""); + } + public override async Task Nested_associate_with_inline() { await base.Nested_associate_with_inline(); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/Associations/OwnedTableSplitting/OwnedTableSplittingStructuralEqualitySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/Associations/OwnedTableSplitting/OwnedTableSplittingStructuralEqualitySqlServerTest.cs index 8f905ca3010..086897796ac 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/Associations/OwnedTableSplitting/OwnedTableSplittingStructuralEqualitySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/Associations/OwnedTableSplitting/OwnedTableSplittingStructuralEqualitySqlServerTest.cs @@ -142,6 +142,51 @@ WHERE [r].[RequiredAssociate_OptionalNestedAssociate_Id] IS NULL OR [r].[Require """); } + public override async Task Optional_associate_nested_associate_with_inline_null() + { + await base.Optional_associate_nested_associate_with_inline_null(); + + AssertSql( + """ +SELECT [r].[Id], [r].[Name], [s].[RootEntityId], [s].[Id], [s].[Int], [s].[Ints], [s].[Name], [s].[String], [s].[AssociateTypeRootEntityId], [s].[AssociateTypeId], [s].[Id0], [s].[Int0], [s].[Ints0], [s].[Name0], [s].[String0], [s].[OptionalNestedAssociate_Id], [s].[OptionalNestedAssociate_Int], [s].[OptionalNestedAssociate_Ints], [s].[OptionalNestedAssociate_Name], [s].[OptionalNestedAssociate_String], [s].[RequiredNestedAssociate_Id], [s].[RequiredNestedAssociate_Int], [s].[RequiredNestedAssociate_Ints], [s].[RequiredNestedAssociate_Name], [s].[RequiredNestedAssociate_String], [r].[OptionalAssociate_Id], [r].[OptionalAssociate_Int], [r].[OptionalAssociate_Ints], [r].[OptionalAssociate_Name], [r].[OptionalAssociate_String], [o].[AssociateTypeRootEntityId], [o].[Id], [o].[Int], [o].[Ints], [o].[Name], [o].[String], [r].[OptionalAssociate_OptionalNestedAssociate_Id], [r].[OptionalAssociate_OptionalNestedAssociate_Int], [r].[OptionalAssociate_OptionalNestedAssociate_Ints], [r].[OptionalAssociate_OptionalNestedAssociate_Name], [r].[OptionalAssociate_OptionalNestedAssociate_String], [r].[OptionalAssociate_RequiredNestedAssociate_Id], [r].[OptionalAssociate_RequiredNestedAssociate_Int], [r].[OptionalAssociate_RequiredNestedAssociate_Ints], [r].[OptionalAssociate_RequiredNestedAssociate_Name], [r].[OptionalAssociate_RequiredNestedAssociate_String], [r].[RequiredAssociate_Id], [r].[RequiredAssociate_Int], [r].[RequiredAssociate_Ints], [r].[RequiredAssociate_Name], [r].[RequiredAssociate_String], [r2].[AssociateTypeRootEntityId], [r2].[Id], [r2].[Int], [r2].[Ints], [r2].[Name], [r2].[String], [r].[RequiredAssociate_OptionalNestedAssociate_Id], [r].[RequiredAssociate_OptionalNestedAssociate_Int], [r].[RequiredAssociate_OptionalNestedAssociate_Ints], [r].[RequiredAssociate_OptionalNestedAssociate_Name], [r].[RequiredAssociate_OptionalNestedAssociate_String], [r].[RequiredAssociate_RequiredNestedAssociate_Id], [r].[RequiredAssociate_RequiredNestedAssociate_Int], [r].[RequiredAssociate_RequiredNestedAssociate_Ints], [r].[RequiredAssociate_RequiredNestedAssociate_Name], [r].[RequiredAssociate_RequiredNestedAssociate_String] +FROM [RootEntity] AS [r] +LEFT JOIN ( + SELECT [r0].[RootEntityId], [r0].[Id], [r0].[Int], [r0].[Ints], [r0].[Name], [r0].[String], [r1].[AssociateTypeRootEntityId], [r1].[AssociateTypeId], [r1].[Id] AS [Id0], [r1].[Int] AS [Int0], [r1].[Ints] AS [Ints0], [r1].[Name] AS [Name0], [r1].[String] AS [String0], [r0].[OptionalNestedAssociate_Id], [r0].[OptionalNestedAssociate_Int], [r0].[OptionalNestedAssociate_Ints], [r0].[OptionalNestedAssociate_Name], [r0].[OptionalNestedAssociate_String], [r0].[RequiredNestedAssociate_Id], [r0].[RequiredNestedAssociate_Int], [r0].[RequiredNestedAssociate_Ints], [r0].[RequiredNestedAssociate_Name], [r0].[RequiredNestedAssociate_String] + FROM [RelatedCollection] AS [r0] + LEFT JOIN [RelatedCollection_NestedCollection] AS [r1] ON [r0].[RootEntityId] = [r1].[AssociateTypeRootEntityId] AND [r0].[Id] = [r1].[AssociateTypeId] +) AS [s] ON [r].[Id] = [s].[RootEntityId] +LEFT JOIN [OptionalRelated_NestedCollection] AS [o] ON CASE + WHEN [r].[OptionalAssociate_Id] IS NOT NULL AND [r].[OptionalAssociate_Int] IS NOT NULL AND [r].[OptionalAssociate_Ints] IS NOT NULL AND [r].[OptionalAssociate_Name] IS NOT NULL AND [r].[OptionalAssociate_String] IS NOT NULL THEN [r].[Id] +END = [o].[AssociateTypeRootEntityId] +LEFT JOIN [RequiredRelated_NestedCollection] AS [r2] ON [r].[Id] = [r2].[AssociateTypeRootEntityId] +WHERE [r].[OptionalAssociate_OptionalNestedAssociate_Id] IS NULL OR [r].[OptionalAssociate_OptionalNestedAssociate_Int] IS NULL OR [r].[OptionalAssociate_OptionalNestedAssociate_Ints] IS NULL OR [r].[OptionalAssociate_OptionalNestedAssociate_Name] IS NULL OR [r].[OptionalAssociate_OptionalNestedAssociate_String] IS NULL +ORDER BY [r].[Id], [s].[RootEntityId], [s].[Id], [s].[AssociateTypeRootEntityId], [s].[AssociateTypeId], [s].[Id0], [o].[AssociateTypeRootEntityId], [o].[Id], [r2].[AssociateTypeRootEntityId] +"""); + } + + public override async Task Optional_associate_nested_associate_with_inline_not_null() + { + await base.Optional_associate_nested_associate_with_inline_not_null(); + + + AssertSql( + """ +SELECT [r].[Id], [r].[Name], [s].[RootEntityId], [s].[Id], [s].[Int], [s].[Ints], [s].[Name], [s].[String], [s].[AssociateTypeRootEntityId], [s].[AssociateTypeId], [s].[Id0], [s].[Int0], [s].[Ints0], [s].[Name0], [s].[String0], [s].[OptionalNestedAssociate_Id], [s].[OptionalNestedAssociate_Int], [s].[OptionalNestedAssociate_Ints], [s].[OptionalNestedAssociate_Name], [s].[OptionalNestedAssociate_String], [s].[RequiredNestedAssociate_Id], [s].[RequiredNestedAssociate_Int], [s].[RequiredNestedAssociate_Ints], [s].[RequiredNestedAssociate_Name], [s].[RequiredNestedAssociate_String], [r].[OptionalAssociate_Id], [r].[OptionalAssociate_Int], [r].[OptionalAssociate_Ints], [r].[OptionalAssociate_Name], [r].[OptionalAssociate_String], [o].[AssociateTypeRootEntityId], [o].[Id], [o].[Int], [o].[Ints], [o].[Name], [o].[String], [r].[OptionalAssociate_OptionalNestedAssociate_Id], [r].[OptionalAssociate_OptionalNestedAssociate_Int], [r].[OptionalAssociate_OptionalNestedAssociate_Ints], [r].[OptionalAssociate_OptionalNestedAssociate_Name], [r].[OptionalAssociate_OptionalNestedAssociate_String], [r].[OptionalAssociate_RequiredNestedAssociate_Id], [r].[OptionalAssociate_RequiredNestedAssociate_Int], [r].[OptionalAssociate_RequiredNestedAssociate_Ints], [r].[OptionalAssociate_RequiredNestedAssociate_Name], [r].[OptionalAssociate_RequiredNestedAssociate_String], [r].[RequiredAssociate_Id], [r].[RequiredAssociate_Int], [r].[RequiredAssociate_Ints], [r].[RequiredAssociate_Name], [r].[RequiredAssociate_String], [r2].[AssociateTypeRootEntityId], [r2].[Id], [r2].[Int], [r2].[Ints], [r2].[Name], [r2].[String], [r].[RequiredAssociate_OptionalNestedAssociate_Id], [r].[RequiredAssociate_OptionalNestedAssociate_Int], [r].[RequiredAssociate_OptionalNestedAssociate_Ints], [r].[RequiredAssociate_OptionalNestedAssociate_Name], [r].[RequiredAssociate_OptionalNestedAssociate_String], [r].[RequiredAssociate_RequiredNestedAssociate_Id], [r].[RequiredAssociate_RequiredNestedAssociate_Int], [r].[RequiredAssociate_RequiredNestedAssociate_Ints], [r].[RequiredAssociate_RequiredNestedAssociate_Name], [r].[RequiredAssociate_RequiredNestedAssociate_String] +FROM [RootEntity] AS [r] +LEFT JOIN ( + SELECT [r0].[RootEntityId], [r0].[Id], [r0].[Int], [r0].[Ints], [r0].[Name], [r0].[String], [r1].[AssociateTypeRootEntityId], [r1].[AssociateTypeId], [r1].[Id] AS [Id0], [r1].[Int] AS [Int0], [r1].[Ints] AS [Ints0], [r1].[Name] AS [Name0], [r1].[String] AS [String0], [r0].[OptionalNestedAssociate_Id], [r0].[OptionalNestedAssociate_Int], [r0].[OptionalNestedAssociate_Ints], [r0].[OptionalNestedAssociate_Name], [r0].[OptionalNestedAssociate_String], [r0].[RequiredNestedAssociate_Id], [r0].[RequiredNestedAssociate_Int], [r0].[RequiredNestedAssociate_Ints], [r0].[RequiredNestedAssociate_Name], [r0].[RequiredNestedAssociate_String] + FROM [RelatedCollection] AS [r0] + LEFT JOIN [RelatedCollection_NestedCollection] AS [r1] ON [r0].[RootEntityId] = [r1].[AssociateTypeRootEntityId] AND [r0].[Id] = [r1].[AssociateTypeId] +) AS [s] ON [r].[Id] = [s].[RootEntityId] +LEFT JOIN [OptionalRelated_NestedCollection] AS [o] ON CASE + WHEN [r].[OptionalAssociate_Id] IS NOT NULL AND [r].[OptionalAssociate_Int] IS NOT NULL AND [r].[OptionalAssociate_Ints] IS NOT NULL AND [r].[OptionalAssociate_Name] IS NOT NULL AND [r].[OptionalAssociate_String] IS NOT NULL THEN [r].[Id] +END = [o].[AssociateTypeRootEntityId] +LEFT JOIN [RequiredRelated_NestedCollection] AS [r2] ON [r].[Id] = [r2].[AssociateTypeRootEntityId] +WHERE [r].[OptionalAssociate_OptionalNestedAssociate_Id] IS NOT NULL AND [r].[OptionalAssociate_OptionalNestedAssociate_Int] IS NOT NULL AND [r].[OptionalAssociate_OptionalNestedAssociate_Ints] IS NOT NULL AND [r].[OptionalAssociate_OptionalNestedAssociate_Name] IS NOT NULL AND [r].[OptionalAssociate_OptionalNestedAssociate_String] IS NOT NULL +ORDER BY [r].[Id], [s].[RootEntityId], [s].[Id], [s].[AssociateTypeRootEntityId], [s].[AssociateTypeId], [s].[Id0], [o].[AssociateTypeRootEntityId], [o].[Id], [r2].[AssociateTypeRootEntityId] +"""); + } + public override async Task Nested_associate_with_inline() { await base.Nested_associate_with_inline(); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonStructuralEqualitySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonStructuralEqualitySqliteTest.cs index b15b2431313..71dac1d0881 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonStructuralEqualitySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonStructuralEqualitySqliteTest.cs @@ -80,6 +80,30 @@ public override async Task Nested_associate_with_inline_null() """); } + public override async Task Optional_associate_nested_associate_with_inline_null() + { + await base.Optional_associate_nested_associate_with_inline_null(); + + AssertSql( + """ +SELECT "r"."Id", "r"."Name", "r"."AssociateCollection", "r"."OptionalAssociate", "r"."RequiredAssociate" +FROM "RootEntity" AS "r" +WHERE "r"."OptionalAssociate" ->> 'OptionalNestedAssociate' IS NULL +"""); + } + + public override async Task Optional_associate_nested_associate_with_inline_not_null() + { + await base.Optional_associate_nested_associate_with_inline_not_null(); + + AssertSql( + """ +SELECT "r"."Id", "r"."Name", "r"."AssociateCollection", "r"."OptionalAssociate", "r"."RequiredAssociate" +FROM "RootEntity" AS "r" +WHERE "r"."OptionalAssociate" ->> 'OptionalNestedAssociate' IS NOT NULL +"""); + } + public override async Task Nested_associate_with_inline() { await base.Nested_associate_with_inline(); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualitySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualitySqliteTest.cs index 049a49801d2..20158b4c4d1 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualitySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualitySqliteTest.cs @@ -176,6 +176,62 @@ LEFT JOIN ( """); } + public override async Task Optional_associate_nested_associate_with_inline_null() + { + await base.Optional_associate_nested_associate_with_inline_null(); + + AssertSql( + """ +SELECT "r"."Id", "r"."Name", "o"."RootEntityId", "o0"."AssociateTypeRootEntityId", "o1"."AssociateTypeRootEntityId", "r0"."RootEntityId", "r1"."AssociateTypeRootEntityId", "r2"."AssociateTypeRootEntityId", "s"."RootEntityId", "s"."Id", "s"."Int", "s"."Ints", "s"."Name", "s"."String", "s"."AssociateTypeRootEntityId", "s"."AssociateTypeId", "s"."AssociateTypeRootEntityId0", "s"."AssociateTypeId0", "s"."AssociateTypeRootEntityId1", "s"."AssociateTypeId1", "s"."Id0", "s"."Int0", "s"."Ints0", "s"."Name0", "s"."String0", "s"."Id1", "s"."Int1", "s"."Ints1", "s"."Name1", "s"."String1", "s"."Id2", "s"."Int2", "s"."Ints2", "s"."Name2", "s"."String2", "o"."Id", "o"."Int", "o"."Ints", "o"."Name", "o"."String", "o2"."AssociateTypeRootEntityId", "o2"."Id", "o2"."Int", "o2"."Ints", "o2"."Name", "o2"."String", "o0"."Id", "o0"."Int", "o0"."Ints", "o0"."Name", "o0"."String", "o1"."Id", "o1"."Int", "o1"."Ints", "o1"."Name", "o1"."String", "r0"."Id", "r0"."Int", "r0"."Ints", "r0"."Name", "r0"."String", "r7"."AssociateTypeRootEntityId", "r7"."Id", "r7"."Int", "r7"."Ints", "r7"."Name", "r7"."String", "r1"."Id", "r1"."Int", "r1"."Ints", "r1"."Name", "r1"."String", "r2"."Id", "r2"."Int", "r2"."Ints", "r2"."Name", "r2"."String" +FROM "RootEntity" AS "r" +LEFT JOIN "OptionalRelated" AS "o" ON "r"."Id" = "o"."RootEntityId" +LEFT JOIN "OptionalRelated_OptionalNested" AS "o0" ON "o"."RootEntityId" = "o0"."AssociateTypeRootEntityId" +LEFT JOIN "OptionalRelated_RequiredNested" AS "o1" ON "o"."RootEntityId" = "o1"."AssociateTypeRootEntityId" +LEFT JOIN "RequiredRelated" AS "r0" ON "r"."Id" = "r0"."RootEntityId" +LEFT JOIN "RequiredRelated_OptionalNested" AS "r1" ON "r0"."RootEntityId" = "r1"."AssociateTypeRootEntityId" +LEFT JOIN "RequiredRelated_RequiredNested" AS "r2" ON "r0"."RootEntityId" = "r2"."AssociateTypeRootEntityId" +LEFT JOIN ( + SELECT "r3"."RootEntityId", "r3"."Id", "r3"."Int", "r3"."Ints", "r3"."Name", "r3"."String", "r4"."AssociateTypeRootEntityId", "r4"."AssociateTypeId", "r5"."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId0", "r5"."AssociateTypeId" AS "AssociateTypeId0", "r6"."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId1", "r6"."AssociateTypeId" AS "AssociateTypeId1", "r6"."Id" AS "Id0", "r6"."Int" AS "Int0", "r6"."Ints" AS "Ints0", "r6"."Name" AS "Name0", "r6"."String" AS "String0", "r4"."Id" AS "Id1", "r4"."Int" AS "Int1", "r4"."Ints" AS "Ints1", "r4"."Name" AS "Name1", "r4"."String" AS "String1", "r5"."Id" AS "Id2", "r5"."Int" AS "Int2", "r5"."Ints" AS "Ints2", "r5"."Name" AS "Name2", "r5"."String" AS "String2" + FROM "RelatedCollection" AS "r3" + LEFT JOIN "RelatedCollection_OptionalNested" AS "r4" ON "r3"."RootEntityId" = "r4"."AssociateTypeRootEntityId" AND "r3"."Id" = "r4"."AssociateTypeId" + LEFT JOIN "RelatedCollection_RequiredNested" AS "r5" ON "r3"."RootEntityId" = "r5"."AssociateTypeRootEntityId" AND "r3"."Id" = "r5"."AssociateTypeId" + LEFT JOIN "RelatedCollection_NestedCollection" AS "r6" ON "r3"."RootEntityId" = "r6"."AssociateTypeRootEntityId" AND "r3"."Id" = "r6"."AssociateTypeId" +) AS "s" ON "r"."Id" = "s"."RootEntityId" +LEFT JOIN "OptionalRelated_NestedCollection" AS "o2" ON "o"."RootEntityId" = "o2"."AssociateTypeRootEntityId" +LEFT JOIN "RequiredRelated_NestedCollection" AS "r7" ON "r0"."RootEntityId" = "r7"."AssociateTypeRootEntityId" +WHERE "o0"."AssociateTypeRootEntityId" IS NULL +ORDER BY "r"."Id", "o"."RootEntityId", "o0"."AssociateTypeRootEntityId", "o1"."AssociateTypeRootEntityId", "r0"."RootEntityId", "r1"."AssociateTypeRootEntityId", "r2"."AssociateTypeRootEntityId", "s"."RootEntityId", "s"."Id", "s"."AssociateTypeRootEntityId", "s"."AssociateTypeId", "s"."AssociateTypeRootEntityId0", "s"."AssociateTypeId0", "s"."AssociateTypeRootEntityId1", "s"."AssociateTypeId1", "s"."Id0", "o2"."AssociateTypeRootEntityId", "o2"."Id", "r7"."AssociateTypeRootEntityId" +"""); + } + + public override async Task Optional_associate_nested_associate_with_inline_not_null() + { + await base.Optional_associate_nested_associate_with_inline_not_null(); + + AssertSql( + """ +SELECT "r"."Id", "r"."Name", "o"."RootEntityId", "o0"."AssociateTypeRootEntityId", "o1"."AssociateTypeRootEntityId", "r0"."RootEntityId", "r1"."AssociateTypeRootEntityId", "r2"."AssociateTypeRootEntityId", "s"."RootEntityId", "s"."Id", "s"."Int", "s"."Ints", "s"."Name", "s"."String", "s"."AssociateTypeRootEntityId", "s"."AssociateTypeId", "s"."AssociateTypeRootEntityId0", "s"."AssociateTypeId0", "s"."AssociateTypeRootEntityId1", "s"."AssociateTypeId1", "s"."Id0", "s"."Int0", "s"."Ints0", "s"."Name0", "s"."String0", "s"."Id1", "s"."Int1", "s"."Ints1", "s"."Name1", "s"."String1", "s"."Id2", "s"."Int2", "s"."Ints2", "s"."Name2", "s"."String2", "o"."Id", "o"."Int", "o"."Ints", "o"."Name", "o"."String", "o2"."AssociateTypeRootEntityId", "o2"."Id", "o2"."Int", "o2"."Ints", "o2"."Name", "o2"."String", "o0"."Id", "o0"."Int", "o0"."Ints", "o0"."Name", "o0"."String", "o1"."Id", "o1"."Int", "o1"."Ints", "o1"."Name", "o1"."String", "r0"."Id", "r0"."Int", "r0"."Ints", "r0"."Name", "r0"."String", "r7"."AssociateTypeRootEntityId", "r7"."Id", "r7"."Int", "r7"."Ints", "r7"."Name", "r7"."String", "r1"."Id", "r1"."Int", "r1"."Ints", "r1"."Name", "r1"."String", "r2"."Id", "r2"."Int", "r2"."Ints", "r2"."Name", "r2"."String" +FROM "RootEntity" AS "r" +LEFT JOIN "OptionalRelated" AS "o" ON "r"."Id" = "o"."RootEntityId" +LEFT JOIN "OptionalRelated_OptionalNested" AS "o0" ON "o"."RootEntityId" = "o0"."AssociateTypeRootEntityId" +LEFT JOIN "OptionalRelated_RequiredNested" AS "o1" ON "o"."RootEntityId" = "o1"."AssociateTypeRootEntityId" +LEFT JOIN "RequiredRelated" AS "r0" ON "r"."Id" = "r0"."RootEntityId" +LEFT JOIN "RequiredRelated_OptionalNested" AS "r1" ON "r0"."RootEntityId" = "r1"."AssociateTypeRootEntityId" +LEFT JOIN "RequiredRelated_RequiredNested" AS "r2" ON "r0"."RootEntityId" = "r2"."AssociateTypeRootEntityId" +LEFT JOIN ( + SELECT "r3"."RootEntityId", "r3"."Id", "r3"."Int", "r3"."Ints", "r3"."Name", "r3"."String", "r4"."AssociateTypeRootEntityId", "r4"."AssociateTypeId", "r5"."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId0", "r5"."AssociateTypeId" AS "AssociateTypeId0", "r6"."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId1", "r6"."AssociateTypeId" AS "AssociateTypeId1", "r6"."Id" AS "Id0", "r6"."Int" AS "Int0", "r6"."Ints" AS "Ints0", "r6"."Name" AS "Name0", "r6"."String" AS "String0", "r4"."Id" AS "Id1", "r4"."Int" AS "Int1", "r4"."Ints" AS "Ints1", "r4"."Name" AS "Name1", "r4"."String" AS "String1", "r5"."Id" AS "Id2", "r5"."Int" AS "Int2", "r5"."Ints" AS "Ints2", "r5"."Name" AS "Name2", "r5"."String" AS "String2" + FROM "RelatedCollection" AS "r3" + LEFT JOIN "RelatedCollection_OptionalNested" AS "r4" ON "r3"."RootEntityId" = "r4"."AssociateTypeRootEntityId" AND "r3"."Id" = "r4"."AssociateTypeId" + LEFT JOIN "RelatedCollection_RequiredNested" AS "r5" ON "r3"."RootEntityId" = "r5"."AssociateTypeRootEntityId" AND "r3"."Id" = "r5"."AssociateTypeId" + LEFT JOIN "RelatedCollection_NestedCollection" AS "r6" ON "r3"."RootEntityId" = "r6"."AssociateTypeRootEntityId" AND "r3"."Id" = "r6"."AssociateTypeId" +) AS "s" ON "r"."Id" = "s"."RootEntityId" +LEFT JOIN "OptionalRelated_NestedCollection" AS "o2" ON "o"."RootEntityId" = "o2"."AssociateTypeRootEntityId" +LEFT JOIN "RequiredRelated_NestedCollection" AS "r7" ON "r0"."RootEntityId" = "r7"."AssociateTypeRootEntityId" +WHERE "o0"."AssociateTypeRootEntityId" IS NOT NULL +ORDER BY "r"."Id", "o"."RootEntityId", "o0"."AssociateTypeRootEntityId", "o1"."AssociateTypeRootEntityId", "r0"."RootEntityId", "r1"."AssociateTypeRootEntityId", "r2"."AssociateTypeRootEntityId", "s"."RootEntityId", "s"."Id", "s"."AssociateTypeRootEntityId", "s"."AssociateTypeId", "s"."AssociateTypeRootEntityId0", "s"."AssociateTypeId0", "s"."AssociateTypeRootEntityId1", "s"."AssociateTypeId1", "s"."Id0", "o2"."AssociateTypeRootEntityId", "o2"."Id", "r7"."AssociateTypeRootEntityId" +"""); + } + public override async Task Nested_associate_with_inline() { await base.Nested_associate_with_inline(); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/Associations/OwnedTableSplitting/OwnedTableSplittingStructuralEqualitySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/Associations/OwnedTableSplitting/OwnedTableSplittingStructuralEqualitySqliteTest.cs index b82ebbd2fde..c5946b67c16 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/Associations/OwnedTableSplitting/OwnedTableSplittingStructuralEqualitySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/Associations/OwnedTableSplitting/OwnedTableSplittingStructuralEqualitySqliteTest.cs @@ -142,6 +142,50 @@ LEFT JOIN "OptionalRelated_NestedCollection" AS "o" ON CASE """); } + public override async Task Optional_associate_nested_associate_with_inline_null() + { + await base.Optional_associate_nested_associate_with_inline_null(); + + AssertSql( + """ +SELECT "r"."Id", "r"."Name", "s"."RootEntityId", "s"."Id", "s"."Int", "s"."Ints", "s"."Name", "s"."String", "s"."AssociateTypeRootEntityId", "s"."AssociateTypeId", "s"."Id0", "s"."Int0", "s"."Ints0", "s"."Name0", "s"."String0", "s"."OptionalNestedAssociate_Id", "s"."OptionalNestedAssociate_Int", "s"."OptionalNestedAssociate_Ints", "s"."OptionalNestedAssociate_Name", "s"."OptionalNestedAssociate_String", "s"."RequiredNestedAssociate_Id", "s"."RequiredNestedAssociate_Int", "s"."RequiredNestedAssociate_Ints", "s"."RequiredNestedAssociate_Name", "s"."RequiredNestedAssociate_String", "r"."OptionalAssociate_Id", "r"."OptionalAssociate_Int", "r"."OptionalAssociate_Ints", "r"."OptionalAssociate_Name", "r"."OptionalAssociate_String", "o"."AssociateTypeRootEntityId", "o"."Id", "o"."Int", "o"."Ints", "o"."Name", "o"."String", "r"."OptionalAssociate_OptionalNestedAssociate_Id", "r"."OptionalAssociate_OptionalNestedAssociate_Int", "r"."OptionalAssociate_OptionalNestedAssociate_Ints", "r"."OptionalAssociate_OptionalNestedAssociate_Name", "r"."OptionalAssociate_OptionalNestedAssociate_String", "r"."OptionalAssociate_RequiredNestedAssociate_Id", "r"."OptionalAssociate_RequiredNestedAssociate_Int", "r"."OptionalAssociate_RequiredNestedAssociate_Ints", "r"."OptionalAssociate_RequiredNestedAssociate_Name", "r"."OptionalAssociate_RequiredNestedAssociate_String", "r"."RequiredAssociate_Id", "r"."RequiredAssociate_Int", "r"."RequiredAssociate_Ints", "r"."RequiredAssociate_Name", "r"."RequiredAssociate_String", "r2"."AssociateTypeRootEntityId", "r2"."Id", "r2"."Int", "r2"."Ints", "r2"."Name", "r2"."String", "r"."RequiredAssociate_OptionalNestedAssociate_Id", "r"."RequiredAssociate_OptionalNestedAssociate_Int", "r"."RequiredAssociate_OptionalNestedAssociate_Ints", "r"."RequiredAssociate_OptionalNestedAssociate_Name", "r"."RequiredAssociate_OptionalNestedAssociate_String", "r"."RequiredAssociate_RequiredNestedAssociate_Id", "r"."RequiredAssociate_RequiredNestedAssociate_Int", "r"."RequiredAssociate_RequiredNestedAssociate_Ints", "r"."RequiredAssociate_RequiredNestedAssociate_Name", "r"."RequiredAssociate_RequiredNestedAssociate_String" +FROM "RootEntity" AS "r" +LEFT JOIN ( + SELECT "r0"."RootEntityId", "r0"."Id", "r0"."Int", "r0"."Ints", "r0"."Name", "r0"."String", "r1"."AssociateTypeRootEntityId", "r1"."AssociateTypeId", "r1"."Id" AS "Id0", "r1"."Int" AS "Int0", "r1"."Ints" AS "Ints0", "r1"."Name" AS "Name0", "r1"."String" AS "String0", "r0"."OptionalNestedAssociate_Id", "r0"."OptionalNestedAssociate_Int", "r0"."OptionalNestedAssociate_Ints", "r0"."OptionalNestedAssociate_Name", "r0"."OptionalNestedAssociate_String", "r0"."RequiredNestedAssociate_Id", "r0"."RequiredNestedAssociate_Int", "r0"."RequiredNestedAssociate_Ints", "r0"."RequiredNestedAssociate_Name", "r0"."RequiredNestedAssociate_String" + FROM "RelatedCollection" AS "r0" + LEFT JOIN "RelatedCollection_NestedCollection" AS "r1" ON "r0"."RootEntityId" = "r1"."AssociateTypeRootEntityId" AND "r0"."Id" = "r1"."AssociateTypeId" +) AS "s" ON "r"."Id" = "s"."RootEntityId" +LEFT JOIN "OptionalRelated_NestedCollection" AS "o" ON CASE + WHEN "r"."OptionalAssociate_Id" IS NOT NULL AND "r"."OptionalAssociate_Int" IS NOT NULL AND "r"."OptionalAssociate_Ints" IS NOT NULL AND "r"."OptionalAssociate_Name" IS NOT NULL AND "r"."OptionalAssociate_String" IS NOT NULL THEN "r"."Id" +END = "o"."AssociateTypeRootEntityId" +LEFT JOIN "RequiredRelated_NestedCollection" AS "r2" ON "r"."Id" = "r2"."AssociateTypeRootEntityId" +WHERE "r"."OptionalAssociate_OptionalNestedAssociate_Id" IS NULL OR "r"."OptionalAssociate_OptionalNestedAssociate_Int" IS NULL OR "r"."OptionalAssociate_OptionalNestedAssociate_Ints" IS NULL OR "r"."OptionalAssociate_OptionalNestedAssociate_Name" IS NULL OR "r"."OptionalAssociate_OptionalNestedAssociate_String" IS NULL +ORDER BY "r"."Id", "s"."RootEntityId", "s"."Id", "s"."AssociateTypeRootEntityId", "s"."AssociateTypeId", "s"."Id0", "o"."AssociateTypeRootEntityId", "o"."Id", "r2"."AssociateTypeRootEntityId" +"""); + } + + public override async Task Optional_associate_nested_associate_with_inline_not_null() + { + await base.Optional_associate_nested_associate_with_inline_not_null(); + + AssertSql( + """ +SELECT "r"."Id", "r"."Name", "s"."RootEntityId", "s"."Id", "s"."Int", "s"."Ints", "s"."Name", "s"."String", "s"."AssociateTypeRootEntityId", "s"."AssociateTypeId", "s"."Id0", "s"."Int0", "s"."Ints0", "s"."Name0", "s"."String0", "s"."OptionalNestedAssociate_Id", "s"."OptionalNestedAssociate_Int", "s"."OptionalNestedAssociate_Ints", "s"."OptionalNestedAssociate_Name", "s"."OptionalNestedAssociate_String", "s"."RequiredNestedAssociate_Id", "s"."RequiredNestedAssociate_Int", "s"."RequiredNestedAssociate_Ints", "s"."RequiredNestedAssociate_Name", "s"."RequiredNestedAssociate_String", "r"."OptionalAssociate_Id", "r"."OptionalAssociate_Int", "r"."OptionalAssociate_Ints", "r"."OptionalAssociate_Name", "r"."OptionalAssociate_String", "o"."AssociateTypeRootEntityId", "o"."Id", "o"."Int", "o"."Ints", "o"."Name", "o"."String", "r"."OptionalAssociate_OptionalNestedAssociate_Id", "r"."OptionalAssociate_OptionalNestedAssociate_Int", "r"."OptionalAssociate_OptionalNestedAssociate_Ints", "r"."OptionalAssociate_OptionalNestedAssociate_Name", "r"."OptionalAssociate_OptionalNestedAssociate_String", "r"."OptionalAssociate_RequiredNestedAssociate_Id", "r"."OptionalAssociate_RequiredNestedAssociate_Int", "r"."OptionalAssociate_RequiredNestedAssociate_Ints", "r"."OptionalAssociate_RequiredNestedAssociate_Name", "r"."OptionalAssociate_RequiredNestedAssociate_String", "r"."RequiredAssociate_Id", "r"."RequiredAssociate_Int", "r"."RequiredAssociate_Ints", "r"."RequiredAssociate_Name", "r"."RequiredAssociate_String", "r2"."AssociateTypeRootEntityId", "r2"."Id", "r2"."Int", "r2"."Ints", "r2"."Name", "r2"."String", "r"."RequiredAssociate_OptionalNestedAssociate_Id", "r"."RequiredAssociate_OptionalNestedAssociate_Int", "r"."RequiredAssociate_OptionalNestedAssociate_Ints", "r"."RequiredAssociate_OptionalNestedAssociate_Name", "r"."RequiredAssociate_OptionalNestedAssociate_String", "r"."RequiredAssociate_RequiredNestedAssociate_Id", "r"."RequiredAssociate_RequiredNestedAssociate_Int", "r"."RequiredAssociate_RequiredNestedAssociate_Ints", "r"."RequiredAssociate_RequiredNestedAssociate_Name", "r"."RequiredAssociate_RequiredNestedAssociate_String" +FROM "RootEntity" AS "r" +LEFT JOIN ( + SELECT "r0"."RootEntityId", "r0"."Id", "r0"."Int", "r0"."Ints", "r0"."Name", "r0"."String", "r1"."AssociateTypeRootEntityId", "r1"."AssociateTypeId", "r1"."Id" AS "Id0", "r1"."Int" AS "Int0", "r1"."Ints" AS "Ints0", "r1"."Name" AS "Name0", "r1"."String" AS "String0", "r0"."OptionalNestedAssociate_Id", "r0"."OptionalNestedAssociate_Int", "r0"."OptionalNestedAssociate_Ints", "r0"."OptionalNestedAssociate_Name", "r0"."OptionalNestedAssociate_String", "r0"."RequiredNestedAssociate_Id", "r0"."RequiredNestedAssociate_Int", "r0"."RequiredNestedAssociate_Ints", "r0"."RequiredNestedAssociate_Name", "r0"."RequiredNestedAssociate_String" + FROM "RelatedCollection" AS "r0" + LEFT JOIN "RelatedCollection_NestedCollection" AS "r1" ON "r0"."RootEntityId" = "r1"."AssociateTypeRootEntityId" AND "r0"."Id" = "r1"."AssociateTypeId" +) AS "s" ON "r"."Id" = "s"."RootEntityId" +LEFT JOIN "OptionalRelated_NestedCollection" AS "o" ON CASE + WHEN "r"."OptionalAssociate_Id" IS NOT NULL AND "r"."OptionalAssociate_Int" IS NOT NULL AND "r"."OptionalAssociate_Ints" IS NOT NULL AND "r"."OptionalAssociate_Name" IS NOT NULL AND "r"."OptionalAssociate_String" IS NOT NULL THEN "r"."Id" +END = "o"."AssociateTypeRootEntityId" +LEFT JOIN "RequiredRelated_NestedCollection" AS "r2" ON "r"."Id" = "r2"."AssociateTypeRootEntityId" +WHERE "r"."OptionalAssociate_OptionalNestedAssociate_Id" IS NOT NULL AND "r"."OptionalAssociate_OptionalNestedAssociate_Int" IS NOT NULL AND "r"."OptionalAssociate_OptionalNestedAssociate_Ints" IS NOT NULL AND "r"."OptionalAssociate_OptionalNestedAssociate_Name" IS NOT NULL AND "r"."OptionalAssociate_OptionalNestedAssociate_String" IS NOT NULL +ORDER BY "r"."Id", "s"."RootEntityId", "s"."Id", "s"."AssociateTypeRootEntityId", "s"."AssociateTypeId", "s"."Id0", "o"."AssociateTypeRootEntityId", "o"."Id", "r2"."AssociateTypeRootEntityId" +"""); + } + public override async Task Nested_associate_with_inline() { await base.Nested_associate_with_inline(); From 0583a458aad137644bcc5697d0a8f20daf7c35fd Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Sat, 31 Jan 2026 16:25:42 +0100 Subject: [PATCH 06/19] Revert "Move to IS_NULL OR NOT IS_DEFINED and add test cases that would fail" This reverts commit c294e3bfabec061c3b789a03757e704d1e6a5e38. --- .../CosmosSqlTranslatingExpressionVisitor.cs | 15 +--- .../Expressions/SqlObjectAccessExpression.cs | 90 +++++++++++++++++++ ...NavigationsStructuralEqualityCosmosTest.cs | 28 +----- .../AssociationsStructuralEqualityTestBase.cs | 8 -- ...plexJsonStructuralEqualitySqlServerTest.cs | 26 ------ ...plittingStructuralEqualitySqlServerTest.cs | 26 ------ ...igationsStructuralEqualitySqlServerTest.cs | 57 ------------ ...wnedJsonStructuralEqualitySqlServerTest.cs | 25 ------ ...igationsStructuralEqualitySqlServerTest.cs | 57 ------------ ...plittingStructuralEqualitySqlServerTest.cs | 45 ---------- .../OwnedJsonStructuralEqualitySqliteTest.cs | 24 ----- ...NavigationsStructuralEqualitySqliteTest.cs | 56 ------------ ...leSplittingStructuralEqualitySqliteTest.cs | 44 --------- 13 files changed, 96 insertions(+), 405 deletions(-) create mode 100644 src/EFCore.Cosmos/Query/Internal/Expressions/SqlObjectAccessExpression.cs diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs index a6c23afd151..41880efc125 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs @@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.EntityFrameworkCore.Cosmos.Internal; using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Cosmos.Query.Internal.Expressions; using Microsoft.EntityFrameworkCore.Internal; using static Microsoft.EntityFrameworkCore.Infrastructure.ExpressionExtensions; @@ -1086,17 +1087,9 @@ private bool TryRewriteEntityEquality( return true; } - var isNull = sqlExpressionFactory.Function("IS_NULL", [entityReference.Object], typeof(bool)); - var isDefined = sqlExpressionFactory.Function("IS_DEFINED", [entityReference.Object], typeof(bool)); - var notDefined = sqlExpressionFactory.Not(isDefined); - var check = sqlExpressionFactory.MakeBinary(ExpressionType.OrElse, isNull, notDefined, typeMappingSource.FindMapping(typeof(bool))) ?? throw new UnreachableException(); - - if (nodeType == ExpressionType.NotEqual) - { - check = sqlExpressionFactory.Not(check); - } - - result = check; + // Treat type as object for null comparison + var access = new SqlObjectAccessExpression(entityReference.Object); + result = sqlExpressionFactory.MakeBinary(nodeType, access, sqlExpressionFactory.Constant(null, typeof(object))!, typeMappingSource.FindMapping(typeof(bool)))!; return true; } diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/SqlObjectAccessExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/SqlObjectAccessExpression.cs new file mode 100644 index 00000000000..4fe2f8c54ac --- /dev/null +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/SqlObjectAccessExpression.cs @@ -0,0 +1,90 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; + +namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal.Expressions; + +/// +/// Represents an structural type object access on a CosmosJSON object +/// +/// +/// 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. +/// +[DebuggerDisplay("{Microsoft.EntityFrameworkCore.Query.ExpressionPrinter.Print(this), nq}")] +public class SqlObjectAccessExpression(Expression @object) + : SqlExpression(typeof(object), CosmosTypeMapping.Default), IAccessExpression +{ + /// + /// 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. + /// + public virtual Expression Object { get; } = @object; + + /// + /// 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. + /// + public virtual string? PropertyName => null; + + /// + /// 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. + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + => Update(visitor.Visit(Object)); + + /// + /// 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. + /// + public virtual SqlObjectAccessExpression Update(Expression @object) + => ReferenceEquals(@object, Object) + ? this + : new SqlObjectAccessExpression(@object); + + /// + /// 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. + /// + protected override void Print(ExpressionPrinter expressionPrinter) + => expressionPrinter.Visit(Object); + + /// + /// 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. + /// + public override bool Equals(object? obj) + => obj != null + && (ReferenceEquals(this, obj) + || obj is SqlObjectAccessExpression expression + && Equals(expression)); + + private bool Equals(SqlObjectAccessExpression expression) + => base.Equals(expression) + && Object.Equals(expression.Object); + + /// + /// 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. + /// + public override int GetHashCode() + => HashCode.Combine(base.GetHashCode(), Object); +} diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualityCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualityCosmosTest.cs index 635f889ef1b..bed53ec3287 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualityCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualityCosmosTest.cs @@ -56,7 +56,7 @@ public override async Task Associate_with_inline_null() """ SELECT VALUE c FROM root c -WHERE (IS_NULL(c["OptionalAssociate"]) OR NOT(IS_DEFINED(c["OptionalAssociate"]))) +WHERE (c["OptionalAssociate"] = null) """); } @@ -71,31 +71,7 @@ public override async Task Nested_associate_with_inline_null() """ SELECT VALUE c FROM root c -WHERE (IS_NULL(c["RequiredAssociate"]["OptionalNestedAssociate"]) OR NOT(IS_DEFINED(c["RequiredAssociate"]["OptionalNestedAssociate"]))) -"""); - } - - public override async Task Optional_associate_nested_associate_with_inline_null() - { - await base.Optional_associate_nested_associate_with_inline_null(); - - AssertSql( -""" -SELECT VALUE c -FROM root c -WHERE (IS_NULL(c["OptionalAssociate"]["OptionalNestedAssociate"]) OR NOT(IS_DEFINED(c["OptionalAssociate"]["OptionalNestedAssociate"]))) -"""); - } - - public override async Task Optional_associate_nested_associate_with_inline_not_null() - { - await base.Optional_associate_nested_associate_with_inline_not_null(); - - AssertSql( -""" -SELECT VALUE c -FROM root c -WHERE NOT((IS_NULL(c["OptionalAssociate"]["OptionalNestedAssociate"]) OR NOT(IS_DEFINED(c["OptionalAssociate"]["OptionalNestedAssociate"])))) +WHERE (c["RequiredAssociate"]["OptionalNestedAssociate"] = null) """); } diff --git a/test/EFCore.Specification.Tests/Query/Associations/AssociationsStructuralEqualityTestBase.cs b/test/EFCore.Specification.Tests/Query/Associations/AssociationsStructuralEqualityTestBase.cs index 1314043ae6c..b786d6be49b 100644 --- a/test/EFCore.Specification.Tests/Query/Associations/AssociationsStructuralEqualityTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/Associations/AssociationsStructuralEqualityTestBase.cs @@ -46,14 +46,6 @@ public virtual async Task Associate_with_parameter_null() public virtual Task Nested_associate_with_inline_null() => AssertQuery(ss => ss.Set().Where(e => e.RequiredAssociate.OptionalNestedAssociate == null)); - [ConditionalFact] - public virtual Task Optional_associate_nested_associate_with_inline_null() - => AssertQuery(ss => ss.Set().Where(e => e.OptionalAssociate!.OptionalNestedAssociate == null)); - - [ConditionalFact] - public virtual Task Optional_associate_nested_associate_with_inline_not_null() - => AssertQuery(ss => ss.Set().Where(e => e.OptionalAssociate!.OptionalNestedAssociate != null)); - [ConditionalFact] public virtual Task Nested_associate_with_inline() => AssertQuery( diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonStructuralEqualitySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonStructuralEqualitySqlServerTest.cs index b9f62da955d..af756237f77 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonStructuralEqualitySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonStructuralEqualitySqlServerTest.cs @@ -130,32 +130,6 @@ WHERE JSON_QUERY([r].[RequiredAssociate], '$.OptionalNestedAssociate') IS NULL """); } - public override async Task Optional_associate_nested_associate_with_inline_null() - { - await base.Optional_associate_nested_associate_with_inline_null(); - - - AssertSql( - """ -SELECT [r].[Id], [r].[Name], [r].[AssociateCollection], [r].[OptionalAssociate], [r].[RequiredAssociate] -FROM [RootEntity] AS [r] -WHERE JSON_QUERY([r].[OptionalAssociate], '$.OptionalNestedAssociate') IS NULL -"""); - } - - public override async Task Optional_associate_nested_associate_with_inline_not_null() - { - await base.Optional_associate_nested_associate_with_inline_not_null(); - - - AssertSql( - """ -SELECT [r].[Id], [r].[Name], [r].[AssociateCollection], [r].[OptionalAssociate], [r].[RequiredAssociate] -FROM [RootEntity] AS [r] -WHERE JSON_QUERY([r].[OptionalAssociate], '$.OptionalNestedAssociate') IS NOT NULL -"""); - } - public override async Task Nested_associate_with_inline() { await base.Nested_associate_with_inline(); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/Associations/ComplexTableSplitting/ComplexTableSplittingStructuralEqualitySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/Associations/ComplexTableSplitting/ComplexTableSplittingStructuralEqualitySqlServerTest.cs index 32b2580d2c3..5a3468c7b73 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/Associations/ComplexTableSplitting/ComplexTableSplittingStructuralEqualitySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/Associations/ComplexTableSplitting/ComplexTableSplittingStructuralEqualitySqlServerTest.cs @@ -80,32 +80,6 @@ WHERE [r].[RequiredAssociate_OptionalNestedAssociate_Id] IS NULL """); } - public override async Task Optional_associate_nested_associate_with_inline_null() - { - await base.Optional_associate_nested_associate_with_inline_null(); - - - AssertSql( - """ -SELECT [r].[Id], [r].[Name], [r].[OptionalAssociate_Id], [r].[OptionalAssociate_Int], [r].[OptionalAssociate_Ints], [r].[OptionalAssociate_Name], [r].[OptionalAssociate_String], [r].[OptionalAssociate_OptionalNestedAssociate_Id], [r].[OptionalAssociate_OptionalNestedAssociate_Int], [r].[OptionalAssociate_OptionalNestedAssociate_Ints], [r].[OptionalAssociate_OptionalNestedAssociate_Name], [r].[OptionalAssociate_OptionalNestedAssociate_String], [r].[OptionalAssociate_RequiredNestedAssociate_Id], [r].[OptionalAssociate_RequiredNestedAssociate_Int], [r].[OptionalAssociate_RequiredNestedAssociate_Ints], [r].[OptionalAssociate_RequiredNestedAssociate_Name], [r].[OptionalAssociate_RequiredNestedAssociate_String], [r].[RequiredAssociate_Id], [r].[RequiredAssociate_Int], [r].[RequiredAssociate_Ints], [r].[RequiredAssociate_Name], [r].[RequiredAssociate_String], [r].[RequiredAssociate_OptionalNestedAssociate_Id], [r].[RequiredAssociate_OptionalNestedAssociate_Int], [r].[RequiredAssociate_OptionalNestedAssociate_Ints], [r].[RequiredAssociate_OptionalNestedAssociate_Name], [r].[RequiredAssociate_OptionalNestedAssociate_String], [r].[RequiredAssociate_RequiredNestedAssociate_Id], [r].[RequiredAssociate_RequiredNestedAssociate_Int], [r].[RequiredAssociate_RequiredNestedAssociate_Ints], [r].[RequiredAssociate_RequiredNestedAssociate_Name], [r].[RequiredAssociate_RequiredNestedAssociate_String] -FROM [RootEntity] AS [r] -WHERE [r].[OptionalAssociate_OptionalNestedAssociate_Id] IS NULL -"""); - } - - public override async Task Optional_associate_nested_associate_with_inline_not_null() - { - await base.Optional_associate_nested_associate_with_inline_not_null(); - - - AssertSql( - """ -SELECT [r].[Id], [r].[Name], [r].[OptionalAssociate_Id], [r].[OptionalAssociate_Int], [r].[OptionalAssociate_Ints], [r].[OptionalAssociate_Name], [r].[OptionalAssociate_String], [r].[OptionalAssociate_OptionalNestedAssociate_Id], [r].[OptionalAssociate_OptionalNestedAssociate_Int], [r].[OptionalAssociate_OptionalNestedAssociate_Ints], [r].[OptionalAssociate_OptionalNestedAssociate_Name], [r].[OptionalAssociate_OptionalNestedAssociate_String], [r].[OptionalAssociate_RequiredNestedAssociate_Id], [r].[OptionalAssociate_RequiredNestedAssociate_Int], [r].[OptionalAssociate_RequiredNestedAssociate_Ints], [r].[OptionalAssociate_RequiredNestedAssociate_Name], [r].[OptionalAssociate_RequiredNestedAssociate_String], [r].[RequiredAssociate_Id], [r].[RequiredAssociate_Int], [r].[RequiredAssociate_Ints], [r].[RequiredAssociate_Name], [r].[RequiredAssociate_String], [r].[RequiredAssociate_OptionalNestedAssociate_Id], [r].[RequiredAssociate_OptionalNestedAssociate_Int], [r].[RequiredAssociate_OptionalNestedAssociate_Ints], [r].[RequiredAssociate_OptionalNestedAssociate_Name], [r].[RequiredAssociate_OptionalNestedAssociate_String], [r].[RequiredAssociate_RequiredNestedAssociate_Id], [r].[RequiredAssociate_RequiredNestedAssociate_Int], [r].[RequiredAssociate_RequiredNestedAssociate_Ints], [r].[RequiredAssociate_RequiredNestedAssociate_Name], [r].[RequiredAssociate_RequiredNestedAssociate_String] -FROM [RootEntity] AS [r] -WHERE [r].[OptionalAssociate_OptionalNestedAssociate_Id] IS NOT NULL -"""); - } - public override async Task Nested_associate_with_inline() { await base.Nested_associate_with_inline(); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/Associations/Navigations/NavigationsStructuralEqualitySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/Associations/Navigations/NavigationsStructuralEqualitySqlServerTest.cs index 0e6d27af594..17eff5f0b9d 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/Associations/Navigations/NavigationsStructuralEqualitySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/Associations/Navigations/NavigationsStructuralEqualitySqlServerTest.cs @@ -176,63 +176,6 @@ WHERE [n].[Id] IS NULL """); } - public override async Task Optional_associate_nested_associate_with_inline_null() - { - await base.Optional_associate_nested_associate_with_inline_null(); - - AssertSql( - """ -SELECT [r].[Id], [r].[Name], [r].[OptionalAssociateId], [r].[RequiredAssociateId], [a].[Id], [n].[Id], [n0].[Id], [a0].[Id], [n1].[Id], [n2].[Id], [s].[Id], [s].[CollectionRootId], [s].[Int], [s].[Ints], [s].[Name], [s].[OptionalNestedAssociateId], [s].[RequiredNestedAssociateId], [s].[String], [s].[Id0], [s].[Id1], [s].[Id2], [s].[CollectionAssociateId], [s].[Int0], [s].[Ints0], [s].[Name0], [s].[String0], [s].[CollectionAssociateId0], [s].[Int1], [s].[Ints1], [s].[Name1], [s].[String1], [s].[CollectionAssociateId1], [s].[Int2], [s].[Ints2], [s].[Name2], [s].[String2], [a].[CollectionRootId], [a].[Int], [a].[Ints], [a].[Name], [a].[OptionalNestedAssociateId], [a].[RequiredNestedAssociateId], [a].[String], [n6].[Id], [n6].[CollectionAssociateId], [n6].[Int], [n6].[Ints], [n6].[Name], [n6].[String], [n].[CollectionAssociateId], [n].[Int], [n].[Ints], [n].[Name], [n].[String], [n0].[CollectionAssociateId], [n0].[Int], [n0].[Ints], [n0].[Name], [n0].[String], [a0].[CollectionRootId], [a0].[Int], [a0].[Ints], [a0].[Name], [a0].[OptionalNestedAssociateId], [a0].[RequiredNestedAssociateId], [a0].[String], [n7].[Id], [n7].[CollectionAssociateId], [n7].[Int], [n7].[Ints], [n7].[Name], [n7].[String], [n1].[CollectionAssociateId], [n1].[Int], [n1].[Ints], [n1].[Name], [n1].[String], [n2].[CollectionAssociateId], [n2].[Int], [n2].[Ints], [n2].[Name], [n2].[String] -FROM [RootEntity] AS [r] -LEFT JOIN [AssociateType] AS [a] ON [r].[OptionalAssociateId] = [a].[Id] -LEFT JOIN [NestedAssociateType] AS [n] ON [a].[OptionalNestedAssociateId] = [n].[Id] -LEFT JOIN [NestedAssociateType] AS [n0] ON [a].[RequiredNestedAssociateId] = [n0].[Id] -INNER JOIN [AssociateType] AS [a0] ON [r].[RequiredAssociateId] = [a0].[Id] -LEFT JOIN [NestedAssociateType] AS [n1] ON [a0].[OptionalNestedAssociateId] = [n1].[Id] -INNER JOIN [NestedAssociateType] AS [n2] ON [a0].[RequiredNestedAssociateId] = [n2].[Id] -LEFT JOIN ( - SELECT [a1].[Id], [a1].[CollectionRootId], [a1].[Int], [a1].[Ints], [a1].[Name], [a1].[OptionalNestedAssociateId], [a1].[RequiredNestedAssociateId], [a1].[String], [n3].[Id] AS [Id0], [n4].[Id] AS [Id1], [n5].[Id] AS [Id2], [n5].[CollectionAssociateId], [n5].[Int] AS [Int0], [n5].[Ints] AS [Ints0], [n5].[Name] AS [Name0], [n5].[String] AS [String0], [n3].[CollectionAssociateId] AS [CollectionAssociateId0], [n3].[Int] AS [Int1], [n3].[Ints] AS [Ints1], [n3].[Name] AS [Name1], [n3].[String] AS [String1], [n4].[CollectionAssociateId] AS [CollectionAssociateId1], [n4].[Int] AS [Int2], [n4].[Ints] AS [Ints2], [n4].[Name] AS [Name2], [n4].[String] AS [String2] - FROM [AssociateType] AS [a1] - LEFT JOIN [NestedAssociateType] AS [n3] ON [a1].[OptionalNestedAssociateId] = [n3].[Id] - INNER JOIN [NestedAssociateType] AS [n4] ON [a1].[RequiredNestedAssociateId] = [n4].[Id] - LEFT JOIN [NestedAssociateType] AS [n5] ON [a1].[Id] = [n5].[CollectionAssociateId] -) AS [s] ON [r].[Id] = [s].[CollectionRootId] -LEFT JOIN [NestedAssociateType] AS [n6] ON [a].[Id] = [n6].[CollectionAssociateId] -LEFT JOIN [NestedAssociateType] AS [n7] ON [a0].[Id] = [n7].[CollectionAssociateId] -WHERE [n].[Id] IS NULL -ORDER BY [r].[Id], [a].[Id], [n].[Id], [n0].[Id], [a0].[Id], [n1].[Id], [n2].[Id], [s].[Id], [s].[Id0], [s].[Id1], [s].[Id2], [n6].[Id] -"""); - } - - public override async Task Optional_associate_nested_associate_with_inline_not_null() - { - await base.Optional_associate_nested_associate_with_inline_not_null(); - - - AssertSql( - """ -SELECT [r].[Id], [r].[Name], [r].[OptionalAssociateId], [r].[RequiredAssociateId], [a].[Id], [n].[Id], [n0].[Id], [a0].[Id], [n1].[Id], [n2].[Id], [s].[Id], [s].[CollectionRootId], [s].[Int], [s].[Ints], [s].[Name], [s].[OptionalNestedAssociateId], [s].[RequiredNestedAssociateId], [s].[String], [s].[Id0], [s].[Id1], [s].[Id2], [s].[CollectionAssociateId], [s].[Int0], [s].[Ints0], [s].[Name0], [s].[String0], [s].[CollectionAssociateId0], [s].[Int1], [s].[Ints1], [s].[Name1], [s].[String1], [s].[CollectionAssociateId1], [s].[Int2], [s].[Ints2], [s].[Name2], [s].[String2], [a].[CollectionRootId], [a].[Int], [a].[Ints], [a].[Name], [a].[OptionalNestedAssociateId], [a].[RequiredNestedAssociateId], [a].[String], [n6].[Id], [n6].[CollectionAssociateId], [n6].[Int], [n6].[Ints], [n6].[Name], [n6].[String], [n].[CollectionAssociateId], [n].[Int], [n].[Ints], [n].[Name], [n].[String], [n0].[CollectionAssociateId], [n0].[Int], [n0].[Ints], [n0].[Name], [n0].[String], [a0].[CollectionRootId], [a0].[Int], [a0].[Ints], [a0].[Name], [a0].[OptionalNestedAssociateId], [a0].[RequiredNestedAssociateId], [a0].[String], [n7].[Id], [n7].[CollectionAssociateId], [n7].[Int], [n7].[Ints], [n7].[Name], [n7].[String], [n1].[CollectionAssociateId], [n1].[Int], [n1].[Ints], [n1].[Name], [n1].[String], [n2].[CollectionAssociateId], [n2].[Int], [n2].[Ints], [n2].[Name], [n2].[String] -FROM [RootEntity] AS [r] -LEFT JOIN [AssociateType] AS [a] ON [r].[OptionalAssociateId] = [a].[Id] -LEFT JOIN [NestedAssociateType] AS [n] ON [a].[OptionalNestedAssociateId] = [n].[Id] -LEFT JOIN [NestedAssociateType] AS [n0] ON [a].[RequiredNestedAssociateId] = [n0].[Id] -INNER JOIN [AssociateType] AS [a0] ON [r].[RequiredAssociateId] = [a0].[Id] -LEFT JOIN [NestedAssociateType] AS [n1] ON [a0].[OptionalNestedAssociateId] = [n1].[Id] -INNER JOIN [NestedAssociateType] AS [n2] ON [a0].[RequiredNestedAssociateId] = [n2].[Id] -LEFT JOIN ( - SELECT [a1].[Id], [a1].[CollectionRootId], [a1].[Int], [a1].[Ints], [a1].[Name], [a1].[OptionalNestedAssociateId], [a1].[RequiredNestedAssociateId], [a1].[String], [n3].[Id] AS [Id0], [n4].[Id] AS [Id1], [n5].[Id] AS [Id2], [n5].[CollectionAssociateId], [n5].[Int] AS [Int0], [n5].[Ints] AS [Ints0], [n5].[Name] AS [Name0], [n5].[String] AS [String0], [n3].[CollectionAssociateId] AS [CollectionAssociateId0], [n3].[Int] AS [Int1], [n3].[Ints] AS [Ints1], [n3].[Name] AS [Name1], [n3].[String] AS [String1], [n4].[CollectionAssociateId] AS [CollectionAssociateId1], [n4].[Int] AS [Int2], [n4].[Ints] AS [Ints2], [n4].[Name] AS [Name2], [n4].[String] AS [String2] - FROM [AssociateType] AS [a1] - LEFT JOIN [NestedAssociateType] AS [n3] ON [a1].[OptionalNestedAssociateId] = [n3].[Id] - INNER JOIN [NestedAssociateType] AS [n4] ON [a1].[RequiredNestedAssociateId] = [n4].[Id] - LEFT JOIN [NestedAssociateType] AS [n5] ON [a1].[Id] = [n5].[CollectionAssociateId] -) AS [s] ON [r].[Id] = [s].[CollectionRootId] -LEFT JOIN [NestedAssociateType] AS [n6] ON [a].[Id] = [n6].[CollectionAssociateId] -LEFT JOIN [NestedAssociateType] AS [n7] ON [a0].[Id] = [n7].[CollectionAssociateId] -WHERE [n].[Id] IS NOT NULL -ORDER BY [r].[Id], [a].[Id], [n].[Id], [n0].[Id], [a0].[Id], [n1].[Id], [n2].[Id], [s].[Id], [s].[Id0], [s].[Id1], [s].[Id2], [n6].[Id] -"""); - } - public override async Task Nested_associate_with_inline() { await base.Nested_associate_with_inline(); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonStructuralEqualitySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonStructuralEqualitySqlServerTest.cs index d6d96194443..87797f8c30a 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonStructuralEqualitySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonStructuralEqualitySqlServerTest.cs @@ -80,31 +80,6 @@ WHERE JSON_QUERY([r].[RequiredAssociate], '$.OptionalNestedAssociate') IS NULL """); } - public override async Task Optional_associate_nested_associate_with_inline_null() - { - await base.Optional_associate_nested_associate_with_inline_null(); - - AssertSql( - """ -SELECT [r].[Id], [r].[Name], [r].[AssociateCollection], [r].[OptionalAssociate], [r].[RequiredAssociate] -FROM [RootEntity] AS [r] -WHERE JSON_QUERY([r].[OptionalAssociate], '$.OptionalNestedAssociate') IS NULL -"""); - } - - public override async Task Optional_associate_nested_associate_with_inline_not_null() - { - await base.Optional_associate_nested_associate_with_inline_not_null(); - - - AssertSql( - """ -SELECT [r].[Id], [r].[Name], [r].[AssociateCollection], [r].[OptionalAssociate], [r].[RequiredAssociate] -FROM [RootEntity] AS [r] -WHERE JSON_QUERY([r].[OptionalAssociate], '$.OptionalNestedAssociate') IS NOT NULL -"""); - } - public override async Task Nested_associate_with_inline() { await base.Nested_associate_with_inline(); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualitySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualitySqlServerTest.cs index 3a2983a99aa..02575f6ea0c 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualitySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualitySqlServerTest.cs @@ -176,63 +176,6 @@ WHERE [r1].[AssociateTypeRootEntityId] IS NULL """); } - public override async Task Optional_associate_nested_associate_with_inline_null() - { - await base.Optional_associate_nested_associate_with_inline_null(); - - AssertSql( - """ -SELECT [r].[Id], [r].[Name], [o].[RootEntityId], [o0].[AssociateTypeRootEntityId], [o1].[AssociateTypeRootEntityId], [r0].[RootEntityId], [r1].[AssociateTypeRootEntityId], [r2].[AssociateTypeRootEntityId], [s].[RootEntityId], [s].[Id], [s].[Int], [s].[Ints], [s].[Name], [s].[String], [s].[AssociateTypeRootEntityId], [s].[AssociateTypeId], [s].[AssociateTypeRootEntityId0], [s].[AssociateTypeId0], [s].[AssociateTypeRootEntityId1], [s].[AssociateTypeId1], [s].[Id0], [s].[Int0], [s].[Ints0], [s].[Name0], [s].[String0], [s].[Id1], [s].[Int1], [s].[Ints1], [s].[Name1], [s].[String1], [s].[Id2], [s].[Int2], [s].[Ints2], [s].[Name2], [s].[String2], [o].[Id], [o].[Int], [o].[Ints], [o].[Name], [o].[String], [o2].[AssociateTypeRootEntityId], [o2].[Id], [o2].[Int], [o2].[Ints], [o2].[Name], [o2].[String], [o0].[Id], [o0].[Int], [o0].[Ints], [o0].[Name], [o0].[String], [o1].[Id], [o1].[Int], [o1].[Ints], [o1].[Name], [o1].[String], [r0].[Id], [r0].[Int], [r0].[Ints], [r0].[Name], [r0].[String], [r7].[AssociateTypeRootEntityId], [r7].[Id], [r7].[Int], [r7].[Ints], [r7].[Name], [r7].[String], [r1].[Id], [r1].[Int], [r1].[Ints], [r1].[Name], [r1].[String], [r2].[Id], [r2].[Int], [r2].[Ints], [r2].[Name], [r2].[String] -FROM [RootEntity] AS [r] -LEFT JOIN [OptionalRelated] AS [o] ON [r].[Id] = [o].[RootEntityId] -LEFT JOIN [OptionalRelated_OptionalNested] AS [o0] ON [o].[RootEntityId] = [o0].[AssociateTypeRootEntityId] -LEFT JOIN [OptionalRelated_RequiredNested] AS [o1] ON [o].[RootEntityId] = [o1].[AssociateTypeRootEntityId] -LEFT JOIN [RequiredRelated] AS [r0] ON [r].[Id] = [r0].[RootEntityId] -LEFT JOIN [RequiredRelated_OptionalNested] AS [r1] ON [r0].[RootEntityId] = [r1].[AssociateTypeRootEntityId] -LEFT JOIN [RequiredRelated_RequiredNested] AS [r2] ON [r0].[RootEntityId] = [r2].[AssociateTypeRootEntityId] -LEFT JOIN ( - SELECT [r3].[RootEntityId], [r3].[Id], [r3].[Int], [r3].[Ints], [r3].[Name], [r3].[String], [r4].[AssociateTypeRootEntityId], [r4].[AssociateTypeId], [r5].[AssociateTypeRootEntityId] AS [AssociateTypeRootEntityId0], [r5].[AssociateTypeId] AS [AssociateTypeId0], [r6].[AssociateTypeRootEntityId] AS [AssociateTypeRootEntityId1], [r6].[AssociateTypeId] AS [AssociateTypeId1], [r6].[Id] AS [Id0], [r6].[Int] AS [Int0], [r6].[Ints] AS [Ints0], [r6].[Name] AS [Name0], [r6].[String] AS [String0], [r4].[Id] AS [Id1], [r4].[Int] AS [Int1], [r4].[Ints] AS [Ints1], [r4].[Name] AS [Name1], [r4].[String] AS [String1], [r5].[Id] AS [Id2], [r5].[Int] AS [Int2], [r5].[Ints] AS [Ints2], [r5].[Name] AS [Name2], [r5].[String] AS [String2] - FROM [RelatedCollection] AS [r3] - LEFT JOIN [RelatedCollection_OptionalNested] AS [r4] ON [r3].[RootEntityId] = [r4].[AssociateTypeRootEntityId] AND [r3].[Id] = [r4].[AssociateTypeId] - LEFT JOIN [RelatedCollection_RequiredNested] AS [r5] ON [r3].[RootEntityId] = [r5].[AssociateTypeRootEntityId] AND [r3].[Id] = [r5].[AssociateTypeId] - LEFT JOIN [RelatedCollection_NestedCollection] AS [r6] ON [r3].[RootEntityId] = [r6].[AssociateTypeRootEntityId] AND [r3].[Id] = [r6].[AssociateTypeId] -) AS [s] ON [r].[Id] = [s].[RootEntityId] -LEFT JOIN [OptionalRelated_NestedCollection] AS [o2] ON [o].[RootEntityId] = [o2].[AssociateTypeRootEntityId] -LEFT JOIN [RequiredRelated_NestedCollection] AS [r7] ON [r0].[RootEntityId] = [r7].[AssociateTypeRootEntityId] -WHERE [o0].[AssociateTypeRootEntityId] IS NULL -ORDER BY [r].[Id], [o].[RootEntityId], [o0].[AssociateTypeRootEntityId], [o1].[AssociateTypeRootEntityId], [r0].[RootEntityId], [r1].[AssociateTypeRootEntityId], [r2].[AssociateTypeRootEntityId], [s].[RootEntityId], [s].[Id], [s].[AssociateTypeRootEntityId], [s].[AssociateTypeId], [s].[AssociateTypeRootEntityId0], [s].[AssociateTypeId0], [s].[AssociateTypeRootEntityId1], [s].[AssociateTypeId1], [s].[Id0], [o2].[AssociateTypeRootEntityId], [o2].[Id], [r7].[AssociateTypeRootEntityId] -"""); - } - - public override async Task Optional_associate_nested_associate_with_inline_not_null() - { - await base.Optional_associate_nested_associate_with_inline_not_null(); - - - AssertSql( - """ -SELECT [r].[Id], [r].[Name], [o].[RootEntityId], [o0].[AssociateTypeRootEntityId], [o1].[AssociateTypeRootEntityId], [r0].[RootEntityId], [r1].[AssociateTypeRootEntityId], [r2].[AssociateTypeRootEntityId], [s].[RootEntityId], [s].[Id], [s].[Int], [s].[Ints], [s].[Name], [s].[String], [s].[AssociateTypeRootEntityId], [s].[AssociateTypeId], [s].[AssociateTypeRootEntityId0], [s].[AssociateTypeId0], [s].[AssociateTypeRootEntityId1], [s].[AssociateTypeId1], [s].[Id0], [s].[Int0], [s].[Ints0], [s].[Name0], [s].[String0], [s].[Id1], [s].[Int1], [s].[Ints1], [s].[Name1], [s].[String1], [s].[Id2], [s].[Int2], [s].[Ints2], [s].[Name2], [s].[String2], [o].[Id], [o].[Int], [o].[Ints], [o].[Name], [o].[String], [o2].[AssociateTypeRootEntityId], [o2].[Id], [o2].[Int], [o2].[Ints], [o2].[Name], [o2].[String], [o0].[Id], [o0].[Int], [o0].[Ints], [o0].[Name], [o0].[String], [o1].[Id], [o1].[Int], [o1].[Ints], [o1].[Name], [o1].[String], [r0].[Id], [r0].[Int], [r0].[Ints], [r0].[Name], [r0].[String], [r7].[AssociateTypeRootEntityId], [r7].[Id], [r7].[Int], [r7].[Ints], [r7].[Name], [r7].[String], [r1].[Id], [r1].[Int], [r1].[Ints], [r1].[Name], [r1].[String], [r2].[Id], [r2].[Int], [r2].[Ints], [r2].[Name], [r2].[String] -FROM [RootEntity] AS [r] -LEFT JOIN [OptionalRelated] AS [o] ON [r].[Id] = [o].[RootEntityId] -LEFT JOIN [OptionalRelated_OptionalNested] AS [o0] ON [o].[RootEntityId] = [o0].[AssociateTypeRootEntityId] -LEFT JOIN [OptionalRelated_RequiredNested] AS [o1] ON [o].[RootEntityId] = [o1].[AssociateTypeRootEntityId] -LEFT JOIN [RequiredRelated] AS [r0] ON [r].[Id] = [r0].[RootEntityId] -LEFT JOIN [RequiredRelated_OptionalNested] AS [r1] ON [r0].[RootEntityId] = [r1].[AssociateTypeRootEntityId] -LEFT JOIN [RequiredRelated_RequiredNested] AS [r2] ON [r0].[RootEntityId] = [r2].[AssociateTypeRootEntityId] -LEFT JOIN ( - SELECT [r3].[RootEntityId], [r3].[Id], [r3].[Int], [r3].[Ints], [r3].[Name], [r3].[String], [r4].[AssociateTypeRootEntityId], [r4].[AssociateTypeId], [r5].[AssociateTypeRootEntityId] AS [AssociateTypeRootEntityId0], [r5].[AssociateTypeId] AS [AssociateTypeId0], [r6].[AssociateTypeRootEntityId] AS [AssociateTypeRootEntityId1], [r6].[AssociateTypeId] AS [AssociateTypeId1], [r6].[Id] AS [Id0], [r6].[Int] AS [Int0], [r6].[Ints] AS [Ints0], [r6].[Name] AS [Name0], [r6].[String] AS [String0], [r4].[Id] AS [Id1], [r4].[Int] AS [Int1], [r4].[Ints] AS [Ints1], [r4].[Name] AS [Name1], [r4].[String] AS [String1], [r5].[Id] AS [Id2], [r5].[Int] AS [Int2], [r5].[Ints] AS [Ints2], [r5].[Name] AS [Name2], [r5].[String] AS [String2] - FROM [RelatedCollection] AS [r3] - LEFT JOIN [RelatedCollection_OptionalNested] AS [r4] ON [r3].[RootEntityId] = [r4].[AssociateTypeRootEntityId] AND [r3].[Id] = [r4].[AssociateTypeId] - LEFT JOIN [RelatedCollection_RequiredNested] AS [r5] ON [r3].[RootEntityId] = [r5].[AssociateTypeRootEntityId] AND [r3].[Id] = [r5].[AssociateTypeId] - LEFT JOIN [RelatedCollection_NestedCollection] AS [r6] ON [r3].[RootEntityId] = [r6].[AssociateTypeRootEntityId] AND [r3].[Id] = [r6].[AssociateTypeId] -) AS [s] ON [r].[Id] = [s].[RootEntityId] -LEFT JOIN [OptionalRelated_NestedCollection] AS [o2] ON [o].[RootEntityId] = [o2].[AssociateTypeRootEntityId] -LEFT JOIN [RequiredRelated_NestedCollection] AS [r7] ON [r0].[RootEntityId] = [r7].[AssociateTypeRootEntityId] -WHERE [o0].[AssociateTypeRootEntityId] IS NOT NULL -ORDER BY [r].[Id], [o].[RootEntityId], [o0].[AssociateTypeRootEntityId], [o1].[AssociateTypeRootEntityId], [r0].[RootEntityId], [r1].[AssociateTypeRootEntityId], [r2].[AssociateTypeRootEntityId], [s].[RootEntityId], [s].[Id], [s].[AssociateTypeRootEntityId], [s].[AssociateTypeId], [s].[AssociateTypeRootEntityId0], [s].[AssociateTypeId0], [s].[AssociateTypeRootEntityId1], [s].[AssociateTypeId1], [s].[Id0], [o2].[AssociateTypeRootEntityId], [o2].[Id], [r7].[AssociateTypeRootEntityId] -"""); - } - public override async Task Nested_associate_with_inline() { await base.Nested_associate_with_inline(); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/Associations/OwnedTableSplitting/OwnedTableSplittingStructuralEqualitySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/Associations/OwnedTableSplitting/OwnedTableSplittingStructuralEqualitySqlServerTest.cs index 086897796ac..8f905ca3010 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/Associations/OwnedTableSplitting/OwnedTableSplittingStructuralEqualitySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/Associations/OwnedTableSplitting/OwnedTableSplittingStructuralEqualitySqlServerTest.cs @@ -142,51 +142,6 @@ WHERE [r].[RequiredAssociate_OptionalNestedAssociate_Id] IS NULL OR [r].[Require """); } - public override async Task Optional_associate_nested_associate_with_inline_null() - { - await base.Optional_associate_nested_associate_with_inline_null(); - - AssertSql( - """ -SELECT [r].[Id], [r].[Name], [s].[RootEntityId], [s].[Id], [s].[Int], [s].[Ints], [s].[Name], [s].[String], [s].[AssociateTypeRootEntityId], [s].[AssociateTypeId], [s].[Id0], [s].[Int0], [s].[Ints0], [s].[Name0], [s].[String0], [s].[OptionalNestedAssociate_Id], [s].[OptionalNestedAssociate_Int], [s].[OptionalNestedAssociate_Ints], [s].[OptionalNestedAssociate_Name], [s].[OptionalNestedAssociate_String], [s].[RequiredNestedAssociate_Id], [s].[RequiredNestedAssociate_Int], [s].[RequiredNestedAssociate_Ints], [s].[RequiredNestedAssociate_Name], [s].[RequiredNestedAssociate_String], [r].[OptionalAssociate_Id], [r].[OptionalAssociate_Int], [r].[OptionalAssociate_Ints], [r].[OptionalAssociate_Name], [r].[OptionalAssociate_String], [o].[AssociateTypeRootEntityId], [o].[Id], [o].[Int], [o].[Ints], [o].[Name], [o].[String], [r].[OptionalAssociate_OptionalNestedAssociate_Id], [r].[OptionalAssociate_OptionalNestedAssociate_Int], [r].[OptionalAssociate_OptionalNestedAssociate_Ints], [r].[OptionalAssociate_OptionalNestedAssociate_Name], [r].[OptionalAssociate_OptionalNestedAssociate_String], [r].[OptionalAssociate_RequiredNestedAssociate_Id], [r].[OptionalAssociate_RequiredNestedAssociate_Int], [r].[OptionalAssociate_RequiredNestedAssociate_Ints], [r].[OptionalAssociate_RequiredNestedAssociate_Name], [r].[OptionalAssociate_RequiredNestedAssociate_String], [r].[RequiredAssociate_Id], [r].[RequiredAssociate_Int], [r].[RequiredAssociate_Ints], [r].[RequiredAssociate_Name], [r].[RequiredAssociate_String], [r2].[AssociateTypeRootEntityId], [r2].[Id], [r2].[Int], [r2].[Ints], [r2].[Name], [r2].[String], [r].[RequiredAssociate_OptionalNestedAssociate_Id], [r].[RequiredAssociate_OptionalNestedAssociate_Int], [r].[RequiredAssociate_OptionalNestedAssociate_Ints], [r].[RequiredAssociate_OptionalNestedAssociate_Name], [r].[RequiredAssociate_OptionalNestedAssociate_String], [r].[RequiredAssociate_RequiredNestedAssociate_Id], [r].[RequiredAssociate_RequiredNestedAssociate_Int], [r].[RequiredAssociate_RequiredNestedAssociate_Ints], [r].[RequiredAssociate_RequiredNestedAssociate_Name], [r].[RequiredAssociate_RequiredNestedAssociate_String] -FROM [RootEntity] AS [r] -LEFT JOIN ( - SELECT [r0].[RootEntityId], [r0].[Id], [r0].[Int], [r0].[Ints], [r0].[Name], [r0].[String], [r1].[AssociateTypeRootEntityId], [r1].[AssociateTypeId], [r1].[Id] AS [Id0], [r1].[Int] AS [Int0], [r1].[Ints] AS [Ints0], [r1].[Name] AS [Name0], [r1].[String] AS [String0], [r0].[OptionalNestedAssociate_Id], [r0].[OptionalNestedAssociate_Int], [r0].[OptionalNestedAssociate_Ints], [r0].[OptionalNestedAssociate_Name], [r0].[OptionalNestedAssociate_String], [r0].[RequiredNestedAssociate_Id], [r0].[RequiredNestedAssociate_Int], [r0].[RequiredNestedAssociate_Ints], [r0].[RequiredNestedAssociate_Name], [r0].[RequiredNestedAssociate_String] - FROM [RelatedCollection] AS [r0] - LEFT JOIN [RelatedCollection_NestedCollection] AS [r1] ON [r0].[RootEntityId] = [r1].[AssociateTypeRootEntityId] AND [r0].[Id] = [r1].[AssociateTypeId] -) AS [s] ON [r].[Id] = [s].[RootEntityId] -LEFT JOIN [OptionalRelated_NestedCollection] AS [o] ON CASE - WHEN [r].[OptionalAssociate_Id] IS NOT NULL AND [r].[OptionalAssociate_Int] IS NOT NULL AND [r].[OptionalAssociate_Ints] IS NOT NULL AND [r].[OptionalAssociate_Name] IS NOT NULL AND [r].[OptionalAssociate_String] IS NOT NULL THEN [r].[Id] -END = [o].[AssociateTypeRootEntityId] -LEFT JOIN [RequiredRelated_NestedCollection] AS [r2] ON [r].[Id] = [r2].[AssociateTypeRootEntityId] -WHERE [r].[OptionalAssociate_OptionalNestedAssociate_Id] IS NULL OR [r].[OptionalAssociate_OptionalNestedAssociate_Int] IS NULL OR [r].[OptionalAssociate_OptionalNestedAssociate_Ints] IS NULL OR [r].[OptionalAssociate_OptionalNestedAssociate_Name] IS NULL OR [r].[OptionalAssociate_OptionalNestedAssociate_String] IS NULL -ORDER BY [r].[Id], [s].[RootEntityId], [s].[Id], [s].[AssociateTypeRootEntityId], [s].[AssociateTypeId], [s].[Id0], [o].[AssociateTypeRootEntityId], [o].[Id], [r2].[AssociateTypeRootEntityId] -"""); - } - - public override async Task Optional_associate_nested_associate_with_inline_not_null() - { - await base.Optional_associate_nested_associate_with_inline_not_null(); - - - AssertSql( - """ -SELECT [r].[Id], [r].[Name], [s].[RootEntityId], [s].[Id], [s].[Int], [s].[Ints], [s].[Name], [s].[String], [s].[AssociateTypeRootEntityId], [s].[AssociateTypeId], [s].[Id0], [s].[Int0], [s].[Ints0], [s].[Name0], [s].[String0], [s].[OptionalNestedAssociate_Id], [s].[OptionalNestedAssociate_Int], [s].[OptionalNestedAssociate_Ints], [s].[OptionalNestedAssociate_Name], [s].[OptionalNestedAssociate_String], [s].[RequiredNestedAssociate_Id], [s].[RequiredNestedAssociate_Int], [s].[RequiredNestedAssociate_Ints], [s].[RequiredNestedAssociate_Name], [s].[RequiredNestedAssociate_String], [r].[OptionalAssociate_Id], [r].[OptionalAssociate_Int], [r].[OptionalAssociate_Ints], [r].[OptionalAssociate_Name], [r].[OptionalAssociate_String], [o].[AssociateTypeRootEntityId], [o].[Id], [o].[Int], [o].[Ints], [o].[Name], [o].[String], [r].[OptionalAssociate_OptionalNestedAssociate_Id], [r].[OptionalAssociate_OptionalNestedAssociate_Int], [r].[OptionalAssociate_OptionalNestedAssociate_Ints], [r].[OptionalAssociate_OptionalNestedAssociate_Name], [r].[OptionalAssociate_OptionalNestedAssociate_String], [r].[OptionalAssociate_RequiredNestedAssociate_Id], [r].[OptionalAssociate_RequiredNestedAssociate_Int], [r].[OptionalAssociate_RequiredNestedAssociate_Ints], [r].[OptionalAssociate_RequiredNestedAssociate_Name], [r].[OptionalAssociate_RequiredNestedAssociate_String], [r].[RequiredAssociate_Id], [r].[RequiredAssociate_Int], [r].[RequiredAssociate_Ints], [r].[RequiredAssociate_Name], [r].[RequiredAssociate_String], [r2].[AssociateTypeRootEntityId], [r2].[Id], [r2].[Int], [r2].[Ints], [r2].[Name], [r2].[String], [r].[RequiredAssociate_OptionalNestedAssociate_Id], [r].[RequiredAssociate_OptionalNestedAssociate_Int], [r].[RequiredAssociate_OptionalNestedAssociate_Ints], [r].[RequiredAssociate_OptionalNestedAssociate_Name], [r].[RequiredAssociate_OptionalNestedAssociate_String], [r].[RequiredAssociate_RequiredNestedAssociate_Id], [r].[RequiredAssociate_RequiredNestedAssociate_Int], [r].[RequiredAssociate_RequiredNestedAssociate_Ints], [r].[RequiredAssociate_RequiredNestedAssociate_Name], [r].[RequiredAssociate_RequiredNestedAssociate_String] -FROM [RootEntity] AS [r] -LEFT JOIN ( - SELECT [r0].[RootEntityId], [r0].[Id], [r0].[Int], [r0].[Ints], [r0].[Name], [r0].[String], [r1].[AssociateTypeRootEntityId], [r1].[AssociateTypeId], [r1].[Id] AS [Id0], [r1].[Int] AS [Int0], [r1].[Ints] AS [Ints0], [r1].[Name] AS [Name0], [r1].[String] AS [String0], [r0].[OptionalNestedAssociate_Id], [r0].[OptionalNestedAssociate_Int], [r0].[OptionalNestedAssociate_Ints], [r0].[OptionalNestedAssociate_Name], [r0].[OptionalNestedAssociate_String], [r0].[RequiredNestedAssociate_Id], [r0].[RequiredNestedAssociate_Int], [r0].[RequiredNestedAssociate_Ints], [r0].[RequiredNestedAssociate_Name], [r0].[RequiredNestedAssociate_String] - FROM [RelatedCollection] AS [r0] - LEFT JOIN [RelatedCollection_NestedCollection] AS [r1] ON [r0].[RootEntityId] = [r1].[AssociateTypeRootEntityId] AND [r0].[Id] = [r1].[AssociateTypeId] -) AS [s] ON [r].[Id] = [s].[RootEntityId] -LEFT JOIN [OptionalRelated_NestedCollection] AS [o] ON CASE - WHEN [r].[OptionalAssociate_Id] IS NOT NULL AND [r].[OptionalAssociate_Int] IS NOT NULL AND [r].[OptionalAssociate_Ints] IS NOT NULL AND [r].[OptionalAssociate_Name] IS NOT NULL AND [r].[OptionalAssociate_String] IS NOT NULL THEN [r].[Id] -END = [o].[AssociateTypeRootEntityId] -LEFT JOIN [RequiredRelated_NestedCollection] AS [r2] ON [r].[Id] = [r2].[AssociateTypeRootEntityId] -WHERE [r].[OptionalAssociate_OptionalNestedAssociate_Id] IS NOT NULL AND [r].[OptionalAssociate_OptionalNestedAssociate_Int] IS NOT NULL AND [r].[OptionalAssociate_OptionalNestedAssociate_Ints] IS NOT NULL AND [r].[OptionalAssociate_OptionalNestedAssociate_Name] IS NOT NULL AND [r].[OptionalAssociate_OptionalNestedAssociate_String] IS NOT NULL -ORDER BY [r].[Id], [s].[RootEntityId], [s].[Id], [s].[AssociateTypeRootEntityId], [s].[AssociateTypeId], [s].[Id0], [o].[AssociateTypeRootEntityId], [o].[Id], [r2].[AssociateTypeRootEntityId] -"""); - } - public override async Task Nested_associate_with_inline() { await base.Nested_associate_with_inline(); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonStructuralEqualitySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonStructuralEqualitySqliteTest.cs index 71dac1d0881..b15b2431313 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonStructuralEqualitySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonStructuralEqualitySqliteTest.cs @@ -80,30 +80,6 @@ public override async Task Nested_associate_with_inline_null() """); } - public override async Task Optional_associate_nested_associate_with_inline_null() - { - await base.Optional_associate_nested_associate_with_inline_null(); - - AssertSql( - """ -SELECT "r"."Id", "r"."Name", "r"."AssociateCollection", "r"."OptionalAssociate", "r"."RequiredAssociate" -FROM "RootEntity" AS "r" -WHERE "r"."OptionalAssociate" ->> 'OptionalNestedAssociate' IS NULL -"""); - } - - public override async Task Optional_associate_nested_associate_with_inline_not_null() - { - await base.Optional_associate_nested_associate_with_inline_not_null(); - - AssertSql( - """ -SELECT "r"."Id", "r"."Name", "r"."AssociateCollection", "r"."OptionalAssociate", "r"."RequiredAssociate" -FROM "RootEntity" AS "r" -WHERE "r"."OptionalAssociate" ->> 'OptionalNestedAssociate' IS NOT NULL -"""); - } - public override async Task Nested_associate_with_inline() { await base.Nested_associate_with_inline(); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualitySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualitySqliteTest.cs index 20158b4c4d1..049a49801d2 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualitySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualitySqliteTest.cs @@ -176,62 +176,6 @@ LEFT JOIN ( """); } - public override async Task Optional_associate_nested_associate_with_inline_null() - { - await base.Optional_associate_nested_associate_with_inline_null(); - - AssertSql( - """ -SELECT "r"."Id", "r"."Name", "o"."RootEntityId", "o0"."AssociateTypeRootEntityId", "o1"."AssociateTypeRootEntityId", "r0"."RootEntityId", "r1"."AssociateTypeRootEntityId", "r2"."AssociateTypeRootEntityId", "s"."RootEntityId", "s"."Id", "s"."Int", "s"."Ints", "s"."Name", "s"."String", "s"."AssociateTypeRootEntityId", "s"."AssociateTypeId", "s"."AssociateTypeRootEntityId0", "s"."AssociateTypeId0", "s"."AssociateTypeRootEntityId1", "s"."AssociateTypeId1", "s"."Id0", "s"."Int0", "s"."Ints0", "s"."Name0", "s"."String0", "s"."Id1", "s"."Int1", "s"."Ints1", "s"."Name1", "s"."String1", "s"."Id2", "s"."Int2", "s"."Ints2", "s"."Name2", "s"."String2", "o"."Id", "o"."Int", "o"."Ints", "o"."Name", "o"."String", "o2"."AssociateTypeRootEntityId", "o2"."Id", "o2"."Int", "o2"."Ints", "o2"."Name", "o2"."String", "o0"."Id", "o0"."Int", "o0"."Ints", "o0"."Name", "o0"."String", "o1"."Id", "o1"."Int", "o1"."Ints", "o1"."Name", "o1"."String", "r0"."Id", "r0"."Int", "r0"."Ints", "r0"."Name", "r0"."String", "r7"."AssociateTypeRootEntityId", "r7"."Id", "r7"."Int", "r7"."Ints", "r7"."Name", "r7"."String", "r1"."Id", "r1"."Int", "r1"."Ints", "r1"."Name", "r1"."String", "r2"."Id", "r2"."Int", "r2"."Ints", "r2"."Name", "r2"."String" -FROM "RootEntity" AS "r" -LEFT JOIN "OptionalRelated" AS "o" ON "r"."Id" = "o"."RootEntityId" -LEFT JOIN "OptionalRelated_OptionalNested" AS "o0" ON "o"."RootEntityId" = "o0"."AssociateTypeRootEntityId" -LEFT JOIN "OptionalRelated_RequiredNested" AS "o1" ON "o"."RootEntityId" = "o1"."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated" AS "r0" ON "r"."Id" = "r0"."RootEntityId" -LEFT JOIN "RequiredRelated_OptionalNested" AS "r1" ON "r0"."RootEntityId" = "r1"."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_RequiredNested" AS "r2" ON "r0"."RootEntityId" = "r2"."AssociateTypeRootEntityId" -LEFT JOIN ( - SELECT "r3"."RootEntityId", "r3"."Id", "r3"."Int", "r3"."Ints", "r3"."Name", "r3"."String", "r4"."AssociateTypeRootEntityId", "r4"."AssociateTypeId", "r5"."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId0", "r5"."AssociateTypeId" AS "AssociateTypeId0", "r6"."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId1", "r6"."AssociateTypeId" AS "AssociateTypeId1", "r6"."Id" AS "Id0", "r6"."Int" AS "Int0", "r6"."Ints" AS "Ints0", "r6"."Name" AS "Name0", "r6"."String" AS "String0", "r4"."Id" AS "Id1", "r4"."Int" AS "Int1", "r4"."Ints" AS "Ints1", "r4"."Name" AS "Name1", "r4"."String" AS "String1", "r5"."Id" AS "Id2", "r5"."Int" AS "Int2", "r5"."Ints" AS "Ints2", "r5"."Name" AS "Name2", "r5"."String" AS "String2" - FROM "RelatedCollection" AS "r3" - LEFT JOIN "RelatedCollection_OptionalNested" AS "r4" ON "r3"."RootEntityId" = "r4"."AssociateTypeRootEntityId" AND "r3"."Id" = "r4"."AssociateTypeId" - LEFT JOIN "RelatedCollection_RequiredNested" AS "r5" ON "r3"."RootEntityId" = "r5"."AssociateTypeRootEntityId" AND "r3"."Id" = "r5"."AssociateTypeId" - LEFT JOIN "RelatedCollection_NestedCollection" AS "r6" ON "r3"."RootEntityId" = "r6"."AssociateTypeRootEntityId" AND "r3"."Id" = "r6"."AssociateTypeId" -) AS "s" ON "r"."Id" = "s"."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS "o2" ON "o"."RootEntityId" = "o2"."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS "r7" ON "r0"."RootEntityId" = "r7"."AssociateTypeRootEntityId" -WHERE "o0"."AssociateTypeRootEntityId" IS NULL -ORDER BY "r"."Id", "o"."RootEntityId", "o0"."AssociateTypeRootEntityId", "o1"."AssociateTypeRootEntityId", "r0"."RootEntityId", "r1"."AssociateTypeRootEntityId", "r2"."AssociateTypeRootEntityId", "s"."RootEntityId", "s"."Id", "s"."AssociateTypeRootEntityId", "s"."AssociateTypeId", "s"."AssociateTypeRootEntityId0", "s"."AssociateTypeId0", "s"."AssociateTypeRootEntityId1", "s"."AssociateTypeId1", "s"."Id0", "o2"."AssociateTypeRootEntityId", "o2"."Id", "r7"."AssociateTypeRootEntityId" -"""); - } - - public override async Task Optional_associate_nested_associate_with_inline_not_null() - { - await base.Optional_associate_nested_associate_with_inline_not_null(); - - AssertSql( - """ -SELECT "r"."Id", "r"."Name", "o"."RootEntityId", "o0"."AssociateTypeRootEntityId", "o1"."AssociateTypeRootEntityId", "r0"."RootEntityId", "r1"."AssociateTypeRootEntityId", "r2"."AssociateTypeRootEntityId", "s"."RootEntityId", "s"."Id", "s"."Int", "s"."Ints", "s"."Name", "s"."String", "s"."AssociateTypeRootEntityId", "s"."AssociateTypeId", "s"."AssociateTypeRootEntityId0", "s"."AssociateTypeId0", "s"."AssociateTypeRootEntityId1", "s"."AssociateTypeId1", "s"."Id0", "s"."Int0", "s"."Ints0", "s"."Name0", "s"."String0", "s"."Id1", "s"."Int1", "s"."Ints1", "s"."Name1", "s"."String1", "s"."Id2", "s"."Int2", "s"."Ints2", "s"."Name2", "s"."String2", "o"."Id", "o"."Int", "o"."Ints", "o"."Name", "o"."String", "o2"."AssociateTypeRootEntityId", "o2"."Id", "o2"."Int", "o2"."Ints", "o2"."Name", "o2"."String", "o0"."Id", "o0"."Int", "o0"."Ints", "o0"."Name", "o0"."String", "o1"."Id", "o1"."Int", "o1"."Ints", "o1"."Name", "o1"."String", "r0"."Id", "r0"."Int", "r0"."Ints", "r0"."Name", "r0"."String", "r7"."AssociateTypeRootEntityId", "r7"."Id", "r7"."Int", "r7"."Ints", "r7"."Name", "r7"."String", "r1"."Id", "r1"."Int", "r1"."Ints", "r1"."Name", "r1"."String", "r2"."Id", "r2"."Int", "r2"."Ints", "r2"."Name", "r2"."String" -FROM "RootEntity" AS "r" -LEFT JOIN "OptionalRelated" AS "o" ON "r"."Id" = "o"."RootEntityId" -LEFT JOIN "OptionalRelated_OptionalNested" AS "o0" ON "o"."RootEntityId" = "o0"."AssociateTypeRootEntityId" -LEFT JOIN "OptionalRelated_RequiredNested" AS "o1" ON "o"."RootEntityId" = "o1"."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated" AS "r0" ON "r"."Id" = "r0"."RootEntityId" -LEFT JOIN "RequiredRelated_OptionalNested" AS "r1" ON "r0"."RootEntityId" = "r1"."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_RequiredNested" AS "r2" ON "r0"."RootEntityId" = "r2"."AssociateTypeRootEntityId" -LEFT JOIN ( - SELECT "r3"."RootEntityId", "r3"."Id", "r3"."Int", "r3"."Ints", "r3"."Name", "r3"."String", "r4"."AssociateTypeRootEntityId", "r4"."AssociateTypeId", "r5"."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId0", "r5"."AssociateTypeId" AS "AssociateTypeId0", "r6"."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId1", "r6"."AssociateTypeId" AS "AssociateTypeId1", "r6"."Id" AS "Id0", "r6"."Int" AS "Int0", "r6"."Ints" AS "Ints0", "r6"."Name" AS "Name0", "r6"."String" AS "String0", "r4"."Id" AS "Id1", "r4"."Int" AS "Int1", "r4"."Ints" AS "Ints1", "r4"."Name" AS "Name1", "r4"."String" AS "String1", "r5"."Id" AS "Id2", "r5"."Int" AS "Int2", "r5"."Ints" AS "Ints2", "r5"."Name" AS "Name2", "r5"."String" AS "String2" - FROM "RelatedCollection" AS "r3" - LEFT JOIN "RelatedCollection_OptionalNested" AS "r4" ON "r3"."RootEntityId" = "r4"."AssociateTypeRootEntityId" AND "r3"."Id" = "r4"."AssociateTypeId" - LEFT JOIN "RelatedCollection_RequiredNested" AS "r5" ON "r3"."RootEntityId" = "r5"."AssociateTypeRootEntityId" AND "r3"."Id" = "r5"."AssociateTypeId" - LEFT JOIN "RelatedCollection_NestedCollection" AS "r6" ON "r3"."RootEntityId" = "r6"."AssociateTypeRootEntityId" AND "r3"."Id" = "r6"."AssociateTypeId" -) AS "s" ON "r"."Id" = "s"."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS "o2" ON "o"."RootEntityId" = "o2"."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS "r7" ON "r0"."RootEntityId" = "r7"."AssociateTypeRootEntityId" -WHERE "o0"."AssociateTypeRootEntityId" IS NOT NULL -ORDER BY "r"."Id", "o"."RootEntityId", "o0"."AssociateTypeRootEntityId", "o1"."AssociateTypeRootEntityId", "r0"."RootEntityId", "r1"."AssociateTypeRootEntityId", "r2"."AssociateTypeRootEntityId", "s"."RootEntityId", "s"."Id", "s"."AssociateTypeRootEntityId", "s"."AssociateTypeId", "s"."AssociateTypeRootEntityId0", "s"."AssociateTypeId0", "s"."AssociateTypeRootEntityId1", "s"."AssociateTypeId1", "s"."Id0", "o2"."AssociateTypeRootEntityId", "o2"."Id", "r7"."AssociateTypeRootEntityId" -"""); - } - public override async Task Nested_associate_with_inline() { await base.Nested_associate_with_inline(); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/Associations/OwnedTableSplitting/OwnedTableSplittingStructuralEqualitySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/Associations/OwnedTableSplitting/OwnedTableSplittingStructuralEqualitySqliteTest.cs index c5946b67c16..b82ebbd2fde 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/Associations/OwnedTableSplitting/OwnedTableSplittingStructuralEqualitySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/Associations/OwnedTableSplitting/OwnedTableSplittingStructuralEqualitySqliteTest.cs @@ -142,50 +142,6 @@ LEFT JOIN "OptionalRelated_NestedCollection" AS "o" ON CASE """); } - public override async Task Optional_associate_nested_associate_with_inline_null() - { - await base.Optional_associate_nested_associate_with_inline_null(); - - AssertSql( - """ -SELECT "r"."Id", "r"."Name", "s"."RootEntityId", "s"."Id", "s"."Int", "s"."Ints", "s"."Name", "s"."String", "s"."AssociateTypeRootEntityId", "s"."AssociateTypeId", "s"."Id0", "s"."Int0", "s"."Ints0", "s"."Name0", "s"."String0", "s"."OptionalNestedAssociate_Id", "s"."OptionalNestedAssociate_Int", "s"."OptionalNestedAssociate_Ints", "s"."OptionalNestedAssociate_Name", "s"."OptionalNestedAssociate_String", "s"."RequiredNestedAssociate_Id", "s"."RequiredNestedAssociate_Int", "s"."RequiredNestedAssociate_Ints", "s"."RequiredNestedAssociate_Name", "s"."RequiredNestedAssociate_String", "r"."OptionalAssociate_Id", "r"."OptionalAssociate_Int", "r"."OptionalAssociate_Ints", "r"."OptionalAssociate_Name", "r"."OptionalAssociate_String", "o"."AssociateTypeRootEntityId", "o"."Id", "o"."Int", "o"."Ints", "o"."Name", "o"."String", "r"."OptionalAssociate_OptionalNestedAssociate_Id", "r"."OptionalAssociate_OptionalNestedAssociate_Int", "r"."OptionalAssociate_OptionalNestedAssociate_Ints", "r"."OptionalAssociate_OptionalNestedAssociate_Name", "r"."OptionalAssociate_OptionalNestedAssociate_String", "r"."OptionalAssociate_RequiredNestedAssociate_Id", "r"."OptionalAssociate_RequiredNestedAssociate_Int", "r"."OptionalAssociate_RequiredNestedAssociate_Ints", "r"."OptionalAssociate_RequiredNestedAssociate_Name", "r"."OptionalAssociate_RequiredNestedAssociate_String", "r"."RequiredAssociate_Id", "r"."RequiredAssociate_Int", "r"."RequiredAssociate_Ints", "r"."RequiredAssociate_Name", "r"."RequiredAssociate_String", "r2"."AssociateTypeRootEntityId", "r2"."Id", "r2"."Int", "r2"."Ints", "r2"."Name", "r2"."String", "r"."RequiredAssociate_OptionalNestedAssociate_Id", "r"."RequiredAssociate_OptionalNestedAssociate_Int", "r"."RequiredAssociate_OptionalNestedAssociate_Ints", "r"."RequiredAssociate_OptionalNestedAssociate_Name", "r"."RequiredAssociate_OptionalNestedAssociate_String", "r"."RequiredAssociate_RequiredNestedAssociate_Id", "r"."RequiredAssociate_RequiredNestedAssociate_Int", "r"."RequiredAssociate_RequiredNestedAssociate_Ints", "r"."RequiredAssociate_RequiredNestedAssociate_Name", "r"."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS "r" -LEFT JOIN ( - SELECT "r0"."RootEntityId", "r0"."Id", "r0"."Int", "r0"."Ints", "r0"."Name", "r0"."String", "r1"."AssociateTypeRootEntityId", "r1"."AssociateTypeId", "r1"."Id" AS "Id0", "r1"."Int" AS "Int0", "r1"."Ints" AS "Ints0", "r1"."Name" AS "Name0", "r1"."String" AS "String0", "r0"."OptionalNestedAssociate_Id", "r0"."OptionalNestedAssociate_Int", "r0"."OptionalNestedAssociate_Ints", "r0"."OptionalNestedAssociate_Name", "r0"."OptionalNestedAssociate_String", "r0"."RequiredNestedAssociate_Id", "r0"."RequiredNestedAssociate_Int", "r0"."RequiredNestedAssociate_Ints", "r0"."RequiredNestedAssociate_Name", "r0"."RequiredNestedAssociate_String" - FROM "RelatedCollection" AS "r0" - LEFT JOIN "RelatedCollection_NestedCollection" AS "r1" ON "r0"."RootEntityId" = "r1"."AssociateTypeRootEntityId" AND "r0"."Id" = "r1"."AssociateTypeId" -) AS "s" ON "r"."Id" = "s"."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS "o" ON CASE - WHEN "r"."OptionalAssociate_Id" IS NOT NULL AND "r"."OptionalAssociate_Int" IS NOT NULL AND "r"."OptionalAssociate_Ints" IS NOT NULL AND "r"."OptionalAssociate_Name" IS NOT NULL AND "r"."OptionalAssociate_String" IS NOT NULL THEN "r"."Id" -END = "o"."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS "r2" ON "r"."Id" = "r2"."AssociateTypeRootEntityId" -WHERE "r"."OptionalAssociate_OptionalNestedAssociate_Id" IS NULL OR "r"."OptionalAssociate_OptionalNestedAssociate_Int" IS NULL OR "r"."OptionalAssociate_OptionalNestedAssociate_Ints" IS NULL OR "r"."OptionalAssociate_OptionalNestedAssociate_Name" IS NULL OR "r"."OptionalAssociate_OptionalNestedAssociate_String" IS NULL -ORDER BY "r"."Id", "s"."RootEntityId", "s"."Id", "s"."AssociateTypeRootEntityId", "s"."AssociateTypeId", "s"."Id0", "o"."AssociateTypeRootEntityId", "o"."Id", "r2"."AssociateTypeRootEntityId" -"""); - } - - public override async Task Optional_associate_nested_associate_with_inline_not_null() - { - await base.Optional_associate_nested_associate_with_inline_not_null(); - - AssertSql( - """ -SELECT "r"."Id", "r"."Name", "s"."RootEntityId", "s"."Id", "s"."Int", "s"."Ints", "s"."Name", "s"."String", "s"."AssociateTypeRootEntityId", "s"."AssociateTypeId", "s"."Id0", "s"."Int0", "s"."Ints0", "s"."Name0", "s"."String0", "s"."OptionalNestedAssociate_Id", "s"."OptionalNestedAssociate_Int", "s"."OptionalNestedAssociate_Ints", "s"."OptionalNestedAssociate_Name", "s"."OptionalNestedAssociate_String", "s"."RequiredNestedAssociate_Id", "s"."RequiredNestedAssociate_Int", "s"."RequiredNestedAssociate_Ints", "s"."RequiredNestedAssociate_Name", "s"."RequiredNestedAssociate_String", "r"."OptionalAssociate_Id", "r"."OptionalAssociate_Int", "r"."OptionalAssociate_Ints", "r"."OptionalAssociate_Name", "r"."OptionalAssociate_String", "o"."AssociateTypeRootEntityId", "o"."Id", "o"."Int", "o"."Ints", "o"."Name", "o"."String", "r"."OptionalAssociate_OptionalNestedAssociate_Id", "r"."OptionalAssociate_OptionalNestedAssociate_Int", "r"."OptionalAssociate_OptionalNestedAssociate_Ints", "r"."OptionalAssociate_OptionalNestedAssociate_Name", "r"."OptionalAssociate_OptionalNestedAssociate_String", "r"."OptionalAssociate_RequiredNestedAssociate_Id", "r"."OptionalAssociate_RequiredNestedAssociate_Int", "r"."OptionalAssociate_RequiredNestedAssociate_Ints", "r"."OptionalAssociate_RequiredNestedAssociate_Name", "r"."OptionalAssociate_RequiredNestedAssociate_String", "r"."RequiredAssociate_Id", "r"."RequiredAssociate_Int", "r"."RequiredAssociate_Ints", "r"."RequiredAssociate_Name", "r"."RequiredAssociate_String", "r2"."AssociateTypeRootEntityId", "r2"."Id", "r2"."Int", "r2"."Ints", "r2"."Name", "r2"."String", "r"."RequiredAssociate_OptionalNestedAssociate_Id", "r"."RequiredAssociate_OptionalNestedAssociate_Int", "r"."RequiredAssociate_OptionalNestedAssociate_Ints", "r"."RequiredAssociate_OptionalNestedAssociate_Name", "r"."RequiredAssociate_OptionalNestedAssociate_String", "r"."RequiredAssociate_RequiredNestedAssociate_Id", "r"."RequiredAssociate_RequiredNestedAssociate_Int", "r"."RequiredAssociate_RequiredNestedAssociate_Ints", "r"."RequiredAssociate_RequiredNestedAssociate_Name", "r"."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS "r" -LEFT JOIN ( - SELECT "r0"."RootEntityId", "r0"."Id", "r0"."Int", "r0"."Ints", "r0"."Name", "r0"."String", "r1"."AssociateTypeRootEntityId", "r1"."AssociateTypeId", "r1"."Id" AS "Id0", "r1"."Int" AS "Int0", "r1"."Ints" AS "Ints0", "r1"."Name" AS "Name0", "r1"."String" AS "String0", "r0"."OptionalNestedAssociate_Id", "r0"."OptionalNestedAssociate_Int", "r0"."OptionalNestedAssociate_Ints", "r0"."OptionalNestedAssociate_Name", "r0"."OptionalNestedAssociate_String", "r0"."RequiredNestedAssociate_Id", "r0"."RequiredNestedAssociate_Int", "r0"."RequiredNestedAssociate_Ints", "r0"."RequiredNestedAssociate_Name", "r0"."RequiredNestedAssociate_String" - FROM "RelatedCollection" AS "r0" - LEFT JOIN "RelatedCollection_NestedCollection" AS "r1" ON "r0"."RootEntityId" = "r1"."AssociateTypeRootEntityId" AND "r0"."Id" = "r1"."AssociateTypeId" -) AS "s" ON "r"."Id" = "s"."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS "o" ON CASE - WHEN "r"."OptionalAssociate_Id" IS NOT NULL AND "r"."OptionalAssociate_Int" IS NOT NULL AND "r"."OptionalAssociate_Ints" IS NOT NULL AND "r"."OptionalAssociate_Name" IS NOT NULL AND "r"."OptionalAssociate_String" IS NOT NULL THEN "r"."Id" -END = "o"."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS "r2" ON "r"."Id" = "r2"."AssociateTypeRootEntityId" -WHERE "r"."OptionalAssociate_OptionalNestedAssociate_Id" IS NOT NULL AND "r"."OptionalAssociate_OptionalNestedAssociate_Int" IS NOT NULL AND "r"."OptionalAssociate_OptionalNestedAssociate_Ints" IS NOT NULL AND "r"."OptionalAssociate_OptionalNestedAssociate_Name" IS NOT NULL AND "r"."OptionalAssociate_OptionalNestedAssociate_String" IS NOT NULL -ORDER BY "r"."Id", "s"."RootEntityId", "s"."Id", "s"."AssociateTypeRootEntityId", "s"."AssociateTypeId", "s"."Id0", "o"."AssociateTypeRootEntityId", "o"."Id", "r2"."AssociateTypeRootEntityId" -"""); - } - public override async Task Nested_associate_with_inline() { await base.Nested_associate_with_inline(); From 9bc1e29cca6c4a8559e0f00ebb1ce4cb1e97fc8b Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Sat, 31 Jan 2026 16:31:56 +0100 Subject: [PATCH 07/19] Fix typo and code style --- .../CosmosQueryableMethodTranslatingExpressionVisitor.cs | 5 ++++- .../Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs index a23361e96d3..22570ad47f4 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs @@ -1521,7 +1521,10 @@ private bool TryApplyPredicate(ShapedQueryExpression source, LambdaExpression pr if (TranslateLambdaExpression(source, predicate) is { } translation) { - if (translation is not SqlConstantExpression { Value: true } && translation is not SqlUnaryExpression { OperatorType: ExpressionType.Not, Operand: SqlConstantExpression { Value: false } }) + if (translation is not SqlConstantExpression { Value: true } && + translation is not SqlUnaryExpression { + OperatorType: ExpressionType.Not, + Operand: SqlConstantExpression { Value: false } }) { select.ApplyPredicate(translation); } diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs index 41880efc125..fc69994f10e 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs @@ -1082,7 +1082,7 @@ private bool TryRewriteEntityEquality( { if (entityType.IsDocumentRoot() && entityReference.Subquery == null) { - // Document root can never be be null + // Document root can never be null result = Visit(Expression.Constant(nodeType != ExpressionType.Equal)); return true; } From 2bcb90f24540a0d21a3415f6277717a46cb62cc8 Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Sat, 31 Jan 2026 16:44:14 +0100 Subject: [PATCH 08/19] Update styling and small changes --- .../CosmosSqlTranslatingExpressionVisitor.cs | 21 +++++++++++-------- .../Expressions/SqlObjectAccessExpression.cs | 6 ++++-- ...NavigationsStructuralEqualityCosmosTest.cs | 4 ++-- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs index fc69994f10e..c6a49212a9b 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs @@ -5,7 +5,6 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.EntityFrameworkCore.Cosmos.Internal; using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; -using Microsoft.EntityFrameworkCore.Cosmos.Query.Internal.Expressions; using Microsoft.EntityFrameworkCore.Internal; using static Microsoft.EntityFrameworkCore.Infrastructure.ExpressionExtensions; @@ -1089,20 +1088,24 @@ private bool TryRewriteEntityEquality( // Treat type as object for null comparison var access = new SqlObjectAccessExpression(entityReference.Object); - result = sqlExpressionFactory.MakeBinary(nodeType, access, sqlExpressionFactory.Constant(null, typeof(object))!, typeMappingSource.FindMapping(typeof(bool)))!; + result = sqlExpressionFactory.MakeBinary( + nodeType, + access, + sqlExpressionFactory.Constant(null, typeof(object))!, + typeMappingSource.FindMapping(typeof(bool)))!; return true; } if (entityType.FindPrimaryKey()?.Properties is not { } primaryKeyProperties) { throw new InvalidOperationException( - CoreStrings.EntityEqualityOnKeylessEntityNotSupported( - nodeType == ExpressionType.Equal - ? equalsMethod ? nameof(object.Equals) : "==" - : equalsMethod - ? "!" + nameof(object.Equals) - : "!=", - entityType.DisplayName())); + CoreStrings.EntityEqualityOnKeylessEntityNotSupported( + nodeType == ExpressionType.Equal + ? equalsMethod ? nameof(object.Equals) : "==" + : equalsMethod + ? "!" + nameof(object.Equals) + : "!=", + entityType.DisplayName())); } if (compareReference is EntityReferenceExpression compareEntityReference) diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/SqlObjectAccessExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/SqlObjectAccessExpression.cs index 4fe2f8c54ac..88ece88d3e7 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/SqlObjectAccessExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/SqlObjectAccessExpression.cs @@ -3,10 +3,12 @@ using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; -namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal.Expressions; +// ReSharper disable once CheckNamespace + +namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; /// -/// Represents an structural type object access on a CosmosJSON object +/// Represents a structural type object access that represents a CosmosJSON object /// /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualityCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualityCosmosTest.cs index bed53ec3287..2af99482851 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualityCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualityCosmosTest.cs @@ -53,7 +53,7 @@ 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) @@ -68,7 +68,7 @@ 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) From 269770b8fcf74d63eff4353073d1210b0a389279 Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Mon, 2 Feb 2026 12:22:06 +0100 Subject: [PATCH 09/19] Use old code pattern --- .../CosmosSqlTranslatingExpressionVisitor.cs | 63 +++++++++++-------- .../OwnedNavigationsCollectionCosmosTest.cs | 13 ++++ ...NavigationsStructuralEqualityCosmosTest.cs | 17 +++++ 3 files changed, 66 insertions(+), 27 deletions(-) diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs index c6a49212a9b..d9217a59d2e 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs @@ -3,8 +3,10 @@ using System.Collections; using System.Diagnostics.CodeAnalysis; +using System.Security.Principal; using Microsoft.EntityFrameworkCore.Cosmos.Internal; using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; using Microsoft.EntityFrameworkCore.Internal; using static Microsoft.EntityFrameworkCore.Infrastructure.ExpressionExtensions; @@ -27,8 +29,8 @@ public class CosmosSqlTranslatingExpressionVisitor( { private const string RuntimeParameterPrefix = "entity_equality_"; - private static readonly MethodInfo ParameterPropertyValueExtractorMethod = - typeof(CosmosSqlTranslatingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(ParameterPropertyValueExtractor))!; + private static readonly MethodInfo ParameterValueExtractorMethod = + typeof(CosmosSqlTranslatingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(ParameterValueExtractor))!; private static readonly MethodInfo ParameterListValueExtractorMethod = typeof(CosmosSqlTranslatingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(ParameterListValueExtractor))!; @@ -1066,28 +1068,31 @@ private bool TryRewriteEntityEquality( bool equalsMethod, [NotNullWhen(true)] out Expression? result) { - var entityReference = left as EntityReferenceExpression ?? right as EntityReferenceExpression; - if (entityReference == null) + var leftEntityReference = left as EntityReferenceExpression; + var rightEntityReference = right as EntityReferenceExpression; + + if (leftEntityReference == null + && rightEntityReference == null) { result = null; return false; } - var entityType = entityReference.EntityType; - var compareReference = entityReference == left ? right : left; - - // Null equality - if (IsNullSqlConstantExpression(compareReference)) + if (IsNullSqlConstantExpression(left) + || IsNullSqlConstantExpression(right)) { - if (entityType.IsDocumentRoot() && entityReference.Subquery == null) + var nonNullEntityReference = (IsNullSqlConstantExpression(left) ? rightEntityReference : leftEntityReference)!; + var entityType1 = nonNullEntityReference.EntityType; + + var shaper = nonNullEntityReference.Parameter ?? (StructuralTypeShaperExpression)(nonNullEntityReference.Subquery ?? throw new UnreachableException()).ShaperExpression; + + if (entityType1.IsDocumentRoot() || !shaper.IsNullable) { - // Document root can never be null result = Visit(Expression.Constant(nodeType != ExpressionType.Equal)); return true; } - // Treat type as object for null comparison - var access = new SqlObjectAccessExpression(entityReference.Object); + var access = new SqlObjectAccessExpression(Visit(shaper.ValueBufferExpression)); result = sqlExpressionFactory.MakeBinary( nodeType, access, @@ -1096,7 +1101,22 @@ private bool TryRewriteEntityEquality( return true; } - if (entityType.FindPrimaryKey()?.Properties is not { } primaryKeyProperties) + var leftEntityType = leftEntityReference?.EntityType; + var rightEntityType = rightEntityReference?.EntityType; + var entityType = leftEntityType ?? rightEntityType; + + Check.DebugAssert(entityType != null, "At least either side should be entityReference so entityType should be non-null."); + + if (leftEntityType != null + && rightEntityType != null + && leftEntityType.GetRootType() != rightEntityType.GetRootType()) + { + result = sqlExpressionFactory.Constant(false); + return true; + } + + var primaryKeyProperties = entityType.FindPrimaryKey()?.Properties; + if (primaryKeyProperties == null) { throw new InvalidOperationException( CoreStrings.EntityEqualityOnKeylessEntityNotSupported( @@ -1108,16 +1128,6 @@ private bool TryRewriteEntityEquality( entityType.DisplayName())); } - if (compareReference is EntityReferenceExpression compareEntityReference) - { - // Comparing of 2 different entity types is always false. - if (entityType.GetRootType() != compareEntityReference.EntityType.GetRootType()) - { - result = Visit(Expression.Constant(false)); - return true; - } - } - result = Visit( primaryKeyProperties.Select(p => Expression.MakeBinary( @@ -1142,7 +1152,7 @@ private Expression CreatePropertyAccessExpression(Expression target, IProperty p case SqlParameterExpression sqlParameterExpression: var lambda = Expression.Lambda( Expression.Call( - ParameterPropertyValueExtractorMethod.MakeGenericMethod(property.ClrType.MakeNullable()), + ParameterValueExtractorMethod.MakeGenericMethod(property.ClrType.MakeNullable()), QueryCompilationContext.QueryContextParameter, Expression.Constant(sqlParameterExpression.Name, typeof(string)), Expression.Constant(property, typeof(IProperty))), @@ -1162,7 +1172,7 @@ when memberInitExpression.Bindings.SingleOrDefault(mb => mb.Member.Name == prope } } - private static T? ParameterPropertyValueExtractor(QueryContext context, string baseParameterName, IProperty property) + private static T? ParameterValueExtractor(QueryContext context, string baseParameterName, IProperty property) { var baseParameter = context.Parameters[baseParameterName]; return baseParameter == null ? (T?)(object?)null : (T?)property.GetGetter().GetClrValue(baseParameter); @@ -1250,7 +1260,6 @@ private EntityReferenceExpression(EntityReferenceExpression typeReference, IType EntityType = (IEntityType)structuralType; } - public Expression Object => (Expression?)Parameter ?? Subquery ?? throw new UnreachableException(); public new StructuralTypeShaperExpression? Parameter { get; } public ShapedQueryExpression? Subquery { get; } public IEntityType EntityType { get; } 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 2af99482851..7db2fb379e3 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualityCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualityCosmosTest.cs @@ -117,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. From d58c1f22f419b349ba078053ade1176fbf676af1 Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Mon, 2 Feb 2026 12:33:47 +0100 Subject: [PATCH 10/19] (ab)use ScalarReferenceExpression --- .../Query/Internal/CosmosAliasManager.cs | 2 +- .../Query/Internal/CosmosQuerySqlGenerator.cs | 13 ++- .../CosmosSqlTranslatingExpressionVisitor.cs | 4 +- .../Expressions/ScalarReferenceExpression.cs | 70 ++++++++++++-- .../Expressions/SqlObjectAccessExpression.cs | 92 ------------------- 5 files changed, 75 insertions(+), 106 deletions(-) delete mode 100644 src/EFCore.Cosmos/Query/Internal/Expressions/SqlObjectAccessExpression.cs diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosAliasManager.cs b/src/EFCore.Cosmos/Query/Internal/CosmosAliasManager.cs index 6227029eae3..42be8480633 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosAliasManager.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosAliasManager.cs @@ -223,7 +223,7 @@ protected override Expression VisitExtension(Expression node) SourceExpression { Alias: { } alias } source when aliasRewritingMap.TryGetValue(alias, out var newAlias) => base.VisitExtension(new SourceExpression(source.Expression, newAlias, source.WithIn)), - ScalarReferenceExpression reference when aliasRewritingMap.TryGetValue(reference.Name, out var newAlias) + ScalarReferenceExpression reference when reference.Name != null && aliasRewritingMap.TryGetValue(reference.Name, out var newAlias) => new ScalarReferenceExpression(newAlias, reference.Type, reference.TypeMapping), ObjectReferenceExpression reference when aliasRewritingMap.TryGetValue(reference.Name, out var newAlias) => new ObjectReferenceExpression(reference.EntityType, newAlias), diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs index 8ac1a8dcaa9..374c9b0fbd7 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs @@ -284,7 +284,18 @@ protected override Expression VisitObjectReference(ObjectReferenceExpression obj /// protected override Expression VisitValueReference(ScalarReferenceExpression scalarReferenceExpression) { - _sqlBuilder.Append(scalarReferenceExpression.Name); + if (scalarReferenceExpression.Object != null) + { + Visit(scalarReferenceExpression.Object); + } + else if (scalarReferenceExpression.Name != null) + { + _sqlBuilder.Append(scalarReferenceExpression.Name); + } + else + { + throw new UnreachableException(); + } return scalarReferenceExpression; } diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs index d9217a59d2e..56c053e24c2 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs @@ -3,10 +3,8 @@ using System.Collections; using System.Diagnostics.CodeAnalysis; -using System.Security.Principal; using Microsoft.EntityFrameworkCore.Cosmos.Internal; using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; -using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; using Microsoft.EntityFrameworkCore.Internal; using static Microsoft.EntityFrameworkCore.Infrastructure.ExpressionExtensions; @@ -1092,7 +1090,7 @@ private bool TryRewriteEntityEquality( return true; } - var access = new SqlObjectAccessExpression(Visit(shaper.ValueBufferExpression)); + var access = new ScalarReferenceExpression(Visit(shaper.ValueBufferExpression)); result = sqlExpressionFactory.MakeBinary( nodeType, access, diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/ScalarReferenceExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/ScalarReferenceExpression.cs index e11286240fd..6ada28ad58a 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/ScalarReferenceExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/ScalarReferenceExpression.cs @@ -3,6 +3,8 @@ // ReSharper disable once CheckNamespace +using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; + namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; /// @@ -15,8 +17,7 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; /// 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 class ScalarReferenceExpression(string name, Type clrType, CoreTypeMapping? typeMapping = null) - : SqlExpression(clrType, typeMapping), IAccessExpression +public class ScalarReferenceExpression : SqlExpression, IAccessExpression { /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -24,7 +25,21 @@ public class ScalarReferenceExpression(string name, Type clrType, CoreTypeMappin /// 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 string Name { get; } = name; + public ScalarReferenceExpression(string name, Type clrType, CoreTypeMapping? typeMapping = null) : base(clrType, typeMapping) + { + Name = name; + } + + /// + /// 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. + /// + public ScalarReferenceExpression(Expression @object) : base(typeof(object), CosmosTypeMapping.Default) + { + Object = @object; + } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -32,7 +47,23 @@ public class ScalarReferenceExpression(string name, Type clrType, CoreTypeMappin /// 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. /// - string IAccessExpression.PropertyName + public virtual Expression? Object { get; } + + /// + /// 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. + /// + public virtual string? Name { get; } + + /// + /// 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. + /// + string? IAccessExpression.PropertyName => Name; /// @@ -42,7 +73,18 @@ string IAccessExpression.PropertyName /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override Expression VisitChildren(ExpressionVisitor visitor) - => this; + => Object != null ? Update(visitor.Visit(Object)) : this; + + /// + /// 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. + /// + public virtual ScalarReferenceExpression Update(Expression @object) + => ReferenceEquals(@object, Object) + ? this + : new ScalarReferenceExpression(@object); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -51,7 +93,7 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public override string ToString() - => Name; + => Name ?? base.ToString(); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -60,7 +102,17 @@ public override string ToString() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override void Print(ExpressionPrinter expressionPrinter) - => expressionPrinter.Append(Name); + { + if (Name != null) + { + expressionPrinter.Append(Name); + } + else + { + expressionPrinter.Visit(Object!); + } + } + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -72,9 +124,9 @@ public override bool Equals(object? obj) => obj is ScalarReferenceExpression other && Equals(other); private bool Equals(ScalarReferenceExpression other) - => ReferenceEquals(this, other) || (base.Equals(other) && Name == other.Name); + => ReferenceEquals(this, other) || (base.Equals(other) && Name == other.Name && Object == other.Object); /// public override int GetHashCode() - => HashCode.Combine(base.GetHashCode(), Name); + => HashCode.Combine(base.GetHashCode(), Name, Object); } diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/SqlObjectAccessExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/SqlObjectAccessExpression.cs deleted file mode 100644 index 88ece88d3e7..00000000000 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/SqlObjectAccessExpression.cs +++ /dev/null @@ -1,92 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; - -// ReSharper disable once CheckNamespace - -namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; - -/// -/// Represents a structural type object access that represents a CosmosJSON object -/// -/// -/// 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. -/// -[DebuggerDisplay("{Microsoft.EntityFrameworkCore.Query.ExpressionPrinter.Print(this), nq}")] -public class SqlObjectAccessExpression(Expression @object) - : SqlExpression(typeof(object), CosmosTypeMapping.Default), IAccessExpression -{ - /// - /// 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. - /// - public virtual Expression Object { get; } = @object; - - /// - /// 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. - /// - public virtual string? PropertyName => null; - - /// - /// 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. - /// - protected override Expression VisitChildren(ExpressionVisitor visitor) - => Update(visitor.Visit(Object)); - - /// - /// 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. - /// - public virtual SqlObjectAccessExpression Update(Expression @object) - => ReferenceEquals(@object, Object) - ? this - : new SqlObjectAccessExpression(@object); - - /// - /// 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. - /// - protected override void Print(ExpressionPrinter expressionPrinter) - => expressionPrinter.Visit(Object); - - /// - /// 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. - /// - public override bool Equals(object? obj) - => obj != null - && (ReferenceEquals(this, obj) - || obj is SqlObjectAccessExpression expression - && Equals(expression)); - - private bool Equals(SqlObjectAccessExpression expression) - => base.Equals(expression) - && Object.Equals(expression.Object); - - /// - /// 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. - /// - public override int GetHashCode() - => HashCode.Combine(base.GetHashCode(), Object); -} From f6f33da026a86dacd9ac0451f12418c4f41fb305 Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Mon, 2 Feb 2026 12:42:29 +0100 Subject: [PATCH 11/19] Improve not false detenction --- .../CosmosQueryableMethodTranslatingExpressionVisitor.cs | 5 +---- .../Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs | 5 +++++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs index 22570ad47f4..3f826412a13 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs @@ -1521,10 +1521,7 @@ private bool TryApplyPredicate(ShapedQueryExpression source, LambdaExpression pr if (TranslateLambdaExpression(source, predicate) is { } translation) { - if (translation is not SqlConstantExpression { Value: true } && - translation is not SqlUnaryExpression { - OperatorType: ExpressionType.Not, - Operand: SqlConstantExpression { Value: false } }) + if (translation is not SqlConstantExpression { Value: true }) { select.ApplyPredicate(translation); } diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs index 56c053e24c2..576bccb5cf9 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs @@ -803,6 +803,11 @@ protected override Expression VisitUnary(UnaryExpression unaryExpression) { var operand = Visit(unaryExpression.Operand); + if (operand is SqlConstantExpression { Value: false } && unaryExpression.NodeType == ExpressionType.Not) + { + return sqlExpressionFactory.Constant(true); + } + if (operand is EntityReferenceExpression entityReferenceExpression && unaryExpression.NodeType is ExpressionType.Convert or ExpressionType.ConvertChecked or ExpressionType.TypeAs) { From ec6f6785cc28dd82c7a1138592c4f4ae7a212e05 Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Mon, 2 Feb 2026 12:44:58 +0100 Subject: [PATCH 12/19] Only use IsNullable --- .../Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs index 576bccb5cf9..dc44e5b9294 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs @@ -1089,7 +1089,7 @@ private bool TryRewriteEntityEquality( var shaper = nonNullEntityReference.Parameter ?? (StructuralTypeShaperExpression)(nonNullEntityReference.Subquery ?? throw new UnreachableException()).ShaperExpression; - if (entityType1.IsDocumentRoot() || !shaper.IsNullable) + if (!shaper.IsNullable) { result = Visit(Expression.Constant(nodeType != ExpressionType.Equal)); return true; From fd0f1aaa0c6da1fdefa24a73dfb6790de0de525c Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Mon, 2 Feb 2026 16:33:16 +0100 Subject: [PATCH 13/19] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs index dc44e5b9294..66c028733c8 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs @@ -5,7 +5,7 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.EntityFrameworkCore.Cosmos.Internal; using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; -using Microsoft.EntityFrameworkCore.Internal; + using static Microsoft.EntityFrameworkCore.Infrastructure.ExpressionExtensions; namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; @@ -1085,7 +1085,7 @@ private bool TryRewriteEntityEquality( || IsNullSqlConstantExpression(right)) { var nonNullEntityReference = (IsNullSqlConstantExpression(left) ? rightEntityReference : leftEntityReference)!; - var entityType1 = nonNullEntityReference.EntityType; + var shaper = nonNullEntityReference.Parameter ?? (StructuralTypeShaperExpression)(nonNullEntityReference.Subquery ?? throw new UnreachableException()).ShaperExpression; From 069cd7c5ec3d2ec34170578fc1300073aeb33a4a Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Mon, 2 Feb 2026 16:41:33 +0100 Subject: [PATCH 14/19] That's what you get for trusting copilot --- .../Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs | 5 +---- .../CosmosTransactionalBatchTest.cs | 1 + 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs index 66c028733c8..f6bab24bb8a 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs @@ -4,8 +4,7 @@ using System.Collections; using System.Diagnostics.CodeAnalysis; using Microsoft.EntityFrameworkCore.Cosmos.Internal; -using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; - +using Microsoft.EntityFrameworkCore.Internal; using static Microsoft.EntityFrameworkCore.Infrastructure.ExpressionExtensions; namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; @@ -1085,8 +1084,6 @@ private bool TryRewriteEntityEquality( || IsNullSqlConstantExpression(right)) { var nonNullEntityReference = (IsNullSqlConstantExpression(left) ? rightEntityReference : leftEntityReference)!; - - var shaper = nonNullEntityReference.Parameter ?? (StructuralTypeShaperExpression)(nonNullEntityReference.Subquery ?? throw new UnreachableException()).ShaperExpression; if (!shaper.IsNullable) 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() { From f4b9bd85086fc5c29e7711e64e0863766761b713 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Mon, 2 Feb 2026 20:56:05 +0100 Subject: [PATCH 15/19] Make SqlBinaryExpression constructible over non-Sql expressions --- .../Query/Internal/CosmosAliasManager.cs | 2 +- .../Query/Internal/CosmosQuerySqlGenerator.cs | 13 +--- ...CosmosReadItemAndPartitionKeysExtractor.cs | 2 +- .../CosmosSqlTranslatingExpressionVisitor.cs | 13 ++-- ...eConverterCompensatingExpressionVisitor.cs | 11 ++- .../Expressions/ScalarReferenceExpression.cs | 70 +++---------------- .../Expressions/SqlBinaryExpression.cs | 16 ++--- .../Query/Internal/SqlExpressionFactory.cs | 4 +- 8 files changed, 35 insertions(+), 96 deletions(-) diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosAliasManager.cs b/src/EFCore.Cosmos/Query/Internal/CosmosAliasManager.cs index 42be8480633..6227029eae3 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosAliasManager.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosAliasManager.cs @@ -223,7 +223,7 @@ protected override Expression VisitExtension(Expression node) SourceExpression { Alias: { } alias } source when aliasRewritingMap.TryGetValue(alias, out var newAlias) => base.VisitExtension(new SourceExpression(source.Expression, newAlias, source.WithIn)), - ScalarReferenceExpression reference when reference.Name != null && aliasRewritingMap.TryGetValue(reference.Name, out var newAlias) + ScalarReferenceExpression reference when aliasRewritingMap.TryGetValue(reference.Name, out var newAlias) => new ScalarReferenceExpression(newAlias, reference.Type, reference.TypeMapping), ObjectReferenceExpression reference when aliasRewritingMap.TryGetValue(reference.Name, out var newAlias) => new ObjectReferenceExpression(reference.EntityType, newAlias), diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs index 374c9b0fbd7..8ac1a8dcaa9 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs @@ -284,18 +284,7 @@ protected override Expression VisitObjectReference(ObjectReferenceExpression obj /// protected override Expression VisitValueReference(ScalarReferenceExpression scalarReferenceExpression) { - if (scalarReferenceExpression.Object != null) - { - Visit(scalarReferenceExpression.Object); - } - else if (scalarReferenceExpression.Name != null) - { - _sqlBuilder.Append(scalarReferenceExpression.Name); - } - else - { - throw new UnreachableException(); - } + _sqlBuilder.Append(scalarReferenceExpression.Name); return scalarReferenceExpression; } 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 f6bab24bb8a..a8022777776 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs @@ -802,11 +802,6 @@ protected override Expression VisitUnary(UnaryExpression unaryExpression) { var operand = Visit(unaryExpression.Operand); - if (operand is SqlConstantExpression { Value: false } && unaryExpression.NodeType == ExpressionType.Not) - { - return sqlExpressionFactory.Constant(true); - } - if (operand is EntityReferenceExpression entityReferenceExpression && unaryExpression.NodeType is ExpressionType.Convert or ExpressionType.ConvertChecked or ExpressionType.TypeAs) { @@ -1084,7 +1079,8 @@ private bool TryRewriteEntityEquality( || IsNullSqlConstantExpression(right)) { var nonNullEntityReference = (IsNullSqlConstantExpression(left) ? rightEntityReference : leftEntityReference)!; - var shaper = nonNullEntityReference.Parameter ?? (StructuralTypeShaperExpression)(nonNullEntityReference.Subquery ?? throw new UnreachableException()).ShaperExpression; + var shaper = nonNullEntityReference.Parameter + ?? (StructuralTypeShaperExpression)nonNullEntityReference.Subquery!.ShaperExpression; if (!shaper.IsNullable) { @@ -1092,11 +1088,12 @@ private bool TryRewriteEntityEquality( return true; } - var access = new ScalarReferenceExpression(Visit(shaper.ValueBufferExpression)); - result = sqlExpressionFactory.MakeBinary( + var access = Visit(shaper.ValueBufferExpression); + result = new SqlBinaryExpression( nodeType, access, sqlExpressionFactory.Constant(null, typeof(object))!, + 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/ScalarReferenceExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/ScalarReferenceExpression.cs index 6ada28ad58a..e11286240fd 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/ScalarReferenceExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/ScalarReferenceExpression.cs @@ -3,8 +3,6 @@ // ReSharper disable once CheckNamespace -using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; - namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; /// @@ -17,7 +15,8 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; /// 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 class ScalarReferenceExpression : SqlExpression, IAccessExpression +public class ScalarReferenceExpression(string name, Type clrType, CoreTypeMapping? typeMapping = null) + : SqlExpression(clrType, typeMapping), IAccessExpression { /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -25,21 +24,7 @@ public class ScalarReferenceExpression : SqlExpression, IAccessExpression /// 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 ScalarReferenceExpression(string name, Type clrType, CoreTypeMapping? typeMapping = null) : base(clrType, typeMapping) - { - Name = name; - } - - /// - /// 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. - /// - public ScalarReferenceExpression(Expression @object) : base(typeof(object), CosmosTypeMapping.Default) - { - Object = @object; - } + public virtual string Name { get; } = name; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -47,23 +32,7 @@ public ScalarReferenceExpression(Expression @object) : base(typeof(object), Cosm /// 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 Expression? Object { get; } - - /// - /// 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. - /// - public virtual string? Name { get; } - - /// - /// 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. - /// - string? IAccessExpression.PropertyName + string IAccessExpression.PropertyName => Name; /// @@ -73,18 +42,7 @@ public ScalarReferenceExpression(Expression @object) : base(typeof(object), Cosm /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override Expression VisitChildren(ExpressionVisitor visitor) - => Object != null ? Update(visitor.Visit(Object)) : this; - - /// - /// 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. - /// - public virtual ScalarReferenceExpression Update(Expression @object) - => ReferenceEquals(@object, Object) - ? this - : new ScalarReferenceExpression(@object); + => this; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -93,7 +51,7 @@ public virtual ScalarReferenceExpression Update(Expression @object) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public override string ToString() - => Name ?? base.ToString(); + => Name; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -102,17 +60,7 @@ public override string ToString() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override void Print(ExpressionPrinter expressionPrinter) - { - if (Name != null) - { - expressionPrinter.Append(Name); - } - else - { - expressionPrinter.Visit(Object!); - } - } - + => expressionPrinter.Append(Name); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -124,9 +72,9 @@ public override bool Equals(object? obj) => obj is ScalarReferenceExpression other && Equals(other); private bool Equals(ScalarReferenceExpression other) - => ReferenceEquals(this, other) || (base.Equals(other) && Name == other.Name && Object == other.Object); + => ReferenceEquals(this, other) || (base.Equals(other) && Name == other.Name); /// public override int GetHashCode() - => HashCode.Combine(base.GetHashCode(), Name, Object); + => HashCode.Combine(base.GetHashCode(), Name); } 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; From 2a9d34a6ca29e8fecfa970e1494fdab45ac0735b Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Tue, 3 Feb 2026 10:11:19 +0100 Subject: [PATCH 16/19] Readd !false optimization --- .../Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs index 5779f70c141..99d68c8d6b6 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs @@ -816,7 +816,7 @@ protected override Expression VisitUnary(UnaryExpression unaryExpression) return unaryExpression.NodeType switch { ExpressionType.Not - => sqlExpressionFactory.Not(sqlOperand!), + => operand is SqlConstantExpression { Value: false } ? sqlExpressionFactory.Constant(true) : sqlExpressionFactory.Not(sqlOperand!), ExpressionType.Negate or ExpressionType.NegateChecked => sqlExpressionFactory.Negate(sqlOperand!), From 9bf09f3df0c1854c050f4fb5917d26a7e43e424e Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Tue, 3 Feb 2026 10:19:04 +0100 Subject: [PATCH 17/19] Assign type mapping --- .../Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs index 99d68c8d6b6..9b9addbc927 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; @@ -1092,7 +1093,7 @@ private bool TryRewriteEntityEquality( result = new SqlBinaryExpression( nodeType, access, - sqlExpressionFactory.Constant(null, typeof(object))!, + sqlExpressionFactory.Constant(null, typeof(object), CosmosTypeMapping.Default)!, typeof(bool), typeMappingSource.FindMapping(typeof(bool)))!; return true; From a47c8327610961967a168b867e075fd2429707f2 Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Tue, 3 Feb 2026 11:20:58 +0100 Subject: [PATCH 18/19] Move to pattern matching in switch and handle !true --- .../CosmosSqlTranslatingExpressionVisitor.cs | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs index 9b9addbc927..2001f294c1e 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs @@ -814,28 +814,32 @@ protected override Expression VisitUnary(UnaryExpression unaryExpression) return QueryCompilationContext.NotTranslatedExpression; } - return unaryExpression.NodeType switch + return unaryExpression switch { - ExpressionType.Not - => operand is SqlConstantExpression { Value: false } ? sqlExpressionFactory.Constant(true) : sqlExpressionFactory.Not(sqlOperand!), - - ExpressionType.Negate or ExpressionType.NegateChecked + { NodeType: ExpressionType.Not, Operand: SqlConstantExpression { Value: false } } + => sqlExpressionFactory.Constant(true), + { NodeType: ExpressionType.Not, Operand: SqlConstantExpression { Value: true } } + => sqlExpressionFactory.Constant(false), + { NodeType: ExpressionType.Not } + => sqlExpressionFactory.Not(sqlOperand!), + + { NodeType: ExpressionType.Negate or ExpressionType.NegateChecked } => sqlExpressionFactory.Negate(sqlOperand!), // Convert nodes can be an explicit user gesture in the query, or they may get introduced by the compiler (e.g. when a Child is // passed as an argument for a parameter of type Parent). The latter type should generally get stripped out as a pure C#/LINQ // artifact that shouldn't affect translation, but the latter may be an indication from the user that they want to apply a // type change. - ExpressionType.Convert or ExpressionType.ConvertChecked or ExpressionType.TypeAs - when operand.Type.IsInterface && unaryExpression.Type.GetInterfaces().Any(e => e == operand.Type) - // We strip out implicit conversions, e.g. float[] -> ReadOnlyMemory (for vector search) - || (unaryExpression.Method is { IsSpecialName: true, Name: "op_Implicit" } - && IsReadOnlyMemory(unaryExpression.Type.UnwrapNullableType())) - || unaryExpression.Type.UnwrapNullableType() == operand.Type - || unaryExpression.Type.UnwrapNullableType() == typeof(Enum) - // Object convert needs to be converted to explicit cast when mismatching types - // But we let it pass here since we don't have explicit cast mechanism here and in some cases object convert is due to value types - || unaryExpression.Type == typeof(object) + { NodeType: ExpressionType.Convert or ExpressionType.ConvertChecked or ExpressionType.TypeAs } + when unaryExpression.Operand.Type.IsInterface && unaryExpression.Type.GetInterfaces().Any(e => e == operand.Type) + // We strip out implicit conversions, e.g. float[] -> ReadOnlyMemory (for vector search) + || (unaryExpression.Method is { IsSpecialName: true, Name: "op_Implicit" } + && IsReadOnlyMemory(unaryExpression.Type.UnwrapNullableType())) + || unaryExpression.Type.UnwrapNullableType() == operand.Type + || unaryExpression.Type.UnwrapNullableType() == typeof(Enum) + // Object convert needs to be converted to explicit cast when mismatching types + // But we let it pass here since we don't have explicit cast mechanism here and in some cases object convert is due to value types + || unaryExpression.Type == typeof(object) => sqlOperand!, _ => QueryCompilationContext.NotTranslatedExpression From 0e00b4c6dd5fddf7d474d6da16e452a00d5f2f73 Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Tue, 3 Feb 2026 11:30:56 +0100 Subject: [PATCH 19/19] Move back to unaryExpression.NodeType switch --- .../CosmosSqlTranslatingExpressionVisitor.cs | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs index 2001f294c1e..20992155bf3 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs @@ -814,32 +814,30 @@ protected override Expression VisitUnary(UnaryExpression unaryExpression) return QueryCompilationContext.NotTranslatedExpression; } - return unaryExpression switch + return unaryExpression.NodeType switch { - { NodeType: ExpressionType.Not, Operand: SqlConstantExpression { Value: false } } - => sqlExpressionFactory.Constant(true), - { NodeType: ExpressionType.Not, Operand: SqlConstantExpression { Value: true } } - => sqlExpressionFactory.Constant(false), - { NodeType: ExpressionType.Not } + ExpressionType.Not when operand is SqlConstantExpression { Value: bool boolValue } + => sqlExpressionFactory.Constant(!boolValue), + ExpressionType.Not => sqlExpressionFactory.Not(sqlOperand!), - { NodeType: ExpressionType.Negate or ExpressionType.NegateChecked } + ExpressionType.Negate or ExpressionType.NegateChecked => sqlExpressionFactory.Negate(sqlOperand!), // Convert nodes can be an explicit user gesture in the query, or they may get introduced by the compiler (e.g. when a Child is // passed as an argument for a parameter of type Parent). The latter type should generally get stripped out as a pure C#/LINQ // artifact that shouldn't affect translation, but the latter may be an indication from the user that they want to apply a // type change. - { NodeType: ExpressionType.Convert or ExpressionType.ConvertChecked or ExpressionType.TypeAs } - when unaryExpression.Operand.Type.IsInterface && unaryExpression.Type.GetInterfaces().Any(e => e == operand.Type) - // We strip out implicit conversions, e.g. float[] -> ReadOnlyMemory (for vector search) - || (unaryExpression.Method is { IsSpecialName: true, Name: "op_Implicit" } - && IsReadOnlyMemory(unaryExpression.Type.UnwrapNullableType())) - || unaryExpression.Type.UnwrapNullableType() == operand.Type - || unaryExpression.Type.UnwrapNullableType() == typeof(Enum) - // Object convert needs to be converted to explicit cast when mismatching types - // But we let it pass here since we don't have explicit cast mechanism here and in some cases object convert is due to value types - || unaryExpression.Type == typeof(object) + ExpressionType.Convert or ExpressionType.ConvertChecked or ExpressionType.TypeAs + when operand.Type.IsInterface && unaryExpression.Type.GetInterfaces().Any(e => e == operand.Type) + // We strip out implicit conversions, e.g. float[] -> ReadOnlyMemory (for vector search) + || (unaryExpression.Method is { IsSpecialName: true, Name: "op_Implicit" } + && IsReadOnlyMemory(unaryExpression.Type.UnwrapNullableType())) + || unaryExpression.Type.UnwrapNullableType() == operand.Type + || unaryExpression.Type.UnwrapNullableType() == typeof(Enum) + // Object convert needs to be converted to explicit cast when mismatching types + // But we let it pass here since we don't have explicit cast mechanism here and in some cases object convert is due to value types + || unaryExpression.Type == typeof(object) => sqlOperand!, _ => QueryCompilationContext.NotTranslatedExpression